evolclaw 3.1.5 → 3.1.6
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 +53 -3
- package/dist/agents/claude-runner.js +69 -24
- package/dist/agents/kit-renderer.js +15 -4
- package/dist/aun/aid/agentmd.js +10 -3
- package/dist/aun/msg/group.js +2 -2
- package/dist/channels/aun.js +98 -12
- package/dist/channels/dingtalk.js +1 -1
- package/dist/channels/feishu.js +31 -9
- package/dist/channels/qqbot.js +1 -1
- package/dist/channels/wechat.js +1 -1
- package/dist/channels/wecom.js +1 -1
- package/dist/cli/agent.js +10 -11
- package/dist/cli/bench.js +1 -5
- package/dist/cli/help.js +8 -0
- package/dist/cli/index.js +91 -128
- package/dist/cli/init.js +37 -21
- package/dist/cli/link-rules.js +1 -7
- package/dist/cli/model.js +231 -6
- package/dist/config-store.js +1 -22
- package/dist/core/command-handler.js +181 -48
- package/dist/core/evolagent.js +0 -18
- package/dist/core/message/im-renderer.js +9 -20
- package/dist/core/message/message-bridge.js +7 -3
- package/dist/core/message/message-processor.js +138 -35
- package/dist/core/relation/peer-identity.js +23 -11
- package/dist/core/trigger/parser.js +4 -4
- package/dist/core/trigger/scheduler.js +20 -6
- package/dist/index.js +55 -5
- package/dist/ipc.js +1 -1
- package/dist/utils/error-utils.js +6 -0
- package/dist/utils/process-introspect.js +7 -5
- package/kits/docs/INDEX.md +4 -8
- package/kits/docs/context-assembly.md +1 -0
- package/kits/docs/evolclaw/INDEX.md +43 -0
- package/kits/docs/evolclaw/group.md +13 -6
- package/kits/docs/evolclaw/model.md +51 -0
- package/kits/docs/evolclaw/msg.md +5 -0
- package/kits/docs/venues/group.md +13 -1
- package/kits/eck_manifest.json +9 -0
- package/kits/rules/06-channel.md +5 -1
- package/kits/templates/system-fragments/baseagent.md +7 -1
- package/kits/templates/system-fragments/channel.md +7 -5
- package/kits/templates/system-fragments/commands.md +19 -0
- package/kits/templates/system-fragments/session.md +9 -0
- package/kits/templates/system-fragments/venue.md +15 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,63 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.1.6 (2026-06-03)
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
- **AUN 图片附件支持** — AUN 私聊/群聊附件中的图片自动检测(元数据/文件名/magic bytes 三重检测),base64 编码后直接传给 Agent,不再要求 Read 工具读取
|
|
8
|
+
- **selfAID session 注入** — 从 channel key 解析 selfAID,透传至 MessageBridge → CommandHandler → `getOrCreateSession`,修复 AUN 多身份场景 session 归属错误
|
|
9
|
+
- **watch-web 单实例保护** — 启动前清理旧实例(按 instance 文件 + 端口兜底),端口冲突时自动切换并提示;前端新增 token 失效自动回配对页、退出按钮、配对码过期刷新
|
|
10
|
+
|
|
11
|
+
### Improvements
|
|
12
|
+
|
|
13
|
+
- **init 流程优化** — `cmdInit` 一次性写入所有可用 baseagents(不只写选中的那个);新增 projects 默认目录询问步骤;默认项目路径改为 `EVOLCLAW_HOME/projects`;取消 init 后自动进入 `agent new`,改为打印提示
|
|
14
|
+
- **1M 上下文窗口** — `claude-opus-4-8` / `claude-sonnet-4-6` 自动追加 `[1m]` 后缀,`autoCompactWindow` 随之调整为 900000
|
|
15
|
+
- **上下文用量追踪** — complete 事件新增 `contextUsage`(totalTokens/maxTokens/percentage),`usage` 字段重命名为 `tokenUsage`,随 `status.completed` 元数据下发
|
|
16
|
+
- **模型别名缓存 TTL** — 从 1h 缩短至 5min
|
|
17
|
+
- **`/cli` 远程透传** — 经消息通道远程执行 CLI,仅 owner 可用,白名单只读+配置命令,spawn 无 shell,15s 超时 + 128KB 截断
|
|
18
|
+
- **proximity ECK 注入** — `same_device/same_network/same_egress_ip` 从 V2 E2EE 解密结果提取,注入 venue.md 条件块渲染
|
|
19
|
+
- **fastaun 0.4.9** — 含 SPKI 双格式指纹匹配(0.4.8)和 watch-web 单实例相关修复(0.4.9)
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
- **Feishu merge_forward 内容重复** — 直接转发时丢弃 quotedText,避免内容重复
|
|
24
|
+
- **Feishu seenMsg 文件写入** — 仅在有记录被 GC 清理时才重写文件;seenMessages 清空时改为 unlink
|
|
25
|
+
- **peer-identity 验签** — 新增 `agentmd-unverified` 来源;缓存命中时从 `verify_status` 恢复验签状态,修复 source 误降级
|
|
26
|
+
- **process-introspect** — 改用 `/proc/stat btime`(绝对 epoch)替代 uptime 计算进程启动时间,修复长时间运行后 PID 复用误判
|
|
27
|
+
- **evolagent.load IPC 超时** — AUN WebSocket 冷启动常 >3s,IPC 超时从默认值调整为 30s
|
|
28
|
+
- **交互路由 markWaiting/unmarkWaiting** — 新增提前占位方法,适配异步发卡片场景的等待计数
|
|
29
|
+
|
|
3
30
|
## v3.1.5 (2026-06-02)
|
|
4
31
|
|
|
32
|
+
### New Features
|
|
33
|
+
|
|
34
|
+
- **Web 监控面板(watch-web)** — 全新 `evolclaw watch` Web 面板:实时会话视图、对话视图、AID 状态、消息流,含上下文大小与费用估算;新增 `src/cli/watch-web/` server + 静态前端 + session/msg/aid 数据源
|
|
35
|
+
- **CLI model 命令** — 新增 `src/cli/model.ts`:动态模型目录管理,`/models` 端点拉取(1h 缓存)+ 静态表 fallback,注入已验证但未上线 API 的模型
|
|
36
|
+
- **飞书交互卡片 JSON 2.0 + 表单** — `FeishuCardManager` 管理 CardKit 实体生命周期,`buildActionCardV2()` 表单卡片,动态输入框追加(card.element.create API)
|
|
37
|
+
- **proactive→interactive 模式切换提示** — 模式切换时注入提示信息
|
|
38
|
+
|
|
39
|
+
### Improvements
|
|
40
|
+
|
|
41
|
+
- **AUN/AID 三主体模型重构** — fastaun 0.3.3 → 0.4.7 大版本跨越:适配三主体模型与 slot 隔离键,`createAid` 拆分为 `registerAid` + `authenticate`,新增 `AIDStore`,`uploadAgentMd` 迁移至 AIDStore
|
|
42
|
+
- **ECK 上下文组装体系对齐** — 四阶段改造:channelKey 第二段 `selfPeerId`→`selfAID`,sessions 目录统一三层化 `<channelType>/<selfAID>/<channelId>/`,新增 `peer-key.ts` helper,ECK manifest 诊断输出
|
|
43
|
+
- **Session 持久化重构** — 统一 `persistSession(session, intent, opts)` 原语替代 `writeSessionIfChanged`/快照模式,内置去重,`markProcessing`/`clearProcessing` 不再扫描全部 chat 目录
|
|
44
|
+
- **idle-check 消息队列可靠性** — 提升空闲检测与消息队列稳定性
|
|
45
|
+
- **飞书卡片 schema 2.0 统一** — 交互卡片统一为 schema 2.0 inline,用户交互期间暂停 idle monitor,修复 V2 卡片回调 schema 不一致(error 200830/200810)
|
|
46
|
+
- **IPC 出站统计** — 新增 `aun-aid-stats-record-outbound` IPC handler,p2p/group/thought.put 出站消息统计追踪
|
|
47
|
+
- **/model 列表逻辑简化** — `listModels` 走缓存,model 显示标签同时展示完整 ID 与短别名
|
|
48
|
+
- **清理冗余日志与代码** — 移除过时诊断日志和未使用的 skills 代码
|
|
49
|
+
|
|
5
50
|
### Bug Fixes
|
|
6
51
|
|
|
7
|
-
- **Trigger channelType 传播** — Trigger 新增 `targetChannelType` 字段,`buildSyntheticMessage` 正确填充 `channelType`,修复触发器消息无法创建 session
|
|
8
|
-
- **
|
|
52
|
+
- **Trigger channelType 传播** — Trigger 新增 `targetChannelType` 字段,`buildSyntheticMessage` 正确填充 `channelType`,修复触发器消息无法创建 session
|
|
53
|
+
- **session selfAID/channelType 注入** — 修复 session 创建时 selfAID 与 channelType 注入缺失
|
|
54
|
+
- **/model 短别名解析** — `/model sonnet` 等短别名正确解析为完整 model ID
|
|
55
|
+
- **aidCreate 成功路径泄漏 AIDStore** — 修复 AID 创建成功后 AIDStore 资源泄漏
|
|
56
|
+
- **peer-identity type 解析** — 修复对端身份类型解析错误
|
|
57
|
+
- **飞书卡片标题重复 emoji** — 修复 resolved card 标题与 checkers summary 中的双 emoji
|
|
58
|
+
- **消息可靠性与 session 迁移** — 提升消息投递可靠性,改进 session 迁移逻辑
|
|
59
|
+
- **session-mapper 过滤 channelName** — `sessionToFile` 不再将 `channelName` 写入 metadata
|
|
9
60
|
- **resolveChatDirFromSession 严格校验** — 缺失 `channelType` 时抛出明确错误而非静默 fallback
|
|
10
|
-
- **/model 别名解析** — `/model sonnet` 等短别名正确解析为完整 model ID
|
|
11
61
|
- **test 脚本修复** — `package.json` test 脚本改为 `vitest run`
|
|
12
62
|
|
|
13
63
|
## v3.1.4 (2026-05-27)
|
|
@@ -24,7 +24,7 @@ const STATIC_MODEL_ALIASES = {
|
|
|
24
24
|
'sonnet': 'claude-sonnet-4-6',
|
|
25
25
|
'haiku': 'claude-haiku-4-5-20251001',
|
|
26
26
|
};
|
|
27
|
-
const MODEL_ALIAS_TTL_MS =
|
|
27
|
+
const MODEL_ALIAS_TTL_MS = 5 * 60 * 1000; // 5min
|
|
28
28
|
const modelAliasCache = new Map(); // key: baseUrl
|
|
29
29
|
const modelAliasInFlight = new Set(); // 去重并发刷新
|
|
30
30
|
/** 从模型 ID 列表中提取各 claude 系列的最新版本(按 major.minor 取最高) */
|
|
@@ -70,9 +70,9 @@ async function refreshModelAliases(baseUrl, apiKey) {
|
|
|
70
70
|
? json.data.map((m) => m?.id).filter((x) => typeof x === 'string')
|
|
71
71
|
: [];
|
|
72
72
|
const aliases = deriveAliasesFromModelIds(ids);
|
|
73
|
-
if (Object.keys(aliases).length > 0) {
|
|
74
|
-
modelAliasCache.set(baseUrl, { aliases, fetchedAt: Date.now() });
|
|
75
|
-
logger.info(`[AgentRunner] Refreshed
|
|
73
|
+
if (ids.length > 0 || Object.keys(aliases).length > 0) {
|
|
74
|
+
modelAliasCache.set(baseUrl, { aliases, ids, fetchedAt: Date.now() });
|
|
75
|
+
logger.info(`[AgentRunner] Refreshed models from ${url}: ${ids.length} ids, aliases ${JSON.stringify(aliases)}`);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
catch {
|
|
@@ -97,6 +97,27 @@ function resolveModelAlias(model, baseUrl) {
|
|
|
97
97
|
// 回退静态表
|
|
98
98
|
return STATIC_MODEL_ALIASES[model] || model;
|
|
99
99
|
}
|
|
100
|
+
/** 支持 1M 上下文窗口的模型 ID 前缀(SDK 通过 `[1m]` 后缀启用)。 */
|
|
101
|
+
const ONE_M_CONTEXT_PREFIXES = ['claude-opus-4-8', 'claude-sonnet-4-6'];
|
|
102
|
+
/**
|
|
103
|
+
* 为支持 1M 上下文的模型追加 `[1m]` 后缀——仅在交给 SDK query() 时调用。
|
|
104
|
+
* 目录与校验层始终使用不带后缀的基础 ID,避免与网关 /models 返回值(无 `[1m]`)冲突。
|
|
105
|
+
*/
|
|
106
|
+
function applyContextWindow(modelId) {
|
|
107
|
+
if (/\[1m\]$/.test(modelId))
|
|
108
|
+
return modelId; // 已带后缀
|
|
109
|
+
if (ONE_M_CONTEXT_PREFIXES.some(p => modelId === p))
|
|
110
|
+
return `${modelId}[1m]`;
|
|
111
|
+
return modelId;
|
|
112
|
+
}
|
|
113
|
+
/** 根据 SDK model 串(含 [1m] 后缀)返回合适的 autoCompactWindow 值。 */
|
|
114
|
+
function contextWindowFor(sdkModel) {
|
|
115
|
+
return /\[1m\]$/.test(sdkModel) ? 900000 : 200000;
|
|
116
|
+
}
|
|
117
|
+
/** 解析别名 + 追加 1M 后缀,得到最终交给 SDK 的 model 串。 */
|
|
118
|
+
function resolveSdkModel(model, baseUrl) {
|
|
119
|
+
return applyContextWindow(resolveModelAlias(model, baseUrl));
|
|
120
|
+
}
|
|
100
121
|
class MessageStream {
|
|
101
122
|
queue = [];
|
|
102
123
|
waiting = null;
|
|
@@ -213,16 +234,23 @@ export class AgentRunner {
|
|
|
213
234
|
getModel() {
|
|
214
235
|
return this.model;
|
|
215
236
|
}
|
|
216
|
-
listModels() {
|
|
217
|
-
// 触发异步刷新(不阻塞)
|
|
218
|
-
if (this.baseUrl)
|
|
219
|
-
refreshModelAliases(this.baseUrl, this.apiKey);
|
|
220
|
-
// 有缓存时返回完整 ID 列表,否则返回短别名
|
|
237
|
+
async listModels() {
|
|
221
238
|
if (this.baseUrl) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
239
|
+
let cached = modelAliasCache.get(this.baseUrl);
|
|
240
|
+
const stale = !cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS);
|
|
241
|
+
// 缓存为空(首次打开)→ 等待刷新;缓存仅过期 → 后台刷新不阻塞
|
|
242
|
+
if (!cached) {
|
|
243
|
+
await refreshModelAliases(this.baseUrl, this.apiKey);
|
|
244
|
+
cached = modelAliasCache.get(this.baseUrl);
|
|
245
|
+
}
|
|
246
|
+
else if (stale) {
|
|
247
|
+
refreshModelAliases(this.baseUrl, this.apiKey);
|
|
248
|
+
}
|
|
249
|
+
// 有缓存时返回网关 /models 的全量原始 ID
|
|
250
|
+
if (cached && cached.ids.length > 0)
|
|
251
|
+
return cached.ids;
|
|
225
252
|
}
|
|
253
|
+
// 无 baseUrl / 刷新超时或失败 → 回退短别名
|
|
226
254
|
return Object.values(STATIC_MODEL_ALIASES);
|
|
227
255
|
}
|
|
228
256
|
/** 将短别名解析为当前代理实际使用的完整 model ID(仅用于展示,不改变持久化值) */
|
|
@@ -413,11 +441,8 @@ export class AgentRunner {
|
|
|
413
441
|
}
|
|
414
442
|
continue;
|
|
415
443
|
}
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
permCtx?.interactionRouter?.unmarkWaiting(sessionId);
|
|
419
|
-
waitMarked = false;
|
|
420
|
-
}
|
|
444
|
+
// 等待用户交互:先 register 接管计数,再 unmark 占位,消除空窗期
|
|
445
|
+
// (unmark 必须在 register 之后,否则计数短暂降为 0 触发 onWaitEnd→resume,idle 时钟被重置)
|
|
421
446
|
const answer = await new Promise((resolve) => {
|
|
422
447
|
permCtx?.interactionRouter?.register(requestId, sessionId, (action, values) => {
|
|
423
448
|
if (action === 'cancel') {
|
|
@@ -442,6 +467,11 @@ export class AgentRunner {
|
|
|
442
467
|
resolve(action);
|
|
443
468
|
}
|
|
444
469
|
});
|
|
470
|
+
// register 已接管计数(计数 +1),现在才能安全释放 markWaiting 占位(计数 -1),避免空窗
|
|
471
|
+
if (waitMarked) {
|
|
472
|
+
permCtx?.interactionRouter?.unmarkWaiting(sessionId);
|
|
473
|
+
waitMarked = false;
|
|
474
|
+
}
|
|
445
475
|
});
|
|
446
476
|
if (answer === null) {
|
|
447
477
|
const firstLabel = q.options[0]?.label || '';
|
|
@@ -656,7 +686,7 @@ export class AgentRunner {
|
|
|
656
686
|
* SDK 原始事件 → 标准 AgentEvent 转换
|
|
657
687
|
* 所有 SDK 特有的事件类型引用封装在此方法内
|
|
658
688
|
*/
|
|
659
|
-
async *transformStream(sdkStream, sessionId) {
|
|
689
|
+
async *transformStream(sdkStream, sessionId, callModel, callEffort, sdkModel) {
|
|
660
690
|
let lastSessionId;
|
|
661
691
|
// tool_use_id → tool_name 映射,用于从 SDKUserMessage 的 tool_result 块中还原工具名
|
|
662
692
|
const toolUseNames = new Map();
|
|
@@ -760,6 +790,19 @@ export class AgentRunner {
|
|
|
760
790
|
const cleanResult = typeof event.result === 'string'
|
|
761
791
|
? event.result.replace(/<thinking>[\s\S]*?<\/thinking>\s*/g, '').trim()
|
|
762
792
|
: event.result;
|
|
793
|
+
// 从 usage 三项求和得到当前上下文占用(与 claude-hud getTotalTokens 相同算法)
|
|
794
|
+
const u = event.usage;
|
|
795
|
+
const totalTokens = u
|
|
796
|
+
? (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0)
|
|
797
|
+
: 0;
|
|
798
|
+
const maxTokens = sdkModel ? contextWindowFor(sdkModel) : 200000;
|
|
799
|
+
const contextUsage = totalTokens > 0 ? {
|
|
800
|
+
totalTokens,
|
|
801
|
+
maxTokens,
|
|
802
|
+
percentage: Math.round((totalTokens / maxTokens) * 100),
|
|
803
|
+
model: callModel ?? this.model,
|
|
804
|
+
effort: callEffort ?? this.effort,
|
|
805
|
+
} : undefined;
|
|
763
806
|
yield {
|
|
764
807
|
type: 'complete',
|
|
765
808
|
result: cleanResult,
|
|
@@ -772,7 +815,8 @@ export class AgentRunner {
|
|
|
772
815
|
terminalReason: event.terminal_reason,
|
|
773
816
|
sessionTitle: event.session_title,
|
|
774
817
|
numTurns: event.num_turns,
|
|
775
|
-
|
|
818
|
+
tokenUsage: event.usage,
|
|
819
|
+
contextUsage,
|
|
776
820
|
};
|
|
777
821
|
// result 是 SDK 流的终结事件,不再等待后续(防止 interrupt 后流不关闭导致挂起)
|
|
778
822
|
return;
|
|
@@ -980,12 +1024,13 @@ export class AgentRunner {
|
|
|
980
1024
|
else {
|
|
981
1025
|
logger.info(`[AgentRunner] systemPromptAppend: none`);
|
|
982
1026
|
}
|
|
1027
|
+
const sdkModel = resolveSdkModel(callModel, this.baseUrl);
|
|
983
1028
|
const commonOptions = {
|
|
984
1029
|
cwd: projectPath,
|
|
985
|
-
model:
|
|
1030
|
+
model: sdkModel,
|
|
986
1031
|
...(callEffort ? { effort: callEffort } : {}),
|
|
987
1032
|
...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
|
|
988
|
-
autoCompactWindow:
|
|
1033
|
+
autoCompactWindow: contextWindowFor(sdkModel),
|
|
989
1034
|
advisorModel: 'haiku',
|
|
990
1035
|
canUseTool: canUseToolCallback,
|
|
991
1036
|
permissionMode: sdkPermissionMode,
|
|
@@ -1110,7 +1155,7 @@ export class AgentRunner {
|
|
|
1110
1155
|
}
|
|
1111
1156
|
let sdkStream;
|
|
1112
1157
|
if (images && images.length > 0) {
|
|
1113
|
-
logger.
|
|
1158
|
+
logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
|
|
1114
1159
|
logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
|
|
1115
1160
|
const stream = new MessageStream();
|
|
1116
1161
|
stream.push(prompt, images);
|
|
@@ -1126,7 +1171,7 @@ export class AgentRunner {
|
|
|
1126
1171
|
this.interruptFns.set(sessionId, () => sdkStream.interrupt());
|
|
1127
1172
|
}
|
|
1128
1173
|
// 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
|
|
1129
|
-
return this.transformStream(sdkStream, sessionId);
|
|
1174
|
+
return this.transformStream(sdkStream, sessionId, callModel, callEffort, sdkModel);
|
|
1130
1175
|
}
|
|
1131
1176
|
async interrupt(sessionId) {
|
|
1132
1177
|
const fn = this.interruptFns.get(sessionId);
|
|
@@ -1165,7 +1210,7 @@ export class AgentRunner {
|
|
|
1165
1210
|
prompt,
|
|
1166
1211
|
options: {
|
|
1167
1212
|
cwd: projectPath,
|
|
1168
|
-
model:
|
|
1213
|
+
model: resolveSdkModel(this.model, this.baseUrl),
|
|
1169
1214
|
resume: agentSessionId,
|
|
1170
1215
|
maxTurns: 1,
|
|
1171
1216
|
permissionMode: this.toSdkPermissionMode(),
|
|
@@ -19,6 +19,9 @@ const PARAM_DESCRIPTIONS = {
|
|
|
19
19
|
peerName: '对端显示名',
|
|
20
20
|
peerRole: '对端角色(owner/admin/guest/anonymous)',
|
|
21
21
|
peerType: '对端类型(human/agent)',
|
|
22
|
+
sameDevice: '对端与本端同一物理设备(E2EE 消息 proximity,仅加密消息有值)',
|
|
23
|
+
sameNetwork: '对端与本端在同一网络内',
|
|
24
|
+
sameEgressIp: '对端与本端共享同一出口 IP',
|
|
22
25
|
groupId: '群组 ID(群聊时)',
|
|
23
26
|
chatType: '聊天类型(private=私聊 / group=群聊 / null=本地开发)',
|
|
24
27
|
channel: '渠道类型(aun/feishu/wechat/dingtalk/qqbot/wecom)',
|
|
@@ -32,12 +35,19 @@ const PARAM_DESCRIPTIONS = {
|
|
|
32
35
|
sessionName: '会话名称',
|
|
33
36
|
sessionKey: '会话路由键(channelType#urlEncode(channelId)#urlEncode(threadId))',
|
|
34
37
|
sessionCreatedAt: '会话创建时间(ISO)',
|
|
38
|
+
timezone: 'IANA 时区名(把 ISO 时间戳转本地时间用,如 Asia/Shanghai)',
|
|
39
|
+
tzOffset: '当前 UTC 偏移(如 +08:00)',
|
|
40
|
+
osInfo: '操作系统及版本(如 Windows 11 Pro (win32 10.0.26200))',
|
|
35
41
|
threadId: '话题 ID(多话题路由时)',
|
|
36
42
|
chatMode: '会话模式(interactive=同步交互 / proactive=主动推送)',
|
|
37
43
|
readonly: '是否只读模式',
|
|
44
|
+
evolclawMode: 'evolclaw 运行模式(dev=源码仓库可直接修改 | install=全局安装包只读)',
|
|
38
45
|
baseAgent: 'base agent 规范值(claude/codex/gemini/hermes)',
|
|
39
46
|
baseAgentName: 'base agent 显示名',
|
|
40
|
-
baseAgentModel: 'base agent
|
|
47
|
+
baseAgentModel: 'base agent 引擎底座模型(evolclaw 作用域无配置时的兜底)',
|
|
48
|
+
effectiveModel: '当前实际生效模型(关系级 > agent级 > 全局 优先级解析结果)',
|
|
49
|
+
modelFallbackActive: 'evolclaw 配置的模型不可用,当前正在使用降级模型',
|
|
50
|
+
modelFallbackModel: '当前降级使用的 base agent 模型名',
|
|
41
51
|
agentSessionId: 'base agent 会话 ID',
|
|
42
52
|
};
|
|
43
53
|
function buildPathMappings(vars) {
|
|
@@ -355,9 +365,10 @@ function isTruthy(val) {
|
|
|
355
365
|
// CHUNK_CONTINUE_6
|
|
356
366
|
// ── Template rendering ──
|
|
357
367
|
function resolveConditions(template, vars) {
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
|
|
368
|
+
// 只匹配**最内层** {{?...}}...{{/}} 块:body 内不允许再出现 {{? ,
|
|
369
|
+
// 否则非贪婪 ([^]*?) 会匹配到嵌套内层的 {{/}},导致外层提前闭合、残留多余 {{/}}。
|
|
370
|
+
// 逐字符负向前瞻 (?!\{\{\?) 排除嵌套起始,配合 do/while 由内向外逐层消解。
|
|
371
|
+
const inner = /\{\{\?(\w+)(?:(!=|=)([^}]*))?\}\}((?:(?!\{\{\?)[^])*?)\{\{\/\}\}/;
|
|
361
372
|
let result = template;
|
|
362
373
|
let prev;
|
|
363
374
|
do {
|
package/dist/aun/aid/agentmd.js
CHANGED
|
@@ -110,6 +110,10 @@ export async function agentmdPut(content, opts) {
|
|
|
110
110
|
* Check if agent.md is up-to-date (30-day TTL), fetch if changed.
|
|
111
111
|
* Returns changed=true + content when a new version was downloaded.
|
|
112
112
|
*
|
|
113
|
+
* verification 透传 SDK 的验签结果:
|
|
114
|
+
* - downloaded(changed=true)时为 SDK downloadAgentMd 的 verification
|
|
115
|
+
* - 命中本地缓存或网络失败 fallback 时为 undefined(调用方需自行离线验签或视为未验证)
|
|
116
|
+
*
|
|
113
117
|
* Note: store.checkAgentMd tracks freshness via the store's in-memory cache,
|
|
114
118
|
* so a freshly-built store reports local_found=false and will fetch.
|
|
115
119
|
*/
|
|
@@ -120,16 +124,19 @@ export async function agentmdSync(aid, opts) {
|
|
|
120
124
|
const localPath = agentMdPath(aid);
|
|
121
125
|
try {
|
|
122
126
|
const check = await store.checkAgentMd(aid, 30);
|
|
123
|
-
// In sync (cache fresh) — return local file content
|
|
127
|
+
// In sync (cache fresh) — return local file content with cached verification status.
|
|
124
128
|
if (check.ok && !check.data.needs_update && check.data.local_found) {
|
|
125
129
|
const content = fs.existsSync(localPath) ? fs.readFileSync(localPath, 'utf-8') : undefined;
|
|
126
|
-
|
|
130
|
+
const verification = check.data.verify_status
|
|
131
|
+
? normalizeVerification({ status: check.data.verify_status, reason: check.data.verify_error || undefined })
|
|
132
|
+
: undefined;
|
|
133
|
+
return { changed: false, content, verification };
|
|
127
134
|
}
|
|
128
135
|
// Needs update (or check failed) — fetch fresh content.
|
|
129
136
|
// SDK's downloadAgentMd persists to disk internally (AgentMdManager.saveRecord → writeContent).
|
|
130
137
|
const fetched = await store.downloadAgentMd(aid);
|
|
131
138
|
if (fetched.ok) {
|
|
132
|
-
return { changed: true, content: fetched.data.content };
|
|
139
|
+
return { changed: true, content: fetched.data.content, verification: normalizeVerification(fetched.data.verification) };
|
|
133
140
|
}
|
|
134
141
|
// Fetch failed (network) — fall back to local file if present.
|
|
135
142
|
const content = fs.existsSync(localPath) ? fs.readFileSync(localPath, 'utf-8') : undefined;
|
package/dist/aun/msg/group.js
CHANGED
|
@@ -70,11 +70,11 @@ export async function groupPull(args) {
|
|
|
70
70
|
export async function groupAck(args) {
|
|
71
71
|
const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
|
|
72
72
|
try {
|
|
73
|
-
const result = await conn.call('group.ack', { group_id: args.groupId,
|
|
73
|
+
const result = await conn.call('group.ack', { group_id: args.groupId, msg_seq: args.seq });
|
|
74
74
|
return {
|
|
75
75
|
ok: true,
|
|
76
76
|
group_id: result?.group_id ?? args.groupId,
|
|
77
|
-
ack_seq: result?.ack_seq ?? args.seq,
|
|
77
|
+
ack_seq: result?.cursor ?? result?.ack_seq ?? args.seq,
|
|
78
78
|
latest_message_seq: result?.latest_message_seq,
|
|
79
79
|
};
|
|
80
80
|
}
|
package/dist/channels/aun.js
CHANGED
|
@@ -576,7 +576,7 @@ export class AUNChannel {
|
|
|
576
576
|
const store = await getAidStore({
|
|
577
577
|
slotId: SLOT.daemon,
|
|
578
578
|
aunPath,
|
|
579
|
-
debug: this.config.aunSdkLog ??
|
|
579
|
+
debug: this.config.aunSdkLog ?? false,
|
|
580
580
|
});
|
|
581
581
|
this.store = store;
|
|
582
582
|
const client = await loadClient(store, aidName);
|
|
@@ -790,18 +790,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
790
790
|
|
|
791
791
|
📋 **日常使用方法**:
|
|
792
792
|
|
|
793
|
-
1.
|
|
794
|
-
2.
|
|
795
|
-
3.
|
|
796
|
-
4. **查看状态**:发送 \`/status\` 查看当前会话状态
|
|
797
|
-
5. **会话管理**:发送 \`/session\` 查看和切换会话
|
|
793
|
+
1. **查看帮助**:发送 \`/help\` 查看所有可用命令
|
|
794
|
+
2. **查看状态**:发送 \`/status\` 查看当前会话状态
|
|
795
|
+
3. **会话管理**:发送 \`/session\` 查看和切换会话
|
|
798
796
|
|
|
799
797
|
💡 **提示**:
|
|
800
798
|
- 直接发送消息即可与 Claude/Codex 对话
|
|
801
|
-
-
|
|
799
|
+
- 支持多会话管理,每个会话独立上下文
|
|
802
800
|
- 所有命令以 \`/\` 开头
|
|
803
801
|
|
|
804
|
-
|
|
802
|
+
现在就可以开始工作了!`;
|
|
805
803
|
// First contact with Owner races against Owner's async cert fetch from
|
|
806
804
|
// gateway PKI; a 3s pause lets the cert propagate. persist_required asks
|
|
807
805
|
// the gateway to durably store the message so Owner can recover it via
|
|
@@ -818,6 +816,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
818
816
|
persist_required: true,
|
|
819
817
|
});
|
|
820
818
|
logger.info(`${this.logPrefix()} Welcome message sent to owner: ${owner}`);
|
|
819
|
+
// Send binding credential for Evol App to persist locally
|
|
820
|
+
await this.sendBindingCredential(owner, agentDisplayName, agentConfig.active_baseagent || 'claude').catch(e => logger.warn(`${this.logPrefix()} Binding credential failed: ${e}`));
|
|
821
821
|
// Mark agent as initialized in config.json (replaces old agent.md frontmatter flag)
|
|
822
822
|
try {
|
|
823
823
|
const fresh = loadAgent(aidName);
|
|
@@ -835,7 +835,57 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
835
835
|
logger.warn(`${this.logPrefix()} Failed to send welcome message: ${e}`);
|
|
836
836
|
}
|
|
837
837
|
}
|
|
838
|
+
async sendBindingCredential(owner, name, baseagent) {
|
|
839
|
+
if (!this.client)
|
|
840
|
+
return;
|
|
841
|
+
await this.callAndTrace('message.send', {
|
|
842
|
+
to: owner,
|
|
843
|
+
payload: { type: 'binding', aid: this.config.aid, name, owner, baseagent },
|
|
844
|
+
encrypt: true,
|
|
845
|
+
persist_required: true,
|
|
846
|
+
});
|
|
847
|
+
logger.info(`${this.logPrefix()} Binding credential sent to owner: ${owner}`);
|
|
848
|
+
}
|
|
838
849
|
// ── Event handlers ──────────────────────────────────────────
|
|
850
|
+
/**
|
|
851
|
+
* 判断附件是否为图片,返回 MIME 类型(非图片返回空)。
|
|
852
|
+
* 多重检测:附件元数据字段 → 文件名后缀 → 文件 magic bytes。
|
|
853
|
+
*/
|
|
854
|
+
detectImageMime(att, filePath) {
|
|
855
|
+
const extToMime = {
|
|
856
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
857
|
+
'.gif': 'image/gif', '.webp': 'image/webp',
|
|
858
|
+
};
|
|
859
|
+
// 1. 附件元数据字段(content_type / mime_type / mimeType)
|
|
860
|
+
const metaCt = (att?.content_type || att?.mime_type || att?.mimeType || '');
|
|
861
|
+
if (typeof metaCt === 'string' && metaCt.startsWith('image/'))
|
|
862
|
+
return metaCt;
|
|
863
|
+
// 2. 文件名后缀
|
|
864
|
+
const name = (att?.filename || att?.object_key || filePath || '').toLowerCase();
|
|
865
|
+
for (const [ext, mime] of Object.entries(extToMime)) {
|
|
866
|
+
if (name.endsWith(ext))
|
|
867
|
+
return mime;
|
|
868
|
+
}
|
|
869
|
+
// 3. magic bytes
|
|
870
|
+
try {
|
|
871
|
+
const { openSync, readSync, closeSync } = require('node:fs');
|
|
872
|
+
const fd = openSync(filePath, 'r');
|
|
873
|
+
const head = Buffer.alloc(12);
|
|
874
|
+
readSync(fd, head, 0, 12, 0);
|
|
875
|
+
closeSync(fd);
|
|
876
|
+
if (head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4e && head[3] === 0x47)
|
|
877
|
+
return 'image/png';
|
|
878
|
+
if (head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff)
|
|
879
|
+
return 'image/jpeg';
|
|
880
|
+
if (head[0] === 0x47 && head[1] === 0x49 && head[2] === 0x46)
|
|
881
|
+
return 'image/gif';
|
|
882
|
+
if (head[0] === 0x52 && head[1] === 0x49 && head[2] === 0x46 && head[3] === 0x46 &&
|
|
883
|
+
head[8] === 0x57 && head[9] === 0x45 && head[10] === 0x42 && head[11] === 0x50)
|
|
884
|
+
return 'image/webp';
|
|
885
|
+
}
|
|
886
|
+
catch { /* not readable, skip */ }
|
|
887
|
+
return '';
|
|
888
|
+
}
|
|
839
889
|
async downloadAttachment(att, channelId) {
|
|
840
890
|
const ownerAid = att.owner_aid || this._aid || '';
|
|
841
891
|
const objectKey = att.object_key;
|
|
@@ -844,7 +894,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
844
894
|
return null;
|
|
845
895
|
}
|
|
846
896
|
const filename = att.filename || objectKey.split('/').pop() || 'unknown';
|
|
847
|
-
|
|
897
|
+
// 安全:始终通过受信任的 ticket 路径获取下载 URL。
|
|
898
|
+
// 不信任 att.url(来自对端消息 payload,可被构造为内网/元数据地址,SSRF)。
|
|
899
|
+
let downloadUrl = '';
|
|
848
900
|
try {
|
|
849
901
|
const ticket = await this.callAndTrace('storage.create_download_ticket', {
|
|
850
902
|
owner_aid: ownerAid,
|
|
@@ -924,12 +976,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
924
976
|
// Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
|
|
925
977
|
const rawAttachments = this.collectAllAttachments(payload);
|
|
926
978
|
let finalText = text;
|
|
979
|
+
const inboundImages = [];
|
|
927
980
|
if (rawAttachments.length > 0 && this.client) {
|
|
928
981
|
const fileParts = [];
|
|
929
982
|
for (const att of rawAttachments) {
|
|
930
983
|
const filePath = await this.downloadAttachment(att, fromAid);
|
|
931
984
|
if (filePath) {
|
|
932
985
|
const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
|
|
986
|
+
const mime = this.detectImageMime(att, filePath);
|
|
987
|
+
if (mime) {
|
|
988
|
+
try {
|
|
989
|
+
const { readFileSync } = await import('node:fs');
|
|
990
|
+
inboundImages.push({ data: readFileSync(filePath).toString('base64'), mimeType: mime });
|
|
991
|
+
}
|
|
992
|
+
catch { /* fallback to file path */ }
|
|
993
|
+
}
|
|
933
994
|
fileParts.push(`[文件: ${name} → ${filePath}]`);
|
|
934
995
|
}
|
|
935
996
|
}
|
|
@@ -938,9 +999,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
938
999
|
if (text)
|
|
939
1000
|
parts.push(text);
|
|
940
1001
|
parts.push(...fileParts);
|
|
941
|
-
|
|
1002
|
+
if (inboundImages.length === 0)
|
|
1003
|
+
parts.push('请使用 Read 工具读取文件内容。');
|
|
942
1004
|
finalText = parts.join('\n\n');
|
|
943
1005
|
}
|
|
1006
|
+
logger.info(`${this.logPrefix()} [img-debug] private attachments=${rawAttachments.length} images=${inboundImages.length}`);
|
|
944
1007
|
}
|
|
945
1008
|
// 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
|
|
946
1009
|
// device_id 仅 SDK 内部多实例去重用,evolclaw session 层面跨端共享会话
|
|
@@ -993,7 +1056,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
993
1056
|
mentions,
|
|
994
1057
|
peerName: displayName || undefined,
|
|
995
1058
|
peerType: peerIdentity.type,
|
|
1059
|
+
sameDevice: msg.same_device === true || undefined,
|
|
1060
|
+
sameNetwork: msg.same_network === true || undefined,
|
|
1061
|
+
sameEgressIp: msg.same_egress_ip === true || undefined,
|
|
996
1062
|
replyContext,
|
|
1063
|
+
images: inboundImages.length > 0 ? inboundImages : undefined,
|
|
997
1064
|
});
|
|
998
1065
|
}
|
|
999
1066
|
async handleIncomingGroupMessage(data) {
|
|
@@ -1144,12 +1211,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1144
1211
|
: mentionedSelf && this._aid ? [this._aid] : [];
|
|
1145
1212
|
// Process attachments
|
|
1146
1213
|
let finalText = strippedText;
|
|
1214
|
+
const inboundImages = [];
|
|
1147
1215
|
if (hasAttachments && this.client) {
|
|
1148
1216
|
const fileParts = [];
|
|
1149
1217
|
for (const att of rawAttachments) {
|
|
1150
1218
|
const filePath = await this.downloadAttachment(att, groupId);
|
|
1151
1219
|
if (filePath) {
|
|
1152
1220
|
const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
|
|
1221
|
+
const mime = this.detectImageMime(att, filePath);
|
|
1222
|
+
if (mime) {
|
|
1223
|
+
try {
|
|
1224
|
+
const { readFileSync } = await import('node:fs');
|
|
1225
|
+
inboundImages.push({ data: readFileSync(filePath).toString('base64'), mimeType: mime });
|
|
1226
|
+
}
|
|
1227
|
+
catch { /* fallback to file path */ }
|
|
1228
|
+
}
|
|
1153
1229
|
fileParts.push(`[文件: ${name} → ${filePath}]`);
|
|
1154
1230
|
}
|
|
1155
1231
|
}
|
|
@@ -1158,7 +1234,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1158
1234
|
if (strippedText)
|
|
1159
1235
|
parts.push(strippedText);
|
|
1160
1236
|
parts.push(...fileParts);
|
|
1161
|
-
|
|
1237
|
+
if (inboundImages.length === 0)
|
|
1238
|
+
parts.push('请使用 Read 工具读取文件内容。');
|
|
1162
1239
|
finalText = parts.join('\n\n');
|
|
1163
1240
|
}
|
|
1164
1241
|
}
|
|
@@ -1191,6 +1268,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1191
1268
|
userId: senderAid,
|
|
1192
1269
|
peerName: displayName || undefined,
|
|
1193
1270
|
peerType: peerIdentity.type,
|
|
1271
|
+
sameDevice: msg.same_device === true || undefined,
|
|
1272
|
+
sameNetwork: msg.same_network === true || undefined,
|
|
1273
|
+
sameEgressIp: msg.same_egress_ip === true || undefined,
|
|
1194
1274
|
text: finalText,
|
|
1195
1275
|
chatType: 'group',
|
|
1196
1276
|
messageId,
|
|
@@ -1198,6 +1278,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1198
1278
|
threadId,
|
|
1199
1279
|
mentions,
|
|
1200
1280
|
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
|
|
1281
|
+
images: inboundImages.length > 0 ? inboundImages : undefined,
|
|
1201
1282
|
});
|
|
1202
1283
|
}
|
|
1203
1284
|
dispatchMessage(event) {
|
|
@@ -1260,10 +1341,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1260
1341
|
peerId: event.userId || event.channelId || '',
|
|
1261
1342
|
peerName: event.peerName,
|
|
1262
1343
|
peerType: event.peerType,
|
|
1344
|
+
sameDevice: event.sameDevice,
|
|
1345
|
+
sameNetwork: event.sameNetwork,
|
|
1346
|
+
sameEgressIp: event.sameEgressIp,
|
|
1263
1347
|
messageId: event.messageId,
|
|
1264
1348
|
threadId: event.threadId,
|
|
1265
1349
|
mentions: mentionObjects,
|
|
1266
1350
|
replyContext,
|
|
1351
|
+
images: event.images,
|
|
1267
1352
|
}).catch(err => {
|
|
1268
1353
|
logger.error(`${this.logPrefix()} Message handler error:`, err);
|
|
1269
1354
|
});
|
|
@@ -2444,7 +2529,7 @@ export class AUNChannelPlugin {
|
|
|
2444
2529
|
const adapter = {
|
|
2445
2530
|
channelName: inst.name,
|
|
2446
2531
|
channelKey: inst.name, // channelName 实际上就是 channelKey
|
|
2447
|
-
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true },
|
|
2532
|
+
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true },
|
|
2448
2533
|
send: async (envelope, payload) => {
|
|
2449
2534
|
const ctx = envelope.replyContext;
|
|
2450
2535
|
const channelId = envelope.channelId;
|
|
@@ -2637,6 +2722,7 @@ export class AUNChannelPlugin {
|
|
|
2637
2722
|
threadId: opts.threadId,
|
|
2638
2723
|
replyContext: opts.replyContext,
|
|
2639
2724
|
source: opts.source,
|
|
2725
|
+
images: opts.images,
|
|
2640
2726
|
});
|
|
2641
2727
|
}), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
|
|
2642
2728
|
},
|
|
@@ -448,7 +448,7 @@ export class DingtalkChannelPlugin {
|
|
|
448
448
|
const adapter = {
|
|
449
449
|
channelName: inst.name,
|
|
450
450
|
channelKey: inst.name,
|
|
451
|
-
capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
|
|
451
|
+
capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false, thread: false },
|
|
452
452
|
send: async (envelope, payload) => {
|
|
453
453
|
const ctx = envelope.replyContext;
|
|
454
454
|
const channelId = envelope.channelId;
|