@abot-ai/runtime 1.1.0 → 1.2.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/README.md +6 -3
- package/dist/src/capabilities/tool-types.d.ts +1 -1
- package/dist/src/model-gateway/providers/ollama/generation-options.d.ts +1 -3
- package/dist/src/model-gateway/providers/ollama/generation-options.js +1 -5
- package/dist/src/model-gateway/providers/ollama/payload.js +0 -8
- package/dist/src/model-gateway/server/request-body.js +1 -0
- package/dist/src/plugin-sdk/index.d.ts +2 -0
- package/dist/src/plugin-sdk/index.js +2 -0
- package/dist/src/plugin-sdk/tool-availability-brief.d.ts +9 -0
- package/dist/src/plugin-sdk/tool-availability-brief.js +48 -0
- package/dist/src/plugin-sdk/tool-availability-overview.d.ts +9 -0
- package/dist/src/plugin-sdk/tool-availability-overview.js +62 -0
- package/dist/src/runtime/adapters/registered-tool-payload-plan.d.ts +0 -9
- package/dist/src/runtime/adapters/registered-tool-payload-plan.js +0 -67
- package/dist/src/runtime/adapters/registered-tool-payload-related-artifacts.d.ts +13 -0
- package/dist/src/runtime/adapters/registered-tool-payload-related-artifacts.js +84 -0
- package/dist/src/runtime/adapters/registered-tool-worker-capabilities/catalog.js +4 -0
- package/dist/src/runtime/adapters/registered-tool-worker-capabilities/execution.js +3 -0
- package/dist/src/runtime/adapters/registered-tool-worker-capabilities/payload-lifecycle.d.ts +1 -0
- package/dist/src/runtime/adapters/registered-tool-worker-capabilities/payload-lifecycle.js +11 -1
- package/dist/src/runtime/adapters/registered-tool-worker-capabilities/payload-stages.d.ts +1 -0
- package/dist/src/runtime/adapters/registered-tool-worker-capabilities/payload-stages.js +9 -1
- package/dist/src/runtime/capabilities/request-bound-tool-registry.js +1 -24
- package/dist/src/runtime/capabilities/tool-availability.d.ts +6 -0
- package/dist/src/runtime/capabilities/tool-availability.js +28 -0
- package/dist/src/runtime/context/capability-brief.d.ts +25 -0
- package/dist/src/runtime/context/capability-brief.js +106 -0
- package/dist/src/runtime/context/request-tool-results-message-binding.d.ts +6 -0
- package/dist/src/runtime/context/request-tool-results-message-binding.js +30 -0
- package/dist/src/runtime/context/request-tool-results-scope.d.ts +3 -0
- package/dist/src/runtime/context/request-tool-results-scope.js +8 -0
- package/dist/src/runtime/context/request-tool-results.d.ts +8 -4
- package/dist/src/runtime/context/request-tool-results.js +49 -38
- package/dist/src/runtime/orchestration/role-calls/candidate-validation.js +8 -1
- package/dist/src/runtime/orchestration/role-calls/child-return.d.ts +4 -0
- package/dist/src/runtime/orchestration/role-calls/child-return.js +25 -5
- package/dist/src/runtime/orchestration/role-calls/command-decoder.js +8 -3
- package/dist/src/runtime/orchestration/role-calls/command-dispatch/child-transitions.d.ts +1 -1
- package/dist/src/runtime/orchestration/role-calls/command-dispatch/child-transitions.js +17 -2
- package/dist/src/runtime/orchestration/role-calls/command-dispatch.d.ts +1 -1
- package/dist/src/runtime/orchestration/role-calls/command-dispatch.js +2 -2
- package/dist/src/runtime/orchestration/role-calls/contracts.d.ts +5 -2
- package/dist/src/runtime/orchestration/role-calls/contracts.js +4 -4
- package/dist/src/runtime/orchestration/role-calls/dependency-results.d.ts +1 -1
- package/dist/src/runtime/orchestration/role-calls/dependency-results.js +1 -0
- package/dist/src/runtime/orchestration/role-calls/index.d.ts +5 -1
- package/dist/src/runtime/orchestration/role-calls/index.js +5 -1
- package/dist/src/runtime/orchestration/role-calls/ledger.d.ts +3 -1
- package/dist/src/runtime/orchestration/role-calls/ledger.js +9 -2
- package/dist/src/runtime/orchestration/role-calls/reducer.d.ts +1 -1
- package/dist/src/runtime/orchestration/role-calls/reducer.js +2 -2
- package/dist/src/runtime/orchestration/role-calls/result-receipt.d.ts +49 -0
- package/dist/src/runtime/orchestration/role-calls/result-receipt.js +188 -0
- package/dist/src/runtime/orchestration/role-calls/return-result-receipt.d.ts +19 -0
- package/dist/src/runtime/orchestration/role-calls/return-result-receipt.js +52 -0
- package/dist/src/runtime/orchestration/role-calls/transactions.js +1 -0
- package/dist/src/runtime/orchestration/role-calls/work-result-lineage.d.ts +34 -0
- package/dist/src/runtime/orchestration/role-calls/work-result-lineage.js +185 -0
- package/dist/src/runtime/orchestration/role-calls/work-result-receipt.d.ts +36 -0
- package/dist/src/runtime/orchestration/role-calls/work-result-receipt.js +187 -0
- package/dist/src/runtime/orchestration/role-executors/activation/result-normalization.js +6 -3
- package/dist/src/runtime/orchestration/role-executors/child-invocation/transaction.js +1 -0
- package/dist/src/runtime/orchestration/role-executors/contracts.d.ts +2 -1
- package/dist/src/runtime/orchestration/worker-capabilities/assignment-provenance.d.ts +36 -0
- package/dist/src/runtime/orchestration/worker-capabilities/assignment-provenance.js +171 -0
- package/dist/src/runtime/orchestration/worker-capabilities/contracts.d.ts +32 -1
- package/dist/src/runtime/orchestration/worker-capabilities/execution/invocation-preparation.d.ts +2 -1
- package/dist/src/runtime/orchestration/worker-capabilities/execution/invocation-preparation.js +3 -0
- package/dist/src/runtime/orchestration/worker-capabilities/execution.js +42 -19
- package/dist/src/runtime/orchestration/worker-capabilities/index.d.ts +2 -0
- package/dist/src/runtime/orchestration/worker-capabilities/index.js +2 -0
- package/dist/src/runtime/orchestration/worker-capabilities/payload-assignment-validation.d.ts +3 -0
- package/dist/src/runtime/orchestration/worker-capabilities/payload-assignment-validation.js +30 -0
- package/dist/src/runtime/orchestration/worker-capabilities/payload-author.js +8 -19
- package/dist/src/runtime/orchestration/worker-capabilities/payload-contracts.d.ts +5 -1
- package/dist/src/runtime/orchestration/worker-capabilities/payload-instructions.js +1 -1
- package/dist/src/runtime/orchestration/worker-capabilities/payload-source-provenance.d.ts +14 -0
- package/dist/src/runtime/orchestration/worker-capabilities/payload-source-provenance.js +132 -0
- package/dist/src/runtime/request/capability-brief.d.ts +5 -0
- package/dist/src/runtime/request/capability-brief.js +24 -0
- package/dist/src/runtime/request/execution-scope.d.ts +2 -1
- package/dist/src/runtime/request/role-executor-composition.js +1 -1
- package/dist/src/runtime/request/worker-capability-composition.d.ts +2 -1
- package/dist/src/runtime/request/worker-capability-composition.js +6 -1
- package/dist/src/runtime/request/worker-capability-payload.js +11 -8
- package/dist/src/runtime/steps/execution-agent/prompt.js +4 -3
- package/dist/src/runtime/steps/planner-decision/capability-catalog-context.d.ts +14 -0
- package/dist/src/runtime/steps/planner-decision/capability-catalog-context.js +87 -0
- package/dist/src/runtime/steps/planner-decision/contracts.d.ts +3 -5
- package/dist/src/runtime/steps/planner-decision/contracts.js +3 -3
- package/dist/src/runtime/steps/planner-decision/format.d.ts +3 -2
- package/dist/src/runtime/steps/planner-decision/format.js +73 -76
- package/dist/src/runtime/steps/planner-decision/input.d.ts +4 -3
- package/dist/src/runtime/steps/planner-decision/input.js +10 -7
- package/dist/src/runtime/steps/planner-decision/parser.d.ts +3 -2
- package/dist/src/runtime/steps/planner-decision/parser.js +10 -19
- package/dist/src/runtime/steps/planner-decision/plan-parser.js +18 -12
- package/dist/src/runtime/steps/planner-decision/prompt.js +33 -28
- package/dist/src/runtime/steps/planner-decision/resume.d.ts +1 -1
- package/dist/src/runtime/steps/planner-decision/resume.js +38 -1
- package/dist/src/runtime/steps/planner-decision/run.d.ts +3 -2
- package/dist/src/runtime/steps/reviewer-decision/audit-coverage.d.ts +10 -0
- package/dist/src/runtime/steps/reviewer-decision/audit-coverage.js +186 -0
- package/dist/src/runtime/steps/reviewer-decision/contracts.d.ts +3 -8
- package/dist/src/runtime/steps/reviewer-decision/contracts.js +12 -6
- package/dist/src/runtime/steps/reviewer-decision/delegated-context.d.ts +24 -0
- package/dist/src/runtime/steps/reviewer-decision/delegated-context.js +70 -0
- package/dist/src/runtime/steps/reviewer-decision/diagnostics.js +1 -1
- package/dist/src/runtime/steps/reviewer-decision/format.js +37 -24
- package/dist/src/runtime/steps/reviewer-decision/input.js +1 -1
- package/dist/src/runtime/steps/reviewer-decision/model-context.d.ts +4 -3
- package/dist/src/runtime/steps/reviewer-decision/model-context.js +92 -10
- package/dist/src/runtime/steps/reviewer-decision/parser.js +19 -1
- package/dist/src/runtime/steps/reviewer-decision/projection.js +42 -68
- package/dist/src/runtime/steps/reviewer-decision/prompt.js +10 -5
- package/dist/src/runtime/steps/reviewer-decision/run.js +12 -3
- package/dist/src/runtime/steps/reviewer-decision/verification-coverage.d.ts +14 -0
- package/dist/src/runtime/steps/reviewer-decision/verification-coverage.js +52 -0
- package/dist/src/runtime/steps/supervisor-decision/capability-brief.d.ts +14 -0
- package/dist/src/runtime/steps/supervisor-decision/capability-brief.js +38 -0
- package/dist/src/runtime/steps/supervisor-decision/contracts.d.ts +7 -3
- package/dist/src/runtime/steps/supervisor-decision/diagnostics.d.ts +2 -0
- package/dist/src/runtime/steps/supervisor-decision/diagnostics.js +8 -2
- package/dist/src/runtime/steps/supervisor-decision/format.js +32 -20
- package/dist/src/runtime/steps/supervisor-decision/input.d.ts +4 -1
- package/dist/src/runtime/steps/supervisor-decision/input.js +19 -1
- package/dist/src/runtime/steps/supervisor-decision/parser.js +62 -59
- package/dist/src/runtime/steps/supervisor-decision/prompt.js +6 -7
- package/dist/src/runtime/steps/supervisor-decision/resume.js +47 -1
- package/dist/src/runtime/steps/supervisor-decision/run.js +10 -0
- package/dist/src/runtime/steps/supervisor-decision/working-directory.js +2 -1
- package/dist/src/runtime/steps/worker-decision/contracts.d.ts +2 -1
- package/dist/src/runtime/steps/worker-decision/input/canonical-state.js +8 -1
- package/dist/src/runtime/steps/worker-decision/input/diagnostic-projection.js +2 -2
- package/dist/src/runtime/steps/worker-decision/input/prompt-context.d.ts +1 -0
- package/dist/src/runtime/steps/worker-decision/input/prompt-context.js +4 -1
- package/dist/src/runtime/steps/worker-decision/input/reference-context.js +34 -14
- package/dist/src/runtime/steps/worker-decision/input/types.d.ts +2 -1
- package/dist/src/runtime/steps/worker-decision/input.d.ts +1 -0
- package/dist/src/runtime/steps/worker-decision/input.js +1 -0
- package/dist/src/runtime/steps/worker-decision/payload-dependency-results.d.ts +8 -0
- package/dist/src/runtime/steps/worker-decision/payload-dependency-results.js +19 -0
- package/dist/src/runtime/steps/worker-decision/prompt.js +8 -8
- package/dist/src/runtime/steps/worker-decision/result-author.js +18 -11
- package/dist/src/runtime/steps/worker-decision/run.js +2 -2
- package/dist/src/web-ui/app/ARCHITECTURE.md +19 -6
- package/dist/src/web-ui/app/app.js +6 -7
- package/dist/src/web-ui/app/components/composer-context-window.d.ts +25 -0
- package/dist/src/web-ui/app/components/composer-context-window.js +155 -0
- package/dist/src/web-ui/app/components/composer-plan.d.ts +9 -0
- package/dist/src/web-ui/app/components/composer-plan.js +128 -0
- package/dist/src/web-ui/app/components/conversation-activity.d.ts +5 -6
- package/dist/src/web-ui/app/components/conversation-activity.js +64 -157
- package/dist/src/web-ui/app/components/conversation-role-cards.d.ts +23 -0
- package/dist/src/web-ui/app/components/conversation-role-cards.js +181 -0
- package/dist/src/web-ui/app/components/conversation-sidebar-layout.js +12 -0
- package/dist/src/web-ui/app/components/conversation-sources.d.ts +7 -0
- package/dist/src/web-ui/app/components/conversation-sources.js +182 -0
- package/dist/src/web-ui/app/components/conversation-view.js +34 -6
- package/dist/src/web-ui/app/components/model-selector.js +4 -1
- package/dist/src/web-ui/app/components/operations-section.js +46 -0
- package/dist/src/web-ui/app/components/workspace-shell.js +86 -78
- package/dist/src/web-ui/app/controllers/realtime-event-controller.d.ts +1 -0
- package/dist/src/web-ui/app/controllers/realtime-event-controller.js +55 -90
- package/dist/src/web-ui/app/controllers/runtime-selection-controller.js +2 -0
- package/dist/src/web-ui/app/controllers/session-controller.js +17 -12
- package/dist/src/web-ui/app/index.html +209 -210
- package/dist/src/web-ui/app/lib/composer-plan-model.d.ts +32 -0
- package/dist/src/web-ui/app/lib/composer-plan-model.js +108 -0
- package/dist/src/web-ui/app/lib/conversation-role-model.d.ts +19 -0
- package/dist/src/web-ui/app/lib/conversation-role-model.js +193 -0
- package/dist/src/web-ui/app/lib/event-presentation.js +30 -9
- package/dist/src/web-ui/app/lib/source-url-policy.d.ts +2 -0
- package/dist/src/web-ui/app/lib/source-url-policy.js +31 -0
- package/dist/src/web-ui/app/lib/task-progress.d.ts +53 -0
- package/dist/src/web-ui/app/lib/task-progress.js +233 -0
- package/dist/src/web-ui/app/lib/tool-invocation-count.d.ts +5 -0
- package/dist/src/web-ui/app/lib/tool-invocation-count.js +21 -0
- package/dist/src/web-ui/app/lib/web-source-event.d.ts +8 -0
- package/dist/src/web-ui/app/lib/web-source-event.js +21 -0
- package/dist/src/web-ui/app/lib/web-sources.d.ts +36 -0
- package/dist/src/web-ui/app/lib/web-sources.js +177 -0
- package/dist/src/web-ui/app/styles/00-tokens-base.css +15 -3
- package/dist/src/web-ui/app/styles/10-shell-sessions.css +24 -34
- package/dist/src/web-ui/app/styles/12-conversation-sidebar.css +93 -0
- package/dist/src/web-ui/app/styles/20-chat-composer.css +55 -472
- package/dist/src/web-ui/app/styles/21-composer-controls.css +368 -0
- package/dist/src/web-ui/app/styles/22-composer-context-window.css +90 -0
- package/dist/src/web-ui/app/styles/23-composer-plan.css +171 -0
- package/dist/src/web-ui/app/styles/24-conversation-role-cards.css +205 -0
- package/dist/src/web-ui/app/styles/25-conversation-sources.css +141 -0
- package/dist/src/web-ui/app/styles/30-inspector-config.css +21 -209
- package/dist/src/web-ui/app/styles/35-memory-onboarding.css +8 -8
- package/dist/src/web-ui/app/styles/36-memory-management.css +13 -13
- package/dist/src/web-ui/app/styles/37-config-operations.css +197 -0
- package/dist/src/web-ui/app/styles/40-config-workspace.css +36 -36
- package/dist/src/web-ui/app/styles/90-responsive.css +14 -77
- package/dist/src/web-ui/app/styles/foundation.css +2 -2
- package/dist/src/web-ui/app/styles.css +7 -0
- package/dist/src/web-ui/app/ui-behavior.d.ts +1 -1
- package/dist/src/web-ui/app/ui-behavior.js +1 -1
- package/docs/configuration.md +15 -8
- package/docs/installation.md +7 -4
- package/docs/known-limitations.md +21 -0
- package/docs/plugins.md +28 -13
- package/docs/runtime-invariant-coverage.md +18 -17
- package/docs/runtime-prompt-policy-contracts.md +27 -17
- package/package.json +4 -3
- package/plugins/capability-brief/source/index.ts +5 -48
- package/plugins/capability-brief/src/index.cjs +46 -33
- package/plugins/system-probe/plugin.json +1 -0
- package/plugins/web/README.md +125 -0
- package/plugins/web/plugin.json +1 -1
- package/plugins/web/skills/web_fetch_skill/SKILL.md +2 -2
- package/plugins/web/skills/web_search_skill/SKILL.md +8 -4
- package/plugins/web/source/errors.ts +5 -1
- package/plugins/web/source/fetch-presentation.ts +61 -0
- package/plugins/web/source/fetch-service.ts +2 -31
- package/plugins/web/source/light/config.ts +196 -0
- package/plugins/web/source/light/crawl/origin-gate.ts +134 -0
- package/plugins/web/source/light/crawl/robots-cache.ts +71 -0
- package/plugins/web/source/light/crawl/robots-parser.ts +132 -0
- package/plugins/web/source/light/crawl/robots-policy.ts +154 -0
- package/plugins/web/source/light/crawl/session-budget.ts +141 -0
- package/plugins/web/source/light/crawl/transport.ts +216 -0
- package/plugins/web/source/light/discovery/candidate-values.ts +63 -0
- package/plugins/web/source/light/discovery/feed-document.ts +127 -0
- package/plugins/web/source/light/discovery/html-links.ts +80 -0
- package/plugins/web/source/light/discovery/html-robots-directives.ts +29 -0
- package/plugins/web/source/light/discovery/markup-tree.ts +145 -0
- package/plugins/web/source/light/discovery/origin-dates.ts +71 -0
- package/plugins/web/source/light/discovery/sitemap-document.ts +63 -0
- package/plugins/web/source/light/documents/cache-policy.ts +28 -0
- package/plugins/web/source/light/documents/document-cache.ts +99 -0
- package/plugins/web/source/light/documents/document-charset.ts +89 -0
- package/plugins/web/source/light/documents/document-contract.ts +20 -0
- package/plugins/web/source/light/documents/document-reader.ts +75 -0
- package/plugins/web/source/light/documents/extract-document.ts +124 -0
- package/plugins/web/source/light/documents/http-cache-freshness.ts +68 -0
- package/plugins/web/source/light/documents/response-decoding.ts +120 -0
- package/plugins/web/source/light/ranking/document-ranking.ts +114 -0
- package/plugins/web/source/light/ranking/query-normalization.ts +69 -0
- package/plugins/web/source/light/search-collection.ts +136 -0
- package/plugins/web/source/light/search-frontier.ts +246 -0
- package/plugins/web/source/light/search-metadata.ts +84 -0
- package/plugins/web/source/light/search-service.ts +154 -0
- package/plugins/web/source/light/search-unavailable.ts +33 -0
- package/plugins/web/source/light/sources/catalog.ts +312 -0
- package/plugins/web/source/light/sources/source-definition.ts +9 -0
- package/plugins/web/source/light/sources/source-selection.ts +35 -0
- package/plugins/web/source/plugin.ts +70 -56
- package/plugins/web/source/public-http.ts +10 -1
- package/plugins/web/source/search-presentation.ts +63 -63
- package/plugins/web/source/search-service-selector.ts +31 -0
- package/plugins/web/source/search-service.ts +6 -0
- package/plugins/web/source/search-source-presentation.ts +76 -0
- package/plugins/web/source/search-types.ts +4 -0
- package/plugins/web/source/source-output-receipts.ts +267 -0
- package/plugins/web/source/source-receipt-budget.ts +51 -0
- package/plugins/web/source/source-receipt-contract.ts +18 -0
- package/plugins/web/src/index.cjs +3214 -288
- package/src/runtime/README.md +30 -9
- package/dist/src/model-gateway/structured-output/budget.d.ts +0 -7
- package/dist/src/model-gateway/structured-output/budget.js +0 -20
- package/dist/src/model-gateway/structured-output/schema-character-estimator.d.ts +0 -1
- package/dist/src/model-gateway/structured-output/schema-character-estimator.js +0 -211
- package/dist/src/model-gateway/structured-output-budget.d.ts +0 -1
- package/dist/src/model-gateway/structured-output-budget.js +0 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Parser } from "htmlparser2";
|
|
2
|
+
|
|
3
|
+
import { WebPluginError } from "../../errors.js";
|
|
4
|
+
|
|
5
|
+
export type MarkupElement = {
|
|
6
|
+
name: string;
|
|
7
|
+
attributes: Readonly<Record<string, string>>;
|
|
8
|
+
children: (MarkupElement | string)[];
|
|
9
|
+
parent?: MarkupElement;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const MAX_MARKUP_NODES = 32_768;
|
|
13
|
+
const MAX_MARKUP_DEPTH = 64;
|
|
14
|
+
|
|
15
|
+
function rejectUnsafeDeclarations(markup: string, xmlMode: boolean): void {
|
|
16
|
+
if (/<!entity\b/iu.test(markup)) {
|
|
17
|
+
throw new WebPluginError(
|
|
18
|
+
"web_response_invalid",
|
|
19
|
+
"Entity declarations are not supported.",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const declarations = markup.match(/<!doctype\b[^>]*>/giu) ?? [];
|
|
23
|
+
const hasUnsafeDoctype = declarations.some(
|
|
24
|
+
(declaration) => xmlMode || !/^<!doctype\s+html\s*>$/iu.test(declaration),
|
|
25
|
+
);
|
|
26
|
+
if (hasUnsafeDoctype) {
|
|
27
|
+
throw new WebPluginError(
|
|
28
|
+
"web_response_invalid",
|
|
29
|
+
"Document type declarations are not supported.",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assertMarkupCapacity(nodes: number, depth: number): void {
|
|
35
|
+
if (nodes > MAX_MARKUP_NODES) {
|
|
36
|
+
throw new WebPluginError(
|
|
37
|
+
"web_response_invalid",
|
|
38
|
+
"The source markup exceeds parsing limits.",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (depth > MAX_MARKUP_DEPTH) {
|
|
42
|
+
throw new WebPluginError(
|
|
43
|
+
"web_response_invalid",
|
|
44
|
+
"The source markup exceeds parsing limits.",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseMarkup(markup: string, xmlMode: boolean): MarkupElement {
|
|
50
|
+
rejectUnsafeDeclarations(markup, xmlMode);
|
|
51
|
+
const root: MarkupElement = {
|
|
52
|
+
name: "#document",
|
|
53
|
+
attributes: {},
|
|
54
|
+
children: [],
|
|
55
|
+
};
|
|
56
|
+
const stack = [root];
|
|
57
|
+
let nodes = 0;
|
|
58
|
+
const parser = new Parser(
|
|
59
|
+
{
|
|
60
|
+
onopentag(name, attributes) {
|
|
61
|
+
nodes += 1;
|
|
62
|
+
assertMarkupCapacity(nodes, stack.length);
|
|
63
|
+
const parent = stack[stack.length - 1]!;
|
|
64
|
+
const element: MarkupElement = {
|
|
65
|
+
name,
|
|
66
|
+
attributes,
|
|
67
|
+
children: [],
|
|
68
|
+
parent,
|
|
69
|
+
};
|
|
70
|
+
parent.children.push(element);
|
|
71
|
+
stack.push(element);
|
|
72
|
+
},
|
|
73
|
+
ontext(text) {
|
|
74
|
+
nodes += 1;
|
|
75
|
+
assertMarkupCapacity(nodes, stack.length);
|
|
76
|
+
stack[stack.length - 1]!.children.push(text);
|
|
77
|
+
},
|
|
78
|
+
onclosetag() {
|
|
79
|
+
if (stack.length > 1) stack.pop();
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
xmlMode,
|
|
84
|
+
decodeEntities: true,
|
|
85
|
+
recognizeCDATA: true,
|
|
86
|
+
lowerCaseTags: !xmlMode,
|
|
87
|
+
lowerCaseAttributeNames: !xmlMode,
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
parser.end(markup);
|
|
91
|
+
return root;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function localName(element: MarkupElement): string {
|
|
95
|
+
return element.name.split(":").at(-1)!.toLowerCase();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function childElements(
|
|
99
|
+
element: MarkupElement,
|
|
100
|
+
): readonly MarkupElement[] {
|
|
101
|
+
return element.children.filter(
|
|
102
|
+
(child): child is MarkupElement => typeof child !== "string",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function descendants(element: MarkupElement): readonly MarkupElement[] {
|
|
107
|
+
const result: MarkupElement[] = [];
|
|
108
|
+
const queue = [...childElements(element)].reverse();
|
|
109
|
+
while (queue.length > 0) {
|
|
110
|
+
const current = queue.pop()!;
|
|
111
|
+
result.push(current);
|
|
112
|
+
const children = childElements(current);
|
|
113
|
+
for (let index = children.length - 1; index >= 0; index -= 1)
|
|
114
|
+
queue.push(children[index]!);
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function elementText(element: MarkupElement): string {
|
|
120
|
+
const values: string[] = [];
|
|
121
|
+
const pending: (MarkupElement | string)[] = [...element.children].reverse();
|
|
122
|
+
while (pending.length > 0) {
|
|
123
|
+
const current = pending.pop()!;
|
|
124
|
+
if (typeof current === "string") {
|
|
125
|
+
values.push(current);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
values.push(" ");
|
|
129
|
+
pending.push(" ");
|
|
130
|
+
for (let index = current.children.length - 1; index >= 0; index -= 1) {
|
|
131
|
+
pending.push(current.children[index]!);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return values.join("").replace(/\s+/gu, " ").trim();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function firstChildText(
|
|
138
|
+
element: MarkupElement,
|
|
139
|
+
names: readonly string[],
|
|
140
|
+
): string {
|
|
141
|
+
const child = childElements(element).find((candidate) =>
|
|
142
|
+
names.includes(localName(candidate)),
|
|
143
|
+
);
|
|
144
|
+
return child ? elementText(child) : "";
|
|
145
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { localName, type MarkupElement } from "./markup-tree.js";
|
|
2
|
+
|
|
3
|
+
export type OriginDates = Readonly<{
|
|
4
|
+
publishedAt?: string;
|
|
5
|
+
updatedAt?: string;
|
|
6
|
+
}>;
|
|
7
|
+
|
|
8
|
+
const PUBLISHED_METADATA_NAMES = [
|
|
9
|
+
"article:published_time",
|
|
10
|
+
"datepublished",
|
|
11
|
+
"pubdate",
|
|
12
|
+
];
|
|
13
|
+
const UPDATED_METADATA_NAMES = [
|
|
14
|
+
"article:modified_time",
|
|
15
|
+
"datemodified",
|
|
16
|
+
"lastmod",
|
|
17
|
+
];
|
|
18
|
+
const ISO_DATE =
|
|
19
|
+
/^(\d{4}-\d{2}-\d{2})(?:T(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d{1,9})?)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d))?$/u;
|
|
20
|
+
|
|
21
|
+
function normalizedOriginDate(raw: string): string | undefined {
|
|
22
|
+
const value = raw.trim();
|
|
23
|
+
if (value.length > 100) return undefined;
|
|
24
|
+
const match = value.match(ISO_DATE);
|
|
25
|
+
if (!match) return undefined;
|
|
26
|
+
const dateOnly = match[1]!;
|
|
27
|
+
const calendarTimestamp = Date.parse(`${dateOnly}T00:00:00.000Z`);
|
|
28
|
+
if (!Number.isFinite(calendarTimestamp)) return undefined;
|
|
29
|
+
if (new Date(calendarTimestamp).toISOString().slice(0, 10) !== dateOnly)
|
|
30
|
+
return undefined;
|
|
31
|
+
const timestamp = Date.parse(value);
|
|
32
|
+
return Number.isFinite(timestamp)
|
|
33
|
+
? new Date(timestamp).toISOString()
|
|
34
|
+
: undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function declaresMetadataDate(
|
|
38
|
+
element: MarkupElement,
|
|
39
|
+
names: readonly string[],
|
|
40
|
+
): boolean {
|
|
41
|
+
if (localName(element) !== "meta") return false;
|
|
42
|
+
const declared = [
|
|
43
|
+
element.attributes.property,
|
|
44
|
+
element.attributes.name,
|
|
45
|
+
element.attributes.itemprop,
|
|
46
|
+
].flatMap((value) => (value ?? "").toLowerCase().split(/\s+/u));
|
|
47
|
+
return declared.some((name) => names.includes(name));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readMetadataDate(
|
|
51
|
+
elements: readonly MarkupElement[],
|
|
52
|
+
names: readonly string[],
|
|
53
|
+
): string | undefined {
|
|
54
|
+
for (const element of elements) {
|
|
55
|
+
if (!declaresMetadataDate(element, names)) continue;
|
|
56
|
+
const date = normalizedOriginDate(element.attributes.content ?? "");
|
|
57
|
+
if (date) return date;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function readOriginDates(
|
|
63
|
+
elements: readonly MarkupElement[],
|
|
64
|
+
): OriginDates {
|
|
65
|
+
const publishedAt = readMetadataDate(elements, PUBLISHED_METADATA_NAMES);
|
|
66
|
+
const updatedAt = readMetadataDate(elements, UPDATED_METADATA_NAMES);
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
...(publishedAt ? { publishedAt } : {}),
|
|
69
|
+
...(updatedAt ? { updatedAt } : {}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LightDocument,
|
|
3
|
+
DiscoveryCandidate,
|
|
4
|
+
} from "../documents/document-contract.js";
|
|
5
|
+
import {
|
|
6
|
+
elementBaseUrl,
|
|
7
|
+
normalizedSourceDate,
|
|
8
|
+
resolveDiscoveryUrl,
|
|
9
|
+
} from "./candidate-values.js";
|
|
10
|
+
import {
|
|
11
|
+
childElements,
|
|
12
|
+
elementText,
|
|
13
|
+
firstChildText,
|
|
14
|
+
localName,
|
|
15
|
+
type MarkupElement,
|
|
16
|
+
} from "./markup-tree.js";
|
|
17
|
+
|
|
18
|
+
export function parseSitemapDocument(
|
|
19
|
+
root: MarkupElement,
|
|
20
|
+
documentUrl: string,
|
|
21
|
+
maxCandidates: number,
|
|
22
|
+
): LightDocument {
|
|
23
|
+
const container = childElements(root)[0] ?? root;
|
|
24
|
+
const isSitemapIndex = localName(container) === "sitemapindex";
|
|
25
|
+
const expectedEntryName = isSitemapIndex ? "sitemap" : "url";
|
|
26
|
+
const links: DiscoveryCandidate[] = [];
|
|
27
|
+
const seen = new Set<string>();
|
|
28
|
+
let partial = false;
|
|
29
|
+
for (const entry of childElements(container)) {
|
|
30
|
+
if (localName(entry) !== expectedEntryName) continue;
|
|
31
|
+
const location = childElements(entry).find(
|
|
32
|
+
(child) => localName(child) === "loc",
|
|
33
|
+
);
|
|
34
|
+
if (!location) continue;
|
|
35
|
+
const url = resolveDiscoveryUrl(
|
|
36
|
+
elementText(location),
|
|
37
|
+
elementBaseUrl(location, documentUrl),
|
|
38
|
+
);
|
|
39
|
+
if (!url || seen.has(url)) continue;
|
|
40
|
+
if (links.length >= maxCandidates) {
|
|
41
|
+
partial = true;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
const updatedAt = normalizedSourceDate(firstChildText(entry, ["lastmod"]));
|
|
45
|
+
seen.add(url);
|
|
46
|
+
links.push(
|
|
47
|
+
Object.freeze({
|
|
48
|
+
url,
|
|
49
|
+
title: "",
|
|
50
|
+
kind: isSitemapIndex ? "sitemap" : "page",
|
|
51
|
+
...(updatedAt ? { updatedAt } : {}),
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
canonicalUrl: documentUrl,
|
|
57
|
+
title: "",
|
|
58
|
+
text: "",
|
|
59
|
+
kind: "sitemap",
|
|
60
|
+
links: Object.freeze(links),
|
|
61
|
+
partial,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PublicHttpResponse } from "../../public-http.js";
|
|
2
|
+
import type { LightConfig } from "../config.js";
|
|
3
|
+
import type { LightDocument } from "./document-contract.js";
|
|
4
|
+
import { httpCacheFreshnessMs } from "./http-cache-freshness.js";
|
|
5
|
+
|
|
6
|
+
function header(response: PublicHttpResponse, name: string): string {
|
|
7
|
+
const value = response.headers[name];
|
|
8
|
+
return Array.isArray(value) ? value.join(",") : (value ?? "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function documentExpiresAt(
|
|
12
|
+
response: PublicHttpResponse,
|
|
13
|
+
kind: LightDocument["kind"],
|
|
14
|
+
config: LightConfig,
|
|
15
|
+
now: number,
|
|
16
|
+
): number {
|
|
17
|
+
const directives = header(response, "cache-control")
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.split(",")
|
|
20
|
+
.map((value) => value.trim());
|
|
21
|
+
const names = new Set(
|
|
22
|
+
directives.map((value) => value.split("=", 1)[0]!.trim()),
|
|
23
|
+
);
|
|
24
|
+
if (names.has("private")) return now;
|
|
25
|
+
if (header(response, "vary").trim() === "*") return now;
|
|
26
|
+
const maximum = kind === "page" ? config.pageTtlMs : config.feedTtlMs;
|
|
27
|
+
return now + httpCacheFreshnessMs(response, maximum, now);
|
|
28
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { FetchedPage } from "../../content.js";
|
|
2
|
+
import type { LightConfig } from "../config.js";
|
|
3
|
+
import type { LightDocument } from "./document-contract.js";
|
|
4
|
+
|
|
5
|
+
export type LightCachedDocument = Readonly<{
|
|
6
|
+
sourceId: string;
|
|
7
|
+
document: LightDocument;
|
|
8
|
+
fetchedAt: string;
|
|
9
|
+
expiresAt: number;
|
|
10
|
+
page?: FetchedPage;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
type CacheEntry = Readonly<{ value: LightCachedDocument; size: number }>;
|
|
14
|
+
|
|
15
|
+
export function createDocumentCache(
|
|
16
|
+
config: Pick<LightConfig, "cacheMaxDocuments" | "cacheMaxBytes">,
|
|
17
|
+
) {
|
|
18
|
+
const entries = new Map<string, CacheEntry>();
|
|
19
|
+
const aliases = new Map<string, string>();
|
|
20
|
+
let bytes = 0;
|
|
21
|
+
|
|
22
|
+
function remove(url: string): void {
|
|
23
|
+
const entry = entries.get(url);
|
|
24
|
+
if (!entry) return;
|
|
25
|
+
bytes -= entry.size;
|
|
26
|
+
entries.delete(url);
|
|
27
|
+
for (const [alias, target] of aliases) {
|
|
28
|
+
if (target === url) aliases.delete(alias);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function get(url: string, now = Date.now()): LightCachedDocument | undefined {
|
|
33
|
+
const canonical = aliases.get(url) ?? url;
|
|
34
|
+
const entry = entries.get(canonical);
|
|
35
|
+
if (!entry) return undefined;
|
|
36
|
+
if (entry.value.expiresAt <= now) {
|
|
37
|
+
remove(canonical);
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
entries.delete(canonical);
|
|
41
|
+
entries.set(canonical, entry);
|
|
42
|
+
return entry.value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function put(
|
|
46
|
+
value: LightCachedDocument,
|
|
47
|
+
requestedUrl: string,
|
|
48
|
+
now = Date.now(),
|
|
49
|
+
): void {
|
|
50
|
+
const excludedFromIndex = value.document.noIndex === true;
|
|
51
|
+
if (excludedFromIndex) {
|
|
52
|
+
remove(value.document.canonicalUrl);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (value.expiresAt <= now) return;
|
|
56
|
+
if (value.document.partial) return;
|
|
57
|
+
if (config.cacheMaxDocuments === 0) return;
|
|
58
|
+
const size = Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
59
|
+
if (size > config.cacheMaxBytes) return;
|
|
60
|
+
const url = value.document.canonicalUrl;
|
|
61
|
+
remove(url);
|
|
62
|
+
while (
|
|
63
|
+
entries.size >= config.cacheMaxDocuments ||
|
|
64
|
+
bytes + size > config.cacheMaxBytes
|
|
65
|
+
) {
|
|
66
|
+
const oldest = entries.keys().next().value;
|
|
67
|
+
if (oldest === undefined) break;
|
|
68
|
+
remove(oldest);
|
|
69
|
+
}
|
|
70
|
+
entries.set(url, { value, size });
|
|
71
|
+
bytes += size;
|
|
72
|
+
aliases.set(requestedUrl, url);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function forSources(
|
|
76
|
+
sourceIds: ReadonlySet<string>,
|
|
77
|
+
now = Date.now(),
|
|
78
|
+
): readonly LightCachedDocument[] {
|
|
79
|
+
const values: LightCachedDocument[] = [];
|
|
80
|
+
for (const [url, entry] of entries) {
|
|
81
|
+
if (entry.value.expiresAt <= now) {
|
|
82
|
+
remove(url);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (sourceIds.has(entry.value.sourceId)) values.push(entry.value);
|
|
86
|
+
}
|
|
87
|
+
return Object.freeze(values);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
get,
|
|
92
|
+
put,
|
|
93
|
+
forSources,
|
|
94
|
+
size: () => entries.size,
|
|
95
|
+
bytes: () => bytes,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type LightDocumentCache = ReturnType<typeof createDocumentCache>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { WebPluginError } from "../../errors.js";
|
|
2
|
+
import type { PublicHttpResponse } from "../../public-http.js";
|
|
3
|
+
|
|
4
|
+
const SUPPORTED_DOCUMENT_ENCODINGS = new Set([
|
|
5
|
+
"utf-8",
|
|
6
|
+
"utf-16le",
|
|
7
|
+
"utf-16be",
|
|
8
|
+
"windows-1252",
|
|
9
|
+
"windows-1255",
|
|
10
|
+
"iso-8859-8",
|
|
11
|
+
"iso-8859-8-i",
|
|
12
|
+
]);
|
|
13
|
+
const MAX_ENCODING_LABEL_CHARS = 64;
|
|
14
|
+
const XML_DECLARATION_PREFIX_BYTES = 1_024;
|
|
15
|
+
|
|
16
|
+
function unsupportedDocumentEncoding(): never {
|
|
17
|
+
throw new WebPluginError(
|
|
18
|
+
"web_response_unsupported",
|
|
19
|
+
"The Light source declares an unsupported character encoding.",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function byteOrderEncoding(body: Uint8Array): string | undefined {
|
|
24
|
+
const prefix = Buffer.from(body.subarray(0, 4)).toString("hex");
|
|
25
|
+
const isUtf32 = prefix === "fffe0000" || prefix === "0000feff";
|
|
26
|
+
if (isUtf32) return unsupportedDocumentEncoding();
|
|
27
|
+
if (prefix.startsWith("efbbbf")) return "utf-8";
|
|
28
|
+
if (prefix.startsWith("fffe")) return "utf-16le";
|
|
29
|
+
if (prefix.startsWith("feff")) return "utf-16be";
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function httpDeclaredEncoding(
|
|
34
|
+
response: PublicHttpResponse,
|
|
35
|
+
): string | undefined {
|
|
36
|
+
const raw = response.headers["content-type"];
|
|
37
|
+
const header = (Array.isArray(raw) ? raw[0] : raw) ?? "";
|
|
38
|
+
const declaration =
|
|
39
|
+
/;\s*charset\s*=\s*(?:"([^"]*)"|'([^']*)'|([^;\s]*))/iu.exec(header);
|
|
40
|
+
if (!declaration) return undefined;
|
|
41
|
+
return declaration[1] ?? declaration[2] ?? declaration[3] ?? "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function xmlDeclaredEncoding(body: Uint8Array): string | undefined {
|
|
45
|
+
const prefix = Buffer.from(
|
|
46
|
+
body.subarray(0, XML_DECLARATION_PREFIX_BYTES),
|
|
47
|
+
).toString("latin1");
|
|
48
|
+
const declaration =
|
|
49
|
+
/^<\?xml\s[^?]*\bencoding\s*=\s*(?:"([^"]*)"|'([^']*)')[^?]*\?>/iu.exec(
|
|
50
|
+
prefix,
|
|
51
|
+
);
|
|
52
|
+
if (!declaration) return undefined;
|
|
53
|
+
return declaration[1] ?? declaration[2] ?? "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function documentDecoder(label: string): TextDecoder {
|
|
57
|
+
const normalized = label.trim();
|
|
58
|
+
if (normalized.length === 0) return unsupportedDocumentEncoding();
|
|
59
|
+
if (normalized.length > MAX_ENCODING_LABEL_CHARS)
|
|
60
|
+
return unsupportedDocumentEncoding();
|
|
61
|
+
let decoder: TextDecoder;
|
|
62
|
+
try {
|
|
63
|
+
decoder = new TextDecoder(normalized, { fatal: true });
|
|
64
|
+
} catch {
|
|
65
|
+
return unsupportedDocumentEncoding();
|
|
66
|
+
}
|
|
67
|
+
const supportedDocumentEncoding = SUPPORTED_DOCUMENT_ENCODINGS.has(
|
|
68
|
+
decoder.encoding,
|
|
69
|
+
);
|
|
70
|
+
if (!supportedDocumentEncoding) return unsupportedDocumentEncoding();
|
|
71
|
+
return decoder;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function decodeLightDocumentText(response: PublicHttpResponse): string {
|
|
75
|
+
const label =
|
|
76
|
+
byteOrderEncoding(response.body) ??
|
|
77
|
+
httpDeclaredEncoding(response) ??
|
|
78
|
+
xmlDeclaredEncoding(response.body) ??
|
|
79
|
+
"utf-8";
|
|
80
|
+
const decoder = documentDecoder(label);
|
|
81
|
+
try {
|
|
82
|
+
return decoder.decode(response.body, { stream: response.partialContent });
|
|
83
|
+
} catch {
|
|
84
|
+
throw new WebPluginError(
|
|
85
|
+
"web_response_invalid",
|
|
86
|
+
"The Light source contains invalid bytes for its character encoding.",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type DiscoveryCandidate = Readonly<{
|
|
2
|
+
url: string;
|
|
3
|
+
title: string;
|
|
4
|
+
kind: "page" | "feed" | "sitemap";
|
|
5
|
+
publishedAt?: string;
|
|
6
|
+
updatedAt?: string;
|
|
7
|
+
text?: string;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export type LightDocument = Readonly<{
|
|
11
|
+
canonicalUrl: string;
|
|
12
|
+
title: string;
|
|
13
|
+
text: string;
|
|
14
|
+
kind: "page" | "feed" | "sitemap";
|
|
15
|
+
links: readonly DiscoveryCandidate[];
|
|
16
|
+
publishedAt?: string;
|
|
17
|
+
updatedAt?: string;
|
|
18
|
+
partial: boolean;
|
|
19
|
+
noIndex?: boolean;
|
|
20
|
+
}>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { FetchedPage } from "../../content.js";
|
|
2
|
+
import { WebPluginError } from "../../errors.js";
|
|
3
|
+
import type { PublicHttpResponse } from "../../public-http.js";
|
|
4
|
+
import type { LightConfig } from "../config.js";
|
|
5
|
+
import type { LightCrawlSession } from "../crawl/transport.js";
|
|
6
|
+
import { documentExpiresAt } from "./cache-policy.js";
|
|
7
|
+
import type {
|
|
8
|
+
LightCachedDocument,
|
|
9
|
+
LightDocumentCache,
|
|
10
|
+
} from "./document-cache.js";
|
|
11
|
+
import { parseLightDocument } from "./extract-document.js";
|
|
12
|
+
import { decodeLightResponse } from "./response-decoding.js";
|
|
13
|
+
|
|
14
|
+
function asFetchedPage(
|
|
15
|
+
record: Omit<LightCachedDocument, "page">,
|
|
16
|
+
response: PublicHttpResponse,
|
|
17
|
+
): FetchedPage | undefined {
|
|
18
|
+
if (record.document.kind !== "page") return undefined;
|
|
19
|
+
const type = response.headers["content-type"];
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
requestedUrl: response.requestedUrl,
|
|
22
|
+
finalUrl: response.finalUrl,
|
|
23
|
+
status: response.status,
|
|
24
|
+
contentType: (Array.isArray(type) ? type[0] : type) ?? "",
|
|
25
|
+
title: record.document.title,
|
|
26
|
+
text: record.document.text,
|
|
27
|
+
bytesRead: response.bytesRead,
|
|
28
|
+
partialContent: record.document.partial,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function readLightDocument(params: {
|
|
33
|
+
url: string;
|
|
34
|
+
sourceId: string;
|
|
35
|
+
session: LightCrawlSession;
|
|
36
|
+
config: LightConfig;
|
|
37
|
+
cache: LightDocumentCache;
|
|
38
|
+
}): Promise<Readonly<{ record: LightCachedDocument; fromCache: boolean }>> {
|
|
39
|
+
params.session.assertActive();
|
|
40
|
+
const cached = params.cache.get(params.url);
|
|
41
|
+
if (cached) return Object.freeze({ record: cached, fromCache: true });
|
|
42
|
+
const raw = await params.session.get(params.url);
|
|
43
|
+
if (raw.finalUrl.length > 1_024)
|
|
44
|
+
throw new WebPluginError(
|
|
45
|
+
"web_target_invalid",
|
|
46
|
+
"The Light source URL exceeds 1024 characters.",
|
|
47
|
+
);
|
|
48
|
+
const remaining =
|
|
49
|
+
params.config.maxTotalBytes - params.session.snapshot().decodedBytes;
|
|
50
|
+
if (remaining <= 0)
|
|
51
|
+
throw new WebPluginError(
|
|
52
|
+
"web_search_budget_exhausted",
|
|
53
|
+
"The Light decoded content budget is exhausted.",
|
|
54
|
+
);
|
|
55
|
+
const response = decodeLightResponse(
|
|
56
|
+
raw,
|
|
57
|
+
Math.min(params.config.maxResponseBytes, remaining),
|
|
58
|
+
params.session.consumeDecodedBytes,
|
|
59
|
+
);
|
|
60
|
+
params.session.assertActive();
|
|
61
|
+
const document = parseLightDocument(response, params.config.maxCandidates);
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const base = Object.freeze({
|
|
64
|
+
sourceId: params.sourceId,
|
|
65
|
+
document,
|
|
66
|
+
fetchedAt: new Date(now).toISOString(),
|
|
67
|
+
expiresAt: documentExpiresAt(response, document.kind, params.config, now),
|
|
68
|
+
});
|
|
69
|
+
const record = Object.freeze({
|
|
70
|
+
...base,
|
|
71
|
+
page: asFetchedPage(base, response),
|
|
72
|
+
});
|
|
73
|
+
params.cache.put(record, params.url, now);
|
|
74
|
+
return Object.freeze({ record, fromCache: false });
|
|
75
|
+
}
|