@magpie-community/opencode-workbuddy-auth 0.1.48 → 0.2.1
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 +36 -4
- package/index.mjs +144 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,6 +61,19 @@ as WorkBuddy's own client sends them; without them WorkBuddy answers as the
|
|
|
61
61
|
personal account, with the personal plan's models. A browser sign-in made
|
|
62
62
|
before 0.1.13 has no enterprise id kept: sign in again.
|
|
63
63
|
|
|
64
|
+
Which account a browser sign-in gives is chosen on WorkBuddy's own sign-in
|
|
65
|
+
page. After a phone sign-in it lists the person's accounts (personal, a
|
|
66
|
+
team, 「WorkBuddy 教育公益版」…) and binds the one picked to the sign-in, so
|
|
67
|
+
to add an education or team account, pick it there. Its account is named
|
|
68
|
+
with the enterprise too (`nickname · enterprise name`, or the end of the
|
|
69
|
+
enterprise id when WorkBuddy names none): one person's personal and
|
|
70
|
+
enterprise accounts share the uid and the nickname, and magpie keeps one
|
|
71
|
+
account per name and uid, so before 0.2.1 signing in to the enterprise
|
|
72
|
+
account replaced the personal one. An enterprise account signed in before
|
|
73
|
+
0.2.1 keeps its old name until its Sign in again, which renames it in
|
|
74
|
+
place; its free-model window, read from usage rows kept under the old
|
|
75
|
+
name, starts again from the new one.
|
|
76
|
+
|
|
64
77
|
Desktop's sign-in is refreshed only when the app hasn't refreshed it
|
|
65
78
|
itself, and the new token is kept in memory, never written back, as magpie's
|
|
66
79
|
built-in WorkBuddy does.
|
|
@@ -397,11 +410,30 @@ entry. An already signed-in browser can simply renew another saved account.
|
|
|
397
410
|
|
|
398
411
|
## Not here
|
|
399
412
|
|
|
400
|
-
magpie's built-in WorkBuddy does more than this package:
|
|
401
|
-
|
|
402
|
-
- the credits meter (`/billing/meter/get-user-resource-summary`);
|
|
403
|
-
- several accounts under one provider.
|
|
413
|
+
magpie's built-in WorkBuddy does more than this package: several accounts
|
|
414
|
+
under one provider.
|
|
404
415
|
|
|
405
416
|
OpenCode's plugin API has no place for these.
|
|
406
417
|
|
|
407
418
|
Credit multipliers accept the international catalog's `x0.34 credits` text as well as `x0.34`. An empty credits value (for example Auto) stays unknown, never free.
|
|
419
|
+
|
|
420
|
+
## Daily check-in
|
|
421
|
+
|
|
422
|
+
The account's daily credits are claimed through the billing meter. magpie
|
|
423
|
+
can run it once a day per account when its WorkBuddy check-in switch is on;
|
|
424
|
+
Settings → Usage → Plugins has that switch and a *Check in now* button.
|
|
425
|
+
The activity status is read first: inactive is `inactive`, a day already
|
|
426
|
+
checked in is `done`, and only an active unchecked day is claimed. A claim
|
|
427
|
+
that WorkBuddy answers 1001, 1002 or 1003 is `done`, `ineligible` or
|
|
428
|
+
`inactive`. A claim turned away 401 while the account's credits still read
|
|
429
|
+
is `ineligible`: that check-in takes WorkBuddy's own app. A transient
|
|
430
|
+
billing failure is retried twice before the card shows the failure.
|
|
431
|
+
Only WorkBuddy (China) checks in: the AI build has no claim endpoint, and
|
|
432
|
+
its plugin carries no check-in hook.
|
|
433
|
+
A request that gets no answer at all leaves the hook as a thrown error, so
|
|
434
|
+
magpie retries it on its 1-minute offline backoff rather than the 30-minute
|
|
435
|
+
one it gives a failed answer (#149).
|
|
436
|
+
|
|
437
|
+
An `inactive` answer is kept for the whole Beijing day: the plugin host
|
|
438
|
+
gives it no recheck, and a manual refresh reads the kept one (tracked in
|
|
439
|
+
[yetone/magpie#2696](https://github.com/yetone/magpie/issues/2696)).
|
package/index.mjs
CHANGED
|
@@ -415,16 +415,26 @@ function enterpriseHeaders(a) {
|
|
|
415
415
|
return id ? { "X-Enterprise-Id": id, "X-Tenant-Id": id } : {}
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
// enterpriseOf is the enterprise the account a sign-in gave belongs to
|
|
419
|
-
// the one /login/account names, else, for an account that
|
|
420
|
-
// the one WorkBuddy's account list has for it — the same
|
|
421
|
-
// own client makes (mergeCurrentAccountDetails): the
|
|
422
|
-
// type, or the only one it last signed in to. None
|
|
418
|
+
// enterpriseOf is the enterprise the account a sign-in gave belongs to,
|
|
419
|
+
// { id, name }: the one /login/account names, else, for an account that
|
|
420
|
+
// isn't personal, the one WorkBuddy's account list has for it — the same
|
|
421
|
+
// choice WorkBuddy's own client makes (mergeCurrentAccountDetails): the
|
|
422
|
+
// uid's only entry of that type, or the only one it last signed in to. None
|
|
423
|
+
// found is personal ({ id: "" }).
|
|
424
|
+
//
|
|
425
|
+
// Which account that is was chosen on WorkBuddy's sign-in page: after a
|
|
426
|
+
// phone sign-in it lists the person's accounts (personal, a team,
|
|
427
|
+
// 「WorkBuddy 教育公益版」…) and binds the one picked to the sign-in's state
|
|
428
|
+
// (POST /console/login/enterprise/<id>?state=…), so the token and
|
|
429
|
+
// /login/account this plugin reads by that state are the chosen account's.
|
|
423
430
|
async function enterpriseOf(site, a, who) {
|
|
431
|
+
const name = (x) => (typeof x?.enterpriseName === "string" ? x.enterpriseName.trim() : "")
|
|
424
432
|
const own = typeof who?.enterpriseId === "string" ? who.enterpriseId.trim() : ""
|
|
425
|
-
if (own) return own
|
|
426
433
|
const type = String(who?.type ?? "").trim().toLowerCase()
|
|
427
|
-
if (!type || type === "personal") return ""
|
|
434
|
+
if (!own && (!type || type === "personal")) return { id: "", name: "" }
|
|
435
|
+
// a name /login/account leaves out is the account list's, as the
|
|
436
|
+
// client merges the two
|
|
437
|
+
if (own && name(who)) return { id: own, name: name(who) }
|
|
428
438
|
let list
|
|
429
439
|
try {
|
|
430
440
|
list = await call(site, "GET", "/v2/plugin/accounts", {
|
|
@@ -432,13 +442,29 @@ async function enterpriseOf(site, a, who) {
|
|
|
432
442
|
"X-Domain": domainOf(site, a),
|
|
433
443
|
})
|
|
434
444
|
} catch {
|
|
435
|
-
return ""
|
|
445
|
+
return { id: own, name: "" }
|
|
436
446
|
}
|
|
437
447
|
const mine = (list?.accounts ?? []).filter((x) => x?.uid === who.uid && typeof x.enterpriseId === "string" && x.enterpriseId.trim())
|
|
448
|
+
if (own) return { id: own, name: name(mine.find((x) => x.enterpriseId.trim() === own)) }
|
|
438
449
|
const same = mine.filter((x) => String(x.type ?? "").trim().toLowerCase() === type)
|
|
439
450
|
const last = mine.filter((x) => x.lastLogin)
|
|
440
451
|
const pick = same.length === 1 ? same[0] : last.length === 1 ? last[0] : mine.length === 1 ? mine[0] : undefined
|
|
441
|
-
return pick ? pick.enterpriseId.trim() : ""
|
|
452
|
+
return pick ? { id: pick.enterpriseId.trim(), name: name(pick) } : { id: "", name: "" }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// accountName is what magpie lists an account a sign-in gave as: the
|
|
456
|
+
// person's name, and for an enterprise (team, education) account the
|
|
457
|
+
// enterprise's too. One person's personal account and their enterprise one
|
|
458
|
+
// share the uid and the nickname, and magpie's plugin host keeps one
|
|
459
|
+
// account per name and uid (settle in host.js): named alike, signing in to
|
|
460
|
+
// 「WorkBuddy 教育公益版」 took the place of the same person's personal
|
|
461
|
+
// account, and the two could never be in one pool. An enterprise WorkBuddy
|
|
462
|
+
// names no name for is told by the end of its id.
|
|
463
|
+
function accountName(who, ent) {
|
|
464
|
+
const person = who?.nickname || who?.phoneNumber || who?.uid || ""
|
|
465
|
+
if (!ent?.id) return person
|
|
466
|
+
const team = ent.name || `enterprise ${ent.id.slice(-6)}`
|
|
467
|
+
return person ? `${person} · ${team}` : team
|
|
442
468
|
}
|
|
443
469
|
|
|
444
470
|
// WorkBuddy's own client names each chat to its gateway four ways: the
|
|
@@ -1541,8 +1567,9 @@ async function browserSignIn(site) {
|
|
|
1541
1567
|
"X-No-Enterprise-Id": "true",
|
|
1542
1568
|
}, [12151], deadline)
|
|
1543
1569
|
a.uid = who.uid ?? ""
|
|
1544
|
-
|
|
1545
|
-
|
|
1570
|
+
const ent = await enterpriseOf(site, a, who)
|
|
1571
|
+
a.enterpriseId = ent.id
|
|
1572
|
+
return success(site, a, accountName(who, ent))
|
|
1546
1573
|
} catch (e) {
|
|
1547
1574
|
return { type: "failed", error: e.message }
|
|
1548
1575
|
}
|
|
@@ -1868,23 +1895,99 @@ function signedOut(site, a, why, expired, note) {
|
|
|
1868
1895
|
// the same: WorkBuddy's message, else its code, else the HTTP status.
|
|
1869
1896
|
async function meter(site, a, path, body = {}) {
|
|
1870
1897
|
const res = await meterAsked(site, a, path, body)
|
|
1898
|
+
const raw = await res.text()
|
|
1871
1899
|
let env = {}
|
|
1900
|
+
let parsed = true
|
|
1872
1901
|
try {
|
|
1873
|
-
env = JSON.parse(
|
|
1874
|
-
} catch {
|
|
1902
|
+
env = JSON.parse(raw) ?? {}
|
|
1903
|
+
} catch {
|
|
1904
|
+
parsed = false
|
|
1905
|
+
}
|
|
1906
|
+
if (res.ok && !parsed) {
|
|
1907
|
+
const unreadable = raw.trim().slice(0, 120) || "(empty)"
|
|
1908
|
+
throw Object.assign(new Error(`unreadable reply ${unreadable}`), { status: res.status, code: 0, unreadable: true })
|
|
1909
|
+
}
|
|
1875
1910
|
const code = Number.parseInt(env.code, 10) || 0
|
|
1911
|
+
// the status, the code and whether the token itself was refused go with
|
|
1912
|
+
// it, for usage's one renewal on a 401 (reissued) and check-in's outcomes
|
|
1913
|
+
const said = { status: res.status, code }
|
|
1914
|
+
if (!res.ok) said.invalidToken = invalidToken(res)
|
|
1915
|
+
const err = (message) => Object.assign(new Error(message), said)
|
|
1876
1916
|
if (!res.ok) {
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
throw Object.assign(new Error(statusText(res.status)), said)
|
|
1883
|
-
}
|
|
1884
|
-
if (code) throw new Error(env.msg || `error ${code}`)
|
|
1917
|
+
if (code) throw err(env.msg || `error ${code}`)
|
|
1918
|
+
if (env.msg) throw err(env.msg)
|
|
1919
|
+
throw err(statusText(res.status))
|
|
1920
|
+
}
|
|
1921
|
+
if (code) throw err(env.msg || `error ${code}`)
|
|
1885
1922
|
return env.data ?? null
|
|
1886
1923
|
}
|
|
1887
1924
|
|
|
1925
|
+
// checkinOf claims the account's daily credits. China has the /v2 meter;
|
|
1926
|
+
// after the activity status names an active, unchecked day. Transient
|
|
1927
|
+
// upstream failures are asked again twice (the bounded retry quota-hub
|
|
1928
|
+
// observed on the billing endpoint; callers may shorten the waits for a
|
|
1929
|
+
// test).
|
|
1930
|
+
async function checkinOf(site, a, { waits = [0, 0] } = {}) {
|
|
1931
|
+
let status
|
|
1932
|
+
try {
|
|
1933
|
+
status = await meter(site, a, "/v2/billing/meter/checkin-activity-status")
|
|
1934
|
+
} catch (e) {
|
|
1935
|
+
if (e?.code === 1001) return { outcome: "done" }
|
|
1936
|
+
// a reply WorkBuddy sent is the day's failed answer; only a request
|
|
1937
|
+
// that got no answer leaves as a throw, for magpie's offline backoff
|
|
1938
|
+
if (e?.status !== undefined || e?.code !== undefined) return { outcome: "failed", message: e.message }
|
|
1939
|
+
throw e
|
|
1940
|
+
}
|
|
1941
|
+
if (!status?.active) return { outcome: "inactive" }
|
|
1942
|
+
if (status.today_checked_in ?? status.todayCheckedIn) {
|
|
1943
|
+
// today_credit is what today actually gave, the built-in's word for a
|
|
1944
|
+
// day already in; daily_credit is the day's grant
|
|
1945
|
+
const credit = Number(status.today_credit ?? status.daily_credit)
|
|
1946
|
+
return { outcome: "done", ...(Number.isFinite(credit) && credit > 0 ? { credit } : {}) }
|
|
1947
|
+
}
|
|
1948
|
+
const paths = ["/v2/billing/meter/daily-checkin"]
|
|
1949
|
+
for (let attempt = 0; ; attempt++) {
|
|
1950
|
+
let i = 0
|
|
1951
|
+
while (i < paths.length) {
|
|
1952
|
+
try {
|
|
1953
|
+
const got = await meter(site, a, paths[i++])
|
|
1954
|
+
// the claim's own answer, as the built-in hands it on: the credit
|
|
1955
|
+
// and streak it granted and what it said, for the card
|
|
1956
|
+
const credit = Number(got?.credit)
|
|
1957
|
+
const streak = Number(got?.streak_days)
|
|
1958
|
+
return {
|
|
1959
|
+
outcome: "claimed",
|
|
1960
|
+
...(Number.isFinite(credit) && credit > 0 ? { credit } : {}),
|
|
1961
|
+
...(Number.isFinite(streak) && streak > 0 ? { streak } : {}),
|
|
1962
|
+
...(got?.message ? { message: String(got.message) } : {}),
|
|
1963
|
+
}
|
|
1964
|
+
} catch (e) {
|
|
1965
|
+
if (e?.code === 1001) return { outcome: "done" }
|
|
1966
|
+
if (e?.code === 1002) return { outcome: "ineligible", message: e.message }
|
|
1967
|
+
if (e?.code === 1003) return { outcome: "inactive", message: e.message }
|
|
1968
|
+
if (e?.status === 401) {
|
|
1969
|
+
try {
|
|
1970
|
+
await meter(site, a, "/billing/meter/get-user-resource-summary")
|
|
1971
|
+
} catch {
|
|
1972
|
+
throw e
|
|
1973
|
+
}
|
|
1974
|
+
return { outcome: "ineligible", message: "WorkBuddy's check-in takes WorkBuddy's own app" }
|
|
1975
|
+
}
|
|
1976
|
+
if (e?.unreadable) return { outcome: "failed", message: `check-in: ${e.message}` }
|
|
1977
|
+
const transient = e?.status >= 500 || e?.code === 10000
|
|
1978
|
+
if (transient && attempt < waits.length) {
|
|
1979
|
+
await new Promise((r) => setTimeout(r, waits[attempt]))
|
|
1980
|
+
break
|
|
1981
|
+
}
|
|
1982
|
+
// a reply WorkBuddy sent is the day's failed answer; only a request
|
|
1983
|
+
// that got no answer leaves as a throw, for magpie's offline backoff
|
|
1984
|
+
if (e?.status !== undefined || e?.code !== undefined) return { outcome: "failed", message: e.message }
|
|
1985
|
+
throw e
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1888
1991
|
// capacity is a capacity the billing API sends, as a string ("438.88")
|
|
1889
1992
|
// or now and then a number; empty is 0.
|
|
1890
1993
|
function capacity(v) {
|
|
@@ -3036,6 +3139,25 @@ function makePlugin(site) {
|
|
|
3036
3139
|
// and is renewed as before.
|
|
3037
3140
|
refreshLead: LEAD_MS,
|
|
3038
3141
|
refresh: (auth) => refreshed(site, auth),
|
|
3142
|
+
// the account's daily check-in: magpie can run it once a day per
|
|
3143
|
+
// account, when its WorkBuddy check-in switch is on; the button in
|
|
3144
|
+
// Settings asks right away. China's meter only: the AI build has no
|
|
3145
|
+
// check-in (#47), so the hook stays off its plugin. Errors leave as
|
|
3146
|
+
// throws, not as failed answers: host.js answers a throwing hook to
|
|
3147
|
+
// magpie as an error, whose wbCheckinFailed keeps the offline words
|
|
3148
|
+
// and magpie's 1-minute backoff, where a returned failed waits the
|
|
3149
|
+
// full 30 minutes (magpie-community/plugins#149). checkinOf returns
|
|
3150
|
+
// an outcome only for answers it read and classified.
|
|
3151
|
+
...(site.ai ? {} : {
|
|
3152
|
+
async checkin(getAuth) {
|
|
3153
|
+
const auth = await getAuth()
|
|
3154
|
+
// an API key reads no credits (see usage), so it has no day to claim
|
|
3155
|
+
if (auth?.type === "api") return { outcome: "ineligible", message: "check-in needs a signed-in account, not an API key" }
|
|
3156
|
+
if (auth?.type !== "oauth" || !(auth.access || auth.source)) return { outcome: "failed", message: "not signed in" }
|
|
3157
|
+
const a = await current(site, client, auth)
|
|
3158
|
+
return await checkinOf(site, a, { waits: [2000, 4000] })
|
|
3159
|
+
},
|
|
3160
|
+
}),
|
|
3039
3161
|
options: [
|
|
3040
3162
|
{
|
|
3041
3163
|
key: FREE_ONLY,
|
|
@@ -3409,4 +3531,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
3409
3531
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
3410
3532
|
|
|
3411
3533
|
// for tests
|
|
3412
|
-
export const _internal = { idFor, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, scopeOf, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, freeWindowOf, tokensShort, freeWindowRows, freeWindowLedger, readLedgerTail, pinned, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
|
3534
|
+
export const _internal = { idFor, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, accountName, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, scopeOf, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, freeWindowOf, tokensShort, freeWindowRows, freeWindowLedger, readLedgerTail, pinned, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, checkinOf, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
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.2.1",
|
|
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",
|