@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.
Files changed (2) hide show
  1. package/dist/static/index.html +282 -245
  2. package/package.json +1 -1
@@ -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 t = getToken();
287
- return t ? { 'Authorization': 'Bearer ' + t } : {};
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 t = prompt('需要 Bearer token 访问安全策略管控台:');
296
- if (t) { localStorage.setItem(TOKEN_KEY, t); return api(path, opts); }
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(t) { if (!t) return ''; return RISK_TYPE_NAMES[t] || t; }
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(t) {
367
- if (!t) return '';
368
- return `<span class="ag-risk-type ag-rt-${escapeHtml(t)}" title="风险类型: ${escapeHtml(t)}">${escapeHtml(riskTypeName(t))}</span>`;
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">内置</span>' : '<span class="ag-badge ag-badge-custom">自定义</span>';
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(r) { return r ? '<span class="ag-badge ag-badge-removed" title="此规则已从当前默认规则集移除,仅作历史保留;如需可手动启用">已移除</span>' : ''; }
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="源码出厂的原始拦截模式。当前模式与之不同时表示已被用户覆盖">原始: ${denyModeLabel(rule.source_deny_mode)}</span>`;
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="当前拦截模式已偏离源码默认值">已覆盖</span>';
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 t = document.createElement('div');
400
- t.className = 'ag-toast ag-toast-' + type;
401
- t.textContent = msg;
402
- document.body.appendChild(t);
403
- setTimeout(() => t.remove(), 3000);
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 覆盖</span>` : '';
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 强制禁用</div>`;
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">层级全局开关</div>
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">规则统计</div>
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">L1 规则启用</div><div class="value">${countEnabled(state.config.L1_rules)}/${countRules(state.config.L1_rules)}</div></div>
457
- <div class="ag-stat-card"><div class="label">L2 规则启用</div><div class="value">${countEnabled(state.config.L2_rules)}/${countRules(state.config.L2_rules)}</div></div>
458
- <div class="ag-stat-card"><div class="label">L3 Skills 启用</div><div class="value">${countEnabledSkills(state.config.L3_skills)}/${countSkills(state.config.L3_skills)}</div></div>
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">Token 用量统计
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)">今日</button>
465
- <button class="ag-btn ag-btn-sm active" data-days="7" onclick="loadOverviewToken(7)">近7天</button>
466
- <button class="ag-btn ag-btn-sm" data-days="0" onclick="loadOverviewToken(0)">全部</button>
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">加载中...</div>
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">总 Token</div><div class="value">${formatTokens(s.total_tokens)}</div></div>
481
- <div class="ag-stat-card"><div class="label">输入 Token</div><div class="value">${formatTokens(s.total_prompt_tokens)}</div></div>
482
- <div class="ag-stat-card"><div class="label">输出 Token</div><div class="value">${formatTokens(s.total_completion_tokens)}</div></div>
483
- <div class="ag-stat-card"><div class="label">LLM 调用次数</div><div class="value">${s.total_calls}</div></div>
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} 次, 输入 ${formatTokens(v.prompt_tokens)}, 输出 ${formatTokens(v.completion_tokens)})</span></div>`;
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 || '<div class="ag-empty">无分项数据</div>';
499
- } catch { el('overview-token-summary').innerHTML = '<div class="ag-empty">无数据</div>'; }
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} 已${!cur ? '启用' : '禁用'}`);
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}')">+ 新增规则</button></div>`;
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} 启用</span></div>
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 += '<div class="ag-empty">无规则</div>';
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' : ''}>仅拦截写入</option>
603
- <option value="deny_read" ${(r.deny_mode || 'deny_write') === 'deny_read' ? 'selected' : ''}>仅拦截读取</option>
604
- <option value="deny_both" ${(r.deny_mode || 'deny_write') === 'deny_both' ? 'selected' : ''}>读写均拦截</option>
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 ? '此规则已从默认规则集移除,可删除清理' : '删除自定义规则'}">删除</button>` : '';
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 = '<div class="ag-card"><div class="ag-empty">无 agent 定制规则</div></div>';
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} 启用</span></div>
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 += '<div class="ag-empty">无规则</div>';
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}')">+ 新增</button></div>`;
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 ? '<span class="ag-badge ag-badge-builtin" title="源码内置规则">内置</span>' : '<span class="ag-badge ag-badge-custom">自定义</span>')
710
- + (r.source_removed ? '<span class="ag-badge ag-badge-removed" title="此规则已从默认规则集移除,仅作历史保留">已移除</span>' : '')
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 ? '触发关键词' : (r.pattern ? '规则' : '规则')));
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,'&quot;')})">删除</button>`;
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,'&quot;')})">${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(`删除规则 ${r.value || r.pattern || r.keyword || r.path || ''}?`)) return;
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(`新增 ${agentId} / ${cat} 规则`, body, true);
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} 启用</span></div>
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 += '<div class="ag-empty">无 skill</div>';
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}')">删除</button>` : '';
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">触发关键词:</span> ${escapeHtml(kw)}</div>
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}')">详情</button>${deleteBtn}</div>
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 详情', '<p style="color:var(--ag-text-tertiary);padding:8px 0">加载中...</p>', false);
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 = '<p style="color:var(--ag-red);padding:8px 0">加载失败: ' + escapeHtml(e.message) + '</p>';
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> 行</span></div><div class="ag-skill-content">';
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">全局规则搜索</div>
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()">搜索</button>
848
- <button class="ag-btn ag-btn-ghost" onclick="clearSearch()">清空</button>
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">搜索范围:L1 规则的正则/原因/风险类型、L2 规则的路径/描述、L3 Skill 的关键词/ID。命中后可直接启用或禁用对应规则。</div>
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">搜索结果 <span class="ag-cat-count" id="search-count">0 条</span></div>
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 = '<div class="ag-empty">未找到匹配的规则</div>'; return; }
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' : ''}>仅拦截写入</option>
894
- <option value="deny_read" ${(r.deny_mode || 'deny_write') === 'deny_read' ? 'selected' : ''}>仅拦截读取</option>
895
- <option value="deny_both" ${(r.deny_mode || 'deny_write') === 'deny_both' ? 'selected' : ''}>读写均拦截</option>
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">分类: ${escapeHtml(r.category)}</span>
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">规则:</span> ${escapeHtml(ruleText)}</div>
907
- ${reason ? '<div class="ag-rule-reason"><span class="ag-rule-field-label">原因:</span> ' + escapeHtml(reason) + '</div>' : ''}
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">Token 用量详细统计
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)">今日</button>
934
- <button class="ag-btn ag-btn-sm" data-days="7" onclick="loadTokenStats(7)">近7天</button>
935
- <button class="ag-btn ag-btn-sm" data-days="30" onclick="loadTokenStats(30)">近30天</button>
936
- <button class="ag-btn ag-btn-sm" data-days="0" onclick="loadTokenStats(0)">全部</button>
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">至</span>
940
- <input type="date" id="token-date-to" class="ag-date-input" title="结束日期">
941
- <button class="ag-btn ag-btn-sm" onclick="loadTokenStatsByRange()">查询</button>
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">加载中...</div>
956
+ <div id="token-summary" class="ag-empty">${t('加载中...', 'Loading...')}</div>
945
957
  </div>
