evolcore 0.0.3 → 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 (143) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +1 -31
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +157 -5
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/channels/aun.js +220 -112
  14. package/dist/channels/daemon.js +18 -4
  15. package/dist/channels/dingtalk.js +52 -137
  16. package/dist/channels/feishu.js +56 -4
  17. package/dist/channels/qqbot.js +23 -1
  18. package/dist/channels/wechat.js +303 -155
  19. package/dist/channels/wecom.js +383 -7
  20. package/dist/cli/aun-commands.js +11 -11
  21. package/dist/cli/handoff-command.js +2 -1
  22. package/dist/cli/init-channel.js +185 -67
  23. package/dist/cli/init.js +7 -5
  24. package/dist/cli/trigger-command.js +92 -11
  25. package/dist/config/access-policy-domain.js +38 -0
  26. package/dist/config/access-policy.js +134 -0
  27. package/dist/config/builtin-role-templates.js +27 -14
  28. package/dist/config/builtin-roles.js +25 -6
  29. package/dist/config/config-field-policy.js +17 -5
  30. package/dist/config/config-manager.js +198 -22
  31. package/dist/config/config-operation-service.js +35 -38
  32. package/dist/config/contact-bind-code.js +274 -0
  33. package/dist/config/contact-book-store.js +173 -5
  34. package/dist/config/contact-book.js +32 -0
  35. package/dist/config/contact-operation-service.js +9 -3
  36. package/dist/config/contact-request-service.js +331 -0
  37. package/dist/config/peer-role-resolver.js +3 -1
  38. package/dist/config/schema-registry.js +49 -24
  39. package/dist/config-store.js +14 -2
  40. package/dist/core/auth/agent-delegation.js +105 -11
  41. package/dist/core/auth/authorization-audit.js +6 -0
  42. package/dist/core/auth/operation-authorizer.js +8 -0
  43. package/dist/core/auth/operation-catalog.js +51 -3
  44. package/dist/core/auth/trigger-authorization.js +15 -0
  45. package/dist/core/bootstrap-service.js +2 -9
  46. package/dist/core/channel-loader.js +6 -2
  47. package/dist/core/command/command-handler.js +10 -13
  48. package/dist/core/command/connect-menu.js +249 -24
  49. package/dist/core/command/evol-menu-version-gate.js +38 -0
  50. package/dist/core/command/group-menu.js +421 -0
  51. package/dist/core/command/menu-handler.js +269 -85
  52. package/dist/core/command/menu-protocol.js +8 -2
  53. package/dist/core/command/menu-token-store.js +102 -0
  54. package/dist/core/command/slash-gate.js +1 -1
  55. package/dist/core/command/slash-handler.js +158 -31
  56. package/dist/core/daemon-file-cache.js +28 -0
  57. package/dist/core/event-catalog.js +29 -0
  58. package/dist/core/evolagent-registry.js +4 -39
  59. package/dist/core/handoff/runtime.js +162 -37
  60. package/dist/core/handoff/store.js +46 -2
  61. package/dist/core/handoff/types.js +1 -0
  62. package/dist/core/inference/text-inference.js +16 -74
  63. package/dist/core/message/file-markers.js +7 -0
  64. package/dist/core/message/im-renderer.js +18 -14
  65. package/dist/core/message/inbound-admission.js +124 -0
  66. package/dist/core/message/message-bridge.js +616 -74
  67. package/dist/core/message/message-log.js +1 -0
  68. package/dist/core/message/message-queue.js +185 -10
  69. package/dist/core/message/peer-mode.js +7 -8
  70. package/dist/core/message/response-engine.js +381 -79
  71. package/dist/core/permission/approval-gateway.js +17 -10
  72. package/dist/core/permission/ec-command-parser.js +101 -46
  73. package/dist/core/permission/tool-policy.js +69 -24
  74. package/dist/core/protected-paths.js +36 -11
  75. package/dist/core/session/session-manager.js +25 -3
  76. package/dist/core/session/session-mapper.js +2 -0
  77. package/dist/core/session/session-renew.js +125 -69
  78. package/dist/core/session/session-turn-coordinator.js +5 -1
  79. package/dist/eck/kit-renderer.js +12 -1
  80. package/dist/eck/message-renderer.js +79 -1
  81. package/dist/index.js +224 -89
  82. package/dist/ipc.js +81 -4
  83. package/dist/paths.js +3 -0
  84. package/dist/response-system/config-resolver.js +29 -0
  85. package/dist/response-system/coordinator.js +8 -32
  86. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  87. package/dist/response-system/index.js +1 -0
  88. package/dist/response-system/modes/single-session/index.js +7 -4
  89. package/dist/trigger/parser.js +55 -15
  90. package/dist/trigger/patch.js +4 -1
  91. package/dist/trigger/scheduler.js +441 -39
  92. package/dist/utils/error-dict.json +7 -0
  93. package/dist/utils/evolcore-version.js +21 -0
  94. package/dist/utils/logger.js +2 -2
  95. package/dist/utils/stable-semver.js +21 -0
  96. package/dist/utils/stats.js +33 -9
  97. package/kits/docs/channels/aun.md +4 -13
  98. package/kits/docs/evolcore/INDEX.md +1 -1
  99. package/kits/docs/evolcore/config.md +47 -2
  100. package/kits/docs/evolcore/contact.md +7 -3
  101. package/kits/docs/evolcore/group-rules.md +46 -4
  102. package/kits/docs/evolcore/group.md +4 -4
  103. package/kits/docs/evolcore/msg.md +5 -5
  104. package/kits/docs/evolcore/trigger.md +25 -8
  105. package/kits/docs/path-registry.md +36 -17
  106. package/kits/eck_message_manifest.json +12 -1
  107. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  108. package/kits/rules/01-overview.md +17 -7
  109. package/kits/rules/02-navigation.md +38 -18
  110. package/kits/rules/03-identity.md +28 -24
  111. package/kits/rules/04-relation.md +44 -28
  112. package/kits/rules/05-venue.md +31 -15
  113. package/kits/rules/06-channel.md +11 -7
  114. package/kits/schemas/_meta.json +18 -7
  115. package/kits/schemas/agent-config.schema.7.json +303 -0
  116. package/kits/schemas/agent-config.schema.8.json +304 -0
  117. package/kits/schemas/agent-config.schema.9.json +374 -0
  118. package/kits/schemas/contact-book.schema.3.json +67 -0
  119. package/kits/schemas/daemon.schema.1.json +3 -2
  120. package/kits/schemas/daemon.schema.2.json +101 -0
  121. package/kits/schemas/daemon.schema.3.json +123 -0
  122. package/kits/schemas/defaults.schema.2.json +85 -0
  123. package/kits/schemas/defaults.schema.3.json +73 -0
  124. package/kits/schemas/relation-config.schema.6.json +46 -0
  125. package/kits/schemas/relation-config.schema.7.json +59 -0
  126. package/kits/schemas/single-session.schema.2.json +57 -0
  127. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  128. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  129. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  130. package/kits/templates/roles/admin.json +46 -0
  131. package/kits/templates/roles/member.json +4 -0
  132. package/kits/templates/roles/visitor.json +4 -0
  133. package/kits/templates/system-fragments/channel.md +12 -2
  134. package/kits/templates/system-fragments/identity.md +3 -1
  135. package/kits/templates/system-fragments/relation.md +1 -1
  136. package/kits/templates/system-fragments/session.md +6 -2
  137. package/package.json +4 -2
  138. package/MIGRATION-0.5.0.md +0 -378
  139. package/ROLE_ACCESS_CONTROL.md +0 -174
  140. package/dist/channels/contact-bind-code.js +0 -134
  141. package/dist/channels/wecom-card.js +0 -101
  142. package/dist/channels/wecom-onboarding.js +0 -82
  143. package/dist/channels/wecom-state.js +0 -191
