@cjhyy/code-shell-server 0.9.50 → 0.9.52

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.
@@ -31,6 +31,10 @@ export declare function createHubConfiguration(options: HubConfigurationOptions)
31
31
  text?: string | undefined;
32
32
  };
33
33
  connections: {
34
+ paramValues?: {
35
+ [k: string]: string | number | boolean;
36
+ } | undefined;
37
+ unavailableParameters?: string[] | undefined;
34
38
  id: string;
35
39
  revision: string;
36
40
  catalogId: string;
@@ -46,6 +50,13 @@ export declare function createHubConfiguration(options: HubConfigurationOptions)
46
50
  adapterKind: string;
47
51
  defaultBaseUrl: string;
48
52
  needsKey: boolean;
53
+ modelPresets: {
54
+ parameters: import("./model-parameter-metadata.js").PublicModelParameter[];
55
+ supportsVision?: boolean | undefined;
56
+ maxContextTokens?: number | undefined;
57
+ value: string;
58
+ label: string;
59
+ }[];
49
60
  }[];
50
61
  skills: {
51
62
  disabledReason?: string | undefined;
@@ -4,6 +4,7 @@ import { SettingsManager, invalidateSkillCache, mergePluginMcpServers, scanSkill
4
4
  import { computeEffectiveDisabledLists, getMergedCatalog } from "@cjhyy/code-shell-core/internal";
5
5
  import { withModelTraceContext } from "@cjhyy/code-shell-core/internal";
6
6
  import { probeConfiguredModel } from "@cjhyy/code-shell-host";
7
+ import { publicModelPresets, publicParameterValues, validateParameterPatch, unavailableParameters, } from "./model-parameter-metadata.js";
7
8
  const ROOT = "/api/v1/configuration";
8
9
  const MAX_BODY_BYTES = 64 * 1024;
9
10
  const MAX_SKILL_BYTES = 2 * 1024 * 1024;
@@ -69,6 +70,7 @@ export function createHubConfiguration(options) {
69
70
  protocol: template?.protocol,
70
71
  adapterKind: template?.adapterKind,
71
72
  defaultBaseUrl: template?.defaultBaseUrl,
73
+ modelPresets: template?.modelPresets,
72
74
  },
73
75
  }))
74
76
  .digest("hex");
@@ -131,6 +133,12 @@ export function createHubConfiguration(options) {
131
133
  baseUrl: displayUrl(connection.baseUrl ?? credential?.baseUrl ?? template?.defaultBaseUrl),
132
134
  hasApiKey: Boolean(credential?.apiKey),
133
135
  needsKey: template?.needsKey !== false,
136
+ ...(connection.tag === "text"
137
+ ? {
138
+ paramValues: publicParameterValues(template?.modelPresets?.find((preset) => preset.value === connection.model), connection.paramValues),
139
+ unavailableParameters: unavailableParameters(template?.modelPresets?.find((preset) => preset.value === connection.model), connection.paramValues),
140
+ }
141
+ : {}),
134
142
  };
135
143
  }),
136
144
  catalog: catalog
@@ -141,6 +149,7 @@ export function createHubConfiguration(options) {
141
149
  adapterKind: entry.adapterKind,
142
150
  defaultBaseUrl: displayUrl(entry.defaultBaseUrl),
143
151
  needsKey: entry.needsKey !== false,
152
+ modelPresets: publicModelPresets(entry),
144
153
  })),
