evolcore 0.0.2 → 0.0.3
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 +44 -793
- package/dist/agents/claude-runner.js +197 -17
- package/dist/agents/codex-runner.js +46 -3
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +21 -4
- package/dist/channels/contact-bind-code.js +134 -0
- package/dist/channels/dingtalk.js +979 -149
- package/dist/channels/feishu.js +130 -54
- package/dist/channels/wecom-card.js +101 -0
- package/dist/channels/wecom-onboarding.js +82 -0
- package/dist/channels/wecom-state.js +191 -0
- package/dist/channels/wecom.js +755 -163
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +88 -33
- 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 +2 -2
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +92 -97
- package/dist/cli/init.js +63 -26
- 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 +55 -15
- package/dist/cli/version.js +6 -1
- package/dist/config/builtin-role-templates.js +22 -10
- package/dist/config/builtin-roles.js +7 -1
- package/dist/config/config-manager.js +221 -17
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-book-store.js +454 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +156 -303
- package/dist/config/contact-operation-service.js +110 -0
- package/dist/config/peer-role-resolver.js +133 -54
- 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-store.js +0 -2
- package/dist/core/auth/authorization-audit.js +10 -1
- package/dist/core/auth/operation-authorizer.js +2 -0
- package/dist/core/auth/operation-catalog.js +56 -0
- package/dist/core/command/command-handler.js +105 -10
- package/dist/core/command/connect-menu.js +374 -0
- package/dist/core/command/menu-handler.js +114 -16
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-handler.js +28 -18
- package/dist/core/daemon-file-cache.js +12 -6
- package/dist/core/event-catalog.js +70 -0
- package/dist/core/evolagent-registry.js +0 -1
- package/dist/core/evolagent.js +20 -9
- package/dist/core/message/im-renderer.js +2 -0
- package/dist/core/message/message-bridge.js +79 -8
- package/dist/core/message/message-queue.js +10 -0
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/response-engine.js +269 -25
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/tool-policy.js +47 -15
- package/dist/core/protected-paths.js +2 -0
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +61 -5
- package/dist/index.js +62 -6
- package/dist/ipc.js +34 -5
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +77 -2
- package/dist/trigger/patch.js +8 -1
- package/dist/trigger/scheduler.js +3 -1
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- 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/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- 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/contact.md +57 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group.md +12 -3
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +9 -3
- 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 +17 -2
- package/kits/eck_manifest.json +12 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/commands.md +3 -1
- package/package.json +4 -4
- 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
|
@@ -18,6 +18,7 @@ import { buildSessionTurnList } from '../session/session-turns.js';
|
|
|
18
18
|
import { resolveConfigCommand } from '../../config/resolved-config-op.js';
|
|
19
19
|
import { AUTHENTICATED_CONFIG_ROLE_ENV, executeResolvedConfigCommand } from '../../config/config-operation-service.js';
|
|
20
20
|
import { hashArgv } from '../auth/authorization-audit.js';
|
|
21
|
+
import { executeResolvedContactCommand, resolveContactCommand } from '../../config/contact-operation-service.js';
|
|
21
22
|
import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
22
23
|
import { validateModelSelectionForRole } from '../model/model-permission.js';
|
|
23
24
|
import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride } from '../role/runtime-policy.js';
|
|
@@ -353,10 +354,13 @@ export class CommandHandler {
|
|
|
353
354
|
validateTriggerRuntimeFields(definition, role, baseagent) {
|
|
354
355
|
if (!role)
|
|
355
356
|
return undefined;
|
|
357
|
+
const executionBaseagent = definition.execution.type === 'trigger_session'
|
|
358
|
+
? definition.execution.baseagent ?? baseagent
|
|
359
|
+
: baseagent;
|
|
356
360
|
if (definition.execution.model) {
|
|
357
361
|
const modelDecision = validateModelSelectionForRole({
|
|
358
362
|
role,
|
|
359
|
-
baseagent,
|
|
363
|
+
baseagent: executionBaseagent,
|
|
360
364
|
requestedModel: definition.execution.model,
|
|
361
365
|
selfAid: definition.agentAid,
|
|
362
366
|
});
|
|
@@ -364,7 +368,7 @@ export class CommandHandler {
|
|
|
364
368
|
return modelDecision.message || '当前角色不允许使用该 Trigger 模型';
|
|
365
369
|
}
|
|
366
370
|
if (definition.execution.effort) {
|
|
367
|
-
const base =
|
|
371
|
+
const base = executionBaseagent || 'claude';
|
|
368
372
|
const effortDecision = validateRuntimeStringFieldOverride({
|
|
369
373
|
selfAid: definition.agentAid,
|
|
370
374
|
role,
|
|
@@ -447,6 +451,29 @@ export class CommandHandler {
|
|
|
447
451
|
const permissionError = this.validateTriggerPermissionModeForRole(permissionMode, isAdmin, triggerRole.role, schedulerAid);
|
|
448
452
|
if (permissionError)
|
|
449
453
|
return { error: permissionError };
|
|
454
|
+
if (parsed.baseagent !== undefined) {
|
|
455
|
+
if (typeof parsed.baseagent !== 'string' || !parsed.baseagent.trim()) {
|
|
456
|
+
return { error: 'Trigger baseagent 不能为空' };
|
|
457
|
+
}
|
|
458
|
+
if (parsed.executionType !== 'trigger_session') {
|
|
459
|
+
return { error: 'Trigger baseagent 仅适用于 trigger_session' };
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const triggerOwner = this.agentRegistry?.get?.(schedulerAid) ?? this.getOwningAgent(channel);
|
|
463
|
+
const executionBaseagent = parsed.executionType === 'trigger_session'
|
|
464
|
+
? parsed.baseagent?.trim() ?? triggerOwner?.baseagent ?? triggerRole.baseagent
|
|
465
|
+
: undefined;
|
|
466
|
+
if (parsed.executionType === 'trigger_session') {
|
|
467
|
+
if (!executionBaseagent)
|
|
468
|
+
return { error: '无法确定 Trigger 会话的 baseagent' };
|
|
469
|
+
const ownerName = triggerOwner?.name || '<unknown>';
|
|
470
|
+
const available = this.getAvailableBaseagentsForOwner(ownerName);
|
|
471
|
+
if (!available.includes(executionBaseagent)) {
|
|
472
|
+
return {
|
|
473
|
+
error: `Trigger baseagent 不可用:${executionBaseagent}(可用:${available.join(' / ') || '无'})`,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
450
477
|
const definition = {
|
|
451
478
|
$schema_version: 3.1,
|
|
452
479
|
id,
|
|
@@ -460,7 +487,10 @@ export class CommandHandler {
|
|
|
460
487
|
execution: {
|
|
461
488
|
type: parsed.executionType,
|
|
462
489
|
...(script ? { script } : { prompt: parsed.prompt || '' }),
|
|
463
|
-
...(parsed.executionType === 'trigger_session' ? {
|
|
490
|
+
...(parsed.executionType === 'trigger_session' ? {
|
|
491
|
+
thread: parsed.triggerThread ?? 'by_trigger',
|
|
492
|
+
baseagent: executionBaseagent,
|
|
493
|
+
} : {}),
|
|
464
494
|
model: parsed.model,
|
|
465
495
|
effort: parsed.effort,
|
|
466
496
|
...(permissionMode !== undefined ? { permissionMode } : {}),
|
|
@@ -955,9 +985,9 @@ export class CommandHandler {
|
|
|
955
985
|
/**
|
|
956
986
|
* 主命令处理入口
|
|
957
987
|
*/
|
|
958
|
-
async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject) {
|
|
988
|
+
async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext) {
|
|
959
989
|
try {
|
|
960
|
-
const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject);
|
|
990
|
+
const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext);
|
|
961
991
|
return result;
|
|
962
992
|
}
|
|
963
993
|
catch (error) {
|
|
@@ -968,8 +998,8 @@ export class CommandHandler {
|
|
|
968
998
|
throw error;
|
|
969
999
|
}
|
|
970
1000
|
}
|
|
971
|
-
async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject) {
|
|
972
|
-
return await handleSlashCommand.call(this, content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject);
|
|
1001
|
+
async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext) {
|
|
1002
|
+
return await handleSlashCommand.call(this, content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext);
|
|
973
1003
|
}
|
|
974
1004
|
async handleTriggerCommand(scheduler, content, channel, channelId, peerId, isAdmin, messageId, chatType, threadId) {
|
|
975
1005
|
if (content === '/trigger') {
|
|
@@ -1117,8 +1147,13 @@ export class CommandHandler {
|
|
|
1117
1147
|
const nextStr = updated.trigger.nextFireAt ? new Date(updated.trigger.nextFireAt).toLocaleString() : '未计算';
|
|
1118
1148
|
return `✅ 触发器已更新:**${updated.trigger.name}**\n下次触发:${nextStr}`;
|
|
1119
1149
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1150
|
+
const createSubcommand = sub.startsWith('create ')
|
|
1151
|
+
? 'create'
|
|
1152
|
+
: sub.startsWith('set ')
|
|
1153
|
+
? 'set'
|
|
1154
|
+
: undefined;
|
|
1155
|
+
if (createSubcommand) {
|
|
1156
|
+
const args = sub.slice(`${createSubcommand} `.length);
|
|
1122
1157
|
const result = parseTriggerSet(args);
|
|
1123
1158
|
if (!result.ok)
|
|
1124
1159
|
return `❌ ${result.error}`;
|
|
@@ -1131,7 +1166,7 @@ export class CommandHandler {
|
|
|
1131
1166
|
return `❌ 未知子命令。用法:
|
|
1132
1167
|
/trigger — 查看活跃触发器
|
|
1133
1168
|
/trigger list [--all] — 查看所有触发器
|
|
1134
|
-
/trigger
|
|
1169
|
+
/trigger create <参数> — 创建触发器(set 为兼容别名)
|
|
1135
1170
|
/trigger update <名称|ID> <参数> — 修改触发器
|
|
1136
1171
|
常用参数:--delay/--at/--cron/--every、--prompt、--model、--effort、--permission(省略则继承;update 可用 inherit 清除覆盖)
|
|
1137
1172
|
/trigger enable <名称|ID> — 启用触发器
|
|
@@ -1477,6 +1512,66 @@ export class CommandHandler {
|
|
|
1477
1512
|
? { ok: true, result }
|
|
1478
1513
|
: { ok: false, code: result.code, error: result.error, ...(result.data !== undefined ? { data: result.data } : {}) };
|
|
1479
1514
|
}
|
|
1515
|
+
/** Agent-managed contact CLI entrypoint. The daemon owns actor resolution and authorization. */
|
|
1516
|
+
async handleContactOperation(argv, sessionId, delegationToken) {
|
|
1517
|
+
if (!this.agentDelegationRegistry) {
|
|
1518
|
+
return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
|
|
1519
|
+
}
|
|
1520
|
+
const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId);
|
|
1521
|
+
if (!delegation.ok)
|
|
1522
|
+
return { ok: false, code: delegation.code, error: delegation.reason };
|
|
1523
|
+
const session = await this.sessionManager.getSessionById(sessionId);
|
|
1524
|
+
if (!session)
|
|
1525
|
+
return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
|
|
1526
|
+
const grant = delegation.grant;
|
|
1527
|
+
let conversationId;
|
|
1528
|
+
try {
|
|
1529
|
+
conversationId = parsePeerKey(grant.peerKey).channelId;
|
|
1530
|
+
}
|
|
1531
|
+
catch {
|
|
1532
|
+
return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
|
|
1533
|
+
}
|
|
1534
|
+
const resolved = resolveContactCommand(argv);
|
|
1535
|
+
if (!resolved.ok)
|
|
1536
|
+
return { ok: false, code: resolved.code, error: resolved.reason };
|
|
1537
|
+
if (resolved.command.selfAid !== grant.selfAid) {
|
|
1538
|
+
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current Agent contact book can be targeted' };
|
|
1539
|
+
}
|
|
1540
|
+
const subject = buildAuthSubject({
|
|
1541
|
+
selfAid: grant.selfAid,
|
|
1542
|
+
actorId: grant.actorId,
|
|
1543
|
+
channel: grant.channel,
|
|
1544
|
+
channelType: grant.channelType,
|
|
1545
|
+
channelId: conversationId,
|
|
1546
|
+
chatType: grant.chatType,
|
|
1547
|
+
conversationId,
|
|
1548
|
+
processOwners: loadDaemonConfig().owners ?? [],
|
|
1549
|
+
});
|
|
1550
|
+
const decision = authorizeOperation({
|
|
1551
|
+
source: 'agent-tool',
|
|
1552
|
+
subject,
|
|
1553
|
+
intent: {
|
|
1554
|
+
operation: resolved.command.operationId,
|
|
1555
|
+
scope: 'agent',
|
|
1556
|
+
source: 'agent-tool',
|
|
1557
|
+
args: {
|
|
1558
|
+
self: resolved.command.selfAid,
|
|
1559
|
+
...('primaryId' in resolved.command ? { primaryId: resolved.command.primaryId } : {}),
|
|
1560
|
+
},
|
|
1561
|
+
},
|
|
1562
|
+
auditMetadata: {
|
|
1563
|
+
argvHash: hashArgv(resolved.command.canonicalArgv),
|
|
1564
|
+
taskId: grant.taskId,
|
|
1565
|
+
messageId: grant.messageId,
|
|
1566
|
+
},
|
|
1567
|
+
});
|
|
1568
|
+
if (!decision.allow)
|
|
1569
|
+
return { ok: false, code: decision.code, error: decision.reason };
|
|
1570
|
+
const result = await executeResolvedContactCommand(resolved.command, grant.actorId);
|
|
1571
|
+
return result.ok
|
|
1572
|
+
? { ok: true, result }
|
|
1573
|
+
: { ok: false, code: result.code, error: result.error, ...(result.data ? { data: result.data } : {}) };
|
|
1574
|
+
}
|
|
1480
1575
|
setAgentDelegationRegistry(registry) {
|
|
1481
1576
|
this.agentDelegationRegistry = registry;
|
|
1482
1577
|
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
|
|
2
|
+
import { getContactSnapshot, resolveContactView } from '../../config/contact-book.js';
|
|
3
|
+
import { mutateContactBook } from '../../config/contact-book-store.js';
|
|
4
|
+
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
5
|
+
/**
|
|
6
|
+
* Connect Menu — 联系人与关系管理协议
|
|
7
|
+
*
|
|
8
|
+
* 定位:role menu 之上的**薄委派层**。
|
|
9
|
+
* - 角色分配 / admin(assignments、resource=assignment/admin)→ 委派给 role-menu,
|
|
10
|
+
* 复用其 agent 级互斥锁、CAS revision、owner/admin 安全护栏与审计链,绝不自行写 relation 文件。
|
|
11
|
+
* - 群组列表(view=groups)→ 委派给 role-menu 的 view=targets(数据源是 AUN live 目录),
|
|
12
|
+
* 群组不落 contact.json(contact book 只接受个人 AID,group 会被拒绝)。
|
|
13
|
+
* - 联系人(view=contacts / contact,add / block / unblock)→ 直接读写 Contact Book v2,
|
|
14
|
+
* contact.json 是联系人列表的唯一事实来源。
|
|
15
|
+
*/
|
|
16
|
+
const MENU_NAME = 'connect';
|
|
17
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
18
|
+
const MAX_PAGE_SIZE = 200;
|
|
19
|
+
/**
|
|
20
|
+
* Map a connect menu request to its authorization operation id.
|
|
21
|
+
* Read views/options → connect.read; contact writes → connect.write;
|
|
22
|
+
* role assignment/admin delegate to the role.* operations role-menu enforces.
|
|
23
|
+
*/
|
|
24
|
+
export function connectMenuOperation(kind, args, value) {
|
|
25
|
+
if (kind === 'options' || kind === 'query')
|
|
26
|
+
return 'connect.read';
|
|
27
|
+
if (kind === 'action')
|
|
28
|
+
return 'connect.write';
|
|
29
|
+
const resource = String(args?.resource ?? '');
|
|
30
|
+
if (resource === 'admin')
|
|
31
|
+
return 'connect.admin';
|
|
32
|
+
const decoded = decodeUpdateValue(value);
|
|
33
|
+
if (decoded === 'admin')
|
|
34
|
+
return 'connect.admin';
|
|
35
|
+
return decoded === null || decoded === false || decoded === 'revoke'
|
|
36
|
+
? 'connect.revoke'
|
|
37
|
+
: 'connect.assign';
|
|
38
|
+
}
|
|
39
|
+
// ── Menu Entry ──
|
|
40
|
+
export async function handleConnectMenu(req, context) {
|
|
41
|
+
const { self } = context;
|
|
42
|
+
if (!self)
|
|
43
|
+
return fail('INVALID_ARGUMENT', 'selfAid is required for connect menu');
|
|
44
|
+
if (req.args?.self !== self) {
|
|
45
|
+
return fail(req.args && Object.prototype.hasOwnProperty.call(req.args, 'self') ? 'PERMISSION_DENIED' : 'INVALID_ARGUMENT', req.args && Object.prototype.hasOwnProperty.call(req.args, 'self')
|
|
46
|
+
? 'args.self does not match the receiving Agent'
|
|
47
|
+
: 'args.self is required for connect menu');
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
if (req.subtype === 'options')
|
|
51
|
+
return await handleOptions(self);
|
|
52
|
+
if (req.subtype === 'query')
|
|
53
|
+
return await handleQuery(req, self, context);
|
|
54
|
+
if (req.subtype === 'update')
|
|
55
|
+
return await handleUpdate(req, context);
|
|
56
|
+
if (req.subtype === 'action')
|
|
57
|
+
return await handleAction(req, self, context);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return fail(errorCode(error), errorMessage(error), errorData(error));
|
|
61
|
+
}
|
|
62
|
+
return fail('INVALID_ARGUMENT', `Unknown subtype: ${req.subtype}`);
|
|
63
|
+
}
|
|
64
|
+
// ── Options ──
|
|
65
|
+
async function handleOptions(self) {
|
|
66
|
+
const snapshot = getContactSnapshot(self);
|
|
67
|
+
const contactCount = snapshot.contacts.size;
|
|
68
|
+
const blockedCount = snapshot.blockedIndex.size;
|
|
69
|
+
return ok({
|
|
70
|
+
options: [
|
|
71
|
+
{
|
|
72
|
+
option: 'contacts',
|
|
73
|
+
label: '联系人列表',
|
|
74
|
+
description: `查看所有联系人(${contactCount} 人,${blockedCount} 人已拉黑)`,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
option: 'groups',
|
|
78
|
+
label: '群组列表',
|
|
79
|
+
description: '查看所在群组',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// ── Query ──
|
|
85
|
+
async function handleQuery(req, self, context) {
|
|
86
|
+
const args = req.args ?? {};
|
|
87
|
+
const view = String(args.view ?? '');
|
|
88
|
+
if (view === 'contacts')
|
|
89
|
+
return queryContacts(args, self);
|
|
90
|
+
if (view === 'contact')
|
|
91
|
+
return queryContact(req, self);
|
|
92
|
+
// Groups and role assignments are delegated to role-menu (authoritative source).
|
|
93
|
+
// Role *definitions* are out of scope here — use `menu name=role` for those.
|
|
94
|
+
if (view === 'groups') {
|
|
95
|
+
const data = await roleMenuQuery(context, { ...args, view: 'targets', targetType: 'group' });
|
|
96
|
+
return ok({
|
|
97
|
+
self,
|
|
98
|
+
view: 'groups',
|
|
99
|
+
items: (data.items ?? []).map((item) => ({
|
|
100
|
+
groupId: item.groupId,
|
|
101
|
+
peerKey: item.peerKey,
|
|
102
|
+
displayName: item.displayName,
|
|
103
|
+
status: item.status,
|
|
104
|
+
memberCount: item.memberCount,
|
|
105
|
+
roleId: item.explicitDefaultRole ?? null,
|
|
106
|
+
effectiveRoleId: item.baselineRole ?? null,
|
|
107
|
+
effectiveRoleSource: item.baselineRoleSource ?? null,
|
|
108
|
+
relationRevision: item.relationRevision,
|
|
109
|
+
})),
|
|
110
|
+
nextCursor: data.nextCursor ?? null,
|
|
111
|
+
total: data.total,
|
|
112
|
+
dataSource: data.dataSource,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (view === 'assignments') {
|
|
116
|
+
const data = await roleMenuQuery(context, { ...args, view: 'assignments', includeDefaults: false });
|
|
117
|
+
return ok({
|
|
118
|
+
self,
|
|
119
|
+
view: 'assignments',
|
|
120
|
+
...(args.roleId === undefined ? {} : { roleId: args.roleId }),
|
|
121
|
+
policyRevision: data.policyRevision,
|
|
122
|
+
items: (data.items ?? []).map(normalizeAssignmentItem),
|
|
123
|
+
nextCursor: data.nextCursor ?? null,
|
|
124
|
+
dataSource: { ...data.dataSource, kind: 'relation-config' },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return fail('INVALID_ARGUMENT', `Unknown view: ${String(view)}`);
|
|
128
|
+
}
|
|
129
|
+
function queryContacts(args, self) {
|
|
130
|
+
const snapshot = getContactSnapshot(self);
|
|
131
|
+
const page = paginate([...snapshot.contacts.keys()].sort(), args, self, primaryId => primaryId);
|
|
132
|
+
const items = page.items.map(primaryId => contactItem(self, primaryId));
|
|
133
|
+
return ok({
|
|
134
|
+
self,
|
|
135
|
+
view: 'contacts',
|
|
136
|
+
items,
|
|
137
|
+
contactRevision: snapshot.contactRevision,
|
|
138
|
+
nextCursor: page.nextCursor,
|
|
139
|
+
dataSource: { kind: 'contact-book', authoritative: true, complete: true },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function queryContact(req, self) {
|
|
143
|
+
const primaryId = String(req.args?.primaryId ?? '').trim();
|
|
144
|
+
if (!primaryId)
|
|
145
|
+
return fail('INVALID_ARGUMENT', 'primaryId is required');
|
|
146
|
+
const snapshot = getContactSnapshot(self);
|
|
147
|
+
if (!snapshot.contacts.has(primaryId))
|
|
148
|
+
return fail('NOT_FOUND', `Contact was not found: ${primaryId}`);
|
|
149
|
+
return ok({ self, view: 'contact', ...contactItem(self, primaryId), contactRevision: snapshot.contactRevision });
|
|
150
|
+
}
|
|
151
|
+
function contactItem(self, primaryId) {
|
|
152
|
+
const view = resolveContactView(self, primaryId);
|
|
153
|
+
return {
|
|
154
|
+
primaryId,
|
|
155
|
+
displayName: view.displayName ?? null,
|
|
156
|
+
status: view.status,
|
|
157
|
+
blocked: view.blocked,
|
|
158
|
+
isOwner: view.isOwner,
|
|
159
|
+
aliases: view.aliases,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function normalizeAssignmentItem(item) {
|
|
163
|
+
const peerKey = item?.target?.peerKey;
|
|
164
|
+
if (typeof peerKey !== 'string')
|
|
165
|
+
return item;
|
|
166
|
+
try {
|
|
167
|
+
const { channelType, channelId } = parsePeerKey(peerKey);
|
|
168
|
+
if (channelType === 'aun')
|
|
169
|
+
return item;
|
|
170
|
+
return {
|
|
171
|
+
...item,
|
|
172
|
+
target: {
|
|
173
|
+
kind: item.target.kind,
|
|
174
|
+
channelType,
|
|
175
|
+
channelId,
|
|
176
|
+
peerKey,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return item;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// ── Update (delegated to role-menu) ──
|
|
185
|
+
async function handleUpdate(req, context) {
|
|
186
|
+
const args = req.args ?? {};
|
|
187
|
+
const resource = args.resource;
|
|
188
|
+
if (resource !== 'assignment' && resource !== 'admin') {
|
|
189
|
+
return fail('INVALID_ARGUMENT', `Unknown resource: ${String(resource)}`);
|
|
190
|
+
}
|
|
191
|
+
if (req.value === undefined || !req.value.trim()) {
|
|
192
|
+
return fail('INVALID_ARGUMENT', 'value is required');
|
|
193
|
+
}
|
|
194
|
+
validateWritableTarget(args.target);
|
|
195
|
+
const value = roleMenuUpdateValue(resource, req.value);
|
|
196
|
+
const data = await roleMenuUpdate(context, args, value);
|
|
197
|
+
return ok(data);
|
|
198
|
+
}
|
|
199
|
+
// ── Action (Contact Book v2) ──
|
|
200
|
+
async function handleAction(req, self, context) {
|
|
201
|
+
const action = req.action;
|
|
202
|
+
if (action === 'add')
|
|
203
|
+
return actionAdd(req, self, context);
|
|
204
|
+
if (action === 'block')
|
|
205
|
+
return actionSetStatus(req, self, context, true);
|
|
206
|
+
if (action === 'unblock')
|
|
207
|
+
return actionSetStatus(req, self, context, false);
|
|
208
|
+
return fail('INVALID_ARGUMENT', `Unknown action: ${String(action)}`);
|
|
209
|
+
}
|
|
210
|
+
async function actionAdd(req, self, context) {
|
|
211
|
+
const args = req.args ?? {};
|
|
212
|
+
const primaryId = String(args.primaryId ?? '').trim();
|
|
213
|
+
if (!primaryId)
|
|
214
|
+
return fail('INVALID_ARGUMENT', 'primaryId is required');
|
|
215
|
+
const expectedContactRevision = requireContactRevision(args);
|
|
216
|
+
const hasDisplayName = Object.prototype.hasOwnProperty.call(args, 'displayName');
|
|
217
|
+
if (hasDisplayName && args.displayName !== null && typeof args.displayName !== 'string') {
|
|
218
|
+
return fail('INVALID_ARGUMENT', 'displayName must be a string or null');
|
|
219
|
+
}
|
|
220
|
+
const current = getContactSnapshot(self).contacts.get(primaryId);
|
|
221
|
+
const displayName = hasDisplayName ? args.displayName : current?.displayName ?? null;
|
|
222
|
+
const result = await mutateContactBook({
|
|
223
|
+
selfAid: self,
|
|
224
|
+
actor: context.actorAid ?? 'connect-menu',
|
|
225
|
+
expectedContactRevision,
|
|
226
|
+
mutation: { type: 'set-display-name', primaryId, displayName },
|
|
227
|
+
});
|
|
228
|
+
const storedDisplayName = result.contact.contacts[primaryId]?.displayName ?? null;
|
|
229
|
+
return ok({
|
|
230
|
+
self,
|
|
231
|
+
action: 'add',
|
|
232
|
+
primaryId,
|
|
233
|
+
displayName: storedDisplayName,
|
|
234
|
+
changed: result.changed,
|
|
235
|
+
contactRevision: result.contactRevision,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
async function actionSetStatus(req, self, context, blocked) {
|
|
239
|
+
const args = req.args ?? {};
|
|
240
|
+
const primaryId = String(args.primaryId ?? '').trim();
|
|
241
|
+
if (!primaryId)
|
|
242
|
+
return fail('INVALID_ARGUMENT', 'primaryId is required');
|
|
243
|
+
const expectedContactRevision = requireContactRevision(args);
|
|
244
|
+
const result = await mutateContactBook({
|
|
245
|
+
selfAid: self,
|
|
246
|
+
actor: context.actorAid ?? 'connect-menu',
|
|
247
|
+
expectedContactRevision,
|
|
248
|
+
mutation: { type: 'set-status', primaryId, status: blocked ? 'blocked' : 'active' },
|
|
249
|
+
});
|
|
250
|
+
return ok({
|
|
251
|
+
self,
|
|
252
|
+
action: blocked ? 'block' : 'unblock',
|
|
253
|
+
primaryId,
|
|
254
|
+
changed: result.changed,
|
|
255
|
+
contactRevision: result.contactRevision,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// ── Helpers ──
|
|
259
|
+
function ok(data) {
|
|
260
|
+
return { type: 'menu.response', id: '', name: MENU_NAME, data };
|
|
261
|
+
}
|
|
262
|
+
function fail(code, message, data) {
|
|
263
|
+
return {
|
|
264
|
+
type: 'menu.response',
|
|
265
|
+
id: '',
|
|
266
|
+
name: MENU_NAME,
|
|
267
|
+
error: { code, message, ...(data === undefined ? {} : { data }) },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function errorCode(error) {
|
|
271
|
+
const code = error?.code;
|
|
272
|
+
if (code === 'INVALID_AID')
|
|
273
|
+
return 'INVALID_ARGUMENT';
|
|
274
|
+
if (code === 'OWNER_PROTECTED')
|
|
275
|
+
return 'NOT_ALLOWED';
|
|
276
|
+
if (code === 'ALIAS_CONFLICT')
|
|
277
|
+
return 'CONFLICT';
|
|
278
|
+
if (code === 'INVALID_CONTACT_BOOK')
|
|
279
|
+
return 'INTERNAL_ERROR';
|
|
280
|
+
return typeof code === 'string' ? code : 'INTERNAL_ERROR';
|
|
281
|
+
}
|
|
282
|
+
function errorMessage(error) {
|
|
283
|
+
if (error instanceof Error)
|
|
284
|
+
return error.message;
|
|
285
|
+
const message = error?.message;
|
|
286
|
+
return typeof message === 'string' ? message : String(error);
|
|
287
|
+
}
|
|
288
|
+
function errorData(error) {
|
|
289
|
+
return error && typeof error === 'object' ? error.data : undefined;
|
|
290
|
+
}
|
|
291
|
+
function requireContactRevision(args) {
|
|
292
|
+
if (!Object.prototype.hasOwnProperty.call(args, 'expectedContactRevision')) {
|
|
293
|
+
throw connectError('INVALID_ARGUMENT', 'expectedContactRevision is required');
|
|
294
|
+
}
|
|
295
|
+
if (typeof args.expectedContactRevision !== 'string' || !args.expectedContactRevision) {
|
|
296
|
+
throw connectError('INVALID_ARGUMENT', 'expectedContactRevision must be a non-empty string');
|
|
297
|
+
}
|
|
298
|
+
return args.expectedContactRevision;
|
|
299
|
+
}
|
|
300
|
+
function decodeUpdateValue(value) {
|
|
301
|
+
const text = String(value ?? '').trim();
|
|
302
|
+
if (!text)
|
|
303
|
+
return undefined;
|
|
304
|
+
try {
|
|
305
|
+
const decoded = JSON.parse(text);
|
|
306
|
+
if (decoded === null || typeof decoded === 'string' || typeof decoded === 'boolean')
|
|
307
|
+
return decoded;
|
|
308
|
+
}
|
|
309
|
+
catch { }
|
|
310
|
+
return text;
|
|
311
|
+
}
|
|
312
|
+
function roleMenuUpdateValue(resource, value) {
|
|
313
|
+
const decoded = decodeUpdateValue(value);
|
|
314
|
+
if (resource === 'assignment') {
|
|
315
|
+
if (decoded !== null && typeof decoded !== 'string') {
|
|
316
|
+
throw connectError('INVALID_ARGUMENT', 'assignment value must be a role ID or null');
|
|
317
|
+
}
|
|
318
|
+
return JSON.stringify(decoded);
|
|
319
|
+
}
|
|
320
|
+
if (![true, false, null, 'grant', 'revoke'].includes(decoded)) {
|
|
321
|
+
throw connectError('INVALID_ARGUMENT', 'admin value must be grant or revoke');
|
|
322
|
+
}
|
|
323
|
+
return JSON.stringify(decoded);
|
|
324
|
+
}
|
|
325
|
+
function validateWritableTarget(target) {
|
|
326
|
+
if (!target || typeof target !== 'object' || Array.isArray(target))
|
|
327
|
+
return;
|
|
328
|
+
const value = target;
|
|
329
|
+
if (typeof value.channelType === 'string' && value.channelType !== 'aun') {
|
|
330
|
+
throw connectError('NOT_SUPPORTED', 'Native non-AUN assignment writes are not supported in Phase 1');
|
|
331
|
+
}
|
|
332
|
+
const kind = value.kind;
|
|
333
|
+
const logicalId = kind === 'private' ? value.peerAid : kind === 'group' ? value.groupId : undefined;
|
|
334
|
+
if (value.peerKey === undefined || typeof logicalId !== 'string')
|
|
335
|
+
return;
|
|
336
|
+
const expected = formatPeerKey('aun', logicalId);
|
|
337
|
+
if (value.peerKey !== expected) {
|
|
338
|
+
throw connectError('INVALID_ARGUMENT', 'target.peerKey does not match the logical target', {
|
|
339
|
+
expectedPeerKey: expected,
|
|
340
|
+
peerKey: value.peerKey,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function paginate(items, args, self, key) {
|
|
345
|
+
const requestedLimit = Number(args.limit ?? DEFAULT_PAGE_SIZE);
|
|
346
|
+
const limit = Number.isInteger(requestedLimit) && requestedLimit > 0
|
|
347
|
+
? Math.min(requestedLimit, MAX_PAGE_SIZE)
|
|
348
|
+
: DEFAULT_PAGE_SIZE;
|
|
349
|
+
let start = 0;
|
|
350
|
+
if (args.cursor !== undefined && args.cursor !== null) {
|
|
351
|
+
try {
|
|
352
|
+
const decoded = JSON.parse(Buffer.from(String(args.cursor), 'base64url').toString('utf8'));
|
|
353
|
+
if (!decoded || typeof decoded.after !== 'string')
|
|
354
|
+
throw new Error();
|
|
355
|
+
start = items.findIndex(item => key(item) > decoded.after);
|
|
356
|
+
if (start < 0)
|
|
357
|
+
start = items.length;
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
throw connectError('INVALID_ARGUMENT', 'cursor is invalid', { self, field: 'cursor' });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const page = items.slice(start, start + limit);
|
|
364
|
+
const hasMore = start + page.length < items.length;
|
|
365
|
+
return {
|
|
366
|
+
items: page,
|
|
367
|
+
nextCursor: hasMore && page.length
|
|
368
|
+
? Buffer.from(JSON.stringify({ after: key(page[page.length - 1]) }), 'utf8').toString('base64url')
|
|
369
|
+
: null,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function connectError(code, message, data) {
|
|
373
|
+
return Object.assign(new Error(message), { code, ...(data === undefined ? {} : { data }) });
|
|
374
|
+
}
|