Files
autman-plugins/js/qdreader_sign_autman.js
T

816 lines
39 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.5.5]
//[platform: web,qq,wx,tg,tb,fs,we]
//[public: false]
//[price: 0]
//[disable: false]
//[description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”;定时使用 `1 0,7 * * *`,每日 00:01 归档并清空日志、每日 07:01 签到;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。]
//[rule: ^启点(账号|查询|ck|CK|cookie|Cookie|签到|读书|归档|日志归档)(.*)$]
//[rule: ^qdreader(.*)$]
//[admin: true]
//[priority: 600]
//[cron: 1 0,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 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"
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.5.5",
"【一级菜单】",
"账号管理:启点账号",
"执行查询:启点查询",
"【快速通道】",
"快速提交CK:启点ck <QX抓到的cookie>",
"直接签到:启点签到",
"指定签到:启点签到 <uid>",
"【配置】",
"签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API,
"定时:每日 00:01 只归档并清空日志;每日 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 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 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: 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) { return obj && (obj.Result !== undefined ? obj.Result : obj.Code !== undefined ? obj.Code : obj.code !== undefined ? obj.code : obj.retcode) }
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)) || ""
}
function nowIso() { try { return new Date().toISOString() } catch (e) { return String(new Date()) } }
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 d = iso ? new Date(iso) : new Date()
if (!d || isNaN(d.getTime())) d = new Date()
var y = d.getFullYear()
var m = String(d.getMonth() + 1); if (m.length < 2) m = "0" + m
var day = String(d.getDate()); if (day.length < 2) day = "0" + day
return y + "-" + m + "-" + day
}
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() {
var d = new Date()
var h = d.getHours ? d.getHours() : 7
// Single cron: 1 0,7 * * *
// 00:01 system run archives only; 07:01 system run signs only.
if (h === 0) return "archive"
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 doCheckin(uid, cookie, source, runId) {
var startedAt = nowIso()
var res = qidianRequest(CHECKIN_PATH, "POST", {}, cookie)
var ok = resultOk(res)
var msg = resultMsg(res) || (ok ? "签到请求成功" : ("签到失败" + (resultCode(res) !== undefined ? " code=" + resultCode(res) : "")))
var result = { uid: String(uid), ok: !!ok, msg: String(msg || ""), time: nowIso(), startedAt: startedAt, source: source || "manual", runId: runId || "" }
setLastResult(uid, result)
return { uid: uid, ok: ok, msg: msg, raw: res, log: result }
}
function signAll(targetUid, allUsers, source) {
source = source || (allUsers ? "cron" : "manual")
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: [] })
return emptyMsg
}
var okCount = 0
var failCount = 0
var items = []
var lines = ["启点读书签到结果:"]
for (var i = 0; i < uids.length; i++) {
var uid = uids[i]
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] || "" })
lines.push((r.ok ? "✅ " : "❌ ") + maskUid(uid) + (remarks[uid] ? "" + remarks[uid] + "" : "") + "" + 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)
lines.push("❌ " + maskUid(uid) + (remarks[uid] ? "" + remarks[uid] + "" : "") + "" + 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 })
lines.push("汇总:成功 " + okCount + " / 失败 " + failCount + " / 总计 " + uids.length)
lines.push("记录时间:" + finishedAt + "" + sourceName(source) + "")
return lines.join("\n")
}
function formatStatusLine(uid, last, remarks) {
var name = maskUid(uid) + (remarks && remarks[uid] ? "" + remarks[uid] + "" : "")
var r = last[uid]
if (!r) return "- " + name + ":暂无执行记录"
return "- " + name + "" + (r.ok ? "✅ " : "❌ ") + (r.msg || "无结果") + (r.time ? " @ " + r.time : "")
}
function formatArchiveStatusLine(uid, archiveInfo, remarks) {
var name = maskUid(uid) + (remarks && remarks[uid] ? "" + remarks[uid] + "" : "")
var r = archiveInfo && archiveInfo.itemMap ? archiveInfo.itemMap[String(uid)] : null
if (!r) return "- " + name + ":归档中暂无该账号记录"
return "- " + name + "" + (r.ok ? "✅ " : "❌ ") + (r.msg || "无结果") + (r.time ? " @ " + r.time : "")
}
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 title = targetUid ? "启点单账号最近签到记录:" : "启点最近签到记录:"
var archiveInfo = latestArchiveForVisible(uids)
var useArchive = archiveInfo && (!run || !run.finishedAt)
if (useArchive) title = targetUid ? "启点单账号最近归档签到记录:" : "启点最近归档签到记录:"
var lines = [title]
if (useArchive) {
var arun = archiveInfo.log || {}
lines.push("最近归档:" + archiveInfo.key + "" + sourceName(arun.source) + " @ " + (arun.finishedAt || "未知时间") + ";成功 " + (arun.okCount || 0) + " / 失败 " + (arun.failCount || 0) + " / 总计 " + (arun.total || 0))
for (var ai = 0; ai < uids.length; ai++) lines.push(formatArchiveStatusLine(uids[ai], archiveInfo, remarks))
} else {
if (!targetUid && run && run.finishedAt) lines.push("最近批次:" + sourceName(run.source) + " @ " + run.finishedAt + ";成功 " + (run.okCount || 0) + " / 失败 " + (run.failCount || 0) + " / 总计 " + (run.total || 0))
for (var i = 0; i < uids.length; i++) lines.push(formatStatusLine(uids[i], last, 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. 清空账号",
"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]),
formatStatusLine(uid, last, 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 uid = chooseUid(store, bind, "请选择要删除的账号:")
if (!uid) return reply("已退出")
return reply(handleDelete("删除 " + uid, store, bind))
}
if (/^(5|清空|全部)$/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]]
setStore(store); setBindStore(bind); setRemarkStore(remarks)
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]
setStore(store); setBindStore(bind); setRemarkStore(remarks)
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()