evolcore 0.0.3 → 0.0.4

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 (143) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +1 -31
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +157 -5
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/channels/aun.js +220 -112
  14. package/dist/channels/daemon.js +18 -4
  15. package/dist/channels/dingtalk.js +52 -137
  16. package/dist/channels/feishu.js +56 -4
  17. package/dist/channels/qqbot.js +23 -1
  18. package/dist/channels/wechat.js +303 -155
  19. package/dist/channels/wecom.js +383 -7
  20. package/dist/cli/aun-commands.js +11 -11
  21. package/dist/cli/handoff-command.js +2 -1
  22. package/dist/cli/init-channel.js +185 -67
  23. package/dist/cli/init.js +7 -5
  24. package/dist/cli/trigger-command.js +92 -11
  25. package/dist/config/access-policy-domain.js +38 -0
  26. package/dist/config/access-policy.js +134 -0
  27. package/dist/config/builtin-role-templates.js +27 -14
  28. package/dist/config/builtin-roles.js +25 -6
  29. package/dist/config/config-field-policy.js +17 -5
  30. package/dist/config/config-manager.js +198 -22
  31. package/dist/config/config-operation-service.js +35 -38
  32. package/dist/config/contact-bind-code.js +274 -0
  33. package/dist/config/contact-book-store.js +173 -5
  34. package/dist/config/contact-book.js +32 -0
  35. package/dist/config/contact-operation-service.js +9 -3
  36. package/dist/config/contact-request-service.js +331 -0
  37. package/dist/config/peer-role-resolver.js +3 -1
  38. package/dist/config/schema-registry.js +49 -24
  39. package/dist/config-store.js +14 -2
  40. package/dist/core/auth/agent-delegation.js +105 -11
  41. package/dist/core/auth/authorization-audit.js +6 -0
  42. package/dist/core/auth/operation-authorizer.js +8 -0
  43. package/dist/core/auth/operation-catalog.js +51 -3
  44. package/dist/core/auth/trigger-authorization.js +15 -0
  45. package/dist/core/bootstrap-service.js +2 -9
  46. package/dist/core/channel-loader.js +6 -2
  47. package/dist/core/command/command-handler.js +10 -13
  48. package/dist/core/command/connect-menu.js +249 -24
  49. package/dist/core/command/evol-menu-version-gate.js +38 -0
  50. package/dist/core/command/group-menu.js +421 -0
  51. package/dist/core/command/menu-handler.js +269 -85
  52. package/dist/core/command/menu-protocol.js +8 -2
  53. package/dist/core/command/menu-token-store.js +102 -0
  54. package/dist/core/command/slash-gate.js +1 -1
  55. package/dist/core/command/slash-handler.js +158 -31
  56. package/dist/core/daemon-file-cache.js +28 -0
  57. package/dist/core/event-catalog.js +29 -0
  58. package/dist/core/evolagent-registry.js +4 -39
  59. package/dist/core/handoff/runtime.js +162 -37
  60. package/dist/core/handoff/store.js +46 -2
  61. package/dist/core/handoff/types.js +1 -0
  62. package/dist/core/inference/text-inference.js +16 -74
  63. package/dist/core/message/file-markers.js +7 -0
  64. package/dist/core/message/im-renderer.js +18 -14
  65. package/dist/core/message/inbound-admission.js +124 -0
  66. package/dist/core/message/message-bridge.js +616 -74
  67. package/dist/core/message/message-log.js +1 -0
  68. package/dist/core/message/message-queue.js +185 -10
  69. package/dist/core/message/peer-mode.js +7 -8
  70. package/dist/core/message/response-engine.js +381 -79
  71. package/dist/core/permission/approval-gateway.js +17 -10
  72. package/dist/core/permission/ec-command-parser.js +101 -46
  73. package/dist/core/permission/tool-policy.js +69 -24
  74. package/dist/core/protected-paths.js +36 -11
  75. package/dist/core/session/session-manager.js +25 -3
  76. package/dist/core/session/session-mapper.js +2 -0
  77. package/dist/core/session/session-renew.js +125 -69
  78. package/dist/core/session/session-turn-coordinator.js +5 -1
  79. package/dist/eck/kit-renderer.js +12 -1
  80. package/dist/eck/message-renderer.js +79 -1
  81. package/dist/index.js +224 -89
  82. package/dist/ipc.js +81 -4
  83. package/dist/paths.js +3 -0
  84. package/dist/response-system/config-resolver.js +29 -0
  85. package/dist/response-system/coordinator.js +8 -32
  86. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  87. package/dist/response-system/index.js +1 -0
  88. package/dist/response-system/modes/single-session/index.js +7 -4
  89. package/dist/trigger/parser.js +55 -15
  90. package/dist/trigger/patch.js +4 -1
  91. package/dist/trigger/scheduler.js +441 -39
  92. package/dist/utils/error-dict.json +7 -0
  93. package/dist/utils/evolcore-version.js +21 -0
  94. package/dist/utils/logger.js +2 -2
  95. package/dist/utils/stable-semver.js +21 -0
  96. package/dist/utils/stats.js +33 -9
  97. package/kits/docs/channels/aun.md +4 -13
  98. package/kits/docs/evolcore/INDEX.md +1 -1
  99. package/kits/docs/evolcore/config.md +47 -2
  100. package/kits/docs/evolcore/contact.md +7 -3
  101. package/kits/docs/evolcore/group-rules.md +46 -4
  102. package/kits/docs/evolcore/group.md +4 -4
  103. package/kits/docs/evolcore/msg.md +5 -5
  104. package/kits/docs/evolcore/trigger.md +25 -8
  105. package/kits/docs/path-registry.md +36 -17
  106. package/kits/eck_message_manifest.json +12 -1
  107. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  108. package/kits/rules/01-overview.md +17 -7
  109. package/kits/rules/02-navigation.md +38 -18
  110. package/kits/rules/03-identity.md +28 -24
  111. package/kits/rules/04-relation.md +44 -28
  112. package/kits/rules/05-venue.md +31 -15
  113. package/kits/rules/06-channel.md +11 -7
  114. package/kits/schemas/_meta.json +18 -7
  115. package/kits/schemas/agent-config.schema.7.json +303 -0
  116. package/kits/schemas/agent-config.schema.8.json +304 -0
  117. package/kits/schemas/agent-config.schema.9.json +374 -0
  118. package/kits/schemas/contact-book.schema.3.json +67 -0
  119. package/kits/schemas/daemon.schema.1.json +3 -2
  120. package/kits/schemas/daemon.schema.2.json +101 -0
  121. package/kits/schemas/daemon.schema.3.json +123 -0
  122. package/kits/schemas/defaults.schema.2.json +85 -0
  123. package/kits/schemas/defaults.schema.3.json +73 -0
  124. package/kits/schemas/relation-config.schema.6.json +46 -0
  125. package/kits/schemas/relation-config.schema.7.json +59 -0
  126. package/kits/schemas/single-session.schema.2.json +57 -0
  127. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  128. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  129. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  130. package/kits/templates/roles/admin.json +46 -0
  131. package/kits/templates/roles/member.json +4 -0
  132. package/kits/templates/roles/visitor.json +4 -0
  133. package/kits/templates/system-fragments/channel.md +12 -2
  134. package/kits/templates/system-fragments/identity.md +3 -1
  135. package/kits/templates/system-fragments/relation.md +1 -1
  136. package/kits/templates/system-fragments/session.md +6 -2
  137. package/package.json +4 -2
  138. package/MIGRATION-0.5.0.md +0 -378
  139. package/ROLE_ACCESS_CONTROL.md +0 -174
  140. package/dist/channels/contact-bind-code.js +0 -134
  141. package/dist/channels/wecom-card.js +0 -101
  142. package/dist/channels/wecom-onboarding.js +0 -82
  143. package/dist/channels/wecom-state.js +0 -191
