@xcanwin/manyoyo 5.7.20 → 5.8.0

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.
@@ -403,6 +403,10 @@ textarea:focus-visible {
403
403
  resize: vertical;
404
404
  }
405
405
 
406
+ .agent-template-modal {
407
+ width: min(760px, calc(100vw - 24px));
408
+ }
409
+
406
410
  .session-head {
407
411
  display: flex;
408
412
  justify-content: space-between;
@@ -1581,14 +1585,36 @@ details.trace-card > .trace-card-summary {
1581
1585
  margin-bottom: 10px;
1582
1586
  }
1583
1587
 
1588
+ .composer-toolbar-meta {
1589
+ display: inline-flex;
1590
+ align-items: center;
1591
+ gap: 10px;
1592
+ margin-left: auto;
1593
+ }
1594
+
1584
1595
  .composer-mode-switch {
1585
1596
  display: inline-flex;
1586
1597
  gap: 8px;
1587
1598
  }
1588
1599
 
1589
- .composer-toolbar-tip {
1600
+ .toolbar-mini-btn {
1601
+ padding: 7px 12px;
1602
+ font-size: 12px;
1603
+ white-space: nowrap;
1604
+ }
1605
+
1606
+ .toolbar-chip {
1607
+ display: inline-flex;
1608
+ align-items: center;
1609
+ min-height: 32px;
1610
+ padding: 7px 12px;
1611
+ border: 1px solid var(--line);
1612
+ border-radius: 10px;
1613
+ background: var(--panel-soft);
1590
1614
  color: var(--muted);
1591
1615
  font-size: 12px;
1616
+ line-height: 1;
1617
+ white-space: nowrap;
1592
1618
  }
1593
1619
 
1594
1620
  .composer-inner {
@@ -1833,8 +1859,10 @@ details.trace-card > .trace-card-summary {
1833
1859
  align-items: flex-start;
1834
1860
  }
1835
1861
 
1836
- .composer-toolbar-tip {
1837
- display: none;
1862
+ .composer-toolbar-meta {
1863
+ width: 100%;
1864
+ justify-content: space-between;
1865
+ margin-left: 0;
1838
1866
  }
1839
1867
 
1840
1868
  .composer-mode-switch {
@@ -105,7 +105,10 @@
105
105
  <button type="button" id="activityAgentBtn" class="secondary is-active">Agent</button>
106
106
  <button type="button" id="activityCommandBtn" class="secondary">命令</button>
107
107
  </div>
108
- <div class="composer-toolbar-tip">活动页支持 Agent 提示词和容器命令两种输入。</div>
108
+ <div class="composer-toolbar-meta">
109
+ <button type="button" id="agentTemplateBtn" class="secondary toolbar-mini-btn">CLI · —</button>
110
+ <span id="activityModelChip" class="toolbar-chip" aria-live="polite">模型 · —</span>
111
+ </div>
109
112
  </div>
110
113
  <div class="composer-inner">
111
114
  <textarea id="commandInput" placeholder="输入容器命令,例如: ls -la"></textarea>
@@ -220,6 +223,31 @@
220
223
  </div>
221
224
  </section>
222
225
  </div>
226
+
227
+ <div id="agentTemplateModal" class="modal-backdrop" hidden>
228
+ <section class="modal agent-template-modal" role="dialog" aria-modal="true" aria-labelledby="agentTemplateTitle">
229
+ <header class="modal-header">
230
+ <h2 id="agentTemplateTitle">设置 CLI / Agent 模板</h2>
231
+ <button type="button" id="agentTemplateCancelBtn" class="secondary">关闭</button>
232
+ </header>
233
+ <div class="modal-body">
234
+ <div id="agentTemplateTip" class="modal-tip"></div>
235
+ <label class="text-block">容器默认 agentPromptCommand
236
+ <textarea id="containerAgentPromptEditor" placeholder="例如 codex exec --skip-git-repo-check {prompt}"></textarea>
237
+ </label>
238
+ <div id="agentTemplateOverrideGroup">
239
+ <label class="text-block">当前 AGENT 覆盖模板
240
+ <textarea id="agentPromptOverrideEditor" placeholder="留空则继承容器默认模板"></textarea>
241
+ </label>
242
+ </div>
243
+ <div id="agentTemplateError" class="modal-error" hidden></div>
244
+ </div>
245
+ <footer class="modal-footer">
246
+ <button type="button" id="agentTemplateResetBtn" class="secondary">恢复当前值</button>
247
+ <button type="button" id="agentTemplateSaveBtn">保存</button>
248
+ </footer>
249
+ </section>
250
+ </div>
223
251
  </div>
224
252
 
225
253
  <script src="/app/vendor/xterm.js"></script>
@@ -50,14 +50,17 @@
50
50
  mobileActionsOpen: false,
51
51
  configModalOpen: false,
52
52
  createModalOpen: false,
53
+ agentTemplateModalOpen: false,
53
54
  configLoading: false,
54
55
  configSaving: false,
55
56
  createLoading: false,
56
57
  createSubmitting: false,
58
+ agentTemplateSaving: false,
57
59
  configSnapshot: null,
58
60
  sessionDetail: null,
59
61
  sessionDetailError: '',
60
62
  sessionDetailRequestId: 0,
63
+ agentTemplateError: '',
61
64
  createAgentPromptAuto: false,
62
65
  createContainerPathBase: '',
63
66
  createDefaults: null,
@@ -167,6 +170,8 @@
167
170
  const activeMeta = document.getElementById('activeMeta');
168
171
  const activityCommandBtn = document.getElementById('activityCommandBtn');
169
172
  const activityAgentBtn = document.getElementById('activityAgentBtn');
173
+ const agentTemplateBtn = document.getElementById('agentTemplateBtn');
174
+ const activityModelChip = document.getElementById('activityModelChip');
170
175
  const messagesNode = document.getElementById('messages');
171
176
  const terminalPanel = document.getElementById('terminalPanel');
172
177
  const detailPanel = document.getElementById('detailPanel');
@@ -182,6 +187,15 @@
182
187
  const sendState = document.getElementById('sendState');
183
188
  const sendBtn = document.getElementById('sendBtn');
184
189
  const stopBtn = document.getElementById('stopBtn');
190
+ const agentTemplateModal = document.getElementById('agentTemplateModal');
191
+ const agentTemplateTip = document.getElementById('agentTemplateTip');
192
+ const containerAgentPromptEditor = document.getElementById('containerAgentPromptEditor');
193
+ const agentTemplateOverrideGroup = document.getElementById('agentTemplateOverrideGroup');
194
+ const agentPromptOverrideEditor = document.getElementById('agentPromptOverrideEditor');
195
+ const agentTemplateError = document.getElementById('agentTemplateError');
196
+ const agentTemplateCancelBtn = document.getElementById('agentTemplateCancelBtn');
197
+ const agentTemplateResetBtn = document.getElementById('agentTemplateResetBtn');
198
+ const agentTemplateSaveBtn = document.getElementById('agentTemplateSaveBtn');
185
199
  const refreshBtn = document.getElementById('refreshBtn');
186
200
  const removeBtn = document.getElementById('removeBtn');
187
201
  const removeAllBtn = document.getElementById('removeAllBtn');
@@ -192,6 +206,8 @@
192
206
  const TERMINAL_MIN_ROWS = 12;
193
207
  const TERMINAL_DEFAULT_COLS = 120;
194
208
  const TERMINAL_DEFAULT_ROWS = 36;
209
+ const WEB_SESSION_KEY_SEPARATOR = '~';
210
+ const WEB_DEFAULT_AGENT_ID = 'default';
195
211
  const YOLO_COMMAND_MAP = {
196
212
  claude: 'IS_SANDBOX=1 claude --dangerously-skip-permissions',
197
213
  cc: 'IS_SANDBOX=1 claude --dangerously-skip-permissions',
@@ -1055,6 +1071,133 @@
1055
1071
  }) || null;
1056
1072
  }
1057
1073
 
1074
+ function parseSessionKey(sessionName) {
1075
+ const raw = String(sessionName || '').trim();
1076
+ if (!raw) {
1077
+ return {
1078
+ containerName: '',
1079
+ agentId: WEB_DEFAULT_AGENT_ID
1080
+ };
1081
+ }
1082
+ const separatorIndex = raw.indexOf(WEB_SESSION_KEY_SEPARATOR);
1083
+ if (separatorIndex === -1) {
1084
+ return {
1085
+ containerName: raw,
1086
+ agentId: WEB_DEFAULT_AGENT_ID
1087
+ };
1088
+ }
1089
+ return {
1090
+ containerName: raw.slice(0, separatorIndex),
1091
+ agentId: raw.slice(separatorIndex + 1) || WEB_DEFAULT_AGENT_ID
1092
+ };
1093
+ }
1094
+
1095
+ function isActiveAgentOverrideEditable() {
1096
+ const sessionRef = parseSessionKey(state.active);
1097
+ return Boolean(state.active && sessionRef.agentId && sessionRef.agentId !== WEB_DEFAULT_AGENT_ID);
1098
+ }
1099
+
1100
+ function showAgentTemplateError(message) {
1101
+ state.agentTemplateError = String(message || '').trim();
1102
+ if (!agentTemplateError) {
1103
+ return;
1104
+ }
1105
+ agentTemplateError.hidden = !state.agentTemplateError;
1106
+ agentTemplateError.textContent = state.agentTemplateError;
1107
+ }
1108
+
1109
+ function fillAgentTemplateForm(detail) {
1110
+ const currentDetail = detail && typeof detail === 'object' ? detail : {};
1111
+ if (containerAgentPromptEditor) {
1112
+ containerAgentPromptEditor.value = currentDetail.containerAgentPromptCommand || '';
1113
+ }
1114
+ if (agentPromptOverrideEditor) {
1115
+ agentPromptOverrideEditor.value = currentDetail.agentPromptCommandOverride || '';
1116
+ }
1117
+ if (agentTemplateOverrideGroup) {
1118
+ agentTemplateOverrideGroup.hidden = !isActiveAgentOverrideEditable();
1119
+ }
1120
+ if (agentTemplateTip) {
1121
+ const sessionName = currentDetail.agentName || state.active || '当前会话';
1122
+ const sourceMap = {
1123
+ agent: '当前 AGENT 覆盖',
1124
+ container: '容器默认模板',
1125
+ inferred: '从容器启动命令推导',
1126
+ none: '未配置'
1127
+ };
1128
+ const sourceLabel = sourceMap[currentDetail.agentPromptSource] || '未配置';
1129
+ const effectiveTemplate = currentDetail.agentPromptCommand || '—';
1130
+ agentTemplateTip.textContent = `${sessionName} · 当前生效来源:${sourceLabel} · 生效模板:${effectiveTemplate}`;
1131
+ }
1132
+ showAgentTemplateError('');
1133
+ }
1134
+
1135
+ async function ensureActiveSessionDetail() {
1136
+ if (!state.active) {
1137
+ return null;
1138
+ }
1139
+ if (state.sessionDetail && state.active === (state.sessionDetail.name || state.active)) {
1140
+ return state.sessionDetail;
1141
+ }
1142
+ await loadSessionDetailForSession(state.active);
1143
+ return state.sessionDetail;
1144
+ }
1145
+
1146
+ async function openAgentTemplateModal() {
1147
+ if (!state.active || state.agentTemplateSaving) {
1148
+ return;
1149
+ }
1150
+ const detail = await ensureActiveSessionDetail();
1151
+ if (!detail) {
1152
+ alert(state.sessionDetailError || '当前会话详情暂时不可用');
1153
+ return;
1154
+ }
1155
+ state.agentTemplateModalOpen = true;
1156
+ fillAgentTemplateForm(detail);
1157
+ syncUi();
1158
+ if (containerAgentPromptEditor) {
1159
+ containerAgentPromptEditor.focus();
1160
+ }
1161
+ }
1162
+
1163
+ function closeAgentTemplateModal() {
1164
+ state.agentTemplateModalOpen = false;
1165
+ showAgentTemplateError('');
1166
+ }
1167
+
1168
+ function resetAgentTemplateModal() {
1169
+ fillAgentTemplateForm(state.sessionDetail || {});
1170
+ }
1171
+
1172
+ async function saveAgentTemplateModal() {
1173
+ if (!state.active || state.agentTemplateSaving) {
1174
+ return;
1175
+ }
1176
+ state.agentTemplateSaving = true;
1177
+ showAgentTemplateError('');
1178
+ syncUi();
1179
+ try {
1180
+ const payload = {
1181
+ containerAgentPromptCommand: containerAgentPromptEditor ? String(containerAgentPromptEditor.value || '').trim() : ''
1182
+ };
1183
+ if (isActiveAgentOverrideEditable() && agentPromptOverrideEditor) {
1184
+ payload.agentPromptCommandOverride = String(agentPromptOverrideEditor.value || '').trim();
1185
+ }
1186
+ const data = await api('/api/sessions/' + encodeURIComponent(state.active) + '/agent-template', {
1187
+ method: 'PUT',
1188
+ body: JSON.stringify(payload)
1189
+ });
1190
+ state.sessionDetail = data && data.detail ? data.detail : state.sessionDetail;
1191
+ await refreshSessionsSilent({ preferredName: state.active });
1192
+ closeAgentTemplateModal();
1193
+ } catch (e) {
1194
+ showAgentTemplateError(e && e.message ? e.message : '保存失败');
1195
+ } finally {
1196
+ state.agentTemplateSaving = false;
1197
+ syncUi();
1198
+ }
1199
+ }
1200
+
1058
1201
  function isAgentRunActiveForSession(sessionName) {
1059
1202
  return Boolean(
1060
1203
  state.agentRun
@@ -1078,6 +1221,24 @@
1078
1221
  return Boolean(active && active.agentEnabled);
1079
1222
  }
1080
1223
 
1224
+ function resolveToolbarCliLabel() {
1225
+ const activeSession = getActiveSession();
1226
+ const detail = state.sessionDetail && state.active ? state.sessionDetail : null;
1227
+ const agentProgram = String(
1228
+ (detail && detail.agentProgram)
1229
+ || (activeSession && activeSession.agentProgram)
1230
+ || ''
1231
+ ).trim();
1232
+ return agentProgram || '未配置';
1233
+ }
1234
+
1235
+ function resolveToolbarModelLabel() {
1236
+ if (!state.active) {
1237
+ return '—';
1238
+ }
1239
+ return '自动';
1240
+ }
1241
+
1081
1242
  function buildActiveMeta(session) {
1082
1243
  if (!session) {
1083
1244
  return '会话不可用';
@@ -1566,6 +1727,13 @@
1566
1727
  let resumeStatusDetail = detail.resumeSupported
1567
1728
  ? '支持 resume,但当前会话还没有最近一次执行记录。'
1568
1729
  : '当前 Agent 程序或模板不支持 resume。';
1730
+ const templateSourceMap = {
1731
+ agent: '当前 AGENT 覆盖',
1732
+ container: '容器默认模板',
1733
+ inferred: '从启动命令推导',
1734
+ none: '未配置'
1735
+ };
1736
+ const templateSourceLabel = templateSourceMap[detail.agentPromptSource] || '未配置';
1569
1737
  if (detail.lastResumeOk === true) {
1570
1738
  resumeStatusValue = '最近成功';
1571
1739
  resumeStatusTone = 'ok';
@@ -1596,6 +1764,7 @@
1596
1764
  commandEntries.push({ label: '启动命令', value: applied.defaultCommand || '—' });
1597
1765
  }
1598
1766
  commandEntries.push({ label: 'Agent 模板', value: detail.agentPromptCommand || '—' });
1767
+ commandEntries.push({ label: '模板来源', value: templateSourceLabel });
1599
1768
  commandEntries.push({ label: 'yolo', value: applied.yolo || '—' });
1600
1769
 
1601
1770
  if (detailSummary) {
@@ -1611,6 +1780,7 @@
1611
1780
  renderKeyValueCard(detailSummary, 'Agent 运行', [
1612
1781
  { label: '已启用', value: detail.agentEnabled ? '是' : '否', tone: detail.agentEnabled ? 'ok' : 'warn' },
1613
1782
  { label: '程序', value: detail.agentProgram || '—' },
1783
+ { label: '模板来源', value: templateSourceLabel },
1614
1784
  { label: '支持 resume', value: detail.resumeSupported ? '是' : '否', tone: detail.resumeSupported ? 'ok' : 'warn' },
1615
1785
  { label: '最近 resume', value: lastResumeText },
1616
1786
  { label: '最近结果', value: detail.lastResumeOk == null ? '暂无' : (detail.lastResumeOk ? '成功' : '失败'), tone: detail.lastResumeOk == null ? 'info' : (detail.lastResumeOk ? 'ok' : 'danger') }
@@ -1728,6 +1898,18 @@
1728
1898
  activityAgentBtn.classList.toggle('is-active', agentMode);
1729
1899
  activityAgentBtn.setAttribute('aria-pressed', agentMode ? 'true' : 'false');
1730
1900
  }
1901
+ if (agentTemplateBtn) {
1902
+ agentTemplateBtn.textContent = `CLI · ${resolveToolbarCliLabel()}`;
1903
+ agentTemplateBtn.title = state.active
1904
+ ? '查看或修改当前会话的 CLI 模板'
1905
+ : '请先选择会话';
1906
+ }
1907
+ if (activityModelChip) {
1908
+ activityModelChip.textContent = `模型 · ${resolveToolbarModelLabel()}`;
1909
+ activityModelChip.title = state.active
1910
+ ? '当前版本暂不单独配置模型,默认跟随 CLI 或容器内配置'
1911
+ : '请先选择会话';
1912
+ }
1731
1913
  if (viewActivityBtn) viewActivityBtn.classList.toggle('is-active', activityTab);
1732
1914
  if (viewTerminalBtn) viewTerminalBtn.classList.toggle('is-active', terminalTab);
1733
1915
  if (viewDetailBtn) viewDetailBtn.classList.toggle('is-active', detailTab);
@@ -1764,6 +1946,9 @@
1764
1946
  if (stopBtn) {
1765
1947
  stopBtn.disabled = !activityTab || !agentMode || !activeAgentRunning || state.agentRun.stopping;
1766
1948
  }
1949
+ if (agentTemplateBtn) {
1950
+ agentTemplateBtn.disabled = !state.active || state.agentTemplateSaving;
1951
+ }
1767
1952
  commandInput.disabled = !activityTab || !state.active || (agentMode && !agentEnabled);
1768
1953
  if (commandInput) {
1769
1954
  commandInput.placeholder = agentMode
@@ -1823,9 +2008,21 @@
1823
2008
  if (directoryPickerModal) {
1824
2009
  directoryPickerModal.hidden = !state.directoryPicker.open;
1825
2010
  }
2011
+ if (agentTemplateModal) {
2012
+ agentTemplateModal.hidden = !state.agentTemplateModalOpen;
2013
+ }
2014
+ if (agentTemplateSaveBtn) {
2015
+ agentTemplateSaveBtn.disabled = state.agentTemplateSaving || !state.active;
2016
+ }
2017
+ if (agentTemplateResetBtn) {
2018
+ agentTemplateResetBtn.disabled = state.agentTemplateSaving;
2019
+ }
2020
+ if (agentTemplateCancelBtn) {
2021
+ agentTemplateCancelBtn.disabled = state.agentTemplateSaving;
2022
+ }
1826
2023
  document.body.classList.toggle(
1827
2024
  'modal-open',
1828
- state.configModalOpen || state.createModalOpen || state.directoryPicker.open
2025
+ state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen
1829
2026
  );
1830
2027
  if (!state.active) {
1831
2028
  sendState.textContent = '未选择会话';
@@ -3009,6 +3206,9 @@
3009
3206
  return;
3010
3207
  }
3011
3208
  state.sessionDetail = data && data.detail ? data.detail : null;
3209
+ if (state.agentTemplateModalOpen && targetSession === state.active) {
3210
+ fillAgentTemplateForm(state.sessionDetail || {});
3211
+ }
3012
3212
  } catch (e) {
3013
3213
  if (requestId !== state.sessionDetailRequestId) {
3014
3214
  return;
@@ -3296,6 +3496,14 @@
3296
3496
  });
3297
3497
  }
3298
3498
 
3499
+ if (agentTemplateBtn) {
3500
+ agentTemplateBtn.addEventListener('click', function () {
3501
+ openAgentTemplateModal().catch(function (e) {
3502
+ alert(e && e.message ? e.message : '加载 Agent 模板失败');
3503
+ });
3504
+ });
3505
+ }
3506
+
3299
3507
  if (configCancelBtn) {
3300
3508
  configCancelBtn.addEventListener('click', function () {
3301
3509
  closeConfigModal();
@@ -3360,6 +3568,25 @@
3360
3568
  });
3361
3569
  }
3362
3570
 
3571
+ if (agentTemplateCancelBtn) {
3572
+ agentTemplateCancelBtn.addEventListener('click', function () {
3573
+ closeAgentTemplateModal();
3574
+ syncUi();
3575
+ });
3576
+ }
3577
+
3578
+ if (agentTemplateResetBtn) {
3579
+ agentTemplateResetBtn.addEventListener('click', function () {
3580
+ resetAgentTemplateModal();
3581
+ });
3582
+ }
3583
+
3584
+ if (agentTemplateSaveBtn) {
3585
+ agentTemplateSaveBtn.addEventListener('click', function () {
3586
+ saveAgentTemplateModal();
3587
+ });
3588
+ }
3589
+
3363
3590
  if (createRun) {
3364
3591
  createRun.addEventListener('change', function () {
3365
3592
  applyCurrentRunDefaults();
@@ -3686,6 +3913,15 @@
3686
3913
  });
3687
3914
  }
3688
3915
 
3916
+ if (agentTemplateModal) {
3917
+ agentTemplateModal.addEventListener('click', function (event) {
3918
+ if (event.target === agentTemplateModal && !state.agentTemplateSaving) {
3919
+ closeAgentTemplateModal();
3920
+ syncUi();
3921
+ }
3922
+ });
3923
+ }
3924
+
3689
3925
  window.addEventListener('keydown', function (event) {
3690
3926
  if (event.key === 'Escape' && state.configModalOpen) {
3691
3927
  closeConfigModal();
@@ -3698,6 +3934,10 @@
3698
3934
  if (event.key === 'Escape' && state.directoryPicker.open) {
3699
3935
  closeDirectoryPicker();
3700
3936
  }
3937
+ if (event.key === 'Escape' && state.agentTemplateModalOpen) {
3938
+ closeAgentTemplateModal();
3939
+ syncUi();
3940
+ }
3701
3941
  if (event.key === 'Escape' && state.mobileSidebarOpen) {
3702
3942
  closeMobileSessionPanel();
3703
3943
  }
package/lib/web/server.js CHANGED
@@ -137,6 +137,7 @@ function createEmptyWebAgentSession(agentId, agentName) {
137
137
  return {
138
138
  agentId,
139
139
  agentName: normalizeWebAgentName(agentId, agentName),
140
+ agentPromptCommand: '',
140
141
  updatedAt: null,
141
142
  messages: [],
142
143
  lastResumeAt: null,
@@ -150,6 +151,9 @@ function normalizeWebAgentSessionRecord(agentId, rawAgent) {
150
151
  return {
151
152
  agentId,
152
153
  agentName: normalizeWebAgentName(agentId, source.agentName),
154
+ agentPromptCommand: typeof source.agentPromptCommand === 'string'
155
+ ? normalizeAgentPromptCommandTemplate(source.agentPromptCommand, `agents.${agentId}.agentPromptCommand`)
156
+ : '',
153
157
  updatedAt: typeof source.updatedAt === 'string' ? source.updatedAt : null,
154
158
  messages: Array.isArray(source.messages) ? source.messages : [],
155
159
  lastResumeAt: typeof source.lastResumeAt === 'string' ? source.lastResumeAt : null,
@@ -253,7 +257,7 @@ function saveWebSessionHistory(webHistoryDir, containerName, history) {
253
257
  ensureWebHistoryDir(webHistoryDir);
254
258
  const filePath = getWebHistoryFile(webHistoryDir, containerName);
255
259
  const normalized = normalizeWebHistoryRecord(containerName, history);
256
- const runtimeMeta = getAgentRuntimeMeta(normalized);
260
+ const runtimeMeta = getAgentRuntimeMeta(normalized.agentPromptCommand || '');
257
261
  const defaultAgent = getWebAgentSession(normalized, WEB_DEFAULT_AGENT_ID) || createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID);
258
262
  const legacyCompatible = {
259
263
  ...normalized,
@@ -383,6 +387,81 @@ function setWebSessionAgentPromptCommand(webHistoryDir, containerName, agentProm
383
387
  saveWebSessionHistory(webHistoryDir, containerName, history);
384
388
  }
385
389
 
390
+ function setWebAgentSessionPromptCommand(webHistoryDir, sessionRefOrContainerName, agentPromptCommand) {
391
+ const sessionRef = typeof sessionRefOrContainerName === 'string'
392
+ ? { containerName: sessionRefOrContainerName, agentId: WEB_DEFAULT_AGENT_ID }
393
+ : sessionRefOrContainerName;
394
+ if (sessionRef.agentId === WEB_DEFAULT_AGENT_ID) {
395
+ throw new Error('默认 AGENT 请直接修改容器级 agentPromptCommand');
396
+ }
397
+ const history = loadWebSessionHistory(webHistoryDir, sessionRef.containerName);
398
+ const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true });
399
+ agentSession.agentPromptCommand = normalizeAgentPromptCommandTemplate(
400
+ agentPromptCommand,
401
+ `agents.${sessionRef.agentId}.agentPromptCommand`
402
+ );
403
+ saveWebSessionHistory(webHistoryDir, sessionRef.containerName, history);
404
+ }
405
+
406
+ function deriveAgentPromptCommandFromDefaultCommand(defaultCommand) {
407
+ const normalizedCommand = String(defaultCommand || '').trim();
408
+ if (!normalizedCommand) {
409
+ return '';
410
+ }
411
+ try {
412
+ return normalizeAgentPromptCommandTemplate(
413
+ resolveAgentPromptCommandTemplate(normalizedCommand),
414
+ 'agentPromptCommand'
415
+ );
416
+ } catch (e) {
417
+ return '';
418
+ }
419
+ }
420
+
421
+ function resolveEffectiveSessionAgentPromptCommand(history, defaultCommand) {
422
+ return resolveEffectiveAgentPromptCommandForSession(history, WEB_DEFAULT_AGENT_ID, defaultCommand);
423
+ }
424
+
425
+ function resolveEffectiveAgentPromptCommandForSession(history, agentId, defaultCommand) {
426
+ const sessionHistory = history && typeof history === 'object' ? history : {};
427
+ const requestedAgentId = String(agentId || WEB_DEFAULT_AGENT_ID).trim() || WEB_DEFAULT_AGENT_ID;
428
+ const agentSession = getWebAgentSession(sessionHistory, requestedAgentId);
429
+ const agentTemplate = agentSession && typeof agentSession.agentPromptCommand === 'string'
430
+ ? normalizeAgentPromptCommandTemplate(agentSession.agentPromptCommand, `agents.${requestedAgentId}.agentPromptCommand`)
431
+ : '';
432
+ if (isAgentPromptCommandEnabled(agentTemplate)) {
433
+ return agentTemplate;
434
+ }
435
+ const historyTemplate = typeof sessionHistory.agentPromptCommand === 'string'
436
+ ? normalizeAgentPromptCommandTemplate(sessionHistory.agentPromptCommand, 'agentPromptCommand')
437
+ : '';
438
+ if (isAgentPromptCommandEnabled(historyTemplate)) {
439
+ return historyTemplate;
440
+ }
441
+ return deriveAgentPromptCommandFromDefaultCommand(defaultCommand);
442
+ }
443
+
444
+ function getEffectiveAgentPromptCommandSource(history, agentId, defaultCommand) {
445
+ const sessionHistory = history && typeof history === 'object' ? history : {};
446
+ const requestedAgentId = String(agentId || WEB_DEFAULT_AGENT_ID).trim() || WEB_DEFAULT_AGENT_ID;
447
+ const agentSession = getWebAgentSession(sessionHistory, requestedAgentId);
448
+ const agentTemplate = agentSession && typeof agentSession.agentPromptCommand === 'string'
449
+ ? normalizeAgentPromptCommandTemplate(agentSession.agentPromptCommand, `agents.${requestedAgentId}.agentPromptCommand`)
450
+ : '';
451
+ if (isAgentPromptCommandEnabled(agentTemplate)) {
452
+ return 'agent';
453
+ }
454
+ const historyTemplate = typeof sessionHistory.agentPromptCommand === 'string'
455
+ ? normalizeAgentPromptCommandTemplate(sessionHistory.agentPromptCommand, 'agentPromptCommand')
456
+ : '';
457
+ if (isAgentPromptCommandEnabled(historyTemplate)) {
458
+ return 'container';
459
+ }
460
+ return isAgentPromptCommandEnabled(deriveAgentPromptCommandFromDefaultCommand(defaultCommand))
461
+ ? 'inferred'
462
+ : 'none';
463
+ }
464
+
386
465
  function patchWebSessionHistory(webHistoryDir, containerName, patch) {
387
466
  const history = loadWebSessionHistory(webHistoryDir, containerName);
388
467
  if (!patch || typeof patch !== 'object') {
@@ -694,10 +773,9 @@ function extractAgentMessageFromStructuredOutput(agentProgram, text) {
694
773
  return '';
695
774
  }
696
775
 
697
- function getAgentRuntimeMeta(history) {
698
- const sessionHistory = history && typeof history === 'object' ? history : {};
699
- const template = normalizeAgentPromptCommandTemplate(sessionHistory.agentPromptCommand, 'agentPromptCommand');
700
- const agentProgram = resolveAgentProgram(template);
776
+ function getAgentRuntimeMeta(template) {
777
+ const normalizedTemplate = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
778
+ const agentProgram = resolveAgentProgram(normalizedTemplate);
701
779
  const resumeCommand = buildAgentResumeCommand(agentProgram);
702
780
  return {
703
781
  agentProgram: agentProgram || '',
@@ -1362,17 +1440,34 @@ function prepareCodexTraceEvent(payload) {
1362
1440
  async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
1363
1441
  const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
1364
1442
  const agentSession = getWebAgentSession(history, sessionRef.agentId, { create: true });
1365
- const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
1366
- if (normalizedTemplate !== history.agentPromptCommand) {
1367
- history.agentPromptCommand = normalizedTemplate;
1443
+ const containerMap = listWebManyoyoContainers(ctx);
1444
+ const containerInfo = containerMap[sessionRef.containerName] || {};
1445
+ const normalizedContainerTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
1446
+ if (normalizedContainerTemplate !== history.agentPromptCommand) {
1447
+ history.agentPromptCommand = normalizedContainerTemplate;
1368
1448
  saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
1369
1449
  }
1370
- if (!isAgentPromptCommandEnabled(history.agentPromptCommand)) {
1450
+ if (agentSession && typeof agentSession.agentPromptCommand === 'string') {
1451
+ const normalizedAgentTemplate = normalizeAgentPromptCommandTemplate(
1452
+ agentSession.agentPromptCommand,
1453
+ `agents.${sessionRef.agentId}.agentPromptCommand`
1454
+ );
1455
+ if (normalizedAgentTemplate !== agentSession.agentPromptCommand) {
1456
+ agentSession.agentPromptCommand = normalizedAgentTemplate;
1457
+ saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
1458
+ }
1459
+ }
1460
+ const effectiveTemplate = resolveEffectiveAgentPromptCommandForSession(
1461
+ history,
1462
+ sessionRef.agentId,
1463
+ containerInfo.defaultCommand
1464
+ );
1465
+ if (!isAgentPromptCommandEnabled(effectiveTemplate)) {
1371
1466
  throw new Error('当前会话未配置 agentPromptCommand');
1372
1467
  }
1373
1468
 
1374
1469
  await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
1375
- const agentMeta = getAgentRuntimeMeta(history);
1470
+ const agentMeta = getAgentRuntimeMeta(effectiveTemplate);
1376
1471
  const hasPriorConversation = hasAgentConversationHistory(agentSession);
1377
1472
  let resumeAttempted = false;
1378
1473
  let resumeSucceeded = false;
@@ -1391,7 +1486,7 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
1391
1486
  const effectivePrompt = resumeSucceeded
1392
1487
  ? prompt
1393
1488
  : buildAgentPromptWithHistory(agentSession, prompt);
1394
- const command = buildWebAgentExecCommand(history.agentPromptCommand, effectivePrompt, agentMeta.agentProgram);
1489
+ const command = buildWebAgentExecCommand(effectiveTemplate, effectivePrompt, agentMeta.agentProgram);
1395
1490
  const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
1396
1491
 
1397
1492
  return {
@@ -2204,11 +2299,23 @@ function listWebManyoyoContainers(ctx) {
2204
2299
  if (!imageName.includes('manyoyo') && !name.startsWith('manyoyo-') && !name.startsWith('my-')) {
2205
2300
  return;
2206
2301
  }
2302
+ let defaultCommand = '';
2303
+ try {
2304
+ defaultCommand = String(
2305
+ ctx.dockerExecArgs(
2306
+ ['inspect', '-f', '{{index .Config.Labels "manyoyo.default_cmd"}}', name],
2307
+ { ignoreError: true }
2308
+ ) || ''
2309
+ ).trim();
2310
+ } catch (e) {
2311
+ defaultCommand = '';
2312
+ }
2207
2313
  map[name] = {
2208
2314
  name,
2209
2315
  status: status || 'unknown',
2210
2316
  image: imageName,
2211
- createdAt: estimateStartTimeFromStatus(status)
2317
+ createdAt: estimateStartTimeFromStatus(status),
2318
+ defaultCommand
2212
2319
  };
2213
2320
  });
2214
2321
 
@@ -2588,7 +2695,6 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
2588
2695
  const containerName = sessionRef && sessionRef.containerName ? sessionRef.containerName : '';
2589
2696
  const agentId = sessionRef && sessionRef.agentId ? sessionRef.agentId : WEB_DEFAULT_AGENT_ID;
2590
2697
  const history = loadWebSessionHistory(state.webHistoryDir, containerName);
2591
- const agentMeta = getAgentRuntimeMeta(history);
2592
2698
  const agentSession = getWebAgentSession(history, agentId)
2593
2699
  || (agentId === WEB_DEFAULT_AGENT_ID ? createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID) : null);
2594
2700
  if (!agentSession) {
@@ -2596,10 +2702,15 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
2596
2702
  }
2597
2703
  const latestMessage = agentSession.messages.length ? agentSession.messages[agentSession.messages.length - 1] : null;
2598
2704
  const containerInfo = containerMap[containerName] || {};
2705
+ const effectiveAgentPromptCommand = resolveEffectiveAgentPromptCommandForSession(history, agentId, containerInfo.defaultCommand);
2706
+ const agentMeta = getAgentRuntimeMeta(effectiveAgentPromptCommand);
2707
+ const effectiveAgentProgram = agentMeta.agentProgram || resolveAgentProgram(effectiveAgentPromptCommand);
2708
+ const effectiveResumeSupported = agentMeta.resumeSupported || Boolean(buildAgentResumeCommand(effectiveAgentProgram));
2599
2709
  const applied = history.applied && typeof history.applied === 'object' && !Array.isArray(history.applied)
2600
2710
  ? history.applied
2601
2711
  : buildSessionFallbackApplied(ctx, state, containerName, history, {
2602
- status: containerInfo.status || 'history'
2712
+ status: containerInfo.status || 'history',
2713
+ defaultCommand: containerInfo.defaultCommand || ''
2603
2714
  });
2604
2715
  const updatedAt = agentSession.updatedAt || history.updatedAt || (latestMessage && latestMessage.timestamp) || containerInfo.createdAt || null;
2605
2716
  return {
@@ -2611,9 +2722,9 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
2611
2722
  image: containerInfo.image || '',
2612
2723
  updatedAt,
2613
2724
  messageCount: agentSession.messages.length,
2614
- agentEnabled: isAgentPromptCommandEnabled(history.agentPromptCommand),
2615
- agentProgram: agentMeta.agentProgram,
2616
- resumeSupported: agentMeta.resumeSupported,
2725
+ agentEnabled: isAgentPromptCommandEnabled(effectiveAgentPromptCommand),
2726
+ agentProgram: effectiveAgentProgram || '',
2727
+ resumeSupported: effectiveResumeSupported,
2617
2728
  hostPath: applied.hostPath || '',
2618
2729
  containerPath: applied.containerPath || ''
2619
2730
  };
@@ -2622,14 +2733,18 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
2622
2733
  function buildSessionFallbackApplied(ctx, state, name, history, summary) {
2623
2734
  const snapshot = readWebConfigSnapshot(state.webConfigPath);
2624
2735
  const defaults = buildConfigDefaults(ctx, snapshot.parseError ? {} : snapshot.parsed);
2625
- const effectiveAgentPromptCommand = history.agentPromptCommand || defaults.agentPromptCommand || '';
2626
- const effectiveAgentProgram = resolveAgentProgram(effectiveAgentPromptCommand) || '';
2627
- const effectiveResumeSupported = Boolean(buildAgentResumeCommand(effectiveAgentProgram));
2628
- const defaultCommand = buildDefaultCommand(
2736
+ const configuredDefaultCommand = buildDefaultCommand(
2629
2737
  defaults.shellPrefix,
2630
2738
  defaults.shell,
2631
2739
  defaults.shellSuffix
2632
2740
  ) || buildStaticContainerRuntime(ctx, name).defaultCommand;
2741
+ const defaultCommand = pickFirstString(
2742
+ summary && summary.defaultCommand,
2743
+ configuredDefaultCommand
2744
+ );
2745
+ const effectiveAgentPromptCommand = resolveEffectiveSessionAgentPromptCommand(history, defaultCommand);
2746
+ const effectiveAgentProgram = resolveAgentProgram(effectiveAgentPromptCommand) || '';
2747
+ const effectiveResumeSupported = Boolean(buildAgentResumeCommand(effectiveAgentProgram));
2633
2748
 
2634
2749
  return {
2635
2750
  containerName: name,
@@ -2656,7 +2771,12 @@ function buildSessionFallbackApplied(ctx, state, name, history, summary) {
2656
2771
  function buildSessionDetail(ctx, state, containerMap, name) {
2657
2772
  const sessionRef = typeof name === 'string' ? parseWebSessionKey(name) : name;
2658
2773
  const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
2659
- const normalizedTemplate = normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand');
2774
+ const containerInfo = containerMap[sessionRef.containerName] || {};
2775
+ const normalizedTemplate = resolveEffectiveAgentPromptCommandForSession(
2776
+ history,
2777
+ sessionRef.agentId,
2778
+ containerInfo.defaultCommand
2779
+ );
2660
2780
  const summary = buildSessionSummary(ctx, state, containerMap, sessionRef);
2661
2781
  const agentSession = getWebAgentSession(history, sessionRef.agentId)
2662
2782
  || (sessionRef.agentId === WEB_DEFAULT_AGENT_ID ? createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID) : null);
@@ -2677,6 +2797,14 @@ function buildSessionDetail(ctx, state, containerMap, name) {
2677
2797
  latestRole: latestMessage && latestMessage.role ? String(latestMessage.role) : '',
2678
2798
  latestTimestamp: latestMessage && latestMessage.timestamp ? latestMessage.timestamp : summary.updatedAt,
2679
2799
  agentPromptCommand: normalizedTemplate || '',
2800
+ containerAgentPromptCommand: typeof history.agentPromptCommand === 'string'
2801
+ ? normalizeAgentPromptCommandTemplate(history.agentPromptCommand, 'agentPromptCommand')
2802
+ : '',
2803
+ agentPromptCommandOverride: agentSession && typeof agentSession.agentPromptCommand === 'string'
2804
+ ? normalizeAgentPromptCommandTemplate(agentSession.agentPromptCommand, `agents.${sessionRef.agentId}.agentPromptCommand`)
2805
+ : '',
2806
+ inferredAgentPromptCommand: deriveAgentPromptCommandFromDefaultCommand(containerInfo.defaultCommand),
2807
+ agentPromptSource: getEffectiveAgentPromptCommandSource(history, sessionRef.agentId, containerInfo.defaultCommand),
2680
2808
  agentProgram: summary.agentProgram || '',
2681
2809
  resumeSupported: summary.resumeSupported === true,
2682
2810
  lastResumeAt: agentSession.lastResumeAt || null,
@@ -3216,6 +3344,63 @@ async function handleWebApi(req, res, pathname, ctx, state) {
3216
3344
  sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), detail });
3217
3345
  }
3218
3346
  },
3347
+ {
3348
+ method: 'PUT',
3349
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/agent-template$/),
3350
+ handler: async match => {
3351
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
3352
+ if (!sessionRef) {
3353
+ return;
3354
+ }
3355
+
3356
+ let payload = null;
3357
+ try {
3358
+ payload = await readJsonBody(req);
3359
+ } catch (e) {
3360
+ sendJson(res, 400, { error: e.message || '请求参数错误' });
3361
+ return;
3362
+ }
3363
+ const normalizedPayload = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
3364
+ const hasContainerTemplate = hasOwn(normalizedPayload, 'containerAgentPromptCommand');
3365
+ const hasAgentOverride = hasOwn(normalizedPayload, 'agentPromptCommandOverride');
3366
+ if (!hasContainerTemplate && !hasAgentOverride) {
3367
+ sendJson(res, 400, { error: '至少提供一个模板字段' });
3368
+ return;
3369
+ }
3370
+ if (hasAgentOverride && sessionRef.agentId === WEB_DEFAULT_AGENT_ID) {
3371
+ sendJson(res, 400, { error: '默认 AGENT 不支持单独覆盖模板,请直接修改容器模板' });
3372
+ return;
3373
+ }
3374
+
3375
+ try {
3376
+ if (hasContainerTemplate) {
3377
+ setWebSessionAgentPromptCommand(
3378
+ state.webHistoryDir,
3379
+ sessionRef.containerName,
3380
+ normalizedPayload.containerAgentPromptCommand
3381
+ );
3382
+ }
3383
+ if (hasAgentOverride) {
3384
+ setWebAgentSessionPromptCommand(
3385
+ state.webHistoryDir,
3386
+ sessionRef,
3387
+ normalizedPayload.agentPromptCommandOverride
3388
+ );
3389
+ }
3390
+ } catch (e) {
3391
+ sendJson(res, 400, { error: e.message || '保存 Agent 模板失败' });
3392
+ return;
3393
+ }
3394
+
3395
+ const containerMap = listWebManyoyoContainers(ctx);
3396
+ const detail = buildSessionDetail(ctx, state, containerMap, sessionRef);
3397
+ sendJson(res, 200, {
3398
+ saved: true,
3399
+ name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
3400
+ detail
3401
+ });
3402
+ }
3403
+ },
3219
3404
  {
3220
3405
  method: 'POST',
3221
3406
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/run$/),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.7.20",
3
+ "version": "5.8.0",
4
4
  "imageVersion": "1.9.0-common",
5
5
  "playwrightCliVersion": "0.1.1",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",