@xcanwin/manyoyo 5.6.8 → 5.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/lib/web/server.js CHANGED
@@ -321,6 +321,7 @@ function hasAgentConversationHistory(history) {
321
321
  for (const message of messages) {
322
322
  if (!message || typeof message !== 'object') continue;
323
323
  if (message.mode !== 'agent') continue;
324
+ if (message.streamTrace === true) continue;
324
325
  if (message.role === 'user' || message.role === 'assistant') {
325
326
  return true;
326
327
  }
@@ -339,7 +340,12 @@ function clipAgentContextMessageText(text) {
339
340
  function buildAgentPromptWithHistory(history, prompt) {
340
341
  const sessionHistory = history && Array.isArray(history.messages) ? history.messages : [];
341
342
  const relevantMessages = sessionHistory
342
- .filter(message => message && message.mode === 'agent' && (message.role === 'user' || message.role === 'assistant'))
343
+ .filter(message => (
344
+ message
345
+ && message.mode === 'agent'
346
+ && message.streamTrace !== true
347
+ && (message.role === 'user' || message.role === 'assistant')
348
+ ))
343
349
  .slice(-WEB_AGENT_CONTEXT_MAX_MESSAGES);
344
350
  if (!relevantMessages.length) {
345
351
  return String(prompt || '');
@@ -370,6 +376,305 @@ function buildAgentPromptWithHistory(history, prompt) {
370
376
  ].join('\n');
371
377
  }
372
378
 
379
+ function prepareCodexTraceEvent(payload) {
380
+ if (!payload || typeof payload !== 'object') {
381
+ return null;
382
+ }
383
+
384
+ const eventType = typeof payload.type === 'string' ? payload.type : '';
385
+ const item = payload.item && typeof payload.item === 'object' && !Array.isArray(payload.item)
386
+ ? payload.item
387
+ : {};
388
+ const itemType = typeof item.type === 'string' ? item.type : '';
389
+ const text = pickFirstString(
390
+ item.title,
391
+ item.summary,
392
+ item.text,
393
+ item.name,
394
+ item.command,
395
+ payload.message,
396
+ payload.text
397
+ );
398
+ const toolName = pickFirstString(
399
+ item.name,
400
+ item.tool_name,
401
+ item.tool,
402
+ item.command
403
+ );
404
+ const commandText = pickFirstString(item.command);
405
+ const mcpServer = pickFirstString(item.server);
406
+ const mcpTool = pickFirstString(item.tool);
407
+ const itemStatus = pickFirstString(item.status);
408
+
409
+ function shortenText(value, maxChars = 140) {
410
+ const raw = clipText(stripAnsi(String(value || '')).replace(/\s+/g, ' ').trim(), maxChars);
411
+ return raw.trim();
412
+ }
413
+
414
+ function summarizeArguments(args) {
415
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
416
+ return '';
417
+ }
418
+ const parts = [];
419
+ for (const [key, value] of Object.entries(args)) {
420
+ if (value === undefined || value === null) continue;
421
+ if (typeof value === 'string') {
422
+ const textValue = value.trim();
423
+ if (!textValue) continue;
424
+ parts.push(`${key}=${shortenText(textValue, 80)}`);
425
+ continue;
426
+ }
427
+ if (typeof value === 'number' || typeof value === 'boolean') {
428
+ parts.push(`${key}=${String(value)}`);
429
+ }
430
+ }
431
+ return parts.slice(0, 3).join(', ');
432
+ }
433
+
434
+ function pickDisplayStatus(defaultStatus) {
435
+ const status = String(itemStatus || defaultStatus || '').trim();
436
+ return status || '';
437
+ }
438
+
439
+ function createTraceEvent(kind, textValue, extra = {}) {
440
+ const normalizedText = String(textValue || '').trim();
441
+ if (!normalizedText) {
442
+ return null;
443
+ }
444
+ return {
445
+ provider: 'codex',
446
+ kind,
447
+ eventType,
448
+ itemType: itemType || '',
449
+ text: normalizedText,
450
+ ...extra
451
+ };
452
+ }
453
+
454
+ if (eventType === 'thread.started') {
455
+ return createTraceEvent('thread', '[会话] Codex 已开始处理', {
456
+ phase: 'started',
457
+ status: 'started'
458
+ });
459
+ }
460
+ if (eventType === 'thread.completed') {
461
+ return createTraceEvent('thread', '[会话] Codex 已完成当前任务', {
462
+ phase: 'completed',
463
+ status: 'completed'
464
+ });
465
+ }
466
+ if (eventType === 'turn.started') {
467
+ return createTraceEvent('turn', '[回合] 开始生成响应', {
468
+ phase: 'started',
469
+ status: 'started'
470
+ });
471
+ }
472
+ if (eventType === 'turn.completed') {
473
+ return createTraceEvent('turn', '[回合] 响应完成', {
474
+ phase: 'completed',
475
+ status: 'completed'
476
+ });
477
+ }
478
+ if (eventType === 'item.started') {
479
+ if (itemType === 'tool_call') {
480
+ return createTraceEvent('tool', `[工具开始] ${toolName || 'tool_call'}`, {
481
+ phase: 'started',
482
+ status: pickDisplayStatus('in_progress'),
483
+ toolName: toolName || 'tool_call'
484
+ });
485
+ }
486
+ if (itemType === 'command_execution') {
487
+ return createTraceEvent('command', `[命令开始] ${commandText || 'command_execution'}`, {
488
+ phase: 'started',
489
+ status: pickDisplayStatus('in_progress'),
490
+ command: commandText || 'command_execution'
491
+ });
492
+ }
493
+ if (itemType === 'mcp_tool_call') {
494
+ const summary = summarizeArguments(item.arguments);
495
+ return createTraceEvent(
496
+ 'mcp',
497
+ summary
498
+ ? `[MCP开始] ${mcpServer || 'mcp'}.${mcpTool || 'tool'} (${summary})`
499
+ : `[MCP开始] ${mcpServer || 'mcp'}.${mcpTool || 'tool'}`,
500
+ {
501
+ phase: 'started',
502
+ status: pickDisplayStatus('in_progress'),
503
+ server: mcpServer || 'mcp',
504
+ tool: mcpTool || 'tool',
505
+ arguments: item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments)
506
+ ? item.arguments
507
+ : null,
508
+ argumentSummary: summary
509
+ }
510
+ );
511
+ }
512
+ if (itemType === 'reasoning') {
513
+ return createTraceEvent('status', text ? `[状态] ${text}` : '[状态] Codex 正在分析', {
514
+ phase: 'started',
515
+ status: pickDisplayStatus('in_progress'),
516
+ detail: text || 'Codex 正在分析'
517
+ });
518
+ }
519
+ if (itemType === 'agent_message') {
520
+ return createTraceEvent('agent_message', text ? `[说明] ${text}` : '[回复] 正在生成最终答复', {
521
+ phase: 'started',
522
+ status: pickDisplayStatus('in_progress'),
523
+ detail: text || '正在生成最终答复'
524
+ });
525
+ }
526
+ return createTraceEvent('event', text ? `[事件开始] ${text}` : `[事件开始] ${itemType || eventType}`, {
527
+ phase: 'started',
528
+ status: pickDisplayStatus('in_progress'),
529
+ detail: text || itemType || eventType
530
+ });
531
+ }
532
+ if (eventType === 'item.completed') {
533
+ if (itemType === 'tool_call') {
534
+ return createTraceEvent('tool', `[工具完成] ${toolName || 'tool_call'}`, {
535
+ phase: 'completed',
536
+ status: pickDisplayStatus('completed'),
537
+ toolName: toolName || 'tool_call'
538
+ });
539
+ }
540
+ if (itemType === 'command_execution') {
541
+ const suffix = itemStatus || (typeof item.exit_code === 'number' ? `exit=${item.exit_code}` : 'completed');
542
+ return createTraceEvent('command', `[命令完成] ${commandText || 'command_execution'} (${suffix})`, {
543
+ phase: 'completed',
544
+ status: pickDisplayStatus(suffix),
545
+ command: commandText || 'command_execution',
546
+ exitCode: typeof item.exit_code === 'number' ? item.exit_code : null
547
+ });
548
+ }
549
+ if (itemType === 'mcp_tool_call') {
550
+ const summary = summarizeArguments(item.arguments);
551
+ return createTraceEvent(
552
+ 'mcp',
553
+ summary
554
+ ? `[MCP完成] ${mcpServer || 'mcp'}.${mcpTool || 'tool'} (${summary})`
555
+ : `[MCP完成] ${mcpServer || 'mcp'}.${mcpTool || 'tool'}`,
556
+ {
557
+ phase: 'completed',
558
+ status: pickDisplayStatus('completed'),
559
+ server: mcpServer || 'mcp',
560
+ tool: mcpTool || 'tool',
561
+ arguments: item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments)
562
+ ? item.arguments
563
+ : null,
564
+ argumentSummary: summary,
565
+ result: item.result !== undefined ? item.result : null,
566
+ error: item.error !== undefined ? item.error : null
567
+ }
568
+ );
569
+ }
570
+ if (itemType === 'reasoning') {
571
+ return createTraceEvent('status', text ? `[状态] ${text}` : '', {
572
+ phase: 'completed',
573
+ status: pickDisplayStatus('completed'),
574
+ detail: text || ''
575
+ });
576
+ }
577
+ if (itemType === 'agent_message') {
578
+ return createTraceEvent('agent_message', text ? `[说明] ${text}` : '[回复] 已生成', {
579
+ phase: 'completed',
580
+ status: pickDisplayStatus('completed'),
581
+ detail: text || '已生成'
582
+ });
583
+ }
584
+ return createTraceEvent('event', text ? `[事件完成] ${text}` : `[事件完成] ${itemType || eventType}`, {
585
+ phase: 'completed',
586
+ status: pickDisplayStatus('completed'),
587
+ detail: text || itemType || eventType
588
+ });
589
+ }
590
+ if (eventType === 'error') {
591
+ return createTraceEvent('error', text ? `[错误] ${text}` : '[错误] Codex 返回了错误事件', {
592
+ status: 'error',
593
+ detail: text || 'Codex 返回了错误事件'
594
+ });
595
+ }
596
+
597
+ return createTraceEvent('event', `[事件] ${eventType}`, {
598
+ status: itemStatus || '',
599
+ detail: eventType
600
+ });
601
+ }
602
+
603
+ async function prepareWebAgentExecution(ctx, state, containerName, prompt) {
604
+ const history = loadWebSessionHistory(state.webHistoryDir, containerName);
605
+ const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
606
+ if (normalizedTemplate !== history.agentPromptCommand) {
607
+ history.agentPromptCommand = normalizedTemplate;
608
+ saveWebSessionHistory(state.webHistoryDir, containerName, history);
609
+ }
610
+ if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
611
+ throw new Error('当前会话未配置 agentPromptCommand');
612
+ }
613
+
614
+ await ensureWebContainer(ctx, state, containerName);
615
+ const agentMeta = getAgentRuntimeMeta(history);
616
+ const hasPriorConversation = hasAgentConversationHistory(history);
617
+ let resumeAttempted = false;
618
+ let resumeSucceeded = false;
619
+ let resumeError = '';
620
+
621
+ if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
622
+ resumeAttempted = true;
623
+ const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
624
+ if (resumeResult.exitCode === 0) {
625
+ resumeSucceeded = true;
626
+ } else {
627
+ resumeError = clipText(String(resumeResult.output || '(无输出)'), 1200);
628
+ }
629
+ }
630
+
631
+ const effectivePrompt = resumeSucceeded
632
+ ? prompt
633
+ : buildAgentPromptWithHistory(history, prompt);
634
+ const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
635
+ const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
636
+
637
+ return {
638
+ history,
639
+ agentMeta,
640
+ command,
641
+ contextMode,
642
+ resumeAttempted,
643
+ resumeSucceeded,
644
+ resumeError
645
+ };
646
+ }
647
+
648
+ function finalizeWebAgentExecution(state, containerName, history, agentMeta, meta, result) {
649
+ appendWebSessionMessage(state.webHistoryDir, containerName, 'assistant', result.output, {
650
+ exitCode: result.exitCode,
651
+ mode: 'agent',
652
+ contextMode: meta.contextMode,
653
+ resumeAttempted: meta.resumeAttempted,
654
+ resumeSucceeded: meta.resumeSucceeded,
655
+ interrupted: result.interrupted === true
656
+ });
657
+ patchWebSessionAgentState(state.webHistoryDir, containerName, {
658
+ agentProgram: agentMeta.agentProgram,
659
+ resumeSupported: agentMeta.resumeSupported,
660
+ lastResumeAt: meta.resumeAttempted ? new Date().toISOString() : history.lastResumeAt || null,
661
+ lastResumeOk: meta.resumeAttempted ? meta.resumeSucceeded : history.lastResumeOk,
662
+ lastResumeError: meta.resumeAttempted ? (meta.resumeSucceeded ? '' : meta.resumeError) : history.lastResumeError || ''
663
+ });
664
+ }
665
+
666
+ function appendWebAgentTraceMessage(webHistoryDir, containerName, content, extra = {}) {
667
+ const text = String(content || '').trim();
668
+ if (!text) {
669
+ return;
670
+ }
671
+ appendWebSessionMessage(webHistoryDir, containerName, 'assistant', text, {
672
+ mode: 'agent',
673
+ streamTrace: true,
674
+ ...extra
675
+ });
676
+ }
677
+
373
678
  function secureStringEqual(a, b) {
374
679
  const aStr = String(a || '');
375
680
  const bStr = String(b || '');
@@ -1177,6 +1482,150 @@ async function execCommandInWebContainer(ctx, containerName, command) {
1177
1482
  });
