@magpie-community/opencode-trae-auth 0.2.0 → 0.2.2

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.
Files changed (3) hide show
  1. package/README.md +12 -5
  2. package/index.mjs +60 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -192,16 +192,23 @@ Trae CN gives credits for a daily check-in (每日签到); Trae Global has no
192
192
  such page, so this is CN only. magpie can press it
193
193
  once a day for each account (Settings, or the switch on the usage card).
194
194
  It asks Trae CN's own pages through
195
- this plugin's fetch, which sends them as the account: its Cloud-IDE-JWT
196
- (renewed first when near its end) and its device id.
195
+ this plugin's fetch, which sends them as the account, as Trae CN's IDE
196
+ (3.3.104) sends them: its Cloud-IDE-JWT (renewed first when near its
197
+ end), its device (`x-device-id`, `x-device-brand`, `x-device-type`,
198
+ `x-os-version`, `x-app-version`) and a client's User-Agent (TRAE SOLO
199
+ CN's Electron shell). Since 0.2.1: Bun's own User-Agent and the chat's
200
+ headers got 9074 「当前参与用户太多,请稍后再试」 every time
201
+ (yetone/magpie#808).
197
202
 
198
203
  - **Status:** `POST api.trae.cn/trae/api/v2/ug/checkin_credits/status`,
199
- body `{}`: `enable`, `checked_in`, `credits`.
204
+ body `{"req_source":1}` (magpie's `{}` is sent as that): `enable`,
205
+ `checked_in`, `credits`.
200
206
  - **Claim:** `POST api.trae.cn/trae/api/v2/ug/checkin_credits/claim`,
201
- body `{}`, only while it is on and today's isn't in.
207
+ the same, only while it is on and today's isn't in.
202
208
  - **What comes back:** Trae's answer as it is. A 401 or code 1001 marks
203
209
  the account for a new sign-in. Code 9095 means this device has checked
204
- in today; nothing sends another device id to get round it.
210
+ in today; nothing sends another device id to get round it, nor to get
211
+ round a 9074.
205
212
 
206
213
  Only `api.trae.cn`'s `/trae/api/` pages are sent this way; any other URL
207
214
  that isn't a chat request is still refused (400). Needs 0.1.4 or later.
package/index.mjs CHANGED
@@ -529,6 +529,42 @@ function ideHeaders(a, site, extra = {}) {
529
529
  }
530
530
  }
531
531
 
532
+ // ugUA is the User-Agent a growth page (/trae/api/v2/ug/…: the daily
533
+ // check-in) is sent with: TRAE SOLO CN's Electron shell, as its version
534
+ // is named. Bun's own (Bun/1.x) reads as a script, and Trae answers a
535
+ // script's check-in with 9074 「当前参与用户太多,请稍后再试」 however
536
+ // seldom it asks (yetone/magpie#808; qilimixingkong/trae-checkin found
537
+ // the same).
538
+ const ugUA = (site) =>
539
+ `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) TRAE-SOLO-CN/${site.clientVersion} Chrome/120.0.6099.291 Electron/28.2.10 Safari/537.36`
540
+
541
+ // ugHeaders are the headers Trae CN's IDE (3.3.104) sends a growth page
542
+ // with, and only those: its JWT, then its device (fb: x-device-id,
543
+ // x-device-brand, x-device-type, x-os-version, x-app-version), not the
544
+ // chat's tokens and versions.
545
+ function ugHeaders(a, site, extra = {}) {
546
+ return {
547
+ "Content-Type": "application/json",
548
+ Authorization: `Cloud-IDE-JWT ${a.token}`,
549
+ "User-Agent": ugUA(site),
550
+ "x-device-id": a.deviceId,
551
+ "x-device-brand": site.brand,
552
+ "x-device-type": "Windows",
553
+ "x-os-version": "10.0.22631",
554
+ "x-app-version": site.clientVersion,
555
+ ...extra,
556
+ }
557
+ }
558
+
559
+ // ugPage: url is one of Trae's growth pages (the check-in)
560
+ const ugPage = (url) => {
561
+ try {
562
+ return new URL(url).pathname.startsWith("/trae/api/v2/ug/")
563
+ } catch {
564
+ return false
565
+ }
566
+ }
567
+
532
568
  // Trae's chat takes text only and has no turn for a tool call or its
533
569
  // result. The agent's tools are named to it natively and in a system
534
570
  // prompt that asks for each call as a tagged block; earlier calls and
@@ -1047,6 +1083,7 @@ class NativeCalls {
1047
1083
  const next = c.arguments
1048
1084
  if (!next || prev.startsWith(next)) return // nothing new
1049
1085
  if (next.startsWith(prev) || done(prev)) o.arguments = next // the arguments so far
1086
+ else if (resent(prev, next)) o.arguments = next // the whole call again, written apart (plugins#24)
1050
1087
  else o.arguments = prev + next // a piece
1051
1088
  }
1052
1089
  take(tools = []) {
@@ -1064,6 +1101,19 @@ class NativeCalls {
1064
1101
  }
1065
1102
  }
1066
1103
 
1104
+ // resent says next is the call's whole arguments sent again after a
1105
+ // partial snapshot, not a piece of them: Trae resends a snapshot whose
1106
+ // punctuation isn't the first one's (− for -, ′ for '), so it doesn't
1107
+ // start with it. A whole object under the same first key as prev's, which
1108
+ // prev and it joined don't make whole; a piece that is a value of its own
1109
+ // (12, "ls", {}) doesn't open with prev's first key.
1110
+ function resent(prev, next) {
1111
+ const key = (s) => s.match(/^\s*\{\s*"((?:[^"\\]|\\.)*)"/)?.[1]
1112
+ if (!next.trimStart().startsWith("{") || !done(next) || done(prev + next)) return false
1113
+ const k = key(prev)
1114
+ return k !== undefined && k === key(next)
1115
+ }
1116
+
1067
1117
  // done says arguments are a whole JSON value already
