dsh-wisp 1.37.0 → 1.44.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 +426 -8
- package/cordis.patch.yml +19 -19
- package/lib/client.js +722 -27
- package/lib/client.template.js +722 -27
- package/lib/index.js +312 -28
- package/package.json +2 -1
- package/tools/engine-probe.mjs +421 -0
- package/tools/smoke.mjs +272 -0
- package/verify-wisp.mjs +4751 -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.44.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,105 @@ 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
|
+
const UPDATE_PACKAGE_NAME = 'dsh-wisp'
|
|
55
|
+
|
|
56
|
+
/** 一眼看懂一个"服务"到底是什么(诊断用;永远不抛)。 */
|
|
57
|
+
function describeService(value) {
|
|
58
|
+
if (value === undefined) return 'undefined'
|
|
59
|
+
if (value === null) return 'null'
|
|
60
|
+
const type = typeof value
|
|
61
|
+
if (type !== 'object' && type !== 'function') return type
|
|
62
|
+
try {
|
|
63
|
+
const keys = Object.keys(value).slice(0, 6).join(',')
|
|
64
|
+
return type + '{' + keys + '}' + (typeof value.fetch === 'function' ? '+fetch' : '-fetch')
|
|
65
|
+
} catch (error) { return type + '(unreadable)' }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the `web` service LATE, on every call — and say HOW, and what was found.
|
|
70
|
+
*
|
|
71
|
+
* `inject.optional: ['web']` hands us `ctx.web`, but only when the service was
|
|
72
|
+
* already provided at the moment this fiber materialized — and this half is
|
|
73
|
+
* inserted by a bundle patch, so it can start BEFORE the web service registers.
|
|
74
|
+
* An optional dependency that was absent at start does not retroactively appear,
|
|
75
|
+
* which would disable the update check forever. `ctx.get('web')` looks the
|
|
76
|
+
* service up at call time instead, so a startup race cannot wedge it.
|
|
77
|
+
*
|
|
78
|
+
* 真实运行里两种路都拿不到时,**必须能说清是哪一种**:`ctx.get` 有没有?
|
|
79
|
+
* 拿到的东西是什么形状?—— 没有这一层,"没有联网的通道"是一句无法追问的话。
|
|
80
|
+
*
|
|
81
|
+
* @returns `{ service, how, why }` — never throws.
|
|
82
|
+
* how: 'ctx.get' | 'ctx.web' | 'none' 拿到了没有、从哪拿的
|
|
83
|
+
* why: 一句话说明"看到了什么"(进了 doctor 与她的失败台词)
|
|
84
|
+
*/
|
|
85
|
+
export function resolveWebService(ctx) {
|
|
86
|
+
let late
|
|
87
|
+
let getThrew = null
|
|
88
|
+
try {
|
|
89
|
+
if (ctx && typeof ctx.get === 'function') late = ctx.get('web')
|
|
90
|
+
else getThrew = 'ctx.get 不是函数(' + describeService(ctx && ctx.get) + ')'
|
|
91
|
+
} catch (error) { getThrew = 'ctx.get 抛错:' + String(error && error.message ? error.message : error).slice(0, 60) }
|
|
92
|
+
if (late && typeof late.fetch === 'function') {
|
|
93
|
+
return { service: late, how: 'ctx.get', why: 'ctx.get("web")→' + describeService(late) }
|
|
94
|
+
}
|
|
95
|
+
let declared
|
|
96
|
+
let webThrew = null
|
|
97
|
+
try { declared = ctx && ctx.web } catch (error) { webThrew = String(error && error.message ? error.message : error).slice(0, 60) }
|
|
98
|
+
if (declared && typeof declared.fetch === 'function') {
|
|
99
|
+
return { service: declared, how: 'ctx.web', why: 'ctx.web→' + describeService(declared) }
|
|
100
|
+
}
|
|
101
|
+
const parts = []
|
|
102
|
+
parts.push('ctx.get("web")→' + describeService(late))
|
|
103
|
+
if (getThrew) parts.push(getThrew)
|
|
104
|
+
parts.push('ctx.web→' + describeService(declared))
|
|
105
|
+
if (webThrew) parts.push('ctx.web 读取抛错:' + webThrew)
|
|
106
|
+
/* 拿到了对象但没有 fetch 也要如实说 —— "有服务但没这个方法"和"根本没有服务"是两件事 */
|
|
107
|
+
const service = (late && typeof late === 'object') ? late : ((declared && typeof declared === 'object') ? declared : undefined)
|
|
108
|
+
if (service) {
|
|
109
|
+
return { service, how: 'partial', why: parts.join('; ') + '(无 fetch)' }
|
|
110
|
+
}
|
|
111
|
+
return { service: undefined, how: 'none', why: parts.join('; ') }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 兜底通道:向插件管理器问"这个包现在是什么版本"。
|
|
116
|
+
*
|
|
117
|
+
* 它本来就是**去 registry 问这个包**的那个人(安装前先 inspect),所以这条路
|
|
118
|
+
* 走的是应用自己那条网络出口、且尊重用户配的镜像源 —— 比我们自己去抓 packument
|
|
119
|
+
* 更接近"安装时会装到哪一版"。`web` 服务拿不到时,这是唯一还活着的更新通道。
|
|
120
|
+
*
|
|
121
|
+
* @returns `{ ok, version, registry }` 或 `{ ok: false, reason, detail }` — never throws.
|
|
122
|
+
*/
|
|
123
|
+
export async function readManagedVersion(ctx, name, timeoutMs) {
|
|
124
|
+
let manager
|
|
125
|
+
try {
|
|
126
|
+
if (!ctx || typeof ctx.get !== 'function') return { ok: false, reason: 'no-ctx-get' }
|
|
127
|
+
manager = ctx.get('pluginManager')
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return { ok: false, reason: 'manager-unreachable', detail: String(error && error.message ? error.message : error).slice(0, 80) }
|
|
130
|
+
}
|
|
131
|
+
if (!manager || typeof manager.inspect !== 'function') {
|
|
132
|
+
return { ok: false, reason: 'no-plugin-manager', detail: describeService(manager) }
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const info = await withTimeout(manager.inspect(name), timeoutMs)
|
|
136
|
+
if (info && info.status === 'accepted' && typeof info.version === 'string') {
|
|
137
|
+
return { ok: true, version: info.version, registry: info.registry ?? null }
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
reason: 'inspect-refused',
|
|
142
|
+
detail: info && info.reason ? String(info.reason).slice(0, 100) : describeService(info),
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return { ok: false, reason: 'inspect-failed', detail: String(error && error.message ? error.message : error).slice(0, 100) }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
43
149
|
/**
|
|
44
150
|
* Fetch one manifest and pull `version` out of it.
|
|
45
151
|
* @param web - the `web` service, or undefined when it is not loaded.
|
|
@@ -48,7 +154,10 @@ const CHECK_TIMEOUT_MS = 8000
|
|
|
48
154
|
* @returns `{ ok, version }` or `{ ok: false, reason }` — never throws.
|
|
49
155
|
*/
|
|
50
156
|
export async function readVersion(web, url, signal) {
|
|
51
|
-
if (!web
|
|
157
|
+
if (!web) return { ok: false, reason: 'no-web-service' }
|
|
158
|
+
/* "有服务但没有 fetch"和"根本没有服务"是两件事 —— 台词的下一步动作不同
|
|
159
|
+
(一个是应用/平台的问题,一个是插件注入的问题),所以别合并成一句。 */
|
|
160
|
+
if (typeof web.fetch !== 'function') return { ok: false, reason: 'web-has-no-fetch', detail: describeService(web) }
|
|
52
161
|
try {
|
|
53
162
|
const res = await web.fetch({ url }, signal)
|
|
54
163
|
if (!res || typeof res.statusCode !== 'number') return { ok: false, reason: 'bad-result' }
|
|
@@ -71,8 +180,7 @@ export async function readVersion(web, url, signal) {
|
|
|
71
180
|
}
|
|
72
181
|
}
|
|
73
182
|
|
|
74
|
-
/** Compare two dotted versions. Returns -1 / 0 / 1; non-numeric parts count as 0. */
|
|
75
|
-
export function compareVersions(a, b) {
|
|
183
|
+
/** Compare two dotted versions. Returns -1 / 0 / 1; non-numeric parts count as 0. */export function compareVersions(a, b) {
|
|
76
184
|
const parse = (v) => String(v).split('-')[0].split('.').map((n) => {
|
|
77
185
|
const x = Number.parseInt(n, 10)
|
|
78
186
|
return Number.isFinite(x) ? x : 0
|
|
@@ -91,12 +199,25 @@ export function compareVersions(a, b) {
|
|
|
91
199
|
* Read every source and report the highest version found.
|
|
92
200
|
* @param web - the `web` service.
|
|
93
201
|
* @param urls - `{ npm, github }` (or any other set of named sources).
|
|
94
|
-
* @param signal - optional cancellation signal.
|
|
202
|
+
* @param signal - optional cancellation signal shared by every source.
|
|
203
|
+
* @param timeoutMs - when given, EACH source gets its own deadline instead.
|
|
95
204
|
* @returns a plain, JSON-serializable verdict (it crosses the sandbox boundary).
|
|
96
205
|
*/
|
|
97
|
-
export async function readPublishedVersion(web, urls, signal) {
|
|
206
|
+
export async function readPublishedVersion(web, urls, signal, timeoutMs) {
|
|
98
207
|
const names = Object.keys(urls ?? {})
|
|
99
|
-
const
|
|
208
|
+
const perSource = typeof timeoutMs === 'number' && timeoutMs > 0
|
|
209
|
+
&& typeof AbortController === 'function' && typeof setTimeout === 'function'
|
|
210
|
+
const results = await Promise.all(names.map((key) => {
|
|
211
|
+
/* 每个源一个**自己的**截止时间。共用一个信号时,一个卡住的镜像会在到点时把
|
|
212
|
+
另一个已经拿到结果的源一起作废 —— 那次检查就整个失败,而其实 npm 是通的。
|
|
213
|
+
(实测:raw.githubusercontent.com 在这台机器上根本连不上。) */
|
|
214
|
+
if (!perSource) return readVersion(web, urls[key], signal)
|
|
215
|
+
const controller = new AbortController()
|
|
216
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
217
|
+
return readVersion(web, urls[key], controller.signal)
|
|
218
|
+
.then((result) => { clearTimeout(timer); return result })
|
|
219
|
+
.catch((error) => { clearTimeout(timer); throw error })
|
|
220
|
+
}))
|
|
100
221
|
const sources = {}
|
|
101
222
|
let best = null
|
|
102
223
|
let from = null
|
|
@@ -119,7 +240,132 @@ export async function readPublishedVersion(web, urls, signal) {
|
|
|
119
240
|
return { ok: true, latest: best, from, sources }
|
|
120
241
|
}
|
|
121
242
|
|
|
122
|
-
|
|
243
|
+
/* ---------------------------------------------------------------------------
|
|
244
|
+
ACCOUNT BALANCE.
|
|
245
|
+
|
|
246
|
+
The shape of the platform service, read from the live Host Service registry
|
|
247
|
+
rather than guessed:
|
|
248
|
+
|
|
249
|
+
ctx.deepseekAccount.getBalance(client: AccountClientMetadata)
|
|
250
|
+
-> { status: 'ready', value: AccountWallet[], bonusWallets: AccountWallet[] }
|
|
251
|
+
| { status: 'failed' }
|
|
252
|
+
| null // signed out, or the grant changed mid-query
|
|
253
|
+
ctx.deepseekAccount.getState()
|
|
254
|
+
-> { status: 'signed-out' | 'credential-stored', links, attempt }
|
|
255
|
+
|
|
256
|
+
AccountClientMetadata = { version, locale, timezoneOffsetSeconds }
|
|
257
|
+
AccountWallet = { currency: 'CNY' | 'USD', balance: string }
|
|
258
|
+
|
|
259
|
+
`null` deliberately means TWO different things, and telling the user the wrong
|
|
260
|
+
one is worse than saying nothing — so when it happens we ask getState() once
|
|
261
|
+
more to find out which. Signed out => "log in first"; still credential-stored
|
|
262
|
+
=> the grant moved under us => "didn't read it, try again".
|
|
263
|
+
--------------------------------------------------------------------------- */
|
|
264
|
+
|
|
265
|
+
/** How long a balance query may take before it is reported as a timeout. */
|
|
266
|
+
export const BALANCE_TIMEOUT_MS = 8000
|
|
267
|
+
|
|
268
|
+
/** Keep only well-formed wallets, and never let an unbounded list cross the sandbox. */
|
|
269
|
+
export function normalizeWallets(list) {
|
|
270
|
+
if (!Array.isArray(list)) return []
|
|
271
|
+
const out = []
|
|
272
|
+
for (const wallet of list) {
|
|
273
|
+
if (!wallet || typeof wallet !== 'object') continue
|
|
274
|
+
if (typeof wallet.balance !== 'string') continue
|
|
275
|
+
out.push({ currency: typeof wallet.currency === 'string' ? wallet.currency : '', balance: wallet.balance })
|
|
276
|
+
if (out.length >= 8) break
|
|
277
|
+
}
|
|
278
|
+
return out
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Settle with the platform call, or reject with `timeout` — whichever is first.
|
|
283
|
+
* The service takes no AbortSignal, so a race is the only way to bound it: an
|
|
284
|
+
* unbounded await here would mean she simply never says anything.
|
|
285
|
+
*/
|
|
286
|
+
function withTimeout(promise, ms) {
|
|
287
|
+
if (!(ms > 0) || typeof setTimeout !== 'function') return Promise.resolve(promise)
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
let settled = false
|
|
290
|
+
const timer = setTimeout(() => {
|
|
291
|
+
if (settled) return
|
|
292
|
+
settled = true
|
|
293
|
+
reject(new Error('timeout'))
|
|
294
|
+
}, ms)
|
|
295
|
+
const finish = (fn, value) => {
|
|
296
|
+
if (settled) return
|
|
297
|
+
settled = true
|
|
298
|
+
if (typeof clearTimeout === 'function') clearTimeout(timer)
|
|
299
|
+
fn(value)
|
|
300
|
+
}
|
|
301
|
+
Promise.resolve(promise).then(
|
|
302
|
+
(value) => finish(resolve, value),
|
|
303
|
+
(error) => finish(reject, error),
|
|
304
|
+
)
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Ask the platform for the account balance.
|
|
310
|
+
* @param account - the `deepseekAccount` service, or undefined when it is not loaded.
|
|
311
|
+
* @param client - `AccountClientMetadata` for the requesting UI.
|
|
312
|
+
* @param timeoutMs - override for tests; defaults to BALANCE_TIMEOUT_MS.
|
|
313
|
+
* @returns a plain, JSON-serializable verdict (it crosses the sandbox boundary) — never throws.
|
|
314
|
+
*/
|
|
315
|
+
export async function readBalance(account, client, timeoutMs) {
|
|
316
|
+
const limit = typeof timeoutMs === 'number' ? timeoutMs : BALANCE_TIMEOUT_MS
|
|
317
|
+
if (!account || typeof account.getBalance !== 'function') {
|
|
318
|
+
return { ok: false, reason: 'no-account-service' }
|
|
319
|
+
}
|
|
320
|
+
let result = null
|
|
321
|
+
try {
|
|
322
|
+
result = await withTimeout(account.getBalance(client), limit)
|
|
323
|
+
} catch (error) {
|
|
324
|
+
const detail = error && error.message ? String(error.message) : String(error)
|
|
325
|
+
return { ok: false, reason: detail === 'timeout' ? 'timeout' : 'call-failed', detail }
|
|
326
|
+
}
|
|
327
|
+
if (result === null || result === undefined) {
|
|
328
|
+
/* null 同时意味着"没登录"和"授权在查询途中换了"。分不清时**不许猜**:
|
|
329
|
+
只有 getState() 明确说 signed-out 才说"去登录",否则一律说"刚才没读到" ——
|
|
330
|
+
叫人去登录一个已经登录的账户,比说"没读到"糟得多。 */
|
|
331
|
+
let state = null
|
|
332
|
+
try {
|
|
333
|
+
if (typeof account.getState === 'function') {
|
|
334
|
+
state = await withTimeout(account.getState(), limit)
|
|
335
|
+
}
|
|
336
|
+
} catch (error) { state = null }
|
|
337
|
+
const signedIn = state && typeof state.status === 'string' ? state.status === 'credential-stored' : null
|
|
338
|
+
return {
|
|
339
|
+
ok: true,
|
|
340
|
+
status: signedIn === false ? 'signed-out' : 'unavailable',
|
|
341
|
+
signedIn,
|
|
342
|
+
wallets: [],
|
|
343
|
+
bonusWallets: [],
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (typeof result !== 'object' || result.status !== 'ready') {
|
|
347
|
+
/* The platform answered, and the answer was "failed" — a result, not a throw. */
|
|
348
|
+
return { ok: true, status: 'failed', wallets: [], bonusWallets: [] }
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
ok: true,
|
|
352
|
+
status: 'ready',
|
|
353
|
+
wallets: normalizeWallets(result.value),
|
|
354
|
+
bonusWallets: normalizeWallets(result.bonusWallets),
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Build the metadata every balance query carries. */
|
|
359
|
+
export function accountClientMetadata(locale) {
|
|
360
|
+
return {
|
|
361
|
+
version: VERSION,
|
|
362
|
+
locale: typeof locale === 'string' && locale !== '' ? locale : 'zh-CN',
|
|
363
|
+
/* The service wants seconds EAST of UTC; getTimezoneOffset() is minutes WEST. */
|
|
364
|
+
timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Register the handlers the browser half reaches through `host.call`. */
|
|
123
369
|
export function registerHandlers(ctx, options = {}) {
|
|
124
370
|
const urls = options.urls && typeof options.urls === 'object' ? options.urls : DEFAULT_SOURCES
|
|
125
371
|
/* `harness` is a sandbox global: the host half is evaluated inside a vm whose
|
|
@@ -131,30 +377,68 @@ export function registerHandlers(ctx, options = {}) {
|
|
|
131
377
|
}
|
|
132
378
|
const dispose = seat.handle('checkUpdate', async (args) => {
|
|
133
379
|
const current = args && typeof args.current === 'string' ? args.current : null
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
380
|
+
/* 服务**在调用时**解析,不用闭包里那个可能早就定格的 ctx.web(见 resolveWebService)。 */
|
|
381
|
+
const resolved = resolveWebService(ctx)
|
|
382
|
+
if (resolved.service) {
|
|
383
|
+
const verdict = await readPublishedVersion(resolved.service, urls, undefined, CHECK_TIMEOUT_MS)
|
|
384
|
+
return { ...verdict, current, diag: { via: 'web', how: resolved.how, why: resolved.why }, checkedAt: new Date().toISOString() }
|
|
139
385
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
386
|
+
/* 没有可用的 web 服务时的兜底:问插件管理器"这个包现在是什么版本"。
|
|
387
|
+
它走的是应用自己那条网络出口(安装前就要去 registry 问),所以这条路
|
|
388
|
+
是"安装时会装到哪一版"的权威答案 —— 而且不依赖插件能否拿到 web。 */
|
|
389
|
+
const managed = await readManagedVersion(ctx, UPDATE_PACKAGE_NAME, CHECK_TIMEOUT_MS)
|
|
390
|
+
const sources = { npm: managed.ok
|
|
391
|
+
? { ok: true, version: managed.version }
|
|
392
|
+
: { ok: false, reason: managed.reason, detail: managed.detail } }
|
|
393
|
+
if (managed.ok) {
|
|
394
|
+
return {
|
|
395
|
+
ok: true,
|
|
396
|
+
latest: managed.version,
|
|
397
|
+
from: 'npm',
|
|
398
|
+
sources,
|
|
399
|
+
current,
|
|
400
|
+
diag: { via: 'pluginManager', how: resolved.how, why: resolved.why, registry: managed.registry ?? null },
|
|
401
|
+
checkedAt: new Date().toISOString(),
|
|
402
|
+
}
|
|
145
403
|
}
|
|
404
|
+
sources.github = { ok: false, reason: 'no-web-service', detail: resolved.why }
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
reason: 'no-web-service',
|
|
408
|
+
sources,
|
|
409
|
+
current,
|
|
410
|
+
diag: { via: 'none', how: resolved.how, why: resolved.why, manager: managed.reason + (managed.detail ? ':' + managed.detail : '') },
|
|
411
|
+
checkedAt: new Date().toISOString(),
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
/* The balance handler. `ctx.deepseekAccount` is undefined unless the platform
|
|
415
|
+
provides it — readBalance() turns that into `no-account-service` rather than
|
|
416
|
+
a throw, so the browser half can say "not in this shell" honestly. */
|
|
417
|
+
const disposeBalance = seat.handle('checkBalance', async (args) => {
|
|
418
|
+
const verdict = await readBalance(
|
|
419
|
+
ctx && ctx.deepseekAccount,
|
|
420
|
+
accountClientMetadata(args && args.locale),
|
|
421
|
+
)
|
|
422
|
+
return { ...verdict, checkedAt: new Date().toISOString() }
|
|
146
423
|
})
|
|
147
|
-
return {
|
|
424
|
+
return {
|
|
425
|
+
registered: true,
|
|
426
|
+
dispose: () => {
|
|
427
|
+
if (typeof dispose === 'function') dispose()
|
|
428
|
+
if (typeof disposeBalance === 'function') disposeBalance()
|
|
429
|
+
},
|
|
430
|
+
urls,
|
|
431
|
+
}
|
|
148
432
|
}
|
|
149
433
|
|
|
150
434
|
export function apply(ctx, config) {
|
|
151
|
-
/* The body stays tiny: register the
|
|
152
|
-
fiber so unloading the package takes
|
|
435
|
+
/* The body stays tiny: register the handlers, and keep their disposer on the
|
|
436
|
+
fiber so unloading the package takes both of them with it. */
|
|
153
437
|
const result = registerHandlers(ctx, config || {})
|
|
154
438
|
if (result.registered && typeof ctx.effect === 'function') {
|
|
155
439
|
ctx.effect(() => () => {
|
|
156
440
|
if (typeof result.dispose === 'function') result.dispose()
|
|
157
|
-
}, 'dsh-wisp:
|
|
441
|
+
}, 'dsh-wisp: host handlers')
|
|
158
442
|
}
|
|
159
443
|
}
|
|
160
444
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-wisp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.44.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",
|