evolcore 0.0.31 → 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 (179) hide show
  1. package/CHANGELOG.md +35 -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 +51 -24
  7. package/dist/agents/ecagent-runner.js +9 -13
  8. package/dist/agents/gemini-runner.js +4 -0
  9. package/dist/aun/aid/store.js +0 -81
  10. package/dist/aun/group/fs-download.js +1 -10
  11. package/dist/aun/group/operations.js +0 -1
  12. package/dist/aun/msg/history.js +1 -0
  13. package/dist/aun/msg/managed-operation.js +130 -32
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/aun/service-proxy.js +12 -18
  17. package/dist/aun/storage/index.js +29 -0
  18. package/dist/channels/aun.js +38 -137
  19. package/dist/channels/daemon.js +257 -2
  20. package/dist/cli/agent-command.js +64 -31
  21. package/dist/cli/agent-create.js +43 -24
  22. package/dist/cli/agent.js +16 -70
  23. package/dist/cli/aun-commands.js +512 -32
  24. package/dist/cli/contact.js +8 -2
  25. package/dist/cli/ctl-command.js +1 -2
  26. package/dist/cli/daemon-commands.js +82 -13
  27. package/dist/cli/debug-mode.js +154 -0
  28. package/dist/cli/diag.js +2 -2
  29. package/dist/cli/handoff-command.js +18 -5
  30. package/dist/cli/index.js +81 -64
  31. package/dist/cli/init.js +2 -2
  32. package/dist/cli/managed-command-guard.js +5 -0
  33. package/dist/cli/memory-command.js +93 -12
  34. package/dist/cli/model.js +14 -138
  35. package/dist/cli/queue-command.js +51 -0
  36. package/dist/cli/response.js +5 -54
  37. package/dist/cli/session-command.js +194 -0
  38. package/dist/cli/skill-command.js +85 -3
  39. package/dist/cli/stats.js +10 -4
  40. package/dist/cli/task-command.js +1 -0
  41. package/dist/cli/task-context.js +16 -0
  42. package/dist/cli/thread-command.js +620 -0
  43. package/dist/cli/trigger-command.js +76 -50
  44. package/dist/config/builtin-roles.js +9 -7
  45. package/dist/config/config-field-policy.js +2 -3
  46. package/dist/config/config-manager.js +7 -2
  47. package/dist/config/resolved-config-op.js +3 -0
  48. package/dist/config-store.js +15 -0
  49. package/dist/core/agent-ipc-registration.js +65 -22
  50. package/dist/core/auth/agent-delegation.js +42 -2
  51. package/dist/core/auth/debug-context.js +139 -0
  52. package/dist/core/auth/delegation-authorization.js +1 -0
  53. package/dist/core/auth/operation-authorizer.js +24 -1
  54. package/dist/core/auth/operation-catalog.js +44 -3
  55. package/dist/core/background-session/session-cleanup.js +212 -0
  56. package/dist/core/background-session/types.js +10 -0
  57. package/dist/core/bootstrap-messages.js +1 -1
  58. package/dist/core/bootstrap-service.js +6 -3
  59. package/dist/core/command/command-handler.js +224 -45
  60. package/dist/core/command/diag-service.js +5 -1
  61. package/dist/core/command/memory-slash.js +6 -2
  62. package/dist/core/command/menu-catalog.js +48 -1
  63. package/dist/core/command/menu-handler.js +92 -16
  64. package/dist/core/command/menu-protocol.js +24 -1
  65. package/dist/core/command/skill-slash.js +322 -0
  66. package/dist/core/command/slash-gate.js +6 -4
  67. package/dist/core/command/slash-handler.js +193 -13
  68. package/dist/core/command/topic-slash.js +528 -0
  69. package/dist/core/evolagent-registry.js +20 -5
  70. package/dist/core/handoff/ipc-registration.js +38 -0
  71. package/dist/core/handoff/runtime.js +5 -11
  72. package/dist/core/handoff/store.js +5 -0
  73. package/dist/core/ipc-registration.js +210 -9
  74. package/dist/core/memory/consolidation-context.js +49 -16
  75. package/dist/core/memory/consolidation-ops-records.js +35 -0
  76. package/dist/core/memory/consolidation-queue-format.js +21 -0
  77. package/dist/core/memory/consolidation.js +174 -180
  78. package/dist/core/message/aun-ipc-sender.js +207 -8
  79. package/dist/core/message/im-renderer.js +149 -6
  80. package/dist/core/message/message-bridge.js +13 -3
  81. package/dist/core/message/message-log.js +1 -0
  82. package/dist/core/message/message-queue.js +131 -3
  83. package/dist/core/message/peer-mode.js +6 -1
  84. package/dist/core/message/response-engine.js +379 -69
  85. package/dist/core/message/subtask-tool-projection.js +140 -0
  86. package/dist/core/model/catalog-service.js +17 -12
  87. package/dist/core/permission/ec-command-parser.js +126 -7
  88. package/dist/core/permission/sandbox-runtime.js +14 -1
  89. package/dist/core/session/session-idle-monitor.js +249 -22
  90. package/dist/core/session/session-manager.js +54 -9
  91. package/dist/core/skill/command-parser.js +13 -0
  92. package/dist/core/skill/index-generator.js +4 -1
  93. package/dist/core/skill/startup.js +41 -0
  94. package/dist/core/skill/switch-resolver.js +55 -29
  95. package/dist/core/task/task-recovery-service.js +2 -0
  96. package/dist/core/task/task-resume-operation.js +11 -0
  97. package/dist/core/thread/command-parser.js +125 -0
  98. package/dist/core/thread/index.js +1 -0
  99. package/dist/core/thread/ipc-operation.js +95 -0
  100. package/dist/core/thread/thread-context-storage.js +131 -0
  101. package/dist/core/thread/thread-operation.js +258 -0
  102. package/dist/core/thread/thread-route.js +33 -0
  103. package/dist/core/thread/thread-service.js +777 -0
  104. package/dist/core/thread/types.js +1 -0
  105. package/dist/eck/message-renderer.js +35 -0
  106. package/dist/index.js +10 -7
  107. package/dist/ipc.js +234 -17
  108. package/dist/paths.js +23 -0
  109. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  110. package/dist/trigger/authorization.js +59 -29
  111. package/dist/trigger/feedback.js +7 -1
  112. package/dist/trigger/ipc-service.js +49 -35
  113. package/dist/trigger/parser.js +63 -1
  114. package/dist/trigger/script-sandbox.js +58 -77
  115. package/dist/trigger/validation.js +14 -0
  116. package/dist/utils/error-utils.js +22 -1
  117. package/kits/docs/AUTHORING.md +1 -1
  118. package/kits/docs/channels/aun.md +8 -0
  119. package/kits/docs/context-assembly.md +1 -0
  120. package/kits/docs/evolcore/INDEX.md +3 -2
  121. package/kits/docs/evolcore/agent-create.md +39 -28
  122. package/kits/docs/evolcore/agent.md +19 -4
  123. package/kits/docs/evolcore/ec-capabilities.md +4 -0
  124. package/kits/docs/evolcore/group-collaboration.md +3 -5
  125. package/kits/docs/evolcore/group.md +3 -4
  126. package/kits/docs/evolcore/memory-consolidation.md +1 -1
  127. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  128. package/kits/docs/evolcore/menu-protocol.md +7 -0
  129. package/kits/docs/evolcore/msg.md +5 -8
  130. package/kits/docs/evolcore/task.md +2 -0
  131. package/kits/docs/evolcore/thread-chain.md +267 -0
  132. package/kits/docs/evolcore/trigger.md +2 -2
  133. package/kits/docs/prompt-loading-architecture.md +1 -0
  134. package/kits/eck_manifest.json +61 -26
  135. package/kits/eck_message_manifest.json +169 -10
  136. package/kits/rules/01-overview.md +0 -9
  137. package/kits/rules/05-venue.md +1 -1
  138. package/kits/rules/06-channel.md +7 -1
  139. package/kits/rules/07-memory-and-thread.md +202 -0
  140. package/kits/schemas/relation-config.schema.9.json +18 -3
  141. package/kits/schemas/role-config.schema.1.json +6 -6
  142. package/kits/schemas/single-session.schema.1.json +6 -6
  143. package/kits/schemas/single-session.schema.2.json +6 -6
  144. package/kits/schemas/single-session.schema.3.json +6 -6
  145. package/kits/skills/.switches.json +12 -1
  146. package/kits/skills/ec-group/SKILL.md +234 -0
  147. package/kits/skills/ec-skill/SKILL.md +10 -1
  148. package/kits/skills/ec-skill/scripts/cli.mjs +15 -1
  149. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  150. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  151. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  152. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  153. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  154. package/kits/skills/thread-ops/SKILL.md +492 -0
  155. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  156. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  157. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  158. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  159. package/kits/templates/bootstrap-welcome.md +7 -7
  160. package/kits/templates/memory-consolidation/forget.md +5 -0
  161. package/kits/templates/memory-consolidation/generalize.md +5 -0
  162. package/kits/templates/memory-consolidation/profile.md +5 -0
  163. package/kits/templates/memory-consolidation/promote.md +5 -0
  164. package/kits/templates/message-fragments/item.md +2 -1
  165. package/kits/templates/message-fragments/thread-context.md +6 -0
  166. package/kits/templates/message-fragments/trigger-session.md +2 -0
  167. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  168. package/kits/templates/roles/member.json +2 -0
  169. package/kits/templates/roles/visitor.json +2 -0
  170. package/kits/templates/system-fragments/bootstrap.md +66 -72
  171. package/kits/templates/system-fragments/channel.md +3 -6
  172. package/kits/templates/system-fragments/commands.md +2 -1
  173. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  174. package/kits/templates/system-fragments/reply.md +3 -2
  175. package/kits/templates/system-fragments/session.md +5 -14
  176. package/package.json +3 -1
  177. package/kits/docs/evolcore/group-fs.md +0 -17
  178. package/kits/docs/venues/aun-group.md +0 -45
  179. package/kits/docs/venues/group.md +0 -43
