@carljia/omd-dsh 0.1.8 → 0.1.9

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
@@ -1,13 +1,26 @@
1
1
  # @carljia/omd-dsh
2
2
 
3
- OMD 理念的 DSH 插件:两个 cordis 行 + 7 个模式 preset + 同步 CLI。以 `dsh.bundle` 形式分发——`dsh plugin add @carljia/omd-dsh` 安装后重启即自动同步预设;也可全局安装后用 `omd-dsh sync`。总览、模式矩阵与安装见仓库根 [README.md](../../README.md)。
3
+ OMD 理念的 DSH 插件:host 行 + 客户端设置页 + 7 个模式 preset + 同步 CLI。以 `dsh.bundle` 形式分发——`dsh plugin add @carljia/omd-dsh` 安装后重启即自动同步预设,并在设置面板出现「omd模型分配」页;也可全局安装后用 `omd-dsh sync`。总览、模式矩阵与安装见仓库根 [README.md](../../README.md)。
4
4
 
5
- ## 分发:bundle + boot 行
5
+ ## 分发:bundle + host 行 + 客户端插件
6
6
 
7
- 包声明 `dsh.bundle.patch` → `cordis.patch.yml`,后者注入宿主行 `@carljia/omd-dsh/boot`。该行在 profile 启动时执行与 `omd-dsh sync` 相同的内核(`lib/sync.js`):渲染并复制 7 个 preset 与 `.omd-vendor/` 行模块、首次生成 `omd-matrix.json`。全程幂等、hash 保护、不覆盖本地修改。
7
+ 包声明 `dsh.bundle.patch` → `cordis.patch.yml`,后者注入宿主行 `@carljia/omd-dsh`(包根,`lib/host.js`)。该行在 profile 启动时执行与 `omd-dsh sync` 相同的内核(`lib/sync.js`)并额外注册 settings 命名空间(见下节「设置页与数据流」)。**注入名必须是包根而不是 `/boot` 子路径**:`dsh-client-modules` 按 loader entry 的 `options.name` == 包名来发现客户端插件(读取 `dsh.client` 与 `exports["./client"]`),浏览器 half(`lib/client.js`,见下)由此被发现。`lib/boot.js` 保留为无 settings 场景的手动回退行,仍可通过 `@carljia/omd-dsh/boot` 挂载。
8
8
 
9
9
  `.omd-vendor/` 里的行模块是**自包含 bundle**(`scripts/postbuild.mjs` 用 esbuild 把 schemastery / dsh-tools / dsh-llm / dsh-subagent 全部打进模块,唯一的例外是 `shared.js`——omd-mode 与 omd-task 共享的按 agent 键控状态,作为相对兄弟模块保持单一实例)。行模块不含任何 `@deepseek-ai/*` 导入,因此 sync **不需要知道 harness 装在哪里**,也不会出现"导入指向的树与运行进程不一致"导致的挂载失败——无论 DSH 从 npx 缓存、全局 npm 还是 profile bundles 加载 agent 机制都能工作。preset 组合里其余裸包名行(如 `@deepseek-ai/dsh-persona`)由 harness 的 loader 在运行时按宿主 base 解析。
10
10
 
11
+ ## 设置页「omd模型分配」与数据流
12
+
13
+ 设置 → 左侧导航 →「omd模型分配」(en: OMD model allocation)可视化编辑 7 个模式的模型矩阵(顶层 provider/model + 可选 reasoningEffort;有 tier 的模式逐档展示 provider/model,tier 的 `hint` 只读展示便于理解档位用途)。保存后**新会话**按新矩阵路由模型,运行中的会话保持创建时的组合。
14
+
15
+ 数据流(详见 docs/ARCHITECTURE.md「settings 命名空间数据流」):
16
+
17
+ - `settings.yaml` 的命名空间 `omd-model-allocation` 是**在线编辑的权威存储**(宿主行注册,`base` = 随包默认矩阵 `omd-matrix.default.json`,`applies: "live"`)。
18
+ - 启动时宿主行**调和一次**:若 `<DSH_HOME>/omd-matrix.json` 存在且与命名空间解析值不同,把文件内容 `replace` 进命名空间——旧版/CLI 的自定义不丢,CLI 手改在下次重启被采纳。
19
+ - 用命名空间解析出的完整矩阵渲染 presets,并写回 `omd-matrix.json`(**导出镜像 + CLI 兼容面**)。
20
+ - 监听命名空间:UI 每次保存 → 重新渲染 presets + 更新镜像文件。保存走 `settings.replace`,携带 `expectedRevision`,冲突(`settings-rejected`)时拒绝并重载最新值。
21
+ - `omd-dsh setup` / `omd-dsh sync` CLI 保持不变(读写 `omd-matrix.json`);宿主运行中直接改文件不会实时生效,与「运行中会话不变、新会话重新 sync」的既有语义一致。
22
+ - 边界:settings.yaml 中该段损坏 → 宿主回退纯文件 sync(老 boot 行为),不阻断启动;`omd-matrix.json` 缺失/损坏 → 用命名空间(默认矩阵)渲染并重建镜像;远程浏览器(非 loopback)→ 设置页只读/不可用;settings 只读 → 客户端禁用保存、宿主仍按解析值渲染。
23
+
11
24
  ## 行:omd-mode
12
25
 
13
26
  按模式(agent preset)固定模型路由。行模块是自包含 bundle,不再做 scope 守卫(bundle 内自带的 dsh-scope 副本读不到 harness 实例写入的 kScope Symbol,守卫会误报——见 docs/ARCHITECTURE.md);请按 preset 组合挂载,误挂到全局组合会导致进程级钉模型。
@@ -59,19 +72,24 @@ omd-dsh <command> [--dry-run]
59
72
 
60
73
  细节见 docs/ARCHITECTURE.md 的「vendored 分发与自包含 bundle」。
61
74
 
62
- ## 集中配置:<DSH_HOME>/omd-matrix.json
75
+ ## 集中配置:<DSH_HOME>/omd-matrix.json 与 settings.yaml 命名空间
76
+
77
+ 所有模式的模型路由(provider/model/reasoningEffort)与 omd_task 各 tier 的模型集中在用户矩阵里。两种入口,同一份数据:
78
+
79
+ - **在线编辑(推荐)**:设置 →「omd模型分配」。保存写入 `settings.yaml` 的 `omd-model-allocation` 命名空间,宿主立即重渲染 presets 并把矩阵镜像回 `<DSH_HOME>/omd-matrix.json`。
80
+ - **CLI**:`omd-dsh setup`(交互向导)/ 手改 `omd-matrix.json` 后 `omd-dsh sync`。宿主运行中直接改文件不会实时生效;下次重启时宿主把文件导入命名空间(与「新会话重新 sync」语义一致)。
63
81
 
64
- 所有模式的模型路由(provider/model/reasoningEffort)与 omd_task 各 tier 的模型集中在用户矩阵 `<DSH_HOME>/omd-matrix.json`(默认 `~/.dsh/omd-matrix.json`):`omd-dsh sync` 首次运行把随包的 deepseek 默认矩阵 `omd-matrix.default.json` 复制为默认配置(或从旧版本包内位置迁移一次),`omd-dsh setup` 更新它,`omd-dsh sync` 读取它并渲染进各 preset。仓库与 npm 包只携带默认矩阵文件,**不含任何个人模型配置**——个人模型配置只留在本机。preset 里 `# [omd-dsh:mode:start] / [omd-dsh:mode:end]` 与 `# [omd-dsh:task:start] / [omd-dsh:task:end]` 之间的区域是自动生成的——改模型请改矩阵后跑 sync,不要手改 fence 之间的内容。
82
+ `omd-dsh sync` 首次运行把随包的 deepseek 默认矩阵 `omd-matrix.default.json` 复制为默认配置(或从旧版本包内位置迁移一次)。仓库与 npm 包只携带默认矩阵文件,**不含任何个人模型配置**——个人模型配置只留在本机(`~/.dsh/settings.yaml` 或 `~/.dsh/omd-matrix.json`)。preset 里 `# [omd-dsh:mode:start] / [omd-dsh:mode:end]` 与 `# [omd-dsh:task:start] / [omd-dsh:task:end]` 之间的区域是自动生成的——改模型请走设置页或矩阵后跑 sync,不要手改 fence 之间的内容。
65
83
 
66
84
  ## 开发
67
85
 
68
86
  ```bash
69
- npm install # 触发 build(tsc + esbuild 打包自包含 bundle)
87
+ npm install # 触发 build(tsc + esbuild 打包自包含 bundle + 客户端 bundle)
70
88
  npm test # vitest 全量测试
71
89
  npm pack # prepack 自动重建,产出 tgz
72
90
  ```
