mslxdff 0.1.231 → 0.1.233

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 (117) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +264 -254
  4. package/src/bench/cline-bench.js +42 -42
  5. package/src/bench/probe.js +70 -70
  6. package/src/bench/report.js +162 -162
  7. package/src/bench/runner.js +77 -77
  8. package/src/bench/via-probe.js +124 -124
  9. package/src/bench/via-routes.js +87 -87
  10. package/src/bench/workbuddy-bench.js +54 -54
  11. package/src/chat/engine.js +160 -160
  12. package/src/chat/gateway.js +163 -163
  13. package/src/chat/orchestrator.js +234 -234
  14. package/src/chat/prompt.js +70 -70
  15. package/src/chat/repl.js +88 -88
  16. package/src/chat/terminal.js +135 -135
  17. package/src/chat/tools.js +306 -306
  18. package/src/chat-pipeline/index.js +155 -145
  19. package/src/chat-pipeline/policy.js +84 -84
  20. package/src/cli/commands/group.js +266 -266
  21. package/src/cli/commands/model/picks.js +50 -50
  22. package/src/cli/commands/provider/bench-via.js +247 -247
  23. package/src/cli/commands/provider/bench.js +141 -141
  24. package/src/cli/commands/provider/index.js +139 -139
  25. package/src/cli/commands/provider/models.js +149 -149
  26. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  27. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  28. package/src/cli/commands/sync.js +310 -310
  29. package/src/cli/status.js +279 -279
  30. package/src/daemon.js +246 -246
  31. package/src/model-capabilities/enrich.js +86 -86
  32. package/src/model-capabilities/index.js +183 -183
  33. package/src/model-capabilities/parse.js +70 -70
  34. package/src/model-trace.js +10 -2
  35. package/src/models.js +292 -225
  36. package/src/peers.js +345 -345
  37. package/src/providers/agentrouter.js +238 -238
  38. package/src/providers/base.js +164 -164
  39. package/src/providers/cline/auth.js +228 -228
  40. package/src/providers/cline/chat.js +307 -307
  41. package/src/providers/cline.js +2 -2
  42. package/src/providers/codearts/chat.js +207 -207
  43. package/src/providers/dispatcher.js +147 -147
  44. package/src/providers/globalqwenwork/cosy.js +123 -123
  45. package/src/providers/globalqwenwork/crypto.js +218 -218
  46. package/src/providers/globalqwenwork/http.js +20 -20
  47. package/src/providers/globalqwenwork/rsa.js +54 -54
  48. package/src/providers/globalqwenwork/sse.js +268 -268
  49. package/src/providers/globalqwenwork/stream.js +130 -130
  50. package/src/providers/globalqwenwork/upstream.js +122 -122
  51. package/src/providers/keyring.js +60 -60
  52. package/src/providers/qoder/account-store.js +18 -0
  53. package/src/providers/qoder/chat.js +292 -203
  54. package/src/providers/qoder/checkin.js +25 -12
  55. package/src/providers/qoder/index.js +311 -249
  56. package/src/providers/qoder/models.js +64 -2
  57. package/src/providers/qoder/oauth.js +42 -1
  58. package/src/providers/qoder/payload.js +116 -80
  59. package/src/providers/qoder/request.js +7 -2
  60. package/src/providers/qoder/session.js +26 -10
  61. package/src/providers/qoder/sse.js +233 -145
  62. package/src/providers/qoder/sticky.js +5 -2
  63. package/src/providers/qoder/wire-messages.js +242 -0
  64. package/src/providers/qwenwork/account-store.js +133 -133
  65. package/src/providers/qwenwork/constants.js +67 -67
  66. package/src/providers/qwenwork/cosy.js +120 -120
  67. package/src/providers/qwenwork/crypto.js +218 -218
  68. package/src/providers/qwenwork/http.js +20 -20
  69. package/src/providers/qwenwork/index.js +337 -337
  70. package/src/providers/qwenwork/payload.js +144 -144
  71. package/src/providers/qwenwork/rsa.js +54 -54
  72. package/src/providers/qwenwork/sse.js +268 -268
  73. package/src/providers/qwenwork/stream.js +130 -130
  74. package/src/providers/qwenwork/upstream.js +120 -120
  75. package/src/providers/qwenwork.js +1 -1
  76. package/src/providers/raccoon/chat.js +1 -1
  77. package/src/providers/raccoon/const.js +219 -202
  78. package/src/providers/raccoon/credits.js +285 -285
  79. package/src/providers/raccoon/index.js +15 -2
  80. package/src/providers/raccoon/models.js +68 -17
  81. package/src/providers/registry.js +89 -89
  82. package/src/providers/workbuddy/auth.js +16 -0
  83. package/src/providers/workbuddy/chat.js +298 -264
  84. package/src/providers/workbuddy/client-headers.js +148 -0
  85. package/src/providers/workbuddy/desktop-info.js +17 -1
  86. package/src/providers/workbuddy/models.js +8 -20
  87. package/src/providers/workbuddy/reshape.js +152 -152
  88. package/src/providers/workbuddy.js +2 -2
  89. package/src/reasoning.js +32 -32
  90. package/src/routes/chat/broadband-handler.js +80 -80
  91. package/src/routes/chat/gateway.js +46 -46
  92. package/src/routes/chat/relay-pipeline.js +282 -282
  93. package/src/routes/chat/via-route-handler.js +156 -156
  94. package/src/routes/hedge.js +260 -260
  95. package/src/routes/models-route.js +167 -167
  96. package/src/routes/peers.js +373 -373
  97. package/src/routes/stream.js +364 -347
  98. package/src/runtime/bootstrap.js +75 -75
  99. package/src/runtime/keep-warm.js +135 -135
  100. package/src/runtime/provider-gate.js +75 -75
  101. package/src/runtime/providers-setup.js +187 -187
  102. package/src/runtime/raccoon-checkin.js +175 -175
  103. package/src/server.js +85 -85
  104. package/src/state/schemas/allowlist.js +92 -92
  105. package/src/sync-opencode.js +280 -280
  106. package/src/transport/index.js +244 -244
  107. package/src/transport/pool.js +56 -56
  108. package/src/transport/retry.js +24 -24
  109. package/src/transport/sse.js +93 -93
  110. package/src/upstream-probe/display.js +52 -52
  111. package/src/upstream-probe/probe.js +49 -49
  112. package/src/upstream-probe/rotate.js +110 -110
  113. package/src/upstream-probe/start.js +45 -45
  114. package/src/upstream.js +355 -355
  115. package/docs/cli_help_mini.md +0 -148
  116. package/src/providers/AGENTS.md +0 -49
  117. package/src/routes/AGENTS.md +0 -37
