@cjhyy/code-shell-server 0.9.21 → 0.9.23
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/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/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/worker-bridge-core.d.ts +9 -1
- package/dist/worker-bridge-core.js +45 -0
- package/package.json +15 -6
package/dist/panels/runtime.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
|
+
import { panelExecutionGate, PanelExecutionBusyError } from "./execution-gate.js";
|
|
1
2
|
import { randomBytes, createHash } from "node:crypto";
|
|
2
3
|
import { constants } from "node:fs";
|
|
3
4
|
import { lstat, mkdir, open, opendir, realpath } from "node:fs/promises";
|
|
4
5
|
import { homedir } from "node:os";
|
|
5
6
|
import { dirname, extname, join, resolve, sep } from "node:path";
|
|
6
|
-
import { listInstalledPanelApps, validateToolArgsStrict, } from "@cjhyy/code-shell-core";
|
|
7
|
+
import { listInstalledPanelApps, inspectProjectPanelApps, CredentialStore, validateToolArgsStrict, } from "@cjhyy/code-shell-core";
|
|
7
8
|
import { panelRuntimeApiVersion, panelProcessMethods, panelResourceMethods, panelToolJobMethods, panelRuntimeCapabilities, PanelBridgeError, panelBridgeFailure, } from "./bridge-contract.js";
|
|
8
9
|
import { PanelResourceService } from "./resources/service.js";
|
|
10
|
+
import { servePanelResource } from "./resources/http.js";
|
|
9
11
|
import { panelConnections, panelConnectionIds, materializePanelConnections, } from "./connections.js";
|
|
10
12
|
import { PanelRuntimeServices } from "./runtime-services.js";
|
|
11
13
|
import { PanelToolJobService, toolJobLimits, } from "./tool-jobs.js";
|
|
12
14
|
import { createPanelToolExecutor } from "./tool-executor.js";
|
|
13
|
-
import {
|
|
15
|
+
import { PanelTaskCookieHost } from "./task-cookie-host.js";
|
|
16
|
+
import { taskCookieFromInput, taskCookieSelection, } from "./task-cookies.js";
|
|
17
|
+
import { desktopPanelDirectoryBookmarks, hubPanelDirectoryBookmarks, } from "./directory-bookmarks.js";
|
|
14
18
|
import { handlePanelProcessDirectory } from "./process-files.js";
|
|
15
19
|
import { PanelAppProcessService, panelExecutableDirectories, panelProcessInfo, } from "./process-service.js";
|
|
16
20
|
import { PanelManagementError } from "./management.js";
|
|
21
|
+
import { panelAutomationMethods } from "./automations.js";
|
|
17
22
|
const ASSETS = "/api/v1/panel-assets/";
|
|
18
23
|
const ROOT = "/api/v1/panels/runtime/";
|
|
19
24
|
const MAX_FILE = 16 * 1024 * 1024;
|
|
@@ -21,6 +26,8 @@ const GRANT_TTL = 30 * 60_000;
|
|
|
21
26
|
const METHODS = [
|
|
22
27
|
"context.get",
|
|
23
28
|
"storage.get",
|
|
29
|
+
"storage.getSnapshot",
|
|
30
|
+
"storage.compareAndSet",
|
|
24
31
|
"storage.set",
|
|
25
32
|
"storage.delete",
|
|
26
33
|
"workspace.info",
|
|
@@ -30,10 +37,16 @@ const METHODS = [
|
|
|
30
37
|
"external.open",
|
|
31
38
|
"agent.submitPrompt",
|
|
32
39
|
"notifications.send",
|
|
40
|
+
...panelAutomationMethods,
|
|
33
41
|
...panelProcessMethods,
|
|
34
42
|
...panelResourceMethods,
|
|
43
|
+
"resources.open",
|
|
44
|
+
"resources.preview",
|
|
45
|
+
"resources.recordAudio",
|
|
35
46
|
...panelToolJobMethods,
|
|
36
47
|
"credentials.connections.list",
|
|
48
|
+
"credentials.cookies.listForTask",
|
|
49
|
+
"credentials.cookies.authorizeProcess",
|
|
37
50
|
"credentials.connections.authorizeProcess",
|
|
38
51
|
"agent.task.models",
|
|
39
52
|
"agent.task.start",
|
|
@@ -55,7 +68,9 @@ const PERMISSIONS = new Set([
|
|
|
55
68
|
"notifications.send",
|
|
56
69
|
"process",
|
|
57
70
|
"resources",
|
|
71
|
+
"resources.directRead",
|
|
58
72
|
"credentials.connections",
|
|
73
|
+
"credentials.cookies",
|
|
59
74
|
"agent.task",
|
|
60
75
|
]);
|
|
61
76
|
const MIME = {
|
|
@@ -75,12 +90,44 @@ const MIME = {
|
|
|
75
90
|
".woff2": "font/woff2",
|
|
76
91
|
".ttf": "font/ttf",
|
|
77
92
|
};
|
|
78
|
-
|
|
93
|
+
// The asset origin is also an allowed script source. Never expose uploaded
|
|
94
|
+
// documents or executable MIME types there, even with a resource permission.
|
|
95
|
+
const INLINE_RESOURCE_TYPES = new Set([
|
|
96
|
+
"image/png",
|
|
97
|
+
"image/jpeg",
|
|
98
|
+
"image/webp",
|
|
99
|
+
"image/gif",
|
|
100
|
+
"image/avif",
|
|
101
|
+
"image/bmp",
|
|
102
|
+
"audio/mpeg",
|
|
103
|
+
"audio/mp4",
|
|
104
|
+
"audio/wav",
|
|
105
|
+
"audio/x-wav",
|
|
106
|
+
"audio/ogg",
|
|
107
|
+
"audio/webm",
|
|
108
|
+
"audio/flac",
|
|
109
|
+
"audio/aac",
|
|
110
|
+
"video/mp4",
|
|
111
|
+
"video/webm",
|
|
112
|
+
"video/quicktime",
|
|
113
|
+
"font/woff",
|
|
114
|
+
"font/woff2",
|
|
115
|
+
"font/ttf",
|
|
116
|
+
"font/otf",
|
|
117
|
+
]);
|
|
118
|
+
const INLINE_RESOURCE_PATH = "_codeshell_resources/";
|
|
119
|
+
export const panelWebCompatibility = (app, options) => ({
|
|
79
120
|
supported: true,
|
|
80
121
|
reasons: [
|
|
81
122
|
...app.permissions
|
|
82
|
-
.filter((permission) => !PERMISSIONS.has(permission)
|
|
123
|
+
.filter((permission) => !PERMISSIONS.has(permission) &&
|
|
124
|
+
!(permission === "automations.manage" && options?.automations))
|
|
83
125
|
.map((permission) => "网页暂不提供 " + permission + ",对应功能需要桌面客户端。"),
|
|
126
|
+
...(app.permissions.includes("credentials.cookies")
|
|
127
|
+
? [
|
|
128
|
+
"网页可选择 Host 已保存的账号用于后台任务和受授权的临时程序;登录采集和浏览器登录恢复仍需桌面端。",
|
|
129
|
+
]
|
|
130
|
+
: []),
|
|
84
131
|
],
|
|
85
132
|
});
|
|
86
133
|
function methodPermission(method) {
|
|
@@ -88,12 +135,16 @@ function methodPermission(method) {
|
|
|
88
135
|
return "resources";
|
|
89
136
|
if (method.startsWith("credentials.connections."))
|
|
90
137
|
return "credentials.connections";
|
|
138
|
+
if (method.startsWith("credentials.cookies."))
|
|
139
|
+
return "credentials.cookies";
|
|
91
140
|
if (method.startsWith("storage."))
|
|
92
141
|
return "storage";
|
|
93
142
|
if (method.startsWith("process.") || method.startsWith("filesystem."))
|
|
94
143
|
return "process";
|
|
95
144
|
if (method.startsWith("agent.task."))
|
|
96
145
|
return "agent.task";
|
|
146
|
+
if (method.startsWith("automations."))
|
|
147
|
+
return "automations.manage";
|
|
97
148
|
if (method === "workspace.info")
|
|
98
149
|
return "workspace.info";
|
|
99
150
|
if (method === "workspace.writeText")
|
|
@@ -248,7 +299,7 @@ function bridgeScript(id, origin) {
|
|
|
248
299
|
const requestId = String(++next);
|
|
249
300
|
const timer = setTimeout(() => {
|
|
250
301
|
pending.delete(requestId); reject(new Error("Panel request timed out"));
|
|
251
|
-
},
|
|
302
|
+
}, ["tasks.start", "tasks.retry", "tasks.queue.set", "resources.recordAudio"].includes(method) ? 30 * 60 * 1000 : 60000);
|
|
252
303
|
pending.set(requestId, { resolve, reject, timer });
|
|
253
304
|
try {
|
|
254
305
|
parent.postMessage({ type: "codeshell-panel:call", instanceId: id, requestId, method, params }, origin);
|
|
@@ -357,6 +408,8 @@ function injectBridge(html, source) {
|
|
|
357
408
|
/** Owner-bound capability grants serve reviewed static bytes to an opaque-origin iframe. */
|
|
358
409
|
export function createPanelRuntime(options) {
|
|
359
410
|
const publicPathPrefix = options.publicPathPrefix ?? "";
|
|
411
|
+
// An admitted task belongs to the project. Login/page grants only control access.
|
|
412
|
+
const projectOwnedTools = options.host === "hub" || !!options.sharedToolJobs;
|
|
360
413
|
if (publicPathPrefix &&
|
|
361
414
|
!/^\/p\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(publicPathPrefix))
|
|
362
415
|
throw new Error("Invalid project public path prefix");
|
|
@@ -364,30 +417,112 @@ export function createPanelRuntime(options) {
|
|
|
364
417
|
const assets = new Map();
|
|
365
418
|
const now = options.now ?? Date.now;
|
|
366
419
|
const services = new PanelRuntimeServices({ dataDir: options.dataDir });
|
|
367
|
-
const installed = options.listInstalled ??
|
|
420
|
+
const installed = options.listInstalled ??
|
|
421
|
+
(options.projectPackages
|
|
422
|
+
? async (appId) => (await inspectProjectPanelApps(options.bindingCwd ?? options.cwd, appId)).apps
|
|
423
|
+
: listInstalledPanelApps);
|
|
368
424
|
let closed = false;
|
|
369
425
|
let generation = 0;
|
|
370
426
|
let nextGuest = 0;
|
|
371
427
|
let nextToolOwner = -1;
|
|
372
428
|
const byGuest = new Map();
|
|
373
|
-
const directoryBookmarks =
|
|
429
|
+
const directoryBookmarks = options.host === "desktop"
|
|
430
|
+
? desktopPanelDirectoryBookmarks(options.dataDir)
|
|
431
|
+
: hubPanelDirectoryBookmarks(options.dataDir);
|
|
374
432
|
const panelDataDirectory = (appId) => join(options.dataDir, "panel-data", appId, digest(Buffer.from(options.bindingCwd ?? options.cwd)).slice(0, 24));
|
|
375
433
|
const toolOwners = new Map();
|
|
376
434
|
const jobOwners = new Map();
|
|
377
435
|
let toolJobs;
|
|
436
|
+
let taskCookies;
|
|
437
|
+
function getTaskCookies() {
|
|
438
|
+
if (options.host !== "hub" || options.sharedToolJobs)
|
|
439
|
+
throw new PanelBridgeError("NOT_SUPPORTED", "Background Cookie access is unavailable");
|
|
440
|
+
return (taskCookies ??= new PanelTaskCookieHost({
|
|
441
|
+
rootDirectory: join(options.dataDir, "panel-task-cookies"),
|
|
442
|
+
// A cloud project must not inherit credentials from the controller's user account.
|
|
443
|
+
credentials: async (scope) => new CredentialStore(scope.projectPath).list("project"),
|
|
444
|
+
authorize: async (scope) => {
|
|
445
|
+
if (!(await installedToolApp(scope)).permissions.includes("credentials.cookies"))
|
|
446
|
+
throw new PanelBridgeError("PERMISSION_DENIED", "Tool requires Cookie permission");
|
|
447
|
+
},
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
function cookieAccess(grant) {
|
|
451
|
+
if (!grant.app.permissions.includes("process") ||
|
|
452
|
+
!grant.app.permissions.includes("resources") ||
|
|
453
|
+
!grant.app.permissions.includes("credentials.cookies"))
|
|
454
|
+
error(403, "后台账号访问缺少面板权限。");
|
|
455
|
+
if (grant.sharedTools) {
|
|
456
|
+
if (!grant.sharedTools.cookies)
|
|
457
|
+
error(501, "当前 Host 不支持后台账号授权。");
|
|
458
|
+
return grant.sharedTools.cookies;
|
|
459
|
+
}
|
|
460
|
+
if (options.sharedToolJobs)
|
|
461
|
+
error(410, "共享任务授权已失效。");
|
|
462
|
+
const scope = {
|
|
463
|
+
appId: grant.app.id,
|
|
464
|
+
projectPath: options.bindingCwd ?? options.cwd,
|
|
465
|
+
revision: grant.revision,
|
|
466
|
+
};
|
|
467
|
+
const host = getTaskCookies();
|
|
468
|
+
return {
|
|
469
|
+
list: (url) => host.list(scope, url),
|
|
470
|
+
check: (selection) => host.check(scope, selection),
|
|
471
|
+
materialize: (selection) => host.materialize(scope, selection),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
async function taskConsentDetail(grant, input, entry) {
|
|
475
|
+
const selection = taskCookieFromInput(input);
|
|
476
|
+
if (!selection)
|
|
477
|
+
return entry;
|
|
478
|
+
const account = await cookieAccess(grant).check(selection);
|
|
479
|
+
return `工具:${entry}\n账号:${account.label}\n站点:${new URL(selection.url).hostname}\n所选登录信息仅交付给已审查的后台程序,任务可在关闭页面后继续。`;
|
|
480
|
+
}
|
|
481
|
+
const localToolRoot = options.host === "desktop"
|
|
482
|
+
? join(options.dataDir, "panel-web-tool-jobs", digest(Buffer.from(options.cwd)).slice(0, 24))
|
|
483
|
+
: join(options.dataDir, "panel-tool-jobs");
|
|
484
|
+
let legacyPresent;
|
|
485
|
+
async function legacyJobs(scope) {
|
|
486
|
+
if (!options.sharedToolJobs || options.host !== "desktop")
|
|
487
|
+
return [];
|
|
488
|
+
const present = await (legacyPresent ??= lstat(localToolRoot)
|
|
489
|
+
.then((info) => {
|
|
490
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
491
|
+
throw new Error("Invalid legacy task store");
|
|
492
|
+
return true;
|
|
493
|
+
})
|
|
494
|
+
.catch((cause) => {
|
|
495
|
+
if (cause.code === "ENOENT")
|
|
496
|
+
return false;
|
|
497
|
+
throw cause;
|
|
498
|
+
}));
|
|
499
|
+
// Opening an existing store marks unfinished jobs interrupted; it never replays them.
|
|
500
|
+
return present
|
|
501
|
+
? (await getToolJobs().list(scope)).map((job) => ({
|
|
502
|
+
...job,
|
|
503
|
+
readOnly: true,
|
|
504
|
+
historySource: "desktop-web-legacy",
|
|
505
|
+
}))
|
|
506
|
+
: [];
|
|
507
|
+
}
|
|
378
508
|
async function installedToolApp(scope) {
|
|
379
509
|
if (closed || scope.projectPath !== (options.bindingCwd ?? options.cwd))
|
|
380
510
|
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);
|
|
511
|
+
const panel = (await snapshot(scope.appId)).panels.find((candidate) => candidate.id === scope.appId);
|
|
512
|
+
const app = (await installed(scope.appId)).find((candidate) => candidate.id === scope.appId);
|
|
383
513
|
if (!panel?.enabled ||
|
|
384
514
|
panel.revision !== scope.revision ||
|
|
515
|
+
(panel.packageDigest !== undefined && panel.packageDigest !== app?.packageDigest) ||
|
|
385
516
|
!app?.permissions.includes("process") ||
|
|
386
517
|
!app.permissions.includes("resources"))
|
|
387
518
|
throw new PanelBridgeError("REVOKED", "Installed tool task is no longer authorized");
|
|
388
519
|
return app;
|
|
389
520
|
}
|
|
390
521
|
const processes = new PanelAppProcessService({
|
|
522
|
+
acquireExecution: (owner) => panelExecutionGate.enter({
|
|
523
|
+
appId: owner.appId,
|
|
524
|
+
projectPath: options.bindingCwd ?? options.cwd,
|
|
525
|
+
}),
|
|
391
526
|
approvalScope: "guest",
|
|
392
527
|
resolvePackageEntry: async (owner, name) => {
|
|
393
528
|
const taskScope = toolOwners.get(owner.guestId);
|
|
@@ -427,7 +562,7 @@ export function createPanelRuntime(options) {
|
|
|
427
562
|
rootDirectory: join(options.dataDir, "panel-app-media"),
|
|
428
563
|
isScopeAuthorized: async (scope) => {
|
|
429
564
|
if (scope.projectPath === (options.bindingCwd ?? options.cwd)) {
|
|
430
|
-
const panel = (await snapshot()).panels.find((candidate) => candidate.id === scope.appId);
|
|
565
|
+
const panel = (await snapshot(scope.appId)).panels.find((candidate) => candidate.id === scope.appId);
|
|
431
566
|
if (panel?.enabled && panel.permissions.includes("resources"))
|
|
432
567
|
return true;
|
|
433
568
|
}
|
|
@@ -457,16 +592,33 @@ export function createPanelRuntime(options) {
|
|
|
457
592
|
revision: grant.revision,
|
|
458
593
|
};
|
|
459
594
|
const input = (params ?? {});
|
|
460
|
-
const
|
|
595
|
+
const shared = grant.sharedTools;
|
|
596
|
+
const service = shared ?? {
|
|
597
|
+
start: (request, signal) => getToolJobs().start(scope, request, signal),
|
|
598
|
+
list: () => getToolJobs().list(scope),
|
|
599
|
+
get: (id) => getToolJobs().get(scope, id),
|
|
600
|
+
cancel: (id) => getToolJobs().cancel(scope, id),
|
|
601
|
+
retry: (id) => getToolJobs().retry(scope, id),
|
|
602
|
+
find: (requestKey) => getToolJobs().find(scope, requestKey),
|
|
603
|
+
getQueue: () => getToolJobs().getQueue(scope),
|
|
604
|
+
setQueue: (update) => getToolJobs().setQueue(scope, update),
|
|
605
|
+
};
|
|
606
|
+
if (options.sharedToolJobs && !shared)
|
|
607
|
+
error(410, "共享任务授权已失效,请重新打开面板。");
|
|
461
608
|
if (method === "tasks.start") {
|
|
462
609
|
const app = await installedToolApp(scope);
|
|
463
610
|
const entry = app.nativeEntries?.[input.entry ?? ""];
|
|
464
611
|
if (!entry)
|
|
465
612
|
error(501, "安装的原生工具不可用。");
|
|
466
|
-
|
|
613
|
+
const detail = await taskConsentDetail(grant, input.input, input.entry);
|
|
614
|
+
if (!(await confirm(grant, `启动 ${grant.app.title.default} 的后台工具?`, detail)))
|
|
467
615
|
error(403, "你取消了后台工具执行。");
|
|
468
616
|
if (!(await authorized(grant)))
|
|
469
617
|
error(410, "面板授权已失效。");
|
|
618
|
+
if (taskCookieFromInput(input.input))
|
|
619
|
+
await taskConsentDetail(grant, input.input, input.entry);
|
|
620
|
+
if (!(await authorized(grant)))
|
|
621
|
+
error(410, "面板授权已失效。");
|
|
470
622
|
const preparation = {
|
|
471
623
|
owner: grant.owner,
|
|
472
624
|
appId: scope.appId,
|
|
@@ -474,31 +626,46 @@ export function createPanelRuntime(options) {
|
|
|
474
626
|
};
|
|
475
627
|
preparingTools.add(preparation);
|
|
476
628
|
try {
|
|
477
|
-
const job = await service.start(
|
|
629
|
+
const job = await service.start({
|
|
478
630
|
entry: { name: input.entry, sha256: entry.sha256 },
|
|
479
631
|
input: input.input,
|
|
480
632
|
recovery: input.recovery ?? "manual",
|
|
481
633
|
requestKey: input.requestKey,
|
|
482
634
|
}, preparation.controller.signal);
|
|
483
|
-
if (!jobOwners.has(job.id))
|
|
635
|
+
if (!projectOwnedTools && !jobOwners.has(job.id))
|
|
484
636
|
jobOwners.set(job.id, { owner: grant.owner, scope });
|
|
485
637
|
if (!(await authorized(grant))) {
|
|
486
|
-
if (jobOwners.get(job.id)?.owner === grant.owner)
|
|
487
|
-
await service.cancel(
|
|
638
|
+
if (!projectOwnedTools && jobOwners.get(job.id)?.owner === grant.owner)
|
|
639
|
+
await service.cancel(job.id);
|
|
488
640
|
error(410, "登录授权已撤销。");
|
|
489
641
|
}
|
|
490
|
-
|
|
642
|
+
if (!shared)
|
|
643
|
+
emitTaskEvent(await service.get(job.id));
|
|
491
644
|
return job;
|
|
492
645
|
}
|
|
493
646
|
catch (cause) {
|
|
494
647
|
if (preparation.controller.signal.aborted)
|
|
495
|
-
error(410,
|
|
648
|
+
error(410, projectOwnedTools
|
|
649
|
+
? "登录授权已撤销。请重新连接后查询任务,已接收的项目任务可能仍在运行。"
|
|
650
|
+
: "登录授权已撤销,输入准备已取消。");
|
|
496
651
|
throw cause;
|
|
497
652
|
}
|
|
498
653
|
finally {
|
|
499
654
|
preparingTools.delete(preparation);
|
|
500
655
|
}
|
|
501
656
|
}
|
|
657
|
+
if (method === "tasks.find")
|
|
658
|
+
return service.find(input.requestKey);
|
|
659
|
+
if (method === "tasks.queue.get")
|
|
660
|
+
return service.getQueue();
|
|
661
|
+
if (method === "tasks.queue.set") {
|
|
662
|
+
// Resuming may launch previously admitted jobs; use the existing owner consent gate.
|
|
663
|
+
if (!(await confirm(grant, `调整 ${grant.app.title.default} 的后台队列?`, "暂停只影响等待任务,正在执行的任务继续运行。")))
|
|
664
|
+
error(403, "你取消了队列调整。");
|
|
665
|
+
if (!(await authorized(grant)))
|
|
666
|
+
error(410, "面板授权已失效。");
|
|
667
|
+
return service.setQueue(params);
|
|
668
|
+
}
|
|
502
669
|
if (method === "tasks.list") {
|
|
503
670
|
const offset = input.offset ?? 0, limit = input.limit ?? 50;
|
|
504
671
|
if (!Number.isSafeInteger(offset) ||
|
|
@@ -507,26 +674,48 @@ export function createPanelRuntime(options) {
|
|
|
507
674
|
limit < 1 ||
|
|
508
675
|
limit > 50)
|
|
509
676
|
throw new PanelBridgeError("INVALID_ARGUMENT", "Invalid task page");
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
677
|
+
const jobs = [...(await service.list()), ...(await legacyJobs(scope))].sort((a, b) => b.createdAt - a.createdAt || a.id.localeCompare(b.id));
|
|
678
|
+
return jobs.slice(offset, offset + limit).map((job) => toolSummary(job));
|
|
679
|
+
}
|
|
680
|
+
if (shared && ["tasks.get", "tasks.cancel", "tasks.retry"].includes(method)) {
|
|
681
|
+
// Explicitly identify legacy IDs. Authorization failures on the shared service
|
|
682
|
+
// must never fall through to a less restrictive coordinator.
|
|
683
|
+
if (!(await shared.has(input.id))) {
|
|
684
|
+
const legacy = (await legacyJobs(scope)).find((job) => job.id === input.id);
|
|
685
|
+
if (legacy) {
|
|
686
|
+
if (method === "tasks.get")
|
|
687
|
+
return legacy;
|
|
688
|
+
throw new PanelBridgeError("NOT_SUPPORTED", "旧网页任务仅供查看,请从原输入创建新的共享任务。");
|
|
689
|
+
}
|
|
690
|
+
}
|
|
513
691
|
}
|
|
514
692
|
if (method === "tasks.get")
|
|
515
|
-
return service.get(
|
|
693
|
+
return service.get(input.id);
|
|
516
694
|
if (method === "tasks.cancel")
|
|
517
|
-
return service.cancel(
|
|
695
|
+
return service.cancel(input.id);
|
|
518
696
|
if (method === "tasks.retry") {
|
|
519
|
-
|
|
697
|
+
const previous = await service.get(input.id);
|
|
698
|
+
if (previous.readOnly)
|
|
699
|
+
throw new PanelBridgeError("NOT_SUPPORTED", "旧任务仅供查看,请检查输入后创建新任务。");
|
|
700
|
+
const detail = await taskConsentDetail(grant, previous.input, previous.entry.name);
|
|
701
|
+
if (!(await confirm(grant, `重试 ${grant.app.title.default} 的后台工具?`, detail)))
|
|
520
702
|
error(403, "你取消了后台工具重试。");
|
|
521
703
|
if (!(await authorized(grant)))
|
|
522
704
|
error(410, "面板授权已失效。");
|
|
523
|
-
|
|
524
|
-
|
|
705
|
+
if (taskCookieFromInput(previous.input))
|
|
706
|
+
await taskConsentDetail(grant, previous.input, previous.entry.name);
|
|
707
|
+
if (!(await authorized(grant)))
|
|
708
|
+
error(410, "面板授权已失效。");
|
|
709
|
+
const job = await service.retry(input.id);
|
|
710
|
+
if (!projectOwnedTools)
|
|
711
|
+
jobOwners.set(job.id, { owner: grant.owner, scope });
|
|
525
712
|
if (!(await authorized(grant))) {
|
|
526
|
-
|
|
527
|
-
|
|
713
|
+
if (!projectOwnedTools)
|
|
714
|
+
await service.cancel(job.id);
|
|
715
|
+
error(410, "登录授权已撤销,请重新连接后查看任务状态。");
|
|
528
716
|
}
|
|
529
|
-
|
|
717
|
+
if (!shared)
|
|
718
|
+
emitTaskEvent(await service.get(job.id));
|
|
530
719
|
return job;
|
|
531
720
|
}
|
|
532
721
|
throw new PanelBridgeError("NOT_SUPPORTED", "Unknown tool task operation");
|
|
@@ -537,10 +726,11 @@ export function createPanelRuntime(options) {
|
|
|
537
726
|
}
|
|
538
727
|
function emitTaskEvent(job) {
|
|
539
728
|
const owner = jobOwners.get(job.id)?.owner;
|
|
540
|
-
if (!owner)
|
|
729
|
+
if (!projectOwnedTools && !owner)
|
|
541
730
|
return;
|
|
542
731
|
for (const grant of grants.values())
|
|
543
|
-
if (grant.owner === owner &&
|
|
732
|
+
if ((projectOwnedTools || grant.owner === owner) &&
|
|
733
|
+
job.scope.projectPath === (options.bindingCwd ?? options.cwd) &&
|
|
544
734
|
grant.app.id === job.scope.appId &&
|
|
545
735
|
grant.revision === job.scope.revision &&
|
|
546
736
|
grant.app.permissions.includes("process") &&
|
|
@@ -553,6 +743,7 @@ export function createPanelRuntime(options) {
|
|
|
553
743
|
if (toolJobs)
|
|
554
744
|
return toolJobs;
|
|
555
745
|
const executor = createPanelToolExecutor({
|
|
746
|
+
...(options.host === "hub" && !options.sharedToolJobs ? { cookies: getTaskCookies() } : {}),
|
|
556
747
|
processes,
|
|
557
748
|
resources,
|
|
558
749
|
owner: (job, send) => {
|
|
@@ -577,6 +768,15 @@ export function createPanelRuntime(options) {
|
|
|
577
768
|
if (!app.permissions.includes("credentials.connections"))
|
|
578
769
|
throw new PanelBridgeError("PERMISSION_DENIED", "Tool requires connection permission");
|
|
579
770
|
},
|
|
771
|
+
authorizeDirectRead: async (scope) => {
|
|
772
|
+
const app = await installedToolApp(scope);
|
|
773
|
+
if (!app.permissions.includes("resources.directRead"))
|
|
774
|
+
throw new PanelBridgeError("PERMISSION_DENIED", "Tool requires direct read permission");
|
|
775
|
+
},
|
|
776
|
+
resolveDirectoryBookmark: async (scope, bookmark) => {
|
|
777
|
+
await installedToolApp(scope);
|
|
778
|
+
return directoryBookmarks.restore(scope.appId, scope.projectPath, bookmark);
|
|
779
|
+
},
|
|
580
780
|
appDataDirectory: async (scope) => {
|
|
581
781
|
await installedToolApp(scope);
|
|
582
782
|
const path = panelDataDirectory(scope.appId);
|
|
@@ -586,27 +786,31 @@ export function createPanelRuntime(options) {
|
|
|
586
786
|
sealedRoot: join(options.dataDir, "panel-app-sealed"),
|
|
587
787
|
});
|
|
588
788
|
toolJobs = new PanelToolJobService({
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
rootDir:
|
|
592
|
-
? join(options.dataDir, "panel-web-tool-jobs", digest(Buffer.from(options.cwd)).slice(0, 24))
|
|
593
|
-
: join(options.dataDir, "panel-tool-jobs"),
|
|
789
|
+
// Separate legacy store when a Desktop coordinator is injected. Existing
|
|
790
|
+
// history stays readable; shared jobs never acquire a second disk lock.
|
|
791
|
+
rootDir: localToolRoot,
|
|
594
792
|
...executor,
|
|
595
793
|
isAuthorized: async (scope) => !!(await installedToolApp(scope).catch(() => null)),
|
|
794
|
+
describePackage: async (scope) => {
|
|
795
|
+
const app = await installedToolApp(scope);
|
|
796
|
+
if (!app.packageDigest)
|
|
797
|
+
throw new Error("Project package digest is unavailable");
|
|
798
|
+
return { version: app.version, packageDigest: app.packageDigest };
|
|
799
|
+
},
|
|
596
800
|
onEvent: emitTaskEvent,
|
|
597
801
|
});
|
|
598
802
|
return toolJobs;
|
|
599
803
|
}
|
|
600
|
-
|
|
601
|
-
function snapshot() {
|
|
804
|
+
const readingSnapshots = new Map();
|
|
805
|
+
function snapshot(appId) {
|
|
602
806
|
// Parallel module requests share current disk work, without a stale TTL
|
|
603
807
|
// that could keep an unbound or updated package authorized.
|
|
604
|
-
const pending =
|
|
605
|
-
|
|
808
|
+
const pending = readingSnapshots.get(appId) ?? options.snapshot(appId);
|
|
809
|
+
readingSnapshots.set(appId, pending);
|
|
606
810
|
void pending
|
|
607
811
|
.finally(() => {
|
|
608
|
-
if (
|
|
609
|
-
|
|
812
|
+
if (readingSnapshots.get(appId) === pending)
|
|
813
|
+
readingSnapshots.delete(appId);
|
|
610
814
|
})
|
|
611
815
|
.catch(() => { });
|
|
612
816
|
return pending;
|
|
@@ -614,6 +818,7 @@ export function createPanelRuntime(options) {
|
|
|
614
818
|
function remove(grant) {
|
|
615
819
|
if (grants.get(grant.id) !== grant)
|
|
616
820
|
return;
|
|
821
|
+
grant.unsubscribeTools?.();
|
|
617
822
|
grants.delete(grant.id);
|
|
618
823
|
assets.delete(grant.asset);
|
|
619
824
|
byGuest.delete(grant.guestId);
|
|
@@ -704,6 +909,9 @@ export function createPanelRuntime(options) {
|
|
|
704
909
|
};
|
|
705
910
|
}
|
|
706
911
|
async function onPanelAction(scope, input) {
|
|
912
|
+
return panelExecutionGate.run({ appId: scope.appId, projectPath: options.bindingCwd ?? options.cwd }, () => onPanelActionAdmitted(scope, input));
|
|
913
|
+
}
|
|
914
|
+
async function onPanelActionAdmitted(scope, input) {
|
|
707
915
|
const grant = grants.get(scope.instanceId);
|
|
708
916
|
if (!grant || grant.owner !== scope.ownerId || !(await authorized(grant)))
|
|
709
917
|
return { ok: false, detail: "面板已关闭或授权已撤销。" };
|
|
@@ -737,13 +945,20 @@ export function createPanelRuntime(options) {
|
|
|
737
945
|
return { ok: false, detail: "用户取消了面板工具操作。" };
|
|
738
946
|
if (grant.toolCalls.size >= 16)
|
|
739
947
|
return { ok: false, detail: "面板工具调用过多。" };
|
|
948
|
+
const releaseExecution = panelExecutionGate.enter({
|
|
949
|
+
appId: grant.app.id,
|
|
950
|
+
projectPath: options.bindingCwd ?? options.cwd,
|
|
951
|
+
});
|
|
740
952
|
const requestId = randomBytes(18).toString("base64url");
|
|
741
953
|
const value = await new Promise((done) => {
|
|
742
|
-
|
|
954
|
+
// Keep the operation occupied after the caller stops waiting. A late reply
|
|
955
|
+
// or revoked guest ends it; a timeout alone is not proof of termination.
|
|
956
|
+
const timer = setTimeout(() => done({ error: "面板工具调用超时。" }), 45_000);
|
|
743
957
|
timer.unref();
|
|
744
958
|
const finish = (result) => {
|
|
745
959
|
if (!grant.toolCalls.delete(requestId))
|
|
746
960
|
return;
|
|
961
|
+
releaseExecution();
|
|
747
962
|
clearTimeout(timer);
|
|
748
963
|
done(result);
|
|
749
964
|
};
|
|
@@ -783,10 +998,12 @@ export function createPanelRuntime(options) {
|
|
|
783
998
|
return false;
|
|
784
999
|
}
|
|
785
1000
|
try {
|
|
786
|
-
const panel = (await snapshot()).panels.find((candidate) => candidate.id === grant.app.id);
|
|
1001
|
+
const panel = (await snapshot(grant.app.id)).panels.find((candidate) => candidate.id === grant.app.id);
|
|
787
1002
|
const info = await lstat(grant.root);
|
|
788
1003
|
const ownerStillAuthorized = await options.isAuthorized(grant.request);
|
|
789
1004
|
const currentOwner = await options.ownerId(grant.request);
|
|
1005
|
+
if (grant.app.permissions.includes("process"))
|
|
1006
|
+
await options.authorizePanelDirectory?.(grant.app, options.bindingCwd ?? options.cwd, options.cwd);
|
|
790
1007
|
const valid = !closed &&
|
|
791
1008
|
grants.get(grant.id) === grant &&
|
|
792
1009
|
grant.expiresAt > now() &&
|
|
@@ -825,7 +1042,7 @@ export function createPanelRuntime(options) {
|
|
|
825
1042
|
!/^[a-f0-9]{64}$/.test(input.revision) ||
|
|
826
1043
|
Object.keys(input).some((key) => !["appId", "revision", "sessionId", "theme", "locale"].includes(key)))
|
|
827
1044
|
error(400, "面板参数无效。");
|
|
828
|
-
const panel = (await snapshot()).panels.find((candidate) => candidate.id === input.appId);
|
|
1045
|
+
const panel = (await snapshot(input.appId)).panels.find((candidate) => candidate.id === input.appId);
|
|
829
1046
|
if (!panel?.enabled || !panel.compatibility.supported)
|
|
830
1047
|
error(403, "请先将面板绑定到当前工作区。");
|
|
831
1048
|
if (panel.revision !== input.revision)
|
|
@@ -833,9 +1050,11 @@ export function createPanelRuntime(options) {
|
|
|
833
1050
|
if (input.sessionId !== undefined &&
|
|
834
1051
|
(typeof input.sessionId !== "string" || !/^[A-Za-z0-9_-]{8,128}$/.test(input.sessionId)))
|
|
835
1052
|
error(400, "会话标识无效。");
|
|
836
|
-
const app = (await installed()).find((candidate) => candidate.id === input.appId);
|
|
1053
|
+
const app = (await installed(input.appId)).find((candidate) => candidate.id === input.appId);
|
|
837
1054
|
if (!app)
|
|
838
1055
|
error(404, "面板已卸载。");
|
|
1056
|
+
if (panel.packageDigest && panel.packageDigest !== app.packageDigest)
|
|
1057
|
+
error(409, "项目面板版本已改变,请重新打开。");
|
|
839
1058
|
const root = await realpath(app.installPath), info = await lstat(app.installPath);
|
|
840
1059
|
if (info.isSymbolicLink() || !info.isDirectory())
|
|
841
1060
|
error(403, "面板安装目录无效。");
|
|
@@ -844,6 +1063,13 @@ export function createPanelRuntime(options) {
|
|
|
844
1063
|
error(404, "找不到面板入口。");
|
|
845
1064
|
if (closed || generation !== startedGeneration || preparation.cancelled)
|
|
846
1065
|
error(410, "面板授权已失效,请重新打开。");
|
|
1066
|
+
const sharedTools = options.sharedToolJobs &&
|
|
1067
|
+
app.permissions.includes("process") &&
|
|
1068
|
+
app.permissions.includes("resources")
|
|
1069
|
+
? await options.sharedToolJobs.bind(app, options.bindingCwd ?? options.cwd)
|
|
1070
|
+
: undefined;
|
|
1071
|
+
if (closed || generation !== startedGeneration || preparation.cancelled)
|
|
1072
|
+
error(410, "面板授权已失效,请重新打开。");
|
|
847
1073
|
for (const grant of grants.values())
|
|
848
1074
|
if (grant.expiresAt <= now())
|
|
849
1075
|
remove(grant);
|
|
@@ -860,12 +1086,32 @@ export function createPanelRuntime(options) {
|
|
|
860
1086
|
capabilities: {
|
|
861
1087
|
...panelRuntimeCapabilities({
|
|
862
1088
|
process: app.permissions.includes("process"),
|
|
1089
|
+
cookieProcess: app.permissions.includes("process") &&
|
|
1090
|
+
app.permissions.includes("resources") &&
|
|
1091
|
+
app.permissions.includes("credentials.cookies") &&
|
|
1092
|
+
(sharedTools
|
|
1093
|
+
? !!sharedTools.cookies?.materialize
|
|
1094
|
+
: options.host === "hub" && !options.sharedToolJobs),
|
|
863
1095
|
resources: app.permissions.includes("resources") ? resources.capabilities() : undefined,
|
|
864
1096
|
tasks: app.permissions.includes("process") && app.permissions.includes("resources")
|
|
865
1097
|
? {
|
|
866
1098
|
available: true,
|
|
1099
|
+
directoryBookmarks: true,
|
|
1100
|
+
queueControl: true,
|
|
1101
|
+
cookieCredentials: app.permissions.includes("credentials.cookies") &&
|
|
1102
|
+
(sharedTools
|
|
1103
|
+
? !!sharedTools.cookies
|
|
1104
|
+
: options.host === "hub" && !options.sharedToolJobs),
|
|
867
1105
|
...toolJobLimits,
|
|
1106
|
+
ownership: projectOwnedTools ? "project" : "session",
|
|
1107
|
+
executionRevision: sharedTools?.scope.revision ?? panel.revision,
|
|
1108
|
+
sharedAcrossDevices: projectOwnedTools,
|
|
1109
|
+
continuesAfterDisconnect: true,
|
|
1110
|
+
continuesAfterLogout: projectOwnedTools,
|
|
868
1111
|
maxHttpResultBytes: toolJobLimits.maxRecordBytes + 128 * 1024,
|
|
1112
|
+
...(app.permissions.includes("resources.directRead")
|
|
1113
|
+
? { directRead: true }
|
|
1114
|
+
: {}),
|
|
869
1115
|
}
|
|
870
1116
|
: undefined,
|
|
871
1117
|
limits: {
|
|
@@ -878,25 +1124,52 @@ export function createPanelRuntime(options) {
|
|
|
878
1124
|
consentTimeoutMs: 50000,
|
|
879
1125
|
},
|
|
880
1126
|
}),
|
|
881
|
-
...(app.permissions.includes("
|
|
1127
|
+
...(app.permissions.includes("resources")
|
|
882
1128
|
? {
|
|
883
1129
|
methodLimits: {
|
|
884
|
-
"
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1130
|
+
"resources.recordAudio": { timeoutMs: 30 * 60 * 1000 },
|
|
1131
|
+
...(app.permissions.includes("process")
|
|
1132
|
+
? {
|
|
1133
|
+
"tasks.start": {
|
|
1134
|
+
maxParamsBytes: toolJobLimits.maxInputBytes,
|
|
1135
|
+
maxResultBytes: toolJobLimits.maxRecordBytes + 128 * 1024,
|
|
1136
|
+
timeoutMs: 30 * 60 * 1000,
|
|
1137
|
+
},
|
|
1138
|
+
"tasks.find": { maxResultBytes: toolJobLimits.maxRecordBytes + 128 * 1024 },
|
|
1139
|
+
"tasks.get": { maxResultBytes: toolJobLimits.maxRecordBytes + 128 * 1024 },
|
|
1140
|
+
"tasks.retry": {
|
|
1141
|
+
maxResultBytes: toolJobLimits.maxRecordBytes + 128 * 1024,
|
|
1142
|
+
timeoutMs: 30 * 60 * 1000,
|
|
1143
|
+
},
|
|
1144
|
+
"tasks.cancel": {
|
|
1145
|
+
maxResultBytes: toolJobLimits.maxRecordBytes + 128 * 1024,
|
|
1146
|
+
},
|
|
1147
|
+
}
|
|
1148
|
+
: {}),
|
|
892
1149
|
},
|
|
893
1150
|
}
|
|
894
1151
|
: {}),
|
|
895
1152
|
},
|
|
896
1153
|
host: options.host,
|
|
897
1154
|
availableMethods: METHODS.filter((method) => {
|
|
1155
|
+
if (method === "credentials.cookies.listForTask" ||
|
|
1156
|
+
method === "credentials.cookies.authorizeProcess")
|
|
1157
|
+
return (app.permissions.includes("credentials.cookies") &&
|
|
1158
|
+
app.permissions.includes("process") &&
|
|
1159
|
+
app.permissions.includes("resources") &&
|
|
1160
|
+
(sharedTools
|
|
1161
|
+
? method === "credentials.cookies.authorizeProcess"
|
|
1162
|
+
? !!sharedTools.cookies?.materialize
|
|
1163
|
+
: !!sharedTools.cookies
|
|
1164
|
+
: options.host === "hub" && !options.sharedToolJobs));
|
|
898
1165
|
if (method.startsWith("agent.task.") && !agentTasks)
|
|
899
1166
|
return false;
|
|
1167
|
+
if (method.startsWith("automations."))
|
|
1168
|
+
return (!!options.automations &&
|
|
1169
|
+
(!method.endsWith("IfRevision") ||
|
|
1170
|
+
options.automations.conditionalMutations === true) &&
|
|
1171
|
+
["automations.manage", "context.workspace", "context.session"].every((permission) => app.permissions.includes(permission)) &&
|
|
1172
|
+
!!input.sessionId);
|
|
900
1173
|
if (method.startsWith("tasks."))
|
|
901
1174
|
return app.permissions.includes("process") && app.permissions.includes("resources");
|
|
902
1175
|
if ([
|
|
@@ -923,6 +1196,7 @@ export function createPanelRuntime(options) {
|
|
|
923
1196
|
context.busy = false;
|
|
924
1197
|
}
|
|
925
1198
|
const grant = {
|
|
1199
|
+
sharedTools,
|
|
926
1200
|
guestId: ++nextGuest,
|
|
927
1201
|
id,
|
|
928
1202
|
asset,
|
|
@@ -948,6 +1222,27 @@ export function createPanelRuntime(options) {
|
|
|
948
1222
|
grants.set(id, grant);
|
|
949
1223
|
byGuest.set(grant.guestId, grant);
|
|
950
1224
|
assets.set(asset, grant);
|
|
1225
|
+
if (sharedTools) {
|
|
1226
|
+
// Keep event order across asynchronous viewer-authorization checks. A slow
|
|
1227
|
+
// or revoked viewer cannot stop the owning coordinator or another viewer.
|
|
1228
|
+
let events = Promise.resolve();
|
|
1229
|
+
let pendingEvents = 0;
|
|
1230
|
+
grant.unsubscribeTools = sharedTools.subscribe((job) => {
|
|
1231
|
+
if (++pendingEvents > 1024) {
|
|
1232
|
+
remove(grant);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
events = events
|
|
1236
|
+
.then(async () => {
|
|
1237
|
+
// The native Host may have revoked project trust independently of
|
|
1238
|
+
// this Web session. Check both authorities before sending metadata.
|
|
1239
|
+
if ((await sharedTools.has(job.id)) && (await authorized(grant)))
|
|
1240
|
+
emit(grant, "tasks.changed", job);
|
|
1241
|
+
pendingEvents--;
|
|
1242
|
+
})
|
|
1243
|
+
.catch(() => remove(grant));
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
951
1246
|
if (!(await authorized(grant))) {
|
|
952
1247
|
remove(grant);
|
|
953
1248
|
error((await options.isAuthorized(request)) ? 410 : 401, "面板授权已失效,请重新打开。");
|
|
@@ -965,6 +1260,9 @@ export function createPanelRuntime(options) {
|
|
|
965
1260
|
}
|
|
966
1261
|
}
|
|
967
1262
|
async function call(grant, input) {
|
|
1263
|
+
return panelExecutionGate.run({ appId: grant.app.id, projectPath: options.bindingCwd ?? options.cwd }, () => callAdmitted(grant, input));
|
|
1264
|
+
}
|
|
1265
|
+
async function callAdmitted(grant, input) {
|
|
968
1266
|
if (!(await authorized(grant)))
|
|
969
1267
|
error(410, "面板授权已失效,请重新打开。");
|
|
970
1268
|
if (Object.keys(input).some((key) => !["method", "params"].includes(key)))
|
|
@@ -1003,6 +1301,23 @@ export function createPanelRuntime(options) {
|
|
|
1003
1301
|
const permission = methodPermission(method);
|
|
1004
1302
|
if (!grant.app.permissions.includes(permission))
|
|
1005
1303
|
error(403, "面板未声明这个权限。");
|
|
1304
|
+
if (method.startsWith("automations.")) {
|
|
1305
|
+
if (!options.automations)
|
|
1306
|
+
error(501, "当前执行环境尚未接入面板自动化。");
|
|
1307
|
+
if (method.endsWith("IfRevision") && !options.automations.conditionalMutations)
|
|
1308
|
+
error(501, "当前执行环境尚未接入自动化版本检查。");
|
|
1309
|
+
if (!grant.app.permissions.includes("context.workspace") ||
|
|
1310
|
+
!grant.app.permissions.includes("context.session") ||
|
|
1311
|
+
typeof grant.context.sessionId !== "string")
|
|
1312
|
+
error(403, "自动化需要已绑定的项目与对话。");
|
|
1313
|
+
return options.automations.call({
|
|
1314
|
+
appId: grant.app.id,
|
|
1315
|
+
cwd: options.cwd,
|
|
1316
|
+
sessionId: grant.context.sessionId,
|
|
1317
|
+
revision: grant.revision,
|
|
1318
|
+
isAuthorized: () => authorized(grant),
|
|
1319
|
+
}, method, params);
|
|
1320
|
+
}
|
|
1006
1321
|
const processOwner = {
|
|
1007
1322
|
guestId: grant.guestId,
|
|
1008
1323
|
appId: grant.app.id,
|
|
@@ -1011,6 +1326,49 @@ export function createPanelRuntime(options) {
|
|
|
1011
1326
|
send: (event, payload) => emit(grant, event, payload),
|
|
1012
1327
|
};
|
|
1013
1328
|
if (method.startsWith("resources.")) {
|
|
1329
|
+
if (method === "resources.recordAudio") {
|
|
1330
|
+
const value = (params ?? {});
|
|
1331
|
+
const maxDurationSeconds = value.maxDurationSeconds ?? 300;
|
|
1332
|
+
const maxBytes = value.maxBytes ?? 16 * 1024 * 1024;
|
|
1333
|
+
if (!value ||
|
|
1334
|
+
typeof value !== "object" ||
|
|
1335
|
+
Array.isArray(value) ||
|
|
1336
|
+
Object.keys(value).some((key) => !["maxDurationSeconds", "maxBytes"].includes(key)) ||
|
|
1337
|
+
!Number.isInteger(maxDurationSeconds) ||
|
|
1338
|
+
Number(maxDurationSeconds) < 1 ||
|
|
1339
|
+
Number(maxDurationSeconds) > 600 ||
|
|
1340
|
+
!Number.isInteger(maxBytes) ||
|
|
1341
|
+
Number(maxBytes) < 1 ||
|
|
1342
|
+
Number(maxBytes) > 25 * 1024 * 1024)
|
|
1343
|
+
error(400, "录音限制无效。");
|
|
1344
|
+
// This effect only opens a trusted chooser. Device access and upload each
|
|
1345
|
+
// require an explicit user action; the iframe retains microphone=().
|
|
1346
|
+
return { effect: "resources.recordAudio", maxDurationSeconds, maxBytes };
|
|
1347
|
+
}
|
|
1348
|
+
if (method === "resources.open" || method === "resources.preview") {
|
|
1349
|
+
const value = params;
|
|
1350
|
+
if (!value ||
|
|
1351
|
+
Object.keys(value).some((key) => key !== "assetId") ||
|
|
1352
|
+
typeof value.assetId !== "string" ||
|
|
1353
|
+
!/^(?:asset|external)-[a-f0-9]{64}$/.test(value.assetId))
|
|
1354
|
+
error(400, "文件资源标识无效。");
|
|
1355
|
+
const asset = await resources.get({ appId: grant.app.id, projectPath: options.bindingCwd ?? options.cwd }, value.assetId);
|
|
1356
|
+
if (!(await authorized(grant)))
|
|
1357
|
+
error(410, "面板授权已失效。");
|
|
1358
|
+
if (method === "resources.preview") {
|
|
1359
|
+
if (!INLINE_RESOURCE_TYPES.has(asset.mimeType))
|
|
1360
|
+
error(400, "此资源类型不能在面板内播放或显示。");
|
|
1361
|
+
return {
|
|
1362
|
+
asset,
|
|
1363
|
+
url: `${grant.origin}${publicPathPrefix}${ASSETS}${grant.asset}/${INLINE_RESOURCE_PATH}${asset.id}`,
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
return {
|
|
1367
|
+
effect: "resources.open",
|
|
1368
|
+
asset,
|
|
1369
|
+
url: `${ROOT}${grant.id}/resources/${asset.id}`,
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1014
1372
|
if ([
|
|
1015
1373
|
"resources.materialize",
|
|
1016
1374
|
"resources.capture",
|
|
@@ -1021,6 +1379,51 @@ export function createPanelRuntime(options) {
|
|
|
1021
1379
|
error(403, "资源工具交接需要process权限。");
|
|
1022
1380
|
return resources.dispatch({ appId: grant.app.id, projectPath: options.bindingCwd ?? options.cwd }, method, params, { resolveDirectory: (handle) => processes.directoryPath(processOwner, handle) });
|
|
1023
1381
|
}
|
|
1382
|
+
if (method === "credentials.cookies.listForTask") {
|
|
1383
|
+
const value = await cookieAccess(grant).list(params?.url);
|
|
1384
|
+
if (!(await authorized(grant)))
|
|
1385
|
+
error(410, "面板授权已失效。");
|
|
1386
|
+
return value;
|
|
1387
|
+
}
|
|
1388
|
+
if (method === "credentials.cookies.authorizeProcess") {
|
|
1389
|
+
const value = params;
|
|
1390
|
+
const selection = taskCookieSelection({
|
|
1391
|
+
credentialId: value?.credentialId,
|
|
1392
|
+
url: value?.url,
|
|
1393
|
+
revision: value?.revision,
|
|
1394
|
+
});
|
|
1395
|
+
const access = cookieAccess(grant);
|
|
1396
|
+
if (!access.materialize)
|
|
1397
|
+
error(501, "当前 Host 不支持临时进程账号授权。");
|
|
1398
|
+
const executable = processes.executableName(processOwner, value.executableHandle);
|
|
1399
|
+
const account = await access.check(selection);
|
|
1400
|
+
if (!(await confirm(grant, "允许程序使用这个已保存账号?", `程序:${executable}\n账号:${account.label}\n站点:${new URL(selection.url).hostname}\n登录信息仅通过私密文件交给所选程序;关闭页面或撤销授权会停止使用。`)))
|
|
1401
|
+
return { authorized: false, cancelled: true };
|
|
1402
|
+
const validate = async () => {
|
|
1403
|
+
await access.check(selection);
|
|
1404
|
+
if (!(await authorized(grant)))
|
|
1405
|
+
error(410, "面板授权已失效。");
|
|
1406
|
+
};
|
|
1407
|
+
await validate();
|
|
1408
|
+
const lease = await access.materialize(selection);
|
|
1409
|
+
try {
|
|
1410
|
+
await validate();
|
|
1411
|
+
const sealed = await processes.grantFileArgument(processOwner, {
|
|
1412
|
+
executableHandle: value.executableHandle,
|
|
1413
|
+
argumentName: "--cookies",
|
|
1414
|
+
path: lease.path,
|
|
1415
|
+
validate,
|
|
1416
|
+
cleanup: () => {
|
|
1417
|
+
void lease.cleanup().catch(() => { });
|
|
1418
|
+
},
|
|
1419
|
+
});
|
|
1420
|
+
return { authorized: true, fileArgumentHandle: sealed.handle, count: lease.count };
|
|
1421
|
+
}
|
|
1422
|
+
catch (cause) {
|
|
1423
|
+
await lease.cleanup();
|
|
1424
|
+
throw cause;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1024
1427
|
if (method === "credentials.connections.list")
|
|
1025
1428
|
return panelConnections(options.cwd);
|
|
1026
1429
|
if (method === "credentials.connections.authorizeProcess") {
|
|
@@ -1089,10 +1492,14 @@ export function createPanelRuntime(options) {
|
|
|
1089
1492
|
};
|
|
1090
1493
|
}
|
|
1091
1494
|
if (method === "filesystem.restoreDirectory") {
|
|
1495
|
+
await options.authorizePanelDirectory?.(grant.app, options.bindingCwd ?? options.cwd, options.cwd);
|
|
1092
1496
|
const saved = directoryBookmarks.restore(grant.app.id, options.bindingCwd ?? options.cwd, params?.bookmark);
|
|
1093
|
-
if (
|
|
1497
|
+
if (!options.authorizePanelDirectory &&
|
|
1498
|
+
saved !== (await realpath(options.cwd)) &&
|
|
1499
|
+
saved !== (await realpath(join(options.cwd, "downloads"))))
|
|
1094
1500
|
throw new PanelBridgeError("PERMISSION_DENIED", "Saved server directory is unavailable");
|
|
1095
1501
|
const restored = await processes.grantDirectory(owner, saved);
|
|
1502
|
+
await options.authorizePanelDirectory?.(grant.app, options.bindingCwd ?? options.cwd, options.cwd);
|
|
1096
1503
|
directoryBookmarks.restore(grant.app.id, options.bindingCwd ?? options.cwd, params?.bookmark);
|
|
1097
1504
|
return { ...restored, bookmark: params.bookmark };
|
|
1098
1505
|
}
|
|
@@ -1111,12 +1518,13 @@ export function createPanelRuntime(options) {
|
|
|
1111
1518
|
if (method === "filesystem.pickDirectory" &&
|
|
1112
1519
|
!(await confirm(grant, "使用服务端下载目录?", directory)))
|
|
1113
1520
|
return null;
|
|
1521
|
+
await options.authorizePanelDirectory?.(grant.app, options.bindingCwd ?? options.cwd, options.cwd);
|
|
1114
1522
|
if (name !== "project")
|
|
1115
1523
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
1116
1524
|
if (!(await authorized(grant)))
|
|
1117
1525
|
error(410, "面板授权已失效。");
|
|
1118
1526
|
const selected = await processes.grantDirectory(owner, directory);
|
|
1119
|
-
if (method !== "filesystem.pickDirectory")
|
|
1527
|
+
if (method !== "filesystem.pickDirectory" && !["downloads", "project"].includes(String(name)))
|
|
1120
1528
|
return selected;
|
|
1121
1529
|
const bookmark = directoryBookmarks.remember(grant.app.id, options.bindingCwd ?? options.cwd, selected.path);
|
|
1122
1530
|
if (!(await authorized(grant)))
|
|
@@ -1175,7 +1583,9 @@ export function createPanelRuntime(options) {
|
|
|
1175
1583
|
}, method, params);
|
|
1176
1584
|
}
|
|
1177
1585
|
return {
|
|
1178
|
-
activeTaskCount: () => (agentTasks?.activeTaskCount?.() ?? 0) +
|
|
1586
|
+
activeTaskCount: () => (agentTasks?.activeTaskCount?.() ?? 0) +
|
|
1587
|
+
(toolJobs?.activeCount() ?? 0) +
|
|
1588
|
+
(options.sharedToolJobs?.activeCount(options.bindingCwd ?? options.cwd) ?? 0),
|
|
1179
1589
|
async panelAction(ownerId, sessionId, input) {
|
|
1180
1590
|
const candidates = [];
|
|
1181
1591
|
for (const grant of grants.values()) {
|
|
@@ -1213,12 +1623,13 @@ export function createPanelRuntime(options) {
|
|
|
1213
1623
|
for (const grant of grants.values())
|
|
1214
1624
|
if (!appId || grant.app.id === appId)
|
|
1215
1625
|
remove(grant);
|
|
1626
|
+
const sharedInvalidation = options.sharedToolJobs?.invalidate(options.bindingCwd ?? options.cwd, appId);
|
|
1216
1627
|
if (!toolJobs)
|
|
1217
|
-
return Promise.resolve();
|
|
1628
|
+
return sharedInvalidation ?? Promise.resolve();
|
|
1218
1629
|
const appIds = appId
|
|
1219
1630
|
? [appId]
|
|
1220
1631
|
: [...new Set([...jobOwners.values()].map((record) => record.scope.appId))];
|
|
1221
|
-
return Promise.all(appIds.map((id) => toolJobs.cancelApp(id))).then(() => undefined);
|
|
1632
|
+
return Promise.all([sharedInvalidation, ...appIds.map((id) => toolJobs.cancelApp(id))]).then(() => undefined);
|
|
1222
1633
|
},
|
|
1223
1634
|
async close() {
|
|
1224
1635
|
closed = true;
|
|
@@ -1232,6 +1643,7 @@ export function createPanelRuntime(options) {
|
|
|
1232
1643
|
preparation.cancelled = true;
|
|
1233
1644
|
try {
|
|
1234
1645
|
await toolJobs?.shutdown();
|
|
1646
|
+
await taskCookies?.shutdown();
|
|
1235
1647
|
}
|
|
1236
1648
|
finally {
|
|
1237
1649
|
processes.close();
|
|
@@ -1252,7 +1664,10 @@ export function createPanelRuntime(options) {
|
|
|
1252
1664
|
return true;
|
|
1253
1665
|
}
|
|
1254
1666
|
const directoryMatch = /^\/api\/v1\/panels\/runtime\/([\w-]+)\/directory\/([a-f0-9-]{36})$/.exec(url.pathname);
|
|
1255
|
-
const
|
|
1667
|
+
const resourceMatch = /^\/api\/v1\/panels\/runtime\/([\w-]+)\/resources\/((?:asset|external)-[a-f0-9]{64})$/.exec(url.pathname);
|
|
1668
|
+
const match = /^\/api\/v1\/panels\/runtime\/([\w-]+)(?:\/(call|renew|events|confirm|tool-results))?$/.exec(url.pathname) ??
|
|
1669
|
+
directoryMatch ??
|
|
1670
|
+
resourceMatch;
|
|
1256
1671
|
const grant = match && grants.get(match[1]);
|
|
1257
1672
|
if (!grant)
|
|
1258
1673
|
error(410, "面板连接已失效,请关闭后重新打开。");
|
|
@@ -1261,7 +1676,26 @@ export function createPanelRuntime(options) {
|
|
|
1261
1676
|
activeGrant = grant;
|
|
1262
1677
|
if (!(await authorized(grant)))
|
|
1263
1678
|
error(410, "面板连接已失效,请关闭后重新打开。");
|
|
1264
|
-
if (
|
|
1679
|
+
if (resourceMatch) {
|
|
1680
|
+
if (!grant.app.permissions.includes("resources"))
|
|
1681
|
+
error(403, "面板未声明资源权限。");
|
|
1682
|
+
if ([...url.searchParams.keys()].some((key) => !["download", "workspace"].includes(key)) ||
|
|
1683
|
+
url.searchParams.getAll("download").length > 1 ||
|
|
1684
|
+
url.searchParams.getAll("workspace").length > 1 ||
|
|
1685
|
+
(url.searchParams.has("download") && url.searchParams.get("download") !== "1") ||
|
|
1686
|
+
(url.searchParams.has("workspace") && url.searchParams.get("workspace") !== options.cwd))
|
|
1687
|
+
error(400, "文件访问参数无效。");
|
|
1688
|
+
await servePanelResource(request, response, {
|
|
1689
|
+
service: resources,
|
|
1690
|
+
scope: { appId: grant.app.id, projectPath: options.bindingCwd ?? options.cwd },
|
|
1691
|
+
id: resourceMatch[2],
|
|
1692
|
+
download: url.searchParams.get("download") === "1",
|
|
1693
|
+
isAuthorized: async () => currentOwner === grant.owner &&
|
|
1694
|
+
(await options.isAuthorized(request)) &&
|
|
1695
|
+
(await authorized(grant)),
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
else if (directoryMatch) {
|
|
1265
1699
|
if (!grant.app.permissions.includes("process"))
|
|
1266
1700
|
error(403, "面板未声明这个权限。");
|
|
1267
1701
|
const root = processes.directoryPath({
|
|
@@ -1346,7 +1780,7 @@ export function createPanelRuntime(options) {
|
|
|
1346
1780
|
error(405, "不支持这个面板请求。");
|
|
1347
1781
|
}
|
|
1348
1782
|
catch (cause) {
|
|
1349
|
-
let status = cause instanceof PanelManagementError
|
|
1783
|
+
let status = cause instanceof PanelManagementError || cause instanceof PanelExecutionBusyError
|
|
1350
1784
|
? cause.status
|
|
1351
1785
|
: cause instanceof PanelBridgeError && cause.code === "RATE_LIMITED"
|
|
1352
1786
|
? 429
|
|
@@ -1398,6 +1832,27 @@ export function createPanelRuntime(options) {
|
|
|
1398
1832
|
return true;
|
|
1399
1833
|
}
|
|
1400
1834
|
const path = parts.map(decodeURIComponent).join("/");
|
|
1835
|
+
if (path.startsWith(INLINE_RESOURCE_PATH)) {
|
|
1836
|
+
if (!grant.app.permissions.includes("resources"))
|
|
1837
|
+
error(403, "面板未声明资源权限。");
|
|
1838
|
+
const id = path.slice(INLINE_RESOURCE_PATH.length);
|
|
1839
|
+
if (!/^(?:asset|external)-[a-f0-9]{64}$/.test(id))
|
|
1840
|
+
error(404, "文件资源标识无效。");
|
|
1841
|
+
const scope = { appId: grant.app.id, projectPath: options.bindingCwd ?? options.cwd };
|
|
1842
|
+
const asset = await resources.get(scope, id).catch(() => error(404, "文件资源不可用。"));
|
|
1843
|
+
if (!INLINE_RESOURCE_TYPES.has(asset.mimeType))
|
|
1844
|
+
error(403, "资源类型不可预览。");
|
|
1845
|
+
// A direct navigation must not turn project data into an active document.
|
|
1846
|
+
response.setHeader("Content-Security-Policy", "default-src 'none'; sandbox");
|
|
1847
|
+
await servePanelResource(request, response, {
|
|
1848
|
+
service: resources,
|
|
1849
|
+
scope,
|
|
1850
|
+
id,
|
|
1851
|
+
download: false,
|
|
1852
|
+
isAuthorized: () => authorized(grant),
|
|
1853
|
+
});
|
|
1854
|
+
return true;
|
|
1855
|
+
}
|
|
1401
1856
|
let bytes;
|
|
1402
1857
|
if (path === "_codeshell_bridge.js")
|
|
1403
1858
|
bytes = Buffer.from(bridgeScript(grant.id, grant.origin));
|
|
@@ -1417,6 +1872,9 @@ export function createPanelRuntime(options) {
|
|
|
1417
1872
|
bytes = Buffer.from(injectBridge(bytes.toString("utf8"), source));
|
|
1418
1873
|
const resourceOrigin = grant.origin;
|
|
1419
1874
|
const assetSource = resourceOrigin + source;
|
|
1875
|
+
// Panels export their own generated files (for example JSON backups).
|
|
1876
|
+
// Download permission does not grant origin access, network fetches or popups.
|
|
1877
|
+
// The embedding iframe must enable the same permission.
|
|
1420
1878
|
response.setHeader("Content-Security-Policy", "default-src 'none'; script-src " +
|
|
1421
1879
|
assetSource +
|
|
1422
1880
|
"; style-src " +
|
|
@@ -1427,7 +1885,7 @@ export function createPanelRuntime(options) {
|
|
|
1427
1885
|
assetSource +
|
|
1428
1886
|
" blob:; font-src " +
|
|
1429
1887
|
assetSource +
|
|
1430
|
-
" data:; connect-src 'none'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'; sandbox allow-scripts; frame-ancestors " +
|
|
1888
|
+
" data:; connect-src 'none'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'; sandbox allow-scripts allow-downloads; frame-ancestors " +
|
|
1431
1889
|
resourceOrigin);
|
|
1432
1890
|
response.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=(), display-capture=()");
|
|
1433
1891
|
}
|