@xcanwin/manyoyo 8.6.7 → 8.6.9
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 +1 -0
- package/lib/agent-instructions.js +50 -0
- package/lib/plugin/playwright.js +4 -2
- package/lib/setup-config.js +6 -1
- package/lib/web/AGENTS.md +5 -0
- package/lib/web/git-changes.js +130 -0
- package/lib/web/index.html +86 -80
- package/lib/web/server.js +383 -28
- package/lib/web/session-meta.js +117 -0
- package/manyoyo.example.json +2 -0
- package/package.json +2 -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, 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 } = 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) {
|
|
@@ -835,6 +863,44 @@ function stripAnsi(text) {
|
|
|
835
863
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
836
864
|
}
|
|
837
865
|
|
|
866
|
+
// Agent 原始输出只保留开头 AGENT_RAW_OUTPUT_MAX_CHARS(会话 id 在开头),另外保留一份结尾:
|
|
867
|
+
// 最终回复和用量都在输出末尾,超过上限后只看开头会把截断点之前的某条中间说明当成最终回复
|
|
868
|
+
const AGENT_RAW_OUTPUT_MAX_CHARS = 32 * 1024 * 1024;
|
|
869
|
+
const AGENT_OUTPUT_TAIL_MAX_CHARS = 8 * 1024 * 1024;
|
|
870
|
+
// 最终回复的上限:远大于正常回复,只挡住把整个文件当回复打出来的极端情况
|
|
871
|
+
const AGENT_FINAL_REPLY_MAX_CHARS = 1000000;
|
|
872
|
+
|
|
873
|
+
function createTailBuffer(maxChars) {
|
|
874
|
+
let parts = [];
|
|
875
|
+
let size = 0;
|
|
876
|
+
return {
|
|
877
|
+
push(text) {
|
|
878
|
+
if (!text) return;
|
|
879
|
+
parts.push(text);
|
|
880
|
+
size += text.length;
|
|
881
|
+
if (size > maxChars * 2) {
|
|
882
|
+
const joined = parts.join('').slice(-maxChars);
|
|
883
|
+
parts = [joined];
|
|
884
|
+
size = joined.length;
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
text() {
|
|
888
|
+
return parts.join('').slice(-maxChars);
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// 结束后的回复文本:结构化输出里提取到的最终回复按 AGENT_FINAL_REPLY_MAX_CHARS(远大于正常回复)保留;
|
|
894
|
+
// 提取不到才退回原始输出,原始输出按 WEB_OUTPUT_MAX_CHARS 截断
|
|
895
|
+
function resolveAgentFinalOutput(agentProgram, { stdout, stdoutTruncated, stdoutTail, raw }) {
|
|
896
|
+
const extracted = (stdoutTruncated ? extractAgentMessageFromStructuredOutput(agentProgram, stdoutTail) : '')
|
|
897
|
+
|| extractAgentMessageFromStructuredOutput(agentProgram, stdout);
|
|
898
|
+
if (extracted) {
|
|
899
|
+
return clipText(stripAnsi(extracted).trim(), AGENT_FINAL_REPLY_MAX_CHARS);
|
|
900
|
+
}
|
|
901
|
+
return clipText(stripAnsi(raw).trim() || '(无输出)');
|
|
902
|
+
}
|
|
903
|
+
|
|
838
904
|
function clipText(text, maxChars = WEB_OUTPUT_MAX_CHARS) {
|
|
839
905
|
if (typeof text !== 'string') return '';
|
|
840
906
|
if (text.length <= maxChars) return text;
|
|
@@ -873,7 +939,8 @@ function quoteBashSingleValue(value) {
|
|
|
873
939
|
function renderAgentPromptCommand(template, prompt) {
|
|
874
940
|
const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
875
941
|
const safePrompt = quoteBashSingleValue(prompt);
|
|
876
|
-
|
|
942
|
+
// 用函数返回替换值:字符串形式会把提示词里的 $' $` $& 当成替换模式
|
|
943
|
+
return templateText.replace(/\{prompt\}/g, () => safePrompt);
|
|
877
944
|
}
|
|
878
945
|
|
|
879
946
|
function buildCodexAgentExecCommand(template, prompt, options = {}) {
|
|
@@ -891,12 +958,17 @@ function buildCodexAgentExecCommand(template, prompt, options = {}) {
|
|
|
891
958
|
const injectedFlags = hasJson ? '' : ' --json';
|
|
892
959
|
const hasModel = /(?:^|\s)(?:-m|--model)(?:\s|$)/.test(suffix);
|
|
893
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
|
+
: '';
|
|
894
966
|
if (sessionId) {
|
|
895
967
|
const promptIndex = suffix.indexOf('{prompt}');
|
|
896
968
|
const resumeSuffix = `${suffix.slice(0, promptIndex)}${quoteBashSingleValue(sessionId)} ${suffix.slice(promptIndex)}`;
|
|
897
|
-
codexTemplate = `${prefix}codex exec resume${injectedFlags}${modelFlag}${resumeSuffix}`;
|
|
969
|
+
codexTemplate = `${prefix}codex exec resume${injectedFlags}${modelFlag}${instructionsFlag}${resumeSuffix}`;
|
|
898
970
|
} else {
|
|
899
|
-
codexTemplate = `${prefix}codex exec${injectedFlags}${modelFlag}${suffix}`;
|
|
971
|
+
codexTemplate = `${prefix}codex exec${injectedFlags}${modelFlag}${instructionsFlag}${suffix}`;
|
|
900
972
|
}
|
|
901
973
|
}
|
|
902
974
|
return codexTemplate === templateText
|
|
@@ -910,10 +982,12 @@ function prependAgentFlags(commandText, matchPattern, flagSpecs) {
|
|
|
910
982
|
return String(commandText || '');
|
|
911
983
|
}
|
|
912
984
|
const prefix = matched[1] || '';
|
|
913
|
-
|
|
985
|
+
// “模板里是否已经写了这个参数”只看原模板:插进去的参数值(比如环境说明正文里的 “-r”)不能让后面的参数被跳过
|
|
986
|
+
const original = matched[matched.length - 1] || '';
|
|
987
|
+
let suffix = original;
|
|
914
988
|
for (let i = flagSpecs.length - 1; i >= 0; i -= 1) {
|
|
915
989
|
const spec = flagSpecs[i];
|
|
916
|
-
if (!spec || !spec.flag || !(spec.pattern instanceof RegExp) || spec.pattern.test(
|
|
990
|
+
if (!spec || !spec.flag || !(spec.pattern instanceof RegExp) || spec.pattern.test(original)) {
|
|
917
991
|
continue;
|
|
918
992
|
}
|
|
919
993
|
suffix = ` ${spec.flag}${suffix}`;
|
|
@@ -940,6 +1014,14 @@ function buildClaudeAgentExecCommand(template, prompt, options = {}) {
|
|
|
940
1014
|
if (model) {
|
|
941
1015
|
flagSpecs.push({ flag: `--model ${quoteBashSingleValue(model)}`, pattern: /(?:^|\s)--model(?:\s|$)/ });
|
|
942
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
|
+
}
|
|
943
1025
|
// 子 Agent 说的话和思考默认不转发(v2.1.211+ 才有 --forward-subagent-text,旧版本不认这个参数会直接报错)。
|
|
944
1026
|
// 'auto':在容器里随命令探测(claude --help 约 50ms),支持就展开成这个参数、不支持展开成空;true:直接加。
|
|
945
1027
|
// 'auto' 的参数放在整条命令末尾,不改变前面已有参数的相对顺序
|
|
@@ -997,14 +1079,20 @@ function buildOpenCodeAgentExecCommand(template, prompt, options = {}) {
|
|
|
997
1079
|
if (model) {
|
|
998
1080
|
flagSpecs.push({ flag: `--model ${quoteBashSingleValue(model)}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
|
|
999
1081
|
}
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
flagSpecs
|
|
1004
|
-
);
|
|
1005
|
-
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
|
|
1006
1085
|
? renderAgentPromptCommand(templateText, prompt)
|
|
1007
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}`;
|
|
1008
1096
|
}
|
|
1009
1097
|
|
|
1010
1098
|
function buildWebAgentExecCommand(template, prompt, agentProgram, options = {}) {
|
|
@@ -1062,6 +1150,11 @@ function extractClaudeAgentMessage(text) {
|
|
|
1062
1150
|
let lastMessage = '';
|
|
1063
1151
|
for (const rawLine of String(text || '').split('\n')) {
|
|
1064
1152
|
const payload = parseJsonObjectLine(rawLine);
|
|
1153
|
+
// result 事件带权威的最终文本;后台任务场景结尾有两个 result(第一个是中间结论),取最后一个
|
|
1154
|
+
if (payload && payload.type === 'result' && typeof payload.result === 'string' && payload.result.trim()) {
|
|
1155
|
+
lastMessage = payload.result.trim();
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1065
1158
|
if (!payload || payload.type !== 'assistant' || pickFirstString(payload.parent_tool_use_id)) {
|
|
1066
1159
|
continue;
|
|
1067
1160
|
}
|
|
@@ -1171,6 +1264,23 @@ function extractClaudeSessionId(text) {
|
|
|
1171
1264
|
return '';
|
|
1172
1265
|
}
|
|
1173
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
|
+
|
|
1174
1284
|
function extractOpenCodeSessionId(text) {
|
|
1175
1285
|
for (const rawLine of String(text || '').split('\n')) {
|
|
1176
1286
|
const payload = parseJsonObjectLine(rawLine);
|
|
@@ -2270,10 +2380,14 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
|
|
|
2270
2380
|
const effectivePrompt = resumeSucceeded
|
|
2271
2381
|
? prompt
|
|
2272
2382
|
: buildAgentPromptWithHistory(agentSession, prompt);
|
|
2383
|
+
// 环境说明每次执行时现读配置:改了设置不用重建容器;运行配置名记在创建时的快照里(旧会话没有,用全局配置)
|
|
2384
|
+
const configSnapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
2385
|
+
const runName = history.runtimeSnapshot && typeof history.runtimeSnapshot.run === 'string' ? history.runtimeSnapshot.run : '';
|
|
2273
2386
|
const command = buildWebAgentExecCommand(effectiveTemplate, effectivePrompt, agentMeta.agentProgram, {
|
|
2274
2387
|
sessionId: sessionIdForRun,
|
|
2275
2388
|
model: agentSession.model || '',
|
|
2276
|
-
forwardSubagentText: agentMeta.agentProgram === 'claude' ? 'auto' : false
|
|
2389
|
+
forwardSubagentText: agentMeta.agentProgram === 'claude' ? 'auto' : false,
|
|
2390
|
+
instructions: buildAgentInstructions(configSnapshot.parseError ? {} : configSnapshot.parsed, runName)
|
|
2277
2391
|
});
|
|
2278
2392
|
const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
|
|
2279
2393
|
|
|
@@ -2291,7 +2405,8 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
|
|
|
2291
2405
|
}
|
|
2292
2406
|
|
|
2293
2407
|
function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, meta, result) {
|
|
2294
|
-
|
|
2408
|
+
// 用量在输出末尾:原始输出超过上限时开头里没有它
|
|
2409
|
+
const turnUsage = extractTurnUsage(agentMeta.agentProgram, result.stdoutTail || result.stdout);
|
|
2295
2410
|
const replyMessage = appendWebSessionMessage(state.webHistoryDir, sessionRef, 'assistant', result.output, {
|
|
2296
2411
|
exitCode: result.exitCode,
|
|
2297
2412
|
mode: 'agent',
|
|
@@ -2313,10 +2428,66 @@ function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, m
|
|
|
2313
2428
|
if (turnUsage) {
|
|
2314
2429
|
patch.usageTotal = accumulateUsageTotal(agentSession.usageTotal, turnUsage);
|
|
2315
2430
|
}
|
|
2431
|
+
if (agentMeta.agentProgram === 'claude') {
|
|
2432
|
+
const slashCommands = extractClaudeInitSlashCommands(result.stdout);
|
|
2433
|
+
if (slashCommands) {
|
|
2434
|
+
patch.slashCommands = slashCommands;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2316
2437
|
patchWebAgentSessionState(state.webHistoryDir, sessionRef, patch);
|
|
2317
2438
|
return replyMessage;
|
|
2318
2439
|
}
|
|
2319
2440
|
|
|
2441
|
+
function resolveWebSessionWorkDir(state, sessionRef) {
|
|
2442
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
2443
|
+
const applied = history.applied && typeof history.applied === 'object' ? history.applied : {};
|
|
2444
|
+
return typeof applied.containerPath === 'string' && applied.containerPath ? applied.containerPath : '/';
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
async function readWebGitChanges(ctx, state, sessionRef) {
|
|
2448
|
+
const result = await execCommandInWebContainer(
|
|
2449
|
+
ctx,
|
|
2450
|
+
sessionRef.containerName,
|
|
2451
|
+
gitChanges.buildGitChangesCommand(resolveWebSessionWorkDir(state, sessionRef)),
|
|
2452
|
+
{ withEnv: false }
|
|
2453
|
+
);
|
|
2454
|
+
return gitChanges.parseGitChangesOutput(result.stdout);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// 每轮结束后在后台读一次 Agent 自己生成的标题(目前只有 claude 会写 ai-title,交互模式实测;
|
|
2458
|
+
// 其他 Agent 只用首条消息)。不阻塞本轮结果;读不到或失败都静默——每轮都会触发,属于高频路径,不写日志
|
|
2459
|
+
async function refreshWebAgentTitle(ctx, state, sessionRef, agentProgram) {
|
|
2460
|
+
if (agentProgram !== 'claude') {
|
|
2461
|
+
return '';
|
|
2462
|
+
}
|
|
2463
|
+
try {
|
|
2464
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
2465
|
+
const agentSession = getWebAgentSession(history, sessionRef.agentId, { includeArchived: true });
|
|
2466
|
+
const command = sessionMeta.buildClaudeTitleReadCommand(agentSession && agentSession.engineSessionId);
|
|
2467
|
+
if (!command) {
|
|
2468
|
+
return '';
|
|
2469
|
+
}
|
|
2470
|
+
const sessionId = agentSession.engineSessionId;
|
|
2471
|
+
const result = await execCommandInWebContainer(ctx, sessionRef.containerName, command);
|
|
2472
|
+
const title = sessionMeta.parseClaudeTitleOutput(result.stdout);
|
|
2473
|
+
// 读标题期间 AGENT 可能被删除或换了引擎会话:写回前重新读一次,不在了就放弃(不能把它重新建出来)
|
|
2474
|
+
const fresh = getWebAgentSession(
|
|
2475
|
+
loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName),
|
|
2476
|
+
sessionRef.agentId,
|
|
2477
|
+
{ includeArchived: true }
|
|
2478
|
+
);
|
|
2479
|
+
if (!fresh || fresh.engineSessionId !== sessionId) {
|
|
2480
|
+
return '';
|
|
2481
|
+
}
|
|
2482
|
+
if (title && title !== fresh.title) {
|
|
2483
|
+
patchWebAgentSessionState(state.webHistoryDir, sessionRef, { title });
|
|
2484
|
+
}
|
|
2485
|
+
return title;
|
|
2486
|
+
} catch (e) {
|
|
2487
|
+
return '';
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2320
2491
|
function appendWebAgentTraceMessage(webHistoryDir, sessionRefOrContainerName, content, extra = {}) {
|
|
2321
2492
|
const text = String(content || '').trim();
|
|
2322
2493
|
if (!text) {
|
|
@@ -2797,6 +2968,9 @@ function validateWebConfigShape(configObject) {
|
|
|
2797
2968
|
if (hasOwn(config, 'agentPromptCommand')) {
|
|
2798
2969
|
normalizeAgentPromptCommandTemplate(config.agentPromptCommand, 'agentPromptCommand');
|
|
2799
2970
|
}
|
|
2971
|
+
if (hasOwn(config, 'agentInstructions') && typeof config.agentInstructions !== 'string') {
|
|
2972
|
+
throw new Error('agentInstructions 必须是字符串');
|
|
2973
|
+
}
|
|
2800
2974
|
if (hasOwn(config, 'runs')) {
|
|
2801
2975
|
const runs = config.runs;
|
|
2802
2976
|
if (runs !== undefined && (typeof runs !== 'object' || runs === null || Array.isArray(runs))) {
|
|
@@ -2807,6 +2981,9 @@ function validateWebConfigShape(configObject) {
|
|
|
2807
2981
|
if (hasOwn(normalizedRun, 'agentPromptCommand')) {
|
|
2808
2982
|
normalizeAgentPromptCommandTemplate(normalizedRun.agentPromptCommand, `runs.${runName}.agentPromptCommand`);
|
|
2809
2983
|
}
|
|
2984
|
+
if (hasOwn(normalizedRun, 'agentInstructions') && typeof normalizedRun.agentInstructions !== 'string') {
|
|
2985
|
+
throw new Error(`runs.${runName}.agentInstructions 必须是字符串`);
|
|
2986
|
+
}
|
|
2810
2987
|
});
|
|
2811
2988
|
}
|
|
2812
2989
|
}
|
|
@@ -3141,6 +3318,7 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
3141
3318
|
// 仅供服务端内部复用(如"创建相同配置容器"/"复制容器"),绝不能透传给任何对外响应字段,
|
|
3142
3319
|
// 因为 env/volumes/ports 可能包含密钥原始值(对外响应不暴露它们)。
|
|
3143
3320
|
runtimeSnapshot: {
|
|
3321
|
+
run: runName || '',
|
|
3144
3322
|
hostPath,
|
|
3145
3323
|
containerPath,
|
|
3146
3324
|
imageName,
|
|
@@ -3778,7 +3956,8 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
3778
3956
|
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
3779
3957
|
);
|
|
3780
3958
|
|
|
3781
|
-
const MAX_RAW_OUTPUT_CHARS =
|
|
3959
|
+
const MAX_RAW_OUTPUT_CHARS = ctx.agentRawOutputMaxChars || AGENT_RAW_OUTPUT_MAX_CHARS;
|
|
3960
|
+
const stdoutTail = createTailBuffer(Math.min(MAX_RAW_OUTPUT_CHARS, AGENT_OUTPUT_TAIL_MAX_CHARS));
|
|
3782
3961
|
let stdoutOutput = '';
|
|
3783
3962
|
let stderrOutput = '';
|
|
3784
3963
|
let stdoutTruncated = false;
|
|
@@ -3788,6 +3967,7 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
3788
3967
|
if (!chunk) return;
|
|
3789
3968
|
const text = chunk.toString('utf-8');
|
|
3790
3969
|
if (!text) return;
|
|
3970
|
+
if (target.tail) target.tail.push(text);
|
|
3791
3971
|
if (target.value.length >= MAX_RAW_OUTPUT_CHARS) {
|
|
3792
3972
|
target.truncated = true;
|
|
3793
3973
|
return;
|
|
@@ -3801,7 +3981,11 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
3801
3981
|
target.value += text;
|
|
3802
3982
|
}
|
|
3803
3983
|
|
|
3984
|
+
// 按流解码:逐块 toString 会把跨块的多字节字符(中文)拆成乱码
|
|
3985
|
+
process.stdout.setEncoding('utf8');
|
|
3986
|
+
process.stderr.setEncoding('utf8');
|
|
3804
3987
|
process.stdout.on('data', chunk => appendChunk(chunk, {
|
|
3988
|
+
tail: stdoutTail,
|
|
3805
3989
|
get value() { return stdoutOutput; },
|
|
3806
3990
|
set value(nextValue) { stdoutOutput = nextValue; },
|
|
3807
3991
|
get truncated() { return stdoutTruncated; },
|
|
@@ -3820,13 +4004,14 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
3820
4004
|
const clippedStdout = stdoutTruncated ? `${stdoutOutput}\n...[stdout-truncated]` : stdoutOutput;
|
|
3821
4005
|
const clippedStderr = stderrTruncated ? `${stderrOutput}\n...[stderr-truncated]` : stderrOutput;
|
|
3822
4006
|
const clippedRaw = `${clippedStdout}${clippedStdout && clippedStderr ? '\n' : ''}${clippedStderr}`;
|
|
3823
|
-
|
|
3824
|
-
const
|
|
3825
|
-
const output =
|
|
4007
|
+
// 结尾只在开头被截断时才需要(拼接最多 16MB,只算一次)
|
|
4008
|
+
const tailText = stdoutTruncated ? stdoutTail.text() : '';
|
|
4009
|
+
const output = resolveAgentFinalOutput(agentProgram, { stdout: clippedStdout, stdoutTruncated, stdoutTail: tailText, raw: clippedRaw });
|
|
3826
4010
|
resolve({
|
|
3827
4011
|
exitCode,
|
|
3828
4012
|
output,
|
|
3829
4013
|
stdout: clippedStdout,
|
|
4014
|
+
...(stdoutTruncated ? { stdoutTail: tailText } : {}),
|
|
3830
4015
|
stderr: clippedStderr
|
|
3831
4016
|
});
|
|
3832
4017
|
});
|
|
@@ -3885,13 +4070,19 @@ async function execJsonCommandInWebContainer(ctx, containerName, command) {
|
|
|
3885
4070
|
}
|
|
3886
4071
|
}
|
|
3887
4072
|
|
|
3888
|
-
|
|
4073
|
+
// options.limit / options.prefix 供输入框 @ 补全用:先按名字前缀过滤、按“目录在前 + 名字”排序后只取前 limit 项,
|
|
4074
|
+
// 只对这些项做 lstat(大目录里全量 lstat 才是慢的部分)
|
|
4075
|
+
function buildContainerFileListCommand(requestedPath, options = {}) {
|
|
4076
|
+
const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 0;
|
|
4077
|
+
const prefix = typeof options.prefix === 'string' ? options.prefix.toLowerCase() : '';
|
|
3889
4078
|
return buildWebContainerNodeCommand(`
|
|
3890
4079
|
// __MANYOYO_FS_LIST__
|
|
3891
4080
|
const fs = require('fs');
|
|
3892
4081
|
const path = require('path');
|
|
3893
4082
|
|
|
3894
4083
|
const requestedPath = ${JSON.stringify(String(requestedPath || '/'))};
|
|
4084
|
+
const limit = ${JSON.stringify(limit)};
|
|
4085
|
+
const prefix = ${JSON.stringify(prefix)};
|
|
3895
4086
|
|
|
3896
4087
|
try {
|
|
3897
4088
|
const realPath = fs.realpathSync(requestedPath);
|
|
@@ -3902,7 +4093,20 @@ try {
|
|
|
3902
4093
|
|
|
3903
4094
|
const root = path.parse(realPath).root;
|
|
3904
4095
|
const parentPath = realPath === root ? '' : path.dirname(realPath);
|
|
3905
|
-
|
|
4096
|
+
let dirents = fs.readdirSync(realPath, { withFileTypes: true });
|
|
4097
|
+
if (prefix) {
|
|
4098
|
+
dirents = dirents.filter(entry => entry.name.toLowerCase().startsWith(prefix));
|
|
4099
|
+
}
|
|
4100
|
+
let truncated = false;
|
|
4101
|
+
if (limit > 0 && dirents.length > limit) {
|
|
4102
|
+
dirents.sort((a, b) => {
|
|
4103
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
4104
|
+
return a.name.localeCompare(b.name, 'zh-CN');
|
|
4105
|
+
});
|
|
4106
|
+
dirents = dirents.slice(0, limit);
|
|
4107
|
+
truncated = true;
|
|
4108
|
+
}
|
|
4109
|
+
const entries = dirents
|
|
3906
4110
|
.map(entry => {
|
|
3907
4111
|
const fullPath = path.join(realPath, entry.name);
|
|
3908
4112
|
let itemStat = null;
|
|
@@ -3951,7 +4155,8 @@ try {
|
|
|
3951
4155
|
process.stdout.write(JSON.stringify({
|
|
3952
4156
|
path: realPath,
|
|
3953
4157
|
parentPath,
|
|
3954
|
-
entries
|
|
4158
|
+
entries,
|
|
4159
|
+
...(truncated ? { truncated: true } : {})
|
|
3955
4160
|
}));
|
|
3956
4161
|
} catch (e) {
|
|
3957
4162
|
process.stdout.write(JSON.stringify({
|
|
@@ -4230,7 +4435,8 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
|
|
|
4230
4435
|
}
|
|
4231
4436
|
|
|
4232
4437
|
return await new Promise((resolve, reject) => {
|
|
4233
|
-
const MAX_RAW_OUTPUT_CHARS =
|
|
4438
|
+
const MAX_RAW_OUTPUT_CHARS = ctx.agentRawOutputMaxChars || AGENT_RAW_OUTPUT_MAX_CHARS;
|
|
4439
|
+
const stdoutTail = createTailBuffer(Math.min(MAX_RAW_OUTPUT_CHARS, AGENT_OUTPUT_TAIL_MAX_CHARS));
|
|
4234
4440
|
let stdoutOutput = '';
|
|
4235
4441
|
let stderrOutput = '';
|
|
4236
4442
|
let stdoutTruncated = false;
|
|
@@ -4245,6 +4451,7 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
|
|
|
4245
4451
|
if (!chunk) return;
|
|
4246
4452
|
const text = chunk.toString('utf-8');
|
|
4247
4453
|
if (!text) return;
|
|
4454
|
+
if (target.tail) target.tail.push(text);
|
|
4248
4455
|
if (target.value.length >= MAX_RAW_OUTPUT_CHARS) {
|
|
4249
4456
|
target.truncated = true;
|
|
4250
4457
|
return;
|
|
@@ -4325,8 +4532,12 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
|
|
|
4325
4532
|
return pending;
|
|
4326
4533
|
}
|
|
4327
4534
|
|
|
4535
|
+
// 按流解码:逐块 toString 会把跨块的多字节字符(中文)拆成乱码
|
|
4536
|
+
process.stdout.setEncoding('utf8');
|
|
4537
|
+
process.stderr.setEncoding('utf8');
|
|
4328
4538
|
process.stdout.on('data', chunk => {
|
|
4329
4539
|
appendChunk(chunk, {
|
|
4540
|
+
tail: stdoutTail,
|
|
4330
4541
|
get value() { return stdoutOutput; },
|
|
4331
4542
|
set value(nextValue) { stdoutOutput = nextValue; },
|
|
4332
4543
|
get truncated() { return stdoutTruncated; },
|
|
@@ -4364,13 +4575,14 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
|
|
|
4364
4575
|
const clippedStdout = stdoutTruncated ? `${stdoutOutput}\n...[stdout-truncated]` : stdoutOutput;
|
|
4365
4576
|
const clippedStderr = stderrTruncated ? `${stderrOutput}\n...[stderr-truncated]` : stderrOutput;
|
|
4366
4577
|
const clippedRaw = `${clippedStdout}${clippedStdout && clippedStderr ? '\n' : ''}${clippedStderr}`;
|
|
4367
|
-
|
|
4368
|
-
const
|
|
4369
|
-
const output =
|
|
4578
|
+
// 结尾只在开头被截断时才需要(拼接最多 16MB,只算一次)
|
|
4579
|
+
const tailText = stdoutTruncated ? stdoutTail.text() : '';
|
|
4580
|
+
const output = resolveAgentFinalOutput(agentProgram, { stdout: clippedStdout, stdoutTruncated, stdoutTail: tailText, raw: clippedRaw });
|
|
4370
4581
|
resolve({
|
|
4371
4582
|
exitCode,
|
|
4372
4583
|
output,
|
|
4373
4584
|
stdout: clippedStdout,
|
|
4585
|
+
...(stdoutTruncated ? { stdoutTail: tailText } : {}),
|
|
4374
4586
|
// 真实的 claude 收到 SIGINT 会正常收尾、退出码为 0(L3 实测):发过信号就算被打断,不能只看退出码
|
|
4375
4587
|
interrupted: runState.stopping === true && (exitCode !== 0 || runState.signaled === true)
|
|
4376
4588
|
});
|
|
@@ -5069,6 +5281,7 @@ async function runWebAgentStreamTurn(ctx, state, sessionRef, prompt, clientMessa
|
|
|
5069
5281
|
resumeError,
|
|
5070
5282
|
engineSessionId
|
|
5071
5283
|
}, result);
|
|
5284
|
+
void refreshWebAgentTitle(ctx, state, sessionRef, agentMeta.agentProgram);
|
|
5072
5285
|
emitStreamEvent({
|
|
5073
5286
|
type: 'result',
|
|
5074
5287
|
exitCode: result.exitCode,
|
|
@@ -5393,6 +5606,9 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef, preloadedHist
|
|
|
5393
5606
|
agentId,
|
|
5394
5607
|
agentName: agentSession.agentName,
|
|
5395
5608
|
agentRemark: agentSession.remark || '',
|
|
5609
|
+
agentTitle: agentSession.title || '',
|
|
5610
|
+
firstPrompt: agentSession.firstPrompt || '',
|
|
5611
|
+
pinned: agentSession.pinned === true,
|
|
5396
5612
|
containerRemark: history.remark || '',
|
|
5397
5613
|
status: containerInfo.status || 'history',
|
|
5398
5614
|
image: containerInfo.image || '',
|
|
@@ -6165,6 +6381,51 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
6165
6381
|
});
|
|
6166
6382
|
}
|
|
6167
6383
|
},
|
|
6384
|
+
{
|
|
6385
|
+
// 系统设置“Agent 环境说明”:内置说明(只读)+ 全局配置里用户追加的说明
|
|
6386
|
+
method: 'GET',
|
|
6387
|
+
match: currentPath => currentPath === '/api/agent-instructions' ? [] : null,
|
|
6388
|
+
handler: async () => {
|
|
6389
|
+
const snapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
6390
|
+
const config = snapshot.parseError ? {} : snapshot.parsed;
|
|
6391
|
+
sendJson(res, 200, {
|
|
6392
|
+
builtin: BUILTIN_AGENT_INSTRUCTIONS,
|
|
6393
|
+
custom: typeof config.agentInstructions === 'string' ? config.agentInstructions : ''
|
|
6394
|
+
});
|
|
6395
|
+
}
|
|
6396
|
+
},
|
|
6397
|
+
{
|
|
6398
|
+
method: 'POST',
|
|
6399
|
+
match: currentPath => currentPath === '/api/agent-instructions' ? [] : null,
|
|
6400
|
+
handler: async () => {
|
|
6401
|
+
const payload = toPlainObject(await readJsonBody(req));
|
|
6402
|
+
if (typeof payload.custom !== 'string') {
|
|
6403
|
+
sendJson(res, 400, { error: 'custom 必须是字符串' });
|
|
6404
|
+
return;
|
|
6405
|
+
}
|
|
6406
|
+
const custom = payload.custom.slice(0, 4000);
|
|
6407
|
+
const snapshot = readWebConfigSnapshot(state.webConfigPath);
|
|
6408
|
+
if (snapshot.parseError) {
|
|
6409
|
+
sendJson(res, 400, { error: '当前配置解析失败,无法继续', detail: snapshot.parseError });
|
|
6410
|
+
return;
|
|
6411
|
+
}
|
|
6412
|
+
// 没写过、也没填内容时不动配置文件
|
|
6413
|
+
if (!custom.trim() && !hasOwn(snapshot.parsed, 'agentInstructions')) {
|
|
6414
|
+
sendJson(res, 200, { saved: false, custom });
|
|
6415
|
+
return;
|
|
6416
|
+
}
|
|
6417
|
+
let nextRaw;
|
|
6418
|
+
try {
|
|
6419
|
+
nextRaw = buildAgentInstructionsConfigRaw(baseRawOf(snapshot), custom);
|
|
6420
|
+
parseAndValidateConfigRaw(nextRaw);
|
|
6421
|
+
} catch (e) {
|
|
6422
|
+
sendJson(res, 400, { error: '配置写入失败' });
|
|
6423
|
+
return;
|
|
6424
|
+
}
|
|
6425
|
+
writeConfigFileSecure(path.resolve(state.webConfigPath), nextRaw);
|
|
6426
|
+
sendJson(res, 200, { saved: true, custom });
|
|
6427
|
+
}
|
|
6428
|
+
},
|
|
6168
6429
|
{
|
|
6169
6430
|
method: 'GET',
|
|
6170
6431
|
match: currentPath => currentPath === '/api/config' ? [] : null,
|
|
@@ -6669,6 +6930,26 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
6669
6930
|
sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), remark });
|
|
6670
6931
|
}
|
|
6671
6932
|
},
|
|
6933
|
+
{
|
|
6934
|
+
method: 'POST',
|
|
6935
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent-pin$/),
|
|
6936
|
+
handler: async match => {
|
|
6937
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
6938
|
+
if (!sessionRef) {
|
|
6939
|
+
return;
|
|
6940
|
+
}
|
|
6941
|
+
const payload = await readJsonBody(req);
|
|
6942
|
+
const pinned = payload.pinned === true;
|
|
6943
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
6944
|
+
// 默认 AGENT 在还没有任何消息时也会出现在列表里(历史里还没有它的记录),允许置顶
|
|
6945
|
+
if (sessionRef.agentId !== WEB_DEFAULT_AGENT_ID && !getWebAgentSession(history, sessionRef.agentId, { includeArchived: true })) {
|
|
6946
|
+
sendJson(res, 404, { error: 'AGENT 不存在' });
|
|
6947
|
+
return;
|
|
6948
|
+
}
|
|
6949
|
+
patchWebAgentSessionState(state.webHistoryDir, sessionRef, { pinned });
|
|
6950
|
+
sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), pinned });
|
|
6951
|
+
}
|
|
6952
|
+
},
|
|
6672
6953
|
{
|
|
6673
6954
|
method: 'GET',
|
|
6674
6955
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/messages$/),
|
|
@@ -6748,12 +7029,17 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
6748
7029
|
}
|
|
6749
7030
|
const requestUrl = new URL(req.url || '/api/sessions/x/fs/list', 'http://localhost');
|
|
6750
7031
|
const targetPath = String(requestUrl.searchParams.get('path') || '/').trim() || '/';
|
|
7032
|
+
const limitParam = Number.parseInt(requestUrl.searchParams.get('limit') || '', 10);
|
|
7033
|
+
const listOptions = {
|
|
7034
|
+
limit: Number.isInteger(limitParam) && limitParam > 0 ? Math.min(limitParam, WEB_FS_LIST_MAX_LIMIT) : 0,
|
|
7035
|
+
prefix: String(requestUrl.searchParams.get('prefix') || '').slice(0, 255)
|
|
7036
|
+
};
|
|
6751
7037
|
|
|
6752
7038
|
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
6753
7039
|
const payload = await execJsonCommandInWebContainer(
|
|
6754
7040
|
ctx,
|
|
6755
7041
|
sessionRef.containerName,
|
|
6756
|
-
buildContainerFileListCommand(targetPath)
|
|
7042
|
+
buildContainerFileListCommand(targetPath, listOptions)
|
|
6757
7043
|
);
|
|
6758
7044
|
if (payload && payload.error) {
|
|
6759
7045
|
sendJson(res, 400, { error: payload.error });
|
|
@@ -6982,6 +7268,68 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
6982
7268
|
sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), detail }, {}, { autoEtag: true });
|
|
6983
7269
|
}
|
|
6984
7270
|
},
|
|
7271
|
+
{
|
|
7272
|
+
// “改动”视图:工作目录所在 git 仓库的改动列表(按需请求,前端不轮询)
|
|
7273
|
+
method: 'GET',
|
|
7274
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/git\/changes$/),
|
|
7275
|
+
handler: async match => {
|
|
7276
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
7277
|
+
if (!sessionRef) {
|
|
7278
|
+
return;
|
|
7279
|
+
}
|
|
7280
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
7281
|
+
const changes = await readWebGitChanges(ctx, state, sessionRef);
|
|
7282
|
+
sendJson(res, 200, changes);
|
|
7283
|
+
}
|
|
7284
|
+
},
|
|
7285
|
+
{
|
|
7286
|
+
method: 'GET',
|
|
7287
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/git\/diff$/),
|
|
7288
|
+
handler: async match => {
|
|
7289
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
7290
|
+
if (!sessionRef) {
|
|
7291
|
+
return;
|
|
7292
|
+
}
|
|
7293
|
+
const requestUrl = new URL(req.url || '/', 'http://localhost');
|
|
7294
|
+
const filePath = String(requestUrl.searchParams.get('path') || '');
|
|
7295
|
+
if (!gitChanges.isAcceptableDiffPath(filePath)) {
|
|
7296
|
+
sendJson(res, 400, { error: '文件路径不合法' });
|
|
7297
|
+
return;
|
|
7298
|
+
}
|
|
7299
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
7300
|
+
// 只给改动列表里出现过的文件看 diff:路径由服务端重新取一次核对,不信任请求
|
|
7301
|
+
const changes = await readWebGitChanges(ctx, state, sessionRef);
|
|
7302
|
+
const entry = changes.files.find(file => file.path === filePath);
|
|
7303
|
+
if (!entry || entry.status === 'untracked') {
|
|
7304
|
+
sendJson(res, 404, { error: '该文件不在改动列表里' });
|
|
7305
|
+
return;
|
|
7306
|
+
}
|
|
7307
|
+
const result = await execCommandInWebContainer(
|
|
7308
|
+
ctx,
|
|
7309
|
+
sessionRef.containerName,
|
|
7310
|
+
gitChanges.buildGitDiffCommand(resolveWebSessionWorkDir(state, sessionRef), filePath),
|
|
7311
|
+
{ withEnv: false }
|
|
7312
|
+
);
|
|
7313
|
+
sendJson(res, 200, { path: filePath, ...gitChanges.parseGitDiffOutput(result.stdout) });
|
|
7314
|
+
}
|
|
7315
|
+
},
|
|
7316
|
+
{
|
|
7317
|
+
// 输入框里打 / 时的候选:只列实测能在 Web 端执行的(见 lib/web/session-meta.js)
|
|
7318
|
+
method: 'GET',
|
|
7319
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/slash-commands$/),
|
|
7320
|
+
handler: async match => {
|
|
7321
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
7322
|
+
if (!sessionRef) {
|
|
7323
|
+
return;
|
|
7324
|
+
}
|
|
7325
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
7326
|
+
const agentSession = getWebAgentSession(history, sessionRef.agentId, { includeArchived: true });
|
|
7327
|
+
sendJson(res, 200, {
|
|
7328
|
+
name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
|
|
7329
|
+
commands: sessionMeta.listUsableSlashCommands(agentSession && agentSession.slashCommands)
|
|
7330
|
+
});
|
|
7331
|
+
}
|
|
7332
|
+
},
|
|
6985
7333
|
{
|
|
6986
7334
|
method: 'GET',
|
|
6987
7335
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/models$/),
|
|
@@ -7221,6 +7569,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
7221
7569
|
resumeError,
|
|
7222
7570
|
engineSessionId
|
|
7223
7571
|
}, result);
|
|
7572
|
+
void refreshWebAgentTitle(ctx, state, sessionRef, agentMeta.agentProgram);
|
|
7224
7573
|
sendJson(res, 200, {
|
|
7225
7574
|
exitCode: result.exitCode,
|
|
7226
7575
|
output: result.output,
|
|
@@ -7532,6 +7881,10 @@ async function startWebServer(options) {
|
|
|
7532
7881
|
agentStopGraceMs: Number.isFinite(options.agentStopGraceMs) && options.agentStopGraceMs > 0
|
|
7533
7882
|
? options.agentStopGraceMs
|
|
7534
7883
|
: agentRunControl.DEFAULT_GRACE_MS,
|
|
7884
|
+
// Agent 原始输出保留开头的上限(测试可调小);结尾另留一份,见 createTailBuffer
|
|
7885
|
+
agentRawOutputMaxChars: Number.isFinite(options.agentRawOutputMaxChars) && options.agentRawOutputMaxChars > 0
|
|
7886
|
+
? options.agentRawOutputMaxChars
|
|
7887
|
+
: AGENT_RAW_OUTPUT_MAX_CHARS,
|
|
7535
7888
|
// 追加提示词“当前步骤结束后插入”等不到步骤边界时,退化成立即打断的等待时间
|
|
7536
7889
|
agentStepFallbackMs: Number.isFinite(options.agentStepFallbackMs) && options.agentStepFallbackMs > 0
|
|
7537
7890
|
? options.agentStepFallbackMs
|
|
@@ -7821,6 +8174,8 @@ module.exports = {
|
|
|
7821
8174
|
__testing: {
|
|
7822
8175
|
prepareStructuredTraceEvents,
|
|
7823
8176
|
buildClaudeAgentExecCommand,
|
|
8177
|
+
buildWebAgentExecCommand,
|
|
8178
|
+
buildContainerFileListCommand,
|
|
7824
8179
|
extractContentDeltaFromPayload,
|
|
7825
8180
|
extractAgentMessageFromStructuredOutput,
|
|
7826
8181
|
appendWebSessionMessage,
|