evolclaw 3.1.10 → 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 +38 -0
- package/README.md +26 -4
- 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 +212 -158
- package/dist/channels/feishu.js +10 -14
- package/dist/channels/wechat.js +8 -2
- package/dist/cli/agent.js +38 -10
- package/dist/cli/index.js +50 -8
- 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/dist/utils/media-cache.js +40 -1
- package/dist/utils/npm-ops.js +13 -3
- package/kits/templates/message-fragments/item.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
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
|
+
|
|
27
|
+
## v3.1.11 (2026-06-04)
|
|
28
|
+
|
|
29
|
+
### Improvements
|
|
30
|
+
|
|
31
|
+
- **统一入站图片识别** — 新增 `bufferToInboundImage()`,AUN/飞书/微信共用 magic-bytes → 元数据 → 后缀判定链,消除各通道重复实现的 `detectImageMime`
|
|
32
|
+
- **AUN 附件处理重构** — 抽出 `processAttachments()`,私聊/群聊统一处理;图片注入视觉通道不再追加冗余 `[文件: …]` 文本行
|
|
33
|
+
- **入站去抖** — bridge 默认 inbound debounce 2s → 0(消息即处理)
|
|
34
|
+
- **Windows npm 安装** — `npmInstallGlobal` 改用 `cmd /c`,消除 Node 22 的 `shell:true` 弃用警告
|
|
35
|
+
|
|
36
|
+
### Bug Fixes
|
|
37
|
+
|
|
38
|
+
- **截断图片缓冲** — `validateImage` 捕获 image-type 的 EndOfStreamError,避免短/截断 buffer 抛异常
|
|
39
|
+
- **evolclaw-web 退出容错** — `evolclaw web` 容忍前台进程信号终止(SIGINT/SIGKILL)/非零退出,不再向用户打印堆栈
|
|
40
|
+
|
|
3
41
|
## v3.1.10 (2026-06-04)
|
|
4
42
|
|
|
5
43
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -13,7 +13,6 @@ EvolClaw 是一个轻量级 AI Agent 网关系统。它为 Claude Code / Codex
|
|
|
13
13
|
- 👥 **双模式会话**:多用户私聊会话隔离,群聊会话共享,满足不同协作场景
|
|
14
14
|
- 🌐 **多渠道接入**:Channel Adapter 模式,飞书 + 微信 + 钉钉 + QQ频道 + 企业微信 + AUN 网络
|
|
15
15
|
- 🤖 **Agent 间互联**:通过 AUN 网络,你的 Agent 可被其他 Agent 发现和调用
|
|
16
|
-
- 🖥️ **终端 TUI 客户端**:`evolclaw tui` 直接在终端与远程 Agent 对话,无需 IM
|
|
17
16
|
- 🔐 **分层权限**:三级权限体系(user/admin/owner),多用户安全隔离
|
|
18
17
|
- 🛠️ **Agent 自管理**:Agent 可通过 CLI 命令自主管理运行时(查看状态、切换模型、调整配置等)
|
|
19
18
|
- 📦 **项目搬家**:`evolclaw mv` 一键迁移项目目录,保留 Claude/Codex/EvolClaw 全部会话历史
|
|
@@ -29,7 +28,6 @@ EvolClaw 是一个轻量级 AI Agent 网关系统。它为 Claude Code / Codex
|
|
|
29
28
|
|
|
30
29
|
- **通勤路上**:手机打开飞书,继续昨晚的代码 review,到公司无缝切回终端
|
|
31
30
|
- **会议间隙**:微信快速问一句「这个接口的返回格式是什么」,Agent 直接查代码回复
|
|
32
|
-
- **终端直连**:`evolclaw tui` 在任意终端直接与远程 Agent 对话,无需打开 IM
|
|
33
31
|
- **Agent 协作**:通过 AUN 网络,让你的 Agent 被其他 Agent 调用,组成分布式协作
|
|
34
32
|
- **外出离开工位**:不带电脑也能通过 IM 给 Agent 下达任务,回来看结果
|
|
35
33
|
- **团队协作**:拉个飞书群,成员共享同一个 Agent 会话,一起讨论和调试
|
|
@@ -170,7 +168,6 @@ evolclaw stop # 停止服务
|
|
|
170
168
|
evolclaw restart # 重启服务
|
|
171
169
|
evolclaw status # 查看状态
|
|
172
170
|
evolclaw logs # 查看日志(tail -f)
|
|
173
|
-
evolclaw tui # 启动 AUN TUI 终端客户端
|
|
174
171
|
evolclaw agent # 管理 EvolAgent(list / show / new / reload)
|
|
175
172
|
evolclaw mv <old> <new> # 项目搬家(保留全部会话)
|
|
176
173
|
evolclaw diagnose # 诊断启动环境
|
|
@@ -270,6 +267,32 @@ evolclaw/
|
|
|
270
267
|
- `/restart` - 重启服务(自愈机制)
|
|
271
268
|
- `/repair` - 检查并修复会话
|
|
272
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
|
+
|
|
273
296
|
## 技术栈
|
|
274
297
|
|
|
275
298
|
- **运行时**:Node.js >= 22 + TypeScript(ES modules)
|
|
@@ -281,7 +304,6 @@ evolclaw/
|
|
|
281
304
|
## TODO
|
|
282
305
|
|
|
283
306
|
- [x] AUN Mesh 网络通道接入
|
|
284
|
-
- [x] TUI 终端客户端(`evolclaw tui`)
|
|
285
307
|
- [x] 项目搬家工具(`evolclaw mv`)
|
|
286
308
|
- [x] 手动授权支持(文本回复 + 飞书卡片)
|
|
287
309
|
- [x] 自动授权可配置(自动放行/自动拒绝)
|
|
@@ -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');
|