@xcanwin/manyoyo 5.11.15 → 6.0.1

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.
Files changed (2) hide show
  1. package/lib/web/server.js +115 -28
  2. package/package.json +1 -1
package/lib/web/server.js CHANGED
@@ -34,6 +34,8 @@ const WEB_TERMINAL_MIN_ROWS = 12;
34
34
  const WEB_AGENT_CONTEXT_MAX_MESSAGES = 24;
35
35
  const WEB_AGENT_CONTEXT_MAX_CHARS = 6000;
36
36
  const WEB_AGENT_CONTEXT_PER_MESSAGE_MAX_CHARS = 600;
37
+ // 支持按 Web Agent 保存的引擎会话 ID 原生续接,避免历史文本被截断。
38
+ const NATIVE_SESSION_RESUME_PROGRAMS = new Set(['claude', 'codex', 'opencode']);
37
39
  const WEB_FILE_PREVIEW_MAX_BYTES = 512 * 1024;
38
40
  const WEB_FILE_EDIT_MAX_BYTES = 2 * 1024 * 1024;
39
41
  const WEB_AUTH_COOKIE_NAME = 'manyoyo_web_auth';
@@ -171,7 +173,8 @@ function createEmptyWebAgentSession(agentId, agentName) {
171
173
  messages: [],
172
174
  lastResumeAt: null,
173
175
  lastResumeOk: null,
174
- lastResumeError: ''
176
+ lastResumeError: '',
177
+ engineSessionId: ''
175
178
  };
176
179
  }
177
180
 
@@ -188,7 +191,8 @@ function normalizeWebAgentSessionRecord(agentId, rawAgent) {
188
191
  messages: Array.isArray(source.messages) ? source.messages : [],
189
192
  lastResumeAt: typeof source.lastResumeAt === 'string' ? source.lastResumeAt : null,
190
193
  lastResumeOk: typeof source.lastResumeOk === 'boolean' ? source.lastResumeOk : null,
191
- lastResumeError: typeof source.lastResumeError === 'string' ? source.lastResumeError : ''
194
+ lastResumeError: typeof source.lastResumeError === 'string' ? source.lastResumeError : '',
195
+ engineSessionId: typeof source.engineSessionId === 'string' ? source.engineSessionId : ''
192
196
  };
193
197
  }
194
198
 
@@ -683,8 +687,9 @@ function renderAgentPromptCommand(template, prompt) {
683
687
  return templateText.replace(/\{prompt\}/g, safePrompt);
684
688
  }
685
689
 
