@xcanwin/manyoyo 6.2.9 → 6.2.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.
@@ -391,10 +391,15 @@ textarea:focus-visible {
391
391
  color: var(--muted);
392
392
  }
393
393
 
394
+ .text-block[hidden] {
395
+ display: none;
396
+ }
397
+
394
398
  .form-grid input,
395
399
  .form-grid select,
396
400
  .text-block textarea,
397
- .text-block input {
401
+ .text-block input,
402
+ .text-block select {
398
403
  border: 1px solid var(--line);
399
404
  border-radius: 10px;
400
405
  padding: 9px 11px;
@@ -121,7 +121,7 @@
121
121
  <button type="button" id="activityAgentBtn" class="secondary is-active">Agent对话</button>
122
122
  <button type="button" id="activityCommandBtn" class="secondary">系统命令</button>
123
123
  <button type="button" id="agentTemplateBtn" class="secondary">CLI · —</button>
124
- <span id="activityModelChip" class="composer-options-info" aria-live="polite">模型 · —</span>
124
+ <button type="button" id="activityModelChip" class="secondary">模型 · —</button>
125
125
  </div>
126
126
  </div>
127
127
  <button type="submit" id="sendBtn">发送</button>
@@ -288,6 +288,31 @@
288
288
  </section>
289
289
  </div>
290
290
 
291
+ <div id="modelModal" class="modal-backdrop" hidden>
292
+ <section class="modal" role="dialog" aria-modal="true" aria-labelledby="modelModalTitle">
293
+ <header class="modal-header">
294
+ <h2 id="modelModalTitle">选择模型</h2>
295
+ <button type="button" id="modelModalCancelBtn" class="secondary">关闭</button>
296
+ </header>
297
+ <div class="modal-body">
298
+ <div id="modelModalLoading" class="modal-tip" hidden>正在获取模型列表…</div>
299
+ <label class="text-block">模型
300
+ <select id="modelSelect">
301
+ <option value="">跟随默认(不传 --model)</option>
302
+ <option value="__custom__">自定义…</option>
303
+ </select>
304
+ </label>
305
+ <label class="text-block" id="modelCustomGroup" hidden>自定义模型名称
306
+ <input type="text" id="modelCustomInput" placeholder="例如 gemini-2.5-pro">
307
+ </label>
308
+ <div id="modelModalError" class="modal-error" hidden></div>
309
+ </div>
310
+ <footer class="modal-footer">
311
+ <button type="button" id="modelModalSaveBtn">保存</button>
312
+ </footer>
313
+ </section>
314
+ </div>
315
+
291
316
  <div id="externalLinkModal" class="modal-backdrop" hidden>
292
317
  <section class="modal" role="dialog" aria-modal="true" aria-labelledby="externalLinkTitle">
293
318
  <header class="modal-header">
@@ -53,6 +53,10 @@
53
53
  createModalOpen: false,
54
54
  agentTemplateModalOpen: false,
55
55
  externalLinkModalOpen: false,
56
+ modelModalOpen: false,
57
+ modelModalSaving: false,
58
+ modelModalLoading: false,
59
+ modelModalCatalog: [],
56
60
  configLoading: false,
57
61
  configSaving: false,
58
62
  configSaveMessage: '',
@@ -216,6 +220,14 @@
216
220
  const agentTemplateCancelBtn = document.getElementById('agentTemplateCancelBtn');
217
221
  const agentTemplateResetBtn = document.getElementById('agentTemplateResetBtn');
218
222
  const agentTemplateSaveBtn = document.getElementById('agentTemplateSaveBtn');
223
+ const modelModal = document.getElementById('modelModal');
224
+ const modelModalLoading = document.getElementById('modelModalLoading');
225
+ const modelSelect = document.getElementById('modelSelect');
226
+ const modelCustomGroup = document.getElementById('modelCustomGroup');
227
+ const modelCustomInput = document.getElementById('modelCustomInput');
228
+ const modelModalError = document.getElementById('modelModalError');
229
+ const modelModalCancelBtn = document.getElementById('modelModalCancelBtn');
230
+ const modelModalSaveBtn = document.getElementById('modelModalSaveBtn');
219
231
  const externalLinkModal = document.getElementById('externalLinkModal');
220
232
  const externalLinkUrl = document.getElementById('externalLinkUrl');
221
233
  const externalLinkCancelBtn = document.getElementById('externalLinkCancelBtn');
@@ -1524,7 +1536,120 @@
1524
1536
  if (!state.active) {
1525
1537
  return '—';
1526
1538
  }
1527
- return '自动';
1539
+ const activeSession = getActiveSession();
1540
+ const detail = state.sessionDetail && state.active === (state.sessionDetail.name || state.active) ? state.sessionDetail : null;
1541
+ const model = String(
1542
+ (detail && detail.model)
1543
+ || (activeSession && activeSession.model)
1544
+ || ''
1545
+ ).trim();
1546
+ return model || '自动';
1547
+ }
1548
+
1549
+ const MODEL_CUSTOM_OPTION_VALUE = '__custom__';
1550
+
1551
+ function populateModelSelect(models, selectedModel) {
1552
+ if (!modelSelect) {
1553
+ return;
1554
+ }
1555
+ modelSelect.innerHTML = '';
1556
+ const followOption = document.createElement('option');
1557
+ followOption.value = '';
1558
+ followOption.textContent = '跟随默认(不传 --model)';
1559
+ modelSelect.appendChild(followOption);
1560
+
1561
+ const knownValues = new Set();
1562
+ (Array.isArray(models) ? models : []).forEach(function (model) {
1563
+ if (!model || !model.value) {
1564
+ return;
1565
+ }
1566
+ knownValues.add(model.value);
1567
+ const option = document.createElement('option');
1568
+ option.value = model.value;
1569
+ option.textContent = model.description ? `${model.label} · ${model.description}` : model.label;
1570
+ modelSelect.appendChild(option);
1571
+ });
1572
+
1573
+ const customOption = document.createElement('option');
1574
+ customOption.value = MODEL_CUSTOM_OPTION_VALUE;
1575
+ customOption.textContent = '自定义…';
1576
+ modelSelect.appendChild(customOption);
1577
+
1578
+ if (selectedModel && knownValues.has(selectedModel)) {
1579
+ modelSelect.value = selectedModel;
1580
+ if (modelCustomGroup) modelCustomGroup.hidden = true;
1581
+ } else if (selectedModel) {
1582
+ modelSelect.value = MODEL_CUSTOM_OPTION_VALUE;
1583
+ if (modelCustomInput) modelCustomInput.value = selectedModel;
1584
+ if (modelCustomGroup) modelCustomGroup.hidden = false;
1585
+ } else {
1586
+ modelSelect.value = '';
1587
+ if (modelCustomGroup) modelCustomGroup.hidden = true;
1588
+ }
1589
+ }
1590
+
1591
+ function showModelModalError(message) {
1592
+ if (!modelModalError) return;
1593
+ modelModalError.textContent = message || '';
1594
+ modelModalError.hidden = !message;
1595
+ }
1596
+
1597
+ async function openModelModal() {
1598
+ if (!state.active || state.modelModalSaving) {
1599
+ return;
1600
+ }
1601
+ const detail = await ensureActiveSessionDetail();
1602
+ if (!detail) {
1603
+ alert(state.sessionDetailError || '当前会话详情暂时不可用');
1604
+ return;
1605
+ }
1606
+ state.modelModalOpen = true;
1607
+ state.modelModalLoading = true;
1608
+ state.modelModalCatalog = [];
1609
+ showModelModalError('');
1610
+ populateModelSelect([], detail.model || '');
1611
+ syncUi();
1612
+ try {
1613
+ const data = await api('/api/sessions/' + encodeURIComponent(state.active) + '/models');
1614
+ state.modelModalCatalog = Array.isArray(data && data.models) ? data.models : [];
1615
+ } catch (e) {
1616
+ state.modelModalCatalog = [];
1617
+ } finally {
1618
+ state.modelModalLoading = false;
1619
+ populateModelSelect(state.modelModalCatalog, detail.model || '');
1620
+ syncUi();
1621
+ }
1622
+ }
1623
+
1624
+ function closeModelModal() {
1625
+ state.modelModalOpen = false;
1626
+ showModelModalError('');
1627
+ }
1628
+
1629
+ async function saveModelModal() {
1630
+ if (!state.active || state.modelModalSaving) {
1631
+ return;
1632
+ }
1633
+ const rawValue = modelSelect && modelSelect.value === MODEL_CUSTOM_OPTION_VALUE
1634
+ ? (modelCustomInput ? modelCustomInput.value : '')
1635
+ : (modelSelect ? modelSelect.value : '');
1636
+ state.modelModalSaving = true;
1637
+ showModelModalError('');
1638
+ syncUi();
1639
+ try {
1640
+ await api('/api/sessions/' + encodeURIComponent(state.active) + '/model', {
1641
+ method: 'POST',
1642
+ body: JSON.stringify({ model: String(rawValue || '').trim() })
1643
+ });
1644
+ await refreshSessionsSilent({ preferredName: state.active });
1645
+ await loadSessionDetailForSession(state.active);
1646
+ closeModelModal();
1647
+ } catch (e) {
1648
+ showModelModalError(e && e.message ? e.message : '保存失败');
1649
+ } finally {
1650
+ state.modelModalSaving = false;
1651
+ syncUi();
1652
+ }
1528
1653
  }
1529
1654
 
1530
1655
  function buildMessageMetaLines(message) {
@@ -2217,7 +2342,7 @@
2217
2342
  if (activityModelChip) {
2218
2343
  activityModelChip.textContent = `模型 · ${resolveToolbarModelLabel()}`;
2219
2344
  activityModelChip.title = state.active
2220
- ? '当前版本暂不单独配置模型,默认跟随 CLI 或容器内配置'
2345
+ ? '点击选择本会话使用的模型'
2221
2346
  : '请先选择会话';
2222
2347
  }
2223
2348
  if (viewActivityBtn) viewActivityBtn.classList.toggle('is-active', activityTab);
@@ -2322,6 +2447,27 @@
2322
2447
  if (externalLinkModal) {
2323
2448
  externalLinkModal.hidden = !state.externalLinkModalOpen;
2324
2449
  }
2450
+ if (modelModal) {
2451
+ modelModal.hidden = !state.modelModalOpen;
2452
+ }
2453
+ if (modelModalLoading) {
2454
+ modelModalLoading.hidden = !state.modelModalLoading;
2455
+ }
2456
+ if (modelSelect) {
2457
+ modelSelect.disabled = state.modelModalSaving || state.modelModalLoading;
2458
+ }
2459
+ if (modelCustomInput) {
2460
+ modelCustomInput.disabled = state.modelModalSaving || state.modelModalLoading;
2461
+ }
2462
+ if (modelModalSaveBtn) {
2463
+ modelModalSaveBtn.disabled = state.modelModalSaving || state.modelModalLoading;
2464
+ }
2465
+ if (modelModalCancelBtn) {
2466
+ modelModalCancelBtn.disabled = state.modelModalSaving;
2467
+ }
2468
+ if (activityModelChip) {
2469
+ activityModelChip.disabled = !state.active;
2470
+ }
2325
2471
  if (agentTemplateSaveBtn) {
2326
2472
  agentTemplateSaveBtn.disabled = state.agentTemplateSaving || !state.active;
2327
2473
  }
@@ -2345,7 +2491,7 @@
2345
2491
  }
2346
2492
  document.body.classList.toggle(
2347
2493
  'modal-open',
2348
- state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen || state.externalLinkModalOpen || state.cloneModal.open
2494
+ state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen || state.externalLinkModalOpen || state.cloneModal.open || state.modelModalOpen
2349
2495
  );
2350
2496
  if (composer) {
2351
2497
  composer.hidden = !activityTab;
@@ -4238,6 +4384,45 @@
4238
4384
  });
