@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.
- package/lib/core/event-store.js +11 -0
- package/lib/web/frontend/app.css +56 -6
- package/lib/web/frontend/app.html +33 -0
- package/lib/web/frontend/app.js +278 -40
- package/lib/web/frontend/file-browser.js +10 -2
- package/lib/web/server.js +81 -76
- package/package.json +1 -1
package/lib/core/event-store.js
CHANGED
|
@@ -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 = {
|
package/lib/web/frontend/app.css
CHANGED
|
@@ -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:
|
|
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:
|
|
752
|
-
height:
|
|
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:
|
|
807
|
+
border-radius: 9px;
|
|
758
808
|
}
|
|
759
809
|
|
|
760
810
|
.tree-node-menu-trigger svg {
|
|
761
|
-
width:
|
|
762
|
-
height:
|
|
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">
|
package/lib/web/frontend/app.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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
|
|
3174
|
-
|
|
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
|
-
|
|
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
|
|
3198
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
3650
|
+
await showNotice(e.message);
|
|
3440
3651
|
}
|
|
3441
3652
|
}
|
|
3442
3653
|
|
|
3443
3654
|
function renderAgentLevel(containerGroup) {
|
|
3655
|
+
closeOpenTreeNodeMenu();
|
|
3444
3656
|
sessionList.innerHTML = '';
|
|
3445
|
-
const
|
|
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} 个容器 / ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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)
|
|
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
|
-
|
|
415
|
-
|
|
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
|
|
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(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
|
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
|
-
|
|
5239
|
-
|
|
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, {
|
|
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 (
|
|
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:
|
|
5274
|
+
removedHistory: removeHistory,
|
|
5270
5275
|
name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId)
|
|
5271
5276
|
});
|
|
5272
5277
|
}
|