@xcanwin/manyoyo 6.2.10 → 6.2.14

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 = {
@@ -272,6 +272,13 @@ textarea:focus-visible {
272
272
  display: none !important;
273
273
  }
274
274
 
275
+ /* showNotice/showConfirm/showPrompt 可能从已经打开的其它 modal(如目录选择器)内部触发,
276
+ 和其它 .modal-backdrop 共用 z-index:80 时,谁在 DOM 里排后面谁盖住谁,容易被埋到底下;
277
+ 这里给一个更高的 z-index,保证它始终浮在最上层 */
278
+ #genericDialogModal {
279
+ z-index: 90;
280
+ }
281
+
275
282
  .modal {
276
283
  width: min(960px, calc(100vw - 24px));
277
284
  max-height: min(92vh, 860px);
@@ -284,6 +291,10 @@ textarea:focus-visible {
284
291
  overflow: hidden;
285
292
  }
286
293
 
294
+ .modal.modal-sm {
295
+ width: min(420px, calc(100vw - 24px));
296
+ }
297
+
287
298
  .modal-header {
288
299
  display: flex;
289
300
  align-items: center;
@@ -322,6 +333,28 @@ textarea:focus-visible {
322
333
  word-break: break-all;
323
334
  }
324
335
 
336
+ .modal-message {
337
+ margin: 0;
338
+ color: var(--text);
339
+ font-size: 13px;
340
+ line-height: 1.6;
341
+ }
342
+
343
+ .modal-dialog-input {
344
+ width: 100%;
345
+ margin-top: 10px;
346
+ border: 1px solid var(--line);
347
+ border-radius: 10px;
348
+ padding: 9px 11px;
349
+ background: var(--panel-strong);
350
+ color: var(--text);
351
+ font-size: 13px;
352
+ }
353
+
354
+ .modal-dialog-input[hidden] {
355
+ display: none;
356
+ }
357
+
325
358
  .modal-error {
326
359
  margin-top: 10px;
327
360
  color: var(--danger);
@@ -573,6 +606,9 @@ textarea:focus-visible {
573
606
 
574
607
  .crumb-item:hover {
575
608
  transform: none;
609
+ /* 不显式设置背景色的话,通用 button:hover 规则的深色背景会覆盖进来,
610
+ 和同色系的文字撞色看不清,这里保持透明背景即可 */
611
+ background: transparent;
576
612
  text-decoration: underline;
577
613
  }
578
614
 
@@ -602,6 +638,11 @@ textarea:focus-visible {
602
638
  border-radius: 999px;
603
639
  }
604
640
 
641
+ .crumb-jump-active:hover {
642
+ /* 同 .crumb-item:hover,避免通用 button:hover 的深色背景和文字撞色 */
643
+ background: var(--panel-soft);
644
+ }
645
+
605
646
  .session-card {
606
647
  position: relative;
607
648
  }
@@ -619,7 +660,7 @@ textarea:focus-visible {
619
660
 
620
661
  .session-card-container .session-card-button,
621
662
  .session-card-agent .session-card-button {
622
- padding-right: 38px;
663
+ padding-right: 46px;
623
664
  }
624
665
 
625
666
  .session-card-button:hover {
@@ -747,19 +788,28 @@ textarea:focus-visible {
747
788
  }
748
789
  }
749
790
 
791
+ .crumb-bar .tree-node-menu {
792
+ position: relative;
793
+ top: auto;
794
+ right: auto;
795
+ transform: none;
796
+ opacity: 1;
797
+ pointer-events: auto;
798
+ }
799
+
750
800
  .tree-node-menu-trigger {
751
- width: 26px;
752
- height: 26px;
801
+ width: 30px;
802
+ height: 30px;
753
803
  padding: 0;
754
804
  display: inline-flex;
755
805
  align-items: center;
756
806
  justify-content: center;
757
- border-radius: 8px;
807
+ border-radius: 9px;
758
808
  }
759
809
 
760
810
  .tree-node-menu-trigger svg {
761
- width: 14px;
762
- height: 14px;
811
+ width: 16px;
812
+ height: 16px;
763
813
  fill: currentColor;
764
814
  }
765
815
 
@@ -215,6 +215,39 @@
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" class="modal-message"></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
+
234
+ <div id="genericDialogModal" class="modal-backdrop" hidden>
235
+ <section class="modal modal-sm" role="dialog" aria-modal="true" aria-labelledby="genericDialogTitle">
236
+ <header class="modal-header">
237
+ <h2 id="genericDialogTitle"></h2>
238
+ <button type="button" id="genericDialogCloseBtn" class="secondary">关闭</button>
239
+ </header>
240
+ <div class="modal-body">
241
+ <p id="genericDialogMessage" class="modal-message"></p>
242
+ <input type="text" id="genericDialogInput" class="modal-dialog-input" hidden />
243
+ </div>
244
+ <footer class="modal-footer">
245
+ <button type="button" id="genericDialogCancelBtn" class="secondary" hidden>取消</button>
246
+ <button type="button" id="genericDialogConfirmBtn">确定</button>
247
+ </footer>
248
+ </section>
249
+ </div>
250
+
218
251
  <div id="directoryPickerModal" class="modal-backdrop" hidden>
219
252
  <section class="modal dir-picker-modal" role="dialog" aria-modal="true" aria-labelledby="directoryPickerTitle">
220
253
  <header class="modal-header">
@@ -179,6 +179,19 @@
179
179
  const createEnv = document.getElementById('createEnv');
180
180
  const createEnvFile = document.getElementById('createEnvFile');
181
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');
188
+ const genericDialogModal = document.getElementById('genericDialogModal');
189
+ const genericDialogTitle = document.getElementById('genericDialogTitle');
190
+ const genericDialogMessage = document.getElementById('genericDialogMessage');
191
+ const genericDialogInput = document.getElementById('genericDialogInput');
192
+ const genericDialogCloseBtn = document.getElementById('genericDialogCloseBtn');
193
+ const genericDialogCancelBtn = document.getElementById('genericDialogCancelBtn');
194
+ const genericDialogConfirmBtn = document.getElementById('genericDialogConfirmBtn');
182
195
  const directoryPickerModal = document.getElementById('directoryPickerModal');
183
196
  const directoryPickerTitle = document.getElementById('directoryPickerTitle');
184
197
  const directoryPickerTip = document.getElementById('directoryPickerTip');
@@ -1330,7 +1343,7 @@
1330
1343
  }
1331
1344
  const detail = await ensureActiveSessionDetail();
1332
1345
  if (!detail) {
1333
- alert(state.sessionDetailError || '当前会话详情暂时不可用');
1346
+ await showNotice(state.sessionDetailError || '当前会话详情暂时不可用');
1334
1347
  return;
1335
1348
  }
1336
1349
  state.agentTemplateModalOpen = true;
@@ -1600,7 +1613,7 @@
1600
1613
  }
1601
1614
  const detail = await ensureActiveSessionDetail();
1602
1615
  if (!detail) {
1603
- alert(state.sessionDetailError || '当前会话详情暂时不可用');
1616
+ await showNotice(state.sessionDetailError || '当前会话详情暂时不可用');
1604
1617
  return;
1605
1618
  }
1606
1619
  state.modelModalOpen = true;
@@ -2635,7 +2648,7 @@
2635
2648
  state.configModalOpen = true;
2636
2649
  setModalVisible(configModal, true);
2637
2650
  } catch (e) {
2638
- alert(e.message);
2651
+ await showNotice(e.message);
2639
2652
  } finally {
2640
2653
  state.configLoading = false;
2641
2654
  syncUi();
@@ -2690,7 +2703,7 @@
2690
2703
  state.createModalOpen = true;
2691
2704
  setModalVisible(createModal, true);
2692
2705
  } catch (e) {
2693
- alert(e.message);
2706
+ await showNotice(e.message);
2694
2707
  } finally {
2695
2708
  state.createLoading = false;
2696
2709
  syncUi();
@@ -2822,7 +2835,7 @@
2822
2835
  if (picker.loading || !picker.currentPath) {
2823
2836
  return;
2824
2837
  }
2825
- const input = window.prompt('请输入新目录名称');
2838
+ const input = await showPrompt('请输入新目录名称');
2826
2839
  const name = String(input || '').trim();
2827
2840
  if (!name) {
2828
2841
  return;
@@ -2928,8 +2941,8 @@
2928
2941
  Promise.all([
2929
2942
  loadMessagesForSession(sessionName),
2930
2943
  loadSessionDetailForSession(sessionName)
2931
- ]).catch(function (e) {
2932
- alert(e.message);
2944
+ ]).catch(async function (e) {
2945
+ await showNotice(e.message);
2933
2946
  });
2934
2947
  }
2935
2948
 
@@ -2955,7 +2968,7 @@
2955
2968
  closeMobileSessionPanel();
2956
2969
  }
2957
2970
  } catch (e) {
2958
- alert(e.message);
2971
+ await showNotice(e.message);
2959
2972
  } finally {
2960
2973
  state.creatingAgent = false;
2961
2974
  syncUi();
@@ -3126,20 +3139,56 @@
3126
3139
  panel.className = 'tree-node-menu-panel';
3127
3140
  panel.hidden = true;
3128
3141
 
3142
+ // 面板展开时挂到 body 上用 position:fixed 定位,避免被 #sessionList 之类的
3143
+ // overflow:auto 滚动容器裁切(面板本来是靠 .tree-node-menu 的 position:absolute
3144
+ // 定位在卡片内的,一旦触发按钮接近滚动容器底部,面板就会被截断)。
3145
+ // 调用时 panel 必须已经是可见状态(display 不为 none),否则 offsetHeight 测不出真实高度。
3146
+ function positionPanel() {
3147
+ const rect = trigger.getBoundingClientRect();
3148
+ panel.style.position = 'fixed';
3149
+ panel.style.left = 'auto';
3150
+ panel.style.right = (window.innerWidth - rect.right) + 'px';
3151
+ panel.style.top = (rect.bottom + 4) + 'px';
3152
+ panel.style.bottom = 'auto';
3153
+ const panelHeight = panel.offsetHeight;
3154
+ if (rect.bottom + 4 + panelHeight > window.innerHeight) {
3155
+ // 下方空间不够时翻转到触发按钮上方展开,避免超出视口底部
3156
+ panel.style.top = 'auto';
3157
+ panel.style.bottom = (window.innerHeight - rect.top + 4) + 'px';
3158
+ }
3159
+ }
3160
+
3129
3161
  function close() {
3130
3162
  panel.hidden = true;
3131
3163
  trigger.setAttribute('aria-expanded', 'false');
3132
3164
  wrap.classList.remove('is-open');
3165
+ if (panel.parentNode === document.body) {
3166
+ wrap.appendChild(panel);
3167
+ }
3168
+ panel.style.position = '';
3169
+ panel.style.top = '';
3170
+ panel.style.bottom = '';
3171
+ panel.style.left = '';
3172
+ panel.style.right = '';
3133
3173
  }
3134
3174
 
3135
3175
  trigger.addEventListener('click', function (event) {
3136
3176
  event.stopPropagation();
3137
3177
  if (panel.hidden) {
3138
3178
  closeOpenTreeNodeMenu();
3179
+ document.body.appendChild(panel);
3139
3180
  panel.hidden = false;
3181
+ positionPanel();
3140
3182
  trigger.setAttribute('aria-expanded', 'true');
3141
3183
  wrap.classList.add('is-open');
3142
- openTreeNodeMenu = { wrap, close };
3184
+ openTreeNodeMenu = {
3185
+ close: close,
3186
+ containsTarget: function (target) {
3187
+ // 面板展开时是 body 的直接子节点,不再是 wrap 的后代,
3188
+ // “点击外部关闭”判断要把它也算进来
3189
+ return wrap.contains(target) || panel.contains(target);
3190
+ }
3191
+ };
3143
3192
  } else {
3144
3193
  close();
3145
3194
  openTreeNodeMenu = null;
@@ -3167,11 +3216,124 @@
3167
3216
  return wrap;
3168
3217
  }
3169
3218
 
3219
+ function confirmRemoveChoice(options) {
3220
+ const opts = options && typeof options === 'object' ? options : {};
3221
+ if (!removeConfirmModal) {
3222
+ return Promise.resolve(confirm(opts.message || '确认删除?') ? 'with-history' : null);
3223
+ }
3224
+ return new Promise(function (resolve) {
3225
+ if (removeConfirmTitle) {
3226
+ removeConfirmTitle.textContent = opts.title || '确认删除';
3227
+ }
3228
+ if (removeConfirmMessage) {
3229
+ removeConfirmMessage.textContent = opts.message || '';
3230
+ }
3231
+ let settled = false;
3232
+ function finish(value) {
3233
+ if (settled) return;
3234
+ settled = true;
3235
+ setModalVisible(removeConfirmModal, false);
3236
+ removeConfirmKeepHistoryBtn.removeEventListener('click', onKeepHistory);
3237
+ removeConfirmWithHistoryBtn.removeEventListener('click', onWithHistory);
3238
+ removeConfirmCancelBtn.removeEventListener('click', onCancel);
3239
+ resolve(value);
3240
+ }
3241
+ function onKeepHistory() { finish('keep-history'); }
3242
+ function onWithHistory() { finish('with-history'); }
3243
+ function onCancel() { finish(null); }
3244
+ removeConfirmKeepHistoryBtn.addEventListener('click', onKeepHistory);
3245
+ removeConfirmWithHistoryBtn.addEventListener('click', onWithHistory);
3246
+ removeConfirmCancelBtn.addEventListener('click', onCancel);
3247
+ setModalVisible(removeConfirmModal, true);
3248
+ });
3249
+ }
3250
+
3251
+ // 原生 alert/confirm/prompt 是同步阻塞的,会卡住整个页面(动画、其它交互全部停摆)。
3252
+ // 这里用一个通用的 Promise 化小弹窗承载 notice/confirm/prompt 三种形态,取代它们。
3253
+ function openGenericDialog(config) {
3254
+ const opts = config && typeof config === 'object' ? config : {};
3255
+ const mode = opts.mode === 'prompt' || opts.mode === 'confirm' ? opts.mode : 'notice';
3256
+ if (!genericDialogModal) {
3257
+ if (mode === 'prompt') {
3258
+ return Promise.resolve(window.prompt(opts.message || '', opts.defaultValue || ''));
3259
+ }
3260
+ if (mode === 'confirm') {
3261
+ return Promise.resolve(confirm(opts.message || ''));
3262
+ }
3263
+ alert(opts.message || '');
3264
+ return Promise.resolve();
3265
+ }
3266
+ return new Promise(function (resolve) {
3267
+ genericDialogTitle.textContent = opts.title
3268
+ || (mode === 'notice' ? '提示' : (mode === 'confirm' ? '请确认' : '请输入'));
3269
+ genericDialogMessage.textContent = opts.message || '';
3270
+ genericDialogCancelBtn.hidden = mode === 'notice';
3271
+ genericDialogInput.hidden = mode !== 'prompt';
3272
+ genericDialogInput.value = mode === 'prompt' ? String(opts.defaultValue || '') : '';
3273
+ genericDialogConfirmBtn.textContent = mode === 'notice' ? '知道了' : '确定';
3274
+
3275
+ let settled = false;
3276
+ function resolveValue() {
3277
+ if (mode === 'confirm') return true;
3278
+ if (mode === 'prompt') return genericDialogInput.value;
3279
+ return undefined;
3280
+ }
3281
+ function cancelValue() {
3282
+ if (mode === 'confirm') return false;
3283
+ if (mode === 'prompt') return null;
3284
+ return undefined;
3285
+ }
3286
+ function finish(value) {
3287
+ if (settled) return;
3288
+ settled = true;
3289
+ setModalVisible(genericDialogModal, false);
3290
+ genericDialogConfirmBtn.removeEventListener('click', onConfirm);
3291
+ genericDialogCancelBtn.removeEventListener('click', onCancel);
3292
+ genericDialogCloseBtn.removeEventListener('click', onCancel);
3293
+ genericDialogInput.removeEventListener('keydown', onInputKeydown);
3294
+ resolve(value);
3295
+ }
3296
+ function onConfirm() { finish(resolveValue()); }
3297
+ function onCancel() { finish(cancelValue()); }
3298
+ function onInputKeydown(event) {
3299
+ if (event.key === 'Enter') {
3300
+ event.preventDefault();
3301
+ onConfirm();
3302
+ }
3303
+ }
3304
+ genericDialogConfirmBtn.addEventListener('click', onConfirm);
3305
+ genericDialogCancelBtn.addEventListener('click', onCancel);
3306
+ genericDialogCloseBtn.addEventListener('click', onCancel);
3307
+ genericDialogInput.addEventListener('keydown', onInputKeydown);
3308
+ setModalVisible(genericDialogModal, true);
3309
+ if (mode === 'prompt') {
3310
+ genericDialogInput.focus();
3311
+ genericDialogInput.select();
3312
+ }
3313
+ });
3314
+ }
3315
+
3316
+ function showNotice(message, title) {
3317
+ return openGenericDialog({ mode: 'notice', message: message, title: title });
3318
+ }
3319
+
3320
+ function showConfirm(message, title) {
3321
+ return openGenericDialog({ mode: 'confirm', message: message, title: title });
3322
+ }
3323
+
3324
+ function showPrompt(message, defaultValue, title) {
3325
+ return openGenericDialog({ mode: 'prompt', message: message, defaultValue: defaultValue, title: title });
3326
+ }
3327
+
3170
3328
  async function removeContainerByName(containerName) {
3171
3329
  const target = String(containerName || '').trim();
3172
3330
  if (!target) return;
3173
- const yes = confirm('确认删除容器 ' + target + ' ?');
3174
- if (!yes) return;
3331
+ const choice = await confirmRemoveChoice({
3332
+ title: '删除容器',
3333
+ message: `确认删除容器 ${target}?可以选择是否同时删除该容器的全部历史记录(消息与事件日志)。`
3334
+ });
3335
+ if (!choice) return;
3336
+ const removeHistory = choice === 'with-history';
3175
3337
  try {
3176
3338
  const wasActiveContainer = parseSessionKey(state.active).containerName === target;
3177
3339
  if (parseSessionKey(state.terminal.sessionName).containerName === target
@@ -3179,7 +3341,8 @@
3179
3341
  disconnectTerminal('容器删除,终端已断开', true);
3180
3342
  }
3181
3343
  await api('/api/sessions/' + encodeURIComponent(target) + '/remove', {
3182
- method: 'POST'
3344
+ method: 'POST',
3345
+ body: JSON.stringify({ removeHistory })
3183
3346
  });
3184
3347
  await refreshSessions({
3185
3348
  preferredName: wasActiveContainer ? target : (state.active || ''),
@@ -3187,15 +3350,19 @@
3187
3350
  reloadMessages: wasActiveContainer
3188
3351
  });
3189
3352
  } catch (e) {
3190
- alert(e.message);
3353
+ await showNotice(e.message);
3191
3354
  }
3192
3355
  }
3193
3356
 
3194
3357
  async function removeAgentSessionByName(sessionName, agentLabel) {
3195
3358
  const target = String(sessionName || '').trim();
3196
3359
  if (!target) return;
3197
- const yes = confirm('确认删除 AGENT ' + (agentLabel || target) + ' ?');
3198
- if (!yes) return;
3360
+ const choice = await confirmRemoveChoice({
3361
+ title: '删除 AGENT',
3362
+ message: `确认删除 AGENT ${agentLabel || target}?可以选择是否同时删除它的历史记录(消息与事件日志)。`
3363
+ });
3364
+ if (!choice) return;
3365
+ const removeHistory = choice === 'with-history';
3199
3366
  const isActive = target === state.active;
3200
3367
  try {
3201
3368
  const containerName = parseSessionKey(target).containerName;
@@ -3203,7 +3370,8 @@
3203
3370
  ? findPreferredSessionNameAfterRemoval(state.sessions, target)
3204
3371
  : '';
3205
3372
  await api('/api/sessions/' + encodeURIComponent(target) + '/remove-with-history', {
3206
- method: 'POST'
3373
+ method: 'POST',
3374
+ body: JSON.stringify({ removeHistory })
3207
3375
  });
3208
3376
  await refreshSessions({
3209
3377
  preferredName: isActive ? (fallbackSessionName || '') : (state.active || ''),
@@ -3212,7 +3380,7 @@
3212
3380
  reloadMessages: isActive
3213
3381
  });
3214
3382
  } catch (e) {
3215
- alert(e.message);
3383
+ await showNotice(e.message);
3216
3384
  }
3217
3385
  }
3218
3386
 
@@ -3296,6 +3464,9 @@
3296
3464
  if (!sessionBreadcrumb) {
3297
3465
  return;
3298
3466
  }
3467
+ // 展开中的菜单面板会被挂到 body 上(见 createTreeNodeMenu),
3468
+ // 这里要重建 DOM 之前先关掉,否则旧面板会变成脱离 wrap 的孤儿节点
3469
+ closeOpenTreeNodeMenu();
3299
3470
  sessionBreadcrumb.innerHTML = '';
3300
3471
 
3301
3472
  const rootCrumb = document.createElement('button');
@@ -3319,11 +3490,50 @@
3319
3490
  sep.textContent = '›';
3320
3491
  sessionBreadcrumb.appendChild(sep);
3321
3492
 
3493
+ const sessionForCrumb = state.sessions.find(function (session) {
3494
+ return getSessionContainerName(session) === state.navContainer;
3495
+ });
3496
+ const containerRemark = sessionForCrumb && sessionForCrumb.containerRemark ? sessionForCrumb.containerRemark : '';
3497
+ const containerGroupForMenu = { containerName: state.navContainer, containerRemark: containerRemark };
3498
+
3322
3499
  const containerCrumb = document.createElement('span');
3323
3500
  containerCrumb.className = 'crumb-item is-current';
3324
- containerCrumb.textContent = state.navContainer;
3325
- containerCrumb.title = state.navContainer;
3501
+ containerCrumb.textContent = containerRemark || state.navContainer;
3502
+ containerCrumb.title = containerRemark ? `${containerRemark}(${state.navContainer})` : state.navContainer;
3326
3503
  sessionBreadcrumb.appendChild(containerCrumb);
3504
+ sessionBreadcrumb.appendChild(createTreeNodeMenu([
3505
+ {
3506
+ label: '新建 AGENT',
3507
+ onClick: function () {
3508
+ createAgentSession(containerGroupForMenu.containerName);
3509
+ }
3510
+ },
3511
+ {
3512
+ label: '编辑备注',
3513
+ onClick: function () {
3514
+ editContainerRemark(containerGroupForMenu);
3515
+ }
3516
+ },
3517
+ {
3518
+ label: '创建相同配置容器',
3519
+ onClick: function () {
3520
+ openCloneModal('clone-config', containerGroupForMenu.containerName);
3521
+ }
3522
+ },
3523
+ {
3524
+ label: '复制容器',
3525
+ onClick: function () {
3526
+ openCloneModal('duplicate', containerGroupForMenu.containerName);
3527
+ }
3528
+ },
3529
+ {
3530
+ label: '删除容器',
3531
+ danger: true,
3532
+ onClick: function () {
3533
+ removeContainerByName(containerGroupForMenu.containerName);
3534
+ }
3535
+ }
3536
+ ]));
3327
3537
  }
3328
3538
 
3329
3539
  const activeSession = getActiveSession();
@@ -3344,6 +3554,7 @@
3344
3554
  }
3345
3555
 
3346
3556
  function renderContainerLevel(groups) {
3557
+ closeOpenTreeNodeMenu();
3347
3558
  sessionList.innerHTML = '';
3348
3559
  if (!groups.length) {
3349
3560
  const empty = document.createElement('div');
@@ -3358,10 +3569,10 @@
3358
3569
  kind: 'container',
3359
3570
  className: status.tone === 'history' ? 'tree-node-tone-history' : '',
3360
3571
  title: containerGroup.containerRemark || containerGroup.containerName,
3361
- meta: containerGroup.containerRemark ? containerGroup.containerName : '',
3572
+ meta: containerGroup.containerRemark ? containerGroup.containerName : containerGroup.hostPath,
3362
3573
  statusLabel: status.label,
3363
3574
  statusTone: status.tone,
3364
- subMeta: containerGroup.hostPath,
3575
+ subMeta: containerGroup.containerRemark ? containerGroup.hostPath : '',
3365
3576
  hasActive: containerContainsActiveSession(containerGroup),
3366
3577
  onClick: function () {
3367
3578
  navigateTo('agents', { container: containerGroup.containerName });
@@ -3406,7 +3617,7 @@
3406
3617
 
3407
3618
  async function editContainerRemark(containerGroup) {
3408
3619
  const current = containerGroup.containerRemark || '';
3409
- const input = window.prompt('设置容器备注(留空清除备注)', current);
3620
+ const input = await showPrompt('设置容器备注(留空清除备注)', current);
3410
3621
  if (input === null) {
3411
3622
  return;
3412
3623
  }
@@ -3418,13 +3629,13 @@
3418
3629
  });
3419
3630
  await refreshSessionsSilent({ preferredName: state.active });
3420
3631
  } catch (e) {
3421
- alert(e.message);
3632
+ await showNotice(e.message);
3422
3633
  }
3423
3634
  }
3424
3635
 
3425
3636
  async function editAgentRemark(session) {
3426
3637
  const current = session.agentRemark || '';
3427
- const input = window.prompt('设置 AGENT 备注(留空清除备注)', current);
3638
+ const input = await showPrompt('设置 AGENT 备注(留空清除备注)', current);
3428
3639
  if (input === null) {
3429
3640
  return;
3430
3641
  }
@@ -3436,13 +3647,15 @@
3436
3647
  });
3437
3648
  await refreshSessionsSilent({ preferredName: state.active });
3438
3649
  } catch (e) {
3439
- alert(e.message);
3650
+ await showNotice(e.message);
3440
3651
  }