@@ -1,15 +1,18 @@
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';
6
7
  import { ipcQuery } from '../ipc.js';
8
+ import { isValidAid as validSendAid } from '../aun/aid/validation.js';
7
9
  import { AGENT_DELEGATION_TOKEN_ENV, readDelegationCredential } from '../core/auth/agent-delegation.js';
8
10
  import { isManagementRole } from '../config/builtin-roles.js';
9
11
  import { readBestTaskRuntimeContext } from './task-context.js';
10
12
  import { assertEcSingleLine } from './cli-argv.js';
11
13
  import { canonicalizeEvolcoreCommandArgv } from '../core/permission/ec-command-parser.js';
12
14
  import { ManagedGroupOperationError } from '../aun/msg/managed-operation.js';
15
+ import { parseDuration } from '../trigger/parser.js';
13
16
  // ==================== AID ====================
14
17
  function resolveAunPath(args) {
15
18
  const idx = args.indexOf('--aun-path');
@@ -1088,6 +1091,7 @@ Options:
1088
1091
  }
1089
1092
  // ==================== Storage ====================
1090
1093
  export async function cmdStorage(args) {
1094
+ args = canonicalizeEvolcoreCommandArgv(['ec', 'storage', ...args]).slice(2);
1091
1095
  const sub = args[0];
1092
1096
  const aunPath = resolveAunPath(args);
1093
1097
  const formatJson = getArgValue(args, '--format') === 'json';
@@ -1142,12 +1146,9 @@ Options:
1142
1146
  console.error('用法: ec storage upload <aid> <local-file> <remote-path> [--public]');
1143
1147
  process.exit(1);
1144
1148
  }