4239
4385
  }
4240
4386
 
4387
+ if (activityModelChip) {
4388
+ activityModelChip.addEventListener('click', function () {
4389
+ closeComposerOptionsMenu();
4390
+ openModelModal().catch(function (e) {
4391
+ alert(e && e.message ? e.message : '加载模型信息失败');
4392
+ });
4393
+ });
4394
+ }
4395
+
4396
+ if (modelModalCancelBtn) {
4397
+ modelModalCancelBtn.addEventListener('click', function () {
4398
+ closeModelModal();
4399
+ syncUi();
4400
+ });
4401
+ }
4402
+
4403
+ if (modelModalSaveBtn) {
4404
+ modelModalSaveBtn.addEventListener('click', function () {
4405
+ saveModelModal();
4406
+ });
4407
+ }
4408
+
4409
+ if (modelSelect) {
4410
+ modelSelect.addEventListener('change', function () {
4411
+ if (modelCustomGroup) {
4412
+ modelCustomGroup.hidden = modelSelect.value !== MODEL_CUSTOM_OPTION_VALUE;
4413
+ }
4414
+ });
4415
+ }
4416
+
4417
+ if (modelModal) {
4418
+ modelModal.addEventListener('click', function (event) {
4419
+ if (event.target === modelModal && !state.modelModalSaving) {
4420
+ closeModelModal();
4421
+ syncUi();
4422
+ }
4423
+ });
4424
+ }
4425
+
4241
4426
  if (configCancelBtn) {
4242
4427
  configCancelBtn.addEventListener('click', function () {
4243
4428
  closeConfigModal();
@@ -4764,6 +4949,10 @@
4764
4949
  closeAgentTemplateModal();
4765
4950
  syncUi();
4766
4951
  }
4952
+ if (event.key === 'Escape' && state.modelModalOpen && !state.modelModalSaving) {
4953
+ closeModelModal();
4954
+ syncUi();
4955
+ }
4767
4956
  if (event.key === 'Escape' && state.externalLinkModalOpen) {
4768
4957
  closeExternalLinkModalView();
4769
4958
  syncUi();
package/lib/web/server.js CHANGED
@@ -190,7 +190,8 @@ function createEmptyWebAgentSession(agentId, agentName) {
190
190
  lastResumeOk: null,
191
191
  lastResumeError: '',
192
192
  engineSessionId: '',
193
- usageTotal: null
193
+ usageTotal: null,
194
+ model: ''
194
195
  };
195
196
  }
196
197
 
@@ -237,7 +238,8 @@ function normalizeWebAgentSessionRecord(agentId, rawAgent) {
237
238
  lastResumeOk: typeof source.lastResumeOk === 'boolean' ? source.lastResumeOk : null,
238
239
  lastResumeError: typeof source.lastResumeError === 'string' ? source.lastResumeError : '',
239
240
  engineSessionId: typeof source.engineSessionId === 'string' ? source.engineSessionId : '',
240
- usageTotal: normalizeWebAgentUsageTotal(source.usageTotal)
241
+ usageTotal: normalizeWebAgentUsageTotal(source.usageTotal),
242
+ model: normalizeStoredAgentModelValue(source.model)
241
243
  };
242
244
  }
243
245
 
@@ -775,6 +777,7 @@ function renderAgentPromptCommand(template, prompt) {
775
777
  function buildCodexAgentExecCommand(template, prompt, options = {}) {
776
778
  const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
777
779
  const sessionId = options && typeof options.sessionId === 'string' ? options.sessionId.trim() : '';
780
+ const model = options && typeof options.model === 'string' ? options.model.trim() : '';
778
781
  const execMatch = templateText.match(
779
782
  /^((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)codex\s+exec\b/
780
783
  );
@@ -784,12 +787,14 @@ function buildCodexAgentExecCommand(template, prompt, options = {}) {
784
787
  const suffix = templateText.slice(execMatch[0].length);
785
788
  const hasJson = /(?:^|\s)--json(?:\s|$)/.test(suffix);
786
789
  const injectedFlags = hasJson ? '' : ' --json';
790
+ const hasModel = /(?:^|\s)(?:-m|--model)(?:\s|$)/.test(suffix);
791
+ const modelFlag = model && !hasModel ? ` --model ${model}` : '';
787
792
  if (sessionId) {
788
793
  const promptIndex = suffix.indexOf('{prompt}');
789
794
  const resumeSuffix = `${suffix.slice(0, promptIndex)}${quoteBashSingleValue(sessionId)} ${suffix.slice(promptIndex)}`;
790
- codexTemplate = `${prefix}codex exec resume${injectedFlags}${resumeSuffix}`;
795
+ codexTemplate = `${prefix}codex exec resume${injectedFlags}${modelFlag}${resumeSuffix}`;
791
796
  } else {
792
- codexTemplate = `${prefix}codex exec${injectedFlags}${suffix}`;
797
+ codexTemplate = `${prefix}codex exec${injectedFlags}${modelFlag}${suffix}`;
793
798
  }
794
799
  }
795
800
  return codexTemplate === templateText
@@ -824,6 +829,10 @@ function buildClaudeAgentExecCommand(template, prompt, options = {}) {
824
829
  if (sessionId) {
825
830
  flagSpecs.push({ flag: `-r ${sessionId}`, pattern: /(?:^|\s)-r(?:\s|$)/ });
826
831
  }
832
+ const model = options && typeof options.model === 'string' ? options.model.trim() : '';
833
+ if (model) {
834
+ flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)--model(?:\s|$)/ });
835
+ }
827
836
  const claudeTemplate = prependAgentFlags(
828
837
  templateText,
829
838
  /^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)claude\b)(.*)$/,
@@ -834,14 +843,19 @@ function buildClaudeAgentExecCommand(template, prompt, options = {}) {
834
843
  : renderAgentPromptCommand(claudeTemplate, prompt);
835
844
  }
836
845
 
837
- function buildGeminiAgentExecCommand(template, prompt) {
846
+ function buildGeminiAgentExecCommand(template, prompt, options = {}) {
838
847
  const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
848
+ const flagSpecs = [
849
+ { flag: '--output-format stream-json', pattern: /(?:^|\s)--output-format(?:\s|$)/ }
850
+ ];
851
+ const model = options && typeof options.model === 'string' ? options.model.trim() : '';
852
+ if (model) {
853
+ flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
854
+ }
839
855
  const geminiTemplate = prependAgentFlags(
840
856
  templateText,
841
857
  /^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)gemini\b)(.*)$/,
842
- [
843
- { flag: '--output-format stream-json', pattern: /(?:^|\s)--output-format(?:\s|$)/ }
844
- ]
858
+ flagSpecs
845
859
  );
846
860
  return geminiTemplate === templateText
847
861
  ? renderAgentPromptCommand(templateText, prompt)
@@ -860,6 +874,10 @@ function buildOpenCodeAgentExecCommand(template, prompt, options = {}) {
860
874
  pattern: /(?:^|\s)(?:--session|-s)(?:\s|$)/
861
875
  });
