deepseek-web-retrieval 0.3.0 → 0.5.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.
- package/CHANGELOG.md +96 -230
- package/README.md +53 -25
- package/dist/cli.js +1358 -617
- package/docs/agents/codex.md +32 -11
- package/docs/agents/opencode-v2.md +5 -5
- package/docs/agents/pi.md +92 -90
- package/docs/design/installer-design.md +72 -9
- package/docs/hosts.jsonc +2 -2
- package/docs/integration.md +14 -9
- package/docs/research/control-matrix.md +48 -13
- package/docs/research/mcp-compatibility.md +9 -4
- package/docs/research/search-api-defaults.md +309 -0
- package/examples/pi.settings.jsonc +17 -0
- package/index.ts +2 -2
- package/package.json +35 -4
- package/src/core/auth.ts +4 -6
- package/src/core/client.ts +65 -53
- package/src/core/constants.ts +30 -24
- package/src/core/descriptions.ts +30 -6
- package/src/core/domains.ts +56 -67
- package/src/core/env.ts +112 -0
- package/src/core/format.ts +53 -50
- package/src/core/index.ts +9 -9
- package/src/core/jsonc.ts +6 -6
- package/src/core/object.ts +4 -0
- package/src/core/options.ts +21 -26
- package/src/core/search.ts +45 -44
- package/src/core/transport.ts +109 -113
- package/src/core/types.ts +47 -47
- package/src/core/url.ts +20 -20
- package/src/installer/apply.ts +256 -81
- package/src/installer/cli.ts +135 -104
- package/src/installer/detect.ts +46 -31
- package/src/installer/formats/index.ts +26 -28
- package/src/installer/formats/json.ts +81 -77
- package/src/installer/formats/rules.ts +55 -28
- package/src/installer/formats/toml.ts +88 -81
- package/src/installer/formats/value.ts +34 -30
- package/src/installer/hosts/codex.ts +100 -54
- package/src/installer/hosts/index.ts +126 -41
- package/src/installer/hosts/opencode.ts +70 -51
- package/src/installer/hosts/pi.ts +269 -75
- package/src/installer/hosts/shared.ts +19 -14
- package/src/installer/operation-lock.ts +54 -0
- package/src/installer/ownership.ts +129 -0
- package/src/installer/paths.ts +7 -5
- package/src/installer/persistence.ts +66 -0
- package/src/installer/render.ts +29 -29
- package/src/installer/revert.ts +33 -29
- package/src/installer/rulesContent.ts +24 -13
- package/src/installer/state.ts +116 -31
- package/src/installer/types.ts +74 -56
- package/src/installer/uninstall.ts +40 -17
- package/src/mcp/auth.ts +7 -8
- package/src/mcp/cli.ts +38 -20
- package/src/mcp/client.ts +35 -0
- package/src/mcp/server.ts +68 -97
- package/src/opencode/auth.ts +24 -26
- package/src/opencode/plugin.ts +99 -87
- package/src/pi/extension.ts +164 -0
- package/src/pi/settings.ts +49 -0
- package/src/mcp/config.ts +0 -124
package/src/core/format.ts
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
MAX_ERROR_BODY_CHARS,
|
|
4
|
-
} from "./constants"
|
|
5
|
-
import type { FormattedFetchContent, SearchResult } from "./types"
|
|
1
|
+
import { FETCH_TRUNCATION_MARKER, MAX_ERROR_BODY_CHARS } from "./constants";
|
|
2
|
+
import type { FormattedFetchContent, SearchResult } from "./types";
|
|
6
3
|
|
|
7
4
|
/** Truncates by Unicode code points so emoji and surrogate pairs are not split. */
|
|
8
5
|
export function truncate(value: string, maxChars: number): string {
|
|
9
|
-
if (maxChars <= 0) return ""
|
|
10
|
-
if (value.length <= maxChars) return value
|
|
11
|
-
return Array.from(value).slice(0, maxChars).join("")
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function formatFetchContent(title: string, content: string, maxChars: number): string {
|
|
15
|
-
return truncate(withTitle(title, content), maxChars)
|
|
6
|
+
if (maxChars <= 0) return "";
|
|
7
|
+
if (value.length <= maxChars) return value;
|
|
8
|
+
return Array.from(value).slice(0, maxChars).join("");
|
|
16
9
|
}
|
|
17
10
|
|
|
18
11
|
/**
|
|
@@ -20,40 +13,36 @@ export function formatFetchContent(title: string, content: string, maxChars: num
|
|
|
20
13
|
* truncation marker counts toward `maxChars`, so the returned content never
|
|
21
14
|
* exceeds the configured budget.
|
|
22
15
|
*/
|
|
23
|
-
export function formatFetchedContent(
|
|
24
|
-
title
|
|
25
|
-
|
|
26
|
-
maxChars:
|
|
27
|
-
):
|
|
28
|
-
const document = withTitle(title, content)
|
|
29
|
-
const length = Array.from(document).length
|
|
30
|
-
if (maxChars <= 0) return { content: "", truncated: length > 0 }
|
|
31
|
-
if (length <= maxChars) return { content: document, truncated: false }
|
|
16
|
+
export function formatFetchedContent(title: string, content: string, maxChars: number): FormattedFetchContent {
|
|
17
|
+
const document = withTitle(title, content);
|
|
18
|
+
const length = Array.from(document).length;
|
|
19
|
+
if (maxChars <= 0) return { content: "", truncated: length > 0 };
|
|
20
|
+
if (length <= maxChars) return { content: document, truncated: false };
|
|
32
21
|
|
|
33
|
-
const markerLength = Array.from(FETCH_TRUNCATION_MARKER).length
|
|
22
|
+
const markerLength = Array.from(FETCH_TRUNCATION_MARKER).length;
|
|
34
23
|
if (maxChars <= markerLength) {
|
|
35
|
-
return { content: truncate(document, maxChars), truncated: true }
|
|
24
|
+
return { content: truncate(document, maxChars), truncated: true };
|
|
36
25
|
}
|
|
37
26
|
return {
|
|
38
27
|
content: `${truncate(document, maxChars - markerLength)}${FETCH_TRUNCATION_MARKER}`,
|
|
39
28
|
truncated: true,
|
|
40
|
-
}
|
|
29
|
+
};
|
|
41
30
|
}
|
|
42
31
|
|
|
43
32
|
function withTitle(title: string, content: string): string {
|
|
44
|
-
const heading = title.trim()
|
|
45
|
-
if (!heading) return content
|
|
33
|
+
const heading = title.trim();
|
|
34
|
+
if (!heading) return content;
|
|
46
35
|
|
|
47
|
-
const body = content.trimStart()
|
|
48
|
-
const lines = body.split(/\r?\n/)
|
|
49
|
-
const firstHeading = lines[0]?.match(/^\\?#\s+(.+?)\s*$/)?.[1]
|
|
36
|
+
const body = content.trimStart();
|
|
37
|
+
const lines = body.split(/\r?\n/);
|
|
38
|
+
const firstHeading = lines[0]?.match(/^\\?#\s+(.+?)\s*$/)?.[1];
|
|
50
39
|
if (firstHeading && normalizeHeading(firstHeading) === normalizeHeading(heading)) {
|
|
51
40
|
// DeepSeek sometimes escapes the leading "#" as "\#"; normalize it so the
|
|
52
41
|
// result is a real Markdown heading instead of a duplicated literal line.
|
|
53
|
-
lines[0] = `# ${heading}
|
|
54
|
-
return lines.join("\n")
|
|
42
|
+
lines[0] = `# ${heading}`;
|
|
43
|
+
return lines.join("\n");
|
|
55
44
|
}
|
|
56
|
-
return `# ${heading}\n\n${content}
|
|
45
|
+
return `# ${heading}\n\n${content}`;
|
|
57
46
|
}
|
|
58
47
|
|
|
59
48
|
function normalizeHeading(value: string): string {
|
|
@@ -61,23 +50,23 @@ function normalizeHeading(value: string): string {
|
|
|
61
50
|
.trim()
|
|
62
51
|
.replace(/\s+/g, " ")
|
|
63
52
|
.replace(/\s+#+\s*$/, "")
|
|
64
|
-
.toLowerCase()
|
|
53
|
+
.toLowerCase();
|
|
65
54
|
}
|
|
66
55
|
|
|
67
56
|
export function redactSecrets(value: string): string {
|
|
68
57
|
return value
|
|
69
58
|
.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-***")
|
|
70
|
-
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer ***")
|
|
59
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer ***");
|
|
71
60
|
}
|
|
72
61
|
|
|
73
62
|
export function formatApiError(operation: string, status: number, body: string): string {
|
|
74
|
-
const detail = redactSecrets(body.trim())
|
|
75
|
-
const detailChars = Array.from(detail)
|
|
63
|
+
const detail = redactSecrets(body.trim());
|
|
64
|
+
const detailChars = Array.from(detail);
|
|
76
65
|
const text = detail
|
|
77
66
|
? detailChars.length > MAX_ERROR_BODY_CHARS
|
|
78
67
|
? `${detailChars.slice(0, MAX_ERROR_BODY_CHARS).join("")}…`
|
|
79
68
|
: detail
|
|
80
|
-
: "empty response body"
|
|
69
|
+
: "empty response body";
|
|
81
70
|
const hint =
|
|
82
71
|
status === 401
|
|
83
72
|
? "invalid API key"
|
|
@@ -87,13 +76,13 @@ export function formatApiError(operation: string, status: number, body: string):
|
|
|
87
76
|
? "permission denied"
|
|
88
77
|
: status === 429
|
|
89
78
|
? "rate limited"
|
|
90
|
-
: undefined
|
|
91
|
-
const label = hint ? `${operation} failed (HTTP ${status}, ${hint})` : `${operation} failed (HTTP ${status})
|
|
92
|
-
return `${label}: ${text}
|
|
79
|
+
: undefined;
|
|
80
|
+
const label = hint ? `${operation} failed (HTTP ${status}, ${hint})` : `${operation} failed (HTTP ${status})`;
|
|
81
|
+
return `${label}: ${text}`;
|
|
93
82
|
}
|
|
94
83
|
|
|
95
84
|
export function formatTimeoutError(operation: string, timeoutMs: number): string {
|
|
96
|
-
return `${operation} timed out after ${timeoutMs} ms
|
|
85
|
+
return `${operation} timed out after ${timeoutMs} ms`;
|
|
97
86
|
}
|
|
98
87
|
|
|
99
88
|
/**
|
|
@@ -101,27 +90,41 @@ export function formatTimeoutError(operation: string, timeoutMs: number): string
|
|
|
101
90
|
* Hosts that ignore structured output read this text.
|
|
102
91
|
*/
|
|
103
92
|
export function formatSearchResults(results: readonly SearchResult[]): string {
|
|
104
|
-
if (results.length === 0) return "No results found."
|
|
93
|
+
if (results.length === 0) return "No results found.";
|
|
105
94
|
|
|
106
95
|
return results
|
|
107
96
|
.map((result, index) => {
|
|
108
|
-
const title = result.title?.trim() || result.url
|
|
109
|
-
const lines = [`${index + 1}. [${escapeBrackets(title)}](${result.url})`]
|
|
97
|
+
const title = result.title?.trim() || result.url;
|
|
98
|
+
const lines = [`${index + 1}. [${escapeBrackets(title)}](${result.url})`];
|
|
110
99
|
if (result.content) {
|
|
111
|
-
lines.push(` ${result.content.replaceAll("\n", " ")}`)
|
|
100
|
+
lines.push(` ${result.content.replaceAll("\n", " ")}`);
|
|
112
101
|
}
|
|
113
102
|
if (result.published !== undefined) {
|
|
114
|
-
lines.push(` Published: ${formatPublished(result.published)}`)
|
|
103
|
+
lines.push(` Published: ${formatPublished(result.published)}`);
|
|
115
104
|
}
|
|
116
|
-
return lines.join("\n")
|
|
105
|
+
return lines.join("\n");
|
|
117
106
|
})
|
|
118
|
-
.join("\n\n")
|
|
107
|
+
.join("\n\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Renders one search result as the structured mirror of its text form; hosts
|
|
112
|
+
* that read structuredContent (every JSON-replacing client excepted) get this
|
|
113
|
+
* shape from both adapters.
|
|
114
|
+
*/
|
|
115
|
+
export function toStructuredResult(result: SearchResult) {
|
|
116
|
+
return {
|
|
117
|
+
url: result.url,
|
|
118
|
+
...(result.title ? { title: result.title } : {}),
|
|
119
|
+
...(result.content ? { content: result.content } : {}),
|
|
120
|
+
...(result.published !== undefined ? { published: result.published } : {}),
|
|
121
|
+
};
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
function escapeBrackets(value: string): string {
|
|
122
|
-
return value.replaceAll("[", "\\[").replaceAll("]", "\\]")
|
|
125
|
+
return value.replaceAll("[", "\\[").replaceAll("]", "\\]");
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
function formatPublished(epochMs: number): string {
|
|
126
|
-
return new Date(epochMs).toISOString().slice(0, 10)
|
|
129
|
+
return new Date(epochMs).toISOString().slice(0, 10);
|
|
127
130
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export * from "./auth"
|
|
2
|
-
export * from "./client"
|
|
3
|
-
export * from "./constants"
|
|
4
|
-
export * from "./domains"
|
|
5
|
-
export * from "./format"
|
|
6
|
-
export * from "./options"
|
|
7
|
-
export * from "./search"
|
|
8
|
-
export * from "./types"
|
|
9
|
-
export * from "./url"
|
|
1
|
+
export * from "./auth";
|
|
2
|
+
export * from "./client";
|
|
3
|
+
export * from "./constants";
|
|
4
|
+
export * from "./domains";
|
|
5
|
+
export * from "./format";
|
|
6
|
+
export * from "./options";
|
|
7
|
+
export * from "./search";
|
|
8
|
+
export * from "./types";
|
|
9
|
+
export * from "./url";
|
package/src/core/jsonc.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parse } from "jsonc-parser/lib/esm/main.js"
|
|
1
|
+
import { parse } from "jsonc-parser/lib/esm/main.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Parses JSONC (JSON with comments and trailing commas) via jsonc-parser,
|
|
@@ -6,11 +6,11 @@ import { parse } from "jsonc-parser/lib/esm/main.js"
|
|
|
6
6
|
* text has errors beyond tolerated comments and trailing commas.
|
|
7
7
|
*/
|
|
8
8
|
export function parseJsonc(text: string): unknown {
|
|
9
|
-
const errors: { error: number; offset: number; length: number }[] = []
|
|
10
|
-
const value = parse(text, errors, { allowTrailingComma: true })
|
|
9
|
+
const errors: { error: number; offset: number; length: number }[] = [];
|
|
10
|
+
const value = parse(text, errors, { allowTrailingComma: true });
|
|
11
11
|
if (errors.length > 0) {
|
|
12
|
-
const first = errors[0]
|
|
13
|
-
throw new SyntaxError(`JSONC parse error at offset ${first.offset} (code ${first.error})`)
|
|
12
|
+
const first = errors[0];
|
|
13
|
+
throw new SyntaxError(`JSONC parse error at offset ${first.offset} (code ${first.error})`);
|
|
14
14
|
}
|
|
15
|
-
return value
|
|
15
|
+
return value;
|
|
16
16
|
}
|
package/src/core/options.ts
CHANGED
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
DEFAULT_API_KEY_SOURCE,
|
|
3
3
|
DEFAULT_FETCH_MAX_CHARS,
|
|
4
4
|
DEFAULT_MAX_RESULTS,
|
|
5
|
-
DEFAULT_MAX_SUMMARY_CHARS,
|
|
6
5
|
DEFAULT_MAX_RETRIES,
|
|
6
|
+
DEFAULT_MAX_SUMMARY_CHARS,
|
|
7
7
|
DEFAULT_MIN_AUTHORITY,
|
|
8
8
|
DEFAULT_MIN_RERANK,
|
|
9
9
|
DEFAULT_RETRY_BASE_DELAY_MS,
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
MAX_RETRY_DELAY_MS,
|
|
14
14
|
MAX_TIMEOUT_MS,
|
|
15
15
|
MIN_TIMEOUT_MS,
|
|
16
|
-
} from "./constants"
|
|
17
|
-
import { normalizeDomains } from "./domains"
|
|
16
|
+
} from "./constants";
|
|
17
|
+
import { normalizeDomains } from "./domains";
|
|
18
18
|
import type {
|
|
19
19
|
ApiKeySource,
|
|
20
20
|
AuthOptions,
|
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
ResolvedOptions,
|
|
24
24
|
RetryOptions,
|
|
25
25
|
SearchOptions,
|
|
26
|
-
} from "./types"
|
|
26
|
+
} from "./types";
|
|
27
27
|
|
|
28
28
|
export function resolvePluginOptions(input?: PluginOptions): ResolvedOptions {
|
|
29
29
|
return {
|
|
@@ -31,7 +31,7 @@ export function resolvePluginOptions(input?: PluginOptions): ResolvedOptions {
|
|
|
31
31
|
fetch: resolveFetchOptions(input),
|
|
32
32
|
auth: resolveAuthOptions(input),
|
|
33
33
|
retry: resolveRetryOptions(input),
|
|
34
|
-
}
|
|
34
|
+
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function resolveSearchOptions(input?: PluginOptions): SearchOptions {
|
|
@@ -40,14 +40,14 @@ export function resolveSearchOptions(input?: PluginOptions): SearchOptions {
|
|
|
40
40
|
maxResults: clampInt(input?.maxResults, DEFAULT_MAX_RESULTS, 0, 50),
|
|
41
41
|
minRerank: finiteNumber(input?.minRerank, DEFAULT_MIN_RERANK),
|
|
42
42
|
minAuthority: finiteNumber(input?.minAuthority, DEFAULT_MIN_AUTHORITY),
|
|
43
|
-
allowUnknownAuthority: input?.allowUnknownAuthority
|
|
43
|
+
allowUnknownAuthority: input?.allowUnknownAuthority !== false,
|
|
44
44
|
blockedDomains: normalizeDomains(input?.blockedDomains),
|
|
45
45
|
includeDomains: normalizeDomains(input?.includeDomains),
|
|
46
46
|
language: optionalText(input?.language),
|
|
47
47
|
maxSummaryChars: clampInt(input?.maxSummaryChars, DEFAULT_MAX_SUMMARY_CHARS, 0, 1_000_000),
|
|
48
48
|
timeoutMs: clampInt(input?.searchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
|
|
49
49
|
enabled: input?.searchEnabled !== false,
|
|
50
|
-
}
|
|
50
|
+
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function resolveFetchOptions(input?: PluginOptions): FetchOptions {
|
|
@@ -58,26 +58,21 @@ export function resolveFetchOptions(input?: PluginOptions): FetchOptions {
|
|
|
58
58
|
timeoutMs: clampInt(input?.fetchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
|
|
59
59
|
blockPrivateHosts: input?.fetchBlockPrivateHosts !== false,
|
|
60
60
|
enabled: input?.fetchEnabled !== false,
|
|
61
|
-
}
|
|
61
|
+
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function resolveAuthOptions(input?: PluginOptions): AuthOptions {
|
|
65
|
-
const value = input?.apiKeySource
|
|
65
|
+
const value = input?.apiKeySource;
|
|
66
66
|
return {
|
|
67
67
|
source: value === "opencode" || value === "env" ? value : (DEFAULT_API_KEY_SOURCE as ApiKeySource),
|
|
68
|
-
}
|
|
68
|
+
};
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export function resolveRetryOptions(input?: PluginOptions): RetryOptions {
|
|
72
72
|
return {
|
|
73
73
|
maxRetries: clampInt(input?.maxRetries, DEFAULT_MAX_RETRIES, 0, MAX_RETRIES),
|
|
74
|
-
baseDelayMs: clampInt(
|
|
75
|
-
|
|
76
|
-
DEFAULT_RETRY_BASE_DELAY_MS,
|
|
77
|
-
0,
|
|
78
|
-
MAX_RETRY_DELAY_MS,
|
|
79
|
-
),
|
|
80
|
-
}
|
|
74
|
+
baseDelayMs: clampInt(input?.retryBaseDelayMs, DEFAULT_RETRY_BASE_DELAY_MS, 0, MAX_RETRY_DELAY_MS),
|
|
75
|
+
};
|
|
81
76
|
}
|
|
82
77
|
|
|
83
78
|
/**
|
|
@@ -86,23 +81,23 @@ export function resolveRetryOptions(input?: PluginOptions): RetryOptions {
|
|
|
86
81
|
*/
|
|
87
82
|
export function resolveFetchTimeoutMs(configuredMs: number, inputSeconds?: unknown): number {
|
|
88
83
|
if (typeof inputSeconds !== "number" || !Number.isFinite(inputSeconds) || inputSeconds <= 0) {
|
|
89
|
-
return configuredMs
|
|
84
|
+
return configuredMs;
|
|
90
85
|
}
|
|
91
|
-
const inputMs = Math.min(MAX_TIMEOUT_MS, Math.max(1, Math.round(inputSeconds * 1000)))
|
|
92
|
-
return Math.min(configuredMs, inputMs)
|
|
86
|
+
const inputMs = Math.min(MAX_TIMEOUT_MS, Math.max(1, Math.round(inputSeconds * 1000)));
|
|
87
|
+
return Math.min(configuredMs, inputMs);
|
|
93
88
|
}
|
|
94
89
|
|
|
95
90
|
function optionalText(value: unknown): string | undefined {
|
|
96
|
-
if (typeof value !== "string") return undefined
|
|
97
|
-
const trimmed = value.trim()
|
|
98
|
-
return trimmed ? trimmed : undefined
|
|
91
|
+
if (typeof value !== "string") return undefined;
|
|
92
|
+
const trimmed = value.trim();
|
|
93
|
+
return trimmed ? trimmed : undefined;
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
function finiteNumber(value: unknown, fallback: number): number {
|
|
102
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback
|
|
97
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
103
98
|
}
|
|
104
99
|
|
|
105
100
|
function clampInt(value: unknown, fallback: number, min: number, max: number): number {
|
|
106
|
-
const number = typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : fallback
|
|
107
|
-
return Math.min(max, Math.max(min, number))
|
|
101
|
+
const number = typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : fallback;
|
|
102
|
+
return Math.min(max, Math.max(min, number));
|
|
108
103
|
}
|
package/src/core/search.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { normalizeDomains } from "./domains"
|
|
2
|
-
import { truncate } from "./format"
|
|
3
|
-
import type { DeepSeekSearchResult, SearchOptions, SearchResult } from "./types"
|
|
4
|
-
import { normalizeResultUrl } from "./url"
|
|
1
|
+
import { normalizeDomains } from "./domains";
|
|
2
|
+
import { truncate } from "./format";
|
|
3
|
+
import type { DeepSeekSearchResult, SearchOptions, SearchResult } from "./types";
|
|
4
|
+
import { normalizeResultUrl } from "./url";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Appends DeepSeek search operators:
|
|
@@ -13,72 +13,73 @@ export function buildQuery(
|
|
|
13
13
|
query: string,
|
|
14
14
|
options: Pick<SearchOptions, "blockedDomains" | "includeDomains" | "language">,
|
|
15
15
|
): string {
|
|
16
|
-
const parts = [query.trim()]
|
|
17
|
-
for (const domain of normalizeDomains(options.blockedDomains)) parts.push(`-site:${domain}`)
|
|
18
|
-
const include = normalizeDomains(options.includeDomains)
|
|
19
|
-
if (include.length) parts.push(include.map((domain) => `site:${domain}`).join(" OR "))
|
|
20
|
-
const language = options.language?.trim().replace(/^lang:/i, "")
|
|
21
|
-
if (language) parts.push(`lang:${language}`)
|
|
22
|
-
return parts.filter((part) => part.length > 0).join(" ")
|
|
16
|
+
const parts = [query.trim()];
|
|
17
|
+
for (const domain of normalizeDomains(options.blockedDomains)) parts.push(`-site:${domain}`);
|
|
18
|
+
const include = normalizeDomains(options.includeDomains);
|
|
19
|
+
if (include.length) parts.push(include.map((domain) => `site:${domain}`).join(" OR "));
|
|
20
|
+
const language = options.language?.trim().replace(/^lang:/i, "");
|
|
21
|
+
if (language) parts.push(`lang:${language}`);
|
|
22
|
+
return parts.filter((part) => part.length > 0).join(" ");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function parsePublished(value: unknown): number | undefined {
|
|
26
|
-
if (typeof value !== "string" || !value.trim()) return undefined
|
|
27
|
-
const timestamp = Date.parse(value)
|
|
28
|
-
return Number.isFinite(timestamp) ? timestamp : undefined
|
|
26
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
27
|
+
const timestamp = Date.parse(value);
|
|
28
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Filters and maps DeepSeek results into the neutral SearchResult shape.
|
|
33
|
-
*
|
|
33
|
+
* A present rerank_score always compares against minRerank; a missing or
|
|
34
|
+
* non-finite score passes only while no positive threshold is set, so the
|
|
35
|
+
* default returns exactly what DeepSeek ranked. Authority -1/missing is
|
|
34
36
|
* "unknown" and only passes when allowUnknownAuthority is enabled.
|
|
35
37
|
*/
|
|
36
|
-
export function processResults(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
): SearchResult[] {
|
|
40
|
-
const results: SearchResult[] = []
|
|
41
|
-
const seen = new Set<string>()
|
|
38
|
+
export function processResults(raw: readonly DeepSeekSearchResult[], options: SearchOptions): SearchResult[] {
|
|
39
|
+
const results: SearchResult[] = [];
|
|
40
|
+
const seen = new Set<string>();
|
|
42
41
|
|
|
43
42
|
for (const item of raw) {
|
|
44
|
-
if (results.length >= options.maxResults) break
|
|
45
|
-
if (!item || typeof item !== "object") continue
|
|
43
|
+
if (results.length >= options.maxResults) break;
|
|
44
|
+
if (!item || typeof item !== "object") continue;
|
|
46
45
|
|
|
47
|
-
const url = normalizeResultUrl(item.url)
|
|
48
|
-
if (!url || seen.has(url)) continue
|
|
46
|
+
const url = normalizeResultUrl(item.url);
|
|
47
|
+
if (!url || seen.has(url)) continue;
|
|
49
48
|
|
|
50
|
-
const rerank = item.rerank_score
|
|
51
|
-
if (typeof rerank
|
|
49
|
+
const rerank = item.rerank_score;
|
|
50
|
+
if (typeof rerank === "number" && Number.isFinite(rerank)) {
|
|
51
|
+
if (rerank < options.minRerank) continue;
|
|
52
|
+
} else if (options.minRerank > 0) {
|
|
53
|
+
// A threshold was explicitly raised, so a result that cannot be scored
|
|
54
|
+
// against it must not slip through.
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
52
57
|
|
|
53
|
-
const authority = item.authority
|
|
54
|
-
const knownAuthority =
|
|
55
|
-
typeof authority === "number" && Number.isFinite(authority) && authority >= 0
|
|
58
|
+
const authority = item.authority;
|
|
59
|
+
const knownAuthority = typeof authority === "number" && Number.isFinite(authority) && authority >= 0;
|
|
56
60
|
if (knownAuthority) {
|
|
57
|
-
if (authority < options.minAuthority) continue
|
|
61
|
+
if (authority < options.minAuthority) continue;
|
|
58
62
|
} else if (!options.allowUnknownAuthority) {
|
|
59
|
-
continue
|
|
63
|
+
continue;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
seen.add(url)
|
|
63
|
-
const content = truncate(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
const published = parsePublished(item.date)
|
|
68
|
-
const title = optionalText(item.title)
|
|
66
|
+
seen.add(url);
|
|
67
|
+
const content = truncate(optionalText(item.summary) ?? optionalText(item.snippet) ?? "", options.maxSummaryChars);
|
|
68
|
+
const published = parsePublished(item.date);
|
|
69
|
+
const title = optionalText(item.title);
|
|
69
70
|
results.push({
|
|
70
71
|
url,
|
|
71
72
|
...(title ? { title } : {}),
|
|
72
73
|
...(content ? { content } : {}),
|
|
73
74
|
...(published !== undefined ? { published } : {}),
|
|
74
|
-
})
|
|
75
|
+
});
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
return results
|
|
78
|
+
return results;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
function optionalText(value: unknown): string | undefined {
|
|
81
|
-
if (typeof value !== "string") return undefined
|
|
82
|
-
const trimmed = value.trim()
|
|
83
|
-
return trimmed ? trimmed : undefined
|
|
82
|
+
if (typeof value !== "string") return undefined;
|
|
83
|
+
const trimmed = value.trim();
|
|
84
|
+
return trimmed ? trimmed : undefined;
|
|
84
85
|
}
|