@michengai/dsh-codex-ui 0.2.76 → 0.2.78

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/index.d.mts CHANGED
@@ -4,6 +4,7 @@ type HostRequest = {
4
4
  method?: string;
5
5
  url?: string;
6
6
  headers?: Record<string, string | string[] | undefined>;
7
+ [Symbol.asyncIterator]?: () => AsyncIterator<Uint8Array | string>;
7
8
  };
8
9
  /**
9
10
  * 判断浏览器请求是否跨站。依赖安装会改写用户配置并拉起子进程,
@@ -14,8 +15,11 @@ type HostRequest = {
14
15
  declare function crossSiteRequest(request: HostRequest): boolean;
15
16
  /** 把安装错误收成可给浏览器看的文案:我们自己的中文说明保留,带本地路径的底层错误脱敏。 */
16
17
  declare function publicDependencyError(error: unknown): string;
18
+ declare class RequestBodyTooLargeError extends Error {}
19
+ /** 有界读取 Node HTTP body;偏好接口只接受很小的 JSON。 */
20
+ declare function readRequestBody(request: HostRequest, maxBytes?: number): Promise<string>;
17
21
  declare const inject: string[];
18
22
  /** 提供不泄露地址、命令和凭证的连接器目录。 */
19
23
  declare function apply(ctx: Context): void;
20
24
  //#endregion
