dsh-edge 0.1.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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +183 -0
- package/README.zh.md +183 -0
- package/THIRD_PARTY_NOTICES.md +199 -0
- package/dist/_headers +3 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/index-C-1AiF3k.js +112 -0
- package/dist/assets/index-CSGf6Qzd.css +1 -0
- package/dist/assets/langs/c-BIGW1oBm.js +2 -0
- package/dist/assets/langs/cpp-DIPi6g--.js +2 -0
- package/dist/assets/langs/csharp-DSvCPggb.js +2 -0
- package/dist/assets/langs/css-CLj8gQPS.js +2 -0
- package/dist/assets/langs/go-C27-OAKa.js +2 -0
- package/dist/assets/langs/html-CfGypltT.js +2 -0
- package/dist/assets/langs/ini-BEwlwnbL.js +2 -0
- package/dist/assets/langs/java-CylS5w8V.js +2 -0
- package/dist/assets/langs/kotlin-BdnUsdx6.js +2 -0
- package/dist/assets/langs/less-B1dDrJ26.js +2 -0
- package/dist/assets/langs/lua-BaeVxFsk.js +2 -0
- package/dist/assets/langs/markdown-Cvjx9yec.js +2 -0
- package/dist/assets/langs/mdx-Cmh6b_Ma.js +2 -0
- package/dist/assets/langs/php-D-uCvoST.js +2 -0
- package/dist/assets/langs/python-B6aJPvgy.js +2 -0
- package/dist/assets/langs/ruby-DEItuUFs.js +2 -0
- package/dist/assets/langs/rust-B1yitclQ.js +2 -0
- package/dist/assets/langs/scss-D5BDwBP9.js +2 -0
- package/dist/assets/langs/sql-CRqJ_cUM.js +2 -0
- package/dist/assets/langs/swift-D82vCrfD.js +2 -0
- package/dist/assets/langs/toml-vGWfd6FD.js +2 -0
- package/dist/assets/langs/xml-sdJ4AIDG.js +2 -0
- package/dist/assets/langs/yaml-Buea-lGh.js +2 -0
- package/dist/assets/vendor-Cjbwl5VI.js +411 -0
- package/dist/assets/vendor-CjyC-hUb.css +1 -0
- package/dist/favicon.svg +8 -0
- package/dist/index.html +35 -0
- package/dist/manifest.webmanifest +16 -0
- package/dist/plugins/@deepseek-ai/dsh-api-gateway/client.js +420 -0
- package/dist/plugins/@deepseek-ai/dsh-api-remotes/client.js +5937 -0
- package/dist/plugins/@deepseek-ai/dsh-client-connection/client.js +10207 -0
- package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +1231 -0
- package/dist/plugins/@deepseek-ai/dsh-client-modules/client.js +259 -0
- package/dist/plugins/@deepseek-ai/dsh-client-runtime/client.js +10539 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +1725 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +1116 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +9832 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +373 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-edge/client.js +549 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +859 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +280 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +456 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +804 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +461 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings/client.js +254 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +609 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +292 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +327 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +710 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +1312 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +1630 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +7370 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +700 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +474 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +2426 -0
- package/dist/plugins/@deepseek-ai/dsh-typert-registry/client.js +1372 -0
- package/package.json +97 -0
- package/scripts/assemble-web.mjs +230 -0
- package/scripts/bundle-size.d.mts +2 -0
- package/scripts/bundle-size.mjs +26 -0
- package/scripts/bundle.mjs +53 -0
- package/scripts/cli.d.mts +39 -0
- package/scripts/cli.mjs +496 -0
- package/scripts/install.d.mts +149 -0
- package/scripts/install.mjs +989 -0
- package/scripts/legal-files.mjs +24 -0
- package/scripts/verify-packed.mjs +32 -0
- package/scripts/wrangler-config.d.mts +18 -0
- package/scripts/wrangler-config.mjs +84 -0
- package/src/agent.ts +119 -0
- package/src/auth.ts +352 -0
- package/src/deepseek.ts +231 -0
- package/src/deployment.ts +147 -0
- package/src/direct-shell-core-empty.ts +10 -0
- package/src/direct-shell-protocol.ts +5 -0
- package/src/direct-shell.ts +341 -0
- package/src/do-session-persistence.ts +1064 -0
- package/src/edge-api.ts +796 -0
- package/src/edge-credentials.ts +56 -0
- package/src/edge-remotes.ts +41 -0
- package/src/edge-workspace-store.ts +339 -0
- package/src/http.ts +212 -0
- package/src/index.ts +196 -0
- package/src/instance.ts +1007 -0
- package/src/isolated-direct-shell-unavailable.ts +11 -0
- package/src/protocol.ts +39 -0
- package/src/release.ts +9 -0
- package/src/session-store.ts +1110 -0
- package/src/sse.ts +86 -0
- package/src/web-search.ts +38 -0
- package/src/workspace.ts +270 -0
- package/wrangler.jsonc +56 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Read-only upstream credential provider over Cloudflare Worker secrets. */
|
|
2
|
+
|
|
3
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
+
import CredentialProvider, {
|
|
5
|
+
credentialRef,
|
|
6
|
+
type CredentialInfo,
|
|
7
|
+
type CredentialRef,
|
|
8
|
+
type ResolvedCredential,
|
|
9
|
+
} from '@deepseek-ai/dsh-credentials'
|
|
10
|
+
|
|
11
|
+
export const EDGE_DEEPSEEK_API_KEY_REF = credentialRef('DEEPSEEK_API_KEY')
|
|
12
|
+
|
|
13
|
+
export interface EdgeCredentialProviderConfig {
|
|
14
|
+
/** Read the current deployment secret without copying it into Durable Object storage. */
|
|
15
|
+
readDeepSeekApiKey(): string | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Resolve deployment credentials through the same seam used by upstream providers. */
|
|
19
|
+
export class EdgeCredentialProvider extends CredentialProvider {
|
|
20
|
+
constructor(
|
|
21
|
+
ctx: Context,
|
|
22
|
+
private readonly config: EdgeCredentialProviderConfig,
|
|
23
|
+
) {
|
|
24
|
+
super(ctx)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Resolve the supported Worker secret for one operation. */
|
|
28
|
+
override resolve(ref: CredentialRef): Promise<ResolvedCredential | undefined> {
|
|
29
|
+
if (ref !== EDGE_DEEPSEEK_API_KEY_REF) return Promise.resolve(undefined)
|
|
30
|
+
const value = this.config.readDeepSeekApiKey()?.trim()
|
|
31
|
+
if (value === undefined || value.length === 0) return Promise.resolve(undefined)
|
|
32
|
+
return Promise.resolve({ value, source: 'worker-secret' })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Describe a Worker secret without exposing its value. */
|
|
36
|
+
override async describe(ref: CredentialRef): Promise<CredentialInfo> {
|
|
37
|
+
const resolved = await this.resolve(ref)
|
|
38
|
+
return {
|
|
39
|
+
configured: resolved !== undefined,
|
|
40
|
+
...resolved === undefined ? {} : { source: resolved.source },
|
|
41
|
+
writable: false,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Cloudflare secrets are changed through deployment tooling, not runtime RPCs. */
|
|
46
|
+
override set(_ref: CredentialRef, _value: string): Promise<void> {
|
|
47
|
+
return Promise.reject(new Error('dsh-edge credentials are read-only; update the Cloudflare Worker secret.'))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Cloudflare secrets are changed through deployment tooling, not runtime RPCs. */
|
|
51
|
+
override unset(_ref: CredentialRef): Promise<void> {
|
|
52
|
+
return Promise.reject(new Error('dsh-edge credentials are read-only; update the Cloudflare Worker secret.'))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default EdgeCredentialProvider
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Minimal generated-Remote compatibility required by the upstream Web UI. */
|
|
2
|
+
|
|
3
|
+
import type { ServerResponse } from '@deepseek-ai/dsh-host-apiproxy/api'
|
|
4
|
+
import { clientRequestSchema } from '@deepseek-ai/dsh-host-apiproxy/api/rpc.schema'
|
|
5
|
+
|
|
6
|
+
const COMMAND_LIST_PATH = '/api/commands/list'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Handle Edge-owned endpoints on upstream's shared generic `/api` channel.
|
|
10
|
+
* The Edge preset currently registers no slash commands, so its truthful
|
|
11
|
+
* catalog is empty; retaining the upstream carrier envelope lets ui-commands
|
|
12
|
+
* stay mounted without a parallel Edge-only client implementation.
|
|
13
|
+
*/
|
|
14
|
+
export async function handleEdgeRemote(request: Request): Promise<Response | undefined> {
|
|
15
|
+
const url = new URL(request.url)
|
|
16
|
+
if (url.pathname !== COMMAND_LIST_PATH) return undefined
|
|
17
|
+
if (request.method !== 'POST') {
|
|
18
|
+
return new Response('method not allowed', { status: 405, headers: { allow: 'POST' } })
|
|
19
|
+
}
|
|
20
|
+
if (request.headers.get('content-type')?.split(';', 1).at(0)?.trim() !== 'application/json') {
|
|
21
|
+
return new Response('expected application/json', { status: 415 })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let wire: unknown
|
|
25
|
+
try {
|
|
26
|
+
wire = await request.json()
|
|
27
|
+
} catch {
|
|
28
|
+
return new Response('invalid JSON body', { status: 400 })
|
|
29
|
+
}
|
|
30
|
+
const parsed = clientRequestSchema.safeParse(wire)
|
|
31
|
+
if (!parsed.success || parsed.data.method !== 'commands/list') {
|
|
32
|
+
return new Response('request path and method do not match', { status: 400 })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const body: ServerResponse = {
|
|
36
|
+
type: 'server-response',
|
|
37
|
+
rpcId: parsed.data.rpcId,
|
|
38
|
+
result: { ok: true, value: [] },
|
|
39
|
+
}
|
|
40
|
+
return Response.json(body)
|
|
41
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/** Cloudflare Durable Object adapter for the upstream workspace domain. */
|
|
2
|
+
|
|
3
|
+
import type { WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-host-apiproxy/api'
|
|
4
|
+
import type { SessionId } from '@deepseek-ai/dsh-session'
|
|
5
|
+
import type { EdgeApiSessionSummary } from './session-store.ts'
|
|
6
|
+
|
|
7
|
+
export const EDGE_WORKSPACE_ID = 'edge-workspace' as WorkspaceId
|
|
8
|
+
export const EDGE_WORKSPACE_PATH = '/workspace'
|
|
9
|
+
|
|
10
|
+
const WORKSPACE_DOMAIN_STATE_KEY = 'dsh-edge:workspace-domain-state:v2'
|
|
11
|
+
const WORKSPACE_RECORD_KEY = 'dsh-edge:workspace-domain-workspaces:edge-workspace:v2'
|
|
12
|
+
|
|
13
|
+
/** Exact upstream workspace-domain global value, backed by DO storage. */
|
|
14
|
+
interface EdgeWorkspaceDomainState {
|
|
15
|
+
initialized: boolean
|
|
16
|
+
workspaceIds: WorkspaceId[]
|
|
17
|
+
archivedSessionIds: SessionId[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Exact upstream workspaces-table value, backed by DO storage. */
|
|
21
|
+
interface EdgeWorkspaceRecord {
|
|
22
|
+
path: string
|
|
23
|
+
title: string
|
|
24
|
+
sessionIds: SessionId[]
|
|
25
|
+
createdAt: string
|
|
26
|
+
updatedAt: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class EdgeWorkspaceStoreError extends Error {
|
|
30
|
+
constructor(
|
|
31
|
+
readonly code: 'NOT_FOUND' | 'INVALID_PATH' | 'MOVE_INVALID',
|
|
32
|
+
message: string,
|
|
33
|
+
) {
|
|
34
|
+
super(message)
|
|
35
|
+
this.name = 'EdgeWorkspaceStoreError'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Preserve the upstream workspace records/global-state split while replacing
|
|
41
|
+
* its Node filesystem and storage-domain backends with one DO transaction.
|
|
42
|
+
*/
|
|
43
|
+
export class EdgeWorkspaceStore {
|
|
44
|
+
private operationTail: Promise<void> = Promise.resolve()
|
|
45
|
+
|
|
46
|
+
constructor(private readonly storage: DurableObjectStorage) {}
|
|
47
|
+
|
|
48
|
+
list(sessions: readonly EdgeApiSessionSummary[]): Promise<{
|
|
49
|
+
items: WorkspaceView[]
|
|
50
|
+
archivedSessionIds: SessionId[]
|
|
51
|
+
}> {
|
|
52
|
+
return this.serialize(async () => {
|
|
53
|
+
const state = await this.readState()
|
|
54
|
+
if (!state.workspaceIds.includes(EDGE_WORKSPACE_ID)) {
|
|
55
|
+
return { items: [], archivedSessionIds: state.archivedSessionIds }
|
|
56
|
+
}
|
|
57
|
+
const record = await this.readOrCreateRecord(sessions)
|
|
58
|
+
return {
|
|
59
|
+
items: [workspaceView(record)],
|
|
60
|
+
archivedSessionIds: state.archivedSessionIds,
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
create(
|
|
66
|
+
path: string,
|
|
67
|
+
sessions: readonly EdgeApiSessionSummary[],
|
|
68
|
+
): Promise<{ workspace: WorkspaceView; created: boolean }> {
|
|
69
|
+
return this.serialize(async () => {
|
|
70
|
+
if (path !== EDGE_WORKSPACE_PATH) {
|
|
71
|
+
throw new EdgeWorkspaceStoreError(
|
|
72
|
+
'INVALID_PATH',
|
|
73
|
+
`Edge workspaces must use ${EDGE_WORKSPACE_PATH}.`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
const state = await this.readState()
|
|
77
|
+
if (state.workspaceIds.includes(EDGE_WORKSPACE_ID)) {
|
|
78
|
+
const record = await this.readOrCreateRecord(sessions)
|
|
79
|
+
return { workspace: workspaceView(record), created: false }
|
|
80
|
+
}
|
|
81
|
+
const now = new Date().toISOString()
|
|
82
|
+
const record: EdgeWorkspaceRecord = {
|
|
83
|
+
path: EDGE_WORKSPACE_PATH,
|
|
84
|
+
title: 'workspace',
|
|
85
|
+
sessionIds: [],
|
|
86
|
+
createdAt: now,
|
|
87
|
+
updatedAt: now,
|
|
88
|
+
}
|
|
89
|
+
await this.storage.transaction(async (txn) => {
|
|
90
|
+
await txn.put(WORKSPACE_RECORD_KEY, record)
|
|
91
|
+
await txn.put(WORKSPACE_DOMAIN_STATE_KEY, {
|
|
92
|
+
...state,
|
|
93
|
+
workspaceIds: [EDGE_WORKSPACE_ID],
|
|
94
|
+
} satisfies EdgeWorkspaceDomainState)
|
|
95
|
+
})
|
|
96
|
+
return { workspace: workspaceView(record), created: true }
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
rename(workspaceId: WorkspaceId, title: string, sessions: readonly EdgeApiSessionSummary[]): Promise<{
|
|
101
|
+
workspace: WorkspaceView
|
|
102
|
+
changed: boolean
|
|
103
|
+
}> {
|
|
104
|
+
return this.serialize(async () => {
|
|
105
|
+
await this.requireWorkspace(workspaceId)
|
|
106
|
+
const current = await this.readOrCreateRecord(sessions)
|
|
107
|
+
if (current.title === title) return { workspace: workspaceView(current), changed: false }
|
|
108
|
+
const next = { ...current, title, updatedAt: new Date().toISOString() }
|
|
109
|
+
await this.storage.put(WORKSPACE_RECORD_KEY, next)
|
|
110
|
+
return { workspace: workspaceView(next), changed: true }
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
delete(workspaceId: WorkspaceId): Promise<void> {
|
|
115
|
+
return this.serialize(async () => {
|
|
116
|
+
const state = await this.requireWorkspace(workspaceId)
|
|
117
|
+
await this.storage.transaction(async (txn) => {
|
|
118
|
+
await txn.delete(WORKSPACE_RECORD_KEY)
|
|
119
|
+
await txn.put(WORKSPACE_DOMAIN_STATE_KEY, {
|
|
120
|
+
...state,
|
|
121
|
+
workspaceIds: [],
|
|
122
|
+
} satisfies EdgeWorkspaceDomainState)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
insertBefore(workspaceId: WorkspaceId, beforeWorkspaceId?: WorkspaceId): Promise<WorkspaceId[]> {
|
|
128
|
+
return this.serialize(async () => {
|
|
129
|
+
const state = await this.requireWorkspace(workspaceId)
|
|
130
|
+
if (beforeWorkspaceId !== undefined && beforeWorkspaceId !== EDGE_WORKSPACE_ID) {
|
|
131
|
+
throw new EdgeWorkspaceStoreError(
|
|
132
|
+
'NOT_FOUND',
|
|
133
|
+
`Workspace "${beforeWorkspaceId}" is not available in this Edge instance.`,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
return [...state.workspaceIds]
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
insertSessionBefore(
|
|
141
|
+
workspaceId: WorkspaceId,
|
|
142
|
+
sessionId: SessionId,
|
|
143
|
+
beforeSessionId: SessionId | undefined,
|
|
144
|
+
sessions: readonly EdgeApiSessionSummary[],
|
|
145
|
+
): Promise<{ workspace: WorkspaceView; changed: boolean }> {
|
|
146
|
+
return this.serialize(async () => {
|
|
147
|
+
await this.requireWorkspace(workspaceId)
|
|
148
|
+
const current = await this.readOrCreateRecord(sessions)
|
|
149
|
+
if (!current.sessionIds.includes(sessionId)) {
|
|
150
|
+
throw new EdgeWorkspaceStoreError(
|
|
151
|
+
'MOVE_INVALID',
|
|
152
|
+
`Session "${sessionId}" is not accounted by workspace "${workspaceId}".`,
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
if (beforeSessionId !== undefined && !current.sessionIds.includes(beforeSessionId)) {
|
|
156
|
+
throw new EdgeWorkspaceStoreError(
|
|
157
|
+
'MOVE_INVALID',
|
|
158
|
+
`Anchor session "${beforeSessionId}" is not accounted by workspace "${workspaceId}".`,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
if (beforeSessionId === sessionId) {
|
|
162
|
+
return { workspace: workspaceView(current), changed: false }
|
|
163
|
+
}
|
|
164
|
+
const without = current.sessionIds.filter(id => id !== sessionId)
|
|
165
|
+
const index = beforeSessionId === undefined ? without.length : without.indexOf(beforeSessionId)
|
|
166
|
+
const sessionIds = [...without.slice(0, index), sessionId, ...without.slice(index)]
|
|
167
|
+
if (sameIds(sessionIds, current.sessionIds)) {
|
|
168
|
+
return { workspace: workspaceView(current), changed: false }
|
|
169
|
+
}
|
|
170
|
+
const next = { ...current, sessionIds, updatedAt: new Date().toISOString() }
|
|
171
|
+
await this.storage.put(WORKSPACE_RECORD_KEY, next)
|
|
172
|
+
return { workspace: workspaceView(next), changed: true }
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
attachSession(
|
|
177
|
+
workspaceId: WorkspaceId,
|
|
178
|
+
sessionId: SessionId,
|
|
179
|
+
sessions: readonly EdgeApiSessionSummary[],
|
|
180
|
+
): Promise<WorkspaceView> {
|
|
181
|
+
return this.serialize(async () => {
|
|
182
|
+
await this.requireWorkspace(workspaceId)
|
|
183
|
+
const current = await this.readOrCreateRecord(sessions)
|
|
184
|
+
if (current.sessionIds.includes(sessionId)) return workspaceView(current)
|
|
185
|
+
const next = {
|
|
186
|
+
...current,
|
|
187
|
+
sessionIds: [sessionId, ...current.sessionIds],
|
|
188
|
+
updatedAt: new Date().toISOString(),
|
|
189
|
+
}
|
|
190
|
+
await this.storage.put(WORKSPACE_RECORD_KEY, next)
|
|
191
|
+
return workspaceView(next)
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
workspaceForSession(
|
|
196
|
+
sessionId: SessionId,
|
|
197
|
+
sessions: readonly EdgeApiSessionSummary[],
|
|
198
|
+
): Promise<WorkspaceId | undefined> {
|
|
199
|
+
return this.serialize(async () => {
|
|
200
|
+
const state = await this.readState()
|
|
201
|
+
if (!state.workspaceIds.includes(EDGE_WORKSPACE_ID)) return undefined
|
|
202
|
+
const current = await this.readOrCreateRecord(sessions)
|
|
203
|
+
return current.sessionIds.includes(sessionId) ? EDGE_WORKSPACE_ID : undefined
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
archivedSessionIds(): Promise<SessionId[]> {
|
|
208
|
+
return this.serialize(async () => [...(await this.readState()).archivedSessionIds])
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
archiveSession(sessionId: SessionId, requireSession: () => Promise<unknown>): Promise<{
|
|
212
|
+
archivedSessionIds: SessionId[]
|
|
213
|
+
changed: boolean
|
|
214
|
+
}> {
|
|
215
|
+
return this.serialize(async () => {
|
|
216
|
+
const state = await this.readState()
|
|
217
|
+
if (state.archivedSessionIds.includes(sessionId)) {
|
|
218
|
+
return { archivedSessionIds: [...state.archivedSessionIds], changed: false }
|
|
219
|
+
}
|
|
220
|
+
await requireSession()
|
|
221
|
+
const archivedSessionIds = [...state.archivedSessionIds, sessionId]
|
|
222
|
+
await this.storage.put(WORKSPACE_DOMAIN_STATE_KEY, {
|
|
223
|
+
...state,
|
|
224
|
+
archivedSessionIds,
|
|
225
|
+
} satisfies EdgeWorkspaceDomainState)
|
|
226
|
+
return { archivedSessionIds, changed: true }
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async requireWorkspace(workspaceId: WorkspaceId): Promise<EdgeWorkspaceDomainState> {
|
|
231
|
+
const state = await this.readState()
|
|
232
|
+
if (workspaceId !== EDGE_WORKSPACE_ID || !state.workspaceIds.includes(workspaceId)) {
|
|
233
|
+
throw new EdgeWorkspaceStoreError(
|
|
234
|
+
'NOT_FOUND',
|
|
235
|
+
`Workspace "${workspaceId}" is not available in this Edge instance.`,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
return state
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async readOrCreateRecord(
|
|
242
|
+
sessions: readonly EdgeApiSessionSummary[],
|
|
243
|
+
): Promise<EdgeWorkspaceRecord> {
|
|
244
|
+
const stored = await this.storage.get(WORKSPACE_RECORD_KEY)
|
|
245
|
+
if (stored !== undefined) return workspaceRecordFromStorage(stored)
|
|
246
|
+
const record = defaultWorkspaceRecord(sessions)
|
|
247
|
+
await this.storage.put(WORKSPACE_RECORD_KEY, record)
|
|
248
|
+
return record
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private async readState(): Promise<EdgeWorkspaceDomainState> {
|
|
252
|
+
const stored = await this.storage.get(WORKSPACE_DOMAIN_STATE_KEY)
|
|
253
|
+
if (stored === undefined) {
|
|
254
|
+
return {
|
|
255
|
+
initialized: true,
|
|
256
|
+
workspaceIds: [EDGE_WORKSPACE_ID],
|
|
257
|
+
archivedSessionIds: [],
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return workspaceDomainStateFromStorage(stored)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private serialize<T>(operation: () => Promise<T>): Promise<T> {
|
|
264
|
+
const result = this.operationTail.then(operation)
|
|
265
|
+
this.operationTail = result.then(() => undefined, () => undefined)
|
|
266
|
+
return result
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function defaultWorkspaceRecord(sessions: readonly EdgeApiSessionSummary[]): EdgeWorkspaceRecord {
|
|
271
|
+
const createdAt = sessions.length === 0
|
|
272
|
+
? new Date(0).toISOString()
|
|
273
|
+
: new Date(Math.min(...sessions.map(session => session.createdAt))).toISOString()
|
|
274
|
+
const updatedAt = sessions.length === 0
|
|
275
|
+
? createdAt
|
|
276
|
+
: new Date(Math.max(...sessions.map(session => session.updatedAt))).toISOString()
|
|
277
|
+
return {
|
|
278
|
+
path: EDGE_WORKSPACE_PATH,
|
|
279
|
+
title: 'Workspace',
|
|
280
|
+
sessionIds: sessions.map(session => session.id),
|
|
281
|
+
createdAt,
|
|
282
|
+
updatedAt,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function workspaceView(record: EdgeWorkspaceRecord): WorkspaceView {
|
|
287
|
+
return { workspaceId: EDGE_WORKSPACE_ID, ...record, sessionIds: [...record.sessionIds] }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function sameIds(left: readonly SessionId[], right: readonly SessionId[]): boolean {
|
|
291
|
+
return left.length === right.length && left.every((id, index) => id === right[index])
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function workspaceDomainStateFromStorage(stored: unknown): EdgeWorkspaceDomainState {
|
|
295
|
+
if (typeof stored !== 'object' || stored === null || Array.isArray(stored)) {
|
|
296
|
+
throw new Error('dsh-edge workspace domain state is invalid.')
|
|
297
|
+
}
|
|
298
|
+
const value = stored as Record<string, unknown>
|
|
299
|
+
const workspaceIds = value.workspaceIds
|
|
300
|
+
const archivedSessionIds = value.archivedSessionIds
|
|
301
|
+
if (value.initialized !== true
|
|
302
|
+
|| !Array.isArray(workspaceIds)
|
|
303
|
+
|| workspaceIds.some(id => id !== EDGE_WORKSPACE_ID)
|
|
304
|
+
|| new Set(workspaceIds).size !== workspaceIds.length
|
|
305
|
+
|| !Array.isArray(archivedSessionIds)
|
|
306
|
+
|| archivedSessionIds.some(id => typeof id !== 'string' || id.length === 0)
|
|
307
|
+
|| new Set(archivedSessionIds).size !== archivedSessionIds.length
|
|
308
|
+
|| Object.hasOwn(value, 'pendingMutation')) {
|
|
309
|
+
throw new Error('dsh-edge workspace domain state is invalid.')
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
initialized: true,
|
|
313
|
+
workspaceIds: workspaceIds.map(() => EDGE_WORKSPACE_ID),
|
|
314
|
+
archivedSessionIds: archivedSessionIds as SessionId[],
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function workspaceRecordFromStorage(stored: unknown): EdgeWorkspaceRecord {
|
|
319
|
+
if (typeof stored !== 'object' || stored === null || Array.isArray(stored)) {
|
|
320
|
+
throw new Error('dsh-edge workspace record is invalid.')
|
|
321
|
+
}
|
|
322
|
+
const value = stored as Record<string, unknown>
|
|
323
|
+
if (value.path !== EDGE_WORKSPACE_PATH
|
|
324
|
+
|| typeof value.title !== 'string'
|
|
325
|
+
|| !Array.isArray(value.sessionIds)
|
|
326
|
+
|| value.sessionIds.some(id => typeof id !== 'string' || id.length === 0)
|
|
327
|
+
|| new Set(value.sessionIds).size !== value.sessionIds.length
|
|
328
|
+
|| typeof value.createdAt !== 'string'
|
|
329
|
+
|| typeof value.updatedAt !== 'string') {
|
|
330
|
+
throw new Error('dsh-edge workspace record is invalid.')
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
path: EDGE_WORKSPACE_PATH,
|
|
334
|
+
title: value.title,
|
|
335
|
+
sessionIds: value.sessionIds as SessionId[],
|
|
336
|
+
createdAt: value.createdAt,
|
|
337
|
+
updatedAt: value.updatedAt,
|
|
338
|
+
}
|
|
339
|
+
}
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/** Shared HTTP parsing, response, and error translation for dsh-edge routes. */
|
|
2
|
+
|
|
3
|
+
import { assertUsableApiKey, LlmError } from '@deepseek-ai/dsh-llm'
|
|
4
|
+
import { EdgeSessionStoreError } from './session-store.ts'
|
|
5
|
+
import { EdgeWorkspaceRequestError } from './workspace.ts'
|
|
6
|
+
|
|
7
|
+
const textEncoder = new TextEncoder()
|
|
8
|
+
|
|
9
|
+
export const MAX_SESSION_CREATE_BODY_BYTES = 8_192
|
|
10
|
+
export const MAX_TURN_BODY_BYTES = 524_288
|
|
11
|
+
export const MAX_WORKSPACE_EXEC_BODY_BYTES = 131_072
|
|
12
|
+
|
|
13
|
+
/** Invalid caller input with an explicit HTTP response status. */
|
|
14
|
+
export class EdgeHttpError extends Error {
|
|
15
|
+
constructor(readonly status: number, message: string) {
|
|
16
|
+
super(message)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Consume a request body without retaining bytes beyond its route limit. */
|
|
21
|
+
export async function readBoundedBody(
|
|
22
|
+
request: Request,
|
|
23
|
+
maxBytes: number,
|
|
24
|
+
message: string,
|
|
25
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
26
|
+
const declared = request.headers.get('content-length')
|
|
27
|
+
let exceeded = declared !== null && /^\d+$/.test(declared) && Number(declared) > maxBytes
|
|
28
|
+
if (request.body === null) return new Uint8Array()
|
|
29
|
+
|
|
30
|
+
const chunks: Uint8Array[] = []
|
|
31
|
+
let byteLength = 0
|
|
32
|
+
const stream = request.body as ReadableStream<Uint8Array>
|
|
33
|
+
const reader = stream.getReader()
|
|
34
|
+
try {
|
|
35
|
+
while (true) {
|
|
36
|
+
const next = await reader.read()
|
|
37
|
+
if (next.done) break
|
|
38
|
+
byteLength += next.value.byteLength
|
|
39
|
+
if (exceeded || byteLength > maxBytes) {
|
|
40
|
+
exceeded = true
|
|
41
|
+
chunks.length = 0
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
chunks.push(next.value)
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
reader.releaseLock()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (exceeded) throw new EdgeHttpError(413, message)
|
|
51
|
+
|
|
52
|
+
const body = new Uint8Array(byteLength)
|
|
53
|
+
let offset = 0
|
|
54
|
+
for (const chunk of chunks) {
|
|
55
|
+
body.set(chunk, offset)
|
|
56
|
+
offset += chunk.byteLength
|
|
57
|
+
}
|
|
58
|
+
return body
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Consume one bounded UTF-8 request body. */
|
|
62
|
+
export async function readBoundedText(
|
|
63
|
+
request: Request,
|
|
64
|
+
maxBytes: number,
|
|
65
|
+
message: string,
|
|
66
|
+
): Promise<string> {
|
|
67
|
+
const body = await readBoundedBody(request, maxBytes, message)
|
|
68
|
+
try {
|
|
69
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(body)
|
|
70
|
+
} catch {
|
|
71
|
+
throw new EdgeHttpError(400, 'The request body must contain valid UTF-8.')
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Parse a size-bounded JSON object request body. */
|
|
76
|
+
export async function readJsonObject(
|
|
77
|
+
request: Request,
|
|
78
|
+
maxBytes: number,
|
|
79
|
+
): Promise<Record<string, unknown>> {
|
|
80
|
+
try {
|
|
81
|
+
const source = await readBoundedText(
|
|
82
|
+
request,
|
|
83
|
+
maxBytes,
|
|
84
|
+
`JSON request bodies are limited to ${maxBytes} bytes.`,
|
|
85
|
+
)
|
|
86
|
+
const body: unknown = JSON.parse(source)
|
|
87
|
+
if (!isRecord(body)) {
|
|
88
|
+
throw new EdgeHttpError(400, 'The request body must be a JSON object.')
|
|
89
|
+
}
|
|
90
|
+
return body
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof EdgeHttpError) throw error
|
|
93
|
+
throw new EdgeHttpError(400, 'The request body must contain valid JSON.')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Release an unread request body before returning an early Worker response. */
|
|
98
|
+
export async function discardUnreadRequestBody(request: Request): Promise<void> {
|
|
99
|
+
if (request.body === null || request.bodyUsed) return
|
|
100
|
+
await request.body.cancel().catch(() => undefined)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Validate a non-empty bounded string field from an HTTP request. */
|
|
104
|
+
export function requireBoundedString(
|
|
105
|
+
value: unknown,
|
|
106
|
+
field: string,
|
|
107
|
+
maxLength: number,
|
|
108
|
+
): string {
|
|
109
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
110
|
+
throw new EdgeHttpError(400, `${field} must be a non-empty string.`)
|
|
111
|
+
}
|
|
112
|
+
if (value.length > maxLength) {
|
|
113
|
+
throw new EdgeHttpError(413, `${field} is limited to ${maxLength} characters.`)
|
|
114
|
+
}
|
|
115
|
+
return value
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Validate a non-empty string field whose wire limit is defined in UTF-8 bytes. */
|
|
119
|
+
export function requireBoundedUtf8String(
|
|
120
|
+
value: unknown,
|
|
121
|
+
field: string,
|
|
122
|
+
maxBytes: number,
|
|
123
|
+
): string {
|
|
124
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
125
|
+
throw new EdgeHttpError(400, `${field} must be a non-empty string.`)
|
|
126
|
+
}
|
|
127
|
+
if (textEncoder.encode(value).byteLength > maxBytes) {
|
|
128
|
+
throw new EdgeHttpError(413, `${field} is limited to ${maxBytes} UTF-8 bytes.`)
|
|
129
|
+
}
|
|
130
|
+
return value
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Resolve the deployment-scoped DeepSeek Worker secret. */
|
|
134
|
+
export function resolveDeepSeekApiKey(configuredKey?: string): string {
|
|
135
|
+
const apiKey = configuredKey
|
|
136
|
+
if (apiKey === undefined) {
|
|
137
|
+
throw new EdgeHttpError(
|
|
138
|
+
401,
|
|
139
|
+
'Configure the DEEPSEEK_API_KEY Worker secret.',
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
return assertUsableApiKey(
|
|
143
|
+
apiKey,
|
|
144
|
+
'dsh-edge',
|
|
145
|
+
'DEEPSEEK_API_KEY',
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Build one JSON response with the development CORS policy. */
|
|
150
|
+
export function jsonResponse(value: unknown, status = 200): Response {
|
|
151
|
+
return Response.json(value, { status, headers: corsHeaders() })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Development-only CORS headers shared by every route. */
|
|
155
|
+
export function corsHeaders(): Record<string, string> {
|
|
156
|
+
return {
|
|
157
|
+
'access-control-allow-headers': 'content-type, last-event-id',
|
|
158
|
+
'access-control-allow-methods': 'GET, PUT, POST, DELETE, OPTIONS',
|
|
159
|
+
'access-control-allow-origin': '*',
|
|
160
|
+
'access-control-expose-headers': 'x-dsh-edge-has-more, x-dsh-edge-next-after',
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Translate known runtime failures without exposing stacks or credentials. */
|
|
165
|
+
export function errorResponse(error: unknown): Response {
|
|
166
|
+
if (error instanceof EdgeHttpError || error instanceof EdgeWorkspaceRequestError) {
|
|
167
|
+
return jsonResponse({ ok: false, error: error.message }, error.status)
|
|
168
|
+
}
|
|
169
|
+
if (error instanceof EdgeSessionStoreError) {
|
|
170
|
+
const status = error.code === 'NOT_FOUND' ? 404 : error.code === 'BUSY' ? 409 : 500
|
|
171
|
+
return jsonResponse({ ok: false, error: error.message, code: error.code }, status)
|
|
172
|
+
}
|
|
173
|
+
if (error instanceof LlmError) {
|
|
174
|
+
return jsonResponse({
|
|
175
|
+
ok: false,
|
|
176
|
+
error: error.message,
|
|
177
|
+
code: error.code,
|
|
178
|
+
...error.failure.status === undefined
|
|
179
|
+
? {}
|
|
180
|
+
: { providerStatus: error.failure.status },
|
|
181
|
+
...error.failure.requestId === undefined
|
|
182
|
+
? {}
|
|
183
|
+
: { providerRequestId: error.failure.requestId },
|
|
184
|
+
}, llmHttpStatus(error.code))
|
|
185
|
+
}
|
|
186
|
+
if (isErrorCode(error, 'ENOENT')) {
|
|
187
|
+
return jsonResponse({ ok: false, error: 'Workspace entry not found.' }, 404)
|
|
188
|
+
}
|
|
189
|
+
console.error(error)
|
|
190
|
+
return jsonResponse({ ok: false, error: 'Internal runtime error.' }, 500)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function llmHttpStatus(code: string): number {
|
|
194
|
+
if (code === 'AUTH' || code === 'MISSING_CREDENTIAL') return 401
|
|
195
|
+
if (code === 'INVALID_CREDENTIAL'
|
|
196
|
+
|| code === 'INVALID_REQUEST'
|
|
197
|
+
|| code === 'CONTEXT_WINDOW_EXCEEDED') return 400
|
|
198
|
+
if (code === 'RATE_LIMIT' || code === 'QUOTA') return 429
|
|
199
|
+
if (code === 'TIMEOUT') return 504
|
|
200
|
+
return 502
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
204
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isErrorCode(error: unknown, code: string): boolean {
|
|
208
|
+
return typeof error === 'object'
|
|
209
|
+
&& error !== null
|
|
210
|
+
&& 'code' in error
|
|
211
|
+
&& error.code === code
|
|
212
|
+
}
|