@michengai/dsh-codex-ui 0.2.49 → 0.2.54

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
@@ -1,7 +1,21 @@
1
1
  import { Context } from "@deepseek-ai/cordis";
2
2
  //#region src/index.d.ts
3
+ type HostRequest = {
4
+ method?: string;
5
+ url?: string;
6
+ headers?: Record<string, string | string[] | undefined>;
7
+ };
8
+ /**
9
+ * 判断浏览器请求是否跨站。依赖安装会改写用户配置并拉起子进程,
10
+ * 恶意网页只需一个表单就能跨站触发,必须先按 Sec-Fetch-Site(优先,
11
+ * 且无法被页面伪造)再按 Origin 与 Host 的比对阻断;无这些头的
12
+ * 非浏览器客户端(curl、CLI)仍然放行。
13
+ */
14
+ declare function crossSiteRequest(request: HostRequest): boolean;
15
+ /** 把安装错误收成可给浏览器看的文案:我们自己的中文说明保留,带本地路径的底层错误脱敏。 */
16
+ declare function publicDependencyError(error: unknown): string;
3
17
  declare const inject: string[];
4
18
  /** 提供不泄露地址、命令和凭证的连接器目录。 */
5
19
  declare function apply(ctx: Context): void;
6
20
  //#endregion
7
- export { apply, inject };
21
+ export { apply, crossSiteRequest, inject, publicDependencyError };
package/dist/index.mjs CHANGED
@@ -20,6 +20,18 @@ const MANAGED_DEPENDENCIES = [
20
20
  {
21
21
  id: "archive",
22
22
  packageName: "@michengai/dsh-archive-manager"
23
+ },
24
+ {
25
+ id: "im",
26
+ packageName: "@michengai/dsh-im-connect"
27
+ },
28
+ {
29
+ id: "schedule",
30
+ packageName: "@michengai/dsh-automation"
31
+ },
32
+ {
33
+ id: "market",
34
+ packageName: "dshmarket"
23
35
  }
24
36
  ];
25
37
  function managedDependency(id) {
@@ -48,12 +60,22 @@ async function installedPackageVersion(packageName) {
48
60
  throw error;
49
61
  }
50
62
  }
63
+ /** npm latest 查询缓存有效期:避免每次打开“关于”页都打 7 个 registry 请求。 */
64
+ const LATEST_CACHE_TTL_MS = 3e5;
65
+ const latestCache = /* @__PURE__ */ new Map();
51
66
  async function npmLatestVersion(packageName) {
67
+ const hit = latestCache.get(packageName);
68
+ if (hit !== void 0 && Date.now() - hit.at < LATEST_CACHE_TTL_MS) return hit.version;
52
69
  try {
53
70
  const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, { signal: AbortSignal.timeout(5e3) });
54
71
  if (!response.ok) return void 0;
55
72
  const manifest = await response.json();
56
- return typeof manifest.version === "string" ? manifest.version : void 0;
73
+ if (typeof manifest.version !== "string") return void 0;
74
+ latestCache.set(packageName, {
75
+ version: manifest.version,
76
+ at: Date.now()
77
+ });
78
+ return manifest.version;
57
79
  } catch {
58
80
  return;
59
81
  }
@@ -61,8 +83,13 @@ async function npmLatestVersion(packageName) {
61
83
  function escapeRegExp(value) {
62
84
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
63
85
  }
86
+ /** 允许写入 YAML 单引号白名单的版本:semver 及常见预发布后缀,禁止引号与空白。 */
87
+ function isSafeReleaseVersion(version) {
88
+ return /^v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version);
89
+ }
64
90
  /** 将用户确认的精确版本合并进 Profile 的 pnpm 发布时间保护例外。 */