73
91
 
74
- 测试桩在 test/stubs/(dsh-tools/dsh-subagent 的轻量替身),不依赖真实 harness。
92
+ 构建产出:`lib/host.js`(宿主行)、`lib/client.js`(客户端 bundle,`window.__ModuleLoader__.load` 信封 + esbuild CJS,仅外部依赖 react)、`lib/vendor/*.mjs`(自包含行 bundle)。测试桩在 test/stubs/(dsh-tools/dsh-subagent 的轻量替身),不依赖真实 harness;`test/sync.test.ts` / `test/host.test.ts` / `test/client-store.test.ts` 覆盖矩阵注入渲染、启动调和判定与客户端保存状态机。
75
93
 
76
94
 
77
95
  ### 子代理透传语义
package/cordis.patch.yml CHANGED
@@ -1,10 +1,17 @@
1
- # omd-dsh bundle patch: inject the boot row that, on startup, materializes
2
- # the 7 OMD agent presets into the user preset root (<DSH_HOME>/.agent-presets).
1
+ # omd-dsh bundle patch: inject the host row under the PACKAGE ROOT name.
3
2
  #
4
- # The boot row (lib/boot.js) resolves the harness tree, copies the presets and
5
- # the vendored row modules, rewrites their @deepseek-ai/* imports to the harness,
6
- # and generates the model matrix on first run — idempotently. Install with:
3
+ # The row (lib/host.js) registers the `omd-model-allocation` settings
4
+ # namespace (settings.yaml = online-edit source), imports CLI/legacy edits
5
+ # from <DSH_HOME>/omd-matrix.json once at startup, renders the 7 OMD presets
6
+ # from the resolved matrix, mirrors the matrix back to omd-matrix.json, and
7
+ # re-renders + re-mirrors on every UI save.
8
+ #
9
+ # The entry name MUST be the bare package name `@carljia/omd-dsh`, not a
10
+ # subpath: dsh-client-modules discovers client plugins by matching loader
11
+ # entry names against package names (reading dsh.client + exports["./client"]),
12
+ # and the browser half (lib/client.js) provides the "omd模型分配" settings
13
+ # section. Install with:
7
14
  # dsh plugin --profile web add @carljia/omd-dsh # then restart dsh.
8
15
  - insert:
9
16
  - id: omd-dsh