@@ -0,0 +1,102 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from 'crypto';
2
+ import path from 'path';
3
+ import { agentDataDir } from '../../paths.js';
4
+ import { atomicReadJson, atomicWriteJson } from '../../utils/atomic-write.js';
5
+ export const DEFAULT_MENU_TOKEN_IDLE_TIMEOUT_SECONDS = 30 * 24 * 60 * 60;
6
+ export const MAX_MENU_TOKENS_PER_AGENT = 1_000;
7
+ function hashToken(token) {
8
+ return createHash('sha256').update(token).digest('hex');
9
+ }
10
+ function isTokenRecord(value) {
11
+ if (!value || typeof value !== 'object' || Array.isArray(value))
12
+ return false;
13
+ const record = value;
14
+ return typeof record.peerAid === 'string'
15
+ && record.peerAid.trim().length > 0
16
+ && typeof record.tokenHash === 'string'
17
+ && /^[a-f0-9]{64}$/.test(record.tokenHash)
18
+ && typeof record.requestedAt === 'number'
19
+ && Number.isFinite(record.requestedAt)
20
+ && record.requestedAt >= 0
21
+ && typeof record.lastUsedAt === 'number'
22
+ && Number.isFinite(record.lastUsedAt)
23
+ && record.lastUsedAt >= 0
24
+ && typeof record.idleTimeout === 'number'
25
+ && Number.isInteger(record.idleTimeout)
26
+ && record.idleTimeout > 0;
27
+ }
28
+ function isExpired(record, now) {
29
+ return now - record.lastUsedAt >= record.idleTimeout * 1000;
30
+ }
31
+ function hashesEqual(left, right) {
32
+ const leftBytes = Buffer.from(left, 'hex');
33
+ const rightBytes = Buffer.from(right, 'hex');
34
+ return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes);
35
+ }
36
+ export function menuTokenStorePath(selfAid) {
37
+ return path.join(agentDataDir(selfAid), 'menu-tokens.json');
38
+ }
39
+ export class MenuTokenStore {
40
+ maxRecords;
41
+ constructor(maxRecords = MAX_MENU_TOKENS_PER_AGENT) {
42
+ this.maxRecords = maxRecords;
43
+ }
44
+ issue(selfAid, peerAid, metadata = {}, now = Date.now()) {
45
+ const file = this.read(selfAid);
46
+ const idleTimeout = DEFAULT_MENU_TOKEN_IDLE_TIMEOUT_SECONDS;
47
+ const tokens = file.tokens.filter(record => record.peerAid !== peerAid && !isExpired(record, now));
48
+ if (tokens.length >= this.maxRecords) {
49
+ if (tokens.length !== file.tokens.length)
50
+ this.write(selfAid, tokens);
51
+ return null;
52
+ }
53
+ const menuToken = `mt_${randomBytes(32).toString('base64url')}`;
54
+ tokens.push({
55
+ peerAid,
56
+ tokenHash: hashToken(menuToken),
57
+ requestedAt: now,
58
+ lastUsedAt: now,
59
+ idleTimeout,
60
+ ...(metadata.clientName ? { clientName: metadata.clientName } : {}),
61
+ ...(metadata.clientVersion ? { clientVersion: metadata.clientVersion } : {}),
62
+ ...(metadata.evolVersion ? { evolVersion: metadata.evolVersion } : {}),
63
+ });
64
+ this.write(selfAid, tokens);
65
+ return { menuToken, idleTimeout };
66
+ }
67
+ use(selfAid, peerAid, menuToken, now = Date.now()) {
68
+ const file = this.read(selfAid);
69
+ const activeTokens = file.tokens.filter(record => !isExpired(record, now));
70
+ const tokenHash = typeof menuToken === 'string' && menuToken.length > 0 && menuToken.length <= 256
71
+ ? hashToken(menuToken)
72
+ : undefined;
73
+ const matchingIndex = tokenHash
74
+ ? activeTokens.findIndex(record => record.peerAid === peerAid && hashesEqual(record.tokenHash, tokenHash))
75
+ : -1;
76
+ if (matchingIndex < 0) {
77
+ if (activeTokens.length !== file.tokens.length)
78
+ this.write(selfAid, activeTokens);
79
+ return false;
80
+ }
81
+ activeTokens[matchingIndex] = { ...activeTokens[matchingIndex], lastUsedAt: now };
82
+ this.write(selfAid, activeTokens);
83
+ return true;
84
+ }
85
+ read(selfAid) {
86
+ try {
87
+ const value = atomicReadJson(menuTokenStorePath(selfAid));
88
+ if (!value || typeof value !== 'object' || Array.isArray(value))
89
+ return { schemaVersion: 1, tokens: [] };
90
+ const file = value;
91
+ if (file.schemaVersion !== 1 || !Array.isArray(file.tokens))
92
+ return { schemaVersion: 1, tokens: [] };
93
+ return { schemaVersion: 1, tokens: file.tokens.filter(isTokenRecord) };
94
+ }
95
+ catch {
96
+ return { schemaVersion: 1, tokens: [] };
97
+ }
98
+ }
99
+ write(selfAid, tokens) {
100
+ atomicWriteJson(menuTokenStorePath(selfAid), { schemaVersion: 1, tokens });
101
+ }
102
+ }
@@ -71,7 +71,7 @@ export function guardRoleCommand(content, activeChatType, isAdmin) {
71
71
  const userGroupCommands = [
72
72
  '/status', '/help', '/evolhelp', '/check', '/chatmode', '/mentionmode',
73
73
  '/model', '/effort', '/baseagent', '/perm', '/activity', '/stop',
74
- '/resume', '/trigger',
74
+ '/resume', '/trigger', '/file',
75
75
  ];
76
76
  const userCommands = activeChatType === 'group' && !isAdmin
77
77
  ? userGroupCommands
@@ -432,9 +432,23 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
432
432
  ...params,
433
433
  subject: authSubject,
434
434
  });
