@xcanwin/manyoyo 6.2.9 → 6.2.11

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.
@@ -99,6 +99,17 @@ class FileEventStore {
99
99
  fs.writeFileSync(temporaryPath, `${JSON.stringify(projection)}\n`);
100
100
  fs.renameSync(temporaryPath, targetPath);
101
101
  }
102
+
103
+ remove(aggregateId) {
104
+ const eventFilePath = this.getEventFilePath(aggregateId);
105
+ if (fs.existsSync(eventFilePath)) {
106
+ fs.unlinkSync(eventFilePath);
107
+ }
108
+ const projectionFilePath = this.getProjectionFilePath(aggregateId);
109
+ if (fs.existsSync(projectionFilePath)) {
110
+ fs.unlinkSync(projectionFilePath);
111
+ }
112
+ }
102
113
  }
103
114
 
104
115
  module.exports = {
@@ -284,6 +284,10 @@ textarea:focus-visible {
284
284
  overflow: hidden;
285
285
  }
286
286
 
287
+ .modal.modal-sm {
288
+ width: min(420px, calc(100vw - 24px));
289
+ }
290
+
287
291
  .modal-header {
288
292
  display: flex;
289
293
  align-items: center;
@@ -391,10 +395,15 @@ textarea:focus-visible {
391
395
  color: var(--muted);
392
396
  }
393
397
 
398
+ .text-block[hidden] {
399
+ display: none;
400
+ }
401
+
394
402
  .form-grid input,
395
403
  .form-grid select,
396
404
  .text-block textarea,
397
- .text-block input {
405
+ .text-block input,
406
+ .text-block select {
398
407
  border: 1px solid var(--line);
399
408
  border-radius: 10px;
400
409
  padding: 9px 11px;
@@ -742,6 +751,15 @@ textarea:focus-visible {
742
751
  }
743
752
  }
744
753
 
754
+ .crumb-bar .tree-node-menu {
755
+ position: relative;
756
+ top: auto;
757
+ right: auto;
758
+ transform: none;
759
+ opacity: 1;
760
+ pointer-events: auto;
761
+ }
762
+
745
763
  .tree-node-menu-trigger {
746
764
  width: 26px;
747
765
  height: 26px;
@@ -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>
@@ -215,6 +215,22 @@
215
215
  </section>
216
216
  </div>
217
217
 
218
+ <div id="removeConfirmModal" class="modal-backdrop" hidden>
219
+ <section class="modal modal-sm" role="dialog" aria-modal="true" aria-labelledby="removeConfirmTitle">
220
+ <header class="modal-header">
221
+ <h2 id="removeConfirmTitle">确认删除</h2>
222
+ <button type="button" id="removeConfirmCancelBtn" class="secondary">关闭</button>
223
+ </header>
224
+ <div class="modal-body">
225
+ <p id="removeConfirmMessage"></p>
226
+ </div>
227
+ <footer class="modal-footer">
228
+ <button type="button" id="removeConfirmKeepHistoryBtn" class="secondary">否,保留历史</button>
229
+ <button type="button" id="removeConfirmWithHistoryBtn" class="danger">是,连同历史一起删除</button>
230
+ </footer>
231
+ </section>
232
+ </div>
233
+
218
234
  <div id="directoryPickerModal" class="modal-backdrop" hidden>
219
235
  <section class="modal dir-picker-modal" role="dialog" aria-modal="true" aria-labelledby="directoryPickerTitle">
220
236
  <header class="modal-header">
@@ -288,6 +304,31 @@
288
304
  </section>
289
305
  </div>
290
306
 
307
+ <div id="modelModal" class="modal-backdrop" hidden>
308
+ <section class="modal" role="dialog" aria-modal="true" aria-labelledby="modelModalTitle">
309
+ <header class="modal-header">
310
+ <h2 id="modelModalTitle">选择模型</h2>
311
+ <button type="button" id="modelModalCancelBtn" class="secondary">关闭</button>
312
+ </header>
313
+ <div class="modal-body">
314
+ <div id="modelModalLoading" class="modal-tip" hidden>正在获取模型列表…</div>
315
+ <label class="text-block">模型
316
+ <select id="modelSelect">
317
+ <option value="">跟随默认(不传 --model)</option>
318
+ <option value="__custom__">自定义…</option>
319
+ </select>
320
+ </label>
321
+ <label class="text-block" id="modelCustomGroup" hidden>自定义模型名称
322
+ <input type="text" id="modelCustomInput" placeholder="例如 gemini-2.5-pro">
323
+ </label>
324
+ <div id="modelModalError" class="modal-error" hidden></div>
325
+ </div>
326
+ <footer class="modal-footer">
327
+ <button type="button" id="modelModalSaveBtn">保存</button>
328
+ </footer>
329
+ </section>
330
+ </div>
331
+
291
332
  <div id="externalLinkModal" class="modal-backdrop" hidden>
292
333
  <section class="modal" role="dialog" aria-modal="true" aria-labelledby="externalLinkTitle">
293
334
  <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: '',
@@ -175,6 +179,12 @@
175
179
  const createEnv = document.getElementById('createEnv');
176
180
  const createEnvFile = document.getElementById('createEnvFile');
177
181
  const createVolumes = document.getElementById('createVolumes');
182
+ const removeConfirmModal = document.getElementById('removeConfirmModal');
183
+ const removeConfirmTitle = document.getElementById('removeConfirmTitle');
184
+ const removeConfirmMessage = document.getElementById('removeConfirmMessage');
185
+ const removeConfirmCancelBtn = document.getElementById('removeConfirmCancelBtn');
186
+ const removeConfirmKeepHistoryBtn = document.getElementById('removeConfirmKeepHistoryBtn');
187
+ const removeConfirmWithHistoryBtn = document.getElementById('removeConfirmWithHistoryBtn');
178
188
  const directoryPickerModal = document.getElementById('directoryPickerModal');
179
189
  const directoryPickerTitle = document.getElementById('directoryPickerTitle');
180
190
  const directoryPickerTip = document.getElementById('directoryPickerTip');
@@ -216,6 +226,14 @@
216
226
  const agentTemplateCancelBtn = document.getElementById('agentTemplateCancelBtn');
217
227
  const agentTemplateResetBtn = document.getElementById('agentTemplateResetBtn');
218
228
  const agentTemplateSaveBtn = document.getElementById('agentTemplateSaveBtn');
229
+ const modelModal = document.getElementById('modelModal');
230
+ const modelModalLoading = document.getElementById('modelModalLoading');
231
+ const modelSelect = document.getElementById('modelSelect');
232
+ const modelCustomGroup = document.getElementById('modelCustomGroup');
233
+ const modelCustomInput = document.getElementById('modelCustomInput');
234
+ const modelModalError = document.getElementById('modelModalError');
235
+ const modelModalCancelBtn = document.getElementById('modelModalCancelBtn');
236
+ const modelModalSaveBtn = document.getElementById('modelModalSaveBtn');
219
237
  const externalLinkModal = document.getElementById('externalLinkModal');
220
238
  const externalLinkUrl = document.getElementById('externalLinkUrl');
221
239
  const externalLinkCancelBtn = document.getElementById('externalLinkCancelBtn');
@@ -1524,7 +1542,120 @@
1524
1542
  if (!state.active) {
1525
1543
  return '—';
1526
1544
  }
1527
- return '自动';
1545
+ const activeSession = getActiveSession();
1546
+ const detail = state.sessionDetail && state.active === (state.sessionDetail.name || state.active) ? state.sessionDetail : null;
1547
+ const model = String(
1548
+ (detail && detail.model)
1549
+ || (activeSession && activeSession.model)
1550
+ || ''
1551
+ ).trim();
1552
+ return model || '自动';
1553
+ }
1554
+
1555
+ const MODEL_CUSTOM_OPTION_VALUE = '__custom__';
1556
+
1557
+ function populateModelSelect(models, selectedModel) {
1558
+ if (!modelSelect) {
1559
+ return;
1560
+ }
1561
+ modelSelect.innerHTML = '';
1562
+ const followOption = document.createElement('option');
1563
+ followOption.value = '';
1564
+ followOption.textContent = '跟随默认(不传 --model)';
1565
+ modelSelect.appendChild(followOption);
1566
+
1567
+ const knownValues = new Set();
1568
+ (Array.isArray(models) ? models : []).forEach(function (model) {
1569
+ if (!model || !model.value) {
1570
+ return;
1571
+ }
1572
+ knownValues.add(model.value);
1573
+ const option = document.createElement('option');
1574
+ option.value = model.value;
1575
+ option.textContent = model.description ? `${model.label} · ${model.description}` : model.label;
1576
+ modelSelect.appendChild(option);
1577
+ });
1578
+
1579
+ const customOption = document.createElement('option');
1580
+ customOption.value = MODEL_CUSTOM_OPTION_VALUE;
1581
+ customOption.textContent = '自定义…';
1582
+ modelSelect.appendChild(customOption);
1583
+
1584
+ if (selectedModel && knownValues.has(selectedModel)) {
1585
+ modelSelect.value = selectedModel;
1586
+ if (modelCustomGroup) modelCustomGroup.hidden = true;
1587
+ } else if (selectedModel) {
1588
+ modelSelect.value = MODEL_CUSTOM_OPTION_VALUE;
1589
+ if (modelCustomInput) modelCustomInput.value = selectedModel;
1590
+ if (modelCustomGroup) modelCustomGroup.hidden = false;
1591
+ } else {
1592
+ modelSelect.value = '';
1593
+ if (modelCustomGroup) modelCustomGroup.hidden = true;
1594
+ }
1595
+ }
1596
+
1597
+ function showModelModalError(message) {
1598
+ if (!modelModalError) return;
1599
+ modelModalError.textContent = message || '';
1600
+ modelModalError.hidden = !message;
1601
+ }
1602
+
1603
+ async function openModelModal() {
1604
+ if (!state.active || state.modelModalSaving) {
1605
+ return;
1606
+ }
1607
+ const detail = await ensureActiveSessionDetail();
1608
+ if (!detail) {
1609
+ alert(state.sessionDetailError || '当前会话详情暂时不可用');
1610
+ return;
1611
+ }
1612
+ state.modelModalOpen = true;
1613
+ state.modelModalLoading = true;
1614
+ state.modelModalCatalog = [];
1615
+ showModelModalError('');
1616
+ populateModelSelect([], detail.model || '');
1617
+ syncUi();
1618
+ try {
1619
+ const data = await api('/api/sessions/' + encodeURIComponent(state.active) + '/models');
1620
+ state.modelModalCatalog = Array.isArray(data && data.models) ? data.models : [];
1621
+ } catch (e) {
1622
+ state.modelModalCatalog = [];
1623
+ } finally {
1624
+ state.modelModalLoading = false;
1625
+ populateModelSelect(state.modelModalCatalog, detail.model || '');
1626
+ syncUi();
1627
+ }
1628
+ }
1629
+
1630
+ function closeModelModal() {
1631
+ state.modelModalOpen = false;
1632
+ showModelModalError('');
1633
+ }
1634
+
1635
+ async function saveModelModal() {
1636
+ if (!state.active || state.modelModalSaving) {
1637
+ return;
1638
+ }
1639
+ const rawValue = modelSelect && modelSelect.value === MODEL_CUSTOM_OPTION_VALUE
1640
+ ? (modelCustomInput ? modelCustomInput.value : '')
1641
+ : (modelSelect ? modelSelect.value : '');
1642
+ state.modelModalSaving = true;
1643
+ showModelModalError('');
1644
+ syncUi();
1645
+ try {
1646
+ await api('/api/sessions/' + encodeURIComponent(state.active) + '/model', {
1647
+ method: 'POST',
1648
+ body: JSON.stringify({ model: String(rawValue || '').trim() })
1649
+ });
1650
+ await refreshSessionsSilent({ preferredName: state.active });
1651
+ await loadSessionDetailForSession(state.active);
1652
+ closeModelModal();
1653
+ } catch (e) {
1654
+ showModelModalError(e && e.message ? e.message : '保存失败');
1655
+ } finally {
1656
+ state.modelModalSaving = false;
1657
+ syncUi();
1658
+ }
1528
1659
  }
1529
1660
 
1530
1661
  function buildMessageMetaLines(message) {
@@ -2217,7 +2348,7 @@
2217
2348
  if (activityModelChip) {
2218
2349
  activityModelChip.textContent = `模型 · ${resolveToolbarModelLabel()}`;
2219
2350
  activityModelChip.title = state.active
2220
- ? '当前版本暂不单独配置模型,默认跟随 CLI 或容器内配置'
2351
+ ? '点击选择本会话使用的模型'
2221
2352
  : '请先选择会话';
2222
2353
  }
2223
2354
  if (viewActivityBtn) viewActivityBtn.classList.toggle('is-active', activityTab);
@@ -2322,6 +2453,27 @@
2322
2453
  if (externalLinkModal) {
2323
2454
  externalLinkModal.hidden = !state.externalLinkModalOpen;
2324
2455
  }
2456
+ if (modelModal) {
2457
+ modelModal.hidden = !state.modelModalOpen;
2458
+ }
2459
+ if (modelModalLoading) {
2460
+ modelModalLoading.hidden = !state.modelModalLoading;
2461
+ }
2462
+ if (modelSelect) {
2463
+ modelSelect.disabled = state.modelModalSaving || state.modelModalLoading;
2464
+ }
2465
+ if (modelCustomInput) {
2466
+ modelCustomInput.disabled = state.modelModalSaving || state.modelModalLoading;
2467
+ }
2468
+ if (modelModalSaveBtn) {
2469
+ modelModalSaveBtn.disabled = state.modelModalSaving || state.modelModalLoading;
2470
+ }
2471
+ if (modelModalCancelBtn) {
2472
+ modelModalCancelBtn.disabled = state.modelModalSaving;
2473
+ }
2474
+ if (activityModelChip) {
2475
+ activityModelChip.disabled = !state.active;
2476
+ }
2325
2477
  if (agentTemplateSaveBtn) {
2326
2478
  agentTemplateSaveBtn.disabled = state.agentTemplateSaving || !state.active;
2327
2479
  }
@@ -2345,7 +2497,7 @@
2345
2497
  }
2346
2498
  document.body.classList.toggle(
2347
2499
  'modal-open',
2348
- state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen || state.externalLinkModalOpen || state.cloneModal.open
2500
+ state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen || state.externalLinkModalOpen || state.cloneModal.open || state.modelModalOpen
2349
2501
  );
2350
2502
  if (composer) {
2351
2503
  composer.hidden = !activityTab;
@@ -3021,11 +3173,47 @@
3021
3173
  return wrap;
3022
3174
  }
3023
3175
 
3176
+ function confirmRemoveChoice(options) {
3177
+ const opts = options && typeof options === 'object' ? options : {};
3178
+ if (!removeConfirmModal) {
3179
+ return Promise.resolve(confirm(opts.message || '确认删除?') ? 'with-history' : null);
3180
+ }
3181
+ return new Promise(function (resolve) {
3182
+ if (removeConfirmTitle) {
3183
+ removeConfirmTitle.textContent = opts.title || '确认删除';
3184
+ }
3185
+ if (removeConfirmMessage) {
3186
+ removeConfirmMessage.textContent = opts.message || '';
3187
+ }
3188
+ let settled = false;
3189
+ function finish(value) {
3190
+ if (settled) return;
3191
+ settled = true;
3192
+ setModalVisible(removeConfirmModal, false);
3193
+ removeConfirmKeepHistoryBtn.removeEventListener('click', onKeepHistory);
3194
+ removeConfirmWithHistoryBtn.removeEventListener('click', onWithHistory);
3195
+ removeConfirmCancelBtn.removeEventListener('click', onCancel);
3196
+ resolve(value);
3197
+ }
3198
+ function onKeepHistory() { finish('keep-history'); }
3199
+ function onWithHistory() { finish('with-history'); }
3200
+ function onCancel() { finish(null); }
3201
+ removeConfirmKeepHistoryBtn.addEventListener('click', onKeepHistory);
3202
+ removeConfirmWithHistoryBtn.addEventListener('click', onWithHistory);
3203
+ removeConfirmCancelBtn.addEventListener('click', onCancel);
3204
+ setModalVisible(removeConfirmModal, true);
3205
+ });
3206
+ }
3207
+
3024
3208
  async function removeContainerByName(containerName) {
3025
3209
  const target = String(containerName || '').trim();
3026
3210
  if (!target) return;
3027
- const yes = confirm('确认删除容器 ' + target + ' ?');
3028
- if (!yes) return;
3211
+ const choice = await confirmRemoveChoice({
3212
+ title: '删除容器',
3213
+ message: `确认删除容器 ${target}?可以选择是否同时删除该容器的全部历史记录(消息与事件日志)。`
3214
+ });
3215
+ if (!choice) return;
3216
+ const removeHistory = choice === 'with-history';
3029
3217
  try {
3030
3218
  const wasActiveContainer = parseSessionKey(state.active).containerName === target;
3031
3219
  if (parseSessionKey(state.terminal.sessionName).containerName === target
@@ -3033,7 +3221,8 @@
3033
3221
  disconnectTerminal('容器删除,终端已断开', true);
3034
3222
  }
3035
3223
  await api('/api/sessions/' + encodeURIComponent(target) + '/remove', {
3036
- method: 'POST'
3224
+ method: 'POST',
3225
+ body: JSON.stringify({ removeHistory })
3037
3226
  });
3038
3227
  await refreshSessions({
3039
3228
  preferredName: wasActiveContainer ? target : (state.active || ''),
@@ -3048,8 +3237,12 @@
3048
3237
  async function removeAgentSessionByName(sessionName, agentLabel) {
3049
3238
  const target = String(sessionName || '').trim();
3050
3239
  if (!target) return;
3051
- const yes = confirm('确认删除 AGENT ' + (agentLabel || target) + ' ?');
3052
- if (!yes) return;
3240
+ const choice = await confirmRemoveChoice({
3241
+ title: '删除 AGENT',
3242
+ message: `确认删除 AGENT ${agentLabel || target}?可以选择是否同时删除它的历史记录(消息与事件日志)。`
3243
+ });
3244
+ if (!choice) return;
3245
+ const removeHistory = choice === 'with-history';
3053
3246
  const isActive = target === state.active;
3054
3247
  try {
3055
3248
  const containerName = parseSessionKey(target).containerName;
@@ -3057,7 +3250,8 @@
3057
3250
  ? findPreferredSessionNameAfterRemoval(state.sessions, target)
3058
3251
  : '';
3059
3252
  await api('/api/sessions/' + encodeURIComponent(target) + '/remove-with-history', {
3060
- method: 'POST'
3253
+ method: 'POST',
3254
+ body: JSON.stringify({ removeHistory })
3061
3255
  });
3062
3256
  await refreshSessions({
3063
3257
  preferredName: isActive ? (fallbackSessionName || '') : (state.active || ''),
@@ -3173,11 +3367,50 @@
3173
3367
  sep.textContent = '›';
3174
3368
  sessionBreadcrumb.appendChild(sep);
3175
3369
 
3370
+ const sessionForCrumb = state.sessions.find(function (session) {
3371
+ return getSessionContainerName(session) === state.navContainer;
3372
+ });
3373
+ const containerRemark = sessionForCrumb && sessionForCrumb.containerRemark ? sessionForCrumb.containerRemark : '';
3374
+ const containerGroupForMenu = { containerName: state.navContainer, containerRemark: containerRemark };
3375
+
3176
3376
  const containerCrumb = document.createElement('span');
3177
3377
  containerCrumb.className = 'crumb-item is-current';
3178
- containerCrumb.textContent = state.navContainer;
3179
- containerCrumb.title = state.navContainer;
3378
+ containerCrumb.textContent = containerRemark || state.navContainer;
3379
+ containerCrumb.title = containerRemark ? `${containerRemark}(${state.navContainer})` : state.navContainer;
3180
3380
  sessionBreadcrumb.appendChild(containerCrumb);
3381
+ sessionBreadcrumb.appendChild(createTreeNodeMenu([
3382
+ {
3383
+ label: '新建 AGENT',
3384
+ onClick: function () {
3385
+ createAgentSession(containerGroupForMenu.containerName);
3386
+ }
3387
+ },
3388
+ {
3389
+ label: '编辑备注',
3390
+ onClick: function () {
3391
+ editContainerRemark(containerGroupForMenu);
3392
+ }
3393
+ },
3394
+ {
3395
+ label: '创建相同配置容器',
3396
+ onClick: function () {
3397
+ openCloneModal('clone-config', containerGroupForMenu.containerName);
3398
+ }
3399
+ },
3400
+ {
3401
+ label: '复制容器',
3402
+ onClick: function () {
3403
+ openCloneModal('duplicate', containerGroupForMenu.containerName);
3404
+ }
3405
+ },
3406
+ {
3407
+ label: '删除容器',
3408
+ danger: true,
3409
+ onClick: function () {
3410
+ removeContainerByName(containerGroupForMenu.containerName);
3411
+ }
3412
+ }
3413
+ ]));
3181
3414
  }
3182
3415
 
3183
3416
  const activeSession = getActiveSession();
@@ -3296,7 +3529,8 @@
3296
3529
 
3297
3530
  function renderAgentLevel(containerGroup) {
3298
3531
  sessionList.innerHTML = '';
3299
- const sessions = containerGroup && Array.isArray(containerGroup.sessions) ? containerGroup.sessions : [];
3532
+ const allSessions = containerGroup && Array.isArray(containerGroup.sessions) ? containerGroup.sessions : [];
3533
+ const sessions = allSessions.filter(function (session) { return !session.synthetic; });
3300
3534
  if (!sessions.length) {
3301
3535
  const empty = document.createElement('div');
3302
3536
  empty.className = 'empty';
@@ -3368,9 +3602,10 @@
3368
3602
  const containerCount = new Set(state.sessions.map(function (session) {
3369
3603
  return session && session.containerName ? session.containerName : '';
3370
3604
  }).filter(Boolean)).size;
3605
+ const agentCount = state.sessions.filter(function (session) { return !session.synthetic; }).length;
3371
3606
  sessionCount.textContent = state.loadingSessions
3372
3607
  ? '加载中...'
3373
- : `${containerCount} 个容器 / ${state.sessions.length} 个 AGENT`;
3608
+ : `${containerCount} 个容器 / ${agentCount} 个 AGENT`;
3374
3609
  renderBreadcrumb();
3375
3610
 
3376
3611
  if (state.loadingSessions) {
@@ -4238,6 +4473,45 @@
4238
4473
  });
4239
4474
  }
4240
4475
 
4476
+ if (activityModelChip) {
4477
+ activityModelChip.addEventListener('click', function () {
4478
+ closeComposerOptionsMenu();
4479
+ openModelModal().catch(function (e) {
4480
+ alert(e && e.message ? e.message : '加载模型信息失败');
4481
+ });
4482
+ });
4483
+ }
4484
+
4485
+ if (modelModalCancelBtn) {
4486
+ modelModalCancelBtn.addEventListener('click', function () {
4487
+ closeModelModal();
4488
+ syncUi();
4489
+ });
4490
+ }
4491
+
4492
+ if (modelModalSaveBtn) {
4493
+ modelModalSaveBtn.addEventListener('click', function () {
4494
+ saveModelModal();
4495
+ });
4496
+ }
4497
+
4498
+ if (modelSelect) {
4499
+ modelSelect.addEventListener('change', function () {
4500
+ if (modelCustomGroup) {
4501
+ modelCustomGroup.hidden = modelSelect.value !== MODEL_CUSTOM_OPTION_VALUE;
4502
+ }
4503
+ });
4504
+ }
4505
+
4506
+ if (modelModal) {
4507
+ modelModal.addEventListener('click', function (event) {
4508
+ if (event.target === modelModal && !state.modelModalSaving) {
4509
+ closeModelModal();
4510
+ syncUi();
4511
+ }
4512
+ });
4513
+ }
4514
+
4241
4515
  if (configCancelBtn) {
4242
4516
  configCancelBtn.addEventListener('click', function () {
4243
4517
  closeConfigModal();
@@ -4691,6 +4965,14 @@
4691
4965
  });
4692
4966
  }
4693
4967
 
4968
+ if (removeConfirmModal) {
4969
+ removeConfirmModal.addEventListener('click', function (event) {
4970
+ if (event.target === removeConfirmModal) {
4971
+ removeConfirmCancelBtn.click();
4972
+ }
4973
+ });
4974
+ }
4975
+
4694
4976
  if (directoryPickerModal) {
4695
4977
  directoryPickerModal.addEventListener('click', function (event) {
4696
4978
  if (event.target === directoryPickerModal && !state.directoryPicker.loading) {
@@ -4764,6 +5046,10 @@
4764
5046
  closeAgentTemplateModal();
4765
5047
  syncUi();
4766
5048
  }
5049
+ if (event.key === 'Escape' && state.modelModalOpen && !state.modelModalSaving) {
5050
+ closeModelModal();
5051
+ syncUi();
5052
+ }
4767
5053
  if (event.key === 'Escape' && state.externalLinkModalOpen) {
4768
5054
  closeExternalLinkModalView();
4769
5055
  syncUi();
package/lib/web/server.js CHANGED
@@ -182,6 +182,7 @@ function createEmptyWebAgentSession(agentId, agentName) {
182
182
  agentName: normalizeWebAgentName(agentId, agentName),
183
183
  agentPromptCommand: '',
184
184
  remark: '',
185
+ archived: false,
185
186
  createdAt: null,
186
187
  updatedAt: null,
187
188
  messages: [],
@@ -190,7 +191,8 @@ function createEmptyWebAgentSession(agentId, agentName) {
190
191
  lastResumeOk: null,
191
192
  lastResumeError: '',
192
193
  engineSessionId: '',
193
- usageTotal: null
194
+ usageTotal: null,
195
+ model: ''
194
196
  };
195
197
  }
196
198
 
@@ -220,6 +222,7 @@ function normalizeWebAgentSessionRecord(agentId, rawAgent) {
220
222
  ? normalizeAgentPromptCommandTemplate(source.agentPromptCommand, `agents.${agentId}.agentPromptCommand`)
221
223
  : '',
222
224
  remark: normalizeRemarkText(source.remark),
225
+ archived: source.archived === true,
223
226
  createdAt: typeof source.createdAt === 'string' ? source.createdAt : null,
224
227
  updatedAt: typeof source.updatedAt === 'string' ? source.updatedAt : null,
225
228
  messages: Array.isArray(source.messages) ? source.messages : [],
@@ -237,7 +240,8 @@ function normalizeWebAgentSessionRecord(agentId, rawAgent) {
237
240
  lastResumeOk: typeof source.lastResumeOk === 'boolean' ? source.lastResumeOk : null,
238
241
  lastResumeError: typeof source.lastResumeError === 'string' ? source.lastResumeError : '',
239
242
  engineSessionId: typeof source.engineSessionId === 'string' ? source.engineSessionId : '',
240
- usageTotal: normalizeWebAgentUsageTotal(source.usageTotal)
243
+ usageTotal: normalizeWebAgentUsageTotal(source.usageTotal),
244
+ model: normalizeStoredAgentModelValue(source.model)
241
245
  };
242
246
  }
243
247
 
@@ -310,7 +314,7 @@ function normalizeWebHistoryRecord(containerName, rawData) {
310
314
  });
311
315
  }
312
316
 
313
- if (!Object.keys(history.agents).length && Array.isArray(data.messages)) {
317
+ if (!Object.keys(history.agents).length && Array.isArray(data.messages) && data.messages.length > 0) {
314
318
  history.agents[WEB_DEFAULT_AGENT_ID] = normalizeWebAgentSessionRecord(WEB_DEFAULT_AGENT_ID, {
315
319
  agentName: data.agentName || WEB_DEFAULT_AGENT_NAME,
316
320
  updatedAt: typeof data.updatedAt === 'string' ? data.updatedAt : null,
@@ -343,16 +347,18 @@ function saveWebSessionHistory(webHistoryDir, containerName, history) {
343
347
  const filePath = getWebHistoryFile(webHistoryDir, containerName);
344
348
  const normalized = normalizeWebHistoryRecord(containerName, history);
345
349
  const runtimeMeta = getAgentRuntimeMeta(normalized.agentPromptCommand || '');
346
- const defaultAgent = getWebAgentSession(normalized, WEB_DEFAULT_AGENT_ID) || createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID);
350
+ const defaultAgent = getWebAgentSession(normalized, WEB_DEFAULT_AGENT_ID);
347
351
  const legacyCompatible = {
348
352
  ...normalized,
349
- messages: Array.isArray(defaultAgent.messages) ? defaultAgent.messages : [],
350
353
  agentProgram: runtimeMeta.agentProgram || '',
351
- resumeSupported: runtimeMeta.resumeSupported === true,
352
- lastResumeAt: defaultAgent.lastResumeAt || null,
353
- lastResumeOk: typeof defaultAgent.lastResumeOk === 'boolean' ? defaultAgent.lastResumeOk : null,
354
- lastResumeError: defaultAgent.lastResumeError || ''
354
+ resumeSupported: runtimeMeta.resumeSupported === true
355
355
  };
356
+ if (defaultAgent) {
357
+ legacyCompatible.messages = Array.isArray(defaultAgent.messages) ? defaultAgent.messages : [];
358
+ legacyCompatible.lastResumeAt = defaultAgent.lastResumeAt || null;
359
+ legacyCompatible.lastResumeOk = typeof defaultAgent.lastResumeOk === 'boolean' ? defaultAgent.lastResumeOk : null;
360
+ legacyCompatible.lastResumeError = defaultAgent.lastResumeError || '';
361
+ }
356
362
  fs.writeFileSync(filePath, JSON.stringify(legacyCompatible, null, 4));
357
363
  }
358
364
 
@@ -364,6 +370,28 @@ function removeWebSessionHistory(webHistoryDir, containerName) {
364
370
  }
365
371
  }
366
372
 
373
+ // 容器可能已经被删过一次(例如并发请求、或用户对着一条"仅历史"幽灵条目重复点删除),
374
+ // containerExists 和真正执行删除之间存在竞态,因此仍需 try/catch 兜底,
375
+ // 吞掉"已经不存在"这一类错误,让操作保持幂等。
376
+ function removeContainerIdempotent(ctx, containerName) {
377
+ if (!ctx.containerExists(containerName)) {
378
+ return { removedContainer: false };
379
+ }
380
+ try {
381
+ ctx.removeContainer(containerName);
382
+ return { removedContainer: true };
383
+ } catch (e) {
384
+ return { removedContainer: false, error: e };
385
+ }
386
+ }
387
+
388
+ function removeAllAgentHistoryArtifacts(webHistoryDir, containerName, agentIds) {
389
+ const eventStore = new FileEventStore(webHistoryDir);
390
+ (Array.isArray(agentIds) ? agentIds : []).forEach(agentId => {
391
+ eventStore.remove(buildWebSessionKey(containerName, agentId));
392
+ });
393
+ }
394
+
367
395
  function listWebHistorySessionNames(webHistoryDir, isValidContainerName) {
368
396
  ensureWebHistoryDir(webHistoryDir);
369
397
  return fs.readdirSync(webHistoryDir)
@@ -403,14 +431,19 @@ function parseWebSessionKey(sessionKey) {
403
431
  };
404
432
  }
405
433
 
434
+ function isVisibleWebAgentSession(agentSession) {
435
+ return Boolean(agentSession) && agentSession.archived !== true;
436
+ }
437
+
406
438
  function getWebAgentSession(history, agentId, options = {}) {
407
439
  const sessionHistory = history && typeof history === 'object' ? history : { agents: {} };
408
440
  if (!sessionHistory.agents || typeof sessionHistory.agents !== 'object' || Array.isArray(sessionHistory.agents)) {
409
441
  sessionHistory.agents = {};
410
442
  }
411
443
  const requestedAgentId = String(agentId || WEB_DEFAULT_AGENT_ID).trim() || WEB_DEFAULT_AGENT_ID;
412
- if (sessionHistory.agents[requestedAgentId]) {
413
- return sessionHistory.agents[requestedAgentId];
444
+ const existing = sessionHistory.agents[requestedAgentId];
445
+ if (isVisibleWebAgentSession(existing)) {
446
+ return existing;
414
447
  }
415
448
  if (options.create === true) {
416
449
  const agentSession = createEmptyWebAgentSession(requestedAgentId);
@@ -425,21 +458,20 @@ function listWebAgentSessions(history, options = {}) {
425
458
  const agents = sessionHistory.agents && typeof sessionHistory.agents === 'object' && !Array.isArray(sessionHistory.agents)
426
459
  ? sessionHistory.agents
427
460
  : {};
428
- const agentIds = Object.keys(agents);
429
- if (!agentIds.length && options.includeSyntheticDefault === true) {
430
- return [createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID)];
431
- }
432
- return agentIds
461
+ const visibleSessions = Object.keys(agents)
433
462
  .map(agentId => agents[agentId])
434
- .filter(Boolean)
435
- .sort((a, b) => {
436
- const orderA = a.agentId === WEB_DEFAULT_AGENT_ID ? 0 : 1;
437
- const orderB = b.agentId === WEB_DEFAULT_AGENT_ID ? 0 : 1;
438
- if (orderA !== orderB) {
439
- return orderA - orderB;
440
- }
441
- return String(a.agentName || '').localeCompare(String(b.agentName || ''), 'zh-CN');
442
- });
463
+ .filter(isVisibleWebAgentSession);
464
+ if (!visibleSessions.length && options.includeSyntheticDefault === true) {
465
+ return [{ ...createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID), synthetic: true }];
466
+ }
467
+ return visibleSessions.sort((a, b) => {
468
+ const orderA = a.agentId === WEB_DEFAULT_AGENT_ID ? 0 : 1;
469
+ const orderB = b.agentId === WEB_DEFAULT_AGENT_ID ? 0 : 1;
470
+ if (orderA !== orderB) {
471
+ return orderA - orderB;
472
+ }
473
+ return String(a.agentName || '').localeCompare(String(b.agentName || ''), 'zh-CN');
474
+ });
443
475
  }
444
476
 
445
477
  function getWebAgentCreationRank(agentId) {
@@ -775,6 +807,7 @@ function renderAgentPromptCommand(template, prompt) {
775
807
  function buildCodexAgentExecCommand(template, prompt, options = {}) {
776
808
  const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
777
809
  const sessionId = options && typeof options.sessionId === 'string' ? options.sessionId.trim() : '';
810
+ const model = options && typeof options.model === 'string' ? options.model.trim() : '';
778
811
  const execMatch = templateText.match(
779
812
  /^((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)codex\s+exec\b/
780
813
  );
@@ -784,12 +817,14 @@ function buildCodexAgentExecCommand(template, prompt, options = {}) {
784
817
  const suffix = templateText.slice(execMatch[0].length);
785
818
  const hasJson = /(?:^|\s)--json(?:\s|$)/.test(suffix);
786
819
  const injectedFlags = hasJson ? '' : ' --json';
820
+ const hasModel = /(?:^|\s)(?:-m|--model)(?:\s|$)/.test(suffix);
821
+ const modelFlag = model && !hasModel ? ` --model ${model}` : '';
787
822
  if (sessionId) {
788
823
  const promptIndex = suffix.indexOf('{prompt}');
789
824
  const resumeSuffix = `${suffix.slice(0, promptIndex)}${quoteBashSingleValue(sessionId)} ${suffix.slice(promptIndex)}`;
790
- codexTemplate = `${prefix}codex exec resume${injectedFlags}${resumeSuffix}`;
825
+ codexTemplate = `${prefix}codex exec resume${injectedFlags}${modelFlag}${resumeSuffix}`;
791
826
  } else {
792
- codexTemplate = `${prefix}codex exec${injectedFlags}${suffix}`;
827
+ codexTemplate = `${prefix}codex exec${injectedFlags}${modelFlag}${suffix}`;
793
828
  }
794
829
  }
795
830
  return codexTemplate === templateText
@@ -824,6 +859,10 @@ function buildClaudeAgentExecCommand(template, prompt, options = {}) {
824
859
  if (sessionId) {
825
860
  flagSpecs.push({ flag: `-r ${sessionId}`, pattern: /(?:^|\s)-r(?:\s|$)/ });
826
861
  }
862
+ const model = options && typeof options.model === 'string' ? options.model.trim() : '';
863
+ if (model) {
864
+ flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)--model(?:\s|$)/ });
865
+ }
827
866
  const claudeTemplate = prependAgentFlags(
828
867
  templateText,
829
868
  /^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)claude\b)(.*)$/,
@@ -834,14 +873,19 @@ function buildClaudeAgentExecCommand(template, prompt, options = {}) {
834
873
  : renderAgentPromptCommand(claudeTemplate, prompt);
835
874
  }
836
875
 
837
- function buildGeminiAgentExecCommand(template, prompt) {
876
+ function buildGeminiAgentExecCommand(template, prompt, options = {}) {
838
877
  const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
878
+ const flagSpecs = [
879
+ { flag: '--output-format stream-json', pattern: /(?:^|\s)--output-format(?:\s|$)/ }
880
+ ];
881
+ const model = options && typeof options.model === 'string' ? options.model.trim() : '';
882
+ if (model) {
883
+ flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
884
+ }
839
885
  const geminiTemplate = prependAgentFlags(
840
886
  templateText,
841
887
  /^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)gemini\b)(.*)$/,
842
- [
843
- { flag: '--output-format stream-json', pattern: /(?:^|\s)--output-format(?:\s|$)/ }
844
- ]
888
+ flagSpecs
845
889
  );
846
890
  return geminiTemplate === templateText
847
891
  ? renderAgentPromptCommand(templateText, prompt)
@@ -860,6 +904,10 @@ function buildOpenCodeAgentExecCommand(template, prompt, options = {}) {
860
904
  pattern: /(?:^|\s)(?:--session|-s)(?:\s|$)/
861
905
  });
862
906
  }
907
+ const model = options && typeof options.model === 'string' ? options.model.trim() : '';
908
+ if (model) {
909
+ flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
910
+ }
863
911
  const opencodeTemplate = prependAgentFlags(
864
912
  templateText,
865
913
  /^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)opencode\b.*?\s+run\b)(.*)$/,
@@ -875,7 +923,7 @@ function buildWebAgentExecCommand(template, prompt, agentProgram, options = {})
875
923
  case 'claude':
876
924
  return buildClaudeAgentExecCommand(template, prompt, options);
877
925
  case 'gemini':
878
- return buildGeminiAgentExecCommand(template, prompt);
926
+ return buildGeminiAgentExecCommand(template, prompt, options);
879
927
  case 'codex':
880
928
  return buildCodexAgentExecCommand(template, prompt, options);
881
929
  case 'opencode':
@@ -1138,6 +1186,162 @@ function accumulateUsageTotal(baseline, turnUsage) {
1138
1186
  };
1139
1187
  }
1140
1188
 
1189
+ // 起一个短命的容器内子进程,通过对应协议问一次元信息(初始化握手 / model 列表),拿到结果就杀掉进程——
1190
+ // 不是长驻会话,不会产生真实对话轮次或费用。
1191
+ function runAgentProtocolProcess(ctx, containerName, command, options = {}) {
1192
+ const stdinLines = Array.isArray(options.stdinLines) ? options.stdinLines : [];
1193
+ const onLine = typeof options.onLine === 'function' ? options.onLine : null;
1194
+ const timeoutMs = typeof options.timeoutMs === 'number' ? options.timeoutMs : 8000;
1195
+ return new Promise(resolve => {
1196
+ let settled = false;
1197
+ let pending = '';
1198
+ let stdoutAll = '';
1199
+ let child;
1200
+ try {
1201
+ child = spawn(ctx.dockerCmd, ['exec', '-i', containerName, '/bin/bash', '-lc', command], { stdio: ['pipe', 'pipe', 'pipe'] });
1202
+ } catch (e) {
1203
+ resolve({ result: null, stdout: '' });
1204
+ return;
1205
+ }
1206
+ const timer = setTimeout(() => finish(null), timeoutMs);
1207
+ function finish(result) {
1208
+ if (settled) return;
1209
+ settled = true;
1210
+ clearTimeout(timer);
1211
+ try { child.kill('SIGKILL'); } catch (e) { /* 进程可能已退出 */ }
1212
+ resolve({ result, stdout: stdoutAll });
1213
+ }
1214
+ child.on('error', () => finish(null));
1215
+ child.on('close', () => finish(null));
1216
+ child.stdout.on('data', chunk => {
1217
+ const text = chunk.toString('utf-8');
1218
+ stdoutAll += text;
1219
+ if (!onLine) {
1220
+ return;
1221
+ }
1222
+ pending += text;
1223
+ let newlineIndex = pending.indexOf('\n');
1224
+ while (newlineIndex !== -1) {
1225
+ const line = pending.slice(0, newlineIndex);
1226
+ pending = pending.slice(newlineIndex + 1);
1227
+ const trimmed = line.trim();
1228
+ if (trimmed) {
1229
+ const payload = parseJsonObjectLine(trimmed);
1230
+ if (payload) {
1231
+ const matched = onLine(payload);
1232
+ if (matched !== undefined) {
1233
+ finish(matched);
1234
+ return;
1235
+ }
1236
+ }
1237
+ }
1238
+ newlineIndex = pending.indexOf('\n');
1239
+ }
1240
+ });
1241
+ try {
1242
+ stdinLines.forEach(line => child.stdin.write(`${line}\n`));
1243
+ if (!stdinLines.length) {
1244
+ child.stdin.end();
1245
+ }
1246
+ } catch (e) { /* 进程可能已提前退出 */ }
1247
+ });
1248
+ }
1249
+
1250
+ function normalizeAgentModelCatalogEntry(item) {
1251
+ const value = pickFirstString(item && item.value, item && item.id, item && item.model);
1252
+ if (!value) {
1253
+ return null;
1254
+ }
1255
+ return {
1256
+ value,
1257
+ label: pickFirstString(item && item.displayName, value),
1258
+ description: pickFirstString(item && item.description)
1259
+ };
1260
+ }
1261
+
1262
+ function extractClaudeModelsFromControlResponse(payload) {
1263
+ if (!payload || payload.type !== 'control_response') {
1264
+ return undefined;
1265
+ }
1266
+ const response = toPlainObject(payload.response);
1267
+ if (response.request_id !== 'manyoyo-model-catalog') {
1268
+ return undefined;
1269
+ }
1270
+ const inner = toPlainObject(response.response);
1271
+ const models = Array.isArray(inner.models) ? inner.models : [];
1272
+ return models.map(normalizeAgentModelCatalogEntry).filter(Boolean);
1273
+ }
1274
+
1275
+ function extractCodexModelsFromListResponse(payload) {
1276
+ if (!payload || payload.id !== 'manyoyo-model-list' || !payload.result) {
1277
+ return undefined;
1278
+ }
1279
+ const data = Array.isArray(payload.result.data) ? payload.result.data : [];
1280
+ return data.map(normalizeAgentModelCatalogEntry).filter(Boolean);
1281
+ }
1282
+
1283
+ function extractOpenCodeModelsFromOutput(text) {
1284
+ return String(text || '')
1285
+ .split('\n')
1286
+ .map(line => line.trim())
1287
+ .filter(Boolean)
1288
+ .map(value => ({ value, label: value, description: '' }));
1289
+ }
1290
+
1291
+ async function fetchAgentModelCatalog(ctx, containerName, agentProgram) {
1292
+ if (agentProgram === 'claude') {
1293
+ const requestLine = JSON.stringify({
1294
+ type: 'control_request',
1295
+ request_id: 'manyoyo-model-catalog',
1296
+ request: { subtype: 'initialize' }
1297
+ });
1298
+ const { result } = await runAgentProtocolProcess(
1299
+ ctx,
1300
+ containerName,
1301
+ 'claude -p --input-format stream-json --output-format stream-json --verbose',
1302
+ { stdinLines: [requestLine], onLine: extractClaudeModelsFromControlResponse }
1303
+ );
1304
+ return Array.isArray(result) ? result : [];
1305
+ }
1306
+ if (agentProgram === 'codex') {
1307
+ const stdinLines = [
1308
+ JSON.stringify({ jsonrpc: '2.0', id: 'manyoyo-init', method: 'initialize', params: { clientInfo: { name: 'manyoyo', version: '1.0.0' } } }),
1309
+ JSON.stringify({ jsonrpc: '2.0', id: 'manyoyo-model-list', method: 'model/list', params: {} })
1310
+ ];
1311
+ const { result } = await runAgentProtocolProcess(
1312
+ ctx,
1313
+ containerName,
1314
+ 'codex app-server',
1315
+ { stdinLines, onLine: extractCodexModelsFromListResponse }
1316
+ );
1317
+ return Array.isArray(result) ? result : [];
1318
+ }
1319
+ if (agentProgram === 'opencode') {
1320
+ const { stdout } = await runAgentProtocolProcess(ctx, containerName, 'opencode models', {});
1321
+ return extractOpenCodeModelsFromOutput(stdout);
1322
+ }
1323
+ // Gemini 的 --acp 模式在认证前就会挂起重试,没有可用的“认证前”探测窗口,暂不支持目录抓取。
1324
+ return [];
1325
+ }
1326
+
1327
+ const SAFE_AGENT_MODEL_VALUE_PATTERN = /^[A-Za-z0-9._\-[\]/]+$/;
1328
+
1329
+ function normalizeAgentModelValueInput(value) {
1330
+ const trimmed = String(value || '').trim();
1331
+ if (!trimmed) {
1332
+ return '';
1333
+ }
1334
+ if (!SAFE_AGENT_MODEL_VALUE_PATTERN.test(trimmed)) {
1335
+ throw new Error(`模型名称包含非法字符: ${trimmed}`);
1336
+ }
1337
+ return trimmed;
1338
+ }
1339
+
1340
+ function normalizeStoredAgentModelValue(value) {
1341
+ const trimmed = typeof value === 'string' ? value.trim() : '';
1342
+ return SAFE_AGENT_MODEL_VALUE_PATTERN.test(trimmed) ? trimmed : '';
1343
+ }
1344
+
1141
1345
  function getAgentRuntimeMeta(template) {
1142
1346
  const normalizedTemplate = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
1143
1347
  const agentProgram = resolveAgentProgram(normalizedTemplate);
@@ -1843,7 +2047,8 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
1843
2047
  ? prompt
1844
2048
  : buildAgentPromptWithHistory(agentSession, prompt);
1845
2049
  const command = buildWebAgentExecCommand(effectiveTemplate, effectivePrompt, agentMeta.agentProgram, {
1846
- sessionId: sessionIdForRun
2050
+ sessionId: sessionIdForRun,
2051
+ model: agentSession.model || ''
1847
2052
  });
1848
2053
  const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
1849
2054
 
@@ -2844,19 +3049,6 @@ async function createClonedContainer(ctx, state, sourceContainerName, requestedN
2844
3049
  };
2845
3050
  }
2846
3051
 
2847
- function resolveContainerNamesByHostPath(ctx, state, hostPath) {
2848
- const target = String(hostPath || '').trim();
2849
- const containerMap = listWebManyoyoContainers(ctx);
2850
- const names = new Set([
2851
- ...Object.keys(containerMap),
2852
- ...listWebHistorySessionNames(state.webHistoryDir, ctx.isValidContainerName)
2853
- ]);
2854
- return Array.from(names).filter(name => {
2855
- const summary = buildSessionSummary(ctx, state, containerMap, { containerName: name, agentId: WEB_DEFAULT_AGENT_ID });
2856
- return summary && String(summary.hostPath || '').trim() === target;
2857
- });
2858
- }
2859
-
2860
3052
  // Estimate container start time from "Up X hours/minutes/seconds" status string.
2861
3053
  // Uses relative time to avoid Podman Machine VM clock drift issues.
2862
3054
  function estimateStartTimeFromStatus(status) {
@@ -3606,11 +3798,13 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
3606
3798
  const containerName = sessionRef && sessionRef.containerName ? sessionRef.containerName : '';
3607
3799
  const agentId = sessionRef && sessionRef.agentId ? sessionRef.agentId : WEB_DEFAULT_AGENT_ID;
3608
3800
  const history = loadWebSessionHistory(state.webHistoryDir, containerName);
3609
- const agentSession = getWebAgentSession(history, agentId)
3801
+ const realAgentSession = getWebAgentSession(history, agentId);
3802
+ const agentSession = realAgentSession
3610
3803
  || (agentId === WEB_DEFAULT_AGENT_ID ? createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID) : null);
3611
3804
  if (!agentSession) {
3612
3805
  return null;
3613
3806
  }
3807
+ const synthetic = !realAgentSession;
3614
3808
  const latestMessage = agentSession.messages.length ? agentSession.messages[agentSession.messages.length - 1] : null;
3615
3809
  const containerInfo = containerMap[containerName] || {};
3616
3810
  const effectiveAgentPromptCommand = resolveEffectiveAgentPromptCommandForSession(history, agentId, containerInfo.defaultCommand);
@@ -3645,8 +3839,10 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
3645
3839
  agentEnabled: isAgentPromptCommandEnabled(effectiveAgentPromptCommand),
3646
3840
  agentProgram: effectiveAgentProgram || '',
3647
3841
  resumeSupported: effectiveResumeSupported,
3842
+ model: agentSession.model || '',
3648
3843
  hostPath: applied.hostPath || '',
3649
- containerPath: applied.containerPath || ''
3844
+ containerPath: applied.containerPath || '',
3845
+ ...(synthetic ? { synthetic: true } : {})
3650
3846
  };
3651
3847
  }
3652
3848
 
@@ -3731,6 +3927,7 @@ function buildSessionDetail(ctx, state, containerMap, name) {
3731
3927
  lastResumeOk: typeof agentSession.lastResumeOk === 'boolean' ? agentSession.lastResumeOk : null,
3732
3928
  lastResumeError: agentSession.lastResumeError || '',
3733
3929
  usageTotal: agentSession.usageTotal || null,
3930
+ model: agentSession.model || '',
3734
3931
  applied
3735
3932
  };
3736
3933
  }
@@ -4392,37 +4589,6 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4392
4589
  sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), remark });
4393
4590
  }
4394
4591
  },
4395
- {
4396
- method: 'POST',
4397
- match: currentPath => currentPath === '/api/sessions/by-directory/remove' ? [] : null,
4398
- handler: async () => {
4399
- const payload = await readJsonBody(req);
4400
- const hostPath = typeof payload.hostPath === 'string' ? payload.hostPath : '';
4401
- if (!hostPath.trim()) {
4402
- sendJson(res, 400, { error: 'hostPath 不能为空' });
4403
- return;
4404
- }
4405
- const targets = resolveContainerNamesByHostPath(ctx, state, hostPath);
4406
- if (!targets.length) {
4407
- sendJson(res, 404, { error: '未找到该工作目录下的容器' });
4408
- return;
4409
- }
4410
- const removed = [];
4411
- const errors = [];
4412
- targets.forEach(name => {
4413
- try {
4414
- if (ctx.containerExists(name)) {
4415
- ctx.removeContainer(name);
4416
- }
4417
- removeWebSessionHistory(state.webHistoryDir, name);
4418
- removed.push(name);
4419
- } catch (e) {
4420
- errors.push({ name, error: e.message || String(e) });
4421
- }
4422
- });
4423
- sendJson(res, 200, { hostPath, removed, errors });
4424
- }
4425
- },
4426
4592
  {
4427
4593
  method: 'GET',
4428
4594
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/messages$/),
@@ -4648,6 +4814,51 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4648
4814
  sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), detail });
4649
4815
  }
