diff --git a/README.md b/README.md index 9647cdd..dcc39a0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # CreditDaddy -**AI 编程工具多账号本地管理 + 每日积分自动领取(领鸡蛋)助手:Qoder · WorkBuddy · ZCode · mirasim · 妙手 · Trae · MiniMax · ModelScope · 豆包 Doubao · 讯飞 Loomy,集成 10Router 额度总览、用量同步与网页会话额度卡(Token Harbor / 小米 MiMo / River / ModelScope 魔搭 / LongCat)** +**AI 编程工具多账号本地管理 + 每日积分自动领取(领鸡蛋)助手:Qoder · WorkBuddy · ZCode · mirasim · 妙手 · Trae · MiniMax · ModelScope · 豆包 Doubao · 讯飞 Loomy · Meoo 秒悟,集成 10Router 额度总览、用量同步与网页会话额度卡(Token Harbor / 小米 MiMo / River / ModelScope 魔搭 / LongCat)** [![Release](https://img.shields.io/github/v/release/techysy/CreditDaddy?label=%E7%89%88%E6%9C%AC&color=2563eb)](https://github.com/techysy/CreditDaddy/releases/latest) [![CI](https://img.shields.io/github/actions/workflow/status/techysy/CreditDaddy/ci.yml?branch=main&label=CI)](https://github.com/techysy/CreditDaddy/actions/workflows/ci.yml) @@ -144,6 +144,7 @@ | River AI | WorkOS localStorage JWT 变体(三源收割) | `$25` 免费额度 `balance_usd`,会话过期自动续期 | | ModelScope 魔搭(issue #25) | 会话 cookie(双源) | 魔粒余额,卡面拆「今日发放 / 长期」两腿;每日额度登录自动发放、无签到端点 | | LongCat 美团(issue #29) | 美团 SSO 会话 cookie | Token 资源包:Balance 汇总 + 逐包明细腿(官方消耗顺序、赠送包徽注、到期日) | +| Meoo 秒悟 | meoo.com 会话 cookie(阿里云 SSO 手机号 + 短信登录) | 每日首登积分(`/api/mc/entitlement/init` 触发,当前 +2000/天、约 6~7 天过期)自动领取 + 积分余额;会话失效(`NEED_LOGIN`)提示重新捕获;首版不推 10Router 卡 | > 注:办公小浣熊曾按 issue #27 调研接入,因 `raccoon_token` 异步写回时序不可靠已于 v1.5.0 回滚移除。 @@ -343,6 +344,7 @@ CreditDaddy/ │ ├── localDetect.js # 客户端已安装环境侦测 │ ├── logger.js # 内存环形日志与按日归档 │ ├── longcatClient.js # LongCat Token 资源包与身份请求 +│ ├── meooClient.js # Meoo 秒悟每日首登积分与积分余额 │ ├── mirasimClient.js # mirasim 套餐 / 额度请求 │ ├── mirasimLocal.js # 本机 mirasim 凭据解密与切号 │ ├── minimaxAuth.js # MiniMax 浏览器登录(OAuth 设备码 + S256 PKCE) diff --git a/package.json b/package.json index acb7767..f1c3cac 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "creditdaddy", "version": "1.7.2", - "description": "CreditDaddy — Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax / ModelScope / 豆包 Doubao / 讯飞 Loomy / WPS 灵犀多账号本地管理与每日积分自动签到助手,及 10Router 供应商 Cookie 会话捕获与外部配额(Token Harbor / 小米 MiMo / River / ModelScope 魔搭 / LongCat)", + "description": "CreditDaddy — Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae / MiniMax / ModelScope / 豆包 Doubao / 讯飞 Loomy / WPS 灵犀 / Meoo 秒悟多账号本地管理与每日积分自动签到助手,及 10Router 供应商 Cookie 会话捕获与外部配额(Token Harbor / 小米 MiMo / River / ModelScope 魔搭 / LongCat)", "homepage": "https://github.com/techysy/CreditDaddy", "repository": { "type": "git", diff --git a/scripts/release-check.sh b/scripts/release-check.sh index 9304ea5..c1adfba 100644 --- a/scripts/release-check.sh +++ b/scripts/release-check.sh @@ -246,7 +246,7 @@ import('./src/constants.js').then(({ PROVIDERS, productOf }) => { " 2>/dev/null || true) if [ -n "$PROVIDERS" ]; then # catpaw 在文案里叫「妙手」,单独给别名 - alias_of() { case "$1" in catpaw) echo "妙手";; tokenharbor) echo "Token Harbor";; 'xiaomi-mimo') echo "MiMo";; lingxi) echo "灵犀";; *) echo "$1";; esac; } + alias_of() { case "$1" in catpaw) echo "妙手";; tokenharbor) echo "Token Harbor";; 'xiaomi-mimo') echo "MiMo";; lingxi) echo "灵犀";; meoo) echo "秒悟";; *) echo "$1";; esac; } ROOT_DESC=$(node -p "require('./package.json').description") STALE=0 while IFS= read -r product; do diff --git a/src/checkin.js b/src/checkin.js index 678b479..95d7520 100644 --- a/src/checkin.js +++ b/src/checkin.js @@ -35,7 +35,7 @@ const ZCODE_FIRST_DELAY_MS = 2 * 60 * 1000; // 签到日 = 对应时点起的日历日:dayKey(now, provider) = (now + (8 - 刷新小时)h) 的 UTC 日期 const DAY_REFRESH_HOUR_UTC8 = { qoder: 10, 'qoder-cn': 10 }; // 领取日志的积分单位:默认 Credits(Qoder/WorkBuddy 系),网页会话线各自带单位 -const CHECKIN_UNIT = { lingxi: '灵点', doubao: '额度' }; +const CHECKIN_UNIT = { lingxi: '灵点', doubao: '额度', meoo: '秒悟积分' }; let timerHandle = null; let zcodeTimerHandle = null; diff --git a/src/constants.js b/src/constants.js index 467c342..93ef0f5 100644 --- a/src/constants.js +++ b/src/constants.js @@ -35,7 +35,7 @@ export const WEB_USAGE_PATH = '/api/v2/me/usages/big_model_credits'; export const JOB_TOKEN_EXCHANGE_PATH = '/api/v1/jobToken/exchange'; // 账号的 provider = 产品 + 区域。新增产品时在此登记,并在 providers.js 挂上对应实现。 -export const PROVIDERS = ['qoder', 'qoder-cn', 'workbuddy', 'workbuddy-intl', 'zcode', 'mirasim', 'catpaw', 'trae', 'trae-intl', 'minimax', 'minimax-global', 'tokenharbor', 'xiaomi-mimo', 'river', 'modelscope', 'longcat', 'doubao', 'loomy', 'lingxi']; +export const PROVIDERS = ['qoder', 'qoder-cn', 'workbuddy', 'workbuddy-intl', 'zcode', 'mirasim', 'catpaw', 'trae', 'trae-intl', 'minimax', 'minimax-global', 'tokenharbor', 'xiaomi-mimo', 'river', 'modelscope', 'longcat', 'doubao', 'loomy', 'lingxi', 'meoo']; export const PROVIDER_LABEL = { qoder: 'Qoder 国际版', 'qoder-cn': 'Qoder 国内版', @@ -56,6 +56,7 @@ export const PROVIDER_LABEL = { doubao: '豆包', loomy: '讯飞 Loomy', lingxi: 'WPS 灵犀', + meoo: 'Meoo 秒悟', }; /** provider → 产品线(qoder / workbuddy / zcode / mirasim / catpaw / trae / minimax / tokenharbor / xiaomi-mimo / river) */ export const productOf = (provider) => { @@ -74,6 +75,7 @@ export const productOf = (provider) => { if (p === 'doubao') return 'doubao'; if (p === 'loomy') return 'loomy'; if (p === 'lingxi') return 'lingxi'; + if (p === 'meoo') return 'meoo'; return 'qoder'; }; /** provider 是否国内版 */ diff --git a/src/cookieSites.js b/src/cookieSites.js index 5b866a0..d324eb2 100644 --- a/src/cookieSites.js +++ b/src/cookieSites.js @@ -184,6 +184,22 @@ export const COOKIE_SITES = [ accountName: 'WPS 灵犀', hint: '每日自动签到(+100 灵点,七天一轮第七天 200)+ 推送 10Router 出余额卡,Cookie 只存本机。多号分别捕获即按 WPS 账号分行并存。', }, + { + id: 'meoo', + name: 'Meoo 秒悟', + provider: 'meoo', + label: 'Meoo 秒悟', + loginUrl: 'https://meoo.com', + homeUrl: 'https://meoo.com', + // 探测 = 权益接口本身(2026-11-13 实测):带会话 cookie 返 { success:true, data:{ userPointInfo… } }; + // 未登录 401 { errCode:'NEED_LOGIN' }。身份走 /api/v1/user/me 的 user_id(多号归户)。 + // 每日首登积分由 entitlement/init 触发(网页端登录后同款调用),CD 每日代调一次。 + verify: 'api', + cookieUrls: ['https://meoo.com'], + accountName: 'Meoo 秒悟', + no10rSync: true, // 首版不推 10Router 外部配额卡(积分只在 Meoo 内消费) + hint: '登录 meoo.com(阿里云账号手机号 + 短信)后捕获会话;CD 每日自动领取首登积分(当前 +2000/天,约 6~7 天后过期)并展示积分余额。会话失效(NEED_LOGIN)时请重新登录捕获,Cookie 只存本机。', + }, // ⚠ 码道 CodeArts(issue #40)已于 2026-10-30 撤下,不再提供: // 华为云控制台会话受闲置超时 + WAF 指印(HWWAFSESID / browserCheckResult)绑定, // 浏览器里能靠 SPA 持续续期,本机无法续 —— 实测新鲜会话当日内即失效(网关回登录页), diff --git a/src/daemon.js b/src/daemon.js index 0fa127e..3b5171c 100644 --- a/src/daemon.js +++ b/src/daemon.js @@ -387,6 +387,36 @@ async function reprobeCookieAccount(site, a) { logger.info('COOKIE-SITE', '{name} 就地重探通过({identity},签到{signed},灵点 {balance})', { name: site.name, identity: uid ? 'uid ' + uidMasked : '无 uid', signed: st.checkedIn ? '已签' : '未签', balance: Number.isFinite(st.balance) ? st.balance : '—' }); return finish({ id: a.id, name: a.name, ok: true, identity: uid ? `uid:${uidMasked}` : null, balance: Number.isFinite(st.balance) ? st.balance : null, quotaSnapshot: Number.isFinite(st.balance) ? { used: 0, total: st.balance, isCreditBalance: true, at: now } : null }); } + if (site.id === 'meoo') { + // Meoo 秒悟:权益接口即探测(只读,不调 entitlement/init——发放动作只走签到线), + // 刷新积分余额快照;身份走 user/me 的 user_id(老行缺失时回填 + 名字升级) + const { fetchMeooEntitlement, fetchMeooIdentity, maskMeooUserId } = await import('./meooClient.js'); + const ent = await fetchMeooEntitlement(cookie).catch((e) => ({ auth: true, ok: false, code: e?.message || 'network' })); + if (!ent.auth) return finish({ id: a.id, name: a.name, ok: false, error: 'Meoo 会话已失效(NEED_LOGIN),请重新登录捕获' }); + if (!ent.ok) return finish({ id: a.id, name: a.name, ok: false, error: '探测失败,请稍后再试或重新登录捕获' }); + const ident = await fetchMeooIdentity(cookie).catch(() => null); + const userId = ident?.ok ? ident.userId : null; + const masked = userId ? maskMeooUserId(userId) : null; + const bal = Number.isFinite(ent.balance) ? ent.balance : null; + await withAccounts((list) => { + const hit = list.find((x) => x.id === a.id); + if (!hit) return; + hit.meta = { + ...(hit.meta || {}), + cookieSiteSession: { cookie, capturedAt: hit.meta?.cookieSiteSession?.capturedAt || now }, + ...(userId && !hit.meta?.identityHash ? { identityHash: identityHashOf(`uid:${userId}`) } : {}), + ...(ent.grants ? { meooGrants: { grants: ent.grants, at: now } } : {}), + ...(bal !== null ? { quotaSnapshot: { used: 0, total: bal, isCreditBalance: true, at: now } } : {}), + }; + if (userId) { + hit.uid = String(userId); + if (canAutoRename(site, hit)) hit.name = `${site.accountName}(${masked})`; + } + hit.updatedAt = now; + }); + logger.info('COOKIE-SITE', '{name} 就地重探通过({identity},积分 {balance})', { name: site.name, identity: masked ? 'uid ' + masked : '无 uid', balance: bal ?? '—' }); + return finish({ id: a.id, name: a.name, ok: true, identity: masked ? `uid:${masked}` : null, balance: bal, quotaSnapshot: bal !== null ? { used: 0, total: bal, isCreditBalance: true, at: now } : null }); + } if (site.id === 'doubao') { // 豆包(issue #32):detail 接口一次拿全(会话有效性 + 签到状态 + 累计点数), // 重探 = 刷新额度快照;无 10R 同步(豆包不推外部配额卡,错误文案不再误导) @@ -764,7 +794,7 @@ async function handleApi(req, res, url) { // 单位标注:各家余额单位互异(Token / USD / CNY / 魔粒 / 积分)且都不是「积分」 // 口径——带 unit 返回,面板 creditSum 按 unit 有无决定是否计入「剩余积分」合计 //(有单位 = 不同量纲,不相加)。HTML 探针站(tokenharbor)是百分比,同样排除。 - const UNIT_BY_SITE = { 'xiaomi-mimo': 'CNY', 'river': 'USD', 'longcat': 'Token', 'modelscope': '魔粒', 'lingxi': '灵点' }; + const UNIT_BY_SITE = { 'xiaomi-mimo': 'CNY', 'river': 'USD', 'longcat': 'Token', 'modelscope': '魔粒', 'lingxi': '灵点', 'meoo': '秒悟积分' }; const unit = UNIT_BY_SITE[site.id] || (hasTotal ? '' : '%'); return json(res, 200, { quota: { total: hasTotal ? total : 100, @@ -1280,11 +1310,13 @@ async function handleApi(req, res, url) { msBalance: a.meta?.msBalance || null, longcatPacks: a.meta?.longcatPacks || null, lingxiPacks: a.meta?.lingxiPacks || null, // 智点资源包快照(issue #53) + meooGrants: a.meta?.meooGrants || null, // Meoo 积分逐笔发放快照(带到期日) lastProbe: a.meta?.lastProbe || null, // 呼吸灯数据源:ok / warn / dead + 失败原因 })); const latest = rows.reduce((t, r) => (r.capturedAt && (!t || r.capturedAt > t) ? r.capturedAt : t), null); return { id: site.id, name: site.name, label: site.label, loginUrl: site.loginUrl, hint: site.hint, + ...(site.no10rSync === true ? { no10rSync: true } : {}), accountId: rows[0]?.id || null, capturedAt: latest, accounts: rows, }; }), @@ -1495,6 +1527,37 @@ async function handleApi(req, res, url) { } } + // Meoo 秒悟:权益接口即探测(带 cookie 返 success:true,未登录 401 NEED_LOGIN)。 + // 只读——不在捕获时调 entitlement/init,首登积分发放统一走签到线。身份 = user/me 的 user_id。 + let meooUserId = null; + let meooMasked = null; + let meooGrants = null; + if (site.id === 'meoo') { + try { + const { fetchMeooEntitlement, fetchMeooIdentity, maskMeooUserId } = await import('./meooClient.js'); + if (!cookie) { + logger.warn('COOKIE-SITE', '{name} 未读到会话 cookie', { name: site.name }); + return json(res, 400, { error: '未读到 Meoo 登录会话,请在登录窗完成登录后再关闭' }); + } + const ent = await fetchMeooEntitlement(cookie); + if (!ent.auth) { + logger.warn('COOKIE-SITE', '{name} 权益接口探测被拒:{code}', { name: site.name, code: ent.code }); + return json(res, 401, { error: '会话不可用,请重新登录该站点后再捕获(探测 401/403)' }); + } + if (!ent.ok) return json(res, 502, { error: '探测站点失败:' + (ent.code || '') }); + const ident = await fetchMeooIdentity(cookie).catch(() => null); + meooUserId = ident?.ok ? ident.userId : null; + meooMasked = meooUserId ? maskMeooUserId(meooUserId) : null; + if (ent.grants) meooGrants = { grants: ent.grants, at: now }; + identity = meooUserId ? `uid:${meooUserId}` : null; // 有 user_id → 哈希归户;无 → 单例兜底 + quotaSnapshot = { used: 0, total: Number(ent.balance) || 0, isCreditBalance: true }; + logger.info('COOKIE-SITE', '{name} 探测通过({identity},积分 {balance})', { name: site.name, identity: meooUserId ? 'uid ' + meooMasked : '无 uid(单例)', balance: Number.isFinite(ent.balance) ? ent.balance : '—' }); + } catch (meooErr) { + logger.warn('COOKIE-SITE', '{name} 权益接口抓取异常:{err}', { name: site.name, err: meooErr.message }); + return json(res, 502, { error: '探测站点失败:' + (meooErr.message || '') }); + } + } + const ih = identityHashOf(mimoUserId || riverSession?.user?.id || msSession?.id || lcSession?.userId || identity); const quotaMeta = quotaSnapshot ? { quotaSnapshot: { ...quotaSnapshot, at: now } } : {}; const cred = riverSession ? riverSession.accessToken : cookie; // 落盘的凭据主体 @@ -1511,6 +1574,7 @@ async function handleApi(req, res, url) { else if (site.id === 'modelscope') accName = `ModelScope (${identity || '会话'})`; else if (site.id === 'longcat') accName = `LongCat (${identity || '会话'})`; else if (site.id === 'lingxi') accName = site.accountName + (lxUidMasked ? `(${lxUidMasked})` : ''); // uid 后缀脱敏(前3后3),归户仍用原始 uid + else if (site.id === 'meoo') accName = site.accountName + (meooMasked ? `(${meooMasked})` : ''); // user_id 脱敏后缀,归户仍用原始值 else accName = site.accountName + (identity ? `(${identity})` : ''); // 自动行名只在「没手改过且现名属自动名家族」时写入(meta.renamed + isAutoGeneratedName 双闸) const autoName = (row) => { if (canAutoRename(site, row)) row.name = accName; }; @@ -1538,6 +1602,8 @@ async function handleApi(req, res, url) { ...(lxSnapshot ? { lingxiPacks: lxSnapshot } : {}), }; if (lcSession?.userId) { hit.uid = String(lcSession.userId); autoName(hit); } + if (meooUserId) { hit.uid = String(meooUserId); autoName(hit); } + if (meooGrants) hit.meta.meooGrants = meooGrants; hit.updatedAt = now; accountId = hit.id; } else { @@ -1546,7 +1612,7 @@ async function handleApi(req, res, url) { provider: site.provider, token: cred, expiresAt: site.id === 'river' ? (riverSession?.expiresAt || undefined) : undefined, - uid: mimoUserId || riverUid || (msSession?.id ? String(msSession.id) : undefined) || (lcSession?.userId ? String(lcSession.userId) : undefined) || (lxUid ? String(lxUid) : undefined) || undefined, + uid: mimoUserId || riverUid || (msSession?.id ? String(msSession.id) : undefined) || (lcSession?.userId ? String(lcSession.userId) : undefined) || (lxUid ? String(lxUid) : undefined) || (meooUserId ? String(meooUserId) : undefined) || undefined, source: 'web-session', meta: { cookieSiteId: site.id, @@ -1561,6 +1627,7 @@ async function handleApi(req, res, url) { ...(lcPacks ? { longcatPacks: longcatSnapshot(lcPacks) } : {}), ...(lcSession ? { longcatSession: lcSession } : {}), ...(lxSnapshot ? { lingxiPacks: lxSnapshot } : {}), + ...(meooGrants ? { meooGrants } : {}), }, }); acc.verified = true; @@ -1623,6 +1690,7 @@ async function handleApi(req, res, url) { // ——失败说明会话可能已死,恰好要把 message 状态卡推上去让 10R 卡面可见。 const touchedSources = new Set(); const sourceOf = async (a) => { + if (site.no10rSync === true) return null; // 不推 10R 卡的站点(如 Meoo)不进推送名单 if (site.id === 'tokenharbor') return 'tokenharbor'; try { const m = await import( @@ -2065,6 +2133,7 @@ async function handleApi(req, res, url) { trae: dayKey(Date.now(), 'trae'), minimax: dayKey(Date.now(), 'minimax'), lingxi: dayKey(Date.now(), 'lingxi'), // 网页会话签到线(#53):0 点翻日,徽标/isDone 的日界比对用 + meoo: dayKey(Date.now(), 'meoo'), // Meoo 首登积分线:同为 0 点翻日 }, scheduler: await getSchedulerInfo(), riskIdentity: riskIdentityAvailable(), diff --git a/src/i18n/en.json b/src/i18n/en.json index 9940c1a..dc5ea97 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -2001,5 +2001,28 @@ "回调里的 AuthCode 已过期,请重新发起": "The AuthCode in the callback has expired — please start again", "回调里没有 AuthCode(授权页协议可能又变了),请重新发起或更新 CreditDaddy": "No AuthCode in the callback (the authorize-page protocol may have changed again) — please start again or update CreditDaddy", "MiniMax Code 国内版": "MiniMax Code (CN)", - "MiniMax Code 国际版": "MiniMax Code (International)" + "MiniMax Code 国际版": "MiniMax Code (International)", + "秒悟积分": "Meoo points", + "Meoo 秒悟": "Meoo", + "登录 meoo.com(阿里云账号手机号 + 短信)后捕获会话;CD 每日自动领取首登积分(当前 +2000/天,约 6~7 天后过期)并展示积分余额。会话失效(NEED_LOGIN)时请重新登录捕获,Cookie 只存本机。": "Sign in to meoo.com (Aliyun account, phone + SMS) and capture the session; CD claims the daily first-login points automatically (currently +2000/day, expiring after about 6–7 days) and shows the points balance. If the session expires (NEED_LOGIN), sign in and capture again. Cookies stay on this machine only.", + "Meoo 会话已失效(NEED_LOGIN),请重新登录捕获": "Meoo session expired (NEED_LOGIN); please sign in and capture again", + "{name} 就地重探通过({identity},积分 {balance})": "{name} re-probe passed ({identity}, points {balance})", + "未读到 Meoo 登录会话,请在登录窗完成登录后再关闭": "No Meoo login session found; please finish signing in before closing the window", + "{name} 权益接口探测被拒:{code}": "{name} entitlement probe rejected: {code}", + "{name} 探测通过({identity},积分 {balance})": "{name} probe passed ({identity}, points {balance})", + "{name} 权益接口抓取异常:{err}": "{name} entitlement fetch error: {err}", + "Meoo 会话缺失,请在「网页会话」页登录 meoo.com 后捕获": "Meoo session missing; sign in to meoo.com on the Web Sessions page and capture", + "Meoo 首登积分领取失败:{err}": "Meoo first-login points claim failed: {err}", + "Meoo 会话已失效({code}),请到「网页会话」页重新登录 meoo.com 并捕获": "Meoo session expired ({code}); sign in to meoo.com again on the Web Sessions page and capture", + "Meoo 账号权益仍在初始化,稍后自动重试": "Meoo account entitlements are still initializing; will retry automatically", + "Meoo 首登积分领取成功": "Meoo first-login points claimed", + "首登积分已发放(+{n} 积分)": "First-login points granted (+{n} points)", + "今日首登积分已发放": "Today's first-login points already granted", + "Meoo 积分查询失败:{err}": "Meoo points query failed: {err}", + "每日首登": "Daily first login", + "邀请奖励": "Invite reward", + "订阅": "Subscription", + "积分余额": "Points balance", + "积分余额 ": "Points balance ", + "(最早 ": " (earliest expires " } diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 5b59c67..ff632c9 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -2001,5 +2001,28 @@ "回调里的 AuthCode 已过期,请重新发起": "コールバックの AuthCode が期限切れです。再度実行してください", "回调里没有 AuthCode(授权页协议可能又变了),请重新发起或更新 CreditDaddy": "コールバックに AuthCode がありません(認可ページのプロトコルがまた変わった可能性があります)。再度実行するか CreditDaddy を更新してください", "MiniMax Code 国内版": "MiniMax Code 国内版", - "MiniMax Code 国际版": "MiniMax Code 国際版" + "MiniMax Code 国际版": "MiniMax Code 国際版", + "秒悟积分": "Meoo ポイント", + "Meoo 秒悟": "Meoo 秒悟", + "登录 meoo.com(阿里云账号手机号 + 短信)后捕获会话;CD 每日自动领取首登积分(当前 +2000/天,约 6~7 天后过期)并展示积分余额。会话失效(NEED_LOGIN)时请重新登录捕获,Cookie 只存本机。": "meoo.com に(Aliyun アカウント・電話番号 + SMS で)ログインしてセッションを取得します。CD が毎日の初回ログインポイントを自動受け取り(現在 +2000/日、約 6〜7 日で失効)、ポイント残高を表示します。セッション失効(NEED_LOGIN)時は再ログインして取得し直してください。Cookie はこの端末にのみ保存されます。", + "Meoo 会话已失效(NEED_LOGIN),请重新登录捕获": "Meoo セッションが失効しました(NEED_LOGIN)。再ログインして取得してください", + "{name} 就地重探通过({identity},积分 {balance})": "{name} その場で再検査 OK({identity}、ポイント {balance})", + "未读到 Meoo 登录会话,请在登录窗完成登录后再关闭": "Meoo のログインセッションが読み取れません。ログインを完了してからウィンドウを閉じてください", + "{name} 权益接口探测被拒:{code}": "{name} 権益 API の検査が拒否されました: {code}", + "{name} 探测通过({identity},积分 {balance})": "{name} 検査 OK({identity}、ポイント {balance})", + "{name} 权益接口抓取异常:{err}": "{name} 権益 API の取得エラー: {err}", + "Meoo 会话缺失,请在「网页会话」页登录 meoo.com 后捕获": "Meoo セッションがありません。「Web セッション」ページで meoo.com にログインして取得してください", + "Meoo 首登积分领取失败:{err}": "Meoo 初回ログインポイントの受け取りに失敗: {err}", + "Meoo 会话已失效({code}),请到「网页会话」页重新登录 meoo.com 并捕获": "Meoo セッションが失効しました({code})。「Web セッション」ページで meoo.com に再ログインして取得してください", + "Meoo 账号权益仍在初始化,稍后自动重试": "Meoo アカウントの権益はまだ初期化中です。後で自動的に再試行します", + "Meoo 首登积分领取成功": "Meoo 初回ログインポイントを受け取りました", + "首登积分已发放(+{n} 积分)": "初回ログインポイント付与(+{n} ポイント)", + "今日首登积分已发放": "本日の初回ログインポイントは付与済み", + "Meoo 积分查询失败:{err}": "Meoo ポイントの照会に失敗: {err}", + "每日首登": "毎日の初回ログイン", + "邀请奖励": "招待報酬", + "订阅": "サブスクリプション", + "积分余额": "ポイント残高", + "积分余额 ": "ポイント残高 ", + "(最早 ": "(最短 " } diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 8be3df5..1da1be3 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -2001,5 +2001,28 @@ "回调里的 AuthCode 已过期,请重新发起": "콜백의 AuthCode가 만료되었습니다. 다시 시작해 주세요", "回调里没有 AuthCode(授权页协议可能又变了),请重新发起或更新 CreditDaddy": "콜백에 AuthCode가 없습니다(인가 페이지 프로토콜이 다시 변경된 것 같습니다). 다시 시작하거나 CreditDaddy를 업데이트해 주세요", "MiniMax Code 国内版": "MiniMax Code 국내판", - "MiniMax Code 国际版": "MiniMax Code 국제판" + "MiniMax Code 国际版": "MiniMax Code 국제판", + "秒悟积分": "Meoo 포인트", + "Meoo 秒悟": "Meoo", + "登录 meoo.com(阿里云账号手机号 + 短信)后捕获会话;CD 每日自动领取首登积分(当前 +2000/天,约 6~7 天后过期)并展示积分余额。会话失效(NEED_LOGIN)时请重新登录捕获,Cookie 只存本机。": "meoo.com에 (Aliyun 계정, 휴대폰 + SMS) 로그인한 뒤 세션을 캡처하세요. CD가 매일 첫 로그인 포인트를 자동으로 받고(현재 +2000/일, 약 6~7일 후 만료) 포인트 잔액을 표시합니다. 세션이 만료되면(NEED_LOGIN) 다시 로그인해 캡처하세요. 쿠키는 이 기기에만 저장됩니다.", + "Meoo 会话已失效(NEED_LOGIN),请重新登录捕获": "Meoo 세션이 만료되었습니다(NEED_LOGIN). 다시 로그인해 캡처하세요", + "{name} 就地重探通过({identity},积分 {balance})": "{name} 재검사 통과({identity}, 포인트 {balance})", + "未读到 Meoo 登录会话,请在登录窗完成登录后再关闭": "Meoo 로그인 세션을 읽지 못했습니다. 로그인을 완료한 뒤 창을 닫으세요", + "{name} 权益接口探测被拒:{code}": "{name} 권익 API 검사 거부: {code}", + "{name} 探测通过({identity},积分 {balance})": "{name} 검사 통과({identity}, 포인트 {balance})", + "{name} 权益接口抓取异常:{err}": "{name} 권익 API 조회 오류: {err}", + "Meoo 会话缺失,请在「网页会话」页登录 meoo.com 后捕获": "Meoo 세션이 없습니다. 「웹 세션」 페이지에서 meoo.com에 로그인해 캡처하세요", + "Meoo 首登积分领取失败:{err}": "Meoo 첫 로그인 포인트 수령 실패: {err}", + "Meoo 会话已失效({code}),请到「网页会话」页重新登录 meoo.com 并捕获": "Meoo 세션이 만료되었습니다({code}). 「웹 세션」 페이지에서 meoo.com에 다시 로그인해 캡처하세요", + "Meoo 账号权益仍在初始化,稍后自动重试": "Meoo 계정 권익이 아직 초기화 중입니다. 잠시 후 자동으로 재시도합니다", + "Meoo 首登积分领取成功": "Meoo 첫 로그인 포인트 수령 성공", + "首登积分已发放(+{n} 积分)": "첫 로그인 포인트 지급(+{n} 포인트)", + "今日首登积分已发放": "오늘의 첫 로그인 포인트는 이미 지급됨", + "Meoo 积分查询失败:{err}": "Meoo 포인트 조회 실패: {err}", + "每日首登": "매일 첫 로그인", + "邀请奖励": "초대 보상", + "订阅": "구독", + "积分余额": "포인트 잔액", + "积分余额 ": "포인트 잔액 ", + "(最早 ": "(가장 이른 만료 " } diff --git a/src/i18n/zh-TW.json b/src/i18n/zh-TW.json index 4b6e3ce..a681ba2 100644 --- a/src/i18n/zh-TW.json +++ b/src/i18n/zh-TW.json @@ -2001,5 +2001,28 @@ "回调里的 AuthCode 已过期,请重新发起": "回調裡的 AuthCode 已過期,請重新發起", "回调里没有 AuthCode(授权页协议可能又变了),请重新发起或更新 CreditDaddy": "回調裡沒有 AuthCode(授權頁協議可能又變了),請重新發起或更新 CreditDaddy", "MiniMax Code 国内版": "MiniMax Code 國內版", - "MiniMax Code 国际版": "MiniMax Code 國際版" + "MiniMax Code 国际版": "MiniMax Code 國際版", + "秒悟积分": "秒悟積分", + "Meoo 秒悟": "Meoo 秒悟", + "登录 meoo.com(阿里云账号手机号 + 短信)后捕获会话;CD 每日自动领取首登积分(当前 +2000/天,约 6~7 天后过期)并展示积分余额。会话失效(NEED_LOGIN)时请重新登录捕获,Cookie 只存本机。": "登入 meoo.com(阿里雲帳號手機號 + 簡訊)後擷取工作階段;CD 每日自動領取首登積分(目前 +2000/天,約 6~7 天後過期)並顯示積分餘額。工作階段失效(NEED_LOGIN)時請重新登入擷取,Cookie 只存本機。", + "Meoo 会话已失效(NEED_LOGIN),请重新登录捕获": "Meoo 工作階段已失效(NEED_LOGIN),請重新登入擷取", + "{name} 就地重探通过({identity},积分 {balance})": "{name} 就地重探通過({identity},積分 {balance})", + "未读到 Meoo 登录会话,请在登录窗完成登录后再关闭": "未讀到 Meoo 登入工作階段,請在登入視窗完成登入後再關閉", + "{name} 权益接口探测被拒:{code}": "{name} 權益介面探測被拒:{code}", + "{name} 探测通过({identity},积分 {balance})": "{name} 探測通過({identity},積分 {balance})", + "{name} 权益接口抓取异常:{err}": "{name} 權益介面抓取異常:{err}", + "Meoo 会话缺失,请在「网页会话」页登录 meoo.com 后捕获": "Meoo 工作階段缺失,請在「網頁工作階段」頁登入 meoo.com 後擷取", + "Meoo 首登积分领取失败:{err}": "Meoo 首登積分領取失敗:{err}", + "Meoo 会话已失效({code}),请到「网页会话」页重新登录 meoo.com 并捕获": "Meoo 工作階段已失效({code}),請到「網頁工作階段」頁重新登入 meoo.com 並擷取", + "Meoo 账号权益仍在初始化,稍后自动重试": "Meoo 帳號權益仍在初始化,稍後自動重試", + "Meoo 首登积分领取成功": "Meoo 首登積分領取成功", + "首登积分已发放(+{n} 积分)": "首登積分已發放(+{n} 積分)", + "今日首登积分已发放": "今日首登積分已發放", + "Meoo 积分查询失败:{err}": "Meoo 積分查詢失敗:{err}", + "每日首登": "每日首登", + "邀请奖励": "邀請獎勵", + "订阅": "訂閱", + "积分余额": "積分餘額", + "积分余额 ": "積分餘額 ", + "(最早 ": "(最早 " } diff --git a/src/meooClient.js b/src/meooClient.js new file mode 100644 index 0000000..efde2e2 --- /dev/null +++ b/src/meooClient.js @@ -0,0 +1,250 @@ +/** + * Meoo 秒悟(meoo.com,阿里云)每日首登积分 + 积分余额抓取。 + * + * 接口(2026-11-13 网页端 bundle 与本机实测): + * GET https://meoo.com/api/mc/entitlement/init + * 网页前端登录后调用它触发「每日首次登录」积分发放(当前 2000 积分/天)。 + * → { success: true, data: { initialized, dailyLoginGranted, dailyLoginGrantAmount, inviteRewardArrivals } } + * initialized=false 时前端按 500/1000/2000/4000ms 退避重查(账号权益尚在初始化)。 + * GET https://meoo.com/api/mc/entitlement/get + * → data.userPointInfo.account.availableBalance(积分余额)、data.planCode、 + * data.benefits[](typeCode=daily_login 的 quota = 每日首登额度) + * GET https://meoo.com/api/v1/user/me + * → data.user_id / username / email;未登录 data.need_login=true + * + * 凭据 = meoo.com 网页会话 Cookie(阿里云 SSO 手机号 + 短信登录后由浏览器写入), + * 不支持 Meoo CLI 的 OAuth Bearer(/api/mc/* 一律 NEED_LOGIN)。会话寿命未知—— + * 失效统一表现为 HTTP 401 或 { success:false, errCode:'NEED_LOGIN' | 'LOGIN_INVALID' }, + * 按「会话已失效,请重新登录捕获」处理,不重试、不猜。 + * + * 首版边界:首登积分 + 余额展示;每日首登积分约 6~7 天后过期、Free 套餐无月度积分; + * 不推 10Router 外部配额卡(site.no10rSync),不做网关。 + */ + +import { fetchJsonRace } from './zcodeClient.js'; +import { FETCH_TIMEOUT_MS } from './constants.js'; +import { t } from './i18n.js'; + +export const MEOO_BASE = 'https://meoo.com'; +export const MEOO_INIT_URL = `${MEOO_BASE}/api/mc/entitlement/init`; +export const MEOO_ENTITLEMENT_URL = `${MEOO_BASE}/api/mc/entitlement/get`; +export const MEOO_USER_URL = `${MEOO_BASE}/api/v1/user/me`; + +/** 前端同款退避(毫秒):initialized=false 时最多再查这几次 */ +export const MEOO_INIT_RETRY_MS = [500, 1000, 2000]; + +const AUTH_CODES = new Set(['NEED_LOGIN', 'LOGIN_INVALID', 'LOGIN_REQUIRED', 'NOT_LOGIN', 'UNAUTHORIZED']); + +function meooHeaders(cookie) { + return { + Cookie: String(cookie || '').trim(), + Referer: `${MEOO_BASE}/`, + Origin: MEOO_BASE, + Accept: 'application/json, text/plain, */*', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36', + }; +} + +/** + * GET 一个 Meoo 网页端点,归一成 { auth, ok, status, data, code }。 + * auth=false = 会话失效(401/403 或业务码 NEED_LOGIN 类,或 user/me 的 need_login=true)。 + */ +async function meooGet(url, cookie, fetchImpl) { + const doIt = fetchImpl || ((u, init) => fetchJsonRace(u, { headers: init.headers, method: 'GET', timeoutMs: FETCH_TIMEOUT_MS })); + const r = await doIt(url, { headers: meooHeaders(cookie) }); + const text = await r.text().catch(() => ''); + let body = null; + try { body = text ? JSON.parse(text) : null; } catch { body = null; } + const code = String(body?.errCode ?? body?.error?.code ?? body?.code ?? ''); + if (r.status === 401 || r.status === 403 || AUTH_CODES.has(code) || body?.data?.need_login === true) { + return { auth: false, ok: false, status: r.status, code: code || `HTTP ${r.status}` }; + } + if (!r.ok) return { auth: true, ok: false, status: r.status, code: code || `HTTP ${r.status}`, message: body?.errMsg || body?.message || '' }; + if (!body || typeof body !== 'object') return { auth: true, ok: false, status: r.status, code: 'non-json' }; + if (body.success === false) return { auth: true, ok: false, status: r.status, code: code || 'success=false', message: body.errMsg || body.message || '' }; + return { auth: true, ok: true, status: r.status, data: body.data ?? body }; +} + +function authError(msg) { + const e = new Error(msg); + e.auth = true; + return e; +} + +/** + * 用户 id 展示脱敏(与灵犀 uid 同口径):≥7 位前 3 后 3,4~6 位前 1 后 1,更短全掩。 + * 只用于行名等显示位;归户哈希 / 账号 uid 仍用原始值。 + */ +export function maskMeooUserId(id) { + const s = String(id ?? ''); + if (s.length >= 7) return s.slice(0, 3) + '****' + s.slice(-3); + if (s.length >= 4) return s.slice(0, 1) + '****' + s.slice(-1); + return '****'; +} + +/** + * 身份:GET /api/v1/user/me。 + * @returns {Promise<{auth: boolean, ok?: boolean, userId?: string|null, code?: string}>} + * username / email 可能是手机号,不回传(只取稳定 user_id 做归户)。 + */ +export async function fetchMeooIdentity(cookie, { fetchImpl } = {}) { + try { + const r = await meooGet(MEOO_USER_URL, cookie, fetchImpl); + if (!r.auth) return { auth: false, code: r.code }; + if (!r.ok) return { auth: true, ok: false, code: r.code }; + const id = r.data?.user_id ?? r.data?.userId ?? r.data?.open_id ?? null; + return { auth: true, ok: true, userId: id != null && String(id) ? String(id) : null }; + } catch (e) { + return { auth: true, ok: false, code: e?.message || 'network' }; + } +} + +/** 把一条 activeGrants 记录拍平成 { source, remaining, total, effectiveAt, expireAt } 列表(形状尽力而为) */ +function flattenGrants(grants) { + const out = []; + for (const g of Array.isArray(grants) ? grants : []) { + const source = String(g?.sourceType || g?.source || ''); + const details = Array.isArray(g?.grantDetails) && g.grantDetails.length ? g.grantDetails : [g]; + for (const d of details) { + const remaining = Number(d?.remainingAmount ?? d?.remaining ?? d?.availableAmount); + if (!Number.isFinite(remaining)) continue; + const total = Number(d?.grantAmount ?? d?.amount ?? d?.totalAmount); + out.push({ + source, + remaining, + total: Number.isFinite(total) ? total : remaining, + effectiveAt: d?.effectiveDate || null, + expireAt: d?.expiresDate || d?.expireDate || null, + }); + } + } + return out; +} + +/** + * 权益与积分余额:GET /api/mc/entitlement/get。 + * @returns {Promise<{auth: boolean, ok?: boolean, balance?: number, planCode?: string, dailyLoginQuota?: number, + * grants?: Array<{source, remaining, total, effectiveAt, expireAt}>, code?: string}>} + */ +export async function fetchMeooEntitlement(cookie, { fetchImpl } = {}) { + try { + const r = await meooGet(MEOO_ENTITLEMENT_URL, cookie, fetchImpl); + if (!r.auth) return { auth: false, code: r.code }; + if (!r.ok) return { auth: true, ok: false, code: r.code }; + const d = r.data || {}; + const upi = d.userPointInfo || {}; + const out = { auth: true, ok: true }; + const bal = Number(upi.account?.availableBalance); + if (Number.isFinite(bal)) out.balance = bal; + if (d.planCode) out.planCode = String(d.planCode).toLowerCase(); + const daily = Array.isArray(d.benefits) ? d.benefits.find((b) => b?.typeCode === 'daily_login') : null; + const dq = Number(daily?.quota); + if (Number.isFinite(dq) && dq > 0) out.dailyLoginQuota = dq; + const grants = flattenGrants(upi.activeGrants); + if (grants.length) out.grants = grants; + return out; + } catch (e) { + return { auth: true, ok: false, code: e?.message || 'network' }; + } +} + +function isoOrNull(v) { + if (!v) return null; + const d = new Date(v); + return Number.isFinite(d.getTime()) ? d.toISOString() : null; +} + +const sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * 每日首登积分:调 entitlement/init(网页端登录后同款调用)。 + * - dailyLoginGranted=true 且金额>0 → checked-in(+N 积分) + * - initialized=true 但本次没有发放 → already(今日已发放,多为网页端已登录过) + * - initialized 一直为 false(前端同款退避后仍未就绪)→ failed,下轮自动重试 + * - NEED_LOGIN / 401 → failed,提示重新登录捕获(不抛,签到轮记一次失败即可) + * @returns {Promise<{ status: 'checked-in'|'already'|'failed', message: string, claimedAmount?: number, amount?: number, balanceAfter?: number, authExpired?: boolean }>} + */ +export async function checkinMeoo(account, ctx = {}) { + const cookie = account?.meta?.cookieSiteSession?.cookie || account?.token; + if (!cookie) { + return { status: 'failed', authExpired: true, message: t('Meoo 会话缺失,请在「网页会话」页登录 meoo.com 后捕获') }; + } + const fetchImpl = ctx?.fetchImpl; + const sleep = ctx?.sleep || sleepMs; + let r; + try { + r = await meooGet(MEOO_INIT_URL, cookie, fetchImpl); + for (const ms of MEOO_INIT_RETRY_MS) { + if (!r.auth || !r.ok || r.data?.initialized) break; + await sleep(ms); + r = await meooGet(MEOO_INIT_URL, cookie, fetchImpl); + } + } catch (e) { + return { status: 'failed', message: t('Meoo 首登积分领取失败:{err}', { err: e?.message || 'network' }) }; + } + if (!r.auth) { + return { status: 'failed', authExpired: true, message: t('Meoo 会话已失效({code}),请到「网页会话」页重新登录 meoo.com 并捕获', { code: r.code || 'NEED_LOGIN' }) }; + } + if (!r.ok) { + return { status: 'failed', message: t('Meoo 首登积分领取失败:{err}', { err: [r.code, r.message].filter(Boolean).join(' ').slice(0, 120) }) }; + } + const data = r.data || {}; + if (!data.initialized) { + return { status: 'failed', message: t('Meoo 账号权益仍在初始化,稍后自动重试') }; + } + // 余额旁路(失败不影响领取结果) + let balanceAfter; + try { + const ent = await fetchMeooEntitlement(cookie, { fetchImpl }); + if (ent.ok && Number.isFinite(ent.balance)) balanceAfter = ent.balance; + } catch { /* 余额拿不到不阻断 */ } + const amount = Number(data.dailyLoginGrantAmount); + if (data.dailyLoginGranted && Number.isFinite(amount) && amount > 0) { + ctx?.log?.(t('Meoo 首登积分领取成功')); + return { + status: 'checked-in', + message: t('首登积分已发放(+{n} 积分)', { n: amount }), + claimedAmount: amount, + amount, + ...(balanceAfter !== undefined ? { balanceAfter } : {}), + }; + } + return { + status: 'already', + message: t('今日首登积分已发放'), + ...(balanceAfter !== undefined ? { balanceAfter } : {}), + }; +} + +/** + * 积分余额(quota 口径:remaining = 可用积分;parts 按发放来源逐笔,带到期日) + */ +export async function fetchMeooQuota(account, ctx = {}) { + const cookie = account?.meta?.cookieSiteSession?.cookie || account?.token; + if (!cookie) throw authError(t('Meoo 会话缺失,请在「网页会话」页登录 meoo.com 后捕获')); + const ent = await fetchMeooEntitlement(cookie, { fetchImpl: ctx?.fetchImpl }); + if (!ent.auth) throw authError(t('Meoo 会话已失效({code}),请到「网页会话」页重新登录 meoo.com 并捕获', { code: ent.code || 'NEED_LOGIN' })); + if (!ent.ok) throw new Error(t('Meoo 积分查询失败:{err}', { err: ent.code || '' })); + const balance = Number.isFinite(ent.balance) ? ent.balance : 0; + const SOURCE_LABEL = { DAILY_LOGIN: '每日首登', INVITE_REWARD: '邀请奖励', SUBSCRIPTION: '订阅' }; + const parts = (ent.grants || []).map((g) => ({ + name: t(SOURCE_LABEL[g.source] || '积分包'), + total: g.total, + used: Math.max(0, g.total - g.remaining), + remaining: g.remaining, + expiresAt: isoOrNull(g.expireAt), + recurring: false, + unit: '秒悟积分', + })); + return { + total: balance, + used: 0, + remaining: balance, + exceeded: false, + plan: ent.planCode ? `Meoo ${ent.planCode}` : 'Meoo', + planExpiresAt: null, + parts: parts.length ? parts : [{ name: t('积分余额'), total: balance, used: 0, remaining: balance, recurring: true, unit: '秒悟积分' }], + empty: balance <= 0, + }; +} diff --git a/src/panel.html b/src/panel.html index 4ec28ed..ca4a4b0 100644 --- a/src/panel.html +++ b/src/panel.html @@ -1303,7 +1303,7 @@ const CHECKIN_PRODUCTS = new Set(['qoder', 'workbuddy', 'trae', 'minimax']); // 网页会话卡支持「立即签到」的站点:provider 在 providers.js 挂了 checkin 且非 entryOnly // (豆包虽挂了 checkin 实现,但定位是纯打开入口、CD 不代签,不在此列) -const FREE_CHECKIN_SITES = new Set(['lingxi']); +const FREE_CHECKIN_SITES = new Set(['lingxi', 'meoo']); const CLIENT_LABEL = { qoder: 'Qoder', workbuddy: 'WorkBuddy', zcode: 'ZCode', mirasim: 'mirasim', catpaw: '妙手', trae: 'Trae', minimax: 'MiniMax', loomy: 'Loomy' }; // 产品页顶部改用「有效套餐 N/M」而不是「剩余积分」的产品(单位不是积分:Token / % / Credits)。 // ⚠️ 只影响**产品页头部文案**;「能不能并进仪表盘剩余积分合计」由下方的 CREDIT_SUM_EXCLUDED @@ -3039,6 +3039,18 @@ return '' + T('智点余额 ') + Number(bal).toLocaleString() + '' + detail; } } + if (site.id === 'meoo') { + // 积分余额 + 最早到期日(每日首登积分约 6~7 天后过期) + const bal = acc.quota?.total; + if (Number.isFinite(bal)) { + const grants = acc.meooGrants?.grants; + const firstExp = Array.isArray(grants) ? (grants.map((g) => g.expireAt).filter(Boolean).sort()[0] || '') : ''; + const detail = firstExp + ? '' + T('(最早 ') + esc(String(firstExp).slice(0, 10)) + T(' 到期)') + '' + : ''; + return '' + T('积分余额 ') + Number(bal).toLocaleString() + '' + detail; + } + } if (site.id === 'tokenharbor') { const used = acc.quota?.used; if (Number.isFinite(used)) { diff --git a/src/providers.js b/src/providers.js index 02e75fa..31abc8d 100644 --- a/src/providers.js +++ b/src/providers.js @@ -21,6 +21,7 @@ const isTraeIntl = (account) => account?.provider === 'trae-intl' || Boolean(acc import { fetchMiniMaxQuota, checkinMiniMax, verifyMiniMaxAccount } from './minimaxClient.js'; import { fetchDoubaoStatus, checkinDoubao } from './doubaoClient.js'; import { checkinLingxi, fetchLingxiQuota } from './lingxiClient.js'; +import { checkinMeoo, fetchMeooQuota } from './meooClient.js'; import { fetchLoomyQuota, verifyLoomyAccount } from './loomyClient.js'; /** 从 userinfo 响应中挑一个可读的显示名 */ export function displayNameFrom(ui) { @@ -256,6 +257,14 @@ const PRODUCTS = { checkin: (account) => checkinLingxi(account), quota: (account) => fetchLingxiQuota(account), }, + meoo: { + label: 'Meoo 秒悟', + // 网页会话捕获线:凭据是 meoo.com 会话 cookie(meta.cookieSiteSession.cookie,阿里云 SSO 登录)。 + // 「签到」= 调网页端同款 entitlement/init 触发每日首登积分(2000/天,约 6~7 天后过期); + // NEED_LOGIN 记失败并提示重新捕获。额度 = 积分余额(独立口径,unit '秒悟积分' 不进剩余积分合计)。 + checkin: (account) => checkinMeoo(account), + quota: (account) => fetchMeooQuota(account), + }, loomy: { label: '讯飞 Loomy', quota: (account) => fetchLoomyQuota(account), diff --git a/test/meoo.test.js b/test/meoo.test.js new file mode 100644 index 0000000..11e80ee --- /dev/null +++ b/test/meoo.test.js @@ -0,0 +1,159 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { + checkinMeoo, fetchMeooQuota, fetchMeooEntitlement, fetchMeooIdentity, maskMeooUserId, + MEOO_INIT_URL, MEOO_ENTITLEMENT_URL, MEOO_USER_URL, +} from '../src/meooClient.js'; +import { getSite } from '../src/cookieSites.js'; +import { PROVIDERS, productOf } from '../src/constants.js'; +import { productImpl } from '../src/providers.js'; + +const res = (status, body) => ({ status, ok: status >= 200 && status < 300, text: async () => JSON.stringify(body) }); +const NEED_LOGIN = { success: false, errCode: 'NEED_LOGIN', errMsg: 'need login' }; + +const entBody = (balance = 4000, extra = {}) => ({ + success: true, + data: { + planCode: 'FREE', + benefits: [{ typeCode: 'daily_login', quota: 2000 }, { typeCode: 'model_credits', quota: 0, used: 0 }], + userPointInfo: { account: { availableBalance: balance, totalGranted: 6000, totalConsumed: 2000 }, ...extra }, + }, +}); + +/** 按 URL 分发的假 fetch;init 支持按调用次序返回不同响应 */ +function mockFetch({ init = [], ent = res(200, entBody()), user = res(200, { success: true, data: { user_id: '1234567890' } }) } = {}) { + const calls = []; + let i = 0; + const fn = async (url, initOpts) => { + calls.push({ url, headers: initOpts?.headers }); + if (url === MEOO_INIT_URL) { const r = init[Math.min(i, init.length - 1)]; i++; return r; } + if (url === MEOO_ENTITLEMENT_URL) return ent; + if (url === MEOO_USER_URL) return user; + throw new Error(`unexpected url: ${url}`); + }; + fn.calls = calls; + return fn; +} + +const account = { meta: { cookieSiteSession: { cookie: 'session=test' } } }; +const noSleep = async () => {}; + +test('首登发放:dailyLoginGranted=true → checked-in +2000,并带回余额', async () => { + const f = mockFetch({ init: [res(200, { success: true, data: { initialized: true, dailyLoginGranted: true, dailyLoginGrantAmount: 2000, inviteRewardArrivals: [] } })] }); + const r = await checkinMeoo(account, { fetchImpl: f, sleep: noSleep }); + assert.equal(r.status, 'checked-in'); + assert.equal(r.claimedAmount, 2000); + assert.equal(r.balanceAfter, 4000); + assert.match(r.message, /\+2000/); + // 会话 cookie 原样带上,Referer/Origin 为 meoo.com + assert.equal(f.calls[0].headers.Cookie, 'session=test'); + assert.equal(f.calls[0].headers.Origin, 'https://meoo.com'); +}); + +test('当日已初始化且无发放 → already(不报虚假 +N)', async () => { + const f = mockFetch({ init: [res(200, { success: true, data: { initialized: true, dailyLoginGranted: false, dailyLoginGrantAmount: 0 } })] }); + const r = await checkinMeoo(account, { fetchImpl: f, sleep: noSleep }); + assert.equal(r.status, 'already'); + assert.equal(r.claimedAmount, undefined); +}); + +test('initialized=false 按前端同款退避重查,就绪后发放', async () => { + const f = mockFetch({ init: [ + res(200, { success: true, data: { initialized: false } }), + res(200, { success: true, data: { initialized: true, dailyLoginGranted: true, dailyLoginGrantAmount: 2000 } }), + ] }); + const r = await checkinMeoo(account, { fetchImpl: f, sleep: noSleep }); + assert.equal(r.status, 'checked-in'); + assert.equal(f.calls.filter((c) => c.url === MEOO_INIT_URL).length, 2); +}); + +test('initialized 一直 false → failed(下轮自动重试),重查次数有上限', async () => { + const f = mockFetch({ init: [res(200, { success: true, data: { initialized: false } })] }); + const r = await checkinMeoo(account, { fetchImpl: f, sleep: noSleep }); + assert.equal(r.status, 'failed'); + assert.ok(f.calls.filter((c) => c.url === MEOO_INIT_URL).length <= 4); +}); + +test('NEED_LOGIN(401)→ failed + authExpired,提示重新登录捕获,不抛异常', async () => { + const f = mockFetch({ init: [res(401, NEED_LOGIN)] }); + const r = await checkinMeoo(account, { fetchImpl: f, sleep: noSleep }); + assert.equal(r.status, 'failed'); + assert.equal(r.authExpired, true); + assert.match(r.message, /NEED_LOGIN/); + assert.match(r.message, /重新登录/); +}); + +test('HTTP 200 但业务码 NEED_LOGIN 同样判会话失效', async () => { + const f = mockFetch({ init: [res(200, NEED_LOGIN)] }); + const r = await checkinMeoo(account, { fetchImpl: f, sleep: noSleep }); + assert.equal(r.status, 'failed'); + assert.equal(r.authExpired, true); +}); + +test('无会话 cookie → failed,不发请求', async () => { + const f = mockFetch({ init: [res(200, {})] }); + const r = await checkinMeoo({ meta: {} }, { fetchImpl: f, sleep: noSleep }); + assert.equal(r.status, 'failed'); + assert.equal(f.calls.length, 0); +}); + +test('权益接口:余额 / 套餐 / 每日首登额度 / 逐笔发放(带到期日)', async () => { + const ent = res(200, entBody(4000, { activeGrants: [ + { sourceType: 'DAILY_LOGIN', grantDetails: [ + { remainingAmount: 2000, grantAmount: 2000, effectiveDate: '2026-11-12', expiresDate: '2026-11-19' }, + { remainingAmount: 0, grantAmount: 2000, effectiveDate: '2026-11-11', expiresDate: '2026-11-18' }, + ] }, + { sourceType: 'INVITE_REWARD', grantDetails: [{ remainingAmount: 2000, grantAmount: 2000, expiresDate: '2026-12-01' }] }, + ] })); + const e = await fetchMeooEntitlement('session=test', { fetchImpl: mockFetch({ ent }) }); + assert.equal(e.auth, true); + assert.equal(e.balance, 4000); + assert.equal(e.planCode, 'free'); + assert.equal(e.dailyLoginQuota, 2000); + assert.equal(e.grants.length, 3); + assert.deepEqual(e.grants[0], { source: 'DAILY_LOGIN', remaining: 2000, total: 2000, effectiveAt: '2026-11-12', expireAt: '2026-11-19' }); + + const q = await fetchMeooQuota(account, { fetchImpl: mockFetch({ ent }) }); + assert.equal(q.remaining, 4000); + assert.equal(q.parts.length, 3); + assert.equal(q.parts[1].used, 2000); + assert.ok(q.parts[0].expiresAt.startsWith('2026-11-19')); + assert.equal(q.parts[0].unit, '秒悟积分'); +}); + +test('权益接口无逐笔明细时 quota 兜底单行余额', async () => { + const q = await fetchMeooQuota(account, { fetchImpl: mockFetch() }); + assert.equal(q.total, 4000); + assert.equal(q.parts.length, 1); + assert.equal(q.empty, false); +}); + +test('quota 遇 NEED_LOGIN 抛 auth 错误', async () => { + await assert.rejects( + () => fetchMeooQuota(account, { fetchImpl: mockFetch({ ent: res(401, NEED_LOGIN) }) }), + (e) => e.auth === true && /NEED_LOGIN/.test(e.message), + ); +}); + +test('身份:user/me 取 user_id;need_login=true 判失效;展示脱敏', async () => { + const ok = await fetchMeooIdentity('session=test', { fetchImpl: mockFetch() }); + assert.equal(ok.userId, '1234567890'); + const dead = await fetchMeooIdentity('session=test', { fetchImpl: mockFetch({ user: res(200, { success: true, data: { need_login: true } }) }) }); + assert.equal(dead.auth, false); + assert.equal(maskMeooUserId('1234567890'), '123****890'); + assert.equal(maskMeooUserId('12345'), '1****5'); + assert.equal(maskMeooUserId('12'), '****'); +}); + +test('接线:站点表 / provider / 产品线 / 签到实现均已登记', () => { + const site = getSite('meoo'); + assert.ok(site); + assert.equal(site.provider, 'meoo'); + assert.equal(site.verify, 'api'); + assert.equal(site.no10rSync, true); + assert.deepEqual(site.cookieUrls, ['https://meoo.com']); + assert.ok(PROVIDERS.includes('meoo')); + assert.equal(productOf('meoo'), 'meoo'); + assert.equal(typeof productImpl('meoo').checkin, 'function'); + assert.equal(typeof productImpl('meoo').quota, 'function'); +});