dsh-deepseek-style-theme 2.0.89 → 2.1.91

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.
Files changed (4) hide show
  1. package/README.md +39 -48
  2. package/lib/client.js +808 -857
  3. package/lib/index.js +143 -390
  4. package/package.json +5 -3
package/lib/index.js CHANGED
@@ -6,9 +6,9 @@
6
6
  // so this plugin owns the whole "open workspace" gesture instead. The same
7
7
  // channel carries the delivered-file gestures the browser half's card menu
8
8
  // needs: open with the default application, and reveal in the file manager.
9
- // It also owns the DSTT settings section and, once per activation, aligns the
10
- // official DeepSeek route's advisory model catalog with the ids the endpoint
11
- // actually advertises (see the catalog-sync block below).
9
+ // It also owns the DSTT settings section and the desktop-wallpaper endpoint.
10
+ // 模型目录同步(1.40.0 起那套"每次启动向端点问一次模型列表、按需对齐
11
+ // llm-deepseek.models")已在 2.1.90 整体删除:官方自带模型目录,选择器内容以官方为准。
12
12
  //
13
13
  // Wire contract: every return is an RpcResult<T> per rpc.schema.js. Error
14
14
  // codes must come from the closed enum (bad-request / internal) — an
@@ -63,11 +63,12 @@ const OPEN_TIMEOUT_MS = 10000;
63
63
  * - always-green → always green, no peak distinction;
64
64
  * - always-blue → always blue, no peak distinction.
65
65
  */
66
- // `settingsNamespace` from @deepseek-ai/dsh-settings was deleted in dsh
67
- // 0.1.2-alpha.1 (the settings service itself is unchanged - register /
68
- // describe / mutate below still take the namespace string), so this is now
69
- // a plain literal. Keep it in sync with the client half's DSTT_NS.
70
- const DSTT_SETTINGS_NS = "deepseek-style-theme";
66
+ // 0.2.0 起,设置命名空间不再是插件随手注册的字符串,而是**profile 里那条插件条目的
67
+ // id**(见下面 dsttSettingsNs)。历史上有过三个名字,按"当前产品优先、旧命名兜底"
68
+ // 排列:`ui-skin-deepseek-style` 是本插件 cordis.patch.yml 里的条目 id,也是两端
69
+ // profile 实际用的 id;后两个是旧版运行时注册用的名字,留着只是为了老 profile 不至于
70
+ // 直接失去持久化。解析失败时返回 null,调用方一律回落到默认值,绝不抛。
71
+ const DSTT_SETTINGS_NS_CANDIDATES = ["ui-skin-deepseek-style", "dsh-deepseek-style-theme", "deepseek-style-theme"];
71
72
  const DSTT_MODES = ["peakvalley-redblue", "peakvalley-redgreen", "always-green", "always-blue"];
72
73
  const DSTT_MODE_DEFAULT = "peakvalley-redblue";
73
74
  // 1.37.x persisted `auto` / `blue` / `green`. The schema still accepts them —
@@ -75,15 +76,6 @@ const DSTT_MODE_DEFAULT = "peakvalley-redblue";
75
76
  // persistence on upgrade — and apply() migrates them to the closest four-mode
76
77
  // value right after registration.
77
78
  const DSTT_LEGACY_MODES = { auto: "peakvalley-redblue", blue: "always-blue", green: "always-green" };
78
- /**
79
- * Catalog-sync policies, most automatic first:
80
- * - auto → align the catalog with the endpoint, but only while every
81
- * advertised id is one this plugin can describe (see syncDeepseekCatalog);
82
- * - add → append-only: adopt newly advertised ids, remove nothing ever;
83
- * - off → probe and report drift, write nothing.
84
- */
85
- const CATALOG_SYNC_POLICIES = ["auto", "add", "off"];
86
- const CATALOG_SYNC_DEFAULT = "auto";
87
79
  // Glass recipes. `liquid` is the Apple-style lens (edge refraction, a flowing
88
80
  // edge highlight, high translucency); `frosted` is the white frosted pane with
89
81
  // the brand-tinted shifting border this theme shipped before, kept byte for byte
@@ -134,21 +126,16 @@ const DsttSettingsSchema = z === null ? null : z.object({
134
126
  z.const("auto"),
135
127
  z.const("blue"),
136
128
  z.const("green")
137
- ]).default(DSTT_MODE_DEFAULT),
138
- catalogSync: z.union([
139
- z.const("auto"),
140
- z.const("add"),
141
- z.const("off")
142
- ]).default(CATALOG_SYNC_DEFAULT),
129
+ ]).default(DSTT_MODE_DEFAULT).volatile(),
143
130
  // The fluid background's pointer brush writes a velocity wake into the flow
144
131
  // field wherever the cursor goes. It is both the most expensive part of the
145
132
  // simulation and the most intrusive visually, so it ships OFF; this setting
146
133
  // exists only to turn it back on.
147
- fluidBrush: z.boolean().default(false),
134
+ fluidBrush: z.boolean().default(false).volatile(),
148
135
  glassStyle: z.union([
149
136
  z.const("liquid"),
150
137
  z.const("frosted")
151
- ]).default(GLASS_STYLE_DEFAULT),
138
+ ]).default(GLASS_STYLE_DEFAULT).volatile(),
152
139
  // Both ids are listed on purpose, and dropping either one breaks an upgrade:
153
140
  // `wide` is what the write path accepts today, and `origin` is what a settings
154
141
  // file written before the rename holds -- register() validates the STORED
@@ -160,19 +147,19 @@ const DsttSettingsSchema = z === null ? null : z.object({
160
147
  z.const("wide"),
161
148
  z.const("origin"),
162
149
  z.const("off")
163
- ]).default(COMPOSER_REFRACTION_DEFAULT),
150
+ ]).default(COMPOSER_REFRACTION_DEFAULT).volatile(),
164
151
  // The `blur()` term in every glass surface's backdrop chain. Turning it off
165
152
  // keeps the translucency, the sheen and the edge refraction and drops only
166
153
  // the frosting -- implemented as four variables driven to 0px, so nothing
167
154
  // else about the recipes moves.
168
- backdropBlur: z.boolean().default(true),
155
+ backdropBlur: z.boolean().default(true).volatile(),
169
156
  // The animated background layer itself: the WebGL2 fluid, or the 2D particle
170
157
  // field on engines without it. Off leaves the static themed background (the
171
158
  // per-colour gradients, or the user's own 自选背景) and changes nothing else.
172
159
  // It exists because some integrated GPUs render the simulation wrong -- an
173
160
  // Intel iGPU under ANGLE/D3D11 turned the noise into drifting squares even
174
161
  // after the mediump fix -- and for those machines a switch beats a fight.
175
- ambientBackground: z.boolean().default(true),
162
+ ambientBackground: z.boolean().default(true).volatile(),
176
163
  // Every id the write path accepts is listed here for the reason spelled out
177
164
  // on composerRefraction above: register() validates the stored document, so
178
165
  // an id the schema does not know throws there and takes DSTT persistence
@@ -182,8 +169,8 @@ const DsttSettingsSchema = z === null ? null : z.object({
182
169
  z.const("white"),
183
170
  z.const("custom"),
184
171
  z.const("bold")
185
- ]).default(BACKGROUND_MODE_DEFAULT),
186
- customBackground: z.string().default("")
172
+ ]).default(BACKGROUND_MODE_DEFAULT).volatile(),
173
+ customBackground: z.string().default("").volatile()
187
174
  });
188
175
 
189
176
  /** Map a legacy 1.37.x mode id onto a four-mode id, or null when nothing to do. */
@@ -198,32 +185,6 @@ function powershellLiteral(path) {
198
185
  return `'${path.replace(/'/g, "''")}'`;
199
186
  }
200
187
 
201
- /**
202
- * Open one path with Shell.Application, then focus the matching Explorer
203
- * window. The open step is strict: any COM failure exits the script non-zero
204
- * so the caller reports a real failure instead of a false success. Focusing
205
- * stays best-effort — a directory that opened but could not be focused is
206
- * still a successful open.
207
- */
208
- function openExplorerForeground(path) {
209
- return new Promise((resolve) => {
210
- const script = [
211
- "$ErrorActionPreference = 'Stop'",
212
- `$p = ${powershellLiteral(path)}`,
213
- "$sh = New-Object -ComObject Shell.Application",
214
- "try { $sh.Open($p) } catch { exit 1 }",
215
- "Start-Sleep -Milliseconds 500",
216
- "$f = $false",
217
- "foreach ($w in @($sh.Windows())) {",
218
- " try { if ($w.Document.Folder.Self.Path -ieq $p) { $null = $w.Focus(); $f = $true; break } } catch {}",
219
- "}",
220
- "if (-not $f) { $null = (New-Object -ComObject WScript.Shell).AppActivate((Split-Path $p -Leaf)) }"
221
- ].join("; ");
222
- execFile("powershell.exe", ["-NoProfile", "-Command", script], { windowsHide: true, timeout: OPEN_TIMEOUT_MS }, (error) => {
223
- resolve(error === null || error === undefined);
224
- });
225
- });
226
- }
227
188
 
228
189
  /**
229
190
  * Run a simple command and resolve with whether it exited cleanly.
@@ -243,23 +204,10 @@ function runOpener(command, args, lenient = false) {
243
204
  });
244
205
  }
245
206
 
246
- /** Open a directory in the platform's default file manager. */
247
- function openPathGeneric(path) {
248
- switch (process.platform) {
249
- case "win32":
250
- return openExplorerForeground(path);
251
- case "darwin":
252
- return runOpener("open", [path]);
253
- case "linux":
254
- return runOpener("xdg-open", [path]);
255
- default:
256
- return Promise.resolve(false);
257
- }
258
- }
259
-
260
207
  /**
261
- * Open one file with the OS default application. Deliberately not
262
- * {@link openPathGeneric}: on Windows `Shell.Application.Open` on a file is the
208
+ * Open one file with the OS default application. Not the same gesture as
209
+ * opening a directory (that helper is gone with 2.1.90's "open workspace"
210
+ * removal): on Windows `Shell.Application.Open` on a file is the
263
211
  * shell's own gesture, while `Start-Process` hands the path to the registered
264
212
  * handler, which is what "用默认应用打开" means.
265
213
  */
