dsh-web-search-pro 0.1.0

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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/LOGIN.md +67 -0
  3. package/README.md +123 -0
  4. package/cordis.patch.yml +21 -0
  5. package/lib/browser-service.d.ts +68 -0
  6. package/lib/browser-service.js +8 -0
  7. package/lib/browser-service.js.map +1 -0
  8. package/lib/config.d.ts +91 -0
  9. package/lib/config.js +72 -0
  10. package/lib/config.js.map +1 -0
  11. package/lib/deps.d.ts +34 -0
  12. package/lib/deps.js +90 -0
  13. package/lib/deps.js.map +1 -0
  14. package/lib/engines.d.ts +71 -0
  15. package/lib/engines.js +562 -0
  16. package/lib/engines.js.map +1 -0
  17. package/lib/extract.d.ts +40 -0
  18. package/lib/extract.js +243 -0
  19. package/lib/extract.js.map +1 -0
  20. package/lib/fetch.d.ts +42 -0
  21. package/lib/fetch.js +175 -0
  22. package/lib/fetch.js.map +1 -0
  23. package/lib/index.d.ts +23 -0
  24. package/lib/index.js +117 -0
  25. package/lib/index.js.map +1 -0
  26. package/lib/memory-cache.d.ts +18 -0
  27. package/lib/memory-cache.js +42 -0
  28. package/lib/memory-cache.js.map +1 -0
  29. package/lib/platform-search.d.ts +34 -0
  30. package/lib/platform-search.js +61 -0
  31. package/lib/platform-search.js.map +1 -0
  32. package/lib/playwright.d.ts +71 -0
  33. package/lib/playwright.js +165 -0
  34. package/lib/playwright.js.map +1 -0
  35. package/lib/router.d.ts +66 -0
  36. package/lib/router.js +332 -0
  37. package/lib/router.js.map +1 -0
  38. package/lib/store.d.ts +117 -0
  39. package/lib/store.js +223 -0
  40. package/lib/store.js.map +1 -0
  41. package/lib/tools.d.ts +28 -0
  42. package/lib/tools.js +542 -0
  43. package/lib/tools.js.map +1 -0
  44. package/lib/util.d.ts +73 -0
  45. package/lib/util.js +220 -0
  46. package/lib/util.js.map +1 -0
  47. package/package.json +59 -0
  48. package/scripts/save-login.mjs +77 -0
