mslxdff 0.1.197 → 0.1.199

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 (95) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/docs/cli_help_mini.md +146 -0
  3. package/package.json +1 -1
  4. package/src/auto.js +254 -254
  5. package/src/bench/cline-bench.js +42 -42
  6. package/src/bench/probe.js +70 -70
  7. package/src/bench/report.js +162 -162
  8. package/src/bench/runner.js +77 -77
  9. package/src/bench/via-probe.js +124 -124
  10. package/src/bench/via-routes.js +87 -87
  11. package/src/bench/workbuddy-bench.js +54 -54
  12. package/src/chat/engine.js +160 -160
  13. package/src/chat/gateway.js +163 -163
  14. package/src/chat/orchestrator.js +234 -234
  15. package/src/chat/prompt.js +70 -70
  16. package/src/chat/repl.js +88 -88
  17. package/src/chat/terminal.js +135 -135
  18. package/src/chat/tools.js +306 -306
  19. package/src/chat-pipeline/index.js +173 -173
  20. package/src/chat-pipeline/policy.js +84 -84
  21. package/src/chat-pipeline/serial-trial.js +8 -6
  22. package/src/cli/commands/group.js +249 -249
  23. package/src/cli/commands/model/picks.js +50 -50
  24. package/src/cli/commands/provider/bench-via.js +247 -247
  25. package/src/cli/commands/provider/bench.js +141 -141
  26. package/src/cli/commands/provider/index.js +137 -136
  27. package/src/cli/commands/provider/models.js +145 -145
  28. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  29. package/src/cli/commands/provider/raccoon-login.js +29 -1
  30. package/src/cli/commands/provider/raccoon-password.js +162 -0
  31. package/src/cli/commands/sync.js +310 -310
  32. package/src/cli/status.js +279 -279
  33. package/src/daemon.js +232 -232
  34. package/src/model-capabilities/enrich.js +86 -86
  35. package/src/model-capabilities/index.js +183 -183
  36. package/src/model-capabilities/parse.js +70 -70
  37. package/src/models.js +225 -225
  38. package/src/providers/AGENTS.md +49 -0
  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 +182 -182
  43. package/src/providers/globalqwenwork/cosy.js +123 -123
  44. package/src/providers/globalqwenwork/crypto.js +218 -218
  45. package/src/providers/globalqwenwork/http.js +20 -20
  46. package/src/providers/globalqwenwork/rsa.js +54 -54
  47. package/src/providers/globalqwenwork/sse.js +268 -268
  48. package/src/providers/globalqwenwork/stream.js +130 -130
  49. package/src/providers/globalqwenwork/upstream.js +122 -122
  50. package/src/providers/keyring.js +60 -60
  51. package/src/providers/qoder/chat.js +203 -203
  52. package/src/providers/qoder/index.js +249 -249
  53. package/src/providers/qoder/sse.js +145 -145
  54. package/src/providers/qwenwork/account-store.js +133 -133
  55. package/src/providers/qwenwork/constants.js +67 -67
  56. package/src/providers/qwenwork/cosy.js +120 -120
  57. package/src/providers/qwenwork/crypto.js +218 -218
  58. package/src/providers/qwenwork/http.js +20 -20
  59. package/src/providers/qwenwork/index.js +337 -337
  60. package/src/providers/qwenwork/payload.js +144 -144
  61. package/src/providers/qwenwork/rsa.js +54 -54
  62. package/src/providers/qwenwork/sse.js +268 -268
  63. package/src/providers/qwenwork/stream.js +130 -130
  64. package/src/providers/qwenwork/upstream.js +120 -120
  65. package/src/providers/qwenwork.js +1 -1
  66. package/src/providers/raccoon/const.js +25 -0
  67. package/src/providers/raccoon/password-login.js +87 -0
  68. package/src/providers/registry.js +79 -79
  69. package/src/providers/workbuddy/chat.js +264 -264
  70. package/src/providers/workbuddy/reshape.js +152 -152
  71. package/src/providers/workbuddy.js +2 -2
  72. package/src/reasoning.js +32 -32
  73. package/src/routes/AGENTS.md +37 -0
  74. package/src/routes/chat/gateway.js +46 -46
  75. package/src/routes/chat/relay-pipeline.js +282 -282
  76. package/src/routes/chat/via-route-handler.js +146 -146
  77. package/src/routes/hedge.js +260 -260
  78. package/src/routes/models-route.js +167 -167
  79. package/src/routes/peers.js +335 -335
  80. package/src/routes/stream.js +347 -347
  81. package/src/runtime/bootstrap.js +67 -67
  82. package/src/runtime/provider-gate.js +36 -36
  83. package/src/runtime/providers-setup.js +168 -168
  84. package/src/server.js +78 -78
  85. package/src/state/schemas/allowlist.js +92 -92
  86. package/src/sync-opencode.js +280 -280
  87. package/src/transport/index.js +244 -244
  88. package/src/transport/pool.js +56 -56
  89. package/src/transport/retry.js +24 -24
  90. package/src/transport/sse.js +93 -93
  91. package/src/upstream-probe/display.js +52 -52
  92. package/src/upstream-probe/probe.js +49 -49
  93. package/src/upstream-probe/rotate.js +110 -110
  94. package/src/upstream-probe/start.js +45 -45
  95. package/src/upstream.js +293 -293