435
- // roleGuard 仅对进程级命令(/restart /reload)放行 daemon owner 绕过,
435
+ const canAccessSlashOperation = (operation, scope) => {
436
+ const args = scope === 'relation'
437
+ ? buildSlashRelationIntentArgs({ selfAid: authSubject.selfAid, peerKey: authSubject.peerKey })
438
+ : {};
439
+ return authorizeOperation({
440
+ source: 'slash',
441
+ intent: { operation, scope, source: 'slash', args },
442
+ subject: authSubject,
443
+ audit: false,
444
+ }).allow;
445
+ };
446
+ // roleGuard 仅对进程级命令(/restart /upgrade /reload)放行 daemon owner 绕过,
436
447
  // 其余命令严格按 agent-channel 的 isAdmin 判定,不越权。
437
- const isProcessLevelSlash = normalizedContent === '/restart' || normalizedContent === '/reload' || normalizedContent.startsWith('/reload ');
448
+ const isProcessLevelSlash = normalizedContent === '/restart'
449
+ || normalizedContent === '/upgrade'
450
+ || normalizedContent === '/reload'
451
+ || normalizedContent.startsWith('/reload ');
438
452
  const roleGuard = guardRoleCommand(normalizedContent, activeChatType, isAdmin || (isDaemonOwner && isProcessLevelSlash));
