evolcore 0.0.17 → 0.0.19
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 +51 -0
- package/bin/codex-managed-hook.mjs +16 -7
- package/bin/install-codex-managed-hooks.mjs +4 -2
- package/dist/agents/claude-runner.js +113 -24
- package/dist/agents/codex-app-server-client.js +6 -1
- package/dist/agents/codex-runner.js +43 -24
- package/dist/agents/ecagent-runner.js +39 -10
- package/dist/agents/gemini-runner.js +90 -19
- package/dist/aun/aid/store.js +36 -0
- package/dist/aun/msg/p2p.js +20 -8
- package/dist/channels/aun.js +159 -21
- package/dist/cli/agent-command.js +67 -6
- package/dist/cli/agent.js +26 -0
- package/dist/cli/command-log.js +23 -4
- package/dist/cli/daemon-commands.js +82 -14
- package/dist/cli/init.js +21 -5
- package/dist/cli/restart-monitor.js +13 -6
- package/dist/cli/watch-logs.js +2 -2
- package/dist/config/builtin-roles.js +5 -1
- package/dist/config/role-ranks.js +4 -0
- package/dist/core/audit/event-key.js +29 -0
- package/dist/core/audit/log-integrity.js +13 -3
- package/dist/core/auth/auth-gateway.js +14 -18
- package/dist/core/auth/authorization-audit.js +110 -3
- package/dist/core/auth/authorization-denial.js +17 -0
- package/dist/core/auth/operation-authorizer.js +143 -18
- package/dist/core/auth/operation-catalog.js +21 -5
- package/dist/core/bootstrap-messages.js +11 -6
- package/dist/core/bootstrap-service.js +26 -4
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +25 -16
- package/dist/core/command/command-handler.js +50 -4
- package/dist/core/command/group-menu.js +1 -1
- package/dist/core/command/menu-catalog.js +32 -7
- package/dist/core/command/menu-handler.js +59 -23
- package/dist/core/command/menu-protocol.js +196 -0
- package/dist/core/command/slash-gate.js +14 -5
- package/dist/core/command/slash-handler.js +81 -99
- package/dist/core/data-migration.js +10 -4
- package/dist/core/event-catalog.js +18 -0
- package/dist/core/message/message-bridge.js +66 -8
- package/dist/core/message/response-engine.js +147 -10
- package/dist/core/permission/ec-command-parser.js +148 -22
- package/dist/core/permission/sandbox-runtime.js +79 -13
- package/dist/core/permission/tool-policy.js +19 -7
- package/dist/index.js +357 -48
- package/dist/ipc.js +81 -5
- package/dist/paths.js +0 -3
- package/dist/utils/atomic-write.js +45 -11
- package/dist/utils/logger.js +27 -0
- package/dist/utils/windows-autostart.js +740 -83
- package/ecagent/dist/harness/agent-harness.d.ts +1 -1
- package/ecagent/dist/harness/agent-harness.js +6 -4
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/group-rules.md +2 -1
- package/kits/docs/identity/ROLE_DETAIL.md +3 -1
- package/kits/docs/path-registry.md +1 -1
- package/kits/eck_manifest.json +25 -16
- package/kits/rules/01-overview.md +5 -5
- package/kits/rules/02-navigation.md +2 -2
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/04-relation.md +4 -4
- package/kits/rules/05-venue.md +5 -5
- package/kits/templates/bootstrap-welcome.md +3 -1
- package/kits/templates/system-fragments/bootstrap.md +17 -9
- package/package.json +1 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
2
3
|
import { loadDaemonConfig } from '../../config-store.js';
|
|
3
4
|
import { readInstalledEvolcoreVersion } from '../../utils/evolcore-version.js';
|
|
4
5
|
import { compareStableSemver, parseStableSemver } from '../../utils/stable-semver.js';
|
|
6
|
+
import { logger } from '../../utils/logger.js';
|
|
5
7
|
export const MENU_REQUEST_TYPES = new Set([
|
|
6
8
|
'menu.token.request',
|
|
7
9
|
'menu.list',
|
|
@@ -40,6 +42,51 @@ export function menuCommandForName(name) {
|
|
|
40
42
|
return undefined;
|
|
41
43
|
return MENU_NAME_COMMANDS[name];
|
|
42
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Preserve a valid adapter receive mark, or capture a local monotonic fallback
|
|
47
|
+
* at the earliest boundary available to the caller.
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeMenuResponseTiming(timing, clock = {}) {
|
|
50
|
+
const receivedAtMono = typeof timing.receivedAtMono === 'number' && Number.isFinite(timing.receivedAtMono)
|
|
51
|
+
? timing.receivedAtMono
|
|
52
|
+
: undefined;
|
|
53
|
+
const receivedAt = typeof timing.receivedAt === 'number' && Number.isFinite(timing.receivedAt)
|
|
54
|
+
? timing.receivedAt
|
|
55
|
+
: undefined;
|
|
56
|
+
if (receivedAtMono !== undefined) {
|
|
57
|
+
return { ...(receivedAt !== undefined ? { receivedAt } : {}), receivedAtMono };
|
|
58
|
+
}
|
|
59
|
+
if (receivedAt !== undefined)
|
|
60
|
+
return { receivedAt };
|
|
61
|
+
return {
|
|
62
|
+
receivedAt: clock.wallNow ?? Date.now(),
|
|
63
|
+
receivedAtMono: clock.monotonicNow ?? performance.now(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Finalize a Menu response at an external transport boundary.
|
|
68
|
+
*
|
|
69
|
+
* Keeping this outside menuSuccess/menuFailure is intentional: dedupe caches
|
|
70
|
+
* response drafts, while every inbound request (including a replay) must get
|
|
71
|
+
* its own inbound-to-outbound processing time.
|
|
72
|
+
*/
|
|
73
|
+
export function withMenuProcessingTime(response, timing, clock = {}) {
|
|
74
|
+
const receivedAtMono = typeof timing.receivedAtMono === 'number' && Number.isFinite(timing.receivedAtMono)
|
|
75
|
+
? timing.receivedAtMono
|
|
76
|
+
: undefined;
|
|
77
|
+
const receivedAt = typeof timing.receivedAt === 'number' && Number.isFinite(timing.receivedAt)
|
|
78
|
+
? timing.receivedAt
|
|
79
|
+
: undefined;
|
|
80
|
+
const elapsed = receivedAtMono !== undefined
|
|
81
|
+
? (clock.monotonicNow ?? performance.now()) - receivedAtMono
|
|
82
|
+
: receivedAt !== undefined
|
|
83
|
+
? (clock.wallNow ?? Date.now()) - receivedAt
|
|
84
|
+
: 0;
|
|
85
|
+
return {
|
|
86
|
+
...response,
|
|
87
|
+
processing_ms: Math.max(0, Math.round(Number.isFinite(elapsed) ? elapsed : 0)),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
43
90
|
export function evaluateEvolMenuVersionGate(input) {
|
|
44
91
|
const minimum = loadDaemonConfig().aun?.minEvolVersion;
|
|
45
92
|
if (!minimum)
|
|
@@ -346,3 +393,152 @@ export class MenuDiagnosticLimiter {
|
|
|
346
393
|
return { log: false, suppressed: previous.suppressed };
|
|
347
394
|
}
|
|
348
395
|
}
|
|
396
|
+
const MAX_LOG_DEPTH = 8;
|
|
397
|
+
const MAX_LOG_KEYS = 100;
|
|
398
|
+
const MAX_LOG_ARRAY_ITEMS = 100;
|
|
399
|
+
const MAX_LOG_STRING_LENGTH = 4096;
|
|
400
|
+
const SENSITIVE_LOG_KEY = /(?:^|[_-])(?:api[_-]?key|access[_-]?token|refresh[_-]?token|menu[_-]?token|token|secret(?:[_-]?key)?|password|passwd|credentials?|authorization|cookie|private[_-]?key|seed)(?:$|[_-])/i;
|
|
401
|
+
const SENSITIVE_LOG_ARG = /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|menu[_-]?token|token|secret|password|passwd|credential|authorization|cookie|private[_-]?key|seed)/i;
|
|
402
|
+
function isSensitiveLogKey(key) {
|
|
403
|
+
const normalized = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2');
|
|
404
|
+
return SENSITIVE_LOG_KEY.test(normalized);
|
|
405
|
+
}
|
|
406
|
+
function sanitizeInlineLogSecrets(value) {
|
|
407
|
+
return value
|
|
408
|
+
.replace(/\b(authorization\s*:\s*Bearer\s+)[^\s"',;]+/gi, '$1[REDACTED]')
|
|
409
|
+
.replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]')
|
|
410
|
+
.replace(/\b(api[_-]?key|access[_-]?token|refresh[_-]?token|menu[_-]?token|token|password|passwd|secret|credential|authorization|cookie)\s*([=:])\s*(?:"[^"]*"|'[^']*'|\S+)/gi, '$1$2[REDACTED]')
|
|
411
|
+
.replace(/(--(?:api-key|access-token|refresh-token|menu-token|token|password|secret|credential))(?:=|\s+)(?:"[^"]*"|'[^']*'|\S+)/gi, '$1 [REDACTED]');
|
|
412
|
+
}
|
|
413
|
+
function sanitizeLogString(value) {
|
|
414
|
+
let sanitized = value;
|
|
415
|
+
const trimmed = value.trim();
|
|
416
|
+
if ((trimmed.startsWith('{') || trimmed.startsWith('[')) && trimmed.length <= 64 * 1024) {
|
|
417
|
+
try {
|
|
418
|
+
const parsed = JSON.parse(trimmed);
|
|
419
|
+
if (parsed && typeof parsed === 'object')
|
|
420
|
+
sanitized = JSON.stringify(sanitizeMenuLogValue(parsed));
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// Ordinary strings may start with JSON punctuation; keep their original shape.
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
sanitized = sanitizeInlineLogSecrets(sanitized);
|
|
427
|
+
if (sanitized.length <= MAX_LOG_STRING_LENGTH)
|
|
428
|
+
return sanitized;
|
|
429
|
+
return `${sanitized.slice(0, MAX_LOG_STRING_LENGTH)}...[TRUNCATED ${sanitized.length - MAX_LOG_STRING_LENGTH} chars]`;
|
|
430
|
+
}
|
|
431
|
+
function sanitizeLogArgv(value, depth, seen) {
|
|
432
|
+
const output = [];
|
|
433
|
+
let redactNext = false;
|
|
434
|
+
for (const item of value.slice(0, MAX_LOG_ARRAY_ITEMS)) {
|
|
435
|
+
if (typeof item !== 'string') {
|
|
436
|
+
output.push(sanitizeMenuLogValue(item, '', depth + 1, seen));
|
|
437
|
+
redactNext = false;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (redactNext) {
|
|
441
|
+
output.push('[REDACTED]');
|
|
442
|
+
redactNext = false;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const sanitized = sanitizeLogString(item);
|
|
446
|
+
output.push(sanitized);
|
|
447
|
+
const token = item.replace(/^--?/, '').replace(/=.*$/, '');
|
|
448
|
+
redactNext = SENSITIVE_LOG_ARG.test(token) && !item.includes('=');
|
|
449
|
+
}
|
|
450
|
+
if (value.length > MAX_LOG_ARRAY_ITEMS)
|
|
451
|
+
output.push(`[TRUNCATED ${value.length - MAX_LOG_ARRAY_ITEMS} items]`);
|
|
452
|
+
return output;
|
|
453
|
+
}
|
|
454
|
+
export function sanitizeMenuLogValue(value, key = '', depth = 0, seen = new WeakSet()) {
|
|
455
|
+
if (isSensitiveLogKey(key))
|
|
456
|
+
return '[REDACTED]';
|
|
457
|
+
if (value === null || value === undefined || typeof value === 'number' || typeof value === 'boolean')
|
|
458
|
+
return value;
|
|
459
|
+
if (typeof value === 'bigint')
|
|
460
|
+
return value.toString();
|
|
461
|
+
if (typeof value === 'string')
|
|
462
|
+
return sanitizeLogString(value);
|
|
463
|
+
if (typeof value !== 'object')
|
|
464
|
+
return String(value);
|
|
465
|
+
if (depth >= MAX_LOG_DEPTH)
|
|
466
|
+
return '[MAX_DEPTH]';
|
|
467
|
+
if (seen.has(value))
|
|
468
|
+
return '[CIRCULAR]';
|
|
469
|
+
seen.add(value);
|
|
470
|
+
if (Array.isArray(value)) {
|
|
471
|
+
const items = value.slice(0, MAX_LOG_ARRAY_ITEMS).map(item => sanitizeMenuLogValue(item, '', depth + 1, seen));
|
|
472
|
+
if (value.length > MAX_LOG_ARRAY_ITEMS)
|
|
473
|
+
items.push(`[TRUNCATED ${value.length - MAX_LOG_ARRAY_ITEMS} items]`);
|
|
474
|
+
return items;
|
|
475
|
+
}
|
|
476
|
+
const output = {};
|
|
477
|
+
const entries = Object.entries(value);
|
|
478
|
+
for (const [entryKey, entryValue] of entries.slice(0, MAX_LOG_KEYS)) {
|
|
479
|
+
output[entryKey] = entryKey === 'argv' && Array.isArray(entryValue)
|
|
480
|
+
? sanitizeLogArgv(entryValue, depth + 1, seen)
|
|
481
|
+
: sanitizeMenuLogValue(entryValue, entryKey, depth + 1, seen);
|
|
482
|
+
}
|
|
483
|
+
if (entries.length > MAX_LOG_KEYS)
|
|
484
|
+
output.$truncated_keys = entries.length - MAX_LOG_KEYS;
|
|
485
|
+
return output;
|
|
486
|
+
}
|
|
487
|
+
function menuRequestId(request) {
|
|
488
|
+
return typeof request.id === 'string' && request.id.trim() ? request.id : undefined;
|
|
489
|
+
}
|
|
490
|
+
function menuFlowBaseRecord(request, context) {
|
|
491
|
+
return {
|
|
492
|
+
source: context.source,
|
|
493
|
+
requestId: menuRequestId(request),
|
|
494
|
+
messageId: context.messageId,
|
|
495
|
+
selfAid: context.selfAid,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function menuRequestBusinessFields(request) {
|
|
499
|
+
return sanitizeMenuLogValue({
|
|
500
|
+
type: request.type,
|
|
501
|
+
name: request.name,
|
|
502
|
+
agent: request.agent,
|
|
503
|
+
action: request.action,
|
|
504
|
+
cmd: request.cmd,
|
|
505
|
+
args: request.args,
|
|
506
|
+
value: request.value,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
function writeMenuFlowRecord(record) {
|
|
510
|
+
try {
|
|
511
|
+
logger.menu(record);
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
// Observability must never affect Menu execution.
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
export function logMenuRequestReceived(request, context) {
|
|
518
|
+
writeMenuFlowRecord({
|
|
519
|
+
...menuFlowBaseRecord(request, context),
|
|
520
|
+
event: 'menu.request.received',
|
|
521
|
+
request: menuRequestBusinessFields(request),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
export function logMenuRequestCompleted(request, response, context, options) {
|
|
525
|
+
const error = response && 'error' in response ? response.error : undefined;
|
|
526
|
+
const transportError = options.transportError instanceof Error
|
|
527
|
+
? { message: sanitizeLogString(options.transportError.message) }
|
|
528
|
+
: options.transportError === undefined
|
|
529
|
+
? undefined
|
|
530
|
+
: { message: sanitizeLogString(String(options.transportError)) };
|
|
531
|
+
const processingMs = response && typeof response.processing_ms === 'number'
|
|
532
|
+
? response.processing_ms
|
|
533
|
+
: undefined;
|
|
534
|
+
writeMenuFlowRecord({
|
|
535
|
+
...menuFlowBaseRecord(request, context),
|
|
536
|
+
event: options.delivery === 'failed' ? 'menu.request.failed' : 'menu.request.completed',
|
|
537
|
+
delivery: options.delivery,
|
|
538
|
+
processingMs,
|
|
539
|
+
reason: options.reason,
|
|
540
|
+
result: response && 'data' in response ? sanitizeMenuLogValue(response.data) : undefined,
|
|
541
|
+
warning: response && 'warning' in response ? sanitizeMenuLogValue(response.warning) : undefined,
|
|
542
|
+
error: error ? sanitizeMenuLogValue(error) : transportError,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// 支持的命令列表
|
|
2
|
-
const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/compact', '/repair', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
|
|
2
|
+
const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/pause', '/compact', '/repair', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
|
|
3
3
|
const deprecatedCommands = ['/clear'];
|
|
4
|
+
const exactBoundaryCommands = new Set(['/pause', '/resume', '/stop']);
|
|
4
5
|
// 命令别名映射
|
|
5
6
|
const aliases = {
|
|
6
7
|
'/s': '/session',
|
|
@@ -9,7 +10,7 @@ const aliases = {
|
|
|
9
10
|
'/base': '/baseagent',
|
|
10
11
|
};
|
|
11
12
|
// 命令快速路径前缀(所有命令都不进入消息队列)
|
|
12
|
-
const quickCommandPrefixes = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/del', '/perm', '/file', '/check', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/base ', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
|
|
13
|
+
const quickCommandPrefixes = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/pause', '/clear', '/compact', '/del', '/perm', '/file', '/check', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/base ', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
|
|
13
14
|
/**
|
|
14
15
|
* 计算两个字符串的 Levenshtein 距离(编辑距离)
|
|
15
16
|
*/
|
|
@@ -39,7 +40,9 @@ function levenshteinDistance(str1, str2) {
|
|
|
39
40
|
return matrix[len1][len2];
|
|
40
41
|
}
|
|
41
42
|
export function isQuickCommand(content) {
|
|
42
|
-
return content === '/s' || quickCommandPrefixes.some(cmd =>
|
|
43
|
+
return content === '/s' || quickCommandPrefixes.some(cmd => exactBoundaryCommands.has(cmd)
|
|
44
|
+
? content === cmd || content.startsWith(cmd + ' ')
|
|
45
|
+
: content.startsWith(cmd));
|
|
43
46
|
}
|
|
44
47
|
export function normalizeSlashContent(content) {
|
|
45
48
|
for (const [alias, full] of Object.entries(aliases)) {
|
|
@@ -50,7 +53,9 @@ export function normalizeSlashContent(content) {
|
|
|
50
53
|
return content;
|
|
51
54
|
}
|
|
52
55
|
export function isRecognizedSlashCommand(content) {
|
|
53
|
-
return commands.some(cmd =>
|
|
56
|
+
return commands.some(cmd => exactBoundaryCommands.has(cmd)
|
|
57
|
+
? content === cmd || content.startsWith(cmd + ' ')
|
|
58
|
+
: content.startsWith(cmd)) ||
|
|
54
59
|
deprecatedCommands.some(cmd => content === cmd || content.startsWith(cmd + ' '));
|
|
55
60
|
}
|
|
56
61
|
export function guardThreadCommand(content, threadId) {
|
|
@@ -70,7 +75,7 @@ export function guardRoleCommand(content, activeChatType, isAdmin) {
|
|
|
70
75
|
// visitor/member 在群聊和私聊中均可访问的只读命令:纯查询形态(带参写操作由各 handler 内部守卫拦截)
|
|
71
76
|
const userGroupCommands = [
|
|
72
77
|
'/status', '/help', '/evolhelp', '/check', '/chatmode', '/mentionmode',
|
|
73
|
-
'/model', '/effort', '/baseagent', '/perm', '/activity', '/stop',
|
|
78
|
+
'/model', '/effort', '/baseagent', '/perm', '/activity', '/stop', '/pause',
|
|
74
79
|
'/resume', '/trigger', '/file',
|
|
75
80
|
];
|
|
76
81
|
const userCommands = activeChatType === 'group' && !isAdmin
|
|
@@ -107,6 +112,8 @@ export async function guardIdleCommand(opts) {
|
|
|
107
112
|
// 话题中:检查话题 session 是否在处理(不创建)
|
|
108
113
|
const threadSession = await opts.sessionManager.getThreadSession(opts.channel, opts.channelId, opts.threadId);
|
|
109
114
|
if (threadSession) {
|
|
115
|
+
if (opts.isSessionPaused?.(threadSession.id))
|
|
116
|
+
return undefined;
|
|
110
117
|
let hasActiveStream = false;
|
|
111
118
|
try {
|
|
112
119
|
hasActiveStream = opts.getAgentForSession(threadSession).hasActiveStream(threadSession.id);
|
|
@@ -123,6 +130,8 @@ export async function guardIdleCommand(opts) {
|
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
132
|
else if (opts.activeSession) {
|
|
133
|
+
if (opts.isSessionPaused?.(opts.activeSession.id))
|
|
134
|
+
return undefined;
|
|
126
135
|
const isBusy = (opts.activeAgent?.hasActiveStream(opts.activeSession.id) ?? false) ||
|
|
127
136
|
opts.messageQueue?.isProcessing(opts.activeSession.id) ||
|
|
128
137
|
(opts.messageQueue?.getQueueLength(opts.activeSession.id) ?? 0) > 0;
|
|
@@ -7,18 +7,18 @@ import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
|
|
|
7
7
|
import crypto from 'crypto';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import fs from 'fs';
|
|
10
|
-
import os from 'os';
|
|
11
10
|
import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../../utils/npm-ops.js';
|
|
12
11
|
import { loadDaemonConfig } from '../../config-store.js';
|
|
13
12
|
import { read as cfgRead, resolveEffective, routeFieldPath, write as cfgWrite, } from '../../config/config-manager.js';
|
|
14
13
|
import { execAgentAction } from './agent-control.js';
|
|
15
14
|
import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
15
|
+
import { authorizationNextStep, formatAuthorizationDenial } from '../auth/authorization-denial.js';
|
|
16
16
|
import { resolvePermissionMode, writeScope } from '../model/config-scope.js';
|
|
17
17
|
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
18
18
|
import { modelMatches } from '../model/model-catalog.js';
|
|
19
19
|
import { formatModelCheck, runModelCheck } from '../model/model-diagnostics.js';
|
|
20
20
|
import { filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
|
|
21
|
-
import { displaySessionTitle
|
|
21
|
+
import { displaySessionTitle } from '../session/session-title.js';
|
|
22
22
|
import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
|
|
23
23
|
import { normalizePermissionMode as normalizePermissionModeContract, PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
|
|
24
24
|
import { isManagementRole } from '../../config/builtin-roles.js';
|
|
@@ -157,7 +157,7 @@ async function authorizeSlashIntent(params) {
|
|
|
157
157
|
const channelType = channel.split('#')[0];
|
|
158
158
|
const peerKey = channelType && peerKeyId ? formatPeerKey(channelType, peerKeyId) : undefined;
|
|
159
159
|
const subject = params.subject
|
|
160
|
-
? { ...params.subject,
|
|
160
|
+
? { ...params.subject, identity }
|
|
161
161
|
: buildAuthSubject({
|
|
162
162
|
selfAid,
|
|
163
163
|
actorId: userId,
|
|
@@ -175,7 +175,13 @@ async function authorizeSlashIntent(params) {
|
|
|
175
175
|
}
|
|
176
176
|
const decision = await authorizeOperation({ source: 'slash', intent, subject });
|
|
177
177
|
if (!decision.allow) {
|
|
178
|
-
|
|
178
|
+
const reasonCode = decision.command?.reasonCode;
|
|
179
|
+
return {
|
|
180
|
+
kind: 'command.error',
|
|
181
|
+
text: formatAuthorizationDenial(decision.reason, reasonCode),
|
|
182
|
+
reason: decision.code,
|
|
183
|
+
...(reasonCode ? { reasonCode, nextStep: authorizationNextStep(reasonCode) } : {}),
|
|
184
|
+
};
|
|
179
185
|
}
|
|
180
186
|
return null;
|
|
181
187
|
}
|
|
@@ -471,6 +477,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
471
477
|
activeAgent: getActiveAgentIfAvailable(),
|
|
472
478
|
sessionManager: this.sessionManager,
|
|
473
479
|
messageQueue: this.messageQueue,
|
|
480
|
+
isSessionPaused: sessionId => this.processor?.isPauseRequested?.(sessionId) ?? false,
|
|
474
481
|
getAgentForSession: session => this.getAgent(channel, session.baseagent),
|
|
475
482
|
});
|
|
476
483
|
if (idleGuard)
|
|
@@ -481,6 +488,17 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
481
488
|
const isCmd = isRecognizedSlashCommand(normalizedContent);
|
|
482
489
|
if (!isCmd)
|
|
483
490
|
return undefined;
|
|
491
|
+
const interruptPausedSessionBeforeReplacement = async (commandSession) => {
|
|
492
|
+
if (commandSession && (this.processor?.isPauseRequested?.(commandSession.id) ?? false)) {
|
|
493
|
+
this.eventBus.publish({
|
|
494
|
+
type: 'task:interrupted',
|
|
495
|
+
sessionId: commandSession.id,
|
|
496
|
+
reason: 'stop',
|
|
497
|
+
agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
|
|
498
|
+
});
|
|
499
|
+
await this.processor.interruptSession(commandSession.id, 'stop');
|
|
500
|
+
}
|
|
501
|
+
};
|
|
484
502
|
// /help 命令不需要会话
|
|
485
503
|
if (normalizedContent === '/help') {
|
|
486
504
|
const canReadModel = canAccessSlashOperation('model.current', 'relation')
|
|
@@ -541,6 +559,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
541
559
|
' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
|
|
542
560
|
' /name <新名称> - 重命名当前会话',
|
|
543
561
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
562
|
+
' /pause - 在下一次工具调用前暂停当前任务',
|
|
563
|
+
' /resume - 继续已暂停的当前任务',
|
|
564
|
+
' /stop - 中断当前任务',
|
|
544
565
|
' /status - 显示会话状态',
|
|
545
566
|
' /check - 检查 EvolAgent 实例健康',
|
|
546
567
|
...((canReadModel || canUseModel || canSetEffort) ? [
|
|
@@ -587,6 +608,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
587
608
|
' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
|
|
588
609
|
' /rewind [N] [chat|file|all] - 查看历史/撤销指定轮次(别名: /rw)',
|
|
589
610
|
' /compact - 压缩会话上下文(减少 token 用量)',
|
|
611
|
+
' /pause - 在下一次工具调用前暂停当前任务',
|
|
612
|
+
' /resume - 继续已暂停的当前任务',
|
|
613
|
+
' /stop - 中断当前任务',
|
|
590
614
|
'',
|
|
591
615
|
'🤖 Agent 与模型:',
|
|
592
616
|
' /baseagent [name] - 查看或切换 Agent 后端(别名: /base)',
|
|
@@ -605,6 +629,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
605
629
|
'',
|
|
606
630
|
'🛠️ 运维:',
|
|
607
631
|
' /status - 显示会话状态',
|
|
632
|
+
' /pause - 在下一次工具调用前暂停当前任务',
|
|
633
|
+
' /resume - 继续已暂停的当前任务',
|
|
608
634
|
' /stop - 中断当前任务',
|
|
609
635
|
' /check - 检查 EvolAgent 实例健康',
|
|
610
636
|
...(isOwner ? [
|
|
@@ -670,6 +696,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
670
696
|
cmds.push({ command: '/perm', args: 'allow|always|deny', description: '审批权限请求', category: '权限管理', roles: [identity.role] });
|
|
671
697
|
// 运维
|
|
672
698
|
cmds.push({ command: '/status', description: '显示会话状态', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] });
|
|
699
|
+
cmds.push({ command: '/pause', description: '在下一次工具调用前暂停当前任务', category: '运维', roles: ['admin', 'owner'] });
|
|
700
|
+
cmds.push({ command: '/resume', description: '继续已暂停的当前任务', category: '运维', roles: ['admin', 'owner'] });
|
|
673
701
|
cmds.push({ command: '/stop', description: '中断当前任务', category: '运维', roles: ['admin', 'owner'] });
|
|
674
702
|
cmds.push({ command: '/check', description: '检查 EvolAgent 实例健康', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] });
|
|
675
703
|
if (isAdmin) {
|
|
@@ -851,92 +879,36 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
851
879
|
return { kind: 'command.result', text: fb.result ?? '✓ 已回答' };
|
|
852
880
|
return { kind: 'command.error', text: '❌ 当前没有待回答的问题' };
|
|
853
881
|
}
|
|
854
|
-
// /resume
|
|
855
|
-
if (normalizedContent
|
|
882
|
+
// /pause and /resume control the current session's next-tool gate.
|
|
883
|
+
if (normalizedContent.startsWith('/pause ')) {
|
|
884
|
+
return { kind: 'command.error', text: '用法: /pause' };
|
|
885
|
+
}
|
|
886
|
+
if (normalizedContent === '/pause') {
|
|
887
|
+
const pauseSession = await getExistingSessionForCommand();
|
|
888
|
+
if (!pauseSession)
|
|
889
|
+
return { kind: 'command.result', text: '当前没有活跃会话' };
|
|
890
|
+
const pauseAgent = this.getAgent(channel, pauseSession.baseagent);
|
|
891
|
+
const hasActiveTask = pauseAgent.hasActiveStream(pauseSession.id)
|
|
892
|
+
|| this.messageQueue.isProcessing(pauseSession.id);
|
|
893
|
+
if (!hasActiveTask)
|
|
894
|
+
return { kind: 'command.result', text: '当前没有正在处理的任务' };
|
|
895
|
+
const requested = this.processor.pauseSession(pauseSession.id);
|
|
896
|
+
return {
|
|
897
|
+
kind: 'command.result',
|
|
898
|
+
text: requested
|
|
899
|
+
? '✓ 已请求暂停,任务将在下一次工具调用前暂停'
|
|
900
|
+
: '当前任务已请求暂停或正在暂停',
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
if (normalizedContent.startsWith('/resume ')) {
|
|
904
|
+
return { kind: 'command.error', text: '用法: /resume' };
|
|
905
|
+
}
|
|
906
|
+
if (normalizedContent === '/resume') {
|
|
856
907
|
const resumeSession = await getExistingSessionForCommand();
|
|
857
908
|
if (!resumeSession)
|
|
858
909
|
return { kind: 'command.result', text: '当前没有活跃会话' };
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
const homeDir = os.homedir();
|
|
862
|
-
const encodedPath = encodePath(resumeSession.projectPath);
|
|
863
|
-
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
864
|
-
if (!fs.existsSync(projectDir)) {
|
|
865
|
-
return { kind: 'command.error', text: '❌ 未找到 Claude 会话记录目录' };
|
|
866
|
-
}
|
|
867
|
-
const jsonlFiles = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
|
|
868
|
-
if (jsonlFiles.length === 0) {
|
|
869
|
-
return { kind: 'command.error', text: '❌ 当前项目没有 Claude 会话记录' };
|
|
870
|
-
}
|
|
871
|
-
const sessions = [];
|
|
872
|
-
for (const file of jsonlFiles) {
|
|
873
|
-
const filePath = path.join(projectDir, file);
|
|
874
|
-
const sessionId = file.replace('.jsonl', '');
|
|
875
|
-
let lastTimestamp = '';
|
|
876
|
-
let firstUserMessage = '';
|
|
877
|
-
let model = '';
|
|
878
|
-
let branch = '';
|
|
879
|
-
let turns = 0;
|
|
880
|
-
try {
|
|
881
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
882
|
-
const lines = content.split('\n').filter(l => l.trim());
|
|
883
|
-
for (const line of lines) {
|
|
884
|
-
const event = JSON.parse(line);
|
|
885
|
-
if (event.timestamp && event.timestamp > lastTimestamp) {
|
|
886
|
-
lastTimestamp = event.timestamp;
|
|
887
|
-
}
|
|
888
|
-
if (event.gitBranch && !branch) {
|
|
889
|
-
branch = event.gitBranch;
|
|
890
|
-
}
|
|
891
|
-
if (event.type === 'user' && event.message?.role === 'user') {
|
|
892
|
-
const msgContent = event.message.content;
|
|
893
|
-
const isToolResult = Array.isArray(msgContent) && msgContent.every((c) => c.type === 'tool_result');
|
|
894
|
-
if (!isToolResult) {
|
|
895
|
-
turns++;
|
|
896
|
-
if (!firstUserMessage) {
|
|
897
|
-
let candidate = '';
|
|
898
|
-
if (typeof msgContent === 'string') {
|
|
899
|
-
candidate = msgContent;
|
|
900
|
-
}
|
|
901
|
-
else if (Array.isArray(msgContent)) {
|
|
902
|
-
const textBlock = msgContent.find((c) => c.type === 'text');
|
|
903
|
-
if (textBlock?.text) {
|
|
904
|
-
candidate = textBlock.text;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
// 跳过 Claude Code 注入的脚手架 prompt,取第一条真人消息
|
|
908
|
-
if (candidate && !isSyntheticCliPrompt(candidate, 'claude')) {
|
|
909
|
-
firstUserMessage = candidate.slice(0, 100);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
if (event.type === 'assistant' && event.message?.model && !model) {
|
|
915
|
-
model = event.message.model;
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
catch {
|
|
920
|
-
continue;
|
|
921
|
-
}
|
|
922
|
-
if (!lastTimestamp)
|
|
923
|
-
continue;
|
|
924
|
-
sessions.push({
|
|
925
|
-
sessionId,
|
|
926
|
-
lastMessageTime: lastTimestamp,
|
|
927
|
-
firstUserMessage: firstUserMessage || '(无消息)',
|
|
928
|
-
model: model || 'unknown',
|
|
929
|
-
turns,
|
|
930
|
-
branch: branch || 'unknown',
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
sessions.sort((a, b) => b.lastMessageTime.localeCompare(a.lastMessageTime));
|
|
934
|
-
return { kind: 'command.result', text: JSON.stringify(sessions, null, 2) };
|
|
935
|
-
}
|
|
936
|
-
catch (error) {
|
|
937
|
-
logger.error('[CommandHandler] /resume failed:', error);
|
|
938
|
-
return { kind: 'command.error', text: `❌ 读取会话记录失败: ${error instanceof Error ? error.message : '未知错误'}` };
|
|
939
|
-
}
|
|
910
|
+
const resumed = this.processor.resumeSession(resumeSession.id);
|
|
911
|
+
return { kind: 'command.result', text: resumed ? '✓ 已继续当前任务' : '当前任务未处于暂停状态' };
|
|
940
912
|
}
|
|
941
913
|
// /baseagent 命令:查看或切换 Agent 后端
|
|
942
914
|
if (normalizedContent === '/baseagent' || normalizedContent.startsWith('/baseagent ')) {
|
|
@@ -988,6 +960,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
988
960
|
if (!owningAgent) {
|
|
989
961
|
return { kind: 'command.error', text: '❌ 当前 channel 无绑定 agent,无法设置 active_baseagent' };
|
|
990
962
|
}
|
|
963
|
+
await interruptPausedSessionBeforeReplacement(await getExistingSessionForCommand());
|
|
991
964
|
const previousDefaultBaseagent = owningAgent.baseagent || this.parseDefaultBaseagent();
|
|
992
965
|
owningAgent.setActiveBaseagent(args);
|
|
993
966
|
this.eventBus.publish({
|
|
@@ -1646,11 +1619,6 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1646
1619
|
if (normalizedContent === '/reload' || normalizedContent.startsWith('/reload ')) {
|
|
1647
1620
|
const aidArg = normalizedContent.slice('/reload'.length).trim() || undefined;
|
|
1648
1621
|
const selfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
|
|
1649
|
-
// agent channel 的 owner/admin 不能跨 agent reload;先返回领域内错误,避免落到
|
|
1650
|
-
// 底层 daemon-owner 约束的英文 reason。
|
|
1651
|
-
if (!isDaemonOwner && aidArg && aidArg !== selfAid) {
|
|
1652
|
-
return { kind: 'command.error', text: '❌ 无权限:跨 agent reload 仅限 daemon owner 使用' };
|
|
1653
|
-
}
|
|
1654
1622
|
const targetAid = aidArg ?? selfAid;
|
|
1655
1623
|
if (!targetAid) {
|
|
1656
1624
|
return { kind: 'command.error', text: '❌ 无法确定目标 agent,请指定 aid:/reload <aid>' };
|
|
@@ -1664,7 +1632,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
1664
1632
|
args: { ...(aidArg ? { aid: aidArg } : {}), ...(selfAid ? { self: selfAid } : {}) },
|
|
1665
1633
|
dangerous: true,
|
|
1666
1634
|
},
|
|
1667
|
-
identity
|
|
1635
|
+
identity,
|
|
1668
1636
|
session: activeSession,
|
|
1669
1637
|
channel,
|
|
1670
1638
|
channelId,
|
|
@@ -2010,12 +1978,16 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2010
1978
|
return { kind: 'command.result', text: `✅ @ 处理模式已切换: ${currentMode ?? '未设置'} → ${arg}` };
|
|
2011
1979
|
}
|
|
2012
1980
|
// /stop 命令:中断当前任务
|
|
1981
|
+
if (normalizedContent.startsWith('/stop ')) {
|
|
1982
|
+
return { kind: 'command.error', text: '用法: /stop' };
|
|
1983
|
+
}
|
|
2013
1984
|
if (normalizedContent === '/stop') {
|
|
2014
1985
|
const stopSession = await getExistingSessionForCommand();
|
|
2015
1986
|
if (!stopSession)
|
|
2016
1987
|
return { kind: 'command.result', text: '当前没有正在处理的任务' };
|
|
2017
1988
|
const stopAgent = this.getAgent(channel, stopSession.baseagent);
|
|
2018
1989
|
const sessionKey = stopSession.id;
|
|
1990
|
+
this.processor?.clearPauseSession?.(sessionKey);
|
|
2019
1991
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
2020
1992
|
const hasActive = stopAgent.hasActiveStream(sessionKey);
|
|
2021
1993
|
const isProcessing = this.messageQueue.isProcessing(sessionKey);
|
|
@@ -2049,6 +2021,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2049
2021
|
if (!session.agentSessionId) {
|
|
2050
2022
|
return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需压缩' };
|
|
2051
2023
|
}
|
|
2024
|
+
await interruptPausedSessionBeforeReplacement(session);
|
|
2052
2025
|
const projectPath = path.isAbsolute(session.projectPath)
|
|
2053
2026
|
? session.projectPath
|
|
2054
2027
|
: path.resolve(process.cwd(), session.projectPath);
|
|
@@ -2168,6 +2141,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2168
2141
|
return { kind: 'command.error', text: `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称` };
|
|
2169
2142
|
}
|
|
2170
2143
|
}
|
|
2144
|
+
await interruptPausedSessionBeforeReplacement(session || activeSession);
|
|
2171
2145
|
const projectPath = this.getEffectiveDefaultPath(channel);
|
|
2172
2146
|
if (sendMessage && session) {
|
|
2173
2147
|
await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session));
|
|
@@ -2291,7 +2265,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2291
2265
|
groups.set(type, []);
|
|
2292
2266
|
groups.get(type).push({ name, status });
|
|
2293
2267
|
}
|
|
2294
|
-
if (!isAdmin) {
|
|
2268
|
+
if (!isAdmin && !isDaemonOwner) {
|
|
2295
2269
|
// visitor/member: 仅显示实例通道摘要
|
|
2296
2270
|
const total = [...groups.values()].flat().length;
|
|
2297
2271
|
const healthy = [...groups.values()].flat().filter(i => i.status.includes('✓')).length;
|
|
@@ -2443,9 +2417,6 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2443
2417
|
if (normalizedContent === '/restart') {
|
|
2444
2418
|
// 进程级操作:必须是 daemon owner(daemon.json.owners),与 menu 协议 /system restart 一致。
|
|
2445
2419
|
// agent-channel 的 owner/admin 角色不足以重启整个 daemon。
|
|
2446
|
-
if (!isDaemonOwner) {
|
|
2447
|
-
return { kind: 'command.error', text: '❌ 无权限:服务重启仅限 daemon owner 使用' };
|
|
2448
|
-
}
|
|
2449
2420
|
const restartSelfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
|
|
2450
2421
|
const authDenied = await authorizeIntent({
|
|
2451
2422
|
intent: {
|
|
@@ -2455,7 +2426,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2455
2426
|
args: {},
|
|
2456
2427
|
dangerous: true,
|
|
2457
2428
|
},
|
|
2458
|
-
identity
|
|
2429
|
+
identity,
|
|
2459
2430
|
session: activeSession,
|
|
2460
2431
|
channel,
|
|
2461
2432
|
channelId,
|
|
@@ -2624,7 +2595,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2624
2595
|
args: {},
|
|
2625
2596
|
dangerous: true,
|
|
2626
2597
|
},
|
|
2627
|
-
identity
|
|
2598
|
+
identity,
|
|
2628
2599
|
session: activeSession,
|
|
2629
2600
|
channel,
|
|
2630
2601
|
channelId,
|
|
@@ -3008,6 +2979,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3008
2979
|
: await this.sessionManager.listImportableCliSessions(projectPath, currentBaseagent);
|
|
3009
2980
|
const cliSession = cliSessions.find((c) => c.uuid.startsWith(sessionName));
|
|
3010
2981
|
if (cliSession) {
|
|
2982
|
+
// Importing a CLI session replaces the active session. Cancel any
|
|
2983
|
+
// pending tool-boundary pause only after the target has been
|
|
2984
|
+
// validated, so an invalid UUID cannot interrupt the current task.
|
|
2985
|
+
await interruptPausedSessionBeforeReplacement(session || activeSession);
|
|
3011
2986
|
const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid, currentBaseagent, selfAID ?? session?.selfAID);
|
|
3012
2987
|
this.eventBus.publish({ type: 'session:imported', sessionId: imported.id, agentSessionId: cliSession.uuid, projectPath });
|
|
3013
2988
|
const projectName = this.getProjectName(projectPath);
|
|
@@ -3038,6 +3013,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3038
3013
|
if (!session.threadId && targetSession.threadId) {
|
|
3039
3014
|
return { kind: 'command.error', text: `❌ 无法从主会话切换到话题会话\n话题会话仅在对应话题内可用` };
|
|
3040
3015
|
}
|
|
3016
|
+
await interruptPausedSessionBeforeReplacement(session);
|
|
3017
|
+
this.processor?.clearPauseSession?.(session.id);
|
|
3018
|
+
this.processor?.clearPauseSession?.(targetSession.id);
|
|
3041
3019
|
const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
|
|
3042
3020
|
if (!switched) {
|
|
3043
3021
|
return { kind: 'command.error', text: `❌ 切换会话失败` };
|
|
@@ -3125,6 +3103,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3125
3103
|
}
|
|
3126
3104
|
this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
|
|
3127
3105
|
const targetAgent = this.getAgent(channel, targetSession.baseagent);
|
|
3106
|
+
this.processor?.clearPauseSession?.(targetSession.id);
|
|
3128
3107
|
await targetAgent.closeSession(targetSession.id);
|
|
3129
3108
|
return { kind: 'command.result', text: `✓ 已删除会话: ${displaySessionTitle(targetSession.name, sessionName)}\n会话文件已保留,可通过 CLI 访问` };
|
|
3130
3109
|
}
|
|
@@ -3141,6 +3120,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3141
3120
|
if (!forkAgent.capabilities?.fork) {
|
|
3142
3121
|
return { kind: 'command.error', text: `❌ 当前 Agent (${forkAgent.name}) 不支持 /fork\n\n可使用 /new 创建新会话替代` };
|
|
3143
3122
|
}
|
|
3123
|
+
await interruptPausedSessionBeforeReplacement(session);
|
|
3144
3124
|
try {
|
|
3145
3125
|
const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
|
|
3146
3126
|
const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
|
|
@@ -3195,6 +3175,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3195
3175
|
if (!['chat', 'file', 'all'].includes(mode)) {
|
|
3196
3176
|
return { kind: 'command.error', text: `❌ 无效模式 "${mode}",可选:chat | file | all` };
|
|
3197
3177
|
}
|
|
3178
|
+
await interruptPausedSessionBeforeReplacement(session);
|
|
3198
3179
|
return { kind: 'command.result', text: await this.handleRewind(session, rewindAgent, turnNum, mode) };
|
|
3199
3180
|
}
|
|
3200
3181
|
// /repair 命令:检查并修复会话文件
|
|
@@ -3204,6 +3185,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
3204
3185
|
return { kind: 'command.result', text: '当前没有活跃会话' };
|
|
3205
3186
|
const repairAgent = this.getAgent(channel, repairSession.baseagent);
|
|
3206
3187
|
const { checkSessionFile, backupSessionFile } = await import('../session/session-file-health.js');
|
|
3188
|
+
await interruptPausedSessionBeforeReplacement(repairSession);
|
|
3207
3189
|
try {
|
|
3208
3190
|
if (!repairSession.agentSessionId) {
|
|
3209
3191
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|