1145
- if (!fs.existsSync(localFile)) {
1146
- console.error(`❌ 文件不存在: ${localFile}`);
1147
- process.exit(1);
1148
- }
1149
1149
  const result = await ipcQuery(resolvePaths().socket, {
1150
1150
  type: 'storage.upload',
1151
+ argv: ['storage', ...args],
1151
1152
  aid,
1152
1153
  localFile,
1153
1154
  remotePath,
@@ -1172,12 +1173,25 @@ Options:
1172
1173
  return;
1173
1174
  }
1174
1175
  const authorization = await ipcQuery(resolvePaths().socket, {
1175
- type: 'storage.auth', operation, aid, sessionId: process.env.EVOLCORE_SESSION_ID, delegationToken,
1176
+ type: 'storage.auth', argv: ['storage', ...args], operation, aid, sessionId: process.env.EVOLCORE_SESSION_ID, delegationToken,
1176
1177
  }, 10_000);
1177
1178
  if (!authorization)
1178
1179
  failManagedGroup(formatJson, 'DAEMON_UNAVAILABLE', 'Unable to connect to the evolcore daemon');
1179
1180
  if (!authorization.ok)
1180
1181
  failManagedGroup(formatJson, authorization.code || 'STORAGE_AUTH_FAILED', authorization.error || 'Storage operation denied');
1182
+ if (sub === 'ls') {
1183
+ const objects = authorization.result?.objects ?? authorization.result ?? [];
1184
+ console.log(Array.isArray(objects) && !objects.length ? '(空)' : JSON.stringify(objects, null, 2));
1185
+ }
1186
+ else if (sub === 'quota')
1187
+ console.log(JSON.stringify(authorization.result, null, 2));
1188
+ else if (formatJson)
1189
+ console.log(JSON.stringify(authorization));
1190
+ else if (sub === 'download')
1191
+ console.log(`✓ 已下载: ${authorization.localPath} (${authorization.size} bytes)`);
1192
+ else
1193
+ console.log(`✓ 已删除: ${authorization.objectKey}`);
1194
+ return;
1181
1195
  }
