dsh-plugin-weapp-gateway 0.8.13 → 0.8.23

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.
@@ -0,0 +1,356 @@
1
+ // Ported from corrinehu/dsh-workbuddy-connect (MIT) src/auth.ts + src/upstream.ts.
2
+ // WorkBuddy 积分余额来源(第 14 家 coding plan provider):凭据解析 + billing 查询。
3
+ // 优先使用同一宿主的 connect 状态接口,由连接插件处理加密登录文件。
4
+ // 接口不存在时兼容旧版明文文件;本模块不刷新令牌,也不写入登录文件。
5
+
6
+ import { readFileSync } from 'node:fs'
7
+ import { homedir } from 'node:os'
8
+ import { join } from 'node:path'
9
+ import { resolveDshHome } from './home-paths-shim.mjs'
10
+
11
+ import { fetchWithRetry, readJsonBounded } from './net.mjs'
12
+ import { fetchWorkBuddyLocal, workBuddyOrigin } from './workbuddy-transport.mjs'
13
+ import { validWorkBuddyAccountId } from './workbuddy-accounts.mjs'
14
+
15
+ export const WORKBUDDY_AUTH_FILENAME = '.workbuddy-auth.json'
16
+ export const WORKBUDDY_AUTH_FILE_ENV = 'WORKBUDDY_AUTH_FILE'
17
+ // 桌面端登录文件相对路径(connect 插件 DESKTOP_AUTH_RELATIVE_PATH 同款)。
18
+ export const WORKBUDDY_DESKTOP_RELATIVE_PATH = ['CodeBuddyExtension', 'Data', 'Public', 'auth', 'workbuddy-desktop.info']
19
+
20
+ // 本模块抛出的错误携带的专属错误码(lib/index.js SERVER_MESSAGES 同名文案键)。
21
+ export const WORKBUDDY_ERROR_CODES = ['workbuddyNotInstalled', 'workbuddyAuthEncrypted', 'workbuddyTokenExpired']
22
+
23
+ const CODEBUDZY_BILLING_BASE = 'https://www.codebuddy.cn'
24
+ const WORKBUDDY_BILLING_BASE = 'https://www.workbuddy.ai'
25
+
26
+ /** Region for a login domain; an empty domain means CN (matching upstream tooling). */
27
+ export function regionOf(domain) {
28
+ const lowered = String(domain ?? '').trim().toLowerCase()
29
+ if (lowered === 'workbuddy.ai' || lowered.endsWith('.workbuddy.ai')) return 'global'
30
+ return 'cn'
31
+ }
32
+
33
+ /** Billing base for a credential's domain. */
34
+ export function workbuddyBillingBase(credential) {
35
+ return regionOf(credential.domain) === 'global' ? WORKBUDDY_BILLING_BASE : CODEBUDZY_BILLING_BASE
36
+ }
37
+
38
+ /** Billing request headers(port 自 upstream.ts billingHeaders)。 */
39
+ export function billingHeadersOf(credential) {
40
+ const headers = {
41
+ authorization: `Bearer ${credential.accessToken}`,
42
+ accept: 'application/json',
43
+ 'content-type': 'application/json',
44
+ }
45
+ if (credential.uid !== '') headers['x-user-id'] = credential.uid
46
+ if (credential.enterpriseId !== undefined && credential.enterpriseId !== '') {
47
+ headers['x-enterprise-id'] = credential.enterpriseId
48
+ headers['x-tenant-id'] = credential.enterpriseId
49
+ }
50
+ if (credential.domain !== '') headers['x-domain'] = credential.domain
51
+ return headers
52
+ }
53
+
54
+ /** Normalize an expiry that may arrive in seconds or milliseconds. */
55
+ function expiryToMs(value) {
56
+ if (!Number.isFinite(value) || value <= 0) return 0
57
+ return value > 1e12 ? value : value * 1000
58
+ }
59
+
60
+ function optionalString(value) {
61
+ return typeof value === 'string' && value !== '' ? value : undefined
62
+ }
63
+
64
+ /** token 已过期判定(expiresAtMs 缺失视为未知,不算过期)。 */
65
+ export function isWorkBuddyTokenExpired(credential, nowMs = Date.now()) {
66
+ return credential?.expiresAtMs > 0 && nowMs >= credential.expiresAtMs
67
+ }
68
+
69
+ /**
70
+ * 解析 WorkBuddy 登录文档,兼容三种在盘形态(connect src/auth.ts):
71
+ * - 插件自有副本 {version:1, credential:{accessToken, expiresAtMs(毫秒), refreshToken, domain, uid, enterpriseId, nickname}};
72
+ * - 桌面端嵌套 {auth:{accessToken, expiresAt(秒或毫秒), refreshToken, domain}, account:{uid, enterpriseId, nickname}};
73
+ * - 扁平(顶层同名字段)。
74
+ * 无法解析或无 accessToken → undefined;加密/无法识别由调用方分类。
75
+ */
76
+ export function parseWorkBuddyAuth(text) {
77
+ let parsed
78
+ try { parsed = JSON.parse(text) } catch { return undefined }
79
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined
80
+ const document = parsed
81
+ let auth, identity
82
+ if (document.auth !== null && typeof document.auth === 'object') {
83
+ auth = document.auth
84
+ identity = document.account !== null && typeof document.account === 'object' ? document.account : {}
85
+ } else if (document.credential !== null && typeof document.credential === 'object' && document.version === 1) {
86
+ // 插件自有副本:credential 顶层即归一化形态(camelCase expiresAtMs)。
87
+ auth = document.credential
88
+ identity = document.credential
89
+ } else {
90
+ auth = document
91
+ identity = document
92
+ }
93
+ const accessToken = typeof auth.accessToken === 'string' ? auth.accessToken : ''
94
+ if (accessToken === '') return undefined
95
+ const expiresAtMs = typeof auth.expiresAtMs === 'number'
96
+ ? (auth.expiresAtMs > 0 ? auth.expiresAtMs : 0)
97
+ : (typeof auth.expiresAt === 'number' ? expiryToMs(auth.expiresAt) : 0)
98
+ const enterpriseId = optionalString(identity.enterpriseId)
99
+ const nickname = optionalString(identity.nickname)
100
+ return {
101
+ accessToken,
102
+ refreshToken: typeof auth.refreshToken === 'string' ? auth.refreshToken : '',
103
+ expiresAtMs,
104
+ domain: optionalString(auth.domain) ?? '',
105
+ uid: optionalString(identity.uid) ?? '',
106
+ ...(enterpriseId === undefined ? {} : { enterpriseId }),
107
+ ...(nickname === undefined ? {} : { nickname }),
108
+ }
109
+ }
110
+
111
+ /** 检测登录文档是否加密/无法识别(WorkBuddy 5.6 的 $wbEncrypted 封装痕迹)。 */
112
+ export function isWorkBuddyAuthEncrypted(text) {
113
+ let parsed
114
+ try { parsed = JSON.parse(text) } catch { return true } // 无法解析即无法识别
115
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return true
116
+ const auth = parsed.auth !== null && typeof parsed.auth === 'object' ? parsed.auth : parsed.version === 1 && parsed.credential ? parsed.credential : parsed
117
+ for (const field of ['accessToken', 'refreshToken']) {
118
+ const value = auth[field]
119
+ if (value === undefined) continue
120
+ if (typeof value === 'string') continue
121
+ if (value !== null && typeof value === 'object' && value.$wbEncrypted !== undefined) return true
122
+ return true // 非字符串的非空字段:本插件不支持的结构,按加密/无法识别处理
123
+ }
124
+ return false
125
+ }
126
+
127
+ /** 按平台列出桌面端登录文件候选(顺序对齐 connect defaultDesktopAuthCandidates)。 */
128
+ export function workbuddyDesktopAuthCandidates(home = homedir(), env = process.env, platform = process.platform) {
129
+ const rel = WORKBUDDY_DESKTOP_RELATIVE_PATH
130
+ if (platform === 'darwin') return [join(home, 'Library', 'Application Support', ...rel)]
131
+ if (platform === 'win32') {
132
+ return [join(home, 'AppData', 'Local', ...rel), join(home, 'AppData', 'Roaming', ...rel)]
133
+ }
134
+ if (platform === 'linux') {
135
+ const configHome = typeof env.XDG_CONFIG_HOME === 'string' && env.XDG_CONFIG_HOME.startsWith('/') ? env.XDG_CONFIG_HOME : join(home, '.config')
136
+ const dataHome = typeof env.XDG_DATA_HOME === 'string' && env.XDG_DATA_HOME.startsWith('/') ? env.XDG_DATA_HOME : join(home, '.local', 'share')
137
+ return [join(configHome, ...rel), join(dataHome, ...rel)]
138
+ }
139
+ return []
140
+ }
141
+
142
+ /** 解析 DSH home:宿主 peer 依赖优先,失败时按 env → ~/.dsh → 旧约定回退。 */
143
+ /** 汇总全部候选路径:env 覆盖 → connect 刷新副本 → 桌面端候选。 */
144
+ export function workbuddyAuthCandidates(env = process.env) {
145
+ const candidates = []
146
+ const override = String(env[WORKBUDDY_AUTH_FILE_ENV] ?? '').trim()
147
+ if (override.length > 0) candidates.push(override)
148
+ candidates.push(join(resolveDshHome(), WORKBUDDY_AUTH_FILENAME))
149
+ candidates.push(...workbuddyDesktopAuthCandidates(homedir(), env))
150
+ return candidates
151
+ }
152
+
153
+ function workbuddyError(code, detail = '') {
154
+ const error = new Error(detail.length > 0 ? `${code}: ${detail}` : code)
155
+ error.code = code
156
+ return error
157
+ }
158
+
159
+ /** Only contact the running DSH server; never send credentials or follow redirects. */
160
+ export async function fetchWorkBuddyConnectCredits(hostOrigin, { signal, fetchImpl = fetchWorkBuddyLocal } = {}) {
161
+ if (!hostOrigin) return null
162
+ const origin = workBuddyOrigin(hostOrigin)
163
+ let signedOut = false, failure
164
+ for (const path of ['/plugins/dsh-workbuddy-connect/status', '/plugins/dsh-workbuddy-connect/ai/status',
165
+ '/plugins/dsh-connect-workbuddy/usage?region=cn', '/plugins/dsh-connect-workbuddy/usage?region=global']) {
166
+ let response, status
167
+ try {
168
+ response = await fetchWithRetry(new URL(path, origin), {
169
+ method: 'GET', redirect: 'error', headers: { accept: 'application/json' }, signal,
170
+ }, { timeoutMs: 35000, attempts: 1, fetchImpl })
171
+ if (response.status === 404) { await response.body?.cancel?.(); continue }
172
+ if (!response.ok) { await response.body?.cancel?.(); failure ??= workbuddyError('workbuddyConnectUnavailable'); continue }
173
+ // Some host versions serve the SPA shell for an unregistered plugin route.
174
+ if (response.headers.get('content-type')?.includes('text/html')) { await response.body?.cancel?.(); continue }
175
+ status = await readJsonBounded(response)
176
+ } catch (error) {
177
+ signal?.throwIfAborted()
178
+ failure ??= error
179
+ continue
180
+ }
181
+ signal?.throwIfAborted()
182
+ if (status?.enabled === false) { signedOut = true; continue }
183
+ if (status?.status === 'signed-out') {
184
+ signedOut = true
185
+ if (status.reasonCode && status.reasonCode !== 'no-credential' || status.selectionLost) failure ??= workbuddyError('workbuddyConnectLoginFailed')
186
+ continue
187
+ }
188
+ if (status?.status !== 'signed-in' || status.credentialRejected === true) { failure ??= workbuddyError('workbuddyConnectLoginFailed'); continue }
189
+ const credits = status.credits
190
+ if (!credits || typeof credits.total !== 'number' || !Number.isFinite(credits.total) || credits.total < 0
191
+ || credits.unlimited !== undefined && typeof credits.unlimited !== 'boolean') { failure ??= workbuddyError('workbuddyConnectCreditsFailed'); continue }
192
+ const expiry = typeof credits.nearestExpiryMs === 'number' && credits.nearestExpiryMs > 0 && credits.nearestExpiryMs <= 8640000000000000
193
+ ? new Date(credits.nearestExpiryMs).toISOString() : undefined
194
+ // Copy only billing fields. Status also contains identity and control keys.
195
+ return { total: credits.total, unlimited: credits.unlimited === true,
196
+ ...(typeof credits.cycleResetTime === 'string' ? { cycleResetTime: credits.cycleResetTime } : expiry ? { cycleResetTime: expiry } : {}),
197
+ ...(Array.isArray(status.accounts) ? { workbuddy: {
198
+ accounts: status.accounts.filter(row => typeof row?.id === 'string' && validWorkBuddyAccountId('cn:' + row.id)).slice(0, 200).map(row => ({
199
+ id: (path.includes('region=global') ? 'global:' : 'cn:') + row.id,
200
+ name: typeof row.accountName === 'string' ? row.accountName.slice(0, 160) : '',
201
+ region: path.includes('region=global') ? 'global' : 'cn',
202
+ current: row.selected === true,
203
+ remaining: row.id === status.accountId ? credits.total : null,
204
+ })), scope: 'current', incomplete: false,
205
+ } } : {}),
206
+ endpoint: new URL(path, origin).href }
207
+ }
208
+ if (failure) throw failure
209
+ // A signed-out connector is authoritative; an old plaintext copy may belong to another account.
210
+ if (signedOut) throw workbuddyError('workbuddyNotInstalled')
211
+ return null
212
+ }
213
+
214
+ /**
215
+ * 定位并解析 WorkBuddy 凭据;全部候选缺失 → 抛 workbuddyNotInstalled;
216
+ * 命中的文件加密/无法识别 → 抛 workbuddyAuthEncrypted;
217
+ * 文件存在但无 accessToken(已登出)→ 视为缺失继续下一候选。
218
+ * 返回 parseWorkBuddyAuth 的结果。
219
+ */
220
+ export function resolveWorkBuddyCredential({ readFile = readFileSync, env = process.env } = {}) {
221
+ const candidates = workbuddyAuthCandidates(env)
222
+ let encrypted = false, expired
223
+ for (const path of candidates) {
224
+ let text
225
+ try { text = readFile(path, 'utf8') } catch { continue }
226
+ if (typeof text !== 'string' || text.trim() === '') continue
227
+ if (isWorkBuddyAuthEncrypted(text)) { encrypted = true; continue }
228
+ const credential = parseWorkBuddyAuth(text)
229
+ if (credential === undefined) continue
230
+ if (isWorkBuddyTokenExpired(credential)) { expired ??= credential; continue }
231
+ return credential
232
+ }
233
+ if (expired) return expired
234
+ if (encrypted) throw workbuddyError('workbuddyAuthEncrypted')
235
+ throw workbuddyError('workbuddyNotInstalled')
236
+ }
237
+
238
+ /** 个人积分响应解析(纯函数):data.Response.Data.Accounts[] → {total, accounts}。 */
239
+ export function parseWorkBuddyPersonalCredits(body) {
240
+ const accounts = body?.data?.Response?.Data?.Accounts
241
+ if (!Array.isArray(accounts)) return null
242
+ let total = 0
243
+ const parsed = []
244
+ for (const raw of accounts) {
245
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) throw workbuddyError('workbuddyInvalidCredits')
246
+ const numberField = key => {
247
+ if (raw[key] === undefined) return undefined
248
+ if (typeof raw[key] !== 'number' || !Number.isFinite(raw[key])) throw workbuddyError('workbuddyInvalidCredits')
249
+ return raw[key]
250
+ }
251
+ const size = numberField('CycleCapacitySize')
252
+ const cycleRemain = numberField('CycleCapacityRemain')
253
+ const cycleUsed = numberField('CycleCapacityUsed')
254
+ const capacityRemain = numberField('CapacityRemain')
255
+ let remain
256
+ if (size > 0) remain = cycleRemain
257
+ else if (cycleRemain > 0 || cycleUsed > 0 || capacityRemain === undefined) remain = cycleRemain
258
+ else remain = capacityRemain
259
+ if (!Number.isFinite(remain)) throw workbuddyError('workbuddyInvalidCredits')
260
+ if (remain < 0) remain = 0
261
+ total += remain
262
+ if (!Number.isFinite(total)) throw workbuddyError('workbuddyInvalidCredits')
263
+ parsed.push({
264
+ packageName: typeof raw.PackageName === 'string' ? raw.PackageName : '(unnamed)',
265
+ remain,
266
+ size: size > 0 ? size : numberField('CapacitySize'),
267
+ })
268
+ }
269
+ return { total, accounts: parsed }
270
+ }
271
+
272
+ /**
273
+ * CN 企业积分响应解析(纯函数,对齐 upstream fetchEnterpriseCredits):
274
+ * 官方两个调用点字段拼写不一致(limitNum/credit vs limit_num/used_num),两者都收;
275
+ * limit===-1 为「不限量」标记;limit 有值但 used 缺失属硬错误(宁报错不猜 0)。
276
+ */
277
+ export function parseWorkBuddyEnterpriseCredits(body) {
278
+ const sources = []
279
+ for (const candidate of [body?.data, body]) {
280
+ if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) continue
281
+ if (candidate.data !== null && typeof candidate.data === 'object' && !Array.isArray(candidate.data)) {
282
+ sources.push(candidate.data)
283
+ }
284
+ sources.push(candidate)
285
+ }
286
+ const numberAt = (source, key) => (typeof source[key] === 'number' && Number.isFinite(source[key]) ? source[key] : undefined)
287
+ let limit, used, resetTime
288
+ for (const source of sources) {
289
+ const candidate = numberAt(source, 'limitNum') ?? numberAt(source, 'limit_num')
290
+ if (candidate === undefined) continue
291
+ limit = candidate
292
+ used = numberAt(source, 'credit') ?? numberAt(source, 'used_num')
293
+ if (typeof source.cycleResetTime === 'string' && source.cycleResetTime !== '') resetTime = source.cycleResetTime
294
+ break
295
+ }
296
+ if (limit === undefined) throw workbuddyError('workbuddyEnterpriseNoQuota')
297
+ if (limit === -1) return { total: 0, unlimited: true, ...(resetTime === undefined ? {} : { cycleResetTime: resetTime }) }
298
+ if (limit < 0) throw workbuddyError('workbuddyInvalidCredits')
299
+ if (used === undefined || used < 0) throw workbuddyError('workbuddyEnterpriseNoUsage')
300
+ let remain = limit - used
301
+ if (remain < 0) remain = 0
302
+ return { total: remain, unlimited: false, ...(resetTime === undefined ? {} : { cycleResetTime: resetTime }) }
303
+ }
304
+
305
+ /** 本地时间 'YYYY-MM-DD HH:mm:ss'(对齐 upstream fetchCredits 的 format)。 */
306
+ function formatLocalTimestamp(date) {
307
+ const pad = (n, width = 2) => String(n).padStart(width, '0')
308
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
309
+ }
310
+
311
+ /**
312
+ * 查询 WorkBuddy 积分余额(port 自 upstream.ts fetchCredits/fetchEnterpriseCredits)。
313
+ * 返回 { total, unlimited, accounts, cycleResetTime? }。
314
+ * 登录态失效(HTTP 401/403 或信封 code 401/403)→ 抛 workbuddyTokenExpired(软错误)。
315
+ */
316
+ export async function fetchWorkBuddyCredits(credential, { signal, fetchImpl = fetch } = {}) {
317
+ const cnEnterprise = regionOf(credential.domain) === 'cn'
318
+ && credential.enterpriseId !== undefined && credential.enterpriseId !== ''
319
+ const url = cnEnterprise
320
+ ? `${CODEBUDZY_BILLING_BASE}/v2/billing/meter/get-enterprise-user-usage`
321
+ : `${workbuddyBillingBase(credential)}/v2/billing/meter/get-user-resource`
322
+ const now = new Date()
323
+ const body = cnEnterprise ? {} : {
324
+ PageNumber: 1,
325
+ PageSize: 100,
326
+ ProductCode: 'p_tcaca',
327
+ Status: [0, 3],
328
+ PackageEndTimeRangeBegin: formatLocalTimestamp(now),
329
+ PackageEndTimeRangeEnd: formatLocalTimestamp(new Date(now.getTime() + 365 * 101 * 24 * 3600 * 1000)),
330
+ }
331
+ const response = await fetchWithRetry(url, {
332
+ method: 'POST', redirect: 'error',
333
+ headers: billingHeadersOf(credential),
334
+ body: JSON.stringify(body),
335
+ signal,
336
+ }, { timeoutMs: 15000, attempts: 2, fetchImpl })
337
+ if (response.status === 401 || response.status === 403) {
338
+ try { await response.body?.cancel?.() } catch {}
339
+ throw workbuddyError('workbuddyTokenExpired', url)
340
+ }
341
+ if (!response.ok) { await response.body?.cancel?.(); throw new Error(`workbuddy billing HTTP ${response.status}`) }
342
+ const envelope = await readJsonBounded(response)
343
+ signal?.throwIfAborted()
344
+ if (envelope === null || typeof envelope !== 'object') throw new Error(`workbuddy billing returned non-JSON (${url})`)
345
+ if (envelope.code !== undefined && String(envelope.code) !== '0') {
346
+ if (['401', '403'].includes(String(envelope.code))) throw workbuddyError('workbuddyTokenExpired', url)
347
+ throw workbuddyError('workbuddyBillingFailed')
348
+ }
349
+ if (cnEnterprise) {
350
+ const parsed = parseWorkBuddyEnterpriseCredits(envelope)
351
+ return { total: parsed.total, unlimited: parsed.unlimited === true, accounts: [], ...parsed }
352
+ }
353
+ const parsed = parseWorkBuddyPersonalCredits(envelope)
354
+ if (parsed === null) throw new Error(`workbuddy billing response carried no Accounts list (${url})`)
355
+ return { total: parsed.total, unlimited: false, ...parsed }
356
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Whaletail Mobile Gateway",
4
+ "description": "Connect DSH to the Whaletail mobile app with secure pairing, remote access, context, agent teams, usage, and voice input."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "鲸尾移动网关",
4
+ "description": "连接 DSH 与鲸尾移动客户端,支持安全配对、远程访问、上下文、编队、额度和语音输入。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,54 +1,54 @@
1
- {
2
- "name": "dsh-plugin-weapp-gateway",
3
- "version": "0.8.13",
4
- "description": "Authenticated mobile gateway for DSH with Session V4 history, live permissions, inbox-based queue sync, built-in WeChat ClawBot, plugin management, LAN and Cloudflare Tunnel access, device pairing, per-session Agent presets, relay access, and speech input",
5
- "main": "lib/index.mjs",
6
- "files": [
7
- "bin",
8
- "helper",
9
- "lib",
10
- "relay",
11
- "docs",
12
- "THIRD-PARTY-NOTICES",
13
- "cordis.patch.yml",
14
- "PROTOCOL.md"
15
- ],
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/huiikeung/dsh-plugin-weapp-gateway.git"
19
- },
20
- "homepage": "https://github.com/huiikeung/dsh-plugin-weapp-gateway#readme",
21
- "bugs": {
22
- "url": "https://github.com/huiikeung/dsh-plugin-weapp-gateway/issues"
23
- },
24
- "dependencies": {
25
- "@deepseek-ai/schemastery": "^3.18.1",
26
- "ws": "^8.18.0",
27
- "qrcode": "^1.5.4"
28
- },
29
- "exports": {
30
- ".": "./lib/index.mjs",
31
- "./client": "./lib/client.js",
32
- "./package.json": "./package.json"
33
- },
34
- "dsh": {
35
- "bundle": {
36
- "patch": "./cordis.patch.yml"
37
- },
38
- "client": {
39
- "platform": "web",
40
- "inject": [
41
- "@deepseek-ai/dsh-client-runtime",
42
- "@deepseek-ai/dsh-client-ui-layout",
43
- "@deepseek-ai/dsh-client-ui-sidebar"
44
- ]
45
- }
46
- },
47
- "license": "MIT",
48
- "scripts": {
49
- "test": "node test/wire-json.test.mjs && node test/setup-ip.test.mjs && node test/host-adapter.test.mjs && node test/clawbot-service.test.mjs && node test/speech-input.test.mjs && node test/speech-official.test.mjs && node test/plugin-management.test.mjs && node test/plugin-updates.test.mjs && node test/session-preset.test.mjs && node test/session-follower.test.mjs && node test/completion-notifications.test.mjs && node --test test/workspace-changes.test.mjs && node test/gateway.test.mjs && node test/auth.test.mjs && node test/lan.test.mjs && node test/cloudflared-binary.test.mjs && node test/cloudflare-tunnel.test.mjs && node --test test/public-endpoint-check.test.mjs && node test/gateway-tools.test.mjs && node test/relay-connector.test.mjs && node test/cloudflare-linux.test.mjs && node test/multi-gateway.test.mjs"
50
- },
51
- "bin": {
52
- "dsh-plugin-weapp-gateway": "bin/setup-ip.mjs"
53
- }
54
- }
1
+ {
2
+ "name": "dsh-plugin-weapp-gateway",
3
+ "version": "0.8.23",
4
+ "description": "鲸尾移动网关:DeepSeek Harness 移动客户端连接与会话同步,支持安全配对、远程访问、上下文、编队、额度和语音输入",
5
+ "main": "lib/index.mjs",
6
+ "files": [
7
+ "bin",
8
+ "helper",
9
+ "lib",
10
+ "relay",
11
+ "docs",
12
+ "THIRD-PARTY-NOTICES",
13
+ "locale/*.json",
14
+ "cordis.patch.yml",
15
+ "PROTOCOL.md"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/huiikeung/dsh-plugin-weapp-gateway.git"
20
+ },
21
+ "homepage": "https://github.com/huiikeung/dsh-plugin-weapp-gateway#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/huiikeung/dsh-plugin-weapp-gateway/issues"
24
+ },
25
+ "dependencies": {
26
+ "@deepseek-ai/schemastery": "^3.18.1",
27
+ "ws": "^8.18.0",
28
+ "qrcode": "^1.5.4"
29
+ },
30
+ "bin": "bin/setup-ip.mjs",
31
+ "scripts": {
32
+ "test": "node test/wire-json.test.mjs && node test/setup-ip.test.mjs && node test/host-adapter.test.mjs && node test/account-bridge.test.mjs && node test/context-bridge.test.mjs && node test/cost-meter.test.mjs && node --test test/cost-meter-dashboard.test.mjs && node test/clawbot-service.test.mjs && node test/speech-input.test.mjs && node test/speech-official.test.mjs && node test/plugin-management.test.mjs && node test/plugin-updates.test.mjs && node test/session-preset.test.mjs && node test/session-follower.test.mjs && node test/completion-notifications.test.mjs && node --test test/workspace-changes.test.mjs && node test/gateway.test.mjs && node test/auth.test.mjs && node test/lan.test.mjs && node test/cloudflared-binary.test.mjs && node test/cloudflare-tunnel.test.mjs && node --test test/public-endpoint-check.test.mjs && node test/gateway-tools.test.mjs && node test/relay-connector.test.mjs && node test/cloudflare-linux.test.mjs && node test/multi-gateway.test.mjs"
33
+ },
34
+ "exports": {
35
+ ".": "./lib/index.mjs",
36
+ "./client": "./lib/client.js",
37
+ "./locale/*.json": "./locale/*.json",
38
+ "./package.json": "./package.json"
39
+ },
40
+ "dsh": {
41
+ "bundle": {
42
+ "patch": "./cordis.patch.yml"
43
+ },
44
+ "client": {
45
+ "platform": "web",
46
+ "inject": [
47
+ "@deepseek-ai/dsh-client-runtime",
48
+ "@deepseek-ai/dsh-client-ui-layout",
49
+ "@deepseek-ai/dsh-client-ui-sidebar"
50
+ ]
51
+ }
52
+ },
53
+ "license": "MIT"
54
+ }