evolcore 0.0.32 → 0.0.33

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 (150) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/bin/install-codex-managed-hooks.mjs +124 -37
  3. package/dist/agents/claude-runner.js +29 -2
  4. package/dist/agents/codex/app-server-client.js +3 -0
  5. package/dist/agents/codex/diagnostics.js +76 -44
  6. package/dist/agents/codex-runner.js +23 -19
  7. package/dist/agents/ecagent-runner.js +5 -13
  8. package/dist/aun/group/operations.js +0 -1
  9. package/dist/aun/msg/history.js +1 -0
  10. package/dist/aun/msg/p2p.js +0 -1
  11. package/dist/aun/outbox.js +50 -1
  12. package/dist/channels/aun.js +28 -2
  13. package/dist/channels/daemon.js +257 -2
  14. package/dist/cli/agent-command.js +58 -24
  15. package/dist/cli/agent.js +1 -1
  16. package/dist/cli/aun-commands.js +365 -29
  17. package/dist/cli/contact.js +8 -2
  18. package/dist/cli/daemon-commands.js +81 -12
  19. package/dist/cli/debug-mode.js +154 -0
  20. package/dist/cli/handoff-command.js +3 -0
  21. package/dist/cli/index.js +81 -64
  22. package/dist/cli/init.js +2 -2
  23. package/dist/cli/managed-command-guard.js +5 -0
  24. package/dist/cli/memory-command.js +8 -1
  25. package/dist/cli/queue-command.js +9 -4
  26. package/dist/cli/session-command.js +194 -0
  27. package/dist/cli/skill-command.js +78 -2
  28. package/dist/cli/task-context.js +1 -0
  29. package/dist/cli/thread-command.js +620 -0
  30. package/dist/cli/trigger-command.js +4 -2
  31. package/dist/config/builtin-roles.js +1 -0
  32. package/dist/config/config-field-policy.js +2 -3
  33. package/dist/config/config-manager.js +7 -2
  34. package/dist/config/resolved-config-op.js +3 -0
  35. package/dist/config-store.js +15 -0
  36. package/dist/core/auth/agent-delegation.js +25 -1
  37. package/dist/core/auth/debug-context.js +139 -0
  38. package/dist/core/auth/operation-authorizer.js +24 -1
  39. package/dist/core/auth/operation-catalog.js +33 -0
  40. package/dist/core/background-session/session-cleanup.js +212 -0
  41. package/dist/core/background-session/types.js +10 -0
  42. package/dist/core/bootstrap-messages.js +1 -1
  43. package/dist/core/bootstrap-service.js +6 -3
  44. package/dist/core/command/command-handler.js +60 -24
  45. package/dist/core/command/memory-slash.js +6 -2
  46. package/dist/core/command/menu-catalog.js +48 -1
  47. package/dist/core/command/menu-handler.js +92 -16
  48. package/dist/core/command/menu-protocol.js +24 -1
  49. package/dist/core/command/skill-slash.js +322 -0
  50. package/dist/core/command/slash-gate.js +6 -4
  51. package/dist/core/command/slash-handler.js +193 -13
  52. package/dist/core/command/topic-slash.js +528 -0
  53. package/dist/core/evolagent-registry.js +20 -5
  54. package/dist/core/handoff/runtime.js +5 -11
  55. package/dist/core/handoff/store.js +5 -0
  56. package/dist/core/ipc-registration.js +139 -2
  57. package/dist/core/memory/consolidation-context.js +49 -16
  58. package/dist/core/memory/consolidation-ops-records.js +35 -0
  59. package/dist/core/memory/consolidation-queue-format.js +21 -0
  60. package/dist/core/memory/consolidation.js +174 -180
  61. package/dist/core/message/aun-ipc-sender.js +161 -6
  62. package/dist/core/message/im-renderer.js +149 -6
  63. package/dist/core/message/message-bridge.js +13 -3
  64. package/dist/core/message/message-log.js +1 -0
  65. package/dist/core/message/peer-mode.js +6 -1
  66. package/dist/core/message/response-engine.js +349 -67
  67. package/dist/core/message/subtask-tool-projection.js +140 -0
  68. package/dist/core/model/catalog-service.js +17 -12
  69. package/dist/core/permission/ec-command-parser.js +126 -7
  70. package/dist/core/session/session-idle-monitor.js +249 -22
  71. package/dist/core/session/session-manager.js +54 -9
  72. package/dist/core/skill/command-parser.js +13 -0
  73. package/dist/core/skill/index-generator.js +4 -1
  74. package/dist/core/skill/startup.js +41 -0
  75. package/dist/core/skill/switch-resolver.js +55 -29
  76. package/dist/core/thread/command-parser.js +125 -0
  77. package/dist/core/thread/index.js +1 -0
  78. package/dist/core/thread/ipc-operation.js +95 -0
  79. package/dist/core/thread/thread-context-storage.js +131 -0
  80. package/dist/core/thread/thread-operation.js +258 -0
  81. package/dist/core/thread/thread-route.js +33 -0
  82. package/dist/core/thread/thread-service.js +777 -0
  83. package/dist/core/thread/types.js +1 -0
  84. package/dist/eck/message-renderer.js +35 -0
  85. package/dist/index.js +8 -5
  86. package/dist/ipc.js +89 -10
  87. package/dist/paths.js +23 -0
  88. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  89. package/dist/trigger/authorization.js +57 -28
  90. package/dist/trigger/feedback.js +7 -1
  91. package/dist/trigger/parser.js +63 -1
  92. package/dist/trigger/validation.js +14 -0
  93. package/dist/utils/error-utils.js +22 -1
  94. package/kits/docs/context-assembly.md +1 -0
  95. package/kits/docs/evolcore/INDEX.md +1 -0
  96. package/kits/docs/evolcore/agent-create.md +39 -28
  97. package/kits/docs/evolcore/agent.md +19 -4
  98. package/kits/docs/evolcore/ec-capabilities.md +4 -0
  99. package/kits/docs/evolcore/group-collaboration.md +3 -5
  100. package/kits/docs/evolcore/group.md +3 -4
  101. package/kits/docs/evolcore/memory-consolidation.md +1 -1
  102. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  103. package/kits/docs/evolcore/menu-protocol.md +7 -0
  104. package/kits/docs/evolcore/msg.md +5 -8
  105. package/kits/docs/evolcore/thread-chain.md +267 -0
  106. package/kits/docs/prompt-loading-architecture.md +1 -0
  107. package/kits/eck_manifest.json +61 -26
  108. package/kits/eck_message_manifest.json +169 -10
  109. package/kits/rules/05-venue.md +1 -1
  110. package/kits/rules/07-memory-and-thread.md +202 -0
  111. package/kits/schemas/relation-config.schema.9.json +18 -3
  112. package/kits/schemas/role-config.schema.1.json +6 -6
  113. package/kits/schemas/single-session.schema.1.json +6 -6
  114. package/kits/schemas/single-session.schema.2.json +6 -6
  115. package/kits/schemas/single-session.schema.3.json +6 -6
  116. package/kits/skills/.switches.json +12 -1
  117. package/kits/skills/ec-group/SKILL.md +234 -0
  118. package/kits/skills/ec-skill/SKILL.md +10 -1
  119. package/kits/skills/ec-skill/scripts/cli.mjs +15 -1
  120. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  121. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  122. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  123. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  124. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  125. package/kits/skills/thread-ops/SKILL.md +492 -0
  126. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  127. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  128. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  129. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  130. package/kits/templates/bootstrap-welcome.md +7 -7
  131. package/kits/templates/memory-consolidation/forget.md +5 -0
  132. package/kits/templates/memory-consolidation/generalize.md +5 -0
  133. package/kits/templates/memory-consolidation/profile.md +5 -0
  134. package/kits/templates/memory-consolidation/promote.md +5 -0
  135. package/kits/templates/message-fragments/item.md +2 -1
  136. package/kits/templates/message-fragments/thread-context.md +6 -0
  137. package/kits/templates/message-fragments/trigger-session.md +2 -0
  138. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  139. package/kits/templates/roles/member.json +2 -0
  140. package/kits/templates/roles/visitor.json +2 -0
  141. package/kits/templates/system-fragments/bootstrap.md +66 -72
  142. package/kits/templates/system-fragments/channel.md +3 -6
  143. package/kits/templates/system-fragments/commands.md +1 -0
  144. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  145. package/kits/templates/system-fragments/reply.md +3 -2
  146. package/kits/templates/system-fragments/session.md +5 -14
  147. package/package.json +3 -1
  148. package/kits/docs/evolcore/group-fs.md +0 -17
  149. package/kits/docs/venues/aun-group.md +0 -45
  150. package/kits/docs/venues/group.md +0 -43
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import crypto from 'crypto';
3
4
  import { agentMdPath, resolvePaths, resolveRoot } from '../paths.js';
