@magpie-community/opencode-trae-auth 0.1.6 → 0.1.8

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 +6 -4
  2. package/index.mjs +91 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -114,13 +114,15 @@ magpie's usage card shows the account's credits:
114
114
  Trae CN gives credits for a daily check-in (每日签到). magpie can press it
115
115
  once a day for each account (Settings, or the switch on the usage card).
116
116
  It asks Trae CN's own pages through
117
- this plugin's fetch, which sends them as the account: its Cloud-IDE-JWT
118
- (renewed first when near its end) and its device id.
117
+ this plugin's fetch, which sends them as the account, as Trae CN's IDE
118
+ does: its Cloud-IDE-JWT (renewed first when near its end) and its
119
+ device's headers (`x-device-id`, `x-device-brand`, `x-device-type`,
120
+ `x-os-version`, `x-app-version`), and no others.
119
121
 
120
122
  - **Status:** `POST api.trae.cn/trae/api/v2/ug/checkin_credits/status`,
121
- body `{}`: `enable`, `checked_in`, `credits`.
123
+ body `{"req_source":1}`: `enable`, `checked_in`, `credits`.
122
124
  - **Claim:** `POST api.trae.cn/trae/api/v2/ug/checkin_credits/claim`,
123
- body `{}`, only while it is on and today's isn't in.
125
+ body `{"req_source":1}`, only while it is on and today's isn't in.
124
126
  - **What comes back:** Trae's answer as it is. A 401 or code 1001 marks
125
127
  the account for a new sign-in. Code 9095 means this device has checked
126
128
  in today; nothing sends another device id to get round it.
package/index.mjs CHANGED
@@ -366,6 +366,42 @@ function ideHeaders(a, extra = {}) {
366
366
  }
367
367
  }
368
368
 
369
+ // ugHeaders are the headers Trae CN's IDE sends its growth pages
370
+ // (api.trae.cn/trae/api/v2/ug/…, the daily check-in) with, and no others:
371
+ // its eb()/fb() (Trae CN 3.3.104's out/main.js) send Content-Type, the
372
+ // Cloud-IDE-JWT and the device (x-device-id, x-device-brand,
373
+ // x-device-type, x-os-version, x-app-version), none of the chat's
374
+ // x-uid, x-app-id, x-ide-token and the like
375
+ function ugHeaders(a, extra = {}) {
376
+ return {
377
+ "Content-Type": "application/json",
378
+ Authorization: `Cloud-IDE-JWT ${a.token}`,
379
+ "x-device-id": a.deviceId,
380
+ "x-device-brand": BRAND,
381
+ "x-device-type": "windows",
382
+ "x-os-version": "Windows 10",
383
+ "x-app-version": CLIENT_VERSION,
384
+ ...extra,
385
+ }
386
+ }
387
+
388
+ // ugBody is the body the IDE posts a growth page: {req_source: 1}, its
389
+ // own (2 is SOLO Lite's), where magpie gave none or {}; anything else
390
+ // given goes as it is
391
+ function ugBody(body) {
392
+ const s = typeof body === "string" ? body.trim() : ""
393
+ if (s === "" || s === "{}") return JSON.stringify({ req_source: 1 })
394
+ return s
395
+ }
396
+
397
+ const ugPage = (url) => {
398
+ try {
399
+ return new URL(url).pathname.startsWith("/trae/api/v2/ug/")
400
+ } catch {
401
+ return false
402
+ }
403
+ }
404
+
369
405
  // Trae's chat takes text only and has no turn for a tool call or its
370
406
  // result. The agent's tools are named to it natively and in a system
371
407
  // prompt that asks for each call as a tagged block; earlier calls and
