opencode-claude-code-ua 0.0.0-stage → 0.3.9
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/LICENSE +23 -0
- package/NOTICE.md +9 -0
- package/README.md +200 -2
- package/assets/icon.svg +9 -0
- package/index.mjs +19 -0
- package/lib/parameters.mjs +108 -0
- package/lib/ua-protocol.mjs +51 -0
- package/lib/ua-test-plan.mjs +232 -0
- package/package.json +80 -4
- package/setup.mjs +39 -0
- package/src/account-profile.mjs +73 -0
- package/src/account-usage.mjs +33 -0
- package/src/accounts.mjs +354 -0
- package/src/connection.mjs +144 -0
- package/src/diagnostics.mjs +37 -0
- package/src/headers.mjs +60 -0
- package/src/key-usage.mjs +73 -0
- package/src/newapi.mjs +330 -0
- package/src/options.mjs +48 -0
- package/src/plugin.mjs +344 -0
- package/src/profile.mjs +217 -0
- package/src/runtime.mjs +39 -0
- package/src/schedule.mjs +88 -0
- package/src/scheduler.mjs +141 -0
- package/src/setup-server.mjs +478 -0
- package/src/store.mjs +58 -0
- package/src/transport.mjs +84 -0
- package/src/ua-model-catalog.mjs +218 -0
- package/src/ua-plugin.mjs +250 -0
- package/src/ua-runtime.mjs +229 -0
- package/src/ua-service.mjs +129 -0
- package/src/ua-store.mjs +660 -0
- package/src/ua-tests.mjs +153 -0
- package/src/ua-transform.mjs +478 -0
- package/src/ua-usage.mjs +64 -0
- package/ui/accounts.css +75 -0
- package/ui/accounts.html +67 -0
- package/ui/accounts.js +786 -0
- package/ui/pro.css +1683 -0
- package/ui/pro.html +347 -0
- package/ui/pro.js +900 -0
- package/ui/theme.js +39 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { applyParameters, compileParameters, isObject } from '../lib/parameters.mjs';
|
|
3
|
+
|
|
4
|
+
// Pinned to sub2api-magpie-plugin a22d079b (0.4.16), not a live client version.
|
|
5
|
+
const CODEX_VERSION = '0.159.0';
|
|
6
|
+
const CLAUDE_VERSION = '2.1.296';
|
|
7
|
+
const IDENTITY_PROMPT = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
8
|
+
const FAST_BETA = 'fast-mode-2026-02-01';
|
|
9
|
+
const LIMITS = { maxBodyBytes: 32 << 20, bodyTimeoutMs: 15000, maxMetadataBytes: 1 << 20 };
|
|
10
|
+
const OPERATIONS = { anthropic: ['messages', 'count_tokens'], responses: ['responses'], 'chat-completions': ['chat/completions'], gemini: ['generateContent', 'streamGenerateContent', 'countTokens'] };
|
|
11
|
+
const TOKEN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
12
|
+
const BAD_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
|
|
13
|
+
const BUSINESS = new Set(['model', 'messages', 'stream', 'tools', 'tool_choice', 'input', 'contents', 'systemInstruction']);
|
|
14
|
+
const PROTECTED = new Set(['authorization', 'proxy-authorization', 'proxy-authenticate', 'www-authenticate', 'x-api-key', 'api-key', 'x-goog-api-key', 'cookie', 'set-cookie', 'host', 'content-length', 'connection', 'transfer-encoding', 'upgrade', 'keep-alive', 'te', 'trailer', 'proxy-connection', 'accept-encoding', 'x-openai-actor-authorization', 'x-anthropic-billing-header', 'x-sub2api-magpie-plugin-speed-model']);
|
|
15
|
+
const VARIABLES = new Set(['session.id', 'request.id', 'client.version', 'model.id', 'reasoning.effort', 'timestamp', 'platform', 'arch', 'terminal']);
|
|
16
|
+
const SESSION_HEADERS = ['session-id', 'session_id', 'x-session-id', 'x-claude-code-session-id', 'x-opencode-session'];
|
|
17
|
+
const CODEX_FAMILY = /^(?:codex_cli_rs|codex-tui|codex_vscode_copilot|codex_vscode|codex_app|codex_chatgpt_desktop|codex_atlas|codex_exec|codex_sdk_ts)$/;
|
|
18
|
+
const VS_BETA = 'claude-code-20250219,context-1m-2025-08-07,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,per-turn-control-2026-07-01,mid-conversation-tool-changes-2026-07-01,advanced-tool-use-2025-11-20,effort-2025-11-24,dangerous-tool-use-2026-09-03,afk-mode-2026-01-31';
|
|
19
|
+
|
|
20
|
+
function error(path, message = 'UA PRO configuration is invalid', status = 400) {
|
|
21
|
+
const e = new Error(message); e.status = status; e.fields = { [path]: message }; return e;
|
|
22
|
+
}
|
|
23
|
+
function known(value, keys, path) {
|
|
24
|
+
if (!isObject(value) || Object.keys(value).some(k => !keys.includes(k))) throw error(path);
|
|
25
|
+
}
|
|
26
|
+
function enumeration(value, values, path) {
|
|
27
|
+
if (!values.includes(value)) throw error(path); return value;
|
|
28
|
+
}
|
|
29
|
+
function text(value, path, max = 16384, blank = true) {
|
|
30
|
+
if (typeof value !== 'string' || value.length > max || /[\u0000-\u001f\u007f]/u.test(value) || (!blank && !value.trim())) throw error(path);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function protectedHeader(name) { return PROTECTED.has(name) || name.startsWith('x-magpie-'); }
|
|
34
|
+
function billingKey(name) { return /^(?:billing(?:[_-].*)?|x[_-]anthropic[_-]billing[_-]header)$/i.test(name); }
|
|
35
|
+
const billingBlock = value => typeof value === 'string' && /^\s*x-anthropic-billing-header\s*:/i.test(value);
|
|
36
|
+
const optional = (value, fallback) => value === undefined ? fallback : value;
|
|
37
|
+
function billingSnapshot(payload) {
|
|
38
|
+
const records = []; const pending = [[payload, 'body']];
|
|
39
|
+
while (pending.length) {
|
|
40
|
+
const [value, path] = pending.pop();
|
|
41
|
+
if (!value || typeof value !== 'object') continue;
|
|
42
|
+
for (const [name, child] of Object.entries(value)) {
|
|
43
|
+
const next = `${path}.${Array.isArray(value) ? '*' : name}`;
|
|
44
|
+
if (billingKey(name)) records.push([next, JSON.stringify(child)]);
|
|
45
|
+
else if (child !== null && typeof child === 'object') pending.push([child, next]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Only actual existing system billing blocks are identity-protected. Prompt
|
|
49
|
+
// prose mentioning the header name remains ordinary user configuration.
|
|
50
|
+
const system = payload.system;
|
|
51
|
+
if (billingBlock(system)) records.push(['body.system.billing', JSON.stringify({ type: 'text', text: system })]);
|
|
52
|
+
if (Array.isArray(system)) for (const block of system) {
|
|
53
|
+
if (block?.type === 'text' && billingBlock(block.text)) records.push(['body.system.billing', JSON.stringify(block)]);
|
|
54
|
+
}
|
|
55
|
+
return JSON.stringify(records.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))));
|
|
56
|
+
}
|
|
57
|
+
function headerText(value, path) {
|
|
58
|
+
text(value, path);
|
|
59
|
+
if (/[^\u0020-\u00ff]/u.test(value)) throw error(path);
|
|
60
|
+
let cursor = 0;
|
|
61
|
+
while (cursor < value.length) {
|
|
62
|
+
const open = value.indexOf('{{', cursor); const close = value.indexOf('}}', cursor);
|
|
63
|
+
if (open < 0 && close < 0) break;
|
|
64
|
+
if (open < 0 || (close >= 0 && close < open)) throw error(path);
|
|
65
|
+
const end = value.indexOf('}}', open + 2);
|
|
66
|
+
if (end < 0 || !VARIABLES.has(value.slice(open + 2, end))) throw error(path);
|
|
67
|
+
cursor = end + 2;
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function json(value, path, depth = 0) {
|
|
72
|
+
if (depth > 32) throw error(path);
|
|
73
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
74
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
75
|
+
if (Array.isArray(value)) return value.map(v => json(v, path, depth + 1));
|
|
76
|
+
if (!isObject(value)) throw error(path);
|
|
77
|
+
const out = {};
|
|
78
|
+
for (const [key, val] of Object.entries(value)) {
|
|
79
|
+
if (BAD_KEYS.has(key) || billingKey(key)) throw error(path);
|
|
80
|
+
out[key] = json(val, path, depth + 1);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function parameters(raw = {}) {
|
|
85
|
+
known(raw, ['defaults', 'overrides', 'remove'], 'body.parameters');
|
|
86
|
+
const result = {};
|
|
87
|
+
for (const section of ['defaults', 'overrides']) {
|
|
88
|
+
const values = optional(raw[section], {});
|
|
89
|
+
if (!isObject(values) || Object.keys(values).some(k => BUSINESS.has(k))) throw error(`body.parameters.${section}`);
|
|
90
|
+
result[section] = json(values, `body.parameters.${section}`);
|
|
91
|
+
}
|
|
92
|
+
const remove = optional(raw.remove, []);
|
|
93
|
+
if (!Array.isArray(remove) || remove.length > 256) throw error('body.parameters.remove');
|
|
94
|
+
result.remove = remove.map(path => {
|
|
95
|
+
if (typeof path !== 'string' || path.length > 512 || !/^[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z_][A-Za-z0-9_-]*)*$/.test(path)) throw error('body.parameters.remove');
|
|
96
|
+
const keys = path.split('.');
|
|
97
|
+
if (BUSINESS.has(keys[0]) || keys.some(k => BAD_KEYS.has(k) || billingKey(k))) throw error('body.parameters.remove');
|
|
98
|
+
return path;
|
|
99
|
+
});
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function validateTemplate(raw) {
|
|
104
|
+
known(raw, ['schemaVersion', 'id', 'name', 'revision', 'kind', 'basedOn', 'protocols', 'operations', 'identity', 'headers', 'body', 'limits', 'archived'], 'template');
|
|
105
|
+
if (raw.schemaVersion !== 1) throw error('schemaVersion');
|
|
106
|
+
if (!Number.isSafeInteger(raw.revision) || raw.revision < 1) throw error('revision');
|
|
107
|
+
const out = { schemaVersion: 1, id: text(raw.id, 'id', 160, false), name: text(raw.name, 'name', 160, false), revision: raw.revision, kind: enumeration(raw.kind, ['builtin', 'custom'], 'kind') };
|
|
108
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(out.id)) throw error('id');
|
|
109
|
+
if (raw.archived !== undefined) {
|
|
110
|
+
if (typeof raw.archived !== 'boolean') throw error('archived');
|
|
111
|
+
out.archived = raw.archived;
|
|
112
|
+
}
|
|
113
|
+
if (raw.basedOn !== undefined) {
|
|
114
|
+
known(raw.basedOn, ['id', 'revision'], 'basedOn');
|
|
115
|
+
if (!Number.isSafeInteger(raw.basedOn.revision) || raw.basedOn.revision < 1) throw error('basedOn.revision');
|
|
116
|
+
out.basedOn = { id: text(raw.basedOn.id, 'basedOn.id', 160, false), revision: raw.basedOn.revision };
|
|
117
|
+
}
|
|
118
|
+
if (!Array.isArray(raw.protocols) || !raw.protocols.length || raw.protocols.length > 4) throw error('protocols');
|
|
119
|
+
out.protocols = [...new Set(raw.protocols.map(p => enumeration(p, Object.keys(OPERATIONS), 'protocols')))];
|
|
120
|
+
if (!Array.isArray(raw.operations) || !raw.operations.length || raw.operations.length > 7) throw error('operations');
|
|
121
|
+
out.operations = [...new Set(raw.operations.map(o => enumeration(o, out.protocols.flatMap(p => OPERATIONS[p]), 'operations')))];
|
|
122
|
+
const identity = optional(raw.identity, {});
|
|
123
|
+
known(identity, ['detector', 'mode', 'session', 'userAgent', 'version'], 'identity');
|
|
124
|
+
out.identity = {
|
|
125
|
+
detector: enumeration(optional(identity.detector, 'none'), ['codex-ua', 'claude-ua', 'none'], 'identity.detector'),
|
|
126
|
+
mode: enumeration(optional(identity.mode, 'preserve'), ['reference-compatible', 'preserve'], 'identity.mode'),
|
|
127
|
+
session: enumeration(optional(identity.session, 'preserve'), ['reference-stable', 'host', 'preserve', 'off'], 'identity.session'),
|
|
128
|
+
userAgent: headerText(optional(identity.userAgent, ''), 'identity.userAgent'), version: headerText(optional(identity.version, ''), 'identity.version'),
|
|
129
|
+
};
|
|
130
|
+
if (!Array.isArray(raw.headers) || raw.headers.length > 128) throw error('headers');
|
|
131
|
+
let headerBytes = 0;
|
|
132
|
+
out.headers = raw.headers.map((h, i) => {
|
|
133
|
+
const path = `headers[${i}]`;
|
|
134
|
+
known(h, ['name', 'action', 'value', 'enabled', 'when', 'operations'], path);
|
|
135
|
+
if (typeof h.name !== 'string' || h.name.length > 256 || !TOKEN.test(h.name)) throw error(`${path}.name`);
|
|
136
|
+
const name = h.name.toLowerCase();
|
|
137
|
+
if (protectedHeader(name) || name === 'content-encoding') throw error(`${path}.name`);
|
|
138
|
+
const action = enumeration(h.action, ['set', 'set-if-missing', 'set-if-empty', 'delete', 'preserve'], `${path}.action`);
|
|
139
|
+
if (h.enabled !== undefined && typeof h.enabled !== 'boolean') throw error(`${path}.enabled`);
|
|
140
|
+
const entry = { name, action, enabled: optional(h.enabled, true), when: enumeration(optional(h.when, 'always'), ['always', 'native', 'third-party'], `${path}.when`) };
|
|
141
|
+
if (h.operations !== undefined) {
|
|
142
|
+
if (!Array.isArray(h.operations) || !h.operations.length || h.operations.length > 7) throw error(`${path}.operations`);
|
|
143
|
+
entry.operations = [...new Set(h.operations.map(o => enumeration(o, out.operations, `${path}.operations`)))];
|
|
144
|
+
}
|
|
145
|
+
if (h.value !== undefined) entry.value = h.value === null ? null : headerText(h.value, `${path}.value`);
|
|
146
|
+
if (!['delete', 'preserve'].includes(action) && h.value === undefined) throw error(`${path}.value`);
|
|
147
|
+
headerBytes += name.length + (entry.value?.length ?? 0) + 4;
|
|
148
|
+
return entry;
|
|
149
|
+
});
|
|
150
|
+
if (headerBytes > 65536) throw error('headers');
|
|
151
|
+
const body = optional(raw.body, {});
|
|
152
|
+
known(body, ['codexMetadata', 'claudeCompatibility', 'identityprompt', 'fastBeta', 'parameters'], 'body');
|
|
153
|
+
if (body.codexMetadata !== undefined && typeof body.codexMetadata !== 'boolean') throw error('body.codexMetadata');
|
|
154
|
+
if (body.fastBeta !== undefined && typeof body.fastBeta !== 'boolean') throw error('body.fastBeta');
|
|
155
|
+
out.body = { codexMetadata: optional(body.codexMetadata, false), claudeCompatibility: enumeration(optional(body.claudeCompatibility, 'off'), ['third-party-messages', 'off'], 'body.claudeCompatibility'), fastBeta: optional(body.fastBeta, false), parameters: parameters(body.parameters) };
|
|
156
|
+
if (body.identityprompt !== undefined) {
|
|
157
|
+
if (typeof body.identityprompt !== 'string' || !body.identityprompt.trim() || body.identityprompt.length > 8192 || body.identityprompt.includes('\0')) throw error('body.identityprompt');
|
|
158
|
+
out.body.identityprompt = body.identityprompt;
|
|
159
|
+
}
|
|
160
|
+
// Reuse Messages' established compile-time parameter restrictions.
|
|
161
|
+
if (out.protocols.includes('anthropic')) {
|
|
162
|
+
try { compileParameters(out.body.parameters); } catch { throw error('body.parameters', 'Messages parameter rules are invalid'); }
|
|
163
|
+
}
|
|
164
|
+
const limits = optional(raw.limits, {});
|
|
165
|
+
known(limits, Object.keys(LIMITS), 'limits'); out.limits = {};
|
|
166
|
+
for (const key of Object.keys(LIMITS)) {
|
|
167
|
+
const max = key === 'maxBodyBytes' ? 64 << 20 : key === 'maxMetadataBytes' ? 4 << 20 : 60000;
|
|
168
|
+
const value = optional(limits[key], LIMITS[key]);
|
|
169
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > max) throw error(`limits.${key}`);
|
|
170
|
+
out.limits[key] = value;
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function header(name, action, value, when = 'always') { return { name, action, ...(value === undefined ? {} : { value }), enabled: true, when }; }
|
|
176
|
+
export function builtinTemplates() {
|
|
177
|
+
const make = (id, name, protocol, detector, version, userAgent) => ({
|
|
178
|
+
schemaVersion: 1, id, name, revision: 1, kind: 'builtin', protocols: [protocol], operations: [...OPERATIONS[protocol]],
|
|
179
|
+
identity: { detector, mode: 'reference-compatible', session: 'reference-stable', userAgent, version }, headers: [],
|
|
180
|
+
body: { codexMetadata: false, claudeCompatibility: 'off', fastBeta: false, parameters: { defaults: {}, overrides: {}, remove: [] } }, limits: { ...LIMITS },
|
|
181
|
+
});
|
|
182
|
+
const codex = make('builtin.codex', 'Codex', 'responses', 'codex-ua', CODEX_VERSION, 'codex_cli_rs/{{client.version}} ({{platform}}; {{arch}}) {{terminal}}');
|
|
183
|
+
codex.headers = [header('openai-beta', 'set-if-missing', 'responses=experimental'), header('originator', 'set-if-missing', 'codex_cli_rs'), header('version', 'set-if-missing', '{{client.version}}')];
|
|
184
|
+
codex.body.codexMetadata = true;
|
|
185
|
+
const claude = make('builtin.claude-code', 'Claude Code', 'anthropic', 'claude-ua', CLAUDE_VERSION, `claude-cli/${CLAUDE_VERSION} (external, cli)`);
|
|
186
|
+
claude.headers = [header('x-app', 'set-if-missing', 'cli'), header('anthropic-version', 'set-if-missing', '2023-06-01')];
|
|
187
|
+
// These empty-value rules apply to third-party Messages inference only.
|
|
188
|
+
claude.headers.push(...[header('x-app', 'set-if-empty', 'cli', 'third-party'), header('anthropic-version', 'set-if-empty', '2023-06-01', 'third-party'), header('anthropic-beta', 'set-if-empty', 'claude-code-20250219', 'third-party')].map(h => ({ ...h, operations: ['messages'] })));
|
|
189
|
+
claude.body.claudeCompatibility = 'third-party-messages'; claude.body.fastBeta = true; claude.body.identityprompt = IDENTITY_PROMPT;
|
|
190
|
+
const custom = make('custom.claude-vscode-2316', 'Claude Code VS Code 2.1.316', 'anthropic', 'claude-ua', '2.1.316', 'claude-cli/2.1.316 (external, claude-vscode, agent-sdk/0.3.316)');
|
|
191
|
+
custom.kind = 'custom'; custom.basedOn = { id: claude.id, revision: 1 }; custom.identity.mode = 'preserve'; custom.identity.session = 'preserve';
|
|
192
|
+
custom.headers = Object.entries({ accept: 'application/json', 'anthropic-beta': VS_BETA, 'anthropic-dangerous-direct-browser-access': 'true', 'anthropic-version': '2023-06-01', 'user-agent': custom.identity.userAgent, 'x-app': 'cli', 'x-stainless-arch': 'x64', 'x-stainless-lang': 'js', 'x-stainless-os': 'Windows', 'x-stainless-package-version': '0.128.0', 'x-stainless-retry-count': '0', 'x-stainless-runtime': 'node', 'x-stainless-runtime-version': 'v26.12.1', 'x-stainless-timeout': '300' }).map(([n, v]) => header(n, 'set', v));
|
|
193
|
+
return [codex, claude, custom];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const hash = (...parts) => createHash('sha256').update(JSON.stringify(parts)).digest('hex');
|
|
197
|
+
function uuid(...parts) {
|
|
198
|
+
const h = hash('codex-identity', ...parts).slice(0, 32);
|
|
199
|
+
const id = h.slice(0, 12) + '4' + h.slice(13, 16) + (8 + (parseInt(h[16], 16) & 3)).toString(16) + h.slice(17);
|
|
200
|
+
return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
|
|
201
|
+
}
|
|
202
|
+
function nativeCaller(detector, headers) {
|
|
203
|
+
const ua = (headers.get('user-agent') ?? '').trim().toLowerCase();
|
|
204
|
+
if (detector === 'claude-ua') return /^claude-cli\/\d+\.\d+\.\d+/.test(ua);
|
|
205
|
+
if (detector !== 'codex-ua') return false;
|
|
206
|
+
const slash = ua.indexOf('/');
|
|
207
|
+
if (slash < 0 || !/^\d+\.\d+\.\d+/.test(ua.slice(slash + 1).split(/[ (]/)[0])) return false;
|
|
208
|
+
if (CODEX_FAMILY.test(ua.slice(0, slash)) || ua.startsWith('codex ')) return true;
|
|
209
|
+
const open = ua.lastIndexOf('('); const close = ua.indexOf(')', open + 1);
|
|
210
|
+
const trailer = open >= 0 && close >= 0 ? ua.slice(open + 1, close).split(';')[0].trim() : '';
|
|
211
|
+
return CODEX_FAMILY.test(trailer) || trailer.startsWith('codex ');
|
|
212
|
+
}
|
|
213
|
+
function signalAbort(signal) {
|
|
214
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException('Request aborted', 'AbortError');
|
|
215
|
+
}
|
|
216
|
+
// Race each asynchronous read, including Blob.text. Cancel clone readers without
|
|
217
|
+
// awaiting tee cancellation: it may wait for the untouched original Request.
|
|
218
|
+
async function readBody(input, init, max, timeoutMs, signal) {
|
|
219
|
+
signalAbort(signal);
|
|
220
|
+
const source = init.body ?? (input instanceof Request ? input.body : undefined);
|
|
221
|
+
if (source === undefined || source === null) return { reason: 'absent' };
|
|
222
|
+
const failSize = () => ({ reason: 'size' });
|
|
223
|
+
if (typeof source === 'string') return Buffer.byteLength(source) <= max ? { text: source } : failSize();
|
|
224
|
+
if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) {
|
|
225
|
+
if (source.byteLength > max) return failSize();
|
|
226
|
+
const bytes = ArrayBuffer.isView(source) ? new Uint8Array(source.buffer, source.byteOffset, source.byteLength) : new Uint8Array(source);
|
|
227
|
+
return { text: new TextDecoder().decode(bytes) };
|
|
228
|
+
}
|
|
229
|
+
const blob = source instanceof Blob;
|
|
230
|
+
if (blob && source.size > max) return failSize();
|
|
231
|
+
if (!blob && (init.body != null || !(input instanceof Request) || input.bodyUsed || !input.body)) return { reason: 'stream' };
|
|
232
|
+
let reader; let timer; let onAbort;
|
|
233
|
+
const stop = new Promise((resolve, reject) => {
|
|
234
|
+
timer = setTimeout(() => resolve({ reason: 'timeout' }), timeoutMs);
|
|
235
|
+
onAbort = () => reject(signal.reason ?? new DOMException('Request aborted', 'AbortError'));
|
|
236
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
237
|
+
});
|
|
238
|
+
const work = async () => {
|
|
239
|
+
if (blob) {
|
|
240
|
+
const value = await source.text();
|
|
241
|
+
return typeof value === 'string' && Buffer.byteLength(value) <= max ? { text: value } : failSize();
|
|
242
|
+
}
|
|
243
|
+
reader = input.clone().body.getReader();
|
|
244
|
+
const chunks = []; let size = 0;
|
|
245
|
+
while (true) {
|
|
246
|
+
const { done, value } = await reader.read();
|
|
247
|
+
signalAbort(signal);
|
|
248
|
+
if (done) return { text: Buffer.concat(chunks, size).toString('utf8') };
|
|
249
|
+
size += value.byteLength;
|
|
250
|
+
if (size > max) return failSize();
|
|
251
|
+
chunks.push(Buffer.from(value));
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
try { return await Promise.race([work(), stop]); }
|
|
255
|
+
catch (e) { signalAbort(signal); return { reason: 'unreadable' }; }
|
|
256
|
+
finally {
|
|
257
|
+
clearTimeout(timer); signal?.removeEventListener('abort', onAbort);
|
|
258
|
+
if (reader) {
|
|
259
|
+
try { void reader.cancel().catch(() => {}); reader.releaseLock(); } catch { /* already closed */ }
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function validUserID(value) {
|
|
264
|
+
if (typeof value !== 'string' || !value.trim()) return false;
|
|
265
|
+
const raw = value.trim();
|
|
266
|
+
if (raw[0] !== '{') return /^user_([a-fA-F0-9]{64})_account_([a-fA-F0-9-]*)_session_([a-fA-F0-9-]{36})$/.test(raw);
|
|
267
|
+
let parsed; try { parsed = JSON.parse(raw); } catch { return false; }
|
|
268
|
+
if (!isObject(parsed)) return false;
|
|
269
|
+
const fields = Object.create(null);
|
|
270
|
+
for (const [name, field] of Object.entries(parsed)) {
|
|
271
|
+
const key = name.toLowerCase();
|
|
272
|
+
if (!['device_id', 'account_uuid', 'session_id'].includes(key) || field === null) continue;
|
|
273
|
+
if (typeof field !== 'string') return false;
|
|
274
|
+
fields[key] = field;
|
|
275
|
+
}
|
|
276
|
+
return Boolean(fields.device_id && fields.session_id);
|
|
277
|
+
}
|
|
278
|
+
function systemBlocks(system, prompt) {
|
|
279
|
+
if (system === undefined || system === null || system === '') return [{ type: 'text', text: prompt }];
|
|
280
|
+
const matches = value => typeof value === 'string' && value.length <= Math.max(512, prompt.length * 2) && value.split(/\s+/).join(' ').trim() === prompt.split(/\s+/).join(' ').trim();
|
|
281
|
+
if (typeof system === 'string') return matches(system) ? [{ type: 'text', text: system }] : [{ type: 'text', text: prompt }, { type: 'text', text: system }];
|
|
282
|
+
if (!Array.isArray(system)) throw error('body.system', 'Messages system must be a string or block array');
|
|
283
|
+
return system.some(b => matches(b?.text)) ? undefined : [{ type: 'text', text: prompt }, ...system];
|
|
284
|
+
}
|
|
285
|
+
function mergeJSON(target, source, defaults) {
|
|
286
|
+
for (const [key, value] of Object.entries(source)) {
|
|
287
|
+
if (!Object.hasOwn(target, key)) target[key] = structuredClone(value);
|
|
288
|
+
else if (isObject(value) && isObject(target[key])) mergeJSON(target[key], value, defaults);
|
|
289
|
+
else if (!defaults) target[key] = structuredClone(value);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function applyGeneric(payload, plan) {
|
|
293
|
+
const result = structuredClone(payload);
|
|
294
|
+
mergeJSON(result, plan.defaults, true); mergeJSON(result, plan.overrides, false);
|
|
295
|
+
for (const path of plan.remove) {
|
|
296
|
+
const keys = path.split('.'); let parent = result;
|
|
297
|
+
for (const key of keys.slice(0, -1)) { parent = isObject(parent) && Object.hasOwn(parent, key) ? parent[key] : undefined; }
|
|
298
|
+
if (isObject(parent)) delete parent[keys.at(-1)];
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
function render(value, variables) {
|
|
303
|
+
let missing = false;
|
|
304
|
+
const output = value.replace(/\{\{([^{}]+)\}\}/g, (_, key) => {
|
|
305
|
+
const replacement = variables[key];
|
|
306
|
+
if (replacement === undefined || replacement === null || replacement === '') { missing = true; return ''; }
|
|
307
|
+
return String(replacement);
|
|
308
|
+
});
|
|
309
|
+
if (missing) return undefined;
|
|
310
|
+
// Substituted context is literal HTTP text, not another template to compile.
|
|
311
|
+
try {
|
|
312
|
+
text(output, 'context');
|
|
313
|
+
if (/[^\u0020-\u00ff]/u.test(output)) throw error('context');
|
|
314
|
+
return output;
|
|
315
|
+
} catch { throw error('context', 'Request context cannot be used in an HTTP header'); }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export async function transformRequest({ template, protocol, operation, input, init = {}, context = {} }) {
|
|
319
|
+
if (!Object.hasOwn(OPERATIONS, protocol) || !OPERATIONS[protocol].includes(operation)) throw error('operation', 'Protocol and operation do not match');
|
|
320
|
+
const t = template == null ? null : validateTemplate(template);
|
|
321
|
+
if (t && (!t.protocols.includes(protocol) || !t.operations.includes(operation))) throw error('template', 'Template does not match protocol and operation');
|
|
322
|
+
const original = new Headers(input instanceof Request ? input.headers : undefined);
|
|
323
|
+
new Headers(init.headers).forEach((value, name) => original.set(name, value));
|
|
324
|
+
const headers = new Headers(original);
|
|
325
|
+
const ua = (original.get('user-agent') ?? '').trim();
|
|
326
|
+
const visibleUA = Boolean(ua && !/^magpie(?:[/-]|$)/i.test(ua));
|
|
327
|
+
const native = t ? nativeCaller(t.identity.detector, original) : false;
|
|
328
|
+
const sessionHeader = SESSION_HEADERS.map(n => original.get(n)?.trim()).find(Boolean);
|
|
329
|
+
const hostSession = typeof context.sessionID === 'string' && context.sessionID.trim() ? context.sessionID : undefined;
|
|
330
|
+
const summary = { templateId: t?.id ?? null, revision: t?.revision ?? null, callerIdentity: native ? 'native' : visibleUA ? 'third-party' : 'unknown', sourceAvailability: { userAgent: visibleUA ? 'available' : 'unknown', session: hostSession ? 'host' : sessionHeader ? 'inbound' : 'unknown', credentialScope: context.credentialScope ? 'available' : 'unknown' }, changes: [] };
|
|
331
|
+
const change = (path, rule, action) => summary.changes.push({ path, rule, action });
|
|
332
|
+
const set = (name, value, rule) => {
|
|
333
|
+
if (headers.get(name) !== value) { headers.set(name, value); change(`headers.${name}`, rule, 'set'); }
|
|
334
|
+
};
|
|
335
|
+
const del = (name, rule) => { if (headers.has(name)) { headers.delete(name); change(`headers.${name}`, rule, 'delete'); } };
|
|
336
|
+
const signal = init.signal ?? (input instanceof Request ? input.signal : undefined);
|
|
337
|
+
signalAbort(signal);
|
|
338
|
+
let resultBody; let rewritten = false;
|
|
339
|
+
if (t) {
|
|
340
|
+
const mode = t.identity.mode === 'reference-compatible';
|
|
341
|
+
const sessionMode = t.identity.session;
|
|
342
|
+
const messages = protocol === 'anthropic' && operation === 'messages';
|
|
343
|
+
const complete = messages && !native && t.body.claudeCompatibility === 'third-party-messages';
|
|
344
|
+
const plan = t.body.parameters;
|
|
345
|
+
const active = !['count_tokens', 'countTokens'].includes(operation) && (Object.keys(plan.defaults).length || Object.keys(plan.overrides).length || plan.remove.length);
|
|
346
|
+
const method = (init.method ?? (input instanceof Request ? input.method : 'POST')).toUpperCase();
|
|
347
|
+
const hasBody = init.body != null || (input instanceof Request && input.body !== null);
|
|
348
|
+
const canBody = !['GET', 'HEAD'].includes(method) && hasBody;
|
|
349
|
+
const required = canBody && (complete || active);
|
|
350
|
+
const encoded = headers.has('content-encoding') && headers.get('content-encoding').toLowerCase().trim() !== 'identity';
|
|
351
|
+
if (required && encoded) throw error('body', 'Request body must be uncompressed for body transformation');
|
|
352
|
+
let payload;
|
|
353
|
+
const needsBodyVariables = t.headers.some(h => h.enabled && typeof h.value === 'string' && (h.value.includes('{{model.id}}') || h.value.includes('{{reasoning.effort}}')));
|
|
354
|
+
const inspect = required || (canBody && ((t.body.codexMetadata && protocol === 'responses') || (messages && t.body.fastBeta) || needsBodyVariables));
|
|
355
|
+
if (inspect && !encoded) {
|
|
356
|
+
const read = await readBody(input, init, required ? t.limits.maxBodyBytes : t.limits.maxMetadataBytes, t.limits.bodyTimeoutMs, signal);
|
|
357
|
+
signalAbort(signal);
|
|
358
|
+
if (read.text !== undefined) {
|
|
359
|
+
try { payload = JSON.parse(read.text); } catch { /* checked below */ }
|
|
360
|
+
if (!isObject(payload)) payload = undefined;
|
|
361
|
+
}
|
|
362
|
+
if (required && !payload) {
|
|
363
|
+
const reasons = { size: 'Request body exceeds the byte limit', timeout: 'Request body read timeout', stream: 'Streaming upload body cannot be transformed', unreadable: 'Request body is unreadable', absent: 'Request body is required' };
|
|
364
|
+
throw error('body', reasons[read.reason] ?? 'Request body must be a JSON object');
|
|
365
|
+
}
|
|
366
|
+
if (!payload) change('body', `inspection-${read.reason ?? 'invalid-json'}`, 'skip');
|
|
367
|
+
}
|
|
368
|
+
const protectedBilling = payload && required ? billingSnapshot(payload) : undefined;
|
|
369
|
+
const bodyModel = typeof payload?.model === 'string' && payload.model.trim() ? payload.model.trim() : undefined;
|
|
370
|
+
const scope = typeof context.credentialScope === 'string' ? context.credentialScope : '';
|
|
371
|
+
const base = typeof context.baseURL === 'string' ? context.baseURL : '';
|
|
372
|
+
if (t.body.codexMetadata && protocol === 'responses' && payload) {
|
|
373
|
+
let size = 0;
|
|
374
|
+
const promote = (name, value) => {
|
|
375
|
+
const lower = name.toLowerCase();
|
|
376
|
+
if (headers.has(lower) || headers.has(lower.replace(/-/g, '_')) || protectedHeader(lower) || typeof value !== 'string' || !value.trim() || size + name.length + value.length + 4 > 8192) return;
|
|
377
|
+
try { headerText(value, 'metadata'); set(lower, value, 'codex-metadata'); size += name.length + value.length + 4; } catch { /* untrusted metadata is optional */ }
|
|
378
|
+
};
|
|
379
|
+
if (isObject(payload.client_metadata)) for (const [field, value] of Object.entries(payload.client_metadata)) {
|
|
380
|
+
const name = field === 'session_id' ? 'session-id' : field === 'thread_id' ? 'thread-id' : field;
|
|
381
|
+
if (['session-id', 'thread-id'].includes(name) || /^x-codex-[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i.test(name)) promote(name, value);
|
|
382
|
+
}
|
|
383
|
+
promote('session-id', payload.prompt_cache_key); promote('thread-id', payload.prompt_cache_key);
|
|
384
|
+
}
|
|
385
|
+
let version = t.identity.version;
|
|
386
|
+
if (t.identity.detector === 'codex-ua') {
|
|
387
|
+
const given = (original.get('version') ?? '').trim();
|
|
388
|
+
const fromUA = /^(?:codex[^/\s]*|acp-extension-codex|Codex Desktop)\/(\d+(?:\.\d+){1,3})(?=\s|$)/i.exec(ua)?.[1];
|
|
389
|
+
version = /^\d+(?:\.\d+){1,3}$/.test(given) ? given : fromUA ?? version;
|
|
390
|
+
}
|
|
391
|
+
const platform = context.platform ?? process.platform;
|
|
392
|
+
const variables = { 'session.id': hostSession, 'request.id': context.requestId, 'client.version': version, 'model.id': context.model ?? bodyModel, 'reasoning.effort': payload?.output_config?.effort, timestamp: typeof context.now === 'function' ? context.now() : context.now ?? Date.now(), platform: platform === 'darwin' ? 'Mac OS' : platform === 'win32' ? 'Windows' : 'Linux', arch: (context.arch ?? process.arch) === 'x64' ? 'x86_64' : context.arch ?? process.arch, terminal: platform === 'darwin' ? 'Apple_Terminal/455' : platform === 'win32' ? 'WindowsTerminal' : 'xterm-256color' };
|
|
393
|
+
if (mode && !native && t.identity.userAgent) {
|
|
394
|
+
const value = render(t.identity.userAgent, variables);
|
|
395
|
+
if (value !== undefined) set('user-agent', value, 'identity-user-agent');
|
|
396
|
+
}
|
|
397
|
+
if (complete && canBody && payload) {
|
|
398
|
+
const system = systemBlocks(payload.system, t.body.identityprompt ?? IDENTITY_PROMPT);
|
|
399
|
+
const metadata = isObject(payload.metadata) ? payload.metadata : {};
|
|
400
|
+
const valid = validUserID(metadata.user_id);
|
|
401
|
+
if (system !== undefined || !valid) {
|
|
402
|
+
payload = { ...payload, ...(system === undefined ? {} : { system }) };
|
|
403
|
+
if (system !== undefined) change('body.system', 'claude-compatibility', 'complete');
|
|
404
|
+
if (!valid) {
|
|
405
|
+
const session = sessionMode === 'off' ? '' : hostSession ?? sessionHeader ?? '';
|
|
406
|
+
if (!hostSession && !sessionHeader) change('context.session', 'claude-session-fallback-unknown', 'derive');
|
|
407
|
+
payload.metadata = { ...metadata, user_id: JSON.stringify({ device_id: hash('claude-device', base, scope), account_uuid: uuid(base, scope, 'claude-account'), session_id: uuid(base, scope, session, 'claude-session') }) };
|
|
408
|
+
change('body.metadata.user_id', 'claude-compatibility', 'complete');
|
|
409
|
+
}
|
|
410
|
+
rewritten = true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (active && canBody && payload) {
|
|
414
|
+
try {
|
|
415
|
+
const next = protocol === 'anthropic' && messages ? applyParameters(payload, compileParameters(plan)) : applyGeneric(payload, plan);
|
|
416
|
+
if (JSON.stringify(next) !== JSON.stringify(payload)) { payload = next; rewritten = true; change('body', 'explicit-parameters', 'merge'); }
|
|
417
|
+
} catch { throw error('body.parameters', 'Request parameter transformation is invalid'); }
|
|
418
|
+
}
|
|
419
|
+
if (messages && mode && !native && ['reference-stable', 'host'].includes(sessionMode) && !headers.get('x-claude-code-session-id')?.trim()) {
|
|
420
|
+
let bodySession;
|
|
421
|
+
const userID = payload?.metadata?.user_id;
|
|
422
|
+
if (typeof userID === 'string') {
|
|
423
|
+
try {
|
|
424
|
+
const identity = JSON.parse(userID);
|
|
425
|
+
const field = Object.keys(identity ?? {}).find(name => name.toLowerCase() === 'session_id');
|
|
426
|
+
if (field && typeof identity[field] === 'string') bodySession = identity[field];
|
|
427
|
+
} catch { bodySession = /_session_([a-fA-F0-9-]{36})$/.exec(userID)?.[1]; }
|
|
428
|
+
}
|
|
429
|
+
const session = bodySession || hostSession || sessionHeader || (sessionMode === 'reference-stable' ? uuid(base, scope, '', 'claude-session') : undefined);
|
|
430
|
+
if (session) {
|
|
431
|
+
text(session, 'context.session', 512, false);
|
|
432
|
+
set('x-claude-code-session-id', session, bodySession ? 'claude-identity-session' : hostSession ? 'host-session' : 'stable-session');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (const h of t.headers) {
|
|
436
|
+
if (!h.enabled || (h.when === 'native' && !native) || (h.when === 'third-party' && native)) continue;
|
|
437
|
+
if (h.operations && !h.operations.includes(operation)) continue;
|
|
438
|
+
if (h.action === 'preserve') continue;
|
|
439
|
+
if (h.action === 'delete' || h.value === null) { del(h.name, 'template-header'); continue; }
|
|
440
|
+
if (h.action === 'set-if-missing' && headers.has(h.name)) continue;
|
|
441
|
+
if (h.action === 'set-if-empty' && headers.get(h.name)?.trim()) continue;
|
|
442
|
+
const value = render(h.value, variables);
|
|
443
|
+
if (value === undefined) { change(`headers.${h.name}`, 'template-variable-unavailable', 'skip'); continue; }
|
|
444
|
+
set(h.name, value, 'template-header');
|
|
445
|
+
}
|
|
446
|
+
if (protocol === 'responses' && !native && ['reference-stable', 'host'].includes(sessionMode)) {
|
|
447
|
+
const given = (headers.get('session-id') ?? headers.get('session_id') ?? '').trim();
|
|
448
|
+
const conversation = hostSession ?? (headers.get('x-session-id') ?? headers.get('x-opencode-session') ?? '').trim();
|
|
449
|
+
const conversationScope = conversation || bodyModel || context.model || '';
|
|
450
|
+
const session = given || hostSession || (sessionMode === 'reference-stable' ? uuid(base, scope, conversationScope, 'session') : undefined);
|
|
451
|
+
if (session) {
|
|
452
|
+
if (!headers.has('session-id')) set('session-id', session, hostSession ? 'host-session' : 'stable-session');
|
|
453
|
+
if (!headers.has('thread-id')) set('thread-id', session, 'stable-session');
|
|
454
|
+
}
|
|
455
|
+
if (sessionMode === 'reference-stable') {
|
|
456
|
+
if (!headers.has('x-codex-window-id')) set('x-codex-window-id', uuid(base, scope, conversationScope, 'window'), 'stable-identity');
|
|
457
|
+
if (!headers.has('x-codex-installation-id')) set('x-codex-installation-id', uuid(base, scope, 'installation'), 'stable-identity');
|
|
458
|
+
if (!hostSession && !sessionHeader) change('context.session', 'codex-session-fallback-unknown', 'derive');
|
|
459
|
+
if (!scope) change('context.credentialScope', 'credential-scope-fallback-unknown', 'derive');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (messages && t.body.fastBeta && payload?.speed === 'fast') {
|
|
463
|
+
const beta = headers.get('anthropic-beta');
|
|
464
|
+
if (!beta?.split(',').some(v => v.trim() === FAST_BETA)) set('anthropic-beta', beta ? `${beta},${FAST_BETA}` : FAST_BETA, 'fast-beta');
|
|
465
|
+
}
|
|
466
|
+
if (rewritten) {
|
|
467
|
+
if (protectedBilling !== undefined && billingSnapshot(payload) !== protectedBilling) throw error('body.parameters', 'Existing billing fields and blocks must remain unchanged');
|
|
468
|
+
resultBody = JSON.stringify(payload);
|
|
469
|
+
if (Buffer.byteLength(resultBody) > t.limits.maxBodyBytes) throw error('body', 'Transformed body exceeds the byte limit');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// Host-owned invariants run last, after all optional template rules.
|
|
473
|
+
const nominated = (original.get('connection') ?? '').split(',').map(v => v.trim().toLowerCase()).filter(v => TOKEN.test(v));
|
|
474
|
+
for (const name of [...headers.keys()]) if (protectedHeader(name) || nominated.includes(name)) del(name, 'transport-cleanup');
|
|
475
|
+
if (rewritten) for (const name of ['content-encoding', 'digest', 'content-digest', 'repr-digest', 'content-md5']) del(name, 'body-rewritten');
|
|
476
|
+
signalAbort(signal);
|
|
477
|
+
return { input, init: { ...init, ...(signal ? { signal } : {}), ...(rewritten ? { body: resultBody } : {}), headers }, summary };
|
|
478
|
+
}
|
package/src/ua-usage.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { matchesAccountSite } from '../lib/ua-protocol.mjs'
|
|
2
|
+
import { accountUsage, accountCheckin } from './account-usage.mjs'
|
|
3
|
+
|
|
4
|
+
export function createUAUsage({ providerId, input = {}, options = {}, accountManager, getConnection, resolveAccount, queryKeyUsage }) {
|
|
5
|
+
async function selected(getAuth, provider) {
|
|
6
|
+
const auth = typeof getAuth === 'function' ? await getAuth() : null
|
|
7
|
+
const connection = auth ? await getConnection(auth, provider) : null
|
|
8
|
+
if (!connection) return { accountId: null, connection, auth }
|
|
9
|
+
if (typeof auth?.metadata?.newApiAccountId === 'string') return { accountId: auth.metadata.newApiAccountId.trim() || null, connection, auth }
|
|
10
|
+
const slot = input.magpie?.account?.(providerId) ?? await resolveAccount?.(auth, connection)
|
|
11
|
+
if (typeof slot === 'string' && Object.hasOwn(options.accountBindings ?? {}, slot)) return { accountId: options.accountBindings[slot], connection, auth }
|
|
12
|
+
return { accountId: connection.newApiAccountId?.trim() || null, connection, auth }
|
|
13
|
+
}
|
|
14
|
+
async function boundAccount(selection) {
|
|
15
|
+
const account = await accountManager.get(selection.accountId)
|
|
16
|
+
if (selection.connection && !matchesAccountSite(selection.connection.baseURL, account)) throw new Error('站点绑定不匹配')
|
|
17
|
+
return account
|
|
18
|
+
}
|
|
19
|
+
const unavailable = message => ({ signIn: 'kept', error: message, windows: [{ name: '站点账号状态', aside: true, display: message }] })
|
|
20
|
+
async function keyFallback(selection, usage, message) {
|
|
21
|
+
if (selection.connection && queryKeyUsage) {
|
|
22
|
+
try {
|
|
23
|
+
const keyUsage = await queryKeyUsage(selection.connection, selection.auth)
|
|
24
|
+
if (keyUsage.balance !== undefined || keyUsage.windows?.some(window => Number.isFinite(window.amount))) {
|
|
25
|
+
return { ...keyUsage, error: `${message};显示已配置连接的 Key 用量,签到凭据需单独核对`,
|
|
26
|
+
windows: [...(keyUsage.windows ?? []), { name: '站点账号状态', aside: true, display: message }] }
|
|
27
|
+
}
|
|
28
|
+
} catch { /* An optional Key reading must not discard the known account snapshot. */ }
|
|
29
|
+
}
|
|
30
|
+
return usage
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
async usage(getAuth, provider = {}) {
|
|
34
|
+
try {
|
|
35
|
+
const selection = await selected(getAuth, provider)
|
|
36
|
+
if (selection.accountId) {
|
|
37
|
+
let account
|
|
38
|
+
try { account = await accountManager.get(selection.accountId) }
|
|
39
|
+
catch { return keyFallback(selection, unavailable('绑定的站点账号无法读取'), '绑定的站点账号无法读取') }
|
|
40
|
+
if (!matchesAccountSite(selection.connection.baseURL, account)) throw new Error('站点绑定不匹配')
|
|
41
|
+
if (!account.enabled) return keyFallback(selection, unavailable('余额与签到账号已停用;聊天不受影响'), '站点账号已停用')
|
|
42
|
+
let refreshed
|
|
43
|
+
try { refreshed = await accountManager.refresh(selection.accountId) }
|
|
44
|
+
catch { return keyFallback(selection, unavailable('站点账号余额未更新'), '站点账号余额未更新') }
|
|
45
|
+
const usage = accountUsage(refreshed)
|
|
46
|
+
return !refreshed.snapshot || refreshed.status?.stale
|
|
47
|
+
? keyFallback(selection, usage, `站点账号余额未更新:${refreshed.status?.error || '尚无账号读数'}`) : usage
|
|
48
|
+
}
|
|
49
|
+
const connection = selection.connection ?? (selection.auth ? await getConnection(selection.auth, provider) : null)
|
|
50
|
+
if (connection && queryKeyUsage) return queryKeyUsage(connection, selection.auth)
|
|
51
|
+
return { signIn: 'kept', windows: [{ name: '可选余额', display: '当前宿主账号未绑定站点账号;聊天不受影响', aside: true }] }
|
|
52
|
+
} catch { return { signIn: 'kept', error: '余额读取未完成,请检查站点账号或连接设置', windows: [{ name: '可选余额', display: '余额读取未完成;模型 API key 不受影响', aside: true }] } }
|
|
53
|
+
},
|
|
54
|
+
async checkin(getAuth, provider = {}) {
|
|
55
|
+
try {
|
|
56
|
+
const selection = await selected(getAuth, provider)
|
|
57
|
+
if (!selection.accountId) return { outcome: 'inactive', message: '请先在 UA PRO 中绑定同站点的余额与签到账号;模型 API key 不替代登录凭据' }
|
|
58
|
+
const account = await boundAccount(selection)
|
|
59
|
+
if (!account.enabled || !account.checkinEnabled) return accountCheckin(account)
|
|
60
|
+
return accountCheckin(await accountManager.checkin(selection.accountId))
|
|
61
|
+
} catch { return { outcome: 'failed', message: '绑定的站点账号签到未完成,请检查账号管理;模型 API key 未改变' } }
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|