@@ -28,10 +28,13 @@ export class DaemonChannel {
28
28
  const pendingId = ctx.attemptId ?? runId;
29
29
  const messageId = pendingId;
30
30
  const startedAt = Date.now();
31
+ const remainingExecutionMs = ctx.executionDeadlineAt === undefined
32
+ ? this.totalExecutionMs
33
+ : Math.max(1, ctx.executionDeadlineAt - startedAt);
31
34
  const promise = new Promise((resolve) => {
32
35
  const watchdogTimer = setTimeout(() => this.watchdogTimeout(pendingId), this.watchdogMs);
33
36
  watchdogTimer.unref?.();
34
- const totalExecutionTimer = setTimeout(() => this.totalExecutionTimeout(pendingId), this.totalExecutionMs);
37
+ const totalExecutionTimer = setTimeout(() => this.totalExecutionTimeout(pendingId, remainingExecutionMs), remainingExecutionMs);
35
38
  totalExecutionTimer.unref?.();
36
39
  this.pending.set(pendingId, {
37
40
  resolve,
@@ -208,6 +211,17 @@ export class DaemonChannel {
208
211
  : `trigger:${trigger.id}`;
209
212
  return await this.sessionManager.getOrCreateSession(this.channelName, channelId, projectPath, threadId, { channelKey: this.channelKey, peerId: `trigger:${trigger.id}`, peerName: trigger.name }, trigger.name, undefined, 'private', baseagent, trigger.agentAid, this.channelName, 'system');
210
213
  }
214
+ executionDeadlineAt(startedAt = Date.now()) {
215
+ return startedAt + this.totalExecutionMs;
216
+ }
217
+ async getResumableConversationSession(sessionId) {
218
+ const session = await this.sessionManager.getSessionById(sessionId);
219
+ if (!session?.agentSessionId)
220
+ return undefined;
221
+ if (!this.sessionManager.checkSessionFileExists(session.projectPath, session.agentSessionId, session.baseagent))
222
+ return undefined;
223
+ return session;
224
+ }
211
225
  /** Internal Trigger audit hook. It does not emit user-visible output. */
212
226
  recordExecutionAnomaly(runId, value) {
213
227
  const slot = this.pending.get(runId);
@@ -330,17 +344,17 @@ export class DaemonChannel {
330
344
  slot.watchdogTimer = setTimeout(() => this.watchdogTimeout(runId), this.watchdogMs);
331
345
  slot.watchdogTimer.unref?.();
332
346
  }
333
- totalExecutionTimeout(runId) {
347
+ totalExecutionTimeout(runId, totalExecutionMs = this.totalExecutionMs) {
334
348
  const slot = this.pending.get(runId);
335
349
  if (!slot || slot.terminalClaim)
336
350
  return;
337
351
  slot.acc.error = {
338
352
  reason: 'daemon_total_execution_timeout',
339
- text: `daemon conversation exceeded total execution limit of ${this.totalExecutionMs}ms`,
353
+ text: `daemon conversation exceeded total execution limit of ${totalExecutionMs}ms`,
340
354
  };
341
355
  if (!this.claimTerminal(slot, 'timeout', {
342
356
  errorType: 'daemon_total_execution_timeout',
343
- totalExecutionMs: this.totalExecutionMs,
357
+ totalExecutionMs,
344
358
  }))
345
359
  return;
346
360
  this.cancelDeliveryInBackground(slot);
@@ -1,14 +1,14 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { createHash, randomInt } from 'crypto';
3
+ import { createHash } from 'crypto';
4
4
  import { logger } from '../utils/logger.js';
5
5
  import { requireOptional } from '../utils/npm-ops.js';
6
6
  import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
7
7
  import { formatItemsAsText } from '../core/message/items-formatter.js';
8
+ import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
8
9
  import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
9
10
  import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
10
- import { isValidAid } from '../aun/aid/validation.js';
11
- import { bindContactAlias } from '../config/contact-book.js';
11
+ import { CONTACT_BIND_CODE_TTL_MS, CONTACT_BIND_MAX_FAILED_ATTEMPTS, ContactBindCodeRegistry, handleExplicitContactBindCommand, } from '../config/contact-bind-code.js';
12
12
  import { resolvePaths } from '../paths.js';
13
13
  const DINGTALK_QUEUED_EMOTION = 'Pin';
14
14
  const DINGTALK_THINKING_EMOTION = 'BusinessTrip';
@@ -125,129 +125,24 @@ export function parseDingtalkCardCallback(data) {
125
125
  values: params,
126
126
  };
127
127
  }
128
- export const DINGTALK_BIND_CODE_TTL_MS = 10 * 60 * 1000;
129
- export const DINGTALK_BIND_MAX_FAILED_ATTEMPTS = 5;
130
- const pendingContactBinds = new Map();
128
+ export const DINGTALK_BIND_CODE_TTL_MS = CONTACT_BIND_CODE_TTL_MS;
129
+ export const DINGTALK_BIND_MAX_FAILED_ATTEMPTS = CONTACT_BIND_MAX_FAILED_ATTEMPTS;
130
+ const dingtalkContactBinds = new ContactBindCodeRegistry({
131
+ channelType: 'dingtalk',
132
+ displayName: '钉钉',
133
+ initCommand: 'ec init dingtalk',
134
+ });
131
135
  export function registerPendingDingtalkContactBind(req) {
132
- const selfAid = String(req.selfAid || '').trim();
133
- const channelName = String(req.channelName || '').trim();
134
- const primaryId = String(req.primaryId || '').trim();
135
- const now = req.now ?? Date.now();
136
- const code = req.code ? String(req.code).trim() : generateDingtalkBindCode();
137
- if (!isValidAid(selfAid))
138
- return { ok: false, error: `invalid selfAid: ${req.selfAid}` };
139
- if (!channelName)
140
- return { ok: false, error: 'missing channelName' };
141
- if (!isValidAid(primaryId))
142
- return { ok: false, error: `invalid primaryId: ${req.primaryId}` };
143
- if (!/^\d{6}$/.test(code))
144
- return { ok: false, error: 'binding code must be 6 digits' };
145
- const key = pendingBindKey(selfAid, channelName);
146
- const replaced = pendingContactBinds.has(key);
147
- const item = {
148
- selfAid,
149
- channelName,
150
- primaryId,
151
- code,
152
- createdAt: now,
153
- expiresAt: now + DINGTALK_BIND_CODE_TTL_MS,
154
- failedAttempts: 0,
155
- maxFailedAttempts: DINGTALK_BIND_MAX_FAILED_ATTEMPTS,
156
- };
157
- pendingContactBinds.set(key, item);
158
- return { ok: true, code, expiresAt: item.expiresAt, replaced };
136
+ return dingtalkContactBinds.register(req);
159
137
  }
160
138
  export function handlePendingDingtalkContactBindMessage(ctx) {
161
- if (ctx.channelType !== 'dingtalk')
162
- return { handled: false };
163
- const selfAid = String(ctx.selfAid || '').trim();
164
- const channelName = String(ctx.channelName || '').trim();
165
- if (!selfAid || !channelName)
166
- return { handled: false };
167
- const key = pendingBindKey(selfAid, channelName);
168
- const item = pendingContactBinds.get(key);
169
- if (!item)
170
- return { handled: false };
171
- const now = ctx.now ?? Date.now();
172
- if (now >= item.expiresAt) {
173
- pendingContactBinds.delete(key);
174
- return {
175
- handled: true,
176
- status: 'expired',
177
- remainingAttempts: 0,
178
- reply: '钉钉身份绑定码已超时,本次绑定失败。请重新执行 ec init dingtalk 并再次扫码绑定。',
179
- };
180
- }
181
- if (ctx.chatType !== 'private')
182
- return { handled: false };
183
- const input = String(ctx.content ?? '').trim();
184
- if (!/^\d+$/.test(input) || !/^\d{6}$/.test(input)) {
185
- return {
186
- handled: true,
187
- status: 'format',
188
- reply: '请直接发送 6 位数字绑定码。',
189
- remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
190
- };
191
- }
192
- if (input !== item.code) {
193
- item.failedAttempts += 1;
194
- const remaining = Math.max(0, item.maxFailedAttempts - item.failedAttempts);
195
- if (remaining === 0) {
196
- pendingContactBinds.delete(key);
197
- return {
198
- handled: true,
199
- status: 'failed',
200
- remainingAttempts: 0,
201
- reply: '绑定码无效,本次钉钉身份绑定失败。请重新执行 ec init dingtalk 并再次扫码绑定。',
202
- };
203
- }
204
- return {
205
- handled: true,
206
- status: 'wrong-code',
207
- remainingAttempts: remaining,
208
- reply: '绑定码错误,请重新发送 6 位数字绑定码。',
209
- };
210
- }
211
- const actorId = String(ctx.actorId || '').trim();
212
- if (!actorId) {
213
- return {
214
- handled: true,
215
- status: 'missing-actor',
216
- remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
217
- reply: '无法识别当前钉钉发送者身份,未建立绑定。请重新发送绑定码或重新执行绑定流程。',
218
- };
219
- }
220
- try {
221
- bindContactAlias(item.selfAid, item.primaryId, 'dingtalk', actorId, item.channelName);
222
- pendingContactBinds.delete(key);
223
- return {
224
- handled: true,
225
- status: 'bound',
226
- reply: `钉钉身份绑定成功:${item.channelName}:${encodeURIComponent(actorId)} -> ${item.primaryId}`,
227
- };
228
- }
229
- catch (error) {
230
- const message = error instanceof Error ? error.message : String(error);
231
- return {
232
- handled: true,
233
- status: 'write-failed',
234
- remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
235
- reply: `钉钉身份绑定写入失败:${message}`,
236
- };
237
- }
139
+ return handleExplicitContactBindCommand(dingtalkContactBinds, ctx);
238
140
  }
239
- export function getPendingDingtalkContactBind(selfAid, channelName) {
240
- const item = pendingContactBinds.get(pendingBindKey(selfAid, channelName));
241
- return item ? { ...item } : null;
141
+ export function getPendingDingtalkContactBind(selfAid, channelName, primaryId) {
142
+ return dingtalkContactBinds.get(selfAid, channelName, primaryId);
242
143
  }
243
144
  export function clearPendingDingtalkContactBinds() {
244
- pendingContactBinds.clear();
245
- }
246
- function generateDingtalkBindCode() {
247
- return String(randomInt(0, 1_000_000)).padStart(6, '0');
248
- }
249
- function pendingBindKey(selfAid, channelName) {
250
- return `${String(selfAid || '').trim()}\u0000${String(channelName || '').trim()}`;
145
+ dingtalkContactBinds.clear();
251
146
  }
252
147
  // ── Webhook SSRF validation ────────────────────────────────────────────────────
253
148
  const WEBHOOK_RE = /^https:\/\/(api|oapi)\.dingtalk\.com\//;
@@ -268,6 +163,9 @@ export class DingtalkChannel {
268
163
  queuedReactions = new Map();
269
164
  thinkingReactions = new Map();
270
165
  taskReactionMessages = new Map();
166
+ // A resumed task has no inbound event in this process. Retain the persisted
167
+ // chat route used to create its replacement and terminal reactions.
168
+ reactionConversationIds = new Map();
271
169
  seenMessages = new Map();
272
170
  interactionCallback;
273
171
  interactionInvalidationCallback;
@@ -813,16 +711,26 @@ export class DingtalkChannel {
813
711
  await this.ensureReaction(messageId, DINGTALK_QUEUED_EMOTION, this.queuedReactions);
814
712
  }
815
713
  /** Upgrade Queued to Thinking without leaving a visible state gap. */
816
- async promoteAckReaction(messageId, taskId, sourceMessageIds) {
714
+ async promoteAckReaction(messageId, taskId, sourceMessageIds, restartResume = false, channelId) {
817
715
  const messageIds = [...new Set([...(sourceMessageIds ?? []), messageId].filter(Boolean))];
818
716
  if (taskId)
819
717
  this.taskReactionMessages.set(taskId, messageIds);
718
+ const fallbackConversationId = this.conversationIdFor(channelId);
719
+ if (fallbackConversationId) {
720
+ for (const id of messageIds)
721
+ this.reactionConversationIds.set(id, fallbackConversationId);
722
+ }
820
723
  // Register every in-flight reaction before yielding. A very short task may
821
724
  // otherwise finalize while later messages in a merged batch are still
822
725
  // untracked, leaving a late BusinessTrip behind.
823
- const thinking = messageIds.map(id => (this.ensureReaction(id, DINGTALK_THINKING_EMOTION, this.thinkingReactions)));
726
+ const thinking = messageIds.map(id => (this.ensureReaction(id, DINGTALK_THINKING_EMOTION, this.thinkingReactions, fallbackConversationId)));
824
727
  await Promise.all(thinking);
825
- await Promise.all(messageIds.map(id => (this.clearReaction(id, DINGTALK_QUEUED_EMOTION, this.queuedReactions))));
728
+ if (restartResume) {
729
+ // Daemon shutdown finalized the old task as Wrong. DingTalk can recall a
730
+ // built-in reaction by name, so no process-local reaction ID is needed.
731
+ await Promise.all(messageIds.map(id => (this.sendEmotion(id, DINGTALK_WRONG_EMOTION, true, fallbackConversationId))));
732
+ }
733
+ await Promise.all(messageIds.map(id => (this.clearReaction(id, DINGTALK_QUEUED_EMOTION, this.queuedReactions, fallbackConversationId))));
826
734
  }
827
735
  /** Clear processing reactions and replace them with Done or Wrong. */
828
736
  async completeAckReaction(taskId, completed) {
@@ -831,24 +739,31 @@ export class DingtalkChannel {
831
739
  return;
832
740
  this.taskReactionMessages.delete(taskId);
833
741
  for (const messageId of messageIds) {
742
+ const fallbackConversationId = this.reactionConversationIds.get(messageId);
834
743
  // Add the terminal state first so users never see an empty reaction gap.
835
- let terminalAdded = await this.sendEmotion(messageId, completed ? DINGTALK_DONE_EMOTION : DINGTALK_WRONG_EMOTION, false);
744
+ let terminalAdded = await this.sendEmotion(messageId, completed ? DINGTALK_DONE_EMOTION : DINGTALK_WRONG_EMOTION, false, fallbackConversationId);
836
745
  if (!terminalAdded) {
837
- terminalAdded = await this.sendEmotion(messageId, completed ? DINGTALK_DONE_EMOTION : DINGTALK_WRONG_EMOTION, false);
746
+ terminalAdded = await this.sendEmotion(messageId, completed ? DINGTALK_DONE_EMOTION : DINGTALK_WRONG_EMOTION, false, fallbackConversationId);
838
747
  }
839
748
  if (!terminalAdded) {
840
749
  logger.warn(`[DingTalk] Failed to add terminal reaction for ${messageId}; preserving processing reaction`);
841
750
  continue;
842
751
  }
843
- await this.clearReaction(messageId, DINGTALK_QUEUED_EMOTION, this.queuedReactions);
844
- await this.clearReaction(messageId, DINGTALK_THINKING_EMOTION, this.thinkingReactions);
752
+ await this.clearReaction(messageId, DINGTALK_QUEUED_EMOTION, this.queuedReactions, fallbackConversationId);
753
+ await this.clearReaction(messageId, DINGTALK_THINKING_EMOTION, this.thinkingReactions, fallbackConversationId);
754
+ this.reactionConversationIds.delete(messageId);
845
755
  }
846
756
  }
847
- async ensureReaction(messageId, emotionName, reactions) {
757
+ conversationIdFor(channelId) {
758
+ if (!channelId)
759
+ return undefined;
760
+ return this.routes.get(channelId)?.conversationId ?? this.conversationIdCache.get(channelId);
761
+ }
762
+ async ensureReaction(messageId, emotionName, reactions, fallbackConversationId) {
848
763
  const existing = reactions.get(messageId);
849
764
  if (existing)
850
765
  return existing;
851
- const pending = this.sendEmotion(messageId, emotionName, false)
766
+ const pending = this.sendEmotion(messageId, emotionName, false, fallbackConversationId)
852
767
  .then((ok) => {
853
768
  if (!ok)
854
769
  reactions.delete(messageId);
@@ -861,18 +776,18 @@ export class DingtalkChannel {
861
776
  reactions.set(messageId, pending);
862
777
  return pending;
863
778
  }
864
- async clearReaction(messageId, emotionName, reactions) {
779
+ async clearReaction(messageId, emotionName, reactions, fallbackConversationId) {
865
780
  const pending = reactions.get(messageId);
866
781
  if (!pending)
867
782
  return;
868
783
  const added = await pending.catch(() => false);
869
784
  if (added)
870
- await this.sendEmotion(messageId, emotionName, true);
785
+ await this.sendEmotion(messageId, emotionName, true, fallbackConversationId);
871
786
  reactions.delete(messageId);
872
787
  }
873
- async sendEmotion(messageId, emotionName, recall) {
874
- const context = this.messageContextCache.get(messageId);
875
- if (!context) {
788
+ async sendEmotion(messageId, emotionName, recall, fallbackConversationId) {
789
+ const openConversationId = this.messageContextCache.get(messageId)?.openConversationId ?? fallbackConversationId;
790
+ if (!openConversationId) {
876
791
  logger.debug(`[DingTalk] Cannot ${recall ? 'recall' : 'add'} emotion: no context for message ${messageId}`);
877
792
  return false;
878
793
  }
@@ -891,7 +806,7 @@ export class DingtalkChannel {
891
806
  body: JSON.stringify({
892
807
  robotCode: this.config.clientId,
893
808
  openMsgId: messageId,
894
- openConversationId: context.openConversationId,
809
+ openConversationId,
895
810
  // Pin / BusinessTrip / Done / Wrong are DingTalk built-in
896
811
  // reactions. emotionType=2 is for custom text reactions and
897
812
  // causes BusinessTrip to fail with system.error.
@@ -1438,7 +1353,7 @@ export class DingtalkChannelPlugin {
1438
1353
  }
1439
1354
  },
1440
1355
  acknowledge: (messageId) => channel.acknowledgeMessage(messageId),
1441
- promoteAck: (messageId, context) => (channel.promoteAckReaction(messageId, context?.taskId, context?.messageIds)),
1356
+ promoteAck: (messageId, context) => (channel.promoteAckReaction(messageId, context?.taskId, context?.messageIds, context?.restartResume, context?.channelId)),
1442
1357
  onInteraction: (callback) => channel.onInteraction(callback),
1443
1358
  onInteractionInvalidated: (callback) => channel.onInteractionInvalidated(callback),
1444
1359
  invalidateInteraction: (interactionId, reason) => channel.invalidateInteraction(interactionId, reason),
@@ -1458,7 +1373,7 @@ export class DingtalkChannelPlugin {
1458
1373
  return {
1459
1374
  channelType: 'dingtalk', adapter, channel,
1460
1375
  policy,
1461
- options: { fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g, supportsImages: true, flushDelay: inst.flushDelay },
1376
+ options: { fileMarkerPattern: createSendFileMarkerPattern(), supportsImages: true, flushDelay: inst.flushDelay },
1462
1377
  connect: () => channel.connect(),
1463
1378
  disconnect: () => channel.disconnect(),
1464
1379
  onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
@@ -4,9 +4,28 @@ import imageType from 'image-type';
4
4
  import { sanitizeFileName, saveToUploads, bufferToInboundImage } from '../utils/media-cache.js';
5
5
  import { logger } from '../utils/logger.js';
6
6
  import { formatItemsAsText } from '../core/message/items-formatter.js';
7
+ import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
7
8
  import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
8
9
  import { chatDirPath, decodeDirSegment } from '../core/session/session-fs-store.js';
9
10
  import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
11
+ import { ContactBindCodeRegistry, handleExplicitContactBindCommand, } from '../config/contact-bind-code.js';
12
+ const feishuContactBinds = new ContactBindCodeRegistry({
13
+ channelType: 'feishu',
14
+ displayName: '飞书',
15
+ initCommand: 'ec init feishu',
16
+ });
17
+ export function registerPendingFeishuContactBind(req) {
18
+ return feishuContactBinds.register(req);
19
+ }
20
+ export function handlePendingFeishuContactBindMessage(ctx) {
21
+ return handleExplicitContactBindCommand(feishuContactBinds, ctx);
22
+ }
23
+ export function getPendingFeishuContactBind(selfAid, channelName, primaryId) {
24
+ return feishuContactBinds.get(selfAid, channelName, primaryId);
25
+ }
26
+ export function clearPendingFeishuContactBinds() {
27
+ feishuContactBinds.clear();
28
+ }
10
29
  export class FeishuChannel {
11
30
  config;
12
31
  agentAid;
@@ -1307,7 +1326,7 @@ export class FeishuChannel {
1307
1326
  this.pinReactions.set(messageId, p);
1308
1327
  }
1309
1328
  /** Runner 开始执行时:添加 StatusInFlight,然后移除 Pin(视觉无闪烁) */
1310
- async promoteAckReaction(messageId, taskId, sourceMessageIds) {
1329
+ async promoteAckReaction(messageId, taskId, sourceMessageIds, restartResume = false) {
1311
1330
  const messageIds = [...new Set([...(sourceMessageIds ?? []), messageId].filter(Boolean))];
1312
1331
  if (taskId)
1313
1332
  this.taskAckMessages.set(taskId, messageIds);
@@ -1328,6 +1347,8 @@ export class FeishuChannel {
1328
1347
  // Every in-flight reaction is registered before this first await, so a
1329
1348
  // fast terminal event cannot race ahead and strand a merged-batch status.
1330
1349
  for (const { id, pending } of promotions) {
1350
+ if (restartResume)
1351
+ await this.clearRestartCrossMark(id);
1331
1352
  const reactionId = await pending;
1332
1353
  if (reactionId) {
1333
1354
  await this.client.im.messageReaction.delete({
@@ -1336,6 +1357,31 @@ export class FeishuChannel {
1336
1357
  }
1337
1358
  }
1338
1359
  }
1360
+ /**
1361
+ * A daemon restart terminates the old task with CrossMark. The reaction IDs
1362
+ * are process-local, so a resumed process must discover and remove its own
1363
+ * stale terminal reaction before it can show the new task state.
1364
+ */
1365
+ async clearRestartCrossMark(messageId) {
1366
+ const reactions = this.client?.im?.messageReaction;
1367
+ if (!reactions?.list || !reactions?.delete)
1368
+ return;
1369
+ try {
1370
+ const response = await reactions.list({
1371
+ path: { message_id: messageId },
1372
+ params: { reaction_type: 'CrossMark', page_size: 50 },
1373
+ });
1374
+ const staleReactionIds = (response?.data?.items ?? [])
1375
+ .filter((reaction) => (reaction?.reaction_type?.emoji_type === 'CrossMark'
1376
+ && reaction?.operator?.operator_type === 'app'
1377
+ && typeof reaction?.reaction_id === 'string'))
1378
+ .map((reaction) => reaction.reaction_id);
1379
+ await Promise.all(staleReactionIds.map(reactionId => (reactions.delete({ path: { message_id: messageId, reaction_id: reactionId } }).catch(() => { }))));
1380
+ }
1381
+ catch {
1382
+ // A missing reaction-list permission must not block the resumed task.
1383
+ }
1384
+ }
1339
1385
  /** 任务终态:用 CheckMark/CrossMark 替换 StatusInFlight。 */
1340
1386
  async finalizeAckReaction(taskId, terminalEmoji) {
1341
1387
  const messageIds = this.taskAckMessages.get(taskId);
@@ -1820,7 +1866,7 @@ export class FeishuChannelPlugin {
1820
1866
  }
1821
1867
  },
1822
1868
  acknowledge: (messageId) => { channel.addPinReaction(messageId); return Promise.resolve(); },
1823
- promoteAck: (messageId, context) => (channel.promoteAckReaction(messageId, context?.taskId, context?.messageIds)),
1869
+ promoteAck: (messageId, context) => (channel.promoteAckReaction(messageId, context?.taskId, context?.messageIds, context?.restartResume)),
1824
1870
  onInteraction: (callback) => channel.onInteraction(callback),
1825
1871
  onInteractionInvalidated: (callback) => channel.onInteractionInvalidated(callback),
1826
1872
  invalidateInteraction: (interactionId, reason) => channel.invalidateInteraction(interactionId, reason),
@@ -1840,17 +1886,23 @@ export class FeishuChannelPlugin {
1840
1886
  return {
1841
1887
  channelType: 'feishu', adapter, channel,
1842
1888
  policy,
1843
- options: { fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g, supportsImages: true, flushDelay: inst.flushDelay },
1889
+ options: { fileMarkerPattern: createSendFileMarkerPattern(), supportsImages: true, flushDelay: inst.flushDelay },
1844
1890
  connect: () => channel.connect(),
1845
1891
  disconnect: () => channel.disconnect(),
1846
1892
  onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
1847
1893
  registerBridge(bridge, channelType) {
1848
1894
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, mentionAids, threadId, rootId, chatType, source }) => {
1895
+ const replyContext = peerId || threadId
1896
+ ? {
1897
+ ...(peerId ? { peerId } : {}),
1898
+ ...(threadId ? { threadId, replyToMessageId: rootId ?? threadId, replyInThread: true } : {}),
1899
+ }
1900
+ : undefined;
1849
1901
  await handler({
1850
1902
  channel: adapter.channelName, channelType, channelId: chatId, content, images,
1851
1903
  selfAID: ctx.agentName, chatType: chatType || 'private',
1852
1904
  peerId: peerId || '', peerName, messageId, mentions, mentionAids, threadId,
1853
- replyContext: threadId ? { threadId, replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
1905
+ replyContext,
1854
1906
  source,
1855
1907
  });
1856
1908
  }), async (channelId, text, replyContext) => {
@@ -3,7 +3,26 @@ import { markdownToPlainText } from '../utils/markdown-to-plain-text.js';
3
3
  import { requireOptional } from '../utils/npm-ops.js';
4
4
  import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
5
5
  import { formatItemsAsText } from '../core/message/items-formatter.js';
6
+ import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
6
7
  import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
8
+ import { ContactBindCodeRegistry, handleExplicitContactBindCommand, } from '../config/contact-bind-code.js';
9
+ const qqbotContactBinds = new ContactBindCodeRegistry({
10
+ channelType: 'qqbot',
11
+ displayName: 'QQ',
12
+ initCommand: 'ec init qqbot',
13
+ });
14
+ export function registerPendingQQBotContactBind(req) {
15
+ return qqbotContactBinds.register(req);
16
+ }
17
+ export function handlePendingQQBotContactBindMessage(ctx) {
18
+ return handleExplicitContactBindCommand(qqbotContactBinds, ctx);
19
+ }
20
+ export function getPendingQQBotContactBind(selfAid, channelName, primaryId) {
21
+ return qqbotContactBinds.get(selfAid, channelName, primaryId);
22
+ }
23
+ export function clearPendingQQBotContactBinds() {
24
+ qqbotContactBinds.clear();
25
+ }
7
26
  // ── QQBotChannel ────────────────────────────────────────────────────────────
8
27
  export class QQBotChannel {
9
28
  agentAid;
@@ -93,6 +112,9 @@ export class QQBotChannel {
93
112
  }
94
113
  logger.info('[QQBot] Disconnected');
95
114
  }
115
+ getStatus() {
116
+ return { connected: this.connected };
117
+ }
96
118
  onMessage(handler) {
97
119
  this.messageHandler = handler;
98
120
  }
@@ -390,7 +412,7 @@ export class QQBotChannelPlugin {
390
412
  return {
391
413
  channelType: 'qqbot', adapter, channel,
392
414
  policy,
393
- options: { fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g, supportsImages: true, flushDelay: inst.flushDelay },
415
+ options: { fileMarkerPattern: createSendFileMarkerPattern(), supportsImages: true, flushDelay: inst.flushDelay },
394
416
  connect: () => channel.connect(),
395
417
  disconnect: () => channel.disconnect(),
396
418
  onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),