dsh-dock 0.11.3 → 0.11.6

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/src/client.jsx CHANGED
@@ -30,7 +30,7 @@ import { feature as fGames } from "../features/games/view.jsx";
30
30
  import { feature as fMobileRelay } from "../features/mobile-relay/view.jsx";
31
31
 
32
32
  const name = "dsh-dock";
33
- const DOCK_VERSION = "0.11.3";
33
+ const DOCK_VERSION = "0.11.6";
34
34
 
35
35
  // ---- 内置功能注册表:新功能 = features/<id>/ 加模块 + 这里 import 一行 ----
36
36
  const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation, fNotify, fRunState, fGames, fMobileRelay];
@@ -238,6 +238,11 @@ function fullDockCss() {
238
238
  // 样式全局注入一次(入口按钮与右下角浮层挂在设置页之外,不能依赖页内 <style>)。
239
239
  // 这里用 SHELL_CSS 唯一命名,并做防御:注入永不抛错、失败只降级不打断启动;
240
240
  // 外部功能注册携带样式时可重算更新(外部样式后到)。
241
+ //
242
+ // ⚠️ 必须同时打 data-plugin="dsh-dock"(官方 claimStyles/removeOwnedStyles 归属协议):
243
+ // 只有 data-plugin-css 的无主 <style> 会被任意后 materialize 的插件 claim 走,
244
+ // 对方卸载时 removeOwnedStyles 会连带删掉功能坞样式——表现为「开关别的插件后样式坏了、
245
+ // 要手动刷新才正常」。见 packages/client/modules claimStyles。
241
246
  function ensureCss() {
242
247
  if (typeof document === "undefined") return;
243
248
  cancelInitialCssSchedule();
@@ -247,19 +252,21 @@ function ensureCss() {
247
252
  dockCssTag = document.querySelector('style[data-plugin-css="dsh-dock"]');
248
253
  } else {
249
254
  dockCssTag = document.createElement("style");
250
- dockCssTag.dataset.pluginCss = "dsh-dock";
251
255
  document.head.appendChild(dockCssTag);
252
256
  }
253
257
  }
258
+ // 归属与标识:缺 data-plugin 的旧标签/被 claim 走的标签一律改回本插件
259
+ dockCssTag.dataset.plugin = "dsh-dock";
260
+ dockCssTag.dataset.pluginCss = "dsh-dock";
254
261
  dockCssTag.textContent = fullDockCss();
255
262
  } catch (e) {
256
263
  console.error("[dsh-dock] ensureCss failed:", e && e.message ? e.message : String(e));
257
264
  }
258
265
  }
