@bolloon/bolloon-agent 0.3.8 → 0.3.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/dist/agents/pi-sdk-tools.js +90 -9
- package/dist/agents/pi-sdk.js +26 -5
- package/dist/bootstrap/exhaust-scrubber.js +233 -0
- package/dist/bootstrap/memory-compressor.js +11 -0
- package/dist/constraint-runtime/src/tools/PolymarketSDK/cancelOrder.js +19 -4
- package/dist/constraint-runtime/src/tools/PolymarketSDK/clobShared.js +104 -0
- package/dist/constraint-runtime/src/tools/PolymarketSDK/createOrder.js +34 -4
- package/dist/constraint-runtime/src/tools/PolymarketSDK/getOrders.js +17 -4
- package/dist/external-engines/delegate.js +148 -0
- package/dist/external-engines/discovery.js +394 -0
- package/dist/external-engines/index.js +9 -0
- package/dist/external-engines/types.js +11 -0
- package/dist/pi-ecosystem-judgment/injection-gate.js +85 -1
- package/dist/web/api-config.html +250 -1
- package/dist/web/client.js +32 -10
- package/dist/web/index.html +47 -23
- package/dist/web/routes-external-engines.js +111 -0
- package/dist/web/routes-judgments.js +8 -2
- package/dist/web/server.js +15 -0
- package/dist/web/style.css +61 -0
- package/package.json +1 -1
package/dist/web/api-config.html
CHANGED
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
<span class="api-tab-icon">🎵</span>
|
|
35
35
|
<span>音频生成</span>
|
|
36
36
|
</button>
|
|
37
|
+
<button class="api-tab" data-tab="engines" onclick="switchTab('engines')">
|
|
38
|
+
<span class="api-tab-icon">🤖</span>
|
|
39
|
+
<span>外部智能体</span>
|
|
40
|
+
</button>
|
|
37
41
|
</div>
|
|
38
42
|
|
|
39
43
|
<!-- LLM 面板 -->
|
|
@@ -63,6 +67,16 @@
|
|
|
63
67
|
</div>
|
|
64
68
|
</div>
|
|
65
69
|
|
|
70
|
+
<!-- 外部智能体面板 -->
|
|
71
|
+
<div class="api-panel" id="panel-engines" data-panel="engines" style="display: none;">
|
|
72
|
+
<div class="video-intro">
|
|
73
|
+
<p>Bolloon 自动发现本机已安装的 AI 编码工具(Codex / Claude Code / OpenCode / OpenClaw / Hermes)与实验目录声明的 API。可在此筛选模型并把它们的 API 当作 Bolloon 的供应商一键导入,无需重复填 Key。委派执行请通过对话里的 <code>delegate_to_engine</code> 工具。</p>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="provider-list" id="engineList">
|
|
76
|
+
<div class="loading-state">加载中...</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
66
80
|
<!-- 配置弹窗 -->
|
|
67
81
|
<div class="modal-overlay" id="configModal" style="display: none;">
|
|
68
82
|
<div class="modal-box">
|
|
@@ -186,6 +200,62 @@
|
|
|
186
200
|
</div>
|
|
187
201
|
</div>
|
|
188
202
|
|
|
203
|
+
<!-- 外部智能体 配置弹窗 -->
|
|
204
|
+
<div class="modal-overlay" id="engineModal" style="display: none;">
|
|
205
|
+
<div class="modal-box">
|
|
206
|
+
<div class="modal-header">
|
|
207
|
+
<div class="modal-title-group">
|
|
208
|
+
<div class="modal-icon" id="engineModalIcon">A</div>
|
|
209
|
+
<div>
|
|
210
|
+
<h2 id="engineModalTitle">配置</h2>
|
|
211
|
+
<p class="modal-subtitle" id="engineModalSubtitle">描述</p>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
<button class="modal-close" onclick="closeEngineModal()">×</button>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<form class="modal-form" id="engineConfigForm">
|
|
218
|
+
<div class="form-group">
|
|
219
|
+
<label>映射供应商</label>
|
|
220
|
+
<select id="engineProviderInput"></select>
|
|
221
|
+
<p class="form-hint">导入时写进该 Bolloon 供应商 (可覆盖发现的映射)</p>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div class="form-group">
|
|
225
|
+
<label>API Key</label>
|
|
226
|
+
<input type="password" id="engineApiKeyInput" placeholder="输入 API Key">
|
|
227
|
+
<p class="form-hint" id="engineApiKeyHint">已有 Key 会保留,输入新值以更新</p>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div class="form-group">
|
|
231
|
+
<label>Base URL</label>
|
|
232
|
+
<input type="text" id="engineBaseUrlInput" placeholder="https://api.example.com/v1">
|
|
233
|
+
<p class="form-hint">留空使用默认地址</p>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div class="form-group">
|
|
237
|
+
<label>模型(可筛选)</label>
|
|
238
|
+
<div class="combobox" id="engineModelCombobox">
|
|
239
|
+
<input type="text" id="engineModelInput" placeholder="输入关键字筛选模型…" autocomplete="off">
|
|
240
|
+
<div class="combobox-list" id="engineModelList" style="display: none;"></div>
|
|
241
|
+
</div>
|
|
242
|
+
<p class="form-hint" id="engineModelHint">从发现到的候选模型里筛选,或手动输入自定义模型名</p>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<div class="form-group">
|
|
246
|
+
<button type="button" class="test-btn" id="engineImportBtn" onclick="importEngine()">
|
|
247
|
+
⚡ 导入为供应商
|
|
248
|
+
</button>
|
|
249
|
+
<div class="test-result" id="engineImportResult" style="display: none;"></div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<div class="form-actions">
|
|
253
|
+
<button type="button" class="btn-cancel" onclick="closeEngineModal()">取消</button>
|
|
254
|
+
</div>
|
|
255
|
+
</form>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
189
259
|
<script>
|
|
190
260
|
// 当前 tab
|
|
191
261
|
let currentTab = 'llm';
|
|
@@ -194,7 +264,9 @@
|
|
|
194
264
|
let llmConfigData = null;
|
|
195
265
|
let videoConfigData = null;
|
|
196
266
|
let audioConfigData = null;
|
|
267
|
+
let engineData = null;
|
|
197
268
|
let currentProvider = null;
|
|
269
|
+
let currentEngine = null;
|
|
198
270
|
let currentProviderType = 'llm'; // 'llm' | 'video' | 'audio'
|
|
199
271
|
|
|
200
272
|
// ==================== Tab 切换 ====================
|
|
@@ -211,6 +283,13 @@
|
|
|
211
283
|
}
|
|
212
284
|
|
|
213
285
|
function updateCountBadge() {
|
|
286
|
+
if (currentTab === 'engines') {
|
|
287
|
+
const list = engineData && engineData.engines ? engineData.engines : [];
|
|
288
|
+
const total = list.length;
|
|
289
|
+
const available = list.filter(e => e.available).length;
|
|
290
|
+
document.getElementById('configCount').textContent = available + '/' + total + ' 智能体可用';
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
214
293
|
const data = currentTab === 'llm' ? llmConfigData :
|
|
215
294
|
currentTab === 'video' ? videoConfigData : audioConfigData;
|
|
216
295
|
if (!data) {
|
|
@@ -235,7 +314,19 @@
|
|
|
235
314
|
|
|
236
315
|
// ==================== 加载 ====================
|
|
237
316
|
async function loadAll() {
|
|
238
|
-
await Promise.all([loadLLMConfig(), loadVideoConfig(), loadAudioConfig()]);
|
|
317
|
+
await Promise.all([loadLLMConfig(), loadVideoConfig(), loadAudioConfig(), loadEngines()]);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function loadEngines() {
|
|
321
|
+
try {
|
|
322
|
+
const resp = await fetch('/api/external-engines');
|
|
323
|
+
engineData = await resp.json();
|
|
324
|
+
renderEngines();
|
|
325
|
+
if (currentTab === 'engines') updateCountBadge();
|
|
326
|
+
} catch (err) {
|
|
327
|
+
const el = document.getElementById('engineList');
|
|
328
|
+
if (el) el.innerHTML = '<div class="error-state">加载失败: ' + err.message + '</div>';
|
|
329
|
+
}
|
|
239
330
|
}
|
|
240
331
|
|
|
241
332
|
async function loadLLMConfig() {
|
|
@@ -518,6 +609,164 @@
|
|
|
518
609
|
}
|
|
519
610
|
};
|
|
520
611
|
|
|
612
|
+
// ==================== 外部智能体 渲染 ====================
|
|
613
|
+
function renderEngines() {
|
|
614
|
+
const listEl = document.getElementById('engineList');
|
|
615
|
+
if (!listEl) return;
|
|
616
|
+
const list = engineData && engineData.engines ? engineData.engines : [];
|
|
617
|
+
if (list.length === 0) {
|
|
618
|
+
listEl.innerHTML = '<div class="error-state">未发现本机已安装的外部编码智能体,也未找到实验目录 API(BOLLOON_EXPERIMENT_API_DIR)</div>';
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
let html = '';
|
|
622
|
+
for (const e of list) {
|
|
623
|
+
const isExperiment = String(e.id).startsWith('experiment:');
|
|
624
|
+
let statusText, statusClass;
|
|
625
|
+
if (e.available) { statusText = '可用'; statusClass = 'status-green'; }
|
|
626
|
+
else if (e.installed) { statusText = '已装未配'; statusClass = 'status-yellow'; }
|
|
627
|
+
else if (e.configured) { statusText = '已配未装'; statusClass = 'status-yellow'; }
|
|
628
|
+
else { statusText = '未发现'; statusClass = 'status-muted'; }
|
|
629
|
+
|
|
630
|
+
html += `
|
|
631
|
+
<div class="provider-card ${e.available ? 'active' : ''}" onclick="openEngineModal('${e.id}')">
|
|
632
|
+
<div class="provider-header">
|
|
633
|
+
<div class="provider-icon">${isExperiment ? '🧪' : e.displayName.charAt(0).toUpperCase()}</div>
|
|
634
|
+
<div class="provider-info">
|
|
635
|
+
<h3 class="provider-name">${e.displayName}</h3>
|
|
636
|
+
<p class="provider-desc">${e.id}${e.provider ? ' → ' + e.provider : ''}${e.cliPath ? ' · 已安装' : ''}</p>
|
|
637
|
+
</div>
|
|
638
|
+
<div class="status-badge ${statusClass}">
|
|
639
|
+
<span class="status-dot"></span>
|
|
640
|
+
${statusText}
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
<div class="provider-footer">
|
|
644
|
+
<div class="config-status">
|
|
645
|
+
<span class="status-icon ${e.configured ? 'configured' : ''}">${e.configured ? '✓' : '○'}</span>
|
|
646
|
+
${e.configured ? '已配置 API' : '未配置 API'}
|
|
647
|
+
</div>
|
|
648
|
+
${e.model ? '<div class="model-info">模型: ' + e.model + '</div>' : ''}
|
|
649
|
+
${e.models && e.models.length ? '<div class="model-info">候选模型: ' + e.models.length + ' 个(可筛选)</div>' : ''}
|
|
650
|
+
<div class="arrow-indicator">›</div>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
`;
|
|
654
|
+
}
|
|
655
|
+
listEl.innerHTML = html;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ==================== 外部智能体 配置弹窗 ====================
|
|
659
|
+
const ENGINE_PROVIDER_FALLBACK = ['openai','anthropic','ollama','openrouter','gemini','minimax','deepseek','kimi','glm','qwen','mimo','local'];
|
|
660
|
+
|
|
661
|
+
function openEngineModal(id) {
|
|
662
|
+
const list = engineData && engineData.engines ? engineData.engines : [];
|
|
663
|
+
const e = list.find(x => x.id === id);
|
|
664
|
+
if (!e) return;
|
|
665
|
+
currentEngine = e;
|
|
666
|
+
|
|
667
|
+
document.getElementById('engineModalIcon').textContent = String(e.displayName).charAt(0).toUpperCase();
|
|
668
|
+
document.getElementById('engineModalTitle').textContent = '配置 ' + e.displayName;
|
|
669
|
+
document.getElementById('engineModalSubtitle').textContent = (e.notes || '') + (e.configPath ? ' · 配置: ' + e.configPath : '');
|
|
670
|
+
|
|
671
|
+
// 映射供应商 select
|
|
672
|
+
const provSel = document.getElementById('engineProviderInput');
|
|
673
|
+
const provKeys = (llmConfigData && llmConfigData.providers) ? Object.keys(llmConfigData.providers) : ENGINE_PROVIDER_FALLBACK;
|
|
674
|
+
const mapped = e.provider || 'openai';
|
|
675
|
+
provSel.innerHTML = provKeys.map(k => `<option value="${k}" ${k === mapped ? 'selected' : ''}>${k}</option>`).join('');
|
|
676
|
+
|
|
677
|
+
document.getElementById('engineApiKeyInput').value = '';
|
|
678
|
+
document.getElementById('engineApiKeyHint').textContent = e.apiKey
|
|
679
|
+
? '当前已发现 (***' + (e.apiKey.slice(-4) || '') + '),输入新值以更新'
|
|
680
|
+
: '输入 API Key';
|
|
681
|
+
document.getElementById('engineBaseUrlInput').value = e.baseUrl || '';
|
|
682
|
+
document.getElementById('engineModelInput').value = e.model || '';
|
|
683
|
+
document.getElementById('engineModelHint').textContent = (e.models && e.models.length)
|
|
684
|
+
? '从 ' + e.models.length + ' 个候选模型里筛选,或手动输入'
|
|
685
|
+
: '手动输入模型名';
|
|
686
|
+
|
|
687
|
+
document.getElementById('engineImportResult').style.display = 'none';
|
|
688
|
+
document.getElementById('engineModal').style.display = 'flex';
|
|
689
|
+
// 准备模型筛选下拉
|
|
690
|
+
renderEngineModelList('');
|
|
691
|
+
setTimeout(() => document.getElementById('engineModelInput').focus(), 50);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function closeEngineModal() {
|
|
695
|
+
document.getElementById('engineModal').style.display = 'none';
|
|
696
|
+
currentEngine = null;
|
|
697
|
+
document.getElementById('engineModelList').style.display = 'none';
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// 模型筛选下拉
|
|
701
|
+
function renderEngineModelList(filter) {
|
|
702
|
+
const listEl = document.getElementById('engineModelList');
|
|
703
|
+
if (!currentEngine || !currentEngine.models) { listEl.style.display = 'none'; return; }
|
|
704
|
+
const f = (filter || '').toLowerCase();
|
|
705
|
+
const matched = currentEngine.models.filter(m => m.toLowerCase().includes(f));
|
|
706
|
+
if (matched.length === 0) { listEl.style.display = 'none'; return; }
|
|
707
|
+
listEl.innerHTML = matched.map(m => `<div class="combobox-option" onclick="selectEngineModel('${m.replace(/'/g, "\\'")}')">${m}</div>`).join('');
|
|
708
|
+
listEl.style.display = 'block';
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function selectEngineModel(model) {
|
|
712
|
+
document.getElementById('engineModelInput').value = model;
|
|
713
|
+
document.getElementById('engineModelList').style.display = 'none';
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
document.getElementById('engineModelInput').addEventListener('input', (ev) => {
|
|
717
|
+
renderEngineModelList(ev.target.value);
|
|
718
|
+
});
|
|
719
|
+
document.getElementById('engineModelInput').addEventListener('focus', (ev) => {
|
|
720
|
+
renderEngineModelList(ev.target.value);
|
|
721
|
+
});
|
|
722
|
+
// 点击空白关闭下拉
|
|
723
|
+
document.addEventListener('click', (ev) => {
|
|
724
|
+
const cb = document.getElementById('engineModelCombobox');
|
|
725
|
+
if (cb && !cb.contains(ev.target)) {
|
|
726
|
+
document.getElementById('engineModelList').style.display = 'none';
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
document.getElementById('engineModal').addEventListener('click', (ev) => {
|
|
730
|
+
if (ev.target === document.getElementById('engineModal')) closeEngineModal();
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
async function importEngine() {
|
|
734
|
+
if (!currentEngine) return;
|
|
735
|
+
const btn = document.getElementById('engineImportBtn');
|
|
736
|
+
const result = document.getElementById('engineImportResult');
|
|
737
|
+
btn.disabled = true;
|
|
738
|
+
requestAnimationFrame(() => { btn.textContent = '⚡ 导入中...'; });
|
|
739
|
+
result.style.display = 'none';
|
|
740
|
+
|
|
741
|
+
const body = {
|
|
742
|
+
id: currentEngine.id,
|
|
743
|
+
model: document.getElementById('engineModelInput').value.trim(),
|
|
744
|
+
provider: document.getElementById('engineProviderInput').value,
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
const resp = await fetch('/api/external-engines/import', {
|
|
749
|
+
method: 'POST',
|
|
750
|
+
headers: { 'Content-Type': 'application/json' },
|
|
751
|
+
body: JSON.stringify(body),
|
|
752
|
+
});
|
|
753
|
+
const data = await resp.json();
|
|
754
|
+
if (!resp.ok) throw new Error(data.error || '导入失败');
|
|
755
|
+
result.className = 'test-result test-success';
|
|
756
|
+
result.innerHTML = '<span>✓</span> 已导入为供应商: ' + (data.provider || '') +
|
|
757
|
+
(data.autoActivated ? '(已激活)' : '') + ',可在「LLM 对话」tab 选用';
|
|
758
|
+
result.style.display = 'flex';
|
|
759
|
+
// 刷新 LLM 配置 + 引擎列表
|
|
760
|
+
await Promise.all([loadLLMConfig(), loadEngines()]);
|
|
761
|
+
} catch (err) {
|
|
762
|
+
result.className = 'test-result test-error';
|
|
763
|
+
result.innerHTML = '<span>✗</span> ' + (err.message || '导入失败');
|
|
764
|
+
result.style.display = 'flex';
|
|
765
|
+
}
|
|
766
|
+
btn.disabled = false;
|
|
767
|
+
requestAnimationFrame(() => { btn.textContent = '⚡ 导入为供应商'; });
|
|
768
|
+
}
|
|
769
|
+
|
|
521
770
|
// ==================== 关闭弹窗 ====================
|
|
522
771
|
document.getElementById('configModal').onclick = function(e) {
|
|
523
772
|
if (e.target === this) closeModal();
|
package/dist/web/client.js
CHANGED
|
@@ -3438,8 +3438,16 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3438
3438
|
});
|
|
3439
3439
|
renderJudgments(lastJudgmentsCache);
|
|
3440
3440
|
}
|
|
3441
|
+
function switchPolarity(polarity) {
|
|
3442
|
+
currentPolarity = polarity;
|
|
3443
|
+
currentAdvancedFilter = null;
|
|
3444
|
+
document.querySelectorAll(".judgment-polarity-tab").forEach((btn) => {
|
|
3445
|
+
btn.classList.toggle("active", btn.dataset.polarity === polarity);
|
|
3446
|
+
});
|
|
3447
|
+
loadJudgments();
|
|
3448
|
+
}
|
|
3441
3449
|
function switchStatusFilter(status) {
|
|
3442
|
-
|
|
3450
|
+
currentAdvancedFilter = status;
|
|
3443
3451
|
document.querySelectorAll(".judgment-status-tab").forEach((btn) => {
|
|
3444
3452
|
btn.classList.toggle("active", btn.dataset.status === status);
|
|
3445
3453
|
});
|
|
@@ -3464,7 +3472,8 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3464
3472
|
if (judgmentsModal) judgmentsModal.classList.remove("active");
|
|
3465
3473
|
}
|
|
3466
3474
|
var currentJudgmentTab = "channel";
|
|
3467
|
-
var
|
|
3475
|
+
var currentPolarity = "positive";
|
|
3476
|
+
var currentAdvancedFilter = null;
|
|
3468
3477
|
var lastJudgmentsCache = [];
|
|
3469
3478
|
function renderJudgments(items) {
|
|
3470
3479
|
if (!judgmentsList) return;
|
|
@@ -3554,7 +3563,7 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3554
3563
|
async function loadJudgments() {
|
|
3555
3564
|
if (!judgmentsList) return;
|
|
3556
3565
|
try {
|
|
3557
|
-
if (
|
|
3566
|
+
if (currentAdvancedFilter === "violations") {
|
|
3558
3567
|
const res2 = await fetch("/api/judgments/violations?limit=50");
|
|
3559
3568
|
if (!res2.ok) throw new Error("HTTP " + res2.status);
|
|
3560
3569
|
const data2 = await res2.json();
|
|
@@ -3562,7 +3571,7 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3562
3571
|
judgmentsLoaded = true;
|
|
3563
3572
|
return;
|
|
3564
3573
|
}
|
|
3565
|
-
if (
|
|
3574
|
+
if (currentAdvancedFilter === "adaptive") {
|
|
3566
3575
|
const res2 = await fetch("/api/judgments/adaptive-suggestions");
|
|
3567
3576
|
if (!res2.ok) throw new Error("HTTP " + res2.status);
|
|
3568
3577
|
const data2 = await res2.json();
|
|
@@ -3570,7 +3579,7 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3570
3579
|
judgmentsLoaded = true;
|
|
3571
3580
|
return;
|
|
3572
3581
|
}
|
|
3573
|
-
if (
|
|
3582
|
+
if (currentAdvancedFilter === "causal") {
|
|
3574
3583
|
const res2 = await fetch("/api/judgments/causal/correlation?topN=10");
|
|
3575
3584
|
if (!res2.ok) throw new Error("HTTP " + res2.status);
|
|
3576
3585
|
const data2 = await res2.json();
|
|
@@ -3578,17 +3587,25 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3578
3587
|
judgmentsLoaded = true;
|
|
3579
3588
|
return;
|
|
3580
3589
|
}
|
|
3581
|
-
const
|
|
3590
|
+
const POSITIVE_TYPES = ["approve", "modify", "escalate"];
|
|
3591
|
+
const fetchStatus = currentPolarity === "positive" ? "active" : "all";
|
|
3592
|
+
const res = await fetch("/api/judgments?status=" + encodeURIComponent(fetchStatus));
|
|
3582
3593
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
3583
3594
|
const data = await res.json();
|
|
3584
|
-
|
|
3585
|
-
|
|
3595
|
+
let list = data.judgments || [];
|
|
3596
|
+
if (currentPolarity === "positive") {
|
|
3597
|
+
list = list.filter((j) => POSITIVE_TYPES.includes(j.decision_type) && (j.status ?? "active") === "active");
|
|
3598
|
+
} else {
|
|
3599
|
+
list = list.filter((j) => j.decision_type === "reject" || ["rejected", "superseded"].includes(j.status ?? ""));
|
|
3600
|
+
}
|
|
3601
|
+
lastJudgmentsCache = list;
|
|
3602
|
+
renderJudgments(list);
|
|
3586
3603
|
if (judgmentsBadge) {
|
|
3587
3604
|
let activeCount;
|
|
3588
|
-
if (
|
|
3605
|
+
if (currentPolarity === "positive") {
|
|
3589
3606
|
activeCount = data.count;
|
|
3590
3607
|
} else {
|
|
3591
|
-
activeCount =
|
|
3608
|
+
activeCount = (data.judgments || []).filter((j) => POSITIVE_TYPES.includes(j.decision_type) && (j.status ?? "active") === "active").length;
|
|
3592
3609
|
}
|
|
3593
3610
|
if (activeCount > 0) {
|
|
3594
3611
|
judgmentsBadge.textContent = activeCount;
|
|
@@ -3863,6 +3880,9 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3863
3880
|
document.querySelectorAll(".judgment-tab").forEach((btn) => {
|
|
3864
3881
|
btn.addEventListener("click", () => switchJudgmentTab(btn.dataset.tab));
|
|
3865
3882
|
});
|
|
3883
|
+
document.querySelectorAll(".judgment-polarity-tab").forEach((btn) => {
|
|
3884
|
+
btn.addEventListener("click", () => switchPolarity(btn.dataset.polarity));
|
|
3885
|
+
});
|
|
3866
3886
|
document.querySelectorAll(".judgment-status-tab").forEach((btn) => {
|
|
3867
3887
|
btn.addEventListener("click", () => switchStatusFilter(btn.dataset.status));
|
|
3868
3888
|
});
|
|
@@ -3980,12 +4000,14 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3980
4000
|
judgmentSubmitBtn.disabled = true;
|
|
3981
4001
|
if (judgmentError) judgmentError.style.display = "none";
|
|
3982
4002
|
try {
|
|
4003
|
+
const polarity = document.querySelector('input[name="judgment-polarity"]:checked')?.value || "positive";
|
|
3983
4004
|
const res = await fetch("/api/judgments", {
|
|
3984
4005
|
method: "POST",
|
|
3985
4006
|
headers: { "Content-Type": "application/json" },
|
|
3986
4007
|
body: JSON.stringify({
|
|
3987
4008
|
decision,
|
|
3988
4009
|
reason: reason || void 0,
|
|
4010
|
+
decision_type: polarity === "negative" ? "reject" : "approve",
|
|
3989
4011
|
context: { domain: judgmentDomain?.value, stakes: judgmentStakes?.value }
|
|
3990
4012
|
})
|
|
3991
4013
|
});
|
package/dist/web/index.html
CHANGED
|
@@ -205,21 +205,39 @@
|
|
|
205
205
|
<input type="text" id="judgment-reason" placeholder="例: 信任 Bolloon 的判断存储">
|
|
206
206
|
</div>
|
|
207
207
|
<div class="form-group judgment-form-row">
|
|
208
|
-
<label
|
|
209
|
-
<
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<
|
|
221
|
-
<
|
|
222
|
-
|
|
208
|
+
<label>分类</label>
|
|
209
|
+
<div class="judgment-polarity-toggle" id="judgment-polarity-toggle">
|
|
210
|
+
<label class="polarity-opt active" data-polarity="positive">
|
|
211
|
+
<input type="radio" name="judgment-polarity" value="positive" checked>
|
|
212
|
+
<span>▲ 正向 <small>采纳</small></span>
|
|
213
|
+
</label>
|
|
214
|
+
<label class="polarity-opt" data-polarity="negative">
|
|
215
|
+
<input type="radio" name="judgment-polarity" value="negative">
|
|
216
|
+
<span>▼ 负向 <small>否决/避免</small></span>
|
|
217
|
+
</label>
|
|
218
|
+
</div>
|
|
219
|
+
<details class="judgment-advanced-fold" style="margin-left:auto;">
|
|
220
|
+
<summary style="font-size:11px;color:#6b7280;cursor:pointer;list-style:none;">高级 (领域/风险)</summary>
|
|
221
|
+
<div style="display:flex;gap:10px;padding:6px 0 0;flex-wrap:wrap;font-size:11px;">
|
|
222
|
+
<label>领域
|
|
223
|
+
<select id="judgment-domain">
|
|
224
|
+
<option value="general">general</option>
|
|
225
|
+
<option value="code">code</option>
|
|
226
|
+
<option value="architecture">architecture</option>
|
|
227
|
+
<option value="security">security</option>
|
|
228
|
+
<option value="testing">testing</option>
|
|
229
|
+
</select>
|
|
230
|
+
</label>
|
|
231
|
+
<label>风险
|
|
232
|
+
<select id="judgment-stakes">
|
|
233
|
+
<option value="medium">medium</option>
|
|
234
|
+
<option value="low">low</option>
|
|
235
|
+
<option value="high">high</option>
|
|
236
|
+
<option value="critical">critical</option>
|
|
237
|
+
</select>
|
|
238
|
+
</label>
|
|
239
|
+
</div>
|
|
240
|
+
</details>
|
|
223
241
|
</div>
|
|
224
242
|
<div class="btn-group">
|
|
225
243
|
<button id="judgment-submit-btn" class="btn-primary">记录</button>
|
|
@@ -235,15 +253,21 @@
|
|
|
235
253
|
</button>
|
|
236
254
|
<button class="judgment-tab" data-tab="global">全局</button>
|
|
237
255
|
</div>
|
|
238
|
-
<!--
|
|
256
|
+
<!-- 2026-07-22 简化: 正向 / 负向 两个主分类 (替换原 6 个 status tab)
|
|
257
|
+
正向 = 采纳类 (approve/modify/escalate), 会注入 prompt 复用
|
|
258
|
+
负向 = 否决/被推翻 (reject/rejected/superseded), 负向回收为避免清单
|
|
259
|
+
高级分析 (违规/自适应/因果) 折叠保留, 数据/API 不删 -->
|
|
239
260
|
<div id="judgments-status-filter" class="judgment-status-bar">
|
|
240
|
-
<
|
|
241
|
-
<button class="judgment-
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
261
|
+
<button class="judgment-polarity-tab active" data-polarity="positive" title="采纳类: approve/modify/escalate, 会注入 prompt 复用">▲ 正向</button>
|
|
262
|
+
<button class="judgment-polarity-tab" data-polarity="negative" title="否决/被推翻: reject/rejected/superseded, 负向回收为避免清单">▼ 负向</button>
|
|
263
|
+
<details class="judgment-advanced-fold" style="margin-left:auto;">
|
|
264
|
+
<summary style="font-size:11px;color:#6b7280;cursor:pointer;list-style:none;">⋯ 高级分析</summary>
|
|
265
|
+
<div style="display:flex;gap:4px;padding:6px 0 0;flex-wrap:wrap;">
|
|
266
|
+
<button class="judgment-status-tab" data-status="violations" style="font-size:11px;">违规记录</button>
|
|
267
|
+
<button class="judgment-status-tab" data-status="adaptive" style="font-size:11px;">📊 自适应</button>
|
|
268
|
+
<button class="judgment-status-tab" data-status="causal" style="font-size:11px;">🔍 因果分析</button>
|
|
269
|
+
</div>
|
|
270
|
+
</details>
|
|
247
271
|
</div>
|
|
248
272
|
<h3 class="judgment-list-header">
|
|
249
273
|
<span id="judgments-list-title">本 channel 的判断力</span>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routes-external-engines.ts — 外部编码智能体 配置/委派 路由
|
|
3
|
+
*
|
|
4
|
+
* 三个能力:
|
|
5
|
+
* GET /api/external-engines 发现本机已装的引擎 (脱敏)
|
|
6
|
+
* POST /api/external-engines/import 把发现的引擎 API 写进 Bolloon provider 体系 (当供应商)
|
|
7
|
+
* POST /api/external-engines/run 委派编码任务给引擎 CLI (子智能体)
|
|
8
|
+
*
|
|
9
|
+
* 复用 routes-llm-config.ts 的 llmConfigStore + initMinimax 做激活.
|
|
10
|
+
*/
|
|
11
|
+
import { discoverEngines, mapEngineToProviderConfig, resolveProvider } from '../external-engines/index.js';
|
|
12
|
+
import { llmConfigStore } from '../llm/config-store.js';
|
|
13
|
+
import { initMinimax } from '../constraints/index.js';
|
|
14
|
+
function maskKey(key) {
|
|
15
|
+
if (!key)
|
|
16
|
+
return '';
|
|
17
|
+
if (key.length <= 4)
|
|
18
|
+
return '***';
|
|
19
|
+
return '***' + key.slice(-4);
|
|
20
|
+
}
|
|
21
|
+
export function registerExternalEngineRoutes(app) {
|
|
22
|
+
// ==================== 发现 ====================
|
|
23
|
+
app.get('/api/external-engines', async (_req, res) => {
|
|
24
|
+
try {
|
|
25
|
+
const engines = await discoverEngines();
|
|
26
|
+
const safe = engines.map((e) => ({
|
|
27
|
+
...e,
|
|
28
|
+
apiKey: maskKey(e.apiKey),
|
|
29
|
+
}));
|
|
30
|
+
res.json({ engines: safe });
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
res.status(500).json({ error: err?.message || String(err) });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
// ==================== 导入为供应商 ====================
|
|
37
|
+
app.post('/api/external-engines/import', async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const { id, model, provider: providerOverride } = req.body || {};
|
|
40
|
+
if (!id)
|
|
41
|
+
return res.status(400).json({ error: 'id 必填' });
|
|
42
|
+
const engines = await discoverEngines();
|
|
43
|
+
const engine = engines.find((e) => e.id === id);
|
|
44
|
+
if (!engine)
|
|
45
|
+
return res.status(404).json({ error: `未发现的引擎: ${id}` });
|
|
46
|
+
if (!engine.configured) {
|
|
47
|
+
return res.status(400).json({ error: `引擎 ${id} 未配置 API key / baseUrl, 无法导入为供应商` });
|
|
48
|
+
}
|
|
49
|
+
// 允许前端在导入时覆盖 model / provider (API 配置 UI 里用户筛选模型后传来)
|
|
50
|
+
if (model && typeof model === 'string' && model.trim()) {
|
|
51
|
+
engine.model = model.trim();
|
|
52
|
+
}
|
|
53
|
+
if (providerOverride && typeof providerOverride === 'string' && providerOverride.trim()) {
|
|
54
|
+
const resolved = resolveProvider(providerOverride.trim(), engine.provider || 'openai');
|
|
55
|
+
engine.provider = resolved;
|
|
56
|
+
}
|
|
57
|
+
let importPatch;
|
|
58
|
+
try {
|
|
59
|
+
importPatch = mapEngineToProviderConfig(engine);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return res.status(400).json({ error: e.message });
|
|
63
|
+
}
|
|
64
|
+
const { provider, patch } = importPatch;
|
|
65
|
+
await llmConfigStore.updateProvider(provider, patch);
|
|
66
|
+
// 导入即激活 (若提供了有效 key, 或不需要 key 的 provider)
|
|
67
|
+
const shouldActivate = !!patch.apiKey || !engine.apiKey;
|
|
68
|
+
let autoActivated = false;
|
|
69
|
+
if (shouldActivate) {
|
|
70
|
+
try {
|
|
71
|
+
await llmConfigStore.setActiveProvider(provider);
|
|
72
|
+
initMinimax({
|
|
73
|
+
provider: provider,
|
|
74
|
+
apiKey: patch.apiKey || undefined,
|
|
75
|
+
baseUrl: patch.baseUrl || undefined,
|
|
76
|
+
model: patch.model || undefined,
|
|
77
|
+
});
|
|
78
|
+
autoActivated = true;
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
// 激活失败不阻断导入 (可能该 provider 仍需别的字段)
|
|
82
|
+
console.warn('[external-engines] 激活失败:', e?.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
res.json({ ok: true, provider, autoActivated, imported: { ...patch, apiKey: maskKey(patch.apiKey) } });
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
res.status(500).json({ error: err?.message || String(err) });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// ==================== 委派执行 ====================
|
|
92
|
+
app.post('/api/external-engines/run', async (req, res) => {
|
|
93
|
+
try {
|
|
94
|
+
const { id, prompt, cwd, model } = req.body || {};
|
|
95
|
+
if (!id)
|
|
96
|
+
return res.status(400).json({ error: 'id 必填' });
|
|
97
|
+
if (!prompt)
|
|
98
|
+
return res.status(400).json({ error: 'prompt 必填' });
|
|
99
|
+
// 动态 import 避免顶层循环依赖 (delegate -> discovery, 这里反向)
|
|
100
|
+
const { delegateToEngine } = await import('../external-engines/delegate.js');
|
|
101
|
+
const result = await delegateToEngine(id, prompt, {
|
|
102
|
+
cwd,
|
|
103
|
+
...(model ? { model: String(model) } : {}),
|
|
104
|
+
});
|
|
105
|
+
res.json(result);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
res.status(500).json({ error: err?.message || String(err) });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -36,15 +36,21 @@ export function registerJudgmentsRoutes(app) {
|
|
|
36
36
|
// 极简版: 只记录 decision + reason; 其它字段可选
|
|
37
37
|
app.post('/api/judgments', async (req, res) => {
|
|
38
38
|
try {
|
|
39
|
-
const { decision, reason, context } = req.body;
|
|
39
|
+
const { decision, reason, context, decision_type } = req.body;
|
|
40
40
|
if (!decision || typeof decision !== 'string' || !decision.trim()) {
|
|
41
41
|
return res.status(400).json({ error: 'decision required' });
|
|
42
42
|
}
|
|
43
|
+
// 2026-07-22: 接受前端 polarity toggle 传来的 decision_type (正=approve / 负=reject)
|
|
44
|
+
// 不传或非法 → 默认 approve (向后兼容)
|
|
45
|
+
const allowedTypes = ['approve', 'reject', 'modify', 'escalate'];
|
|
46
|
+
const finalType = decision_type && allowedTypes.includes(decision_type)
|
|
47
|
+
? decision_type
|
|
48
|
+
: 'approve';
|
|
43
49
|
const { storeHumanJudgment, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
44
50
|
await initializeValueStore();
|
|
45
51
|
const j = await storeHumanJudgment({
|
|
46
52
|
decision: decision.trim(),
|
|
47
|
-
decision_type:
|
|
53
|
+
decision_type: finalType,
|
|
48
54
|
reasons: reason ? [reason.trim()] : [],
|
|
49
55
|
values_derived: [],
|
|
50
56
|
context: {
|