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.
Files changed (85) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/agents/claude-runner.js +348 -156
  3. package/dist/agents/kit-renderer.js +176 -21
  4. package/dist/aun/aid/agentmd.js +68 -103
  5. package/dist/aun/aid/client.js +1 -29
  6. package/dist/aun/aid/identity.js +105 -64
  7. package/dist/aun/aid/index.js +2 -1
  8. package/dist/aun/aid/store.js +74 -0
  9. package/dist/aun/msg/p2p.js +26 -2
  10. package/dist/aun/rpc/connection.js +23 -30
  11. package/dist/channels/aun.js +77 -88
  12. package/dist/channels/dingtalk.js +1 -0
  13. package/dist/channels/feishu.js +270 -190
  14. package/dist/channels/qqbot.js +1 -0
  15. package/dist/channels/wechat.js +1 -0
  16. package/dist/channels/wecom.js +1 -0
  17. package/dist/cli/agent.js +11 -5
  18. package/dist/cli/bench.js +40 -23
  19. package/dist/cli/index.js +170 -44
  20. package/dist/cli/init-channel.js +5 -1
  21. package/dist/cli/model.js +324 -0
  22. package/dist/cli/net-check.js +133 -50
  23. package/dist/cli/watch-msg.js +7 -7
  24. package/dist/cli/watch-web/debug-log.js +18 -0
  25. package/dist/cli/watch-web/server.js +306 -0
  26. package/dist/cli/watch-web/sources/aid.js +63 -0
  27. package/dist/cli/watch-web/sources/msg.js +70 -0
  28. package/dist/cli/watch-web/sources/session.js +638 -0
  29. package/dist/cli/watch-web/sources/types.js +10 -0
  30. package/dist/cli/watch-web/static/app.js +546 -0
  31. package/dist/cli/watch-web/static/index.html +54 -0
  32. package/dist/cli/watch-web/static/style.css +247 -0
  33. package/dist/core/channel-loader.js +7 -4
  34. package/dist/core/command-handler.js +81 -86
  35. package/dist/core/evolagent-registry.js +1 -1
  36. package/dist/core/evolagent.js +4 -4
  37. package/dist/core/interaction-router.js +59 -0
  38. package/dist/core/message/message-bridge.js +6 -6
  39. package/dist/core/message/message-log.js +2 -2
  40. package/dist/core/message/message-processor.js +86 -101
  41. package/dist/core/message/stream-idle-monitor.js +21 -0
  42. package/dist/core/model/model-catalog.js +215 -0
  43. package/dist/core/model/model-scope.js +250 -0
  44. package/dist/core/relation/peer-identity.js +40 -49
  45. package/dist/core/relation/peer-key.js +16 -0
  46. package/dist/core/session/session-fs-store.js +34 -55
  47. package/dist/core/session/session-key.js +24 -0
  48. package/dist/core/session/session-manager.js +308 -251
  49. package/dist/core/session/session-mapper.js +9 -4
  50. package/dist/core/trigger/manager.js +3 -3
  51. package/dist/core/trigger/scheduler.js +2 -1
  52. package/dist/index.js +6 -2
  53. package/dist/ipc.js +22 -0
  54. package/kits/docs/GUIDE.md +2 -2
  55. package/kits/docs/INDEX.md +11 -7
  56. package/kits/docs/channels/aun.md +56 -17
  57. package/kits/docs/channels/feishu.md +41 -12
  58. package/kits/docs/context-assembly.md +181 -0
  59. package/kits/docs/evolclaw/agent.md +49 -0
  60. package/kits/docs/evolclaw/aid.md +49 -0
  61. package/kits/docs/evolclaw/ctl.md +46 -0
  62. package/kits/docs/evolclaw/group.md +82 -0
  63. package/kits/docs/evolclaw/msg.md +86 -0
  64. package/kits/docs/evolclaw/rpc.md +35 -0
  65. package/kits/docs/evolclaw/storage.md +49 -0
  66. package/kits/docs/venues/aun-group.md +10 -0
  67. package/kits/docs/venues/aun-private.md +10 -0
  68. package/kits/docs/venues/client-desktop.md +10 -0
  69. package/kits/docs/venues/client-mobile.md +10 -0
  70. package/kits/docs/venues/feishu-group.md +13 -0
  71. package/kits/docs/venues/feishu-private.md +9 -0
  72. package/kits/docs/venues/group.md +11 -0
  73. package/kits/docs/venues/private.md +10 -0
  74. package/kits/eck_manifest.json +72 -36
  75. package/kits/rules/01-overview.md +20 -10
  76. package/kits/rules/06-channel.md +30 -27
  77. package/kits/templates/system-fragments/session.md +10 -3
  78. package/kits/templates/system-fragments/venue.md +9 -0
  79. package/package.json +11 -6
  80. package/dist/aun/aid/lifecycle-log.js +0 -33
  81. package/dist/utils/aid-lifecycle-log.js +0 -33
  82. package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
  83. package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
  84. package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
  85. package/kits/docs/evolclaw/tools.md +0 -25
@@ -1,4 +1,5 @@
1
1
  import { formatTimestamp } from './session-fs-store.js';
