dsh-theone 0.3.9 → 0.3.13
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 +77 -53
- package/README.zh.md +82 -58
- package/cordis.patch.yml +0 -0
- package/dist/catalog-types.d.ts +0 -0
- package/dist/catalog-types.js +0 -0
- package/dist/client-locales.d.ts +20 -0
- package/dist/client-locales.js +40 -0
- package/dist/client-navigation.d.ts +0 -0
- package/dist/client-navigation.js +0 -0
- package/dist/client.d.ts +0 -0
- package/dist/client.js +375 -11
- package/dist/gateway-compaction.d.ts +0 -0
- package/dist/gateway-compaction.js +0 -0
- package/dist/history-catalog.d.ts +0 -0
- package/dist/history-catalog.js +0 -0
- package/dist/index.d.ts +17 -3
- package/dist/index.js +107 -23
- package/dist/linkage.d.ts +0 -0
- package/dist/linkage.js +0 -0
- package/dist/llm-router.d.ts +0 -0
- package/dist/llm-router.js +0 -0
- package/dist/model-json.d.ts +0 -0
- package/dist/model-json.js +0 -0
- package/dist/router.d.ts +0 -0
- package/dist/router.js +0 -0
- package/dist/routing-policy.d.ts +0 -0
- package/dist/routing-policy.js +0 -0
- package/dist/run.d.ts +0 -0
- package/dist/run.js +0 -0
- package/dist/settings-types.d.ts +0 -0
- package/dist/settings-types.js +0 -0
- package/dist/settings.d.ts +0 -0
- package/dist/settings.js +0 -0
- package/dist/store.d.ts +0 -0
- package/dist/store.js +0 -0
- package/dist/types.d.ts +0 -0
- package/dist/types.js +0 -0
- package/dist/update.d.ts +55 -0
- package/dist/update.js +121 -0
- package/docs/plugin-v0.1.md +0 -0
- package/docs/plugin-v0.1.zh.md +0 -0
- package/docs/plugin-v0.2.md +0 -0
- package/docs/plugin-v0.2.zh.md +0 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -19,10 +19,26 @@ import { EDITABLE_SETTINGS_KEYS, RESTART_SETTINGS_KEYS } from "./settings-types.
|
|
|
19
19
|
import { validateSettings } from "./settings.js";
|
|
20
20
|
import { RESTART_CODE, WorkerRun } from "./run.js";
|
|
21
21
|
import { buildBriefing, LINK_SIGNAL, relatedTopics } from "./linkage.js";
|
|
22
|
+
import { PACKAGE_NAME, Updater } from "./update.js";
|
|
22
23
|
function redactDescriptor(text) {
|
|
23
24
|
return text.replace(/\bsk-[a-zA-Z0-9_-]{16,}|\bBearer\s+\S+/gi, '[REDACTED]')
|
|
24
25
|
.replace(/((?:api[_ -]?key|password|密码|密钥)\s*[:=:]\s*)\S+/gi, '$1[REDACTED]');
|
|
25
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Main chat's model entries besides plain "TheOne": "TheOne · <model>" still answers through TheOne,
|
|
29
|
+
* with that model doing the routing and the background work.
|
|
30
|
+
*/
|
|
31
|
+
const VIA = 'via:';
|
|
32
|
+
export function viaModel(selection) { return `${VIA}${selection.provider}/${selection.model}`; }
|
|
33
|
+
export function parseVia(id) {
|
|
34
|
+
if (!id?.startsWith(VIA))
|
|
35
|
+
return undefined;
|
|
36
|
+
const rest = id.slice(VIA.length);
|
|
37
|
+
const slash = rest.indexOf('/');
|
|
38
|
+
if (slash <= 0 || slash === rest.length - 1 || rest.slice(0, slash) === 'theone')
|
|
39
|
+
return undefined;
|
|
40
|
+
return { provider: rest.slice(0, slash), model: rest.slice(slash + 1) };
|
|
41
|
+
}
|
|
26
42
|
/** Gateway provider delegates each accepted input to its context's DSH worker. */
|
|
27
43
|
class GatewayAdapter extends LlmAdapter {
|
|
28
44
|
service;
|
|
@@ -31,15 +47,11 @@ class GatewayAdapter extends LlmAdapter {
|
|
|
31
47
|
this.service = service;
|
|
32
48
|
}
|
|
33
49
|
providerInfo(provider) { return { id: provider, name: 'TheOne' }; }
|
|
34
|
-
|
|
35
|
-
// The entry accepts whatever the backing model accepts (e.g. images).
|
|
36
|
-
const info = await this.service.gatewayModelInfo(provider).catch(() => undefined);
|
|
37
|
-
return [{ provider, id: 'gateway', name: 'TheOne', inputModalities: [...(info?.inputModalities ?? ['text'])] }];
|
|
38
|
-
}
|
|
50
|
+
listModels(provider) { return this.service.gatewayModels(provider); }
|
|
39
51
|
resolveModel(provider, model, signal) {
|
|
40
|
-
if (model !== 'gateway')
|
|
41
|
-
throw new Error('TheOne only exposes
|
|
42
|
-
return this.service.gatewayModelInfo(provider, signal);
|
|
52
|
+
if (model !== 'gateway' && !parseVia(model))
|
|
53
|
+
throw new Error('TheOne only exposes its gateway models');
|
|
54
|
+
return this.service.gatewayModelInfo(provider, signal, model);
|
|
43
55
|
}
|
|
44
56
|
providerRetryPolicy() { return resolveRetryPolicy({ mode: 'normal', maxRetries: 0 }, 'theone.retry'); }
|
|
45
57
|
stream(options) { return this.service.answer(options); }
|
|
@@ -85,6 +97,28 @@ function reasonLabel(reason) {
|
|
|
85
97
|
function settingsConfig(values) {
|
|
86
98
|
return { ...values, workerProvider: values.workerProvider ?? undefined, workerModel: values.workerModel ?? undefined, contextsPath: values.contextsPath ?? undefined };
|
|
87
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Switch an installed bundle off and on so DSH loads its new version. This disposes the running
|
|
102
|
+
* TheOne, so it runs after the current request has answered and outlives this instance.
|
|
103
|
+
*/
|
|
104
|
+
function reloadBundle(manager, bundle) {
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
void (async () => {
|
|
107
|
+
const off = await manager.setBundleEnabled(bundle, false);
|
|
108
|
+
if (off.application === 'failed' || off.application === 'cancelled')
|
|
109
|
+
return;
|
|
110
|
+
await manager.setBundleEnabled(bundle, true);
|
|
111
|
+
})().catch(error => console.warn('TheOne could not reload itself after updating; restart DSH to apply the update.', error));
|
|
112
|
+
}, 300);
|
|
113
|
+
}
|
|
114
|
+
function readOwnVersion() {
|
|
115
|
+
try {
|
|
116
|
+
return String(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version ?? '0.0.0');
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return '0.0.0';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
88
122
|
function readDescriptors(path) {
|
|
89
123
|
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
90
124
|
if (!Array.isArray(value))
|
|
@@ -132,6 +166,9 @@ export default class TheOne extends Service {
|
|
|
132
166
|
closing = new Map();
|
|
133
167
|
/** A model the user picked in main chat's own model selector; it answers through the Workers. */
|
|
134
168
|
pickedModel;
|
|
169
|
+
adapter;
|
|
170
|
+
/** Update checks and one-click install through DSH's plugin manager. */
|
|
171
|
+
updater;
|
|
135
172
|
/** Sessions whose current step answers through TheOne; only these refuse to run tools themselves. */
|
|
136
173
|
throughTheOne = new WeakMap();
|
|
137
174
|
constructor(ctx, config) {
|
|
@@ -164,6 +201,18 @@ export default class TheOne extends Service {
|
|
|
164
201
|
}
|
|
165
202
|
descriptors ??= config.contextsPath ? readDescriptors(config.contextsPath) : [];
|
|
166
203
|
this.gatewayDirectory = resolve(dirname(databasePath), 'gateway');
|
|
204
|
+
this.updater = new Updater(readOwnVersion(), () => {
|
|
205
|
+
// The profile's package.json records how this plugin was installed (GitHub, npm or a local path).
|
|
206
|
+
const dir = ctx.profileContext?.dir;
|
|
207
|
+
if (!dir)
|
|
208
|
+
return undefined;
|
|
209
|
+
try {
|
|
210
|
+
return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies?.[PACKAGE_NAME];
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
167
216
|
ctx.effect(() => async () => {
|
|
168
217
|
try {
|
|
169
218
|
await Promise.all([...this.workers.values()].map(handle => handle.dispose()));
|
|
@@ -183,7 +232,7 @@ export default class TheOne extends Service {
|
|
|
183
232
|
this.catalog.start();
|
|
184
233
|
}
|
|
185
234
|
this.registerCatalogChannel();
|
|
186
|
-
ctx.llm.registerAdapter(['theone'], new GatewayAdapter(this));
|
|
235
|
+
this.adapter = ctx.llm.registerAdapter(['theone'], new GatewayAdapter(this));
|
|
187
236
|
ctx.on('session/event', (session, event) => {
|
|
188
237
|
if (event.type === 'turn/end') {
|
|
189
238
|
if (session.id === this.reservedGateway)
|
|
@@ -264,8 +313,8 @@ export default class TheOne extends Service {
|
|
|
264
313
|
return config;
|
|
265
314
|
}
|
|
266
315
|
this.throughTheOne.set(payload.agent, true);
|
|
267
|
-
// The resolved selection is authoritative; TheOne
|
|
268
|
-
this.pickedModel = config.provider === 'theone' ?
|
|
316
|
+
// The resolved selection is authoritative; plain TheOne means "follow DSH's selected model".
|
|
317
|
+
this.pickedModel = config.provider === 'theone' ? parseVia(config.model) : { provider: config.provider, model: config.model };
|
|
269
318
|
return config.provider === 'theone' ? config : { ...config, provider: 'theone', model: 'gateway' };
|
|
270
319
|
}, { prepend: true });
|
|
271
320
|
ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
|
|
@@ -278,7 +327,7 @@ export default class TheOne extends Service {
|
|
|
278
327
|
// Know main chat's model choice before routing, so this very message is classified with it.
|
|
279
328
|
const pickedNow = selected?.signal === signal ? selected.model : undefined;
|
|
280
329
|
if (provider === 'theone')
|
|
281
|
-
this.pickedModel =
|
|
330
|
+
this.pickedModel = parseVia(pickedNow ?? agent.options.model);
|
|
282
331
|
else if (provider && pickedNow)
|
|
283
332
|
this.pickedModel = { provider, model: pickedNow };
|
|
284
333
|
const users = decision.messages.filter(message => message.source.kind === 'user');
|
|
@@ -566,6 +615,18 @@ export default class TheOne extends Service {
|
|
|
566
615
|
return Response.json({ error: code }, { status: code === 'GATEWAY_BUSY' ? 409 : 400 });
|
|
567
616
|
}
|
|
568
617
|
} }));
|
|
618
|
+
// Is a newer TheOne out, and install it with DSH's plugin manager (it loads after a restart).
|
|
619
|
+
child.effect(() => connection.fetch.register({ path: '/api/theone/update', methods: ['GET', 'POST'], requestBody: 'buffered', fetch: async (request) => {
|
|
620
|
+
if (request.method === 'POST' && (this.active || this.reservedGateway))
|
|
621
|
+
return Response.json({ ...await this.updater.status(), error: 'GATEWAY_BUSY' }, { status: 409 });
|
|
622
|
+
const manager = this.ctx.get('pluginManager');
|
|
623
|
+
// With DSH's hot reload, switching the bundle off and on loads the new version without a DSH restart.
|
|
624
|
+
const live = !!this.ctx.get('hmr') && typeof manager?.setBundleEnabled === 'function';
|
|
625
|
+
const status = request.method === 'POST'
|
|
626
|
+
? await this.updater.install(manager, live ? bundle => reloadBundle(manager, bundle) : undefined)
|
|
627
|
+
: await this.updater.status(new URL(request.url).searchParams.has('force'));
|
|
628
|
+
return Response.json(status, { headers: { 'cache-control': 'no-store' } });
|
|
629
|
+
} }));
|
|
569
630
|
child.effect(() => connection.fetch.register({ path: '/api/theone/sessions', methods: ['GET'], requestBody: 'buffered', fetch: async () => Response.json({ sessions: await this.attachableSessions() }, { headers: { 'cache-control': 'no-store' } }) }));
|
|
570
631
|
// The user's corrections to topic linking always take precedence over what was learned.
|
|
571
632
|
child.effect(() => connection.fetch.register({ path: '/api/theone/links', methods: ['POST'], requestBody: 'buffered', fetch: async (request) => {
|
|
@@ -701,7 +762,7 @@ export default class TheOne extends Service {
|
|
|
701
762
|
const defaultSelection = this.ctx.agentDefaultModel.currentSelection();
|
|
702
763
|
const selection = c.workerProvider && c.workerModel
|
|
703
764
|
? { provider: c.workerProvider, model: c.workerModel }
|
|
704
|
-
: defaultSelection.provider !== 'theone' ? defaultSelection : this.store.rememberedModel(c.gatewayKey);
|
|
765
|
+
: defaultSelection.provider !== 'theone' ? defaultSelection : parseVia(defaultSelection.model) ?? this.store.rememberedModel(c.gatewayKey);
|
|
705
766
|
let model = selection ? { provider: selection.provider, model: selection.model } : null;
|
|
706
767
|
let modelUnavailable = !model;
|
|
707
768
|
if (selection) {
|
|
@@ -729,10 +790,7 @@ export default class TheOne extends Service {
|
|
|
729
790
|
savedValues = validateSettings(saved.values, { contextsPath: c.contextsPath ?? null });
|
|
730
791
|
}
|
|
731
792
|
catch { /* Keep the current usable form. */ }
|
|
732
|
-
|
|
733
|
-
const listed = await Promise.all(this.ctx.llm.listProviders().filter(provider => provider.id !== 'theone').map(provider => Promise.race([this.ctx.llm.listModels(provider.id), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000).unref())])
|
|
734
|
-
.then(models => models.map(model => ({ provider: provider.id, id: model.id, name: model.name })), () => [])));
|
|
735
|
-
const models = listed.flat();
|
|
793
|
+
const models = (await this.offeredModels()).map(model => ({ provider: model.provider, id: model.id, name: model.name }));
|
|
736
794
|
return { values, model, models, modelUnavailable, savedValues, revision: saved?.revision ?? 0,
|
|
737
795
|
restartRequired: RESTART_SETTINGS_KEYS.some(key => savedValues[key] !== activeEditable[key]) };
|
|
738
796
|
}
|
|
@@ -744,13 +802,21 @@ export default class TheOne extends Service {
|
|
|
744
802
|
const { historyCatalog: _catalog, catalogIntervalMs: _interval, ...live } = settingsConfig(values);
|
|
745
803
|
if (live.routerMode !== this.config.routerMode)
|
|
746
804
|
this.router = this.routerFor(live.routerMode);
|
|
805
|
+
const modelChanged = live.workerProvider !== this.config.workerProvider || live.workerModel !== this.config.workerModel;
|
|
747
806
|
this.config = { ...this.config, ...live };
|
|
807
|
+
// Main chat's reasoning levels and image input are the background model's; DSH re-reads them on this signal.
|
|
808
|
+
if (modelChanged)
|
|
809
|
+
try {
|
|
810
|
+
this.adapter?.replace(['theone']);
|
|
811
|
+
}
|
|
812
|
+
catch { /* Released during shutdown. */ }
|
|
748
813
|
this.store.seed(descriptors);
|
|
749
814
|
}
|
|
750
815
|
/** Capture before Web saves the gateway itself as DSH's new default. */
|
|
751
816
|
captureDefaultModel() {
|
|
752
|
-
const
|
|
753
|
-
|
|
817
|
+
const current = this.ctx.agentDefaultModel.currentSelection();
|
|
818
|
+
const selection = current.provider === 'theone' ? parseVia(current.model) : current;
|
|
819
|
+
if (selection?.provider && selection.model)
|
|
754
820
|
this.store.rememberModel(this.config.gatewayKey, selection);
|
|
755
821
|
}
|
|
756
822
|
backingModel() {
|
|
@@ -764,12 +830,28 @@ export default class TheOne extends Service {
|
|
|
764
830
|
throw new RouterFailure('ROUTER_MODEL_MISSING');
|
|
765
831
|
return selection;
|
|
766
832
|
}
|
|
767
|
-
/**
|
|
768
|
-
async
|
|
769
|
-
const
|
|
833
|
+
/** Every model DSH offers besides TheOne; a provider that cannot list its models in time offers none. */
|
|
834
|
+
async offeredModels() {
|
|
835
|
+
const listed = await Promise.all(this.ctx.llm.listProviders().filter(provider => provider.id !== 'theone').map(provider => Promise.race([this.ctx.llm.listModels(provider.id), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000).unref())])
|
|
836
|
+
.catch(() => [])));
|
|
837
|
+
return listed.flat();
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Main chat's model menu has one TheOne entry; the background model is chosen with the button
|
|
841
|
+
* beside it. Selections of the older "TheOne · <model>" entries still resolve.
|
|
842
|
+
*/
|
|
843
|
+
async gatewayModels(provider) {
|
|
844
|
+
// The entry accepts whatever the backing model accepts (e.g. images).
|
|
845
|
+
const info = await this.gatewayModelInfo(provider).catch(() => undefined);
|
|
846
|
+
return [{ provider, id: 'gateway', name: 'TheOne', inputModalities: [...(info?.inputModalities ?? ['text'])] }];
|
|
847
|
+
}
|
|
848
|
+
/** The entry has exactly the backing model's capacity, including DSH overrides. */
|
|
849
|
+
async gatewayModelInfo(provider = 'theone', signal, model = 'gateway') {
|
|
850
|
+
const chosen = parseVia(model);
|
|
851
|
+
const info = { provider, id: model, name: 'TheOne Gateway', inputModalities: ['text'] };
|
|
770
852
|
let selection;
|
|
771
853
|
try {
|
|
772
|
-
selection = this.backingModel();
|
|
854
|
+
selection = chosen ?? this.backingModel();
|
|
773
855
|
}
|
|
774
856
|
catch (error) {
|
|
775
857
|
if (error instanceof RouterFailure && error.code === 'ROUTER_MODEL_MISSING')
|
|
@@ -777,6 +859,8 @@ export default class TheOne extends Service {
|
|
|
777
859
|
throw error;
|
|
778
860
|
}
|
|
779
861
|
const backing = await this.ctx.llm.resolveModelInfo(selection.provider, selection.model, signal);
|
|
862
|
+
if (chosen)
|
|
863
|
+
info.name = `TheOne · ${backing.name}`;
|
|
780
864
|
// Image input and the thinking-effort choices are the backing model's, so main chat offers the same controls.
|
|
781
865
|
return { ...info, ...(backing.inputModalities ? { inputModalities: [...backing.inputModalities] } : {}),
|
|
782
866
|
...(backing.reasoning ? { reasoning: backing.reasoning } : {}),
|
package/dist/linkage.d.ts
CHANGED
|
File without changes
|
package/dist/linkage.js
CHANGED
|
File without changes
|
package/dist/llm-router.d.ts
CHANGED
|
File without changes
|
package/dist/llm-router.js
CHANGED
|
File without changes
|
package/dist/model-json.d.ts
CHANGED
|
File without changes
|
package/dist/model-json.js
CHANGED
|
File without changes
|
package/dist/router.d.ts
CHANGED
|
File without changes
|
package/dist/router.js
CHANGED
|
File without changes
|
package/dist/routing-policy.d.ts
CHANGED
|
File without changes
|
package/dist/routing-policy.js
CHANGED
|
File without changes
|
package/dist/run.d.ts
CHANGED
|
File without changes
|
package/dist/run.js
CHANGED
|
File without changes
|
package/dist/settings-types.d.ts
CHANGED
|
File without changes
|
package/dist/settings-types.js
CHANGED
|
File without changes
|
package/dist/settings.d.ts
CHANGED
|
File without changes
|
package/dist/settings.js
CHANGED
|
File without changes
|
package/dist/store.d.ts
CHANGED
|
File without changes
|
package/dist/store.js
CHANGED
|
File without changes
|
package/dist/types.d.ts
CHANGED
|
File without changes
|
package/dist/types.js
CHANGED
|
File without changes
|
package/dist/update.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export declare const REPOSITORY = "YunongDai2005/dsh-theone";
|
|
2
|
+
export declare const PACKAGE_NAME = "dsh-theone";
|
|
3
|
+
/** Where this copy was installed from, which decides where its updates come from. */
|
|
4
|
+
export type UpdateSource = 'github' | 'npm' | 'other';
|
|
5
|
+
export interface UpdateStatus {
|
|
6
|
+
current: string;
|
|
7
|
+
latest?: string;
|
|
8
|
+
/** A newer version exists. */
|
|
9
|
+
available: boolean;
|
|
10
|
+
/** It can be installed from here (otherwise reinstall by hand). */
|
|
11
|
+
installable: boolean;
|
|
12
|
+
source: UpdateSource;
|
|
13
|
+
/** installing → reloading (TheOne restarts itself; DSH keeps running), restart (applies after DSH restarts) or failed. */
|
|
14
|
+
state?: 'installing' | 'reloading' | 'restart' | 'failed';
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
/** The DSH plugin manager's install call, as TheOne uses it. */
|
|
18
|
+
export interface PluginInstaller {
|
|
19
|
+
installBundle(spec: string, options?: {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
application: string;
|
|
23
|
+
bundle?: string;
|
|
24
|
+
error?: {
|
|
25
|
+
code?: string;
|
|
26
|
+
message?: string;
|
|
27
|
+
} | unknown;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
/** Compare dotted versions numerically; a pre-release sorts before its release. */
|
|
31
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
32
|
+
/** Read the dependency spec DSH saved for this plugin: GitHub, npm, or something else (a local path, a tarball). */
|
|
33
|
+
export declare function installSource(spec: string | undefined): UpdateSource;
|
|
34
|
+
/** Checks for a newer TheOne and installs it through DSH's own plugin manager. */
|
|
35
|
+
export declare class Updater {
|
|
36
|
+
readonly current: string;
|
|
37
|
+
private readonly spec;
|
|
38
|
+
private readonly fetcher;
|
|
39
|
+
private readonly now;
|
|
40
|
+
private checked?;
|
|
41
|
+
private state?;
|
|
42
|
+
private error?;
|
|
43
|
+
private pending?;
|
|
44
|
+
constructor(current: string, spec: () => string | undefined, fetcher?: typeof fetch, now?: () => number);
|
|
45
|
+
get source(): UpdateSource;
|
|
46
|
+
/** The latest known status; checks again at most every six hours (or now, when forced). */
|
|
47
|
+
status(force?: boolean): Promise<UpdateStatus>;
|
|
48
|
+
/**
|
|
49
|
+
* Install the newer version. With `reload`, TheOne then restarts itself so the new version runs
|
|
50
|
+
* without restarting DSH; otherwise it is loaded the next time DSH starts.
|
|
51
|
+
*/
|
|
52
|
+
install(installer: PluginInstaller | undefined, reload?: (bundle: string) => void): Promise<UpdateStatus>;
|
|
53
|
+
private snapshot;
|
|
54
|
+
private check;
|
|
55
|
+
}
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export const REPOSITORY = 'YunongDai2005/dsh-theone';
|
|
2
|
+
export const PACKAGE_NAME = 'dsh-theone';
|
|
3
|
+
/** Compare dotted versions numerically; a pre-release sorts before its release. */
|
|
4
|
+
export function compareVersions(a, b) {
|
|
5
|
+
const parse = (value) => {
|
|
6
|
+
const [core, pre] = value.trim().replace(/^v/, '').split('-', 2);
|
|
7
|
+
return { parts: core.split('.').map(part => Number.parseInt(part, 10) || 0), pre };
|
|
8
|
+
};
|
|
9
|
+
const x = parse(a), y = parse(b);
|
|
10
|
+
for (let index = 0; index < Math.max(x.parts.length, y.parts.length); index++) {
|
|
11
|
+
const difference = (x.parts[index] ?? 0) - (y.parts[index] ?? 0);
|
|
12
|
+
if (difference)
|
|
13
|
+
return Math.sign(difference);
|
|
14
|
+
}
|
|
15
|
+
if (x.pre === y.pre)
|
|
16
|
+
return 0;
|
|
17
|
+
return x.pre === undefined ? 1 : y.pre === undefined ? -1 : x.pre < y.pre ? -1 : 1;
|
|
18
|
+
}
|
|
19
|
+
/** Read the dependency spec DSH saved for this plugin: GitHub, npm, or something else (a local path, a tarball). */
|
|
20
|
+
export function installSource(spec) {
|
|
21
|
+
if (!spec)
|
|
22
|
+
return 'other';
|
|
23
|
+
if (spec.toLowerCase().includes(REPOSITORY.toLowerCase()))
|
|
24
|
+
return 'github';
|
|
25
|
+
if (/^(?:npm:dsh-theone@)?[\^~>=<\s]*\d|^latest$|^\*$/.test(spec))
|
|
26
|
+
return 'npm';
|
|
27
|
+
return 'other';
|
|
28
|
+
}
|
|
29
|
+
const HOUR = 3600000;
|
|
30
|
+
/** Checks for a newer TheOne and installs it through DSH's own plugin manager. */
|
|
31
|
+
export class Updater {
|
|
32
|
+
current;
|
|
33
|
+
spec;
|
|
34
|
+
fetcher;
|
|
35
|
+
now;
|
|
36
|
+
checked;
|
|
37
|
+
state;
|
|
38
|
+
error;
|
|
39
|
+
pending;
|
|
40
|
+
constructor(current, spec, fetcher = fetch, now = Date.now) {
|
|
41
|
+
this.current = current;
|
|
42
|
+
this.spec = spec;
|
|
43
|
+
this.fetcher = fetcher;
|
|
44
|
+
this.now = now;
|
|
45
|
+
}
|
|
46
|
+
get source() { return installSource(this.spec()); }
|
|
47
|
+
/** The latest known status; checks again at most every six hours (or now, when forced). */
|
|
48
|
+
async status(force = false) {
|
|
49
|
+
const age = this.checked ? this.now() - this.checked.at : Infinity;
|
|
50
|
+
if (this.state !== 'installing' && (force || age > (this.checked?.error ? HOUR / 2 : 6 * HOUR))) {
|
|
51
|
+
this.pending ??= this.check().finally(() => { this.pending = undefined; });
|
|
52
|
+
await this.pending;
|
|
53
|
+
}
|
|
54
|
+
return this.snapshot();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Install the newer version. With `reload`, TheOne then restarts itself so the new version runs
|
|
58
|
+
* without restarting DSH; otherwise it is loaded the next time DSH starts.
|
|
59
|
+
*/
|
|
60
|
+
async install(installer, reload) {
|
|
61
|
+
const status = await this.status();
|
|
62
|
+
if (!status.available || !status.installable || !installer || this.state === 'installing' || this.state === 'reloading' || this.state === 'restart')
|
|
63
|
+
return this.snapshot();
|
|
64
|
+
const spec = this.source === 'github' ? `github:${REPOSITORY}#${this.checked.sha}` : `${PACKAGE_NAME}@${this.checked.latest}`;
|
|
65
|
+
this.state = 'installing';
|
|
66
|
+
this.error = undefined;
|
|
67
|
+
try {
|
|
68
|
+
const result = await installer.installBundle(spec, { enabled: true });
|
|
69
|
+
if (result.application === 'applied' || result.application === 'restart-required') {
|
|
70
|
+
this.state = reload ? 'reloading' : 'restart';
|
|
71
|
+
reload?.(result.bundle ?? PACKAGE_NAME);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this.state = 'failed';
|
|
75
|
+
const error = result.error;
|
|
76
|
+
this.error = String(error?.code ?? error?.message ?? result.application).slice(0, 120);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
this.state = 'failed';
|
|
81
|
+
this.error = (error instanceof Error ? error.message : String(error)).slice(0, 120);
|
|
82
|
+
}
|
|
83
|
+
return this.snapshot();
|
|
84
|
+
}
|
|
85
|
+
snapshot() {
|
|
86
|
+
const latest = this.checked?.latest;
|
|
87
|
+
const source = this.source;
|
|
88
|
+
const available = !!latest && compareVersions(latest, this.current) > 0;
|
|
89
|
+
return { current: this.current, ...(latest ? { latest } : {}), available, source,
|
|
90
|
+
installable: available && (source === 'npm' || (source === 'github' && !!this.checked?.sha)),
|
|
91
|
+
...(this.state ? { state: this.state } : {}), ...(this.error ?? this.checked?.error ? { error: this.error ?? this.checked?.error } : {}) };
|
|
92
|
+
}
|
|
93
|
+
async check() {
|
|
94
|
+
const signal = AbortSignal.timeout(10000);
|
|
95
|
+
try {
|
|
96
|
+
if (this.source === 'npm') {
|
|
97
|
+
const response = await this.fetcher(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { signal });
|
|
98
|
+
if (!response.ok)
|
|
99
|
+
throw new Error(`npm ${response.status}`);
|
|
100
|
+
this.checked = { at: this.now(), latest: String((await response.json()).version ?? '') || undefined };
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// A GitHub install updates to the exact commit whose version was read.
|
|
104
|
+
const commit = await this.fetcher(`https://api.github.com/repos/${REPOSITORY}/commits/main`, { signal, headers: { accept: 'application/vnd.github.sha' } });
|
|
105
|
+
if (!commit.ok)
|
|
106
|
+
throw new Error(`GitHub ${commit.status}`);
|
|
107
|
+
const sha = (await commit.text()).trim();
|
|
108
|
+
if (!/^[0-9a-f]{40}$/.test(sha))
|
|
109
|
+
throw new Error('GitHub returned no commit');
|
|
110
|
+
const manifest = await this.fetcher(`https://raw.githubusercontent.com/${REPOSITORY}/${sha}/package.json`, { signal });
|
|
111
|
+
if (!manifest.ok)
|
|
112
|
+
throw new Error(`GitHub ${manifest.status}`);
|
|
113
|
+
const latest = String((await manifest.json()).version ?? '') || undefined;
|
|
114
|
+
this.checked = { at: this.now(), latest, sha };
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// Offline or rate-limited: keep what was known and try again later.
|
|
118
|
+
this.checked = { ...this.checked, at: this.now(), error: (error instanceof Error ? error.message : String(error)).slice(0, 120) };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
package/docs/plugin-v0.1.md
CHANGED
|
File without changes
|
package/docs/plugin-v0.1.zh.md
CHANGED
|
File without changes
|
package/docs/plugin-v0.2.md
CHANGED
|
File without changes
|
package/docs/plugin-v0.2.zh.md
CHANGED
|
File without changes
|