dsh-plugins-jizuo-creator-video 0.3.29 → 0.3.30

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 CHANGED
@@ -12,7 +12,7 @@ dsh-plugins-jizuo-creator-video
12
12
  dsh plugin --profile desktop add dsh-plugins-jizuo-creator-video
13
13
  ```
14
14
 
15
- 这是已验证 v0.3.29 插件包的 npm 安装入口,内含原始插件及共享核心,无需额外安装依赖。
15
+ 这是已验证 v0.3.30 插件包的 npm 安装入口,内含原始插件及共享核心,无需额外安装依赖。
16
16
 
17
17
  已有 GitHub 安装版本时,请先卸载对应的旧安装项,再安装此 npm 包,完成后重启 DSH;卸载插件不会删除作品或对话。不要同时安装同一功能的 GitHub 包与 npm 包。
18
18
 
@@ -7,7 +7,7 @@
7
7
  支持 **DeepSeek Harness Desktop 0.2.0-rc.2**。在左侧「插件」→「添加插件」粘贴下面这一行,确认来源后安装:
8
8
 
9
9
  ```text
10
- https://github.com/ShikangPang/jizuo-creator-video/releases/download/v0.3.29/jizuo-video-plugin-0.3.29.tgz
10
+ https://github.com/ShikangPang/jizuo-creator-video/releases/download/v0.3.30/jizuo-video-plugin-0.3.30.tgz
11
11
  ```
12
12
 
13
13
  只填安装包地址,不要填整个安装命令。安装完成后「已安装」中显示「即作视频」,可分别启用、关闭、配置或卸载。共享核心已包含在安装包中,自动加载,不需要另装即作桌面端或手动安装核心插件。保留 Harness 原有新会话、插件与工作区入口;创作项目统一显示在工作区区域,小说和视频以前置图标区分,不再提供独立的创作面板。
@@ -17,7 +17,7 @@ https://github.com/ShikangPang/jizuo-creator-video/releases/download/v0.3.29/jiz
17
17
  也可使用 Desktop 菜单安装的 dsh 命令:
18
18
 
19
19
  ```sh
