evolcore 0.0.2 → 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 (206) hide show
  1. package/CHANGELOG.md +93 -778
  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 +198 -48
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +203 -8
  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/aun/outbox.js +8 -0
  14. package/dist/channels/aun.js +241 -116
  15. package/dist/channels/daemon.js +18 -4
  16. package/dist/channels/dingtalk.js +1005 -260
  17. package/dist/channels/feishu.js +184 -56
  18. package/dist/channels/qqbot.js +23 -1
  19. package/dist/channels/wechat.js +303 -155
  20. package/dist/channels/wecom.js +1132 -164
  21. package/dist/cli/agent-command.js +2 -1
  22. package/dist/cli/aun-commands.js +98 -43
  23. package/dist/cli/bench.js +2 -2
  24. package/dist/cli/contact.js +71 -0
  25. package/dist/cli/ctl-command.js +2 -2
  26. package/dist/cli/daemon-commands.js +77 -214
  27. package/dist/cli/handoff-command.js +4 -3
  28. package/dist/cli/help.js +9 -5
  29. package/dist/cli/index.js +132 -116
  30. package/dist/cli/init-channel.js +242 -129
  31. package/dist/cli/init.js +70 -31
  32. package/dist/cli/model.js +2 -1
  33. package/dist/cli/net-check.js +2 -2
  34. package/dist/cli/queue-command.js +30 -6
  35. package/dist/cli/raw-key-input.js +25 -0
  36. package/dist/cli/response.js +5 -6
  37. package/dist/cli/restart-monitor.js +25 -1
  38. package/dist/cli/stats.js +6 -4
  39. package/dist/cli/trigger-command.js +146 -25
  40. package/dist/cli/version.js +6 -1
  41. package/dist/config/access-policy-domain.js +38 -0
  42. package/dist/config/access-policy.js +134 -0
  43. package/dist/config/builtin-role-templates.js +42 -17
  44. package/dist/config/builtin-roles.js +31 -6
  45. package/dist/config/config-field-policy.js +17 -5
  46. package/dist/config/config-manager.js +409 -29
  47. package/dist/config/config-operation-service.js +35 -38
  48. package/dist/config/contact-alias.js +68 -0
  49. package/dist/config/contact-bind-code.js +274 -0
  50. package/dist/config/contact-book-store.js +622 -0
  51. package/dist/config/contact-book-v2-startup.js +35 -0
  52. package/dist/config/contact-book.js +185 -300
  53. package/dist/config/contact-operation-service.js +116 -0
  54. package/dist/config/contact-request-service.js +331 -0
  55. package/dist/config/peer-role-resolver.js +136 -55
  56. package/dist/config/role-ranks.js +18 -0
  57. package/dist/config/role-service.js +16 -19
  58. package/dist/config/role-store.js +16 -5
  59. package/dist/config/roles.js +10 -1
  60. package/dist/config/schema-registry.js +49 -24
  61. package/dist/config-store.js +14 -4
  62. package/dist/core/auth/agent-delegation.js +105 -11
  63. package/dist/core/auth/authorization-audit.js +16 -1
  64. package/dist/core/auth/operation-authorizer.js +10 -0
  65. package/dist/core/auth/operation-catalog.js +105 -1
  66. package/dist/core/auth/trigger-authorization.js +15 -0
  67. package/dist/core/bootstrap-service.js +2 -9
  68. package/dist/core/channel-loader.js +6 -2
  69. package/dist/core/command/command-handler.js +113 -21
  70. package/dist/core/command/connect-menu.js +599 -0
  71. package/dist/core/command/evol-menu-version-gate.js +38 -0
  72. package/dist/core/command/group-menu.js +421 -0
  73. package/dist/core/command/menu-handler.js +382 -100
  74. package/dist/core/command/menu-protocol.js +8 -2
  75. package/dist/core/command/menu-token-store.js +102 -0
  76. package/dist/core/command/role-menu.js +128 -29
  77. package/dist/core/command/slash-gate.js +1 -1
  78. package/dist/core/command/slash-handler.js +186 -49
  79. package/dist/core/daemon-file-cache.js +40 -6
  80. package/dist/core/event-catalog.js +99 -0
  81. package/dist/core/evolagent-registry.js +4 -40
  82. package/dist/core/evolagent.js +20 -9
  83. package/dist/core/handoff/runtime.js +162 -37
  84. package/dist/core/handoff/store.js +46 -2
  85. package/dist/core/handoff/types.js +1 -0
  86. package/dist/core/inference/text-inference.js +16 -74
  87. package/dist/core/message/file-markers.js +7 -0
  88. package/dist/core/message/im-renderer.js +20 -14
  89. package/dist/core/message/inbound-admission.js +124 -0
  90. package/dist/core/message/message-bridge.js +670 -57
  91. package/dist/core/message/message-log.js +1 -0
  92. package/dist/core/message/message-queue.js +195 -10
  93. package/dist/core/message/message-utils.js +8 -2
  94. package/dist/core/message/peer-mode.js +7 -8
  95. package/dist/core/message/response-engine.js +642 -96
  96. package/dist/core/message/send-receipt.js +24 -0
  97. package/dist/core/message/stream-debouncer.js +11 -2
  98. package/dist/core/permission/approval-gateway.js +17 -10
  99. package/dist/core/permission/ec-command-parser.js +101 -46
  100. package/dist/core/permission/tool-policy.js +115 -38
  101. package/dist/core/protected-paths.js +38 -11
  102. package/dist/core/session/session-fs-store.js +44 -3
  103. package/dist/core/session/session-manager.js +86 -8
  104. package/dist/core/session/session-mapper.js +2 -0
  105. package/dist/core/session/session-renew.js +125 -69
  106. package/dist/core/session/session-turn-coordinator.js +5 -1
  107. package/dist/eck/kit-renderer.js +12 -1
  108. package/dist/eck/message-renderer.js +79 -1
  109. package/dist/index.js +285 -94
  110. package/dist/ipc.js +114 -8
  111. package/dist/paths.js +3 -0
  112. package/dist/response-system/config-resolver.js +29 -0
  113. package/dist/response-system/coordinator.js +8 -32
  114. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  115. package/dist/response-system/index.js +1 -0
  116. package/dist/response-system/modes/single-session/index.js +7 -4
  117. package/dist/stats/billing.js +20 -8
  118. package/dist/trigger/manager.js +3 -0
  119. package/dist/trigger/parser.js +126 -11
  120. package/dist/trigger/patch.js +12 -2
  121. package/dist/trigger/scheduler.js +444 -40
  122. package/dist/trigger/validation.js +13 -0
  123. package/dist/utils/aid-bind.js +43 -29
  124. package/dist/utils/error-dict.json +7 -0
  125. package/dist/utils/evolcore-version.js +21 -0
  126. package/dist/utils/instance-registry.js +14 -7
  127. package/dist/utils/log-writer.js +46 -0
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/media-cache.js +4 -1
  130. package/dist/utils/model-prices.jsonl +6 -3
  131. package/dist/utils/restart-safety.js +31 -0
  132. package/dist/utils/stable-semver.js +21 -0
  133. package/dist/utils/stats.js +33 -9
  134. package/dist/utils/system-memory.js +62 -0
  135. package/kits/docs/INDEX.md +2 -1
  136. package/kits/docs/channels/aun.md +4 -13
  137. package/kits/docs/evolcore/INDEX.md +5 -3
  138. package/kits/docs/evolcore/agent.md +9 -1
  139. package/kits/docs/evolcore/aid.md +5 -2
  140. package/kits/docs/evolcore/config.md +47 -2
  141. package/kits/docs/evolcore/contact.md +61 -0
  142. package/kits/docs/evolcore/fs.md +9 -0
  143. package/kits/docs/evolcore/group-rules.md +46 -4
  144. package/kits/docs/evolcore/group.md +15 -6
  145. package/kits/docs/evolcore/model.md +4 -1
  146. package/kits/docs/evolcore/msg.md +12 -6
  147. package/kits/docs/evolcore/response.md +16 -21
  148. package/kits/docs/evolcore/rpc.md +2 -0
  149. package/kits/docs/evolcore/stats.md +15 -2
  150. package/kits/docs/evolcore/storage.md +1 -0
  151. package/kits/docs/evolcore/trigger.md +42 -10
  152. package/kits/docs/path-registry.md +36 -17
  153. package/kits/eck_manifest.json +12 -0
  154. package/kits/eck_message_manifest.json +12 -1
  155. package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
  156. package/kits/rules/01-overview.md +17 -7
  157. package/kits/rules/02-navigation.md +38 -18
  158. package/kits/rules/03-identity.md +28 -24
  159. package/kits/rules/04-relation.md +44 -28
  160. package/kits/rules/05-venue.md +31 -15
  161. package/kits/rules/06-channel.md +11 -7
  162. package/kits/schemas/_meta.json +21 -7
  163. package/kits/schemas/agent-config.schema.6.json +322 -0
  164. package/kits/schemas/agent-config.schema.7.json +303 -0
  165. package/kits/schemas/agent-config.schema.8.json +304 -0
  166. package/kits/schemas/agent-config.schema.9.json +374 -0
  167. package/kits/schemas/contact-book.schema.2.json +43 -0
  168. package/kits/schemas/contact-book.schema.3.json +67 -0
  169. package/kits/schemas/daemon.schema.1.json +3 -2
  170. package/kits/schemas/daemon.schema.2.json +101 -0
  171. package/kits/schemas/daemon.schema.3.json +123 -0
  172. package/kits/schemas/defaults.schema.2.json +85 -0
  173. package/kits/schemas/defaults.schema.3.json +73 -0
  174. package/kits/schemas/relation-config.schema.5.json +47 -0
  175. package/kits/schemas/relation-config.schema.6.json +46 -0
  176. package/kits/schemas/relation-config.schema.7.json +59 -0
  177. package/kits/schemas/role-config.schema.1.json +1 -0
  178. package/kits/schemas/role-registry.schema.1.json +2 -2
  179. package/kits/schemas/single-session.schema.2.json +57 -0
  180. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  181. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  182. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  183. package/kits/templates/roles/admin.json +47 -0
  184. package/kits/templates/roles/member.json +5 -0
  185. package/kits/templates/roles/owner.json +1 -0
  186. package/kits/templates/roles/visitor.json +5 -0
  187. package/kits/templates/system-fragments/channel.md +12 -2
  188. package/kits/templates/system-fragments/commands.md +3 -1
  189. package/kits/templates/system-fragments/identity.md +3 -1
  190. package/kits/templates/system-fragments/relation.md +1 -1
  191. package/kits/templates/system-fragments/session.md +6 -2
  192. package/package.json +7 -5
  193. package/MIGRATION-0.5.0.md +0 -378
  194. package/ROLE_ACCESS_CONTROL.md +0 -174
  195. package/assets/brand/evolcore/README.md +0 -19
  196. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  197. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  198. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  199. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  200. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  201. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  202. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  203. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  204. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  205. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  206. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -1,4 +1,4 @@
