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.
- package/README.md +39 -48
- package/lib/client.js +808 -857
- package/lib/index.js +143 -390
- 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
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
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.
|
|
262
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
495
|
-
const
|
|
496
|
-
if (
|
|
497
|
-
const
|
|
498
|
-
|
|
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
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
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
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
889
|
-
//
|
|
890
|
-
//
|
|
891
|
-
//
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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(
|
|
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
|
-
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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]
|
|
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
|
-
|
|
971
|
-
|
|
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
|
-
|
|
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 };
|