65
91
  function applyReleaseExclude(source, packageName, version) {
92
+ if (!isSafeReleaseVersion(version)) throw new Error("npm 返回了无法识别的最新版本。");
66
93
  const eol = source.includes("\r\n") ? "\r\n" : "\n";
67
94
  const existing = new RegExp(`^ - '${escapeRegExp(packageName)}@([^']*)'\\s*$`, "m").exec(source);
68
95
  if (existing !== null) {
@@ -142,6 +169,7 @@ function pluginCommandError(stderr) {
142
169
  const detail = stderr.replace(/\s+/g, " ").trim();
143
170
  if (detail.includes("minimumReleaseAge") || detail.includes("Release age")) return /* @__PURE__ */ new Error("更新被 pnpm 发布时间保护拦截。请确认已写入当前版本白名单后重试。");
144
171
  if (detail.includes("EPERM") || detail.includes("EBUSY") || detail.includes("EACCES")) return /* @__PURE__ */ new Error("无法覆盖正在运行的插件文件。请先停止 DSH Web,再点击更新。");
172
+ if (detail.includes("NO_MATCHING_VERSION") || detail.includes("No matching version")) return /* @__PURE__ */ new Error("当前 npm 镜像还没有这个版本。请稍后重试,或改用官方源安装。");
145
173
  return /* @__PURE__ */ new Error("从 npm 安装或更新依赖失败。请检查网络、npm registry 或发布时间保护后重试。");
146
174
  }
147
175
  /**
@@ -183,14 +211,29 @@ function runDshPlugin(args) {
183
211
  });
184
212
  });
185
213
  }
214
+ /** 并发安装互斥:pnpm 锁文件竞争会触发 EPERM/EBUSY,同一时间只允许一个安装进程。 */
215
+ let installing = false;
186
216
  /** 仅允许安装固定依赖,避免把浏览器输入转成任意命令。 */
187
217
  async function installDependency(id) {
218
+ if (installing) throw new Error("已有依赖安装正在进行,请等待完成后再试。");
219
+ installing = true;
220
+ try {
221
+ return await installDependencyLocked(id);
222
+ } finally {
223
+ installing = false;
224
+ }
225
+ }
226
+ async function installDependencyLocked(id) {
188
227
  const dependency = managedDependency(id);
189
228
  if (dependency === void 0) throw new Error("不支持安装该依赖。");
190
229
  const latestVersion = await npmLatestVersion(dependency.packageName);
191
230
  if (latestVersion === void 0) throw new Error("无法获取 npm 最新版本,请检查网络或 npm registry 后重试。");
192
231
  await ensureLatestReleaseAllowed(dependency.packageName, latestVersion);
193
- await runDshPlugin(["add", `${dependency.packageName}@${latestVersion}`]);
232
+ await runDshPlugin([
233
+ "add",
234
+ `${dependency.packageName}@${latestVersion}`,
235
+ "--registry=https://registry.npmjs.org/"
236
+ ]);
194
237
  const installed = await installedPackageVersion(dependency.packageName);
195
238
  if (installed !== latestVersion) throw new Error(`已请求 ${dependency.packageName}@${latestVersion},但当前仍是 ${installed ?? "未安装"}。请先停止 DSH Web 后再更新。`);
196
239
  return dependencyStatuses();
@@ -206,7 +249,6 @@ function requireService(ctx, key, method) {
206
249
  function hostServices(ctx) {
207
250
  return {
208
251
  webServer: requireService(ctx, "webServer", "register"),
209
- sessions: requireService(ctx, "sessions", "get"),
210
252
  agents: requireService(ctx, "agents", "get"),
211
253
  tools: requireService(ctx, "tools", "schemas")
212
254
  };
@@ -215,9 +257,41 @@ function hostServices(ctx) {
215
257
  //#region src/index.ts
216
258
  const connectorsEndpoint = "/api/michengai/codex-ui/connectors";
217
259
  const dependenciesEndpoint = "/api/michengai/codex-ui/dependencies";
260
+ function headerValue(headers, name) {
261
+ const value = headers[name];
262
+ if (typeof value === "string") return value;
263
+ if (Array.isArray(value)) return value[0];
264
+ }
265
+ /**
266
+ * 判断浏览器请求是否跨站。依赖安装会改写用户配置并拉起子进程,
267
+ * 恶意网页只需一个表单就能跨站触发,必须先按 Sec-Fetch-Site(优先,
268
+ * 且无法被页面伪造)再按 Origin 与 Host 的比对阻断;无这些头的
269
+ * 非浏览器客户端(curl、CLI)仍然放行。
270
+ */
271
+ function crossSiteRequest(request) {
272
+ const headers = request.headers;
273
+ if (headers === void 0) return false;
274
+ const site = headerValue(headers, "sec-fetch-site");
275
+ if (site === "same-origin" || site === "none") return false;
276
+ if (site !== void 0) return true;
277
+ const origin = headerValue(headers, "origin");
278
+ if (origin === void 0) return false;
279
+ const host = headerValue(headers, "host");
280
+ if (host === void 0) return true;
281
+ try {
282
+ return new URL(origin).host !== host;
283
+ } catch {
284
+ return true;
285
+ }
286
+ }
287
+ /** 把安装错误收成可给浏览器看的文案:我们自己的中文说明保留,带本地路径的底层错误脱敏。 */
288
+ function publicDependencyError(error) {
289
+ const message = error instanceof Error ? error.message : "依赖管理暂不可用。";
290
+ if (/[A-Za-z]:[\\/]|\/(?:home|Users|var|tmp)\//.test(message)) return "依赖管理暂不可用,请查看服务端日志。";
291
+ return message;
292
+ }
218
293
  const inject = [
219
294
  "webServer",
220
- "sessions",
221
295
  "agents",
222
296
  "tools"
223
297
  ];
@@ -283,6 +357,14 @@ function apply(ctx) {
283
357
  return;
284
358
  }
285
359
  if (request.method === "POST") {
360
+ if (crossSiteRequest(request)) {
361
+ response.writeHead(403, {
362
+ "content-type": "application/json; charset=utf-8",
363
+ "cache-control": "no-store"
364
+ });
365
+ response.end(JSON.stringify({ error: "已拒绝跨站请求。" }));
366
+ return;
367
+ }
286
368
  const dependencies = await installDependency(url.searchParams.get("dependency"));
287
369
  response.writeHead(200, {
288
370
  "content-type": "application/json; charset=utf-8",
@@ -297,12 +379,12 @@ function apply(ctx) {
297
379
  response.writeHead(405);
298
380
  response.end();
299
381
  } catch (error) {
300
- const message = error instanceof Error ? error.message : "依赖管理暂不可用。";
382
+ ctx.logger.warn("dependencies endpoint failed: %s", error);
301
383
  response.writeHead(503, {
302
384
  "content-type": "application/json; charset=utf-8",
303
385
  "cache-control": "no-store"
304
386
  });
305
- response.end(JSON.stringify({ error: message }));
387
+ response.end(JSON.stringify({ error: publicDependencyError(error) }));
306
388
  }
307
389
  }
308
390
  });
@@ -313,4 +395,4 @@ function apply(ctx) {
313
395
  }, "michengai-codex-ui: catalogs");
314
396
  }
315
397
  //#endregion
316
- export { apply, inject };
398
+ export { apply, crossSiteRequest, inject, publicDependencyError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-codex-ui",
3
- "version": "0.2.49",
3
+ "version": "0.2.54",
4
4
  "description": "以 Codex 风格重构 DSH Web 侧栏的独立客户端插件",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "scripts": {
55
55
  "build": "tsc --noEmit && tsdown",
56
- "test": "tsx tests/pinned-sessions.assert.ts && tsx tests/session-manager.assert.ts && tsx tests/workspace-browser.assert.ts && tsx tests/sidebar-search.assert.ts && vitest run tests/client-runtime.integration.spec.ts && tsx tests/settings-integration.assert.ts && tsx tests/about-dependencies.assert.ts && tsx tests/dependency-manager.assert.ts && tsx tests/conversation-visuals.assert.ts && tsx tests/client-bundle.assert.ts && tsx tests/codex-suite.assert.ts"
56
+ "test": "tsx tests/permission-i18n.assert.ts && tsx tests/hover-tip.assert.ts && tsx tests/companion-slots.assert.ts && tsx tests/channel-api.assert.ts && tsx tests/schedule-sessions.assert.ts && tsx tests/session-tree.assert.ts && tsx tests/pinned-sessions.assert.ts && tsx tests/session-manager.assert.ts && tsx tests/workspace-browser.assert.ts && tsx tests/sidebar-search.assert.ts && tsx tests/settings-navigation.assert.ts && tsx tests/settings-nav-icons.assert.ts && tsx tests/locales.assert.ts && vitest run tests/client-runtime.integration.spec.ts && tsx tests/settings-integration.assert.ts && tsx tests/about-dependencies.assert.ts && tsx tests/dependency-manager.assert.ts && tsx tests/conversation-bubbles.assert.ts && tsx tests/conversation-header.assert.ts && tsx tests/conversation-visuals.assert.ts && tsdown && tsx tests/client-bundle.assert.ts && tsx tests/codex-suite.assert.ts"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@deepseek-ai/cordis": ">=4.0.1 <5.0.0",
@@ -71,8 +71,12 @@
71
71
  "react": "^18.2.0"
72
72
  },
73
73
  "peerDependenciesMeta": {
74
- "@michengai/dsh-agency-agents": { "optional": true },
75
- "@michengai/dsh-skills-manager": { "optional": true }
74
+ "@michengai/dsh-agency-agents": {
75
+ "optional": true
76
+ },
77
+ "@michengai/dsh-skills-manager": {
78
+ "optional": true
79
+ }
76
80
  },
77
81
  "devDependencies": {
78
82
  "@deepseek-ai/cordis": "4.0.1",
@@ -86,17 +90,15 @@
86
90
  "@deepseek-ai/dsh-client-ui-settings-general": "0.1.0-rc.6",
87
91
  "@deepseek-ai/dsh-client-ui-sidebar": "0.1.0-rc.6",
88
92
  "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6",
93
+ "@types/jsdom": "^30.0.0",
89
94
  "@types/node": "^26.0.0",
90
95
  "@types/react": "~18.3.1",
96
+ "jsdom": "^29.1.1",
91
97
  "react": "^18.3.1",
92
98
  "react-dom": "^18.3.1",
93
99
  "tsdown": "^0.22.2",
94
100
  "tsx": "^4.22.4",
95
101
  "typescript": "^6.0.3",
96
- "vitest": "^4.1.8",
97
- "jsdom": "^29.1.1"
102
+ "vitest": "^4.1.8"
98
103
  }
99
104
  }
100
-
101
-
102
-