21
- export { apply, crossSiteRequest, inject, publicDependencyError };
25
+ export { RequestBodyTooLargeError, apply, crossSiteRequest, inject, publicDependencyError, readRequestBody };
package/dist/index.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
- import { readFile, unlink, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
- import { resolve, sep } from "node:path";
4
+ import { basename, dirname, join, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { randomUUID } from "node:crypto";
6
7
  //#region src/dependencies.ts
7
8
  const SUITE_PACKAGE = "@michengai/dsh-codex-suite";
8
9
  const SUITE_MEMBER_PACKAGES = [
@@ -468,9 +469,75 @@ function hostServices(ctx) {
468
469
  };
469
470
  }
470
471
  //#endregion
472
+ //#region src/workspace-preferences.ts
473
+ const WORKSPACE_PREFERENCES_FILE = ".dsh-codex-ui-preferences.json";
474
+ /** Desktop 和普通 DSH Web 共用 Profile;服务换端口或重启后目录仍保持稳定。 */
475
+ function workspacePreferencesPath(profileDir = process.env.DSH_PROFILE_DIR ?? resolve(homedir(), ".dsh", "profiles", "web")) {
476
+ return resolve(profileDir, WORKSPACE_PREFERENCES_FILE);
477
+ }
478
+ /** 严格校验来自 HTTP 或磁盘的数据,避免损坏配置被静默写回。 */
479
+ function parsePinnedWorkspaceIds(value) {
480
+ if (!Array.isArray(value) || value.length > 1e3) return void 0;
481
+ if (!value.every((id) => typeof id === "string" && id.trim() !== "" && id.length <= 256)) return void 0;
482
+ return [...new Set(value)];
483
+ }
484
+ function parseWorkspacePreferences(value) {
485
+ if (value === null || typeof value !== "object") return void 0;
486
+ const record = value;
487
+ if (record.version !== 1) return void 0;
488
+ const pinnedWorkspaceIds = parsePinnedWorkspaceIds(record.pinnedWorkspaceIds);
489
+ return pinnedWorkspaceIds === void 0 ? void 0 : {
490
+ version: 1,
491
+ pinnedWorkspaceIds
492
+ };
493
+ }
494
+ async function readWorkspacePreferences(path = workspacePreferencesPath()) {
495
+ try {
496
+ const preferences = parseWorkspacePreferences(JSON.parse(await readFile(path, "utf8")));
497
+ if (preferences === void 0) throw new Error("置顶偏好文件格式无效。");
498
+ return {
499
+ ...preferences,
500
+ exists: true
501
+ };
502
+ } catch (error) {
503
+ if (error.code === "ENOENT") return {
504
+ version: 1,
505
+ pinnedWorkspaceIds: [],
506
+ exists: false
507
+ };
508
+ throw error;
509
+ }
510
+ }
511
+ function temporaryPath(path) {
512
+ return join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
513
+ }
514
+ let writeQueue = Promise.resolve();
515
+ /** 串行、原子保存,避免快速拖动排序产生乱序或半截 JSON。 */
516
+ function writeWorkspacePreferences(pinnedWorkspaceIds, path = workspacePreferencesPath()) {
517
+ const normalized = parsePinnedWorkspaceIds([...pinnedWorkspaceIds]);
518
+ if (normalized === void 0) return Promise.reject(/* @__PURE__ */ new Error("置顶工作区数据无效。"));
519
+ const task = writeQueue.catch(() => void 0).then(async () => {
520
+ await mkdir(dirname(path), { recursive: true });
521
+ const temporary = temporaryPath(path);
522
+ try {
523
+ await writeFile(temporary, `${JSON.stringify({
524
+ version: 1,
525
+ pinnedWorkspaceIds: normalized
526
+ }, void 0, 2)}\n`, "utf8");
527
+ await rename(temporary, path);
528
+ } finally {
529
+ await rm(temporary, { force: true }).catch(() => void 0);
530
+ }
531
+ });
532
+ writeQueue = task;
533
+ return task;
534
+ }
535
+ //#endregion
471
536
  //#region src/index.ts
472
537
  const connectorsEndpoint = "/api/michengai/codex-ui/connectors";
473
538
  const dependenciesEndpoint = "/api/michengai/codex-ui/dependencies";
539
+ const preferencesEndpoint = "/api/michengai/codex-ui/preferences";
540
+ const maxPreferencesBodyBytes = 32768;
474
541
  function headerValue(headers, name) {
475
542
  const value = headers[name];
476
543
  if (typeof value === "string") return value;
@@ -504,6 +571,22 @@ function publicDependencyError(error) {
504
571
  if (/[A-Za-z]:[\\/]|\/(?:home|root|Users|var|tmp)\//.test(message)) return "依赖管理暂不可用,请查看服务端日志。";
505
572
  return message;
506
573
  }
574
+ var RequestBodyTooLargeError = class extends Error {};
575
+ /** 有界读取 Node HTTP body;偏好接口只接受很小的 JSON。 */
576
+ async function readRequestBody(request, maxBytes = maxPreferencesBodyBytes) {
577
+ const declared = Number(headerValue(request.headers ?? {}, "content-length"));
578
+ if (Number.isFinite(declared) && declared > maxBytes) throw new RequestBodyTooLargeError("请求体过大。");
579
+ if (request[Symbol.asyncIterator] === void 0) return "";
580
+ const chunks = [];
581
+ let length = 0;
582
+ for await (const chunk of request) {
583
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
584
+ length += buffer.length;
585
+ if (length > maxBytes) throw new RequestBodyTooLargeError("请求体过大。");
586
+ chunks.push(buffer);
587
+ }
588
+ return Buffer.concat(chunks).toString("utf8");
589
+ }
507
590
  const inject = [
508
591
  "webServer",
509
592
  "agents",
@@ -609,12 +692,86 @@ function apply(ctx) {
609
692
  }
610
693
  }
611
694
  });
695
+ const disposePreferences = host.webServer.register({
696
+ kind: "exact",
697
+ path: preferencesEndpoint,
698
+ handler: async (request, response) => {
699
+ try {
700
+ if (request.method === "GET" || request.method === "HEAD") {
701
+ const preferences = await readWorkspacePreferences();
702
+ response.writeHead(200, {
703
+ "content-type": "application/json; charset=utf-8",
704
+ "cache-control": "no-store"
705
+ });
706
+ response.end(request.method === "HEAD" ? void 0 : JSON.stringify(preferences));
707
+ return;
708
+ }
709
+ if (request.method === "PUT") {
710
+ if (crossSiteRequest(request)) {
711
+ response.writeHead(403, {
712
+ "content-type": "application/json; charset=utf-8",
713
+ "cache-control": "no-store"
714
+ });
715
+ response.end(JSON.stringify({ error: "已拒绝跨站请求。" }));
716
+ return;
717
+ }
718
+ const body = JSON.parse(await readRequestBody(request));
719
+ const pinnedWorkspaceIds = body !== null && typeof body === "object" ? parsePinnedWorkspaceIds(body.pinnedWorkspaceIds) : void 0;
720
+ if (pinnedWorkspaceIds === void 0) {
721
+ response.writeHead(400, {
722
+ "content-type": "application/json; charset=utf-8",
723
+ "cache-control": "no-store"
724
+ });
725
+ response.end(JSON.stringify({ error: "置顶偏好格式无效。" }));
726
+ return;
727
+ }
728
+ await writeWorkspacePreferences(pinnedWorkspaceIds);
729
+ response.writeHead(200, {
730
+ "content-type": "application/json; charset=utf-8",
731
+ "cache-control": "no-store"
732
+ });
733
+ response.end(JSON.stringify({
734
+ version: 1,
735
+ pinnedWorkspaceIds,
736
+ exists: true
737
+ }));
738
+ return;
739
+ }
740
+ response.writeHead(405, { allow: "GET, HEAD, PUT" });
741
+ response.end();
742
+ } catch (error) {
743
+ if (error instanceof RequestBodyTooLargeError) {
744
+ response.writeHead(413, {
745
+ "content-type": "application/json; charset=utf-8",
746
+ "cache-control": "no-store"
747
+ });
748
+ response.end(JSON.stringify({ error: "请求体过大。" }));
749
+ return;
750
+ }
751
+ if (error instanceof SyntaxError) {
752
+ response.writeHead(400, {
753
+ "content-type": "application/json; charset=utf-8",
754
+ "cache-control": "no-store"
755
+ });
756
+ response.end(JSON.stringify({ error: "置顶偏好格式无效。" }));
757
+ return;
758
+ }
759
+ ctx.logger.warn("preferences endpoint failed: %s", error);
760
+ response.writeHead(503, {
761
+ "content-type": "application/json; charset=utf-8",
762
+ "cache-control": "no-store"
763
+ });
764
+ response.end(JSON.stringify({ error: "置顶偏好暂不可用。" }));
765
+ }
766
+ }
767
+ });
612
768
  return () => {
613
769
  disposeDependencyInstaller();
614
770
  disposeConnectors();
615
771
  disposeDependencies();
772
+ disposePreferences();
616
773
  };
617
774
  }, "michengai-codex-ui: catalogs");
