@xcanwin/manyoyo 6.2.9 → 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 +192 -3
- package/lib/web/server.js +232 -10
- 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) {
|
|
@@ -2217,7 +2342,7 @@
|
|
|
2217
2342
|
if (activityModelChip) {
|
|
2218
2343
|
activityModelChip.textContent = `模型 · ${resolveToolbarModelLabel()}`;
|
|
2219
2344
|
activityModelChip.title = state.active
|
|
2220
|
-
? '
|
|
2345
|
+
? '点击选择本会话使用的模型'
|
|
2221
2346
|
: '请先选择会话';
|
|
2222
2347
|
}
|
|
2223
2348
|
if (viewActivityBtn) viewActivityBtn.classList.toggle('is-active', activityTab);
|
|
@@ -2322,6 +2447,27 @@
|
|
|
2322
2447
|
if (externalLinkModal) {
|
|
2323
2448
|
externalLinkModal.hidden = !state.externalLinkModalOpen;
|
|
2324
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
|
+
}
|
|
2325
2471
|
if (agentTemplateSaveBtn) {
|
|
2326
2472
|
agentTemplateSaveBtn.disabled = state.agentTemplateSaving || !state.active;
|
|
2327
2473
|
}
|
|
@@ -2345,7 +2491,7 @@
|
|
|
2345
2491
|
}
|
|
2346
2492
|
document.body.classList.toggle(
|
|
2347
2493
|
'modal-open',
|
|
2348
|
-
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
|
|
2349
2495
|
);
|
|
2350
2496
|
if (composer) {
|
|
2351
2497
|
composer.hidden = !activityTab;
|
|
@@ -4238,6 +4384,45 @@
|
|
|
4238
4384
|
});
|
|
4239
4385
|
}
|
|
4240
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
|
+
|
|
4241
4426
|
if (configCancelBtn) {
|
|
4242
4427
|
configCancelBtn.addEventListener('click', function () {
|
|
4243
4428
|
closeConfigModal();
|
|
@@ -4764,6 +4949,10 @@
|
|
|
4764
4949
|
closeAgentTemplateModal();
|
|
4765
4950
|
syncUi();
|
|
4766
4951
|
}
|
|
4952
|
+
if (event.key === 'Escape' && state.modelModalOpen && !state.modelModalSaving) {
|
|
4953
|
+
closeModelModal();
|
|
4954
|
+
syncUi();
|
|
4955
|
+
}
|
|
4767
4956
|
if (event.key === 'Escape' && state.externalLinkModalOpen) {
|
|
4768
4957
|
closeExternalLinkModalView();
|
|
4769
4958
|
syncUi();
|
package/lib/web/server.js
CHANGED
|
@@ -190,7 +190,8 @@ function createEmptyWebAgentSession(agentId, agentName) {
|
|
|
190
190
|
lastResumeOk: null,
|
|
191
191
|
lastResumeError: '',
|
|
192
192
|
engineSessionId: '',
|
|
193
|
-
usageTotal: null
|
|
193
|
+
usageTotal: null,
|
|
194
|
+
model: ''
|
|
194
195
|
};
|
|
195
196
|
}
|
|
196
197
|
|
|
@@ -237,7 +238,8 @@ function normalizeWebAgentSessionRecord(agentId, rawAgent) {
|
|
|
237
238
|
lastResumeOk: typeof source.lastResumeOk === 'boolean' ? source.lastResumeOk : null,
|
|
238
239
|
lastResumeError: typeof source.lastResumeError === 'string' ? source.lastResumeError : '',
|
|
239
240
|
engineSessionId: typeof source.engineSessionId === 'string' ? source.engineSessionId : '',
|
|
240
|
-
usageTotal: normalizeWebAgentUsageTotal(source.usageTotal)
|
|
241
|
+
usageTotal: normalizeWebAgentUsageTotal(source.usageTotal),
|
|
242
|
+
model: normalizeStoredAgentModelValue(source.model)
|
|
241
243
|
};
|
|
242
244
|
}
|
|
243
245
|
|
|
@@ -775,6 +777,7 @@ function renderAgentPromptCommand(template, prompt) {
|
|
|
775
777
|
function buildCodexAgentExecCommand(template, prompt, options = {}) {
|
|
776
778
|
const templateText = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
777
779
|
const sessionId = options && typeof options.sessionId === 'string' ? options.sessionId.trim() : '';
|
|
780
|
+
const model = options && typeof options.model === 'string' ? options.model.trim() : '';
|
|
778
781
|
const execMatch = templateText.match(
|
|
779
782
|
/^((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)codex\s+exec\b/
|
|
780
783
|
);
|
|
@@ -784,12 +787,14 @@ function buildCodexAgentExecCommand(template, prompt, options = {}) {
|
|
|
784
787
|
const suffix = templateText.slice(execMatch[0].length);
|
|
785
788
|
const hasJson = /(?:^|\s)--json(?:\s|$)/.test(suffix);
|
|
786
789
|
const injectedFlags = hasJson ? '' : ' --json';
|
|
790
|
+
const hasModel = /(?:^|\s)(?:-m|--model)(?:\s|$)/.test(suffix);
|
|
791
|
+
const modelFlag = model && !hasModel ? ` --model ${model}` : '';
|
|
787
792
|
if (sessionId) {
|
|
788
793
|
const promptIndex = suffix.indexOf('{prompt}');
|
|
789
794
|
const resumeSuffix = `${suffix.slice(0, promptIndex)}${quoteBashSingleValue(sessionId)} ${suffix.slice(promptIndex)}`;
|
|
790
|
-
codexTemplate = `${prefix}codex exec resume${injectedFlags}${resumeSuffix}`;
|
|
795
|
+
codexTemplate = `${prefix}codex exec resume${injectedFlags}${modelFlag}${resumeSuffix}`;
|
|
791
796
|
} else {
|
|
792
|
-
codexTemplate = `${prefix}codex exec${injectedFlags}${suffix}`;
|
|
797
|
+
codexTemplate = `${prefix}codex exec${injectedFlags}${modelFlag}${suffix}`;
|
|
793
798
|
}
|
|
794
799
|
}
|
|
795
800
|
return codexTemplate === templateText
|
|
@@ -824,6 +829,10 @@ function buildClaudeAgentExecCommand(template, prompt, options = {}) {
|
|
|
824
829
|
if (sessionId) {
|
|
825
830
|
flagSpecs.push({ flag: `-r ${sessionId}`, pattern: /(?:^|\s)-r(?:\s|$)/ });
|
|
826
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
|
+
}
|
|
827
836
|
const claudeTemplate = prependAgentFlags(
|
|
828
837
|
templateText,
|
|
829
838
|
/^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)claude\b)(.*)$/,
|
|
@@ -834,14 +843,19 @@ function buildClaudeAgentExecCommand(template, prompt, options = {}) {
|
|
|
834
843
|
: renderAgentPromptCommand(claudeTemplate, prompt);
|
|
835
844
|
}
|
|
836
845
|
|
|
837
|
-
function buildGeminiAgentExecCommand(template, prompt) {
|
|
846
|
+
function buildGeminiAgentExecCommand(template, prompt, options = {}) {
|
|
838
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
|
+
}
|
|
839
855
|
const geminiTemplate = prependAgentFlags(
|
|
840
856
|
templateText,
|
|
841
857
|
/^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)gemini\b)(.*)$/,
|
|
842
|
-
|
|
843
|
-
{ flag: '--output-format stream-json', pattern: /(?:^|\s)--output-format(?:\s|$)/ }
|
|
844
|
-
]
|
|
858
|
+
flagSpecs
|
|
845
859
|
);
|
|
846
860
|
return geminiTemplate === templateText
|
|
847
861
|
? renderAgentPromptCommand(templateText, prompt)
|
|
@@ -860,6 +874,10 @@ function buildOpenCodeAgentExecCommand(template, prompt, options = {}) {
|
|
|
860
874
|
pattern: /(?:^|\s)(?:--session|-s)(?:\s|$)/
|
|
861
875
|
});
|
|
862
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
|
+
}
|
|
863
881
|
const opencodeTemplate = prependAgentFlags(
|
|
864
882
|
templateText,
|
|
865
883
|
/^(((?:(?:[A-Za-z_][A-Za-z0-9_]*=)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s]+)\s+)*)opencode\b.*?\s+run\b)(.*)$/,
|
|
@@ -875,7 +893,7 @@ function buildWebAgentExecCommand(template, prompt, agentProgram, options = {})
|
|
|
875
893
|
case 'claude':
|
|
876
894
|
return buildClaudeAgentExecCommand(template, prompt, options);
|
|
877
895
|
case 'gemini':
|
|
878
|
-
return buildGeminiAgentExecCommand(template, prompt);
|
|
896
|
+
return buildGeminiAgentExecCommand(template, prompt, options);
|
|
879
897
|
case 'codex':
|
|
880
898
|
return buildCodexAgentExecCommand(template, prompt, options);
|
|
881
899
|
case 'opencode':
|
|
@@ -1138,6 +1156,162 @@ function accumulateUsageTotal(baseline, turnUsage) {
|
|
|
1138
1156
|
};
|
|
1139
1157
|
}
|
|
1140
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
|
+
|
|
1141
1315
|
function getAgentRuntimeMeta(template) {
|
|
1142
1316
|
const normalizedTemplate = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
|
|
1143
1317
|
const agentProgram = resolveAgentProgram(normalizedTemplate);
|
|
@@ -1843,7 +2017,8 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
|
|
|
1843
2017
|
? prompt
|
|
1844
2018
|
: buildAgentPromptWithHistory(agentSession, prompt);
|
|
1845
2019
|
const command = buildWebAgentExecCommand(effectiveTemplate, effectivePrompt, agentMeta.agentProgram, {
|
|
1846
|
-
sessionId: sessionIdForRun
|
|
2020
|
+
sessionId: sessionIdForRun,
|
|
2021
|
+
model: agentSession.model || ''
|
|
1847
2022
|
});
|
|
1848
2023
|
const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
|
|
1849
2024
|
|
|
@@ -3645,6 +3820,7 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
|
|
|
3645
3820
|
agentEnabled: isAgentPromptCommandEnabled(effectiveAgentPromptCommand),
|
|
3646
3821
|
agentProgram: effectiveAgentProgram || '',
|
|
3647
3822
|
resumeSupported: effectiveResumeSupported,
|
|
3823
|
+
model: agentSession.model || '',
|
|
3648
3824
|
hostPath: applied.hostPath || '',
|
|
3649
3825
|
containerPath: applied.containerPath || ''
|
|
3650
3826
|
};
|
|
@@ -3731,6 +3907,7 @@ function buildSessionDetail(ctx, state, containerMap, name) {
|
|
|
3731
3907
|
lastResumeOk: typeof agentSession.lastResumeOk === 'boolean' ? agentSession.lastResumeOk : null,
|
|
3732
3908
|
lastResumeError: agentSession.lastResumeError || '',
|
|
3733
3909
|
usageTotal: agentSession.usageTotal || null,
|
|
3910
|
+
model: agentSession.model || '',
|
|
3734
3911
|
applied
|
|
3735
3912
|
};
|
|
3736
3913
|
}
|
|
@@ -4648,6 +4825,51 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
4648
4825
|
sendJson(res, 200, { name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId), detail });
|
|
4649
4826
|
}
|
|
4650
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
|
+
},
|
|
4651
4873
|
{
|
|
4652
4874
|
method: 'GET',
|
|
4653
4875
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/audit$/),
|