1182
1196
  const { storageUpload, storageDownload, storageLs, storageRm, storageQuota } = await import('../aun/storage/index.js');
1183
1197
  if (sub === 'upload') {
@@ -1295,6 +1309,176 @@ Options:
1295
1309
  process.exit(1);
1296
1310
  }
1297
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
+ }
1298
1482
  export async function cmdMsg(args) {
1299
1483
  args = canonicalizeEvolcoreCommandArgv(['ec', 'msg', ...args]).slice(2);
1300
1484
  const sub = args[0];
@@ -1305,7 +1489,7 @@ export async function cmdMsg(args) {
1305
1489
  console.log(`用法: ec msg <command> <from-aid> [args...] [options]
1306
1490
 
1307
1491
  Commands:
1308
- send <from> <to> <text> --return <none|required> 发送文本(跨会话时 --return 必填)
1492
+ send <from> <to> <text> 发送文本
1309
1493
  send <from> <to> --text-from-file <path> 从文件读取文本内容
1310
1494
  send <from> <to> --file <path> [--as <type>] 发送文件(image|video|voice|file)
1311
1495
  send <from> <to> --link <url> [--title T] 发送链接卡片
@@ -1316,6 +1500,10 @@ Commands:
1316
1500
  online <from> <target-aid> [<target-aid>...] 查询在线状态
1317
1501
  history <from> [<target-aid>|selector] [筛选选项] 查询本地消息历史
1318
1502
 
1503
+ 托管会话简化语法(EVOLCORE_SESSION_ID 已设时):
1504
+ send <text> --thread <id> from/to 从运行时上下文与话题索引自动推断
1505
+ send <text> 发送到当前会话对端
1506
+
1319
1507
  Options:
1320
1508
  --text-from-file <path> 从文件读取文本(UTF-8),用于超长消息或避免 Shell 转义
1321
1509
  --handoff-context <text> 仅托管会话:为目标会话补充本地上下文,不发给对端
@@ -1324,7 +1512,7 @@ Options:
1324
1512
  --format json 输出 JSON 格式
1325
1513
  --encrypt 强制端到端加密(未指定时使用 daemon 默认值)
1326
1514
  --no-encrypt 强制明文发送(优先于 --encrypt)
1327
- --thread <id> 指定话题 ID(用于多话题路由)
1515
+ --thread <id> 指定目标话题 ID(话题必须存在于本端索引;from/to 可省略)
1328
1516
  --session <latest|all|id> 按 EvolCore 逻辑会话过滤历史(默认 latest)
1329
1517
  --before <time> 只返回该时间之前的历史
1330
1518
  --after <time> 只返回该时间之后的历史
@@ -1333,7 +1521,10 @@ Options:
1333
1521
  --channel-name <name> 历史渠道实例名(非 AUN 渠道必须匹配 Agent 配置)
1334
1522
  --channel-key <key> 非 AUN 已落盘渠道的完整实例键(与 --peer-id 成对)
1335
1523
  --peer-id <id> 非 AUN 渠道的原生对端 ID(与 --channel-key 成对)
1336
- --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
1337
1528
  --content-type <mime> 显式覆盖 MIME(仅 --file 模式)
1338
1529
  --as <type> 附件类型:image|video|voice|file(仅 --file 模式)
1339
1530
  --text <说明> 附件说明文字(仅 --file 模式)
@@ -1344,12 +1535,12 @@ Options:
1344
1535
  (用于发送恰好等于某 flag 的文本,如 send a b -- --encrypt)
1345
1536
 
1346
1537
  示例:
1347
- ec msg send alice.agentid.pub bob.agentid.pub "hello" --return none
1348
- ec msg send alice.agentid.pub bob.agentid.pub --text-from-file long-message.txt --return none
1349
- ec msg send alice.agentid.pub bob.agentid.pub "讨论项目A" --thread "project-A" --return required
1350
- ec msg send alice.agentid.pub bob.agentid.pub --file ./pic.png --return none
1351
- ec msg send alice.agentid.pub bob.agentid.pub --file ./demo.mp4 --as video --return none
1352
- 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"
1353
1544
  ec msg pull alice.agentid.pub --app my-bot
1354
1545
  ec msg ack alice.agentid.pub 42 --app my-bot
1355
1546
  ec msg recall alice.agentid.pub msg-uuid-1 msg-uuid-2
@@ -1359,13 +1550,16 @@ Options:
1359
1550
  return;
1360
1551
  }
1361
1552
  if (sub === 'history' && wantsHelp(args)) {
1362
- 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 均可省略,从运行时上下文自动推断。
1363
1556
 
1364
1557
  查询指定会话对的本地历史,不调用 message.pull,也不影响收件箱游标。非 AUN selector 按已落盘的 session metadata、完整 channelKey 与原生 peer ID 精确读取,不能传入文件路径。
1365
1558
 
1366
1559
  托管会话限制:普通会话只能使用当前 self AID 和当前 AUN 私聊对端;可信 daemon Trigger 可用精确的 --channel-key/--peer-id selector 读取已落盘的非 AUN 私聊历史。托管任务不支持 --channel、--channel-name、--aun-path 或 --app。
1367
1560
 
1368
1561
  Options:
1562
+ --thread <id> 按话题 ID 过滤(托管会话中 from/to 可省略)
1369
1563
  --session <latest|all|id> 默认 latest;all 查询全部;也可指定会话 ID
1370
1564
  --limit <N> 返回最新 N 条(默认 50,最大 500)
1371
1565
  --before <time> 开区间;epoch 毫秒或 ISO 时间
@@ -1380,9 +1574,41 @@ Options:
1380
1574
  ec msg history alice.agentid.pub bob.agentid.pub --session latest
1381
1575
  ec msg history alice.agentid.pub bob.agentid.pub --session all
1382
1576
  ec msg history alice.agentid.pub bob.agentid.pub --session meta_20260713_1783920000000 --format json
1383
- 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`);
1384
1579
  return;
1385
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
+ }
1386
1612
  const from = args[1];
1387
1613
  if (!from) {
1388
1614
  console.error('❌ 缺少 <from-aid> 参数');
@@ -1398,6 +1624,8 @@ Options:
1398
1624
  renderManagedMsgResult(sub, result, formatJson);
1399
1625
  return;
1400
1626
  }
1627
+ if (process.env.EVOLCORE_SESSION_ID && sub === 'send')
1628
+ return runManagedSend(['msg', ...args], formatJson);
1401
1629
  const { msgSend, msgPull, msgAck, msgRecall, msgOnline, msgHistory } = await import('../aun/msg/index.js');
1402
1630
  const commonOpts = { aunPath, slotId: appSlot };
1403
1631
  const requireConsumerSlot = (operation) => {
@@ -1503,6 +1731,7 @@ Options:
1503
1731
  before,
1504
1732
  after,
1505
1733
  direction: (getArgValue(args, '--direction') ?? 'all'),
1734
+ threadId: getArgValue(args, '--thread'),
1506
1735
  });
