argo-search 2.9.8 → 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.
- package/README.en.md +30 -20
- package/README.es.md +30 -20
- package/README.ja.md +30 -20
- package/README.ko.md +30 -20
- package/README.md +32 -22
- package/SKILL.md +1 -1
- package/bin/argo +4 -0
- package/bin/argo.js +1 -1
- package/config.yaml +52 -5
- package/docs/ARGO_INTRO.md +5 -3
- package/docs/RELEASE_NOTES_v2.9.9.md +78 -0
- package/package.json +3 -2
- package/packages/dsh-plugin/README.md +3 -2
- package/packages/dsh-plugin/dsh/client.js +98 -32
- package/packages/dsh-plugin/dsh/diagnostics.js +43 -0
- package/packages/dsh-plugin/dsh/index.js +23 -1
- package/packages/dsh-plugin/package.json +2 -2
- package/packages/dsh-plugin/test/client.test.js +2 -1
- package/scripts/adaptive.py +102 -42
- package/scripts/bench_tail_ab.py +281 -0
- package/scripts/bounded_run.py +91 -49
- package/scripts/clock_bridge.py +22 -0
- package/scripts/content_security.py +119 -4
- package/scripts/dispatch_result.py +9 -2
- package/scripts/engine_add.py +602 -0
- package/scripts/engine_dispatch.py +9 -0
- package/scripts/evidence_loop.py +4 -0
- package/scripts/fetch_quality.py +13 -0
- package/scripts/fetch_v3.py +3 -3
- package/scripts/http_client.py +24 -38
- package/scripts/image_vision/vision_probe +0 -0
- package/scripts/local_engines.py +321 -0
- package/scripts/matrix_search_eval.py +111 -1
- package/scripts/mcp_transport.py +1 -1
- package/scripts/net_proxy.py +27 -0
- package/scripts/recovery.py +7 -3
- package/scripts/route_combo.py +19 -5
- package/scripts/route_domains.py +138 -12
- package/scripts/safe_conn.py +52 -0
- package/scripts/search_entry.py +1 -0
- package/scripts/search_pipeline.py +20 -2
- package/scripts/search_rank.py +6 -0
- package/scripts/serp_guard.py +23 -0
- package/scripts/tail_completion.py +212 -0
- package/scripts/url_safety.py +126 -3
- package/scripts/usage_log.py +45 -0
|
@@ -48,6 +48,9 @@ window.__ModuleLoader__.load({
|
|
|
48
48
|
tabAdvanced: '高级',
|
|
49
49
|
tabDashboard: '看板',
|
|
50
50
|
dashboardTitle: '运行监控与健康态看板',
|
|
51
|
+
routeHealthTitle: '路由观测',
|
|
52
|
+
routeHealthHint: '最近的路由决策与引擎状态(宿主提供时显示)。',
|
|
53
|
+
routeHealthFallback: '在终端运行 `argo stats` 或 `argo engine list` 查看路由健康、引擎成功率与配额消耗。',
|
|
51
54
|
dashboardHint: '实时监控常驻 MCP 连接池、上游搜索源拓扑与额度状态,提供一键预设与快速调优。',
|
|
52
55
|
poolTitle: '常驻 MCP 连接池监控',
|
|
53
56
|
poolStatusHealthy: '就绪(正常)',
|
|
@@ -83,6 +86,8 @@ window.__ModuleLoader__.load({
|
|
|
83
86
|
concurrency: '并发连接数',
|
|
84
87
|
toolsTitle: '原生工具',
|
|
85
88
|
toolsHint: '勾选即注册为原生一等工具(CLI 单发,不依赖 MCP)。长尾工具只在需要时开,避免每轮工具面膨胀。',
|
|
89
|
+
toolPresetTitle: '推荐组合',
|
|
90
|
+
toolPresetHint: '按场景一键套用(会覆盖当前勾选)。工具分区见下方。',
|
|
86
91
|
coreTools: '高频路径',
|
|
87
92
|
longTools: '长尾取证',
|
|
88
93
|
nativeTimeout: '单次调用超时 (ms)',
|
|
@@ -163,6 +168,9 @@ window.__ModuleLoader__.load({
|
|
|
163
168
|
tabAdvanced: 'Advanced',
|
|
164
169
|
tabDashboard: 'Dashboard',
|
|
165
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.',
|
|
166
174
|
dashboardHint: 'Real-time overview of the resident MCP connection pool, upstream search engine matrix, and quick workflow presets.',
|
|
167
175
|
poolTitle: 'Resident MCP Pool Monitoring',
|
|
168
176
|
poolStatusHealthy: 'Ready (Healthy)',
|
|
@@ -198,6 +206,8 @@ window.__ModuleLoader__.load({
|
|
|
198
206
|
concurrency: 'Concurrent connections',
|
|
199
207
|
toolsTitle: 'Native tools',
|
|
200
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.',
|
|
201
211
|
coreTools: 'Hot path',
|
|
202
212
|
longTools: 'Long tail',
|
|
203
213
|
nativeTimeout: 'Per-call timeout (ms)',
|
|
@@ -259,25 +269,41 @@ window.__ModuleLoader__.load({
|
|
|
259
269
|
},
|
|
260
270
|
};
|
|
261
271
|
|
|
272
|
+
// 工具分区(场景化):核心 / 取证 / 本地 / 长尾。
|
|
273
|
+
// 旧版只有 core/long 两组,16 个长尾工具平铺,新用户不知道该开哪些;
|
|
274
|
+
// 按使用场景分组后,配合下方「推荐组合」卡可以一键套用。
|
|
262
275
|
const TOOLS = [
|
|
263
|
-
{ id: 'argo_search', group: 'core', zh: '统一搜索', en: 'Unified search' },
|
|
264
|
-
{ id: 'argo_fetch', group: 'core', zh: '智能抓取', en: 'Smart fetch' },
|
|
265
|
-
{ id: 'argo_evidence', group: '
|
|
266
|
-
{ id: '
|
|
267
|
-
{ id: '
|
|
268
|
-
{ id: '
|
|
269
|
-
{ id: '
|
|
270
|
-
{ id: '
|
|
271
|
-
{ id: '
|
|
272
|
-
{ id: '
|
|
273
|
-
{ id: '
|
|
274
|
-
{ id: '
|
|
275
|
-
{ id: '
|
|
276
|
-
{ id: '
|
|
277
|
-
{ id: '
|
|
278
|
-
{ id: 'argo_answer', group: '
|
|
279
|
-
{ id: 'argo_watch', group: '
|
|
280
|
-
{ id: 'argo_cite', group: '
|
|
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' },
|
|
281
307
|
];
|
|
282
308
|
|
|
283
309
|
const KEYS = [
|
|
@@ -617,6 +643,8 @@ window.__ModuleLoader__.load({
|
|
|
617
643
|
const [envBusy, setEnvBusy] = useState('');
|
|
618
644
|
const [engineFilter, setEngineFilter] = useState('');
|
|
619
645
|
const [keyFilter, setKeyFilter] = useState('');
|
|
646
|
+
// 路由观测(宿主经 remote 事件推送;无推送时卡片显示终端命令提示)
|
|
647
|
+
const [routeHealth, setRouteHealth] = useState(null);
|
|
620
648
|
|
|
621
649
|
const dirty = useRef(new Set());
|
|
622
650
|
// 读-改-写共享配置字段(keyRefs / nativeTools / argoEnv)时读实时快照,
|
|
@@ -672,10 +700,14 @@ window.__ModuleLoader__.load({
|
|
|
672
700
|
useEffect(() => {
|
|
673
701
|
if (props.view === 'summary') return undefined;
|
|
674
702
|
void refreshCredentials();
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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(); };
|
|
679
711
|
}, [state]);
|
|
680
712
|
|
|
681
713
|
const edit = (key, text) => {
|
|
@@ -778,6 +810,14 @@ window.__ModuleLoader__.load({
|
|
|
778
810
|
void setField('nativeTools', ordered);
|
|
779
811
|
};
|
|
780
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
|
+
|
|
781
821
|
const saveKey = async (ref) => {
|
|
782
822
|
const key = asString(keyDraft[ref]).trim();
|
|
783
823
|
if (!key || !props.credentials) return;
|
|
@@ -975,8 +1015,8 @@ window.__ModuleLoader__.load({
|
|
|
975
1015
|
? h('p', { style: error ? S.error : S.notice, role: 'status' }, message)
|
|
976
1016
|
: h('p', { style: S.status }, disabled ? t('readonly') : '');
|
|
977
1017
|
|
|
978
|
-
|
|
979
|
-
const
|
|
1018
|
+
// 按 TOOL_GROUPS 顺序分组渲染(场景化分区,替代旧的 core/long 两分)
|
|
1019
|
+
const toolsByGroup = (gid) => TOOLS.filter((tool) => tool.group === gid);
|
|
980
1020
|
const toolCheckbox = (tool) => h('label', { key: tool.id, style: S.check }, [
|
|
981
1021
|
h('input', {
|
|
982
1022
|
key: 'i',
|
|
@@ -1100,6 +1140,19 @@ window.__ModuleLoader__.load({
|
|
|
1100
1140
|
]),
|
|
1101
1141
|
]));
|
|
1102
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
|
+
|
|
1103
1156
|
panel.push(h(Card, { key: 'presets', title: t('presetTitle'), hint: t('presetHint') }, [
|
|
1104
1157
|
h('div', { key: 'grid', style: S.presetGrid }, [
|
|
1105
1158
|
h('div', {
|
|
@@ -1219,15 +1272,27 @@ window.__ModuleLoader__.load({
|
|
|
1219
1272
|
}
|
|
1220
1273
|
|
|
1221
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
|
+
]));
|
|
1222
1289
|
panel.push(h(Card, { key: 'tools', title: t('toolsTitle'), hint: t('toolsHint') }, [
|
|
1223
|
-
|
|
1224
|
-
h('
|
|
1225
|
-
h('div', { key: '
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
h('span', { key: 'l', style: S.label }, t('longTools')),
|
|
1230
|
-
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
|
+
]),
|
|
1231
1296
|
]),
|
|
1232
1297
|
h('div', { key: 'g2', style: S.grid }, [
|
|
1233
1298
|
h(NumberField, {
|
|
@@ -1526,6 +1591,7 @@ window.__ModuleLoader__.load({
|
|
|
1526
1591
|
// 生成产物)同名同集合,ENGINE_TOGGLES 必须与 index.js 的 ARGO_ENV_SPECS
|
|
1527
1592
|
// 同名同集合——两边任何一边增删而另一边没跟上,测试即红。
|
|
1528
1593
|
exports.TOOLS = TOOLS;
|
|
1594
|
+
exports.TOOL_GROUPS = TOOL_GROUPS;
|
|
1529
1595
|
exports.ENGINE_TOGGLES = ENGINE_TOGGLES;
|
|
1530
1596
|
|
|
1531
1597
|
return module.exports;
|
|
@@ -5,6 +5,49 @@
|
|
|
5
5
|
* 避免状态探查逻辑与核心检索逻辑耦合。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
9
|
+
import { homedir } from 'node:os'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 读 argo 的本地使用日志,汇总成「路由观测」卡片数据。
|
|
14
|
+
*
|
|
15
|
+
* 数据来源与 stats_cli.py / usage_log.py 同一处(<state>/usage_log/*.jsonl),
|
|
16
|
+
* 只读、不写、不出本机。argo 未运行过(无日志)时返回 { samples: [] },
|
|
17
|
+
* 面板据此显示终端命令提示——不假装有数据。
|
|
18
|
+
*
|
|
19
|
+
* @param {string} [stateDir] - argo 状态目录;缺省用 ARGO_STATE_DIR 或平台惯例
|
|
20
|
+
* @param {number} [limit] - 最近几条
|
|
21
|
+
*/
|
|
22
|
+
export function getRouteHealth(stateDir, limit = 10) {
|
|
23
|
+
const dir = stateDir
|
|
24
|
+
|| process.env.ARGO_STATE_DIR
|
|
25
|
+
|| join(homedir(), '.cache', 'unified-search')
|
|
26
|
+
const samples = []
|
|
27
|
+
try {
|
|
28
|
+
const routePath = join(dir, 'usage_log', 'route.jsonl')
|
|
29
|
+
if (existsSync(routePath)) {
|
|
30
|
+
const lines = readFileSync(routePath, 'utf8').split('\n').filter(Boolean)
|
|
31
|
+
for (const line of lines.slice(-limit)) {
|
|
32
|
+
try {
|
|
33
|
+
const row = JSON.parse(line)
|
|
34
|
+
if (row && typeof row === 'object') {
|
|
35
|
+
samples.push({
|
|
36
|
+
query: row.query || row.original_query || '',
|
|
37
|
+
engine: row.engine || (row.decision && row.decision.engine) || '',
|
|
38
|
+
domain: row.domain || (row.decision && row.decision.domain) || '',
|
|
39
|
+
confidence: row.confidence || (row.decision && row.decision.confidence) || '',
|
|
40
|
+
reason: row.reason || (row.decision && row.decision.reason) || '',
|
|
41
|
+
ts: row.ts || row.timestamp || '',
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
} catch { /* 单行坏数据不影响其余 */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch { /* 读不到就空 */ }
|
|
48
|
+
return { samples, source: 'usage_log/route.jsonl', stateDir: dir }
|
|
49
|
+
}
|
|
50
|
+
|
|
8
51
|
/**
|
|
9
52
|
* 获取当前常驻 MCP 连接池的运行时指标。
|
|
10
53
|
*
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
import {
|
|
63
63
|
getMcpPoolStats,
|
|
64
64
|
resetMcpPool,
|
|
65
|
+
getRouteHealth,
|
|
65
66
|
} from './diagnostics.js'
|
|
66
67
|
|
|
67
68
|
/**
|
|
@@ -450,6 +451,18 @@ export function apply(ctx, providedConfig = {}) {
|
|
|
450
451
|
}
|
|
451
452
|
|
|
452
453
|
const nativeRegistrations = new Map()
|
|
454
|
+
// 路由观测推送:原生工具执行后读 argo 使用日志,经 remote 事件推给设置面板。
|
|
455
|
+
// 失败静默(诊断是增值,不是依赖);无 remote 事件总线时跳过。
|
|
456
|
+
const emitRouteHealth = () => {
|
|
457
|
+
try {
|
|
458
|
+
if (!ctx.remote || typeof ctx.remote.$emit !== 'function') return
|
|
459
|
+
const data = getRouteHealth()
|
|
460
|
+
if (Array.isArray(data.samples) && data.samples.length > 0) {
|
|
461
|
+
ctx.remote.$emit('route-health/updated', data)
|
|
462
|
+
}
|
|
463
|
+
} catch { /* ignore */ }
|
|
464
|
+
}
|
|
465
|
+
|
|
453
466
|
const syncNativeTools = () => {
|
|
454
467
|
const desired = asStringArray(config.nativeTools, 20)
|
|
455
468
|
for (const [kind, dispose] of nativeRegistrations) {
|
|
@@ -459,7 +472,16 @@ export function apply(ctx, providedConfig = {}) {
|
|
|
459
472
|
}
|
|
460
473
|
for (const kind of desired) {
|
|
461
474
|
if (nativeRegistrations.has(kind)) continue
|
|
462
|
-
|
|
475
|
+
const tool = makeNativeTool(config, kind)
|
|
476
|
+
const originalExecute = tool.execute
|
|
477
|
+
if (typeof originalExecute === 'function') {
|
|
478
|
+
tool.execute = async (args, exec) => {
|
|
479
|
+
const result = await originalExecute(args, exec)
|
|
480
|
+
emitRouteHealth()
|
|
481
|
+
return result
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
nativeRegistrations.set(kind, ctx.tools.register(tool))
|
|
463
485
|
}
|
|
464
486
|
}
|
|
465
487
|
syncNativeTools()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taxueseek/argo-dsh",
|
|
3
|
-
"version": "2.9.
|
|
4
|
-
"description": "Argo 阿尔戈 — DSH plugin:argo 原生搜索/抓取工具 + web_search seam + wide_research 编排;自带设置页(接入/工具/API 密钥/研究/高级);argo MCP 19 工具全量面按需挂载。安装:dsh plugin --profile web add \"github:taxueseek/argo#main&path:packages/dsh-plugin\"",
|
|
3
|
+
"version": "2.9.9",
|
|
4
|
+
"description": "Argo 阿尔戈 — DSH plugin:argo 原生搜索/抓取工具 + web_search seam + wide_research 编排;自带设置页(接入/工具/API 密钥/研究/高级);argo MCP 19 工具全量面按需挂载。安装:dsh plugin --profile web add @taxueseek/argo-dsh(或从 GitHub main 装:dsh plugin --profile web add \"github:taxueseek/argo#main&path:packages/dsh-plugin\")",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dsh/index.js",
|
|
@@ -122,9 +122,10 @@ test('client TOOLS mirror native-tools.mjs (drift gate)', () => {
|
|
|
122
122
|
const specNames = Object.keys(NATIVE_TOOLS).sort()
|
|
123
123
|
const toolIds = [...mod.TOOLS].map((tool) => tool.id).sort()
|
|
124
124
|
assert.deepEqual(toolIds, specNames, '面板 TOOLS 与原生工具规格表漂移,请同步两边')
|
|
125
|
+
const groupIds = new Set([...mod.TOOL_GROUPS].map((grp) => grp.id))
|
|
125
126
|
for (const tool of mod.TOOLS) {
|
|
126
127
|
assert.ok(tool.zh && tool.en, `tool ${tool.id} 缺双语标签`)
|
|
127
|
-
assert.ok(
|
|
128
|
+
assert.ok(groupIds.has(tool.group), `tool ${tool.id} 分组非法: ${tool.group}`)
|
|
128
129
|
}
|
|
129
130
|
})
|
|
130
131
|
|
package/scripts/adaptive.py
CHANGED
|
@@ -82,6 +82,21 @@ def quality_factor(avg_quality: float | None, sum_quality: float | int | None) -
|
|
|
82
82
|
min(QUALITY_FACTOR_MAX, 0.75 + 0.5 * avg))
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def scenario_of(decision: dict | None) -> dict[str, str]:
|
|
86
|
+
"""从路由决策里抽 (domain, lang) 场景标签,供 record 分桶使用。
|
|
87
|
+
|
|
88
|
+
独立成函数(而非写在 search_pipeline 的 record 调用里)是为了让
|
|
89
|
+
search_pipeline 的 record 调用保持 3 行——它是祖父文件(行数上限门禁),
|
|
90
|
+
把场景抽取塞进调用点会让它超限。
|
|
91
|
+
"""
|
|
92
|
+
d = decision or {}
|
|
93
|
+
feats = d.get("features") or {}
|
|
94
|
+
return {
|
|
95
|
+
"domain": str(d.get("domain") or ""),
|
|
96
|
+
"lang": str(feats.get("primary_lang") or ""),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
85
100
|
class AdaptiveLearner:
|
|
86
101
|
"""自适应学习引擎:追踪引擎表现并输出推荐分数。"""
|
|
87
102
|
|
|
@@ -166,18 +181,33 @@ class AdaptiveLearner:
|
|
|
166
181
|
# 同理补列。历史行 quality=0 表示「当时没回写质量」,不是「质量为零」——
|
|
167
182
|
# 评分侧以 SUM(quality) > 0 判定有无数据,故这批行不会拉低任何引擎。
|
|
168
183
|
conn.execute("ALTER TABLE engine_perf ADD COLUMN quality REAL DEFAULT 0.0")
|
|
184
|
+
if "domain" not in cols:
|
|
185
|
+
# 场景分桶(2026-10-24):domain/lang 让学习器能表达「这个引擎在
|
|
186
|
+
# A 域强、B 域弱」。历史行留空串 = 「未分桶」,回退全局分。
|
|
187
|
+
conn.execute("ALTER TABLE engine_perf ADD COLUMN domain TEXT NOT NULL DEFAULT ''")
|
|
188
|
+
if "lang" not in cols:
|
|
189
|
+
conn.execute("ALTER TABLE engine_perf ADD COLUMN lang TEXT NOT NULL DEFAULT ''")
|
|
169
190
|
conn.execute(
|
|
170
191
|
"CREATE INDEX IF NOT EXISTS idx_perf_engine_time ON engine_perf(engine, created_at)"
|
|
171
192
|
)
|
|
193
|
+
conn.execute(
|
|
194
|
+
"CREATE INDEX IF NOT EXISTS idx_perf_engine_scenario "
|
|
195
|
+
"ON engine_perf(engine, domain, lang, created_at)"
|
|
196
|
+
)
|
|
172
197
|
cutoff = time.time() - WINDOW_DAYS * 86400
|
|
173
198
|
conn.execute("DELETE FROM engine_perf WHERE created_at < ?", (cutoff,))
|
|
174
199
|
self._prune_foreign(conn)
|
|
175
200
|
conn.commit()
|
|
176
201
|
|
|
177
202
|
def record(self, engine: str, success: bool, latency_ms: float, cost: float = 0.0,
|
|
178
|
-
empty: bool = False, quality: float | None = None
|
|
203
|
+
empty: bool = False, quality: float | None = None,
|
|
204
|
+
domain: str = "", lang: str = ""):
|
|
179
205
|
"""记录一次引擎调用结果。
|
|
180
206
|
|
|
207
|
+
``domain`` / ``lang`` 是**可选场景标签**(默认空 = 不回退旧调用方):
|
|
208
|
+
给出时按 (engine, domain, lang) 分桶学习,让「引擎在 A 域强、B 域弱」
|
|
209
|
+
可表达;不给时与旧口径完全一致(全局单桶)。见 ``get_score``。
|
|
210
|
+
|
|
181
211
|
``empty=True`` 表示「引擎正常,但这次查询它没有结果」——与「失败」是
|
|
182
212
|
两回事,见 `get_score` 的说明。默认 False 保持旧调用方行为不变。
|
|
183
213
|
|
|
@@ -187,6 +217,8 @@ class AdaptiveLearner:
|
|
|
187
217
|
`SUM(quality) > 0` 判定有无数据,故未回写的行不会拖累任何引擎。
|
|
188
218
|
"""
|
|
189
219
|
q = 0.0 if quality is None else max(0.0, min(1.0, float(quality)))
|
|
220
|
+
dom = str(domain or "")[:64]
|
|
221
|
+
lg = str(lang or "")[:16]
|
|
190
222
|
with self._lock:
|
|
191
223
|
if not hasattr(self, "_session_engines"):
|
|
192
224
|
self._session_engines = set()
|
|
@@ -194,18 +226,64 @@ class AdaptiveLearner:
|
|
|
194
226
|
conn = self._connect()
|
|
195
227
|
conn.execute(
|
|
196
228
|
"INSERT INTO engine_perf "
|
|
197
|
-
"(engine, success, latency_ms, cost, created_at, empty, quality) "
|
|
198
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
229
|
+
"(engine, success, latency_ms, cost, created_at, empty, quality, domain, lang) "
|
|
230
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
199
231
|
(engine, 1 if success else 0, latency_ms, cost, time.time(),
|
|
200
|
-
1 if empty else 0, q),
|
|
232
|
+
1 if empty else 0, q, dom, lg),
|
|
201
233
|
)
|
|
202
234
|
if getattr(self, "_batch_depth", 0) > 0:
|
|
203
235
|
self._batch_dirty = True
|
|
204
236
|
else:
|
|
205
237
|
conn.commit()
|
|
206
|
-
|
|
238
|
+
# 清掉该引擎的全部缓存键(含场景分桶键),见 get_score 的缓存口径
|
|
239
|
+
for _k in [k for k in self._score_cache if k[0] == engine]:
|
|
240
|
+
self._score_cache.pop(_k, None)
|
|
207
241
|
|
|
208
|
-
def
|
|
242
|
+
def _score_from_row(self, row) -> float:
|
|
243
|
+
"""把一条聚合行转成 0~1 推荐分(口径见 get_score)。"""
|
|
244
|
+
total, judged, judged_ok, avg_latency, avg_cost, avg_q, sum_q, last_ts = row
|
|
245
|
+
total = int(total or 0)
|
|
246
|
+
judged = int(judged or 0)
|
|
247
|
+
if not total:
|
|
248
|
+
return 0.5
|
|
249
|
+
last_ts = float(last_ts or 0)
|
|
250
|
+
if last_ts and (time.time() - last_ts) > STALE_AFTER_SECONDS:
|
|
251
|
+
return 0.5
|
|
252
|
+
if judged == 0:
|
|
253
|
+
return 0.5
|
|
254
|
+
success_rate = (judged_ok or 0) / judged
|
|
255
|
+
latency_factor = min(1.0, 2000.0 / max(avg_latency or 2000, 1))
|
|
256
|
+
cost_factor = max(0.3, 1.0 - (avg_cost or 0.0) * 10)
|
|
257
|
+
return round(success_rate * latency_factor * cost_factor
|
|
258
|
+
* quality_factor(avg_q, sum_q), 4)
|
|
259
|
+
|
|
260
|
+
def _global_row(self, engine: str):
|
|
261
|
+
cutoff = time.time() - WINDOW_DAYS * 86400
|
|
262
|
+
return self._connect().execute(
|
|
263
|
+
"SELECT COUNT(*), "
|
|
264
|
+
" SUM(CASE WHEN empty = 0 THEN 1 ELSE 0 END), "
|
|
265
|
+
" SUM(CASE WHEN empty = 0 THEN success ELSE 0 END), "
|
|
266
|
+
" AVG(latency_ms), AVG(cost), "
|
|
267
|
+
" AVG(quality), SUM(quality), MAX(created_at) "
|
|
268
|
+
"FROM engine_perf WHERE engine = ? AND created_at > ?",
|
|
269
|
+
(engine, cutoff),
|
|
270
|
+
).fetchone()
|
|
271
|
+
|
|
272
|
+
def _bucket_row(self, engine: str, domain: str, lang: str):
|
|
273
|
+
cutoff = time.time() - WINDOW_DAYS * 86400
|
|
274
|
+
return self._connect().execute(
|
|
275
|
+
"SELECT COUNT(*), "
|
|
276
|
+
" SUM(CASE WHEN empty = 0 THEN 1 ELSE 0 END), "
|
|
277
|
+
" SUM(CASE WHEN empty = 0 THEN success ELSE 0 END), "
|
|
278
|
+
" AVG(latency_ms), AVG(cost), "
|
|
279
|
+
" AVG(quality), SUM(quality), MAX(created_at) "
|
|
280
|
+
"FROM engine_perf WHERE engine = ? AND created_at > ? "
|
|
281
|
+
"AND domain = ? AND lang = ?",
|
|
282
|
+
(engine, cutoff, domain, lang),
|
|
283
|
+
).fetchone()
|
|
284
|
+
|
|
285
|
+
def get_score(self, engine: str, domain: str | None = None,
|
|
286
|
+
lang: str | None = None) -> float:
|
|
209
287
|
"""获取引擎的综合推荐分数(0.0 ~ 1.0)。
|
|
210
288
|
|
|
211
289
|
**只按「有明确结果」的调用算成功率**(`empty=0`)。
|
|
@@ -219,50 +297,32 @@ class AdaptiveLearner:
|
|
|
219
297
|
openalex(0.29)、hackernews(0.10)、twitter(0.17)、open_library(0.17)。
|
|
220
298
|
|
|
221
299
|
延迟与成本仍按全部调用统计——空结果也是真实开销。
|
|
300
|
+
|
|
301
|
+
**场景分桶**(2026-10-24):给 `domain` / `lang` 时,按 (engine, domain,
|
|
302
|
+
lang) 聚合出「桶分」,与全局分加权融合(桶样本 <3 时权重 0.3,≥3 时 0.5)。
|
|
303
|
+
这让「引擎在 A 域强、B 域弱」可表达,新引擎也能在建议域里被更快识别。
|
|
304
|
+
桶为空时回退全局分——旧调用方(不传 domain/lang)行为逐位不变。
|
|
222
305
|
"""
|
|
306
|
+
key = (engine, domain or "", lang or "")
|
|
223
307
|
now = time.time()
|
|
224
|
-
cached = self._score_cache.get(
|
|
308
|
+
cached = self._score_cache.get(key)
|
|
225
309
|
if cached and cached[1] > now:
|
|
226
310
|
return cached[0]
|
|
227
311
|
|
|
228
312
|
with self._lock:
|
|
229
|
-
cached = self._score_cache.get(
|
|
313
|
+
cached = self._score_cache.get(key)
|
|
230
314
|
if cached and cached[1] > now:
|
|
231
315
|
return cached[0]
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
).fetchone()
|
|
243
|
-
total, judged, judged_ok, avg_latency, avg_cost, avg_q, sum_q, last_ts = row
|
|
244
|
-
total = int(total or 0)
|
|
245
|
-
judged = int(judged or 0)
|
|
246
|
-
if not total:
|
|
247
|
-
score = 0.5 # 无数据时中性分
|
|
248
|
-
else:
|
|
249
|
-
# 数据过时恢复:最后一次调用距今过久 → 中性分,给恢复探测机会
|
|
250
|
-
last_ts = float(last_ts or 0)
|
|
251
|
-
if last_ts and (now - last_ts) > STALE_AFTER_SECONDS:
|
|
252
|
-
score = 0.5
|
|
253
|
-
elif judged == 0:
|
|
254
|
-
# 窗口内全是空结果:没有任何可判定健康与否的调用 → 中性分。
|
|
255
|
-
# 给中性分而不是 0:否则「每次都只是没搜到」会把源打死。
|
|
256
|
-
score = 0.5
|
|
257
|
-
else:
|
|
258
|
-
success_rate = (judged_ok or 0) / judged
|
|
259
|
-
latency_factor = min(1.0, 2000.0 / max(avg_latency or 2000, 1))
|
|
260
|
-
cost_factor = max(0.3, 1.0 - (avg_cost or 0.0) * 10)
|
|
261
|
-
q_factor = quality_factor(avg_q, sum_q)
|
|
262
|
-
score = round(success_rate * latency_factor * cost_factor
|
|
263
|
-
* q_factor, 4)
|
|
264
|
-
|
|
265
|
-
self._score_cache[engine] = (score, now + self.SCORE_CACHE_TTL)
|
|
316
|
+
score = self._score_from_row(self._global_row(engine))
|
|
317
|
+
if domain or lang:
|
|
318
|
+
brow = self._bucket_row(engine, domain or "", lang or "")
|
|
319
|
+
b_judged = int(brow[1] or 0)
|
|
320
|
+
if b_judged > 0:
|
|
321
|
+
b_score = self._score_from_row(brow)
|
|
322
|
+
w = 0.5 if b_judged >= 3 else 0.3
|
|
323
|
+
score = round((1 - w) * score + w * b_score, 4)
|
|
324
|
+
|
|
325
|
+
self._score_cache[key] = (score, now + self.SCORE_CACHE_TTL)
|
|
266
326
|
return score
|
|
267
327
|
|
|
268
328
|
def get_ranking(self) -> list[tuple[str, float]]:
|