dsh-web-search-pro 0.1.3 → 0.1.4

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/LOGIN.md CHANGED
@@ -16,16 +16,29 @@ node scripts/save-login.mjs all login-state.json
16
16
  node scripts/save-login.mjs zhihu login-state.json
17
17
  ```
18
18
 
19
- 然后把文件路径填到 `$DSH_HOME/settings.yaml`:
19
+ 然后在 dsh-browser 的部署配置中创建命名、按域名授权的 AuthProfile:
20
20
 
21
21
  ```yaml
22
+ # cordis.yml 中的 dsh-browser 插件行
23
+ - id: browser
24
+ name: '@anweat/dsh-browser'
25
+ config:
26
+ authProfiles:
27
+ china-community:
28
+ storageStatePath: 'D:/secrets/login-state.json'
29
+ allowedDomains: [zhihu.com, weibo.com, douban.com, baidu.com, douyin.com, kuaishou.com]
30
+ persistState: false
31
+
32
+ # $DSH_HOME/settings.yaml
33
+ ---
22
34
  web-search-pro:
23
- playwright:
24
- storageStatePath: 'login-state.json' # 绝对路径更稳
35
+ browserBindings:
36
+ zhihu: { authProfile: china-community }
37
+ weibo: { authProfile: china-community }
25
38
  ```
26
39
 
27
40
  > storageState 是 Playwright 的标准登录态文件(cookies + origins),
28
- > 同一份文件可同时含多个站点的登录态(脚本会依次登录并合并)。
41
+ > 同一份文件可含多个站点登录态,但每个 AuthProfile 必须显式给出 `allowedDomains`;访问其他域名会被拒绝。`persistState` 默认 false,只有明确开启才会原子回写新 Cookie/localStorage。
29
42
 
30
43
  ## 方式二:用 Cookie 插件导出
31
44
 
@@ -41,6 +54,34 @@ web-search-pro:
41
54
 
42
55
  每个 cookie 至少需要 `name` `value` `domain` `path`(可补 `expires` `httpOnly` `secure`)。
43
56
 
57
+ 不要把 Cookie JSON 作为模型工具参数传入;导入到本地 storageState 文件后只在配置中引用路径。
58
+
59
+ ## 受控脚本增强(RulePack V2)
60
+
61
+ dsh-browser 可配置命名 RulePack。它支持有界的 `waitFor` / `click` / `scroll` / `wait` 步骤;可选 init script 只能引用本地文件,必须提供 SHA-256,且文件不超过 64KB。RulePack 同样按 `matches` 限域,不接受模型直接提交任意 JavaScript。
62
+
63
+ ```yaml
64
+ rulePacks:
65
+ zhihu-enhanced:
66
+ matches: [zhihu.com]
67
+ initScriptPath: 'D:/dsh/rules/zhihu-init.js'
68
+ initScriptSha256: '<64位sha256>'
69
+ steps:
70
+ - { type: waitFor, selector: '.SearchResult-Card', timeoutMs: 10000 }
71
+ - { type: scroll, deltaY: 1600, repeat: 2, waitMs: 300 }
72
+ ```
73
+
74
+ ## OpenCLI 社区平台
75
+
76
+ Reddit / 小红书 / Twitter / Instagram / Facebook 走 OpenCLI Browser Bridge,使用当前 Chrome 扩展会话,不读取或导出 Cookie。`dsh-browser` 已包含 OpenCLI CLI,但 Chrome 扩展必须已安装并连接:
77
+
78
+ ```bash
79
+ opencli doctor
80
+ opencli reddit search "DeepSeek Harness" -f yaml
81
+ ```
82
+
83
+ 若 `doctor` 显示 daemon 正常但 extension disconnected,请启动安装了 OpenCLI 扩展的 Chrome profile;不要把 Quark 或禁用扩展的 Playwright 临时 profile 当作替代。只有需要在终端独立诊断时,才需要额外全局安装 `@jackwener/opencli`。
84
+
44
85
  ## 结果选择器可配置
45
86
 
46
87
  站点改版后如果提取不到结果,不用改代码——在 `settings.yaml` 按平台覆盖选择器:
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  增强型、可持久化的扩展网页搜索插件 for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)。
4
4
 
5
- 一个 DSH **bundle 插件**,把多引擎网页搜索、平台搜索、持久化缓存、脚本猫式按站提取、Playwright 渲染打包成模型可直接调用的 9 个工具。灵感来自 [MediaCrawler](https://github.com/NanmiCoder/MediaCrawler)、[Agent-Reach](https://github.com/Panniantong/Agent-Reach)、脚本猫/油猴 userscript、opencli playwright。
5
+ 一个 DSH **bundle 插件**,把多引擎网页搜索、平台搜索、持久化缓存、受控按站增强和 Playwright 渲染打包成模型可直接调用的 11 个工具。路由控制面借鉴 Agent-Reach 的后端探测、顺序选择和失败冷却思路,核心逻辑为本项目原生 TypeScript 实现。
6
6
 
7
7
  ## 安装
8
8
 
@@ -14,22 +14,44 @@ dsh plugin --profile web add ./dsh-web-search-pro
14
14
  dsh --profile web
15
15
  ```