20
- dsh plugin --profile desktop add https://github.com/ShikangPang/jizuo-creator-video/releases/download/v0.3.29/jizuo-video-plugin-0.3.29.tgz
20
+ dsh plugin --profile desktop add https://github.com/ShikangPang/jizuo-creator-video/releases/download/v0.3.30/jizuo-video-plugin-0.3.30.tgz
21
21
  ```
22
22
 
23
23
  源码克隆后运行 `node scripts/install.mjs --all` 一次安装五个插件,默认 desktop;Web 用户显式传 `--profile web`。脚本严格检查宿主版本。不要用旧版全局 dsh 命令安装到新版 Desktop,也不要绕过兼容检查。
@@ -26,7 +26,7 @@ dsh plugin --profile desktop add https://github.com/ShikangPang/jizuo-creator-vi
26
26
 
27
27
  启用即作写作插件后,点击工作区列表顶部的“导入小说”。支持 TXT、Markdown 和 DOCX,单文件最大 10 MB。选择文件后预览章节、修改作品名称,确认后创建新的小说项目,展开“正文”即可编辑。取消预览不会创建作品;首章前的内容保留为前言。TXT 支持 UTF-8 和 GB18030 编码。
28
28
 
29
- 已安装多个即作插件时,请将它们一并更新到 0.3.29,并重启 Harness,使共享核心版本一致。
29
+ 已安装多个即作插件时,请将它们一并更新到 0.3.30,并重启 Harness,使共享核心版本一致。
30
30
 
31
31
  ## 项目与数据
32
32
 
@@ -9742,6 +9742,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9742
9742
  "image/jpeg",
9743
9743
  "image/webp"
9744
9744
  ]);
9745
+ const ASSETS_PER_PAGE = 60;
9745
9746
  function VideoAssetsPanel({ project, episode: suppliedEpisode, remote }) {
9746
9747
  const [selectedEpisodeId, setSelectedEpisodeId] = (0, react.useState)("");
9747
9748
  const episode = suppliedEpisode ?? project.episodes.find((item) => !item.deletedAt && item.id === selectedEpisodeId);
@@ -9757,7 +9758,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9757
9758
  const [targetShotId, setTargetShotId] = (0, react.useState)("");
9758
9759
  const [search, setSearch] = (0, react.useState)("");
9759
9760
  const [kind, setKind] = (0, react.useState)("all");
9760
- const visibleAssets = project.assets.filter((asset) => (kind === "all" || asset.kind === kind) && asset.label.toLocaleLowerCase().includes(search.trim().toLocaleLowerCase()));
9761
+ const query = search.trim().toLocaleLowerCase();
9762
+ const visibleAssets = project.assets.filter((asset) => (kind === "all" || asset.kind === kind) && asset.label.toLocaleLowerCase().includes(query));
9763
+ const pageKey = JSON.stringify([
9764
+ project.workId,
9765
+ kind,
9766
+ query
9767
+ ]);
9768
+ const [pagination, setPagination] = (0, react.useState)({
9769
+ key: "",
9770
+ page: 0
9771
+ });
9772
+ const pageCount = Math.max(1, Math.ceil(visibleAssets.length / ASSETS_PER_PAGE));
9773
+ const page = Math.min(pagination.key === pageKey ? pagination.page : 0, pageCount - 1);
9774
+ const pageAssets = visibleAssets.slice(page * ASSETS_PER_PAGE, (page + 1) * ASSETS_PER_PAGE);
9761
9775
  const images = project.assets.filter((asset) => asset.kind === "image");
9762
9776
  const shots = episode?.shots.filter((shot) => !shot.archived) ?? [];
9763
9777
  const targetShot = shots.find((shot) => shot.id === targetShotId);
@@ -9972,7 +9986,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9972
9986
  })] }),
9973
9987
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9974
9988
  className: "jz-video-asset-grid",
9975
- children: visibleAssets.map((asset) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", { children: [
9989
+ children: pageAssets.map((asset) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", { children: [
9976
9990
  asset.kind === "image" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VideoAssetPreview, {
9977
9991
  remote,
9978
9992
  workId: project.workId,
@@ -10016,6 +10030,44 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
10016
10030
  }
10017
10031
  })
10018
10032
  ] }, asset.id))
10033
+ }),
10034
+ pageCount > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
10035
+ className: "jz-video-tools",
10036
+ "aria-label": "素材分页",
10037
+ children: [
10038
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
10039
+ type: "button",
10040
+ "aria-label": "上一页素材",
10041
+ disabled: page === 0,
10042
+ onClick: () => setPagination({
10043
+ key: pageKey,
10044
+ page: page - 1
10045
+ }),
10046
+ children: "上一页"
10047
+ }),
10048
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
10049
+ "aria-live": "polite",
10050
+ children: [
10051
+ "第 ",
10052
+ page + 1,
10053
+ " / ",
10054
+ pageCount,
10055
+ " 页,共 ",
10056
+ visibleAssets.length,
10057
+ " 项"
10058
+ ]
10059
+ }),
10060
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
10061
+ type: "button",
10062
+ "aria-label": "下一页素材",
10063
+ disabled: page + 1 >= pageCount,
10064
+ onClick: () => setPagination({
10065
+ key: pageKey,
10066
+ page: page + 1
10067
+ }),
10068
+ children: "下一页"
10069
+ })
10070
+ ]
10019
10071
  })
10020
10072
  ] }),
10021
10073
  !visibleAssets.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -7,7 +7,7 @@
7
7
  支持 **DeepSeek Harness Desktop 0.2.0-rc.2**。在左侧「插件」→「添加插件」粘贴下面这一行,确认来源后安装:
8
8
 
9
9
  ```text
10
- https://github.com/ShikangPang/jizuo-creator-writing/releases/download/v0.3.29/jizuo-writing-plugin-0.3.29.tgz
10
+ https://github.com/ShikangPang/jizuo-creator-writing/releases/download/v0.3.30/jizuo-writing-plugin-0.3.30.tgz
11
11
  ```
12
12
 
13
13
  只填安装包地址,不要填整个安装命令。安装完成后「已安装」中显示「即作写作」,可分别启用、关闭、配置或卸载。共享核心已包含在安装包中,自动加载,不需要另装即作桌面端或手动安装核心插件。保留 Harness 原有新会话、插件与工作区入口;创作项目统一显示在工作区区域,小说和视频以前置图标区分,不再提供独立的创作面板。
@@ -17,7 +17,7 @@ https://github.com/ShikangPang/jizuo-creator-writing/releases/download/v0.3.29/j
17
17
  也可使用 Desktop 菜单安装的 dsh 命令:
18
18
 
19
19
  ```sh
