Files
deepseek-harness/packages/host/apiproxy/src/index.ts
imccyu 070a2a7f1e refactor(commands): move the command service to Remote
`CommandService.list` and `execute` carry the wire contract directly through
`@Remote`, and the Client assembly mounts the generated commands
contribution. The legacy API Proxy route, its schemas, the map rows, the
generated client methods and the fixture's command domain are removed, so the
catalog and the admission call have one owner again.

`Session.command()` keeps a result-shaped public face for parity with the
prompt, cancel and attachment neighbours it sits beside, and reads the
generated namespace through one `SessionRemotes` parameter. The Session
cluster declares that face against the owning business package rather than the
generated contribution: the Host compiler aggregate builds this package, and
it runs before any contribution is emitted.

Migrated calls lose the `title-invalid` class of protocol-only error codes and
report `internal`; no production caller branched on them.
2026-08-11 23:33:15 +08:00

119 lines
4.6 KiB
TypeScript

/**
* @deepseek-ai/dsh-host-apiproxy — the API gateway every client shape shares:
* the ApiProxy contract (api/: types + zod schemas, browser-safe), the fetch
* carrier pair (fetch/: toFetchHandler on the host side, AbstractApiClient +
* platform subclasses on the client side), and the host-side implementation
* (api-proxy.ts: createApiProxy + the ApiProxyService gateway plugin providing
* `ctx.apiProxy`). Transport-agnostic by design: this package registers no
* routes — physical carriers wrap `ctx.apiProxy` themselves.
*
* The gateway consumes `ctx.agentDefaultModel`, the transport-independent default
* shared with direct entry points. Switching models persists through that
* service; sessions that have already logged a selection remain unchanged.
*/
import { Context, Service } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-agent-default-model'
import type { ApiProxy } from './api/index.ts'
import { createApiProxy } from './api-proxy.ts'
import {
DEFAULT_SESSION_LOG_COMPRESSION_LEVEL,
type SessionLogCompressionLevel,
} from './session-export.ts'
export type * from './api/index.ts'
export { RpcId } from './api/rpc.ts'
export { toFetchHandler } from './fetch/handler.ts'
export { AbstractApiClient, InProcessApiClient } from './fetch/client.ts'
export type { IApiClient } from './fetch/client.ts'
export { createApiProxy } from './api-proxy.ts'
export type { ApiProxyDefaults } from './api-proxy.ts'
declare module '@deepseek-ai/cordis' {
interface Context {
/** The host-side ApiProxy implementation (the transport-agnostic gateway face). */
apiProxy: ApiProxy
}
}
/** Gateway plugin configuration. */
export interface Config {
/**
* Whether this deployment can hand paths to a native desktop opener —
* the `hasDocument` capability the agent-preset roster reports. Absent,
* the platform is asked (macOS/Windows/WSL yes; Linux only with a display
* server); set it explicitly where detection misleads, e.g. `false` in a
* container whose DISPLAY points nowhere a user can see.
*/
nativeOpen?: boolean
/**
* DEFLATE level for every session-log ZIP entry: `0` stores without
* compression, `1` favors CPU/latency, and `9` favors archive size.
* @default 6
*/
sessionExportCompressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
}
/**
* The API gateway service: implements the ApiProxy contract over the composed
* host context and provides it as `ctx.apiProxy`. The Host cwd is the default
* project directory.
*/
export class ApiProxyService extends Service implements ApiProxy {
static inject = [
'agentDefaultModel', 'agents', 'attachments', 'directoryPicker', 'llm', 'sessions', 'subagents', 'sessionQuery',
'tools', 'userInteraction', 'workspace',
]
static Config: z<Config> = z.object({
nativeOpen: z.boolean(),
sessionExportCompressionLevel: z.number().step(1).min(0).max(9)
.default(DEFAULT_SESSION_LOG_COMPRESSION_LEVEL) as z<SessionLogCompressionLevel>,
})
readonly sessions: ApiProxy['sessions']
readonly subagents: ApiProxy['subagents']
readonly workspace: ApiProxy['workspace']
readonly host: ApiProxy['host']
readonly goals: ApiProxy['goals']
readonly skills: ApiProxy['skills']
readonly agentPresets: ApiProxy['agentPresets']
readonly settings: ApiProxy['settings']
readonly credentials: ApiProxy['credentials']
readonly llm: ApiProxy['llm']
readonly events: ApiProxy['events']
readonly downloads: ApiProxy['downloads']
readonly respond: ApiProxy['respond']
constructor(ctx: Context, config: Config) {
super(ctx, 'apiProxy')
const api = createApiProxy(ctx, {
defaultModelSelection: () => ctx.agentDefaultModel.currentSelection(),
saveDefaultModelSelection: selection => ctx.agentDefaultModel.saveSelection(selection),
cwd: process.cwd(),
...config.nativeOpen === undefined ? {} : { canOpenPath: () => config.nativeOpen as boolean },
...(config.sessionExportCompressionLevel === undefined
? {}
: { sessionExportCompressionLevel: config.sessionExportCompressionLevel }),
})
this.sessions = api.sessions
this.subagents = api.subagents
this.workspace = api.workspace
this.host = api.host
this.goals = api.goals
this.skills = api.skills
this.agentPresets = api.agentPresets
this.settings = api.settings
this.credentials = api.credentials
this.llm = api.llm
this.events = api.events
this.downloads = api.downloads
// createApiProxy returns closures (no `this` capture), so the bind is
// behavior-neutral.
this.respond = api.respond.bind(api)
}
}
export default ApiProxyService