@cjhyy/code-shell-server 0.9.48 → 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/desktop-web/http-api.d.ts +9 -0
- package/dist/desktop-web/http-api.js +17 -1
- package/dist/http-byte-range.d.ts +5 -0
- package/dist/http-byte-range.js +19 -0
- package/dist/hub/files.js +71 -9
- package/dist/index.panels.d.ts +1 -0
- package/dist/index.panels.js +1 -0
- package/dist/mobile-remote/access-passcode.d.ts +5 -0
- package/dist/mobile-remote/access-passcode.js +48 -9
- package/dist/mobile-remote/mobile-client-event-validator.js +54 -4
- 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 +11 -1
- package/dist/mobile-remote/remote-host-manager.js +140 -56
- package/dist/mobile-remote/tunnel-manager.d.ts +1 -0
- package/dist/mobile-remote/tunnel-manager.js +45 -20
- package/dist/panels/background-service-process.d.ts +39 -0
- package/dist/panels/background-service-process.js +457 -0
- package/dist/panels/background-service-store.d.ts +67 -0
- package/dist/panels/background-service-store.js +539 -0
- package/dist/panels/background-services.d.ts +193 -0
- package/dist/panels/background-services.js +1159 -0
- package/dist/panels/bridge-contract.d.ts +1 -1
- package/dist/panels/execution-gate.d.ts +7 -1
- package/dist/panels/execution-gate.js +26 -0
- package/dist/panels/management.js +49 -13
- package/dist/panels/process-files.js +28 -3
- package/dist/panels/resources/library.d.ts +1 -4
- package/dist/panels/resources/library.js +19 -21
- package/dist/panels/resources/references.js +21 -8
- package/dist/panels/runtime.js +2 -2
- package/dist/panels/tool-jobs.d.ts +2 -0
- package/dist/panels/tool-jobs.js +1 -1
- package/dist/sessions-service.d.ts +4 -0
- package/dist/sessions-service.js +17 -0
- package/package.json +6 -6
|
@@ -11,7 +11,7 @@ export declare const panelBridgeLimits: Readonly<{
|
|
|
11
11
|
callTimeoutMs: 15000;
|
|
12
12
|
consentTimeoutMs: number;
|
|
13
13
|
}>;
|
|
14
|
-
export type PanelBridgeErrorCode = "PERMISSION_DENIED" | "REVOKED" | "SESSION_CHANGED" | "NOT_SUPPORTED" | "RATE_LIMITED" | "PARAMS_TOO_LARGE" | "RESULT_TOO_LARGE" | "TIMEOUT" | "INVALID_ARGUMENT" | "OPERATION_FAILED";
|
|
14
|
+
export type PanelBridgeErrorCode = "PERMISSION_DENIED" | "REVOKED" | "SESSION_CHANGED" | "NOT_SUPPORTED" | "RATE_LIMITED" | "PARAMS_TOO_LARGE" | "RESULT_TOO_LARGE" | "TIMEOUT" | "INVALID_ARGUMENT" | "OPERATION_FAILED" | "SERVICE_CAPACITY" | "SERVICE_CONFIG_CONFLICT" | "SERVICE_REVISION_CONFLICT" | "SERVICE_REQUEST_KEY_CONFLICT";
|
|
15
15
|
export declare class PanelBridgeError extends Error {
|
|
16
16
|
readonly code: PanelBridgeErrorCode;
|
|
17
17
|
readonly retryAfterMs?: number | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** What holds an execution, so a refused change can name it. Host-assigned only. */
|
|
2
2
|
export interface PanelExecutionHolder {
|
|
3
|
-
kind: "panel-call" | "agent-tool" | "process" | "agent-task" | "tool-job" | "automation";
|
|
3
|
+
kind: "panel-call" | "agent-tool" | "process" | "agent-task" | "tool-job" | "automation" | "service";
|
|
4
4
|
/** Desktop WebContents id of the Panel instance that owns this execution. */
|
|
5
5
|
guestId?: number;
|
|
6
6
|
/** Trusted Host description, such as a native tool entry name. */
|
|
@@ -32,6 +32,12 @@ export declare class PanelExecutionBusyError extends Error {
|
|
|
32
32
|
export declare class PanelExecutionGate {
|
|
33
33
|
private readonly executions;
|
|
34
34
|
private readonly sources;
|
|
35
|
+
private readonly settled;
|
|
36
|
+
private readonly serviceDrains;
|
|
37
|
+
registerServiceDrain(drain: (matches: (scope: PanelExecutionScope) => boolean) => Promise<void>): () => void;
|
|
38
|
+
/** Call only after validating the concrete authorized catalog mutation. */
|
|
39
|
+
drainServices(matches: (scope: PanelExecutionScope) => boolean): Promise<void>;
|
|
40
|
+
onMutationSettled(listener: () => void): () => void;
|
|
35
41
|
private readonly mutations;
|
|
36
42
|
enter(scope: PanelExecutionScope): () => void;
|
|
37
43
|
run<T>(scope: PanelExecutionScope, work: () => Promise<T>): Promise<T>;
|
|
@@ -27,6 +27,24 @@ export class PanelExecutionBusyError extends Error {
|
|
|
27
27
|
export class PanelExecutionGate {
|
|
28
28
|
executions = new Set();
|
|
29
29
|
sources = new Set();
|
|
30
|
+
settled = new Set();
|
|
31
|
+
serviceDrains = new Set();
|
|
32
|
+
registerServiceDrain(drain) {
|
|
33
|
+
this.serviceDrains.add(drain);
|
|
34
|
+
return () => {
|
|
35
|
+
this.serviceDrains.delete(drain);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Call only after validating the concrete authorized catalog mutation. */
|
|
39
|
+
async drainServices(matches) {
|
|
40
|
+
await Promise.all([...this.serviceDrains].map((drain) => drain(matches)));
|
|
41
|
+
}
|
|
42
|
+
onMutationSettled(listener) {
|
|
43
|
+
this.settled.add(listener);
|
|
44
|
+
return () => {
|
|
45
|
+
this.settled.delete(listener);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
30
48
|
mutations = new Set();
|
|
31
49
|
enter(scope) {
|
|
32
50
|
const held = {
|
|
@@ -98,6 +116,14 @@ export class PanelExecutionGate {
|
|
|
98
116
|
}
|
|
99
117
|
finally {
|
|
100
118
|
this.mutations.delete(matches);
|
|
119
|
+
for (const listener of this.settled) {
|
|
120
|
+
try {
|
|
121
|
+
listener();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* observer */
|
|
125
|
+
}
|
|
126
|
+
}
|
|
101
127
|
}
|
|
102
128
|
}
|
|
103
129
|
}
|
|
@@ -506,6 +506,13 @@ export function createPanelManagement(options) {
|
|
|
506
506
|
catalogMutation = next.catch(() => undefined);
|
|
507
507
|
return next;
|
|
508
508
|
}
|
|
509
|
+
async function drainSelectedServices(id, expected, guard, matches) {
|
|
510
|
+
const current = await selectedApp(id);
|
|
511
|
+
if (revision(current.app, current.digest) !== expected)
|
|
512
|
+
throw new PanelManagementError(409, "conflict", "面板已改变,请刷新后重试。");
|
|
513
|
+
await guard();
|
|
514
|
+
await panelExecutionGate.drainServices(matches);
|
|
515
|
+
}
|
|
509
516
|
function setBinding(app, bound, expected) {
|
|
510
517
|
options.assertBinding?.();
|
|
511
518
|
if (!hasProject)
|
|
@@ -658,10 +665,15 @@ export function createPanelManagement(options) {
|
|
|
658
665
|
throw new PanelManagementError(403, "review_owner", "请在当前设备重新审阅这个版本。");
|
|
659
666
|
await assertAuthorized(context, held.generation);
|
|
660
667
|
const review = held.public;
|
|
661
|
-
|
|
662
|
-
|
|
668
|
+
const assertReviewLease = () => {
|
|
669
|
+
if (closed || generation(context.ownerId) !== held.generation)
|
|
670
|
+
throw new PanelManagementError(401, "login_required", "登录已失效,请重新登录。");
|
|
663
671
|
if (restores.get(token) !== held || held.public.expiresAt <= now())
|
|
664
672
|
throw new PanelManagementError(409, "review_expired", "版本审阅已失效,请重新选择。");
|
|
673
|
+
};
|
|
674
|
+
const validateReview = async (guard) => {
|
|
675
|
+
await assertAuthorized(context, held.generation);
|
|
676
|
+
assertReviewLease();
|
|
665
677
|
const current = await restorationSelection(review.appId);
|
|
666
678
|
if (current.revision !== review.expectedRevision)
|
|
667
679
|
throw new PanelManagementError(409, "conflict", "项目面板已改变,请重新审阅。");
|
|
@@ -670,12 +682,25 @@ export function createPanelManagement(options) {
|
|
|
670
682
|
throw new PanelManagementError(400, "unsupported", "这个版本无法在当前环境使用。");
|
|
671
683
|
await guard();
|
|
672
684
|
await assertAuthorized(context, held.generation);
|
|
673
|
-
|
|
674
|
-
|
|
685
|
+
assertReviewLease();
|
|
686
|
+
if (projectState(review.appId) !== held.projectState)
|
|
687
|
+
throw new PanelManagementError(409, "conflict", "项目面板配置已改变,请重新审阅。");
|
|
688
|
+
return target;
|
|
689
|
+
};
|
|
690
|
+
return mutate(context, { appId: review.appId, kind: "binding" }, async (guard) => {
|
|
691
|
+
const target = await validateReview(guard);
|
|
692
|
+
assertReviewLease();
|
|
675
693
|
setBinding(target, true, { projectState: held.projectState });
|
|
676
694
|
restores.delete(token);
|
|
677
695
|
await options.onChanged?.(review.appId, "binding");
|
|
678
696
|
return { id: review.appId, packageDigest: review.packageDigest };
|
|
697
|
+
}, async (guard, matches) => {
|
|
698
|
+
// A reviewed version selection changes the executor's package just like
|
|
699
|
+
// unbinding. Validate authority and exact retained bytes before drain,
|
|
700
|
+
// then revalidate them before writing. Other executions still block.
|
|
701
|
+
await validateReview(guard);
|
|
702
|
+
assertReviewLease();
|
|
703
|
+
await panelExecutionGate.drainServices(matches);
|
|
679
704
|
});
|
|
680
705
|
},
|
|
681
706
|
assertAuthorized,
|
|
@@ -761,6 +786,8 @@ export function createPanelManagement(options) {
|
|
|
761
786
|
throw new PanelManagementError(409, "already_installed", "这个面板已经安装,请确认项目更新。");
|
|
762
787
|
let admitted = false;
|
|
763
788
|
const assertReviewLease = () => {
|
|
789
|
+
if (closed || generation(context.ownerId) !== held.generation)
|
|
790
|
+
throw new PanelManagementError(401, "login_required", "登录已失效,请重新登录。");
|
|
764
791
|
if (!admitted && (reviews.get(token) !== held || held.public.expiresAt <= now()))
|
|
765
792
|
throw new PanelManagementError(409, "review_expired", "预览已失效,请重新检查来源。");
|
|
766
793
|
};
|
|
@@ -783,6 +810,7 @@ export function createPanelManagement(options) {
|
|
|
783
810
|
};
|
|
784
811
|
return mutate(context, { appId: held.public.preview.id, kind: held.public.kind }, async (guard) => {
|
|
785
812
|
await validateReview(guard);
|
|
813
|
+
assertReviewLease();
|
|
786
814
|
const id = held.public.preview.id;
|
|
787
815
|
const installed = await installReviewedLocalPanelApp(held.source, held.digest, new Date(now()).toISOString(), {
|
|
788
816
|
overwrite: held.public.kind === "update",
|
|
@@ -798,6 +826,7 @@ export function createPanelManagement(options) {
|
|
|
798
826
|
if (projectState(id) !== held.projectState)
|
|
799
827
|
throw new PanelManagementError(409, "conflict", "项目面板配置已改变,请重新预览。");
|
|
800
828
|
await guard();
|
|
829
|
+
assertReviewLease();
|
|
801
830
|
},
|
|
802
831
|
});
|
|
803
832
|
reviews.delete(token);
|
|
@@ -813,7 +842,7 @@ export function createPanelManagement(options) {
|
|
|
813
842
|
invalidateSkillCache();
|
|
814
843
|
await options.onChanged?.(id, held.public.kind);
|
|
815
844
|
return { id, packageDigest: installed.packageDigest };
|
|
816
|
-
},
|
|
845
|
+
}, held.public.kind === "update"
|
|
817
846
|
? async (guard, matches) => {
|
|
818
847
|
await validateReview(guard);
|
|
819
848
|
// A local directory/ZIP may have changed since preview. Check its
|
|
@@ -823,12 +852,19 @@ export function createPanelManagement(options) {
|
|
|
823
852
|
if (preview.id !== held.public.preview.id || preview.reviewToken !== held.digest)
|
|
824
853
|
throw new PanelManagementError(409, "conflict", "来源已改变,请重新检查更新。");
|
|
825
854
|
await validateReview(guard);
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
855
|
+
assertReviewLease();
|
|
856
|
+
if (approval?.releaseExecutions) {
|
|
857
|
+
// Force owns the review while its holders exit. TTL expiry during
|
|
858
|
+
// cancellation must not invalidate consent; revocation is checked.
|
|
859
|
+
admitted = true;
|
|
860
|
+
reviews.delete(token);
|
|
861
|
+
await approval.releaseExecutions(held.public.preview.id, matches);
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
// Ordinary updates drain only services. Other executions still
|
|
865
|
+
// block, and failed publication must leave this review retryable.
|
|
866
|
+
await panelExecutionGate.drainServices(matches);
|
|
867
|
+
}
|
|
832
868
|
}
|
|
833
869
|
: undefined);
|
|
834
870
|
},
|
|
@@ -852,7 +888,7 @@ export function createPanelManagement(options) {
|
|
|
852
888
|
setBinding(app, bound, { ...current, revision: expected });
|
|
853
889
|
await options.onChanged?.(id, "binding");
|
|
854
890
|
return snapshot();
|
|
855
|
-
});
|
|
891
|
+
}, (guard, matches) => drainSelectedServices(id, expected, guard, matches));
|
|
856
892
|
},
|
|
857
893
|
async remove(context, id, expected) {
|
|
858
894
|
assertId(id);
|
|
@@ -878,7 +914,7 @@ export function createPanelManagement(options) {
|
|
|
878
914
|
invalidateSkillCache();
|
|
879
915
|
await options.onChanged?.(id, "remove");
|
|
880
916
|
return { removed: true };
|
|
881
|
-
});
|
|
917
|
+
}, (guard, matches) => drainSelectedServices(id, expected, guard, matches));
|
|
882
918
|
},
|
|
883
919
|
cancelOwner(owner) {
|
|
884
920
|
generations.set(owner, generation(owner) + 1);
|
|
@@ -3,6 +3,7 @@ import { lstat, open, opendir, realpath } from "node:fs/promises";
|
|
|
3
3
|
import { extname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import { rangeBounds } from "../http-byte-range.js";
|
|
6
7
|
const MAX_DOWNLOAD_BYTES = 2 * 1024 * 1024 * 1024;
|
|
7
8
|
const MAX_ENTRIES = 200;
|
|
8
9
|
const MAX_EXAMINED = 4096;
|
|
@@ -227,20 +228,44 @@ export async function handlePanelProcessDirectory(request, response, options) {
|
|
|
227
228
|
fail(404, "下载文件已经改变。");
|
|
228
229
|
await verifyDirectory();
|
|
229
230
|
await authorize();
|
|
230
|
-
|
|
231
|
+
// Directory outputs are mutable and have no stable representation validator.
|
|
232
|
+
// An If-Range condition cannot be confirmed, so return the complete file.
|
|
233
|
+
const range = request.headers["if-range"] === undefined ? request.headers.range : undefined;
|
|
234
|
+
const bounds = range === undefined
|
|
235
|
+
? { start: 0, end: metadata.size - 1 }
|
|
236
|
+
: metadata.size > 0
|
|
237
|
+
? rangeBounds(range, metadata.size)
|
|
238
|
+
: null;
|
|
239
|
+
const headers = {
|
|
231
240
|
"Content-Type": "application/octet-stream",
|
|
232
241
|
"Content-Disposition": disposition(name),
|
|
233
|
-
"
|
|
242
|
+
"Accept-Ranges": "bytes",
|
|
234
243
|
"Content-Security-Policy": "default-src 'none'; sandbox",
|
|
235
244
|
"Cache-Control": "no-store",
|
|
236
245
|
"X-Content-Type-Options": "nosniff",
|
|
237
246
|
"Referrer-Policy": "no-referrer",
|
|
247
|
+
};
|
|
248
|
+
if (!bounds) {
|
|
249
|
+
response.writeHead(416, {
|
|
250
|
+
...headers,
|
|
251
|
+
"Content-Range": `bytes */${metadata.size}`,
|
|
252
|
+
"Content-Length": 0,
|
|
253
|
+
});
|
|
254
|
+
response.end();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
response.writeHead(range === undefined ? 200 : 206, {
|
|
258
|
+
...headers,
|
|
259
|
+
"Content-Length": bounds.end - bounds.start + 1,
|
|
260
|
+
...(range === undefined
|
|
261
|
+
? {}
|
|
262
|
+
: { "Content-Range": `bytes ${bounds.start}-${bounds.end}/${metadata.size}` }),
|
|
238
263
|
});
|
|
239
264
|
if (request.method === "HEAD" || metadata.size === 0) {
|
|
240
265
|
response.end();
|
|
241
266
|
return;
|
|
242
267
|
}
|
|
243
|
-
const input = file.createReadStream({ autoClose: false,
|
|
268
|
+
const input = file.createReadStream({ autoClose: false, ...bounds });
|
|
244
269
|
let checking = false;
|
|
245
270
|
const timer = setInterval(() => {
|
|
246
271
|
if (checking)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Readable } from "node:stream";
|
|
2
2
|
import type { MediaAsset, MediaScope } from "./types.js";
|
|
3
|
+
export { rangeBounds } from "../../http-byte-range.js";
|
|
3
4
|
export declare function resourceMimeType(name: string, requested?: string): string;
|
|
4
5
|
export interface MediaLibraryOptions {
|
|
5
6
|
rootDirectory: string;
|
|
@@ -33,10 +34,6 @@ export interface MediaReadResult {
|
|
|
33
34
|
body: Readable | null;
|
|
34
35
|
}
|
|
35
36
|
export declare function resourceName(value: string): string;
|
|
36
|
-
export declare function rangeBounds(range: string, size: number): {
|
|
37
|
-
start: number;
|
|
38
|
-
end: number;
|
|
39
|
-
} | null;
|
|
40
37
|
/**
|
|
41
38
|
* Host-owned immutable, scope-local media. importFile accepts only paths already
|
|
42
39
|
* authorized by the caller (for example native file selection); guest APIs must
|
|
@@ -3,6 +3,8 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { lstat, mkdir, open, readdir, rename, rm } from "node:fs/promises";
|
|
4
4
|
import { basename, extname, join } from "node:path";
|
|
5
5
|
import { mediaDirectory, mediaScopeKey, readMediaJson, writeMediaJson } from "./storage.js";
|
|
6
|
+
import { rangeBounds } from "../../http-byte-range.js";
|
|
7
|
+
export { rangeBounds } from "../../http-byte-range.js";
|
|
6
8
|
const ASSET_ID = /^asset-[a-f0-9]{64}$/;
|
|
7
9
|
const MIME_BY_EXTENSION = {
|
|
8
10
|
".mp4": "video/mp4",
|
|
@@ -93,24 +95,6 @@ function assetRecord(raw, id) {
|
|
|
93
95
|
createdAt: value.createdAt,
|
|
94
96
|
};
|
|
95
97
|
}
|
|
96
|
-
export function rangeBounds(range, size) {
|
|
97
|
-
const match = /^bytes=(\d*)-(\d*)$/.exec(range);
|
|
98
|
-
if (!match || (!match[1] && !match[2]))
|
|
99
|
-
return null;
|
|
100
|
-
const first = match[1] ? Number(match[1]) : undefined;
|
|
101
|
-
const last = match[2] ? Number(match[2]) : undefined;
|
|
102
|
-
if ((first !== undefined && !Number.isSafeInteger(first)) ||
|
|
103
|
-
(last !== undefined && !Number.isSafeInteger(last)))
|
|
104
|
-
return null;
|
|
105
|
-
if (first === undefined) {
|
|
106
|
-
if (!last || last <= 0)
|
|
107
|
-
return null;
|
|
108
|
-
return { start: Math.max(0, size - last), end: size - 1 };
|
|
109
|
-
}
|
|
110
|
-
if (first >= size || (last !== undefined && last < first))
|
|
111
|
-
return null;
|
|
112
|
-
return { start: first, end: Math.min(last ?? size - 1, size - 1) };
|
|
113
|
-
}
|
|
114
98
|
/**
|
|
115
99
|
* Host-owned immutable, scope-local media. importFile accepts only paths already
|
|
116
100
|
* authorized by the caller (for example native file selection); guest APIs must
|
|
@@ -141,8 +125,15 @@ export class ResourceLibrary {
|
|
|
141
125
|
async importFile(scope, sourcePath, options = {}) {
|
|
142
126
|
const verify = async () => {
|
|
143
127
|
options.signal?.throwIfAborted();
|
|
144
|
-
|
|
145
|
-
|
|
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;
|
|
146
137
|
options.signal?.throwIfAborted();
|
|
147
138
|
};
|
|
148
139
|
await verify();
|
|
@@ -416,7 +407,14 @@ export class ResourceLibrary {
|
|
|
416
407
|
if (!info.isFile() || info.size !== asset.bytes)
|
|
417
408
|
throw new Error("Stored media content changed");
|
|
418
409
|
await this.authorize(scope);
|
|
419
|
-
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
|
+
};
|
|
420
418
|
}
|
|
421
419
|
catch (error) {
|
|
422
420
|
await handle.close();
|
|
@@ -25,12 +25,11 @@ function referenceId(value) {
|
|
|
25
25
|
function validIdentity(value) {
|
|
26
26
|
return value && Number.isSafeInteger(value.dev) && Number.isSafeInteger(value.ino);
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
(relocating || a.ctimeMs === b.ctimeMs));
|
|
28
|
+
// Like other editors, recognize an original by file, size and modification time.
|
|
29
|
+
// ctime is not identity: macOS changes it for tags, "last opened" and other
|
|
30
|
+
// extended attributes, which would otherwise turn unchanged originals "changed".
|
|
31
|
+
function sameSource(a, b) {
|
|
32
|
+
return a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs;
|
|
34
33
|
}
|
|
35
34
|
function view(record, state = "available") {
|
|
36
35
|
return {
|
|
@@ -221,7 +220,21 @@ export class ExternalResourceReferences {
|
|
|
221
220
|
if (error.code !== "ENOENT")
|
|
222
221
|
throw error;
|
|
223
222
|
}
|
|
224
|
-
|
|
223
|
+
const names = (await readdir(directory)).filter((name) => REFERENCE_ID.test(name.replace(/\.json$/, "")));
|
|
224
|
+
// IDs include the ctime seen at creation. Choosing the same unchanged
|
|
225
|
+
// original again must return its reference, not a duplicate asset.
|
|
226
|
+
for (const name of names) {
|
|
227
|
+
const existing = await this.record(scope, name.replace(/\.json$/, "")).catch(() => undefined);
|
|
228
|
+
if (existing?.root === root &&
|
|
229
|
+
existing.path === path &&
|
|
230
|
+
sameSource(existing.source, selected.initial) &&
|
|
231
|
+
existing.ancestors.length === selected.directory.identities.length &&
|
|
232
|
+
existing.ancestors.every((identity, index) => sameResourceIdentity(identity, selected.directory.identities[index]))) {
|
|
233
|
+
await selected.verify();
|
|
234
|
+
return view(existing);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (names.length >= MAX_REFERENCES)
|
|
225
238
|
throw new Error("External reference count limit exceeded");
|
|
226
239
|
const record = {
|
|
227
240
|
schemaVersion: 1,
|
|
@@ -302,7 +315,7 @@ export class ExternalResourceReferences {
|
|
|
302
315
|
const selected = await this.selected(scope, root, resourceRelativePath(path), options);
|
|
303
316
|
try {
|
|
304
317
|
// Metadata alone cannot establish that an unrelated replacement is the original file.
|
|
305
|
-
if (!sameSource(previous.source, selected.initial
|
|
318
|
+
if (!sameSource(previous.source, selected.initial))
|
|
306
319
|
throw new Error("This is a different file; create a new external reference instead");
|
|
307
320
|
const record = {
|
|
308
321
|
...previous,
|
package/dist/panels/runtime.js
CHANGED
|
@@ -349,7 +349,7 @@ function bridgeScript(id, origin) {
|
|
|
349
349
|
call: ask,
|
|
350
350
|
callResult: (method, params) => ask(method, params).then(value => ({ ok: true, value }), error => ({ ok: false, error: { code: error.code || "OPERATION_FAILED", message: error.message, ...(typeof error.retryAfterMs === "number" ? { retryAfterMs: error.retryAfterMs } : {}) } })),
|
|
351
351
|
on: (name, listener) => {
|
|
352
|
-
if (!["context.changed", "process.output", "process.exit", "agent.task.changed", "tasks.changed", "media.job.changed"].includes(name) || typeof listener !== "function") {
|
|
352
|
+
if (!["context.changed", "process.output", "process.exit", "agent.task.changed", "tasks.changed", "services.changed", "media.job.changed"].includes(name) || typeof listener !== "function") {
|
|
353
353
|
throw new Error("Invalid event listener");
|
|
354
354
|
}
|
|
355
355
|
let list = listeners.get(name);
|
|
@@ -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 });
|
|
@@ -47,6 +47,8 @@ export interface ToolJobProgress {
|
|
|
47
47
|
export type ToolJobStatus = "queued" | "running" | "cancelling" | "succeeded" | "failed" | "cancelled" | "interrupted";
|
|
48
48
|
export interface ToolJob {
|
|
49
49
|
id: string;
|
|
50
|
+
/** Caller-owned idempotency identity, returned with its authorized receipt. */
|
|
51
|
+
requestKey?: string;
|
|
50
52
|
scope: ToolJobScope;
|
|
51
53
|
entry: ToolJobEntry;
|
|
52
54
|
package?: ToolJobPackage;
|
package/dist/panels/tool-jobs.js
CHANGED
|
@@ -100,7 +100,7 @@ function progressValue(progress) {
|
|
|
100
100
|
}, toolJobLimits.maxProgressBytes);
|
|
101
101
|
}
|
|
102
102
|
function publicJob(job) {
|
|
103
|
-
const { version: _version,
|
|
103
|
+
const { version: _version, requestDigest: _requestDigest, ...value } = job;
|
|
104
104
|
return toolJobJson(value, toolJobLimits.maxRecordBytes);
|
|
105
105
|
}
|
|
106
106
|
function packageValue(value) {
|
|
@@ -38,6 +38,8 @@ export interface DiskSessionMeta {
|
|
|
38
38
|
engineSessionId: string;
|
|
39
39
|
cwd: string;
|
|
40
40
|
title: string;
|
|
41
|
+
model?: string;
|
|
42
|
+
provider?: string;
|
|
41
43
|
updatedAt: number;
|
|
42
44
|
/** Durable creation time, when supplied by the engine's state schema. */
|
|
43
45
|
createdAt?: number;
|
|
@@ -69,6 +71,8 @@ export interface ListDiskSessionsResult {
|
|
|
69
71
|
export declare function listDiskSessions(opts: {
|
|
70
72
|
limit: number;
|
|
71
73
|
cursor?: string;
|
|
74
|
+
/** Display-only project roots filter, applied before pagination. */
|
|
75
|
+
cwds?: readonly string[];
|
|
72
76
|
includeArchived?: boolean;
|
|
73
77
|
/** Host read models may include children; ordinary sidebar queries stay top-level. */
|
|
74
78
|
includeSubagents?: boolean;
|
package/dist/sessions-service.js
CHANGED
|
@@ -217,6 +217,13 @@ export async function listDiskSessions(opts, baseDir = sessionsRoot()) {
|
|
|
217
217
|
// Preserve the public adapter's explicit relative-root form. Host receives
|
|
218
218
|
// one absolute root; it never guesses a workspace for the caller.
|
|
219
219
|
baseDir = path.resolve(baseDir);
|
|
220
|
+
const pathKey = (cwd) => {
|
|
221
|
+
const normalized = path.resolve(cwd);
|
|
222
|
+
return process.platform === "darwin" || process.platform === "win32"
|
|
223
|
+
? normalized.toLocaleLowerCase("en-US")
|
|
224
|
+
: normalized;
|
|
225
|
+
};
|
|
226
|
+
const cwdRoots = opts.cwds?.map(pathKey);
|
|
220
227
|
const dirs = (await scanSessionDirectories(baseDir)).filter(({ id }) => !isInternalEphemeralSessionId(id));
|
|
221
228
|
dirs.sort(compareDiskSessionPosition);
|
|
222
229
|
const decodedCursor = decodeDiskSessionCursor(opts.cursor);
|
|
@@ -288,6 +295,14 @@ export async function listDiskSessions(opts, baseDir = sessionsRoot()) {
|
|
|
288
295
|
// is intentionally NOT filtered here — the renderer handles that via
|
|
289
296
|
// isNoRepoCwd. (deleted-project resurrection)
|
|
290
297
|
const cwdStr = typeof state.cwd === "string" ? state.cwd : "";
|
|
298
|
+
if (cwdRoots &&
|
|
299
|
+
(!cwdStr ||
|
|
300
|
+
!cwdRoots.some((root) => {
|
|
301
|
+
const candidate = pathKey(cwdStr);
|
|
302
|
+
return (candidate === root ||
|
|
303
|
+
candidate.startsWith(root.endsWith(path.sep) ? root : root + path.sep));
|
|
304
|
+
})))
|
|
305
|
+
continue;
|
|
291
306
|
// A child transcript remains useful after its temporary worktree is gone;
|
|
292
307
|
// targeted reads do not rebuild projects in the sidebar.
|
|
293
308
|
const childParentId = typeof state.parentSessionId === "string" && state.parentSessionId
|
|
@@ -325,6 +340,8 @@ export async function listDiskSessions(opts, baseDir = sessionsRoot()) {
|
|
|
325
340
|
? { clientMessageId: state.clientMessageId }
|
|
326
341
|
: {}),
|
|
327
342
|
engineSessionId: id,
|
|
343
|
+
...(typeof state.model === "string" ? { model: state.model } : {}),
|
|
344
|
+
...(typeof state.provider === "string" ? { provider: state.provider } : {}),
|
|
328
345
|
cwd: typeof state.cwd === "string" ? state.cwd : "",
|
|
329
346
|
// Prefer the LLM-generated title (now persisted to state); fall back to
|
|
330
347
|
// the raw first-message summary, then the id. This lets the disk rebuild
|
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"
|