evolclaw 3.1.4 → 3.1.5
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 +10 -0
- package/dist/agents/claude-runner.js +348 -156
- package/dist/agents/kit-renderer.js +176 -21
- package/dist/aun/aid/agentmd.js +68 -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/p2p.js +26 -2
- package/dist/aun/rpc/connection.js +23 -30
- package/dist/channels/aun.js +77 -88
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +270 -190
- package/dist/channels/qqbot.js +1 -0
- package/dist/channels/wechat.js +1 -0
- package/dist/channels/wecom.js +1 -0
- package/dist/cli/agent.js +11 -5
- package/dist/cli/bench.js +40 -23
- package/dist/cli/index.js +170 -44
- package/dist/cli/init-channel.js +5 -1
- package/dist/cli/model.js +324 -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/core/channel-loader.js +7 -4
- package/dist/core/command-handler.js +81 -86
- package/dist/core/evolagent-registry.js +1 -1
- package/dist/core/evolagent.js +4 -4
- package/dist/core/interaction-router.js +59 -0
- package/dist/core/message/message-bridge.js +6 -6
- package/dist/core/message/message-log.js +2 -2
- package/dist/core/message/message-processor.js +86 -101
- 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 +40 -49
- 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/scheduler.js +2 -1
- package/dist/index.js +6 -2
- package/dist/ipc.js +22 -0
- package/kits/docs/GUIDE.md +2 -2
- package/kits/docs/INDEX.md +11 -7
- package/kits/docs/channels/aun.md +56 -17
- package/kits/docs/channels/feishu.md +41 -12
- package/kits/docs/context-assembly.md +181 -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 +82 -0
- package/kits/docs/evolclaw/msg.md +86 -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 +11 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.json +72 -36
- package/kits/rules/01-overview.md +20 -10
- package/kits/rules/06-channel.md +30 -27
- package/kits/templates/system-fragments/session.md +10 -3
- package/kits/templates/system-fragments/venue.md +9 -0
- package/package.json +11 -6
- 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
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 2. 仅在 agent.md 内容变化时重写 peer-identity.json
|
|
7
7
|
* 3. 支持入站和出站消息的身份查询
|
|
8
8
|
*
|
|
9
|
-
* 信源:对端的 agent.md(通过
|
|
9
|
+
* 信源:对端的 agent.md(通过 AIDStore.checkAgentMd + downloadAgentMd,由 agentmdSync 封装)
|
|
10
10
|
* 判定规则:type !== 'human' → agent
|
|
11
11
|
* 缓存位置:$AGENT_DIR/relations/<channel>#<urlEncode(peerId)>/peer-identity.json
|
|
12
12
|
*/
|
|
@@ -15,6 +15,7 @@ import * as path from 'path';
|
|
|
15
15
|
import * as crypto from 'crypto';
|
|
16
16
|
import { logger } from '../../utils/logger.js';
|
|
17
17
|
import { agentMdPath } from '../../paths.js';
|
|
18
|
+
import { formatPeerKey } from './peer-key.js';
|
|
18
19
|
/**
|
|
19
20
|
* 对端身份缓存管理器
|
|
20
21
|
*/
|
|
@@ -24,16 +25,16 @@ export class PeerIdentityCache {
|
|
|
24
25
|
/**
|
|
25
26
|
* 获取 peer-identity.json 文件路径
|
|
26
27
|
*/
|
|
27
|
-
static getFilePath(
|
|
28
|
-
const peerKey =
|
|
28
|
+
static getFilePath(channelType, peerId, agentDir) {
|
|
29
|
+
const peerKey = formatPeerKey(channelType, peerId);
|
|
29
30
|
return path.join(agentDir, 'relations', peerKey, 'peer-identity.json');
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
32
33
|
* 从文件读取缓存
|
|
33
34
|
* @returns PeerIdentity | null(缓存不存在)
|
|
34
35
|
*/
|
|
35
|
-
static get(
|
|
36
|
-
const filePath = this.getFilePath(
|
|
36
|
+
static get(channelType, peerId, agentDir) {
|
|
37
|
+
const filePath = this.getFilePath(channelType, peerId, agentDir);
|
|
37
38
|
try {
|
|
38
39
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
39
40
|
return JSON.parse(content);
|
|
@@ -47,8 +48,8 @@ export class PeerIdentityCache {
|
|
|
47
48
|
* @param maxAgeMs 最大缓存时间(默认 30 天)
|
|
48
49
|
* @returns true=需要刷新
|
|
49
50
|
*/
|
|
50
|
-
static needsRefresh(
|
|
51
|
-
const cached = this.get(
|
|
51
|
+
static needsRefresh(channelType, peerId, agentDir, maxAgeMs = this.CACHE_MAX_AGE_MS) {
|
|
52
|
+
const cached = this.get(channelType, peerId, agentDir);
|
|
52
53
|
if (!cached)
|
|
53
54
|
return true;
|
|
54
55
|
return Date.now() - cached.lastCheckedAt > maxAgeMs;
|
|
@@ -56,8 +57,8 @@ export class PeerIdentityCache {
|
|
|
56
57
|
/**
|
|
57
58
|
* 从 agent.md 更新身份信息
|
|
58
59
|
*/
|
|
59
|
-
static updateFromAgentMd(
|
|
60
|
-
const typeMatch = agentMd.match(/^type:\s*["']?(\
|
|
60
|
+
static updateFromAgentMd(channelType, peerId, agentDir, agentMd, verifiedAt) {
|
|
61
|
+
const typeMatch = agentMd.match(/^type:\s*["']?([^"'\n]+?)["']?\s*$/m);
|
|
61
62
|
const nameMatch = agentMd.match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
62
63
|
const type = typeMatch?.[1] || 'unknown';
|
|
63
64
|
const isAgent = type !== 'human';
|
|
@@ -75,11 +76,11 @@ export class PeerIdentityCache {
|
|
|
75
76
|
lastCheckedAt: now,
|
|
76
77
|
source: 'agentmd',
|
|
77
78
|
};
|
|
78
|
-
const filePath = this.getFilePath(
|
|
79
|
+
const filePath = this.getFilePath(channelType, peerId, agentDir);
|
|
79
80
|
try {
|
|
80
81
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
81
82
|
fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
|
|
82
|
-
logger.debug(`[PeerIdentityCache] Updated: ${
|
|
83
|
+
logger.debug(`[PeerIdentityCache] Updated: ${channelType}#${peerId} type=${type} isAgent=${isAgent}`);
|
|
83
84
|
}
|
|
84
85
|
catch (err) {
|
|
85
86
|
logger.warn(`[PeerIdentityCache] Failed to write cache: ${filePath} err=${err}`);
|
|
@@ -89,9 +90,9 @@ export class PeerIdentityCache {
|
|
|
89
90
|
/**
|
|
90
91
|
* 仅更新 lastCheckedAt(内容未变时的轻量操作)
|
|
91
92
|
*/
|
|
92
|
-
static touchLastChecked(
|
|
93
|
+
static touchLastChecked(channelType, peerId, agentDir, cached) {
|
|
93
94
|
const updated = { ...cached, lastCheckedAt: Date.now() };
|
|
94
|
-
const filePath = this.getFilePath(
|
|
95
|
+
const filePath = this.getFilePath(channelType, peerId, agentDir);
|
|
95
96
|
try {
|
|
96
97
|
fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), 'utf-8');
|
|
97
98
|
}
|
|
@@ -101,7 +102,7 @@ export class PeerIdentityCache {
|
|
|
101
102
|
/**
|
|
102
103
|
* 标记为 unknown(验签失败或无 agent.md)
|
|
103
104
|
*/
|
|
104
|
-
static markUnknown(
|
|
105
|
+
static markUnknown(channelType, peerId, agentDir) {
|
|
105
106
|
const identity = {
|
|
106
107
|
aid: peerId,
|
|
107
108
|
type: 'unknown',
|
|
@@ -112,11 +113,11 @@ export class PeerIdentityCache {
|
|
|
112
113
|
lastCheckedAt: Date.now(),
|
|
113
114
|
source: 'unknown',
|
|
114
115
|
};
|
|
115
|
-
const filePath = this.getFilePath(
|
|
116
|
+
const filePath = this.getFilePath(channelType, peerId, agentDir);
|
|
116
117
|
try {
|
|
117
118
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
118
119
|
fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
|
|
119
|
-
logger.debug(`[PeerIdentityCache] Marked unknown: ${
|
|
120
|
+
logger.debug(`[PeerIdentityCache] Marked unknown: ${channelType}#${peerId}`);
|
|
120
121
|
}
|
|
121
122
|
catch (err) {
|
|
122
123
|
logger.warn(`[PeerIdentityCache] Failed to write unknown cache: ${filePath} err=${err}`);
|
|
@@ -126,47 +127,37 @@ export class PeerIdentityCache {
|
|
|
126
127
|
/**
|
|
127
128
|
* 完整流程:缓存检查 → agentmdSync(check+fetch)→ 按 changed 决定是否重写
|
|
128
129
|
*
|
|
129
|
-
* @param
|
|
130
|
+
* @param channelType 渠道类型(如 'aun')
|
|
130
131
|
* @param peerId 对端 ID(AUN 是 AID)
|
|
131
132
|
* @param agentDir agent 数据根目录
|
|
132
|
-
* @param
|
|
133
|
+
* @param store AIDStore 实例(由调用方提供,负责 checkAgentMd + downloadAgentMd)
|
|
133
134
|
* @param forceRefresh 强制刷新(忽略缓存时效)
|
|
134
|
-
* @returns PeerIdentity
|
|
135
135
|
*/
|
|
136
|
-
static async resolve(
|
|
136
|
+
static async resolve(channelType, peerId, agentDir, store, forceRefresh = false) {
|
|
137
137
|
// 1. 缓存检查
|
|
138
|
-
if (!forceRefresh && !this.needsRefresh(
|
|
139
|
-
const cached = this.get(
|
|
138
|
+
if (!forceRefresh && !this.needsRefresh(channelType, peerId, agentDir)) {
|
|
139
|
+
const cached = this.get(channelType, peerId, agentDir);
|
|
140
140
|
if (cached) {
|
|
141
|
-
logger.debug(`[PeerIdentityCache] Cache hit: ${
|
|
141
|
+
logger.debug(`[PeerIdentityCache] Cache hit: ${channelType}#${peerId} type=${cached.type} age=${Math.floor((Date.now() - cached.lastCheckedAt) / 1000 / 60 / 60 / 24)}d`);
|
|
142
142
|
return cached;
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
-
// 2.
|
|
145
|
+
// 2. 通过 agentmdSync 拉取(内部走 store.checkAgentMd → store.downloadAgentMd)
|
|
146
146
|
try {
|
|
147
|
-
logger.debug(`[PeerIdentityCache] Syncing agent.md: ${
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// 本地已是最新,读本地文件
|
|
152
|
-
const localPath = agentMdPath(peerId);
|
|
153
|
-
try {
|
|
154
|
-
content = fs.readFileSync(localPath, 'utf-8');
|
|
155
|
-
}
|
|
156
|
-
catch { /* ignore */ }
|
|
157
|
-
}
|
|
147
|
+
logger.debug(`[PeerIdentityCache] Syncing agent.md: ${channelType}#${peerId}`);
|
|
148
|
+
const { agentmdSync } = await import('../../aun/aid/agentmd.js');
|
|
149
|
+
const result = await agentmdSync(peerId, { store });
|
|
150
|
+
const content = result.content;
|
|
158
151
|
if (!content) {
|
|
159
|
-
|
|
160
|
-
const info = await aunClient.fetchAgentMd(peerId);
|
|
161
|
-
content = info.content;
|
|
152
|
+
throw new Error('agent.md content unavailable');
|
|
162
153
|
}
|
|
163
154
|
// 3. 比较 hash,仅在变化时重写 peer-identity.json
|
|
164
155
|
const newHash = 'sha256:' + crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
165
|
-
const cached = this.get(
|
|
156
|
+
const cached = this.get(channelType, peerId, agentDir);
|
|
166
157
|
if (cached && cached.agentMdHash === newHash && cached.source === 'agentmd') {
|
|
167
|
-
return this.touchLastChecked(
|
|
158
|
+
return this.touchLastChecked(channelType, peerId, agentDir, cached);
|
|
168
159
|
}
|
|
169
|
-
return this.updateFromAgentMd(
|
|
160
|
+
return this.updateFromAgentMd(channelType, peerId, agentDir, content, Date.now());
|
|
170
161
|
}
|
|
171
162
|
catch (err) {
|
|
172
163
|
// 4. 网络失败,fallback 本地文件
|
|
@@ -176,26 +167,26 @@ export class PeerIdentityCache {
|
|
|
176
167
|
const localContent = fs.readFileSync(localPath, 'utf-8');
|
|
177
168
|
logger.info(`[PeerIdentityCache] Network failed, using local agent.md for ${peerId}`);
|
|
178
169
|
const localHash = 'sha256:' + crypto.createHash('sha256').update(localContent, 'utf-8').digest('hex');
|
|
179
|
-
const cached = this.get(
|
|
170
|
+
const cached = this.get(channelType, peerId, agentDir);
|
|
180
171
|
if (cached && cached.agentMdHash === localHash && cached.source === 'agentmd') {
|
|
181
|
-
return this.touchLastChecked(
|
|
172
|
+
return this.touchLastChecked(channelType, peerId, agentDir, cached);
|
|
182
173
|
}
|
|
183
|
-
return this.updateFromAgentMd(
|
|
174
|
+
return this.updateFromAgentMd(channelType, peerId, agentDir, localContent, cached?.verifiedAt ?? 0);
|
|
184
175
|
}
|
|
185
176
|
}
|
|
186
177
|
catch { /* ignore fs errors */ }
|
|
187
|
-
logger.warn(`[PeerIdentityCache] Failed to resolve: ${
|
|
188
|
-
return this.markUnknown(
|
|
178
|
+
logger.warn(`[PeerIdentityCache] Failed to resolve: ${channelType}#${peerId} err=${err instanceof Error ? err.message : String(err)}`);
|
|
179
|
+
return this.markUnknown(channelType, peerId, agentDir);
|
|
189
180
|
}
|
|
190
181
|
}
|
|
191
182
|
/**
|
|
192
183
|
* 清除指定对端的缓存
|
|
193
184
|
*/
|
|
194
|
-
static clear(
|
|
195
|
-
const filePath = this.getFilePath(
|
|
185
|
+
static clear(channelType, peerId, agentDir) {
|
|
186
|
+
const filePath = this.getFilePath(channelType, peerId, agentDir);
|
|
196
187
|
try {
|
|
197
188
|
fs.unlinkSync(filePath);
|
|
198
|
-
logger.debug(`[PeerIdentityCache] Cleared: ${
|
|
189
|
+
logger.debug(`[PeerIdentityCache] Cleared: ${channelType}#${peerId}`);
|
|
199
190
|
}
|
|
200
191
|
catch {
|
|
201
192
|
// 文件不存在,忽略
|