259
- // 刷新页时宿主内容优先绘制:功能坞样式很大,首帧同步解析会让整页短暂停顿。
260
- // 首次注入延后到浏览器空闲期;之后的外部模块注册仍走 ensureCss() 立即更新。
266
+ // 首次注入延后到浏览器空闲期:功能坞样式很大,首帧同步解析会让整页短暂停顿。
267
+ // 之后的外部模块注册仍走 ensureCss() 立即更新;apply() 另有同步一击(见下)。
261
268
  function scheduleInitialCss() {
262
- if (typeof document === "undefined" || initialCssSchedule || (dockCssTag && dockCssTag.isConnected)) return;
269
+ if (typeof document === "undefined" || initialCssSchedule || (dockCssTag && dockCssTag.isConnected && dockCssTag.dataset.plugin === "dsh-dock")) return;
263
270
  const run = () => {
264
271
  initialCssSchedule = null;
265
272
  ensureCss();
@@ -649,7 +656,10 @@ const ctxRef = { current: null };
649
656
 
650
657
  export function apply(ctx) {
651
658
  ctxRef.current = ctx;
652
- scheduleInitialCss();
659
+ // 同步注入归属完整的样式:插件开关/HMR 重进 apply 时若只靠 idle,
660
+ // 窗口期内其它插件 materialize 会 claim 走无主标签(或标签已被对方删掉)。
661
+ // 首屏成本可接受(textContent 写入),换「开合插件样式不坏」。
662
+ ensureCss();
653
663
  initFeatureState(BUILTIN_FEATURES.concat(PLANNED_FEATURES));
654
664
  const slots = ctx.get("slots");
655
665
  if (slots === undefined) return;
package/src/host-core.js CHANGED
@@ -1,27 +1,36 @@
1
1
  // dsh-dock · 宿主共享内核:各功能模块(features/*/host.js)共用的常量与工具。
2
2
  // 放在 src/ 而非 features/:不属于任何单个功能;提取功能模块独立成包时按需随包复制。
3
+ import { readFileSync, existsSync } from 'node:fs'
4
+ import { join } from 'node:path'
5
+ import { homedir } from 'node:os'
3
6
  import z from '@deepseek-ai/schemastery'
4
7
 
5
- /** dsh-dock 自有 settings 命名空间(插件级配置,如图片理解代理)。 */
8
+ /** dsh-dock 自有 settings 命名空间 = profile 条目 id(cordis.patch.yml 的 id)。 */
6
9
  export const DOCK_NS = 'dsh-dock'
7
10
 
8
11
  /** 自有命名空间的 schema(功能开关 + 图片理解代理 + 任务动画/任务通知配置 + 远程访问账号)。
9
- * 运行状态模块只读共享追踪、不写配置,因此没有自己的段。 */
12
+ * 运行状态模块只读共享追踪、不写配置,因此没有自己的段。
13
+ *
14
+ * dsh ≥ 0.1.7-alpha.1:settings 服务改为 SettingsForms——
15
+ * ① 插件必须导出同名 Config(否则 mutate 报 No configurable plugin entry);
16
+ * ② ns 是 profile 条目 id(此处 DOCK_NS=cordis.patch.yml 的 id);
17
+ * ③ 活编辑路径必须落在 .volatile() 字段上,否则 write 抛 not volatile。
18
+ * 每个顶层段整体 volatile:段内任意 path 可被面板 mutate,无需逐字段标记。 */
10
19
  export const DockConfig = z.object({
11
20
  // 宿主侧功能开关(id -> boolean):客户端面板 toggle 即时同步过来,
12
21
  // 重启 dsh 后按此表决定宿主半部 setup 哪些功能(路由注册、事件订阅等)。
13
- features: z.dict(z.boolean()).default({}),
22
+ features: z.dict(z.boolean()).default({}).volatile(),
14
23
  remoteAuth: z.object({
15
24
  /** 远程访问登录账号(明文用户名;密码只存加盐哈希)。 */
16
25
  username: z.string().default(''),
17
26
  passwordHash: z.string().default(''),
18
27
  salt: z.string().default(''),
19
- }).default({}),
28
+ }).default({}).volatile(),
20
29
  visionProxy: z.object({
21
30
  enabled: z.boolean().default(false),
22
31
  provider: z.string().default(''),
23
32
  model: z.string().default(''),
24
- }).default({}),
33
+ }).default({}).volatile(),
25
34
  // 【用量记录】模块的单价配置:官网抓取的刊例价常与实际计费不符(第三方/中转、
26
35
  // 折扣、自建端点等),这里保存用户自填单价并优先用于费用估算。
27
36
  // 详见 features/tokenlog/host.js 的 resolvePricing(优先级:自定义 > 官网 > 内置 > 兜底)。
@@ -59,7 +68,7 @@ export const DockConfig = z.object({
59
68
  cacheRead: z.number().default(0.43),
60
69
  cacheWrite: z.number().default(4.32),
61
70
  }).default({}),
62
- }).default({}),
71
+ }).default({}).volatile(),
63
72
  // 【任务动画】模块:只放动画本身(动效开关/模式/桌面伙伴大小)。
64
73
  // 通知相关的字段已全部迁往下面的 notify 段(见 migrateNotifyConfig);
65
74
  // 旧值不在 schema 里也会被 schemastery 原样保留,迁移读得到。
@@ -68,7 +77,7 @@ export const DockConfig = z.object({
68
77
  effectMode: z.string().default('flow'),
69
78
  // 桌面伙伴场景缩放(0.85~2.2;浮层右下角也可直接拖动缩放)
70
79
  robotScale: z.number().default(1.35),
71
- }).default({}),
80
+ }).default({}).volatile(),
72
81
  // 【任务通知】模块:页内卡片 / 提示音 / 浏览器系统通知 / 群机器人推送。
73
82
  // 模块自身的启停由 features.notify 开关负责(独立菜单项),这里只放通知行为配置。
74
83
  notify: z.object({
@@ -92,9 +101,99 @@ export const DockConfig = z.object({
92
101
  feishuWebhook: z.string().default(''),
93
102
  // 一次性迁移标记:animation 段的旧通知配置已搬到本段(搬完置 true,避免每次启动重复写)
94
103
  migratedFromAnimation: z.boolean().default(false),
95
- }).default({}),
104
+ }).default({}).volatile(),
96
105
  })
97
106
 