@@ -574,16 +610,60 @@ class TextTools {
574
610
  function nativeCall(tc) {
575
611
  const f = tc?.function ?? tc?.function_call ?? tc
576
612
  const name = f?.name ?? tc?.tool_name ?? ""
577
- if (!name) return null
578
- const a = f?.arguments ?? tc?.params ?? tc?.input ?? tc?.parameters ?? {}
579
- return { id: tc?.id || tc?.tool_call_id || "", name: String(name), arguments: typeof a === "string" ? a : JSON.stringify(a) }
613
+ const index = Number.isInteger(tc?.index) ? tc.index : null
614
+ if (!name && index === null && !tc?.id) return null
615
+ const a = f?.arguments ?? f?.args ?? tc?.params ?? tc?.input ?? tc?.parameters ?? ""
616
+ return { id: tc?.id || tc?.tool_call_id || "", index, name: String(name), arguments: typeof a === "string" ? a : JSON.stringify(a) }
617
+ }
618
+
619
+ // NativeCalls puts together the tool calls Trae streams: a call comes in
620
+ // several events under one id (or index), each with the arguments so far
621
+ // or only the new piece, so a call is sent on once the answer is whole,
622
+ // not from its first event (#799: bash's command cut to `echo "T`)
623
+ class NativeCalls {
624
+ constructor() {
625
+ this.calls = []
626
+ }
627
+ push(tc) {
628
+ const c = nativeCall(tc)
629
+ if (!c) return
630
+ let o = this.calls.find((x) => (c.id && x.id === c.id) || (!c.id && c.index !== null && x.index === c.index))
631
+ if (!o && !c.id && c.index === null)
632
+ o = this.calls.findLast((x) => x.name === c.name && (x.arguments === c.arguments || (!done(x.arguments) && c.arguments.startsWith(x.arguments))))
633
+ if (!o) {
634
+ if (!c.name) return
635
+ this.calls.push({ ...c })
636
+ return
637
+ }
638
+ if (!o.name) o.name = c.name
639
+ if (c.index !== null && o.index === null) o.index = c.index
640
+ const prev = o.arguments
641
+ const next = c.arguments
642
+ if (!next || prev.startsWith(next)) return // nothing new
643
+ if (next.startsWith(prev) || done(prev)) o.arguments = next // the arguments so far
644
+ else o.arguments = prev + next // a piece
645
+ }
646
+ take() {
647
+ return this.calls.filter((c) => c.name).map(({ id, name, arguments: a }) => ({ id: id || callId(), name, arguments: a || "{}" }))
648
+ }
649
+ }
650
+
651
+ // done says arguments are a whole JSON value already
652
+ function done(a) {
653
+ if (!a) return false
654
+ try {
655
+ JSON.parse(a)
656
+ return true
657
+ } catch {
658
+ return false
659
+ }
580
660
  }
581
661
 
582
662
  // parts turns Trae's events into what the answer is made of: text,
583
663
  // reasoning, tool calls, the token counts, the finish, or an error.
584
664
  async function* parts(events) {
585
665
  const tt = new TextTools()
586
- const seen = new Set()
666
+ const nc = new NativeCalls()
587
667
  for await (const { event, data } of events) {
588
668
  const name = eventName(event)
589
669
  const d = data && typeof data === "object" ? data : {}
@@ -614,20 +694,14 @@ async function* parts(events) {
614
694
  if (r.text) yield { text: r.text }
615
695
  for (const c of r.calls) yield { call: { ...c, id: callId() } }
616
696
  }
617
- for (const tc of Array.isArray(d.tool_calls) ? d.tool_calls : []) {
618
- const c = nativeCall(tc)
619
- if (!c) continue
620
- const key = c.id || c.name + c.arguments
621
- if (seen.has(key)) continue
622
- seen.add(key)
623
- yield { call: { ...c, id: c.id || callId() } }
624
- }
697
+ for (const tc of Array.isArray(d.tool_calls) ? d.tool_calls : []) nc.push(tc)
625
698
  const u = tokensOf(d.usage)
626
699
  if (u) yield { usage: u }
627
700
  }
628
701
  const r = tt.push("", true)
629
702
  if (r.text) yield { text: r.text }
630
703
  for (const c of r.calls) yield { call: { ...c, id: callId() } }
704
+ for (const c of nc.take()) yield { call: c }
631
705
  }
632
706
 
633
707
  // first reads events up to the first that is the answer's, so an answer
@@ -848,10 +922,13 @@ export const TraeCNAuthPlugin = async ({ client }) => {
848
922
  if (e instanceof Expired) return errorResponse(401, `${a0.name || a0.uid}: ${e.message}`, "expired")
849
923
  throw e
850
924
  }
925
+ // the check-in goes as the IDE sends it (ugHeaders, ugBody); Trae's
926
+ // other pages with the chat's headers
927
+ const ug = ugPage(url)
851
928
  const res = await fetch(url, {
852
929
  method,
853
- headers: ideHeaders(a, { Accept: "application/json" }),
854
- body: method === "GET" || method === "HEAD" ? undefined : typeof body === "string" ? body : "{}",
930
+ headers: ug ? ugHeaders(a, { Accept: "application/json" }) : ideHeaders(a, { Accept: "application/json" }),
931
+ body: method === "GET" || method === "HEAD" ? undefined : ug ? ugBody(body) : typeof body === "string" ? body : "{}",
855
932
  signal: signal ?? AbortSignal.timeout(20_000),
856
933
  })
857
934
  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.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "OpenCode provider plugin for Trae CN (trae.cn): your Trae CN account's models, free tier included, with Trae's own browser sign-in",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",