439
453
  if (roleGuard)
440
454
  return roleGuard;
@@ -459,18 +473,50 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
459
473
  return undefined;
460
474
  // /help 命令不需要会话
461
475
  if (normalizedContent === '/help') {
476
+ const canReadModel = canAccessSlashOperation('model.current', 'relation')
477
+ || canAccessSlashOperation('model.list', 'relation');
478
+ const canUseModel = canAccessSlashOperation('model.use', 'relation');
479
+ const canSetEffort = canAccessSlashOperation('model.effort', 'relation');
480
+ const canReadPermission = canAccessSlashOperation('permission.current', 'relation');
481
+ const canAnswerPermission = canAccessSlashOperation('permission.answer', 'relation');
482
+ const canReadChatmode = canAccessSlashOperation('chatmode.current', 'relation');
483
+ const canSetChatmode = canAccessSlashOperation('chatmode.update', 'relation');
484
+ const canReadMentionmode = activeChatType === 'group'
485
+ && canAccessSlashOperation('mentionmode.current', 'relation');
486
+ const canSetMentionmode = activeChatType === 'group'
487
+ && canAccessSlashOperation('mentionmode.update', 'relation');
488
+ const canUseFile = canAccessSlashOperation('file.fetch', 'filesystem');
462
489
  const appendProcessCommands = (lines) => {
463
490
  if (!isDaemonOwner)
464
491
  return;
465
- lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /reload [aid] - 热重载 Agent 配置');
492
+ lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置');
466
493
  };
467
494
  if (!isAdmin && activeChatType === 'group') {
468
495
  const lines = [
469
496
  '可用命令:',
497
+ ...((canReadModel || canUseModel || canSetEffort) ? [
498
+ '',
499
+ '🤖 Agent 与模型:',
500
+ ...(canReadModel || canUseModel ? [` /model${canUseModel ? ' [model]' : ''} - 查看${canUseModel ? '或切换' : ''}模型`] : []),
501
+ ...(canSetEffort ? [' /effort [level] - 查看或切换推理强度'] : []),
502
+ ] : []),
503
+ ...((canReadChatmode || canSetChatmode || canReadMentionmode || canSetMentionmode) ? [
504
+ '',
505
+ '💬 聊天设置:',
506
+ ...(canReadChatmode || canSetChatmode ? [` /chatmode${canSetChatmode ? ' [interactive|proactive]' : ''} - 查看${canSetChatmode ? '或切换' : ''}会话模式`] : []),
507
+ ...(canReadMentionmode || canSetMentionmode ? [` /mentionmode${canSetMentionmode ? ' [mention-only|disabled]' : ''} - 查看${canSetMentionmode ? '或切换' : ''}群聊 @ 处理模式`] : []),
508
+ ] : []),
509
+ ...(canReadPermission || canAnswerPermission ? [
510
+ '',
511
+ '🔐 权限管理:',
512
+ ...(canReadPermission ? [' /perm - 查看当前权限模式'] : []),
513
+ ...(canAnswerPermission ? [' /perm allow|always|deny - 审批权限请求'] : []),
514
+ ] : []),
470
515
  '',
471
516
  '其他:',
472
517
  ' /status - 显示会话状态',
473
518
  ' /check - 检查 EvolAgent 实例健康',
519
+ ...(canUseFile ? [' /file <path> - 发送项目内文件'] : []),
474
520
  ' /help - 显示此帮助信息',
475
521
  ];
476
522
  appendProcessCommands(lines);
@@ -487,6 +533,28 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
487
533
  ' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
488
534
  ' /status - 显示会话状态',
489
535
  ' /check - 检查 EvolAgent 实例健康',
536
+ ...((canReadModel || canUseModel || canSetEffort) ? [
537
+ '',
538
+ '🤖 Agent 与模型:',
539
+ ...(canReadModel || canUseModel ? [` /model${canUseModel ? ' [model]' : ''} - 查看${canUseModel ? '或切换' : ''}模型`] : []),
540
+ ...(canSetEffort ? [' /effort [level] - 查看或切换推理强度'] : []),
541
+ ] : []),
542
+ ...((canReadChatmode || canSetChatmode) ? [
543
+ '',
544
+ '💬 聊天设置:',
545
+ ` /chatmode${canSetChatmode ? ' [interactive|proactive]' : ''} - 查看${canSetChatmode ? '或切换' : ''}会话模式`,
546
+ ] : []),
547
+ ...(canReadPermission || canAnswerPermission ? [
548
+ '',
549
+ '🔐 权限管理:',
550
+ ...(canReadPermission ? [' /perm - 查看当前权限模式'] : []),
551
+ ...(canAnswerPermission ? [' /perm allow|always|deny - 审批权限请求'] : []),
552
+ ] : []),
553
+ ...(canUseFile ? [
554
+ '',
555
+ '🧰 工具:',
556
+ ' /file <path> - 发送项目内文件',
557
+ ] : []),
490
558
  '',
491
559
  '❓ 帮助:',
492
560
  ' /help - 显示此帮助信息',
@@ -512,18 +580,18 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
512
580
  '',
513
581
  '🤖 Agent 与模型:',
514
582
  ' /baseagent [name] - 查看或切换 Agent 后端(别名: /base)',
515
- ' /model [model] - 查看或切换模型',
516
- ' /effort [level] - 查看或切换推理强度',
583
+ ...(canReadModel || canUseModel ? [` /model${canUseModel ? ' [model]' : ''} - 查看${canUseModel ? '或切换' : ''}模型`] : []),
584
+ ...(canSetEffort ? [' /effort [level] - 查看或切换推理强度'] : []),
517
585
  '',
518
586
  '💬 聊天设置:',
519
587
  ' /activity [all|text|none] - 查看/控制中间输出显示模式',
520
- ' /chatmode [interactive|proactive] - 查看/切换会话模式(被动响应或主动推进)',
521
- ' /mentionmode [mention-only|disabled] - 查看/切换群聊 @ 处理模式(仅@响应或全部响应,仅群聊)',
588
+ ...(canReadChatmode || canSetChatmode ? [` /chatmode${canSetChatmode ? ' [interactive|proactive]' : ''} - 查看${canSetChatmode ? '/切换' : ''}会话模式(被动响应或主动推进)`] : []),
589
+ ...(canReadMentionmode || canSetMentionmode ? [` /mentionmode${canSetMentionmode ? ' [mention-only|disabled]' : ''} - 查看${canSetMentionmode ? '/切换' : ''}群聊 @ 处理模式`] : []),
522
590
  '',
523
591
  '🔐 权限管理:',
524
- ' /perm - 查看当前权限模式',
525
- ...(isAdmin ? [' 权限模式请通过角色策略编辑器修改'] : []),
526
- ' /perm allow|always|deny - 审批权限请求',
592
+ ...(canReadPermission ? [' /perm - 查看当前权限模式'] : []),
593
+ ...(canReadPermission ? [' 权限模式请通过角色策略编辑器修改'] : []),
594
+ ...(canAnswerPermission ? [' /perm allow|always|deny - 审批权限请求'] : []),
527
595
  '',
528
596
  '🛠️ 运维:',
529
597
  ' /status - 显示会话状态',
@@ -539,7 +607,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
539
607
  ...(!isDaemonOwner && isAdmin ? [
540
608
  ' /reload - 热重载当前 Agent 配置',
541
609
  ] : []),
542
- ...(isAdmin ? [
610
+ ...(canUseFile ? [
543
611
  '',
544
612
  '🧰 工具:',
545
613
  ` /file ${isOwner ? '[channel] ' : ''}<path> - 发送项目内文件`,
@@ -553,6 +621,19 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
553
621
  // /evolhelp 命令:返回 JSON 格式的命令列表(供程序解析)
554
622
  if (normalizedContent === '/evolhelp') {
555
623
  const cmds = [];
624
+ const canReadModel = canAccessSlashOperation('model.current', 'relation')
625
+ || canAccessSlashOperation('model.list', 'relation');
626
+ const canUseModel = canAccessSlashOperation('model.use', 'relation');
627
+ const canSetEffort = canAccessSlashOperation('model.effort', 'relation');
628
+ const canReadPermission = canAccessSlashOperation('permission.current', 'relation');
629
+ const canAnswerPermission = canAccessSlashOperation('permission.answer', 'relation');
630
+ const canReadChatmode = canAccessSlashOperation('chatmode.current', 'relation');
631
+ const canSetChatmode = canAccessSlashOperation('chatmode.update', 'relation');
632
+ const canReadMentionmode = activeChatType === 'group'
633
+ && canAccessSlashOperation('mentionmode.current', 'relation');
634
+ const canSetMentionmode = activeChatType === 'group'
635
+ && canAccessSlashOperation('mentionmode.update', 'relation');
636
+ const canUseFile = canAccessSlashOperation('file.fetch', 'filesystem');
556
637
  // 项目
557
638
  cmds.push({ command: '/pwd', description: '显示当前项目路径', category: '项目', roles: ['admin', 'owner'] });
558
639
  // 会话管理
@@ -566,16 +647,17 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
566
647
  cmds.push({ command: '/compact', description: '压缩会话上下文(减少 token 用量)', category: '会话管理', roles: ['admin', 'owner'] });
567
648
  }
568
649
  // Agent 与模型
569
- if (isAdmin) {
650
+ if (isAdmin)
570
651
  cmds.push({ command: '/baseagent', aliases: ['/base'], args: '[name]', description: '查看或切换 Agent 后端', category: 'Agent 与模型', roles: ['admin', 'owner'] });
571
- cmds.push({ command: '/model', args: '[model]', description: '查看或切换模型', category: 'Agent 与模型', roles: ['admin', 'owner'] });
572
- cmds.push({ command: '/effort', args: '[level]', description: '查看或切换推理强度', category: 'Agent 与模型', roles: ['admin', 'owner'] });
573
- }
652
+ if (canReadModel || canUseModel)
653
+ cmds.push({ command: '/model', ...(canUseModel ? { args: '[model]' } : {}), description: `查看${canUseModel ? '或切换' : ''}模型`, category: 'Agent 与模型', roles: [identity.role] });
654
+ if (canSetEffort)
655
+ cmds.push({ command: '/effort', args: '[level]', description: '查看或切换推理强度', category: 'Agent 与模型', roles: [identity.role] });
574
656
  // 权限管理
575
- if (isAdmin) {
576
- cmds.push({ command: '/perm', description: '查看当前角色决定的权限模式', category: '权限管理', roles: ['admin', 'owner'] });
577
- cmds.push({ command: '/perm', args: 'allow|always|deny', description: '审批权限请求', category: '权限管理', roles: ['admin', 'owner'] });
578
- }
657
+ if (canReadPermission)
658
+ cmds.push({ command: '/perm', description: '查看当前角色决定的权限模式', category: '权限管理', roles: [identity.role] });
659
+ if (canAnswerPermission)
660
+ cmds.push({ command: '/perm', args: 'allow|always|deny', description: '审批权限请求', category: '权限管理', roles: [identity.role] });
579
661
  // 运维
580
662
  cmds.push({ command: '/status', description: '显示会话状态', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] });
581
663
  cmds.push({ command: '/stop', description: '中断当前任务', category: '运维', roles: ['admin', 'owner'] });
@@ -588,19 +670,19 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
588
670
  }
589
671
  if (isDaemonOwner) {
590
672
  cmds.push({ command: '/restart', description: '重启服务', category: '运维', roles: ['daemon-owner'] });
673
+ cmds.push({ command: '/upgrade', description: '检查版本更新', category: '运维', roles: ['daemon-owner'] });
591
674
  cmds.push({ command: '/reload', args: '[aid]', description: '热重载 Agent 配置', category: '运维', roles: ['daemon-owner'] });
592
675
  }
593
676
  if (!isDaemonOwner && isAdmin) {
594
677
  cmds.push({ command: '/reload', description: '热重载当前 Agent 配置', category: '运维', roles: ['admin', 'owner'] });
595
678
  }
596
- if (isAdmin) {
597
- cmds.push({ command: '/file', args: isOwner ? '[channel] <path>' : '<path>', description: '发送项目内文件', category: '工具', roles: ['admin', 'owner'] });
598
- }
679
+ if (canUseFile)
680
+ cmds.push({ command: '/file', args: isOwner ? '[channel] <path>' : '<path>', description: '发送项目内文件', category: '工具', roles: [identity.role] });
599
681
  // 聊天设置
600
- if (isAdmin) {
601
- cmds.push({ command: '/chatmode', args: '[interactive|proactive]', description: '查看/切换会话模式(被动响应或主动推进)', category: '聊天设置', roles: ['admin', 'owner'] });
602
- cmds.push({ command: '/mentionmode', args: '[mention-only|disabled]', description: '查看/切换群聊 @ 处理模式(仅@响应或全部响应)', category: '聊天设置', roles: ['admin', 'owner'] });
603
- }
682
+ if (canReadChatmode || canSetChatmode)
683
+ cmds.push({ command: '/chatmode', ...(canSetChatmode ? { args: '[interactive|proactive]' } : {}), description: `查看${canSetChatmode ? '/切换' : ''}会话模式(被动响应或主动推进)`, category: '聊天设置', roles: [identity.role] });
684
+ if (canReadMentionmode || canSetMentionmode)
685
+ cmds.push({ command: '/mentionmode', ...(canSetMentionmode ? { args: '[mention-only|disabled]' } : {}), description: `查看${canSetMentionmode ? '/切换' : ''}群聊 @ 处理模式`, category: '聊天设置', roles: [identity.role] });
604
686
  // 交互
605
687
  cmds.push({ command: '/ask', args: '<选项>', description: '回答 Agent 的交互式问题', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] });
606
688
  // 帮助
@@ -2083,7 +2165,21 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2083
2165
  await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session));
2084
2166
  }