1507
1736
  if (!result.ok) {
1508
1737
  if (format === 'json')
@@ -1612,11 +1841,38 @@ Options:
1612
1841
  ? false
1613
1842
  : args.includes('--encrypt') ? true : undefined;
1614
1843
  const thread = getArgValue(args, '--thread');
1615
- const returnPolicyRaw = getArgValue(args, '--return');
1616
- if (returnPolicyRaw && returnPolicyRaw !== 'required' && returnPolicyRaw !== 'none') {
1617
- console.error(`❌ --return 仅支持 required|none: ${returnPolicyRaw}`);
1618
- 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
+ }
1619
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)。
1620
1876
  // SDK 尚未提供字节进度回调,只展示上传开始和成功完成。
1621
1877
  const onUploadProgress = formatJson ? undefined : (info) => {
1622
1878
  if (info.phase === 'upload')
@@ -1624,7 +1880,8 @@ Options:
1624
1880
  else if (info.phase === 'done')
1625
1881
  process.stderr.write(' ✓ upload complete\n');
1626
1882
  };
1627
- 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 });
1628
1885
  if (!result.ok) {
1629
1886
  if (formatJson) {
1630
1887
  console.log(JSON.stringify(result));
@@ -1634,6 +1891,31 @@ Options:
1634
1891
  }
1635
1892
  process.exit(1);
1636
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;
1637
1919
  if (formatJson) {
1638
1920
  console.log(JSON.stringify(result));
1639
1921
  }
@@ -1836,7 +2118,7 @@ visitor 不开放历史;群权限仍由托管会话校验。`);
1836
2118
  console.log(`用法: ec group <command> <from-aid> [args...] [options]
1837
2119
 
1838
2120
  消息:
1839
- send <from> <group-id> <text> --return <none|required> 发送群文本(跨会话时 --return 必填)
2121
+ send <from> <group-id> <text> 发送群文本
1840
2122
  send <from> <group-id> --file <path> [--as <type>] 发送群文件
1841
2123
  send <from> <group-id> --payload <json> 发送自定义 payload
1842
2124
  pull <from> <group-id> [--after-seq N] [--limit N] --app <name> 拉取群消息(必须使用独立 slot)
@@ -1886,8 +2168,11 @@ Options:
1886
2168
  --format json 输出 JSON 格式
1887
2169
  --encrypt 强制端到端加密(未指定时使用 daemon 默认值;仅 send)
1888
2170
  --no-encrypt 强制明文发送(优先于 --encrypt;仅 send)
1889
- --thread <id> 指定话题 ID(仅 send)
1890
- --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)
1891
2176
  --mention <aid> 发送时 @ 某个成员(可多次,或用逗号分隔多个 aid)
