@magpie-community/opencode-trae-auth 0.1.3 → 0.1.5

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 +19 -0
  2. package/index.mjs +68 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -107,3 +107,22 @@ magpie's usage card shows the account's credits:
107
107
  unlimited) and what the pack has used.
108
108
  - **What it shows:** credits left as the balance, and the share used as a
109
109
  window.
110
+
111
+ ## Daily check-in
112
+
113
+ Trae CN gives credits for a daily check-in (每日签到). magpie can press it
114
+ once a day for each account (Settings, or the switch on the usage card).
115
+ It asks Trae CN's own pages through
116
+ this plugin's fetch, which sends them as the account: its Cloud-IDE-JWT
117
+ (renewed first when near its end) and its device id.
118
+
119
+ - **Status:** `POST api.trae.cn/trae/api/v2/ug/checkin_credits/status`,
120
+ body `{}`: `enable`, `checked_in`, `credits`.
121
+ - **Claim:** `POST api.trae.cn/trae/api/v2/ug/checkin_credits/claim`,
122
+ body `{}`, only while it is on and today's isn't in.
123
+ - **What comes back:** Trae's answer as it is. A 401 or code 1001 marks
124
+ the account for a new sign-in. Code 9095 means this device has checked
125
+ in today; nothing sends another device id to get round it.
126
+
127
+ Only `api.trae.cn`'s `/trae/api/` pages are sent this way; any other URL
128
+ that isn't a chat request is still refused (400). Needs 0.1.4 or later.
package/index.mjs CHANGED
@@ -6,6 +6,9 @@
6
6
  // agent's /api/agent/v3/llm_utils_chat, which answers in its own SSE
7
7
  // events. Worked out from two open-source relays (wangqi233/trae2api and
8
8
  // autumnsentiment/Trae2api-cn); not run against a real account here.
9
+ // The account's fetch also sends Trae CN's own pages on api.trae.cn
10
+ // (/trae/api/…) as the account, for magpie's daily check-in (每日签到:
11
+ // /trae/api/v2/ug/checkin_credits/status, then /claim).
9
12
  import { randomBytes, randomUUID, randomInt } from "node:crypto"
10
13
  import { createServer, STATUS_CODES } from "node:http"
11
14
 
@@ -132,6 +135,18 @@ function apiOf(a) {
132
135
  return /mchost\.guru|trae-api-/i.test(h) ? h : HOSTS.api
133
136
  }
134
137
 
138
+ // ownPage: url is one of Trae CN's own JSON pages on api.trae.cn
139
+ // (/trae/api/…: the daily check-in, credits), which the account's fetch
140
+ // sends as the account rather than as a chat
141
+ function ownPage(url) {
142
+ try {
143
+ const u = new URL(url)
144
+ return u.origin === new URL(HOSTS.auth).origin && u.pathname.startsWith("/trae/api/")
145
+ } catch {
146
+ return false
147
+ }
148
+ }
149
+
135
150
  // ---- errors --------------------------------------------------------------------
136
151
 
