From d09073c33cd70f00e959cc88f1b813acc979fa0f Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 20 May 2026 12:16:36 +0800 Subject: [PATCH] Revert "fix(qdreader): split archive and sign cron plugins" This reverts commit 2413bd5b124568990f939d0c20599e85682c4e12. --- README.md | 9 +- docs/autman-plugin-rules.md | 2 +- js/qdreader_archive_autman.js | 814 -------------------- js/qdreader_sign_autman.js | 13 +- plugins/qdreader/README.md | 16 +- plugins/qdreader/qdreader_archive_autman.js | 814 -------------------- plugins/qdreader/qdreader_sign_autman.js | 13 +- tests/qdreader_plugin.test.mjs | 10 - 8 files changed, 23 insertions(+), 1668 deletions(-) delete mode 100644 js/qdreader_archive_autman.js delete mode 100644 plugins/qdreader/qdreader_archive_autman.js diff --git a/README.md b/README.md index e8221fb..33d6912 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,9 @@ GET /api/sms/stats ### 起点读书 QDReader 签到插件 - 正式源码:`plugins/qdreader/qdreader_sign_autman.js` -- 归档定时源码:`plugins/qdreader/qdreader_archive_autman.js` -- 实机 JS 副本:`js/qdreader_sign_autman.js` / `js/qdreader_archive_autman.js` -- 定时运行:签到 `cron: 1 7 * * *`;归档 `cron: 1 0 * * *`,拆成两个简单独立 cron,避免 Autman 对 `0,7` 这类逗号 hour 表达式兼容不一致 -- 手动运行:`启点签到` / `启点签到 ` +- 实机 JS 副本:`js/qdreader_sign_autman.js` +- 定时运行:`cron: 15 7 * * *`,每日 07:15 自动签到 +- 手动运行:`起点签到` / `起点签到 ` - Cookie 维护:`起点ck ` / `起点ck 查询` / `起点ck 导出` / `起点ck 删除 ` - v2.2.0:增强 Autman 存储/消息/请求兼容,增加重试、签名返回兼容、定时静默开关、输出脱敏和 Node VM 测试。 @@ -89,3 +88,5 @@ node tests/qdreader_plugin.test.mjs - [Autman 插件开发规则与仓库约定](docs/autman-plugin-rules.md) - [JS 微服务路由插件模板](templates/js-router-plugin-template.js) - [Python middleware 插件模板](templates/python-middleware-plugin-template.py) + + diff --git a/docs/autman-plugin-rules.md b/docs/autman-plugin-rules.md index 623f11e..9c775c0 100644 --- a/docs/autman-plugin-rules.md +++ b/docs/autman-plugin-rules.md @@ -619,7 +619,7 @@ QDReader 这次踩坑说明:账号 ID 识别成功,不代表 Autman 会话 - `xxx查询` 的数据来源必须写清楚:如果没有实时接口,就命名为“最近签到记录/最后执行日志”,读取 `last_run_json` 和 `last_result_json`,不要让用户误以为是实时查询。 - 执行动作要同时写批次日志和单账号日志;成功、业务失败、请求异常都要落日志,异常不能只显示在当次回复里。 -- 如果插件需要按天判断执行情况,不要把归档和运行任务放在同一个业务入口。实测排查定时未触发时,优先避免逗号 hour 表达式兼容风险,拆成两个简单独立 cron/插件:0 点归档插件只归档上一份批次日志到 `archive_json` 并清空 `last_run_json` / `last_result_json`,7 点签到插件只执行签到/业务动作并写 `last_run_json` / `last_result_json`。只有确认当前 Autman 运行时稳定支持多 hour 表达式后,才考虑合并为一个 cron 并按小时分流。查询菜单优先展示当日/当前批次;当日日志为空时回退最近归档,按归档结果反馈成功/失败。 +- 如果插件需要按天判断执行情况,不要把归档和运行任务放在同一个 cron 入口。优先用一个插件文件;如果两个动作只相差小时、分钟相同,可合并为一个 cron(如 QDReader 用 `1 0,7 * * *`),入口按小时分流:0 点分支只归档上一份批次日志到 `archive_json` 并清空 `last_run_json` / `last_result_json`,7 点分支只执行签到/业务动作并写 `last_run_json` / `last_result_json`。如果分钟不同或无法安全按小时分流,再使用多个独立 cron;只有在当前 Autman 运行环境确认不可靠支持单插件多 `[cron]` 时,才退而拆成两个插件文件。查询菜单优先展示当日/当前批次;当日日志为空时回退最近归档,按归档结果反馈成功/失败。 - 普通回复永远不要输出完整 Cookie;只展示脱敏 UID、备注、Cookie key 摘要或长度。 - 高风险的导出/批量导入功能默认不做,除非 BOSS 明确需要。 - Node VM 测试应模拟:`sender.reply`、`sender.listen`、`sender.getUserID`、`get/set`、`bucketGet/bucketSet`、`request`。 diff --git a/js/qdreader_archive_autman.js b/js/qdreader_archive_autman.js deleted file mode 100644 index 13618cd..0000000 --- a/js/qdreader_archive_autman.js +++ /dev/null @@ -1,814 +0,0 @@ -//[open_source: true] -//[create_at: 2026-05-17 03:00:00] -//[title: 启点读书日志归档] -//[author: Hermes] -//[service: BOSS] -//[class: 工具类] -//[version: 2.5.6] -//[platform: web,qq,wx,tg,tb,fs,we] -//[public: false] -//[price: 0] -//[disable: false] -//[description: 启点读书(QDReader) 日志归档插件:仅用于独立定时 `1 0 * * *`,每日 00:01 归档上一批签到日志并清空当日日志;不暴露手动规则,避免与签到插件共用逗号 hour cron 导致 Autman 定时兼容问题。] -//[admin: true] -//[priority: 600] -//[cron: 1 0 * * *] - - -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.6", - "【一级菜单】", - "账号管理:启点账号", - "执行查询:启点查询", - "【快速通道】", - "快速提交CK:启点ck ", - "直接签到:启点签到", - "指定签到:启点签到 ", - "【配置】", - "签名网关: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 - // Compatibility fallback: the sign plugin is scheduled at 07:01 only. - // If Autman manually/debug-runs this file around 00:xx, keep archive-only behavior. - // The normal midnight archive is handled by qdreader_archive_autman.js with cron 1 0 * * *. - 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 抓到的完整 Cookie(q:退出)") - var ck = listenInput(60000) - if (!ck || /^(q|退出|取消)$/i.test(ck)) return reply("已退出") - var save = handleSaveCookie(ck, store, bind) - var remarkMsg = (save.ok && save.uids.length === 1) ? maybePromptRemark(save.uids[0], store) : "" - return reply(save.text + remarkMsg) - } - 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 ", uids: [] } - var me = currentOwnerId() - if (!me) return { ok: false, text: "无法识别当前用户,已拒绝保存CK,避免错误绑定。请确认插件在聊天会话中触发;诊断: " + currentUserDebug(), uids: [] } - var lines = [] - var savedUids = [] - for (var i = 0; i < entries.length; i++) { - var cookie = entries[i].cookie - if (cookie.indexOf("=") < 0 || cookie.indexOf(";") < 0) return { ok: false, text: "未识别到 Cookie。请发送:启点ck {\"uid\":\"cookie\"} 或 启点ck ", uids: savedUids } - var parsedUid = extractUid(cookie) - var uid = entries[i].uid || parsedUid - if (!uid) return { ok: false, text: "无法从 Cookie 识别 uid。请确认 QX 抓的是启点读书福利中心 getlogininfo 请求,Cookie 内应包含 QDHeader 或 uid。", uids: savedUids } - var isNew = !store[uid] - var oldOwner = bind[uid] || "" - if (!store[uid] && oldOwner && oldOwner !== me) { - delete bind[uid] - oldOwner = "" - lines.push("检测到 uid: " + uid + " 只有旧绑定没有CK,已自动清理残留绑定") - } - if (store[uid] && oldOwner && oldOwner !== me && canRepairBinding(oldOwner, me)) { - lines.push("检测到 uid: " + uid + " 旧绑定来自历史兜底标识 " + maskUid(oldOwner) + ",已修复为当前用户") - oldOwner = "" - } - if (store[uid] && oldOwner && me && oldOwner !== me) { - lines.push("无权更新 uid: " + uid + "(该账号已绑定其他用户)") - continue - } - var changed = store[uid] !== cookie - store[uid] = cookie - bind[uid] = me - savedUids.push(uid) - lines.push("启点读书CK" + (isNew ? "新增" : (changed ? "更新" : "未变化")) + "成功") - lines.push("uid: " + uid) - if (entries.length > 1 && parsedUid && parsedUid !== uid) lines.push("提示: JSON uid 与 Cookie 解析 uid 不一致,已按 JSON uid 保存") - } - setStore(store); setBindStore(bind) - lines.push(me ? ("绑定用户: " + maskUid(me)) : "绑定用户: 未识别") - lines.push("账号数: " + visibleUids(store, bind, false).length) - lines.push("cookie: " + maskCookie(entries[0].cookie)) - lines.push("签到:发送 启点签到") - return { ok: savedUids.length > 0, text: lines.join("\n"), uids: savedUids } -} -function main() { - var text = contentText() - var isCronRun = !text - var cmd = isCronRun ? cronMode() : commandName(text) - var arg = isCronRun ? "" : trim(stripCommand(text).replace(/^(sign|archive)\s*/i, "")) - if (!arg && cmd !== "sign" && cmd !== "archive" && cmd !== "account" && cmd !== "status") { reply(usage()); return } - if (/^(帮助|help|h)$/i.test(arg)) { reply(usage()); return } - - var store = getStore() - var bind = getBindStore() - if (cmd === "archive" || /^(归档|日志归档|archive)(?:\s+|$)/i.test(arg)) { - reply(formatArchiveReply(archiveRunLog(isCronRun ? "midnight-archive" : "manual-archive"))) - return - } - if (cmd === "sign" || /^(签到|sign|run)(?:\s+|$)/i.test(arg)) { - var target = trim(arg.replace(/^(签到|sign|run)\s*/i, "")) - var out = signAll(target, isCronRun, isCronRun ? "cron" : "manual") - if (!(isCronRun && getBool(CRON_SILENT_KEY, false) && out === "还没有保存启点读书CK")) reply(out) - return - } - if (cmd === "account") { accountMenu(store, bind); return } - if (cmd === "status") { queryMenu(); return } - if (cmd === "ck" && /^(查询|状态|status|list|账号|管理)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;快速提交CK:启点ck "); return } - var delMsg = handleDelete(arg, store, bind) - if (delMsg !== null) { reply(delMsg); return } - if (cmd === "ck") { - var save = handleSaveCookie(arg, store, bind) - if (save.ok && save.uids.length === 1) save.text += maybePromptRemark(save.uids[0], store) - reply(save.text) - return - } - var fallbackSave = handleSaveCookie(arg, store, bind) - reply(fallbackSave.text) -} -main() diff --git a/js/qdreader_sign_autman.js b/js/qdreader_sign_autman.js index edf1d1c..2a72750 100644 --- a/js/qdreader_sign_autman.js +++ b/js/qdreader_sign_autman.js @@ -4,17 +4,17 @@ //[author: Hermes] //[service: BOSS] //[class: 工具类] -//[version: 2.5.6] +//[version: 2.5.5] //[platform: web,qq,wx,tg,tb,fs,we] //[public: false] //[price: 0] //[disable: false] -//[description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”;定时签到使用独立 cron `1 7 * * *`;日志归档由 `qdreader_archive_autman.js` 独立 cron `1 0 * * *` 执行,避免 Autman 对逗号 hour cron 兼容不一致导致定时不触发;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。] +//[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 7 * * *] +//[cron: 1 0,7 * * *] var BUCKET = "hermes_qidian" @@ -260,7 +260,7 @@ function contentText() { } function usage() { return [ - "启点读书签到插件 v2.5.6", + "启点读书签到插件 v2.5.5", "【一级菜单】", "账号管理:启点账号", "执行查询:启点查询", @@ -454,9 +454,8 @@ function latestArchiveForVisible(uids) { function cronMode() { var d = new Date() var h = d.getHours ? d.getHours() : 7 - // Compatibility fallback: the sign plugin is scheduled at 07:01 only. - // If Autman manually/debug-runs this file around 00:xx, keep archive-only behavior. - // The normal midnight archive is handled by qdreader_archive_autman.js with cron 1 0 * * *. + // Single cron: 1 0,7 * * * + // 00:01 system run archives only; 07:01 system run signs only. if (h === 0) return "archive" return "sign" } diff --git a/plugins/qdreader/README.md b/plugins/qdreader/README.md index 9d39e3a..4095047 100644 --- a/plugins/qdreader/README.md +++ b/plugins/qdreader/README.md @@ -1,21 +1,19 @@ # 启点读书 QDReader 签到 Autman 插件 -QDReader/起点读书 Autman 签到插件。用户侧触发词统一使用“启点”,避免平台关键词麻烦;QX 只负责抓取 Cookie。账号管理、执行查询和手动签到在主签到插件内完成;日志归档拆成独立 Autman 插件。定时使用两个简单 cron:主签到插件 `1 7 * * *` 每日 07:01 执行签到,归档插件 `1 0 * * *` 每日 00:01 只归档并清空日志。这样避免 Autman 对 `0,7` 逗号 hour 表达式兼容不一致时导致定时不触发。 +QDReader/起点读书 Autman 签到插件。用户侧触发词统一使用“启点”,避免平台关键词麻烦;QX 只负责抓取 Cookie。账号管理、执行查询、签到和日志归档都在同一个 Autman 插件内完成;业务上使用一个 cron 表达式 `1 0,7 * * *`:每日 00:01 只归档清日志,每日 07:01 只执行签到。 ## 文件位置 正式插件文件: ```text -plugins/qdreader/qdreader_sign_autman.js # 主插件:账号管理、查询、手动签到;cron 07:01 签到 -plugins/qdreader/qdreader_archive_autman.js # 归档插件:无手动 rule;cron 00:01 归档清日志 +plugins/qdreader/qdreader_sign_autman.js # 单插件单 cron:00:01 归档清日志;07:01 运行签到 ``` 实机 JS 导入副本: ```text js/qdreader_sign_autman.js -js/qdreader_archive_autman.js ``` Raw 链接: @@ -23,8 +21,6 @@ Raw 链接: ```text https://gitea.chickliu.fun/Hermes/autman-plugins/raw/branch/main/plugins/qdreader/qdreader_sign_autman.js https://gitea.chickliu.fun/Hermes/autman-plugins/raw/branch/main/js/qdreader_sign_autman.js -https://gitea.chickliu.fun/Hermes/autman-plugins/raw/branch/main/plugins/qdreader/qdreader_archive_autman.js -https://gitea.chickliu.fun/Hermes/autman-plugins/raw/branch/main/js/qdreader_archive_autman.js ``` ## 当前交互设计 @@ -136,9 +132,9 @@ set(key, value) 3. `POST https://h5.if.qidian.com/argus/api/v3/checkin/checkin` 完成签到。 4. 写入单账号当日结果 `qdreader_last_result_json`,包括 `ok/msg/time/source/runId`。 5. 写入当日批次日志 `qdreader_last_run_json`,包括触发来源、开始/结束时间、成功/失败/总数和每账号结果;即使账号执行异常也会记录失败日志。 -6. 主签到插件 cron:`1 7 * * *`,每日 07:01 只执行签到并反馈结果,不做归档。 -7. 归档插件 cron:`1 0 * * *`,每日 00:01 只做日志归档:把上一份 `qdreader_last_run_json` 按完成日期写入 `qdreader_archive_json`,再清空 `qdreader_last_result_json` / `qdreader_last_run_json`;不请求签名网关、不执行签到。 -8. 两个定时拆成独立插件,避免 Autman 对逗号 hour 表达式兼容不一致。 +6. 插件内只有一个 cron:`1 0,7 * * *`。 +7. 每日 00:01 触发时只做日志归档;系统触发按小时分流,0 点分支归档:把上一份 `qdreader_last_run_json` 按完成日期写入 `qdreader_archive_json`,再清空 `qdreader_last_result_json` / `qdreader_last_run_json`;不请求签名网关、不执行签到。 +8. 每日 07:01 触发时只执行签到并反馈结果,不做归档。 9. 汇总每个账号签到结果。 ## 查询数据来源 @@ -157,8 +153,6 @@ qdreader_archive_json # 每日零点归档后的历史批次 ```bash node -c plugins/qdreader/qdreader_sign_autman.js -node -c plugins/qdreader/qdreader_archive_autman.js node -c js/qdreader_sign_autman.js -node -c js/qdreader_archive_autman.js node tests/qdreader_plugin.test.mjs ``` diff --git a/plugins/qdreader/qdreader_archive_autman.js b/plugins/qdreader/qdreader_archive_autman.js deleted file mode 100644 index 13618cd..0000000 --- a/plugins/qdreader/qdreader_archive_autman.js +++ /dev/null @@ -1,814 +0,0 @@ -//[open_source: true] -//[create_at: 2026-05-17 03:00:00] -//[title: 启点读书日志归档] -//[author: Hermes] -//[service: BOSS] -//[class: 工具类] -//[version: 2.5.6] -//[platform: web,qq,wx,tg,tb,fs,we] -//[public: false] -//[price: 0] -//[disable: false] -//[description: 启点读书(QDReader) 日志归档插件:仅用于独立定时 `1 0 * * *`,每日 00:01 归档上一批签到日志并清空当日日志;不暴露手动规则,避免与签到插件共用逗号 hour cron 导致 Autman 定时兼容问题。] -//[admin: true] -//[priority: 600] -//[cron: 1 0 * * *] - - -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.6", - "【一级菜单】", - "账号管理:启点账号", - "执行查询:启点查询", - "【快速通道】", - "快速提交CK:启点ck ", - "直接签到:启点签到", - "指定签到:启点签到 ", - "【配置】", - "签名网关: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 - // Compatibility fallback: the sign plugin is scheduled at 07:01 only. - // If Autman manually/debug-runs this file around 00:xx, keep archive-only behavior. - // The normal midnight archive is handled by qdreader_archive_autman.js with cron 1 0 * * *. - 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 抓到的完整 Cookie(q:退出)") - var ck = listenInput(60000) - if (!ck || /^(q|退出|取消)$/i.test(ck)) return reply("已退出") - var save = handleSaveCookie(ck, store, bind) - var remarkMsg = (save.ok && save.uids.length === 1) ? maybePromptRemark(save.uids[0], store) : "" - return reply(save.text + remarkMsg) - } - 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 ", uids: [] } - var me = currentOwnerId() - if (!me) return { ok: false, text: "无法识别当前用户,已拒绝保存CK,避免错误绑定。请确认插件在聊天会话中触发;诊断: " + currentUserDebug(), uids: [] } - var lines = [] - var savedUids = [] - for (var i = 0; i < entries.length; i++) { - var cookie = entries[i].cookie - if (cookie.indexOf("=") < 0 || cookie.indexOf(";") < 0) return { ok: false, text: "未识别到 Cookie。请发送:启点ck {\"uid\":\"cookie\"} 或 启点ck ", uids: savedUids } - var parsedUid = extractUid(cookie) - var uid = entries[i].uid || parsedUid - if (!uid) return { ok: false, text: "无法从 Cookie 识别 uid。请确认 QX 抓的是启点读书福利中心 getlogininfo 请求,Cookie 内应包含 QDHeader 或 uid。", uids: savedUids } - var isNew = !store[uid] - var oldOwner = bind[uid] || "" - if (!store[uid] && oldOwner && oldOwner !== me) { - delete bind[uid] - oldOwner = "" - lines.push("检测到 uid: " + uid + " 只有旧绑定没有CK,已自动清理残留绑定") - } - if (store[uid] && oldOwner && oldOwner !== me && canRepairBinding(oldOwner, me)) { - lines.push("检测到 uid: " + uid + " 旧绑定来自历史兜底标识 " + maskUid(oldOwner) + ",已修复为当前用户") - oldOwner = "" - } - if (store[uid] && oldOwner && me && oldOwner !== me) { - lines.push("无权更新 uid: " + uid + "(该账号已绑定其他用户)") - continue - } - var changed = store[uid] !== cookie - store[uid] = cookie - bind[uid] = me - savedUids.push(uid) - lines.push("启点读书CK" + (isNew ? "新增" : (changed ? "更新" : "未变化")) + "成功") - lines.push("uid: " + uid) - if (entries.length > 1 && parsedUid && parsedUid !== uid) lines.push("提示: JSON uid 与 Cookie 解析 uid 不一致,已按 JSON uid 保存") - } - setStore(store); setBindStore(bind) - lines.push(me ? ("绑定用户: " + maskUid(me)) : "绑定用户: 未识别") - lines.push("账号数: " + visibleUids(store, bind, false).length) - lines.push("cookie: " + maskCookie(entries[0].cookie)) - lines.push("签到:发送 启点签到") - return { ok: savedUids.length > 0, text: lines.join("\n"), uids: savedUids } -} -function main() { - var text = contentText() - var isCronRun = !text - var cmd = isCronRun ? cronMode() : commandName(text) - var arg = isCronRun ? "" : trim(stripCommand(text).replace(/^(sign|archive)\s*/i, "")) - if (!arg && cmd !== "sign" && cmd !== "archive" && cmd !== "account" && cmd !== "status") { reply(usage()); return } - if (/^(帮助|help|h)$/i.test(arg)) { reply(usage()); return } - - var store = getStore() - var bind = getBindStore() - if (cmd === "archive" || /^(归档|日志归档|archive)(?:\s+|$)/i.test(arg)) { - reply(formatArchiveReply(archiveRunLog(isCronRun ? "midnight-archive" : "manual-archive"))) - return - } - if (cmd === "sign" || /^(签到|sign|run)(?:\s+|$)/i.test(arg)) { - var target = trim(arg.replace(/^(签到|sign|run)\s*/i, "")) - var out = signAll(target, isCronRun, isCronRun ? "cron" : "manual") - if (!(isCronRun && getBool(CRON_SILENT_KEY, false) && out === "还没有保存启点读书CK")) reply(out) - return - } - if (cmd === "account") { accountMenu(store, bind); return } - if (cmd === "status") { queryMenu(); return } - if (cmd === "ck" && /^(查询|状态|status|list|账号|管理)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;快速提交CK:启点ck "); return } - var delMsg = handleDelete(arg, store, bind) - if (delMsg !== null) { reply(delMsg); return } - if (cmd === "ck") { - var save = handleSaveCookie(arg, store, bind) - if (save.ok && save.uids.length === 1) save.text += maybePromptRemark(save.uids[0], store) - reply(save.text) - return - } - var fallbackSave = handleSaveCookie(arg, store, bind) - reply(fallbackSave.text) -} -main() diff --git a/plugins/qdreader/qdreader_sign_autman.js b/plugins/qdreader/qdreader_sign_autman.js index edf1d1c..2a72750 100644 --- a/plugins/qdreader/qdreader_sign_autman.js +++ b/plugins/qdreader/qdreader_sign_autman.js @@ -4,17 +4,17 @@ //[author: Hermes] //[service: BOSS] //[class: 工具类] -//[version: 2.5.6] +//[version: 2.5.5] //[platform: web,qq,wx,tg,tb,fs,we] //[public: false] //[price: 0] //[disable: false] -//[description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”;定时签到使用独立 cron `1 7 * * *`;日志归档由 `qdreader_archive_autman.js` 独立 cron `1 0 * * *` 执行,避免 Autman 对逗号 hour cron 兼容不一致导致定时不触发;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。] +//[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 7 * * *] +//[cron: 1 0,7 * * *] var BUCKET = "hermes_qidian" @@ -260,7 +260,7 @@ function contentText() { } function usage() { return [ - "启点读书签到插件 v2.5.6", + "启点读书签到插件 v2.5.5", "【一级菜单】", "账号管理:启点账号", "执行查询:启点查询", @@ -454,9 +454,8 @@ function latestArchiveForVisible(uids) { function cronMode() { var d = new Date() var h = d.getHours ? d.getHours() : 7 - // Compatibility fallback: the sign plugin is scheduled at 07:01 only. - // If Autman manually/debug-runs this file around 00:xx, keep archive-only behavior. - // The normal midnight archive is handled by qdreader_archive_autman.js with cron 1 0 * * *. + // Single cron: 1 0,7 * * * + // 00:01 system run archives only; 07:01 system run signs only. if (h === 0) return "archive" return "sign" } diff --git a/tests/qdreader_plugin.test.mjs b/tests/qdreader_plugin.test.mjs index e1a3c53..79b1d4d 100644 --- a/tests/qdreader_plugin.test.mjs +++ b/tests/qdreader_plugin.test.mjs @@ -557,13 +557,3 @@ test('user cannot overwrite another user bound cookie', () => { assert.equal(JSON.parse(r.store.qdreader_cookie_json)['12345'], cookie); assert.equal(JSON.parse(r.store.qdreader_user_bind_json)['12345'], '987654321'); }); - -test('qdreader cron is split into simple independent schedules for Autman compatibility', () => { - const signCode = fs.readFileSync(new URL('../plugins/qdreader/qdreader_sign_autman.js', import.meta.url), 'utf8'); - const archiveCode = fs.readFileSync(new URL('../plugins/qdreader/qdreader_archive_autman.js', import.meta.url), 'utf8'); - assert.match(signCode, /\/\/\[cron: 1 7 \* \* \*\]/); - assert.doesNotMatch(signCode, /\/\/\[cron: 1 0,7 \* \* \*\]/); - assert.match(archiveCode, /\/\/\[cron: 1 0 \* \* \*\]/); - assert.doesNotMatch(archiveCode, /\/\/\[rule:/); - assert.match(archiveCode, /日志归档插件/); -});