1892
2177
  --mention-all 发送时 @ 所有人
1893
2178
  --content-type <mime> 显式覆盖 MIME(仅 --file 模式)
@@ -1901,9 +2186,10 @@ Options:
1901
2186
 
1902
2187
  格式示例(.example.invalid 是保留的虚构域,以下 AID 均不是真实地址):
1903
2188
  ec group create sender.example.invalid "Dev Team" --visibility private
1904
- 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"
1905
2190
  ec group send sender.example.invalid 12345.example.invalid "@member 看下 PR" --mention member.example.invalid
1906
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
1907
2193
  ec group rules sender.example.invalid 12345.example.invalid set ./rules.md
1908
2194
  ec group invite sender.example.invalid 12345.example.invalid member.example.invalid
1909
2195
  ec group members sender.example.invalid 12345.example.invalid
@@ -1935,6 +2221,21 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
1935
2221
  renderManagedGroupResult(args, result, formatJson);
1936
2222
  return;
1937
2223
  }
2224
+ if (process.env.EVOLCORE_SESSION_ID && sub === 'send') {
2225
+ if (args.includes('--aun-path') || args.includes('--app')) {
2226
+ failManagedGroup(formatJson, 'NOT_ALLOWED', '--aun-path and --app are not allowed in a managed task');
2227
+ }
2228
+ let groupId;
2229
+ try {
2230
+ groupId = normalizeGroupAid(args[2] ?? '', from.slice(from.indexOf('.') + 1));
2231
+ }
2232
+ catch (error) {
2233
+ failManagedGroup(formatJson, 'INVALID_ARGUMENT', `Invalid group AID: ${error instanceof Error ? error.message : String(error)}`);
2234
+ }
2235
+ const runtime = await readBestTaskRuntimeContext();
2236
+ validateManagedGroupScope(from, groupId, runtime, formatJson);
2237
+ return runManagedSend(['group', ...args], formatJson);
2238
+ }
1938
2239
  const { groupSend, groupPull, groupAck, groupHistory, parseGroupHistoryArgs, groupCreate, groupInfo, groupList, groupUpdate, groupDissolve, groupSuspend, groupResume, groupJoin, groupLeave, groupInvite, groupInvites, groupAcceptInvite, groupRejectInvite, groupRevokeInvite, groupKick, groupMembers, groupOnline, groupSetRole, groupTransferOwner, groupBan, groupUnban, groupBanlist, groupBlock, groupUnblock, groupBlocklist, groupReportStatus, groupMemberStatuses, groupRules, groupUpdateRules, groupRulesFileGet, groupRulesFileSet, groupRulesFilePublish, } = await import('../aun/msg/index.js');
1939
2240
  const commonOpts = { aunPath, slotId: appSlot };
1940
2241
  if (['block', 'unblock', 'blocklist', 'report-status', 'member-statuses'].includes(sub)) {
@@ -2128,7 +2429,16 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2128
2429
  failManagedGroup(formatJson, 'NOT_ALLOWED', '--aun-path and --app are not allowed in a managed task');
2129
2430
  }
2130
2431
  const runtime = await readBestTaskRuntimeContext();
2131
- 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
+ }
2132
2442
  }
2133
2443
  const optionsEnd = args.indexOf('--', 3);
2134
2444
  const optionArgs = optionsEnd >= 0 ? args.slice(0, optionsEnd) : args;
@@ -2136,7 +2446,7 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2136
2446
  const fileVal = getSendArgValue('--file');
2137
2447
  const payloadVal = getSendArgValue('--payload');
2138
2448
  const bodyPositionals = collectPositional(args, 3);
