creditdaddy 1.4.0 → 1.5.0

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/src/constants.js CHANGED
@@ -1,99 +1,109 @@
1
- /**
2
- * CreditDaddy 常量 — 支持的产品线与 Qoder 官方 OpenAPI 端点(WorkBuddy 端点见 workbuddyClient.js)。
3
- *
4
- * 端点与请求头结构来自社区逆向成果(10router 项目实测可用):
5
- * openapi.qoder.sh 国际版:活动/签到、userinfo、配额、PAT 兑换
6
- * openapi.qoder.com.cn 国内版:同上
7
- *
8
- * ⚠️ 非官方接口,Qoder 调整服务端时可能失效。
9
- */
10
-
11
- import { readFileSync } from 'node:fs';
12
-
13
- export const OPENAPI_BASE = 'https://openapi.qoder.sh';
14
- export const CN_OPENAPI_BASE = 'https://openapi.qoder.com.cn';
15
-
16
- // 网页端(账号设置 → 用量明细)同源 API:逐资源包明细只在这里提供,且只认浏览器登录后的会话 Cookie。
17
- export const WEB_BASE = 'https://qoder.com';
18
- export const CN_WEB_BASE = 'https://qoder.cn';
19
-
20
- export const LOGIN_URL = 'https://qoder.com/device/selectAccounts';
21
- export const CN_LOGIN_URL = 'https://qoder.cn/device/selectAccounts';
22
-
23
- // 活动(签到)列表与领取
24
- export const CAMPAIGNS_PATH = '/sash/api/v1/me/campaigns?clientType=10';
25
- export const CAMPAIGN_CLAIM_PATH = (id) => `/sash/api/v1/me/campaigns/${encodeURIComponent(id)}/claim`;
26
-
27
- // 账号信息与配额
28
- export const USERINFO_PATH = '/api/v1/userinfo';
29
- export const QUOTA_USAGE_PATH = '/api/v2/quota/usage';
30
- // 网页端逐资源包用量明细(plan / 个人资源包 / 组织资源包,含每包到期时间)。
31
- // 仅接受浏览器登录的会话 Cookie(dt- / jt- token 一律 401),见 qoderClient.fetchUsageDetail。
32
- export const WEB_USAGE_PATH = '/api/v2/me/usages/big_model_credits';
33
-
34
- // PAT (pt-...) → 短期 job token (jt-...) 兑换(普通 JSON POST,无需 COSY 签名)
35
- export const JOB_TOKEN_EXCHANGE_PATH = '/api/v1/jobToken/exchange';
36
-
37
- // 账号的 provider = 产品 + 区域。新增产品时在此登记,并在 providers.js 挂上对应实现。
38
- export const PROVIDERS = ['qoder', 'qoder-cn', 'workbuddy', 'workbuddy-intl', 'zcode', 'mirasim', 'catpaw', 'trae', 'minimax'];
39
- export const PROVIDER_LABEL = {
40
- qoder: 'Qoder 国际版',
41
- 'qoder-cn': 'Qoder 国内版',
42
- workbuddy: 'WorkBuddy 国内版',
43
- 'workbuddy-intl': 'WorkBuddy 国际版',
44
- zcode: 'ZCode',
45
- mirasim: 'mirasim',
46
- catpaw: '妙手',
47
- trae: 'Trae',
48
- minimax: 'MiniMax',
49
- };
50
- /** provider → 产品线(qoder / workbuddy / zcode / mirasim / catpaw / trae / minimax) */
51
- export const productOf = (provider) => {
52
- const p = String(provider);
53
- if (p.startsWith('workbuddy')) return 'workbuddy';
54
- if (p.startsWith('zcode')) return 'zcode';
55
- if (p === 'mirasim') return 'mirasim';
56
- if (p === 'catpaw') return 'catpaw';
57
- if (p.startsWith('trae')) return 'trae';
58
- if (p === 'minimax') return 'minimax';
59
- return 'qoder';
60
- };
61
- /** provider 是否国内版 */
62
- export const isCnProvider = (provider) => provider === 'qoder-cn' || provider === 'workbuddy';
63
-
64
- /** Qoder 网页端同源 API 的 host(用量明细逐资源包接口在这里,只认浏览器会话 Cookie) */
65
- export function webBase(provider) {
66
- return provider === 'qoder-cn' ? CN_WEB_BASE : WEB_BASE;
67
- }
68
-
69
- /** 签到请求头(源自 10router qoderCheckin.js,clientType=10 与官方客户端一致) */
70
- export function buildQoderHeaders(token) {
71
- return {
72
- Authorization: `Bearer ${token}`,
73
- 'Cosy-ClientType': '10',
74
- 'Cosy-Version': '0.3.3',
75
- 'Cosy-MachineOS':
76
- process.platform === 'win32' ? 'windows'
77
- : process.platform === 'darwin' ? 'macos' : 'linux',
78
- 'User-Agent': 'Qoder',
79
- Accept: 'application/json',
80
- };
81
- }
82
-
83
- /** PAT 兑换请求头(源自 10router qoderModels.js,模拟官方 qodercli) */
84
- export function buildExchangeHeaders() {
85
- return {
86
- 'Content-Type': 'application/json',
87
- Accept: 'application/json',
88
- 'User-Agent': 'qodercli/1.0.0',
89
- 'Cosy-Version': '1.0.0',
90
- 'Cosy-ClientType': '5',
91
- };
92
- }
93
-
94
- export const FETCH_TIMEOUT_MS = 15000;
95
-
96
- // 版本号 / 项目主页以 package.json 为唯一来源(桌面版 / fpk 打包时都会一并拷入 package.json)
97
- const PKG = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
98
- export const APP_VERSION = PKG.version;
99
- export const PROJECT_URL = PKG.homepage || 'https://github.com/techysy/CreditDaddy';
1
+ /**
2
+ * CreditDaddy 常量 — 支持的产品线与 Qoder 官方 OpenAPI 端点(WorkBuddy 端点见 workbuddyClient.js)。
3
+ *
4
+ * 端点与请求头结构来自社区逆向成果(10router 项目实测可用):
5
+ * openapi.qoder.sh 国际版:活动/签到、userinfo、配额、PAT 兑换
6
+ * openapi.qoder.com.cn 国内版:同上
7
+ *
8
+ * ⚠️ 非官方接口,Qoder 调整服务端时可能失效。
9
+ */
10
+
11
+ import { readFileSync } from 'node:fs';
12
+
13
+ export const OPENAPI_BASE = 'https://openapi.qoder.sh';
14
+ export const CN_OPENAPI_BASE = 'https://openapi.qoder.com.cn';
15
+
16
+ // 网页端(账号设置 → 用量明细)同源 API:逐资源包明细只在这里提供,且只认浏览器登录后的会话 Cookie。
17
+ export const WEB_BASE = 'https://qoder.com';
18
+ export const CN_WEB_BASE = 'https://qoder.cn';
19
+
20
+ export const LOGIN_URL = 'https://qoder.com/device/selectAccounts';
21
+ export const CN_LOGIN_URL = 'https://qoder.cn/device/selectAccounts';
22
+
23
+ // 活动(签到)列表与领取
24
+ export const CAMPAIGNS_PATH = '/sash/api/v1/me/campaigns?clientType=10';
25
+ export const CAMPAIGN_CLAIM_PATH = (id) => `/sash/api/v1/me/campaigns/${encodeURIComponent(id)}/claim`;
26
+
27
+ // 账号信息与配额
28
+ export const USERINFO_PATH = '/api/v1/userinfo';
29
+ export const QUOTA_USAGE_PATH = '/api/v2/quota/usage';
30
+ // 网页端逐资源包用量明细(plan / 个人资源包 / 组织资源包,含每包到期时间)。
31
+ // 仅接受浏览器登录的会话 Cookie(dt- / jt- token 一律 401),见 qoderClient.fetchUsageDetail。
32
+ export const WEB_USAGE_PATH = '/api/v2/me/usages/big_model_credits';
33
+
34
+ // PAT (pt-...) → 短期 job token (jt-...) 兑换(普通 JSON POST,无需 COSY 签名)
35
+ export const JOB_TOKEN_EXCHANGE_PATH = '/api/v1/jobToken/exchange';
36
+
37
+ // 账号的 provider = 产品 + 区域。新增产品时在此登记,并在 providers.js 挂上对应实现。
38
+ export const PROVIDERS = ['qoder', 'qoder-cn', 'workbuddy', 'workbuddy-intl', 'zcode', 'mirasim', 'catpaw', 'trae', 'minimax', 'tokenharbor', 'xiaomi-mimo', 'river', 'modelscope', 'longcat'];
39
+ export const PROVIDER_LABEL = {
40
+ qoder: 'Qoder 国际版',
41
+ 'qoder-cn': 'Qoder 国内版',
42
+ workbuddy: 'WorkBuddy 国内版',
43
+ 'workbuddy-intl': 'WorkBuddy 国际版',
44
+ zcode: 'ZCode',
45
+ mirasim: 'mirasim',
46
+ catpaw: '妙手',
47
+ trae: 'Trae',
48
+ minimax: 'MiniMax',
49
+ tokenharbor: 'Token Harbor',
50
+ 'xiaomi-mimo': '小米 MiMo',
51
+ river: 'River AI',
52
+ modelscope: 'ModelScope 魔搭',
53
+ longcat: 'LongCat',
54
+ };
55
+ /** provider → 产品线(qoder / workbuddy / zcode / mirasim / catpaw / trae / minimax / tokenharbor / xiaomi-mimo / river) */
56
+ export const productOf = (provider) => {
57
+ const p = String(provider);
58
+ if (p.startsWith('workbuddy')) return 'workbuddy';
59
+ if (p.startsWith('zcode')) return 'zcode';
60
+ if (p === 'mirasim') return 'mirasim';
61
+ if (p === 'catpaw') return 'catpaw';
62
+ if (p.startsWith('trae')) return 'trae';
63
+ if (p === 'minimax') return 'minimax';
64
+ if (p === 'tokenharbor') return 'tokenharbor';
65
+ if (p === 'xiaomi-mimo') return 'xiaomi-mimo';
66
+ if (p === 'river') return 'river';
67
+ if (p === 'modelscope') return 'modelscope';
68
+ if (p === 'longcat') return 'longcat';
69
+ return 'qoder';
70
+ };
71
+ /** provider 是否国内版 */
72
+ export const isCnProvider = (provider) => provider === 'qoder-cn' || provider === 'workbuddy';
73
+
74
+ /** Qoder 网页端同源 API 的 host(用量明细逐资源包接口在这里,只认浏览器会话 Cookie) */
75
+ export function webBase(provider) {
76
+ return provider === 'qoder-cn' ? CN_WEB_BASE : WEB_BASE;
77
+ }
78
+
79
+ /** 签到请求头(源自 10router qoderCheckin.js,clientType=10 与官方客户端一致) */
80
+ export function buildQoderHeaders(token) {
81
+ return {
82
+ Authorization: `Bearer ${token}`,
83
+ 'Cosy-ClientType': '10',
84
+ 'Cosy-Version': '0.3.3',
85
+ 'Cosy-MachineOS':
86
+ process.platform === 'win32' ? 'windows'
87
+ : process.platform === 'darwin' ? 'macos' : 'linux',
88
+ 'User-Agent': 'Qoder',
89
+ Accept: 'application/json',
90
+ };
91
+ }
92
+
93
+ /** PAT 兑换请求头(源自 10router qoderModels.js,模拟官方 qodercli) */
94
+ export function buildExchangeHeaders() {
95
+ return {
96
+ 'Content-Type': 'application/json',
97
+ Accept: 'application/json',
98
+ 'User-Agent': 'qodercli/1.0.0',
99
+ 'Cosy-Version': '1.0.0',
100
+ 'Cosy-ClientType': '5',
101
+ };
102
+ }
103
+
104
+ export const FETCH_TIMEOUT_MS = 15000;
105
+
106
+ // 版本号 / 项目主页以 package.json 为唯一来源(桌面版 / fpk 打包时都会一并拷入 package.json)
107
+ const PKG = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
108
+ export const APP_VERSION = PKG.version;
109
+ export const PROJECT_URL = PKG.homepage || 'https://github.com/techysy/CreditDaddy';
@@ -0,0 +1,261 @@
1
+ /**
2
+ * 通用「浏览器站点 cookie 捕获」(issue #23)——配置驱动的首个接入方:Token Harbor。
3
+ *
4
+ * 适用面:额度/信息只存在于登录后的 dashboard HTML、凭据只能是浏览器会话 cookie 的供应商。
5
+ * 配置表加一行即可接入新站点(登录页 URL / 收割域 / 探测规则),10Router 侧零改动——
6
+ * 同步走既有 `providerSpecificData.creditDaddyWebSession.cookie` 传输(与 qoder 同形状)。
7
+ *
8
+ * ⚠ 探测头必须与 10Router `services/usage/tokenharbor.js` 同款的浏览器形:裸 fetch 的 UA
9
+ * 会拿到「HTML 空壳 / bot 拦截」(配额标记根本不在 HTML 里,探测必然误报「登录未完成」)。
10
+ *
11
+ * 测试用后门(仅测试改动探测地址):环境变量 `CD_PROBE_<站点 id 大写,非字母数字→_>`,
12
+ * 如 CD_PROBE_TOKENHARBOR 指向本地假站。生产配置永远取 probe.url。
13
+ */
14
+
15
+ import { fetchJsonRace } from './zcodeClient.js';
16
+
17
+ /** 浏览器形探测头(对齐 10Router tokenharbor 探针:裸 UA 会被边缘节点喂空壳页) */
18
+ export function dashboardHeaders(site) {
19
+ return {
20
+ Cookie: '',
21
+ Accept: site?.probe?.accept || 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
22
+ 'Accept-Language': 'en-US,en;q=0.5',
23
+ Referer: (site.homeUrl || site.probe.url.split('/').slice(0, 3).join('/')) + '/',
24
+ 'Upgrade-Insecure-Requests': '1',
25
+ 'User-Agent':
26
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
27
+ };
28
+ }
29
+
30
+ /** 登录页壳判定(会话已失效被挡回登录页):没有配额标记且页面主题就是「登录」表单 */
31
+ function looksLikeLoginPage(html) {
32
+ return /sign\s?in|log\s?in/i.test(html);
33
+ }
34
+
35
+ /**
36
+ * Token Harbor dashboard HTML → 免费额度读数(与 10Router `services/usage/tokenharbor-free.js`
37
+ * 同款口径——两处解析同一页面,规则改动需同步)。捕获时从浏览器探针 HTML 里解析一次,
38
+ * 数字进 `meta.quotaSnapshot`,供 issue #24 的额度推送直接消费(推送不再打网络)。
39
+ * @returns {{ used: number, resetAt: string|null } | null} 解析不出配额返回 null
40
+ */
41
+ export function parseTokenHarborQuota(html) {
42
+ const text = String(html || '');
43
+ let used = null;
44
+ const allowanceMatch = text.match(/Free allowance[\s\S]*?(\d+)%\s*(?:used|of total)/i);
45
+ if (allowanceMatch) used = parseInt(allowanceMatch[1], 10);
46
+ else {
47
+ const generalPercentMatch = text.match(/Free allowance[\s\S]*?(\d+)%/i);
48
+ if (generalPercentMatch) used = parseInt(generalPercentMatch[1], 10);
49
+ }
50
+ if (used === null) return null;
51
+ let resetAt = null;
52
+ const resetMatch = text.match(/Resets?\s+(?:in\s+)?(?:(\d+)\s*[dD])?\s*(?:(\d+)\s*[hH])?\s*(?:(\d+)\s*[mM])?/i);
53
+ if (resetMatch && (resetMatch[1] || resetMatch[2] || resetMatch[3])) {
54
+ const days = parseInt(resetMatch[1] || '0', 10);
55
+ const hours = parseInt(resetMatch[2] || '0', 10);
56
+ const minutes = parseInt(resetMatch[3] || '0', 10);
57
+ const msOffset = (days * 24 * 60 + hours * 60 + minutes) * 60 * 1000;
58
+ if (msOffset > 0) resetAt = new Date(Date.now() + msOffset).toISOString();
59
+ }
60
+ return { used, resetAt };
61
+ }
62
+
63
+ export const COOKIE_SITES = [
64
+ {
65
+ id: 'tokenharbor',
66
+ name: 'Token Harbor',
67
+ provider: 'tokenharbor',
68
+ label: 'Token Harbor 免费额度',
69
+ loginUrl: 'https://tokenharbor.ai/login',
70
+ homeUrl: 'https://tokenharbor.ai',
71
+ // 探测标记:由于 Token Harbor 的 dashboard 页面将 Free allowance 移至客户端动态组件中渲染,
72
+ // SSR 阶段只直出用户上下文(userEmail)。为兼顾 Node 探测与隐藏浏览器探针,满足其一即代表会话正常有效。
73
+ probe: {
74
+ url: 'https://tokenharbor.ai/dashboard',
75
+ match: (html) => html.includes('Free allowance') || /userEmail[\\"]*:[\\"]*[^\\",]+@[^\\",]+/.test(html),
76
+ },
77
+ // 免费账号要多账号轮换(#23 增补):身份优先从 Next.js SSR state 的 userEmail 抽,兜底邮箱正则。
78
+ identity: {
79
+ fromHtml: {
80
+ exec: (str) => {
81
+ const m = /userEmail[\\"]*:[\\"]*([^\\",]+)/.exec(str);
82
+ if (m && m[1] && m[1].includes('@')) return [m[1].trim()];
83
+ return /[\w.+-]+@[\w-]+\.[\w.]+/.exec(str);
84
+ },
85
+ },
86
+ },
87
+ // issue #24:捕获时顺手解析网页读数,存 meta.quotaSnapshot 供额度推送消费
88
+ quota: { fromHtml: parseTokenHarborQuota },
89
+ accountName: 'Token Harbor 网页会话',
90
+ hint: '登录后 dashboard 才有 Free allowance 配额;凭据只存本机(账号库加密),同步到 10Router 时按 creditDaddyWebSession 形状传输。',
91
+ },
92
+ {
93
+ id: 'xiaomi-mimo',
94
+ name: 'Xiaomi MiMo',
95
+ provider: 'xiaomi-mimo',
96
+ label: '小米 MiMo 控制台余额',
97
+ loginUrl: 'https://platform.xiaomimimo.com/console/balance',
98
+ homeUrl: 'https://platform.xiaomimimo.com',
99
+ probe: {
100
+ url: 'https://platform.xiaomimimo.com/api/v1/userProfile',
101
+ match: 'userId',
102
+ accept: 'application/json, text/plain, */*',
103
+ },
104
+ accountName: '小米 MiMo 控制台会话',
105
+ hint: '登录后自动抓取 UID 与控制台充值余额;通过外部配额(issue #24/26)推送到 10Router 配额追踪器显示余额卡片。',
106
+ },
107
+ {
108
+ id: 'modelscope',
109
+ name: 'ModelScope 魔搭',
110
+ provider: 'modelscope',
111
+ label: 'ModelScope 魔方余额',
112
+ loginUrl: 'https://modelscope.cn',
113
+ homeUrl: 'https://modelscope.cn',
114
+ // 魔方余额 API 本身就是探测(GET /openapi/v1/magicubes/balance 带会话 cookie);
115
+ // 每日魔方额度为登录自动发放(前端无 claim 端点),故无签到自动化,只读余额出卡。
116
+ cookieUrls: ['https://modelscope.cn', 'https://www.modelscope.cn'],
117
+ verify: 'api',
118
+ accountName: 'ModelScope 网页会话',
119
+ identity: { fromHtml: /[\w.+-]+@[\w-]+\.[\w.]+/ },
120
+ hint: '登录魔搭后自动抓取魔方(Magicube)余额;每日额度自动发放无需签到,token 只存本机,余额数字推 10Router 出卡(同名认领可挂连接卡)。',
121
+ },
122
+ {
123
+ id: 'river',
124
+ name: 'River AI',
125
+ provider: 'river',
126
+ label: 'River AI 免费额度',
127
+ loginUrl: 'https://console.river.ai',
128
+ homeUrl: 'https://console.river.ai',
129
+ // 凭据主体是 WorkOS 会话——实测(2026-10-14)SPA 的 token 只在内存 Map,localStorage
130
+ // 通常为空;持久会话走 cookie(WORKOS_REFRESH_SESSION @ auth.console.river.ai 等)。
131
+ // 收割面必须覆盖**三个源**(console / auth / login 子域),storage 键做前缀扫描兜底。
132
+ // 探测/身份/额度由 daemon 的 river 线一步完成(cookie 直连 → Bearer → 刷新兑换),不走 HTML 探针。
133
+ storageKeys: ['workos:access-token', 'workos:refresh-token', 'workos:expires-at', 'workos:user'],
134
+ storagePrefixes: ['workos:'],
135
+ cookieUrls: ['https://console.river.ai', 'https://auth.console.river.ai', 'https://login.console.river.ai'],
136
+ verify: 'api',
137
+ accountName: 'River AI 网页会话',
138
+ hint: '登录 console.river.ai 后自动抓取 WorkOS 会话与 Billing 余额($25 免费额度);token 只存本机,推送 10Router 只带余额数字,会话过期自动刷新(单次轮换即时落盘)。',
139
+ },
140
+ {
141
+ id: 'longcat',
142
+ name: 'LongCat',
143
+ provider: 'longcat',
144
+ label: 'LongCat Token 资源包',
145
+ loginUrl: 'https://longcat.chat/pc/login',
146
+ homeUrl: 'https://longcat.chat',
147
+ // 资源包汇总 API 即探测:带对 cookie 返 200 {code:0},未登录 401「用户未登录」(2026-10-15 实测)
148
+ probe: {
149
+ url: 'https://longcat.chat/api/pay/quota/metering/token-packs/summary',
150
+ match: '"code"',
151
+ accept: 'application/json, text/plain, */*',
152
+ },
153
+ verify: 'api',
154
+ accountName: 'LongCat 网页会话',
155
+ hint: '登录 longcat.chat 后自动抓取 Token 资源包余量与逐包明细(当前包 + 其他包,按官方消耗顺序排列);推送 10Router 只带 token 数字,会话 cookie 只存本机。',
156
+ },
157
+ ];
158
+
159
+ export const getSite = (id) => COOKIE_SITES.find((s) => s.id === id) || null;
160
+
161
+ /** 按登录窗口 URL 找到对应站点配置(登录页或其同源首页) */
162
+ export function siteForLoginUrl(url) {
163
+ const u = String(url || '');
164
+ return COOKIE_SITES.find((s) => u.startsWith(s.loginUrl) || u.startsWith(s.homeUrl)) || null;
165
+ }
166
+
167
+ /** cookies.get({url}) 可能返回 host-only 与域级同名项——按 name 去重留后写者,拼成 handler 接受的 plain string */
168
+ export function serializeCookies(cookies) {
169
+ const byName = new Map();
170
+ for (const c of cookies || []) {
171
+ if (c && typeof c.name === 'string' && c.name) byName.set(c.name, String(c.value ?? ''));
172
+ }
173
+ return [...byName.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
174
+ }
175
+
176
+ function probeUrl(site) {
177
+ const env = process.env['CD_PROBE_' + site.id.toUpperCase().replace(/[^A-Z0-9]/g, '_')];
178
+ return env || site.probe.url;
179
+ }
180
+
181
+ function testProbeMatch(site, text) {
182
+ if (!site?.probe?.match) return true;
183
+ if (typeof site.probe.match === 'function') return Boolean(site.probe.match(text));
184
+ return text.includes(site.probe.match);
185
+ }
186
+
187
+ /**
188
+ * 探测一份会话 cookie 是否仍然有效。
189
+ * 探测走 fetchJsonRace(跟随全局代理配置——Token Harbor 对大陆/港澳有封锁,出口代理是必须项)。
190
+ * @returns {Promise<{ok: boolean, reason?: 'invalid'|'no-match'|'http'|'network', status?: number, html?: string}>}
191
+ */
192
+ export async function verifySiteCookie(site, cookie, { fetchImpl } = {}) {
193
+ const url = probeUrl(site);
194
+ const doFetch = fetchImpl || ((u, init) => fetchJsonRace(u, { headers: init.headers, timeoutMs: 12000 }));
195
+ try {
196
+ const headers = { ...dashboardHeaders(site), Cookie: String(cookie || '') };
197
+ const r = await doFetch(url, { headers });
198
+ if (r.status === 401 || r.status === 403) return { ok: false, reason: 'invalid', status: r.status };
199
+ if (r.status !== 200) return { ok: false, reason: 'http', status: r.status };
200
+ const text = await r.text();
201
+ if (!testProbeMatch(site, text)) {
202
+ // 被挡回登录页 = 会话失效(与 10Router 的 AUTH_EXPIRED 分类对齐),不是「页面结构变化」
203
+ return { ok: false, reason: looksLikeLoginPage(text) ? 'invalid' : 'no-match', status: r.status };
204
+ }
205
+ return { ok: true, status: r.status, html: text };
206
+ } catch (e) {
207
+ return { ok: false, reason: 'network', error: e?.message || String(e) };
208
+ }
209
+ }
210
+
211
+ /**
212
+ * 用「浏览器出品的 HTML」做探针(credit 桌面壳隐藏 Chromium 窗口的 probeHtml)——
213
+ * 不经任何网络层,标记判定与网络探针完全同款(marker / 登录页壳分类),只做纯文本判断。
214
+ */
215
+ export function verifyProbeHtml(site, html) {
216
+ const text = String(html || '');
217
+ if (!testProbeMatch(site, text)) {
218
+ return { ok: false, reason: looksLikeLoginPage(text) ? 'invalid' : 'no-match' };
219
+ }
220
+ return { ok: true };
221
+ }
222
+
223
+
224
+ /** 从浏览器探针产 HTML 里抽登录身份(邮箱等);抽不到返 null(调用方按单例兜底)。 */
225
+ export function extractIdentity(site, html) {
226
+ if (!site.identity || !site.identity.fromHtml) return null;
227
+ const m = site.identity.fromHtml.exec(String(html || ''));
228
+ return m && m[0] ? m[0] : null;
229
+ }
230
+
231
+ /** 从浏览器探针 HTML 里抽额度读数(site.quota.fromHtml);解析不出返 null,不影响既有行。 */
232
+ export function extractQuotaSnapshot(site, html) {
233
+ if (!site.quota || typeof site.quota.fromHtml !== 'function') return null;
234
+ try { return site.quota.fromHtml(html) || null; } catch { return null; }
235
+ }
236
+
237
+ /** 归户键的短哈希(不暴露身份原文;merge/dedupe 都只比对它) */
238
+ export function identityHashOf(value) {
239
+ if (!value) return null;
240
+ let h = 0;
241
+ const s = String(value);
242
+ for (let i = 0; i < s.length; i++) h = ((h * 31 + s.charCodeAt(i)) | 0) >>> 0;
243
+ return 'ih' + h.toString(36);
244
+ }
245
+
246
+ /**
247
+ * 行名是否属于「自动名家族」(站点默认名[+身份后缀] / Xiaomi MiMo (UID: …) / River AI (…) / ModelScope (…))。
248
+ * 自动名可以随身份解析升级(如「ModelScope (会话)」→「ModelScope (x@y.com)」);
249
+ * 不在家族里的名字视为**用户手改的备注名**(10R 同名认领键),一律不动——
250
+ * 这层判定独立于 meta.renamed 标记,把历史改名(旧版无标记)也保护进来。
251
+ */
252
+ export function isAutoGeneratedName(site, name) {
253
+ const n = String(name || '');
254
+ if (!n) return true; // 无名 → 允许回填自动名
255
+ const acc = String(site?.accountName || '');
256
+ const escaped = acc.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
257
+ return (acc && new RegExp(`^${escaped}(\\(.+\\))?$`).test(n))
258
+ || /^Xiaomi MiMo \(UID: .+\)$/.test(n)
259
+ || /^River AI \(.+\)$/.test(n)
260
+ || /^ModelScope \(.+\)$/.test(n);
261
+ }