@xcanwin/manyoyo 8.7.0 → 8.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agent-adapters/index.js +23 -6
- package/lib/init-config.js +4 -1
- package/lib/json5-text-edit.js +51 -0
- package/lib/model-service-containers.js +63 -0
- package/lib/model-service-import.js +98 -0
- package/lib/model-services.js +313 -0
- package/lib/provider-presets.js +45 -15
- package/lib/setup-cli.js +18 -5
- package/lib/setup-config.js +6 -1
- package/lib/setup.js +52 -5
- package/lib/web/index.html +87 -85
- package/lib/web/model-services.js +248 -0
- package/lib/web/server.js +97 -77
- package/manyoyo.example.json +3 -1
- package/package.json +1 -1
package/lib/provider-presets.js
CHANGED
|
@@ -4,71 +4,74 @@
|
|
|
4
4
|
// 只收录实测可用(Base URL 不带 Key 请求得到 401)且官方文档给了接入方式的服务商。
|
|
5
5
|
// claude:Anthropic 兼容接口;codex:Responses 接口(只收官方文档确认支持 Responses 的)。
|
|
6
6
|
// models 只作“模型”输入框的占位提示,不会自动写入配置;defaultModel 是官方文档要求必须填的,用户可改。
|
|
7
|
-
// Codex
|
|
8
|
-
// 由 lib/agent-adapters 的 withCodexBaseUrl
|
|
7
|
+
// Codex 0.164 实测:exec 不认 OPENAI_BASE_URL / OPENAI_API_KEY / OPENAI_MODEL,向导写的仍是环境变量,
|
|
8
|
+
// 由 lib/agent-adapters 的 withCodexBaseUrl 在容器里展开成自定义 model_provider + --model 才生效,所以 Codex 条目必须有 defaultModel。
|
|
9
|
+
// extraEnv:这家服务必须附带的变量(写进同一个运行配置)。Claude Code 2.1.310 的“工具搜索”会把工具收进 ToolSearch,
|
|
10
|
+
// 智谱的兼容接口用不了(实测模型看不到 Bash),要 ENABLE_TOOL_SEARCH=false;DeepSeek 实测不需要。其余没有 Key 实测的先带上(代价只是多耗一点 token)。
|
|
9
11
|
// 智谱的接口把鉴权失败写在 200 响应体里(`{"code":401,...}`),其余三家直接返回 HTTP 401。
|
|
10
12
|
const PROVIDER_PRESETS = [
|
|
11
13
|
{
|
|
12
14
|
id: 'deepseek',
|
|
13
15
|
label: 'DeepSeek',
|
|
14
16
|
docUrl: 'https://api-docs.deepseek.com/guides/coding_agents',
|
|
15
|
-
claude: { baseUrl: 'https://api.deepseek.com/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN', models: ['deepseek-flash[1m]'], defaultModel: 'deepseek-flash[1m]' }
|
|
17
|
+
claude: { baseUrl: 'https://api.deepseek.com/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN', models: ['deepseek-flash[1m]'], defaultModel: 'deepseek-flash[1m]' },
|
|
18
|
+
codex: { baseUrl: 'https://api.deepseek.com', keyEnv: 'OPENAI_API_KEY', models: ['deepseek-flash'], defaultModel: 'deepseek-flash' }
|
|
16
19
|
},
|
|
17
20
|
{
|
|
18
21
|
id: 'zhipu',
|
|
19
22
|
label: '智谱 GLM',
|
|
20
23
|
docUrl: 'https://docs.bigmodel.cn/cn/coding-plan/tool/claude',
|
|
21
|
-
claude: { baseUrl: 'https://open.bigmodel.cn/api/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN' },
|
|
22
|
-
codex: { baseUrl: 'https://open.bigmodel.cn/api/v1', keyEnv: 'OPENAI_API_KEY', docUrl: 'https://docs.bigmodel.cn/cn/coding-plan/tool/codex' }
|
|
24
|
+
claude: { baseUrl: 'https://open.bigmodel.cn/api/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN', extraEnv: { ENABLE_TOOL_SEARCH: 'false' } },
|
|
25
|
+
codex: { baseUrl: 'https://open.bigmodel.cn/api/v1', keyEnv: 'OPENAI_API_KEY', models: ['glm-5.3'], defaultModel: 'glm-5.3', docUrl: 'https://docs.bigmodel.cn/cn/coding-plan/tool/codex' }
|
|
23
26
|
},
|
|
24
27
|
{
|
|
25
28
|
id: 'zai',
|
|
26
29
|
label: 'Z.ai(智谱国际)',
|
|
27
30
|
docUrl: 'https://docs.z.ai/devpack/tool/claude',
|
|
28
|
-
claude: { baseUrl: 'https://api.z.ai/api/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN' }
|
|
31
|
+
claude: { baseUrl: 'https://api.z.ai/api/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN', extraEnv: { ENABLE_TOOL_SEARCH: 'false' } }
|
|
29
32
|
},
|
|
30
33
|
{
|
|
31
34
|
id: 'kimi',
|
|
32
35
|
label: 'Kimi',
|
|
33
36
|
docUrl: 'https://platform.kimi.com/docs/guide/claude-code-kimi.md',
|
|
34
|
-
claude: { baseUrl: 'https://api.moonshot.cn/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN' },
|
|
37
|
+
claude: { baseUrl: 'https://api.moonshot.cn/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN', extraEnv: { ENABLE_TOOL_SEARCH: 'false' } },
|
|
35
38
|
codex: { baseUrl: 'https://api.moonshot.cn/v1', keyEnv: 'OPENAI_API_KEY', docUrl: 'https://platform.kimi.com/docs/guide/codex-kimi.md' }
|
|
36
39
|
},
|
|
37
40
|
{
|
|
38
41
|
id: 'kimi-code',
|
|
39
42
|
label: 'Kimi Code(订阅,海外)',
|
|
40
43
|
docUrl: 'https://www.kimi.com/code/docs/en/third-party-tools/claude-code.html',
|
|
41
|
-
claude: { baseUrl: 'https://api.kimi.ai/coding/', keyEnv: 'ANTHROPIC_API_KEY' }
|
|
44
|
+
claude: { baseUrl: 'https://api.kimi.ai/coding/', keyEnv: 'ANTHROPIC_API_KEY', extraEnv: { ENABLE_TOOL_SEARCH: 'false' } }
|
|
42
45
|
},
|
|
43
46
|
{
|
|
44
47
|
id: 'aliyun',
|
|
45
48
|
label: '阿里云百炼',
|
|
46
49
|
docUrl: 'https://help.aliyun.com/zh/model-studio/claude-code',
|
|
47
|
-
claude: { baseUrl: 'https://coding.dashscope.aliyuncs.com/apps/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN' }
|
|
50
|
+
claude: { baseUrl: 'https://coding.dashscope.aliyuncs.com/apps/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN', extraEnv: { ENABLE_TOOL_SEARCH: 'false' } }
|
|
48
51
|
},
|
|
49
52
|
{
|
|
50
53
|
id: 'tencent',
|
|
51
54
|
label: '腾讯云',
|
|
52
55
|
docUrl: 'https://cloud.tencent.com/document/product/1823/130097',
|
|
53
|
-
claude: { baseUrl: 'https://api.lkeap.cloud.tencent.com/coding/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN', models: ['glm-5'] }
|
|
56
|
+
claude: { baseUrl: 'https://api.lkeap.cloud.tencent.com/coding/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN', models: ['glm-5'], extraEnv: { ENABLE_TOOL_SEARCH: 'false' } }
|
|
54
57
|
},
|
|
55
58
|
{
|
|
56
59
|
id: 'volcengine',
|
|
57
60
|
label: '火山方舟',
|
|
58
61
|
docUrl: 'https://www.volcengine.com/docs/82379/1928262',
|
|
59
|
-
claude: { baseUrl: 'https://ark.cn-beijing.volces.com/api/coding', keyEnv: 'ANTHROPIC_AUTH_TOKEN' },
|
|
62
|
+
claude: { baseUrl: 'https://ark.cn-beijing.volces.com/api/coding', keyEnv: 'ANTHROPIC_AUTH_TOKEN', extraEnv: { ENABLE_TOOL_SEARCH: 'false' } },
|
|
60
63
|
codex: { baseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3', keyEnv: 'OPENAI_API_KEY' }
|
|
61
64
|
},
|
|
62
65
|
{
|
|
63
66
|
id: 'minimax',
|
|
64
67
|
label: 'MiniMax',
|
|
65
68
|
docUrl: 'https://platform.minimax.cn/docs/token-plan/claude-code',
|
|
66
|
-
claude: { baseUrl: 'https://api.minimax.cn/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN' },
|
|
69
|
+
claude: { baseUrl: 'https://api.minimax.cn/anthropic', keyEnv: 'ANTHROPIC_AUTH_TOKEN', extraEnv: { ENABLE_TOOL_SEARCH: 'false' } },
|
|
67
70
|
codex: { baseUrl: 'https://api.minimax.cn/v1', keyEnv: 'OPENAI_API_KEY', docUrl: 'https://platform.minimax.cn/docs/token-plan/codex' }
|
|
68
71
|
}
|
|
69
72
|
];
|
|
70
73
|
|
|
71
|
-
/** 某个 Agent 可用的预设(扁平:{id, label, docUrl, baseUrl, keyEnv, models, defaultModel}) */
|
|
74
|
+
/** 某个 Agent 可用的预设(扁平:{id, label, docUrl, baseUrl, keyEnv, models, defaultModel, extraEnv}) */
|
|
72
75
|
function providersForAgent(agentId) {
|
|
73
76
|
return PROVIDER_PRESETS
|
|
74
77
|
.filter(item => item[agentId])
|
|
@@ -79,8 +82,35 @@ function providersForAgent(agentId) {
|
|
|
79
82
|
baseUrl: item[agentId].baseUrl,
|
|
80
83
|
keyEnv: item[agentId].keyEnv,
|
|
81
84
|
models: [...(item[agentId].models || [])],
|
|
82
|
-
defaultModel: item[agentId].defaultModel || ''
|
|
85
|
+
defaultModel: item[agentId].defaultModel || '',
|
|
86
|
+
extraEnv: { ...(item[agentId].extraEnv || {}) }
|
|
83
87
|
}));
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
|
|
90
|
+
function normalizeUrl(value) {
|
|
91
|
+
return String(value || '').trim().replace(/\/+$/, '').toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 按 Base URL 找某个 Agent 的预设,对不上返回 null */
|
|
95
|
+
function findProviderByBaseUrl(agentId, baseUrl) {
|
|
96
|
+
const url = normalizeUrl(baseUrl);
|
|
97
|
+
if (!url) return null;
|
|
98
|
+
return providersForAgent(agentId).find(item => normalizeUrl(item.baseUrl) === url) || null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Codex 接了第三方(env 里有 OPENAI_BASE_URL)时的模型下拉:只列这家的预设模型和当前模型。
|
|
103
|
+
* `codex app-server` 的目录永远是 OpenAI 的模型名,选了第三方会直接报错。不是第三方返回 null。
|
|
104
|
+
*/
|
|
105
|
+
function thirdPartyCodexModels(env) {
|
|
106
|
+
const source = env && typeof env === 'object' ? env : {};
|
|
107
|
+
const baseUrl = typeof source.OPENAI_BASE_URL === 'string' ? source.OPENAI_BASE_URL.trim() : '';
|
|
108
|
+
if (!baseUrl) return null;
|
|
109
|
+
const provider = findProviderByBaseUrl('codex', baseUrl);
|
|
110
|
+
const names = [...(provider ? provider.models : [])];
|
|
111
|
+
const current = typeof source.OPENAI_MODEL === 'string' ? source.OPENAI_MODEL.trim() : '';
|
|
112
|
+
if (current && !names.includes(current)) names.push(current);
|
|
113
|
+
return names.map(value => ({ value, label: value, description: provider ? provider.label : '' }));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { PROVIDER_PRESETS, providersForAgent, findProviderByBaseUrl, thirdPartyCodexModels };
|
package/lib/setup-cli.js
CHANGED
|
@@ -10,6 +10,7 @@ const readline = require('readline');
|
|
|
10
10
|
const { Writable } = require('stream');
|
|
11
11
|
const JSON5 = require('json5');
|
|
12
12
|
const { listSetupAgents, buildAgentRunProfile, validateSetupPassword, listMirrorPresets } = require('./setup');
|
|
13
|
+
const { uniqueRunName } = require('./model-services');
|
|
13
14
|
const { baseRawOf, buildAgentConfigRaw, buildPasswordConfigRaw, shouldWriteMirrors, buildMirrorsConfigRaw } = require('./setup-config');
|
|
14
15
|
const { normalizeMirrors } = require('./runtime-normalizers');
|
|
15
16
|
const { writeConfigFileSecure } = require('./secure-file');
|
|
@@ -155,6 +156,8 @@ async function runSetupCli(deps) {
|
|
|
155
156
|
keyName = agent.requiredAnyOf[keyIndex];
|
|
156
157
|
}
|
|
157
158
|
let defaultModel = '';
|
|
159
|
+
// 名字里的服务商标记:选了预设用预设 id,官方 Key 是 official,自定义地址是 custom
|
|
160
|
+
let nameTag = modeIndex === 1 ? 'custom' : 'official';
|
|
158
161
|
if (modeIndex === 1) {
|
|
159
162
|
const providers = agent.providers || [];
|
|
160
163
|
const baseUrlItems = [
|
|
@@ -168,6 +171,8 @@ async function runSetupCli(deps) {
|
|
|
168
171
|
env[agent.baseUrlKey] = provider.baseUrl;
|
|
169
172
|
keyName = provider.keyEnv;
|
|
170
173
|
defaultModel = provider.defaultModel;
|
|
174
|
+
nameTag = provider.id;
|
|
175
|
+
Object.assign(env, provider.extraEnv || {});
|
|
171
176
|
log(`官方说明:${provider.docUrl}`);
|
|
172
177
|
} else if (baseIndex < providers.length + agent.baseUrlPresets.length) {
|
|
173
178
|
env[agent.baseUrlKey] = agent.baseUrlPresets[baseIndex - providers.length].value;
|
|
@@ -188,7 +193,11 @@ async function runSetupCli(deps) {
|
|
|
188
193
|
env[keyName] = key;
|
|
189
194
|
const model = String(await prompter.ask(defaultModel
|
|
190
195
|
? `模型(${agent.modelKey},回车使用 ${defaultModel}): `
|
|
191
|
-
: `模型(${agent.modelKey}
|
|
196
|
+
: `模型(${agent.modelKey},${agent.id === 'codex' && modeIndex === 1 ? '必填' : '可选,回车跳过'}): `)).trim() || defaultModel;
|
|
197
|
+
if (!model && agent.id === 'codex' && modeIndex === 1) {
|
|
198
|
+
log('Codex 接兼容服务必须指定模型,已退出,没有改动任何配置。');
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
192
201
|
if (model) env[agent.modelKey] = model;
|
|
193
202
|
|
|
194
203
|
log('\n第 3 / 4 步 · 工作目录');
|
|
@@ -215,14 +224,18 @@ async function runSetupCli(deps) {
|
|
|
215
224
|
const exists = fs.existsSync(configPath);
|
|
216
225
|
const snapshot = { exists, raw: exists ? fs.readFileSync(configPath, 'utf-8') : '' };
|
|
217
226
|
const parsed = exists && snapshot.raw.trim() ? JSON5.parse(snapshot.raw) : {};
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
227
|
+
// 已有同名运行配置时新增一套(名字带服务商),绝不覆盖;第一次配置仍叫 Agent 的名字
|
|
228
|
+
const runs = parsed.runs && typeof parsed.runs === 'object' ? parsed.runs : {};
|
|
229
|
+
const runName = runs[agent.id] ? uniqueRunName(runs, `${agent.id}-${nameTag}`) : agent.id;
|
|
230
|
+
// 默认工作目录根不写进运行配置:新建容器各自建 <根>/<时间> 子目录
|
|
231
|
+
const profile = buildAgentRunProfile(agent.id, env, { hostPath: hostPath === path.resolve(defaultWork) ? '' : hostPath });
|
|
232
|
+
if (runName !== agent.id) profile.containerName = `my-${runName}-{now}`;
|
|
233
|
+
let nextRaw = buildAgentConfigRaw(baseRawOf(snapshot), runName, profile);
|
|
221
234
|
nextRaw = buildPasswordConfigRaw(nextRaw, password);
|
|
222
235
|
if (shouldWriteMirrors(parsed, mirrors)) nextRaw = buildMirrorsConfigRaw(nextRaw, normalizeMirrors(mirrors));
|
|
223
236
|
JSON5.parse(nextRaw);
|
|
224
237
|
writeConfigFileSecure(configPath, nextRaw);
|
|
225
|
-
log(`已保存到 ${configPath}(权限 0600
|
|
238
|
+
log(`已保存到 ${configPath}(权限 0600):运行配置 ${runName}(${agent.id})、工作目录 ${hostPath}、登录密码${Object.keys(mirrors).length > 0 ? '、软件源' : ''}。`);
|
|
226
239
|
} catch (error) {
|
|
227
240
|
if (error instanceof InputClosedError) {
|
|
228
241
|
log('输入已结束,没有改动任何配置。');
|
package/lib/setup-config.js
CHANGED
|
@@ -38,4 +38,9 @@ function buildBooleanConfigRaw(baseRaw, key, value) {
|
|
|
38
38
|
return upsertValueByPath(baseRaw, [key], value ? 'true' : 'false');
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// 向导“跳过”要落盘,否则刷新后又弹
|
|
42
|
+
function buildSetupSkippedConfigRaw(baseRaw) {
|
|
43
|
+
return upsertValueByPath(baseRaw, ['serve', 'setupSkipped'], 'true');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { buildSetupSkippedConfigRaw, buildBooleanConfigRaw, EMPTY_CONFIG_RAW, baseRawOf, buildAgentConfigRaw, buildPasswordConfigRaw, shouldWriteMirrors, buildMirrorsConfigRaw, buildAgentInstructionsConfigRaw };
|
package/lib/setup.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const { AGENT_ENV_SCHEMA } = require('./init-config');
|
|
4
4
|
const { parseEnvEntry } = require('./runtime-normalizers');
|
|
5
5
|
const { providersForAgent } = require('./provider-presets');
|
|
6
|
+
const { findAdapterByAlias } = require('./agent-adapters');
|
|
7
|
+
const { resolveAgentProgram } = require('./agent-resume');
|
|
6
8
|
|
|
7
9
|
const SETUP_AGENTS = Object.keys(AGENT_ENV_SCHEMA);
|
|
8
10
|
|
|
@@ -32,10 +34,31 @@ function hasNonEmptyRequiredKey(spec, env) {
|
|
|
32
34
|
return spec.requiredAnyOf.some(key => typeof source[key] === 'string' && source[key].trim() !== '');
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
//
|
|
37
|
+
// 运行配置属于哪个 Agent:yolo → shell / agentPromptCommand 里的程序 → 名字前缀;认不出返回空串
|
|
38
|
+
function identifyRunAgent(name, run) {
|
|
39
|
+
if (!isPlainObject(run)) return '';
|
|
40
|
+
const profile = run;
|
|
41
|
+
const byYolo = findAdapterByAlias(profile.yolo);
|
|
42
|
+
if (byYolo && AGENT_ENV_SCHEMA[byYolo.id]) return byYolo.id;
|
|
43
|
+
for (const text of [profile.shell, profile.agentPromptCommand]) {
|
|
44
|
+
if (typeof text !== 'string' || !text.trim()) continue;
|
|
45
|
+
const program = resolveAgentProgram(text);
|
|
46
|
+
if (AGENT_ENV_SCHEMA[program]) return program;
|
|
47
|
+
}
|
|
48
|
+
const prefix = String(name || '').toLowerCase().split(/[-_.\s]/)[0];
|
|
49
|
+
const byName = findAdapterByAlias(prefix);
|
|
50
|
+
return byName && AGENT_ENV_SCHEMA[byName.id] ? byName.id : '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 已有可用的运行配置:不论叫什么名字,能认出 Agent 且必填变量(任意一个)非空
|
|
36
54
|
function getConfiguredAgents(config) {
|
|
37
55
|
const runs = isPlainObject(config && config.runs) ? config.runs : {};
|
|
38
|
-
|
|
56
|
+
const found = new Set();
|
|
57
|
+
for (const [name, run] of Object.entries(runs)) {
|
|
58
|
+
const agent = identifyRunAgent(name, run);
|
|
59
|
+
if (agent && hasNonEmptyRequiredKey(AGENT_ENV_SCHEMA[agent], run.env)) found.add(agent);
|
|
60
|
+
}
|
|
61
|
+
return SETUP_AGENTS.filter(id => found.has(id));
|
|
39
62
|
}
|
|
40
63
|
|
|
41
64
|
/**
|
|
@@ -70,13 +93,18 @@ function buildAgentRunProfile(agent, envInput, options = {}) {
|
|
|
70
93
|
const existingEnv = isPlainObject(existing.env) ? existing.env : {};
|
|
71
94
|
const profile = { ...existing };
|
|
72
95
|
profile.env = { ...existingEnv, ...provided };
|
|
96
|
+
// 换成订阅令牌时,同一个运行配置里的 API Key 类变量和 Base URL 要删掉:Claude Code 同时有 Key 时优先用 Key,订阅就被绕过了
|
|
97
|
+
if (agent === 'claude' && provided.CLAUDE_CODE_OAUTH_TOKEN && !['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY'].some(key => provided[key])) {
|
|
98
|
+
for (const key of ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL']) delete profile.env[key];
|
|
99
|
+
}
|
|
73
100
|
if (!hasNonEmptyRequiredKey(spec, profile.env)) {
|
|
74
101
|
throw new Error(`请填写 ${spec.requiredAnyOf.join(' 或 ')}`);
|
|
75
102
|
}
|
|
76
103
|
if (profile.containerName === undefined) profile.containerName = `my-${agent}-{now}`;
|
|
77
104
|
if (profile.yolo === undefined) profile.yolo = spec.yolo;
|
|
105
|
+
// 只写用户选定的目录。默认的工作目录根不写:新建容器按 {quickChatPath}/{now} 各建一个子目录,
|
|
106
|
+
// 写了根目录,对话框里目录是空白、每个新容器都会把整个工作目录根挂进去
|
|
78
107
|
if (options.hostPath) profile.hostPath = options.hostPath;
|
|
79
|
-
else if (profile.hostPath === undefined && options.defaultHostPath) profile.hostPath = options.defaultHostPath;
|
|
80
108
|
return profile;
|
|
81
109
|
}
|
|
82
110
|
|
|
@@ -86,6 +114,16 @@ const CONNECTION_RULES = [
|
|
|
86
114
|
pattern: /\b(401|403)\b|unauthori[sz]ed|invalid[ _-]?(x-)?api[ _-]?key|incorrect api key|authentication[ _](failed|error)|forbidden|permission_denied|invalid[ _]token/i,
|
|
87
115
|
message: '认证失败:Key 或令牌无效,请检查后重新填写'
|
|
88
116
|
},
|
|
117
|
+
{
|
|
118
|
+
category: 'quota',
|
|
119
|
+
pattern: /\b429\b|rate[ _-]?limit|too many requests|insufficient[ _](quota|balance)|quota exceeded|余额不足|无可用资源包|\b1113\b/i,
|
|
120
|
+
message: '额度不足或请求太频繁:请检查账户余额 / 套餐额度;套餐 Key 不能用在按量计费的地址上,反过来也一样'
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
category: 'model',
|
|
124
|
+
pattern: /model(?! metadata)[^\n]{0,80}(not found|does not exist|not exist|not supported|unknown)|(unknown|invalid|unsupported) model|no such model|模型[^\n]{0,20}(不存在|不支持)/i,
|
|
125
|
+
message: '模型名不存在或该 Key 不能用这个模型,请检查模型名'
|
|
126
|
+
},
|
|
89
127
|
{
|
|
90
128
|
category: 'network',
|
|
91
129
|
pattern: /ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|EHOSTUNREACH|ENETUNREACH|fetch failed|getaddrinfo|could not resolve|network (is )?unreachable|connection (refused|reset|timed out)|socket hang up|unable to connect/i,
|
|
@@ -94,13 +132,21 @@ const CONNECTION_RULES = [
|
|
|
94
132
|
];
|
|
95
133
|
|
|
96
134
|
/**
|
|
97
|
-
* 把容器内最小请求的结果归类为 success / network / auth / missing_cli / other。
|
|
135
|
+
* 把容器内最小请求的结果归类为 success / no_tools / quota / model / network / auth / missing_cli / other。
|
|
98
136
|
* 返回值只含固定文案和经过脱敏的尾部片段,不回传完整输出。
|
|
99
137
|
*/
|
|
100
|
-
function classifyConnectionResult(result, secrets = []) {
|
|
138
|
+
function classifyConnectionResult(result, secrets = [], options = {}) {
|
|
101
139
|
const exitCode = result && Number.isInteger(result.exitCode) ? result.exitCode : 1;
|
|
102
140
|
const output = String(result && result.output || '');
|
|
103
141
|
if (exitCode === 0) {
|
|
142
|
+
// 只回一句 OK 发现不了“能连通但调不了工具”,测试要求执行一条命令并读回结果
|
|
143
|
+
if (options.expectOutput && !output.includes(options.expectOutput)) {
|
|
144
|
+
return {
|
|
145
|
+
category: 'no_tools',
|
|
146
|
+
message: '能连通,但模型没有执行命令:Agent 在这个服务上可能调不了工具,换个模型或服务商再试',
|
|
147
|
+
detail: redactSecrets(output.trim().slice(-300), secrets)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
104
150
|
return { category: 'success', message: '连接成功', detail: '' };
|
|
105
151
|
}
|
|
106
152
|
const detail = redactSecrets(output.trim().slice(-300), secrets);
|
|
@@ -188,6 +234,7 @@ module.exports = {
|
|
|
188
234
|
SETUP_AGENTS,
|
|
189
235
|
listSetupAgents,
|
|
190
236
|
getConfiguredAgents,
|
|
237
|
+
identifyRunAgent,
|
|
191
238
|
buildAgentRunProfile,
|
|
192
239
|
classifyConnectionResult,
|
|
193
240
|
redactSecrets
|