diff --git a/js/qdreader_sign_autman.js b/js/qdreader_sign_autman.js index 8b31c1f..5236f19 100644 --- a/js/qdreader_sign_autman.js +++ b/js/qdreader_sign_autman.js @@ -9,19 +9,18 @@ //[public: false] //[price: 0] //[disable: false] -//[description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”和每日 07:15 定时签到;带多环境存储兼容、请求重试、签名响应兼容、结果摘要和安全脱敏。] +//[description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”和每日 07:15 定时签到;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。] //[rule: ^启点(账号|查询|ck|CK|cookie|Cookie|签到|读书)(.*)$] //[rule: ^qdreader(.*)$] //[admin: true] //[priority: 600] //[cron: 15 7 * * *] -//[param: {"required":false,"key":"hermes_qidian.qdreader_cookie_json","bool":false,"placeholder":"{\"uid\":\"cookie\"}","name":"QDReader Cookie JSON","desc":"机器人交互维护,JSON Map 格式;兼容读取旧 otto bucket"}] -//[param: {"required":false,"key":"hermes_qidian.qdreader_sign_api","bool":false,"placeholder":"https://api.120399.xyz/qdreader","name":"QDReader 签名网关","desc":"默认 https://api.120399.xyz/qdreader;插件会自动追加 /sign,可替换为兼容 /sign 的网关"}] +//[param: {"required":false,"key":"hermes_qidian.qdreader_cookie_json","bool":false,"placeholder":"{\"uid\":\"cookie\"}","name":"QDReader Cookie JSON","desc":"机器人交互维护,JSON Map 格式"}] +//[param: {"required":false,"key":"hermes_qidian.qdreader_sign_api","bool":false,"placeholder":"https://api.120399.xyz/qdreader","name":"QDReader 签名网关","desc":"默认 https://api.120399.xyz/qdreader;插件会自动追加 /sign"}] //[param: {"required":false,"key":"hermes_qidian.qdreader_retry_count","bool":false,"placeholder":"1","name":"请求失败重试次数","desc":"默认 1,最大 3,仅对网络异常/非业务异常重试"}] //[param: {"required":false,"key":"hermes_qidian.qdreader_cron_silent","bool":true,"placeholder":"false","name":"定时无账号时静默","desc":"true 时定时触发且没有账号不回复"}] var BUCKET = "hermes_qidian" -var LEGACY_BUCKET = "otto" var STORE_KEY = "qdreader_cookie_json" var BIND_KEY = "qdreader_user_bind_json" var REMARK_KEY = "qdreader_remark_json" @@ -44,7 +43,6 @@ 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) {} - try { if (typeof bucketGet === "function") { var lv = bucketGet(LEGACY_BUCKET, key); if (lv !== undefined && lv !== null && String(lv) !== "") return lv } } catch (e) {} return def } function cfgSet(key, value) { @@ -283,44 +281,13 @@ function normalizeSignHeaders(signed) { if (!ok || !isObject(headers)) throw new Error("签名失败:" + JSON.stringify(signed).slice(0, 500)) return headers } -function signBaseCandidates(rawBase) { - rawBase = String(rawBase || DEFAULT_SIGN_API).replace(/\/+$/, "") - var out = [rawBase] - if (/^https?:\/\/api\.120399\.xyz$/i.test(rawBase)) out.push(rawBase + "/qdreader") - if (/^https?:\/\/api\.120399\.xyz\/QDReader$/i.test(rawBase)) out.push(rawBase.replace(/\/QDReader$/i, "/qdreader")) - var seen = {} - return out.filter(function (x) { if (seen[x]) return false; seen[x] = true; return true }) -} -function signErrorRetriable(e) { - var msg = e && e.message ? e.message : String(e || "") - return /签名网关无返回|Cannot\s+POST|404|Not\s+Found/i.test(msg) -} -function isLikelyEmptyHttpResponse(x) { - return !x || (typeof x === "string" && !trim(x)) -} -function requestSignHeaders(rawBase, payload) { - var bases = signBaseCandidates(rawBase) - var lastErr = null - for (var i = 0; i < bases.length; i++) { - try { - var signed = postJson(bases[i] + "/sign", payload) - if (i + 1 < bases.length && isLikelyEmptyHttpResponse(signed)) throw new Error("签名网关无返回") - return normalizeSignHeaders(signed) - } catch (e) { - lastErr = e - debug("QDReader sign gateway " + bases[i] + "/sign failed: " + (e && e.message ? e.message : String(e))) - if (!signErrorRetriable(e)) break - } - } - throw lastErr || new Error("签名网关无返回") -} 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 = requestSignHeaders(signApi, payload) + 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 } diff --git a/plugins/qdreader/README.md b/plugins/qdreader/README.md index eda66eb..b850f45 100644 --- a/plugins/qdreader/README.md +++ b/plugins/qdreader/README.md @@ -98,14 +98,12 @@ qdreader_retry_count # 请求失败重试次数 qdreader_cron_silent # 定时无账号静默 ``` -读取兼容顺序: +读取顺序: ```text bucketGet("hermes_qidian", key) ↓ fallback get(key) - ↓ fallback -bucketGet("otto", key) # 旧数据兼容 ``` 写入顺序: @@ -123,7 +121,7 @@ set(key, value) - 再读取 `uid/userid/userId/UserId`; - 最后才回退 `ywguid/gwguid/guid`,与 QX 抓包脚本保持一致; - 同时提取 `qdh/QDHeader/guid/qimei/qimei36` 供签名网关使用。 -2. `POST /sign` 获取请求签名头。默认签名网关基址是 `https://api.120399.xyz/qdreader`;若旧配置仍写成 `https://api.120399.xyz`,插件会在根 `/sign` 无返回/404 时自动回退到 `/qdreader/sign`。 +2. `POST /sign` 获取请求签名头。默认签名网关基址是 `https://api.120399.xyz/qdreader`。 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`,包括触发来源、开始/结束时间、成功/失败/总数和每账号结果;即使账号执行异常也会记录失败日志。 diff --git a/plugins/qdreader/qdreader_sign_autman.js b/plugins/qdreader/qdreader_sign_autman.js index 8b31c1f..5236f19 100644 --- a/plugins/qdreader/qdreader_sign_autman.js +++ b/plugins/qdreader/qdreader_sign_autman.js @@ -9,19 +9,18 @@ //[public: false] //[price: 0] //[disable: false] -//[description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”和每日 07:15 定时签到;带多环境存储兼容、请求重试、签名响应兼容、结果摘要和安全脱敏。] +//[description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”和每日 07:15 定时签到;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。] //[rule: ^启点(账号|查询|ck|CK|cookie|Cookie|签到|读书)(.*)$] //[rule: ^qdreader(.*)$] //[admin: true] //[priority: 600] //[cron: 15 7 * * *] -//[param: {"required":false,"key":"hermes_qidian.qdreader_cookie_json","bool":false,"placeholder":"{\"uid\":\"cookie\"}","name":"QDReader Cookie JSON","desc":"机器人交互维护,JSON Map 格式;兼容读取旧 otto bucket"}] -//[param: {"required":false,"key":"hermes_qidian.qdreader_sign_api","bool":false,"placeholder":"https://api.120399.xyz/qdreader","name":"QDReader 签名网关","desc":"默认 https://api.120399.xyz/qdreader;插件会自动追加 /sign,可替换为兼容 /sign 的网关"}] +//[param: {"required":false,"key":"hermes_qidian.qdreader_cookie_json","bool":false,"placeholder":"{\"uid\":\"cookie\"}","name":"QDReader Cookie JSON","desc":"机器人交互维护,JSON Map 格式"}] +//[param: {"required":false,"key":"hermes_qidian.qdreader_sign_api","bool":false,"placeholder":"https://api.120399.xyz/qdreader","name":"QDReader 签名网关","desc":"默认 https://api.120399.xyz/qdreader;插件会自动追加 /sign"}] //[param: {"required":false,"key":"hermes_qidian.qdreader_retry_count","bool":false,"placeholder":"1","name":"请求失败重试次数","desc":"默认 1,最大 3,仅对网络异常/非业务异常重试"}] //[param: {"required":false,"key":"hermes_qidian.qdreader_cron_silent","bool":true,"placeholder":"false","name":"定时无账号时静默","desc":"true 时定时触发且没有账号不回复"}] var BUCKET = "hermes_qidian" -var LEGACY_BUCKET = "otto" var STORE_KEY = "qdreader_cookie_json" var BIND_KEY = "qdreader_user_bind_json" var REMARK_KEY = "qdreader_remark_json" @@ -44,7 +43,6 @@ 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) {} - try { if (typeof bucketGet === "function") { var lv = bucketGet(LEGACY_BUCKET, key); if (lv !== undefined && lv !== null && String(lv) !== "") return lv } } catch (e) {} return def } function cfgSet(key, value) { @@ -283,44 +281,13 @@ function normalizeSignHeaders(signed) { if (!ok || !isObject(headers)) throw new Error("签名失败:" + JSON.stringify(signed).slice(0, 500)) return headers } -function signBaseCandidates(rawBase) { - rawBase = String(rawBase || DEFAULT_SIGN_API).replace(/\/+$/, "") - var out = [rawBase] - if (/^https?:\/\/api\.120399\.xyz$/i.test(rawBase)) out.push(rawBase + "/qdreader") - if (/^https?:\/\/api\.120399\.xyz\/QDReader$/i.test(rawBase)) out.push(rawBase.replace(/\/QDReader$/i, "/qdreader")) - var seen = {} - return out.filter(function (x) { if (seen[x]) return false; seen[x] = true; return true }) -} -function signErrorRetriable(e) { - var msg = e && e.message ? e.message : String(e || "") - return /签名网关无返回|Cannot\s+POST|404|Not\s+Found/i.test(msg) -} -function isLikelyEmptyHttpResponse(x) { - return !x || (typeof x === "string" && !trim(x)) -} -function requestSignHeaders(rawBase, payload) { - var bases = signBaseCandidates(rawBase) - var lastErr = null - for (var i = 0; i < bases.length; i++) { - try { - var signed = postJson(bases[i] + "/sign", payload) - if (i + 1 < bases.length && isLikelyEmptyHttpResponse(signed)) throw new Error("签名网关无返回") - return normalizeSignHeaders(signed) - } catch (e) { - lastErr = e - debug("QDReader sign gateway " + bases[i] + "/sign failed: " + (e && e.message ? e.message : String(e))) - if (!signErrorRetriable(e)) break - } - } - throw lastErr || new Error("签名网关无返回") -} 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 = requestSignHeaders(signApi, payload) + 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 } diff --git a/tests/qdreader_plugin.test.mjs b/tests/qdreader_plugin.test.mjs index e75421c..0e49443 100644 --- a/tests/qdreader_plugin.test.mjs +++ b/tests/qdreader_plugin.test.mjs @@ -127,33 +127,16 @@ test('bucketGet/bucketSet fallback works when get/set are unavailable in Autman- assert.equal(JSON.parse(bucketStore['hermes_qidian.qdreader_user_bind_json'])['12345'], undefined); }); -test('reads legacy otto bucket but writes new hermes_qidian bucket', () => { +test('hermes_qidian bucket values are read and written without legacy bucket fallback', () => { const bucketStore = { - 'otto.qdreader_cookie_json': JSON.stringify({ '12345': cookie }), - 'otto.qdreader_user_bind_json': JSON.stringify({ '12345': 'user-a' }), + 'hermes_qidian.qdreader_cookie_json': JSON.stringify({ '12345': cookie }), + 'hermes_qidian.qdreader_user_bind_json': JSON.stringify({ '12345': 'user-a' }), }; const r = runPlugin({ content: '启点账号', bucketStore, userId: 'user-a', listens: ['3', '1', '主号'] }); assert.match(r.replies.join('\n'), /已更新备注/); assert.equal(JSON.parse(r.bucketStore['hermes_qidian.qdreader_remark_json'])['12345'], '主号'); }); -test('legacy default sign api base falls back to qdreader path when root /sign is empty', () => { - const store = { qdreader_cookie_json: JSON.stringify({ '12345': cookie }), qdreader_sign_api: 'https://api.120399.xyz' }; - const r = runPlugin({ - content: '启点签到', - store, - requests: [ - { statusCode: 404, body: '' }, - { body: { code: 200, data: { 'QD-Sign': 'sig2' } } }, - { statusCode: 200, body: { code: 0, msg: '签到成功' } }, - ], - }); - assert.equal(r.calls.length, 3); - assert.match(r.calls[0].url, /api\.120399\.xyz\/sign$/); - assert.match(r.calls[1].url, /api\.120399\.xyz\/qdreader\/sign$/); - assert.match(r.replies[0], /✅ 12\*\*\*45/); -}); - test('qidian request retries transient request errors', () => { const store = { qdreader_cookie_json: JSON.stringify({ '12345': cookie }), qdreader_retry_count: '2' }; const r = runPlugin({