@cjhyy/code-shell-server 0.9.49 → 0.9.50
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/mobile-remote/mobile-client-event-validator.js +24 -3
- package/dist/mobile-remote/pending-approvals.d.ts +13 -1
- package/dist/mobile-remote/pending-approvals.js +45 -9
- package/dist/mobile-remote/remote-host-manager.d.ts +4 -0
- package/dist/mobile-remote/remote-host-manager.js +8 -0
- package/dist/panels/resources/library.js +17 -3
- package/dist/panels/runtime.js +1 -1
- package/package.json +6 -6
|
@@ -28,7 +28,8 @@ export function parseMobileClientEvent(value) {
|
|
|
28
28
|
break;
|
|
29
29
|
case "chat.send":
|
|
30
30
|
valid =
|
|
31
|
-
|
|
31
|
+
optionalMember(event.permissionMode, PERMISSION_MODES) &&
|
|
32
|
+
boundedOptionalEmptyString(event.text, MAX_MESSAGE_LENGTH) &&
|
|
32
33
|
(event.modelKey === undefined ||
|
|
33
34
|
(boundedString(event.modelKey, MAX_ID_LENGTH) &&
|
|
34
35
|
/^(codex|claude-code)\/[^\r\n\0]*$/.test(event.modelKey))) &&
|
|
@@ -74,20 +75,37 @@ export function parseMobileClientEvent(value) {
|
|
|
74
75
|
((boundedString(event.throughEventId, 512) && event.cursor === undefined) ||
|
|
75
76
|
(boundedString(event.cursor, 2048) && event.throughEventId === undefined));
|
|
76
77
|
break;
|
|
78
|
+
case "session.historyView":
|
|
79
|
+
case "session.historyDetail":
|
|
80
|
+
valid =
|
|
81
|
+
boundedString(event.sessionId, 128) &&
|
|
82
|
+
boundedString(event.recoveryId, 128) &&
|
|
83
|
+
boundedString(event.requestId, 128) &&
|
|
84
|
+
(event.type === "session.historyView"
|
|
85
|
+
? optionalBoundedString(event.before, 2048) &&
|
|
86
|
+
Object.keys(event).every((key) => ["type", "sessionId", "recoveryId", "requestId", "before"].includes(key))
|
|
87
|
+
: boundedString(event.ref, 2048) &&
|
|
88
|
+
optionalBoundedString(event.cursor, 2048) &&
|
|
89
|
+
Object.keys(event).every((key) => ["type", "sessionId", "recoveryId", "requestId", "ref", "cursor"].includes(key)));
|
|
90
|
+
break;
|
|
77
91
|
case "session.outputJournal":
|
|
78
92
|
valid =
|
|
79
93
|
boundedString(event.sessionId, 128) &&
|
|
80
94
|
boundedString(event.recoveryId, 128) &&
|
|
81
95
|
boundedString(event.requestId, 128) &&
|
|
82
96
|
optionalBoundedString(event.after, 2048) &&
|
|
83
|
-
optionalBoundedString(event.through, 2048)
|
|
97
|
+
optionalBoundedString(event.through, 2048) &&
|
|
98
|
+
(event.frozenSnapshot === undefined || event.frozenSnapshot === true) &&
|
|
99
|
+
(event.maxFrames === undefined ||
|
|
100
|
+
(isPositiveInteger(event.maxFrames) && event.maxFrames <= 2048));
|
|
84
101
|
break;
|
|
85
102
|
case "session.history":
|
|
86
103
|
valid = boundedString(event.sessionId, MAX_ID_LENGTH);
|
|
87
104
|
break;
|
|
88
105
|
case "session.create":
|
|
89
106
|
valid =
|
|
90
|
-
|
|
107
|
+
optionalMember(event.permissionMode, PERMISSION_MODES) &&
|
|
108
|
+
optionalBoundedString(event.clientRequestId, MAX_ID_LENGTH) &&
|
|
91
109
|
(event.cwd === undefined || event.cwd === null || boundedPath(event.cwd)) &&
|
|
92
110
|
(event.projectId === undefined ||
|
|
93
111
|
event.projectId === null ||
|
|
@@ -105,6 +123,9 @@ export function parseMobileClientEvent(value) {
|
|
|
105
123
|
boundedString(event.approvalId, MAX_ID_LENGTH) &&
|
|
106
124
|
(event.decision === "approve" || event.decision === "reject") &&
|
|
107
125
|
optionalBoundedString(event.sessionId, MAX_ID_LENGTH) &&
|
|
126
|
+
optionalBoundedString(event.connectionId, MAX_ID_LENGTH) &&
|
|
127
|
+
(event.generation === undefined ||
|
|
128
|
+
(Number.isSafeInteger(event.generation) && event.generation >= 0)) &&
|
|
108
129
|
optionalBoundedString(event.reason, MAX_MESSAGE_LENGTH) &&
|
|
109
130
|
optionalBoundedString(event.answer, MAX_MESSAGE_LENGTH) &&
|
|
110
131
|
optionalMember(event.scope, SCOPES) &&
|
|
@@ -5,15 +5,27 @@
|
|
|
5
5
|
* are stateful: keep the latest unresolved raw JSON-RPC request here and replay
|
|
6
6
|
* it to a phone when that phone binds to the matching session.
|
|
7
7
|
*/
|
|
8
|
+
export interface PendingMobileApprovalRoute {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
requestId: string;
|
|
11
|
+
connectionId?: string;
|
|
12
|
+
generation?: number;
|
|
13
|
+
expiresAt?: number;
|
|
14
|
+
}
|
|
8
15
|
export declare class PendingMobileApprovals {
|
|
16
|
+
private readonly now;
|
|
9
17
|
private readonly bySession;
|
|
10
18
|
private workerGeneration;
|
|
19
|
+
constructor(now?: () => number);
|
|
11
20
|
/** Requests belong to the worker that can answer them, never its successor. */
|
|
12
21
|
setWorkerState(generation: number, alive: boolean): void;
|
|
13
22
|
observeOutboundLine(line: string): void;
|
|
14
|
-
resolve(requestId: string): void;
|
|
23
|
+
resolve(requestId: string, route?: Partial<PendingMobileApprovalRoute>): void;
|
|
15
24
|
replayLines(sessionId: string): string[];
|
|
25
|
+
/** The stored worker envelope, rather than a phone's mutable current session, owns routing. */
|
|
26
|
+
find(sessionId: string, requestId: string): PendingMobileApprovalRoute | undefined;
|
|
16
27
|
/** The desktop has a queue across sessions; return only its current pending requests. */
|
|
17
28
|
replayAllLines(): string[];
|
|
18
29
|
forgetSession(sessionId: string): void;
|
|
30
|
+
private pruneExpired;
|
|
19
31
|
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tracks currently pending core approval requests for mobile clients.
|
|
3
|
-
*
|
|
4
|
-
* Session snapshots intentionally contain only stream events. Approval requests
|
|
5
|
-
* are stateful: keep the latest unresolved raw JSON-RPC request here and replay
|
|
6
|
-
* it to a phone when that phone binds to the matching session.
|
|
7
|
-
*/
|
|
8
1
|
export class PendingMobileApprovals {
|
|
2
|
+
now;
|
|
9
3
|
bySession = new Map();
|
|
10
4
|
workerGeneration;
|
|
5
|
+
constructor(now = Date.now) {
|
|
6
|
+
this.now = now;
|
|
7
|
+
}
|
|
11
8
|
/** Requests belong to the worker that can answer them, never its successor. */
|
|
12
9
|
setWorkerState(generation, alive) {
|
|
13
10
|
if (!alive || this.workerGeneration !== generation)
|
|
@@ -32,6 +29,8 @@ export class PendingMobileApprovals {
|
|
|
32
29
|
return;
|
|
33
30
|
if (typeof requestId !== "string" || !requestId)
|
|
34
31
|
return;
|
|
32
|
+
if (typeof params?.expiresAt === "number" && params.expiresAt <= this.now())
|
|
33
|
+
return;
|
|
35
34
|
let pending = this.bySession.get(sessionId);
|
|
36
35
|
if (!pending) {
|
|
37
36
|
pending = new Map();
|
|
@@ -44,24 +43,61 @@ export class PendingMobileApprovals {
|
|
|
44
43
|
const params = msg.params;
|
|
45
44
|
const requestId = params?.requestId;
|
|
46
45
|
if (typeof requestId === "string" && requestId)
|
|
47
|
-
this.resolve(requestId);
|
|
46
|
+
this.resolve(requestId, params ?? {});
|
|
48
47
|
}
|
|
49
48
|
}
|
|
50
|
-
resolve(requestId) {
|
|
49
|
+
resolve(requestId, route = {}) {
|
|
51
50
|
for (const [sessionId, pending] of this.bySession) {
|
|
51
|
+
if (route.sessionId !== undefined && route.sessionId !== sessionId)
|
|
52
|
+
continue;
|
|
53
|
+
const line = pending.get(requestId);
|
|
54
|
+
if (!line)
|
|
55
|
+
continue;
|
|
56
|
+
const params = JSON.parse(line).params;
|
|
57
|
+
if ((params.connectionId !== undefined || params.generation !== undefined) &&
|
|
58
|
+
(route.sessionId !== sessionId ||
|
|
59
|
+
route.connectionId !== params.connectionId ||
|
|
60
|
+
route.generation !== params.generation))
|
|
61
|
+
continue;
|
|
62
|
+
if ((route.connectionId !== undefined && route.connectionId !== params.connectionId) ||
|
|
63
|
+
(route.generation !== undefined && route.generation !== params.generation))
|
|
64
|
+
continue;
|
|
52
65
|
pending.delete(requestId);
|
|
53
66
|
if (pending.size === 0)
|
|
54
67
|
this.bySession.delete(sessionId);
|
|
55
68
|
}
|
|
56
69
|
}
|
|
57
70
|
replayLines(sessionId) {
|
|
71
|
+
this.pruneExpired();
|
|
58
72
|
return [...(this.bySession.get(sessionId)?.values() ?? [])];
|
|
59
73
|
}
|
|
74
|
+
/** The stored worker envelope, rather than a phone's mutable current session, owns routing. */
|
|
75
|
+
find(sessionId, requestId) {
|
|
76
|
+
this.pruneExpired();
|
|
77
|
+
const line = this.bySession.get(sessionId)?.get(requestId);
|
|
78
|
+
if (!line)
|
|
79
|
+
return undefined;
|
|
80
|
+
const { connectionId, generation, expiresAt } = JSON.parse(line).params;
|
|
81
|
+
return { sessionId, requestId, connectionId, generation, expiresAt };
|
|
82
|
+
}
|
|
60
83
|
/** The desktop has a queue across sessions; return only its current pending requests. */
|
|
61
84
|
replayAllLines() {
|
|
85
|
+
this.pruneExpired();
|
|
62
86
|
return [...this.bySession.values()].flatMap((pending) => [...pending.values()]);
|
|
63
87
|
}
|
|
64
88
|
forgetSession(sessionId) {
|
|
65
89
|
this.bySession.delete(sessionId);
|
|
66
90
|
}
|
|
91
|
+
pruneExpired() {
|
|
92
|
+
const now = this.now();
|
|
93
|
+
for (const [sessionId, pending] of this.bySession) {
|
|
94
|
+
for (const [requestId, line] of pending) {
|
|
95
|
+
const expiresAt = JSON.parse(line).params.expiresAt;
|
|
96
|
+
if (typeof expiresAt === "number" && expiresAt <= now)
|
|
97
|
+
pending.delete(requestId);
|
|
98
|
+
}
|
|
99
|
+
if (!pending.size)
|
|
100
|
+
this.bySession.delete(sessionId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
67
103
|
}
|
|
@@ -40,6 +40,10 @@ export interface RemoteHostManagerOptions {
|
|
|
40
40
|
outputJournal?: boolean;
|
|
41
41
|
/** Enable only when the attached selected-session host implements legacy base pages. */
|
|
42
42
|
legacyBasePages?: boolean;
|
|
43
|
+
/** Enable only when the selected-session reader supports opt-in dense pages. */
|
|
44
|
+
outputJournalPageFrames?: 2048;
|
|
45
|
+
/** Selected-session Host implements verified display views and bounded detail parts. */
|
|
46
|
+
historyView?: boolean;
|
|
43
47
|
onClientEvent: (event: unknown, ws: WebSocket) => void | Promise<void>;
|
|
44
48
|
/**
|
|
45
49
|
* Absolute path to the built mobile app (out/mobile). Defaults to the
|
|
@@ -478,7 +478,11 @@ export class RemoteHostManager extends EventEmitter {
|
|
|
478
478
|
? {
|
|
479
479
|
capabilities: {
|
|
480
480
|
outputJournal: 1,
|
|
481
|
+
...(this.opts.historyView ? { historyView: 1 } : {}),
|
|
481
482
|
...(this.opts.legacyBasePages ? { legacyBasePages: 1 } : {}),
|
|
483
|
+
...(this.opts.outputJournalPageFrames === 2048
|
|
484
|
+
? { outputJournalPageFrames: 2048 }
|
|
485
|
+
: {}),
|
|
482
486
|
},
|
|
483
487
|
}
|
|
484
488
|
: {}),
|
|
@@ -501,7 +505,11 @@ export class RemoteHostManager extends EventEmitter {
|
|
|
501
505
|
? {
|
|
502
506
|
capabilities: {
|
|
503
507
|
outputJournal: 1,
|
|
508
|
+
...(this.opts.historyView ? { historyView: 1 } : {}),
|
|
504
509
|
...(this.opts.legacyBasePages ? { legacyBasePages: 1 } : {}),
|
|
510
|
+
...(this.opts.outputJournalPageFrames === 2048
|
|
511
|
+
? { outputJournalPageFrames: 2048 }
|
|
512
|
+
: {}),
|
|
505
513
|
},
|
|
506
514
|
}
|
|
507
515
|
: {}),
|
|
@@ -125,8 +125,15 @@ export class ResourceLibrary {
|
|
|
125
125
|
async importFile(scope, sourcePath, options = {}) {
|
|
126
126
|
const verify = async () => {
|
|
127
127
|
options.signal?.throwIfAborted();
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
// Both checks must complete before any source read or publication. They
|
|
129
|
+
// are independent; a captured directory may itself recheck the same scope.
|
|
130
|
+
const checks = await Promise.allSettled([
|
|
131
|
+
this.authorize(scope),
|
|
132
|
+
Promise.resolve().then(() => options.assertAuthorized?.()),
|
|
133
|
+
]);
|
|
134
|
+
for (const check of checks)
|
|
135
|
+
if (check.status === "rejected")
|
|
136
|
+
throw check.reason;
|
|
130
137
|
options.signal?.throwIfAborted();
|
|
131
138
|
};
|
|
132
139
|
await verify();
|
|
@@ -400,7 +407,14 @@ export class ResourceLibrary {
|
|
|
400
407
|
if (!info.isFile() || info.size !== asset.bytes)
|
|
401
408
|
throw new Error("Stored media content changed");
|
|
402
409
|
await this.authorize(scope);
|
|
403
|
-
return {
|
|
410
|
+
return {
|
|
411
|
+
status,
|
|
412
|
+
headers,
|
|
413
|
+
// Bulk readers authorize every delivered block. Bound read-ahead to
|
|
414
|
+
// 1 MiB so large downloads and integrity rechecks do not repeat a full
|
|
415
|
+
// project/package inspection for each default 64 KiB filesystem read.
|
|
416
|
+
body: handle.createReadStream({ ...bounds, autoClose: true, highWaterMark: 1024 * 1024 }),
|
|
417
|
+
};
|
|
404
418
|
}
|
|
405
419
|
catch (error) {
|
|
406
420
|
await handle.close();
|
package/dist/panels/runtime.js
CHANGED
|
@@ -1606,7 +1606,7 @@ export function createPanelRuntime(options) {
|
|
|
1606
1606
|
error(400, "不支持这个服务端目录。");
|
|
1607
1607
|
if (method === "filesystem.pickDirectory" &&
|
|
1608
1608
|
!(await confirm(grant, "使用服务端下载目录?", directory)))
|
|
1609
|
-
return
|
|
1609
|
+
return { cancelled: true };
|
|
1610
1610
|
await options.authorizePanelDirectory?.(grant.app, options.bindingCwd ?? options.cwd, options.cwd);
|
|
1611
1611
|
if (name !== "project")
|
|
1612
1612
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
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.50",
|
|
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",
|
|
@@ -62,14 +62,14 @@
|
|
|
62
62
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@cjhyy/code-shell-capability-coding": "0.9.
|
|
66
|
-
"@cjhyy/code-shell-core": "0.9.
|
|
67
|
-
"@cjhyy/code-shell-web": "0.9.
|
|
65
|
+
"@cjhyy/code-shell-capability-coding": "0.9.50",
|
|
66
|
+
"@cjhyy/code-shell-core": "0.9.50",
|
|
67
|
+
"@cjhyy/code-shell-web": "0.9.50",
|
|
68
68
|
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
69
69
|
"@types/ws": "^8.18.1",
|
|
70
70
|
"ws": "^8.21.0",
|
|
71
|
-
"@cjhyy/code-shell-link": "0.9.
|
|
72
|
-
"@cjhyy/code-shell-host": "0.9.
|
|
71
|
+
"@cjhyy/code-shell-link": "0.9.50",
|
|
72
|
+
"@cjhyy/code-shell-host": "0.9.50"
|
|
73
73
|
},
|
|
74
74
|
"engines": {
|
|
75
75
|
"node": ">=20.10"
|