1
- import { ConfigError, ConfigTarget, ensureFile, listFields, read, resolveEffectiveFieldWithSource, resolveEffectiveWithSources, validateConfigFile, write, } from './config-manager.js';
1
+ import { ConfigError, ConfigTarget, ensureFile, listFields, mutateConfig, read, resolveEffectiveFieldWithSource, resolveEffectiveWithSources, validateConfigFile, } from './config-manager.js';
2
2
  import { collectConfigFiles, diffVersions, listAllVersions, prune, readCurrent, restore, snapshot, isConfigSnapshotsEnabled, } from './snapshot.js';
3
3
  import { readBootLog } from './boot-log.js';
4
4
  import { currentVersion, fieldSchemaDefault, isSchemaName, listSchemaNames, listSchemaVersions, readRawSchema, schemaHistoryEntry, } from './schema-registry.js';
@@ -302,27 +302,26 @@ function executeSet(op, options) {
302
302
  if (!op.route)
303
303
  return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
304
304
  const selector = selectorFor(op, options);
305
- const existing = read(op.route.target, selector) || {};
306
- if (op.route.merge === 'list') {
307
- const previous = getNested(existing, op.field);
308
- const previousItems = Array.isArray(previous) ? previous : [];
309
- const nextItems = Array.isArray(op.value) ? op.value : [op.value];
310
- const merged = [...new Set([...previousItems, ...nextItems].map(value => typeof value === 'object' ? JSON.stringify(value) : value))]
311
- .map(value => {
312
- try {
313
- return typeof value === 'string' && value.startsWith('{') ? JSON.parse(value) : value;
314
- }
315
- catch {
316
- return value;
317
- }
318
- });
319
- setNested(existing, op.field, merged);
320
- }
321
- else {
322
- setNested(existing, op.field, op.value);
323
- }
324
- ensureFile(op.route.target, selector);
325
- write(op.route.target, existing, selector);
305
+ mutateConfig(op.route.target, selector, existing => {
306
+ if (op.route.merge === 'list') {
307
+ const previous = getNested(existing, op.field);
308
+ const previousItems = Array.isArray(previous) ? previous : [];
309
+ const nextItems = Array.isArray(op.value) ? op.value : [op.value];
310
+ const merged = [...new Set([...previousItems, ...nextItems].map(value => typeof value === 'object' ? JSON.stringify(value) : value))]
311
+ .map(value => {
312
+ try {
313
+ return typeof value === 'string' && value.startsWith('{') ? JSON.parse(value) : value;
314
+ }
315
+ catch {
316
+ return value;
317
+ }
318
+ });
319
+ setNested(existing, op.field, merged);
320
+ }
321
+ else {
322
+ setNested(existing, op.field, op.value);
323
+ }
324
+ });
326
325
  return {
327
326
  ok: true,
328
327
  subcommand: 'set',
@@ -336,22 +335,20 @@ function executeUnset(op, options) {
336
335
  if (!op.route)
337
336
  return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
338
337
  const selector = selectorFor(op, options);
339
- const existing = read(op.route.target, selector);
340
- if (!existing) {
341
- return { ok: true, subcommand: 'unset', field: op.field, removed: false, scope: op.configScope };
342
- }
343
- const parts = op.field.split('.');
344
- let current = existing;
345
- for (let i = 0; i < parts.length - 1; i++) {
346
- if (current == null)
347
- break;
348
- current = current[parts[i]];
349
- }
350
- const leaf = parts[parts.length - 1];
351
- const removed = !!current && typeof current === 'object' && Object.prototype.hasOwnProperty.call(current, leaf);
352
- if (removed)
353
- delete current[leaf];
354
- write(op.route.target, existing, selector);
338
+ const { value: removed } = mutateConfig(op.route.target, selector, existing => {
339
+ const parts = op.field.split('.');
340
+ let current = existing;
341
+ for (let i = 0; i < parts.length - 1; i++) {
342
+ if (current == null)
343
+ break;
344
+ current = current[parts[i]];
345
+ }
346
+ const leaf = parts[parts.length - 1];
347
+ const exists = !!current && typeof current === 'object' && Object.prototype.hasOwnProperty.call(current, leaf);
348
+ if (exists)
349
+ delete current[leaf];
350
+ return exists;
351
+ });
355
352
  return { ok: true, subcommand: 'unset', field: op.field, removed, scope: op.configScope };
356
353
  }
357
354
  function selectorFor(command, options = {}) {
@@ -0,0 +1,68 @@
1
+ import { isValidAid } from '../aun/aid/validation.js';
2
+ const CHANNEL_SEGMENT = /^[A-Za-z0-9_-]+$/;
3
+ /** Parse the canonical channel instance key used by Contact aliases. */
4
+ export function parseContactChannelKey(value) {
5
+ if (typeof value !== 'string')
6
+ return null;
7
+ const text = value.trim();
8
+ const parts = text.split('#');
9
+ if (parts.length !== 3)
10
+ return null;
11
+ const [rawType, selfAid, channelName] = parts;
12
+ const channelType = rawType.toLowerCase();
13
+ if (!CHANNEL_SEGMENT.test(channelType)
14
+ || rawType !== channelType
15
+ || !isValidAid(selfAid)
16
+ || !CHANNEL_SEGMENT.test(channelName))
17
+ return null;
18
+ return {
19
+ channelType,
20
+ selfAid,
21
+ channelName,
22
+ channelKey: `${channelType}#${selfAid}#${channelName}`,
23
+ };
24
+ }
25
+ /** Validate that a runtime channelKey belongs to the expected Agent and channel type. */
26
+ export function normalizeContactChannelKey(selfAid, channelType, channelKey) {
27
+ const parsed = parseContactChannelKey(channelKey);
28
+ const expectedType = String(channelType || '').trim().toLowerCase();
29
+ const expectedSelf = String(selfAid || '').trim();
30
+ if (!parsed || parsed.channelType !== expectedType || parsed.selfAid !== expectedSelf)
31
+ return null;
32
+ return parsed.channelKey;
33
+ }
34
+ /** Format `<channelKey>:<urlEncode(peerId)>`. */
35
+ export function formatContactAlias(channelKey, peerId) {
36
+ const parsed = parseContactChannelKey(channelKey);
37
+ const peer = String(peerId || '').trim();
38
+ if (!parsed || parsed.channelType === 'aun' || !peer) {
39
+ throw new Error(`Invalid Contact alias input: ${String(channelKey)}:${String(peerId)}`);
40
+ }
41
+ return `${parsed.channelKey}:${encodeURIComponent(peer)}`;
42
+ }
43
+ /** Parse and canonicalize a stored Contact alias. Non-canonical URL encoding is rejected. */
44
+ export function parseContactAlias(value, expectedSelfAid) {
45
+ if (typeof value !== 'string')
46
+ return null;
47
+ const text = value.trim();
48
+ const separator = text.indexOf(':');
49
+ if (separator <= 0 || separator === text.length - 1)
50
+ return null;
51
+ const parsedChannel = parseContactChannelKey(text.slice(0, separator));
52
+ if (!parsedChannel || parsedChannel.channelType === 'aun')
53
+ return null;
54
+ if (expectedSelfAid !== undefined && parsedChannel.selfAid !== String(expectedSelfAid || '').trim())
55
+ return null;
56
+ const encodedPeerId = text.slice(separator + 1);
57
+ let peerId;
58
+ try {
59
+ peerId = decodeURIComponent(encodedPeerId);
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ if (!peerId || encodeURIComponent(peerId) !== encodedPeerId)
65
+ return null;
66
+ const alias = `${parsedChannel.channelKey}:${encodedPeerId}`;
67
+ return { ...parsedChannel, peerId, alias };
68
+ }
@@ -0,0 +1,274 @@
1
+ import { randomInt } from 'crypto';
2
+ import { isValidAid } from '../aun/aid/validation.js';
3
+ import { isAcceptedContact, resolveContactView } from './contact-book.js';
4
+ import { formatContactAlias, normalizeContactChannelKey } from './contact-alias.js';
5
+ import { mutateContactBookSync } from './contact-book-store.js';
6
+ export const CONTACT_BIND_CODE_TTL_MS = 10 * 60 * 1000;
7
+ export const CONTACT_BIND_MAX_FAILED_ATTEMPTS = 5;
8
+ export const CONTACT_BIND_MAX_PENDING_PER_CHANNEL = 100;
9
+ export const CONTACT_BIND_MAX_PENDING_TOTAL = 5_000;
10
+ export const CONTACT_BIND_MAX_INVALID_ACTORS = 1_000;
11
+ /** In-memory, one-time channel identity proof used before writing a Contact alias. */
12
+ export class ContactBindCodeRegistry {
13
+ options;
14
+ /** `selfAid + channelName + primaryId` -> pending request. */
15
+ pendingByPrimary = new Map();
16
+ /** `selfAid + channelName + code` -> pending request key. */
17
+ pendingByCode = new Map();
18
+ /** Scope -> pending request keys, used for bounded cleanup and capacity checks. */
19
+ pendingKeysByScope = new Map();
20
+ /** Unknown-code throttling is deliberately separate from any pending request. */
21
+ invalidAttemptsByActor = new Map();
22
+ constructor(options) {
23
+ this.options = options;
24
+ }
25
+ register(req) {
26
+ const selfAid = String(req.selfAid || '').trim();
27
+ const primaryId = String(req.primaryId || '').trim();
28
+ const now = req.now ?? Date.now();
29
+ const requestedCode = req.code ? String(req.code).trim() : undefined;
30
+ if (!isValidAid(selfAid))
31
+ return { ok: false, error: `invalid selfAid: ${req.selfAid}` };
32
+ const channelName = normalizeContactChannelKey(selfAid, this.options.channelType, req.channelName);
33
+ if (!channelName)
34
+ return { ok: false, error: `invalid channelName: ${req.channelName}` };
35
+ if (!isValidAid(primaryId))
36
+ return { ok: false, error: `invalid primaryId: ${req.primaryId}` };
37
+ const contact = resolveContactView(selfAid, primaryId);
38
+ if (!contact.isOwner && !isAcceptedContact(selfAid, primaryId)) {
39
+ return { ok: false, error: `only an active contact or Agent owner can bind an identity: ${primaryId}` };
40
+ }
41
+ if (requestedCode && !/^\d{6}$/.test(requestedCode))
42
+ return { ok: false, error: 'binding code must be 6 digits' };
43
+ const scope = pendingScopeKey(selfAid, channelName);
44
+ this.purgeExpiredAll(now);
45
+ const key = pendingBindKey(selfAid, channelName, primaryId);
46
+ const replaced = this.pendingByPrimary.has(key);
47
+ const code = requestedCode ?? this.generateUniqueCode(scope);
48
+ if (!code)
49
+ return { ok: false, error: 'unable to allocate a unique binding code' };
50
+ const codeKey = pendingCodeKey(scope, code);
51
+ const existingCodeOwner = this.pendingByCode.get(codeKey);
52
+ if (existingCodeOwner && existingCodeOwner !== key) {
53
+ return { ok: false, error: 'binding code is already active for this channel instance' };
54
+ }
55
+ const pendingInScope = this.pendingKeysByScope.get(scope)?.size ?? 0;
56
+ if (!replaced && pendingInScope >= CONTACT_BIND_MAX_PENDING_PER_CHANNEL) {
57
+ return { ok: false, error: 'too many pending binding requests for this channel instance' };
58
+ }
59
+ if (!replaced && this.pendingByPrimary.size >= CONTACT_BIND_MAX_PENDING_TOTAL) {
60
+ return { ok: false, error: 'too many pending binding requests in this process' };
61
+ }
62
+ if (replaced)
63
+ this.removePending(key);
64
+ const item = {
65
+ selfAid,
66
+ channelName,
67
+ primaryId,
68
+ code,
69
+ createdAt: now,
70
+ expiresAt: now + CONTACT_BIND_CODE_TTL_MS,
71
+ };
72
+ this.pendingByPrimary.set(key, item);
73
+ this.pendingByCode.set(codeKey, key);
74
+ const scopeKeys = this.pendingKeysByScope.get(scope) ?? new Set();
75
+ scopeKeys.add(key);
76
+ this.pendingKeysByScope.set(scope, scopeKeys);
77
+ return { ok: true, code, expiresAt: item.expiresAt, replaced };
78
+ }
79
+ handle(ctx) {
80
+ if (ctx.channelType !== this.options.channelType)
81
+ return { handled: false };
82
+ const selfAid = String(ctx.selfAid || '').trim();
83
+ const channelName = normalizeContactChannelKey(selfAid, this.options.channelType, ctx.channelName);
84
+ if (!channelName)
85
+ return { handled: false };
86
+ const now = ctx.now ?? Date.now();
87
+ if (ctx.chatType !== 'private')
88
+ return { handled: false };
89
+ const input = String(ctx.content ?? '').trim();
90
+ const scope = pendingScopeKey(selfAid, channelName);
91
+ const codeKey = /^\d{6}$/.test(input) ? pendingCodeKey(scope, input) : undefined;
92
+ const pendingKey = codeKey ? this.pendingByCode.get(codeKey) : undefined;
93
+ const matchingItem = pendingKey ? this.pendingByPrimary.get(pendingKey) : undefined;
94
+ if (matchingItem && now >= matchingItem.expiresAt) {
95
+ this.removePending(pendingKey);
96
+ return {
97
+ handled: true,
98
+ status: 'expired',
99
+ remainingAttempts: 0,
100
+ reply: this.expiredReply(),
101
+ };
102
+ }
103
+ this.purgeExpiredAll(now);
104
+ const hasPending = (this.pendingKeysByScope.get(scope)?.size ?? 0) > 0;
105
+ if (!hasPending)
106
+ return { handled: false };
107
+ if (!/^\d{6}$/.test(input)) {
108
+ return {
109
+ handled: true,
110
+ status: 'format',
111
+ reply: '请发送 /bind <6 位数字绑定码>。',
112
+ remainingAttempts: CONTACT_BIND_MAX_FAILED_ATTEMPTS,
113
+ };
114
+ }
115
+ if (!matchingItem) {
116
+ const invalidAttempt = this.recordInvalidAttempt(scope, String(ctx.actorId || '').trim(), now);
117
+ if (invalidAttempt.remaining > 0) {
118
+ return {
119
+ handled: true,
120
+ status: 'wrong-code',
121
+ remainingAttempts: invalidAttempt.remaining,
122
+ reply: '绑定码错误,请重新发送 /bind <6 位数字绑定码>。',
123
+ };
124
+ }
125
+ return {
126
+ handled: true,
127
+ status: 'rate-limited',
128
+ remainingAttempts: 0,
129
+ reply: '绑定码错误次数过多,请稍后再试。有效绑定码仍可直接提交。',
130
+ };
131
+ }
132
+ const actorId = String(ctx.actorId || '').trim();
133
+ if (!actorId) {
134
+ return {
135
+ handled: true,
136
+ status: 'missing-actor',
137
+ remainingAttempts: CONTACT_BIND_MAX_FAILED_ATTEMPTS,
138
+ reply: `无法识别当前${this.options.displayName}发送者身份,未建立绑定。请重新发送绑定码或重新执行绑定流程。`,
139
+ };
140
+ }
141
+ // Claim first. A matching one-time code is never reusable, including when the
142
+ // final Contact Book validation or write fails.
143
+ this.removePending(pendingKey);
144
+ try {
145
+ const alias = formatContactAlias(matchingItem.channelName, actorId);
146
+ mutateContactBookSync({
147
+ selfAid: matchingItem.selfAid,
148
+ actor: actorId,
149
+ mutation: { type: 'bind-alias-eligible', primaryId: matchingItem.primaryId, alias },
150
+ });
151
+ return {
152
+ handled: true,
153
+ status: 'bound',
154
+ reply: `${this.options.displayName}身份绑定成功:${matchingItem.channelName}:${encodeURIComponent(actorId)} -> ${matchingItem.primaryId}`,
155
+ };
156
+ }
157
+ catch (error) {
158
+ const message = error instanceof Error ? error.message : String(error);
159
+ return {
160
+ handled: true,
161
+ status: 'write-failed',
162
+ remainingAttempts: CONTACT_BIND_MAX_FAILED_ATTEMPTS,
163
+ reply: `${this.options.displayName}身份绑定写入失败:${message}`,
164
+ };
165
+ }
166
+ }
167
+ get(selfAid, channelName, primaryId) {
168
+ const normalizedSelfAid = String(selfAid || '').trim();
169
+ const normalizedChannelName = normalizeContactChannelKey(normalizedSelfAid, this.options.channelType, channelName);
170
+ if (!normalizedChannelName)
171
+ return null;
172
+ const scope = pendingScopeKey(normalizedSelfAid, normalizedChannelName);
173
+ const key = primaryId
174
+ ? pendingBindKey(normalizedSelfAid, normalizedChannelName, primaryId)
175
+ : this.pendingKeysByScope.get(scope)?.values().next().value;
176
+ const item = key ? this.pendingByPrimary.get(key) : undefined;
177
+ return item ? { ...item } : null;
178
+ }
179
+ clear() {
180
+ this.pendingByPrimary.clear();
181
+ this.pendingByCode.clear();
182
+ this.pendingKeysByScope.clear();
183
+ this.invalidAttemptsByActor.clear();
184
+ }
185
+ noPendingReply() {
186
+ return `当前没有待验证的${this.options.displayName}身份绑定码。请先在目标 Agent 的 AUN 私聊发送 /bind ${this.options.channelType},或执行 ${this.options.initCommand} 生成新的绑定码。`;
187
+ }
188
+ generateUniqueCode(scope) {
189
+ for (let attempt = 0; attempt < 100; attempt += 1) {
190
+ const code = generateBindCode();
191
+ if (!this.pendingByCode.has(pendingCodeKey(scope, code)))
192
+ return code;
193
+ }
194
+ return null;
195
+ }
196
+ purgeExpiredAll(now) {
197
+ for (const [key, item] of this.pendingByPrimary) {
198
+ if (!item || now >= item.expiresAt)
199
+ this.removePending(key);
200
+ }
201
+ }
202
+ removePending(key) {
203
+ const item = this.pendingByPrimary.get(key);
204
+ if (!item)
205
+ return;
206
+ const scope = pendingScopeKey(item.selfAid, item.channelName);
207
+ this.pendingByPrimary.delete(key);
208
+ this.pendingByCode.delete(pendingCodeKey(scope, item.code));
209
+ const scopeKeys = this.pendingKeysByScope.get(scope);
210
+ scopeKeys?.delete(key);
211
+ if (scopeKeys?.size === 0)
212
+ this.pendingKeysByScope.delete(scope);
213
+ }
214
+ recordInvalidAttempt(scope, actorId, now) {
215
+ const key = `${scope}\u0000${actorId || '<unknown>'}`;
216
+ this.purgeExpiredInvalidAttempts(now);
217
+ const current = this.invalidAttemptsByActor.get(key);
218
+ const attempts = current && now < current.expiresAt ? current.attempts + 1 : 1;
219
+ this.invalidAttemptsByActor.delete(key);
220
+ if (this.invalidAttemptsByActor.size >= CONTACT_BIND_MAX_INVALID_ACTORS) {
221
+ const oldestKey = this.invalidAttemptsByActor.keys().next().value;
222
+ if (oldestKey)
223
+ this.invalidAttemptsByActor.delete(oldestKey);
224
+ }
225
+ this.invalidAttemptsByActor.set(key, { attempts, expiresAt: now + CONTACT_BIND_CODE_TTL_MS });
226
+ return { remaining: Math.max(0, CONTACT_BIND_MAX_FAILED_ATTEMPTS - attempts) };
227
+ }
228
+ purgeExpiredInvalidAttempts(now) {
229
+ for (const [key, value] of this.invalidAttemptsByActor) {
230
+ if (now >= value.expiresAt)
231
+ this.invalidAttemptsByActor.delete(key);
232
+ }
233
+ }
234
+ expiredReply() {
235
+ return `${this.options.displayName}身份绑定码已超时。请重新在 AUN 私聊目标 Agent 发送 /bind ${this.options.channelType},或执行 ${this.options.initCommand} 并再次绑定。`;
236
+ }
237
+ }
238
+ /**
239
+ * Reserve `/bind` for channel identity binding so it is never treated as an
240
+ * ordinary chat message. A pending request determines whether the command can
241
+ * complete a binding; otherwise the user receives setup guidance.
242
+ */
243
+ export function handleExplicitContactBindCommand(registry, ctx) {
244
+ const match = /^\s*\/bind(?:\s+(.+?))?\s*$/i.exec(String(ctx.content ?? ''));
245
+ if (!match)
246
+ return { handled: false };
247
+ if (ctx.chatType !== 'private') {
248
+ return {
249
+ handled: true,
250
+ status: 'private-only',
251
+ reply: '身份绑定仅支持私聊机器人。请在私聊中发送 /bind <6 位数字绑定码>。',
252
+ };
253
+ }
254
+ const result = registry.handle({ ...ctx, content: match[1] ?? '' });
255
+ if (result.handled)
256
+ return result;
257
+ return {
258
+ handled: true,
259
+ status: 'no-pending',
260
+ reply: registry.noPendingReply(),
261
+ };
262
+ }
263
+ function generateBindCode() {
264
+ return String(randomInt(0, 1_000_000)).padStart(6, '0');
265
+ }
266
+ function pendingScopeKey(selfAid, channelName) {
267
+ return `${String(selfAid || '').trim()}\u0000${String(channelName || '').trim()}`;
268
+ }
269
+ function pendingBindKey(selfAid, channelName, primaryId) {
270
+ return `${pendingScopeKey(selfAid, channelName)}\u0000${String(primaryId || '').trim()}`;
271
+ }
272
+ function pendingCodeKey(scope, code) {
273
+ return `${scope}\u0000${code}`;
274
+ }