@magpie-community/opencode-workbuddy-auth 0.1.12 → 0.1.14
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 +17 -0
- package/index.mjs +127 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,14 @@ included, doesn't mark the account, as the built-in never did; magpie
|
|
|
43
43
|
tries again later, and the old token is used meanwhile.
|
|
44
44
|
OpenCode doesn't call `auth.refresh`; the check before each request stays.
|
|
45
45
|
|
|
46
|
+
An enterprise (team) account is asked for as the enterprise. A browser
|
|
47
|
+
sign-in keeps the enterprise id WorkBuddy names for the account (from
|
|
48
|
+
`/login/account`, else its account list), and desktop's is read from the
|
|
49
|
+
app's file. Every request then carries `X-Enterprise-Id` and `X-Tenant-Id`,
|
|
50
|
+
as WorkBuddy's own client sends them; without them WorkBuddy answers as the
|
|
51
|
+
personal account, with the personal plan's models. A browser sign-in made
|
|
52
|
+
before 0.1.13 has no enterprise id kept: sign in again.
|
|
53
|
+
|
|
46
54
|
Desktop's sign-in is refreshed only when the app hasn't refreshed it
|
|
47
55
|
itself, and the new token is kept in memory, never written back, as magpie's
|
|
48
56
|
built-in WorkBuddy does.
|
|
@@ -111,6 +119,15 @@ is used.
|
|
|
111
119
|
|
|
112
120
|
A model's reasoning levels are its variants (`reasoningEffort`).
|
|
113
121
|
|
|
122
|
+
## Credits in magpie
|
|
123
|
+
|
|
124
|
+
magpie's usage card shows the account's credits from the resource summary.
|
|
125
|
+
It also says when the soonest credits left run out (`runsOutAt`, and how
|
|
126
|
+
many: `runsOut`), from the paid and free package lists WorkBuddy's own
|
|
127
|
+
client reads. The daily Free Plan slice isn't counted: it ends at the same
|
|
128
|
+
midnight on every account. magpie's smart routing spends the account whose
|
|
129
|
+
credits run out first.
|
|
130
|
+
|
|
114
131
|
## Not here
|
|
115
132
|
|
|
116
133
|
magpie's built-in WorkBuddy does more than this package:
|
package/index.mjs
CHANGED
|
@@ -172,6 +172,7 @@ function renew(site, a, save) {
|
|
|
172
172
|
"X-Refresh-Token": a.refresh,
|
|
173
173
|
"X-Auth-Refresh-Source": "plugin",
|
|
174
174
|
"X-Domain": domainOf(site, a),
|
|
175
|
+
...enterpriseHeaders(a),
|
|
175
176
|
}, {})
|
|
176
177
|
if (!got?.accessToken) throw Object.assign(new Error("WorkBuddy gave no refreshed token"), { bare: true })
|
|
177
178
|
const next = merge(a, got)
|
|
@@ -259,10 +260,48 @@ function sign(site, a, headers) {
|
|
|
259
260
|
headers.set("X-Agent-Purpose", "conversation")
|
|
260
261
|
headers.set("X-IDE-Name", "WorkBuddy")
|
|
261
262
|
headers.set("X-IDE-Version", UA_VERSION)
|
|
263
|
+
for (const [k, v] of Object.entries(enterpriseHeaders(a))) headers.set(k, v)
|
|
262
264
|
if (!site.ai) return
|
|
263
265
|
headers.set("X-Agent-Type", "main")
|
|
264
266
|
}
|
|
265
267
|
|
|
268
|
+
// enterpriseHeaders names the enterprise (team) an account signed in as, as
|
|
269
|
+
// WorkBuddy's own client does on every request it makes for that account
|
|
270
|
+
// (its auth interceptor: X-Enterprise-Id and X-Tenant-Id, both the
|
|
271
|
+
// account's enterpriseId). Without them WorkBuddy answers as the personal
|
|
272
|
+
// account: /v3/config lists the personal plan's models and not the
|
|
273
|
+
// enterprise's (#75). A personal account has none and sends none.
|
|
274
|
+
function enterpriseHeaders(a) {
|
|
275
|
+
const id = typeof a?.enterpriseId === "string" ? a.enterpriseId.trim() : ""
|
|
276
|
+
return id ? { "X-Enterprise-Id": id, "X-Tenant-Id": id } : {}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// enterpriseOf is the enterprise the account a sign-in gave belongs to:
|
|
280
|
+
// the one /login/account names, else, for an account that isn't personal,
|
|
281
|
+
// the one WorkBuddy's account list has for it — the same choice WorkBuddy's
|
|
282
|
+
// own client makes (mergeCurrentAccountDetails): the uid's only entry of that
|
|
283
|
+
// type, or the only one it last signed in to. None found is personal.
|
|
284
|
+
async function enterpriseOf(site, a, who) {
|
|
285
|
+
const own = typeof who?.enterpriseId === "string" ? who.enterpriseId.trim() : ""
|
|
286
|
+
if (own) return own
|
|
287
|
+
const type = String(who?.type ?? "").trim().toLowerCase()
|
|
288
|
+
if (!type || type === "personal") return ""
|
|
289
|
+
let list
|
|
290
|
+
try {
|
|
291
|
+
list = await call(site, "GET", "/v2/plugin/accounts", {
|
|
292
|
+
Authorization: "Bearer " + a.access,
|
|
293
|
+
"X-Domain": domainOf(site, a),
|
|
294
|
+
})
|
|
295
|
+
} catch {
|
|
296
|
+
return ""
|
|
297
|
+
}
|
|
298
|
+
const mine = (list?.accounts ?? []).filter((x) => x?.uid === who.uid && typeof x.enterpriseId === "string" && x.enterpriseId.trim())
|
|
299
|
+
const same = mine.filter((x) => String(x.type ?? "").trim().toLowerCase() === type)
|
|
300
|
+
const last = mine.filter((x) => x.lastLogin)
|
|
301
|
+
const pick = same.length === 1 ? same[0] : last.length === 1 ? last[0] : mine.length === 1 ? mine[0] : undefined
|
|
302
|
+
return pick ? pick.enterpriseId.trim() : ""
|
|
303
|
+
}
|
|
304
|
+
|
|
266
305
|
// WorkBuddy's own client names each chat to its gateway four ways: the
|
|
267
306
|
// conversation it belongs to, the user send (turn) it answers, the message
|
|
268
307
|
// itself, and the request that carried it. Its backend counts a user send as
|
|
@@ -759,6 +798,7 @@ async function browserSignIn(site) {
|
|
|
759
798
|
"X-No-Enterprise-Id": "true",
|
|
760
799
|
}, [12151], deadline)
|
|
761
800
|
a.uid = who.uid ?? ""
|
|
801
|
+
a.enterpriseId = await enterpriseOf(site, a, who)
|
|
762
802
|
return success(site, a, who.nickname || who.phoneNumber || who.uid)
|
|
763
803
|
} catch (e) {
|
|
764
804
|
return { type: "failed", error: e.message }
|
|
@@ -776,6 +816,7 @@ function success(site, a, name) {
|
|
|
776
816
|
accountId: name || site.name,
|
|
777
817
|
uid: a.uid ?? "",
|
|
778
818
|
domain: a.domain ?? "",
|
|
819
|
+
...(a.enterpriseId ? { enterpriseId: a.enterpriseId } : {}),
|
|
779
820
|
...(a.refreshExpiresAt ? { refreshExpiresAt: a.refreshExpiresAt } : {}),
|
|
780
821
|
...(a.tokenType ? { tokenType: a.tokenType } : {}),
|
|
781
822
|
}
|
|
@@ -808,6 +849,7 @@ function readDesktop(site) {
|
|
|
808
849
|
domain: typeof t.domain === "string" ? t.domain : "",
|
|
809
850
|
tokenType: typeof t.tokenType === "string" ? t.tokenType : "",
|
|
810
851
|
uid: acct.uid,
|
|
852
|
+
...(typeof acct.enterpriseId === "string" && acct.enterpriseId.trim() ? { enterpriseId: acct.enterpriseId.trim() } : {}),
|
|
811
853
|
name: acct.nickname || acct.phoneNumber || acct.uid,
|
|
812
854
|
}
|
|
813
855
|
} catch {
|
|
@@ -875,13 +917,13 @@ const statusText = (n) => STATUS_TEXT[n] || `HTTP ${n}`
|
|
|
875
917
|
|
|
876
918
|
// meter asks the billing API as magpie's built-in did, its errors said
|
|
877
919
|
// the same: WorkBuddy's message, else its code, else the HTTP status.
|
|
878
|
-
async function meter(site, a, path) {
|
|
920
|
+
async function meter(site, a, path, body = {}) {
|
|
879
921
|
const res = await fetch(site.endpoint + path, {
|
|
880
922
|
method: "POST",
|
|
881
923
|
headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "WorkBuddy/" + UA_VERSION,
|
|
882
924
|
Authorization: "Bearer " + a.access, "X-User-Id": a.uid ?? "", "X-Domain": domainOf(site, a), "X-Product": "SaaS",
|
|
883
|
-
"X-IDE-Type": "WorkBuddy" },
|
|
884
|
-
body:
|
|
925
|
+
"X-IDE-Type": "WorkBuddy", ...enterpriseHeaders(a) },
|
|
926
|
+
body: JSON.stringify(body),
|
|
885
927
|
signal: AbortSignal.timeout(20000),
|
|
886
928
|
})
|
|
887
929
|
let env = {}
|
|
@@ -927,6 +969,76 @@ function usageOf(sum, plan) {
|
|
|
927
969
|
return out
|
|
928
970
|
}
|
|
929
971
|
|
|
972
|
+
// The packages' codes, as WorkBuddy's own client lists them when it asks
|
|
973
|
+
// for each package's credits (its COMMODITY_CODES): the API refuses a
|
|
974
|
+
// detail list without them (10001 "PackageCodes required").
|
|
975
|
+
const CODE = (n, s) => `TCACA_code_${n}_${s}`
|
|
976
|
+
const PAID_CODES = [CODE("002", "AkiJS3ZHF5"), CODE("005", "maRGyrHhw1"), CODE("003", "FAnt7lcmRT"), CODE("023", "4xbGhMrE6q"),
|
|
977
|
+
CODE("026", "BaESVICNoi"), CODE("027", "0FCGVA6vSa"), CODE("009", "0XmEQc2xOf"), CODE("038", "OhvqZtiPKr"), CODE("036", "lupO5WgNdG")]
|
|
978
|
+
const DAILY_CODE = CODE("001", "PqouKr6QWV")
|
|
979
|
+
const FREE_CODES = [DAILY_CODE, CODE("008", "cfWoLwvjU4"), CODE("035", "ArVxJcGDsm"), CODE("006", "DbXS0lrypC"), CODE("039", "KRcQj7wUat"),
|
|
980
|
+
CODE("040", "mi9rCYg46x"), CODE("007", "nzdH5h4Nl0"), CODE("028", "NtpWi0jzXs"), CODE("037", "WxOD3MpI2o"), CODE("029", "6wCGEWquYy"),
|
|
981
|
+
CODE("030", "BjSt89qTvr")]
|
|
982
|
+
// a slice package (CapacityType 4) is refreshed each day: its credits are
|
|
983
|
+
// lost at the same midnight on every account, so they tell none apart
|
|
984
|
+
const SLICE = "4"
|
|
985
|
+
|
|
986
|
+
// beijing is WorkBuddy's own time ("2026-10-13 00:39:27" is UTC+8, as its
|
|
987
|
+
// DeductionEndTime for the same package says).
|
|
988
|
+
const beijing = (ms) => new Date(ms + 8 * 3600_000).toISOString().slice(0, 19).replace("T", " ")
|
|
989
|
+
|
|
990
|
+
// packagesOf is every package of the account with credits, paid and free,
|
|
991
|
+
// as WorkBuddy's client asks for them: active and used-up (Status 0, 3),
|
|
992
|
+
// up to 10 pages of 200.
|
|
993
|
+
async function packagesOf(site, a, now = Date.now()) {
|
|
994
|
+
const day = beijing(now).slice(0, 10)
|
|
995
|
+
const lists = [
|
|
996
|
+
["/billing/meter/get-user-resource-paid-packages", { PackageCodes: PAID_CODES, NeedRenewInfo: true }],
|
|
997
|
+
["/billing/meter/get-user-resource-free-packages", { PackageCodes: FREE_CODES, SlicePeriodStartTime: day + " 00:00:00", SlicePeriodEndTime: day + " 23:59:59" }],
|
|
998
|
+
]
|
|
999
|
+
const out = []
|
|
1000
|
+
for (const [path, extra] of lists) {
|
|
1001
|
+
for (let page = 1; page <= 10; page++) {
|
|
1002
|
+
const d = await meter(site, a, path, { PageNumber: page, PageSize: 200, Status: [0, 3], ...extra })
|
|
1003
|
+
const got = d?.Accounts ?? []
|
|
1004
|
+
out.push(...got)
|
|
1005
|
+
if (got.length < 200 || out.length >= (d?.TotalCount ?? 0)) break
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return out
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// endOf is when a package's credits are lost: its DeductionEndTime, else
|
|
1012
|
+
// ExpiredTime, else the cycle's end (UTC+8), as WorkBuddy's client reads it.
|
|
1013
|
+
function endOf(p) {
|
|
1014
|
+
const ms = Number(p?.DeductionEndTime)
|
|
1015
|
+
if (ms > 0) return ms
|
|
1016
|
+
for (const v of [p?.ExpiredTime, p?.CycleEndTime]) {
|
|
1017
|
+
if (typeof v !== "string" || !v.trim()) continue
|
|
1018
|
+
const t = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(v.trim()) ? Date.parse(v.trim().replace(" ", "T") + "+08:00") : Date.parse(v)
|
|
1019
|
+
if (t > 0) return t
|
|
1020
|
+
}
|
|
1021
|
+
return 0
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// runsOutOf is the soonest moment credits left in one of the account's
|
|
1025
|
+
// packages are lost, and how many (cdredfox on X): smart routing spends
|
|
1026
|
+
// the account whose credits expire first. Only packages the summary
|
|
1027
|
+
// counts, with credits left, and not the daily slice; null when none.
|
|
1028
|
+
function runsOutOf(sum, packages, now = Date.now()) {
|
|
1029
|
+
const counted = new Set((sum?.Packages ?? []).map((p) => p?.PackageCode))
|
|
1030
|
+
let at = 0, amount = 0
|
|
1031
|
+
for (const p of packages ?? []) {
|
|
1032
|
+
if (!counted.has(p?.PackageCode) || p?.PackageCode === DAILY_CODE || String(p?.CapacityType) === SLICE) continue
|
|
1033
|
+
const left = capacity(p?.CycleCapacityRemainPrecise ?? p?.CycleCapacityRemain)
|
|
1034
|
+
const end = endOf(p)
|
|
1035
|
+
if (!(left > 0) || end <= now) continue
|
|
1036
|
+
if (!at || end < at) at = end, amount = left
|
|
1037
|
+
else if (end === at) amount += left
|
|
1038
|
+
}
|
|
1039
|
+
return at ? { runsOutAt: new Date(at).toISOString(), runsOut: amount } : null
|
|
1040
|
+
}
|
|
1041
|
+
|
|
930
1042
|
// ---- models -----------------------------------------------------------------
|
|
931
1043
|
|
|
932
1044
|
// variants are the reasoning levels as OpenCode gives them to the AI SDK.
|
|
@@ -1124,7 +1236,17 @@ function makePlugin(site) {
|
|
|
1124
1236
|
try {
|
|
1125
1237
|
const a = await current(site, client, auth)
|
|
1126
1238
|
// the meter has no /v2 prefix, on either site
|
|
1127
|
-
|
|
1239
|
+
const sum = await meter(site, a, "/billing/meter/get-user-resource-summary")
|
|
1240
|
+
const out = usageOf(sum, auth.plan)
|
|
1241
|
+
// when the credits left run out, for routing to spend them first;
|
|
1242
|
+
// a list that can't be read leaves it unknown, the card stands
|
|
1243
|
+
const credits = out.windows.find((w) => w.name === "Credits")
|
|
1244
|
+
if (credits) {
|
|
1245
|
+
try {
|
|
1246
|
+
Object.assign(credits, runsOutOf(sum, await packagesOf(site, a)) ?? {})
|
|
1247
|
+
} catch {}
|
|
1248
|
+
}
|
|
1249
|
+
return { ...out, signIn: "kept" }
|
|
1128
1250
|
} catch (e) {
|
|
1129
1251
|
return { error: e?.message ?? String(e), signIn: "kept" }
|
|
1130
1252
|
}
|
|
@@ -1216,4 +1338,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
1216
1338
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
1217
1339
|
|
|
1218
1340
|
// for tests
|
|
1219
|
-
export const _internal = { withSystem, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf }
|
|
1341
|
+
export const _internal = { runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-workbuddy-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "OpenCode provider plugins for WorkBuddy's plans: WorkBuddy (CodeBuddy, copilot.tencent.com) and WorkBuddy AI (www.workbuddy.ai)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|