16
16
 
17
- > dsh-browser 需先发布到 npm(本地测试可用 `dsh plugin --profile web add ../dsh-browser ../dsh-web-search-pro` 一条命令显式列两个)。
17
+ > npm 安装会解析 `@anweat/dsh-browser`;本地联调可用 `dsh plugin --profile web add ../dsh-browser ../dsh-web-search-pro` 一条命令显式列两个。
18
18
  > 依赖 `@deepseek-ai/*` 已发布到 npm(`^0.1.0-rc.6`,与社区 dsh-cc-tui 一致)。
19
19
  > 若你的 harness 是本地源码 checkout(如 `0.1.0-rc.5`),版本号可能有出入——用
20
20
  > `dsh plugin --profile web add ./<path>` 并在 profile 的 `pnpm-workspace.yaml`
21
21
  > 里对齐版本后重装即可。
22
22
 
23
- ## 工具(9 个)
23
+ ## 快速使用与适用情形
24
+
25
+ 安装并重启后,直接在 DSH 会话里要求模型调用工具即可:
26
+
27
+ ```text
28
+ 请调用 web_backend_status 检查后端,然后用 web_search_pro 搜索
29
+ "DeepSeek Harness community feedback",指定 exa、fresh=true、返回 8 条来源。
30
+ ```
31
+
32
+ | 情形 | 推荐入口 | 说明 |
33
+ |---|---|---|
34
+ | 日常网页搜索 | `web_search_pro` | 默认按配置顺序回退;需要强制刷新时传 `fresh=true` |
35
+ | 语义研究、社区观点 | `web_search_pro` + `exa` | 有 API Key 时走原生 Exa API;只有 Exa MCP 连接时自动经 `mcporter` 回退 |
36
+ | 已知 URL 的批量正文 | `web_exa_contents` | 直接调用 Exa `/contents`,必须配置 `EXA_API_KEY` |
37
+ | GitHub/B站/Reddit 等平台 | `web_platform_search` | Reddit 等 OpenCLI 平台需要 Chrome 扩展在线;中文受限站点使用 AuthProfile |
38
+ | 登录后页面或私有论坛 | `browserBindings` + AuthProfile | Cookie 保存在本地 storageState,按域名授权,默认只读 |
39
+ | 页面改版、懒加载 | `platformRules` 或 RulePack | 优先改选择器;需要等待/点击/滚动时再使用有界 RulePack |
40
+
41
+ 先运行 `web_backend_status` 判断后端是否 ready。指定单一引擎时失败会原样返回;不指定时才会按 `engines` 顺序自动回退。
42
+
43
+ ## 工具(11 个)
24
44
 
25
45
  | 工具 | 作用 |
26
46
  |---|---|
27
47
  | `web_search_pro` | 多引擎搜索 + RRF 融合 + 内存/SQLite 双层缓存 + 历史 |
48
+ | `web_exa_contents` | 原生 Exa `/contents` 批量正文抓取(1-100 URL) |
28
49
  | `web_fetch_pro` | 可读化抓取(Jina → HTTP+规则抽取 → Playwright 兜底)+ 快照缓存 |
29
50
  | `web_platform_search` | 20 平台:GitHub/B站/YouTube/V2EX/小红书/Twitter/Reddit/IG/FB/RSS + 知乎/微博/豆瓣/贴吧/抖音/快手(Playwright 登录态) |
30
51
  | `web_snapshot` | Playwright 全页截图 + HTML + 文本落盘 |
31
52
  | `web_history` / `web_cache_clear` / `web_search_stats` | 持久历史 / 清缓存 / 存储统计 |
32
53
  | `web_rule` | 持久化按站提取规则(脚本猫式,list/upsert/remove) |
54
+ | `web_backend_status` | 无副作用后端探测、失败/冷却诊断与 CLI 状态 |
33
55
  | `web_deps` | 检测/安装外部依赖(gh/bili/yt-dlp/opencli/agent-reach/mcporter/playwright) |
34
56
 
35
57
  ## 配置
@@ -40,12 +62,16 @@ dsh --profile web
40
62
 