4650
4816
  },
4817
+ {
4818
+ method: 'GET',
4819
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/models$/),
4820
+ handler: async match => {
4821
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
4822
+ if (!sessionRef) {
4823
+ return;
4824
+ }
4825
+ const containerMap = listWebManyoyoContainers(ctx);
4826
+ const containerInfo = containerMap[sessionRef.containerName] || {};
4827
+ const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
4828
+ const effectiveTemplate = resolveEffectiveAgentPromptCommandForSession(
4829
+ history,
4830
+ sessionRef.agentId,
4831
+ containerInfo.defaultCommand
4832
+ );
4833
+ const agentProgram = resolveAgentProgram(effectiveTemplate);
4834
+ const models = await fetchAgentModelCatalog(ctx, sessionRef.containerName, agentProgram);
4835
+ sendJson(res, 200, {
4836
+ name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
4837
+ agentProgram,
4838
+ models
4839
+ });
4840
+ }
4841
+ },
4842
+ {
4843
+ method: 'POST',
4844
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/model$/),
4845
+ handler: async match => {
4846
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
4847
+ if (!sessionRef) {
4848
+ return;
4849
+ }
4850
+ const payload = await readJsonBody(req);
4851
+ let model;
4852
+ try {
4853
+ model = normalizeAgentModelValueInput(payload.model);
4854
+ } catch (e) {
4855
+ sendJson(res, 400, { error: e.message || String(e) });
4856
+ return;
4857
+ }
4858
+ patchWebAgentSessionState(state.webHistoryDir, sessionRef, { model });
4859
+ sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), model });
4860
+ }
4861
+ },
4651
4862
  {
4652
4863
  method: 'GET',
4653
4864
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/audit$/),
@@ -5013,12 +5224,23 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5013
5224
  return;