2
+ import { formatSessionKey, DEFAULT_THREAD_ID } from './session-key.js';
2
3
  export function sessionToFile(session) {
3
4
  const metadata = {};
4
5
  if (session.metadata) {
@@ -15,19 +16,22 @@ export function sessionToFile(session) {
15
16
  if (session.metadata.resumeAt)
16
17
  metadata.resumeAt = session.metadata.resumeAt;
17
18
  for (const [k, v] of Object.entries(session.metadata)) {
18
- if (['isActive', 'channelName', 'permissionMode', 'peerId', 'peerName', 'groupId', 'replyContext', 'agentSessions', 'resumeAt'].includes(k))
19
+ if (['isActive', 'channelKey', 'channelName', 'permissionMode', 'peerId', 'peerName', 'groupId', 'replyContext', 'agentSessions', 'resumeAt'].includes(k))
19
20
  continue;
20
21
  if (v !== undefined)
21
22
  metadata[k] = v;
22
23
  }
23
24
  }
24
25
  const now = session.updatedAt || Date.now();
26
+ const channelType = session.channelType || session.channel;
27
+ const threadId = session.threadId || DEFAULT_THREAD_ID;
25
28
  return {
26
29
  id: session.id,
27
30
  channel: session.channel,
28
- channelType: session.channelType || session.channel,
31
+ channelType,
29
32
  channelId: session.channelId,
30
- selfId: session.selfId ?? null,
33
+ sessionKey: formatSessionKey(channelType, session.channelId, threadId),
34
+ selfAID: session.selfAID,
31
35
  agentType: session.agentId || 'claude',
32
36
  threadId: session.threadId || '',
33
37
  chatType: session.chatType || 'private',
@@ -71,7 +75,8 @@ export function fileToSession(file) {
71
75
  channel: file.channel,
72
76
  channelType: file.channelType,
73
77
  channelId: file.channelId,
74
- selfId: file.selfId ?? undefined,
78
+ sessionKey: file.sessionKey || formatSessionKey(file.channelType, file.channelId, file.threadId || DEFAULT_THREAD_ID),
79
+ selfAID: file.selfAID,
75
80
  agentId: file.agentType,
76
81
  threadId: file.threadId,
77
82
  chatType: file.chatType,
@@ -23,7 +23,7 @@ export class TriggerManager {
23
23
  const raw = fs.readFileSync(this.triggersPath, 'utf8');
24
24
  const data = JSON.parse(raw);
25
25
  this.triggers = new Map(Object.entries(data.triggers ?? {}));
26
- // Migrate old channel key format: "selfPeerId#type#name" → "type#selfPeerId#name"
26
+ // Migrate old channel key format: "selfAID#type#name" → "type#selfAID#name"
27
27
  let migrated = false;
28
28
  for (const trigger of this.triggers.values()) {
29
29
  const fixed = this.migrateChannelKey(trigger.targetChannel);
@@ -52,7 +52,7 @@ export class TriggerManager {
52
52
  }
53
53
  }
54
54
  /**
55
- * Detect and fix old format "selfPeerId#type#name" → current "type#selfPeerId#name".
55
+ * Detect and fix old format "selfAID#type#name" → current "type#selfAID#name".
56
56
  * Old format: first segment contains '.' (AID like "evolai.agentid.pub").
57
57
  */
58
58
  migrateChannelKey(key) {
@@ -63,7 +63,7 @@ export class TriggerManager {
63
63
  return key;
64
64
  const [first, second, third] = parts;
65
65
  if (first.includes('.') && !second.includes('.')) {
66
- return formatChannelKey({ type: second, selfPeerId: first, name: third });
66
+ return formatChannelKey({ type: second, selfAID: first, name: third });
67
67
  }
68
68
  return key;
69
69
  }
@@ -211,8 +211,9 @@ export class TriggerScheduler {
211
211
  buildSyntheticMessage(trigger, messageId) {
212
212
  return {
213
213
  channel: trigger.targetChannel,
214
+ channelType: trigger.targetChannelType,
214
215
  channelId: trigger.targetChannelId,
215
- selfId: this.aid,
216
+ selfAID: this.aid,
216
217
  threadId: trigger.targetThreadId ?? '',
217
218
  agentId: trigger.agentId,
218
219
  chatType: 'private',
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ import { StatsCollector } from './utils/stats.js';
26
26
  import { AidStatsCollector } from './utils/stats.js';
27
27
  import { PermissionGateway } from './core/permission.js';
28
28
  import { InteractionRouter } from './core/interaction-router.js';
29
- import { ChannelLoader } from './core/channel-loader.js';
29
+ import { ChannelLoader, tryParseChannelKey } from './core/channel-loader.js';
30
30
  import { AgentLoader } from './core/baseagent-loader.js';
31
31
  import { EvolAgentRegistry } from './core/evolagent-registry.js';
32
32
  import { buildReloadHooks } from './core/channel-loader.js';
@@ -553,7 +553,8 @@ async function main() {
553
553
  const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelKey);
554
554
  const effectiveDefault = owningAgent?.projectPath
555
555
  ?? primaryAgent.projectPath;
556
- const session = await sessionManager.getOrCreateSession(inst.adapter.channelKey, channelId, effectiveDefault, undefined, undefined, undefined, undefined);
556
+ const parsedKey = tryParseChannelKey(inst.adapter.channelKey);
557
+ const session = await sessionManager.getOrCreateSession(inst.adapter.channelKey, channelId, effectiveDefault, undefined, undefined, undefined, undefined, undefined, undefined, parsedKey?.selfAID, parsedKey?.type);
557
558
  return path.isAbsolute(session.projectPath)
558
559
  ? session.projectPath
559
560
  : path.resolve(process.cwd(), session.projectPath);
@@ -866,6 +867,9 @@ async function main() {
866
867
  queued: messageQueue.getQueueLengthByAgent(agentName),
867
868
  }));
868
869
  ipcServer.setAunAidStatsProvider(() => aidStatsCollector.getAllSnapshots());
870
+ ipcServer.setAunAidStatsRecorder((params) => {
871
+ aidStatsCollector.recordOutbound(params.aid, params.toPeer, Buffer.byteLength(params.text || '', 'utf-8'), params.text, false, params.encrypt, params.chatmode);
872
+ });
869
873
  // ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
870
874
  const reloadHooks = buildReloadHooks({
871
875
  channelLoader,
package/dist/ipc.js CHANGED
@@ -11,6 +11,7 @@ export class IpcServer {
11
11
  agentRegistry;
12
12
  aunAidProvider;
13
13
  aunAidStatsProvider;
14
+ aunAidStatsRecorder;
14
15
  constructor(socketPath, getStatus, commandExecutor) {
15
16
  this.socketPath = socketPath;
16
17
  this.getStatus = getStatus;
@@ -28,6 +29,10 @@ export class IpcServer {
28
29
  setAunAidStatsProvider(provider) {
29
30
  this.aunAidStatsProvider = provider;
30
31
  }
32
+ /** Inject AUN AID stats recorder for aun-aid-stats-record-outbound IPC handler */
33
+ setAunAidStatsRecorder(recorder) {
34
+ this.aunAidStatsRecorder = recorder;
35
+ }
31
36
  start() {
32
37
  // Remove stale socket file (Unix only — named pipes auto-cleanup on process exit)
33
38
  if (!isNamedPipe(this.socketPath)) {
@@ -97,6 +102,23 @@ export class IpcServer {
97
102
  const stats = this.aunAidStatsProvider ? this.aunAidStatsProvider() : [];
98
103
  return { ok: true, stats };
99
104
  }
105
+ case 'aun-aid-stats-record-outbound': {
106
+ if (!this.aunAidStatsRecorder)
107
+ return { ok: false, error: 'recorder not configured' };
108
+ try {
109
+ this.aunAidStatsRecorder({
110
+ aid: cmd.aid,
111
+ toPeer: cmd.toPeer,
112
+ text: cmd.text ?? '',
113
+ encrypt: cmd.encrypt,
114
+ chatmode: cmd.chatmode,
115
+ });
116
+ return { ok: true };
117
+ }
118
+ catch (e) {
119
+ return { ok: false, error: String(e?.message || e) };
120
+ }
121
+ }
100
122
  case 'ctl': {
101
123
  if (!this.commandExecutor)
102
124
  return { ok: false, error: 'ctl not configured' };
@@ -2,14 +2,14 @@
2
2
 
3
3
  ## 查阅流程
4
4
 
5
- 1. 先看 `$KITS_RULES`(自动加载的 8 个规则文件)了解机制骨架
5
+ 1. 先看 `$KITS_RULES`(自动加载的 6 个规则文件 01-06)了解机制骨架
6
6
  2. 需要详细信息时,按 `INDEX.md` 找到对应文档路径
7
7
  3. Read 对应文档
8
8
 
9
9
  ## 路径解析
10
10
 
11
11
  文档中用 `$名称` 引用路径。解析步骤:
12
- 1. 查 `$KITS_RULES/01-entry.md` 的路径体系速查表
12
+ 1. 查 `$KITS_RULES/02-navigation.md` 的路径体系速查表
13
13
  2. 如需完整定义,Read `$KITS_DOCS/path-registry.md`
14
14
  3. 如需运行时实际值,Read `$ECK/path-registry.md`
15
15
 
@@ -8,6 +8,7 @@
8
8
  |------|------|------|
9
9
  | 查阅指南 | `GUIDE.md` | 文档查阅流程 |
10
10
  | 路径定义 | `path-registry.md` | 所有预定义路径及派生规则 |
11
+ | 上下文组装机制 | `context-assembly.md` | manifest 装配机制:when 条件/合并覆盖/模板渲染/运行时变量/调试输出 |
11
12
 
12
13
  ## AUN 协议
13
14
 
@@ -20,11 +21,14 @@
20
21
 
21
22
  | 文档 | 路径 | 说明 |
22
23
  |------|------|------|
23
- | Agent 命令 | `evolclaw/AGENT_CMD.md` | agent 全生命周期管理命令 |
24
- | 运行时工具 | `evolclaw/tools.md` | ctl 命令集 |
24
+ | 私聊消息 | `evolclaw/msg.md` | `ec msg` 私聊收发消息 |
25
+ | 群聊消息 | `evolclaw/group.md` | `ec group` 群聊收发与群管理 |
26
+ | Agent 生命周期 | `evolclaw/agent.md` | `ec agent` 创建/启停/配置/热重载 |
27
+ | AID 身份 | `evolclaw/aid.md` | `ec aid` 身份/证书/名片/探测对端 |
28
+ | 文件存储 | `evolclaw/storage.md` | `ec storage` 上传/下载/配额 |
29
+ | 运行时自管理 | `evolclaw/ctl.md` | `ec ctl` 切模型/推理强度/压缩/重启 |
30
+ | 底层 RPC | `evolclaw/rpc.md` | `ec rpc` 直接调 AUN 协议方法 |
25
31
  | 自我总结 | `evolclaw/self-summary.md` | 自我总结流程指南 |
26
- | 私聊消息 | `evolclaw/MSG_PRIVATE.md` | 私聊消息命令 |
27
- | 群聊消息 | `evolclaw/MSG_GROUP.md` | 群聊消息命令 |
28
32
 
29
33
  ## 身份
30
34
 
@@ -35,12 +39,12 @@
35
39
  | AID 档案规范 | `identity/AID_PROFILE_SPEC.md` | AID 档案格式规范 |
36
40
  | 路径运维 | `identity/PATH_OPS.md` | 路径运维操作 |
37
41
 
38
- ## 渠道
42
+ ## 渠道(知识文档·按需加载,不依赖当前渠道)
39
43
 
40
44
  | 文档 | 路径 | 说明 |
41
45
  |------|------|------|
42
- | AUN 渠道 | `channels/aun.md` | AUN 通信约定 |
43
- | 飞书渠道 | `channels/feishu.md` | 飞书渠道接入 |
46
+ | AUN 渠道 | `channels/aun.md` | AUN 渠道配置、参数与特有机制(网关发现/E2EE/群 ID/证书链) |
47
+ | 飞书渠道 | `channels/feishu.md` | 飞书渠道配置、参数与特有机制(appId/合并转发/卡片/user_id) |
44
48
 
45
49
  ## ECK 模板
46
50
 
@@ -1,25 +1,64 @@
1
- # AUN 通信约定
1
+ # AUN 渠道
2
2
 
3
- ## 消息收发
3
+ > 知识性文档:AUN 渠道的配置、参数与特有机制。**不依赖当前会话渠道**——任意会话都可按需 Read。
4
+ > 运行时"怎么发消息"由注入的 `[channel]` 段决定(aun 渠道用 `ec msg send` / `ec group send`),不在本文。
5
+ > 底层协议方法与 RPC 逃生通道见 `evolclaw/rpc.md`。
4
6
 
5
- - 私聊:`message.send` / `message.receive`
6
- - 群聊:`group.message.send` / `group.message.receive`
7
- - 消息格式:纯文本 / Markdown / 文件引用
7
+ ## 概述
8
8
 
9
- ## 身份识别
9
+ AUN 渠道把 agent 接入 AUN(Agent Union Network),对端以 **AID** 为身份标识(既是身份也是地址)。evolclaw 通过 AUN SDK(`@agentunion/fastaun`)维护到 Gateway 的 WebSocket 长连接,支持私聊、群聊、文件、E2EE 加密。
10
10
 
11
- - 每条入站消息携带发送者 AID
12
- - 通过 `https://<aid>/agent.md` 获取对端名片
13
- - 名片包含:名称、能力声明、联系方式
11
+ ## 配置
14
12
 
15
- ## 连接管理
13
+ AUN 渠道是**隐式**的——它不写在 `channels[]` 数组里,而是从 agent 自身的 AID 自动创建。一个 agent 就是一个 AID,启动时 evolclaw 用顶层 `aid` 字段隐式构造唯一的 aun 实例(`channel-loader.ts`)。所以真实的 agent config.json 里 `channels` 数组通常是空的(飞书等非 aun 渠道才往里加)。
16
14
 
17
- - evolclaw 自动维护 WebSocket 长连接
18
- - 断线自动重连(SDK 内置退避策略)
19
- - 连接状态可通过 `evolclaw ctl aid` 查看
15
+ ### agent 配置(`agents/<aid>/config.json`)
20
16
 
21
- ## 群聊
17
+ ```json
18
+ {
19
+ "$schema_version": 1,
20
+ "aid": "myagent.agentid.pub",
21
+ "enabled": true,
22
+ "owners": ["owner.agentid.pub"],
23
+ "channels": [],
24
+ "active_baseagent": "claude"
25
+ }
26
+ ```
22
27
 
23
- - ID 格式:`<issuer>/<group-name>`
24
- - 被 @ 时才默认响应(可通过 venue policy 配置)
25
- - 群消息按窗口批量推送(batch_window_seconds)
28
+ | 字段 | 说明 |
29
+ |------|------|
30
+ | `aid` | agent 的 AID,**即 aun 渠道身份**(隐式创建,无需在 channels[] 声明) |
31
+ | `enabled` | agent 是否启用 |
32
+ | `owners` | owner 的 **AID** 列表(最高权限);首个 owner 用于首次连接发欢迎消息 |
33
+ | `admins` | admin 的 AID 列表 |
34
+ | `channels` | 非 aun 渠道(飞书等)的实例数组;aun 不在此 |
35
+
36
+ > `owners`/`admins` 用 **AID**,不是渠道原生 ID。
37
+
38
+ ### 进程级配置(`$EVOLCLAW_HOME/config.json`)
39
+
40
+ E2EE 加密种子是**进程级**的,所有 agent 共享,不在 agent config 里:
41
+
42
+ ```json
43
+ {
44
+ "aun": {
45
+ "encryptionSeed": "<seed>"
46
+ }
47
+ }
48
+ ```
49
+
50
+ 留空时 SDK 依次回退 `AUN_ENCRYPTION_SEED` 环境变量 → 默认 `'evol'`。
51
+
52
+ > Gateway 不在配置里手填:连接时从 AID 自动发现(见下)。Keystore 默认落在 `$EVOLCLAW_HOME`。
53
+
54
+ ## 特有机制
55
+
56
+ - **隐式创建**:aun 渠道由顶层 `aid` 派生,无独立配置实例;这是它与飞书等渠道最大的不同。
57
+ - **网关发现**:连接前查询 `https://<aid>/.well-known/aun-gateway` 获取网关地址(AID 本身即域名);per-agent 流程不提供手填 gateway 的入口,发现失败即连接失败。
58
+ - **身份与信任**:每条入站消息携带发送者 AID,经四级 X.509 证书链(Root CA → Registry CA → Issuer CA → Agent)验签。
59
+ - **agent.md 缓存**:对端名片 `https://<aid>/agent.md` 由 **AUN SDK 自动拉取并缓存**(`AgentMdManager`,带 ETag/TTL,默认 1 天内不重复探测)。缓存落在 **`$EVOLCLAW_HOME/AIDs/<aid>/`**,每个 AID 两个文件:`agent.md`(带签名正文)+ `agentmd.json`(元数据:etag/last_modified/checked_at/verify_status)。本端与对端的 agent.md 都缓存于此。
60
+ - 关系层另存一份**派生**的精简身份 `relations/<peerKey>/peer-identity.json`(type/isAgent/name),由 evolclaw 从 agent.md 提取,不是 agent.md 本体。
61
+ - **E2EE 加密**:可选端到端加密,回复默认跟随对端消息加密状态(密文回密文,明文回明文);种子由进程级 `aun.encryptionSeed` 提供。
62
+ - **断线重连**:SDK 内置退避策略自动重连。实时连接状态用 `ec watch aid` 查看(`ec aid` 是身份/证书管理,不显示连接状态)。
63
+ - **群 ID 格式**:`group.{issuer}/{group_name}`(新格式)或 `{group_no}.{issuer}`(数字群号,如 `11117.agentid.pub`)。
64
+ - **身份体系**:对端以 AID 标识,关系层 peerKey 形如 `aun#alice.aid.pub`。
@@ -1,27 +1,56 @@
1
1
  # 飞书渠道
2
2
 
3
- <!-- TODO: 填充飞书渠道接入文档 -->
3
+ > 知识性文档:飞书渠道的配置、参数与特有机制。**不依赖当前会话渠道**——任意会话都可按需 Read(在 aun 会话里也可查飞书)。
4
+ > 运行时"怎么发消息"由注入的 `[channel]` 段决定(飞书是 evolclaw 自动回复,agent 不调 CLI),不在本文。
4
5
 
5
6
  ## 概述
6
7
 
7
- 飞书渠道通过 evolclaw 的 feishu channel 实现,支持:
8
- - 单聊消息收发
9
- - 群聊消息收发
10
- - 合并转发消息解析
11
- - 文件/图片/视频消息
8
+ 飞书渠道通过 evolclaw 的 feishu channel 插件接入,对端以飞书 `user_id`(如 `ou_xxx`)为身份标识。支持单聊/群聊收发、合并转发解析、富文本卡片、文件/图片/视频消息。
12
9
 
13
10
  ## 配置
14
11
 
15
- evolclaw 配置中启用飞书渠道:
12
+ 飞书渠道按 **per-agent** 配置,写在 `agents/<aid>/config.json` 的 `channels` 数组里。每个元素是一个独立实例,靠 `name` 区分;同一 agent 可配多个飞书实例。
16
13
 
17
14
  ```json
18
15
  {
19
- "channels": {
20
- "feishu": {
16
+ "aid": "myagent.aid.pub",
17
+ "channels": [
18
+ {
19
+ "type": "feishu",
20
+ "name": "main",
21
21
  "enabled": true,
22
- "appId": "<app-id>",
23
- "appSecret": "<app-secret>"
22
+ "appId": "cli_xxx",
23
+ "appSecret": "<app-secret>",
24
+ "owners": ["ou_owner_user_id"],
25
+ "admins": ["ou_admin_user_id"]
24
26
  }
25
- }
27
+ ]
26
28
  }
27
29
  ```
30
+
31
+ ### 实例字段
32
+
33
+ | 字段 | 必填 | 说明 |
34
+ |------|------|------|
35
+ | `type` | 是 | 固定 `"feishu"` |
36
+ | `name` | 是(数组形式) | 该 agent 内飞书实例的本地标识,不含 `#` |
37
+ | `enabled` | 否 | 默认启用;`false` 关闭该实例 |
38
+ | `appId` | 是 | 飞书自建应用 App ID |
39
+ | `appSecret` | 是 | 飞书自建应用 App Secret |
40
+ | `owners` | 否 | owner 的飞书 `user_id` 列表(最高权限) |
41
+ | `admins` | 否 | admin 的飞书 `user_id` 列表 |
42
+ | `flushDelay` | 否 | flush 间隔(秒),覆盖全局值 |
43
+ | `debounce` | 否 | 入站消息去抖间隔(秒),覆盖全局值 |
44
+ | `showActivities` | 否 | 思考过程展示范围:`all` / `dm-only` / `owner-dm-only` / `none` |
45
+
46
+ > `owners`/`admins` 用飞书原生 `user_id`,**不是 AID**。
47
+
48
+ agent 级(config.json 顶层)相关开关:
49
+ - `enable_rich_content`:启用富文本卡片渲染,默认关闭。
50
+
51
+ ## 特有机制
52
+
53
+ - **合并转发解析**:飞书的 `merge_forward` 消息会被自动展开为文本注入上下文(含"以下是引用的原消息"包裹)。
54
+ - **交互卡片**:富内容开启时,部分回复以飞书卡片形式发送;卡片按钮**仅发起者可操作**,他人点击返回提示。
55
+ - **身份体系**:对端以 `user_id` 标识,关系层 peerKey 形如 `feishu#ou_xxx`。
56
+ - **自动回复**:飞书是非 aun 渠道,回复由 evolclaw 自动完成,agent 无需调用 `ec msg send`。
@@ -0,0 +1,181 @@
1
+ # 上下文组装机制(manifest)
2
+
3
+ ECK 把"每条消息该给 base agent 看哪些上下文"这件事,交给一份 **manifest** 声明式描述,由 `renderKitSections`(`src/agents/kit-renderer.ts`)在每次处理消息时执行。本文是这套机制的完整参考。
4
+
5
+ > 本文是按需载入文档。日常对话不需要读它——只有在排查"为什么某段上下文没出现/出现了"、要改 manifest、或要看实际渲染结果时才 Read。
6
+
7
+ ## 三个阶段
8
+
9
+ evolclaw 收到消息 → 构造一份 **vars**(运行时变量,见下)→ `renderKitSections`:
10
+
11
+ 1. **选段**:按 manifest 顺序遍历每个 section,用它的 `when` 条件 + 当前 vars 判断是否加载。
12
+ 2. **渲染**:解析 section 路径里的 `$NAME` / `{{key}}` 占位符定位文件;`needsInjection:true` 的文件再跑一遍模板渲染(条件块 + 变量替换)。
13
+ 3. **拼装**:所有命中的文件内容包进一个 `<system-reminder>` 块,作为 system prompt 的一部分注入。
14
+
15
+ 整个过程对每次消息执行一次,结果同时落盘到调试目录(见"调试机制")。
16
+
17
+ ## manifest 文件位置
18
+
19
+ | 文件 | 角色 | 性质 |
20
+ |------|------|------|
21
+ | `$KITS/eck_manifest.json` | 基础 manifest(开发时维护) | 只读 |
22
+ | `$ECK/eck_manifest.json` | 用户覆盖(可选,默认不存在) | 可写 |
23
+
24
+ 加载时合并两者(见"合并与覆盖")。manifest 只加载一次并缓存。
25
+
26
+ ## section 字段
27
+
28
+ manifest 是 `{ "$schema_version": 1, "sections": [...] }`。每个 section:
29
+
30
+ | 字段 | 必填 | 含义 |
31
+ |------|------|------|
32
+ | `id` | ✓ | 唯一标识,覆盖合并的键 |
33
+ | `type` | ✓ | `file`(单文件)或 `directory`(整目录) |
34
+ | `file` | type=file | 文件路径,可含 `$NAME` / `{{key}}` |
35
+ | `path` | type=directory | 目录路径 |
36
+ | `pattern` | | 目录匹配 glob,默认 `*.md`(支持 `*` 和 `{a,b}`) |
37
+ | `order` | ✓ | 排序,**升序**决定上下文中出现的先后 |
38
+ | `needsInjection` | ✓ | `true`=按模板渲染(解析 `{{}}` 条件与变量);`false`=原样读入 |
39
+ | `when` | ✓ | 加载条件,`"always"` 或条件对象(见下) |
40
+ | `enabled` | | `false` 时整段跳过 |
41
+ | `description` | | 调试输出里的人类可读标签 |
42
+
43
+ ## when 条件求值
44
+
45
+ `when` 是 `"always"` 或一个对象。求值规则(`evaluateWhen`):
46
+
47
+ | 写法 | 含义 |
48
+ |------|------|
49
+ | `"always"` | 恒为真 |
50
+ | `{ "var": "X", "eq": V }` | `vars.X === V`;`eq:null` 匹配"未注入"(null/undefined) |
51
+ | `{ "var": "X", "neq": V }` | `vars.X !== V`;`neq:null` 匹配"已注入" |
52
+ | `{ "var": "X", "in": [...] }` | `vars.X` 在数组内 |
53
+ | `{ "var": "X", "nin": [...] }` | `vars.X` 不在数组内 |
54
+ | `{ "any": ["A","B"] }` | A、B 任一为真值 |
55
+ | `{ "all": ["A","B"] }` | A、B 全为真值 |
56
+
57
+ "真值"判定:非 `undefined`/`null`/`false`/`""`/`0`。
58
+
59
+ 典型用法:`{ "var": "chatType", "neq": null }` = "非 coding 场景才加载";`{ "var": "groupId", "neq": null }` = "群聊才加载"。
60
+
61
+ ## 合并与覆盖
62
+
63
+ 无 `$ECK/eck_manifest.json` 时,直接用基础 manifest。存在时:
64
+
65
+ - 覆盖文件含 `"mode": "replace"` → **完全替换**,只用覆盖文件的 sections。
66
+ - 否则 **patch 合并**:以 `id` 为键,覆盖文件里同 id 的字段浅合并进基础 section(`{...base, ...override}`),新 id 追加。
67
+
68
+ 合并后统一按 `order` 升序排序。改某段行为(如关掉某层、调顺序、换条件)优先用覆盖文件,不动基础 manifest。
69
+
70
+ ## 路径与模板渲染
71
+
72
+ ### 路径占位符(所有 section 的 file/path)
73
+
74
+ - `$NAME`(大写)→ 从 vars 取真值,如 `$KITS_DOCS` → 包内文档目录。
75
+ - `{{key}}` → 从 vars 取真值,如 `{{chatType}}` → `private`,`{{peerKey}}` → `aun#alice.aid.pub`。
76
+
77
+ 任一占位符解析为空 → 该 section 视为"未解析",跳过(调试输出标 `unresolved-vars`)。文件不存在 → 标 `not-exist`,也跳过。这是**正常机制**:很多 section 靠"路径解析不出来"自然落选(如 coding 场景没有 `$PERSONAL_DIR`)。
78
+
79
+ ### 模板渲染(仅 needsInjection:true 的文件)
80
+
81
+ 读入跑三遍:
82
+
83
+ 1. **条件块**(内层优先,可嵌套):
84
+ - `{{?key}}...{{/}}` — key 为真值才保留
85
+ - `{{?key=value}}...{{/}}` — `String(vars.key)===value` 才保留
86
+ - `{{?key!=value}}...{{/}}` — 不等才保留
87
+ 2. **变量替换**:`{{key}}` 替换为值,非真值替换为空串。
88
+ 3. **清理**:删掉空行。
89
+
90
+ `needsInjection:false` 的文件原样读入,不做任何替换(如 rules 目录、persona.md、各 venue 文档)。
91
+
92
+ ## 运行时变量(vars)目录
93
+
94
+ vars 由 evolclaw 在 `message-processor.ts` 按当前会话构造。分两类:
95
+
96
+ **路径类**(供占位符展开成真实路径):
97
+ `EVOLCLAW_HOME`、`PACKAGE_ROOT`、`CURRENT_PROJECT`、`KITS`、`KITS_RULES`、`KITS_DOCS`、`KITS_TEMPLATES`、`KITS_FRAGMENTS`、`PERSONAL_DIR`、`RELATIONS_DIR`、`VENUES_DIR`。
98
+
99
+ **内容/场景类**(供 when 判断与模板替换):
100
+
101
+ | 变量 | 含义 |
102
+ |------|------|
103
+ | `selfAid` / `selfName` | 当前 agent 身份 |
104
+ | `hasPersona` / `hasWorkingMemory` | 是否有人格 / 当前关注 |
105
+ | `peerId` / `peerKey` / `peerName` | 对端原生 ID / 跨渠道键 / 显示名 |
106
+ | `peerRole` / `peerType` | 对端角色(owner/admin/guest/...)/ 类型(human/agent) |
107
+ | `groupId` | 群 ID(群聊时) |
108
+ | `chatType` | `private` / `group` / `null`(coding) |
109
+ | `channel` | 渠道类型(aun/feishu/...)|
110
+ | `dispatch` | 群分发模式(mention/broadcast) |
111
+ | `clientType` | 客户端类型(desktop/web/mobile) |
112
+ | `permissionMode` / `readonly` | 权限模式 / 是否只读 |
113
+ | `capabilities` | 当前渠道能力(图片输入/输出、文件发送) |
114
+ | `project` / `CURRENT_PROJECT` | 项目目录名 / 完整路径 |
115
+ | `sessionId` / `sessionName` / `sessionKey` / `sessionCreatedAt` / `threadId` | 会话标识与元信息 |
116
+ | `chatMode` | `interactive` / `proactive` |
117
+ | `baseAgent` / `baseAgentName` / `baseAgentModel` / `agentSessionId` | base agent 信息 |
118
+
119
+ coding 场景(无 channel/无身份)下,`chatType`、`channel`、`selfAid`、`peer*` 等均为空——这正是身份/关系/环境/渠道层落选的原因。
120
+
121
+ ## 默认 manifest 的段(按 order)
122
+
123
+ | order | id | 类型 | 加载条件(when) | inject |
124
+ |-------|----|----|------------------|--------|
125
+ | 10 | rules | 目录 `$KITS_RULES` | always | ✗ |
126
+ | 20 | identity-layer | fragment | chatType≠null | ✓ |
127
+ | 21 | persona | `$PERSONAL_DIR/persona.md` | chatType≠null | ✗ |
128
+ | 22 | working-memory | `$PERSONAL_DIR/memory/working.md` | chatType≠null | ✗ |
129
+ | 30 | relation-layer | fragment | chatType∈{private,group} | ✓ |
130
+ | 35 | peer-profile | `$RELATIONS_DIR/{{peerKey}}/profile.md` | peerKey≠null | ✗ |
131
+ | 40 | venue-fragment | fragment | chatType≠null | ✓ |
132
+ | 41 | venue-chattype | `$KITS_DOCS/venues/{{chatType}}.md` | chatType≠null | ✗ |
133
+ | 42 | venue-channel-chattype | `$KITS_DOCS/venues/{{channel}}-{{chatType}}.md` | chatType≠null | ✗ |
134
+ | 43 | venue-group-profile | `$VENUES_DIR/{{channel}}#{{groupId}}/profile.md` | groupId≠null | ✗ |
135
+ | 44 | venue-client | `$KITS_DOCS/venues/client-{{clientType}}.md` | clientType≠null | ✗ |
136
+ | 50 | channel-layer | fragment | channel≠null | ✓ |
137
+ | 60 | session | fragment | always | ✓ |
138
+ | 70 | baseagent | fragment | baseAgent≠null | ✓ |
139
+
140
+ > 注意:`session`(60) 是 `always`,`baseagent`(70) 只看 `baseAgent` 是否注入——这两段**与 chatType 无关**,coding 场景也会加载。所谓"coding 仅 rules"是近似说法:精确地说 coding 场景命中的是 rules + session + baseagent(其余因 chatType/channel 为空而落选)。
141
+
142
+ ## 输出结构
143
+
144
+ 所有命中 section 的文件内容拼成一个块,注入 system prompt:
145
+
146
+ ```
147
+ <system-reminder>
148
+ EvolClaw Context Kit documents are shown below.
149
+
150
+ Contenu de <展示路径> (<id — description>):
151
+
152
+ <文件内容>
153
+
154
+ ...(每个文件一段,按 order)
155
+
156
+ IMPORTANT: Use this context when it affects the current interaction.
157
+ </system-reminder>
158
+ ```
159
+
160
+ 展示路径会把绝对路径回缩成 `$KITS_RULES`/`$AGENT_DIR` 等别名。`needsInjection:true` 的段额外单独汇总到 `fragments-*.md` 调试文件(见下)。
161
+
162
+ ## 调试机制(看实际渲染结果)
163
+
164
+ 每次渲染都把结果落盘到 **`$EVOLCLAW_HOME/data/eck-debug/`**,文件名带时间戳(`<字段>-YYYY-MM-DD-HH-MM-SS`):
165
+
166
+ | 文件 | 内容 |
167
+ |------|------|
168
+ | `vars-<ts>.json` | 本次注入的全部 vars 真值 + 每个变量的中文说明 |
169
+ | `context-<ts>.md` | 最终注入 base agent 的完整 `<system-reminder>` 块(即上面"输出结构"的实际产物) |
170
+ | `fragments-<ts>.md` | 仅经过模板渲染(needsInjection)的段,单独汇总,便于核对 `{{}}` 替换是否正确 |
171
+ | `manifest-<ts>.md` | **诊断报告**:每个 section 的 when 是否通过、路径解析状态(ok/unresolved-vars/not-exist/skipped)、是否被用上、是否注入;含未解析占位符清单 |
172
+
173
+ 排查"某段为什么没出现":看 `manifest-<ts>.md` 里该 section 的 `status` 和 `unresolved tokens`。看"实际给 agent 的全文":读 `context-<ts>.md`。
174
+
175
+ 调试文件保留 24 小时,`cleanEckDebug` 自动清理过期的。
176
+
177
+ ## 缓存与热加载
178
+
179
+ - **manifest 缓存**:`_manifestCache`,进程内只加载一次。改了 manifest 需触发 `invalidateKitCache()`(或重启)才生效。
180
+ - **文件内容缓存**:`_sessionPathCache` 按 `sessionId` 缓存已读文件内容;`invalidateSessionCache(sessionId)` 清单个会话。
181
+ - rules / fragments / docs 这些 `kits/` 下的文件,改完一般随会话重建生效(缓存按会话隔离);manifest 本身的结构改动需要 invalidate。
@@ -0,0 +1,49 @@
1
+ # ec agent — EvolAgent 生命周期管理
2
+
3
+ 管理本机托管的 EvolAgent(创建、启停、配置、热重载)。触发词:创建/新建/列出/查看/启用/禁用/删除/热重载/改配置。
4
+
5
+ ## 子命令
6
+
7
+ ```bash
8
+ # 列出所有 agent
9
+ ec agent list
10
+
11
+ # 查看 agent 详情(身份 + 配置 + 连接 + 会话 + 路径)
12
+ ec agent show <aid>
13
+
14
+ # 创建 agent(交互式)
15
+ ec agent new [aid]
16
+
17
+ # 创建 agent(非交互式,自动化场景)
18
+ ec agent new <aid> --non-interactive --project <绝对路径> \
19
+ [--baseagent claude|codex|gemini] [--owner <aid>] \
20
+ [--name "<显示名>"] [--description "<text>"] [--force]
21
+
22
+ # 启用 / 停用
23
+ ec agent enable <aid>
24
+ ec agent disable <aid>
25
+
26
+ # 读 / 改单个配置字段(支持点路径,如 active_baseagent)
27
+ ec agent get <aid> <key>
28
+ ec agent set <aid> <key> <val>
29
+
30
+ # 改 agent 名称(更新 agent.md 并重新上传)
31
+ ec agent rename <aid> "<name>"
32
+
33
+ # 热重载配置(无参数 = 全量 resync:扫磁盘,新增上线/删除下线/修改热更新)
34
+ ec agent reload [aid]
35
+
36
+ # 删除 agent(--purge 连数据一并清除)
37
+ ec agent delete <aid> [--purge]
38
+ ```
39
+
40
+ ## 非交互创建必填项
41
+
42
+ `ec agent new <aid> --non-interactive` 时:
43
+ - 必填:`--project <absolute path>`
44
+ - 可选:`--baseagent`(默认 PATH 中第一个可用项)、`--owner`、`--name`、`--description`、`--force`(覆盖已有 config.json)
45
+
46
+ ## 通用约定
47
+
48
+ - `--format json` — 输出 JSON
49
+ - `--help` / `-h` — 各子命令均支持,查看详细用法
@@ -0,0 +1,49 @@
1
+ # ec aid — AID 身份管理
2
+
3
+ 管理本地 AID 身份(证书、私钥、agent.md 名片),以及远程探测对端 AID。触发词:身份/证书/名片/探测对端/创建身份。
4
+
5
+ ## 子命令
6
+
7
+ ```bash
8
+ # 列出本地所有 AID(实测 sign+verify)
9
+ ec aid list [筛选选项] [--no-verify]
10
+
11
+ # 查看本地 AID 详情(证书有效期、私钥状态、签名能力)
12
+ ec aid show <aid>
13
+
14
+ # 创建新 AID 身份
15
+ ec aid new <aid>
16
+
17
+ # 远程探测 AID(是否存在 + 网关 + agent.md)
18
+ ec aid lookup <aid>
19
+
20
+ # agent.md 名片:签名上传 / 下载验签
21
+ ec aid agentmd put <aid>
22
+ ec aid agentmd get <aid>
23
+
24
+ # 删除指定本地 AID(无网络注销)
25
+ ec aid delete <aid>
26
+ ```
27
+
28
+ ## list 筛选选项
29
+
30
+ 不指定 = 列出 mine + broken + peer-cert(隐藏 no-cert)。可组合:
31
+ - `--mine` — 仅本地可用身份(实测可签名+验签通过)
32
+ - `--broken` — 仅有私钥但不可用(公钥不匹配 / 证书过期 / sign 失败)
33
+ - `--peer-cert` — 仅对端 AID(无私钥,有公钥证书)
34
+ - `--no-cert` — 仅无私钥无证书的孤儿目录
35
+ - `--no-verify` — 跳过 sign+verify 实测,仅静态扫描(更快)
36
+
37
+ 输出图标:`🔑` 有私钥 · `✅` 实测可签名/验签 · `❌` 不可签名 · `⌛` 证书过期 · `📜` 有公钥证书 · `📄` 有 agent.md
38
+
39
+ ## delete 批量清理
40
+
41
+ 批量删除**默认 dry-run,加 `--yes` 才真正执行**:
42
+ - `ec aid delete --orphan` — 清理无私钥的外部 AID 缓存
43
+ - `ec aid delete --no-cert` — 清理无私钥也无公钥证书的孤儿目录
44
+ - `ec aid delete --unrecoverable` — 清理云端公钥已变更、本地不可恢复的 AID
45
+
46
+ ## 通用约定
47
+
48
+ - `--format json` — 输出 JSON
49
+ - `--help` / `-h` — 各子命令均支持