@xcanwin/manyoyo 6.2.10 → 6.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/core/event-store.js +11 -0
- package/lib/web/frontend/app.css +13 -0
- package/lib/web/frontend/app.html +16 -0
- package/lib/web/frontend/app.js +107 -10
- 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
|
@@ -284,6 +284,10 @@ textarea:focus-visible {
|
|
|
284
284
|
overflow: hidden;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
.modal.modal-sm {
|
|
288
|
+
width: min(420px, calc(100vw - 24px));
|
|
289
|
+
}
|
|
290
|
+
|
|
287
291
|
.modal-header {
|
|
288
292
|
display: flex;
|
|
289
293
|
align-items: center;
|
|
@@ -747,6 +751,15 @@ textarea:focus-visible {
|
|
|
747
751
|
}
|
|
748
752
|
}
|
|
749
753
|
|
|
754
|
+
.crumb-bar .tree-node-menu {
|
|
755
|
+
position: relative;
|
|
756
|
+
top: auto;
|
|
757
|
+
right: auto;
|
|
758
|
+
transform: none;
|
|
759
|
+
opacity: 1;
|
|
760
|
+
pointer-events: auto;
|
|
761
|
+
}
|
|
762
|
+
|
|
750
763
|
.tree-node-menu-trigger {
|
|
751
764
|
width: 26px;
|
|
752
765
|
height: 26px;
|
|
@@ -215,6 +215,22 @@
|
|
|
215
215
|
</section>
|
|
216
216
|
</div>
|
|
217
217
|
|
|
218
|
+
<div id="removeConfirmModal" class="modal-backdrop" hidden>
|
|
219
|
+
<section class="modal modal-sm" role="dialog" aria-modal="true" aria-labelledby="removeConfirmTitle">
|
|
220
|
+
<header class="modal-header">
|
|
221
|
+
<h2 id="removeConfirmTitle">确认删除</h2>
|
|
222
|
+
<button type="button" id="removeConfirmCancelBtn" class="secondary">关闭</button>
|
|
223
|
+
</header>
|
|
224
|
+
<div class="modal-body">
|
|
225
|
+
<p id="removeConfirmMessage"></p>
|
|
226
|
+
</div>
|
|
227
|
+
<footer class="modal-footer">
|
|
228
|
+
<button type="button" id="removeConfirmKeepHistoryBtn" class="secondary">否,保留历史</button>
|
|
229
|
+
<button type="button" id="removeConfirmWithHistoryBtn" class="danger">是,连同历史一起删除</button>
|
|
230
|
+
</footer>
|
|
231
|
+
</section>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
218
234
|
<div id="directoryPickerModal" class="modal-backdrop" hidden>
|
|
219
235
|
<section class="modal dir-picker-modal" role="dialog" aria-modal="true" aria-labelledby="directoryPickerTitle">
|
|
220
236
|
<header class="modal-header">
|
package/lib/web/frontend/app.js
CHANGED
|
@@ -179,6 +179,12 @@
|
|
|
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');
|
|
182
188
|
const directoryPickerModal = document.getElementById('directoryPickerModal');
|
|
183
189
|
const directoryPickerTitle = document.getElementById('directoryPickerTitle');
|
|
184
190
|
const directoryPickerTip = document.getElementById('directoryPickerTip');
|
|
@@ -3167,11 +3173,47 @@
|
|
|
3167
3173
|
return wrap;
|
|
3168
3174
|
}
|
|
3169
3175
|
|
|
3176
|
+
function confirmRemoveChoice(options) {
|
|
3177
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
3178
|
+
if (!removeConfirmModal) {
|
|
3179
|
+
return Promise.resolve(confirm(opts.message || '确认删除?') ? 'with-history' : null);
|
|
3180
|
+
}
|
|
3181
|
+
return new Promise(function (resolve) {
|
|
3182
|
+
if (removeConfirmTitle) {
|
|
3183
|
+
removeConfirmTitle.textContent = opts.title || '确认删除';
|
|
3184
|
+
}
|
|
3185
|
+
if (removeConfirmMessage) {
|
|
3186
|
+
removeConfirmMessage.textContent = opts.message || '';
|
|
3187
|
+
}
|
|
3188
|
+
let settled = false;
|
|
3189
|
+
function finish(value) {
|
|
3190
|
+
if (settled) return;
|
|
3191
|
+
settled = true;
|
|
3192
|
+
setModalVisible(removeConfirmModal, false);
|
|
3193
|
+
removeConfirmKeepHistoryBtn.removeEventListener('click', onKeepHistory);
|
|
3194
|
+
removeConfirmWithHistoryBtn.removeEventListener('click', onWithHistory);
|
|
3195
|
+
removeConfirmCancelBtn.removeEventListener('click', onCancel);
|
|
3196
|
+
resolve(value);
|
|
3197
|
+
}
|
|
3198
|
+
function onKeepHistory() { finish('keep-history'); }
|
|
3199
|
+
function onWithHistory() { finish('with-history'); }
|
|
3200
|
+
function onCancel() { finish(null); }
|
|
3201
|
+
removeConfirmKeepHistoryBtn.addEventListener('click', onKeepHistory);
|
|
3202
|
+
removeConfirmWithHistoryBtn.addEventListener('click', onWithHistory);
|
|
3203
|
+
removeConfirmCancelBtn.addEventListener('click', onCancel);
|
|
3204
|
+
setModalVisible(removeConfirmModal, true);
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3170
3208
|
async function removeContainerByName(containerName) {
|
|
3171
3209
|
const target = String(containerName || '').trim();
|
|
3172
3210
|
if (!target) return;
|
|
3173
|
-
const
|
|
3174
|
-
|
|
3211
|
+
const choice = await confirmRemoveChoice({
|
|
3212
|
+
title: '删除容器',
|
|
3213
|
+
message: `确认删除容器 ${target}?可以选择是否同时删除该容器的全部历史记录(消息与事件日志)。`
|
|
3214
|
+
});
|
|
3215
|
+
if (!choice) return;
|
|
3216
|
+
const removeHistory = choice === 'with-history';
|
|
3175
3217
|
try {
|
|
3176
3218
|
const wasActiveContainer = parseSessionKey(state.active).containerName === target;
|
|
3177
3219
|
if (parseSessionKey(state.terminal.sessionName).containerName === target
|
|
@@ -3179,7 +3221,8 @@
|
|
|
3179
3221
|
disconnectTerminal('容器删除,终端已断开', true);
|
|
3180
3222
|
}
|
|
3181
3223
|
await api('/api/sessions/' + encodeURIComponent(target) + '/remove', {
|
|
3182
|
-
method: 'POST'
|
|
3224
|
+
method: 'POST',
|
|
3225
|
+
body: JSON.stringify({ removeHistory })
|
|
3183
3226
|
});
|
|
3184
3227
|
await refreshSessions({
|
|
3185
3228
|
preferredName: wasActiveContainer ? target : (state.active || ''),
|
|
@@ -3194,8 +3237,12 @@
|
|
|
3194
3237
|
async function removeAgentSessionByName(sessionName, agentLabel) {
|
|
3195
3238
|
const target = String(sessionName || '').trim();
|
|
3196
3239
|
if (!target) return;
|
|
3197
|
-
const
|
|
3198
|
-
|
|
3240
|
+
const choice = await confirmRemoveChoice({
|
|
3241
|
+
title: '删除 AGENT',
|
|
3242
|
+
message: `确认删除 AGENT ${agentLabel || target}?可以选择是否同时删除它的历史记录(消息与事件日志)。`
|
|
3243
|
+
});
|
|
3244
|
+
if (!choice) return;
|
|
3245
|
+
const removeHistory = choice === 'with-history';
|
|
3199
3246
|
const isActive = target === state.active;
|
|
3200
3247
|
try {
|
|
3201
3248
|
const containerName = parseSessionKey(target).containerName;
|
|
@@ -3203,7 +3250,8 @@
|
|
|
3203
3250
|
? findPreferredSessionNameAfterRemoval(state.sessions, target)
|
|
3204
3251
|
: '';
|
|
3205
3252
|
await api('/api/sessions/' + encodeURIComponent(target) + '/remove-with-history', {
|
|
3206
|
-
method: 'POST'
|
|
3253
|
+
method: 'POST',
|
|
3254
|
+
body: JSON.stringify({ removeHistory })
|
|
3207
3255
|
});
|
|
3208
3256
|
await refreshSessions({
|
|
3209
3257
|
preferredName: isActive ? (fallbackSessionName || '') : (state.active || ''),
|
|
@@ -3319,11 +3367,50 @@
|
|
|
3319
3367
|
sep.textContent = '›';
|
|
3320
3368
|
sessionBreadcrumb.appendChild(sep);
|
|
3321
3369
|
|
|
3370
|
+
const sessionForCrumb = state.sessions.find(function (session) {
|
|
3371
|
+
return getSessionContainerName(session) === state.navContainer;
|
|
3372
|
+
});
|
|
3373
|
+
const containerRemark = sessionForCrumb && sessionForCrumb.containerRemark ? sessionForCrumb.containerRemark : '';
|
|
3374
|
+
const containerGroupForMenu = { containerName: state.navContainer, containerRemark: containerRemark };
|
|
3375
|
+
|
|
3322
3376
|
const containerCrumb = document.createElement('span');
|
|
3323
3377
|
containerCrumb.className = 'crumb-item is-current';
|
|
3324
|
-
containerCrumb.textContent = state.navContainer;
|
|
3325
|
-
containerCrumb.title = state.navContainer;
|
|
3378
|
+
containerCrumb.textContent = containerRemark || state.navContainer;
|
|
3379
|
+
containerCrumb.title = containerRemark ? `${containerRemark}(${state.navContainer})` : state.navContainer;
|
|
3326
3380
|
sessionBreadcrumb.appendChild(containerCrumb);
|
|
3381
|
+
sessionBreadcrumb.appendChild(createTreeNodeMenu([
|
|
3382
|
+
{
|
|
3383
|
+
label: '新建 AGENT',
|
|
3384
|
+
onClick: function () {
|
|
3385
|
+
createAgentSession(containerGroupForMenu.containerName);
|
|
3386
|
+
}
|
|
3387
|
+
},
|
|
3388
|
+
{
|
|
3389
|
+
label: '编辑备注',
|
|
3390
|
+
onClick: function () {
|
|
3391
|
+
editContainerRemark(containerGroupForMenu);
|
|
3392
|
+
}
|
|
3393
|
+
},
|
|
3394
|
+
{
|
|
3395
|
+
label: '创建相同配置容器',
|
|
3396
|
+
onClick: function () {
|
|
3397
|
+
openCloneModal('clone-config', containerGroupForMenu.containerName);
|
|
3398
|
+
}
|
|
3399
|
+
},
|
|
3400
|
+
{
|
|
3401
|
+
label: '复制容器',
|
|
3402
|
+
onClick: function () {
|
|
3403
|
+
openCloneModal('duplicate', containerGroupForMenu.containerName);
|
|
3404
|
+
}
|
|
3405
|
+
},
|
|
3406
|
+
{
|
|
3407
|
+
label: '删除容器',
|
|
3408
|
+
danger: true,
|
|
3409
|
+
onClick: function () {
|
|
3410
|
+
removeContainerByName(containerGroupForMenu.containerName);
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
]));
|
|
3327
3414
|
}
|
|
3328
3415
|
|
|
3329
3416
|
const activeSession = getActiveSession();
|
|
@@ -3442,7 +3529,8 @@
|
|
|
3442
3529
|
|
|
3443
3530
|
function renderAgentLevel(containerGroup) {
|
|
3444
3531
|
sessionList.innerHTML = '';
|
|
3445
|
-
const
|
|
3532
|
+
const allSessions = containerGroup && Array.isArray(containerGroup.sessions) ? containerGroup.sessions : [];
|
|
3533
|
+
const sessions = allSessions.filter(function (session) { return !session.synthetic; });
|
|
3446
3534
|
if (!sessions.length) {
|
|
3447
3535
|
const empty = document.createElement('div');
|
|
3448
3536
|
empty.className = 'empty';
|
|
@@ -3514,9 +3602,10 @@
|
|
|
3514
3602
|
const containerCount = new Set(state.sessions.map(function (session) {
|
|
3515
3603
|
return session && session.containerName ? session.containerName : '';
|
|
3516
3604
|
}).filter(Boolean)).size;
|
|
3605
|
+
const agentCount = state.sessions.filter(function (session) { return !session.synthetic; }).length;
|
|
3517
3606
|
sessionCount.textContent = state.loadingSessions
|
|
3518
3607
|
? '加载中...'
|
|
3519
|
-
: `${containerCount} 个容器 / ${
|
|
3608
|
+
: `${containerCount} 个容器 / ${agentCount} 个 AGENT`;
|
|
3520
3609
|
renderBreadcrumb();
|
|
3521
3610
|
|
|
3522
3611
|
if (state.loadingSessions) {
|
|
@@ -4876,6 +4965,14 @@
|
|
|
4876
4965
|
});
|
|
4877
4966
|
}
|
|
4878
4967
|
|
|
4968
|
+
if (removeConfirmModal) {
|
|
4969
|
+
removeConfirmModal.addEventListener('click', function (event) {
|
|
4970
|
+
if (event.target === removeConfirmModal) {
|
|
4971
|
+
removeConfirmCancelBtn.click();
|
|
4972
|
+
}
|
|
4973
|
+
});
|
|
4974
|
+
}
|
|
4975
|
+
|
|
4879
4976
|
if (directoryPickerModal) {
|
|
4880
4977
|
directoryPickerModal.addEventListener('click', function (event) {
|
|
4881
4978
|
if (event.target === directoryPickerModal && !state.directoryPicker.loading) {
|
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
|
}
|