3441
3652
  }
3442
3653
 
3443
3654
  function renderAgentLevel(containerGroup) {
3655
+ closeOpenTreeNodeMenu();
3444
3656
  sessionList.innerHTML = '';
3445
- const sessions = containerGroup && Array.isArray(containerGroup.sessions) ? containerGroup.sessions : [];
3657
+ const allSessions = containerGroup && Array.isArray(containerGroup.sessions) ? containerGroup.sessions : [];
3658
+ const sessions = allSessions.filter(function (session) { return !session.synthetic; });
3446
3659
  if (!sessions.length) {
3447
3660
  const empty = document.createElement('div');
3448
3661
  empty.className = 'empty';
@@ -3514,9 +3727,10 @@
3514
3727
  const containerCount = new Set(state.sessions.map(function (session) {
3515
3728
  return session && session.containerName ? session.containerName : '';
3516
3729
  }).filter(Boolean)).size;
3730
+ const agentCount = state.sessions.filter(function (session) { return !session.synthetic; }).length;
3517
3731
  sessionCount.textContent = state.loadingSessions
3518
3732
  ? '加载中...'
3519
- : `${containerCount} 个容器 / ${state.sessions.length} 个 AGENT`;
3733
+ : `${containerCount} 个容器 / ${agentCount} 个 AGENT`;
3520
3734
  renderBreadcrumb();
3521
3735
 
3522
3736
  if (state.loadingSessions) {
@@ -3673,7 +3887,7 @@
3673
3887
  closeCloneModal();
3674
3888
  await loadSessions(data.name);
3675
3889
  if (data.resumeMayFail) {
3676
- alert('复制完成。注意:AGENT 对话历史已复制,但底层 CLI 会话续接可能失败,如遇失败请以新对话继续。');
3890
+ await showNotice('复制完成。注意:AGENT 对话历史已复制,但底层 CLI 会话续接可能失败,如遇失败请以新对话继续。');
3677
3891
  }
3678
3892
  } catch (e) {
3679
3893
  state.cloneModal.submitting = false;
@@ -4378,8 +4592,8 @@
4378
4592
  if (agentTemplateBtn) {
4379
4593
  agentTemplateBtn.addEventListener('click', function () {
4380
4594
  closeComposerOptionsMenu();
4381
- openAgentTemplateModal().catch(function (e) {
4382
- alert(e && e.message ? e.message : '加载 Agent 模板失败');
4595
+ openAgentTemplateModal().catch(async function (e) {
4596
+ await showNotice(e && e.message ? e.message : '加载 Agent 模板失败');
4383
4597
  });
4384
4598
  });
4385
4599
  }
@@ -4387,8 +4601,8 @@
4387
4601
  if (activityModelChip) {
4388
4602
  activityModelChip.addEventListener('click', function () {
4389
4603
  closeComposerOptionsMenu();
4390
- openModelModal().catch(function (e) {
4391
- alert(e && e.message ? e.message : '加载模型信息失败');
4604
+ openModelModal().catch(async function (e) {
4605
+ await showNotice(e && e.message ? e.message : '加载模型信息失败');
4392
4606
  });
4393
4607
  });
4394
4608
  }
@@ -4622,7 +4836,7 @@
4622
4836
  body: JSON.stringify({})
4623
4837
  });
4624
4838
  } catch (e) {
4625
- alert(e.message);
4839
+ await showNotice(e.message);
4626
4840
  state.agentRun.stopping = false;
4627
4841
  syncUi();
4628
4842
  }
@@ -4684,7 +4898,7 @@
4684
4898
  if (mode === 'agent') {
4685
4899
  const recovered = await recoverAgentRunFromServer(submitSession);
4686
4900
  if (!recovered && document.visibilityState !== 'hidden') {
4687
- alert(e.message);
4901
+ await showNotice(e.message);
4688
4902
  }
4689
4903
  } else {
4690
4904
  if (state.active === submitSession) {
@@ -4693,7 +4907,7 @@
4693
4907
  });
4694
4908
  renderMessages(state.messages);
4695
4909
  }
4696
- alert(e.message);
4910
+ await showNotice(e.message);
4697
4911
  }
4698
4912
  } finally {
4699
4913
  state.sending = false;
@@ -4876,6 +5090,22 @@
4876
5090
  });