107
+ // ── 本地配置镜像(替代已移除的 settings.get) ─────────────────────────────
108
+ // apply(ctx, config) 绑定带 volatile 引用的 Config;每次操作前 plainify 一次取快照。
109
+ // settings.mutate 成功后由调用方/ volatile-update 刷新。测试桩可用 seedDockConfig 注入。
110
+
111
+ /** @type {null | Record<string, unknown>} */
112
+ let dockConfigBound = null
113
+
114
+ /** 是否为 cosmokit Volatile 引用(跨副本用 Symbol 协议识别,不依赖包导入)。 */
115
+ function isVolatileRef(value) {
116
+ return typeof value === 'object' && value !== null && Symbol.for('cosmokit.volatile.write') in value
117
+ }
118
+
119
+ /** 递归解开 volatile 引用 → 可 JSON 化的普通对象。 */
120
+ export function plainifyDockConfig(value) {
121
+ if (isVolatileRef(value)) return plainifyDockConfig(value.get())
122
+ if (Array.isArray(value)) return value.map(plainifyDockConfig)
123
+ if (value !== null && typeof value === 'object') {
124
+ const out = {}
125
+ for (const [k, v] of Object.entries(value)) out[k] = plainifyDockConfig(v)
126
+ return out
127
+ }
128
+ return value
129
+ }
130
+
131
+ /** 由 index.js apply 挂载/刷新:持有 Config 引用并在 loader/volatile-update 后重读。 */
132
+ export function bindDockConfig(config) {
133
+ dockConfigBound = config === undefined || config === null ? null : plainifyDockConfig(config)
134
+ return dockConfigBound
135
+ }
136
+
137
+ /** 测试/本地桩:直接写入镜像(绕过宿主 settings)。 */
138
+ export function seedDockConfig(value) {
139
+ dockConfigBound = value === undefined || value === null ? null : plainifyDockConfig(value)
140
+ }
141
+
142
+ /**
143
+ * 读自有命名空间根对象。优先级:
144
+ * 1) apply 绑定的 Config 镜像(新宿主活值)
145
+ * 2) 测试/旧宿主的 settings.get(若仍存在)
146
+ * 3) settings.describe 里 ns=DOCK_NS 的 value(SettingsForms 读路径)
147
+ * 4) null(调用方回退默认)
148
+ */
149
+ export function readDockRoot(ctx) {
150
+ if (dockConfigBound) return dockConfigBound
151
+ try {
152
+ const settings = ctx && typeof ctx.get === 'function' ? ctx.get('settings') : null
153
+ if (settings) {
154
+ if (typeof settings.get === 'function') {
155
+ const v = settings.get(DOCK_NS)
156
+ if (v && typeof v === 'object') return v
157
+ }
158
+ if (typeof settings.describe === 'function') {
159
+ const views = settings.describe({ redactSecrets: false }) || []
160
+ const hit = views.find((d) => d && d.ns === DOCK_NS && d.value && typeof d.value === 'object')
161
+ if (hit) return hit.value
162
+ }
163
+ }
164
+ } catch { /* settings 未挂载 */ }
165
+ return null
166
+ }
167
+
168
+ /**
169
+ * 写自有命名空间一段(走 SettingsForms.mutate;成功后同步本地镜像)。
170
+ * @param {object} ctx Cordis context
171
+ * @param {string[]} path 段路径(如 ['animation'])
172
+ * @param {unknown} value 整段值
173
+ * @param {number} [expectedRevision] describe 的 revision(乐观锁,可选)
174
+ */
175
+ export async function mutateDockSection(ctx, path, value, expectedRevision) {
176
+ const settings = ctx && typeof ctx.get === 'function' ? ctx.get('settings') : null
177
+ if (!settings || typeof settings.mutate !== 'function') {
178
+ throw new Error('settings 服务不可用,配置无法持久化')
179
+ }
180
+ await settings.mutate(DOCK_NS, [{ op: 'set', path, value }], expectedRevision)
181
+ if (dockConfigBound) {
182
+ const next = { ...dockConfigBound }
183
+ let node = next
184
+ for (let i = 0; i < path.length - 1; i++) {
185
+ const key = path[i]
186
+ const child = node[key] && typeof node[key] === 'object' && !Array.isArray(node[key]) ? { ...node[key] } : {}
187
+ node[key] = child
188
+ node = child
189
+ }
190
+ if (path.length > 0) next[path[path.length - 1]] = value
191
+ else Object.assign(next, value)
192
+ dockConfigBound = next
193
+ }
194
+ return value
195
+ }
196
+
98
197
  /** 通知配置字段清单(schema、迁移、客户端默认值三处共用同一份键名)。 */
