evolcore 0.0.32 → 0.0.34
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 +61 -0
- package/bin/ec.js +3 -2
- package/bin/install-codex-managed-hooks.mjs +135 -37
- package/dist/agents/baseagent-availability.js +20 -36
- package/dist/agents/claude-runner.js +111 -15
- package/dist/agents/codex/app-server-client.js +3 -0
- package/dist/agents/codex/diagnostics.js +112 -54
- package/dist/agents/codex-runner.js +241 -63
- package/dist/agents/ecagent-runner.js +78 -33
- package/dist/agents/runner-types.js +5 -6
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/group/operations.js +0 -1
- package/dist/aun/msg/history.js +1 -0
- package/dist/aun/msg/p2p.js +0 -1
- package/dist/aun/outbox.js +50 -1
- package/dist/channels/aun.js +44 -8
- package/dist/channels/daemon.js +261 -3
- package/dist/cli/agent-command.js +58 -24
- package/dist/cli/agent.js +1 -1
- package/dist/cli/aun-commands.js +368 -31
- package/dist/cli/contact.js +8 -2
- package/dist/cli/ctl-command.js +3 -1
- package/dist/cli/daemon-commands.js +197 -27
- package/dist/cli/debug-mode.js +154 -0
- package/dist/cli/fs-command.js +2 -1
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +98 -67
- package/dist/cli/init.js +2 -2
- package/dist/cli/integrity-command.js +196 -0
- package/dist/cli/managed-command-guard.js +5 -0
- package/dist/cli/memory-command.js +166 -146
- package/dist/cli/queue-command.js +9 -4
- package/dist/cli/session-command.js +194 -0
- package/dist/cli/skill-command.js +78 -2
- package/dist/cli/task-context.js +1 -0
- package/dist/cli/thread-command.js +622 -0
- package/dist/cli/trigger-command.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +6 -3
- package/dist/config/config-manager.js +26 -2
- package/dist/config/contact-book-store.js +5 -0
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config/role-service.js +13 -0
- package/dist/config/runtime-env.js +16 -0
- package/dist/config-store.js +45 -0
- package/dist/core/audit/log-integrity.js +3 -1
- package/dist/core/auth/agent-delegation.js +133 -36
- package/dist/core/auth/debug-context.js +139 -0
- package/dist/core/auth/delegation-diagnostics.js +414 -0
- package/dist/core/auth/operation-authorizer.js +25 -2
- package/dist/core/auth/operation-catalog.js +49 -0
- 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/agent-control.js +18 -26
- package/dist/core/command/command-handler.js +197 -27
- package/dist/core/command/daemon-diagnostics.js +1 -0
- package/dist/core/command/memory-slash.js +6 -2
- package/dist/core/command/menu-catalog.js +51 -1
- package/dist/core/command/menu-handler.js +114 -23
- package/dist/core/command/menu-protocol.js +25 -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 +195 -13
- package/dist/core/command/topic-slash.js +528 -0
- package/dist/core/daemon-bootstrap.js +4 -7
- package/dist/core/evolagent-registry.js +20 -5
- package/dist/core/file-integrity/read.js +61 -0
- package/dist/core/file-integrity/store.js +262 -0
- package/dist/core/handoff/runtime.js +5 -11
- package/dist/core/handoff/store.js +5 -0
- package/dist/core/ipc-registration.js +175 -3
- package/dist/core/memory/batch-meta.js +132 -0
- package/dist/core/memory/consolidation-context.js +83 -29
- package/dist/core/memory/consolidation-ops-records.js +49 -16
- package/dist/core/memory/consolidation-queue-format.js +21 -0
- package/dist/core/memory/consolidation.js +264 -205
- package/dist/core/memory/index-manager.js +19 -28
- package/dist/core/memory/memory-admin.js +19 -14
- package/dist/core/memory/memory-document.js +53 -9
- package/dist/core/memory/memory-writer.js +30 -15
- package/dist/core/message/aun-ipc-sender.js +161 -6
- package/dist/core/message/im-renderer.js +169 -10
- package/dist/core/message/message-bridge.js +15 -4
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/peer-mode.js +6 -1
- package/dist/core/message/response-engine.js +929 -110
- 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 +217 -9
- package/dist/core/permission/protected-paths.js +5 -1
- package/dist/core/permission/readonly-shell-query.js +43 -5
- package/dist/core/permission/session-path-grants.js +334 -0
- package/dist/core/permission/tool-canonical.js +226 -0
- package/dist/core/permission/tool-policy.js +30 -62
- package/dist/core/role/runtime-policy.js +21 -10
- package/dist/core/session/session-fs-store.js +123 -8
- package/dist/core/session/session-idle-monitor.js +249 -22
- package/dist/core/session/session-manager.js +129 -14
- package/dist/core/skill/command-parser.js +13 -0
- package/dist/core/skill/index-generator.js +21 -4
- package/dist/core/skill/startup.js +41 -0
- package/dist/core/skill/switch-resolver.js +55 -29
- package/dist/core/system-session/grants.js +40 -0
- package/dist/core/system-session/memory.js +222 -0
- package/dist/core/system-session/registry.js +74 -0
- package/dist/core/system-session/types.js +14 -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 +770 -0
- package/dist/core/thread/types.js +1 -0
- package/dist/eck/kit-renderer.js +7 -2
- package/dist/eck/manifest-engine.js +24 -4
- package/dist/eck/message-renderer.js +35 -0
- package/dist/index.js +19 -7
- package/dist/ipc.js +140 -50
- package/dist/paths.js +93 -0
- package/dist/response-system/engines/v1/proactive-flow.js +169 -80
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/authorization.js +57 -28
- package/dist/trigger/feedback.js +13 -2
- package/dist/trigger/parser.js +70 -1
- package/dist/trigger/system-definitions.js +3 -3
- package/dist/trigger/validation.js +14 -0
- package/dist/utils/cross-platform.js +11 -2
- package/dist/utils/error-dict.json +18 -4
- package/dist/utils/error-utils.js +143 -1
- package/dist/utils/instance-registry.js +13 -8
- package/dist/utils/log-writer.js +12 -0
- package/dist/utils/process-introspect.js +11 -0
- package/kits/docs/context-assembly.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +40 -28
- package/kits/docs/evolcore/agent.md +21 -6
- package/kits/docs/evolcore/config.md +2 -2
- package/kits/docs/evolcore/ctl.md +2 -2
- package/kits/docs/evolcore/ec-capabilities.md +5 -1
- package/kits/docs/evolcore/fs-architecture.md +4 -5
- package/kits/docs/evolcore/fs.md +1 -1
- package/kits/docs/evolcore/group-collaboration.md +4 -6
- package/kits/docs/evolcore/group.md +11 -12
- package/kits/docs/evolcore/memory-architecture.md +4 -5
- package/kits/docs/evolcore/memory-consolidation.md +19 -15
- package/kits/docs/evolcore/memory-tags.md +3 -1
- package/kits/docs/evolcore/memory.md +24 -12
- package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
- package/kits/docs/evolcore/menu-protocol.md +7 -0
- package/kits/docs/evolcore/model.md +16 -0
- package/kits/docs/evolcore/msg.md +8 -11
- package/kits/docs/evolcore/thread-chain.md +267 -0
- package/kits/docs/prompt-loading-architecture.md +1 -1
- package/kits/eck_manifest.json +55 -40
- package/kits/eck_manifest.memory.json +0 -12
- package/kits/eck_message_manifest.json +169 -10
- package/kits/rules/01-overview.md +11 -0
- package/kits/rules/03-identity.md +6 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +1 -1
- package/kits/rules/07-memory-and-thread.md +199 -0
- package/kits/schemas/_meta.json +7 -1
- package/kits/schemas/agent-config.schema.13.json +1 -1
- package/kits/schemas/daemon.schema.7.json +140 -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 +16 -2
- package/kits/skills/ec-skill/scripts/skill.mjs +1 -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 +7 -2
- package/kits/templates/memory-consolidation/generalize.md +9 -4
- package/kits/templates/memory-consolidation/notes.md +11 -11
- package/kits/templates/memory-consolidation/profile.md +8 -3
- package/kits/templates/memory-consolidation/promote.md +8 -3
- package/kits/templates/memory-consolidation/workflow.md +6 -6
- package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
- 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 +67 -72
- package/kits/templates/system-fragments/channel.md +3 -6
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/managed-runtime.md +15 -0
- package/kits/templates/system-fragments/memory.md +16 -9
- package/kits/templates/system-fragments/reply.md +7 -6
- package/kits/templates/system-fragments/session.md +5 -14
- package/package.json +6 -3
- package/dist/core/memory/memory-session-policy.js +0 -41
- package/dist/core/memory/memory-signature.js +0 -257
- 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,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
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { normalizeThreadPeer } from './thread-service.js';
|
|
2
|
+
export function executeThreadOperation(request, threadService, sendSystemMsg) {
|
|
3
|
+
return _executeThreadOperation(request, threadService, sendSystemMsg);
|
|
4
|
+
}
|
|
5
|
+
async function _executeThreadOperation(request, threadService, sendSystemMsg) {
|
|
6
|
+
const op = String(request.op);
|
|
7
|
+
const selfAid = typeof request.selfAid === 'string' ? request.selfAid : '';
|
|
8
|
+
if (!selfAid)
|
|
9
|
+
return { ok: false, code: 'MISSING_SELF_AID', error: 'selfAid is required' };
|
|
10
|
+
// 入口归一:调用方可能传入裸 ID 或带渠道前缀的写法,统一成索引里存储的形态。
|
|
11
|
+
// 归一失败(解析不出)时保留原值,由下游按"无此对端"处理,不改变 arg 缺失的语义。
|
|
12
|
+
request = { ...request, peer: normalizeThreadPeer(request.peer) ?? request.peer };
|
|
13
|
+
try {
|
|
14
|
+
switch (op) {
|
|
15
|
+
case 'list': {
|
|
16
|
+
const result = threadService.listThreads({
|
|
17
|
+
selfAid,
|
|
18
|
+
peer: request.peer,
|
|
19
|
+
status: request.status,
|
|
20
|
+
label: request.label ?? undefined,
|
|
21
|
+
unlabeled: request.unlabeled,
|
|
22
|
+
sort: request.sort,
|
|
23
|
+
page: request.page,
|
|
24
|
+
limit: request.limit,
|
|
25
|
+
});
|
|
26
|
+
const threads = result.threads.map(t => ({
|
|
27
|
+
...t,
|
|
28
|
+
running: false,
|
|
29
|
+
peerName: t.peerName ?? t.peer,
|
|
30
|
+
}));
|
|
31
|
+
return { ok: true, result: { ...result, threads } };
|
|
32
|
+
}
|
|
33
|
+
case 'list-peers': {
|
|
34
|
+
const peers = threadService.listPeers(selfAid);
|
|
35
|
+
return { ok: true, result: { peers } };
|
|
36
|
+
}
|
|
37
|
+
case 'show': {
|
|
38
|
+
let peer = request.peer;
|
|
39
|
+
let threadId = request.threadId;
|
|
40
|
+
if ((!peer || !threadId) && request.sessionId) {
|
|
41
|
+
const resolved = threadService.resolveCurrentThread(selfAid, request.sessionId);
|
|
42
|
+
if (resolved) {
|
|
43
|
+
peer = resolved.peer;
|
|
44
|
+
threadId = resolved.threadId;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!peer)
|
|
48
|
+
return { ok: false, code: 'MISSING_ARGS', error: 'peer is required' };
|
|
49
|
+
// peer-only ref (no threadId) → show main session
|
|
50
|
+
if (!threadId) {
|
|
51
|
+
const result = await threadService.showMainSession(selfAid, peer);
|
|
52
|
+
if (!result)
|
|
53
|
+
return { ok: false, code: 'NOT_FOUND', error: `与 ${peer} 无活跃主会话` };
|
|
54
|
+
return { ok: true, result };
|
|
55
|
+
}
|
|
56
|
+
const result = await threadService.showThread({ selfAid, peer, threadId });
|
|
57
|
+
// 索引健康检查:检测 session 文件是否存在
|
|
58
|
+
const indexWarning = result._indexWarning;
|
|
59
|
+
const resp = { ok: true, result };
|
|
60
|
+
if (indexWarning)
|
|
61
|
+
resp.indexWarning = indexWarning;
|
|
62
|
+
return resp;
|
|
63
|
+
}
|
|
64
|
+
case 'check-index': {
|
|
65
|
+
const result = threadService.checkIndex(selfAid);
|
|
66
|
+
let warning;
|
|
67
|
+
if (result.missing > 0 && result.stale > 0) {
|
|
68
|
+
warning = `索引异常:${result.missing} 条 session 文件缺失(数据问题),${result.stale} 条属性与文件不一致(写盘时序)`;
|
|
69
|
+
}
|
|
70
|
+
else if (result.missing > 0) {
|
|
71
|
+
warning = `索引异常:${result.missing} 条 session 文件缺失,rebuild-index 后相关话题的 rename/label/status 改动将回退`;
|
|
72
|
+
}
|
|
73
|
+
else if (result.stale > 0) {
|
|
74
|
+
warning = `索引轻微不一致:${result.stale} 条话题属性(name/label/status)与 session 文件最后一行不同步,rebuild-index 可修复`;
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, result, indexWarning: warning };
|
|
77
|
+
}
|
|
78
|
+
case 'history': {
|
|
79
|
+
let peer = request.peer;
|
|
80
|
+
let threadId = request.threadId;
|
|
81
|
+
if (!peer && request.sessionId) {
|
|
82
|
+
const resolved = threadService.resolveCurrentThread(selfAid, request.sessionId);
|
|
83
|
+
if (resolved) {
|
|
84
|
+
peer = resolved.peer;
|
|
85
|
+
threadId = resolved.threadId;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// threadId 可选:不传时读主会话(threadId=null)消息
|
|
89
|
+
if (!peer)
|
|
90
|
+
return { ok: false, code: 'MISSING_ARGS', error: 'peer is required' };
|
|
91
|
+
const result = await threadService.historyThread({
|
|
92
|
+
selfAid,
|
|
93
|
+
peer,
|
|
94
|
+
threadId,
|
|
95
|
+
limit: request.limit,
|
|
96
|
+
before: request.before,
|
|
97
|
+
after: request.after,
|
|
98
|
+
direction: request.direction,
|
|
99
|
+
});
|
|
100
|
+
return { ok: true, result };
|
|
101
|
+
}
|
|
102
|
+
case 'create': {
|
|
103
|
+
if (!request.peer || !request.name)
|
|
104
|
+
return { ok: false, code: 'MISSING_ARGS', error: 'peer and name required' };
|
|
105
|
+
const result = await threadService.createThread({
|
|
106
|
+
selfAid,
|
|
107
|
+
peer: request.peer,
|
|
108
|
+
peerKind: request.peerKind ?? 'private',
|
|
109
|
+
name: request.name,
|
|
110
|
+
label: request.label ?? undefined,
|
|
111
|
+
topicStatus: request.topicStatus,
|
|
112
|
+
});
|
|
113
|
+
if (sendSystemMsg) {
|
|
114
|
+
try {
|
|
115
|
+
await sendSystemMsg({
|
|
116
|
+
from: selfAid,
|
|
117
|
+
to: request.peer,
|
|
118
|
+
threadId: result.threadId,
|
|
119
|
+
topicName: result.name,
|
|
120
|
+
peerKind: request.peerKind ?? 'private',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// 系统消息发送失败不影响本地创建
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { ok: true, result };
|
|
128
|
+
}
|
|
129
|
+
case 'rename': {
|
|
130
|
+
let peer = request.peer;
|
|
131
|
+
let threadId = request.threadId;
|
|
132
|
+
if ((!peer || !threadId) && request.sessionId) {
|
|
133
|
+
const resolved = threadService.resolveCurrentThread(selfAid, request.sessionId);
|
|
134
|
+
if (resolved) {
|
|
135
|
+
peer = resolved.peer;
|
|
136
|
+
threadId = resolved.threadId;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!peer || !threadId || !request.name)
|
|
140
|
+
return { ok: false, code: 'MISSING_ARGS', error: 'peer, threadId and name required' };
|
|
141
|
+
const result = await threadService.renameThread({ selfAid, peer, threadId, name: request.name });
|
|
142
|
+
return { ok: true, result };
|
|
143
|
+
}
|
|
144
|
+
case 'label': {
|
|
145
|
+
let peer = request.peer;
|
|
146
|
+
let threadId = request.threadId;
|
|
147
|
+
if ((!peer || !threadId) && request.sessionId) {
|
|
148
|
+
const resolved = threadService.resolveCurrentThread(selfAid, request.sessionId);
|
|
149
|
+
if (resolved) {
|
|
150
|
+
peer = resolved.peer;
|
|
151
|
+
threadId = resolved.threadId;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!peer || !threadId)
|
|
155
|
+
return { ok: false, code: 'MISSING_ARGS', error: 'peer and threadId required' };
|
|
156
|
+
await threadService.labelThread({ selfAid, peer, threadId, label: request.label ?? null });
|
|
157
|
+
return { ok: true };
|
|
158
|
+
}
|
|
159
|
+
case 'status': {
|
|
160
|
+
let peer = request.peer;
|
|
161
|
+
let threadId = request.threadId;
|
|
162
|
+
if ((!peer || !threadId) && request.sessionId) {
|
|
163
|
+
const resolved = threadService.resolveCurrentThread(selfAid, request.sessionId);
|
|
164
|
+
if (resolved) {
|
|
165
|
+
peer = resolved.peer;
|
|
166
|
+
threadId = resolved.threadId;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!peer || !threadId || !request.topicStatus)
|
|
170
|
+
return { ok: false, code: 'MISSING_ARGS', error: 'peer, threadId and topicStatus required' };
|
|
171
|
+
await threadService.statusThread({ selfAid, peer, threadId, topicStatus: request.topicStatus });
|
|
172
|
+
return { ok: true };
|
|
173
|
+
}
|
|
174
|
+
case 'rebuild-index': {
|
|
175
|
+
const dryRun = request.dryRun === true;
|
|
176
|
+
const result = await threadService.rebuildIndex(selfAid, dryRun);
|
|
177
|
+
if (dryRun) {
|
|
178
|
+
const { rebuilt } = result;
|
|
179
|
+
const check = threadService.checkIndex(selfAid);
|
|
180
|
+
return { ok: true, result: { rebuilt, missing: check.missing, missingIds: check.missingIds, dryRun: true } };
|
|
181
|
+
}
|
|
182
|
+
return { ok: true, result };
|
|
183
|
+
}
|
|
184
|
+
case 'delete': {
|
|
185
|
+
let peer = request.peer;
|
|
186
|
+
let threadId = request.threadId;
|
|
187
|
+
if ((!peer || !threadId) && request.sessionId) {
|
|
188
|
+
const resolved = threadService.resolveCurrentThread(selfAid, request.sessionId);
|
|
189
|
+
if (resolved) {
|
|
190
|
+
peer = resolved.peer;
|
|
191
|
+
threadId = resolved.threadId;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!peer || !threadId)
|
|
195
|
+
return { ok: false, code: 'MISSING_ARGS', error: 'peer and threadId required' };
|
|
196
|
+
await threadService.deleteThread(selfAid, peer, threadId);
|
|
197
|
+
return { ok: true };
|
|
198
|
+
}
|
|
199
|
+
case 'summary-show': {
|
|
200
|
+
if (!request.sessionId)
|
|
201
|
+
return { ok: false, code: 'MISSING_ARGS', error: 'sessionId required' };
|
|
202
|
+
const result = threadService.showSummary(selfAid, request.sessionId);
|
|
203
|
+
return { ok: true, result: result ?? null };
|
|
204
|
+
}
|
|
205
|
+
case 'summary-set': {
|
|
206
|
+
if (!request.sessionId || !request.content)
|
|
207
|
+
return { ok: false, code: 'MISSING_ARGS', error: 'sessionId and content required' };
|
|
208
|
+
// 覆盖游标由 service 自动记录(写入时刻话题最后一条消息的 ts),调用方不用传。
|
|
209
|
+
const result = threadService.setSummary(selfAid, request.sessionId, request.content, request.ifRev);
|
|
210
|
+
// 顺带改名:总结时往往正好把话题主题想清楚了,一次命令写完更省事。
|
|
211
|
+
// 调用方(CLI / slash)保证 name 一定存在;`-` 表示"保持原标题不改"。
|
|
212
|
+
// 主会话没有话题名可改(threadId 为空),忽略该参数而不是报错。
|
|
213
|
+
const rawName = typeof request.name === 'string' ? request.name.trim() : '';
|
|
214
|
+
let name = rawName && rawName !== '-' ? rawName : undefined;
|
|
215
|
+
let threadId = request.threadId;
|
|
216
|
+
if (name && !threadId) {
|
|
217
|
+
const resolved = threadService.resolveCurrentThread(selfAid, request.sessionId);
|
|
218
|
+
threadId = resolved?.threadId;
|
|
219
|
+
}
|
|
220
|
+
let peer = request.peer;
|
|
221
|
+
if (name && threadId && !peer) {
|
|
222
|
+
peer = threadService.readThreadIndex(selfAid).threads.find(t => t.threadId === threadId)?.peer;
|
|
223
|
+
}
|
|
224
|
+
if (name && threadId && peer) {
|
|
225
|
+
try {
|
|
226
|
+
const renamed = await threadService.renameThread({ selfAid, peer, threadId, name });
|
|
227
|
+
return { ok: true, result: { ...result, renamedTo: renamed.name, renamedChanged: renamed.changed } };
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
// 摘要已写入,改名失败不回滚——如实返回,让调用方知道只有改名这步没成
|
|
231
|
+
return {
|
|
232
|
+
ok: true,
|
|
233
|
+
result: { ...result, renameError: e?.message ?? String(e) },
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { ok: true, result };
|
|
238
|
+
}
|
|
239
|
+
case 'summary-log': {
|
|
240
|
+
if (!request.sessionId)
|
|
241
|
+
return { ok: false, code: 'MISSING_ARGS', error: 'sessionId required' };
|
|
242
|
+
const result = threadService.logSummary(selfAid, request.sessionId);
|
|
243
|
+
return { ok: true, result };
|
|
244
|
+
}
|
|
245
|
+
case 'summary-clear': {
|
|
246
|
+
if (!request.sessionId)
|
|
247
|
+
return { ok: false, code: 'MISSING_ARGS', error: 'sessionId required' };
|
|
248
|
+
threadService.clearSummary(selfAid, request.sessionId);
|
|
249
|
+
return { ok: true };
|
|
250
|
+
}
|
|
251
|
+
default:
|
|
252
|
+
return { ok: false, code: 'UNKNOWN_OP', error: `Unknown op: ${op}` };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
return { ok: false, code: error.code ?? 'ERROR', error: error.message ?? String(error) };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { sameGroupAid } from '../../aun/group/identity.js';
|
|
2
|
+
import { ThreadService } from './thread-service.js';
|
|
3
|
+
/**
|
|
4
|
+
* 解析 `--thread` 跨话题发送的目标话题。
|
|
5
|
+
*
|
|
6
|
+
* 话题必须存在于发送方本端索引、属于 AUN 渠道、peerKind 与发送 scope 一致;
|
|
7
|
+
* 给出 target 时还必须与话题 peer 相同(群按群 AID 规范化比较)。
|
|
8
|
+
* 返回 undefined 表示该 thread 不能作为跨话题路由依据。
|
|
9
|
+
*/
|
|
10
|
+
export function resolveThreadRouteTarget(selfAid, threadId, scope, target) {
|
|
11
|
+
if (!selfAid || !threadId)
|
|
12
|
+
return undefined;
|
|
13
|
+
let entry;
|
|
14
|
+
try {
|
|
15
|
+
entry = ThreadService.findThreadById(selfAid.replace(/^@/, ''), threadId);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
if (!entry)
|
|
21
|
+
return undefined;
|
|
22
|
+
if ((entry.channelType ?? 'aun') !== 'aun')
|
|
23
|
+
return undefined;
|
|
24
|
+
if (entry.peerKind !== (scope === 'group' ? 'group' : 'private'))
|
|
25
|
+
return undefined;
|
|
26
|
+
if (target === undefined)
|
|
27
|
+
return entry;
|
|
28
|
+
const normalizedTarget = target.replace(/^@/, '');
|
|
29
|
+
const matches = scope === 'group'
|
|
30
|
+
? sameGroupAid(entry.peer, normalizedTarget)
|
|
31
|
+
: entry.peer === normalizedTarget;
|
|
32
|
+
return matches ? entry : undefined;
|
|
33
|
+
}
|