@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/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
- const turnUsage = extractTurnUsage(agentMeta.agentProgram, result.stdout);
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 = 32 * 1024 * 1024;
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
- const extractedAgentMessage = extractAgentMessageFromStructuredOutput(agentProgram, clippedStdout);
3824
- const cleanOutputSource = extractedAgentMessage || clippedRaw;
3825
- const output = clipText(stripAnsi(cleanOutputSource).trim() || '(无输出)');
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 = 32 * 1024 * 1024;
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
- const extractedAgentMessage = extractAgentMessageFromStructuredOutput(agentProgram, clippedStdout);
4368
- const cleanOutputSource = extractedAgentMessage || clippedRaw;
4369
- const output = clipText(stripAnsi(cleanOutputSource).trim() || '(无输出)');
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.7",
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",