mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 初步测试自动过盾,添加适配器模型管理
This commit is contained in:
@@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### ✨ Added
|
||||
- **适配器描述**
|
||||
- 为每个适配器添加描述,可以在 WebUI 中的适配器设置页面点击查看每个适配器的描述和使用方法。
|
||||
- **适配器模型管理**
|
||||
- 为每个适配器添加模型列表管理,支持黑名单和白名单,可用于禁用网站出现问题的模型
|
||||
- **调试适配器**
|
||||
- 多种检测网站聚合,IP 纯净度查询等,并初步测试自动过盾
|
||||
|
||||
## [3.4.3] - 2025-12-26
|
||||
|
||||
|
||||
@@ -122,6 +122,13 @@ backend:
|
||||
lmarena:
|
||||
# 开启后直接返回图片 URL (但其他不支持该选项的适配器仍然会返回 Base64)
|
||||
returnUrl: false
|
||||
# 该适配器的模型黑白名单 (每个适配器都可以使用该功能,配置上级为适配器ID,推荐使用 WebUI 修改)
|
||||
# modelFilter:
|
||||
# mode: whitelist # 白名单whitelist 黑名单blacklist
|
||||
# list: # 仅启用和仅禁用的模型列表
|
||||
# - gemini-3-pro-image-preview
|
||||
# - gemini-3-pro-image-preview-2k
|
||||
# - gemini-2.5-flash-image-preview
|
||||
|
||||
|
||||
queue:
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* @fileoverview 浏览器测试适配器
|
||||
* 提供多种浏览器测试功能,包括 Cloudflare Turnstile 验证、指纹检测等
|
||||
*
|
||||
* 模型类型:
|
||||
* - cloudflare-turnstile: 点击验证后截屏
|
||||
* - 其他 image 类型: 加载页面后截屏
|
||||
* - text 类型: 返回页面文本内容
|
||||
*/
|
||||
|
||||
import { sleep } from '../engine/utils.js';
|
||||
import {
|
||||
gotoWithCheck,
|
||||
normalizePageError,
|
||||
moveMouseAway,
|
||||
} from '../utils/index.js';
|
||||
import { clickTurnstile } from '../utils/CloudflareBypass.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
/**
|
||||
* 执行 Turnstile 验证并截屏
|
||||
*/
|
||||
async function handleTurnstile(page, meta) {
|
||||
const TARGET_URL = 'https://nopecha.com/captcha/turnstile';
|
||||
const HOST_SELECTOR = '#example-container5';
|
||||
|
||||
logger.info('适配器', '开启 Turnstile 测试...', meta);
|
||||
await gotoWithCheck(page, TARGET_URL);
|
||||
|
||||
// 等待页面加载
|
||||
await sleep(3000, 4000);
|
||||
|
||||
// 使用通用 Cloudflare 验证码点击器
|
||||
const result = await clickTurnstile(page, HOST_SELECTOR, {
|
||||
timeout: 10000,
|
||||
waitAfterClick: 3000,
|
||||
meta
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return { error: result.error };
|
||||
}
|
||||
|
||||
// 截屏并返回
|
||||
logger.info('适配器', '正在截屏...', meta);
|
||||
const screenshot = await page.screenshot({ type: 'png', fullPage: true });
|
||||
const base64 = screenshot.toString('base64');
|
||||
|
||||
return { image: `data:image/png;base64,${base64}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理普通 image 类型:加载页面后截屏
|
||||
*/
|
||||
async function handleImagePage(page, url, meta) {
|
||||
logger.info('适配器', `正在加载页面: ${url}`, meta);
|
||||
await gotoWithCheck(page, url);
|
||||
|
||||
// 等待页面加载完成
|
||||
await sleep(3000, 5000);
|
||||
|
||||
// 截屏并返回
|
||||
logger.info('适配器', '正在截屏...', meta);
|
||||
const screenshot = await page.screenshot({ type: 'png', fullPage: true });
|
||||
const base64 = screenshot.toString('base64');
|
||||
|
||||
return { image: `data:image/png;base64,${base64}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 ping0.cc:检测并处理 Cloudflare 验证后截屏
|
||||
*/
|
||||
async function handlePing0(page, url, meta) {
|
||||
logger.info('适配器', `正在加载页面: ${url}`, meta);
|
||||
await gotoWithCheck(page, url);
|
||||
|
||||
// 等待页面加载
|
||||
await sleep(2000, 3000);
|
||||
|
||||
// 检测是否有 Cloudflare 验证码
|
||||
const cfElement = await page.$('#captcha-element');
|
||||
if (cfElement) {
|
||||
logger.info('适配器', '检测到 Cloudflare 验证码,正在处理...', meta);
|
||||
|
||||
const result = await clickTurnstile(page, '#captcha-element', {
|
||||
timeout: 10000,
|
||||
waitAfterClick: 5000,
|
||||
meta
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
logger.warn('适配器', `Cloudflare 验证失败: ${result.error}`, meta);
|
||||
// 继续截屏,可能验证页面也有价值
|
||||
}
|
||||
|
||||
// 等待页面跳转或刷新
|
||||
await sleep(3000, 5000);
|
||||
}
|
||||
|
||||
// 截屏并返回
|
||||
logger.info('适配器', '正在截屏...', meta);
|
||||
const screenshot = await page.screenshot({ type: 'png', fullPage: true });
|
||||
const base64 = screenshot.toString('base64');
|
||||
|
||||
return { image: `data:image/png;base64,${base64}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 text 类型:返回页面文本内容
|
||||
*/
|
||||
async function handleTextPage(page, url, meta) {
|
||||
logger.info('适配器', `正在加载页面: ${url}`, meta);
|
||||
await gotoWithCheck(page, url);
|
||||
|
||||
// 等待页面加载完成
|
||||
await sleep(1000, 2000);
|
||||
|
||||
// 获取页面文本内容
|
||||
const textContent = await page.evaluate(() => document.body.innerText);
|
||||
logger.info('适配器', `获取文本内容,长度: ${textContent.length}`, meta);
|
||||
|
||||
return { text: textContent.trim() };
|
||||
}
|
||||
|
||||
/**
|
||||
* 主生成函数
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
|
||||
try {
|
||||
// 查找模型配置
|
||||
const modelConfig = manifest.models.find(m => m.id === modelId);
|
||||
if (!modelConfig) {
|
||||
return { error: `未找到模型配置: ${modelId}` };
|
||||
}
|
||||
|
||||
const { url, type } = modelConfig;
|
||||
|
||||
// 根据模型 ID 和类型分发处理
|
||||
if (modelId === 'cloudflare-turnstile') {
|
||||
// Turnstile 验证特殊处理
|
||||
return await handleTurnstile(page, meta);
|
||||
} else if (modelId === 'ping0') {
|
||||
// ping0.cc 需要 Cloudflare 验证
|
||||
return await handlePing0(page, url, meta);
|
||||
} else if (type === 'text') {
|
||||
// text 类型返回页面文本
|
||||
return await handleTextPage(page, url, meta);
|
||||
} else {
|
||||
// 其他 image 类型截屏返回
|
||||
return await handleImagePage(page, url, meta);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
const pageError = normalizePageError(err, meta);
|
||||
if (pageError) return pageError;
|
||||
|
||||
logger.error('适配器', '任务失败', { ...meta, error: err.message });
|
||||
return { error: `任务失败: ${err.message}` };
|
||||
} finally {
|
||||
await moveMouseAway(page);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 适配器 manifest
|
||||
*/
|
||||
export const manifest = {
|
||||
id: 'test',
|
||||
displayName: '浏览器检测,仅供调试使用',
|
||||
description: '包含 Cloudflare Turnstile 验证测试、浏览器指纹检测、IP 纯净度查询等功能,仅供调试使用。',
|
||||
|
||||
getTargetUrl(config, workerConfig) {
|
||||
return 'https://abrahamjuliot.github.io/creepjs/';
|
||||
},
|
||||
|
||||
models: [
|
||||
{ id: 'cloudflare-turnstile', imagePolicy: 'forbidden', type: 'image', url: 'https://nopecha.com/captcha/turnstile' },
|
||||
{ id: 'creepjs', imagePolicy: 'forbidden', type: 'image', url: 'https://abrahamjuliot.github.io/creepjs/' },
|
||||
{ id: 'antibot', imagePolicy: 'forbidden', type: 'image', url: 'https://bot.sannysoft.com/' },
|
||||
{ id: 'browserleaks-js', imagePolicy: 'forbidden', type: 'image', url: 'https://browserleaks.com/javascript' },
|
||||
{ id: 'browserleaks-ip', imagePolicy: 'forbidden', type: 'image', url: 'https://browserleaks.com/ip' },
|
||||
{ id: 'ip', imagePolicy: 'forbidden', type: 'text', url: 'https://api.ip.sb/ip' },
|
||||
{ id: 'webgl', imagePolicy: 'forbidden', type: 'image', url: 'https://get.webgl.org/' },
|
||||
{ id: 'ping0', imagePolicy: 'forbidden', type: 'image', url: 'https://ping0.cc/' },
|
||||
],
|
||||
|
||||
navigationHandlers: [],
|
||||
generate
|
||||
};
|
||||
@@ -1,212 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Cloudflare Turnstile 测试适配器
|
||||
* 使用 shadowRootUnl 访问 closed shadow-root 内的元素
|
||||
*
|
||||
* HTML 结构:
|
||||
* #example-container5 > div > closed shadow-root > iframe
|
||||
* iframe body > closed shadow-root > ... > input[type="checkbox"]
|
||||
*/
|
||||
|
||||
import {
|
||||
sleep,
|
||||
safeClick
|
||||
} from '../engine/utils.js';
|
||||
import {
|
||||
gotoWithCheck,
|
||||
normalizePageError,
|
||||
moveMouseAway,
|
||||
} from '../utils/index.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
// --- 配置常量 ---
|
||||
//const TARGET_URL = 'https://nopecha.com/captcha/turnstile';
|
||||
const TARGET_URL = 'https://nowsecure.nl/';
|
||||
|
||||
|
||||
/**
|
||||
* 递归查找具有 shadowRootUnl 的子元素
|
||||
*/
|
||||
async function findElementWithShadowRoot(hostHandle) {
|
||||
return await hostHandle.evaluateHandle(el => {
|
||||
for (const child of el.querySelectorAll('*')) {
|
||||
if (child.shadowRootUnl) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Turnstile 验证任务
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
|
||||
try {
|
||||
logger.info('适配器', '开启 Turnstile 测试...', meta);
|
||||
await gotoWithCheck(page, TARGET_URL);
|
||||
|
||||
// 等待页面加载
|
||||
await sleep(3000, 4000);
|
||||
|
||||
// 1. 获取宿主元素
|
||||
logger.info('适配器', '正在查找宿主元素...', meta);
|
||||
//const hostLocator = page.locator('#example-container5');
|
||||
const hostLocator = page.locator('.cf-turnstile').first();
|
||||
|
||||
await hostLocator.waitFor({ state: 'visible', timeout: 10000 });
|
||||
const hostHandle = await hostLocator.elementHandle();
|
||||
|
||||
if (!hostHandle) {
|
||||
return { error: '无法获取宿主元素句柄' };
|
||||
}
|
||||
|
||||
// 2. 查找有 shadowRootUnl 的子元素
|
||||
logger.info('适配器', '正在查找有 shadowRootUnl 的子元素...', meta);
|
||||
const childWithShadowHandle = await findElementWithShadowRoot(hostHandle);
|
||||
const childElement = childWithShadowHandle.asElement();
|
||||
|
||||
if (!childElement) {
|
||||
return { error: '未找到有 shadowRootUnl 的子元素' };
|
||||
}
|
||||
|
||||
logger.info('适配器', '找到有 shadowRootUnl 的子元素', meta);
|
||||
|
||||
// 3. 获取第一层 shadow-root 并找到 iframe
|
||||
const shadowRootHandle = await childElement.evaluateHandle(el => el.shadowRootUnl);
|
||||
const iframeHandle = await shadowRootHandle.evaluateHandle(root => root?.querySelector('iframe'));
|
||||
const iframeElement = iframeHandle.asElement();
|
||||
|
||||
if (!iframeElement) {
|
||||
return { error: '第一层 shadow-root 内未找到 iframe' };
|
||||
}
|
||||
|
||||
logger.info('适配器', '找到 iframe,正在进入 iframe 内部...', meta);
|
||||
|
||||
// 4. 获取 iframe 的 contentDocument (使用 contentFrame)
|
||||
const frame = await iframeElement.contentFrame();
|
||||
if (!frame) {
|
||||
logger.warn('适配器', '无法获取 iframe 的 contentFrame,尝试坐标点击...', meta);
|
||||
// 降级方案:坐标点击
|
||||
const box = await iframeElement.boundingBox();
|
||||
if (box) {
|
||||
const checkboxX = box.x + 28;
|
||||
const checkboxY = box.y + box.height / 2;
|
||||
await page.mouse.move(checkboxX, checkboxY, { steps: 10 });
|
||||
await sleep(300, 500);
|
||||
await page.mouse.click(checkboxX, checkboxY);
|
||||
logger.info('适配器', '已点击 checkbox(坐标模式)', meta);
|
||||
await sleep(5000, 8000);
|
||||
return { text: 'Turnstile 验证已点击(坐标模式)' };
|
||||
}
|
||||
return { error: '无法获取 iframe 边界框' };
|
||||
}
|
||||
|
||||
// 5. 在 iframe 内查找 body 或有 shadowRootUnl 的元素
|
||||
logger.info('适配器', '正在查找 iframe 内的 shadow-root...', meta);
|
||||
|
||||
// 等待 iframe 内容加载
|
||||
await sleep(1000, 2000);
|
||||
|
||||
// 尝试获取 iframe 内 body 的 shadowRootUnl
|
||||
const bodyWithShadowHandle = await frame.evaluateHandle(() => {
|
||||
// 先检查 body 本身
|
||||
if (document.body && document.body.shadowRootUnl) {
|
||||
return document.body;
|
||||
}
|
||||
// 遍历所有元素查找有 shadowRootUnl 的
|
||||
for (const el of document.querySelectorAll('*')) {
|
||||
if (el.shadowRootUnl) {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const bodyElement = bodyWithShadowHandle.asElement();
|
||||
if (!bodyElement) {
|
||||
logger.warn('适配器', 'iframe 内未找到有 shadowRootUnl 的元素,尝试坐标点击...', meta);
|
||||
const box = await iframeElement.boundingBox();
|
||||
if (box) {
|
||||
const checkboxX = box.x + 28;
|
||||
const checkboxY = box.y + box.height / 2;
|
||||
await page.mouse.move(checkboxX, checkboxY, { steps: 10 });
|
||||
await sleep(300, 500);
|
||||
await page.mouse.click(checkboxX, checkboxY);
|
||||
logger.info('适配器', '已点击 checkbox(坐标模式)', meta);
|
||||
await sleep(5000, 8000);
|
||||
return { text: 'Turnstile 验证已点击(坐标模式)' };
|
||||
}
|
||||
return { error: 'iframe 内未找到有 shadowRootUnl 的元素' };
|
||||
}
|
||||
|
||||
logger.info('适配器', '找到 iframe 内的 shadowRootUnl 宿主', meta);
|
||||
|
||||
// 6. 获取 iframe 内部的 shadow-root 并查找 checkbox
|
||||
const innerShadowRootHandle = await bodyElement.evaluateHandle(el => el.shadowRootUnl);
|
||||
const checkboxHandle = await innerShadowRootHandle.evaluateHandle(root => {
|
||||
if (!root) return null;
|
||||
// 查找 input[type="checkbox"]
|
||||
const checkbox = root.querySelector('input[type="checkbox"]');
|
||||
if (checkbox) return checkbox;
|
||||
// 备用:查找任何 input
|
||||
return root.querySelector('input');
|
||||
});
|
||||
|
||||
const checkboxElement = checkboxHandle.asElement();
|
||||
if (!checkboxElement) {
|
||||
logger.warn('适配器', 'iframe shadow-root 内未找到 checkbox,尝试坐标点击...', meta);
|
||||
const box = await iframeElement.boundingBox();
|
||||
if (box) {
|
||||
const checkboxX = box.x + 28;
|
||||
const checkboxY = box.y + box.height / 2;
|
||||
await page.mouse.move(checkboxX, checkboxY, { steps: 10 });
|
||||
await sleep(300, 500);
|
||||
await page.mouse.click(checkboxX, checkboxY);
|
||||
logger.info('适配器', '已点击 checkbox(坐标模式)', meta);
|
||||
await sleep(5000, 8000);
|
||||
return { text: 'Turnstile 验证已点击(坐标模式)' };
|
||||
}
|
||||
return { error: 'iframe shadow-root 内未找到 checkbox' };
|
||||
}
|
||||
|
||||
logger.info('适配器', '找到 checkbox,正在点击...', meta);
|
||||
|
||||
// 7. 点击 checkbox
|
||||
await safeClick(page, checkboxElement, { bias: 'random' });
|
||||
logger.info('适配器', '已点击 checkbox(直接模式)', meta);
|
||||
await sleep(5000, 8000);
|
||||
|
||||
return { text: 'Turnstile 验证已点击(直接模式)' };
|
||||
|
||||
} catch (err) {
|
||||
const pageError = normalizePageError(err, meta);
|
||||
if (pageError) return pageError;
|
||||
|
||||
logger.error('适配器', '任务失败', { ...meta, error: err.message });
|
||||
return { error: `任务失败: ${err.message}` };
|
||||
} finally {
|
||||
await moveMouseAway(page);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 适配器 manifest
|
||||
*/
|
||||
export const manifest = {
|
||||
id: 'turnstile_test',
|
||||
displayName: 'Cloudflare Turnstile Test (CF人机验证码测试)',
|
||||
description: '测试适配器,用于验证浏览器能否自动通过 Cloudflare Turnstile 人机验证。仅供调试使用。',
|
||||
|
||||
getTargetUrl(config, workerConfig) {
|
||||
return TARGET_URL;
|
||||
},
|
||||
|
||||
models: [
|
||||
{ id: 'cloudflare-turnstile', imagePolicy: 'forbidden', type: 'text' }
|
||||
],
|
||||
|
||||
navigationHandlers: [],
|
||||
generate
|
||||
};
|
||||
@@ -34,6 +34,10 @@ export class PoolManager {
|
||||
// 先加载所有适配器
|
||||
await registry.loadAll();
|
||||
|
||||
// 注入适配器配置(用于模型过滤)
|
||||
const adapterConfig = this.config.backend?.adapter || {};
|
||||
registry.setAdapterConfig(adapterConfig);
|
||||
|
||||
// 解析登录模式参数
|
||||
let loginWorkerName = null;
|
||||
const loginArg = process.argv.find(arg => arg.startsWith('-login'));
|
||||
|
||||
+47
-9
@@ -33,9 +33,43 @@ class AdapterRegistry {
|
||||
constructor() {
|
||||
/** @type {Map<string, object>} */
|
||||
this.adapters = new Map();
|
||||
/** @type {object} 适配器配置(来自 config.yaml) */
|
||||
this.adapterConfig = {};
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置适配器配置
|
||||
* @param {object} config - 适配器配置对象
|
||||
*/
|
||||
setAdapterConfig(config) {
|
||||
this.adapterConfig = config || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模型是否启用
|
||||
* @param {string} adapterId - 适配器 ID
|
||||
* @param {string} modelId - 模型 ID
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isModelEnabled(adapterId, modelId) {
|
||||
const adapterCfg = this.adapterConfig[adapterId];
|
||||
if (!adapterCfg?.modelFilter) return true;
|
||||
|
||||
const { mode, list } = adapterCfg.modelFilter;
|
||||
if (!list || !Array.isArray(list)) return true;
|
||||
|
||||
const inList = list.includes(modelId);
|
||||
|
||||
if (mode === 'whitelist') {
|
||||
// 白名单模式:只有在列表中的才启用
|
||||
return inList;
|
||||
} else {
|
||||
// 黑名单模式(默认):在列表中的被禁用
|
||||
return !inList;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描并加载所有适配器
|
||||
*/
|
||||
@@ -192,14 +226,16 @@ class AdapterRegistry {
|
||||
return { object: 'list', data: [] };
|
||||
}
|
||||
|
||||
const data = adapter.models.map(m => ({
|
||||
id: m.id,
|
||||
object: 'model',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
owned_by: id,
|
||||
image_policy: m.imagePolicy,
|
||||
type: m.type || 'image' // Default to image if not specified
|
||||
}));
|
||||
const data = adapter.models
|
||||
.filter(m => this.isModelEnabled(id, m.id))
|
||||
.map(m => ({
|
||||
id: m.id,
|
||||
object: 'model',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
owned_by: id,
|
||||
image_policy: m.imagePolicy,
|
||||
type: m.type || 'image'
|
||||
}));
|
||||
|
||||
return { object: 'list', data };
|
||||
}
|
||||
@@ -213,7 +249,9 @@ class AdapterRegistry {
|
||||
supportsModel(adapterId, modelId) {
|
||||
const adapter = this.getAdapter(adapterId);
|
||||
if (!adapter?.models) return false;
|
||||
return adapter.models.some(m => m.id === modelId);
|
||||
// 检查模型是否存在且未被禁用
|
||||
const modelExists = adapter.models.some(m => m.id === modelId);
|
||||
return modelExists && this.isModelEnabled(adapterId, modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* @fileoverview Cloudflare 验证绕过工具
|
||||
* 提供通用的 Cloudflare Turnstile 验证码点击功能
|
||||
*/
|
||||
|
||||
import { sleep, safeClick } from '../engine/utils.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
/**
|
||||
* 递归查找具有 shadowRootUnl 的子元素
|
||||
* @param {ElementHandle} hostHandle - 宿主元素句柄
|
||||
* @returns {Promise<ElementHandle|null>}
|
||||
*/
|
||||
async function findElementWithShadowRoot(hostHandle) {
|
||||
return await hostHandle.evaluateHandle(el => {
|
||||
for (const child of el.querySelectorAll('*')) {
|
||||
if (child.shadowRootUnl) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 Cloudflare Turnstile 验证码点击器
|
||||
*
|
||||
* 支持穿透多层 closed shadow-root 和 iframe 找到并点击 checkbox
|
||||
*
|
||||
* @param {Page} page - Playwright page 对象
|
||||
* @param {string} hostSelector - 宿主元素选择器,如 '#example-container5' 或 '.cf-turnstile'
|
||||
* @param {object} [options={}] - 配置选项
|
||||
* @param {number} [options.timeout=10000] - 等待超时时间
|
||||
* @param {number} [options.waitAfterClick=5000] - 点击后等待时间
|
||||
* @param {object} [options.meta={}] - 日志元数据
|
||||
* @returns {Promise<{success: boolean, error?: string}>}
|
||||
*/
|
||||
export async function clickTurnstile(page, hostSelector, options = {}) {
|
||||
const {
|
||||
timeout = 10000,
|
||||
waitAfterClick = 5000,
|
||||
meta = {}
|
||||
} = options;
|
||||
|
||||
try {
|
||||
// 1. 获取宿主元素
|
||||
logger.info('人机盾', '正在查找宿主元素...', meta);
|
||||
const hostLocator = page.locator(hostSelector).first();
|
||||
|
||||
await hostLocator.waitFor({ state: 'visible', timeout });
|
||||
const hostHandle = await hostLocator.elementHandle();
|
||||
|
||||
if (!hostHandle) {
|
||||
return { success: false, error: '无法获取宿主元素句柄' };
|
||||
}
|
||||
|
||||
// 2. 查找有 shadowRootUnl 的子元素
|
||||
logger.info('人机盾', '正在查找 shadowRootUnl 子元素...', meta);
|
||||
const childWithShadowHandle = await findElementWithShadowRoot(hostHandle);
|
||||
const childElement = childWithShadowHandle.asElement();
|
||||
|
||||
if (!childElement) {
|
||||
return { success: false, error: '未找到有 shadowRootUnl 的子元素' };
|
||||
}
|
||||
|
||||
logger.debug('人机盾', '找到 shadowRootUnl 子元素', meta);
|
||||
|
||||
// 3. 获取第一层 shadow-root 并找到 iframe
|
||||
const shadowRootHandle = await childElement.evaluateHandle(el => el.shadowRootUnl);
|
||||
const iframeHandle = await shadowRootHandle.evaluateHandle(root => root?.querySelector('iframe'));
|
||||
const iframeElement = iframeHandle.asElement();
|
||||
|
||||
if (!iframeElement) {
|
||||
return { success: false, error: '第一层 shadow-root 内未找到 iframe' };
|
||||
}
|
||||
|
||||
logger.debug('人机盾', '找到 iframe,正在进入...', meta);
|
||||
|
||||
// 4. 获取 iframe 的 contentFrame
|
||||
const frame = await iframeElement.contentFrame();
|
||||
|
||||
// 辅助函数:坐标点击
|
||||
const clickByCoordinates = async () => {
|
||||
const box = await iframeElement.boundingBox();
|
||||
if (!box) return false;
|
||||
|
||||
const checkboxX = box.x + 28;
|
||||
const checkboxY = box.y + box.height / 2;
|
||||
await page.mouse.move(checkboxX, checkboxY, { steps: 10 });
|
||||
await sleep(300, 500);
|
||||
await page.mouse.click(checkboxX, checkboxY);
|
||||
logger.info('人机盾', '我是人类! (坐标模式)', meta);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!frame) {
|
||||
logger.warn('人机盾', '无法获取 iframe contentFrame,尝试坐标点击...', meta);
|
||||
if (await clickByCoordinates()) {
|
||||
await sleep(waitAfterClick, waitAfterClick + 3000);
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: '无法获取 iframe 边界框' };
|
||||
}
|
||||
|
||||
// 5. 在 iframe 内查找有 shadowRootUnl 的元素
|
||||
logger.debug('人机盾', '正在查找 iframe 内的 shadow-root...', meta);
|
||||
await sleep(1000, 2000);
|
||||
|
||||
const bodyWithShadowHandle = await frame.evaluateHandle(() => {
|
||||
if (document.body && document.body.shadowRootUnl) {
|
||||
return document.body;
|
||||
}
|
||||
for (const el of document.querySelectorAll('*')) {
|
||||
if (el.shadowRootUnl) {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const bodyElement = bodyWithShadowHandle.asElement();
|
||||
if (!bodyElement) {
|
||||
logger.warn('人机盾', 'iframe 内未找到 shadowRootUnl 元素,尝试坐标点击...', meta);
|
||||
if (await clickByCoordinates()) {
|
||||
await sleep(waitAfterClick, waitAfterClick + 3000);
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: 'iframe 内未找到有 shadowRootUnl 的元素' };
|
||||
}
|
||||
|
||||
logger.debug('人机盾', '找到 iframe 内的 shadowRootUnl 宿主', meta);
|
||||
|
||||
// 6. 获取 iframe 内部的 shadow-root 并查找 checkbox
|
||||
const innerShadowRootHandle = await bodyElement.evaluateHandle(el => el.shadowRootUnl);
|
||||
const checkboxHandle = await innerShadowRootHandle.evaluateHandle(root => {
|
||||
if (!root) return null;
|
||||
const checkbox = root.querySelector('input[type="checkbox"]');
|
||||
if (checkbox) return checkbox;
|
||||
return root.querySelector('input');
|
||||
});
|
||||
|
||||
const checkboxElement = checkboxHandle.asElement();
|
||||
if (!checkboxElement) {
|
||||
logger.warn('人机盾', 'shadow-root 内未找到 checkbox,尝试坐标点击...', meta);
|
||||
if (await clickByCoordinates()) {
|
||||
await sleep(waitAfterClick, waitAfterClick + 3000);
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: 'iframe shadow-root 内未找到 checkbox' };
|
||||
}
|
||||
|
||||
// 7. 直接点击 checkbox
|
||||
logger.info('人机盾', '找到 checkbox,正在点击...', meta);
|
||||
await safeClick(page, checkboxElement, { bias: 'random' });
|
||||
logger.info('人机盾', '我是人类!(元素模式)', meta);
|
||||
|
||||
await sleep(waitAfterClick, waitAfterClick + 3000);
|
||||
return { success: true };
|
||||
|
||||
} catch (err) {
|
||||
logger.error('人机盾', `点击失败: ${err.message}`, meta);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
@@ -371,16 +371,19 @@ export function createAdminRouter(context) {
|
||||
if (method === 'GET' && pathname === '/adapters') {
|
||||
const adapters = [];
|
||||
const adapterIds = registry.getAdapterIds();
|
||||
const adapterConfig = getAdaptersConfig();
|
||||
|
||||
for (const id of adapterIds) {
|
||||
const adapter = registry.getAdapter(id);
|
||||
if (adapter) {
|
||||
const config = adapterConfig[id] || {};
|
||||
adapters.push({
|
||||
id: adapter.id,
|
||||
displayName: adapter.displayName || adapter.id,
|
||||
description: adapter.description || '',
|
||||
modelCount: adapter.models?.length || 0,
|
||||
models: (adapter.models || []).map(m => m.id),
|
||||
modelFilter: config.modelFilter || { mode: 'blacklist', list: [] },
|
||||
configSchema: adapter.configSchema || []
|
||||
});
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{c as i,I as u}from"./index-BaE9FvKY.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M928 140H96c-17.7 0-32 14.3-32 32v496c0 17.7 14.3 32 32 32h380v112H304c-8.8 0-16 7.2-16 16v48c0 4.4 3.6 8 8 8h432c4.4 0 8-3.6 8-8v-48c0-8.8-7.2-16-16-16H548V700h380c17.7 0 32-14.3 32-32V172c0-17.7-14.3-32-32-32zm-40 488H136V212h752v416z"}}]},name:"desktop",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){s(r,a,e[a])})}return r}function s(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=c({},t,e.attrs);return i(u,c({},n,{icon:l}),null)};o.displayName="DesktopOutlined";o.inheritAttrs=!1;export{o as D};
|
||||
import{c as i,I as u}from"./index-CeQVA4cs.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M928 140H96c-17.7 0-32 14.3-32 32v496c0 17.7 14.3 32 32 32h380v112H304c-8.8 0-16 7.2-16 16v48c0 4.4 3.6 8 8 8h432c4.4 0 8-3.6 8-8v-48c0-8.8-7.2-16-16-16H548V700h380c17.7 0 32-14.3 32-32V172c0-17.7-14.3-32-32-32zm-40 488H136V212h752v416z"}}]},name:"desktop",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){s(r,a,e[a])})}return r}function s(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=c({},t,e.attrs);return i(u,c({},n,{icon:l}),null)};o.displayName="DesktopOutlined";o.inheritAttrs=!1;export{o as D};
|
||||
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
import{c as l,I as P,_ as q,j as J,r as p,l as Q,o as X,a as Y,b as N,d as b,w as o,g as v,f,h as m,u as C,R as k,y as Z,D as K,i as L,e as S,t as _,m as ee,F as te,s as R,M as ae,z as le,A as ne,B as oe}from"./index-BaE9FvKY.js";var se={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M304 280h56c4.4 0 8-3.6 8-8 0-28.3 5.9-53.2 17.1-73.5 10.6-19.4 26-34.8 45.4-45.4C450.9 142 475.7 136 504 136h16c28.3 0 53.2 5.9 73.5 17.1 19.4 10.6 34.8 26 45.4 45.4C650 218.9 656 243.7 656 272c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-40-8.8-76.7-25.9-108.1a184.31 184.31 0 00-74-74C596.7 72.8 560 64 520 64h-16c-40 0-76.7 8.8-108.1 25.9a184.31 184.31 0 00-74 74C304.8 195.3 296 232 296 272c0 4.4 3.6 8 8 8z"}},{tag:"path",attrs:{d:"M940 512H792V412c76.8 0 139-62.2 139-139 0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8a63 63 0 01-63 63H232a63 63 0 01-63-63c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 76.8 62.2 139 139 139v100H84c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h148v96c0 6.5.2 13 .7 19.3C164.1 728.6 116 796.7 116 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-44.2 23.9-82.9 59.6-103.7a273 273 0 0022.7 49c24.3 41.5 59 76.2 100.5 100.5S460.5 960 512 960s99.8-13.9 141.3-38.2a281.38 281.38 0 00123.2-149.5A120 120 0 01836 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-79.3-48.1-147.4-116.7-176.7.4-6.4.7-12.8.7-19.3v-96h148c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM716 680c0 36.8-9.7 72-27.8 102.9-17.7 30.3-43 55.6-73.3 73.3C584 874.3 548.8 884 512 884s-72-9.7-102.9-27.8c-30.3-17.7-55.6-43-73.3-73.3A202.75 202.75 0 01308 680V412h408v268z"}}]},name:"bug",theme:"outlined"};function D(n){for(var t=1;t<arguments.length;t++){var a=arguments[t]!=null?Object(arguments[t]):{},s=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(s=s.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),s.forEach(function(c){re(n,c,a[c])})}return n}function re(n,t,a){return t in n?Object.defineProperty(n,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):n[t]=a,n}var j=function(t,a){var s=D({},t,a.attrs);return l(P,D({},s,{icon:se}),null)};j.displayName="BugOutlined";j.inheritAttrs=!1;var ce={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 720a48 48 0 1096 0 48 48 0 10-96 0zm16-304v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8zm475.7 440l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48zm-783.5-27.9L512 239.9l339.8 588.2H172.2z"}}]},name:"warning",theme:"outlined"};function E(n){for(var t=1;t<arguments.length;t++){var a=arguments[t]!=null?Object(arguments[t]):{},s=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(s=s.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),s.forEach(function(c){ie(n,c,a[c])})}return n}function ie(n,t,a){return t in n?Object.defineProperty(n,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):n[t]=a,n}var I=function(t,a){var s=E({},t,a.attrs);return l(P,E({},s,{icon:ce}),null)};I.displayName="WarningOutlined";I.inheritAttrs=!1;const ue={class:"toolbar"},de={class:"toolbar-row"},fe={class:"toolbar-row"},ve={style:{"margin-bottom":"12px",color:"#8c8c8c","font-size":"12px"}},me={key:0,style:{color:"#1890ff","margin-left":"8px"}},pe={class:"log-container"},ge={class:"log-time"},_e={class:"log-module"},he={class:"log-message"},Oe={__name:"logs",setup(n){const t=J(),a=p([]),s=p(!1),c=p(0),h=p(!1),g=p(null),y=p(""),w=p("all"),U={INFO:{color:"#1890ff",icon:oe},WARN:{color:"#faad14",icon:I},ERRO:{color:"#ff4d4f",icon:ne},DBUG:{color:"#722ed1",icon:j}},z=async()=>{s.value=!0;try{const r=await fetch("/admin/logs?lines=500",{headers:t.getHeaders()});if(r.ok){const e=await r.json();a.value=A(e.logs||[]),c.value=e.total||0}}catch{R.error("获取日志失败")}finally{s.value=!1}},A=r=>r.map((e,i)=>{const d=e.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(\w+)\] \[([^\]]+)\] (.*)$/);return d?{id:i,time:d[1],level:d[2],module:d[3],message:d[4],raw:e}:{id:i,raw:e,level:"INFO",time:"",module:"",message:e}}),x=Q(()=>a.value.filter(e=>{if(w.value!=="all"&&e.level!==w.value)return!1;if(y.value){const i=y.value.toLowerCase();return e.raw.toLowerCase().includes(i)}return!0}).reverse()),F=()=>{ae.confirm({title:"确认清除日志",content:"此操作将删除所有系统日志文件,是否继续?",okText:"确认清除",okType:"danger",cancelText:"取消",async onOk(){try{(await fetch("/admin/logs",{method:"DELETE",headers:t.getHeaders()})).ok?(R.success("日志已清除"),a.value=[],c.value=0):R.error("清除失败")}catch{R.error("请求失败")}}})},M=()=>{const r=a.value.map(O=>O.raw).join(`
|
||||
import{c as l,I as P,_ as q,j as J,r as p,l as Q,o as X,a as Y,b as N,d as b,w as o,g as v,f,h as m,u as C,R as k,y as Z,D as K,i as L,e as S,t as _,m as ee,F as te,s as R,M as ae,z as le,A as ne,B as oe}from"./index-CeQVA4cs.js";var se={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M304 280h56c4.4 0 8-3.6 8-8 0-28.3 5.9-53.2 17.1-73.5 10.6-19.4 26-34.8 45.4-45.4C450.9 142 475.7 136 504 136h16c28.3 0 53.2 5.9 73.5 17.1 19.4 10.6 34.8 26 45.4 45.4C650 218.9 656 243.7 656 272c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-40-8.8-76.7-25.9-108.1a184.31 184.31 0 00-74-74C596.7 72.8 560 64 520 64h-16c-40 0-76.7 8.8-108.1 25.9a184.31 184.31 0 00-74 74C304.8 195.3 296 232 296 272c0 4.4 3.6 8 8 8z"}},{tag:"path",attrs:{d:"M940 512H792V412c76.8 0 139-62.2 139-139 0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8a63 63 0 01-63 63H232a63 63 0 01-63-63c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 76.8 62.2 139 139 139v100H84c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h148v96c0 6.5.2 13 .7 19.3C164.1 728.6 116 796.7 116 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-44.2 23.9-82.9 59.6-103.7a273 273 0 0022.7 49c24.3 41.5 59 76.2 100.5 100.5S460.5 960 512 960s99.8-13.9 141.3-38.2a281.38 281.38 0 00123.2-149.5A120 120 0 01836 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-79.3-48.1-147.4-116.7-176.7.4-6.4.7-12.8.7-19.3v-96h148c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM716 680c0 36.8-9.7 72-27.8 102.9-17.7 30.3-43 55.6-73.3 73.3C584 874.3 548.8 884 512 884s-72-9.7-102.9-27.8c-30.3-17.7-55.6-43-73.3-73.3A202.75 202.75 0 01308 680V412h408v268z"}}]},name:"bug",theme:"outlined"};function D(n){for(var t=1;t<arguments.length;t++){var a=arguments[t]!=null?Object(arguments[t]):{},s=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(s=s.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),s.forEach(function(c){re(n,c,a[c])})}return n}function re(n,t,a){return t in n?Object.defineProperty(n,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):n[t]=a,n}var j=function(t,a){var s=D({},t,a.attrs);return l(P,D({},s,{icon:se}),null)};j.displayName="BugOutlined";j.inheritAttrs=!1;var ce={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 720a48 48 0 1096 0 48 48 0 10-96 0zm16-304v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8zm475.7 440l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48zm-783.5-27.9L512 239.9l339.8 588.2H172.2z"}}]},name:"warning",theme:"outlined"};function E(n){for(var t=1;t<arguments.length;t++){var a=arguments[t]!=null?Object(arguments[t]):{},s=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(s=s.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),s.forEach(function(c){ie(n,c,a[c])})}return n}function ie(n,t,a){return t in n?Object.defineProperty(n,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):n[t]=a,n}var I=function(t,a){var s=E({},t,a.attrs);return l(P,E({},s,{icon:ce}),null)};I.displayName="WarningOutlined";I.inheritAttrs=!1;const ue={class:"toolbar"},de={class:"toolbar-row"},fe={class:"toolbar-row"},ve={style:{"margin-bottom":"12px",color:"#8c8c8c","font-size":"12px"}},me={key:0,style:{color:"#1890ff","margin-left":"8px"}},pe={class:"log-container"},ge={class:"log-time"},_e={class:"log-module"},he={class:"log-message"},Oe={__name:"logs",setup(n){const t=J(),a=p([]),s=p(!1),c=p(0),h=p(!1),g=p(null),y=p(""),w=p("all"),U={INFO:{color:"#1890ff",icon:oe},WARN:{color:"#faad14",icon:I},ERRO:{color:"#ff4d4f",icon:ne},DBUG:{color:"#722ed1",icon:j}},z=async()=>{s.value=!0;try{const r=await fetch("/admin/logs?lines=500",{headers:t.getHeaders()});if(r.ok){const e=await r.json();a.value=A(e.logs||[]),c.value=e.total||0}}catch{R.error("获取日志失败")}finally{s.value=!1}},A=r=>r.map((e,i)=>{const d=e.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(\w+)\] \[([^\]]+)\] (.*)$/);return d?{id:i,time:d[1],level:d[2],module:d[3],message:d[4],raw:e}:{id:i,raw:e,level:"INFO",time:"",module:"",message:e}}),x=Q(()=>a.value.filter(e=>{if(w.value!=="all"&&e.level!==w.value)return!1;if(y.value){const i=y.value.toLowerCase();return e.raw.toLowerCase().includes(i)}return!0}).reverse()),F=()=>{ae.confirm({title:"确认清除日志",content:"此操作将删除所有系统日志文件,是否继续?",okText:"确认清除",okType:"danger",cancelText:"取消",async onOk(){try{(await fetch("/admin/logs",{method:"DELETE",headers:t.getHeaders()})).ok?(R.success("日志已清除"),a.value=[],c.value=0):R.error("清除失败")}catch{R.error("请求失败")}}})},M=()=>{const r=a.value.map(O=>O.raw).join(`
|
||||
`),e=new Blob([r],{type:"text/plain"}),i=URL.createObjectURL(e),d=document.createElement("a");d.href=i,d.download=`system-${new Date().toISOString().split("T")[0]}.log`,d.click(),URL.revokeObjectURL(i)},T=r=>{h.value=r,r?(z(),g.value=setInterval(z,5e3)):g.value&&(clearInterval(g.value),g.value=null)};return X(()=>{z()}),Y(()=>{g.value&&clearInterval(g.value)}),(r,e)=>{const i=f("a-select-option"),d=f("a-select"),O=f("a-button"),B=f("a-tooltip"),V=f("a-space"),W=f("a-input-search"),$=f("a-tag"),H=f("a-empty"),G=f("a-card");return b(),N(G,{title:"系统日志",bordered:!1},{default:o(()=>[v("div",ue,[v("div",de,[l(d,{value:w.value,"onUpdate:value":e[0]||(e[0]=u=>w.value=u),style:{width:"90px"},size:"small"},{default:o(()=>[l(i,{value:"all"},{default:o(()=>[...e[3]||(e[3]=[m("全部",-1)])]),_:1}),l(i,{value:"INFO"},{default:o(()=>[...e[4]||(e[4]=[m("INFO",-1)])]),_:1}),l(i,{value:"WARN"},{default:o(()=>[...e[5]||(e[5]=[m("WARN",-1)])]),_:1}),l(i,{value:"ERRO"},{default:o(()=>[...e[6]||(e[6]=[m("ERROR",-1)])]),_:1}),l(i,{value:"DBUG"},{default:o(()=>[...e[7]||(e[7]=[m("DEBUG",-1)])]),_:1})]),_:1},8,["value"]),l(V,{size:4},{default:o(()=>[l(B,{title:h.value?"关闭自动刷新":"开启自动刷新"},{default:o(()=>[l(O,{size:"small",type:h.value?"primary":"default",onClick:e[1]||(e[1]=u=>T(!h.value))},{icon:o(()=>[l(C(k))]),_:1},8,["type"])]),_:1},8,["title"]),l(B,{title:"导出日志"},{default:o(()=>[l(O,{size:"small",onClick:M},{icon:o(()=>[l(C(Z))]),_:1})]),_:1}),l(B,{title:"清除日志"},{default:o(()=>[l(O,{size:"small",danger:"",onClick:F},{icon:o(()=>[l(C(K))]),_:1})]),_:1})]),_:1})]),v("div",fe,[l(W,{value:y.value,"onUpdate:value":e[2]||(e[2]=u=>y.value=u),placeholder:"搜索日志",size:"small","enter-button":"","allow-clear":"",style:{width:"100%"}},null,8,["value"])])]),v("div",ve,[m(" 共 "+_(c.value)+" 条日志,当前显示 "+_(x.value.length)+" 条 ",1),h.value?(b(),L("span",me,[l(C(k),{spin:!0}),e[8]||(e[8]=m(" 自动刷新中 ",-1))])):S("",!0)]),v("div",pe,[(b(!0),L(te,null,ee(x.value,u=>(b(),L("div",{key:u.id,class:le(["log-line","level-"+u.level.toLowerCase()])},[v("span",ge,_(u.time),1),l($,{color:U[u.level]?.color||"#8c8c8c",size:"small",style:{margin:"0 8px"}},{default:o(()=>[m(_(u.level),1)]),_:2},1032,["color"]),v("span",_e,"["+_(u.module)+"]",1),v("span",he,_(u.message),1)],2))),128)),x.value.length===0?(b(),N(H,{key:0,description:"暂无日志"})):S("",!0)])]),_:1})}}},ye=q(Oe,[["__scopeId","data-v-6c3b8e99"]]);export{ye as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{_ as b,j as w,k,o as c,b as C,d as S,w as n,c as o,g as e,f as a,h as i}from"./index-BaE9FvKY.js";const B={style:{"margin-bottom":"8px"}},T={style:{"margin-bottom":"8px"}},z={style:{"margin-bottom":"8px"}},U={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},j={style:{"margin-bottom":"8px"}},M={style:{"margin-bottom":"8px"}},q={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},L={__name:"server",setup(N){const d=w(),s=k({port:5173,authToken:"",keepaliveMode:"comment",queueBuffer:2,imageLimit:5});c(async()=>{await d.fetchServerConfig(),Object.assign(s,d.serverConfig)});const m=async()=>{await d.saveServerConfig(s)};return(V,t)=>{const p=a("a-input-number"),r=a("a-col"),y=a("a-input-password"),u=a("a-select-option"),g=a("a-select"),f=a("a-row"),v=a("a-button"),x=a("a-card"),_=a("a-layout");return S(),C(_,{style:{background:"transparent"}},{default:n(()=>[o(x,{title:"服务器设置",bordered:!1,style:{width:"100%"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",B,[t[5]||(t[5]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"监听端口",-1)),t[6]||(t[6]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 设置服务器监听的端口号,默认为 5173 ",-1)),o(p,{value:s.port,"onUpdate:value":t[0]||(t[0]=l=>s.port=l),min:1,max:65535,placeholder:"请输入端口号",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",T,[t[7]||(t[7]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"鉴权 Token",-1)),t[8]||(t[8]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 用于 API 请求鉴权的密钥,留空则不启用鉴权 ",-1)),o(y,{value:s.authToken,"onUpdate:value":t[1]||(t[1]=l=>s.authToken=l),placeholder:"请输入 Token",type:"password"},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",z,[t[11]||(t[11]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"心跳包类型",-1)),t[12]||(t[12]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 选择 SSE 流式响应的心跳包格式 ",-1)),o(g,{value:s.keepaliveMode,"onUpdate:value":t[2]||(t[2]=l=>s.keepaliveMode=l),style:{width:"100%"},placeholder:"请选择心跳包类型"},{default:n(()=>[o(u,{value:"comment"},{default:n(()=>[...t[9]||(t[9]=[i("Comment - 注释格式",-1)])]),_:1}),o(u,{value:"content"},{default:n(()=>[...t[10]||(t[10]=[i("Content - 内容格式",-1)])]),_:1})]),_:1},8,["value"])])]),_:1})]),_:1}),e("div",U,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[13]||(t[13]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1}),o(x,{title:"队列设置",bordered:!1,style:{width:"100%","margin-top":"10px"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",j,[t[14]||(t[14]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"队列缓冲区大小",-1)),t[15]||(t[15]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 非流式请求的额外排队数(设为 0 则不限制非流式请求数量)"),e("br"),i(" 实际队列上限 = Workers数量 + 缓冲区大小 ")],-1)),o(p,{value:s.queueBuffer,"onUpdate:value":t[3]||(t[3]=l=>s.queueBuffer=l),min:0,max:100,placeholder:"默认为 2",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",M,[t[16]||(t[16]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"图片数量上限",-1)),t[17]||(t[17]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 单次请求最多支持的图片附件数量"),e("br"),i(" 网页最多支持10个附件,超出会被丢弃 ")],-1)),o(p,{value:s.imageLimit,"onUpdate:value":t[4]||(t[4]=l=>s.imageLimit=l),min:1,max:10,placeholder:"默认为 5",style:{width:"100%"}},null,8,["value"])])]),_:1})]),_:1}),e("div",q,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[18]||(t[18]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1})]),_:1})}}},A=b(L,[["__scopeId","data-v-bd32923f"]]);export{A as default};
|
||||
import{_ as b,j as w,k,o as c,b as C,d as S,w as n,c as o,g as e,f as a,h as i}from"./index-CeQVA4cs.js";const B={style:{"margin-bottom":"8px"}},T={style:{"margin-bottom":"8px"}},z={style:{"margin-bottom":"8px"}},U={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},j={style:{"margin-bottom":"8px"}},M={style:{"margin-bottom":"8px"}},q={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},L={__name:"server",setup(N){const d=w(),s=k({port:5173,authToken:"",keepaliveMode:"comment",queueBuffer:2,imageLimit:5});c(async()=>{await d.fetchServerConfig(),Object.assign(s,d.serverConfig)});const m=async()=>{await d.saveServerConfig(s)};return(V,t)=>{const p=a("a-input-number"),r=a("a-col"),y=a("a-input-password"),u=a("a-select-option"),g=a("a-select"),f=a("a-row"),v=a("a-button"),x=a("a-card"),_=a("a-layout");return S(),C(_,{style:{background:"transparent"}},{default:n(()=>[o(x,{title:"服务器设置",bordered:!1,style:{width:"100%"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",B,[t[5]||(t[5]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"监听端口",-1)),t[6]||(t[6]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 设置服务器监听的端口号,默认为 5173 ",-1)),o(p,{value:s.port,"onUpdate:value":t[0]||(t[0]=l=>s.port=l),min:1,max:65535,placeholder:"请输入端口号",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",T,[t[7]||(t[7]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"鉴权 Token",-1)),t[8]||(t[8]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 用于 API 请求鉴权的密钥,留空则不启用鉴权 ",-1)),o(y,{value:s.authToken,"onUpdate:value":t[1]||(t[1]=l=>s.authToken=l),placeholder:"请输入 Token",type:"password"},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",z,[t[11]||(t[11]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"心跳包类型",-1)),t[12]||(t[12]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 选择 SSE 流式响应的心跳包格式 ",-1)),o(g,{value:s.keepaliveMode,"onUpdate:value":t[2]||(t[2]=l=>s.keepaliveMode=l),style:{width:"100%"},placeholder:"请选择心跳包类型"},{default:n(()=>[o(u,{value:"comment"},{default:n(()=>[...t[9]||(t[9]=[i("Comment - 注释格式",-1)])]),_:1}),o(u,{value:"content"},{default:n(()=>[...t[10]||(t[10]=[i("Content - 内容格式",-1)])]),_:1})]),_:1},8,["value"])])]),_:1})]),_:1}),e("div",U,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[13]||(t[13]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1}),o(x,{title:"队列设置",bordered:!1,style:{width:"100%","margin-top":"10px"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",j,[t[14]||(t[14]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"队列缓冲区大小",-1)),t[15]||(t[15]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 非流式请求的额外排队数(设为 0 则不限制非流式请求数量)"),e("br"),i(" 实际队列上限 = Workers数量 + 缓冲区大小 ")],-1)),o(p,{value:s.queueBuffer,"onUpdate:value":t[3]||(t[3]=l=>s.queueBuffer=l),min:0,max:100,placeholder:"默认为 2",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",M,[t[16]||(t[16]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"图片数量上限",-1)),t[17]||(t[17]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 单次请求最多支持的图片附件数量"),e("br"),i(" 网页最多支持10个附件,超出会被丢弃 ")],-1)),o(p,{value:s.imageLimit,"onUpdate:value":t[4]||(t[4]=l=>s.imageLimit=l),min:1,max:10,placeholder:"默认为 5",style:{width:"100%"}},null,8,["value"])])]),_:1})]),_:1}),e("div",q,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[18]||(t[18]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1})]),_:1})}}},A=b(L,[["__scopeId","data-v-bd32923f"]]);export{A as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{x as i,j as a,s as r}from"./index-BaE9FvKY.js";const u=i("system",{state:()=>({status:"",version:"1.0.0",systemVersion:"",uptime:0,cpuUsage:0,memoryUsage:{total:0,used:0,free:0},safeMode:{enabled:!1,reason:null},stats:{totalRequests:0,successRate:0,activeWorkers:0,totalWorkers:0,avgResponseTime:0}}),actions:{async fetchStatus(){const t=a();try{const e=await fetch("/admin/status",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.$patch(s)}}catch(e){console.error("Failed to fetch system status:",e)}},async fetchStats(){const t=a();try{const e=await fetch("/admin/stats",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.stats=s}}catch(e){console.error("Failed to fetch stats:",e)}},async restartService(t={}){const e=a(),{loginMode:s,workerName:n}=t;try{const o=await(await fetch("/admin/restart",{method:"POST",headers:{...e.getHeaders(),"Content-Type":"application/json"},body:JSON.stringify({loginMode:s,workerName:n})})).json();return o.success?(r.success(o.message||"服务重启中..."),!0):(r.error("重启失败"),!1)}catch{return r.error("重启请求失败"),!1}},async stopService(){const t=a();try{const s=await(await fetch("/admin/stop",{method:"POST",headers:t.getHeaders()})).json();return s.success?(r.success(s.message||"服务停止中..."),!0):(r.error("停止失败"),!1)}catch{return r.error("停止请求失败"),!1}}}});export{u};
|
||||
import{x as i,j as a,s as r}from"./index-CeQVA4cs.js";const u=i("system",{state:()=>({status:"",version:"1.0.0",systemVersion:"",uptime:0,cpuUsage:0,memoryUsage:{total:0,used:0,free:0},safeMode:{enabled:!1,reason:null},stats:{totalRequests:0,successRate:0,activeWorkers:0,totalWorkers:0,avgResponseTime:0}}),actions:{async fetchStatus(){const t=a();try{const e=await fetch("/admin/status",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.$patch(s)}}catch(e){console.error("Failed to fetch system status:",e)}},async fetchStats(){const t=a();try{const e=await fetch("/admin/stats",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.stats=s}}catch(e){console.error("Failed to fetch stats:",e)}},async restartService(t={}){const e=a(),{loginMode:s,workerName:n}=t;try{const o=await(await fetch("/admin/restart",{method:"POST",headers:{...e.getHeaders(),"Content-Type":"application/json"},body:JSON.stringify({loginMode:s,workerName:n})})).json();return o.success?(r.success(o.message||"服务重启中..."),!0):(r.error("重启失败"),!1)}catch{return r.error("重启请求失败"),!1}},async stopService(){const t=a();try{const s=await(await fetch("/admin/stop",{method:"POST",headers:t.getHeaders()})).json();return s.success?(r.success(s.message||"服务停止中..."),!0):(r.error("停止失败"),!1)}catch{return r.error("停止请求失败"),!1}}}});export{u};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -6,7 +6,7 @@
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WebAI2API</title>
|
||||
<script type="module" crossorigin src="/assets/index-BaE9FvKY.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CeQVA4cs.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BVr8U7Bl.css">
|
||||
</head>
|
||||
|
||||
|
||||
@@ -10,6 +10,12 @@ const drawerVisible = ref(false);
|
||||
const currentAdapter = ref(null);
|
||||
const currentConfig = reactive({});
|
||||
|
||||
// 模型过滤配置
|
||||
const modelFilter = reactive({
|
||||
mode: 'blacklist',
|
||||
list: []
|
||||
});
|
||||
|
||||
// 挂载时获取数据
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
@@ -21,6 +27,45 @@ onMounted(async () => {
|
||||
// 适配器列表
|
||||
const adapters = computed(() => settingsStore.adaptersMeta);
|
||||
|
||||
// 检查模型是否启用
|
||||
const isModelEnabled = (modelId) => {
|
||||
const inList = modelFilter.list.includes(modelId);
|
||||
if (modelFilter.mode === 'whitelist') {
|
||||
return inList;
|
||||
} else {
|
||||
return !inList;
|
||||
}
|
||||
};
|
||||
|
||||
// 切换模型启用/禁用
|
||||
const toggleModel = (modelId, enabled) => {
|
||||
const idx = modelFilter.list.indexOf(modelId);
|
||||
|
||||
if (modelFilter.mode === 'whitelist') {
|
||||
// 白名单模式:启用=加入列表,禁用=移出列表
|
||||
if (enabled && idx === -1) {
|
||||
modelFilter.list.push(modelId);
|
||||
} else if (!enabled && idx !== -1) {
|
||||
modelFilter.list.splice(idx, 1);
|
||||
}
|
||||
} else {
|
||||
// 黑名单模式:禁用=加入列表,启用=移出列表
|
||||
if (!enabled && idx === -1) {
|
||||
modelFilter.list.push(modelId);
|
||||
} else if (enabled && idx !== -1) {
|
||||
modelFilter.list.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 切换模式时重置列表
|
||||
const onModeChange = (newMode) => {
|
||||
if (newMode !== modelFilter.mode) {
|
||||
modelFilter.mode = newMode;
|
||||
modelFilter.list = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 打开抽屉进行编辑
|
||||
const handleEdit = (adapter) => {
|
||||
currentAdapter.value = adapter;
|
||||
@@ -41,6 +86,11 @@ const handleEdit = (adapter) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化模型过滤配置
|
||||
const filter = adapter.modelFilter || { mode: 'blacklist', list: [] };
|
||||
modelFilter.mode = filter.mode || 'blacklist';
|
||||
modelFilter.list = [...(filter.list || [])];
|
||||
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
@@ -49,11 +99,22 @@ const handleSave = async () => {
|
||||
if (!currentAdapter.value) return;
|
||||
|
||||
const configToSave = {
|
||||
[currentAdapter.value.id]: { ...currentConfig }
|
||||
[currentAdapter.value.id]: {
|
||||
...currentConfig,
|
||||
modelFilter: {
|
||||
mode: modelFilter.mode,
|
||||
list: [...modelFilter.list]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const success = await settingsStore.saveAdapterConfig(configToSave);
|
||||
if (success) {
|
||||
// 更新本地缓存
|
||||
const adapter = settingsStore.adaptersMeta.find(a => a.id === currentAdapter.value.id);
|
||||
if (adapter) {
|
||||
adapter.modelFilter = { mode: modelFilter.mode, list: [...modelFilter.list] };
|
||||
}
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
};
|
||||
@@ -76,7 +137,7 @@ const handleSave = async () => {
|
||||
style="font-size: 18px; color: #1890ff; margin-right: 8px; flex-shrink: 0;" />
|
||||
<span
|
||||
style="font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{
|
||||
item.id }}</span>
|
||||
item.id }}</span>
|
||||
</div>
|
||||
<SettingOutlined style="font-size: 16px; color: #8c8c8c; flex-shrink: 0;" />
|
||||
</div>
|
||||
@@ -95,11 +156,39 @@ const handleSave = async () => {
|
||||
{{ currentAdapter.description }}
|
||||
</div>
|
||||
|
||||
<!-- 模型管理折叠面板 -->
|
||||
<a-collapse v-if="currentAdapter.models && currentAdapter.models.length > 0" style="margin-bottom: 16px;">
|
||||
<a-collapse-panel key="models" header="模型管理">
|
||||
<!-- 模式选择 -->
|
||||
<div style="margin-bottom: 12px;">
|
||||
<span style="margin-right: 12px; color: #666;">过滤模式:</span>
|
||||
<a-radio-group :value="modelFilter.mode" @change="e => onModeChange(e.target.value)">
|
||||
<a-radio value="blacklist">黑名单</a-radio>
|
||||
<a-radio value="whitelist">白名单</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #999; margin-bottom: 12px;">
|
||||
{{ modelFilter.mode === 'blacklist' ? '关闭的模型将被禁用,其他模型可用' : '仅开启的模型可用,其他模型禁用' }}
|
||||
</div>
|
||||
|
||||
<!-- 模型列表 -->
|
||||
<div style="max-height: 300px; overflow-y: auto;">
|
||||
<div v-for="modelId in currentAdapter.models" :key="modelId"
|
||||
style="display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f0f0f0;">
|
||||
<span style="font-size: 13px; color: #333;">{{ modelId }}</span>
|
||||
<a-switch :checked="isModelEnabled(modelId)"
|
||||
@change="checked => toggleModel(modelId, checked)" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
|
||||
<!-- 其他配置项 -->
|
||||
<div v-if="!currentAdapter.configSchema || currentAdapter.configSchema.length === 0">
|
||||
<a-empty description="该适配器没有可配置项" />
|
||||
<a-empty v-if="!currentAdapter.models || currentAdapter.models.length === 0" description="该适配器没有可配置项" />
|
||||
</div>
|
||||
|
||||
<a-form layout="vertical" v-else>
|
||||
<a-form layout="vertical" v-if="currentAdapter.configSchema && currentAdapter.configSchema.length > 0">
|
||||
<template v-for="field in currentAdapter.configSchema" :key="field.key">
|
||||
<a-form-item :label="field.label" :required="field.required">
|
||||
<!-- 字符串输入 -->
|
||||
|
||||
Reference in New Issue
Block a user