@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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/client.js +517 -404
- package/dist/index.d.mts +5 -1
- package/dist/index.mjs +160 -3
- package/package.json +7 -7
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.
|
|
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
|
+
}
|