2085
2167
  const newSessionBaseagent = this.agentRegistry?.resolveByChannel(channel)?.baseagent || this.parseDefaultBaseagent();
2086
- const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, newSessionBaseagent);
2168
+ const previousMetadata = session?.metadata || activeSession?.metadata || {};
2169
+ const newSessionIdentityMetadata = activeChatType === 'group'
2170
+ ? {
2171
+ ...(previousMetadata.channelKey ? { channelKey: previousMetadata.channelKey } : {}),
2172
+ groupId: previousMetadata.groupId || channelId,
2173
+ ...(previousMetadata.groupName ? { groupName: previousMetadata.groupName } : {}),
2174
+ ...(previousMetadata.dispatchMode ? { dispatchMode: previousMetadata.dispatchMode } : {}),
2175
+ }
2176
+ : {
2177
+ ...(previousMetadata.channelKey ? { channelKey: previousMetadata.channelKey } : {}),
2178
+ ...((userId || previousMetadata.peerId) ? { peerId: userId || previousMetadata.peerId } : {}),
2179
+ ...(previousMetadata.peerName ? { peerName: previousMetadata.peerName } : {}),
2180
+ ...(previousMetadata.peerType ? { peerType: previousMetadata.peerType } : {}),
2181
+ };
2182
+ const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, newSessionBaseagent, newSessionIdentityMetadata);
2087
2183
  const previousAgent = getActiveAgentIfAvailable();