618
775
  }
619
776
  //#endregion
620
- export { apply, crossSiteRequest, inject, publicDependencyError };
777
+ export { RequestBodyTooLargeError, apply, crossSiteRequest, inject, publicDependencyError, readRequestBody };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-codex-ui",
3
- "version": "0.2.76",
3
+ "version": "0.2.78",
4
4
  "description": "以 Codex 风格重构 DSH Web 侧栏的独立客户端插件",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
@@ -51,6 +51,11 @@
51
51
  "platform": "web"
52
52
  }
53
53
  },
54
+ "scripts": {
55
+ "build": "tsc --noEmit && tsdown",
56
+ "typecheck": "tsc --noEmit",
57
+ "test": "pnpm run typecheck && vitest run tests/client-runtime.integration.spec.ts && tsdown && node tests/run-assertions.mjs"
58
+ },
54
59
  "peerDependencies": {
55
60
  "@deepseek-ai/cordis": ">=4.0.1 <5.0.0",
56
61
  "@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.0 <0.2.0",
@@ -96,10 +101,5 @@
96
101
  "tsx": "^4.22.4",
97
102
  "typescript": "^6.0.3",
98
103
  "vitest": "^4.1.8"
99
- },
100
- "scripts": {
101
- "build": "tsc --noEmit && tsdown",
102
- "typecheck": "tsc --noEmit",
103
- "test": "pnpm run typecheck && vitest run tests/client-runtime.integration.spec.ts && tsdown && node tests/run-assertions.mjs"
104
104
  }
105
- }
105
+ }