mslxdff 0.1.136 → 0.1.140
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/package.json +2 -2
- package/src/chat/gateway.js +1 -1
- package/src/chat/prompt.js +1 -1
- package/src/chat/terminal.js +1 -1
- package/src/chooser.js +31 -12
- package/src/cli/commands/model/list-live.js +22 -0
- package/src/cli/commands/model/list-providers.js +1 -1
- package/src/cli/commands/model/list-render.js +2 -2
- package/src/cli/commands/model/list.js +2 -0
- package/src/cli/commands/model/live-models.js +24 -0
- package/src/cli/commands/provider/add.js +2 -1
- package/src/cli/commands/provider/bench-via.js +1 -1
- package/src/cli/commands/provider/cline-free.js +94 -0
- package/src/cli/commands/provider/cline-login.js +20 -15
- package/src/cli/commands/provider/codearts-login.js +45 -0
- package/src/cli/commands/provider/index.js +34 -0
- package/src/cli/commands/provider/keys.js +13 -2
- package/src/cli/commands/provider/models.js +13 -1
- package/src/cli/commands/provider/qoder-checkin.js +81 -0
- package/src/cli/commands/provider/qoder-login.js +103 -0
- package/src/cli/commands/provider/traework-login.js +202 -0
- package/src/cli/commands/provider/workbuddy-login.js +1 -1
- package/src/cli/commands/workbuddy.js +14 -21
- package/src/cli/index.js +3 -0
- package/src/cli/interactive.js +41 -4
- package/src/cli/provider-row.js +1 -1
- package/src/compat.js +23 -5
- package/src/model-capabilities/merge.js +2 -2
- package/src/providers/classify.js +1 -1
- package/src/providers/cline/auth.js +1 -1
- package/src/providers/cline/chat.js +14 -11
- package/src/providers/cline/free-catalog.js +71 -0
- package/src/providers/cline/index.js +2 -16
- package/src/providers/cline/models.js +28 -51
- package/src/providers/codearts/auth-pool.js +148 -0
- package/src/providers/codearts/chat.js +176 -0
- package/src/providers/codearts/const.js +60 -0
- package/src/providers/codearts/dpop.js +95 -0
- package/src/providers/codearts/index.js +160 -0
- package/src/providers/codearts/login.js +214 -0
- package/src/providers/codearts/models.js +182 -0
- package/src/providers/codearts/sign.js +96 -0
- package/src/providers/codearts/sse.js +204 -0
- package/src/providers/codearts/stream.js +209 -0
- package/src/providers/codearts/sts.js +124 -0
- package/src/providers/codearts.js +1 -0
- package/src/providers/model-id.js +4 -4
- package/src/providers/qoder/account-store.js +107 -0
- package/src/providers/qoder/aggregate.js +51 -0
- package/src/providers/qoder/baseprompt.json +592 -0
- package/src/providers/qoder/chat.js +72 -0
- package/src/providers/qoder/checkin.js +160 -0
- package/src/providers/qoder/constants.js +45 -0
- package/src/providers/qoder/encode.js +42 -0
- package/src/providers/qoder/fingerprint.js +39 -0
- package/src/providers/qoder/index.js +106 -0
- package/src/providers/qoder/models.js +62 -0
- package/src/providers/qoder/oauth.js +85 -0
- package/src/providers/qoder/payload.js +156 -0
- package/src/providers/qoder/request.js +26 -0
- package/src/providers/qoder/session.js +135 -0
- package/src/providers/qoder/sse.js +46 -0
- package/src/providers/qoder/stream.js +81 -0
- package/src/providers/qoder.js +2 -0
- package/src/providers/registry.js +16 -1
- package/src/providers/share-keys.js +7 -2
- package/src/providers/traework/account-store.js +149 -0
- package/src/providers/traework/chat.js +211 -0
- package/src/providers/traework/checkin.js +35 -0
- package/src/providers/traework/constants.js +21 -0
- package/src/providers/traework/errors.js +52 -0
- package/src/providers/traework/headers.js +47 -0
- package/src/providers/traework/index.js +122 -0
- package/src/providers/traework/models.js +97 -0
- package/src/providers/traework/payload.js +86 -0
- package/src/providers/traework/sse.js +184 -0
- package/src/providers/traework/token.js +52 -0
- package/src/providers/traework.js +2 -0
- package/src/providers/workbuddy/account-store.js +73 -7
- package/src/providers/workbuddy/chat.js +1 -1
- package/src/providers/workbuddy/index.js +10 -20
- package/src/readline-compat.js +9 -10
- package/src/routes/models-route.js +19 -1
- package/src/runtime/bootstrap.js +10 -0
- package/src/runtime/provider-gate.js +30 -0
- package/src/runtime/providers-setup.js +18 -3
- package/src/runtime/qoder-checkin.js +93 -0
- package/src/runtime/server-lifecycle.js +2 -2
- package/src/runtime/traework-checkin.js +104 -0
- package/src/state/migrations/cline-unify.js +94 -0
- package/src/state/migrations.js +33 -0
- package/src/state/schemas/token.js +6 -2
- package/src/state/schemas/use-group.js +4 -4
- package/src/sync-workbuddy.js +1 -1
- package/src/upstream-engine/index.js +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// qoder 对话服务:上游恒 stream:true;请求方 stream=true → 真流式转发边收边吐,
|
|
2
|
+
// false → 聚合回 JSON。装配走 request.js,帧格式走 sse.js,两条管线共享。
|
|
3
|
+
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
4
|
+
import { normalizeRegion } from "./constants.js";
|
|
5
|
+
import { buildUpstreamRequest } from "./request.js";
|
|
6
|
+
import { reshapeQoderStream } from "./stream.js";
|
|
7
|
+
import { aggregateQoderStream, toCompletionJson } from "./aggregate.js";
|
|
8
|
+
import { errorStatus } from "./sse.js";
|
|
9
|
+
|
|
10
|
+
function errRes(status, msg, type = "upstream_error") {
|
|
11
|
+
return new Response(JSON.stringify({ error: { message: msg, type } }), { status, headers: { "Content-Type": "application/json" } });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mapUpstreamError(status, detail) {
|
|
15
|
+
return { kind: status === 401 || status === 403 ? "auth" : "upstream", status, detail: String(detail || "").slice(0, 300) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createChatService({ id = "qoder", fetchImpl, timeoutMs } = {}) {
|
|
19
|
+
if (!fetchImpl) fetchImpl = compatFetch;
|
|
20
|
+
|
|
21
|
+
// body: OpenAI chat 请求;sess: 已建 COSY 会话;region: 该账号所属区(每号独立选端点)。返回 Response。
|
|
22
|
+
async function runChat(body, sess, region = "global") {
|
|
23
|
+
const stream = body?.stream !== false;
|
|
24
|
+
const model = body?.model || "qfmodel";
|
|
25
|
+
const chatId = "chatcmpl-qoder-" + Math.random().toString(16).slice(2, 10);
|
|
26
|
+
const tools = Array.isArray(body?.tools) && body.tools.length ? body.tools : null;
|
|
27
|
+
const messages = Array.isArray(body?.messages) ? body.messages : [];
|
|
28
|
+
const maxTokens = Number(body?.max_tokens) || 0;
|
|
29
|
+
|
|
30
|
+
let upRes;
|
|
31
|
+
try {
|
|
32
|
+
const req = buildUpstreamRequest({ sess, region: normalizeRegion(region), model, messages, tools, maxTokens });
|
|
33
|
+
upRes = await fetchImpl(req.url, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: req.headers,
|
|
36
|
+
body: req.bodyStr,
|
|
37
|
+
signal: timeoutSignal(timeoutMs || 120_000),
|
|
38
|
+
});
|
|
39
|
+
} catch (e) {
|
|
40
|
+
const detail = String(e?.message || e).slice(0, 300);
|
|
41
|
+
if (stream) return reshapeQoderStream(new Response("data: [DONE]\n\n"), { model, chatId });
|
|
42
|
+
return errRes(502, detail);
|
|
43
|
+
}
|
|
44
|
+
if (upRes.status !== 200) {
|
|
45
|
+
const detail = await upRes.text().catch(() => "");
|
|
46
|
+
const e = mapUpstreamError(upRes.status, detail);
|
|
47
|
+
if (stream) {
|
|
48
|
+
// 首包错误也走流内 error 事件(与旧 reshapeStream catch 语义一致:200 + event:error + DONE)
|
|
49
|
+
const enc = new TextEncoder();
|
|
50
|
+
const s = new ReadableStream({
|
|
51
|
+
start(c) {
|
|
52
|
+
c.enqueue(enc.encode(`data: ${JSON.stringify({ id: chatId, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model, choices: [{ index: 0, delta: {}, finish_reason: null }] })}\n\n`));
|
|
53
|
+
c.enqueue(enc.encode(`event: error\ndata: ${JSON.stringify({ message: e.detail, type: e.kind })}\n\ndata: [DONE]\n\n`));
|
|
54
|
+
c.close();
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return new Response(s, { status: 200, headers: { "Content-Type": "text/event-stream" } });
|
|
58
|
+
}
|
|
59
|
+
return errRes(errorStatus(e), e.detail, e.kind);
|
|
60
|
+
}
|
|
61
|
+
if (stream) return reshapeQoderStream(upRes, { model, chatId });
|
|
62
|
+
try {
|
|
63
|
+
const agg = await aggregateQoderStream(upRes);
|
|
64
|
+
const out = toCompletionJson({ model, chatId, ...agg });
|
|
65
|
+
return new Response(JSON.stringify(out), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return errRes(errorStatus(e), String(e?.detail || e?.message || e).slice(0, 300), e?.kind || "upstream_error");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { runChat };
|
|
72
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// qoder 签到/额度(移植 qoder2api checkin.go,修两处硬伤):
|
|
2
|
+
// ① 域名按区域选(Go 硬编码 openapi.qoder.com.cn → 国际号必然 401 TOKEN_EXPIRE);
|
|
3
|
+
// ② 活动类型放宽(Go 只认 CLAIM_BENEFIT,国际站活动是 VIEW_DETAILS 会被漏掉)。
|
|
4
|
+
// 认证只需 Bearer deviceToken + cosy-clienttype:10,无 COSY 签名、claim 空 body。
|
|
5
|
+
// 主流程:cn 走 daily-check-in(GET status → POST claim,409=今日已领);
|
|
6
|
+
// 404/非 200 → 回落 campaigns(GET /sash/api/v1/me/campaigns → POST .../{id}/claim)。
|
|
7
|
+
// Note: 域名必须按每号 region 选(两站账号不通用),默认只领 CLAIM_BENEFIT — 见 .agents/notes/implemented/feature/2026-09-21-qoder-checkin-region-endpoints.md
|
|
8
|
+
import { EP_CAMPAIGNS, EP_CHECKIN_STATUS, EP_CHECKIN_CLAIM, EP_QUOTA, openapiBase, normalizeRegion } from "./constants.js";
|
|
9
|
+
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
10
|
+
|
|
11
|
+
export function checkinHeaders(deviceToken) {
|
|
12
|
+
return {
|
|
13
|
+
authorization: "Bearer " + String(deviceToken || ""),
|
|
14
|
+
accept: "application/json",
|
|
15
|
+
"accept-language": "zh-CN",
|
|
16
|
+
"user-agent": "Qoder",
|
|
17
|
+
"cosy-clienttype": "10",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function call(fetchImpl, region, path, { method = "GET", deviceToken, body, timeoutMs = 20000 } = {}) {
|
|
22
|
+
const base = openapiBase(region);
|
|
23
|
+
const headers = checkinHeaders(deviceToken);
|
|
24
|
+
const init = { method, headers, signal: timeoutSignal(timeoutMs) };
|
|
25
|
+
if (method !== "GET") {
|
|
26
|
+
headers.origin = base;
|
|
27
|
+
if (body !== undefined) init.body = JSON.stringify(body);
|
|
28
|
+
}
|
|
29
|
+
let res;
|
|
30
|
+
try {
|
|
31
|
+
res = await fetchImpl(base + path, init);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return { status: 0, json: null, text: "", error: String(e?.message || e).slice(0, 200) };
|
|
34
|
+
}
|
|
35
|
+
const text = await res.text().catch(() => "");
|
|
36
|
+
let json = null;
|
|
37
|
+
if (text) { try { json = JSON.parse(text); } catch {} }
|
|
38
|
+
return { status: res.status, json, text };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function listCampaigns({ deviceToken, region, fetchImpl = compatFetch, timeoutMs } = {}) {
|
|
42
|
+
return call(fetchImpl, region, EP_CAMPAIGNS, { deviceToken, timeoutMs });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function claimCampaign({ deviceToken, region, campaignId, fetchImpl = compatFetch, timeoutMs } = {}) {
|
|
46
|
+
return call(fetchImpl, region, `${EP_CAMPAIGNS}/${campaignId}/claim`, { method: "POST", deviceToken, timeoutMs });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function dailyCheckinStatus({ deviceToken, region, fetchImpl = compatFetch, timeoutMs } = {}) {
|
|
50
|
+
return call(fetchImpl, region, EP_CHECKIN_STATUS, { deviceToken, timeoutMs });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function dailyCheckinClaim({ deviceToken, region, fetchImpl = compatFetch, timeoutMs } = {}) {
|
|
54
|
+
return call(fetchImpl, region, EP_CHECKIN_CLAIM, { method: "POST", deviceToken, body: {}, timeoutMs });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 余额:/api/v2/quota/usage → userQuota{total,used,remaining,unit} + isQuotaExceeded
|
|
58
|
+
export async function fetchQuota({ deviceToken, region, fetchImpl = compatFetch, timeoutMs } = {}) {
|
|
59
|
+
const r = await call(fetchImpl, region, EP_QUOTA, { deviceToken, timeoutMs });
|
|
60
|
+
if (r.status !== 200) return { ok: false, status: r.status, error: r.error || r.text.slice(0, 160) };
|
|
61
|
+
const q = r.json?.userQuota || {};
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
total: Number(q.total) || 0,
|
|
65
|
+
used: Number(q.used) || 0,
|
|
66
|
+
remaining: Number(q.remaining) || 0,
|
|
67
|
+
unit: String(q.unit || "credits"),
|
|
68
|
+
exhausted: Boolean(r.json?.isQuotaExceeded),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 选活动:优先 CLAIM_BENEFIT(真积分),allowPromo 时也认 VIEW_DETAILS 等促销条目。
|
|
73
|
+
export function pickCampaign(campaigns, { allowPromo = false } = {}) {
|
|
74
|
+
const list = Array.isArray(campaigns) ? campaigns : [];
|
|
75
|
+
let benefit = null;
|
|
76
|
+
let promo = null;
|
|
77
|
+
let claimed = false;
|
|
78
|
+
for (const c of list) {
|
|
79
|
+
if (c?.claimStatus === "CLAIMED") claimed = true;
|
|
80
|
+
if (c?.claimStatus !== "CLAIMABLE") continue;
|
|
81
|
+
if (c?.actionType === "CLAIM_BENEFIT") benefit = benefit || c;
|
|
82
|
+
else if (allowPromo) promo = promo || c;
|
|
83
|
+
}
|
|
84
|
+
return { target: benefit || promo, claimed };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 单账号签到:返回 { ok, status: claimed|already|no_campaign|error, amount, streak, ... }
|
|
88
|
+
export async function runCheckin({ deviceToken, region, fetchImpl = compatFetch, allowPromo = false, probeOnly = false, timeoutMs } = {}) {
|
|
89
|
+
const reg = normalizeRegion(region);
|
|
90
|
+
const r = {
|
|
91
|
+
region: reg, ok: false, status: "error", message: "", amount: 0,
|
|
92
|
+
streak: 0, totalDays: 0, totalCredits: 0, reward: 0,
|
|
93
|
+
};
|
|
94
|
+
if (!deviceToken) { r.message = "无 device token(先 mslxdff -provider qoder login)"; return r; }
|
|
95
|
+
|
|
96
|
+
const st = await dailyCheckinStatus({ deviceToken, region: reg, fetchImpl, timeoutMs });
|
|
97
|
+
if (st.status === 200 && st.json?.status) {
|
|
98
|
+
r.streak = Number(st.json.currentStreakDays) || 0;
|
|
99
|
+
r.totalDays = Number(st.json.totalClaimDays) || 0;
|
|
100
|
+
r.totalCredits = Number(st.json.totalRewardCredits) || 0;
|
|
101
|
+
r.reward = Number(st.json.rewardCredits) || 0;
|
|
102
|
+
}
|
|
103
|
+
if (st.status === 401) { r.message = `device token 失效(HTTP 401):请重新 mslxdff -provider qoder login`; return r; }
|
|
104
|
+
|
|
105
|
+
// 干跑:只报状态,不发任何 claim 请求
|
|
106
|
+
if (probeOnly && st.status === 200 && st.json?.status) {
|
|
107
|
+
r.ok = true;
|
|
108
|
+
r.status = st.json.status === "CLAIMED" ? "already" : "claimable";
|
|
109
|
+
r.message = `干跑:status=${st.json.status} 每日奖励 ${r.reward}`;
|
|
110
|
+
return r;
|
|
111
|
+
}
|
|
112
|
+
// 主流程:daily-check-in(国内站)
|
|
113
|
+
if (st.status === 200 && st.json?.status) {
|
|
114
|
+
const claim = await dailyCheckinClaim({ deviceToken, region: reg, fetchImpl, timeoutMs });
|
|
115
|
+
if (claim.status === 409) { r.ok = true; r.status = "already"; r.message = "今日已领取"; return r; }
|
|
116
|
+
if (claim.status === 200) {
|
|
117
|
+
r.ok = true; r.status = "claimed";
|
|
118
|
+
r.amount = Number(claim.json?.rewardCredits) || r.reward || 0;
|
|
119
|
+
r.streak += 1; r.totalDays += 1; r.totalCredits += r.amount;
|
|
120
|
+
r.message = `签到成功 +${r.amount}`;
|
|
121
|
+
return r;
|
|
122
|
+
}
|
|
123
|
+
// 其它错误 → 保留统计,回落 campaigns
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const list = await listCampaigns({ deviceToken, region: reg, fetchImpl, timeoutMs });
|
|
127
|
+
if (list.status === 401) { r.message = "device token 失效(HTTP 401):请重新 mslxdff -provider qoder login"; return r; }
|
|
128
|
+
if (list.status !== 200) {
|
|
129
|
+
r.message = `查询活动失败 HTTP ${list.status || "-"}: ${String(list.error || list.text).slice(0, 160)}`;
|
|
130
|
+
return r;
|
|
131
|
+
}
|
|
132
|
+
const { target, claimed } = pickCampaign(list.json?.campaigns, { allowPromo });
|
|
133
|
+
if (probeOnly) {
|
|
134
|
+
r.ok = true; r.status = target ? "claimable" : "no_campaign";
|
|
135
|
+
r.message = target
|
|
136
|
+
? `干跑:可领 ${target.actionType} ${target.campaignKey || ""}`.trim()
|
|
137
|
+
: (claimed ? "干跑:今日已领取" : "干跑:无可用签到活动");
|
|
138
|
+
return r;
|
|
139
|
+
}
|
|
140
|
+
if (!target) {
|
|
141
|
+
r.ok = true; r.status = "no_campaign";
|
|
142
|
+
r.message = claimed ? "今日已领取" : "无可用签到活动";
|
|
143
|
+
return r;
|
|
144
|
+
}
|
|
145
|
+
const claim = await claimCampaign({ deviceToken, region: reg, campaignId: target.campaignId, fetchImpl, timeoutMs });
|
|
146
|
+
if (claim.status !== 200 || claim.json?.status !== "CLAIMED") {
|
|
147
|
+
r.message = `领取失败 HTTP ${claim.status || "-"}: ${String(claim.error || claim.text).slice(0, 160)}`;
|
|
148
|
+
return r;
|
|
149
|
+
}
|
|
150
|
+
r.ok = true;
|
|
151
|
+
r.amount = Number(claim.json?.benefit?.amount) || 0;
|
|
152
|
+
r.expiresAt = String(claim.json?.expiresAt || "");
|
|
153
|
+
r.campaignKey = String(target.campaignKey || "");
|
|
154
|
+
if (claim.json?.replayed) { r.status = "already"; r.message = "今日已领取(幂等返回)"; }
|
|
155
|
+
else {
|
|
156
|
+
r.status = "claimed";
|
|
157
|
+
r.message = r.amount > 0 ? `领取成功 +${r.amount} ${r.campaignKey}`.trim() : `领取成功 ${r.campaignKey}`.trim();
|
|
158
|
+
}
|
|
159
|
+
return r;
|
|
160
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// qoder 上游技术常量(转译 qoder2api account/region.go,禁止改值)
|
|
2
|
+
export const OAUTH_CLIENT_ID = "e883ade2-e6e3-4d6d-adf7-f92ceff5fdcb";
|
|
3
|
+
|
|
4
|
+
// Region → 端点(global=国际站 *.qoder.sh / cn=国内站 *.qoder.com.cn)
|
|
5
|
+
export const ENDPOINTS = {
|
|
6
|
+
global: {
|
|
7
|
+
deviceLoginBase: "https://qoder.com/device/selectAccounts",
|
|
8
|
+
pollEndpoint: "https://openapi.qoder.sh/api/v1/deviceToken/poll",
|
|
9
|
+
userinfoBase: "https://openapi.qoder.sh/api/v1/userinfo",
|
|
10
|
+
chatStreamURL: "https://api1.qoder.sh/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1",
|
|
11
|
+
modelListURL: "https://api2.qoder.sh/algo/api/v2/model/list?Encode=1",
|
|
12
|
+
jobTokenURL: "https://center.qoder.sh/algo/api/v3/user/jobToken?Encode=1",
|
|
13
|
+
openapiBase: "https://openapi.qoder.sh",
|
|
14
|
+
},
|
|
15
|
+
cn: {
|
|
16
|
+
deviceLoginBase: "https://qoder.com.cn/device/selectAccounts",
|
|
17
|
+
pollEndpoint: "https://openapi.qoder.com.cn/api/v1/deviceToken/poll",
|
|
18
|
+
userinfoBase: "https://openapi.qoder.com.cn/api/v1/userinfo",
|
|
19
|
+
chatStreamURL: "https://gateway.qoder.com.cn/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1",
|
|
20
|
+
modelListURL: "https://gateway.qoder.com.cn/algo/api/v2/model/list?Encode=1",
|
|
21
|
+
jobTokenURL: "https://gateway.qoder.com.cn/algo/api/v3/user/jobToken?Encode=1",
|
|
22
|
+
openapiBase: "https://openapi.qoder.com.cn",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function normalizeRegion(s) {
|
|
27
|
+
const v = String(s || "").trim().toLowerCase();
|
|
28
|
+
return v === "cn" ? "cn" : "global";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getEndpoints(region) {
|
|
32
|
+
return ENDPOINTS[normalizeRegion(region)];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 签到/额度路径(同 base 换域名):global=openapi.qoder.sh(有 campaigns,daily-check-in 404);
|
|
36
|
+
// cn=openapi.qoder.com.cn(两者皆有;Go 版签到硬编码该域,故只服务国内号)。
|
|
37
|
+
export const EP_CAMPAIGNS = "/sash/api/v1/me/campaigns";
|
|
38
|
+
export const EP_CHECKIN_STATUS = "/sash/api/v1/me/daily-check-in/status";
|
|
39
|
+
export const EP_CHECKIN_CLAIM = "/sash/api/v1/me/daily-check-in/claim";
|
|
40
|
+
export const EP_PLAN = "/api/v2/user/plan";
|
|
41
|
+
export const EP_QUOTA = "/api/v2/quota/usage";
|
|
42
|
+
|
|
43
|
+
export function openapiBase(region) {
|
|
44
|
+
return getEndpoints(region).openapiBase;
|
|
45
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// qoder COSY 自定义 base64 编解码(转译 qoder2api internal/cosy/encoding.go)
|
|
2
|
+
// 自定义字母表 + 1/3 尾部轮转,`$` 代 `=`
|
|
3
|
+
const CUSTOM = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
|
|
4
|
+
const STD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
5
|
+
const CUSTOM_PAD = "$";
|
|
6
|
+
|
|
7
|
+
const c2s = new Array(128).fill(-1);
|
|
8
|
+
const s2c = new Array(128).fill(-1);
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < 64; i++) {
|
|
11
|
+
c2s[CUSTOM.charCodeAt(i)] = STD.charCodeAt(i);
|
|
12
|
+
s2c[STD.charCodeAt(i)] = CUSTOM.charCodeAt(i);
|
|
13
|
+
}
|
|
14
|
+
c2s[CUSTOM_PAD.charCodeAt(0)] = "=".charCodeAt(0);
|
|
15
|
+
s2c["=".charCodeAt(0)] = CUSTOM_PAD.charCodeAt(0);
|
|
16
|
+
|
|
17
|
+
export function cosyEncode(plain) {
|
|
18
|
+
const std = Buffer.from(plain).toString("base64");
|
|
19
|
+
const n = std.length;
|
|
20
|
+
const a = Math.floor(n / 3);
|
|
21
|
+
const rearranged = std.slice(n - a) + std.slice(a, n - a) + std.slice(0, a);
|
|
22
|
+
const out = new Array(n);
|
|
23
|
+
for (let i = 0; i < n; i++) {
|
|
24
|
+
const c = rearranged.charCodeAt(i);
|
|
25
|
+
if (c >= 128 || s2c[c] < 0) throw new Error("char out of alphabet: " + c);
|
|
26
|
+
out[i] = String.fromCharCode(s2c[c]);
|
|
27
|
+
}
|
|
28
|
+
return out.join("");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function cosyDecode(encoded) {
|
|
32
|
+
const n = encoded.length;
|
|
33
|
+
const mapped = new Array(n);
|
|
34
|
+
for (let i = 0; i < n; i++) {
|
|
35
|
+
const c = encoded.charCodeAt(i);
|
|
36
|
+
if (c >= 128 || c2s[c] < 0) throw new Error("char out of custom alphabet: " + c);
|
|
37
|
+
mapped[i] = String.fromCharCode(c2s[c]);
|
|
38
|
+
}
|
|
39
|
+
const a = Math.floor(n / 3);
|
|
40
|
+
const std = mapped.slice(n - a).join("") + mapped.slice(a, n - a).join("") + mapped.slice(0, a).join("");
|
|
41
|
+
return Buffer.from(std, "base64");
|
|
42
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// qoder 设备指纹派生(转译 qoder2api internal/cosy/fingerprint.go + session.go)
|
|
2
|
+
// fingerprint: 由 seed(uid 优先,无 uid 用 credential)派生 machineId/machineToken/machineType
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
export function fingerprintSeed(uid, credential) {
|
|
6
|
+
if (uid) return uid;
|
|
7
|
+
return "cred:" + credential;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function deriveMachineId(seed) {
|
|
11
|
+
return createHash("md5").update("machine:" + seed).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function deriveMachineType(seed) {
|
|
15
|
+
return createHash("md5").update("machinetype:" + seed).digest("hex").slice(0, 18);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function deriveMachineToken(seed) {
|
|
19
|
+
const sum = createHash("sha512").update("machinetoken:" + seed).digest();
|
|
20
|
+
return sum.toString("base64url").slice(0, 43);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function newUUID() {
|
|
24
|
+
const b = randomBytes(16);
|
|
25
|
+
const hex = b.toString("hex");
|
|
26
|
+
return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20,32)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function newRequestId() {
|
|
30
|
+
return randomBytes(12).toString("hex");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function unixSec() {
|
|
34
|
+
return Math.floor(Date.now() / 1000);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function unixMs() {
|
|
38
|
+
return Date.now();
|
|
39
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// qoder provider 门面(原生直连,零桥依赖):
|
|
2
|
+
// 多号 = keys[] 里每条 device_token blob(auths/qoder-<uid>.json 落盘由 login 维护);
|
|
3
|
+
// 每请求 round-robin 选号 → 建会话 → COSY 签名直调上游;恒 local-only 不借 key。
|
|
4
|
+
import { loadProviderKeys, loadProviderConfig } from "../../state.js";
|
|
5
|
+
import { listAccountDocs } from "./account-store.js";
|
|
6
|
+
import { defaultStateFile } from "../../state/store.js";
|
|
7
|
+
import { createKeyRing } from "../keyring.js";
|
|
8
|
+
import { envInt } from "../base.js";
|
|
9
|
+
import { accountFromBlob } from "./account-store.js";
|
|
10
|
+
import { normalizeRegion } from "./constants.js";
|
|
11
|
+
import { fingerprintSeed, deriveMachineId, deriveMachineToken, deriveMachineType } from "./fingerprint.js";
|
|
12
|
+
import { newSession } from "./session.js";
|
|
13
|
+
import { createChatService } from "./chat.js";
|
|
14
|
+
import { createModelsService } from "./models.js";
|
|
15
|
+
import { compatFetch } from "../../compat.js";
|
|
16
|
+
|
|
17
|
+
function buildSessionFor(cred) {
|
|
18
|
+
const uid = cred.uid || cred.deviceToken.slice(0, 8);
|
|
19
|
+
const seed = fingerprintSeed(uid, cred.deviceToken);
|
|
20
|
+
const identity = {
|
|
21
|
+
name: cred.name || "", aid: uid, uid, yxUid: "",
|
|
22
|
+
organizationId: cred.organizationId || "", organizationName: cred.organizationName || "",
|
|
23
|
+
userType: cred.userType || "personal_standard",
|
|
24
|
+
securityOauthToken: cred.deviceToken, refreshToken: cred.refreshToken || "",
|
|
25
|
+
};
|
|
26
|
+
return newSession(identity, deriveMachineId(seed), deriveMachineToken(seed), deriveMachineType(seed));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createQoderProvider({
|
|
30
|
+
id = "qoder",
|
|
31
|
+
apiKeys,
|
|
32
|
+
file,
|
|
33
|
+
region,
|
|
34
|
+
fetchImpl,
|
|
35
|
+
cooldownMs = envInt("MSLXDFF_QODER_COOLDOWN_MS", 30_000),
|
|
36
|
+
connectTimeoutMs = Number(process.env.MSLXDFF_QODER_TIMEOUT_MS) || 120_000,
|
|
37
|
+
} = {}) {
|
|
38
|
+
const keys = (() => {
|
|
39
|
+
if (Array.isArray(apiKeys) && apiKeys.length) return [...new Set(apiKeys.map((k) => String(k).trim()).filter(Boolean))];
|
|
40
|
+
try { return loadProviderKeys(id, file ? { file } : {}); } catch { return []; }
|
|
41
|
+
})();
|
|
42
|
+
let authList = [];
|
|
43
|
+
try { authList = loadProviderConfig(id, file ? { file } : {})?.auths || []; } catch {}
|
|
44
|
+
// auths 行的 region 会被 state.normalizeAuths 剥掉:每号真实 region 从 auths/qoder-<uid>.json 读(单一源)
|
|
45
|
+
try {
|
|
46
|
+
const docs = listAccountDocs();
|
|
47
|
+
for (const d of docs) {
|
|
48
|
+
const row = authList.find((a) => String(a?.uid) === String(d.uid));
|
|
49
|
+
if (row) row.region = d.doc?.auth?.region || "global";
|
|
50
|
+
else authList.push({ uid: d.uid, refreshToken: d.doc?.auth?.refreshToken || "", region: d.doc?.auth?.region || "global", name: d.doc?.account?.name || "" });
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
const ring = createKeyRing(keys, { cooldownMs });
|
|
54
|
+
if (!fetchImpl) fetchImpl = compatFetch;
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// 凭据 → 会话(会话含 RSA/AES 临时密钥,进程内可复用;此处按请求轻建,成本低)
|
|
59
|
+
function pickSession() {
|
|
60
|
+
const key = ring.next() || keys[0] || "";
|
|
61
|
+
const blob = accountFromBlob(key);
|
|
62
|
+
if (!blob?.deviceToken) return null;
|
|
63
|
+
const auth = authList.find((a) => String(a?.refreshToken || "") === String(blob.refreshToken || "")) || authList[0] || {};
|
|
64
|
+
const region = normalizeRegion(region0 || auth.region || "global");
|
|
65
|
+
const sess = buildSessionFor({ ...blob, uid: auth.uid, name: auth.name, region });
|
|
66
|
+
return { sess, region };
|
|
67
|
+
}
|
|
68
|
+
const region0 = region; // 构造参数优先
|
|
69
|
+
|
|
70
|
+
const chatSvc = createChatService({ id, fetchImpl, timeoutMs: connectTimeoutMs });
|
|
71
|
+
// models 服务按号选区:构造时无固定 region,listModels(sess, region) 动态传
|
|
72
|
+
const modelsSvc = createModelsService({ id, fetchImpl });
|
|
73
|
+
|
|
74
|
+
async function chat(body) {
|
|
75
|
+
const picked = pickSession();
|
|
76
|
+
if (!picked) {
|
|
77
|
+
return new Response(JSON.stringify({ error: { message: "qoder: 无可用账号 — 先跑 mslxdff -provider qoder login", type: "auth_error" } }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
78
|
+
}
|
|
79
|
+
const stripped = { ...body };
|
|
80
|
+
if (typeof stripped.model === "string" && stripped.model.startsWith("qoder/")) stripped.model = stripped.model.slice(6);
|
|
81
|
+
return chatSvc.runChat(stripped, picked.sess, picked.region);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function listModels() {
|
|
85
|
+
const picked = pickSession();
|
|
86
|
+
if (!picked) return [];
|
|
87
|
+
try { return await modelsSvc.listModels(picked.sess, picked.region); } catch { return []; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function preheat() {
|
|
91
|
+
try {
|
|
92
|
+
const picked = pickSession();
|
|
93
|
+
if (!picked) return { ok: false, error: "no account" };
|
|
94
|
+
await modelsSvc.listModels(picked.sess, picked.region);
|
|
95
|
+
return { ok: true };
|
|
96
|
+
} catch (e) { return { ok: false, error: String(e?.message || e).slice(0, 120) }; }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function close() {}
|
|
100
|
+
async function chatWithKeys(body, keysOverride) {
|
|
101
|
+
const tmp = createQoderProvider({ id, apiKeys: keysOverride, file, fetchImpl, cooldownMs, connectTimeoutMs });
|
|
102
|
+
return tmp.chat(body);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { id, chat, chatWithKeys, listModels, preheat, close, keyRing: ring, baseUrl: "qoder://native", region: normalizeRegion(region || authList[0]?.region) };
|
|
106
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// qoder 模型服务(转译 bridge.go ListModels/extractModels):原生 COSY 签名拉 /model/list,
|
|
2
|
+
// 10min 缓存;暴露全部 15 个 key(含 enable=false 的展示,allowlist 由上层过滤)。
|
|
3
|
+
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
4
|
+
import { getEndpoints, normalizeRegion } from "./constants.js";
|
|
5
|
+
import { buildCosyHeaders, pathSigFrom } from "./session.js";
|
|
6
|
+
import { joinModelId } from "../model-id.js";
|
|
7
|
+
|
|
8
|
+
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
9
|
+
const CREATED = 1753600000;
|
|
10
|
+
const OWNER = "qoder";
|
|
11
|
+
|
|
12
|
+
export function extractModels(chatList) {
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const m of Array.isArray(chatList) ? chatList : []) {
|
|
15
|
+
if (!m?.key) continue;
|
|
16
|
+
out.push({
|
|
17
|
+
id: m.key,
|
|
18
|
+
name: String(m.display_name || m.key),
|
|
19
|
+
enable: Boolean(m.enable),
|
|
20
|
+
isDefault: Boolean(m.is_default),
|
|
21
|
+
isReasoning: Boolean(m.is_reasoning),
|
|
22
|
+
contextLength: Number(m.max_input_tokens) || 0,
|
|
23
|
+
priceFactor: Number(m.price_factor) || 0,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createModelsService({ id = "qoder", fetchImpl, clock = Date.now } = {}) {
|
|
30
|
+
let cache = null, fetchedAt = 0;
|
|
31
|
+
|
|
32
|
+
async function listModels(sess, region = "global") {
|
|
33
|
+
const normRegion = normalizeRegion(region);
|
|
34
|
+
const now = clock();
|
|
35
|
+
if (cache && now - fetchedAt < CACHE_TTL_MS) return cache;
|
|
36
|
+
const ep = getEndpoints(normRegion);
|
|
37
|
+
const url = ep.modelListURL;
|
|
38
|
+
const headers = buildCosyHeaders(sess, pathSigFrom(url), "", "application/json");
|
|
39
|
+
const res = await fetchImpl(url, { headers, signal: timeoutSignal(15000) });
|
|
40
|
+
if (!res.ok) throw new Error(`models http ${res.status}`);
|
|
41
|
+
const j = await res.json().catch(() => ({}));
|
|
42
|
+
const models = extractModels(j.chat) || extractModels(j.assistant);
|
|
43
|
+
if (!models.length) throw new Error("models list empty");
|
|
44
|
+
cache = models.map((m) => ({
|
|
45
|
+
id: joinModelId(id, m.id),
|
|
46
|
+
object: "model",
|
|
47
|
+
created: CREATED,
|
|
48
|
+
owned_by: OWNER,
|
|
49
|
+
name: m.name,
|
|
50
|
+
context_length: m.contextLength,
|
|
51
|
+
enable: m.enable,
|
|
52
|
+
is_reasoning: m.isReasoning,
|
|
53
|
+
price_factor: m.priceFactor,
|
|
54
|
+
}));
|
|
55
|
+
fetchedAt = now;
|
|
56
|
+
return cache;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function clearCache() { cache = null; fetchedAt = 0; }
|
|
60
|
+
|
|
61
|
+
return { listModels, clearCache, _getCache: () => cache };
|
|
62
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// qoder OAuth 设备授权(转译 qoder2api account/oauth.go):
|
|
2
|
+
// 1. startLogin: PKCE(S256) + nonce → 返回授权链接
|
|
3
|
+
// 2. pollDeviceToken: 轮询 deviceToken/poll,404=未授权继续,200={token:dt-xxx, refresh_token:drt-xxx}
|
|
4
|
+
// 3. fetchUserInfo: GET /api/v1/userinfo(Bearer dt-xxx)→ uid/name/organization_id
|
|
5
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
6
|
+
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
7
|
+
import { OAUTH_CLIENT_ID, getEndpoints } from "./constants.js";
|
|
8
|
+
|
|
9
|
+
const POLL_MS = 3000;
|
|
10
|
+
export const POLL_TIMEOUT_MS = 10 * 60 * 1000; // 对齐 qoder2api 10 分钟
|
|
11
|
+
|
|
12
|
+
export function pkce() {
|
|
13
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
14
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
15
|
+
return { verifier, challenge };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hex16() {
|
|
19
|
+
return randomBytes(16).toString("hex");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildLoginUrl({ region = "global", nonce, challenge } = {}) {
|
|
23
|
+
const ep = getEndpoints(region);
|
|
24
|
+
const params = new URLSearchParams({
|
|
25
|
+
nonce,
|
|
26
|
+
challenge,
|
|
27
|
+
challenge_method: "S256",
|
|
28
|
+
client_id: OAUTH_CLIENT_ID,
|
|
29
|
+
});
|
|
30
|
+
return `${ep.deviceLoginBase}?${params.toString()}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 单次轮询:404/空 token → 未授权(返回 null);200 且有 token → bundle;5xx → 抛错。
|
|
34
|
+
export async function pollDeviceToken({ region = "global", nonce, verifier, fetchImpl = compatFetch } = {}) {
|
|
35
|
+
const ep = getEndpoints(region);
|
|
36
|
+
const url = `${ep.pollEndpoint}?nonce=${encodeURIComponent(nonce)}&verifier=${encodeURIComponent(verifier)}&challenge_method=S256`;
|
|
37
|
+
let res;
|
|
38
|
+
try {
|
|
39
|
+
res = await fetchImpl(url, { signal: timeoutSignal(20000) });
|
|
40
|
+
} catch (e) {
|
|
41
|
+
throw new Error(`轮询失败: ${e.message}`);
|
|
42
|
+
}
|
|
43
|
+
if (res.status === 404) return null; // 还没授权
|
|
44
|
+
if (res.status >= 500) throw new Error(`deviceToken 端点故障: HTTP ${res.status}`);
|
|
45
|
+
const txt = await res.text().catch(() => "");
|
|
46
|
+
let j;
|
|
47
|
+
try { j = JSON.parse(txt); } catch { throw new Error(`deviceToken 返回非 JSON: ${txt.slice(0, 200)}`); }
|
|
48
|
+
const deviceToken = String(j.token || "");
|
|
49
|
+
if (!deviceToken) return null;
|
|
50
|
+
return { deviceToken, refreshToken: String(j.refresh_token || "") };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// userinfo:ResolveOAuthUserID 优先 id/userId/uid(qoder2api bridge.go)。
|
|
54
|
+
export async function fetchUserInfo({ region = "global", deviceToken, fetchImpl = compatFetch } = {}) {
|
|
55
|
+
const ep = getEndpoints(region);
|
|
56
|
+
const res = await fetchImpl(ep.userinfoBase, {
|
|
57
|
+
headers: { Authorization: `Bearer ${deviceToken}` },
|
|
58
|
+
signal: timeoutSignal(20000),
|
|
59
|
+
});
|
|
60
|
+
const txt = await res.text().catch(() => "");
|
|
61
|
+
let j;
|
|
62
|
+
try { j = JSON.parse(txt); } catch { throw new Error(`userinfo 返回非 JSON: HTTP ${res.status} ${txt.slice(0, 200)}`); }
|
|
63
|
+
if (!res.ok) throw new Error(`userinfo HTTP ${res.status}: ${txt.slice(0, 200)}`);
|
|
64
|
+
const uid = String(j.id || j.userId || j.uid || "");
|
|
65
|
+
if (!uid) throw new Error(`userinfo 无 uid: ${txt.slice(0, 200)}`);
|
|
66
|
+
return {
|
|
67
|
+
uid,
|
|
68
|
+
name: String(j.name || ""),
|
|
69
|
+
email: String(j.email || ""),
|
|
70
|
+
userType: String(j.userType || j.user_type || "personal_standard"),
|
|
71
|
+
organizationId: String(j.organization_id || ""),
|
|
72
|
+
organizationName: String(j.organization_name || ""),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 轮询直到授权成功,返回 bundle;超时抛错。deps.log 便于测试注入。
|
|
77
|
+
export async function waitForAuth({ region, nonce, verifier, fetchImpl = compatFetch, log = console.log, deadline = Date.now() + POLL_TIMEOUT_MS } = {}) {
|
|
78
|
+
for (;;) {
|
|
79
|
+
if (Date.now() > deadline) throw new Error("授权超时(10 分钟),请重新运行 mslxdff -provider qoder login");
|
|
80
|
+
const bundle = await pollDeviceToken({ region, nonce, verifier, fetchImpl });
|
|
81
|
+
if (bundle) return bundle;
|
|
82
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
83
|
+
try { process.stdout.write("."); } catch {}
|
|
84
|
+
}
|
|
85
|
+
}
|