evolcore 0.0.3 → 0.0.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 +83 -0
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +1 -31
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +157 -5
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/channels/aun.js +220 -112
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +52 -137
- package/dist/channels/feishu.js +56 -4
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +383 -7
- package/dist/cli/aun-commands.js +11 -11
- package/dist/cli/daemon-commands.js +46 -15
- package/dist/cli/handoff-command.js +2 -1
- package/dist/cli/init-channel.js +185 -67
- package/dist/cli/init.js +49 -27
- package/dist/cli/trigger-command.js +92 -11
- package/dist/config/access-policy-domain.js +18 -0
- package/dist/config/access-policy.js +110 -0
- package/dist/config/builtin-role-templates.js +27 -14
- package/dist/config/builtin-roles.js +25 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +198 -22
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +173 -5
- package/dist/config/contact-book.js +32 -0
- package/dist/config/contact-operation-service.js +9 -3
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +3 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -2
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +6 -0
- package/dist/core/auth/operation-authorizer.js +8 -0
- package/dist/core/auth/operation-catalog.js +51 -3
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +10 -13
- package/dist/core/command/connect-menu.js +229 -26
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +269 -85
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +16 -0
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +158 -31
- package/dist/core/daemon-file-cache.js +28 -0
- package/dist/core/event-catalog.js +29 -0
- package/dist/core/evolagent-registry.js +4 -39
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +18 -14
- package/dist/core/message/inbound-admission.js +134 -0
- package/dist/core/message/message-bridge.js +616 -74
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +185 -10
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +381 -79
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +69 -24
- package/dist/core/protected-paths.js +36 -11
- package/dist/core/session/session-fs-store.js +19 -4
- package/dist/core/session/session-manager.js +232 -4
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +224 -89
- package/dist/ipc.js +81 -4
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/trigger/parser.js +55 -15
- package/dist/trigger/patch.js +4 -1
- package/dist/trigger/scheduler.js +441 -39
- package/dist/utils/cross-platform.js +8 -2
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/process-introspect.js +19 -3
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +8 -3
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +4 -4
- package/kits/docs/evolcore/msg.md +5 -5
- package/kits/docs/evolcore/trigger.md +25 -8
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +18 -7
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +364 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +46 -0
- package/kits/templates/roles/member.json +4 -0
- package/kits/templates/roles/visitor.json +4 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +4 -2
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/dist/channels/contact-bind-code.js +0 -134
- package/dist/channels/wecom-card.js +0 -101
- package/dist/channels/wecom-onboarding.js +0 -82
- package/dist/channels/wecom-state.js +0 -191
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,89 @@
|
|
|
3
3
|
本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
|
|
4
4
|
[`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
|
|
5
5
|
|
|
6
|
+
## 0.0.5 (2026-07-31)
|
|
7
|
+
|
|
8
|
+
### 消息准入
|
|
9
|
+
|
|
10
|
+
- 群聊准入统一为开放、联系人、Owner 三种行为,移除额外的渠道白名单配置。
|
|
11
|
+
- 访问策略的修改权限收归 Agent Owner。
|
|
12
|
+
- 完善联系人准入、拉黑与 Owner 权限边界的判定。
|
|
13
|
+
- Connect 群组入口展示实时群组数量。
|
|
14
|
+
|
|
15
|
+
### 运行可靠性
|
|
16
|
+
|
|
17
|
+
- 修复旧版本遗留的 AUN 会话目录身份恢复:可唯一判定身份时安全迁移,无法判定时保持原样并告警。
|
|
18
|
+
- 修正 Linux 容器内进程启动时间与运行时长的计算偏差。
|
|
19
|
+
|
|
20
|
+
### 初始化体验
|
|
21
|
+
|
|
22
|
+
- 优化初始化向导的目录输入,支持家目录简写、错误重试与显式跳过。
|
|
23
|
+
- 未完成初始化时自动运行向导,完成后继续启动服务。
|
|
24
|
+
- 未配置 self-agent 时按实际可用入口给出创建提示。
|
|
25
|
+
|
|
26
|
+
## 0.0.4 (2026-07-30)
|
|
27
|
+
|
|
28
|
+
### 联系人身份与消息准入
|
|
29
|
+
|
|
30
|
+
- 联系人身份绑定统一扩展到飞书、微信、钉钉、QQ 和企业微信渠道,收敛为显式 `/bind` 命令。
|
|
31
|
+
- 支持通过 AUN 私聊自助申请绑定码,并可在保留机器人配置时单独重绑渠道联系人。
|
|
32
|
+
- 新增私聊入站消息准入链路,覆盖联系人申请、审批、拦截与过期处理。
|
|
33
|
+
- 新增联系人请求服务,申请与状态流转改为原子操作,避免并发请求产生不一致。
|
|
34
|
+
- 限制单实例和进程级待绑定请求数量,并限制无效绑定码的连续尝试次数。
|
|
35
|
+
- 拒绝对不存在的联系人执行身份绑定和状态变更。
|
|
36
|
+
- 群聊准入按 `access.policyMode` 分级:`open` 对所有已配置渠道开放,`contacts` 将 Agent 已加入并接收到的
|
|
37
|
+
群聊视为授权,`owners` 仅 Owner 消息开放。
|
|
38
|
+
|
|
39
|
+
### 菜单与授权
|
|
40
|
+
|
|
41
|
+
- 新增群组菜单协议,支持群列表查询、群级 @ 提及和群规则加载策略的查询与更新。
|
|
42
|
+
- 群操作纳入权限目录、角色能力判定和审计约束,菜单与斜杠命令的可见性统一走运行时授权检查。
|
|
43
|
+
- 群级策略通过版本比对和原子写入避免并发覆盖。
|
|
44
|
+
- AUN 菜单增加 Evol 最低版本门禁和加密访问令牌校验。
|
|
45
|
+
- 菜单令牌改为持久化哈希存储并按空闲时间失效,且不再进入追踪日志和观察转发。
|
|
46
|
+
- Connect 菜单准入策略的校验与写入收敛为原子操作,补齐策略域、操作目录和内置角色的授权边界。
|
|
47
|
+
|
|
48
|
+
### 跨会话协作
|
|
49
|
+
|
|
50
|
+
- 跨会话消息必须显式声明 `return none` 或 `required`,Trigger 禁止使用 `required` 等待回流。
|
|
51
|
+
- 聚合同一目标会话的待处理请求和同一来源身份的返回结果,避免重复触发模型轮次。
|
|
52
|
+
- `ec handoff list` 输出新增 `RETURN_POLICY` 列。
|
|
53
|
+
- Trigger 创建必须显式指定 `--exec script|trigger-session|target-session`,不再隐式推断。
|
|
54
|
+
- 新增 `--prompt-file <path>` 承载多行或超长 prompt,与 `--prompt`、`--file` 互斥。
|
|
55
|
+
|
|
56
|
+
### 安全边界
|
|
57
|
+
|
|
58
|
+
- AUN keystore `encryptionSeed` 收口为内置常量,发现自定义配置立即失败关闭。
|
|
59
|
+
- 为 Codex 受控命令绑定一次性委托票据,任务或会话撤销时同步清理。
|
|
60
|
+
- 收紧 EC 字面量解析,按空命令、控制字符、未转义展开、引号错误和命令组合分类失败关闭。
|
|
61
|
+
- Agent 角色目录从不可读 H 类调整为只读 L 类,RoleService 为唯一修改入口。
|
|
62
|
+
- L-class 扩展对 `grep`、`rg`、`find`、`sed` 的安全只读支持。
|
|
63
|
+
|
|
64
|
+
### 运行可靠性
|
|
65
|
+
|
|
66
|
+
- 守护进程退出时冻结消息队列、关闭任务终态、等待中断屏障并保留可恢复主消息。
|
|
67
|
+
- Trigger 持久化执行检查点与会话绑定,支持重启续跑、期限继承及启动并发去重。
|
|
68
|
+
- 微信渠道区分连接、断开和重新认证状态,令牌刷新后重试待发送消息。
|
|
69
|
+
- API 重试增加单次无事件超时,中断卡住的 runner 后继续退避重试。
|
|
70
|
+
- Session Renew 判定模型改由响应模式桶的 `auxiliaryModel`/`auxiliaryEffort` 决定。
|
|
71
|
+
- 直连推理凭据改为整对解析,`apiKey` 与 `baseUrl` 必须来自同一来源。
|
|
72
|
+
- 入队前写入最终会话 ID 与已解析身份,执行阶段不再重新计算角色。
|
|
73
|
+
- Claude Agent SDK 固定到 `0.3.220`,Codex 自动压缩阈值调整为 185000 token。
|
|
74
|
+
|
|
75
|
+
### 管理界面
|
|
76
|
+
|
|
77
|
+
- 会话列表新增类型徽标与筛选,消息视图展示 Agent 显示名。
|
|
78
|
+
- 用量视图的模型与粒度筛选改为变更即查询,模型访问明细默认停留在“全部”。
|
|
79
|
+
- 补全联系人及 AUN 群名、渠道色彩、Runner 子任务展示和 Trigger 模型归属提示。
|
|
80
|
+
|
|
81
|
+
### 修复
|
|
82
|
+
|
|
83
|
+
- 规范 Group Menu 非法请求的协议错误码映射。
|
|
84
|
+
- 保留私聊和群聊 menu 请求的加密状态、话题信息与传输回复上下文。
|
|
85
|
+
- 关联文件统一发送为标准 file 载荷,先返回 `menu.response` 再执行文件发送。
|
|
86
|
+
- 统一 `agent.md` 展示名称解析与缓存,`identity` fragment 仅在名称存在时渲染 `selfName`。
|
|
87
|
+
- 飞书私聊出站补齐 `replyContext.peerId` 与 `routeId` 关联。
|
|
88
|
+
|
|
6
89
|
## 0.0.3 (2026-07-25)
|
|
7
90
|
|
|
8
91
|
### 多渠道交互
|
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ EvolCore 是一个轻量级 AI Agent 网关系统。它为 Claude Code / Codex
|
|
|
43
43
|
1. **消息渠道层** (`src/channels/`) - Feishu + WeChat + DingTalk + QQBot + WeCom + AUN 网络
|
|
44
44
|
2. **消息队列层** (`src/core/message/message-queue.ts`) - 会话级串行处理 + 中断支持
|
|
45
45
|
3. **命令处理层** (`src/core/command/`) - 斜杠命令处理(slash-handler / menu-handler / command-handler)
|
|
46
|
-
4. **消息处理层** (`src/core/message/
|
|
46
|
+
4. **消息处理层** (`src/core/message/response-engine.ts`) - 统一事件处理引擎
|
|
47
47
|
5. **会话管理层** (`src/core/session/session-manager.ts`) - 多项目会话管理
|
|
48
48
|
6. **交互路由层** (`src/core/interaction-router.ts`) - 卡片交互回调注册与路由
|
|
49
49
|
7. **会话存储层** - JSONL 文件(CLI 共用)+ 文件系统(每 chat 一个目录,含 active.json / meta_*.jsonl / messages.jsonl / health.jsonl)
|
|
@@ -63,7 +63,7 @@ MessageQueue.enqueue(streamKey, message)
|
|
|
63
63
|
↓ 否 ↓
|
|
64
64
|
MessageQueue.processNext() ←──────────────┘
|
|
65
65
|
↓
|
|
66
|
-
|
|
66
|
+
ResponseEngine.processMessage()
|
|
67
67
|
↓
|
|
68
68
|
├─ 解析会话和项目路径
|
|
69
69
|
├─ 创建 StreamFlusher(3 秒批量发送)
|
|
@@ -78,12 +78,26 @@ MessageProcessor.processMessage()
|
|
|
78
78
|
### 环境要求
|
|
79
79
|
|
|
80
80
|
- **操作系统**:macOS / Linux / Windows
|
|
81
|
-
- **Node.js** >=
|
|
81
|
+
- **Node.js** >= 22(依赖 `node:sqlite`)
|
|
82
82
|
- **Claude Code** >= 2.1.32(`npm install -g @anthropic-ai/claude-code`)
|
|
83
83
|
|
|
84
84
|
### 1. 安装
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
**一行安装(macOS / Linux)**:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
curl -fsSL https://raw.githubusercontent.com/eLeanwang/evolcore/main/scripts/install.sh | bash
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Windows PowerShell**:
|
|
93
|
+
|
|
94
|
+
```powershell
|
|
95
|
+
irm https://raw.githubusercontent.com/eLeanwang/evolcore/main/scripts/install.ps1 | iex
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
安装器会在新实例上自动进入 `ec init` 交互向导,完成后运行 `ec start` 和 `ec status`。已有配置的升级场景会跳过自动初始化,不会停止或重启服务。详细流程见 [一键安装脚本说明](docs/evolcore-install-script.md)。
|
|
99
|
+
|
|
100
|
+
**npm 全局安装**:
|
|
87
101
|
|
|
88
102
|
```bash
|
|
89
103
|
npm install -g evolcore
|
|
@@ -128,7 +142,7 @@ ec init aun
|
|
|
128
142
|
|
|
129
143
|
交互式引导完成以下配置:
|
|
130
144
|
- 环境检查(Node.js >= 22、claude CLI、SDK 版本)
|
|
131
|
-
-
|
|
145
|
+
- 渠道选择(飞书 / 微信 / 钉钉 / QQ 频道 / 企业微信 / AUN)并扫码登录
|
|
132
146
|
- 默认项目路径
|
|
133
147
|
- 模型选择(sonnet/opus/haiku)
|
|
134
148
|
- owner 配置:默认走 **Evol App 扫码绑定**,若无 Evol App 可手工录入 AID 兜底
|
|
@@ -207,7 +221,8 @@ evolcore/
|
|
|
207
221
|
│ │ │ └── slash-gate.ts # 权限前置拦截
|
|
208
222
|
│ │ ├── message/
|
|
209
223
|
│ │ │ ├── message-bridge.ts # 渠道 ↔ 核心消息桥
|
|
210
|
-
│ │ │ ├──
|
|
224
|
+
│ │ │ ├── response-engine.ts # 统一响应处理引擎
|
|
225
|
+
│ │ │ ├── inbound-admission.ts # 私聊入站准入
|
|
211
226
|
│ │ │ ├── message-queue.ts # 消息队列(串行+中断)
|
|
212
227
|
│ │ │ ├── message-cache.ts # 消息缓存
|
|
213
228
|
│ │ │ ├── message-log.ts # 每 chat 的 messages.jsonl
|
|
@@ -216,11 +231,11 @@ evolcore/
|
|
|
216
231
|
│ │ │ ├── adapters/ # 各后端会话文件适配器
|
|
217
232
|
│ │ │ ├── session-fs-store.ts # 文件系统存储原语
|
|
218
233
|
│ │ │ └── session-manager.ts # 会话管理(多项目支持)
|
|
219
|
-
│ │ ├──
|
|
234
|
+
│ │ ├── auth/ # 授权审计与 Trigger 鉴权
|
|
220
235
|
│ │ ├── evolagent.ts # EvolAgent 实体
|
|
221
236
|
│ │ ├── evolagent-registry.ts # Agent 注册表(扫描/路由/热重载)
|
|
222
237
|
│ │ ├── interaction-router.ts # 卡片交互回调路由
|
|
223
|
-
│ │ └──
|
|
238
|
+
│ │ └── protected-paths.ts # 受保护路径分级
|
|
224
239
|
│ ├── channels/
|
|
225
240
|
│ │ ├── feishu.ts # 飞书 WebSocket 渠道
|
|
226
241
|
│ │ ├── wechat.ts # 微信 ClawBot 渠道
|
|
@@ -228,13 +243,18 @@ evolcore/
|
|
|
228
243
|
│ │ ├── qqbot.ts # QQ 频道渠道
|
|
229
244
|
│ │ ├── wecom.ts # 企业微信 AI Bot 渠道
|
|
230
245
|
│ │ └── aun.ts # AUN Mesh 网络渠道
|
|
246
|
+
│ ├── config/ # 配置分层、访问策略、联系人绑定
|
|
247
|
+
│ ├── eck/ # ECK 上下文组装(manifest / kit 渲染)
|
|
248
|
+
│ ├── response-system/ # 响应模式决策与参数解析
|
|
249
|
+
│ ├── trigger/ # 触发器引擎
|
|
250
|
+
│ ├── stats/ # 用量统计与计费
|
|
231
251
|
│ ├── cli/ # CLI 命令
|
|
232
252
|
│ ├── utils/ # 工具函数
|
|
233
253
|
│ ├── types.ts # 类型定义
|
|
234
254
|
│ ├── config-store.ts # 配置加载
|
|
235
255
|
│ ├── paths.ts # 路径解析
|
|
236
256
|
│ └── index.ts # 主入口
|
|
237
|
-
└── kits/ #
|
|
257
|
+
└── kits/ # 共享上下文模板(rules / docs / schemas)
|
|
238
258
|
```
|
|
239
259
|
|
|
240
260
|
## 斜杠命令
|
|
@@ -252,6 +272,11 @@ evolcore/
|
|
|
252
272
|
- `/check` - 系统健康检查(摘要)
|
|
253
273
|
- `/help` - 显示所有命令
|
|
254
274
|
|
|
275
|
+
**身份绑定**:
|
|
276
|
+
- `/bind <6 位绑定码>` - 将当前 IM 账号绑定到已登记的联系人身份(仅私聊)。
|
|
277
|
+
支持飞书、微信、钉钉、QQ 频道、企业微信。绑定码有效期 10 分钟,可由
|
|
278
|
+
`ec init <channel>` 生成,或在目标 Agent 的 AUN 私聊发送 `/bind <channel>` 自助申请。
|
|
279
|
+
|
|
255
280
|
### 管理员级命令(Admin+ 可用)
|
|
256
281
|
|
|
257
282
|
**项目**:
|
|
@@ -280,6 +305,16 @@ evolcore/
|
|
|
280
305
|
- `/restart` - 重启服务(自愈机制)
|
|
281
306
|
- `/repair` - 检查并修复会话
|
|
282
307
|
|
|
308
|
+
### 联系人与关系管理(menu 协议)
|
|
309
|
+
|
|
310
|
+
`/connect` 与 `/group` 是 menu 协议操作,通过 Evol App 或 EC Web 调用,不是 IM 里的斜杠命令。
|
|
311
|
+
|
|
312
|
+
- **`/connect`** - 联系人与关系管理:联系人列表、群组列表、角色分配、管理员授权、拉黑状态。
|
|
313
|
+
准入策略(`connect.access.write`)为 owner 专属,角色分配与 admin 授权委派给 `role` menu 执行,
|
|
314
|
+
复用其 agent 级互斥锁、CAS revision 与审计链。
|
|
315
|
+
- **`/group`** - 群组管理:群列表、群级 @ 提及策略、群规则加载策略的查询与更新。
|
|
316
|
+
群操作纳入权限目录与角色能力判定,通过版本比对和原子写入避免并发覆盖。
|
|
317
|
+
|
|
283
318
|
### ⚠️ 进程级 menu 操作鉴权(v3.2 Breaking)
|
|
284
319
|
|
|
285
320
|
进程级 menu 操作(`/system restart/upgrade`、`/agent` agent 生命周期管理)的鉴权已迁移到
|
|
@@ -309,7 +344,7 @@ v3.2 新增进程级身份标识。启动时自动生成 `ec+5位数字.agentid.
|
|
|
309
344
|
## 技术栈
|
|
310
345
|
|
|
311
346
|
- **运行时**:Node.js >= 22 + TypeScript(ES modules)
|
|
312
|
-
- **AI 后端**:@anthropic-ai/claude-agent-sdk
|
|
347
|
+
- **AI 后端**:@anthropic-ai/claude-agent-sdk 0.3.220(固定版本)、Codex CLI app-server、Gemini CLI
|
|
313
348
|
- **消息渠道**:飞书(@larksuiteoapi/node-sdk)、微信(ClawBot ilink API)、钉钉(dingtalk-stream)、QQ频道(pure-qqbot)、企业微信(AI Bot API)、AUN 网络
|
|
314
349
|
- **数据存储**:文件系统(per-chat 目录) + JSONL(CLI 共用)
|
|
315
350
|
- **测试框架**:Vitest
|
package/bin/ec-safe-output.js
CHANGED
|
@@ -6,6 +6,13 @@ import { spawn } from 'child_process';
|
|
|
6
6
|
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
7
7
|
const EC_EXECUTABLES = new Set(['ec']);
|
|
8
8
|
const HOST_PROCESS_EXECUTABLES = new Set(['ps', 'pgrep', 'pidof', 'kill', 'pkill', 'killall']);
|
|
9
|
+
const HEAD_EXECUTABLE = ['/usr/bin/head', '/bin/head'].find(candidate => {
|
|
10
|
+
try {
|
|
11
|
+
return fs.statSync(candidate).isFile();
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
9
16
|
|
|
10
17
|
function fail(message) {
|
|
11
18
|
process.stderr.write(`ec-safe-output: ${message}\n`);
|
|
@@ -34,6 +41,33 @@ function validateCommand(payload) {
|
|
|
34
41
|
return !payload.argv.some(arg => /\bnode(?:js|\.exe)?\b/i.test(arg));
|
|
35
42
|
}
|
|
36
43
|
|
|
44
|
+
function validateStdinOnlyHeadArgs(args) {
|
|
45
|
+
if (args === undefined) return true;
|
|
46
|
+
if (!Array.isArray(args) || args.some(arg => typeof arg !== 'string')) return false;
|
|
47
|
+
let optionsEnded = false;
|
|
48
|
+
let expectsOptionValue = false;
|
|
49
|
+
for (const arg of args) {
|
|
50
|
+
if (expectsOptionValue) {
|
|
51
|
+
expectsOptionValue = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (optionsEnded) {
|
|
55
|
+
if (arg !== '-') return false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (arg === '--') {
|
|
59
|
+
optionsEnded = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (arg === '-') continue;
|
|
63
|
+
if (!arg.startsWith('-')) return false;
|
|
64
|
+
if (arg === '-n' || arg === '-c' || arg === '--lines' || arg === '--bytes') {
|
|
65
|
+
expectsOptionValue = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
37
71
|
function resolveExecutionPaths(payload) {
|
|
38
72
|
if (typeof payload.workspace !== 'string' || typeof payload.cwd !== 'string') fail('invalid workspace');
|
|
39
73
|
let workspace;
|
|
@@ -87,36 +121,19 @@ function openOutputFile(payload, paths) {
|
|
|
87
121
|
const payload = decodePayload(process.argv[2]);
|
|
88
122
|
if (!payload) fail('invalid payload');
|
|
89
123
|
if (!validateCommand(payload)) fail('command is not permitted');
|
|
90
|
-
if (payload.
|
|
91
|
-
|
|
92
|
-
fail('invalid head line limit');
|
|
93
|
-
}
|
|
124
|
+
if (!validateStdinOnlyHeadArgs(payload.headArgs)) fail('head may only consume stdin');
|
|
125
|
+
if (payload.headArgs !== undefined && !HEAD_EXECUTABLE) fail('trusted head executable is unavailable');
|
|
94
126
|
|
|
95
127
|
const paths = resolveExecutionPaths(payload);
|
|
96
128
|
const output = openOutputFile(payload, paths);
|
|
97
129
|
let bytesWritten = 0;
|
|
98
|
-
let linesWritten = 0;
|
|
99
130
|
let limitReached = false;
|
|
100
131
|
let writeFailed = false;
|
|
101
132
|
|
|
102
133
|
function writeLimited(chunk) {
|
|
103
134
|
if (limitReached || writeFailed) return;
|
|
104
135
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
105
|
-
|
|
106
|
-
if (payload.headLines !== undefined) {
|
|
107
|
-
let cursor = 0;
|
|
108
|
-
while (cursor < allowed) {
|
|
109
|
-
const newline = buffer.indexOf(0x0a, cursor);
|
|
110
|
-
if (newline < 0 || newline >= allowed) break;
|
|
111
|
-
linesWritten++;
|
|
112
|
-
cursor = newline + 1;
|
|
113
|
-
if (linesWritten >= payload.headLines) {
|
|
114
|
-
allowed = cursor;
|
|
115
|
-
limitReached = true;
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
136
|
+
const allowed = Math.min(buffer.length, MAX_OUTPUT_BYTES - bytesWritten);
|
|
120
137
|
if (allowed <= 0) {
|
|
121
138
|
limitReached = true;
|
|
122
139
|
return;
|
|
@@ -132,13 +149,35 @@ function writeLimited(chunk) {
|
|
|
132
149
|
}
|
|
133
150
|
}
|
|
134
151
|
|
|
152
|
+
const head = payload.headArgs === undefined
|
|
153
|
+
? undefined
|
|
154
|
+
: spawn(HEAD_EXECUTABLE, payload.headArgs, {
|
|
155
|
+
cwd: paths.cwd,
|
|
156
|
+
env: process.env,
|
|
157
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
158
|
+
});
|
|
159
|
+
if (head) {
|
|
160
|
+
head.stdout.on('data', writeLimited);
|
|
161
|
+
// Early-exit options such as --help or a small -n/-c close stdin before the
|
|
162
|
+
// EC process finishes. Ignore EPIPE and keep draining EC output below.
|
|
163
|
+
head.stdin.on('error', () => {});
|
|
164
|
+
}
|
|
165
|
+
|
|
135
166
|
const child = spawn(payload.argv[0], payload.argv.slice(1), {
|
|
136
167
|
cwd: paths.cwd,
|
|
137
168
|
env: process.env,
|
|
138
169
|
stdio: ['ignore', 'pipe', payload.mergeStderr ? 'pipe' : 'inherit'],
|
|
139
170
|
});
|
|
140
|
-
child.stdout
|
|
141
|
-
if (
|
|
171
|
+
const sourceStreams = [child.stdout, ...(payload.mergeStderr ? [child.stderr] : [])];
|
|
172
|
+
if (head) {
|
|
173
|
+
for (const stream of sourceStreams) {
|
|
174
|
+
// The no-op data listener keeps draining the source if head exits early.
|
|
175
|
+
stream.on('data', () => {});
|
|
176
|
+
stream.pipe(head.stdin, { end: false });
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
for (const stream of sourceStreams) stream.on('data', writeLimited);
|
|
180
|
+
}
|
|
142
181
|
|
|
143
182
|
child.once('error', error => {
|
|
144
183
|
if (output) {
|
|
@@ -148,7 +187,13 @@ child.once('error', error => {
|
|
|
148
187
|
fail(`unable to start command: ${error.message}`);
|
|
149
188
|
});
|
|
150
189
|
|
|
151
|
-
|
|
190
|
+
let sourceClosed = false;
|
|
191
|
+
let sourceCode = 1;
|
|
192
|
+
let headClosed = !head;
|
|
193
|
+
let headCode = 1;
|
|
194
|
+
|
|
195
|
+
function finish() {
|
|
196
|
+
if (!sourceClosed || !headClosed) return;
|
|
152
197
|
if (output) {
|
|
153
198
|
try { fs.closeSync(output.fd); } catch { writeFailed = true; }
|
|
154
199
|
}
|
|
@@ -157,5 +202,25 @@ child.once('close', code => {
|
|
|
157
202
|
fail('failed to write command output');
|
|
158
203
|
}
|
|
159
204
|
if (output) process.stderr.write(`Saved output to ${output.target}\n`);
|
|
160
|
-
process.exitCode =
|
|
205
|
+
process.exitCode = head ? headCode : sourceCode;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (head) {
|
|
209
|
+
head.once('error', error => {
|
|
210
|
+
writeFailed = true;
|
|
211
|
+
process.stderr.write(`ec-safe-output: unable to start head: ${error.message}\n`);
|
|
212
|
+
});
|
|
213
|
+
head.once('close', code => {
|
|
214
|
+
headCode = code ?? 1;
|
|
215
|
+
headClosed = true;
|
|
216
|
+
for (const stream of sourceStreams) stream.unpipe(head.stdin);
|
|
217
|
+
finish();
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
child.once('close', code => {
|
|
222
|
+
sourceCode = code ?? 1;
|
|
223
|
+
sourceClosed = true;
|
|
224
|
+
if (head && !head.stdin.destroyed) head.stdin.end();
|
|
225
|
+
finish();
|
|
161
226
|
});
|
package/dist/agents/baseagent.js
CHANGED
|
@@ -32,6 +32,22 @@ export function normalizeBaseagent(input) {
|
|
|
32
32
|
const key = String(input || '').trim().toLowerCase().replace(/_/g, '-');
|
|
33
33
|
return BASEAGENT_ALIASES[key] || { canonical: 'unknown', displayName: input ? String(input) : 'Unknown' };
|
|
34
34
|
}
|
|
35
|
+
function directInferencePair(apiKey, baseUrl) {
|
|
36
|
+
const normalizedApiKey = directInferenceValue(apiKey);
|
|
37
|
+
const normalizedBaseUrl = directInferenceValue(baseUrl);
|
|
38
|
+
if (!normalizedApiKey || !normalizedBaseUrl)
|
|
39
|
+
return undefined;
|
|
40
|
+
return { apiKey: normalizedApiKey, baseUrl: normalizedBaseUrl };
|
|
41
|
+
}
|
|
42
|
+
function directInferenceValue(...values) {
|
|
43
|
+
for (const value of values) {
|
|
44
|
+
const normalized = value?.trim();
|
|
45
|
+
const lower = normalized?.toLowerCase();
|
|
46
|
+
if (normalized && !lower?.includes('your-') && !lower?.includes('placeholder'))
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
35
51
|
function loadClaudeSettings() {
|
|
36
52
|
try {
|
|
37
53
|
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
@@ -42,6 +58,21 @@ function loadClaudeSettings() {
|
|
|
42
58
|
catch { }
|
|
43
59
|
return {};
|
|
44
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a complete credential/endpoint pair for direct Anthropic HTTP calls.
|
|
63
|
+
* A pair must come from one source so partial configuration cannot be mixed
|
|
64
|
+
* across Agent config, the process environment, and Claude Code settings.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveAnthropicDirectConfig(override) {
|
|
67
|
+
const explicit = directInferencePair(override?.apiKey, override?.baseUrl);
|
|
68
|
+
if (explicit)
|
|
69
|
+
return explicit;
|
|
70
|
+
const environment = directInferencePair(directInferenceValue(process.env.ANTHROPIC_AUTH_TOKEN, process.env.ANTHROPIC_API_KEY), process.env.ANTHROPIC_BASE_URL);
|
|
71
|
+
if (environment)
|
|
72
|
+
return environment;
|
|
73
|
+
const settings = loadClaudeSettings();
|
|
74
|
+
return directInferencePair(directInferenceValue(settings.env?.ANTHROPIC_AUTH_TOKEN, settings.env?.ANTHROPIC_API_KEY), settings.env?.ANTHROPIC_BASE_URL);
|
|
75
|
+
}
|
|
45
76
|
export function resolveAnthropicConfig(config, override) {
|
|
46
77
|
const settings = loadClaudeSettings();
|
|
47
78
|
const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
|
|
@@ -103,6 +134,21 @@ function loadCodexSettings() {
|
|
|
103
134
|
catch { }
|
|
104
135
|
return {};
|
|
105
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Resolve a complete credential/endpoint pair for direct OpenAI HTTP calls.
|
|
139
|
+
* auth.json and config.toml together form the Codex SDK source; fields are
|
|
140
|
+
* never combined with a partial Agent or process-environment configuration.
|
|
141
|
+
*/
|
|
142
|
+
export function resolveOpenaiDirectConfig(override) {
|
|
143
|
+
const explicit = directInferencePair(override?.apiKey, override?.baseUrl);
|
|
144
|
+
if (explicit)
|
|
145
|
+
return explicit;
|
|
146
|
+
const environment = directInferencePair(process.env.OPENAI_API_KEY, process.env.OPENAI_BASE_URL);
|
|
147
|
+
if (environment)
|
|
148
|
+
return environment;
|
|
149
|
+
const settings = loadCodexSettings();
|
|
150
|
+
return directInferencePair(settings.apiKey, settings.baseUrl);
|
|
151
|
+
}
|
|
106
152
|
export function resolveOpenaiConfig(config, override) {
|
|
107
153
|
const codexSettings = loadCodexSettings();
|
|
108
154
|
const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
|
|
@@ -855,20 +855,6 @@ export class AgentRunner {
|
|
|
855
855
|
const customText = values?.custom_text;
|
|
856
856
|
finish({ kind: 'answered', value: typeof customText === 'string' && customText.trim() ? customText.trim() : null });
|
|
857
857
|
}
|
|
858
|
-
else if (action === '_show_input') {
|
|
859
|
-
if (permCtx?.interceptNextMessage) {
|
|
860
|
-
permCtx.interactionRouter?.markWaiting(sessionId);
|
|
861
|
-
sendPrompt?.('✏️ 请直接发送你的自定义回复').catch(() => { });
|
|
862
|
-
permCtx.interceptNextMessage(sessionId, (msg) => {
|
|
863
|
-
permCtx.interactionRouter?.unmarkWaiting(sessionId);
|
|
864
|
-
const text = (msg.content || '').trim();
|
|
865
|
-
finish({ kind: 'answered', value: text || null });
|
|
866
|
-
});
|
|
867
|
-
}
|
|
868
|
-
else {
|
|
869
|
-
finish({ kind: 'cancelled', reason: 'explicit_cancel' });
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
858
|
else if (action === 'submit' && q.multiSelect && values) {
|
|
873
859
|
const selected = [];
|
|
874
860
|
q.options.forEach((opt, idx) => {
|
|
@@ -900,7 +886,6 @@ export class AgentRunner {
|
|
|
900
886
|
});
|
|
901
887
|
if (settlement.kind === 'cancelled') {
|
|
902
888
|
try {
|
|
903
|
-
permCtx.cancelIntercept?.(sessionId);
|
|
904
889
|
try {
|
|
905
890
|
await permCtx.turn?.onInteractionSettled?.(requestId, settlement.reason === 'timeout' ? 'timed_out' : 'cancelled');
|
|
906
891
|
}
|
|
@@ -1133,20 +1118,6 @@ export class AgentRunner {
|
|
|
1133
1118
|
const feedback = typeof values?.custom_text === 'string' ? values.custom_text.trim() : '';
|
|
1134
1119
|
deny(feedback || '用户提交了反馈');
|
|
1135
1120
|
}
|
|
1136
|
-
else if (trimmed === '_show_input') {
|
|
1137
|
-
if (permCtx.interceptNextMessage) {
|
|
1138
|
-
router.markWaiting(sessionId);
|
|
1139
|
-
sendPrompt?.('✏️ 请直接发送你的反馈意见').catch(() => { });
|
|
1140
|
-
permCtx.interceptNextMessage(sessionId, msg => {
|
|
1141
|
-
router.unmarkWaiting(sessionId);
|
|
1142
|
-
const feedback = (msg.content || '').trim();
|
|
1143
|
-
deny(feedback || '用户提交了反馈');
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
else {
|
|
1147
|
-
deny('用户提交了反馈');
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
1121
|
else if (trimmed === '2' || trimmed.toLowerCase() === 'reject' || trimmed === '拒绝') {
|
|
1151
1122
|
deny('用户拒绝了计划');
|
|
1152
1123
|
}
|
|
@@ -1173,7 +1144,6 @@ export class AgentRunner {
|
|
|
1173
1144
|
});
|
|
1174
1145
|
try {
|
|
1175
1146
|
if (settlement.kind === 'cancelled') {
|
|
1176
|
-
permCtx.cancelIntercept?.(sessionId);
|
|
1177
1147
|
try {
|
|
1178
1148
|
await permCtx.turn?.onInteractionSettled?.(requestId, settlement.reason === 'timeout' ? 'timed_out' : 'cancelled');
|
|
1179
1149
|
}
|
|
@@ -1886,7 +1856,7 @@ export class AgentRunner {
|
|
|
1886
1856
|
}
|
|
1887
1857
|
// These tools need the dedicated canUseTool interaction handlers below.
|
|
1888
1858
|
if (toolName === 'AskUserQuestion' || toolName === 'ExitPlanMode') {
|
|
1889
|
-
return hookDecision('
|
|
1859
|
+
return hookDecision('ask', undefined, updatedInput);
|
|
1890
1860
|
}
|
|
1891
1861
|
if (callPermissionMode === 'readonly') {
|
|
1892
1862
|
const permCtx = this.permissionContexts.get(sessionId);
|
|
@@ -167,6 +167,74 @@ export class CodexAppServerClient {
|
|
|
167
167
|
await this.request('thread/metadata/update', params);
|
|
168
168
|
return true;
|
|
169
169
|
}
|
|
170
|
+
async threadShellCommand(threadId, command, timeoutMs = 10_000) {
|
|
171
|
+
let commandItemId;
|
|
172
|
+
let output = '';
|
|
173
|
+
let settle;
|
|
174
|
+
let fail;
|
|
175
|
+
const completion = new Promise((resolve, reject) => {
|
|
176
|
+
settle = resolve;
|
|
177
|
+
fail = reject;
|
|
178
|
+
});
|
|
179
|
+
const unsubscribe = this.onNotification(notification => {
|
|
180
|
+
const params = (notification.params || {});
|
|
181
|
+
const notificationThreadId = params.threadId ?? params.thread_id;
|
|
182
|
+
if (notificationThreadId !== threadId)
|
|
183
|
+
return;
|
|
184
|
+
const item = params.item && typeof params.item === 'object'
|
|
185
|
+
? params.item
|
|
186
|
+
: undefined;
|
|
187
|
+
const itemId = typeof item?.id === 'string'
|
|
188
|
+
? item.id
|
|
189
|
+
: typeof params.itemId === 'string'
|
|
190
|
+
? params.itemId
|
|
191
|
+
: undefined;
|
|
192
|
+
const source = item?.source;
|
|
193
|
+
const isUserShell = source === 'userShell' || source === 'user_shell';
|
|
194
|
+
if (notification.method === 'item/started'
|
|
195
|
+
&& item?.type === 'commandExecution'
|
|
196
|
+
&& isUserShell
|
|
197
|
+
&& (item.command === command || commandItemId === undefined)) {
|
|
198
|
+
commandItemId = itemId;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (notification.method === 'item/commandExecution/outputDelta'
|
|
202
|
+
&& commandItemId
|
|
203
|
+
&& itemId === commandItemId
|
|
204
|
+
&& typeof params.delta === 'string') {
|
|
205
|
+
output += params.delta;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (notification.method === 'item/completed'
|
|
209
|
+
&& item?.type === 'commandExecution'
|
|
210
|
+
&& isUserShell
|
|
211
|
+
&& (!commandItemId || itemId === commandItemId)) {
|
|
212
|
+
const aggregated = typeof item.aggregatedOutput === 'string'
|
|
213
|
+
? item.aggregatedOutput
|
|
214
|
+
: typeof item.aggregated_output === 'string'
|
|
215
|
+
? item.aggregated_output
|
|
216
|
+
: output;
|
|
217
|
+
const exitCode = typeof item.exitCode === 'number'
|
|
218
|
+
? item.exitCode
|
|
219
|
+
: typeof item.exit_code === 'number'
|
|
220
|
+
? item.exit_code
|
|
221
|
+
: undefined;
|
|
222
|
+
settle({ output: aggregated, ...(exitCode !== undefined ? { exitCode } : {}) });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
const timer = setTimeout(() => {
|
|
226
|
+
fail(new Error(`thread/shellCommand timed out for thread ${threadId}`));
|
|
227
|
+
}, timeoutMs);
|
|
228
|
+
timer.unref?.();
|
|
229
|
+
try {
|
|
230
|
+
await this.request('thread/shellCommand', { threadId, command });
|
|
231
|
+
return await completion;
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
unsubscribe();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
170
238
|
async turnInterrupt(threadId, turnId) {
|
|
171
239
|
await this.request('turn/interrupt', { threadId, turnId });
|
|
172
240
|
return true;
|