686
- function buildCodexAgentExecCommand(template, prompt) {
690
+ function buildCodexAgentExecCommand(template, prompt, options = {}) {
687
691
  const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
692
+ const sessionId = options && typeof options.sessionId === 'string' ? options.sessionId.trim() : '';
688
693
  const execMatch = templateText.match(
689
694
  /^((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)codex\s+exec\b/
690
695
  );
@@ -694,7 +699,13 @@ function buildCodexAgentExecCommand(template, prompt) {
694
699
  const suffix = templateText.slice(execMatch[0].length);
695
700
  const hasJson = /(?:^|\s)--json(?:\s|$)/.test(suffix);
696
701
  const injectedFlags = hasJson ? '' : ' --json';
697
- codexTemplate = `${prefix}codex exec${injectedFlags}${suffix}`;
702
+ if (sessionId) {
703
+ const promptIndex = suffix.indexOf('{prompt}');
704
+ const resumeSuffix = `${suffix.slice(0, promptIndex)}${quoteBashSingleValue(sessionId)} ${suffix.slice(promptIndex)}`;
705
+ codexTemplate = `${prefix}codex exec resume${injectedFlags}${resumeSuffix}`;
706
+ } else {
707
+ codexTemplate = `${prefix}codex exec${injectedFlags}${suffix}`;
708
+ }
698
709
  }
699
710
  return codexTemplate === templateText
700
711
  ? renderAgentPromptCommand(templateText, prompt)
@@ -718,15 +729,20 @@ function prependAgentFlags(commandText, matchPattern, flagSpecs) {
718
729
  return `${prefix}${suffix}`;
719
730
  }
720
731
 
721
- function buildClaudeAgentExecCommand(template, prompt) {
732
+ function buildClaudeAgentExecCommand(template, prompt, options = {}) {
722
733
  const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
734
+ const flagSpecs = [
735
+ { flag: '--verbose', pattern: /(?:^|\s)--verbose(?:\s|$)/ },
736
+ { flag: '--output-format stream-json', pattern: /(?:^|\s)--output-format(?:\s|$)/ }
737
+ ];
738
+ const sessionId = options && typeof options.sessionId === 'string' ? options.sessionId.trim() : '';
739
+ if (sessionId) {
740
+ flagSpecs.push({ flag: `-r ${sessionId}`, pattern: /(?:^|\s)-r(?:\s|$)/ });
741
+ }
723
742
  const claudeTemplate = prependAgentFlags(
724
743
  templateText,
725
744
  /^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)claude\b)(.*)$/,
726
- [
727
- { flag: '--verbose', pattern: /(?:^|\s)--verbose(?:\s|$)/ },
728
- { flag: '--output-format stream-json', pattern: /(?:^|\s)--output-format(?:\s|$)/ }
729
- ]
745
+ flagSpecs
730
746
  );
731
747
  return claudeTemplate === templateText
732
748
  ? renderAgentPromptCommand(templateText, prompt)
@@ -747,30 +763,38 @@ function buildGeminiAgentExecCommand(template, prompt) {
747
763
  : renderAgentPromptCommand(geminiTemplate, prompt);
748
764
  }
749
765
 
750
- function buildOpenCodeAgentExecCommand(template, prompt) {
766
+ function buildOpenCodeAgentExecCommand(template, prompt, options = {}) {
751
767
  const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
768
+ const sessionId = options && typeof options.sessionId === 'string' ? options.sessionId.trim() : '';
769
+ const flagSpecs = [
770
+ { flag: '--format json', pattern: /(?:^|\s)--format(?:\s|$)/ }
771
+ ];
772
+ if (sessionId) {
773
+ flagSpecs.push({
774
+ flag: `--session ${quoteBashSingleValue(sessionId)}`,
775
+ pattern: /(?:^|\s)(?:--session|-s)(?:\s|$)/
776
+ });
777
+ }
752
778
  const opencodeTemplate = prependAgentFlags(
753
779
  templateText,
754
- /^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)opencode\s+run\b)(.*)$/,
755
- [
756
- { flag: '--format json', pattern: /(?:^|\s)--format(?:\s|$)/ }
757
- ]
780
+ /^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)opencode\b.*?\s+run\b)(.*)$/,
781
+ flagSpecs
758
782
  );
759
783
  return opencodeTemplate === templateText
760
784
  ? renderAgentPromptCommand(templateText, prompt)
761
785
  : renderAgentPromptCommand(opencodeTemplate, prompt);
762
786
  }
763
787
 