4877
5091
  }
4878
5092
 
5093
+ if (removeConfirmModal) {
5094
+ removeConfirmModal.addEventListener('click', function (event) {
5095
+ if (event.target === removeConfirmModal) {
5096
+ removeConfirmCancelBtn.click();
5097
+ }
5098
+ });
5099
+ }
5100
+
5101
+ if (genericDialogModal) {
5102
+ genericDialogModal.addEventListener('click', function (event) {
5103
+ if (event.target === genericDialogModal) {
5104
+ genericDialogCloseBtn.click();
5105
+ }
5106
+ });
5107
+ }
5108
+
4879
5109
  if (directoryPickerModal) {
4880
5110
  directoryPickerModal.addEventListener('click', function (event) {
4881
5111
  if (event.target === directoryPickerModal && !state.directoryPicker.loading) {
@@ -4972,6 +5202,12 @@
4972
5202
  if (event.key === 'Escape' && openTreeNodeMenu) {
4973
5203
  closeOpenTreeNodeMenu();
4974
5204
  }
5205
+ if (event.key === 'Escape' && removeConfirmModal && !removeConfirmModal.hidden) {
5206
+ removeConfirmCancelBtn.click();
5207
+ }
5208
+ if (event.key === 'Escape' && genericDialogModal && !genericDialogModal.hidden) {
5209
+ genericDialogCloseBtn.click();
5210
+ }
4975
5211
  });
4976
5212
 
4977
5213
  function onLayoutMediaChange() {
@@ -5029,7 +5265,7 @@
5029
5265
  if (composerOptionsPanel && composerOptionsPanel.contains(target)) return;
5030
5266
  closeComposerOptionsMenu();
5031
5267
  }
5032
- if (openTreeNodeMenu && !openTreeNodeMenu.wrap.contains(target)) {
5268
+ if (openTreeNodeMenu && !openTreeNodeMenu.containsTarget(target)) {
5033
5269
  closeOpenTreeNodeMenu();
5034
5270
  }
5035
5271
  });
@@ -5061,8 +5297,10 @@
5061
5297
  root: filesPanel,
5062
5298
  api,
5063
5299
  onError: function (message) {
5064
- alert(message);
5065
- }
5300
+ showNotice(message);
5301
+ },
5302
+ confirmFn: showConfirm,
5303
+ promptFn: showPrompt
5066
5304
  });
