@winspan/claude-forge 8.15.0 → 8.16.0
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/official-agents.d.ts.map +1 -1
- package/dist/agents/official-agents.js +12 -24
- package/dist/agents/official-agents.js.map +1 -1
- package/dist/cli/commands/menu.js +183 -0
- package/dist/cli/commands/menu.js.map +1 -1
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.js +1 -1
- package/dist/core/constants.js.map +1 -1
- package/dist/daemon/handlers/user-prompt.d.ts +3 -5
- package/dist/daemon/handlers/user-prompt.d.ts.map +1 -1
- package/dist/daemon/handlers/user-prompt.js +8 -17
- package/dist/daemon/handlers/user-prompt.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +12 -7
- package/dist/daemon/index.js.map +1 -1
- package/dist/engine/conventions/routing.yaml +13 -3
- package/dist/engine/dsl/parser.d.ts +6 -0
- package/dist/engine/dsl/parser.d.ts.map +1 -1
- package/dist/engine/dsl/parser.js +19 -0
- package/dist/engine/dsl/parser.js.map +1 -1
- package/dist/intelligence/classifier.d.ts +1 -1
- package/dist/intelligence/classifier.d.ts.map +1 -1
- package/dist/intelligence/classifier.js +26 -18
- package/dist/intelligence/classifier.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +77 -0
- package/dist/web/server.js.map +1 -1
- package/dist/web/static/index.html +197 -97
- package/package.json +1 -1
|
@@ -353,19 +353,15 @@
|
|
|
353
353
|
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
|
354
354
|
注入
|
|
355
355
|
</a>
|
|
356
|
-
<a onclick="nav('live')" id="nav-live">
|
|
357
|
-
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="4"/></svg>
|
|
358
|
-
实时
|
|
359
|
-
</a>
|
|
360
356
|
<div class="nav-section-title">配置</div>
|
|
357
|
+
<a onclick="nav('ai-config')" id="nav-ai-config">
|
|
358
|
+
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6M5.6 5.6l4.2 4.2m4.2 4.2l4.2 4.2M1 12h6m6 0h6M5.6 18.4l4.2-4.2m4.2-4.2l4.2-4.2"/></svg>
|
|
359
|
+
AI 配置
|
|
360
|
+
</a>
|
|
361
361
|
<a onclick="nav('routing')" id="nav-routing">
|
|
362
362
|
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M9 6h9M6 9v9"/></svg>
|
|
363
363
|
Agent 路由
|
|
364
364
|
</a>
|
|
365
|
-
<a onclick="nav('rules')" id="nav-rules">
|
|
366
|
-
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
|
|
367
|
-
规则
|
|
368
|
-
</a>
|
|
369
365
|
</aside>
|
|
370
366
|
|
|
371
367
|
<!-- Main -->
|
|
@@ -459,23 +455,50 @@
|
|
|
459
455
|
</div>
|
|
460
456
|
</div>
|
|
461
457
|
|
|
462
|
-
<!--
|
|
463
|
-
<div id="page-
|
|
464
|
-
<div class="
|
|
465
|
-
<
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
458
|
+
<!-- AI Config -->
|
|
459
|
+
<div id="page-ai-config" class="page">
|
|
460
|
+
<div class="panel">
|
|
461
|
+
<div class="panel-header">
|
|
462
|
+
<span class="panel-title">AI 配置</span>
|
|
463
|
+
<div style="display:flex;gap:0.5rem">
|
|
464
|
+
<button class="btn" onclick="testAIConnection()">🔌 测试连接</button>
|
|
465
|
+
<button class="btn btn-primary" onclick="saveAIConfig()">💾 保存</button>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
<div class="panel-body">
|
|
469
|
+
<div style="max-width:600px">
|
|
470
|
+
<div style="margin-bottom:1rem">
|
|
471
|
+
<label style="display:block;margin-bottom:0.25rem;font-weight:500;font-size:0.875rem">API Key</label>
|
|
472
|
+
<div style="display:flex;gap:0.5rem">
|
|
473
|
+
<input type="password" id="ai-api-key" class="search-box" style="flex:1" placeholder="sk-...">
|
|
474
|
+
<button class="btn" onclick="toggleAPIKeyVisibility()" id="toggle-api-key-btn">👁️ 显示</button>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
<div style="margin-bottom:1rem">
|
|
478
|
+
<label style="display:block;margin-bottom:0.25rem;font-weight:500;font-size:0.875rem">Base URL</label>
|
|
479
|
+
<input type="text" id="ai-base-url" class="search-box" placeholder="https://api.anthropic.com">
|
|
480
|
+
</div>
|
|
481
|
+
<div style="margin-bottom:1rem">
|
|
482
|
+
<label style="display:block;margin-bottom:0.25rem;font-weight:500;font-size:0.875rem">Provider</label>
|
|
483
|
+
<input type="text" id="ai-provider" class="search-box" value="anthropic" readonly style="background:var(--bg-secondary)">
|
|
484
|
+
</div>
|
|
485
|
+
<div style="margin-bottom:1rem">
|
|
486
|
+
<label style="display:block;margin-bottom:0.25rem;font-weight:500;font-size:0.875rem">Model</label>
|
|
487
|
+
<div style="display:flex;gap:0.5rem">
|
|
488
|
+
<select id="ai-model" class="btn" style="flex:1">
|
|
489
|
+
<option value="">加载中...</option>
|
|
490
|
+
</select>
|
|
491
|
+
<button class="btn" onclick="refreshAIModels()">↻ 刷新模型列表</button>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
<div id="ai-config-success" style="margin-top:1rem;padding:0.75rem;background:var(--primary);color:white;border-radius:var(--radius-sm);display:none"></div>
|
|
495
|
+
<div id="ai-config-error" style="margin-top:1rem;padding:0.75rem;background:var(--red);color:white;border-radius:var(--radius-sm);display:none"></div>
|
|
496
|
+
<div style="margin-top:1rem;padding:0.75rem;background:var(--bg-secondary);border-radius:var(--radius-sm);font-size:0.875rem;color:var(--text-muted)">
|
|
497
|
+
<strong>提示:</strong>配置保存后需要重启 daemon 才能生效。<br>
|
|
498
|
+
运行 <code style="background:var(--bg-card);padding:0.125rem 0.375rem;border-radius:4px">cf daemon stop && cf daemon start</code>
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
479
502
|
</div>
|
|
480
503
|
</div>
|
|
481
504
|
|
|
@@ -518,9 +541,10 @@
|
|
|
518
541
|
<div class="toolbar" style="display:flex;gap:0.5rem">
|
|
519
542
|
<select class="btn" id="routing-filter-obeyed" onchange="loadRoutingEvents()">
|
|
520
543
|
<option value="">全部状态</option>
|
|
544
|
+
<option value="forced">已强路由</option>
|
|
521
545
|
<option value="1">遵守 (obeyed)</option>
|
|
522
546
|
<option value="0">违抗 (refused)</option>
|
|
523
|
-
<option value="null"
|
|
547
|
+
<option value="null">待判定/未路由</option>
|
|
524
548
|
</select>
|
|
525
549
|
<input class="search-box" id="routing-filter-agent" placeholder="按 agent 名过滤..." oninput="loadRoutingEvents()">
|
|
526
550
|
</div>
|
|
@@ -646,8 +670,7 @@
|
|
|
646
670
|
|
|
647
671
|
<script>
|
|
648
672
|
const API = '';
|
|
649
|
-
let allEvents = [], allSessions = [], allInjections = []
|
|
650
|
-
let liveSource = null;
|
|
673
|
+
let allEvents = [], allSessions = [], allInjections = [];
|
|
651
674
|
let charts = {};
|
|
652
675
|
|
|
653
676
|
// === Navigation ===
|
|
@@ -658,16 +681,15 @@ function nav(page) {
|
|
|
658
681
|
const pageEl = document.getElementById('page-' + page);
|
|
659
682
|
if (navEl) navEl.classList.add('active');
|
|
660
683
|
if (pageEl) pageEl.classList.add('active');
|
|
661
|
-
const titles = { dashboard:'仪表盘', sessions:'会话', events:'事件', injections:'注入',
|
|
684
|
+
const titles = { dashboard:'仪表盘', sessions:'会话', events:'事件', injections:'注入', 'ai-config':'AI 配置', routing:'Agent 路由' };
|
|
662
685
|
document.getElementById('topbar-title').textContent = titles[page] || page;
|
|
663
686
|
closeDrawer();
|
|
664
687
|
if (page === 'dashboard') loadDashboard();
|
|
665
688
|
else if (page === 'sessions') loadSessions();
|
|
666
689
|
else if (page === 'events') loadEvents();
|
|
667
690
|
else if (page === 'injections') loadInjections();
|
|
668
|
-
else if (page === '
|
|
691
|
+
else if (page === 'ai-config') loadAIConfig();
|
|
669
692
|
else if (page === 'routing') loadRouting();
|
|
670
|
-
else if (page === 'live' && !liveSource) toggleLive();
|
|
671
693
|
}
|
|
672
694
|
|
|
673
695
|
function refreshPage() {
|
|
@@ -801,41 +823,6 @@ function renderToolChart(data) {
|
|
|
801
823
|
});
|
|
802
824
|
}
|
|
803
825
|
|
|
804
|
-
// === Live ===
|
|
805
|
-
function toggleLive() {
|
|
806
|
-
if (liveSource) {
|
|
807
|
-
liveSource.close(); liveSource = null;
|
|
808
|
-
document.getElementById('live-status').textContent = '未连接';
|
|
809
|
-
document.getElementById('live-status').className = 'badge badge-warn';
|
|
810
|
-
document.getElementById('live-btn').textContent = '连接';
|
|
811
|
-
document.getElementById('live-btn').className = 'btn btn-primary';
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
liveSource = new EventSource(API + '/api/events/stream');
|
|
815
|
-
document.getElementById('live-status').textContent = '实时中';
|
|
816
|
-
document.getElementById('live-status').className = 'badge badge-live';
|
|
817
|
-
document.getElementById('live-btn').textContent = '断开连接';
|
|
818
|
-
document.getElementById('live-btn').className = 'btn';
|
|
819
|
-
liveSource.onmessage = function(ev) {
|
|
820
|
-
try {
|
|
821
|
-
const e = JSON.parse(ev.data);
|
|
822
|
-
if (e.type === 'connected') return;
|
|
823
|
-
const log = document.getElementById('live-log');
|
|
824
|
-
const line = document.createElement('div');
|
|
825
|
-
line.className = 'live-log-line';
|
|
826
|
-
const tool = e.tool_name ? `<span style="color:#93c5fd">${e.tool_name}</span>` : '';
|
|
827
|
-
const detail = (e.user_prompt || e.tool_input?.command || e.tool_input?.file_path || '').toString().slice(0, 60);
|
|
828
|
-
line.innerHTML = `<span style="color:#64748b">${fmtTime(e.timestamp)}</span> ${badgeHook(e.hook_type)} ${tool} <span style="color:#cbd5e1">${detail}</span>`;
|
|
829
|
-
log.appendChild(line);
|
|
830
|
-
log.scrollTop = log.scrollHeight;
|
|
831
|
-
} catch {}
|
|
832
|
-
};
|
|
833
|
-
liveSource.onerror = function() {
|
|
834
|
-
document.getElementById('live-status').textContent = '连接错误';
|
|
835
|
-
document.getElementById('live-status').className = 'badge badge-block';
|
|
836
|
-
};
|
|
837
|
-
}
|
|
838
|
-
|
|
839
826
|
// === Sessions ===
|
|
840
827
|
function sessionListItem(s) {
|
|
841
828
|
const prompt = (s.first_prompt || '(无提示词)').slice(0, 60);
|
|
@@ -1085,32 +1072,6 @@ function openInjDrawer(inj) {
|
|
|
1085
1072
|
}
|
|
1086
1073
|
|
|
1087
1074
|
// === Rules ===
|
|
1088
|
-
async function loadRules() {
|
|
1089
|
-
document.getElementById('rules-list').innerHTML = loading();
|
|
1090
|
-
try {
|
|
1091
|
-
const res = await fetch(API + '/api/rules');
|
|
1092
|
-
allRules = await res.json();
|
|
1093
|
-
renderRules(allRules);
|
|
1094
|
-
} catch {
|
|
1095
|
-
document.getElementById('rules-list').innerHTML = empty('加载规则失败');
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
function renderRules(list) {
|
|
1100
|
-
document.getElementById('rules-list').innerHTML = list.length === 0 ? empty('未找到规则') : list.map(r => '<div class="list-item fade-in" onclick="openRuleDrawer(' + JSON.stringify(r).replace(/"/g,'"') + ')"><div style="flex:1;min-width:0"><div style="font-size:0.875rem;font-weight:500;margin-bottom:2px">' + (r.name || r.id) + '</div><div style="font-size:0.75rem;color:var(--text-dim)">' + (r.description || '') + '</div></div><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-dim);flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg></div>').join('');
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
function filterRules() {
|
|
1104
|
-
const q = document.getElementById('rules-search').value.toLowerCase();
|
|
1105
|
-
renderRules(allRules.filter(r => (r.name||'').toLowerCase().includes(q) || (r.description||'').toLowerCase().includes(q)));
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
function openRuleDrawer(r) {
|
|
1109
|
-
let html = '<div class="detail-section"><div class="detail-label">名称</div><div class="detail-value" style="font-weight:600">' + (r.name || '—') + '</div></div>';
|
|
1110
|
-
html += '<div class="detail-section"><div class="detail-label">描述</div><div class="detail-value">' + (r.description || '—') + '</div></div>';
|
|
1111
|
-
if (r.stats) html += '<div class="detail-section"><div class="detail-label">统计</div><div class="detail-value">触发 ' + (r.stats.totalTriggers || 0) + ' 次 | 阻断 ' + (r.stats.blockCount || 0) + ' 次 | 警告 ' + (r.stats.warnCount || 0) + ' 次</div></div>';
|
|
1112
|
-
openDrawer(r.name || '规则', html);
|
|
1113
|
-
}
|
|
1114
1075
|
|
|
1115
1076
|
// === Agent Routing ===
|
|
1116
1077
|
let routingCurrentTab = 'overview';
|
|
@@ -1146,6 +1107,136 @@ async function loadRouting() {
|
|
|
1146
1107
|
if (routingCurrentTab === 'recommendations') return loadRecommendations();
|
|
1147
1108
|
}
|
|
1148
1109
|
|
|
1110
|
+
// === AI Config ===
|
|
1111
|
+
let aiApiKeyMasked = true;
|
|
1112
|
+
let aiApiKeyOriginal = '';
|
|
1113
|
+
|
|
1114
|
+
async function loadAIConfig() {
|
|
1115
|
+
try {
|
|
1116
|
+
const r = await fetch(API + '/api/config/ai');
|
|
1117
|
+
if (!r.ok) throw new Error('Failed to load config');
|
|
1118
|
+
const data = await r.json();
|
|
1119
|
+
|
|
1120
|
+
document.getElementById('ai-api-key').value = data.api_key || '';
|
|
1121
|
+
document.getElementById('ai-base-url').value = data.base_url || '';
|
|
1122
|
+
document.getElementById('ai-provider').value = data.provider || 'anthropic';
|
|
1123
|
+
aiApiKeyOriginal = data.api_key || '';
|
|
1124
|
+
aiApiKeyMasked = true;
|
|
1125
|
+
document.getElementById('ai-api-key').type = 'password';
|
|
1126
|
+
document.getElementById('toggle-api-key-btn').textContent = '👁️ 显示';
|
|
1127
|
+
|
|
1128
|
+
// Pre-fill model dropdown with current model
|
|
1129
|
+
const modelSelect = document.getElementById('ai-model');
|
|
1130
|
+
modelSelect.innerHTML = `<option value="${data.model}">${data.model}</option>`;
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
showAIError(`加载配置失败: ${err.message}`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function toggleAPIKeyVisibility() {
|
|
1137
|
+
const input = document.getElementById('ai-api-key');
|
|
1138
|
+
const btn = document.getElementById('toggle-api-key-btn');
|
|
1139
|
+
if (input.type === 'password') {
|
|
1140
|
+
input.type = 'text';
|
|
1141
|
+
btn.textContent = '🙈 隐藏';
|
|
1142
|
+
} else {
|
|
1143
|
+
input.type = 'password';
|
|
1144
|
+
btn.textContent = '👁️ 显示';
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async function refreshAIModels() {
|
|
1149
|
+
const modelSelect = document.getElementById('ai-model');
|
|
1150
|
+
const currentValue = modelSelect.value;
|
|
1151
|
+
modelSelect.innerHTML = '<option value="">加载中...</option>';
|
|
1152
|
+
try {
|
|
1153
|
+
const r = await fetch(API + '/api/ai/models');
|
|
1154
|
+
if (!r.ok) {
|
|
1155
|
+
const err = await r.json().catch(() => ({ error: r.statusText }));
|
|
1156
|
+
throw new Error(err.error || r.statusText);
|
|
1157
|
+
}
|
|
1158
|
+
const data = await r.json();
|
|
1159
|
+
const models = data.data || [];
|
|
1160
|
+
if (models.length === 0) {
|
|
1161
|
+
modelSelect.innerHTML = '<option value="">无可用模型</option>';
|
|
1162
|
+
showAIError('未找到可用模型');
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
modelSelect.innerHTML = models.map(m => {
|
|
1166
|
+
const selected = m.id === currentValue ? 'selected' : '';
|
|
1167
|
+
return `<option value="${m.id}" ${selected}>${m.id}</option>`;
|
|
1168
|
+
}).join('');
|
|
1169
|
+
showAISuccess(`已加载 ${models.length} 个模型`);
|
|
1170
|
+
} catch (err) {
|
|
1171
|
+
modelSelect.innerHTML = `<option value="${currentValue}">${currentValue}</option>`;
|
|
1172
|
+
showAIError(`加载模型列表失败: ${err.message}`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
async function saveAIConfig() {
|
|
1177
|
+
const apiKeyInput = document.getElementById('ai-api-key').value;
|
|
1178
|
+
const baseUrl = document.getElementById('ai-base-url').value;
|
|
1179
|
+
const provider = document.getElementById('ai-provider').value;
|
|
1180
|
+
const model = document.getElementById('ai-model').value;
|
|
1181
|
+
|
|
1182
|
+
const body = { base_url: baseUrl, provider, model };
|
|
1183
|
+
|
|
1184
|
+
// Only send api_key if user changed it (not the masked placeholder)
|
|
1185
|
+
if (apiKeyInput && apiKeyInput !== aiApiKeyOriginal && !apiKeyInput.includes('***')) {
|
|
1186
|
+
body.api_key = apiKeyInput;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
try {
|
|
1190
|
+
const r = await fetch(API + '/api/config/ai', {
|
|
1191
|
+
method: 'PUT',
|
|
1192
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1193
|
+
body: JSON.stringify(body),
|
|
1194
|
+
});
|
|
1195
|
+
if (!r.ok) {
|
|
1196
|
+
const err = await r.json().catch(() => ({ error: r.statusText }));
|
|
1197
|
+
throw new Error(err.error || r.statusText);
|
|
1198
|
+
}
|
|
1199
|
+
showAISuccess('✓ 保存成功!重启 daemon 后生效');
|
|
1200
|
+
loadAIConfig();
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
showAIError(`保存失败: ${err.message}`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async function testAIConnection() {
|
|
1207
|
+
showAISuccess('正在测试连接...');
|
|
1208
|
+
try {
|
|
1209
|
+
// Use the /api/ai/models endpoint as a connectivity test (less costly than /messages)
|
|
1210
|
+
const r = await fetch(API + '/api/ai/models');
|
|
1211
|
+
if (!r.ok) {
|
|
1212
|
+
const err = await r.json().catch(() => ({ error: r.statusText }));
|
|
1213
|
+
throw new Error(err.error || r.statusText);
|
|
1214
|
+
}
|
|
1215
|
+
const data = await r.json();
|
|
1216
|
+
const count = (data.data || []).length;
|
|
1217
|
+
showAISuccess(`✓ 连接成功!上游返回 ${count} 个模型`);
|
|
1218
|
+
} catch (err) {
|
|
1219
|
+
showAIError(`连接失败: ${err.message}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function showAISuccess(msg) {
|
|
1224
|
+
const el = document.getElementById('ai-config-success');
|
|
1225
|
+
const errEl = document.getElementById('ai-config-error');
|
|
1226
|
+
el.textContent = msg;
|
|
1227
|
+
el.style.display = 'block';
|
|
1228
|
+
errEl.style.display = 'none';
|
|
1229
|
+
setTimeout(() => { el.style.display = 'none'; }, 5000);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function showAIError(msg) {
|
|
1233
|
+
const el = document.getElementById('ai-config-error');
|
|
1234
|
+
const okEl = document.getElementById('ai-config-success');
|
|
1235
|
+
el.textContent = msg;
|
|
1236
|
+
el.style.display = 'block';
|
|
1237
|
+
okEl.style.display = 'none';
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1149
1240
|
function routingWindow() {
|
|
1150
1241
|
const el = document.getElementById('routing-window');
|
|
1151
1242
|
return el ? el.value : '168';
|
|
@@ -1213,19 +1304,28 @@ async function loadRoutingEvents() {
|
|
|
1213
1304
|
const obeyed = document.getElementById('routing-filter-obeyed')?.value || '';
|
|
1214
1305
|
const agent = document.getElementById('routing-filter-agent')?.value || '';
|
|
1215
1306
|
const params = new URLSearchParams({ limit: '100' });
|
|
1216
|
-
|
|
1307
|
+
// 'forced' is a client-side filter: keep is_forced=1 rows regardless of obeyed.
|
|
1308
|
+
if (obeyed !== '' && obeyed !== 'forced') params.set('obeyed', obeyed);
|
|
1217
1309
|
if (agent) params.set('agent', agent);
|
|
1218
1310
|
const r = await fetch(API + '/api/routing/events?' + params.toString());
|
|
1219
|
-
|
|
1311
|
+
let rows = await r.json();
|
|
1312
|
+
if (obeyed === 'forced') rows = rows.filter(e => e.is_forced === 1);
|
|
1220
1313
|
const tbody = document.getElementById('routing-events-tbody');
|
|
1221
1314
|
if (!rows || rows.length === 0) {
|
|
1222
1315
|
tbody.innerHTML = '<tr><td colspan="5">' + empty('暂无路由事件') + '</td></tr>';
|
|
1223
1316
|
return;
|
|
1224
1317
|
}
|
|
1225
1318
|
tbody.innerHTML = rows.map(e => {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1319
|
+
let status;
|
|
1320
|
+
if (!e.is_forced) {
|
|
1321
|
+
status = '<span class="badge" style="background:var(--bg-secondary);color:var(--text-dim)">未路由</span>';
|
|
1322
|
+
} else if (e.obeyed === 1) {
|
|
1323
|
+
status = '<span class="badge badge-allow">遵守</span>';
|
|
1324
|
+
} else if (e.obeyed === 0) {
|
|
1325
|
+
status = '<span class="badge badge-block">违抗</span>';
|
|
1326
|
+
} else {
|
|
1327
|
+
status = '<span class="badge badge-info">未判定</span>';
|
|
1328
|
+
}
|
|
1229
1329
|
const ts = new Date(e.ts).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' });
|
|
1230
1330
|
return '<tr onclick="openRoutingEventDrawer(' + JSON.stringify(e).replace(/"/g,'"') + ')">'
|
|
1231
1331
|
+ '<td style="color:var(--text-dim);font-size:0.8rem">' + ts + '</td>'
|