4
5
  import { isExplicitGroupId, normalizeGroupAid, sameGroupAid } from '../aun/group/index.js';
5
6
  import { getArgValue, isHelpFlag, wantsHelp } from './help.js';
@@ -11,6 +12,7 @@ import { readBestTaskRuntimeContext } from './task-context.js';
11
12
  import { assertEcSingleLine } from './cli-argv.js';
12
13
  import { canonicalizeEvolcoreCommandArgv } from '../core/permission/ec-command-parser.js';
13
14
  import { ManagedGroupOperationError } from '../aun/msg/managed-operation.js';
15
+ import { parseDuration } from '../trigger/parser.js';
14
16
  // ==================== AID ====================
15
17
  function resolveAunPath(args) {
16
18
  const idx = args.indexOf('--aun-path');
@@ -1307,6 +1309,176 @@ Options:
1307
1309
  process.exit(1);
1308
1310
  }
1309
1311
  // ==================== Msg ====================
1312
+ /**
1313
+ * 构造跨话题复查 Trigger 的定义。
1314
+ *
1315
+ * `ec msg send --thread T2 --check-every 5m` 发送成功后自动创建。
1316
+ *
1317
+ * 使用 `interval` 而非 `delay`:周期触发,只有显式
1318
+ * `ec trigger cancel` 或 `ec trigger update --every <新时长>` 才改变其状态。
1319
+ * 一次性 Trigger 会静默消失,无法承担「一直跟进直到显式结束」的语义。
1320
+ */
1321
+ export function buildThreadCheckTrigger(input) {
1322
+ const now = Date.now();
1323
+ return {
1324
+ $schema_version: 3.1,
1325
+ agentAid: input.agentAid,
1326
+ id: input.id,
1327
+ name: input.name,
1328
+ enabled: true,
1329
+ createdAt: now,
1330
+ updatedAt: now,
1331
+ // T10: labels 携带 kind + 业务上下文,渲染层按 kind=thread-check 路由到专属模板
1332
+ labels: {
1333
+ kind: 'thread-check',
1334
+ originThreadId: input.originThreadId,
1335
+ targetThreadId: input.targetThreadId,
1336
+ originPeer: input.originPeer,
1337
+ checkEvery: input.checkEvery,
1338
+ },
1339
+ ...(input.origin ? { origin: input.origin } : {}),
1340
+ // 周期触发:每 everyMs 唤醒一次,直到显式取消/调整
1341
+ source: { type: 'interval', everyMs: input.everyMs },
1342
+ execution: {
1343
+ type: 'trigger_session',
1344
+ prompt: input.prompt,
1345
+ thread: 'by_trigger',
1346
+ onError: 'retry',
1347
+ noopSentinel: '[[NOOP]]',
1348
+ },
1349
+ feedback: {
1350
+ strategy: 'target',
1351
+ target: {
1352
+ channelKey: input.channelKey,
1353
+ channelId: input.targetChannelId,
1354
+ session: 'thread',
1355
+ threadId: input.targetThreadId,
1356
+ chatType: input.feedbackChatType ?? 'private',
1357
+ },
1358
+ },
1359
+ reliability: { concurrency: 'forbid', missedPolicy: 'run_once', retry: { maxAttempts: 0, backoffMs: 30_000 } },
1360
+ };
1361
+ }
1362
+ /**
1363
+ * 解析并校验 `--check-every`。返回 `{ durationMs }` 或错误。
1364
+ * 私聊与群聊两条跨话题发送路径共用,保证错误码一致。
1365
+ */
1366
+ function parseCheckEveryArgs(checkEveryRaw, hasCheckEvery) {
1367
+ if (!hasCheckEvery) {
1368
+ return { code: 'CHECK_EVERY_REQUIRED', error: '跨话题发送必须指定 --check-every <时长>(如 5m / 30m / 2h)' };
1369
+ }
1370
+ if (checkEveryRaw === undefined)
1371
+ return { durationMs: 5 * 60 * 1000 };
1372
+ const parsed = parseDuration(checkEveryRaw);
1373
+ if (parsed === null) {
1374
+ return { code: 'INVALID_DURATION', error: `--check-every 时长格式无效: ${checkEveryRaw}(示例: 5m / 30m / 2h / 1d)` };
1375
+ }
1376
+ return { durationMs: parsed };
1377
+ }
1378
+ /**
1379
+ * 跨话题发送成功后的统一收尾:创建周期复查 Trigger + 写入本端 thread-inbox。
1380
+ *
1381
+ * 私聊(`ec msg send --thread`)与群聊(`ec group send --thread`)共用,
1382
+ * 保证两条路径行为一致(M2 修复前群聊路径完全不做这两件事)。
1383
+ *
1384
+ * 设计见 `docs/thread-chain/thread-context-mechanism.md` §十、`thread-cli-spec-v2.md` §3.4。
1385
+ * 任意一步失败都不影响消息已发出的事实:返回 `triggerId` 为 undefined 时调用方照常输出发送结果。
1386
+ */
1387
+ async function applyCrossThreadSendEffects(input) {
1388
+ const runtime = await readBestTaskRuntimeContext();
1389
+ const originThreadId = runtime?.threadId || 'main';
1390
+ const originPeer = runtime?.channelId || input.targetChannelId;
1391
+ const originSessionId = runtime?.sessionId;
1392
+ if (!originSessionId)
1393
+ return { inboxWritten: false };
1394
+ // Trigger 的 id 与 name 是两个不同的东西(见 src/trigger/validation.ts:727):
1395
+ // id = `trig_<ts>_<8位hex>` —— 唯一的持久化标识,`ec trigger show/update/cancel` 只认它
1396
+ // name = `check-<目标threadId>-from-<来源threadId>-<唯一后缀>` —— 人读标签,不参与定位
1397
+ //
1398
+ // 这里**在 IPC 之前**就把 id 定下来,有三个理由:
1399
+ // 1. prompt 里要写 `ec trigger cancel <id>`,而 prompt 是 definition 的一部分,
1400
+ // create 之前就得拼完,拿不到 daemon 的返回值;
1401
+ // 2. definition 带上 id 后 daemon 直接采用(normalizeV3Phase2 认 `raw.id`),
1402
+ // 落盘 id 与 prompt 中的 id 天然一致,不存在"返回值和 prompt 两套值"的漂移;
1403
+ // 3. D7:此前这里把拼出来的 name 当 id 用,导致 inbox 的 triggerId、`--format json`
1404
+ // 的 trigger_id、prompt 里的参考命令三处全部不可用(`ec trigger cancel` 报 not found)。
1405
+ const triggerSeed = `${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
1406
+ const triggerId = `trig_${triggerSeed}`;
1407
+ // D8:name 必须每次不同。此前用固定的 `check-<T>-from-<origin>`,同一
1408
+ // (目标, 来源) 对第二次发送就撞 `trigger name already exists`(assertUnique 按 name 去重),
1409
+ // 结果消息照发、inbox 照写、Trigger 静默不建 —— 完全无感知。
1410
+ // 每次 `--check-every` 都该有自己的 new Trigger(要复用应走 update),所以追加唯一后缀。
1411
+ const triggerName = `check-${input.targetThreadId}-from-${originThreadId}-${triggerSeed.split('_')[1]}`;
1412
+ const userPromptPart = input.triggerPromptExtra
1413
+ ? input.triggerPromptExtra
1414
+ : `请检查该话题的最新进度,根据情况:继续等待、调整周期(ec trigger update ${triggerId} --every <时长>)或结束复查(ec trigger cancel ${triggerId})。`;
1415
+ const prompt = [
1416
+ `【Trigger 复查】话题 ${input.targetThreadId}(对端 ${input.targetChannelId})`,
1417
+ `这是周期复查:每 ${input.checkEveryLabel} 唤醒一次,直到你显式结束它。`,
1418
+ ``,
1419
+ userPromptPart,
1420
+ ``,
1421
+ `参考命令:`,
1422
+ ` ec thread history ${input.targetChannelId}/${input.targetThreadId} --limit 10`,
1423
+ ` ec thread history ${originPeer}/${originThreadId} --after ${Date.now()}`,
1424
+ ` ec trigger cancel ${triggerId}`,
1425
+ ` ec trigger update ${triggerId} --every <时长>`,
1426
+ `Trigger id:${triggerId}`,
1427
+ `Trigger 名称:${triggerName}`,
1428
+ ].join('\n');
1429
+ let createdTriggerId;
1430
+ try {
1431
+ const { formatChannelKey } = await import('../core/channel-loader.js');
1432
+ const triggerDef = buildThreadCheckTrigger({
1433
+ agentAid: input.selfAid,
1434
+ id: triggerId,
1435
+ name: triggerName,
1436
+ prompt,
1437
+ everyMs: input.durationMs,
1438
+ checkEvery: input.checkEveryLabel,
1439
+ channelKey: formatChannelKey({ type: 'aun', selfAID: input.selfAid, name: 'main' }),
1440
+ targetChannelId: input.targetChannelId,
1441
+ targetThreadId: input.targetThreadId,
1442
+ originThreadId,
1443
+ originPeer,
1444
+ feedbackChatType: input.feedbackChatType,
1445
+ });
1446
+ const triggerRes = await ipcQuery(resolvePaths().socket, {
1447
+ type: 'trigger.create',
1448
+ definition: triggerDef,
1449
+ files: [],
1450
+ enable: true,
1451
+ // daemon 侧 authenticatedTriggerActor 先看 actorSessionId:缺了它会回落到
1452
+ // controlToken 分支(CLI 从不读 control.token),必然抛
1453
+ // "valid local control token is required"。与 trigger-command.ts 的组装保持一致。
1454
+ actorSessionId: originSessionId,
1455
+ delegationToken: readDelegationCredential(),
1456
+ }, 30_000);
1457
+ if (!triggerRes?.ok) {
1458
+ process.stderr.write(`⚠ Trigger 创建失败: ${triggerRes?.error ?? 'unknown'}(inbox 仍会写入)\n`);
1459
+ }
1460
+ else {
1461
+ // 以 daemon 落盘的结果为准(definition 已带 id,正常应与预设值相同;
1462
+ // 若 daemon 因任何原因改写了 id,回填真值而不是本地预设值)。
1463
+ createdTriggerId = triggerRes.trigger?.id ?? triggerId;
1464
+ }
1465
+ }
1466
+ catch (e) {
1467
+ process.stderr.write(`⚠ Trigger 创建异常: ${e?.message ?? e}(inbox 仍会写入)\n`);
1468
+ }
1469
+ const { ThreadService } = await import('../core/thread/index.js');
1470
+ ThreadService.appendInbox(input.selfAid, {
1471
+ messageId: input.messageId,
1472
+ content: input.contentSummary,
1473
+ timestamp: input.timestamp,
1474
+ originThreadId,
1475
+ originPeer,
1476
+ originSessionId,
1477
+ targetSessionId: input.targetSessionId,
1478
+ ...(createdTriggerId ? { triggerId: createdTriggerId } : {}),
1479
+ });
1480
+ return { triggerId: createdTriggerId, inboxWritten: true };
1481
+ }
1310
1482
  export async function cmdMsg(args) {
1311
1483
  args = canonicalizeEvolcoreCommandArgv(['ec', 'msg', ...args]).slice(2);
1312
1484
  const sub = args[0];
@@ -1317,7 +1489,7 @@ export async function cmdMsg(args) {
1317
1489
  console.log(`用法: ec msg <command> <from-aid> [args...] [options]
1318
1490
 
1319
1491
  Commands:
1320
- send <from> <to> <text> --return <none|required> 发送文本(跨会话时 --return 必填)
1492
+ send <from> <to> <text> 发送文本
1321
1493
  send <from> <to> --text-from-file <path> 从文件读取文本内容
1322
1494
  send <from> <to> --file <path> [--as <type>] 发送文件(image|video|voice|file)
1323
1495
  send <from> <to> --link <url> [--title T] 发送链接卡片
@@ -1328,6 +1500,10 @@ Commands:
1328
1500
  online <from> <target-aid> [<target-aid>...] 查询在线状态
1329
1501
  history <from> [<target-aid>|selector] [筛选选项] 查询本地消息历史
1330
1502
 
1503
+ 托管会话简化语法(EVOLCORE_SESSION_ID 已设时):
1504
+ send <text> --thread <id> from/to 从运行时上下文与话题索引自动推断
1505
+ send <text> 发送到当前会话对端
1506
+
1331
1507
  Options:
1332
1508
  --text-from-file <path> 从文件读取文本(UTF-8),用于超长消息或避免 Shell 转义
1333
1509
  --handoff-context <text> 仅托管会话:为目标会话补充本地上下文,不发给对端
@@ -1336,7 +1512,7 @@ Options:
1336
1512
  --format json 输出 JSON 格式
1337
1513
  --encrypt 强制端到端加密(未指定时使用 daemon 默认值)
1338
1514
  --no-encrypt 强制明文发送(优先于 --encrypt)
1339
- --thread <id> 指定话题 ID(用于多话题路由)
1515
+ --thread <id> 指定目标话题 ID(话题必须存在于本端索引;from/to 可省略)
1340
1516
  --session <latest|all|id> 按 EvolCore 逻辑会话过滤历史(默认 latest)
1341
1517
  --before <time> 只返回该时间之前的历史
1342
1518
  --after <time> 只返回该时间之后的历史
@@ -1345,7 +1521,10 @@ Options:
1345
1521
  --channel-name <name> 历史渠道实例名(非 AUN 渠道必须匹配 Agent 配置)
1346
1522
  --channel-key <key> 非 AUN 已落盘渠道的完整实例键(与 --peer-id 成对)
1347
1523
  --peer-id <id> 非 AUN 渠道的原生对端 ID(与 --channel-key 成对)
1348
- --return <none|required> 跨会话发送必填:none=仅补充目标上下文,required=回答回流来源
1524
+ --check-every <duration> 跨话题发送后自动创建**周期**复查 Trigger(如 5m/30m/2h),跨话题时**必填**
1525
+ 每 duration 唤醒一次,直到显式 ec trigger cancel 结束
1526
+ Trigger 名:check-<目标threadId>-from-<来源threadId>-<唯一后缀>
1527
+ 取消/调整用返回的 trigger_id(trig_…),show/update/cancel 只认 id
1349
1528
  --content-type <mime> 显式覆盖 MIME(仅 --file 模式)
1350
1529
  --as <type> 附件类型:image|video|voice|file(仅 --file 模式)
1351
1530
  --text <说明> 附件说明文字(仅 --file 模式)
@@ -1356,12 +1535,12 @@ Options:
1356
1535
  (用于发送恰好等于某 flag 的文本,如 send a b -- --encrypt)
1357
1536
 
1358
1537
  示例:
1359
- ec msg send alice.agentid.pub bob.agentid.pub "hello" --return none
1360
- ec msg send alice.agentid.pub bob.agentid.pub --text-from-file long-message.txt --return none
1361
- ec msg send alice.agentid.pub bob.agentid.pub "讨论项目A" --thread "project-A" --return required
1362
- ec msg send alice.agentid.pub bob.agentid.pub --file ./pic.png --return none
1363
- ec msg send alice.agentid.pub bob.agentid.pub --file ./demo.mp4 --as video --return none
1364
- ec msg send alice.agentid.pub bob.agentid.pub --link https://example.com --title "AUN" --return none
1538
+ ec msg send alice.agentid.pub bob.agentid.pub "hello"
1539
+ ec msg send alice.agentid.pub bob.agentid.pub --text-from-file long-message.txt
1540
+ ec msg send alice.agentid.pub bob.agentid.pub "讨论项目A" --thread "thread-xxx"
1541
+ ec msg send alice.agentid.pub bob.agentid.pub --file ./pic.png
1542
+ ec msg send alice.agentid.pub bob.agentid.pub --file ./demo.mp4 --as video
1543
+ ec msg send alice.agentid.pub bob.agentid.pub --link https://example.com --title "AUN"
1365
1544
  ec msg pull alice.agentid.pub --app my-bot
1366
1545
  ec msg ack alice.agentid.pub 42 --app my-bot
1367
1546
  ec msg recall alice.agentid.pub msg-uuid-1 msg-uuid-2
@@ -1371,13 +1550,16 @@ Options:
1371
1550
  return;
1372
1551
  }