41
63
  ```yaml
42
64
  web-search-pro:
43
- exaApiKey: 'sk-...' # 或环境变量 EXA_API_KEY / .credentials.yaml
44
- jinaApiKey: 'jina_...' # 或环境变量 JINA_API_KEY
65
+ exaApiKeyEnv: EXA_API_KEY # 推荐:运行环境或凭据服务,不把密钥写入配置
66
+ jinaApiKeyEnv: JINA_API_KEY
45
67
  engines: [ddg, bing, exa, seam, jina]
46
68
  parallelEngines: false
47
69
  ttlSeconds: 3600
48
70
  searchMaxResults: 8
71
+ browserBindings:
72
+ zhihu:
73
+ authProfile: china-community
74
+ rulePack: zhihu-enhanced
49
75
  ```
50
76
 
51
77
  2. **cordis.yml `config:`**(部署级默认值,见 `cordis.patch.yml`)。
@@ -59,18 +85,22 @@ dsh --profile web
59
85
  |---|---|---|
60
86
  | bili-cli | B站后端 | `uv tool install bili-cli` / `pipx install bili-cli` |
61
87
  | yt-dlp | YouTube 后端 | `uv tool install yt-dlp` / `pip install yt-dlp` |
62
- | opencli | 小红书/Twitter/Reddit/IG/FB | `npm i -g opencli` |
88
+ | opencli | 小红书/Twitter/Reddit/IG/FB | dsh-browser 内置;扩展未连接时用 `opencli doctor` 诊断 |
63
89
  | agent-reach | agent-reach 后端 | `uv tool install agent-reach` / `pip install agent-reach` |
64
- | playwright | 渲染/截图后端 | `npm i -g playwright && playwright install chromium` |
90
+ | mcporter | 无裸 API Key 时的 Exa MCP 回退 | `npm i -g mcporter` |
91
+ | playwright | 渲染/截图后端 | 由 dsh-browser 内置;缺 Chromium 时调用 `browser_install` |
65
92
 
66
93
  ## 平台与引擎
67
94
 
68
- `seam`(ctx.web/DeepSeek 原生)· `exa` · `ddg` · `bing` · `jina` · `github`(REST 搜索 API,免 CLI;可选 `$GITHUB_TOKEN`/`githubToken` 提升限额并解锁代码搜索)· `bilibili` · `v2ex` · `youtube`。默认顺序 `ddg, bing, exa, seam, jina`(免费优先),失败自动回退;`multi` 并行融合。
95
+ `seam`(ctx.web/DeepSeek 原生)· `exa` · `ddg` · `bing` · `jina` · `github`(REST 搜索 API,免 CLI;可选 `$GITHUB_TOKEN`/`githubToken` 提升限额并解锁代码搜索)· `bilibili` · `v2ex` · `youtube`。默认顺序 `ddg, bing, exa, seam, jina`(免费优先),失败自动回退;失败后短时冷却,`web_backend_status` 可查看原因;`multi` 并行融合。
96
+
97
+ Exa 优先使用原生 API 客户端:`web_search_pro` 可传 `exaType`、域名包含/排除、发布时间范围和 category。若没有裸 API Key、但启用了 CLI 后端且 Exa MCP 已连接,搜索会自动通过 `mcporter` 完成;该兼容路径只支持 query + 结果数,高级筛选和 `web_exa_contents` 仍要求 `EXA_API_KEY`。不同选项、结果数、引擎顺序和单/多引擎模式使用不同缓存指纹。
69
98
 
70
99
  ## 开发
71
100
 
72
101
  ```bash
73
102
  pnpm install
103
+ pnpm test
74
104
  pnpm build # tsc src → lib
75
105
  ```
76
106
 
@@ -87,8 +117,8 @@ zhihu / weibo / douban / tieba / douyin / kuaishou 的免登录公开接口都
87
117
  所以走 **Playwright 驱动登录态浏览器**(借鉴 MediaCrawler 思路、MIT 独立实现,未用其签名算法):
88
118
 
89
119
  1. 登录一次保存登录态:`node scripts/save-login.mjs all login-state.json`
90
- 2. 在 `$DSH_HOME/settings.yaml` 里设 `playwright.storageStatePath`
91
- 3. 站点改版时无需改代码,用 `platformRules` 按平台覆盖结果选择器
120
+ 2. 在 dsh-browser 配置中声明按域名隔离的 `authProfiles`
121
+ 3. `browserBindings` 把平台绑定到 profile;站点改版时用 `platformRules` 或 dsh-browser `rulePacks`
92
122
 
93
123
  详见 [LOGIN.md](./LOGIN.md)。
94
124
 