@@ -1,5 +1,8 @@
1
- // qoder 模型服务(转译 bridge.go ListModels/extractModels):原生 COSY 签名拉 /model/list,
2
- // 10min 缓存(按 region 分桶);只暴露 enable=true 的可用模型(enable=false 调对话必挂,不展示)。
1
+ // qoder 模型服务(对齐官方 qodercli + 外部参考 magpie qoder 插件/CLIProxyAPI):
2
+ // 原生 COSY 签名拉 /model/list,10min 缓存(按 region 分桶);
3
+ // 只暴露 enable=true 的可调用模型(enable=false 调对话必挂,不展示)。
4
+ // context_config(多窗口 200K/400K/1M)与 thinking_config(推理档位)随目录带出,
5
+ // 供 payload 选窗口/档位、供能力层展示——旧版只留 max_input_tokens,拿不到多窗口是"不好用"的一环。
3
6
  import { compatFetch, timeoutSignal } from "../../compat.js";
4
7
  import { getEndpoints, normalizeRegion } from "./constants.js";
5
8
  import { buildCosyHeaders, pathSigFrom } from "./session.js";
@@ -9,10 +12,57 @@ const CACHE_TTL_MS = 10 * 60 * 1000;
9
12
  const CREATED = 1753600000;
10
13
  const OWNER = "qoder";
11
14
 
