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
|
@@ -2,6 +2,58 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { getPackageRoot } from '../../paths.js';
|
|
4
4
|
import { resolveRoot } from '../../paths.js';
|
|
5
|
+
/**
|
|
6
|
+
* 覆盖链求值:给定技能名与三级开关,判定最终是否启用、由谁决定。
|
|
7
|
+
*
|
|
8
|
+
* `resolveSkills`(只要启用集合)与 `resolveAllSkillStates`(要全量+归因)
|
|
9
|
+
* 共用这一份实现,避免两条路径对同一条覆盖链给出不同答案。
|
|
10
|
+
*/
|
|
11
|
+
function evaluateSkillSwitch(name, sets) {
|
|
12
|
+
const b = getSwitch(sets.builtin, name);
|
|
13
|
+
const hasBuiltinLock = b.locked === true;
|
|
14
|
+
if (hasBuiltinLock) {
|
|
15
|
+
return { enabled: b.enabled === true, decidedBy: 'builtin(locked)', locked: true, overridable: false };
|
|
16
|
+
}
|
|
17
|
+
if (name in sets.agent) {
|
|
18
|
+
return { enabled: getSwitch(sets.agent, name).enabled === true, decidedBy: 'agent', locked: false, overridable: true };
|
|
19
|
+
}
|
|
20
|
+
if (name in sets.local) {
|
|
21
|
+
return { enabled: getSwitch(sets.local, name).enabled === true, decidedBy: 'local', locked: false, overridable: true };
|
|
22
|
+
}
|
|
23
|
+
if (name in sets.builtin) {
|
|
24
|
+
return { enabled: b.enabled === true, decidedBy: 'builtin', locked: false, overridable: true };
|
|
25
|
+
}
|
|
26
|
+
// 三级都没有显式开关 → 缺省禁用(装完必须显式启用)
|
|
27
|
+
return { enabled: false, decidedBy: 'default(缺省禁用)', locked: false, overridable: true };
|
|
28
|
+
}
|
|
29
|
+
/** 读取三级开关文件 */
|
|
30
|
+
function readSkillSwitchSets(paths) {
|
|
31
|
+
return {
|
|
32
|
+
builtin: readSwitchesFile(path.join(paths.builtin, '.switches.json')),
|
|
33
|
+
local: readSwitchesFile(path.join(paths.local, '.switches.json')),
|
|
34
|
+
agent: readSwitchesFile(path.join(paths.agent, '.switches.json')),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 全量解析:返回所有已安装技能(含未启用),带开关归因。
|
|
39
|
+
*
|
|
40
|
+
* 与 `resolveSkills` 的区别是保留禁用技能——斜杠菜单和 `ec skill list`
|
|
41
|
+
* 需要展示"装了但没开"的技能,否则用户看不出为什么某个技能没生效。
|
|
42
|
+
*/
|
|
43
|
+
export function resolveAllSkillStates(agentDir, override) {
|
|
44
|
+
const paths = getSkillPaths(agentDir, override);
|
|
45
|
+
const builtinNames = scanSkillNames(paths.builtin);
|
|
46
|
+
const localNames = scanSkillNames(paths.local);
|
|
47
|
+
const sets = readSkillSwitchSets(paths);
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const name of new Set([...builtinNames, ...localNames])) {
|
|
50
|
+
// 本机有 SKILL.md 即算本机技能(本机覆盖内置)
|
|
51
|
+
const source = localNames.has(name) ? 'local' : 'builtin';
|
|
52
|
+
const skillDir = path.join(source === 'local' ? paths.local : paths.builtin, name);
|
|
53
|
+
out.push({ name, source, skillDir, ...evaluateSkillSwitch(name, sets) });
|
|
54
|
+
}
|
|
55
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
56
|
+
}
|
|
5
57
|
function getSkillPaths(agentDir, override) {
|
|
6
58
|
return {
|
|
7
59
|
builtin: override?.builtin ?? path.join(getPackageRoot(), 'kits', 'skills'),
|
|
@@ -73,41 +125,15 @@ export function resolveSkills(agentDir, override) {
|
|
|
73
125
|
const localNames = scanSkillNames(paths.local);
|
|
74
126
|
const allNames = new Set([...builtinNames, ...localNames]);
|
|
75
127
|
// ② 读取三级汇总开关文件
|
|
76
|
-
const
|
|
77
|
-
const localSwitches = readSwitchesFile(path.join(paths.local, '.switches.json'));
|
|
78
|
-
const agentSwitches = readSwitchesFile(path.join(paths.agent, '.switches.json'));
|
|
128
|
+
const sets = readSkillSwitchSets(paths);
|
|
79
129
|
const results = [];
|
|
80
130
|
for (const name of allNames) {
|
|
81
131
|
// ③ 判断来源:本机有 SKILL.md → local,否则 builtin
|
|
82
132
|
const source = localNames.has(name) ? 'local' : 'builtin';
|
|
83
133
|
const skillRoot = source === 'local' ? paths.local : paths.builtin;
|
|
84
134
|
const skillDir = path.join(skillRoot, name);
|
|
85
|
-
// ④
|
|
86
|
-
|
|
87
|
-
// 内置 locked 时,直接由内置 enabled 决定
|
|
88
|
-
if (builtinSwitch.locked) {
|
|
89
|
-
if (builtinSwitch.enabled) {
|
|
90
|
-
results.push({ name, source, enabled: true, skillDir });
|
|
91
|
-
}
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
// 否则从 Agent 级往下找第一个存在的开关
|
|
95
|
-
const agentSwitch = getSwitch(agentSwitches, name);
|
|
96
|
-
if (name in agentSwitches) {
|
|
97
|
-
if (agentSwitch.enabled) {
|
|
98
|
-
results.push({ name, source, enabled: true, skillDir });
|
|
99
|
-
}
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
const localSwitch = getSwitch(localSwitches, name);
|
|
103
|
-
if (name in localSwitches) {
|
|
104
|
-
if (localSwitch.enabled) {
|
|
105
|
-
results.push({ name, source, enabled: true, skillDir });
|
|
106
|
-
}
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
// 三级都没有显式开关 → 用缺省(enabled:false,装完必须显式启用)
|
|
110
|
-
if (builtinSwitch.enabled) {
|
|
135
|
+
// ④ 覆盖链求值(与 resolveAllSkillStates 共用)
|
|
136
|
+
if (evaluateSkillSwitch(name, sets).enabled) {
|
|
111
137
|
results.push({ name, source, enabled: true, skillDir });
|
|
112
138
|
}
|
|
113
139
|
}
|
|
@@ -133,6 +133,8 @@ export class TaskRecoveryService {
|
|
|
133
133
|
agentSessionId: session.agentSessionId, baseagent: session.baseagent,
|
|
134
134
|
automatic: request.automatic === true,
|
|
135
135
|
origin,
|
|
136
|
+
approvalInteractionPolicy: context?.approvalInteractionPolicy
|
|
137
|
+
?? (request.automatic ? 'deny' : 'interactive'),
|
|
136
138
|
};
|
|
137
139
|
const replyContext = replyContextFromSession(session);
|
|
138
140
|
const message = {
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
/** IPC has authenticated the transport; this enforces the Agent/task boundary. */
|
|
2
2
|
export async function executeTaskResume(request, deps) {
|
|
3
|
+
if (request.argv) {
|
|
4
|
+
const argv = request.argv;
|
|
5
|
+
if (argv[0] !== 'task' || argv[1] !== 'resume' || !argv[2]?.trim() || argv[2].startsWith('-')
|
|
6
|
+
|| !(argv.length === 3 || (argv.length === 5 && argv[3] === '--format' && argv[4] === 'json'))) {
|
|
7
|
+
return { ok: false, code: 'INVALID_ARGUMENT', error: 'Invalid task resume argv' };
|
|
8
|
+
}
|
|
9
|
+
request = { ...request, targetSessionId: argv[2].trim() };
|
|
10
|
+
}
|
|
11
|
+
else if (request.sessionId) {
|
|
12
|
+
return { ok: false, code: 'INVALID_REQUEST', error: 'Managed task resume requires argv' };
|
|
13
|
+
}
|
|
3
14
|
const authorization = request.sessionId
|
|
4
15
|
? await deps.authorizeManaged({
|
|
5
16
|
sessionId: request.sessionId,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { isValidAid } from '../../aun/aid/validation.js';
|
|
2
|
+
import { normalizeThreadPeer } from './thread-service.js';
|
|
3
|
+
const STATUSES = new Set(['pending', 'in_progress', 'completed', 'archived']);
|
|
4
|
+
const COMMON_OPTIONS = ['--self', '--format', '--json'];
|
|
5
|
+
const VALUE_OPTIONS = new Set(['--self', '--agent', '--format', '--name', '--label', '--status', '--sort', '--page', '--limit', '--before', '--after', '--if-rev']);
|
|
6
|
+
const OPTIONS = {
|
|
7
|
+
peers: [], list: ['--status', '--label', '--unlabeled', '--sort', '--page', '--limit'],
|
|
8
|
+
show: [], history: ['--limit', '--before', '--after'],
|
|
9
|
+
create: ['--name', '--label', '--status', '--group'], rename: [], label: ['--clear'], status: [],
|
|
10
|
+
delete: [], 'rebuild-index': ['--check', '--agent'], 'check-index': ['--agent'],
|
|
11
|
+
'summary-show': [], 'summary-set': ['--name', '--if-rev'], 'summary-log': [], 'summary-clear': [],
|
|
12
|
+
};
|
|
13
|
+
export function resolveThreadCommandArgv(argv) {
|
|
14
|
+
if (argv[0] !== 'thread' || argv.some(value => typeof value !== 'string' || value.includes('\0')))
|
|
15
|
+
throw new Error('Invalid thread argv');
|
|
16
|
+
const command = argv[1] === 'summary' ? `summary-${argv[2]}` : argv[1];
|
|
17
|
+
if (!Object.hasOwn(OPTIONS, command))
|
|
18
|
+
throw new Error('Unknown thread command');
|
|
19
|
+
const allowed = new Set([...COMMON_OPTIONS, ...OPTIONS[command]]);
|
|
20
|
+
const options = new Map();
|
|
21
|
+
const positional = [];
|
|
22
|
+
for (let index = command.startsWith('summary-') ? 3 : 2; index < argv.length; index++) {
|
|
23
|
+
const value = argv[index];
|
|
24
|
+
if (!value.startsWith('--')) {
|
|
25
|
+
positional.push(value);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!allowed.has(value) || options.has(value))
|
|
29
|
+
throw new Error(`Invalid or duplicate option: ${value}`);
|
|
30
|
+
if (!VALUE_OPTIONS.has(value)) {
|
|
31
|
+
options.set(value, true);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const argument = argv[++index];
|
|
35
|
+
if (argument === undefined || argument.startsWith('--'))
|
|
36
|
+
throw new Error(`Missing value: ${value}`);
|
|
37
|
+
options.set(value, argument);
|
|
38
|
+
}
|
|
39
|
+
const text = (flag) => options.get(flag);
|
|
40
|
+
const number = (flag, minimum = 0) => {
|
|
41
|
+
if (!options.has(flag))
|
|
42
|
+
return undefined;
|
|
43
|
+
const value = Number(text(flag));
|
|
44
|
+
if (!Number.isSafeInteger(value) || value < minimum)
|
|
45
|
+
throw new Error(`Invalid number: ${flag}`);
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
const selfAidValue = text('--self');
|
|
49
|
+
const agentAidValue = text('--agent');
|
|
50
|
+
const selfAid = (agentAidValue ?? selfAidValue)?.replace(/^@/, '');
|
|
51
|
+
if (selfAidValue !== undefined && agentAidValue !== undefined
|
|
52
|
+
&& selfAidValue.replace(/^@/, '') !== agentAidValue.replace(/^@/, '')) {
|
|
53
|
+
throw new Error('--self and --agent must target the same Agent');
|
|
54
|
+
}
|
|
55
|
+
if (selfAid !== undefined && !isValidAid(selfAid))
|
|
56
|
+
throw new Error('Invalid self AID');
|
|
57
|
+
if (text('--format') !== undefined && text('--format') !== 'json')
|
|
58
|
+
throw new Error('Only JSON format is supported');
|
|
59
|
+
const status = text('--status') ?? (command === 'status' ? positional[1] : undefined);
|
|
60
|
+
if (status !== undefined && !STATUSES.has(status))
|
|
61
|
+
throw new Error('Invalid topic status');
|
|
62
|
+
const sort = text('--sort');
|
|
63
|
+
if (sort !== undefined && sort !== 'recent' && sort !== 'created')
|
|
64
|
+
throw new Error('Invalid sort order');
|
|
65
|
+
const maximum = ['peers', 'rebuild-index', 'check-index'].includes(command) ? 0
|
|
66
|
+
: ['rename', 'status', 'label'].includes(command) ? 2 : command === 'summary-set' ? Infinity : 1;
|
|
67
|
+
const minimum = maximum === 0 ? 0 : ['rename', 'status', 'summary-set'].includes(command) ? 2 : 1;
|
|
68
|
+
if (positional.length < minimum || positional.length > maximum)
|
|
69
|
+
throw new Error('Invalid thread arguments');
|
|
70
|
+
const op = (command === 'peers' ? 'list-peers' : command);
|
|
71
|
+
const request = { type: 'thread.op', op, selfAid };
|
|
72
|
+
if (command === 'list' || command === 'create') {
|
|
73
|
+
request.peer = normalizeThreadPeer(positional[0]);
|
|
74
|
+
if (!request.peer)
|
|
75
|
+
throw new Error('Invalid peer');
|
|
76
|
+
}
|
|
77
|
+
else if (maximum !== 0) {
|
|
78
|
+
request.ref = positional[0];
|
|
79
|
+
if (request.ref !== 'current') {
|
|
80
|
+
const pieces = positional[0].split('/');
|
|
81
|
+
if (pieces.length > 2 || !pieces[0] || pieces[1] === '')
|
|
82
|
+
throw new Error('Invalid thread reference');
|
|
83
|
+
request.peer = normalizeThreadPeer(pieces[0]);
|
|
84
|
+
request.threadId = pieces[1];
|
|
85
|
+
if (!request.peer || request.threadId && !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(request.threadId))
|
|
86
|
+
throw new Error('Invalid thread reference');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (command === 'list')
|
|
90
|
+
Object.assign(request, {
|
|
91
|
+
status, label: text('--label'), unlabeled: options.has('--unlabeled'), sort,
|
|
92
|
+
page: number('--page', 1), limit: number('--limit', 1),
|
|
93
|
+
});
|
|
94
|
+
if (command === 'history')
|
|
95
|
+
Object.assign(request, { limit: number('--limit', 1), before: number('--before'), after: number('--after') });
|
|
96
|
+
if (command === 'create') {
|
|
97
|
+
if (!text('--name')?.trim())
|
|
98
|
+
throw new Error('Topic name is required');
|
|
99
|
+
Object.assign(request, { name: text('--name'), label: text('--label'), topicStatus: status, peerKind: options.has('--group') ? 'group' : 'private' });
|
|
100
|
+
}
|
|
101
|
+
if (command === 'rename') {
|
|
102
|
+
if (!positional[1].trim())
|
|
103
|
+
throw new Error('Topic name is required');
|
|
104
|
+
request.name = positional[1];
|
|
105
|
+
}
|
|
106
|
+
if (command === 'label') {
|
|
107
|
+
if (options.has('--clear') ? positional.length !== 1 : positional.length !== 2)
|
|
108
|
+
throw new Error('Specify a label or --clear');
|
|
109
|
+
request.label = options.has('--clear') ? null : positional[1];
|
|
110
|
+
}
|
|
111
|
+
if (command === 'status')
|
|
112
|
+
request.topicStatus = status;
|
|
113
|
+
if (command === 'rebuild-index')
|
|
114
|
+
request.dryRun = options.has('--check');
|
|
115
|
+
if (command === 'summary-set') {
|
|
116
|
+
if (!text('--name')?.trim())
|
|
117
|
+
throw new Error('Summary requires --name; use - to keep the current name');
|
|
118
|
+
request.name = text('--name');
|
|
119
|
+
request.content = positional.slice(1).join(' ');
|
|
120
|
+
if (!request.content.trim())
|
|
121
|
+
throw new Error('Summary content is required');
|
|
122
|
+
request.ifRev = number('--if-rev');
|
|
123
|
+
}
|
|
124
|
+
return request;
|
|
125
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ThreadService, ThreadServiceError, normalizeThreadPeer } from './thread-service.js';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { trustedExecutionIdentityInput } from '../auth/agent-delegation.js';
|
|
2
|
+
import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
3
|
+
import { isManagementRole } from '../../config/builtin-roles.js';
|
|
4
|
+
import { parsePeerKey } from '../relation/peer-identity.js';
|
|
5
|
+
import { resolveThreadCommandArgv } from './command-parser.js';
|
|
6
|
+
import { executeThreadOperation } from './thread-operation.js';
|
|
7
|
+
import { ThreadService } from './thread-service.js';
|
|
8
|
+
export function createThreadOperationExecutor(dependencies) {
|
|
9
|
+
const threadService = new ThreadService(dependencies.sessionManager);
|
|
10
|
+
return async (incoming) => {
|
|
11
|
+
try {
|
|
12
|
+
if (!Array.isArray(incoming.argv))
|
|
13
|
+
return { ok: false, code: 'INVALID_REQUEST', error: 'Thread operations require argv' };
|
|
14
|
+
const request = resolveThreadCommandArgv(incoming.argv);
|
|
15
|
+
const token = typeof incoming.delegationToken === 'string' ? incoming.delegationToken.trim() : '';
|
|
16
|
+
let currentPeer;
|
|
17
|
+
let relationOnly = false;
|
|
18
|
+
if (token) {
|
|
19
|
+
if (!incoming.sessionId)
|
|
20
|
+
return { ok: false, code: 'INVALID_REQUEST', error: 'Caller sessionId is required' };
|
|
21
|
+
const delegation = dependencies.agentDelegationRegistry.validate(token, incoming.sessionId, incoming.delegationCommandHash);
|
|
22
|
+
if (!delegation.ok)
|
|
23
|
+
return { ok: false, code: delegation.code, error: delegation.reason };
|
|
24
|
+
const session = await dependencies.sessionManager.getSessionById(incoming.sessionId);
|
|
25
|
+
if (!session || session.selfAID !== delegation.grant.selfAid)
|
|
26
|
+
return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation does not match the caller session' };
|
|
27
|
+
if (request.selfAid && request.selfAid !== delegation.grant.selfAid)
|
|
28
|
+
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current Agent can be targeted' };
|
|
29
|
+
request.selfAid = delegation.grant.selfAid;
|
|
30
|
+
const peer = parsePeerKey(delegation.grant.peerKey);
|
|
31
|
+
currentPeer = peer.channelId;
|
|
32
|
+
const subject = buildAuthSubject({
|
|
33
|
+
selfAid: request.selfAid, actorId: delegation.grant.actorId,
|
|
34
|
+
channel: delegation.grant.channel, channelType: delegation.grant.channelType,
|
|
35
|
+
channelId: currentPeer, conversationId: currentPeer, chatType: delegation.grant.chatType,
|
|
36
|
+
permissionMode: delegation.grant.permissionMode, processOwners: dependencies.getProcessOwners(),
|
|
37
|
+
...trustedExecutionIdentityInput(delegation.grant),
|
|
38
|
+
});
|
|
39
|
+
relationOnly = subject.dataScope === 'relation';
|
|
40
|
+
const readOnly = ['list', 'list-peers', 'show', 'history', 'check-index', 'summary-show', 'summary-log'].includes(request.op);
|
|
41
|
+
const management = isManagementRole(subject.role) || subject.processRole === 'fullaccess-run';
|
|
42
|
+
if (delegation.grant.chatType === 'group' && !management) {
|
|
43
|
+
return { ok: false, code: 'NOT_ALLOWED', error: 'Group thread operations require an administrator role' };
|
|
44
|
+
}
|
|
45
|
+
if (!readOnly && (!management || subject.permissionMode === 'readonly'))
|
|
46
|
+
return { ok: false, code: 'NOT_ALLOWED', error: 'Thread changes require a writable administrator task' };
|
|
47
|
+
const globalOperation = ['list-peers', 'check-index', 'rebuild-index'].includes(request.op);
|
|
48
|
+
const decision = authorizeOperation({
|
|
49
|
+
source: 'agent-tool', subject,
|
|
50
|
+
intent: { operation: `ec.thread.${request.op}`, scope: globalOperation && !(relationOnly && request.op === 'list-peers') ? 'agent' : 'relation', source: 'agent-tool', args: { self: request.selfAid, peer: request.peer ?? currentPeer, peerKey: delegation.grant.peerKey } },
|
|
51
|
+
auditMetadata: { taskId: delegation.grant.taskId, messageId: delegation.grant.messageId },
|
|
52
|
+
});
|
|
53
|
+
if (!decision.allow)
|
|
54
|
+
return { ok: false, code: decision.code, error: decision.reason };
|
|
55
|
+
if (relationOnly && globalOperation && request.op !== 'list-peers')
|
|
56
|
+
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Index management requires Agent scope' };
|
|
57
|
+
if (request.ref === 'current') {
|
|
58
|
+
const indexed = threadService.resolveCurrentThread(request.selfAid, session.id);
|
|
59
|
+
request.peer = indexed?.peer ?? currentPeer;
|
|
60
|
+
request.threadId = indexed?.threadId ?? session.threadId ?? undefined;
|
|
61
|
+
request.sessionId = session.id;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
if (incoming.sessionId || incoming.delegationToken || incoming.localControl !== true)
|
|
66
|
+
return { ok: false, code: 'INVALID_CONTROL_TOKEN', error: 'Valid local control token is required' };
|
|
67
|
+
if (!request.selfAid)
|
|
68
|
+
return { ok: false, code: 'INVALID_REQUEST', error: 'Host commands require --self' };
|
|
69
|
+
if (request.ref === 'current')
|
|
70
|
+
return { ok: false, code: 'INVALID_REQUEST', error: 'current requires a managed caller session' };
|
|
71
|
+
}
|
|
72
|
+
if (relationOnly && request.peer && request.peer !== currentPeer)
|
|
73
|
+
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current relation can be targeted' };
|
|
74
|
+
if (request.op.startsWith('summary-') && !request.sessionId) {
|
|
75
|
+
const target = request.threadId
|
|
76
|
+
? await threadService.showThread({ selfAid: request.selfAid, peer: request.peer, threadId: request.threadId })
|
|
77
|
+
: await threadService.showMainSession(request.selfAid, request.peer);
|
|
78
|
+
if (!target)
|
|
79
|
+
return { ok: false, code: 'NOT_FOUND', error: 'Target session not found' };
|
|
80
|
+
request.sessionId = target.sessionId;
|
|
81
|
+
}
|
|
82
|
+
if (request.sessionId && !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(request.sessionId))
|
|
83
|
+
return { ok: false, code: 'INVALID_REQUEST', error: 'Invalid target sessionId' };
|
|
84
|
+
const response = await executeThreadOperation(request, threadService, dependencies.sendSystemMsg);
|
|
85
|
+
if (response.ok && request.op === 'delete' && request.threadId)
|
|
86
|
+
dependencies.onDeleted?.(request.selfAid, request.threadId);
|
|
87
|
+
if (response.ok && relationOnly && request.op === 'list-peers')
|
|
88
|
+
response.result = { peers: response.result.peers.filter(entry => entry.peer === currentPeer) };
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return { ok: false, code: 'INVALID_ARGUMENT', error: error instanceof Error ? error.message : String(error) };
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { agentThreadInboxDir, agentThreadInboxPath, agentSessionsDir } from '../../paths.js';
|
|
4
|
+
import { appendJsonl, readAllJsonlLines } from '../session/session-fs-store.js';
|
|
5
|
+
import { logger } from '../../utils/logger.js';
|
|
6
|
+
/**
|
|
7
|
+
* Thread Context 收件箱(thread-inbox)。
|
|
8
|
+
*
|
|
9
|
+
* 跨话题发送成功后,发送方把「我从哪个话题跨到哪个话题发了什么」写进自己的
|
|
10
|
+
* inbox;目标会话下次被唤醒时读取一次、注入渲染、然后物理删除整个文件。
|
|
11
|
+
*
|
|
12
|
+
* 设计要点:
|
|
13
|
+
* - 无「已消费」标记 —— 注入即删除,文件为空即删除文件
|
|
14
|
+
* - 一个目标会话一个文件,多条记录追加(支持多方并发)
|
|
15
|
+
* - 读取时跳过损坏行,不因单行损坏丢失全部提醒
|
|
16
|
+
*/
|
|
17
|
+
export class ThreadContextStorage {
|
|
18
|
+
/** 追加一条 inbox 记录。 */
|
|
19
|
+
append(aid, entry) {
|
|
20
|
+
const filePath = agentThreadInboxPath(aid, entry.targetSessionId);
|
|
21
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
22
|
+
appendJsonl(filePath, entry);
|
|
23
|
+
}
|
|
24
|
+
/** 读取某目标会话的全部 inbox 记录。文件不存在或损坏时返回空数组。 */
|
|
25
|
+
readAll(aid, targetSessionId) {
|
|
26
|
+
const filePath = agentThreadInboxPath(aid, targetSessionId);
|
|
27
|
+
if (!fs.existsSync(filePath))
|
|
28
|
+
return [];
|
|
29
|
+
// readAllJsonlLines 内部已跳过损坏行
|
|
30
|
+
return readAllJsonlLines(filePath).filter(entry => entry && typeof entry.messageId === 'string' && typeof entry.targetSessionId === 'string');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 删除 inbox 文件。注入完成后调用 —— 这是「注入一次」语义的落点。
|
|
34
|
+
* 文件不存在时静默返回。
|
|
35
|
+
*/
|
|
36
|
+
deleteFile(aid, targetSessionId) {
|
|
37
|
+
const filePath = agentThreadInboxPath(aid, targetSessionId);
|
|
38
|
+
try {
|
|
39
|
+
fs.unlinkSync(filePath);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
if (e.code !== 'ENOENT') {
|
|
43
|
+
logger.warn(`[ThreadContext] 删除 inbox 失败: ${filePath}: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** inbox 目录是否为空(无任何待注入记录)。 */
|
|
48
|
+
isEmpty(aid) {
|
|
49
|
+
const dir = agentThreadInboxDir(aid);
|
|
50
|
+
try {
|
|
51
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
if (readAllJsonlLines(path.join(dir, file)).length > 0)
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (e.code === 'ENOENT')
|
|
60
|
+
return true;
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 空洞检测:来源话题在 (afterTimestamp, now] 区间内有多少条真实对话消息。
|
|
66
|
+
*
|
|
67
|
+
* 只统计对话消息(in/out),跳过协议/系统消息。检测失败时返回保守值
|
|
68
|
+
* (hasNewMessages=false + error),不因检测异常打断消息处理主流程。
|
|
69
|
+
*/
|
|
70
|
+
detectGap(aid, originSessionId, afterTimestamp) {
|
|
71
|
+
try {
|
|
72
|
+
const sessionsDir = agentSessionsDir(aid);
|
|
73
|
+
const chatDir = this.findChatDirForSession(sessionsDir, originSessionId);
|
|
74
|
+
if (!chatDir)
|
|
75
|
+
return { hasNewMessages: false, count: 0 };
|
|
76
|
+
const logPath = path.join(chatDir, 'messages.jsonl');
|
|
77
|
+
if (!fs.existsSync(logPath))
|
|
78
|
+
return { hasNewMessages: false, count: 0 };
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const hits = readAllJsonlLines(logPath).filter((m) => {
|
|
81
|
+
const ts = typeof m?.ts === 'number' ? m.ts : undefined;
|
|
82
|
+
if (ts === undefined || ts <= afterTimestamp || ts > now)
|
|
83
|
+
return false;
|
|
84
|
+
// 只算真实对话消息
|
|
85
|
+
return m.dir === 'in' || m.dir === 'out';
|
|
86
|
+
});
|
|
87
|
+
if (hits.length === 0)
|
|
88
|
+
return { hasNewMessages: false, count: 0 };
|
|
89
|
+
const stamps = hits.map((m) => m.ts).sort((a, b) => a - b);
|
|
90
|
+
return {
|
|
91
|
+
hasNewMessages: true,
|
|
92
|
+
count: hits.length,
|
|
93
|
+
firstTimestamp: stamps[0],
|
|
94
|
+
lastTimestamp: stamps[stamps.length - 1],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
logger.warn(`[ThreadContext] 空洞检测失败: session=${originSessionId}: ${e.message}`);
|
|
99
|
+
return { hasNewMessages: false, count: 0, error: '无法检测' };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 按会话 ID 定位 chatDir。sessionId 形如 meta_xxx,其所在 chatDir 由
|
|
104
|
+
* sessions/<channelKey>/ 布局决定,需要扫描定位(与 SessionManager 同策略)。
|
|
105
|
+
*/
|
|
106
|
+
findChatDirForSession(sessionsDir, sessionId) {
|
|
107
|
+
let roots;
|
|
108
|
+
try {
|
|
109
|
+
roots = fs.readdirSync(sessionsDir);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
for (const root of roots) {
|
|
115
|
+
const rootPath = path.join(sessionsDir, root);
|
|
116
|
+
// 顶层:sessions/<channelKey>/<sessionId>.jsonl
|
|
117
|
+
try {
|
|
118
|
+
if (fs.statSync(path.join(rootPath, `${sessionId}.jsonl`)).isFile())
|
|
119
|
+
return rootPath;
|
|
120
|
+
}
|
|
121
|
+
catch { /* 继续找 */ }
|
|
122
|
+
// 话题:sessions/<channelKey>/_threads/<sessionId>.jsonl
|
|
123
|
+
try {
|
|
124
|
+
if (fs.statSync(path.join(rootPath, '_threads', `${sessionId}.jsonl`)).isFile())
|
|
125
|
+
return rootPath;
|
|
126
|
+
}
|
|
127
|
+
catch { /* 继续找 */ }
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|