mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`, `verify-translation-pairing --write` for the touched bilingual pairs, `gen-doc-graphs`, and one typert snapshot whose ids embed character offsets. `pnpm run rescope-vendor --check` verifies the result. Renames nine vendored packages (cordis, cosmokit, schemastery and the six @cordisjs plugins) and every reference that resolves them: manifest names and dependency keys, module specifiers including declare-module merges, cordis.yml plugin names, tsconfig paths, every Markdown fence, and `docs/` prose. Directory names, upstream versions, and dependency ranges are unchanged, so vendor/README.md still reads as an upstream snapshot; its manifest table gains an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed at each fork's origin. The tutorial tier follows the rename end to end: its yaml fences named plugins the Loader can no longer resolve, its `ts ignore-check` fences disagreed with the compiled fences beside them, and its prose quoted both. The contracts that told readers to keep upstream names — the root convention and the vendoring cookbook's tree comment and manifest invariant — now say to rescope instead. Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle purity gate now names the vendored libraries a browser bundle inlines, and the files where a bare `cordis` is an agent-preset id keep that product data.
203 lines
8.1 KiB
TypeScript
203 lines
8.1 KiB
TypeScript
/**
|
|
* Service Definition for the web access capability seam (`ctx.web`): registries and provider-selecting execution for search and
|
|
* fetch. Duplicate ids are rejected. At execution time, a configured provider must exist and
|
|
* be usable; without one, exactly one usable provider is required, so selection never depends
|
|
* on registration order.
|
|
* @module @deepseek-ai/dsh-web
|
|
*/
|
|
|
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
import z from '@deepseek-ai/schemastery'
|
|
import type {
|
|
WebFetchProvider,
|
|
WebFetchRequest,
|
|
WebFetchResult,
|
|
WebSearchProvider,
|
|
WebSearchRequest,
|
|
WebSearchResult,
|
|
} from './types.ts'
|
|
import { WebError } from './types.ts'
|
|
|
|
export {
|
|
WebError,
|
|
} from './types.ts'
|
|
export type {
|
|
WebFetchBody,
|
|
WebFetchProvider,
|
|
WebFetchRequest,
|
|
WebFetchResult,
|
|
WebSearchProvider,
|
|
WebSearchRequest,
|
|
WebSearchResult,
|
|
WebSearchSource,
|
|
} from './types.ts'
|
|
|
|
declare module '@deepseek-ai/cordis' {
|
|
interface Context {
|
|
web: WebService
|
|
}
|
|
}
|
|
|
|
/** Selection inputs for execution-time provider resolution. */
|
|
interface Selection<P> {
|
|
/** The configured provider id for this capability, if any. */
|
|
readonly configuredId?: string
|
|
/** Providers registered for this capability kind. */
|
|
readonly providers: ReadonlyMap<string, P>
|
|
}
|
|
|
|
/**
|
|
* Config for the web seam. `searchProvider` / `fetchProvider` pin which provider
|
|
* wins for each capability; both are optional (a single registered usable
|
|
* provider auto-selects). Operational overrides such as environment variables
|
|
* must feed these same fields rather than introduce a hidden priority chain.
|
|
*/
|
|
export interface WebServiceConfig {
|
|
/** Explicit search provider id. Omitted = auto-select when exactly one usable. */
|
|
readonly searchProvider?: string
|
|
/** Explicit fetch provider id. Omitted = auto-select when exactly one usable. */
|
|
readonly fetchProvider?: string
|
|
}
|
|
|
|
/**
|
|
* The web access service. Registered as `ctx.web` (one instance per context).
|
|
*
|
|
* Selection semantics (resolved at execution time, never order-dependent):
|
|
* - A configured id that is registered and `available()` → that provider.
|
|
* - A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`.
|
|
* - A configured id registered but unavailable →
|
|
* `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`.
|
|
* - No id configured, exactly one registered usable provider → that provider.
|
|
* - No id configured, multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`.
|
|
* - No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`.
|
|
*/
|
|
export class WebService extends Service {
|
|
/**
|
|
* Provider selection config. Operational env overrides feed the SAME fields:
|
|
* `$DSH_WEB_SEARCH_PROVIDER` / `$DSH_WEB_FETCH_PROVIDER` are equivalent to
|
|
* `searchProvider` / `fetchProvider` and are NOT a hidden priority chain.
|
|
*/
|
|
static Config: z<WebServiceConfig> = z.object({
|
|
searchProvider: z.string(),
|
|
fetchProvider: z.string(),
|
|
})
|
|
|
|
private searchProviders = new Map<string, WebSearchProvider>()
|
|
private fetchProviders = new Map<string, WebFetchProvider>()
|
|
private readonly searchProviderId: string | undefined
|
|
private readonly fetchProviderId: string | undefined
|
|
|
|
constructor(ctx: Context, config: WebServiceConfig = {}) {
|
|
super(ctx, 'web')
|
|
this.searchProviderId = config.searchProvider ?? process.env.DSH_WEB_SEARCH_PROVIDER
|
|
this.fetchProviderId = config.fetchProvider ?? process.env.DSH_WEB_FETCH_PROVIDER
|
|
}
|
|
|
|
/**
|
|
* Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
|
|
* if its id is already registered for search. Returns a disposer; disposed
|
|
* with the calling fiber.
|
|
* @param provider - the provider; its `id` is the registry key.
|
|
* @returns the disposer that unregisters the provider.
|
|
*/
|
|
registerSearchProvider(provider: WebSearchProvider): () => void {
|
|
return this.registerProvider(this.searchProviders, provider)
|
|
}
|
|
|
|
/**
|
|
* Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
|
|
* if its id is already registered for fetch. Returns a disposer; disposed
|
|
* with the calling fiber.
|
|
* @param provider - the provider; its `id` is the registry key.
|
|
* @returns the disposer that unregisters the provider.
|
|
*/
|
|
registerFetchProvider(provider: WebFetchProvider): () => void {
|
|
return this.registerProvider(this.fetchProviders, provider)
|
|
}
|
|
|
|
private registerProvider<P extends { readonly id: string }>(store: Map<string, P>, provider: P): () => void {
|
|
if (store.has(provider.id)) {
|
|
throw new WebError(`a web provider with id "${provider.id}" is already registered`, 'WEB_DUPLICATE_PROVIDER')
|
|
}
|
|
const dispose = this.ctx.effect(function* () {
|
|
store.set(provider.id, provider)
|
|
yield () => store.delete(provider.id)
|
|
}, 'web.registerProvider()')
|
|
// ctx.effect's disposer returns Promise<void>; our disposer API is
|
|
// synchronous fire-and-forget — discard the (always-resolved) promise.
|
|
return () => void dispose()
|
|
}
|
|
|
|
/**
|
|
* Run one search through the selected provider. Resolves the provider at call
|
|
* time with the selection rules above; throws {@link WebError} when the
|
|
* capability cannot run. The seam enforces `request.maxResults` on the result:
|
|
* if the provider over-returns, `sources[]` is truncated and `truncated` set.
|
|
* @param request - the query and optional result limit.
|
|
* @param signal - optional cancellation signal forwarded to the provider.
|
|
* @returns the provider's results, capped to `request.maxResults`.
|
|
*/
|
|
async search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult> {
|
|
const provider = resolveProvider({
|
|
providers: this.searchProviders,
|
|
...this.searchProviderId !== undefined ? { configuredId: this.searchProviderId } : {},
|
|
})
|
|
const result = await provider.search(request, signal)
|
|
return capSources(result, request.maxResults)
|
|
}
|
|
|
|
/**
|
|
* Retrieve one URL through the selected provider. Resolves the provider at
|
|
* call time with the selection rules above; throws {@link WebError} when the
|
|
* capability cannot run. A non-2xx response is a result, not a throw.
|
|
* @param request - the URL plus retrieval options.
|
|
* @param signal - optional cancellation signal forwarded to the provider.
|
|
* @returns the retrieval outcome; non-2xx responses resolve descriptively.
|
|
*/
|
|
async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult> {
|
|
const provider = resolveProvider({
|
|
providers: this.fetchProviders,
|
|
...this.fetchProviderId !== undefined ? { configuredId: this.fetchProviderId } : {},
|
|
})
|
|
return provider.fetch(request, signal)
|
|
}
|
|
}
|
|
|
|
interface ResolvableProvider {
|
|
readonly id: string
|
|
available(): boolean
|
|
}
|
|
|
|
/** Resolve the selected provider or throw the matching {@link WebError}. */
|
|
function resolveProvider<P extends ResolvableProvider>(selection: Selection<P>): P {
|
|
const { configuredId, providers } = selection
|
|
if (configuredId !== undefined) {
|
|
const provider = providers.get(configuredId)
|
|
if (!provider) {
|
|
throw new WebError(`configured web provider "${configuredId}" is not registered`, 'WEB_PROVIDER_CONFIGURED_MISSING')
|
|
}
|
|
if (!provider.available()) {
|
|
throw new WebError(`configured web provider "${configuredId}" is registered but unavailable`, 'WEB_PROVIDER_CONFIGURED_UNAVAILABLE')
|
|
}
|
|
return provider
|
|
}
|
|
const usable = [...providers.values()].filter(provider => provider.available())
|
|
const [single] = usable
|
|
if (single === undefined) {
|
|
throw new WebError('no usable web provider is registered', 'WEB_PROVIDER_UNAVAILABLE')
|
|
}
|
|
if (usable.length > 1) {
|
|
const ids = usable.map(provider => provider.id).join(', ')
|
|
throw new WebError(`multiple usable web providers are registered (${ids}); configure one explicitly`, 'WEB_PROVIDER_AMBIGUOUS')
|
|
}
|
|
return single
|
|
}
|
|
|
|
/** Enforce `maxResults` on a search result: truncate `sources[]` and flag it. */
|
|
function capSources(result: WebSearchResult, maxResults: number | undefined): WebSearchResult {
|
|
if (maxResults === undefined || result.sources.length <= maxResults) return result
|
|
return { ...result, sources: result.sources.slice(0, maxResults), truncated: true }
|
|
}
|
|
|
|
export default WebService
|