diff --git a/cli-manifest.json b/cli-manifest.json index 99ae2eb56..4724cd452 100644 --- a/cli-manifest.json +++ b/cli-manifest.json @@ -9804,6 +9804,34 @@ "navigateBefore": false, "siteSession": "persistent" }, + { + "site": "deepseek", + "name": "usage", + "description": "Read DeepSeek platform usage: balance, cumulative spending, time-dimension spending, API requests, and Tokens.", + "access": "read", + "domain": "platform.deepseek.com", + "strategy": "cookie", + "browser": true, + "args": [], + "columns": [ + "balance", + "bonusBalance", + "cumulativeSpend", + "monthlySpend", + "monthlyApiCalls", + "monthlyTokens", + "currentTokenEstimation", + "timePeriod", + "periodSpend", + "periodApiCalls", + "periodTokens" + ], + "type": "js", + "modulePath": "deepseek/usage.js", + "sourceFile": "deepseek/usage.js", + "navigateBefore": true, + "siteSession": "persistent" + }, { "site": "deepseek", "name": "whoami", @@ -21123,21 +21151,25 @@ { "site": "kimi", "name": "usage", - "description": "Read Kimi Code console usage cards: weekly quota, rate limit, membership, and model permission.", + "description": "Read Kimi membership quota usage from the subscription page: total usage, rate limits, gift quota, and booster balance.", "access": "read", "domain": "kimi.com", "strategy": "cookie", "browser": true, "args": [], "columns": [ - "weeklyUsagePct", - "weeklyResetIn", - "rateLimitPct", - "rateLimitResetIn", "membershipName", - "membershipTier", - "modelPermission", - "modelCost" + "membershipValidUntil", + "totalUsagePct", + "totalResetIn", + "fiveHourUsagePct", + "fiveHourResetIn", + "sevenDayUsagePct", + "sevenDayResetIn", + "giftUsagePct", + "giftValidUntil", + "balance", + "monthlySpend" ], "type": "js", "modulePath": "kimi/usage.js", diff --git a/clis/deepseek/usage.js b/clis/deepseek/usage.js new file mode 100644 index 000000000..6194c535b --- /dev/null +++ b/clis/deepseek/usage.js @@ -0,0 +1,178 @@ +// DeepSeek platform usage summary. +// Reads data from the https://platform.deepseek.com/usage page. +// Uses the internal API for account summary + innerText extraction for time-dimension cards. + +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { CommandExecutionError } from '@jackwener/opencli/errors'; + +const DS_DOMAIN = 'platform.deepseek.com'; +const USAGE_URL = 'https://platform.deepseek.com/usage'; + +// This code runs in the browser via page.evaluate. +// Backslash sequences are doubled because they are inside a JS template literal. +const EVAL_JS = ` + var BASE = 'https://platform.deepseek.com'; + + // --- Auth: read token from localStorage --- + let token = ''; + var raw = localStorage.getItem('userToken'); + if (raw) { try { var parsed = JSON.parse(raw); token = parsed.value || ''; } catch(e) { token = raw; } } + + async function fetchJson(url) { + var headers = { Accept: 'application/json' }; + if (token) headers['Authorization'] = 'Bearer ' + token; + var r = await fetch(url, { headers: headers }); + if (!r.ok) throw new Error('HTTP ' + r.status); + var d = await r.json(); + if (d.code !== 0) throw new Error('API code=' + d.code); + var biz = d.data; + if (biz && biz.biz_code !== 0) throw new Error('API biz_code=' + biz.biz_code); + return biz && biz.biz_data; + } + + // --- API: user summary --- + async function fetchApiData() { + var summary = await fetchJson(BASE + '/api/v0/users/get_user_summary'); + var normalW = (summary.normal_wallets || []).find(function(w) { return w.currency === 'CNY'; }); + var bonusW = (summary.bonus_wallets || []).find(function(w) { return w.currency === 'CNY'; }); + return { + balance: normalW ? Number(normalW.balance).toFixed(2) : '0', + bonusBalance: bonusW ? Number(bonusW.balance).toFixed(2) : '0', + cumulativeSpend: (summary.total_costs || []).length > 0 + ? Number(summary.total_costs[0].amount).toFixed(2) : '0', + monthlySpend: (summary.monthly_costs || []).length > 0 + ? Number(summary.monthly_costs[0].amount).toFixed(2) : '0', + monthlyTokens: summary.monthly_token_usage || '0', + monthlyApiCalls: String(summary.total_usage || 0), + currentTokenEstimation: summary.total_available_token_estimation || '0', + }; + } + + // --- DOM: extract period card data from innerText --- + function extractDomData() { + var text = document.body.innerText; + var lines = text.split('\\n'); + var result = {}; + + // Find time period label (e.g. "近 7 天", "本月") + for (var i = 0; i < lines.length; i++) { + var m = lines[i].match(/近\\s*\\d+\\s*[天月]/); + if (m) { result.timePeriod = m[0]; break; } + } + + // Find period spend / API calls / Tokens + // These appear after "导出" and before "消费金额(CNY)" + var foundCount = 0; + for (var i = 0; i < lines.length && foundCount < 3; i++) { + var line = lines[i].trim(); + + if (line === '消费金额') { + // Skip if preceded by "累计消费" or "充值余额" + var prevBlock = (i > 0 ? lines[i-1] : '') + (i > 1 ? lines[i-2] : '') + (i > 2 ? lines[i-3] : ''); + if (prevBlock.includes('累计消费') || prevBlock.includes('充值余额')) continue; + // Look for ¥ value in next lines + for (var j = i + 1; j < Math.min(i + 4, lines.length); j++) { + var val = lines[j].trim(); + var vm = val.match(/^[¥¥]\\s*([\\d,.]+)/); + if (vm) { + result.periodSpend = vm[1].replace(/,/g, ''); + foundCount++; + break; + } + } + } else if (line === 'API 请求次数') { + for (var j = i + 1; j < Math.min(i + 4, lines.length); j++) { + var val = lines[j].trim(); + if (/^[\\d,]+$/.test(val)) { + result.periodApiCalls = val.replace(/,/g, ''); + foundCount++; + break; + } + } + } else if (line === 'Tokens') { + for (var j = i + 1; j < Math.min(i + 4, lines.length); j++) { + var val = lines[j].trim(); + if (/^[\\d,]+$/.test(val)) { + result.periodTokens = val.replace(/,/g, ''); + foundCount++; + break; + } + } + } + } + + return result; + } + + var domData = extractDomData(); + + // Call API and merge + return fetchApiData().then(function(apiData) { + return { + balance: apiData.balance, + bonusBalance: apiData.bonusBalance, + cumulativeSpend: apiData.cumulativeSpend, + monthlySpend: apiData.monthlySpend, + monthlyApiCalls: apiData.monthlyApiCalls, + monthlyTokens: apiData.monthlyTokens, + currentTokenEstimation: apiData.currentTokenEstimation, + timePeriod: domData.timePeriod || '近 7 天', + periodSpend: domData.periodSpend || '0', + periodApiCalls: domData.periodApiCalls || '0', + periodTokens: domData.periodTokens || '0', + }; + }); +`; + +cli({ + site: 'deepseek', + name: 'usage', + access: 'read', + description: 'Read DeepSeek platform usage: balance, cumulative spending, time-dimension spending, API requests, and Tokens.', + domain: DS_DOMAIN, + strategy: Strategy.COOKIE, + browser: true, + siteSession: 'persistent', + navigateBefore: true, + args: [], + columns: [ + 'balance', + 'bonusBalance', + 'cumulativeSpend', + 'monthlySpend', + 'monthlyApiCalls', + 'monthlyTokens', + 'currentTokenEstimation', + 'timePeriod', + 'periodSpend', + 'periodApiCalls', + 'periodTokens', + ], + func: async (page) => { + await page.goto(USAGE_URL); + await page.wait(3); + + const data = await page.evaluate(`(() => {${EVAL_JS}})()`); + + if (!data || typeof data !== 'object' || Array.isArray(data)) { + throw new CommandExecutionError('deepseek usage returned malformed payload: expected object'); + } + if (data.balance === undefined) { + throw new CommandExecutionError('deepseek usage returned malformed payload: missing balance'); + } + + return [{ + balance: String(data.balance), + bonusBalance: String(data.bonusBalance), + cumulativeSpend: String(data.cumulativeSpend), + monthlySpend: String(data.monthlySpend), + monthlyApiCalls: String(data.monthlyApiCalls), + monthlyTokens: String(data.monthlyTokens), + currentTokenEstimation: String(data.currentTokenEstimation), + timePeriod: String(data.timePeriod || '近 7 天'), + periodSpend: String(data.periodSpend), + periodApiCalls: String(data.periodApiCalls), + periodTokens: String(data.periodTokens), + }]; + }, +}); diff --git a/clis/kimi/usage.js b/clis/kimi/usage.js index 7751b5a23..9bde52d24 100644 --- a/clis/kimi/usage.js +++ b/clis/kimi/usage.js @@ -1,14 +1,11 @@ -// Kimi Code console usage summary. -// Reads the four dashboard cards from https://www.kimi.com/code/console +// Kimi membership quota usage summary. +// Reads usage cards from https://www.kimi.com/membership/subscription?tab=quota import { cli, Strategy } from '@jackwener/opencli/registry'; -import { - CommandExecutionError, -} from '@jackwener/opencli/errors'; +import { CommandExecutionError } from '@jackwener/opencli/errors'; const KIMI_DOMAIN = 'kimi.com'; -const KIMI_URL = 'https://www.kimi.com/'; -const CONSOLE_URL = `${KIMI_URL}code/console`; +const QUOTA_URL = 'https://www.kimi.com/membership/subscription?tab=quota'; const IS_VISIBLE_JS = ` const isVisible = (el) => { @@ -21,38 +18,27 @@ const IS_VISIBLE_JS = ` }; `; -const CATEGORIES = ['本周用量', '频限明细', '我的权益', '模型权限']; - function parsePct(value) { const m = String(value || '').match(/(\d+(?:\.\d+)?)\s*%/); return m ? Number(m[1]) : null; } -function requireCard(cards, name) { - const values = cards[name]; - if (!Array.isArray(values) || values.length === 0) { - throw new CommandExecutionError(`kimi usage returned malformed payload: missing "${name}" card`); - } - const normalized = values.map((value) => String(value || '').trim()).filter(Boolean); - if (normalized.length === 0) { - throw new CommandExecutionError(`kimi usage returned malformed payload: missing "${name}" card`); - } - return normalized; +function normalize(s) { + return String(s || '').trim(); } -function requirePct(values, name) { - const pct = parsePct(values[0]); - if (!Number.isFinite(pct)) { - throw new CommandExecutionError(`kimi usage returned malformed payload: "${name}" card is missing a percentage`); +function requireFinite(value, name) { + if (!Number.isFinite(value)) { + throw new CommandExecutionError(`kimi usage returned malformed payload: missing or invalid "${name}"`); } - return pct; + return value; } cli({ site: 'kimi', name: 'usage', access: 'read', - description: 'Read Kimi Code console usage cards: weekly quota, rate limit, membership, and model permission.', + description: 'Read Kimi membership quota usage from the subscription page: total usage, rate limits, gift quota, and booster balance.', domain: KIMI_DOMAIN, strategy: Strategy.COOKIE, browser: true, @@ -60,85 +46,105 @@ cli({ navigateBefore: true, args: [], columns: [ - 'weeklyUsagePct', - 'weeklyResetIn', - 'rateLimitPct', - 'rateLimitResetIn', 'membershipName', - 'membershipTier', - 'modelPermission', - 'modelCost', + 'membershipValidUntil', + 'totalUsagePct', + 'totalResetIn', + 'fiveHourUsagePct', + 'fiveHourResetIn', + 'sevenDayUsagePct', + 'sevenDayResetIn', + 'giftUsagePct', + 'giftValidUntil', + 'balance', + 'monthlySpend', ], func: async (page) => { - await page.goto(CONSOLE_URL); + await page.goto(QUOTA_URL); await page.wait(3); - const cards = await page.evaluate(`(() => { + const data = await page.evaluate(`(() => { ${IS_VISIBLE_JS} - const getDirectText = (el) => { - let text = ''; - for (const node of el.childNodes) { - if (node.nodeType === Node.TEXT_NODE) text += node.textContent; - } - return text.trim(); - }; - - const cards = {}; - const section = document.querySelector('section'); - if (!section) return cards; - - // The first child of the first
holds the 4 dashboard cards. - const cardContainer = section.children[0]; - if (!cardContainer) return cards; - - const cardDivs = Array.from(cardContainer.children).filter(isVisible).slice(0, 4); - for (const card of cardDivs) { - const header = card.children[0]; - const body = card.children[1]; - if (!header || !body) continue; - - const categoryEl = header.querySelector('p'); - const category = categoryEl ? getDirectText(categoryEl) : ''; - if (!category || !${JSON.stringify(CATEGORIES)}.includes(category)) continue; - - const values = [...new Set( - Array.from(body.querySelectorAll('span, p, div')) - .filter(isVisible) - .map(getDirectText) - .filter((t) => t) - )]; - - if (values.length > 0) { - cards[category] = values.slice(0, 3); + const result = {}; + + // Usage sections: total, 5-hour, 7-day, gift + const sections = Array.from(document.querySelectorAll('.usage-section')).filter(isVisible); + for (const section of sections) { + const titleEl = section.querySelector('.usage-section-title'); + const contentEl = section.querySelector('.usage-section-content'); + if (!titleEl || !contentEl) continue; + + const titleSpans = Array.from(titleEl.querySelectorAll('span')) + .filter(isVisible) + .map((s) => s.textContent.trim()); + const label = titleSpans[0] || ''; + const pct = titleSpans[1] || ''; + const contentText = contentEl.innerText.trim().replace(/\\s+/g, ' '); + + if (label === '总使用量') { + if (contentText.includes('后重置')) { + result.totalUsagePct = pct; + const m = contentText.match(/Kimi\\s*Code\\s*(.+)/); + result.totalResetIn = m ? m[1].trim() : null; + } else if (contentText.includes('截止至')) { + result.giftUsagePct = pct; + const m = contentText.match(/截止至\\s*(.+)/); + result.giftValidUntil = m ? m[1].trim() : null; + } + } else if (label === '5 小时用量') { + const m = contentText.match(/Code\\s+([\\d.]+)%\\s*(.+)/); + if (m) { + result.fiveHourUsagePct = m[1] + '%'; + result.fiveHourResetIn = m[2].trim(); + } + } else if (label === '7 天用量') { + const m = contentText.match(/Code\\s+([\\d.]+)%\\s*(.+)/); + if (m) { + result.sevenDayUsagePct = m[1] + '%'; + result.sevenDayResetIn = m[2].trim(); + } } } - return cards; + + // Booster balance + const booster = document.querySelector('.booster'); + if (booster && isVisible(booster)) { + const text = booster.innerText.trim().replace(/\\s+/g, ' '); + const balanceMatch = text.match(/当前余额\\s*¥\\s*([\\d.]+)/); + const spendMatch = text.match(/本月消费\\s*¥\\s*([\\d.]+)\\s*\\/\\s*(.+)/); + result.balance = balanceMatch ? '¥' + balanceMatch[1] : null; + result.monthlySpend = spendMatch ? '¥' + spendMatch[1] + ' / ' + spendMatch[2].trim() : null; + } + + // Membership header + const h1 = document.querySelector('h1'); + result.membershipName = h1 ? h1.textContent.trim() : null; + + const bodyText = document.body.innerText.trim().replace(/\\s+/g, ' '); + const validMatch = bodyText.match(/有效期至:\\s*(\\d{4}-\\d{2}-\\d{2})/); + result.membershipValidUntil = validMatch ? validMatch[1] : null; + + return result; })()`); - if (!cards || typeof cards !== 'object' || Array.isArray(cards)) { - throw new CommandExecutionError('kimi usage returned malformed payload: usage cards must be an object'); + if (!data || typeof data !== 'object' || Array.isArray(data)) { + throw new CommandExecutionError('kimi usage returned malformed payload: expected object'); } - if (Object.keys(cards).length === 0) { - throw new CommandExecutionError('kimi usage returned malformed payload: no usage cards found on the console page'); - } - - const weekly = requireCard(cards, '本周用量'); - const rate = requireCard(cards, '频限明细'); - const member = requireCard(cards, '我的权益'); - const model = requireCard(cards, '模型权限'); - const weeklyUsagePct = requirePct(weekly, '本周用量'); - const rateLimitPct = requirePct(rate, '频限明细'); return [{ - weeklyUsagePct, - weeklyResetIn: weekly.find((t) => /重置/.test(t)) || null, - rateLimitPct, - rateLimitResetIn: rate.find((t) => /重置/.test(t)) || null, - membershipName: member[0] || null, - membershipTier: member.find((t) => t !== member[0]) || null, - modelPermission: model[0] || null, - modelCost: model.find((t) => t !== model[0]) || null, + membershipName: normalize(data.membershipName) || null, + membershipValidUntil: data.membershipValidUntil || null, + totalUsagePct: requireFinite(parsePct(data.totalUsagePct), 'totalUsagePct'), + totalResetIn: normalize(data.totalResetIn) || null, + fiveHourUsagePct: parsePct(data.fiveHourUsagePct), + fiveHourResetIn: normalize(data.fiveHourResetIn) || null, + sevenDayUsagePct: parsePct(data.sevenDayUsagePct), + sevenDayResetIn: normalize(data.sevenDayResetIn) || null, + giftUsagePct: parsePct(data.giftUsagePct), + giftValidUntil: normalize(data.giftValidUntil) || null, + balance: normalize(data.balance) || null, + monthlySpend: normalize(data.monthlySpend) || null, }]; }, });