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.
- package/PROTOCOL.md +168 -1
- package/README.md +63 -14
- package/THIRD-PARTY-NOTICES/dsh-cost-meter.txt +31 -0
- package/bin/setup-ip.mjs +0 -0
- package/lib/account-bridge.mjs +57 -0
- package/lib/client.js +5 -5
- package/lib/context-bridge.mjs +1715 -0
- package/lib/cost-meter-bridge.mjs +794 -0
- package/lib/cost-meter-dashboard-bridge.mjs +1449 -0
- package/lib/dsh-host-adapter.mjs +1 -1
- package/lib/index.mjs +111 -15
- package/lib/plugin-updates.mjs +103 -10
- package/lib/vendor/cost-meter/aliyun-balance.mjs +123 -0
- package/lib/vendor/cost-meter/coding-plans.mjs +1668 -0
- package/lib/vendor/cost-meter/credentials-shim.mjs +8 -0
- package/lib/vendor/cost-meter/custom-balance.mjs +281 -0
- package/lib/vendor/cost-meter/factory-quota.mjs +69 -0
- package/lib/vendor/cost-meter/gateway-quota-adapters.mjs +1006 -0
- package/lib/vendor/cost-meter/gateway-quotas.mjs +664 -0
- package/lib/vendor/cost-meter/home-paths-shim.mjs +13 -0
- package/lib/vendor/cost-meter/net.mjs +183 -0
- package/lib/vendor/cost-meter/opendesign.mjs +204 -0
- package/lib/vendor/cost-meter/tokenrhythm-balance.mjs +95 -0
- package/lib/vendor/cost-meter/workbuddy-accounts.mjs +44 -0
- package/lib/vendor/cost-meter/workbuddy-transport.mjs +38 -0
- package/lib/vendor/cost-meter/workbuddy.mjs +356 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +54 -54
|
@@ -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
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-plugin-weapp-gateway",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "lib/index.mjs",
|
|
6
|
-
"files": [
|
|
7
|
-
"bin",
|
|
8
|
-
"helper",
|
|
9
|
-
"lib",
|
|
10
|
-
"relay",
|
|
11
|
-
"docs",
|
|
12
|
-
"THIRD-PARTY-NOTICES",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
},
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|