@@ -98,9 +128,9 @@ zhihu / weibo / douban / tieba / douyin / kuaishou 的免登录公开接口都
98
128
  web_history 支持:kind/query/engine/platform 过滤、replay(用 queryId 回放已存结果)、
99
129
  export(把过滤后的历史+结果写成 JSON 文件)。
100
130
 
101
- ## 自定义平台(解析 cookie 去搜索)
131
+ ## 自定义平台
102
132
 
103
- 在 settings.yaml 里定义任意站点(URL 模板 + 结果选择器 + 可选 Cookie),
133
+ 在 settings.yaml 里定义任意站点(URL 模板 + 结果选择器),
104
134
  web_platform_search 就能直接搜它——不需要改代码:
105
135
 
106
136
  web-search-pro:
@@ -111,12 +141,16 @@ web_platform_search 就能直接搜它——不需要改代码:
111
141
  item: '.bili-video-card'
112
142
  title: '.bili-video-card__info--tit'
113
143
  link: 'a'
114
- # 需要登录的站点补 cookie(a=b; c=d,自动应用到 url 域名)
144
+ # 需要登录时优先通过 browserBindings 绑定命名 authProfile。
115
145
  myforum:
116
146
  name: '某论坛'
117
147
  url: 'https://forum.example.com/search?q={query}'
118
148
  item: '.thread'
119
149
  title: '.thread-title a'
120
150
  link: '.thread-title a'
121
- cookie: 'sessionid=abc123; csrftoken=xyz'
151
+ browserBindings:
152
+ myforum:
153
+ authProfile: forum
154
+
155
+ 旧版 `customPlatforms.*.cookie` 仍兼容,但会让 Cookie 明文进入配置;新配置应使用 dsh-browser 的命名 AuthProfile,状态文件不要提交到仓库。
122
156
 