15
+ const positive = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
16
+
17
+ // context_config → { windows:[...升序], defaultWindow }(Qoder 的 Context 设置:200K/400K/1M)
18
+ function parseContextConfig(value) {
19
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
20
+ const windows = [];
21
+ let defaultWindow;
22
+ for (const raw of Object.values(value)) {
23
+ if (typeof raw !== "object" || raw === null) continue;
24
+ const tc = positive(raw.token_count);
25
+ if (tc === undefined) continue;
26
+ windows.push(tc);
27
+ if (raw.is_default === true) defaultWindow = tc;
28
+ }
29
+ windows.sort((a, b) => a - b);
30
+ return {
31
+ ...(windows.length ? { windows } : {}),
32
+ ...(defaultWindow !== undefined ? { defaultWindow } : {}),
33
+ };
34
+ }
35
+
36
+ // thinking_config → { efforts:[...], defaultEffort, enabled }(推理档位与是否可关思考)
37
+ const EFFORT_ORDER = ["low", "medium", "high", "xhigh", "max"];
38
+ function parseThinkingConfig(value) {
39
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
40
+ const out = {};
41
+ const isDefault = (e) => typeof e === "object" && e !== null && e.is_default === true;
42
+ const enabled = value.enabled && typeof value.enabled === "object" ? value.enabled : null;
43
+ if (enabled) {
44
+ out.thinks = true;
45
+ const raw = typeof enabled.efforts === "object" && enabled.efforts !== null && !Array.isArray(enabled.efforts) ? enabled.efforts : null;
46
+ if (raw) {
47
+ const efforts = Object.keys(raw).filter((k) => k.trim());
48
+ efforts.sort((a, b) => (EFFORT_ORDER.indexOf(a) < 0 ? 99 : EFFORT_ORDER.indexOf(a)) - (EFFORT_ORDER.indexOf(b) < 0 ? 99 : EFFORT_ORDER.indexOf(b)));
49
+ if (efforts.length) out.efforts = efforts;
50
+ for (const [k, v] of Object.entries(raw)) if (isDefault(v)) out.defaultEffort = k;
51
+ }
52
+ }
53
+ if (value.disabled && typeof value.disabled === "object") out.alwaysThinks = false;
54
+ // enabled.is_default 与 disabled.is_default 都为 true/都 false → 上游给的是"默认开还是默认关"
55
+ if (enabled && isDefault(value.disabled)) out.thinkDefault = false;
56
+ else if (enabled && isDefault(enabled)) out.thinkDefault = true;
57
+ return out;
58
+ }
59
+
12
60
  export function extractModels(chatList) {
13
61
  const out = [];
14
62
  for (const m of Array.isArray(chatList) ? chatList : []) {
15
63
  if (!m?.key) continue;
64
+ const ctx = parseContextConfig(m.context_config);
65
+ const think = parseThinkingConfig(m.thinking_config);
16
66
  out.push({
17
67
  id: m.key,
18
68
  name: String(m.display_name || m.key),
@@ -20,12 +70,18 @@ export function extractModels(chatList) {
20
70
  isDefault: Boolean(m.is_default),
21
71
  isReasoning: Boolean(m.is_reasoning),
22
72
  contextLength: Number(m.max_input_tokens) || 0,
73
+ maxOutputTokens: positive(m.max_output_tokens),
23
74
  priceFactor: Number(m.price_factor) || 0,
75
+ isVl: m.is_vl === true,
76
+ source: m.source || "system",
77
+ ...ctx,
78
+ ...think,
24
79
  });
25
80
  }
26
81
  return out;
27
82
  }
28
83
 
84
+
29
85
  export function createModelsService({ id = "qoder", fetchImpl, clock = Date.now } = {}) {
30
86
  let cache = null, fetchedAt = 0;
31
87
 
@@ -59,6 +115,12 @@ export function createModelsService({ id = "qoder", fetchImpl, clock = Date.now
59
115
  enable: true,
60
116
  is_reasoning: m.isReasoning,
61
117
  price_factor: m.priceFactor,
118
+ // 供 payload 选窗口/档位、能力层展示(随目录带出,不进 /v1/models 也无害,额外字段客户端忽略)
119
+ ...(m.windows ? { context_windows: m.windows } : {}),
120
+ ...(m.defaultWindow ? { default_window: m.defaultWindow } : {}),
121
+ ...(m.efforts ? { reasoning_efforts: m.efforts } : {}),
122
+ ...(m.defaultEffort ? { default_reasoning_effort: m.defaultEffort } : {}),
123
+ ...(m.maxOutputTokens ? { max_output_tokens: m.maxOutputTokens } : {}),
62
124
  }));
63
125
  cacheByRegion.set(normRegion, { list, at: now });
64
126
  // 兼容旧单测 _getCache:默认读 global 桶
@@ -4,7 +4,7 @@
4
4
  // 3. fetchUserInfo: GET /api/v1/userinfo(Bearer dt-xxx)→ uid/name/organization_id
5
5
  import { randomBytes, createHash } from "node:crypto";
6
6
  import { compatFetch, timeoutSignal } from "../../compat.js";
7
- import { OAUTH_CLIENT_ID, getEndpoints } from "./constants.js";
7
+ import { OAUTH_CLIENT_ID, getEndpoints, openapiBase } from "./constants.js";
8
8
 
9
9
  const POLL_MS = 3000;
10
10
  export const POLL_TIMEOUT_MS = 10 * 60 * 1000; // 对齐 qoder2api 10 分钟
@@ -82,4 +82,45 @@ export async function waitForAuth({ region, nonce, verifier, fetchImpl = compatF
82
82
  await new Promise((r) => setTimeout(r, POLL_MS));
83
83
  try { process.stdout.write("."); } catch {}
84
84
  }
85
+ }
86
+
87
+ // device token 续期(对齐官方 CLI + magpie refreshDevice):deviceToken 时效短(约 1 天),
88
+ // 官方在收到 105 Login expired 时用 refresh_token 换全新的一对(旧 refresh_token 一次性,换到即弃)。
89
+ // POST {openapiBase}/api/v1/deviceToken/refresh body{refresh_token} → { token|device_token, refresh_token }。
90
+ // 只带 refresh_token 认证(过期 deviceToken 无需当 Bearer);401/403 = refresh 也被拒 → 真需重登。
91
+ // 抛错语义:{ retryable } 标记——网络/5xx 可稍后再试,4xx(refresh 作废)不可重试。
92
+ export async function refreshDeviceToken({ region = "global", refreshToken, fetchImpl = compatFetch } = {}) {
93
+ const rt = String(refreshToken || "").trim();
94
+ if (!rt) { const e = new Error("无 refresh_token,需重新登录"); e.retryable = false; throw e; }
95
+ const url = `${openapiBase(region)}/api/v1/deviceToken/refresh`;
96
+ let res;
97
+ try {
98
+ res = await fetchImpl(url, {
99
+ method: "POST",
100
+ headers: { accept: "application/json", "content-type": "application/json" },
101
+ body: JSON.stringify({ refresh_token: rt }),
102
+ signal: timeoutSignal(20000),
103
+ });
104
+ } catch (e) {
105
+ const err = new Error(`deviceToken 刷新请求失败: ${String(e?.message || e).slice(0, 160)}`);
106
+ err.retryable = true; throw err;
107
+ }
108
+ const txt = await res.text().catch(() => "");
109
+ if (res.status === 401 || res.status === 403) {
110
+ const err = new Error(`deviceToken 刷新被拒 HTTP ${res.status}(refresh_token 已作废,需重新登录)`);
111
+ err.retryable = false; throw err;
112
+ }
113
+ if (!res.ok) {
114
+ const err = new Error(`deviceToken 刷新上游 HTTP ${res.status}: ${txt.slice(0, 160)}`);
115
+ err.retryable = true; throw err;
116
+ }
117
+ let j;
118
+ try { j = JSON.parse(txt); } catch { const err = new Error(`deviceToken 刷新返回非 JSON: ${txt.slice(0, 160)}`); err.retryable = false; throw err; }
119
+ const deviceToken = String(j.token || j.device_token || "").trim();
120
+ const newRefresh = String(j.refresh_token || j.refreshToken || "").trim();
121
+ if (!deviceToken || !newRefresh) {
122
+ const err = new Error("deviceToken 刷新返回不完整 token 对(缺 token 或 refresh_token)");
123
+ err.retryable = false; throw err;
124
+ }
125
+ return { deviceToken, refreshToken: newRefresh };
85
126
  }
@@ -1,13 +1,28 @@
1
- // qoder 消息/payload 组装(转译 qoder2api bridge.go CallQoderWithOpts + messages.go)
2
- // 模板:baseprompt.json(vendored 数据文件,含 {UUID1..5}/{TIME1} 占位符,加载时替换一次)
1
+ // qoder 请求体装配(对齐官方 qodercli 线格式;外部参考:dsh-qoder-connect wire/serialize.ts、magpie qoder 插件)。
2
+ // 消息形状转换层(OpenAI→Qoder wire messages / 会话指纹 / 缓存标记 / tool 配对)已拆至 wire-messages.js
3
+ // (体积门 + 职责边界)。本文件只管:
4
+ // - 模板外壳加载(baseprompt.json 只作 parameters/model_config 骨架,messages/tools 一律重建);
5
+ // - business.id = request_set_id(上游把排队条目与 business id 绑定,各发一个 UUID 会让排队/回放关联失效);
6
+ // - business.product=cli(我们伪装的是 qodercli,不是 IDE);
7
+ // - 思考档位/上下文窗口按模型目录(meta)补写;模型名映射 mapModel。
3
8
  import { readFileSync } from "node:fs";
4
9
  import { join, dirname } from "node:path";
5
10
  import { fileURLToPath } from "node:url";
6
11
  import { newUUID, unixMs } from "./fingerprint.js";
12
+ import {
13
+ normalizeContent, buildUserMessage, buildStructuredMessage,
14
+ toQoderWireMessages, conversationSessionId, cacheMark,
15
+ } from "./wire-messages.js";
16
+
17
+ // 转换层符号 re-export(外部 import 路径保持 payload.js 不变;新代码建议直接 import wire-messages.js)
18
+ export {
19
+ blankResponseMeta, buildUserMessage, buildStructuredMessage, normalizeContent,
20
+ withQoderPersona, toQoderWireMessages, conversationSessionId, cacheMark,
21
+ } from "./wire-messages.js";
7
22
 
8
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
24
 
10
- // 模板单例:进程内加载一次(替换占位符后 JSON.parse),每请求深拷贝
25
+ // 模板单例:进程内加载一次(替换占位符后 JSON.parse)
11
26
  let _template = null;
12
27
  export function loadTemplate(templatePath) {
13
28
  if (_template && !templatePath) return _template;
@@ -18,56 +33,113 @@ export function loadTemplate(templatePath) {
18
33
  return _template;
19
34
  }
20
35
 
21
- export function blankResponseMeta() {
22
- return {
23
- id: "",
24
- usage: {
25
- prompt_tokens: 0, completion_tokens: 0, total_tokens: 0,
26
- completion_tokens_details: { reasoning_tokens: 0 },
27
- prompt_tokens_details: { cached_tokens: 0 },
28
- },
29
- };
30
- }
31
-
32
- // OpenAI user 消息 → Qoder contents 形状
33
- export function buildUserMessage(text) {
34
- return {
35
- role: "user", content: "",
36
- contents: [{ type: "text", text: String(text || "") }],
37
- response_meta: blankResponseMeta(),
38
- reasoning_content_signature: "",
39
- };
36
+ // 档位对齐(参考 magpie effortFor):把客户端要档位映射到模型声明档位集合里的最近一档(平手取高)。
37
+ const EFFORT_RANK = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
38
+ export function fitEffort(want, levels) {
39
+ const w = String(want || "").toLowerCase();
40
+ if (!Array.isArray(levels) || !levels.length || levels.includes(w)) return w;
41
+ const at = EFFORT_RANK.indexOf(w);
42
+ if (at < 0) return w;
43
+ let best = w, dist = EFFORT_RANK.length;
44
+ for (const l of levels) {
45
+ const i = EFFORT_RANK.indexOf(l);
46
+ if (i < 0 || l === "none") continue;
47
+ const d = Math.abs(i - at);
48
+ if (d < dist || (d === dist && i > at)) { best = l; dist = d; }
49
+ }
50
+ return best;
40
51
  }
41
52
 
42
- export function buildStructuredMessage(role, text) {
43
- return { role, content: String(text || ""), response_meta: blankResponseMeta(), reasoning_content_signature: "" };
53
+ // 上下文窗口选择(参考 magpie windowFor):请求装得下默认窗口就用默认;
54
+ // 装不下按能容纳的最小窗口升档(bytes/3 粗估 + 预留回复长度),全装不下给最大档。
55
+ export function pickContextWindow(need, meta) {
56
+ const base = Number(meta?.defaultWindow) || Number(meta?.context_length) || 0;
57
+ const windows = Array.isArray(meta?.windows) ? meta.windows : [];
58
+ if (base > 0 && need <= base) return base;
59
+ return windows.find((w) => w >= need) ?? (windows.length ? windows[windows.length - 1] : 0);
44
60
  }
45
61
 
46
- export function normalizeContent(content) {
47
- if (typeof content === "string") return content;
48
- if (content == null) return "";
49
- if (Array.isArray(content)) {
50
- let out = "";
51
- for (const b of content) {
52
- const t = typeof b === "object" && b ? b.text : b;
53
- if (typeof t === "string") out += (out ? "\n\n" : "") + t;
62
+ // 组装上游请求体。template 外壳字段(parameters/model_config 骨架等)保留,
63
+ // messages/tools/ids/chat_context/business 一律重建。
64
+ // meta:模型目录行(context_windows/default_window/reasoning_efforts/default_reasoning_effort/max_output_tokens);
65
+ // effort:客户端声明的思考档位原始串(可空)。
66
+ export function buildQoderBody({ template, userId, userType, model, messages, tools, maxTokens, isReasoning, meta, effort, preserveTemplateHistory = false }) {
67
+ const tmpl = template || loadTemplate();
68
+ const body = JSON.parse(JSON.stringify(tmpl));
69
+ const nid = newUUID();
70
+ const setId = newUUID();
71
+ body.request_id = nid;
72
+ body.chat_record_id = nid;
73
+ body.request_set_id = setId;
74
+ body.stream = true;
75
+ body.aliyun_user_type = userType || "personal_standard";
76
+ if (body.model_config) {
77
+ body.model_config.key = model;
78
+ if (isReasoning) body.model_config.is_reasoning = true;
79
+ }
80
+ // 推理标志两处镜像写(对齐官方 CLI 请求形状),只写 model_config 上游不吐 reasoning_content
81
+ if (isReasoning && body.chat_context?.extra?.modelConfig) {
82
+ body.chat_context.extra.modelConfig.is_reasoning = true;
83
+ body.chat_context.extra.modelConfig.key = model; // 与 model_config.key 同步,不留模板 "auto"
84
+ }
85
+ const wireMessages = cacheMark(preserveTemplateHistory
86
+ ? buildQoderMessages(tmpl, messages, Array.isArray(tools) && tools.length > 0) // 回退通道:MSLXDFF_QODER_LEGACY_WIRE=1
87
+ : toQoderWireMessages(messages));
88
+ body.session_id = conversationSessionId(userId, model, wireMessages);
89
+ const outCap = maxTokens > 0 ? maxTokens : (Number(meta?.max_output_tokens) || 0);
90
+ if (outCap > 0 && body.parameters) body.parameters.max_tokens = outCap;
91
+ // 思考参数按真值口径补写(models.js 目录行为 snake_case):enable_thinking + 命中模型声明档位才发 reasoning_effort。
92
+ // 只在模型确实支持时补写,避免把未知参数/无效档位打给不认的模型(fitEffort 兜底后仍须 ∈ efforts 才发)。
93
+ if (body.parameters) {
94
+ const efforts = Array.isArray(meta?.reasoning_efforts) ? meta.reasoning_efforts : [];
95
+ const want = String(effort || "").trim().toLowerCase();
96
+ if (isReasoning) {
97
+ body.parameters.enable_thinking = true;
98
+ if (efforts.length) {
99
+ const cand = want && EFFORT_RANK.includes(want)
100
+ ? fitEffort(want, efforts)
101
+ : String(meta?.default_reasoning_effort || efforts[0] || "");
102
+ if (efforts.includes(cand)) body.parameters.reasoning_effort = cand;
103
+ }
104
+ } else if (want === "none" && efforts.length) {
105
+ body.parameters.enable_thinking = false; // 模型可关思考才发关;不可关的(无 efforts)保持默认不干预
106
+ }
107
+ // 上下文窗口升档(仅模型声明了多窗口才发)
108
+ const windows = Array.isArray(meta?.context_windows) ? meta.context_windows : [];
109
+ if (windows.length) {
110
+ const need = Math.ceil(JSON.stringify({ m: wireMessages, t: tools || [] }).length / 3) + (outCap || 32000);
111
+ const w = pickContextWindow(need, { defaultWindow: meta.default_window, context_length: meta.context_length, windows });
112
+ if (w > 0) body.parameters.context_length = w;
54
113
  }
55
- return out;
56
114
  }
57
- return JSON.stringify(content);
58
- }
59
-
60
- // 提取最后一条 user 消息文本(仿 ExtractLatestUserPrompt)
61
- export function extractLatestUserPrompt(messages) {
62
- for (let i = messages.length - 1; i >= 0; i--) {
63
- const m = messages[i];
64
- if (m?.role === "user") return normalizeContent(m.content);
115
+ // chat_context 的 text/originalContent 是纯字符串(旧模板形状是 {type,text} 对象——官方从不发对象形状)
116
+ const prompt = (() => {
117
+ for (let i = wireMessages.length - 1; i >= 0; i--) {
118
+ const m = wireMessages[i];
119
+ if (m?.role === "user") return normalizeContent(m.contents?.map((p) => p.text ?? "").join("\n\n") || m.content);
120
+ }
121
+ return "";
122
+ })();
123
+ if (body.business) {
124
+ body.business.id = setId; // 排队条目与 business.id 绑定,必须 = request_set_id
125
+ body.business.product = "cli";
126
+ body.business.version = "1.0.0";
127
+ body.business.begin_at = unixMs();
128
+ body.business.name = prompt.length > 30 ? prompt.slice(0, 30) : prompt;
129
+ }
130
+ if (body.chat_context) {
131
+ body.chat_context.text = prompt;
132
+ if (body.chat_context.extra) body.chat_context.extra.originalContent = prompt;
65
133
  }
66
- return "";
134
+ body.messages = wireMessages;
135
+ // tools 空数组契约:模板内嵌的 14 个 Qoder CLI 工具永不出门;无工具时上游官方亦发 []
136
+ body.tools = Array.isArray(tools) && tools.length ? tools : [];
137
+ return { body, prompt, mcSource: body.model_config?.source || "system" };
67
138
  }
68
139
 
69
- // OpenAI messages → Qoder messages(仿 BuildQoderMessages:无 system 前置模板 system;
70
- // user 含 tool_result 数组拆成 tool 行;assistant tool_calls 透传)
140
+ // OpenAI messages → Qoder messages(旧版形状:无 system 前置模板 system;user=contents[];
141
+ // assistant tool_calls 透传;含 tool_result 数组拆成 tool 行)。
142
+ // 仅保留给 MSLXDFF_QODER_LEGACY_WIRE=1 回退通道;正常路径用 wire-messages.js 的 toQoderWireMessages。
71
143
  export function buildQoderMessages(template, incoming, toolsEnabled) {
72
144
  const rebuilt = [];
73
145
  const hasSystem = (incoming || []).some((m) => m?.role === "system");
@@ -105,42 +177,6 @@ export function buildQoderMessages(template, incoming, toolsEnabled) {
105
177
  return rebuilt;
106
178
  }
107
179
 
108
- // 组装上游请求体(仿 CallQoderWithOpts)
109
- export function buildQoderBody({ template, userType, model, messages, tools, maxTokens, isReasoning }) {
110
- const body = JSON.parse(JSON.stringify(template || loadTemplate()));
111
- const nid = newUUID();
112
- body.request_id = nid;
113
- body.chat_record_id = nid;
114
- body.request_set_id = newUUID();
115
- body.session_id = newUUID();
116
- body.stream = true;
117
- body.aliyun_user_type = userType || "personal_standard";
118
- if (body.model_config) {
119
- body.model_config.key = model;
120
- if (isReasoning) body.model_config.is_reasoning = true;
121
- }
122
- // 推理标志两处镜像写(对齐官方 CLI 请求形状),只写 model_config 上游不吐 reasoning_content
123
- if (isReasoning && body.chat_context?.extra?.modelConfig) {
124
- body.chat_context.extra.modelConfig.is_reasoning = true;
125
- body.chat_context.extra.modelConfig.key = model; // 与 model_config.key 同步,不留模板 "auto"(评审路1 P1#5)
126
- }
127
- if (maxTokens > 0 && body.parameters) body.parameters.max_tokens = maxTokens;
128
- const prompt = extractLatestUserPrompt(messages);
129
- if (body.business) {
130
- body.business.id = newUUID();
131
- body.business.begin_at = unixMs();
132
- body.business.name = prompt.length > 30 ? prompt.slice(0, 30) : prompt;
133
- }
134
- if (body.chat_context) {
135
- if (body.chat_context.text) body.chat_context.text.text = prompt;
136
- if (body.chat_context.extra?.originalContent) body.chat_context.extra.originalContent.text = prompt;
137
- }
138
- body.messages = messages;
139
- // tools 空数组契约:模板内嵌的 14 个 Qoder CLI 工具永不出门;无工具时上游官方亦发 []
140
- body.tools = Array.isArray(tools) && tools.length ? tools : [];
141
- return { body, prompt, mcSource: body.model_config?.source || "system" };
142
- }
143
-
144
180
  // 客户端模型名 → 上游 key(仿 defaultModelMapping 双向 substring,家族关键字兜底)
145
181
  const DEFAULT_MAPPING = {
146
182
  opus: "qmodel_38max", sonnet: "gmodel", haiku: "qfmodel",
@@ -1,20 +1,25 @@
1
1
  // qoder 上游请求装配(纯函数,无网络):OpenAI body → COSY 签名请求(url/headers/bodyStr)。
2
2
  // 从 chat.js 拆出,使真流式/聚合两条管线共享同一装配口(单一来源,防两处签名漂移)。
3
+ // MSLXDFF_QODER_LEGACY_WIRE=1 → 回退旧形状(模板注入历史 + OpenAI messages 原样)。
3
4
  import { getEndpoints, normalizeRegion } from "./constants.js";
4
5
  import { buildCosyHeaders, pathSigFrom } from "./session.js";
5
6
  import { cosyEncode } from "./encode.js";
6
7
  import { buildQoderBody, mapModel } from "./payload.js";
7
8
 
8
- export function buildUpstreamRequest({ sess, region, model, messages, tools, maxTokens, isReasoning }) {
9
+ export function buildUpstreamRequest({ sess, region, model, messages, tools, maxTokens, isReasoning, meta, effort }) {
9
10
  const upstreamModel = mapModel(model);
10
11
  const { body, mcSource } = buildQoderBody({
11
12
  template: undefined,
13
+ userId: sess?.identity?.uid,
12
14
  userType: sess?.identity?.userType,
13
15
  model: upstreamModel,
14
16
  messages,
15
17
  tools,
16
18
  maxTokens,
17
19
  isReasoning,
20
+ meta,
21
+ effort,
22
+ preserveTemplateHistory: process.env.MSLXDFF_QODER_LEGACY_WIRE === "1",
18
23
  });
19
24
  const ep = getEndpoints(normalizeRegion(region));
20
25
  const url = ep.chatStreamURL;
@@ -23,5 +28,5 @@ export function buildUpstreamRequest({ sess, region, model, messages, tools, max
23
28
  headers["x-model-key"] = upstreamModel;
24
29
  headers["x-model-source"] = mcSource;
25
30
  headers["accept"] = "text/event-stream";
26
- return { url, headers, bodyStr, upstreamModel, mcSource };
31
+ return { url, headers, bodyStr, upstreamModel, mcSource, requestSetId: body.request_set_id, sessionId: body.session_id, modelKey: upstreamModel };
27
32
  }
@@ -6,7 +6,12 @@ import { cosyEncode } from "./encode.js";
6
6
  import { newUUID, unixSec } from "./fingerprint.js";
7
7
 
8
8
  export const APP_CODE = "cosy";
9
- export const VERSION = "1.0.10";
9
+ // 身份版本(对齐外部参考 dsh-qoder-connect wire/cosy.ts 的 CLI 口径 1.1.47;env 可覆盖以便上游升版)
10
+ export const VERSION = process.env.MSLXDFF_QODER_CLI_VERSION || "1.1.47";
11
+ export const CLI_UA = `qoder/${VERSION}`;
12
+ const MACHINE_OS = process.platform === "win32"
13
+ ? (process.arch === "arm64" ? "aarch64_windows" : "x86_64_windows")
14
+ : (process.arch === "arm64" ? "aarch64_linux" : "x86_64_linux");
10
15
  // base64("war, war never changes")
11
16
  const SECRET_B64 = "d2FyLCB3YXIgbmV2ZXIgY2hhbmdlcw==";
12
17
 
@@ -106,30 +111,41 @@ export function pathSigFrom(url) {
106
111
  }
107
112
  }
108
113
 
109
- // 构造请求头
110
- export function buildCosyHeaders(sess, pathSig, bodyStr, accept) {
114
+ // 构造请求头(身份对齐 qodercli 参考实现:UA=qoder/<ver>、补 bodyhash/sigpath/machineos/x-request-id,
115
+ // 摘掉 IDE 伪装头 cosy-scene/business-*——上游按请求头指纹分级,IDE+Go UA 是排队重灾区)
116
+ // correlation: { requestId, sessionId } —— 队列探活与推理必须携带同一对 id 才能关联。
117
+ export function buildCosyHeaders(sess, pathSig, bodyStr, accept, correlation) {
111
118
  const payloadB64 = buildPayloadB64(sess.info);
112
119
  const date = String(unixSec());
113
- const sig = signRequest(payloadB64, sess.cosyKey, date, bodyStr || "", pathSig);
120
+ const body = bodyStr || "";
121
+ const sig = signRequest(payloadB64, sess.cosyKey, date, body, pathSig);
114
122
  const bearer = composeBearer(payloadB64, sig);
123
+ const bodyHash = createHash("md5").update(body).digest("hex");
115
124
  return {
116
- "cosy-data-policy": "agree",
125
+ "cosy-data-policy": "disagree",
117
126
  "content-type": "application/json",
118
127
  "cosy-machinetype": sess.machineType,
128
+ "cosy-machinetoken": sess.machineToken,
129
+ "cosy-machineos": MACHINE_OS,
119
130
  "cosy-clienttype": "5",
131
+ "cosy-clientip": "127.0.0.1",
132
+ "cosy-bodyhash": bodyHash,
133
+ "cosy-bodylength": String(Buffer.byteLength(body)),
134
+ "cosy-sigpath": pathSig,
135
+ "cosy-organization-id": sess.identity?.organizationId || "",
136
+ "cosy-organization-tags": "",
120
137
  "cosy-date": date,
121
138
  "cosy-user": sess.identity.uid,
122
139
  "cosy-key": sess.cosyKey,
123
140
  "cache-control": "no-cache",
124
141
  "accept": accept || "application/json",
142
+ "accept-encoding": "identity",
125
143
  "authorization": bearer,
126
144
  "cosy-version": VERSION,
127
145
  "cosy-machineid": sess.machineId,
128
- "cosy-machinetoken": sess.machineToken,
129
146
  "login-version": "v2",
130
- "user-agent": "Go-http-client/2.0",
131
- "cosy-scene": "assistant",
132
- "cosy-business-product": "ide",
133
- "cosy-business-type": "agent",
147
+ "user-agent": CLI_UA,
148
+ "x-request-id": correlation?.requestId || newUUID(),
149
+ ...(correlation?.sessionId ? { "x-session-id": String(correlation.sessionId) } : {}),
134
150
  };
135
151
  }