package/lib/index.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * web-search-pro — 增强型、可持久化的扩展网页搜索插件 for DeepSeek Harness.
3
+ *
4
+ * - Multi-backend search routing with automatic fallback (agent-reach style):
5
+ * ctx.web seam / Exa / DuckDuckGo / Bing / Jina + platform backends
6
+ * (gh, bili-cli, yt-dlp, sov2ex, opencli, agent-reach).
7
+ * - Persistent SQLite store (MediaCrawler style): search queries + results,
8
+ * page snapshots, and user-extended per-site extraction rules survive
9
+ * restarts and are reused within a configurable TTL.
10
+ * - Userscript-style per-site extraction rules ("脚本猫/油猴" style) applied
11
+ * by the fetch pipeline (Jina Reader → HTTP+extraction → Playwright).
12
+ * - Optional ctx.web provider registration so the built-in web_search /
13
+ * web_fetch tools can route through this plugin.
14
+ *
15
+ * @module web-search-pro
16
+ */
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
20
+ import { Config, resolveConfig } from "./config.js";
21
+ import { Store } from "./store.js";
22
+ import { SearchRouter } from "./router.js";
23
+ import { FetchService } from "./fetch.js";
24
+ import { registerTools } from "./tools.js";
25
+ export const name = 'web-search-pro';
26
+ export const inject = ['tools', 'systemPrompt', 'browser'];
27
+ export { Config };
28
+ const TOOL_NAMES = [
29
+ 'web_search_pro', 'web_fetch_pro', 'web_platform_search', 'web_snapshot',
30
+ 'web_history', 'web_cache_clear', 'web_rule', 'web_search_stats', 'web_deps',
31
+ ];
32
+ export function apply(ctx, config) {
33
+ const resolved = resolveConfig(config);
34
+ const dbPath = resolved.dbPath;
35
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
36
+ // 1. Persistent store (closed on plugin unload).
37
+ const store = new Store(dbPath);
38
+ ctx.effect(() => () => store.close());
39
+ // 2. Browser service (provided by dsh-browser; inject: ['browser']).
40
+ const browser = ctx.get('browser');
41
+ // 3. Hot-reloadable config source: while a settings service exists, the
42
+ // $DSH_HOME/settings.yaml `web-search-pro:` section (validated against the
43
+ // same schema) overlays this composition entry and every operation re-reads
44
+ // it — edit keys/engines in the file without touching cordis.yml.
45
+ // installSettingsSection: 官方可选 settings 消费者接线(base=插件配置,
46
+ // 用户段落热重载;settings 服务缺席时回退到插件配置)。
47
+ // Stable accessor over a mutable source ref: installSettingsSection swaps
48
+ // the source thunk on settings attach/detach, while router/fetch/tools all
49
+ // hold the SAME stable `dynamic` closure that dereferences it per call —
50
+ // so hot-reloaded sections reach every consumer.
51
+ let resolveSource = () => resolved;
52
+ const dynamic = () => resolveSource();
53
+ installSettingsSection(ctx, settingsNamespace('web-search-pro'), Config, resolved, {
54
+ setSource: (current) => { resolveSource = () => current(); },
55
+ onChange: () => { },
56
+ });
57
+ // 4. Services.
58
+ const router = new SearchRouter(ctx, resolved, store, dynamic, browser);
59
+ const fetchSvc = new FetchService(store, dynamic, browser);
60
+ // 5. Tools.
61
+ registerTools({ ctx, config: resolved, dynamic, store, router, fetch: fetchSvc, browser });
62
+ // 5. Optional ctx.web provider registration: the built-in web_search /
63
+ // web_fetch tools route through this plugin when configured via
64
+ // DSH_WEB_SEARCH_PROVIDER=web-search-pro (or the web row's
65
+ // searchProvider). Registration is idempotent per fiber (effect-scoped).
66
+ const web = ctx.get('web');
67
+ if (web && resolved.registerProvider) {
68
+ web.registerSearchProvider({
69
+ id: resolved.providerId,
70
+ available: () => router.anyEngineAvailable(),
71
+ search: (request, signal) => router.searchAsProvider(request, signal),
72
+ });
73
+ web.registerFetchProvider({
74
+ id: resolved.providerId,
75
+ available: () => true,
76
+ fetch: async (request, signal) => {
77
+ const out = await fetchSvc.fetchPage(request.url, {
78
+ mode: 'auto',
79
+ signal,
80
+ maxChars: 200_000,
81
+ fresh: false,
82
+ persist: true,
83
+ });
84
+ return {
85
+ url: out.url,
86
+ statusCode: out.statusCode ?? 200,
87
+ body: { kind: 'text', content: out.text },
88
+ truncated: false,
89
+ };
90
+ },
91
+ });
92
+ }
93
+ // 6. System prompt guidance.
94
+ ctx.systemPrompt.section({
95
+ name: 'tool:web-search-pro',
96
+ order: 112,
97
+ text: 'For web research prefer the persistent enhanced tools: web_search_pro (multi-engine search with caching and history), web_platform_search (GitHub/B站/YouTube/V2EX/小红书/Twitter/Reddit/RSS/知乎/微博/豆瓣/贴吧/抖音/快手…), web_fetch_pro (readable extraction with per-site rules), and web_snapshot (headless-browser capture). Cite the relevant URLs as markdown links in your answer. The browser runtime (Playwright + chromium) and opencli are bundled in the dsh-browser plugin — no global CLIs needed; use browser_open/browser_click/browser_type/browser_scroll/browser_read/browser_screenshot for interactive multi-step browsing. Before relying on a remaining external CLI backend (gh/bili/yt-dlp/agent-reach), run web_deps action=check. Chinese communities (zhihu/weibo/douban/tieba/douyin/kuaishou) need the user to log in once to reuse their browser login state — if they return no results, tell the user to run scripts/save-login.mjs and set the dsh-browser storageStatePath.',
98
+ });
99
+ // 7. Apply marker for diagnostics (proves live registration).
100
+ if (resolved.verbose) {
101
+ try {
102
+ const markerPath = path.join(path.dirname(dbPath), 'apply.log');
103
+ fs.appendFileSync(markerPath, JSON.stringify({
104
+ ts: new Date().toISOString(),
105
+ plugin: name,
106
+ dbPath,
107
+ tools: TOOL_NAMES,
108
+ provider: resolved.registerProvider ? resolved.providerId : undefined,
109
+ engines: resolved.engines,
110
+ browser: 'injected',
111
+ }) + '\n', 'utf8');
112
+ }
113
+ catch { /* marker is best-effort */ }
114
+ }
115
+ ctx.logger?.(name).info('web-search-pro loaded: db=' + dbPath + ' engines=[' + resolved.engines.join(',') + ']');
116
+ }
117
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Minimal in-process LRU cache (Map + recency bump). Layered above SQLite:
3
+ * hot searches/page fetches resolve in microseconds without touching the DB.
4
+ * @module web-search-pro/memory-cache
5
+ */
6
+ export interface LruEntry<T> {
7
+ ts: number;
8
+ value: T;
9
+ }
10
+ export declare class LruCache<T> {
11
+ private readonly capacity;
12
+ private readonly map;
13
+ constructor(capacity: number);
14
+ get(key: string, ttlMs: number): T | undefined;
15
+ set(key: string, value: T): void;
16
+ clear(): void;
17
+ get size(): number;
18
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Minimal in-process LRU cache (Map + recency bump). Layered above SQLite:
3
+ * hot searches/page fetches resolve in microseconds without touching the DB.
4
+ * @module web-search-pro/memory-cache
5
+ */
6
+ export class LruCache {
7
+ capacity;
8
+ map = new Map();
9
+ constructor(capacity) {
10
+ this.capacity = capacity;
11
+ }
12
+ get(key, ttlMs) {
13
+ const entry = this.map.get(key);
14
+ if (!entry)
15
+ return undefined;
16
+ if (Date.now() - entry.ts > ttlMs) {
17
+ this.map.delete(key);
18
+ return undefined;
19
+ }
20
+ // recency bump
21
+ this.map.delete(key);
22
+ this.map.set(key, entry);
23
+ return entry.value;
24
+ }
25
+ set(key, value) {
26
+ this.map.delete(key);
27
+ this.map.set(key, { ts: Date.now(), value });
28
+ while (this.map.size > this.capacity) {
29
+ const oldest = this.map.keys().next().value;
30
+ if (oldest === undefined)
31
+ break;
32
+ this.map.delete(oldest);
33
+ }
34
+ }
35
+ clear() {
36
+ this.map.clear();
37
+ }
38
+ get size() {
39
+ return this.map.size;
40
+ }
41
+ }
42
+ //# sourceMappingURL=memory-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-cache.js","sourceRoot":"","sources":["../src/memory-cache.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,MAAM,OAAO,QAAQ;IAGU;IAFZ,GAAG,GAAG,IAAI,GAAG,EAAuB,CAAA;IAErD,YAA6B,QAAgB;QAAhB,aAAQ,GAAR,QAAQ,CAAQ;IAAG,CAAC;IAEjD,GAAG,CAAC,GAAW,EAAE,KAAa;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC/B,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAA;QAC5B,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC;YAClC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACpB,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,eAAe;QACf,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACpB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QACxB,OAAO,KAAK,CAAC,KAAK,CAAA;IACpB,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,KAAQ;QACvB,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACpB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;QAC5C,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YACrC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAA2B,CAAA;YACjE,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAK;YAC/B,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACzB,CAAC;IACH,CAAC;IAED,KAAK;QACH,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAA;IAClB,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAA;IACtB,CAAC;CACF"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Chinese-community platform search via Playwright (MediaCrawler's approach,
3
+ * implemented cleanly with our own per-site rules): open each platform's search
4
+ * page in the user's logged-in browser (storageState) and read the rendered
5
+ * result list. The browser performs any signing the site needs — we never call
6
+ * private APIs or reproduce sign algorithms.
7
+ *
8
+ * NOTE: each platform requires the user's login state (storageState). Result
9
+ * selectors are best-effort and may need tuning per site's current markup.
10
+ * @module web-search-pro/platform-search
11
+ */
12
+ export interface PlatformSearchSpec {
13
+ id: string;
14
+ label: string;
15
+ /** Build the search-page URL for a query. */
16
+ url: (query: string) => string;
17
+ /** Selector for one result item. */
18
+ item: string;
19
+ /** Selector for the title element inside an item (textContent). */
20
+ title: string;
21
+ /** Selector for the link element inside an item (href). */
22
+ link: string;
23
+ /** Optional selector for the snippet text inside an item. */
24
+ text?: string;
25
+ }
26
+ /** Best-effort search-page specs for Chinese community platforms. */
27
+ export declare const PLATFORM_SEARCH_SPECS: Record<string, PlatformSearchSpec>;
28
+ /** Parse a raw Cookie header ("a=b; c=d") into Playwright cookies for the URL's domain. */
29
+ export declare function parseCookieString(cookie: string, url: string): {
30
+ name: string;
31
+ value: string;
32
+ domain: string;
33
+ path: string;
34
+ }[];
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Chinese-community platform search via Playwright (MediaCrawler's approach,
3
+ * implemented cleanly with our own per-site rules): open each platform's search
4
+ * page in the user's logged-in browser (storageState) and read the rendered
5
+ * result list. The browser performs any signing the site needs — we never call
6
+ * private APIs or reproduce sign algorithms.
7
+ *
8
+ * NOTE: each platform requires the user's login state (storageState). Result
9
+ * selectors are best-effort and may need tuning per site's current markup.
10
+ * @module web-search-pro/platform-search
11
+ */
12
+ /** Best-effort search-page specs for Chinese community platforms. */
13
+ export const PLATFORM_SEARCH_SPECS = {
14
+ zhihu: {
15
+ id: 'zhihu', label: '知乎',
16
+ url: q => 'https://www.zhihu.com/search?type=content&q=' + encodeURIComponent(q),
17
+ item: '.SearchResult-Card', title: '.ContentItem-title', link: '.ContentItem-title a', text: '.Highlight',
18
+ },
19
+ weibo: {
20
+ id: 'weibo', label: '微博',
21
+ url: q => 'https://s.weibo.com/weibo?q=' + encodeURIComponent(q),
22
+ item: '.card-wrap', title: '.txt', link: '.from a', text: '.txt',
23
+ },
24
+ douban: {
25
+ id: 'douban', label: '豆瓣',
26
+ url: q => 'https://www.douban.com/search?q=' + encodeURIComponent(q),
27
+ item: '.result', title: '.title a', link: '.title a', text: '.content p',
28
+ },
29
+ tieba: {
30
+ id: 'tieba', label: '百度贴吧',
31
+ url: q => 'https://tieba.baidu.com/f/search/res?ie=utf-8&qw=' + encodeURIComponent(q),
32
+ item: '.s_post', title: '.p_title a', link: '.p_title a', text: '.p_content',
33
+ },
34
+ douyin: {
35
+ id: 'douyin', label: '抖音',
36
+ url: q => 'https://www.douyin.com/search/' + encodeURIComponent(q),
37
+ item: '[data-e2e="search-item"]', title: 'a', link: 'a', text: 'p',
38
+ },
39
+ kuaishou: {
40
+ id: 'kuaishou', label: '快手',
41
+ url: q => 'https://www.kuaishou.com/search/video?searchKey=' + encodeURIComponent(q),
42
+ item: '.video-card', title: '.video-info-title', link: 'a', text: '.video-info-title',
43
+ },
44
+ };
45
+ /** Parse a raw Cookie header ("a=b; c=d") into Playwright cookies for the URL's domain. */
46
+ export function parseCookieString(cookie, url) {
47
+ let domain = '';
48
+ try {
49
+ domain = new URL(url).hostname;
50
+ }
51
+ catch { /* domain stays empty; caller must handle */ }
52
+ return cookie.split(';').map(p => p.trim()).filter(Boolean).map(p => {
53
+ const eq = p.indexOf('=');
54
+ const name = eq >= 0 ? p.slice(0, eq).trim() : p.trim();
55
+ const value = eq >= 0 ? p.slice(eq + 1).trim() : '';
56
+ return { name, value, domain, path: '/' };
57
+ }).filter(c => c.name.length > 0 && c.domain.length > 0);
58
+ }
59
+ // (Page-side extraction moved into the dsh-browser plugin's BrowserService;
60
+ // web-search-pro now calls browser.searchResults(url, spec, opts).)
61
+ //# sourceMappingURL=platform-search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-search.js","sourceRoot":"","sources":["../src/platform-search.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Playwright integration (MediaCrawler-style): headless browser rendering
3
+ * with optional persistent login state (storageState), used as the last-resort
4
+ * fetch backend and for page snapshots. The browser is launched lazily and
5
+ * reused within the plugin lifetime; the plugin disposes it on unload.
6
+ *
7
+ * The page-side extractor is a raw JS string (not a closure): tsx/esbuild
8
+ * injects a __name helper into compiled closures, which does not exist in the
9
+ * page context. Strings pass through unevaluated.
10
+ * @module web-search-pro/playwright
11
+ */
12
+ import type { ExtractRule } from './extract.ts';
13
+ import { type PlatformSearchSpec } from './platform-search.ts';
14
+ export interface PlaywrightConfig {
15
+ enabled: boolean;
16
+ headless: boolean;
17
+ /** 'chromium' (bundled) or 'msedge' (system Edge). */
18
+ channel: string;
19
+ /** Path to a Playwright storageState JSON (persisted login, like MediaCrawler). */
20
+ storageStatePath?: string;
21
+ /** Explicit module path override. */
22
+ modulePath?: string;
23
+ }
24
+ export interface RenderResult {
25
+ title: string;
26
+ text: string;
27
+ html: string;
28
+ usedRule: string | undefined;
29
+ }
30
+ export declare class PlaywrightManager {
31
+ private readonly config;
32
+ private browser;
33
+ private launching;
34
+ constructor(config: PlaywrightConfig);
35
+ private ensure;
36
+ close(): Promise<void>;
37
+ /** Render one page: goto, settle, extract via rules in the page itself. */
38
+ render(url: string, rules: readonly ExtractRule[], opts?: {
39
+ signal: AbortSignal | undefined;
40
+ maxChars?: number;
41
+ waitMs?: number;
42
+ }): Promise<RenderResult>;
43
+ /** Render + save a screenshot and HTML snapshot to disk (web_snapshot tool). */
44
+ snapshot(url: string, rules: readonly ExtractRule[], opts: {
45
+ signal: AbortSignal | undefined;
46
+ outDir: string;
47
+ maxChars?: number;
48
+ }): Promise<{
49
+ title: string;
50
+ text: string;
51
+ screenshotPath: string;
52
+ htmlPath: string;
53
+ usedRule: string | undefined;
54
+ }>;
55
+ /** Drive one platform's search page and extract its result list (Chinese communities / custom platforms). */
56
+ searchResults(url: string, spec: PlatformSearchSpec, opts: {
57
+ signal: AbortSignal | undefined;
58
+ count: number;
59
+ waitMs?: number;
60
+ cookies?: {
61
+ name: string;
62
+ value: string;
63
+ domain: string;
64
+ path: string;
65
+ }[];
66
+ }): Promise<{
67
+ url: string;
68
+ title: string;
69
+ snippet?: string;
70
+ }[]>;
71
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Playwright integration (MediaCrawler-style): headless browser rendering
3
+ * with optional persistent login state (storageState), used as the last-resort
4
+ * fetch backend and for page snapshots. The browser is launched lazily and
5
+ * reused within the plugin lifetime; the plugin disposes it on unload.
6
+ *
7
+ * The page-side extractor is a raw JS string (not a closure): tsx/esbuild
8
+ * injects a __name helper into compiled closures, which does not exist in the
9
+ * page context. Strings pass through unevaluated.
10
+ * @module web-search-pro/playwright
11
+ */
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { resolvePlaywright, uid, capText } from "./util.js";
15
+ import { searchPlatformResults } from "./platform-search.js";
16
+ /** Page-side extractor: rules-aware content picker + innerText (runs in the page). */
17
+ const EXTRACTOR_FN = "(ruleList) => {\n const doc = document\n const title = doc.title ? doc.title.trim() : ''\n let host = ''\n try { host = location.hostname } catch (e) {}\n const norm = (h) => h.replace(/^www\\\\./i, '').toLowerCase()\n let rule = null\n for (const r of ruleList) {\n const rh = norm(r.hostname)\n if (host === rh || host.endsWith('.' + rh)) rule = r\n }\n const pick = (selectors) => {\n for (const sel of selectors) {\n try {\n const el = doc.querySelector(sel)\n if (el && (el.textContent || '').trim().length > 40) return el\n } catch (e) {}\n }\n return null\n }\n const content = rule ? pick(rule.contentSelectors) : (pick(['article', 'main', '[role=\"main\"]']) || doc.body)\n if (content && rule && rule.removeSelectors) {\n for (const sel of rule.removeSelectors) {\n try { content.querySelectorAll(sel).forEach((el) => el.remove()) } catch (e) {}\n }\n }\n const text = content ? (content.innerText || content.textContent || '') : ''\n return { title, text, html: doc.documentElement.outerHTML.slice(0, 2000000), usedRule: rule ? rule.hostname : null }\n}";
18
+ /**
19
+ * Evaluate the extractor with the rule list embedded: the raw string is
20
+ * evaluated as an expression by Playwright, so an IIFE form is required —
21
+ * Playwright does not invoke string "functions" with the argument.
22
+ */
23
+ function evaluateExtractor(page, rules) {
24
+ return page.evaluate('(' + EXTRACTOR_FN + ')(' + JSON.stringify(JSON.parse(JSON.stringify(rules))) + ')');
25
+ }
26
+ "(ruleList) => {\n const doc = document\n const title = doc.title ? doc.title.trim() : ''\n let host = ''\n try { host = location.hostname } catch (e) {}\n const norm = (h) => h.replace(/^www\\\\./i, '').toLowerCase()\n let rule = null\n for (const r of ruleList) {\n const rh = norm(r.hostname)\n if (host === rh || host.endsWith('.' + rh)) rule = r\n }\n const pick = (selectors) => {\n for (const sel of selectors) {\n try {\n const el = doc.querySelector(sel)\n if (el && (el.textContent || '').trim().length > 40) return el\n } catch (e) {}\n }\n return null\n }\n const content = rule ? pick(rule.contentSelectors) : (pick(['article', 'main', '[role=\"main\"]']) || doc.body)\n if (content && rule && rule.removeSelectors) {\n for (const sel of rule.removeSelectors) {\n try { content.querySelectorAll(sel).forEach((el) => el.remove()) } catch (e) {}\n }\n }\n const text = content ? (content.innerText || content.textContent || '') : ''\n return { title, text, html: doc.documentElement.outerHTML.slice(0, 2000000), usedRule: rule ? rule.hostname : null }\n}";
27
+ export class PlaywrightManager {
28
+ config;
29
+ browser = undefined;
30
+ launching;
31
+ constructor(config) {
32
+ this.config = config;
33
+ }
34
+ async ensure() {
35
+ if (this.browser)
36
+ return this.browser;
37
+ if (!this.launching) {
38
+ this.launching = (async () => {
39
+ const pw = resolvePlaywright(this.config.modulePath);
40
+ const launchOptions = { headless: this.config.headless };
41
+ if (this.config.channel === 'msedge')
42
+ launchOptions.channel = 'msedge';
43
+ this.browser = await pw.chromium.launch(launchOptions);
44
+ return this.browser;
45
+ })().catch((error) => {
46
+ this.launching = undefined;
47
+ throw error;
48
+ });
49
+ }
50
+ return this.launching;
51
+ }
52
+ async close() {
53
+ const b = this.browser;
54
+ this.browser = undefined;
55
+ this.launching = undefined;
56
+ if (b) {
57
+ try {
58
+ await b.close();
59
+ }
60
+ catch { /* already closed */ }
61
+ }
62
+ }
63
+ /** Render one page: goto, settle, extract via rules in the page itself. */
64
+ async render(url, rules, opts = { signal: undefined }) {
65
+ const browser = await this.ensure();
66
+ const context = await browser.newContext(this.config.storageStatePath ? { storageState: this.config.storageStatePath } : {});
67
+ const page = await context.newPage();
68
+ const onAbort = () => void page.close().catch(() => { });
69
+ if (opts.signal?.aborted)
70
+ onAbort();
71
+ else
72
+ opts.signal?.addEventListener('abort', onAbort);
73
+ try {
74
+ page.setDefaultTimeout(20_000);
75
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 25_000 });
76
+ await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => { });
77
+ if (opts.waitMs)
78
+ await page.waitForTimeout(opts.waitMs);
79
+ const data = await evaluateExtractor(page, rules);
80
+ return {
81
+ title: data.title,
82
+ text: capText(String(data.text ?? '').replace(/\n{3,}/g, '\n\n').trim(), opts.maxChars ?? 200_000),
83
+ html: data.html,
84
+ usedRule: data.usedRule ?? undefined,
85
+ };
86
+ }
87
+ catch (error) {
88
+ throw new Error('playwright render failed for ' + url + ': ' + String(error).slice(0, 300));
89
+ }
90
+ finally {
91
+ opts.signal?.removeEventListener('abort', onAbort);
92
+ await context.close().catch(() => { });
93
+ }
94
+ }
95
+ /** Render + save a screenshot and HTML snapshot to disk (web_snapshot tool). */
96
+ async snapshot(url, rules, opts) {
97
+ const browser = await this.ensure();
98
+ const context = await browser.newContext(this.config.storageStatePath ? { storageState: this.config.storageStatePath } : {});
99
+ const page = await context.newPage();
100
+ const onAbort = () => void page.close().catch(() => { });
101
+ if (opts.signal?.aborted)
102
+ onAbort();
103
+ else
104
+ opts.signal?.addEventListener('abort', onAbort);
105
+ fs.mkdirSync(opts.outDir, { recursive: true });
106
+ const stamp = Date.now() + '-' + uid().slice(0, 8);
107
+ const screenshotPath = path.join(opts.outDir, stamp + '.png');
108
+ const htmlPath = path.join(opts.outDir, stamp + '.html');
109
+ try {
110
+ page.setDefaultTimeout(25_000);
111
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
112
+ await page.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => { });
113
+ await page.screenshot({ path: screenshotPath, fullPage: true });
114
+ const html = await page.content();
115
+ fs.writeFileSync(htmlPath, html, 'utf8');
116
+ const data = await evaluateExtractor(page, rules);
117
+ return {
118
+ title: data.title,
119
+ text: capText(String(data.text ?? '').replace(/\n{3,}/g, '\n\n').trim(), opts.maxChars ?? 200_000),
120
+ screenshotPath,
121
+ htmlPath,
122
+ usedRule: data.usedRule ?? undefined,
123
+ };
124
+ }
125
+ catch (error) {
126
+ throw new Error('playwright snapshot failed for ' + url + ': ' + String(error).slice(0, 300));
127
+ }
128
+ finally {
129
+ opts.signal?.removeEventListener('abort', onAbort);
130
+ await context.close().catch(() => { });
131
+ }
132
+ }
133
+ /** Drive one platform's search page and extract its result list (Chinese communities / custom platforms). */
134
+ async searchResults(url, spec, opts) {
135
+ const browser = await this.ensure();
136
+ const context = await browser.newContext(this.config.storageStatePath ? { storageState: this.config.storageStatePath } : {});
137
+ if (opts.cookies?.length)
138
+ await context.addCookies(opts.cookies).catch(() => { });
139
+ const page = await context.newPage();
140
+ const onAbort = () => void page.close().catch(() => { });
141
+ if (opts.signal?.aborted)
142
+ onAbort();
143
+ else
144
+ opts.signal?.addEventListener('abort', onAbort);
145
+ try {
146
+ page.setDefaultTimeout(25_000);
147
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
148
+ await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => { });
149
+ if (opts.waitMs)
150
+ await page.waitForTimeout(opts.waitMs);
151
+ // scroll once to trigger lazy lists, then extract
152
+ await page.mouse?.wheel(0, 2000).catch(() => { });
153
+ await page.waitForTimeout(800);
154
+ return await searchPlatformResults(page, spec, opts.count);
155
+ }
156
+ catch (error) {
157
+ throw new Error('playwright platform search failed for ' + url + ': ' + String(error).slice(0, 300));
158
+ }
159
+ finally {
160
+ opts.signal?.removeEventListener('abort', onAbort);
161
+ await context.close().catch(() => { });
162
+ }
163
+ }
164
+ }
165
+ //# sourceMappingURL=playwright.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright.js","sourceRoot":"","sources":["../src/playwright.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Search orchestration: engine ordering/fallback (agent-reach style routing),
3
+ * optional parallel multi-engine merging, SQLite caching, and persistence.
4
+ * @module web-search-pro/router
5
+ */
6
+ import type { Context } from '@deepseek-ai/cordis';
7
+ import type { WebSearchRequest, WebSearchResult } from '@deepseek-ai/dsh-web';
8
+ import type { Store } from './store.ts';
9
+ import type { ResolvedConfig } from './config.ts';
10
+ import { LruCache } from './memory-cache.ts';
11
+ import type { BrowserService } from './browser-service.ts';
12
+ export interface RouterSearchOptions {
13
+ query: string;
14
+ /** Engine ids to try, in order. Defaults to config.engines. */
15
+ engines?: string[];
16
+ count: number;
17
+ /** Bypass the fresh-cache lookup. */
18
+ fresh: boolean;
19
+ /** Run all requested engines in parallel and merge results. */
20
+ multi: boolean;
21
+ signal: AbortSignal | undefined;
22
+ /** True when called from the ctx.web provider (prevents seam recursion). */
23
+ skipSeam?: boolean;
24
+ }
25
+ export interface RouterSearchResult {
26
+ content?: string;
27
+ sources: {
28
+ url: string;
29
+ title?: string;
30
+ snippet?: string;
31
+ publishedAt?: string;
32
+ }[];
33
+ engine: string;
34
+ enginesTried: string[];
35
+ fromCache: boolean;
36
+ }
37
+ export declare class SearchRouter {
38
+ private readonly ctx;
39
+ private readonly config;
40
+ private readonly store;
41
+ private readonly dynamic;
42
+ private readonly browser?;
43
+ private readonly memory;
44
+ constructor(ctx: Context, config: ResolvedConfig, store: Store, dynamic?: () => ResolvedConfig, browser?: BrowserService | undefined, memory?: LruCache<RouterSearchResult>);
45
+ /** Resolve a key through credentials first, then process env. */
46
+ private resolveKey;
47
+ private deps;
48
+ /** Sync key check for available() (no credential resolution — env/literal only). */
49
+ private depsSync;
50
+ /** Whether any configured engine is currently usable. */
51
+ anyEngineAvailable(): boolean;
52
+ private build;
53
+ private buildSync;
54
+ /** Run a full search with caching + persistence. */
55
+ search(opts: RouterSearchOptions): Promise<RouterSearchResult>;
56
+ /** Platform search (web_platform_search tool) with the same cache+persist flow. */
57
+ platformSearch(platform: string, query: string, url: string | undefined, count: number, opts: {
58
+ signal?: AbortSignal;
59
+ fresh?: boolean;
60
+ }): Promise<RouterSearchResult>;
61
+ /**
62
+ * ctx.web provider adapter: route the seam request through this router.
63
+ * Returns a WebSearchResult-shaped value for the built-in web_search tool.
64
+ */
65
+ searchAsProvider(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
66
+ }