@xcanwin/manyoyo 8.6.8 → 8.6.10
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/bin/manyoyo.js +41 -3
- package/lib/agent-instructions.js +73 -0
- package/lib/app-update.js +20 -8
- package/lib/code-update.js +165 -0
- package/lib/container-state.js +21 -1
- package/lib/plugin/playwright.js +4 -2
- package/lib/setup-config.js +10 -1
- package/lib/web/AGENTS.md +4 -0
- package/lib/web/container-manage.js +33 -0
- package/lib/web/git-changes.js +130 -0
- package/lib/web/index.html +95 -90
- package/lib/web/server.js +402 -20
- package/lib/web/session-meta.js +117 -0
- package/manyoyo.example.json +5 -0
- package/package.json +1 -1
package/lib/web/server.js
CHANGED
|
@@ -12,7 +12,10 @@ const JSON5 = require('json5');
|
|
|
12
12
|
const { buildContainerRunArgs, hasNetworkArg } = require('../container-run');
|
|
13
13
|
const { buildContainerIntegration, mergeIntegration } = require('../plugin');
|
|
14
14
|
const { applyAptMirror } = require('../mirrors');
|
|
15
|
+
const { BUILTIN_AGENT_INSTRUCTIONS, buildAgentInstructions, composeSystemPrompt, SYSTEM_PROMPT_MAX_CHARS, toTomlBasicString } = require('../agent-instructions');
|
|
15
16
|
const containerState = require('../container-state');
|
|
17
|
+
const sessionMeta = require('./session-meta');
|
|
18
|
+
const gitChanges = require('./git-changes');
|
|
16
19
|
const { buildExecArgs, resolveContainerId, readMergedEnv } = require('../container-exec');
|
|
17
20
|
const { createNetworkManager, NETWORK_NAME } = require('../container-network');
|
|
18
21
|
const { normalizePolicy, isUnrestricted: isUnrestrictedPolicy } = require('../network-policy');
|
|
@@ -34,7 +37,7 @@ const { resolveRuntimeConfig } = require('../runtime-resolver');
|
|
|
34
37
|
const { AGENT_ENV_SCHEMA } = require('../init-config');
|
|
35
38
|
const { ensureImagePresent, pullImageProcess } = require('../image-pull');
|
|
36
39
|
const { waitForImport } = require('../offline-import');
|
|
37
|
-
const { baseRawOf, buildAgentConfigRaw, buildPasswordConfigRaw, shouldWriteMirrors, buildMirrorsConfigRaw } = require('../setup-config');
|
|
40
|
+
const { baseRawOf, buildAgentConfigRaw, buildPasswordConfigRaw, shouldWriteMirrors, buildMirrorsConfigRaw, buildAgentInstructionsConfigRaw, buildBooleanConfigRaw } = require('../setup-config');
|
|
38
41
|
const { listSetupAgents, getConfiguredAgents, buildAgentRunProfile, classifyConnectionResult, redactSecrets, validateSetupPassword, listMirrorPresets } = require('../setup');
|
|
39
42
|
const { applyDoctorFixes } = require('../doctor');
|
|
40
43
|
const { writeConfigFileSecure } = require('../secure-file');
|
|
@@ -96,6 +99,7 @@ const NATIVE_SESSION_RESUME_PROGRAMS = new Set(['claude', 'codex', 'opencode']);
|
|
|
96
99
|
const WEB_FILE_PREVIEW_MAX_BYTES = 512 * 1024;
|
|
97
100
|
const WEB_FILE_EDIT_MAX_BYTES = 2 * 1024 * 1024;
|
|
98
101
|
const WEB_FILE_RAW_MAX_CONCURRENT_PER_CONTAINER = 4;
|
|
102
|
+
const WEB_FS_LIST_MAX_LIMIT = 200;
|
|
99
103
|
const IMAGE_EXTENSION_CONTENT_TYPES = {
|
|
100
104
|
'.png': 'image/png',
|
|
101
105
|
'.jpg': 'image/jpeg',
|
|
@@ -221,7 +225,11 @@ function createEmptyWebAgentSession(agentId, agentName) {
|
|
|
221
225
|
usageTotal: null,
|
|
222
226
|
model: '',
|
|
223
227
|
queue: [],
|
|
224
|
-
queuePaused: false
|
|
228
|
+
queuePaused: false,
|
|
229
|
+
title: '',
|
|
230
|
+
firstPrompt: '',
|
|
231
|
+
pinned: false,
|
|
232
|
+
slashCommands: null
|
|
225
233
|
};
|
|
226
234
|
}
|
|
227
235
|
|
|
@@ -275,7 +283,23 @@ function normalizeWebAgentSessionRecord(agentId, rawAgent) {
|
|
|
275
283
|
usageTotal: normalizeWebAgentUsageTotal(source.usageTotal),
|
|
276
284
|
model: normalizeStoredAgentModelValue(source.model),
|
|
277
285
|
queue: agentQueue.normalizeStoredQueue(source.queue),
|
|
278
|
-
queuePaused: source.queuePaused === true
|
|
286
|
+
queuePaused: source.queuePaused === true,
|
|
287
|
+
title: sessionMeta.normalizeTitle(source.title),
|
|
288
|
+
firstPrompt: sessionMeta.normalizeFirstPrompt(source.firstPrompt),
|
|
289
|
+
pinned: source.pinned === true,
|
|
290
|
+
slashCommands: normalizeStoredSlashCommands(source.slashCommands)
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// claude 每次运行开头的 init 事件里带的命令列表:all 是全部,skills 是其中的 skill,terminal 是只在终端里可用的
|
|
295
|
+
function normalizeStoredSlashCommands(value) {
|
|
296
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
all: sessionMeta.normalizeSlashCommands(value.all),
|
|
301
|
+
skills: sessionMeta.normalizeSlashCommands(value.skills),
|
|
302
|
+
terminal: sessionMeta.normalizeSlashCommands(value.terminal)
|
|
279
303
|
};
|
|
280
304
|
}
|
|
281
305
|
|
|
@@ -570,6 +594,10 @@ function appendWebSessionMessage(webHistoryDir, sessionRefOrContainerName, role,
|
|
|
570
594
|
if (!agentSession.createdAt) {
|
|
571
595
|
agentSession.createdAt = timestamp;
|
|
572
596
|
}
|
|
597
|
+
// 首条 Agent 对话消息只记一次,作为没有标题时的会话名(消息会被裁剪,不能现算)
|
|
598
|
+
if (role === 'user' && extra.mode !== 'command' && !agentSession.firstPrompt) {
|
|
599
|
+
agentSession.firstPrompt = sessionMeta.normalizeFirstPrompt(content);
|
|
600
|
+
}
|
|
573
601
|
agentSession.messages.push(message);
|
|
574
602
|
|
|
575
603
|
if (agentSession.messages.length > WEB_HISTORY_MAX_MESSAGES) {
|
|
@@ -911,7 +939,8 @@ function quoteBashSingleValue(value) {
|
|
|
911
939
|
function renderAgentPromptCommand(template, prompt) {
|
|
912
940
|
const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
913
941
|
const safePrompt = quoteBashSingleValue(prompt);
|
|
914
|
-
|
|
942
|
+
// 用函数返回替换值:字符串形式会把提示词里的 $' $` $& 当成替换模式
|
|
943
|
+
return templateText.replace(/\{prompt\}/g, () => safePrompt);
|
|
915
944
|
}
|
|
916
945
|
|
|
917
946
|
function buildCodexAgentExecCommand(template, prompt, options = {}) {
|
|
@@ -929,12 +958,17 @@ function buildCodexAgentExecCommand(template, prompt, options = {}) {
|
|
|
929
958
|
const injectedFlags = hasJson ? '' : ' --json';
|
|
930
959
|
const hasModel = /(?:^|\s)(?:-m|--model)(?:\s|$)/.test(suffix);
|
|
931
960
|
const modelFlag = model && !hasModel ? ` --model ${quoteBashSingleValue(model)}` : '';
|
|
961
|
+
// 环境说明:模板里已经写了 developer_instructions 就以用户的为准
|
|
962
|
+
const instructions = options && typeof options.instructions === 'string' ? options.instructions : '';
|
|
963
|
+
const instructionsFlag = instructions && !/developer_instructions/.test(suffix)
|
|
964
|
+
? ` -c ${quoteBashSingleValue(`developer_instructions=${toTomlBasicString(instructions)}`)}`
|
|
965
|
+
: '';
|
|
932
966
|
if (sessionId) {
|
|
933
967
|
const promptIndex = suffix.indexOf('{prompt}');
|
|
934
968
|
const resumeSuffix = `${suffix.slice(0, promptIndex)}${quoteBashSingleValue(sessionId)} ${suffix.slice(promptIndex)}`;
|
|
935
|
-
codexTemplate = `${prefix}codex exec resume${injectedFlags}${modelFlag}${resumeSuffix}`;
|
|
969
|
+
codexTemplate = `${prefix}codex exec resume${injectedFlags}${modelFlag}${instructionsFlag}${resumeSuffix}`;
|
|
936
970
|
} else {
|
|
937
|
-
codexTemplate = `${prefix}codex exec${injectedFlags}${modelFlag}${suffix}`;
|
|
971
|
+
codexTemplate = `${prefix}codex exec${injectedFlags}${modelFlag}${instructionsFlag}${suffix}`;
|
|
938
972
|
}
|
|
939
973
|
}
|
|
940
974
|
return codexTemplate === templateText
|
|
@@ -948,10 +982,12 @@ function prependAgentFlags(commandText, matchPattern, flagSpecs) {
|
|
|
948
982
|
return String(commandText || '');
|
|
949
983
|
}
|
|
950
984
|
const prefix = matched[1] || '';
|
|
951
|
-
|
|
985
|
+
// “模板里是否已经写了这个参数”只看原模板:插进去的参数值(比如环境说明正文里的 “-r”)不能让后面的参数被跳过
|
|
986
|
+
const original = matched[matched.length - 1] || '';
|
|
987
|
+
let suffix = original;
|
|
952
988
|
for (let i = flagSpecs.length - 1; i >= 0; i -= 1) {
|
|
953
989
|
const spec = flagSpecs[i];
|
|
954
|
-
if (!spec || !spec.flag || !(spec.pattern instanceof RegExp) || spec.pattern.test(
|
|
990
|
+
if (!spec || !spec.flag || !(spec.pattern instanceof RegExp) || spec.pattern.test(original)) {
|
|
955
991
|
continue;
|
|
956
992
|
}
|
|
957
993
|
suffix = ` ${spec.flag}${suffix}`;
|
|
@@ -978,6 +1014,14 @@ function buildClaudeAgentExecCommand(template, prompt, options = {}) {
|
|
|
978
1014
|
if (model) {
|
|
979
1015
|
flagSpecs.push({ flag: `--model ${quoteBashSingleValue(model)}`, pattern: /(?:^|\s)--model(?:\s|$)/ });
|
|
980
1016
|
}
|
|
1017
|
+
// 环境说明:追加在内置系统提示之后;模板里已经写了 --append-system-prompt(-file) 就以用户的为准
|
|
1018
|
+
const instructions = options && typeof options.instructions === 'string' ? options.instructions : '';
|
|
1019
|
+
if (instructions) {
|
|
1020
|
+
flagSpecs.push({
|
|
1021
|
+
flag: `--append-system-prompt ${quoteBashSingleValue(instructions)}`,
|
|
1022
|
+
pattern: /(?:^|\s)--append-system-prompt(?:-file)?(?:\s|$)/
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
981
1025
|
// 子 Agent 说的话和思考默认不转发(v2.1.211+ 才有 --forward-subagent-text,旧版本不认这个参数会直接报错)。
|
|
982
1026
|
// 'auto':在容器里随命令探测(claude --help 约 50ms),支持就展开成这个参数、不支持展开成空;true:直接加。
|
|
983
1027
|
// 'auto' 的参数放在整条命令末尾,不改变前面已有参数的相对顺序
|
|
@@ -1035,14 +1079,20 @@ function buildOpenCodeAgentExecCommand(template, prompt, options = {}) {
|
|
|
1035
1079
|
if (model) {
|
|
1036
1080
|
flagSpecs.push({ flag: `--model ${quoteBashSingleValue(model)}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
|
|
1037
1081
|
}
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
flagSpecs
|
|
1042
|
-
);
|
|
1043
|
-
return opencodeTemplate === templateText
|
|
1082
|
+
const opencodePattern = /^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)opencode\b.*?\s+run\b)(.*)$/;
|
|
1083
|
+
const opencodeTemplate = prependAgentFlags(templateText, opencodePattern, flagSpecs);
|
|
1084
|
+
const rendered = opencodeTemplate === templateText
|
|
1044
1085
|
? renderAgentPromptCommand(templateText, prompt)
|
|
1045
1086
|
: renderAgentPromptCommand(opencodeTemplate, prompt);
|
|
1087
|
+
// 环境说明:opencode 的 instructions 只认文件路径。OPENCODE_CONFIG_CONTENT 会与用户自己的配置合并
|
|
1088
|
+
//(instructions 数组拼接,实测 1.19.3),用户已经设置了这个变量或在模板里写了它时不动
|
|
1089
|
+
const instructions = options && typeof options.instructions === 'string' ? options.instructions : '';
|
|
1090
|
+
if (!instructions || !opencodePattern.test(templateText) || /OPENCODE_CONFIG_CONTENT/.test(templateText)) {
|
|
1091
|
+
return rendered;
|
|
1092
|
+
}
|
|
1093
|
+
const file = '/tmp/manyoyo-agent-instructions.md';
|
|
1094
|
+
const content = quoteBashSingleValue(JSON.stringify({ instructions: [file] }));
|
|
1095
|
+
return `if [ -z "\${OPENCODE_CONFIG_CONTENT:-}" ]; then printf '%s' ${quoteBashSingleValue(instructions)} > ${file} && export OPENCODE_CONFIG_CONTENT=${content}; fi; ${rendered}`;
|
|
1046
1096
|
}
|
|
1047
1097
|
|
|
1048
1098
|
function buildWebAgentExecCommand(template, prompt, agentProgram, options = {}) {
|
|
@@ -1214,6 +1264,23 @@ function extractClaudeSessionId(text) {
|
|
|
1214
1264
|
return '';
|
|
1215
1265
|
}
|
|
1216
1266
|
|
|
1267
|
+
function extractClaudeInitSlashCommands(text) {
|
|
1268
|
+
for (const rawLine of String(text || '').split('\n')) {
|
|
1269
|
+
const payload = parseJsonObjectLine(rawLine);
|
|
1270
|
+
if (payload && payload.type === 'system' && payload.subtype === 'init') {
|
|
1271
|
+
if (!Array.isArray(payload.slash_commands)) {
|
|
1272
|
+
return null;
|
|
1273
|
+
}
|
|
1274
|
+
return normalizeStoredSlashCommands({
|
|
1275
|
+
all: payload.slash_commands,
|
|
1276
|
+
skills: payload.skills,
|
|
1277
|
+
terminal: payload.terminal_slash_commands
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return null;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1217
1284
|
function extractOpenCodeSessionId(text) {
|
|
1218
1285
|
for (const rawLine of String(text || '').split('\n')) {
|
|
1219
1286
|
const payload = parseJsonObjectLine(rawLine);
|
|
@@ -2313,10 +2380,18 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
|
|
|
2313
2380
|
const effectivePrompt = resumeSucceeded
|
|
2314
2381
|
? prompt
|
|
2315
2382
|
: buildAgentPromptWithHistory(agentSession, prompt);
|
|
2383
|
+
// 环境说明每次执行时现读配置:改了设置不用重建容器;运行配置名记在创建时的快照里(旧会话没有,用全局配置)
|
|
2384
|
+
const configSnapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
2385
|
+
const runName = history.runtimeSnapshot && typeof history.runtimeSnapshot.run === 'string' ? history.runtimeSnapshot.run : '';
|
|
2316
2386
|
const command = buildWebAgentExecCommand(effectiveTemplate, effectivePrompt, agentMeta.agentProgram, {
|
|
2317
2387
|
sessionId: sessionIdForRun,
|
|
2318
2388
|
model: agentSession.model || '',
|
|
2319
|
-
forwardSubagentText: agentMeta.agentProgram === 'claude' ? 'auto' : false
|
|
2389
|
+
forwardSubagentText: agentMeta.agentProgram === 'claude' ? 'auto' : false,
|
|
2390
|
+
instructions: buildAgentInstructions(
|
|
2391
|
+
configSnapshot.parseError ? {} : configSnapshot.parsed,
|
|
2392
|
+
runName,
|
|
2393
|
+
readContainerSystemPrompt(ctx, sessionRef.containerName)
|
|
2394
|
+
)
|
|
2320
2395
|
});
|
|
2321
2396
|
const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
|
|
2322
2397
|
|
|
@@ -2357,10 +2432,66 @@ function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, m
|
|
|
2357
2432
|
if (turnUsage) {
|
|
2358
2433
|
patch.usageTotal = accumulateUsageTotal(agentSession.usageTotal, turnUsage);
|
|
2359
2434
|
}
|
|
2435
|
+
if (agentMeta.agentProgram === 'claude') {
|
|
2436
|
+
const slashCommands = extractClaudeInitSlashCommands(result.stdout);
|
|
2437
|
+
if (slashCommands) {
|
|
2438
|
+
patch.slashCommands = slashCommands;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2360
2441
|
patchWebAgentSessionState(state.webHistoryDir, sessionRef, patch);
|
|
2361
2442
|
return replyMessage;
|
|
2362
2443
|
}
|
|
2363
2444
|
|
|
2445
|
+
function resolveWebSessionWorkDir(state, sessionRef) {
|
|
2446
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
2447
|
+
const applied = history.applied && typeof history.applied === 'object' ? history.applied : {};
|
|
2448
|
+
return typeof applied.containerPath === 'string' && applied.containerPath ? applied.containerPath : '/';
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
async function readWebGitChanges(ctx, state, sessionRef) {
|
|
2452
|
+
const result = await execCommandInWebContainer(
|
|
2453
|
+
ctx,
|
|
2454
|
+
sessionRef.containerName,
|
|
2455
|
+
gitChanges.buildGitChangesCommand(resolveWebSessionWorkDir(state, sessionRef)),
|
|
2456
|
+
{ withEnv: false }
|
|
2457
|
+
);
|
|
2458
|
+
return gitChanges.parseGitChangesOutput(result.stdout);
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// 每轮结束后在后台读一次 Agent 自己生成的标题(目前只有 claude 会写 ai-title,交互模式实测;
|
|
2462
|
+
// 其他 Agent 只用首条消息)。不阻塞本轮结果;读不到或失败都静默——每轮都会触发,属于高频路径,不写日志
|
|
2463
|
+
async function refreshWebAgentTitle(ctx, state, sessionRef, agentProgram) {
|
|
2464
|
+
if (agentProgram !== 'claude') {
|
|
2465
|
+
return '';
|
|
2466
|
+
}
|
|
2467
|
+
try {
|
|
2468
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
2469
|
+
const agentSession = getWebAgentSession(history, sessionRef.agentId, { includeArchived: true });
|
|
2470
|
+
const command = sessionMeta.buildClaudeTitleReadCommand(agentSession && agentSession.engineSessionId);
|
|
2471
|
+
if (!command) {
|
|
2472
|
+
return '';
|
|
2473
|
+
}
|
|
2474
|
+
const sessionId = agentSession.engineSessionId;
|
|
2475
|
+
const result = await execCommandInWebContainer(ctx, sessionRef.containerName, command);
|
|
2476
|
+
const title = sessionMeta.parseClaudeTitleOutput(result.stdout);
|
|
2477
|
+
// 读标题期间 AGENT 可能被删除或换了引擎会话:写回前重新读一次,不在了就放弃(不能把它重新建出来)
|
|
2478
|
+
const fresh = getWebAgentSession(
|
|
2479
|
+
loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName),
|
|
2480
|
+
sessionRef.agentId,
|
|
2481
|
+
{ includeArchived: true }
|
|
2482
|
+
);
|
|
2483
|
+
if (!fresh || fresh.engineSessionId !== sessionId) {
|
|
2484
|
+
return '';
|
|
2485
|
+
}
|
|
2486
|
+
if (title && title !== fresh.title) {
|
|
2487
|
+
patchWebAgentSessionState(state.webHistoryDir, sessionRef, { title });
|
|
2488
|
+
}
|
|
2489
|
+
return title;
|
|
2490
|
+
} catch (e) {
|
|
2491
|
+
return '';
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2364
2495
|
function appendWebAgentTraceMessage(webHistoryDir, sessionRefOrContainerName, content, extra = {}) {
|
|
2365
2496
|
const text = String(content || '').trim();
|
|
2366
2497
|
if (!text) {
|
|
@@ -2841,6 +2972,14 @@ function validateWebConfigShape(configObject) {
|
|
|
2841
2972
|
if (hasOwn(config, 'agentPromptCommand')) {
|
|
2842
2973
|
normalizeAgentPromptCommandTemplate(config.agentPromptCommand, 'agentPromptCommand');
|
|
2843
2974
|
}
|
|
2975
|
+
if (hasOwn(config, 'agentInstructions') && typeof config.agentInstructions !== 'string') {
|
|
2976
|
+
throw new Error('agentInstructions 必须是字符串');
|
|
2977
|
+
}
|
|
2978
|
+
['agentInstructionsBuiltinEnabled', 'agentInstructionsCustomEnabled'].forEach(key => {
|
|
2979
|
+
if (hasOwn(config, key) && typeof config[key] !== 'boolean') {
|
|
2980
|
+
throw new Error(`${key} 必须是布尔值`);
|
|
2981
|
+
}
|
|
2982
|
+
});
|
|
2844
2983
|
if (hasOwn(config, 'runs')) {
|
|
2845
2984
|
const runs = config.runs;
|
|
2846
2985
|
if (runs !== undefined && (typeof runs !== 'object' || runs === null || Array.isArray(runs))) {
|
|
@@ -2851,6 +2990,9 @@ function validateWebConfigShape(configObject) {
|
|
|
2851
2990
|
if (hasOwn(normalizedRun, 'agentPromptCommand')) {
|
|
2852
2991
|
normalizeAgentPromptCommandTemplate(normalizedRun.agentPromptCommand, `runs.${runName}.agentPromptCommand`);
|
|
2853
2992
|
}
|
|
2993
|
+
if (hasOwn(normalizedRun, 'agentInstructions') && typeof normalizedRun.agentInstructions !== 'string') {
|
|
2994
|
+
throw new Error(`runs.${runName}.agentInstructions 必须是字符串`);
|
|
2995
|
+
}
|
|
2854
2996
|
});
|
|
2855
2997
|
}
|
|
2856
2998
|
}
|
|
@@ -3063,6 +3205,10 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
3063
3205
|
throw new Error('该网络设置会放开限制(自定义模式、很宽的允许规则或绑定非本机地址),需要确认(confirmRisk)');
|
|
3064
3206
|
}
|
|
3065
3207
|
|
|
3208
|
+
// 系统提示词:请求里填了就用它(创建时定下),否则按系统设置里的默认 + 补充(各自的开关生效)
|
|
3209
|
+
const systemPrompt = normalizeSystemPromptInput(requestOptions.systemPrompt)
|
|
3210
|
+
|| { enabled: true, text: composeSystemPrompt(config, runName) };
|
|
3211
|
+
|
|
3066
3212
|
const containerName = resolvedBase.containerName;
|
|
3067
3213
|
validateContainerNameStrict(containerName);
|
|
3068
3214
|
|
|
@@ -3180,11 +3326,13 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
3180
3326
|
containerPorts,
|
|
3181
3327
|
autostart: manage.autostart,
|
|
3182
3328
|
network: manage.network,
|
|
3329
|
+
systemPrompt,
|
|
3183
3330
|
agentPromptCommand,
|
|
3184
3331
|
defaultCommand: buildDefaultCommand(shellPrefix, shell, shellSuffix) || '/bin/bash',
|
|
3185
3332
|
// 仅供服务端内部复用(如"创建相同配置容器"/"复制容器"),绝不能透传给任何对外响应字段,
|
|
3186
3333
|
// 因为 env/volumes/ports 可能包含密钥原始值(对外响应不暴露它们)。
|
|
3187
3334
|
runtimeSnapshot: {
|
|
3335
|
+
run: runName || '',
|
|
3188
3336
|
hostPath,
|
|
3189
3337
|
containerPath,
|
|
3190
3338
|
imageName,
|
|
@@ -3200,6 +3348,7 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
3200
3348
|
volumes: resolvedBase.volumes || [],
|
|
3201
3349
|
ports: resolvedBase.ports || [],
|
|
3202
3350
|
autostart: manage.autostart,
|
|
3351
|
+
systemPrompt,
|
|
3203
3352
|
// 复制容器时端口暴露(宿主机端口会冲突)与 peers(按 id 引用别的容器)不带过去
|
|
3204
3353
|
network: { ...manage.network, expose: [], peers: { inbound: [] } }
|
|
3205
3354
|
},
|
|
@@ -3224,6 +3373,27 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
3224
3373
|
};
|
|
3225
3374
|
}
|
|
3226
3375
|
|
|
3376
|
+
// 容器自己存的系统提示词;旧容器(没有 id 或没存过)返回 null,按系统设置现算
|
|
3377
|
+
function readContainerSystemPrompt(ctx, containerName) {
|
|
3378
|
+
try {
|
|
3379
|
+
const homeDir = ctx.homeDir || os.homedir();
|
|
3380
|
+
const id = resolveContainerId(ctx.dockerExecArgs, containerName);
|
|
3381
|
+
return id && containerState.stateExists(homeDir, id) ? containerState.readSystemPrompt(homeDir, id) : null;
|
|
3382
|
+
} catch (e) {
|
|
3383
|
+
return null;
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
// 请求里的系统提示词 {enabled, text};没给返回 null,给了但格式不对抛错
|
|
3388
|
+
function normalizeSystemPromptInput(value) {
|
|
3389
|
+
if (value === undefined || value === null) return null;
|
|
3390
|
+
if (typeof value !== 'object' || Array.isArray(value) || typeof value.text !== 'string'
|
|
3391
|
+
|| (value.enabled !== undefined && typeof value.enabled !== 'boolean')) {
|
|
3392
|
+
throw new Error('systemPrompt 必须是 {enabled, text}');
|
|
3393
|
+
}
|
|
3394
|
+
return { enabled: value.enabled !== false, text: value.text.slice(0, SYSTEM_PROMPT_MAX_CHARS) };
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3227
3397
|
function resolveUniqueContainerName(ctx, state, baseName) {
|
|
3228
3398
|
const containerMap = listWebManyoyoContainers(ctx);
|
|
3229
3399
|
const historyNames = listWebHistorySessionNames(state.webHistoryDir, ctx.isValidContainerName);
|
|
@@ -3263,6 +3433,7 @@ function buildCloneCreateOptionsFromSnapshot(runtimeSnapshot, fallbackHistory) {
|
|
|
3263
3433
|
volumes: runtimeSnapshot.volumes || [],
|
|
3264
3434
|
ports: runtimeSnapshot.ports || [],
|
|
3265
3435
|
autostart: runtimeSnapshot.autostart || '',
|
|
3436
|
+
systemPrompt: runtimeSnapshot.systemPrompt || undefined,
|
|
3266
3437
|
network: runtimeSnapshot.network || undefined,
|
|
3267
3438
|
// 快照来自服务端、原容器创建时已确认过,复制不必再确认
|
|
3268
3439
|
confirmRisk: runtimeSnapshot.network ? true : undefined
|
|
@@ -3743,6 +3914,7 @@ async function ensureWebContainer(ctx, state, containerInput, messageSessionRef
|
|
|
3743
3914
|
envLines: userEnvLines,
|
|
3744
3915
|
envFiles: runtime.envFiles,
|
|
3745
3916
|
autostart: runtime.autostart,
|
|
3917
|
+
systemPrompt: runtime.systemPrompt,
|
|
3746
3918
|
network: networkPolicy,
|
|
3747
3919
|
netRequired: !isUnrestrictedPolicy(networkPolicy),
|
|
3748
3920
|
meta: { name: runtime.containerName }
|
|
@@ -3936,13 +4108,19 @@ async function execJsonCommandInWebContainer(ctx, containerName, command) {
|
|
|
3936
4108
|
}
|
|
3937
4109
|
}
|
|
3938
4110
|
|
|
3939
|
-
|
|
4111
|
+
// options.limit / options.prefix 供输入框 @ 补全用:先按名字前缀过滤、按“目录在前 + 名字”排序后只取前 limit 项,
|
|
4112
|
+
// 只对这些项做 lstat(大目录里全量 lstat 才是慢的部分)
|
|
4113
|
+
function buildContainerFileListCommand(requestedPath, options = {}) {
|
|
4114
|
+
const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 0;
|
|
4115
|
+
const prefix = typeof options.prefix === 'string' ? options.prefix.toLowerCase() : '';
|
|
3940
4116
|
return buildWebContainerNodeCommand(`
|
|
3941
4117
|
// __MANYOYO_FS_LIST__
|
|
3942
4118
|
const fs = require('fs');
|
|
3943
4119
|
const path = require('path');
|
|
3944
4120
|
|
|
3945
4121
|
const requestedPath = ${JSON.stringify(String(requestedPath || '/'))};
|
|
4122
|
+
const limit = ${JSON.stringify(limit)};
|
|
4123
|
+
const prefix = ${JSON.stringify(prefix)};
|
|
3946
4124
|
|
|
3947
4125
|
try {
|
|
3948
4126
|
const realPath = fs.realpathSync(requestedPath);
|
|
@@ -3953,7 +4131,20 @@ try {
|
|
|
3953
4131
|
|
|
3954
4132
|
const root = path.parse(realPath).root;
|
|
3955
4133
|
const parentPath = realPath === root ? '' : path.dirname(realPath);
|
|
3956
|
-
|
|
4134
|
+
let dirents = fs.readdirSync(realPath, { withFileTypes: true });
|
|
4135
|
+
if (prefix) {
|
|
4136
|
+
dirents = dirents.filter(entry => entry.name.toLowerCase().startsWith(prefix));
|
|
4137
|
+
}
|
|
4138
|
+
let truncated = false;
|
|
4139
|
+
if (limit > 0 && dirents.length > limit) {
|
|
4140
|
+
dirents.sort((a, b) => {
|
|
4141
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
4142
|
+
return a.name.localeCompare(b.name, 'zh-CN');
|
|
4143
|
+
});
|
|
4144
|
+
dirents = dirents.slice(0, limit);
|
|
4145
|
+
truncated = true;
|
|
4146
|
+
}
|
|
4147
|
+
const entries = dirents
|
|
3957
4148
|
.map(entry => {
|
|
3958
4149
|
const fullPath = path.join(realPath, entry.name);
|
|
3959
4150
|
let itemStat = null;
|
|
@@ -4002,7 +4193,8 @@ try {
|
|
|
4002
4193
|
process.stdout.write(JSON.stringify({
|
|
4003
4194
|
path: realPath,
|
|
4004
4195
|
parentPath,
|
|
4005
|
-
entries
|
|
4196
|
+
entries,
|
|
4197
|
+
...(truncated ? { truncated: true } : {})
|
|
4006
4198
|
}));
|
|
4007
4199
|
} catch (e) {
|
|
4008
4200
|
process.stdout.write(JSON.stringify({
|
|
@@ -5127,6 +5319,7 @@ async function runWebAgentStreamTurn(ctx, state, sessionRef, prompt, clientMessa
|
|
|
5127
5319
|
resumeError,
|
|
5128
5320
|
engineSessionId
|
|
5129
5321
|
}, result);
|
|
5322
|
+
void refreshWebAgentTitle(ctx, state, sessionRef, agentMeta.agentProgram);
|
|
5130
5323
|
emitStreamEvent({
|
|
5131
5324
|
type: 'result',
|
|
5132
5325
|
exitCode: result.exitCode,
|
|
@@ -5451,6 +5644,9 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef, preloadedHist
|
|
|
5451
5644
|
agentId,
|
|
5452
5645
|
agentName: agentSession.agentName,
|
|
5453
5646
|
agentRemark: agentSession.remark || '',
|
|
5647
|
+
agentTitle: agentSession.title || '',
|
|
5648
|
+
firstPrompt: agentSession.firstPrompt || '',
|
|
5649
|
+
pinned: agentSession.pinned === true,
|
|
5454
5650
|
containerRemark: history.remark || '',
|
|
5455
5651
|
status: containerInfo.status || 'history',
|
|
5456
5652
|
image: containerInfo.image || '',
|
|
@@ -6223,6 +6419,65 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
6223
6419
|
});
|
|
6224
6420
|
}
|
|
6225
6421
|
},
|
|
6422
|
+
{
|
|
6423
|
+
// 系统设置“Agent 环境说明”:内置说明(只读)+ 全局配置里用户追加的说明
|
|
6424
|
+
method: 'GET',
|
|
6425
|
+
match: currentPath => currentPath === '/api/agent-instructions' ? [] : null,
|
|
6426
|
+
handler: async () => {
|
|
6427
|
+
const snapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
6428
|
+
const config = snapshot.parseError ? {} : snapshot.parsed;
|
|
6429
|
+
sendJson(res, 200, {
|
|
6430
|
+
builtin: BUILTIN_AGENT_INSTRUCTIONS,
|
|
6431
|
+
custom: typeof config.agentInstructions === 'string' ? config.agentInstructions : '',
|
|
6432
|
+
builtinEnabled: config.agentInstructionsBuiltinEnabled !== false,
|
|
6433
|
+
customEnabled: config.agentInstructionsCustomEnabled !== false,
|
|
6434
|
+
// 新建容器时系统提示词的初始内容
|
|
6435
|
+
composed: composeSystemPrompt(config, '')
|
|
6436
|
+
});
|
|
6437
|
+
}
|
|
6438
|
+
},
|
|
6439
|
+
{
|
|
6440
|
+
method: 'POST',
|
|
6441
|
+
match: currentPath => currentPath === '/api/agent-instructions' ? [] : null,
|
|
6442
|
+
handler: async () => {
|
|
6443
|
+
const payload = toPlainObject(await readJsonBody(req));
|
|
6444
|
+
const toggles = [['builtinEnabled', 'agentInstructionsBuiltinEnabled'], ['customEnabled', 'agentInstructionsCustomEnabled']];
|
|
6445
|
+
if (payload.custom !== undefined && typeof payload.custom !== 'string') {
|
|
6446
|
+
sendJson(res, 400, { error: 'custom 必须是字符串' });
|
|
6447
|
+
return;
|
|
6448
|
+
}
|
|
6449
|
+
if (toggles.some(([field]) => payload[field] !== undefined && typeof payload[field] !== 'boolean')) {
|
|
6450
|
+
sendJson(res, 400, { error: '开关必须是布尔值' });
|
|
6451
|
+
return;
|
|
6452
|
+
}
|
|
6453
|
+
const snapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
6454
|
+
if (snapshot.parseError) {
|
|
6455
|
+
sendJson(res, 400, { error: '当前配置解析失败,无法继续', detail: snapshot.parseError });
|
|
6456
|
+
return;
|
|
6457
|
+
}
|
|
6458
|
+
let nextRaw = baseRawOf(snapshot);
|
|
6459
|
+
let changed = false;
|
|
6460
|
+
try {
|
|
6461
|
+
// 没写过、也没填内容时不动配置文件;开关保持默认(开)且没写过时同理
|
|
6462
|
+
if (typeof payload.custom === 'string' && (payload.custom.trim() || hasOwn(snapshot.parsed, 'agentInstructions'))) {
|
|
6463
|
+
nextRaw = buildAgentInstructionsConfigRaw(nextRaw, payload.custom.slice(0, 4000));
|
|
6464
|
+
changed = true;
|
|
6465
|
+
}
|
|
6466
|
+
toggles.forEach(([field, key]) => {
|
|
6467
|
+
if (typeof payload[field] === 'boolean' && (payload[field] === false || hasOwn(snapshot.parsed, key))) {
|
|
6468
|
+
nextRaw = buildBooleanConfigRaw(nextRaw, key, payload[field]);
|
|
6469
|
+
changed = true;
|
|
6470
|
+
}
|
|
6471
|
+
});
|
|
6472
|
+
if (changed) parseAndValidateConfigRaw(nextRaw);
|
|
6473
|
+
} catch (e) {
|
|
6474
|
+
sendJson(res, 400, { error: '配置写入失败' });
|
|
6475
|
+
return;
|
|
6476
|
+
}
|
|
6477
|
+
if (changed) writeConfigFileSecure(path.resolve(state.webConfigPath), nextRaw);
|
|
6478
|
+
sendJson(res, 200, { saved: changed });
|
|
6479
|
+
}
|
|
6480
|
+
},
|
|
6226
6481
|
{
|
|
6227
6482
|
method: 'GET',
|
|
6228
6483
|
match: currentPath => currentPath === '/api/config' ? [] : null,
|
|
@@ -6727,6 +6982,26 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
6727
6982
|
sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), remark });
|
|
6728
6983
|
}
|
|
6729
6984
|
},
|
|
6985
|
+
{
|
|
6986
|
+
method: 'POST',
|
|
6987
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent-pin$/),
|
|
6988
|
+
handler: async match => {
|
|
6989
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
6990
|
+
if (!sessionRef) {
|
|
6991
|
+
return;
|
|
6992
|
+
}
|
|
6993
|
+
const payload = await readJsonBody(req);
|
|
6994
|
+
const pinned = payload.pinned === true;
|
|
6995
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
6996
|
+
// 默认 AGENT 在还没有任何消息时也会出现在列表里(历史里还没有它的记录),允许置顶
|
|
6997
|
+
if (sessionRef.agentId !== WEB_DEFAULT_AGENT_ID && !getWebAgentSession(history, sessionRef.agentId, { includeArchived: true })) {
|
|
6998
|
+
sendJson(res, 404, { error: 'AGENT 不存在' });
|
|
6999
|
+
return;
|
|
7000
|
+
}
|
|
7001
|
+
patchWebAgentSessionState(state.webHistoryDir, sessionRef, { pinned });
|
|
7002
|
+
sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), pinned });
|
|
7003
|
+
}
|
|
7004
|
+
},
|
|
6730
7005
|
{
|
|
6731
7006
|
method: 'GET',
|
|
6732
7007
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/messages$/),
|
|
@@ -6806,12 +7081,17 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
6806
7081
|
}
|
|
6807
7082
|
const requestUrl = new URL(req.url || '/api/sessions/x/fs/list', 'http://localhost');
|
|
6808
7083
|
const targetPath = String(requestUrl.searchParams.get('path') || '/').trim() || '/';
|
|
7084
|
+
const limitParam = Number.parseInt(requestUrl.searchParams.get('limit') || '', 10);
|
|
7085
|
+
const listOptions = {
|
|
7086
|
+
limit: Number.isInteger(limitParam) && limitParam > 0 ? Math.min(limitParam, WEB_FS_LIST_MAX_LIMIT) : 0,
|
|
7087
|
+
prefix: String(requestUrl.searchParams.get('prefix') || '').slice(0, 255)
|
|
7088
|
+
};
|
|
6809
7089
|
|
|
6810
7090
|
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
6811
7091
|
const payload = await execJsonCommandInWebContainer(
|
|
6812
7092
|
ctx,
|
|
6813
7093
|
sessionRef.containerName,
|
|
6814
|
-
buildContainerFileListCommand(targetPath)
|
|
7094
|
+
buildContainerFileListCommand(targetPath, listOptions)
|
|
6815
7095
|
);
|
|
6816
7096
|
if (payload && payload.error) {
|
|
6817
7097
|
sendJson(res, 400, { error: payload.error });
|
|
@@ -7040,6 +7320,68 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
7040
7320
|
sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), detail }, {}, { autoEtag: true });
|
|
7041
7321
|
}
|
|
7042
7322
|
},
|
|
7323
|
+
{
|
|
7324
|
+
// “改动”视图:工作目录所在 git 仓库的改动列表(按需请求,前端不轮询)
|
|
7325
|
+
method: 'GET',
|
|
7326
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/git\/changes$/),
|
|
7327
|
+
handler: async match => {
|
|
7328
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
7329
|
+
if (!sessionRef) {
|
|
7330
|
+
return;
|
|
7331
|
+
}
|
|
7332
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
7333
|
+
const changes = await readWebGitChanges(ctx, state, sessionRef);
|
|
7334
|
+
sendJson(res, 200, changes);
|
|
7335
|
+
}
|
|
7336
|
+
},
|
|
7337
|
+
{
|
|
7338
|
+
method: 'GET',
|
|
7339
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/git\/diff$/),
|
|
7340
|
+
handler: async match => {
|
|
7341
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
7342
|
+
if (!sessionRef) {
|
|
7343
|
+
return;
|
|
7344
|
+
}
|
|
7345
|
+
const requestUrl = new URL(req.url || '/', 'http://localhost');
|
|
7346
|
+
const filePath = String(requestUrl.searchParams.get('path') || '');
|
|
7347
|
+
if (!gitChanges.isAcceptableDiffPath(filePath)) {
|
|
7348
|
+
sendJson(res, 400, { error: '文件路径不合法' });
|
|
7349
|
+
return;
|
|
7350
|
+
}
|
|
7351
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
7352
|
+
// 只给改动列表里出现过的文件看 diff:路径由服务端重新取一次核对,不信任请求
|
|
7353
|
+
const changes = await readWebGitChanges(ctx, state, sessionRef);
|
|
7354
|
+
const entry = changes.files.find(file => file.path === filePath);
|
|
7355
|
+
if (!entry || entry.status === 'untracked') {
|
|
7356
|
+
sendJson(res, 404, { error: '该文件不在改动列表里' });
|
|
7357
|
+
return;
|
|
7358
|
+
}
|
|
7359
|
+
const result = await execCommandInWebContainer(
|
|
7360
|
+
ctx,
|
|
7361
|
+
sessionRef.containerName,
|
|
7362
|
+
gitChanges.buildGitDiffCommand(resolveWebSessionWorkDir(state, sessionRef), filePath),
|
|
7363
|
+
{ withEnv: false }
|
|
7364
|
+
);
|
|
7365
|
+
sendJson(res, 200, { path: filePath, ...gitChanges.parseGitDiffOutput(result.stdout) });
|
|
7366
|
+
}
|
|
7367
|
+
},
|
|
7368
|
+
{
|
|
7369
|
+
// 输入框里打 / 时的候选:只列实测能在 Web 端执行的(见 lib/web/session-meta.js)
|
|
7370
|
+
method: 'GET',
|
|
7371
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/slash-commands$/),
|
|
7372
|
+
handler: async match => {
|
|
7373
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
7374
|
+
if (!sessionRef) {
|
|
7375
|
+
return;
|
|
7376
|
+
}
|
|
7377
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
7378
|
+
const agentSession = getWebAgentSession(history, sessionRef.agentId, { includeArchived: true });
|
|
7379
|
+
sendJson(res, 200, {
|
|
7380
|
+
name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
|
|
7381
|
+
commands: sessionMeta.listUsableSlashCommands(agentSession && agentSession.slashCommands)
|
|
7382
|
+
});
|
|
7383
|
+
}
|
|
7384
|
+
},
|
|
7043
7385
|
{
|
|
7044
7386
|
method: 'GET',
|
|
7045
7387
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/models$/),
|
|
@@ -7279,6 +7621,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
7279
7621
|
resumeError,
|
|
7280
7622
|
engineSessionId
|
|
7281
7623
|
}, result);
|
|
7624
|
+
void refreshWebAgentTitle(ctx, state, sessionRef, agentMeta.agentProgram);
|
|
7282
7625
|
sendJson(res, 200, {
|
|
7283
7626
|
exitCode: result.exitCode,
|
|
7284
7627
|
output: result.output,
|
|
@@ -7433,6 +7776,39 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
7433
7776
|
sendJson(res, 200, { ok: true, stopping: true });
|
|
7434
7777
|
}
|
|
7435
7778
|
},
|
|
7779
|
+
{
|
|
7780
|
+
// 容器菜单里的“启动容器”:与对话前的拉起同一条路径(含网络规则下发)
|
|
7781
|
+
method: 'POST',
|
|
7782
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/container\/start$/),
|
|
7783
|
+
handler: async match => {
|
|
7784
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
7785
|
+
if (!sessionRef) return;
|
|
7786
|
+
if (!ctx.containerExists(sessionRef.containerName)) {
|
|
7787
|
+
sendJson(res, 404, { error: `容器不存在: ${sessionRef.containerName}` });
|
|
7788
|
+
return;
|
|
7789
|
+
}
|
|
7790
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
7791
|
+
sendJson(res, 200, { ok: true });
|
|
7792
|
+
}
|
|
7793
|
+
},
|
|
7794
|
+
{
|
|
7795
|
+
method: 'POST',
|
|
7796
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/container\/stop$/),
|
|
7797
|
+
handler: async match => {
|
|
7798
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
7799
|
+
if (!sessionRef) return;
|
|
7800
|
+
if (!ctx.containerExists(sessionRef.containerName)) {
|
|
7801
|
+
sendJson(res, 404, { error: `容器不存在: ${sessionRef.containerName}` });
|
|
7802
|
+
return;
|
|
7803
|
+
}
|
|
7804
|
+
if (state.agentRuns && state.agentRuns.has(sessionRef.containerName)) {
|
|
7805
|
+
sendJson(res, 409, { error: '容器里有 AGENT 正在执行,请先停止任务' });
|
|
7806
|
+
return;
|
|
7807
|
+
}
|
|
7808
|
+
await ctx.dockerExecAsync(['stop', '-t', '10', sessionRef.containerName], { timeout: 60000 });
|
|
7809
|
+
sendJson(res, 200, { ok: true });
|
|
7810
|
+
}
|
|
7811
|
+
},
|
|
7436
7812
|
{
|
|
7437
7813
|
method: 'POST',
|
|
7438
7814
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove$/),
|
|
@@ -7512,7 +7888,11 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
7512
7888
|
state.containerNames.set(id, name);
|
|
7513
7889
|
},
|
|
7514
7890
|
ensureWebContainer,
|
|
7515
|
-
execCommandInWebContainer
|
|
7891
|
+
execCommandInWebContainer,
|
|
7892
|
+
readConfig: () => {
|
|
7893
|
+
const snapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
7894
|
+
return snapshot.parseError ? {} : snapshot.parsed;
|
|
7895
|
+
}
|
|
7516
7896
|
}
|
|
7517
7897
|
}));
|
|
7518
7898
|
|
|
@@ -7883,6 +8263,8 @@ module.exports = {
|
|
|
7883
8263
|
__testing: {
|
|
7884
8264
|
prepareStructuredTraceEvents,
|
|
7885
8265
|
buildClaudeAgentExecCommand,
|
|
8266
|
+
buildWebAgentExecCommand,
|
|
8267
|
+
buildContainerFileListCommand,
|
|
7886
8268
|
extractContentDeltaFromPayload,
|
|
7887
8269
|
extractAgentMessageFromStructuredOutput,
|
|
7888
8270
|
appendWebSessionMessage,
|