764
- function buildWebAgentExecCommand(template, prompt, agentProgram) {
788
+ function buildWebAgentExecCommand(template, prompt, agentProgram, options = {}) {
765
789
  switch (agentProgram) {
766
790
  case 'claude':
767
- return buildClaudeAgentExecCommand(template, prompt);
791
+ return buildClaudeAgentExecCommand(template, prompt, options);
768
792
  case 'gemini':
769
793
  return buildGeminiAgentExecCommand(template, prompt);
770
794
  case 'codex':
771
- return buildCodexAgentExecCommand(template, prompt);
795
+ return buildCodexAgentExecCommand(template, prompt, options);
772
796
  case 'opencode':
773
- return buildOpenCodeAgentExecCommand(template, prompt);
797
+ return buildOpenCodeAgentExecCommand(template, prompt, options);
774
798
  default:
775
799
  break;
776
800
  }
@@ -874,7 +898,7 @@ function extractOpenCodeAgentMessage(text) {
874
898
  if (role && role !== 'assistant') {
875
899
  continue;
876
900
  }
877
- const content = collectStructuredText(message.content || payload.content || payload.text || payload);
901
+ const content = collectStructuredText(message.content || payload.content || payload.text || payload.part || payload);
878
902
  if (!content) {
879
903
  continue;
880
904
  }
@@ -905,6 +929,50 @@ function extractAgentMessageFromStructuredOutput(agentProgram, text) {
905
929
  return '';
906
930
  }
907
931
 
932
+ function extractCodexThreadId(text) {
933
+ for (const rawLine of String(text || '').split('\n')) {
934
+ const payload = parseJsonObjectLine(rawLine);
935
+ if (payload && payload.type === 'thread.started' && typeof payload.thread_id === 'string') {
936
+ return payload.thread_id.trim();
937
+ }
938
+ }
939
+ return '';
940
+ }
941
+
942
+ function extractClaudeSessionId(text) {
943
+ for (const rawLine of String(text || '').split('\n')) {
944
+ const payload = parseJsonObjectLine(rawLine);
945
+ if (payload && payload.type === 'system' && payload.subtype === 'init' && typeof payload.session_id === 'string') {
946
+ return payload.session_id.trim();
947
+ }
948
+ }
949
+ return '';
950
+ }
951
+
952
+ function extractOpenCodeSessionId(text) {
953
+ for (const rawLine of String(text || '').split('\n')) {
954
+ const payload = parseJsonObjectLine(rawLine);
955
+ const sessionId = payload && pickFirstString(payload.sessionID, payload.session_id);
956
+ if (sessionId) {
957
+ return sessionId;
958
+ }
959
+ }
960
+ return '';
961
+ }
962
+
963
+ function extractEngineSessionId(agentProgram, text) {
964
+ if (agentProgram === 'codex') {
965
+ return extractCodexThreadId(text);
966
+ }
967
+ if (agentProgram === 'claude') {
968
+ return extractClaudeSessionId(text);
969
+ }
970
+ if (agentProgram === 'opencode') {
971
+ return extractOpenCodeSessionId(text);
972
+ }
973
+ return '';
974
+ }
975
+
908
976
  function getAgentRuntimeMeta(template) {
909
977
  const normalizedTemplate = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
910
978
  const agentProgram = resolveAgentProgram(normalizedTemplate);
@@ -1603,11 +1671,19 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
1603
1671
  await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
1604
1672
  const agentMeta = getAgentRuntimeMeta(effectiveTemplate);
1605
1673
  const hasPriorConversation = hasAgentConversationHistory(agentSession);
1674
+ const useNativeSessionResume = NATIVE_SESSION_RESUME_PROGRAMS.has(agentMeta.agentProgram);
1606
1675
  let resumeAttempted = false;
1607
1676
  let resumeSucceeded = false;
1608
1677
  let resumeError = '';
1678
+ let sessionIdForRun = '';
1609
1679
 
1610
- if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
1680
+ if (useNativeSessionResume && hasPriorConversation && agentSession.engineSessionId) {
1681
+ // 已有引擎会话 ID 时直接续接;旧 Web 会话缺少 ID 时注入一次历史来建立新会话,
1682
+ // 会话 ID 由引擎自己生成,执行结束后从输出中提取(见 finalizeWebAgentExecution)。
1683
+ sessionIdForRun = agentSession.engineSessionId;
1684
+ resumeAttempted = true;
1685
+ resumeSucceeded = true;
1686
+ } else if (!useNativeSessionResume && hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
1611
1687
  resumeAttempted = true;
1612
1688
  const resumeResult = await execCommandInWebContainer(ctx, sessionRef.containerName, agentMeta.resumeCommand);
1613
1689
  if (resumeResult.exitCode === 0) {
@@ -1620,7 +1696,9 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
1620
1696
  const effectivePrompt = resumeSucceeded
1621
1697
  ? prompt
1622
1698
  : buildAgentPromptWithHistory(agentSession, prompt);
1623
- const command = buildWebAgentExecCommand(effectiveTemplate, effectivePrompt, agentMeta.agentProgram);
1699
+ const command = buildWebAgentExecCommand(effectiveTemplate, effectivePrompt, agentMeta.agentProgram, {
1700
+ sessionId: sessionIdForRun
1701
+ });
1624
1702
  const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
1625
1703
 
1626
1704
  return {
@@ -1631,7 +1709,8 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
1631
1709
  contextMode,
1632
1710
  resumeAttempted,
1633
1711
  resumeSucceeded,
1634
- resumeError
1712
+ resumeError,
1713
+ engineSessionId: sessionIdForRun
1635
1714
  };
1636
1715
  }
1637
1716
 
@@ -1644,11 +1723,16 @@ function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, m
1644
1723
  resumeSucceeded: meta.resumeSucceeded,
1645
1724
  interrupted: result.interrupted === true
1646
1725
  });
1647
- patchWebAgentSessionState(state.webHistoryDir, sessionRef, {
1726
+ const patch = {
1648
1727
  lastResumeAt: meta.resumeAttempted ? new Date().toISOString() : (agentSession.lastResumeAt || null),
1649
1728
  lastResumeOk: meta.resumeAttempted ? meta.resumeSucceeded : agentSession.lastResumeOk,
1650
1729
  lastResumeError: meta.resumeAttempted ? (meta.resumeSucceeded ? '' : meta.resumeError) : (agentSession.lastResumeError || '')
1651
- });
1730
+ };
1731
+ const engineSessionId = meta.engineSessionId || extractEngineSessionId(agentMeta.agentProgram, result.stdout);
1732
+ if (engineSessionId) {
1733
+ patch.engineSessionId = engineSessionId;
1734
+ }
1735
+ patchWebAgentSessionState(state.webHistoryDir, sessionRef, patch);
1652
1736
  }
1653
1737
 
1654
1738
  function appendWebAgentTraceMessage(webHistoryDir, sessionRefOrContainerName, content, extra = {}) {
@@ -3044,6 +3128,7 @@ async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerNa
3044
3128
  resolve({
3045
3129
  exitCode,
3046
3130
  output,
3131
+ stdout: clippedStdout,
3047
3132
  interrupted: exitCode !== 0 && runState.stopping === true
3048
3133
  });
3049
3134
  });
@@ -4056,7 +4141,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4056
4141
  return;
4057
4142
  }