1068
1118
  function done(a) {
1069
1119
  if (!a) return false
@@ -1461,14 +1511,18 @@ const makePlugin = (site) => async ({ client }) => {
1461
1511
  if (e instanceof Expired) return errorResponse(401, `${a0.name || a0.uid}: ${e.message}`, "expired")
1462
1512
  throw e
1463
1513
  }
1464
- // Trae's pages, the check-in among them, go with the chat's headers and
1465
- // the body given ({}): 0.1.7 sent the check-in with the IDE's growth
1466
- // headers and {req_source: 1} instead, and a check-in that worked on
1467
- // 0.1.6 failed from then on (yetone/magpie#808)
1514
+ // a growth page (the check-in) goes as the IDE sends it: its headers,
1515
+ // a client's User-Agent, and {req_source: 1} for an empty body. Every
1516
+ // check-in sent with Bun's User-Agent came back 9074, with the chat's
1517
+ // headers and {} (0.1.6, 0.1.9) as with the IDE's (0.1.7)
1518
+ // (yetone/magpie#808). Other pages go with the chat's headers.
1519
+ const ug = ugPage(url)
1520
+ let sent = method === "GET" || method === "HEAD" ? undefined : typeof body === "string" ? body : "{}"
1521
+ if (ug && sent !== undefined && /^\s*(\{\s*\})?\s*$/.test(sent)) sent = JSON.stringify({ req_source: 1 })
1468
1522
  const res = await fetch(url, {
1469
1523
  method,
1470
- headers: ideHeaders(a, site, { Accept: "application/json" }),
1471
- body: method === "GET" || method === "HEAD" ? undefined : typeof body === "string" ? body : "{}",
1524
+ headers: ug ? ugHeaders(a, site, { Accept: "application/json" }) : ideHeaders(a, site, { Accept: "application/json" }),
1525
+ body: sent,
1472
1526
  signal: signal ?? AbortSignal.timeout(20_000),
1473
1527
  })
1474
1528
  const text = await res.text()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-trae-auth",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "OpenCode provider plugins for Trae: Trae CN (trae.cn) and Trae Global (trae.ai), free tier included, with Trae's own browser sign-in",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",