mslxdff 0.1.203 → 0.1.207
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/mslxdff.js +1 -1
- package/docs/cli_help_mini.md +146 -0
- package/package.json +38 -38
- package/plugins/README.md +14 -14
- package/plugins/prefer-model.mjs.example +14 -14
- package/src/anthropic/canonical-json.js +55 -0
- package/src/anthropic/count-tokens.js +61 -61
- package/src/anthropic/request-body.js +183 -126
- package/src/anthropic/request-shape.js +60 -0
- package/src/anthropic/session-id.js +58 -0
- package/src/anthropic/shared.js +18 -18
- package/src/anthropic/sse-aggregate.js +56 -56
- package/src/anthropic/tool-media.js +75 -0
- package/src/anthropic/translate.js +296 -287
- package/src/anthropic/web-search.js +361 -361
- package/src/api-kind.js +31 -31
- package/src/autostart.js +288 -288
- package/src/bench/probe.js +1 -1
- package/src/bench/runner.js +1 -1
- package/src/bench/via-probe.js +1 -1
- package/src/bench/via.js +144 -144
- package/src/bench/workbuddy-bench.js +1 -1
- package/src/chat/config.js +11 -11
- package/src/chat/cooling.js +81 -81
- package/src/chat/direct.js +58 -58
- package/src/chat/index.js +6 -6
- package/src/chat/spinner.js +38 -38
- package/src/chat/sse.js +69 -69
- package/src/chat/stats.js +247 -247
- package/src/chat/store.js +48 -48
- package/src/chat/tool-handlers.js +67 -67
- package/src/chat/tools.js +1 -1
- package/src/chat/upstream.js +59 -59
- package/src/chat-pipeline/auto-race.js +158 -158
- package/src/chat-pipeline/empty-turn.js +147 -147
- package/src/chat-pipeline/engine.js +17 -17
- package/src/chat-pipeline/index.js +8 -40
- package/src/chat-pipeline/serial-trial.js +279 -283
- package/src/chat-pipeline/terminal-verdict.js +132 -147
- package/src/chooser.js +54 -54
- package/src/claude-desktop/apply.js +101 -101
- package/src/claude-desktop/context1m.js +38 -38
- package/src/claude-desktop/doctor.js +146 -146
- package/src/claude-desktop/slots.js +152 -152
- package/src/cli/bootstrap.js +1 -1
- package/src/cli/commands/claude-desktop.js +284 -284
- package/src/cli/commands/daemon.js +231 -231
- package/src/cli/commands/model/list-live.js +22 -22
- package/src/cli/commands/model/list-providers.js +76 -76
- package/src/cli/commands/model/list-render.js +77 -77
- package/src/cli/commands/model/list-sort.js +52 -52
- package/src/cli/commands/model/list-tty.js +76 -76
- package/src/cli/commands/model/list.js +166 -166
- package/src/cli/commands/model/live-models.js +24 -24
- package/src/cli/commands/model/stats.js +43 -43
- package/src/cli/commands/model/status.js +47 -47
- package/src/cli/commands/model.js +51 -51
- package/src/cli/commands/provider/add.js +108 -108
- package/src/cli/commands/provider/allowlist.js +99 -99
- package/src/cli/commands/provider/cline-free.js +99 -99
- package/src/cli/commands/provider/cline-login.js +218 -218
- package/src/cli/commands/provider/cline-quota.js +65 -65
- package/src/cli/commands/provider/codearts-login.js +45 -45
- package/src/cli/commands/provider/config.js +119 -119
- package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
- package/src/cli/commands/provider/keys-guard.js +50 -50
- package/src/cli/commands/provider/keys.js +161 -161
- package/src/cli/commands/provider/qoder-checkin.js +81 -81
- package/src/cli/commands/provider/qoder-login.js +102 -102
- package/src/cli/commands/provider/raccoon-login.js +180 -180
- package/src/cli/commands/provider/raccoon-password.js +162 -162
- package/src/cli/commands/provider/raccoon-quota.js +109 -109
- package/src/cli/commands/provider/traework-checkin.js +144 -144
- package/src/cli/commands/provider/traework-login.js +164 -164
- package/src/cli/commands/provider/workbuddy-login.js +153 -153
- package/src/cli/commands/provider/zcode-login.js +77 -77
- package/src/cli/commands/provider/zcode-quota.js +55 -55
- package/src/cli/commands/provider.js +1 -1
- package/src/cli/commands/stats.js +158 -149
- package/src/cli/commands/system.js +261 -261
- package/src/cli/commands/timezone.js +47 -47
- package/src/cli/commands/use-group.js +60 -60
- package/src/cli/commands/workbuddy.js +189 -189
- package/src/cli/format.js +141 -141
- package/src/cli/group-helpers.js +70 -70
- package/src/cli/help.js +80 -80
- package/src/cli/index.js +70 -70
- package/src/cli/interactive.js +121 -121
- package/src/cli/join-core.js +99 -99
- package/src/cli/join-wizard.js +175 -175
- package/src/cli/policy.js +136 -136
- package/src/cli/provider-row.js +126 -126
- package/src/cli/util.js +24 -24
- package/src/compat.js +53 -53
- package/src/events.js +38 -38
- package/src/free-lane.js +131 -131
- package/src/free-watcher.js +63 -63
- package/src/groups.js +262 -262
- package/src/logs.js +195 -195
- package/src/metrics.js +71 -62
- package/src/model-capabilities/context-lookup.js +65 -65
- package/src/model-capabilities/merge.js +142 -142
- package/src/model-trace.js +187 -187
- package/src/models.js +1 -1
- package/src/opencode-identity.js +72 -72
- package/src/peers.js +254 -254
- package/src/plugins.js +107 -107
- package/src/providers/AGENTS.md +49 -0
- package/src/providers/base.js +164 -164
- package/src/providers/classify.js +22 -22
- package/src/providers/cline/allowlist-sync.js +93 -93
- package/src/providers/cline/free-catalog.js +71 -71
- package/src/providers/cline/headers.js +37 -37
- package/src/providers/cline/index.js +130 -130
- package/src/providers/cline/models.js +150 -150
- package/src/providers/cline/usage.js +274 -274
- package/src/providers/codearts/auth-pool.js +148 -148
- package/src/providers/codearts/concurrency.js +34 -34
- package/src/providers/codearts/const.js +65 -65
- package/src/providers/codearts/dpop.js +95 -95
- package/src/providers/codearts/index.js +175 -175
- package/src/providers/codearts/login.js +214 -214
- package/src/providers/codearts/models.js +182 -182
- package/src/providers/codearts/sign.js +96 -96
- package/src/providers/codearts/sse.js +204 -204
- package/src/providers/codearts/stream.js +209 -209
- package/src/providers/codearts/sts.js +124 -124
- package/src/providers/codearts.js +1 -1
- package/src/providers/dispatcher.js +146 -146
- package/src/providers/generic.js +156 -156
- package/src/providers/globalqwenwork/account-store.js +141 -141
- package/src/providers/globalqwenwork/constants.js +73 -73
- package/src/providers/globalqwenwork/crypto.js +2 -2
- package/src/providers/globalqwenwork/http.js +2 -2
- package/src/providers/globalqwenwork/index.js +331 -331
- package/src/providers/globalqwenwork/payload.js +147 -147
- package/src/providers/globalqwenwork/rsa.js +2 -2
- package/src/providers/globalqwenwork/sse.js +2 -2
- package/src/providers/globalqwenwork/stream.js +2 -2
- package/src/providers/globalqwenwork.js +1 -1
- package/src/providers/globalworkbuddy/account-store.js +198 -198
- package/src/providers/globalworkbuddy/auth.js +97 -97
- package/src/providers/globalworkbuddy/catalog.js +138 -138
- package/src/providers/globalworkbuddy/constants.js +73 -73
- package/src/providers/globalworkbuddy/device-auth.js +120 -120
- package/src/providers/globalworkbuddy/errors.js +98 -98
- package/src/providers/globalworkbuddy/headers.js +89 -89
- package/src/providers/globalworkbuddy/models.js +96 -96
- package/src/providers/globalworkbuddy/payload.js +80 -80
- package/src/providers/index.js +5 -5
- package/src/providers/model-id.js +172 -172
- package/src/providers/opencode.js +19 -19
- package/src/providers/openrouter.js +31 -31
- package/src/providers/qoder/account-store.js +106 -106
- package/src/providers/qoder/aggregate.js +60 -60
- package/src/providers/qoder/baseprompt.json +591 -591
- package/src/providers/qoder/checkin.js +160 -160
- package/src/providers/qoder/constants.js +44 -44
- package/src/providers/qoder/encode.js +41 -41
- package/src/providers/qoder/fingerprint.js +38 -38
- package/src/providers/qoder/models.js +77 -77
- package/src/providers/qoder/oauth.js +84 -84
- package/src/providers/qoder/payload.js +162 -162
- package/src/providers/qoder/request.js +27 -27
- package/src/providers/qoder/session.js +134 -134
- package/src/providers/qoder/sticky.js +38 -38
- package/src/providers/qoder/stream.js +116 -116
- package/src/providers/qoder.js +1 -1
- package/src/providers/raccoon/account-store.js +216 -216
- package/src/providers/raccoon/auth.js +67 -67
- package/src/providers/raccoon/chat.js +228 -228
- package/src/providers/raccoon/const.js +180 -180
- package/src/providers/raccoon/credits.js +185 -185
- package/src/providers/raccoon/crypto.js +43 -43
- package/src/providers/raccoon/envelope.js +31 -31
- package/src/providers/raccoon/headers.js +45 -45
- package/src/providers/raccoon/index.js +216 -216
- package/src/providers/raccoon/login.js +279 -279
- package/src/providers/raccoon/models.js +160 -160
- package/src/providers/raccoon/password-login.js +102 -102
- package/src/providers/raccoon/qr-codewords.js +115 -115
- package/src/providers/raccoon/qr.js +236 -236
- package/src/providers/raccoon/sse.js +135 -135
- package/src/providers/responses-channel.js +120 -120
- package/src/providers/share-keys.js +53 -53
- package/src/providers/traework/account-store.js +234 -234
- package/src/providers/traework/chat.js +217 -217
- package/src/providers/traework/checkin.js +77 -77
- package/src/providers/traework/constants.js +41 -41
- package/src/providers/traework/errors.js +52 -52
- package/src/providers/traework/headers.js +79 -79
- package/src/providers/traework/index.js +132 -132
- package/src/providers/traework/market-identity.js +116 -116
- package/src/providers/traework/models.js +98 -98
- package/src/providers/traework/payload.js +86 -86
- package/src/providers/traework/sse.js +184 -184
- package/src/providers/traework/token.js +52 -52
- package/src/providers/traework.js +2 -2
- package/src/providers/workbuddy/account-store.js +186 -186
- package/src/providers/workbuddy/auth.js +158 -158
- package/src/providers/workbuddy/balance.js +86 -86
- package/src/providers/workbuddy/cat-travel.js +125 -125
- package/src/providers/workbuddy/checkin.js +75 -75
- package/src/providers/workbuddy/desktop-info.js +78 -78
- package/src/providers/workbuddy/growth-api.js +67 -67
- package/src/providers/workbuddy/growth-plans.js +63 -63
- package/src/providers/workbuddy/growth.js +181 -181
- package/src/providers/workbuddy/index.js +217 -217
- package/src/providers/workbuddy/models.js +126 -126
- package/src/providers/workbuddy/payload.js +141 -141
- package/src/providers/workbuddy/rotation-log.js +83 -83
- package/src/providers/workbuddy/sanitize-tools.js +61 -61
- package/src/providers/workbuddy/sdk-chat.js +26 -26
- package/src/providers/workbuddy-balance.js +78 -78
- package/src/providers/zcode/account-store.js +129 -129
- package/src/providers/zcode/auth.js +28 -28
- package/src/providers/zcode/chat.js +179 -179
- package/src/providers/zcode/const.js +54 -54
- package/src/providers/zcode/context-shape.js +162 -162
- package/src/providers/zcode/headers.js +93 -93
- package/src/providers/zcode/index.js +124 -124
- package/src/providers/zcode/models.js +65 -65
- package/src/providers/zcode/oauth.js +120 -120
- package/src/providers/zcode/quota.js +176 -176
- package/src/providers/zcode/sse.js +196 -196
- package/src/readline-compat.js +25 -25
- package/src/responses/translate.js +311 -311
- package/src/routes/AGENTS.md +37 -0
- package/src/routes/chat/broadband-handler.js +72 -72
- package/src/routes/chat/empty-turn-report.js +31 -31
- package/src/routes/chat/exhausted-handler.js +87 -87
- package/src/routes/chat/hedge-handler.js +130 -130
- package/src/routes/chat/index.js +1 -1
- package/src/routes/chat/local-handler.js +63 -63
- package/src/routes/chat/peer-handler.js +81 -81
- package/src/routes/chat.js +3 -3
- package/src/routes/fallback.js +88 -88
- package/src/routes/groups-relay.js +214 -214
- package/src/routes/groups.js +122 -122
- package/src/routes/helpers.js +93 -83
- package/src/routes/index.js +147 -147
- package/src/routes/messages-route.js +375 -365
- package/src/routes/peers.js +10 -2
- package/src/routes/relay-queue.js +181 -181
- package/src/routes/relay.js +60 -60
- package/src/routes/responses-route.js +213 -213
- package/src/routes/stream-body.js +84 -84
- package/src/routes/stream-hold.js +337 -337
- package/src/routes/stream-scan.js +366 -366
- package/src/routes.js +3 -3
- package/src/runtime/auto-update.js +113 -113
- package/src/runtime/broadband-stream.js +79 -79
- package/src/runtime/broadband.js +103 -103
- package/src/runtime/group-sync.js +81 -81
- package/src/runtime/keep-warm.js +120 -120
- package/src/runtime/lifecycle-forensics.js +116 -116
- package/src/runtime/lifecycle-log.js +87 -87
- package/src/runtime/log-stamp.js +73 -73
- package/src/runtime/proc-info.js +137 -137
- package/src/runtime/qoder-checkin.js +93 -93
- package/src/runtime/raccoon-checkin.js +126 -126
- package/src/runtime/server-lifecycle.js +189 -189
- package/src/runtime/traework-checkin.js +118 -118
- package/src/runtime/workbuddy-checkin.js +120 -120
- package/src/runtime/workbuddy-growth.js +133 -133
- package/src/state/facade.js +58 -58
- package/src/state/memory.js +162 -162
- package/src/state/merge.js +25 -25
- package/src/state/migrations/cline-unify.js +94 -94
- package/src/state/migrations.js +33 -33
- package/src/state/persist.js +42 -42
- package/src/state/provider-config.js +142 -142
- package/src/state/schemas/group.js +31 -31
- package/src/state/schemas/model.js +94 -94
- package/src/state/schemas/peer.js +31 -31
- package/src/state/schemas/port.js +10 -10
- package/src/state/schemas/provider.js +202 -202
- package/src/state/schemas/timezone.js +70 -70
- package/src/state/schemas/token.js +48 -48
- package/src/state/schemas/use-group.js +102 -102
- package/src/state/store.js +176 -176
- package/src/state.js +1 -1
- package/src/sync-claude-desktop.js +234 -234
- package/src/sync-claude.js +152 -152
- package/src/sync-codex.js +116 -116
- package/src/sync-workbuddy.js +158 -158
- package/src/talk-full-redact.js +129 -129
- package/src/talk-full.js +131 -131
- package/src/talk-log.js +257 -257
- package/src/text-tokens.js +52 -52
- package/src/thinking-effort.js +67 -67
- package/src/time.js +106 -106
- package/src/timeline.js +26 -26
- package/src/transport/index.js +1 -1
- package/src/upstream-engine/index.js +74 -74
- package/src/upstream-engine/mode.js +15 -15
- package/src/upstream-engine/sdk/attempt.js +221 -221
- package/src/upstream-engine/sdk/chat.js +28 -28
- package/src/upstream-engine/sdk/convert.js +192 -192
- package/src/upstream-engine/sdk/diagnose.js +62 -62
- package/src/upstream-engine/sdk/dispatch.js +55 -55
- package/src/upstream-engine/sdk/responses.js +176 -176
- package/src/upstream-engine/sdk/sse.js +166 -166
- package/src/upstream-responses.js +305 -305
- package/src/usage/cache-fields.js +58 -0
- package/src/usage/record.js +109 -106
- package/src/usage/report.js +209 -195
- package/src/v6pool/claim.js +157 -157
- package/src/v6pool/config.js +65 -65
- package/src/v6pool/detect.js +130 -130
- package/src/v6pool/index.js +152 -152
- package/src/v6pool/prefix.js +140 -140
- package/src/v6pool/probe.js +40 -40
- package/src/v6pool/report.js +104 -104
- package/src/v6pool/rotate.js +263 -263
- package/src/warp-pool/config.js +74 -74
- package/src/warp-pool/health.js +106 -106
- package/src/warp-pool/identity.js +141 -141
- package/src/warp-pool/index.js +358 -358
- package/src/warp-pool/keys.js +29 -29
- package/src/warp-pool/nodes.js +95 -95
- package/src/warp-pool/ports.js +160 -160
- package/src/warp-pool/register.js +113 -113
- package/src/warp-pool/report.js +232 -232
- package/src/warp-pool/revive.js +168 -168
- package/src/warp-pool/rotate.js +214 -214
- package/src/warp-pool/strategy.js +43 -43
- package/src/warp-pool/verdict.js +69 -69
- package/src/warp-pool/wireproxy.js +162 -162
|
@@ -1,160 +1,160 @@
|
|
|
1
|
-
// raccoon 模型目录:上游 `model_catalog` 解析 + 兜底目录 + 积分倍率展示名 + allowlist 补齐。
|
|
2
|
-
// 倍率是这家供应商的**成本唯一载体**(积分制,不是免费额度),所以它在模型名与列表里都必须出现:
|
|
3
|
-
// 0 → 「免费」;1 → 也显示 `x1`(省略会让用户以为「没倍率=不扣分」);促销 → `x0.2→x0.1`。
|
|
4
|
-
import {
|
|
5
|
-
RACCOON_FALLBACK_MODELS,
|
|
6
|
-
RACCOON_REQUEST_TIMEOUT_MS,
|
|
7
|
-
raccoonModelCatalogUrl,
|
|
8
|
-
} from "./const.js";
|
|
9
|
-
import { raccoonAuthHeaders } from "./headers.js";
|
|
10
|
-
import { parseRaccoonEnvelope } from "./envelope.js";
|
|
11
|
-
import { loadProviderAllowedModels, saveProviderAllowedModels } from "../../state/schemas/allowlist.js";
|
|
12
|
-
import { joinModelId } from "../model-id.js";
|
|
13
|
-
|
|
14
|
-
const pickString = (src, keys) => {
|
|
15
|
-
for (const k of keys) {
|
|
16
|
-
const v = src?.[k];
|
|
17
|
-
if (typeof v === "string" && v.trim().length > 0) return v.trim();
|
|
18
|
-
}
|
|
19
|
-
return "";
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const pickNumber = (src, keys) => {
|
|
23
|
-
for (const k of keys) {
|
|
24
|
-
const v = src?.[k];
|
|
25
|
-
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
26
|
-
if (typeof v === "string" && /^-?\d+(\.\d+)?$/.test(v.trim())) return Number(v.trim());
|
|
27
|
-
}
|
|
28
|
-
return undefined;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const pickBool = (src, keys) => {
|
|
32
|
-
for (const k of keys) {
|
|
33
|
-
const v = src?.[k];
|
|
34
|
-
if (typeof v === "boolean") return v;
|
|
35
|
-
}
|
|
36
|
-
return undefined;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/** 兜底目录(const.js 的静态表,字段名已归一)。 */
|
|
40
|
-
export function raccoonFallbackModels() {
|
|
41
|
-
return RACCOON_FALLBACK_MODELS.map((m) => ({ ...m }));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** 单条目录条目:字段名容错(缺字段即省略,不猜、不填假值)。 */
|
|
45
|
-
function parseRaccoonModelEntry(raw) {
|
|
46
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
47
|
-
const id = pickString(raw, ["id", "modelId", "model_id", "modelName", "name"]);
|
|
48
|
-
if (!id) return null;
|
|
49
|
-
const model = { id };
|
|
50
|
-
const name = pickString(raw, ["modelName", "name", "displayName", "description"]);
|
|
51
|
-
if (name) model.name = name;
|
|
52
|
-
const ctx = pickNumber(raw, ["contextWindow", "context_window", "contextLength", "context_length"]);
|
|
53
|
-
if (ctx !== undefined && ctx > 0) model.contextWindow = ctx;
|
|
54
|
-
const maxTokens = pickNumber(raw, ["maxTokens", "max_tokens", "maxCompletionTokens"]);
|
|
55
|
-
if (maxTokens !== undefined && maxTokens > 0) model.maxTokens = maxTokens;
|
|
56
|
-
const img = pickBool(raw, ["supportsImage", "supports_image", "supportsVision"]);
|
|
57
|
-
if (img !== undefined) model.supportsImage = img;
|
|
58
|
-
const eff = pickNumber(raw, ["effectiveMultiplier", "effective_multiplier", "multiplier", "costMultiplier"]);
|
|
59
|
-
if (eff !== undefined && eff >= 0) model.multiplier = eff;
|
|
60
|
-
const base = pickNumber(raw, ["baseMultiplier", "base_multiplier"]);
|
|
61
|
-
if (base !== undefined && base > 0) model.baseMultiplier = base;
|
|
62
|
-
return model;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** 目录信封 → 条目数组。兼容 `data` 为数组、`data.models`、`data.list`、`data.items` 四种形状。 */
|
|
66
|
-
export function parseRaccoonCatalog(data) {
|
|
67
|
-
const list = Array.isArray(data)
|
|
68
|
-
? data
|
|
69
|
-
: Array.isArray(data?.models)
|
|
70
|
-
? data.models
|
|
71
|
-
: Array.isArray(data?.list)
|
|
72
|
-
? data.list
|
|
73
|
-
: Array.isArray(data?.items)
|
|
74
|
-
? data.items
|
|
75
|
-
: [];
|
|
76
|
-
const out = [];
|
|
77
|
-
const seen = new Set();
|
|
78
|
-
for (const raw of list) {
|
|
79
|
-
const m = parseRaccoonModelEntry(raw);
|
|
80
|
-
if (!m || seen.has(m.id)) continue;
|
|
81
|
-
seen.add(m.id);
|
|
82
|
-
out.push(m);
|
|
83
|
-
}
|
|
84
|
-
return out;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** 倍率文案:0 → 免费;促销 → `x基→x实`;其余 → `x实`(含 x1)。 */
|
|
88
|
-
export function raccoonMultiplierLabel(model) {
|
|
89
|
-
const eff = typeof model?.multiplier === "number" ? model.multiplier : undefined;
|
|
90
|
-
if (eff === undefined) return "";
|
|
91
|
-
if (eff === 0) return "免费";
|
|
92
|
-
const fmt = (v) => String(Number(v.toFixed(4)));
|
|
93
|
-
const base = typeof model?.baseMultiplier === "number" ? model.baseMultiplier : undefined;
|
|
94
|
-
if (base !== undefined && base > eff) return `x${fmt(base)}→x${fmt(eff)}`;
|
|
95
|
-
return `x${fmt(eff)}`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** 列表/模型卡展示名:`<名字或 id> · <倍率>`(无倍率时省略后缀)。 */
|
|
99
|
-
export function raccoonDisplayName(model) {
|
|
100
|
-
const base = model?.name || model?.id || "";
|
|
101
|
-
const label = raccoonMultiplierLabel(model);
|
|
102
|
-
return label ? `${base} · ${label}` : base;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** 拉上游目录;失败/空目录返回 null(调用方回落兜底表)。 */
|
|
106
|
-
export async function fetchRaccoonModelCatalog({
|
|
107
|
-
credential,
|
|
108
|
-
fetchImpl = fetch,
|
|
109
|
-
env = process.env,
|
|
110
|
-
timeoutMs = RACCOON_REQUEST_TIMEOUT_MS,
|
|
111
|
-
} = {}) {
|
|
112
|
-
if (!credential?.access_token) return null;
|
|
113
|
-
try {
|
|
114
|
-
const res = await fetchImpl(raccoonModelCatalogUrl(), {
|
|
115
|
-
method: "GET",
|
|
116
|
-
headers: raccoonAuthHeaders(credential, { env }),
|
|
117
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
118
|
-
});
|
|
119
|
-
if (!res.ok) return null;
|
|
120
|
-
const envelope = parseRaccoonEnvelope(await res.json(), res.status);
|
|
121
|
-
if (envelope.code !== 0) return null;
|
|
122
|
-
const models = parseRaccoonCatalog(envelope.data);
|
|
123
|
-
return models.length > 0 ? models : null;
|
|
124
|
-
} catch {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** 上游优先、兜底次之。返回 `{ models, source }`,source ∈ upstream|fallback。 */
|
|
130
|
-
export async function listRaccoonModels({ credential, fetchImpl, env } = {}) {
|
|
131
|
-
const upstream = await fetchRaccoonModelCatalog({ credential, fetchImpl, env });
|
|
132
|
-
if (upstream) return { models: upstream, source: "upstream" };
|
|
133
|
-
return { models: raccoonFallbackModels(), source: "fallback" };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** 登录后把兜底目录 id 只增不减并入 allowlist(避免「登录成功却处处 403」)。 */
|
|
137
|
-
export function seedRaccoonAllowlist({ ids, file } = {}) {
|
|
138
|
-
const opts = file ? { file } : {};
|
|
139
|
-
const target = ids || RACCOON_FALLBACK_MODELS.map((m) => m.id);
|
|
140
|
-
const cur = loadProviderAllowedModels("raccoon", opts);
|
|
141
|
-
const added = target.filter((m) => !cur.includes(m));
|
|
142
|
-
if (added.length) saveProviderAllowedModels("raccoon", [...cur, ...added], opts);
|
|
143
|
-
return { allowed: [...cur, ...added], added };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** 对外目录条目(`/v1/models` 与 CLI models 共用形状)。 */
|
|
147
|
-
export function buildRaccoonModelList({ id = "raccoon", models } = {}) {
|
|
148
|
-
const list = models || raccoonFallbackModels();
|
|
149
|
-
return list.map((m) => ({
|
|
150
|
-
id: joinModelId(id, m.id),
|
|
151
|
-
object: "model",
|
|
152
|
-
owned_by: "raccoon",
|
|
153
|
-
name: raccoonDisplayName(m),
|
|
154
|
-
context_length: m.contextWindow,
|
|
155
|
-
max_tokens: m.maxTokens,
|
|
156
|
-
supports_image: m.supportsImage,
|
|
157
|
-
multiplier: m.multiplier,
|
|
158
|
-
mark: raccoonMultiplierLabel(m) || undefined,
|
|
159
|
-
}));
|
|
160
|
-
}
|
|
1
|
+
// raccoon 模型目录:上游 `model_catalog` 解析 + 兜底目录 + 积分倍率展示名 + allowlist 补齐。
|
|
2
|
+
// 倍率是这家供应商的**成本唯一载体**(积分制,不是免费额度),所以它在模型名与列表里都必须出现:
|
|
3
|
+
// 0 → 「免费」;1 → 也显示 `x1`(省略会让用户以为「没倍率=不扣分」);促销 → `x0.2→x0.1`。
|
|
4
|
+
import {
|
|
5
|
+
RACCOON_FALLBACK_MODELS,
|
|
6
|
+
RACCOON_REQUEST_TIMEOUT_MS,
|
|
7
|
+
raccoonModelCatalogUrl,
|
|
8
|
+
} from "./const.js";
|
|
9
|
+
import { raccoonAuthHeaders } from "./headers.js";
|
|
10
|
+
import { parseRaccoonEnvelope } from "./envelope.js";
|
|
11
|
+
import { loadProviderAllowedModels, saveProviderAllowedModels } from "../../state/schemas/allowlist.js";
|
|
12
|
+
import { joinModelId } from "../model-id.js";
|
|
13
|
+
|
|
14
|
+
const pickString = (src, keys) => {
|
|
15
|
+
for (const k of keys) {
|
|
16
|
+
const v = src?.[k];
|
|
17
|
+
if (typeof v === "string" && v.trim().length > 0) return v.trim();
|
|
18
|
+
}
|
|
19
|
+
return "";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const pickNumber = (src, keys) => {
|
|
23
|
+
for (const k of keys) {
|
|
24
|
+
const v = src?.[k];
|
|
25
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
26
|
+
if (typeof v === "string" && /^-?\d+(\.\d+)?$/.test(v.trim())) return Number(v.trim());
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const pickBool = (src, keys) => {
|
|
32
|
+
for (const k of keys) {
|
|
33
|
+
const v = src?.[k];
|
|
34
|
+
if (typeof v === "boolean") return v;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** 兜底目录(const.js 的静态表,字段名已归一)。 */
|
|
40
|
+
export function raccoonFallbackModels() {
|
|
41
|
+
return RACCOON_FALLBACK_MODELS.map((m) => ({ ...m }));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 单条目录条目:字段名容错(缺字段即省略,不猜、不填假值)。 */
|
|
45
|
+
function parseRaccoonModelEntry(raw) {
|
|
46
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
47
|
+
const id = pickString(raw, ["id", "modelId", "model_id", "modelName", "name"]);
|
|
48
|
+
if (!id) return null;
|
|
49
|
+
const model = { id };
|
|
50
|
+
const name = pickString(raw, ["modelName", "name", "displayName", "description"]);
|
|
51
|
+
if (name) model.name = name;
|
|
52
|
+
const ctx = pickNumber(raw, ["contextWindow", "context_window", "contextLength", "context_length"]);
|
|
53
|
+
if (ctx !== undefined && ctx > 0) model.contextWindow = ctx;
|
|
54
|
+
const maxTokens = pickNumber(raw, ["maxTokens", "max_tokens", "maxCompletionTokens"]);
|
|
55
|
+
if (maxTokens !== undefined && maxTokens > 0) model.maxTokens = maxTokens;
|
|
56
|
+
const img = pickBool(raw, ["supportsImage", "supports_image", "supportsVision"]);
|
|
57
|
+
if (img !== undefined) model.supportsImage = img;
|
|
58
|
+
const eff = pickNumber(raw, ["effectiveMultiplier", "effective_multiplier", "multiplier", "costMultiplier"]);
|
|
59
|
+
if (eff !== undefined && eff >= 0) model.multiplier = eff;
|
|
60
|
+
const base = pickNumber(raw, ["baseMultiplier", "base_multiplier"]);
|
|
61
|
+
if (base !== undefined && base > 0) model.baseMultiplier = base;
|
|
62
|
+
return model;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 目录信封 → 条目数组。兼容 `data` 为数组、`data.models`、`data.list`、`data.items` 四种形状。 */
|
|
66
|
+
export function parseRaccoonCatalog(data) {
|
|
67
|
+
const list = Array.isArray(data)
|
|
68
|
+
? data
|
|
69
|
+
: Array.isArray(data?.models)
|
|
70
|
+
? data.models
|
|
71
|
+
: Array.isArray(data?.list)
|
|
72
|
+
? data.list
|
|
73
|
+
: Array.isArray(data?.items)
|
|
74
|
+
? data.items
|
|
75
|
+
: [];
|
|
76
|
+
const out = [];
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
for (const raw of list) {
|
|
79
|
+
const m = parseRaccoonModelEntry(raw);
|
|
80
|
+
if (!m || seen.has(m.id)) continue;
|
|
81
|
+
seen.add(m.id);
|
|
82
|
+
out.push(m);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 倍率文案:0 → 免费;促销 → `x基→x实`;其余 → `x实`(含 x1)。 */
|
|
88
|
+
export function raccoonMultiplierLabel(model) {
|
|
89
|
+
const eff = typeof model?.multiplier === "number" ? model.multiplier : undefined;
|
|
90
|
+
if (eff === undefined) return "";
|
|
91
|
+
if (eff === 0) return "免费";
|
|
92
|
+
const fmt = (v) => String(Number(v.toFixed(4)));
|
|
93
|
+
const base = typeof model?.baseMultiplier === "number" ? model.baseMultiplier : undefined;
|
|
94
|
+
if (base !== undefined && base > eff) return `x${fmt(base)}→x${fmt(eff)}`;
|
|
95
|
+
return `x${fmt(eff)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 列表/模型卡展示名:`<名字或 id> · <倍率>`(无倍率时省略后缀)。 */
|
|
99
|
+
export function raccoonDisplayName(model) {
|
|
100
|
+
const base = model?.name || model?.id || "";
|
|
101
|
+
const label = raccoonMultiplierLabel(model);
|
|
102
|
+
return label ? `${base} · ${label}` : base;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 拉上游目录;失败/空目录返回 null(调用方回落兜底表)。 */
|
|
106
|
+
export async function fetchRaccoonModelCatalog({
|
|
107
|
+
credential,
|
|
108
|
+
fetchImpl = fetch,
|
|
109
|
+
env = process.env,
|
|
110
|
+
timeoutMs = RACCOON_REQUEST_TIMEOUT_MS,
|
|
111
|
+
} = {}) {
|
|
112
|
+
if (!credential?.access_token) return null;
|
|
113
|
+
try {
|
|
114
|
+
const res = await fetchImpl(raccoonModelCatalogUrl(), {
|
|
115
|
+
method: "GET",
|
|
116
|
+
headers: raccoonAuthHeaders(credential, { env }),
|
|
117
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
118
|
+
});
|
|
119
|
+
if (!res.ok) return null;
|
|
120
|
+
const envelope = parseRaccoonEnvelope(await res.json(), res.status);
|
|
121
|
+
if (envelope.code !== 0) return null;
|
|
122
|
+
const models = parseRaccoonCatalog(envelope.data);
|
|
123
|
+
return models.length > 0 ? models : null;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 上游优先、兜底次之。返回 `{ models, source }`,source ∈ upstream|fallback。 */
|
|
130
|
+
export async function listRaccoonModels({ credential, fetchImpl, env } = {}) {
|
|
131
|
+
const upstream = await fetchRaccoonModelCatalog({ credential, fetchImpl, env });
|
|
132
|
+
if (upstream) return { models: upstream, source: "upstream" };
|
|
133
|
+
return { models: raccoonFallbackModels(), source: "fallback" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 登录后把兜底目录 id 只增不减并入 allowlist(避免「登录成功却处处 403」)。 */
|
|
137
|
+
export function seedRaccoonAllowlist({ ids, file } = {}) {
|
|
138
|
+
const opts = file ? { file } : {};
|
|
139
|
+
const target = ids || RACCOON_FALLBACK_MODELS.map((m) => m.id);
|
|
140
|
+
const cur = loadProviderAllowedModels("raccoon", opts);
|
|
141
|
+
const added = target.filter((m) => !cur.includes(m));
|
|
142
|
+
if (added.length) saveProviderAllowedModels("raccoon", [...cur, ...added], opts);
|
|
143
|
+
return { allowed: [...cur, ...added], added };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 对外目录条目(`/v1/models` 与 CLI models 共用形状)。 */
|
|
147
|
+
export function buildRaccoonModelList({ id = "raccoon", models } = {}) {
|
|
148
|
+
const list = models || raccoonFallbackModels();
|
|
149
|
+
return list.map((m) => ({
|
|
150
|
+
id: joinModelId(id, m.id),
|
|
151
|
+
object: "model",
|
|
152
|
+
owned_by: "raccoon",
|
|
153
|
+
name: raccoonDisplayName(m),
|
|
154
|
+
context_length: m.contextWindow,
|
|
155
|
+
max_tokens: m.maxTokens,
|
|
156
|
+
supports_image: m.supportsImage,
|
|
157
|
+
multiplier: m.multiplier,
|
|
158
|
+
mark: raccoonMultiplierLabel(m) || undefined,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
// raccoon 密码登录(手机号 + 密码)—— 多账号自动化添加的捷径(免扫码)。
|
|
2
|
-
// 契约、端点选择理由与「为什么必须加密」见 const.js 的 raccoonPasswordLoginUrl 注释;
|
|
3
|
-
// 加密算法与 key 见 crypto.js(单一源)。
|
|
4
|
-
//
|
|
5
|
-
// ⚠ 安全红线:密码**只作为请求体出现一次**(且出站前已被加密)。本模块不落盘、不打印、不记日志;
|
|
6
|
-
// 抛出的错误里也绝不带上请求体原文(上游 message 是它自己的文案,安全)。
|
|
7
|
-
import {
|
|
8
|
-
RACCOON_PASSWORD_ERRORS,
|
|
9
|
-
RACCOON_REQUEST_TIMEOUT_MS,
|
|
10
|
-
raccoonPasswordLoginUrl,
|
|
11
|
-
} from "./const.js";
|
|
12
|
-
import { raccoonAnonymousHeaders } from "./headers.js";
|
|
13
|
-
import { isRaccoonOk, parseRaccoonEnvelope, raccoonEnvelopeText } from "./envelope.js";
|
|
14
|
-
import { decodeJwtExpMs } from "./auth.js";
|
|
15
|
-
import { raccoonEncryptField } from "./crypto.js";
|
|
16
|
-
|
|
17
|
-
/** nation_code 归一:接受 `"86"` / `"+86"` / `86`,统一成不带加号的数字串。 */
|
|
18
|
-
export function normalizeNationCode(raw) {
|
|
19
|
-
const s = String(raw ?? "").trim().replace(/^\+/, "");
|
|
20
|
-
return /^\d{1,4}$/.test(s) ? s : "";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** 手机号归一:只留数字(去空格、连字符、括号)。空结果由调用方判错。 */
|
|
24
|
-
export function normalizePhone(raw) {
|
|
25
|
-
return String(raw ?? "").replace(/[^\d]/g, "");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 上游登录失败 → 人话 Error。命中码表用它,否则透出上游 message(绝不带请求体)。 */
|
|
29
|
-
function passwordLoginError(envelope, status) {
|
|
30
|
-
const key = String(envelope?.code ?? "");
|
|
31
|
-
const friendly = RACCOON_PASSWORD_ERRORS[key];
|
|
32
|
-
const err = new Error(`raccoon: 密码登录失败:${friendly || raccoonEnvelopeText(envelope, `HTTP ${status}`)}`);
|
|
33
|
-
err.code = envelope?.code;
|
|
34
|
-
err.status = status;
|
|
35
|
-
return err;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 手机号 + 密码 → credential(与扫码登录**同一形状**,可直接喂 saveRaccoonAccount)。
|
|
40
|
-
*
|
|
41
|
-
* 返回 `{ access_token, refresh_token, expires_at?, office_identity? }`;
|
|
42
|
-
* 失败抛「可操作」的错误(码表命中给人话,未命中透出上游文案)。
|
|
43
|
-
* 本地校验先于出网:手机号/密码为空当场报错,不白跑一次请求。
|
|
44
|
-
*
|
|
45
|
-
* ⚠ 出站前 phone/password 都经 `raccoonEncryptField` 加密(上游契约,见 const.js)——
|
|
46
|
-
* 发明文只会换来一句「手机号或密码不正确」,把格式错误伪装成密码错误。
|
|
47
|
-
*/
|
|
48
|
-
export async function loginRaccoonWithPassword({ phone, password, nationCode = "86" } = {}, {
|
|
49
|
-
fetchImpl = fetch,
|
|
50
|
-
env = process.env,
|
|
51
|
-
timeoutMs = RACCOON_REQUEST_TIMEOUT_MS,
|
|
52
|
-
} = {}) {
|
|
53
|
-
const nc = normalizeNationCode(nationCode);
|
|
54
|
-
const ph = normalizePhone(phone);
|
|
55
|
-
if (!nc) throw new Error("raccoon: 国家码不合法(用 86 这类数字,或 +86)");
|
|
56
|
-
if (!ph) throw new Error("raccoon: 手机号不能为空");
|
|
57
|
-
if (String(password ?? "").length === 0) throw new Error("raccoon: 密码不能为空");
|
|
58
|
-
|
|
59
|
-
// 未知错误码时透出上游 message 而非裸码 —— 上游新旧通道对同一错误的形态不同:
|
|
60
|
-
// 新 IAM 把 message 放顶层(AUTH_ACCOUNT_OR_PASSWORD_INVALID),旧 web 只有 message/details。
|
|
61
|
-
const body = {
|
|
62
|
-
nation_code: nc,
|
|
63
|
-
phone: raccoonEncryptField(ph),
|
|
64
|
-
password: raccoonEncryptField(String(password)),
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
let res;
|
|
68
|
-
try {
|
|
69
|
-
res = await fetchImpl(raccoonPasswordLoginUrl(), {
|
|
70
|
-
method: "POST",
|
|
71
|
-
headers: raccoonAnonymousHeaders({ env }),
|
|
72
|
-
body: JSON.stringify(body),
|
|
73
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
74
|
-
});
|
|
75
|
-
} catch (error) {
|
|
76
|
-
throw new Error(`raccoon: 密码登录请求失败:${error?.message ?? error}`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
let parsed;
|
|
80
|
-
try {
|
|
81
|
-
parsed = await res.json();
|
|
82
|
-
} catch {
|
|
83
|
-
throw new Error(`raccoon: 密码登录响应不是 JSON(HTTP ${res.status})`);
|
|
84
|
-
}
|
|
85
|
-
const envelope = parseRaccoonEnvelope(parsed, res.status);
|
|
86
|
-
// 上游把错误码同时塞在 `code` 与 `message`:web 通道 `{code:"AUTH_...",message:"AUTH_..."}`
|
|
87
|
-
// 时 message 是裸码,不能当人话用 —— 故这里把 code 也纳入,让码表能命中。
|
|
88
|
-
if (!isRaccoonOk(envelope)) throw passwordLoginError(envelope, res.status);
|
|
89
|
-
|
|
90
|
-
const data = envelope.data ?? {};
|
|
91
|
-
const accessToken = typeof data.access_token === "string" ? data.access_token : "";
|
|
92
|
-
if (!accessToken) throw new Error("raccoon: 密码登录响应缺少 access_token");
|
|
93
|
-
const refreshToken = typeof data.refresh_token === "string" ? data.refresh_token : "";
|
|
94
|
-
const officeIdentity = typeof data.office_identity === "string" ? data.office_identity : "";
|
|
95
|
-
const expMs = decodeJwtExpMs(accessToken);
|
|
96
|
-
return {
|
|
97
|
-
access_token: accessToken,
|
|
98
|
-
refresh_token: refreshToken,
|
|
99
|
-
...(expMs === undefined ? {} : { expires_at: String(expMs) }),
|
|
100
|
-
...(officeIdentity ? { office_identity: officeIdentity } : {}),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
1
|
+
// raccoon 密码登录(手机号 + 密码)—— 多账号自动化添加的捷径(免扫码)。
|
|
2
|
+
// 契约、端点选择理由与「为什么必须加密」见 const.js 的 raccoonPasswordLoginUrl 注释;
|
|
3
|
+
// 加密算法与 key 见 crypto.js(单一源)。
|
|
4
|
+
//
|
|
5
|
+
// ⚠ 安全红线:密码**只作为请求体出现一次**(且出站前已被加密)。本模块不落盘、不打印、不记日志;
|
|
6
|
+
// 抛出的错误里也绝不带上请求体原文(上游 message 是它自己的文案,安全)。
|
|
7
|
+
import {
|
|
8
|
+
RACCOON_PASSWORD_ERRORS,
|
|
9
|
+
RACCOON_REQUEST_TIMEOUT_MS,
|
|
10
|
+
raccoonPasswordLoginUrl,
|
|
11
|
+
} from "./const.js";
|
|
12
|
+
import { raccoonAnonymousHeaders } from "./headers.js";
|
|
13
|
+
import { isRaccoonOk, parseRaccoonEnvelope, raccoonEnvelopeText } from "./envelope.js";
|
|
14
|
+
import { decodeJwtExpMs } from "./auth.js";
|
|
15
|
+
import { raccoonEncryptField } from "./crypto.js";
|
|
16
|
+
|
|
17
|
+
/** nation_code 归一:接受 `"86"` / `"+86"` / `86`,统一成不带加号的数字串。 */
|
|
18
|
+
export function normalizeNationCode(raw) {
|
|
19
|
+
const s = String(raw ?? "").trim().replace(/^\+/, "");
|
|
20
|
+
return /^\d{1,4}$/.test(s) ? s : "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 手机号归一:只留数字(去空格、连字符、括号)。空结果由调用方判错。 */
|
|
24
|
+
export function normalizePhone(raw) {
|
|
25
|
+
return String(raw ?? "").replace(/[^\d]/g, "");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 上游登录失败 → 人话 Error。命中码表用它,否则透出上游 message(绝不带请求体)。 */
|
|
29
|
+
function passwordLoginError(envelope, status) {
|
|
30
|
+
const key = String(envelope?.code ?? "");
|
|
31
|
+
const friendly = RACCOON_PASSWORD_ERRORS[key];
|
|
32
|
+
const err = new Error(`raccoon: 密码登录失败:${friendly || raccoonEnvelopeText(envelope, `HTTP ${status}`)}`);
|
|
33
|
+
err.code = envelope?.code;
|
|
34
|
+
err.status = status;
|
|
35
|
+
return err;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 手机号 + 密码 → credential(与扫码登录**同一形状**,可直接喂 saveRaccoonAccount)。
|
|
40
|
+
*
|
|
41
|
+
* 返回 `{ access_token, refresh_token, expires_at?, office_identity? }`;
|
|
42
|
+
* 失败抛「可操作」的错误(码表命中给人话,未命中透出上游文案)。
|
|
43
|
+
* 本地校验先于出网:手机号/密码为空当场报错,不白跑一次请求。
|
|
44
|
+
*
|
|
45
|
+
* ⚠ 出站前 phone/password 都经 `raccoonEncryptField` 加密(上游契约,见 const.js)——
|
|
46
|
+
* 发明文只会换来一句「手机号或密码不正确」,把格式错误伪装成密码错误。
|
|
47
|
+
*/
|
|
48
|
+
export async function loginRaccoonWithPassword({ phone, password, nationCode = "86" } = {}, {
|
|
49
|
+
fetchImpl = fetch,
|
|
50
|
+
env = process.env,
|
|
51
|
+
timeoutMs = RACCOON_REQUEST_TIMEOUT_MS,
|
|
52
|
+
} = {}) {
|
|
53
|
+
const nc = normalizeNationCode(nationCode);
|
|
54
|
+
const ph = normalizePhone(phone);
|
|
55
|
+
if (!nc) throw new Error("raccoon: 国家码不合法(用 86 这类数字,或 +86)");
|
|
56
|
+
if (!ph) throw new Error("raccoon: 手机号不能为空");
|
|
57
|
+
if (String(password ?? "").length === 0) throw new Error("raccoon: 密码不能为空");
|
|
58
|
+
|
|
59
|
+
// 未知错误码时透出上游 message 而非裸码 —— 上游新旧通道对同一错误的形态不同:
|
|
60
|
+
// 新 IAM 把 message 放顶层(AUTH_ACCOUNT_OR_PASSWORD_INVALID),旧 web 只有 message/details。
|
|
61
|
+
const body = {
|
|
62
|
+
nation_code: nc,
|
|
63
|
+
phone: raccoonEncryptField(ph),
|
|
64
|
+
password: raccoonEncryptField(String(password)),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let res;
|
|
68
|
+
try {
|
|
69
|
+
res = await fetchImpl(raccoonPasswordLoginUrl(), {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: raccoonAnonymousHeaders({ env }),
|
|
72
|
+
body: JSON.stringify(body),
|
|
73
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new Error(`raccoon: 密码登录请求失败:${error?.message ?? error}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = await res.json();
|
|
82
|
+
} catch {
|
|
83
|
+
throw new Error(`raccoon: 密码登录响应不是 JSON(HTTP ${res.status})`);
|
|
84
|
+
}
|
|
85
|
+
const envelope = parseRaccoonEnvelope(parsed, res.status);
|
|
86
|
+
// 上游把错误码同时塞在 `code` 与 `message`:web 通道 `{code:"AUTH_...",message:"AUTH_..."}`
|
|
87
|
+
// 时 message 是裸码,不能当人话用 —— 故这里把 code 也纳入,让码表能命中。
|
|
88
|
+
if (!isRaccoonOk(envelope)) throw passwordLoginError(envelope, res.status);
|
|
89
|
+
|
|
90
|
+
const data = envelope.data ?? {};
|
|
91
|
+
const accessToken = typeof data.access_token === "string" ? data.access_token : "";
|
|
92
|
+
if (!accessToken) throw new Error("raccoon: 密码登录响应缺少 access_token");
|
|
93
|
+
const refreshToken = typeof data.refresh_token === "string" ? data.refresh_token : "";
|
|
94
|
+
const officeIdentity = typeof data.office_identity === "string" ? data.office_identity : "";
|
|
95
|
+
const expMs = decodeJwtExpMs(accessToken);
|
|
96
|
+
return {
|
|
97
|
+
access_token: accessToken,
|
|
98
|
+
refresh_token: refreshToken,
|
|
99
|
+
...(expMs === undefined ? {} : { expires_at: String(expMs) }),
|
|
100
|
+
...(officeIdentity ? { office_identity: officeIdentity } : {}),
|
|
101
|
+
};
|
|
102
|
+
}
|