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.
- package/CHANGELOG.md +93 -778
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +198 -48
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +203 -8
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +241 -116
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +1005 -260
- package/dist/channels/feishu.js +184 -56
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +1132 -164
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +98 -43
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +4 -3
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +242 -129
- package/dist/cli/init.js +70 -31
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +146 -25
- package/dist/cli/version.js +6 -1
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +42 -17
- package/dist/config/builtin-roles.js +31 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +409 -29
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +622 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +185 -300
- package/dist/config/contact-operation-service.js +116 -0
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +136 -55
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -4
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +16 -1
- package/dist/core/auth/operation-authorizer.js +10 -0
- package/dist/core/auth/operation-catalog.js +105 -1
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +113 -21
- package/dist/core/command/connect-menu.js +599 -0
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +382 -100
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +186 -49
- package/dist/core/daemon-file-cache.js +40 -6
- package/dist/core/event-catalog.js +99 -0
- package/dist/core/evolagent-registry.js +4 -40
- package/dist/core/evolagent.js +20 -9
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +20 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +670 -57
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +195 -10
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +642 -96
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +115 -38
- package/dist/core/protected-paths.js +38 -11
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +86 -8
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +285 -94
- package/dist/ipc.js +114 -8
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +126 -11
- package/dist/trigger/patch.js +12 -2
- package/dist/trigger/scheduler.js +444 -40
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +61 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +15 -6
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +12 -6
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +42 -10
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +21 -7
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +47 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/commands.md +3 -1
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +7 -5
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +0 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigError, ConfigTarget, ensureFile, listFields, read, resolveEffectiveFieldWithSource, resolveEffectiveWithSources, validateConfigFile,
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
+
}
|