@cjhyy/code-shell-server 0.9.22 → 0.9.25
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 +45 -1
- package/dist/bin/code-shell-settings-recovery.d.ts +2 -0
- package/dist/bin/code-shell-settings-recovery.js +4 -0
- package/dist/desktop-web/http-api.js +14 -0
- package/dist/environment-identity.d.ts +4 -0
- package/dist/environment-identity.js +67 -0
- package/dist/hub/auth-store.d.ts +2 -0
- package/dist/hub/auth-store.js +7 -0
- package/dist/index.auth.d.ts +3 -0
- package/dist/index.auth.js +2 -0
- package/dist/index.links.d.ts +1 -0
- package/dist/index.links.js +1 -0
- package/dist/index.panels.d.ts +7 -0
- package/dist/index.panels.js +7 -0
- package/dist/index.remote-relay.d.ts +5 -0
- package/dist/index.remote-relay.js +6 -0
- package/dist/index.serve.d.ts +2 -0
- package/dist/index.serve.js +2 -0
- package/dist/links/http.d.ts +3 -0
- package/dist/links/http.js +11 -2
- package/dist/links/remote-configuration.d.ts +4 -0
- package/dist/links/remote-configuration.js +32 -0
- package/dist/links/service.d.ts +6 -1
- package/dist/links/service.js +241 -19
- package/dist/mobile-remote/mobile-upload-service.d.ts +4 -0
- package/dist/mobile-remote/mobile-upload-service.js +69 -11
- package/dist/mobile-remote/remote-host-manager.d.ts +10 -5
- package/dist/mobile-remote/remote-host-manager.js +53 -5
- package/dist/panels/agent-task-service.js +9 -2
- package/dist/panels/automations.d.ts +49 -0
- package/dist/panels/automations.js +134 -0
- package/dist/panels/bridge-contract.d.ts +5 -3
- package/dist/panels/bridge-contract.js +11 -1
- package/dist/panels/directory-bookmarks.d.ts +9 -1
- package/dist/panels/directory-bookmarks.js +64 -5
- package/dist/panels/execution-gate.d.ts +26 -0
- package/dist/panels/execution-gate.js +70 -0
- package/dist/panels/http.d.ts +26 -2
- package/dist/panels/http.js +7 -1
- package/dist/panels/hub-automations.d.ts +31 -0
- package/dist/panels/hub-automations.js +352 -0
- package/dist/panels/management-http.d.ts +20 -2
- package/dist/panels/management-http.js +15 -2
- package/dist/panels/management.d.ts +26 -4
- package/dist/panels/management.js +382 -39
- package/dist/panels/package-mutation.d.ts +8 -0
- package/dist/panels/package-mutation.js +23 -0
- package/dist/panels/process-service.d.ts +3 -0
- package/dist/panels/process-service.js +77 -4
- package/dist/panels/resources/clone.d.ts +13 -0
- package/dist/panels/resources/clone.js +99 -0
- package/dist/panels/resources/http.d.ts +11 -0
- package/dist/panels/resources/http.js +74 -0
- package/dist/panels/resources/references.d.ts +5 -0
- package/dist/panels/resources/references.js +26 -0
- package/dist/panels/resources/service.js +63 -27
- package/dist/panels/runtime-services.js +16 -3
- package/dist/panels/runtime.d.ts +16 -4
- package/dist/panels/runtime.js +522 -64
- package/dist/panels/shared-tool-jobs.d.ts +40 -0
- package/dist/panels/shared-tool-jobs.js +37 -0
- package/dist/panels/storage-store.d.ts +21 -0
- package/dist/panels/storage-store.js +43 -0
- package/dist/panels/task-cookie-host.d.ts +26 -0
- package/dist/panels/task-cookie-host.js +145 -0
- package/dist/panels/task-cookies.d.ts +51 -0
- package/dist/panels/task-cookies.js +183 -0
- package/dist/panels/tool-executor.d.ts +21 -5
- package/dist/panels/tool-executor.js +171 -30
- package/dist/panels/tool-jobs-storage.d.ts +2 -0
- package/dist/panels/tool-jobs-storage.js +34 -0
- package/dist/panels/tool-jobs.d.ts +49 -3
- package/dist/panels/tool-jobs.js +270 -15
- package/dist/panels/types.d.ts +43 -0
- package/dist/project-runtime/backup.d.ts +26 -0
- package/dist/project-runtime/backup.js +500 -0
- package/dist/project-runtime/control-server.d.ts +4 -0
- package/dist/project-runtime/control-server.js +19 -1
- package/dist/project-runtime/docker-provider.d.ts +4 -0
- package/dist/project-runtime/docker-provider.js +16 -1
- package/dist/project-runtime/managed-entry.js +4 -0
- package/dist/project-runtime/registry.d.ts +2 -0
- package/dist/project-runtime/registry.js +15 -0
- package/dist/project-runtime/seccomp.d.ts +8 -0
- package/dist/project-runtime/seccomp.js +72 -0
- package/dist/project-runtime/types.d.ts +2 -0
- package/dist/remote-relay/byte-stream.d.ts +9 -0
- package/dist/remote-relay/byte-stream.js +71 -0
- package/dist/remote-relay/connector.d.ts +27 -0
- package/dist/remote-relay/connector.js +197 -0
- package/dist/remote-relay/local-stream.d.ts +10 -0
- package/dist/remote-relay/local-stream.js +120 -0
- package/dist/remote-relay/protocol.d.ts +39 -0
- package/dist/remote-relay/protocol.js +64 -0
- package/dist/serve/automation-worker.d.ts +16 -0
- package/dist/serve/automation-worker.js +126 -0
- package/dist/serve/cli.d.ts +3 -0
- package/dist/serve/cli.js +17 -0
- package/dist/serve/headless-server.d.ts +3 -0
- package/dist/serve/headless-server.js +140 -10
- package/dist/serve/settings-recovery-cli.d.ts +29 -0
- package/dist/serve/settings-recovery-cli.js +128 -0
- package/dist/session-http-responses.d.ts +7 -0
- package/dist/session-http-responses.js +40 -0
- package/dist/sessions-service.d.ts +6 -2
- package/dist/sessions-service.js +23 -4
- package/dist/worker-bridge-core.d.ts +5 -0
- package/dist/worker-bridge-core.js +23 -0
- package/package.json +15 -6
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { panelExecutionGate } from "./execution-gate.js";
|
|
2
|
+
import { panelPackageMutationMatches } from "./package-mutation.js";
|
|
1
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { lstatSync, mkdirSync } from "node:fs";
|
|
3
|
-
import { basename, join, resolve } from "node:path";
|
|
4
|
-
import { SettingsManager, PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, discoverGitPanelApps, installReviewedLocalPanelApp, invalidateSkillCache, listInstalledPanelApps,
|
|
4
|
+
import { lstatSync, mkdirSync, statSync } from "node:fs";
|
|
5
|
+
import { basename, extname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { SettingsManager, PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, discoverGitPanelApps, installReviewedLocalPanelApp, invalidateSkillCache, listInstalledPanelApps, inspectProjectPanelApps, listRetainedPanelAppPackages, resolvePanelAppPackage, parsePanelAppPackagePins, retainInstalledPanelApp, userHome, panelAppsRoot, previewLocalPanelApp, resolvePanelAppBindingPolicy, uninstallPanelApp, } from "@cjhyy/code-shell-core";
|
|
5
7
|
import { mutateJsonFile } from "@cjhyy/code-shell-core/internal";
|
|
6
8
|
import { getRefCommit, parseGithubUrl } from "@cjhyy/code-shell-core/internal/skills";
|
|
7
9
|
const REVIEW_TTL = 8 * 60_000;
|
|
@@ -154,6 +156,7 @@ export function createPanelManagement(options) {
|
|
|
154
156
|
const workspace = resolve(options.bindingCwd ?? options.cwd);
|
|
155
157
|
const hasProject = workspace !== resolve(join(userHome(), ".code-shell", "no-repo"));
|
|
156
158
|
const reviews = new Map();
|
|
159
|
+
const restores = new Map();
|
|
157
160
|
const generations = new Map();
|
|
158
161
|
let closed = false;
|
|
159
162
|
let networkBusy = 0;
|
|
@@ -174,22 +177,49 @@ export function createPanelManagement(options) {
|
|
|
174
177
|
.update(JSON.stringify({
|
|
175
178
|
app,
|
|
176
179
|
digest,
|
|
180
|
+
...(options.projectPackages
|
|
181
|
+
? { pin: parsePanelAppPackagePins(project ?? projectSettings())[app.id] ?? null }
|
|
182
|
+
: {}),
|
|
177
183
|
bound: current.boundApps.has(app.id),
|
|
178
184
|
disabled: current.globalDisabledApps.has(app.id),
|
|
179
185
|
}))
|
|
180
186
|
.digest("hex");
|
|
181
187
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
function projectSettings() {
|
|
189
|
+
return new SettingsManager(workspace, "full").getRawForScope("project", workspace, {
|
|
190
|
+
strict: true,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// Compare the app-specific state under the project settings lock after package
|
|
194
|
+
// installation. Installing bytes must never overwrite another device's binding.
|
|
195
|
+
function projectState(id, project = projectSettings()) {
|
|
196
|
+
const current = policy(project);
|
|
197
|
+
return JSON.stringify({
|
|
198
|
+
bound: current.boundApps.has(id),
|
|
199
|
+
disabled: current.globalDisabledApps.has(id),
|
|
200
|
+
pin: options.projectPackages ? (parsePanelAppPackagePins(project)[id] ?? null) : null,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const listApps = () => options.projectPackages
|
|
204
|
+
? inspectProjectPanelApps(workspace).then((result) => result.apps)
|
|
205
|
+
: listInstalledPanelApps();
|
|
206
|
+
async function inspectApp(app) {
|
|
207
|
+
const info = lstatSync(app.installPath);
|
|
188
208
|
if (info.isSymbolicLink() || !info.isDirectory())
|
|
189
209
|
throw new PanelManagementError(403, "unsafe_path", "面板安装目录不支持链接。");
|
|
190
|
-
const preview = await previewLocalPanelApp({ kind: "dir", path:
|
|
210
|
+
const preview = await previewLocalPanelApp({ kind: "dir", path: app.installPath });
|
|
191
211
|
return { app, digest: preview.reviewToken };
|
|
192
212
|
}
|
|
213
|
+
async function selectedApp(id) {
|
|
214
|
+
const app = (await listApps()).find((item) => item.id === id);
|
|
215
|
+
if (!app)
|
|
216
|
+
throw new PanelManagementError(404, "not_found", "找不到这个面板,请刷新列表。");
|
|
217
|
+
return inspectApp(app);
|
|
218
|
+
}
|
|
219
|
+
async function catalogState(id) {
|
|
220
|
+
const app = (await listInstalledPanelApps()).find((item) => item.id === id);
|
|
221
|
+
return JSON.stringify(app ? await inspectApp(app) : null);
|
|
222
|
+
}
|
|
193
223
|
function savedSource(app, saved) {
|
|
194
224
|
if (typeof app.source === "string")
|
|
195
225
|
return;
|
|
@@ -207,19 +237,41 @@ export function createPanelManagement(options) {
|
|
|
207
237
|
commit: SHA.test(parsed.source.ref ?? "") ? parsed.source.ref : "",
|
|
208
238
|
};
|
|
209
239
|
}
|
|
210
|
-
async function snapshot() {
|
|
240
|
+
async function snapshot(appId) {
|
|
241
|
+
if (appId !== undefined)
|
|
242
|
+
assertSafePanelAppId(appId);
|
|
243
|
+
options.assertBinding?.();
|
|
244
|
+
const inspected = options.projectPackages
|
|
245
|
+
? await inspectProjectPanelApps(workspace, appId)
|
|
246
|
+
: undefined;
|
|
247
|
+
const apps = inspected?.apps ?? (await listInstalledPanelApps(appId));
|
|
211
248
|
options.assertBinding?.();
|
|
212
|
-
const
|
|
249
|
+
const project = projectSettings();
|
|
250
|
+
const current = policy(project);
|
|
213
251
|
const saved = origins();
|
|
214
252
|
const panels = [];
|
|
215
|
-
|
|
216
|
-
|
|
253
|
+
const issues = (inspected?.issues ?? []).map((issue) => ({
|
|
254
|
+
id: issue.id,
|
|
255
|
+
code: issue.code,
|
|
256
|
+
version: issue.pin?.version,
|
|
257
|
+
packageDigest: issue.pin?.packageDigest,
|
|
258
|
+
bound: current.boundApps.has(issue.id),
|
|
259
|
+
globalDisabled: current.globalDisabledApps.has(issue.id),
|
|
260
|
+
revision: createHash("sha256")
|
|
261
|
+
.update(JSON.stringify({
|
|
262
|
+
unavailable: issue.selectionKey,
|
|
263
|
+
state: projectState(issue.id, project),
|
|
264
|
+
}))
|
|
265
|
+
.digest("hex"),
|
|
266
|
+
}));
|
|
267
|
+
for (const listed of apps) {
|
|
268
|
+
const { app, digest } = await inspectApp(listed);
|
|
217
269
|
const source = savedSource(app, saved);
|
|
218
270
|
const { installPath: _path, source: _source, installedAt: _installedAt, lastUpdated: _lastUpdated, ...publicApp } = app;
|
|
219
271
|
const support = compatibility(app);
|
|
220
272
|
panels.push({
|
|
221
273
|
...publicApp,
|
|
222
|
-
revision: revision(app, digest),
|
|
274
|
+
revision: revision(app, digest, project),
|
|
223
275
|
bound: current.boundApps.has(app.id),
|
|
224
276
|
globalDisabled: current.globalDisabledApps.has(app.id),
|
|
225
277
|
enabled: current.hasProject &&
|
|
@@ -236,7 +288,48 @@ export function createPanelManagement(options) {
|
|
|
236
288
|
compatibility: support,
|
|
237
289
|
});
|
|
238
290
|
}
|
|
239
|
-
|
|
291
|
+
// Never attach a new revision to stale displayed binding state. File and
|
|
292
|
+
// package inspection above can yield to another device's mutation.
|
|
293
|
+
for (const app of [...panels, ...issues]) {
|
|
294
|
+
const pin = options.projectPackages ? parsePanelAppPackagePins(project)[app.id] : undefined;
|
|
295
|
+
if (projectState(app.id, project) !== projectState(app.id) ||
|
|
296
|
+
current.globalDisabledApps.has(app.id) !== policy().globalDisabledApps.has(app.id) ||
|
|
297
|
+
(pin && (pin.packageDigest !== app.packageDigest || pin.version !== app.version)))
|
|
298
|
+
throw new PanelManagementError(409, "conflict", "项目面板配置已改变,请刷新后重试。");
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
panels,
|
|
302
|
+
...(issues.length ? { issues } : {}),
|
|
303
|
+
workspace,
|
|
304
|
+
hasProject,
|
|
305
|
+
canRestorePackages: !!options.projectPackages && hasProject,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
// A fault row is diagnostic only: it never supplies executable descriptors or permissions.
|
|
309
|
+
// Restoring it explicitly reviews every target permission because its old manifest is unavailable.
|
|
310
|
+
async function restorationSelection(id) {
|
|
311
|
+
const value = await snapshot();
|
|
312
|
+
const app = value.panels.find((item) => item.id === id);
|
|
313
|
+
if (app)
|
|
314
|
+
return {
|
|
315
|
+
title: app.title,
|
|
316
|
+
revision: app.revision,
|
|
317
|
+
permissions: app.permissions,
|
|
318
|
+
current: { version: app.version, packageDigest: app.packageDigest },
|
|
319
|
+
};
|
|
320
|
+
const issue = value.issues?.find((item) => item.id === id);
|
|
321
|
+
if (!issue)
|
|
322
|
+
throw new PanelManagementError(404, "not_found", "找不到这个面板,请刷新列表。");
|
|
323
|
+
return {
|
|
324
|
+
title: { default: issue.id },
|
|
325
|
+
revision: issue.revision,
|
|
326
|
+
permissions: [],
|
|
327
|
+
current: {
|
|
328
|
+
version: issue.version ?? "未记录",
|
|
329
|
+
packageDigest: issue.packageDigest,
|
|
330
|
+
unavailable: true,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
240
333
|
}
|
|
241
334
|
async function network(context, work) {
|
|
242
335
|
const expected = generation(context.ownerId);
|
|
@@ -269,6 +362,9 @@ export function createPanelManagement(options) {
|
|
|
269
362
|
for (const [token, value] of reviews)
|
|
270
363
|
if (value.public.expiresAt <= now())
|
|
271
364
|
reviews.delete(token);
|
|
365
|
+
for (const [token, value] of restores)
|
|
366
|
+
if (value.public.expiresAt <= now())
|
|
367
|
+
restores.delete(token);
|
|
272
368
|
}
|
|
273
369
|
async function issue(context, input, current) {
|
|
274
370
|
return network(context, async (guard) => {
|
|
@@ -304,12 +400,78 @@ export function createPanelManagement(options) {
|
|
|
304
400
|
generation: generation(context.ownerId),
|
|
305
401
|
public: value,
|
|
306
402
|
source: pinnedSource,
|
|
403
|
+
origin: source,
|
|
404
|
+
digest,
|
|
405
|
+
projectState: projectState(preview.id),
|
|
406
|
+
catalogState: current?.catalogState,
|
|
407
|
+
});
|
|
408
|
+
return value;
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
async function issueProjectSource(context, input, expected) {
|
|
412
|
+
if (!options.allowLocalSources || !options.projectPackages || !hasProject)
|
|
413
|
+
throw new PanelManagementError(403, "unsupported", "这个入口不支持本地项目安装。");
|
|
414
|
+
return network(context, async (guard) => {
|
|
415
|
+
prune();
|
|
416
|
+
if (reviews.size >= MAX_REVIEWS)
|
|
417
|
+
throw new PanelManagementError(429, "busy", "待确认的面板过多,请先完成已有预览。");
|
|
418
|
+
if (!input || typeof input !== "object")
|
|
419
|
+
invalid();
|
|
420
|
+
let source;
|
|
421
|
+
let origin;
|
|
422
|
+
if (input.kind === "git") {
|
|
423
|
+
origin = await pinned(input, guard);
|
|
424
|
+
source = {
|
|
425
|
+
kind: "git",
|
|
426
|
+
url: origin.url,
|
|
427
|
+
ref: origin.commit,
|
|
428
|
+
...(origin.subdir ? { subdir: origin.subdir } : {}),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
if ((input.kind !== "dir" && input.kind !== "zip") ||
|
|
433
|
+
typeof input.path !== "string" ||
|
|
434
|
+
!isAbsolute(input.path) ||
|
|
435
|
+
input.path.length > 8192 ||
|
|
436
|
+
input.path.includes("\0"))
|
|
437
|
+
invalid();
|
|
438
|
+
source = { kind: input.kind, path: input.path };
|
|
439
|
+
}
|
|
440
|
+
const preview = await previewLocalPanelApp(source);
|
|
441
|
+
await guard();
|
|
442
|
+
if (expected && preview.id !== expected.id)
|
|
443
|
+
throw new PanelManagementError(409, "conflict", "来源中的面板标识已经改变。");
|
|
444
|
+
const listed = (await listApps()).find((app) => app.id === preview.id);
|
|
445
|
+
const current = listed ? await inspectApp(listed) : undefined;
|
|
446
|
+
const currentRevision = current ? revision(current.app, current.digest) : null;
|
|
447
|
+
if (expected && currentRevision !== expected.revision)
|
|
448
|
+
throw new PanelManagementError(409, "conflict", "面板已改变,请刷新后重试。");
|
|
449
|
+
const catalog = await catalogState(preview.id);
|
|
450
|
+
await guard();
|
|
451
|
+
const { reviewToken: digest, ...publicPreview } = preview;
|
|
452
|
+
const value = {
|
|
453
|
+
reviewToken: randomBytes(32).toString("base64url"),
|
|
454
|
+
expiresAt: now() + REVIEW_TTL,
|
|
455
|
+
kind: current ? "update" : "install",
|
|
456
|
+
preview: publicPreview,
|
|
457
|
+
expectedRevision: currentRevision,
|
|
458
|
+
compatibility: compatibility(preview),
|
|
459
|
+
...(current ? { installedVersion: current.app.version } : {}),
|
|
460
|
+
};
|
|
461
|
+
reviews.set(value.reviewToken, {
|
|
462
|
+
owner: context.ownerId,
|
|
463
|
+
generation: generation(context.ownerId),
|
|
464
|
+
public: value,
|
|
465
|
+
source,
|
|
466
|
+
origin,
|
|
307
467
|
digest,
|
|
468
|
+
projectState: projectState(preview.id),
|
|
469
|
+
catalogState: catalog,
|
|
308
470
|
});
|
|
309
471
|
return value;
|
|
310
472
|
});
|
|
311
473
|
}
|
|
312
|
-
async function mutate(context, work) {
|
|
474
|
+
async function mutate(context, change, work) {
|
|
313
475
|
const expected = generation(context.ownerId);
|
|
314
476
|
const guard = () => assertAuthorized(context, expected);
|
|
315
477
|
const run = async () => {
|
|
@@ -318,20 +480,23 @@ export function createPanelManagement(options) {
|
|
|
318
480
|
await guard();
|
|
319
481
|
return work(guard);
|
|
320
482
|
};
|
|
321
|
-
return options.withMutation ? options.withMutation(write) : write();
|
|
483
|
+
return panelExecutionGate.mutate(panelPackageMutationMatches({ ...change, projectPath: options.bindingCwd ?? options.cwd }), () => (options.withMutation ? options.withMutation(write) : write()));
|
|
322
484
|
};
|
|
323
485
|
const next = catalogMutation.then(run, run);
|
|
324
486
|
catalogMutation = next.catch(() => undefined);
|
|
325
487
|
return next;
|
|
326
488
|
}
|
|
327
|
-
function setBinding(app,
|
|
489
|
+
function setBinding(app, bound, expected) {
|
|
328
490
|
options.assertBinding?.();
|
|
329
491
|
if (!hasProject)
|
|
330
492
|
throw new PanelManagementError(400, "project_required", "请先选择项目,再绑定面板。");
|
|
331
493
|
const manager = new SettingsManager(workspace, "full");
|
|
332
494
|
manager.mutateSettingsForScope("project", workspace, (current) => {
|
|
333
495
|
options.assertBinding?.();
|
|
334
|
-
if (expected &&
|
|
496
|
+
if (expected &&
|
|
497
|
+
("revision" in expected
|
|
498
|
+
? revision(expected.app, expected.digest, current) !== expected.revision
|
|
499
|
+
: projectState(app.id, current) !== expected.projectState))
|
|
335
500
|
throw new PanelManagementError(409, "conflict", "面板配置已改变,请刷新后重试。");
|
|
336
501
|
const bindings = new Set(Array.isArray(current.panelAppBindings)
|
|
337
502
|
? current.panelAppBindings.filter((value) => typeof value === "string")
|
|
@@ -341,6 +506,17 @@ export function createPanelManagement(options) {
|
|
|
341
506
|
else
|
|
342
507
|
bindings.delete(app.id);
|
|
343
508
|
current.panelAppBindings = [...bindings].sort();
|
|
509
|
+
if (options.projectPackages) {
|
|
510
|
+
const pins = parsePanelAppPackagePins(current);
|
|
511
|
+
if (bound) {
|
|
512
|
+
if (!app.packageDigest)
|
|
513
|
+
throw new Error("Cannot bind an unverified Panel package");
|
|
514
|
+
pins[app.id] = { version: app.version, packageDigest: app.packageDigest };
|
|
515
|
+
}
|
|
516
|
+
else
|
|
517
|
+
delete pins[app.id];
|
|
518
|
+
current.panelAppPins = pins;
|
|
519
|
+
}
|
|
344
520
|
const overrides = current.panelAppOverrides &&
|
|
345
521
|
typeof current.panelAppOverrides === "object" &&
|
|
346
522
|
!Array.isArray(current.panelAppOverrides)
|
|
@@ -354,6 +530,121 @@ export function createPanelManagement(options) {
|
|
|
354
530
|
}
|
|
355
531
|
return {
|
|
356
532
|
snapshot,
|
|
533
|
+
async packageHistory(context, id, expected) {
|
|
534
|
+
assertId(id);
|
|
535
|
+
assertRevision(expected);
|
|
536
|
+
if (!options.projectPackages || !hasProject)
|
|
537
|
+
throw new PanelManagementError(400, "project_required", "请先选择支持独立版本的项目。");
|
|
538
|
+
return network(context, async (guard) => {
|
|
539
|
+
const current = await restorationSelection(id);
|
|
540
|
+
if (current.revision !== expected)
|
|
541
|
+
throw new PanelManagementError(409, "conflict", "项目面板已改变,请刷新后重试。");
|
|
542
|
+
const inventory = await listRetainedPanelAppPackages(id);
|
|
543
|
+
await guard();
|
|
544
|
+
const latest = await restorationSelection(id);
|
|
545
|
+
if (latest.revision !== expected)
|
|
546
|
+
throw new PanelManagementError(409, "conflict", "项目面板已改变,请刷新后重试。");
|
|
547
|
+
return {
|
|
548
|
+
appId: id,
|
|
549
|
+
title: current.title,
|
|
550
|
+
expectedRevision: expected,
|
|
551
|
+
current: current.current,
|
|
552
|
+
versions: inventory.packages.map((app) => ({
|
|
553
|
+
version: app.version,
|
|
554
|
+
packageDigest: app.packageDigest,
|
|
555
|
+
permissions: app.permissions,
|
|
556
|
+
compatibility: compatibility(app),
|
|
557
|
+
})),
|
|
558
|
+
unavailablePackages: inventory.unavailableDigests.length,
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
},
|
|
562
|
+
async previewRestore(context, id, digest, expected) {
|
|
563
|
+
assertId(id);
|
|
564
|
+
assertRevision(expected);
|
|
565
|
+
if (typeof digest !== "string" || !/^[a-f0-9]{64}$/.test(digest))
|
|
566
|
+
invalid();
|
|
567
|
+
if (!options.projectPackages || !hasProject)
|
|
568
|
+
throw new PanelManagementError(400, "project_required", "请先选择支持独立版本的项目。");
|
|
569
|
+
return network(context, async (guard) => {
|
|
570
|
+
prune();
|
|
571
|
+
if (restores.size >= MAX_REVIEWS)
|
|
572
|
+
throw new PanelManagementError(429, "busy", "待确认的版本过多,请稍后重试。");
|
|
573
|
+
const current = await restorationSelection(id);
|
|
574
|
+
if (!policy().boundApps.has(id))
|
|
575
|
+
throw new PanelManagementError(400, "not_bound", "请先绑定项目,再选择项目版本。");
|
|
576
|
+
if (current.revision !== expected)
|
|
577
|
+
throw new PanelManagementError(409, "conflict", "项目面板已改变,请刷新后重试。");
|
|
578
|
+
const state = projectState(id);
|
|
579
|
+
const target = await resolvePanelAppPackage(id, digest);
|
|
580
|
+
await guard();
|
|
581
|
+
const latest = await restorationSelection(id);
|
|
582
|
+
if (latest.revision !== expected || projectState(id) !== state)
|
|
583
|
+
throw new PanelManagementError(409, "conflict", "项目面板已改变,请重新审阅。");
|
|
584
|
+
const value = {
|
|
585
|
+
appId: id,
|
|
586
|
+
title: target.title,
|
|
587
|
+
version: target.version,
|
|
588
|
+
packageDigest: digest,
|
|
589
|
+
permissions: target.permissions,
|
|
590
|
+
addedPermissions: target.permissions.filter((permission) => !current.permissions.includes(permission)),
|
|
591
|
+
compatibility: compatibility(target),
|
|
592
|
+
current: current.current,
|
|
593
|
+
expectedRevision: expected,
|
|
594
|
+
reviewToken: randomBytes(32).toString("base64url"),
|
|
595
|
+
expiresAt: now() + REVIEW_TTL,
|
|
596
|
+
};
|
|
597
|
+
restores.set(value.reviewToken, {
|
|
598
|
+
owner: context.ownerId,
|
|
599
|
+
generation: generation(context.ownerId),
|
|
600
|
+
projectState: state,
|
|
601
|
+
public: structuredClone(value),
|
|
602
|
+
});
|
|
603
|
+
return value;
|
|
604
|
+
});
|
|
605
|
+
},
|
|
606
|
+
async cancelRestore(context, token) {
|
|
607
|
+
if (typeof token !== "string")
|
|
608
|
+
invalid();
|
|
609
|
+
await assertAuthorized(context);
|
|
610
|
+
prune();
|
|
611
|
+
const held = restores.get(token);
|
|
612
|
+
if (held && held.owner !== context.ownerId)
|
|
613
|
+
throw new PanelManagementError(403, "review_owner", "请在当前设备取消这个版本审阅。");
|
|
614
|
+
restores.delete(token);
|
|
615
|
+
return { cancelled: true };
|
|
616
|
+
},
|
|
617
|
+
async restore(context, token) {
|
|
618
|
+
if (typeof token !== "string")
|
|
619
|
+
invalid();
|
|
620
|
+
prune();
|
|
621
|
+
const held = restores.get(token);
|
|
622
|
+
if (!held)
|
|
623
|
+
throw new PanelManagementError(409, "review_expired", "版本审阅已失效,请重新选择。");
|
|
624
|
+
if (held.owner !== context.ownerId)
|
|
625
|
+
throw new PanelManagementError(403, "review_owner", "请在当前设备重新审阅这个版本。");
|
|
626
|
+
await assertAuthorized(context, held.generation);
|
|
627
|
+
const review = held.public;
|
|
628
|
+
return mutate(context, { appId: review.appId, kind: "binding" }, async (guard) => {
|
|
629
|
+
await assertAuthorized(context, held.generation);
|
|
630
|
+
if (restores.get(token) !== held || held.public.expiresAt <= now())
|
|
631
|
+
throw new PanelManagementError(409, "review_expired", "版本审阅已失效,请重新选择。");
|
|
632
|
+
const current = await restorationSelection(review.appId);
|
|
633
|
+
if (current.revision !== review.expectedRevision)
|
|
634
|
+
throw new PanelManagementError(409, "conflict", "项目面板已改变,请重新审阅。");
|
|
635
|
+
const target = await resolvePanelAppPackage(review.appId, review.packageDigest);
|
|
636
|
+
if (target.version !== review.version || !compatibility(target).supported)
|
|
637
|
+
throw new PanelManagementError(400, "unsupported", "这个版本无法在当前环境使用。");
|
|
638
|
+
await guard();
|
|
639
|
+
await assertAuthorized(context, held.generation);
|
|
640
|
+
if (restores.get(token) !== held || held.public.expiresAt <= now())
|
|
641
|
+
throw new PanelManagementError(409, "review_expired", "版本审阅已失效,请重新选择。");
|
|
642
|
+
setBinding(target, true, { projectState: held.projectState });
|
|
643
|
+
restores.delete(token);
|
|
644
|
+
await options.onChanged?.(review.appId, "binding");
|
|
645
|
+
return { id: review.appId, packageDigest: review.packageDigest };
|
|
646
|
+
});
|
|
647
|
+
},
|
|
357
648
|
assertAuthorized,
|
|
358
649
|
async discover(context, input) {
|
|
359
650
|
return network(context, async (guard) => {
|
|
@@ -381,7 +672,7 @@ export function createPanelManagement(options) {
|
|
|
381
672
|
assertId(id);
|
|
382
673
|
assertRevision(expected);
|
|
383
674
|
await assertAuthorized(context);
|
|
384
|
-
const current = await
|
|
675
|
+
const current = await selectedApp(id);
|
|
385
676
|
const currentRevision = revision(current.app, current.digest);
|
|
386
677
|
if (currentRevision !== expected)
|
|
387
678
|
throw new PanelManagementError(409, "conflict", "面板已改变,请刷新后重试。");
|
|
@@ -389,9 +680,37 @@ export function createPanelManagement(options) {
|
|
|
389
680
|
if (!source)
|
|
390
681
|
throw new PanelManagementError(400, "local_source", "这个面板由本地来源安装,请在桌面端更新。");
|
|
391
682
|
const { commit: _commit, ...input } = source;
|
|
392
|
-
return issue(context, input, {
|
|
683
|
+
return issue(context, input, {
|
|
684
|
+
...current,
|
|
685
|
+
revision: currentRevision,
|
|
686
|
+
catalogState: await catalogState(id),
|
|
687
|
+
});
|
|
393
688
|
},
|
|
394
|
-
|
|
689
|
+
previewProjectSource: issueProjectSource,
|
|
690
|
+
async previewProjectUpdate(context, id, expected) {
|
|
691
|
+
if (!options.allowLocalSources || !options.projectPackages || !hasProject)
|
|
692
|
+
throw new PanelManagementError(403, "unsupported", "这个入口不支持本地项目安装。");
|
|
693
|
+
assertId(id);
|
|
694
|
+
assertRevision(expected);
|
|
695
|
+
await assertAuthorized(context);
|
|
696
|
+
const { app, digest } = await selectedApp(id);
|
|
697
|
+
if (revision(app, digest) !== expected)
|
|
698
|
+
throw new PanelManagementError(409, "conflict", "面板已改变,请刷新后重试。");
|
|
699
|
+
let input;
|
|
700
|
+
if (typeof app.source !== "string")
|
|
701
|
+
input = app.source;
|
|
702
|
+
else {
|
|
703
|
+
const info = statSync(app.source);
|
|
704
|
+
if (info.isDirectory())
|
|
705
|
+
input = { kind: "dir", path: app.source };
|
|
706
|
+
else if (info.isFile() && extname(app.source).toLowerCase() === ".zip")
|
|
707
|
+
input = { kind: "zip", path: app.source };
|
|
708
|
+
else
|
|
709
|
+
throw new PanelManagementError(400, "invalid_source", "原始来源不是文件夹或 ZIP 包。");
|
|
710
|
+
}
|
|
711
|
+
return issueProjectSource(context, input, { id, revision: expected });
|
|
712
|
+
},
|
|
713
|
+
async install(context, token, bind = true, approval) {
|
|
395
714
|
if (typeof token !== "string" || typeof bind !== "boolean")
|
|
396
715
|
invalid();
|
|
397
716
|
prune();
|
|
@@ -401,42 +720,55 @@ export function createPanelManagement(options) {
|
|
|
401
720
|
if (held.owner !== context.ownerId)
|
|
402
721
|
throw new PanelManagementError(403, "review_owner", "请在当前设备重新预览这个来源。");
|
|
403
722
|
await assertAuthorized(context, held.generation);
|
|
404
|
-
|
|
723
|
+
if (approval?.expectedId && approval.expectedId !== held.public.preview.id)
|
|
724
|
+
invalid();
|
|
725
|
+
if (approval && held.public.kind === "update" && !approval.overwrite)
|
|
726
|
+
throw new PanelManagementError(409, "already_installed", "这个面板已经安装,请确认项目更新。");
|
|
727
|
+
return mutate(context, { appId: held.public.preview.id, kind: held.public.kind }, async (guard) => {
|
|
405
728
|
await assertAuthorized(context, held.generation);
|
|
406
729
|
if (reviews.get(token) !== held || held.public.expiresAt <= now())
|
|
407
730
|
throw new PanelManagementError(409, "review_expired", "预览已失效,请重新检查来源。");
|
|
408
731
|
const id = held.public.preview.id;
|
|
409
732
|
if (held.public.kind === "update") {
|
|
410
|
-
const current = await
|
|
411
|
-
if (revision(current.app, current.digest) !== held.public.expectedRevision
|
|
733
|
+
const current = await selectedApp(id);
|
|
734
|
+
if (revision(current.app, current.digest) !== held.public.expectedRevision ||
|
|
735
|
+
(await catalogState(id)) !== held.catalogState)
|
|
412
736
|
throw new PanelManagementError(409, "conflict", "面板已改变,请重新检查更新。");
|
|
413
737
|
}
|
|
414
738
|
else if ((await listInstalledPanelApps()).some((item) => item.id === id))
|
|
415
739
|
throw new PanelManagementError(409, "already_installed", "这个面板已经安装,请检查更新。");
|
|
740
|
+
if (projectState(id) !== held.projectState)
|
|
741
|
+
throw new PanelManagementError(409, "conflict", "项目面板配置已改变,请重新预览。");
|
|
416
742
|
await guard();
|
|
417
743
|
const installed = await installReviewedLocalPanelApp(held.source, held.digest, new Date(now()).toISOString(), {
|
|
418
744
|
overwrite: held.public.kind === "update",
|
|
419
745
|
expectedId: id,
|
|
420
|
-
recordedRef: held.
|
|
746
|
+
recordedRef: held.origin?.ref,
|
|
421
747
|
beforeCommit: async () => {
|
|
422
748
|
if (held.public.kind === "update") {
|
|
423
|
-
const current = await
|
|
424
|
-
if (revision(current.app, current.digest) !== held.public.expectedRevision
|
|
749
|
+
const current = await selectedApp(id);
|
|
750
|
+
if (revision(current.app, current.digest) !== held.public.expectedRevision ||
|
|
751
|
+
(await catalogState(id)) !== held.catalogState)
|
|
425
752
|
throw new PanelManagementError(409, "conflict", "面板已改变,请重新检查更新。");
|
|
426
753
|
}
|
|
754
|
+
if (projectState(id) !== held.projectState)
|
|
755
|
+
throw new PanelManagementError(409, "conflict", "项目面板配置已改变,请重新预览。");
|
|
427
756
|
await guard();
|
|
428
757
|
},
|
|
429
758
|
});
|
|
430
759
|
reviews.delete(token);
|
|
431
760
|
origins((value) => {
|
|
432
|
-
|
|
761
|
+
if (held.origin)
|
|
762
|
+
value[id] = { source: held.origin, lastUpdated: installed.lastUpdated };
|
|
763
|
+
else
|
|
764
|
+
delete value[id];
|
|
433
765
|
});
|
|
434
766
|
await guard();
|
|
435
767
|
if (bind && hasProject && held.public.compatibility.supported)
|
|
436
|
-
setBinding(installed, held.
|
|
768
|
+
setBinding(installed, true, { projectState: held.projectState });
|
|
437
769
|
invalidateSkillCache();
|
|
438
770
|
await options.onChanged?.(id, held.public.kind);
|
|
439
|
-
return { id };
|
|
771
|
+
return { id, packageDigest: installed.packageDigest };
|
|
440
772
|
});
|
|
441
773
|
},
|
|
442
774
|
async binding(context, id, bound, expected) {
|
|
@@ -444,12 +776,19 @@ export function createPanelManagement(options) {
|
|
|
444
776
|
assertRevision(expected);
|
|
445
777
|
if (typeof bound !== "boolean")
|
|
446
778
|
invalid();
|
|
447
|
-
return mutate(context, async (guard) => {
|
|
448
|
-
const current = await
|
|
779
|
+
return mutate(context, { appId: id, kind: "binding" }, async (guard) => {
|
|
780
|
+
const current = await selectedApp(id);
|
|
449
781
|
if (bound && !compatibility(current.app).supported)
|
|
450
782
|
throw new PanelManagementError(400, "unsupported", "这个面板需要当前 Web 环境未提供的能力。");
|
|
783
|
+
// Legacy installs need a retained copy before the project can pin them.
|
|
784
|
+
// An already pinned project must keep its selected version, not the catalog's.
|
|
785
|
+
const app = bound &&
|
|
786
|
+
options.projectPackages &&
|
|
787
|
+
!parsePanelAppPackagePins(projectSettings())[current.app.id]
|
|
788
|
+
? await retainInstalledPanelApp(current.app.id, current.app.packageDigest)
|
|
789
|
+
: current.app;
|
|
451
790
|
await guard();
|
|
452
|
-
setBinding(
|
|
791
|
+
setBinding(app, bound, { ...current, revision: expected });
|
|
453
792
|
await options.onChanged?.(id, "binding");
|
|
454
793
|
return snapshot();
|
|
455
794
|
});
|
|
@@ -457,14 +796,14 @@ export function createPanelManagement(options) {
|
|
|
457
796
|
async remove(context, id, expected) {
|
|
458
797
|
assertId(id);
|
|
459
798
|
assertRevision(expected);
|
|
460
|
-
return mutate(context, async (guard) => {
|
|
461
|
-
const current = await
|
|
799
|
+
return mutate(context, { appId: id, kind: "remove" }, async (guard) => {
|
|
800
|
+
const current = await selectedApp(id);
|
|
462
801
|
if (revision(current.app, current.digest) !== expected)
|
|
463
802
|
throw new PanelManagementError(409, "conflict", "面板已改变,请刷新后重试。");
|
|
464
803
|
await guard();
|
|
465
804
|
await uninstallPanelApp(id, {
|
|
466
805
|
beforeCommit: async () => {
|
|
467
|
-
const latest = await
|
|
806
|
+
const latest = await selectedApp(id);
|
|
468
807
|
if (revision(latest.app, latest.digest) !== expected)
|
|
469
808
|
throw new PanelManagementError(409, "conflict", "面板已改变,请刷新后重试。");
|
|
470
809
|
await guard();
|
|
@@ -474,7 +813,7 @@ export function createPanelManagement(options) {
|
|
|
474
813
|
delete value[id];
|
|
475
814
|
});
|
|
476
815
|
if (hasProject)
|
|
477
|
-
setBinding(current.app,
|
|
816
|
+
setBinding(current.app, false);
|
|
478
817
|
invalidateSkillCache();
|
|
479
818
|
await options.onChanged?.(id, "remove");
|
|
480
819
|
return { removed: true };
|
|
@@ -485,10 +824,14 @@ export function createPanelManagement(options) {
|
|
|
485
824
|
for (const [token, held] of reviews)
|
|
486
825
|
if (held.owner === owner)
|
|
487
826
|
reviews.delete(token);
|
|
827
|
+
for (const [token, held] of restores)
|
|
828
|
+
if (held.owner === owner)
|
|
829
|
+
restores.delete(token);
|
|
488
830
|
},
|
|
489
831
|
close() {
|
|
490
832
|
closed = true;
|
|
491
833
|
reviews.clear();
|
|
834
|
+
restores.clear();
|
|
492
835
|
},
|
|
493
836
|
};
|
|
494
837
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type PanelExecutionScope } from "./execution-gate.js";
|
|
2
|
+
export interface PanelPackageMutation {
|
|
3
|
+
appId: string;
|
|
4
|
+
projectPath: string;
|
|
5
|
+
kind: "install" | "update" | "binding" | "remove";
|
|
6
|
+
}
|
|
7
|
+
/** Pinned projects retain their bytes when a different project updates the catalog. */
|
|
8
|
+
export declare function panelPackageMutationMatches(change: PanelPackageMutation): (scope: PanelExecutionScope) => boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { projectPanelAppPackagePins, resolvePanelAppBindingProjectPath, } from "@cjhyy/code-shell-core";
|
|
2
|
+
import { panelExecutionProject } from "./execution-gate.js";
|
|
3
|
+
/** Pinned projects retain their bytes when a different project updates the catalog. */
|
|
4
|
+
export function panelPackageMutationMatches(change) {
|
|
5
|
+
const project = panelExecutionProject(resolvePanelAppBindingProjectPath(change.projectPath));
|
|
6
|
+
return (scope) => {
|
|
7
|
+
if (scope.appId !== change.appId)
|
|
8
|
+
return false;
|
|
9
|
+
if (change.kind === "remove")
|
|
10
|
+
return true;
|
|
11
|
+
const bindingProject = resolvePanelAppBindingProjectPath(scope.projectPath);
|
|
12
|
+
if (panelExecutionProject(bindingProject) === project)
|
|
13
|
+
return true;
|
|
14
|
+
if (change.kind === "binding")
|
|
15
|
+
return false;
|
|
16
|
+
try {
|
|
17
|
+
return !projectPanelAppPackagePins(bindingProject, change.appId)[change.appId];
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return true;
|
|
21
|
+
} // An unreadable project cannot prove that it is insulated.
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -13,6 +13,8 @@ export interface PanelProcessOwner {
|
|
|
13
13
|
send(event: "process.output" | "process.exit", payload: Record<string, unknown>): void;
|
|
14
14
|
}
|
|
15
15
|
export interface PanelAppProcessServiceOptions {
|
|
16
|
+
/** Host lease from before async preparation until the actual process group has exited. */
|
|
17
|
+
acquireExecution?(owner: PanelProcessOwner): () => void;
|
|
16
18
|
env?: NodeJS.ProcessEnv;
|
|
17
19
|
platform?: NodeJS.Platform;
|
|
18
20
|
/** Host-owned executable directories (for example CodeShell's managed bin). */
|
|
@@ -93,6 +95,7 @@ export declare class PanelAppProcessService {
|
|
|
93
95
|
argumentName: unknown;
|
|
94
96
|
path: string;
|
|
95
97
|
cleanup?: () => void;
|
|
98
|
+
validate?: () => Promise<void>;
|
|
96
99
|
}): Promise<{
|
|
97
100
|
handle: string;
|
|
98
101
|
}>;
|