@jayyuen66/dsh-ocr-review 0.1.3 → 0.1.4-rc.1

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
@@ -20,7 +20,7 @@ English · [中文](README.zh-CN.md)
20
20
  - Platforms: macOS and Linux are the targets. Process reaping (the reaper guard around `ocr_review` / `ocr_scan`) depends on a POSIX toolchain - bash traps and job control plus the process-group semantics of `ps` / `pgrep` / `kill`. None of that exists on Windows, so the package runs the bare command there (guarded by `process.platform`): no reaping, but it still works. The cost is that a hard host exit can leave an orphaned OCR process.
21
21
  - The provider list and the key status come from the official host channels `ctx.settings.describe()` (the `value.providers` of the `llm-pi-ai` entry) and `ctx.credentials`.
22
22
  - When either is missing the card shows the degradation reason while tools and settings keep working.
23
- - The host must be `>=0.2.0-rc.2`: declared in `peerDependencies` (the host checks it on plugin install from 0.1.7-rc; alpha.1 has no such gate yet) and in `engines.dsh` (same value, read by nothing).
23
+ - The host must be `>= 0.2.1-alpha.1`: declared in `peerDependencies` (the host checks it on plugin install from 0.1.7-rc; alpha.1 has no such gate yet) and in `engines.dsh` (same value, read by nothing).
24
24
 
25
25
  ## Installation
26
26
 
@@ -30,7 +30,10 @@ dsh plugin --profile web add @jayyuen66/dsh-ocr-review
30
30
 
31
31
  - The packages are on the public npm registry, so installation needs no credentials.
32
32
  - The published files are only `host.js`, `client.js`, `cordis.patch.yml` and `scripts` (`prepack` rebuilds both bundles); source repository: see `repository.url` in package.json.
