Files
UHD_Media_Tool/apps/server/src/modules/websearch/capabilities.ts
T
rootandClaude Opus 5.5 d53df25fc8 feat(agent): web.search / web.fetch via Brave Search (WEB-1)
- adapters/websearch: Brave client (X-Subscription-Token, error classes),
  result normalization with redaction/truncation, SSRF guard, HTML→text fetch
  (10s, 512KB, text/html|text/plain only)
- modules/websearch: 10-min PG cache (region websearch), cross-process
  per-minute limit, web.fetch allowlist = URLs from the caller's latest
  web.search; POST /websearch/test
- settings.agent.webSearch (key write-only → secret_ref); settings card
  under Agent; web.* offered to the model only when enabled, plus a system
  prompt rule; outputs marked untrusted
- capability count 27 → 29; e2e E13; runbook docs/runbook/websearch.md

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 00:14:08 +08:00

45 lines
1.6 KiB
TypeScript

import { WebSearchError } from "@umt/adapters";
import { CapabilityError, type CapabilityErrorCode, type CapabilityRegistry } from "@umt/agent";
import { capabilitySpec } from "@umt/contracts";
import { allowScopeKey, type WebSearchService, WebSearchServiceError } from "./service.ts";
// web.search / web.fetch(read,low):错误按类别映射成能力错误;输出本身带 trust = untrusted 与不可信提示,
// 进入模型上下文前 PiAgentRuntime 还会整体过一遍 redactForModel。
const CODE: Record<string, CapabilityErrorCode> = {
disabled: "precondition",
not_allowed: "precondition",
blocked: "precondition",
unsupported: "precondition",
bad_request: "precondition",
auth: "precondition",
quota: "budget",
rate: "budget",
upstream: "precondition",
network: "precondition",
timeout: "precondition",
protocol: "precondition",
};
async function guard<T>(fn: () => Promise<T>): Promise<T> {
try {
return await fn();
} catch (e) {
if (e instanceof WebSearchError || e instanceof WebSearchServiceError)
throw new CapabilityError(CODE[e.kind] ?? "precondition", e.message, { kind: e.kind });
throw e;
}
}
export function registerWebSearchCapabilities(registry: CapabilityRegistry, svc: () => WebSearchService): void {
registry.register(capabilitySpec("web.search"), (input, c) =>
guard(async () => {
const { durationMs: _d, ...out } = await svc().search(input, { scopeKey: allowScopeKey(c), signal: c.signal });
return out;
}),
);
registry.register(capabilitySpec("web.fetch"), (input, c) =>
guard(() => svc().fetch(input.url, { scopeKey: allowScopeKey(c), signal: c.signal })),
);
}