// ==UserScript== // @name Trae CN 每日签到 // @namespace https://scriptcat.org/ // @version 1.0 // @description 自动领取 Trae CN 每日签到积分,显示当前积分与连续签到天数 // @author Fiallsnu // @icon https://www.trae.cn/favicon.ico // @crontab 0 8-23 * * * // @connect api.trae.cn // @connect www.trae.cn // @connect trae.cn // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_cookie // @grant GM_openInTab // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // @grant GM_log // @license MIT // ==/UserScript== /* * Trae CN 每日签到 * * 原理:浏览器登录 https://www.trae.cn 后,Cookie 里有 X-Cloudide-Session。 * 用它换 JWT,再查询/领取每日签到积分。不需要账号密码。 * * 通知内容: * - 签到成功:本次获得积分、当前积分总量、连续签到天数 * - 今日已签到:当前积分 * - 重复点击:前三次显示积分,之后随机名言(点击复制),再之后小彩蛋 * * 隐私:只读取 trae.cn 自己的 X-Cloudide-Session 用于换 JWT; * 不打印、不保存、不上传该 Cookie,所有请求只发往 api.trae.cn。 */ return new Promise((resolve) => { const API = 'https://api.trae.cn'; const SITE = 'https://www.trae.cn'; const UA = 'VSCode 1.107.1 (TRAE SOLO CN)'; const MAX_DEVICE_RETRY = 2; const QUOTES = [ '种一棵树最好的时间是十年前,其次是现在。', 'Stay hungry, stay foolish. —— Steve Jobs', '不积跬步,无以至千里;不积小流,无以成江海。—— 荀子', '路漫漫其修远兮,吾将上下而求索。—— 屈原', 'The best way to predict the future is to invent it. —— Alan Kay', '生活的理想,就是为了理想的生活。—— 张闻天', '千里之行,始于足下。—— 老子', '纸上得来终觉浅,绝知此事要躬行。—— 陆游', '天行健,君子以自强不息。——《周易》', '宝剑锋从磨砺出,梅花香自苦寒来。', 'The only way to do great work is to love what you do. —— Steve Jobs', 'Simplicity is the ultimate sophistication. —— Leonardo da Vinci', '知之为知之,不知为不知,是知也。—— 孔子', '长风破浪会有时,直挂云帆济沧海。—— 李白', '日拱一卒,功不唐捐。', '凡是过往,皆为序章。—— 莎士比亚', '慢慢来,比较快。' ]; const EGGS = [ '积分没涨,但你的手速 +1。', '再点下去,签到按钮就要包浆了。', 'Trae 说它记住你了,明天再来。' ]; function today() { return fmt(new Date()); } function yesterday() { const d = new Date(); d.setDate(d.getDate() - 1); return fmt(d); } function fmt(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); } function pad(n) { return n < 10 ? '0' + n : '' + n; } function randomDevice() { return String(Math.floor(1e15 + Math.random() * 9e15)); } // 牌堆式抽取:一轮内不重复,抽完自动重置,避免名言反复出现 function nextQuote() { let pool = GM_getValue('trae_quote_pool', []); if (!Array.isArray(pool)) pool = []; pool = pool.filter(function (i) { return typeof i === 'number' && i >= 0 && i < QUOTES.length; }); if (!pool.length) { pool = QUOTES.map(function (_, i) { return i; }); } const idx = Math.floor(Math.random() * pool.length); const q = QUOTES[pool[idx]]; pool.splice(idx, 1); GM_setValue('trae_quote_pool', pool); return q; } function notify(title, text, open) { const o = { title: 'Trae:' + title, text: text || ' ', timeout: 12000 }; if (open) { o.onclick = function () { GM_openInTab(SITE + '/'); }; } GM_notification(o); } function notifyCopy(title, text) { GM_notification({ title: 'Trae:' + title, text: text, timeout: 15000, onclick: function () { GM_setClipboard(text); GM_notification({ title: 'Trae:已复制', text: text, timeout: 4000 }); } }); } function readSession(cb) { GM_cookie.list({ url: SITE + '/', name: 'X-Cloudide-Session' }, function (cookies, err) { if (err) { GM_log('[Trae] 读取 cookie 失败:' + err, 'error'); return cb(''); } cb(cookies && cookies.length ? cookies[0].value : ''); }); } function body(r) { if (r && r.response && typeof r.response === 'object') return r.response; try { return JSON.parse((r && r.responseText) || '{}'); } catch (e) { return {}; } } function post(path, headers, data, cb) { GM_xmlhttpRequest({ method: 'POST', url: API + path, headers: headers, data: data || '', responseType: 'json', timeout: 15000, onload: function (r) { cb(r.status, body(r)); }, onerror: function (e) { GM_log('[Trae] 网络错误:' + e, 'warn'); cb(0, {}); }, ontimeout: function () { GM_log('[Trae] 请求超时', 'warn'); cb(0, {}); } }); } function authError(code, message) { if ([401, 1001, 1002, 1003, 10085].indexOf(Number(code)) >= 0) return true; return /登录|过期|认证|未授权|authenticate|unauthorized/i.test(String(message || '')); } function loginExpired() { notify('登录已失效', '请打开 trae.cn 重新登录后再试', true); resolve('登录失效'); } function getToken(session, cb) { post('/cloudide/api/v3/common/GetUserToken', { 'Cookie': 'X-Cloudide-Session=' + session, 'Referer': SITE + '/', 'Origin': SITE, 'User-Agent': UA, 'Accept': 'application/json, text/plain, */*' }, '', function (status, d) { const token = (d.Result && d.Result.Token) || d.Token || ''; if (status !== 200 || !token) { GM_log('[Trae] GetUserToken 失败 HTTP ' + status, 'warn'); return cb(''); } cb(token); }); } function apiHeaders(token, device) { return { 'Authorization': 'Cloud-IDE-JWT ' + token, 'X-User-Region': 'cn', 'x-device-id': device, 'Content-Type': 'application/json', 'User-Agent': UA }; } // 当前积分总量 = total_amount - consumed_amount function getUsage(token, cb) { post('/trae/api/v2/pay/web_user_ent_usage', apiHeaders(token, randomDevice()), '{"require_usage":true}', function (status, d) { if (status !== 200 || !d || !d.usage_summary) return cb(null); const total = Number(d.usage_summary.total_amount); const used = Number(d.usage_summary.consumed_amount); if (!isFinite(total)) return cb(null); cb(Math.max(0, total - (isFinite(used) ? used : 0))); }); } function bumpDays() { const last = GM_getValue('trae_last_date', ''); const days = Number(GM_getValue('trae_days', 0)) || 0; let next; if (last === today()) next = days || 1; else if (last === yesterday()) next = days + 1; else next = 1; GM_setValue('trae_last_date', today()); GM_setValue('trae_days', next); return next; } function claim(token, device, reward, attempt) { post('/trae/api/v2/ug/checkin_credits/claim', apiHeaders(token, device), '{}', function (status, d) { if (status === 200 && (Number(d.code) === 0 || Number(d.code) === 200)) { GM_setValue('trae_checkin', today()); const days = bumpDays(); return getUsage(token, function (total) { if (total !== null) GM_setValue('trae_credits', total); let text = '获得 ' + reward + ' 积分'; if (total !== null) text += ' · 当前积分 ' + total; text += ' · 连续签到 ' + days + ' 天'; notify('签到成功', text); resolve('签到成功'); }); } if (Number(d.code) === 9074 || Number(d.code) === 9095) { if (attempt < MAX_DEVICE_RETRY) { GM_log('[Trae] 设备风控,换设备重试', 'warn'); return claim(token, randomDevice(), reward, attempt + 1); } } if (authError(d.code, d.message)) return loginExpired(); notify('签到失败', d.message || ('HTTP ' + status)); resolve('签到失败'); }); } // 当天已签到后的重复点击:1-3 次显示积分,4-6 次名言,7 次起彩蛋 function repeatFlow() { const now = Date.now(); const lastDate = GM_getValue('trae_run_date', ''); const last = Number(GM_getValue('trae_run_at', 0)) || 0; const streak = (lastDate === today() && now - last < 5 * 60 * 1000) ? (Number(GM_getValue('trae_streak', 0)) || 0) + 1 : 1; GM_setValue('trae_run_date', today()); GM_setValue('trae_run_at', now); GM_setValue('trae_streak', streak); // 自动运行(整点)一天只提示一次,避免每小时弹窗 const auto = new Date().getMinutes() <= 1; if (auto && GM_getValue('trae_auto_notified', '') === today()) return; if (auto) GM_setValue('trae_auto_notified', today()); if (streak <= 3) { const credits = GM_getValue('trae_credits', ''); notify('今日已签到', credits === '' ? '今日已签到,请明日再来~' : '今日已签到 · 当前积分 ' + credits); return; } if (streak <= 6) { notifyCopy('今日已签到', nextQuote()); return; } notifyCopy('隐藏成就', EGGS[Math.floor(Math.random() * EGGS.length)]); } function start() { if (GM_getValue('trae_checkin', '') === today()) { GM_log('[Trae] 今日已签到,跳过'); repeatFlow(); return resolve('今日已签到'); } readSession(function (session) { if (!session) { notify('未登录', '未找到 trae.cn 登录态,请先登录', true); return resolve('未登录'); } getToken(session, function (token) { if (!token) return loginExpired(); const device = randomDevice(); post('/trae/api/v2/ug/checkin_credits/status', apiHeaders(token, device), '{}', function (status, d) { if (status === 200 && d.checked_in) { GM_setValue('trae_checkin', today()); GM_log('[Trae] 今日已签到'); repeatFlow(); return resolve('今日已签到'); } if (status === 200 && d.enable === false) { notify('签到未开启', d.message || '当前活动未开启'); return resolve('未开启'); } if (authError(d.code, d.message)) return loginExpired(); const reward = (Number(d.credits) || 0) + (Number(d.extra_credits) || 0); claim(token, device, reward, 0); }); }); }); } start(); });