99
198
  export const NOTIFY_FIELDS = [
100
199
  'notifyOnComplete', 'notifyOnError', 'notifyOnConfirm', 'notifyStayMs',
@@ -112,9 +211,7 @@ export const NOTIFY_FIELDS = [
112
211
  */
113
212
  export async function migrateNotifyConfig(ctx) {
114
213
  try {
115
- const settings = ctx.get('settings')
116
- if (!settings || typeof settings.get !== 'function' || typeof settings.mutate !== 'function') return
117
- const root = settings.get(DOCK_NS)
214
+ const root = readDockRoot(ctx)
118
215
  const notify = root && typeof root === 'object' && root.notify && typeof root.notify === 'object' ? root.notify : null
119
216
  if (notify && notify.migratedFromAnimation === true) return
120
217
  const animation = root && typeof root === 'object' && root.animation && typeof root.animation === 'object' ? root.animation : null
@@ -127,7 +224,7 @@ export async function migrateNotifyConfig(ctx) {
127
224
  // 避免 settings 命名空间尚未生效时误把「空迁移」当完成(那会丢掉旧配置)。
128
225
  if (copied === 0) return
129
226
  const next = Object.assign({}, notify || {}, legacy, { migratedFromAnimation: true })
130
- await settings.mutate(DOCK_NS, [{ op: 'set', path: ['notify'], value: next }])
227
+ await mutateDockSection(ctx, ['notify'], next)
131
228
  console.log('[dsh-dock] notify config migrated from animation section; fields copied:', copied)
132
229
  } catch (e) {
133
230
  console.warn('[dsh-dock] notify config migration skipped:', (e && e.message) || String(e))
@@ -144,20 +241,55 @@ export async function migrateNotifyConfig(ctx) {
144
241
  */
145
242
  export async function migrateModelsFeatureId(ctx) {
146
243
  try {
147
- const settings = ctx.get('settings')
148
- if (!settings || typeof settings.get !== 'function' || typeof settings.mutate !== 'function') return
149
- const root = settings.get(DOCK_NS)
244
+ const root = readDockRoot(ctx)
150
245
  const features = root && typeof root === 'object' && root.features && typeof root.features === 'object' ? root.features : null
151
246
  if (!features || typeof features.models !== 'boolean' || typeof features.modelconfig === 'boolean') return
152
247
  const next = Object.assign({}, features, { modelconfig: features.models })
153
248
  delete next.models
154
- await settings.mutate(DOCK_NS, [{ op: 'set', path: ['features'], value: next }])
249
+ await mutateDockSection(ctx, ['features'], next)
155
250
  console.log('[dsh-dock] feature id migrated: features.models -> features.modelconfig =', features.models)
156
251
  } catch (e) {
157
252
  console.warn('[dsh-dock] models feature id migration skipped:', (e && e.message) || String(e))
158
253
  }
159
254
  }
160
255
 
256
+ /**
257
+ * 一次性迁移:旧 settings.yaml 已被宿主改名为 settings.yaml.imported;
258
+ * 当时 dsh-dock 尚未导出 Config,importLegacyDocument 对本段 update 会失败,
259
+ * 功能开关表停在 imported 文件里。配置可写后把 features 补写进 profile。
260
+ */
261
+ export async function migrateImportedFeatures(ctx) {
262
+ try {
263
+ const root = readDockRoot(ctx)
264
+ const features = root && typeof root === 'object' && root.features && typeof root.features === 'object' ? root.features : null
265
+ if (features && Object.keys(features).length > 0) return
266
+ const home = process.env.DSH_HOME || join(homedir(), '.dsh')
267
+ const file = join(home, 'settings.yaml.imported')
268
+ if (!existsSync(file)) return
269
+ const text = readFileSync(file, 'utf8')
270
+ // 极简 YAML:仅取 dsh-dock.features 下的 `key: true|false` 行(避免引入完整解析器)
271
+ const lines = text.split(/\r?\n/)
272
+ let inDock = false
273
+ let inFeatures = false
274
+ const imported = {}
275
+ for (const line of lines) {
276
+ if (/^dsh-dock:\s*$/.test(line)) { inDock = true; inFeatures = false; continue }
277
+ if (inDock && /^\S/.test(line) && !/^dsh-dock:/.test(line)) { inDock = false; inFeatures = false }
278
+ if (inDock && /^\s{2}features:\s*$/.test(line)) { inFeatures = true; continue }
279
+ if (inDock && inFeatures && /^\s{2}\S/.test(line)) { inFeatures = false }
280
+ if (inDock && inFeatures) {
281
+ const m = /^\s{4}([A-Za-z0-9_-]+):\s*(true|false)\s*$/.exec(line)
282
+ if (m) imported[m[1]] = m[2] === 'true'
283
+ }
284
+ }
285
+ if (Object.keys(imported).length === 0) return
286
+ await mutateDockSection(ctx, ['features'], imported)
287
+ console.log('[dsh-dock] features imported from settings.yaml.imported:', imported)
288
+ } catch (e) {
289
+ console.warn('[dsh-dock] imported features migration skipped:', (e && e.message) || String(e))
290
+ }
291
+ }
292
+
161
293
  /** 任务动画 effectMode 合法值(客户端动画模式)。 */
162
294
  export const ANIMATION_MODES = [
163
295
  'flow', 'breathe', 'ring', 'orbit', 'robot', 'matrix', 'stars', 'aurora', 'space',