145
154
  skills: allSkills.map((skill) => ({
146
155
  name: skill.name,
@@ -200,7 +209,15 @@ export function createHubConfiguration(options) {
200
209
  });
201
210
  }
202
211
  function updateConnection(input) {
203
- knownFields(input, ["id", "catalogId", "model", "baseUrl", "apiKey", "expectedRevision"]);
212
+ knownFields(input, [
213
+ "id",
214
+ "catalogId",
215
+ "model",
216
+ "baseUrl",
217
+ "apiKey",
218
+ "expectedRevision",
219
+ "paramValues",
220
+ ]);
204
221
  const id = boundedString(input.id, "id", 128);
205
222
  const catalogId = boundedString(input.catalogId, "catalogId", 128);
206
223
  const model = boundedString(input.model, "model", 256);
@@ -213,6 +230,21 @@ export function createHubConfiguration(options) {
213
230
  const effective = settingsManager().get();
214
231
  checkConnectionRevision(input, id, effective);
215
232
  const existing = effective.modelConnections.find((entry) => entry.id === id);
233
+ if (input.paramValues !== undefined &&
234
+ (existing ? typeof input.expectedRevision !== "string" : input.expectedRevision !== null))
235
+ throw new HubConfigurationError(409, "请载入最新连接后再保存模型参数。");
236
+ const parameters = input.paramValues === undefined
237
+ ? undefined
238
+ : validateParameterPatch(template.modelPresets?.find((preset) => preset.value === model), input.paramValues);
239
+ if (input.paramValues !== undefined && parameters === undefined)
240
+ throw new HubConfigurationError(400, "模型参数不受支持或超出允许范围,请刷新后检查。");
241
+ const mergedParameters = { ...existing?.paramValues };
242
+ for (const [key, value] of Object.entries(parameters ?? {})) {
243
+ if (value === null)
244
+ delete mergedParameters[key];
245
+ else
246
+ mergedParameters[key] = value;
247
+ }
216
248
  if (existing && (existing.tag !== "text" || existing.catalogId !== catalogId)) {
217
249
  throw new HubConfigurationError(400, "existing connection provider cannot be changed");
218
250
  }
@@ -246,6 +278,7 @@ export function createHubConfiguration(options) {
246
278
  model,
247
279
  ...(credentialId ? { credentialId } : {}),
248
280
  ...(baseUrl !== undefined ? { baseUrl: baseUrl || template.defaultBaseUrl } : {}),
281
+ ...(parameters !== undefined ? { paramValues: mergedParameters } : {}),
249
282
  };
250
283
  current.modelConnections = [
251
284
  ...effective.modelConnections.filter((entry) => entry.id !== id),
@@ -0,0 +1,26 @@
1
+ import type { getMergedCatalog } from "@cjhyy/code-shell-core/internal";
2
+ type Catalog = ReturnType<typeof getMergedCatalog>[number];
3
+ type Preset = NonNullable<Catalog["modelPresets"]>[number];
4
+ export interface PublicModelParameter {
5
+ name: string;
6
+ label: string;
7
+ control: "enum" | "number" | "toggle";
8
+ options?: string[];
9
+ min?: number;
10
+ max?: number;
11
+ default?: string | number | boolean;
12
+ }
13
+ export declare function modelParameters(preset?: Preset): PublicModelParameter[];
14
+ export declare function publicParameterValues(preset: Preset | undefined, values: unknown): {
15
+ [k: string]: string | number | boolean;
16
+ };
17
+ export declare function unavailableParameters(preset: Preset | undefined, values: unknown): string[];
18
+ export declare function validateParameterPatch(preset: Preset | undefined, input: unknown): Record<string, string | number | boolean | null> | undefined;
19
+ export declare function publicModelPresets(entry: Catalog): {
20
+ parameters: PublicModelParameter[];
21
+ supportsVision?: boolean | undefined;
22
+ maxContextTokens?: number | undefined;
23
+ value: string;
24
+ label: string;
25
+ }[];
26
+ export {};
@@ -0,0 +1,92 @@
1
+ /** Deliberately limited to text-generation controls; credential, endpoint,
2
+ * header, arbitrary text and wire-path declarations never reach the browser. */
3
+ const NAMES = new Set([
4
+ "reasoning",
5
+ "reasoning_effort",
6
+ "thinking_type",
7
+ "temperature",
8
+ "top_p",
9
+ "max_tokens",
10
+ ]);
11
+ const FIELDS = {
12
+ reasoning: [
13
+ "reasoning",
14
+ "reasoning_effort",
15
+ "reasoning.effort",
16
+ "thinking.budget_tokens",
17
+ "thinking.type",
18
+ ],
19
+ reasoning_effort: ["reasoning_effort"],
20
+ thinking_type: ["thinking_type", "thinking.type"],
21
+ temperature: ["temperature"],
22
+ top_p: ["top_p"],
23
+ max_tokens: ["max_tokens"],
24
+ };
25
+ export function modelParameters(preset) {
26
+ return (preset?.params ?? []).flatMap((spec) => {
27
+ if (!NAMES.has(spec.name) ||
28
+ preset.params.filter((candidate) => candidate.name === spec.name).length !== 1 ||
29
+ (spec.control !== "enum" && spec.control !== "number" && spec.control !== "toggle") ||
30
+ !FIELDS[spec.name]?.includes(spec.wire?.field ?? spec.name))
31
+ return [];
32
+ const result = {
33
+ name: spec.name,
34
+ label: spec.label ?? spec.name,
35
+ control: spec.control,
36
+ ...(spec.control === "enum" ? { options: [...(spec.options ?? [])] } : {}),
37
+ ...(spec.control === "number" ? { min: spec.min ?? 0, max: spec.max ?? 1_000_000_000 } : {}),
38
+ };
39
+ if (validParameter(result, spec.default))
40
+ result.default = spec.default;
41
+ return [result];
42
+ });
43
+ }
44
+ function validParameter(spec, value) {
45
+ if (spec.control === "enum")
46
+ return typeof value === "string" && value.length <= 256 && !!spec.options?.includes(value);
47
+ if (spec.control === "toggle")
48
+ return typeof value === "boolean";
49
+ if (spec.control !== "number")
50
+ return false;
51
+ return (typeof value === "number" &&
52
+ Number.isFinite(value) &&
53
+ value >= (spec.min ?? 0) &&
54
+ value <= (spec.max ?? 1_000_000_000));
55
+ }
56
+ export function publicParameterValues(preset, values) {
57
+ const source = values && typeof values === "object" && !Array.isArray(values) ? values : {};
58
+ return Object.fromEntries(modelParameters(preset).flatMap((spec) => {
59
+ const value = source[spec.name];
60
+ return validParameter(spec, value) ? [[spec.name, value]] : [];
61
+ }));
62
+ }
63
+ export function unavailableParameters(preset, values) {
64
+ const source = values && typeof values === "object" && !Array.isArray(values) ? values : {};
65
+ return modelParameters(preset)
66
+ .filter((spec) => Object.hasOwn(source, spec.name) &&
67
+ !validParameter(spec, source[spec.name]))
68
+ .map((spec) => spec.name);
69
+ }
70
+ export function validateParameterPatch(preset, input) {
71
+ if (!input || typeof input !== "object" || Array.isArray(input))
72
+ return undefined;
73
+ const specs = modelParameters(preset);
74
+ const patch = Object.entries(input);
75
+ if (patch.length > NAMES.size)
76
+ return undefined;
77
+ if (patch.some(([key, value]) => {
78
+ const spec = specs.find((candidate) => candidate.name === key);
79
+ return !spec || (value !== null && !validParameter(spec, value));
80
+ }))
81
+ return undefined;
82
+ return input;
83
+ }
84
+ export function publicModelPresets(entry) {
85
+ return (entry.modelPresets ?? []).map((preset) => ({
86
+ value: preset.value,
87
+ label: preset.label ?? preset.value,
88
+ ...(preset.maxContextTokens !== undefined ? { maxContextTokens: preset.maxContextTokens } : {}),
89
+ ...(preset.supportsVision !== undefined ? { supportsVision: preset.supportsVision } : {}),
90
+ parameters: modelParameters(preset),
91
+ }));
92
+ }
@@ -1,7 +1,7 @@
1
1
  export { createPanelHttp } from "./panels/http.js";
2
2
  export * from "./panels/automations.js";
3
3
  export * from "./panels/hub-automations.js";
4
- export { createPanelManagement, PanelManagementError } from "./panels/management.js";
4
+ export { createPanelManagement, createPanelSnapshotReads, PanelManagementError, type PanelSnapshotReads, } from "./panels/management.js";
5
5
  export { createPanelRuntime, panelWebCompatibility } from "./panels/runtime.js";
6
6
  export { createHubPanelBinding } from "./panels/hub-binding.js";
7
7
  export * from "./panels/project-scope.js";
@@ -1,7 +1,7 @@
1
1
  export { createPanelHttp } from "./panels/http.js";
2
2
  export * from "./panels/automations.js";
3
3
  export * from "./panels/hub-automations.js";
4
- export { createPanelManagement, PanelManagementError } from "./panels/management.js";
4
+ export { createPanelManagement, createPanelSnapshotReads, PanelManagementError, } from "./panels/management.js";
5
5
  export { createPanelRuntime, panelWebCompatibility } from "./panels/runtime.js";
6
6
  export { createHubPanelBinding } from "./panels/hub-binding.js";
7
7
  export * from "./panels/project-scope.js";
@@ -14,6 +14,10 @@ export interface DispatchMobileChatTurnInput {
14
14
  attachments?: MobileImageAttachment[];
15
15
  clientMessageId?: string;
16
16
  modelTraceSource?: string;
17
+ /** Host-resolved per-run model pool key; never a global model setting. */
18
+ model?: string;
19
+ configuredModel?: import("@cjhyy/code-shell-core/internal").ConfiguredTextModelIntent;
20
+ workspaceProfile?: string;
17
21
  permissionMode?: PermissionMode;
18
22
  runId: string;
19
23
  bridge: MobileRunBridge;
@@ -44,6 +44,9 @@ export async function dispatchMobileChatTurn(input) {
44
44
  id: input.runId,
45
45
  params: {
46
46
  task: text,
47
+ ...(input.model ? { model: input.model } : {}),
48
+ ...(input.configuredModel ? { configuredModel: { ...input.configuredModel } } : {}),
49
+ ...(input.workspaceProfile ? { workspaceProfile: input.workspaceProfile } : {}),
47
50
  ...(input.modelTraceSource && /^panel:[a-z][a-z0-9-]{0,63}$/.test(input.modelTraceSource)
48
51
  ? { modelTraceSource: input.modelTraceSource }
49
52
  : {}),
@@ -28,8 +28,14 @@ export function parseMobileClientEvent(value) {
28
28
  break;
29
29
  case "chat.send":
30
30
  valid =
31
- optionalMember(event.permissionMode, PERMISSION_MODES) &&
31
+ event.configuredModel === undefined &&
32
+ event.model === undefined &&
33
+ optionalMember(event.permissionMode, PERMISSION_MODES) &&
32
34
  boundedOptionalEmptyString(event.text, MAX_MESSAGE_LENGTH) &&
35
+ (event.nativeConnectionId === undefined ||
36
+ (boundedTrimmedString(event.nativeConnectionId, MAX_ID_LENGTH) &&
37
+ !/[\r\n\0]/.test(event.nativeConnectionId))) &&
38
+ !(event.modelKey !== undefined && event.nativeConnectionId !== undefined) &&
33
39
  (event.modelKey === undefined ||
34
40
  (boundedString(event.modelKey, MAX_ID_LENGTH) &&
35
41
  /^(codex|claude-code)\/[^\r\n\0]*$/.test(event.modelKey))) &&
@@ -44,6 +44,8 @@ export interface RemoteHostManagerOptions {
44
44
  outputJournalPageFrames?: 2048;
45
45
  /** Selected-session Host implements verified display views and bounded detail parts. */
46
46
  historyView?: boolean;
47
+ /** The attached command Host resolves native text connections per authorized Session. */
48
+ nativeModelSelection?: boolean;
47
49
  onClientEvent: (event: unknown, ws: WebSocket) => void | Promise<void>;
48
50
  /**
49
51
  * Absolute path to the built mobile app (out/mobile). Defaults to the
@@ -474,13 +474,18 @@ export class RemoteHostManager extends EventEmitter {
474
474
  return {
475
475
  type: "pair.ok",
476
476
  device,
477
- ...(this.opts.outputJournal
477
+ ...(this.opts.outputJournal || this.opts.nativeModelSelection
478
478
  ? {
479
479
  capabilities: {
480
- outputJournal: 1,
481
- ...(this.opts.historyView ? { historyView: 1 } : {}),
482
- ...(this.opts.legacyBasePages ? { legacyBasePages: 1 } : {}),
483
- ...(this.opts.outputJournalPageFrames === 2048
480
+ ...(this.opts.outputJournal ? { outputJournal: 1 } : {}),
481
+ ...(this.opts.nativeModelSelection ? { nativeModelSelection: 1 } : {}),
482
+ ...(this.opts.outputJournal && this.opts.historyView
483
+ ? { historyView: 1 }
484
+ : {}),
485
+ ...(this.opts.outputJournal && this.opts.legacyBasePages
486
+ ? { legacyBasePages: 1 }
487
+ : {}),
488
+ ...(this.opts.outputJournal && this.opts.outputJournalPageFrames === 2048
484
489
  ? { outputJournalPageFrames: 2048 }
485
490
  : {}),
486
491
  },
@@ -501,13 +506,18 @@ export class RemoteHostManager extends EventEmitter {
501
506
  return {
502
507
  type: "auth.ok",
503
508
  device,
504
- ...(this.opts.outputJournal
509
+ ...(this.opts.outputJournal || this.opts.nativeModelSelection
505
510
  ? {
506
511
  capabilities: {
507
- outputJournal: 1,
508
- ...(this.opts.historyView ? { historyView: 1 } : {}),
509
- ...(this.opts.legacyBasePages ? { legacyBasePages: 1 } : {}),
510
- ...(this.opts.outputJournalPageFrames === 2048
512
+ ...(this.opts.outputJournal ? { outputJournal: 1 } : {}),
513
+ ...(this.opts.nativeModelSelection ? { nativeModelSelection: 1 } : {}),
514
+ ...(this.opts.outputJournal && this.opts.historyView
515
+ ? { historyView: 1 }
516
+ : {}),
517
+ ...(this.opts.outputJournal && this.opts.legacyBasePages
518
+ ? { legacyBasePages: 1 }
519
+ : {}),
520
+ ...(this.opts.outputJournal && this.opts.outputJournalPageFrames === 2048
511
521
  ? { outputJournalPageFrames: 2048 }
512
522
  : {}),
513
523
  },
@@ -0,0 +1,48 @@
1
+ export declare const directoryPickerLifetime: number;
2
+ export interface PanelDirectorySelection {
3
+ path: string;
4
+ assertCurrent(): void;
5
+ }
6
+ /** Used only after a Host bookmark proves a previous choice; a client path is never sufficient. */
7
+ export declare function capturePanelWorkspaceDirectory(cwd: string, path: string): PanelDirectorySelection;
8
+ /** A pending parent-UI choice of an existing project directory, not filesystem authority. */
9
+ export declare class PanelDirectoryPicker {
10
+ private readonly cwd;
11
+ private readonly now;
12
+ readonly rootPath: string;
13
+ readonly rootToken: string;
14
+ readonly initialToken: string;
15
+ readonly expiresAt: number;
16
+ private readonly root;
17
+ private readonly choices;
18
+ private closed;
19
+ private reading;
20
+ private tokenBytes;
21
+ constructor(cwd: string, now: () => number);
22
+ close(): void;
23
+ private assertRoot;
24
+ assertCurrent(): void;
25
+ private check;
26
+ private get;
27
+ private add;
28
+ list(token: unknown, authorize: () => Promise<void>): Promise<{
29
+ rootToken: string;
30
+ directories: {
31
+ name: string;
32
+ token: string;
33
+ }[];
34
+ truncated: boolean;
35
+ parentToken?: string | undefined;
36
+ directoryToken: string;
37
+ relativePath: string;
38
+ name: string;
39
+ }>;
40
+ assertListing(listing: {
41
+ directoryToken: string;
42
+ directories: Array<{
43
+ token: string;
44
+ }>;
45
+ }): void;
46
+ /** Called synchronously after final authorization, while this exact confirmation is pending. */
47
+ select(token: unknown): PanelDirectorySelection;
48
+ }
@@ -0,0 +1,247 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { lstatSync, realpathSync } from "node:fs";
3
+ import { opendir } from "node:fs/promises";
4
+ import { basename, isAbsolute, join, relative, sep } from "node:path";
5
+ import { PanelManagementError } from "./management.js";
6
+ import { PanelBridgeError } from "./bridge-contract.js";
7
+ export const directoryPickerLifetime = 5 * 60_000;
8
+ const TOKEN = /^[A-Za-z0-9_-]{24}$/;
9
+ const MAX_TOKENS = 1024;
10
+ const MAX_ENTRIES = 200;
11
+ const MAX_SCAN = 4096;
12
+ const MAX_TOKEN_BYTES = 1024 * 1024;
13
+ function unavailable() {
14
+ throw new PanelManagementError(409, "panel_directory_picker", "目录选择已失效,请重新发起。");
15
+ }
16
+ function identity(path) {
17
+ try {
18
+ const info = lstatSync(path, { bigint: true });
19
+ if (!info.isDirectory() || info.isSymbolicLink() || realpathSync(path) !== path)
20
+ unavailable();
21
+ return { path, dev: info.dev, ino: info.ino };
22
+ }
23
+ catch {
24
+ return unavailable();
25
+ }
26
+ }
27
+ function assertIdentity(expected) {
28
+ const actual = identity(expected.path);
29
+ if (actual.dev !== expected.dev || actual.ino !== expected.ino)
30
+ unavailable();
31
+ }
32
+ function visibleName(name) {
33
+ return (name.length > 0 &&
34
+ name.length <= 255 &&
35
+ !name.startsWith(".") &&
36
+ !/[\\/\u0000-\u001f\u007f]/u.test(name));
37
+ }
38
+ /** Used only after a Host bookmark proves a previous choice; a client path is never sufficient. */
39
+ export function capturePanelWorkspaceDirectory(cwd, path) {
40
+ let root;
41
+ try {
42
+ root = realpathSync(cwd);
43
+ }
44
+ catch {
45
+ return unavailable();
46
+ }
47
+ const rel = relative(root, path);
48
+ if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`))
49
+ unavailable();
50
+ const parts = rel.split(sep).filter(Boolean);
51
+ if (parts.length > 64)
52
+ unavailable();
53
+ const chain = [identity(root)];
54
+ for (let i = 0; i < parts.length; i++)
55
+ chain.push(identity(join(root, ...parts.slice(0, i + 1))));
56
+ const assertCurrent = () => {
57
+ try {
58
+ if (realpathSync(cwd) !== root)
59
+ unavailable();
60
+ }
61
+ catch {
62
+ unavailable();
63
+ }
64
+ for (const entry of chain)
65
+ assertIdentity(entry);
66
+ };
67
+ assertCurrent();
68
+ return { path, assertCurrent };
69
+ }
70
+ /** A pending parent-UI choice of an existing project directory, not filesystem authority. */
71
+ export class PanelDirectoryPicker {
72
+ cwd;
73
+ now;
74
+ rootPath;
75
+ rootToken;
76
+ initialToken;
77
+ expiresAt;
78
+ root;
79
+ choices = new Map();
80
+ closed = false;
81
+ reading = false;
82
+ tokenBytes = 0;
83
+ constructor(cwd, now) {
84
+ this.cwd = cwd;
85
+ this.now = now;
86
+ this.rootPath = realpathSync(cwd);
87
+ this.root = identity(this.rootPath);
88
+ this.expiresAt = now() + directoryPickerLifetime;
89
+ this.rootToken = this.add([], [this.root]).token;
90
+ const path = join(this.rootPath, "downloads");
91
+ let child;
92
+ try {
93
+ child = identity(path);
94
+ }
95
+ catch (error) {
96
+ if (!(error instanceof PanelManagementError) &&
97
+ error.code !== "ENOENT")
98
+ throw error;
99
+ }
100
+ this.initialToken = child
101
+ ? this.add(["downloads"], [this.root, child], this.rootToken).token
102
+ : this.rootToken;
103
+ this.assertCurrent();
104
+ }
105
+ close() {
106
+ this.closed = true;
107
+ this.choices.clear();
108
+ this.tokenBytes = 0;
109
+ }
110
+ assertRoot() {
111
+ try {
112
+ if (realpathSync(this.cwd) !== this.rootPath)
113
+ unavailable();
114
+ }
115
+ catch {
116
+ unavailable();
117
+ }
118
+ assertIdentity(this.root);
119
+ }
120
+ assertCurrent() {
121
+ if (this.closed || this.now() >= this.expiresAt)
122
+ unavailable();
123
+ this.assertRoot();
124
+ }
125
+ check(choice) {
126
+ this.assertCurrent();
127
+ for (const entry of choice.chain)
128
+ assertIdentity(entry);
129
+ }
130
+ get(token) {
131
+ const choice = typeof token === "string" && TOKEN.test(token) && this.choices.get(token);
132
+ if (!choice)
133
+ unavailable();
134
+ this.check(choice);
135
+ return choice;
136
+ }
137
+ add(parts, chain, parentToken) {
138
+ const existing = [...this.choices.values()].find((item) => item.parts.join("/") === parts.join("/"));
139
+ if (existing) {
140
+ this.check(existing);
141
+ return existing;
142
+ }
143
+ const bytes = Buffer.byteLength(join(this.rootPath, ...parts)) +
144
+ Buffer.byteLength(parts.join("/")) +
145
+ chain.length * 32 +
146
+ 256;
147
+ if (this.choices.size >= MAX_TOKENS ||
148
+ parts.length > 64 ||
149
+ this.tokenBytes + bytes > MAX_TOKEN_BYTES)
150
+ unavailable();
151
+ const choice = {
152
+ token: randomBytes(18).toString("base64url"),
153
+ parts,
154
+ chain,
155
+ ...(parentToken ? { parentToken } : {}),
156
+ };
157
+ this.choices.set(choice.token, choice);
158
+ this.tokenBytes += bytes;
159
+ return choice;
160
+ }
161
+ async list(token, authorize) {
162
+ if (this.reading)
163
+ throw new PanelBridgeError("RATE_LIMITED", "目录正在读取,请稍后重试。", 50);
164
+ this.reading = true;
165
+ try {
166
+ const choice = this.get(token);
167
+ await authorize();
168
+ this.check(choice);
169
+ const directories = [];
170
+ let truncated = false;
171
+ {
172
+ const directory = await opendir(join(this.rootPath, ...choice.parts));
173
+ let scanned = 0;
174
+ try {
175
+ this.check(choice);
176
+ for await (const entry of directory) {
177
+ this.check(choice);
178
+ if (++scanned > MAX_SCAN) {
179
+ truncated = true;
180
+ break;
181
+ }
182
+ if (!entry.isDirectory() || entry.isSymbolicLink() || !visibleName(entry.name))
183
+ continue;
184
+ if (directories.length >= MAX_ENTRIES) {
185
+ truncated = true;
186
+ break;
187
+ }
188
+ const parts = [...choice.parts, entry.name];
189
+ const path = join(this.rootPath, ...parts);
190
+ let child;
191
+ try {
192
+ child = identity(path);
193
+ }
194
+ catch {
195
+ continue;
196
+ }
197
+ const item = this.add(parts, [...choice.chain, child], choice.token);
198
+ directories.push({ name: entry.name, token: item.token });
199
+ }
200
+ }
201
+ finally {
202
+ // for-await closes its directory; the explicit close covers failure before iteration.
203
+ try {
204
+ await directory.close();
205
+ }
206
+ catch {
207
+ /* Already closed by the iterator. */
208
+ }
209
+ }
210
+ }
211
+ await authorize();
212
+ this.check(choice);
213
+ for (const item of directories)
214
+ this.get(item.token);
215
+ return {
216
+ directoryToken: choice.token,
217
+ relativePath: choice.parts.join("/"),
218
+ name: choice.parts.at(-1) ?? (basename(this.rootPath) || this.rootPath),
219
+ ...(choice.parentToken ? { parentToken: choice.parentToken } : {}),
220
+ rootToken: this.rootToken,
221
+ directories: directories.sort((a, b) => a.name.localeCompare(b.name)),
222
+ truncated,
223
+ };
224
+ }
225
+ finally {
226
+ this.reading = false;
227
+ }
228
+ }
229
+ assertListing(listing) {
230
+ this.get(listing.directoryToken);
231
+ for (const item of listing.directories)
232
+ this.get(item.token);
233
+ }
234
+ /** Called synchronously after final authorization, while this exact confirmation is pending. */
235
+ select(token) {
236
+ const choice = this.get(token);
237
+ const path = join(this.rootPath, ...choice.parts);
238
+ const chain = [...choice.chain];
239
+ const assertCurrent = () => {
240
+ this.assertRoot();
241
+ for (const entry of chain)
242
+ assertIdentity(entry);
243
+ };
244
+ assertCurrent();
245
+ return { path, assertCurrent };
246
+ }
247
+ }
@@ -17,7 +17,7 @@ export declare function createPanelHttp(options: PanelManagementHttpOptions & {
17
17
  authorizePanelDirectory?: PanelDirectoryAuthorizer;
18
18
  }): {
19
19
  service: {
20
- snapshot: (appId?: string) => Promise<import("./types.js").PanelSnapshot>;
20
+ snapshot: (appId?: string, reads?: import("./management.js").PanelSnapshotReads) => Promise<import("./types.js").PanelSnapshot>;
21
21
  packageHistory(context: import("./types.js").PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelPackageHistory>;
22
22
  previewRestore(context: import("./types.js").PanelOperationContext, id: unknown, digest: unknown, expected: unknown): Promise<import("./types.js").PanelPackageRestoreReview>;
23
23
  cancelRestore(context: import("./types.js").PanelOperationContext, token: unknown): Promise<{
@@ -8,7 +8,7 @@ export interface PanelManagementHttpOptions extends PanelManagementOptions {
8
8
  /** Call only after the host's same-origin authentication boundary. */
9
9
  export declare function createPanelManagementHttp(options: PanelManagementHttpOptions): {
10
10
  service: {
11
- snapshot: (appId?: string) => Promise<import("./types.js").PanelSnapshot>;
11
+ snapshot: (appId?: string, reads?: import("./management.js").PanelSnapshotReads) => Promise<import("./types.js").PanelSnapshot>;
12
12
  packageHistory(context: PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelPackageHistory>;
13
13
  previewRestore(context: PanelOperationContext, id: unknown, digest: unknown, expected: unknown): Promise<import("./types.js").PanelPackageRestoreReview>;
14
14
  cancelRestore(context: PanelOperationContext, token: unknown): Promise<{
@@ -1,5 +1,5 @@
1
1
  import { type PanelExecutionScope } from "./execution-gate.js";
2
- import { type PanelAppPreview, type PanelAppSourceInput } from "@cjhyy/code-shell-core";
2
+ import { type PanelAppInspectionReads, type PanelAppPreview, type PanelAppSourceInput } from "@cjhyy/code-shell-core";
3
3
  import type { PanelCompatibility, PanelDiscovery, PanelOperationContext, PanelReview, PanelProjectReview, PanelSnapshot, PanelPackageHistory, PanelPackageRestoreReview } from "./types.js";
4
4
  export declare class PanelManagementError extends Error {
5
5
  readonly status: number;
@@ -28,8 +28,17 @@ export interface PanelManagementOptions {
28
28
  repo: string;
29
29
  }, ref: string) => Promise<string>;
30
30
  }
31
+ /**
32
+ * Verified reads shared by the snapshots of one multi-project request, so the
33
+ * same package is not re-verified once per project. Create one per request.
34
+ */
35
+ export interface PanelSnapshotReads {
36
+ inspection: PanelAppInspectionReads;
37
+ digests: Map<string, Promise<string>>;
38
+ }
39
+ export declare function createPanelSnapshotReads(): PanelSnapshotReads;
31
40
  export declare function createPanelManagement(options: PanelManagementOptions): {
32
- snapshot: (appId?: string) => Promise<PanelSnapshot>;
41
+ snapshot: (appId?: string, reads?: PanelSnapshotReads) => Promise<PanelSnapshot>;
33
42
  packageHistory(context: PanelOperationContext, id: unknown, expected: unknown): Promise<PanelPackageHistory>;
34
43
  previewRestore(context: PanelOperationContext, id: unknown, digest: unknown, expected: unknown): Promise<PanelPackageRestoreReview>;
35
44
  cancelRestore(context: PanelOperationContext, token: unknown): Promise<{
@@ -3,7 +3,7 @@ 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, currentPanelAppPackagePins, installedPanelAppUpdatedAt, retainInstalledPanelApp, userHome, panelAppsRoot, previewLocalPanelApp, resolveGitHubPanelAppCommit, resolvePanelAppBindingPolicy, uninstallPanelApp, } from "@cjhyy/code-shell-core";
6
+ import { SettingsManager, PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, discoverGitPanelApps, installReviewedLocalPanelApp, invalidateSkillCache, listInstalledPanelApps, inspectProjectPanelApps, listRetainedPanelAppPackages, resolvePanelAppPackage, parsePanelAppPackagePins, currentPanelAppPackagePins, installedPanelAppUpdatedAt, retainInstalledPanelApp, userHome, panelAppsRoot, previewLocalPanelApp, inspectInstalledPanelAppDigest, resolveGitHubPanelAppCommit, resolvePanelAppBindingPolicy, uninstallPanelApp, } from "@cjhyy/code-shell-core";
7
7
  import { mutateJsonFile } from "@cjhyy/code-shell-core/internal";
8
8
  import { parseGithubUrl } from "@cjhyy/code-shell-core/internal/skills";
9
9
  const REVIEW_TTL = 8 * 60_000;
@@ -151,6 +151,9 @@ function assertRevision(revision) {
151
151
  if (typeof revision !== "string" || !/^[a-f0-9]{64}$/.test(revision))
152
152
  invalid();
153
153
  }
154
+ export function createPanelSnapshotReads() {
155
+ return { inspection: { packages: new Map() }, digests: new Map() };
156
+ }
154
157
  export function createPanelManagement(options) {
155
158
  const now = options.now ?? Date.now;
156
159
  const workspace = resolve(options.bindingCwd ?? options.cwd);
@@ -206,12 +209,17 @@ export function createPanelManagement(options) {
206
209
  const listApps = () => options.projectPackages
207
210
  ? inspectProjectPanelApps(workspace).then((result) => result.apps)
208
211
  : listInstalledPanelApps();
209
- async function inspectApp(app) {
212
+ async function inspectApp(app, reads) {
210
213
  const info = lstatSync(app.installPath);
211
214
  if (info.isSymbolicLink() || !info.isDirectory())
212
215
  throw new PanelManagementError(403, "unsafe_path", "面板安装目录不支持链接。");
213
- const preview = await previewLocalPanelApp({ kind: "dir", path: app.installPath });
214
- return { app, digest: preview.reviewToken };
216
+ // Same digest a dir preview reports, without re-hashing unchanged installed bytes.
217
+ let digest = reads?.digests.get(app.installPath);
218
+ if (!digest) {
219
+ digest = inspectInstalledPanelAppDigest(app.installPath);
220
+ reads?.digests.set(app.installPath, digest);
221
+ }
222
+ return { app, digest: await digest };
215
223
  }
216
224
  async function selectedApp(id) {
217
225
  const app = (await listApps()).find((item) => item.id === id);
@@ -240,12 +248,12 @@ export function createPanelManagement(options) {
240
248
  commit: SHA.test(parsed.source.ref ?? "") ? parsed.source.ref : "",
241
249
  };
242
250
  }
243
- async function snapshot(appId) {
251
+ async function snapshot(appId, reads) {
244
252
  if (appId !== undefined)
245
253
  assertSafePanelAppId(appId);
246
254
  options.assertBinding?.();
247
255
  const inspected = options.projectPackages
248
- ? await inspectProjectPanelApps(workspace, appId)
256
+ ? await inspectProjectPanelApps(workspace, appId, reads?.inspection)
249
257
  : undefined;
250
258
  const apps = inspected?.apps ?? (await listInstalledPanelApps(appId));
251
259
  options.assertBinding?.();
@@ -268,7 +276,7 @@ export function createPanelManagement(options) {
268
276
  .digest("hex"),
269
277
  }));
270
278
  for (const listed of apps) {
271
- const { app, digest } = await inspectApp(listed);
279
+ const { app, digest } = await inspectApp(listed, reads);
272
280
  const source = savedSource(app, saved);
273
281
  const { installPath: _path, source: _source, installedAt: _installedAt, lastUpdated: _lastUpdated, ...publicApp } = app;
274
282
  const support = compatibility(app);
@@ -18,6 +18,7 @@ import { PanelTaskCookieHost } from "./task-cookie-host.js";
18
18
  import { taskCookieFromInput, taskCookieSelection, } from "./task-cookies.js";
19
19
  import { desktopPanelDirectoryBookmarks, hubPanelDirectoryBookmarks, } from "./directory-bookmarks.js";
20
20
  import { handlePanelProcessDirectory } from "./process-files.js";
21
+ import { PanelDirectoryPicker, capturePanelWorkspaceDirectory, directoryPickerLifetime, } from "./directory-picker.js";
21
22
  import { PanelAppProcessService, panelExecutableDirectories, panelProcessInfo, } from "./process-service.js";
22
23
  import { PanelManagementError } from "./management.js";
23
24
  import { panelAutomationMethods } from "./automations.js";
@@ -305,7 +306,7 @@ function bridgeScript(id, origin) {
305
306
  const requestId = String(++next);
306
307
  const timer = setTimeout(() => {
307
308
  pending.delete(requestId); reject(new Error("Panel request timed out"));
308
- }, ["tasks.start", "tasks.retry", "tasks.queue.set", "resources.recordAudio", "resources.recordVideo"].includes(method) ? 30 * 60 * 1000 : 60000);
309
+ }, ["tasks.start", "tasks.retry", "tasks.queue.set", "resources.recordAudio", "resources.recordVideo"].includes(method) ? 30 * 60 * 1000 : method === "filesystem.pickDirectory" ? 6 * 60 * 1000 : 60000);
309
310
  pending.set(requestId, { resolve, reject, timer });
310
311
  try {
311
312
  parent.postMessage({ type: "codeshell-panel:call", instanceId: id, requestId, method, params }, origin);
@@ -911,6 +912,52 @@ export function createPanelRuntime(options) {
911
912
  });
912
913
  return result && (await authorized(grant));
913
914
  }
915
+ async function pickDirectory(grant) {
916
+ if (!(await authorized(grant)))
917
+ error(410, "面板授权已失效。");
918
+ if ([...grant.confirmations.values()].some((item) => item.picker && item.result === undefined))
919
+ error(409, "请先完成当前目录选择。");
920
+ for (const [id, item] of grant.confirmations) {
921
+ if (item.result !== undefined)
922
+ grant.confirmations.delete(id);
923
+ }
924
+ if (grant.confirmations.size >= 64)
925
+ error(429, "待确认操作过多。");
926
+ const picker = new PanelDirectoryPicker(options.cwd, now);
927
+ const requestId = randomBytes(18).toString("base64url");
928
+ const pending = { picker };
929
+ const allowed = await new Promise((done) => {
930
+ const timer = setTimeout(() => pending.finish(false), directoryPickerLifetime);
931
+ timer.unref();
932
+ pending.finish = (value) => {
933
+ if (pending.result !== undefined)
934
+ return;
935
+ pending.result = value;
936
+ clearTimeout(timer);
937
+ picker.close();
938
+ done(value);
939
+ };
940
+ grant.confirmations.set(requestId, pending);
941
+ emit(grant, "host.confirm", {
942
+ requestId,
943
+ title: options.host === "desktop" ? "选择电脑上的目录" : "选择服务器上的目录",
944
+ body: "仅可选择当前项目内的目录,确认后授予此面板使用。",
945
+ expiresAt: picker.expiresAt,
946
+ directoryPicker: {
947
+ kind: "workspace-directory",
948
+ rootPath: picker.rootPath,
949
+ rootToken: picker.rootToken,
950
+ initialToken: picker.initialToken,
951
+ },
952
+ });
953
+ });
954
+ if (!(await authorized(grant)))
955
+ error(410, "面板授权已失效。");
956
+ if (!allowed)
957
+ return undefined;
958
+ pending.selection.assertCurrent();
959
+ return pending.selection;
960
+ }
914
961
  function taskScope(grant) {
915
962
  return {
916
963
  instanceId: grant.id,
@@ -1166,12 +1213,19 @@ export function createPanelRuntime(options) {
1166
1213
  consentTimeoutMs: 50000,
1167
1214
  },
1168
1215
  }),
1169
- ...(app.permissions.includes("resources")
1216
+ ...(app.permissions.includes("resources") || app.permissions.includes("process")
1170
1217
  ? {
1171
1218
  methodLimits: {
1172
- "resources.recordAudio": { timeoutMs: 30 * 60 * 1000 },
1173
- "resources.recordVideo": { timeoutMs: 30 * 60 * 1000 },
1219
+ ...(app.permissions.includes("resources")
1220
+ ? {
1221
+ "resources.recordAudio": { timeoutMs: 30 * 60 * 1000 },
1222
+ "resources.recordVideo": { timeoutMs: 30 * 60 * 1000 },
1223
+ }
1224
+ : {}),
1174
1225
  ...(app.permissions.includes("process")
1226
+ ? { "filesystem.pickDirectory": { timeoutMs: 6 * 60_000 } }
1227
+ : {}),
1228
+ ...(app.permissions.includes("process") && app.permissions.includes("resources")
1175
1229
  ? {
1176
1230
  "tasks.start": {
1177
1231
  maxParamsBytes: toolJobLimits.maxInputBytes,
@@ -1583,16 +1637,30 @@ export function createPanelRuntime(options) {
1583
1637
  if (method === "filesystem.restoreDirectory") {
1584
1638
  await options.authorizePanelDirectory?.(grant.app, options.bindingCwd ?? options.cwd, options.cwd);
1585
1639
  const saved = directoryBookmarks.restore(grant.app.id, options.bindingCwd ?? options.cwd, params?.bookmark);
1586
- if (!options.authorizePanelDirectory &&
1587
- saved !== (await realpath(options.cwd)) &&
1588
- saved !== (await realpath(join(options.cwd, "downloads"))))
1589
- throw new PanelBridgeError("PERMISSION_DENIED", "Saved server directory is unavailable");
1590
- const restored = await processes.grantDirectory(owner, saved);
1640
+ const workspaceSelection = !options.authorizePanelDirectory
1641
+ ? capturePanelWorkspaceDirectory(options.cwd, saved)
1642
+ : undefined;
1643
+ const restored = await processes.grantDirectory(owner, saved, workspaceSelection);
1591
1644
  await options.authorizePanelDirectory?.(grant.app, options.bindingCwd ?? options.cwd, options.cwd);
1592
1645
  directoryBookmarks.restore(grant.app.id, options.bindingCwd ?? options.cwd, params?.bookmark);
1646
+ workspaceSelection?.assertCurrent();
1593
1647
  return { ...restored, bookmark: params.bookmark };
1594
1648
  }
1595
- const name = method === "filesystem.pickDirectory" ? "downloads" : params?.name;
1649
+ if (method === "filesystem.pickDirectory") {
1650
+ const selection = await pickDirectory(grant);
1651
+ if (!selection)
1652
+ return { cancelled: true };
1653
+ selection.assertCurrent();
1654
+ const selected = await processes.grantDirectory(owner, selection.path, selection);
1655
+ selection.assertCurrent();
1656
+ if (!(await authorized(grant)))
1657
+ error(410, "面板授权已失效。");
1658
+ selection.assertCurrent();
1659
+ const bookmark = directoryBookmarks.remember(grant.app.id, options.bindingCwd ?? options.cwd, selected.path);
1660
+ processes.directoryPath(owner, selected.handle);
1661
+ return { ...selected, bookmark };
1662
+ }
1663
+ const name = params?.name;
1596
1664
  const directory = name === "project"
1597
1665
  ? options.cwd
1598
1666
  : name === "downloads"
@@ -1604,16 +1672,13 @@ export function createPanelRuntime(options) {
1604
1672
  : undefined;
1605
1673
  if (!directory)
1606
1674
  error(400, "不支持这个服务端目录。");
1607
- if (method === "filesystem.pickDirectory" &&
1608
- !(await confirm(grant, "使用服务端下载目录?", directory)))
1609
- return { cancelled: true };
1610
1675
  await options.authorizePanelDirectory?.(grant.app, options.bindingCwd ?? options.cwd, options.cwd);
1611
1676
  if (name !== "project")
1612
1677
  await mkdir(directory, { recursive: true, mode: 0o700 });
1613
1678
  if (!(await authorized(grant)))
1614
1679
  error(410, "面板授权已失效。");
1615
1680
  const selected = await processes.grantDirectory(owner, directory);
1616
- if (method !== "filesystem.pickDirectory" && !["downloads", "project"].includes(String(name)))
1681
+ if (!["downloads", "project"].includes(String(name)))
1617
1682
  return selected;
1618
1683
  const bookmark = directoryBookmarks.remember(grant.app.id, options.bindingCwd ?? options.cwd, selected.path);
1619
1684
  if (!(await authorized(grant)))
@@ -1780,7 +1845,7 @@ export function createPanelRuntime(options) {
1780
1845
  }
1781
1846
  const directoryMatch = /^\/api\/v1\/panels\/runtime\/([\w-]+)\/directory\/([a-f0-9-]{36})$/.exec(url.pathname);
1782
1847
  const resourceMatch = /^\/api\/v1\/panels\/runtime\/([\w-]+)\/resources\/((?:asset|external)-[a-f0-9]{64})$/.exec(url.pathname);
1783
- const match = /^\/api\/v1\/panels\/runtime\/([\w-]+)(?:\/(call|renew|events|confirm|tool-results))?$/.exec(url.pathname) ??
1848
+ const match = /^\/api\/v1\/panels\/runtime\/([\w-]+)(?:\/(call|renew|events|confirm|directory-picker|tool-results))?$/.exec(url.pathname) ??
1784
1849
  directoryMatch ??
1785
1850
  resourceMatch;
1786
1851
  const grant = match && grants.get(match[1]);
@@ -1850,15 +1915,57 @@ export function createPanelRuntime(options) {
1850
1915
  const batch = grant.events.slice(0, 128);
1851
1916
  json(response, 200, { events: batch, cursor: batch.at(-1)?.id ?? after });
1852
1917
  }
1918
+ else if (request.method === "POST" && match[2] === "directory-picker") {
1919
+ if (request.headers.origin !== grant.origin)
1920
+ error(403, "目录选择仅供当前宿主页面使用。");
1921
+ const input = await body(request);
1922
+ if (Object.keys(input).some((key) => !["requestId", "directoryToken"].includes(key)))
1923
+ error(400, "目录选择参数无效。");
1924
+ const pending = typeof input.requestId === "string" && grant.confirmations.get(input.requestId);
1925
+ if (!pending || !pending.picker || pending.result !== undefined)
1926
+ error(409, "目录选择已失效,请重新发起。");
1927
+ const assertPending = async () => {
1928
+ if (!(await authorized(grant)))
1929
+ error(410, "面板授权已失效。");
1930
+ if (pending.result !== undefined || now() >= pending.picker.expiresAt) {
1931
+ pending.finish(false);
1932
+ error(409, "目录选择已失效,请重新发起。");
1933
+ }
1934
+ };
1935
+ await assertPending();
1936
+ const listing = await pending.picker.list(input.directoryToken, assertPending);
1937
+ await assertPending();
1938
+ pending.picker.assertListing(listing);
1939
+ json(response, 200, { requestId: input.requestId, ...listing });
1940
+ }
1853
1941
  else if (request.method === "POST" && match[2] === "confirm") {
1854
1942
  const input = await body(request);
1855
1943
  const pending = typeof input.requestId === "string" && grant.confirmations.get(input.requestId);
1856
1944
  if (!pending ||
1857
1945
  typeof input.allowed !== "boolean" ||
1858
- Object.keys(input).some((key) => !["requestId", "allowed"].includes(key)))
1946
+ Object.keys(input).some((key) => !["requestId", "allowed", ...(pending?.picker ? ["directoryToken"] : [])].includes(key)) ||
1947
+ (input.allowed === false && input.directoryToken !== undefined))
1859
1948
  error(400, "找不到这个待确认操作。");
1860
1949
  if (!(await authorized(grant)))
1861
1950
  error(410, "面板授权已失效。");
1951
+ if (pending.picker) {
1952
+ if (request.headers.origin !== grant.origin)
1953
+ error(403, "目录选择仅供当前宿主页面使用。");
1954
+ if (input.allowed) {
1955
+ if (pending.result === undefined) {
1956
+ if (now() >= pending.picker.expiresAt) {
1957
+ pending.finish(false);
1958
+ error(409, "目录选择已失效,请重新发起。");
1959
+ }
1960
+ pending.selection = pending.picker.select(input.directoryToken);
1961
+ pending.directoryToken = input.directoryToken;
1962
+ }
1963
+ else if (pending.result !== true ||
1964
+ input.directoryToken !== pending.directoryToken) {
1965
+ error(409, "目录选择已失效,请重新发起。");
1966
+ }
1967
+ }
1968
+ }
1862
1969
  pending.finish(input.allowed);
1863
1970
  json(response, 200, { accepted: true, allowed: pending.result });
1864
1971
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-server",
3
- "version": "0.9.50",
3
+ "version": "0.9.52",
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.50",
66
- "@cjhyy/code-shell-core": "0.9.50",
67
- "@cjhyy/code-shell-web": "0.9.50",
65
+ "@cjhyy/code-shell-capability-coding": "0.9.52",
66
+ "@cjhyy/code-shell-core": "0.9.52",
67
+ "@cjhyy/code-shell-web": "0.9.52",
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.50",
72
- "@cjhyy/code-shell-host": "0.9.50"
71
+ "@cjhyy/code-shell-link": "0.9.52",
72
+ "@cjhyy/code-shell-host": "0.9.52"
73
73
  },
74
74
  "engines": {
75
75
  "node": ">=20.10"