feat: add qdreader autman plugin and qx rewrite

This commit is contained in:
2026-05-17 02:49:14 +08:00
commit 88381c3487
6 changed files with 373 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
# qdreader-autman-qx
起点读书(QDReader)自动化辅助:
- `autman/`:Autman 路由插件,负责接收/保存 QX 上传的 Cookie Map。
- `qx/rewrite/`Quantumult X 重写 JS,打开起点读书福利中心时自动获取/更新 Cookie。
- `qx/subscribe/`Quantumult X 订阅片段和订阅 JSON。
- `source/`:上游 QDReader 脚本备份,用于 QX 定时任务运行。
## 1. Autman 插件
导入:`autman/qdreader_cookie_autman.js`
路由:
```text
/m/qdreader/cookie
```
功能:
- `GET /m/qdreader/cookie`:查看当前 Cookie 状态。
- `POST /m/qdreader/cookie`:新增/更新 Cookie。
POST JSON 示例:
```json
{
"action": "update",
"uid": "可选,不传则从 QDHeader 或响应体识别",
"cookie": "QDReader Cookie 字符串",
"response_body": "可选 getlogininfo 响应体",
"token": "可选,与插件参数 qdreader_token 一致"
}
```
插件将 Cookie 保存为 Autman 参数 `qdreader_cookie_json`,格式兼容上游脚本的 `QDREADER_COOKIE`
```json
{"uid":"cookie"}
```
## 2. Quantumult X 重写
订阅片段:
```text
https://gitea.chickliu.fun/Hermes/qdreader-autman-qx/raw/branch/main/qx/subscribe/qdreader.qx.snippet
```
手动配置核心内容:
```ini
[mitm]
hostname = h5.if.qidian.com
[rewrite_local]
^https?:\/\/h5\.if\.qidian\.com\/argus\/api\/v1\/user\/getlogininfo url script-response-body https://gitea.chickliu.fun/Hermes/qdreader-autman-qx/raw/branch/main/qx/rewrite/qdreader_cookie.js
```
抓取步骤:
1. Quantumult X 启用 MitM 并信任证书。
2. 启用上面的重写。
3. 打开起点读书 App → 我的 → 福利中心。
4. 看到“Cookie 新增/更新成功”通知即可。
可选同步 Autman:在 QX 的持久化变量中设置:
- `QDREADER_AUTMAN_URL`:例如 `http://你的Autman:端口/m/qdreader/cookie`
- `QDREADER_AUTMAN_TOKEN`:与 Autman 插件参数 `qdreader_token` 一致
## 3. 定时签到
QX 片段已包含上游脚本定时任务:
```ini
11 2 * * * https://gitea.chickliu.fun/Hermes/qdreader-autman-qx/raw/branch/main/source/qdreader.upstream.js, tag=起点读书自动签到, enabled=true
```
> 说明:上游脚本内部依赖签名网关 `https://api.120399.xyz`。本仓库保留上游运行脚本,并额外提供更清晰的 Cookie 自动获取与 Autman 同步入口。
## 4. 风险提示
本项目仅供个人学习、研究与自动化测试使用,不保证长期可用。目标接口、风控策略或签名网关变化都可能导致失效。
+166
View File
@@ -0,0 +1,166 @@
//[open_source: true]
//[create_at: 2026-05-17 02:50:00]
//[title: 起点读书签到]
//[author: Hermes]
//[service: BOSS]
//[class: 工具类]
//[version: 1.0.0]
//[platform: web,qq,wx,tg,tb]
//[public: false]
//[price: 0]
//[disable: false]
//[description: 起点读书(QDReader) Autman 路由插件:接收 Quantumult X 重写上传的 Cookie,按 uid 自动新增/更新;也可查看 Cookie 状态。实际每日签到建议仍由 QX 定时运行仓库内 qdreader.upstream.js,或把 QDREADER_COOKIE JSON 同步到 Node/青龙环境执行。]
//[router: /qdreader/cookie]
//[method: get]
//[method: post]
//[param: {"required":false,"key":"otto.qdreader_token","bool":false,"placeholder":"访问Token","name":"接口鉴权Token","desc":"外部请求携带 access_token 或 token;留空则不校验"}]
//[param: {"required":false,"key":"otto.qdreader_cookie_json","bool":false,"placeholder":"{\"uid\":\"cookie\"}","name":"QDReader Cookie JSON","desc":"由 QX 重写自动维护,JSON Map 格式"}]
var STORE_KEY = "qdreader_cookie_json"
function replyJSON(obj) {
if (typeof response === "function") response(obj)
else if (typeof sender !== "undefined" && sender.reply) sender.reply(obj)
else if (typeof sendText === "function") sendText(JSON.stringify(obj))
else console.log(JSON.stringify(obj))
}
function fail(code, msg, data) {
replyJSON({ ok: false, status: "fail", retcode: code, msg: msg, data: data || {} })
}
function ok(data, msg) {
replyJSON({ ok: true, status: "ok", retcode: 0, msg: msg || "success", data: data || {} })
}
function safeJsonParse(s, def) {
if (!s) return def
try { return JSON.parse(s) } catch (e) { return def }
}
function getStore() {
var raw = get(STORE_KEY) || "{}"
var obj = safeJsonParse(raw, {})
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return {}
return obj
}
function setStore(obj) {
var s = JSON.stringify(obj)
set(STORE_KEY, s)
return s
}
function getHeader(obj, name) {
if (!obj) return ""
var lower = String(name).toLowerCase()
for (var k in obj) if (String(k).toLowerCase() === lower) return obj[k]
return ""
}
function parseCookieString(cookie) {
var jar = {}
String(cookie || "").split(";").forEach(function (part) {
var i = part.indexOf("=")
if (i <= 0) return
var k = part.slice(0, i).trim()
var v = part.slice(i + 1).trim()
if (k) jar[k] = v
})
return jar
}
function b64decode(s) {
s = String(s || "")
try {
if (typeof Buffer !== "undefined") return Buffer.from(s, "base64").toString("utf8")
} catch (e) {}
try { return decodeURIComponent(escape(atob(s))) } catch (e) { return "" }
}
function uidFromQDHeader(qdheader) {
var decoded = b64decode(qdheader)
if (!decoded) return ""
var parts = decoded.split("|")
return String(parts[0] || "").trim()
}
function extractUid(cookie, body) {
var jar = parseCookieString(cookie)
var uid = uidFromQDHeader(jar.QDHeader || jar.qdheader || jar.QDHEADER || "")
if (uid) return uid
if (jar.uid) return String(jar.uid)
var obj = safeJsonParse(body, null)
try {
uid = obj && obj.Data && obj.Data.UserInfo && (obj.Data.UserInfo.UserId || obj.Data.UserInfo.userId)
if (uid) return String(uid)
} catch (e) {}
return ""
}
function readRequest() {
var method = typeof getMethod === "function" ? getMethod() : "get"
var params = typeof getRouterParams === "function" ? (getRouterParams() || {}) : {}
var body = {}
var raw = ""
if (String(method).toLowerCase() === "post") {
raw = typeof getRouterData === "function" ? (getRouterData() || "") : ""
body = safeJsonParse(raw, {})
}
return { method: method, params: params, body: body, raw: raw }
}
var req = readRequest()
var token = req.body.access_token || req.body.token || req.params.access_token || req.params.token || ""
var configuredToken = get("qdreader_token") || ""
if (configuredToken && configuredToken !== token) {
fail(-1, "token错误")
return
}
var action = req.body.action || req.params.action || "status"
var cookie = req.body.cookie || req.body.ck || req.params.cookie || req.params.ck || ""
var uid = req.body.uid || req.params.uid || ""
var responseBody = req.body.response_body || req.body.body || ""
if (action === "status") {
var store = getStore()
ok({ count: Object.keys(store).length, uids: Object.keys(store), envName: "QDREADER_COOKIE" }, "起点读书 Cookie 接收插件已启用")
return
}
if (action === "delete") {
var storeDel = getStore()
if (!uid) { fail(-2, "uid不能为空"); return }
var existed = !!storeDel[uid]
delete storeDel[uid]
var rawDel = setStore(storeDel)
ok({ uid: uid, existed: existed, count: Object.keys(storeDel).length, qdreader_cookie: rawDel }, existed ? "已删除" : "原本不存在")
return
}
if (action === "replace_all") {
var incoming = req.body.cookies || req.body.qdreader_cookie || req.body.QDREADER_COOKIE || req.params.cookies || ""
var obj = typeof incoming === "object" ? incoming : safeJsonParse(incoming, null)
if (!obj || typeof obj !== "object" || Array.isArray(obj)) { fail(-3, "cookies 必须是 JSON Map"); return }
var rawAll = setStore(obj)
ok({ count: Object.keys(obj).length, uids: Object.keys(obj), qdreader_cookie: rawAll }, "已整体替换")
return
}
// default: update cookie
if (!cookie) {
fail(-4, "cookie不能为空", { usage: "POST JSON: {action:'update', uid:'可选', cookie:'Cookie字符串', response_body:'可选'}" })
return
}
uid = uid || extractUid(cookie, responseBody)
if (!uid) {
fail(-5, "无法识别 uidCookie 中缺少 QDHeader/uid,或 response_body 无 UserInfo.UserId")
return
}
var store = getStore()
var isNew = !store[uid]
var changed = store[uid] !== cookie
store[uid] = cookie
var rawStore = setStore(store)
ok({ uid: uid, is_new: isNew, changed: changed, count: Object.keys(store).length, envName: "QDREADER_COOKIE", qdreader_cookie: rawStore }, (isNew ? "新增" : (changed ? "更新" : "未变化")) + "成功")
+92
View File
@@ -0,0 +1,92 @@
/*
* 起点读书 Cookie 自动获取/更新 - Quantumult X
* 用法:打开起点读书 App → 我的 → 福利中心,拦截 getlogininfo 后自动保存本地 QDREADER_COOKIE,并可选上传到 Autman。
* 可选配置(QX 持久化变量):
* QDREADER_AUTMAN_URL 例如 http://你的Autman:端口/m/qdreader/cookie
* QDREADER_AUTMAN_TOKEN 与 Autman 插件 qdreader_token 一致;留空不传 token
*/
const KEY = 'QDREADER_COOKIE'
const AUTMAN_URL_KEY = 'QDREADER_AUTMAN_URL'
const AUTMAN_TOKEN_KEY = 'QDREADER_AUTMAN_TOKEN'
function getHeader(headers, name) {
const lower = String(name).toLowerCase()
for (const k in (headers || {})) if (String(k).toLowerCase() === lower) return headers[k]
return ''
}
function parseCookie(cookie) {
const jar = {}
String(cookie || '').split(';').forEach(part => {
const i = part.indexOf('=')
if (i <= 0) return
const k = part.slice(0, i).trim()
const v = part.slice(i + 1).trim()
if (k) jar[k] = v
})
return jar
}
function b64decode(s) {
try { return decodeURIComponent(escape(atob(String(s || '')))) } catch (e) { return '' }
}
function uidFromQDHeader(qdheader) {
const decoded = b64decode(qdheader)
return decoded ? String(decoded.split('|')[0] || '').trim() : ''
}
function parseJson(s, def) { try { return JSON.parse(s) } catch (e) { return def } }
function getUid(cookie, body) {
const jar = parseCookie(cookie)
let uid = uidFromQDHeader(jar.QDHeader || jar.qdheader || jar.QDHEADER || '')
if (uid) return uid
if (jar.uid) return String(jar.uid)
const obj = parseJson(body || '', null)
uid = obj && obj.Data && obj.Data.UserInfo && (obj.Data.UserInfo.UserId || obj.Data.UserInfo.userId)
return uid ? String(uid) : ''
}
function notify(title, sub, msg) { $notify(title, sub || '', msg || '') }
const cookie = getHeader($request && $request.headers, 'Cookie')
if (!cookie) {
notify('起点读书', 'Cookie 获取失败', '请求中没有 Cookie')
$done({})
} else {
const body = typeof $response !== 'undefined' ? ($response.body || '') : ''
const uid = getUid(cookie, body)
if (!uid) {
notify('起点读书', 'Cookie 获取失败', '无法识别 uid,请确认已登录并进入福利中心')
$done({})
} else {
const old = parseJson($prefs.valueForKey(KEY) || '{}', {}) || {}
const isNew = !old[uid]
const changed = old[uid] !== cookie
old[uid] = cookie
$prefs.setValueForKey(JSON.stringify(old), KEY)
const autmanUrl = $prefs.valueForKey(AUTMAN_URL_KEY) || ''
const autmanToken = $prefs.valueForKey(AUTMAN_TOKEN_KEY) || ''
const doneMsg = `Uid: ${uid}\n账号数: ${Object.keys(old).length}`
if (autmanUrl) {
$task.fetch({
url: autmanUrl,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'update', uid, cookie, response_body: body, token: autmanToken })
}).then(resp => {
let msg = doneMsg
try {
const data = JSON.parse(resp.body || '{}')
msg += `\nAutman: ${data.ok ? '同步成功' : ('同步失败 ' + (data.msg || ''))}`
} catch (e) { msg += `\nAutman HTTP: ${resp.statusCode}` }
notify('起点读书', (isNew ? 'Cookie 新增成功' : (changed ? 'Cookie 更新成功' : 'Cookie 未变化')), msg)
$done({})
}, err => {
notify('起点读书', 'Cookie 本地已保存,Autman 同步失败', `${doneMsg}\n${err && err.error || err}`)
$done({})
})
} else {
notify('起点读书', (isNew ? 'Cookie 新增成功' : (changed ? 'Cookie 更新成功' : 'Cookie 未变化')), doneMsg)
$done({})
}
}
}
+9
View File
@@ -0,0 +1,9 @@
[mitm]
hostname = h5.if.qidian.com
[rewrite_local]
# > 起点读书获取/更新 Cookie
^https?:\/\/h5\.if\.qidian\.com\/argus\/api\/v1\/user\/getlogininfo url script-response-body https://gitea.chickliu.fun/Hermes/qdreader-autman-qx/raw/branch/main/qx/rewrite/qdreader_cookie.js
[task_local]
11 2 * * * https://gitea.chickliu.fun/Hermes/qdreader-autman-qx/raw/branch/main/source/qdreader.upstream.js, tag=起点读书自动签到, img-url=https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/97/40/5e/97405e49-87bb-3f0a-bf3a-91cf8fdf99a0/AppIcon-0-0-1x_U007epad-0-1-0-0-sRGB-GLES2_U002c0-85-220.png/144x144.png, enabled=true
+20
View File
@@ -0,0 +1,20 @@
{
"name": "起点读书 QDReader",
"description": "Quantumult X 重写:自动获取/更新起点读书 Cookie;本地保存 QDREADER_COOKIE,可选同步到 Autman 插件。",
"mitm": ["h5.if.qidian.com"],
"rewrite": [
{
"name": "起点读书获取/更新 Cookie",
"match": "^https?:\\/\\/h5\\.if\\.qidian\\.com\\/argus\\/api\\/v1\\/user\\/getlogininfo",
"type": "script-response-body",
"script": "https://gitea.chickliu.fun/Hermes/qdreader-autman-qx/raw/branch/main/qx/rewrite/qdreader_cookie.js"
}
],
"task_local": [
"11 2 * * * https://gitea.chickliu.fun/Hermes/qdreader-autman-qx/raw/branch/main/source/qdreader.upstream.js, tag=起点读书自动签到, enabled=true"
],
"prefs": {
"QDREADER_AUTMAN_URL": "可选:Autman 路由地址,如 http://host:port/m/qdreader/cookie",
"QDREADER_AUTMAN_TOKEN": "可选:Autman 插件 qdreader_token"
}
}
File diff suppressed because one or more lines are too long