Files
autman-plugins/js/qdreader_sign_autman.js
T

357 lines
17 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.2.0]
//[platform: web,qq,wx,tg,tb,fs,we]
//[public: false]
//[price: 0]
//[disable: false]
//[description: 起点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“起点签到”和每日 07:15 定时签到;带多环境存储兼容、请求重试、签名响应兼容、结果摘要和安全脱敏。]
//[rule: ^起点(ck|CK|cookie|Cookie|签到|读书)(.*)$]
//[rule: ^qdreader(.*)$]
//[admin: true]
//[priority: 600]
//[cron: 15 7 * * *]
//[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,可替换为兼容 /sign 的网关"}]
//[param: {"required":false,"key":"otto.qdreader_retry_count","bool":false,"placeholder":"1","name":"请求失败重试次数","desc":"默认 1,最大 3,仅对网络异常/非业务异常重试"}]
//[param: {"required":false,"key":"otto.qdreader_cron_silent","bool":true,"placeholder":"false","name":"定时无账号时静默","desc":"true 时定时触发且没有账号不回复"}]
var BUCKET = "otto"
var STORE_KEY = "qdreader_cookie_json"
var SIGN_API_KEY = "qdreader_sign_api"
var RETRY_KEY = "qdreader_retry_count"
var CRON_SILENT_KEY = "qdreader_cron_silent"
var DEFAULT_SIGN_API = "https://api.120399.xyz"
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 get === "function") { var v = get(key); if (v !== undefined && v !== null && String(v) !== "") return v } } catch (e) {}
try { if (typeof bucketGet === "function") { var bv = bucketGet(BUCKET, key); if (bv !== undefined && bv !== null && String(bv) !== "") return bv } } catch (e) {}
return def
}
function cfgSet(key, value) {
var s = String(value)
var ok = false
try { if (typeof set === "function") { set(key, s); ok = true } } catch (e) {}
try { if (typeof bucketSet === "function") { bucketSet(BUCKET, 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 getStore() {
var raw = cfgGet(STORE_KEY, "{}")
var obj = safeJsonParse(raw, {})
if (!isObject(obj)) return {}
var clean = {}
for (var uid in obj) if (obj.hasOwnProperty(uid) && obj[uid]) clean[String(uid)] = String(obj[uid])
return clean
}
function setStore(obj) { return cfgSet(STORE_KEY, JSON.stringify(obj || {})) }
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 || "")
try { if (typeof Buffer !== "undefined") return Buffer.from(s, "base64").toString("utf8") } catch (e) {}
try { return decodeURIComponent(escape(atob(s))) } catch (e) { return "" }
}
function uidFromQDHeader(qdheader) {
var decoded = b64decode(qdheader)
var uid = decoded ? trim(decoded.split("|")[0] || "") : ""
return /^\d+$/.test(uid) ? uid : ""
}
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)
if (jar.userid) return String(jar.userid)
return ""
}
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.2.0",
"提交CK:起点ck <QX抓到的cookie>",
"签到全部:起点签到",
"签到单号:起点签到 <uid>",
"查询CK:起点ck 查询",
"导出CK:起点ck 导出(会输出完整 Cookie,仅管理员自用)",
"删除CK:起点ck 删除 <uid|all>",
"配置签名网关:Autman 参数 otto.qdreader_sign_api,默认 " + DEFAULT_SIGN_API,
"定时:每日 07:15 自动执行;QX 只负责抓 cookie。"
].join("\n")
}
function stripCommand(text) {
var s = String(text || "")
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 (/^qdreader\s+sign(?:\s|$)/i.test(text)) return "sign"
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 || ""
return { jar: jar, cookie: cookieToString(jar), qdh: jar.QDHeader || jar.qdh || "", qdheader: jar.QDHeader || jar.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 (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" }, 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 doCheckin(uid, cookie) {
var res = qidianRequest(CHECKIN_PATH, "POST", {}, cookie)
var ok = resultOk(res)
var msg = resultMsg(res) || (ok ? "签到请求成功" : ("签到失败" + (resultCode(res) !== undefined ? " code=" + resultCode(res) : "")))
return { uid: uid, ok: ok, msg: msg, raw: res }
}
function signAll(targetUid) {
var store = getStore()
var uids = Object.keys(store).sort()
if (targetUid) uids = uids.filter(function (u) { return u === targetUid })
if (!uids.length) return targetUid ? ("未找到 uid: " + targetUid) : "还没有保存起点读书CK"
var okCount = 0
var failCount = 0
var lines = ["起点读书签到结果:"]
for (var i = 0; i < uids.length; i++) {
var uid = uids[i]
try {
var r = doCheckin(uid, store[uid])
if (r.ok) okCount++; else failCount++
lines.push((r.ok ? "✅ " : "❌ ") + maskUid(uid) + "" + r.msg)
} catch (e) {
failCount++
lines.push("❌ " + maskUid(uid) + "" + (e && e.message ? e.message : String(e)))
}
}
lines.push("汇总:成功 " + okCount + " / 失败 " + failCount + " / 总计 " + uids.length)
return lines.join("\n")
}
function importJson(arg, store) {
var obj = safeJsonParse(arg, null)
if (!isObject(obj)) return "JSON格式错误,示例:起点ck json {\"uid\":\"cookie\"}"
var count = 0
for (var uid in obj) {
if (!obj.hasOwnProperty(uid) || !obj[uid]) continue
var cookie = String(obj[uid])
var realUid = extractUid(cookie) || uid
store[String(realUid)] = cookie
count++
}
setStore(store)
return "起点读书CK JSON已导入/更新 " + count + " 个账号,当前共 " + Object.keys(store).length + " 个账号\n发送 起点签到 可执行签到"
}
function handleDelete(arg, store) {
var del = arg.match(/^(删除|delete|del|rm)\s+(\S+)/i)
if (!del) return null
var duid = del[2]
if (/^(all|全部|所有)$/i.test(duid)) {
var n = Object.keys(store).length
setStore({})
return "已删除全部起点读书CK,共 " + n + " 个账号"
}
var existed = !!store[duid]
delete store[duid]
setStore(store)
return existed ? ("已删除 uid: " + duid) : ("未找到 uid: " + duid)
}
function handleSaveCookie(arg, store) {
var cookie = arg
if (cookie.indexOf("=") < 0 || cookie.indexOf(";") < 0) return "未识别到 Cookie。请发送:起点ck <QX抓到的完整cookie>"
var uid = extractUid(cookie)
if (!uid) return "无法从 Cookie 识别 uid。请确认 QX 抓的是起点读书福利中心 getlogininfo 请求,Cookie 内应包含 QDHeader 或 uid。"
var isNew = !store[uid]
var changed = store[uid] !== cookie
store[uid] = cookie
setStore(store)
return ["起点读书CK" + (isNew ? "新增" : (changed ? "更新" : "未变化")) + "成功", "uid: " + uid, "账号数: " + Object.keys(store).length, "cookie: " + maskCookie(cookie), "签到:发送 起点签到"].join("\n")
}
function main() {
var text = contentText()
var isCronRun = !text
var cmd = isCronRun ? "sign" : commandName(text)
var arg = isCronRun ? "" : trim(stripCommand(text).replace(/^sign\s*/i, ""))
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 = trim(arg.replace(/^(签到|sign|run)\s*/i, ""))
var out = signAll(target)
if (!(isCronRun && getBool(CRON_SILENT_KEY, false) && out === "还没有保存起点读书CK")) reply(out)
return
}
if (/^(查询|状态|status|list)$/i.test(arg)) {
var uids = Object.keys(store).sort()
reply(uids.length ? ("起点读书CK:共 " + uids.length + " 个账号\n" + uids.map(maskUid).join("\n") + "\n发送 起点签到 可执行签到") : "还没有保存起点读书CK")
return
}
if (/^(导出|export)$/i.test(arg)) { reply("QDREADER_COOKIE=" + JSON.stringify(store)); return }
var delMsg = handleDelete(arg, store)
if (delMsg !== null) { reply(delMsg); return }
var jsonMatch = arg.match(/^json\s+([\s\S]+)$/i)
if (jsonMatch) { reply(importJson(jsonMatch[1], store)); return }
reply(handleSaveCookie(arg, store))
}
main()