862
876
  }
877
+ const model = options && typeof options.model === 'string' ? options.model.trim() : '';
878
+ if (model) {
879
+ flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
880
+ }
863
881
  const opencodeTemplate = prependAgentFlags(
864
882
  templateText,
865
883
  /^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)opencode\b.*?\s+run\b)(.*)$/,
@@ -875,7 +893,7 @@ function buildWebAgentExecCommand(template, prompt, agentProgram, options = {})
875
893
  case 'claude':
876
894
  return buildClaudeAgentExecCommand(template, prompt, options);
877
895
  case 'gemini':
878
- return buildGeminiAgentExecCommand(template, prompt);
896
+ return buildGeminiAgentExecCommand(template, prompt, options);
879
897
  case 'codex':
880
898
  return buildCodexAgentExecCommand(template, prompt, options);
881
899
  case 'opencode':
@@ -1138,6 +1156,162 @@ function accumulateUsageTotal(baseline, turnUsage) {
1138
1156
  };
1139
1157
  }
1140
1158
 
1159
+ // 起一个短命的容器内子进程,通过对应协议问一次元信息(初始化握手 / model 列表),拿到结果就杀掉进程——
1160
+ // 不是长驻会话,不会产生真实对话轮次或费用。
1161
+ function runAgentProtocolProcess(ctx, containerName, command, options = {}) {
1162
+ const stdinLines = Array.isArray(options.stdinLines) ? options.stdinLines : [];
1163
+ const onLine = typeof options.onLine === 'function' ? options.onLine : null;
1164
+ const timeoutMs = typeof options.timeoutMs === 'number' ? options.timeoutMs : 8000;
1165
+ return new Promise(resolve => {
1166
+ let settled = false;
1167
+ let pending = '';
1168
+ let stdoutAll = '';
1169
+ let child;
1170
+ try {
1171
+ child = spawn(ctx.dockerCmd, ['exec', '-i', containerName, '/bin/bash', '-lc', command], { stdio: ['pipe', 'pipe', 'pipe'] });
1172
+ } catch (e) {
1173
+ resolve({ result: null, stdout: '' });
1174
+ return;
1175
+ }
1176
+ const timer = setTimeout(() => finish(null), timeoutMs);
1177
+ function finish(result) {
1178
+ if (settled) return;
1179
+ settled = true;
1180
+ clearTimeout(timer);
1181
+ try { child.kill('SIGKILL'); } catch (e) { /* 进程可能已退出 */ }
1182
+ resolve({ result, stdout: stdoutAll });
1183
+ }
1184
+ child.on('error', () => finish(null));
1185
+ child.on('close', () => finish(null));
1186
+ child.stdout.on('data', chunk => {
1187
+ const text = chunk.toString('utf-8');
1188
+ stdoutAll += text;
1189
+ if (!onLine) {
1190
+ return;
1191
+ }
1192
+ pending += text;
1193
+ let newlineIndex = pending.indexOf('\n');
1194
+ while (newlineIndex !== -1) {
1195
+ const line = pending.slice(0, newlineIndex);
1196
+ pending = pending.slice(newlineIndex + 1);
1197
+ const trimmed = line.trim();
1198
+ if (trimmed) {
1199
+ const payload = parseJsonObjectLine(trimmed);
1200
+ if (payload) {
1201
+ const matched = onLine(payload);
1202
+ if (matched !== undefined) {
1203
+ finish(matched);
1204
+ return;
1205
+ }
1206
+ }
1207
+ }
1208
+ newlineIndex = pending.indexOf('\n');
1209
+ }
1210
+ });
1211
+ try {
1212
+ stdinLines.forEach(line => child.stdin.write(`${line}\n`));
1213
+ if (!stdinLines.length) {
1214
+ child.stdin.end();
1215
+ }
1216
+ } catch (e) { /* 进程可能已提前退出 */ }
1217
+ });
1218
+ }
1219
+
1220
+ function normalizeAgentModelCatalogEntry(item) {
1221
+ const value = pickFirstString(item && item.value, item && item.id, item && item.model);
1222
+ if (!value) {
1223
+ return null;
1224
+ }
1225
+ return {
1226
+ value,
1227
+ label: pickFirstString(item && item.displayName, value),
1228
+ description: pickFirstString(item && item.description)
1229
+ };
1230
+ }
1231
+
1232
+ function extractClaudeModelsFromControlResponse(payload) {
1233
+ if (!payload || payload.type !== 'control_response') {
1234
+ return undefined;
1235
+ }
1236
+ const response = toPlainObject(payload.response);
1237
+ if (response.request_id !== 'manyoyo-model-catalog') {
1238
+ return undefined;
1239
+ }
1240
+ const inner = toPlainObject(response.response);
1241
+ const models = Array.isArray(inner.models) ? inner.models : [];
1242
+ return models.map(normalizeAgentModelCatalogEntry).filter(Boolean);
1243
+ }
1244
+
1245
+ function extractCodexModelsFromListResponse(payload) {
1246
+ if (!payload || payload.id !== 'manyoyo-model-list' || !payload.result) {
1247
+ return undefined;
1248
+ }
1249
+ const data = Array.isArray(payload.result.data) ? payload.result.data : [];
1250
+ return data.map(normalizeAgentModelCatalogEntry).filter(Boolean);
1251
+ }
1252
+
1253
+ function extractOpenCodeModelsFromOutput(text) {
1254
+ return String(text || '')
1255
+ .split('\n')
1256
+ .map(line => line.trim())
1257
+ .filter(Boolean)
1258
+ .map(value => ({ value, label: value, description: '' }));
1259
+ }
1260
+
1261
+ async function fetchAgentModelCatalog(ctx, containerName, agentProgram) {
1262
+ if (agentProgram === 'claude') {
1263
+ const requestLine = JSON.stringify({
1264
+ type: 'control_request',
1265
+ request_id: 'manyoyo-model-catalog',
1266
+ request: { subtype: 'initialize' }
1267
+ });
1268
+ const { result } = await runAgentProtocolProcess(
1269
+ ctx,
1270
+ containerName,
1271
+ 'claude -p --input-format stream-json --output-format stream-json --verbose',
1272
+ { stdinLines: [requestLine], onLine: extractClaudeModelsFromControlResponse }
1273
+ );
1274
+ return Array.isArray(result) ? result : [];
1275
+ }
1276
+ if (agentProgram === 'codex') {
1277
+ const stdinLines = [
1278
+ JSON.stringify({ jsonrpc: '2.0', id: 'manyoyo-init', method: 'initialize', params: { clientInfo: { name: 'manyoyo', version: '1.0.0' } } }),
1279
+ JSON.stringify({ jsonrpc: '2.0', id: 'manyoyo-model-list', method: 'model/list', params: {} })
1280
+ ];
1281
+ const { result } = await runAgentProtocolProcess(
1282
+ ctx,
1283
+ containerName,
1284
+ 'codex app-server',
1285
+ { stdinLines, onLine: extractCodexModelsFromListResponse }
1286
+ );
1287
+ return Array.isArray(result) ? result : [];
1288
+ }
1289
+ if (agentProgram === 'opencode') {
1290
+ const { stdout } = await runAgentProtocolProcess(ctx, containerName, 'opencode models', {});
1291
+ return extractOpenCodeModelsFromOutput(stdout);
1292
+ }
1293
+ // Gemini 的 --acp 模式在认证前就会挂起重试,没有可用的“认证前”探测窗口,暂不支持目录抓取。
1294
+ return [];
1295
+ }
1296
+
1297
+ const SAFE_AGENT_MODEL_VALUE_PATTERN = /^[A-Za-z0-9._\-[\]/]+$/;
1298
+
1299
+ function normalizeAgentModelValueInput(value) {
1300
+ const trimmed = String(value || '').trim();
1301
+ if (!trimmed) {
1302
+ return '';
1303
+ }
1304
+ if (!SAFE_AGENT_MODEL_VALUE_PATTERN.test(trimmed)) {
1305
+ throw new Error(`模型名称包含非法字符: ${trimmed}`);
1306
+ }
1307
+ return trimmed;
1308
+ }
1309
+
1310
+ function normalizeStoredAgentModelValue(value) {
1311
+ const trimmed = typeof value === 'string' ? value.trim() : '';
1312
+ return SAFE_AGENT_MODEL_VALUE_PATTERN.test(trimmed) ? trimmed : '';
1313
+ }
1314
+
1141
1315
  function getAgentRuntimeMeta(template) {
1142
1316
  const normalizedTemplate = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
1143
1317
  const agentProgram = resolveAgentProgram(normalizedTemplate);
@@ -1843,7 +2017,8 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
1843
2017
  ? prompt
1844
2018
  : buildAgentPromptWithHistory(agentSession, prompt);
1845
2019
  const command = buildWebAgentExecCommand(effectiveTemplate, effectivePrompt, agentMeta.agentProgram, {
1846
- sessionId: sessionIdForRun
2020
+ sessionId: sessionIdForRun,
2021
+ model: agentSession.model || ''
1847
2022
  });
1848
2023
  const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
1849
2024
 
@@ -3645,6 +3820,7 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
3645
3820
  agentEnabled: isAgentPromptCommandEnabled(effectiveAgentPromptCommand),
3646
3821
  agentProgram: effectiveAgentProgram || '',
3647
3822
  resumeSupported: effectiveResumeSupported,
3823
+ model: agentSession.model || '',
3648
3824
  hostPath: applied.hostPath || '',
3649
3825
  containerPath: applied.containerPath || ''
3650
3826
  };
