@cjhyy/code-shell-server 0.9.7 → 0.9.9

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.
Files changed (109) hide show
  1. package/README.md +85 -23
  2. package/dist/bin/code-shell-project-runtime.d.ts +2 -0
  3. package/dist/bin/code-shell-project-runtime.js +8 -0
  4. package/dist/desktop-web/http-api.d.ts +43 -0
  5. package/dist/desktop-web/http-api.js +376 -0
  6. package/dist/hub/approval-lease.d.ts +30 -0
  7. package/dist/hub/approval-lease.js +89 -0
  8. package/dist/hub/auth-http.d.ts +23 -0
  9. package/dist/hub/auth-http.js +259 -0
  10. package/dist/hub/auth-store.d.ts +52 -0
  11. package/dist/hub/auth-store.js +316 -0
  12. package/dist/hub/configuration.d.ts +64 -0
  13. package/dist/hub/configuration.js +555 -0
  14. package/dist/hub/files.d.ts +9 -0
  15. package/dist/hub/files.js +278 -0
  16. package/dist/hub/mcp-configuration.d.ts +51 -0
  17. package/dist/hub/mcp-configuration.js +558 -0
  18. package/dist/hub/mcp-probe.d.ts +24 -0
  19. package/dist/hub/mcp-probe.js +392 -0
  20. package/dist/hub/model-probe.d.ts +20 -0
  21. package/dist/hub/model-probe.js +193 -0
  22. package/dist/hub/outbound-transport.d.ts +36 -0
  23. package/dist/hub/outbound-transport.js +122 -0
  24. package/dist/hub/run-replay.d.ts +42 -0
  25. package/dist/hub/run-replay.js +89 -0
  26. package/dist/hub/session-management.d.ts +36 -0
  27. package/dist/hub/session-management.js +405 -0
  28. package/dist/hub/skills-management.d.ts +43 -0
  29. package/dist/hub/skills-management.js +487 -0
  30. package/dist/hub/uploads.d.ts +22 -0
  31. package/dist/hub/uploads.js +241 -0
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.desktop-web.d.ts +2 -0
  34. package/dist/index.desktop-web.js +2 -0
  35. package/dist/index.js +3 -0
  36. package/dist/index.links.d.ts +6 -0
  37. package/dist/index.links.js +6 -0
  38. package/dist/index.panels.d.ts +9 -0
  39. package/dist/index.panels.js +8 -0
  40. package/dist/index.serve.d.ts +1 -0
  41. package/dist/index.serve.js +1 -0
  42. package/dist/index.storage.d.ts +2 -0
  43. package/dist/index.storage.js +2 -0
  44. package/dist/links/catalog.d.ts +10 -0
  45. package/dist/links/catalog.js +41 -0
  46. package/dist/links/device-oauth.d.ts +52 -0
  47. package/dist/links/device-oauth.js +255 -0
  48. package/dist/links/http.d.ts +34 -0
  49. package/dist/links/http.js +144 -0
  50. package/dist/links/service.d.ts +54 -0
  51. package/dist/links/service.js +578 -0
  52. package/dist/links/types.d.ts +6 -0
  53. package/dist/links/types.js +1 -0
  54. package/dist/mobile-remote/mobile-client-event-validator.js +5 -2
  55. package/dist/mobile-remote/pending-approvals.js +2 -0
  56. package/dist/mobile-remote/remote-host-manager.d.ts +8 -0
  57. package/dist/mobile-remote/remote-host-manager.js +90 -15
  58. package/dist/mobile-remote/room-manager.js +17 -5
  59. package/dist/panels/agent-task-host.d.ts +32 -0
  60. package/dist/panels/agent-task-host.js +457 -0
  61. package/dist/panels/agent-task-models.d.ts +27 -0
  62. package/dist/panels/agent-task-models.js +51 -0
  63. package/dist/panels/agent-task-service.d.ts +105 -0
  64. package/dist/panels/agent-task-service.js +283 -0
  65. package/dist/panels/http.d.ts +34 -0
  66. package/dist/panels/http.js +37 -0
  67. package/dist/panels/hub-binding.d.ts +12 -0
  68. package/dist/panels/hub-binding.js +174 -0
  69. package/dist/panels/management-http.d.ts +29 -0
  70. package/dist/panels/management-http.js +119 -0
  71. package/dist/panels/management.d.ts +39 -0
  72. package/dist/panels/management.js +494 -0
  73. package/dist/panels/process-files.d.ts +14 -0
  74. package/dist/panels/process-files.js +299 -0
  75. package/dist/panels/process-service.d.ts +96 -0
  76. package/dist/panels/process-service.js +501 -0
  77. package/dist/panels/runtime-services.d.ts +24 -0
  78. package/dist/panels/runtime-services.js +462 -0
  79. package/dist/panels/runtime.d.ts +51 -0
  80. package/dist/panels/runtime.js +1063 -0
  81. package/dist/panels/storage-store.d.ts +9 -0
  82. package/dist/panels/storage-store.js +154 -0
  83. package/dist/panels/types.d.ts +52 -0
  84. package/dist/panels/types.js +1 -0
  85. package/dist/project-runtime/control-server.d.ts +19 -0
  86. package/dist/project-runtime/control-server.js +300 -0
  87. package/dist/project-runtime/docker-provider.d.ts +33 -0
  88. package/dist/project-runtime/docker-provider.js +487 -0
  89. package/dist/project-runtime/lock.d.ts +6 -0
  90. package/dist/project-runtime/lock.js +43 -0
  91. package/dist/project-runtime/managed-entry.d.ts +10 -0
  92. package/dist/project-runtime/managed-entry.js +113 -0
  93. package/dist/project-runtime/manager.d.ts +40 -0
  94. package/dist/project-runtime/manager.js +249 -0
  95. package/dist/project-runtime/proxy.d.ts +22 -0
  96. package/dist/project-runtime/proxy.js +706 -0
  97. package/dist/project-runtime/registry.d.ts +54 -0
  98. package/dist/project-runtime/registry.js +286 -0
  99. package/dist/project-runtime/types.d.ts +43 -0
  100. package/dist/project-runtime/types.js +1 -0
  101. package/dist/serve/cli.d.ts +5 -0
  102. package/dist/serve/cli.js +82 -23
  103. package/dist/serve/headless-server.d.ts +11 -0
  104. package/dist/serve/headless-server.js +737 -83
  105. package/dist/session-titles-store.d.ts +17 -0
  106. package/dist/session-titles-store.js +174 -0
  107. package/dist/static-files.js +1 -0
  108. package/dist/worker-bridge-core.js +17 -6
  109. package/package.json +19 -5
