@xcanwin/manyoyo 8.6.7 → 8.6.8
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/lib/web/AGENTS.md +1 -0
- package/lib/web/index.html +86 -85
- package/lib/web/server.js +71 -9
- package/package.json +2 -1
package/lib/web/server.js
CHANGED
|
@@ -835,6 +835,44 @@ function stripAnsi(text) {
|
|
|
835
835
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
836
836
|
}
|
|
837
837
|
|
|
838
|
+
// Agent 原始输出只保留开头 AGENT_RAW_OUTPUT_MAX_CHARS(会话 id 在开头),另外保留一份结尾:
|
|
839
|
+
// 最终回复和用量都在输出末尾,超过上限后只看开头会把截断点之前的某条中间说明当成最终回复
|
|
840
|
+
const AGENT_RAW_OUTPUT_MAX_CHARS = 32 * 1024 * 1024;
|
|
841
|
+
const AGENT_OUTPUT_TAIL_MAX_CHARS = 8 * 1024 * 1024;
|
|
842
|
+
// 最终回复的上限:远大于正常回复,只挡住把整个文件当回复打出来的极端情况
|
|
843
|
+
const AGENT_FINAL_REPLY_MAX_CHARS = 1000000;
|
|
844
|
+
|
|
845
|
+
function createTailBuffer(maxChars) {
|
|
846
|
+
let parts = [];
|
|
847
|
+
let size = 0;
|
|
848
|
+
return {
|
|
849
|
+
push(text) {
|
|
850
|
+
if (!text) return;
|
|
851
|
+
parts.push(text);
|
|
852
|
+
size += text.length;
|
|
853
|
+
if (size > maxChars * 2) {
|
|
854
|
+
const joined = parts.join('').slice(-maxChars);
|
|
855
|
+
parts = [joined];
|
|
856
|
+
size = joined.length;
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
text() {
|
|
860
|
+
return parts.join('').slice(-maxChars);
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// 结束后的回复文本:结构化输出里提取到的最终回复按 AGENT_FINAL_REPLY_MAX_CHARS(远大于正常回复)保留;
|
|
866
|
+
// 提取不到才退回原始输出,原始输出按 WEB_OUTPUT_MAX_CHARS 截断
|
|
867
|
+
function resolveAgentFinalOutput(agentProgram, { stdout, stdoutTruncated, stdoutTail, raw }) {
|
|
868
|
+
const extracted = (stdoutTruncated ? extractAgentMessageFromStructuredOutput(agentProgram, stdoutTail) : '')
|
|
869
|
+
|| extractAgentMessageFromStructuredOutput(agentProgram, stdout);
|
|
870
|
+
if (extracted) {
|
|
871
|
+
return clipText(stripAnsi(extracted).trim(), AGENT_FINAL_REPLY_MAX_CHARS);
|
|
872
|
+
}
|
|
873
|
+
return clipText(stripAnsi(raw).trim() || '(无输出)');
|
|
874
|
+
}
|
|
875
|
+
|
|
838
876
|
function clipText(text, maxChars = WEB_OUTPUT_MAX_CHARS) {
|
|
839
877
|
if (typeof text !== 'string') return '';
|
|
840
878
|
if (text.length <= maxChars) return text;
|
|
@@ -1062,6 +1100,11 @@ function extractClaudeAgentMessage(text) {
|
|
|
1062
1100
|
let lastMessage = '';
|
|
1063
1101
|
for (const rawLine of String(text || '').split('\n')) {
|
|
1064
1102
|
const payload = parseJsonObjectLine(rawLine);
|
|
1103
|
+
// result 事件带权威的最终文本;后台任务场景结尾有两个 result(第一个是中间结论),取最后一个
|
|
1104
|
+
if (payload && payload.type === 'result' && typeof payload.result === 'string' && payload.result.trim()) {
|
|
1105
|
+
lastMessage = payload.result.trim();
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1065
1108
|
if (!payload || payload.type !== 'assistant' || pickFirstString(payload.parent_tool_use_id)) {
|
|
1066
1109
|
continue;
|
|
1067
1110
|
}
|
|
@@ -2291,7 +2334,8 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
|
|
|
2291
2334
|
}
|
|
2292
2335
|
|
|
2293
2336
|
function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, meta, result) {
|
|
2294
|
-
|
|
2337
|
+
// 用量在输出末尾:原始输出超过上限时开头里没有它
|
|
2338
|
+
const turnUsage = extractTurnUsage(agentMeta.agentProgram, result.stdoutTail || result.stdout);
|
|
2295
2339
|
const replyMessage = appendWebSessionMessage(state.webHistoryDir, sessionRef, 'assistant', result.output, {
|
|
2296
2340
|
exitCode: result.exitCode,
|
|
2297
2341
|
mode: 'agent',
|
|
@@ -3778,7 +3822,8 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
3778
3822
|
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
3779
3823
|
);
|
|
3780
3824
|
|
|
3781
|
-
const MAX_RAW_OUTPUT_CHARS =
|
|
3825
|
+
const MAX_RAW_OUTPUT_CHARS = ctx.agentRawOutputMaxChars || AGENT_RAW_OUTPUT_MAX_CHARS;
|
|
3826
|
+
const stdoutTail = createTailBuffer(Math.min(MAX_RAW_OUTPUT_CHARS, AGENT_OUTPUT_TAIL_MAX_CHARS));
|
|
3782
3827
|
let stdoutOutput = '';
|
|
3783
3828
|
let stderrOutput = '';
|
|
3784
3829
|
let stdoutTruncated = false;
|
|
@@ -3788,6 +3833,7 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
3788
3833
|
if (!chunk) return;
|
|
3789
3834
|
const text = chunk.toString('utf-8');
|
|
3790
3835
|
if (!text) return;
|
|
3836
|
+
if (target.tail) target.tail.push(text);
|
|
3791
3837
|
if (target.value.length >= MAX_RAW_OUTPUT_CHARS) {
|
|
3792
3838
|
target.truncated = true;
|
|
3793
3839
|
return;
|
|
@@ -3801,7 +3847,11 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
3801
3847
|
target.value += text;
|
|
3802
3848
|
}
|
|
3803
3849
|
|
|
3850
|
+
// 按流解码:逐块 toString 会把跨块的多字节字符(中文)拆成乱码
|
|
3851
|
+
process.stdout.setEncoding('utf8');
|
|
3852
|
+
process.stderr.setEncoding('utf8');
|
|
3804
3853
|
process.stdout.on('data', chunk => appendChunk(chunk, {
|
|
3854
|
+
tail: stdoutTail,
|
|
3805
3855
|
get value() { return stdoutOutput; },
|
|
3806
3856
|
set value(nextValue) { stdoutOutput = nextValue; },
|
|
3807
3857
|
get truncated() { return stdoutTruncated; },
|
|
@@ -3820,13 +3870,14 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
3820
3870
|
const clippedStdout = stdoutTruncated ? `${stdoutOutput}\n...[stdout-truncated]` : stdoutOutput;
|
|
3821
3871
|
const clippedStderr = stderrTruncated ? `${stderrOutput}\n...[stderr-truncated]` : stderrOutput;
|
|
3822
3872
|
const clippedRaw = `${clippedStdout}${clippedStdout && clippedStderr ? '\n' : ''}${clippedStderr}`;
|
|
3823
|
-
|
|
3824
|
-
const
|
|
3825
|
-
const output =
|
|
3873
|
+
// 结尾只在开头被截断时才需要(拼接最多 16MB,只算一次)
|
|
3874
|
+
const tailText = stdoutTruncated ? stdoutTail.text() : '';
|
|
3875
|
+
const output = resolveAgentFinalOutput(agentProgram, { stdout: clippedStdout, stdoutTruncated, stdoutTail: tailText, raw: clippedRaw });
|
|
3826
3876
|
resolve({
|
|
3827
3877
|
exitCode,
|
|
3828
3878
|
output,
|
|
3829
3879
|
stdout: clippedStdout,
|
|
3880
|
+
...(stdoutTruncated ? { stdoutTail: tailText } : {}),
|
|
3830
3881
|
stderr: clippedStderr
|
|
3831
3882
|
});
|
|
3832
3883
|
});
|
|
@@ -4230,7 +4281,8 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
|
|
|
4230
4281
|
}
|
|
4231
4282
|
|
|
4232
4283
|
return await new Promise((resolve, reject) => {
|
|
4233
|
-
const MAX_RAW_OUTPUT_CHARS =
|
|
4284
|
+
const MAX_RAW_OUTPUT_CHARS = ctx.agentRawOutputMaxChars || AGENT_RAW_OUTPUT_MAX_CHARS;
|
|
4285
|
+
const stdoutTail = createTailBuffer(Math.min(MAX_RAW_OUTPUT_CHARS, AGENT_OUTPUT_TAIL_MAX_CHARS));
|
|
4234
4286
|
let stdoutOutput = '';
|
|
4235
4287
|
let stderrOutput = '';
|
|
4236
4288
|
let stdoutTruncated = false;
|
|
@@ -4245,6 +4297,7 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
|
|
|
4245
4297
|
if (!chunk) return;
|
|
4246
4298
|
const text = chunk.toString('utf-8');
|
|
4247
4299
|
if (!text) return;
|
|
4300
|
+
if (target.tail) target.tail.push(text);
|
|
4248
4301
|
if (target.value.length >= MAX_RAW_OUTPUT_CHARS) {
|
|
4249
4302
|
target.truncated = true;
|
|
4250
4303
|
return;
|
|
@@ -4325,8 +4378,12 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
|
|
|
4325
4378
|
return pending;
|
|
4326
4379
|
}
|
|
4327
4380
|
|
|
4381
|
+
// 按流解码:逐块 toString 会把跨块的多字节字符(中文)拆成乱码
|
|
4382
|
+
process.stdout.setEncoding('utf8');
|
|
4383
|
+
process.stderr.setEncoding('utf8');
|
|
4328
4384
|
process.stdout.on('data', chunk => {
|
|
4329
4385
|
appendChunk(chunk, {
|
|
4386
|
+
tail: stdoutTail,
|
|
4330
4387
|
get value() { return stdoutOutput; },
|
|
4331
4388
|
set value(nextValue) { stdoutOutput = nextValue; },
|
|
4332
4389
|
get truncated() { return stdoutTruncated; },
|
|
@@ -4364,13 +4421,14 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
|
|
|
4364
4421
|
const clippedStdout = stdoutTruncated ? `${stdoutOutput}\n...[stdout-truncated]` : stdoutOutput;
|
|
4365
4422
|
const clippedStderr = stderrTruncated ? `${stderrOutput}\n...[stderr-truncated]` : stderrOutput;
|
|
4366
4423
|
const clippedRaw = `${clippedStdout}${clippedStdout && clippedStderr ? '\n' : ''}${clippedStderr}`;
|
|
4367
|
-
|
|
4368
|
-
const
|
|
4369
|
-
const output =
|
|
4424
|
+
// 结尾只在开头被截断时才需要(拼接最多 16MB,只算一次)
|
|
4425
|
+
const tailText = stdoutTruncated ? stdoutTail.text() : '';
|
|
4426
|
+
const output = resolveAgentFinalOutput(agentProgram, { stdout: clippedStdout, stdoutTruncated, stdoutTail: tailText, raw: clippedRaw });
|
|
4370
4427
|
resolve({
|
|
4371
4428
|
exitCode,
|
|
4372
4429
|
output,
|
|
4373
4430
|
stdout: clippedStdout,
|
|
4431
|
+
...(stdoutTruncated ? { stdoutTail: tailText } : {}),
|
|
4374
4432
|
// 真实的 claude 收到 SIGINT 会正常收尾、退出码为 0(L3 实测):发过信号就算被打断,不能只看退出码
|
|
4375
4433
|
interrupted: runState.stopping === true && (exitCode !== 0 || runState.signaled === true)
|
|
4376
4434
|
});
|
|
@@ -7532,6 +7590,10 @@ async function startWebServer(options) {
|
|
|
7532
7590
|
agentStopGraceMs: Number.isFinite(options.agentStopGraceMs) && options.agentStopGraceMs > 0
|
|
7533
7591
|
? options.agentStopGraceMs
|
|
7534
7592
|
: agentRunControl.DEFAULT_GRACE_MS,
|
|
7593
|
+
// Agent 原始输出保留开头的上限(测试可调小);结尾另留一份,见 createTailBuffer
|
|
7594
|
+
agentRawOutputMaxChars: Number.isFinite(options.agentRawOutputMaxChars) && options.agentRawOutputMaxChars > 0
|
|
7595
|
+
? options.agentRawOutputMaxChars
|
|
7596
|
+
: AGENT_RAW_OUTPUT_MAX_CHARS,
|
|
7535
7597
|
// 追加提示词“当前步骤结束后插入”等不到步骤边界时,退化成立即打断的等待时间
|
|
7536
7598
|
agentStepFallbackMs: Number.isFinite(options.agentStepFallbackMs) && options.agentStepFallbackMs > 0
|
|
7537
7599
|
? options.agentStepFallbackMs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcanwin/manyoyo",
|
|
3
|
-
"version": "8.6.
|
|
3
|
+
"version": "8.6.8",
|
|
4
4
|
"imageVersion": "2.2.0-common",
|
|
5
5
|
"playwrightCliVersion": "0.1.19",
|
|
6
6
|
"patchrightCoreVersion": "1.63.0",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"test": "jest --coverage && npm run test:web",
|
|
46
46
|
"test:unit": "jest test/ --testPathIgnorePatterns '<rootDir>/temp/' '<rootDir>/.claude/' '<rootDir>/test/integration/' && npm run test:web",
|
|
47
47
|
"test:integration": "jest test/integration/ --runInBand",
|
|
48
|
+
"test:experience": "jest test/integration/web-chat-experience.test.js --runInBand",
|
|
48
49
|
"test:installer": "jest test/installer.test.js",
|
|
49
50
|
"lint:sh": "command -v shellcheck >/dev/null 2>&1 || { echo '未安装 shellcheck:macOS 用 brew install shellcheck,Debian/Ubuntu 用 sudo apt install shellcheck,或用 docker run --rm -v \"$PWD:/mnt\" -w /mnt koalaman/shellcheck:stable scripts/install.sh scripts/offline/*.sh' >&2; exit 1; } && shellcheck -s sh -x -e SC1091 scripts/install.sh scripts/offline/*.sh",
|
|
50
51
|
"docs:dev": "vitepress dev docs --host 127.0.0.1 --port 5173",
|