20
- dsh plugin --profile desktop add https://github.com/ShikangPang/jizuo-creator-writing/releases/download/v0.3.29/jizuo-writing-plugin-0.3.29.tgz
20
+ dsh plugin --profile desktop add https://github.com/ShikangPang/jizuo-creator-writing/releases/download/v0.3.30/jizuo-writing-plugin-0.3.30.tgz
21
21
  ```
22
22
 
23
23
  源码克隆后运行 `node scripts/install.mjs --all` 一次安装五个插件,默认 desktop;Web 用户显式传 `--profile web`。脚本严格检查宿主版本。不要用旧版全局 dsh 命令安装到新版 Desktop,也不要绕过兼容检查。
@@ -26,7 +26,7 @@ dsh plugin --profile desktop add https://github.com/ShikangPang/jizuo-creator-wr
26
26
 
27
27
  启用即作写作插件后,点击工作区列表顶部的“导入小说”。支持 TXT、Markdown 和 DOCX,单文件最大 10 MB。选择文件后预览章节、修改作品名称,确认后创建新的小说项目,展开“正文”即可编辑。取消预览不会创建作品;首章前的内容保留为前言。TXT 支持 UTF-8 和 GB18030 编码。
28
28
 
29
- 已安装多个即作插件时,请将它们一并更新到 0.3.29,并重启 Harness,使共享核心版本一致。
29
+ 已安装多个即作插件时,请将它们一并更新到 0.3.30,并重启 Harness,使共享核心版本一致。
30
30
 
31
31
  ## 项目与数据
32
32
 
@@ -10522,12 +10522,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
10522
10522
  }
10523
10523
  };
10524
10524
  refresh();
10525
- const timer = window.setInterval(() => {
10526
- refresh();
10527
- }, BACKGROUND_REFRESH_INTERVAL_MS);
10528
10525
  return () => {
10529
10526
  active = false;
10530
- window.clearInterval(timer);
10531
10527
  };
10532
10528
  }, [
10533
10529
  expandedVolumeId,
@@ -4,6 +4,7 @@ import { access, link, lstat, mkdir, open, readFile, readdir, realpath, rename,
4
4
  import { closeSync, constants, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, watch, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
6
  import { homedir } from "node:os";
7
+ import { inflateRawSync } from "node:zlib";
7
8
  import { isDeepStrictEqual } from "node:util";
8
9
  import { createServer } from "node:http";
9
10
  //#region \0rolldown/runtime.js
@@ -64627,7 +64628,7 @@ var require_underline = /* @__PURE__ */ __commonJSMin$1(((exports) => {
64627
64628
  }
64628
64629
  }));
64629
64630
  //#endregion
64630
- //#region ../work-domain/src/importer.ts
64631
+ //#region ../work-domain/src/docxLimits.ts
64631
64632
  var import_lib = /* @__PURE__ */ __toESM$1((/* @__PURE__ */ __commonJSMin$1(((exports) => {
64632
64633
  var _ = (init_index_all(), __toCommonJS(index_all_exports));
64633
64634
  var docxReader = require_docx_reader();
@@ -64679,6 +64680,64 @@ var import_lib = /* @__PURE__ */ __toESM$1((/* @__PURE__ */ __commonJSMin$1(((ex
64679
64680
  });
64680
64681
  }
64681
64682
  })))(), 1);
64683
+ const MAX_ENTRIES = 2048;
64684
+ const MAX_ENTRY_BYTES = 16777216;
64685
+ const MAX_TOTAL_BYTES = 33554432;
64686
+ const invalid$1 = () => new JizuoError("validation_error", "无法读取 Word 文档,请确认文件为有效的 DOCX 格式后重试");
64687
+ const oversized = () => new JizuoError("validation_error", "Word 文档解压内容超过安全限制,请拆分后再导入");
64688
+ /** Check both ZIP metadata and bounded actual inflation before handing bytes to Mammoth. */
64689
+ function validateDocxArchive(source) {
64690
+ let end = -1;
64691
+ for (let at = source.length - 22; at >= Math.max(0, source.length - 22 - 65535); at -= 1) if (source.readUInt32LE(at) === 101010256 && at + 22 + source.readUInt16LE(at + 20) === source.length) {
64692
+ end = at;
64693
+ break;
64694
+ }
64695
+ if (end < 0) throw invalid$1();
64696
+ const count = source.readUInt16LE(end + 10);
64697
+ if (count > MAX_ENTRIES) throw new JizuoError("validation_error", "Word 文档包含过多文件条目,请简化文档后再导入");
64698
+ if (source.readUInt32LE(end + 4) !== 0 || count !== source.readUInt16LE(end + 8)) throw invalid$1();
64699
+ const centralSize = source.readUInt32LE(end + 12);
64700
+ const centralStart = source.readUInt32LE(end + 16);
64701
+ if (centralStart + centralSize !== end) throw invalid$1();
64702
+ let cursor = centralStart;
64703
+ let total = 0;
64704
+ const names = /* @__PURE__ */ new Set();
64705
+ for (let index = 0; index < count; index += 1) {
64706
+ if (cursor + 46 > end || source.readUInt32LE(cursor) !== 33639248) throw invalid$1();
64707
+ const flags = source.readUInt16LE(cursor + 8);
64708
+ const method = source.readUInt16LE(cursor + 10);
64709
+ const compressedSize = source.readUInt32LE(cursor + 20);
64710
+ const size = source.readUInt32LE(cursor + 24);
64711
+ const nameSize = source.readUInt16LE(cursor + 28);
64712
+ const next = cursor + 46 + nameSize + source.readUInt16LE(cursor + 30) + source.readUInt16LE(cursor + 32);
64713
+ const local = source.readUInt32LE(cursor + 42);
64714
+ if (next > end || flags & 1 || method !== 0 && method !== 8 || source.readUInt16LE(cursor + 34) !== 0) throw invalid$1();
64715
+ if (size > MAX_ENTRY_BYTES || total + size > MAX_TOTAL_BYTES) throw oversized();
64716
+ const name = source.subarray(cursor + 46, cursor + 46 + nameSize);
64717
+ const key = name.toString("utf8");
64718
+ if (names.has(key) || key.split("/").includes("..")) throw invalid$1();
64719
+ names.add(key);
64720
+ if (local + 30 > centralStart || source.readUInt32LE(local) !== 67324752 || source.readUInt16LE(local + 8) !== method || source.readUInt16LE(local + 6) !== flags) throw invalid$1();
64721
+ const localNameSize = source.readUInt16LE(local + 26);
64722
+ const start = local + 30 + localNameSize + source.readUInt16LE(local + 28);
64723
+ if (start + compressedSize > centralStart || !source.subarray(local + 30, local + 30 + localNameSize).equals(name)) throw invalid$1();
64724
+ const compressed = source.subarray(start, start + compressedSize);
64725
+ let expandedSize = compressedSize;
64726
+ if (method === 8) try {
64727
+ expandedSize = inflateRawSync(compressed, { maxOutputLength: Math.min(MAX_ENTRY_BYTES, MAX_TOTAL_BYTES - total) || 1 }).length;
64728
+ } catch (error) {
64729
+ if (error.code === "ERR_BUFFER_TOO_LARGE") throw oversized();
64730
+ throw invalid$1();
64731
+ }
64732
+ if (expandedSize > MAX_ENTRY_BYTES || total + expandedSize > MAX_TOTAL_BYTES) throw oversized();
64733
+ if (expandedSize !== size) throw invalid$1();
64734
+ total += expandedSize;
64735
+ cursor = next;
64736
+ }
64737
+ if (cursor !== end) throw invalid$1();
64738
+ }
64739
+ //#endregion
64740
+ //#region ../work-domain/src/importer.ts
64682
64741
  function isChapterTitle(title) {
64683
64742
  return /^第[\d零〇一二三四五六七八九十百千万两]+章(?:$|[\s::、..—-])/u.test(title.trim());
64684
64743
  }
@@ -64697,7 +64756,8 @@ function splitMarkdown(markdown, fallbackTitle) {
64697
64756
  const plainChapters = splitText(markdown, fallbackTitle);
64698
64757
  if (plainChapters.some((chapter) => isChapterTitle(chapter.title))) return plainChapters;
64699
64758
  }
64700
- const selected = chapterHeadings.length > 0 ? chapterHeadings : headings.filter((heading) => heading.level === Math.max(...headings.map(({ level }) => level)));
64759
+ const deepestLevel = headings.reduce((level, heading) => Math.max(level, heading.level), 0);
64760
+ const selected = chapterHeadings.length > 0 ? chapterHeadings : headings.filter((heading) => heading.level === deepestLevel);
64701
64761
  if (selected.length === 0) return [{
64702
64762
  title: fallbackTitle,
64703
64763
  content: markdown.trim()
@@ -64751,18 +64811,42 @@ function decodeHtml(value) {
64751
64811
  function htmlToMarkdown(html) {
64752
64812
  return html.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/giu, (_match, level, content) => `\n${"#".repeat(Number(level))} ${decodeHtml(content).trim()}\n`).replace(/<p[^>]*>([\s\S]*?)<\/p>/giu, (_match, content) => `\n${decodeHtml(content)}\n`).replace(/<li[^>]*>([\s\S]*?)<\/li>/giu, (_match, content) => `\n- ${decodeHtml(content)}\n`).replace(/<[^>]+>/gu, "").replace(/\n{3,}/gu, "\n\n").trim();
64753
64813
  }
64814
+ function checkSourceSize(size) {
64815
+ if (size > 10485760) throw new JizuoError("validation_error", "文件超过 10 MB,请拆分后再导入");
64816
+ }
64817
+ async function readLocalSource(path) {
64818
+ const file = await open(path, "r");
64819
+ try {
64820
+ const stat = await file.stat();
64821
+ if (!stat.isFile()) throw new JizuoError("validation_error", "请选择有效的小说文件");
64822
+ checkSourceSize(stat.size);
64823
+ const chunks = [];
64824
+ let size = 0;
64825
+ while (true) {
64826
+ const chunk = Buffer.alloc(Math.min(65536, 10485761 - size));
64827
+ const { bytesRead } = await file.read(chunk);
64828
+ if (bytesRead === 0) return Buffer.concat(chunks, size);
64829
+ size += bytesRead;
64830
+ checkSourceSize(size);
64831
+ chunks.push(chunk.subarray(0, bytesRead));
64832
+ }
64833
+ } finally {
64834
+ await file.close();
64835
+ }
64836
+ }
64754
64837
  async function previewImport(rawInput) {
64755
64838
  const input = PreviewImportInput.parse(rawInput);
64756
- const source = input.sourceBase64 === void 0 ? await readFile(input.sourcePath) : Buffer.from(input.sourceBase64, "base64");
64839
+ const source = input.sourceBase64 === void 0 ? await readLocalSource(input.sourcePath) : Buffer.from(input.sourceBase64, "base64");
64757
64840
  if (input.sourceBase64 !== void 0) {
64758
64841
  if (source.toString("base64") !== input.sourceBase64) throw new JizuoError("validation_error", "文件内容无效,请重新选择小说文件");
64759
- if (source.length > 10485760) throw new JizuoError("validation_error", "文件超过 10 MB,请拆分后再导入");
64760
64842
  }
64843
+ checkSourceSize(source.length);
64761
64844
  if (source.length === 0) throw new JizuoError("validation_error", "文件为空,请选择有正文的小说文件");
64762
64845
  const suggestedTitle = basename(input.sourcePath, extname(input.sourcePath));
64763
64846
  let chapters;
64764
64847
  const warnings = [];
64765
64848
  if (input.format === "docx") {
64849
+ validateDocxArchive(source);
64766
64850
  let converted;
64767
64851
  try {
64768
64852
  converted = await import_lib.convertToHtml({ buffer: source });
@@ -70434,6 +70518,12 @@ const DEFAULT_MAX_RESPONSE_BYTES = 262144;
70434
70518
  const CALLBACK_MAX_BODY_BYTES = 65536;
70435
70519
  const LOGIN_TTL_MS = 18e4;
70436
70520
  const SESSION_TTL_MS = 6048e5;
70521
+ const vaultOperations = /* @__PURE__ */ new WeakMap();
70522
+ function withVault(vault, action) {
70523
+ const pending = (vaultOperations.get(vault) ?? Promise.resolve()).then(action);
70524
+ vaultOperations.set(vault, pending.catch(() => {}));
70525
+ return pending;
70526
+ }
70437
70527
  const NspoxUser = object$1({
70438
70528
  id: string().trim().min(1).max(160),
70439
70529
  name: string().trim().min(1).max(160),
@@ -70521,6 +70611,7 @@ var AccountGateway = class {
70521
70611
  now;
70522
70612
  randomBytes;
70523
70613
  maxResponseBytes;
70614
+ requestTimeoutMs;
70524
70615
  vault;
70525
70616
  pendingLogin;
70526
70617
  constructor(options = {}) {
@@ -70528,8 +70619,10 @@ var AccountGateway = class {
70528
70619
  this.now = options.now ?? Date.now;
70529
70620
  this.randomBytes = options.randomBytes ?? (() => globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)));
70530
70621
  this.maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
70622
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 15e3;
70531
70623
  this.vault = options.vault ?? new MemoryTokenVault();
70532
70624
  if (!Number.isSafeInteger(this.maxResponseBytes) || this.maxResponseBytes < 128) throw new JizuoError("validation_error", "平台响应大小上限无效");
70625
+ if (!Number.isSafeInteger(this.requestTimeoutMs) || this.requestTimeoutMs < 1 || this.requestTimeoutMs > 12e4) throw new JizuoError("validation_error", "平台请求超时时间无效");
70533
70626
  }
70534
70627
  async beginBrowserLogin() {
70535
70628
  this.closePendingLogin();
@@ -70630,7 +70723,7 @@ var AccountGateway = class {
70630
70723
  }
70631
70724
  async logout() {
70632
70725
  this.closePendingLogin();
70633
- await this.vault.clear();
70726
+ await withVault(this.vault, () => this.vault.clear());
70634
70727
  }
70635
70728
  async handleCallback(server, request, response) {
70636
70729
  const pending = this.pendingLogin;
@@ -70672,7 +70765,7 @@ var AccountGateway = class {
70672
70765
  ...refreshToken === "" ? {} : { refreshToken }
70673
70766
  };
70674
70767
  try {
70675
- await this.vault.set(tokens);
70768
+ await withVault(this.vault, () => this.vault.set(tokens));
70676
70769
  } catch {
70677
70770
  pending.consumed = false;
70678
70771
  sendHtml(response, 500, callbackPage(false));
@@ -70694,14 +70787,31 @@ var AccountGateway = class {
70694
70787
  pending.server.close();
70695
70788
  }
70696
70789
  async requireAccessToken() {
70697
- const tokens = await this.vault.get();
70698
- if (tokens === void 0 || tokens.expiresAt <= this.now()) {
70699
- await this.vault.clear();
70700
- throw new JizuoError("credential_required", "请先通过浏览器登录 NSPOX 账号");
70701
- }
70702
- return tokens.accessToken;
70790
+ return withVault(this.vault, async () => {
70791
+ const tokens = await this.vault.get();
70792
+ if (tokens === void 0 || tokens.expiresAt <= this.now()) {
70793
+ await this.vault.clear();
70794
+ throw new JizuoError("credential_required", "请先通过浏览器登录 NSPOX 账号");
70795
+ }
70796
+ return tokens.accessToken;
70797
+ });
70703
70798
  }
70704
70799
  async requestJson(path, options) {
70800
+ const controller = new AbortController();
70801
+ let timeout;
70802
+ const deadline = new Promise((_resolve, reject) => {
70803
+ timeout = setTimeout(() => {
70804
+ reject(new JizuoError("runtime_unavailable", "NSPOX 账号服务响应超时,请稍后重试"));
70805
+ controller.abort();
70806
+ }, this.requestTimeoutMs);
70807
+ });
70808
+ try {
70809
+ return await Promise.race([this.readResponse(path, options, controller.signal), deadline]);
70810
+ } finally {
70811
+ clearTimeout(timeout);
70812
+ }
70813
+ }
70814
+ async readResponse(path, options, signal) {
70705
70815
  const url = validateProductionUrl(new URL(path, PRODUCTION_ORIGIN).href);
70706
70816
  const headers = new Headers({
70707
70817
  accept: "application/json",
@@ -70712,30 +70822,65 @@ var AccountGateway = class {
70712
70822
  response = await this.fetchImpl(url, {
70713
70823
  method: "GET",
70714
70824
  headers,
70715
- redirect: "manual"
70825
+ redirect: "manual",
70826
+ signal
70716
70827
  });
70717
70828
  } catch {
70718
70829
  throw new JizuoError("runtime_unavailable", "无法连接 NSPOX 账号服务");
70719
70830
  }
70720
- if (response.url !== "") validateProductionUrl(response.url);
70721
- if (response.status >= 300 && response.status < 400) {
70722
- const location = response.headers.get("location");
70723
- if (location !== null) validateProductionUrl(new URL(location, url).href);
70724
- throw new JizuoError("denied", "平台响应发生了未允许的重定向");
70725
- }
70726
- if (response.status === 401) {
70727
- await this.vault.clear();
70728
- throw new JizuoError("credential_required", "NSPOX 登录状态已失效");
70729
- }
70730
- if (!response.ok) throw new JizuoError("runtime_unavailable", "NSPOX 账号服务暂时不可用", { status: response.status });
70731
- const declaredSize = Number(response.headers.get("content-length"));
70732
- if (Number.isFinite(declaredSize) && declaredSize > this.maxResponseBytes) throw new JizuoError("validation_error", "平台响应超过安全大小限制");
70733
- const text = await response.text();
70734
- if (new TextEncoder().encode(text).byteLength > this.maxResponseBytes) throw new JizuoError("validation_error", "平台响应超过安全大小限制");
70735
70831
  try {
70736
- return JSON.parse(text);
70737
- } catch {
70738
- throw new JizuoError("validation_error", "平台响应不是有效 JSON");
70832
+ signal.throwIfAborted();
70833
+ if (response.url !== "") validateProductionUrl(response.url);
70834
+ if (response.status >= 300 && response.status < 400) {
70835
+ const location = response.headers.get("location");
70836
+ if (location !== null) validateProductionUrl(new URL(location, url).href);
70837
+ throw new JizuoError("denied", "平台响应发生了未允许的重定向");
70838
+ }
70839
+ if (response.status === 401) {
70840
+ await withVault(this.vault, async () => {
70841
+ if ((await this.vault.get())?.accessToken === options.accessToken) await this.vault.clear();
70842
+ });
70843
+ throw new JizuoError("credential_required", "NSPOX 登录状态已失效");
70844
+ }
70845
+ if (!response.ok) throw new JizuoError("runtime_unavailable", "NSPOX 账号服务暂时不可用", { status: response.status });
70846
+ const declaredSize = Number(response.headers.get("content-length"));
70847
+ if (Number.isFinite(declaredSize) && declaredSize > this.maxResponseBytes) throw new JizuoError("validation_error", "平台响应超过安全大小限制");
70848
+ const text = await this.readBoundedBody(response, signal);
70849
+ try {
70850
+ return JSON.parse(text);
70851
+ } catch {
70852
+ throw new JizuoError("validation_error", "平台响应不是有效 JSON");
70853
+ }
70854
+ } finally {
70855
+ if (response.body !== null && !response.body.locked) response.body.cancel().catch(() => {});
70856
+ }
70857
+ }
70858
+ async readBoundedBody(response, signal) {
70859
+ if (response.body === null) return "";
70860
+ const reader = response.body.getReader();
70861
+ const cancel = () => {
70862
+ reader.cancel().catch(() => {});
70863
+ };
70864
+ signal.addEventListener("abort", cancel, { once: true });
70865
+ const chunks = [];
70866
+ let size = 0;
70867
+ try {
70868
+ while (true) {
70869
+ signal.throwIfAborted();
70870
+ const { done, value } = await reader.read();
70871
+ signal.throwIfAborted();
70872
+ if (done) return new TextDecoder().decode(Buffer.concat(chunks, size));
70873
+ size += value.byteLength;
70874
+ if (size > this.maxResponseBytes) throw new JizuoError("validation_error", "平台响应超过安全大小限制");
70875
+ chunks.push(value);
70876
+ }
70877
+ } catch (error) {
70878
+ if (error instanceof JizuoError) throw error;
70879
+ throw new JizuoError("runtime_unavailable", "读取 NSPOX 账号响应失败,请稍后重试");
70880
+ } finally {
70881
+ signal.removeEventListener("abort", cancel);
70882
+ cancel();
70883
+ reader.releaseLock();
70739
70884
  }
70740
70885
  }
70741
70886
  };