2088
2184
  if (session && previousAgent) {
2089
2185
  // Reset agent backend state so the new
@@ -2460,8 +2556,24 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2460
2556
  }
2461
2557
  // /upgrade 命令:检查版本更新,提示用户手动重启
2462
2558
  if (normalizedContent === '/upgrade') {
2463
- if (!isAdmin)
2464
- return { kind: 'command.error', text: '❌ 无权限:升级检查仅限管理员使用' };
2559
+ const upgradeAuthDenied = await authorizeIntent({
2560
+ intent: {
2561
+ operation: 'system.upgrade',
2562
+ scope: 'process',
2563
+ source: 'slash',
2564
+ args: {},
2565
+ dangerous: true,
2566
+ },
2567
+ identity: isDaemonOwner ? { ...identity, role: 'owner' } : identity,
2568
+ session: activeSession,
2569
+ channel,
2570
+ channelId,
2571
+ userId,
2572
+ selfAid: this.agentRegistry?.resolveByChannel(channel)?.aid,
2573
+ isDaemonOwner,
2574
+ });
2575
+ if (upgradeAuthDenied)
2576
+ return upgradeAuthDenied;
2465
2577
  if (isLinkedInstall()) {
2466
2578
  return { kind: 'command.result', text: '⏭ 开发模式,跳过升级检查' };
2467
2579
  }
@@ -2493,11 +2605,26 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2493
2605
  }
