From a22f37988e8aa7542b204bcb04b4d4bf6b5110e3 Mon Sep 17 00:00:00 2001
From: iwvw <2285740204@qq.com>
Date: Fri, 20 Mar 2026 20:29:46 +0800
Subject: [PATCH 1/6] feat(gemini-cli): add gemini-3.1-flash-lite-preview &
auto update matrix from quotas
---
modules/gemini-cli-api/gemini-matrix.json | 8 +++
modules/gemini-cli-api/router.js | 62 +++++++++++++++++++++++
2 files changed, 70 insertions(+)
diff --git a/modules/gemini-cli-api/gemini-matrix.json b/modules/gemini-cli-api/gemini-matrix.json
index 9f67c14..5b40c79 100644
--- a/modules/gemini-cli-api/gemini-matrix.json
+++ b/modules/gemini-cli-api/gemini-matrix.json
@@ -46,5 +46,13 @@
"search": false,
"fakeStream": false,
"antiTrunc": false
+ },
+ "gemini-3.1-flash-lite-preview": {
+ "base": true,
+ "maxThinking": false,
+ "noThinking": false,
+ "search": false,
+ "fakeStream": false,
+ "antiTrunc": false
}
}
\ No newline at end of file
diff --git a/modules/gemini-cli-api/router.js b/modules/gemini-cli-api/router.js
index 7e06193..d0aac29 100644
--- a/modules/gemini-cli-api/router.js
+++ b/modules/gemini-cli-api/router.js
@@ -292,6 +292,14 @@ const DEFAULT_MATRIX = {
fakeStream: false,
antiTrunc: false,
},
+ 'gemini-3.1-flash-lite-preview': {
+ base: true,
+ maxThinking: false,
+ noThinking: false,
+ search: false,
+ fakeStream: false,
+ antiTrunc: false,
+ },
};
// 辅助函数:读取矩阵配置
@@ -317,6 +325,33 @@ function saveMatrixConfig(config) {
}
}
+// 辅助函数:自动把新模型加入矩阵
+function autoAddModelsToMatrix(modelIds) {
+ if (!modelIds || modelIds.length === 0) return;
+ const matrixConfig = getMatrixConfig();
+ let matrixUpdated = false;
+
+ modelIds.forEach(modelId => {
+ // 忽略带特殊后缀的变体模型,防止将变体本身存入矩阵基础配置
+ if (modelId && !modelId.includes('/') && !modelId.includes('-search') && !modelId.includes('-thinking') && !matrixConfig[modelId]) {
+ matrixConfig[modelId] = {
+ base: true,
+ maxThinking: false,
+ noThinking: false,
+ search: false,
+ fakeStream: false,
+ antiTrunc: false,
+ };
+ matrixUpdated = true;
+ logger.info(`[GCLI] Auto-added new model from quota to matrix: ${modelId}`);
+ }
+ });
+
+ if (matrixUpdated) {
+ saveMatrixConfig(matrixConfig);
+ }
+}
+
/**
* 获取模型矩阵配置 (内部 API)
*/
@@ -594,6 +629,16 @@ router.get('/quotas', async (req, res) => {
// 使用 client 获取模型列表和额度
const quotas = await client.getQuotas(account);
+
+ // 自动将新获取的模型加入系统矩阵配置
+ try {
+ if (quotas) {
+ autoAddModelsToMatrix(Object.keys(quotas));
+ }
+ } catch (e) {
+ logger.error(`[quotas] Auto add models error: ${e.message}`);
+ }
+
res.json(quotas);
} catch (e) {
console.error('获取额度失败:', e);
@@ -664,6 +709,23 @@ router.get('/quotas/all', async (req, res) => {
})
);
+ // 自动将新获取的模型加入系统矩阵配置
+ try {
+ const modelIds = new Set();
+ results.forEach(result => {
+ if (result && result.buckets) {
+ result.buckets.forEach(bucket => {
+ if (bucket && bucket.modelId) {
+ modelIds.add(bucket.modelId);
+ }
+ });
+ }
+ });
+ autoAddModelsToMatrix(Array.from(modelIds));
+ } catch (e) {
+ logger.error(`[quotas/all] Auto add models error: ${e.message}`);
+ }
+
res.json(results.filter(Boolean));
} catch (e) {
console.error('[GCLI] Quota fetch error:', e);
From 55d92a90d21be7e115241019033604f716b95c7e Mon Sep 17 00:00:00 2001
From: iwvw <2285740204@qq.com>
Date: Fri, 27 Mar 2026 12:26:39 +0800
Subject: [PATCH 2/6] feat(deepseek): add system prompt, vision support and
token estimation
---
api-monitor.code-workspace | 11 ++--
modules/deepseek-api/deepseek-client.js | 15 ++++-
modules/deepseek-api/router.js | 61 ++++++++++++++++----
modules/deepseek-api/tokenizer.js | 74 +++++++++++++++++++++++++
package-lock.json | 21 ++++++-
5 files changed, 164 insertions(+), 18 deletions(-)
create mode 100644 modules/deepseek-api/tokenizer.js
diff --git a/api-monitor.code-workspace b/api-monitor.code-workspace
index ef9f5d2..517e0b2 100644
--- a/api-monitor.code-workspace
+++ b/api-monitor.code-workspace
@@ -1,7 +1,8 @@
{
- "folders": [
- {
- "path": "."
- }
- ]
+ "folders": [
+ {
+ "path": "."
+ }
+ ],
+ "settings": {}
}
\ No newline at end of file
diff --git a/modules/deepseek-api/deepseek-client.js b/modules/deepseek-api/deepseek-client.js
index 6e1cc23..4afe5e0 100644
--- a/modules/deepseek-api/deepseek-client.js
+++ b/modules/deepseek-api/deepseek-client.js
@@ -342,12 +342,23 @@ function buildCompletionPayload(sessionId, messages, model, options = {}) {
return '';
}).join('\n\n');
- // 最后一条用户消息作为 prompt
+ // 提取 system 消息并合并内容
+ const systemPrompt = messages
+ .filter(m => m.role === 'system')
+ .map(m => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content)))
+ .join('\n\n');
+
+ // 最后一条用户消息作为基础 prompt
const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
- const userPrompt = lastUserMsg
+ let userPrompt = lastUserMsg
? (typeof lastUserMsg.content === 'string' ? lastUserMsg.content : JSON.stringify(lastUserMsg.content))
: '';
+ // 如果有 system prompt,则附加到 userPrompt 前面
+ if (systemPrompt) {
+ userPrompt = `${systemPrompt}\n\n${userPrompt}`;
+ }
+
const payload = {
chat_session_id: sessionId,
prompt: userPrompt,
diff --git a/modules/deepseek-api/router.js b/modules/deepseek-api/router.js
index 2e6029d..ef315b5 100644
--- a/modules/deepseek-api/router.js
+++ b/modules/deepseek-api/router.js
@@ -10,6 +10,7 @@ const router = express.Router();
const storage = require('./storage');
const client = require('./deepseek-client');
const { parseSSEStream, collectStream } = require('./sse-parser');
+const { estimateMessagesTokens, estimateTokens } = require('./tokenizer');
const { createLogger } = require('../../src/utils/logger');
const logger = createLogger('DS-Router');
const { getSession, getSessionById } = require('../../src/services/session');
@@ -493,30 +494,61 @@ router.post(['/v1/chat/completions', '/chat/completions'], requireApiKey, async
if (typeof file_ids === 'string') file_ids = [file_ids];
const hasFiles = Array.isArray(file_ids) && file_ids.length > 0;
- // 获取可用账号
- let account;
+ // 1. 获取可用账号列表
const allAccounts = storage.getAccounts().filter(a => a.enable !== 0);
if (allAccounts.length === 0) {
return res.status(503).json({ error: { message: 'No enabled accounts available', type: 'service_unavailable' } });
}
- // 如果有文件,强制选择拥有该文件的账号
+ // 2. 选择账号
+ let account;
+ // 如果有文件,优先选择拥有该文件的账号
if (hasFiles) {
const fileCache = storage.getFileCache(file_ids[0]);
if (fileCache) {
account = allAccounts.find(a => a.id === fileCache.account_id);
- if (!account) {
- return res.status(400).json({ error: { message: `Account for file ${file_ids[0]} not found or disabled`, type: 'invalid_request_error' } });
- }
- logger.info(`[文件对话] 强制使用账号: ${account.name || account.id} (文件 ID: ${file_ids[0]})`);
}
}
-
- // 如果没选定账号(无文件或文件缓存未命中),则随机选
+ // 如果没选定账号,随机选
if (!account) {
account = allAccounts[Math.floor(Math.random() * allAccounts.length)];
}
+ // 3. 自动视觉:解析并上传 Base64 图片
+ const uploadedFileIds = [];
+ for (const msg of messages) {
+ if (Array.isArray(msg.content)) {
+ for (const part of msg.content) {
+ if (part.type === 'image_url' && part.image_url?.url?.startsWith('data:')) {
+ try {
+ const base64Data = part.image_url.url.split(',')[1];
+ const buffer = Buffer.from(base64Data, 'base64');
+ const mimeMatch = part.image_url.url.match(/^data:(image\/[a-z]+);base64,/);
+ const ext = mimeMatch ? mimeMatch[1].split('/')[1] : 'png';
+ const fileName = `vision_${Date.now()}.${ext}`;
+
+ const token = await client.getAccessToken(account.id);
+ // 视觉上传需要一个临时会话,如果此时还没有上下文,先创建一个
+ const uploadSessionId = await client.createSession(token);
+ const fileId = await client.uploadFile(token, uploadSessionId, buffer, fileName);
+ uploadedFileIds.push(fileId);
+ logger.info(`[自动视觉] 账号 ${account.name} 上传成功: ${fileId}`);
+ // 记录缓存
+ storage.saveFileCache(fileId, account.id, uploadSessionId, fileName, buffer.length);
+ } catch (uploadErr) {
+ logger.warn(`[自动视觉] 上传失败: ${uploadErr.message}`);
+ }
+ }
+ }
+ }
+ }
+ if (uploadedFileIds.length > 0) {
+ file_ids = [...(file_ids || []), ...uploadedFileIds];
+ }
+
+ // 计算预估 Prompt Token
+ const promptTokens = estimateMessagesTokens(messages);
+
try {
// 1. 获取 Token
let token;
@@ -630,6 +662,11 @@ router.post(['/v1/chat/completions', '/chat/completions'], requireApiKey, async
const doneChunk = {
id: completionId, object: 'chat.completion.chunk', created, model,
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
+ usage: {
+ prompt_tokens: promptTokens,
+ completion_tokens: estimateTokens(fullContent),
+ total_tokens: promptTokens + estimateTokens(fullContent)
+ }
};
res.write(`data: ${JSON.stringify(doneChunk)}\n\n`);
res.write('data: [DONE]\n\n');
@@ -675,7 +712,11 @@ router.post(['/v1/chat/completions', '/chat/completions'], requireApiKey, async
message: { role: 'assistant', content: result.content, reasoning_content: result.thinking },
finish_reason: 'stop',
}],
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
+ usage: {
+ prompt_tokens: promptTokens,
+ completion_tokens: estimateTokens(result.content),
+ total_tokens: promptTokens + estimateTokens(result.content)
+ },
};
// 记录会话继承 (保存 message_id 作为下次的 parent_id)
saveToSessionCache(result.content, sessionId, result.message_id);
diff --git a/modules/deepseek-api/tokenizer.js b/modules/deepseek-api/tokenizer.js
new file mode 100644
index 0000000..b669b69
--- /dev/null
+++ b/modules/deepseek-api/tokenizer.js
@@ -0,0 +1,74 @@
+/**
+ * 简易 Token 估算工具
+ * 用于在网页反代中模拟 OpenAI 的 usage 统计
+ */
+
+/**
+ * 估算文本的 Token 数量
+ * 规则:
+ * 1. 中文字符(包括标点)约为 2 tokens
+ * 2. 英文单词(按空格切分)约为 1.3 tokens
+ * 3. 其他非空白字符约为 1 token
+ */
+function estimateTokens(text) {
+ if (!text || typeof text !== 'string') return 0;
+
+ let count = 0;
+
+ // 1. 处理中文、日文、韩文(CJK)字符
+ const cjkMatches = text.match(/[\u4e00-\u9fa5\u3040-\u30ff\uac00-\ud7af\uff01-\uffee]/g);
+ if (cjkMatches) {
+ count += cjkMatches.length * 2;
+ }
+
+ // 2. 移除 CJK 字符后处理英文/数字
+ const remaining = text.replace(/[\u4e00-\u9fa5\u3040-\u30ff\uac00-\ud7af\uff01-\uffee]/g, ' ');
+ const words = remaining.trim().split(/\s+/);
+
+ for (const word of words) {
+ if (word.length === 0) continue;
+ // 简单模拟:长度超过 4 的单词按 [长/3] 计,短单词计 1
+ if (word.length > 4) {
+ count += Math.ceil(word.length / 3) * 1.3;
+ } else {
+ count += 1.3;
+ }
+ }
+
+ return Math.ceil(count);
+}
+
+/**
+ * 估算消息列表的总 Token 数
+ */
+function estimateMessagesTokens(messages) {
+ if (!Array.isArray(messages)) return 0;
+
+ let total = 0;
+ for (const msg of messages) {
+ // 角色名开销
+ total += 4;
+
+ if (typeof msg.content === 'string') {
+ total += estimateTokens(msg.content);
+ } else if (Array.isArray(msg.content)) {
+ for (const part of msg.content) {
+ if (part.type === 'text') {
+ total += estimateTokens(part.text);
+ } else if (part.type === 'image_url') {
+ // 图片在 OpenAI 中通常固定计费(如 85-1105 tokens)
+ // 网页版作为附件,这里统一模拟计为 500
+ total += 500;
+ }
+ }
+ }
+ }
+ // API 响应的基本开销
+ total += 3;
+ return total;
+}
+
+module.exports = {
+ estimateTokens,
+ estimateMessagesTokens
+};
diff --git a/package-lock.json b/package-lock.json
index f396eb3..a91a1b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1661,6 +1661,7 @@
"integrity": "sha512-ugkH3kOgjT8P1mTMY29yCOgEh+KuVMAn8uBxeY0aMqaUgIMysfpnFv+Aepp2CtvI9ygr22NC+OiKl+u+eEaQHw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.2",
"@pixi/display": "7.4.2"
@@ -1696,6 +1697,7 @@
"integrity": "sha512-UbMtgSEnyCOFPzbE6ThB9qopXxbZ5GCof2ArB4FXOC5Xi/83MOIIYg5kf5M8689C5HJMhg2SrJu3xLKppF+CMg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@pixi/color": "7.4.2",
"@pixi/constants": "7.4.2",
@@ -1717,6 +1719,7 @@
"integrity": "sha512-DaD0J7gIlNlzO0Fdlby/0OH+tB5LtCY6rgFeCBKVDnzmn8wKW3zYZRenWBSFJ0Psx6vLqXYkSIM/rcokaKviIw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.2"
}
@@ -1800,6 +1803,7 @@
"integrity": "sha512-gOXBbIUx6CRZP1fmsis2wLzzSsofrqmIHhbf1gIkZMIQaLsc9T7brj+PaLTTiOiyJgnvGN5j20RZnkERWWKV0Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.2"
}
@@ -1810,6 +1814,7 @@
"integrity": "sha512-80I3g813td7Fnzi7IJSiR3z8gZlKblk6WN+5z6WnscQROcNEpck6lgWS/Lf/IdeHB/FtUKJCbx7RzxkUhiRTvA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@pixi/core": "^7.0.0-X"
}
@@ -1840,6 +1845,7 @@
"integrity": "sha512-ykZiR59Gvj80UKs9qm7jeUTKvn+wWk6HBVJOmJbK9jFK5juakDWp7BbH26U78Q61EWj97kI1FdfcbMkuQ7rqkA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.2"
}
@@ -2149,6 +2155,7 @@
"integrity": "sha512-Ccf/OVQsB+HQV0Fyf5lwD+jk1jeU7uSIqEjbxenNNssmEdB7S5qlkTBV2EJTHT83+T6Z9OMOHsreJZerydpjeg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@pixi/core": "7.4.2",
"@pixi/display": "7.4.2"
@@ -3260,7 +3267,8 @@
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/accepts": {
"version": "1.3.8",
@@ -3280,6 +3288,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4369,6 +4378,7 @@
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10"
}
@@ -4790,6 +4800,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
+ "peer": true,
"engines": {
"node": ">=12"
}
@@ -5470,6 +5481,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5807,6 +5819,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -8739,6 +8752,7 @@
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -9880,6 +9894,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -10181,6 +10196,7 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -10298,6 +10314,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -10311,6 +10328,7 @@
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vitest/expect": "4.0.16",
"@vitest/mocker": "4.0.16",
@@ -10457,6 +10475,7 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.26",
"@vue/compiler-sfc": "3.5.26",
From 0571c2039e6198e2d7232ef70b95eff4bcbb5ae6 Mon Sep 17 00:00:00 2001
From: iwvw <2285740204@qq.com>
Date: Sun, 29 Mar 2026 23:43:36 +0800
Subject: [PATCH 3/6] ds
---
api-monitor.code-workspace | 4 +-
modules/deepseek-api/deepseek-client.js | 182 ++++++++++++++---
modules/deepseek-api/router.js | 230 +++++++++++++++++++++-
modules/deepseek-api/sse-parser.js | 112 ++++++++---
modules/deepseek-api/test-stability.js | 137 +++++++++++++
modules/gemini-cli-api/gemini-matrix.json | 2 +-
modules/uptime-api/storage.js | 24 +++
modules/zeabur-api/zeabur-api.js | 5 +-
src/routes/v1.js | 20 +-
9 files changed, 636 insertions(+), 80 deletions(-)
create mode 100644 modules/deepseek-api/test-stability.js
diff --git a/api-monitor.code-workspace b/api-monitor.code-workspace
index 517e0b2..876a149 100644
--- a/api-monitor.code-workspace
+++ b/api-monitor.code-workspace
@@ -3,6 +3,6 @@
{
"path": "."
}
- ],
- "settings": {}
+ ],
+ "settings": {}
}
\ No newline at end of file
diff --git a/modules/deepseek-api/deepseek-client.js b/modules/deepseek-api/deepseek-client.js
index 4afe5e0..088d610 100644
--- a/modules/deepseek-api/deepseek-client.js
+++ b/modules/deepseek-api/deepseek-client.js
@@ -21,6 +21,8 @@ const DS_SESSION_URL = 'https://chat.deepseek.com/api/v0/chat_session/create';
const DS_POW_URL = 'https://chat.deepseek.com/api/v0/chat/create_pow_challenge';
const DS_COMPLETION_URL = 'https://chat.deepseek.com/api/v0/chat/completion';
const DS_UPLOAD_URL = 'https://chat.deepseek.com/api/v0/chat/upload_file';
+const DS_DELETE_SESSION_URL = 'https://chat.deepseek.com/api/v0/chat_session/delete';
+const DS_DELETE_ALL_SESSIONS_URL = 'https://chat.deepseek.com/api/v0/chat_session/delete_all';
const BASE_HEADERS = {
'Host': 'chat.deepseek.com',
@@ -265,11 +267,34 @@ async function uploadFile(token, sessionId, fileBuffer, fileName) {
/**
* 检查 token 是否失效
*/
-function isTokenInvalid(status, code, msg) {
+function isTokenInvalid(status, code, msg, bizCode, bizMsg) {
if (status === 401 || status === 403) return true;
if (code === 40001 || code === 40002 || code === 40003) return true;
- const m = (msg || '').toLowerCase();
- return m.includes('token') || m.includes('unauthorized');
+ if (bizCode === 40001 || bizCode === 40002 || bizCode === 40003) return true;
+ const m = ((msg || '') + ' ' + (bizMsg || '')).toLowerCase();
+ return m.includes('token') || m.includes('unauthorized') ||
+ m.includes('expired') || m.includes('not login') ||
+ m.includes('login required') || m.includes('invalid jwt');
+}
+
+/**
+ * 判断是否应尝试刷新 Token(更精确的判断)
+ * 对 HTTP 200 但 biz_code 异常的情况做认证相关性检测
+ */
+function shouldAttemptRefresh(status, code, bizCode, msg, bizMsg) {
+ if (isTokenInvalid(status, code, msg, bizCode, bizMsg)) return true;
+ // HTTP 200/code=0 但 biz_code 非零时,检查是否是认证相关的失败
+ if (status === 200 && code === 0 && bizCode !== 0) {
+ const combined = ((msg || '') + ' ' + (bizMsg || '')).toLowerCase();
+ const authKeywords = [
+ 'auth', 'authorization', 'credential', 'expired',
+ 'invalid jwt', 'jwt', 'login', 'not login',
+ 'session expired', 'token', 'unauthorized',
+ '登录', '未登录', '认证', '凭证', '会话过期', '令牌',
+ ];
+ return authKeywords.some(kw => combined.includes(kw));
+ }
+ return false;
}
// ==================== 账号令牌管理 ====================
@@ -323,45 +348,110 @@ async function refreshToken(accountId) {
return token;
}
+// ==================== 消息格式化 (移植自 ds2api/internal/prompt/messages.go) ====================
+
+// Markdown 图片模式 - 移除 ! 前缀防止 DeepSeek 渲染异常
+const MARKDOWN_IMAGE_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
+
+/**
+ * 标准化消息内容,支持字符串、数组和其他格式
+ * 移植自 ds2api NormalizeContent()
+ */
+function normalizeContent(content) {
+ if (!content) return '';
+ if (typeof content === 'string') return content;
+ if (Array.isArray(content)) {
+ const parts = [];
+ for (const item of content) {
+ if (!item || typeof item !== 'object') continue;
+ const type = (item.type || '').toLowerCase().trim();
+ // 支持 text / output_text / input_text 类型
+ if (type === 'text' || type === 'output_text' || type === 'input_text') {
+ if (item.text) parts.push(item.text);
+ else if (item.content) parts.push(item.content);
+ }
+ }
+ return parts.join('\n');
+ }
+ return JSON.stringify(content);
+}
+
+/**
+ * 使用 DeepSeek 特殊标记格式化消息
+ * 移植自 ds2api MessagesPrepare()
+ *
+ * 核心改进:使用 DeepSeek 原生的特殊 token 标记来构建 prompt
+ * 这对 R1 深度思考的上下文理解有显著提升
+ */
+function messagesPrepare(messages) {
+ // 1. 预处理:标准化每条消息
+ const processed = messages.map(m => ({
+ role: m.role || 'user',
+ text: normalizeContent(m.content),
+ }));
+
+ if (processed.length === 0) return '';
+
+ // 2. 合并连续相同角色的消息
+ const merged = [];
+ for (const msg of processed) {
+ if (merged.length > 0 && merged[merged.length - 1].role === msg.role) {
+ merged[merged.length - 1].text += '\n\n' + msg.text;
+ } else {
+ merged.push({ ...msg });
+ }
+ }
+
+ // 3. 使用 DeepSeek 特殊标记格式化
+ const parts = [];
+ for (let i = 0; i < merged.length; i++) {
+ const m = merged[i];
+ switch (m.role) {
+ case 'assistant':
+ parts.push(`<|Assistant|>${m.text}<|end▁of▁sentence|>`);
+ break;
+ case 'tool':
+ if (i > 0) {
+ parts.push(`<|Tool|>${m.text}`);
+ } else {
+ parts.push(m.text);
+ }
+ break;
+ case 'system':
+ // 清晰的 system 边界能显著改善 R1 和 V3 的上下文理解
+ if (m.text.trim()) {
+ parts.push(`\n${m.text.trim()}\n\n\n`);
+ }
+ break;
+ case 'user':
+ // 始终为 user 消息添加标记,R1 推理在显式标记用户回合时效果最佳
+ parts.push(`<|User|>${m.text}`);
+ break;
+ default:
+ parts.push(m.text);
+ break;
+ }
+ }
+
+ // 4. 移除 Markdown 图片的 ! 前缀
+ return parts.join('').replace(MARKDOWN_IMAGE_RE, '[$1]($2)');
+}
+
/**
* 构建 DeepSeek Completion 请求体
+ * 使用增强的消息格式化(DeepSeek 特殊标记)
*/
function buildCompletionPayload(sessionId, messages, model, options = {}) {
- const settings = storage.getSettings();
-
// 确定是否启用思考模式
const isReasoner = model.includes('reasoner');
const isSearch = model.includes('search');
- // 构建提示词
- const prompt = messages.map(m => {
- if (typeof m.content === 'string') return m.content;
- if (Array.isArray(m.content)) {
- return m.content.map(p => p.text || '').join('\n');
- }
- return '';
- }).join('\n\n');
-
- // 提取 system 消息并合并内容
- const systemPrompt = messages
- .filter(m => m.role === 'system')
- .map(m => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content)))
- .join('\n\n');
-
- // 最后一条用户消息作为基础 prompt
- const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
- let userPrompt = lastUserMsg
- ? (typeof lastUserMsg.content === 'string' ? lastUserMsg.content : JSON.stringify(lastUserMsg.content))
- : '';
-
- // 如果有 system prompt,则附加到 userPrompt 前面
- if (systemPrompt) {
- userPrompt = `${systemPrompt}\n\n${userPrompt}`;
- }
+ // 使用增强的格式化器构建 prompt
+ const prompt = messagesPrepare(messages);
const payload = {
chat_session_id: sessionId,
- prompt: userPrompt,
+ prompt: prompt,
ref_file_ids: options.file_ids || [],
thinking_enabled: isReasoner,
search_enabled: isSearch,
@@ -375,6 +465,33 @@ function buildCompletionPayload(sessionId, messages, model, options = {}) {
return payload;
}
+// ==================== 会话清理 (移植自 ds2api/internal/deepseek/client_session_delete.go) ====================
+
+/**
+ * 删除单个 DeepSeek 会话
+ */
+async function deleteSession(token, sessionId) {
+ if (!sessionId) return;
+ try {
+ const headers = { ...BASE_HEADERS, authorization: `Bearer ${token}` };
+ await postJSON(DS_DELETE_SESSION_URL, headers, { chat_session_id: sessionId });
+ } catch (e) {
+ logger.warn(`删除会话失败: ${e.message}`);
+ }
+}
+
+/**
+ * 删除所有 DeepSeek 会话
+ */
+async function deleteAllSessions(token) {
+ try {
+ const headers = { ...BASE_HEADERS, authorization: `Bearer ${token}` };
+ await postJSON(DS_DELETE_ALL_SESSIONS_URL, headers, {});
+ } catch (e) {
+ logger.warn(`删除所有会话失败: ${e.message}`);
+ }
+}
+
module.exports = {
login,
createSession,
@@ -384,5 +501,8 @@ module.exports = {
refreshToken,
buildCompletionPayload,
uploadFile,
+ deleteSession,
+ deleteAllSessions,
+ normalizeContent,
BASE_HEADERS,
};
diff --git a/modules/deepseek-api/router.js b/modules/deepseek-api/router.js
index ef315b5..007c000 100644
--- a/modules/deepseek-api/router.js
+++ b/modules/deepseek-api/router.js
@@ -17,6 +17,149 @@ const { getSession, getSessionById } = require('../../src/services/session');
const fs = require('fs');
const path = require('path');
+// ==================== 常量 (移植自 ds2api) ====================
+
+const KEEPALIVE_INTERVAL_MS = 5000; // KeepAlive 心跳间隔
+const STREAM_IDLE_TIMEOUT_MS = 30000; // 流式空闲超时
+const MAX_KEEPALIVE_NO_CONTENT = 10; // 最大无内容心跳次数
+
+// ==================== 智能模型解析 (移植自 ds2api/config/models.go) ====================
+
+// 内置默认模型别名 - 让客户端用任何主流模型名都能通过
+const DEFAULT_MODEL_ALIASES = {
+ 'gpt-4o': 'deepseek-chat',
+ 'gpt-4.1': 'deepseek-chat',
+ 'gpt-4.1-mini': 'deepseek-chat',
+ 'gpt-4.1-nano': 'deepseek-chat',
+ 'gpt-5': 'deepseek-chat',
+ 'gpt-5-mini': 'deepseek-chat',
+ 'gpt-5-codex': 'deepseek-reasoner',
+ 'o1': 'deepseek-reasoner',
+ 'o1-mini': 'deepseek-reasoner',
+ 'o3': 'deepseek-reasoner',
+ 'o3-mini': 'deepseek-reasoner',
+ 'claude-sonnet-4-5': 'deepseek-chat',
+ 'claude-haiku-4-5': 'deepseek-chat',
+ 'claude-opus-4-6': 'deepseek-reasoner',
+ 'claude-3-5-sonnet': 'deepseek-chat',
+ 'claude-3-5-haiku': 'deepseek-chat',
+ 'claude-3-opus': 'deepseek-reasoner',
+ 'gemini-2.5-pro': 'deepseek-chat',
+ 'gemini-2.5-flash': 'deepseek-chat',
+ 'llama-3.1-70b-instruct': 'deepseek-chat',
+ 'qwen-max': 'deepseek-chat',
+};
+
+const SUPPORTED_DS_MODELS = new Set([
+ 'deepseek-chat', 'deepseek-reasoner',
+ 'deepseek-chat-search', 'deepseek-reasoner-search',
+]);
+
+const KNOWN_FAMILY_PREFIXES = [
+ 'gpt-', 'o1', 'o3', 'claude-', 'gemini-', 'llama-', 'qwen-', 'mistral-', 'command-',
+];
+
+/**
+ * 解析模型名称 → 真实 DeepSeek 模型
+ * 优先级: 数据库重定向 > 默认别名 > 智能推断 > 原始值
+ */
+function resolveModel(requestedModel) {
+ if (!requestedModel) return { resolved: 'deepseek-chat', original: '' };
+ const model = requestedModel.toLowerCase().trim();
+
+ // 1. 原生支持的 DeepSeek 模型
+ if (SUPPORTED_DS_MODELS.has(model)) {
+ return { resolved: model, original: requestedModel };
+ }
+
+ // 2. 数据库存储的重定向
+ const redirects = storage.getModelRedirects();
+ const redirect = redirects.find(r => r.source_model.toLowerCase() === model);
+ if (redirect && SUPPORTED_DS_MODELS.has(redirect.target_model.toLowerCase())) {
+ return { resolved: redirect.target_model.toLowerCase(), original: requestedModel };
+ }
+
+ // 3. 默认别名
+ if (DEFAULT_MODEL_ALIASES[model]) {
+ return { resolved: DEFAULT_MODEL_ALIASES[model], original: requestedModel };
+ }
+
+ // 4. 智能推断:对已知模型系列进行关键词匹配
+ const isKnownFamily = KNOWN_FAMILY_PREFIXES.some(p => model.startsWith(p));
+ if (isKnownFamily) {
+ const useReasoner = model.includes('reason') || model.includes('reasoner') ||
+ model.startsWith('o1') || model.startsWith('o3') ||
+ model.includes('opus') || model.includes('r1');
+ const useSearch = model.includes('search');
+
+ if (useReasoner && useSearch) return { resolved: 'deepseek-reasoner-search', original: requestedModel };
+ if (useReasoner) return { resolved: 'deepseek-reasoner', original: requestedModel };
+ if (useSearch) return { resolved: 'deepseek-chat-search', original: requestedModel };
+ return { resolved: 'deepseek-chat', original: requestedModel };
+ }
+
+ // 5. 未知模型:默认 deepseek-chat
+ return { resolved: 'deepseek-chat', original: requestedModel };
+}
+
+// ==================== 智能账号选择器 (移植自 ds2api/account/pool) ====================
+
+const accountInUse = new Map(); // accountId -> 当前并发数
+let lastSelectedIdx = -1; // 轮转索引
+const MAX_INFLIGHT_PER_ACCOUNT = 2; // 每个账号最大并发
+
+/**
+ * 智能选择可用账号
+ * 策略: 有Token优先 → 并发控制 → 轮转使用
+ */
+function selectAccount(allAccounts, preferAccountId = null) {
+ if (allAccounts.length === 0) return null;
+
+ // 如果指定了优先账号且可用
+ if (preferAccountId) {
+ const preferred = allAccounts.find(a => a.id === preferAccountId);
+ if (preferred && canUseAccount(preferred.id)) {
+ return preferred;
+ }
+ }
+
+ // 分离有Token和无Token的账号
+ const withToken = allAccounts.filter(a => a.token && a.token.trim());
+ const withoutToken = allAccounts.filter(a => !a.token || !a.token.trim());
+
+ // 先尝试有Token的,再尝试无Token的
+ for (const pool of [withToken, withoutToken]) {
+ if (pool.length === 0) continue;
+ // 轮转选择 (round-robin)
+ for (let i = 0; i < pool.length; i++) {
+ const idx = (lastSelectedIdx + 1 + i) % pool.length;
+ const account = pool[idx];
+ if (canUseAccount(account.id)) {
+ lastSelectedIdx = idx;
+ accountInUse.set(account.id, (accountInUse.get(account.id) || 0) + 1);
+ return account;
+ }
+ }
+ }
+
+ // 所有账号都满负载,不再强行回退(避免挤占官方单账号并发导致静默失败)
+ return null;
+}
+
+function canUseAccount(accountId) {
+ return (accountInUse.get(accountId) || 0) < MAX_INFLIGHT_PER_ACCOUNT;
+}
+
+function releaseAccount(accountId) {
+ if (!accountId) return;
+ const count = accountInUse.get(accountId) || 0;
+ if (count <= 1) {
+ accountInUse.delete(accountId);
+ } else {
+ accountInUse.set(accountId, count - 1);
+ }
+}
+
// ==================== Session 缓存 (用于维系上下文对话) ====================
// 外部客户端可能将 reasoning_content 与 content 合并发回
// 因此使用子串包含匹配,同时持久化到数据库以支持重启后续接
@@ -485,10 +628,12 @@ router.post(['/v1/chat/completions', '/chat/completions'], requireApiKey, async
try {
let { model, messages, stream, file_ids } = req.body;
- // 处理模型重定向
- const redirects = storage.getModelRedirects();
- const redirect = redirects.find(r => r.source_model === model);
- if (redirect) model = redirect.target_model;
+ // 智能模型解析 (移植自 ds2api)
+ const { resolved: resolvedModel, original: originalModel } = resolveModel(model);
+ model = resolvedModel;
+ if (originalModel !== resolvedModel) {
+ logger.info(`[模型映射] ${originalModel} → ${resolvedModel}`);
+ }
// 如果 file_ids 是数组但为空,忽略它;如果是字符串,转为数组
if (typeof file_ids === 'string') file_ids = [file_ids];
@@ -500,18 +645,21 @@ router.post(['/v1/chat/completions', '/chat/completions'], requireApiKey, async
return res.status(503).json({ error: { message: 'No enabled accounts available', type: 'service_unavailable' } });
}
- // 2. 选择账号
+ // 2. 智能选择账号 (移植自 ds2api account pool)
let account;
// 如果有文件,优先选择拥有该文件的账号
if (hasFiles) {
const fileCache = storage.getFileCache(file_ids[0]);
if (fileCache) {
- account = allAccounts.find(a => a.id === fileCache.account_id);
+ account = selectAccount(allAccounts, fileCache.account_id);
}
}
- // 如果没选定账号,随机选
+ // 智能轮转选择
+ if (!account) {
+ account = selectAccount(allAccounts);
+ }
if (!account) {
- account = allAccounts[Math.floor(Math.random() * allAccounts.length)];
+ return res.status(503).json({ error: { message: 'All accounts are busy', type: 'service_unavailable' } });
}
// 3. 自动视觉:解析并上传 Base64 图片
@@ -633,12 +781,49 @@ router.post(['/v1/chat/completions', '/chat/completions'], requireApiKey, async
let fullReasoning = '';
let firstTokenTime = null;
let responseMessageId = null;
+ let hasReceivedContent = false;
+ let lastContentTime = Date.now();
+ let keepaliveCount = 0;
+
+ // 流式超时保护 (移植自 ds2api stream engine)
+ const keepaliveTimer = setInterval(() => {
+ if (!hasReceivedContent) {
+ keepaliveCount++;
+ if (keepaliveCount >= MAX_KEEPALIVE_NO_CONTENT) {
+ logger.warn(`[流式超时] 连续 ${MAX_KEEPALIVE_NO_CONTENT} 次心跳无内容,终止流`);
+ clearInterval(keepaliveTimer);
+ if (!res.writableEnded) {
+ res.write(`: keepalive timeout\n\n`);
+ res.end();
+ }
+ releaseAccount(account.id);
+ return;
+ }
+ }
+ if (hasReceivedContent && (Date.now() - lastContentTime) > STREAM_IDLE_TIMEOUT_MS) {
+ logger.warn(`[流式超时] 空闲超过 ${STREAM_IDLE_TIMEOUT_MS}ms,终止流`);
+ clearInterval(keepaliveTimer);
+ if (!res.writableEnded) {
+ res.end();
+ }
+ releaseAccount(account.id);
+ return;
+ }
+ // 发送 SSE 注释保持连接
+ if (!res.writableEnded) {
+ res.write(`: keepalive\n\n`);
+ if (res.flush) res.flush();
+ }
+ }, KEEPALIVE_INTERVAL_MS);
parseSSEStream(
dsResponse,
isReasoner,
(type, text) => {
if (firstTokenTime === null) firstTokenTime = Date.now() - startTime;
+ hasReceivedContent = true;
+ lastContentTime = Date.now();
+ keepaliveCount = 0;
if (type === 'thinking') {
fullReasoning += text;
@@ -658,6 +843,8 @@ router.post(['/v1/chat/completions', '/chat/completions'], requireApiKey, async
if (res.flush) res.flush();
},
() => {
+ clearInterval(keepaliveTimer);
+
// 发送结束标记
const doneChunk = {
id: completionId, object: 'chat.completion.chunk', created, model,
@@ -675,6 +862,17 @@ router.post(['/v1/chat/completions', '/chat/completions'], requireApiKey, async
// 记录会话继承 (保存 message_id 作为下次的 parent_id)
saveToSessionCache(fullContent, sessionId, responseMessageId);
+ // 会话自动清理 (移植自 ds2api)
+ const autoDelete = storage.getSetting('AUTO_DELETE_SESSIONS');
+ if (autoDelete === '1' || autoDelete === 'true') {
+ client.deleteAllSessions(token).catch(e =>
+ logger.warn(`[自动清理] 删除会话失败: ${e.message}`)
+ );
+ }
+
+ // 释放账号
+ releaseAccount(account.id);
+
// 记录日志
storage.recordLog({
accountId: account.id, model, is_balanced: req.lb,
@@ -689,7 +887,9 @@ router.post(['/v1/chat/completions', '/chat/completions'], requireApiKey, async
});
},
(err) => {
+ clearInterval(keepaliveTimer);
logger.error(`Stream error: ${err.message}`);
+ releaseAccount(account.id);
if (!res.headersSent) {
res.status(500).json({ error: { message: err.message } });
} else {
@@ -721,6 +921,17 @@ router.post(['/v1/chat/completions', '/chat/completions'], requireApiKey, async
// 记录会话继承 (保存 message_id 作为下次的 parent_id)
saveToSessionCache(result.content, sessionId, result.message_id);
+ // 会话自动清理 (移植自 ds2api)
+ const autoDelete = storage.getSetting('AUTO_DELETE_SESSIONS');
+ if (autoDelete === '1' || autoDelete === 'true') {
+ client.deleteAllSessions(token).catch(e =>
+ logger.warn(`[自动清理] 删除会话失败: ${e.message}`)
+ );
+ }
+
+ // 释放账号
+ releaseAccount(account.id);
+
// 记录日志
storage.recordLog({
accountId: account.id, model, is_balanced: req.lb,
@@ -735,6 +946,9 @@ router.post(['/v1/chat/completions', '/chat/completions'], requireApiKey, async
} catch (error) {
logger.error(`[DS] Account ${account.name} failed: ${error.message}`);
+ // 释放账号
+ releaseAccount(account.id);
+
// 记录错误日志
storage.recordLog({
accountId: account.id, model, is_balanced: req.lb,
diff --git a/modules/deepseek-api/sse-parser.js b/modules/deepseek-api/sse-parser.js
index 5edca74..acaef77 100644
--- a/modules/deepseek-api/sse-parser.js
+++ b/modules/deepseek-api/sse-parser.js
@@ -21,6 +21,7 @@ const logger = createLogger('DS-SSE');
const SKIP_PATHS = new Set([
'quasi_status',
'response/status',
+ 'response/search_status',
]);
const SKIP_CONTAINS = [
@@ -28,8 +29,31 @@ const SKIP_CONTAINS = [
'elapsed_secs',
'pending_fragment',
'conversation_mode',
+ 'fragments/-1/status',
+ 'fragments/-2/status',
+ 'fragments/-3/status',
];
+// Unicode 上标数字映射
+const SUPERSCRIPT_DIGITS = { '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹' };
+
+/**
+ * 将 [citation:N] 转换为带超链接的 Unicode 上标
+ * 有 URL 时: [citation:8] → [⁽⁸⁾](url)
+ * 无 URL 时: [citation:8] → ⁽⁸⁾
+ */
+function formatCitations(text, searchResults) {
+ return text.replace(/\[citation:(\d+)\]/g, (_, num) => {
+ const idx = parseInt(num, 10);
+ const superNum = num.split('').map(d => SUPERSCRIPT_DIGITS[d] || d).join('');
+ const sup = `⁽${superNum}⁾`;
+ // 在已收集的搜索结果中查找对应来源 URL
+ const ref = searchResults.find(r => (r.cite_index || r.index) === idx);
+ const url = ref && (ref.url || ref.link || ref.href);
+ return url ? `[${sup}](${url})` : sup;
+ });
+}
+
/**
* 解析 DeepSeek SSE 流
*/
@@ -37,6 +61,12 @@ function parseSSEStream(response, isReasoner, onData, onEnd, onError, onMeta) {
let buffer = '';
response.setEncoding('utf8');
+ // 包装 onData:自动转换 [citation:N] → [⁽ᴺ⁾](url)
+ const originalOnData = onData;
+ onData = (type, text) => {
+ originalOnData(type, formatCitations(text, searchResults));
+ };
+
// 深度思考模式下从 thinking 开始,否则从 content 开始
let currentType = isReasoner ? 'thinking' : 'content';
let currentEvent = '';
@@ -62,9 +92,7 @@ function parseSSEStream(response, isReasoner, onData, onEnd, onError, onMeta) {
if (!trimmed.startsWith('data: ')) continue;
const dataStr = trimmed.slice(6).trim();
- try {
- require('fs').appendFileSync('ds_raw_log.txt', dataStr + '\n');
- } catch (e) {}
+
if (!dataStr || dataStr === '[DONE]' || dataStr === '{}') continue;
@@ -76,30 +104,45 @@ function parseSSEStream(response, isReasoner, onData, onEnd, onError, onMeta) {
const data = JSON.parse(dataStr);
let handled = false;
- // --- 1. 初始 response 对象 ---
- if (data.v && typeof data.v === 'object' && data.v.response) {
- const resp = data.v.response;
- if (onMeta && resp.message_id) {
- onMeta({ message_id: resp.message_id });
+ // --- 1. 初始 response 对象与业务错误检测 ---
+ if (data.v && typeof data.v === 'object') {
+ const bizCode = data.v.code || data.v.response?.code;
+ const bizMsg = data.v.msg || data.v.response?.msg;
+
+ if (bizCode !== undefined && bizCode !== 0 && bizCode !== "0") {
+ logger.warn(`[DeepSeek 业务错误] 代码: ${bizCode}, 信息: ${bizMsg}`);
+ onError(new Error(bizMsg || `DeepSeek Error (${bizCode})`));
+ return;
}
- if (resp.fragments && Array.isArray(resp.fragments)) {
- for (const frag of resp.fragments) {
- const fType = frag.type;
- const fContent = frag.content || frag.v || "";
- if (fContent) {
- if (fType === 'THINK') {
- onData('thinking', fContent);
- handled = true;
- } else if (['RESPONSE', 'CONTENT', 'TEXT', 'ANSWER'].includes(fType)) {
- onData('content', fContent);
- handled = true;
- } else if (fType === 'SEARCH' && Array.isArray(frag.results)) {
- frag.results.forEach(r => searchResults.push(r));
+
+ if (data.v.response) {
+ const resp = data.v.response;
+ if (onMeta && resp.message_id) {
+ onMeta({ message_id: resp.message_id });
+ }
+ if (resp.fragments && Array.isArray(resp.fragments)) {
+ for (const frag of resp.fragments) {
+ const fType = frag.type;
+ const fContent = frag.content || frag.v || "";
+ if (fContent) {
+ if (fType === 'THINK') {
+ onData('thinking', fContent);
+ handled = true;
+ } else if (['RESPONSE', 'CONTENT', 'TEXT', 'ANSWER'].includes(fType)) {
+ onData('content', fContent);
+ handled = true;
+ } else if (fType === 'SEARCH') {
+ const results = frag.results || frag.search_results || frag.references || [];
+ if (Array.isArray(results)) {
+ logger.debug(`[搜索] 初始 SEARCH fragment 收集到 ${results.length} 条结果`);
+ results.forEach(r => searchResults.push(r));
+ }
+ }
}
}
}
+ if (handled) continue;
}
- if (handled) continue;
}
// --- 2. 识别路径并动态确定类型 ---
@@ -116,8 +159,16 @@ function parseSSEStream(response, isReasoner, onData, onEnd, onError, onMeta) {
if (handled) continue;
}
- // 搜索结果
- if (data.p.match(/response\/fragments\/\d+\/results/) && Array.isArray(data.v)) {
+ // 搜索结果 (覆盖各种可能的路径)
+ if (data.p.match(/response\/fragments\/(-?\d+)\/(results|search_results|references)/) && Array.isArray(data.v)) {
+ logger.debug(`[搜索] 增量路径 ${data.p} 收集到 ${data.v.length} 条结果`);
+ data.v.forEach(item => searchResults.push(item));
+ continue;
+ }
+
+ // 搜索结果也可能出现在 response/search_results 等路径
+ if ((data.p === 'response/search_results' || data.p === 'search_results') && Array.isArray(data.v)) {
+ logger.debug(`[搜索] 顶层路径 ${data.p} 收集到 ${data.v.length} 条结果`);
data.v.forEach(item => searchResults.push(item));
continue;
}
@@ -185,15 +236,18 @@ function parseSSEStream(response, isReasoner, onData, onEnd, onError, onMeta) {
}
}
+ logger.debug(`[搜索] 流结束, 共收集 ${searchResults.length} 条搜索结果`);
if (searchResults.length > 0) {
+ logger.info(`[搜索引用] 共 ${searchResults.length} 条来源, 样本: ${JSON.stringify(searchResults[0])}`);
let refText = '\n\n---\n**参考资料:**\n';
const seen = new Set();
- searchResults.slice().sort((a, b) => (a.cite_index || 0) - (b.cite_index || 0)).forEach(item => {
- const idx = item.cite_index || 0;
- if (!seen.has(idx)) {
+ searchResults.slice().sort((a, b) => (a.cite_index || a.index || 0) - (b.cite_index || b.index || 0)).forEach(item => {
+ const idx = item.cite_index || item.index || 0;
+ const url = item.url || item.link || item.href || '';
+ if (!seen.has(idx) && url) {
seen.add(idx);
- const title = item.title || item.site_name || item.url;
- refText += `[${idx}] [${title}](${item.url})\n`;
+ const title = item.title || item.site_name || item.name || url;
+ refText += `[${idx}] [${title}](${url})\n`;
}
});
onData('content', refText);
diff --git a/modules/deepseek-api/test-stability.js b/modules/deepseek-api/test-stability.js
new file mode 100644
index 0000000..36379af
--- /dev/null
+++ b/modules/deepseek-api/test-stability.js
@@ -0,0 +1,137 @@
+/**
+ * DeepSeek 模型稳定性测试脚本
+ * 测试所有 4 个模型 + 别名映射
+ */
+
+const http = require('http');
+
+const BASE_URL = 'http://localhost:5173';
+const API_KEY = 'ssln5014.';
+
+function request(model, message, stream = false) {
+ return new Promise((resolve, reject) => {
+ const body = JSON.stringify({
+ model,
+ messages: [{ role: 'user', content: message }],
+ stream,
+ });
+
+ const options = {
+ hostname: 'localhost',
+ port: 5173,
+ path: '/v1/chat/completions',
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${API_KEY}`,
+ 'Content-Length': Buffer.byteLength(body),
+ },
+ };
+
+ const startTime = Date.now();
+ const req = http.request(options, (res) => {
+ let data = '';
+ res.setEncoding('utf8');
+ res.on('data', chunk => { data += chunk; });
+ res.on('end', () => {
+ const elapsed = Date.now() - startTime;
+ resolve({ status: res.statusCode, data, elapsed, headers: res.headers });
+ });
+ });
+
+ req.on('error', (e) => reject(e));
+ req.setTimeout(120000, () => {
+ req.destroy(new Error('timeout'));
+ });
+ req.write(body);
+ req.end();
+ });
+}
+
+async function testModel(model, label, stream = false) {
+ const prefix = stream ? '[流式]' : '[非流式]';
+ console.log(`\n${'='.repeat(60)}`);
+ console.log(`${prefix} 测试模型: ${model} (${label})`);
+ console.log('='.repeat(60));
+
+ try {
+ const prompt = stream
+ ? '用一句话介绍你自己'
+ : '回复:TEST_OK';
+
+ const result = await request(model, prompt, stream);
+
+ if (result.status === 200) {
+ if (stream) {
+ // 解析 SSE 数据
+ const lines = result.data.split('\n').filter(l => l.startsWith('data: '));
+ let content = '';
+ let reasoning = '';
+ let hasUsage = false;
+ for (const line of lines) {
+ const dataStr = line.slice(6).trim();
+ if (dataStr === '[DONE]') continue;
+ try {
+ const chunk = JSON.parse(dataStr);
+ const delta = chunk.choices?.[0]?.delta || {};
+ if (delta.content) content += delta.content;
+ if (delta.reasoning_content) reasoning += delta.reasoning_content;
+ if (chunk.usage) hasUsage = true;
+ } catch (_) { }
+ }
+ console.log(`✅ 成功 | ${result.elapsed}ms`);
+ if (reasoning) console.log(`💭 推理: ${reasoning.slice(0, 100)}...`);
+ console.log(`📝 回复: ${content.slice(0, 200)}`);
+ console.log(`📊 SSE chunks: ${lines.length}, usage: ${hasUsage ? '✅' : '❌'}`);
+ } else {
+ const json = JSON.parse(result.data);
+ const msg = json.choices?.[0]?.message || {};
+ console.log(`✅ 成功 | ${result.elapsed}ms`);
+ if (msg.reasoning_content) {
+ console.log(`💭 推理: ${msg.reasoning_content.slice(0, 100)}...`);
+ }
+ console.log(`📝 回复: ${(msg.content || '').slice(0, 200)}`);
+ console.log(`📊 用量: prompt=${json.usage?.prompt_tokens}, completion=${json.usage?.completion_tokens}, total=${json.usage?.total_tokens}`);
+ }
+ } else {
+ console.log(`❌ 失败 | HTTP ${result.status} | ${result.elapsed}ms`);
+ console.log(` 错误: ${result.data.slice(0, 300)}`);
+ }
+ } catch (e) {
+ console.log(`❌ 异常: ${e.message}`);
+ }
+}
+
+async function main() {
+ console.log('🚀 DeepSeek 模型稳定性测试');
+ console.log(`🔗 端点: ${BASE_URL}/v1/chat/completions`);
+ console.log(`🔑 API Key: ${API_KEY.slice(0, 4)}****`);
+ console.log(`⏰ 时间: ${new Date().toLocaleString()}`);
+
+ // 1. 基础模型 - 非流式
+ await testModel('deepseek-chat', '基础对话', false);
+
+ // 2. 基础模型 - 流式
+ await testModel('deepseek-chat', '基础对话-流式', true);
+
+ // 3. 推理模型 - 流式
+ await testModel('deepseek-reasoner', '深度思考', true);
+
+ // 4. 搜索模型 - 流式
+ await testModel('deepseek-chat-search', '联网搜索', true);
+
+ // 5. 模型别名测试 — gpt-4o → deepseek-chat
+ await testModel('gpt-4o', '别名映射 gpt-4o→chat', true);
+
+ // 6. 模型别名测试 — o3 → deepseek-reasoner
+ await testModel('o3', '别名映射 o3→reasoner', true);
+
+ console.log('\n' + '='.repeat(60));
+ console.log('🏁 测试完成');
+ console.log('='.repeat(60));
+}
+
+main().catch(e => {
+ console.error('测试脚本异常:', e);
+ process.exit(1);
+});
diff --git a/modules/gemini-cli-api/gemini-matrix.json b/modules/gemini-cli-api/gemini-matrix.json
index 5b40c79..fe59052 100644
--- a/modules/gemini-cli-api/gemini-matrix.json
+++ b/modules/gemini-cli-api/gemini-matrix.json
@@ -48,7 +48,7 @@
"antiTrunc": false
},
"gemini-3.1-flash-lite-preview": {
- "base": true,
+ "base": false,
"maxThinking": false,
"noThinking": false,
"search": false,
diff --git a/modules/uptime-api/storage.js b/modules/uptime-api/storage.js
index 7e215c6..afddb0e 100644
--- a/modules/uptime-api/storage.js
+++ b/modules/uptime-api/storage.js
@@ -17,6 +17,25 @@ class UptimeStorage {
constructor() {
// 延迟初始化,在首次调用时检查迁移
this._migrated = false;
+ this._columnsChecked = false;
+ }
+
+ _checkColumns() {
+ if (this._columnsChecked) return;
+ this._columnsChecked = true;
+ const db = getDb();
+ try {
+ // 检查 uptime_monitors 是否有 created_at 列
+ const info = db.prepare("PRAGMA table_info(uptime_monitors)").all();
+ const hasCreatedAt = info.some(c => c.name === 'created_at');
+ if (!hasCreatedAt) {
+ logger.info('正在为 uptime_monitors 添加 created_at 和 updated_at 列...');
+ db.prepare("ALTER TABLE uptime_monitors ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP").run();
+ db.prepare("ALTER TABLE uptime_monitors ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP").run();
+ }
+ } catch (e) {
+ logger.warn(`检查列定义失败: ${e.message}`);
+ }
}
/**
@@ -122,6 +141,7 @@ class UptimeStorage {
// ==================== 监控项 CRUD ====================
getAll() {
+ this._checkColumns();
this._ensureMigrated();
const db = getDb();
const monitors = db.prepare('SELECT * FROM uptime_monitors ORDER BY created_at DESC').all();
@@ -129,6 +149,7 @@ class UptimeStorage {
}
getActive() {
+ this._checkColumns();
this._ensureMigrated();
const db = getDb();
const monitors = db.prepare('SELECT * FROM uptime_monitors WHERE active = 1').all();
@@ -136,6 +157,7 @@ class UptimeStorage {
}
getById(id) {
+ this._checkColumns();
this._ensureMigrated();
const db = getDb();
const m = db.prepare('SELECT * FROM uptime_monitors WHERE id = ?').get(id);
@@ -143,6 +165,7 @@ class UptimeStorage {
}
create(data) {
+ this._checkColumns();
this._ensureMigrated();
const db = getDb();
const result = db.prepare(`
@@ -166,6 +189,7 @@ class UptimeStorage {
}
update(id, data) {
+ this._checkColumns();
this._ensureMigrated();
const db = getDb();
const fields = [];
diff --git a/modules/zeabur-api/zeabur-api.js b/modules/zeabur-api/zeabur-api.js
index 9e98aac..10f5830 100644
--- a/modules/zeabur-api/zeabur-api.js
+++ b/modules/zeabur-api/zeabur-api.js
@@ -217,10 +217,11 @@ async function fetchAccountData(token) {
}
`;
+ // serviceCostsThisMonth 已被 Zeabur 移除,此处不再查询
const serviceCostsQuery = `
query {
me {
- serviceCostsThisMonth
+ _id
}
}
`;
@@ -265,7 +266,7 @@ async function fetchAccountData(token) {
}
const aihub = aihubData?.data?.aihubTenant || {};
- const serviceCosts = serviceCostsData?.data?.me?.serviceCostsThisMonth || 0;
+ const serviceCosts = 0; // 该字段已废弃,默认返回 0
// 在后端直接转换地域为中文
const projects = queryProjects.map(project => {
diff --git a/src/routes/v1.js b/src/routes/v1.js
index 43b8c4e..9a90de1 100644
--- a/src/routes/v1.js
+++ b/src/routes/v1.js
@@ -336,14 +336,20 @@ const dispatch = async (req, res, next) => {
// --- A. 精确匹配前缀优先 ---
- // 尝试匹配 DeepSeek 前缀或 deepseek-* 模型名
+ // 尝试匹配 DeepSeek 前缀或 deepseek-* 模型名及已知别名 (gpt-*, o1, o3, claude- 等)
if (dsEnabled) {
- if (dsPrefix && fullModelId.startsWith(dsPrefix)) {
- req.body.model = fullModelId.substring(dsPrefix.length);
- return dsRouter(req, res, next);
- }
- // 无前缀时,通过模型名关键词匹配
- if (!dsPrefix && (fullModelId.startsWith('deepseek-') || fullModelId.startsWith('deepseek/'))) {
+ const isDsPrefixMatch = dsPrefix && fullModelId.startsWith(dsPrefix);
+ const dsAliases = ['gpt-', 'o1', 'o3', 'claude-', 'llama-', 'qwen-'];
+ const isDsAliasMatch = !dsPrefix && (
+ fullModelId.startsWith('deepseek-') ||
+ fullModelId.startsWith('deepseek/') ||
+ dsAliases.some(p => fullModelId.startsWith(p))
+ );
+
+ if (isDsPrefixMatch || isDsAliasMatch) {
+ if (isDsPrefixMatch) {
+ req.body.model = fullModelId.substring(dsPrefix.length);
+ }
return dsRouter(req, res, next);
}
}
From c04698230080ddc357cc0006196f284e002a28b8 Mon Sep 17 00:00:00 2001
From: iwvw <2285740204@qq.com>
Date: Mon, 30 Mar 2026 00:11:09 +0800
Subject: [PATCH 4/6] ds
---
modules/gemini-cli-api/gemini-matrix.json | 8 --
modules/gemini-cli-api/router.js | 100 +++++++++++++++++-----
2 files changed, 77 insertions(+), 31 deletions(-)
diff --git a/modules/gemini-cli-api/gemini-matrix.json b/modules/gemini-cli-api/gemini-matrix.json
index fe59052..9f67c14 100644
--- a/modules/gemini-cli-api/gemini-matrix.json
+++ b/modules/gemini-cli-api/gemini-matrix.json
@@ -46,13 +46,5 @@
"search": false,
"fakeStream": false,
"antiTrunc": false
- },
- "gemini-3.1-flash-lite-preview": {
- "base": false,
- "maxThinking": false,
- "noThinking": false,
- "search": false,
- "fakeStream": false,
- "antiTrunc": false
}
}
\ No newline at end of file
diff --git a/modules/gemini-cli-api/router.js b/modules/gemini-cli-api/router.js
index d0aac29..50b9c69 100644
--- a/modules/gemini-cli-api/router.js
+++ b/modules/gemini-cli-api/router.js
@@ -71,7 +71,38 @@ const autoCheckService = {
return;
}
- // 获取要检测的模型列表(复用现有逻辑)
+ // 1. 自动全量更新模型矩阵 (每 12 小时执行一次)
+ const settings = storage.getSettings();
+ const lastSyncTime = parseInt(settings.lastAutoSyncTime) || 0;
+ const nowMs = Date.now();
+ const twelveHoursMs = 12 * 3600 * 1000;
+
+ if (nowMs - lastSyncTime > twelveHoursMs) {
+ logger.info('[GCLI AutoCheck] 达到同步周期,开始自动全量更新模型矩阵...');
+ try {
+ // 仅拉取启用账号的额度信息进行聚合
+ const enabledAccounts = accounts.filter(a => a.enable !== 0);
+ const results = await Promise.all(
+ enabledAccounts.map(async (account) => {
+ try {
+ return await getAccountQuota(account, true); // 强制刷新
+ } catch (e) {
+ return null;
+ }
+ })
+ );
+ const allUpstreamModels = aggregateUpstreamModels(results);
+ if (allUpstreamModels.length > 0) {
+ syncModelsToMatrix(allUpstreamModels, true); // 全量同步并自动清理
+ storage.updateSetting('lastAutoSyncTime', nowMs.toString());
+ logger.info(`[GCLI AutoCheck] 自动清理完成,当前上游可用模型数: ${allUpstreamModels.length}`);
+ }
+ } catch (syncErr) {
+ logger.error(`[GCLI AutoCheck] 自动同步矩阵失败: ${syncErr.message}`);
+ }
+ }
+
+ // 2. 获取要检测的模型列表 (原有常规逻辑)
const set = new Set();
const redirects = storage.getModelRedirects();
if (Array.isArray(redirects)) {
@@ -87,7 +118,6 @@ const autoCheckService = {
let modelsToCheck = Array.from(set);
// 应用禁用模型过滤
- const settings = storage.getSettings();
if (settings.disabledCheckModels) {
try {
const disabledModels = JSON.parse(settings.disabledCheckModels);
@@ -99,8 +129,8 @@ const autoCheckService = {
if (modelsToCheck.length === 0) {
modelsToCheck = [
- 'gemini-2.5-pro',
- 'gemini-2.5-flash',
+ 'gemini-2.1-pro',
+ 'gemini-2.0-flash-exp',
'gemini-1.5-pro',
'gemini-1.5-flash',
];
@@ -325,14 +355,33 @@ function saveMatrixConfig(config) {
}
}
-// 辅助函数:自动把新模型加入矩阵
-function autoAddModelsToMatrix(modelIds) {
- if (!modelIds || modelIds.length === 0) return;
+// 辅助函数:聚合上游获取到的模型 ID
+function aggregateUpstreamModels(results) {
+ const modelIds = new Set();
+ if (!Array.isArray(results)) return [];
+
+ results.forEach(result => {
+ if (result && result.buckets) {
+ result.buckets.forEach(bucket => {
+ if (bucket && bucket.modelId) {
+ modelIds.add(bucket.modelId);
+ }
+ });
+ }
+ });
+
+ return Array.from(modelIds);
+}
+
+// 辅助函数:将模型同步到矩阵 (支持全量同步/自动清理)
+function syncModelsToMatrix(upstreamModelIds, isFullSync = false) {
+ if (!upstreamModelIds || upstreamModelIds.length === 0) return;
const matrixConfig = getMatrixConfig();
let matrixUpdated = false;
- modelIds.forEach(modelId => {
- // 忽略带特殊后缀的变体模型,防止将变体本身存入矩阵基础配置
+ // 1. 添加并更新上游存在的模型
+ upstreamModelIds.forEach(modelId => {
+ // 忽略带特殊后缀的变体模型
if (modelId && !modelId.includes('/') && !modelId.includes('-search') && !modelId.includes('-thinking') && !matrixConfig[modelId]) {
matrixConfig[modelId] = {
base: true,
@@ -347,6 +396,18 @@ function autoAddModelsToMatrix(modelIds) {
}
});
+ // 2. 全量同步模式下:自动清理上游已不存在的模型
+ if (isFullSync) {
+ Object.keys(matrixConfig).forEach(modelId => {
+ // 如果模型不在上游列表,则执行清理 (移除 DEFAULT_MATRIX 强制保留限制)
+ if (!upstreamModelIds.includes(modelId)) {
+ delete matrixConfig[modelId];
+ matrixUpdated = true;
+ logger.info(`[GCLI] Auto-removed stale model from matrix: ${modelId}`);
+ }
+ });
+ }
+
if (matrixUpdated) {
saveMatrixConfig(matrixConfig);
}
@@ -630,10 +691,10 @@ router.get('/quotas', async (req, res) => {
// 使用 client 获取模型列表和额度
const quotas = await client.getQuotas(account);
- // 自动将新获取的模型加入系统矩阵配置
+ // 自动将新获取的模型加入系统矩阵配置 (只增不减)
try {
if (quotas) {
- autoAddModelsToMatrix(Object.keys(quotas));
+ syncModelsToMatrix(Object.keys(quotas), false);
}
} catch (e) {
logger.error(`[quotas] Auto add models error: ${e.message}`);
@@ -709,19 +770,12 @@ router.get('/quotas/all', async (req, res) => {
})
);
- // 自动将新获取的模型加入系统矩阵配置
+ // 自动将新获取的模型加入系统矩阵配置 (全量同步/自动清理)
try {
- const modelIds = new Set();
- results.forEach(result => {
- if (result && result.buckets) {
- result.buckets.forEach(bucket => {
- if (bucket && bucket.modelId) {
- modelIds.add(bucket.modelId);
- }
- });
- }
- });
- autoAddModelsToMatrix(Array.from(modelIds));
+ const modelIds = aggregateUpstreamModels(results);
+ if (modelIds.length > 0) {
+ syncModelsToMatrix(modelIds, true);
+ }
} catch (e) {
logger.error(`[quotas/all] Auto add models error: ${e.message}`);
}
From b6a865abdcb6603b3940a3a89731588dabe7df74 Mon Sep 17 00:00:00 2001
From: iwvw <2285740204@qq.com>
Date: Thu, 2 Apr 2026 11:20:07 +0800
Subject: [PATCH 5/6] checkout: temporary commit for worktree checkout
From f9687608a7fda588e1985ec243b4f365b0e02d81 Mon Sep 17 00:00:00 2001
From: iwvw <2285740204@qq.com>
Date: Fri, 3 Apr 2026 12:50:53 +0800
Subject: [PATCH 6/6] checkout: temporary commit for worktree checkout
---
api-monitor.code-workspace | 3 +
eslint.config.js | 8 +-
modules/ai-draw-api/service.js | 2 +-
modules/antigravity-api/antigravity-client.js | 2 +-
modules/antigravity-api/router.js | 6 +-
modules/deepseek-api/sse-parser.js | 10 +-
modules/gemini-cli-api/gemini-client.js | 6 +-
modules/gemini-cli-api/router.js | 6 +-
modules/notification-api/migrate.js | 2 +-
modules/server-api/agent-service.js | 2 +-
modules/tencent-api/tencent-api.js | 4 +-
modules/uptime-api/storage.js | 6 +-
src/css/dashboard.css | 14 +-
src/css/styles.css | 169 +++---------------
src/db/models/System.js | 2 +-
src/js/composables/usePagination.js | 2 +-
src/js/modules/dashboard.js | 2 +-
src/js/modules/host.js | 2 +-
src/js/modules/openai.js | 4 +-
src/js/modules/ssh.js | 4 +-
vite.config.mjs | 29 +--
21 files changed, 86 insertions(+), 199 deletions(-)
diff --git a/api-monitor.code-workspace b/api-monitor.code-workspace
index 876a149..56de368 100644
--- a/api-monitor.code-workspace
+++ b/api-monitor.code-workspace
@@ -2,6 +2,9 @@
"folders": [
{
"path": "."
+ },
+ {
+ "path": "../AmGo"
}
],
"settings": {}
diff --git a/eslint.config.js b/eslint.config.js
index ae50a37..cdc6add 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -98,7 +98,7 @@ module.exports = [
},
rules: {
// 基础规则
- // TODO: 后续逐步修复未使用变量后,可将此规则改为 'error'
+ // TODO: 后续逐步修复未使用变量后,可将 varsIgnorePattern 收窄
'no-unused-vars': [
'warn',
{
@@ -121,9 +121,9 @@ module.exports = [
'eol-last': 'off',
// 最佳实践
- eqeqeq: 'off', // 允许 == (项目中很多是有意的)
- 'no-var': 'warn',
- 'prefer-const': 'off',
+ eqeqeq: ['error', 'always', { null: 'ignore' }], // 强制 ===,但允许 == null 简写
+ 'no-var': 'error',
+ 'prefer-const': ['warn', { destructuring: 'all' }],
'no-throw-literal': 'warn',
'no-prototype-builtins': 'off',
'no-useless-escape': 'off', // 很多正则有意使用转义
diff --git a/modules/ai-draw-api/service.js b/modules/ai-draw-api/service.js
index dc9ab15..e0d8751 100644
--- a/modules/ai-draw-api/service.js
+++ b/modules/ai-draw-api/service.js
@@ -349,7 +349,7 @@ class AIDrawService {
const title = titleMatch ? titleMatch[1].trim() : 'Untitled';
// 移除脚本和样式标签,提取文本内容
- let content = html
+ const content = html
.replace(/