dsh-lcx-codex 0.4.3-pre.3 → 0.4.3
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.md +68 -99
- package/THIRD_PARTY_NOTICES.md +64 -0
- package/lib/auxiliary-usage.js +63 -0
- package/lib/client.js +1363 -295
- package/lib/dsh-compat.js +80 -18
- package/lib/dsh-responses.js +75 -8
- package/lib/grok-native-search.js +153 -79
- package/lib/index.js +159 -101
- package/lib/invocation-policy-scope.js +261 -0
- package/lib/json-store.js +11 -4
- package/lib/pi-responses-runtime.js +1571 -0
- package/lib/responses-request.js +1 -1
- package/lib/responses-stream.js +163 -92
- package/lib/route.js +17 -3
- package/lib/search-accounting.js +86 -0
- package/lib/search-usage.js +86 -0
- package/lib/transport.js +38 -10
- package/lib/types/client/index.d.ts +12 -0
- package/lib/types/client/search-media.d.ts +16 -0
- package/lib/web-run-output.js +22 -0
- package/lib/web-search-alpha.js +230 -28
- package/lib/web-search-capability.js +26 -1
- package/lib/web-search-hosted.js +119 -9
- package/lib/web-search-ref-store.js +88 -7
- package/package.json +50 -26
- package/lib/types/compact-v2.d.ts +0 -104
- package/lib/types/dsh-compat.d.ts +0 -78
- package/lib/types/dsh-responses.d.ts +0 -82
- package/lib/types/grok-native-search.d.ts +0 -39
- package/lib/types/json-store.d.ts +0 -10
- package/lib/types/native-checkpoint.d.ts +0 -213
- package/lib/types/responses-request.d.ts +0 -59
- package/lib/types/responses-stream.d.ts +0 -64
- package/lib/types/route.d.ts +0 -153
- package/lib/types/service-mutex.d.ts +0 -14
- package/lib/types/token-budget.d.ts +0 -50
- package/lib/types/transport.d.ts +0 -21
- package/lib/types/web-run-output.d.ts +0 -29
- package/lib/types/web-search-alpha.d.ts +0 -286
- package/lib/types/web-search-capability.d.ts +0 -26
- package/lib/types/web-search-hosted.d.ts +0 -246
- package/lib/types/web-search-ref-store.d.ts +0 -22
package/lib/transport.js
CHANGED
|
@@ -22,6 +22,16 @@ function retryableStatus(status) {
|
|
|
22
22
|
status === 429 ||
|
|
23
23
|
(status >= 500 && status <= 599));
|
|
24
24
|
}
|
|
25
|
+
// Retain only an allowlisted class, never upstream messages or account balances.
|
|
26
|
+
function isQuotaError(text) {
|
|
27
|
+
try {
|
|
28
|
+
const code = JSON.parse(text)?.error?.code;
|
|
29
|
+
return code === "insufficient_user_quota" || code === "insufficient_quota";
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
25
35
|
function retryAfterFromHeaders(headers) {
|
|
26
36
|
const rawMillis = headers?.get?.("retry-after-ms");
|
|
27
37
|
const millis = Number(rawMillis);
|
|
@@ -54,11 +64,16 @@ async function sleep(ms, signal) {
|
|
|
54
64
|
await new Promise((resolve, reject) => {
|
|
55
65
|
if (signal?.aborted)
|
|
56
66
|
return reject(signal.reason);
|
|
57
|
-
const
|
|
58
|
-
signal?.addEventListener("abort", () => {
|
|
67
|
+
const onAbort = () => {
|
|
59
68
|
clearTimeout(timer);
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
signal?.removeEventListener("abort", onAbort);
|
|
70
|
+
reject(signal?.reason);
|
|
71
|
+
};
|
|
72
|
+
const timer = setTimeout(() => {
|
|
73
|
+
signal?.removeEventListener("abort", onAbort);
|
|
74
|
+
resolve();
|
|
75
|
+
}, ms);
|
|
76
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
62
77
|
});
|
|
63
78
|
}
|
|
64
79
|
function combinedSignal(signal, timeoutMs) {
|
|
@@ -116,9 +131,10 @@ export async function fetchJsonWithRetry(url, body, headers = {}, signal, timeou
|
|
|
116
131
|
});
|
|
117
132
|
const text = await readLimited(response, response.ok ? maxResponseBytes : Math.min(maxResponseBytes, MAX_ERROR_BYTES));
|
|
118
133
|
if (!response.ok) {
|
|
119
|
-
const
|
|
134
|
+
const quota = isQuotaError(text);
|
|
135
|
+
const retryable = !quota && retryableStatus(response.status);
|
|
120
136
|
const providerRetryAfterMs = retryAfterFromHeaders(response.headers);
|
|
121
|
-
const error = makeError(`HTTP ${response.status}`, retryable ? "LCX_HTTP_RETRYABLE" : "LCX_HTTP_ERROR", {
|
|
137
|
+
const error = makeError(`HTTP ${response.status}`, quota ? "LCX_INSUFFICIENT_QUOTA" : retryable ? "LCX_HTTP_RETRYABLE" : "LCX_HTTP_ERROR", {
|
|
122
138
|
status: response.status,
|
|
123
139
|
retryable,
|
|
124
140
|
requestId: response.headers.get("x-request-id") ??
|
|
@@ -189,10 +205,11 @@ export async function fetchSseWithRetry(url, body, headers = {}, signal, timeout
|
|
|
189
205
|
redirect: "error",
|
|
190
206
|
});
|
|
191
207
|
if (!response.ok) {
|
|
192
|
-
await readLimited(response, Math.min(options.maxResponseBytes ?? DEFAULT_MAX_BYTES, MAX_ERROR_BYTES));
|
|
193
|
-
const
|
|
208
|
+
const text = await readLimited(response, Math.min(options.maxResponseBytes ?? DEFAULT_MAX_BYTES, MAX_ERROR_BYTES));
|
|
209
|
+
const quota = isQuotaError(text);
|
|
210
|
+
const retryable = !quota && retryableStatus(response.status);
|
|
194
211
|
const providerRetryAfterMs = retryAfterFromHeaders(response.headers);
|
|
195
|
-
const error = makeError(`HTTP ${response.status}`, retryable ? "LCX_HTTP_RETRYABLE" : "LCX_HTTP_ERROR", {
|
|
212
|
+
const error = makeError(`HTTP ${response.status}`, quota ? "LCX_INSUFFICIENT_QUOTA" : retryable ? "LCX_HTTP_RETRYABLE" : "LCX_HTTP_ERROR", {
|
|
196
213
|
status: response.status,
|
|
197
214
|
retryable,
|
|
198
215
|
requestId: response.headers.get("x-request-id") ??
|
|
@@ -288,6 +305,17 @@ export async function consumeSse(response, onEvent, options = {}) {
|
|
|
288
305
|
dispatch();
|
|
289
306
|
}
|
|
290
307
|
finally {
|
|
291
|
-
|
|
308
|
+
try {
|
|
309
|
+
await reader.cancel();
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// Cleanup must not replace the stream/parse/cancellation outcome.
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
reader.releaseLock();
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// The acquired reader owns no further work after settlement.
|
|
319
|
+
}
|
|
292
320
|
}
|
|
293
321
|
}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import type { ClientModuleLoaderTarget } from "@deepseek-ai/dsh-client-modules/client";
|
|
2
|
+
import { type SearchMediaItem } from "./search-media.js";
|
|
3
|
+
type SearchMediaData = {
|
|
4
|
+
readonly items: readonly SearchMediaItem[];
|
|
5
|
+
readonly provider: string;
|
|
6
|
+
readonly model: string;
|
|
7
|
+
};
|
|
8
|
+
declare module "@deepseek-ai/dsh-client-ui-chat/client" {
|
|
9
|
+
interface ChatNodeDataMap {
|
|
10
|
+
"lcx-search-media": SearchMediaData;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
2
13
|
declare global {
|
|
3
14
|
interface Window {
|
|
4
15
|
__ModuleLoader__: Pick<ClientModuleLoaderTarget, "load">;
|
|
5
16
|
}
|
|
6
17
|
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SearchMediaItem = {
|
|
2
|
+
readonly kind: "image" | "video" | "page";
|
|
3
|
+
readonly url: string;
|
|
4
|
+
readonly poster?: string;
|
|
5
|
+
readonly previewUrl?: string;
|
|
6
|
+
readonly sourceUrl?: string;
|
|
7
|
+
readonly caption?: string;
|
|
8
|
+
readonly structured?: true;
|
|
9
|
+
};
|
|
10
|
+
export declare const SEARCH_MEDIA_LIMIT = 60;
|
|
11
|
+
export type StructuredMediaTool = "web_search" | "websearch_gpt_advanced";
|
|
12
|
+
/** Narrow LCX-owned tool-private media metadata without reading model-visible output. */
|
|
13
|
+
export declare function structuredSearchMedia(meta: unknown, expectedTool: StructuredMediaTool): readonly SearchMediaItem[];
|
|
14
|
+
export declare function mergeSearchMedia(structured: readonly SearchMediaItem[], fallback: readonly SearchMediaItem[]): readonly SearchMediaItem[];
|
|
15
|
+
/** Extract bounded remote media linked by visible assistant prose. */
|
|
16
|
+
export declare function extractSearchMedia(text: string): readonly SearchMediaItem[];
|
package/lib/web-run-output.js
CHANGED
|
@@ -152,6 +152,28 @@ export function outputLinks(blocks) {
|
|
|
152
152
|
}
|
|
153
153
|
return links;
|
|
154
154
|
}
|
|
155
|
+
export function mergeWebRunLinks(base, extra) {
|
|
156
|
+
const links = [];
|
|
157
|
+
for (const link of [...base, ...extra]) {
|
|
158
|
+
if (!Number.isSafeInteger(link.id) || !link.label)
|
|
159
|
+
continue;
|
|
160
|
+
const existing = links.find((value) => value.id === link.id);
|
|
161
|
+
if (!existing) {
|
|
162
|
+
links.push({
|
|
163
|
+
id: link.id,
|
|
164
|
+
label: link.label,
|
|
165
|
+
...(link.domain ? { domain: link.domain } : {}),
|
|
166
|
+
...(link.url ? { url: link.url } : {}),
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!existing.domain && link.domain)
|
|
171
|
+
existing.domain = link.domain;
|
|
172
|
+
if (!existing.url && link.url)
|
|
173
|
+
existing.url = link.url;
|
|
174
|
+
}
|
|
175
|
+
return links;
|
|
176
|
+
}
|
|
155
177
|
export function outputPdfRefs(blocks) {
|
|
156
178
|
const refs = [];
|
|
157
179
|
for (const block of blocks) {
|
package/lib/web-search-alpha.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { isIP } from "node:net";
|
|
3
|
-
import {
|
|
3
|
+
import { ServiceMutex } from "./service-mutex.js";
|
|
4
|
+
import { fetchJsonWithRetry } from "./transport.js";
|
|
5
|
+
import { mergeWebRunLinks, outputDomains, outputLineRange, outputLinks, outputPdfRefs, parseWebRunOutput, } from "./web-run-output.js";
|
|
4
6
|
export const ALPHA_ACTIONS = [
|
|
5
7
|
"search_query",
|
|
6
8
|
"image_query",
|
|
@@ -125,9 +127,16 @@ function integer(value, field, min = 0, max = 10000) {
|
|
|
125
127
|
}
|
|
126
128
|
function date(value, field) {
|
|
127
129
|
const normalized = text(value, field, 10);
|
|
128
|
-
if (!/^\d{4}-\d{2}-\d{2}$/u.test(normalized)
|
|
129
|
-
Number.isNaN(Date.parse(`${normalized}T00:00:00Z`)))
|
|
130
|
+
if (!/^\d{4}-\d{2}-\d{2}$/u.test(normalized))
|
|
130
131
|
throw failure(`websearch_alpha.${field} must use YYYY-MM-DD`);
|
|
132
|
+
const year = Number(normalized.slice(0, 4));
|
|
133
|
+
const month = Number(normalized.slice(5, 7));
|
|
134
|
+
const day = Number(normalized.slice(8, 10));
|
|
135
|
+
const utc = new Date(Date.UTC(year, month - 1, day));
|
|
136
|
+
if (utc.getUTCFullYear() !== year ||
|
|
137
|
+
utc.getUTCMonth() !== month - 1 ||
|
|
138
|
+
utc.getUTCDate() !== day)
|
|
139
|
+
throw failure(`websearch_alpha.${field} must be a real calendar date`);
|
|
131
140
|
return normalized;
|
|
132
141
|
}
|
|
133
142
|
function normalizeDomains(value) {
|
|
@@ -514,15 +523,92 @@ function safeResult(value, seen = new Set()) {
|
|
|
514
523
|
}
|
|
515
524
|
return result;
|
|
516
525
|
}
|
|
517
|
-
function
|
|
518
|
-
|
|
519
|
-
|
|
526
|
+
function sha256(value) {
|
|
527
|
+
return createHash("sha256").update(value).digest("hex");
|
|
528
|
+
}
|
|
529
|
+
function canonicalJson(value, seen = new Set()) {
|
|
530
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
531
|
+
return JSON.stringify(value);
|
|
532
|
+
if (typeof value === "number")
|
|
533
|
+
return Number.isFinite(value) ? JSON.stringify(value) : undefined;
|
|
534
|
+
if (typeof value !== "object" || seen.has(value))
|
|
535
|
+
return undefined;
|
|
536
|
+
seen.add(value);
|
|
537
|
+
let result;
|
|
538
|
+
if (Array.isArray(value)) {
|
|
539
|
+
const items = value.map(item => canonicalJson(item, seen));
|
|
540
|
+
if (items.every((item) => item !== undefined))
|
|
541
|
+
result = `[${items.join(",")}]`;
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
545
|
+
const items = [];
|
|
546
|
+
for (const [key, item] of entries) {
|
|
547
|
+
const encoded = canonicalJson(item, seen);
|
|
548
|
+
if (encoded === undefined) {
|
|
549
|
+
result = undefined;
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
items.push(`${JSON.stringify(key)}:${encoded}`);
|
|
553
|
+
result = `{${items.join(",")}}`;
|
|
554
|
+
}
|
|
555
|
+
if (entries.length === 0)
|
|
556
|
+
result = "{}";
|
|
557
|
+
}
|
|
558
|
+
seen.delete(value);
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
function explicitArtifactFingerprint(value) {
|
|
562
|
+
const explicit = Object.fromEntries(["provenance", "provider_provenance"]
|
|
563
|
+
.filter(key => Object.hasOwn(value, key))
|
|
564
|
+
.map(key => [key, value[key]]));
|
|
565
|
+
if (Object.keys(explicit).length === 0)
|
|
566
|
+
return undefined;
|
|
567
|
+
const canonical = canonicalJson(explicit);
|
|
568
|
+
return canonical === undefined ? sha256("invalid-explicit-provenance") : sha256(canonical);
|
|
569
|
+
}
|
|
570
|
+
function alphaRefCollision(refId) {
|
|
571
|
+
return Object.assign(new Error(`Alpha reference collision in one provider response: ${refId}`), {
|
|
572
|
+
code: "LCX_ALPHA_REF_COLLISION",
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
function addRefObservation(artifacts, observation) {
|
|
576
|
+
const accepted = artifacts.observations.get(observation.refId);
|
|
577
|
+
if (!accepted) {
|
|
578
|
+
artifacts.observations.set(observation.refId, observation);
|
|
579
|
+
artifacts.refs.push(observation.refId);
|
|
580
|
+
artifacts.refRecords.push(observation);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if ((accepted.url && observation.url && accepted.url !== observation.url) ||
|
|
584
|
+
(accepted.provenance.artifactFingerprint && observation.provenance.artifactFingerprint &&
|
|
585
|
+
accepted.provenance.artifactFingerprint !== observation.provenance.artifactFingerprint)) {
|
|
586
|
+
throw alphaRefCollision(observation.refId);
|
|
587
|
+
}
|
|
588
|
+
const merged = {
|
|
589
|
+
refId: accepted.refId,
|
|
590
|
+
...(accepted.url || observation.url ? { url: accepted.url ?? observation.url } : {}),
|
|
591
|
+
provenance: {
|
|
592
|
+
...accepted.provenance,
|
|
593
|
+
...(accepted.provenance.artifactFingerprint || observation.provenance.artifactFingerprint
|
|
594
|
+
? { artifactFingerprint: accepted.provenance.artifactFingerprint ?? observation.provenance.artifactFingerprint }
|
|
595
|
+
: {}),
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
artifacts.observations.set(observation.refId, merged);
|
|
599
|
+
artifacts.refRecords[artifacts.refRecords.indexOf(accepted)] = merged;
|
|
600
|
+
}
|
|
601
|
+
function artifacts(results, provenance) {
|
|
602
|
+
const result = { refs: [], refRecords: [], sources: [], observations: new Map() };
|
|
603
|
+
const seenSources = new Set();
|
|
604
|
+
const visit = (value, inheritedArtifact) => {
|
|
520
605
|
if (!isObject(value) && !Array.isArray(value))
|
|
521
606
|
return;
|
|
522
607
|
if (Array.isArray(value)) {
|
|
523
|
-
value.forEach(visit);
|
|
608
|
+
value.forEach(item => visit(item, inheritedArtifact));
|
|
524
609
|
return;
|
|
525
610
|
}
|
|
611
|
+
const artifactFingerprint = explicitArtifactFingerprint(value) ?? inheritedArtifact;
|
|
526
612
|
const candidate = typeof value.ref_id === "string" && value.ref_id
|
|
527
613
|
? value.ref_id
|
|
528
614
|
: typeof value.id === "string" && /^turn[\w-]+$/u.test(value.id)
|
|
@@ -530,14 +616,15 @@ function artifacts(results) {
|
|
|
530
616
|
: undefined;
|
|
531
617
|
const refId = isAlphaHttpUrl(candidate) ? undefined : candidate;
|
|
532
618
|
const url = httpUrl(value.url ?? value.source_url ?? value.source_website_url ?? candidate);
|
|
533
|
-
if (refId
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
619
|
+
if (refId)
|
|
620
|
+
addRefObservation(result, {
|
|
621
|
+
refId,
|
|
622
|
+
...(url ? { url } : {}),
|
|
623
|
+
provenance: { ...provenance, ...(artifactFingerprint ? { artifactFingerprint } : {}) },
|
|
624
|
+
});
|
|
538
625
|
if (url && !seenSources.has(url)) {
|
|
539
626
|
seenSources.add(url);
|
|
540
|
-
sources.push({
|
|
627
|
+
result.sources.push({
|
|
541
628
|
url,
|
|
542
629
|
...(typeof value.title === "string" ? { title: value.title } : {}),
|
|
543
630
|
...(typeof value.snippet === "string"
|
|
@@ -546,21 +633,67 @@ function artifacts(results) {
|
|
|
546
633
|
...(refId ? { refId } : {}),
|
|
547
634
|
});
|
|
548
635
|
}
|
|
636
|
+
Object.entries(value)
|
|
637
|
+
.filter(([key]) => key !== "encrypted_output" && key !== "encrypted_content")
|
|
638
|
+
.forEach(([, item]) => visit(item, artifactFingerprint));
|
|
639
|
+
};
|
|
640
|
+
visit(results);
|
|
641
|
+
return result;
|
|
642
|
+
}
|
|
643
|
+
function structuredLinks(results) {
|
|
644
|
+
const links = [];
|
|
645
|
+
const visit = (value) => {
|
|
646
|
+
if (Array.isArray(value)) {
|
|
647
|
+
value.forEach(visit);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (!isObject(value))
|
|
651
|
+
return;
|
|
652
|
+
if (Array.isArray(value.links)) {
|
|
653
|
+
for (const item of value.links) {
|
|
654
|
+
if (!isObject(item))
|
|
655
|
+
continue;
|
|
656
|
+
const id = item.id ?? item.link_id ?? item.linkId;
|
|
657
|
+
if (typeof id !== "number" || !Number.isSafeInteger(id))
|
|
658
|
+
continue;
|
|
659
|
+
const url = httpUrl(item.url ?? item.href);
|
|
660
|
+
const label = (typeof item.label === "string" && item.label.trim()) ||
|
|
661
|
+
(typeof item.title === "string" && item.title.trim()) ||
|
|
662
|
+
url ||
|
|
663
|
+
String(id);
|
|
664
|
+
const domain = typeof item.domain === "string" && item.domain.trim()
|
|
665
|
+
? item.domain.trim().slice(0, 253)
|
|
666
|
+
: undefined;
|
|
667
|
+
if (!links.some((link) => link.id === id)) {
|
|
668
|
+
links.push({
|
|
669
|
+
id,
|
|
670
|
+
label: String(label).slice(0, 500),
|
|
671
|
+
...(domain ? { domain } : {}),
|
|
672
|
+
...(url ? { url } : {}),
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
549
677
|
Object.values(value).forEach(visit);
|
|
550
678
|
};
|
|
551
679
|
visit(results);
|
|
552
|
-
return
|
|
680
|
+
return links;
|
|
553
681
|
}
|
|
554
682
|
const ACTION_ERROR = /^\s*(?:Error parsing function call\b|Invalid function_name=|Invalid function call\b)/iu;
|
|
555
683
|
const ALPHA_SEMANTIC_FAILURE = /^\s*(?:reference(?: id)?\s+(?:is\s+)?(?:invalid|unavailable)\b|unable to access requested content\b|service unavailable\b)/iu;
|
|
556
684
|
const ALPHA_COMMAND_ERROR_ENVELOPE = /^\s*Internal Error\s*\([^\r\n)]*\)\s*(?:\r?\n[ \t]*)+(?:\uE200cite\uE202[^\uE201\r\n]+\uE201[ \t]*(?:\[wordlim:\s*\d+\][ \t]*)?)?Unable to resolve (open|find|click|screenshot) call\s*:/iu;
|
|
685
|
+
const ALPHA_FETCH_FAILURE_ENVELOPE = /^\s*Internal Error\s*\([^\r\n)]*\)\s*(?:\r?\n[ \t]*)+(?:\uE200cite\uE202[^\uE201\r\n]+\uE201[ \t]*(?:\[wordlim:\s*\d+\][ \t]*)?)?Source:\s*(open|find|click|screenshot)\s*\([^\r\n]*\)\s*;\s*Total lines:\s*\d+\s*(?:\r?\n)L0:[ \t]*Failed to fetch \S+:[ \t]*\(\d{3}\)/iu;
|
|
557
686
|
const ALPHA_REFERENCE_ERROR_ENVELOPE = /^\s*(?:\*\*Result:\*\*[ \t]*)?Unable to (?:execute[ \t]+)?`?(open|find|click|screenshot)`?(?=[\s:])([^\r\n]*)/iu;
|
|
558
687
|
function alphaCommandErrorEnvelope(output, action) {
|
|
559
|
-
const
|
|
560
|
-
|
|
688
|
+
const text = String(output ?? "");
|
|
689
|
+
const command = text.match(ALPHA_COMMAND_ERROR_ENVELOPE);
|
|
690
|
+
if (command && command[1].toLowerCase() === action)
|
|
691
|
+
return true;
|
|
692
|
+
const fetchFailure = text.match(ALPHA_FETCH_FAILURE_ENVELOPE);
|
|
693
|
+
if (fetchFailure && fetchFailure[1].toLowerCase() === action)
|
|
561
694
|
return true;
|
|
562
695
|
// Match the action failure header, never error phrases quoted inside a page.
|
|
563
|
-
const reference =
|
|
696
|
+
const reference = text.match(ALPHA_REFERENCE_ERROR_ENVELOPE);
|
|
564
697
|
return Boolean(reference &&
|
|
565
698
|
reference[1].toLowerCase() === action &&
|
|
566
699
|
/\b(?:reference(?: id)?|turn[\w-]+)\b/iu.test(reference[2]) &&
|
|
@@ -578,7 +711,14 @@ export function parseAlphaSearchResponse(response, options) {
|
|
|
578
711
|
alphaCommandErrorEnvelope(response.output, action))))
|
|
579
712
|
throw failure("LCX Alpha Web Search could not execute the requested action", "LCX_ALPHA_ACTION_FAILED");
|
|
580
713
|
const results = safeResult(response.results ?? []);
|
|
581
|
-
const
|
|
714
|
+
const originKind = typeof response.id === "string" && response.id ? "response" : "request";
|
|
715
|
+
const originValue = originKind === "response" ? String(response.id) : String(options.requestId ?? "");
|
|
716
|
+
const provenance = {
|
|
717
|
+
action: action ?? "unknown",
|
|
718
|
+
originKind,
|
|
719
|
+
originFingerprint: sha256(originValue),
|
|
720
|
+
};
|
|
721
|
+
const a = artifacts(response.results ?? [], provenance);
|
|
582
722
|
const outputBlocks = parseWebRunOutput(response.output);
|
|
583
723
|
for (const block of outputBlocks)
|
|
584
724
|
if (block.url) {
|
|
@@ -592,13 +732,11 @@ export function parseAlphaSearchResponse(response, options) {
|
|
|
592
732
|
for (const ref of block.references ?? []) {
|
|
593
733
|
if (isAlphaHttpUrl(ref))
|
|
594
734
|
continue;
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
...(block.url ? { url: block.url } : {}),
|
|
601
|
-
});
|
|
735
|
+
addRefObservation(a, {
|
|
736
|
+
refId: ref,
|
|
737
|
+
...(block.url ? { url: block.url } : {}),
|
|
738
|
+
provenance,
|
|
739
|
+
});
|
|
602
740
|
}
|
|
603
741
|
for (const source of outputBlocks.flatMap((b) => b.url ? [{ url: b.url, ...(b.title ? { title: b.title } : {}) }] : []))
|
|
604
742
|
if (!a.sources.some((x) => x.url === source.url))
|
|
@@ -614,7 +752,7 @@ export function parseAlphaSearchResponse(response, options) {
|
|
|
614
752
|
sources: a.sources,
|
|
615
753
|
citations: a.sources.map((s) => ({ ...s })),
|
|
616
754
|
outputBlocks,
|
|
617
|
-
links: outputLinks(outputBlocks),
|
|
755
|
+
links: mergeWebRunLinks(outputLinks(outputBlocks), structuredLinks(results)),
|
|
618
756
|
pdfRefs: outputPdfRefs(outputBlocks),
|
|
619
757
|
domains: outputDomains(outputBlocks),
|
|
620
758
|
...(outputLineRange(outputBlocks)
|
|
@@ -633,18 +771,82 @@ export function parseAlphaSearchResponse(response, options) {
|
|
|
633
771
|
refRecords: a.refRecords,
|
|
634
772
|
};
|
|
635
773
|
}
|
|
774
|
+
function opaqueRefList(value) {
|
|
775
|
+
return Array.isArray(value)
|
|
776
|
+
? value.filter((ref) => typeof ref === "string" && Boolean(ref) && !isAlphaHttpUrl(ref))
|
|
777
|
+
: [];
|
|
778
|
+
}
|
|
779
|
+
function sourceUrlForRef(sources, refId) {
|
|
780
|
+
const match = sources.find((source) => source.refId === refId && typeof source.url === "string" && source.url);
|
|
781
|
+
return typeof match?.url === "string" ? match.url : undefined;
|
|
782
|
+
}
|
|
636
783
|
export function renderAlphaSearchResult(value) {
|
|
637
784
|
const data = isObject(value) ? value : {};
|
|
638
785
|
const parts = [];
|
|
639
786
|
if (typeof data.content === "string")
|
|
640
787
|
parts.push(data.content);
|
|
641
|
-
|
|
642
|
-
|
|
788
|
+
const sources = Array.isArray(data.sources) ? data.sources.filter(isObject) : [];
|
|
789
|
+
const refs = opaqueRefList(data.refs);
|
|
790
|
+
if (refs.length) {
|
|
791
|
+
parts.push(`可继续操作的引用:\n${refs.map((ref) => {
|
|
792
|
+
const url = sourceUrlForRef(sources, ref);
|
|
793
|
+
return url ? `- opaqueRef=${ref} url=${url}` : `- opaqueRef=${ref}`;
|
|
794
|
+
}).join("\n")}`);
|
|
795
|
+
}
|
|
796
|
+
const links = Array.isArray(data.links)
|
|
797
|
+
? data.links.filter((link) => isObject(link) && Number.isSafeInteger(link.id))
|
|
798
|
+
: [];
|
|
799
|
+
if (links.length) {
|
|
800
|
+
parts.push(`可点击链接:\n${links.map((link) => {
|
|
801
|
+
const bits = [`linkId=${String(link.id)}`];
|
|
802
|
+
if (typeof link.label === "string" && link.label)
|
|
803
|
+
bits.push(`label=${link.label}`);
|
|
804
|
+
if (typeof link.domain === "string" && link.domain)
|
|
805
|
+
bits.push(`domain=${link.domain}`);
|
|
806
|
+
if (typeof link.url === "string" && link.url)
|
|
807
|
+
bits.push(`url=${link.url}`);
|
|
808
|
+
return `- ${bits.join(" ")}`;
|
|
809
|
+
}).join("\n")}`);
|
|
810
|
+
}
|
|
811
|
+
const pdfRefs = opaqueRefList(data.pdfRefs);
|
|
812
|
+
if (pdfRefs.length)
|
|
813
|
+
parts.push(`PDF引用:\n${pdfRefs.map((ref) => `- opaqueRef=${ref}`).join("\n")}`);
|
|
814
|
+
if (sources.length)
|
|
815
|
+
parts.push(`来源:\n${sources.map((source) => `- [${String(source.title ?? source.url ?? "")}](${String(source.url ?? "")})`).join("\n")}`);
|
|
643
816
|
if (Array.isArray(data.warnings) && data.warnings.length)
|
|
644
817
|
parts.push(data.warnings.map((warning) => `警告:${String(warning)}`).join("\n"));
|
|
645
818
|
parts.push(`检索时间:${String(data.retrievedAt ?? "")}`);
|
|
646
819
|
return [{ type: "text", text: parts.filter(Boolean).join("\n\n") }];
|
|
647
820
|
}
|
|
821
|
+
export const ALPHA_STATEFUL_RETRY_MAX_ATTEMPTS = 1;
|
|
822
|
+
const alphaSessionLocks = new Map();
|
|
823
|
+
export function alphaSearchRetryOptions(maxResponseBytes) {
|
|
824
|
+
return {
|
|
825
|
+
maxAttempts: ALPHA_STATEFUL_RETRY_MAX_ATTEMPTS,
|
|
826
|
+
...(typeof maxResponseBytes === "number" ? { maxResponseBytes } : {}),
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
export async function runWithAlphaSessionLock(sessionId, signal, task) {
|
|
830
|
+
if (typeof sessionId !== "string" || !sessionId)
|
|
831
|
+
throw failure("websearch_alpha requires a DSH session", "LCX_ALPHA_SESSION_REQUIRED");
|
|
832
|
+
let entry = alphaSessionLocks.get(sessionId);
|
|
833
|
+
if (!entry) {
|
|
834
|
+
entry = { mutex: new ServiceMutex(), users: 0 };
|
|
835
|
+
alphaSessionLocks.set(sessionId, entry);
|
|
836
|
+
}
|
|
837
|
+
entry.users += 1;
|
|
838
|
+
try {
|
|
839
|
+
return await entry.mutex.run(signal, task);
|
|
840
|
+
}
|
|
841
|
+
finally {
|
|
842
|
+
entry.users -= 1;
|
|
843
|
+
if (entry.users === 0 && alphaSessionLocks.get(sessionId) === entry)
|
|
844
|
+
alphaSessionLocks.delete(sessionId);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
export async function fetchAlphaSearchJson(options) {
|
|
848
|
+
return runWithAlphaSessionLock(options.sessionId, options.signal, () => fetchJsonWithRetry(options.url, options.body, options.headers, options.signal, options.timeoutMs, alphaSearchRetryOptions(options.maxResponseBytes)));
|
|
849
|
+
}
|
|
648
850
|
function probeFailureState(error) {
|
|
649
851
|
const detail = isObject(error) ? error : {};
|
|
650
852
|
if ([404, 405].includes(detail.status) ||
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { ALPHA_PROBE_VERSION } from "./web-search-alpha.js";
|
|
2
|
+
import { ALPHA_ACTIONS, ALPHA_PROBE_VERSION, ALPHA_SEARCH_PARAMETERS } from "./web-search-alpha.js";
|
|
3
3
|
import { JsonStore } from "./json-store.js";
|
|
4
4
|
const VERSION = 1;
|
|
5
5
|
const CLASSIFICATIONS = new Set(["native", "command-capable", "emulated-search-only", "unsupported", "unknown"]);
|
|
@@ -46,6 +46,31 @@ export function alphaCapabilityUsable(record) {
|
|
|
46
46
|
return validRecord(record) && record.probeVersion === ALPHA_PROBE_VERSION &&
|
|
47
47
|
(record.classification === "native" || record.classification === "command-capable");
|
|
48
48
|
}
|
|
49
|
+
export function alphaActionState(record, action) {
|
|
50
|
+
if (!validRecord(record) || typeof action !== "string" || !action)
|
|
51
|
+
return undefined;
|
|
52
|
+
const state = record.actions[action];
|
|
53
|
+
return isActionState(state) ? state : undefined;
|
|
54
|
+
}
|
|
55
|
+
export function assertAlphaActionAllowed(record, action) {
|
|
56
|
+
if (alphaActionState(record, action) === "unsupported") {
|
|
57
|
+
throw Object.assign(new Error(`Alpha action is unsupported for this verified route: ${String(action)}`), { code: "LCX_ALPHA_ACTION_UNSUPPORTED" });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function alphaAdvertisedActions(record) {
|
|
61
|
+
if (!validRecord(record))
|
|
62
|
+
return [...ALPHA_ACTIONS];
|
|
63
|
+
return ALPHA_ACTIONS.filter((action) => record.actions[action] !== "unsupported");
|
|
64
|
+
}
|
|
65
|
+
export function alphaSearchParametersFor(record) {
|
|
66
|
+
return {
|
|
67
|
+
...ALPHA_SEARCH_PARAMETERS,
|
|
68
|
+
properties: {
|
|
69
|
+
...ALPHA_SEARCH_PARAMETERS.properties,
|
|
70
|
+
action: { type: "string", enum: alphaAdvertisedActions(record) },
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
49
74
|
export class AlphaCapabilityStore {
|
|
50
75
|
store;
|
|
51
76
|
constructor(file) {
|