2494
2606
  // /file 命令:发送项目内文件,支持 /file path 和 /file channel path
2495
2607
  if (normalizedContent.startsWith('/file')) {
2496
- if (!isAdmin)
2497
- return { kind: 'command.error', text: '❌ 无权限:此命令仅限管理员使用' };
2498
2608
  // 飞书会将 .md 等后缀自动转为 Markdown 链接: foo.md → [foo.md](http://foo.md/)
2499
2609
  // 还原: 将 [text](url) 替换为 text
2500
2610
  const rawArg = normalizedContent.slice(5).trim().replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
2611
+ const fileAuthDenied = await authorizeIntent({
2612
+ intent: {
2613
+ operation: 'file.fetch',
2614
+ scope: 'filesystem',
2615
+ source: 'slash',
2616
+ args: rawArg ? { filePath: rawArg } : {},
2617
+ },
2618
+ identity,
2619
+ session: activeSession,
2620
+ channel,
2621
+ channelId,
2622
+ userId,
2623
+ selfAid: this.agentRegistry?.resolveByChannel(channel)?.aid,
2624
+ isDaemonOwner,
2625
+ });
2626
+ if (fileAuthDenied)
2627
+ return fileAuthDenied;
2501
2628
  if (!rawArg) {
2502
2629
  const usage = isOwner
2503
2630
  ? '用法: /file <相对路径> 或 /file <渠道> <相对路径>\n示例: /file src/index.ts\n示例: /file feishu report.md'
@@ -214,6 +214,34 @@ function readFileOrNull(file) {
214
214
  return null;
215
215
  }
216
216
  }
217
+ /**
218
+ * 带错误捕获的 reader。默认 reader 把所有读盘异常吞成 null,调用方无从区分
219
+ * ENOENT(文件确实没有)与 EACCES/EISDIR(部署或权限事故)——对随包发布的
220
+ * 文件(schema、角色模板)来说这层信息正是定位问题的关键。
221
+ *
222
+ * 用法:建一个实例,把 `read` 交给 fileCache,loader 里拿到 null 时用
223
+ * `lastError()` 拼文案、把 `lastErrorCause()` 挂到新 Error 的 `cause` 上——
224
+ * 后者保住原始异常对象,`code`/`path`/`errno` 这些结构化字段不至于只剩字符串。
225
+ * 注意 loader 只在未命中时执行,与 read 同步成对调用,因此闭包里的 last
226
+ * 不会串到别的文件上。
227
+ */
228
+ export function createErrorCapturingReader() {
229
+ let last = null;
230
+ return {
231
+ read: (file) => {
232
+ try {
233
+ last = null;
234
+ return fs.readFileSync(file, 'utf-8');
235
+ }
236
+ catch (error) {
237
+ last = error;
238
+ return null;
239
+ }
240
+ },
241
+ lastError: () => (last instanceof Error ? last.message : last === null ? 'unreadable' : String(last)),
242
+ lastErrorCause: () => last,
243
+ };
244
+ }
217
245
  /** 缓存值内存占用近似:用读入的原始字符串长度(字节级近似),null 记 0。 */
218
246
  function approxBytes(raw) {
219
247
  return raw === null ? 0 : raw.length;
@@ -89,9 +89,37 @@ const CATALOG = [
89
89
  { path: 'name', type: 'string', optional: true },
90
90
  { path: 'chatType', type: 'string', optional: true },
91
91
  { path: 'threadId', type: 'string', optional: true },
92
+ { path: 'cause', type: 'string', optional: true },
93
+ { path: 'sourceSessionId', type: 'string', optional: true },
94
+ { path: 'renewSource', type: 'string', optional: true },
92
95
  { path: 'timestamp', type: 'number', optional: true },
93
96
  ],
94
97
  },
98
+ {
99
+ type: 'session:renew-evaluated',
100
+ namespace: 'session',
101
+ name: 'renew-evaluated',
102
+ description: 'Session Renew 判定完成',
103
+ fields: [
104
+ { path: 'candidateSessionId', type: 'string' },
105
+ { path: 'selectedSessionId', type: 'string' },
106
+ { path: 'renewed', type: 'boolean' },
107
+ { path: 'decision', type: 'string' },
108
+ { path: 'source', type: 'string' },
109
+ { path: 'channel', type: 'string' },
110
+ { path: 'channelName', type: 'string', optional: true },
111
+ { path: 'channelId', type: 'string' },
112
+ { path: 'selfAid', type: 'string', optional: true },
113
+ { path: 'idleHours', type: 'number' },
114
+ { path: 'responseMode', type: 'string', optional: true },
115
+ { path: 'baseagent', type: 'string', optional: true },
116
+ { path: 'model', type: 'string', optional: true },
117
+ { path: 'effort', type: 'string', optional: true },
118
+ { path: 'fallbackReason', type: 'string', optional: true },
119
+ { path: 'durationMs', type: 'number' },
120
+ { path: 'timestamp', type: 'number' },
121
+ ],
122
+ },
95
123
  {
96
124
  type: 'session:switched',
97
125
  namespace: 'session',
@@ -407,6 +435,7 @@ const CATALOG = [
407
435
  { path: 'description', type: 'string', optional: true },
408
436
  { path: 'subagentType', type: 'string', optional: true },
409
437
  { path: 'status', type: 'string' },
438
+ { path: 'terminalReason', type: 'string', optional: true },
410
439
  { path: 'outputFile', type: 'string', optional: true },
411
440
  { path: 'summary', type: 'string', optional: true },
412
441
  { path: 'usage', type: 'object', optional: true },
@@ -2,7 +2,8 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { EvolAgent } from './evolagent.js';
4
4
  import { logger } from '../utils/logger.js';
5
- import { agentMdPath, agentPersonalDir } from '../paths.js';
5
+ import { agentPersonalDir } from '../paths.js';
6
+ import { invalidateAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
6
7
  import { loadAllAgents, ensureAgentDirSkeleton, loadAgent, validateAgentConfig, } from '../config-store.js';
7
8
  import { resolveEffective } from '../config/config-manager.js';
8
9
  // ── Channel Fingerprint ───────────────────────────────────────────────────
@@ -210,9 +211,8 @@ export class EvolAgentRegistry {
210
211
  return [...this.agents.values()].map(a => this.toInfo(a));
211
212
  }
212
213
  invalidateAgentDisplayCache(aid) {
213
- this.displayNameCache.delete(aid);
214
+ invalidateAgentDisplayName(aid);
214
215
  this.personalNameCache.delete(aid);
215
- this.displayNamePending.delete(aid);
216
216
  }
217
217
  runnableAgents() {
218
218
  return [...this.agents.values()].filter(a => a.status === 'stopped');
@@ -442,43 +442,8 @@ export class EvolAgentRegistry {
442
442
  }
443
443
  return null;
444
444
  }
445
- displayNameCache = new Map();
446
- displayNamePending = new Set();
447
445
  resolveDisplayName(aid) {
448
- const cached = this.displayNameCache.get(aid);
449
- if (cached)
450
- return cached;
451
- try {
452
- const mdPath = agentMdPath(aid);
453
- if (fs.existsSync(mdPath)) {
454
- const content = fs.readFileSync(mdPath, 'utf-8');
455
- const fm = content.match(/^---\n([\s\S]*?)\n---/);
456
- if (fm) {
457
- const nm = fm[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
458
- if (nm?.[1]) {
459
- this.displayNameCache.set(aid, nm[1]);
460
- return nm[1];
461
- }
462
- }
463
- }
464
- }
465
- catch { }
466
- if (!this.displayNamePending.has(aid)) {
467
- this.displayNamePending.add(aid);
468
- import('../aun/aid/index.js').then(({ agentmdGet }) => {
469
- agentmdGet(aid).then(content => {
470
- if (typeof content === 'string') {
471
- const fm = content.match(/^---\n([\s\S]*?)\n---/);
472
- if (fm) {
473
- const nm = fm[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
474
- if (nm?.[1])
475
- this.displayNameCache.set(aid, nm[1]);
476
- }
477
- }
478
- }).catch(() => { }).finally(() => this.displayNamePending.delete(aid));
479
- }).catch(() => { this.displayNamePending.delete(aid); });
480
- }
481
- return undefined;
446
+ return resolveAgentDisplayName(aid);
482
447
  }
483
448
  personalNameCache = new Map();
484
449
  resolvePersonalName(aid) {