@magpie-community/opencode-workbuddy-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 +15 -0
- package/index.mjs +77 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,6 +99,15 @@ models to each shape below and Kimi's, GPT's, Gemini's and GLM's to some:
|
|
|
99
99
|
(`.`, `:`, `/`, a space, CJK) is sent with `_` there, and the model's
|
|
100
100
|
calls come back under the tool's own name.
|
|
101
101
|
|
|
102
|
+
WorkBuddy AI's `gemini-3.8-flash` answers 400 "the conversation must end
|
|
103
|
+
with a user message" (11153) to a chat whose tool results are followed by
|
|
104
|
+
the user's message, as magpie sends Claude Code's reminders, words typed
|
|
105
|
+
mid-task and a tool's images. Such a chat goes again with an empty
|
|
106
|
+
assistant message between the results and the user's message, which it
|
|
107
|
+
serves; a model that refused it once gets it that way from then on, until
|
|
108
|
+
the plugin restarts. A chat ending with the assistant's message (a prefill)
|
|
109
|
+
is refused as before.
|
|
110
|
+
|
|
102
111
|
## Models
|
|
103
112
|
|
|
104
113
|
When you are signed in, the models are the ones WorkBuddy's product config
|
|
@@ -134,6 +143,12 @@ client reads. The daily Free Plan slice isn't counted: it ends at the same
|
|
|
134
143
|
midnight on every account. magpie's smart routing spends the account whose
|
|
135
144
|
credits run out first.
|
|
136
145
|
|
|
146
|
+
An enterprise (team) account's resource summary lists no packages. Its
|
|
147
|
+
credits are read from the enterprise meter
|
|
148
|
+
(`/billing/meter/get-enterprise-user-usage`), the one WorkBuddy's own usage
|
|
149
|
+
page reads: the cycle's credits used of its limit, and when the cycle
|
|
150
|
+
resets (magpie #1904).
|
|
151
|
+
|
|
137
152
|
## Not here
|
|
138
153
|
|
|
139
154
|
magpie's built-in WorkBuddy does more than this package:
|
package/index.mjs
CHANGED
|
@@ -1056,6 +1056,25 @@ function usageOf(sum, plan) {
|
|
|
1056
1056
|
return out
|
|
1057
1057
|
}
|
|
1058
1058
|
|
|
1059
|
+
// enterpriseUsageOf is an enterprise (team) account's credits, from
|
|
1060
|
+
// /billing/meter/get-enterprise-user-usage, the meter WorkBuddy's own usage
|
|
1061
|
+
// page (www.workbuddy.cn/profile/usage) reads for one (magpie #1904,
|
|
1062
|
+
// CnsMaple): the resource summary answers such an account with no
|
|
1063
|
+
// packages at all. It counts the cycle's credits flat — credit used of
|
|
1064
|
+
// limitNum — and when the cycle began and resets (UTC+8). null when it
|
|
1065
|
+
// grants none.
|
|
1066
|
+
function enterpriseUsageOf(u) {
|
|
1067
|
+
const limit = capacity(u?.limitNum)
|
|
1068
|
+
if (!(limit > 0)) return null
|
|
1069
|
+
const used = capacity(u?.credit)
|
|
1070
|
+
const w = { name: "Credits", used: (100 * used) / limit, display: `${compact(used)} / ${compact(limit)}`, amount: used, limit, unit: "credits" }
|
|
1071
|
+
const at = (v) => (typeof v === "string" && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(v.trim()) ? Date.parse(v.trim().replace(" ", "T") + "+08:00") : 0)
|
|
1072
|
+
const reset = at(u?.cycleResetTime), start = at(u?.cycleStartTime)
|
|
1073
|
+
if (reset > 0) w.resetsAt = new Date(reset).toISOString()
|
|
1074
|
+
if (reset > start && start > 0) w.span = (reset - start) / 1000
|
|
1075
|
+
return w
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1059
1078
|
// The packages' codes, as WorkBuddy's own client lists them when it asks
|
|
1060
1079
|
// for each package's credits (its COMMODITY_CODES): the API refuses a
|
|
1061
1080
|
// detail list without them (10001 "PackageCodes required").
|
|
@@ -1243,7 +1262,37 @@ async function liveModels(site, a) {
|
|
|
1243
1262
|
|
|
1244
1263
|
// ---- the plugins --------------------------------------------------------------
|
|
1245
1264
|
|
|
1265
|
+
// AFTER_TOOLS_REFUSED is WorkBuddy's 400 code 11153, "the conversation must
|
|
1266
|
+
// end with a user message", which WorkBuddy AI's gemini-3.8-flash answers to a
|
|
1267
|
+
// chat whose tool results are followed by the user's message (#113): Claude
|
|
1268
|
+
// Code's reminders, a user's words sent mid-task, a tool's images, which
|
|
1269
|
+
// magpie hands over in a user message after the results. It does end with a
|
|
1270
|
+
// user message; the same chat with any other message between them is served.
|
|
1271
|
+
// The code is matched whole, as with 14018.
|
|
1272
|
+
const AFTER_TOOLS_REFUSED = /must end with a user message|["']code["']\s*:\s*["']?11153(?![\d.])/i
|
|
1273
|
+
|
|
1274
|
+
// replyAfterTools is a chat ending with tool results then the user's
|
|
1275
|
+
// message(s), with an empty assistant message between them, as if the model
|
|
1276
|
+
// had said nothing after the results. Tried with a real WorkBuddy AI
|
|
1277
|
+
// account on 2026-10-14: gemini-3.8-flash serves it and follows the user's
|
|
1278
|
+
// message as a user's, where folded into the tool's result it was read as
|
|
1279
|
+
// the tool's output and its ask left undone. null for any other chat — one
|
|
1280
|
+
// ending with the assistant's message is a prefill, refused as it is.
|
|
1281
|
+
function replyAfterTools(body) {
|
|
1282
|
+
const b = chatOf(body)
|
|
1283
|
+
const msgs = b?.messages
|
|
1284
|
+
if (!Array.isArray(msgs)) return null
|
|
1285
|
+
let i = msgs.length - 1
|
|
1286
|
+
while (i >= 0 && msgs[i]?.role === "user") i--
|
|
1287
|
+
if (i === msgs.length - 1 || i < 0 || msgs[i]?.role !== "tool") return null
|
|
1288
|
+
msgs.splice(i + 1, 0, { role: "assistant", content: "" })
|
|
1289
|
+
return JSON.stringify(b)
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1246
1292
|
function makePlugin(site) {
|
|
1293
|
+
// the models of this site that refused tool results then the user's
|
|
1294
|
+
// message, for this run: their next chats go with replyAfterTools at once
|
|
1295
|
+
const endsAfterTools = new Set()
|
|
1247
1296
|
return async ({ client }) => ({
|
|
1248
1297
|
config: async (config) => {
|
|
1249
1298
|
config.provider ??= {}
|
|
@@ -1309,7 +1358,22 @@ function makePlugin(site) {
|
|
|
1309
1358
|
const fit = fitted(body)
|
|
1310
1359
|
body = normalizeToolChoice(fit.body)
|
|
1311
1360
|
attend(headers, body, session)
|
|
1312
|
-
const
|
|
1361
|
+
const send = (b) => fetch(req ? req.url : input, { ...init, method: init?.method ?? req?.method, headers, body: b })
|
|
1362
|
+
// a model known to turn tool results then the user's message away
|
|
1363
|
+
// gets the empty reply between them up front (see endsAfterTools)
|
|
1364
|
+
const model = chatOf(body)?.model
|
|
1365
|
+
const quiet = endsAfterTools.has(model) ? replyAfterTools(body) : null
|
|
1366
|
+
let res = await send(quiet ?? body)
|
|
1367
|
+
if (res.status === 400 && !quiet) {
|
|
1368
|
+
const text = await res.text()
|
|
1369
|
+
const again = AFTER_TOOLS_REFUSED.test(text) ? replyAfterTools(body) : null
|
|
1370
|
+
if (again) {
|
|
1371
|
+
endsAfterTools.add(model)
|
|
1372
|
+
res = await send(again)
|
|
1373
|
+
} else {
|
|
1374
|
+
res = new Response(text, { status: res.status, statusText: res.statusText, headers: res.headers })
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1313
1377
|
return res.status >= 400 ? explained(res) : kept(await renamed(res, fit.names))
|
|
1314
1378
|
},
|
|
1315
1379
|
}
|
|
@@ -1325,6 +1389,17 @@ function makePlugin(site) {
|
|
|
1325
1389
|
// the meter has no /v2 prefix, on either site
|
|
1326
1390
|
const sum = await meter(site, a, "/billing/meter/get-user-resource-summary")
|
|
1327
1391
|
const out = usageOf(sum, auth.plan)
|
|
1392
|
+
// an enterprise account's summary has no packages: its credits
|
|
1393
|
+
// are the enterprise meter's (#1904). A personal account's
|
|
1394
|
+
// refusal of it (10001 "uid or enterpriseID is empty") leaves
|
|
1395
|
+
// the card as the summary said.
|
|
1396
|
+
if (!out.windows.length) {
|
|
1397
|
+
let ent = null
|
|
1398
|
+
try {
|
|
1399
|
+
ent = enterpriseUsageOf(await meter(site, a, "/billing/meter/get-enterprise-user-usage"))
|
|
1400
|
+
} catch {}
|
|
1401
|
+
if (ent) return { plan: auth.plan || "Enterprise", windows: [ent], signIn: "kept" }
|
|
1402
|
+
}
|
|
1328
1403
|
// when the credits left run out, for routing to spend them first;
|
|
1329
1404
|
// a list that can't be read leaves it unknown, the card stands
|
|
1330
1405
|
const credits = out.windows.find((w) => w.name === "Credits")
|
|
@@ -1438,4 +1513,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
1438
1513
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
1439
1514
|
|
|
1440
1515
|
// for tests
|
|
1441
|
-
export const _internal = { notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf }
|
|
1516
|
+
export const _internal = { replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_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.20",
|
|
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",
|