@@ -300,298 +248,55 @@ function failure(code, message, details) {
300
248
  return { ok: false, error: { code, message, details } };
301
249
  }
302
250
 
303
- /**
304
- * Advisory model-catalog sync for the official DeepSeek route.
305
- *
306
- * `dsh-llm-deepseek` deliberately never probes its gateway — `listModels()`
307
- * returns the declared catalog and the README states the defaults are published
308
- * "without probing gateway availability" — so the composer's model selector can
309
- * drift from what the endpoint actually serves, and nothing in the harness would
310
- * ever notice. On activation this plugin therefore asks the endpoint which ids
311
- * it advertises (the same call the Models page makes: GET {baseURL}/models) and
312
- * aligns `llm-deepseek.models` with the answer.
313
- *
314
- * Two properties keep that from being a liability:
315
- *
316
- * 1. It never invents capability metadata. The endpoint reports ids only, so
317
- * it cannot supply `inputModalities`, `systemPromptUpdate` or a context
318
- * window. Entries that already exist are preserved verbatim and new ids are
319
- * filled from KNOWN_CATALOG_ENTRIES; an id this plugin cannot describe is
320
- * reported rather than guessed, because a guessed entry silently downgrades
321
- * a vision model to text-only.
322
- * 2. It only rewrites the list wholesale while every advertised id is
323
- * describable. An official route advertises a handful of DeepSeek ids; an
324
- * aggregating gateway in front of `baseURL` advertises its entire
325
- * catalogue, and "the endpoint is authoritative" would then replace a
326
- * curated two-entry catalog with hundreds of unrelated rows. A
327
- * foreign-looking answer degrades to append-only plus a warning.
328
- *
329
- * Every failure mode (no settings, no credential, offline gateway, renamed
330
- * service, concurrent settings edit) is a no-op for the theme — a skin must
331
- * never be able to break the model catalog — and it only writes on real drift.
332
- * `catalogSync: "off"` probes and reports without ever writing.
333
- */
334
- const LLM_DEEPSEEK_NS = "llm-deepseek";
335
- /** Public endpoint; a deployment may point elsewhere through `$DEEPSEEK_BASE_URL`. */
336
- const PUBLIC_DEEPSEEK_BASE_URL = "https://api.deepseek.com";
337
- const DEEPSEEK_BASE_URL_ENV = "DEEPSEEK_BASE_URL";
338
- const DEFAULT_API_KEY_ENV = "DEEPSEEK_API_KEY";
339
- const CATALOG_SYNC_TIMEOUT_MS = 5000;
340
- /** Retry offsets (ms) while a boot-order prerequisite is still missing. */
341
- const CATALOG_SYNC_RETRIES = [0, 2000, 6000, 15000];
342
- /**
343
- * Outcomes worth retrying: activation order is not ours to choose, so the
344
- * provider may not have registered `llm-deepseek` yet and the credentials
345
- * service may not be reachable at the instant this plugin activates. A failed
346
- * or answered probe (in-sync / updated / endpoint-unreachable) is final.
347
- */
348
- const CATALOG_SYNC_RETRYABLE = ["provider-absent", "no-credentials"];
349
-
350
- /**
351
- * Capability metadata for ids this plugin knows by name. Only consulted for ids
352
- * the endpoint advertises that the stored catalog does not describe yet; an
353
- * existing entry is never overwritten, so a user's own edits survive.
354
- */
355
- const KNOWN_CATALOG_ENTRIES = {
356
- "deepseek-flash": {
357
- name: "DeepSeek-V41-Flash",
358
- contextWindow: 1000000,
359
- inputModalities: ["text", "image"],
360
- imagePixelBudget: 640000,
361
- imageMaxBytes: 1048576,
362
- systemPromptUpdate: "in-history"
363
- },
364
- "deepseek-v4-pro": {
365
- name: "DeepSeek-V4-Pro",
366
- contextWindow: 1000000,
367
- inputModalities: ["text"]
368
- }
369
- };
370
-
371
- /** One registered namespace's descriptor (resolved value plus revision), or null. */
372
- function settingsDescriptor(ctx, ns) {
251
+ /** Every settings namespace the service currently reports, or null. */
252
+ function settingsNamespaces(ctx) {
373
253
  const settings = ctx.get("settings");
374
254
  if (settings === undefined || settings === null || typeof settings.describe !== "function") return null;
375
- let descriptors;
376
255
  try {
377
- descriptors = settings.describe({ redactSecrets: true });
256
+ const descriptors = settings.describe({ redactSecrets: true });
257
+ return Array.isArray(descriptors) ? descriptors : null;
378
258
  } catch (error) {
379
259
  return null;
380
260
  }
381
- if (!Array.isArray(descriptors)) return null;
382
- const descriptor = descriptors.find((candidate) => String(candidate.ns) === ns);
383
- if (descriptor === undefined || descriptor === null) return null;
384
- const value = descriptor.value;
385
- if (value === null || typeof value !== "object") return null;
386
- return { value, revision: descriptor.revision };
387
- }
388
-
389
- /** The resolved `llm-deepseek` settings section, or null while it is absent. */
390
- function llmDeepseekSection(ctx) {
391
- return settingsDescriptor(ctx, LLM_DEEPSEEK_NS);
392
- }
393
-
394
- /** The configured catalog-sync policy, falling back to the schema default. */
395
- function catalogSyncPolicy(ctx) {
396
- const descriptor = settingsDescriptor(ctx, DSTT_SETTINGS_NS);
397
- const policy = descriptor === null ? undefined : descriptor.value.catalogSync;
398
- return CATALOG_SYNC_POLICIES.includes(policy) ? policy : CATALOG_SYNC_DEFAULT;
399
- }
400
-
401
- /**
402
- * The endpoint the official adapter itself would dial, resolved in that
403
- * adapter's own order (`dsh-llm-deepseek` resolves `config.baseURL ??
404
- * launchEnvironment.get("DEEPSEEK_BASE_URL") ?? "https://api.deepseek.com"`).
405
- * Falling straight back to the public host would post the resolved credential to
406
- * api.deepseek.com on a deployment whose gateway lives behind that variable —
407
- * and that credential may be a gateway token, not a DeepSeek key. The service's
408
- * own fallback is a raw `process.env` snapshot, so consulting `process.env` only
409
- * while the service is absent matches the adapter exactly instead of widening
410
- * trust.
411
- */
412
- function resolveBaseURL(ctx, section) {
413
- if (typeof section.baseURL === "string" && section.baseURL !== "") return section.baseURL;
414
- const environment = ctx.get("launchEnvironment");
415
- if (environment === undefined || environment === null || typeof environment.get !== "function") {
416
- const ambient = process.env[DEEPSEEK_BASE_URL_ENV];
417
- return typeof ambient === "string" && ambient !== "" ? ambient : PUBLIC_DEEPSEEK_BASE_URL;
418
- }
419
- try {
420
- const hit = environment.get(DEEPSEEK_BASE_URL_ENV);
421
- const value = hit === undefined || hit === null ? undefined : hit.value;
422
- if (typeof value === "string" && value !== "") return value;
423
- } catch (error) {
424
- // Fall through to the public endpoint, exactly like the adapter does.
425
- }
426
- return PUBLIC_DEEPSEEK_BASE_URL;
427
261
  }
428
262
 
429
263
  /**
430
- * Resolve the provider API key the same way `dsh-llm-deepseek` does: through the
431
- * credentials service under the configured `apiKeyEnv`, falling back to the
432
- * ambient environment of the launching process.
433
- */
434
- async function resolveProviderKey(ctx, apiKeyEnv) {
435
- const ref = typeof apiKeyEnv === "string" && apiKeyEnv !== "" ? apiKeyEnv : DEFAULT_API_KEY_ENV;
436
- const credentials = ctx.get("credentials");
437
- if (credentials !== undefined && credentials !== null && typeof credentials.resolve === "function") {
438
- try {
439
- const hit = await credentials.resolve(ref);
440
- if (hit !== undefined && hit !== null && typeof hit.value === "string" && hit.value !== "") return hit.value;
441
- } catch (error) {
442
- // Fall through to the ambient environment.
443
- }
444
- }
445
- const ambient = typeof process !== "undefined" && process.env !== undefined ? process.env[ref] : undefined;
446
- return typeof ambient === "string" && ambient !== "" ? ambient : null;
447
- }
448
-
449
- /** Ask the endpoint which model ids it advertises, in endpoint order. */
450
- async function fetchEndpointModelIds(baseURL, key) {
451
- if (typeof fetch !== "function") return null;
452
- const url = String(baseURL).replace(/\/+$/, "") + "/models";
453
- const signal = typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function"
454
- ? AbortSignal.timeout(CATALOG_SYNC_TIMEOUT_MS)
455
- : undefined;
456
- const response = await fetch(url, {
457
- headers: { authorization: "Bearer " + key, accept: "application/json" },
458
- ...(signal === undefined ? {} : { signal })
459
- });
460
- if (!response.ok) throw new Error("endpoint answered " + String(response.status));
461
- const body = await response.json();
462
- const rows = body !== null && typeof body === "object" && Array.isArray(body.data) ? body.data : [];
463
- const ids = [];
464
- for (const row of rows) {
465
- const id = row !== null && typeof row === "object" && typeof row.id === "string" ? row.id.trim() : "";
466
- if (id !== "" && !ids.includes(id)) ids.push(id);
467
- }
468
- return ids;
469
- }
470
-
471
- /** One catalog entry for an id the stored catalog does not describe yet. */
472
- function catalogEntryFor(id) {
473
- const known = Object.prototype.hasOwnProperty.call(KNOWN_CATALOG_ENTRIES, id) ? KNOWN_CATALOG_ENTRIES[id] : undefined;
474
- return known === undefined ? { id, name: id, inputModalities: ["text"] } : { id, ...known };
475
- }
476
-
477
- /** Whether this plugin can describe an id without inventing capability metadata. */
478
- function isDescribable(id) {
479
- return Object.prototype.hasOwnProperty.call(KNOWN_CATALOG_ENTRIES, id);
480
- }
481
-
482
- /** The ids of a catalog array, in order, ignoring malformed entries. */
483
- function catalogIds(entries) {
484
- return entries
485
- .map((entry) => (entry !== null && typeof entry === "object" && typeof entry.id === "string" ? entry.id : ""))
486
- .filter((id) => id !== "");
487
- }
488
-
489
- /**
490
- * Align the stored catalog with the endpoint's id list, preserving surviving
491
- * entries verbatim. Returns one of: provider-absent | no-credentials |
492
- * endpoint-unreachable | in-sync | updated | conflict.
264
+ * The settings namespace this plugin's profile entry answers to, or null.
265
+ *
266
+ * 0.2.0 makes the namespace the *entry id* instead of a string the plugin
267
+ * registers, so it can no longer be a compile-time constant: the id belongs to
268
+ * the profile, and a profile is free to insert this plugin under another one.
269
+ * Candidates are tried in order and the first one the service actually reports
270
+ * wins. The old registered name is last on purpose -- a profile that still
271
+ * carries it should keep persisting under it rather than silently start a
272
+ * second, empty namespace.
493
273
  */
494
- async function syncDeepseekCatalog(ctx) {
495
- const section = llmDeepseekSection(ctx);
496
- if (section === null) return "provider-absent";
497
- const settings = ctx.get("settings");
498
- if (settings === undefined || settings === null || typeof settings.mutate !== "function") return "provider-absent";
499
- const policy = catalogSyncPolicy(ctx);
500
- const baseURL = resolveBaseURL(ctx, section.value);
501
- const key = await resolveProviderKey(ctx, section.value.apiKeyEnv);
502
- if (key === null) return "no-credentials";
503
- let ids;
504
- try {
505
- ids = await fetchEndpointModelIds(baseURL, key);
506
- } catch (error) {
507
- return "endpoint-unreachable";
508
- }
509
- if (ids === null || ids.length === 0) return "endpoint-unreachable";
510
- const current = Array.isArray(section.value.models) ? section.value.models : [];
511
- const byId = new Map();
512
- for (const entry of current) {
513
- if (entry !== null && typeof entry === "object" && typeof entry.id === "string") byId.set(entry.id, entry);
514
- }
515
- const currentIds = catalogIds(current);
516
- // Advertised ids this plugin knows nothing about: reported, never guessed.
517
- const undescribed = ids.filter((id) => !byId.has(id) && !isDescribable(id));
518
-
519
- if (policy === "off") {
520
- const missing = ids.filter((id) => !byId.has(id));
521
- const stale = currentIds.filter((id) => !ids.includes(id));
522
- if (missing.length === 0 && stale.length === 0) return "in-sync";
523
- console.warn(
524
- `[deepseek-style-theme] model catalog drift at ${baseURL} (catalogSync is "off", nothing written) — endpoint adds: ${missing.join(", ") || "none"}; endpoint no longer lists: ${stale.join(", ") || "none"}`
525
- );
526
- return "in-sync";
527
- }
528
-
529
- // A wholesale rewrite is only safe while every advertised id is one this
530
- // plugin can describe; otherwise the answer looks like a gateway catalogue
531
- // rather than the official route, and aligning to it would replace a curated
532
- // catalog with unrelated rows.
533
- const rewrites = policy === "auto" && undescribed.length === 0;
534
- let models;
535
- if (rewrites) {
536
- if (currentIds.length === ids.length && currentIds.every((id, index) => id === ids[index])) return "in-sync";
537
- models = ids.map((id) => (byId.has(id) ? byId.get(id) : catalogEntryFor(id)));
538
- } else {
539
- const added = ids.filter((id) => !byId.has(id) && isDescribable(id));
540
- if (added.length === 0) {
541
- if (undescribed.length > 0) {
542
- console.warn(
543
- `[deepseek-style-theme] ${baseURL} advertises ${undescribed.length} model id(s) this plugin cannot describe (${undescribed.slice(0, 5).join(", ")}); leaving llm-deepseek.models untouched rather than guessing their capabilities.`
544
- );
545
- }
546
- return "in-sync";
547
- }
548
- models = [...current, ...added.map((id) => catalogEntryFor(id))];
274
+ function dsttSettingsNs(ctx) {
275
+ const descriptors = settingsNamespaces(ctx);
276
+ if (descriptors === null) return null;
277
+ for (const ns of DSTT_SETTINGS_NS_CANDIDATES) {
278
+ if (descriptors.some((candidate) => candidate !== null && typeof candidate === "object" && String(candidate.ns) === ns)) return ns;
549
279
  }
550
-
551
- const removed = currentIds.filter((id) => !models.some((entry) => entry.id === id));
552
- try {
553
- // Pinned to the revision this decision was read from: a settings edit the
554
- // user made in the meantime wins, and this activation simply does not sync.
555
- await settings.mutate(LLM_DEEPSEEK_NS, [{ op: "set", path: ["models"], value: models }], section.revision);
556
- } catch (error) {
557
- return "conflict";
558
- }
559
- if (removed.length > 0) {
560
- console.warn(
561
- `[deepseek-style-theme] model catalog synced from ${baseURL}; removed id(s) the endpoint does not list: ${removed.join(", ")}`
562
- );
563
- } else {
564
- console.info(`[deepseek-style-theme] model catalog synced from ${baseURL}`);
565
- }
566
- return "updated";
280
+ return null;
567
281
  }
568
282
 
569
283
  /**
570
- * Run the catalog sync once per activation, retrying briefly while a boot-order
571
- * prerequisite is still missing (the provider namespace or the credentials
572
- * service). Disposal cancels any pending retry.
284
+ * One namespace's descriptor (resolved value plus revision), or null.
285
+ *
286
+ * The namespace defaults to this plugin's own, resolved on every call rather
287
+ * than captured once: the profile patch is editable at runtime (that is what
288
+ * the settings service writes through), so an entry can appear or move while
289
+ * the process is up.
573
290
  */
574
- function startCatalogSync(ctx) {
575
- return ctx.effect(() => {
576
- let cancelled = false;
577
- let timer = null;
578
- const attempt = (step) => {
579
- if (cancelled) return;
580
- syncDeepseekCatalog(ctx).then((outcome) => {
581
- if (cancelled) return;
582
- // "updated" already logged exactly what it changed where it changed it.
583
- if (outcome === "updated") return;
584
- if (CATALOG_SYNC_RETRYABLE.includes(outcome) && step + 1 < CATALOG_SYNC_RETRIES.length) {
585
- timer = setTimeout(() => attempt(step + 1), CATALOG_SYNC_RETRIES[step + 1]);
586
- }
587
- }).catch(() => {});
588
- };
589
- attempt(0);
590
- return () => {
591
- cancelled = true;
592
- if (timer !== null) clearTimeout(timer);
593
- };
594
- }, "deepseek-style-theme: model catalog sync");
291
+ function settingsDescriptor(ctx, ns = dsttSettingsNs(ctx)) {
292
+ if (ns === null || ns === undefined) return null;
293
+ const descriptors = settingsNamespaces(ctx);
294
+ if (descriptors === null) return null;
295
+ const descriptor = descriptors.find((candidate) => String(candidate.ns) === ns);
296
+ if (descriptor === undefined || descriptor === null) return null;
297
+ const value = descriptor.value;
298
+ if (value === null || typeof value !== "object") return null;
299
+ return { value, revision: descriptor.revision };
595
300
  }
596
301
 
597
302
  /**
@@ -747,7 +452,7 @@ function localPathOf(payload) {
747
452
  * the client rejects any off-enum answer anyway.
748
453
  */
749
454
  function dsttRead(ctx) {
750
- const descriptor = settingsDescriptor(ctx, DSTT_SETTINGS_NS);
455
+ const descriptor = settingsDescriptor(ctx);
751
456
  if (descriptor === null) return failure("internal", "settings service unavailable", {});
752
457
  const stored = descriptor.value.mode;
753
458
  return {
@@ -793,8 +498,15 @@ async function dsttWrite(ctx, mode, fluidBrush, glassStyle, composerRefraction,
793
498
  if (settings === undefined || settings === null || typeof settings.mutate !== "function") {
794
499
  return failure("internal", "settings service unavailable", {});
795
500
  }
501
+ // 命名空间是按 profile 条目 id 解析出来的,解析不到就不能硬写:写一个不存在
502
+ // 的 ns 会被服务以 `No configurable plugin entry` 拒掉,而那时错误信息会把
503
+ // "插件没被正确插入 profile"说成"设置写入失败",误导排查方向。
504
+ const ns = dsttSettingsNs(ctx);
505
+ if (ns === null) {
506
+ return failure("internal", `no settings entry for this plugin (tried ${DSTT_SETTINGS_NS_CANDIDATES.join(", ")})`, {});
507
+ }
796
508
  try {
797
- await settings.mutate(DSTT_SETTINGS_NS, ops);
509
+ await settings.mutate(ns, ops);
798
510
  } catch (error) {
799
511
  return failure("internal", error instanceof Error ? error.message : "settings write failed", {});
800
512
  }
@@ -819,7 +531,7 @@ async function dsttWrite(ctx, mode, fluidBrush, glassStyle, composerRefraction,
819
531
  * the client always gets one of the four ids it can gate its stylesheet on.
820
532
  */
821
533
  function dsttBackgroundValue(ctx) {
822
- const descriptor = settingsDescriptor(ctx, DSTT_SETTINGS_NS);
534
+ const descriptor = settingsDescriptor(ctx);
823
535
  const stored = descriptor === null ? undefined : descriptor.value.backgroundMode;
824
536
  return BACKGROUND_MODES.includes(stored) ? stored : BACKGROUND_MODE_DEFAULT;
825
537
  }
@@ -838,19 +550,19 @@ function normalizeCustomBackground(value) {
838
550
 
839
551
  /** The stored custom background (empty string when nothing is set). */
840
552
  function dsttCustomBackgroundValue(ctx) {
841
- const descriptor = settingsDescriptor(ctx, DSTT_SETTINGS_NS);
553
+ const descriptor = settingsDescriptor(ctx);
842
554
  return normalizeCustomBackground(descriptor === null ? "" : descriptor.value.customBackground);
843
555
  }
844
556
 
845
557
  /** Whether the animated background layer may mount; on unless it was turned off. */
846
558
  function dsttAmbientValue(ctx) {
847
- const descriptor = settingsDescriptor(ctx, DSTT_SETTINGS_NS);
559
+ const descriptor = settingsDescriptor(ctx);
848
560
  return descriptor === null ? true : descriptor.value.ambientBackground !== false;
849
561
  }
850
562
 
851
563
  /** The stored backdrop-blur preference, defaulting to on. */
852
564
  function dsttBlurValue(ctx) {
853
- const descriptor = settingsDescriptor(ctx, DSTT_SETTINGS_NS);
565
+ const descriptor = settingsDescriptor(ctx);
854
566
  return descriptor === null ? true : descriptor.value.backdropBlur !== false;
855
567
  }
856
568
 
@@ -861,7 +573,7 @@ function dsttBlurValue(ctx) {
861
573
  * client only ever sees one id per state.
862
574
  */
863
575
  function dsttComposerValue(ctx) {
864
- const descriptor = settingsDescriptor(ctx, DSTT_SETTINGS_NS);
576
+ const descriptor = settingsDescriptor(ctx);
865
577
  const stored = descriptor === null ? undefined : descriptor.value.composerRefraction;
866
578
  if (stored === "origin") return "wide";
867
579
  return COMPOSER_REFRACTIONS.includes(stored) ? stored : COMPOSER_REFRACTION_DEFAULT;
@@ -869,7 +581,7 @@ function dsttComposerValue(ctx) {
869
581
 
870
582
  /** The stored fluid-brush preference, defaulting to off. */
871
583
  function dsttBrushValue(ctx) {
872
- const descriptor = settingsDescriptor(ctx, DSTT_SETTINGS_NS);
584
+ const descriptor = settingsDescriptor(ctx);
873
585
  return descriptor === null ? false : descriptor.value.fluidBrush === true;
874
586
  }
875
587
 
@@ -879,39 +591,72 @@ function dsttBrushValue(ctx) {
879
591
  * ids it can gate its stylesheet on.
880
592
  */
881
593
  function dsttGlassValue(ctx) {
882
- const descriptor = settingsDescriptor(ctx, DSTT_SETTINGS_NS);
594
+ const descriptor = settingsDescriptor(ctx);
883
595
  const stored = descriptor === null ? undefined : descriptor.value.glassStyle;
884
596
  return GLASS_STYLES.includes(stored) ? stored : GLASS_STYLE_DEFAULT;
885
597
  }
886
598
 
887
599
  function apply(ctx) {
888
- // Both registrations below are best-effort on purpose: dsh ships breaking
889
- // changes to the settings / connection APIs between releases, and a throw
890
- // here fails this bundle's loader entry — with it the whole plugin tree,
891
- // which is far worse than a theme that merely loses its persistence.
600
+ // Best-effort on purpose: dsh ships breaking changes to the settings API
601
+ // between releases, and a throw here fails this bundle's loader entry — with
602
+ // it the whole plugin tree, which is far worse than a theme that merely
603
+ // loses its persistence.
604
+ //
605
+ // 0.2.0 removed `settings.register()` outright, so there is nothing to
606
+ // register any more: the namespace, its shape (`Config` above) and the
607
+ // profile entry that carries them are all owned by the product now. What is
608
+ // left for this plugin to do at boot is notice when that wiring is missing
609
+ // and say so once, in the loader log, instead of failing silently and
610
+ // leaving the user to discover it as "my switches reset every restart".
892
611
  ctx.inject(["settings"], (settingsCtx) => {
893
- if (DsttSettingsSchema === null) {
894
- console.warn("[deepseek-style-theme] DSTT settings not registered (Schemastery is missing, see above): the mode will not persist");
895
- } else {
896
- try {
897
- settingsCtx.settings.register(DSTT_SETTINGS_NS, DsttSettingsSchema);
898
- const descriptor = settingsDescriptor(settingsCtx, DSTT_SETTINGS_NS);
612
+ // 0.2.0 的两条时序事实,这个块长成这样全是为了它们:
613
+ //
614
+ // 1. 设置服务的 describe() 只列出**已经 ACTIVE**(fiber.state === 2)的条目,
615
+ // 而本插件此刻正在自己的激活过程中,自己那条还是 LOADING。所以在这里
616
+ // 立刻解析命名空间必然是空的 —— 那不是配置错误,是时机不到。
617
+ // 实测证据:条目存在、runtime.Config 是 function,唯独 state === 1。
618
+ // 2. 取值路径(settingsDescriptor / dsttSettingsNs)是惰性的,每次调用都
619
+ // 重新解析,所以运行期的读写本来就正确。真正需要"开机做一次"的只有
620
+ // 下面两件事:自检(没接线就要说出来)与老 mode 值迁移。
621
+ //
622
+ // 于是退避重试到就绪为止;到点仍没有才出声,并把服务实际提供的命名空间
623
+ // 一并列出来 —— 少了那一行,下一个人只会看到"没有设置条目",那句话是真的,
624
+ // 但完全帮不上忙(这一版就是这么绕了一圈才定位到的)。
625
+ try {
626
+ if (DsttSettingsSchema === null) {
627
+ console.warn("[deepseek-style-theme] DSTT settings have no schema (Schemastery is missing, see above): the mode will not persist");
628
+ return;
629
+ }
630
+ let attempts = 0;
631
+ const settle = () => {
632
+ attempts += 1;
633
+ const ns = dsttSettingsNs(settingsCtx);
634
+ if (ns === null) {
635
+ // 40 × 500ms = 20s:整棵插件树启动远用不了这么久,超过就是真没接线。
636
+ if (attempts < 40) return;
637
+ clearInterval(timer);
638
+ const available = (settingsNamespaces(settingsCtx) ?? [])
639
+ .map((row) => (row === null || typeof row !== "object" ? "?" : String(row.ns)))
640
+ .slice(0, 12);
641
+ console.warn(`[deepseek-style-theme] no settings entry for this plugin (tried ${DSTT_SETTINGS_NS_CANDIDATES.join(", ")}) after ${attempts} attempts: DSTT preferences will not persist. The settings service reports ${available.length} namespace(s): ${available.length === 0 ? "(none)" : available.join(", ")}`);
642
+ return;
643
+ }
644
+ clearInterval(timer);
645
+ const descriptor = settingsDescriptor(settingsCtx, ns);
899
646
  const stored = descriptor === null ? undefined : descriptor.value.mode;
900
647
  const migrated = migrateLegacyMode(stored);
901
648
  if (migrated !== null) {
902
- settingsCtx.settings.mutate(DSTT_SETTINGS_NS, [{ op: "set", path: ["mode"], value: migrated }])
649
+ settingsCtx.settings.mutate(ns, [{ op: "set", path: ["mode"], value: migrated }])
903
650
  .then(() => console.warn(`[deepseek-style-theme] migrated DSTT mode "${stored}" -> "${migrated}"`))
904
651
  .catch(() => {});
905
652
  }
906
- } catch (error) {
907
- console.warn("[deepseek-style-theme] settings.register failed (DSTT mode will not persist):", error);
908
- }
909
- }
910
- // Activation-time, best-effort, silent unless it really changed something.
911
- try {
912
- startCatalogSync(settingsCtx);
653
+ };
654
+ const timer = setInterval(settle, 500);
655
+ // 这个定时器不该拖着进程不让它退出。
656
+ if (typeof timer.unref === "function") timer.unref();
657
+ settle();
913
658
  } catch (error) {
914
- console.warn("[deepseek-style-theme] catalog sync unavailable:", error);
659
+ console.warn("[deepseek-style-theme] DSTT settings wiring failed (the mode will not persist):", error);
915
660
  }
916
661
  });
917
662
 
@@ -967,18 +712,9 @@ function apply(ctx) {
967
712
  }
968
713
  return { ok: true, value: { opened: true } };
969
714
  }
970
- if (endpoint !== "dshome/explorer.open") {
971
- return failure("bad-request", `unknown endpoint "${endpoint}"`, { issues: [] });
972
- }
973
- const path = localPathOf(payload);
974
- if (path === null) {
975
- return failure("bad-request", "payload.path must be a non-empty absolute local path", { issues: [] });
976
- }
977
- const opened = await openPathGeneric(path);
978
- if (!opened) {
979
- return failure("internal", "the system file manager failed to open the path", {});
980
- }
981
- return { ok: true, value: { opened: true } };
715
+ // dshome/explorer.open 已随"打开工作区"菜单一并删除(2.1.90,需求第 9 条):
716
+ // 那个手势改用官方的 open-in-app,本插件不再自己开文件管理器。
717
+ return failure("bad-request", `unknown endpoint "${endpoint}"`, { issues: [] });
982
718
  } catch (error) {
983
719
  const message = error instanceof Error ? error.message : "unexpected failure in the rpc handler";
984
720
  return failure("internal", message, {});
@@ -1113,4 +849,21 @@ function sendJson(res, status, value) {
1113
849
  // plugin's channel without tripping the guard described in apply().
1114
850
  const inject = ["webServer"];
1115
851
 
1116
- export { apply, inject };
852
+ // 0.2.0 changed how a plugin owns settings, and the change is structural rather
853
+ // than a rename. There is no `settings.register(ns, schema)` any more anywhere
854
+ // in the product; a settings namespace IS a profile entry, its id is the
855
+ // namespace, and its shape is read off `fiber.runtime.Config` -- this export.
856
+ // The service also refuses to write a field that is not marked volatile
857
+ // (`volatile` is a Schemastery method that sets meta.volatile), which is why
858
+ // every field above carries `.volatile()`: without it the section still renders
859
+ // but every write fails with "has no volatile fields". Exporting `undefined`
860
+ // when Schemastery is missing keeps the loader on "no schema" instead of
861
+ // handing it a broken one.
862
+ const Config = DsttSettingsSchema === null ? undefined : DsttSettingsSchema;
863
+
864
+ // 两处都挂:cordis 读结构的位置是 `runtime.Config`,而 `runtime` 在功能型插件上
865
+ // 可能是模块命名空间,也可能是被选中的那个函数本身。哪一种是实现细节,不值得赌,
866
+ // 所以函数上也带一份。代价是同一个对象有两个名字,改的时候两边一起改。
867
+ apply.Config = Config;
868
+
869
+ export { apply, inject, Config };