33
- - Runtime value dependencies are `@jayyuen66/dsh-plugin-shared`, `@deepseek-ai/schemastery` (the host's fork of schemastery — only it implements the 0.1.7 `.volatile()` resolution) and `js-yaml` (the last one used only by `scripts/get-cred.mjs`).
33
+ - No `@deepseek-ai/*` goes in `dependencies`, and that is not a style preference. Consumers of this package are dsh plugins installed into a profile (`nodeLinker: hoisted`, `autoInstallPeers: false`), so any host package listed under `dependencies` is written into that profile as a **real directory**, coexisting with the copy the host process already loaded. `@deepseek-ai/dsh-tools` keys its `TOOL_RUNTIME_SCHEDULER` with a plain `Symbol` (not `Symbol.for`), so symbol identity is per module instance: the host's `dsh-agent-loop` reads a `ToolRuntime` mounted from the duplicate using its own copy's symbol, gets `undefined`, and **every native tool call throws** `Cannot read properties of undefined (reading 'prepare')` — the whole profile loses its tools. This package's build, typecheck and tests all stay green right up to that point, so `test/publish-manifest.test.ts` pins the invariant in the manifest instead.
34
+ - The four host packages this bundle imports as **values** are required (non-optional) `peerDependencies`: `@deepseek-ai/schemastery` (the host's fork of schemastery — only it implements the 0.1.7 `.volatile()` resolution), `@deepseek-ai/dsh-credentials`, `@deepseek-ai/dsh-home-paths` and `@deepseek-ai/dsh-atomic-write`. The host provides them at runtime, and a profile installs no peers, so no duplicate appears. `build-host.mjs` externalizes `dependencies`, `peerDependencies` *and* `optionalDependencies`, so `host.js` keeps the bare specifiers unchanged.
35
+ - Those four are repeated in `devDependencies` — that is what lets this repository's own typecheck, build and tests resolve them. Every other `@deepseek-ai/*` is type-only (erased at build, no specifier left in `host.js`) and stays in `devDependencies` alone.
36
+ - What is left in `dependencies` is genuinely this package's own runtime code: `@jayyuen66/dsh-plugin-shared` and `js-yaml` (the last one used only by `scripts/get-cred.mjs`).
34
37
 
35
38
  ## pnpm blocks the dependency script on install (ERR_PNPM_IGNORED_BUILDS)
36
39
 
package/README.zh-CN.md CHANGED
@@ -19,7 +19,7 @@
19
19
  - 平台:目标平台是 macOS / Linux。进程收割(`ocr_review` / `ocr_scan` 的 reaper 守护)依赖 POSIX 工具链——bash 的 trap 与作业控制,加上 `ps` / `pgrep` / `kill` 的进程组语义。Windows 上这层整体缺席,本包按 `process.platform` 直接放行裸命令(不收割但能跑);代价是宿主硬退出后 OCR 可能留下孤儿进程。
20
20
  - provider 清单与 key 状态走宿主官方通道 `ctx.settings.describe()`(`llm-pi-ai` 那条的 `value.providers`)与 `ctx.credentials`。
21
21
  - 两者缺席时卡片显示降级原因,工具与设置照常可用。
22
- - 宿主版本要求 `>=0.2.0-rc.2`:写在 `peerDependencies`(0.1.7-rc 起宿主装插件时校验它;alpha.1 还没有这道门)与 `engines.dsh`(同值、无人读)。
22
+ - 宿主版本要求 `>= 0.2.1-alpha.1`:写在 `peerDependencies`(0.1.7-rc 起宿主装插件时校验它;alpha.1 还没有这道门)与 `engines.dsh`(同值、无人读)。
23
23
 
24
24
  ## 安装
25
25
 
@@ -56,7 +56,10 @@ allowBuilds:
56
56
  **关于这个 postinstall:** 它在正常路径下什么都不做。平台二进制由上游的 `optionalDependencies`(`@alibaba-group/ocr-<os>-<arch>`,各自带 `os` / `cpu` 字段,pnpm 只装匹配当前平台的那个)提供,`install.js` 检测到就打印 `Binary provided by platform package, skipping download.` 后直接返回。实测放行与不放行都能让 `ocr` 正常执行,差别只是要不要在安装期跑那段脚本——它只在平台包装不上时才下载兜底,而在 pnpm 下平台包必然装得上。
57
57
 
58
58
  如果你更希望完全不执行安装期脚本,可以写 `'@alibaba-group/open-code-review': false`:`ocr` 同样可用(launcher 直接从平台包目录取二进制),但宿主 `readPendingBuilds()` 只认值为 `set this to true or false` 的条目,改成 `false` 后官方审批流程就不再能代为授权,只能手工维护。
59
- - 运行期值依赖 `@jayyuen66/dsh-plugin-shared`、`@deepseek-ai/schemastery`(宿主 fork 的 schemastery,0.1.7 的 `.volatile()` 解析只在它有实现)与 `js-yaml`(后者只被 `scripts/get-cred.mjs` 用)。
59
+ - `dependencies` 里不放任何 `@deepseek-ai/*`,这不是风格偏好。本包的消费方是 dsh 插件,被装进 profile(`nodeLinker: hoisted`、`autoInstallPeers: false`),只要宿主包出现在 `dependencies`,pnpm 就会把它在那颗 profile 里落成**真实目录**,与宿主进程已经加载的那份并存。而 `@deepseek-ai/dsh-tools` 的 `TOOL_RUNTIME_SCHEDULER` 是普通 `Symbol`(不是 `Symbol.for`),符号身份按模块实例计算——宿主 `dsh-agent-loop` 拿自己那份的 Symbol 去读由副本挂载出来的 `ToolRuntime`,得到 `undefined`,于是**每一次原生工具调用都抛** `Cannot read properties of undefined (reading 'prepare')`,整个 profile 的工具一起失效。本包的构建、类型检查与测试在这之前全部照绿,所以判据只能由 `test/publish-manifest.test.ts` 落在 manifest 面。
60
+ - 产物里以**值**导入的四枚宿主包是非 optional 的 `peerDependencies`:`@deepseek-ai/schemastery`(宿主 fork 的 schemastery,0.1.7 的 `.volatile()` 解析只在它有实现)、`@deepseek-ai/dsh-credentials`、`@deepseek-ai/dsh-home-paths`、`@deepseek-ai/dsh-atomic-write`。运行期由宿主在场提供,而 profile 不装 peer,所以不产生副本;`build-host.mjs` 对 `dependencies`、`peerDependencies` 与 `optionalDependencies` 三张表一律外部化,`host.js` 里的裸说明符因此一字不变。
61
+ - 这四枚同时另列一份进 `devDependencies`——那才是本包自己的 typecheck、build、test 解析得到它们的方式。其余 `@deepseek-ai/*` 全是 type-only(构建即擦除,`host.js` 里不留说明符),只留在 `devDependencies`。
62
+ - `dependencies` 剩下的确实是本包自己的运行期代码:`@jayyuen66/dsh-plugin-shared` 与 `js-yaml`(后者只被 `scripts/get-cred.mjs` 用)。
60
63
 
61
64
  ## 在 dsh 里启用
62
65
 
package/client.js CHANGED
@@ -1,16 +1,17 @@
1
1
  // GENERATED by buildClient() in build-client.mjs — edit src/client-entry.ts, then run `node build-client.mjs`.
2
2
  window.__ModuleLoader__.load({
3
- id: '@jayyuen66/dsh-ocr-review',
3
+ id: "@jayyuen66/dsh-ocr-review",
4
4
  factory: (require) => {
5
- var module = { exports: {} };
6
- var exports = module.exports;
5
+ 'use strict';
6
+ const module = { exports: {} };
7
+ let exports = module.exports;
7
8
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
9
  let react = require("react");
9
10
  //#region src/ui-messages.ts
10
11
  const UI_MESSAGES = {
11
12
  zh: {
12
13
  cardTitle: "open-code-review 代码审查",
13
- cardDescription: "ocr 工具配置:选择 provider/model 应用到 OCR(key 走 api_key_cmd 动态读取,不落明文)+ 审查参数设置",
14
+ cardDescription: "ocr 工具配置:选择 provider/model 应用到 OCR(应用后 key 由 api_key_cmd 动态读取,不再写入明文)+ 审查参数设置",
14
15
  statusReadOnly: "当前作用域只读",
15
16
  statusDirty: "有未保存的修改,点「保存」生效",
16
17
  statusClean: "无未保存的修改",
@@ -18,6 +19,7 @@ const UI_MESSAGES = {
18
19
  saving: "保存中…",
19
20
  revert: "撤销",
20
21
  saveFailed: "保存失败:",
22
+ saveRefused: "未写入(宿主拒收或跳过):{fields}",
21
23
  loadProvidersFailed: "加载 providers 失败:{error}(端点未就绪?重启 dsh 后重试)",
22
24
  loadingProviders: "加载 providers…",
23
25
  noKeySuffix: "(未配置 key)",
@@ -68,7 +70,7 @@ const UI_MESSAGES = {
68
70
  },
69
71
  en: {
70
72
  cardTitle: "open-code-review code review",
71
- cardDescription: "ocr tool setup: pick a provider/model and apply it to OCR (keys are read dynamically via api_key_cmd, never stored in plain text) + review parameters",
73
+ cardDescription: "ocr tool setup: pick a provider/model and apply it to OCR (once applied, keys are read dynamically via api_key_cmd and no longer written in plain text) + review parameters",
72
74
  statusReadOnly: "This scope is read-only",
73
75
  statusDirty: "Unsaved changes — press Save to apply",
74
76
  statusClean: "No unsaved changes",
@@ -76,6 +78,7 @@ const UI_MESSAGES = {
76
78
  saving: "Saving…",
77
79
  revert: "Revert",
78
80
  saveFailed: "save failed: ",
81
+ saveRefused: "not written (host refused or skipped): {fields}",
79
82
  loadProvidersFailed: "failed to load providers: {error} (endpoint not ready? restart dsh and retry)",
80
83
  loadingProviders: "loading providers…",
81
84
  noKeySuffix: " (no key configured)",
@@ -126,14 +129,12 @@ const UI_MESSAGES = {
126
129
  }
127
130
  };
128
131
  //#endregion
129
- //#region node_modules/.pnpm/@jayyuen66+dsh-plugin-shared@0.1.0_@deepseek-ai+cordis@4.0.4_eslint-plugin-sonarjs@4.2._a5b0027b68963132c312048d042510c0/node_modules/@jayyuen66/dsh-plugin-shared/dist/record.js
130
- /** 对象类型守卫:非 `null` 的 `object` 且非数组(`Record` 索引签名投影的基础)。 */
131
- function isRecord(value) {
132
- return typeof value === "object" && value !== null && !Array.isArray(value);
132
+ //#region node_modules/.pnpm/@jayyuen66+dsh-plugin-shared@0.1.3_@deepseek-ai+cordis@4.0.5-alpha.1_@deepseek-ai+dsh-c_6c028874c2202469b2088f67c885abe7/node_modules/@jayyuen66/dsh-plugin-shared/dist/record.js
133
+ function e(e) {
134
+ return typeof e == `object` && !!e && !Array.isArray(e);
133
135
  }
134
- /** 从 `unknown` 投影单个字段;非 `Record` → `undefined`。 */
135
- function fieldOf(value, key) {
136
- return isRecord(value) ? value[key] : void 0;
136
+ function t(t, n) {
137
+ return e(t) ? t[n] : void 0;
137
138
  }
138
139
  //#endregion
139
140
  //#region src/client-entry.ts
@@ -176,43 +177,43 @@ function errorSummary(error) {
176
177
  }
177
178
  function modelOf(raw) {
178
179
  return {
179
- id: strOf(fieldOf(raw, "id")),
180
- name: strOf(fieldOf(raw, "name"))
180
+ id: strOf(t(raw, "id")),
181
+ name: strOf(t(raw, "name"))
181
182
  };
182
183
  }
183
184
  function providerOf(raw) {
184
- const modelsArr = fieldOf(raw, "models");
185
+ const modelsArr = t(raw, "models");
185
186
  const models = Array.isArray(modelsArr) ? modelsArr.map((item) => modelOf(item)) : [];
186
- const apiKeyEnvRaw = fieldOf(raw, "apiKeyEnv");
187
+ const apiKeyEnvRaw = t(raw, "apiKeyEnv");
187
188
  return {
188
- name: strOf(fieldOf(raw, "name")),
189
- displayName: strOf(fieldOf(raw, "displayName")),
189
+ name: strOf(t(raw, "name")),
190
+ displayName: strOf(t(raw, "displayName")),
190
191
  apiKeyEnv: typeof apiKeyEnvRaw === "string" ? apiKeyEnvRaw : null,
191
- baseURL: strOf(fieldOf(raw, "baseURL")),
192
+ baseURL: strOf(t(raw, "baseURL")),
192
193
  models,
193
- hasKey: fieldOf(raw, "hasKey") === true
194
+ hasKey: t(raw, "hasKey") === true
194
195
  };
195
196
  }
196
197
  function providersPayloadOf(json) {
197
- const providersArr = fieldOf(json, "providers");
198
+ const providersArr = t(json, "providers");
198
199
  const providers = Array.isArray(providersArr) ? providersArr.map((item) => providerOf(item)) : [];
199
- const currentRaw = fieldOf(json, "current");
200
+ const currentRaw = t(json, "current");
200
201
  const current = {
201
- provider: strOf(fieldOf(currentRaw, "provider")),
202
- model: strOf(fieldOf(currentRaw, "model")),
203
- keyIsDynamic: fieldOf(currentRaw, "keyIsDynamic") === true,
204
- url: strOf(fieldOf(currentRaw, "url"))
202
+ provider: strOf(t(currentRaw, "provider")),
203
+ model: strOf(t(currentRaw, "model")),
204
+ keyIsDynamic: t(currentRaw, "keyIsDynamic") === true,
205
+ url: strOf(t(currentRaw, "url"))
205
206
  };
206
- const sourceRaw = fieldOf(json, "source");
207
+ const sourceRaw = t(json, "source");
207
208
  return {
208
209
  providers,
209
210
  current,
210
- getCredReady: fieldOf(json, "getCredReady") === true,
211
+ getCredReady: t(json, "getCredReady") === true,
211
212
  source: {
212
- status: strOf(fieldOf(sourceRaw, "status")),
213
- message: strOf(fieldOf(sourceRaw, "message"))
213
+ status: strOf(t(sourceRaw, "status")),
214
+ message: strOf(t(sourceRaw, "message"))
214
215
  },
215
- csrf: strOf(fieldOf(json, "csrf"))
216
+ csrf: strOf(t(json, "csrf"))
216
217
  };
217
218
  }
218
219
  /** 三行表单控件(数字/文本/下拉)共用的类名 = CARD_CSS 里 `.ocr-select{…}` 那条规则。
@@ -361,7 +362,7 @@ function OcrPluginCard(props) {
361
362
  }
362
363
  }, headRow, chevron);
363
364
  const body = open ? (0, react.createElement)("div", { className: "ocr-card-body" }, children) : null;
364
- return (0, react.createElement)("li", { className: `ocr-card${open ? " ocr-card-open" : ""}` }, header, body);
365
+ return (0, react.createElement)("div", { className: `ocr-card${open ? " ocr-card-open" : ""}` }, header, body);
365
366
  }
366
367
  /** 整数数字输入行(保存条模式:合法输入上报草稿,不直写)。 */
367
368
  function NumberRow(props) {
@@ -698,7 +699,7 @@ async function refreshProviders(sinks) {
698
699
  try {
699
700
  const res = await fetch(PROVIDERS_URL, { headers: { accept: "application/json" } });
700
701
  if (!res.ok) {
701
- const detail = strOf(fieldOf(await res.json().catch(() => ({})), "error"));
702
+ const detail = strOf(t(await res.json().catch(() => ({})), "error"));
702
703
  throw new Error(detail === "" ? `HTTP ${res.status}` : detail);
703
704
  }
704
705
  const payload = providersPayloadOf(await res.json());
@@ -729,11 +730,11 @@ async function postWithCsrf(url, body, deps) {
729
730
  }
730
731
  /** POST select:把选中的 provider/model 写进 OCR 配置,回执里按 autoVerify 决定是否带 llm test。 */
731
732
  async function runApplySelection(deps) {
732
- const { t, effectiveProvider, effectiveModel } = deps;
733
+ const { t: t$1, effectiveProvider, effectiveModel } = deps;
733
734
  if (!effectiveProvider || !effectiveModel) {
734
735
  deps.setActionMsg({
735
736
  ok: false,
736
- text: t("pickProviderModel")
737
+ text: t$1("pickProviderModel")
737
738
  });
738
739
  return;
739
740
  }
@@ -744,14 +745,14 @@ async function runApplySelection(deps) {
744
745
  provider: effectiveProvider,
745
746
  model: effectiveModel
746
747
  }))).json();
747
- if (fieldOf(selJson, "ok") === true) {
748
+ if (t(selJson, "ok") === true) {
748
749
  let testText;
749
- const llmTestRaw = fieldOf(selJson, "llmTest");
750
- if (isRecord(llmTestRaw)) {
751
- const testOutput = strOf(fieldOf(llmTestRaw, "output")).slice(0, 400);
752
- testText = fieldOf(llmTestRaw, "ok") === true ? t("llmTestPassed") : `${t("llmTestFailed")}${testOutput}`;
753
- } else testText = t("notVerified");
754
- const appliedText = t("applied", {
750
+ const llmTestRaw = t(selJson, "llmTest");
751
+ if (e(llmTestRaw)) {
752
+ const testOutput = strOf(t(llmTestRaw, "output")).slice(0, 400);
753
+ testText = t(llmTestRaw, "ok") === true ? t$1("llmTestPassed") : `${t$1("llmTestFailed")}${testOutput}`;
754
+ } else testText = t$1("notVerified");
755
+ const appliedText = t$1("applied", {
755
756
  provider: effectiveProvider,
756
757
  model: effectiveModel,
757
758
  test: testText
@@ -762,14 +763,14 @@ async function runApplySelection(deps) {
762
763
  });
763
764
  deps.refresh();
764
765
  } else {
765
- const failure = strOf(fieldOf(selJson, "error"));
766
+ const failure = strOf(t(selJson, "error"));
766
767
  deps.setActionMsg({
767
768
  ok: false,
768
- text: failure === "" ? t("applyFailed") : failure
769
+ text: failure === "" ? t$1("applyFailed") : failure
769
770
  });
770
771
  }
771
772
  } catch (error) {
772
- const failedText = `${t("requestFailed")}${errorSummary(error)}`;
773
+ const failedText = `${t$1("requestFailed")}${errorSummary(error)}`;
773
774
  deps.setActionMsg({
774
775
  ok: false,
775
776
  text: failedText
@@ -780,24 +781,24 @@ async function runApplySelection(deps) {
780
781
  }
781
782
  /** POST migrate:明文 api_key 一键迁到 api_key_cmd,回执把迁移/跳过两个名单拼成一行。 */
782
783
  async function runMigrateKeys(deps) {
783
- const { t } = deps;
784
+ const { t: t$2 } = deps;
784
785
  deps.setBusy(true);
785
786
  deps.setActionMsg(null);
786
787
  try {
787
788
  const json = await (await deps.post(MIGRATE_URL)).json();
788
- const migratedVal = fieldOf(json, "migrated");
789
- const skippedVal = fieldOf(json, "skippedUnknown");
789
+ const migratedVal = t(json, "migrated");
790
+ const skippedVal = t(json, "skippedUnknown");
790
791
  const migrated = Array.isArray(migratedVal) ? migratedVal.filter((item) => typeof item === "string") : [];
791
792
  const skipped = Array.isArray(skippedVal) ? skippedVal.filter((item) => typeof item === "string") : [];
792
- const ok = fieldOf(json, "ok") === true;
793
- const head = ok ? (migrated.length > 0 ? t("migratedPrefix") + migrated.join(", ") : t("noPlainKey")) + (skipped.length > 0 ? t("skippedPrefix") + skipped.join(", ") : "") : t("migrateFailed");
793
+ const ok = t(json, "ok") === true;
794
+ const head = ok ? (migrated.length > 0 ? t$2("migratedPrefix") + migrated.join(", ") : t$2("noPlainKey")) + (skipped.length > 0 ? t$2("skippedPrefix") + skipped.join(", ") : "") : t$2("migrateFailed");
794
795
  deps.setActionMsg({
795
796
  ok,
796
797
  text: head
797
798
  });
798
799
  deps.refresh();
799
800
  } catch (error) {
800
- const failedText = `${t("requestFailed")}${errorSummary(error)}`;
801
+ const failedText = `${t$2("requestFailed")}${errorSummary(error)}`;
801
802
  deps.setActionMsg({
802
803
  ok: false,
803
804
  text: failedText
@@ -808,19 +809,19 @@ async function runMigrateKeys(deps) {
808
809
  }
809
810
  /** POST test:手动触发 ocr llm test,输出原样回显在动作条上。 */
810
811
  async function runTestConnection(deps) {
811
- const { t } = deps;
812
+ const { t: t$3 } = deps;
812
813
  deps.setBusy(true);
813
814
  deps.setActionMsg(null);
814
815
  try {
815
816
  const json = await (await deps.post(TEST_URL)).json();
816
- const ok = fieldOf(json, "ok") === true;
817
- const testOutput = strOf(fieldOf(json, "output"));
817
+ const ok = t(json, "ok") === true;
818
+ const testOutput = strOf(t(json, "output"));
818
819
  deps.setActionMsg({
819
820
  ok,
820
- text: (ok ? t("llmTestPassed") : t("llmTestFailed")) + testOutput
821
+ text: (ok ? t$3("llmTestPassed") : t$3("llmTestFailed")) + testOutput
821
822
  });
822
823
  } catch (error) {
823
- const failedText = `${t("requestFailed")}${errorSummary(error)}`;
824
+ const failedText = `${t$3("requestFailed")}${errorSummary(error)}`;
824
825
  deps.setActionMsg({
825
826
  ok: false,
826
827
  text: failedText
@@ -829,7 +830,10 @@ async function runTestConnection(deps) {
829
830
  deps.setBusy(false);
830
831
  }
831
832
  }
832
- /** 只写差异字段:touched 值为 undefined → 写 null(= 清回 schema 默认)。 */
833
+ /** 只写差异字段:touched 值为 undefined → 写 null(= 清回 schema 默认)。
834
+ * 受理位必须回来:`set` 回 `false` 表示宿主拒收或跳过了这次写入(值没落盘),
835
+ * 这时不能清 touched、也不能报成功——否则用户看到「已保存」而配置原封未动。
836
+ * 传输失败仍以 rejection 表达。 */
833
837
  async function saveSettingsDraft(deps) {
834
838
  const { t, value, touched } = deps;
835
839
  const keys = diffTouched(touched, value);
@@ -838,11 +842,16 @@ async function saveSettingsDraft(deps) {
838
842
  deps.setSaveError(null);
839
843
  const ops = keys.map(async (field) => {
840
844
  const draft = touched[field];
841
- await deps.set(field, draft === void 0 ? null : draft);
845
+ return deps.set(field, draft === void 0 ? null : draft);
842
846
  });
843
847
  try {
844
- await Promise.all(ops);
848
+ const settled = await Promise.all(ops);
845
849
  deps.setSettingsBusy(false);
850
+ const refused = keys.filter((_field, index) => settled[index] !== true);
851
+ if (refused.length > 0) {
852
+ deps.setSaveError(t("saveRefused", { fields: refused.join(", ") }));
853
+ return;
854
+ }
846
855
  deps.setTouched({});
847
856
  } catch (error) {
848
857
  deps.setSettingsBusy(false);
@@ -1014,13 +1023,7 @@ function apply(ctx) {
1014
1023
  inject: () => ({
1015
1024
  t,
1016
1025
  hooks: { card: store },
1017
- set: async (field, value) => {
1018
- try {
1019
- await scope.set(field, value);
1020
- } catch (error) {
1021
- console.error(`[ocr-review-card] set ${field} failed:`, error);
1022
- }
1023
- }
1026
+ set: (field, value) => scope.set(field, value)
1024
1027
  })
1025
1028
  }, OcrCard);
1026
1029
  });
package/cordis.patch.yml CHANGED
@@ -1,4 +1,4 @@
1
- # ocr-review(2026-09-14):open-code-review 工作区审查插件(bundle)。
1
+ # ocr-review:open-code-review 工作区审查插件(bundle)。
2
2
  # 双面包机制:本包含 Host 半(host.ts:5 个工具 ocr_review/ocr_scan/
3
3
  # ocr_delegate_preview/ocr_delegate_rule/ocr_session + settings namespace「ocr-review」+
4
4
  # webServer 配置端点)与 Client 半(src/client-entry.ts → client.js:设置卡片,