@covro/covro 0.1.1-dev.1 → 0.1.1-dev.3
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 +2 -1
- package/README.zh-CN.md +2 -1
- package/dist/codex/adapter.js +41 -2
- package/dist/codex/adapter.js.map +2 -2
- package/dist/codex/transport.js +2 -2
- package/dist/codex/transport.js.map +2 -2
- package/dist/server/http.js +30 -0
- package/dist/server/http.js.map +2 -2
- package/dist/server/json-files.js +27 -0
- package/dist/server/json-files.js.map +7 -0
- package/dist/server/local-files.js.map +1 -1
- package/dist/server/markdown.js.map +2 -2
- package/dist/server/service.js +24 -1
- package/dist/server/service.js.map +2 -2
- package/dist/server/shell.js +128 -0
- package/dist/server/shell.js.map +7 -0
- package/dist/shared/shell.js +15 -0
- package/dist/shared/shell.js.map +7 -0
- package/dist/web/assets/index-CMa1knJw.js +138 -0
- package/dist/web/assets/index-Dr0Qkixk.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -3
- package/dist/web/assets/index-Db6eo_Kb.js +0 -83
- package/dist/web/assets/index-L3S1ZiO0.css +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/codex/transport.ts"],
|
|
4
|
-
"sourcesContent": ["import { EventEmitter } from 'node:events';\nimport { AppError } from '../shared/errors.js';\nimport type { Readable, Writable } from 'node:stream';\n\nexport type RpcId = string | number;\nexport type RpcNotification = { method: string; params?: unknown };\nexport type RpcServerRequest = RpcNotification & { id: RpcId };\nexport const DEFAULT_MAX_LINE_BYTES = 64 * 1024 * 1024;\nexport class CodexError extends AppError {\n constructor(\n code: string,\n message: string,\n public readonly uncertain = false,\n public readonly rpcCode?: number,\n diagnostics?: Record<string, unknown>,\n ) {\n super(code, message, 503, { uncertain, rpcCode, ...diagnostics });\n this.name = 'CodexError';\n }\n}\n\n/** Buffers at most one bounded line. Decoding the complete byte sequence preserves split UTF-8. */\nexport class JsonLineReader {\n private parts: Buffer[] = [];\n private size = 0;\n private readonly decoder = new TextDecoder('utf-8', { fatal: true });\n constructor(\n private readonly consume: (value: unknown) => void,\n private readonly maxBytes = DEFAULT_MAX_LINE_BYTES,\n ) {}\n push(chunk: Buffer): void {\n let start = 0;\n while (start < chunk.length) {\n const newline = chunk.indexOf(10, start);\n const end = newline < 0 ? chunk.length : newline;\n const part = chunk.subarray(start, end);\n this.size += part.length;\n if (this.size > this.maxBytes) {\n const receivedBytes = this.size;\n this.parts = [];\n this.size = 0;\n throw new CodexError(\n 'PROTOCOL_ERROR',\n `Codex JSONL message exceeds the configured limit (received at least ${receivedBytes} bytes; limit ${this.maxBytes} bytes)`,\n true,\n undefined,\n { receivedBytes, maxLineBytes: this.maxBytes },\n );\n }\n if (part.length) this.parts.push(part);\n if (newline < 0) break;\n this.flush();\n start = newline + 1;\n }\n }\n finish(): void {\n if (this.size) this.flush();\n }\n private flush(): void {\n const bytes = Buffer.concat(this.parts, this.size);\n this.parts = [];\n this.size = 0;\n try {\n const line = this.decoder.decode(bytes).trim();\n if (line) this.consume(JSON.parse(line) as unknown);\n } catch (error) {\n if (error instanceof CodexError) throw error;\n throw new CodexError('PROTOCOL_ERROR', 'Invalid UTF-8 or JSON from Codex', true);\n }\n }\n}\n\ntype Pending = {\n method: string;\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n timer: NodeJS.Timeout;\n};\nexport type TransportOptions = {\n timeoutMs?: number;\n maxLineBytes?: number;\n maxPending?: number;\n maxWriteBytes?: number;\n};\n\n/** RPC receipt deadlines do not cancel upstream turns. A timeout always reports an uncertain outcome. */\nexport class RpcTransport extends EventEmitter {\n private readonly pending = new Map<RpcId, Pending>();\n private nextId = 0;\n private failure?: CodexError;\n private readonly reader: JsonLineReader;\n constructor(\n private readonly input: Writable,\n output: Readable,\n private readonly options: TransportOptions = {},\n ) {\n super();\n this.reader = new JsonLineReader((value) => this.receive(value), options.maxLineBytes);\n output.on('data', (chunk: Buffer) => {\n if (this.failure) return;\n try {\n this.reader.push(chunk);\n } catch (error) {\n this.fail(asCodexError(error));\n }\n });\n output.on('end', () => {\n if (this.failure) return;\n try {\n this.reader.finish();\n } catch (error) {\n this.fail(asCodexError(error));\n }\n this.fail(new CodexError('CODEX_UNAVAILABLE', 'Codex stdout reached EOF', true));\n });\n output.on('close', () =>\n this.fail(new CodexError('CODEX_UNAVAILABLE', 'Codex stdout closed', true)),\n );\n output.on('error', (error) =>\n this.fail(new CodexError('CODEX_UNAVAILABLE', error.message, true)),\n );\n input.on('error', (error) =>\n this.fail(new CodexError('CODEX_UNAVAILABLE', error.message, true)),\n );\n }\n request<T>(method: string
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { EventEmitter } from 'node:events';\nimport { AppError } from '../shared/errors.js';\nimport type { Readable, Writable } from 'node:stream';\n\nexport type RpcId = string | number;\nexport type RpcNotification = { method: string; params?: unknown };\nexport type RpcServerRequest = RpcNotification & { id: RpcId };\nexport const DEFAULT_MAX_LINE_BYTES = 64 * 1024 * 1024;\nexport class CodexError extends AppError {\n constructor(\n code: string,\n message: string,\n public readonly uncertain = false,\n public readonly rpcCode?: number,\n diagnostics?: Record<string, unknown>,\n ) {\n super(code, message, 503, { uncertain, rpcCode, ...diagnostics });\n this.name = 'CodexError';\n }\n}\n\n/** Buffers at most one bounded line. Decoding the complete byte sequence preserves split UTF-8. */\nexport class JsonLineReader {\n private parts: Buffer[] = [];\n private size = 0;\n private readonly decoder = new TextDecoder('utf-8', { fatal: true });\n constructor(\n private readonly consume: (value: unknown) => void,\n private readonly maxBytes = DEFAULT_MAX_LINE_BYTES,\n ) {}\n push(chunk: Buffer): void {\n let start = 0;\n while (start < chunk.length) {\n const newline = chunk.indexOf(10, start);\n const end = newline < 0 ? chunk.length : newline;\n const part = chunk.subarray(start, end);\n this.size += part.length;\n if (this.size > this.maxBytes) {\n const receivedBytes = this.size;\n this.parts = [];\n this.size = 0;\n throw new CodexError(\n 'PROTOCOL_ERROR',\n `Codex JSONL message exceeds the configured limit (received at least ${receivedBytes} bytes; limit ${this.maxBytes} bytes)`,\n true,\n undefined,\n { receivedBytes, maxLineBytes: this.maxBytes },\n );\n }\n if (part.length) this.parts.push(part);\n if (newline < 0) break;\n this.flush();\n start = newline + 1;\n }\n }\n finish(): void {\n if (this.size) this.flush();\n }\n private flush(): void {\n const bytes = Buffer.concat(this.parts, this.size);\n this.parts = [];\n this.size = 0;\n try {\n const line = this.decoder.decode(bytes).trim();\n if (line) this.consume(JSON.parse(line) as unknown);\n } catch (error) {\n if (error instanceof CodexError) throw error;\n throw new CodexError('PROTOCOL_ERROR', 'Invalid UTF-8 or JSON from Codex', true);\n }\n }\n}\n\ntype Pending = {\n method: string;\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n timer: NodeJS.Timeout;\n};\nexport type TransportOptions = {\n timeoutMs?: number;\n maxLineBytes?: number;\n maxPending?: number;\n maxWriteBytes?: number;\n};\n\n/** RPC receipt deadlines do not cancel upstream turns. A timeout always reports an uncertain outcome. */\nexport class RpcTransport extends EventEmitter {\n private readonly pending = new Map<RpcId, Pending>();\n private nextId = 0;\n private failure?: CodexError;\n private readonly reader: JsonLineReader;\n constructor(\n private readonly input: Writable,\n output: Readable,\n private readonly options: TransportOptions = {},\n ) {\n super();\n this.reader = new JsonLineReader((value) => this.receive(value), options.maxLineBytes);\n output.on('data', (chunk: Buffer) => {\n if (this.failure) return;\n try {\n this.reader.push(chunk);\n } catch (error) {\n this.fail(asCodexError(error));\n }\n });\n output.on('end', () => {\n if (this.failure) return;\n try {\n this.reader.finish();\n } catch (error) {\n this.fail(asCodexError(error));\n }\n this.fail(new CodexError('CODEX_UNAVAILABLE', 'Codex stdout reached EOF', true));\n });\n output.on('close', () =>\n this.fail(new CodexError('CODEX_UNAVAILABLE', 'Codex stdout closed', true)),\n );\n output.on('error', (error) =>\n this.fail(new CodexError('CODEX_UNAVAILABLE', error.message, true)),\n );\n input.on('error', (error) =>\n this.fail(new CodexError('CODEX_UNAVAILABLE', error.message, true)),\n );\n }\n request<T>(\n method: string,\n params: unknown,\n id: RpcId = this.nextId++,\n timeoutMs = this.options.timeoutMs ?? 30_000,\n ): Promise<T> {\n if (this.failure) return Promise.reject(this.failure);\n if (this.pending.has(id))\n return Promise.reject(new CodexError('PROTOCOL_ERROR', 'Duplicate outgoing RPC id'));\n if (this.pending.size >= (this.options.maxPending ?? 256))\n return Promise.reject(new CodexError('CODEX_BUSY', 'Too many pending Codex requests'));\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pending.delete(id);\n reject(\n new CodexError(\n 'RESULT_UNKNOWN',\n `Codex ${method} acknowledgement timed out; it may have executed`,\n true,\n ),\n );\n }, timeoutMs);\n this.pending.set(id, { method, resolve: (result) => resolve(result as T), reject, timer });\n void this.write({ id, method, params }).catch((error: unknown) => {\n const pending = this.pending.get(id);\n if (!pending) return;\n clearTimeout(pending.timer);\n this.pending.delete(id);\n pending.reject(asCodexError(error));\n });\n });\n }\n notify(method: string, params?: unknown): Promise<void> {\n return this.write({ method, ...(params === undefined ? {} : { params }) });\n }\n respond(id: RpcId, result: unknown): Promise<void> {\n return this.write({ id, result });\n }\n reject(id: RpcId, code: number, message: string): Promise<void> {\n return this.write({ id, error: { code, message } });\n }\n fail(error: CodexError): void {\n if (this.failure) return;\n error = new CodexError(error.code, error.message, error.uncertain, error.rpcCode, {\n ...(isRecord(error.details) ? error.details : {}),\n pendingRequests: [...this.pending].map(([id, pending]) => ({ id, method: pending.method })),\n });\n this.failure = error;\n for (const pending of this.pending.values()) {\n clearTimeout(pending.timer);\n pending.reject(error);\n }\n this.pending.clear();\n this.emit('failure', error);\n }\n private async write(message: unknown): Promise<void> {\n if (this.failure) throw this.failure;\n const data = JSON.stringify(message) + '\\n';\n if (this.input.destroyed || !this.input.writable)\n throw new CodexError('CODEX_UNAVAILABLE', 'Codex stdin is closed', true);\n if (\n this.input.writableLength + Buffer.byteLength(data) >\n (this.options.maxWriteBytes ?? 4 * 1024 * 1024)\n ) {\n throw new CodexError('CODEX_BUSY', 'Codex stdin buffer limit reached');\n }\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n const error = new CodexError('RESULT_UNKNOWN', 'Codex stdin write timed out', true);\n this.fail(error);\n reject(error);\n }, this.options.timeoutMs ?? 30_000);\n this.input.write(data, (error) => {\n clearTimeout(timer);\n if (error) reject(new CodexError('CODEX_UNAVAILABLE', error.message, true));\n else resolve();\n });\n });\n }\n private receive(value: unknown): void {\n if (!isRecord(value))\n throw new CodexError('PROTOCOL_ERROR', 'Codex sent a non-object RPC message', true);\n const hasId = Object.hasOwn(value, 'id');\n if (hasId && !isRpcId(value.id))\n throw new CodexError('PROTOCOL_ERROR', 'Codex sent an invalid RPC id', true);\n if (typeof value.method === 'string') {\n if (Object.hasOwn(value, 'result') || Object.hasOwn(value, 'error'))\n throw new CodexError('PROTOCOL_ERROR', 'Codex mixed a method and RPC result', true);\n if (hasId) this.emit('request', value as RpcServerRequest);\n else this.emit('notification', value as RpcNotification);\n return;\n }\n if (!hasId || Object.hasOwn(value, 'result') === Object.hasOwn(value, 'error'))\n throw new CodexError('PROTOCOL_ERROR', 'Unrecognized Codex RPC message', true);\n const pending = this.pending.get(value.id as RpcId);\n if (!pending) return; // Late receipts after a timeout must never cause a prompt to be resent.\n this.pending.delete(value.id as RpcId);\n clearTimeout(pending.timer);\n if (Object.hasOwn(value, 'error')) {\n if (\n !isRecord(value.error) ||\n typeof value.error.message !== 'string' ||\n typeof value.error.code !== 'number'\n ) {\n const error = new CodexError('PROTOCOL_ERROR', 'Malformed Codex RPC error', true);\n pending.reject(error);\n throw error;\n }\n pending.reject(\n new CodexError(\n value.error.code === -32601 ? 'PROTOCOL_INCOMPATIBLE' : 'CODEX_REQUEST_FAILED',\n value.error.message,\n false,\n value.error.code,\n ),\n );\n } else pending.resolve(value.result);\n }\n}\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\nexport function isRpcId(value: unknown): value is RpcId {\n return typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value));\n}\nexport function asCodexError(error: unknown): CodexError {\n return error instanceof CodexError\n ? error\n : new CodexError(\n 'CODEX_UNAVAILABLE',\n error instanceof Error ? error.message : String(error),\n true,\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/server/http.js
CHANGED
|
@@ -12,6 +12,7 @@ import { socketHandlers } from "./websocket.js";
|
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { releaseChannels } from "../shared/updates.js";
|
|
14
14
|
import { settingsSchema, updateRequestSchema } from "../updates/manager.js";
|
|
15
|
+
import { startShellSchema } from "../shared/shell.js";
|
|
15
16
|
function createApp({
|
|
16
17
|
config,
|
|
17
18
|
service,
|
|
@@ -83,6 +84,29 @@ function createApp({
|
|
|
83
84
|
return c.json(await configuration.export());
|
|
84
85
|
});
|
|
85
86
|
app.get("/api/system", (c) => c.json(service.getSystem()));
|
|
87
|
+
app.get(
|
|
88
|
+
"/api/shell",
|
|
89
|
+
(c) => c.json(
|
|
90
|
+
service.shell.snapshot(
|
|
91
|
+
c.req.query("epoch"),
|
|
92
|
+
z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0).parse(c.req.query("after"))
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
app.post("/api/projects/:projectId/shell", async (c) => {
|
|
97
|
+
const op = await service.startShell(
|
|
98
|
+
id(c.req.param("projectId")),
|
|
99
|
+
await parse(c, startShellSchema)
|
|
100
|
+
);
|
|
101
|
+
return c.json(op, status(op));
|
|
102
|
+
});
|
|
103
|
+
app.post("/api/shell/:commandId/interrupt", async (c) => {
|
|
104
|
+
const op = await service.interruptShell(
|
|
105
|
+
id(c.req.param("commandId")),
|
|
106
|
+
await parse(c, schemas.operationSchema)
|
|
107
|
+
);
|
|
108
|
+
return c.json(op, status(op));
|
|
109
|
+
});
|
|
86
110
|
app.post("/api/system/compatibility/confirm", async (c) => {
|
|
87
111
|
const op = await service.confirmCompatibility(
|
|
88
112
|
await parse(c, schemas.confirmCompatibilitySchema)
|
|
@@ -182,6 +206,12 @@ function createApp({
|
|
|
182
206
|
await service.getThreadMarkdown(id(c.req.param("threadId")), c.req.query("path"))
|
|
183
207
|
);
|
|
184
208
|
});
|
|
209
|
+
app.get("/api/threads/:threadId/json", async (c) => {
|
|
210
|
+
c.header("Cache-Control", "private, no-store");
|
|
211
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
212
|
+
c.header("Cross-Origin-Resource-Policy", "same-origin");
|
|
213
|
+
return c.json(await service.getThreadJson(id(c.req.param("threadId")), c.req.query("path")));
|
|
214
|
+
});
|
|
185
215
|
app.get(
|
|
186
216
|
"/api/operations/:operationId",
|
|
187
217
|
(c) => c.json(service.getOperation(id(c.req.param("operationId"))))
|
package/dist/server/http.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/server/http.ts"],
|
|
4
|
-
"sourcesContent": ["import { Hono } from 'hono';\nimport { bodyLimit } from 'hono/body-limit';\nimport { serveStatic } from '@hono/node-server/serve-static';\nimport { upgradeWebSocket, type HttpBindings } from '@hono/node-server';\nimport type { ZodType } from 'zod';\nimport { ZodError } from 'zod';\nimport { AppError } from '../shared/errors.js';\nimport { IMAGE_REQUEST_BYTES } from '../shared/images.js';\nimport * as schemas from '../shared/validation.js';\nimport type { Operation } from '../shared/types.js';\nimport type { AppConfig } from './config.js';\nimport { ConfigStore } from './config-store.js';\nimport { createAuthenticator, type Authenticator, type Identity } from './auth.js';\nimport type { GatewayService } from './service.js';\nimport { socketHandlers } from './websocket.js';\nimport { z } from 'zod';\nimport { releaseChannels, type UpdateController } from '../shared/updates.js';\nimport { settingsSchema, updateRequestSchema } from '../updates/manager.js';\nexport interface HttpDependencies {\n config: AppConfig;\n service: GatewayService;\n staticDir: string;\n authenticate?: Authenticator;\n updates?: UpdateController;\n}\nexport function createApp({\n config,\n service,\n staticDir,\n authenticate = createAuthenticator(config),\n updates,\n}: HttpDependencies) {\n const app = new Hono<{\n Bindings: HttpBindings;\n Variables: { identity: Identity; operationId?: string };\n }>();\n app.onError((error, c) => {\n const failure =\n error instanceof AppError\n ? error\n : error instanceof ZodError || error instanceof SyntaxError\n ? new AppError('VALIDATION_FAILED', 'Invalid request data', 400)\n : new AppError('INTERNAL_ERROR', 'An internal error occurred', 500);\n return c.json({ error: failure.toJSON(c.get('operationId')) }, failure.status as 400);\n });\n app.get('/api/health', (c) => c.json({ ok: true }));\n app.use('*', async (c, next) => {\n c.set('identity', await authenticate(c.req.raw, c.req.path === '/ws'));\n await next();\n });\n app.use('/api/*', (c, next) =>\n bodyLimit({\n maxSize: /^\\/api\\/threads\\/[^/]+\\/(turns|steer)$/.test(c.req.path)\n ? IMAGE_REQUEST_BYTES\n : 512 * 1024,\n onError: () => {\n throw new AppError('BODY_TOO_LARGE', 'Request body exceeds limit', 413);\n },\n })(c, next),\n );\n app.use('/api/*', async (c, next) => {\n c.header('Cache-Control', 'no-store');\n if (\n service.isInMaintenance() &&\n !['GET', 'HEAD', 'OPTIONS'].includes(c.req.method) &&\n !c.req.path.startsWith('/api/updates')\n )\n throw new AppError('UPDATE_MAINTENANCE', '\u6B63\u5728\u5347\u7EA7\uFF0C\u8BF7\u7B49\u5F85\u670D\u52A1\u91CD\u65B0\u8FDE\u63A5', 503);\n await next();\n });\n if (updates) {\n app.get('/api/updates', async (c) => c.json(await updates.status()));\n app.post('/api/updates/check', async (c) => {\n const input = z\n .object({ channel: z.enum(releaseChannels).optional() })\n .strict()\n .parse(await c.req.json());\n return c.json(await updates.check(input.channel));\n });\n app.post('/api/updates/settings', async (c) =>\n c.json(await updates.configure(settingsSchema.parse(await c.req.json()))),\n );\n app.post('/api/updates/run', async (c) =>\n c.json(await updates.start(updateRequestSchema.parse(await c.req.json())), 202),\n );\n }\n const id = (value: string) => schemas.idSchema.parse(value);\n const parse = async <T>(\n c: { req: { json: () => Promise<unknown> }; set: (key: 'operationId', value: string) => void },\n schema: ZodType<T>,\n ): Promise<T> => {\n const value: unknown = await c.req.json();\n if (\n value &&\n typeof value === 'object' &&\n 'clientOperationId' in value &&\n typeof value.clientOperationId === 'string'\n )\n c.set('operationId', value.clientOperationId);\n return schema.parse(value);\n };\n const status = (op: Operation) =>\n op.status === 'dispatching' || op.status === 'accepted' ? (202 as const) : (200 as const);\n const configuration = new ConfigStore(config);\n app.use('/api/config*', async (c, next) => {\n c.header('Cache-Control', 'no-store');\n await next();\n });\n app.get('/api/config', async (c) => c.json(await configuration.snapshot()));\n app.post('/api/config', async (c) => c.json(await configuration.save(await c.req.json())));\n app.post('/api/config/import', async (c) =>\n c.json(await configuration.previewImport(await c.req.json())),\n );\n app.get('/api/config/export', async (c) => {\n c.header('Content-Disposition', 'attachment; filename=\"codex-web-config.json\"');\n return c.json(await configuration.export());\n });\n app.get('/api/system', (c) => c.json(service.getSystem()));\n app.post('/api/system/compatibility/confirm', async (c) => {\n const op = await service.confirmCompatibility(\n await parse(c, schemas.confirmCompatibilitySchema),\n );\n return c.json(op, status(op));\n });\n app.get('/api/projects', async (c) => c.json({ data: await service.listProjects() }));\n app.get('/api/projects/:projectId/runtime-settings', async (c) =>\n c.json(await service.readRuntimeSettings(id(c.req.param('projectId')))),\n );\n app.post('/api/projects/:projectId/model-defaults', async (c) => {\n const op = await service.saveModelDefaults(\n id(c.req.param('projectId')),\n await parse(c, schemas.saveModelDefaultsSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/projects', async (c) =>\n c.json(service.addProject(await parse(c, schemas.createProjectSchema))),\n );\n app.post('/api/projects/:projectId/rename', async (c) =>\n c.json(\n service.renameProject(\n id(c.req.param('projectId')),\n await parse(c, schemas.renameProjectSchema),\n ),\n ),\n );\n app.post('/api/projects/:projectId/remove', async (c) => {\n await parse(c, schemas.removeProjectSchema);\n return c.json(service.removeProject(id(c.req.param('projectId'))));\n });\n app.get('/api/projects/:projectId/threads', async (c) =>\n c.json(\n await service.listThreadSummaries(\n id(c.req.param('projectId')),\n c.req.query('cursor') === undefined\n ? undefined\n : schemas.cursorSchema.parse(c.req.query('cursor')),\n schemas.archivedQuerySchema.parse(c.req.query('archived')),\n ),\n ),\n );\n app.get('/api/threads', async (c) =>\n c.json(\n await service.listThreads(\n id(c.req.query('projectId') ?? ''),\n c.req.query('cursor') === undefined\n ? undefined\n : schemas.cursorSchema.parse(c.req.query('cursor')),\n schemas.archivedQuerySchema.parse(c.req.query('archived')),\n ),\n ),\n );\n app.post('/api/threads/:threadId/archive', async (c) => {\n return c.json(\n await service.setThreadArchived(\n id(c.req.param('threadId')),\n await parse(c, schemas.archiveThreadSchema),\n ),\n );\n });\n app.get('/api/threads/:threadId', async (c) =>\n c.json(await service.getThread(id(c.req.param('threadId')))),\n );\n app.get('/api/threads/:threadId/metadata', async (c) =>\n c.json(await service.getThreadMetadata(id(c.req.param('threadId')))),\n );\n app.get('/api/threads/:threadId/export', async (c) => {\n const exported = await service.exportThread(id(c.req.param('threadId')));\n c.header('Content-Type', 'text/markdown; charset=utf-8');\n c.header('Content-Disposition', `attachment; filename=\"${exported.filename}\"`);\n c.header('Cache-Control', 'no-store');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('X-Export-Completeness', exported.partial ? 'partial' : 'saved-history');\n return c.body(exported.markdown);\n });\n app.get('/api/threads/:threadId/images', async (c) => {\n c.header('Cache-Control', 'private, no-store');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Cross-Origin-Resource-Policy', 'same-origin');\n c.header('Content-Security-Policy', \"default-src 'none'; sandbox\");\n const image = await service.getThreadImage(id(c.req.param('threadId')), c.req.query('path'));\n return c.body(image.bytes, 200, {\n 'Content-Type': image.contentType,\n 'Content-Length': String(image.bytes.length),\n });\n });\n app.get('/api/threads/:threadId/markdown', async (c) => {\n c.header('Cache-Control', 'private, no-store');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Cross-Origin-Resource-Policy', 'same-origin');\n return c.json(\n await service.getThreadMarkdown(id(c.req.param('threadId')), c.req.query('path')),\n );\n });\n app.get('/api/operations/:operationId', (c) =>\n c.json(service.getOperation(id(c.req.param('operationId')))),\n );\n app.post('/api/projects/:projectId/threads', async (c) => {\n const op = await service.createThread(\n id(c.req.param('projectId')),\n await parse(c, schemas.createThreadSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/resume', async (c) => {\n const op = await service.resumeThread(\n id(c.req.param('threadId')),\n await parse(c, schemas.resumeThreadSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/fork', async (c) => {\n const op = await service.forkThread(\n id(c.req.param('threadId')),\n await parse(c, schemas.forkThreadSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/rename', async (c) => {\n const op = await service.renameThread(\n id(c.req.param('threadId')),\n await parse(c, schemas.renameThreadSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/turns', async (c) => {\n const op = await service.startTurn(\n id(c.req.param('threadId')),\n await parse(c, schemas.startTurnSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/review', async (c) => {\n const op = await service.startReview(\n id(c.req.param('threadId')),\n await parse(c, schemas.startReviewSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/steer', async (c) => {\n const op = await service.steer(\n id(c.req.param('threadId')),\n await parse(c, schemas.steerSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/compact', async (c) => {\n const op = await service.compactThread(\n id(c.req.param('threadId')),\n await parse(c, schemas.operationSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/interrupt', async (c) => {\n const op = await service.interrupt(\n id(c.req.param('threadId')),\n await parse(c, schemas.interruptSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/actions/:actionId/resolve', async (c) => {\n const op = await service.resolveAction(\n id(c.req.param('actionId')),\n await parse(c, schemas.resolveActionSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/operations/:operationId/reconcile', async (c) => {\n const op = await service.reconcileOperation(\n id(c.req.param('operationId')),\n await parse(c, schemas.reconcileSchema),\n );\n return c.json(op, status(op));\n });\n app.get(\n '/ws',\n upgradeWebSocket((c) =>\n socketHandlers(service, config, c.get('identity'), c.req.query('overview') === '1'),\n ),\n );\n app.all('/api/*', () => {\n throw new AppError('NOT_FOUND', 'API route not found', 404);\n });\n app.all('/api', () => {\n throw new AppError('NOT_FOUND', 'API route not found', 404);\n });\n app.all('/ws/*', () => {\n throw new AppError('NOT_FOUND', 'WebSocket route not found', 404);\n });\n app.use('*', async (c, next) => {\n if (!c.req.path.startsWith('/assets/')) c.header('Cache-Control', 'no-store');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Referrer-Policy', 'same-origin');\n c.header(\n 'Content-Security-Policy',\n // Remote Markdown images load directly in the browser, including HTTPS redirects.\n // Keep scripts, connections and all other resource policies restricted as before.\n \"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'\",\n );\n await next();\n });\n app.get('*', serveStatic({ root: staticDir }));\n app.get('*', async (c, next) => {\n if (!c.req.header('accept')?.includes('text/html') || /\\.[^/]+$/.test(c.req.path))\n return c.json({ error: { code: 'NOT_FOUND', message: 'Resource not found' } }, 404);\n return serveStatic({ path: `${staticDir}/index.html` })(c, next);\n });\n app.notFound((c) => c.json({ error: { code: 'NOT_FOUND', message: 'Resource not found' } }, 404));\n return app;\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,YAAY;AACrB,SAAS,iBAAiB;AAC1B,SAAS,mBAAmB;AAC5B,SAAS,wBAA2C;AAEpD,SAAS,gBAAgB;AACzB,SAAS,gBAAgB;AACzB,SAAS,2BAA2B;AACpC,YAAY,aAAa;AAGzB,SAAS,mBAAmB;AAC5B,SAAS,2BAA8D;AAEvE,SAAS,sBAAsB;AAC/B,SAAS,SAAS;AAClB,SAAS,uBAA8C;AACvD,SAAS,gBAAgB,2BAA2B;
|
|
4
|
+
"sourcesContent": ["import { Hono } from 'hono';\nimport { bodyLimit } from 'hono/body-limit';\nimport { serveStatic } from '@hono/node-server/serve-static';\nimport { upgradeWebSocket, type HttpBindings } from '@hono/node-server';\nimport type { ZodType } from 'zod';\nimport { ZodError } from 'zod';\nimport { AppError } from '../shared/errors.js';\nimport { IMAGE_REQUEST_BYTES } from '../shared/images.js';\nimport * as schemas from '../shared/validation.js';\nimport type { Operation } from '../shared/types.js';\nimport type { AppConfig } from './config.js';\nimport { ConfigStore } from './config-store.js';\nimport { createAuthenticator, type Authenticator, type Identity } from './auth.js';\nimport type { GatewayService } from './service.js';\nimport { socketHandlers } from './websocket.js';\nimport { z } from 'zod';\nimport { releaseChannels, type UpdateController } from '../shared/updates.js';\nimport { settingsSchema, updateRequestSchema } from '../updates/manager.js';\nimport { startShellSchema } from '../shared/shell.js';\nexport interface HttpDependencies {\n config: AppConfig;\n service: GatewayService;\n staticDir: string;\n authenticate?: Authenticator;\n updates?: UpdateController;\n}\nexport function createApp({\n config,\n service,\n staticDir,\n authenticate = createAuthenticator(config),\n updates,\n}: HttpDependencies) {\n const app = new Hono<{\n Bindings: HttpBindings;\n Variables: { identity: Identity; operationId?: string };\n }>();\n app.onError((error, c) => {\n const failure =\n error instanceof AppError\n ? error\n : error instanceof ZodError || error instanceof SyntaxError\n ? new AppError('VALIDATION_FAILED', 'Invalid request data', 400)\n : new AppError('INTERNAL_ERROR', 'An internal error occurred', 500);\n return c.json({ error: failure.toJSON(c.get('operationId')) }, failure.status as 400);\n });\n app.get('/api/health', (c) => c.json({ ok: true }));\n app.use('*', async (c, next) => {\n c.set('identity', await authenticate(c.req.raw, c.req.path === '/ws'));\n await next();\n });\n app.use('/api/*', (c, next) =>\n bodyLimit({\n maxSize: /^\\/api\\/threads\\/[^/]+\\/(turns|steer)$/.test(c.req.path)\n ? IMAGE_REQUEST_BYTES\n : 512 * 1024,\n onError: () => {\n throw new AppError('BODY_TOO_LARGE', 'Request body exceeds limit', 413);\n },\n })(c, next),\n );\n app.use('/api/*', async (c, next) => {\n c.header('Cache-Control', 'no-store');\n if (\n service.isInMaintenance() &&\n !['GET', 'HEAD', 'OPTIONS'].includes(c.req.method) &&\n !c.req.path.startsWith('/api/updates')\n )\n throw new AppError('UPDATE_MAINTENANCE', '\u6B63\u5728\u5347\u7EA7\uFF0C\u8BF7\u7B49\u5F85\u670D\u52A1\u91CD\u65B0\u8FDE\u63A5', 503);\n await next();\n });\n if (updates) {\n app.get('/api/updates', async (c) => c.json(await updates.status()));\n app.post('/api/updates/check', async (c) => {\n const input = z\n .object({ channel: z.enum(releaseChannels).optional() })\n .strict()\n .parse(await c.req.json());\n return c.json(await updates.check(input.channel));\n });\n app.post('/api/updates/settings', async (c) =>\n c.json(await updates.configure(settingsSchema.parse(await c.req.json()))),\n );\n app.post('/api/updates/run', async (c) =>\n c.json(await updates.start(updateRequestSchema.parse(await c.req.json())), 202),\n );\n }\n const id = (value: string) => schemas.idSchema.parse(value);\n const parse = async <T>(\n c: { req: { json: () => Promise<unknown> }; set: (key: 'operationId', value: string) => void },\n schema: ZodType<T>,\n ): Promise<T> => {\n const value: unknown = await c.req.json();\n if (\n value &&\n typeof value === 'object' &&\n 'clientOperationId' in value &&\n typeof value.clientOperationId === 'string'\n )\n c.set('operationId', value.clientOperationId);\n return schema.parse(value);\n };\n const status = (op: Operation) =>\n op.status === 'dispatching' || op.status === 'accepted' ? (202 as const) : (200 as const);\n const configuration = new ConfigStore(config);\n app.use('/api/config*', async (c, next) => {\n c.header('Cache-Control', 'no-store');\n await next();\n });\n app.get('/api/config', async (c) => c.json(await configuration.snapshot()));\n app.post('/api/config', async (c) => c.json(await configuration.save(await c.req.json())));\n app.post('/api/config/import', async (c) =>\n c.json(await configuration.previewImport(await c.req.json())),\n );\n app.get('/api/config/export', async (c) => {\n c.header('Content-Disposition', 'attachment; filename=\"codex-web-config.json\"');\n return c.json(await configuration.export());\n });\n app.get('/api/system', (c) => c.json(service.getSystem()));\n app.get('/api/shell', (c) =>\n c.json(\n service.shell.snapshot(\n c.req.query('epoch'),\n z.coerce\n .number()\n .int()\n .min(0)\n .max(Number.MAX_SAFE_INTEGER)\n .default(0)\n .parse(c.req.query('after')),\n ),\n ),\n );\n app.post('/api/projects/:projectId/shell', async (c) => {\n const op = await service.startShell(\n id(c.req.param('projectId')),\n await parse(c, startShellSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/shell/:commandId/interrupt', async (c) => {\n const op = await service.interruptShell(\n id(c.req.param('commandId')),\n await parse(c, schemas.operationSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/system/compatibility/confirm', async (c) => {\n const op = await service.confirmCompatibility(\n await parse(c, schemas.confirmCompatibilitySchema),\n );\n return c.json(op, status(op));\n });\n app.get('/api/projects', async (c) => c.json({ data: await service.listProjects() }));\n app.get('/api/projects/:projectId/runtime-settings', async (c) =>\n c.json(await service.readRuntimeSettings(id(c.req.param('projectId')))),\n );\n app.post('/api/projects/:projectId/model-defaults', async (c) => {\n const op = await service.saveModelDefaults(\n id(c.req.param('projectId')),\n await parse(c, schemas.saveModelDefaultsSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/projects', async (c) =>\n c.json(service.addProject(await parse(c, schemas.createProjectSchema))),\n );\n app.post('/api/projects/:projectId/rename', async (c) =>\n c.json(\n service.renameProject(\n id(c.req.param('projectId')),\n await parse(c, schemas.renameProjectSchema),\n ),\n ),\n );\n app.post('/api/projects/:projectId/remove', async (c) => {\n await parse(c, schemas.removeProjectSchema);\n return c.json(service.removeProject(id(c.req.param('projectId'))));\n });\n app.get('/api/projects/:projectId/threads', async (c) =>\n c.json(\n await service.listThreadSummaries(\n id(c.req.param('projectId')),\n c.req.query('cursor') === undefined\n ? undefined\n : schemas.cursorSchema.parse(c.req.query('cursor')),\n schemas.archivedQuerySchema.parse(c.req.query('archived')),\n ),\n ),\n );\n app.get('/api/threads', async (c) =>\n c.json(\n await service.listThreads(\n id(c.req.query('projectId') ?? ''),\n c.req.query('cursor') === undefined\n ? undefined\n : schemas.cursorSchema.parse(c.req.query('cursor')),\n schemas.archivedQuerySchema.parse(c.req.query('archived')),\n ),\n ),\n );\n app.post('/api/threads/:threadId/archive', async (c) => {\n return c.json(\n await service.setThreadArchived(\n id(c.req.param('threadId')),\n await parse(c, schemas.archiveThreadSchema),\n ),\n );\n });\n app.get('/api/threads/:threadId', async (c) =>\n c.json(await service.getThread(id(c.req.param('threadId')))),\n );\n app.get('/api/threads/:threadId/metadata', async (c) =>\n c.json(await service.getThreadMetadata(id(c.req.param('threadId')))),\n );\n app.get('/api/threads/:threadId/export', async (c) => {\n const exported = await service.exportThread(id(c.req.param('threadId')));\n c.header('Content-Type', 'text/markdown; charset=utf-8');\n c.header('Content-Disposition', `attachment; filename=\"${exported.filename}\"`);\n c.header('Cache-Control', 'no-store');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('X-Export-Completeness', exported.partial ? 'partial' : 'saved-history');\n return c.body(exported.markdown);\n });\n app.get('/api/threads/:threadId/images', async (c) => {\n c.header('Cache-Control', 'private, no-store');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Cross-Origin-Resource-Policy', 'same-origin');\n c.header('Content-Security-Policy', \"default-src 'none'; sandbox\");\n const image = await service.getThreadImage(id(c.req.param('threadId')), c.req.query('path'));\n return c.body(image.bytes, 200, {\n 'Content-Type': image.contentType,\n 'Content-Length': String(image.bytes.length),\n });\n });\n app.get('/api/threads/:threadId/markdown', async (c) => {\n c.header('Cache-Control', 'private, no-store');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Cross-Origin-Resource-Policy', 'same-origin');\n return c.json(\n await service.getThreadMarkdown(id(c.req.param('threadId')), c.req.query('path')),\n );\n });\n app.get('/api/threads/:threadId/json', async (c) => {\n c.header('Cache-Control', 'private, no-store');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Cross-Origin-Resource-Policy', 'same-origin');\n return c.json(await service.getThreadJson(id(c.req.param('threadId')), c.req.query('path')));\n });\n app.get('/api/operations/:operationId', (c) =>\n c.json(service.getOperation(id(c.req.param('operationId')))),\n );\n app.post('/api/projects/:projectId/threads', async (c) => {\n const op = await service.createThread(\n id(c.req.param('projectId')),\n await parse(c, schemas.createThreadSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/resume', async (c) => {\n const op = await service.resumeThread(\n id(c.req.param('threadId')),\n await parse(c, schemas.resumeThreadSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/fork', async (c) => {\n const op = await service.forkThread(\n id(c.req.param('threadId')),\n await parse(c, schemas.forkThreadSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/rename', async (c) => {\n const op = await service.renameThread(\n id(c.req.param('threadId')),\n await parse(c, schemas.renameThreadSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/turns', async (c) => {\n const op = await service.startTurn(\n id(c.req.param('threadId')),\n await parse(c, schemas.startTurnSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/review', async (c) => {\n const op = await service.startReview(\n id(c.req.param('threadId')),\n await parse(c, schemas.startReviewSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/steer', async (c) => {\n const op = await service.steer(\n id(c.req.param('threadId')),\n await parse(c, schemas.steerSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/compact', async (c) => {\n const op = await service.compactThread(\n id(c.req.param('threadId')),\n await parse(c, schemas.operationSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/threads/:threadId/interrupt', async (c) => {\n const op = await service.interrupt(\n id(c.req.param('threadId')),\n await parse(c, schemas.interruptSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/actions/:actionId/resolve', async (c) => {\n const op = await service.resolveAction(\n id(c.req.param('actionId')),\n await parse(c, schemas.resolveActionSchema),\n );\n return c.json(op, status(op));\n });\n app.post('/api/operations/:operationId/reconcile', async (c) => {\n const op = await service.reconcileOperation(\n id(c.req.param('operationId')),\n await parse(c, schemas.reconcileSchema),\n );\n return c.json(op, status(op));\n });\n app.get(\n '/ws',\n upgradeWebSocket((c) =>\n socketHandlers(service, config, c.get('identity'), c.req.query('overview') === '1'),\n ),\n );\n app.all('/api/*', () => {\n throw new AppError('NOT_FOUND', 'API route not found', 404);\n });\n app.all('/api', () => {\n throw new AppError('NOT_FOUND', 'API route not found', 404);\n });\n app.all('/ws/*', () => {\n throw new AppError('NOT_FOUND', 'WebSocket route not found', 404);\n });\n app.use('*', async (c, next) => {\n if (!c.req.path.startsWith('/assets/')) c.header('Cache-Control', 'no-store');\n c.header('X-Content-Type-Options', 'nosniff');\n c.header('Referrer-Policy', 'same-origin');\n c.header(\n 'Content-Security-Policy',\n // Remote Markdown images load directly in the browser, including HTTPS redirects.\n // Keep scripts, connections and all other resource policies restricted as before.\n \"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'\",\n );\n await next();\n });\n app.get('*', serveStatic({ root: staticDir }));\n app.get('*', async (c, next) => {\n if (!c.req.header('accept')?.includes('text/html') || /\\.[^/]+$/.test(c.req.path))\n return c.json({ error: { code: 'NOT_FOUND', message: 'Resource not found' } }, 404);\n return serveStatic({ path: `${staticDir}/index.html` })(c, next);\n });\n app.notFound((c) => c.json({ error: { code: 'NOT_FOUND', message: 'Resource not found' } }, 404));\n return app;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { extname } from "node:path";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { readLocalFile } from "./local-files.js";
|
|
4
|
+
const MAX_JSON_BYTES = 2 * 1024 * 1024;
|
|
5
|
+
async function readLocalJson(path, scope) {
|
|
6
|
+
const file = await readLocalFile(path, scope, {
|
|
7
|
+
code: "JSON",
|
|
8
|
+
label: "JSON",
|
|
9
|
+
maxBytes: MAX_JSON_BYTES,
|
|
10
|
+
sizeError: "JSON \u6587\u4EF6\u8D85\u8FC7 2 MiB \u9884\u89C8\u9650\u5236",
|
|
11
|
+
validatePath(candidate) {
|
|
12
|
+
if (extname(candidate).toLowerCase() !== ".json")
|
|
13
|
+
throw new AppError("UNSUPPORTED_JSON_TYPE", "\u4EC5\u652F\u6301 .json \u6587\u4EF6", 415);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
try {
|
|
17
|
+
const content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(file.bytes);
|
|
18
|
+
return { path: file.path, content };
|
|
19
|
+
} catch {
|
|
20
|
+
throw new AppError("INVALID_JSON_CONTENT", "\u6587\u4EF6\u4E0D\u662F\u6709\u6548\u7684 UTF-8 JSON \u6587\u672C", 415);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
MAX_JSON_BYTES,
|
|
25
|
+
readLocalJson
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=json-files.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/server/json-files.ts"],
|
|
4
|
+
"sourcesContent": ["import { extname } from 'node:path';\nimport { AppError } from '../shared/errors.js';\nimport type { TextFile } from '../shared/types.js';\nimport { readLocalFile, type FileScope } from './local-files.js';\n\nexport const MAX_JSON_BYTES = 2 * 1024 * 1024;\n\nexport async function readLocalJson(path: string | undefined, scope: FileScope): Promise<TextFile> {\n const file = await readLocalFile(path, scope, {\n code: 'JSON',\n label: 'JSON',\n maxBytes: MAX_JSON_BYTES,\n sizeError: 'JSON \u6587\u4EF6\u8D85\u8FC7 2 MiB \u9884\u89C8\u9650\u5236',\n validatePath(candidate) {\n if (extname(candidate).toLowerCase() !== '.json')\n throw new AppError('UNSUPPORTED_JSON_TYPE', '\u4EC5\u652F\u6301 .json \u6587\u4EF6', 415);\n },\n });\n try {\n const content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(file.bytes);\n return { path: file.path, content };\n } catch {\n throw new AppError('INVALID_JSON_CONTENT', '\u6587\u4EF6\u4E0D\u662F\u6709\u6548\u7684 UTF-8 JSON \u6587\u672C', 415);\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe;AACxB,SAAS,gBAAgB;AAEzB,SAAS,qBAAqC;AAEvC,MAAM,iBAAiB,IAAI,OAAO;AAEzC,eAAsB,cAAc,MAA0B,OAAqC;AACjG,QAAM,OAAO,MAAM,cAAc,MAAM,OAAO;AAAA,IAC5C,MAAM;AAAA,IACN,OAAO;AAAA,IACP,UAAU;AAAA,IACV,WAAW;AAAA,IACX,aAAa,WAAW;AACtB,UAAI,QAAQ,SAAS,EAAE,YAAY,MAAM;AACvC,cAAM,IAAI,SAAS,yBAAyB,yCAAgB,GAAG;AAAA,IACnE;AAAA,EACF,CAAC;AACD,MAAI;AACF,UAAM,UAAU,IAAI,YAAY,SAAS,EAAE,OAAO,MAAM,WAAW,KAAK,CAAC,EAAE,OAAO,KAAK,KAAK;AAC5F,WAAO,EAAE,MAAM,KAAK,MAAM,QAAQ;AAAA,EACpC,QAAQ;AACN,UAAM,IAAI,SAAS,wBAAwB,sEAAyB,GAAG;AAAA,EACzE;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/server/local-files.ts"],
|
|
4
|
-
"sourcesContent": ["import { constants, realpathSync, statSync } from 'node:fs';\nimport { open, realpath, stat } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { isAbsolute, parse, resolve } from 'node:path';\nimport { AppError } from '../shared/errors.js';\nimport { containsPath } from './projects.js';\n\nexport interface FileRoot {\n canonicalRoot: string;\n aliases: string[];\n}\n\nexport interface FileScope {\n cwd: string;\n roots: readonly FileRoot[];\n}\n\n/** Capture server-owned roots once; clients cannot nominate a temporary directory. */\nexport function temporaryFileRoots(): FileRoot[] {\n const roots = new Map<string, FileRoot>();\n for (const directory of [tmpdir(), ...(process.platform === 'win32' ? [] : ['/tmp'])]) {\n if (!isAbsolute(directory)) continue;\n const alias = resolve(directory);\n if (alias === parse(alias).root) continue;\n try {\n const canonicalRoot = realpathSync(alias);\n if (canonicalRoot === parse(canonicalRoot).root || !statSync(canonicalRoot).isDirectory())\n continue;\n const root = roots.get(canonicalRoot) ?? { canonicalRoot, aliases: [] };\n if (!root.aliases.includes(alias)) root.aliases.push(alias);\n roots.set(canonicalRoot, root);\n } catch {\n // Unavailable temp locations do not prevent previews in the remaining roots.\n }\n }\n return [...roots.values()];\n}\n\ninterface FilePolicy {\n code: 'IMAGE' | 'MARKDOWN';\n label: string;\n maxBytes: number;\n sizeError: string;\n validatePath: (path: string) => void;\n}\n\nfunction outsideProject(policy: FilePolicy): AppError {\n return new AppError(\n `${policy.code}_OUTSIDE_PROJECT`,\n `\u53EA\u80FD\u9884\u89C8\u5F53\u524D\u4F1A\u8BDD\u6240\u5C5E\u9879\u76EE\u6216\u670D\u52A1\u5668\u4E34\u65F6\u76EE\u5F55\u5185\u7684${policy.label}`,\n 403,\n );\n}\n\nfunction changedFile(policy: FilePolicy): AppError {\n return new AppError(`${policy.code}_CHANGED`, `${policy.label}\u6587\u4EF6\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u9884\u89C8`, 409);\n}\n\n/** Shared project/temp authorization and bounded, verified file reads for previews. */\nexport async function readLocalFile(\n path: string | undefined,\n scope: FileScope,\n policy: FilePolicy,\n) {\n if (\n !path ||\n path.length > 4096 ||\n [...path].some(\n (character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,\n ) ||\n /^[a-z][a-z\\d+.-]*:/i.test(path) ||\n path.startsWith('//') ||\n path.startsWith('\\\\\\\\')\n )\n throw new AppError(`INVALID_${policy.code}_PATH`, `${policy.label}\u8DEF\u5F84\u65E0\u6548`, 400);\n\n const candidate = resolve(scope.cwd, path);\n // Both spellings matter on systems where /tmp or the registered directory is an alias.\n if (\n !scope.roots.some(\n (root) =>\n containsPath(root.canonicalRoot, candidate) ||\n root.aliases.some((alias) => containsPath(alias, candidate)),\n )\n )\n throw outsideProject(policy);\n const isAllowed = (path: string) =>\n scope.roots.some((root) => containsPath(root.canonicalRoot, path));\n policy.validatePath(candidate);\n\n try {\n const canonical = await realpath(candidate);\n if (!isAllowed(canonical)) throw outsideProject(policy);\n // NOFOLLOW rejects a last-component symlink swapped in after realpath. NONBLOCK\n // ensures opening a substituted FIFO cannot leave the request waiting forever.\n const file = await open(\n canonical,\n constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,\n );\n try {\n const before = await file.stat();\n if (!before.isFile())\n throw new AppError(`${policy.code}_NOT_FILE`, `${policy.label}\u8DEF\u5F84\u4E0D\u662F\u666E\u901A\u6587\u4EF6`, 415);\n if (before.size > policy.maxBytes)\n throw new AppError(`${policy.code}_TOO_LARGE`, policy.sizeError, 413);\n\n // Recheck the path and opened inode to detect ordinary concurrent replacements.\n // Node's portable file API cannot make parent-directory traversal race-free.\n const verifyPath = async () => {\n const current = await realpath(candidate);\n if (!isAllowed(current)) throw outsideProject(policy);\n if (current !== canonical) throw changedFile(policy);\n const currentStat = await stat(current);\n if (currentStat.dev !== before.dev || currentStat.ino !== before.ino)\n throw changedFile(policy);\n };\n await verifyPath();\n // One extra byte detects growth without letting readFile allocate unbounded data.\n const data = Buffer.alloc(before.size + 1);\n let length = 0;\n while (length < data.length) {\n const { bytesRead } = await file.read(data, length, data.length - length, length);\n if (!bytesRead) break;\n length += bytesRead;\n }\n if (length > policy.maxBytes)\n throw new AppError(`${policy.code}_TOO_LARGE`, policy.sizeError, 413);\n const after = await file.stat();\n if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs)\n throw changedFile(policy);\n await verifyPath();\n const bytes = data.subarray(0, length);\n return { bytes, path: canonical };\n } finally {\n await file.close();\n }\n } catch (error) {\n if (error instanceof AppError) throw error;\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT' || code === 'ENOTDIR')\n throw new AppError(`${policy.code}_NOT_FOUND`, `${policy.label}\u6587\u4EF6\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664`, 404);\n if (code === 'ELOOP') throw outsideProject(policy);\n throw new AppError(`${policy.code}_UNREADABLE`, `${policy.label}\u6587\u4EF6\u4E0D\u53EF\u8BFB\u53D6`, 403);\n }\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { constants, realpathSync, statSync } from 'node:fs';\nimport { open, realpath, stat } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { isAbsolute, parse, resolve } from 'node:path';\nimport { AppError } from '../shared/errors.js';\nimport { containsPath } from './projects.js';\n\nexport interface FileRoot {\n canonicalRoot: string;\n aliases: string[];\n}\n\nexport interface FileScope {\n cwd: string;\n roots: readonly FileRoot[];\n}\n\n/** Capture server-owned roots once; clients cannot nominate a temporary directory. */\nexport function temporaryFileRoots(): FileRoot[] {\n const roots = new Map<string, FileRoot>();\n for (const directory of [tmpdir(), ...(process.platform === 'win32' ? [] : ['/tmp'])]) {\n if (!isAbsolute(directory)) continue;\n const alias = resolve(directory);\n if (alias === parse(alias).root) continue;\n try {\n const canonicalRoot = realpathSync(alias);\n if (canonicalRoot === parse(canonicalRoot).root || !statSync(canonicalRoot).isDirectory())\n continue;\n const root = roots.get(canonicalRoot) ?? { canonicalRoot, aliases: [] };\n if (!root.aliases.includes(alias)) root.aliases.push(alias);\n roots.set(canonicalRoot, root);\n } catch {\n // Unavailable temp locations do not prevent previews in the remaining roots.\n }\n }\n return [...roots.values()];\n}\n\ninterface FilePolicy {\n code: 'IMAGE' | 'MARKDOWN' | 'JSON';\n label: string;\n maxBytes: number;\n sizeError: string;\n validatePath: (path: string) => void;\n}\n\nfunction outsideProject(policy: FilePolicy): AppError {\n return new AppError(\n `${policy.code}_OUTSIDE_PROJECT`,\n `\u53EA\u80FD\u9884\u89C8\u5F53\u524D\u4F1A\u8BDD\u6240\u5C5E\u9879\u76EE\u6216\u670D\u52A1\u5668\u4E34\u65F6\u76EE\u5F55\u5185\u7684${policy.label}`,\n 403,\n );\n}\n\nfunction changedFile(policy: FilePolicy): AppError {\n return new AppError(`${policy.code}_CHANGED`, `${policy.label}\u6587\u4EF6\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u9884\u89C8`, 409);\n}\n\n/** Shared project/temp authorization and bounded, verified file reads for previews. */\nexport async function readLocalFile(\n path: string | undefined,\n scope: FileScope,\n policy: FilePolicy,\n) {\n if (\n !path ||\n path.length > 4096 ||\n [...path].some(\n (character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,\n ) ||\n /^[a-z][a-z\\d+.-]*:/i.test(path) ||\n path.startsWith('//') ||\n path.startsWith('\\\\\\\\')\n )\n throw new AppError(`INVALID_${policy.code}_PATH`, `${policy.label}\u8DEF\u5F84\u65E0\u6548`, 400);\n\n const candidate = resolve(scope.cwd, path);\n // Both spellings matter on systems where /tmp or the registered directory is an alias.\n if (\n !scope.roots.some(\n (root) =>\n containsPath(root.canonicalRoot, candidate) ||\n root.aliases.some((alias) => containsPath(alias, candidate)),\n )\n )\n throw outsideProject(policy);\n const isAllowed = (path: string) =>\n scope.roots.some((root) => containsPath(root.canonicalRoot, path));\n policy.validatePath(candidate);\n\n try {\n const canonical = await realpath(candidate);\n if (!isAllowed(canonical)) throw outsideProject(policy);\n // NOFOLLOW rejects a last-component symlink swapped in after realpath. NONBLOCK\n // ensures opening a substituted FIFO cannot leave the request waiting forever.\n const file = await open(\n canonical,\n constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,\n );\n try {\n const before = await file.stat();\n if (!before.isFile())\n throw new AppError(`${policy.code}_NOT_FILE`, `${policy.label}\u8DEF\u5F84\u4E0D\u662F\u666E\u901A\u6587\u4EF6`, 415);\n if (before.size > policy.maxBytes)\n throw new AppError(`${policy.code}_TOO_LARGE`, policy.sizeError, 413);\n\n // Recheck the path and opened inode to detect ordinary concurrent replacements.\n // Node's portable file API cannot make parent-directory traversal race-free.\n const verifyPath = async () => {\n const current = await realpath(candidate);\n if (!isAllowed(current)) throw outsideProject(policy);\n if (current !== canonical) throw changedFile(policy);\n const currentStat = await stat(current);\n if (currentStat.dev !== before.dev || currentStat.ino !== before.ino)\n throw changedFile(policy);\n };\n await verifyPath();\n // One extra byte detects growth without letting readFile allocate unbounded data.\n const data = Buffer.alloc(before.size + 1);\n let length = 0;\n while (length < data.length) {\n const { bytesRead } = await file.read(data, length, data.length - length, length);\n if (!bytesRead) break;\n length += bytesRead;\n }\n if (length > policy.maxBytes)\n throw new AppError(`${policy.code}_TOO_LARGE`, policy.sizeError, 413);\n const after = await file.stat();\n if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs)\n throw changedFile(policy);\n await verifyPath();\n const bytes = data.subarray(0, length);\n return { bytes, path: canonical };\n } finally {\n await file.close();\n }\n } catch (error) {\n if (error instanceof AppError) throw error;\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT' || code === 'ENOTDIR')\n throw new AppError(`${policy.code}_NOT_FOUND`, `${policy.label}\u6587\u4EF6\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664`, 404);\n if (code === 'ELOOP') throw outsideProject(policy);\n throw new AppError(`${policy.code}_UNREADABLE`, `${policy.label}\u6587\u4EF6\u4E0D\u53EF\u8BFB\u53D6`, 403);\n }\n}\n"],
|
|
5
5
|
"mappings": "AAAA,SAAS,WAAW,cAAc,gBAAgB;AAClD,SAAS,MAAM,UAAU,YAAY;AACrC,SAAS,cAAc;AACvB,SAAS,YAAY,OAAO,eAAe;AAC3C,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAatB,SAAS,qBAAiC;AAC/C,QAAM,QAAQ,oBAAI,IAAsB;AACxC,aAAW,aAAa,CAAC,OAAO,GAAG,GAAI,QAAQ,aAAa,UAAU,CAAC,IAAI,CAAC,MAAM,CAAE,GAAG;AACrF,QAAI,CAAC,WAAW,SAAS,EAAG;AAC5B,UAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAI,UAAU,MAAM,KAAK,EAAE,KAAM;AACjC,QAAI;AACF,YAAM,gBAAgB,aAAa,KAAK;AACxC,UAAI,kBAAkB,MAAM,aAAa,EAAE,QAAQ,CAAC,SAAS,aAAa,EAAE,YAAY;AACtF;AACF,YAAM,OAAO,MAAM,IAAI,aAAa,KAAK,EAAE,eAAe,SAAS,CAAC,EAAE;AACtE,UAAI,CAAC,KAAK,QAAQ,SAAS,KAAK,EAAG,MAAK,QAAQ,KAAK,KAAK;AAC1D,YAAM,IAAI,eAAe,IAAI;AAAA,IAC/B,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO,CAAC,GAAG,MAAM,OAAO,CAAC;AAC3B;AAUA,SAAS,eAAe,QAA8B;AACpD,SAAO,IAAI;AAAA,IACT,GAAG,OAAO,IAAI;AAAA,IACd,uIAAyB,OAAO,KAAK;AAAA,IACrC;AAAA,EACF;AACF;AAEA,SAAS,YAAY,QAA8B;AACjD,SAAO,IAAI,SAAS,GAAG,OAAO,IAAI,YAAY,GAAG,OAAO,KAAK,kFAAiB,GAAG;AACnF;AAGA,eAAsB,cACpB,MACA,OACA,QACA;AACA,MACE,CAAC,QACD,KAAK,SAAS,QACd,CAAC,GAAG,IAAI,EAAE;AAAA,IACR,CAAC,cAAc,UAAU,WAAW,CAAC,IAAI,MAAM,UAAU,WAAW,CAAC,MAAM;AAAA,EAC7E,KACA,sBAAsB,KAAK,IAAI,KAC/B,KAAK,WAAW,IAAI,KACpB,KAAK,WAAW,MAAM;AAEtB,UAAM,IAAI,SAAS,WAAW,OAAO,IAAI,SAAS,GAAG,OAAO,KAAK,4BAAQ,GAAG;AAE9E,QAAM,YAAY,QAAQ,MAAM,KAAK,IAAI;AAEzC,MACE,CAAC,MAAM,MAAM;AAAA,IACX,CAAC,SACC,aAAa,KAAK,eAAe,SAAS,KAC1C,KAAK,QAAQ,KAAK,CAAC,UAAU,aAAa,OAAO,SAAS,CAAC;AAAA,EAC/D;AAEA,UAAM,eAAe,MAAM;AAC7B,QAAM,YAAY,CAACA,UACjB,MAAM,MAAM,KAAK,CAAC,SAAS,aAAa,KAAK,eAAeA,KAAI,CAAC;AACnE,SAAO,aAAa,SAAS;AAE7B,MAAI;AACF,UAAM,YAAY,MAAM,SAAS,SAAS;AAC1C,QAAI,CAAC,UAAU,SAAS,EAAG,OAAM,eAAe,MAAM;AAGtD,UAAM,OAAO,MAAM;AAAA,MACjB;AAAA,MACA,UAAU,WAAW,UAAU,aAAa,UAAU;AAAA,IACxD;AACA,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,KAAK;AAC/B,UAAI,CAAC,OAAO,OAAO;AACjB,cAAM,IAAI,SAAS,GAAG,OAAO,IAAI,aAAa,GAAG,OAAO,KAAK,oDAAY,GAAG;AAC9E,UAAI,OAAO,OAAO,OAAO;AACvB,cAAM,IAAI,SAAS,GAAG,OAAO,IAAI,cAAc,OAAO,WAAW,GAAG;AAItE,YAAM,aAAa,YAAY;AAC7B,cAAM,UAAU,MAAM,SAAS,SAAS;AACxC,YAAI,CAAC,UAAU,OAAO,EAAG,OAAM,eAAe,MAAM;AACpD,YAAI,YAAY,UAAW,OAAM,YAAY,MAAM;AACnD,cAAM,cAAc,MAAM,KAAK,OAAO;AACtC,YAAI,YAAY,QAAQ,OAAO,OAAO,YAAY,QAAQ,OAAO;AAC/D,gBAAM,YAAY,MAAM;AAAA,MAC5B;AACA,YAAM,WAAW;AAEjB,YAAM,OAAO,OAAO,MAAM,OAAO,OAAO,CAAC;AACzC,UAAI,SAAS;AACb,aAAO,SAAS,KAAK,QAAQ;AAC3B,cAAM,EAAE,UAAU,IAAI,MAAM,KAAK,KAAK,MAAM,QAAQ,KAAK,SAAS,QAAQ,MAAM;AAChF,YAAI,CAAC,UAAW;AAChB,kBAAU;AAAA,MACZ;AACA,UAAI,SAAS,OAAO;AAClB,cAAM,IAAI,SAAS,GAAG,OAAO,IAAI,cAAc,OAAO,WAAW,GAAG;AACtE,YAAM,QAAQ,MAAM,KAAK,KAAK;AAC9B,UAAI,WAAW,OAAO,QAAQ,MAAM,SAAS,OAAO,QAAQ,MAAM,YAAY,OAAO;AACnF,cAAM,YAAY,MAAM;AAC1B,YAAM,WAAW;AACjB,YAAM,QAAQ,KAAK,SAAS,GAAG,MAAM;AACrC,aAAO,EAAE,OAAO,MAAM,UAAU;AAAA,IAClC,UAAE;AACA,YAAM,KAAK,MAAM;AAAA,IACnB;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAiB,SAAU,OAAM;AACrC,UAAM,OAAQ,MAAgC;AAC9C,QAAI,SAAS,YAAY,SAAS;AAChC,YAAM,IAAI,SAAS,GAAG,OAAO,IAAI,cAAc,GAAG,OAAO,KAAK,gEAAc,GAAG;AACjF,QAAI,SAAS,QAAS,OAAM,eAAe,MAAM;AACjD,UAAM,IAAI,SAAS,GAAG,OAAO,IAAI,eAAe,GAAG,OAAO,KAAK,wCAAU,GAAG;AAAA,EAC9E;AACF;",
|
|
6
6
|
"names": ["path"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/server/markdown.ts"],
|
|
4
|
-
"sourcesContent": ["import { extname } from 'node:path';\nimport { AppError } from '../shared/errors.js';\nimport type {
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe;AACxB,SAAS,gBAAgB;AAEzB,SAAS,qBAAqC;AAEvC,MAAM,qBAAqB,IAAI,OAAO;AAE7C,eAAsB,kBACpB,MACA,
|
|
4
|
+
"sourcesContent": ["import { extname } from 'node:path';\nimport { AppError } from '../shared/errors.js';\nimport type { TextFile } from '../shared/types.js';\nimport { readLocalFile, type FileScope } from './local-files.js';\n\nexport const MAX_MARKDOWN_BYTES = 2 * 1024 * 1024;\n\nexport async function readLocalMarkdown(\n path: string | undefined,\n scope: FileScope,\n): Promise<TextFile> {\n const file = await readLocalFile(path, scope, {\n code: 'MARKDOWN',\n label: 'Markdown',\n maxBytes: MAX_MARKDOWN_BYTES,\n sizeError: 'Markdown \u6587\u4EF6\u8D85\u8FC7 2 MiB \u9884\u89C8\u9650\u5236',\n validatePath(candidate) {\n if (!/^(\\.md|\\.markdown)$/i.test(extname(candidate)))\n throw new AppError('UNSUPPORTED_MARKDOWN_TYPE', '\u4EC5\u652F\u6301 .md \u548C .markdown \u6587\u4EF6', 415);\n },\n });\n try {\n const content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(file.bytes);\n if (content.includes('\\0')) throw new Error('Binary content');\n return { path: file.path, content };\n } catch {\n throw new AppError('INVALID_MARKDOWN_CONTENT', '\u6587\u4EF6\u4E0D\u662F\u6709\u6548\u7684 UTF-8 Markdown \u6587\u672C', 415);\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe;AACxB,SAAS,gBAAgB;AAEzB,SAAS,qBAAqC;AAEvC,MAAM,qBAAqB,IAAI,OAAO;AAE7C,eAAsB,kBACpB,MACA,OACmB;AACnB,QAAM,OAAO,MAAM,cAAc,MAAM,OAAO;AAAA,IAC5C,MAAM;AAAA,IACN,OAAO;AAAA,IACP,UAAU;AAAA,IACV,WAAW;AAAA,IACX,aAAa,WAAW;AACtB,UAAI,CAAC,uBAAuB,KAAK,QAAQ,SAAS,CAAC;AACjD,cAAM,IAAI,SAAS,6BAA6B,wDAA0B,GAAG;AAAA,IACjF;AAAA,EACF,CAAC;AACD,MAAI;AACF,UAAM,UAAU,IAAI,YAAY,SAAS,EAAE,OAAO,MAAM,WAAW,KAAK,CAAC,EAAE,OAAO,KAAK,KAAK;AAC5F,QAAI,QAAQ,SAAS,IAAI,EAAG,OAAM,IAAI,MAAM,gBAAgB;AAC5D,WAAO,EAAE,MAAM,KAAK,MAAM,QAAQ;AAAA,EACpC,QAAQ;AACN,UAAM,IAAI,SAAS,4BAA4B,0EAA6B,GAAG;AAAA,EACjF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/server/service.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ShellService } from "./shell.js";
|
|
1
2
|
import { realpathSync } from "node:fs";
|
|
2
3
|
import { AppError } from "../shared/errors.js";
|
|
3
4
|
import { threadNameError } from "../shared/thread-name.js";
|
|
@@ -16,6 +17,7 @@ import { renderThreadExport } from "./thread-export.js";
|
|
|
16
17
|
import { readLocalImage } from "./images.js";
|
|
17
18
|
import { temporaryFileRoots } from "./local-files.js";
|
|
18
19
|
import { readLocalMarkdown } from "./markdown.js";
|
|
20
|
+
import { readLocalJson } from "./json-files.js";
|
|
19
21
|
class GatewayService {
|
|
20
22
|
constructor(options) {
|
|
21
23
|
this.options = options;
|
|
@@ -23,6 +25,7 @@ class GatewayService {
|
|
|
23
25
|
this.turnUsage = new TurnUsageStore(this.store.database);
|
|
24
26
|
this.projects = options.projects;
|
|
25
27
|
this.codex = options.codex;
|
|
28
|
+
this.shell = new ShellService(this.codex);
|
|
26
29
|
this.events = new EventHub(options.limits?.eventBytes);
|
|
27
30
|
this.projection = new Projection(options.limits?.itemBytes);
|
|
28
31
|
this.currentEpoch = this.codex.epoch;
|
|
@@ -30,6 +33,7 @@ class GatewayService {
|
|
|
30
33
|
this.removeListener = this.codex.onEvent((event) => this.consume(event));
|
|
31
34
|
}
|
|
32
35
|
options;
|
|
36
|
+
shell;
|
|
33
37
|
events;
|
|
34
38
|
projection;
|
|
35
39
|
store;
|
|
@@ -60,6 +64,21 @@ class GatewayService {
|
|
|
60
64
|
listProjects() {
|
|
61
65
|
return this.projects.list();
|
|
62
66
|
}
|
|
67
|
+
startShell(projectId, input) {
|
|
68
|
+
return this.perform("shell.start", projectId, input, async () => {
|
|
69
|
+
this.ensureCompatibilityConfirmed();
|
|
70
|
+
const project = this.projects.get(projectId);
|
|
71
|
+
this.store.dispatch(input.clientOperationId);
|
|
72
|
+
return { result: { shellCommand: this.shell.start(project, input) } };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
interruptShell(id, input) {
|
|
76
|
+
return this.perform("shell.interrupt", id, input, async () => {
|
|
77
|
+
this.store.dispatch(input.clientOperationId);
|
|
78
|
+
await this.shell.interrupt(id);
|
|
79
|
+
return { result: { interrupted: true } };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
63
82
|
isInMaintenance() {
|
|
64
83
|
return this.maintenance;
|
|
65
84
|
}
|
|
@@ -67,6 +86,7 @@ class GatewayService {
|
|
|
67
86
|
this.maintenance = value;
|
|
68
87
|
}
|
|
69
88
|
prepareUpdate() {
|
|
89
|
+
if (this.shell.active()) return { ready: false, reason: "\u7B49\u5F85 Shell \u547D\u4EE4\u7ED3\u675F" };
|
|
70
90
|
if (this.inFlightOperations.size || this.resumes.size || this.pendingThreadCreations.size)
|
|
71
91
|
return { ready: false, reason: "\u7B49\u5F85\u6B63\u5728\u63D0\u4EA4\u6216\u6062\u590D\u7684\u64CD\u4F5C\u5B8C\u6210" };
|
|
72
92
|
if (this.store.activeMarkers().length || this.store.unresolvedOperations().length)
|
|
@@ -98,7 +118,7 @@ class GatewayService {
|
|
|
98
118
|
const active = this.projection.values().find(
|
|
99
119
|
(thread) => thread.projectId === projectId && (isActive(thread.status) || thread.actions.some((action) => ["pending", "responding"].includes(action.status)))
|
|
100
120
|
);
|
|
101
|
-
if (pending || marker || active)
|
|
121
|
+
if (pending || marker || active || this.shell.active(projectId))
|
|
102
122
|
throw new AppError(
|
|
103
123
|
"PROJECT_BUSY",
|
|
104
124
|
"\u9879\u76EE\u4ECD\u6709\u6D3B\u52A8\u4EFB\u52A1\u6216\u7ED3\u679C\u5F85\u6838\u5BF9\u7684\u64CD\u4F5C\uFF1B\u8BF7\u5148\u7ED3\u675F\u4EFB\u52A1\u5E76\u6838\u5BF9\u65E7\u64CD\u4F5C",
|
|
@@ -408,6 +428,9 @@ class GatewayService {
|
|
|
408
428
|
getThreadMarkdown(threadId, path) {
|
|
409
429
|
return this.readThreadFile(threadId, path, readLocalMarkdown);
|
|
410
430
|
}
|
|
431
|
+
getThreadJson(threadId, path) {
|
|
432
|
+
return this.readThreadFile(threadId, path, readLocalJson);
|
|
433
|
+
}
|
|
411
434
|
async readThreadFile(threadId, path, read) {
|
|
412
435
|
let link = this.store.getThreadLink(threadId);
|
|
413
436
|
if (!link) {
|