evolclaw 3.1.4 → 3.1.6
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/CHANGELOG.md +60 -0
- package/dist/agents/claude-runner.js +398 -161
- package/dist/agents/kit-renderer.js +191 -25
- package/dist/aun/aid/agentmd.js +75 -103
- package/dist/aun/aid/client.js +1 -29
- package/dist/aun/aid/identity.js +105 -64
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/msg/group.js +2 -2
- package/dist/aun/msg/p2p.js +26 -2
- package/dist/aun/rpc/connection.js +23 -30
- package/dist/channels/aun.js +174 -99
- package/dist/channels/dingtalk.js +2 -1
- package/dist/channels/feishu.js +301 -199
- package/dist/channels/qqbot.js +2 -1
- package/dist/channels/wechat.js +2 -1
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent.js +21 -16
- package/dist/cli/bench.js +41 -28
- package/dist/cli/help.js +8 -0
- package/dist/cli/index.js +176 -87
- package/dist/cli/init-channel.js +5 -1
- package/dist/cli/init.js +37 -21
- package/dist/cli/link-rules.js +1 -7
- package/dist/cli/model.js +549 -0
- package/dist/cli/net-check.js +133 -50
- package/dist/cli/watch-msg.js +7 -7
- package/dist/cli/watch-web/debug-log.js +18 -0
- package/dist/cli/watch-web/server.js +306 -0
- package/dist/cli/watch-web/sources/aid.js +63 -0
- package/dist/cli/watch-web/sources/msg.js +70 -0
- package/dist/cli/watch-web/sources/session.js +638 -0
- package/dist/cli/watch-web/sources/types.js +10 -0
- package/dist/cli/watch-web/static/app.js +546 -0
- package/dist/cli/watch-web/static/index.html +54 -0
- package/dist/cli/watch-web/static/style.css +247 -0
- package/dist/config-store.js +1 -22
- package/dist/core/channel-loader.js +7 -4
- package/dist/core/command-handler.js +261 -133
- package/dist/core/evolagent-registry.js +1 -1
- package/dist/core/evolagent.js +4 -22
- package/dist/core/interaction-router.js +59 -0
- package/dist/core/message/im-renderer.js +9 -20
- package/dist/core/message/message-bridge.js +13 -9
- package/dist/core/message/message-log.js +2 -2
- package/dist/core/message/message-processor.js +211 -123
- package/dist/core/message/stream-idle-monitor.js +21 -0
- package/dist/core/model/model-catalog.js +215 -0
- package/dist/core/model/model-scope.js +250 -0
- package/dist/core/relation/peer-identity.js +58 -55
- package/dist/core/relation/peer-key.js +16 -0
- package/dist/core/session/session-fs-store.js +34 -55
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +308 -251
- package/dist/core/session/session-mapper.js +9 -4
- package/dist/core/trigger/manager.js +3 -3
- package/dist/core/trigger/parser.js +4 -4
- package/dist/core/trigger/scheduler.js +22 -7
- package/dist/index.js +61 -7
- package/dist/ipc.js +23 -1
- package/dist/utils/error-utils.js +6 -0
- package/dist/utils/process-introspect.js +7 -5
- package/kits/docs/GUIDE.md +2 -2
- package/kits/docs/INDEX.md +8 -8
- package/kits/docs/channels/aun.md +56 -17
- package/kits/docs/channels/feishu.md +41 -12
- package/kits/docs/context-assembly.md +182 -0
- package/kits/docs/evolclaw/INDEX.md +43 -0
- package/kits/docs/evolclaw/agent.md +49 -0
- package/kits/docs/evolclaw/aid.md +49 -0
- package/kits/docs/evolclaw/ctl.md +46 -0
- package/kits/docs/evolclaw/group.md +89 -0
- package/kits/docs/evolclaw/model.md +51 -0
- package/kits/docs/evolclaw/msg.md +91 -0
- package/kits/docs/evolclaw/rpc.md +35 -0
- package/kits/docs/evolclaw/storage.md +49 -0
- package/kits/docs/venues/aun-group.md +10 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +23 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.json +81 -36
- package/kits/rules/01-overview.md +20 -10
- package/kits/rules/06-channel.md +34 -27
- package/kits/templates/system-fragments/baseagent.md +7 -1
- package/kits/templates/system-fragments/channel.md +7 -5
- package/kits/templates/system-fragments/commands.md +19 -0
- package/kits/templates/system-fragments/session.md +19 -3
- package/kits/templates/system-fragments/venue.md +24 -0
- package/package.json +10 -5
- package/dist/aun/aid/lifecycle-log.js +0 -33
- package/dist/utils/aid-lifecycle-log.js +0 -33
- package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
- package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
- package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
- package/kits/docs/evolclaw/tools.md +0 -25
package/dist/cli/init.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
import readline from 'readline';
|
|
3
4
|
import { resolvePaths, ensureDataDirs } from '../paths.js';
|
|
4
5
|
import { commandExists } from '../utils/cross-platform.js';
|
|
@@ -10,11 +11,6 @@ function ask(rl, question) {
|
|
|
10
11
|
return new Promise(resolve => rl.question(question, resolve));
|
|
11
12
|
}
|
|
12
13
|
const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
|
|
13
|
-
const BASEAGENT_ENV_KEY = {
|
|
14
|
-
claude: 'ANTHROPIC_API_KEY',
|
|
15
|
-
codex: 'OPENAI_API_KEY',
|
|
16
|
-
gemini: 'GEMINI_API_KEY',
|
|
17
|
-
};
|
|
18
14
|
function isBaseagentAvailable(baseagent) {
|
|
19
15
|
if (baseagent === 'codex')
|
|
20
16
|
return isCodexSdkAvailable();
|
|
@@ -26,16 +22,19 @@ function detectAvailable() {
|
|
|
26
22
|
function pickDefault(available) {
|
|
27
23
|
return (available.includes('claude') ? 'claude' : available[0]);
|
|
28
24
|
}
|
|
29
|
-
function buildDefaults(chosen) {
|
|
30
|
-
const
|
|
25
|
+
function buildDefaults(chosen, available, projectsDefaultPath) {
|
|
26
|
+
const baseagents = {};
|
|
27
|
+
for (const b of available)
|
|
28
|
+
baseagents[b] = {};
|
|
31
29
|
return {
|
|
32
30
|
$schema_version: 1,
|
|
33
31
|
active_baseagent: chosen,
|
|
34
|
-
baseagents
|
|
32
|
+
baseagents,
|
|
33
|
+
...(projectsDefaultPath ? { projects: { defaultPath: projectsDefaultPath } } : {}),
|
|
35
34
|
};
|
|
36
35
|
}
|
|
37
|
-
function writeDefaults(
|
|
38
|
-
saveDefaultsSafe(buildDefaults(chosen));
|
|
36
|
+
function writeDefaults(chosen, available, projectsDefaultPath) {
|
|
37
|
+
saveDefaultsSafe(buildDefaults(chosen, available, projectsDefaultPath));
|
|
39
38
|
}
|
|
40
39
|
// ==================== Main ====================
|
|
41
40
|
export async function cmdInit(options) {
|
|
@@ -85,7 +84,7 @@ export async function cmdInit(options) {
|
|
|
85
84
|
else {
|
|
86
85
|
chosen = pickDefault(available);
|
|
87
86
|
}
|
|
88
|
-
writeDefaults(
|
|
87
|
+
writeDefaults(chosen, available);
|
|
89
88
|
console.log(`✓ 已${exists ? '覆盖' : '创建'}: ${defaultsPath}`);
|
|
90
89
|
console.log(` active_baseagent: ${chosen}`);
|
|
91
90
|
const { agents } = loadAllAgents();
|
|
@@ -118,12 +117,33 @@ export async function cmdInit(options) {
|
|
|
118
117
|
}
|
|
119
118
|
return chosen;
|
|
120
119
|
}
|
|
120
|
+
async function askProjectsDefaultPath() {
|
|
121
|
+
const defaultDir = path.join(p.root, 'projects', 'default');
|
|
122
|
+
const input = (await ask(rl, `项目默认目录 [${defaultDir}]: `)).trim();
|
|
123
|
+
const resolved = input || defaultDir;
|
|
124
|
+
if (!path.isAbsolute(resolved)) {
|
|
125
|
+
console.log(' ⚠ 需要绝对路径,已跳过');
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
if (!fs.existsSync(resolved)) {
|
|
129
|
+
const create = (await ask(rl, ` 目录不存在,是否创建?[Y/n]: `)).trim().toLowerCase();
|
|
130
|
+
if (create === '' || create === 'y' || create === 'yes') {
|
|
131
|
+
fs.mkdirSync(resolved, { recursive: true });
|
|
132
|
+
console.log(` ✓ 已创建 ${resolved}`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return resolved;
|
|
139
|
+
}
|
|
121
140
|
try {
|
|
122
141
|
if (exists) {
|
|
123
142
|
const ans = (await ask(rl, `配置文件已存在: ${defaultsPath}\n 是否覆盖?[y/N] `)).trim().toLowerCase();
|
|
124
143
|
if (ans === 'y' || ans === 'yes') {
|
|
125
144
|
const chosen = await askBaseagent();
|
|
126
|
-
|
|
145
|
+
const projectsDefaultPath = await askProjectsDefaultPath();
|
|
146
|
+
writeDefaults(chosen, available, projectsDefaultPath);
|
|
127
147
|
console.log(`\n✓ 已覆盖: ${defaultsPath}`);
|
|
128
148
|
console.log(` active_baseagent: ${chosen}\n`);
|
|
129
149
|
}
|
|
@@ -133,20 +153,16 @@ export async function cmdInit(options) {
|
|
|
133
153
|
}
|
|
134
154
|
else {
|
|
135
155
|
const chosen = await askBaseagent();
|
|
136
|
-
|
|
156
|
+
const projectsDefaultPath = await askProjectsDefaultPath();
|
|
157
|
+
writeDefaults(chosen, available, projectsDefaultPath);
|
|
137
158
|
console.log(`\n✓ 已创建: ${defaultsPath}`);
|
|
138
159
|
console.log(` active_baseagent: ${chosen}\n`);
|
|
139
160
|
}
|
|
140
|
-
// ── 5.
|
|
161
|
+
// ── 5. 提示创建 agent ──
|
|
141
162
|
const { agents } = loadAllAgents();
|
|
142
163
|
if (agents.length === 0) {
|
|
143
|
-
console.log('
|
|
144
|
-
console.log('
|
|
145
|
-
const { agentCreateInteractive } = await import('./agent.js');
|
|
146
|
-
const result = await agentCreateInteractive({ rl });
|
|
147
|
-
if (!result.ok) {
|
|
148
|
-
console.error(`❌ ${result.error}`);
|
|
149
|
-
}
|
|
164
|
+
console.log('提示:尚无 agent,运行以下命令创建:');
|
|
165
|
+
console.log(' evolclaw agent new <aid>.agentid.pub');
|
|
150
166
|
}
|
|
151
167
|
}
|
|
152
168
|
finally {
|
package/dist/cli/link-rules.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { kitsRulesDir, resolvePaths } from '../paths.js';
|
|
4
4
|
import { atomicWriteJson, atomicReadJson } from '../utils/atomic-write.js';
|
|
5
|
-
import { wantsHelp } from './help.js';
|
|
5
|
+
import { wantsHelp, getArgValue } from './help.js';
|
|
6
6
|
const isWindows = process.platform === 'win32';
|
|
7
7
|
const KNOWN_BASEAGENTS = ['cc', 'codex', 'gemini'];
|
|
8
8
|
function statePath() {
|
|
@@ -232,12 +232,6 @@ function resolveBaseAgent(input) {
|
|
|
232
232
|
console.error(` Supported: ${KNOWN_BASEAGENTS.join(', ')}`);
|
|
233
233
|
process.exit(1);
|
|
234
234
|
}
|
|
235
|
-
function getArgValue(args, flag) {
|
|
236
|
-
const idx = args.indexOf(flag);
|
|
237
|
-
if (idx === -1 || idx + 1 >= args.length)
|
|
238
|
-
return undefined;
|
|
239
|
-
return args[idx + 1];
|
|
240
|
-
}
|
|
241
235
|
function pathEquals(a, b) {
|
|
242
236
|
if (isWindows) {
|
|
243
237
|
return path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
|
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `evolclaw model` —— 面向 agent 的模型管理命令集。
|
|
3
|
+
*
|
|
4
|
+
* 三级作用域由参数决定:
|
|
5
|
+
* (无) → 全局 defaults.json
|
|
6
|
+
* --self → agent config.json
|
|
7
|
+
* --self --peer → 关系 relations/<peerKey>/preferences.json
|
|
8
|
+
*
|
|
9
|
+
* 改某作用域后,对应范围所有会话的下一条消息即时生效(运行时按 关系>agent>全局 解析)。
|
|
10
|
+
* 与对话内 slash(/model /setmodel /effort /baseagent)互不影响。
|
|
11
|
+
* 设计见 docs/model-command-design.md。
|
|
12
|
+
*/
|
|
13
|
+
import { isHelpFlag, wantsHelp, getArgValue } from './help.js';
|
|
14
|
+
import { ModelScopeError, normalizePeer, determineScope, activeBaseagent, readScope, writeScope, clearScope, resolveEffectiveModel, } from '../core/model/model-scope.js';
|
|
15
|
+
import { loadDefaults, loadAgent } from '../config-store.js';
|
|
16
|
+
import { resolveAnthropicConfig } from '../agents/resolve.js';
|
|
17
|
+
import { getCatalog, getModelInfo } from '../core/model/model-catalog.js';
|
|
18
|
+
const ALL_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
|
|
19
|
+
const SCOPE_LABEL = {
|
|
20
|
+
global: '全局', agent: 'agent级', relation: '关系级',
|
|
21
|
+
};
|
|
22
|
+
/** 输出 JSON 并退出(success=false 时 exit 1)。 */
|
|
23
|
+
function emit(formatJson, payload, textFn) {
|
|
24
|
+
if (formatJson) {
|
|
25
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log(textFn());
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function fail(formatJson, code, message) {
|
|
32
|
+
if (formatJson) {
|
|
33
|
+
console.log(JSON.stringify({ ok: false, code, error: message }, null, 2));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
console.error(`❌ ${message} (${code})`);
|
|
37
|
+
}
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 解析 --self / --peer 为作用域选择器。
|
|
42
|
+
*/
|
|
43
|
+
function parseSelector(args, formatJson) {
|
|
44
|
+
const self = getArgValue(args, '--self');
|
|
45
|
+
const peerRaw = getArgValue(args, '--peer');
|
|
46
|
+
let peerKey;
|
|
47
|
+
if (peerRaw !== undefined) {
|
|
48
|
+
try {
|
|
49
|
+
peerKey = normalizePeer(peerRaw);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
if (e instanceof ModelScopeError)
|
|
53
|
+
fail(formatJson, e.code, e.message);
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const sel = { self, peerKey };
|
|
58
|
+
// 触发依赖校验(PEER_WITHOUT_SELF)
|
|
59
|
+
try {
|
|
60
|
+
determineScope(sel);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
if (e instanceof ModelScopeError)
|
|
64
|
+
fail(formatJson, e.code, e.message);
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
return sel;
|
|
68
|
+
}
|
|
69
|
+
const HELP = `用法: evolclaw model <command> [options]
|
|
70
|
+
|
|
71
|
+
Commands:
|
|
72
|
+
list 列出可用模型,标注各作用域命中
|
|
73
|
+
current 显示按优先级解析后实际生效的模型 + 来源
|
|
74
|
+
info <model-id> 查看单个模型详情(厂商/上下文/价格/模态/effort/状态)
|
|
75
|
+
use <model-id> 设置模型(作用域由 --self/--peer 决定)
|
|
76
|
+
reset 清除指定作用域的设置,回落上一级
|
|
77
|
+
effort <level> 设置推理强度(low|medium|high|xhigh|max|auto)
|
|
78
|
+
check 诊断网关连通性与模型可用性(分阶段输出进度)
|
|
79
|
+
|
|
80
|
+
作用域(由参数决定,越具体越优先:关系 > agent > 全局):
|
|
81
|
+
(无参数) 全局默认 → defaults.json
|
|
82
|
+
--self <aid> agent级 → config.json
|
|
83
|
+
--self <aid> --peer <X> 关系级 → relations/<peerKey>/preferences.json
|
|
84
|
+
|
|
85
|
+
改某作用域后,对应范围所有会话的下一条消息即时生效。
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
--self <aid> 本端 AID
|
|
89
|
+
--peer <X> 对端:channelType#channelId 或裸 aid(裸 aid 视为 aun#<aid>)
|
|
90
|
+
--effort <level> (use 专用)同时设置推理强度
|
|
91
|
+
--format json 输出 JSON
|
|
92
|
+
--help, -h 各子命令均支持
|
|
93
|
+
|
|
94
|
+
示例:
|
|
95
|
+
evolclaw model list
|
|
96
|
+
evolclaw model current --self bot.agentid.pub --peer aun#alice.agentid.pub
|
|
97
|
+
evolclaw model info deepseek-v4-pro
|
|
98
|
+
evolclaw model use opus
|
|
99
|
+
evolclaw model use deepseek-v4-pro --self bot.agentid.pub --peer alice.agentid.pub
|
|
100
|
+
evolclaw model effort high --self bot.agentid.pub
|
|
101
|
+
evolclaw model reset --self bot.agentid.pub --peer alice.agentid.pub
|
|
102
|
+
evolclaw model check --self bot.agentid.pub`;
|
|
103
|
+
async function dispatch(sub, args, formatJson) {
|
|
104
|
+
switch (sub) {
|
|
105
|
+
case 'list': return await cmdList(args, formatJson);
|
|
106
|
+
case 'current': return await cmdCurrent(args, formatJson);
|
|
107
|
+
case 'info': return await cmdInfo(args, formatJson);
|
|
108
|
+
case 'use': return await cmdUse(args, formatJson);
|
|
109
|
+
case 'reset': return await cmdReset(args, formatJson);
|
|
110
|
+
case 'effort': return await cmdEffort(args, formatJson);
|
|
111
|
+
case 'check': return await cmdCheck(args, formatJson);
|
|
112
|
+
default:
|
|
113
|
+
fail(formatJson, 'UNKNOWN_SUBCOMMAND', `未知子命令: ${sub}(model --help 查看用法)`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ── list ──────────────────────────────────────────────────────────────
|
|
117
|
+
const ICON = {
|
|
118
|
+
global: '⬡', agent: '◆', relation: '★',
|
|
119
|
+
};
|
|
120
|
+
async function cmdList(args, formatJson) {
|
|
121
|
+
if (wantsHelp(args)) {
|
|
122
|
+
console.log(HELP);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const sel = parseSelector(args, formatJson);
|
|
126
|
+
const ba = activeBaseagent(sel.self);
|
|
127
|
+
const cat = await getCatalog(sel.self, ba);
|
|
128
|
+
const resolved = resolveEffectiveModel(sel, ba);
|
|
129
|
+
// 各作用域当前值(仅可达作用域)
|
|
130
|
+
const scopes = {};
|
|
131
|
+
scopes.global = readScope('global', sel, ba);
|
|
132
|
+
if (sel.self)
|
|
133
|
+
scopes.agent = readScope('agent', sel, ba);
|
|
134
|
+
if (sel.self && sel.peerKey)
|
|
135
|
+
scopes.relation = readScope('relation', sel, ba);
|
|
136
|
+
emit(formatJson, {
|
|
137
|
+
ok: true,
|
|
138
|
+
effective: { model: resolved.model ?? null, source: resolved.source ?? null },
|
|
139
|
+
scopes,
|
|
140
|
+
catalogSource: cat.source,
|
|
141
|
+
models: cat.models,
|
|
142
|
+
}, () => {
|
|
143
|
+
const lines = [];
|
|
144
|
+
lines.push(`当前生效: ${resolved.model ?? '(未设置,回落 SDK 默认)'}` +
|
|
145
|
+
(resolved.source ? ` (来源: ${SCOPE_LABEL[resolved.source]})` : ''));
|
|
146
|
+
lines.push('');
|
|
147
|
+
const srcTag = cat.source === 'mock' ? ' [mock]'
|
|
148
|
+
: cat.source === 'remote' ? ' [remote]'
|
|
149
|
+
: '';
|
|
150
|
+
lines.push(`可用模型 (${cat.models.length})${srcTag}:`);
|
|
151
|
+
const byScope = (m) => {
|
|
152
|
+
const tags = [];
|
|
153
|
+
for (const s of ['relation', 'agent', 'global']) {
|
|
154
|
+
if (scopes[s]?.model === m)
|
|
155
|
+
tags.push(`${ICON[s]}${SCOPE_LABEL[s]}`);
|
|
156
|
+
}
|
|
157
|
+
return tags.join(' ');
|
|
158
|
+
};
|
|
159
|
+
for (const e of cat.models) {
|
|
160
|
+
const live = resolved.model === e.id ? '✓' : ' ';
|
|
161
|
+
const tag = byScope(e.id);
|
|
162
|
+
lines.push(` ${live} ${e.id.padEnd(28)} ${tag}`.trimEnd());
|
|
163
|
+
}
|
|
164
|
+
return lines.join('\n');
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// ── current ───────────────────────────────────────────────────────────
|
|
168
|
+
async function cmdCurrent(args, formatJson) {
|
|
169
|
+
if (wantsHelp(args)) {
|
|
170
|
+
console.log(HELP);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const sel = parseSelector(args, formatJson);
|
|
174
|
+
const ba = activeBaseagent(sel.self);
|
|
175
|
+
const resolved = resolveEffectiveModel(sel, ba);
|
|
176
|
+
emit(formatJson, {
|
|
177
|
+
ok: true,
|
|
178
|
+
model: resolved.model ?? null,
|
|
179
|
+
effort: resolved.effort ?? null,
|
|
180
|
+
source: resolved.source ?? null,
|
|
181
|
+
chain: resolved.chain.map(c => ({ scope: c.scope, model: c.model ?? null, hit: c.hit })),
|
|
182
|
+
}, () => {
|
|
183
|
+
const lines = [];
|
|
184
|
+
lines.push(`当前生效模型: ${resolved.model ?? '(未设置,回落 SDK 默认)'}`);
|
|
185
|
+
lines.push(`推理强度: ${resolved.effort ?? 'auto'}`);
|
|
186
|
+
lines.push(`来源: ${resolved.source ? SCOPE_LABEL[resolved.source] : '无'}`);
|
|
187
|
+
const chain = resolved.chain.map(c => `${SCOPE_LABEL[c.scope]}${c.hit ? ' ✓' : ''}${c.model ? `(${c.model})` : ''}`).join(' > ');
|
|
188
|
+
lines.push(`解析链: ${chain}`);
|
|
189
|
+
return lines.join('\n');
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// ── info ──────────────────────────────────────────────────────────────
|
|
193
|
+
async function cmdInfo(args, formatJson) {
|
|
194
|
+
if (wantsHelp(args)) {
|
|
195
|
+
console.log(HELP);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const modelId = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
|
|
199
|
+
if (!modelId)
|
|
200
|
+
fail(formatJson, 'MISSING_MODEL_ID', 'info 需要 <model-id>');
|
|
201
|
+
const self = getArgValue(args, '--self');
|
|
202
|
+
const info = await getModelInfo(modelId, self);
|
|
203
|
+
emit(formatJson, { ok: true, ...info }, () => {
|
|
204
|
+
const price = info.pricing;
|
|
205
|
+
const fmtPrice = (v) => v === null ? '— (mock)' : `$${v} / 1M tokens`;
|
|
206
|
+
return [
|
|
207
|
+
`模型: ${info.id}`,
|
|
208
|
+
` 厂商: ${info.owned_by}`,
|
|
209
|
+
` 上下文窗口: ${info.context_window ?? '—'} tokens`,
|
|
210
|
+
` 最大输出: ${info.max_output_tokens ?? '—'} tokens`,
|
|
211
|
+
` 输入价格: ${fmtPrice(price?.input_per_mtok ?? null)}`,
|
|
212
|
+
` 输出价格: ${fmtPrice(price?.output_per_mtok ?? null)}`,
|
|
213
|
+
` 支持模态: ${info.modalities.join(', ')}${info.mocked ? ' (mock)' : ''}`,
|
|
214
|
+
` 推理强度: ${info.supports_effort ? '支持' : '不支持'}`,
|
|
215
|
+
` 状态: ${info.status === 'available' ? '✓ 可用' : info.status}`,
|
|
216
|
+
].join('\n');
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// ── use / effort / reset 共用写入逻辑 ───────────────────────────────────
|
|
220
|
+
function describeWrite(scope, sel, model, effort, formatJson) {
|
|
221
|
+
emit(formatJson, {
|
|
222
|
+
ok: true,
|
|
223
|
+
scope,
|
|
224
|
+
self: sel.self ?? null,
|
|
225
|
+
peerKey: sel.peerKey ?? null,
|
|
226
|
+
model: model ?? undefined,
|
|
227
|
+
effort: effort ?? undefined,
|
|
228
|
+
}, () => {
|
|
229
|
+
const lines = ['✓ 已设置', ` 作用域: ${SCOPE_LABEL[scope]}` +
|
|
230
|
+
(sel.peerKey ? ` (${sel.peerKey})` : '') + (sel.self ? ` [self=${sel.self}]` : '')];
|
|
231
|
+
if (model !== null)
|
|
232
|
+
lines.push(` 模型: ${model}`);
|
|
233
|
+
if (effort !== null)
|
|
234
|
+
lines.push(` 推理强度: ${effort}`);
|
|
235
|
+
lines.push(' 生效: 该范围所有会话的下一条消息起生效。');
|
|
236
|
+
return lines.join('\n');
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async function cmdUse(args, formatJson) {
|
|
240
|
+
if (wantsHelp(args)) {
|
|
241
|
+
console.log(HELP);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const modelId = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
|
|
245
|
+
if (!modelId)
|
|
246
|
+
fail(formatJson, 'MISSING_MODEL_ID', 'use 需要 <model-id>');
|
|
247
|
+
const sel = parseSelector(args, formatJson);
|
|
248
|
+
const scope = determineScope(sel);
|
|
249
|
+
const ba = activeBaseagent(sel.self);
|
|
250
|
+
// 校验模型在 catalog 中
|
|
251
|
+
const cat = await getCatalog(sel.self, ba);
|
|
252
|
+
if (!cat.models.some(m => m.id === modelId)) {
|
|
253
|
+
fail(formatJson, 'UNKNOWN_MODEL', `模型不在目录中: ${modelId}(model list 查看可用模型)`);
|
|
254
|
+
}
|
|
255
|
+
const effort = getArgValue(args, '--effort');
|
|
256
|
+
if (effort !== undefined && !ALL_EFFORTS.includes(effort)) {
|
|
257
|
+
fail(formatJson, 'INVALID_EFFORT', `无效推理强度: ${effort}(${ALL_EFFORTS.join('/')})`);
|
|
258
|
+
}
|
|
259
|
+
const effortVal = effort === 'auto' ? null : (effort ?? undefined);
|
|
260
|
+
try {
|
|
261
|
+
writeScope(scope, sel, ba, { model: modelId, effort: effortVal });
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
if (e instanceof ModelScopeError)
|
|
265
|
+
fail(formatJson, e.code, e.message);
|
|
266
|
+
throw e;
|
|
267
|
+
}
|
|
268
|
+
describeWrite(scope, sel, modelId, effort ?? null, formatJson);
|
|
269
|
+
}
|
|
270
|
+
async function cmdEffort(args, formatJson) {
|
|
271
|
+
if (wantsHelp(args)) {
|
|
272
|
+
console.log(HELP);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const level = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
|
|
276
|
+
if (!level)
|
|
277
|
+
fail(formatJson, 'INVALID_EFFORT', 'effort 需要 <level>(low|medium|high|xhigh|max|auto)');
|
|
278
|
+
if (!ALL_EFFORTS.includes(level)) {
|
|
279
|
+
fail(formatJson, 'INVALID_EFFORT', `无效推理强度: ${level}(${ALL_EFFORTS.join('/')})`);
|
|
280
|
+
}
|
|
281
|
+
const sel = parseSelector(args, formatJson);
|
|
282
|
+
const scope = determineScope(sel);
|
|
283
|
+
const ba = activeBaseagent(sel.self);
|
|
284
|
+
const val = level === 'auto' ? null : level;
|
|
285
|
+
try {
|
|
286
|
+
writeScope(scope, sel, ba, { effort: val });
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
if (e instanceof ModelScopeError)
|
|
290
|
+
fail(formatJson, e.code, e.message);
|
|
291
|
+
throw e;
|
|
292
|
+
}
|
|
293
|
+
describeWrite(scope, sel, null, level, formatJson);
|
|
294
|
+
}
|
|
295
|
+
async function cmdReset(args, formatJson) {
|
|
296
|
+
if (wantsHelp(args)) {
|
|
297
|
+
console.log(HELP);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const sel = parseSelector(args, formatJson);
|
|
301
|
+
const scope = determineScope(sel);
|
|
302
|
+
const ba = activeBaseagent(sel.self);
|
|
303
|
+
try {
|
|
304
|
+
clearScope(scope, sel, ba);
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
if (e instanceof ModelScopeError)
|
|
308
|
+
fail(formatJson, e.code, e.message);
|
|
309
|
+
throw e;
|
|
310
|
+
}
|
|
311
|
+
emit(formatJson, {
|
|
312
|
+
ok: true, scope, self: sel.self ?? null, peerKey: sel.peerKey ?? null,
|
|
313
|
+
}, () => {
|
|
314
|
+
return `✓ 已清除 ${SCOPE_LABEL[scope]} 设置,回落上一级(该范围所有会话下条消息生效)`;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
/** 带超时的 fetch,返回 Response 或 null(超时/失败)。 */
|
|
318
|
+
async function fetchWithTimeout(url, opts, timeoutMs) {
|
|
319
|
+
const controller = new AbortController();
|
|
320
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
321
|
+
try {
|
|
322
|
+
const resp = await fetch(url, { ...opts, signal: controller.signal });
|
|
323
|
+
clearTimeout(timer);
|
|
324
|
+
return resp;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
clearTimeout(timer);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/** 解析网关 baseUrl 和 apiKey(复用 resolveCreds 逻辑,但从 CLI 层调用)。 */
|
|
332
|
+
function resolveGatewayCreds(self) {
|
|
333
|
+
try {
|
|
334
|
+
const defaults = loadDefaults();
|
|
335
|
+
const agentCfg = self ? loadAgent(self) : null;
|
|
336
|
+
const block = agentCfg?.baseagents || defaults?.baseagents || {};
|
|
337
|
+
const claudeCfg = block.claude || {};
|
|
338
|
+
const r = resolveAnthropicConfig({ agents: { claude: claudeCfg } }, claudeCfg);
|
|
339
|
+
return { baseUrl: r.baseUrl, apiKey: r.apiKey };
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return {};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/** 对单个模型发一次最小探测请求,返回延迟 ms 或错误信息。
|
|
346
|
+
* 按网关风格顺序探测:Anthropic Messages API → OpenAI chat completions → 仅列表校验
|
|
347
|
+
*/
|
|
348
|
+
async function probeModel(baseUrl, apiKey, modelId, timeoutMs) {
|
|
349
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
350
|
+
const authHeaders = apiKey
|
|
351
|
+
? { 'x-api-key': apiKey, Authorization: `Bearer ${apiKey}` }
|
|
352
|
+
: {};
|
|
353
|
+
// 风格 1:Anthropic Messages API
|
|
354
|
+
{
|
|
355
|
+
const t0 = Date.now();
|
|
356
|
+
const resp = await fetchWithTimeout(`${base}/v1/messages`, {
|
|
357
|
+
method: 'POST',
|
|
358
|
+
headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01', ...authHeaders },
|
|
359
|
+
body: JSON.stringify({ model: modelId, max_tokens: 5, messages: [{ role: 'user', content: 'ok' }] }),
|
|
360
|
+
}, timeoutMs);
|
|
361
|
+
const ms = Date.now() - t0;
|
|
362
|
+
if (resp && resp.ok)
|
|
363
|
+
return { ok: true, ms, style: 'anthropic' };
|
|
364
|
+
if (resp && resp.status >= 400 && resp.status < 500) {
|
|
365
|
+
// 4xx 说明网关支持此风格,但模型不可用
|
|
366
|
+
let errMsg = `HTTP ${resp.status}`;
|
|
367
|
+
try {
|
|
368
|
+
const j = await resp.json();
|
|
369
|
+
errMsg = j?.error?.message || j?.detail || j?.message || errMsg;
|
|
370
|
+
}
|
|
371
|
+
catch { /* ignore */ }
|
|
372
|
+
return { ok: false, ms, error: String(errMsg).slice(0, 80), style: 'anthropic' };
|
|
373
|
+
}
|
|
374
|
+
// 非 4xx(超时/连接失败/5xx)→ 继续尝试下一种风格
|
|
375
|
+
}
|
|
376
|
+
// 风格 2:OpenAI chat completions
|
|
377
|
+
{
|
|
378
|
+
const t0 = Date.now();
|
|
379
|
+
const resp = await fetchWithTimeout(`${base}/v1/chat/completions`, {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
headers: { 'Content-Type': 'application/json', ...authHeaders },
|
|
382
|
+
body: JSON.stringify({ model: modelId, max_tokens: 5, messages: [{ role: 'user', content: 'ok' }] }),
|
|
383
|
+
}, timeoutMs);
|
|
384
|
+
const ms = Date.now() - t0;
|
|
385
|
+
if (resp && resp.ok) {
|
|
386
|
+
// 校验响应体有 choices 字段,否则可能是网关假装成功
|
|
387
|
+
try {
|
|
388
|
+
const j = await resp.json();
|
|
389
|
+
if (j?.choices || j?.id)
|
|
390
|
+
return { ok: true, ms, style: 'openai' };
|
|
391
|
+
}
|
|
392
|
+
catch { /* ignore */ }
|
|
393
|
+
}
|
|
394
|
+
if (resp && resp.status >= 400 && resp.status < 500) {
|
|
395
|
+
let errMsg = `HTTP ${resp.status}`;
|
|
396
|
+
try {
|
|
397
|
+
const j = await resp.json();
|
|
398
|
+
errMsg = j?.error?.message || j?.detail || j?.message || errMsg;
|
|
399
|
+
}
|
|
400
|
+
catch { /* ignore */ }
|
|
401
|
+
return { ok: false, ms, error: String(errMsg).slice(0, 80), style: 'openai' };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// 两种风格都探测不到:无法判断可用性
|
|
405
|
+
return { ok: false, ms: 0, error: '网关不支持标准探测(无法确认可用性)', style: 'unknown' };
|
|
406
|
+
}
|
|
407
|
+
async function cmdCheck(args, formatJson) {
|
|
408
|
+
if (wantsHelp(args)) {
|
|
409
|
+
console.log(HELP);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const sel = parseSelector(args, formatJson);
|
|
413
|
+
const ba = activeBaseagent(sel.self);
|
|
414
|
+
const steps = [];
|
|
415
|
+
const log = (step) => {
|
|
416
|
+
steps.push(step);
|
|
417
|
+
if (!formatJson) {
|
|
418
|
+
const icon = step.ok ? '✓' : '✗';
|
|
419
|
+
const ms = step.ms !== undefined ? ` (${step.ms}ms)` : '';
|
|
420
|
+
console.log(`[${icon}] ${step.label.padEnd(14)} ${step.detail}${ms}`);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
const { baseUrl, apiKey } = resolveGatewayCreds(sel.self);
|
|
424
|
+
// ── 阶段 1:DNS + 连通性 ──────────────────────────────────────────
|
|
425
|
+
if (!formatJson)
|
|
426
|
+
console.log('\n网关诊断\n' + '─'.repeat(50));
|
|
427
|
+
if (!baseUrl) {
|
|
428
|
+
log({ label: '网关地址', ok: false, detail: '未配置自定义网关(baseUrl),使用官方 Anthropic 端点' });
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
// DNS + TCP:用一次 HEAD 请求探测
|
|
432
|
+
const t0 = Date.now();
|
|
433
|
+
const pingResp = await fetchWithTimeout(baseUrl.replace(/\/+$/, '') + '/v1/models', {
|
|
434
|
+
method: 'GET',
|
|
435
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
436
|
+
}, 4000);
|
|
437
|
+
const pingMs = Date.now() - t0;
|
|
438
|
+
if (!pingResp) {
|
|
439
|
+
log({ label: '网关连通', ok: false, detail: `${baseUrl} 无法连接(DNS/TCP 失败)`, ms: pingMs });
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
log({ label: '网关连通', ok: true, detail: baseUrl, ms: pingMs });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// ── 阶段 2:认证 + 模型列表 ─────────────────────────────────────
|
|
446
|
+
if (!formatJson)
|
|
447
|
+
console.log('');
|
|
448
|
+
const cat = await getCatalog(sel.self, ba);
|
|
449
|
+
const modelIds = cat.models.filter(m => m.owned_by !== 'alias').map(m => m.id);
|
|
450
|
+
if (cat.source === 'mock') {
|
|
451
|
+
log({ label: '模型列表', ok: false, detail: `无法获取远端列表,使用内置 mock(${modelIds.length} 个)` });
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
log({ label: '模型列表', ok: true, detail: `来源: ${cat.source},共 ${modelIds.length} 个模型` });
|
|
455
|
+
}
|
|
456
|
+
// ── 阶段 3:当前配置模型 ──────────────────────────────────────────
|
|
457
|
+
if (!formatJson)
|
|
458
|
+
console.log('');
|
|
459
|
+
const resolved = resolveEffectiveModel(sel, ba);
|
|
460
|
+
const configuredModel = resolved.model;
|
|
461
|
+
if (!configuredModel) {
|
|
462
|
+
log({ label: '配置模型', ok: true, detail: '未配置(将使用 base agent 默认模型)' });
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
log({ label: '配置模型', ok: true, detail: `${configuredModel}(来源: ${resolved.source ?? '未知'})` });
|
|
466
|
+
}
|
|
467
|
+
// ── 阶段 4:模型可用性探测 ────────────────────────────────────────
|
|
468
|
+
// 仅在有自定义网关时才做 API 探测(官方端点跳过,避免消耗 token)
|
|
469
|
+
const probeResults = [];
|
|
470
|
+
if (baseUrl) {
|
|
471
|
+
if (!formatJson)
|
|
472
|
+
console.log('\n模型可用性探测\n' + '─'.repeat(50));
|
|
473
|
+
// 别名解析:从目录里找别名对应的完整 ID
|
|
474
|
+
const aliasMap = new Map(); // alias → full id
|
|
475
|
+
for (const entry of cat.models) {
|
|
476
|
+
if (entry.owned_by === 'alias') {
|
|
477
|
+
// 找目录中与别名前缀匹配的完整 ID
|
|
478
|
+
const match = cat.models.find(m => m.owned_by !== 'alias' && m.id.includes(entry.id));
|
|
479
|
+
if (match)
|
|
480
|
+
aliasMap.set(entry.id, match.id);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const resolveId = (id) => aliasMap.get(id) ?? id;
|
|
484
|
+
// 优先探测当前配置的模型(解析别名),其余为非别名 ID
|
|
485
|
+
const configResolved = configuredModel ? resolveId(configuredModel) : undefined;
|
|
486
|
+
const allProbeIds = [
|
|
487
|
+
...(configResolved ? [{ display: configuredModel, probe: configResolved }] : []),
|
|
488
|
+
...modelIds
|
|
489
|
+
.filter(id => id !== configResolved)
|
|
490
|
+
.map(id => ({ display: id, probe: id })),
|
|
491
|
+
];
|
|
492
|
+
// 轮次 1:配置模型 + 1 个额外(并发 2,快速验证主要路径)
|
|
493
|
+
const round1 = allProbeIds.slice(0, 2);
|
|
494
|
+
const r1 = await Promise.all(round1.map(({ display, probe }) => probeModel(baseUrl, apiKey, probe, 5000).then(r => ({ id: display, displayId: display, ...r }))));
|
|
495
|
+
probeResults.push(...r1);
|
|
496
|
+
if (!formatJson)
|
|
497
|
+
r1.forEach(r => log({ label: r.id.slice(0, 20), ok: r.ok, detail: r.ok ? '可用' : (r.error ?? '不可用'), ms: r.ms }));
|
|
498
|
+
// 轮次 2:剩余,2 个一组(避免触发限速)
|
|
499
|
+
const rest = allProbeIds.slice(2);
|
|
500
|
+
for (let i = 0; i < rest.length; i += 2) {
|
|
501
|
+
const batch = rest.slice(i, i + 2);
|
|
502
|
+
const br = await Promise.all(batch.map(({ display, probe }) => probeModel(baseUrl, apiKey, probe, 5000).then(r => ({ id: display, displayId: display, ...r }))));
|
|
503
|
+
probeResults.push(...br);
|
|
504
|
+
if (!formatJson)
|
|
505
|
+
br.forEach(r => log({ label: r.id.slice(0, 20), ok: r.ok, detail: r.ok ? '可用' : (r.error ?? '不可用'), ms: r.ms }));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
if (!formatJson)
|
|
510
|
+
console.log('\n(跳过模型探测:未配置自定义网关)');
|
|
511
|
+
}
|
|
512
|
+
// ── 汇总 ─────────────────────────────────────────────────────────
|
|
513
|
+
const availableModels = probeResults.filter(r => r.ok).map(r => r.id);
|
|
514
|
+
const configModelOk = configuredModel ? probeResults.find(r => r.id === configuredModel)?.ok : undefined;
|
|
515
|
+
if (!formatJson) {
|
|
516
|
+
console.log('\n' + '─'.repeat(50));
|
|
517
|
+
if (configuredModel && configModelOk === false) {
|
|
518
|
+
console.log(`⚠️ 当前配置的模型 ${configuredModel} 不可用`);
|
|
519
|
+
if (availableModels.length > 0) {
|
|
520
|
+
console.log(` 可用模型:${availableModels.slice(0, 3).join('、')}${availableModels.length > 3 ? ` 等 ${availableModels.length} 个` : ''}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else if (configuredModel && configModelOk === true) {
|
|
524
|
+
console.log(`✓ 当前配置的模型 ${configuredModel} 可用`);
|
|
525
|
+
}
|
|
526
|
+
else if (!baseUrl) {
|
|
527
|
+
console.log(`✓ 使用官方 Anthropic 端点,无需探测`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
emit(formatJson, {
|
|
532
|
+
ok: true,
|
|
533
|
+
steps,
|
|
534
|
+
configuredModel: configuredModel ?? null,
|
|
535
|
+
configModelAvailable: configModelOk ?? null,
|
|
536
|
+
availableModels,
|
|
537
|
+
catalogSource: cat.source,
|
|
538
|
+
}, () => '');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
export async function cmdModel(args) {
|
|
542
|
+
const sub = args[0];
|
|
543
|
+
const formatJson = getArgValue(args, '--format') === 'json';
|
|
544
|
+
if (!sub || isHelpFlag(sub)) {
|
|
545
|
+
console.log(HELP);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
await dispatch(sub, args, formatJson);
|
|
549
|
+
}
|