946
- <div class="ag-card"><div class="ag-section-title">按步骤分组</div><div id="token-by-step"></div></div>
947
- <div class="ag-card"><div class="ag-section-title">按模型分组</div><div id="token-by-model"></div></div>
948
- <div class="ag-card"><div class="ag-section-title">Token 消耗趋势(按模型)</div><div id="token-trend-chart"></div></div>
949
- <div class="ag-card"><div class="ag-section-title">最近调用记录</div><div id="token-recent" class="ag-empty">加载中...</div></div>`;
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">总 Token</div><div class="value">' + formatTokens(s.total_tokens) + '</div></div>' +
996
- '<div class="ag-stat-card"><div class="label">输入 Token</div><div class="value">' + formatTokens(s.total_prompt_tokens) + '</div></div>' +
997
- '<div class="ag-stat-card"><div class="label">输出 Token</div><div class="value">' + formatTokens(s.total_completion_tokens) + '</div></div>' +
998
- '<div class="ag-stat-card"><div class="label">调用次数</div><div class="value">' + s.total_calls + '</div></div></div>';
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 || '<div class="ag-empty">无分项数据</div>';
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 || '<div class="ag-empty">无分模型数据</div>';
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 = '<div class="ag-empty">暂无记录</div>'; return; }
1075
- let html = '<table class="ag-table"><tr><th>时间</th><th>步骤</th><th>模型</th><th>输入</th><th>输出</th><th>总计</th></tr>';
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 = '<div class="ag-empty">加载失败</div>'; }
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 = '<div class="ag-empty">当前没有环境变量覆盖</div>';
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">环境变量覆盖</div>
1106
- <div class="ag-section-desc">环境变量优先级最高,会覆盖 Web UI 的设置。清除环境变量后 Web 设置才能生效。</div>
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">配置文件路径</div>
1122
+ <div class="ag-section-title">${t('配置文件路径', 'Config File Path')}</div>
1111
1123
  <div style="font-size:13px;line-height:1.8">
1112
- <p>配置文件: ~/.config/agentmoss/agent_moss_runtime.json</p>
1113
- <p>环境变量 AGENT_MOSS_RUNTIME_CONFIG_PATH 可指定自定义路径</p>
1114
- <p>优先级: 环境变量 > runtime JSON > config.json > 源码默认值</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">访问 Token</div>
1119
- <div class="ag-section-desc">此处设置的是浏览器本地 token,console 调用 API 时以 Authorization: Bearer &lt;token&gt; 携带。服务端是否要求认证取决于环境变量 AGENT_MOSS_CONSOLE_TOKEN:未设置则本机免认证;已设置则请求须携带相同值才放行。当前本地 token:${getToken() ? '已设置' : '未设置'}</div>
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('已保存')">保存</button></div>
1130
+ <div class="ag-section-title">${t('访问 Token', 'Access Token')}</div>
1131
+ <div class="ag-section-desc">${t('此处设置的是浏览器本地 token,console 调用 API 时以 Authorization: Bearer &lt;token&gt; 携带。服务端是否要求认证取决于环境变量 AGENT_MOSS_CONSOLE_TOKEN:未设置则本机免认证;已设置则请求须携带相同值才放行。当前本地 token:', 'This sets the browser-local token used as Authorization: Bearer &lt;token&gt; 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">危险操作</div>
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'))}">重置规则到出厂默认</button>
1126
- <button class="ag-btn ag-btn-danger" style="margin-left:8px" onclick="resetTokenStats()">清除 Token 统计</button>
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">规则类型<span class="ag-required">*</span></div><select class="ag-select" id="rule-type" onchange="updateRuleForm()"><option value="pattern">正则模式匹配</option><option value="keyword">关键词匹配</option></select><div class="ag-form-hint">正则模式:用正则表达式匹配命令(如 <code>\\brm\\s+-rf</code>);关键词匹配:命令中包含指定文字即命中。</div></div>
1138
- <div class="ag-form-group" id="form-pattern"><div class="ag-form-label">正则 Pattern<span class="ag-required">*</span></div><input class="ag-input" id="input-pattern" placeholder="如: \\brm\\s+-rf"><div class="ag-form-hint">Python 正则表达式,命中该正则的命令会触发本规则。</div></div>
1139
- <div class="ag-form-group" id="form-keyword" style="display:none"><div class="ag-form-label">关键词<span class="ag-required">*</span></div><input class="ag-input" id="input-keyword" placeholder="如: ignore previous instructions"><div class="ag-form-hint">命令文本中包含此关键词即命中(不区分大小写)。</div></div>
1140
- <div class="ag-form-group"><div class="ag-form-label">风险等级<span class="ag-required">*</span></div><select class="ag-select" id="input-risk-level"><option value="critical">Critical(严重)</option><option value="high">High(高危)</option><option value="medium">Medium(中危)</option><option value="low">Low(低危)</option></select><div class="ag-form-hint"><strong>critical / high</strong>:在 L1 层直接拦截(Deny),命令不会执行;<br><strong>medium / low</strong>:不在 L1 拦截,转交 L3 LLM 深度分析判断。</div></div>
1141
- <div class="ag-form-group"><div class="ag-form-label">风险类型</div><input class="ag-input" id="input-risk-type" placeholder="如: script_execution, file_access, data_exfiltration"><div class="ag-form-hint">风险分类标签,用于统计与展示。可选值:script_execution(脚本执行)、file_access(文件访问)、data_exfiltration(数据外泄)等,可自定义。</div></div>
1142
- <div class="ag-form-group"><div class="ag-form-label">拦截原因<span class="ag-required">*</span></div><input class="ag-input" id="input-reason" placeholder="描述为什么这条规则要拦截"><div class="ag-form-hint">命中时展示给用户的说明文字,应写清为什么危险。L3 分析时也会参考此原因。</div></div>
1143
- <div class="ag-form-group"><div class="ag-form-label">语言(仅关键词类型)</div><select class="ag-select" id="input-lang"><option value="en">English</option><option value="zh">中文</option></select><div class="ag-form-hint">关键词的语言,影响匹配时的分词处理。</div></div>
1144
- <div class="ag-form-group"><div class="ag-form-label">所属分类</div><select class="ag-select" id="input-category"><option value="user_custom">用户自定义</option><option value="dangerous_commands">危险命令检测</option><option value="injection_detection">注入攻击检测</option></select></div>`;
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">规则类型<span class="ag-required">*</span></div><select class="ag-select" id="rule-type-l2" onchange="updateL2RuleForm()"><option value="path">敏感路径</option><option value="pattern">正则模式</option></select><div class="ag-form-hint">敏感路径:匹配文件路径(如 <code>/etc/shadow</code>);正则模式:用正则匹配命令文本。</div></div>
1147
- <div class="ag-form-group" id="form-l2-path"><div class="ag-form-label">路径<span class="ag-required">*</span></div><input class="ag-input" id="input-l2-path" placeholder="如: /etc/shadow, .ssh/id_rsa"><div class="ag-form-hint">要拦截的文件路径,多个用英文逗号分隔。命令访问到这些路径即命中。</div></div>
1148
- <div class="ag-form-group" id="form-l2-pattern" style="display:none"><div class="ag-form-label">正则 Pattern<span class="ag-required">*</span></div><input class="ag-input" id="input-l2-pattern" placeholder="如: \\bpasswd\\s+--stdin"><div class="ag-form-hint">Python 正则表达式,命中即触发。</div></div>
1149
- <div class="ag-form-group"><div class="ag-form-label">描述<span class="ag-required">*</span></div><input class="ag-input" id="input-l2-desc" placeholder="路径/规则的中文描述"><div class="ag-form-hint">命中时展示给用户的说明。</div></div>
1150
- <div class="ag-form-group"><div class="ag-form-label">风险等级<span class="ag-required">*</span></div><select class="ag-select" id="input-l2-risk-level"><option value="critical">Critical(严重)</option><option value="high">High(高危)</option><option value="medium">Medium(中危)</option><option value="low">Low(低危)</option></select><div class="ag-form-hint"><strong>critical / high</strong>:在 L2 层直接拦截;<br><strong>medium / low</strong>:不在 L2 拦截,转交 L3 LLM 判断。</div></div>
1151
- <div class="ag-form-group"><div class="ag-form-label">拦截模式</div><select class="ag-select" id="input-l2-deny-mode"><option value="deny_write">仅拦截写入</option><option value="deny_read">仅拦截读取</option><option value="deny_both">读写均拦截</option></select><div class="ag-form-hint">选择该路径的拦截模式:仅拦截写入(默认)、仅拦截读取(如 /dev/random)、读写均拦截(凭据文件)。</div></div>
1152
- <div class="ag-form-group"><div class="ag-form-label">所属分类</div><select class="ag-select" id="input-l2-category"><option value="sensitive_path_access">敏感路径访问</option><option value="password_modify_consent">密码修改授权</option><option value="user_deletion_consent">用户删除授权</option><option value="intent_consistency">意图一致性</option></select></div>`;
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">Skill ID(文件名)</div><input class="ag-input" id="input-skill-id" placeholder="如: my_custom_guard"><div class="ag-form-hint">Skill 文件名(不含扩展名),英文+下划线。会成为该 Skill 的唯一标识。</div></div>
1159
- <div class="ag-form-group"><div class="ag-form-label">触发关键词<span class="ag-required">*</span></div><input class="ag-input" id="input-skill-keywords" placeholder="逗号分隔, 如: custom, 特殊, special"><div class="ag-form-hint">命令/动作中包含这些关键词时,该 Skill 会被匹配并注入 L3 的 LLM prompt。</div></div>
1160
- <div class="ag-form-group"><div class="ag-form-label">Skill 内容(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">Markdown 格式的规则说明。命中关键词后会作为提示注入 LLM。</div></div>
1161
- <div class="ag-form-group"><div class="ag-form-label">所属分类</div><select class="ag-select" id="input-skill-category"><option value="user_custom">用户自定义</option><option value="file_operations">文件操作</option><option value="network_security">网络安全</option><option value="intent_and_general">意图/通用</option></select></div>`);
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 t = document.getElementById('rule-type')?.value;
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 = t === 'pattern' ? '' : 'none';
1167
- if (k) k.style.display = t === 'keyword' ? '' : 'none';
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 t = document.getElementById('rule-type-l2')?.value;
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 = t === 'path' ? '' : 'none';
1173
- if (k) k.style.display = t === 'pattern' ? '' : 'none';
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(t => {
1231
- t.onclick = () => {
1242
+ document.querySelectorAll('.ag-tab[data-page]').forEach(tab => {
1243
+ tab.onclick = () => {
1232
1244
  stopTokenAutoRefresh();
1233
- state.currentPage = t.dataset.page;
1245
+ state.currentPage = tab.dataset.page;
1234
1246
  document.querySelectorAll('.ag-tab').forEach(x => x.classList.remove('active'));
1235
- t.classList.add('active');
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
- refreshConfig().catch(e => { document.body.innerHTML = '<div style="padding:40px;color:#FF3B30">加载失败: ' + e.message + '</div>'; });
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenhkl/agent-moss",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "Multi-layer security analysis engine for AI agents (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",