@magpie-community/opencode-zcode-auth 0.1.18 → 0.1.20
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 +24 -1
- package/index.mjs +114 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,9 +24,23 @@ over Anthropic's Messages API. Provider id: `zcode`.
|
|
|
24
24
|
ZCode's does: ZCode's headers, ZCode's system prompt ahead of the
|
|
25
25
|
agent's, the day in a `<system-reminder>` turn before the agent's
|
|
26
26
|
turns, and the sign-in's device id in `metadata.user_id`. GLM Coding
|
|
27
|
-
Plan and team requests go as the agent sent them.
|
|
27
|
+
Plan and team requests go as the agent sent them. One it still turns
|
|
28
|
+
away (a 405, the block page or code 3012) comes back with what to do
|
|
29
|
+
about it, as magpie's built-in said: "ZCode's Start Plan still turned
|
|
30
|
+
this request away, though magpie sends it as the ZCode app does; …
|
|
31
|
+
Use an account with a GLM Coding Plan, or add another provider to this
|
|
32
|
+
group".
|
|
28
33
|
- The Start Plan's token can't be refreshed. When it runs out, sign in
|
|
29
34
|
again.
|
|
35
|
+
- **ZCode: a team's GLM Coding Plan**
|
|
36
|
+
- For an account with a plan of its own and a seat on a team's plan
|
|
37
|
+
(#285). The sign-ins above take the account's own plan, as ZCode does
|
|
38
|
+
by default; this one asks which site, signs in the same way, and
|
|
39
|
+
takes the team seat's `zcode-team-api-key`, as ZCode does when its
|
|
40
|
+
plan picker is switched to the team's plan.
|
|
41
|
+
- It is kept as an account of its own, named `<email> (team)`, beside
|
|
42
|
+
the account's own plan, so both can be used.
|
|
43
|
+
- With no seat on a team's plan, it says so and signs in to nothing.
|
|
30
44
|
- **ZCode app's sign-in**
|
|
31
45
|
- Uses the account the ZCode app on this computer is signed in to. For
|
|
32
46
|
every request it reads ZCode's own credentials (`~/.zcode/v2`), so it
|
|
@@ -96,6 +110,15 @@ behind an error, so a gift hiccup must not black out a working Coding
|
|
|
96
110
|
Plan. A gift that goes out of date while requests use it simply stops
|
|
97
111
|
showing.
|
|
98
112
|
|
|
113
|
+
**Resets.** A GLM Coding Plan of the account's own, and a team seat,
|
|
114
|
+
show the resets the vendor's usage page lists
|
|
115
|
+
(`/api/biz/customer-package-reset/list`, `targetType=PERSONAL` with the
|
|
116
|
+
plan's key, `TEAM` with the team's sign-in): how many of the five hours
|
|
117
|
+
and of the week are still available, and when each runs out, soonest
|
|
118
|
+
first. A team's are marked a team's. They are only read; a reset is
|
|
119
|
+
spent on the vendor's own page. A list that can't be read, or has none
|
|
120
|
+
left, shows no count.
|
|
121
|
+
|
|
99
122
|
**Gift plans ZCode holds but has not granted.** ZCode hands out one-off
|
|
100
123
|
trust-build / gift plans (a day's ZCode Trust Build, say) that are not
|
|
101
124
|
on the account until the ZCode app claims them. The plugin reads the
|
package/index.mjs
CHANGED
|
@@ -436,9 +436,12 @@ async function codingUsage(s) {
|
|
|
436
436
|
const level = typeof d.level === "string" ? d.level : ""
|
|
437
437
|
if (level) out.plan = "GLM Coding " + level[0].toUpperCase() + level.slice(1)
|
|
438
438
|
out.windows = limitWindows(d)
|
|
439
|
+
const resets = personalResets(root, s.key)
|
|
439
440
|
try {
|
|
440
441
|
Object.assign(out, termOf(await call("GET", root + "/api/biz/subscription/list", { auth: s.key })))
|
|
441
442
|
} catch {}
|
|
443
|
+
const r = await resets
|
|
444
|
+
if (r) out.resets = r
|
|
442
445
|
return out
|
|
443
446
|
}
|
|
444
447
|
|
|
@@ -708,31 +711,56 @@ async function teamUsage(s, key) {
|
|
|
708
711
|
}
|
|
709
712
|
|
|
710
713
|
// teamResets is how many resets a team member may spend, of the five hours
|
|
711
|
-
// and of the week:
|
|
712
|
-
// null when it can't be told or there are none.
|
|
714
|
+
// and of the week, marked a team's (team: true), as the built-in's
|
|
715
|
+
// zhipuTeamResets did. null when it can't be told or there are none.
|
|
713
716
|
async function teamResets(root, auth, headers) {
|
|
717
|
+
const r = await resetsOf(root + "/api/biz/customer-package-reset/list?targetType=TEAM", auth, headers)
|
|
718
|
+
if (r) r.team = true
|
|
719
|
+
return r
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// personalResets is how many resets a GLM Coding Plan of a person's own
|
|
723
|
+
// holds, as bigmodel.cn's usage page lists them, asked with the plan's
|
|
724
|
+
// key (the built-in's zhipuPersonalResets). Only read: a reset is spent
|
|
725
|
+
// on the vendor's own page, never from here.
|
|
726
|
+
async function personalResets(root, key) {
|
|
727
|
+
return resetsOf(root + "/api/biz/customer-package-reset/list?targetType=PERSONAL", key)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// resetsOf reads customer-package-reset/list: the resets of the five
|
|
731
|
+
// hours and of the week, of which only those still available count (one
|
|
732
|
+
// run out stays listed a week, available false), each with its own end
|
|
733
|
+
// (each, soonest first, one with no end last) and the soonest as until.
|
|
734
|
+
// null when the list can't be read, so no count is shown rather than 0,
|
|
735
|
+
// and when it holds none.
|
|
736
|
+
async function resetsOf(url, auth, headers) {
|
|
714
737
|
let d
|
|
715
738
|
try {
|
|
716
|
-
d = (await call("GET",
|
|
739
|
+
d = (await call("GET", url, { auth, headers })) ?? {}
|
|
717
740
|
} catch {
|
|
718
741
|
return null
|
|
719
742
|
}
|
|
720
743
|
let until = 0
|
|
721
|
-
const
|
|
744
|
+
const each = []
|
|
745
|
+
const count = (rs, window) => {
|
|
722
746
|
let n = 0
|
|
723
747
|
for (const x of Array.isArray(rs) ? rs : []) {
|
|
724
748
|
if (x?.available !== true) continue
|
|
725
749
|
n++
|
|
726
750
|
const t = when(x.expireTime)
|
|
727
751
|
if (t && (!until || t < until)) until = t
|
|
752
|
+
each.push(t ? { until: new Date(t).toISOString(), window } : { window })
|
|
728
753
|
}
|
|
729
754
|
return n
|
|
730
755
|
}
|
|
731
|
-
const fiveHour = count(d.fiveHourResets)
|
|
732
|
-
const weekly = count(d.weekResets)
|
|
756
|
+
const fiveHour = count(d.fiveHourResets, "fiveHour")
|
|
757
|
+
const weekly = count(d.weekResets, "weekly")
|
|
733
758
|
if (fiveHour + weekly === 0) return null
|
|
759
|
+
const end = (c) => (c.until ? Date.parse(c.until) : Infinity)
|
|
760
|
+
each.sort((a, b) => (end(a) === end(b) ? 0 : end(a) < end(b) ? -1 : 1))
|
|
734
761
|
const r = { count: fiveHour + weekly, byWindow: true, fiveHour, weekly }
|
|
735
762
|
if (until) r.until = new Date(until).toISOString()
|
|
763
|
+
r.each = each
|
|
736
764
|
return r
|
|
737
765
|
}
|
|
738
766
|
|
|
@@ -819,13 +847,29 @@ async function teamSignIn(site, root, auth, info) {
|
|
|
819
847
|
|
|
820
848
|
// signedIn is what a sign-in gives: the account's coding plan key and plan;
|
|
821
849
|
// or, with none of its own, a seat on a team's plan; or else ZCode's token
|
|
822
|
-
// for its Start Plan while the account has one.
|
|
823
|
-
|
|
850
|
+
// for its Start Plan while the account has one. Asked for the team's plan
|
|
851
|
+
// (onTeam), it is the team seat alone, as ZCode's plan picker gives a team
|
|
852
|
+
// plan beside the account's own (a "team-coding-plan" selection): an
|
|
853
|
+
// account with both is signed in to each as an account of its own (#285).
|
|
854
|
+
async function signedIn(site, token, jwt, device, onTeam = false) {
|
|
824
855
|
const name = SITES[site].name
|
|
825
856
|
const root = SITES[site].api
|
|
826
857
|
let s = null
|
|
827
858
|
let err = null
|
|
828
859
|
let team = ""
|
|
860
|
+
if (onTeam) {
|
|
861
|
+
const auth = await bizAuth(site, token)
|
|
862
|
+
let info
|
|
863
|
+
try {
|
|
864
|
+
info = await call("GET", root + "/api/biz/customer/getCustomerInfo", { auth })
|
|
865
|
+
} catch (e) {
|
|
866
|
+
throw new Error(`${name} account: ${e.message}`)
|
|
867
|
+
}
|
|
868
|
+
const t = await teamSignIn(site, root, auth, info)
|
|
869
|
+
if (t.s) return { ...t.s, jwt, plan: t.plan }
|
|
870
|
+
if (t.none) throw new Error(`this ${name} account has no seat on a team's GLM Coding Plan — sign in with "ZCode: ${name} GLM Coding Plan" for its own plan`)
|
|
871
|
+
throw new Error(t.why ?? t.err)
|
|
872
|
+
}
|
|
829
873
|
try {
|
|
830
874
|
const auth = await bizAuth(site, token)
|
|
831
875
|
let info
|
|
@@ -863,11 +907,15 @@ async function signedIn(site, token, jwt, device) {
|
|
|
863
907
|
)
|
|
864
908
|
}
|
|
865
909
|
|
|
910
|
+
// TEAM marks a team seat's account name beside the account's own.
|
|
911
|
+
const TEAM = "team"
|
|
866
912
|
const who = (u) => first(String(u?.email ?? "").replace(/@phone\.local$/, ""), u?.name, u?.user_id, "ZCode")
|
|
867
913
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
868
914
|
|
|
869
915
|
// authorize opens ZCode's sign-in flow on site; its callback polls it.
|
|
870
|
-
|
|
916
|
+
// onTeam signs in to the account's seat on a team's plan, named apart from
|
|
917
|
+
// the account's own plan (the host keeps one entry per account name).
|
|
918
|
+
async function authorize(site, log, onTeam = false) {
|
|
871
919
|
const poll = "Bearer " + Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex")
|
|
872
920
|
const device = uuid()
|
|
873
921
|
let flow
|
|
@@ -908,7 +956,7 @@ async function authorize(site, log) {
|
|
|
908
956
|
if (got.status !== "ready" || !token) return fail("ZCode sign-in: unexpected answer " + got.status)
|
|
909
957
|
let s
|
|
910
958
|
try {
|
|
911
|
-
s = await signedIn(site, token, first(got.token), device)
|
|
959
|
+
s = await signedIn(site, token, first(got.token), device, onTeam)
|
|
912
960
|
} catch (e) {
|
|
913
961
|
return fail(e.message)
|
|
914
962
|
}
|
|
@@ -919,7 +967,7 @@ async function authorize(site, log) {
|
|
|
919
967
|
access: s.key || s.jwt || "",
|
|
920
968
|
// the key doesn't run out; ZCode's token, when it is all there is, does
|
|
921
969
|
expires: s.key || isTeam(s) ? 0 : jwtExpiry(s.jwt),
|
|
922
|
-
accountId: who(got.user),
|
|
970
|
+
accountId: onTeam ? `${who(got.user)} (${TEAM})` : who(got.user),
|
|
923
971
|
}
|
|
924
972
|
}
|
|
925
973
|
},
|
|
@@ -1311,6 +1359,36 @@ function model(m, url) {
|
|
|
1311
1359
|
// the plan serves (ARNO's "Freeflow", provider zcode-start) sends it: its
|
|
1312
1360
|
// headers (startHeaders) and its body (dress). The texts are prompt.mjs.
|
|
1313
1361
|
|
|
1362
|
+
// START_BLOCKED is what the Start Plan still turning a request away means,
|
|
1363
|
+
// word for word magpie's provider.ZCodeStartBlockedHint (#425), which
|
|
1364
|
+
// magpie's Routing page knows and says in the user's language. It is put
|
|
1365
|
+
// in place of the block page, whose words magpie would otherwise read as a
|
|
1366
|
+
// network firewall's (provider.BlockedHint).
|
|
1367
|
+
const START_BLOCKED = "ZCode's Start Plan still turned this request away, though magpie sends it as the ZCode app does; it can be a network block of this IP, or ZCode checking for something new. Use an account with a GLM Coding Plan, or add another provider to this group"
|
|
1368
|
+
const EDGE_BLOCKED = /request has been blocked|errors\.aliyun\.com/i
|
|
1369
|
+
const START_REFUSED = /unusual activity|"code"\s*:\s*"?3012\b/i
|
|
1370
|
+
|
|
1371
|
+
// startRefused says a Start Plan answer is that refusal, as the built-in's
|
|
1372
|
+
// zcodeStartExplain read it: a 405, a block page or code 3012.
|
|
1373
|
+
const startRefused = (status, text) => status >= 400 && (status === 405 || EDGE_BLOCKED.test(text) || START_REFUSED.test(text))
|
|
1374
|
+
|
|
1375
|
+
// startBlocked is the refusal's answer as the built-in passed it on: what
|
|
1376
|
+
// the vendor said (its status for a block page), then START_BLOCKED.
|
|
1377
|
+
function startBlocked(res, text) {
|
|
1378
|
+
let said = ""
|
|
1379
|
+
if (!EDGE_BLOCKED.test(text)) {
|
|
1380
|
+
try {
|
|
1381
|
+
const v = JSON.parse(text)
|
|
1382
|
+
said = first(v?.error?.message, typeof v?.error === "string" ? v.error : "", v?.message, v?.msg)
|
|
1383
|
+
} catch {}
|
|
1384
|
+
}
|
|
1385
|
+
said ||= `${res.status} ${STATUS_TEXT[res.status] ?? res.statusText}`.trim()
|
|
1386
|
+
const h = new Headers(res.headers)
|
|
1387
|
+
h.set("content-type", "application/json")
|
|
1388
|
+
const error = { type: "permission_error", message: `${said} — ${START_BLOCKED}` }
|
|
1389
|
+
return new Response(JSON.stringify({ type: "error", error }), { status: res.status, statusText: res.statusText, headers: h })
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1314
1392
|
const osVersion = () => `${process.platform} ${release()} ${arch()}`
|
|
1315
1393
|
const osCategory = () => (process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux")
|
|
1316
1394
|
|
|
@@ -1594,6 +1672,13 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1594
1672
|
h.set("Authorization", "Bearer " + key)
|
|
1595
1673
|
}
|
|
1596
1674
|
let res = await fetch(url, { ...opts, headers: h })
|
|
1675
|
+
// the Start Plan turning the request away says what to do about
|
|
1676
|
+
// it, as the built-in's zcodeStartExplain did
|
|
1677
|
+
if (start && res.status >= 400) {
|
|
1678
|
+
const text = await res.text()
|
|
1679
|
+
if (startRefused(res.status, text)) return kept(startBlocked(res, text))
|
|
1680
|
+
res = new Response(text, { status: res.status, statusText: res.statusText, headers: res.headers })
|
|
1681
|
+
}
|
|
1597
1682
|
// a trial entry's gift answer that can't serve the request is
|
|
1598
1683
|
// the agent's answer, not the coding plan's: the user picked the
|
|
1599
1684
|
// gift, so a spent bucket says 429 (its quota error inside a 200
|
|
@@ -1668,6 +1753,24 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1668
1753
|
methods: [
|
|
1669
1754
|
oauth("zai"),
|
|
1670
1755
|
oauth("bigmodel"),
|
|
1756
|
+
// a team's plan beside the account's own (#285): the sign-ins above
|
|
1757
|
+
// take the account's own plan when it has one
|
|
1758
|
+
{
|
|
1759
|
+
type: "oauth",
|
|
1760
|
+
label: "ZCode: a team's GLM Coding Plan",
|
|
1761
|
+
prompts: [
|
|
1762
|
+
{
|
|
1763
|
+
type: "select",
|
|
1764
|
+
key: "site",
|
|
1765
|
+
message: "Where is the team's plan?",
|
|
1766
|
+
options: [
|
|
1767
|
+
{ label: "Z.ai", value: "zai", hint: "z.ai" },
|
|
1768
|
+
{ label: "BigModel (智谱)", value: "bigmodel", hint: "bigmodel.cn" },
|
|
1769
|
+
],
|
|
1770
|
+
},
|
|
1771
|
+
],
|
|
1772
|
+
authorize: (inputs) => authorize(inputs?.site === "bigmodel" ? "bigmodel" : "zai", log, true),
|
|
1773
|
+
},
|
|
1671
1774
|
{ type: "oauth", label: "ZCode app's sign-in", authorize: async () => asZCode() },
|
|
1672
1775
|
{
|
|
1673
1776
|
type: "api",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-zcode-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "OpenCode provider plugin: signs in to Z.ai's or BigModel's (智谱) GLM Coding Plan as ZCode does, including team seats and ZCode's Start Plan",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|