5014
5225
  }
5015
5226
 
5016
- if (ctx.containerExists(sessionRef.containerName)) {
5017
- ctx.removeContainer(sessionRef.containerName);
5227
+ const payload = await readJsonBody(req);
5228
+ const removeHistory = payload.removeHistory === true;
5229
+ const { removedContainer } = removeContainerIdempotent(ctx, sessionRef.containerName);
5230
+
5231
+ if (removeHistory) {
5232
+ const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
5233
+ removeAllAgentHistoryArtifacts(state.webHistoryDir, sessionRef.containerName, Object.keys(history.agents || {}));
5234
+ removeWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
5235
+ } else if (removedContainer) {
5018
5236
  appendWebSessionMessage(state.webHistoryDir, sessionRef, 'system', `容器 ${sessionRef.containerName} 已删除。`);
5019
5237
  }
5020
5238
 
5021
- sendJson(res, 200, { removed: true, name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId) });
5239
+ sendJson(res, 200, {
5240
+ removed: true,
5241
+ removedHistory: removeHistory,
5242
+ name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId)
5243
+ });
5022
5244
  }
5023
5245
  },
5024
5246
  {
@@ -5030,12 +5252,17 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5030
5252
  return;
5031
5253
  }
5032
5254
 
5255
+ const payload = await readJsonBody(req);
5256
+ const removeHistory = payload.removeHistory === true;
5257
+
5033
5258
  const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
5034
5259
  if (history.agents && typeof history.agents === 'object') {
5035
- if (sessionRef.agentId === WEB_DEFAULT_AGENT_ID) {
5036
- delete history.agents[WEB_DEFAULT_AGENT_ID];
5037
- } else {
5260
+ if (removeHistory) {
5038
5261
  delete history.agents[sessionRef.agentId];
5262
+ removeAllAgentHistoryArtifacts(state.webHistoryDir, sessionRef.containerName, [sessionRef.agentId]);
5263
+ } else if (history.agents[sessionRef.agentId]) {
5264
+ history.agents[sessionRef.agentId].archived = true;
5265
+ history.agents[sessionRef.agentId].updatedAt = new Date().toISOString();
5039
5266
  }
5040
5267
  }
5041
5268
  if (!Object.keys(history.agents || {}).length && !ctx.containerExists(sessionRef.containerName)) {
@@ -5044,7 +5271,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5044
5271
  saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
5045
5272
  }
5046
5273
  sendJson(res, 200, {
5047
- removedHistory: true,
5274
+ removedHistory: removeHistory,
5048
5275
  name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId)
5049
5276
  });
5050
5277
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "6.2.9",
3
+ "version": "6.2.11",
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",