@cjhyy/code-shell-server 0.9.19 → 0.9.21
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/dist/index.panels.d.ts +1 -0
- package/dist/index.panels.js +1 -0
- package/dist/panels/bridge-contract.js +1 -0
- package/dist/panels/directory-bookmarks.d.ts +7 -0
- package/dist/panels/directory-bookmarks.js +141 -0
- package/dist/panels/http.d.ts +2 -2
- package/dist/panels/http.js +3 -3
- package/dist/panels/runtime.d.ts +2 -2
- package/dist/panels/runtime.js +291 -31
- package/dist/panels/tool-jobs.d.ts +2 -1
- package/dist/panels/tool-jobs.js +27 -1
- package/dist/serve/headless-server.js +2 -2
- package/package.json +5 -5
package/dist/index.panels.d.ts
CHANGED
package/dist/index.panels.js
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The guest receives only a random bookmark. A path saved in guest storage grants nothing. */
|
|
2
|
+
export declare class PanelAppDirectoryBookmarks {
|
|
3
|
+
private readonly file;
|
|
4
|
+
constructor(file: string);
|
|
5
|
+
remember(appId: string, projectPath: string, path: string): string;
|
|
6
|
+
restore(appId: string, projectPath: string, id: unknown): string;
|
|
7
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute } from "node:path";
|
|
4
|
+
import { acquireFileLock } from "@cjhyy/code-shell-core/internal";
|
|
5
|
+
const MAX_RECORDS = 256;
|
|
6
|
+
const MAX_BYTES = 256 * 1024;
|
|
7
|
+
const ID = /^[a-f0-9-]{36}$/i;
|
|
8
|
+
function valid(record) {
|
|
9
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
10
|
+
return false;
|
|
11
|
+
const item = record;
|
|
12
|
+
return (typeof item.id === "string" &&
|
|
13
|
+
ID.test(item.id) &&
|
|
14
|
+
typeof item.appId === "string" &&
|
|
15
|
+
/^[a-z][a-z0-9-]{0,63}$/.test(item.appId) &&
|
|
16
|
+
typeof item.projectPath === "string" &&
|
|
17
|
+
isAbsolute(item.projectPath) &&
|
|
18
|
+
typeof item.path === "string" &&
|
|
19
|
+
isAbsolute(item.path) &&
|
|
20
|
+
item.path.length <= 32_768 &&
|
|
21
|
+
item.projectPath.length <= 32_768 &&
|
|
22
|
+
Number.isSafeInteger(item.dev) &&
|
|
23
|
+
Number.isSafeInteger(item.ino) &&
|
|
24
|
+
Number.isSafeInteger(item.savedAt));
|
|
25
|
+
}
|
|
26
|
+
function read(file) {
|
|
27
|
+
let fd;
|
|
28
|
+
try {
|
|
29
|
+
const entry = lstatSync(file);
|
|
30
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_BYTES)
|
|
31
|
+
throw new Error("Panel directory bookmarks are not a bounded regular file");
|
|
32
|
+
fd = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
33
|
+
const opened = fstatSync(fd);
|
|
34
|
+
if (!opened.isFile() || opened.size > MAX_BYTES)
|
|
35
|
+
throw new Error("Panel directory bookmarks are not a bounded regular file");
|
|
36
|
+
const document = JSON.parse(readFileSync(fd, "utf8"));
|
|
37
|
+
if (!document || typeof document !== "object" || Array.isArray(document))
|
|
38
|
+
throw new Error("Invalid Panel directory bookmarks");
|
|
39
|
+
const value = document;
|
|
40
|
+
if (value.version !== 1 ||
|
|
41
|
+
!Array.isArray(value.bookmarks) ||
|
|
42
|
+
value.bookmarks.length > MAX_RECORDS ||
|
|
43
|
+
!value.bookmarks.every(valid))
|
|
44
|
+
throw new Error("Invalid Panel directory bookmarks");
|
|
45
|
+
return value.bookmarks;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error.code === "ENOENT")
|
|
49
|
+
return [];
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (fd !== undefined)
|
|
54
|
+
closeSync(fd);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function write(file, bookmarks) {
|
|
58
|
+
const parent = dirname(file);
|
|
59
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
60
|
+
const directory = lstatSync(parent);
|
|
61
|
+
if (!directory.isDirectory() || directory.isSymbolicLink())
|
|
62
|
+
throw new Error("Invalid Panel bookmark directory");
|
|
63
|
+
try {
|
|
64
|
+
const existing = lstatSync(file);
|
|
65
|
+
if (!existing.isFile() || existing.isSymbolicLink())
|
|
66
|
+
throw new Error("Invalid Panel bookmark file");
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code !== "ENOENT")
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
const serialized = JSON.stringify({ version: 1, bookmarks });
|
|
73
|
+
if (Buffer.byteLength(serialized) > MAX_BYTES)
|
|
74
|
+
throw new Error("Panel directory bookmarks are full");
|
|
75
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
76
|
+
try {
|
|
77
|
+
writeFileSync(temporary, serialized, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
78
|
+
renameSync(temporary, file);
|
|
79
|
+
if (process.platform !== "win32")
|
|
80
|
+
chmodSync(file, 0o600);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
rmSync(temporary, { force: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The guest receives only a random bookmark. A path saved in guest storage grants nothing. */
|
|
87
|
+
export class PanelAppDirectoryBookmarks {
|
|
88
|
+
file;
|
|
89
|
+
constructor(file) {
|
|
90
|
+
this.file = file;
|
|
91
|
+
}
|
|
92
|
+
remember(appId, projectPath, path) {
|
|
93
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(appId) || !isAbsolute(projectPath) || !isAbsolute(path))
|
|
94
|
+
throw new Error("Invalid Panel directory bookmark scope");
|
|
95
|
+
const info = lstatSync(path);
|
|
96
|
+
if (!info.isDirectory() || info.isSymbolicLink() || realpathSync(path) !== path)
|
|
97
|
+
throw new Error("Selected directory changed; choose it again");
|
|
98
|
+
const bookmark = {
|
|
99
|
+
id: randomUUID(),
|
|
100
|
+
appId,
|
|
101
|
+
projectPath,
|
|
102
|
+
path,
|
|
103
|
+
dev: info.dev,
|
|
104
|
+
ino: info.ino,
|
|
105
|
+
savedAt: Date.now(),
|
|
106
|
+
};
|
|
107
|
+
mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 });
|
|
108
|
+
const release = acquireFileLock(this.file);
|
|
109
|
+
try {
|
|
110
|
+
const previous = read(this.file);
|
|
111
|
+
write(this.file, [
|
|
112
|
+
bookmark,
|
|
113
|
+
...previous.filter((item) => !(item.appId === appId && item.projectPath === projectPath && item.path === path)),
|
|
114
|
+
].slice(0, MAX_RECORDS));
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
release();
|
|
118
|
+
}
|
|
119
|
+
return bookmark.id;
|
|
120
|
+
}
|
|
121
|
+
restore(appId, projectPath, id) {
|
|
122
|
+
if (typeof id !== "string" || !ID.test(id))
|
|
123
|
+
throw new Error("Invalid directory bookmark");
|
|
124
|
+
const bookmark = read(this.file).find((item) => item.id === id && item.appId === appId && item.projectPath === projectPath);
|
|
125
|
+
if (!bookmark)
|
|
126
|
+
throw new Error("Saved directory is unavailable; choose it again");
|
|
127
|
+
try {
|
|
128
|
+
const info = lstatSync(bookmark.path);
|
|
129
|
+
if (!info.isDirectory() ||
|
|
130
|
+
info.isSymbolicLink() ||
|
|
131
|
+
info.dev !== bookmark.dev ||
|
|
132
|
+
info.ino !== bookmark.ino ||
|
|
133
|
+
realpathSync(bookmark.path) !== bookmark.path)
|
|
134
|
+
throw new Error("Saved directory changed");
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
throw new Error("Saved directory changed; choose it again");
|
|
138
|
+
}
|
|
139
|
+
return bookmark.path;
|
|
140
|
+
}
|
|
141
|
+
}
|
package/dist/panels/http.d.ts
CHANGED
|
@@ -25,10 +25,10 @@ export declare function createPanelHttp(options: PanelManagementHttpOptions & {
|
|
|
25
25
|
};
|
|
26
26
|
activeTaskCount: () => number;
|
|
27
27
|
panelAction: (ownerId: string, sessionId: string, input: Record<string, unknown>) => Promise<unknown>;
|
|
28
|
-
invalidate: (appId?: string) => void
|
|
28
|
+
invalidate: (appId?: string) => Promise<void>;
|
|
29
29
|
ownsAssets: (request: IncomingMessage) => boolean;
|
|
30
30
|
handleAssets: (request: IncomingMessage, response: ServerResponse) => Promise<boolean>;
|
|
31
31
|
handle(request: IncomingMessage, response: ServerResponse): Promise<boolean>;
|
|
32
32
|
cancelOwner(owner: string): void;
|
|
33
|
-
close(): void
|
|
33
|
+
close(): Promise<void>;
|
|
34
34
|
};
|
package/dist/panels/http.js
CHANGED
|
@@ -6,7 +6,7 @@ export function createPanelHttp(options) {
|
|
|
6
6
|
...options,
|
|
7
7
|
compatibility: options.compatibility ?? panelWebCompatibility,
|
|
8
8
|
onChanged: async (id, kind) => {
|
|
9
|
-
runtime?.invalidate(id);
|
|
9
|
+
await runtime?.invalidate(id);
|
|
10
10
|
await options.onChanged?.(id, kind);
|
|
11
11
|
},
|
|
12
12
|
});
|
|
@@ -29,8 +29,8 @@ export function createPanelHttp(options) {
|
|
|
29
29
|
runtime.cancelOwner(owner);
|
|
30
30
|
management.cancelOwner(owner);
|
|
31
31
|
},
|
|
32
|
-
close() {
|
|
33
|
-
runtime.close();
|
|
32
|
+
async close() {
|
|
33
|
+
await runtime.close();
|
|
34
34
|
management.close();
|
|
35
35
|
},
|
|
36
36
|
};
|
package/dist/panels/runtime.d.ts
CHANGED
|
@@ -44,8 +44,8 @@ export declare function createPanelRuntime(options: PanelRuntimeOptions): {
|
|
|
44
44
|
panelAction(ownerId: string, sessionId: string, input: Record<string, unknown>): Promise<unknown>;
|
|
45
45
|
ownsAssets(request: IncomingMessage): boolean;
|
|
46
46
|
cancelOwner: (owner: string) => void;
|
|
47
|
-
invalidate(appId?: string): void
|
|
48
|
-
close(): Promise<
|
|
47
|
+
invalidate(appId?: string): Promise<void>;
|
|
48
|
+
close(): Promise<void>;
|
|
49
49
|
handle(request: IncomingMessage, response: ServerResponse): Promise<boolean>;
|
|
50
50
|
handleAssets(request: IncomingMessage, response: ServerResponse): Promise<boolean>;
|
|
51
51
|
};
|
package/dist/panels/runtime.js
CHANGED
|
@@ -4,10 +4,13 @@ import { lstat, mkdir, open, opendir, realpath } from "node:fs/promises";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, extname, join, resolve, sep } from "node:path";
|
|
6
6
|
import { listInstalledPanelApps, validateToolArgsStrict, } from "@cjhyy/code-shell-core";
|
|
7
|
-
import { panelRuntimeApiVersion, panelProcessMethods, panelResourceMethods, panelRuntimeCapabilities, PanelBridgeError, panelBridgeFailure, } from "./bridge-contract.js";
|
|
7
|
+
import { panelRuntimeApiVersion, panelProcessMethods, panelResourceMethods, panelToolJobMethods, panelRuntimeCapabilities, PanelBridgeError, panelBridgeFailure, } from "./bridge-contract.js";
|
|
8
8
|
import { PanelResourceService } from "./resources/service.js";
|
|
9
9
|
import { panelConnections, panelConnectionIds, materializePanelConnections, } from "./connections.js";
|
|
10
10
|
import { PanelRuntimeServices } from "./runtime-services.js";
|
|
11
|
+
import { PanelToolJobService, toolJobLimits, } from "./tool-jobs.js";
|
|
12
|
+
import { createPanelToolExecutor } from "./tool-executor.js";
|
|
13
|
+
import { PanelAppDirectoryBookmarks } from "./directory-bookmarks.js";
|
|
11
14
|
import { handlePanelProcessDirectory } from "./process-files.js";
|
|
12
15
|
import { PanelAppProcessService, panelExecutableDirectories, panelProcessInfo, } from "./process-service.js";
|
|
13
16
|
import { PanelManagementError } from "./management.js";
|
|
@@ -29,6 +32,7 @@ const METHODS = [
|
|
|
29
32
|
"notifications.send",
|
|
30
33
|
...panelProcessMethods,
|
|
31
34
|
...panelResourceMethods,
|
|
35
|
+
...panelToolJobMethods,
|
|
32
36
|
"credentials.connections.list",
|
|
33
37
|
"credentials.connections.authorizeProcess",
|
|
34
38
|
"agent.task.models",
|
|
@@ -244,7 +248,7 @@ function bridgeScript(id, origin) {
|
|
|
244
248
|
const requestId = String(++next);
|
|
245
249
|
const timer = setTimeout(() => {
|
|
246
250
|
pending.delete(requestId); reject(new Error("Panel request timed out"));
|
|
247
|
-
}, 60000);
|
|
251
|
+
}, method === "tasks.start" ? 30 * 60 * 1000 : 60000);
|
|
248
252
|
pending.set(requestId, { resolve, reject, timer });
|
|
249
253
|
try {
|
|
250
254
|
parent.postMessage({ type: "codeshell-panel:call", instanceId: id, requestId, method, params }, origin);
|
|
@@ -288,7 +292,7 @@ function bridgeScript(id, origin) {
|
|
|
288
292
|
call: ask,
|
|
289
293
|
callResult: (method, params) => ask(method, params).then(value => ({ ok: true, value }), error => ({ ok: false, error: { code: error.code || "OPERATION_FAILED", message: error.message, ...(typeof error.retryAfterMs === "number" ? { retryAfterMs: error.retryAfterMs } : {}) } })),
|
|
290
294
|
on: (name, listener) => {
|
|
291
|
-
if (!["context.changed", "process.output", "process.exit", "agent.task.changed", "media.job.changed"].includes(name) || typeof listener !== "function") {
|
|
295
|
+
if (!["context.changed", "process.output", "process.exit", "agent.task.changed", "tasks.changed", "media.job.changed"].includes(name) || typeof listener !== "function") {
|
|
292
296
|
throw new Error("Invalid event listener");
|
|
293
297
|
}
|
|
294
298
|
let list = listeners.get(name);
|
|
@@ -364,10 +368,36 @@ export function createPanelRuntime(options) {
|
|
|
364
368
|
let closed = false;
|
|
365
369
|
let generation = 0;
|
|
366
370
|
let nextGuest = 0;
|
|
371
|
+
let nextToolOwner = -1;
|
|
367
372
|
const byGuest = new Map();
|
|
373
|
+
const directoryBookmarks = new PanelAppDirectoryBookmarks(join(options.dataDir, "panel-web-directory-bookmarks.json"));
|
|
374
|
+
const panelDataDirectory = (appId) => join(options.dataDir, "panel-data", appId, digest(Buffer.from(options.bindingCwd ?? options.cwd)).slice(0, 24));
|
|
375
|
+
const toolOwners = new Map();
|
|
376
|
+
const jobOwners = new Map();
|
|
377
|
+
let toolJobs;
|
|
378
|
+
async function installedToolApp(scope) {
|
|
379
|
+
if (closed || scope.projectPath !== (options.bindingCwd ?? options.cwd))
|
|
380
|
+
throw new PanelBridgeError("REVOKED", "Tool task workspace is unavailable");
|
|
381
|
+
const panel = (await snapshot()).panels.find((candidate) => candidate.id === scope.appId);
|
|
382
|
+
const app = (await installed()).find((candidate) => candidate.id === scope.appId);
|
|
383
|
+
if (!panel?.enabled ||
|
|
384
|
+
panel.revision !== scope.revision ||
|
|
385
|
+
!app?.permissions.includes("process") ||
|
|
386
|
+
!app.permissions.includes("resources"))
|
|
387
|
+
throw new PanelBridgeError("REVOKED", "Installed tool task is no longer authorized");
|
|
388
|
+
return app;
|
|
389
|
+
}
|
|
368
390
|
const processes = new PanelAppProcessService({
|
|
369
391
|
approvalScope: "guest",
|
|
370
392
|
resolvePackageEntry: async (owner, name) => {
|
|
393
|
+
const taskScope = toolOwners.get(owner.guestId);
|
|
394
|
+
if (taskScope) {
|
|
395
|
+
const app = await installedToolApp(taskScope);
|
|
396
|
+
const entry = app.nativeEntries?.[name];
|
|
397
|
+
if (!entry)
|
|
398
|
+
throw new PanelBridgeError("REVOKED", "Installed tool entry is unavailable");
|
|
399
|
+
return { path: join(app.installPath, entry.entry), sha256: entry.sha256 };
|
|
400
|
+
}
|
|
371
401
|
const grant = byGuest.get(owner.guestId);
|
|
372
402
|
const entry = grant?.app.nativeEntries?.[name];
|
|
373
403
|
if (!grant ||
|
|
@@ -379,10 +409,15 @@ export function createPanelRuntime(options) {
|
|
|
379
409
|
},
|
|
380
410
|
extraPathDirectories: () => panelExecutableDirectories(join(options.dataDir, "panel-bin"), { home: homedir() }),
|
|
381
411
|
isOwnerAuthorized: async (owner) => {
|
|
412
|
+
const taskScope = toolOwners.get(owner.guestId);
|
|
413
|
+
if (taskScope)
|
|
414
|
+
return !!(await installedToolApp(taskScope).catch(() => null));
|
|
382
415
|
const grant = byGuest.get(owner.guestId);
|
|
383
416
|
return !!grant && (await authorized(grant));
|
|
384
417
|
},
|
|
385
418
|
confirmExecution: async (input) => {
|
|
419
|
+
if (toolOwners.has(input.guestId))
|
|
420
|
+
return true;
|
|
386
421
|
const grant = byGuest.get(input.guestId);
|
|
387
422
|
return (!!grant &&
|
|
388
423
|
(await confirm(grant, `允许 ${input.appTitle} 运行 ${input.executable}?`, `程序将在服务端运行,可使用当前运行用户的文件与网络权限。\n程序:${input.executablePath}`)));
|
|
@@ -391,6 +426,11 @@ export function createPanelRuntime(options) {
|
|
|
391
426
|
const resources = new PanelResourceService({
|
|
392
427
|
rootDirectory: join(options.dataDir, "panel-app-media"),
|
|
393
428
|
isScopeAuthorized: async (scope) => {
|
|
429
|
+
if (scope.projectPath === (options.bindingCwd ?? options.cwd)) {
|
|
430
|
+
const panel = (await snapshot()).panels.find((candidate) => candidate.id === scope.appId);
|
|
431
|
+
if (panel?.enabled && panel.permissions.includes("resources"))
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
394
434
|
for (const grant of grants.values())
|
|
395
435
|
if (grant.app.id === scope.appId &&
|
|
396
436
|
scope.projectPath === (options.bindingCwd ?? options.cwd) &&
|
|
@@ -407,6 +447,156 @@ export function createPanelRuntime(options) {
|
|
|
407
447
|
}, 60_000);
|
|
408
448
|
reaper.unref();
|
|
409
449
|
const preparing = new Set();
|
|
450
|
+
const preparingTools = new Set();
|
|
451
|
+
async function dispatchToolJobs(grant, method, params) {
|
|
452
|
+
if (!grant.app.permissions.includes("process") || !grant.app.permissions.includes("resources"))
|
|
453
|
+
error(403, "原生后台任务需要 process 和 resources 权限。");
|
|
454
|
+
const scope = {
|
|
455
|
+
appId: grant.app.id,
|
|
456
|
+
projectPath: options.bindingCwd ?? options.cwd,
|
|
457
|
+
revision: grant.revision,
|
|
458
|
+
};
|
|
459
|
+
const input = (params ?? {});
|
|
460
|
+
const service = getToolJobs();
|
|
461
|
+
if (method === "tasks.start") {
|
|
462
|
+
const app = await installedToolApp(scope);
|
|
463
|
+
const entry = app.nativeEntries?.[input.entry ?? ""];
|
|
464
|
+
if (!entry)
|
|
465
|
+
error(501, "安装的原生工具不可用。");
|
|
466
|
+
if (!(await confirm(grant, `启动 ${grant.app.title.default} 的后台工具?`, input.entry)))
|
|
467
|
+
error(403, "你取消了后台工具执行。");
|
|
468
|
+
if (!(await authorized(grant)))
|
|
469
|
+
error(410, "面板授权已失效。");
|
|
470
|
+
const preparation = {
|
|
471
|
+
owner: grant.owner,
|
|
472
|
+
appId: scope.appId,
|
|
473
|
+
controller: new AbortController(),
|
|
474
|
+
};
|
|
475
|
+
preparingTools.add(preparation);
|
|
476
|
+
try {
|
|
477
|
+
const job = await service.start(scope, {
|
|
478
|
+
entry: { name: input.entry, sha256: entry.sha256 },
|
|
479
|
+
input: input.input,
|
|
480
|
+
recovery: input.recovery ?? "manual",
|
|
481
|
+
requestKey: input.requestKey,
|
|
482
|
+
}, preparation.controller.signal);
|
|
483
|
+
if (!jobOwners.has(job.id))
|
|
484
|
+
jobOwners.set(job.id, { owner: grant.owner, scope });
|
|
485
|
+
if (!(await authorized(grant))) {
|
|
486
|
+
if (jobOwners.get(job.id)?.owner === grant.owner)
|
|
487
|
+
await service.cancel(scope, job.id);
|
|
488
|
+
error(410, "登录授权已撤销。");
|
|
489
|
+
}
|
|
490
|
+
emitTaskEvent(await service.get(scope, job.id));
|
|
491
|
+
return job;
|
|
492
|
+
}
|
|
493
|
+
catch (cause) {
|
|
494
|
+
if (preparation.controller.signal.aborted)
|
|
495
|
+
error(410, "登录授权已撤销,输入准备已取消。");
|
|
496
|
+
throw cause;
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
preparingTools.delete(preparation);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (method === "tasks.list") {
|
|
503
|
+
const offset = input.offset ?? 0, limit = input.limit ?? 50;
|
|
504
|
+
if (!Number.isSafeInteger(offset) ||
|
|
505
|
+
offset < 0 ||
|
|
506
|
+
!Number.isSafeInteger(limit) ||
|
|
507
|
+
limit < 1 ||
|
|
508
|
+
limit > 50)
|
|
509
|
+
throw new PanelBridgeError("INVALID_ARGUMENT", "Invalid task page");
|
|
510
|
+
return (await service.list(scope))
|
|
511
|
+
.slice(offset, offset + limit)
|
|
512
|
+
.map((job) => toolSummary(job));
|
|
513
|
+
}
|
|
514
|
+
if (method === "tasks.get")
|
|
515
|
+
return service.get(scope, input.id);
|
|
516
|
+
if (method === "tasks.cancel")
|
|
517
|
+
return service.cancel(scope, input.id);
|
|
518
|
+
if (method === "tasks.retry") {
|
|
519
|
+
if (!(await confirm(grant, `重试 ${grant.app.title.default} 的后台工具?`, input.id ?? "")))
|
|
520
|
+
error(403, "你取消了后台工具重试。");
|
|
521
|
+
if (!(await authorized(grant)))
|
|
522
|
+
error(410, "面板授权已失效。");
|
|
523
|
+
const job = await service.retry(scope, input.id);
|
|
524
|
+
jobOwners.set(job.id, { owner: grant.owner, scope });
|
|
525
|
+
if (!(await authorized(grant))) {
|
|
526
|
+
await service.cancel(scope, job.id);
|
|
527
|
+
error(410, "登录授权已撤销,后台工具已取消。");
|
|
528
|
+
}
|
|
529
|
+
emitTaskEvent(await service.get(scope, job.id));
|
|
530
|
+
return job;
|
|
531
|
+
}
|
|
532
|
+
throw new PanelBridgeError("NOT_SUPPORTED", "Unknown tool task operation");
|
|
533
|
+
}
|
|
534
|
+
function toolSummary(job) {
|
|
535
|
+
const { input: _input, result: _result, ...summary } = job;
|
|
536
|
+
return summary;
|
|
537
|
+
}
|
|
538
|
+
function emitTaskEvent(job) {
|
|
539
|
+
const owner = jobOwners.get(job.id)?.owner;
|
|
540
|
+
if (!owner)
|
|
541
|
+
return;
|
|
542
|
+
for (const grant of grants.values())
|
|
543
|
+
if (grant.owner === owner &&
|
|
544
|
+
grant.app.id === job.scope.appId &&
|
|
545
|
+
grant.revision === job.scope.revision &&
|
|
546
|
+
grant.app.permissions.includes("process") &&
|
|
547
|
+
grant.app.permissions.includes("resources"))
|
|
548
|
+
emit(grant, "tasks.changed", toolSummary(job));
|
|
549
|
+
if (["succeeded", "failed", "cancelled", "interrupted"].includes(job.status))
|
|
550
|
+
jobOwners.delete(job.id);
|
|
551
|
+
}
|
|
552
|
+
function getToolJobs() {
|
|
553
|
+
if (toolJobs)
|
|
554
|
+
return toolJobs;
|
|
555
|
+
const executor = createPanelToolExecutor({
|
|
556
|
+
processes,
|
|
557
|
+
resources,
|
|
558
|
+
owner: (job, send) => {
|
|
559
|
+
const guestId = nextToolOwner--;
|
|
560
|
+
toolOwners.set(guestId, job.scope);
|
|
561
|
+
return {
|
|
562
|
+
guestId,
|
|
563
|
+
appId: job.scope.appId,
|
|
564
|
+
appTitle: job.scope.appId,
|
|
565
|
+
revision: job.scope.revision,
|
|
566
|
+
send,
|
|
567
|
+
};
|
|
568
|
+
},
|
|
569
|
+
releaseOwner: (owner) => {
|
|
570
|
+
toolOwners.delete(owner.guestId);
|
|
571
|
+
},
|
|
572
|
+
authorize: async (scope) => {
|
|
573
|
+
await installedToolApp(scope);
|
|
574
|
+
},
|
|
575
|
+
authorizeConnections: async (scope) => {
|
|
576
|
+
const app = await installedToolApp(scope);
|
|
577
|
+
if (!app.permissions.includes("credentials.connections"))
|
|
578
|
+
throw new PanelBridgeError("PERMISSION_DENIED", "Tool requires connection permission");
|
|
579
|
+
},
|
|
580
|
+
appDataDirectory: async (scope) => {
|
|
581
|
+
await installedToolApp(scope);
|
|
582
|
+
const path = panelDataDirectory(scope.appId);
|
|
583
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
584
|
+
return path;
|
|
585
|
+
},
|
|
586
|
+
sealedRoot: join(options.dataDir, "panel-app-sealed"),
|
|
587
|
+
});
|
|
588
|
+
toolJobs = new PanelToolJobService({
|
|
589
|
+
// Desktop's IPC bridge has its own in-memory coordinator for panel-tool-jobs.
|
|
590
|
+
// A Desktop Web workspace must not open the same store concurrently.
|
|
591
|
+
rootDir: options.host === "desktop"
|
|
592
|
+
? join(options.dataDir, "panel-web-tool-jobs", digest(Buffer.from(options.cwd)).slice(0, 24))
|
|
593
|
+
: join(options.dataDir, "panel-tool-jobs"),
|
|
594
|
+
...executor,
|
|
595
|
+
isAuthorized: async (scope) => !!(await installedToolApp(scope).catch(() => null)),
|
|
596
|
+
onEvent: emitTaskEvent,
|
|
597
|
+
});
|
|
598
|
+
return toolJobs;
|
|
599
|
+
}
|
|
410
600
|
let readingSnapshot;
|
|
411
601
|
function snapshot() {
|
|
412
602
|
// Parallel module requests share current disk work, without a stale TTL
|
|
@@ -570,12 +760,18 @@ export function createPanelRuntime(options) {
|
|
|
570
760
|
};
|
|
571
761
|
}
|
|
572
762
|
function cancelOwner(owner) {
|
|
763
|
+
for (const preparation of preparingTools)
|
|
764
|
+
if (preparation.owner === owner)
|
|
765
|
+
preparation.controller.abort();
|
|
573
766
|
for (const preparation of preparing)
|
|
574
767
|
if (preparation.owner === owner)
|
|
575
768
|
preparation.cancelled = true;
|
|
576
769
|
for (const grant of grants.values())
|
|
577
770
|
if (grant.owner === owner)
|
|
578
771
|
remove(grant);
|
|
772
|
+
for (const [id, record] of jobOwners)
|
|
773
|
+
if (record.owner === owner)
|
|
774
|
+
void toolJobs?.cancel(record.scope, id).catch(() => { });
|
|
579
775
|
}
|
|
580
776
|
async function authorized(grant) {
|
|
581
777
|
if (closed ||
|
|
@@ -661,23 +857,48 @@ export function createPanelRuntime(options) {
|
|
|
661
857
|
theme: input.theme === "dark" ? "dark" : "light",
|
|
662
858
|
locale: typeof input.locale === "string" ? input.locale.slice(0, 32) : "zh-CN",
|
|
663
859
|
apiVersion: panelRuntimeApiVersion,
|
|
664
|
-
capabilities:
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
860
|
+
capabilities: {
|
|
861
|
+
...panelRuntimeCapabilities({
|
|
862
|
+
process: app.permissions.includes("process"),
|
|
863
|
+
resources: app.permissions.includes("resources") ? resources.capabilities() : undefined,
|
|
864
|
+
tasks: app.permissions.includes("process") && app.permissions.includes("resources")
|
|
865
|
+
? {
|
|
866
|
+
available: true,
|
|
867
|
+
...toolJobLimits,
|
|
868
|
+
maxHttpResultBytes: toolJobLimits.maxRecordBytes + 128 * 1024,
|
|
869
|
+
}
|
|
870
|
+
: undefined,
|
|
871
|
+
limits: {
|
|
872
|
+
maxParamsBytes: 3 * 1024 * 1024,
|
|
873
|
+
maxResultBytes: 3 * 1024 * 1024,
|
|
874
|
+
rateWindowMs: 60000,
|
|
875
|
+
maxCallsPerWindow: 240,
|
|
876
|
+
maxTransferCallsPerWindow: 2048,
|
|
877
|
+
callTimeoutMs: 60000,
|
|
878
|
+
consentTimeoutMs: 50000,
|
|
879
|
+
},
|
|
880
|
+
}),
|
|
881
|
+
...(app.permissions.includes("process") && app.permissions.includes("resources")
|
|
882
|
+
? {
|
|
883
|
+
methodLimits: {
|
|
884
|
+
"tasks.start": {
|
|
885
|
+
maxParamsBytes: toolJobLimits.maxInputBytes,
|
|
886
|
+
maxResultBytes: toolJobLimits.maxRecordBytes + 128 * 1024,
|
|
887
|
+
timeoutMs: 30 * 60 * 1000,
|
|
888
|
+
},
|
|
889
|
+
"tasks.get": { maxResultBytes: toolJobLimits.maxRecordBytes + 128 * 1024 },
|
|
890
|
+
"tasks.retry": { maxResultBytes: toolJobLimits.maxRecordBytes + 128 * 1024 },
|
|
891
|
+
"tasks.cancel": { maxResultBytes: toolJobLimits.maxRecordBytes + 128 * 1024 },
|
|
892
|
+
},
|
|
893
|
+
}
|
|
894
|
+
: {}),
|
|
895
|
+
},
|
|
677
896
|
host: options.host,
|
|
678
897
|
availableMethods: METHODS.filter((method) => {
|
|
679
898
|
if (method.startsWith("agent.task.") && !agentTasks)
|
|
680
899
|
return false;
|
|
900
|
+
if (method.startsWith("tasks."))
|
|
901
|
+
return app.permissions.includes("process") && app.permissions.includes("resources");
|
|
681
902
|
if ([
|
|
682
903
|
"resources.materialize",
|
|
683
904
|
"resources.capture",
|
|
@@ -767,6 +988,8 @@ export function createPanelRuntime(options) {
|
|
|
767
988
|
grant.calls = history;
|
|
768
989
|
if (method === "context.get")
|
|
769
990
|
return grant.context;
|
|
991
|
+
if (method.startsWith("tasks."))
|
|
992
|
+
return dispatchToolJobs(grant, method, params);
|
|
770
993
|
if (method.startsWith("tools.")) {
|
|
771
994
|
const name = params?.name;
|
|
772
995
|
if (typeof name !== "string" || !grant.app.agent?.tools.some((tool) => tool.name === name))
|
|
@@ -865,23 +1088,40 @@ export function createPanelRuntime(options) {
|
|
|
865
1088
|
url: `${ROOT}${grant.id}/directory/${handle}`,
|
|
866
1089
|
};
|
|
867
1090
|
}
|
|
1091
|
+
if (method === "filesystem.restoreDirectory") {
|
|
1092
|
+
const saved = directoryBookmarks.restore(grant.app.id, options.bindingCwd ?? options.cwd, params?.bookmark);
|
|
1093
|
+
if (saved !== (await realpath(join(options.cwd, "downloads"))))
|
|
1094
|
+
throw new PanelBridgeError("PERMISSION_DENIED", "Saved server directory is unavailable");
|
|
1095
|
+
const restored = await processes.grantDirectory(owner, saved);
|
|
1096
|
+
directoryBookmarks.restore(grant.app.id, options.bindingCwd ?? options.cwd, params?.bookmark);
|
|
1097
|
+
return { ...restored, bookmark: params.bookmark };
|
|
1098
|
+
}
|
|
868
1099
|
const name = method === "filesystem.pickDirectory" ? "downloads" : params?.name;
|
|
869
|
-
const directory = name === "
|
|
870
|
-
?
|
|
871
|
-
: name === "
|
|
872
|
-
? join(options.
|
|
873
|
-
: name === "
|
|
874
|
-
? join(options.dataDir, "panel-
|
|
875
|
-
:
|
|
1100
|
+
const directory = name === "project"
|
|
1101
|
+
? options.cwd
|
|
1102
|
+
: name === "downloads"
|
|
1103
|
+
? join(options.cwd, "downloads")
|
|
1104
|
+
: name === "user-bin"
|
|
1105
|
+
? join(options.dataDir, "panel-bin")
|
|
1106
|
+
: name === "app-data"
|
|
1107
|
+
? panelDataDirectory(grant.app.id)
|
|
1108
|
+
: undefined;
|
|
876
1109
|
if (!directory)
|
|
877
1110
|
error(400, "不支持这个服务端目录。");
|
|
878
1111
|
if (method === "filesystem.pickDirectory" &&
|
|
879
1112
|
!(await confirm(grant, "使用服务端下载目录?", directory)))
|
|
880
1113
|
return null;
|
|
881
|
-
|
|
1114
|
+
if (name !== "project")
|
|
1115
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
882
1116
|
if (!(await authorized(grant)))
|
|
883
1117
|
error(410, "面板授权已失效。");
|
|
884
|
-
|
|
1118
|
+
const selected = await processes.grantDirectory(owner, directory);
|
|
1119
|
+
if (method !== "filesystem.pickDirectory")
|
|
1120
|
+
return selected;
|
|
1121
|
+
const bookmark = directoryBookmarks.remember(grant.app.id, options.bindingCwd ?? options.cwd, selected.path);
|
|
1122
|
+
if (!(await authorized(grant)))
|
|
1123
|
+
error(410, "面板授权已失效。");
|
|
1124
|
+
return { ...selected, bookmark };
|
|
885
1125
|
}
|
|
886
1126
|
if (["external.open", "agent.submitPrompt", "notifications.send"].includes(method)) {
|
|
887
1127
|
if (!grant.app.permissions.includes(method))
|
|
@@ -935,7 +1175,7 @@ export function createPanelRuntime(options) {
|
|
|
935
1175
|
}, method, params);
|
|
936
1176
|
}
|
|
937
1177
|
return {
|
|
938
|
-
activeTaskCount: () => agentTasks?.activeTaskCount?.() ?? 0,
|
|
1178
|
+
activeTaskCount: () => (agentTasks?.activeTaskCount?.() ?? 0) + (toolJobs?.activeCount() ?? 0),
|
|
939
1179
|
async panelAction(ownerId, sessionId, input) {
|
|
940
1180
|
const candidates = [];
|
|
941
1181
|
for (const grant of grants.values()) {
|
|
@@ -967,20 +1207,36 @@ export function createPanelRuntime(options) {
|
|
|
967
1207
|
cancelOwner,
|
|
968
1208
|
invalidate(appId) {
|
|
969
1209
|
generation++;
|
|
1210
|
+
for (const preparation of preparingTools)
|
|
1211
|
+
if (!appId || preparation.appId === appId)
|
|
1212
|
+
preparation.controller.abort();
|
|
970
1213
|
for (const grant of grants.values())
|
|
971
1214
|
if (!appId || grant.app.id === appId)
|
|
972
1215
|
remove(grant);
|
|
1216
|
+
if (!toolJobs)
|
|
1217
|
+
return Promise.resolve();
|
|
1218
|
+
const appIds = appId
|
|
1219
|
+
? [appId]
|
|
1220
|
+
: [...new Set([...jobOwners.values()].map((record) => record.scope.appId))];
|
|
1221
|
+
return Promise.all(appIds.map((id) => toolJobs.cancelApp(id))).then(() => undefined);
|
|
973
1222
|
},
|
|
974
|
-
close() {
|
|
1223
|
+
async close() {
|
|
975
1224
|
closed = true;
|
|
1225
|
+
for (const preparation of preparingTools)
|
|
1226
|
+
preparation.controller.abort();
|
|
976
1227
|
clearInterval(reaper);
|
|
977
1228
|
for (const grant of grants.values())
|
|
978
1229
|
remove(grant);
|
|
979
|
-
processes.close();
|
|
980
1230
|
assets.clear();
|
|
981
1231
|
for (const preparation of preparing)
|
|
982
1232
|
preparation.cancelled = true;
|
|
983
|
-
|
|
1233
|
+
try {
|
|
1234
|
+
await toolJobs?.shutdown();
|
|
1235
|
+
}
|
|
1236
|
+
finally {
|
|
1237
|
+
processes.close();
|
|
1238
|
+
await Promise.all([resources.shutdown(), Promise.resolve(agentTasks?.close())]);
|
|
1239
|
+
}
|
|
984
1240
|
},
|
|
985
1241
|
async handle(request, response) {
|
|
986
1242
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
@@ -1075,10 +1331,14 @@ export function createPanelRuntime(options) {
|
|
|
1075
1331
|
json(response, 200, { accepted: true });
|
|
1076
1332
|
}
|
|
1077
1333
|
else if (request.method === "POST" && match[2] === "call") {
|
|
1078
|
-
const
|
|
1334
|
+
const input = await body(request);
|
|
1335
|
+
const value = await call(grant, input);
|
|
1079
1336
|
if (!(await authorized(grant)))
|
|
1080
1337
|
error(410, "面板授权已失效,请重新打开。");
|
|
1081
|
-
|
|
1338
|
+
const resultLimit = typeof input.method === "string" && input.method.startsWith("tasks.")
|
|
1339
|
+
? toolJobLimits.maxRecordBytes + 128 * 1024
|
|
1340
|
+
: 3 * 1024 * 1024;
|
|
1341
|
+
if (Buffer.byteLength(JSON.stringify(value) ?? "null") > resultLimit)
|
|
1082
1342
|
throw new PanelBridgeError("RESULT_TOO_LARGE", "Panel result exceeds the response limit");
|
|
1083
1343
|
json(response, 200, value);
|
|
1084
1344
|
}
|
|
@@ -78,6 +78,7 @@ export declare class PanelToolJobService {
|
|
|
78
78
|
private pumping;
|
|
79
79
|
private shutdownPromise?;
|
|
80
80
|
constructor(options: PanelToolJobServiceOptions);
|
|
81
|
+
activeCount(): number;
|
|
81
82
|
private now;
|
|
82
83
|
private exclusive;
|
|
83
84
|
private authorize;
|
|
@@ -91,7 +92,7 @@ export declare class PanelToolJobService {
|
|
|
91
92
|
get(rawScope: ToolJobScope, id: string): Promise<ToolJob & {
|
|
92
93
|
readOnly: boolean;
|
|
93
94
|
}>;
|
|
94
|
-
start(rawScope: ToolJobScope, request: ToolJobRequest): Promise<ToolJob>;
|
|
95
|
+
start(rawScope: ToolJobScope, request: ToolJobRequest, signal?: AbortSignal): Promise<ToolJob>;
|
|
95
96
|
private schedule;
|
|
96
97
|
private pump;
|
|
97
98
|
private run;
|
package/dist/panels/tool-jobs.js
CHANGED
|
@@ -110,6 +110,10 @@ export class PanelToolJobService {
|
|
|
110
110
|
this.options = options;
|
|
111
111
|
this.storage = new ToolJobStorage(options.rootDir);
|
|
112
112
|
}
|
|
113
|
+
activeCount() {
|
|
114
|
+
return (this.preparing.size +
|
|
115
|
+
[...this.jobs.values()].filter((job) => !TERMINAL.has(job.status)).length);
|
|
116
|
+
}
|
|
113
117
|
now() {
|
|
114
118
|
return this.options.now?.() ?? Date.now();
|
|
115
119
|
}
|
|
@@ -204,7 +208,8 @@ export class PanelToolJobService {
|
|
|
204
208
|
const job = this.lookup(scope, id);
|
|
205
209
|
return { ...publicJob(job), readOnly: job.scope.revision !== scope.revision };
|
|
206
210
|
}
|
|
207
|
-
async start(rawScope, request) {
|
|
211
|
+
async start(rawScope, request, signal) {
|
|
212
|
+
signal?.throwIfAborted();
|
|
208
213
|
const scope = scopeValue(rawScope);
|
|
209
214
|
const entry = entryValue(request?.entry);
|
|
210
215
|
if (!["manual", "retry"].includes(request.recovery))
|
|
@@ -228,6 +233,7 @@ export class PanelToolJobService {
|
|
|
228
233
|
await this.authorize(scope);
|
|
229
234
|
let done;
|
|
230
235
|
const reservation = await this.exclusive(async () => {
|
|
236
|
+
signal?.throwIfAborted();
|
|
231
237
|
if (this.stopping)
|
|
232
238
|
throw new Error("tool job service is shutting down");
|
|
233
239
|
if (request.requestKey) {
|
|
@@ -264,8 +270,14 @@ export class PanelToolJobService {
|
|
|
264
270
|
if (reservation.existing)
|
|
265
271
|
return publicJob(reservation.existing);
|
|
266
272
|
const { id, controller } = reservation;
|
|
273
|
+
const abort = () => controller.abort(signal?.reason);
|
|
274
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
275
|
+
if (signal?.aborted)
|
|
276
|
+
abort();
|
|
267
277
|
try {
|
|
278
|
+
controller.signal.throwIfAborted();
|
|
268
279
|
const workDir = await this.storage.directory(id, true, true);
|
|
280
|
+
controller.signal.throwIfAborted();
|
|
269
281
|
const prepared = this.options.prepareInput
|
|
270
282
|
? await this.options.prepareInput(scope, input, workDir, controller.signal)
|
|
271
283
|
: input;
|
|
@@ -292,6 +304,19 @@ export class PanelToolJobService {
|
|
|
292
304
|
[...this.jobs.values()].some((other) => sameScope(other.scope, scope) && other.requestKey === request.requestKey))
|
|
293
305
|
throw new Error("tool job request key is already being prepared");
|
|
294
306
|
await this.commit(job);
|
|
307
|
+
// Cancellation can arrive during the durable write. The serial fence
|
|
308
|
+
// prevents the scheduler from selecting this job before cancellation.
|
|
309
|
+
if (controller.signal.aborted) {
|
|
310
|
+
job.status = "cancelled";
|
|
311
|
+
job.completedAt = this.now();
|
|
312
|
+
job.error = {
|
|
313
|
+
code: "CANCELLED",
|
|
314
|
+
message: "Task was cancelled before execution.",
|
|
315
|
+
retryable: job.recovery === "retry",
|
|
316
|
+
};
|
|
317
|
+
await this.commit(job);
|
|
318
|
+
controller.signal.throwIfAborted();
|
|
319
|
+
}
|
|
295
320
|
});
|
|
296
321
|
this.schedule();
|
|
297
322
|
return publicJob(job);
|
|
@@ -303,6 +328,7 @@ export class PanelToolJobService {
|
|
|
303
328
|
throw error;
|
|
304
329
|
}
|
|
305
330
|
finally {
|
|
331
|
+
signal?.removeEventListener("abort", abort);
|
|
306
332
|
this.preparing.delete(id);
|
|
307
333
|
done();
|
|
308
334
|
}
|
|
@@ -917,7 +917,7 @@ export async function startHeadlessServer(opts) {
|
|
|
917
917
|
await mcp?.close();
|
|
918
918
|
configuration?.close();
|
|
919
919
|
links?.close();
|
|
920
|
-
panels?.close();
|
|
920
|
+
await panels?.close();
|
|
921
921
|
files?.close();
|
|
922
922
|
bridge.kill();
|
|
923
923
|
wss.close();
|
|
@@ -947,7 +947,7 @@ export async function startHeadlessServer(opts) {
|
|
|
947
947
|
await mcp?.close();
|
|
948
948
|
configuration?.close();
|
|
949
949
|
links?.close();
|
|
950
|
-
panels?.close();
|
|
950
|
+
await panels?.close();
|
|
951
951
|
files?.close();
|
|
952
952
|
for (const tab of tabs) {
|
|
953
953
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cjhyy/code-shell-server",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.21",
|
|
4
4
|
"description": "CodeShell server transport layer — mobile remote HTTP/WS host, pairing, passcode gate, tunnel, rooms, uploads, and session/attachment services. Electron-free; precursor of the auth gateway.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -51,13 +51,13 @@
|
|
|
51
51
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@cjhyy/code-shell-capability-coding": "0.9.
|
|
55
|
-
"@cjhyy/code-shell-core": "0.9.
|
|
56
|
-
"@cjhyy/code-shell-web": "0.9.
|
|
54
|
+
"@cjhyy/code-shell-capability-coding": "0.9.21",
|
|
55
|
+
"@cjhyy/code-shell-core": "0.9.21",
|
|
56
|
+
"@cjhyy/code-shell-web": "0.9.21",
|
|
57
57
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
58
58
|
"@types/ws": "^8.18.1",
|
|
59
59
|
"ws": "^8.21.0",
|
|
60
|
-
"@cjhyy/code-shell-link": "0.9.
|
|
60
|
+
"@cjhyy/code-shell-link": "0.9.21"
|
|
61
61
|
},
|
|
62
62
|
"engines": {
|
|
63
63
|
"node": ">=20.10"
|