@@ -0,0 +1,38 @@
1
+ export interface BackendProbe {
2
+ available: boolean;
3
+ reason?: string;
4
+ }
5
+ export interface Backend<I, O> {
6
+ id: string;
7
+ probe(): BackendProbe;
8
+ run(input: I): Promise<O>;
9
+ }
10
+ export interface BackendDiagnostic {
11
+ id: string;
12
+ available: boolean;
13
+ state: 'ready' | 'unavailable' | 'cooldown';
14
+ reason?: string;
15
+ lastError?: string;
16
+ cooldownUntil?: string;
17
+ }
18
+ export declare class BackendRegistry<I, O> {
19
+ private readonly options;
20
+ private readonly entries;
21
+ private readonly failures;
22
+ constructor(options?: {
23
+ cooldownMs?: number;
24
+ });
25
+ register(backend: Backend<I, O>): this;
26
+ run(input: I, options: {
27
+ preferred: readonly string[];
28
+ override?: string;
29
+ }): Promise<O>;
30
+ runSelected(input: I, options: {
31
+ preferred: readonly string[];
32
+ override?: string;
33
+ }): Promise<{
34
+ id: string;
35
+ value: O;
36
+ }>;
37
+ diagnostics(): BackendDiagnostic[];
38
+ }
@@ -0,0 +1,59 @@
1
+ export class BackendRegistry {
2
+ options;
3
+ entries = new Map();
4
+ failures = new Map();
5
+ constructor(options = {}) {
6
+ this.options = options;
7
+ }
8
+ register(backend) {
9
+ if (this.entries.has(backend.id))
10
+ throw new Error('duplicate backend: ' + backend.id);
11
+ this.entries.set(backend.id, backend);
12
+ return this;
13
+ }
14
+ async run(input, options) {
15
+ return (await this.runSelected(input, options)).value;
16
+ }
17
+ async runSelected(input, options) {
18
+ const ids = options.override ? [options.override] : options.preferred;
19
+ const errors = [];
20
+ for (const id of ids) {
21
+ const backend = this.entries.get(id);
22
+ if (!backend) {
23
+ errors.push(id + ': unknown');
24
+ continue;
25
+ }
26
+ const failed = this.failures.get(id);
27
+ if (failed && failed.until > Date.now()) {
28
+ errors.push(id + ': cooldown');
29
+ continue;
30
+ }
31
+ const probe = backend.probe();
32
+ if (!probe.available) {
33
+ errors.push(id + ': ' + (probe.reason ?? 'unavailable'));
34
+ continue;
35
+ }
36
+ try {
37
+ const result = await backend.run(input);
38
+ this.failures.delete(id);
39
+ return { id, value: result };
40
+ }
41
+ catch (error) {
42
+ const message = error instanceof Error ? error.message : String(error);
43
+ this.failures.set(id, { message, until: Date.now() + (this.options.cooldownMs ?? 30_000) });
44
+ errors.push(id + ': ' + message);
45
+ }
46
+ }
47
+ throw new Error('no backend succeeded: ' + errors.join('; '));
48
+ }
49
+ diagnostics() {
50
+ return [...this.entries.values()].map(backend => {
51
+ const probe = backend.probe();
52
+ const failed = this.failures.get(backend.id);
53
+ if (failed && failed.until > Date.now())
54
+ return { id: backend.id, available: probe.available, state: 'cooldown', lastError: failed.message, cooldownUntil: new Date(failed.until).toISOString() };
55
+ return { id: backend.id, available: probe.available, state: probe.available ? 'ready' : 'unavailable', ...probe.reason ? { reason: probe.reason } : {} };
56
+ });
57
+ }
58
+ }
59
+ //# sourceMappingURL=backend-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend-registry.js","sourceRoot":"","sources":["../src/backend-registry.ts"],"names":[],"mappings":"AAIA,MAAM,OAAO,eAAe;IAGG;IAFZ,OAAO,GAAG,IAAI,GAAG,EAAyB,CAAA;IAC1C,QAAQ,GAAG,IAAI,GAAG,EAA8C,CAAA;IACjF,YAA6B,UAAmC,EAAE;QAArC,YAAO,GAAP,OAAO,CAA8B;IAAG,CAAC;IAEtE,QAAQ,CAAC,OAAsB;QAC7B,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;QACrF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;QACrC,OAAO,IAAI,CAAA;IACb,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,KAAQ,EAAE,OAA4D;QAC9E,OAAO,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,CAAA;IACvD,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAAQ,EAAE,OAA4D;QACtF,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAA;QACrE,MAAM,MAAM,GAAa,EAAE,CAAA;QAC3B,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;YACrB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YACpC,IAAI,CAAC,OAAO,EAAE,CAAC;gBAAC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,WAAW,CAAC,CAAC;gBAAC,SAAQ;YAAC,CAAC;YACzD,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YACpC,IAAI,MAAM,IAAI,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;gBAAC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,YAAY,CAAC,CAAC;gBAAC,SAAQ;YAAC,CAAC;YACrF,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,CAAA;YAC7B,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;gBAAC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,aAAa,CAAC,CAAC,CAAC;gBAAC,SAAQ;YAAC,CAAC;YAC5F,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;gBACvC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;gBACxB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAA;YAC9B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;gBACtE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,MAAM,CAAC,EAAE,CAAC,CAAA;gBAC3F,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,CAAA;YAClC,CAAC;QACH,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAC/D,CAAC;IAED,WAAW;QACT,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE;YAC9C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,CAAA;YAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YAC5C,IAAI,MAAM,IAAI,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE;gBAAE,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,CAAC,OAAO,EAAE,aAAa,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,EAAE,CAAA;YACjM,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAA;QAC1J,CAAC,CAAC,CAAA;IACJ,CAAC;CACF"}
@@ -38,11 +38,15 @@ export interface BrowserService {
38
38
  signal?: AbortSignal;
39
39
  maxChars?: number;
40
40
  waitMs?: number;
41
+ authProfile?: string;
42
+ rulePack?: string;
41
43
  }): Promise<BrowserRenderResult>;
42
44
  snapshot(url: string, rules: readonly RenderRule[], opts: {
43
45
  signal?: AbortSignal;
44
46
  outDir: string;
45
47
  maxChars?: number;
48
+ authProfile?: string;
49
+ rulePack?: string;
46
50
  }): Promise<BrowserSnapshotResult>;
47
51
  searchResults(url: string, spec: {
48
52
  item: string;
@@ -59,6 +63,8 @@ export interface BrowserService {
59
63
  domain: string;
60
64
  path: string;
61
65
  }[];
66
+ authProfile?: string;
67
+ rulePack?: string;
62
68
  }): Promise<BrowserSearchItem[]>;
