evolcore 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +83 -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/daemon-commands.js +46 -15
  22. package/dist/cli/handoff-command.js +2 -1
  23. package/dist/cli/init-channel.js +185 -67
  24. package/dist/cli/init.js +49 -27
  25. package/dist/cli/trigger-command.js +92 -11
  26. package/dist/config/access-policy-domain.js +18 -0
  27. package/dist/config/access-policy.js +110 -0
  28. package/dist/config/builtin-role-templates.js +27 -14
  29. package/dist/config/builtin-roles.js +25 -6
  30. package/dist/config/config-field-policy.js +17 -5
  31. package/dist/config/config-manager.js +198 -22
  32. package/dist/config/config-operation-service.js +35 -38
  33. package/dist/config/contact-bind-code.js +274 -0
  34. package/dist/config/contact-book-store.js +173 -5
  35. package/dist/config/contact-book.js +32 -0
  36. package/dist/config/contact-operation-service.js +9 -3
  37. package/dist/config/contact-request-service.js +331 -0
  38. package/dist/config/peer-role-resolver.js +3 -1
  39. package/dist/config/schema-registry.js +49 -24
  40. package/dist/config-store.js +14 -2
  41. package/dist/core/auth/agent-delegation.js +105 -11
  42. package/dist/core/auth/authorization-audit.js +6 -0
  43. package/dist/core/auth/operation-authorizer.js +8 -0
  44. package/dist/core/auth/operation-catalog.js +51 -3
  45. package/dist/core/auth/trigger-authorization.js +15 -0
  46. package/dist/core/bootstrap-service.js +2 -9
  47. package/dist/core/channel-loader.js +6 -2
  48. package/dist/core/command/command-handler.js +10 -13
  49. package/dist/core/command/connect-menu.js +229 -26
  50. package/dist/core/command/evol-menu-version-gate.js +38 -0
  51. package/dist/core/command/group-menu.js +421 -0
  52. package/dist/core/command/menu-handler.js +269 -85
  53. package/dist/core/command/menu-protocol.js +8 -2
  54. package/dist/core/command/menu-token-store.js +102 -0
  55. package/dist/core/command/role-menu.js +16 -0
  56. package/dist/core/command/slash-gate.js +1 -1
  57. package/dist/core/command/slash-handler.js +158 -31
  58. package/dist/core/daemon-file-cache.js +28 -0
  59. package/dist/core/event-catalog.js +29 -0
  60. package/dist/core/evolagent-registry.js +4 -39
  61. package/dist/core/handoff/runtime.js +162 -37
  62. package/dist/core/handoff/store.js +46 -2
  63. package/dist/core/handoff/types.js +1 -0
  64. package/dist/core/inference/text-inference.js +16 -74
  65. package/dist/core/message/file-markers.js +7 -0
  66. package/dist/core/message/im-renderer.js +18 -14
  67. package/dist/core/message/inbound-admission.js +134 -0
  68. package/dist/core/message/message-bridge.js +616 -74
  69. package/dist/core/message/message-log.js +1 -0
  70. package/dist/core/message/message-queue.js +185 -10
  71. package/dist/core/message/peer-mode.js +7 -8
  72. package/dist/core/message/response-engine.js +381 -79
  73. package/dist/core/permission/approval-gateway.js +17 -10
  74. package/dist/core/permission/ec-command-parser.js +101 -46
  75. package/dist/core/permission/tool-policy.js +69 -24
  76. package/dist/core/protected-paths.js +36 -11
  77. package/dist/core/session/session-fs-store.js +19 -4
  78. package/dist/core/session/session-manager.js +232 -4
  79. package/dist/core/session/session-mapper.js +2 -0
  80. package/dist/core/session/session-renew.js +125 -69
  81. package/dist/core/session/session-turn-coordinator.js +5 -1
  82. package/dist/eck/kit-renderer.js +12 -1
  83. package/dist/eck/message-renderer.js +79 -1
  84. package/dist/index.js +224 -89
  85. package/dist/ipc.js +81 -4
  86. package/dist/paths.js +3 -0
  87. package/dist/response-system/config-resolver.js +29 -0
  88. package/dist/response-system/coordinator.js +8 -32
  89. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  90. package/dist/response-system/index.js +1 -0
  91. package/dist/response-system/modes/single-session/index.js +7 -4
  92. package/dist/trigger/parser.js +55 -15
  93. package/dist/trigger/patch.js +4 -1
  94. package/dist/trigger/scheduler.js +441 -39
  95. package/dist/utils/cross-platform.js +8 -2
  96. package/dist/utils/error-dict.json +7 -0
  97. package/dist/utils/evolcore-version.js +21 -0
  98. package/dist/utils/logger.js +2 -2
  99. package/dist/utils/process-introspect.js +19 -3
  100. package/dist/utils/stable-semver.js +21 -0
  101. package/dist/utils/stats.js +33 -9
  102. package/kits/docs/channels/aun.md +4 -13
  103. package/kits/docs/evolcore/INDEX.md +1 -1
  104. package/kits/docs/evolcore/config.md +47 -2
  105. package/kits/docs/evolcore/contact.md +8 -3
  106. package/kits/docs/evolcore/group-rules.md +46 -4
  107. package/kits/docs/evolcore/group.md +4 -4
  108. package/kits/docs/evolcore/msg.md +5 -5
  109. package/kits/docs/evolcore/trigger.md +25 -8
  110. package/kits/docs/path-registry.md +36 -17
  111. package/kits/eck_message_manifest.json +12 -1
  112. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  113. package/kits/rules/01-overview.md +17 -7
  114. package/kits/rules/02-navigation.md +38 -18
  115. package/kits/rules/03-identity.md +28 -24
  116. package/kits/rules/04-relation.md +44 -28
  117. package/kits/rules/05-venue.md +31 -15
  118. package/kits/rules/06-channel.md +11 -7
  119. package/kits/schemas/_meta.json +18 -7
  120. package/kits/schemas/agent-config.schema.7.json +303 -0
  121. package/kits/schemas/agent-config.schema.8.json +304 -0
  122. package/kits/schemas/agent-config.schema.9.json +364 -0
  123. package/kits/schemas/contact-book.schema.3.json +67 -0
  124. package/kits/schemas/daemon.schema.1.json +3 -2
  125. package/kits/schemas/daemon.schema.2.json +101 -0
  126. package/kits/schemas/daemon.schema.3.json +123 -0
  127. package/kits/schemas/defaults.schema.2.json +85 -0
  128. package/kits/schemas/defaults.schema.3.json +73 -0
  129. package/kits/schemas/relation-config.schema.6.json +46 -0
  130. package/kits/schemas/relation-config.schema.7.json +59 -0
  131. package/kits/schemas/single-session.schema.2.json +57 -0
  132. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  133. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  134. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  135. package/kits/templates/roles/admin.json +46 -0
  136. package/kits/templates/roles/member.json +4 -0
  137. package/kits/templates/roles/visitor.json +4 -0
  138. package/kits/templates/system-fragments/channel.md +12 -2
  139. package/kits/templates/system-fragments/identity.md +3 -1
  140. package/kits/templates/system-fragments/relation.md +1 -1
  141. package/kits/templates/system-fragments/session.md +6 -2
  142. package/package.json +4 -2
  143. package/MIGRATION-0.5.0.md +0 -378
  144. package/ROLE_ACCESS_CONTROL.md +0 -174
  145. package/dist/channels/contact-bind-code.js +0 -134
  146. package/dist/channels/wecom-card.js +0 -101
  147. package/dist/channels/wecom-onboarding.js +0 -82
  148. package/dist/channels/wecom-state.js +0 -191