137
152
  class Expired extends Error {
@@ -819,6 +834,32 @@ export const TraeCNAuthPlugin = async ({ client }) => {
819
834
  return { status: res.status, text, v: parseJSON(text) }
820
835
  }
821
836
 
837
+ // ownRequest sends a request for one of Trae CN's own JSON pages as the
838
+ // account, as its IDE does: magpie asks the daily check-in
839
+ // (/trae/api/v2/ug/checkin_credits/status, then /claim) through the
840
+ // account's fetch, as it does WorkBuddy's. Trae's answer comes back as it
841
+ // is; a token it turned away (401, or code 1001 in a 200) marks the
842
+ // sign-in, as usage does.
843
+ const ownRequest = async (getAuth, a0, url, method, body, signal) => {
844
+ let a
845
+ try {
846
+ a = await fresh(getAuth)
847
+ } catch (e) {
848
+ if (e instanceof Expired) return errorResponse(401, `${a0.name || a0.uid}: ${e.message}`, "expired")
849
+ throw e
850
+ }
851
+ const res = await fetch(url, {
852
+ method,
853
+ headers: ideHeaders(a, { Accept: "application/json" }),
854
+ body: method === "GET" || method === "HEAD" ? undefined : typeof body === "string" ? body : "{}",
855
+ signal: signal ?? AbortSignal.timeout(20_000),
856
+ })
857
+ const text = await res.text()
858
+ const lapsed = res.status === 401 || lapsedCode(errorOf(text).code)
859
+ const headers = { "content-type": res.headers.get("content-type") ?? "application/json", "X-Magpie-Sign-In": lapsed ? "expired" : a.renewed ? "renewed" : "kept" }
860
+ return new Response(text, { status: res.status, headers })
861
+ }
862
+
822
863
  const usage = async (getAuth) => {
823
864
  let a
824
865
  try {
@@ -889,7 +930,8 @@ export const TraeCNAuthPlugin = async ({ client }) => {
889
930
  // The lists come from batch_get_detail_param, as TRAE SOLO CN asks them;
890
931
  // when that gives none, from get_detail_param, one function at a time.
891
932
  // A model several functions list is asked through the first whose entry
892
- // names a __dev model (one Trae serves), else the first.
933
+ // names a __dev model (one Trae serves) and isn't misnamed, else the
934
+ // first that names one, else the first.
893
935
  const liveModels = async (a) => {
894
936
  let lists = null
895
937
  try {
@@ -907,13 +949,35 @@ export const TraeCNAuthPlugin = async ({ client }) => {
907
949
  if (g.status === "fulfilled") lists[FUNCTIONS[i]] = g.value
908
950
  })
909
951
  }
952
+ // a function can give a model another's name: chat_v3 calls
953
+ // deepseek-v4.1-flash "DeepSeek-V4-Flash 正式版", as it does
954
+ // DeepSeek-V4-Flash-Official, where SOLO's lists call it
955
+ // DeepSeek-V4.1-Flash (yetone/magpie#681) — Trae's usage page then books
956
+ // it under that name too. Such an entry is misnamed: its name is another
957
+ // model's in the same list, and other lists name it otherwise.
958
+ const nameOf = (m) => String(m.display_config?.display_name || m.display_name || m.display_model_name || m.config_name)
959
+ const names = new Map() // id → the names its lists give it
960
+ for (const fn of FUNCTIONS) {
961
+ for (const m of lists[fn] ?? []) {
962
+ if (!chatModel(m)) continue
963
+ const id = String(m.config_name)
964
+ names.set(id, (names.get(id) ?? new Set()).add(nameOf(m)))
965
+ }
966
+ }
967
+ const misnamed = (fn, m) => {
968
+ const id = String(m.config_name), name = nameOf(m)
969
+ return names.get(id).size > 1 && (lists[fn] ?? []).some((o) => chatModel(o) && String(o.config_name) !== id && nameOf(o) === name)
970
+ }
971
+ // a function's entry is worth more when it names a __dev model, then
972
+ // when it isn't misnamed; the first of the best is taken
973
+ const worth = (fn, m) => (devModel(m) ? 2 : 0) + (misnamed(fn, m) ? 0 : 1)
910
974
  const out = new Map()
911
975
  for (const fn of FUNCTIONS) {
912
976
  for (const m of lists[fn] ?? []) {
913
977
  if (!chatModel(m)) continue
914
978
  const id = String(m.config_name)
915
979
  const was = out.get(id)
916
- if (!was || (!devModel(was.m) && devModel(m))) out.set(id, { m, fn })
980
+ if (!was || worth(fn, m) > was.worth) out.set(id, { m, fn, worth: worth(fn, m) })
917
981
  }
918
982
  }
919
983
  for (const [id, { m, fn }] of out) {
@@ -981,6 +1045,8 @@ export const TraeCNAuthPlugin = async ({ client }) => {
981
1045
  let body = init.body ?? (r0 ? await r0.text() : undefined)
982
1046
  if (body instanceof ArrayBuffer) body = new TextDecoder().decode(body)
983
1047
  else if (ArrayBuffer.isView(body)) body = new TextDecoder().decode(new Uint8Array(body.buffer, body.byteOffset, body.byteLength))
1048
+ const url = r0 ? r0.url : input instanceof URL ? input.href : String(input)
1049
+ if (ownPage(url)) return ownRequest(getAuth, a0, url, init.method ?? r0?.method ?? "POST", body, init.signal ?? r0?.signal)
984
1050
  const req = parseJSON(body)
985
1051
  if (!Array.isArray(req.messages)) return errorResponse(400, "Trae CN: only chat completions are served")
986
1052
  let a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-trae-auth",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",