argo-search 2.9.7 → 2.9.9

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 (154) hide show
  1. package/README.en.md +73 -150
  2. package/README.es.md +73 -150
  3. package/README.ja.md +79 -156
  4. package/README.ko.md +78 -155
  5. package/README.md +80 -159
  6. package/SKILL.md +3 -3
  7. package/assets/readme/hero.svg +2 -2
  8. package/backends/domain_profiles.json +48 -0
  9. package/backends/engine_registry.yaml +118 -4
  10. package/backends/quota_profiles.json +92 -12
  11. package/backends/source_types_cn.json +27 -1
  12. package/bin/argo +78 -5
  13. package/bin/argo.js +33 -2
  14. package/config.yaml +269 -63
  15. package/docs/ARGO_INTRO.md +5 -3
  16. package/docs/ENGINE_CATALOG.md +50 -39
  17. package/docs/MCP_SETUP.md +3 -1
  18. package/docs/RELEASE_NOTES_v2.9.8.md +73 -0
  19. package/docs/RELEASE_NOTES_v2.9.9.md +78 -0
  20. package/engines/specs/cfets.yaml +41 -0
  21. package/engines/specs/defillama.yaml +38 -0
  22. package/engines/specs/firecrawl.yaml +1 -1
  23. package/engines/specs/fxtwitter.yaml +5 -0
  24. package/engines/specs/gleif.yaml +55 -0
  25. package/engines/specs/hkex.yaml +40 -0
  26. package/engines/specs/keenable_free.yaml +36 -0
  27. package/engines/specs/shfe.yaml +39 -0
  28. package/engines/specs/stackexchange.yaml +2 -2
  29. package/engines/specs/tavily_free.yaml +56 -0
  30. package/engines/specs/yuanbao.yaml +93 -0
  31. package/mcp/clients.yaml +32 -0
  32. package/package.json +7 -2
  33. package/packages/dsh-plugin/README.md +25 -6
  34. package/packages/dsh-plugin/dsh/client.js +402 -47
  35. package/packages/dsh-plugin/dsh/diagnostics.js +97 -0
  36. package/packages/dsh-plugin/dsh/index.js +135 -1876
  37. package/packages/dsh-plugin/dsh/native-tools.mjs +7 -1
  38. package/packages/dsh-plugin/dsh/runner.js +666 -0
  39. package/packages/dsh-plugin/dsh/wide-research.js +878 -0
  40. package/packages/dsh-plugin/package.json +3 -3
  41. package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
  42. package/packages/dsh-plugin/test/client.test.js +177 -2
  43. package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
  44. package/packages/dsh-plugin/test/index.test.js +44 -0
  45. package/scripts/adaptive.py +126 -43
  46. package/scripts/article_meta.py +131 -0
  47. package/scripts/bench_tail_ab.py +281 -0
  48. package/scripts/bounded_run.py +91 -49
  49. package/scripts/budget.py +247 -0
  50. package/scripts/budget_cli.py +93 -0
  51. package/scripts/build_release_assets.sh +65 -0
  52. package/scripts/cache.py +60 -72
  53. package/scripts/ci_drift_gate.py +90 -0
  54. package/scripts/clock_bridge.py +22 -0
  55. package/scripts/content_boundary.py +46 -0
  56. package/scripts/content_security.py +119 -4
  57. package/scripts/content_signals.py +26 -5
  58. package/scripts/cross_lingual_trace.py +293 -0
  59. package/scripts/dispatch_result.py +105 -0
  60. package/scripts/engine_add.py +602 -0
  61. package/scripts/engine_dispatch.py +58 -86
  62. package/scripts/engine_env.py +5 -0
  63. package/scripts/engine_families.py +6 -0
  64. package/scripts/engine_policy.py +7 -7
  65. package/scripts/engines.py +21 -0
  66. package/scripts/engines_base.py +3 -3
  67. package/scripts/engines_builders.py +12 -0
  68. package/scripts/engines_builders_batch10.py +48 -3
  69. package/scripts/engines_builders_batch14.py +656 -0
  70. package/scripts/engines_builders_batch15.py +249 -0
  71. package/scripts/engines_builders_keyless.py +111 -0
  72. package/scripts/engines_builders_tech.py +0 -1
  73. package/scripts/evidence.py +31 -3
  74. package/scripts/evidence_loop.py +58 -22
  75. package/scripts/fetch_quality.py +13 -0
  76. package/scripts/fetch_v3.py +28 -3
  77. package/scripts/gen_routing_snapshot.py +11 -0
  78. package/scripts/http_client.py +24 -38
  79. package/scripts/install.ps1 +18 -0
  80. package/scripts/install.sh +16 -1
  81. package/scripts/job.py +3 -3
  82. package/scripts/local_seek.py +41 -5
  83. package/scripts/macro_countries.py +15 -0
  84. package/scripts/matrix_search_eval.py +192 -0
  85. package/scripts/mcp_compact.py +234 -0
  86. package/scripts/mcp_handlers.py +60 -642
  87. package/scripts/mcp_launch.sh +10 -8
  88. package/scripts/mcp_local.py +241 -0
  89. package/scripts/mcp_server.py +19 -14
  90. package/scripts/mcp_setup.py +33 -9
  91. package/scripts/mcp_tools.py +1 -0
  92. package/scripts/mcp_transport.py +1 -1
  93. package/scripts/mcp_warm.py +127 -0
  94. package/scripts/net_proxy.py +27 -0
  95. package/scripts/output_transparency.py +200 -0
  96. package/scripts/paid_policy.py +98 -0
  97. package/scripts/quota.py +158 -13
  98. package/scripts/recovery.py +7 -3
  99. package/scripts/recovery_runner.py +150 -0
  100. package/scripts/replay_eval.py +18 -0
  101. package/scripts/research.py +37 -21
  102. package/scripts/research_expand.py +24 -1
  103. package/scripts/retry_policy.py +76 -0
  104. package/scripts/route.py +6 -6
  105. package/scripts/route_cache.py +18 -0
  106. package/scripts/route_combo.py +26 -5
  107. package/scripts/route_domains.py +170 -31
  108. package/scripts/safe_conn.py +52 -0
  109. package/scripts/search.py +15 -27
  110. package/scripts/search_attribution.py +100 -3
  111. package/scripts/search_cli.py +24 -11
  112. package/scripts/search_entry.py +8 -1
  113. package/scripts/search_output.py +99 -15
  114. package/scripts/search_pipeline.py +62 -195
  115. package/scripts/search_rank.py +6 -0
  116. package/scripts/search_types.py +107 -9
  117. package/scripts/semantic_evidence.py +179 -3
  118. package/scripts/serp_guard.py +138 -7
  119. package/scripts/serp_kill_log.py +25 -2
  120. package/scripts/stats_cli.py +13 -1
  121. package/scripts/tail_completion.py +212 -0
  122. package/scripts/url_safety.py +126 -3
  123. package/scripts/usage_log.py +45 -0
  124. package/scripts/wide_research.py +45 -0
  125. package/scripts/zhihu_quota.py +139 -0
  126. package/sub-skills/local-search/bm25.py +27 -3
  127. package/sub-skills/local-search/cli_runner.py +331 -0
  128. package/sub-skills/local-search/parsers.py +328 -0
  129. package/sub-skills/local-search/search_v3.py +31 -674
  130. package/sub-skills/local-seek/scripts/seek_rank.py +7 -3
  131. package/assets/readme/settings-keys.png +0 -0
  132. package/assets/readme/settings-tools.png +0 -0
  133. package/sub-skills/ego-search/references/browser-runtime.md +0 -43
  134. package/sub-skills/ego-search/references/install.md +0 -67
  135. package/sub-skills/ego-search/references/original-ego-upgrade.md +0 -51
  136. package/sub-skills/ego-search/tests/test_query_normalize.py +0 -68
  137. package/sub-skills/js-run/tests/fixtures/challenge_like.js +0 -27
  138. package/sub-skills/js-run/tests/test_jsrun.py +0 -165
  139. package/sub-skills/local-seek/references/auto-behaviors.md +0 -24
  140. package/sub-skills/local-seek/references/fd-mdfind-recipes.md +0 -62
  141. package/sub-skills/local-seek/references/git-integration.md +0 -54
  142. package/sub-skills/local-seek/references/remote-handoff.md +0 -46
  143. package/sub-skills/local-seek/references/rg-recipes.md +0 -96
  144. package/sub-skills/local-seek/references/strategies.md +0 -52
  145. package/sub-skills/local-seek/references/structural-search.md +0 -59
  146. package/sub-skills/local-seek/tests/test_content_rank.py +0 -222
  147. package/sub-skills/local-seek/tests/test_determinism_20261006.py +0 -145
  148. package/sub-skills/local-seek/tests/test_noise_tier.py +0 -101
  149. package/sub-skills/local-seek/tests/test_pcre2_probe.py +0 -136
  150. package/sub-skills/local-seek/tests/test_rank_pinyin.py +0 -77
  151. package/sub-skills/local-seek/tests/test_regression_20261003.py +0 -169
  152. package/sub-skills/local-seek/tests/test_regression_20261006.py +0 -146
  153. package/sub-skills/local-seek/tests/test_seek_degradation.py +0 -111
  154. package/sub-skills/local-seek/tests/test_seek_fts.py +0 -313