5067
5305
  }
5068
5306
 
@@ -5076,7 +5314,7 @@
5076
5314
  setMobileSessionPanel(false);
5077
5315
  document.body.classList.add('agent-mode');
5078
5316
  syncUi();
5079
- loadSessions().catch(function (e) {
5080
- alert(e.message);
5317
+ loadSessions().catch(async function (e) {
5318
+ await showNotice(e.message);
5081
5319
  });
5082
5320
  })();
@@ -75,6 +75,14 @@
75
75
  const onError = options && typeof options.onError === 'function'
76
76
  ? options.onError
77
77
  : function (message) { window.alert(message); };
78
+ // confirmFn/promptFn 可由调用方注入非阻塞的自定义弹窗,缺省退回原生 confirm/prompt
79
+ // (和上面 onError 的兜底写法保持一致)。
80
+ const confirmFn = options && typeof options.confirmFn === 'function'
81
+ ? options.confirmFn
82
+ : function (message) { return Promise.resolve(window.confirm(message)); };
83
+ const promptFn = options && typeof options.promptFn === 'function'
84
+ ? options.promptFn
85
+ : function (message, defaultValue) { return Promise.resolve(window.prompt(message, defaultValue)); };
78
86
  if (!root || typeof api !== 'function') {
79
87
  return {
80
88
  sync: function () {}
@@ -455,7 +463,7 @@
455
463
  const fileSize = Number(selectedEntry && selectedEntry.size);
456
464
  const requiresReadonlyConfirm = Number.isFinite(fileSize) && fileSize >= FILE_EDIT_MAX_BYTES;
457
465
  if (requiresReadonlyConfirm) {
458
- const yes = window.confirm(`文件较大(${formatBytes(fileSize)}),继续后将以只读方式全量预览,无法保存。是否继续?`);
466
+ const yes = await confirmFn(`文件较大(${formatBytes(fileSize)}),继续后将以只读方式全量预览,无法保存。是否继续?`);
459
467
  if (!yes) {
460
468
  return;
461
469
  }
@@ -553,7 +561,7 @@
553
561
  if (!state.sessionName || state.historyOnly === true || state.loadingList || state.loadingFile) {
554
562
  return;
555
563
  }
556
- const input = window.prompt('请输入新目录名称');
564
+ const input = await promptFn('请输入新目录名称');
557
565
  const name = String(input || '').trim();
558
566
  if (!name) {
559
567
  return;
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: [],
@@ -221,6 +222,7 @@ function normalizeWebAgentSessionRecord(agentId, rawAgent) {
221
222
  ? normalizeAgentPromptCommandTemplate(source.agentPromptCommand, `agents.${agentId}.agentPromptCommand`)
222
223
  : '',
223
224
  remark: normalizeRemarkText(source.remark),
225
+ archived: source.archived === true,
224
226
  createdAt: typeof source.createdAt === 'string' ? source.createdAt : null,
225
227
  updatedAt: typeof source.updatedAt === 'string' ? source.updatedAt : null,
226
228
  messages: Array.isArray(source.messages) ? source.messages : [],
@@ -312,7 +314,7 @@ function normalizeWebHistoryRecord(containerName, rawData) {
312
314
  });
313
315
  }
314
316
 
315
- 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) {
316
318
  history.agents[WEB_DEFAULT_AGENT_ID] = normalizeWebAgentSessionRecord(WEB_DEFAULT_AGENT_ID, {
317
319
  agentName: data.agentName || WEB_DEFAULT_AGENT_NAME,
318
320
  updatedAt: typeof data.updatedAt === 'string' ? data.updatedAt : null,
@@ -345,16 +347,18 @@ function saveWebSessionHistory(webHistoryDir, containerName, history) {
345
347
  const filePath = getWebHistoryFile(webHistoryDir, containerName);
346
348
  const normalized = normalizeWebHistoryRecord(containerName, history);
347
349
  const runtimeMeta = getAgentRuntimeMeta(normalized.agentPromptCommand || '');
348
- const defaultAgent = getWebAgentSession(normalized, WEB_DEFAULT_AGENT_ID) || createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID);
350
+ const defaultAgent = getWebAgentSession(normalized, WEB_DEFAULT_AGENT_ID);
349
351
  const legacyCompatible = {
350
352
  ...normalized,
351
- messages: Array.isArray(defaultAgent.messages) ? defaultAgent.messages : [],
352
353
  agentProgram: runtimeMeta.agentProgram || '',
353
- resumeSupported: runtimeMeta.resumeSupported === true,
354
- lastResumeAt: defaultAgent.lastResumeAt || null,
355
- lastResumeOk: typeof defaultAgent.lastResumeOk === 'boolean' ? defaultAgent.lastResumeOk : null,
356
- lastResumeError: defaultAgent.lastResumeError || ''
354
+ resumeSupported: runtimeMeta.resumeSupported === true
357
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
+ }
358
362
  fs.writeFileSync(filePath, JSON.stringify(legacyCompatible, null, 4));
359
363
  }
360
364
 
@@ -366,6 +370,28 @@ function removeWebSessionHistory(webHistoryDir, containerName) {
366
370
  }
367
371
  }
368
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
+
369
395
  function listWebHistorySessionNames(webHistoryDir, isValidContainerName) {
370
396
  ensureWebHistoryDir(webHistoryDir);
371
397
  return fs.readdirSync(webHistoryDir)
@@ -405,14 +431,19 @@ function parseWebSessionKey(sessionKey) {
405
431
  };
406
432
  }
407
433
 
434
+ function isVisibleWebAgentSession(agentSession) {
435
+ return Boolean(agentSession) && agentSession.archived !== true;
436
+ }
437
+
408
438
  function getWebAgentSession(history, agentId, options = {}) {
409
439
  const sessionHistory = history && typeof history === 'object' ? history : { agents: {} };
410
440
  if (!sessionHistory.agents || typeof sessionHistory.agents !== 'object' || Array.isArray(sessionHistory.agents)) {
411
441
  sessionHistory.agents = {};
412
442
  }
413
443
  const requestedAgentId = String(agentId || WEB_DEFAULT_AGENT_ID).trim() || WEB_DEFAULT_AGENT_ID;
414
- if (sessionHistory.agents[requestedAgentId]) {
415
- return sessionHistory.agents[requestedAgentId];
444
+ const existing = sessionHistory.agents[requestedAgentId];
445
+ if (isVisibleWebAgentSession(existing)) {
446
+ return existing;
416
447
  }
417
448
  if (options.create === true) {
418
449
  const agentSession = createEmptyWebAgentSession(requestedAgentId);
@@ -427,21 +458,20 @@ function listWebAgentSessions(history, options = {}) {
427
458
  const agents = sessionHistory.agents && typeof sessionHistory.agents === 'object' && !Array.isArray(sessionHistory.agents)
428
459
  ? sessionHistory.agents
429
460
  : {};
430
- const agentIds = Object.keys(agents);
431
- if (!agentIds.length && options.includeSyntheticDefault === true) {
432
- return [createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID)];
433
- }
434
- return agentIds
461
+ const visibleSessions = Object.keys(agents)
435
462
  .map(agentId => agents[agentId])
436
- .filter(Boolean)
437
- .sort((a, b) => {
438
- const orderA = a.agentId === WEB_DEFAULT_AGENT_ID ? 0 : 1;
439
- const orderB = b.agentId === WEB_DEFAULT_AGENT_ID ? 0 : 1;
440
- if (orderA !== orderB) {
441
- return orderA - orderB;
442
- }
443
- return String(a.agentName || '').localeCompare(String(b.agentName || ''), 'zh-CN');
444
- });
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
+ });
445
475
  }
446
476
 
447
477
  function getWebAgentCreationRank(agentId) {
@@ -3019,19 +3049,6 @@ async function createClonedContainer(ctx, state, sourceContainerName, requestedN
3019
3049
  };
3020
3050
  }
3021
3051
 
3022
- function resolveContainerNamesByHostPath(ctx, state, hostPath) {
3023
- const target = String(hostPath || '').trim();
3024
- const containerMap = listWebManyoyoContainers(ctx);
3025
- const names = new Set([
3026
- ...Object.keys(containerMap),
3027
- ...listWebHistorySessionNames(state.webHistoryDir, ctx.isValidContainerName)
3028
- ]);
3029
- return Array.from(names).filter(name => {
3030
- const summary = buildSessionSummary(ctx, state, containerMap, { containerName: name, agentId: WEB_DEFAULT_AGENT_ID });
3031
- return summary && String(summary.hostPath || '').trim() === target;
3032
- });
3033
- }
3034
-
3035
3052
  // Estimate container start time from "Up X hours/minutes/seconds" status string.
3036
3053
  // Uses relative time to avoid Podman Machine VM clock drift issues.
3037
3054
  function estimateStartTimeFromStatus(status) {
@@ -3781,11 +3798,13 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
3781
3798
  const containerName = sessionRef && sessionRef.containerName ? sessionRef.containerName : '';
3782
3799
  const agentId = sessionRef && sessionRef.agentId ? sessionRef.agentId : WEB_DEFAULT_AGENT_ID;
3783
3800
  const history = loadWebSessionHistory(state.webHistoryDir, containerName);
3784
- const agentSession = getWebAgentSession(history, agentId)
3801
+ const realAgentSession = getWebAgentSession(history, agentId);
3802
+ const agentSession = realAgentSession
3785
3803
  || (agentId === WEB_DEFAULT_AGENT_ID ? createEmptyWebAgentSession(WEB_DEFAULT_AGENT_ID) : null);
3786
3804
  if (!agentSession) {
3787
3805
  return null;
3788
3806
  }
3807
+ const synthetic = !realAgentSession;
3789
3808
  const latestMessage = agentSession.messages.length ? agentSession.messages[agentSession.messages.length - 1] : null;
3790
3809
  const containerInfo = containerMap[containerName] || {};
3791
3810
  const effectiveAgentPromptCommand = resolveEffectiveAgentPromptCommandForSession(history, agentId, containerInfo.defaultCommand);
@@ -3822,7 +3841,8 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
3822
3841
  resumeSupported: effectiveResumeSupported,
3823
3842
  model: agentSession.model || '',
3824
3843
  hostPath: applied.hostPath || '',
3825
- containerPath: applied.containerPath || ''
3844
+ containerPath: applied.containerPath || '',
3845
+ ...(synthetic ? { synthetic: true } : {})
3826
3846
  };
3827
3847
  }
3828
3848
 
@@ -4569,37 +4589,6 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4569
4589
  sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), remark });