@@ -1,13 +1,17 @@
1
1
  import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
2
5
  import { logger } from '../utils/logger.js';
3
6
  import { requireOptional } from '../utils/npm-ops.js';
4
7
  import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
5
8
  import { formatItemsAsText } from '../core/message/items-formatter.js';
9
+ import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
6
10
  import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
7
11
  import { bufferToInboundImage, sanitizeFileName, saveToUploads, validateUrl } from '../utils/media-cache.js';
8
- import { PersistentWecomCardStore, PersistentWecomDeduper, createWecomCardTaskId, wecomChannelDataPaths, } from './wecom-state.js';
9
- import { buildWecomInteractionCard, buildWecomTerminalCard, buildWecomUnknownCard, parseWecomCardResponse, } from './wecom-card.js';
10
- import { ContactBindCodeRegistry, } from './contact-bind-code.js';
12
+ import { agentDataDir } from '../paths.js';
13
+ import { atomicReadJson, atomicWriteJson } from '../utils/atomic-write.js';
14
+ import { ContactBindCodeRegistry, handleExplicitContactBindCommand, } from '../config/contact-bind-code.js';
11
15
  function wecomErrorCode(error) {
12
16
  if (!error || typeof error !== 'object')
13
17
  return undefined;
@@ -93,14 +97,384 @@ export function registerPendingWecomContactBind(req) {
93
97
  return wecomContactBinds.register(req);
94
98
  }
95
99
  export function handlePendingWecomContactBindMessage(ctx) {
96
- return wecomContactBinds.handle(ctx);
100
+ return handleExplicitContactBindCommand(wecomContactBinds, ctx);
97
101
  }
98
- export function getPendingWecomContactBind(selfAid, channelName) {
99
- return wecomContactBinds.get(selfAid, channelName);
102
+ export function getPendingWecomContactBind(selfAid, channelName, primaryId) {
103
+ return wecomContactBinds.get(selfAid, channelName, primaryId);
100
104
  }
101
105
  export function clearPendingWecomContactBinds() {
102
106
  wecomContactBinds.clear();
103
107
  }
108
+ // ── Persistent channel state ──────────────────────────────────────────────────
109
+ const STORE_VERSION = 1;
110
+ const DEFAULT_DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
111
+ const DEFAULT_CARD_RETENTION_MS = 24 * 60 * 60 * 1000;
112
+ function writePrivateJson(filePath, value) {
113
+ atomicWriteJson(filePath, value);
114
+ try {
115
+ fs.chmodSync(filePath, 0o600);
116
+ }
117
+ catch { /* best effort on non-POSIX filesystems */ }
118
+ }
119
+ export class PersistentWecomDeduper {
120
+ filePath;
121
+ channelKey;
122
+ entries = new Map();
123
+ ttlMs;
124
+ maxEntries;
125
+ constructor(filePath, channelKey, opts) {
126
+ this.filePath = filePath;
127
+ this.channelKey = channelKey;
128
+ this.ttlMs = opts?.ttlMs ?? DEFAULT_DEDUP_TTL_MS;
129
+ this.maxEntries = opts?.maxEntries ?? 20_000;
130
+ this.load();
131
+ }
132
+ checkAndMark(messageId, now = Date.now()) {
133
+ this.evict(now);
134
+ const seenAt = this.entries.get(messageId);
135
+ if (seenAt !== undefined && now - seenAt <= this.ttlMs)
136
+ return true;
137
+ this.entries.set(messageId, now);
138
+ this.trimToLimit();
139
+ this.flush();
140
+ return false;
141
+ }
142
+ get size() {
143
+ return this.entries.size;
144
+ }
145
+ load() {
146
+ if (!this.filePath)
147
+ return;
148
+ try {
149
+ const stored = atomicReadJson(this.filePath);
150
+ if (!stored || stored.version !== STORE_VERSION || stored.channelKey !== this.channelKey)
151
+ return;
152
+ for (const [id, timestamp] of Object.entries(stored.entries ?? {})) {
153
+ if (id && Number.isFinite(timestamp))
154
+ this.entries.set(id, timestamp);
155
+ }
156
+ const before = this.entries.size;
157
+ this.evict(Date.now());
158
+ this.trimToLimit();
159
+ if (this.entries.size !== before)
160
+ this.flush();
161
+ }
162
+ catch (error) {
163
+ logger.warn(`[WeCom] Ignoring unreadable dedup store for ${this.channelKey}: ${String(error)}`);
164
+ this.entries.clear();
165
+ }
166
+ }
167
+ evict(now) {
168
+ for (const [id, timestamp] of this.entries) {
169
+ if (now - timestamp > this.ttlMs)
170
+ this.entries.delete(id);
171
+ }
172
+ }
173
+ trimToLimit() {
174
+ if (this.entries.size <= this.maxEntries)
175
+ return;
176
+ const oldest = [...this.entries.entries()].sort((a, b) => a[1] - b[1]);
177
+ for (let i = 0; i < oldest.length - this.maxEntries; i++) {
178
+ this.entries.delete(oldest[i][0]);
179
+ }
180
+ }
181
+ flush() {
182
+ writePrivateJson(this.filePath, {
183
+ version: STORE_VERSION,
184
+ channelKey: this.channelKey,
185
+ entries: Object.fromEntries(this.entries),
186
+ });
187
+ }
188
+ }
189
+ export class PersistentWecomCardStore {
190
+ filePath;
191
+ channelKey;
192
+ records = new Map();
193
+ constructor(filePath, channelKey) {
194
+ this.filePath = filePath;
195
+ this.channelKey = channelKey;
196
+ if (filePath)
197
+ this.load();
198
+ }
199
+ set(record) {
200
+ this.records.set(record.taskId, record);
201
+ this.cleanup();
202
+ this.flush();
203
+ }
204
+ get(taskId) {
205
+ this.cleanup();
206
+ return this.records.get(taskId);
207
+ }
208
+ findByInteractionId(interactionId) {
209
+ this.cleanup();
210
+ return [...this.records.values()].find(record => record.interaction.id === interactionId);
211
+ }
212
+ listPendingByChat(chatId) {
213
+ this.cleanup();
214
+ return [...this.records.values()].filter(record => (record.chatId === chatId
215
+ && record.status === 'pending'
216
+ && (!record.expiresAt || record.expiresAt >= Date.now())));
217
+ }
218
+ resolve(taskId, action) {
219
+ const record = this.records.get(taskId);
220
+ if (!record)
221
+ return undefined;
222
+ record.status = 'resolved';
223
+ record.resolution = action;
224
+ record.updatedAt = Date.now();
225
+ this.flush();
226
+ return record;
227
+ }
228
+ invalidateByInteractionId(interactionId, reason) {
229
+ const record = this.findByInteractionId(interactionId);
230
+ if (!record || record.status === 'resolved')
231
+ return record;
232
+ record.status = 'invalidated';
233
+ record.invalidationReason = reason;
234
+ record.updatedAt = Date.now();
235
+ this.flush();
236
+ return record;
237
+ }
238
+ load() {
239
+ if (!this.filePath)
240
+ return;
241
+ try {
242
+ const stored = atomicReadJson(this.filePath);
243
+ if (!stored || stored.version !== STORE_VERSION || stored.channelKey !== this.channelKey)
244
+ return;
245
+ for (const record of stored.records ?? []) {
246
+ if (record?.taskId && record?.interaction?.id)
247
+ this.records.set(record.taskId, record);
248
+ }
249
+ this.cleanup(true);
250
+ }
251
+ catch (error) {
252
+ logger.warn(`[WeCom] Ignoring unreadable card store for ${this.channelKey}: ${String(error)}`);
253
+ this.records.clear();
254
+ }
255
+ }
256
+ cleanup(flushWhenChanged = false) {
257
+ const now = Date.now();
258
+ let changed = false;
259
+ for (const [taskId, record] of this.records) {
260
+ const terminalExpiry = record.updatedAt + DEFAULT_CARD_RETENTION_MS;
261
+ const pendingExpiry = (record.expiresAt ?? record.createdAt + 7 * DEFAULT_CARD_RETENTION_MS)
262
+ + DEFAULT_CARD_RETENTION_MS;
263
+ const expiresAt = record.status === 'pending' ? pendingExpiry : terminalExpiry;
264
+ if (now > expiresAt) {
265
+ this.records.delete(taskId);
266
+ changed = true;
267
+ }
268
+ }
269
+ if (changed && flushWhenChanged)
270
+ this.flush();
271
+ }
272
+ flush() {
273
+ if (!this.filePath)
274
+ return;
275
+ writePrivateJson(this.filePath, {
276
+ version: STORE_VERSION,
277
+ channelKey: this.channelKey,
278
+ records: [...this.records.values()],
279
+ });
280
+ }
281
+ }
282
+ export function wecomChannelDataPaths(agentAid, channelKey) {
283
+ const safeKey = encodeURIComponent(channelKey);
284
+ const dir = path.join(agentDataDir(agentAid), 'channels', safeKey);
285
+ return {
286
+ dedupFile: path.join(dir, 'wecom-message-dedup.json'),
287
+ cardFile: path.join(dir, 'wecom-cards.json'),
288
+ };
289
+ }
290
+ export function createWecomCardTaskId(channelKey, interactionId) {
291
+ const hash = crypto.createHash('sha256').update(`${channelKey}\0${interactionId}`).digest('hex').slice(0, 32);
292
+ return `ec_${hash}`;
293
+ }
294
+ // ── Template cards ────────────────────────────────────────────────────────────
295
+ function truncate(value, max) {
296
+ if (!value)
297
+ return undefined;
298
+ const chars = [...value.trim()];
299
+ return chars.length <= max ? value.trim() : `${chars.slice(0, Math.max(1, max - 1)).join('')}...`;
300
+ }
301
+ function buttonStyle(style) {
302
+ if (style === 'primary')
303
+ return 2;
304
+ if (style === 'danger')
305
+ return 3;
306
+ return 1;
307
+ }
308
+ export function buildWecomInteractionCard(interaction, taskId) {
309
+ const bodyParts = [];
310
+ if (interaction.kind.body)
311
+ bodyParts.push(interaction.kind.body);
312
+ if (interaction.kind.kind === 'action' && interaction.kind.checkers?.length) {
313
+ bodyParts.push(interaction.kind.checkers.map((item, index) => `${index + 1}. ${item.label}`).join('\n'));
314
+ }
315
+ return {
316
+ card_type: 'button_interaction',
317
+ task_id: taskId,
318
+ main_title: {
319
+ title: truncate(interaction.kind.title, 26),
320
+ desc: truncate(bodyParts.join('\n\n'), 120),
321
+ },
322
+ button_list: interaction.kind.buttons
323
+ .map((button, index) => ({ button, index }))
324
+ .filter(({ button }) => !('disabled' in button) || !button.disabled)
325
+ .slice(0, 6)
326
+ .map(({ button, index }) => ({
327
+ text: truncate(button.label, 10) || `选项 ${index + 1}`,
328
+ style: buttonStyle(button.style),
329
+ key: interaction.kind.kind === 'command-card' ? `cmd_${index}` : `act_${index}`,
330
+ })),
331
+ };
332
+ }
333
+ export function parseWecomCardResponse(record, event, operatorId) {
334
+ const eventKey = String(event.event_key ?? '').trim();
335
+ const match = /^(cmd|act)_(\d+)$/.exec(eventKey);
336
+ if (!match)
337
+ return null;
338
+ const index = Number(match[2]);
339
+ if (record.interaction.kind.kind === 'command-card') {
340
+ const button = record.interaction.kind.buttons[index];
341
+ if (!button || button.disabled)
342
+ return null;
343
+ return { command: button.command };
344
+ }
345
+ const button = record.interaction.kind.buttons[index];
346
+ if (!button)
347
+ return null;
348
+ const values = {};
349
+ const selectedItems = event.selected_items?.selected_item;
350
+ if (Array.isArray(selectedItems)) {
351
+ for (const selected of selectedItems) {
352
+ const questionKey = String(selected?.question_key ?? '').trim();
353
+ const optionIds = selected?.option_ids?.option_id;
354
+ if (questionKey && Array.isArray(optionIds))
355
+ values[questionKey] = optionIds.filter(Boolean);
356
+ }
357
+ }
358
+ return {
359
+ response: {
360
+ type: 'interaction.response',
361
+ id: record.interaction.id,
362
+ action: button.key,
363
+ values: Object.keys(values).length > 0 ? values : undefined,
364
+ operatorId,
365
+ },
366
+ };
367
+ }
368
+ export function buildWecomTerminalCard(record, status, detail) {
369
+ const statusText = status === 'resolved'
370
+ ? `已处理${detail ? `:${detail}` : ''}`
371
+ : status === 'rejected'
372
+ ? '仅卡片发起者可操作'
373
+ : status === 'unavailable'
374
+ ? '处理服务暂不可用,请重新发起'
375
+ : `卡片已失效${detail ? `:${detail}` : ''}`;
376
+ return {
377
+ card_type: 'text_notice',
378
+ task_id: record.taskId,
379
+ main_title: {
380
+ title: truncate(record.interaction.kind.title, 26),
381
+ desc: truncate(statusText, 120),
382
+ },
383
+ sub_title_text: truncate(record.interaction.kind.body, 120),
384
+ };
385
+ }
386
+ export function buildWecomUnknownCard(taskId) {
387
+ return {
388
+ card_type: 'text_notice',
389
+ task_id: taskId,
390
+ main_title: {
391
+ title: '卡片已失效',
392
+ desc: '本地状态不存在,请重新发起操作',
393
+ },
394
+ };
395
+ }
396
+ // ── QR onboarding ─────────────────────────────────────────────────────────────
397
+ const WECOM_QR_GENERATE_URL = 'https://work.weixin.qq.com/ai/qc/generate';
398
+ const WECOM_QR_QUERY_URL = 'https://work.weixin.qq.com/ai/qc/query_result';
399
+ const WECOM_QR_PAGE_URL = 'https://work.weixin.qq.com/ai/qc/gen';
400
+ function platformCode() {
401
+ if (os.platform() === 'darwin')
402
+ return 1;
403
+ if (os.platform() === 'win32')
404
+ return 2;
405
+ if (os.platform() === 'linux')
406
+ return 3;
407
+ return 0;
408
+ }
409
+ async function fetchJson(url, fetchImpl, signal) {
410
+ const timeoutSignal = AbortSignal.timeout(15_000);
411
+ const response = await fetchImpl(url, {
412
+ headers: { Accept: 'application/json' },
413
+ signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
414
+ });
415
+ if (!response.ok)
416
+ throw new Error(`企微扫码服务请求失败: HTTP ${response.status}`);
417
+ return response.json();
418
+ }
419
+ export async function createWecomQrSession(fetchImpl = fetch) {
420
+ const url = new URL(WECOM_QR_GENERATE_URL);
421
+ url.searchParams.set('source', 'wecom-cli');
422
+ url.searchParams.set('plat', String(platformCode()));
423
+ const payload = await fetchJson(url, fetchImpl);
424
+ const scode = String(payload?.data?.scode ?? '').trim();
425
+ const authUrl = String(payload?.data?.auth_url ?? '').trim();
426
+ if (!scode || !authUrl)
427
+ throw new Error('企微扫码服务未返回 scode 或授权地址');
428
+ const browserUrl = new URL(WECOM_QR_PAGE_URL);
429
+ browserUrl.searchParams.set('source', 'wecom-cli');
430
+ browserUrl.searchParams.set('scode', scode);
431
+ return { scode, authUrl, browserUrl: browserUrl.toString() };
432
+ }
433
+ export async function pollWecomQrCredentials(scode, opts) {
434
+ const fetchImpl = opts?.fetchImpl ?? fetch;
435
+ const timeoutMs = opts?.timeoutMs ?? 5 * 60 * 1000;
436
+ const intervalMs = opts?.intervalMs ?? 3_000;
437
+ const deadline = Date.now() + timeoutMs;
438
+ const url = new URL(WECOM_QR_QUERY_URL);
439
+ url.searchParams.set('scode', scode);
440
+ let previousStatus = '';
441
+ while (Date.now() < deadline) {
442
+ if (opts?.signal?.aborted)
443
+ throw new Error('企微扫码已取消');
444
+ const payload = await fetchJson(url, fetchImpl, opts?.signal);
445
+ const status = String(payload?.data?.status ?? 'waiting');
446
+ if (status !== previousStatus) {
447
+ opts?.onStatus?.(status);
448
+ previousStatus = status;
449
+ }
450
+ if (status === 'success') {
451
+ const botId = String(payload?.data?.bot_info?.botid ?? '').trim();
452
+ const secret = String(payload?.data?.bot_info?.secret ?? '').trim();
453
+ if (!botId || !secret)
454
+ throw new Error('扫码成功,但企微未返回 Bot ID 或 Secret');
455
+ return { botId, secret };
456
+ }
457
+ if (status === 'expired' || status === 'cancelled' || status === 'rejected') {
458
+ throw new Error(`企微扫码未完成: ${status}`);
459
+ }
460
+ await new Promise((resolve, reject) => {
461
+ const finish = () => {
462
+ opts?.signal?.removeEventListener('abort', onAbort);
463
+ resolve();
464
+ };
465
+ const timer = setTimeout(finish, intervalMs);
466
+ const onAbort = () => {
467
+ clearTimeout(timer);
468
+ opts?.signal?.removeEventListener('abort', onAbort);
469
+ reject(new Error('企微扫码已取消'));
470
+ };
471
+ opts?.signal?.addEventListener('abort', onAbort, { once: true });
472
+ if (opts?.signal?.aborted)
473
+ onAbort();
474
+ });
475
+ }
476
+ throw new Error('企微扫码超时,请重新生成二维码');
477
+ }
104
478
  // ── WecomChannel ───────────────────────────────────────────────────────────────
105
479
  export class WecomChannel {
106
480
  agentAid;
@@ -1082,6 +1456,8 @@ export class WecomChannelPlugin {
1082
1456
  await channel.finishStream(envelope.replyContext);
1083
1457
  return;
1084
1458
  case 'status.interrupted':
1459
+ await channel.finishStream(envelope.replyContext, payload.metadata?.reason === 'daemon_restart' ? '服务正在重启,将继续处理…' : '任务已中止');
1460
+ return;
1085
1461
  case 'status.timeout':
1086
1462
  await channel.finishStream(envelope.replyContext, '任务已中止');
1087
1463
  return;
@@ -1136,7 +1512,7 @@ export class WecomChannelPlugin {
1136
1512
  return {
1137
1513
  channelType: 'wecom', adapter, channel,
1138
1514
  policy,
1139
- options: { fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g, supportsImages: true, flushDelay: inst.flushDelay },
1515
+ options: { fileMarkerPattern: createSendFileMarkerPattern(), supportsImages: true, flushDelay: inst.flushDelay },
1140
1516
  connect: () => channel.connect(),
1141
1517
  disconnect: () => channel.disconnect(),
1142
1518
  onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
@@ -791,7 +791,7 @@ export async function cmdMsg(args) {
791
791
  console.log(`用法: ec msg <command> <from-aid> [args...] [options]
792
792
 
793
793
  Commands:
794
- send <from> <to> <text> 发送文本
794
+ send <from> <to> <text> --return <none|required> 发送文本(跨会话时 --return 必填)
795
795
  send <from> <to> --text-from-file <path> 从文件读取文本内容
796
796
  send <from> <to> --file <path> [--as <type>] 发送文件(image|video|voice|file)
797
797
  send <from> <to> --link <url> [--title T] 发送链接卡片
@@ -814,7 +814,7 @@ Options:
814
814
  --before <time> 只返回该时间之前的历史
815
815
  --after <time> 只返回该时间之后的历史
816
816
  --direction <value> 历史方向:in|out|all
817
- --return <required|none> 跨会话回流策略(跨会话默认 required
817
+ --return <none|required> 跨会话发送必填:none=仅补充目标上下文,required=回答回流来源
818
818
  --content-type <mime> 显式覆盖 MIME(仅 --file 模式)
819
819
  --as <type> 附件类型:image|video|voice|file(仅 --file 模式)
820
820
  --text <说明> 附件说明文字(仅 --file 模式)
@@ -825,12 +825,12 @@ Options:
825
825
  (用于发送恰好等于某 flag 的文本,如 send a b -- --encrypt)
826
826
 
827
827
  示例:
828
- ec msg send alice.agentid.pub bob.agentid.pub "hello"
829
- ec msg send alice.agentid.pub bob.agentid.pub --text-from-file long-message.txt
830
- ec msg send alice.agentid.pub bob.agentid.pub "讨论项目A" --thread "project-A"
831
- ec msg send alice.agentid.pub bob.agentid.pub --file ./pic.png
832
- ec msg send alice.agentid.pub bob.agentid.pub --file ./demo.mp4 --as video
833
- ec msg send alice.agentid.pub bob.agentid.pub --link https://example.com --title "AUN"
828
+ ec msg send alice.agentid.pub bob.agentid.pub "hello" --return none
829
+ ec msg send alice.agentid.pub bob.agentid.pub --text-from-file long-message.txt --return none
830
+ ec msg send alice.agentid.pub bob.agentid.pub "讨论项目A" --thread "project-A" --return required
831
+ ec msg send alice.agentid.pub bob.agentid.pub --file ./pic.png --return none
832
+ ec msg send alice.agentid.pub bob.agentid.pub --file ./demo.mp4 --as video --return none
833
+ ec msg send alice.agentid.pub bob.agentid.pub --link https://example.com --title "AUN" --return none
834
834
  ec msg pull alice.agentid.pub --app my-bot
835
835
  ec msg ack alice.agentid.pub 42 --app my-bot
836
836
  ec msg recall alice.agentid.pub msg-uuid-1 msg-uuid-2
@@ -1234,7 +1234,7 @@ export async function cmdGroup(args) {
1234
1234
  console.log(`用法: ec group <command> <from-aid> [args...] [options]
1235
1235
 
1236
1236
  消息:
1237
- send <from> <group-id> <text> 发送群文本
1237
+ send <from> <group-id> <text> --return <none|required> 发送群文本(跨会话时 --return 必填)
1238
1238
  send <from> <group-id> --file <path> [--as <type>] 发送群文件
1239
1239
  send <from> <group-id> --payload <json> 发送自定义 payload
1240
1240
  pull <from> <group-id> [--after-seq N] [--limit N] 拉取群消息
@@ -1273,7 +1273,7 @@ Options:
1273
1273
  --encrypt 启用端到端加密(仅 send)
1274
1274
  --no-encrypt 强制明文发送(优先于 --encrypt;仅 send)
1275
1275
  --thread <id> 指定话题 ID(仅 send)
1276
- --return <required|none> 跨会话回流策略(仅 send)
1276
+ --return <none|required> 跨会话发送必填(仅 send;Trigger 只能使用 none
1277
1277
  --mention <aid> 发送时 @ 某个成员(可多次,或用逗号分隔多个 aid)
1278
1278
  --mention-all 发送时 @ 所有人
1279
1279
  --content-type <mime> 显式覆盖 MIME(仅 --file 模式)
@@ -1286,7 +1286,7 @@ Options:
1286
1286
 
1287
1287
  格式示例(.example.invalid 是保留的虚构域,以下 AID 均不是真实地址):
1288
1288
  ec group create sender.example.invalid "Dev Team" --visibility private
1289
- ec group send sender.example.invalid 12345.example.invalid "hello team"
1289
+ ec group send sender.example.invalid 12345.example.invalid "hello team" --return none
1290
1290
  ec group send sender.example.invalid 12345.example.invalid "@member 看下 PR" --mention member.example.invalid
1291
1291
  ec group send sender.example.invalid 12345.example.invalid --file ./arch.png
1292
1292
  ec group rules sender.example.invalid 12345.example.invalid set ./rules.md
@@ -29,6 +29,22 @@ import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
29
29
  import { WEB_CLI_BIN, WEB_PACKAGE_LATEST, WEB_PACKAGE_NAME } from '../product.js';
30
30
  import { rotateStdoutLog } from '../utils/log-writer.js';
31
31
  const execFileAsync = promisify(execFile);
32
+ function printNoSelfAgentHints(options) {
33
+ const { daemonConfig, ecwebStarted, skipped } = options;
34
+ console.log('\nℹ 未配置任何 self-agent,Control Plane 已启动。');
35
+ console.log(' 命令行:ec agent new <aid>.agentid.pub');
36
+ if (daemonConfig.aid && (daemonConfig.owners?.length ?? 0) > 0) {
37
+ console.log(' Evol App:可通过进程级菜单创建 agent');
38
+ }
39
+ if (ecwebStarted) {
40
+ console.log(' ECWeb:可通过控制台创建 agent');
41
+ }
42
+ if (skipped.length > 0) {
43
+ console.log('跳过的目录:');
44
+ for (const skippedAgent of skipped)
45
+ console.log(` - ${skippedAgent.dirName}: ${skippedAgent.reason}`);
46
+ }
47
+ }
32
48
  async function probeDaemon(socketPath, timeoutMs = 1000) {
33
49
  const response = await ipcQuery(socketPath, { type: 'ping' }, timeoutMs);
34
50
  if (response?.pong !== true || !Number.isInteger(response.pid) || response.pid <= 0)
@@ -196,8 +212,16 @@ export async function cmdStart(opts = {}) {
196
212
  }
197
213
  else {
198
214
  console.log('⚡ 未检测到初始化配置,自动启动初始化向导...\n');
199
- await cmdInit();
200
- return;
215
+ // cmdStart 直接调用 cmdInit,不会经过 CLI 的 `init` 分发分支;
216
+ // 这里需显式抑制 AUN SDK 的常规 keystore 日志。
217
+ const { suppressSdkLogs } = await import('../aun/aid/index.js');
218
+ suppressSdkLogs();
219
+ await cmdInit({ invokedByStart: true });
220
+ if (!loadDefaults()) {
221
+ console.log('⚠ 初始化未完成,未启动 EvolCore。');
222
+ return;
223
+ }
224
+ console.log('\n初始化完成,开始启动 EvolCore ....');
201
225
  }
202
226
  }
203
227
  // 控制 AID 门禁:缺 aid 且交互式 → 只补全控制 AID + owners(不重走 baseagent 向导)。
@@ -236,19 +260,12 @@ export async function cmdStart(opts = {}) {
236
260
  }
237
261
  // 检查至少有一个 self-agent
238
262
  const { agents, skipped } = loadAllAgents();
263
+ const isControlAidBootstrap = opts.bindBootstrap && !!daemonCfgStart.aid;
264
+ const shouldPrintNoSelfAgentHints = agents.length === 0 && !isControlAidBootstrap;
239
265
  if (agents.length === 0) {
240
- if (opts.bindBootstrap && daemonCfgStart.aid) {
266
+ if (isControlAidBootstrap) {
241
267
  console.log('ℹ 未配置任何 self-agent,绑定 bootstrap 将仅启动控制 AID');
242
268
  }
243
- else {
244
- console.log('ℹ 未配置任何 self-agent,将仅启动 Control Plane。');
245
- console.log(' 可通过控制 AID 远程创建 agent,或稍后运行 ec agent new <aid>.agentid.pub');
246
- if (skipped.length > 0) {
247
- console.log(`跳过的目录:`);
248
- for (const s of skipped)
249
- console.log(` - ${s.dirName}: ${s.reason}`);
250
- }
251
- }
252
269
  }
253
270
  // 检查 instance 目录中的进程状态
254
271
  const status = scanInstances();
@@ -323,10 +340,22 @@ export async function cmdStart(opts = {}) {
323
340
  console.log(` Logs: ${p.logs}/`);
324
341
  console.log(`⏱ ready in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
325
342
  if (!ecwebStartedBeforeDaemon) {
326
- startEcwebIfEnabled(p).catch((err) => {
343
+ startEcwebIfEnabled(p)
344
+ .then((ecwebStarted) => {
345
+ if (shouldPrintNoSelfAgentHints) {
346
+ printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted, skipped });
347
+ }
348
+ })
349
+ .catch((err) => {
327
350
  console.error(`⚠ ECWeb 启动检查失败: ${err instanceof Error ? err.message : String(err)}`);
351
+ if (shouldPrintNoSelfAgentHints) {
352
+ printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: false, skipped });
353
+ }
328
354
  });
329
355
  }
356
+ else if (shouldPrintNoSelfAgentHints) {
357
+ printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: true, skipped });
358
+ }
330
359
  }, 500);
331
360
  let forwardingShutdown = false;
332
361
  const forwardSignal = (signal) => {
@@ -426,8 +455,10 @@ export async function cmdStart(opts = {}) {
426
455
  }
427
456
  console.log(`⏱ done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
428
457
  // ECWeb 自动后台启动
429
- if (!ecwebStartedBeforeDaemon)
430
- await startEcwebIfEnabled(p);
458
+ const ecwebStarted = ecwebStartedBeforeDaemon || await startEcwebIfEnabled(p);
459
+ if (shouldPrintNoSelfAgentHints) {
460
+ printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted, skipped });
461
+ }
431
462
  return;
432
463
  }
433
464
  // 超时
@@ -246,11 +246,12 @@ Commands:
246
246
  console.log('没有匹配的 handoff');
247
247
  return;
248
248
  }
249
- console.log('ID\tSTATE\tORIGIN\tTARGET\tUPDATED\tATTENTION');
249
+ console.log('ID\tSTATE\tRETURN_POLICY\tORIGIN\tTARGET\tUPDATED\tATTENTION');
250
250
  for (const item of result.handoffs) {
251
251
  console.log([
252
252
  item.handoff_id,
253
253
  item.state,
254
+ item.return_policy,
254
255
  item.origin_session_id,
255
256
  item.target_session_id,
256
257
  formatTimestamp(item.updated_at),