@magpie-community/opencode-trae-auth 0.2.5 → 0.2.6
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 +12 -0
- package/index.mjs +78 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,18 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
69
69
|
`solo_work_lite`, SOLO's Work mode; `solo_agent`, the TRAE agent's;
|
|
70
70
|
`solo_agent_lite`). If Trae answers 4001, 4023 or 1005, the plugin tries
|
|
71
71
|
the others once and remembers which one worked.
|
|
72
|
+
- `session_id` is the conversation's, as the IDE keeps one per conversation:
|
|
73
|
+
the session magpie (or OpenCode) names the request by, else the chat's
|
|
74
|
+
first user message, which every later turn repeats, together with the
|
|
75
|
+
account the request is made as. Trae counts a conversation by it, so a
|
|
76
|
+
fresh one per request made one user send that takes several steps (a tool
|
|
77
|
+
result sent back, a retry, another function answering) count as a
|
|
78
|
+
conversation of its own — several rows in the usage detail, each under no
|
|
79
|
+
title. One conversation answered by two accounts (magpie failing over, or
|
|
80
|
+
rotating, inside it) is named twice then, as two conversations, so Trae
|
|
81
|
+
never reads one account's conversation as another's; that costs the two
|
|
82
|
+
halves of such a conversation their single row. `request_id` is the
|
|
83
|
+
request's own, fresh on every one.
|
|
72
84
|
- **Headers:** TRAE SOLO CN 0.1.69's (`x-ide-version`/`x-app-version`
|
|
73
85
|
0.1.69, version code 20260917; Trae offers a model only to clients new
|
|
74
86
|
enough for it): `Authorization: Cloud-IDE-JWT …`, `X-Cloudide-Token`,
|
package/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// The account's fetch also sends Trae CN's own pages on api.trae.cn
|
|
10
10
|
// (/trae/api/…) as the account, for magpie's daily check-in (每日签到:
|
|
11
11
|
// /trae/api/v2/ug/checkin_credits/status, then /claim).
|
|
12
|
-
import { generateKeyPairSync, randomBytes, randomUUID, randomInt, sign } from "node:crypto"
|
|
12
|
+
import { createHash, generateKeyPairSync, randomBytes, randomUUID, randomInt, sign } from "node:crypto"
|
|
13
13
|
import { createServer, STATUS_CODES } from "node:http"
|
|
14
14
|
|
|
15
15
|
const SITES = {
|
|
@@ -767,14 +767,72 @@ const nativeTools = (tools) =>
|
|
|
767
767
|
return { type: "function", function: { name: f.name, description: f.description ?? "", parameters: typeof f.parameters === "string" ? f.parameters : JSON.stringify(f.parameters ?? {}) } }
|
|
768
768
|
})
|
|
769
769
|
|
|
770
|
+
// SESSION carries the conversation magpie (or OpenCode) names a request by,
|
|
771
|
+
// from the chat.headers hook to the loader's fetch. It goes no further.
|
|
772
|
+
const SESSION = "x-magpie-trae-session"
|
|
773
|
+
|
|
774
|
+
// SALT is what the id is derived with, so it says nothing of the words it was
|
|
775
|
+
// made from. New in each process, as Trae's own session ids are one session's
|
|
776
|
+
// own within a run.
|
|
777
|
+
const SALT = randomBytes(16).toString("hex")
|
|
778
|
+
|
|
779
|
+
// conversationID is the conversation a chat belongs to, as the UUID the
|
|
780
|
+
// request's session_id wants: the session magpie or OpenCode names (its
|
|
781
|
+
// chat.headers hook), else the chat's first user message, which every later
|
|
782
|
+
// turn of the conversation repeats. "" when neither names one — a body that
|
|
783
|
+
// isn't a chat, or one with no user message in it — and the caller then makes
|
|
784
|
+
// a fresh id per request, as before: a made-up conversation of the request's
|
|
785
|
+
// own is the very splitting this is here to end.
|
|
786
|
+
//
|
|
787
|
+
// who is the account the request is made as (its uid, else its name), and it
|
|
788
|
+
// is part of what the id is made of, so one conversation answered by two
|
|
789
|
+
// accounts (magpie failing over, or rotating, inside it) is two conversations
|
|
790
|
+
// as Trae is concerned — an id a fresh random one never linked, and one that
|
|
791
|
+
// would let Trae read one conversation as another's. The cost is that such a
|
|
792
|
+
// conversation's two halves are counted apart again, which is what it did
|
|
793
|
+
// before this named one at all.
|
|
794
|
+
function conversationID(chat, session, who = "") {
|
|
795
|
+
const account = String(who ?? "").trim()
|
|
796
|
+
if (session) return uuidOf("magpie-trae-session|" + account + "|" + session)
|
|
797
|
+
const first = (Array.isArray(chat?.messages) ? chat.messages : []).find((m) => m?.role === "user")
|
|
798
|
+
const text = partText(first?.content ?? "")
|
|
799
|
+
return text.trim() ? uuidOf("magpie-trae-first|" + account + "|" + text) : ""
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// uuidOf is a version 4 UUID made of a hash's first 16 bytes: the shape
|
|
803
|
+
// randomUUID() returns, since that is what session_id carried before.
|
|
804
|
+
function uuidOf(s) {
|
|
805
|
+
const b = Buffer.from(createHash("sha256").update(SALT + "|" + s).digest().subarray(0, 16))
|
|
806
|
+
b[6] = (b[6] & 0x0f) | 0x40
|
|
807
|
+
b[8] = (b[8] & 0x3f) | 0x80
|
|
808
|
+
const h = b.toString("hex")
|
|
809
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// sessionOf takes the session a chat.headers hook named off the headers, with
|
|
813
|
+
// the header: it goes no further than here. Its length is capped as magpie's
|
|
814
|
+
// own session ids are, the id being derived from it anyway.
|
|
815
|
+
function sessionOf(headers) {
|
|
816
|
+
const s = String(headers.get(SESSION) ?? "").trim()
|
|
817
|
+
headers.delete(SESSION)
|
|
818
|
+
return s.slice(0, 128)
|
|
819
|
+
}
|
|
820
|
+
|
|
770
821
|
// chatBody is the request for llm_utils_chat; modelName is the model the
|
|
771
822
|
// function's list names for the config (its __dev one), when it named one,
|
|
772
823
|
// and most the max_tokens it gives that model (0: none given), which a
|
|
773
824
|
// request asking more is held to. A Max model (max: {base, window}) asks
|
|
774
825
|
// the config base by its __max model, max_tokens always set, and gives
|
|
775
826
|
// the prompt the room the window has beside it, as the IDE's Max mode does.
|
|
776
|
-
|
|
777
|
-
|
|
827
|
+
//
|
|
828
|
+
// session is the conversation's id, made by conversationID, so every request
|
|
829
|
+
// of one conversation carries one: Trae counts a conversation by the session
|
|
830
|
+
// the requests name, and a request naming a new session each time is counted
|
|
831
|
+
// as a conversation of its own — one user send that takes several steps (a
|
|
832
|
+
// tool result sent back, a retry, another function answering) shows up in the
|
|
833
|
+
// usage detail as several, each under no title at all. request_id stays a
|
|
834
|
+
// fresh id per request, being that request's own.
|
|
835
|
+
function chatBody(req, fn, modelName, most = 0, max = null, session = "") {
|
|
778
836
|
const config = max?.base ?? req.model
|
|
779
837
|
const body = {
|
|
780
838
|
messages: traeMessages(req),
|
|
@@ -783,8 +841,8 @@ function chatBody(req, fn, modelName, most = 0, max = null) {
|
|
|
783
841
|
model: config,
|
|
784
842
|
...(modelName ? { model_name: modelName } : {}),
|
|
785
843
|
stream: true, // Trae answers in SSE either way
|
|
786
|
-
request_id:
|
|
787
|
-
session_id: session,
|
|
844
|
+
request_id: randomUUID(),
|
|
845
|
+
session_id: session || randomUUID(),
|
|
788
846
|
}
|
|
789
847
|
const asked = req.max_completion_tokens ?? req.max_tokens
|
|
790
848
|
if (Number.isFinite(asked) && asked > 0) body.max_tokens = most > 0 ? Math.min(Math.floor(asked), most) : Math.floor(asked)
|
|
@@ -2078,6 +2136,14 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2078
2136
|
models: { ...site.models, ...(was.models ?? {}) },
|
|
2079
2137
|
}
|
|
2080
2138
|
},
|
|
2139
|
+
// the conversation's id, which the requests below carry as their
|
|
2140
|
+
// session_id: magpie's (or OpenCode's) session for this request, when it
|
|
2141
|
+
// names one
|
|
2142
|
+
"chat.headers": async (input, output) => {
|
|
2143
|
+
const id = input?.model?.providerID ?? input?.provider?.info?.id
|
|
2144
|
+
if (id !== site.id || !input?.sessionID) return
|
|
2145
|
+
output.headers[SESSION] = String(input.sessionID).slice(0, 128)
|
|
2146
|
+
},
|
|
2081
2147
|
provider: {
|
|
2082
2148
|
id: site.id,
|
|
2083
2149
|
// the account's own list, when Trae answers; else the list above
|
|
@@ -2156,11 +2222,16 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2156
2222
|
if (outer.aborted) ac.abort()
|
|
2157
2223
|
else outer.addEventListener("abort", () => ac.abort(), { once: true })
|
|
2158
2224
|
}
|
|
2225
|
+
// the conversation this request belongs to, from the chat.headers
|
|
2226
|
+
// hook (magpie's session) or the chat's own first user message, as
|
|
2227
|
+
// the account answering it, so a failover inside one conversation
|
|
2228
|
+
// is not one Trae reads as another's
|
|
2229
|
+
const session = conversationID(req, sessionOf(new Headers(init.headers ?? r0?.headers ?? {})), who)
|
|
2159
2230
|
for (const fn of [...new Set(fns)]) {
|
|
2160
2231
|
const res = await fetch(apiOf(a, site) + "/api/agent/v3/llm_utils_chat", {
|
|
2161
2232
|
method: "POST",
|
|
2162
2233
|
headers: ideHeaders(a, site, { Accept: "text/event-stream", "X-Request-ID": randomUUID() }),
|
|
2163
|
-
body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max)),
|
|
2234
|
+
body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max, session)),
|
|
2164
2235
|
signal: ac.signal,
|
|
2165
2236
|
})
|
|
2166
2237
|
if (!res.ok) {
|
|
@@ -2211,4 +2282,4 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2211
2282
|
export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
|
|
2212
2283
|
export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
|
|
2213
2284
|
|
|
2214
|
-
export const _internal = { HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, toolNamed, traeMessages, chatBody, credits, dollarUsageOf, whenOf, newDevice, proofOf, SITES }
|
|
2285
|
+
export const _internal = { HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, toolNamed, traeMessages, chatBody, credits, dollarUsageOf, whenOf, newDevice, proofOf, SITES, SESSION, conversationID }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-trae-auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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",
|