@xcanwin/manyoyo 6.2.8 → 6.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/web/frontend/app.css +6 -1
- package/lib/web/frontend/app.html +26 -1
- package/lib/web/frontend/app.js +199 -3
- package/lib/web/server.js +338 -11
- package/package.json +1 -1
package/lib/web/frontend/app.css
CHANGED
|
@@ -391,10 +391,15 @@ textarea:focus-visible {
|
|
|
391
391
|
color: var(--muted);
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
.text-block[hidden] {
|
|
395
|
+
display: none;
|
|
396
|
+
}
|
|
397
|
+
|
|
394
398
|
.form-grid input,
|
|
395
399
|
.form-grid select,
|
|
396
400
|
.text-block textarea,
|
|
397
|
-
.text-block input
|
|
401
|
+
.text-block input,
|
|
402
|
+
.text-block select {
|
|
398
403
|
border: 1px solid var(--line);
|
|
399
404
|
border-radius: 10px;
|
|
400
405
|
padding: 9px 11px;
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
<button type="button" id="activityAgentBtn" class="secondary is-active">Agent对话</button>
|
|
122
122
|
<button type="button" id="activityCommandBtn" class="secondary">系统命令</button>
|
|
123
123
|
<button type="button" id="agentTemplateBtn" class="secondary">CLI · —</button>
|
|
124
|
-
<
|
|
124
|
+
<button type="button" id="activityModelChip" class="secondary">模型 · —</button>
|
|
125
125
|
</div>
|
|
126
126
|
</div>
|
|
127
127
|
<button type="submit" id="sendBtn">发送</button>
|
|
@@ -288,6 +288,31 @@
|
|
|
288
288
|
</section>
|
|
289
289
|
</div>
|
|
290
290
|
|
|
291
|
+
<div id="modelModal" class="modal-backdrop" hidden>
|
|
292
|
+
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="modelModalTitle">
|
|
293
|
+
<header class="modal-header">
|
|
294
|
+
<h2 id="modelModalTitle">选择模型</h2>
|
|
295
|
+
<button type="button" id="modelModalCancelBtn" class="secondary">关闭</button>
|
|
296
|
+
</header>
|
|
297
|
+
<div class="modal-body">
|
|
298
|
+
<div id="modelModalLoading" class="modal-tip" hidden>正在获取模型列表…</div>
|
|
299
|
+
<label class="text-block">模型
|
|
300
|
+
<select id="modelSelect">
|
|
301
|
+
<option value="">跟随默认(不传 --model)</option>
|
|
302
|
+
<option value="__custom__">自定义…</option>
|
|
303
|
+
</select>
|
|
304
|
+
</label>
|
|
305
|
+
<label class="text-block" id="modelCustomGroup" hidden>自定义模型名称
|
|
306
|
+
<input type="text" id="modelCustomInput" placeholder="例如 gemini-2.5-pro">
|
|
307
|
+
</label>
|
|
308
|
+
<div id="modelModalError" class="modal-error" hidden></div>
|
|
309
|
+
</div>
|
|
310
|
+
<footer class="modal-footer">
|
|
311
|
+
<button type="button" id="modelModalSaveBtn">保存</button>
|
|
312
|
+
</footer>
|
|
313
|
+
</section>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
291
316
|
<div id="externalLinkModal" class="modal-backdrop" hidden>
|
|
292
317
|
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="externalLinkTitle">
|
|
293
318
|
<header class="modal-header">
|
package/lib/web/frontend/app.js
CHANGED
|
@@ -53,6 +53,10 @@
|
|
|
53
53
|
createModalOpen: false,
|
|
54
54
|
agentTemplateModalOpen: false,
|
|
55
55
|
externalLinkModalOpen: false,
|
|
56
|
+
modelModalOpen: false,
|
|
57
|
+
modelModalSaving: false,
|
|
58
|
+
modelModalLoading: false,
|
|
59
|
+
modelModalCatalog: [],
|
|
56
60
|
configLoading: false,
|
|
57
61
|
configSaving: false,
|
|
58
62
|
configSaveMessage: '',
|
|
@@ -216,6 +220,14 @@
|
|
|
216
220
|
const agentTemplateCancelBtn = document.getElementById('agentTemplateCancelBtn');
|
|
217
221
|
const agentTemplateResetBtn = document.getElementById('agentTemplateResetBtn');
|
|
218
222
|
const agentTemplateSaveBtn = document.getElementById('agentTemplateSaveBtn');
|
|
223
|
+
const modelModal = document.getElementById('modelModal');
|
|
224
|
+
const modelModalLoading = document.getElementById('modelModalLoading');
|
|
225
|
+
const modelSelect = document.getElementById('modelSelect');
|
|
226
|
+
const modelCustomGroup = document.getElementById('modelCustomGroup');
|
|
227
|
+
const modelCustomInput = document.getElementById('modelCustomInput');
|
|
228
|
+
const modelModalError = document.getElementById('modelModalError');
|
|
229
|
+
const modelModalCancelBtn = document.getElementById('modelModalCancelBtn');
|
|
230
|
+
const modelModalSaveBtn = document.getElementById('modelModalSaveBtn');
|
|
219
231
|
const externalLinkModal = document.getElementById('externalLinkModal');
|
|
220
232
|
const externalLinkUrl = document.getElementById('externalLinkUrl');
|
|
221
233
|
const externalLinkCancelBtn = document.getElementById('externalLinkCancelBtn');
|
|
@@ -1524,7 +1536,120 @@
|
|
|
1524
1536
|
if (!state.active) {
|
|
1525
1537
|
return '—';
|
|
1526
1538
|
}
|
|
1527
|
-
|
|
1539
|
+
const activeSession = getActiveSession();
|
|
1540
|
+
const detail = state.sessionDetail && state.active === (state.sessionDetail.name || state.active) ? state.sessionDetail : null;
|
|
1541
|
+
const model = String(
|
|
1542
|
+
(detail && detail.model)
|
|
1543
|
+
|| (activeSession && activeSession.model)
|
|
1544
|
+
|| ''
|
|
1545
|
+
).trim();
|
|
1546
|
+
return model || '自动';
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const MODEL_CUSTOM_OPTION_VALUE = '__custom__';
|
|
1550
|
+
|
|
1551
|
+
function populateModelSelect(models, selectedModel) {
|
|
1552
|
+
if (!modelSelect) {
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
modelSelect.innerHTML = '';
|
|
1556
|
+
const followOption = document.createElement('option');
|
|
1557
|
+
followOption.value = '';
|
|
1558
|
+
followOption.textContent = '跟随默认(不传 --model)';
|
|
1559
|
+
modelSelect.appendChild(followOption);
|
|
1560
|
+
|
|
1561
|
+
const knownValues = new Set();
|
|
1562
|
+
(Array.isArray(models) ? models : []).forEach(function (model) {
|
|
1563
|
+
if (!model || !model.value) {
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
knownValues.add(model.value);
|
|
1567
|
+
const option = document.createElement('option');
|
|
1568
|
+
option.value = model.value;
|
|
1569
|
+
option.textContent = model.description ? `${model.label} · ${model.description}` : model.label;
|
|
1570
|
+
modelSelect.appendChild(option);
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
const customOption = document.createElement('option');
|
|
1574
|
+
customOption.value = MODEL_CUSTOM_OPTION_VALUE;
|
|
1575
|
+
customOption.textContent = '自定义…';
|
|
1576
|
+
modelSelect.appendChild(customOption);
|
|
1577
|
+
|
|
1578
|
+
if (selectedModel && knownValues.has(selectedModel)) {
|
|
1579
|
+
modelSelect.value = selectedModel;
|
|
1580
|
+
if (modelCustomGroup) modelCustomGroup.hidden = true;
|
|
1581
|
+
} else if (selectedModel) {
|
|
1582
|
+
modelSelect.value = MODEL_CUSTOM_OPTION_VALUE;
|
|
1583
|
+
if (modelCustomInput) modelCustomInput.value = selectedModel;
|
|
1584
|
+
if (modelCustomGroup) modelCustomGroup.hidden = false;
|
|
1585
|
+
} else {
|
|
1586
|
+
modelSelect.value = '';
|
|
1587
|
+
if (modelCustomGroup) modelCustomGroup.hidden = true;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function showModelModalError(message) {
|
|
1592
|
+
if (!modelModalError) return;
|
|
1593
|
+
modelModalError.textContent = message || '';
|
|
1594
|
+
modelModalError.hidden = !message;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
async function openModelModal() {
|
|
1598
|
+
if (!state.active || state.modelModalSaving) {
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
const detail = await ensureActiveSessionDetail();
|
|
1602
|
+
if (!detail) {
|
|
1603
|
+
alert(state.sessionDetailError || '当前会话详情暂时不可用');
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
state.modelModalOpen = true;
|
|
1607
|
+
state.modelModalLoading = true;
|
|
1608
|
+
state.modelModalCatalog = [];
|
|
1609
|
+
showModelModalError('');
|
|
1610
|
+
populateModelSelect([], detail.model || '');
|
|
1611
|
+
syncUi();
|
|
1612
|
+
try {
|
|
1613
|
+
const data = await api('/api/sessions/' + encodeURIComponent(state.active) + '/models');
|
|
1614
|
+
state.modelModalCatalog = Array.isArray(data && data.models) ? data.models : [];
|
|
1615
|
+
} catch (e) {
|
|
1616
|
+
state.modelModalCatalog = [];
|
|
1617
|
+
} finally {
|
|
1618
|
+
state.modelModalLoading = false;
|
|
1619
|
+
populateModelSelect(state.modelModalCatalog, detail.model || '');
|
|
1620
|
+
syncUi();
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function closeModelModal() {
|
|
1625
|
+
state.modelModalOpen = false;
|
|
1626
|
+
showModelModalError('');
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
async function saveModelModal() {
|
|
1630
|
+
if (!state.active || state.modelModalSaving) {
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
const rawValue = modelSelect && modelSelect.value === MODEL_CUSTOM_OPTION_VALUE
|
|
1634
|
+
? (modelCustomInput ? modelCustomInput.value : '')
|
|
1635
|
+
: (modelSelect ? modelSelect.value : '');
|
|
1636
|
+
state.modelModalSaving = true;
|
|
1637
|
+
showModelModalError('');
|
|
1638
|
+
syncUi();
|
|
1639
|
+
try {
|
|
1640
|
+
await api('/api/sessions/' + encodeURIComponent(state.active) + '/model', {
|
|
1641
|
+
method: 'POST',
|
|
1642
|
+
body: JSON.stringify({ model: String(rawValue || '').trim() })
|
|
1643
|
+
});
|
|
1644
|
+
await refreshSessionsSilent({ preferredName: state.active });
|
|
1645
|
+
await loadSessionDetailForSession(state.active);
|
|
1646
|
+
closeModelModal();
|
|
1647
|
+
} catch (e) {
|
|
1648
|
+
showModelModalError(e && e.message ? e.message : '保存失败');
|
|
1649
|
+
} finally {
|
|
1650
|
+
state.modelModalSaving = false;
|
|
1651
|
+
syncUi();
|
|
1652
|
+
}
|
|
1528
1653
|
}
|
|
1529
1654
|
|
|
1530
1655
|
function buildMessageMetaLines(message) {
|
|
@@ -2083,6 +2208,13 @@
|
|
|
2083
2208
|
{ label: '最近 resume', value: lastResumeText },
|
|
2084
2209
|
{ label: '最近结果', value: detail.lastResumeOk == null ? '暂无' : (detail.lastResumeOk ? '成功' : '失败'), tone: detail.lastResumeOk == null ? 'info' : (detail.lastResumeOk ? 'ok' : 'danger') }
|
|
2085
2210
|
]);
|
|
2211
|
+
renderKeyValueCard(detailSummary, '用量统计', detail.usageTotal ? [
|
|
2212
|
+
{ label: '累计输入 tokens', value: String(detail.usageTotal.inputTokens) },
|
|
2213
|
+
{ label: '累计输出 tokens', value: String(detail.usageTotal.outputTokens) },
|
|
2214
|
+
{ label: '累计花费', value: typeof detail.usageTotal.costUsd === 'number' ? `$${detail.usageTotal.costUsd.toFixed(4)}` : '暂不支持' }
|
|
2215
|
+
] : [
|
|
2216
|
+
{ label: '状态', value: '暂无数据(当前 Agent 程序不支持用量统计,或还未执行过对话)', tone: 'info' }
|
|
2217
|
+
]);
|
|
2086
2218
|
renderKeyValueCard(detailSummary, '最近活动', [
|
|
2087
2219
|
{ label: '最近角色', value: latestRoleLabel },
|
|
2088
2220
|
{ label: '最近时间', value: latestTimestampText },
|
|
@@ -2210,7 +2342,7 @@
|
|
|
2210
2342
|
if (activityModelChip) {
|
|
2211
2343
|
activityModelChip.textContent = `模型 · ${resolveToolbarModelLabel()}`;
|
|
2212
2344
|
activityModelChip.title = state.active
|
|
2213
|
-
? '
|
|
2345
|
+
? '点击选择本会话使用的模型'
|
|
2214
2346
|
: '请先选择会话';
|
|
2215
2347
|
}
|
|
2216
2348
|
if (viewActivityBtn) viewActivityBtn.classList.toggle('is-active', activityTab);
|
|
@@ -2315,6 +2447,27 @@
|
|
|
2315
2447
|
if (externalLinkModal) {
|
|
2316
2448
|
externalLinkModal.hidden = !state.externalLinkModalOpen;
|
|
2317
2449
|
}
|
|
2450
|
+
if (modelModal) {
|
|
2451
|
+
modelModal.hidden = !state.modelModalOpen;
|
|
2452
|
+
}
|
|
2453
|
+
if (modelModalLoading) {
|
|
2454
|
+
modelModalLoading.hidden = !state.modelModalLoading;
|
|
2455
|
+
}
|
|
2456
|
+
if (modelSelect) {
|
|
2457
|
+
modelSelect.disabled = state.modelModalSaving || state.modelModalLoading;
|
|
2458
|
+
}
|
|
2459
|
+
if (modelCustomInput) {
|
|
2460
|
+
modelCustomInput.disabled = state.modelModalSaving || state.modelModalLoading;
|
|
2461
|
+
}
|
|
2462
|
+
if (modelModalSaveBtn) {
|
|
2463
|
+
modelModalSaveBtn.disabled = state.modelModalSaving || state.modelModalLoading;
|
|
2464
|
+
}
|
|
2465
|
+
if (modelModalCancelBtn) {
|
|
2466
|
+
modelModalCancelBtn.disabled = state.modelModalSaving;
|
|
2467
|
+
}
|
|
2468
|
+
if (activityModelChip) {
|
|
2469
|
+
activityModelChip.disabled = !state.active;
|
|
2470
|
+
}
|
|
2318
2471
|
if (agentTemplateSaveBtn) {
|
|
2319
2472
|
agentTemplateSaveBtn.disabled = state.agentTemplateSaving || !state.active;
|
|
2320
2473
|
}
|
|
@@ -2338,7 +2491,7 @@
|
|
|
2338
2491
|
}
|
|
2339
2492
|
document.body.classList.toggle(
|
|
2340
2493
|
'modal-open',
|
|
2341
|
-
state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen || state.externalLinkModalOpen || state.cloneModal.open
|
|
2494
|
+
state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen || state.externalLinkModalOpen || state.cloneModal.open || state.modelModalOpen
|
|
2342
2495
|
);
|
|
2343
2496
|
if (composer) {
|
|
2344
2497
|
composer.hidden = !activityTab;
|
|
@@ -4231,6 +4384,45 @@
|
|
|
4231
4384
|
});
|
|
4232
4385
|
}
|
|
4233
4386
|
|
|
4387
|
+
if (activityModelChip) {
|
|
4388
|
+
activityModelChip.addEventListener('click', function () {
|
|
4389
|
+
closeComposerOptionsMenu();
|
|
4390
|
+
openModelModal().catch(function (e) {
|
|
4391
|
+
alert(e && e.message ? e.message : '加载模型信息失败');
|
|
4392
|
+
});
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4396
|
+
if (modelModalCancelBtn) {
|
|
4397
|
+
modelModalCancelBtn.addEventListener('click', function () {
|
|
4398
|
+
closeModelModal();
|
|
4399
|
+
syncUi();
|
|
4400
|
+
});
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
if (modelModalSaveBtn) {
|
|
4404
|
+
modelModalSaveBtn.addEventListener('click', function () {
|
|
4405
|
+
saveModelModal();
|
|
4406
|
+
});
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
if (modelSelect) {
|
|
4410
|
+
modelSelect.addEventListener('change', function () {
|
|
4411
|
+
if (modelCustomGroup) {
|
|
4412
|
+
modelCustomGroup.hidden = modelSelect.value !== MODEL_CUSTOM_OPTION_VALUE;
|
|
4413
|
+
}
|
|
4414
|
+
});
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
if (modelModal) {
|
|
4418
|
+
modelModal.addEventListener('click', function (event) {
|
|
4419
|
+
if (event.target === modelModal && !state.modelModalSaving) {
|
|
4420
|
+
closeModelModal();
|
|
4421
|
+
syncUi();
|
|
4422
|
+
}
|
|
4423
|
+
});
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4234
4426
|
if (configCancelBtn) {
|
|
4235
4427
|
configCancelBtn.addEventListener('click', function () {
|
|
4236
4428
|
closeConfigModal();
|
|
@@ -4757,6 +4949,10 @@
|
|
|
4757
4949
|
closeAgentTemplateModal();
|
|
4758
4950
|
syncUi();
|
|
4759
4951
|
}
|
|
4952
|
+
if (event.key === 'Escape' && state.modelModalOpen && !state.modelModalSaving) {
|
|
4953
|
+
closeModelModal();
|
|
4954
|
+
syncUi();
|
|
4955
|
+
}
|
|
4760
4956
|
if (event.key === 'Escape' && state.externalLinkModalOpen) {
|
|
4761
4957
|
closeExternalLinkModalView();
|
|
4762
4958
|
syncUi();
|
package/lib/web/server.js
CHANGED
|
@@ -189,7 +189,26 @@ function createEmptyWebAgentSession(agentId, agentName) {
|
|
|
189
189
|
lastResumeAt: null,
|
|
190
190
|
lastResumeOk: null,
|
|
191
191
|
lastResumeError: '',
|
|
192
|
-
engineSessionId: ''
|
|
192
|
+
engineSessionId: '',
|
|
193
|
+
usageTotal: null,
|
|
194
|
+
model: ''
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeWebAgentUsageTotal(value) {
|
|
199
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
if (typeof value.inputTokens !== 'number' || typeof value.outputTokens !== 'number') {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
if (typeof value.costUsd !== 'number' && value.costUsd !== null) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
inputTokens: value.inputTokens,
|
|
210
|
+
outputTokens: value.outputTokens,
|
|
211
|
+
costUsd: typeof value.costUsd === 'number' ? value.costUsd : null
|
|
193
212
|
};
|
|
194
213
|
}
|
|
195
214
|
|
|
@@ -218,7 +237,9 @@ function normalizeWebAgentSessionRecord(agentId, rawAgent) {
|
|
|
218
237
|
lastResumeAt: typeof source.lastResumeAt === 'string' ? source.lastResumeAt : null,
|
|
219
238
|
lastResumeOk: typeof source.lastResumeOk === 'boolean' ? source.lastResumeOk : null,
|
|
220
239
|
lastResumeError: typeof source.lastResumeError === 'string' ? source.lastResumeError : '',
|
|
221
|
-
engineSessionId: typeof source.engineSessionId === 'string' ? source.engineSessionId : ''
|
|
240
|
+
engineSessionId: typeof source.engineSessionId === 'string' ? source.engineSessionId : '',
|
|
241
|
+
usageTotal: normalizeWebAgentUsageTotal(source.usageTotal),
|
|
242
|
+
model: normalizeStoredAgentModelValue(source.model)
|
|
222
243
|
};
|
|
223
244
|
}
|
|
224
245
|
|
|
@@ -756,6 +777,7 @@ function renderAgentPromptCommand(template, prompt) {
|
|
|
756
777
|
function buildCodexAgentExecCommand(template, prompt, options = {}) {
|
|
757
778
|
const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
758
779
|
const sessionId = options && typeof options.sessionId === 'string' ? options.sessionId.trim() : '';
|
|
780
|
+
const model = options && typeof options.model === 'string' ? options.model.trim() : '';
|
|
759
781
|
const execMatch = templateText.match(
|
|
760
782
|
/^((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)codex\s+exec\b/
|
|
761
783
|
);
|
|
@@ -765,12 +787,14 @@ function buildCodexAgentExecCommand(template, prompt, options = {}) {
|
|
|
765
787
|
const suffix = templateText.slice(execMatch[0].length);
|
|
766
788
|
const hasJson = /(?:^|\s)--json(?:\s|$)/.test(suffix);
|
|
767
789
|
const injectedFlags = hasJson ? '' : ' --json';
|
|
790
|
+
const hasModel = /(?:^|\s)(?:-m|--model)(?:\s|$)/.test(suffix);
|
|
791
|
+
const modelFlag = model && !hasModel ? ` --model ${model}` : '';
|
|
768
792
|
if (sessionId) {
|
|
769
793
|
const promptIndex = suffix.indexOf('{prompt}');
|
|
770
794
|
const resumeSuffix = `${suffix.slice(0, promptIndex)}${quoteBashSingleValue(sessionId)} ${suffix.slice(promptIndex)}`;
|
|
771
|
-
codexTemplate = `${prefix}codex exec resume${injectedFlags}${resumeSuffix}`;
|
|
795
|
+
codexTemplate = `${prefix}codex exec resume${injectedFlags}${modelFlag}${resumeSuffix}`;
|
|
772
796
|
} else {
|
|
773
|
-
codexTemplate = `${prefix}codex exec${injectedFlags}${suffix}`;
|
|
797
|
+
codexTemplate = `${prefix}codex exec${injectedFlags}${modelFlag}${suffix}`;
|
|
774
798
|
}
|
|
775
799
|
}
|
|
776
800
|
return codexTemplate === templateText
|
|
@@ -805,6 +829,10 @@ function buildClaudeAgentExecCommand(template, prompt, options = {}) {
|
|
|
805
829
|
if (sessionId) {
|
|
806
830
|
flagSpecs.push({ flag: `-r ${sessionId}`, pattern: /(?:^|\s)-r(?:\s|$)/ });
|
|
807
831
|
}
|
|
832
|
+
const model = options && typeof options.model === 'string' ? options.model.trim() : '';
|
|
833
|
+
if (model) {
|
|
834
|
+
flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)--model(?:\s|$)/ });
|
|
835
|
+
}
|
|
808
836
|
const claudeTemplate = prependAgentFlags(
|
|
809
837
|
templateText,
|
|
810
838
|
/^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)claude\b)(.*)$/,
|
|
@@ -815,14 +843,19 @@ function buildClaudeAgentExecCommand(template, prompt, options = {}) {
|
|
|
815
843
|
: renderAgentPromptCommand(claudeTemplate, prompt);
|
|
816
844
|
}
|
|
817
845
|
|
|
818
|
-
function buildGeminiAgentExecCommand(template, prompt) {
|
|
846
|
+
function buildGeminiAgentExecCommand(template, prompt, options = {}) {
|
|
819
847
|
const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
848
|
+
const flagSpecs = [
|
|
849
|
+
{ flag: '--output-format stream-json', pattern: /(?:^|\s)--output-format(?:\s|$)/ }
|
|
850
|
+
];
|
|
851
|
+
const model = options && typeof options.model === 'string' ? options.model.trim() : '';
|
|
852
|
+
if (model) {
|
|
853
|
+
flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
|
|
854
|
+
}
|
|
820
855
|
const geminiTemplate = prependAgentFlags(
|
|
821
856
|
templateText,
|
|
822
857
|
/^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)gemini\b)(.*)$/,
|
|
823
|
-
|
|
824
|
-
{ flag: '--output-format stream-json', pattern: /(?:^|\s)--output-format(?:\s|$)/ }
|
|
825
|
-
]
|
|
858
|
+
flagSpecs
|
|
826
859
|
);
|
|
827
860
|
return geminiTemplate === templateText
|
|
828
861
|
? renderAgentPromptCommand(templateText, prompt)
|
|
@@ -841,6 +874,10 @@ function buildOpenCodeAgentExecCommand(template, prompt, options = {}) {
|
|
|
841
874
|
pattern: /(?:^|\s)(?:--session|-s)(?:\s|$)/
|
|
842
875
|
});
|
|
843
876
|
}
|
|
877
|
+
const model = options && typeof options.model === 'string' ? options.model.trim() : '';
|
|
878
|
+
if (model) {
|
|
879
|
+
flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
|
|
880
|
+
}
|
|
844
881
|
const opencodeTemplate = prependAgentFlags(
|
|
845
882
|
templateText,
|
|
846
883
|
/^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)opencode\b.*?\s+run\b)(.*)$/,
|
|
@@ -856,7 +893,7 @@ function buildWebAgentExecCommand(template, prompt, agentProgram, options = {})
|
|
|
856
893
|
case 'claude':
|
|
857
894
|
return buildClaudeAgentExecCommand(template, prompt, options);
|
|
858
895
|
case 'gemini':
|
|
859
|
-
return buildGeminiAgentExecCommand(template, prompt);
|
|
896
|
+
return buildGeminiAgentExecCommand(template, prompt, options);
|
|
860
897
|
case 'codex':
|
|
861
898
|
return buildCodexAgentExecCommand(template, prompt, options);
|
|
862
899
|
case 'opencode':
|
|
@@ -1039,6 +1076,242 @@ function extractEngineSessionId(agentProgram, text) {
|
|
|
1039
1076
|
return '';
|
|
1040
1077
|
}
|
|
1041
1078
|
|
|
1079
|
+
function extractClaudeTurnUsage(text) {
|
|
1080
|
+
let result = null;
|
|
1081
|
+
for (const rawLine of String(text || '').split('\n')) {
|
|
1082
|
+
const payload = parseJsonObjectLine(rawLine);
|
|
1083
|
+
if (!payload || payload.type !== 'result') {
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
const usage = toPlainObject(payload.usage);
|
|
1087
|
+
result = {
|
|
1088
|
+
inputTokens: typeof usage.input_tokens === 'number' ? usage.input_tokens : null,
|
|
1089
|
+
outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : null,
|
|
1090
|
+
costUsd: typeof payload.total_cost_usd === 'number' ? payload.total_cost_usd : null
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
return result;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function extractCodexTurnUsage(text) {
|
|
1097
|
+
let result = null;
|
|
1098
|
+
for (const rawLine of String(text || '').split('\n')) {
|
|
1099
|
+
const payload = parseJsonObjectLine(rawLine);
|
|
1100
|
+
if (!payload || payload.type !== 'turn.completed') {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
const usage = toPlainObject(payload.usage);
|
|
1104
|
+
result = {
|
|
1105
|
+
inputTokens: typeof usage.input_tokens === 'number' ? usage.input_tokens : null,
|
|
1106
|
+
outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : null,
|
|
1107
|
+
costUsd: null
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
return result;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function extractOpenCodeTurnUsage(text) {
|
|
1114
|
+
let result = null;
|
|
1115
|
+
for (const rawLine of String(text || '').split('\n')) {
|
|
1116
|
+
const payload = parseJsonObjectLine(rawLine);
|
|
1117
|
+
if (!payload || payload.type !== 'step_finish') {
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const part = toPlainObject(payload.part);
|
|
1121
|
+
const tokens = toPlainObject(part.tokens);
|
|
1122
|
+
result = {
|
|
1123
|
+
inputTokens: typeof tokens.input === 'number' ? tokens.input : null,
|
|
1124
|
+
outputTokens: typeof tokens.output === 'number' ? tokens.output : null,
|
|
1125
|
+
costUsd: typeof part.cost === 'number' ? part.cost : null
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
return result;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Gemini 的非交互 stream-json 输出目前没有已确认的 usage/token 字段可解析,故不返回数据(而非猜测字段名)。
|
|
1132
|
+
function extractTurnUsage(agentProgram, text) {
|
|
1133
|
+
if (agentProgram === 'claude') {
|
|
1134
|
+
return extractClaudeTurnUsage(text);
|
|
1135
|
+
}
|
|
1136
|
+
if (agentProgram === 'codex') {
|
|
1137
|
+
return extractCodexTurnUsage(text);
|
|
1138
|
+
}
|
|
1139
|
+
if (agentProgram === 'opencode') {
|
|
1140
|
+
return extractOpenCodeTurnUsage(text);
|
|
1141
|
+
}
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function accumulateUsageTotal(baseline, turnUsage) {
|
|
1146
|
+
const base = baseline && typeof baseline === 'object'
|
|
1147
|
+
? baseline
|
|
1148
|
+
: { inputTokens: 0, outputTokens: 0, costUsd: null };
|
|
1149
|
+
const baseCost = typeof base.costUsd === 'number' ? base.costUsd : null;
|
|
1150
|
+
const turnCost = typeof turnUsage.costUsd === 'number' ? turnUsage.costUsd : null;
|
|
1151
|
+
return {
|
|
1152
|
+
inputTokens: (base.inputTokens || 0) + (turnUsage.inputTokens || 0),
|
|
1153
|
+
outputTokens: (base.outputTokens || 0) + (turnUsage.outputTokens || 0),
|
|
1154
|
+
// 引擎从未上报过费用(如 Codex)时保持 null,不要伪装成"花费为 0"
|
|
1155
|
+
costUsd: baseCost === null && turnCost === null ? null : (baseCost || 0) + (turnCost || 0)
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// 起一个短命的容器内子进程,通过对应协议问一次元信息(初始化握手 / model 列表),拿到结果就杀掉进程——
|
|
1160
|
+
// 不是长驻会话,不会产生真实对话轮次或费用。
|
|
1161
|
+
function runAgentProtocolProcess(ctx, containerName, command, options = {}) {
|
|
1162
|
+
const stdinLines = Array.isArray(options.stdinLines) ? options.stdinLines : [];
|
|
1163
|
+
const onLine = typeof options.onLine === 'function' ? options.onLine : null;
|
|
1164
|
+
const timeoutMs = typeof options.timeoutMs === 'number' ? options.timeoutMs : 8000;
|
|
1165
|
+
return new Promise(resolve => {
|
|
1166
|
+
let settled = false;
|
|
1167
|
+
let pending = '';
|
|
1168
|
+
let stdoutAll = '';
|
|
1169
|
+
let child;
|
|
1170
|
+
try {
|
|
1171
|
+
child = spawn(ctx.dockerCmd, ['exec', '-i', containerName, '/bin/bash', '-lc', command], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1172
|
+
} catch (e) {
|
|
1173
|
+
resolve({ result: null, stdout: '' });
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
1177
|
+
function finish(result) {
|
|
1178
|
+
if (settled) return;
|
|
1179
|
+
settled = true;
|
|
1180
|
+
clearTimeout(timer);
|
|
1181
|
+
try { child.kill('SIGKILL'); } catch (e) { /* 进程可能已退出 */ }
|
|
1182
|
+
resolve({ result, stdout: stdoutAll });
|
|
1183
|
+
}
|
|
1184
|
+
child.on('error', () => finish(null));
|
|
1185
|
+
child.on('close', () => finish(null));
|
|
1186
|
+
child.stdout.on('data', chunk => {
|
|
1187
|
+
const text = chunk.toString('utf-8');
|
|
1188
|
+
stdoutAll += text;
|
|
1189
|
+
if (!onLine) {
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
pending += text;
|
|
1193
|
+
let newlineIndex = pending.indexOf('\n');
|
|
1194
|
+
while (newlineIndex !== -1) {
|
|
1195
|
+
const line = pending.slice(0, newlineIndex);
|
|
1196
|
+
pending = pending.slice(newlineIndex + 1);
|
|
1197
|
+
const trimmed = line.trim();
|
|
1198
|
+
if (trimmed) {
|
|
1199
|
+
const payload = parseJsonObjectLine(trimmed);
|
|
1200
|
+
if (payload) {
|
|
1201
|
+
const matched = onLine(payload);
|
|
1202
|
+
if (matched !== undefined) {
|
|
1203
|
+
finish(matched);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
newlineIndex = pending.indexOf('\n');
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
try {
|
|
1212
|
+
stdinLines.forEach(line => child.stdin.write(`${line}\n`));
|
|
1213
|
+
if (!stdinLines.length) {
|
|
1214
|
+
child.stdin.end();
|
|
1215
|
+
}
|
|
1216
|
+
} catch (e) { /* 进程可能已提前退出 */ }
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function normalizeAgentModelCatalogEntry(item) {
|
|
1221
|
+
const value = pickFirstString(item && item.value, item && item.id, item && item.model);
|
|
1222
|
+
if (!value) {
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
value,
|
|
1227
|
+
label: pickFirstString(item && item.displayName, value),
|
|
1228
|
+
description: pickFirstString(item && item.description)
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function extractClaudeModelsFromControlResponse(payload) {
|
|
1233
|
+
if (!payload || payload.type !== 'control_response') {
|
|
1234
|
+
return undefined;
|
|
1235
|
+
}
|
|
1236
|
+
const response = toPlainObject(payload.response);
|
|
1237
|
+
if (response.request_id !== 'manyoyo-model-catalog') {
|
|
1238
|
+
return undefined;
|
|
1239
|
+
}
|
|
1240
|
+
const inner = toPlainObject(response.response);
|
|
1241
|
+
const models = Array.isArray(inner.models) ? inner.models : [];
|
|
1242
|
+
return models.map(normalizeAgentModelCatalogEntry).filter(Boolean);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function extractCodexModelsFromListResponse(payload) {
|
|
1246
|
+
if (!payload || payload.id !== 'manyoyo-model-list' || !payload.result) {
|
|
1247
|
+
return undefined;
|
|
1248
|
+
}
|
|
1249
|
+
const data = Array.isArray(payload.result.data) ? payload.result.data : [];
|
|
1250
|
+
return data.map(normalizeAgentModelCatalogEntry).filter(Boolean);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function extractOpenCodeModelsFromOutput(text) {
|
|
1254
|
+
return String(text || '')
|
|
1255
|
+
.split('\n')
|
|
1256
|
+
.map(line => line.trim())
|
|
1257
|
+
.filter(Boolean)
|
|
1258
|
+
.map(value => ({ value, label: value, description: '' }));
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
async function fetchAgentModelCatalog(ctx, containerName, agentProgram) {
|
|
1262
|
+
if (agentProgram === 'claude') {
|
|
1263
|
+
const requestLine = JSON.stringify({
|
|
1264
|
+
type: 'control_request',
|
|
1265
|
+
request_id: 'manyoyo-model-catalog',
|
|
1266
|
+
request: { subtype: 'initialize' }
|
|
1267
|
+
});
|
|
1268
|
+
const { result } = await runAgentProtocolProcess(
|
|
1269
|
+
ctx,
|
|
1270
|
+
containerName,
|
|
1271
|
+
'claude -p --input-format stream-json --output-format stream-json --verbose',
|
|
1272
|
+
{ stdinLines: [requestLine], onLine: extractClaudeModelsFromControlResponse }
|
|
1273
|
+
);
|
|
1274
|
+
return Array.isArray(result) ? result : [];
|
|
1275
|
+
}
|
|
1276
|
+
if (agentProgram === 'codex') {
|
|
1277
|
+
const stdinLines = [
|
|
1278
|
+
JSON.stringify({ jsonrpc: '2.0', id: 'manyoyo-init', method: 'initialize', params: { clientInfo: { name: 'manyoyo', version: '1.0.0' } } }),
|
|
1279
|
+
JSON.stringify({ jsonrpc: '2.0', id: 'manyoyo-model-list', method: 'model/list', params: {} })
|
|
1280
|
+
];
|
|
1281
|
+
const { result } = await runAgentProtocolProcess(
|
|
1282
|
+
ctx,
|
|
1283
|
+
containerName,
|
|
1284
|
+
'codex app-server',
|
|
1285
|
+
{ stdinLines, onLine: extractCodexModelsFromListResponse }
|
|
1286
|
+
);
|
|
1287
|
+
return Array.isArray(result) ? result : [];
|
|
1288
|
+
}
|
|
1289
|
+
if (agentProgram === 'opencode') {
|
|
1290
|
+
const { stdout } = await runAgentProtocolProcess(ctx, containerName, 'opencode models', {});
|
|
1291
|
+
return extractOpenCodeModelsFromOutput(stdout);
|
|
1292
|
+
}
|
|
1293
|
+
// Gemini 的 --acp 模式在认证前就会挂起重试,没有可用的“认证前”探测窗口,暂不支持目录抓取。
|
|
1294
|
+
return [];
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const SAFE_AGENT_MODEL_VALUE_PATTERN = /^[A-Za-z0-9._\-[\]/]+$/;
|
|
1298
|
+
|
|
1299
|
+
function normalizeAgentModelValueInput(value) {
|
|
1300
|
+
const trimmed = String(value || '').trim();
|
|
1301
|
+
if (!trimmed) {
|
|
1302
|
+
return '';
|
|
1303
|
+
}
|
|
1304
|
+
if (!SAFE_AGENT_MODEL_VALUE_PATTERN.test(trimmed)) {
|
|
1305
|
+
throw new Error(`模型名称包含非法字符: ${trimmed}`);
|
|
1306
|
+
}
|
|
1307
|
+
return trimmed;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function normalizeStoredAgentModelValue(value) {
|
|
1311
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
1312
|
+
return SAFE_AGENT_MODEL_VALUE_PATTERN.test(trimmed) ? trimmed : '';
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1042
1315
|
function getAgentRuntimeMeta(template) {
|
|
1043
1316
|
const normalizedTemplate = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
1044
1317
|
const agentProgram = resolveAgentProgram(normalizedTemplate);
|
|
@@ -1744,7 +2017,8 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
|
|
|
1744
2017
|
? prompt
|
|
1745
2018
|
: buildAgentPromptWithHistory(agentSession, prompt);
|
|
1746
2019
|
const command = buildWebAgentExecCommand(effectiveTemplate, effectivePrompt, agentMeta.agentProgram, {
|
|
1747
|
-
sessionId: sessionIdForRun
|
|
2020
|
+
sessionId: sessionIdForRun,
|
|
2021
|
+
model: agentSession.model || ''
|
|
1748
2022
|
});
|
|
1749
2023
|
const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
|
|
1750
2024
|
|
|
@@ -1762,13 +2036,15 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
|
|
|
1762
2036
|
}
|
|
1763
2037
|
|
|
1764
2038
|
function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, meta, result) {
|
|
2039
|
+
const turnUsage = extractTurnUsage(agentMeta.agentProgram, result.stdout);
|
|
1765
2040
|
appendWebSessionMessage(state.webHistoryDir, sessionRef, 'assistant', result.output, {
|
|
1766
2041
|
exitCode: result.exitCode,
|
|
1767
2042
|
mode: 'agent',
|
|
1768
2043
|
contextMode: meta.contextMode,
|
|
1769
2044
|
resumeAttempted: meta.resumeAttempted,
|
|
1770
2045
|
resumeSucceeded: meta.resumeSucceeded,
|
|
1771
|
-
interrupted: result.interrupted === true
|
|
2046
|
+
interrupted: result.interrupted === true,
|
|
2047
|
+
...(turnUsage ? { usage: turnUsage } : {})
|
|
1772
2048
|
});
|
|
1773
2049
|
const patch = {
|
|
1774
2050
|
lastResumeAt: meta.resumeAttempted ? new Date().toISOString() : (agentSession.lastResumeAt || null),
|
|
@@ -1779,6 +2055,9 @@ function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, m
|
|
|
1779
2055
|
if (engineSessionId) {
|
|
1780
2056
|
patch.engineSessionId = engineSessionId;
|
|
1781
2057
|
}
|
|
2058
|
+
if (turnUsage) {
|
|
2059
|
+
patch.usageTotal = accumulateUsageTotal(agentSession.usageTotal, turnUsage);
|
|
2060
|
+
}
|
|
1782
2061
|
patchWebAgentSessionState(state.webHistoryDir, sessionRef, patch);
|
|
1783
2062
|
}
|
|
1784
2063
|
|
|
@@ -3541,6 +3820,7 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
|
|
|
3541
3820
|
agentEnabled: isAgentPromptCommandEnabled(effectiveAgentPromptCommand),
|
|
3542
3821
|
agentProgram: effectiveAgentProgram || '',
|
|
3543
3822
|
resumeSupported: effectiveResumeSupported,
|
|
3823
|
+
model: agentSession.model || '',
|
|
3544
3824
|
hostPath: applied.hostPath || '',
|
|
3545
3825
|
containerPath: applied.containerPath || ''
|
|
3546
3826
|
};
|
|
@@ -3626,6 +3906,8 @@ function buildSessionDetail(ctx, state, containerMap, name) {
|
|
|
3626
3906
|
lastResumeAt: agentSession.lastResumeAt || null,
|
|
3627
3907
|
lastResumeOk: typeof agentSession.lastResumeOk === 'boolean' ? agentSession.lastResumeOk : null,
|
|
3628
3908
|
lastResumeError: agentSession.lastResumeError || '',
|
|
3909
|
+
usageTotal: agentSession.usageTotal || null,
|
|
3910
|
+
model: agentSession.model || '',
|
|
3629
3911
|
applied
|
|
3630
3912
|
};
|
|
3631
3913
|
}
|
|
@@ -4543,6 +4825,51 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
4543
4825
|
sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), detail });
|
|
4544
4826
|
}
|
|
4545
4827
|
},
|
|
4828
|
+
{
|
|
4829
|
+
method: 'GET',
|
|
4830
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/models$/),
|
|
4831
|
+
handler: async match => {
|
|
4832
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
4833
|
+
if (!sessionRef) {
|
|
4834
|
+
return;
|
|
4835
|
+
}
|
|
4836
|
+
const containerMap = listWebManyoyoContainers(ctx);
|
|
4837
|
+
const containerInfo = containerMap[sessionRef.containerName] || {};
|
|
4838
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
4839
|
+
const effectiveTemplate = resolveEffectiveAgentPromptCommandForSession(
|
|
4840
|
+
history,
|
|
4841
|
+
sessionRef.agentId,
|
|
4842
|
+
containerInfo.defaultCommand
|
|
4843
|
+
);
|
|
4844
|
+
const agentProgram = resolveAgentProgram(effectiveTemplate);
|
|
4845
|
+
const models = await fetchAgentModelCatalog(ctx, sessionRef.containerName, agentProgram);
|
|
4846
|
+
sendJson(res, 200, {
|
|
4847
|
+
name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
|
|
4848
|
+
agentProgram,
|
|
4849
|
+
models
|
|
4850
|
+
});
|
|
4851
|
+
}
|
|
4852
|
+
},
|
|
4853
|
+
{
|
|
4854
|
+
method: 'POST',
|
|
4855
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/model$/),
|
|
4856
|
+
handler: async match => {
|
|
4857
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
4858
|
+
if (!sessionRef) {
|
|
4859
|
+
return;
|
|
4860
|
+
}
|
|
4861
|
+
const payload = await readJsonBody(req);
|
|
4862
|
+
let model;
|
|
4863
|
+
try {
|
|
4864
|
+
model = normalizeAgentModelValueInput(payload.model);
|
|
4865
|
+
} catch (e) {
|
|
4866
|
+
sendJson(res, 400, { error: e.message || String(e) });
|
|
4867
|
+
return;
|
|
4868
|
+
}
|
|
4869
|
+
patchWebAgentSessionState(state.webHistoryDir, sessionRef, { model });
|
|
4870
|
+
sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), model });
|
|
4871
|
+
}
|
|
4872
|
+
},
|
|
4546
4873
|
{
|
|
4547
4874
|
method: 'GET',
|
|
4548
4875
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/audit$/),
|