1373
1552
  if (sub === 'history' && wantsHelp(args)) {
1374
- console.log(`用法: ec msg history <self-aid> [<target-aid> | --channel-key <key> --peer-id <id>] [options]
1553
+ console.log(`用法: ec msg history [<self-aid> [<target-aid>]] [--thread <id>] [options]
1554
+
1555
+ 托管会话中 --thread <id> 时 self-aid 和 target-aid 均可省略,从运行时上下文自动推断。
1375
1556
 
1376
1557
  查询指定会话对的本地历史,不调用 message.pull,也不影响收件箱游标。非 AUN selector 按已落盘的 session metadata、完整 channelKey 与原生 peer ID 精确读取,不能传入文件路径。
1377
1558
 
1378
1559
  托管会话限制:普通会话只能使用当前 self AID 和当前 AUN 私聊对端;可信 daemon Trigger 可用精确的 --channel-key/--peer-id selector 读取已落盘的非 AUN 私聊历史。托管任务不支持 --channel、--channel-name、--aun-path 或 --app。
1379
1560
 
1380
1561
  Options:
1562
+ --thread <id> 按话题 ID 过滤(托管会话中 from/to 可省略)
1381
1563
  --session <latest|all|id> 默认 latest;all 查询全部;也可指定会话 ID
1382
1564
  --limit <N> 返回最新 N 条(默认 50,最大 500)
1383
1565
  --before <time> 开区间;epoch 毫秒或 ISO 时间
@@ -1392,9 +1574,41 @@ Options:
1392
1574
  ec msg history alice.agentid.pub bob.agentid.pub --session latest
1393
1575
  ec msg history alice.agentid.pub bob.agentid.pub --session all
1394
1576
  ec msg history alice.agentid.pub bob.agentid.pub --session meta_20260713_1783920000000 --format json
1395
- ec msg history alice.agentid.pub --channel-key feishu#alice.agentid.pub#main --peer-id ou_xxx --session all --format json`);
1577
+ ec msg history alice.agentid.pub --channel-key feishu#alice.agentid.pub#main --peer-id ou_xxx --session all --format json
1578
+ ec msg history --thread thread-xxx-0001-aaaa --limit 20`);
1396
1579
  return;
1397
1580
  }
1581
+ // P1b.6.2: 托管会话 send 时,from/to 可省略——从 runtime 和话题索引推断
1582
+ // history --thread 时同理:from/to 均可省略
1583
+ if (process.env.EVOLCORE_SESSION_ID && (sub === 'send' || (sub === 'history' && getArgValue(args, '--thread')))) {
1584
+ const { isValidAid: checkAid } = await import('../aun/aid/index.js');
1585
+ const hasFrom = typeof args[1] === 'string' && checkAid(args[1]);
1586
+ const hasTo = typeof args[2] === 'string' && checkAid(args[2]);
1587
+ if (!hasFrom || !hasTo) {
1588
+ const runtime = await readBestTaskRuntimeContext();
1589
+ if (runtime?.selfAid) {
1590
+ const inferredFrom = runtime.selfAid.replace(/^@/, '');
1591
+ let inferredTo;
1592
+ const threadArg = getArgValue(args, '--thread');
1593
+ if (threadArg) {
1594
+ const { ThreadService } = await import('../core/thread/index.js');
1595
+ inferredTo = ThreadService.findThreadById(inferredFrom, threadArg)?.peer;
1596
+ }
1597
+ if (!inferredTo) {
1598
+ inferredTo = runtime.channelId?.replace(/^@/, '');
1599
+ }
1600
+ if (!hasFrom && !hasTo && inferredTo) {
1601
+ args.splice(1, 0, inferredFrom, inferredTo);
1602
+ }
1603
+ else if (!hasFrom && inferredTo) {
1604
+ args.splice(1, 0, inferredFrom);
1605
+ }
1606
+ else if (!hasTo && inferredTo) {
1607
+ args.splice(2, 0, inferredTo);
1608
+ }
1609
+ }
1610
+ }
1611
+ }
1398
1612
  const from = args[1];
1399
1613
  if (!from) {
1400
1614
  console.error('❌ 缺少 <from-aid> 参数');
@@ -1517,6 +1731,7 @@ Options:
1517
1731
  before,
1518
1732
  after,
1519
1733
  direction: (getArgValue(args, '--direction') ?? 'all'),
1734
+ threadId: getArgValue(args, '--thread'),
1520
1735
  });
1521
1736
  if (!result.ok) {
1522
1737
  if (format === 'json')
@@ -1626,11 +1841,38 @@ Options:
1626
1841
  ? false
1627
1842
  : args.includes('--encrypt') ? true : undefined;
1628
1843
  const thread = getArgValue(args, '--thread');
1629
- const returnPolicyRaw = getArgValue(args, '--return');
1630
- if (returnPolicyRaw && returnPolicyRaw !== 'required' && returnPolicyRaw !== 'none') {
1631
- console.error(`❌ --return 仅支持 required|none: ${returnPolicyRaw}`);
1632
- process.exit(1);
1844
+ // P1b.6.5: 话题存在性校验 — --thread 指定的话题必须在本端索引里存在
1845
+ let threadEntry;
1846
+ if (thread) {
1847
+ const { ThreadService, normalizeThreadPeer } = await import('../core/thread/index.js');
1848
+ threadEntry = ThreadService.findThreadById(from, thread);
1849
+ if (!threadEntry || normalizeThreadPeer(threadEntry.peer) !== normalizeThreadPeer(to)) {
1850
+ if (formatJson) {
1851
+ console.log(JSON.stringify({ ok: false, code: 'TOPIC_NOT_FOUND', error: `话题不存在: ${thread}` }));
1852
+ }
1853
+ else {
1854
+ console.error(`❌ 话题不存在: ${thread}`);
1855
+ }
1856
+ process.exit(1);
1857
+ }
1633
1858
  }
1859
+ // T8: 跨话题发送必须显式指定复查周期(发送前校验,避免消息已发出才报错)
1860
+ const checkEveryRaw = getArgValue(args, '--check-every');
1861
+ const hasCheckEvery = args.includes('--check-every') || checkEveryRaw !== undefined;
1862
+ const triggerPromptExtra = getArgValue(args, '--trigger-prompt');
1863
+ let checkEveryDurationMs;
1864
+ if (thread) {
1865
+ const parsedCheck = parseCheckEveryArgs(checkEveryRaw, hasCheckEvery);
1866
+ if ('error' in parsedCheck) {
1867
+ if (formatJson)
1868
+ console.log(JSON.stringify({ ok: false, code: parsedCheck.code, error: parsedCheck.error }));
1869
+ else
1870
+ console.error(`❌ ${parsedCheck.error}`);
1871
+ process.exit(1);
1872
+ }
1873
+ checkEveryDurationMs = parsedCheck.durationMs;
1874
+ }
1875
+ // --return 参数已移除:跨会话 send 不再需要指定返回策略(内部默认 none)。
1634
1876
  // SDK 尚未提供字节进度回调,只展示上传开始和成功完成。
1635
1877
  const onUploadProgress = formatJson ? undefined : (info) => {
1636
1878
  if (info.phase === 'upload')
@@ -1638,7 +1880,8 @@ Options:
1638
1880
  else if (info.phase === 'done')
1639
1881
  process.stderr.write(' ✓ upload complete\n');
1640
1882
  };
1641
- const result = await msgSend({ from, to, body, encrypt, thread, returnPolicy: returnPolicyRaw, handoffContext: getArgValue(args, '--handoff-context'), onUploadProgress, ...commonOpts });
1883
+ const runtime = await readBestTaskRuntimeContext();
1884
+ const result = await msgSend({ from, to, body, encrypt, thread, handoffContext: getArgValue(args, '--handoff-context'), onUploadProgress, ...commonOpts });
1642
1885
  if (!result.ok) {
1643
1886
  if (formatJson) {
1644
1887
  console.log(JSON.stringify(result));
@@ -1648,6 +1891,31 @@ Options:
1648
1891
  }
1649
1892
  process.exit(1);
1650
1893
  }
1894
+ // T2: 跨话题 send 成功后创建复查 Trigger 并写入本端 inbox(Thread Context)。
1895
+ // 私有逻辑集中在 applyCrossThreadSendEffects,与群聊路径共用。
1896
+ let createdTriggerId;
1897
+ if (thread && threadEntry && result.message_id && runtime?.sessionId) {
1898
+ const effects = await applyCrossThreadSendEffects({
1899
+ selfAid: from,
1900
+ targetThreadId: thread,
1901
+ targetSessionId: threadEntry.sessionId,
1902
+ targetChannelId: to,
1903
+ feedbackChatType: 'private',
1904
+ messageId: result.message_id,
1905
+ contentSummary: body.mode === 'text' ? body.text
1906
+ : body.mode === 'link' ? `[link] ${body.url}`
1907
+ : body.mode === 'file' ? `[file] ${body.filePath}`
1908
+ : '[payload]',
1909
+ timestamp: result.timestamp ?? Date.now(),
1910
+ durationMs: checkEveryDurationMs ?? 5 * 60 * 1000,
1911
+ checkEveryLabel: checkEveryRaw || '5m',
1912
+ triggerPromptExtra,
1913
+ });
1914
+ createdTriggerId = effects.triggerId;
1915
+ }
1916
+ // E4: 把复查 Trigger ID 回填到返回值,兑现 spec §3.4 的 `{ message_id, trigger_id }` 契约。
1917
+ if (createdTriggerId)
1918
+ result.trigger_id = createdTriggerId;
1651
1919
  if (formatJson) {
1652
1920
  console.log(JSON.stringify(result));
1653
1921
  }
@@ -1850,7 +2118,7 @@ visitor 不开放历史;群权限仍由托管会话校验。`);
1850
2118
  console.log(`用法: ec group <command> <from-aid> [args...] [options]
1851
2119
 
1852
2120
  消息:
1853
- send <from> <group-id> <text> --return <none|required> 发送群文本(跨会话时 --return 必填)
2121
+ send <from> <group-id> <text> 发送群文本
1854
2122
  send <from> <group-id> --file <path> [--as <type>] 发送群文件
1855
2123
  send <from> <group-id> --payload <json> 发送自定义 payload
1856
2124
  pull <from> <group-id> [--after-seq N] [--limit N] --app <name> 拉取群消息(必须使用独立 slot)
@@ -1900,8 +2168,11 @@ Options:
1900
2168
  --format json 输出 JSON 格式
1901
2169
  --encrypt 强制端到端加密(未指定时使用 daemon 默认值;仅 send)
1902
2170
  --no-encrypt 强制明文发送(优先于 --encrypt;仅 send)
1903
- --thread <id> 指定话题 ID(仅 send)
1904
- --return <none|required> 跨会话发送必填(仅 send;Trigger 只能使用 none)
2171
+ --thread <id> 指定话题 ID(仅 send;话题必须存在于本端索引)
2172
+ --check-every <时长> 跨话题发送(带 --thread)时**必填**,创建周期复查 Trigger(如 5m/30m/2h)
2173
+ 无值时默认 5m;Trigger 名:check-<目标threadId>-from-<来源threadId>-<唯一后缀>
2174
+ 取消/调整用返回的 trigger_id(trig_…),show/update/cancel 只认 id
2175
+ --trigger-prompt <text> 自定义复查 Trigger prompt 中的用户段(仅 --check-every)
1905
2176
  --mention <aid> 发送时 @ 某个成员(可多次,或用逗号分隔多个 aid)
1906
2177
  --mention-all 发送时 @ 所有人
1907
2178
  --content-type <mime> 显式覆盖 MIME(仅 --file 模式)
@@ -1915,9 +2186,10 @@ Options:
1915
2186
 
1916
2187
  格式示例(.example.invalid 是保留的虚构域,以下 AID 均不是真实地址):
1917
2188
  ec group create sender.example.invalid "Dev Team" --visibility private
1918
- ec group send sender.example.invalid 12345.example.invalid "hello team" --return none
2189
+ ec group send sender.example.invalid 12345.example.invalid "hello team"
1919
2190
  ec group send sender.example.invalid 12345.example.invalid "@member 看下 PR" --mention member.example.invalid
1920
2191
  ec group send sender.example.invalid 12345.example.invalid --file ./arch.png
2192
+ ec group send sender.example.invalid g-12345.example.invalid "进度?" --thread thread-xxx --check-every 30m
1921
2193
  ec group rules sender.example.invalid 12345.example.invalid set ./rules.md
1922
2194
  ec group invite sender.example.invalid 12345.example.invalid member.example.invalid
1923
2195
  ec group members sender.example.invalid 12345.example.invalid
@@ -2157,7 +2429,16 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2157
2429
  failManagedGroup(formatJson, 'NOT_ALLOWED', '--aun-path and --app are not allowed in a managed task');
2158
2430
  }
2159
2431
  const runtime = await readBestTaskRuntimeContext();
2160
- validateManagedGroupScope(from, groupId, runtime, formatJson);
2432
+ // 跨话题发送:目标群由 --thread 话题决定,不受「只能操作当前群」限制;
2433
+ // 发送者仍必须是当前 self AID,daemon 侧按目标话题 relation 复核。
2434
+ const threadArg = getArgValue(args, '--thread');
2435
+ const { resolveThreadRouteTarget } = await import('../core/thread/thread-route.js');
2436
+ if (threadArg && resolveThreadRouteTarget(from, threadArg, 'group', groupId)) {
2437
+ validateManagedGroupScope(from, undefined, runtime, formatJson);
2438
+ }
2439
+ else {
2440
+ validateManagedGroupScope(from, groupId, runtime, formatJson);
2441
+ }
2161
2442
  }
2162
2443
  const optionsEnd = args.indexOf('--', 3);
2163
2444
  const optionArgs = optionsEnd >= 0 ? args.slice(0, optionsEnd) : args;
@@ -2165,7 +2446,7 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2165
2446
  const fileVal = getSendArgValue('--file');
2166
2447
  const payloadVal = getSendArgValue('--payload');
2167
2448
  const bodyPositionals = collectPositional(args, 3);
2168
- const bodyFlags = ['--file', '--payload', '--as', '--content-type', '--text', '--transcript', '--thread', '--return'];
2449
+ const bodyFlags = ['--file', '--payload', '--as', '--content-type', '--text', '--transcript', '--thread', '--check-every', '--trigger-prompt'];
2169
2450
  for (let index = 3; index < args.length; index++) {
2170
2451
  const flag = args[index];
2171
2452
  if (flag === '--')
@@ -2238,9 +2519,24 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2238
2519
  ? false
2239
2520
  : args.includes('--encrypt') ? true : undefined;
2240
2521
  const thread = getSendArgValue('--thread');
2241
- const returnPolicyRaw = getSendArgValue('--return');
2242
- if (returnPolicyRaw && returnPolicyRaw !== 'required' && returnPolicyRaw !== 'none') {
2243
- failManagedGroup(formatJson, 'INVALID_ARGUMENT', `--return 仅支持 required|none: ${returnPolicyRaw}`);
2522
+ // M2: 群话题跨话题发送与私聊同样必须指定 --check-every,并同样创建复查 Trigger + 写 inbox。
2523
+ const groupCheckEveryRaw = getSendArgValue('--check-every');
2524
+ const groupHasCheckEvery = optionArgs.includes('--check-every') || groupCheckEveryRaw !== undefined;
2525
+ const groupTriggerPromptExtra = getSendArgValue('--trigger-prompt');
2526
+ let groupCheckEveryMs;
2527
+ let groupThreadEntry;
2528
+ if (thread) {
2529
+ // 话题必须存在于本端索引(与私聊路径一致,不自动创建)。
2530
+ const { ThreadService } = await import('../core/thread/index.js');
2531
+ groupThreadEntry = ThreadService.findThreadById(from, thread);
2532
+ if (!groupThreadEntry || normalizeGroupAid(groupThreadEntry.peer, from.slice(from.indexOf('.') + 1)) !== groupId) {
2533
+ failManagedGroup(formatJson, 'TOPIC_NOT_FOUND', `话题不存在: ${thread}`);
2534
+ }
2535
+ const parsedCheck = parseCheckEveryArgs(groupCheckEveryRaw, groupHasCheckEvery);
2536
+ if ('error' in parsedCheck) {
2537
+ failManagedGroup(formatJson, parsedCheck.code, parsedCheck.error);
2538
+ }
2539
+ groupCheckEveryMs = parsedCheck.durationMs;
2244
2540
  }
2245
2541
  const result = await groupSend({
2246
2542
  from,
@@ -2249,9 +2545,33 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2249
2545
  mentions: mentions.length ? mentions : undefined,
2250
2546
  encrypt: encryptGroup,
2251
2547
  thread,
2252
- returnPolicy: returnPolicyRaw,
2253
2548
  ...commonOpts,
2254
2549
  });
2550
+ // T2.6: 群聊跨话题发送成功后,同样写入本端 inbox(与私聊路径一致)
2551
+ if (result && result.ok !== false && thread && groupThreadEntry) {
2552
+ const r = result;
2553
+ const messageId = r.message_id ?? r.message?.message_id;
2554
+ if (messageId) {
2555
+ const effects = await applyCrossThreadSendEffects({
2556
+ selfAid: from,
2557
+ targetThreadId: thread,
2558
+ targetSessionId: groupThreadEntry.sessionId,
2559
+ targetChannelId: groupId,
2560
+ feedbackChatType: 'group',
2561
+ messageId: String(messageId),
2562
+ contentSummary: body.mode === 'text' ? body.text
2563
+ : body.mode === 'link' ? `[link] ${body.url}`
2564
+ : body.mode === 'file' ? `[file] ${body.filePath}`
2565
+ : '[payload]',
2566
+ timestamp: r.timestamp ?? Date.now(),
2567
+ durationMs: groupCheckEveryMs ?? 5 * 60 * 1000,
2568
+ checkEveryLabel: groupCheckEveryRaw || '5m',
2569
+ triggerPromptExtra: groupTriggerPromptExtra,
2570
+ });
2571
+ if (effects.triggerId)
2572
+ result.trigger_id = effects.triggerId;
2573
+ }
2574
+ }
2255
2575
  outputResult(result, () => {
2256
2576
  const r = result;
2257
2577
  if (r.handoff_id && r.status === 'delivered')
@@ -2798,7 +3118,7 @@ export function resolveManagedSendArgv(argv) {
2798
3118
  const booleans = new Set();
2799
3119
  const text = [];
2800
3120
  const mentions = [];
2801
- const valueFlags = new Set(['--format', '--file', '--payload', '--text-from-file', '--link', '--title', '--description', '--as', '--content-type', '--text', '--transcript', '--thread', '--return', '--handoff-context', '--mention']);
3121
+ const valueFlags = new Set(['--format', '--file', '--payload', '--text-from-file', '--link', '--title', '--description', '--as', '--content-type', '--text', '--transcript', '--thread', '--check-every', '--trigger-prompt', '--return', '--handoff-context', '--mention']);
2802
3122
  const booleanFlags = new Set(['--encrypt', '--no-encrypt', '--mention-all']);
2803
3123
  for (let index = 4; index < argv.length; index++) {
2804
3124
  const flag = argv[index];
@@ -2840,6 +3160,20 @@ export function resolveManagedSendArgv(argv) {
2840
3160
  const handoffContext = values.get('--handoff-context');
2841
3161
  if (handoffContext !== undefined && (scope !== 'msg' || !handoffContext.trim() || handoffContext.length > 65536))
2842
3162
  throw new Error('Invalid private handoff context');
3163
+ const thread = values.get('--thread');
3164
+ const checkEvery = values.get('--check-every');
3165
+ const triggerPromptExtra = values.get('--trigger-prompt');
3166
+ if (!thread && (checkEvery !== undefined || triggerPromptExtra !== undefined))
3167
+ throw new Error('Cross-thread options require --thread');
3168
+ let checkEveryMs;
3169
+ if (thread) {
3170
+ if (checkEvery === undefined)
3171
+ throw new Error('Cross-thread sends require --check-every');
3172
+ const parsedCheckEveryMs = parseDuration(checkEvery);
3173
+ if (parsedCheckEveryMs === null)
3174
+ throw new Error('Invalid --check-every duration');
3175
+ checkEveryMs = parsedCheckEveryMs;
3176
+ }
2843
3177
  if (scope === 'msg' && (mentions.length || booleans.has('--mention-all')))
2844
3178
  throw new Error('Mentions require a group send');
2845
3179
  if (scope === 'group' && (values.has('--link') || values.has('--text-from-file')))
@@ -2878,7 +3212,7 @@ export function resolveManagedSendArgv(argv) {
2878
3212
  mentions.push({ scope: 'all' });
2879
3213
  return { aid, to, scope, payload, file, textFile: values.get('--text-from-file'),
2880
3214
  encrypt: booleans.has('--no-encrypt') ? false : booleans.has('--encrypt') ? true : undefined,
2881
- thread: values.get('--thread'), returnPolicy: returnPolicy, handoffContext,
3215
+ thread, checkEvery, checkEveryMs: checkEveryMs ?? undefined, triggerPromptExtra, returnPolicy: returnPolicy, handoffContext,
2882
3216
  mentions: mentions.length ? mentions : undefined };
2883
3217
  }
2884
3218
  /**
@@ -2900,7 +3234,9 @@ const VALUE_FLAGS = new Set([
2900
3234
  '--as', '--content-type', '--mention', '--visibility', '--join-mode',
2901
3235
  '--group-id', '--name', '--message', '--answer', '--page', '--size',
2902
3236
  '--member-type',
2903
- '--aun-path', '--thread', '--return', '--handoff-context',
3237
+ '--aun-path', '--thread', '--handoff-context',
3238
+ // 跨话题发送的复查参数:漏登记会让 collectPositional 把它们当正文(正文污染)
3239
+ '--check-every', '--trigger-prompt',
2904
3240
  ]);
2905
3241
  const BOOLEAN_FLAGS = new Set([
2906
3242
  '--encrypt', '--no-encrypt', '--mention-all', '--clear',
@@ -11,8 +11,14 @@ export async function cmdContact(args) {
11
11
  }
12
12
  const json = getArgValue(args, '--format') === 'json';
13
13
  if (process.env.EVOLCORE_SESSION_ID) {
14
- const result = await runManaged(args, json);
15
- output(result, json);
14
+ try {
15
+ const result = await runManaged(args, json);
16
+ output(result, json);
17
+ }
18
+ catch (err) {
19
+ console.error(`❌ ${err.message ?? String(err)}`);
20
+ process.exitCode = 1;
21
+ }
16
22
  return;
17
23
  }
18
24
  if (!args.includes('--self') && isListCommand(args) && !process.env.EVOLCORE_SELF_AID?.trim()) {