evolclaw 3.1.11 → 3.2.0
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 +24 -0
- package/README.md +26 -0
- package/dist/agents/kit-renderer.js +5 -1
- package/dist/agents/manifest-engine.js +108 -35
- package/dist/agents/message-renderer.js +2 -0
- package/dist/aun/aid/control-aid.js +67 -0
- package/dist/aun/aid/identity.js +20 -7
- package/dist/aun/aid/store.js +2 -2
- package/dist/channels/aun.js +161 -61
- package/dist/channels/feishu.js +3 -3
- package/dist/cli/agent.js +38 -10
- package/dist/cli/index.js +31 -3
- package/dist/cli/init-channel.js +38 -148
- package/dist/cli/init.js +162 -82
- package/dist/config-store.js +38 -7
- package/dist/core/cache/file-cache.js +216 -0
- package/dist/core/command-handler.js +291 -68
- package/dist/core/evolagent-registry.js +3 -0
- package/dist/core/evolagent.js +28 -23
- package/dist/core/message/command-handler-agent-control.js +153 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/message-bridge.js +5 -3
- package/dist/core/message/message-processor.js +44 -36
- package/dist/core/message/message-queue.js +13 -6
- package/dist/core/model/model-scope.js +39 -6
- package/dist/core/session/adapters/claude-session-file-adapter.js +48 -5
- package/dist/evolclaw-config.js +11 -0
- package/dist/index.js +57 -2
- package/dist/ipc.js +6 -0
- package/dist/paths.js +7 -3
- package/kits/templates/message-fragments/item.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.2.0 (2026-06-05)
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
- **控制 AID(control AID)** — 进程级控制身份,启动时以 `pureIdentity` 模式连接 AUN(跳过 evolagent onboarding)。生成采用 `ec+5位数字` 候选 + PKI 权威查重 + fail-fast;缺失时进入 init(TTY 守卫,headless 仅告警)。`evolclaw status` 新增控制 AID 连接状态展示
|
|
8
|
+
- **Menu 协议 / agent 控制面** — `/system`、`/agent` 迁移到 owners 鉴权;trigger 接入菜单协议(直连 manager/scheduler,无文本拼装);agent 创建支持 accepted-return + 构建进度(`create-status.json` + `onPhase` 回调 + model/chatmode);新增 agent query/options 与 project 兜底
|
|
9
|
+
- **进程级 owners 配置层** — 新增 `evolclaw.json` 进程级配置,`config.json` 合并入 `evolclaw.json`(弃用 `ProcessConfig`);process-level owners 从 `defaults.json` 迁移到 `evolclaw.json`;新增 `isProcessLevelOwner` 鉴权辅助
|
|
10
|
+
- **Observer 模式重构** — AUN owner 间消息互转发;`evolclaw init aun` 简化为 owner-only 配置;`mergeForAgent` 输出补全 `dispatch`/`observable`
|
|
11
|
+
|
|
12
|
+
### Improvements
|
|
13
|
+
|
|
14
|
+
- **统一 FileCache** — 新增 mtime-gated 统一文件缓存,迁移 relation prefs、manifest+fragment、persona/working 读取与 model-scope 缓存;新增 Cache watch 视图监控缓存命中
|
|
15
|
+
- **消息信封渲染补全** — @AID 列表与群名补全,修复 proximity 信息丢失
|
|
16
|
+
- **Idle 监控解耦** — idle notify/warn 改为事件总线发布(`runner:idle-notify`/`runner:idle-warn`,携带 idleSec/事件数/工具名),与通道发送解耦;超时诊断信息下沉到事件 payload
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
- **多 agent 群广播去重** — 消息队列改按 `sessionKey:messageId` 去重,允许同一消息广播给多个 agent
|
|
21
|
+
- **AUN owner 入站转发** — owner 来源的入站消息正确转发给其它 owners
|
|
22
|
+
- **单 agent reload 缓存失效** — reload 时失效 identity 层缓存
|
|
23
|
+
- **CLI /slist 过滤** — 过滤掉程序化 SDK session,不在 `/slist` 显示
|
|
24
|
+
- **控制 AID 查重走 GET** — 改用 `store.resolve`(GET)替代 `store.exists`(HEAD),规避部分 Gateway 对 HEAD 空响应断连导致的误判
|
|
25
|
+
- **启动门控修复** — gate 直接调 `initTail` 而非完整 `cmdInit`;AID 生成失败时跳过 owners 提示;抑制控制 AID gate 路径的 SDK keystore 日志
|
|
26
|
+
|
|
3
27
|
## v3.1.11 (2026-06-04)
|
|
4
28
|
|
|
5
29
|
### Improvements
|
package/README.md
CHANGED
|
@@ -267,6 +267,32 @@ evolclaw/
|
|
|
267
267
|
- `/restart` - 重启服务(自愈机制)
|
|
268
268
|
- `/repair` - 检查并修复会话
|
|
269
269
|
|
|
270
|
+
### ⚠️ 进程级 menu 操作鉴权(v3.2 Breaking)
|
|
271
|
+
|
|
272
|
+
进程级 menu 操作(`/system restart/upgrade`、`/agent` agent 生命周期管理)的鉴权已迁移到
|
|
273
|
+
`evolclaw.json` 顶层 `owners` 字段(v3.2 起,不再读 `agents/defaults.json`)。
|
|
274
|
+
升级后**必须**在 `evolclaw.json` 配置 `owners`,否则这些操作一律返回 `FORBIDDEN`(daemon 启动时也会 warn 提示)。
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"owners": ["eleans-2022.agentid.pub"]
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`evolclaw init` 交互流程会在生成控制 AID 后提示录入 owners(可跳过后手动编辑)。
|
|
283
|
+
|
|
284
|
+
- **`owners`**:进程级管理者 AID 名单。可执行 `/system`(重启/升级)与 `/agent`
|
|
285
|
+
(create / delete / enable / disable / list / show)。
|
|
286
|
+
- 关系级的 `/trigger`(set / cancel / update / list)仍走 channel 角色(owner/admin)+ scoped 鉴权,**不**受 `owners` 影响。
|
|
287
|
+
- `/agent create` 为「受理即返回」:立即回 `{ accepted: true, aid }`,后台跑完整创建流程并把各
|
|
288
|
+
环节写入 `agents/<aid>/create-status.json`;客户端用 `menu.query name=agent args={aid}` 轮询
|
|
289
|
+
`createProgress.status` 直到 `ready` / `failed`。
|
|
290
|
+
|
|
291
|
+
### 控制 AID(control AID)
|
|
292
|
+
|
|
293
|
+
v3.2 新增进程级身份标识。启动时自动生成 `ec+5位数字.agentid.pub` 格式的控制 AID,以
|
|
294
|
+
`pureIdentity` 模式接入 AUN 网络(跳过 evolagent onboarding)。`evolclaw status` 可查看控制 AID 连接状态。
|
|
295
|
+
|
|
270
296
|
## 技术栈
|
|
271
297
|
|
|
272
298
|
- **运行时**:Node.js >= 22 + TypeScript(ES modules)
|
|
@@ -61,7 +61,11 @@ export function renderKitSections(ctx) {
|
|
|
61
61
|
}
|
|
62
62
|
const files = loadSectionFiles(section, ctx.vars, sessionCache);
|
|
63
63
|
diag.fileCount = files.length;
|
|
64
|
+
// 路径解析成功但读出 0 文件 → 文件/目录不存在(存在性不再单独 syscall,
|
|
65
|
+
// 由内容读取顺带得到;详见 manifest-engine.resolvePathWithDiag)。
|
|
64
66
|
if (files.length === 0) {
|
|
67
|
+
if (diag.resolveStatus === 'ok')
|
|
68
|
+
diag.resolveStatus = 'not-exist';
|
|
65
69
|
diagnostics.push(diag);
|
|
66
70
|
continue;
|
|
67
71
|
}
|
|
@@ -125,7 +129,7 @@ const PARAM_DESCRIPTIONS = {
|
|
|
125
129
|
peerName: '对端显示名',
|
|
126
130
|
peerRole: '对端角色(owner/admin/guest/anonymous)',
|
|
127
131
|
peerType: '对端类型(human/agent)',
|
|
128
|
-
sameDevice: '对端与本端同一物理设备(
|
|
132
|
+
sameDevice: '对端与本端同一物理设备(SDK 0.4.9 起明文/密文消息均可携带,具体字段以网关下发为准)',
|
|
129
133
|
sameNetwork: '对端与本端在同一网络内',
|
|
130
134
|
sameEgressIp: '对端与本端共享同一出口 IP',
|
|
131
135
|
groupId: '群组 ID(群聊时)',
|
|
@@ -5,24 +5,25 @@ import fs from 'fs';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { kitsDir, resolveRoot, getPackageRoot } from '../paths.js';
|
|
7
7
|
import { logger } from '../utils/logger.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import { fileCache } from '../core/cache/file-cache.js';
|
|
9
|
+
// ── Manifest loading / cache ──
|
|
10
|
+
// manifest 定义随包发布、运行期靠 reload/重启刷新 → on-reload(group 'kits')。
|
|
11
|
+
// base + eck override 合成结果以 base 文件路径为键缓存;loader 内读两个文件。
|
|
12
|
+
/** 清空所有 manifest 缓存(manifest 结构变更后调用,由 invalidateKitCache 串联)。 */
|
|
11
13
|
export function invalidateManifestCache() {
|
|
12
|
-
|
|
14
|
+
fileCache.invalidateGroup('kits');
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* 加载并合并 manifest。基础文件在 $KITS/<filename>,
|
|
16
18
|
* 覆盖文件在 $EVOLCLAW_HOME/eck/<filename>(可选)。结果按 order 升序缓存。
|
|
17
19
|
*/
|
|
18
20
|
export function loadManifest(filename) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return sections;
|
|
21
|
+
const kitsPath = path.join(kitsDir(), filename);
|
|
22
|
+
return fileCache.get(kitsPath, () => {
|
|
23
|
+
const sections = loadAndMergeManifest(filename);
|
|
24
|
+
logger.info(`[ManifestEngine] Loaded ${filename}: ${sections.length} sections`);
|
|
25
|
+
return sections;
|
|
26
|
+
}, { policy: 'on-reload', group: 'kits' });
|
|
26
27
|
}
|
|
27
28
|
function loadAndMergeManifest(filename) {
|
|
28
29
|
const kitsPath = path.join(kitsDir(), filename);
|
|
@@ -86,9 +87,72 @@ export function evaluateWhen(when, vars) {
|
|
|
86
87
|
return true;
|
|
87
88
|
}
|
|
88
89
|
export function isTruthy(val) {
|
|
90
|
+
if (Array.isArray(val))
|
|
91
|
+
return val.length > 0; // 空数组视为假,使 {{?arr}} / {{#each}} 落空
|
|
89
92
|
return val !== undefined && val !== null && val !== false && val !== '' && val !== 0;
|
|
90
93
|
}
|
|
91
94
|
// ── Template rendering ──
|
|
95
|
+
/**
|
|
96
|
+
* 展开 {{#each KEY}}BODY{{/each}} 循环块(在条件/变量替换之前跑)。
|
|
97
|
+
* - vars[KEY] 为非空数组才展开;每个元素构造子作用域:
|
|
98
|
+
* 对象元素 → { ...vars, ...el }(字段可用 {{field}} 访问)
|
|
99
|
+
* 标量元素 → { ...vars, '.': el }({{.}} 访问当前元素)
|
|
100
|
+
* 另注入 {{@index}}(0 基序号)。
|
|
101
|
+
* - body 经完整 renderTemplate 递归渲染,天然支持嵌套 each / 条件。
|
|
102
|
+
* - 非数组或空数组 → 整块渲染为空串。
|
|
103
|
+
* 用深度扫描定位**最外层** each 块(正则无法平衡嵌套),从外向内展开。
|
|
104
|
+
*/
|
|
105
|
+
function resolveEach(template, vars, stripBlankLines) {
|
|
106
|
+
const OPEN = /\{\{#each\s+([A-Za-z_]\w*)\}\}/g;
|
|
107
|
+
let result = '';
|
|
108
|
+
let cursor = 0;
|
|
109
|
+
OPEN.lastIndex = 0;
|
|
110
|
+
let m;
|
|
111
|
+
while ((m = OPEN.exec(template)) !== null) {
|
|
112
|
+
const blockStart = m.index;
|
|
113
|
+
const key = m[1];
|
|
114
|
+
const bodyStart = OPEN.lastIndex;
|
|
115
|
+
// 从 bodyStart 起按深度找配对的 {{/each}}
|
|
116
|
+
const TOKEN = /\{\{#each\s+[A-Za-z_]\w*\}\}|\{\{\/each\}\}/g;
|
|
117
|
+
TOKEN.lastIndex = bodyStart;
|
|
118
|
+
let depth = 1;
|
|
119
|
+
let bodyEnd = -1;
|
|
120
|
+
let blockEnd = -1;
|
|
121
|
+
let t;
|
|
122
|
+
while ((t = TOKEN.exec(template)) !== null) {
|
|
123
|
+
if (t[0].startsWith('{{#each'))
|
|
124
|
+
depth++;
|
|
125
|
+
else {
|
|
126
|
+
depth--;
|
|
127
|
+
if (depth === 0) {
|
|
128
|
+
bodyEnd = t.index;
|
|
129
|
+
blockEnd = TOKEN.lastIndex;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (bodyEnd === -1)
|
|
135
|
+
break; // 无配对,剩余原样输出
|
|
136
|
+
// 输出块前的原文
|
|
137
|
+
result += template.slice(cursor, blockStart);
|
|
138
|
+
const body = template.slice(bodyStart, bodyEnd);
|
|
139
|
+
const arr = vars[key];
|
|
140
|
+
if (Array.isArray(arr)) {
|
|
141
|
+
for (let i = 0; i < arr.length; i++) {
|
|
142
|
+
const el = arr[i];
|
|
143
|
+
const scope = (el && typeof el === 'object' && !Array.isArray(el))
|
|
144
|
+
? { ...vars, ...el, '@index': i }
|
|
145
|
+
: { ...vars, '.': el, '@index': i };
|
|
146
|
+
result += renderTemplate(body, scope, stripBlankLines);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// 数组以外(含 undefined / 非数组)→ 整块跳过(不输出)
|
|
150
|
+
cursor = blockEnd;
|
|
151
|
+
OPEN.lastIndex = blockEnd;
|
|
152
|
+
}
|
|
153
|
+
result += template.slice(cursor);
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
92
156
|
function resolveConditions(template, vars) {
|
|
93
157
|
// 只匹配**最内层** {{?...}}...{{/}} 块(逐字符负向前瞻排除嵌套),do/while 由内向外消解。
|
|
94
158
|
const inner = /\{\{\?(\w+)(?:(!=|=)([^}]*))?\}\}((?:(?!\{\{\?)[^])*?)\{\{\/\}\}/;
|
|
@@ -111,11 +175,15 @@ function resolveConditions(template, vars) {
|
|
|
111
175
|
* 紧凑);false 时保留空行(消息正文用,正文多段结构不能被压扁)。
|
|
112
176
|
*/
|
|
113
177
|
export function renderTemplate(template, vars, stripBlankLines = true) {
|
|
114
|
-
let result =
|
|
115
|
-
result = result
|
|
178
|
+
let result = resolveEach(template, vars, stripBlankLines);
|
|
179
|
+
result = resolveConditions(result, vars);
|
|
180
|
+
// 变量替换:支持普通名、当前元素 {{.}}、循环序号 {{@index}}。
|
|
181
|
+
result = result.replace(/\{\{(\.|@index|\w+)\}\}/g, (_match, key) => {
|
|
116
182
|
const val = vars[key];
|
|
117
|
-
if (!isTruthy(val))
|
|
118
|
-
return '';
|
|
183
|
+
if (!isTruthy(val) && val !== 0)
|
|
184
|
+
return ''; // 0 是有效序号/值,保留
|
|
185
|
+
if (val === 0)
|
|
186
|
+
return '0';
|
|
119
187
|
return String(val);
|
|
120
188
|
});
|
|
121
189
|
if (stripBlankLines)
|
|
@@ -146,11 +214,10 @@ export function resolvePathWithDiag(rawPath, vars) {
|
|
|
146
214
|
if (unresolved.length > 0) {
|
|
147
215
|
return { resolved, status: 'unresolved-vars', unresolvedTokens: unresolved };
|
|
148
216
|
}
|
|
149
|
-
// 路径规范化:模板里 ../
|
|
217
|
+
// 路径规范化:模板里 ../ 等相对片段折叠成真实路径。
|
|
218
|
+
// 不再在此 existsSync——存在性由随后经 fileCache 的内容读取顺带得到(file
|
|
219
|
+
// section 读出 null 即不存在),避免每 section 每消息一次 syscall。
|
|
150
220
|
resolved = path.normalize(resolved);
|
|
151
|
-
if (!fs.existsSync(resolved)) {
|
|
152
|
-
return { resolved, status: 'not-exist', unresolvedTokens: unresolved };
|
|
153
|
-
}
|
|
154
221
|
return { resolved, status: 'ok', unresolvedTokens: unresolved };
|
|
155
222
|
}
|
|
156
223
|
function resolvePath(rawPath, vars) {
|
|
@@ -174,29 +241,35 @@ export function loadSectionFiles(section, vars, sessionCache) {
|
|
|
174
241
|
return [];
|
|
175
242
|
}
|
|
176
243
|
function loadFileSection(filePath, vars, sessionCache) {
|
|
244
|
+
void sessionCache; // 内容跨 session 共享,改走全局 fileCache(on-reload)
|
|
177
245
|
const resolved = resolvePath(filePath, vars);
|
|
178
246
|
if (!resolved)
|
|
179
247
|
return null;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
sessionCache.set(resolved, content);
|
|
185
|
-
return [resolved, content];
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
248
|
+
// 内容跨 session 共享:用全局 fileCache(on-reload,reload/重启时失效),
|
|
249
|
+
// 不再按 session 重复缓存同一文件内容。
|
|
250
|
+
const content = fileCache.getText(resolved, { policy: 'on-reload', group: 'kits' });
|
|
251
|
+
return content === null ? null : [resolved, content];
|
|
190
252
|
}
|
|
191
253
|
function readDirectoryFiles(dirPath, pattern) {
|
|
192
254
|
const glob = pattern || '*.md';
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
255
|
+
// 目录列表 + 各文件内容均走 fileCache(on-reload)。目录列表以 "<dir>|<glob>"
|
|
256
|
+
// 为键缓存文件名数组;各文件内容走 fileCache.getText 共享。
|
|
257
|
+
const names = fileCache.get(`${dirPath} ${glob}`, () => {
|
|
258
|
+
try {
|
|
259
|
+
return fs.readdirSync(dirPath).filter(f => matchGlob(f, glob)).sort();
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
}, { policy: 'on-reload', group: 'kits' });
|
|
265
|
+
const out = [];
|
|
266
|
+
for (const f of names) {
|
|
267
|
+
const fp = path.join(dirPath, f);
|
|
268
|
+
const content = fileCache.getText(fp, { policy: 'on-reload', group: 'kits' });
|
|
269
|
+
if (content !== null)
|
|
270
|
+
out.push([f, content]);
|
|
199
271
|
}
|
|
272
|
+
return out;
|
|
200
273
|
}
|
|
201
274
|
function matchGlob(filename, pattern) {
|
|
202
275
|
const regex = pattern
|
|
@@ -44,6 +44,8 @@ function renderOneItem(item, sessionVars, sessionCache, contentSentinel) {
|
|
|
44
44
|
sameDevice: item.sameDevice ?? sessionVars.sameDevice,
|
|
45
45
|
sameNetwork: item.sameNetwork ?? sessionVars.sameNetwork,
|
|
46
46
|
sameEgressIp: item.sameEgressIp ?? sessionVars.sameEgressIp,
|
|
47
|
+
// 模板引擎不支持数组循环:被 @ 的 AID 预先 join 成串,空则 undefined 使 {{?mentionAids}} 落空。
|
|
48
|
+
mentionAids: (item.mentionAids && item.mentionAids.length > 0) ? item.mentionAids.join(',') : undefined,
|
|
47
49
|
now: formatLocalTime(item.timestamp ?? Date.now(), sessionVars.timezone ? String(sessionVars.timezone) : undefined),
|
|
48
50
|
// content held as a per-call random sentinel, swapped back post-render.
|
|
49
51
|
// Using a UUID means no real message can collide with it.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { aidCreate } from './index.js';
|
|
3
|
+
import { getAidStore, SLOT } from './store.js';
|
|
4
|
+
import { logger } from '../../utils/logger.js';
|
|
5
|
+
const MAX_ATTEMPTS = 5;
|
|
6
|
+
/** 生成候选控制 AID:ec + 5位随机数字 + .agentid.pub */
|
|
7
|
+
export function candidateAid() {
|
|
8
|
+
const n = crypto.randomInt(10000, 100000); // 5 位:10000-99999
|
|
9
|
+
return `ec${n}.agentid.pub`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 候选 AID 是否已在 PKI 注册。
|
|
13
|
+
*
|
|
14
|
+
* 不用 store.exists(它走 HTTP HEAD /pki/cert/<aid>)——部分 Gateway 实现
|
|
15
|
+
* (Python websockets HTTP 处理)对 HEAD 直接空响应断连(curl 52 / socket hang up),
|
|
16
|
+
* 导致 exists 误报"网关不可达"。改用 store.resolve(走 GET),语义等价且 GET 正常返回:
|
|
17
|
+
* - resolve ok → 证书存在(HTTP 200)→ 已注册
|
|
18
|
+
* - CERT_NOT_FOUND → 404 → 未注册
|
|
19
|
+
* - 其它 error → 真·网络错误,向上抛出供 fail-fast
|
|
20
|
+
* skipAgentMd:true 避免多拉一次 agent.md(控制 AID 本就不传 agent.md)。
|
|
21
|
+
*/
|
|
22
|
+
async function candidateExists(store, candidate) {
|
|
23
|
+
const r = await store.resolve(candidate, { skipAgentMd: true });
|
|
24
|
+
if (r.ok)
|
|
25
|
+
return true;
|
|
26
|
+
if (r.error?.code === 'CERT_NOT_FOUND')
|
|
27
|
+
return false;
|
|
28
|
+
throw new Error(`Gateway 不可达,无法查重控制 AID:${r.error?.message ?? 'unknown'}`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 生成控制 AID:循环候选 → candidateExists 查重(权威 PKI 判据)→ 不冲突则 aidCreate。
|
|
32
|
+
* - 查重走 GET 证书(见 candidateExists;不拉 agent.md,控制 AID 本就不传 agent.md)
|
|
33
|
+
* - fail-fast:查重探测失败(网关不可达)立即抛错,不掩盖成"均冲突"
|
|
34
|
+
* - agent.md 不上传:aidCreate 仅注册身份 + 写私钥,不调 agentmdPut
|
|
35
|
+
*/
|
|
36
|
+
export async function generateControlAid() {
|
|
37
|
+
const store = await getAidStore({ slotId: SLOT.cli });
|
|
38
|
+
try {
|
|
39
|
+
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
|
40
|
+
const candidate = candidateAid();
|
|
41
|
+
if (await candidateExists(store, candidate)) {
|
|
42
|
+
logger.info(`[control-aid] ${candidate} 已注册,重试 (${i + 1}/${MAX_ATTEMPTS})`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const created = await aidCreate(candidate);
|
|
46
|
+
// 清理 aidCreate 内部另开的 client/store——关闭失败不可丢弃已注册的 AID(否则下次 init
|
|
47
|
+
// 会把它当冲突,白白消耗一次重试)。close 异常降级为 warn。
|
|
48
|
+
try {
|
|
49
|
+
await created.client?.close?.();
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
logger.warn(`[control-aid] client.close() 失败(非致命): ${e}`);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
await created.store?.close?.();
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
logger.warn(`[control-aid] store.close() 失败(非致命): ${e}`);
|
|
59
|
+
}
|
|
60
|
+
return { aid: created.aid, gateway: created.gateway };
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`无法生成控制 AID:连续 ${MAX_ATTEMPTS} 次候选均冲突`);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
store.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/aun/aid/identity.js
CHANGED
|
@@ -443,6 +443,7 @@ export async function probePkiRecoverability(aid, opts) {
|
|
|
443
443
|
}
|
|
444
444
|
// ==================== Lookup ====================
|
|
445
445
|
export async function aidLookup(aid) {
|
|
446
|
+
// gateway:well-known 探测(保留,供 aid lookup 命令展示)
|
|
446
447
|
let gateway = '';
|
|
447
448
|
try {
|
|
448
449
|
const gwResp = await fetch(`https://${aid}/.well-known/aun-gateway`, { redirect: 'follow' });
|
|
@@ -464,16 +465,28 @@ export async function aidLookup(aid) {
|
|
|
464
465
|
}
|
|
465
466
|
}
|
|
466
467
|
catch { /* ignore */ }
|
|
468
|
+
const { agentmdGet } = await import('./agentmd.js');
|
|
469
|
+
const store = await getAidStore({ slotId: SLOT.cli });
|
|
467
470
|
try {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
return { exists:
|
|
471
|
+
// 权威注册判据:PKI 证书 HEAD(与 agent.md 无关)
|
|
472
|
+
const existsResult = await store.exists(aid);
|
|
473
|
+
if (!existsResult.ok) {
|
|
474
|
+
return { exists: false, aid, gateway, error: existsResult.error?.message ?? 'exists check failed' };
|
|
472
475
|
}
|
|
473
|
-
|
|
476
|
+
const exists = existsResult.data.exists;
|
|
477
|
+
if (!exists) {
|
|
478
|
+
return { exists: false, aid, gateway };
|
|
479
|
+
}
|
|
480
|
+
// 已注册:尽力拉 agent.md content(无 agent.md 不影响 exists)
|
|
481
|
+
let content;
|
|
482
|
+
try {
|
|
483
|
+
content = await agentmdGet(aid, { store });
|
|
484
|
+
}
|
|
485
|
+
catch { /* registered but no agent.md — content stays undefined */ }
|
|
486
|
+
return { exists, aid, gateway, content };
|
|
474
487
|
}
|
|
475
|
-
|
|
476
|
-
|
|
488
|
+
finally {
|
|
489
|
+
store.close();
|
|
477
490
|
}
|
|
478
491
|
}
|
|
479
492
|
function lifecycleLogPath(aid) {
|
package/dist/aun/aid/store.js
CHANGED
|
@@ -39,10 +39,10 @@ export class AidLoadError extends Error {
|
|
|
39
39
|
*/
|
|
40
40
|
export async function getAidStore(opts) {
|
|
41
41
|
const { aunPath: defaultAunPath } = await import('../../paths.js');
|
|
42
|
-
const {
|
|
42
|
+
const { loadEvolclawConfig } = await import('../../evolclaw-config.js');
|
|
43
43
|
const { AIDStore } = await import('@agentunion/fastaun');
|
|
44
44
|
const aunPath = opts.aunPath ?? defaultAunPath();
|
|
45
|
-
const encryptionSeed =
|
|
45
|
+
const encryptionSeed = loadEvolclawConfig().aun?.encryptionSeed
|
|
46
46
|
?? process.env.AUN_ENCRYPTION_SEED
|
|
47
47
|
?? 'evol';
|
|
48
48
|
const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
|