4570
4590
  }
4571
4591
  },
4572
- {
4573
- method: 'POST',
4574
- match: currentPath => currentPath === '/api/sessions/by-directory/remove' ? [] : null,
4575
- handler: async () => {
4576
- const payload = await readJsonBody(req);
4577
- const hostPath = typeof payload.hostPath === 'string' ? payload.hostPath : '';
4578
- if (!hostPath.trim()) {
4579
- sendJson(res, 400, { error: 'hostPath 不能为空' });
4580
- return;
4581
- }
4582
- const targets = resolveContainerNamesByHostPath(ctx, state, hostPath);
4583
- if (!targets.length) {
4584
- sendJson(res, 404, { error: '未找到该工作目录下的容器' });
4585
- return;
4586
- }
4587
- const removed = [];
4588
- const errors = [];
4589
- targets.forEach(name => {
4590
- try {
4591
- if (ctx.containerExists(name)) {
4592
- ctx.removeContainer(name);
4593
- }
4594
- removeWebSessionHistory(state.webHistoryDir, name);
4595
- removed.push(name);
4596
- } catch (e) {
4597
- errors.push({ name, error: e.message || String(e) });
4598
- }
4599
- });
4600
- sendJson(res, 200, { hostPath, removed, errors });
4601
- }
4602
- },
4603
4592
  {
4604
4593
  method: 'GET',
4605
4594
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/messages$/),
@@ -5235,12 +5224,23 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5235
5224
  return;
