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
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-catalog: 模型目录与单模型元数据。
|
|
3
|
+
*
|
|
4
|
+
* 目录来源(按序回退):
|
|
5
|
+
* {baseUrl}/v1/models → {baseUrl}/models → 内置 mock catalog
|
|
6
|
+
*
|
|
7
|
+
* baseUrl/apiKey 复用 baseagent 凭据解析链(与发消息同源),命令不接收 url/key 参数。
|
|
8
|
+
* 单模型详情(info)现阶段全部为 mock,{baseUrl}/models 接口补全字段后自动接真实数据。
|
|
9
|
+
*
|
|
10
|
+
* 详见 docs/model-command-design.md。
|
|
11
|
+
*/
|
|
12
|
+
import { loadDefaults, loadAgent } from '../../config-store.js';
|
|
13
|
+
import { resolveAnthropicConfig, resolveOpenaiConfig } from '../../agents/resolve.js';
|
|
14
|
+
import { activeBaseagent } from './model-scope.js';
|
|
15
|
+
/** 取指定 baseagent 的 baseUrl + apiKey(复用现有解析链)。 */
|
|
16
|
+
function resolveCreds(self, ba) {
|
|
17
|
+
const baseagent = ba || activeBaseagent(self);
|
|
18
|
+
const agentCfg = self ? loadAgent(self) : null;
|
|
19
|
+
const defaults = loadDefaults();
|
|
20
|
+
const block = (agentCfg?.baseagents || defaults?.baseagents || {});
|
|
21
|
+
try {
|
|
22
|
+
if (baseagent === 'codex') {
|
|
23
|
+
const c = block.codex;
|
|
24
|
+
const r = resolveOpenaiConfig({ agents: { codex: c } }, c);
|
|
25
|
+
return { baseUrl: r.baseUrl, apiKey: r.apiKey };
|
|
26
|
+
}
|
|
27
|
+
const c = block.claude;
|
|
28
|
+
const r = resolveAnthropicConfig({ agents: { claude: c } }, c);
|
|
29
|
+
return { baseUrl: r.baseUrl, apiKey: r.apiKey };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** 从对象/字符串条目里提取 id(兼容 id/name/model 字段)。 */
|
|
36
|
+
function pickId(item) {
|
|
37
|
+
if (typeof item === 'string')
|
|
38
|
+
return item;
|
|
39
|
+
if (item && typeof item === 'object') {
|
|
40
|
+
const v = item.id ?? item.name ?? item.model;
|
|
41
|
+
return typeof v === 'string' ? v : undefined;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/** 从对象条目里提取厂商(兼容 owned_by/owner/provider)。 */
|
|
46
|
+
function pickOwner(item) {
|
|
47
|
+
if (item && typeof item === 'object') {
|
|
48
|
+
const v = item.owned_by ?? item.owner ?? item.provider;
|
|
49
|
+
return typeof v === 'string' ? v : undefined;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 通用容错解析:
|
|
55
|
+
* - 容器:json.data[] | json.models[] | json.data.models[] | 裸数组
|
|
56
|
+
* - 条目:字符串 或 {id|name|model, owned_by|owner|provider, created}
|
|
57
|
+
*/
|
|
58
|
+
function genericParse(json) {
|
|
59
|
+
const arr = Array.isArray(json) ? json
|
|
60
|
+
: Array.isArray(json?.data) ? json.data
|
|
61
|
+
: Array.isArray(json?.models) ? json.models
|
|
62
|
+
: Array.isArray(json?.data?.models) ? json.data.models
|
|
63
|
+
: [];
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const item of arr) {
|
|
66
|
+
const id = pickId(item);
|
|
67
|
+
if (!id)
|
|
68
|
+
continue;
|
|
69
|
+
out.push({ id, owned_by: pickOwner(item), created: typeof item?.created === 'number' ? item.created : undefined });
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 网关专用 parser 注册表(扩展点)。
|
|
75
|
+
* 当前为空:ModelGate 是标准 OpenAI list 风格,genericParse 已覆盖。
|
|
76
|
+
* 示例(接入异形网关时取消注释并按实际格式实现):
|
|
77
|
+
* {
|
|
78
|
+
* name: 'some-gateway',
|
|
79
|
+
* match: (url) => url.includes('some-gateway.example.com'),
|
|
80
|
+
* parse: (json) => (json?.result?.modelList ?? []).map((m: any) => ({ id: m.code, owned_by: m.vendor })),
|
|
81
|
+
* }
|
|
82
|
+
*/
|
|
83
|
+
const GATEWAY_PARSERS = [];
|
|
84
|
+
/** 选择 parser 并解析(专用优先,否则通用容错)。 */
|
|
85
|
+
export function parseModelList(json, url) {
|
|
86
|
+
for (const rule of GATEWAY_PARSERS) {
|
|
87
|
+
try {
|
|
88
|
+
if (rule.match(url, json))
|
|
89
|
+
return rule.parse(json);
|
|
90
|
+
}
|
|
91
|
+
catch { /* 专用 parser 失败则继续尝试通用解析 */ }
|
|
92
|
+
}
|
|
93
|
+
return genericParse(json);
|
|
94
|
+
}
|
|
95
|
+
async function fetchModelList(url, apiKey) {
|
|
96
|
+
try {
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
99
|
+
const resp = await fetch(url, {
|
|
100
|
+
signal: controller.signal,
|
|
101
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
102
|
+
});
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
if (!resp.ok)
|
|
105
|
+
return null;
|
|
106
|
+
const json = await resp.json();
|
|
107
|
+
const entries = parseModelList(json, url);
|
|
108
|
+
return entries.length > 0 ? entries : null;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** 内置 mock catalog(接口未就绪时的兜底;实测可用模型,见 docs 附录 A)。 */
|
|
115
|
+
const MOCK_CATALOG = [
|
|
116
|
+
{ id: 'claude-opus-4-7', owned_by: 'anthropic' },
|
|
117
|
+
{ id: 'claude-opus-4-6', owned_by: 'anthropic' },
|
|
118
|
+
{ id: 'claude-sonnet-4-6', owned_by: 'anthropic' },
|
|
119
|
+
{ id: 'claude-haiku-4-5-20251001', owned_by: 'anthropic' },
|
|
120
|
+
{ id: 'deepseek-v4-pro', owned_by: 'deepseek' },
|
|
121
|
+
{ id: 'deepseek-v4-flash', owned_by: 'deepseek' },
|
|
122
|
+
{ id: 'kimi-k2.6', owned_by: 'moonshot' },
|
|
123
|
+
{ id: 'kimi-k2.5', owned_by: 'moonshot' },
|
|
124
|
+
{ id: 'glm-5.1', owned_by: 'zhipu' },
|
|
125
|
+
{ id: 'glm-5', owned_by: 'zhipu' },
|
|
126
|
+
{ id: 'glm-4.7', owned_by: 'zhipu' },
|
|
127
|
+
{ id: 'MiniMax-M2.7', owned_by: 'minimax' },
|
|
128
|
+
];
|
|
129
|
+
/**
|
|
130
|
+
* 远端模型目录接口(临时)。本地 baseUrl 的 /v1/models、/models 尚未实现,
|
|
131
|
+
* 自动降级到此接口拉取真实可用模型列表;待本地接口就绪后此级自然不再命中。
|
|
132
|
+
*/
|
|
133
|
+
const REMOTE_CATALOG_URL = 'https://mg-new.evolai.cn/claude-proxy/models';
|
|
134
|
+
/**
|
|
135
|
+
* 各 baseagent 的稳定别名(cc/SDK 自动解析到最新版本)。
|
|
136
|
+
* 这些是一等公民——defaults.json 默认就存别名(如 "opus"),必须可选可校验。
|
|
137
|
+
*/
|
|
138
|
+
const KNOWN_ALIASES = {
|
|
139
|
+
claude: ['opus', 'sonnet', 'haiku'],
|
|
140
|
+
codex: [],
|
|
141
|
+
gemini: [],
|
|
142
|
+
};
|
|
143
|
+
/** 把别名作为虚拟条目并入目录头部(owned_by 标 alias,便于展示与校验)。 */
|
|
144
|
+
function withAliases(models, ba) {
|
|
145
|
+
const aliases = KNOWN_ALIASES[ba] || [];
|
|
146
|
+
const existing = new Set(models.map(m => m.id));
|
|
147
|
+
const aliasEntries = aliases
|
|
148
|
+
.filter(a => !existing.has(a))
|
|
149
|
+
.map(a => ({ id: a, owned_by: 'alias' }));
|
|
150
|
+
return [...aliasEntries, ...models];
|
|
151
|
+
}
|
|
152
|
+
/** 拉取模型目录:v1/models → models → 远端接口 → mock,并并入别名。 */
|
|
153
|
+
export async function getCatalog(self, ba) {
|
|
154
|
+
const baseagent = ba || activeBaseagent(self);
|
|
155
|
+
const { baseUrl, apiKey } = resolveCreds(self, baseagent);
|
|
156
|
+
if (baseUrl) {
|
|
157
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
158
|
+
const v1 = await fetchModelList(`${base}/v1/models`, apiKey);
|
|
159
|
+
if (v1)
|
|
160
|
+
return { models: withAliases(v1, baseagent), source: 'v1/models' };
|
|
161
|
+
const plain = await fetchModelList(`${base}/models`, apiKey);
|
|
162
|
+
if (plain)
|
|
163
|
+
return { models: withAliases(plain, baseagent), source: 'models' };
|
|
164
|
+
}
|
|
165
|
+
// 本地接口未实现 → 降级到远端目录接口(无需鉴权)
|
|
166
|
+
const remote = await fetchModelList(REMOTE_CATALOG_URL, apiKey);
|
|
167
|
+
if (remote)
|
|
168
|
+
return { models: withAliases(remote, baseagent), source: 'remote' };
|
|
169
|
+
return { models: withAliases(MOCK_CATALOG, baseagent), source: 'mock' };
|
|
170
|
+
}
|
|
171
|
+
/** claude 系判定:完整 ID 或别名(opus/sonnet/haiku)。 */
|
|
172
|
+
function isClaudeFamily(id) {
|
|
173
|
+
return /^claude-/.test(id) || ['opus', 'sonnet', 'haiku'].includes(id);
|
|
174
|
+
}
|
|
175
|
+
/** 单模型详情(现阶段 mock)。 */
|
|
176
|
+
export async function getModelInfo(modelId, self, ba) {
|
|
177
|
+
const cat = await getCatalog(self, ba);
|
|
178
|
+
const entry = cat.models.find(m => m.id === modelId);
|
|
179
|
+
const claudeFamily = isClaudeFamily(modelId);
|
|
180
|
+
// 远端目录把所有模型的 owned_by 标成网关名 'ModelGate',别名标成 'alias',
|
|
181
|
+
// 两者都不是真实厂商 → 回退到按 ID 推断。
|
|
182
|
+
const NON_VENDOR = new Set(['alias', 'ModelGate', '']);
|
|
183
|
+
const rawOwner = entry?.owned_by;
|
|
184
|
+
const owner = claudeFamily
|
|
185
|
+
? 'anthropic'
|
|
186
|
+
: (rawOwner && !NON_VENDOR.has(rawOwner) ? rawOwner : inferOwner(modelId));
|
|
187
|
+
return {
|
|
188
|
+
id: modelId,
|
|
189
|
+
owned_by: owner,
|
|
190
|
+
context_window: claudeFamily ? 200000 : 128000,
|
|
191
|
+
max_output_tokens: 8192,
|
|
192
|
+
pricing: { input_per_mtok: null, output_per_mtok: null, currency: 'USD' },
|
|
193
|
+
modalities: ['text'],
|
|
194
|
+
supports_effort: claudeFamily,
|
|
195
|
+
status: entry ? 'available' : 'unknown',
|
|
196
|
+
mocked: true,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function inferOwner(id) {
|
|
200
|
+
if (/^claude-/.test(id))
|
|
201
|
+
return 'anthropic';
|
|
202
|
+
if (/^gpt-/.test(id))
|
|
203
|
+
return 'openai';
|
|
204
|
+
if (/^gemini-/.test(id))
|
|
205
|
+
return 'google';
|
|
206
|
+
if (/^deepseek-/.test(id))
|
|
207
|
+
return 'deepseek';
|
|
208
|
+
if (/^kimi-/.test(id))
|
|
209
|
+
return 'moonshot';
|
|
210
|
+
if (/^glm-/.test(id))
|
|
211
|
+
return 'zhipu';
|
|
212
|
+
if (/^MiniMax-/i.test(id))
|
|
213
|
+
return 'minimax';
|
|
214
|
+
return 'unknown';
|
|
215
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-scope: 模型/推理强度的三级作用域读写与解析。
|
|
3
|
+
*
|
|
4
|
+
* 作用域(越具体越优先):
|
|
5
|
+
* 关系级 > agent级 > 全局
|
|
6
|
+
*
|
|
7
|
+
* 存储位置:
|
|
8
|
+
* 全局 agents/defaults.json → baseagents.<ba>.{model,effort}
|
|
9
|
+
* agent agents/<self>/config.json → baseagents.<ba>.{model,effort}
|
|
10
|
+
* 关系 agents/<self>/relations/<peerKey>/preferences.json → {model,effort}
|
|
11
|
+
*
|
|
12
|
+
* 改关系级/agent级/全局后,对应范围所有会话的下一条消息即时生效
|
|
13
|
+
* (运行时每条消息按 关系>agent>全局 解析,不缓存、不绑会话)。
|
|
14
|
+
*
|
|
15
|
+
* 详见 docs/model-command-design.md。
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { agentRelationsDir } from '../../paths.js';
|
|
20
|
+
import { loadDefaults, saveDefaultsSafe, loadAgent, saveAgent } from '../../config-store.js';
|
|
21
|
+
import { formatPeerKey, parsePeerKey } from '../relation/peer-key.js';
|
|
22
|
+
export class ModelScopeError extends Error {
|
|
23
|
+
code;
|
|
24
|
+
constructor(code, message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.name = 'ModelScopeError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// ── peer 归一化 ────────────────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* 把 `--peer` 入参归一为规范 peerKey。
|
|
33
|
+
* 接受两种形态:
|
|
34
|
+
* - `channelType#channelId`(channelId 可能已 urlEncode,统一 round-trip 重编码)
|
|
35
|
+
* - 裸 aid(无 '#')→ 视为 `aun#<aid>`
|
|
36
|
+
*/
|
|
37
|
+
export function normalizePeer(input) {
|
|
38
|
+
const raw = (input || '').trim();
|
|
39
|
+
if (!raw)
|
|
40
|
+
throw new ModelScopeError('INVALID_PEER', '--peer 不能为空');
|
|
41
|
+
if (raw.includes('#')) {
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = parsePeerKey(raw);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new ModelScopeError('INVALID_PEER', `无法解析 --peer: ${raw}`);
|
|
48
|
+
}
|
|
49
|
+
if (!parsed.channelType || !parsed.channelId) {
|
|
50
|
+
throw new ModelScopeError('INVALID_PEER', `无法解析 --peer: ${raw}`);
|
|
51
|
+
}
|
|
52
|
+
return formatPeerKey(parsed.channelType, parsed.channelId);
|
|
53
|
+
}
|
|
54
|
+
// 裸 aid → AUN 原生对端
|
|
55
|
+
return formatPeerKey('aun', raw);
|
|
56
|
+
}
|
|
57
|
+
// ── 作用域判定 ────────────────────────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* 由选择器判定作用域,并校验参数依赖关系。
|
|
60
|
+
* 依赖:peerKey 须配 self。
|
|
61
|
+
*/
|
|
62
|
+
export function determineScope(sel) {
|
|
63
|
+
const hasSelf = !!sel.self;
|
|
64
|
+
const hasPeer = !!sel.peerKey;
|
|
65
|
+
if (hasPeer && !hasSelf) {
|
|
66
|
+
throw new ModelScopeError('PEER_WITHOUT_SELF', '--peer 必须配合 --self 使用');
|
|
67
|
+
}
|
|
68
|
+
if (hasPeer)
|
|
69
|
+
return 'relation';
|
|
70
|
+
if (hasSelf)
|
|
71
|
+
return 'agent';
|
|
72
|
+
return 'global';
|
|
73
|
+
}
|
|
74
|
+
// ── baseagent 解析(global/agent 级模型挂在 baseagents.<ba> 下)──────────
|
|
75
|
+
/** 取本端的活跃 baseagent;无 self 时取全局默认。 */
|
|
76
|
+
export function activeBaseagent(self) {
|
|
77
|
+
try {
|
|
78
|
+
if (self) {
|
|
79
|
+
const cfg = loadAgent(self);
|
|
80
|
+
if (cfg?.active_baseagent)
|
|
81
|
+
return cfg.active_baseagent;
|
|
82
|
+
}
|
|
83
|
+
const d = loadDefaults();
|
|
84
|
+
if (d?.active_baseagent)
|
|
85
|
+
return d.active_baseagent;
|
|
86
|
+
}
|
|
87
|
+
catch { /* fall through */ }
|
|
88
|
+
return 'claude';
|
|
89
|
+
}
|
|
90
|
+
/** codex 的推理强度字段名是 reasoning,其余是 effort。 */
|
|
91
|
+
function effortField(ba) {
|
|
92
|
+
return ba === 'codex' ? 'reasoning' : 'effort';
|
|
93
|
+
}
|
|
94
|
+
// ── 关系级文件路径 ─────────────────────────────────────────────────────
|
|
95
|
+
function relationPrefsPath(self, peerKey) {
|
|
96
|
+
return path.join(agentRelationsDir(self), peerKey, 'preferences.json');
|
|
97
|
+
}
|
|
98
|
+
function readJsonSafe(file) {
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function writeJsonAtomic(file, data) {
|
|
107
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
108
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
109
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
110
|
+
fs.renameSync(tmp, file);
|
|
111
|
+
}
|
|
112
|
+
// ── 读:单作用域 ──────────────────────────────────────────────────────
|
|
113
|
+
/** 读取指定作用域当前存的 {model,effort}(未设为空对象)。 */
|
|
114
|
+
export function readScope(scope, sel, ba) {
|
|
115
|
+
switch (scope) {
|
|
116
|
+
case 'global': {
|
|
117
|
+
const block = (loadDefaults()?.baseagents || {});
|
|
118
|
+
const c = block[ba] || {};
|
|
119
|
+
return { model: c.model, effort: c[effortField(ba)] };
|
|
120
|
+
}
|
|
121
|
+
case 'agent': {
|
|
122
|
+
const cfg = sel.self ? loadAgent(sel.self) : null;
|
|
123
|
+
const c = (cfg?.baseagents || {})[ba] || {};
|
|
124
|
+
return { model: c.model, effort: c[effortField(ba)] };
|
|
125
|
+
}
|
|
126
|
+
case 'relation': {
|
|
127
|
+
const p = readJsonSafe(relationPrefsPath(sel.self, sel.peerKey));
|
|
128
|
+
return { model: p?.model, effort: p?.effort };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// ── 写:单作用域 ──────────────────────────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* 写入指定作用域。patch 中 undefined 的字段不动;显式 null 删除该字段。
|
|
135
|
+
*/
|
|
136
|
+
export function writeScope(scope, sel, ba, patch) {
|
|
137
|
+
switch (scope) {
|
|
138
|
+
case 'global':
|
|
139
|
+
writeConfigBlock(scope, sel, ba, patch);
|
|
140
|
+
return;
|
|
141
|
+
case 'agent':
|
|
142
|
+
writeConfigBlock(scope, sel, ba, patch);
|
|
143
|
+
return;
|
|
144
|
+
case 'relation':
|
|
145
|
+
writeFlatFile(relationPrefsPath(sel.self, sel.peerKey), patch);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function writeConfigBlock(scope, sel, ba, patch) {
|
|
150
|
+
const ef = effortField(ba);
|
|
151
|
+
if (scope === 'global') {
|
|
152
|
+
const block = {};
|
|
153
|
+
const sub = {};
|
|
154
|
+
if (patch.model !== undefined)
|
|
155
|
+
sub.model = patch.model === null ? undefined : patch.model;
|
|
156
|
+
if (patch.effort !== undefined)
|
|
157
|
+
sub[ef] = patch.effort === null ? undefined : patch.effort;
|
|
158
|
+
block[ba] = sub;
|
|
159
|
+
// saveDefaultsSafe 做深合并;删除字段需读改写
|
|
160
|
+
if (patch.model === null || patch.effort === null) {
|
|
161
|
+
const d = loadDefaults() || { $schema_version: 1 };
|
|
162
|
+
d.baseagents = d.baseagents || {};
|
|
163
|
+
d.baseagents[ba] = d.baseagents[ba] || {};
|
|
164
|
+
if (patch.model === null)
|
|
165
|
+
delete d.baseagents[ba].model;
|
|
166
|
+
else if (patch.model !== undefined)
|
|
167
|
+
d.baseagents[ba].model = patch.model;
|
|
168
|
+
if (patch.effort === null)
|
|
169
|
+
delete d.baseagents[ba][ef];
|
|
170
|
+
else if (patch.effort !== undefined)
|
|
171
|
+
d.baseagents[ba][ef] = patch.effort;
|
|
172
|
+
saveDefaultsSafe({ baseagents: d.baseagents });
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
saveDefaultsSafe({ baseagents: block });
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// agent 级
|
|
180
|
+
const cfg = loadAgent(sel.self);
|
|
181
|
+
if (!cfg)
|
|
182
|
+
throw new ModelScopeError('AGENT_NOT_FOUND', `agent 不存在: ${sel.self}`);
|
|
183
|
+
cfg.baseagents = cfg.baseagents || {};
|
|
184
|
+
cfg.baseagents[ba] = cfg.baseagents[ba] || {};
|
|
185
|
+
const sub = cfg.baseagents[ba];
|
|
186
|
+
if (patch.model === null)
|
|
187
|
+
delete sub.model;
|
|
188
|
+
else if (patch.model !== undefined)
|
|
189
|
+
sub.model = patch.model;
|
|
190
|
+
if (patch.effort === null)
|
|
191
|
+
delete sub[ef];
|
|
192
|
+
else if (patch.effort !== undefined)
|
|
193
|
+
sub[ef] = patch.effort;
|
|
194
|
+
saveAgent(cfg);
|
|
195
|
+
}
|
|
196
|
+
function writeFlatFile(file, patch) {
|
|
197
|
+
const cur = readJsonSafe(file) || {};
|
|
198
|
+
if (patch.model === null)
|
|
199
|
+
delete cur.model;
|
|
200
|
+
else if (patch.model !== undefined)
|
|
201
|
+
cur.model = patch.model;
|
|
202
|
+
if (patch.effort === null)
|
|
203
|
+
delete cur.effort;
|
|
204
|
+
else if (patch.effort !== undefined)
|
|
205
|
+
cur.effort = patch.effort;
|
|
206
|
+
cur.updatedAt = Date.now();
|
|
207
|
+
writeJsonAtomic(file, cur);
|
|
208
|
+
}
|
|
209
|
+
// ── 清除:单作用域 ────────────────────────────────────────────────────
|
|
210
|
+
/** 清除指定作用域的 model+effort(关系级直接删文件)。 */
|
|
211
|
+
export function clearScope(scope, sel, ba) {
|
|
212
|
+
if (scope === 'relation') {
|
|
213
|
+
try {
|
|
214
|
+
fs.unlinkSync(relationPrefsPath(sel.self, sel.peerKey));
|
|
215
|
+
}
|
|
216
|
+
catch { /* already gone */ }
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
writeScope(scope, sel, ba, { model: null, effort: null });
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 按 关系>agent>全局 解析实际生效的 model(首个有 model 的作用域命中)。
|
|
223
|
+
* 仅就给定选择器可达的作用域参与:无 peer 不读关系级,无 self 只读全局。
|
|
224
|
+
* 不读 ~/.claude/settings.json,不使用硬编码默认。
|
|
225
|
+
*
|
|
226
|
+
* 运行时(message-processor)每条消息调用本函数,结果直接传入 runQuery,
|
|
227
|
+
* 不缓存、不绑会话——故改关系级/agent级后该范围所有会话下条消息即时生效。
|
|
228
|
+
*/
|
|
229
|
+
export function resolveEffectiveModel(sel, ba) {
|
|
230
|
+
const baseagent = ba || activeBaseagent(sel.self);
|
|
231
|
+
const order = [];
|
|
232
|
+
if (sel.peerKey && sel.self)
|
|
233
|
+
order.push('relation');
|
|
234
|
+
if (sel.self)
|
|
235
|
+
order.push('agent');
|
|
236
|
+
order.push('global');
|
|
237
|
+
const chain = [];
|
|
238
|
+
let resolved = { chain };
|
|
239
|
+
for (const scope of order) {
|
|
240
|
+
const prefs = readScope(scope, sel, baseagent);
|
|
241
|
+
const hit = !!prefs.model && resolved.source === undefined;
|
|
242
|
+
chain.push({ scope, model: prefs.model, effort: prefs.effort, hit });
|
|
243
|
+
if (hit) {
|
|
244
|
+
resolved.model = prefs.model;
|
|
245
|
+
resolved.effort = prefs.effort;
|
|
246
|
+
resolved.source = scope;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return resolved;
|
|
250
|
+
}
|