@@ -3731,6 +3907,7 @@ function buildSessionDetail(ctx, state, containerMap, name) {
3731
3907
  lastResumeOk: typeof agentSession.lastResumeOk === 'boolean' ? agentSession.lastResumeOk : null,
3732
3908
  lastResumeError: agentSession.lastResumeError || '',
3733
3909
  usageTotal: agentSession.usageTotal || null,
3910
+ model: agentSession.model || '',
3734
3911
  applied
3735
3912
  };
3736
3913
  }
@@ -4648,6 +4825,51 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4648
4825
  sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), detail });
4649
4826
  }
4650
4827
  },
4828
+ {
4829
+ method: 'GET',
4830
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/models$/),
4831
+ handler: async match => {
4832
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
4833
+ if (!sessionRef) {
4834
+ return;
4835
+ }
4836
+ const containerMap = listWebManyoyoContainers(ctx);
4837
+ const containerInfo = containerMap[sessionRef.containerName] || {};
4838
+ const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
4839
+ const effectiveTemplate = resolveEffectiveAgentPromptCommandForSession(
4840
+ history,
4841
+ sessionRef.agentId,
4842
+ containerInfo.defaultCommand
4843
+ );
4844
+ const agentProgram = resolveAgentProgram(effectiveTemplate);
4845
+ const models = await fetchAgentModelCatalog(ctx, sessionRef.containerName, agentProgram);
4846
+ sendJson(res, 200, {
4847
+ name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
4848
+ agentProgram,
4849
+ models
4850
+ });
4851
+ }
4852
+ },
4853
+ {
4854
+ method: 'POST',
4855
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/model$/),
4856
+ handler: async match => {
4857
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
4858
+ if (!sessionRef) {
4859
+ return;
4860
+ }
4861
+ const payload = await readJsonBody(req);
4862
+ let model;
4863
+ try {
4864
+ model = normalizeAgentModelValueInput(payload.model);
4865
+ } catch (e) {
4866
+ sendJson(res, 400, { error: e.message || String(e) });
4867
+ return;
4868
+ }
4869
+ patchWebAgentSessionState(state.webHistoryDir, sessionRef, { model });
4870
+ sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), model });
4871
+ }
4872
+ },
4651
4873
  {
4652
4874
  method: 'GET',
4653
4875
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/audit$/),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "6.2.9",
3
+ "version": "6.2.10",
4
4
  "imageVersion": "1.9.1-common",
5
5
  "playwrightCliVersion": "0.1.18",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",