From 5dc7ec84489562ac0e2e76062fbb6626a49a20bf Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 20 Jun 2026 14:35:27 +0800 Subject: [PATCH] fix(qdreader): sync full Autman sign plugin --- autman/qdreader_sign_autman.js | 1366 +++++++++++++++++++++++++++++--- 1 file changed, 1250 insertions(+), 116 deletions(-) diff --git a/autman/qdreader_sign_autman.js b/autman/qdreader_sign_autman.js index cd693d8..64b08da 100644 --- a/autman/qdreader_sign_autman.js +++ b/autman/qdreader_sign_autman.js @@ -1,105 +1,348 @@ //[open_source: true] //[create_at: 2026-05-17 03:00:00] -//[title: 起点读书签到] +//[title: 启点读书签到] //[author: Hermes] //[service: BOSS] //[class: 工具类] -//[version: 2.0.0] -//[platform: web,qq,wx,tg,tb] -//[public: false] -//[price: 0] -//[disable: false] -//[description: 起点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,并在机器人内执行签到/查询/导出/删除。签名沿用上游 QDReader 的 api.120399.xyz 签名网关。] -//[rule: ^起点(ck|CK|cookie|Cookie|签到|读书)(.*)$] -//[rule: ^qdreader(.*)$] -//[param: {"required":false,"key":"otto.qdreader_cookie_json","bool":false,"placeholder":"{\"uid\":\"cookie\"}","name":"QDReader Cookie JSON","desc":"机器人交互维护,JSON Map 格式"}] -//[param: {"required":false,"key":"otto.qdreader_sign_api","bool":false,"placeholder":"https://api.120399.xyz","name":"QDReader 签名网关","desc":"默认 https://api.120399.xyz"}] +// [version: 2.15.3] +// [platform: web,qq,wx,tg,tb,fs,we] +// [public: false] +// [price: 0] +// [disable: false] +// [description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”;定时使用 `1 7 * * *` 每日 07:01 签到;完全体可选任务按上游原项目解密流程执行;每次签到运行结束后立即归档并清空最近运行日志,不再依赖单独归档定时;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。] +// [rule: ^启点(账号|查询|ck|CK|cookie|Cookie|签到|读书|归档|日志归档)(.*)$] +// [rule: ^qdreader(.*)$] +// [admin: true] +// [priority: 600] +// [cron: 1 7 * * *] + +var BUCKET = "hermes_qidian" var STORE_KEY = "qdreader_cookie_json" +var BIND_KEY = "qdreader_user_bind_json" +var REMARK_KEY = "qdreader_remark_json" var SIGN_API_KEY = "qdreader_sign_api" -var DEFAULT_SIGN_API = "https://api.120399.xyz" +var RETRY_KEY = "qdreader_retry_count" +var CRON_SILENT_KEY = "qdreader_cron_silent" +var TASK_PREF_KEY = "qdreader_task_pref_json" +var ADV_JOB_ENABLE_KEY = "qdreader_adv_job_enable" +var EXTRA_ADV_JOB_ENABLE_KEY = "qdreader_extra_adv_job_enable" +var LOTTERY_ENABLE_KEY = "qdreader_lottery_enable" +var WEEKLY_EXCHANGE_ENABLE_KEY = "qdreader_weekly_exchange_enable" +var CHAPTER_CARD_ENABLE_KEY = "qdreader_chapter_card_enable" +var MESSAGE_BOX_ENABLE_KEY = "qdreader_message_box_enable" +var LAST_RESULT_KEY = "qdreader_last_result_json" +var LAST_RUN_KEY = "qdreader_last_run_json" +var ARCHIVE_KEY = "qdreader_archive_json" +var DEFAULT_SIGN_API = "https://api.120399.xyz/qdreader" var QIDIAN_BASE = "https://h5.if.qidian.com" +var CHECKIN_PATH = "/argus/api/v3/checkin/checkin" +var QD_TASKS = [ + { key: "reward", label: "领取奖励", handler: "receiveGifts", defaultEnabled: true, builtin: true }, + { key: "adv", label: "激励任务", handler: "adv", settingKey: ADV_JOB_ENABLE_KEY }, + { key: "extraAdv", label: "每日任务", handler: "daily", settingKey: EXTRA_ADV_JOB_ENABLE_KEY }, + { key: "lottery", label: "每日抽奖", handler: "lottery", settingKey: LOTTERY_ENABLE_KEY }, + { key: "weeklyExchange", label: "每周兑换", handler: "exchange", settingKey: WEEKLY_EXCHANGE_ENABLE_KEY, sundayOnly: true }, + { key: "chapterCard", label: "章节卡片", handler: "chapterCard", settingKey: CHAPTER_CARD_ENABLE_KEY }, + { key: "messageBox", label: "大咖荐书", handler: "messageBox", settingKey: MESSAGE_BOX_ENABLE_KEY } +] + +function defined(name) { try { return typeof this[name] !== "undefined" } catch (e) { return false } } +function globalValue(name) { try { return this[name] } catch (e) { return undefined } } +function safeJsonParse(s, def) { try { return JSON.parse(s) } catch (e) { return def } } +function isObject(x) { return !!x && typeof x === "object" && !Array.isArray(x) } +function trim(s) { return String(s === undefined || s === null ? "" : s).replace(/^\s+|\s+$/g, "") } +function lower(s) { return String(s || "").toLowerCase() } + +function cfgGet(key, def) { + try { if (typeof bucketGet === "function") { var bv = bucketGet(BUCKET, key); if (bv !== undefined && bv !== null && String(bv) !== "") return bv } } catch (e) {} + try { if (typeof get === "function") { var v = get(key); if (v !== undefined && v !== null && String(v) !== "") return v } } catch (e) {} + return def +} +function cfgSet(key, value) { + var s = String(value) + var ok = false + try { if (typeof bucketSet === "function") { bucketSet(BUCKET, key, s); ok = true } } catch (e) {} + try { if (typeof set === "function") { set(key, s); ok = true } } catch (e) {} + if (!ok) throw new Error("当前 Autman 环境没有 set/bucketSet,无法保存配置") + return s +} +function getBool(key, def) { + var v = cfgGet(key, def ? "true" : "false") + if (typeof v === "boolean") return v + v = lower(trim(v)) + if (v === "1" || v === "true" || v === "yes" || v === "on" || v === "启用" || v === "是") return true + if (v === "0" || v === "false" || v === "no" || v === "off" || v === "禁用" || v === "否") return false + return !!def +} +function getInt(key, def, min, max) { + var n = parseInt(cfgGet(key, String(def)), 10) + if (isNaN(n)) n = def + if (min !== undefined && n < min) n = min + if (max !== undefined && n > max) n = max + return n +} function reply(msg) { - if (typeof sender !== "undefined" && sender.reply) sender.reply(msg) - else if (typeof sendText === "function") sendText(typeof msg === "string" ? msg : JSON.stringify(msg)) - else console.log(typeof msg === "string" ? msg : JSON.stringify(msg)) + var out = typeof msg === "string" ? msg : JSON.stringify(msg) + if (!out) return + try { if (typeof sender !== "undefined" && sender && sender.reply) { sender.reply(out); return } } catch (e) {} + try { if (typeof sendText === "function") { sendText(out); return } } catch (e) {} + try { if (typeof push === "function") { push({ content: out }); return } } catch (e) {} + try { console.log(out) } catch (e) {} } -function safeJsonParse(s, def) { try { return JSON.parse(s) } catch (e) { return def } } -function getStore() { - var raw = get(STORE_KEY) || "{}" - var obj = safeJsonParse(raw, {}) - if (!obj || typeof obj !== "object" || Array.isArray(obj)) return {} - return obj +function pushToUser(userId, content) { + userId = trim(userId || "") + content = typeof content === "string" ? content : JSON.stringify(content) + if (!userId || !content) return false + if (typeof push !== "function") return false + var payloads = [ + { imType: "wx", userID: userId, content: content }, + { imType: "we", userID: userId, content: content }, + { imType: "web", userID: userId, content: content }, + { userID: userId, content: content } + ] + for (var i = 0; i < payloads.length; i++) { + try { push(payloads[i]); return true } catch (e) {} + } + return false } -function setStore(obj) { var s = JSON.stringify(obj); set(STORE_KEY, s); return s } +function debug(msg) { try { if (typeof Debug === "function") Debug(String(msg)); else console.log(String(msg)) } catch (e) {} } + +function cleanStringMap(obj) { + if (!isObject(obj)) return {} + var clean = {} + for (var k in obj) if (obj.hasOwnProperty(k) && obj[k] !== undefined && obj[k] !== null && String(obj[k]) !== "") clean[String(k)] = String(obj[k]) + return clean +} +function getStore() { return cleanStringMap(safeJsonParse(cfgGet(STORE_KEY, "{}"), {})) } +function setStore(obj) { return cfgSet(STORE_KEY, JSON.stringify(obj || {})) } +function getBindStore() { return cleanStringMap(safeJsonParse(cfgGet(BIND_KEY, "{}"), {})) } +function setBindStore(obj) { return cfgSet(BIND_KEY, JSON.stringify(obj || {})) } +function getRemarkStore() { return cleanStringMap(safeJsonParse(cfgGet(REMARK_KEY, "{}"), {})) } +function setRemarkStore(obj) { return cfgSet(REMARK_KEY, JSON.stringify(obj || {})) } +function cookieGuid(cookie) { + var jar = parseCookieString(cookie) + return trim(jar.ywguid || jar.gwguid || jar.guid || "") +} +function remarksForStore(store) { + var remarks = getRemarkStore() + var changed = false + for (var uid in (store || {})) if (store.hasOwnProperty(uid)) { + var guid = cookieGuid(store[uid]) + if (guid && guid !== uid && remarks[guid] && !remarks[uid]) { + remarks[uid] = remarks[guid] + delete remarks[guid] + changed = true + } + } + if (changed) setRemarkStore(remarks) + return remarks +} +function callNoArg(obj, name) { + try { if (obj && typeof obj[name] === "function") return obj[name]() } catch (e) {} + return "" +} +function currentUserId() { + var v = "" + try { if (typeof GetUserID === "function") v = GetUserID() || "" } catch (e) {} + try { if (!v && typeof Sender !== "undefined" && Sender && typeof Sender.GetUserID === "function") v = Sender.GetUserID() || "" } catch (e) {} + var names = ["getUserID", "getUserId", "getUserid", "getUser", "getSenderID", "getSenderId", "getSender", "getFromUserID", "getFromUserId"] + try { if (!v && typeof sender !== "undefined" && sender) for (var i = 0; i < names.length && !v; i++) v = callNoArg(sender, names[i]) || "" } catch (e) {} + var globals = ["userId", "userID", "senderID", "senderId", "fromUserId", "fromUserID"] + for (var j = 0; j < globals.length && !v; j++) { try { if (typeof this[globals[j]] !== "undefined") v = this[globals[j]] || "" } catch (e) {} } + for (var k = 0; k < names.length && !v; k++) { try { if (typeof this[names[k]] === "function") v = this[names[k]]() || "" } catch (e) {} } + return trim(v) +} +function currentOwnerId() { + return currentUserId() +} +function currentIsAdmin() { + try { if (typeof isAdmin === "function" && isAdmin()) return true } catch (e) {} + try { if (typeof sender !== "undefined" && sender && typeof sender.isAdmin === "function" && sender.isAdmin()) return true } catch (e2) {} + try { if (typeof Sender !== "undefined" && Sender && typeof Sender.isAdmin === "function" && Sender.isAdmin()) return true } catch (e3) {} + return false +} +function looksLikeLegacyFallbackOwner(owner) { + owner = trim(owner) + if (!owner) return false + if (owner === "__qdreader_default_owner__") return true + if (/^[A-Za-z0-9._-]{1,32}$/.test(owner) && !/^\d{5,}$/.test(owner)) return true + return false +} +function canRepairBinding(oldOwner, me) { + if (!oldOwner || oldOwner === me) return true + return currentIsAdmin() && looksLikeLegacyFallbackOwner(oldOwner) +} +function currentUserDebug() { + var parts = [] + var fns = ["GetUserID", "GetChatID", "GetImType", "ImType", "GetUsername"] + for (var f = 0; f < fns.length; f++) try { if (typeof this[fns[f]] === "function") { var gv = this[fns[f]](); parts.push(fns[f] + "=" + (gv ? maskUid(String(gv)) : "空")) } } catch (e) {} + try { if (typeof Sender !== "undefined" && Sender) { + var snames = ["GetUserID", "GetChatID", "GetImType", "GetUsername"] + for (var s = 0; s < snames.length; s++) if (typeof Sender[snames[s]] === "function") { + var sv = Sender[snames[s]](); parts.push("Sender." + snames[s] + "=" + (sv ? maskUid(String(sv)) : "空")) + } + } } catch (e) {} + try { if (typeof sender !== "undefined" && sender) { + var names = ["getUserID", "getUserId", "getUserid", "getUser", "getSenderID", "getSenderId", "getSender", "getFromUserID", "getFromUserId", "getChatID", "getChatId", "getImtype"] + for (var i = 0; i < names.length; i++) if (typeof sender[names[i]] === "function") { + var v = callNoArg(sender, names[i]); parts.push("sender." + names[i] + "=" + (v ? maskUid(String(v)) : "空")) + } + } } catch (e) {} + return parts.length ? parts.join("; ") : "未发现可用用户方法" +} +function boundToCurrent(uid, bind) { + var owner = bind && bind[String(uid)] ? String(bind[String(uid)]) : "" + var me = currentOwnerId() + return !!owner && !!me && owner === me +} +function visibleUids(store, bind, allUsers) { + var uids = Object.keys(store || {}).sort() + if (allUsers) return uids + return uids.filter(function (uid) { return boundToCurrent(uid, bind || {}) }) +} + function parseCookieString(cookie) { var jar = {} String(cookie || "").split(";").forEach(function (part) { var i = part.indexOf("=") if (i <= 0) return - var k = part.slice(0, i).trim() - var v = part.slice(i + 1).trim() + var k = trim(part.slice(0, i)) + var v = trim(part.slice(i + 1)) if (k) jar[k] = v }) return jar } function cookieToString(jar) { var arr = [] - for (var k in jar) if (jar[k] !== undefined && jar[k] !== null && jar[k] !== "") arr.push(k + "=" + jar[k]) + for (var k in jar) if (jar.hasOwnProperty(k) && jar[k] !== undefined && jar[k] !== null && jar[k] !== "") arr.push(k + "=" + jar[k]) return arr.join("; ") } function b64decode(s) { - s = String(s || "") + s = String(s || "").trim() + try { s = decodeURIComponent(s) } catch (e) {} + s = s.replace(/-/g, "+").replace(/_/g, "/") + while (s.length % 4) s += "=" try { if (typeof Buffer !== "undefined") return Buffer.from(s, "base64").toString("utf8") } catch (e) {} - try { return decodeURIComponent(escape(atob(s))) } catch (e) { return "" } + try { if (typeof atob === "function") return decodeURIComponent(escape(atob(s))) } catch (e) {} + try { + var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" + var output = "", buffer = 0, bits = 0 + for (var i = 0; i < s.length; i++) { + var c = chars.indexOf(s.charAt(i)) + if (c < 0) continue + if (c === 64) break + buffer = (buffer << 6) | c + bits += 6 + if (bits >= 8) { + bits -= 8 + output += String.fromCharCode((buffer >> bits) & 0xff) + } + } + try { return decodeURIComponent(escape(output)) } catch (e2) { return output } + } catch (e3) { return "" } +} +function isRealUid(uid) { + uid = String(uid === undefined || uid === null ? "" : uid).trim() + return /^\d{5,}$/.test(uid) ? uid : "" +} +function uidFromQDHeader(qdheader) { + var decoded = b64decode(qdheader) + if (!decoded) return "" + var parts = decoded.split("|").map(function (x) { return trim(x || "") }) + for (var i = 0; i < [0, 11].length; i++) { + var uid = isRealUid(parts[[0, 11][i]]) + if (uid) return uid + } + return "" } -function uidFromQDHeader(qdheader) { var decoded = b64decode(qdheader); return decoded ? String(decoded.split("|")[0] || "").trim() : "" } function extractUid(cookie) { var jar = parseCookieString(cookie) var uid = uidFromQDHeader(jar.QDHeader || jar.qdheader || jar.QDHEADER || "") if (uid) return uid - if (jar.uid) return String(jar.uid) - return "" + uid = isRealUid(jar.uid || jar.userid || jar.userId || jar.UserId) + if (uid) return uid + return isRealUid(jar.ywguid || jar.gwguid || jar.guid) +} +function maskCookie(cookie) { + cookie = String(cookie || "") + if (!cookie) return "" + var jar = parseCookieString(cookie) + var keys = [] + for (var k in jar) if (jar.hasOwnProperty(k)) keys.push(k) + var prefix = keys.slice(0, 5).join(",") + return "[cookie keys:" + prefix + (keys.length > 5 ? ",..." : "") + "; len=" + cookie.length + "]" +} +function maskUid(uid) { + uid = String(uid || "") + if (uid.length <= 4) return uid + return uid.slice(0, 2) + "***" + uid.slice(-2) +} + +function runtimeImType() { + var im = "" + try { if (typeof ImType === "function") im = ImType() || "" } catch (e) {} + try { if (!im && typeof GetImType === "function") im = GetImType() || "" } catch (e2) {} + return lower(trim(im)) +} +function isSchedulerImType(im) { + im = lower(trim(im)) + return im === "cron" || im === "task" || im === "fake" } -function maskCookie(cookie) { cookie = String(cookie || ""); return cookie.length <= 24 ? cookie : cookie.slice(0, 12) + "..." + cookie.slice(-8) } function contentText() { var s = "" - try { if (typeof getContent === "function") s = getContent() || "" } catch (e) {} - try { if (!s && typeof sender !== "undefined" && sender.getContent) s = sender.getContent() || "" } catch (e) {} - try { if (!s && typeof message !== "undefined") s = String(message) } catch (e) {} - return String(s || "").trim() + if (isSchedulerImType(runtimeImType())) return "" + try { if (typeof getContent === "function") s = getContent() || "" } catch (e3) {} + try { if (!s && typeof GetContent === "function") s = GetContent() || "" } catch (e4) {} + try { if (!s && typeof sender !== "undefined" && sender && sender.getContent) s = sender.getContent() || "" } catch (e5) {} + try { if (!s && typeof sender !== "undefined" && sender && sender.getMessage) s = sender.getMessage() || "" } catch (e6) {} + try { if (!s && typeof message !== "undefined") s = String(message) } catch (e7) {} + return trim(s) } function usage() { return [ - "起点读书签到插件", - "提交CK:起点ck ", - "签到全部:起点签到", - "签到单号:起点签到 ", - "查询CK:起点ck 查询", - "导出CK:起点ck 导出", - "删除CK:起点ck 删除 ", - "说明:QX只负责抓cookie;签到由本 Autman 插件执行。" + "启点读书签到插件 v2.15.3", + "【一级菜单】", + "账号管理:启点账号", + "执行查询:启点查询", + "【快速通道】", + "快速提交CK:启点ck ", + "直接签到:启点签到", + "指定签到:启点签到 ", + "账号任务:启点账号 → 账号详情/任务配置", + "【配置】", + "签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API, + "完全体任务:领取奖励固定执行;激励/每日/抽奖/兑换/章节卡/大咖荐书按账号单独管理,默认关闭。", + "定时:每日 07:01 自动执行签到链路;QX 只负责抓 cookie。" ].join("\n") } function stripCommand(text) { - return String(text || "") - .replace(/^起点(?:ck|CK|cookie|Cookie|签到|读书)\s*/i, "") - .replace(/^qdreader\s*/i, "") - .trim() + var s = String(text || "") + if (/^启点(?:签到|读书)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:签到|读书)\s*/i, "")) + if (/^启点(?:归档|日志归档)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:归档|日志归档)\s*/i, "")) + if (/^启点(?:查询)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:查询)\s*/i, "")) + if (/^启点(?:账号)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:账号)\s*/i, "")) + if (/^启点(?:ck|CK|cookie|Cookie)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:ck|CK|cookie|Cookie)\s*/i, "")) + return trim(s.replace(/^qdreader\s*/i, "")) } function commandName(text) { - if (/^起点(?:签到|读书)(?:\s|$)/i.test(text)) return "sign" + if (/^启点(?:签到|读书)(?:\s|$)/i.test(text)) return "sign" + if (/^启点(?:归档|日志归档)(?:\s|$)/i.test(text)) return "archive" if (/^qdreader\s+sign(?:\s|$)/i.test(text)) return "sign" + if (/^qdreader\s+archive(?:\s|$)/i.test(text)) return "archive" + if (/^启点(?:查询)(?:\s|$)/i.test(text)) return "status" + if (/^启点(?:账号)(?:\s|$)/i.test(text)) return "account" return "ck" } function qs(obj) { var parts = [] for (var k in (obj || {})) { + if (!obj.hasOwnProperty(k)) continue var v = obj[k] if (v === undefined || v === null) continue if (Array.isArray(v)) { for (var i = 0; i < v.length; i++) parts.push(encodeURIComponent(k + "[" + i + "]") + "=" + encodeURIComponent(String(v[i]))) } - else if (typeof v === "object") { for (var sk in v) parts.push(encodeURIComponent(k + "[" + sk + "]") + "=" + encodeURIComponent(String(v[sk]))) } + else if (typeof v === "object") { for (var sk in v) if (v.hasOwnProperty(sk)) parts.push(encodeURIComponent(k + "[" + sk + "]") + "=" + encodeURIComponent(String(v[sk]))) } else parts.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(v))) } return parts.join("&") @@ -110,33 +353,71 @@ function normalizeCookie(cookie) { if (session) jar.sessionyw = session var appid = jar.appId || jar.appid || "" if (appid) jar.appId = appid - return { jar: jar, cookie: cookieToString(jar), qdh: jar.QDHeader || jar.qdh || "", qdheader: jar.QDHeader || jar.qdheader || "", uid: extractUid(cookie), guid: jar.gwguid || jar.guid || "", qimei: jar.qimei || "", qimei36: jar.qimei36 || jar.QIMEI36 || "" } + var guid = jar.ywguid || jar.gwguid || jar.guid || "" + var qdh = jar.qdh || jar.QDH || "" + var qdheader = jar.QDHeader || jar.qdheader || jar.QDHEADER || "" + return { jar: jar, cookie: cookieToString(jar), qdh: qdh, qdheader: qdheader, uid: extractUid(cookie), guid: guid, qimei: jar.qimei || jar.QIMEI || "", qimei36: jar.qimei36 || jar.QIMEI36 || "" } } function httpReq(opt) { + if (!opt.headers) opt.headers = {} if (typeof request === "function") return request(opt) if (typeof $request === "function") return $request(opt) throw new Error("当前 Autman 环境没有 request/$request 方法,无法发起签到请求") } function parseResp(res) { - var body = res && (res.body !== undefined ? res.body : (res.data !== undefined ? res.data : res)) + if (res === undefined || res === null) return null + var body = null + if (res && typeof res === "object") { + if (res.body !== undefined) body = res.body + else if (res.data !== undefined) body = res.data + else if (res.content !== undefined) body = res.content + else if (res.responseText !== undefined) body = res.responseText + else if (res.statusCode !== undefined || res.status !== undefined || res.headers !== undefined) body = "" + else body = res + } else { + body = res + } if (typeof body === "string") return safeJsonParse(body, body) return body } +function requestWithRetry(opt, retryCount) { + var lastErr = null + var attempts = Math.max(1, (retryCount || 0) + 1) + for (var i = 0; i < attempts; i++) { + try { + return httpReq(opt) + } catch (e) { + lastErr = e + debug("QDReader request retry " + (i + 1) + "/" + attempts + " failed: " + (e && e.message ? e.message : String(e))) + } + } + throw lastErr || new Error("请求失败") +} function postJson(url, data) { - return parseResp(httpReq({ url: url, method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) })) + var retry = getInt(RETRY_KEY, 1, 0, 3) + return parseResp(requestWithRetry({ url: url, method: "post", dataType: "json", timeOut: 10000, headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify(data) }, retry)) +} +function normalizeSignHeaders(signed) { + if (typeof signed === "string") signed = safeJsonParse(signed, {}) + if (!signed) throw new Error("签名网关无返回") + var code = signed.code !== undefined ? signed.code : (signed.retcode !== undefined ? signed.retcode : signed.status) + var ok = code === undefined || code === 0 || code === "0" || code === 200 || code === "200" || code === "ok" || code === true + var data = signed.data || signed.result || signed.headers || signed + var headers = data && (data.headers || data.header || data) + if (!ok || !isObject(headers)) throw new Error("签名失败:" + JSON.stringify(signed).slice(0, 500)) + return headers } function qidianRequest(path, method, data, cookie) { method = String(method || "GET").toUpperCase() var info = normalizeCookie(cookie) - var signApi = (get(SIGN_API_KEY) || DEFAULT_SIGN_API).replace(/\/$/, "") - var payload = { qdh: info.qdh, qdheader: info.qdheader, url: QIDIAN_BASE + path, method: method, data: data || {}, uid: info.uid, guid: info.guid, qimei: info.qimei, qimei36: info.qimei36 } - var signed = postJson(signApi + "/sign", payload) - if (typeof signed === "string") signed = safeJsonParse(signed, {}) - if (!signed || (signed.code !== 200 && signed.code !== "200") || !signed.data) throw new Error("签名失败:" + JSON.stringify(signed)) - var headers = signed.data + if (!info.uid) throw new Error("Cookie 缺少 uid/QDHeader") + var signApi = String(cfgGet(SIGN_API_KEY, DEFAULT_SIGN_API)).replace(/\/+$/, "") + var targetUrl = /^https?:\/\//i.test(path) ? path : QIDIAN_BASE + path + var payload = { qdh: info.qdh, qdheader: info.qdheader, url: targetUrl, method: lower(method), data: data || {}, uid: info.uid, guid: info.guid, qimei: info.qimei, qimei36: info.qimei36 } + var headers = normalizeSignHeaders(postJson(signApi + "/sign", payload)) headers.Cookie = info.cookie if (!headers["User-Agent"]) headers["User-Agent"] = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/QDReaderiOS/5.9.456/728/QDReaderAppstore" - var req = { url: QIDIAN_BASE + path, method: method, headers: headers } + var req = { url: targetUrl, method: lower(method), dataType: "json", timeOut: 10000, headers: headers } if (method === "GET") { var query = qs(data || {}) if (query) req.url += (req.url.indexOf("?") >= 0 ? "&" : "?") + query @@ -144,75 +425,928 @@ function qidianRequest(path, method, data, cookie) { req.headers["Content-Type"] = req.headers["Content-Type"] || "application/x-www-form-urlencoded" req.body = qs(data || {}) } - var out = parseResp(httpReq(req)) + var retry = getInt(RETRY_KEY, 1, 0, 3) + var out = parseResp(requestWithRetry(req, retry)) if (typeof out === "string") out = safeJsonParse(out, out) return out } +function resultCode(obj) { + if (!obj) return undefined + var keys = ["Result", "result", "Code", "code", "retcode", "RetCode"] + for (var i = 0; i < keys.length; i++) if (obj[keys[i]] !== undefined) return obj[keys[i]] + var data = obj.Data || obj.data || obj.ResultData || obj.resultData + if (data && typeof data === "object") for (var j = 0; j < keys.length; j++) if (data[keys[j]] !== undefined) return data[keys[j]] + return undefined +} function resultOk(obj) { - var code = obj && (obj.Result !== undefined ? obj.Result : obj.Code !== undefined ? obj.Code : obj.code) - return code === undefined || code === 0 || code === "0" || code === 200 || code === "200" + var code = resultCode(obj) + var status = obj && (obj.status || obj.Status) + return code === undefined || code === 0 || code === "0" || code === 200 || code === "200" || status === "ok" || status === "success" } -function resultMsg(obj) { return (obj && (obj.Message || obj.Msg || obj.message || obj.msg)) || "" } -function doCheckin(uid, cookie) { - var res = qidianRequest("/argus/api/v3/checkin/checkin", "POST", {}, cookie) - var ok = resultOk(res) - var msg = resultMsg(res) || (ok ? "签到请求成功" : "签到失败") - return { uid: uid, ok: ok, msg: msg, raw: res } +function resultMsg(obj) { + if (!obj) return "" + return obj.Message || obj.Msg || obj.message || obj.msg || obj.toast || (obj.data && (obj.data.message || obj.data.msg || obj.data.toast)) || (obj.Data && (obj.Data.message || obj.Data.msg || obj.Data.toast)) || "" } -function signAll(targetUid) { - var store = getStore() - var uids = Object.keys(store) - if (targetUid) uids = uids.filter(function (u) { return u === targetUid }) - if (!uids.length) return targetUid ? ("未找到 uid: " + targetUid) : "还没有保存起点读书CK" - var lines = ["起点读书签到结果:"] - for (var i = 0; i < uids.length; i++) { - var uid = uids[i] - try { - var r = doCheckin(uid, store[uid]) - lines.push((r.ok ? "✅ " : "❌ ") + uid + ":" + r.msg) - } catch (e) { - lines.push("❌ " + uid + ":" + (e && e.message ? e.message : String(e))) +function successLikeMsg(msg) { + msg = String(msg || "") + return /今日已签到|已经签到|已签到|重复签到|已领取|已经领取|已完成|完成|无可领取|无礼包|暂无可领取/.test(msg) +} +function taskResultOk(obj) { + return resultOk(obj) || successLikeMsg(resultMsg(obj)) +} +function isWhitelistBlockedError(msg) { + msg = String(msg || "") + return /url\s*不在允许的接口列表内|不在允许的接口列表|参数异常.*允许的接口/.test(msg) +} +function pad2(n) { n = String(n); return n.length < 2 ? "0" + n : n } +function beijingDate(dateLike) { + var d = dateLike ? new Date(dateLike) : new Date() + if (!d || isNaN(d.getTime())) d = new Date() + return new Date(d.getTime() + 8 * 60 * 60 * 1000) +} +function beijingTime(dateLike) { + var d = beijingDate(dateLike) + return d.getUTCFullYear() + "-" + pad2(d.getUTCMonth() + 1) + "-" + pad2(d.getUTCDate()) + " " + pad2(d.getUTCHours()) + ":" + pad2(d.getUTCMinutes()) + ":" + pad2(d.getUTCSeconds()) +} +function nowIso() { return beijingTime() } +function setLastResult(uid, result) { + var obj = cleanStringMap(safeJsonParse(cfgGet(LAST_RESULT_KEY, "{}"), {})) + obj[String(uid)] = JSON.stringify(result || {}) + cfgSet(LAST_RESULT_KEY, JSON.stringify(obj)) +} +function getLastResults() { + var raw = cleanStringMap(safeJsonParse(cfgGet(LAST_RESULT_KEY, "{}"), {})) + var out = {} + for (var uid in raw) if (raw.hasOwnProperty(uid)) out[uid] = safeJsonParse(raw[uid], { msg: raw[uid] }) + return out +} +function setLastRunLog(log) { cfgSet(LAST_RUN_KEY, JSON.stringify(log || {})) } +function getLastRunLog() { return safeJsonParse(cfgGet(LAST_RUN_KEY, "{}"), {}) || {} } +function getArchiveStore() { return safeJsonParse(cfgGet(ARCHIVE_KEY, "{}"), {}) || {} } +function setArchiveStore(obj) { cfgSet(ARCHIVE_KEY, JSON.stringify(obj || {})) } +function localDateKey(iso) { + var s = String(iso || "") + var m0 = s.match(/^(\d{4}-\d{2}-\d{2})/) + if (m0) return m0[1] + var d = beijingDate(iso) + return d.getUTCFullYear() + "-" + pad2(d.getUTCMonth() + 1) + "-" + pad2(d.getUTCDate()) +} +function archiveRunLog(reason) { + var run = getLastRunLog() + var archive = getArchiveStore() + var archived = false + var key = "" + if (run && run.finishedAt && run.total !== undefined) { + key = localDateKey(run.finishedAt) + archive[key] = run + archive[key].archivedAt = nowIso() + archive[key].archiveReason = reason || "daily-rollover" + archived = true + } + setArchiveStore(archive) + cfgSet(LAST_RESULT_KEY, "{}") + cfgSet(LAST_RUN_KEY, "{}") + return { archived: archived, key: key, archive: archive, run: run || {} } +} +function latestArchiveForVisible(uids) { + var archive = getArchiveStore() + var keys = [] + for (var k in archive) if (archive.hasOwnProperty(k)) keys.push(k) + keys.sort() + for (var i = keys.length - 1; i >= 0; i--) { + var log = archive[keys[i]] || {} + var items = log.items || [] + var map = {} + for (var j = 0; j < items.length; j++) map[String(items[j].uid)] = items[j] + if (!uids || !uids.length) return { key: keys[i], log: log, itemMap: map } + for (var u = 0; u < uids.length; u++) if (map[String(uids[u])]) return { key: keys[i], log: log, itemMap: map } + } + return null +} +function cronMode() { + // Cron only runs the signing branch. Archive is performed immediately after each sign run. + return "sign" +} +function formatArchiveReply(ret) { + if (ret && ret.archived) { + var run = ret.run || {} + return "启点读书日志归档完成:" + ret.key + "\n" + + "来源:" + sourceName(run.source) + "\n" + + "成功 " + (run.okCount || 0) + " / 失败 " + (run.failCount || 0) + " / 总计 " + (run.total || 0) + "\n" + + "已清空当日运行日志,后续查询可从归档判断签到情况" + } + return "启点读书日志归档完成:无可归档批次;已清空当日运行日志" +} +function sourceName(source) { + if (source === "cron") return "定时任务/零点任务" + if (source === "menu") return "菜单立即执行" + if (source === "manual") return "手动签到" + return source || "未知" +} +function getTaskPrefs() { return safeJsonParse(cfgGet(TASK_PREF_KEY, "{}"), {}) || {} } +function setTaskPrefs(obj) { return cfgSet(TASK_PREF_KEY, JSON.stringify(obj || {})) } +function accountTaskPrefs(uid) { + var all = getTaskPrefs() + var prefs = all[String(uid)] + return isObject(prefs) ? prefs : {} +} +function taskEnabledForUid(task, uid) { + if (task.defaultEnabled || task.builtin) return true + if (task.sundayOnly) { var d = beijingDate(); if (d.getUTCDay() !== 0) return false } + var prefs = accountTaskPrefs(uid) + return prefs[task.key] === true || prefs[task.key] === "true" || prefs[task.key] === 1 || prefs[task.key] === "1" +} +function taskEnabled(task) { return taskEnabledForUid(task, "") } +function taskStatusLines(uid) { + var lines = ["启点账号任务开关:" + (uid ? maskUid(uid) : "未选择账号")] + var prefs = uid ? accountTaskPrefs(uid) : {} + for (var i = 0; i < QD_TASKS.length; i++) { + var t = QD_TASKS[i] + var enabled = t.builtin || t.defaultEnabled || prefs[t.key] === true || prefs[t.key] === "true" || prefs[t.key] === 1 || prefs[t.key] === "1" + var suffix = t.builtin ? "(固定)" : "" + if (t.sundayOnly) suffix += "(仅周日执行)" + lines.push((i + 1) + ". " + t.label + ":" + (enabled ? "开启" : "关闭") + suffix) + } + lines.push("回复:序号 on/off 切换,例如 2 on;all on 一键开启;all off 一键关闭;q 退出") + return lines.join("\n") +} +function taskMenuForUid(uid) { + if (!uid) return reply("未选择账号,已退出") + reply(taskStatusLines(uid)) + var op = listenInput(60000) + if (!op || /^(q|退出|取消)$/i.test(op)) return reply("已退出") + var all = getTaskPrefs() + var prefs = isObject(all[String(uid)]) ? all[String(uid)] : {} + var allMatch = op.match(/^(all|全部|所有|一键|全开|全关)\s*(on|off|开启|关闭|启用|禁用|1|0)?$/i) + if (allMatch) { + var enableAll = /^(全开)$/i.test(allMatch[1]) || /^(on|开启|启用|1)$/i.test(allMatch[2] || "") + if (/^(全关)$/i.test(allMatch[1])) enableAll = false + if (!allMatch[2] && /^(all|全部|所有|一键)$/i.test(allMatch[1])) return reply("请指定 all on 或 all off") + for (var ai = 0; ai < QD_TASKS.length; ai++) { + var at = QD_TASKS[ai] + if (at.builtin || !at.settingKey) continue + prefs[at.key] = !!enableAll + } + all[String(uid)] = prefs + setTaskPrefs(all) + return reply("已" + (enableAll ? "一键开启" : "一键关闭") + "账号 " + maskUid(uid) + " 的全部可选任务\n" + taskStatusLines(uid)) + } + var m = op.match(/^(\d+)\s*(on|off|开启|关闭|启用|禁用|1|0)?$/i) + if (!m) return reply("未识别选项,已退出") + var idx = parseInt(m[1], 10) - 1 + if (idx < 0 || idx >= QD_TASKS.length) return reply("编号无效,已退出") + var task = QD_TASKS[idx] + if (task.builtin || !task.settingKey) return reply(task.label + " 为固定任务,不能关闭") + var currently = prefs[task.key] === true || prefs[task.key] === "true" || prefs[task.key] === 1 || prefs[task.key] === "1" + var enable = m[2] ? /^(on|开启|启用|1)$/i.test(m[2]) : !currently + prefs[task.key] = !!enable + all[String(uid)] = prefs + setTaskPrefs(all) + return reply(task.label + "已" + (enable ? "开启" : "关闭") + "(账号 " + maskUid(uid) + ")\n" + taskStatusLines(uid)) +} +function qdGet(path, data, cookie) { return qidianRequest(path, "GET", data || {}, cookie) } +function qdPost(path, data, cookie) { return qidianRequest(path, "POST", data || {}, cookie) } +function firstArray() { + for (var i = 0; i < arguments.length; i++) if (Array.isArray(arguments[i])) return arguments[i] + return [] +} +function nested(obj, path) { + var cur = obj + for (var i = 0; i < path.length; i++) { + if (!cur || cur[path[i]] === undefined || cur[path[i]] === null) return undefined + cur = cur[path[i]] + } + return cur +} +function payloadData(obj) { return (obj && (obj.Data || obj.data)) || {} } +function looksLikeVideoTask(task) { return isObject(task) && !!taskIdOf(task) && (task["IsFinished"] !== undefined || task["IsReceived"] !== undefined || task["Title"] !== undefined || task["TaskName"] !== undefined) } +function taskArrayScore(list, kind) { + if (!Array.isArray(list) || !list.length) return 0 + var score = 0 + for (var i = 0; i < list.length; i++) { + var t = list[i] || {} + if (!looksLikeVideoTask(t)) continue + var title = taskTitle(t) + score += 2 + if (kind === "daily" && (title === "完成1个广告任务得奖励" || title === "完成3个广告任务得奖励" || t["IsReceived"] !== undefined)) score += 5 + if (kind === "adv" && (t["IsFinished"] !== undefined || /激励|碎片|福利|广告/.test(title))) score += 3 + } + return score +} +function findTaskListByShape(root, kind) { + var best = [] + var bestScore = 0 + var seen = [] + function walk(node, depth) { + if (!node || depth > 8) return + if (Array.isArray(node)) { + var score = taskArrayScore(node, kind) + if (score > bestScore) { bestScore = score; best = node } + for (var i = 0; i < node.length; i++) walk(node[i], depth + 1) + return + } + if (!isObject(node) || seen.indexOf(node) >= 0) return + seen.push(node) + for (var k in node) walk(node[k], depth + 1) + } + walk(root, 0) + return best +} +function advTaskList(main) { + return firstArray( + nested(main, ["Data", "DailyBenefitModule", "TaskList"]), + nested(main, ["data", "DailyBenefitModule", "TaskList"]), + nested(main, ["Data", "AdvTaskList"]), + nested(main, ["data", "AdvTaskList"]), + findTaskListByShape(payloadData(main), "adv") + ) +} +function dailyTaskList(main) { + return firstArray( + nested(main, ["Data", "VideoRewardTab", "TaskList"]), + nested(main, ["data", "VideoRewardTab", "TaskList"]), + nested(main, ["Data", "TaskList"]), + nested(main, ["data", "TaskList"]), + findTaskListByShape(payloadData(main), "daily") + ) +} +function surpriseBenefit(data) { + return data.SurpriseBenefit || data.SurpriseTask || data.SurpriseBenefitTask || null +} +function makeVideoPayload(taskId) { + return { taskId: String(taskId || ""), BanId: "0", BanMessage: "", CaptchaAId: "", CaptchaURL: "", CaptchaType: "0", Challenge: "", Gt: "", NewCaptcha: "0", Offline: "0", PhoneNumber: "", SessionKey: "" } +} +function field(obj, names, def) { + for (var i = 0; i < names.length; i++) if (obj && obj[names[i]] !== undefined && obj[names[i]] !== null) return obj[names[i]] + return def +} +function taskTitle(task) { return String(field(task, ["Title", "TaskName", "Name", "TaskTitle", "Desc", "Description"], "")) } +function taskIdOf(task) { return field(task, ["TaskId", "TaskID", "taskId", "TaskIDStr", "Id", "id"], "") } +function taskFinished(task) { return Number(field(task, ["IsFinished", "Finished", "Status", "status"], 0)) === 1 } +function taskIdKey(id) { return String(id === undefined || id === null ? "" : id) } +function taskRunCount(task, def) { + var n = Number(field(task, ["NeedCount", "needCount", "Count", "count", "TotalCount", "totalCount", "Times", "times", "Limit", "limit"], def || 1)) + if (!n || isNaN(n) || n < 1) n = def || 1 + if (n > 10) n = 10 + return n +} +function taskAccepted(res) { return taskResultOk(res) || Number(resultCode(res)) === 9 } +function riskConf(res) { + var data = res && (res.Data || res.data || res.ResultData || res.resultData) + return data && (data.RiskConf || data.riskConf || data.Risk || data.risk) +} +function finishVideoTask(taskOrId, cookie, repeat) { + var tid = typeof taskOrId === "object" ? taskIdOf(taskOrId || {}) : taskOrId + var n = repeat || (typeof taskOrId === "object" ? taskRunCount(taskOrId, 1) : 1) + var ok = 0 + var last = null + if (!tid) return { ok: 0, total: 0, last: null } + for (var i = 0; i < n; i++) { + last = qdPost("/argus/api/v1/video/adv/finishWatch", makeVideoPayload(tid), cookie) + if (taskAccepted(last)) ok++ + else break + } + return { ok: ok, total: n, last: last, taskId: tid } +} +function makeBase62Token(num) { + var alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + var n = BigInt(num) + var zero = BigInt(0) + var base = BigInt(62) + if (n === zero) return "0" + var out = "" + while (n > zero) { out = alphabet[Number(n % base)] + out; n = n / base } + return out +} +function doReceiveGifts(cookie) { + var res = qdPost("/argus/api/v3/checkin/receivegifts", { GameId: "0" }, cookie) + var ok = taskResultOk(res) + return { name: "领取奖励", ok: !!ok, msg: resultMsg(res) || (ok ? "领取成功" : "领取失败"), path: "/argus/api/v3/checkin/receivegifts" } +} +function doAdvTask(cookie) { + var main = qdGet("/argus/api/v2/video/adv/mainPage", {}, cookie) + if (!taskResultOk(main)) return { name: "激励任务", ok: false, msg: resultMsg(main) || "获取任务列表失败", path: "/argus/api/v2/video/adv/mainPage" } + var tasks = advTaskList(main) + if (!tasks.length) return { name: "激励任务", ok: false, msg: "任务列表为空", path: "/argus/api/v2/video/adv/mainPage" } + if (taskFinished(tasks[tasks.length - 1])) return { name: "激励任务", ok: true, msg: "任务已完成", path: "/argus/api/v2/video/adv/mainPage" } + var submittedCount = 0 + var riskSubmitted = false + for (var i = 0; i < tasks.length; i++) { + var t = tasks[i] || {} + if (taskFinished(t)) continue + var tid = taskIdOf(t) + if (!tid) continue + var fr = finishVideoTask(tid, cookie, 1) + submittedCount += fr.ok + if (riskConf(fr.last)) riskSubmitted = true + if (fr.ok < fr.total) return { name: "激励任务", ok: false, msg: resultMsg(fr.last) || ("第 " + (i + 1) + " 个任务失败"), path: "/argus/api/v1/video/adv/finishWatch" } + } + var verify = qdGet("/argus/api/v2/video/adv/mainPage", {}, cookie) + var after = advTaskList(verify) + var finished = !!(after && after.length && after.every(function (t) { return taskFinished(t) })) + if (finished) return { name: "激励任务", ok: true, msg: "任务已完成", path: "/argus/api/v2/video/adv/mainPage" } + if (submittedCount > 0 && riskSubmitted) return { name: "激励任务", ok: true, msg: "已提交,状态待刷新", path: "/argus/api/v2/video/adv/mainPage" } + return { name: "激励任务", ok: false, msg: submittedCount ? "部分任务未完成" : "未找到可提交任务", path: "/argus/api/v2/video/adv/mainPage" } +} +function taskReceived(task) { + return Number(field(task, ["IsReceived", "Received", "Status", "status"], 0)) === 1 +} +function doDailyTask(cookie) { + var main = qdGet("/argus/api/v2/video/adv/mainPage", {}, cookie) + if (!taskResultOk(main)) return { name: "每日任务", ok: false, msg: resultMsg(main) || "获取任务列表失败", path: "/argus/api/v2/video/adv/mainPage" } + var data = payloadData(main) + var done = 0 + var pre = surpriseBenefit(data) + if (pre && !taskFinished(pre) && taskIdOf(pre)) { + var pr = finishVideoTask(taskIdOf(pre), cookie, 1) + done += pr.ok + if (pr.ok < pr.total) return { name: "每日任务", ok: false, msg: resultMsg(pr.last) || "惊喜福利任务失败", path: "/argus/api/v1/video/adv/finishWatch" } + } + var tasks = dailyTaskList(main) + if (!tasks.length) return { name: "每日任务", ok: false, msg: "任务列表为空", path: "/argus/api/v2/video/adv/mainPage" } + var oneAd = null + var target = null + for (var i = 0; i < tasks.length; i++) { + var title = taskTitle(tasks[i]) + if (!oneAd && title === "完成1个广告任务得奖励") oneAd = tasks[i] + if (!target && title === "完成3个广告任务得奖励") target = tasks[i] + } + if (oneAd && !taskReceived(oneAd) && taskIdOf(oneAd)) { + var ar = finishVideoTask(taskIdOf(oneAd), cookie, 1) + done += ar.ok + if (ar.ok < ar.total) return { name: "每日任务", ok: false, msg: resultMsg(ar.last) || "完成1个广告任务失败", path: "/argus/api/v1/video/adv/finishWatch" } + } + if (!target) return { name: "每日任务", ok: false, msg: "未找到每日任务任务", path: "/argus/api/v2/video/adv/mainPage" } + if (taskReceived(target)) return { name: "每日任务", ok: true, msg: "任务已完成", path: "/argus/api/v2/video/adv/mainPage" } + var tid = taskIdOf(target) + if (!tid) return { name: "每日任务", ok: false, msg: "每日任务缺少 TaskId", path: "/argus/api/v2/video/adv/mainPage" } + var tr = finishVideoTask(tid, cookie, 3) + done += tr.ok + if (tr.ok < tr.total) return { name: "每日任务", ok: false, msg: resultMsg(tr.last) || "每日任务广告失败", path: "/argus/api/v1/video/adv/finishWatch" } + var verify = qdGet("/argus/api/v2/video/adv/mainPage", {}, cookie) + var after = dailyTaskList(verify) + var finished = false + for (var v = 0; v < after.length; v++) if (taskTitle(after[v]) === "完成3个广告任务得奖励" && taskReceived(after[v])) { finished = true; break } + return { name: "每日任务", ok: finished, msg: finished ? "任务已完成" : "任务未完成", path: "/argus/api/v2/video/adv/mainPage" } +} +function doLotteryTask(cookie) { + var detail = qdGet("/argus/api/v2/checkin/detail", {}, cookie) + if (!taskResultOk(detail)) return { name: "每日抽奖", ok: false, msg: resultMsg(detail) || "获取抽奖信息失败", path: "/argus/api/v2/checkin/detail" } + var data = detail.Data || detail.data || {} + var times = Number(data.HasVideoUrge || 0) + Number(data.LotteryCount || data.LotteryChance || data.ChanceCount || 0) + if (!times) return { name: "每日抽奖", ok: true, msg: "无可用抽奖次数", path: "/argus/api/v2/checkin/detail" } + times = Math.min(times, 10) + var okCount = 0 + for (var i = 0; i < times; i++) { var r = qdPost("/argus/api/v2/checkin/lottery", { GameId: "0", IsVideo: i < Number(data.HasVideoUrge || 0) ? "1" : "0" }, cookie); if (taskResultOk(r)) okCount++ } + return { name: "每日抽奖", ok: okCount > 0, msg: "成功 " + okCount + "/" + times + " 次抽奖", path: "/argus/api/v2/checkin/lottery" } +} +function doWeeklyExchange(cookie) { + var page = qdGet("/argus/api/v3/checkin/checkinexchangepage", {}, cookie) + if (!taskResultOk(page)) return { name: "每周兑换", ok: false, msg: resultMsg(page) || "获取兑换页失败", path: "/argus/api/v3/checkin/checkinexchangepage" } + var data = page.Data || page.data || {} + var balance = Number(data.Balance || data.Score || 0) + if (balance <= 0) return { name: "每周兑换", ok: false, msg: "余额不足(" + balance + ")", path: "/argus/api/v3/checkin/checkinexchangepage" } + var goods = firstArray(data.Goods, data.GoodsList, data.GiftList, data.List, data.Items) + if (!goods.length) return { name: "每周兑换", ok: false, msg: "章节卡列表为空", path: "/argus/api/v3/checkin/checkinexchangepage" } + var chosen = null + for (var i = 0; i < goods.length; i++) { + var price = Number(goods[i].GoodsScore || goods[i].NeedScore || goods[i].Price || goods[i].Score || 0) + if (price > 0 && price <= balance && (!chosen || price > Number(chosen.GoodsScore || chosen.NeedScore || chosen.Price || chosen.Score || 0))) chosen = goods[i] + } + if (!chosen) return { name: "每周兑换", ok: false, msg: "余额不足(" + balance + ")", path: "/argus/api/v3/checkin/checkinexchangepage" } + var gid = chosen.GoodsId || chosen.Id || chosen.GoodsID + var priceText = Number(chosen.GoodsScore || chosen.NeedScore || chosen.Price || chosen.Score || 0) + var cardCount = chosen.ChapterCardCount || chosen.CardCount || chosen.Count || "" + var r = qdPost("/argus/api/v3/checkin/exchangegoods", { goodId: gid }, cookie) + var code = Number(r && (r.result !== undefined ? r.result : (r.Result !== undefined ? r.Result : (r.code !== undefined ? r.code : r.Code)))) + if (code === -220000) return { name: "每周兑换", ok: false, msg: "未到兑换时间", path: "/argus/api/v3/checkin/exchangegoods" } + var ok = taskResultOk(r) + var successMsg = cardCount ? ("消耗 " + priceText + " 点兑换 " + cardCount + " 点章节卡") : "兑换成功" + return { name: "每周兑换", ok: ok, msg: resultMsg(r) || (ok ? successMsg : "兑换失败"), path: "/argus/api/v3/checkin/exchangegoods" } +} +function doChapterCard(cookie) { + var res = qdGet("/argus/api/v1/readtime/scoremall/chaptercardwithbook", {}, cookie) + var ok = taskResultOk(res) + var cards = firstArray(nested(res, ["Data", "Cards"]), nested(res, ["data", "Cards"]), nested(res, ["Data", "ChapterCards"]), nested(res, ["data", "ChapterCards"])) + return { name: "章节卡片", ok: !!ok, msg: ok ? (cards.length ? ("章节卡 " + cards.length + " 张") : "暂无章节卡") : (resultMsg(res) || "查询失败"), path: "/argus/api/v1/readtime/scoremall/chaptercardwithbook" } +} +function messageTokenFromCookie(cookie) { + var jar = parseCookieString(cookie) + var uid = extractUid(cookie) + var raw = uid ? ((BigInt(uid) * BigInt(4) & BigInt("0x7ffffffffffffffc")) ^ BigInt("0x113a4f")) : BigInt(0) + return raw ? makeBase62Token(raw) : (uid || jar.ywguid || "") +} +function recentCreateTime(v) { + if (!v) return false + var t = typeof v === "string" ? new Date(v).getTime() : Number(v) + if (!t || isNaN(t)) return false + if (t < 1000000000000) t = t * 1000 + var now = (new Date()).getTime() + return now - t >= 0 && now - t <= 24 * 60 * 60 * 1000 +} +function beijingHour(dateLike) { return beijingDate(dateLike).getUTCHours() } +function messageBoxWindowLabel() { return "全天" } +function messageBoxAllowedNow() { + return true +} +function orderIdFromText(s) { + var m = String(s || "").match(/orderId=([a-f0-9]+)/i) + return m ? m[1] : "" +} +function doMessageBox(cookie) { + var token = messageTokenFromCookie(cookie) + if (!token) return { name: "大咖荐书", ok: true, msg: "缺少消息 token,已跳过", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" } + var baseParams = { appappend: token, categoryIds: "0", pg: "1", pz: "10" } + var res = qdGet("https://magev6.if.qidian.com/argus/api/v2/message/pullmessage", baseParams, cookie) + if (!taskResultOk(res)) return { name: "大咖荐书", ok: false, msg: resultMsg(res) || "获取消息列表失败", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" } + var groups = firstArray(nested(res, ["Data", "UserList"]), nested(res, ["data", "UserList"]), nested(res, ["Data", "List"]), nested(res, ["data", "List"]), nested(res, ["Data", "MsgList"]), nested(res, ["data", "MsgList"])) + if (!groups.length) return { name: "大咖荐书", ok: true, msg: "无消息", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" } + var senders = [] + for (var i = 0; i < groups.length; i++) if (Number(field(groups[i], ["RedPoint", "redPoint"], 0)) === 1 && field(groups[i], ["FromId", "fromId", "SenderId", "senderId", "FromUserId", "fromUserId"], "")) senders.push(groups[i]) + if (!senders.length) return { name: "大咖荐书", ok: true, msg: "无未读消息", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" } + var handled = 0 + for (var s = 0; s < senders.length; s++) { + var sid = field(senders[s], ["FromId", "fromId", "SenderId", "senderId", "FromUserId", "fromUserId"], "") + var msgRes = qdGet("https://magev6.if.qidian.com/argus/api/v2/message/pullmessage", { appappend: token, categoryIds: "0", fromId: sid, pg: "1", pz: "10" }, cookie) + if (!taskResultOk(msgRes)) continue + var msgs = firstArray(nested(msgRes, ["Data", "MsgList"]), nested(msgRes, ["data", "MsgList"]), nested(msgRes, ["Data", "List"]), nested(msgRes, ["data", "List"]), nested(msgRes, ["Data", "Messages"]), nested(msgRes, ["data", "Messages"])) + for (var j = 0; j < msgs.length; j++) { + if (!recentCreateTime(field(msgs[j], ["CreateTime", "createTime"], ""))) continue + var orderId = orderIdFromText(field(msgs[j], ["ActionUrl", "actionUrl", "RefUrl", "refUrl", "Content", "content", "Link", "link", "Url", "url"], "")) + if (!orderId) continue + var detail = qdGet("/argus/api/v1/interaction/rewarddetail", { orderId: orderId, refresh: "0" }, cookie) + if (!taskResultOk(detail)) continue + var data = detail.Data || detail.data || {} + var chance = Number(data.ChanceCount || 0) + var bookStatus = Number(data.BookStatus || 0) + var bookId = data.BookId || "" + if (data.Status !== undefined && Number(data.Status) !== 1) continue + if (chance <= 0 && bookStatus !== 0 && bookStatus !== 1 && bookStatus !== 2) continue + if (chance > 0) { var cb = qdPost("/argus/api/v1/interaction/rewardcallback", { orderId: orderId, checkRisk: "1" }, cookie); if (taskResultOk(cb) || String(resultCode(cb)) === "0") handled++; continue } + if (bookStatus === 0 || bookStatus === 1) { if (!bookId) continue; var add = qdPost("/argus/api/v1/interaction/rewardaddshelf", { orderId: orderId, bookId: bookId }, cookie); if (taskResultOk(add) || String(resultCode(add)) === "0") handled++; continue } + if (bookStatus === 2) { var rc = qdPost("/argus/api/v1/interaction/rewardcallback", { orderId: orderId, checkRisk: "1" }, cookie); if (!taskResultOk(rc) && String(resultCode(rc)) !== "0") continue; var r2 = qdPost("/argus/api/v1/interaction/rewardcallback", { orderId: orderId, checkRisk: "0" }, cookie); if (taskResultOk(r2) || String(resultCode(r2)) === "0") handled++ } } } + return { name: "大咖荐书", ok: true, msg: handled ? ("成功处理 " + handled + " 个荐书") : "无待处理荐书", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" } +} +function doExtraTask(task, cookie) { + if (task.handler === "receiveGifts") return doReceiveGifts(cookie) + if (task.handler === "adv") return doAdvTask(cookie) + if (task.handler === "daily") return doDailyTask(cookie) + if (task.handler === "lottery") return doLotteryTask(cookie) + if (task.handler === "exchange") return doWeeklyExchange(cookie) + if (task.handler === "chapterCard") return doChapterCard(cookie) + if (task.handler === "messageBox") return doMessageBox(cookie) + var data = task.body || task.params || {} + var res = qidianRequest(task.path, task.method || "GET", data, cookie) + var ok = taskResultOk(res) + var msg = resultMsg(res) || (ok ? "完成" : ("失败" + (resultCode(res) !== undefined ? " code=" + resultCode(res) : ""))) + return { name: task.label, ok: !!ok, msg: String(msg || ""), path: task.path } +} +function doTaskChain(uid, cookie, source, runId) { + var startedAt = nowIso() + var details = [] + var checkin = qidianRequest(CHECKIN_PATH, "POST", {}, cookie) + var ok = taskResultOk(checkin) + var msg = resultMsg(checkin) || (ok ? "签到请求成功" : ("签到失败" + (resultCode(checkin) !== undefined ? " code=" + resultCode(checkin) : ""))) + details.push({ name: "签到结果", ok: !!ok, msg: String(msg || ""), path: CHECKIN_PATH }) + for (var i = 0; i < QD_TASKS.length; i++) { + var task = QD_TASKS[i] + if (!taskEnabledForUid(task, uid)) continue + try { details.push(doExtraTask(task, cookie)) } + catch (e) { + var tmsg = e && e.message ? e.message : String(e) + if (isWhitelistBlockedError(tmsg)) details.push({ name: task.label, ok: true, skipped: true, msg: "签名网关暂不支持该接口,已跳过", rawMsg: tmsg, path: task.path }) + else details.push({ name: task.label, ok: false, msg: tmsg, path: task.path }) + } + } + var failed = [] + for (var j = 0; j < details.length; j++) if (!details[j].ok && !details[j].skipped) failed.push(details[j].name + ":" + details[j].msg) + var finalOk = failed.length === 0 + var detailMsg = [] + for (var k = 0; k < details.length; k++) detailMsg.push(details[k].name + (details[k].skipped ? "⏭" : (details[k].ok ? "✅" : "❌")) + (details[k].msg ? "(" + displayMsg(details[k].msg) + ")" : "")) + var finalMsg = (finalOk ? "完全体任务完成" : "部分任务失败") + ":" + detailMsg.join(";") + var result = { uid: String(uid), ok: !!finalOk, msg: String(finalMsg || ""), time: nowIso(), startedAt: startedAt, source: source || "manual", runId: runId || "", tasks: details } + setLastResult(uid, result) + return { uid: uid, ok: finalOk, msg: finalMsg, raw: checkin, log: result } +} +function doCheckin(uid, cookie, source, runId) { + return doTaskChain(uid, cookie, source, runId) +} +function signAll(targetUid, allUsers, source, archiveAfterRun) { + source = source || (allUsers ? "cron" : "manual") + if (archiveAfterRun === undefined) archiveAfterRun = true + var startedAt = nowIso() + var runId = startedAt.replace(/[-:.TZ]/g, "").slice(0, 14) + "_" + source + var store = getStore() + var bind = getBindStore() + var remarks = remarksForStore(store) + var uids = visibleUids(store, bind, !!allUsers) + if (targetUid) uids = uids.filter(function (u) { return u === targetUid }) + if (!uids.length) { + var emptyMsg = targetUid ? ("未找到 uid: " + targetUid) : "还没有保存启点读书CK" + setLastRunLog({ runId: runId, source: source, startedAt: startedAt, finishedAt: nowIso(), targetUid: targetUid || "", total: 0, okCount: 0, failCount: 0, message: emptyMsg, items: [] }) + if (archiveAfterRun) archiveRunLog(source + "-after-sign") + return emptyMsg + } + var okCount = 0 + var failCount = 0 + var items = [] + var accountReplies = [] + var accountNotices = [] + for (var i = 0; i < uids.length; i++) { + var uid = uids[i] + var prefix = "启点读书签到结果:\n" + var suffix = "" + if (remarks[uid]) suffix = "(" + remarks[uid] + ")" + try { + var r = doCheckin(uid, store[uid], source, runId) + if (r.ok) okCount++; else failCount++ + items.push({ uid: String(uid), ok: !!r.ok, msg: String(r.msg || ""), time: r.log.time, remark: remarks[uid] || "" }) + var msg = prefix + (r.ok ? "✅ " : "❌ ") + maskUid(uid) + suffix + ":" + r.msg + accountReplies.push(msg) + accountNotices.push({ uid: String(uid), owner: bind[String(uid)] || "", msg: msg }) + } catch (e) { + failCount++ + var emsg = e && e.message ? e.message : String(e) + var er = { uid: String(uid), ok: false, msg: emsg, time: nowIso(), startedAt: startedAt, source: source, runId: runId, error: true, remark: remarks[uid] || "" } + setLastResult(uid, er) + items.push(er) + var errMsg = prefix + "❌ " + maskUid(uid) + suffix + ":" + emsg + accountReplies.push(errMsg) + accountNotices.push({ uid: String(uid), owner: bind[String(uid)] || "", msg: errMsg }) + } + } + var finishedAt = nowIso() + setLastRunLog({ runId: runId, source: source, startedAt: startedAt, finishedAt: finishedAt, targetUid: targetUid || "", allUsers: !!allUsers, total: uids.length, okCount: okCount, failCount: failCount, items: items }) + var tail = [] + tail.push("汇总:成功 " + okCount + " / 失败 " + failCount + " / 总计 " + uids.length) + tail.push("记录时间:" + finishedAt + "(" + sourceName(source) + ")") + var archiveLine = "" + if (archiveAfterRun) { + var ar = archiveRunLog(source + "-after-sign") + if (ar && ar.archived) { archiveLine = "日志已归档:" + ar.key; tail.push(archiveLine) } + } + if (allUsers && source === "cron") { + for (var pi = 0; pi < accountNotices.length; pi++) { + var notice = accountNotices[pi] + var body = notice.msg + "\n" + tail.join("\n") + if (!pushToUser(notice.owner, body)) debug("QDReader cron push skipped for uid " + notice.uid + ": empty owner or push unavailable") + } + return tail.join("\n") + } + if (accountReplies.length > 1) { + for (var ai = 0; ai < accountReplies.length; ai++) reply(accountReplies[ai]) + return tail.join("\n") + } + return accountReplies[0] + "\n" + tail.join("\n") +} + +function shortTime(iso) { + if (!iso) return "未知时间" + var s = String(iso) + var m = s.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})/) + if (m) return m[1] + " " + m[2] + return beijingTime(iso).slice(0, 16) +} +function displayMsg(msg) { + msg = trim(msg || "无结果") + if (!msg) return "无结果" + msg = msg.replace(/\s+/g, " ") + if (msg.length > 60) msg = msg.slice(0, 57) + "..." + return msg +} +function accountName(uid, remarks) { + return maskUid(uid) + (remarks && remarks[uid] ? "[" + remarks[uid] + "]" : "") +} +function compactTaskMsg(msg) { + msg = displayMsg(msg) + msg = msg.replace(/^完全体任务完成:/, "") + msg = msg.replace(/^部分任务失败:/, "") + return msg +} +function formatResultLine(uid, r, remarks, emptyText) { + var name = accountName(uid, remarks) + if (!r) return "- " + name + ":⚪ " + (emptyText || "暂无执行记录") + var status = r.ok ? "✅" : "❌" + var line = "- " + name + ":" + status + " " + compactTaskMsg(r.msg) + if (r.time) line += "\n 时间:" + shortTime(r.time) + return line +} +function formatRunSummary(log, label, key) { + log = log || {} + var lines = [] + lines.push("批次:" + (key || log.runId || "最近") + "(" + (label || "最近记录") + ")") + lines.push("时间:" + shortTime(log.finishedAt) + " | 来源:" + sourceName(log.source)) + lines.push("统计:成功 " + (log.okCount || 0) + " / 失败 " + (log.failCount || 0) + " / 总计 " + (log.total || 0)) + return lines +} +function queryStatus(targetUid) { + var store = getStore() + var bind = getBindStore() + var last = getLastResults() + var run = getLastRunLog() + var remarks = remarksForStore(store) + var uids = visibleUids(store, bind, false) + if (targetUid) { + if (uids.indexOf(targetUid) < 0) return store[targetUid] ? ("无权查询 uid: " + targetUid) : ("未找到 uid: " + targetUid) + uids = [targetUid] + } + if (!uids.length) return "当前用户还没有保存启点读书CK" + var archiveInfo = latestArchiveForVisible(uids) + var useArchive = archiveInfo && (!run || !run.finishedAt) + var lines = [targetUid ? "启点查询|单账号" : "启点查询|最近签到"] + if (useArchive) { + var arun = archiveInfo.log || {} + lines = lines.concat(formatRunSummary(arun, "归档", archiveInfo.key)) + lines.push("明细:") + for (var ai = 0; ai < uids.length; ai++) lines.push(formatResultLine(uids[ai], archiveInfo.itemMap[String(uids[ai])], remarks, "归档中暂无该账号记录")) + return lines.join("\n") + } + if (!targetUid && run && run.finishedAt) lines = lines.concat(formatRunSummary(run, "最近", "")) + else if (run && run.finishedAt) lines.push("时间:" + shortTime(run.finishedAt) + " | 来源:" + sourceName(run.source)) + else lines.push("批次:暂无") + lines.push("明细:") + for (var i = 0; i < uids.length; i++) lines.push(formatResultLine(uids[i], last[uids[i]], remarks, "暂无执行记录")) + if (archiveInfo) lines.push("归档:" + archiveInfo.key) return lines.join("\n") } -var text = contentText() -var cmd = commandName(text) -var arg = stripCommand(text).replace(/^sign\s*/i, "").trim() -if (!arg && cmd !== "sign") { reply(usage()); return } -if (/^(帮助|help|h)$/i.test(arg)) { reply(usage()); return } - -var store = getStore() - -if (cmd === "sign" || /^(签到|sign|run)(?:\s+|$)/i.test(arg)) { - var target = arg.replace(/^(签到|sign|run)\s*/i, "").trim() - reply(signAll(target)) - return +function listenInput(timeoutMs) { + try { if (typeof sender !== "undefined" && sender && sender.listen) return trim(sender.listen(timeoutMs || 60000)) } catch (e) {} + return "" +} +function accountMenuText(store, bind) { + var uids = visibleUids(store || getStore(), bind || getBindStore(), false) + var remarks = remarksForStore(store || {}) + var lines = ["启点账号管理", "账号列表如下:"] + if (!uids.length) lines.push("暂无账号,请回复 0 添加/更新账号") + else { + for (var i = 0; i < uids.length; i++) { + var uid = uids[i] + lines.push((i + 1) + "、" + maskUid(uid) + (remarks[uid] ? "[" + remarks[uid] + "]" : "")) + } + } + lines.push("0、添加/更新账号") + lines.push("-删除账号") + lines.push("q、退出") + return lines.join("\n") +} +function queryMenuText() { + return [ + "启点查询(最近签到记录)", + "1. 查看最近签到记录", + "2. 查询单个账号最近记录", + "3. 立即执行签到", + "说明:附加任务开关在 启点账号 → 账号任务配置 中按账号管理", + "q. 退出", + "请回复序号" + ].join("\n") +} +function accountListText(store, bind) { + var uids = visibleUids(store, bind, false) + var remarks = remarksForStore(store) + if (!uids.length) return "当前用户还没有保存启点读书CK" + var lines = ["启点账号:当前用户共 " + uids.length + " 个账号"] + for (var i = 0; i < uids.length; i++) { + var uid = uids[i] + lines.push(maskUid(uid) + (remarks[uid] ? "(" + remarks[uid] + ")" : "")) + } + lines.push("发送 启点签到 可执行签到") + return lines.join("\n") } -if (/^(查询|状态|status|list)$/i.test(arg)) { - var uids = Object.keys(store) - reply(uids.length ? ("起点读书CK:共 " + uids.length + " 个账号\n" + uids.join("\n") + "\n发送 起点签到 可执行签到") : "还没有保存起点读书CK") - return + +function accountSelectText(store, bind, title) { + var uids = visibleUids(store, bind, false) + var remarks = remarksForStore(store) + if (!uids.length) return { text: "当前用户还没有保存启点读书CK", uids: [] } + var lines = [title || "请选择账号:"] + for (var i = 0; i < uids.length; i++) { + var uid = uids[i] + lines.push((i + 1) + ". " + maskUid(uid) + (remarks[uid] ? "(" + remarks[uid] + ")" : "")) + } + lines.push("q. 退出") + lines.push("请回复编号") + return { text: lines.join("\n"), uids: uids } } -if (/^(导出|export)$/i.test(arg)) { reply("QDREADER_COOKIE=" + JSON.stringify(store)); return } -var del = arg.match(/^(删除|delete|del|rm)\s+(\S+)/i) -if (del) { var duid = del[2]; var existed = !!store[duid]; delete store[duid]; setStore(store); reply(existed ? ("已删除 uid: " + duid) : ("未找到 uid: " + duid)); return } -var jsonMatch = arg.match(/^json\s+([\s\S]+)$/i) -if (jsonMatch) { - var obj = safeJsonParse(jsonMatch[1], null) - if (!obj || typeof obj !== "object" || Array.isArray(obj)) { reply("JSON格式错误,示例:起点ck json {\"uid\":\"cookie\"}"); return } - var count = 0 - for (var uid in obj) { if (!obj[uid]) continue; store[uid] = String(obj[uid]); count++ } - setStore(store); reply("起点读书CK JSON已导入/更新 " + count + " 个账号,当前共 " + Object.keys(store).length + " 个账号\n发送 起点签到 可执行签到"); return +function chooseUid(store, bind, title) { + var sel = accountSelectText(store, bind, title) + reply(sel.text) + if (!sel.uids.length) return "" + var op = listenInput(60000) + if (!op || /^(q|退出|取消)$/i.test(op)) return "" + var n = parseInt(op, 10) + if (isNaN(n) || n < 1 || n > sel.uids.length) { reply("编号无效,已退出"); return "" } + return sel.uids[n - 1] } -var cookie = arg -if (cookie.indexOf("=") < 0 || cookie.indexOf(";") < 0) { reply("未识别到 Cookie。请发送:起点ck "); return } -var uid = extractUid(cookie) -if (!uid) { reply("无法从 Cookie 识别 uid。请确认 QX 抓的是起点读书福利中心 getlogininfo 请求,Cookie 内应包含 QDHeader 或 uid。"); return } -var isNew = !store[uid] -var changed = store[uid] !== cookie -store[uid] = cookie -setStore(store) -reply(["起点读书CK" + (isNew ? "新增" : (changed ? "更新" : "未变化")) + "成功", "uid: " + uid, "账号数: " + Object.keys(store).length, "cookie: " + maskCookie(cookie), "签到:发送 起点签到"].join("\n")) +function maybePromptRemark(uid, store) { + if (!uid) return "" + reply("是否为该账号添加备注?回复备注名,发送 n/跳过 则不添加") + var remark = listenInput(60000) + if (!remark || /^(n|no|否|不|跳过|q|退出|取消)$/i.test(remark)) return "" + var remarks = store ? remarksForStore(store) : getRemarkStore() + remarks[uid] = remark.slice(0, 30) + setRemarkStore(remarks) + return "\n备注: " + remarks[uid] +} +function accountDetailText(uid, store, bind) { + var uids = visibleUids(store, bind, false) + if (uids.indexOf(uid) < 0) return store[uid] ? ("无权查看 uid: " + uid) : ("未找到 uid: " + uid) + var remarks = remarksForStore(store) + var last = getLastResults() + return [ + "启点账号详情:", + "uid: " + maskUid(uid), + "备注: " + (remarks[uid] || "未设置"), + "cookie: " + maskCookie(store[uid]), + taskStatusLines(uid).replace(/\n回复:[\s\S]*$/, ""), + formatResultLine(uid, last[uid], remarks, "暂无执行记录"), + "修改备注请回复新备注名,发送 清空 可删除备注,发送 q 退出" + ].join("\n") +} +function accountConfigText(uid, store, bind) { + var detail = accountDetailText(uid, store, bind) + if (/^(无权|未找到)/.test(detail)) return detail + return detail + "\n\n账号操作:\n1、修改备注\n2、任务配置\n3、立即签到该账号\n-删除该账号\nq、返回/退出" +} +function accountConfigMenu(uid, store, bind) { + if (!uid) return reply("未选择账号,已退出") + var cfg = accountConfigText(uid, store, bind) + reply(cfg) + if (/^(无权|未找到)/.test(cfg)) return + var op = listenInput(60000) + if (!op || /^(q|退出|取消|返回)$/i.test(op)) return reply("已退出") + if (/^(1|备注|改名|名称)$/i.test(op)) { + reply("请回复新备注名;发送 清空 可删除备注;q 退出") + var remark = listenInput(60000) + if (!remark || /^(q|退出|取消)$/i.test(remark)) return reply("已退出") + var remarks = remarksForStore(store) + if (/^(清空|删除|none)$/i.test(remark)) delete remarks[uid]; else remarks[uid] = remark.slice(0, 30) + setRemarkStore(remarks) + return reply("已更新备注:" + maskUid(uid) + (remarks[uid] ? "(" + remarks[uid] + ")" : "")) + } + if (/^(2|任务|配置|开关)$/i.test(op)) return taskMenuForUid(uid) + if (/^(3|签到|执行|立即)$/i.test(op)) return reply(signAll(uid, false, "menu")) + if (/^-1$/.test(op) || /^(删除|delete|del|rm)$/i.test(op)) return reply(handleDelete("删除 " + uid, store, bind)) + return reply("未识别选项,已退出") +} +function accountDetailMenu(store, bind) { + var uid = chooseUid(store, bind, "请选择要查看/修改备注的账号:") + if (!uid) return reply("已退出") + return accountConfigMenu(uid, store, bind) +} +function accountMenu(store, bind) { + var uids = visibleUids(store, bind, false) + reply(accountMenuText(store, bind)) + var op = listenInput(60000) + if (!op || /^(q|退出|取消)$/i.test(op)) return reply("已退出") + if (/^(0|a|add|添加|新增|更新)$/i.test(op)) { + reply("请在60秒内发送 QX 抓到的完整 Cookie(q:退出)") + var ck = listenInput(60000) + if (!ck || /^(q|退出|取消)$/i.test(ck)) return reply("已退出") + var save = handleSaveCookie(ck, store, bind) + var remarkMsg = (save.ok && save.uids.length === 1) ? maybePromptRemark(save.uids[0], store) : "" + return reply(save.text + remarkMsg) + } + var n = parseInt(op, 10) + if (!isNaN(n) && String(n) === op.replace(/^\+/, "")) { + if (n > 0) { + if (n > uids.length) return reply("编号无效,已退出") + return accountConfigMenu(uids[n - 1], store, bind) + } + if (n < 0) { + var idx = Math.abs(n) + if (idx > uids.length) return reply("编号无效,已退出") + return reply(handleDelete("删除 " + uids[idx - 1], store, bind)) + } + } + return reply("未识别选项,已退出") +} +function queryMenu() { + reply(queryMenuText()) + var op = listenInput(60000) + if (!op || /^(q|退出|取消)$/i.test(op)) return reply("已退出") + if (/^(1|查看|查询|状态|结果)$/i.test(op)) return reply(queryStatus()) + if (/^(2|单个|单号|账号)$/i.test(op)) { + var store = getStore() + var bind = getBindStore() + var uid = chooseUid(store, bind, "请选择要查询的账号:") + if (!uid) return reply("已退出") + return reply(queryStatus(uid)) + } + if (/^(3|执行|签到|立即)$/i.test(op)) return reply(signAll("", false, "menu")) + return reply("未识别选项,已退出") +} +function handleDelete(arg, store, bind) { + var del = arg.match(/^(删除|delete|del|rm)\s+(\S+)/i) + if (!del) return null + var duid = del[2] + var uids = visibleUids(store, bind, false) + if (/^(all|全部|所有)$/i.test(duid)) { + for (var i = 0; i < uids.length; i++) { delete store[uids[i]]; delete bind[uids[i]] } + var remarks = remarksForStore(store); for (var j = 0; j < uids.length; j++) delete remarks[uids[j]] + var allPrefs = getTaskPrefs(); for (var p = 0; p < uids.length; p++) delete allPrefs[uids[p]] + setStore(store); setBindStore(bind); setRemarkStore(remarks); setTaskPrefs(allPrefs) + return "已删除全部当前用户绑定的启点读书CK,共 " + uids.length + " 个账号" + } + if (uids.indexOf(duid) < 0) return store[duid] ? ("无权删除 uid: " + duid) : ("未找到 uid: " + duid) + delete store[duid]; delete bind[duid] + var remarks = remarksForStore(store); delete remarks[duid] + var prefs = getTaskPrefs(); delete prefs[duid] + setStore(store); setBindStore(bind); setRemarkStore(remarks); setTaskPrefs(prefs) + return "已删除 uid: " + duid +} +function normalizeCookieInput(arg) { + var s = trim(arg) + var obj = safeJsonParse(s, null) + if (isObject(obj)) { + var out = [] + for (var uid in obj) if (obj.hasOwnProperty(uid)) { + var realUid = isRealUid(uid) + var cookie = trim(obj[uid]) + if (!realUid || !cookie) continue + out.push({ uid: realUid, cookie: cookie }) + } + return out + } + return [{ uid: "", cookie: s }] +} +function handleSaveCookie(arg, store, bind) { + var entries = normalizeCookieInput(arg) + if (!entries.length) return { ok: false, text: "未识别到 Cookie。请发送:启点ck {\"uid\":\"cookie\"} 或 启点ck ", uids: [] } + var me = currentOwnerId() + if (!me) return { ok: false, text: "无法识别当前用户,已拒绝保存CK,避免错误绑定。请确认插件在聊天会话中触发;诊断: " + currentUserDebug(), uids: [] } + var lines = [] + var savedUids = [] + for (var i = 0; i < entries.length; i++) { + var cookie = entries[i].cookie + if (cookie.indexOf("=") < 0 || cookie.indexOf(";") < 0) return { ok: false, text: "未识别到 Cookie。请发送:启点ck {\"uid\":\"cookie\"} 或 启点ck ", uids: savedUids } + var parsedUid = extractUid(cookie) + var uid = entries[i].uid || parsedUid + if (!uid) return { ok: false, text: "无法从 Cookie 识别 uid。请确认 QX 抓的是启点读书福利中心 getlogininfo 请求,Cookie 内应包含 QDHeader 或 uid。", uids: savedUids } + var isNew = !store[uid] + var oldOwner = bind[uid] || "" + if (!store[uid] && oldOwner && oldOwner !== me) { + delete bind[uid] + oldOwner = "" + lines.push("检测到 uid: " + uid + " 只有旧绑定没有CK,已自动清理残留绑定") + } + if (store[uid] && oldOwner && oldOwner !== me && canRepairBinding(oldOwner, me)) { + lines.push("检测到 uid: " + uid + " 旧绑定来自历史兜底标识 " + maskUid(oldOwner) + ",已修复为当前用户") + oldOwner = "" + } + if (store[uid] && oldOwner && me && oldOwner !== me) { + lines.push("无权更新 uid: " + uid + "(该账号已绑定其他用户)") + continue + } + var changed = store[uid] !== cookie + store[uid] = cookie + bind[uid] = me + savedUids.push(uid) + lines.push("启点读书CK" + (isNew ? "新增" : (changed ? "更新" : "未变化")) + "成功") + lines.push("uid: " + uid) + if (entries.length > 1 && parsedUid && parsedUid !== uid) lines.push("提示: JSON uid 与 Cookie 解析 uid 不一致,已按 JSON uid 保存") + } + setStore(store); setBindStore(bind) + lines.push(me ? ("绑定用户: " + maskUid(me)) : "绑定用户: 未识别") + lines.push("账号数: " + visibleUids(store, bind, false).length) + lines.push("cookie: " + maskCookie(entries[0].cookie)) + lines.push("签到:发送 启点签到") + return { ok: savedUids.length > 0, text: lines.join("\n"), uids: savedUids } +} +function main() { + var text = contentText() + var isCronRun = !text + var cmd = isCronRun ? cronMode() : commandName(text) + var arg = isCronRun ? "" : trim(stripCommand(text).replace(/^(sign|archive)\s*/i, "")) + if (!arg && cmd !== "sign" && cmd !== "archive" && cmd !== "account" && cmd !== "status") { reply(usage()); return } + if (/^(帮助|help|h)$/i.test(arg)) { reply(usage()); return } + + var store = getStore() + var bind = getBindStore() + if (cmd === "archive" || /^(归档|日志归档|archive)(?:\s+|$)/i.test(arg)) { + reply(formatArchiveReply(archiveRunLog(isCronRun ? "midnight-archive" : "manual-archive"))) + return + } + if (cmd === "sign" || /^(签到|sign|run)(?:\s+|$)/i.test(arg)) { + var target = trim(arg.replace(/^(签到|sign|run)\s*/i, "")) + var out = signAll(target, isCronRun, isCronRun ? "cron" : "manual") + if (!(isCronRun && getBool(CRON_SILENT_KEY, false) && out === "还没有保存启点读书CK")) reply(out) + return + } + if (cmd === "account") { accountMenu(store, bind); return } + if (cmd === "status") { queryMenu(); return } + if (cmd === "ck" && /^(查询|状态|status|list|账号|管理|任务|配置)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;任务配置在 启点账号 → 账号任务配置;快速提交CK:启点ck "); return } + var delMsg = handleDelete(arg, store, bind) + if (delMsg !== null) { reply(delMsg); return } + if (cmd === "ck") { + var save = handleSaveCookie(arg, store, bind) + if (save.ok && save.uids.length === 1) save.text += maybePromptRemark(save.uids[0], store) + reply(save.text) + return + } + var fallbackSave = handleSaveCookie(arg, store, bind) + reply(fallbackSave.text) +} +main()