@cjhyy/code-shell-server 0.9.29 → 0.9.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/links/http.js +2 -0
- package/dist/links/remote-catalog.d.ts +1 -1
- package/dist/links/remote-catalog.js +5 -1
- package/dist/links/service.js +33 -1
- package/dist/mobile-remote/mobile-chat-turn.d.ts +3 -1
- package/dist/mobile-remote/mobile-chat-turn.js +12 -3
- package/dist/mobile-remote/mobile-client-event-validator.js +15 -0
- package/dist/mobile-remote/remote-host-manager.d.ts +4 -0
- package/dist/mobile-remote/remote-host-manager.js +37 -2
- package/dist/panels/browser-capture.d.ts +15 -0
- package/dist/panels/browser-capture.js +35 -0
- package/dist/panels/management.d.ts +4 -2
- package/dist/panels/management.js +6 -4
- package/dist/panels/runtime.js +27 -1
- package/dist/remote-relay/connector.d.ts +3 -1
- package/dist/remote-relay/connector.js +56 -8
- package/dist/remote-relay/local-stream.d.ts +3 -1
- package/package.json +5 -5
package/dist/links/http.js
CHANGED
|
@@ -113,6 +113,7 @@ export function createLinkHttp(options) {
|
|
|
113
113
|
"connectionId",
|
|
114
114
|
"expectedRevision",
|
|
115
115
|
"authModeId",
|
|
116
|
+
"resourceUrl",
|
|
116
117
|
]);
|
|
117
118
|
result = await service.startAuthorization(owner, input, input.authModeId);
|
|
118
119
|
}
|
|
@@ -170,6 +171,7 @@ export function createLinkHttp(options) {
|
|
|
170
171
|
"connectionId",
|
|
171
172
|
"expectedRevision",
|
|
172
173
|
...(token ? ["token"] : []),
|
|
174
|
+
...(url.pathname.endsWith("/remote") ? ["resourceUrl"] : []),
|
|
173
175
|
]);
|
|
174
176
|
result = token
|
|
175
177
|
? await service.connectToken(owner, input)
|
|
@@ -8,7 +8,7 @@ export declare const REMOTE_LINK_ADAPTERS: ({
|
|
|
8
8
|
id: "github";
|
|
9
9
|
name: "GitHub";
|
|
10
10
|
group: "repositories";
|
|
11
|
-
actions: readonly ["list_repositories", "get_readme", "get_file", "list_issues", "get_issue", "list_pull_requests", "get_pull_request", "create_issue", "get_repository", "get_starred", "set_starred"];
|
|
11
|
+
actions: readonly ["list_repositories", "get_readme", "get_file", "list_issues", "get_issue", "list_pull_requests", "get_pull_request", "create_issue", "update_issue", "get_repository", "get_starred", "set_starred"];
|
|
12
12
|
} | {
|
|
13
13
|
methodId: string;
|
|
14
14
|
authModeId: string;
|
|
@@ -20,10 +20,14 @@ export async function readRemoteProviderCatalog(configuration) {
|
|
|
20
20
|
/** A legacy v1 service has no catalog. Its one already-reviewed adapter remains compatible. */
|
|
21
21
|
export async function readRemoteProviderCapabilities(configuration) {
|
|
22
22
|
const issuer = beginRemoteLinkAuthorization(configuration).configuration.issuer;
|
|
23
|
+
const scopes = REMOTE_LINK_ADAPTERS.flatMap((adapter) => adapter.scopes);
|
|
24
|
+
const capabilities = JSON.stringify({ version: 1, scopes });
|
|
25
|
+
if (scopes.length > 128 || Buffer.byteLength(capabilities) > 4_096)
|
|
26
|
+
throw new Error("Host Link capabilities exceed catalog negotiation limits");
|
|
23
27
|
const response = await fetch(new URL("/api/v1/links/providers", issuer), {
|
|
24
28
|
redirect: "error",
|
|
25
29
|
signal: AbortSignal.timeout(5_000),
|
|
26
|
-
headers: { Accept: "application/json" },
|
|
30
|
+
headers: { Accept: "application/json", "X-CodeShell-Link-Capabilities": capabilities },
|
|
27
31
|
});
|
|
28
32
|
if ([404, 501].includes(response.status)) {
|
|
29
33
|
await response.body?.cancel();
|
package/dist/links/service.js
CHANGED
|
@@ -186,6 +186,14 @@ export function createLinkService(options = {}) {
|
|
|
186
186
|
id: meta.linkAccountId,
|
|
187
187
|
label: meta.linkAccountLabel,
|
|
188
188
|
resources: meta.linkResourceLabels?.slice(0, 100) ?? [],
|
|
189
|
+
...(remote && meta.linkResourceGroups
|
|
190
|
+
? {
|
|
191
|
+
resourceGroups: meta.linkResourceGroups.map((group) => ({
|
|
192
|
+
id: group.id,
|
|
193
|
+
items: group.items.map((item) => ({ id: item.id, label: item.label })),
|
|
194
|
+
})),
|
|
195
|
+
}
|
|
196
|
+
: {}),
|
|
189
197
|
},
|
|
190
198
|
capabilityIds: meta.linkCapabilityIds ?? [],
|
|
191
199
|
verifiedAt: meta.linkLastVerifiedAt,
|
|
@@ -664,6 +672,8 @@ export function createLinkService(options = {}) {
|
|
|
664
672
|
async function startAuthorization(context, input, rawAuthModeId) {
|
|
665
673
|
await authorize(context);
|
|
666
674
|
const authModeId = string(rawAuthModeId, 80);
|
|
675
|
+
if (input?.resourceUrl !== undefined && authModeId !== "remote-link")
|
|
676
|
+
throw new LinkServiceError(400, "invalid_request");
|
|
667
677
|
if (authModeId === "remote-link")
|
|
668
678
|
await refreshRemoteCatalog(context);
|
|
669
679
|
const provider = snapshot().providers.find((candidate) => candidate.id === input?.providerId);
|
|
@@ -1029,7 +1039,12 @@ export function createLinkService(options = {}) {
|
|
|
1029
1039
|
checkCapacity();
|
|
1030
1040
|
if (!input ||
|
|
1031
1041
|
input.methodId !== "remote-link" ||
|
|
1032
|
-
!REMOTE_LINK_ADAPTERS.some((adapter) => adapter.id === input.providerId)
|
|
1042
|
+
!REMOTE_LINK_ADAPTERS.some((adapter) => adapter.id === input.providerId) ||
|
|
1043
|
+
(input.resourceUrl !== undefined &&
|
|
1044
|
+
(input.providerId !== "figma" ||
|
|
1045
|
+
!input.connectionId ||
|
|
1046
|
+
typeof input.resourceUrl !== "string" ||
|
|
1047
|
+
!input.resourceUrl)))
|
|
1033
1048
|
throw new LinkServiceError(400, "invalid_request");
|
|
1034
1049
|
const config = options.remoteLink?.();
|
|
1035
1050
|
if (!config)
|
|
@@ -1055,11 +1070,24 @@ export function createLinkService(options = {}) {
|
|
|
1055
1070
|
throw new LinkServiceError(409, "conflict");
|
|
1056
1071
|
}
|
|
1057
1072
|
const prepared = { id, expected, input: { ...input, label: string(input.label) } };
|
|
1073
|
+
if (input.resourceUrl !== undefined && !expected)
|
|
1074
|
+
throw new LinkServiceError(400, "invalid_request");
|
|
1058
1075
|
let attempt;
|
|
1059
1076
|
try {
|
|
1077
|
+
let resourceFiles;
|
|
1078
|
+
if (input.providerId === "figma") {
|
|
1079
|
+
// Recover the old whitelist from Host custody. Browser input can only suggest one new file.
|
|
1080
|
+
const groups = expected?.meta?.linkResourceGroups;
|
|
1081
|
+
if (expected && (!groups || groups.length !== 1 || groups[0]?.id !== "files"))
|
|
1082
|
+
throw new Error("Invalid saved Figma resources");
|
|
1083
|
+
resourceFiles = groups?.[0]?.items.map((item) => item.id) ?? [];
|
|
1084
|
+
if (input.resourceUrl !== undefined)
|
|
1085
|
+
resourceFiles.push(input.resourceUrl);
|
|
1086
|
+
}
|
|
1060
1087
|
attempt = beginRemoteLinkAuthorization(config, now(), {
|
|
1061
1088
|
providerId: input.providerId,
|
|
1062
1089
|
actions: capabilities.actions,
|
|
1090
|
+
...(resourceFiles !== undefined ? { resourceFiles } : {}),
|
|
1063
1091
|
});
|
|
1064
1092
|
}
|
|
1065
1093
|
catch {
|
|
@@ -1123,6 +1151,10 @@ export function createLinkService(options = {}) {
|
|
|
1123
1151
|
},
|
|
1124
1152
|
});
|
|
1125
1153
|
await check();
|
|
1154
|
+
if (job.attempt.providerId === "figma" &&
|
|
1155
|
+
job.prepared.expected &&
|
|
1156
|
+
credential.meta?.linkAccountId !== job.prepared.expected.meta?.linkAccountId)
|
|
1157
|
+
throw new LinkServiceError(409, "conflict");
|
|
1126
1158
|
const guarded = {
|
|
1127
1159
|
ownerId: context.ownerId,
|
|
1128
1160
|
authorize: async () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { markAttachmentsSent, type InputAttachmentMeta } from "../attachment-service.js";
|
|
2
|
-
import { type MobileRunBridge } from "./mobile-run-dispatch.js";
|
|
2
|
+
import { type MobileRunBridge, type MobileRunAcceptance, type MobileRunRequest } from "./mobile-run-dispatch.js";
|
|
3
3
|
import type { MobileUploadService } from "./mobile-upload-service.js";
|
|
4
4
|
import type { MobileAttachmentSummary, MobileImageAttachment, PermissionMode } from "./types.js";
|
|
5
5
|
import type { WorkerFrameMeta } from "../worker-bridge-core.js";
|
|
@@ -20,6 +20,8 @@ export interface DispatchMobileChatTurnInput {
|
|
|
20
20
|
uploads: ChatTurnUploads;
|
|
21
21
|
resolveWorkspace: (sessionId: string, fallbackCwd: string) => Promise<string>;
|
|
22
22
|
markSent?: typeof markAttachmentsSent;
|
|
23
|
+
/** Host-owned alternative producer; receives the same materialized input and ack boundary. */
|
|
24
|
+
submit?: (request: MobileRunRequest) => Promise<MobileRunAcceptance>;
|
|
23
25
|
}
|
|
24
26
|
export type DispatchMobileChatTurnResult = {
|
|
25
27
|
ok: true;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { markAttachmentsSent } from "../attachment-service.js";
|
|
2
2
|
import { stablePromptHash } from "../client-message-id.js";
|
|
3
3
|
import { materializeMobileAttachments } from "./mobile-attachments.js";
|
|
4
|
-
import { injectMobileRunAndAwaitAcceptance } from "./mobile-run-dispatch.js";
|
|
4
|
+
import { injectMobileRunAndAwaitAcceptance, } from "./mobile-run-dispatch.js";
|
|
5
5
|
async function settleClaims(uploads, deviceId, claims, action) {
|
|
6
6
|
await Promise.allSettled(claims.map((claim) => uploads[action](deviceId, claim.uploadId, claim.claimId)));
|
|
7
7
|
}
|
|
@@ -40,7 +40,7 @@ export async function dispatchMobileChatTurn(input) {
|
|
|
40
40
|
}
|
|
41
41
|
const clientMessageId = suppliedClientMessageId ||
|
|
42
42
|
`mobile:${input.sessionId}:${input.runId}:${stablePromptHash(`${text}\0${attachmentHash}`)}`;
|
|
43
|
-
const
|
|
43
|
+
const request = {
|
|
44
44
|
id: input.runId,
|
|
45
45
|
params: {
|
|
46
46
|
task: text,
|
|
@@ -52,7 +52,16 @@ export async function dispatchMobileChatTurn(input) {
|
|
|
52
52
|
attachments: materialized.metas,
|
|
53
53
|
...(input.permissionMode ? { permissionMode: input.permissionMode } : {}),
|
|
54
54
|
},
|
|
55
|
-
}
|
|
55
|
+
};
|
|
56
|
+
let acceptance;
|
|
57
|
+
try {
|
|
58
|
+
acceptance = input.submit
|
|
59
|
+
? await input.submit(request)
|
|
60
|
+
: await injectMobileRunAndAwaitAcceptance(input.bridge, request, input.meta);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
acceptance = { ok: false, message: "The selected runtime rejected the submission" };
|
|
64
|
+
}
|
|
56
65
|
if (!acceptance.ok) {
|
|
57
66
|
await settleClaims(input.uploads, input.deviceId, materialized.claims, "release");
|
|
58
67
|
return { ok: false, message: acceptance.message };
|
|
@@ -41,6 +41,21 @@ export function parseMobileClientEvent(value) {
|
|
|
41
41
|
event.size <= MAX_MOBILE_IMAGE_BYTES;
|
|
42
42
|
break;
|
|
43
43
|
case "session.select":
|
|
44
|
+
valid =
|
|
45
|
+
boundedString(event.sessionId, MAX_ID_LENGTH) &&
|
|
46
|
+
optionalBoundedString(event.recoveryId, 128);
|
|
47
|
+
break;
|
|
48
|
+
case "session.recovery.cancel":
|
|
49
|
+
valid = true;
|
|
50
|
+
break;
|
|
51
|
+
case "session.outputJournal":
|
|
52
|
+
valid =
|
|
53
|
+
boundedString(event.sessionId, 128) &&
|
|
54
|
+
boundedString(event.recoveryId, 128) &&
|
|
55
|
+
boundedString(event.requestId, 128) &&
|
|
56
|
+
optionalBoundedString(event.after, 2048) &&
|
|
57
|
+
optionalBoundedString(event.through, 2048);
|
|
58
|
+
break;
|
|
44
59
|
case "session.history":
|
|
45
60
|
valid = boundedString(event.sessionId, MAX_ID_LENGTH);
|
|
46
61
|
break;
|
|
@@ -36,6 +36,7 @@ export interface RemoteHostStarted {
|
|
|
36
36
|
}
|
|
37
37
|
export interface RemoteHostManagerOptions {
|
|
38
38
|
devices: TrustedDeviceStore;
|
|
39
|
+
outputJournal?: boolean;
|
|
39
40
|
onClientEvent: (event: unknown, ws: WebSocket) => void | Promise<void>;
|
|
40
41
|
/**
|
|
41
42
|
* Absolute path to the built mobile app (out/mobile). Defaults to the
|
|
@@ -116,6 +117,9 @@ export declare class RemoteHostManager extends EventEmitter {
|
|
|
116
117
|
* one phone's session/permission state never leaks onto another's screen.
|
|
117
118
|
* A device with no live socket (raced a disconnect) is a silent no-op. */
|
|
118
119
|
sendToDevice(deviceId: string, event: MobileServerEvent): void;
|
|
120
|
+
/** Reply only to the currently authenticated tab; retired/revoked viewers receive nothing. */
|
|
121
|
+
hasAuthenticatedViewer(viewerId: string, deviceId: string): boolean;
|
|
122
|
+
sendToViewer(viewerId: string, event: MobileServerEvent): void;
|
|
119
123
|
/** Send a raw JSON-RPC worker line to ONLY one authenticated device. */
|
|
120
124
|
sendRawToDevice(deviceId: string, payload: string): void;
|
|
121
125
|
/** Broadcast a raw line (e.g. a mirrored worker→renderer JSON-RPC line) to
|
|
@@ -406,7 +406,11 @@ export class RemoteHostManager extends EventEmitter {
|
|
|
406
406
|
if (!device) {
|
|
407
407
|
return { type: "pair.failed", message: "Pairing token expired or invalid" };
|
|
408
408
|
}
|
|
409
|
-
return {
|
|
409
|
+
return {
|
|
410
|
+
type: "pair.ok",
|
|
411
|
+
device,
|
|
412
|
+
...(this.opts.outputJournal ? { capabilities: { outputJournal: 1 } } : {}),
|
|
413
|
+
};
|
|
410
414
|
}
|
|
411
415
|
if (event.type === "auth.device") {
|
|
412
416
|
let device;
|
|
@@ -418,7 +422,11 @@ export class RemoteHostManager extends EventEmitter {
|
|
|
418
422
|
}
|
|
419
423
|
if (!device)
|
|
420
424
|
return { type: "auth.failed", message: "Device is not trusted" };
|
|
421
|
-
return {
|
|
425
|
+
return {
|
|
426
|
+
type: "auth.ok",
|
|
427
|
+
device,
|
|
428
|
+
...(this.opts.outputJournal ? { capabilities: { outputJournal: 1 } } : {}),
|
|
429
|
+
};
|
|
422
430
|
}
|
|
423
431
|
return undefined;
|
|
424
432
|
}
|
|
@@ -438,6 +446,33 @@ export class RemoteHostManager extends EventEmitter {
|
|
|
438
446
|
}
|
|
439
447
|
}
|
|
440
448
|
}
|
|
449
|
+
/** Reply only to the currently authenticated tab; retired/revoked viewers receive nothing. */
|
|
450
|
+
hasAuthenticatedViewer(viewerId, deviceId) {
|
|
451
|
+
for (const client of this.wss?.clients ?? []) {
|
|
452
|
+
if (client.readyState === client.OPEN &&
|
|
453
|
+
this.authed.get(client) === deviceId &&
|
|
454
|
+
this.viewers.get(client) === viewerId)
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
sendToViewer(viewerId, event) {
|
|
460
|
+
const payload = JSON.stringify(event);
|
|
461
|
+
for (const client of this.wss?.clients ?? []) {
|
|
462
|
+
if (client.readyState === client.OPEN &&
|
|
463
|
+
this.authed.has(client) &&
|
|
464
|
+
this.viewers.get(client) === viewerId) {
|
|
465
|
+
// A selected viewer cannot accumulate an unbounded outbound page queue
|
|
466
|
+
// by issuing new reads while refusing to consume prior replies.
|
|
467
|
+
if (client.bufferedAmount + Buffer.byteLength(payload) > 4 * 1024 * 1024) {
|
|
468
|
+
this.releaseSocket(client);
|
|
469
|
+
client.terminate();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
client.send(payload);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
441
476
|
/** Send a raw JSON-RPC worker line to ONLY one authenticated device. */
|
|
442
477
|
sendRawToDevice(deviceId, payload) {
|
|
443
478
|
for (const client of this.wss?.clients ?? []) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Bounded, domain-neutral requests for the trusted browser's recording chooser. */
|
|
2
|
+
export declare const panelVideoLimits: Readonly<{
|
|
3
|
+
maxDurationSeconds: 1200;
|
|
4
|
+
maxBytes: number;
|
|
5
|
+
defaultDurationSeconds: 300;
|
|
6
|
+
defaultBytes: number;
|
|
7
|
+
}>;
|
|
8
|
+
export interface PanelVideoOptions {
|
|
9
|
+
source: "camera" | "screen";
|
|
10
|
+
microphone: boolean;
|
|
11
|
+
systemAudio: boolean;
|
|
12
|
+
maxDurationSeconds: number;
|
|
13
|
+
maxBytes: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function panelVideoOptions(raw: unknown): PanelVideoOptions;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Bounded, domain-neutral requests for the trusted browser's recording chooser. */
|
|
2
|
+
export const panelVideoLimits = Object.freeze({
|
|
3
|
+
maxDurationSeconds: 1200,
|
|
4
|
+
maxBytes: 200 * 1024 * 1024,
|
|
5
|
+
defaultDurationSeconds: 300,
|
|
6
|
+
defaultBytes: 64 * 1024 * 1024,
|
|
7
|
+
});
|
|
8
|
+
export function panelVideoOptions(raw) {
|
|
9
|
+
const value = raw;
|
|
10
|
+
if (!value ||
|
|
11
|
+
typeof value !== "object" ||
|
|
12
|
+
Array.isArray(value) ||
|
|
13
|
+
Object.keys(value).some((key) => !["source", "microphone", "systemAudio", "maxDurationSeconds", "maxBytes"].includes(key)) ||
|
|
14
|
+
(value.source !== "camera" && value.source !== "screen") ||
|
|
15
|
+
(value.microphone !== undefined && typeof value.microphone !== "boolean") ||
|
|
16
|
+
(value.systemAudio !== undefined && typeof value.systemAudio !== "boolean") ||
|
|
17
|
+
(value.source !== "screen" && value.systemAudio === true))
|
|
18
|
+
throw new Error("视频录制请求无效。");
|
|
19
|
+
const maxDurationSeconds = value.maxDurationSeconds ?? panelVideoLimits.defaultDurationSeconds;
|
|
20
|
+
const maxBytes = value.maxBytes ?? panelVideoLimits.defaultBytes;
|
|
21
|
+
if (!Number.isSafeInteger(maxDurationSeconds) ||
|
|
22
|
+
Number(maxDurationSeconds) < 1 ||
|
|
23
|
+
Number(maxDurationSeconds) > panelVideoLimits.maxDurationSeconds ||
|
|
24
|
+
!Number.isSafeInteger(maxBytes) ||
|
|
25
|
+
Number(maxBytes) < 1 ||
|
|
26
|
+
Number(maxBytes) > panelVideoLimits.maxBytes)
|
|
27
|
+
throw new Error("视频录制限制无效。");
|
|
28
|
+
return {
|
|
29
|
+
source: value.source,
|
|
30
|
+
microphone: value.microphone !== false,
|
|
31
|
+
systemAudio: value.systemAudio === true,
|
|
32
|
+
maxDurationSeconds: Number(maxDurationSeconds),
|
|
33
|
+
maxBytes: Number(maxBytes),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type PanelAppPreview, type PanelAppSourceInput } from "@cjhyy/code-shell-core";
|
|
2
|
-
import { getRefCommit } from "@cjhyy/code-shell-core/internal/skills";
|
|
3
2
|
import type { PanelCompatibility, PanelDiscovery, PanelOperationContext, PanelReview, PanelProjectReview, PanelSnapshot, PanelPackageHistory, PanelPackageRestoreReview } from "./types.js";
|
|
4
3
|
export declare class PanelManagementError extends Error {
|
|
5
4
|
readonly status: number;
|
|
@@ -23,7 +22,10 @@ export interface PanelManagementOptions {
|
|
|
23
22
|
compatibility?: (app: Pick<PanelAppPreview, "permissions" | "agent">) => PanelCompatibility;
|
|
24
23
|
now?: () => number;
|
|
25
24
|
/** Network seam only; production still uses the reviewed Core installer. */
|
|
26
|
-
resolveCommit?:
|
|
25
|
+
resolveCommit?: (info: {
|
|
26
|
+
owner: string;
|
|
27
|
+
repo: string;
|
|
28
|
+
}, ref: string) => Promise<string>;
|
|
27
29
|
}
|
|
28
30
|
export declare function createPanelManagement(options: PanelManagementOptions): {
|
|
29
31
|
snapshot: (appId?: string) => Promise<PanelSnapshot>;
|
|
@@ -3,9 +3,9 @@ import { panelPackageMutationMatches } from "./package-mutation.js";
|
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { lstatSync, mkdirSync, statSync } from "node:fs";
|
|
5
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";
|
|
6
|
+
import { SettingsManager, PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, discoverGitPanelApps, installReviewedLocalPanelApp, invalidateSkillCache, listInstalledPanelApps, inspectProjectPanelApps, listRetainedPanelAppPackages, resolvePanelAppPackage, parsePanelAppPackagePins, retainInstalledPanelApp, userHome, panelAppsRoot, previewLocalPanelApp, resolveGitHubPanelAppCommit, resolvePanelAppBindingPolicy, uninstallPanelApp, } from "@cjhyy/code-shell-core";
|
|
7
7
|
import { mutateJsonFile } from "@cjhyy/code-shell-core/internal";
|
|
8
|
-
import {
|
|
8
|
+
import { parseGithubUrl } from "@cjhyy/code-shell-core/internal/skills";
|
|
9
9
|
const REVIEW_TTL = 8 * 60_000;
|
|
10
10
|
const MAX_REVIEWS = 8;
|
|
11
11
|
const MAX_NETWORK = 2;
|
|
@@ -47,7 +47,7 @@ export function publicPanelError(error) {
|
|
|
47
47
|
return new PanelManagementError(503, "github_unavailable", "GitHub 暂时不可用,请稍后重试。");
|
|
48
48
|
if (/(?:ENOTFOUND|EAI_AGAIN|ECONNRESET|ECONNREFUSED|UND_ERR_SOCKET)/.test(code) ||
|
|
49
49
|
/^(?:fetch failed|Failed to fetch)$/i.test(message) ||
|
|
50
|
-
/GitHub source download failed/i.test(message))
|
|
50
|
+
/GitHub source (?:download|ref lookup) failed/i.test(message))
|
|
51
51
|
return new PanelManagementError(502, "github_network", "服务端无法连接 GitHub,请检查网络、DNS 或代理设置后重试。");
|
|
52
52
|
if (error instanceof PanelAppInstallError)
|
|
53
53
|
return new PanelManagementError(400, "invalid_source", error.message);
|
|
@@ -352,7 +352,9 @@ export function createPanelManagement(options) {
|
|
|
352
352
|
const ref = parsed.source.ref ?? "HEAD";
|
|
353
353
|
const commit = SHA.test(ref)
|
|
354
354
|
? ref.toLowerCase()
|
|
355
|
-
:
|
|
355
|
+
: options.resolveCommit
|
|
356
|
+
? await options.resolveCommit({ owner: parsed.owner, repo: parsed.repo }, ref)
|
|
357
|
+
: await resolveGitHubPanelAppCommit(parsed.source);
|
|
356
358
|
await guard();
|
|
357
359
|
if (!SHA.test(commit))
|
|
358
360
|
throw new PanelManagementError(502, "invalid_source", "GitHub 没有返回有效的提交版本。");
|
package/dist/panels/runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { panelVideoLimits, panelVideoOptions } from "./browser-capture.js";
|
|
1
2
|
import { panelExecutionGate, PanelExecutionBusyError } from "./execution-gate.js";
|
|
2
3
|
import { randomBytes, createHash } from "node:crypto";
|
|
3
4
|
import { constants } from "node:fs";
|
|
@@ -43,6 +44,8 @@ const METHODS = [
|
|
|
43
44
|
"resources.open",
|
|
44
45
|
"resources.preview",
|
|
45
46
|
"resources.recordAudio",
|
|
47
|
+
"resources.recordVideo",
|
|
48
|
+
"resources.recordVideo.capabilities",
|
|
46
49
|
...panelToolJobMethods,
|
|
47
50
|
"credentials.connections.list",
|
|
48
51
|
"credentials.cookies.listForTask",
|
|
@@ -299,7 +302,7 @@ function bridgeScript(id, origin) {
|
|
|
299
302
|
const requestId = String(++next);
|
|
300
303
|
const timer = setTimeout(() => {
|
|
301
304
|
pending.delete(requestId); reject(new Error("Panel request timed out"));
|
|
302
|
-
}, ["tasks.start", "tasks.retry", "tasks.queue.set", "resources.recordAudio"].includes(method) ? 30 * 60 * 1000 : 60000);
|
|
305
|
+
}, ["tasks.start", "tasks.retry", "tasks.queue.set", "resources.recordAudio", "resources.recordVideo"].includes(method) ? 30 * 60 * 1000 : 60000);
|
|
303
306
|
pending.set(requestId, { resolve, reject, timer });
|
|
304
307
|
try {
|
|
305
308
|
parent.postMessage({ type: "codeshell-panel:call", instanceId: id, requestId, method, params }, origin);
|
|
@@ -1128,6 +1131,7 @@ export function createPanelRuntime(options) {
|
|
|
1128
1131
|
? {
|
|
1129
1132
|
methodLimits: {
|
|
1130
1133
|
"resources.recordAudio": { timeoutMs: 30 * 60 * 1000 },
|
|
1134
|
+
"resources.recordVideo": { timeoutMs: 30 * 60 * 1000 },
|
|
1131
1135
|
...(app.permissions.includes("process")
|
|
1132
1136
|
? {
|
|
1133
1137
|
"tasks.start": {
|
|
@@ -1326,6 +1330,28 @@ export function createPanelRuntime(options) {
|
|
|
1326
1330
|
send: (event, payload) => emit(grant, event, payload),
|
|
1327
1331
|
};
|
|
1328
1332
|
if (method.startsWith("resources.")) {
|
|
1333
|
+
if (method === "resources.recordVideo.capabilities") {
|
|
1334
|
+
if (params !== undefined &&
|
|
1335
|
+
(!params ||
|
|
1336
|
+
typeof params !== "object" ||
|
|
1337
|
+
Array.isArray(params) ||
|
|
1338
|
+
Object.keys(params).length))
|
|
1339
|
+
error(400, "视频录制能力请求无效。");
|
|
1340
|
+
// Device/encoder support belongs to the trusted browser, not this server.
|
|
1341
|
+
return {
|
|
1342
|
+
effect: method,
|
|
1343
|
+
maxDurationSeconds: panelVideoLimits.maxDurationSeconds,
|
|
1344
|
+
maxBytes: panelVideoLimits.maxBytes,
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
if (method === "resources.recordVideo") {
|
|
1348
|
+
try {
|
|
1349
|
+
return { effect: method, ...panelVideoOptions(params) };
|
|
1350
|
+
}
|
|
1351
|
+
catch {
|
|
1352
|
+
error(400, "视频录制请求或限制无效。");
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1329
1355
|
if (method === "resources.recordAudio") {
|
|
1330
1356
|
const value = (params ?? {});
|
|
1331
1357
|
const maxDurationSeconds = value.maxDurationSeconds ?? 300;
|
|
@@ -11,7 +11,9 @@ export interface DeviceRelayConnectorOptions {
|
|
|
11
11
|
relayOrigin: string;
|
|
12
12
|
hostId: string;
|
|
13
13
|
publicOrigin: string;
|
|
14
|
-
credential
|
|
14
|
+
credential?: string;
|
|
15
|
+
/** Main-owned rotating credentials, resolved before each control connection. */
|
|
16
|
+
getCredential?: (signal: AbortSignal) => Promise<string>;
|
|
15
17
|
/** Target is always 127.0.0.1; stopping its Host revokes this capability. */
|
|
16
18
|
localHost: RelayLocalTarget;
|
|
17
19
|
/** Additional trusted CA for private installations; TLS verification stays enabled. */
|
|
@@ -5,7 +5,8 @@ export function createDeviceRelayConnector(options) {
|
|
|
5
5
|
if (!isRelayOrigin(options.relayOrigin) ||
|
|
6
6
|
!isRelayOrigin(options.publicOrigin) ||
|
|
7
7
|
!isRelayHostId(options.hostId) ||
|
|
8
|
-
!isRelayToken(options.credential) ||
|
|
8
|
+
(!options.getCredential && !isRelayToken(options.credential)) ||
|
|
9
|
+
(options.getCredential !== undefined && typeof options.getCredential !== "function") ||
|
|
9
10
|
!options.localHost ||
|
|
10
11
|
!Number.isInteger(options.localHost.port) ||
|
|
11
12
|
options.localHost.port < 1 ||
|
|
@@ -27,6 +28,7 @@ export function createDeviceRelayConnector(options) {
|
|
|
27
28
|
let control;
|
|
28
29
|
let reconnect;
|
|
29
30
|
let failures = 0;
|
|
31
|
+
let credentialAttempt;
|
|
30
32
|
const pending = new Set();
|
|
31
33
|
const streams = new Set();
|
|
32
34
|
const state = (value) => {
|
|
@@ -39,18 +41,62 @@ export function createDeviceRelayConnector(options) {
|
|
|
39
41
|
};
|
|
40
42
|
const track = (done) => {
|
|
41
43
|
pending.add(done);
|
|
42
|
-
void done.finally(() => pending.delete(done));
|
|
44
|
+
void done.finally(() => pending.delete(done)).catch(() => { });
|
|
43
45
|
};
|
|
44
|
-
const
|
|
46
|
+
const retry = (attempt) => {
|
|
47
|
+
if (!running || generation !== attempt || config.localHost.signal.aborted)
|
|
48
|
+
return;
|
|
49
|
+
const delay = Math.min(30_000, 500 * 2 ** Math.min(failures++, 6));
|
|
50
|
+
reconnect = setTimeout(() => track(connect(attempt)), delay * (0.75 + Math.random() * 0.5));
|
|
51
|
+
};
|
|
52
|
+
const connect = async (attempt) => {
|
|
45
53
|
if (!running || generation !== attempt || config.localHost.signal.aborted)
|
|
46
54
|
return;
|
|
47
55
|
state("connecting");
|
|
48
56
|
if (!running || generation !== attempt || config.localHost.signal.aborted)
|
|
49
57
|
return;
|
|
58
|
+
const acquisition = new AbortController();
|
|
59
|
+
credentialAttempt?.abort();
|
|
60
|
+
credentialAttempt = acquisition;
|
|
61
|
+
let credential;
|
|
62
|
+
try {
|
|
63
|
+
credential = config.getCredential
|
|
64
|
+
? await new Promise((resolve, reject) => {
|
|
65
|
+
const abort = () => reject(new Error("Relay credential acquisition cancelled"));
|
|
66
|
+
acquisition.signal.addEventListener("abort", abort, { once: true });
|
|
67
|
+
Promise.resolve()
|
|
68
|
+
.then(() => config.getCredential(acquisition.signal))
|
|
69
|
+
.then(resolve, reject)
|
|
70
|
+
.finally(() => acquisition.signal.removeEventListener("abort", abort))
|
|
71
|
+
.catch(() => { });
|
|
72
|
+
})
|
|
73
|
+
: config.credential;
|
|
74
|
+
if (!isRelayToken(credential))
|
|
75
|
+
throw new Error("Invalid relay credential");
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (!running || generation !== attempt || acquisition.signal.aborted)
|
|
79
|
+
return;
|
|
80
|
+
if (error?.status === 401 ||
|
|
81
|
+
error?.status === 403) {
|
|
82
|
+
running = false;
|
|
83
|
+
state("unauthorized");
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
state("disconnected");
|
|
87
|
+
retry(attempt);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!running ||
|
|
92
|
+
generation !== attempt ||
|
|
93
|
+
acquisition.signal.aborted ||
|
|
94
|
+
config.localHost.signal.aborted)
|
|
95
|
+
return;
|
|
50
96
|
const url = new URL(RELAY_CONNECT_PATH, config.relayOrigin);
|
|
51
97
|
url.protocol = "wss:";
|
|
52
98
|
const ws = new WebSocket(url, {
|
|
53
|
-
headers: { Authorization: `Bearer ${
|
|
99
|
+
headers: { Authorization: `Bearer ${credential}` },
|
|
54
100
|
ca: config.ca,
|
|
55
101
|
followRedirects: false,
|
|
56
102
|
perMessageDeflate: false,
|
|
@@ -79,8 +125,7 @@ export function createDeviceRelayConnector(options) {
|
|
|
79
125
|
state("disconnected");
|
|
80
126
|
if (!running || generation !== attempt || control || config.localHost.signal.aborted)
|
|
81
127
|
return resolve();
|
|
82
|
-
|
|
83
|
-
reconnect = setTimeout(() => connect(attempt), delay * (0.75 + Math.random() * 0.5));
|
|
128
|
+
retry(attempt);
|
|
84
129
|
}
|
|
85
130
|
resolve();
|
|
86
131
|
})));
|
|
@@ -146,7 +191,7 @@ export function createDeviceRelayConnector(options) {
|
|
|
146
191
|
};
|
|
147
192
|
if (streams.size >= RELAY_MAX_STREAMS)
|
|
148
193
|
return failed();
|
|
149
|
-
const stream = openRelayStream(config, message, failed);
|
|
194
|
+
const stream = openRelayStream({ ...config, credential }, message, failed);
|
|
150
195
|
owned.set(message.streamId, stream);
|
|
151
196
|
streams.add(stream);
|
|
152
197
|
const done = stream.done.then(() => {
|
|
@@ -166,7 +211,8 @@ export function createDeviceRelayConnector(options) {
|
|
|
166
211
|
config.localHost.signal.addEventListener("abort", revoke, { once: true });
|
|
167
212
|
running = true;
|
|
168
213
|
failures = 0;
|
|
169
|
-
connect(++generation);
|
|
214
|
+
const acquisition = connect(++generation);
|
|
215
|
+
track(acquisition);
|
|
170
216
|
},
|
|
171
217
|
close() {
|
|
172
218
|
if (closing)
|
|
@@ -174,6 +220,8 @@ export function createDeviceRelayConnector(options) {
|
|
|
174
220
|
running = false;
|
|
175
221
|
config.localHost.signal.removeEventListener("abort", revoke);
|
|
176
222
|
generation++;
|
|
223
|
+
credentialAttempt?.abort();
|
|
224
|
+
credentialAttempt = undefined;
|
|
177
225
|
clearTimeout(reconnect);
|
|
178
226
|
reconnect = undefined;
|
|
179
227
|
control?.terminate();
|
|
@@ -5,6 +5,8 @@ export interface RelayLocalStream {
|
|
|
5
5
|
done: Promise<void>;
|
|
6
6
|
}
|
|
7
7
|
/** One fixed loopback connection. Never accept a target, URL, or port from control messages. */
|
|
8
|
-
export declare function openRelayStream(config: DeviceRelayConnectorOptions
|
|
8
|
+
export declare function openRelayStream(config: DeviceRelayConnectorOptions & {
|
|
9
|
+
credential: string;
|
|
10
|
+
}, message: Extract<RelayControlMessage, {
|
|
9
11
|
type: "open";
|
|
10
12
|
}>, failed: () => void): RelayLocalStream;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cjhyy/code-shell-server",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.31",
|
|
4
4
|
"description": "CodeShell server transport layer — mobile remote HTTP/WS host, pairing, passcode gate, tunnel, rooms, uploads, and session/attachment services. Electron-free; precursor of the auth gateway.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,13 +60,13 @@
|
|
|
60
60
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@cjhyy/code-shell-capability-coding": "0.9.
|
|
64
|
-
"@cjhyy/code-shell-core": "0.9.
|
|
65
|
-
"@cjhyy/code-shell-web": "0.9.
|
|
63
|
+
"@cjhyy/code-shell-capability-coding": "0.9.31",
|
|
64
|
+
"@cjhyy/code-shell-core": "0.9.31",
|
|
65
|
+
"@cjhyy/code-shell-web": "0.9.31",
|
|
66
66
|
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
67
67
|
"@types/ws": "^8.18.1",
|
|
68
68
|
"ws": "^8.21.0",
|
|
69
|
-
"@cjhyy/code-shell-link": "0.9.
|
|
69
|
+
"@cjhyy/code-shell-link": "0.9.31"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
72
72
|
"node": ">=20.10"
|