63
69
  opencli(args: string[], opts?: {
64
70
  timeoutMs?: number;
@@ -0,0 +1,17 @@
1
+ export interface SearchCacheInput {
2
+ query: string;
3
+ engines: readonly string[];
4
+ count: number;
5
+ multi: boolean;
6
+ exa?: Record<string, unknown>;
7
+ }
8
+ export interface PlatformCacheInput {
9
+ platform: string;
10
+ query: string;
11
+ url?: string;
12
+ count: number;
13
+ authProfile?: string;
14
+ rulePack?: string;
15
+ }
16
+ export declare function createSearchCacheKey(input: SearchCacheInput): string;
17
+ export declare function createPlatformCacheKey(input: PlatformCacheInput): string;
@@ -0,0 +1,22 @@
1
+ import crypto from 'node:crypto';
2
+ function canonical(value) {
3
+ if (Array.isArray(value))
4
+ return value.map(canonical);
5
+ if (value && typeof value === 'object') {
6
+ return Object.fromEntries(Object.entries(value)
7
+ .filter(([, v]) => v !== undefined)
8
+ .sort(([a], [b]) => a.localeCompare(b))
9
+ .map(([k, v]) => [k, canonical(v)]));
10
+ }
11
+ return value;
12
+ }
13
+ function fingerprint(kind, input) {
14
+ return kind + ':v2:' + crypto.createHash('sha256').update(JSON.stringify(canonical(input))).digest('hex');
15
+ }
16
+ export function createSearchCacheKey(input) {
17
+ return fingerprint('search', { ...input, query: input.query.trim().replace(/\s+/g, ' ').toLowerCase() });
18
+ }
19
+ export function createPlatformCacheKey(input) {
20
+ return fingerprint('platform', { ...input, query: input.query.trim().replace(/\s+/g, ' ').toLowerCase() });
21
+ }
22
+ //# sourceMappingURL=cache-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-key.js","sourceRoot":"","sources":["../src/cache-key.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAA;AAmBhC,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACrD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC;aACvE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC;aAClC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;aACtC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACxC,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,KAAc;IAC/C,OAAO,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC3G,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAuB;IAC1D,OAAO,WAAW,CAAC,QAAQ,EAAE,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;AAC1G,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,KAAyB;IAC9D,OAAO,WAAW,CAAC,UAAU,EAAE,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;AAC5G,CAAC"}
package/lib/config.d.ts CHANGED
@@ -15,6 +15,12 @@ export interface CustomPlatformSpec {
15
15
  /** Optional raw Cookie header (`a=b; c=d`); cookies are applied to the URL's domain. */
16
16
  cookie?: string;
17
17
  }
18
+ export interface BrowserBinding {
19
+ /** Named dsh-browser auth profile. */
20
+ authProfile?: string;
21
+ /** Named dsh-browser enhancement rule pack. */
22
+ rulePack?: string;
23
+ }
18
24
  export interface Config {
19
25
  /** SQLite database path; defaults to $DSH_HOME/data/web-search-pro/store.db */
20
26
  dbPath?: string;
@@ -71,6 +77,8 @@ export interface Config {
71
77
  }>;
72
78
  /** User-defined custom platform search: url template + selectors + optional cookie. */
73
79
  customPlatforms?: Record<string, CustomPlatformSpec>;
80
+ /** Per-platform binding to domain-scoped dsh-browser auth/rule profiles. */
81
+ browserBindings?: Record<string, BrowserBinding>;
74
82
  /** Snapshot options. The browser runtime itself (channel/headless/storageStatePath) is provided by the dsh-browser plugin via the `browser` service. */
75
83
  playwright: {
76
84
  /** Gate the playwright fallback backend in web_fetch_pro. */
package/lib/config.js CHANGED
@@ -44,6 +44,10 @@ export const Config = z.object({
44
44
  text: z.string(),
45
45
  cookie: z.string(),
46
46
  })),
47
+ browserBindings: z.dict(z.object({
48
+ authProfile: z.string(),
49
+ rulePack: z.string(),
50
+ })),
47
51
  playwright: z.object({
48
52
  enabled: z.boolean().default(true),
49
53
  snapshotDir: z.string(),
package/lib/config.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/lib/deps.js CHANGED
@@ -37,7 +37,7 @@ const DEPS = [
37
37
  ],
38
38
  },
39
39
  {
40
- id: 'mcporter', label: 'mcporter', usedBy: '(预留:Exa MCP 后端)',
40
+ id: 'mcporter', label: 'mcporter', usedBy: 'Exa MCP fallback',
41
41
  installs: [
42
42
  { installer: 'npm', command: 'npm i -g mcporter' },
43
43
  ],
package/lib/deps.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"deps.js","sourceRoot":"","sources":["../src/deps.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"deps.js","sourceRoot":"","sources":["../src/deps.ts"],"names":[],"mappings":"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"}
package/lib/engines.d.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import type { WebSearchSource, WebRuntime } from '@deepseek-ai/dsh-web';
8
8
  import type { BrowserService } from './browser-service.ts';
9
9
  import type { CustomPlatformSpec } from './config.ts';
10
+ import { type ExaSearchRequest } from './exa-client.ts';
10
11
  export interface SearchOutcome {
11
12
  /** Provider-generated answer/summary text, when any. */
12
13
  content?: string;
@@ -17,7 +18,14 @@ export interface Engine {
17
18
  label: string;
18
19
  /** Cheap local availability check; must not do network I/O. */
19
20
  available(): boolean;
20
- search(query: string, count: number, signal?: AbortSignal): Promise<SearchOutcome>;
21
+ search(query: string, count: number, signal?: AbortSignal, options?: EngineSearchOptions): Promise<SearchOutcome>;
22
+ }
23
+ export interface EngineSearchOptions {
24
+ exa?: Omit<ExaSearchRequest, 'query' | 'numResults'>;
25
+ browser?: {
26
+ authProfile?: string;
27
+ rulePack?: string;
28
+ };
21
29
  }
22
30
  export declare class EngineError extends Error {
23
31
  readonly code: string;
@@ -48,6 +56,8 @@ export interface EngineDeps {
48
56
  skipSeam: boolean;
49
57
  }
50
58
  export declare function seamEngine(deps: EngineDeps): Engine;
59
+ /** Parse mcporter's human-readable Exa response into the router's native source shape. */
60
+ export declare function parseMcporterExaSearch(output: string, count: number): WebSearchSource[];
51
61
  export declare function exaEngine(deps: EngineDeps): Engine;
52
62
  export declare function ddgEngine(): Engine;
53
63
  export declare function bingEngine(): Engine;
package/lib/engines.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { httpGet, runCli, jsYaml, stripTags, capText, decodeRedirectUrl } from "./util.js";
8
8
  import { PLATFORM_SEARCH_SPECS, parseCookieString } from "./platform-search.js";
9
+ import { ExaClient } from "./exa-client.js";
9
10
  export class EngineError extends Error {
10
11
  code;
11
12
  retryable;
@@ -28,7 +29,7 @@ export function seamEngine(deps) {
28
29
  id: 'seam',
29
30
  label: 'DeepSeek 原生搜索 (ctx.web)',
30
31
  available: () => !!deps.web && !deps.skipSeam,
31
- async search(query, count, signal) {
32
+ async search(query, count, signal, options) {
32
33
  if (!deps.web)
33
34
  throw new EngineError('ctx.web seam unavailable', 'ENGINE_UNAVAILABLE');
34
35
  const result = await withTimeout(deps.web.search({ query, maxResults: count }, signal), 45_000, 'seam search');
@@ -36,30 +37,66 @@ export function seamEngine(deps) {
36
37
  },
37
38
  };
38
39
  }
39
- // ── Exa (API key) ────────────────────────────────────────────────────────────
40
+ // ── Exa (native API, with connected MCP fallback) ───────────────────────────
41
+ /** Parse mcporter's human-readable Exa response into the router's native source shape. */
42
+ export function parseMcporterExaSearch(output, count) {
43
+ const sources = [];
44
+ for (const block of output.split(/\r?\n---\r?\n/g)) {
45
+ const url = /^URL:\s*(https?:\/\/\S+)\s*$/im.exec(block)?.[1];
46
+ if (!url)
47
+ continue;
48
+ const title = /^Title:\s*(.+?)\s*$/im.exec(block)?.[1]?.trim();
49
+ const publishedAt = /^Published:\s*(.+?)\s*$/im.exec(block)?.[1]?.trim();
50
+ const highlights = /(?:^|\r?\n)Highlights:\s*([\s\S]*)$/i.exec(block)?.[1]
51
+ ?.replace(/\r?\n\s*\.\.\.\s*$/g, '')
52
+ .trim();
53
+ sources.push({
54
+ url,
55
+ ...title ? { title } : {},
56
+ ...highlights ? { snippet: capText(stripTags(highlights), 400) } : {},
57
+ ...publishedAt && publishedAt !== 'N/A' ? { publishedAt } : {},
58
+ });
59
+ if (sources.length >= count)
60
+ break;
61
+ }
62
+ return sources;
63
+ }
40
64
  export function exaEngine(deps) {
41
65
  const key = () => deps.exaApiKey || process.env.EXA_API_KEY;
42
66
  return {
43
67
  id: 'exa',
44
68
  label: 'Exa',
45
- available: () => (key()?.length ?? 0) > 0,
46
- async search(query, count, signal) {
47
- const res = await httpGet('https://api.exa.ai/search', {
48
- method: 'POST',
49
- headers: { 'x-api-key': key(), 'content-type': 'application/json' },
50
- body: JSON.stringify({ query, numResults: Math.min(count, 10), type: 'auto', contents: { text: false, highlights: true } }),
51
- signal,
52
- timeoutMs: 30_000,
53
- });
54
- if (!res.ok)
55
- throw new EngineError('Exa API error HTTP ' + res.status, 'ENGINE_ERROR');
56
- const data = JSON.parse(res.text);
57
- const sources = (data.results ?? []).map(r => ({
58
- url: r.url ?? '',
59
- ...r.title ? { title: r.title } : {},
60
- ...(r.highlights?.length ? { snippet: capText(r.highlights.join(' '), 400) } : {}),
61
- ...r.publishedDate ? { publishedAt: r.publishedDate } : {},
62
- })).filter(s => s.url.length > 0);
69
+ // A literal key enables the native client. In CLI-enabled profiles,
70
+ // mcporter can use the user's existing Exa MCP connection without copying
71
+ // credentials into this process.
72
+ available: () => (key()?.length ?? 0) > 0 || deps.enableCli,
73
+ async search(query, count, signal, options) {
74
+ const apiKey = key();
75
+ if (apiKey) {
76
+ const data = await new ExaClient({ apiKey }).search({ query, numResults: Math.min(count, 100), type: 'auto', ...options?.exa }, signal);
77
+ const sources = data.map(r => ({
78
+ url: r.url,
79
+ ...r.title ? { title: r.title } : {},
80
+ ...(r.highlights?.length ? { snippet: capText(r.highlights.join(' '), 400) } : {}),
81
+ ...r.publishedDate ? { publishedAt: r.publishedDate } : {},
82
+ }));
83
+ return { sources };
84
+ }
85
+ if (!deps.enableCli)
86
+ throw new EngineError('Exa unavailable: configure EXA_API_KEY or enable CLI backends with mcporter', 'ENGINE_UNAVAILABLE', false);
87
+ const result = await runCli('mcporter', [
88
+ 'call',
89
+ 'exa.web_search_exa',
90
+ 'query=' + query,
91
+ 'numResults=' + String(Math.min(count, 100)),
92
+ ], { timeoutMs: 60_000, signal, maxOutput: 4 * 1024 * 1024 });
93
+ if (result.code !== 0) {
94
+ const detail = result.stderr.trim() || result.stdout.trim() || 'exit ' + result.code;
95
+ throw new EngineError('Exa MCP search failed: ' + capText(detail, 300), 'ENGINE_ERROR', true);
96
+ }
97
+ const sources = parseMcporterExaSearch(result.stdout, count);
98
+ if (!sources.length)
99
+ throw new EngineError('Exa MCP returned no parseable results', 'ENGINE_EMPTY', true);
63
100
  return { sources };
64
101
  },
65
102
  };
@@ -525,12 +562,12 @@ export function customPlatformEngine(id, spec, deps) {
525
562
  id: 'custom-' + id,
526
563
  label: spec.name + ' (自定义)',
527
564
  available: () => !!deps.browser,
528
- async search(query, count, signal) {
565
+ async search(query, count, signal, options) {
529
566
  if (!deps.browser)
530
567
  throw new EngineError('custom platform search unavailable (no browser service)', 'ENGINE_UNAVAILABLE', false);
531
568
  const url = spec.url.replace(/{query}/g, encodeURIComponent(query));
532
569
  const cookies = spec.cookie ? parseCookieString(spec.cookie, url) : undefined;
533
- const sources = await deps.browser.searchResults(url, searchSpec, { signal, count, cookies });
570
+ const sources = await deps.browser.searchResults(url, searchSpec, { signal, count, cookies, ...options?.browser });
534
571
  if (!sources.length)
535
572
  throw new EngineError('自定义平台 ' + spec.name + ' 未取到结果:检查 url 的 {query} 占位、item/title/link 选择器,或补充 cookie。', 'ENGINE_EMPTY', false);
536
573
  return { sources };
@@ -544,12 +581,12 @@ export function playwrightPlatformEngine(platform, deps) {
544
581
  id: 'playwright-' + platform,
545
582
  label: (builtin?.label ?? platform) + ' (Playwright)',
546
583
  available: () => !!builtin && !!deps.browser,
547
- async search(query, count, signal) {
584
+ async search(query, count, signal, options) {
548
585
  if (!builtin || !deps.browser)
549
586
  throw new EngineError('playwright platform search unavailable for ' + platform, 'ENGINE_UNAVAILABLE', false);
550
587
  const override = deps.platformRules?.[platform];
551
588
  const spec = { ...builtin, ...override ?? {} };
552
- const sources = await deps.browser.searchResults(spec.url(query), spec, { signal, count });
589
+ const sources = await deps.browser.searchResults(spec.url(query), spec, { signal, count, ...options?.browser });
553
590
  if (!sources.length) {
554
591
  throw new EngineError(builtin.label + ' 未取到结果:该平台需要浏览器登录态(复用你已登录的浏览器)。运行 node scripts/save-login.mjs 登录一次并设置 dsh-browser 的 storageStatePath;或到 $DSH_HOME/settings.yaml 的 platformRules.' + platform + ' 微调结果选择器。', 'ENGINE_EMPTY', false);
555
592
  }