@@ -1,145 +1,145 @@
1
- import { join } from "node:path";
2
- import { defaultStateFile } from "../../../state.js";
3
- import { logDir } from "../../../logs.js";
4
- import { normalizeProviderId } from "../../../providers/model-id.js";
5
-
6
- export async function handleProviderModels(id, sub, args, rest) {
7
- if (!(sub === "models" || sub === "show-models" || sub === "list-models" || sub === "ls")) return false;
8
- // 供应商 id 归一(clinebot/cline-bot → cline):老脚本 `-provider clinebot models` 仍走 cline 直查
9
- id = normalizeProviderId(id);
10
- const { loadProviderConfig, loadProviderKeys, isModelAllowed } = await import("../../../state.js");
11
- const wantsJson = args.includes("--json") || args.includes("-json");
12
- const cfg = loadProviderConfig(id);
13
- if (id === "opencode" || id === "oc") {
14
- try {
15
- const cacheFile = join(logDir(), "models.json");
16
- const { readFileSync } = await import("node:fs");
17
- const raw = JSON.parse(readFileSync(cacheFile, "utf8"));
18
- const ids = (raw.data || []).map((m) => m.id).filter((x) => !String(x).includes("/"));
19
- if (wantsJson) {
20
- console.log(JSON.stringify({ object: "list", data: ids.map((id) => ({ id, object: "model" })) }, null, 2));
21
- } else {
22
- console.log(`opencode models (${ids.length}):`);
23
- for (const mid of ids) console.log(` ${mid}`);
24
- }
25
- } catch (e) {
26
- console.error(`could not read models cache: ${String(e?.message || e)}`);
27
- process.exit(1);
28
- }
29
- process.exit(0);
30
- }
31
- try {
32
- const { createGenericProvider } = await import("../../../providers/generic.js");
33
- const { createWorkbuddyProvider } = await import("../../../providers/workbuddy.js");
34
- const baseUrl = cfg?.baseUrl || (id === "workbuddy" ? "https://copilot.tencent.com" : "");
35
- const keys = loadProviderKeys(id);
36
- const auths = cfg?.auths || [];
37
- let provider;
38
- if (id === "workbuddy") {
39
- provider = createWorkbuddyProvider({ baseUrl, apiKeys: keys, auths, file: defaultStateFile() });
40
- } else if (id === "cline") {
41
- // 与聚合目录同源:recommended-models 的 free(generic 的 /api/v1/models 是全量目录,含大量不可用条目)
42
- const { createClineProvider } = await import("../../../providers/cline.js");
43
- provider = createClineProvider({ id, baseUrl: baseUrl || undefined, apiKeys: keys, file: defaultStateFile() });
44
- } else if (id === "codearts") {
45
- const { createCodeartsProvider } = await import("../../../providers/codearts.js");
46
- provider = createCodeartsProvider({ baseUrl: baseUrl || undefined, apiKeys: keys, file: defaultStateFile() });
47
- } else if (id === "traework") {
48
- const { createTraeworkProvider } = await import("../../../providers/traework.js");
49
- provider = createTraeworkProvider({ baseUrl: baseUrl || undefined, apiKeys: keys, auths, file: defaultStateFile() });
50
- } else if (id === "qoder") {
51
- const { createQoderProvider } = await import("../../../providers/qoder/index.js");
52
- provider = createQoderProvider({ id, apiKeys: keys, file: defaultStateFile() });
53
- } else if (id === "qwenwork") {
54
- const { createQwenworkProvider } = await import("../../../providers/qwenwork/index.js");
55
- provider = createQwenworkProvider({ id, apiKeys: keys, file: defaultStateFile() });
56
- } else if (id === "globalqwenwork") {
57
- const { createGlobalQwenworkProvider } = await import("../../../providers/globalqwenwork/index.js");
58
- provider = createGlobalQwenworkProvider({ id, apiKeys: keys, file: defaultStateFile() });
59
- } else if (id === "zcode") {
60
- const { createZcodeProvider } = await import("../../../providers/zcode/index.js");
61
- provider = createZcodeProvider({ id, apiKeys: keys, file: defaultStateFile() });
62
- } else if (id === "raccoon") {
63
- const { createRaccoonProvider } = await import("../../../providers/raccoon/index.js");
64
- provider = createRaccoonProvider({ id, apiKeys: keys, file: defaultStateFile() });
65
- } else {
66
- if (!baseUrl) {
67
- console.error(`provider ${id}: missing baseUrl — set via: mslxdff -provider ${id} set-url <baseUrl>`);
68
- process.exit(1);
69
- }
70
- provider = createGenericProvider({ id, baseUrl, apiKeys: keys, file: defaultStateFile() });
71
- }
72
- const all = await provider.listModels();
73
- const markAllowed = (mid) => {
74
- const raw = String(mid || "").includes("/") ? String(mid).split("/").slice(1).join("/") : String(mid);
75
- const checkRaw = mid.startsWith(`${id}/`) ? mid.slice(id.length + 1) : raw;
76
- return isModelAllowed(id, checkRaw);
77
- };
78
- if (wantsJson) {
79
- const filtered = all.filter((m) => markAllowed(m.id));
80
- console.log(JSON.stringify({ object: "list", data: filtered }, null, 2));
81
- } else {
82
- const allowedCount = all.filter((m) => markAllowed(m.id)).length;
83
- console.log(`${id} models (${all.length} total, ${allowedCount} ✓ allowed${allowedCount !== all.length ? `, ${all.length - allowedCount} x blocked by allowlist` : ""}):`);
84
- const fmtPrice = (m) => {
85
- const c = String(m.credits || "").trim();
86
- if (c) {
87
- const m0 = c.match(/x\s*([\d.]+)/i);
88
- if (m0) return `x${m0[1]}`;
89
- return c.replace(/\s*credits\s*/gi, "").trim().replace(/\s+/g, " ");
90
- }
91
- if (m.pricing && typeof m.pricing === "object") {
92
- const p = m.pricing.prompt ?? m.pricing.input ?? m.pricing.completion ?? "";
93
- if (p) return String(p);
94
- }
95
- if (m.price_factor != null && String(m.price_factor).trim() !== "") return `x${m.price_factor}`;
96
- if (m.price != null && String(m.price).trim()) return String(m.price).trim();
97
- if (String(m.id).endsWith("/auto")) return "浮动";
98
- return "—";
99
- };
100
- const fmtBadge = (m) => {
101
- const tags = Array.isArray(m.tags) ? m.tags : [];
102
- const b = tags.find((t) => String(t).includes("限时免费") || String(t).toLowerCase().includes("free"));
103
- if (!b) return "";
104
- const part = String(b).split(":")[1];
105
- return part ? ` [${part}]` : ` [${b}]`;
106
- };
107
- // 能力列:上下文 k + 📷识图 🧠推理 🔧工具调用(workbuddy 原生字段;其他供应商无这些字段则显示 —)
108
- const fmtCaps = (m) => {
109
- const ctx = Number(m.maxInputTokens ?? m.context_length) || null;
110
- const reasoning = m.supportsReasoning ?? m.is_reasoning;
111
- const hasAny = ctx || m.supportsImages != null || reasoning != null || m.supportsToolCall != null;
112
- if (!hasAny) return "—";
113
- const parts = [];
114
- if (ctx) parts.push(ctx >= 1_000_000 ? `${Math.round(ctx / 100000) / 10}M` : `${Math.round(ctx / 1000)}k`);
115
- if (m.supportsImages && !m.disabledMultimodal) parts.push("📷");
116
- if (reasoning) parts.push("🧠");
117
- if (m.supportsToolCall) parts.push("🔧");
118
- return parts.join(" ");
119
- };
120
- const idW = Math.max(22, ...all.map((m) => String(m.id).length)) + 2;
121
- const priceW = Math.max(6, ...all.map((m) => fmtPrice(m).length)) + 2;
122
- const capsW = Math.max(4, ...all.map((m) => fmtCaps(m).length)) + 2;
123
- for (const m of all) {
124
- const ok = markAllowed(m.id);
125
- const price = fmtPrice(m);
126
- const badge = fmtBadge(m);
127
- const name = m.name ? ` ${m.name}` : "";
128
- const blocked = ok ? "" : " [blocked — allowlist]";
129
- const line = ` ${ok ? "✓" : "x"} ${String(m.id).padEnd(idW)}${String(price).padEnd(priceW)}${fmtCaps(m).padEnd(capsW)}${name}${badge}${blocked}`;
130
- console.log(line);
131
- }
132
- if (!all.length) console.log(` (no models — check baseUrl/keys or try: curl ${baseUrl}/models)`);
133
- else if (allowedCount === 0) console.log(` tip: all blocked — mslxdff -provider ${id} allowAny on 或 allowlist set <model...>`);
134
- else if (allowedCount !== all.length) console.log(` tip: blocked 仅影响 /v1/chat 调用,展示已全量列出`);
135
- if (id === "zcode") {
136
- console.log(` tip: 目录/额度/模型调用全通(上游 v3.14.4 起模型请求免验证码,docs/adr/0038);额度耗尽返回 429 quota_exhausted,按日窗口重置`);
137
- }
138
- }
139
- try { await provider.close?.(); } catch {}
140
- } catch (e) {
141
- console.error(`could not list ${id} models: ${String(e?.message || e)}`);
142
- process.exit(1);
143
- }
144
- process.exit(0);
145
- }
1
+ import { join } from "node:path";
2
+ import { defaultStateFile } from "../../../state.js";
3
+ import { logDir } from "../../../logs.js";
4
+ import { normalizeProviderId } from "../../../providers/model-id.js";
5
+
6
+ export async function handleProviderModels(id, sub, args, rest) {
7
+ if (!(sub === "models" || sub === "show-models" || sub === "list-models" || sub === "ls")) return false;
8
+ // 供应商 id 归一(clinebot/cline-bot → cline):老脚本 `-provider clinebot models` 仍走 cline 直查
9
+ id = normalizeProviderId(id);
10
+ const { loadProviderConfig, loadProviderKeys, isModelAllowed } = await import("../../../state.js");
11
+ const wantsJson = args.includes("--json") || args.includes("-json");
12
+ const cfg = loadProviderConfig(id);
13
+ if (id === "opencode" || id === "oc") {
14
+ try {
15
+ const cacheFile = join(logDir(), "models.json");
16
+ const { readFileSync } = await import("node:fs");
17
+ const raw = JSON.parse(readFileSync(cacheFile, "utf8"));
18
+ const ids = (raw.data || []).map((m) => m.id).filter((x) => !String(x).includes("/"));
19
+ if (wantsJson) {
20
+ console.log(JSON.stringify({ object: "list", data: ids.map((id) => ({ id, object: "model" })) }, null, 2));
21
+ } else {
22
+ console.log(`opencode models (${ids.length}):`);
23
+ for (const mid of ids) console.log(` ${mid}`);
24
+ }
25
+ } catch (e) {
26
+ console.error(`could not read models cache: ${String(e?.message || e)}`);
27
+ process.exit(1);
28
+ }
29
+ process.exit(0);
30
+ }
31
+ try {
32
+ const { createGenericProvider } = await import("../../../providers/generic.js");
33
+ const { createWorkbuddyProvider } = await import("../../../providers/workbuddy.js");
34
+ const baseUrl = cfg?.baseUrl || (id === "workbuddy" ? "https://copilot.tencent.com" : "");
35
+ const keys = loadProviderKeys(id);
36
+ const auths = cfg?.auths || [];
37
+ let provider;
38
+ if (id === "workbuddy") {
39
+ provider = createWorkbuddyProvider({ baseUrl, apiKeys: keys, auths, file: defaultStateFile() });
40
+ } else if (id === "cline") {
41
+ // 与聚合目录同源:recommended-models 的 free(generic 的 /api/v1/models 是全量目录,含大量不可用条目)
42
+ const { createClineProvider } = await import("../../../providers/cline.js");
43
+ provider = createClineProvider({ id, baseUrl: baseUrl || undefined, apiKeys: keys, file: defaultStateFile() });
44
+ } else if (id === "codearts") {
45
+ const { createCodeartsProvider } = await import("../../../providers/codearts.js");
46
+ provider = createCodeartsProvider({ baseUrl: baseUrl || undefined, apiKeys: keys, file: defaultStateFile() });
47
+ } else if (id === "traework") {
48
+ const { createTraeworkProvider } = await import("../../../providers/traework.js");
49
+ provider = createTraeworkProvider({ baseUrl: baseUrl || undefined, apiKeys: keys, auths, file: defaultStateFile() });
50
+ } else if (id === "qoder") {
51
+ const { createQoderProvider } = await import("../../../providers/qoder/index.js");
52
+ provider = createQoderProvider({ id, apiKeys: keys, file: defaultStateFile() });
53
+ } else if (id === "qwenwork") {
54
+ const { createQwenworkProvider } = await import("../../../providers/qwenwork/index.js");
55
+ provider = createQwenworkProvider({ id, apiKeys: keys, file: defaultStateFile() });
56
+ } else if (id === "globalqwenwork") {
57
+ const { createGlobalQwenworkProvider } = await import("../../../providers/globalqwenwork/index.js");
58
+ provider = createGlobalQwenworkProvider({ id, apiKeys: keys, file: defaultStateFile() });
59
+ } else if (id === "zcode") {
60
+ const { createZcodeProvider } = await import("../../../providers/zcode/index.js");
61
+ provider = createZcodeProvider({ id, apiKeys: keys, file: defaultStateFile() });
62
+ } else if (id === "raccoon") {
63
+ const { createRaccoonProvider } = await import("../../../providers/raccoon/index.js");
64
+ provider = createRaccoonProvider({ id, apiKeys: keys, file: defaultStateFile() });
65
+ } else {
66
+ if (!baseUrl) {
67
+ console.error(`provider ${id}: missing baseUrl — set via: mslxdff -provider ${id} set-url <baseUrl>`);
68
+ process.exit(1);
69
+ }
70
+ provider = createGenericProvider({ id, baseUrl, apiKeys: keys, file: defaultStateFile() });
71
+ }
72
+ const all = await provider.listModels();
73
+ const markAllowed = (mid) => {
74
+ const raw = String(mid || "").includes("/") ? String(mid).split("/").slice(1).join("/") : String(mid);
75
+ const checkRaw = mid.startsWith(`${id}/`) ? mid.slice(id.length + 1) : raw;
76
+ return isModelAllowed(id, checkRaw);
77
+ };
78
+ if (wantsJson) {
79
+ const filtered = all.filter((m) => markAllowed(m.id));
80
+ console.log(JSON.stringify({ object: "list", data: filtered }, null, 2));
81
+ } else {
82
+ const allowedCount = all.filter((m) => markAllowed(m.id)).length;
83
+ console.log(`${id} models (${all.length} total, ${allowedCount} ✓ allowed${allowedCount !== all.length ? `, ${all.length - allowedCount} x blocked by allowlist` : ""}):`);
84
+ const fmtPrice = (m) => {
85
+ const c = String(m.credits || "").trim();
86
+ if (c) {
87
+ const m0 = c.match(/x\s*([\d.]+)/i);
88
+ if (m0) return `x${m0[1]}`;
89
+ return c.replace(/\s*credits\s*/gi, "").trim().replace(/\s+/g, " ");
90
+ }
91
+ if (m.pricing && typeof m.pricing === "object") {
92
+ const p = m.pricing.prompt ?? m.pricing.input ?? m.pricing.completion ?? "";
93
+ if (p) return String(p);
94
+ }
95
+ if (m.price_factor != null && String(m.price_factor).trim() !== "") return `x${m.price_factor}`;
96
+ if (m.price != null && String(m.price).trim()) return String(m.price).trim();
97
+ if (String(m.id).endsWith("/auto")) return "浮动";
98
+ return "—";
99
+ };
100
+ const fmtBadge = (m) => {
101
+ const tags = Array.isArray(m.tags) ? m.tags : [];
102
+ const b = tags.find((t) => String(t).includes("限时免费") || String(t).toLowerCase().includes("free"));
103
+ if (!b) return "";
104
+ const part = String(b).split(":")[1];
105
+ return part ? ` [${part}]` : ` [${b}]`;
106
+ };
107
+ // 能力列:上下文 k + 📷识图 🧠推理 🔧工具调用(workbuddy 原生字段;其他供应商无这些字段则显示 —)
108
+ const fmtCaps = (m) => {
109
+ const ctx = Number(m.maxInputTokens ?? m.context_length) || null;
110
+ const reasoning = m.supportsReasoning ?? m.is_reasoning;
111
+ const hasAny = ctx || m.supportsImages != null || reasoning != null || m.supportsToolCall != null;
112
+ if (!hasAny) return "—";
113
+ const parts = [];
114
+ if (ctx) parts.push(ctx >= 1_000_000 ? `${Math.round(ctx / 100000) / 10}M` : `${Math.round(ctx / 1000)}k`);
115
+ if (m.supportsImages && !m.disabledMultimodal) parts.push("📷");
116
+ if (reasoning) parts.push("🧠");
117
+ if (m.supportsToolCall) parts.push("🔧");
118
+ return parts.join(" ");
119
+ };
120
+ const idW = Math.max(22, ...all.map((m) => String(m.id).length)) + 2;
121
+ const priceW = Math.max(6, ...all.map((m) => fmtPrice(m).length)) + 2;
122
+ const capsW = Math.max(4, ...all.map((m) => fmtCaps(m).length)) + 2;
123
+ for (const m of all) {
124
+ const ok = markAllowed(m.id);
125
+ const price = fmtPrice(m);
126
+ const badge = fmtBadge(m);
127
+ const name = m.name ? ` ${m.name}` : "";
128
+ const blocked = ok ? "" : " [blocked — allowlist]";
129
+ const line = ` ${ok ? "✓" : "x"} ${String(m.id).padEnd(idW)}${String(price).padEnd(priceW)}${fmtCaps(m).padEnd(capsW)}${name}${badge}${blocked}`;
130
+ console.log(line);
131
+ }
132
+ if (!all.length) console.log(` (no models — check baseUrl/keys or try: curl ${baseUrl}/models)`);
133
+ else if (allowedCount === 0) console.log(` tip: all blocked — mslxdff -provider ${id} allowAny on 或 allowlist set <model...>`);
134
+ else if (allowedCount !== all.length) console.log(` tip: blocked 仅影响 /v1/chat 调用,展示已全量列出`);
135
+ if (id === "zcode") {
136
+ console.log(` tip: 目录/额度/模型调用全通(上游 v3.14.4 起模型请求免验证码,docs/adr/0038);额度耗尽返回 429 quota_exhausted,按日窗口重置`);
137
+ }
138
+ }
139
+ try { await provider.close?.(); } catch {}
140
+ } catch (e) {
141
+ console.error(`could not list ${id} models: ${String(e?.message || e)}`);
142
+ process.exit(1);
143
+ }
144
+ process.exit(0);
145
+ }
@@ -1,119 +1,119 @@
1
- // mslxdff -provider qwenwork login — 千问办公设备授权(PKCE + poll),对齐 qoder-login。
2
- // 授权后落盘 auths/qwenwork-<uid>.json + state 双写,默认 allowAny=false 并种 3 个实测模型(ADR-0037)。
3
- import { compatFetch } from "../../../compat.js";
4
- import { normalizeProviderId } from "../../../providers/model-id.js";
5
- import { newDeviceFlow, pollGrant, userInfo, accountContext, expiryUnix } from "../../../providers/qwenwork/upstream.js";
6
-
7
- const POLL_MS = 3000;
8
- const POLL_TIMEOUT_MS = 10 * 60 * 1000;
9
-
10
- export async function handleQwenworkLogin(id, sub, rest = [], deps = {}) {
11
- if (normalizeProviderId(id) !== "qwenwork") return false;
12
- if (sub !== "login" && sub !== "auth" && sub !== "oauth") return false;
13
- const fetchImpl = deps.fetchImpl || compatFetch;
14
- const log = deps.log || ((m) => console.log(m));
15
-
16
- const flow = await newDeviceFlow();
17
- log("=".repeat(60));
18
- log(" 千问办公(QwenWork)登录");
19
- log("=".repeat(60));
20
- log("");
21
- log(" 1. 在浏览器打开下面链接,用已登录千问办公的账号确认授权");
22
- log(" 2. 授权成功后会自动完成,不要关闭本终端");
23
- log("");
24
- log(` ${flow.url}`);
25
- log("");
26
- log("🔄 等待你授权(自动轮询,每 3 秒,最多 10 分钟)...");
27
-
28
- const deadline = Date.now() + POLL_TIMEOUT_MS;
29
- let grant = null;
30
- for (;;) {
31
- if (Date.now() > deadline) {
32
- console.error("\n❌ 授权超时(10 分钟),请重新运行 mslxdff -provider qwenwork login");
33
- process.exit(1);
34
- }
35
- let outcome;
36
- try {
37
- outcome = await pollGrant(flow.nonce, flow.verifier, fetchImpl);
38
- } catch (e) {
39
- console.error(`\n❌ 轮询失败: ${e.message}`);
40
- process.exit(1);
41
- }
42
- if (!outcome.pending) { grant = outcome.grant; break; }
43
- try { process.stdout.write("."); } catch {}
44
- await new Promise((r) => setTimeout(r, POLL_MS));
45
- }
46
- const accessToken = grant.token || grant.device_token;
47
- const refreshToken = grant.refresh_token || "";
48
- if (!accessToken) {
49
- console.error("\n❌ 未获取到 token");
50
- process.exit(1);
51
- }
52
- log(`\n✅ 授权成功!token=${String(accessToken).slice(0, 12)}...`);
53
- if (refreshToken) log(` refreshToken=${String(refreshToken).slice(0, 12)}...`);
54
-
55
- let uid = String(grant.user_id || "");
56
- let name = String(grant.user_name || "");
57
- let email = "";
58
- try {
59
- const info = await userInfo(accessToken, fetchImpl);
60
- if (info?.id) uid = String(info.id);
61
- if (info?.name) name = String(info.name);
62
- if (info?.email) email = String(info.email);
63
- log(` UID: ${uid} 昵称: ${name || "(空)"} 邮箱: ${email || "(空)"}`);
64
- } catch (e) {
65
- log(` ⚠️ userinfo 失败: ${e.message}(用 grant 兜底)`);
66
- if (!uid) uid = String(accessToken).slice(0, 8);
67
- }
68
- if (!uid) {
69
- console.error("❌ 无法获取用户标识");
70
- process.exit(1);
71
- }
72
-
73
- const { saveQwenworkAccount } = await import("../../../providers/qwenwork/account-store.js");
74
- const save = deps.saveAccount || saveQwenworkAccount;
75
- const saved = await save({
76
- uid,
77
- accessToken,
78
- refreshToken,
79
- expiresAt: expiryUnix(grant),
80
- name,
81
- email,
82
- });
83
-
84
- log("");
85
- log("=".repeat(60));
86
- log("✅ 登录完成!");
87
- log(` UID: ${uid}`);
88
- log(` 账号数: ${saved.accounts}${saved.updated ? "(旧号凭证已更新)" : ""}`);
89
- log(` 数据文件: ${saved.file}`);
90
- log("=".repeat(60));
91
- // 安全默认:allowAny 关 + 只放 3 个实测模型(与 qoder 惯例故意不同,见 ADR-0037)
92
- try {
93
- const { saveProviderAllowAnyModels, saveProviderAllowedModels } = await import("../../../state.js");
94
- saveProviderAllowAnyModels("qwenwork", false);
95
- saveProviderAllowedModels("qwenwork", ["flash", "pro", "qwen3.8-max-preview"]);
96
- log(" allowAnyModels=off + allowlist 已种 flash/pro/qwen3.8-max-preview(额度池供应商,防 auto 烧分)");
97
- } catch {}
98
- // 额度展示
99
- try {
100
- const ctx = await accountContext(accessToken, fetchImpl);
101
- const plan = ctx?.plan || {};
102
- const quota = ctx?.quota || {};
103
- if (plan?.name || quota?.remaining !== undefined) {
104
- log(` 套餐: ${plan?.name || "-"} 积分剩余: ${quota?.remaining ?? "-"}`);
105
- }
106
- } catch {
107
- log(" 额度查询失败,不影响使用");
108
- }
109
-
110
- log("下一步:");
111
- log(" mslxdff -provider qwenwork models 列模型列表");
112
- log(" mslxdff -restart(用户终端跑) 重启网关使新账号生效");
113
- log(" 然后 curl http://localhost:8989/v1/chat/completions -d '{\"model\":\"qwenwork/flash\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'");
114
- log("");
115
- log("多账号:重复 `mslxdff -provider qwenwork login` 追加,每号独立 uid,对话自动轮换");
116
-
117
- if (!deps.noExit) process.exit(0);
118
- return true;
119
- }
1
+ // mslxdff -provider qwenwork login — 千问办公设备授权(PKCE + poll),对齐 qoder-login。
2
+ // 授权后落盘 auths/qwenwork-<uid>.json + state 双写,默认 allowAny=false 并种 3 个实测模型(ADR-0037)。
3
+ import { compatFetch } from "../../../compat.js";
4
+ import { normalizeProviderId } from "../../../providers/model-id.js";
5
+ import { newDeviceFlow, pollGrant, userInfo, accountContext, expiryUnix } from "../../../providers/qwenwork/upstream.js";
6
+
7
+ const POLL_MS = 3000;
8
+ const POLL_TIMEOUT_MS = 10 * 60 * 1000;
9
+
10
+ export async function handleQwenworkLogin(id, sub, rest = [], deps = {}) {
11
+ if (normalizeProviderId(id) !== "qwenwork") return false;
12
+ if (sub !== "login" && sub !== "auth" && sub !== "oauth") return false;
13
+ const fetchImpl = deps.fetchImpl || compatFetch;
14
+ const log = deps.log || ((m) => console.log(m));
15
+
16
+ const flow = await newDeviceFlow();
17
+ log("=".repeat(60));
18
+ log(" 千问办公(QwenWork)登录");
19
+ log("=".repeat(60));
20
+ log("");
21
+ log(" 1. 在浏览器打开下面链接,用已登录千问办公的账号确认授权");
22
+ log(" 2. 授权成功后会自动完成,不要关闭本终端");
23
+ log("");
24
+ log(` ${flow.url}`);
25
+ log("");
26
+ log("🔄 等待你授权(自动轮询,每 3 秒,最多 10 分钟)...");
27
+
28
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
29
+ let grant = null;
30
+ for (;;) {
31
+ if (Date.now() > deadline) {
32
+ console.error("\n❌ 授权超时(10 分钟),请重新运行 mslxdff -provider qwenwork login");
33
+ process.exit(1);
34
+ }
35
+ let outcome;
36
+ try {
37
+ outcome = await pollGrant(flow.nonce, flow.verifier, fetchImpl);
38
+ } catch (e) {
39
+ console.error(`\n❌ 轮询失败: ${e.message}`);
40
+ process.exit(1);
41
+ }
42
+ if (!outcome.pending) { grant = outcome.grant; break; }
43
+ try { process.stdout.write("."); } catch {}
44
+ await new Promise((r) => setTimeout(r, POLL_MS));
45
+ }
46
+ const accessToken = grant.token || grant.device_token;
47
+ const refreshToken = grant.refresh_token || "";
48
+ if (!accessToken) {
49
+ console.error("\n❌ 未获取到 token");
50
+ process.exit(1);
51
+ }
52
+ log(`\n✅ 授权成功!token=${String(accessToken).slice(0, 12)}...`);
53
+ if (refreshToken) log(` refreshToken=${String(refreshToken).slice(0, 12)}...`);
54
+
55
+ let uid = String(grant.user_id || "");
56
+ let name = String(grant.user_name || "");
57
+ let email = "";
58
+ try {
59
+ const info = await userInfo(accessToken, fetchImpl);
60
+ if (info?.id) uid = String(info.id);
61
+ if (info?.name) name = String(info.name);
62
+ if (info?.email) email = String(info.email);
63
+ log(` UID: ${uid} 昵称: ${name || "(空)"} 邮箱: ${email || "(空)"}`);
64
+ } catch (e) {
65
+ log(` ⚠️ userinfo 失败: ${e.message}(用 grant 兜底)`);
66
+ if (!uid) uid = String(accessToken).slice(0, 8);
67
+ }
68
+ if (!uid) {
69
+ console.error("❌ 无法获取用户标识");
70
+ process.exit(1);
71
+ }
72
+
73
+ const { saveQwenworkAccount } = await import("../../../providers/qwenwork/account-store.js");
74
+ const save = deps.saveAccount || saveQwenworkAccount;
75
+ const saved = await save({
76
+ uid,
77
+ accessToken,
78
+ refreshToken,
79
+ expiresAt: expiryUnix(grant),
80
+ name,
81
+ email,
82
+ });
83
+
84
+ log("");
85
+ log("=".repeat(60));
86
+ log("✅ 登录完成!");
87
+ log(` UID: ${uid}`);
88
+ log(` 账号数: ${saved.accounts}${saved.updated ? "(旧号凭证已更新)" : ""}`);
89
+ log(` 数据文件: ${saved.file}`);
90
+ log("=".repeat(60));
91
+ // 安全默认:allowAny 关 + 只放 3 个实测模型(与 qoder 惯例故意不同,见 ADR-0037)
92
+ try {
93
+ const { saveProviderAllowAnyModels, saveProviderAllowedModels } = await import("../../../state.js");
94
+ saveProviderAllowAnyModels("qwenwork", false);
95
+ saveProviderAllowedModels("qwenwork", ["flash", "pro", "qwen3.8-max-preview"]);
96
+ log(" allowAnyModels=off + allowlist 已种 flash/pro/qwen3.8-max-preview(额度池供应商,防 auto 烧分)");
97
+ } catch {}
98
+ // 额度展示
99
+ try {
100
+ const ctx = await accountContext(accessToken, fetchImpl);
101
+ const plan = ctx?.plan || {};
102
+ const quota = ctx?.quota || {};
103
+ if (plan?.name || quota?.remaining !== undefined) {
104
+ log(` 套餐: ${plan?.name || "-"} 积分剩余: ${quota?.remaining ?? "-"}`);
105
+ }
106
+ } catch {
107
+ log(" 额度查询失败,不影响使用");
108
+ }
109
+
110
+ log("下一步:");
111
+ log(" mslxdff -provider qwenwork models 列模型列表");
112
+ log(" mslxdff -restart(用户终端跑) 重启网关使新账号生效");
113
+ log(" 然后 curl http://localhost:8989/v1/chat/completions -d '{\"model\":\"qwenwork/flash\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'");
114
+ log("");
115
+ log("多账号:重复 `mslxdff -provider qwenwork login` 追加,每号独立 uid,对话自动轮换");
116
+
117
+ if (!deps.noExit) process.exit(0);
118
+ return true;
119
+ }
@@ -27,6 +27,15 @@ const RELOGIN_HINT = " (可重试:mslxdff -provider raccoon login)";
27
27
  export async function handleRaccoonLogin(id, sub, rest = [], deps = {}) {
28
28
  const pid = normalizeProviderId(id) || String(id || "").toLowerCase();
29
29
  if (pid !== "raccoon") return false;
30
+ // `login --password` 走手机号+密码(多账号免扫码);无该旗标仍是扫码。分派必须在
31
+ // LOGIN_SUBS 判定之前,且只在此处转交 —— 扫码路径本身零改动。
32
+ if (LOGIN_SUBS.has(sub) && rest.some((x) => {
33
+ const s = String(x);
34
+ return s === "--password" || s.startsWith("--password=") || s === "-p" || s === "--phone" || s.startsWith("--phone=");
35
+ })) {
36
+ const { handleRaccoonPasswordLogin } = await import("./raccoon-password.js");
37
+ return handleRaccoonPasswordLogin(id, sub, rest, deps);
38
+ }
30
39
  if (!LOGIN_SUBS.has(sub)) return false;
31
40
 
32
41
  const fetchImpl = deps.fetchImpl || compatFetch;
@@ -93,7 +102,25 @@ export async function handleRaccoonLogin(id, sub, rest = [], deps = {}) {
93
102
  return true;
94
103
  }
95
104
 
96
- const credential = out.credential;
105
+ return await finishRaccoonLogin({ credential: out.credential, fetchImpl, deps, log });
106
+ }
107
+
108
+ /**
109
+ * 登录成功后的收尾四步(**扫码与密码两条路共用,单一源**):
110
+ * confirm 会话 → 取用户信息 → 落盘 + 补 allowlist → 领一次性登录奖励 → 打印回执。
111
+ *
112
+ * 抽出来的理由不只是省行数:这四步的顺序有硬约束(必须先 confirm 才能用 token;领奖必须在
113
+ * access_token 刚签发的窗口内),任何一条路径自己重写一遍都会漏掉其中一步且难以察觉。
114
+ *
115
+ * @param {object} a
116
+ * @param {object} a.credential 已签发的凭据(两种登录方式同形)
117
+ * @param {Function} a.fetchImpl
118
+ * @param {object} a.deps 与原 CLI 依赖注入同形(dir/file/deviceId/saveAccount/claimReward/skipReward/env)
119
+ * @param {Function} a.log
120
+ * @param {string} [a.methodNote] 登录方式说明行(如密码登录标注手机号),纯展示
121
+ * @returns {Promise<boolean>} 恒 true(表示「已处理」,交还 CLI 派发链)
122
+ */
123
+ export async function finishRaccoonLogin({ credential, fetchImpl, deps = {}, log, methodNote = "" }) {
97
124
  // 新登录的会话是「未确认」状态:不先 POST /iam/confirm,此后所有业务端点(含下面的领奖励、
98
125
  // 以及 daemon 的 chat)都会撞 `400 code 200901 iam_confirm_required`。登录时当场确认一次,
99
126
  // 让落盘的凭据从一开始就是可用的;确认失败不阻断(chat.js 有懒重试兜底),只在末尾如实提示。
@@ -134,6 +161,7 @@ export async function handleRaccoonLogin(id, sub, rest = [], deps = {}) {
134
161
 
135
162
  log("");
136
163
  log(`✅ 登录成功!uid=${uid} 昵称=${info.name || "(空)"} token=${raccoonTokenFingerprint(credential.access_token)}…`);
164
+ if (methodNote) log(methodNote);
137
165
  log(` 凭据: ${saved.file}`);
138
166
  if (!confirmed) log(" ⚠ 会话确认(iam/confirm)暂未成功 —— 不影响使用(调用时会自动重试确认),若持续报错请重跑 login");
139
167
  if (allow.added.length) log(` allowlist 已补齐 ${allow.added.length} 个模型:${allow.added.join(", ")}`);