dsh-wisp 1.37.0 → 1.42.0
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 +344 -7
- package/cordis.patch.yml +14 -19
- package/lib/client.js +535 -17
- package/lib/client.template.js +535 -17
- package/lib/index.js +211 -29
- package/package.json +2 -1
- package/tools/engine-probe.mjs +421 -0
- package/tools/smoke.mjs +272 -0
- package/verify-wisp.mjs +4457 -3719
package/lib/index.js
CHANGED
|
@@ -10,9 +10,15 @@
|
|
|
10
10
|
// for a sandboxed half to reach a real capability, so the update check goes
|
|
11
11
|
// through it — nothing is bypassed.
|
|
12
12
|
//
|
|
13
|
-
// 2.
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// 2. Answer `host.call('checkBalance')` — the account balance, through the
|
|
14
|
+
// `deepseekAccount` service. NOT through our own HTTP call with an API key:
|
|
15
|
+
// that service is documented as "only Host consumers can obtain a request
|
|
16
|
+
// credential", which is exactly the right shape — the platform owns the
|
|
17
|
+
// login, the plugin never sees a token, and there is nothing to leak into
|
|
18
|
+
// the page (the browser half could not read a key even if we had one).
|
|
19
|
+
// `web` and `deepseekAccount` are both declared as OPTIONAL injects on
|
|
20
|
+
// purpose: a required inject that nothing provides would leave this fiber
|
|
21
|
+
// `waiting` forever, and the companion would simply disappear from the UI.
|
|
16
22
|
//
|
|
17
23
|
// WHY TWO SOURCES. npm's registry is the authoritative one, but a publish can sit
|
|
18
24
|
// in npm's staging queue for a long time (observed: `PUT 202`, still 404 after 18
|
|
@@ -25,11 +31,15 @@
|
|
|
25
31
|
/** Cordis plugin name used by loader diagnostics. */
|
|
26
32
|
export const name = 'wisp'
|
|
27
33
|
|
|
34
|
+
/** Stamped into every balance request. Must match package.json — the preflight guards it. */
|
|
35
|
+
export const VERSION = '1.42.0'
|
|
36
|
+
|
|
28
37
|
/**
|
|
29
|
-
* `web`
|
|
30
|
-
* the fiber in `waiting` forever, and the user's
|
|
38
|
+
* `web` and `deepseekAccount` are optional on purpose. A required inject that
|
|
39
|
+
* nothing provides keeps the fiber in `waiting` forever, and the user's
|
|
40
|
+
* companion would vanish.
|
|
31
41
|
*/
|
|
32
|
-
export const inject = { required: [], optional: ['web'] }
|
|
42
|
+
export const inject = { required: [], optional: ['web', 'deepseekAccount'] }
|
|
33
43
|
|
|
34
44
|
/** The two places a published version can be read from. Overridable per install. */
|
|
35
45
|
export const DEFAULT_SOURCES = {
|
|
@@ -37,9 +47,35 @@ export const DEFAULT_SOURCES = {
|
|
|
37
47
|
github: 'https://raw.githubusercontent.com/969246694/dsh-wisp/main/package.json',
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
/** How long
|
|
50
|
+
/** How long ONE source may take before it is reported as a timeout. */
|
|
41
51
|
const CHECK_TIMEOUT_MS = 8000
|
|
42
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the `web` service LATE, on every call.
|
|
55
|
+
*
|
|
56
|
+
* `inject.optional: ['web']` hands us `ctx.web`, but only when the service was
|
|
57
|
+
* already provided at the moment this fiber materialized — and this half is
|
|
58
|
+
* inserted by a bundle patch, so it can start BEFORE the web service registers.
|
|
59
|
+
* An optional dependency that was absent at start does not retroactively appear,
|
|
60
|
+
* which would disable the update check forever. `ctx.get('web')` looks the
|
|
61
|
+
* service up at call time instead, so a startup race cannot wedge it.
|
|
62
|
+
*
|
|
63
|
+
* @returns the service, or undefined when this shell really has none.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveWebService(ctx) {
|
|
66
|
+
try {
|
|
67
|
+
if (ctx && typeof ctx.get === 'function') {
|
|
68
|
+
const late = ctx.get('web')
|
|
69
|
+
if (late && typeof late.fetch === 'function') return late
|
|
70
|
+
}
|
|
71
|
+
} catch (error) { /* restricted context: fall through to the declared property */ }
|
|
72
|
+
try {
|
|
73
|
+
const declared = ctx && ctx.web
|
|
74
|
+
if (declared && typeof declared.fetch === 'function') return declared
|
|
75
|
+
} catch (error) { /* no such property */ }
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
43
79
|
/**
|
|
44
80
|
* Fetch one manifest and pull `version` out of it.
|
|
45
81
|
* @param web - the `web` service, or undefined when it is not loaded.
|
|
@@ -71,8 +107,7 @@ export async function readVersion(web, url, signal) {
|
|
|
71
107
|
}
|
|
72
108
|
}
|
|
73
109
|
|
|
74
|
-
/** Compare two dotted versions. Returns -1 / 0 / 1; non-numeric parts count as 0. */
|
|
75
|
-
export function compareVersions(a, b) {
|
|
110
|
+
/** Compare two dotted versions. Returns -1 / 0 / 1; non-numeric parts count as 0. */export function compareVersions(a, b) {
|
|
76
111
|
const parse = (v) => String(v).split('-')[0].split('.').map((n) => {
|
|
77
112
|
const x = Number.parseInt(n, 10)
|
|
78
113
|
return Number.isFinite(x) ? x : 0
|
|
@@ -91,12 +126,25 @@ export function compareVersions(a, b) {
|
|
|
91
126
|
* Read every source and report the highest version found.
|
|
92
127
|
* @param web - the `web` service.
|
|
93
128
|
* @param urls - `{ npm, github }` (or any other set of named sources).
|
|
94
|
-
* @param signal - optional cancellation signal.
|
|
129
|
+
* @param signal - optional cancellation signal shared by every source.
|
|
130
|
+
* @param timeoutMs - when given, EACH source gets its own deadline instead.
|
|
95
131
|
* @returns a plain, JSON-serializable verdict (it crosses the sandbox boundary).
|
|
96
132
|
*/
|
|
97
|
-
export async function readPublishedVersion(web, urls, signal) {
|
|
133
|
+
export async function readPublishedVersion(web, urls, signal, timeoutMs) {
|
|
98
134
|
const names = Object.keys(urls ?? {})
|
|
99
|
-
const
|
|
135
|
+
const perSource = typeof timeoutMs === 'number' && timeoutMs > 0
|
|
136
|
+
&& typeof AbortController === 'function' && typeof setTimeout === 'function'
|
|
137
|
+
const results = await Promise.all(names.map((key) => {
|
|
138
|
+
/* 每个源一个**自己的**截止时间。共用一个信号时,一个卡住的镜像会在到点时把
|
|
139
|
+
另一个已经拿到结果的源一起作废 —— 那次检查就整个失败,而其实 npm 是通的。
|
|
140
|
+
(实测:raw.githubusercontent.com 在这台机器上根本连不上。) */
|
|
141
|
+
if (!perSource) return readVersion(web, urls[key], signal)
|
|
142
|
+
const controller = new AbortController()
|
|
143
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
144
|
+
return readVersion(web, urls[key], controller.signal)
|
|
145
|
+
.then((result) => { clearTimeout(timer); return result })
|
|
146
|
+
.catch((error) => { clearTimeout(timer); throw error })
|
|
147
|
+
}))
|
|
100
148
|
const sources = {}
|
|
101
149
|
let best = null
|
|
102
150
|
let from = null
|
|
@@ -119,7 +167,132 @@ export async function readPublishedVersion(web, urls, signal) {
|
|
|
119
167
|
return { ok: true, latest: best, from, sources }
|
|
120
168
|
}
|
|
121
169
|
|
|
122
|
-
|
|
170
|
+
/* ---------------------------------------------------------------------------
|
|
171
|
+
ACCOUNT BALANCE.
|
|
172
|
+
|
|
173
|
+
The shape of the platform service, read from the live Host Service registry
|
|
174
|
+
rather than guessed:
|
|
175
|
+
|
|
176
|
+
ctx.deepseekAccount.getBalance(client: AccountClientMetadata)
|
|
177
|
+
-> { status: 'ready', value: AccountWallet[], bonusWallets: AccountWallet[] }
|
|
178
|
+
| { status: 'failed' }
|
|
179
|
+
| null // signed out, or the grant changed mid-query
|
|
180
|
+
ctx.deepseekAccount.getState()
|
|
181
|
+
-> { status: 'signed-out' | 'credential-stored', links, attempt }
|
|
182
|
+
|
|
183
|
+
AccountClientMetadata = { version, locale, timezoneOffsetSeconds }
|
|
184
|
+
AccountWallet = { currency: 'CNY' | 'USD', balance: string }
|
|
185
|
+
|
|
186
|
+
`null` deliberately means TWO different things, and telling the user the wrong
|
|
187
|
+
one is worse than saying nothing — so when it happens we ask getState() once
|
|
188
|
+
more to find out which. Signed out => "log in first"; still credential-stored
|
|
189
|
+
=> the grant moved under us => "didn't read it, try again".
|
|
190
|
+
--------------------------------------------------------------------------- */
|
|
191
|
+
|
|
192
|
+
/** How long a balance query may take before it is reported as a timeout. */
|
|
193
|
+
export const BALANCE_TIMEOUT_MS = 8000
|
|
194
|
+
|
|
195
|
+
/** Keep only well-formed wallets, and never let an unbounded list cross the sandbox. */
|
|
196
|
+
export function normalizeWallets(list) {
|
|
197
|
+
if (!Array.isArray(list)) return []
|
|
198
|
+
const out = []
|
|
199
|
+
for (const wallet of list) {
|
|
200
|
+
if (!wallet || typeof wallet !== 'object') continue
|
|
201
|
+
if (typeof wallet.balance !== 'string') continue
|
|
202
|
+
out.push({ currency: typeof wallet.currency === 'string' ? wallet.currency : '', balance: wallet.balance })
|
|
203
|
+
if (out.length >= 8) break
|
|
204
|
+
}
|
|
205
|
+
return out
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Settle with the platform call, or reject with `timeout` — whichever is first.
|
|
210
|
+
* The service takes no AbortSignal, so a race is the only way to bound it: an
|
|
211
|
+
* unbounded await here would mean she simply never says anything.
|
|
212
|
+
*/
|
|
213
|
+
function withTimeout(promise, ms) {
|
|
214
|
+
if (!(ms > 0) || typeof setTimeout !== 'function') return Promise.resolve(promise)
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
let settled = false
|
|
217
|
+
const timer = setTimeout(() => {
|
|
218
|
+
if (settled) return
|
|
219
|
+
settled = true
|
|
220
|
+
reject(new Error('timeout'))
|
|
221
|
+
}, ms)
|
|
222
|
+
const finish = (fn, value) => {
|
|
223
|
+
if (settled) return
|
|
224
|
+
settled = true
|
|
225
|
+
if (typeof clearTimeout === 'function') clearTimeout(timer)
|
|
226
|
+
fn(value)
|
|
227
|
+
}
|
|
228
|
+
Promise.resolve(promise).then(
|
|
229
|
+
(value) => finish(resolve, value),
|
|
230
|
+
(error) => finish(reject, error),
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Ask the platform for the account balance.
|
|
237
|
+
* @param account - the `deepseekAccount` service, or undefined when it is not loaded.
|
|
238
|
+
* @param client - `AccountClientMetadata` for the requesting UI.
|
|
239
|
+
* @param timeoutMs - override for tests; defaults to BALANCE_TIMEOUT_MS.
|
|
240
|
+
* @returns a plain, JSON-serializable verdict (it crosses the sandbox boundary) — never throws.
|
|
241
|
+
*/
|
|
242
|
+
export async function readBalance(account, client, timeoutMs) {
|
|
243
|
+
const limit = typeof timeoutMs === 'number' ? timeoutMs : BALANCE_TIMEOUT_MS
|
|
244
|
+
if (!account || typeof account.getBalance !== 'function') {
|
|
245
|
+
return { ok: false, reason: 'no-account-service' }
|
|
246
|
+
}
|
|
247
|
+
let result = null
|
|
248
|
+
try {
|
|
249
|
+
result = await withTimeout(account.getBalance(client), limit)
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const detail = error && error.message ? String(error.message) : String(error)
|
|
252
|
+
return { ok: false, reason: detail === 'timeout' ? 'timeout' : 'call-failed', detail }
|
|
253
|
+
}
|
|
254
|
+
if (result === null || result === undefined) {
|
|
255
|
+
/* null 同时意味着"没登录"和"授权在查询途中换了"。分不清时**不许猜**:
|
|
256
|
+
只有 getState() 明确说 signed-out 才说"去登录",否则一律说"刚才没读到" ——
|
|
257
|
+
叫人去登录一个已经登录的账户,比说"没读到"糟得多。 */
|
|
258
|
+
let state = null
|
|
259
|
+
try {
|
|
260
|
+
if (typeof account.getState === 'function') {
|
|
261
|
+
state = await withTimeout(account.getState(), limit)
|
|
262
|
+
}
|
|
263
|
+
} catch (error) { state = null }
|
|
264
|
+
const signedIn = state && typeof state.status === 'string' ? state.status === 'credential-stored' : null
|
|
265
|
+
return {
|
|
266
|
+
ok: true,
|
|
267
|
+
status: signedIn === false ? 'signed-out' : 'unavailable',
|
|
268
|
+
signedIn,
|
|
269
|
+
wallets: [],
|
|
270
|
+
bonusWallets: [],
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (typeof result !== 'object' || result.status !== 'ready') {
|
|
274
|
+
/* The platform answered, and the answer was "failed" — a result, not a throw. */
|
|
275
|
+
return { ok: true, status: 'failed', wallets: [], bonusWallets: [] }
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
status: 'ready',
|
|
280
|
+
wallets: normalizeWallets(result.value),
|
|
281
|
+
bonusWallets: normalizeWallets(result.bonusWallets),
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Build the metadata every balance query carries. */
|
|
286
|
+
export function accountClientMetadata(locale) {
|
|
287
|
+
return {
|
|
288
|
+
version: VERSION,
|
|
289
|
+
locale: typeof locale === 'string' && locale !== '' ? locale : 'zh-CN',
|
|
290
|
+
/* The service wants seconds EAST of UTC; getTimezoneOffset() is minutes WEST. */
|
|
291
|
+
timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Register the handlers the browser half reaches through `host.call`. */
|
|
123
296
|
export function registerHandlers(ctx, options = {}) {
|
|
124
297
|
const urls = options.urls && typeof options.urls === 'object' ? options.urls : DEFAULT_SOURCES
|
|
125
298
|
/* `harness` is a sandbox global: the host half is evaluated inside a vm whose
|
|
@@ -131,30 +304,39 @@ export function registerHandlers(ctx, options = {}) {
|
|
|
131
304
|
}
|
|
132
305
|
const dispose = seat.handle('checkUpdate', async (args) => {
|
|
133
306
|
const current = args && typeof args.current === 'string' ? args.current : null
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
timer = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS)
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
const verdict = await readPublishedVersion(ctx && ctx.web, urls, controller ? controller.signal : undefined)
|
|
142
|
-
return { ...verdict, current, checkedAt: new Date().toISOString() }
|
|
143
|
-
} finally {
|
|
144
|
-
if (timer !== null && typeof clearTimeout === 'function') clearTimeout(timer)
|
|
145
|
-
}
|
|
307
|
+
/* 服务**在调用时**解析,不用闭包里那个可能早就定格的 ctx.web(见 resolveWebService)。 */
|
|
308
|
+
const web = resolveWebService(ctx)
|
|
309
|
+
const verdict = await readPublishedVersion(web, urls, undefined, CHECK_TIMEOUT_MS)
|
|
310
|
+
return { ...verdict, current, checkedAt: new Date().toISOString() }
|
|
146
311
|
})
|
|
147
|
-
|
|
312
|
+
/* The balance handler. `ctx.deepseekAccount` is undefined unless the platform
|
|
313
|
+
provides it — readBalance() turns that into `no-account-service` rather than
|
|
314
|
+
a throw, so the browser half can say "not in this shell" honestly. */
|
|
315
|
+
const disposeBalance = seat.handle('checkBalance', async (args) => {
|
|
316
|
+
const verdict = await readBalance(
|
|
317
|
+
ctx && ctx.deepseekAccount,
|
|
318
|
+
accountClientMetadata(args && args.locale),
|
|
319
|
+
)
|
|
320
|
+
return { ...verdict, checkedAt: new Date().toISOString() }
|
|
321
|
+
})
|
|
322
|
+
return {
|
|
323
|
+
registered: true,
|
|
324
|
+
dispose: () => {
|
|
325
|
+
if (typeof dispose === 'function') dispose()
|
|
326
|
+
if (typeof disposeBalance === 'function') disposeBalance()
|
|
327
|
+
},
|
|
328
|
+
urls,
|
|
329
|
+
}
|
|
148
330
|
}
|
|
149
331
|
|
|
150
332
|
export function apply(ctx, config) {
|
|
151
|
-
/* The body stays tiny: register the
|
|
152
|
-
fiber so unloading the package takes
|
|
333
|
+
/* The body stays tiny: register the handlers, and keep their disposer on the
|
|
334
|
+
fiber so unloading the package takes both of them with it. */
|
|
153
335
|
const result = registerHandlers(ctx, config || {})
|
|
154
336
|
if (result.registered && typeof ctx.effect === 'function') {
|
|
155
337
|
ctx.effect(() => () => {
|
|
156
338
|
if (typeof result.dispose === 'function') result.dispose()
|
|
157
|
-
}, 'dsh-wisp:
|
|
339
|
+
}, 'dsh-wisp: host handlers')
|
|
158
340
|
}
|
|
159
341
|
}
|
|
160
342
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-wisp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.42.0",
|
|
4
4
|
"description": "DeepSeek娘 — an unofficial floating desktop companion for the DeepSeek Harness UI. Zero dependencies; sprites inlined as data URIs; schedules on the Client timer service instead of the trapped browser timer globals.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"build.mjs",
|
|
32
32
|
"verify-wisp.mjs",
|
|
33
33
|
"tools/smoke.mjs",
|
|
34
|
+
"tools/engine-probe.mjs",
|
|
34
35
|
"tools/keyout.mjs",
|
|
35
36
|
"tools/assets.mjs",
|
|
36
37
|
"tools/tiers.mjs",
|