@kenhkl/agent-moss 0.12.1 → 0.12.2
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/static/index.html +282 -245
- package/package.json +1 -1
package/dist/static/index.html
CHANGED
|
@@ -279,12 +279,30 @@ body {
|
|
|
279
279
|
</div>
|
|
280
280
|
|
|
281
281
|
<script>
|
|
282
|
+
// ===== i18n =====
|
|
283
|
+
// 语言来源优先级:URL ?lang=en > ?lang=zh > 系统/文档语言。默认 zh。
|
|
284
|
+
// en 模式下所有用户可见文案翻译为英文;数据契约 key(deny_write/critical/分类key/风险类型key)
|
|
285
|
+
// 保持原样,它们是接口取值,翻错会破坏与后端对接。
|
|
286
|
+
function detectLang() {
|
|
287
|
+
const params = new URLSearchParams(window.location.search);
|
|
288
|
+
const q = params.get('lang');
|
|
289
|
+
if (q === 'en' || q === 'zh') return q;
|
|
290
|
+
const docLang = (document.documentElement.lang || '').toLowerCase();
|
|
291
|
+
return docLang.indexOf('zh') === 0 ? 'zh' : 'zh'; // 默认中文
|
|
292
|
+
}
|
|
293
|
+
const LANG = detectLang();
|
|
294
|
+
const isEn = LANG === 'en';
|
|
295
|
+
/** t(zh, en):中文为默认回退,en 模式下返回英文翻译。 */
|
|
296
|
+
function t(zh, en) { return isEn ? (en != null && en !== '' ? en : zh) : zh; }
|
|
297
|
+
function applyTitle() { document.title = t('AgentMoss 安全策略管控台', 'AgentMoss Policy Console'); }
|
|
298
|
+
document.documentElement.lang = isEn ? 'en' : 'zh-CN';
|
|
299
|
+
|
|
282
300
|
// ===== API client =====
|
|
283
301
|
const TOKEN_KEY = 'am_console_token';
|
|
284
302
|
function getToken() { return localStorage.getItem(TOKEN_KEY) || ''; }
|
|
285
303
|
function authHeaders() {
|
|
286
|
-
const
|
|
287
|
-
return
|
|
304
|
+
const t_ = getToken();
|
|
305
|
+
return t_ ? { 'Authorization': 'Bearer ' + t_ } : {};
|
|
288
306
|
}
|
|
289
307
|
async function api(path, opts = {}) {
|
|
290
308
|
const res = await fetch('/console/api/' + path, {
|
|
@@ -292,9 +310,9 @@ async function api(path, opts = {}) {
|
|
|
292
310
|
...opts,
|
|
293
311
|
});
|
|
294
312
|
if (res.status === 401) {
|
|
295
|
-
const
|
|
296
|
-
if (
|
|
297
|
-
throw new Error('未授权');
|
|
313
|
+
const t_ = prompt(t('需要 Bearer token 访问安全策略管控台:', 'Bearer token required to access the policy console: '));
|
|
314
|
+
if (t_) { localStorage.setItem(TOKEN_KEY, t_); return api(path, opts); }
|
|
315
|
+
throw new Error(t('未授权', 'Unauthorized'));
|
|
298
316
|
}
|
|
299
317
|
if (!res.ok) {
|
|
300
318
|
const d = await res.json().catch(() => ({}));
|
|
@@ -310,48 +328,48 @@ let state = { config: {}, envOverrides: {}, currentPage: 'overview' };
|
|
|
310
328
|
const expandedCats = new Set();
|
|
311
329
|
let currentModalAction = null;
|
|
312
330
|
|
|
313
|
-
// ===== 静态文案 =====
|
|
331
|
+
// ===== 静态文案 / i18n 名称字典 =====
|
|
314
332
|
const LAYER_NAMES = {
|
|
315
|
-
L1_heuristic: { name: 'L1 静态检测', desc: '正则/关键词匹配,仅看下一步动作' },
|
|
316
|
-
L2_logic_rules: { name: 'L2 逻辑规则', desc: 'read_before_write + 授权检测使用历史动作序列,敏感路径/危险模式仅匹配下一步' },
|
|
317
|
-
L3_llm_analysis: { name: 'L3 LLM 分析', desc: 'Skill 匹配 + LLM 语义判断' },
|
|
333
|
+
L1_heuristic: { name: t('L1 静态检测', 'L1 Heuristic Detection'), desc: t('正则/关键词匹配,仅看下一步动作', 'Regex/keyword matching; only inspects the next action') },
|
|
334
|
+
L2_logic_rules: { name: t('L2 逻辑规则', 'L2 Logic Rules'), desc: t('read_before_write + 授权检测使用历史动作序列,敏感路径/危险模式仅匹配下一步', 'read_before_write + authorization checks use historical action sequences; sensitive paths/dangerous patterns match only the next action') },
|
|
335
|
+
L3_llm_analysis: { name: t('L3 LLM 分析', 'L3 LLM Analysis'), desc: t('Skill 匹配 + LLM 语义判断', 'Skill matching + LLM semantic judgment') },
|
|
318
336
|
};
|
|
319
337
|
const L1_CATEGORY_NAMES = {
|
|
320
|
-
dangerous_commands: '⚡ 危险命令检测',
|
|
321
|
-
injection_detection: '🎯 注入攻击检测',
|
|
322
|
-
sensitive_path_access: '🛡️ 敏感路径访问',
|
|
323
|
-
user_custom: '🔧 用户自定义',
|
|
338
|
+
dangerous_commands: t('⚡ 危险命令检测', '⚡ Dangerous Commands'),
|
|
339
|
+
injection_detection: t('🎯 注入攻击检测', '🎯 Injection Attack Detection'),
|
|
340
|
+
sensitive_path_access: t('🛡️ 敏感路径访问', '🛡️ Sensitive Path Access'),
|
|
341
|
+
user_custom: t('🔧 用户自定义', '🔧 User Custom'),
|
|
324
342
|
};
|
|
325
343
|
const L2_CATEGORY_NAMES = {
|
|
326
|
-
intent_consistency: '🎯 意图一致性 📋对比原始意图',
|
|
327
|
-
password_modify_consent: '🔑 密码修改授权 📋涉及历史分析',
|
|
328
|
-
user_deletion_consent: '👤 用户删除授权 📋涉及历史分析',
|
|
329
|
-
read_before_write: '📄 读写一致性 📋涉及历史分析',
|
|
330
|
-
user_custom: '🔧 用户自定义',
|
|
344
|
+
intent_consistency: t('🎯 意图一致性 📋对比原始意图', '🎯 Intent Consistency 📋vs original intent'),
|
|
345
|
+
password_modify_consent: t('🔑 密码修改授权 📋涉及历史分析', '🔑 Password Modify Consent 📋uses history'),
|
|
346
|
+
user_deletion_consent: t('👤 用户删除授权 📋涉及历史分析', '👤 User Deletion Consent 📋uses history'),
|
|
347
|
+
read_before_write: t('📄 读写一致性 📋涉及历史分析', '📄 Read-before-Write 📋uses history'),
|
|
348
|
+
user_custom: t('🔧 用户自定义', '🔧 User Custom'),
|
|
331
349
|
};
|
|
332
350
|
const L3_CATEGORY_NAMES = {
|
|
333
|
-
file_operations: '📁 文件操作',
|
|
334
|
-
network_security: '🌐 网络安全',
|
|
335
|
-
persistence_supply_chain: '🔒 持久化/供应链',
|
|
336
|
-
intent_and_general: '🎯 意图/通用',
|
|
337
|
-
resource: '💻 资耗',
|
|
338
|
-
user_custom: '🔧 用户自定义',
|
|
351
|
+
file_operations: t('📁 文件操作', '📁 File Operations'),
|
|
352
|
+
network_security: t('🌐 网络安全', '🌐 Network Security'),
|
|
353
|
+
persistence_supply_chain: t('🔒 持久化/供应链', '🔒 Persistence/Supply Chain'),
|
|
354
|
+
intent_and_general: t('🎯 意图/通用', '🎯 Intent/General'),
|
|
355
|
+
resource: t('💻 资耗', '💻 Resource Exhaustion'),
|
|
356
|
+
user_custom: t('🔧 用户自定义', '🔧 User Custom'),
|
|
339
357
|
};
|
|
340
358
|
const RISK_TYPE_NAMES = {
|
|
341
|
-
script_execution: '脚本执行', file_access: '文件访问', data_exfiltration: '数据外泄',
|
|
342
|
-
privilege_escalation: '提权', persistence: '持久化后门', lateral_movement: '横向移动',
|
|
343
|
-
config_tampering: '配置篡改', supply_chain: '供应链', resource_exhaustion: '资源耗尽',
|
|
344
|
-
intent_deviation: '意图偏离',
|
|
359
|
+
script_execution: t('脚本执行', 'Script Execution'), file_access: t('文件访问', 'File Access'), data_exfiltration: t('数据外泄', 'Data Exfiltration'),
|
|
360
|
+
privilege_escalation: t('提权', 'Privilege Escalation'), persistence: t('持久化后门', 'Persistence Backdoor'), lateral_movement: t('横向移动', 'Lateral Movement'),
|
|
361
|
+
config_tampering: t('配置篡改', 'Config Tampering'), supply_chain: t('供应链', 'Supply Chain'), resource_exhaustion: t('资源耗尽', 'Resource Exhaustion'),
|
|
362
|
+
intent_deviation: t('意图偏离', 'Intent Deviation'),
|
|
345
363
|
};
|
|
346
364
|
const RISK_TOOLTIPS = {
|
|
347
|
-
critical: '严重:必须拒绝,不可逆破坏或提权操作',
|
|
348
|
-
high: '高危:强烈建议拒绝,需人工审核',
|
|
349
|
-
medium: '中危:需谨慎评估,结合意图判断',
|
|
350
|
-
low: '低危:常规操作,可按需放行',
|
|
365
|
+
critical: t('严重:必须拒绝,不可逆破坏或提权操作', 'Critical: must reject; irreversible damage or privilege escalation'),
|
|
366
|
+
high: t('高危:强烈建议拒绝,需人工审核', 'High: strongly recommend rejecting; requires human review'),
|
|
367
|
+
medium: t('中危:需谨慎评估,结合意图判断', 'Medium: evaluate carefully; judge with intent'),
|
|
368
|
+
low: t('低危:常规操作,可按需放行', 'Low: normal operation; may allow as needed'),
|
|
351
369
|
};
|
|
352
|
-
const DENY_MODE_LABELS = { deny_write: '仅拦截写入', deny_read: '仅拦截读取', deny_both: '读写均拦截' };
|
|
370
|
+
const DENY_MODE_LABELS = { deny_write: t('仅拦截写入', 'Block Write Only'), deny_read: t('仅拦截读取', 'Block Read Only'), deny_both: t('读写均拦截', 'Block Both Read & Write') };
|
|
353
371
|
|
|
354
|
-
function riskTypeName(
|
|
372
|
+
function riskTypeName(tt) { if (!tt) return ''; return RISK_TYPE_NAMES[tt] || tt; }
|
|
355
373
|
function denyModeLabel(m) { return DENY_MODE_LABELS[m] || m; }
|
|
356
374
|
function denyModeClass(m) { return ({ deny_write: 'deny-write', deny_read: 'deny-read', deny_both: 'deny-both' })[m] || ''; }
|
|
357
375
|
|
|
@@ -363,23 +381,23 @@ function riskBadge(level) {
|
|
|
363
381
|
const tip = RISK_TOOLTIPS[level] || RISK_TOOLTIPS.low;
|
|
364
382
|
return `<span class="ag-badge ${cls}" title="${tip}">${level || 'low'}</span>`;
|
|
365
383
|
}
|
|
366
|
-
function riskTypeBadge(
|
|
367
|
-
if (!
|
|
368
|
-
return `<span class="ag-risk-type ag-rt-${escapeHtml(
|
|
384
|
+
function riskTypeBadge(tt) {
|
|
385
|
+
if (!tt) return '';
|
|
386
|
+
return `<span class="ag-risk-type ag-rt-${escapeHtml(tt)}" title="${t('风险类型: ', 'Risk type: ')}${escapeHtml(tt)}">${escapeHtml(riskTypeName(tt))}</span>`;
|
|
369
387
|
}
|
|
370
388
|
function builtinBadge(builtin) {
|
|
371
|
-
return builtin !== false ? '<span class="ag-badge ag-badge-builtin"
|
|
389
|
+
return builtin !== false ? '<span class="ag-badge ag-badge-builtin">' + t('内置', 'Built-in') + '</span>' : '<span class="ag-badge ag-badge-custom">' + t('自定义', 'Custom') + '</span>';
|
|
372
390
|
}
|
|
373
|
-
function removedBadge(
|
|
391
|
+
function removedBadge(removed) { return removed ? '<span class="ag-badge ag-badge-removed" title="' + t('此规则已从当前默认规则集移除,仅作历史保留;如需可手动启用', 'This rule has been removed from the current default set and is kept for history; you can enable it manually') + '">' + t('已移除', 'Removed') + '</span>' : ''; }
|
|
374
392
|
function denyModeBadge(mode) {
|
|
375
393
|
const m = mode || 'deny_write';
|
|
376
|
-
return `<span class="ag-deny-mode ${denyModeClass(m)}" title="拦截模式: ${denyModeLabel(m)}">${denyModeLabel(m)}</span>`;
|
|
394
|
+
return `<span class="ag-deny-mode ${denyModeClass(m)}" title="${t('拦截模式: ', 'Interception mode: ')}${denyModeLabel(m)}">${denyModeLabel(m)}</span>`;
|
|
377
395
|
}
|
|
378
396
|
function sourceDenyModeBadges(rule) {
|
|
379
397
|
if (rule.builtin !== false && rule.source_deny_mode) {
|
|
380
|
-
let h = `<span class="ag-source-badge" title="源码出厂的原始拦截模式。当前模式与之不同时表示已被用户覆盖"
|
|
398
|
+
let h = `<span class="ag-source-badge" title="${t('源码出厂的原始拦截模式。当前模式与之不同时表示已被用户覆盖', 'Source default interception mode. A different current mode means the user has overridden it')}">${t('原始: ', 'Source: ')}${denyModeLabel(rule.source_deny_mode)}</span>`;
|
|
381
399
|
if (rule.deny_mode && rule.deny_mode !== rule.source_deny_mode) {
|
|
382
|
-
h += '<span class="ag-overridden-hint" title="当前拦截模式已偏离源码默认值"
|
|
400
|
+
h += '<span class="ag-overridden-hint" title="' + t('当前拦截模式已偏离源码默认值', 'Current mode deviates from the source default') + '">' + t('已覆盖', 'Overridden') + '</span>';
|
|
383
401
|
}
|
|
384
402
|
return h;
|
|
385
403
|
}
|
|
@@ -389,18 +407,18 @@ function toggle(on, disabled, onclick) {
|
|
|
389
407
|
return `<button class="ag-toggle ${on ? 'on' : ''} ${disabled ? 'disabled' : ''}" ${disabled ? 'disabled' : ''} onclick="${disabled ? '' : onclick}"><span class="ag-toggle-knob"></span></button>`;
|
|
390
408
|
}
|
|
391
409
|
function formatTokens(n) {
|
|
392
|
-
if (n >= 1e8) return (n / 1e8).toFixed(1) + '亿';
|
|
393
|
-
if (n >= 1e4) return (n / 1e4).toFixed(1) + '万';
|
|
410
|
+
if (n >= 1e8) return (n / 1e8).toFixed(1) + t('亿', 'B');
|
|
411
|
+
if (n >= 1e4) return (n / 1e4).toFixed(1) + t('万', 'W');
|
|
394
412
|
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
395
413
|
return String(n || 0);
|
|
396
414
|
}
|
|
397
415
|
function showToast(msg, type) {
|
|
398
416
|
if (!type) type = 'success';
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
document.body.appendChild(
|
|
403
|
-
setTimeout(() =>
|
|
417
|
+
const tt = document.createElement('div');
|
|
418
|
+
tt.className = 'ag-toast ag-toast-' + type;
|
|
419
|
+
tt.textContent = msg;
|
|
420
|
+
document.body.appendChild(tt);
|
|
421
|
+
setTimeout(() => tt.remove(), 3000);
|
|
404
422
|
}
|
|
405
423
|
function openModal(title, bodyHtml, showActions) {
|
|
406
424
|
el('modal-title').textContent = title;
|
|
@@ -435,14 +453,14 @@ function renderCurrent() {
|
|
|
435
453
|
function render_overview() {
|
|
436
454
|
const layers = state.config.layers || {};
|
|
437
455
|
const eo = state.envOverrides || {};
|
|
438
|
-
const envBadge = k => eo[k] && eo[k].overridden ? `<span class="ag-env-badge">⚠ 已被 env
|
|
456
|
+
const envBadge = k => eo[k] && eo[k].overridden ? `<span class="ag-env-badge">⚠ ${t('已被 env 覆盖', 'Overridden by env')}</span>` : '';
|
|
439
457
|
const envOverrideBox = k => {
|
|
440
458
|
if (!eo[k] || !eo[k].overridden) return '';
|
|
441
|
-
return `<div class="ag-env-override-badge">⚠️ 环境变量 ${eo[k].env_var || ''}=1
|
|
459
|
+
return `<div class="ag-env-override-badge">⚠️ ${t('环境变量', 'Env var')} ${eo[k].env_var || ''}=1 ${t('强制禁用', 'forced disabled')}</div>`;
|
|
442
460
|
};
|
|
443
461
|
el('page-overview').innerHTML = `
|
|
444
462
|
<div class="ag-card">
|
|
445
|
-
<div class="ag-section-title"
|
|
463
|
+
<div class="ag-section-title">${t('层级全局开关', 'Layer Master Switches')}</div>
|
|
446
464
|
${Object.entries(LAYER_NAMES).map(([key, info]) => {
|
|
447
465
|
const enabled = layers[key] !== false;
|
|
448
466
|
const overridden = eo[key] && eo[key].overridden;
|
|
@@ -451,22 +469,23 @@ function render_overview() {
|
|
|
451
469
|
}).join('')}
|
|
452
470
|
</div>
|
|
453
471
|
<div class="ag-card">
|
|
454
|
-
<div class="ag-section-title"
|
|
472
|
+
<div class="ag-section-title">${t('规则统计', 'Rule Statistics')}</div>
|
|
455
473
|
<div class="ag-stat-grid">
|
|
456
|
-
<div class="ag-stat-card"><div class="label"
|
|
457
|
-
<div class="ag-stat-card"><div class="label"
|
|
458
|
-
<div class="ag-stat-card"><div class="label"
|
|
474
|
+
<div class="ag-stat-card"><div class="label">${t('L1 规则启用', 'L1 Rules Enabled')}</div><div class="value">${countEnabled(state.config.L1_rules)}/${countRules(state.config.L1_rules)}</div></div>
|
|
475
|
+
<div class="ag-stat-card"><div class="label">${t('L2 规则启用', 'L2 Rules Enabled')}</div><div class="value">${countEnabled(state.config.L2_rules)}/${countRules(state.config.L2_rules)}</div></div>
|
|
476
|
+
<div class="ag-stat-card"><div class="label">${t('L3 Skills 启用', 'L3 Skills Enabled')}</div><div class="value">${countEnabledSkills(state.config.L3_skills)}/${countSkills(state.config.L3_skills)}</div></div>
|
|
459
477
|
</div>
|
|
460
478
|
</div>
|
|
461
479
|
<div class="ag-card">
|
|
462
|
-
<div class="ag-section-title"
|
|
480
|
+
<div class="ag-section-title">${t('Token 用量统计', 'Token Usage')
|
|
481
|
+
}
|
|
463
482
|
<div style="margin-left:auto;display:flex;gap:6px">
|
|
464
|
-
<button class="ag-btn ag-btn-sm" data-days="1" onclick="loadOverviewToken(1)"
|
|
465
|
-
<button class="ag-btn ag-btn-sm active" data-days="7" onclick="loadOverviewToken(7)"
|
|
466
|
-
<button class="ag-btn ag-btn-sm" data-days="0" onclick="loadOverviewToken(0)"
|
|
483
|
+
<button class="ag-btn ag-btn-sm" data-days="1" onclick="loadOverviewToken(1)">${t('今日', 'Today')}</button>
|
|
484
|
+
<button class="ag-btn ag-btn-sm active" data-days="7" onclick="loadOverviewToken(7)">${t('近7天', '7 days')}</button>
|
|
485
|
+
<button class="ag-btn ag-btn-sm" data-days="0" onclick="loadOverviewToken(0)">${t('全部', 'All')}</button>
|
|
467
486
|
</div>
|
|
468
487
|
</div>
|
|
469
|
-
<div id="overview-token-summary" class="ag-empty"
|
|
488
|
+
<div id="overview-token-summary" class="ag-empty">${t('加载中...', 'Loading...')}</div>
|
|
470
489
|
<div id="overview-token-breakdown" style="margin-top:16px"></div>
|
|
471
490
|
</div>`;
|
|
472
491
|
loadOverviewToken(7);
|
|
@@ -477,26 +496,26 @@ async function loadOverviewToken(days) {
|
|
|
477
496
|
const s = await api('token-stats?days=' + days);
|
|
478
497
|
el('overview-token-summary').innerHTML = `
|
|
479
498
|
<div class="ag-stat-grid">
|
|
480
|
-
<div class="ag-stat-card"><div class="label"
|
|
481
|
-
<div class="ag-stat-card"><div class="label"
|
|
482
|
-
<div class="ag-stat-card"><div class="label"
|
|
483
|
-
<div class="ag-stat-card"><div class="label"
|
|
499
|
+
<div class="ag-stat-card"><div class="label">${t('总 Token', 'Total Tokens')}</div><div class="value">${formatTokens(s.total_tokens)}</div></div>
|
|
500
|
+
<div class="ag-stat-card"><div class="label">${t('输入 Token', 'Prompt Tokens')}</div><div class="value">${formatTokens(s.total_prompt_tokens)}</div></div>
|
|
501
|
+
<div class="ag-stat-card"><div class="label">${t('输出 Token', 'Completion Tokens')}</div><div class="value">${formatTokens(s.total_completion_tokens)}</div></div>
|
|
502
|
+
<div class="ag-stat-card"><div class="label">${t('LLM 调用次数', 'LLM Calls')}</div><div class="value">${s.total_calls}</div></div>
|
|
484
503
|
</div>`;
|
|
485
504
|
const byStep = s.by_step || {};
|
|
486
|
-
const known = [{ key: 'L3_security_judge', name: 'L3 安全判断' }, { key: 'step2_policy_gen', name: '策略生成' }];
|
|
505
|
+
const known = [{ key: 'L3_security_judge', name: t('L3 安全判断', 'L3 Security Judge') }, { key: 'step2_policy_gen', name: t('策略生成', 'Policy Generation') }];
|
|
487
506
|
const rendered = new Set();
|
|
488
507
|
let html = '';
|
|
489
508
|
for (const { key, name } of known) {
|
|
490
509
|
const v = byStep[key] || { total_tokens: 0, prompt_tokens: 0, completion_tokens: 0, calls: 0 };
|
|
491
510
|
rendered.add(key);
|
|
492
|
-
html += `<div class="ag-progress-row"><span class="ag-progress-label">${name}</span><span class="ag-progress-value">${formatTokens(v.total_tokens)}</span><span class="ag-progress-pct">(${v.calls}
|
|
511
|
+
html += `<div class="ag-progress-row"><span class="ag-progress-label">${name}</span><span class="ag-progress-value">${formatTokens(v.total_tokens)}</span><span class="ag-progress-pct">(${v.calls} ${t('次', 'calls')}, ${t('输入', 'in')} ${formatTokens(v.prompt_tokens)}, ${t('输出', 'out')} ${formatTokens(v.completion_tokens)})</span></div>`;
|
|
493
512
|
}
|
|
494
513
|
for (const [step, v] of Object.entries(byStep)) {
|
|
495
514
|
if (rendered.has(step)) continue;
|
|
496
|
-
html += `<div class="ag-progress-row"><span class="ag-progress-label">${step}</span><span class="ag-progress-value">${formatTokens(v.total_tokens)}</span><span class="ag-progress-pct">(${v.calls} 次)</span></div>`;
|
|
515
|
+
html += `<div class="ag-progress-row"><span class="ag-progress-label">${step}</span><span class="ag-progress-value">${formatTokens(v.total_tokens)}</span><span class="ag-progress-pct">(${v.calls} ${t('次', 'calls')})</span></div>`;
|
|
497
516
|
}
|
|
498
|
-
el('overview-token-breakdown').innerHTML = html ||
|
|
499
|
-
} catch { el('overview-token-summary').innerHTML =
|
|
517
|
+
el('overview-token-breakdown').innerHTML = html || `<div class="ag-empty">${t('无分项数据', 'No breakdown data')}</div>`;
|
|
518
|
+
} catch { el('overview-token-summary').innerHTML = `<div class="ag-empty">${t('无数据', 'No data')}</div>`; }
|
|
500
519
|
}
|
|
501
520
|
function countRules(lr) { if (!lr) return 0; return Object.values(lr).reduce((s, c) => s + (c.rules ? c.rules.length : 0), 0); }
|
|
502
521
|
function countEnabled(lr) {
|
|
@@ -516,8 +535,8 @@ async function toggleLayer(key) {
|
|
|
516
535
|
state.config = await apiPut('layers', { layer_key: key, enabled: !cur });
|
|
517
536
|
state.envOverrides = await api('env-overrides');
|
|
518
537
|
renderCurrent();
|
|
519
|
-
showToast(`${LAYER_NAMES[key]?.name || key}
|
|
520
|
-
} catch (e) { showToast('操作失败: ' + e.message, 'error'); }
|
|
538
|
+
showToast(`${LAYER_NAMES[key]?.name || key} ${t('已', '')}${!cur ? t('启用', 'enabled') : t('禁用', 'disabled')}`);
|
|
539
|
+
} catch (e) { showToast(t('操作失败: ', 'Operation failed: ') + e.message, 'error'); }
|
|
521
540
|
}
|
|
522
541
|
|
|
523
542
|
// ===== 分类开关 =====
|
|
@@ -526,16 +545,16 @@ async function toggleCategory(layer, category) {
|
|
|
526
545
|
const cur = state.config[layer]?.[category]?.category_enabled !== false;
|
|
527
546
|
state.config = await apiPut('categories/enabled', { layer, category, enabled: !cur });
|
|
528
547
|
renderCurrent();
|
|
529
|
-
showToast('分类已' + (!cur ? '启用' : '禁用'));
|
|
530
|
-
} catch (e) { showToast('操作失败: ' + e.message, 'error'); }
|
|
548
|
+
showToast(t('分类已', 'Category ') + (!cur ? t('启用', 'enabled') : t('禁用', 'disabled')));
|
|
549
|
+
} catch (e) { showToast(t('操作失败: ', 'Operation failed: ') + e.message, 'error'); }
|
|
531
550
|
}
|
|
532
551
|
async function toggleSkillCategory(category) {
|
|
533
552
|
try {
|
|
534
553
|
const cur = state.config.L3_skills?.[category]?.category_enabled !== false;
|
|
535
554
|
state.config = await apiPut('skill-categories/enabled', { category, enabled: !cur });
|
|
536
555
|
renderCurrent();
|
|
537
|
-
showToast('Skill 分类已' + (!cur ? '启用' : '禁用'));
|
|
538
|
-
} catch (e) { showToast('操作失败: ' + e.message, 'error'); }
|
|
556
|
+
showToast(t('Skill 分类已', 'Skill category ') + (!cur ? t('启用', 'enabled') : t('禁用', 'disabled')));
|
|
557
|
+
} catch (e) { showToast(t('操作失败: ', 'Operation failed: ') + e.message, 'error'); }
|
|
539
558
|
}
|
|
540
559
|
|
|
541
560
|
// ===== 折叠展开 =====
|
|
@@ -552,13 +571,11 @@ function toggleExpand(id) {
|
|
|
552
571
|
// ===== L1/L2 规则页 =====
|
|
553
572
|
function renderRuleLayer(layerKey, containerId, categoryNames) {
|
|
554
573
|
const layer = state.config[layerKey] || {};
|
|
555
|
-
// 按 categoryNames 定义的顺序渲染,保证 user_custom 等固定类目在最末。
|
|
556
|
-
// 落盘 JSON 的键顺序会被合并/迁移逻辑打乱,不能直接 Object.entries(layer)。
|
|
557
574
|
const orderedEntries = [
|
|
558
575
|
...Object.keys(categoryNames).filter(k => k in layer).map(k => [k, layer[k]]),
|
|
559
576
|
...Object.entries(layer).filter(([k]) => !(k in categoryNames)),
|
|
560
577
|
];
|
|
561
|
-
let html = `<div class="ag-card" style="padding:8px 16px"><button class="ag-btn ag-btn-primary" onclick="showAddRuleModal('${layerKey}')">+
|
|
578
|
+
let html = `<div class="ag-card" style="padding:8px 16px"><button class="ag-btn ag-btn-primary" onclick="showAddRuleModal('${layerKey}')">+ ${t('新增规则', 'Add Rule')}</button></div>`;
|
|
562
579
|
for (const [catName, catData] of orderedEntries) {
|
|
563
580
|
const displayName = categoryNames[catName] || catName;
|
|
564
581
|
const rules = catData.rules || [];
|
|
@@ -567,7 +584,7 @@ function renderRuleLayer(layerKey, containerId, categoryNames) {
|
|
|
567
584
|
const expanded = expandedCats.has(`${containerId}-${catName}`);
|
|
568
585
|
html += `<div class="ag-card">
|
|
569
586
|
<div class="ag-cat-header" onclick="toggleExpand('${containerId}-${catName}')">
|
|
570
|
-
<div class="ag-cat-name">${escapeHtml(displayName)} <span class="ag-cat-count">${enabledCount}/${rules.length}
|
|
587
|
+
<div class="ag-cat-name">${escapeHtml(displayName)} <span class="ag-cat-count">${enabledCount}/${rules.length} ${t('启用', 'enabled')}</span></div>
|
|
571
588
|
<div class="ag-cat-controls">
|
|
572
589
|
<div onclick="event.stopPropagation();toggleCategory('${layerKey}','${catName}')">${toggle(catEnabled, false, '')}</div>
|
|
573
590
|
<span class="ag-expand-icon ${expanded ? 'open' : ''}" id="expand-${containerId}-${catName}">▼</span>
|
|
@@ -575,7 +592,7 @@ function renderRuleLayer(layerKey, containerId, categoryNames) {
|
|
|
575
592
|
</div>
|
|
576
593
|
<div class="ag-rule-list" id="list-${containerId}-${catName}" style="display:${expanded ? '' : 'none'}">`;
|
|
577
594
|
if (!rules.length) {
|
|
578
|
-
html +=
|
|
595
|
+
html += `<div class="ag-empty">${t('无规则', 'No rules')}</div>`;
|
|
579
596
|
} else {
|
|
580
597
|
for (const r of rules) {
|
|
581
598
|
html += renderRuleItem(layerKey, catName, r);
|
|
@@ -590,21 +607,21 @@ function renderRuleItem(layerKey, catName, r) {
|
|
|
590
607
|
const isL2Sensitive = isL2 && catName === 'sensitive_path_access';
|
|
591
608
|
let patternDisplay = r.pattern || r.keyword || r.path || (r.intent_keywords && r.intent_keywords.join(', ')) || r.reason || r.id || '';
|
|
592
609
|
let reasonDisplay = r.reason || r.desc || '';
|
|
593
|
-
let patternLabel = '规则';
|
|
610
|
+
let patternLabel = t('规则', 'Rule');
|
|
594
611
|
if (isL2) {
|
|
595
|
-
if (r.intent_keywords) patternLabel = '触发关键词';
|
|
596
|
-
else if (r.path) patternLabel = '敏感路径';
|
|
597
|
-
else if (r.pattern) patternLabel = '检测模式';
|
|
612
|
+
if (r.intent_keywords) patternLabel = t('触发关键词', 'Trigger Keywords');
|
|
613
|
+
else if (r.path) patternLabel = t('敏感路径', 'Sensitive Path');
|
|
614
|
+
else if (r.pattern) patternLabel = t('检测模式', 'Detection Pattern');
|
|
598
615
|
}
|
|
599
|
-
const reasonLabel = isL2 ? '规则说明' : '原因';
|
|
600
|
-
const tooltip = `${patternLabel}: ${patternDisplay}\n${reasonLabel}: ${reasonDisplay || '(无)'}\nID: ${r.id}`;
|
|
616
|
+
const reasonLabel = isL2 ? t('规则说明', 'Rule Description') : t('原因', 'Reason');
|
|
617
|
+
const tooltip = `${patternLabel}: ${patternDisplay}\n${reasonLabel}: ${reasonDisplay || t('(无)', '(none)')}\nID: ${r.id}`;
|
|
601
618
|
const denyModeSelect = isL2Sensitive ? `<select class="ag-deny-select" onchange="updateDenyMode('${layerKey}','${catName}','${r.id}',this.value)" ${r.enabled ? '' : 'disabled'}>
|
|
602
|
-
<option value="deny_write" ${(r.deny_mode || 'deny_write') === 'deny_write' ? 'selected' : ''}
|
|
603
|
-
<option value="deny_read" ${(r.deny_mode || 'deny_write') === 'deny_read' ? 'selected' : ''}
|
|
604
|
-
<option value="deny_both" ${(r.deny_mode || 'deny_write') === 'deny_both' ? 'selected' : ''}
|
|
619
|
+
<option value="deny_write" ${(r.deny_mode || 'deny_write') === 'deny_write' ? 'selected' : ''}>${t('仅拦截写入', 'Block Write Only')}</option>
|
|
620
|
+
<option value="deny_read" ${(r.deny_mode || 'deny_write') === 'deny_read' ? 'selected' : ''}>${t('仅拦截读取', 'Block Read Only')}</option>
|
|
621
|
+
<option value="deny_both" ${(r.deny_mode || 'deny_write') === 'deny_both' ? 'selected' : ''}>${t('读写均拦截', 'Block Both')}</option>
|
|
605
622
|
</select>` : '';
|
|
606
|
-
const skipL3 = isL2 ? `<label class="ag-skip-l3" title="禁用该规则时,L3 对相关路径/关键词做豁免放行(但仍分析命令其他部分的安全风险)。关闭则 L3 可能独立拦截"><input type="checkbox" ${r.skip_l3_on_disabled ? 'checked' : ''} onchange="updateSkipL3('${layerKey}','${catName}','${r.id}',this.checked)"> 禁用时豁免L3</label>` : '';
|
|
607
|
-
const deleteBtn = (r.builtin === false || r.source_removed) ? `<button class="ag-btn ag-btn-danger ag-btn-sm" onclick="deleteRule('${layerKey}','${catName}','${r.id}')" title="${r.source_removed ? '此规则已从默认规则集移除,可删除清理' : '删除自定义规则'}"
|
|
623
|
+
const skipL3 = isL2 ? `<label class="ag-skip-l3" title="${t('禁用该规则时,L3 对相关路径/关键词做豁免放行(但仍分析命令其他部分的安全风险)。关闭则 L3 可能独立拦截', 'When this rule is disabled, L3 exempts the related path/keyword (but still analyzes other parts of the command). Turning off lets L3 block independently')}"><input type="checkbox" ${r.skip_l3_on_disabled ? 'checked' : ''} onchange="updateSkipL3('${layerKey}','${catName}','${r.id}',this.checked)"> ${t('禁用时豁免L3', 'Exempt L3 when disabled')}</label>` : '';
|
|
624
|
+
const deleteBtn = (r.builtin === false || r.source_removed) ? `<button class="ag-btn ag-btn-danger ag-btn-sm" onclick="deleteRule('${layerKey}','${catName}','${r.id}')" title="${r.source_removed ? t('此规则已从默认规则集移除,可删除清理', 'This rule was removed from the default set; you may delete it') : t('删除自定义规则', 'Delete custom rule')}">${t('删除', 'Delete')}</button>` : '';
|
|
608
625
|
return `<div class="ag-rule-item" title="${escapeHtml(tooltip)}">
|
|
609
626
|
<div class="ag-rule-body">
|
|
610
627
|
<div class="ag-rule-tags">${builtinBadge(r.builtin)} ${removedBadge(r.source_removed)} ${riskBadge(r.risk_level)} ${riskTypeBadge(r.risk_type)}${isL2Sensitive ? ' ' + denyModeBadge(r.deny_mode) + ' ' + sourceDenyModeBadges(r) : ''}</div>
|
|
@@ -621,50 +638,49 @@ async function toggleRule(layer, cat, id, en) {
|
|
|
621
638
|
try {
|
|
622
639
|
state.config = await apiPut('rules/enabled', { layer, category: cat, rule_id: id, enabled: en });
|
|
623
640
|
renderCurrent(); if (state.currentPage === 'search') searchRules();
|
|
624
|
-
showToast('规则 ' + id + ' 已' + (en ? '启用' : '禁用'));
|
|
625
|
-
} catch (e) { showToast('操作失败: ' + e.message, 'error'); }
|
|
641
|
+
showToast(t('规则 ', 'Rule ') + id + t(' 已', ' ') + (en ? t('启用', 'enabled') : t('禁用', 'disabled')));
|
|
642
|
+
} catch (e) { showToast(t('操作失败: ', 'Operation failed: ') + e.message, 'error'); }
|
|
626
643
|
}
|
|
627
644
|
async function updateDenyMode(layer, cat, id, mode) {
|
|
628
645
|
try {
|
|
629
646
|
state.config = await apiPut('rules/deny_mode', { layer, category: cat, rule_id: id, deny_mode: mode });
|
|
630
647
|
renderCurrent(); if (state.currentPage === 'search') searchRules();
|
|
631
|
-
showToast('拦截模式已更新为 ' + denyModeLabel(mode));
|
|
632
|
-
} catch (e) { showToast('操作失败: ' + e.message, 'error'); }
|
|
648
|
+
showToast(t('拦截模式已更新为 ', 'Interception mode updated to ') + denyModeLabel(mode));
|
|
649
|
+
} catch (e) { showToast(t('操作失败: ', 'Operation failed: ') + e.message, 'error'); }
|
|
633
650
|
}
|
|
634
651
|
async function updateSkipL3(layer, cat, id, skip) {
|
|
635
652
|
try {
|
|
636
653
|
state.config = await apiPut('rules/skip_l3', { layer, category: cat, rule_id: id, skip_l3_on_disabled: skip });
|
|
637
654
|
renderCurrent(); if (state.currentPage === 'search') searchRules();
|
|
638
|
-
showToast('规则 ' + id + ' 禁用时豁免L3 已' + (skip ? '开启' : '关闭'));
|
|
639
|
-
} catch (e) { showToast('操作失败: ' + e.message, 'error'); }
|
|
655
|
+
showToast(t('规则 ', 'Rule ') + id + t(' 禁用时豁免L3 已', ' disable-exempt-L3 ') + (skip ? t('开启', 'ON') : t('关闭', 'OFF')));
|
|
656
|
+
} catch (e) { showToast(t('操作失败: ', 'Operation failed: ') + e.message, 'error'); }
|
|
640
657
|
}
|
|
641
658
|
async function deleteRule(layer, cat, id) {
|
|
642
|
-
// 找到该规则判断是自定义还是已移除,给不同确认文案
|
|
643
659
|
const layerData = (state.config[layer] || {})[cat] || {};
|
|
644
660
|
const rule = (layerData.rules || []).find(r => r.id === id) || {};
|
|
645
661
|
const msg = rule.source_removed
|
|
646
|
-
? '确定删除已移除的规则 "' + id + '"?\n(此规则已从默认规则集移除,删除可清理历史保留。下次升级不会再恢复)'
|
|
647
|
-
: '确定删除自定义规则 "' + id + '"?';
|
|
662
|
+
? t('确定删除已移除的规则 "', 'Delete removed rule "') + id + t('"?\n(此规则已从默认规则集移除,删除可清理历史保留。下次升级不会再恢复)', '"?\n(This rule was removed from the default set. Deleting cleans up the historical entry. It will not be restored on the next upgrade.)')
|
|
663
|
+
: t('确定删除自定义规则 "', 'Delete custom rule "') + id + t('"?', '"?');
|
|
648
664
|
if (!confirm(msg)) return;
|
|
649
665
|
try {
|
|
650
666
|
state.config = await apiDelete('rules', { layer, category: cat, rule_id: id });
|
|
651
|
-
renderCurrent(); showToast('规则 ' + id + ' 已删除');
|
|
652
|
-
} catch (e) { showToast('删除失败: ' + e.message, 'error'); }
|
|
667
|
+
renderCurrent(); showToast(t('规则 ', 'Rule ') + id + t(' 已删除', ' deleted'));
|
|
668
|
+
} catch (e) { showToast(t('删除失败: ', 'Delete failed: ') + e.message, 'error'); }
|
|
653
669
|
}
|
|
654
670
|
|
|
655
671
|
// ===== Agent 定制规则页 =====
|
|
656
672
|
const AGENT_CATEGORY_NAMES = {
|
|
657
|
-
builtin_safe_action_types: '安全操作白名单(跳 L2+L3)',
|
|
658
|
-
builtin_safe_skills: '信任内置 Skill(跳 L2+L3)',
|
|
659
|
-
extra_command_patterns: '专属危险命令正则(L1 追加)',
|
|
660
|
-
extra_injection_keywords: '专属注入关键词(L1 追加)',
|
|
661
|
-
extra_sensitive_paths: '专属敏感路径(L1 追加)',
|
|
673
|
+
builtin_safe_action_types: t('安全操作白名单(跳 L2+L3)', 'Safe Action Whitelist (skip L2+L3)'),
|
|
674
|
+
builtin_safe_skills: t('信任内置 Skill(跳 L2+L3)', 'Trusted Built-in Skills (skip L2+L3)'),
|
|
675
|
+
extra_command_patterns: t('专属危险命令正则(L1 追加)', 'Dedicated Dangerous Command Regex (L1 extra)'),
|
|
676
|
+
extra_injection_keywords: t('专属注入关键词(L1 追加)', 'Dedicated Injection Keywords (L1 extra)'),
|
|
677
|
+
extra_sensitive_paths: t('专属敏感路径(L1 追加)', 'Dedicated Sensitive Paths (L1 extra)'),
|
|
662
678
|
};
|
|
663
679
|
function render_agents() {
|
|
664
680
|
const agents = (state.agentRules && state.agentRules.agents) || {};
|
|
665
681
|
const ids = Object.keys(agents);
|
|
666
682
|
if (!ids.length) {
|
|
667
|
-
el('page-agents').innerHTML =
|
|
683
|
+
el('page-agents').innerHTML = `<div class="ag-card"><div class="ag-empty">${t('无 agent 定制规则', 'No agent custom rules')}</div></div>`;
|
|
668
684
|
return;
|
|
669
685
|
}
|
|
670
686
|
let html = '';
|
|
@@ -681,16 +697,16 @@ function render_agents() {
|
|
|
681
697
|
const expanded = expandedCats.has(`agents-${agentId}-${catName}`);
|
|
682
698
|
const enCount = rules.filter(r => r.enabled).length;
|
|
683
699
|
html += `<div class="ag-cat-header" onclick="toggleExpand('agents-${agentId}-${catName}')">
|
|
684
|
-
<div class="ag-cat-name">${escapeHtml(AGENT_CATEGORY_NAMES[catName] || catName)} <span class="ag-cat-count">${enCount}/${rules.length}
|
|
700
|
+
<div class="ag-cat-name">${escapeHtml(AGENT_CATEGORY_NAMES[catName] || catName)} <span class="ag-cat-count">${enCount}/${rules.length} ${t('启用', 'enabled')}</span></div>
|
|
685
701
|
<div class="ag-cat-controls"><div onclick="event.stopPropagation();toggleAgentCategory('${agentId}','${catName}')">${toggle(catEnabled, false, '')}</div><span class="ag-expand-icon ${expanded ? 'open' : ''}">▼</span></div>
|
|
686
702
|
</div>
|
|
687
703
|
<div class="ag-rule-list" id="list-agents-${agentId}-${catName}" style="display:${expanded ? '' : 'none'}">`;
|
|
688
704
|
if (!rules.length) {
|
|
689
|
-
html +=
|
|
705
|
+
html += `<div class="ag-empty">${t('无规则', 'No rules')}</div>`;
|
|
690
706
|
} else {
|
|
691
707
|
for (const r of rules) html += renderAgentRuleItem(agentId, catName, r);
|
|
692
708
|
}
|
|
693
|
-
html += `<div style="padding:8px 0"><button class="ag-btn ag-btn-ghost" onclick="showAddAgentRuleModal('${agentId}','${catName}')">+
|
|
709
|
+
html += `<div style="padding:8px 0"><button class="ag-btn ag-btn-ghost" onclick="showAddAgentRuleModal('${agentId}','${catName}')">+ ${t('新增', 'Add')}</button></div>`;
|
|
694
710
|
html += '</div>';
|
|
695
711
|
}
|
|
696
712
|
html += '</div>';
|
|
@@ -698,21 +714,16 @@ function render_agents() {
|
|
|
698
714
|
el('page-agents').innerHTML = html;
|
|
699
715
|
}
|
|
700
716
|
function renderAgentRuleItem(agentId, catName, r) {
|
|
701
|
-
// Agent 定制规则按类别字段不同,分别提取展示维度:
|
|
702
|
-
// builtin_safe_action_types / builtin_safe_skills → value(白名单,无 risk)
|
|
703
|
-
// extra_command_patterns → pattern + risk_level + risk_type + reason
|
|
704
|
-
// extra_injection_keywords → keyword + risk_level + lang
|
|
705
|
-
// extra_sensitive_paths → path + risk_level + desc
|
|
706
717
|
const isValue = catName === 'builtin_safe_action_types' || catName === 'builtin_safe_skills';
|
|
707
718
|
const patternDisplay = r.value || r.pattern || r.keyword || r.path || r.id || JSON.stringify(r).slice(0, 60);
|
|
708
719
|
const reasonDisplay = r.reason || r.desc || r.lang || '';
|
|
709
|
-
const tagLine = (r.builtin !== false ?
|
|
710
|
-
+ (r.source_removed ?
|
|
720
|
+
const tagLine = (r.builtin !== false ? `<span class="ag-badge ag-badge-builtin" title="${t('源码内置规则', 'Built-in source rule')}">${t('内置', 'Built-in')}</span>` : `<span class="ag-badge ag-badge-custom">${t('自定义', 'Custom')}</span>`)
|
|
721
|
+
+ (r.source_removed ? `<span class="ag-badge ag-badge-removed" title="${t('此规则已从默认规则集移除,仅作历史保留', 'This rule was removed from the default set and is kept as history')}">${t('已移除', 'Removed')}</span>` : '')
|
|
711
722
|
+ (isValue ? '' : (riskBadge(r.risk_level) + (r.risk_type ? riskTypeBadge(r.risk_type) : '')));
|
|
712
|
-
const patternLabel = isValue ? '白名单' : (r.path ? '敏感路径' : (r.keyword ? '触发关键词' : (
|
|
713
|
-
const reasonLabel = r.reason || r.desc ? '原因' : (r.lang ? '语言' : '说明');
|
|
714
|
-
const tooltip = `${patternLabel}: ${patternDisplay}\n${reasonLabel}: ${reasonDisplay || '(无)'}`;
|
|
715
|
-
const delBtn = r.builtin ? '' : `<button class="ag-btn ag-btn-ghost ag-btn-sm" onclick="deleteAgentRule('${agentId}','${catName}',${JSON.stringify(r).replace(/"/g,'"')})"
|
|
723
|
+
const patternLabel = isValue ? t('白名单', 'Whitelist') : (r.path ? t('敏感路径', 'Sensitive Path') : (r.keyword ? t('触发关键词', 'Trigger Keywords') : t('规则', 'Rule')));
|
|
724
|
+
const reasonLabel = r.reason || r.desc ? t('原因', 'Reason') : (r.lang ? t('语言', 'Language') : t('说明', 'Description'));
|
|
725
|
+
const tooltip = `${patternLabel}: ${patternDisplay}\n${reasonLabel}: ${reasonDisplay || t('(无)', '(none)')}`;
|
|
726
|
+
const delBtn = r.builtin ? '' : `<button class="ag-btn ag-btn-ghost ag-btn-sm" onclick="deleteAgentRule('${agentId}','${catName}',${JSON.stringify(r).replace(/"/g,'"')})">${t('删除', 'Delete')}</button>`;
|
|
716
727
|
return `<div class="ag-rule-item" title="${escapeHtml(tooltip)}">
|
|
717
728
|
<div class="ag-rule-body">
|
|
718
729
|
<div class="ag-rule-tags">${tagLine}</div>
|
|
@@ -740,7 +751,7 @@ async function toggleAgentCategory(agentId, cat) {
|
|
|
740
751
|
}
|
|
741
752
|
async function deleteAgentRule(agentId, cat, r) {
|
|
742
753
|
const fp = { value: r.value, pattern: r.pattern, keyword: r.keyword, path: r.path };
|
|
743
|
-
if (!confirm(
|
|
754
|
+
if (!confirm(t('删除规则 ', 'Delete rule ') + (r.value || r.pattern || r.keyword || r.path || '') + '?')) return;
|
|
744
755
|
try {
|
|
745
756
|
state.agentRules = await apiDelete('agents/rules', { agent_id: agentId, category: cat, ...fp });
|
|
746
757
|
render_agents();
|
|
@@ -749,17 +760,17 @@ async function deleteAgentRule(agentId, cat, r) {
|
|
|
749
760
|
function showAddAgentRuleModal(agentId, cat) {
|
|
750
761
|
const isValueCat = cat === 'builtin_safe_action_types' || cat === 'builtin_safe_skills';
|
|
751
762
|
const body = isValueCat
|
|
752
|
-
? `<input class="ag-input" id="ar-value" placeholder="值(前缀尾随 - 或精确工具名)" style="width:100%">`
|
|
753
|
-
: `<input class="ag-input" id="ar-pattern" placeholder="正则/关键词/路径" style="width:100%;margin-bottom:8px">
|
|
763
|
+
? `<input class="ag-input" id="ar-value" placeholder="${t('值(前缀尾随 - 或精确工具名)', 'Value (prefix/suffix - or exact tool name)')}" style="width:100%">`
|
|
764
|
+
: `<input class="ag-input" id="ar-pattern" placeholder="${t('正则/关键词/路径', 'Regex/keyword/path')}" style="width:100%;margin-bottom:8px">
|
|
754
765
|
<input class="ag-input" id="ar-risk_level" placeholder="risk_level (high/medium/...)" style="width:100%">`;
|
|
755
|
-
openModal(
|
|
766
|
+
openModal(t('新增 ', 'Add ') + agentId + ' / ' + cat + t(' 规则', ' rule'), body, true);
|
|
756
767
|
currentModalAction = { type: 'agent_rule', agentId, cat, isValueCat };
|
|
757
768
|
}
|
|
758
769
|
|
|
759
770
|
// ===== L3 Skills 页 =====
|
|
760
771
|
function render_l3() {
|
|
761
772
|
const l3 = state.config.L3_skills || {};
|
|
762
|
-
let html = `<div class="ag-card" style="padding:8px 16px"><button class="ag-btn ag-btn-primary" onclick="showAddSkillModal()">+ 新增 Skill</button></div>`;
|
|
773
|
+
let html = `<div class="ag-card" style="padding:8px 16px"><button class="ag-btn ag-btn-primary" onclick="showAddSkillModal()">+ ${t('新增 Skill', 'Add Skill')}</button></div>`;
|
|
763
774
|
for (const [catName, catData] of Object.entries(l3)) {
|
|
764
775
|
const displayName = L3_CATEGORY_NAMES[catName] || catName;
|
|
765
776
|
const skills = catData.skills || [];
|
|
@@ -768,7 +779,7 @@ function render_l3() {
|
|
|
768
779
|
const expanded = expandedCats.has('page-l3-' + catName);
|
|
769
780
|
html += `<div class="ag-card">
|
|
770
781
|
<div class="ag-cat-header" onclick="toggleExpand('page-l3-${catName}')">
|
|
771
|
-
<div class="ag-cat-name">${escapeHtml(displayName)} <span class="ag-cat-count">${enabledCount}/${skills.length}
|
|
782
|
+
<div class="ag-cat-name">${escapeHtml(displayName)} <span class="ag-cat-count">${enabledCount}/${skills.length} ${t('启用', 'enabled')}</span></div>
|
|
772
783
|
<div class="ag-cat-controls">
|
|
773
784
|
<div onclick="event.stopPropagation();toggleSkillCategory('${catName}')">${toggle(catEnabled, false, '')}</div>
|
|
774
785
|
<span class="ag-expand-icon ${expanded ? 'open' : ''}" id="expand-page-l3-${catName}">▼</span>
|
|
@@ -776,17 +787,17 @@ function render_l3() {
|
|
|
776
787
|
</div>
|
|
777
788
|
<div class="ag-rule-list" id="list-page-l3-${catName}" style="display:${expanded ? '' : 'none'}">`;
|
|
778
789
|
if (!skills.length) {
|
|
779
|
-
html +=
|
|
790
|
+
html += `<div class="ag-empty">${t('无 skill', 'No skill')}</div>`;
|
|
780
791
|
} else {
|
|
781
792
|
for (const s of skills) {
|
|
782
793
|
const kw = (s.keywords || []).join(', ');
|
|
783
|
-
const deleteBtn = s.builtin === false ? `<button class="ag-btn ag-btn-danger ag-btn-sm" onclick="deleteSkill('${s.id}','${catName}')"
|
|
794
|
+
const deleteBtn = s.builtin === false ? `<button class="ag-btn ag-btn-danger ag-btn-sm" onclick="deleteSkill('${s.id}','${catName}')">${t('删除', 'Delete')}</button>` : '';
|
|
784
795
|
html += `<div class="ag-rule-item">
|
|
785
796
|
<div class="ag-rule-body">
|
|
786
797
|
<div class="ag-rule-tags">${builtinBadge(s.builtin)} <strong>${escapeHtml(s.id)}</strong></div>
|
|
787
|
-
<div class="ag-rule-reason"><span class="ag-rule-field-label"
|
|
798
|
+
<div class="ag-rule-reason"><span class="ag-rule-field-label">${t('触发关键词:', 'Trigger keywords: ')}</span> ${escapeHtml(kw)}</div>
|
|
788
799
|
</div>
|
|
789
|
-
<div class="ag-rule-controls">${toggle(s.enabled, false, `toggleSkill('${s.id}',${!s.enabled})`)}<button class="ag-btn ag-btn-ghost ag-btn-sm" onclick="showSkillDetail('${s.id}')"
|
|
800
|
+
<div class="ag-rule-controls">${toggle(s.enabled, false, `toggleSkill('${s.id}',${!s.enabled})`)}<button class="ag-btn ag-btn-ghost ag-btn-sm" onclick="showSkillDetail('${s.id}')">${t('详情', 'Details')}</button>${deleteBtn}</div>
|
|
790
801
|
</div>`;
|
|
791
802
|
}
|
|
792
803
|
}
|
|
@@ -798,29 +809,29 @@ async function toggleSkill(skillId, en) {
|
|
|
798
809
|
try {
|
|
799
810
|
state.config = await apiPut('skills/enabled', { skill_id: skillId, enabled: en });
|
|
800
811
|
renderCurrent(); if (state.currentPage === 'search') searchRules();
|
|
801
|
-
showToast('Skill ' + skillId + ' 已' + (en ? '启用' : '禁用'));
|
|
802
|
-
} catch (e) { showToast('操作失败: ' + e.message, 'error'); }
|
|
812
|
+
showToast('Skill ' + skillId + t(' 已', ' ') + (en ? t('启用', 'enabled') : t('禁用', 'disabled')));
|
|
813
|
+
} catch (e) { showToast(t('操作失败: ', 'Operation failed: ') + e.message, 'error'); }
|
|
803
814
|
}
|
|
804
815
|
async function deleteSkill(skillId, cat) {
|
|
805
|
-
if (!confirm('确定删除自定义 Skill "' + skillId + '"?')) return;
|
|
816
|
+
if (!confirm(t('确定删除自定义 Skill "', 'Delete custom Skill "') + skillId + t('"?', '"? '))) return;
|
|
806
817
|
try {
|
|
807
818
|
state.config = await apiDelete('skills', { skill_id: skillId, category: cat });
|
|
808
|
-
renderCurrent(); showToast('Skill ' + skillId + ' 已删除');
|
|
809
|
-
} catch (e) { showToast('删除失败: ' + e.message, 'error'); }
|
|
819
|
+
renderCurrent(); showToast('Skill ' + skillId + t(' 已删除', ' deleted'));
|
|
820
|
+
} catch (e) { showToast(t('删除失败: ', 'Delete failed: ') + e.message, 'error'); }
|
|
810
821
|
}
|
|
811
822
|
async function showSkillDetail(skillId) {
|
|
812
|
-
openModal('🤖 Skill 详情', '
|
|
823
|
+
openModal(t('🤖 Skill 详情', '🤖 Skill Details'), `<p style="color:var(--ag-text-tertiary);padding:8px 0">${t('加载中...', 'Loading...')}</p>`, false);
|
|
813
824
|
el('modal').classList.add('ag-modal-wide');
|
|
814
825
|
try {
|
|
815
826
|
const data = await api('skills/' + encodeURIComponent(skillId) + '/content');
|
|
816
827
|
el('modal-body').innerHTML = renderSkillContent(data.content || '', skillId, data.path || '');
|
|
817
828
|
} catch (e) {
|
|
818
|
-
el('modal-body').innerHTML =
|
|
829
|
+
el('modal-body').innerHTML = `<p style="color:var(--ag-red);padding:8px 0">${t('加载失败: ', 'Load failed: ')}${escapeHtml(e.message)}</p>`;
|
|
819
830
|
}
|
|
820
831
|
}
|
|
821
832
|
function renderSkillContent(md, skillId, filePath) {
|
|
822
833
|
const lines = (md || '').split('\n');
|
|
823
|
-
let html = '<div class="ag-skill-detail-header"><span class="ag-skill-detail-id">' + escapeHtml(skillId) + '</span></div><div class="ag-skill-meta"><span class="ag-skill-meta-item">📄 <strong>' + escapeHtml(filePath || '(未知)') + '</strong></span><span class="ag-skill-meta-item">📏 <strong>' + lines.length + '</strong>
|
|
834
|
+
let html = '<div class="ag-skill-detail-header"><span class="ag-skill-detail-id">' + escapeHtml(skillId) + '</span></div><div class="ag-skill-meta"><span class="ag-skill-meta-item">📄 <strong>' + escapeHtml(filePath || t('(未知)', '(unknown)')) + '</strong></span><span class="ag-skill-meta-item">📏 <strong>' + lines.length + '</strong> ' + t('行', 'lines') + '</span></div><div class="ag-skill-content">';
|
|
824
835
|
let inCode = false, codeBuf = [], textBuf = [];
|
|
825
836
|
const flushText = () => { if (textBuf.length) { html += escapeHtml(textBuf.join('\n')); textBuf = []; } };
|
|
826
837
|
const flushCode = () => { if (codeBuf.length) { html += '<pre><code>' + escapeHtml(codeBuf.join('\n')) + '</code></pre>'; codeBuf = []; } };
|
|
@@ -841,16 +852,16 @@ function renderSkillContent(md, skillId, filePath) {
|
|
|
841
852
|
function render_search() {
|
|
842
853
|
el('page-search').innerHTML = `
|
|
843
854
|
<div class="ag-card">
|
|
844
|
-
<div class="ag-section-title"
|
|
855
|
+
<div class="ag-section-title">${t('全局规则搜索', 'Global Rule Search')}</div>
|
|
845
856
|
<div style="display:flex;gap:8px">
|
|
846
|
-
<input class="ag-input" id="search-input" placeholder="输入关键词搜索规则,如: sysfs, rm -rf, /etc/shadow, 数据外泄..." oninput="searchRules()" style="flex:1">
|
|
847
|
-
<button class="ag-btn ag-btn-primary" onclick="searchRules()"
|
|
848
|
-
<button class="ag-btn ag-btn-ghost" onclick="clearSearch()"
|
|
857
|
+
<input class="ag-input" id="search-input" placeholder="${t('输入关键词搜索规则,如: sysfs, rm -rf, /etc/shadow, 数据外泄...', 'Search rules by keyword, e.g. sysfs, rm -rf, /etc/shadow, data exfiltration...')}" oninput="searchRules()" style="flex:1">
|
|
858
|
+
<button class="ag-btn ag-btn-primary" onclick="searchRules()">${t('搜索', 'Search')}</button>
|
|
859
|
+
<button class="ag-btn ag-btn-ghost" onclick="clearSearch()">${t('清空', 'Clear')}</button>
|
|
849
860
|
</div>
|
|
850
|
-
<div class="ag-form-hint" style="margin-top:8px"
|
|
861
|
+
<div class="ag-form-hint" style="margin-top:8px">${t('搜索范围:L1 规则的正则/原因/风险类型、L2 规则的路径/描述、L3 Skill 的关键词/ID。命中后可直接启用或禁用对应规则。', 'Search scope: L1 rule regex/reason/risk type, L2 rule path/description, L3 Skill keyword/ID. You can enable or disable the matching rule directly.')}</div>
|
|
851
862
|
</div>
|
|
852
863
|
<div class="ag-card" id="search-results-card" style="display:none">
|
|
853
|
-
<div class="ag-section-title"
|
|
864
|
+
<div class="ag-section-title">${t('搜索结果', 'Search Results')} <span class="ag-cat-count" id="search-count">0 ${t('条', 'results')}</span></div>
|
|
854
865
|
<div id="search-results"></div>
|
|
855
866
|
</div>`;
|
|
856
867
|
setTimeout(() => { const inp = el('search-input'); if (inp) inp.focus(); }, 0);
|
|
@@ -878,38 +889,38 @@ function searchRules() {
|
|
|
878
889
|
if (hay.includes(q)) results.push({ layer: 'L3_skills', layerName: 'L3', category: cat, ...s, isSkill: true });
|
|
879
890
|
}
|
|
880
891
|
}
|
|
881
|
-
cnt.textContent = results.length + ' 条';
|
|
892
|
+
cnt.textContent = results.length + ' ' + t('条', 'results');
|
|
882
893
|
card.style.display = '';
|
|
883
|
-
if (!results.length) { list.innerHTML =
|
|
894
|
+
if (!results.length) { list.innerHTML = `<div class="ag-empty">${t('未找到匹配的规则', 'No matching rules found')}</div>`; return; }
|
|
884
895
|
list.innerHTML = results.map(renderSearchItem).join('');
|
|
885
896
|
}
|
|
886
897
|
function renderSearchItem(r) {
|
|
887
898
|
const isL2Sensitive = r.layer === 'L2_rules' && r.category === 'sensitive_path_access';
|
|
888
899
|
let ruleText = r.pattern || r.path || r.keyword || (r.keywords && r.keywords.join(', ')) || (r.intent_keywords && r.intent_keywords.join(', ')) || r.reason || r.desc || '';
|
|
889
900
|
const reason = r.reason || r.desc || '';
|
|
890
|
-
const tooltip = '规则: ' + ruleText + '\n原因: ' + (reason || '(无)') + '\nID: ' + r.id;
|
|
901
|
+
const tooltip = t('规则: ', 'Rule: ') + ruleText + t('\n原因: ', '\nReason: ') + (reason || t('(无)', '(none)')) + '\nID: ' + r.id;
|
|
891
902
|
const toggleFn = r.isSkill ? "toggleSkill('" + r.id + "'," + (!r.enabled) + ")" : "toggleRule('" + r.layer + "','" + r.category + "','" + r.id + "'," + (!r.enabled) + ")";
|
|
892
903
|
const denyModeSelect = isL2Sensitive ? `<select class="ag-deny-select" onchange="updateDenyMode('${r.layer}','${r.category}','${r.id}',this.value)" ${r.enabled ? '' : 'disabled'}>
|
|
893
|
-
<option value="deny_write" ${(r.deny_mode || 'deny_write') === 'deny_write' ? 'selected' : ''}
|
|
894
|
-
<option value="deny_read" ${(r.deny_mode || 'deny_write') === 'deny_read' ? 'selected' : ''}
|
|
895
|
-
<option value="deny_both" ${(r.deny_mode || 'deny_write') === 'deny_both' ? 'selected' : ''}
|
|
904
|
+
<option value="deny_write" ${(r.deny_mode || 'deny_write') === 'deny_write' ? 'selected' : ''}>${t('仅拦截写入', 'Block Write Only')}</option>
|
|
905
|
+
<option value="deny_read" ${(r.deny_mode || 'deny_write') === 'deny_read' ? 'selected' : ''}>${t('仅拦截读取', 'Block Read Only')}</option>
|
|
906
|
+
<option value="deny_both" ${(r.deny_mode || 'deny_write') === 'deny_both' ? 'selected' : ''}>${t('读写均拦截', 'Block Both')}</option>
|
|
896
907
|
</select>` : '';
|
|
897
|
-
const skipL3 = r.layer === 'L2_rules' ? `<label class="ag-skip-l3"><input type="checkbox" ${r.skip_l3_on_disabled ? 'checked' : ''} onchange="updateSkipL3('${r.layer}','${r.category}','${r.id}',this.checked)"> 禁用时豁免L3</label>` : '';
|
|
908
|
+
const skipL3 = r.layer === 'L2_rules' ? `<label class="ag-skip-l3"><input type="checkbox" ${r.skip_l3_on_disabled ? 'checked' : ''} onchange="updateSkipL3('${r.layer}','${r.category}','${r.id}',this.checked)"> ${t('禁用时豁免L3', 'Exempt L3 when disabled')}</label>` : '';
|
|
898
909
|
return `<div class="ag-rule-item" title="${escapeHtml(tooltip)}">
|
|
899
910
|
<div class="ag-rule-body">
|
|
900
911
|
<div class="ag-rule-tags">
|
|
901
912
|
<span class="ag-layer-badge ag-layer-${r.layerName.toLowerCase()}">${r.layerName}</span>
|
|
902
913
|
${builtinBadge(r.builtin)} ${removedBadge(r.source_removed)} ${riskBadge(r.risk_level)} ${riskTypeBadge(r.risk_type)}
|
|
903
914
|
${isL2Sensitive ? denyModeBadge(r.deny_mode) + ' ' + sourceDenyModeBadges(r) : ''}
|
|
904
|
-
<span class="ag-search-cat"
|
|
915
|
+
<span class="ag-search-cat">${t('分类: ', 'Category: ')}${escapeHtml(r.category)}</span>
|
|
905
916
|
</div>
|
|
906
|
-
<div class="ag-rule-pattern"><span class="ag-rule-field-label"
|
|
907
|
-
${reason ?
|
|
917
|
+
<div class="ag-rule-pattern"><span class="ag-rule-field-label">${t('规则:', 'Rule:')}</span> ${escapeHtml(ruleText)}</div>
|
|
918
|
+
${reason ? `<div class="ag-rule-reason"><span class="ag-rule-field-label">${t('原因:', 'Reason:')}</span> ${escapeHtml(reason)}</div>` : ''}
|
|
908
919
|
<div class="ag-rule-id">ID: ${escapeHtml(r.id)}</div>
|
|
909
920
|
</div>
|
|
910
921
|
<div class="ag-rule-controls">
|
|
911
922
|
${denyModeSelect}
|
|
912
|
-
<span class="ag-status-text ${r.enabled ? 'ag-status-on' : 'ag-status-off'}">${r.enabled ? '已启用' : '已禁用'}</span>
|
|
923
|
+
<span class="ag-status-text ${r.enabled ? 'ag-status-on' : 'ag-status-off'}">${r.enabled ? t('已启用', 'Enabled') : t('已禁用', 'Disabled')}</span>
|
|
913
924
|
${toggle(r.enabled, false, toggleFn)}
|
|
914
925
|
${skipL3}
|
|
915
926
|
</div>
|
|
@@ -928,25 +939,26 @@ let trendChartInstance = null;
|
|
|
928
939
|
function render_token() {
|
|
929
940
|
el('page-token').innerHTML = `
|
|
930
941
|
<div class="ag-card">
|
|
931
|
-
<div class="ag-section-title"
|
|
942
|
+
<div class="ag-section-title">${t('Token 用量详细统计', 'Detailed Token Statistics')
|
|
943
|
+
}
|
|
932
944
|
<div style="margin-left:auto;display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
|
933
|
-
<button class="ag-btn ag-btn-sm" data-days="1" onclick="loadTokenStats(1)"
|
|
934
|
-
<button class="ag-btn ag-btn-sm" data-days="7" onclick="loadTokenStats(7)"
|
|
935
|
-
<button class="ag-btn ag-btn-sm" data-days="30" onclick="loadTokenStats(30)"
|
|
936
|
-
<button class="ag-btn ag-btn-sm" data-days="0" onclick="loadTokenStats(0)"
|
|
945
|
+
<button class="ag-btn ag-btn-sm" data-days="1" onclick="loadTokenStats(1)">${t('今日', 'Today')}</button>
|
|
946
|
+
<button class="ag-btn ag-btn-sm" data-days="7" onclick="loadTokenStats(7)">${t('近7天', '7 days')}</button>
|
|
947
|
+
<button class="ag-btn ag-btn-sm" data-days="30" onclick="loadTokenStats(30)">${t('近30天', '30 days')}</button>
|
|
948
|
+
<button class="ag-btn ag-btn-sm" data-days="0" onclick="loadTokenStats(0)">${t('全部', 'All')}</button>
|
|
937
949
|
<span style="width:1px;height:20px;background:var(--ag-divider);margin:0 4px"></span>
|
|
938
|
-
<input type="date" id="token-date-from" class="ag-date-input" title="开始日期">
|
|
939
|
-
<span class="ag-date-sep"
|
|
940
|
-
<input type="date" id="token-date-to" class="ag-date-input" title="结束日期">
|
|
941
|
-
<button class="ag-btn ag-btn-sm" onclick="loadTokenStatsByRange()"
|
|
950
|
+
<input type="date" id="token-date-from" class="ag-date-input" title="${t('开始日期', 'Start date')}">
|
|
951
|
+
<span class="ag-date-sep">${t('至', 'to')}</span>
|
|
952
|
+
<input type="date" id="token-date-to" class="ag-date-input" title="${t('结束日期', 'End date')}">
|
|
953
|
+
<button class="ag-btn ag-btn-sm" onclick="loadTokenStatsByRange()">${t('查询', 'Query')}</button>
|
|
942
954
|
</div>
|
|
943
955
|
</div>
|
|
944
|
-
<div id="token-summary" class="ag-empty"
|
|
956
|
+
<div id="token-summary" class="ag-empty">${t('加载中...', 'Loading...')}</div>
|
|
945
957
|
</div>
|
|
946
|
-
<div class="ag-card"><div class="ag-section-title"
|
|
947
|
-
<div class="ag-card"><div class="ag-section-title"
|
|
948
|
-
<div class="ag-card"><div class="ag-section-title"
|
|
949
|
-
<div class="ag-card"><div class="ag-section-title"
|
|
958
|
+
<div class="ag-card"><div class="ag-section-title">${t('按步骤分组', 'By Step')}</div><div id="token-by-step"></div></div>
|
|
959
|
+
<div class="ag-card"><div class="ag-section-title">${t('按模型分组', 'By Model')}</div><div id="token-by-model"></div></div>
|
|
960
|
+
<div class="ag-card"><div class="ag-section-title">${t('Token 消耗趋势(按模型)', 'Token Consumption Trend (by Model)')}</div><div id="token-trend-chart"></div></div>
|
|
961
|
+
<div class="ag-card"><div class="ag-section-title">${t('最近调用记录', 'Recent Calls')}</div><div id="token-recent" class="ag-empty">${t('加载中...', 'Loading...')}</div></div>`;
|
|
950
962
|
startTokenAutoRefresh();
|
|
951
963
|
}
|
|
952
964
|
function startTokenAutoRefresh() {
|
|
@@ -975,8 +987,8 @@ async function loadTokenStats(days) {
|
|
|
975
987
|
async function loadTokenStatsByRange() {
|
|
976
988
|
const from = el('token-date-from') ? el('token-date-from').value : '';
|
|
977
989
|
const to = el('token-date-to') ? el('token-date-to').value : '';
|
|
978
|
-
if (!from || !to) { showToast('请选择开始和结束日期', 'error'); return; }
|
|
979
|
-
if (from > to) { showToast('开始日期不能晚于结束日期', 'error'); return; }
|
|
990
|
+
if (!from || !to) { showToast(t('请选择开始和结束日期', 'Please select start and end dates'), 'error'); return; }
|
|
991
|
+
if (from > to) { showToast(t('开始日期不能晚于结束日期', 'Start date cannot be later than end date'), 'error'); return; }
|
|
980
992
|
document.querySelectorAll('#page-token [data-days]').forEach(b => b.classList.remove('active'));
|
|
981
993
|
stopTokenAutoRefresh();
|
|
982
994
|
try {
|
|
@@ -988,18 +1000,18 @@ async function loadTokenStatsByRange() {
|
|
|
988
1000
|
renderTokenByModel(s);
|
|
989
1001
|
loadTokenTrend(null, from, to);
|
|
990
1002
|
loadTokenRecent();
|
|
991
|
-
} catch (e) { showToast('查询失败: ' + e.message, 'error'); }
|
|
1003
|
+
} catch (e) { showToast(t('查询失败: ', 'Query failed: ') + e.message, 'error'); }
|
|
992
1004
|
}
|
|
993
1005
|
function renderTokenSummary(s) {
|
|
994
1006
|
el('token-summary').innerHTML = '<div class="ag-stat-grid">' +
|
|
995
|
-
'<div class="ag-stat-card"><div class="label"
|
|
996
|
-
'<div class="ag-stat-card"><div class="label"
|
|
997
|
-
'<div class="ag-stat-card"><div class="label"
|
|
998
|
-
'<div class="ag-stat-card"><div class="label"
|
|
1007
|
+
'<div class="ag-stat-card"><div class="label">' + t('总 Token', 'Total Tokens') + '</div><div class="value">' + formatTokens(s.total_tokens) + '</div></div>' +
|
|
1008
|
+
'<div class="ag-stat-card"><div class="label">' + t('输入 Token', 'Prompt Tokens') + '</div><div class="value">' + formatTokens(s.total_prompt_tokens) + '</div></div>' +
|
|
1009
|
+
'<div class="ag-stat-card"><div class="label">' + t('输出 Token', 'Completion Tokens') + '</div><div class="value">' + formatTokens(s.total_completion_tokens) + '</div></div>' +
|
|
1010
|
+
'<div class="ag-stat-card"><div class="label">' + t('调用次数', 'Calls') + '</div><div class="value">' + s.total_calls + '</div></div></div>';
|
|
999
1011
|
}
|
|
1000
1012
|
function renderTokenByStep(s) {
|
|
1001
1013
|
const byStep = s.by_step || {};
|
|
1002
|
-
const known = [{ key: 'L3_security_judge', name: 'L3 安全判断' }, { key: 'step2_policy_gen', name: '策略生成' }];
|
|
1014
|
+
const known = [{ key: 'L3_security_judge', name: t('L3 安全判断', 'L3 Security Judge') }, { key: 'step2_policy_gen', name: t('策略生成', 'Policy Generation') }];
|
|
1003
1015
|
const rendered = new Set();
|
|
1004
1016
|
let html = '';
|
|
1005
1017
|
const total = s.total_tokens || 0;
|
|
@@ -1007,14 +1019,14 @@ function renderTokenByStep(s) {
|
|
|
1007
1019
|
const v = byStep[key] || { total_tokens: 0, calls: 0 };
|
|
1008
1020
|
rendered.add(key);
|
|
1009
1021
|
const pct = total > 0 ? (v.total_tokens / total * 100).toFixed(1) : 0;
|
|
1010
|
-
html += '<div class="ag-progress-row"><span class="ag-progress-label">' + name + '</span><span class="ag-progress-value">' + formatTokens(v.total_tokens) + '</span><span class="ag-progress-pct">' + pct + '% (' + v.calls + ' 次)</span><div class="ag-progress-bar"><div class="ag-progress-fill" style="width:' + pct + '%"></div></div></div>';
|
|
1022
|
+
html += '<div class="ag-progress-row"><span class="ag-progress-label">' + name + '</span><span class="ag-progress-value">' + formatTokens(v.total_tokens) + '</span><span class="ag-progress-pct">' + pct + '% (' + v.calls + ' ' + t('次', 'calls') + ')</span><div class="ag-progress-bar"><div class="ag-progress-fill" style="width:' + pct + '%"></div></div></div>';
|
|
1011
1023
|
}
|
|
1012
1024
|
for (const [step, v] of Object.entries(byStep)) {
|
|
1013
1025
|
if (rendered.has(step)) continue;
|
|
1014
1026
|
const pct = total > 0 ? (v.total_tokens / total * 100).toFixed(1) : 0;
|
|
1015
|
-
html += '<div class="ag-progress-row"><span class="ag-progress-label">' + step + '</span><span class="ag-progress-value">' + formatTokens(v.total_tokens) + '</span><span class="ag-progress-pct">' + pct + '% (' + v.calls + ' 次)</span><div class="ag-progress-bar"><div class="ag-progress-fill" style="width:' + pct + '%"></div></div></div>';
|
|
1027
|
+
html += '<div class="ag-progress-row"><span class="ag-progress-label">' + step + '</span><span class="ag-progress-value">' + formatTokens(v.total_tokens) + '</span><span class="ag-progress-pct">' + pct + '% (' + v.calls + ' ' + t('次', 'calls') + ')</span><div class="ag-progress-bar"><div class="ag-progress-fill" style="width:' + pct + '%"></div></div></div>';
|
|
1016
1028
|
}
|
|
1017
|
-
el('token-by-step').innerHTML = html ||
|
|
1029
|
+
el('token-by-step').innerHTML = html || `<div class="ag-empty">${t('无分项数据', 'No breakdown data')}</div>`;
|
|
1018
1030
|
}
|
|
1019
1031
|
function renderTokenByModel(s) {
|
|
1020
1032
|
const byModel = s.by_model || {};
|
|
@@ -1022,9 +1034,9 @@ function renderTokenByModel(s) {
|
|
|
1022
1034
|
let html = '';
|
|
1023
1035
|
for (const [model, v] of Object.entries(byModel)) {
|
|
1024
1036
|
const pct = total > 0 ? (v.total_tokens / total * 100).toFixed(1) : 0;
|
|
1025
|
-
html += '<div class="ag-progress-row"><span class="ag-progress-label">' + escapeHtml(model) + '</span><span class="ag-progress-value">' + formatTokens(v.total_tokens) + '</span><span class="ag-progress-pct">' + pct + '% (' + v.calls + ' 次)</span><div class="ag-progress-bar"><div class="ag-progress-fill green" style="width:' + pct + '%"></div></div></div>';
|
|
1037
|
+
html += '<div class="ag-progress-row"><span class="ag-progress-label">' + escapeHtml(model) + '</span><span class="ag-progress-value">' + formatTokens(v.total_tokens) + '</span><span class="ag-progress-pct">' + pct + '% (' + v.calls + ' ' + t('次', 'calls') + ')</span><div class="ag-progress-bar"><div class="ag-progress-fill green" style="width:' + pct + '%"></div></div></div>';
|
|
1026
1038
|
}
|
|
1027
|
-
el('token-by-model').innerHTML = html ||
|
|
1039
|
+
el('token-by-model').innerHTML = html || `<div class="ag-empty">${t('无分模型数据', 'No per-model data')}</div>`;
|
|
1028
1040
|
}
|
|
1029
1041
|
async function loadTokenTrend(days, fromOverride, toOverride) {
|
|
1030
1042
|
const trendEl = el('token-trend-chart');
|
|
@@ -1046,7 +1058,7 @@ function renderTokenTrend(data) {
|
|
|
1046
1058
|
if (!trendChartInstance) trendChartInstance = echarts.init(dom);
|
|
1047
1059
|
trendChartInstance.resize();
|
|
1048
1060
|
const dates = data.dates || [], models = data.models || [], series = data.series || {};
|
|
1049
|
-
const fmtTokens = (n) => { if (n >= 1e8) return (n / 1e8).toFixed(1) + '亿'; if (n >= 1e4) return (n / 1e4).toFixed(1) + '万'; return String(n || 0); };
|
|
1061
|
+
const fmtTokens = (n) => { if (n >= 1e8) return (n / 1e8).toFixed(1) + t('亿', 'B'); if (n >= 1e4) return (n / 1e4).toFixed(1) + t('万', 'W'); return String(n || 0); };
|
|
1050
1062
|
let xMin, xMax;
|
|
1051
1063
|
if (dates.length > 0) { const d = 86400000; xMin = new Date(dates[0]).getTime() - d; xMax = new Date(dates[dates.length - 1]).getTime() + d; }
|
|
1052
1064
|
trendChartInstance.setOption({
|
|
@@ -1071,21 +1083,21 @@ async function loadTokenRecent() {
|
|
|
1071
1083
|
if (!recentEl) return;
|
|
1072
1084
|
try {
|
|
1073
1085
|
const records = await api('token-stats/recent?limit=10');
|
|
1074
|
-
if (!records || records.length === 0) { recentEl.innerHTML =
|
|
1075
|
-
let html = '<table class="ag-table"><tr><th
|
|
1086
|
+
if (!records || records.length === 0) { recentEl.innerHTML = `<div class="ag-empty">${t('暂无记录', 'No records yet')}</div>`; return; }
|
|
1087
|
+
let html = '<table class="ag-table"><tr><th>' + t('时间', 'Time') + '</th><th>' + t('步骤', 'Step') + '</th><th>' + t('模型', 'Model') + '</th><th>' + t('输入', 'Input') + '</th><th>' + t('输出', 'Output') + '</th><th>' + t('总计', 'Total') + '</th></tr>';
|
|
1076
1088
|
for (const r of records.reverse()) {
|
|
1077
|
-
const stepName = r.step === 'L3_security_judge' ? 'L3 安全判断' : r.step === 'step2_policy_gen' ? '策略生成' : escapeHtml(r.step);
|
|
1089
|
+
const stepName = r.step === 'L3_security_judge' ? t('L3 安全判断', 'L3 Security Judge') : r.step === 'step2_policy_gen' ? t('策略生成', 'Policy Generation') : escapeHtml(r.step);
|
|
1078
1090
|
const ts = r.timestamp ? escapeHtml(r.timestamp.slice(0, 19)) : '';
|
|
1079
1091
|
html += '<tr><td>' + ts + '</td><td>' + stepName + '</td><td style="color:var(--ag-text-secondary)">' + escapeHtml(r.model || '') + '</td><td>' + r.prompt_tokens + '</td><td>' + r.completion_tokens + '</td><td><strong>' + r.total_tokens + '</strong></td></tr>';
|
|
1080
1092
|
}
|
|
1081
1093
|
html += '</table>';
|
|
1082
1094
|
recentEl.innerHTML = html;
|
|
1083
|
-
} catch (e) { recentEl.innerHTML =
|
|
1095
|
+
} catch (e) { recentEl.innerHTML = `<div class="ag-empty">${t('加载失败', 'Load failed')}</div>`; }
|
|
1084
1096
|
}
|
|
1085
1097
|
async function resetTokenStats() {
|
|
1086
|
-
if (!confirm('确定清除所有 Token 统计数据?')) return;
|
|
1087
|
-
try { await apiPost('token-stats/reset'); showToast('Token 统计已清除'); loadTokenStats(7); }
|
|
1088
|
-
catch (e) { showToast('清除失败: ' + e.message, 'error'); }
|
|
1098
|
+
if (!confirm(t('确定清除所有 Token 统计数据?', 'Clear all Token statistics?'))) return;
|
|
1099
|
+
try { await apiPost('token-stats/reset'); showToast(t('Token 统计已清除', 'Token statistics cleared')); loadTokenStats(7); }
|
|
1100
|
+
catch (e) { showToast(t('清除失败: ', 'Clear failed: ') + e.message, 'error'); }
|
|
1089
1101
|
}
|
|
1090
1102
|
|
|
1091
1103
|
// ===== 设置页 =====
|
|
@@ -1093,37 +1105,37 @@ function render_settings() {
|
|
|
1093
1105
|
const eo = state.envOverrides || {};
|
|
1094
1106
|
let envHtml = '';
|
|
1095
1107
|
if (Object.keys(eo).length === 0) {
|
|
1096
|
-
envHtml =
|
|
1108
|
+
envHtml = `<div class="ag-empty">${t('当前没有环境变量覆盖', 'No environment variable overrides currently')}</div>`;
|
|
1097
1109
|
} else {
|
|
1098
1110
|
for (const [k, v] of Object.entries(eo)) {
|
|
1099
1111
|
const layerName = LAYER_NAMES[k]?.name || k;
|
|
1100
|
-
envHtml += '<div class="ag-rule-item"><span class="ag-badge ' + (v.overridden ? 'ag-badge-critical' : 'ag-badge-low') + '">' + (v.overridden ? '强制禁用' : '无覆盖') + '</span><span style="font-size:13px">' + layerName + ': ' + escapeHtml(v.env_var || '') + '=' + escapeHtml(v.env_value || '') + '</span></div>';
|
|
1112
|
+
envHtml += '<div class="ag-rule-item"><span class="ag-badge ' + (v.overridden ? 'ag-badge-critical' : 'ag-badge-low') + '">' + (v.overridden ? t('强制禁用', 'Forced disabled') : t('无覆盖', 'No override')) + '</span><span style="font-size:13px">' + layerName + ': ' + escapeHtml(v.env_var || '') + '=' + escapeHtml(v.env_value || '') + '</span></div>';
|
|
1101
1113
|
}
|
|
1102
1114
|
}
|
|
1103
1115
|
el('page-settings').innerHTML = `
|
|
1104
1116
|
<div class="ag-card">
|
|
1105
|
-
<div class="ag-section-title"
|
|
1106
|
-
<div class="ag-section-desc"
|
|
1117
|
+
<div class="ag-section-title">${t('环境变量覆盖', 'Environment Variable Overrides')}</div>
|
|
1118
|
+
<div class="ag-section-desc">${t('环境变量优先级最高,会覆盖 Web UI 的设置。清除环境变量后 Web 设置才能生效。', 'Environment variables take highest priority and override Web UI settings. Web settings take effect only after clearing the environment variables.')}</div>
|
|
1107
1119
|
${envHtml}
|
|
1108
1120
|
</div>
|
|
1109
1121
|
<div class="ag-card">
|
|
1110
|
-
<div class="ag-section-title"
|
|
1122
|
+
<div class="ag-section-title">${t('配置文件路径', 'Config File Path')}</div>
|
|
1111
1123
|
<div style="font-size:13px;line-height:1.8">
|
|
1112
|
-
<p
|
|
1113
|
-
<p
|
|
1114
|
-
<p
|
|
1124
|
+
<p>${t('配置文件: ', 'Config file: ')}~/.config/agentmoss/agent_moss_runtime.json</p>
|
|
1125
|
+
<p>${t('环境变量 AGENT_MOSS_RUNTIME_CONFIG_PATH 可指定自定义路径', 'Environment variable AGENT_MOSS_RUNTIME_CONFIG_PATH can set a custom path')}</p>
|
|
1126
|
+
<p>${t('优先级: 环境变量 > runtime JSON > config.json > 源码默认值', 'Priority: env var > runtime JSON > config.json > source defaults')}</p>
|
|
1115
1127
|
</div>
|
|
1116
1128
|
</div>
|
|
1117
1129
|
<div class="ag-card">
|
|
1118
|
-
<div class="ag-section-title"
|
|
1119
|
-
<div class="ag-section-desc"
|
|
1120
|
-
<input class="ag-input" id="settings-token" placeholder="输入新 token" value="${getToken()}">
|
|
1121
|
-
<div style="margin-top:8px"><button class="ag-btn ag-btn-primary" onclick="localStorage.setItem('am_console_token',document.getElementById('settings-token').value);showToast('已保存')"
|
|
1130
|
+
<div class="ag-section-title">${t('访问 Token', 'Access Token')}</div>
|
|
1131
|
+
<div class="ag-section-desc">${t('此处设置的是浏览器本地 token,console 调用 API 时以 Authorization: Bearer <token> 携带。服务端是否要求认证取决于环境变量 AGENT_MOSS_CONSOLE_TOKEN:未设置则本机免认证;已设置则请求须携带相同值才放行。当前本地 token:', 'This sets the browser-local token used as Authorization: Bearer <token> on console API calls. Whether the server requires auth depends on AGENT_MOSS_CONSOLE_TOKEN: when unset, local access is allowed without auth; when set, requests must carry the matching value. Current local token: ')}${getToken() ? t('已设置', 'set') : t('未设置', 'not set')}</div>
|
|
1132
|
+
<input class="ag-input" id="settings-token" placeholder="${t('输入新 token', 'Enter new token')}" value="${getToken()}">
|
|
1133
|
+
<div style="margin-top:8px"><button class="ag-btn ag-btn-primary" onclick="localStorage.setItem('am_console_token',document.getElementById('settings-token').value);showToast(t('已保存','Saved'))">${t('保存', 'Save')}</button></div>
|
|
1122
1134
|
</div>
|
|
1123
1135
|
<div class="ag-card">
|
|
1124
|
-
<div class="ag-section-title"
|
|
1125
|
-
<button class="ag-btn ag-btn-danger" onclick="if(confirm('确定重置所有规则到出厂默认?所有自定义规则和开关设置将丢失。')){apiPost('reset').then(c=>{state.config=c;state.envOverrides=api('env-overrides');renderCurrent();showToast('配置已重置到出厂默认')}).catch(e=>showToast('重置失败: '+e.message,'error'))}"
|
|
1126
|
-
<button class="ag-btn ag-btn-danger" style="margin-left:8px" onclick="resetTokenStats()"
|
|
1136
|
+
<div class="ag-section-title">${t('危险操作', 'Danger Zone')}</div>
|
|
1137
|
+
<button class="ag-btn ag-btn-danger" onclick="if(confirm(t('确定重置所有规则到出厂默认?所有自定义规则和开关设置将丢失。','Reset all rules to factory defaults? All custom rules and switch settings will be lost.'))){apiPost('reset').then(c=>{state.config=c;state.envOverrides=api('env-overrides');renderCurrent();showToast(t('配置已重置到出厂默认','Configuration reset to factory defaults'))}).catch(e=>showToast(t('重置失败: ','Reset failed: ')+e.message,'error'))}">${t('重置规则到出厂默认', 'Reset Rules to Factory Defaults')}</button>
|
|
1138
|
+
<button class="ag-btn ag-btn-danger" style="margin-left:8px" onclick="resetTokenStats()">${t('清除 Token 统计', 'Clear Token Statistics')}</button>
|
|
1127
1139
|
</div>`;
|
|
1128
1140
|
}
|
|
1129
1141
|
|
|
@@ -1131,46 +1143,46 @@ function render_settings() {
|
|
|
1131
1143
|
function showAddRuleModal(layer) {
|
|
1132
1144
|
currentModalAction = { type: 'add_rule', layer };
|
|
1133
1145
|
const isL1 = layer === 'L1_rules', isL2 = layer === 'L2_rules';
|
|
1134
|
-
const title = isL1 ? '新增 L1 静态规则' : '新增 L2 规则';
|
|
1146
|
+
const title = isL1 ? t('新增 L1 静态规则', 'Add L1 Static Rule') : t('新增 L2 规则', 'Add L2 Rule');
|
|
1135
1147
|
let formHtml = '';
|
|
1136
1148
|
if (isL1) {
|
|
1137
|
-
formHtml = `<div class="ag-form-group"><div class="ag-form-label"
|
|
1138
|
-
<div class="ag-form-group" id="form-pattern"><div class="ag-form-label"
|
|
1139
|
-
<div class="ag-form-group" id="form-keyword" style="display:none"><div class="ag-form-label"
|
|
1140
|
-
<div class="ag-form-group"><div class="ag-form-label"
|
|
1141
|
-
<div class="ag-form-group"><div class="ag-form-label"
|
|
1142
|
-
<div class="ag-form-group"><div class="ag-form-label"
|
|
1143
|
-
<div class="ag-form-group"><div class="ag-form-label"
|
|
1144
|
-
<div class="ag-form-group"><div class="ag-form-label"
|
|
1149
|
+
formHtml = `<div class="ag-form-group"><div class="ag-form-label">${t('规则类型', 'Rule Type')}<span class="ag-required">*</span></div><select class="ag-select" id="rule-type" onchange="updateRuleForm()"><option value="pattern">${t('正则模式匹配', 'Regex pattern matching')}</option><option value="keyword">${t('关键词匹配', 'Keyword matching')}</option></select><div class="ag-form-hint">${t('正则模式:用正则表达式匹配命令(如 ', 'Regex mode: match the command with a regex (e.g. ')}<code>\\brm\\s+-rf</code>${t(');关键词匹配:命令中包含指定文字即命中。', '); keyword mode: command containing the text matches.')}</div></div>
|
|
1150
|
+
<div class="ag-form-group" id="form-pattern"><div class="ag-form-label">${t('正则 Pattern', 'Regex Pattern')}<span class="ag-required">*</span></div><input class="ag-input" id="input-pattern" placeholder="${t('如: ', 'e.g. ')}${'\\brm\\s+-rf'}"><div class="ag-form-hint">${t('Python 正则表达式,命中该正则的命令会触发本规则。', 'Python regex. Commands matching this regex trigger this rule.')}</div></div>
|
|
1151
|
+
<div class="ag-form-group" id="form-keyword" style="display:none"><div class="ag-form-label">${t('关键词', 'Keyword')}<span class="ag-required">*</span></div><input class="ag-input" id="input-keyword" placeholder="${t('如: ignore previous instructions', 'e.g. ignore previous instructions')}"><div class="ag-form-hint">${t('命令文本中包含此关键词即命中(不区分大小写)。', 'Command text containing this keyword matches (case-insensitive).')}</div></div>
|
|
1152
|
+
<div class="ag-form-group"><div class="ag-form-label">${t('风险等级', 'Risk Level')}<span class="ag-required">*</span></div><select class="ag-select" id="input-risk-level"><option value="critical">Critical(${t('严重', 'Critical')})</option><option value="high">High(${t('高危', 'High')})</option><option value="medium">Medium(${t('中危', 'Medium')})</option><option value="low">Low(${t('低危', 'Low')})</option></select><div class="ag-form-hint"><strong>critical / high</strong>:${t('在 L1 层直接拦截(Deny),命令不会执行;', 'block directly at L1 (Deny), the command will not run;')}<br><strong>medium / low</strong>:${t('不在 L1 拦截,转交 L3 LLM 深度分析判断。', 'not blocked at L1; handed to L3 LLM deep analysis.')}</div></div>
|
|
1153
|
+
<div class="ag-form-group"><div class="ag-form-label">${t('风险类型', 'Risk Type')}</div><input class="ag-input" id="input-risk-type" placeholder="${t('如: script_execution, file_access, data_exfiltration', 'e.g. script_execution, file_access, data_exfiltration')}"><div class="ag-form-hint">${t('风险分类标签,用于统计与展示。可选值:script_execution(脚本执行)、file_access(文件访问)、data_exfiltration(数据外泄)等,可自定义。', 'Risk classification label for stats and display. Options: script_execution, file_access, data_exfiltration, etc. Custom values allowed.')}</div></div>
|
|
1154
|
+
<div class="ag-form-group"><div class="ag-form-label">${t('拦截原因', 'Block Reason')}<span class="ag-required">*</span></div><input class="ag-input" id="input-reason" placeholder="${t('描述为什么这条规则要拦截', 'Describe why this rule blocks')}"><div class="ag-form-hint">${t('命中时展示给用户的说明文字,应写清为什么危险。L3 分析时也会参考此原因。', 'Text shown to the user on match; explain why it is dangerous. L3 analysis also references this reason.')}</div></div>
|
|
1155
|
+
<div class="ag-form-group"><div class="ag-form-label">${t('语言(仅关键词类型)', 'Language (keyword type only)')}</div><select class="ag-select" id="input-lang"><option value="en">English</option><option value="zh">中文</option></select><div class="ag-form-hint">${t('关键词的语言,影响匹配时的分词处理。', 'The language of the keyword affects tokenization during matching.')}</div></div>
|
|
1156
|
+
<div class="ag-form-group"><div class="ag-form-label">${t('所属分类', 'Category')}</div><select class="ag-select" id="input-category"><option value="user_custom">${t('用户自定义', 'User custom')}</option><option value="dangerous_commands">${t('危险命令检测', 'Dangerous commands')}</option><option value="injection_detection">${t('注入攻击检测', 'Injection detection')}</option></select></div>`;
|
|
1145
1157
|
} else if (isL2) {
|
|
1146
|
-
formHtml = `<div class="ag-form-group"><div class="ag-form-label"
|
|
1147
|
-
<div class="ag-form-group" id="form-l2-path"><div class="ag-form-label"
|
|
1148
|
-
<div class="ag-form-group" id="form-l2-pattern" style="display:none"><div class="ag-form-label"
|
|
1149
|
-
<div class="ag-form-group"><div class="ag-form-label"
|
|
1150
|
-
<div class="ag-form-group"><div class="ag-form-label"
|
|
1151
|
-
<div class="ag-form-group"><div class="ag-form-label"
|
|
1152
|
-
<div class="ag-form-group"><div class="ag-form-label"
|
|
1158
|
+
formHtml = `<div class="ag-form-group"><div class="ag-form-label">${t('规则类型', 'Rule Type')}<span class="ag-required">*</span></div><select class="ag-select" id="rule-type-l2" onchange="updateL2RuleForm()"><option value="path">${t('敏感路径', 'Sensitive path')}</option><option value="pattern">${t('正则模式', 'Regex pattern')}</option></select><div class="ag-form-hint">${t('敏感路径:匹配文件路径(如 ', 'Sensitive path: match a file path (e.g. ')}<code>/etc/shadow</code>${t(');正则模式:用正则匹配命令文本。', '); regex mode: match command text with regex.')}</div></div>
|
|
1159
|
+
<div class="ag-form-group" id="form-l2-path"><div class="ag-form-label">${t('路径', 'Path')}<span class="ag-required">*</span></div><input class="ag-input" id="input-l2-path" placeholder="${t('如: /etc/shadow, .ssh/id_rsa', 'e.g. /etc/shadow, .ssh/id_rsa')}"><div class="ag-form-hint">${t('要拦截的文件路径,多个用英文逗号分隔。命令访问到这些路径即命中。', 'File paths to block; separate multiple with commas. Commands accessing these paths match.')}</div></div>
|
|
1160
|
+
<div class="ag-form-group" id="form-l2-pattern" style="display:none"><div class="ag-form-label">${t('正则 Pattern', 'Regex Pattern')}<span class="ag-required">*</span></div><input class="ag-input" id="input-l2-pattern" placeholder="${t('如: ', 'e.g. ')}${'\\bpasswd\\s+--stdin'}"><div class="ag-form-hint">${t('Python 正则表达式,命中即触发。', 'Python regex; triggers on match.')}</div></div>
|
|
1161
|
+
<div class="ag-form-group"><div class="ag-form-label">${t('描述', 'Description')}<span class="ag-required">*</span></div><input class="ag-input" id="input-l2-desc" placeholder="${t('路径/规则的中文描述', 'Chinese description of the path/rule')}"><div class="ag-form-hint">${t('命中时展示给用户的说明。', 'Description shown to the user on match.')}</div></div>
|
|
1162
|
+
<div class="ag-form-group"><div class="ag-form-label">${t('风险等级', 'Risk Level')}<span class="ag-required">*</span></div><select class="ag-select" id="input-l2-risk-level"><option value="critical">Critical(${t('严重', 'Critical')})</option><option value="high">High(${t('高危', 'High')})</option><option value="medium">Medium(${t('中危', 'Medium')})</option><option value="low">Low(${t('低危', 'Low')})</option></select><div class="ag-form-hint"><strong>critical / high</strong>:${t('在 L2 层直接拦截;', 'block directly at L2;')}<br><strong>medium / low</strong>:${t('不在 L2 拦截,转交 L3 LLM 判断。', 'not blocked at L2; handed to L3 LLM.')}</div></div>
|
|
1163
|
+
<div class="ag-form-group"><div class="ag-form-label">${t('拦截模式', 'Interception Mode')}</div><select class="ag-select" id="input-l2-deny-mode"><option value="deny_write">${t('仅拦截写入', 'Block Write Only')}</option><option value="deny_read">${t('仅拦截读取', 'Block Read Only')}</option><option value="deny_both">${t('读写均拦截', 'Block Both')}</option></select><div class="ag-form-hint">${t('选择该路径的拦截模式:仅拦截写入(默认)、仅拦截读取(如 /dev/random)、读写均拦截(凭据文件)。', 'Choose interception mode: block write only (default), block read only (e.g. /dev/random), block both (credential files).')}</div></div>
|
|
1164
|
+
<div class="ag-form-group"><div class="ag-form-label">${t('所属分类', 'Category')}</div><select class="ag-select" id="input-l2-category"><option value="sensitive_path_access">${t('敏感路径访问', 'Sensitive path access')}</option><option value="password_modify_consent">${t('密码修改授权', 'Password modify consent')}</option><option value="user_deletion_consent">${t('用户删除授权', 'User deletion consent')}</option><option value="intent_consistency">${t('意图一致性', 'Intent consistency')}</option></select></div>`;
|
|
1153
1165
|
}
|
|
1154
1166
|
openModal(title, formHtml);
|
|
1155
1167
|
}
|
|
1156
1168
|
function showAddSkillModal() {
|
|
1157
1169
|
currentModalAction = { type: 'add_skill' };
|
|
1158
|
-
openModal('新增 L3 Skill', `<div class="ag-form-group"><div class="ag-form-label"
|
|
1159
|
-
<div class="ag-form-group"><div class="ag-form-label"
|
|
1160
|
-
<div class="ag-form-group"><div class="ag-form-label"
|
|
1161
|
-
<div class="ag-form-group"><div class="ag-form-label"
|
|
1170
|
+
openModal(t('新增 L3 Skill', 'Add L3 Skill'), `<div class="ag-form-group"><div class="ag-form-label">${t('Skill ID(文件名)', 'Skill ID (file name)')}</div><input class="ag-input" id="input-skill-id" placeholder="${t('如: my_custom_guard', 'e.g. my_custom_guard')}"><div class="ag-form-hint">${t('Skill 文件名(不含扩展名),英文+下划线。会成为该 Skill 的唯一标识。', 'Skill file name (without extension), letters + underscores. Becomes the Skill’s unique ID.')}</div></div>
|
|
1171
|
+
<div class="ag-form-group"><div class="ag-form-label">${t('触发关键词', 'Trigger Keywords')}<span class="ag-required">*</span></div><input class="ag-input" id="input-skill-keywords" placeholder="${t('逗号分隔, 如: custom, 特殊, special', 'comma-separated, e.g. custom, special')}"><div class="ag-form-hint">${t('命令/动作中包含这些关键词时,该 Skill 会被匹配并注入 L3 的 LLM prompt。', 'When the command/action contains these keywords, the Skill matches and is injected into the L3 LLM prompt.')}</div></div>
|
|
1172
|
+
<div class="ag-form-group"><div class="ag-form-label">${t('Skill 内容(Markdown)', 'Skill Content (Markdown)')}<span class="ag-required">*</span></div><textarea class="ag-input" id="input-skill-content" rows="4" placeholder="# Skill 规则标题\n\n## 适用场景\n..."></textarea><div class="ag-form-hint">${t('Markdown 格式的规则说明。命中关键词后会作为提示注入 LLM。', 'Markdown rule description. Injected into the LLM prompt on keyword match.')}</div></div>
|
|
1173
|
+
<div class="ag-form-group"><div class="ag-form-label">${t('所属分类', 'Category')}</div><select class="ag-select" id="input-skill-category"><option value="user_custom">${t('用户自定义', 'User custom')}</option><option value="file_operations">${t('文件操作', 'File operations')}</option><option value="network_security">${t('网络安全', 'Network security')}</option><option value="intent_and_general">${t('意图/通用', 'Intent/general')}</option></select></div>`);
|
|
1162
1174
|
}
|
|
1163
1175
|
function updateRuleForm() {
|
|
1164
|
-
const
|
|
1176
|
+
const tt = document.getElementById('rule-type')?.value;
|
|
1165
1177
|
const p = document.getElementById('form-pattern'), k = document.getElementById('form-keyword');
|
|
1166
|
-
if (p) p.style.display =
|
|
1167
|
-
if (k) k.style.display =
|
|
1178
|
+
if (p) p.style.display = tt === 'pattern' ? '' : 'none';
|
|
1179
|
+
if (k) k.style.display = tt === 'keyword' ? '' : 'none';
|
|
1168
1180
|
}
|
|
1169
1181
|
function updateL2RuleForm() {
|
|
1170
|
-
const
|
|
1182
|
+
const tt = document.getElementById('rule-type-l2')?.value;
|
|
1171
1183
|
const p = document.getElementById('form-l2-path'), k = document.getElementById('form-l2-pattern');
|
|
1172
|
-
if (p) p.style.display =
|
|
1173
|
-
if (k) k.style.display =
|
|
1184
|
+
if (p) p.style.display = tt === 'path' ? '' : 'none';
|
|
1185
|
+
if (k) k.style.display = tt === 'pattern' ? '' : 'none';
|
|
1174
1186
|
}
|
|
1175
1187
|
async function submitModal() {
|
|
1176
1188
|
if (!currentModalAction) return;
|
|
@@ -1198,7 +1210,7 @@ async function submitModal() {
|
|
|
1198
1210
|
else { body.pattern = document.getElementById('input-l2-pattern')?.value; body.risk_type = 'script_execution'; }
|
|
1199
1211
|
}
|
|
1200
1212
|
state.config = await apiPost('rules', body);
|
|
1201
|
-
showToast('规则已添加');
|
|
1213
|
+
showToast(t('规则已添加', 'Rule added'));
|
|
1202
1214
|
} else if (currentModalAction.type === 'add_skill') {
|
|
1203
1215
|
const keywords = (document.getElementById('input-skill-keywords')?.value || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
1204
1216
|
state.config = await apiPost('skills', {
|
|
@@ -1207,45 +1219,70 @@ async function submitModal() {
|
|
|
1207
1219
|
keywords,
|
|
1208
1220
|
content: document.getElementById('input-skill-content')?.value,
|
|
1209
1221
|
});
|
|
1210
|
-
showToast('Skill 已添加');
|
|
1222
|
+
showToast(t('Skill 已添加', 'Skill added'));
|
|
1211
1223
|
} else if (currentModalAction.type === 'agent_rule') {
|
|
1212
1224
|
const { agentId, cat, isValueCat } = currentModalAction;
|
|
1213
1225
|
const payload = { agent_id: agentId, category: cat };
|
|
1214
1226
|
if (isValueCat) {
|
|
1215
1227
|
payload.value = el('ar-value').value.trim();
|
|
1216
|
-
if (!payload.value) { showToast('值不能为空', 'error'); return; }
|
|
1228
|
+
if (!payload.value) { showToast(t('值不能为空', 'Value cannot be empty'), 'error'); return; }
|
|
1217
1229
|
} else {
|
|
1218
1230
|
payload.pattern = el('ar-pattern').value.trim();
|
|
1219
1231
|
payload.risk_level = el('ar-risk_level').value.trim() || undefined;
|
|
1220
1232
|
}
|
|
1221
1233
|
state.agentRules = await apiPost('agents/rules', payload);
|
|
1222
|
-
showToast('Agent 规则已添加');
|
|
1234
|
+
showToast(t('Agent 规则已添加', 'Agent rule added'));
|
|
1223
1235
|
}
|
|
1224
1236
|
renderCurrent();
|
|
1225
1237
|
closeModal();
|
|
1226
|
-
} catch (e) { showToast('添加失败: ' + e.message, 'error'); }
|
|
1238
|
+
} catch (e) { showToast(t('添加失败: ', 'Add failed: ') + e.message, 'error'); }
|
|
1227
1239
|
}
|
|
1228
1240
|
|
|
1229
1241
|
// ===== Tab nav + theme =====
|
|
1230
|
-
document.querySelectorAll('.ag-tab[data-page]').forEach(
|
|
1231
|
-
|
|
1242
|
+
document.querySelectorAll('.ag-tab[data-page]').forEach(tab => {
|
|
1243
|
+
tab.onclick = () => {
|
|
1232
1244
|
stopTokenAutoRefresh();
|
|
1233
|
-
state.currentPage =
|
|
1245
|
+
state.currentPage = tab.dataset.page;
|
|
1234
1246
|
document.querySelectorAll('.ag-tab').forEach(x => x.classList.remove('active'));
|
|
1235
|
-
|
|
1247
|
+
tab.classList.add('active');
|
|
1236
1248
|
renderCurrent();
|
|
1237
1249
|
};
|
|
1238
1250
|
});
|
|
1251
|
+
function applyTabLabels() {
|
|
1252
|
+
const labels = {
|
|
1253
|
+
overview: t('概览', 'Overview'),
|
|
1254
|
+
search: t('搜索规则', 'Search Rules'),
|
|
1255
|
+
l1: t('L1 规则', 'L1 Rules'),
|
|
1256
|
+
l2: t('L2 规则', 'L2 Rules'),
|
|
1257
|
+
l3: t('L3 Skills', 'L3 Skills'),
|
|
1258
|
+
agents: t('Agent 定制', 'Agent Rules'),
|
|
1259
|
+
token: t('Token', 'Token'),
|
|
1260
|
+
settings: t('设置', 'Settings'),
|
|
1261
|
+
};
|
|
1262
|
+
document.querySelectorAll('.ag-tab[data-page]').forEach(tab => {
|
|
1263
|
+
tab.textContent = labels[tab.dataset.page] || tab.textContent;
|
|
1264
|
+
});
|
|
1265
|
+
const themeBtn = el('theme-btn');
|
|
1266
|
+
if (themeBtn) themeBtn.textContent = document.documentElement.getAttribute('data-theme') === 'dark' ? t('☀️ 浅色', '☀️ Light') : t('🌙 暗色', '🌙 Dark');
|
|
1267
|
+
const closeBtn = document.querySelector('.ag-modal-close');
|
|
1268
|
+
if (closeBtn) closeBtn.setAttribute('title', t('关闭', 'Close'));
|
|
1269
|
+
const cancelBtn = el('modal-actions') ? document.querySelector('#modal-actions .ag-btn-ghost') : null;
|
|
1270
|
+
if (cancelBtn) cancelBtn.textContent = t('取消', 'Cancel');
|
|
1271
|
+
const submitBtn = el('modal-submit');
|
|
1272
|
+
if (submitBtn) submitBtn.textContent = t('确认', 'Confirm');
|
|
1273
|
+
}
|
|
1239
1274
|
el('theme-btn').onclick = () => {
|
|
1240
1275
|
const cur = document.documentElement.getAttribute('data-theme');
|
|
1241
1276
|
const next = cur === 'dark' ? 'light' : 'dark';
|
|
1242
1277
|
document.documentElement.setAttribute('data-theme', next);
|
|
1243
|
-
el('theme-btn').textContent = next === 'dark' ? '☀️ 浅色' : '🌙 暗色';
|
|
1278
|
+
el('theme-btn').textContent = next === 'dark' ? t('☀️ 浅色', '☀️ Light') : t('🌙 暗色', '🌙 Dark');
|
|
1244
1279
|
if (state.currentPage === 'token') render_token();
|
|
1245
1280
|
};
|
|
1246
1281
|
|
|
1247
1282
|
// ===== Init =====
|
|
1248
|
-
|
|
1283
|
+
applyTitle();
|
|
1284
|
+
applyTabLabels();
|
|
1285
|
+
refreshConfig().catch(e => { document.body.innerHTML = '<div style="padding:40px;color:#FF3B30">' + t('加载失败: ', 'Load failed: ') + escapeHtml(e.message) + '</div>'; });
|
|
1249
1286
|
</script>
|
|
1250
1287
|
</body>
|
|
1251
1288
|
</html>
|