@@ -0,0 +1,9 @@
1
+ /** Matches the existing Desktop namespace, so its remote Web shares saved panel state. */
2
+ export declare function panelAppStoragePath(dataDir: string, appId: string, projectPath: string): string;
3
+ export declare const DEFAULT_PANEL_APP_STORAGE_QUOTA_BYTES: number;
4
+ export type PanelAppStorage = Record<string, unknown>;
5
+ export declare function panelAppStorageQuotaBytes(requested?: number): number;
6
+ export declare function panelAppStorageKey(params: unknown): string;
7
+ export declare function preparePanelAppStorage(file: string): Promise<void>;
8
+ export declare function readPanelAppStorage(file: string, quotaBytes: number): Promise<PanelAppStorage>;
9
+ export declare function writePanelAppStorage(file: string, storage: PanelAppStorage, quotaBytes: number, beforeCommit?: () => Promise<void>): Promise<void>;
@@ -0,0 +1,154 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { chmod, lstat, mkdir, open, rename, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ /** Matches the existing Desktop namespace, so its remote Web shares saved panel state. */
6
+ export function panelAppStoragePath(dataDir, appId, projectPath) {
7
+ const namespace = createHash("sha256")
8
+ .update("codeshell-panel-app-storage-v2")
9
+ .update("\0")
10
+ .update(appId)
11
+ .update("\0")
12
+ .update(projectPath)
13
+ .digest("hex");
14
+ return join(dataDir, "panel-app-storage", `${namespace}.json`);
15
+ }
16
+ export const DEFAULT_PANEL_APP_STORAGE_QUOTA_BYTES = 256 * 1024;
17
+ const MAX_PANEL_APP_STORAGE_QUOTA_BYTES = 16 * 1024 * 1024;
18
+ const MAX_PANEL_APP_STORAGE_KEYS = 10_000;
19
+ const STORAGE_KEY_PATTERN = /^[a-zA-Z0-9._-]{1,80}$/;
20
+ export function panelAppStorageQuotaBytes(requested) {
21
+ if (requested === undefined)
22
+ return DEFAULT_PANEL_APP_STORAGE_QUOTA_BYTES;
23
+ if (!Number.isSafeInteger(requested) || requested <= 0) {
24
+ throw new Error("Panel App storage quota must be a positive safe integer");
25
+ }
26
+ return Math.min(requested, MAX_PANEL_APP_STORAGE_QUOTA_BYTES);
27
+ }
28
+ export function panelAppStorageKey(params) {
29
+ const key = params?.key;
30
+ if (typeof key !== "string" || !STORAGE_KEY_PATTERN.test(key)) {
31
+ throw new Error("storage key must match [a-zA-Z0-9._-]{1,80}");
32
+ }
33
+ return key;
34
+ }
35
+ function emptyStorage() {
36
+ // A null prototype is important here: valid app keys include names such as
37
+ // `__proto__`, `constructor`, and `toString`. They must remain data keys and
38
+ // never read or mutate JavaScript's object prototype.
39
+ return Object.create(null);
40
+ }
41
+ async function assertRealStorageParent(file, create) {
42
+ const parent = dirname(file);
43
+ if (create)
44
+ await mkdir(parent, { recursive: true, mode: 0o700 });
45
+ const metadata = await lstat(parent);
46
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
47
+ throw new Error("Panel App storage parent must be a real directory");
48
+ }
49
+ }
50
+ async function storageFileMetadata(file) {
51
+ try {
52
+ return await lstat(file);
53
+ }
54
+ catch (error) {
55
+ if (error.code === "ENOENT")
56
+ return null;
57
+ throw error;
58
+ }
59
+ }
60
+ export async function preparePanelAppStorage(file) {
61
+ await assertRealStorageParent(file, true);
62
+ const metadata = await storageFileMetadata(file);
63
+ if (metadata && (metadata.isSymbolicLink() || !metadata.isFile())) {
64
+ throw new Error("Panel App storage target must be a regular file");
65
+ }
66
+ if (!metadata) {
67
+ // Materialize an empty store so the caller can take a PER-FILE lock on it.
68
+ // Locking this path (rather than the shared parent directory) is what keeps
69
+ // one panel app's storage write from stalling every other app; the lock
70
+ // library needs the target to exist. `wx` keeps this a no-op under a race.
71
+ try {
72
+ await writeFile(file, "{}\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
73
+ }
74
+ catch (error) {
75
+ if (error.code !== "EEXIST")
76
+ throw error;
77
+ }
78
+ }
79
+ }
80
+ export async function readPanelAppStorage(file, quotaBytes) {
81
+ const quota = panelAppStorageQuotaBytes(quotaBytes);
82
+ let handle;
83
+ try {
84
+ await assertRealStorageParent(file, false);
85
+ const metadata = await storageFileMetadata(file);
86
+ if (!metadata)
87
+ return emptyStorage();
88
+ if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.size > quota) {
89
+ throw new Error("Panel App storage must be a bounded regular file");
90
+ }
91
+ handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
92
+ const opened = await handle.stat();
93
+ if (!opened.isFile() || opened.size > quota) {
94
+ throw new Error("Panel App storage must be a bounded regular file");
95
+ }
96
+ const parsed = JSON.parse(await handle.readFile("utf8"));
97
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
98
+ throw new Error("Panel App storage root must be an object");
99
+ }
100
+ const entries = Object.entries(parsed);
101
+ if (entries.length > MAX_PANEL_APP_STORAGE_KEYS) {
102
+ throw new Error("Panel App storage contains too many keys");
103
+ }
104
+ const storage = emptyStorage();
105
+ for (const [key, value] of entries) {
106
+ if (!STORAGE_KEY_PATTERN.test(key))
107
+ throw new Error("Panel App storage contains an invalid key");
108
+ storage[key] = value;
109
+ }
110
+ return storage;
111
+ }
112
+ catch (error) {
113
+ if (error.code === "ENOENT")
114
+ return emptyStorage();
115
+ throw error;
116
+ }
117
+ finally {
118
+ await handle?.close().catch(() => undefined);
119
+ }
120
+ }
121
+ export async function writePanelAppStorage(file, storage, quotaBytes, beforeCommit) {
122
+ const quota = panelAppStorageQuotaBytes(quotaBytes);
123
+ const entries = Object.entries(storage);
124
+ if (entries.length > MAX_PANEL_APP_STORAGE_KEYS) {
125
+ throw new Error("Panel App storage contains too many keys");
126
+ }
127
+ for (const [key] of entries) {
128
+ if (!STORAGE_KEY_PATTERN.test(key))
129
+ throw new Error("Panel App storage contains an invalid key");
130
+ }
131
+ const serialized = `${JSON.stringify(storage)}\n`;
132
+ if (Buffer.byteLength(serialized, "utf8") > quota) {
133
+ throw new Error("Panel App storage quota exceeded");
134
+ }
135
+ await preparePanelAppStorage(file);
136
+ const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
137
+ try {
138
+ await writeFile(temporary, serialized, {
139
+ encoding: "utf8",
140
+ mode: 0o600,
141
+ flag: "wx",
142
+ });
143
+ // A remote owner can disconnect or lose its binding while staging bytes.
144
+ // Recheck both authorization and the target after staging, before publishing.
145
+ await beforeCommit?.();
146
+ await preparePanelAppStorage(file);
147
+ await rename(temporary, file);
148
+ if (process.platform !== "win32")
149
+ await chmod(file, 0o600);
150
+ }
151
+ finally {
152
+ await rm(temporary, { force: true }).catch(() => undefined);
153
+ }
154
+ }
@@ -0,0 +1,52 @@
1
+ import type { GitPanelAppDiscoveryCandidate, GitPanelAppDiscoveryIssue, PanelAppPreview } from "@cjhyy/code-shell-core";
2
+ export interface PanelCompatibility {
3
+ supported: boolean;
4
+ reasons: string[];
5
+ }
6
+ export interface PanelGitSource {
7
+ kind: "git";
8
+ url: string;
9
+ /** The original branch/tag, retained separately from the pinned archive commit. */
10
+ ref: string;
11
+ subdir?: string;
12
+ commit: string;
13
+ }
14
+ export interface ManagedPanel extends Omit<PanelAppPreview, "reviewToken" | "alreadyInstalled" | "warnings" | "source"> {
15
+ revision: string;
16
+ bound: boolean;
17
+ enabled: boolean;
18
+ globalDisabled: boolean;
19
+ updatable: boolean;
20
+ source: {
21
+ kind: "git" | "local";
22
+ label: string;
23
+ url?: string;
24
+ ref?: string;
25
+ commit?: string;
26
+ subdir?: string;
27
+ };
28
+ compatibility: PanelCompatibility;
29
+ }
30
+ export interface PanelSnapshot {
31
+ panels: ManagedPanel[];
32
+ workspace: string;
33
+ hasProject: boolean;
34
+ }
35
+ export interface PanelDiscovery {
36
+ panels: GitPanelAppDiscoveryCandidate[];
37
+ issues: GitPanelAppDiscoveryIssue[];
38
+ source: PanelGitSource;
39
+ }
40
+ export interface PanelReview {
41
+ reviewToken: string;
42
+ expiresAt: number;
43
+ kind: "install" | "update";
44
+ preview: Omit<PanelAppPreview, "reviewToken">;
45
+ source: PanelGitSource;
46
+ expectedRevision: string | null;
47
+ compatibility: PanelCompatibility;
48
+ }
49
+ export interface PanelOperationContext {
50
+ ownerId: string;
51
+ authorize: () => boolean | Promise<boolean>;
52
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import type { ProjectRuntimeProvider } from "./types.js";
2
+ export interface ProjectControlServerOptions {
3
+ host: string;
4
+ port: number;
5
+ dataDir: string;
6
+ publicOrigin?: string;
7
+ staticRootDir?: string;
8
+ runtimeImage?: string;
9
+ /** Dependency injection for lifecycle/transport integration tests. */
10
+ provider?: ProjectRuntimeProvider;
11
+ }
12
+ /** A small account/project gateway. It never creates a worker, executes tools or mounts cwd. */
13
+ export declare function startProjectControlServer(options: ProjectControlServerOptions): Promise<{
14
+ url: string;
15
+ port: number;
16
+ bootstrapToken: string | undefined;
17
+ generatedPasscode: undefined;
18
+ close(): Promise<void>;
19
+ }>;
@@ -0,0 +1,300 @@
1
+ import { createServer } from "node:http";
2
+ import { readFileSync } from "node:fs";
3
+ import { extname } from "node:path";
4
+ import { createHubAuth } from "../hub/auth-http.js";
5
+ import { resolveSafe } from "../mobile-remote/mobile-static.js";
6
+ import { contentTypeFor } from "../static-files.js";
7
+ import { createDockerProjectProvider } from "./docker-provider.js";
8
+ import { ProjectManager } from "./manager.js";
9
+ import { createProjectRuntimeProxy } from "./proxy.js";
10
+ import { ProjectRegistry, ProjectRegistryError } from "./registry.js";
11
+ function json(response, status, value) {
12
+ if (response.destroyed || response.writableEnded)
13
+ return;
14
+ response.writeHead(status, {
15
+ "Content-Type": "application/json; charset=utf-8",
16
+ "Cache-Control": "no-store",
17
+ });
18
+ response.end(JSON.stringify(value));
19
+ }
20
+ async function readBody(request) {
21
+ if (!/^application\/json(?:\s*;|$)/i.test(request.headers["content-type"] ?? ""))
22
+ throw new ProjectRegistryError(415, "请使用 JSON 提交。");
23
+ let size = 0;
24
+ const chunks = [];
25
+ await new Promise((resolve, reject) => {
26
+ const cleanup = () => {
27
+ request.off("data", onData);
28
+ request.off("end", onEnd);
29
+ request.off("error", onError);
30
+ request.off("aborted", onAborted);
31
+ };
32
+ const onError = (error) => {
33
+ cleanup();
34
+ reject(error);
35
+ };
36
+ const onAborted = () => onError(new ProjectRegistryError(400, "请求已中断。"));
37
+ const onEnd = () => {
38
+ cleanup();
39
+ resolve();
40
+ };
41
+ const onData = (chunk) => {
42
+ size += chunk.length;
43
+ if (size > 8192) {
44
+ cleanup();
45
+ request.resume();
46
+ reject(new ProjectRegistryError(413, "请求过大。"));
47
+ }
48
+ else
49
+ chunks.push(Buffer.from(chunk));
50
+ };
51
+ request.on("data", onData);
52
+ request.once("end", onEnd);
53
+ request.once("error", onError);
54
+ request.once("aborted", onAborted);
55
+ });
56
+ try {
57
+ const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
58
+ if (value && typeof value === "object" && !Array.isArray(value))
59
+ return value;
60
+ }
61
+ catch {
62
+ /* Invalid JSON is a client error. */
63
+ }
64
+ throw new ProjectRegistryError(400, "请求格式无效。");
65
+ }
66
+ /** A small account/project gateway. It never creates a worker, executes tools or mounts cwd. */
67
+ export async function startProjectControlServer(options) {
68
+ const registry = new ProjectRegistry(options.dataDir);
69
+ let proxy;
70
+ let manager;
71
+ let provider;
72
+ let server;
73
+ let shuttingDown = false;
74
+ try {
75
+ const auth = await createHubAuth({
76
+ dataDir: options.dataDir,
77
+ publicOrigin: options.publicOrigin,
78
+ onRevoke: (sessionId) => {
79
+ if (shuttingDown)
80
+ return;
81
+ void proxy?.revokeOwner(sessionId).catch(async () => {
82
+ if (shuttingDown)
83
+ return;
84
+ // The revoked browser's transports are already closed. If its inner
85
+ // session cannot be revoked, stop the single administrator's runtimes.
86
+ try {
87
+ await manager?.stopAll();
88
+ }
89
+ catch {
90
+ console.error("[project-control] could not confirm project termination after session revocation.");
91
+ }
92
+ });
93
+ },
94
+ });
95
+ let listeningOrigin = "";
96
+ const publicOrigin = () => options.publicOrigin ?? listeningOrigin;
97
+ provider =
98
+ options.provider ??
99
+ createDockerProjectProvider({
100
+ installationId: registry.installationId,
101
+ dataDir: options.dataDir,
102
+ image: options.runtimeImage,
103
+ });
104
+ manager = new ProjectManager({
105
+ registry,
106
+ provider,
107
+ publicOrigin,
108
+ isSessionActive: (session) => auth.store
109
+ .listSessions()
110
+ .some((item) => item.id === session.id && item.username === session.username),
111
+ revokeProject: async (id) => {
112
+ await proxy?.revokeProject(id);
113
+ },
114
+ });
115
+ const projects = manager;
116
+ proxy = createProjectRuntimeProxy({
117
+ auth,
118
+ publicOrigin,
119
+ resolveTarget: (id, session) => projects.resolveTarget(session, id),
120
+ onRevocationFailure: (id) => {
121
+ // Ordered shutdown below owns termination of every runtime. Do not
122
+ // launch background cleanup that could outlive registry ownership.
123
+ if (shuttingDown)
124
+ return;
125
+ void projects.stopAfterRevocationFailure(id).catch(() => {
126
+ console.error("[project-control] could not confirm project termination after runtime revocation failed.");
127
+ });
128
+ },
129
+ });
130
+ const transport = proxy;
131
+ await projects.reconcile();
132
+ const handle = async (request, response) => {
133
+ if (shuttingDown)
134
+ throw new ProjectRegistryError(503, "项目服务正在关闭。");
135
+ response.setHeader("X-Content-Type-Options", "nosniff");
136
+ response.setHeader("Referrer-Policy", "no-referrer");
137
+ const url = new URL(request.url ?? "/", "http://localhost");
138
+ const pathname = url.pathname;
139
+ if (pathname === "/health" && request.method === "GET") {
140
+ json(response, 200, { status: "ok", mode: "project-sandbox", runtime: "docker" });
141
+ return;
142
+ }
143
+ if (await auth.handle(request, response))
144
+ return;
145
+ if (await transport.handle(request, response))
146
+ return;
147
+ if (pathname === "/api/v1/projects" || pathname.startsWith("/api/v1/projects/")) {
148
+ if (!auth.isOriginAllowed(request))
149
+ throw new ProjectRegistryError(403, "请求来源无效。");
150
+ const session = await auth.authenticate(request);
151
+ if (!session)
152
+ throw new ProjectRegistryError(401, "请先登录。");
153
+ if (url.search)
154
+ throw new ProjectRegistryError(400, "项目请求参数无效。");
155
+ if (pathname === "/api/v1/projects" && request.method === "GET") {
156
+ const availability = await provider.availability();
157
+ json(response, 200, {
158
+ runtime: "docker",
159
+ ...availability,
160
+ projects: projects.list(session),
161
+ });
162
+ return;
163
+ }
164
+ if (pathname === "/api/v1/projects" && request.method === "POST") {
165
+ const body = await readBody(request);
166
+ if (Object.keys(body).some((key) => key !== "name"))
167
+ throw new ProjectRegistryError(400, "仅支持设置项目名称。");
168
+ json(response, 201, { project: projects.create(session, body.name) });
169
+ return;
170
+ }
171
+ const match = /^\/api\/v1\/projects\/([a-f0-9-]{36})(?:\/(start|stop))?$/.exec(pathname);
172
+ if (match && request.method === "GET" && !match[2]) {
173
+ json(response, 200, { project: projects.get(session, match[1]) });
174
+ return;
175
+ }
176
+ if (match?.[2] && request.method === "POST") {
177
+ const body = await readBody(request);
178
+ if (Object.keys(body).length)
179
+ throw new ProjectRegistryError(400, "启动和停止不接受额外参数。");
180
+ const project = match[2] === "start"
181
+ ? await projects.start(session, match[1])
182
+ : await projects.stop(session, match[1]);
183
+ json(response, 200, { project });
184
+ return;
185
+ }
186
+ throw new ProjectRegistryError(404, "找不到项目接口。");
187
+ }
188
+ if (pathname.startsWith("/api/") || pathname.startsWith("/p/") || pathname === "/ws") {
189
+ throw new ProjectRegistryError(404, "请先选择并启动项目。");
190
+ }
191
+ if (request.method !== "GET" && request.method !== "HEAD")
192
+ throw new ProjectRegistryError(405, "仅支持读取网页。");
193
+ const root = options.staticRootDir;
194
+ if (!root)
195
+ throw new ProjectRegistryError(404, "请先构建 Web 应用。");
196
+ let decoded;
197
+ try {
198
+ decoded = decodeURIComponent(pathname);
199
+ }
200
+ catch {
201
+ throw new ProjectRegistryError(400, "地址无效。");
202
+ }
203
+ let file = resolveSafe(root, decoded.replace(/^\//, ""));
204
+ if (!file && request.headers.accept?.includes("text/html"))
205
+ file = resolveSafe(root, "index.html");
206
+ if (!file)
207
+ throw new ProjectRegistryError(404, "找不到网页资源。");
208
+ const bytes = readFileSync(file);
209
+ response.writeHead(200, {
210
+ "Content-Type": contentTypeFor(extname(file)),
211
+ "Content-Length": bytes.length,
212
+ "Cache-Control": "no-cache",
213
+ });
214
+ response.end(request.method === "HEAD" ? undefined : bytes);
215
+ };
216
+ server = createServer((request, response) => {
217
+ void handle(request, response).catch((error) => {
218
+ if (response.headersSent) {
219
+ response.destroy();
220
+ return;
221
+ }
222
+ json(response, error instanceof ProjectRegistryError ? error.status : 503, {
223
+ error: error instanceof ProjectRegistryError
224
+ ? error.message
225
+ : "项目服务暂不可用,请稍后重试。",
226
+ });
227
+ });
228
+ });
229
+ server.requestTimeout = 180000;
230
+ server.headersTimeout = 10000;
231
+ server.on("upgrade", (request, socket, head) => {
232
+ if (shuttingDown) {
233
+ socket.destroy();
234
+ return;
235
+ }
236
+ void transport
237
+ .handleUpgrade(request, socket, head)
238
+ .then((handled) => {
239
+ if (!handled)
240
+ socket.destroy();
241
+ })
242
+ .catch(() => socket.destroy());
243
+ });
244
+ const listener = server;
245
+ await new Promise((resolve, reject) => {
246
+ listener.once("error", reject);
247
+ listener.listen(options.port, options.host, () => {
248
+ listener.off("error", reject);
249
+ resolve();
250
+ });
251
+ });
252
+ const address = listener.address();
253
+ if (!address || typeof address === "string")
254
+ throw new Error("Missing project listener address");
255
+ const host = ["0.0.0.0", "::"].includes(options.host) ? "127.0.0.1" : options.host;
256
+ listeningOrigin = `http://${host.includes(":") ? `[${host}]` : host}:${address.port}`;
257
+ let closing;
258
+ return {
259
+ url: listeningOrigin,
260
+ port: address.port,
261
+ bootstrapToken: auth.bootstrapToken,
262
+ generatedPasscode: undefined,
263
+ close() {
264
+ return (closing ??= (async () => {
265
+ shuttingDown = true;
266
+ const stopped = new Promise((resolve) => listener.close(() => resolve()));
267
+ try {
268
+ // Revoke streams and inner sessions before stopping their servers.
269
+ // If logout is unreachable, confirmed runtime termination is the
270
+ // fallback; a real stop failure must still reject close().
271
+ await transport.close().catch(() => { });
272
+ await projects.close();
273
+ }
274
+ finally {
275
+ listener.closeAllConnections();
276
+ try {
277
+ await stopped;
278
+ }
279
+ finally {
280
+ registry.close();
281
+ }
282
+ }
283
+ })());
284
+ },
285
+ };
286
+ }
287
+ catch (error) {
288
+ shuttingDown = true;
289
+ server?.close();
290
+ server?.closeAllConnections();
291
+ try {
292
+ await proxy?.close().catch(() => { });
293
+ await (manager ? manager.close() : provider?.close())?.catch(() => { });
294
+ }
295
+ finally {
296
+ registry.close();
297
+ }
298
+ throw error;
299
+ }
300
+ }
@@ -0,0 +1,33 @@
1
+ import type { ProjectRuntimeProvider, ProjectRuntimeRecord } from "./types.js";
2
+ export declare const DEFAULT_PROJECT_RUNTIME_IMAGE = "codeshell-project-runtime:local";
3
+ export declare const PROJECT_RUNTIME_SECRET_PATH = "/run/codeshell-project.json";
4
+ export declare const PROJECT_RUNTIME_LABEL = "io.codeshell.project-runtime";
5
+ export interface DockerCommandResult {
6
+ stdout: string;
7
+ stderr: string;
8
+ exitCode: number;
9
+ }
10
+ export interface DockerProjectProviderOptions {
11
+ installationId: string;
12
+ dataDir: string;
13
+ image?: string;
14
+ limits?: {
15
+ memoryMb?: number;
16
+ cpus?: number;
17
+ pids?: number;
18
+ };
19
+ commandTimeoutMs?: number;
20
+ startupTimeoutMs?: number;
21
+ /** Test seam; production always invokes Docker directly, without a shell. */
22
+ command?: (args: string[], options: {
23
+ signal: AbortSignal;
24
+ }) => Promise<DockerCommandResult>;
25
+ healthCheck?: (url: string, signal: AbortSignal) => Promise<boolean>;
26
+ }
27
+ export declare class ProjectRuntimeError extends Error {
28
+ constructor(message: string);
29
+ }
30
+ export declare function validateProjectRuntimeRecord(project: ProjectRuntimeRecord): void;
31
+ export declare function validateProjectPublicOrigin(value: string): string;
32
+ /** An administrator-owned Docker control client. Project tasks never receive this client. */
33
+ export declare function createDockerProjectProvider(options: DockerProjectProviderOptions): ProjectRuntimeProvider;