@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/agent-resume.js +10 -1
- package/lib/web/frontend/app.css +158 -0
- package/lib/web/frontend/app.html +4 -1
- package/lib/web/frontend/app.js +518 -27
- package/lib/web/server.js +617 -52
- package/package.json +1 -1
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 =>
|
|
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
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
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) {
|