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.
Files changed (42) hide show
  1. package/README.md +68 -99
  2. package/THIRD_PARTY_NOTICES.md +64 -0
  3. package/lib/auxiliary-usage.js +63 -0
  4. package/lib/client.js +1363 -295
  5. package/lib/dsh-compat.js +80 -18
  6. package/lib/dsh-responses.js +75 -8
  7. package/lib/grok-native-search.js +153 -79
  8. package/lib/index.js +159 -101
  9. package/lib/invocation-policy-scope.js +261 -0
  10. package/lib/json-store.js +11 -4
  11. package/lib/pi-responses-runtime.js +1571 -0
  12. package/lib/responses-request.js +1 -1
  13. package/lib/responses-stream.js +163 -92
  14. package/lib/route.js +17 -3
  15. package/lib/search-accounting.js +86 -0
  16. package/lib/search-usage.js +86 -0
  17. package/lib/transport.js +38 -10
  18. package/lib/types/client/index.d.ts +12 -0
  19. package/lib/types/client/search-media.d.ts +16 -0
  20. package/lib/web-run-output.js +22 -0
  21. package/lib/web-search-alpha.js +230 -28
  22. package/lib/web-search-capability.js +26 -1
  23. package/lib/web-search-hosted.js +119 -9
  24. package/lib/web-search-ref-store.js +88 -7
  25. package/package.json +50 -26
  26. package/lib/types/compact-v2.d.ts +0 -104
  27. package/lib/types/dsh-compat.d.ts +0 -78
  28. package/lib/types/dsh-responses.d.ts +0 -82
  29. package/lib/types/grok-native-search.d.ts +0 -39
  30. package/lib/types/json-store.d.ts +0 -10
  31. package/lib/types/native-checkpoint.d.ts +0 -213
  32. package/lib/types/responses-request.d.ts +0 -59
  33. package/lib/types/responses-stream.d.ts +0 -64
  34. package/lib/types/route.d.ts +0 -153
  35. package/lib/types/service-mutex.d.ts +0 -14
  36. package/lib/types/token-budget.d.ts +0 -50
  37. package/lib/types/transport.d.ts +0 -21
  38. package/lib/types/web-run-output.d.ts +0 -29
  39. package/lib/types/web-search-alpha.d.ts +0 -286
  40. package/lib/types/web-search-capability.d.ts +0 -26
  41. package/lib/types/web-search-hosted.d.ts +0 -246
  42. 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 timer = setTimeout(resolve, ms);
58
- signal?.addEventListener("abort", () => {
67
+ const onAbort = () => {
59
68
  clearTimeout(timer);
60
- reject(signal.reason);
61
- }, { once: true });
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 retryable = retryableStatus(response.status);
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 retryable = retryableStatus(response.status);
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
- await reader.cancel().catch(() => undefined);
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[];
@@ -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) {
@@ -1,6 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { isIP } from "node:net";
3
- import { outputDomains, outputLineRange, outputLinks, outputPdfRefs, parseWebRunOutput, } from "./web-run-output.js";
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 artifacts(results) {
518
- const refs = [], refRecords = [], sources = [], seenRefs = new Set(), seenSources = new Set();
519
- const visit = (value) => {
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 && !seenRefs.has(refId)) {
534
- seenRefs.add(refId);
535
- refs.push(refId);
536
- refRecords.push({ refId, ...(url ? { url } : {}) });
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 { refs, refRecords, sources };
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 match = String(output ?? "").match(ALPHA_COMMAND_ERROR_ENVELOPE);
560
- if (match && match[1].toLowerCase() === action)
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 = String(output ?? "").match(ALPHA_REFERENCE_ERROR_ENVELOPE);
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 a = artifacts(results);
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
- if (!a.refs.includes(ref))
596
- a.refs.push(ref);
597
- if (!a.refRecords.some((item) => item.refId === ref))
598
- a.refRecords.push({
599
- refId: ref,
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
- if (Array.isArray(data.sources) && data.sources.length)
642
- parts.push(`来源:\n${data.sources.filter(isObject).map((source) => `- [${String(source.title ?? source.url ?? "")}](${String(source.url ?? "")})`).join("\n")}`);
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) {