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,6 +1,9 @@
1
1
  import { agentContactConfig } from '../paths.js';
2
2
  import { isValidAid } from '../aun/aid/validation.js';
3
- import { ConfigTarget, read, validateConfig, write } from './config-manager.js';
3
+ import { isExplicitGroupId } from '../aun/group-identity.js';
4
+ import { ConfigTarget, read, validateConfig } from './config-manager.js';
5
+ import { ContactMutationError, contactRevision, mutateContactBookSync } from './contact-book-store.js';
6
+ import { formatContactAlias, normalizeContactChannelKey, parseContactAlias, } from './contact-alias.js';
4
7
  export class ContactBookError extends Error {
5
8
  code;
6
9
  constructor(code, message) {
@@ -10,13 +13,11 @@ export class ContactBookError extends Error {
10
13
  }
11
14
  }
12
15
  let aliasMapCache = new WeakMap();
13
- const emptyAliasMap = {
14
- scopedAliasToPrimary: new Map(),
15
- conflictingScopedAliases: new Set(),
16
- legacyAliasToPrimary: new Map(),
17
- conflictingLegacyAliases: new Set(),
16
+ let snapshotByBook = new WeakMap();
17
+ const EMPTY_ALIAS_MAP = {
18
+ aliasToPrimary: new Map(),
19
+ conflictingAliases: new Set(),
18
20
  };
19
- const SCOPED_ALIAS_PREFIX = 'channel-key:';
20
21
  export function agentContactConfigPath(aid) {
21
22
  return agentContactConfig(aid);
22
23
  }
@@ -26,346 +27,230 @@ export function readContactBook(selfAid, opts = {}) {
26
27
  return read(ConfigTarget.Contact, { self: selfAid }, { cache: opts.cache ?? true });
27
28
  }
28
29
  export function resolvePrincipal(selfAid, channelType, actorId, channelKey) {
29
- const normalizedActor = String(actorId || '').trim();
30
- const normalizedChannel = String(channelType || '').trim().toLowerCase();
31
- if (normalizedChannel === 'aun') {
32
- return isValidAid(normalizedActor)
33
- ? { principalId: normalizedActor, principalStatus: 'native_aid' }
34
- : { principalStatus: normalizedActor ? 'unmapped' : 'error' };
30
+ const actor = String(actorId || '').trim();
31
+ const type = String(channelType || '').trim().toLowerCase();
32
+ if (type === 'aun') {
33
+ return isValidAid(actor)
34
+ ? { principalId: actor, principalStatus: 'native_aid' }
35
+ : { principalStatus: actor ? 'unmapped' : 'error' };
35
36
  }
36
- const normalizedSelf = String(selfAid || '').trim();
37
- const normalizedChannelKey = normalizeChannelKey(normalizedSelf, normalizedChannel, channelKey);
38
- const legacyAlias = normalizeLegacyAlias(normalizedChannel, normalizedActor);
39
- if (!isValidAid(normalizedSelf) || !normalizedChannelKey || !legacyAlias) {
37
+ const canonicalChannelKey = normalizeContactChannelKey(selfAid, type, channelKey);
38
+ if (!isValidAid(String(selfAid || '').trim()) || !actor || !canonicalChannelKey) {
40
39
  return { principalStatus: 'error' };
41
40
  }
42
- const scopedAlias = scopedAliasLookupKey(normalizedChannelKey, normalizedActor);
43
41
  try {
44
- const aliases = loadAliasMap(normalizedSelf);
45
- if (aliases.conflictingScopedAliases.has(scopedAlias)) {
46
- return { principalStatus: 'conflict' };
47
- }
48
- const scopedPrincipalId = aliases.scopedAliasToPrimary.get(scopedAlias);
49
- if (scopedPrincipalId) {
50
- return { principalId: scopedPrincipalId, principalStatus: 'mapped' };
51
- }
52
- // Legacy aliases did not identify a channel instance. They remain trusted only
53
- // while the configured channel type has exactly one matching instance.
54
- if (!isUnambiguousLegacyChannel(normalizedSelf, normalizedChannel, normalizedChannelKey)) {
55
- return { principalStatus: 'unmapped' };
56
- }
57
- if (aliases.conflictingLegacyAliases.has(legacyAlias)) {
42
+ const aliases = loadAliasMap(selfAid);
43
+ const exact = formatContactAlias(canonicalChannelKey, actor);
44
+ if (aliases.conflictingAliases.has(exact))
58
45
  return { principalStatus: 'conflict' };
59
- }
60
- const legacyPrincipalId = aliases.legacyAliasToPrimary.get(legacyAlias);
61
- return legacyPrincipalId
62
- ? { principalId: legacyPrincipalId, principalStatus: 'mapped' }
63
- : { principalStatus: 'unmapped' };
46
+ const mapped = aliases.aliasToPrimary.get(exact);
47
+ if (mapped)
48
+ return { principalId: mapped, principalStatus: 'mapped' };
49
+ return { principalStatus: 'unmapped' };
64
50
  }
65
51
  catch {
66
52
  return { principalStatus: 'error' };
67
53
  }
68
54
  }
69
55
  export function resolvePrimaryId(selfAid, channelType, actorId, channelKey) {
70
- const normalizedActor = String(actorId || '').trim();
71
- return resolvePrincipal(selfAid, channelType, normalizedActor, channelKey).principalId ?? normalizedActor;
56
+ const actor = String(actorId || '').trim();
57
+ return resolvePrincipal(selfAid, channelType, actor, channelKey).principalId ?? actor;
72
58
  }
73
59
  export function bindContactAlias(selfAid, primaryId, channelType, actorId, channelKey) {
74
60
  const primary = normalizePrimaryId(primaryId);
75
- const alias = requireScopedAlias(selfAid, channelType, actorId, channelKey);
76
- const legacyAlias = requireLegacyAlias(channelType, actorId);
77
- ensureNativeAidIsNotRemapped(primary, channelType, actorId);
78
- const next = cloneContactBook(readContactBook(selfAid, { cache: true }));
79
- ensureScopedAliasAvailable(next, selfAid, primary, alias.lookupKey);
80
- const contacts = next.contacts ?? {};
81
- let removedLegacyAlias = false;
82
- // An explicit scoped bind supersedes every unscoped copy of the same alias.
83
- // Otherwise unbinding the scoped entry could silently reactivate a legacy
84
- // mapping owned by another principal.
85
- for (const [contactId, value] of Object.entries(contacts)) {
86
- const entry = normalizeContactEntry(value);
87
- const filtered = entry.aliases.filter(item => normalizeLegacyAliasString(item) !== legacyAlias);
88
- if (filtered.length !== entry.aliases.length) {
89
- contacts[contactId] = { ...entry, aliases: filtered };
90
- removedLegacyAlias = true;
61
+ const type = String(channelType || '').trim().toLowerCase();
62
+ const actor = String(actorId || '').trim();
63
+ if (type === 'aun') {
64
+ if (!isValidAid(actor) || actor !== primary) {
65
+ throw new ContactBookError('NATIVE_AID_REMAP_FORBIDDEN', `Native AID "${actor}" cannot be remapped to "${primary}"`);
91
66
  }
92
- }
93
- const current = normalizeContactEntry(contacts[primary]);
94
- const alreadyScoped = current.aliases.some(value => (normalizeScopedAliasString(value, selfAid)?.lookupKey === alias.lookupKey));
95
- if (alreadyScoped && !removedLegacyAlias) {
96
67
  return false;
97
68
  }
98
- contacts[primary] = {
99
- ...current,
100
- aliases: alreadyScoped
101
- ? current.aliases
102
- : [...current.aliases, alias.serialized],
103
- };
104
- next.contacts = contacts;
105
- write(ConfigTarget.Contact, next, { self: selfAid });
106
- return true;
69
+ const canonicalChannelKey = normalizeContactChannelKey(selfAid, type, channelKey);
70
+ if (!canonicalChannelKey || !actor) {
71
+ throw new ContactBookError('INVALID_ALIAS', `Invalid contact alias: ${String(channelKey)}:${String(actorId)}`);
72
+ }
73
+ if (!getContactSnapshot(selfAid).contacts.has(primary)) {
74
+ throw new ContactBookError('NOT_FOUND', `Contact was not found: ${primary}`);
75
+ }
76
+ const alias = formatContactAlias(canonicalChannelKey, actor);
77
+ try {
78
+ return mutateContactBookSync({
79
+ selfAid,
80
+ mutation: { type: 'bind-alias', primaryId: primary, alias },
81
+ }).changed;
82
+ }
83
+ catch (error) {
84
+ if (error instanceof ContactMutationError)
85
+ throw new ContactBookError(error.code, error.message);
86
+ throw error;
87
+ }
107
88
  }
108
89
  export function unbindContactAlias(selfAid, primaryId, channelType, actorId, channelKey) {
109
90
  const primary = normalizePrimaryId(primaryId);
110
- const alias = requireScopedAlias(selfAid, channelType, actorId, channelKey);
111
- const legacyAlias = requireLegacyAlias(channelType, actorId);
112
- const next = cloneContactBook(readContactBook(selfAid, { cache: true }));
113
- const contacts = next.contacts ?? {};
114
- let changed = false;
115
- // Remove the requested scoped binding from the target principal and purge
116
- // every unscoped copy of the same raw alias. Legacy aliases do not carry a
117
- // channel-instance identity, so retaining one on another contact would
118
- // silently reactivate that principal after this scoped binding is revoked.
119
- for (const [contactId, value] of Object.entries(contacts)) {
120
- const entry = normalizeContactEntry(value);
121
- const filtered = entry.aliases.filter(item => {
122
- const isTargetScoped = contactId.trim() === primary
123
- && normalizeScopedAliasString(item, selfAid)?.lookupKey === alias.lookupKey;
124
- const isLegacyCopy = normalizeLegacyAliasString(item) === legacyAlias;
125
- return !isTargetScoped && !isLegacyCopy;
126
- });
127
- if (filtered.length !== entry.aliases.length) {
128
- contacts[contactId] = { ...entry, aliases: filtered };
129
- changed = true;
130
- }
91
+ const type = String(channelType || '').trim().toLowerCase();
92
+ const actor = String(actorId || '').trim();
93
+ const canonicalChannelKey = normalizeContactChannelKey(selfAid, type, channelKey);
94
+ if (!canonicalChannelKey || type === 'aun' || !actor) {
95
+ throw new ContactBookError('INVALID_ALIAS', `Invalid contact alias: ${String(channelKey)}:${String(actorId)}`);
131
96
  }
132
- if (!changed) {
133
- return false;
97
+ const alias = formatContactAlias(canonicalChannelKey, actor);
98
+ try {
99
+ return mutateContactBookSync({
100
+ selfAid,
101
+ mutation: { type: 'unbind-alias', primaryId: primary, alias },
102
+ }).changed;
103
+ }
104
+ catch (error) {
105
+ if (error instanceof ContactMutationError)
106
+ throw new ContactBookError(error.code, error.message);
107
+ throw error;
134
108
  }
135
- next.contacts = contacts;
136
- write(ConfigTarget.Contact, next, { self: selfAid });
137
- return true;
138
109
  }
139
110
  export function clearContactBookCache() {
140
111
  aliasMapCache = new WeakMap();
112
+ snapshotByBook = new WeakMap();
141
113
  }
142
- function loadAliasMap(selfAid) {
143
- const book = readContactBook(selfAid, { cache: true });
144
- if (!book)
145
- return emptyAliasMap;
146
- const cached = aliasMapCache.get(book);
114
+ export function getContactSnapshot(selfAid) {
115
+ const book = readContactBook(selfAid, { cache: true }) ?? { $schema_version: 2, contacts: {} };
116
+ const cached = snapshotByBook.get(book);
147
117
  if (cached)
148
118
  return cached;
149
- const validationErrors = validateConfig(ConfigTarget.Contact, book);
150
- if (validationErrors.length > 0) {
151
- throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact book for ${selfAid}: ${validationErrors.join('; ')}`);
152
- }
153
- const built = buildAliasMap(book, selfAid);
154
- aliasMapCache.set(book, built);
155
- return built;
156
- }
157
- function buildAliasMap(config, selfAid) {
158
- const scopedAliasToPrimary = new Map();
159
- const conflictingScopedAliases = new Set();
160
- const legacyAliasToPrimary = new Map();
161
- const conflictingLegacyAliases = new Set();
162
- const contacts = config?.contacts;
163
- if (!contacts || typeof contacts !== 'object' || Array.isArray(contacts)) {
164
- return {
165
- scopedAliasToPrimary,
166
- conflictingScopedAliases,
167
- legacyAliasToPrimary,
168
- conflictingLegacyAliases,
169
- };
170
- }
171
- for (const [rawPrimaryId, entry] of Object.entries(contacts)) {
172
- const primaryId = rawPrimaryId.trim();
173
- if (!isValidAid(primaryId) || !entry || typeof entry !== 'object' || Array.isArray(entry))
174
- continue;
175
- const aliases = Array.isArray(entry.aliases) ? entry.aliases : [];
176
- for (const rawAlias of aliases) {
177
- const scoped = normalizeScopedAliasString(rawAlias, selfAid);
178
- if (scoped) {
179
- addAliasMapping(scopedAliasToPrimary, conflictingScopedAliases, scoped.lookupKey, primaryId);
180
- }
181
- else {
182
- const legacy = normalizeLegacyAliasString(rawAlias);
183
- if (legacy) {
184
- addAliasMapping(legacyAliasToPrimary, conflictingLegacyAliases, legacy, primaryId);
185
- }
186
- }
187
- }
119
+ const errors = validateConfig(ConfigTarget.Contact, book);
120
+ if (errors.length) {
121
+ throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact book for ${selfAid}: ${errors.join('; ')}`);
188
122
  }
189
- return {
190
- scopedAliasToPrimary,
191
- conflictingScopedAliases,
192
- legacyAliasToPrimary,
193
- conflictingLegacyAliases,
194
- };
195
- }
196
- function addAliasMapping(aliases, conflicts, alias, primaryId) {
197
- if (conflicts.has(alias))
198
- return;
199
- const existing = aliases.get(alias);
200
- if (existing && existing !== primaryId) {
201
- aliases.delete(alias);
202
- conflicts.add(alias);
203
- return;
204
- }
205
- aliases.set(alias, primaryId);
206
- }
207
- function cloneContactBook(config) {
208
- const next = config ? { ...config } : {};
209
- const contacts = {};
210
- const rawContacts = config?.contacts;
211
- if (rawContacts && typeof rawContacts === 'object' && !Array.isArray(rawContacts)) {
212
- for (const [primaryId, entry] of Object.entries(rawContacts)) {
213
- contacts[primaryId] = normalizeContactEntry(entry);
214
- }
123
+ const aliases = buildAliasMap(book, selfAid);
124
+ if (aliases.conflictingAliases.size) {
125
+ throw new ContactBookError('ALIAS_CONFLICT', `Conflicting contact aliases: ${[...aliases.conflictingAliases].join(', ')}`);
215
126
  }
216
- next.contacts = contacts;
217
- return next;
127
+ const snapshot = buildSnapshot(book, selfAid, aliases);
128
+ snapshotByBook.set(book, snapshot);
129
+ return snapshot;
218
130
  }
219
- function normalizeContactEntry(value) {
220
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
221
- return { aliases: [] };
222
- }
223
- const entry = value;
131
+ export function resolveContactView(selfAid, primaryId) {
132
+ const id = String(primaryId || '').trim();
133
+ const owner = listAgentOwners(selfAid).includes(id);
134
+ const entry = getContactSnapshot(selfAid).contacts.get(id);
135
+ const status = owner ? 'active' : entry?.status ?? 'active';
224
136
  return {
225
- ...entry,
226
- aliases: Array.isArray(entry.aliases)
227
- ? entry.aliases.filter((item) => typeof item === 'string')
228
- : [],
137
+ primaryId: id,
138
+ ...(entry?.displayName ? { displayName: entry.displayName } : {}),
139
+ aliases: entry?.aliases ?? [],
140
+ status,
141
+ blocked: status === 'blocked',
142
+ isOwner: owner,
143
+ source: owner ? 'owner' : entry ? 'contact' : 'none',
229
144
  };
230
145
  }
231
- function ensureScopedAliasAvailable(config, selfAid, primaryId, alias) {
232
- const owners = findScopedAliasOwners(config, selfAid, alias);
233
- if (owners.size === 0)
234
- return;
235
- if (owners.size === 1 && owners.has(primaryId))
236
- return;
237
- throw new ContactBookError('ALIAS_CONFLICT', `Alias "${alias}" is already bound to ${[...owners].join(', ')}`);
146
+ export function isBlockedContact(selfAid, primaryId) {
147
+ return resolveContactView(selfAid, primaryId).blocked;
238
148
  }
239
- function findScopedAliasOwners(config, selfAid, alias) {
240
- const owners = new Set();
241
- const contacts = config.contacts;
242
- if (!contacts || typeof contacts !== 'object' || Array.isArray(contacts))
243
- return owners;
244
- for (const [rawPrimaryId, entry] of Object.entries(contacts)) {
245
- const primaryId = rawPrimaryId.trim();
246
- if (!primaryId || !entry || typeof entry !== 'object' || Array.isArray(entry))
247
- continue;
248
- const aliases = Array.isArray(entry.aliases) ? entry.aliases : [];
249
- if (aliases.some(value => normalizeScopedAliasString(value, selfAid)?.lookupKey === alias)) {
250
- owners.add(primaryId);
251
- }
252
- }
253
- return owners;
149
+ export function isAcceptedContact(selfAid, primaryId) {
150
+ const id = String(primaryId || '').trim();
151
+ const snapshot = getContactSnapshot(selfAid);
152
+ const entry = snapshot.contacts.get(id);
153
+ return !!entry && (entry.status === undefined || entry.status === 'active');
254
154
  }
255
- function normalizePrimaryId(value) {
256
- const primaryId = String(value || '').trim();
257
- if (!isValidAid(primaryId)) {
258
- throw new ContactBookError('INVALID_PRIMARY_ID', `Invalid contact primary AID: ${String(value)}`);
259
- }
260
- return primaryId;
261
- }
262
- function requireLegacyAlias(channelType, actorId) {
263
- const alias = normalizeLegacyAlias(channelType, actorId);
264
- if (!alias) {
265
- throw new ContactBookError('INVALID_ALIAS', `Invalid contact alias: ${String(channelType)}:${String(actorId)}`);
266
- }
267
- return alias;
268
- }
269
- function normalizeLegacyAlias(channelType, actorId) {
270
- const channel = String(channelType || '').trim().toLowerCase();
271
- const id = String(actorId || '').trim();
272
- if (!/^[a-z0-9_-]+$/.test(channel) || !id)
273
- return null;
274
- return `${channel}:${id}`;
275
- }
276
- function normalizeLegacyAliasString(value) {
277
- if (typeof value !== 'string')
278
- return null;
279
- const trimmed = value.trim();
280
- if (trimmed.startsWith(SCOPED_ALIAS_PREFIX))
281
- return null;
282
- const idx = trimmed.indexOf(':');
283
- if (idx <= 0 || idx === trimmed.length - 1)
284
- return null;
285
- return normalizeLegacyAlias(trimmed.slice(0, idx), trimmed.slice(idx + 1));
286
- }
287
- function requireScopedAlias(selfAid, channelType, actorId, channelKey) {
288
- const normalizedActor = String(actorId || '').trim();
289
- const normalizedChannel = String(channelType || '').trim().toLowerCase();
290
- const normalizedChannelKey = normalizeChannelKey(selfAid, normalizedChannel, channelKey);
291
- if (!normalizedActor || !normalizedChannelKey) {
292
- throw new ContactBookError('INVALID_CHANNEL_ALIAS', `Invalid scoped contact alias: ${String(channelKey)}:${String(actorId)}`);
293
- }
294
- return {
295
- lookupKey: scopedAliasLookupKey(normalizedChannelKey, normalizedActor),
296
- serialized: serializeScopedAlias(normalizedChannelKey, normalizedActor),
297
- };
298
- }
299
- function normalizeScopedAliasString(value, selfAid) {
300
- if (typeof value !== 'string')
301
- return null;
302
- const trimmed = value.trim();
303
- if (!trimmed.startsWith(SCOPED_ALIAS_PREFIX))
304
- return null;
305
- const encoded = trimmed.slice(SCOPED_ALIAS_PREFIX.length);
306
- const separator = encoded.indexOf(':');
307
- if (separator <= 0 || separator === encoded.length - 1)
308
- return null;
155
+ /**
156
+ * 对端展示名:联系人簿 displayName principalId。仅用于展示(预览行/日志),
157
+ * 关系键始终用原生 peerId,不受此影响。
158
+ *
159
+ * 非 AUN 渠道的 actorId 是渠道原生 ID(飞书 open_id 等),直接展示不可读;
160
+ * 联系人簿已把它 alias 到主体 AID 上,取其 displayName 即可。
161
+ * 未绑定/联系人簿损坏时返回 undefined,由调用方按自己的兜底链继续。
162
+ */
163
+ export function resolvePeerDisplayLabel(selfAid, channelType, actorId, channelKey) {
164
+ const actor = String(actorId || '').trim();
165
+ if (!String(selfAid || '').trim() || !actor)
166
+ return undefined;
309
167
  try {
310
- const channelKey = decodeURIComponent(encoded.slice(0, separator));
311
- const actorId = decodeURIComponent(encoded.slice(separator + 1)).trim();
312
- const channelType = channelKey.split('#', 1)[0]?.toLowerCase() ?? '';
313
- const normalizedChannelKey = normalizeChannelKey(selfAid, channelType, channelKey);
314
- if (!normalizedChannelKey || !actorId)
315
- return null;
316
- return {
317
- lookupKey: scopedAliasLookupKey(normalizedChannelKey, actorId),
318
- serialized: serializeScopedAlias(normalizedChannelKey, actorId),
319
- };
168
+ const { principalId } = resolvePrincipal(selfAid, channelType, actor, channelKey);
169
+ if (!principalId)
170
+ return undefined;
171
+ return resolveContactView(selfAid, principalId).displayName || principalId;
320
172
  }
321
173
  catch {
322
- return null;
174
+ // 联系人簿损坏(校验失败/alias 冲突)不应影响消息处理
175
+ return undefined;
323
176
  }
324
177
  }
325
- function serializeScopedAlias(channelKey, actorId) {
326
- return `${SCOPED_ALIAS_PREFIX}${encodeURIComponent(channelKey)}:${encodeURIComponent(actorId)}`;
327
- }
328
- function scopedAliasLookupKey(channelKey, actorId) {
329
- return JSON.stringify([channelKey, actorId]);
178
+ function buildSnapshot(book, selfAid, aliases) {
179
+ const contacts = new Map();
180
+ const blockedIndex = new Set();
181
+ const owners = new Set(listAgentOwners(selfAid));
182
+ for (const [primaryId, rawEntry] of Object.entries(book.contacts)) {
183
+ if (!isValidAid(primaryId) || isExplicitGroupId(primaryId)) {
184
+ throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact AID: ${primaryId}`);
185
+ }
186
+ if (rawEntry.status === 'blocked' && owners.has(primaryId)) {
187
+ throw new ContactBookError('INVALID_CONTACT_BOOK', `Owner contact ${primaryId} cannot be blocked`);
188
+ }
189
+ const entry = Object.freeze({
190
+ ...rawEntry,
191
+ ...(rawEntry.aliases ? { aliases: Object.freeze([...rawEntry.aliases]) } : {}),
192
+ });
193
+ contacts.set(primaryId, entry);
194
+ if (entry.status === 'blocked')
195
+ blockedIndex.add(primaryId);
196
+ }
197
+ return Object.freeze({
198
+ contacts,
199
+ aliasIndex: aliases.aliasToPrimary,
200
+ conflictingAliases: aliases.conflictingAliases,
201
+ blockedIndex,
202
+ contactRevision: contactRevision(book),
203
+ });
330
204
  }
331
- function normalizeChannelKey(selfAid, channelType, value) {
332
- const key = typeof value === 'string' ? value.trim() : '';
333
- const parts = key.split('#');
334
- if (parts.length !== 3)
335
- return null;
336
- const [rawType, keySelfAid, name] = parts;
337
- const normalizedType = rawType.toLowerCase();
338
- if (!/^[a-z0-9_-]+$/.test(normalizedType)
339
- || normalizedType !== channelType
340
- || keySelfAid !== selfAid
341
- || !name) {
342
- return null;
205
+ function loadAliasMap(selfAid) {
206
+ const book = readContactBook(selfAid, { cache: true });
207
+ if (!book)
208
+ return EMPTY_ALIAS_MAP;
209
+ const cached = aliasMapCache.get(book);
210
+ if (cached)
211
+ return cached;
212
+ const errors = validateConfig(ConfigTarget.Contact, book);
213
+ if (errors.length) {
214
+ throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact book for ${selfAid}: ${errors.join('; ')}`);
343
215
  }
344
- return `${normalizedType}#${keySelfAid}#${name}`;
216
+ const built = buildAliasMap(book, selfAid);
217
+ aliasMapCache.set(book, built);
218
+ return built;
345
219
  }
346
- function isUnambiguousLegacyChannel(selfAid, channelType, channelKey) {
347
- try {
348
- const agent = read(ConfigTarget.Agent, { self: selfAid }, { cache: true });
349
- const matches = new Set();
350
- for (const candidate of agent?.channels ?? []) {
351
- if (!candidate || typeof candidate !== 'object')
352
- continue;
353
- const candidateType = String(candidate.type || '').trim().toLowerCase();
354
- const candidateName = String(candidate.name || '').trim();
355
- if (candidateType !== channelType || !candidateName || candidateName.includes('#'))
220
+ function buildAliasMap(book, selfAid) {
221
+ const aliasToPrimary = new Map();
222
+ const conflictingAliases = new Set();
223
+ for (const [primaryId, entry] of Object.entries(book?.contacts ?? {})) {
224
+ if (!isValidAid(primaryId) || isExplicitGroupId(primaryId) || !entry || typeof entry !== 'object')
225
+ continue;
226
+ for (const rawAlias of entry.aliases ?? []) {
227
+ const parsed = parseContactAlias(rawAlias, selfAid);
228
+ if (!parsed) {
229
+ throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact alias for ${selfAid}: ${String(rawAlias)}`);
230
+ }
231
+ const alias = parsed.alias;
232
+ if (conflictingAliases.has(alias))
356
233
  continue;
357
- matches.add(`${candidateType}#${selfAid}#${candidateName}`);
234
+ const existing = aliasToPrimary.get(alias);
235
+ if (existing && existing !== primaryId) {
236
+ aliasToPrimary.delete(alias);
237
+ conflictingAliases.add(alias);
238
+ }
239
+ else {
240
+ aliasToPrimary.set(alias, primaryId);
241
+ }
358
242
  }
359
- return matches.size === 1 && matches.has(channelKey);
360
- }
361
- catch {
362
- return false;
363
243
  }
244
+ return { aliasToPrimary, conflictingAliases };
364
245
  }
365
- function ensureNativeAidIsNotRemapped(primaryId, channelType, actorId) {
366
- const channel = String(channelType || '').trim().toLowerCase();
367
- const actor = String(actorId || '').trim();
368
- if (channel === 'aun' && isValidAid(actor) && actor !== primaryId) {
369
- throw new ContactBookError('NATIVE_AID_REMAP_FORBIDDEN', `Native AID "${actor}" cannot be remapped to "${primaryId}"`);
246
+ function normalizePrimaryId(value) {
247
+ const id = String(value || '').trim();
248
+ if (!isValidAid(id) || isExplicitGroupId(id)) {
249
+ throw new ContactBookError('INVALID_PRIMARY_ID', `Invalid individual contact AID: ${String(value)}`);
370
250
  }
251
+ return id;
252
+ }
253
+ function listAgentOwners(selfAid) {
254
+ const agent = read(ConfigTarget.Agent, { self: selfAid }, { cache: true });
255
+ return [...new Set((agent?.owners ?? []).map(value => String(value || '').trim()).filter(Boolean))];
371
256
  }