4058
4143
 
4059
- const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
4144
+ const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError, engineSessionId } = prepared;
4060
4145
  appendWebSessionMessage(state.webHistoryDir, sessionRef, 'user', prompt, {
4061
4146
  mode: 'agent',
4062
4147
  contextMode
@@ -4068,7 +4153,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4068
4153
  contextMode,
4069
4154
  resumeAttempted,
4070
4155
  resumeSucceeded,
4071
- resumeError
4156
+ resumeError,
4157
+ engineSessionId
4072
4158
  }, result);
4073
4159
  sendJson(res, 200, {
4074
4160
  exitCode: result.exitCode,
@@ -4124,7 +4210,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4124
4210
  return;
4125
4211
  }
4126
4212
 
4127
- const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError } = prepared;
4213
+ const { agentSession, agentMeta, command, contextMode, resumeAttempted, resumeSucceeded, resumeError, engineSessionId } = prepared;
4128
4214
  const traceLines = ['[执行过程]'];
4129
4215
  const traceEvents = [];
4130
4216
  let streamingReplyMessageId = '';
@@ -4226,7 +4312,8 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4226
4312
  contextMode,
4227
4313
  resumeAttempted,
4228
4314
  resumeSucceeded,
4229
- resumeError
4315
+ resumeError,
4316
+ engineSessionId
4230
4317
  }, result);
4231
4318
  sendNdjson(res, {
4232
4319
  type: 'result',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.11.15",
3
+ "version": "6.0.1",
4
4
  "imageVersion": "1.9.1-common",
5
5
  "playwrightCliVersion": "0.1.14",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",