1178
1483
  }
1179
1484
 
1485
+ async function execAgentInWebContainerStream(ctx, state, containerName, command, options = {}) {
1486
+ const opts = options && typeof options === 'object' ? options : {};
1487
+ const agentProgram = typeof opts.agentProgram === 'string' ? opts.agentProgram : '';
1488
+ const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : () => {};
1489
+ const process = spawn(
1490
+ ctx.dockerCmd,
1491
+ ['exec', containerName, '/bin/bash', '-lc', command],
1492
+ { stdio: ['ignore', 'pipe', 'pipe'] }
1493
+ );
1494
+
1495
+ const runState = {
1496
+ containerName,
1497
+ process,
1498
+ command,
1499
+ startedAt: new Date().toISOString(),
1500
+ stopping: false
1501
+ };
1502
+ state.agentRuns.set(containerName, runState);
1503
+
1504
+ return await new Promise((resolve, reject) => {
1505
+ const MAX_RAW_OUTPUT_CHARS = 32 * 1024 * 1024;
1506
+ let stdoutOutput = '';
1507
+ let stderrOutput = '';
1508
+ let stdoutTruncated = false;
1509
+ let stderrTruncated = false;
1510
+ let stdoutPending = '';
1511
+ let stderrPending = '';
1512
+ function appendChunk(chunk, target) {
1513
+ if (!chunk) return;
1514
+ const text = chunk.toString('utf-8');
1515
+ if (!text) return;
1516
+ if (target.value.length >= MAX_RAW_OUTPUT_CHARS) {
1517
+ target.truncated = true;
1518
+ return;
1519
+ }
1520
+ const remain = MAX_RAW_OUTPUT_CHARS - target.value.length;
1521
+ if (text.length > remain) {
1522
+ target.value += text.slice(0, remain);
1523
+ target.truncated = true;
1524
+ return;
1525
+ }
1526
+ target.value += text;
1527
+ }
1528
+
1529
+ function emitStdoutTraceLine(line) {
1530
+ const rawLine = String(line || '').trim();
1531
+ if (!rawLine) {
1532
+ return;
1533
+ }
1534
+ if (agentProgram === 'codex') {
1535
+ let payload = null;
1536
+ try {
1537
+ payload = JSON.parse(rawLine);
1538
+ } catch (e) {
1539
+ payload = null;
1540
+ }
1541
+ if (payload) {
1542
+ const traceEvent = prepareCodexTraceEvent(payload);
1543
+ if (traceEvent && traceEvent.text) {
1544
+ onEvent({
1545
+ type: 'trace',
1546
+ stream: 'stdout',
1547
+ text: traceEvent.text,
1548
+ traceEvent
1549
+ });
1550
+ }
1551
+ return;
1552
+ }
1553
+ if (/^OpenAI Codex\b/.test(rawLine) || /^tokens used\b/i.test(rawLine)) {
1554
+ return;
1555
+ }
1556
+ }
1557
+ onEvent({ type: 'trace', stream: 'stdout', text: rawLine });
1558
+ }
1559
+
1560
+ function emitStderrTraceLine(line) {
1561
+ const rawLine = String(line || '').trim();
1562
+ if (!rawLine) {
1563
+ return;
1564
+ }
1565
+ onEvent({ type: 'trace', stream: 'stderr', text: `[stderr] ${rawLine}` });
1566
+ }
1567
+
1568
+ function drainLines(text, carry, handleLine) {
1569
+ let pending = carry + String(text || '');
1570
+ let newlineIndex = pending.indexOf('\n');
1571
+ while (newlineIndex !== -1) {
1572
+ const line = pending.slice(0, newlineIndex).replace(/\r$/, '');
1573
+ handleLine(line);
1574
+ pending = pending.slice(newlineIndex + 1);
1575
+ newlineIndex = pending.indexOf('\n');
1576
+ }
1577
+ return pending;
1578
+ }
1579
+
1580
+ process.stdout.on('data', chunk => {
1581
+ appendChunk(chunk, {
1582
+ get value() { return stdoutOutput; },
1583
+ set value(nextValue) { stdoutOutput = nextValue; },
1584
+ get truncated() { return stdoutTruncated; },
1585
+ set truncated(nextValue) { stdoutTruncated = nextValue; }
1586
+ });
1587
+ stdoutPending = drainLines(chunk.toString('utf-8'), stdoutPending, emitStdoutTraceLine);
1588
+ });
1589
+ process.stderr.on('data', chunk => {
1590
+ appendChunk(chunk, {
1591
+ get value() { return stderrOutput; },
1592
+ set value(nextValue) { stderrOutput = nextValue; },
1593
+ get truncated() { return stderrTruncated; },
1594
+ set truncated(nextValue) { stderrTruncated = nextValue; }
1595
+ });
1596
+ stderrPending = drainLines(chunk.toString('utf-8'), stderrPending, emitStderrTraceLine);
1597
+ });
1598
+
1599
+ process.on('error', error => {
1600
+ state.agentRuns.delete(containerName);
1601
+ reject(error);
1602
+ });
1603
+ process.on('close', code => {
1604
+ state.agentRuns.delete(containerName);
1605
+ if (stdoutPending) {
1606
+ emitStdoutTraceLine(stdoutPending);
1607
+ stdoutPending = '';
1608
+ }
1609
+ if (stderrPending) {
1610
+ emitStderrTraceLine(stderrPending);
1611
+ stderrPending = '';
1612
+ }
1613
+ const exitCode = typeof code === 'number' ? code : 1;
1614
+ const clippedStdout = stdoutTruncated ? `${stdoutOutput}\n...[stdout-truncated]` : stdoutOutput;
1615
+ const clippedStderr = stderrTruncated ? `${stderrOutput}\n...[stderr-truncated]` : stderrOutput;
1616
+ const clippedRaw = `${clippedStdout}${clippedStdout && clippedStderr ? '\n' : ''}${clippedStderr}`;
1617
+ const extractedJsonAgentMessage = extractAgentMessageFromCodexJsonl(clippedStdout);
1618
+ const cleanOutputSource = extractedJsonAgentMessage || clippedRaw;
1619
+ const output = clipText(stripAnsi(cleanOutputSource).trim() || '(无输出)');
1620
+ resolve({
1621
+ exitCode,
1622
+ output,
1623
+ interrupted: exitCode !== 0 && runState.stopping === true
1624
+ });
1625
+ });
1626
+ });
1627
+ }
1628
+
1180
1629
  function readRequestBody(req) {
1181
1630
  return new Promise((resolve, reject) => {
1182
1631
  let body = '';
@@ -1213,6 +1662,24 @@ function sendJson(res, statusCode, payload, extraHeaders = {}) {
1213
1662
  res.end(JSON.stringify(payload));
1214
1663
  }
1215
1664
 
1665
+ function sendNdjson(res, payload) {
1666
+ res.write(`${JSON.stringify(payload)}\n`);
1667
+ }
1668
+
1669
+ function stopWebAgentRun(state, containerName) {
1670
+ const runState = state.agentRuns.get(containerName);
1671
+ if (!runState || !runState.process || runState.process.killed) {
1672
+ return false;
1673
+ }
1674
+ runState.stopping = true;
1675
+ try {
1676
+ runState.process.kill('SIGTERM');
1677
+ } catch (e) {
1678
+ return false;
1679
+ }
1680
+ return true;
1681
+ }
1682
+
1216
1683
  function sendHtml(res, statusCode, html, extraHeaders = {}) {
1217
1684
  res.writeHead(statusCode, {
1218
1685
  'Content-Type': 'text/html; charset=utf-8',
@@ -1807,72 +2274,162 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1807
2274
  return;
1808
2275
  }
1809
2276
 
1810
- const history = loadWebSessionHistory(state.webHistoryDir, containerName);
1811
- const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
1812
- if (normalizedTemplate !== history.agentPromptCommand) {
1813
- history.agentPromptCommand = normalizedTemplate;
1814
- saveWebSessionHistory(state.webHistoryDir, containerName, history);
1815
- }
1816
- if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
1817
- sendJson(res, 400, { error: '当前会话未配置 agentPromptCommand' });
2277
+ let prepared = null;
2278
+ try {
2279
+ prepared = await prepareWebAgentExecution(ctx, state, containerName, prompt);
2280
+ } catch (e) {
2281
+ sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
1818
2282
  return;
1819
2283
  }
1820
2284
 
1821
- await ensureWebContainer(ctx, state, containerName);
1822
- const agentMeta = getAgentRuntimeMeta(history);
1823
- const hasPriorConversation = hasAgentConversationHistory(history);
1824
- let resumeAttempted = false;
1825
- let resumeSucceeded = false;
1826
- let resumeError = '';
1827
- if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
1828
- resumeAttempted = true;
1829
- const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
1830
- if (resumeResult.exitCode === 0) {
1831
- resumeSucceeded = true;
1832
- } else {
1833
- resumeError = clipText(String(resumeResult.output || '(无输出)'), 1200);
1834
- }
1835
- }
1836
-
1837
- const effectivePrompt = resumeSucceeded
1838
- ? prompt
1839
- : buildAgentPromptWithHistory(history, prompt);
1840
- const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
1841
- const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
2285
+ const { history, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
1842
2286
  appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
1843
2287
  mode: 'agent',
1844
2288
  contextMode
1845
2289
  });
1846
2290
  const result = await execCommandInWebContainer(ctx, containerName, command);
1847
- appendWebSessionMessage(
1848
- state.webHistoryDir,
1849
- containerName,
1850
- 'assistant',
1851
- result.output,
1852
- {
1853
- exitCode: result.exitCode,
1854
- mode: 'agent',
1855
- contextMode,
1856
- resumeAttempted,
1857
- resumeSucceeded
1858
- }
1859
- );
1860
- patchWebSessionAgentState(state.webHistoryDir, containerName, {
1861
- agentProgram: agentMeta.agentProgram,
1862
- resumeSupported: agentMeta.resumeSupported,
1863
- lastResumeAt: resumeAttempted ? new Date().toISOString() : history.lastResumeAt || null,
1864
- lastResumeOk: resumeAttempted ? resumeSucceeded : history.lastResumeOk,
1865
- lastResumeError: resumeAttempted ? (resumeSucceeded ? '' : resumeError) : history.lastResumeError || ''
1866
- });
2291
+ finalizeWebAgentExecution(state, containerName, history, agentMeta, {
2292
+ contextMode,
2293
+ resumeAttempted,
2294
+ resumeSucceeded,
2295
+ resumeError
2296
+ }, result);
1867
2297
  sendJson(res, 200, {
1868
2298
  exitCode: result.exitCode,
1869
2299
  output: result.output,
1870
2300
  contextMode,
1871
2301
  resumeAttempted,
1872
- resumeSucceeded
2302
+ resumeSucceeded,
2303
+ interrupted: result.interrupted === true
1873
2304
  });
1874
2305
  }
1875
2306
  },
2307
+ {
2308
+ method: 'POST',
2309
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stream$/),
2310
+ handler: async match => {
2311
+ const containerName = getValidSessionName(ctx, res, match[1]);
2312
+ if (!containerName) {
2313
+ return;
2314
+ }
2315
+
2316
+ const payload = await readJsonBody(req);
2317
+ const prompt = (payload.prompt || '').trim();
2318
+ if (!prompt) {
2319
+ sendJson(res, 400, { error: 'prompt 不能为空' });
2320
+ return;
2321
+ }
2322
+ if (state.agentRuns.has(containerName)) {
2323
+ sendJson(res, 409, { error: '当前会话已有运行中的 agent 任务' });
2324
+ return;
2325
+ }
2326
+
2327
+ let prepared = null;
2328
+ try {
2329
+ prepared = await prepareWebAgentExecution(ctx, state, containerName, prompt);
2330
+ } catch (e) {
2331
+ sendJson(res, 400, { error: e && e.message ? e.message : 'Agent 执行准备失败' });
2332
+ return;
2333
+ }
2334
+
2335
+ const { history, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
2336
+ const traceLines = ['[执行过程]'];
2337
+ const traceEvents = [];
2338
+ appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
2339
+ mode: 'agent',
2340
+ contextMode
2341
+ });
2342
+
2343
+ res.writeHead(200, {
2344
+ 'Content-Type': 'application/x-ndjson; charset=utf-8',
2345
+ 'Cache-Control': 'no-store',
2346
+ 'X-Accel-Buffering': 'no'
2347
+ });
2348
+ sendNdjson(res, {
2349
+ type: 'meta',
2350
+ containerName,
2351
+ contextMode,
2352
+ resumeAttempted,
2353
+ resumeSucceeded,
2354
+ agentProgram: agentMeta.agentProgram
2355
+ });
2356
+ if (contextMode) {
2357
+ traceLines.push(`上下文模式: ${contextMode}`);
2358
+ }
2359
+ if (resumeAttempted) {
2360
+ traceLines.push(resumeSucceeded ? '会话恢复成功' : '会话恢复失败,已回退到历史注入');
2361
+ }
2362
+
2363
+ try {
2364
+ const result = await execAgentInWebContainerStream(ctx, state, containerName, command, {
2365
+ agentProgram: agentMeta.agentProgram,
2366
+ onEvent: event => {
2367
+ if (event && event.type === 'trace' && event.text) {
2368
+ traceLines.push(String(event.text));
2369
+ if (event.traceEvent && typeof event.traceEvent === 'object') {
2370
+ traceEvents.push(event.traceEvent);
2371
+ }
2372
+ }
2373
+ sendNdjson(res, event);
2374
+ }
2375
+ });
2376
+ traceLines.push(result.interrupted === true ? '[任务] 已停止' : '[任务] 已完成');
2377
+ appendWebAgentTraceMessage(state.webHistoryDir, containerName, traceLines.join('\n'), {
2378
+ traceEvents,
2379
+ contextMode,
2380
+ resumeAttempted,
2381
+ resumeSucceeded,
2382
+ interrupted: result.interrupted === true
2383
+ });
2384
+ finalizeWebAgentExecution(state, containerName, history, agentMeta, {
2385
+ contextMode,
2386
+ resumeAttempted,
2387
+ resumeSucceeded,
2388
+ resumeError
2389
+ }, result);
2390
+ sendNdjson(res, {
2391
+ type: 'result',
2392
+ exitCode: result.exitCode,
2393
+ output: result.output,
2394
+ contextMode,
2395
+ resumeAttempted,
2396
+ resumeSucceeded,
2397
+ interrupted: result.interrupted === true
2398
+ });
2399
+ } catch (e) {
2400
+ traceLines.push(`[错误] ${e && e.message ? e.message : 'Agent 执行失败'}`);
2401
+ appendWebAgentTraceMessage(state.webHistoryDir, containerName, traceLines.join('\n'), {
2402
+ traceEvents,
2403
+ contextMode,
2404
+ resumeAttempted,
2405
+ resumeSucceeded,
2406
+ interrupted: true
2407
+ });
2408
+ sendNdjson(res, {
2409
+ type: 'error',
2410
+ error: e && e.message ? e.message : 'Agent 执行失败'
2411
+ });
2412
+ } finally {
2413
+ res.end();
2414
+ }
2415
+ }
2416
+ },
2417
+ {
2418
+ method: 'POST',
2419
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent\/stop$/),
2420
+ handler: async match => {
2421
+ const containerName = getValidSessionName(ctx, res, match[1]);
2422
+ if (!containerName) {
2423
+ return;
2424
+ }
2425
+ const stopped = stopWebAgentRun(state, containerName);
2426
+ if (!stopped) {
2427
+ sendJson(res, 404, { error: '当前会话没有运行中的 agent 任务' });
2428
+ return;
2429
+ }
2430
+ sendJson(res, 200, { ok: true, stopping: true });
2431
+ }
2432
+ },
1876
2433
  {
1877
2434
  method: 'POST',
1878
2435
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/remove$/),
@@ -1971,7 +2528,8 @@ async function startWebServer(options) {
1971
2528
  webHistoryDir: options.webHistoryDir || path.join(os.homedir(), '.manyoyo', 'web-history'),
1972
2529
  webConfigPath: options.webConfigPath || getDefaultWebConfigPath(),
1973
2530
  authSessions: new Map(),
1974
- terminalSessions: new Map()
2531
+ terminalSessions: new Map(),
2532
+ agentRuns: new Map()
1975
2533
  };
1976
2534
 
1977
2535
  ensureWebHistoryDir(state.webHistoryDir);
@@ -2197,6 +2755,13 @@ async function startWebServer(options) {
2197
2755
  }
2198
2756
  }
2199
2757
  state.terminalSessions.clear();
2758
+ for (const runState of state.agentRuns.values()) {
2759
+ const child = runState && runState.process;
2760
+ if (child && !child.killed) {
2761
+ try { child.kill('SIGTERM'); } catch (e) {}
2762
+ }
2763
+ }
2764
+ state.agentRuns.clear();
2200
2765
 
2201
2766
  const closeHttp = () => {
2202
2767
  if (!server.listening) {