evolcore 0.0.31 → 0.0.33
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 +35 -0
- package/bin/install-codex-managed-hooks.mjs +124 -37
- package/dist/agents/claude-runner.js +29 -2
- package/dist/agents/codex/app-server-client.js +3 -0
- package/dist/agents/codex/diagnostics.js +76 -44
- package/dist/agents/codex-runner.js +51 -24
- package/dist/agents/ecagent-runner.js +9 -13
- package/dist/agents/gemini-runner.js +4 -0
- package/dist/aun/aid/store.js +0 -81
- package/dist/aun/group/fs-download.js +1 -10
- package/dist/aun/group/operations.js +0 -1
- package/dist/aun/msg/history.js +1 -0
- package/dist/aun/msg/managed-operation.js +130 -32
- package/dist/aun/msg/p2p.js +0 -1
- package/dist/aun/outbox.js +50 -1
- package/dist/aun/service-proxy.js +12 -18
- package/dist/aun/storage/index.js +29 -0
- package/dist/channels/aun.js +38 -137
- package/dist/channels/daemon.js +257 -2
- package/dist/cli/agent-command.js +64 -31
- package/dist/cli/agent-create.js +43 -24
- package/dist/cli/agent.js +16 -70
- package/dist/cli/aun-commands.js +512 -32
- package/dist/cli/contact.js +8 -2
- package/dist/cli/ctl-command.js +1 -2
- package/dist/cli/daemon-commands.js +82 -13
- package/dist/cli/debug-mode.js +154 -0
- package/dist/cli/diag.js +2 -2
- package/dist/cli/handoff-command.js +18 -5
- package/dist/cli/index.js +81 -64
- package/dist/cli/init.js +2 -2
- package/dist/cli/managed-command-guard.js +5 -0
- package/dist/cli/memory-command.js +93 -12
- package/dist/cli/model.js +14 -138
- package/dist/cli/queue-command.js +51 -0
- package/dist/cli/response.js +5 -54
- package/dist/cli/session-command.js +194 -0
- package/dist/cli/skill-command.js +85 -3
- package/dist/cli/stats.js +10 -4
- package/dist/cli/task-command.js +1 -0
- package/dist/cli/task-context.js +16 -0
- package/dist/cli/thread-command.js +620 -0
- package/dist/cli/trigger-command.js +76 -50
- package/dist/config/builtin-roles.js +9 -7
- package/dist/config/config-field-policy.js +2 -3
- package/dist/config/config-manager.js +7 -2
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config-store.js +15 -0
- package/dist/core/agent-ipc-registration.js +65 -22
- package/dist/core/auth/agent-delegation.js +42 -2
- package/dist/core/auth/debug-context.js +139 -0
- package/dist/core/auth/delegation-authorization.js +1 -0
- package/dist/core/auth/operation-authorizer.js +24 -1
- package/dist/core/auth/operation-catalog.js +44 -3
- package/dist/core/background-session/session-cleanup.js +212 -0
- package/dist/core/background-session/types.js +10 -0
- package/dist/core/bootstrap-messages.js +1 -1
- package/dist/core/bootstrap-service.js +6 -3
- package/dist/core/command/command-handler.js +224 -45
- package/dist/core/command/diag-service.js +5 -1
- package/dist/core/command/memory-slash.js +6 -2
- package/dist/core/command/menu-catalog.js +48 -1
- package/dist/core/command/menu-handler.js +92 -16
- package/dist/core/command/menu-protocol.js +24 -1
- package/dist/core/command/skill-slash.js +322 -0
- package/dist/core/command/slash-gate.js +6 -4
- package/dist/core/command/slash-handler.js +193 -13
- package/dist/core/command/topic-slash.js +528 -0
- package/dist/core/evolagent-registry.js +20 -5
- package/dist/core/handoff/ipc-registration.js +38 -0
- package/dist/core/handoff/runtime.js +5 -11
- package/dist/core/handoff/store.js +5 -0
- package/dist/core/ipc-registration.js +210 -9
- package/dist/core/memory/consolidation-context.js +49 -16
- package/dist/core/memory/consolidation-ops-records.js +35 -0
- package/dist/core/memory/consolidation-queue-format.js +21 -0
- package/dist/core/memory/consolidation.js +174 -180
- package/dist/core/message/aun-ipc-sender.js +207 -8
- package/dist/core/message/im-renderer.js +149 -6
- package/dist/core/message/message-bridge.js +13 -3
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +131 -3
- package/dist/core/message/peer-mode.js +6 -1
- package/dist/core/message/response-engine.js +379 -69
- package/dist/core/message/subtask-tool-projection.js +140 -0
- package/dist/core/model/catalog-service.js +17 -12
- package/dist/core/permission/ec-command-parser.js +126 -7
- package/dist/core/permission/sandbox-runtime.js +14 -1
- package/dist/core/session/session-idle-monitor.js +249 -22
- package/dist/core/session/session-manager.js +54 -9
- package/dist/core/skill/command-parser.js +13 -0
- package/dist/core/skill/index-generator.js +4 -1
- package/dist/core/skill/startup.js +41 -0
- package/dist/core/skill/switch-resolver.js +55 -29
- package/dist/core/task/task-recovery-service.js +2 -0
- package/dist/core/task/task-resume-operation.js +11 -0
- package/dist/core/thread/command-parser.js +125 -0
- package/dist/core/thread/index.js +1 -0
- package/dist/core/thread/ipc-operation.js +95 -0
- package/dist/core/thread/thread-context-storage.js +131 -0
- package/dist/core/thread/thread-operation.js +258 -0
- package/dist/core/thread/thread-route.js +33 -0
- package/dist/core/thread/thread-service.js +777 -0
- package/dist/core/thread/types.js +1 -0
- package/dist/eck/message-renderer.js +35 -0
- package/dist/index.js +10 -7
- package/dist/ipc.js +234 -17
- package/dist/paths.js +23 -0
- package/dist/response-system/engines/v1/proactive-flow.js +169 -80
- package/dist/trigger/authorization.js +59 -29
- package/dist/trigger/feedback.js +7 -1
- package/dist/trigger/ipc-service.js +49 -35
- package/dist/trigger/parser.js +63 -1
- package/dist/trigger/script-sandbox.js +58 -77
- package/dist/trigger/validation.js +14 -0
- package/dist/utils/error-utils.js +22 -1
- package/kits/docs/AUTHORING.md +1 -1
- package/kits/docs/channels/aun.md +8 -0
- package/kits/docs/context-assembly.md +1 -0
- package/kits/docs/evolcore/INDEX.md +3 -2
- package/kits/docs/evolcore/agent-create.md +39 -28
- package/kits/docs/evolcore/agent.md +19 -4
- package/kits/docs/evolcore/ec-capabilities.md +4 -0
- package/kits/docs/evolcore/group-collaboration.md +3 -5
- package/kits/docs/evolcore/group.md +3 -4
- package/kits/docs/evolcore/memory-consolidation.md +1 -1
- package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
- package/kits/docs/evolcore/menu-protocol.md +7 -0
- package/kits/docs/evolcore/msg.md +5 -8
- package/kits/docs/evolcore/task.md +2 -0
- package/kits/docs/evolcore/thread-chain.md +267 -0
- package/kits/docs/evolcore/trigger.md +2 -2
- package/kits/docs/prompt-loading-architecture.md +1 -0
- package/kits/eck_manifest.json +61 -26
- package/kits/eck_message_manifest.json +169 -10
- package/kits/rules/01-overview.md +0 -9
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +7 -1
- package/kits/rules/07-memory-and-thread.md +202 -0
- package/kits/schemas/relation-config.schema.9.json +18 -3
- package/kits/schemas/role-config.schema.1.json +6 -6
- package/kits/schemas/single-session.schema.1.json +6 -6
- package/kits/schemas/single-session.schema.2.json +6 -6
- package/kits/schemas/single-session.schema.3.json +6 -6
- package/kits/skills/.switches.json +12 -1
- package/kits/skills/ec-group/SKILL.md +234 -0
- package/kits/skills/ec-skill/SKILL.md +10 -1
- package/kits/skills/ec-skill/scripts/cli.mjs +15 -1
- package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
- package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
- package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
- package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
- package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
- package/kits/skills/thread-ops/SKILL.md +492 -0
- package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
- package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
- package/kits/skills/wechat-article-reader/SKILL.md +97 -0
- package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
- package/kits/templates/bootstrap-welcome.md +7 -7
- package/kits/templates/memory-consolidation/forget.md +5 -0
- package/kits/templates/memory-consolidation/generalize.md +5 -0
- package/kits/templates/memory-consolidation/profile.md +5 -0
- package/kits/templates/memory-consolidation/promote.md +5 -0
- package/kits/templates/message-fragments/item.md +2 -1
- package/kits/templates/message-fragments/thread-context.md +6 -0
- package/kits/templates/message-fragments/trigger-session.md +2 -0
- package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
- package/kits/templates/roles/member.json +2 -0
- package/kits/templates/roles/visitor.json +2 -0
- package/kits/templates/system-fragments/bootstrap.md +66 -72
- package/kits/templates/system-fragments/channel.md +3 -6
- package/kits/templates/system-fragments/commands.md +2 -1
- package/kits/templates/system-fragments/managed-runtime.md +15 -0
- package/kits/templates/system-fragments/reply.md +3 -2
- package/kits/templates/system-fragments/session.md +5 -14
- package/package.json +3 -1
- package/kits/docs/evolcore/group-fs.md +0 -17
- package/kits/docs/venues/aun-group.md +0 -45
- package/kits/docs/venues/group.md +0 -43
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* topic-slash.ts — `/thread` 斜杠命令:话题查看与管理
|
|
3
|
+
*
|
|
4
|
+
* 权限(沿用 /memory 的两级模型):
|
|
5
|
+
* - 只读(list / show / history / summary show / summary log / peers):
|
|
6
|
+
* 私聊所有角色可看;群聊仅 admin+ 可看
|
|
7
|
+
* - 写(create / rename / label / status / delete / summary set / summary clear):
|
|
8
|
+
* 仅 admin+(群聊同样要求 admin,且只能操作本群关系)
|
|
9
|
+
* - rebuild-index:仅 admin+,且默认只报告不写盘(--write 才落盘)
|
|
10
|
+
*
|
|
11
|
+
* 操作对象定位:默认当前对端(私聊=对端 AID,群聊=群 ID);显式 <ref> 支持
|
|
12
|
+
* `current`、`<peer>`(该对端主会话)、`<peer>/<threadId>` 三种,与 `ec thread` 一致。
|
|
13
|
+
* 不支持跨关系浏览以外的越权:非本关系的话题需要显式给出 peer,且按同一套角色判定。
|
|
14
|
+
*/
|
|
15
|
+
import { ThreadService, ThreadServiceError } from '../thread/thread-service.js';
|
|
16
|
+
const TOPIC_STATUSES = ['pending', 'in_progress', 'completed', 'archived'];
|
|
17
|
+
const USAGE = [
|
|
18
|
+
'用法(更名与总结默认作用于当前话题,无需手写 ref):',
|
|
19
|
+
' /thread rename <新名称> 重命名当前话题',
|
|
20
|
+
' /thread summary set <内容> --name <新标题|-> 写入 summary(--name 必填;- 不改名)',
|
|
21
|
+
' /thread summary 查看当前话题 summary',
|
|
22
|
+
' /thread summary log 查看 summary 历史版本',
|
|
23
|
+
' /thread summary clear 清空 summary(历史保留)',
|
|
24
|
+
'',
|
|
25
|
+
' /thread 当前对端的话题列表(等价 /thread list)',
|
|
26
|
+
' /thread list all 本节点所有对端的话题列表',
|
|
27
|
+
' /thread list [ref] [--status <s>] [--label <l>] [--page <n>]',
|
|
28
|
+
' /thread peers 列出有话题记录的所有对端',
|
|
29
|
+
' /thread show [ref] 查看话题元数据',
|
|
30
|
+
' /thread history [ref] [--limit <n>] [--after <ts>] [--before <ts>]',
|
|
31
|
+
' 读取话题历史消息(--after 上次 summary 的 coveredThroughTs 读增量)',
|
|
32
|
+
'',
|
|
33
|
+
' /thread create <名称> [--label <l>] [--status <s>] 新建话题',
|
|
34
|
+
' /thread label [ref] <标签|--clear> 设置/清除标签',
|
|
35
|
+
' /thread status [ref] <状态> 更新话题业务状态',
|
|
36
|
+
' /thread delete [ref] 删除话题(session 文件移入 _trash)',
|
|
37
|
+
' /thread rebuild-index [--write] 检查/重建话题索引',
|
|
38
|
+
'',
|
|
39
|
+
'ref 格式: current | <peer> | <peer>/<threadId>',
|
|
40
|
+
' 省略时:更名/总结取当前话题,list/show/history 取当前对端;',
|
|
41
|
+
' 也可用 --ref <ref> 显式指定(写子命令用它比位置参数更稳)。',
|
|
42
|
+
`状态值: ${TOPIC_STATUSES.join(' | ')}`,
|
|
43
|
+
].join('\n');
|
|
44
|
+
const result = (text) => ({ kind: 'command.result', text });
|
|
45
|
+
const error = (text) => ({ kind: 'command.error', text: `❌ ${text}` });
|
|
46
|
+
/** 只读子命令:私聊所有角色;群聊仅 admin+。 */
|
|
47
|
+
export function canReadTopicSlash(ctx) {
|
|
48
|
+
return ctx.chatType === 'group' ? ctx.isAdmin : true;
|
|
49
|
+
}
|
|
50
|
+
/** 写子命令:仅 admin+(私聊与群聊同判据)。 */
|
|
51
|
+
export function canWriteTopicSlash(ctx) {
|
|
52
|
+
return ctx.isAdmin;
|
|
53
|
+
}
|
|
54
|
+
/** evolhelp / 菜单是否展示 /thread:能定位到当前关系即可。 */
|
|
55
|
+
export function canUseTopicSlash(ctx) {
|
|
56
|
+
if (!ctx.peerKey)
|
|
57
|
+
return ctx.chatType !== 'group';
|
|
58
|
+
return ctx.chatType === 'group' ? ctx.isAdmin : true;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* evolhelp「话题管理」条目。
|
|
62
|
+
*
|
|
63
|
+
* 排序按使用频次:**更名 / 总结** 排第一档(日常最高频的写入与读取),
|
|
64
|
+
* **话题列表** 排第二档,其余查看类跟随,写操作(新建/状态/删除)与索引维护居后。
|
|
65
|
+
* 只读条目对所有角色展开,写条目仅 admin+。
|
|
66
|
+
*/
|
|
67
|
+
export function topicHelpEntries(ctx) {
|
|
68
|
+
const category = '话题管理';
|
|
69
|
+
const roles = [ctx.role];
|
|
70
|
+
const write = canWriteTopicSlash(ctx);
|
|
71
|
+
const out = [];
|
|
72
|
+
// 第一档:更名 + 总结
|
|
73
|
+
if (write) {
|
|
74
|
+
out.push({ command: '/thread rename', description: '重命名当前话题', args: '<新名称>', category, roles }, { command: '/thread summary set', description: '写入当前话题 summary', args: '<内容>', category, roles });
|
|
75
|
+
}
|
|
76
|
+
out.push({ command: '/thread summary', description: '查看当前话题 summary', args: '[ref]', category, roles }, { command: '/thread summary log', description: '查看 summary 历史版本', args: '[ref]', category, roles });
|
|
77
|
+
// 第二档:本节点话题列表 + 增量续读
|
|
78
|
+
out.push({ command: '/thread', aliases: ['/thread list'], description: '当前对端的话题列表', category, roles }, { command: '/thread list all', description: '本节点所有对端的话题列表', category, roles }, { command: '/thread peers', description: '有话题记录的所有对端', category, roles }, { command: '/thread show', description: '查看话题元数据', args: '[ref]', category, roles }, { command: '/thread history', description: '读取话题历史消息', args: '[ref] [--limit <n>]', category, roles });
|
|
79
|
+
// 第三档:其余写操作与索引维护
|
|
80
|
+
if (write) {
|
|
81
|
+
out.push({ command: '/thread create', description: '新建话题', args: '<名称> [--label <l>]', category, roles }, { command: '/thread label', description: '设置/清除话题标签', args: '[ref] <标签|--clear>', category, roles }, { command: '/thread status', description: '更新话题业务状态', args: '[ref] <状态>', category, roles }, { command: '/thread delete', description: '删除话题', args: '[ref]', category, roles }, { command: '/thread rebuild-index', description: '检查话题索引健康状态', category, roles });
|
|
82
|
+
}
|
|
83
|
+
out.push({ command: '/thread help', description: '全部子命令说明', category, roles });
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 把 ref 解析成 { peer, threadId }。
|
|
88
|
+
*
|
|
89
|
+
* - 省略 / `current`:优先用当前会话所在话题;无话题时退回当前对端主会话
|
|
90
|
+
* - `<peer>`:该对端主会话
|
|
91
|
+
* - `<peer>/<threadId>`:指定话题
|
|
92
|
+
*/
|
|
93
|
+
function resolveRef(raw, ctx, service) {
|
|
94
|
+
const ref = (raw ?? 'current').trim();
|
|
95
|
+
if (ref === 'current' || !ref) {
|
|
96
|
+
if (!ctx.peerKey)
|
|
97
|
+
return error('当前会话没有关系上下文,请显式指定 <peer>');
|
|
98
|
+
// 先按 sessionId 反查话题(主会话反查不到)
|
|
99
|
+
if (ctx.selfAid && ctx.sessionId) {
|
|
100
|
+
const resolved = service.resolveCurrentThread(ctx.selfAid, ctx.sessionId);
|
|
101
|
+
if (resolved)
|
|
102
|
+
return resolved;
|
|
103
|
+
}
|
|
104
|
+
if (ctx.threadId && ctx.selfAid) {
|
|
105
|
+
return { peer: ctx.peerKey, threadId: ctx.threadId };
|
|
106
|
+
}
|
|
107
|
+
return { peer: ctx.peerKey };
|
|
108
|
+
}
|
|
109
|
+
const parsed = ThreadService.resolveRef(ref);
|
|
110
|
+
if (parsed.kind === 'current') {
|
|
111
|
+
return resolveRef(undefined, ctx, service);
|
|
112
|
+
}
|
|
113
|
+
if (parsed.kind === 'peer')
|
|
114
|
+
return { peer: parsed.peer };
|
|
115
|
+
return { peer: parsed.peer, threadId: parsed.threadId };
|
|
116
|
+
}
|
|
117
|
+
function isPayload(v) {
|
|
118
|
+
return typeof v === 'object' && v !== null && 'kind' in v;
|
|
119
|
+
}
|
|
120
|
+
// ── 格式化 ──
|
|
121
|
+
function formatTime(ts) {
|
|
122
|
+
if (!ts)
|
|
123
|
+
return '-';
|
|
124
|
+
const d = new Date(ts);
|
|
125
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
126
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
127
|
+
}
|
|
128
|
+
function truncate(s, n) {
|
|
129
|
+
const flat = s.replace(/\s+/g, ' ').trim();
|
|
130
|
+
return flat.length > n ? `${flat.slice(0, n)}…` : flat;
|
|
131
|
+
}
|
|
132
|
+
function formatThreadLine(t, currentThreadId) {
|
|
133
|
+
const mark = currentThreadId && t.threadId === currentThreadId ? ' ←当前' : '';
|
|
134
|
+
// 标题只放标题本身;状态/标签各自成字段,不与标题拼接
|
|
135
|
+
const bits = [
|
|
136
|
+
t.name,
|
|
137
|
+
t.label ? `标签:${t.label}` : '',
|
|
138
|
+
t.topicStatus ? `状态:${t.topicStatus}` : '',
|
|
139
|
+
formatTime(t.lastActiveAt),
|
|
140
|
+
t.lastMsgSnippet ? truncate(t.lastMsgSnippet, 30) : '',
|
|
141
|
+
].filter(Boolean);
|
|
142
|
+
return ` ${bits.join(' · ')}${mark}\n ref: ${t.peer}/${t.threadId} (${t.msgTotal ?? 0} 条)`;
|
|
143
|
+
}
|
|
144
|
+
function formatThreadList(threads, opts, currentThreadId) {
|
|
145
|
+
if (threads.length === 0)
|
|
146
|
+
return '(没有话题)';
|
|
147
|
+
const lines = [`共 ${opts.total} 个话题(第 ${opts.page} 页,每页 ${opts.limit})`, ''];
|
|
148
|
+
for (const t of threads)
|
|
149
|
+
lines.push(formatThreadLine(t, currentThreadId));
|
|
150
|
+
if (opts.hasMore)
|
|
151
|
+
lines.push('', `还有更多:/thread list <peer> --page ${opts.page + 1}`);
|
|
152
|
+
return lines.join('\n');
|
|
153
|
+
}
|
|
154
|
+
function formatHistory(messages, opts) {
|
|
155
|
+
if (messages.length === 0)
|
|
156
|
+
return '(没有消息)';
|
|
157
|
+
const lines = [`${opts.threadId ? `话题 ${opts.threadId}` : '主会话'} · 共 ${opts.total} 条,显示 ${messages.length} 条`, ''];
|
|
158
|
+
for (const m of messages) {
|
|
159
|
+
const who = m.dir === 'out' ? '→' : '←';
|
|
160
|
+
lines.push(` [${formatTime(m.timestamp)}] ${who} ${truncate(m.content, 120)}`);
|
|
161
|
+
}
|
|
162
|
+
return lines.join('\n');
|
|
163
|
+
}
|
|
164
|
+
/** 解析 `--key value` / `--flag`,其余进 positional。 */
|
|
165
|
+
function parseArgs(argv) {
|
|
166
|
+
const positional = [];
|
|
167
|
+
const flags = {};
|
|
168
|
+
for (let i = 0; i < argv.length; i++) {
|
|
169
|
+
const a = argv[i];
|
|
170
|
+
if (a.startsWith('--')) {
|
|
171
|
+
const key = a.slice(2);
|
|
172
|
+
const next = argv[i + 1];
|
|
173
|
+
if (next === undefined || next.startsWith('--'))
|
|
174
|
+
flags[key] = true;
|
|
175
|
+
else {
|
|
176
|
+
flags[key] = next;
|
|
177
|
+
i++;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
positional.push(a);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { positional, flags };
|
|
185
|
+
}
|
|
186
|
+
function normalizeStatus(raw) {
|
|
187
|
+
if (typeof raw !== 'string')
|
|
188
|
+
return undefined;
|
|
189
|
+
return TOPIC_STATUSES.includes(raw) ? raw : undefined;
|
|
190
|
+
}
|
|
191
|
+
function needStatus(raw) {
|
|
192
|
+
if (typeof raw !== 'string' || !raw)
|
|
193
|
+
return error(`缺少状态值\n可选: ${TOPIC_STATUSES.join(' / ')}`);
|
|
194
|
+
const s = normalizeStatus(raw);
|
|
195
|
+
if (!s)
|
|
196
|
+
return error(`无效状态: ${raw}\n可选: ${TOPIC_STATUSES.join(' / ')}`);
|
|
197
|
+
return s;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 解析 ref 来源。
|
|
201
|
+
*
|
|
202
|
+
* 优先 `--ref <ref>`(写子命令用它最稳,名称里含 `/` 也不会被误判);
|
|
203
|
+
* 否则看首个位置参数是否**明确像 ref**(`current` 或含 `/`)。
|
|
204
|
+
* 都不满足时返回 undefined,由 resolveRef 落到"当前话题 / 当前对端"默认值。
|
|
205
|
+
*/
|
|
206
|
+
function refSource(positional, flags) {
|
|
207
|
+
if (typeof flags.ref === 'string')
|
|
208
|
+
return flags.ref;
|
|
209
|
+
const first = positional[0];
|
|
210
|
+
if (first === 'current' || (first !== undefined && first.includes('/')))
|
|
211
|
+
return first;
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
/** 从 positional 里剔除已被当作 ref 的首参数。 */
|
|
215
|
+
function stripRefArg(positional, ref) {
|
|
216
|
+
return ref !== undefined && positional[0] === ref ? positional.slice(1) : positional;
|
|
217
|
+
}
|
|
218
|
+
export async function handleTopicSlash(ctx) {
|
|
219
|
+
if (!ctx.selfAid)
|
|
220
|
+
return error('无法确定当前 agent AID');
|
|
221
|
+
const service = new ThreadService(ctx.sessionManager);
|
|
222
|
+
const argv = ctx.content.trim().split(/\s+/).slice(1);
|
|
223
|
+
const sub = argv[0];
|
|
224
|
+
const { positional, flags } = parseArgs(argv.slice(1));
|
|
225
|
+
const write = canWriteTopicSlash(ctx);
|
|
226
|
+
try {
|
|
227
|
+
if (sub === 'help')
|
|
228
|
+
return result(USAGE);
|
|
229
|
+
// 只读闸门:群聊中非 admin 一律拦下(写操作另有 canWriteTopicSlash)
|
|
230
|
+
if (!canReadTopicSlash(ctx)) {
|
|
231
|
+
return error('群聊中话题查看仅限管理员,输入 /thread help 查看详情');
|
|
232
|
+
}
|
|
233
|
+
// 无参 / list:话题列表
|
|
234
|
+
if (!sub || sub === 'list') {
|
|
235
|
+
return listThreads(ctx, service, positional, flags);
|
|
236
|
+
}
|
|
237
|
+
if (sub === 'peers')
|
|
238
|
+
return listPeers(ctx, service);
|
|
239
|
+
if (sub === 'show') {
|
|
240
|
+
const ref = refSource(positional, flags);
|
|
241
|
+
const target = resolveRef(ref, ctx, service);
|
|
242
|
+
if (isPayload(target))
|
|
243
|
+
return target;
|
|
244
|
+
if (!target.threadId)
|
|
245
|
+
return error('当前是主会话,不显示话题元数据;请指定 <peer>/<threadId>');
|
|
246
|
+
const t = service.findInIndex(ctx.selfAid, target.peer, target.threadId);
|
|
247
|
+
if (!t)
|
|
248
|
+
return error(`话题不存在: ${target.peer}/${target.threadId}`);
|
|
249
|
+
const summary = service.showSummary(ctx.selfAid, t.sessionId);
|
|
250
|
+
const lines = [
|
|
251
|
+
`话题: ${t.name}`,
|
|
252
|
+
`ref: ${t.peer}/${t.threadId}`,
|
|
253
|
+
`session: ${t.sessionId}`,
|
|
254
|
+
`对端: ${t.peer}(${t.peerKind === 'group' ? '群' : '私聊'})`,
|
|
255
|
+
`状态: ${t.topicStatus}`,
|
|
256
|
+
`标签: ${t.label ?? '(无)'}`,
|
|
257
|
+
`创建: ${formatTime(t.createdAt)} 最近活跃: ${formatTime(t.lastActiveAt)}`,
|
|
258
|
+
`消息: ${t.msgTotal ?? 0} 条(24h 内 ${t.msg24h ?? 0} 条)`,
|
|
259
|
+
];
|
|
260
|
+
if (summary)
|
|
261
|
+
lines.push('', `summary (rev ${summary.rev}): ${truncate(summary.content, 300)}`);
|
|
262
|
+
return result(lines.join('\n'));
|
|
263
|
+
}
|
|
264
|
+
if (sub === 'history') {
|
|
265
|
+
const ref = refSource(positional, flags);
|
|
266
|
+
const target = resolveRef(ref, ctx, service);
|
|
267
|
+
if (isPayload(target))
|
|
268
|
+
return target;
|
|
269
|
+
const limit = typeof flags.limit === 'string' ? parseInt(flags.limit, 10) : 20;
|
|
270
|
+
if (!Number.isFinite(limit) || limit <= 0)
|
|
271
|
+
return error('--limit 必须是正整数');
|
|
272
|
+
const h = await service.historyThread({
|
|
273
|
+
selfAid: ctx.selfAid,
|
|
274
|
+
peer: target.peer,
|
|
275
|
+
threadId: target.threadId,
|
|
276
|
+
limit,
|
|
277
|
+
});
|
|
278
|
+
return result(formatHistory(h.messages, { total: h.total, peer: h.peer, threadId: h.threadId }));
|
|
279
|
+
}
|
|
280
|
+
if (sub === 'summary') {
|
|
281
|
+
// 必须 await:summaryCommand 内部的异步异常要落进本函数的 try/catch,
|
|
282
|
+
// 否则会成为未处理拒绝,用户拿到的是超时/静默而不是错误文案。
|
|
283
|
+
return await summaryCommand(ctx, service, positional, flags, write);
|
|
284
|
+
}
|
|
285
|
+
// ── 以下为写操作 ──
|
|
286
|
+
if (!write) {
|
|
287
|
+
return error(ctx.chatType === 'group'
|
|
288
|
+
? '群聊中话题管理仅限管理员,输入 /thread help 查看可用的只读命令'
|
|
289
|
+
: '无权限:话题管理仅限管理员');
|
|
290
|
+
}
|
|
291
|
+
if (sub === 'create') {
|
|
292
|
+
const name = positional[0];
|
|
293
|
+
if (!name)
|
|
294
|
+
return error('用法: /thread create <名称> [--label <标签>] [--status <状态>]');
|
|
295
|
+
if (!ctx.peerKey)
|
|
296
|
+
return error('当前会话没有关系上下文,无法新建话题');
|
|
297
|
+
const status = flags.status !== undefined ? needStatus(flags.status) : undefined;
|
|
298
|
+
if (isPayload(status))
|
|
299
|
+
return status;
|
|
300
|
+
if (flags.status !== undefined && !status)
|
|
301
|
+
return error('无效状态值');
|
|
302
|
+
const created = await service.createThread({
|
|
303
|
+
selfAid: ctx.selfAid,
|
|
304
|
+
peer: ctx.peerKey,
|
|
305
|
+
peerKind: ctx.chatType === 'group' ? 'group' : 'private',
|
|
306
|
+
name,
|
|
307
|
+
...(typeof flags.label === 'string' ? { label: flags.label } : {}),
|
|
308
|
+
...(status ? { topicStatus: status } : {}),
|
|
309
|
+
});
|
|
310
|
+
return result(`✅ 已创建话题: ${created.name}\nref: ${created.peer}/${created.threadId}`);
|
|
311
|
+
}
|
|
312
|
+
if (sub === 'rename') {
|
|
313
|
+
const ref = refSource(positional, flags);
|
|
314
|
+
const name = stripRefArg(positional, ref).join(' ').trim();
|
|
315
|
+
if (!name)
|
|
316
|
+
return error('用法: /thread rename <新名称>(默认改当前话题;跨话题用 --ref <peer>/<threadId>)');
|
|
317
|
+
const target = resolveRef(ref, ctx, service);
|
|
318
|
+
if (isPayload(target))
|
|
319
|
+
return target;
|
|
320
|
+
if (!target.threadId)
|
|
321
|
+
return error('不能重命名主会话;请指定 <peer>/<threadId> 或用 --ref');
|
|
322
|
+
const r = await service.renameThread({ selfAid: ctx.selfAid, peer: target.peer, threadId: target.threadId, name });
|
|
323
|
+
return result(r.changed ? `✅ 已重命名为 ${r.name}` : `话题名称已是 ${r.name}`);
|
|
324
|
+
}
|
|
325
|
+
if (sub === 'label') {
|
|
326
|
+
const ref = refSource(positional, flags);
|
|
327
|
+
const target = resolveRef(ref, ctx, service);
|
|
328
|
+
if (isPayload(target))
|
|
329
|
+
return target;
|
|
330
|
+
if (!target.threadId)
|
|
331
|
+
return error('不能标记主会话;请指定 <peer>/<threadId>');
|
|
332
|
+
const clear = flags.clear === true;
|
|
333
|
+
const labelArg = stripRefArg(positional, ref).join(' ').trim();
|
|
334
|
+
if (!clear && !labelArg)
|
|
335
|
+
return error('用法: /thread label <标签> 或 /thread label --clear');
|
|
336
|
+
const label = clear ? null : labelArg;
|
|
337
|
+
await service.labelThread({ selfAid: ctx.selfAid, peer: target.peer, threadId: target.threadId, label });
|
|
338
|
+
return result(clear ? '✅ 已清除标签' : `✅ 已设置标签: ${label}`);
|
|
339
|
+
}
|
|
340
|
+
if (sub === 'status') {
|
|
341
|
+
const ref = refSource(positional, flags);
|
|
342
|
+
const target = resolveRef(ref, ctx, service);
|
|
343
|
+
if (isPayload(target))
|
|
344
|
+
return target;
|
|
345
|
+
if (!target.threadId)
|
|
346
|
+
return error('不能更新主会话状态;请指定 <peer>/<threadId>');
|
|
347
|
+
const status = needStatus(stripRefArg(positional, ref)[0]);
|
|
348
|
+
if (isPayload(status))
|
|
349
|
+
return status;
|
|
350
|
+
await service.statusThread({ selfAid: ctx.selfAid, peer: target.peer, threadId: target.threadId, topicStatus: status });
|
|
351
|
+
return result(`✅ 状态已更新为 ${status}`);
|
|
352
|
+
}
|
|
353
|
+
if (sub === 'delete') {
|
|
354
|
+
const ref = refSource(positional, flags);
|
|
355
|
+
const target = resolveRef(ref, ctx, service);
|
|
356
|
+
if (isPayload(target))
|
|
357
|
+
return target;
|
|
358
|
+
if (!target.threadId)
|
|
359
|
+
return error('不能删除主会话;请指定 <peer>/<threadId>');
|
|
360
|
+
await service.deleteThread(ctx.selfAid, target.peer, target.threadId);
|
|
361
|
+
return result(`✅ 已删除话题 ${target.threadId}(session 文件移入 _trash)`);
|
|
362
|
+
}
|
|
363
|
+
if (sub === 'rebuild-index') {
|
|
364
|
+
const doWrite = flags.write === true;
|
|
365
|
+
const r = await service.rebuildIndex(ctx.selfAid, !doWrite);
|
|
366
|
+
const parts = doWrite
|
|
367
|
+
? [`✅ 索引已重建(${r.rebuilt} 条)`]
|
|
368
|
+
: [`索引检查:${r.rebuilt} 条可重建`];
|
|
369
|
+
if (r.missing)
|
|
370
|
+
parts.push(`⚠️ ${r.missing} 条 session 文件缺失`);
|
|
371
|
+
if (!doWrite)
|
|
372
|
+
parts.push('', '加 --write 才实际写盘。');
|
|
373
|
+
return result(parts.join('\n'));
|
|
374
|
+
}
|
|
375
|
+
return error(`未知子命令: ${sub}\n\n${USAGE}`);
|
|
376
|
+
}
|
|
377
|
+
catch (e) {
|
|
378
|
+
if (e instanceof ThreadServiceError)
|
|
379
|
+
return error(`${e.message}${e.code ? `(${e.code})` : ''}`);
|
|
380
|
+
return error(e instanceof Error ? e.message : String(e));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async function listThreads(ctx, service, positional, flags) {
|
|
384
|
+
const all = flags.all === true;
|
|
385
|
+
let peer;
|
|
386
|
+
if (!all) {
|
|
387
|
+
const ref = refSource(positional, flags);
|
|
388
|
+
const target = resolveRef(ref, ctx, service);
|
|
389
|
+
if (isPayload(target))
|
|
390
|
+
return target;
|
|
391
|
+
peer = target.peer;
|
|
392
|
+
}
|
|
393
|
+
else if (typeof flags.peer === 'string') {
|
|
394
|
+
peer = flags.peer;
|
|
395
|
+
}
|
|
396
|
+
else if (typeof positional[0] === 'string') {
|
|
397
|
+
peer = positional[0];
|
|
398
|
+
}
|
|
399
|
+
const status = flags.status !== undefined ? normalizeStatus(flags.status) : undefined;
|
|
400
|
+
if (flags.status !== undefined && !status)
|
|
401
|
+
return error(`无效状态: ${String(flags.status)}`);
|
|
402
|
+
const page = typeof flags.page === 'string' ? parseInt(flags.page, 10) : 1;
|
|
403
|
+
const limit = typeof flags.limit === 'string' ? parseInt(flags.limit, 10) : 10;
|
|
404
|
+
const r = service.listThreads({
|
|
405
|
+
selfAid: ctx.selfAid,
|
|
406
|
+
...(peer ? { peer } : {}),
|
|
407
|
+
...(status ? { status } : {}),
|
|
408
|
+
...(typeof flags.label === 'string' ? { label: flags.label } : {}),
|
|
409
|
+
...(flags.unlabeled === true ? { unlabeled: true } : {}),
|
|
410
|
+
...(flags.sort === 'created' ? { sort: 'created' } : {}),
|
|
411
|
+
page: Number.isFinite(page) && page > 0 ? page : 1,
|
|
412
|
+
limit: Number.isFinite(limit) && limit > 0 ? limit : 10,
|
|
413
|
+
});
|
|
414
|
+
const current = resolveRef(undefined, ctx, service);
|
|
415
|
+
const currentThreadId = isPayload(current) ? undefined : current.threadId;
|
|
416
|
+
return result(formatThreadList(r.threads, r, currentThreadId));
|
|
417
|
+
}
|
|
418
|
+
function listPeers(ctx, service) {
|
|
419
|
+
const peers = service.listPeers(ctx.selfAid);
|
|
420
|
+
if (peers.length === 0)
|
|
421
|
+
return result('(没有任何对端的话题记录)');
|
|
422
|
+
const lines = [`${peers.length} 个对端:`, ''];
|
|
423
|
+
for (const p of peers) {
|
|
424
|
+
lines.push(` ${p.peer} ${p.peerKind === 'group' ? '群' : '私聊'} ${p.threadCount} 个话题 ${formatTime(p.lastActiveAt)}`);
|
|
425
|
+
}
|
|
426
|
+
return result(lines.join('\n'));
|
|
427
|
+
}
|
|
428
|
+
async function summaryCommand(ctx, service, positional, flags, write) {
|
|
429
|
+
const action = positional[0];
|
|
430
|
+
const rest = positional.slice(1);
|
|
431
|
+
// summary log [ref]
|
|
432
|
+
if (action === 'log') {
|
|
433
|
+
const ref = refSource(rest, flags);
|
|
434
|
+
const target = resolveRef(ref, ctx, service);
|
|
435
|
+
if (isPayload(target))
|
|
436
|
+
return target;
|
|
437
|
+
const sessionId = await sessionIdFor(ctx, service, target);
|
|
438
|
+
if (typeof sessionId !== 'string')
|
|
439
|
+
return sessionId;
|
|
440
|
+
const versions = service.logSummary(ctx.selfAid, sessionId);
|
|
441
|
+
if (versions.length === 0)
|
|
442
|
+
return result('(没有 summary 历史)');
|
|
443
|
+
const lines = [`summary 历史(${versions.length} 个版本)`, ''];
|
|
444
|
+
for (const v of versions)
|
|
445
|
+
lines.push(` rev ${v.rev} ${formatTime(v.updatedAt)} ${truncate(v.content, 120)}`);
|
|
446
|
+
return result(lines.join('\n'));
|
|
447
|
+
}
|
|
448
|
+
// summary set <内容> / summary clear
|
|
449
|
+
if (action === 'set' || action === 'clear') {
|
|
450
|
+
if (!write) {
|
|
451
|
+
return error(ctx.chatType === 'group'
|
|
452
|
+
? '群聊中修改话题 summary 仅限管理员'
|
|
453
|
+
: '无权限:修改话题 summary 仅限管理员');
|
|
454
|
+
}
|
|
455
|
+
const ref = refSource(rest, flags);
|
|
456
|
+
const target = resolveRef(ref, ctx, service);
|
|
457
|
+
if (isPayload(target))
|
|
458
|
+
return target;
|
|
459
|
+
const sessionId = await sessionIdFor(ctx, service, target);
|
|
460
|
+
if (typeof sessionId !== 'string')
|
|
461
|
+
return sessionId;
|
|
462
|
+
if (action === 'clear') {
|
|
463
|
+
service.clearSummary(ctx.selfAid, sessionId);
|
|
464
|
+
return result('✅ 已清空当前 summary(历史版本保留)');
|
|
465
|
+
}
|
|
466
|
+
const content = stripRefArg(rest, ref).join(' ').trim();
|
|
467
|
+
if (!content)
|
|
468
|
+
return error('用法: /thread summary set <内容> --name <新标题|->');
|
|
469
|
+
// --name 必填:显式表达本条摘要对应的标题;不想改名传 `-`。
|
|
470
|
+
if (typeof flags.name !== 'string') {
|
|
471
|
+
return error('缺少 --name <新标题>\n'
|
|
472
|
+
+ '不改名请显式传 --name -,例:\n'
|
|
473
|
+
+ ' /thread summary set 摘要内容 --name -');
|
|
474
|
+
}
|
|
475
|
+
// 覆盖游标由 service 自动记录,无需传参
|
|
476
|
+
const ifRev = typeof flags['if-rev'] === 'string' ? parseInt(flags['if-rev'], 10) : undefined;
|
|
477
|
+
const r = service.setSummary(ctx.selfAid, sessionId, content, Number.isFinite(ifRev) ? ifRev : undefined);
|
|
478
|
+
const cursorNote = r.coveredThroughTs ? `,覆盖至 ${new Date(r.coveredThroughTs).toLocaleString('zh-CN')}` : '';
|
|
479
|
+
// --name:顺带改名。主会话没有话题名,跳过改名校验而不是报错。
|
|
480
|
+
const rawName = flags.name.trim();
|
|
481
|
+
const newName = rawName && rawName !== '-' ? rawName : undefined;
|
|
482
|
+
if (newName && !target.threadId) {
|
|
483
|
+
return result(`✅ summary 已写入(rev ${r.rev}${cursorNote})\n(主会话没有话题名,--name 已忽略)`);
|
|
484
|
+
}
|
|
485
|
+
if (newName && target.threadId) {
|
|
486
|
+
try {
|
|
487
|
+
const renamed = await service.renameThread({
|
|
488
|
+
selfAid: ctx.selfAid, peer: target.peer, threadId: target.threadId, name: newName,
|
|
489
|
+
});
|
|
490
|
+
return result(`✅ summary 已写入(rev ${r.rev}${cursorNote})\n`
|
|
491
|
+
+ `✅ 话题已改名: ${renamed.name}`);
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
const msg = e instanceof ThreadServiceError ? e.message : String(e);
|
|
495
|
+
return result(`✅ summary 已写入(rev ${r.rev}${cursorNote})\n⚠️ 改名失败(摘要已写入): ${msg}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return result(`✅ summary 已写入(rev ${r.rev}${cursorNote})`);
|
|
499
|
+
}
|
|
500
|
+
// summary [ref]:查看
|
|
501
|
+
const ref = refSource(positional, flags);
|
|
502
|
+
const target = resolveRef(ref, ctx, service);
|
|
503
|
+
if (isPayload(target))
|
|
504
|
+
return target;
|
|
505
|
+
const sessionId = await sessionIdFor(ctx, service, target);
|
|
506
|
+
if (typeof sessionId !== 'string')
|
|
507
|
+
return sessionId;
|
|
508
|
+
const entry = service.showSummary(ctx.selfAid, sessionId);
|
|
509
|
+
if (!entry)
|
|
510
|
+
return result('(当前没有 summary)');
|
|
511
|
+
return result(`summary (rev ${entry.rev}, ${formatTime(entry.updatedAt)})\n\n${entry.content}`);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* 从 { peer, threadId } 解析出 sessionId。
|
|
515
|
+
* 主会话(无 threadId)走 showMainSession;话题走索引里的 sessionId。
|
|
516
|
+
*/
|
|
517
|
+
async function sessionIdFor(ctx, service, target) {
|
|
518
|
+
if (target.threadId) {
|
|
519
|
+
const entry = service.findInIndex(ctx.selfAid, target.peer, target.threadId);
|
|
520
|
+
if (!entry)
|
|
521
|
+
return error(`话题不存在: ${target.peer}/${target.threadId}`);
|
|
522
|
+
return entry.sessionId;
|
|
523
|
+
}
|
|
524
|
+
const main = await service.showMainSession(ctx.selfAid, target.peer);
|
|
525
|
+
if (!main)
|
|
526
|
+
return error(`与 ${target.peer} 无活跃主会话`);
|
|
527
|
+
return main.sessionId;
|
|
528
|
+
}
|
|
@@ -2,7 +2,7 @@ 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 { agentPersonalDir } from '../paths.js';
|
|
5
|
+
import { agentPersonalDir, aidLocalDir } from '../paths.js';
|
|
6
6
|
import { invalidateAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
7
7
|
import { loadAllAgents, ensureAgentDirSkeleton, loadAgent, validateAgentConfig, } from '../config-store.js';
|
|
8
8
|
import { ConfigTarget, read as readConfig, resolveEffective } from '../config/config-manager.js';
|
|
@@ -576,12 +576,12 @@ export class EvolAgentRegistry {
|
|
|
576
576
|
if (fs.existsSync(personaPath)) {
|
|
577
577
|
const content = fs.readFileSync(personaPath, 'utf-8');
|
|
578
578
|
const bold = content.match(/我[叫是]\s*\*{1,2}(.+?)\*{1,2}/);
|
|
579
|
-
if (bold?.[1]) {
|
|
579
|
+
if (bold?.[1] && bold[1] !== aid) {
|
|
580
580
|
this.personalNameCache.set(aid, bold[1]);
|
|
581
581
|
return bold[1];
|
|
582
582
|
}
|
|
583
|
-
const plain = content.match(/(?:我是|我叫)\s*([^\s((,,。.)]+)/);
|
|
584
|
-
if (plain?.[1]) {
|
|
583
|
+
const plain = content.match(/(?:我是|我叫)\s*([^\s*_((,,。.)]+)/);
|
|
584
|
+
if (plain?.[1] && plain[1] !== aid) {
|
|
585
585
|
this.personalNameCache.set(aid, plain[1]);
|
|
586
586
|
return plain[1];
|
|
587
587
|
}
|
|
@@ -600,6 +600,21 @@ export class EvolAgentRegistry {
|
|
|
600
600
|
const baseagent = safeInfoValue(() => agent.baseagent, agent.config.active_baseagent ?? '');
|
|
601
601
|
const model = baseagent ? safeInfoValue(() => agent.model, undefined) : undefined;
|
|
602
602
|
const effort = baseagent ? safeInfoValue(() => agent.effort, undefined) : undefined;
|
|
603
|
+
// lastActivity は in-memory のみ更新される。daemon 再起動後は常に undefined に
|
|
604
|
+
// なるため、aun.db-wal(なければ aun.db)の mtime をディスク fallback として使う。
|
|
605
|
+
let lastActivity = agent.lastActivity;
|
|
606
|
+
if (!lastActivity) {
|
|
607
|
+
try {
|
|
608
|
+
const aidPath = aidLocalDir(agent.aid);
|
|
609
|
+
const walPath = path.join(aidPath, 'aun.db-wal');
|
|
610
|
+
const dbPath = path.join(aidPath, 'aun.db');
|
|
611
|
+
const candidate = fs.existsSync(walPath) ? walPath : dbPath;
|
|
612
|
+
if (fs.existsSync(candidate)) {
|
|
613
|
+
lastActivity = fs.statSync(candidate).mtimeMs;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
catch { }
|
|
617
|
+
}
|
|
603
618
|
return {
|
|
604
619
|
name: displayName || agent.name || agent.aid,
|
|
605
620
|
displayName,
|
|
@@ -613,7 +628,7 @@ export class EvolAgentRegistry {
|
|
|
613
628
|
model,
|
|
614
629
|
effort,
|
|
615
630
|
owners,
|
|
616
|
-
lastActivity
|
|
631
|
+
lastActivity,
|
|
617
632
|
activeSessions: agent.activeSessions,
|
|
618
633
|
error: agent.error,
|
|
619
634
|
};
|
|
@@ -2,6 +2,8 @@ import { isManagementRole } from '../../config/builtin-roles.js';
|
|
|
2
2
|
import { hasTrustedFullAccessDelegation, trustedExecutionIdentityInput, } from '../auth/agent-delegation.js';
|
|
3
3
|
import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
4
4
|
import { parsePeerKey } from '../relation/peer-identity.js';
|
|
5
|
+
import { parseReturn } from '../../cli/handoff-command.js';
|
|
6
|
+
import { readTriggerLocalFile } from '../../trigger/local-files.js';
|
|
5
7
|
export function registerHandoffIpc({ ipcServer, agentDelegationRegistry, responseEngine, sessionManager, agentRegistry, handoffRuntime, getProcessOwners, }) {
|
|
6
8
|
const resolveManagedHandoffScope = async (params, operation) => {
|
|
7
9
|
const delegation = agentDelegationRegistry.validate(params.delegationToken, params.sessionId, params.delegationCommandHash);
|
|
@@ -67,6 +69,8 @@ export function registerHandoffIpc({ ipcServer, agentDelegationRegistry, respons
|
|
|
67
69
|
managementRole,
|
|
68
70
|
role: subject.role,
|
|
69
71
|
actorId: delegation.grant.actorId,
|
|
72
|
+
projectPath: session.projectPath,
|
|
73
|
+
sessionRuntimeDir: delegation.grant.sessionRuntimeDir,
|
|
70
74
|
};
|
|
71
75
|
};
|
|
72
76
|
const resolveHandoffQueryScope = async (params, operation) => {
|
|
@@ -87,6 +91,22 @@ export function registerHandoffIpc({ ipcServer, agentDelegationRegistry, respons
|
|
|
87
91
|
const scope = await resolveManagedHandoffScope({ ...params, sessionId: params.sessionId }, 'ec.handoff.return');
|
|
88
92
|
if (!scope.ok)
|
|
89
93
|
return scope;
|
|
94
|
+
if (!params.argv || params.argv[0] !== 'handoff' || params.argv[1] !== 'return') {
|
|
95
|
+
return { ok: false, code: 'ARGUMENT_MISMATCH', error: 'Handoff return requires its approved argv' };
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const parsed = parseReturn(params.argv.slice(2), {
|
|
99
|
+
canonical: true,
|
|
100
|
+
readFile: file => new TextDecoder('utf-8', { fatal: true }).decode(readTriggerLocalFile(file, {
|
|
101
|
+
selfAid: scope.aid, projectPath: scope.projectPath,
|
|
102
|
+
sessionRuntimeDir: scope.sessionRuntimeDir, cwd: scope.projectPath,
|
|
103
|
+
})),
|
|
104
|
+
});
|
|
105
|
+
params = { ...params, ...parsed };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return { ok: false, code: 'HANDOFF_RETURN_CONTENT_FORBIDDEN', error: error instanceof Error ? error.message : String(error) };
|
|
109
|
+
}
|
|
90
110
|
return responseEngine.returnAuthorizedHandoffResult({ sessionId: params.sessionId, handoffId: params.handoffId, content: params.content }, scope.aid);
|
|
91
111
|
};
|
|
92
112
|
const recoverHandoff = async (params) => {
|
|
@@ -98,6 +118,24 @@ export function registerHandoffIpc({ ipcServer, agentDelegationRegistry, respons
|
|
|
98
118
|
if (scope.role !== 'owner') {
|
|
99
119
|
return { ok: false, code: 'ROLE_ACCESS_DENIED', error: 'handoff recovery requires the current Agent owner' };
|
|
100
120
|
}
|
|
121
|
+
if (!params.argv || params.argv[0] !== 'handoff' || params.argv[1] !== 'recover-return') {
|
|
122
|
+
return { ok: false, code: 'ARGUMENT_MISMATCH', error: 'Handoff recovery requires its approved argv' };
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const parsed = parseReturn(params.argv.slice(2), {
|
|
126
|
+
canonical: true,
|
|
127
|
+
readFile: file => new TextDecoder('utf-8', { fatal: true }).decode(readTriggerLocalFile(file, {
|
|
128
|
+
selfAid: scope.aid, projectPath: scope.projectPath,
|
|
129
|
+
sessionRuntimeDir: scope.sessionRuntimeDir, cwd: scope.projectPath,
|
|
130
|
+
})),
|
|
131
|
+
});
|
|
132
|
+
if (!parsed.handoffId)
|
|
133
|
+
return { ok: false, code: 'INVALID_HANDOFF_ID', error: 'Handoff recovery requires an ID' };
|
|
134
|
+
params = { ...params, handoffId: parsed.handoffId, content: parsed.content };
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
return { ok: false, code: 'HANDOFF_RETURN_CONTENT_FORBIDDEN', error: error instanceof Error ? error.message : String(error) };
|
|
138
|
+
}
|
|
101
139
|
return responseEngine.recoverAuthorizedHandoffResult({
|
|
102
140
|
sessionId: params.sessionId,
|
|
103
141
|
handoffId: params.handoffId,
|