10
- name: '@carljia/omd-dsh/boot'
17
+ name: '@carljia/omd-dsh'
@@ -0,0 +1,11 @@
1
+ import * as React from "react";
2
+ import { OmdMatrixController } from "./omd-matrix-controller.js";
3
+ import type { OmdScope } from "./omd-matrix-controller.js";
4
+ /** Cordis services this browser plugin requires (fiber service waiting). */
5
+ export declare const inject: string[];
6
+ export declare function apply(ctx: any): void;
7
+ export declare function OmdModelAllocationSection(props: {
8
+ scope: OmdScope;
9
+ controller: OmdMatrixController;
10
+ t: (key: string) => string;
11
+ }): React.ReactElement;
package/lib/client.js ADDED
@@ -0,0 +1,427 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@carljia/omd-dsh",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ "use strict";
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
15
+ var __export = (target, all) => {
16
+ for (var name in all)
17
+ __defProp(target, name, { get: all[name], enumerable: true });
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") {
21
+ for (let key of __getOwnPropNames(from))
22
+ if (!__hasOwnProp.call(to, key) && key !== except)
23
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
+ }
25
+ return to;
26
+ };
27
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
28
+ // If the importer is in node compatibility mode or this is not an ESM
29
+ // file that has been converted to a CommonJS file using a Babel-
30
+ // compatible transform (i.e. "__esModule" has not been set), then set
31
+ // "default" to the CommonJS "module.exports" for node compatibility.
32
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
33
+ mod
34
+ ));
35
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
36
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
37
+
38
+ // src/client.tsx
39
+ var client_exports = {};
40
+ __export(client_exports, {
41
+ OmdModelAllocationSection: () => OmdModelAllocationSection,
42
+ apply: () => apply,
43
+ inject: () => inject
44
+ });
45
+ module.exports = __toCommonJS(client_exports);
46
+ var React = __toESM(require("react"), 1);
47
+
48
+ // src/omd-matrix-controller.ts
49
+ var OMD_SETTINGS_NS = "omd-model-allocation";
50
+ function cloneMatrix(value) {
51
+ if (value === void 0) return null;
52
+ return JSON.parse(JSON.stringify(value));
53
+ }
54
+ function messageOf(error) {
55
+ return error instanceof Error ? error.message : String(error);
56
+ }
57
+ var OmdMatrixController = class {
58
+ constructor(api, scope, describe) {
59
+ __publicField(this, "api", api);
60
+ __publicField(this, "scope", scope);
61
+ __publicField(this, "describe", describe);
62
+ __publicField(this, "draft");
63
+ __publicField(this, "status", "idle");
64
+ __publicField(this, "error", null);
65
+ __publicField(this, "saved", false);
66
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
67
+ __publicField(this, "unsubscribeScope");
68
+ /**
69
+ * Cached snapshot handed to `useSyncExternalStore`. React requires
70
+ * `getSnapshot()` to return the SAME reference between changes — a fresh
71
+ * object per call throws "The result of getSnapshot should be cached" and
72
+ * crashes the whole settings entry (blank page). Rebuilt only in `emit()`.
73
+ */
74
+ __publicField(this, "snapshot");
75
+ this.draft = cloneMatrix(scope.getSnapshot().value);
76
+ this.snapshot = { draft: this.draft, status: this.status, error: this.error, saved: this.saved };
77
+ this.unsubscribeScope = scope.subscribe(() => this.onScopeChange());
78
+ }
79
+ /** Release the scope subscription (plugin fiber teardown). */
80
+ dispose() {
81
+ this.unsubscribeScope();
82
+ this.listeners.clear();
83
+ }
84
+ /** Stable snapshot reference (uSES-safe): the SAME object until state changes. */
85
+ getSnapshot() {
86
+ return this.snapshot;
87
+ }
88
+ subscribe(listener) {
89
+ this.listeners.add(listener);
90
+ return () => {
91
+ this.listeners.delete(listener);
92
+ };
93
+ }
94
+ emit() {
95
+ this.snapshot = { draft: this.draft, status: this.status, error: this.error, saved: this.saved };
96
+ for (const listener of [...this.listeners]) listener();
97
+ }
98
+ /** External change (mirror reload after a rejected write, another window):
99
+ * rebuild the draft from the fresh value, unless a save is in flight. */
100
+ onScopeChange() {
101
+ if (this.status === "saving") return;
102
+ this.refreshDraft();
103
+ this.emit();
104
+ }
105
+ refreshDraft() {
106
+ const value = this.scope.getSnapshot().value;
107
+ if (value !== void 0) this.draft = cloneMatrix(value);
108
+ }
109
+ /** Whether the Save / Reset controls may run. */
110
+ get canSave() {
111
+ const snap = this.scope.getSnapshot();
112
+ return snap.status === "ready" && snap.writable && this.status !== "saving" && this.draft !== null;
113
+ }
114
+ /** Whether the namespace accepts writes at all (read-only provider / memory mode). */
115
+ get writable() {
116
+ return this.scope.getSnapshot().writable;
117
+ }
118
+ /** Edit one mode's top-level field; an empty string removes the override
119
+ * (the field re-inherits the composition base / schema default). */
120
+ patchMode(modeId, field, value) {
121
+ const draft = this.draft;
122
+ if (draft === null) return;
123
+ const modes = { ...draft.modes };
124
+ const cfg = { ...modes[modeId] ?? {} };
125
+ if (value === "") delete cfg[field];
126
+ else cfg[field] = value;
127
+ modes[modeId] = cfg;
128
+ this.draft = { ...draft, modes };
129
+ this.saved = false;
130
+ this.emit();
131
+ }
132
+ /** Edit one tier's provider/model; an empty string removes the override. */
133
+ patchTier(modeId, tier, field, value) {
134
+ const draft = this.draft;
135
+ if (draft === null) return;
136
+ const modes = { ...draft.modes };
137
+ const cfg = { ...modes[modeId] ?? {} };
138
+ const tiers = { ...cfg.tiers ?? {} };
139
+ const tierCfg = { ...tiers[tier] ?? {} };
140
+ if (value === "") delete tierCfg[field];
141
+ else tierCfg[field] = value;
142
+ tiers[tier] = tierCfg;
143
+ cfg.tiers = tiers;
144
+ modes[modeId] = cfg;
145
+ this.draft = { ...draft, modes };
146
+ this.saved = false;
147
+ this.emit();
148
+ }
149
+ /**
150
+ * Persist the whole draft matrix through `settings.replace` (wholesale
151
+ * section replacement — the removal/reset path merge cannot express). The
152
+ * revision fence makes a stale editor fail loudly instead of overwriting.
153
+ * @returns once the write settled and the snapshot reflects the outcome.
154
+ */
155
+ async save() {
156
+ if (!this.canSave || this.draft === null) return;
157
+ const snap = this.scope.getSnapshot();
158
+ this.status = "saving";
159
+ this.error = null;
160
+ this.emit();
161
+ const response = await this.replace({ section: this.draft, revision: snap.revision });
162
+ if (response.ok) this.saved = true;
163
+ else {
164
+ this.error = response.message;
165
+ await this.describe.load();
166
+ }
167
+ this.refreshDraft();
168
+ this.status = "idle";
169
+ this.emit();
170
+ }
171
+ /** Restore the shipped default matrix: replace with an empty section. */
172
+ async reset() {
173
+ if (!this.canSave) return;
174
+ const snap = this.scope.getSnapshot();
175
+ this.status = "saving";
176
+ this.error = null;
177
+ this.emit();
178
+ const response = await this.replace({ section: {}, revision: snap.revision });
179
+ if (response.ok) this.saved = true;
180
+ else this.error = response.message;
181
+ await this.describe.load();
182
+ this.refreshDraft();
183
+ this.status = "idle";
184
+ this.emit();
185
+ }
186
+ async replace(request) {
187
+ try {
188
+ const response = await this.api.settings.replace({
189
+ ns: OMD_SETTINGS_NS,
190
+ section: request.section,
191
+ ...request.revision === void 0 ? {} : { expectedRevision: request.revision }
192
+ });
193
+ if (response.result.ok) return { ok: true };
194
+ return { ok: false, message: response.result.error.message };
195
+ } catch (error) {
196
+ return { ok: false, message: messageOf(error) };
197
+ }
198
+ }
199
+ };
200
+
201
+ // src/client.tsx
202
+ var NS = "settings.omd";
203
+ var MODE_ORDER = ["executor", "ultraworker", "planner", "reviewer", "explorer", "librarian", "chat"];
204
+ var en = {
205
+ nav: "OMD model allocation",
206
+ title: "OMD model allocation",
207
+ intro: "Model routes for the 7 OMD agent presets. Changes apply to sessions you start from now on \u2014 running sessions keep the models they began with.",
208
+ loading: "Loading the model matrix\u2026",
209
+ unavailable: "The model matrix is not available from this browser (settings are process-local for remote connections).",
210
+ readOnly: "The settings document is read-only here; the current matrix is shown for reference.",
211
+ mode: "Mode",
212
+ provider: "Provider",
213
+ model: "Model",
214
+ reasoningEffort: "Reasoning effort",
215
+ reasoningEffortHint: "Optional; leave empty to let the provider decide.",
216
+ tiers: "Tiers",
217
+ tier: "Tier",
218
+ hint: "Purpose",
219
+ save: "Save",
220
+ saving: "Saving\u2026",
221
+ reset: "Restore defaults",
222
+ saved: "Saved \u2014 new sessions will use the new matrix.",
223
+ errorPrefix: "Save failed:",
224
+ renderError: "The model matrix form could not be rendered:",
225
+ conflict: "The matrix changed on the host; the form was reloaded with the latest values.",
226
+ executor: "Executor",
227
+ ultraworker: "Ultraworker",
228
+ planner: "Planner",
229
+ reviewer: "Reviewer",
230
+ explorer: "Explorer",
231
+ librarian: "Librarian",
232
+ chat: "Chat"
233
+ };
234
+ var zh = {
235
+ nav: "omd\u6A21\u578B\u5206\u914D",
236
+ title: "omd \u6A21\u578B\u5206\u914D",
237
+ intro: "7 \u4E2A OMD agent \u9884\u8BBE\u7684\u6A21\u578B\u8DEF\u7531\u3002\u4FDD\u5B58\u540E\u5BF9\u4E4B\u540E\u65B0\u5EFA\u7684\u4F1A\u8BDD\u751F\u6548\u2014\u2014\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\u4FDD\u6301\u5B83\u5F00\u59CB\u65F6\u7684\u6A21\u578B\u3002",
238
+ loading: "\u6B63\u5728\u52A0\u8F7D\u6A21\u578B\u77E9\u9635\u2026",
239
+ unavailable: "\u5F53\u524D\u6D4F\u89C8\u5668\u65E0\u6CD5\u8BBF\u95EE\u6A21\u578B\u77E9\u9635\uFF08\u8FDC\u7A0B\u8FDE\u63A5\u65F6\u8BBE\u7F6E\u4EC5\u5728\u672C\u673A\u8FDB\u7A0B\u5185\u6709\u6548\uFF09\u3002",
240
+ readOnly: "\u8BBE\u7F6E\u6587\u6863\u5F53\u524D\u53EA\u8BFB\uFF1B\u4EE5\u4E0B\u5C55\u793A\u73B0\u6709\u77E9\u9635\uFF0C\u4EC5\u4F9B\u53C2\u8003\u3002",
241
+ mode: "\u6A21\u5F0F",
242
+ provider: "Provider",
243
+ model: "Model",
244
+ reasoningEffort: "\u63A8\u7406\u5F3A\u5EA6",
245
+ reasoningEffortHint: "\u53EF\u9009\uFF1B\u7559\u7A7A\u4EA4\u7ED9 provider \u9ED8\u8BA4\u3002",
246
+ tiers: "\u6863\u4F4D\uFF08Tiers\uFF09",
247
+ tier: "\u6863\u4F4D",
248
+ hint: "\u7528\u9014",
249
+ save: "\u4FDD\u5B58",
250
+ saving: "\u4FDD\u5B58\u4E2D\u2026",
251
+ reset: "\u6062\u590D\u9ED8\u8BA4",
252
+ saved: "\u5DF2\u4FDD\u5B58\u2014\u2014\u65B0\u4F1A\u8BDD\u5C06\u6309\u65B0\u77E9\u9635\u8DEF\u7531\u6A21\u578B\u3002",
253
+ errorPrefix: "\u4FDD\u5B58\u5931\u8D25\uFF1A",
254
+ renderError: "\u6A21\u578B\u77E9\u9635\u8868\u5355\u6E32\u67D3\u5931\u8D25\uFF1A",
255
+ conflict: "\u77E9\u9635\u5DF2\u5728\u5BBF\u4E3B\u4FA7\u53D8\u66F4\uFF0C\u8868\u5355\u5DF2\u6309\u6700\u65B0\u503C\u91CD\u65B0\u52A0\u8F7D\u3002",
256
+ executor: "\u6267\u884C\u8005",
257
+ ultraworker: "\u8D85\u80FD\u5DE5\u4F5C\u8005",
258
+ planner: "\u89C4\u5212\u8005",
259
+ reviewer: "\u8BC4\u5BA1\u8005",
260
+ explorer: "\u63A2\u7D22\u8005",
261
+ librarian: "\u56FE\u4E66\u7BA1\u7406\u5458",
262
+ chat: "\u5BF9\u8BDD"
263
+ };
264
+ var inject = ["slots", "locale", "connection", "settingsScope"];
265
+ function apply(ctx) {
266
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "omd-dsh: settings section dictionaries");
267
+ const { api } = ctx.get("connection");
268
+ const describe = ctx.settingsScope.describe();
269
+ const scope = ctx.settingsScope.bind({ namespace: OMD_SETTINGS_NS });
270
+ const controller = new OmdMatrixController(api, scope, describe);
271
+ const t = ctx.locale.bind(NS);
272
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
273
+ name: "settings.section",
274
+ id: "omd-model-allocation",
275
+ order: 30,
276
+ // after Models (10) and Agent presets (20)
277
+ label: () => t("nav"),
278
+ locale: NS,
279
+ inject: () => ({ scope, controller, t })
280
+ }, OmdModelAllocationSection));
281
+ }
282
+ var styles = {
283
+ section: { maxWidth: 720, color: "var(--dsw-alias-label-primary)", display: "flex", flexDirection: "column", gap: 12 },
284
+ title: { margin: 0, fontSize: 18, fontWeight: 600 },
285
+ intro: { color: "var(--dsw-alias-label-tertiary)", margin: 0, fontSize: 13, lineHeight: "20px" },
286
+ notice: { color: "var(--dsw-alias-state-warn-label)", margin: 0, fontSize: 12, lineHeight: "18px" },
287
+ savedNotice: { color: "var(--dsw-alias-state-success-primary)", margin: 0, fontSize: 12, lineHeight: "18px" },
288
+ error: { color: "var(--dsw-alias-state-error-primary)", margin: 0, fontSize: 12, lineHeight: "18px" },
289
+ card: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", borderRadius: 12, padding: "14px 16px", display: "flex", flexDirection: "column", gap: 10 },
290
+ cardHead: { display: "flex", alignItems: "baseline", gap: 8 },
291
+ cardTitle: { margin: 0, fontSize: 15, fontWeight: 600 },
292
+ cardRoute: { color: "var(--dsw-alias-label-tertiary)", fontSize: 12, lineHeight: "18px" },
293
+ field: { display: "flex", flexDirection: "column", gap: 4 },
294
+ fieldLabel: { color: "var(--dsw-alias-label-secondary)", fontSize: 12, fontWeight: 500, lineHeight: "18px" },
295
+ input: { boxSizing: "border-box", border: "1px solid var(--dsw-alias-border-l2)", width: "100%", height: 32, font: "inherit", background: "var(--dsw-alias-bg-layer-1)", color: "var(--dsw-alias-label-primary)", borderRadius: 8, padding: "0 10px", fontSize: 14, lineHeight: "22px" },
296
+ inputDisabled: { opacity: 0.6, cursor: "default" },
297
+ tierRow: { borderTop: "1px solid var(--dsw-alias-border-l2)", paddingTop: 10, display: "flex", flexDirection: "column", gap: 8 },
298
+ tierHead: { display: "flex", alignItems: "baseline", gap: 8 },
299
+ tierName: { margin: 0, fontSize: 13, fontWeight: 600 },
300
+ tierHint: { color: "var(--dsw-alias-label-tertiary)", fontSize: 12, lineHeight: "18px", flex: 1, overflowWrap: "anywhere" },
301
+ grid2: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: 8 },
302
+ actions: { display: "flex", alignItems: "center", gap: 8, marginTop: 4 },
303
+ primaryButton: { boxSizing: "border-box", height: 36, font: "inherit", cursor: "pointer", border: "none", borderRadius: 18, padding: "0 14px", fontSize: 14, lineHeight: "22px", background: "var(--dsw-alias-button-primary-fill)", color: "var(--dsw-alias-label-primary-foreground)" },
304
+ secondaryButton: { boxSizing: "border-box", height: 36, font: "inherit", cursor: "pointer", border: "1px solid var(--dsw-alias-border-l2)", background: "transparent", color: "var(--dsw-alias-label-primary)", borderRadius: 18, padding: "0 14px", fontSize: 14, lineHeight: "22px" },
305
+ buttonDisabled: { opacity: 0.4, cursor: "default" }
306
+ };
307
+ var OmdRenderBoundary = class extends React.Component {
308
+ constructor() {
309
+ super(...arguments);
310
+ __publicField(this, "state", { error: null });
311
+ }
312
+ static getDerivedStateFromError(error) {
313
+ return { error };
314
+ }
315
+ render() {
316
+ if (this.state.error !== null) {
317
+ return /* @__PURE__ */ React.createElement("div", { style: styles.section }, /* @__PURE__ */ React.createElement("h2", { style: styles.title }, this.props.t("nav")), /* @__PURE__ */ React.createElement("p", { style: styles.error, role: "alert" }, this.props.t("renderError"), " ", this.state.error instanceof Error ? this.state.error.message : String(this.state.error)));
318
+ }
319
+ return this.props.children ?? null;
320
+ }
321
+ };
322
+ function OmdModelAllocationSection(props) {
323
+ return /* @__PURE__ */ React.createElement(OmdRenderBoundary, { t: props.t }, /* @__PURE__ */ React.createElement(OmdModelAllocationBody, { ...props }));
324
+ }
325
+ function OmdModelAllocationBody(props) {
326
+ const { scope, controller, t } = props;
327
+ const scopeSnapshot = React.useSyncExternalStore(
328
+ React.useCallback((listener) => scope.subscribe(listener), [scope]),
329
+ React.useCallback(() => scope.getSnapshot(), [scope]),
330
+ React.useCallback(() => scope.getSnapshot(), [scope])
331
+ // SSR/test renderers; no-op in the browser
332
+ );
333
+ const ui = React.useSyncExternalStore(
334
+ React.useCallback((listener) => controller.subscribe(listener), [controller]),
335
+ React.useCallback(() => controller.getSnapshot(), [controller]),
336
+ React.useCallback(() => controller.getSnapshot(), [controller])
337
+ );
338
+ if (scopeSnapshot.status === "unavailable") {
339
+ return /* @__PURE__ */ React.createElement("div", { style: styles.section }, /* @__PURE__ */ React.createElement("h2", { style: styles.title }, t("nav")), /* @__PURE__ */ React.createElement("p", { style: styles.intro }, t("unavailable")));
340
+ }
341
+ if (scopeSnapshot.status !== "ready" || ui.draft === null) {
342
+ return /* @__PURE__ */ React.createElement("div", { style: styles.section }, /* @__PURE__ */ React.createElement("h2", { style: styles.title }, t("nav")), /* @__PURE__ */ React.createElement("p", { style: styles.intro }, t("loading")));
343
+ }
344
+ const matrix = ui.draft;
345
+ const readOnly = !controller.writable;
346
+ return /* @__PURE__ */ React.createElement("div", { style: styles.section }, /* @__PURE__ */ React.createElement("h2", { style: styles.title }, t("nav")), /* @__PURE__ */ React.createElement("p", { style: styles.intro }, t("intro")), readOnly ? /* @__PURE__ */ React.createElement("p", { style: styles.notice }, t("readOnly")) : null, ui.saved ? /* @__PURE__ */ React.createElement("p", { style: styles.savedNotice, role: "status" }, t("saved")) : null, ui.error !== null ? /* @__PURE__ */ React.createElement("p", { style: styles.error, role: "alert" }, t("errorPrefix"), " ", ui.error, ui.error.toLowerCase().includes("revision") || ui.error.toLowerCase().includes("changed") ? ` ${t("conflict")}` : "") : null, MODE_ORDER.map((modeId) => {
347
+ const cfg = matrix.modes[modeId] ?? {};
348
+ const route = cfg.provider !== void 0 || cfg.model !== void 0 ? `${cfg.provider ?? "?"}/${cfg.model ?? "?"}` : "";
349
+ const modeName = t(modeId) ?? modeId;
350
+ return /* @__PURE__ */ React.createElement("div", { key: modeId, style: styles.card }, /* @__PURE__ */ React.createElement("div", { style: styles.cardHead }, /* @__PURE__ */ React.createElement("h3", { style: styles.cardTitle }, modeName), /* @__PURE__ */ React.createElement("span", { style: styles.cardRoute }, modeId, route !== "" ? ` \xB7 ${route}` : "")), /* @__PURE__ */ React.createElement("div", { style: styles.grid2 }, /* @__PURE__ */ React.createElement("label", { style: styles.field }, /* @__PURE__ */ React.createElement("span", { style: styles.fieldLabel }, t("provider")), /* @__PURE__ */ React.createElement(
351
+ "input",
352
+ {
353
+ style: { ...styles.input, ...readOnly ? styles.inputDisabled : {} },
354
+ value: cfg.provider ?? "",
355
+ disabled: readOnly || ui.status === "saving",
356
+ spellCheck: false,
357
+ placeholder: "deepseek-official",
358
+ onChange: (event) => controller.patchMode(modeId, "provider", event.target.value)
359
+ }
360
+ )), /* @__PURE__ */ React.createElement("label", { style: styles.field }, /* @__PURE__ */ React.createElement("span", { style: styles.fieldLabel }, t("model")), /* @__PURE__ */ React.createElement(
361
+ "input",
362
+ {
363
+ style: { ...styles.input, ...readOnly ? styles.inputDisabled : {} },
364
+ value: cfg.model ?? "",
365
+ disabled: readOnly || ui.status === "saving",
366
+ spellCheck: false,
367
+ placeholder: "deepseek-v4-pro",
368
+ onChange: (event) => controller.patchMode(modeId, "model", event.target.value)
369
+ }
370
+ ))), /* @__PURE__ */ React.createElement("label", { style: styles.field }, /* @__PURE__ */ React.createElement("span", { style: styles.fieldLabel }, t("reasoningEffort")), /* @__PURE__ */ React.createElement(
371
+ "input",
372
+ {
373
+ style: { ...styles.input, ...readOnly ? styles.inputDisabled : {} },
374
+ value: cfg.reasoningEffort ?? "",
375
+ disabled: readOnly || ui.status === "saving",
376
+ spellCheck: false,
377
+ placeholder: "",
378
+ title: t("reasoningEffortHint"),
379
+ onChange: (event) => controller.patchMode(modeId, "reasoningEffort", event.target.value)
380
+ }
381
+ )), cfg.tiers !== void 0 && Object.keys(cfg.tiers).length > 0 ? /* @__PURE__ */ React.createElement("div", { style: styles.tierRow }, /* @__PURE__ */ React.createElement("span", { style: styles.fieldLabel }, t("tiers")), Object.entries(cfg.tiers).map(([tierName, tier]) => /* @__PURE__ */ React.createElement("div", { key: tierName, style: styles.tierRow }, /* @__PURE__ */ React.createElement("div", { style: styles.tierHead }, /* @__PURE__ */ React.createElement("h4", { style: styles.tierName }, tierName), /* @__PURE__ */ React.createElement("span", { style: styles.tierHint }, tier.hint ?? "")), /* @__PURE__ */ React.createElement("div", { style: styles.grid2 }, /* @__PURE__ */ React.createElement("label", { style: styles.field }, /* @__PURE__ */ React.createElement("span", { style: styles.fieldLabel }, t("provider")), /* @__PURE__ */ React.createElement(
382
+ "input",
383
+ {
384
+ style: { ...styles.input, ...readOnly ? styles.inputDisabled : {} },
385
+ value: tier.provider ?? "",
386
+ disabled: readOnly || ui.status === "saving",
387
+ spellCheck: false,
388
+ onChange: (event) => controller.patchTier(modeId, tierName, "provider", event.target.value)
389
+ }
390
+ )), /* @__PURE__ */ React.createElement("label", { style: styles.field }, /* @__PURE__ */ React.createElement("span", { style: styles.fieldLabel }, t("model")), /* @__PURE__ */ React.createElement(
391
+ "input",
392
+ {
393
+ style: { ...styles.input, ...readOnly ? styles.inputDisabled : {} },
394
+ value: tier.model ?? "",
395
+ disabled: readOnly || ui.status === "saving",
396
+ spellCheck: false,
397
+ onChange: (event) => controller.patchTier(modeId, tierName, "model", event.target.value)
398
+ }
399
+ )))))) : null);
400
+ }), !readOnly ? /* @__PURE__ */ React.createElement("div", { style: styles.actions }, /* @__PURE__ */ React.createElement(
401
+ "button",
402
+ {
403
+ type: "button",
404
+ style: { ...styles.primaryButton, ...!controller.canSave ? styles.buttonDisabled : {} },
405
+ disabled: !controller.canSave,
406
+ onClick: () => {
407
+ controller.save();
408
+ }
409
+ },
410
+ ui.status === "saving" ? t("saving") : t("save")
411
+ ), /* @__PURE__ */ React.createElement(
412
+ "button",
413
+ {
414
+ type: "button",
415
+ style: { ...styles.secondaryButton, ...!controller.canSave ? styles.buttonDisabled : {} },
416
+ disabled: !controller.canSave,
417
+ onClick: () => {
418
+ controller.reset();
419
+ }
420
+ },
421
+ t("reset")
422
+ )) : null);
423
+ }
424
+
425
+ return module.exports;
426
+ }
427
+ });
package/lib/host.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type { Matrix } from "./sync.js";
2
+ /**
3
+ * @module @carljia/omd-dsh
4
+ *
5
+ * omd-dsh host row (package root entry, injected by cordis.patch.yml):
6
+ *
7
+ * 1. registers the `omd-model-allocation` settings namespace (base = the
8
+ * shipped default matrix, `applies: "live"`), making the settings
9
+ * document (<DSH_HOME>/settings.yaml) the authoritative "online edit"
10
+ * store for the model matrix;
11
+ * 2. reconciles once at startup: if <DSH_HOME>/omd-matrix.json exists and
12
+ * differs from the namespace's resolved value, its content is imported
13
+ * into the namespace (`replace`) — CLI edits and pre-settings installs
14
+ * survive the migration, and CLI edits are picked up on the next restart;
15
+ * 3. renders the 7 presets from the namespace's resolved matrix and mirrors
16
+ * that matrix back to omd-matrix.json (the export mirror + CLI face);
17
+ * 4. watches the namespace: every UI save re-renders the presets and
18
+ * re-mirrors the file. New sessions route with the new matrix; running
19
+ * sessions keep the combination they started with.
20
+ *
21
+ * The browser half (lib/client.js) binds the same namespace and renders the
22
+ * "omd模型分配" settings section (see src/client.tsx). Because the client
23
+ * plugin discovery (dsh-client-modules) matches loader entries by package
24
+ * name, the bundle patch injects THIS row under the bare package name
25
+ * `@carljia/omd-dsh` — not a `/boot` subpath.
26
+ *
27
+ * Failure policy: a broken settings document / namespace must never brick the
28
+ * host's boot — any failure here falls back to the plain file sync (the old
29
+ * boot behavior) and is logged.
30
+ */
31
+ /** Cordis plugin name used by loader diagnostics. */
32
+ export declare const name = "omd-dsh";
33
+ /** Requires the settings service (provided by dsh-base → dsh-settings-file in every profile). */
34
+ export declare const inject: string[];
35
+ /**
36
+ * Startup reconciliation decision (pure, unit-tested): the file matrix is
37
+ * imported into the settings namespace exactly when it exists AND differs
38
+ * from the namespace's currently resolved value. Equal values need no write;
39
+ * a missing file leaves the namespace (defaults / user section) authoritative.
40
+ */
41
+ export declare function shouldImportFile(fileMatrix: Matrix | undefined, resolved: Matrix): boolean;
42
+ export declare function apply(ctx: any): Promise<void>;
package/lib/host.js ADDED
@@ -0,0 +1,103 @@
1
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
2
+ import { readDefaultMatrix, readMatrixFileIfExists, matrixEquals, runSyncWithMatrix, runSync, saveMatrix, MatrixSchema, MATRIX_PATH, } from "./sync.js";
3
+ /**
4
+ * @module @carljia/omd-dsh
5
+ *
6
+ * omd-dsh host row (package root entry, injected by cordis.patch.yml):
7
+ *
8
+ * 1. registers the `omd-model-allocation` settings namespace (base = the
9
+ * shipped default matrix, `applies: "live"`), making the settings
10
+ * document (<DSH_HOME>/settings.yaml) the authoritative "online edit"
11
+ * store for the model matrix;
12
+ * 2. reconciles once at startup: if <DSH_HOME>/omd-matrix.json exists and
13
+ * differs from the namespace's resolved value, its content is imported
14
+ * into the namespace (`replace`) — CLI edits and pre-settings installs
15
+ * survive the migration, and CLI edits are picked up on the next restart;
16
+ * 3. renders the 7 presets from the namespace's resolved matrix and mirrors
17
+ * that matrix back to omd-matrix.json (the export mirror + CLI face);
18
+ * 4. watches the namespace: every UI save re-renders the presets and
19
+ * re-mirrors the file. New sessions route with the new matrix; running
20
+ * sessions keep the combination they started with.
21
+ *
22
+ * The browser half (lib/client.js) binds the same namespace and renders the
23
+ * "omd模型分配" settings section (see src/client.tsx). Because the client
24
+ * plugin discovery (dsh-client-modules) matches loader entries by package
25
+ * name, the bundle patch injects THIS row under the bare package name
26
+ * `@carljia/omd-dsh` — not a `/boot` subpath.
27
+ *
28
+ * Failure policy: a broken settings document / namespace must never brick the
29
+ * host's boot — any failure here falls back to the plain file sync (the old
30
+ * boot behavior) and is logged.
31
+ */
32
+ /** Cordis plugin name used by loader diagnostics. */
33
+ export const name = "omd-dsh";
34
+ /** Requires the settings service (provided by dsh-base → dsh-settings-file in every profile). */
35
+ export const inject = ["settings"];
36
+ const NS = settingsNamespace("omd-model-allocation");
37
+ /**
38
+ * Startup reconciliation decision (pure, unit-tested): the file matrix is
39
+ * imported into the settings namespace exactly when it exists AND differs
40
+ * from the namespace's currently resolved value. Equal values need no write;
41
+ * a missing file leaves the namespace (defaults / user section) authoritative.
42
+ */
43
+ export function shouldImportFile(fileMatrix, resolved) {
44
+ return fileMatrix !== undefined && !matrixEquals(fileMatrix, resolved);
45
+ }
46
+ function makeLog(ctx) {
47
+ return {
48
+ info: (message) => { try {
49
+ ctx?.logger?.info?.(message);
50
+ }
51
+ catch {
52
+ console.log(message);
53
+ } },
54
+ error: (message) => { try {
55
+ ctx?.logger?.error?.(message);
56
+ }
57
+ catch {
58
+ console.error(message);
59
+ } },
60
+ };
61
+ }
62
+ export async function apply(ctx) {
63
+ const log = makeLog(ctx);
64
+ const flags = { dryRun: false, verbose: false };
65
+ try {
66
+ const settings = ctx.settings; // SettingsProvider (dsh-base provides it in every profile)
67
+ const scope = settings.register(NS, MatrixSchema, {
68
+ base: readDefaultMatrix(),
69
+ applies: "live",
70
+ });
71
+ // 启动期调和:文件 → 命名空间(幂等;导入 CLI/旧版自定义,CLI 手改在下次重启被采纳)
72
+ const fileMatrix = readMatrixFileIfExists();
73
+ if (shouldImportFile(fileMatrix, scope.get())) {
74
+ log.info("omd-dsh: importing " + MATRIX_PATH + " into settings namespace " + NS);
75
+ await scope.replace(fileMatrix);
76
+ }
77
+ // 用命名空间解析出的完整矩阵渲染 presets,并把该矩阵写回镜像文件
78
+ const resolved = scope.get();
79
+ saveMatrix(resolved);
80
+ await runSyncWithMatrix(resolved, flags, log.info);
81
+ // 后续 UI 编辑:重渲染 + 更新镜像(watch 仅在解析值真正变化时触发)
82
+ scope.watch((next) => {
83
+ log.info("omd-dsh: matrix changed via settings — re-syncing presets and mirroring omd-matrix.json");
84
+ try {
85
+ saveMatrix(next);
86
+ }
87
+ catch (error) {
88
+ log.error("omd-dsh: failed to mirror " + MATRIX_PATH + ": " + (error instanceof Error ? error.message : String(error)));
89
+ }
90
+ runSyncWithMatrix(next, flags, log.info).catch((error) => log.error("omd-dsh re-sync failed: " + (error instanceof Error ? error.message : String(error))));
91
+ });
92
+ }
93
+ catch (error) {
94
+ // settings 不可用 / 命名空间已损坏时退回纯文件 sync(保持老 boot 行为)
95
+ log.error("omd-dsh: settings namespace failed, falling back to file sync: " + (error instanceof Error ? error.message : String(error)));
96
+ try {
97
+ await runSync(flags, log.info);
98
+ }
99
+ catch (fallbackError) {
100
+ log.error("omd-dsh: file sync fallback failed: " + (fallbackError instanceof Error ? fallbackError.message : String(fallbackError)));
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,108 @@
1
+ import type { Matrix } from "./sync.js";
2
+ /**
3
+ * Client-side edit state machine for the omd model matrix — deliberately free
4
+ * of React and DOM so it can be unit-tested against fake wire faces
5
+ * (test/client-store.test.ts).
6
+ *
7
+ * The controller mirrors one settings namespace scope (`ctx.settingsScope`
8
+ * bind) and writes through `api.settings.replace` with the scope's revision
9
+ * as `expectedRevision` — a stale editor is refused rather than silently
10
+ * overwriting a concurrent change (`settings-rejected` on the wire).
11
+ */
12
+ /** Settings namespace the host row registers (src/host.ts) — must stay in step. */
13
+ export declare const OMD_SETTINGS_NS = "omd-model-allocation";
14
+ /** Client-side sync state of one settings namespace (wire-contract subset). */
15
+ export interface OmdScopeSnapshot {
16
+ status: "loading" | "ready" | "unavailable";
17
+ value: Matrix | undefined;
18
+ revision: number | undefined;
19
+ writable: boolean;
20
+ mode: "host" | "memory";
21
+ }
22
+ /** The `settings.replace` wire face this controller needs. */
23
+ export interface OmdSettingsApi {
24
+ settings: {
25
+ replace(request: {
26
+ ns: string;
27
+ section: object;
28
+ expectedRevision?: number;
29
+ }): Promise<{
30
+ result: {
31
+ ok: true;
32
+ value: unknown;
33
+ };
34
+ } | {
35
+ result: {
36
+ ok: false;
37
+ error: {
38
+ message: string;
39
+ };
40
+ };
41
+ }>;
42
+ };
43
+ }
44
+ /** The shared describe mirror face (used to reload Host state after a rejected write). */
45
+ export interface OmdDescribeFace {
46
+ load(): Promise<unknown>;
47
+ }
48
+ /** The bound scope as the controller consumes it. */
49
+ export interface OmdScope {
50
+ getSnapshot(): OmdScopeSnapshot;
51
+ subscribe(listener: () => void): () => void;
52
+ }
53
+ export interface OmdControllerSnapshot {
54
+ draft: Matrix | null;
55
+ status: "idle" | "saving";
56
+ error: string | null;
57
+ saved: boolean;
58
+ }
59
+ type ModeField = "provider" | "model" | "reasoningEffort";
60
+ export declare class OmdMatrixController {
61
+ private readonly api;
62
+ private readonly scope;
63
+ private readonly describe;
64
+ draft: Matrix | null;
65
+ status: "idle" | "saving";
66
+ error: string | null;
67
+ saved: boolean;
68
+ private readonly listeners;
69
+ private readonly unsubscribeScope;
70
+ /**
71
+ * Cached snapshot handed to `useSyncExternalStore`. React requires
72
+ * `getSnapshot()` to return the SAME reference between changes — a fresh
73
+ * object per call throws "The result of getSnapshot should be cached" and
74
+ * crashes the whole settings entry (blank page). Rebuilt only in `emit()`.
75
+ */
76
+ private snapshot;
77
+ constructor(api: OmdSettingsApi, scope: OmdScope, describe: OmdDescribeFace);
78
+ /** Release the scope subscription (plugin fiber teardown). */
79
+ dispose(): void;
80
+ /** Stable snapshot reference (uSES-safe): the SAME object until state changes. */
81
+ getSnapshot(): OmdControllerSnapshot;
82
+ subscribe(listener: () => void): () => void;
83
+ private emit;
84
+ /** External change (mirror reload after a rejected write, another window):
85
+ * rebuild the draft from the fresh value, unless a save is in flight. */
86
+ private onScopeChange;
87
+ private refreshDraft;
88
+ /** Whether the Save / Reset controls may run. */
89
+ get canSave(): boolean;
90
+ /** Whether the namespace accepts writes at all (read-only provider / memory mode). */
91
+ get writable(): boolean;
92
+ /** Edit one mode's top-level field; an empty string removes the override
93
+ * (the field re-inherits the composition base / schema default). */
94
+ patchMode(modeId: string, field: ModeField, value: string): void;
95
+ /** Edit one tier's provider/model; an empty string removes the override. */
96
+ patchTier(modeId: string, tier: string, field: "provider" | "model", value: string): void;
97
+ /**
98
+ * Persist the whole draft matrix through `settings.replace` (wholesale
99
+ * section replacement — the removal/reset path merge cannot express). The
100
+ * revision fence makes a stale editor fail loudly instead of overwriting.
101
+ * @returns once the write settled and the snapshot reflects the outcome.
102
+ */
103
+ save(): Promise<void>;
104
+ /** Restore the shipped default matrix: replace with an empty section. */
105
+ reset(): Promise<void>;
106
+ private replace;
107
+ }
108
+ export {};
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Client-side edit state machine for the omd model matrix — deliberately free
3
+ * of React and DOM so it can be unit-tested against fake wire faces
4
+ * (test/client-store.test.ts).
5
+ *
6
+ * The controller mirrors one settings namespace scope (`ctx.settingsScope`
7
+ * bind) and writes through `api.settings.replace` with the scope's revision
8
+ * as `expectedRevision` — a stale editor is refused rather than silently
9
+ * overwriting a concurrent change (`settings-rejected` on the wire).
10
+ */
11
+ /** Settings namespace the host row registers (src/host.ts) — must stay in step. */
12
+ export const OMD_SETTINGS_NS = "omd-model-allocation";
13
+ function cloneMatrix(value) {
14
+ if (value === undefined)
15
+ return null;
16
+ return JSON.parse(JSON.stringify(value));
17
+ }
18
+ function messageOf(error) {
19
+ return error instanceof Error ? error.message : String(error);
20
+ }
21
+ export class OmdMatrixController {
22
+ api;
23
+ scope;
24
+ describe;
25
+ draft;
26
+ status = "idle";
27
+ error = null;
28
+ saved = false;
29
+ listeners = new Set();
30
+ unsubscribeScope;
31
+ /**
32
+ * Cached snapshot handed to `useSyncExternalStore`. React requires
33
+ * `getSnapshot()` to return the SAME reference between changes — a fresh
34
+ * object per call throws "The result of getSnapshot should be cached" and
35
+ * crashes the whole settings entry (blank page). Rebuilt only in `emit()`.
36
+ */
37
+ snapshot;
38
+ constructor(api, scope, describe) {
39
+ this.api = api;
40
+ this.scope = scope;
41
+ this.describe = describe;
42
+ this.draft = cloneMatrix(scope.getSnapshot().value);
43
+ this.snapshot = { draft: this.draft, status: this.status, error: this.error, saved: this.saved };
44
+ this.unsubscribeScope = scope.subscribe(() => this.onScopeChange());
45
+ }
46
+ /** Release the scope subscription (plugin fiber teardown). */
47
+ dispose() {
48
+ this.unsubscribeScope();
49
+ this.listeners.clear();
50
+ }
51
+ /** Stable snapshot reference (uSES-safe): the SAME object until state changes. */
52
+ getSnapshot() {
53
+ return this.snapshot;
54
+ }
55
+ subscribe(listener) {
56
+ this.listeners.add(listener);
57
+ return () => { this.listeners.delete(listener); };
58
+ }
59
+ emit() {
60
+ this.snapshot = { draft: this.draft, status: this.status, error: this.error, saved: this.saved };
61
+ for (const listener of [...this.listeners])
62
+ listener();
63
+ }
64
+ /** External change (mirror reload after a rejected write, another window):
65
+ * rebuild the draft from the fresh value, unless a save is in flight. */
66
+ onScopeChange() {
67
+ if (this.status === "saving")
68
+ return;
69
+ this.refreshDraft();
70
+ this.emit();
71
+ }
72
+ refreshDraft() {
73
+ const value = this.scope.getSnapshot().value;
74
+ if (value !== undefined)
75
+ this.draft = cloneMatrix(value);
76
+ }
77
+ /** Whether the Save / Reset controls may run. */
78
+ get canSave() {
79
+ const snap = this.scope.getSnapshot();
80
+ return snap.status === "ready" && snap.writable && this.status !== "saving" && this.draft !== null;
81
+ }
82
+ /** Whether the namespace accepts writes at all (read-only provider / memory mode). */
83
+ get writable() {
84
+ return this.scope.getSnapshot().writable;
85
+ }
86
+ /** Edit one mode's top-level field; an empty string removes the override
87
+ * (the field re-inherits the composition base / schema default). */
88
+ patchMode(modeId, field, value) {
89
+ const draft = this.draft;
90
+ if (draft === null)
91
+ return;
92
+ const modes = { ...draft.modes };
93
+ const cfg = { ...(modes[modeId] ?? {}) };
94
+ if (value === "")
95
+ delete cfg[field];
96
+ else
97
+ cfg[field] = value;
98
+ modes[modeId] = cfg;
99
+ this.draft = { ...draft, modes };
100
+ this.saved = false;
101
+ this.emit();
102
+ }
103
+ /** Edit one tier's provider/model; an empty string removes the override. */
104
+ patchTier(modeId, tier, field, value) {
105
+ const draft = this.draft;
106
+ if (draft === null)
107
+ return;
108
+ const modes = { ...draft.modes };
109
+ const cfg = { ...(modes[modeId] ?? {}) };
110
+ const tiers = { ...(cfg.tiers ?? {}) };
111
+ const tierCfg = { ...(tiers[tier] ?? {}) };
112
+ if (value === "")
113
+ delete tierCfg[field];
114
+ else
115
+ tierCfg[field] = value;
116
+ tiers[tier] = tierCfg;
117
+ cfg.tiers = tiers;
118
+ modes[modeId] = cfg;
119
+ this.draft = { ...draft, modes };
120
+ this.saved = false;
121
+ this.emit();
122
+ }
123
+ /**
124
+ * Persist the whole draft matrix through `settings.replace` (wholesale
125
+ * section replacement — the removal/reset path merge cannot express). The
126
+ * revision fence makes a stale editor fail loudly instead of overwriting.
127
+ * @returns once the write settled and the snapshot reflects the outcome.
128
+ */
129
+ async save() {
130
+ if (!this.canSave || this.draft === null)
131
+ return;
132
+ const snap = this.scope.getSnapshot();
133
+ this.status = "saving";
134
+ this.error = null;
135
+ this.emit();
136
+ const response = await this.replace({ section: this.draft, revision: snap.revision });
137
+ if (response.ok)
138
+ this.saved = true;
139
+ else {
140
+ this.error = response.message;
141
+ await this.describe.load();
142
+ }
143
+ this.refreshDraft();
144
+ this.status = "idle";
145
+ this.emit();
146
+ }
147
+ /** Restore the shipped default matrix: replace with an empty section. */
148
+ async reset() {
149
+ if (!this.canSave)
150
+ return;
151
+ const snap = this.scope.getSnapshot();
152
+ this.status = "saving";
153
+ this.error = null;
154
+ this.emit();
155
+ const response = await this.replace({ section: {}, revision: snap.revision });
156
+ if (response.ok)
157
+ this.saved = true;
158
+ else
159
+ this.error = response.message;
160
+ await this.describe.load();
161
+ this.refreshDraft();
162
+ this.status = "idle";
163
+ this.emit();
164
+ }
165
+ async replace(request) {
166
+ try {
167
+ const response = await this.api.settings.replace({
168
+ ns: OMD_SETTINGS_NS,
169
+ section: request.section,
170
+ ...(request.revision === undefined ? {} : { expectedRevision: request.revision }),
171
+ });
172
+ if (response.result.ok)
173
+ return { ok: true };
174
+ return { ok: false, message: response.result.error.message };
175
+ }
176
+ catch (error) {
177
+ return { ok: false, message: messageOf(error) };
178
+ }
179
+ }
180
+ }
package/lib/sync.d.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  /**
2
2
  * omd-dsh sync core — materialize the OMD presets into <DSH_HOME>/.agent-presets.
3
3
  *
4
- * Shared by the CLI (`omd-dsh sync`) and the bundle boot row (`dsh plugin add`
5
- * + restart): render each preset's omd-mode / omd-task rows from the user's
6
- * model matrix, and copy the presets plus the vendored row modules into
7
- * .agent-presets.
4
+ * Shared by the CLI (`omd-dsh sync`), the bundle boot row (`dsh plugin add`
5
+ * + restart), and the host settings row (lib/host.js): render each preset's
6
+ * omd-mode / omd-task rows from the model matrix, and copy the presets plus
7
+ * the vendored row modules into .agent-presets.
8
+ *
9
+ * The matrix has two faces: `runSync` reads/writes the CLI face
10
+ * (<DSH_HOME>/omd-matrix.json, the `omd-dsh setup` target), while the host
11
+ * settings row resolves the matrix from the settings namespace
12
+ * (`omd-model-allocation` in settings.yaml) and feeds it to
13
+ * `runSyncWithMatrix` — the file becomes an exported mirror of the namespace
14
+ * plus the CLI compatibility face (see src/host.ts).
8
15
  *
9
16
  * The vendored rows are SELF-CONTAINED bundles (see scripts/postbuild.mjs):
10
17
  * every dependency is bundled in and they carry no @deepseek-ai imports, so
@@ -48,6 +55,26 @@ export declare const VENDOR_SOURCES: string[];
48
55
  /** User-owned model matrix: lives under DSH_HOME, never inside the package or the repo. */
49
56
  export declare const MATRIX_PATH: string;
50
57
  export declare function dshHome(): string;
58
+ export declare const MatrixSchema: any;
59
+ export declare function readDefaultMatrix(): Matrix;
60
+ /**
61
+ * Read the user's matrix file (<DSH_HOME>/omd-matrix.json) without touching
62
+ * the filesystem beyond the read: missing or malformed returns undefined and
63
+ * NEVER auto-generates — the caller decides what to fall back to (settings
64
+ * namespace resolved value / default matrix). This is the host row's
65
+ * "import CLI / legacy edits into the settings namespace" read.
66
+ */
67
+ export declare function readMatrixFileIfExists(): Matrix | undefined;
68
+ /** Structural equality over the small JSON matrix (stringify equality suffices). */
69
+ export declare function matrixEquals(a: Matrix, b: Matrix): boolean;
51
70
  export declare function loadMatrix(flags: SyncFlags, log?: (msg: string) => void): Matrix;
52
71
  export declare function saveMatrix(m: Matrix): void;
72
+ /**
73
+ * Render and materialize the presets from an ALREADY-RESOLVED matrix. This is
74
+ * the shared core between the CLI file path (`runSync`) and the settings
75
+ * namespace path (host row): the matrix comes from the caller, everything
76
+ * after `loadMatrix(...)` is here.
77
+ */
78
+ export declare function runSyncWithMatrix(matrix: Matrix, flags: SyncFlags, log?: (msg: string) => void): Promise<void>;
79
+ /** CLI / manual-fallback path: load the matrix from <DSH_HOME>/omd-matrix.json (generating/migrating it on first run), then render. */
53
80
  export declare function runSync(flags: SyncFlags, log?: (msg: string) => void): Promise<void>;
package/lib/sync.js CHANGED
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import { dirname, join, relative, resolve } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { fileURLToPath } from "node:url";
6
+ import z from "@deepseek-ai/schemastery";
6
7
  export const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
7
8
  export const VENDOR_SOURCES = ["omd-mode.mjs", "omd-task.mjs", "omd-ulw.mjs", "omd-plan.mjs", "omd-start-work.mjs", "omd-mode-switch.mjs", "shared.js"];
8
9
  /** User-owned model matrix: lives under DSH_HOME, never inside the package or the repo. */
@@ -45,6 +46,32 @@ async function collectSourceFiles(rootDir) {
45
46
  }
46
47
  // ── matrix ──
47
48
  const DEFAULT_MATRIX_PATH = join(PACKAGE_ROOT, "omd-matrix.default.json");
49
+ /** Settings-namespace schema for the model matrix (see plan §5). Every field
50
+ * is optional: `base` (the shipped default matrix) fills whatever the user
51
+ * document omits, and extra keys (future fields) pass through untouched. */
52
+ const TierSchema = z.object({
53
+ provider: z.string(),
54
+ model: z.string(),
55
+ hint: z.string(),
56
+ persona: z.string(),
57
+ maxTokens: z.number(),
58
+ toolFilter: z.object({
59
+ allow: z.array(z.string()),
60
+ deny: z.array(z.string()),
61
+ denyShell: z.boolean(),
62
+ }),
63
+ });
64
+ const ModeSchema = z.object({
65
+ provider: z.string(),
66
+ model: z.string(),
67
+ reasoningEffort: z.string(),
68
+ tiers: z.dict(TierSchema),
69
+ });
70
+ export const MatrixSchema = z.object({
71
+ version: z.number(),
72
+ defaults: z.object({ provider: z.string() }),
73
+ modes: z.dict(ModeSchema),
74
+ });
48
75
  function parseMatrix(text) {
49
76
  try {
50
77
  const parsed = JSON.parse(text);
@@ -54,7 +81,7 @@ function parseMatrix(text) {
54
81
  catch { /* fall through */ }
55
82
  return undefined;
56
83
  }
57
- function readDefaultMatrix() {
84
+ export function readDefaultMatrix() {
58
85
  if (!existsSync(DEFAULT_MATRIX_PATH))
59
86
  throw new Error("omd-dsh: missing default matrix file " + DEFAULT_MATRIX_PATH + " (broken package — reinstall @carljia/omd-dsh)");
60
87
  const parsed = parseMatrix(readFileSync(DEFAULT_MATRIX_PATH, "utf8"));
@@ -62,6 +89,27 @@ function readDefaultMatrix() {
62
89
  throw new Error("omd-dsh: malformed default matrix file " + DEFAULT_MATRIX_PATH + " (broken package — reinstall @carljia/omd-dsh)");
63
90
  return parsed;
64
91
  }
92
+ /**
93
+ * Read the user's matrix file (<DSH_HOME>/omd-matrix.json) without touching
94
+ * the filesystem beyond the read: missing or malformed returns undefined and
95
+ * NEVER auto-generates — the caller decides what to fall back to (settings
96
+ * namespace resolved value / default matrix). This is the host row's
97
+ * "import CLI / legacy edits into the settings namespace" read.
98
+ */
99
+ export function readMatrixFileIfExists() {
100
+ if (!existsSync(MATRIX_PATH))
101
+ return undefined;
102
+ try {
103
+ return parseMatrix(readFileSync(MATRIX_PATH, "utf8"));
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ }
109
+ /** Structural equality over the small JSON matrix (stringify equality suffices). */
110
+ export function matrixEquals(a, b) {
111
+ return JSON.stringify(a) === JSON.stringify(b);
112
+ }
65
113
  function writeMatrixFile(path, text) {
66
114
  mkdirSync(dirname(path), { recursive: true });
67
115
  writeFileSync(path, text, "utf8");
@@ -192,8 +240,13 @@ async function locallyModified(dir, meta) {
192
240
  }
193
241
  return undefined;
194
242
  }
195
- export async function runSync(flags, log = console.log) {
196
- const matrix = loadMatrix(flags, log);
243
+ /**
244
+ * Render and materialize the presets from an ALREADY-RESOLVED matrix. This is
245
+ * the shared core between the CLI file path (`runSync`) and the settings
246
+ * namespace path (host row): the matrix comes from the caller, everything
247
+ * after `loadMatrix(...)` is here.
248
+ */
249
+ export async function runSyncWithMatrix(matrix, flags, log = console.log) {
197
250
  const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
198
251
  const sourceVersion = manifest.version;
199
252
  const presetsSourceDir = join(PACKAGE_ROOT, "presets");
@@ -343,3 +396,7 @@ export async function runSync(flags, log = console.log) {
343
396
  const summary = ["synced", "updated", "conflicts", "orphan", "removed"].map((key) => report[key].length + " " + key).join(", ");
344
397
  log("omd-dsh sync: " + summary + (flags.dryRun ? " (dry-run)" : ""));
345
398
  }
399
+ /** CLI / manual-fallback path: load the matrix from <DSH_HOME>/omd-matrix.json (generating/migrating it on first run), then render. */
400
+ export async function runSync(flags, log = console.log) {
401
+ await runSyncWithMatrix(loadMatrix(flags, log), flags, log);
402
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carljia/omd-dsh",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "OMD 理念的 DeepSeek Harness 插件:模式能力边界 + 按模式配模型 + tier 差异化子代理委派",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -24,10 +24,14 @@
24
24
  "node": ">=20"
25
25
  },
26
26
  "type": "module",
27
- "main": "lib/index.js",
28
- "types": "lib/index.d.ts",
27
+ "main": "lib/host.js",
28
+ "types": "lib/host.d.ts",
29
29
  "exports": {
30
30
  ".": {
31
+ "types": "./lib/host.d.ts",
32
+ "default": "./lib/host.js"
33
+ },
34
+ "./omd-mode": {
31
35
  "types": "./lib/index.d.ts",
32
36
  "default": "./lib/index.js"
33
37
  },
@@ -39,6 +43,7 @@
39
43
  "types": "./lib/boot.d.ts",
40
44
  "default": "./lib/boot.js"
41
45
  },
46
+ "./client": "./lib/client.js",
42
47
  "./package.json": "./package.json"
43
48
  },
44
49
  "bin": {
@@ -47,6 +52,10 @@
47
52
  "dsh": {
48
53
  "bundle": {
49
54
  "patch": "./cordis.patch.yml"
55
+ },
56
+ "client": {
57
+ "platform": "web",
58
+ "inject": []
50
59
  }
51
60
  },
52
61
  "files": [
@@ -62,15 +71,21 @@
62
71
  "@deepseek-ai/cordis": "^4.0.1",
63
72
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
64
73
  "@deepseek-ai/dsh-scope": "^0.1.1-rc.2",
74
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
65
75
  "@deepseek-ai/dsh-subagent": "^0.1.1-rc.2",
66
76
  "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2",
67
77
  "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
68
78
  "@deepseek-ai/schemastery": "^3.18.1"
69
79
  },
70
80
  "devDependencies": {
81
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
71
82
  "@deepseek-ai/schemastery": "^3.18.1",
72
83
  "@types/node": "^24.0.0",
84
+ "@types/react": "^18.3.1",
85
+ "@types/react-dom": "^18.3.0",
73
86
  "esbuild": "^0.28.2",
87
+ "react": "^18.2.0",
88
+ "react-dom": "^18.2.0",
74
89
  "typescript": "^5.6.0",
75
90
  "vitest": "^3.0.0"
76
91
  },