Files
autman-plugins/plugins/qdreader/qdreader_sign_autman.js
T

1208 lines
64 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//[open_source: true]
//[create_at: 2026-05-17 03:00:00]
//[title: 启点读书签到]
//[author: Hermes]
//[service: BOSS]
//[class: 工具类]
//[version: 2.10.9]
//[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 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) {
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 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 = 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.hasOwnProperty(k) && jar[k] !== undefined && jar[k] !== null && jar[k] !== "") arr.push(k + "=" + jar[k])
return arr.join("; ")
}
function b64decode(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 { 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 extractUid(cookie) {
var jar = parseCookieString(cookie)
var uid = uidFromQDHeader(jar.QDHeader || jar.qdheader || jar.QDHEADER || "")
if (uid) return uid
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 contentText() {
var s = ""
try { if (typeof getContent === "function") s = getContent() || "" } catch (e) {}
try { if (!s && typeof GetContent === "function") s = GetContent() || "" } catch (e) {}
try { if (!s && typeof input === "function") s = input() || "" } catch (e) {}
try { if (!s && typeof sender !== "undefined" && sender && sender.getContent) s = sender.getContent() || "" } catch (e) {}
try { if (!s && typeof sender !== "undefined" && sender && sender.getMessage) s = sender.getMessage() || "" } catch (e) {}
try { if (!s && typeof message !== "undefined") s = String(message) } catch (e) {}
return trim(s)
}
function usage() {
return [
"启点读书签到插件 v2.10.9",
"【一级菜单】",
"账号管理:启点账号",
"执行查询:启点查询",
"【快速通道】",
"快速提交CK:启点ck <QX抓到的cookie>",
"直接签到:启点签到",
"指定签到:启点签到 <uid>",
"账号任务:启点账号 → 账号详情/任务配置",
"【配置】",
"签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API,
"完全体任务:领取奖励固定执行;激励/每日/抽奖/兑换/章节卡/大咖荐书按账号单独管理,默认关闭。",
"定时:每日 07:01 自动执行签到链路;QX 只负责抓 cookie。"
].join("\n")
}
function stripCommand(text) {
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 "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) if (v.hasOwnProperty(sk)) parts.push(encodeURIComponent(k + "[" + sk + "]") + "=" + encodeURIComponent(String(v[sk]))) }
else parts.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(v)))
}
return parts.join("&")
}
function normalizeCookie(cookie) {
var jar = parseCookieString(cookie)
var session = jar.sessionyw || jar.session || jar.sessionKey || ""
if (session) jar.sessionyw = session
var appid = jar.appId || jar.appid || ""
if (appid) jar.appId = appid
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) {
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) {
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)
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: 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
} else {
req.headers["Content-Type"] = req.headers["Content-Type"] || "application/x-www-form-urlencoded"
req.body = qs(data || {})
}
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 = 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) {
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 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 onall 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 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 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 = firstArray(nested(main, ["Data", "VideoRewardTab", "TaskList"]), nested(main, ["data", "VideoRewardTab", "TaskList"]), nested(main, ["Data", "TaskList"]), nested(main, ["data", "TaskList"]))
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
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(t, cookie, 1)
submittedCount += fr.ok
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 = firstArray(nested(verify, ["Data", "VideoRewardTab", "TaskList"]), nested(verify, ["data", "VideoRewardTab", "TaskList"]), nested(verify, ["Data", "TaskList"]), nested(verify, ["data", "TaskList"]))
var finished = !!(after && after.length && taskFinished(after[after.length - 1]))
return { name: "激励任务", ok: finished, msg: finished ? "已完成" : (submittedCount ? "任务未完成" : "未找到可提交任务"), path: "/argus/api/v2/video/adv/mainPage" }
}
function doDailyTask(cookie) {
var list = qdGet("/argus/api/v3/checkin/packagelist", {}, cookie)
if (!taskResultOk(list)) return { name: "每日任务", ok: false, msg: resultMsg(list) || "获取任务列表失败", path: "/argus/api/v3/checkin/packagelist" }
var data = list.Data || list.data || {}
var pre = field(data, ["Task", "SurpriseTask", "CurrentTask"], null)
var done = 0
if (pre && !taskFinished(pre) && taskIdOf(pre)) {
var pr = finishVideoTask(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 = firstArray(nested(list, ["Data", "PackageList", "TaskList"]), nested(list, ["data", "PackageList", "TaskList"]), nested(list, ["Data", "TaskList"]), nested(list, ["data", "TaskList"]), nested(list, ["Data", "PackageList"]), nested(list, ["data", "PackageList"]))
var oneAd = null
var target = null
for (var i = 0; i < tasks.length; i++) {
var n = taskTitle(tasks[i])
if (!oneAd && /完成\s*1\s*个广告任务|1个广告|one.?ad/i.test(n)) oneAd = tasks[i]
if (!target && n === "每日任务") target = tasks[i]
}
if (!target) for (var k = 0; k < tasks.length; k++) { var n2 = taskTitle(tasks[k]); if (/每日|daily/i.test(n2)) { target = tasks[k]; break } }
if (oneAd && oneAd !== target && !taskFinished(oneAd) && taskIdOf(oneAd)) {
var ar = finishVideoTask(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: true, msg: done ? ("完成 " + done + " 次") : "无每日任务", path: "/argus/api/v3/checkin/packagelist" }
if (taskFinished(target)) return { name: "每日任务", ok: true, msg: "已完成", path: "/argus/api/v3/checkin/packagelist" }
var tid = taskIdOf(target)
if (!tid) return { name: "每日任务", ok: true, msg: done ? ("完成 " + done + " 次") : "无每日任务", path: "/argus/api/v3/checkin/packagelist" }
var tr = finishVideoTask(target, cookie, taskRunCount(target, 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 = firstArray(nested(verify, ["Data", "VideoRewardTab", "TaskList"]), nested(verify, ["data", "VideoRewardTab", "TaskList"]), nested(verify, ["Data", "TaskList"]), nested(verify, ["data", "TaskList"]))
var finished = false
for (var v = 0; v < after.length; v++) if (taskIdKey(taskIdOf(after[v])) === taskIdKey(tid) && taskFinished(after[v])) { finished = true; break }
if (!finished) for (var x = 0; x < after.length; x++) if (taskTitle(after[x]) === "每日任务" && taskFinished(after[x])) { finished = true; break }
return { name: "每日任务", ok: finished || done > 0, msg: finished ? "已完成" : (done > 0 ? ("完成 " + done + " 次") : "任务未完成"), path: "/argus/api/v3/checkin/packagelist" }
}
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)
var goods = firstArray(data.GoodsList, data.GiftList, data.List, data.Items)
var chosen = null
for (var i = 0; i < goods.length; i++) { var price = Number(goods[i].NeedScore || goods[i].Price || goods[i].Score || 0); if (price > 0 && price <= balance && (!chosen || price > Number(chosen.NeedScore || chosen.Price || chosen.Score || 0))) chosen = goods[i] }
if (!chosen) return { name: "每周兑换", ok: true, msg: "无可兑换商品或余额不足(" + balance + ")", path: "/argus/api/v3/checkin/checkinexchangepage" }
var r = qdPost("/argus/api/v3/checkin/exchangegoods", { GoodsId: chosen.GoodsId || chosen.Id || chosen.GoodsID }, cookie)
return { name: "每周兑换", ok: taskResultOk(r), msg: resultMsg(r) || (taskResultOk(r) ? "兑换成功" : "兑换失败"), 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 "20:00-22:00" }
function messageBoxAllowedNow() {
var h = beijingHour()
return h >= 20 && h < 22
}
function orderIdFromText(s) {
var m = String(s || "").match(/orderId=([a-f0-9]+)/i)
return m ? m[1] : ""
}
function doMessageBox(cookie) {
if (!messageBoxAllowedNow()) return { name: "大咖荐书", ok: true, skipped: true, msg: "不在执行时间 " + messageBoxWindowLabel() + ",已跳过", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" }
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 = { token: token, type: "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", "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], ["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], ["SenderId", "senderId", "FromUserId", "fromUserId"], "")
var msgRes = qdGet("https://magev6.if.qidian.com/argus/api/v2/message/pullmessage", { token: token, type: "0", senderId: sid, pg: "1", pz: "10" }, cookie)
if (!taskResultOk(msgRes)) continue
var msgs = firstArray(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], ["Content", "content", "Link", "link", "Url", "url"], ""))
if (!orderId) continue
var detail = qdGet("/argus/api/v2/recommend/getrewarddetail", { orderId: orderId, checkRisk: "0" }, cookie)
if (!taskResultOk(detail)) continue
var data = detail.Data || detail.data || {}
var chance = Number(data.ChanceCount || 0)
var rewardType = Number(data.RewardType || data.Type || 0)
var bookId = data.BookId || ""
if (data.Status !== undefined && Number(data.Status) !== 1) continue
if (chance > 0) { var cb = qdPost("/argus/api/v2/recommend/rewardcallback", { orderId: orderId, checkRisk: "1" }, cookie); if (taskResultOk(cb)) handled++; continue }
if (rewardType === 1 || rewardType === 2) { if (!bookId) continue; var add = qdPost("/argus/api/v2/recommend/addbookshelf", { orderId: orderId, bookId: bookId }, cookie); if (taskResultOk(add)) handled++; continue }
if (rewardType === 3) { var rc = qdPost("/argus/api/v2/recommend/rewardcallback", { orderId: orderId, checkRisk: "1" }, cookie); if (!taskResultOk(rc)) continue; var r2 = qdPost("/argus/api/v2/recommend/rewardcallback", { orderId: orderId, checkRisk: "0" }, cookie); if (taskResultOk(r2)) 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 = []
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] || "" })
accountReplies.push(prefix + (r.ok ? "✅ " : "❌ ") + maskUid(uid) + suffix + "" + r.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)
accountReplies.push(prefix + "❌ " + maskUid(uid) + suffix + "" + emsg)
}
}
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) + "")
if (archiveAfterRun) {
var ar = archiveRunLog(source + "-after-sign")
if (ar && ar.archived) tail.push("日志已归档:" + ar.key)
}
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 formatResultLine(uid, r, remarks, emptyText) {
var name = accountName(uid, remarks)
if (!r) return "• " + name + ":⚪ " + (emptyText || "暂无执行记录")
return "• " + name + "" + (r.ok ? "✅ " : "❌ ") + displayMsg(r.msg) + (r.time ? "\n 时间:" + shortTime(r.time) : "")
}
function formatRunSummary(log, label, key) {
log = log || {}
var lines = []
lines.push((label || "最近批次") + (key ? "" + key : ""))
lines.push("来源:" + sourceName(log.source) + " 时间:" + shortTime(log.finishedAt))
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 = []
if (useArchive) {
var arun = archiveInfo.log || {}
lines.push(targetUid ? "启点单账号最近签到记录" : "启点最近签到记录")
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")
}
lines.push(targetUid ? "启点单账号最近签到记录" : "启点最近签到记录")
if (!targetUid && run && run.finishedAt) lines = lines.concat(formatRunSummary(run, "最近批次", ""))
else if (run && run.finishedAt) lines.push("时间:" + shortTime(run.finishedAt) + " 来源:" + sourceName(run.source))
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")
}
function listenInput(timeoutMs) {
try { if (typeof sender !== "undefined" && sender && sender.listen) return trim(sender.listen(timeoutMs || 60000)) } catch (e) {}
return ""
}
function accountMenuText() {
return [
"启点账号管理",
"1. 查看账号",
"2. 添加/更新账号",
"3. 账号详情/修改备注",
"4. 账号任务配置",
"5. 删除账号",
"6. 清空账号",
"q. 退出",
"请回复序号"
].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")
}
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 }
}
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]
}
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 accountDetailMenu(store, bind) {
var uid = chooseUid(store, bind, "请选择要查看/修改备注的账号:")
if (!uid) return reply("已退出")
var detail = accountDetailText(uid, store, bind)
reply(detail)
if (/^(无权|未找到)/.test(detail)) return
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] + "" : ""))
}
function accountMenu(store, bind) {
reply(accountMenuText())
var op = listenInput(60000)
if (!op || /^(q|退出|取消)$/i.test(op)) return reply("已退出")
if (/^(1|查看|查询|列表|账号)$/i.test(op)) return reply(accountListText(store, bind))
if (/^(2|添加|新增|更新)$/i.test(op)) {
reply("请在60秒内发送 QX 抓到的完整 Cookieq:退出)")
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)
}
if (/^(3|详情|备注|改名|名称)$/i.test(op)) return accountDetailMenu(store, bind)
if (/^(4|任务|配置|开关)$/i.test(op)) {
var taskUid = chooseUid(store, bind, "请选择要配置任务的账号:")
if (!taskUid) return reply("已退出")
return taskMenuForUid(taskUid)
}
if (/^(5|删除)$/i.test(op)) {
var uid = chooseUid(store, bind, "请选择要删除的账号:")
if (!uid) return reply("已退出")
return reply(handleDelete("删除 " + uid, store, bind))
}
if (/^(6|清空|全部)$/i.test(op)) return reply(handleDelete("删除 all", 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 <QX抓到的完整cookie>", 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 <QX抓到的完整cookie>", 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 <cookie>"); 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()