2139
- 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'];
2140
2450
  for (let index = 3; index < args.length; index++) {
2141
2451
  const flag = args[index];
2142
2452
  if (flag === '--')
@@ -2209,9 +2519,24 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2209
2519
  ? false
2210
2520
  : args.includes('--encrypt') ? true : undefined;
2211
2521
  const thread = getSendArgValue('--thread');
2212
- const returnPolicyRaw = getSendArgValue('--return');
2213
- if (returnPolicyRaw && returnPolicyRaw !== 'required' && returnPolicyRaw !== 'none') {
2214
- 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;
2215
2540
  }
2216
2541
  const result = await groupSend({
2217
2542
  from,
@@ -2220,9 +2545,33 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2220
2545
  mentions: mentions.length ? mentions : undefined,
2221
2546
  encrypt: encryptGroup,
2222
2547
  thread,
2223
- returnPolicy: returnPolicyRaw,
2224
2548
  ...commonOpts,
2225
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
+ }
2226
2575
  outputResult(result, () => {
2227
2576
  const r = result;
2228
2577
  if (r.handoff_id && r.status === 'delivered')
@@ -2737,6 +3086,135 @@ g- 是名称的一部分,会完整保留;默认命名格式为 g-10000 至 g
2737
3086
  process.exit(1);
2738
3087
  }
2739
3088
  // ==================== Main ====================
3089
+ async function runManagedSend(argv, formatJson) {
3090
+ const delegationToken = readDelegationCredential();
3091
+ if (!delegationToken)
3092
+ failManagedGroup(formatJson, 'DELEGATION_REQUIRED', 'Managed send requires delegation');
3093
+ const runtime = await readBestTaskRuntimeContext();
3094
+ const result = await ipcQuery(resolvePaths().socket, {
3095
+ type: 'aun-msg-send', argv, delegationToken,
3096
+ originSessionId: process.env.EVOLCORE_SESSION_ID, originMessageId: runtime?.messageId,
3097
+ }, null);
3098
+ if (!result)
3099
+ failManagedGroup(formatJson, 'AUN_DAEMON_UNAVAILABLE', 'Unable to connect to the EvolCore daemon');
3100
+ if (!result.ok)
3101
+ failManagedGroup(formatJson, result.code ?? 'AUN_SEND_FAILED', result.error ?? 'Send failed');
3102
+ if (formatJson)
3103
+ console.log(JSON.stringify(result));
3104
+ else if (result.handoff_id)
3105
+ console.log(`✓ handoff ${result.handoff_id} ${result.status === 'delivered' ? '已投递' : '已排队'} ${result.message_id ?? ''}`.trimEnd());
3106
+ else
3107
+ console.log(`✓ 已发送 message_id=${result.message_id ?? '-'} seq=${result.seq ?? result.message?.seq ?? '-'}`);
3108
+ }
3109
+ export function resolveManagedSendArgv(argv) {
3110
+ if (!['msg', 'group'].includes(argv[0]) || argv[1] !== 'send' || !validSendAid(argv[2]))
3111
+ throw new Error('Invalid managed send command');
3112
+ const scope = argv[0];
3113
+ const aid = argv[2];
3114
+ const to = scope === 'group' ? normalizeGroupAid(argv[3], aid.slice(aid.indexOf('.') + 1)) : argv[3];
3115
+ if (!to || !validSendAid(to))
3116
+ throw new Error('Invalid send target');
3117
+ const values = new Map();
3118
+ const booleans = new Set();
3119
+ const text = [];
3120
+ const mentions = [];
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']);
3122
+ const booleanFlags = new Set(['--encrypt', '--no-encrypt', '--mention-all']);
3123
+ for (let index = 4; index < argv.length; index++) {
3124
+ const flag = argv[index];
3125
+ if (flag === '--') {
3126
+ text.push(...argv.slice(index + 1));
3127
+ break;
3128
+ }
3129
+ if (flag === '--app' || flag === '--aun-path')
3130
+ throw new Error(`${flag} is not allowed for managed sends`);
3131
+ if (booleanFlags.has(flag))
3132
+ booleans.add(flag);
3133
+ else if (valueFlags.has(flag)) {
3134
+ const value = argv[++index];
3135
+ if (value === undefined || valueFlags.has(value) || booleanFlags.has(value))
3136
+ throw new Error(`${flag} requires a value`);
3137
+ if (flag === '--mention') {
3138
+ for (const mention of value.split(',').map(item => item.trim()).filter(Boolean)) {
3139
+ if (!validSendAid(mention))
3140
+ throw new Error('Invalid mention AID');
3141
+ mentions.push({ aid: mention });
3142
+ }
3143
+ }
3144
+ else {
3145
+ if (values.has(flag))
3146
+ throw new Error(`Duplicate send option: ${flag}`);
3147
+ values.set(flag, value);
3148
+ }
3149
+ }
3150
+ else
3151
+ text.push(flag);
3152
+ }
3153
+ if (booleans.has('--encrypt') && booleans.has('--no-encrypt'))
3154
+ throw new Error('Encryption options are mutually exclusive');
3155
+ if (values.has('--format') && values.get('--format') !== 'json')
3156
+ throw new Error('--format only supports json');
3157
+ const returnPolicy = values.get('--return');
3158
+ if (returnPolicy !== undefined && !['none', 'required'].includes(returnPolicy))
3159
+ throw new Error('Invalid return policy');
3160
+ const handoffContext = values.get('--handoff-context');
3161
+ if (handoffContext !== undefined && (scope !== 'msg' || !handoffContext.trim() || handoffContext.length > 65536))
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
+ }
3177
+ if (scope === 'msg' && (mentions.length || booleans.has('--mention-all')))
3178
+ throw new Error('Mentions require a group send');
3179
+ if (scope === 'group' && (values.has('--link') || values.has('--text-from-file')))
3180
+ throw new Error('Unsupported group send body');
3181
+ const modes = ['--file', '--payload', '--text-from-file', '--link'].filter(flag => values.has(flag));
3182
+ if (modes.length > 1 || (modes.length && text.length))
3183
+ throw new Error('Message body modes are mutually exclusive');
3184
+ let payload;
3185
+ let file;
3186
+ if (values.has('--file')) {
3187
+ const as = values.get('--as');
3188
+ if (as && !['image', 'video', 'voice', 'file'].includes(as))
3189
+ throw new Error('Invalid attachment type');
3190
+ if (values.has('--transcript') && as !== 'voice')
3191
+ throw new Error('--transcript requires --as voice');
3192
+ file = { filePath: values.get('--file'), as, contentType: values.get('--content-type'), text: values.get('--text'), transcript: values.get('--transcript') };
3193
+ }
3194
+ else if (values.has('--payload')) {
3195
+ payload = JSON.parse(values.get('--payload'));
3196
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
3197
+ throw new Error('--payload must be a JSON object');
3198
+ }
3199
+ else if (values.has('--link')) {
3200
+ assertEcSingleLine(values.get('--title'), '--title');
3201
+ payload = { type: 'link', url: values.get('--link'), ...(values.get('--title') ? { title: values.get('--title') } : {}),
3202
+ ...(values.get('--description') ? { description: values.get('--description') } : {}) };
3203
+ }
3204
+ else if (!values.has('--text-from-file')) {
3205
+ if (!text.join(' '))
3206
+ throw new Error('Message content is required');
3207
+ payload = { type: 'text', text: text.join(' ') };
3208
+ }
3209
+ if (!file && ['--as', '--content-type', '--text', '--transcript'].some(flag => values.has(flag)))
3210
+ throw new Error('Attachment options require --file');
3211
+ if (booleans.has('--mention-all'))
3212
+ mentions.push({ scope: 'all' });
3213
+ return { aid, to, scope, payload, file, textFile: values.get('--text-from-file'),
3214
+ encrypt: booleans.has('--no-encrypt') ? false : booleans.has('--encrypt') ? true : undefined,
3215
+ thread, checkEvery, checkEveryMs: checkEveryMs ?? undefined, triggerPromptExtra, returnPolicy: returnPolicy, handoffContext,
3216
+ mentions: mentions.length ? mentions : undefined };
3217
+ }
2740
3218
  /**
2741
3219
  * 收集位置参数(从 startIdx 开始)。
2742
3220
  *
@@ -2756,7 +3234,9 @@ const VALUE_FLAGS = new Set([
2756
3234
  '--as', '--content-type', '--mention', '--visibility', '--join-mode',
2757
3235
  '--group-id', '--name', '--message', '--answer', '--page', '--size',
2758
3236
  '--member-type',
2759
- '--aun-path', '--thread', '--return', '--handoff-context',
3237
+ '--aun-path', '--thread', '--handoff-context',
3238
+ // 跨话题发送的复查参数:漏登记会让 collectPositional 把它们当正文(正文污染)
3239
+ '--check-every', '--trigger-prompt',
2760
3240
  ]);
2761
3241
  const BOOLEAN_FLAGS = new Set([
2762
3242
  '--encrypt', '--no-encrypt', '--mention-all', '--clear',