@@ -46,6 +46,29 @@ window.__ModuleLoader__.load({
46
46
  tabKeys: '密钥',
47
47
  tabResearch: '研究',
48
48
  tabAdvanced: '高级',
49
+ tabDashboard: '看板',
50
+ dashboardTitle: '运行监控与健康态看板',
51
+ routeHealthTitle: '路由观测',
52
+ routeHealthHint: '最近的路由决策与引擎状态(宿主提供时显示)。',
53
+ routeHealthFallback: '在终端运行 `argo stats` 或 `argo engine list` 查看路由健康、引擎成功率与配额消耗。',
54
+ dashboardHint: '实时监控常驻 MCP 连接池、上游搜索源拓扑与额度状态,提供一键预设与快速调优。',
55
+ poolTitle: '常驻 MCP 连接池监控',
56
+ poolStatusHealthy: '就绪(正常)',
57
+ presetTitle: '工作流快速预设',
58
+ presetHint: '一键对齐常用工作场景的并发与引擎策略,免去逐项手动调优。',
59
+ presetFast: '极速日常',
60
+ presetFastDesc: '免密免费源优先,关闭 TF-IDF 重排与数值交叉比对,超低延迟',
61
+ presetStandard: '平衡生产(推荐)',
62
+ presetStandardDesc: '开启 TF-IDF 重排、数值交叉核验、主流付费源,标准超时',
63
+ presetDeep: '深度调研',
64
+ presetDeepDesc: '激活更多取证工具、对冲自适应宽限、语义证据层、更大并发',
65
+ presetApplied: '已应用预设:',
66
+ matrixTitle: '核心引擎拓扑与覆盖全景',
67
+ matrixHint: '当前可用引擎的接入层级与角色分工。带免密标识的引擎无需密钥即可直接使用。',
68
+ matrixConfigured: '密钥已就绪',
69
+ matrixKeyless: '免密层可用',
70
+ matrixPending: '待配密钥',
71
+ searchFilterPlaceholder: '输入关键词快速筛选...',
49
72
  summary: 'Argo · web_search {search} · web_fetch {fetch} · 原生工具 {tools} 个 · 研究 {workers}/{max}',
50
73
  accessTitle: '接入方式',
51
74
  searchSeam: '宿主 web_search 走 argo',
@@ -63,6 +86,8 @@ window.__ModuleLoader__.load({
63
86
  concurrency: '并发连接数',
64
87
  toolsTitle: '原生工具',
65
88
  toolsHint: '勾选即注册为原生一等工具(CLI 单发,不依赖 MCP)。长尾工具只在需要时开,避免每轮工具面膨胀。',
89
+ toolPresetTitle: '推荐组合',
90
+ toolPresetHint: '按场景一键套用(会覆盖当前勾选)。工具分区见下方。',
66
91
  coreTools: '高频路径',
67
92
  longTools: '长尾取证',
68
93
  nativeTimeout: '单次调用超时 (ms)',
@@ -109,6 +134,8 @@ window.__ModuleLoader__.load({
109
134
  groupNet: '网络出口',
110
135
  groupLocal: '本机检索',
111
136
  groupMisc: '其他',
137
+ groupPaid: '额度与预算',
138
+ groupPaidHint: '免费与付费源统一管理:金额计费的源设月度预算,按次数限额的源设日/月次数上限;0=不限制。额度或预算用尽后该源自动跳过并回落免费源。改动对下一次新建连接生效。',
112
139
  envDefault: '默认',
113
140
  envRange: '需在 {min}–{max} 之间',
114
141
  keySourceFile: '凭证库',
@@ -139,6 +166,29 @@ window.__ModuleLoader__.load({
139
166
  tabKeys: 'Keys',
140
167
  tabResearch: 'Research',
141
168
  tabAdvanced: 'Advanced',
169
+ tabDashboard: 'Dashboard',
170
+ dashboardTitle: 'Runtime Monitoring & Health Dashboard',
171
+ routeHealthTitle: 'Routing telemetry',
172
+ routeHealthHint: 'Recent routing decisions and engine status (when provided by the host).',
173
+ routeHealthFallback: 'Run `argo stats` or `argo engine list` in a terminal to inspect routing health, engine success rates and quota usage.',
174
+ dashboardHint: 'Real-time overview of the resident MCP connection pool, upstream search engine matrix, and quick workflow presets.',
175
+ poolTitle: 'Resident MCP Pool Monitoring',
176
+ poolStatusHealthy: 'Ready (Healthy)',
177
+ presetTitle: 'Workflow Presets',
178
+ presetHint: 'One-click presets tailored for common research and search workflows.',
179
+ presetFast: 'Fast & Free',
180
+ presetFastDesc: 'Keyless & free sources, disable rerank & crosscheck for lowest latency',
181
+ presetStandard: 'Balanced (Recommended)',
182
+ presetStandardDesc: 'TF-IDF rerank, numeric crosscheck, mainstream paid sources, standard timeouts',
183
+ presetDeep: 'Deep Research',
184
+ presetDeepDesc: 'Full forensic tools, adaptive hedge grace, semantic evidence layer, max concurrency',
185
+ presetApplied: 'Preset applied:',
186
+ matrixTitle: 'Core Engine Topology & Coverage',
187
+ matrixHint: 'Access tiers and roles of available engines. Keyless engines work without an API key.',
188
+ matrixConfigured: 'Key Ready',
189
+ matrixKeyless: 'Keyless Ready',
190
+ matrixPending: 'Key Required',
191
+ searchFilterPlaceholder: 'Filter by keyword or engine...',
142
192
  summary: 'Argo · web_search {search} · web_fetch {fetch} · {tools} native tools · research {workers}/{max}',
143
193
  accessTitle: 'Access',
144
194
  searchSeam: 'Route host web_search to argo',
@@ -156,6 +206,8 @@ window.__ModuleLoader__.load({
156
206
  concurrency: 'Concurrent connections',
157
207
  toolsTitle: 'Native tools',
158
208
  toolsHint: 'Checked tools register as native first-class tools (single-shot CLI, no MCP). Enable long-tail tools only when needed.',
209
+ toolPresetTitle: 'Recommended sets',
210
+ toolPresetHint: 'Apply a scenario preset in one click (replaces current selection). See the grouped tools below.',
159
211
  coreTools: 'Hot path',
160
212
  longTools: 'Long tail',
161
213
  nativeTimeout: 'Per-call timeout (ms)',
@@ -202,6 +254,8 @@ window.__ModuleLoader__.load({
202
254
  groupNet: 'Network',
203
255
  groupLocal: 'Local search',
204
256
  groupMisc: 'Misc',
257
+ groupPaid: 'Quotas & budgets',
258
+ groupPaidHint: 'Free and paid sources share one manager: money-metered sources take a monthly budget, count-capped sources take daily/monthly call caps; 0 = unlimited. When a budget or cap is exhausted, that source is skipped and free fallbacks take over. Changes apply to the next new connection.',
205
259
  envDefault: 'default',
206
260
  envRange: 'must be between {min} and {max}',
207
261
  keySourceFile: 'credential store',
@@ -215,29 +269,46 @@ window.__ModuleLoader__.load({
215
269
  },
216
270
  };
217
271
 
272
+ // 工具分区(场景化):核心 / 取证 / 本地 / 长尾。
273
+ // 旧版只有 core/long 两组,16 个长尾工具平铺,新用户不知道该开哪些;
274
+ // 按使用场景分组后,配合下方「推荐组合」卡可以一键套用。
218
275
  const TOOLS = [
219
- { id: 'argo_search', group: 'core', zh: '统一搜索', en: 'Unified search' },
220
- { id: 'argo_fetch', group: 'core', zh: '智能抓取', en: 'Smart fetch' },
221
- { id: 'argo_evidence', group: 'long', zh: '来源可信度评估', en: 'Source credibility' },
222
- { id: 'argo_local_search', group: 'long', zh: '本机文件搜索', en: 'Local file search' },
223
- { id: 'argo_local_read', group: 'long', zh: '本机文件读取', en: 'Local file read' },
224
- { id: 'argo_recompute', group: 'long', zh: '可复算执行器', en: 'Recompute' },
225
- { id: 'argo_clarify', group: 'long', zh: '意图消歧', en: 'Intent clarification' },
226
- { id: 'argo_crawl', group: 'long', zh: '站点级爬取', en: 'Site crawl' },
227
- { id: 'argo_social_search', group: 'long', zh: '社交平台搜索', en: 'Social search' },
228
- { id: 'argo_article', group: 'long', zh: '公众号全文', en: 'WeChat article' },
229
- { id: 'argo_job', group: 'long', zh: '招聘聚合', en: 'Job aggregation' },
230
- { id: 'argo_extract', group: 'long', zh: '结构化提取', en: 'Structured extract' },
231
- { id: 'argo_preflight', group: 'long', zh: '批量 URL 预检', en: 'URL preflight' },
232
- { id: 'argo_pdf', group: 'long', zh: 'PDF 提取', en: 'PDF extract' },
233
- { id: 'argo_screenshot', group: 'long', zh: '网页截图', en: 'Screenshot' },
234
- { id: 'argo_answer', group: 'long', zh: '带引用直答', en: 'Cited answer' },
235
- { id: 'argo_watch', group: 'long', zh: '网页变化监控', en: 'Page watch' },
236
- { id: 'argo_cite', group: 'long', zh: 'DOI 引用生成', en: 'DOI citation' },
276
+ { id: 'argo_search', group: 'core', zh: '统一搜索', en: 'Unified search', sceneZh: '日常一问的首选', sceneEn: 'Default for everyday queries' },
277
+ { id: 'argo_fetch', group: 'core', zh: '智能抓取', en: 'Smart fetch', sceneZh: '读某个具体页面', sceneEn: 'Read a specific page' },
278
+ { id: 'argo_evidence', group: 'forensics', zh: '来源可信度评估', en: 'Source credibility', sceneZh: '高后果问题的证据分级', sceneEn: 'Grade evidence for high-stakes answers' },
279
+ { id: 'argo_clarify', group: 'forensics', zh: '意图消歧', en: 'Intent clarification', sceneZh: '查询有歧义时先消歧', sceneEn: 'Disambiguate an ambiguous query' },
280
+ { id: 'argo_crawl', group: 'forensics', zh: '站点级爬取', en: 'Site crawl', sceneZh: '整站/多页采集', sceneEn: 'Site-level or multi-page collection' },
281
+ { id: 'argo_extract', group: 'forensics', zh: '结构化提取', en: 'Structured extract', sceneZh: '从页面抽表格/字段', sceneEn: 'Extract tables/fields from a page' },
282
+ { id: 'argo_pdf', group: 'forensics', zh: 'PDF 提取', en: 'PDF extract', sceneZh: '读 PDF 正文', sceneEn: 'Read PDF text' },
283
+ { id: 'argo_screenshot', group: 'forensics', zh: '网页截图', en: 'Screenshot', sceneZh: '取证留档/看渲染结果', sceneEn: 'Archive or inspect a rendered page' },
284
+ { id: 'argo_article', group: 'forensics', zh: '公众号全文', en: 'WeChat article', sceneZh: '读公众号文章正文', sceneEn: 'Read a WeChat article' },
285
+ { id: 'argo_local_search', group: 'local', zh: '本机文件搜索', en: 'Local file search', sceneZh: '搜本机笔记/代码', sceneEn: 'Search local notes/code' },
286
+ { id: 'argo_local_read', group: 'local', zh: '本机文件读取', en: 'Local file read', sceneZh: '读本机文件', sceneEn: 'Read a local file' },
287
+ { id: 'argo_recompute', group: 'local', zh: '可复算执行器', en: 'Recompute', sceneZh: '本地可复算任务(需授权)', sceneEn: 'Local recompute (needs authorization)' },
288
+ { id: 'argo_social_search', group: 'longtail', zh: '社交平台搜索', en: 'Social search', sceneZh: '舆情/UGC 采集', sceneEn: 'Sentiment / UGC collection' },
289
+ { id: 'argo_job', group: 'longtail', zh: '招聘聚合', en: 'Job aggregation', sceneZh: '招聘/岗位聚合', sceneEn: 'Job aggregation' },
290
+ { id: 'argo_preflight', group: 'longtail', zh: '批量 URL 预检', en: 'URL preflight', sceneZh: '批量检查 URL 可达性', sceneEn: 'Batch-check URL reachability' },
291
+ { id: 'argo_answer', group: 'longtail', zh: '带引用直答', en: 'Cited answer', sceneZh: '在给定语料里直答', sceneEn: 'Answer from a given corpus' },
292
+ { id: 'argo_watch', group: 'longtail', zh: '网页变化监控', en: 'Page watch', sceneZh: '盯页面变化', sceneEn: 'Watch a page for changes' },
293
+ { id: 'argo_cite', group: 'longtail', zh: 'DOI 引用生成', en: 'DOI citation', sceneZh: '生成引用条目', sceneEn: 'Generate a citation' },
294
+ ];
295
+
296
+ // 分区展示名 + 推荐组合(场景化,让用户一键套用而不是逐个勾选)
297
+ const TOOL_GROUPS = [
298
+ { id: 'core', zh: '高频路径', en: 'Hot path' },
299
+ { id: 'forensics', zh: '取证与核验', en: 'Forensics' },
300
+ { id: 'local', zh: '本机', en: 'Local' },
301
+ { id: 'longtail', zh: '长尾', en: 'Long tail' },
302
+ ];
303
+ const TOOL_PRESETS = [
304
+ { id: 'daily', zh: '日常', en: 'Daily', tools: ['argo_search', 'argo_fetch'], descZh: '一问一搜,最快', descEn: 'Fastest for simple queries' },
305
+ { id: 'research', zh: '研究', en: 'Research', tools: ['argo_search', 'argo_fetch', 'argo_evidence', 'argo_clarify'], descZh: '加证据分级与消歧', descEn: 'Add evidence grading + disambiguation' },
306
+ { id: 'forensics', zh: '取证', en: 'Forensics', tools: ['argo_search', 'argo_fetch', 'argo_crawl', 'argo_pdf', 'argo_screenshot', 'argo_extract'], descZh: '整站/多页/PDF 留档', descEn: 'Site/multi-page/PDF archiving' },
237
307
  ];
238
308
 
239
309
  const KEYS = [
240
310
  { ref: 'DEEPSEEK_API_KEY', common: true, zh: 'DeepSeek 官方搜索', en: 'DeepSeek official search' },
311
+ { ref: 'ARGO_YUANBAO_API_KEY', common: true, zh: '腾讯元宝搜索', en: 'Tencent Yuanbao' },
241
312
  { ref: 'TAVILY_API_KEY', common: true, zh: 'Tavily', en: 'Tavily' },
242
313
  { ref: 'EXA_API_KEY', common: true, zh: 'Exa', en: 'Exa' },
243
314
  { ref: 'BOCHA_API_KEY', common: true, zh: '博查', en: 'Bocha' },
@@ -261,12 +332,25 @@ window.__ModuleLoader__.load({
261
332
  { ref: 'UNPAYWALL_EMAIL', zh: 'Unpaywall 邮箱(非密钥)', en: 'Unpaywall email (not secret)' },
262
333
  ];
263
334
 
335
+ const CORE_ENGINES = [
336
+ { id: 'deepseek', nameZh: 'DeepSeek 搜索', nameEn: 'DeepSeek Search', roleZh: '通用搜索 / 官方对齐', roleEn: 'General / Official Seam', ref: 'DEEPSEEK_API_KEY', keyless: false },
337
+ { id: 'yuanbao', nameZh: '腾讯元宝', nameEn: 'Tencent Yuanbao', roleZh: '中文对冲主力', roleEn: 'Chinese General Primary', ref: 'ARGO_YUANBAO_API_KEY', keyless: false },
338
+ { id: 'tavily', nameZh: 'Tavily Search', nameEn: 'Tavily Search', roleZh: 'AI 知识增强检索', roleEn: 'AI Knowledge Retrieval', ref: 'TAVILY_API_KEY', keyless: false },
339
+ { id: 'exa', nameZh: 'Exa Neural', nameEn: 'Exa Neural', roleZh: '神经语义精准搜索', roleEn: 'Neural Semantic Search', ref: 'EXA_API_KEY', keyless: false },
340
+ { id: 'brave', nameZh: 'Brave Search', nameEn: 'Brave Search', roleZh: '独立全球 Web 索引', roleEn: 'Independent Global Index', ref: 'BRAVE_API_KEY', keyless: true },
341
+ { id: 'bocha', nameZh: '博查 Bocha', nameEn: 'Bocha', roleZh: '垂直多源聚合', roleEn: 'Vertical Multi-source', ref: 'BOCHA_API_KEY', keyless: false },
342
+ { id: 'jina', nameZh: 'Jina Reader', nameEn: 'Jina Reader', roleZh: '正文高保真提取', roleEn: 'Article Text Extraction', ref: '', keyless: true },
343
+ { id: 'tinyfish', nameZh: 'TinyFish', nameEn: 'TinyFish', roleZh: '无头渲染抓取兜底', roleEn: 'Headless Render Fallback', ref: 'TINYFISH_API_KEY', keyless: true },
344
+ { id: 'zhihu', nameZh: '知乎 Zhihu', nameEn: 'Zhihu', roleZh: '中文专业问答舆情', roleEn: 'Professional Q&A / Social', ref: 'ZHIHU_ACCESS_SECRET', keyless: true },
345
+ ];
346
+
264
347
  // argo 引擎开关(argoEnv 的 KEY=VALUE 行)。与 index.js 的 ARGO_ENV_SPECS
265
348
  // 同一份名单,由 test/client.test.js 的漂移门禁核对;kind 决定控件:
266
- // bool=开关(invert 语义反转,如 NO_CACHE 是「关缓存」)、num=数字、
267
- // str=文本、list=名单。def 仅作「未配置时显示什么」,缺省即不写该行。
349
+ // bool=开关、num=数字、str=文本、list=名单。def 仅作「未配置时显示什么」,
350
+ // 缺省即不写该行。开关文案写正向行为或明确的关闭动作,不再用 invert 反转
351
+ // (历史 bug 见 index.js 注释)。
268
352
  const ENGINE_TOGGLES = [
269
- { name: 'ARGO_NO_CACHE', kind: 'bool', invert: true, group: 'cache', def: false, zh: '关闭搜索结果缓存', en: 'Disable result cache', hintZh: 'argo 默认缓存 1 小时;调试期关掉缓存看实时结果', hintEn: 'argo caches results for 1h by default; turn off to see live results' },
353
+ { name: 'ARGO_NO_CACHE', kind: 'bool', group: 'cache', def: false, zh: '测试模式(关闭结果缓存)', en: 'Test mode (disable result cache)', hintZh: '日常保持关闭:结果缓存 1 小时。改进 Skill/插件、需要看实时结果时再打开', hintEn: 'Off by default: results are cached for 1h. Enable only when developing the skill/plugin' },
270
354
  { name: 'ARGO_TFIDF_RERANK', kind: 'bool', group: 'cache', def: true, zh: 'TF-IDF 重排', en: 'TF-IDF rerank', hintZh: '多引擎结果融合前的相关性重排,默认开', hintEn: 'Relevance rerank before fusion, on by default' },
271
355
  { name: 'ARGO_NUMBER_CROSSCHECK', kind: 'bool', group: 'cache', def: true, zh: '数值交叉校验', en: 'Number cross-check', hintZh: '对查询里的数值主张做跨源核对,默认开', hintEn: 'Cross-check numeric claims across sources, on by default' },
272
356
  { name: 'ARGO_MINHASH_DEDUPE', kind: 'bool', group: 'cache', def: true, zh: 'MinHash 去重', en: 'MinHash dedupe', hintZh: '近重复结果合并,默认开', hintEn: 'Merge near-duplicate results, on by default' },
@@ -289,7 +373,14 @@ window.__ModuleLoader__.load({
289
373
  { name: 'ARGO_LOCAL_SEEK_PATH', kind: 'str', group: 'local', zh: 'seek.py 绝对路径', en: 'seek.py path', hintZh: '显式指定 seek.py,优先于根目录候选', hintEn: 'Explicit seek.py, wins over root candidates' },
290
374
  { name: 'ARGO_LOCAL_READ_DIRS', kind: 'list', group: 'local', zh: '本机读取白名单', en: 'Local read allowlist', hintZh: '逗号分隔;不配置则 local_read 一律不放行', hintEn: 'Comma-separated; empty means local_read allows nothing' },
291
375
  { name: 'ARGO_TEXT_LEXICON', kind: 'str', group: 'local', zh: '词表 JSON 路径', en: 'Lexicon JSON path', hintZh: '文本处理词表覆盖,默认用打包词表', hintEn: 'Text lexicon override; default uses the bundled one' },
292
- { name: 'ARGO_NO_PREWARM', kind: 'bool', invert: true, group: 'misc', def: false, zh: '关闭连接预热', en: 'Disable prewarm', hintZh: 'MCP 握手后预热引擎注册表,默认开', hintEn: 'Prewarm the engine registry after handshake, on by default' },
376
+ { name: 'ARGO_PAID_SEARCH', kind: 'str', group: 'paid', def: 'auto', zh: '付费源策略', en: 'Paid source policy', hintZh: 'auto=优势域自动使用付费源;off=只用免费源。额度见下方', hintEn: 'auto = use paid sources in proven domains; off = free sources only. See quotas below' },
377
+ { name: 'ARGO_BUDGET_DEEPSEEK_SEARCH', kind: 'num', group: 'paid', def: 5, min: 0, max: 1000, step: 1, zh: 'DeepSeek 月度预算(元)', en: 'DeepSeek monthly budget (CNY)', hintZh: '0=关闭;¥5≈1000次成功调用(¥0.005/次)', hintEn: '0 = off; CNY 5 ≈ 1000 successful calls (CNY 0.005 each)' },
378
+ { name: 'ARGO_BUDGET_EXA', kind: 'num', group: 'paid', def: 10, min: 0, max: 10000, step: 1, zh: 'Exa 每月预算(美元)', en: 'Exa monthly budget (USD)', hintZh: '默认 $10/月(约 1000 次);0=关闭自动使用。按成功调用计费', hintEn: 'Default $10/month (~1000 calls); 0 = off. Billed per successful call' },
379
+ { name: 'ARGO_QUOTA_ANYSEARCH_LIMIT', kind: 'num', group: 'paid', def: 1000, min: 0, max: 1000000, step: 1, zh: 'AnySearch 每日次数上限', en: 'AnySearch daily call cap', hintZh: '默认 1000/日;0=不限制。改动对下一次新建连接生效', hintEn: 'Default 1000/day; 0 = unlimited. Applies to new connections' },
380
+ { name: 'ARGO_QUOTA_ZHIHU_LIMIT', kind: 'num', group: 'paid', def: 5000, min: 0, max: 1000000, step: 1, zh: '知乎 每日次数上限', en: 'Zhihu daily call cap', hintZh: '默认 5000/日;0=不限制。改动对下一次新建连接生效', hintEn: 'Default 5000/day; 0 = unlimited. Applies to new connections' },
381
+ { name: 'ARGO_QUOTA_YUANBAO_LIMIT', kind: 'num', group: 'paid', def: 2000, min: 0, max: 1000000, step: 1, zh: '元宝 每月次数上限', en: 'Yuanbao monthly call cap', hintZh: '默认 2000/月;0=不限制。改动对下一次新建连接生效', hintEn: 'Default 2000/month; 0 = unlimited. Applies to new connections' },
382
+ { name: 'ARGO_QUOTA_TAVILY_LIMIT', kind: 'num', group: 'paid', def: 1000, min: 0, max: 1000000, step: 1, zh: 'Tavily 每月次数上限', en: 'Tavily monthly call cap', hintZh: '默认 1000/月;0=不限制。改动对下一次新建连接生效', hintEn: 'Default 1000/month; 0 = unlimited. Applies to new connections' },
383
+ { name: 'ARGO_NO_PREWARM', kind: 'bool', group: 'misc', def: false, zh: '关闭连接预热', en: 'Disable prewarm', hintZh: 'MCP 握手后预热引擎注册表,默认开;开启本项即关闭预热', hintEn: 'Prewarm the engine registry after handshake, on by default; enable this to turn it off' },
293
384
  { name: 'ARGO_USAGE_LOG', kind: 'bool', group: 'misc', def: true, zh: '使用日志', en: 'Usage log', hintZh: 'argo 侧 JSONL 使用账本,默认开', hintEn: 'argo JSONL usage ledger, on by default' },
294
385
  { name: 'ARGO_ENGINE_HTTP_CLIENT', kind: 'bool', group: 'misc', def: true, zh: 'HttpClient 连接池', en: 'HttpClient pool', hintZh: '引擎 HTTP 走连接池,默认开;=0 回退 urllib(诊断用)', hintEn: 'Pooled engine HTTP, on by default; =0 falls back to urllib' },
295
386
  { name: 'ARGO_ADMISSION_TTL_S', kind: 'num', group: 'misc', def: 1, min: 0, max: 60, step: 0.5, zh: '准入记忆 TTL(秒)', en: 'Admission memo TTL (s)', hintZh: '引擎准入状态记忆时长,默认 1;0 每次现查', hintEn: 'Admission memo window, default 1; 0 = always fresh' },
@@ -391,6 +482,43 @@ window.__ModuleLoader__.load({
391
482
  actions: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' },
392
483
  switchWrap: { display: 'flex', alignItems: 'center', gap: 8 },
393
484
  divider: { height: '.5px', background: 'var(--dsw-alias-border-l2, #eee)', margin: '2px 0' },
485
+ metricGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))', gap: 10 },
486
+ metricCard: {
487
+ display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 14px',
488
+ borderRadius: 10, border: '.5px solid var(--dsw-alias-border-l3, #e5e7eb)',
489
+ background: 'var(--dsw-alias-bg-layer-2, #f8fafc)',
490
+ },
491
+ metricLabel: { fontSize: 11, fontWeight: 500, color: 'var(--dsw-alias-label-tertiary, #7b818b)', textTransform: 'uppercase', letterSpacing: '0.04em' },
492
+ metricValue: { fontSize: 18, fontWeight: 600, lineHeight: '22px', color: 'var(--dsw-alias-label-primary, #1f2328)' },
493
+ metricSub: { fontSize: 11, lineHeight: '16px', color: 'var(--dsw-alias-label-secondary, #4b5563)' },
494
+ presetGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 10 },
495
+ presetCard: (active) => ({
496
+ display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 14px',
497
+ borderRadius: 10, cursor: 'pointer', textAlign: 'left',
498
+ border: active ? '1.5px solid var(--dsw-alias-state-business-primary, #3b6ef6)' : '.5px solid var(--dsw-alias-border-l3, #e5e7eb)',
499
+ background: active ? 'var(--dsw-alias-bg-layer-1, #fff)' : 'var(--dsw-alias-settings-card-fill, #fff)',
500
+ boxShadow: active ? '0 1px 3px rgba(59, 110, 246, 0.12)' : 'none',
501
+ }),
502
+ matrixGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 8 },
503
+ matrixCard: {
504
+ display: 'flex', flexDirection: 'column', gap: 4, padding: '10px 12px',
505
+ borderRadius: 8, border: '.5px solid var(--dsw-alias-border-l3, #e5e7eb)',
506
+ background: 'var(--dsw-alias-bg-layer-1, #fff)',
507
+ },
508
+ badge: (color) => ({
509
+ display: 'inline-flex', alignItems: 'center', gap: 4, padding: '2px 8px',
510
+ borderRadius: 999, fontSize: 10, fontWeight: 500, lineHeight: '14px',
511
+ background: color === 'green' ? 'rgba(46, 160, 67, 0.12)' : (color === 'blue' ? 'rgba(59, 110, 246, 0.12)' : 'rgba(100, 116, 139, 0.12)'),
512
+ color: color === 'green' ? '#2da44e' : (color === 'blue' ? '#3b6ef6' : '#64748b'),
513
+ border: '.5px solid ' + (color === 'green' ? '#2da44e' : (color === 'blue' ? '#3b6ef6' : '#64748b')),
514
+ }),
515
+ filterInput: {
516
+ width: '100%', boxSizing: 'border-box', font: 'inherit', fontSize: 12, lineHeight: '18px',
517
+ padding: '6px 10px', borderRadius: 8,
518
+ border: '.5px solid var(--dsw-alias-border-l4, #d0d7de)',
519
+ background: 'var(--dsw-alias-bg-layer-1, #fff)', color: 'inherit',
520
+ marginBottom: 8,
521
+ },
394
522
  };
395
523
 
396
524
  function asString(value, fallback) {
@@ -499,7 +627,7 @@ window.__ModuleLoader__.load({
499
627
  const initialLang = locale && locale.getSnapshot && locale.getSnapshot().active === 'en' ? 'en' : 'zh';
500
628
  const [lang, setLang] = useState(initialLang);
501
629
 
502
- const [tab, setTab] = useState('access');
630
+ const [tab, setTab] = useState('dashboard');
503
631
  const [draft, setDraft] = useState(() => toDraft(value));
504
632
  const [busy, setBusy] = useState('');
505
633
  const [message, setMessage] = useState('');
@@ -513,6 +641,10 @@ window.__ModuleLoader__.load({
513
641
  // argoEnv 引擎开关:bool 类即时写;num/str/list 类走草稿 + 分组保存。
514
642
  const [envDraft, setEnvDraft] = useState({});
515
643
  const [envBusy, setEnvBusy] = useState('');
644
+ const [engineFilter, setEngineFilter] = useState('');
645
+ const [keyFilter, setKeyFilter] = useState('');
646
+ // 路由观测(宿主经 remote 事件推送;无推送时卡片显示终端命令提示)
647
+ const [routeHealth, setRouteHealth] = useState(null);
516
648
 
517
649
  const dirty = useRef(new Set());
518
650
  // 读-改-写共享配置字段(keyRefs / nativeTools / argoEnv)时读实时快照,
@@ -568,10 +700,14 @@ window.__ModuleLoader__.load({
568
700
  useEffect(() => {
569
701
  if (props.view === 'summary') return undefined;
570
702
  void refreshCredentials();
571
- const off = props.remote && typeof props.remote.$on === 'function'
572
- ? props.remote.$on('credentials/reference-updated', () => { void refreshCredentials(); })
573
- : undefined;
574
- return typeof off === 'function' ? off : undefined;
703
+ const offs = [];
704
+ if (props.remote && typeof props.remote.$on === 'function') {
705
+ const offCreds = props.remote.$on('credentials/reference-updated', () => { void refreshCredentials(); });
706
+ const offRoute = props.remote.$on('route-health/updated', (data) => { setRouteHealth(data); });
707
+ if (typeof offCreds === 'function') offs.push(offCreds);
708
+ if (typeof offRoute === 'function') offs.push(offRoute);
709
+ }
710
+ return () => { for (const off of offs) off(); };
575
711
  }, [state]);
576
712
 
577
713
  const edit = (key, text) => {
@@ -674,6 +810,14 @@ window.__ModuleLoader__.load({
674
810
  void setField('nativeTools', ordered);
675
811
  };
676
812
 
813
+ // 推荐组合:一键套用一组工具(场景化分区之上的快捷入口)。
814
+ // 只增不减会覆盖用户已有勾选,所以是「设为这组」而非「追加这组」。
815
+ const applyToolPreset = async (tools) => {
816
+ const ordered = TOOLS.map((tool) => tool.id).filter((toolId) => tools.includes(toolId));
817
+ for (const extra of tools) if (!ordered.includes(extra)) ordered.push(extra);
818
+ await props.set('nativeTools', ordered);
819
+ };
820
+
677
821
  const saveKey = async (ref) => {
678
822
  const key = asString(keyDraft[ref]).trim();
679
823
  if (!key || !props.credentials) return;
@@ -737,8 +881,9 @@ window.__ModuleLoader__.load({
737
881
  const ENV_FALSY = ['', '0', 'false', 'no', 'off', 'n', 'disable', 'disabled', 'none'];
738
882
  const envBoolState = (spec, map) => {
739
883
  const raw = map[spec.name];
740
- const on = raw === undefined ? spec.def === true : !ENV_FALSY.includes(String(raw).trim().toLowerCase());
741
- return spec.invert ? !on : on;
884
+ return raw === undefined
885
+ ? spec.def === true
886
+ : !ENV_FALSY.includes(String(raw).trim().toLowerCase());
742
887
  };
743
888
  const writeEnvLines = async (updates) => {
744
889
  const map = envMap();
@@ -754,9 +899,8 @@ window.__ModuleLoader__.load({
754
899
  };
755
900
  const toggleEnv = (spec, on) => {
756
901
  if (disabled) return;
757
- // invert 语义反转:UI「开」对 invert 开关意味着 env 取 falsy
758
- // (缓存开 → ARGO_NO_CACHE=0;关 → =1)。
759
- const value = on === (spec.invert === true) ? '0' : '1';
902
+ // 开关状态直接落 0/1:文案与 env 同向(开=1,关=0),不做反转。
903
+ const value = on ? '1' : '0';
760
904
  void writeEnvLines({ [spec.name]: value })
761
905
  .then((ok) => report(ok, ok ? t('saved') : t('refused')))
762
906
  .catch((cause) => report(false, cause && cause.message ? cause.message : String(cause)));
@@ -795,6 +939,54 @@ window.__ModuleLoader__.load({
795
939
  }
796
940
  };
797
941
 
942
+ const applyPreset = async (type) => {
943
+ if (disabled) return;
944
+ setBusy('preset');
945
+ try {
946
+ const map = envMap();
947
+ const ops = [];
948
+ if (type === 'fast') {
949
+ map.ARGO_TFIDF_RERANK = '0';
950
+ map.ARGO_NUMBER_CROSSCHECK = '0';
951
+ map.ARGO_PAID_SEARCH = 'off';
952
+ map.ARGO_ADAPTIVE_GRACE = '1';
953
+ map.ARGO_SERIAL_STAGGER_S = '0.5';
954
+ ops.push({ op: 'set', path: ['searchTimeoutMs'], value: 15000 });
955
+ ops.push({ op: 'set', path: ['fetchTimeoutMs'], value: 15000 });
956
+ } else if (type === 'standard') {
957
+ map.ARGO_TFIDF_RERANK = '1';
958
+ map.ARGO_NUMBER_CROSSCHECK = '1';
959
+ map.ARGO_MINHASH_DEDUPE = '1';
960
+ map.ARGO_PAID_SEARCH = 'auto';
961
+ map.ARGO_ADAPTIVE_GRACE = '1';
962
+ map.ARGO_SERIAL_STAGGER_S = '0.8';
963
+ ops.push({ op: 'set', path: ['searchTimeoutMs'], value: 30000 });
964
+ ops.push({ op: 'set', path: ['fetchTimeoutMs'], value: 25000 });
965
+ } else if (type === 'deep') {
966
+ map.ARGO_TFIDF_RERANK = '1';
967
+ map.ARGO_NUMBER_CROSSCHECK = '1';
968
+ map.ARGO_MINHASH_DEDUPE = '1';
969
+ map.ARGO_SEMANTIC_EVIDENCE = '1';
970
+ map.ARGO_ADAPTIVE_GRACE = '1';
971
+ map.ARGO_PAID_SEARCH = 'auto';
972
+ ops.push({ op: 'set', path: ['searchTimeoutMs'], value: 45000 });
973
+ ops.push({ op: 'set', path: ['fetchTimeoutMs'], value: 35000 });
974
+ ops.push({ op: 'set', path: ['nativeTimeoutMs'], value: 60000 });
975
+ }
976
+ const envLines = Object.keys(map)
977
+ .filter((name) => ENV_NAMES.has(name))
978
+ .map((name) => `${name}=${map[name]}`);
979
+ ops.push({ op: 'set', path: ['argoEnv'], value: envLines });
980
+ const ok = await props.mutate(ops);
981
+ const presetName = type === 'fast' ? t('presetFast') : (type === 'standard' ? t('presetStandard') : t('presetDeep'));
982
+ report(ok, ok ? `${t('presetApplied')} ${presetName}` : t('refused'));
983
+ } catch (cause) {
984
+ report(false, cause && cause.message ? cause.message : String(cause));
985
+ } finally {
986
+ setBusy('');
987
+ }
988
+ };
989
+
798
990
  if (props.view === 'summary') {
799
991
  const summary = t('summary')
800
992
  .replace('{search}', value.searchProviderEnabled !== false ? t('on') : t('off'))
@@ -823,8 +1015,8 @@ window.__ModuleLoader__.load({
823
1015
  ? h('p', { style: error ? S.error : S.notice, role: 'status' }, message)
824
1016
  : h('p', { style: S.status }, disabled ? t('readonly') : '');
825
1017
 
826
- const coreTools = TOOLS.filter((tool) => tool.group === 'core');
827
- const longTools = TOOLS.filter((tool) => tool.group === 'long');
1018
+ // 按 TOOL_GROUPS 顺序分组渲染(场景化分区,替代旧的 core/long 两分)
1019
+ const toolsByGroup = (gid) => TOOLS.filter((tool) => tool.group === gid);
828
1020
  const toolCheckbox = (tool) => h('label', { key: tool.id, style: S.check }, [
829
1021
  h('input', {
830
1022
  key: 'i',
@@ -913,6 +1105,108 @@ window.__ModuleLoader__.load({
913
1105
 
914
1106
  const panel = [];
915
1107
 
1108
+ if (tab === 'dashboard') {
1109
+ const configuredKeyCount = Object.values(credentials).filter((c) => c && c.configured).length;
1110
+ const keylessEngineCount = CORE_ENGINES.filter((e) => e.keyless).length;
1111
+ const seamSearchOn = value.searchProviderEnabled !== false;
1112
+ const seamFetchOn = value.fetchProviderEnabled !== false;
1113
+ const activeToolCount = asArray(value.nativeTools).length;
1114
+ const poolConcurrency = asNumber(value.searchConcurrency, DEFAULTS.searchConcurrency);
1115
+ const poolIdleMs = asNumber(value.searchIdleMs, DEFAULTS.searchIdleMs);
1116
+
1117
+ panel.push(h('div', { key: 'metrics', style: S.metricGrid }, [
1118
+ h('div', { key: 'pool', style: S.metricCard }, [
1119
+ h('span', { key: 'l', style: S.metricLabel }, t('poolTitle')),
1120
+ h('div', { key: 'v', style: { display: 'flex', alignItems: 'center', gap: 6 } }, [
1121
+ h('span', { key: 'dot', style: S.dot(true) }),
1122
+ h('span', { key: 'val', style: S.metricValue }, `${poolConcurrency} 并发`),
1123
+ ]),
1124
+ h('span', { key: 's', style: S.metricSub }, `${t('poolStatusHealthy')} · 保活 ${poolIdleMs / 1000}s`),
1125
+ ]),
1126
+ h('div', { key: 'seam', style: S.metricCard }, [
1127
+ h('span', { key: 'l', style: S.metricLabel }, t('accessTitle')),
1128
+ h('span', { key: 'val', style: S.metricValue }, seamSearchOn && seamFetchOn ? '双路接管' : (seamSearchOn || seamFetchOn ? '单路接管' : '未接管')),
1129
+ h('span', { key: 's', style: S.metricSub }, `Search: ${seamSearchOn ? 'ON' : 'OFF'} · Fetch: ${seamFetchOn ? 'ON' : 'OFF'}`),
1130
+ ]),
1131
+ h('div', { key: 'tools', style: S.metricCard }, [
1132
+ h('span', { key: 'l', style: S.metricLabel }, t('toolsTitle')),
1133
+ h('span', { key: 'val', style: S.metricValue }, `${activeToolCount} / ${TOOLS.length}`),
1134
+ h('span', { key: 's', style: S.metricSub }, '已就绪原生 MCP 工具'),
1135
+ ]),
1136
+ h('div', { key: 'keys', style: S.metricCard }, [
1137
+ h('span', { key: 'l', style: S.metricLabel }, t('keysTitle')),
1138
+ h('span', { key: 'val', style: S.metricValue }, `${configuredKeyCount} 配置 · ${keylessEngineCount} 免密`),
1139
+ h('span', { key: 's', style: S.metricSub }, '覆盖全局多源引擎'),
1140
+ ]),
1141
+ ]));
1142
+
1143
+ // 路由观测卡:宿主经 remote 事件推送 routeHealth 时展示最近路由决策;
1144
+ // 未推送时给出终端命令提示——不让面板空白,也不假装有数据。
1145
+ panel.push(h(Card, { key: 'route', title: t('routeHealthTitle'), hint: t('routeHealthHint') }, [
1146
+ routeHealth && Array.isArray(routeHealth.samples) && routeHealth.samples.length > 0
1147
+ ? h('div', { key: 'rows', style: { display: 'flex', flexDirection: 'column', gap: 6 } },
1148
+ routeHealth.samples.slice(0, 5).map((row, i) =>
1149
+ h('div', { key: 'r' + i, style: S.keyRow }, [
1150
+ h('span', { key: 'q', style: S.label }, String(row.query || '').slice(0, 40)),
1151
+ h('span', { key: 'd', style: S.sub }, `${row.domain || '-'} · ${row.engine || '-'}`),
1152
+ ])))
1153
+ : h('p', { key: 'hint', style: S.hint }, t('routeHealthFallback')),
1154
+ ]));
1155
+
1156
+ panel.push(h(Card, { key: 'presets', title: t('presetTitle'), hint: t('presetHint') }, [
1157
+ h('div', { key: 'grid', style: S.presetGrid }, [
1158
+ h('div', {
1159
+ key: 'fast',
1160
+ style: S.presetCard(false),
1161
+ onClick: () => { void applyPreset('fast'); },
1162
+ }, [
1163
+ h('div', { key: 't', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } }, [
1164
+ h('span', { key: 'title', style: S.label }, t('presetFast')),
1165
+ h('span', { key: 'badge', style: S.badge('blue') }, '15s 超时'),
1166
+ ]),
1167
+ h('span', { key: 'd', style: S.sub }, t('presetFastDesc')),
1168
+ ]),
1169
+ h('div', {
1170
+ key: 'standard',
1171
+ style: S.presetCard(true),
1172
+ onClick: () => { void applyPreset('standard'); },
1173
+ }, [
1174
+ h('div', { key: 't', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } }, [
1175
+ h('span', { key: 'title', style: S.label }, t('presetStandard')),
1176
+ h('span', { key: 'badge', style: S.badge('green') }, '标准生产'),
1177
+ ]),
1178
+ h('span', { key: 'd', style: S.sub }, t('presetStandardDesc')),
1179
+ ]),
1180
+ h('div', {
1181
+ key: 'deep',
1182
+ style: S.presetCard(false),
1183
+ onClick: () => { void applyPreset('deep'); },
1184
+ }, [
1185
+ h('div', { key: 't', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } }, [
1186
+ h('span', { key: 'title', style: S.label }, t('presetDeep')),
1187
+ h('span', { key: 'badge', style: S.badge('blue') }, '深度核验'),
1188
+ ]),
1189
+ h('span', { key: 'd', style: S.sub }, t('presetDeepDesc')),
1190
+ ]),
1191
+ ]),
1192
+ ]));
1193
+
1194
+ panel.push(h(Card, { key: 'matrix', title: t('matrixTitle'), hint: t('matrixHint') }, [
1195
+ h('div', { key: 'grid', style: S.matrixGrid }, CORE_ENGINES.map((engine) => {
1196
+ const hasKey = engine.ref && credentials[engine.ref] && credentials[engine.ref].configured;
1197
+ const badgeType = hasKey ? 'green' : (engine.keyless ? 'blue' : 'gray');
1198
+ const badgeText = hasKey ? t('matrixConfigured') : (engine.keyless ? t('matrixKeyless') : t('matrixPending'));
1199
+ return h('div', { key: engine.id, style: S.matrixCard }, [
1200
+ h('div', { key: 'h', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 6 } }, [
1201
+ h('span', { key: 'n', style: S.label }, lang === 'en' ? engine.nameEn : engine.nameZh),
1202
+ h('span', { key: 'b', style: S.badge(badgeType) }, badgeText),
1203
+ ]),
1204
+ h('span', { key: 'r', style: S.sub }, lang === 'en' ? engine.roleEn : engine.roleZh),
1205
+ ]);
1206
+ })),
1207
+ ]));
1208
+ }
1209
+
916
1210
  if (tab === 'access') {
917
1211
  panel.push(h(Card, { key: 'seam', title: t('accessTitle') }, [
918
1212
  h(Switch, {
@@ -978,15 +1272,27 @@ window.__ModuleLoader__.load({
978
1272
  }
979
1273
 
980
1274
  if (tab === 'tools') {
1275
+ panel.push(h(Card, { key: 'presets', title: t('toolPresetTitle'), hint: t('toolPresetHint') }, [
1276
+ h('div', { key: 'grid', style: S.presetGrid }, TOOL_PRESETS.map((preset) =>
1277
+ h('button', {
1278
+ key: preset.id, type: 'button', disabled,
1279
+ style: S.presetCard(false),
1280
+ onClick: () => { void applyToolPreset(preset.tools); },
1281
+ }, [
1282
+ h('div', { key: 't', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } }, [
1283
+ h('span', { key: 'title', style: S.label }, lang === 'en' ? preset.en : preset.zh),
1284
+ h('span', { key: 'badge', style: S.badge('blue') }, String(preset.tools.length) + ' 工具'),
1285
+ ]),
1286
+ h('span', { key: 'd', style: S.sub }, lang === 'en' ? preset.descEn : preset.descZh),
1287
+ ]))),
1288
+ ]));
981
1289
  panel.push(h(Card, { key: 'tools', title: t('toolsTitle'), hint: t('toolsHint') }, [
982
- h('div', { key: 'core', style: { display: 'flex', flexDirection: 'column', gap: 4 } }, [
983
- h('span', { key: 'l', style: S.label }, t('coreTools')),
984
- h('div', { key: 'g', style: S.toolGrid }, coreTools.map(toolCheckbox)),
985
- ]),
986
- h('div', { key: 'div', style: S.divider }),
987
- h('div', { key: 'long', style: { display: 'flex', flexDirection: 'column', gap: 4 } }, [
988
- h('span', { key: 'l', style: S.label }, t('longTools')),
989
- h('div', { key: 'g', style: S.toolGrid }, longTools.map(toolCheckbox)),
1290
+ ...TOOL_GROUPS.map((grp, gi) => [
1291
+ gi > 0 ? h('div', { key: 'div' + gi, style: S.divider }) : null,
1292
+ h('div', { key: grp.id, style: { display: 'flex', flexDirection: 'column', gap: 4 } }, [
1293
+ h('span', { key: 'l', style: S.label }, lang === 'en' ? grp.en : grp.zh),
1294
+ h('div', { key: 'g', style: S.toolGrid }, toolsByGroup(grp.id).map(toolCheckbox)),
1295
+ ]),
990
1296
  ]),
991
1297
  h('div', { key: 'g2', style: S.grid }, [
992
1298
  h(NumberField, {
@@ -1007,15 +1313,50 @@ window.__ModuleLoader__.load({
1007
1313
 
1008
1314
  if (tab === 'engines') {
1009
1315
  const map = envMap();
1010
- const groups = ['cache', 'fetch', 'semantic', 'engines', 'net', 'local', 'misc'];
1316
+ // 渲染顺序即面板顺序。**不是**从 ENGINE_TOGGLES 去重推导——那样分组
1317
+ // 顺序会随白名单增删而漂移,用户每次升级都看到重排的面板。
1318
+ //
1319
+ // 历史 bug(2026-10-21 定位):`paid` 一度不在这个清单里,于是
1320
+ // ARGO_PAID_SEARCH 与 ARGO_BUDGET_DEEPSEEK_SEARCH 两条开关在
1321
+ // index.js 白名单、client.js ENGINE_TOGGLES、双语文案三处都齐全,
1322
+ // 却在面板上完全不显示——用户配好了预算却找不到入口,还以为是插件
1323
+ // 没装。漂移门禁只核对「两边名单同集合、字段一致」,核对不到「这个
1324
+ // 分组到底渲不渲染」;已由 test/client.test.js 的「engines tab renders
1325
+ // a control for every ENGINE_TOGGLES switch」补上(见该测试注释)。
1326
+ const groups = ['cache', 'fetch', 'semantic', 'engines', 'net', 'local', 'paid', 'misc'];
1011
1327
  const groupTitle = (group) => t('group' + group.charAt(0).toUpperCase() + group.slice(1));
1012
1328
  panel.push(h('p', { key: 'hint', style: S.hint }, t('enginesHint')));
1329
+ panel.push(h('input', {
1330
+ key: 'filter',
1331
+ type: 'text',
1332
+ style: S.filterInput,
1333
+ placeholder: t('searchFilterPlaceholder'),
1334
+ value: engineFilter,
1335
+ onChange: (event) => setEngineFilter(event.target.value),
1336
+ }));
1337
+ const query = engineFilter.trim().toLowerCase();
1013
1338
  for (const group of groups) {
1014
- const specs = ENGINE_TOGGLES.filter((spec) => spec.group === group);
1339
+ let specs = ENGINE_TOGGLES.filter((spec) => spec.group === group);
1340
+ if (query) {
1341
+ const groupNameMatch = group.toLowerCase().includes(query) || groupTitle(group).toLowerCase().includes(query);
1342
+ if (!groupNameMatch) {
1343
+ specs = specs.filter((spec) => {
1344
+ const name = spec.name.toLowerCase();
1345
+ const zh = (spec.zh || '').toLowerCase();
1346
+ const en = (spec.en || '').toLowerCase();
1347
+ const hintZh = (spec.hintZh || '').toLowerCase();
1348
+ const hintEn = (spec.hintEn || '').toLowerCase();
1349
+ return name.includes(query) || zh.includes(query) || en.includes(query) || hintZh.includes(query) || hintEn.includes(query);
1350
+ });
1351
+ }
1352
+ }
1015
1353
  if (specs.length === 0) continue;
1016
1354
  const bools = specs.filter((spec) => spec.kind === 'bool');
1017
1355
  const fields = specs.filter((spec) => spec.kind !== 'bool');
1018
1356
  const children = [];
1357
+ if (group === 'paid') {
1358
+ children.push(h('p', { key: 'groupHint', style: S.hint }, t('groupPaidHint')));
1359
+ }
1019
1360
  for (const spec of bools) {
1020
1361
  // 未配置时显示 argo 默认态并标注「默认」,配置过的显示自定义值,
1021
1362
  // 让「这一行到底生不生效」在面板上一眼可辨。
@@ -1078,10 +1419,22 @@ window.__ModuleLoader__.load({
1078
1419
  }
1079
1420
 
1080
1421
  if (tab === 'keys') {
1422
+ const query = keyFilter.trim().toLowerCase();
1423
+ const displayKeys = query
1424
+ ? KEYS.filter((entry) => entry.ref.toLowerCase().includes(query) || (entry.zh && entry.zh.toLowerCase().includes(query)) || (entry.en && entry.en.toLowerCase().includes(query)))
1425
+ : visibleKeys;
1081
1426
  panel.push(h(Card, { key: 'keys', title: t('keysTitle'), hint: t('keysHint') }, [
1082
- h('span', { key: 'l', style: S.label }, t('commonKeys')),
1083
- ...visibleKeys.map(keyRow),
1084
- h('div', { key: 'a', style: S.actions }, [
1427
+ h('input', {
1428
+ key: 'filter',
1429
+ type: 'text',
1430
+ style: S.filterInput,
1431
+ placeholder: t('searchFilterPlaceholder'),
1432
+ value: keyFilter,
1433
+ onChange: (event) => setKeyFilter(event.target.value),
1434
+ }),
1435
+ h('span', { key: 'l', style: S.label }, query ? t('keysTitle') : t('commonKeys')),
1436
+ ...displayKeys.map(keyRow),
1437
+ query ? null : h('div', { key: 'a', style: S.actions }, [
1085
1438
  h('button', {
1086
1439
  key: 'more',
1087
1440
  type: 'button',
@@ -1178,6 +1531,7 @@ window.__ModuleLoader__.load({
1178
1531
  h('p', { key: 'i', style: S.hint }, t('intro')),
1179
1532
  ]),
1180
1533
  h('div', { key: 'tabs', style: S.tabs }, [
1534
+ tabButton('dashboard', t('tabDashboard')),
1181
1535
  tabButton('access', t('tabAccess')),
1182
1536
  tabButton('tools', t('tabTools')),
1183
1537
  tabButton('engines', t('tabEngines')),
@@ -1237,6 +1591,7 @@ window.__ModuleLoader__.load({
1237
1591
  // 生成产物)同名同集合,ENGINE_TOGGLES 必须与 index.js 的 ARGO_ENV_SPECS
1238
1592
  // 同名同集合——两边任何一边增删而另一边没跟上,测试即红。
1239
1593
  exports.TOOLS = TOOLS;
1594
+ exports.TOOL_GROUPS = TOOL_GROUPS;
1240
1595
  exports.ENGINE_TOGGLES = ENGINE_TOGGLES;
1241
1596
 
1242
1597
  return module.exports;