5236
5225
  }
5237
5226
 
5238
- if (ctx.containerExists(sessionRef.containerName)) {
5239
- 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) {
5240
5236
  appendWebSessionMessage(state.webHistoryDir, sessionRef, 'system', `容器 ${sessionRef.containerName} 已删除。`);
5241
5237
  }
5242
5238
 
5243
- 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
+ });
5244
5244
  }
5245
5245
  },
5246
5246
  {
@@ -5252,12 +5252,17 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5252
5252
  return;
5253
5253
  }
5254
5254
 
5255
+ const payload = await readJsonBody(req);
5256
+ const removeHistory = payload.removeHistory === true;
5257
+
5255
5258
  const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
5256
5259
  if (history.agents && typeof history.agents === 'object') {
5257
- if (sessionRef.agentId === WEB_DEFAULT_AGENT_ID) {
5258
- delete history.agents[WEB_DEFAULT_AGENT_ID];
5259
- } else {
5260
+ if (removeHistory) {
5260
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();
5261
5266
  }
5262
5267
  }
5263
5268
  if (!Object.keys(history.agents || {}).length && !ctx.containerExists(sessionRef.containerName)) {
@@ -5266,7 +5271,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
5266
5271
  saveWebSessionHistory(state.webHistoryDir, sessionRef.containerName, history);
5267
5272
  }
5268
5273
  sendJson(res, 200, {
5269
- removedHistory: true,
5274
+ removedHistory: removeHistory,
5270
5275
  name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId)
5271
5276
  });
5272
5277
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "6.2.10",
3
+ "version": "6.2.14",
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",