Compare commits
39
Commits
2413bd5b12
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ef173c6c7 | ||
|
|
012c3a9cb1 | ||
|
|
ebcdc67960 | ||
|
|
dfacfcb26d | ||
|
|
fa0e80666a | ||
|
|
becdf739dc | ||
|
|
e03ad733f4 | ||
|
|
7388cf8eb5 | ||
|
|
8d59be2d71 | ||
|
|
48ca7341f9 | ||
|
|
a8dc7233a7 | ||
|
|
50824baae4 | ||
|
|
56808b9f18 | ||
|
|
a7792012a1 | ||
|
|
aae3fc2ba2 | ||
|
|
373130c178 | ||
|
|
c7011a43ea | ||
|
|
b7afbf421a | ||
|
|
eaae089589 | ||
|
|
25d3f2c73b | ||
|
|
11ab7559fe | ||
|
|
fc4b3bc587 | ||
|
|
d2a7e49ec5 | ||
|
|
c94a3f16f7 | ||
|
|
1b546fa3e1 | ||
|
|
8ddb732232 | ||
|
|
46369d9af2 | ||
|
|
e035a760de | ||
|
|
c850f2bbbe | ||
|
|
510f7f086a | ||
|
|
9091137852 | ||
|
|
85405751b8 | ||
|
|
6aa020ca49 | ||
|
|
bf4377a5df | ||
|
|
93d529ec5f | ||
|
|
277ad71042 | ||
|
|
5bc2a818e1 | ||
|
|
74a8e45405 | ||
|
|
d09073c33c |
@@ -62,15 +62,18 @@ GET /api/sms/stats
|
||||
|
||||
初始通用仓库骨架已创建。等待 BOSS 上传既有 Autman 插件后,再按实际风格补齐实现。
|
||||
|
||||
### 起点读书 QDReader 签到插件
|
||||
### 启点读书 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: 1 7 * * *`,每日 07:01 自动签到;签到结束后立即归档并清空最近运行日志
|
||||
- 手动运行:`启点签到` / `启点签到 <uid>`
|
||||
- Cookie 维护:`起点ck <cookie>` / `起点ck 查询` / `起点ck 导出` / `起点ck 删除 <uid|all>`
|
||||
- v2.2.0:增强 Autman 存储/消息/请求兼容,增加重试、签名返回兼容、定时静默开关、输出脱敏和 Node VM 测试。
|
||||
- Cookie 维护:`启点账号` / `启点ck <cookie>` / `启点查询`
|
||||
- 任务配置入口:`启点账号` → `账号任务配置`,按账号保存到 `qdreader_task_pref_json`;`all on/off` 只影响选中的账号
|
||||
- 默认执行:主签到 `/argus/api/v3/checkin/checkin` + 固定低风险领取奖励 `/argus/api/v3/checkin/receivegifts`
|
||||
- 可选任务(默认关闭):激励任务 `mainPage`/`finishWatch`、每日任务 `packagelist`/`finishWatch`、每日抽奖 `detail`/`lottery`、每周兑换 `checkinexchangepage`/`exchangegoods`(仅周日;按上游 `Goods`/`GoodsScore` 选择并提交 `goodId`)、章节卡片 `chaptercardwithbook`、大咖荐书 `pullmessage`
|
||||
- 签名环境:相对路径走 `https://h5.if.qidian.com`;大咖荐书使用绝对地址 `https://magev6.if.qidian.com/argus/api/v2/message/pullmessage`;请求签名 method 使用小写;Autman 运行时需支持 `BigInt`(插件避免使用 BigInt 字面量以兼容较旧 JS 解析器)
|
||||
- v2.10.3:复核全部任务矩阵、补齐执行窗口/接口文档,并移除 BigInt 字面量,保留 Autman 单插件、独立 bucket、多用户隔离、手动只跑当前用户、cron 跑全部账号、运行后立即归档清日志。
|
||||
|
||||
Raw 导入:
|
||||
|
||||
|
||||
+135
-1
@@ -103,6 +103,8 @@ Python 示例:
|
||||
|
||||
- `rule` 支持正则,可写多行表示多个触发规则。
|
||||
- `cron` 支持 5 位或 6 位 cron 表达式。
|
||||
- 定时入口必须按运行环境判断,而不是只靠“消息内容为空”。推荐先检测 `ImType()` / `GetImType()` 是否为 `cron` / `task` / `fake`,命中后直接走定时分支;BOSS 实测 Autman cron 触发时日志上下文为 `content: ""` 且 `ImType/GetImType: "fake"`。不要在主入口用 `input()` 兜底读取命令内容。`input()` 是交互等待函数,定时场景可能返回 `q`、阻塞或把空触发误判成普通命令,导致 cron 已触发但业务没跑。参考茅台插件模式:`ImType() === "cron" ? "fake" : ImType()` 后显式进入 `cronTask()`。
|
||||
- cron 头注兼容性优先使用带空格格式 `// [cron: 1 7 * * *]`,避免不同 Autman 解析器只识别一种注释风格。
|
||||
- `admin: true` 表示仅管理员可执行。
|
||||
- `priority` 数字越大优先级越高。
|
||||
- `bypass: true` 常用于全拦截规则,例如 `[rule: ?]`,表示旁路并行处理。
|
||||
@@ -619,8 +621,140 @@ 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`。
|
||||
- 测试必须覆盖:快速提交、菜单添加、备注提示、详情改备注、编号选择删除/查询、旧 bucket 兼容、新 bucket 写入、用户隔离、手动执行只跑当前用户、cron 跑全部账号。
|
||||
|
||||
|
||||
## 12. QDReader 账号级任务开关约定
|
||||
|
||||
启点/QDReader 这类多账号签到插件的附加任务(广告、抽奖、兑换、章节卡、大咖荐书等)不要再放全局开关里统一控制。BOSS 确认更合适的模型是“直接写进账号里,对每个账号单独管理”。仓库实现约定如下:
|
||||
|
||||
- Cookie、用户绑定、备注仍分别保存;附加任务开关保存为 `qdreader_task_pref_json`。
|
||||
- `qdreader_task_pref_json` 结构为 `uid -> { taskKey: boolean }`,例如 `{ "12345": { "adv": true } }`。
|
||||
- `启点账号` 菜单内提供“账号任务配置”,先选择账号,再切换该账号的任务开关;支持 `all on` / `all off` 一键开启或关闭该账号全部可选任务。
|
||||
- 定时/手动签到执行时按 uid 读取自己的任务配置;一个账号开启广告任务不影响其它账号。
|
||||
- 删除单个账号或清空当前用户账号时,同步删除该 uid 的任务配置,避免残留配置误作用于重新添加的账号。
|
||||
- 旧全局开关 key 可留作历史数据,但不要作为运行依据;测试必须覆盖“一个账号开启,另一个账号不执行”的隔离场景。
|
||||
|
||||
|
||||
QDReader 签到结果判定补充:
|
||||
|
||||
- 上游返回“今日已签到/已经签到/重复签到”应视为成功,不应把账号计入失败。
|
||||
- 签名网关返回 `url 不在允许的接口列表内` 说明本地可选任务接口尚未被网关白名单支持;对这类可选任务应显示为“已跳过”,不要让整个账号失败。
|
||||
- 保留原始错误到任务对象的 `rawMsg` 即可,聊天摘要只显示短的跳过原因,避免刷屏。
|
||||
|
||||
## 12. QDReader 上游完全体任务修复记录
|
||||
|
||||
- 不要只凭接口名猜测“激励任务/每日任务/大咖荐书”的调用链;应先反混淆上游 `Yuheng0101/X/Tasks/QDReader/qdreader.js`,再按真实流程移植。
|
||||
- 激励任务 `finishWatch` 可能返回 `result=9` 或 `Result=9`,上游将其视为可继续/已受理,再回查 `mainPage` 判断任务是否完成;插件不能把大小写不同的 `result=9` 直接计为失败。
|
||||
- 每日任务不是简单取任务列表第一个。上游会先处理 `packagelist` 里的惊喜/前置任务,再从任务列表按标题定位“每日任务”,执行多次 `finishWatch` 后回查完成状态。
|
||||
- 大咖荐书不是一次 `pullmessage` 即结束。消息 token 需要由 `UserId` 经过 `(uid * 4 & 0x7ffffffffffffffc) ^ 0x113a4f` 后转 base62;流程是:拉会话列表(`appappend` + `categoryIds=0`)→ 对红点会话带 `fromId` 拉消息 → 从最近消息的 `ActionUrl/RefUrl` 提取 `orderId` → `/argus/api/v1/interaction/rewarddetail`(`refresh=0`)→ 根据 `ChanceCount/BookStatus` 走 `/argus/api/v1/interaction/rewardcallback` 或 `/argus/api/v1/interaction/rewardaddshelf`。如果直接用十进制 token、旧的 `token/type/senderId` 参数,或旧的 `/argus/api/v2/recommend/...` 路径,实测会偏离当前上游并容易返回“参数错误”。
|
||||
- 每周兑换按上游字段执行:读取兑换页 `Goods` / `GoodsScore` / `ChapterCardCount`,选择当前余额可覆盖的最高 `GoodsScore` 章节卡商品,并用小写 `goodId` 调用 `exchangegoods`;`result=-220000` 明确视为“未到兑换时间”。
|
||||
- 大咖荐书不应再做固定北京时间窗口限制;当前反混淆上游里没有 `20:00-22:00` 之类限制,只保留“消息必须是最近一段时间”的过滤。若接口在某些时段返回空列表/无未读消息,按正常无任务处理,不要在插件侧提前跳过。
|
||||
- “无可领取礼包/今日已签到”属于幂等成功类提示;这类提示不应让整个账号汇总失败。
|
||||
- 每次修复这类链路必须补 VM 回归测试:请求路径、签名 payload 里的参数、特殊返回码分类、最终账号成功/失败汇总。
|
||||
|
||||
## 14. QDReader 定时签到成功复盘:从“代码看起来对”到“现场确实跑通”
|
||||
|
||||
本次 `2.10.x` 修复最终确认启点定时签到已成功运行,关键经验如下,后续遇到“定时任务似乎没跑/跑了但结果异常”时按这个顺序复核:
|
||||
|
||||
### 14.1 先确认 Autman 实际能调度到插件入口
|
||||
|
||||
- 插件必须保留简单明确的 cron 头注,例如 `//[cron: 1 7 * * *]`,避免未验证的复杂多时间表达式。
|
||||
- JS 插件顶层必须真的调用 `main()`;cron 触发时通常没有聊天内容,入口要把“空 content”解释为系统定时运行,而不是当作非法输入退出。
|
||||
- cron 分支要只做一件事:执行签到。归档应在签到结束后立即完成,不再依赖另一个午夜 cron 或拆分插件。
|
||||
- 手动 `启点签到` 与 cron 应复用同一条执行链,只区分账号范围:手动只跑当前用户账号,cron 跑全部账号。这样手动跑通才有诊断价值。
|
||||
|
||||
### 14.2 状态判断不能只看“最近日志”
|
||||
|
||||
- 当前实现签到结束后会把 `last_result_json` / `last_run_json` 复制到 `archive_json` 后清空近期日志;所以“最近日志为空”不代表没跑。
|
||||
- 查询逻辑必须在近期日志为空时回退读取 `archive_json`,并展示归档批次、来源、时间、成功/失败/总数、单账号结果。
|
||||
- 归档 key 和展示时间必须按北京时间生成,避免 UTC 边界导致今天的签到归到昨天。
|
||||
|
||||
### 14.3 完全体任务要有矩阵,而不是靠猜
|
||||
|
||||
默认安全任务:
|
||||
|
||||
| 任务 | 默认 | 时间/条件 | 接口 |
|
||||
|---|---:|---|---|
|
||||
| 主签到 | 开 | cron / 手动 | `/argus/api/v3/checkin/checkin` |
|
||||
| 领取奖励 | 开 | 签到后固定执行 | `/argus/api/v3/checkin/receivegifts` |
|
||||
|
||||
可选任务必须按账号保存在 `qdreader_task_pref_json`,默认关闭:
|
||||
|
||||
| 任务 | 时间/条件 | 接口链 |
|
||||
|---|---|---|
|
||||
| 激励任务 | 账号开启后执行 | `video/adv/mainPage` → `video/adv/finishWatch` → 回查 `mainPage` |
|
||||
| 每日任务 | 账号开启后执行 | `checkin/packagelist` → `finishWatch` → 回查 |
|
||||
| 每日抽奖 | 账号开启后执行 | `checkin/detail` → `checkin/lottery` |
|
||||
| 每周兑换 | 账号开启且北京时间周日 | `checkin/checkinexchangepage` → `checkin/exchangegoods` |
|
||||
| 章节卡片 | 账号开启后执行 | `readtime/scoremall/chaptercardwithbook` |
|
||||
| 大咖荐书 | 账号开启 | `magev6.if.qidian.com/.../pullmessage` → `getRewardDetail` → `rewardCallback/rewardAddShelf` |
|
||||
|
||||
### 14.4 上游返回和签名网关要按实测分类
|
||||
|
||||
- `今日已签到`、`重复签到`、`已领取`、`已完成`、`无可领取礼包` 等幂等提示应视为成功类,不应让账号失败。
|
||||
- 激励任务 `finishWatch` 的 `Result/result = 9` 是上游可接受状态,要继续回查完成状态,而不是直接失败。
|
||||
- BOSS 明确要求 QDReader 激励任务“依照原项目,不做我以为的逻辑修改”。原项目的激励任务控制流是:读取 `VideoRewardTab.TaskList`;如果最后一项 `IsFinished === 1` 直接完成;否则遍历所有未完成且有 `TaskId` 的列表项,每项提交一次 `/argus/api/v1/video/adv/finishWatch`;最后再回查 `mainPage`,仍以回查后列表最后一项 `IsFinished === 1` 作为完成判断。
|
||||
- 因此不要在激励任务里加入本地推测过滤,例如跳过“每日签到/其它入口任务”、只校验本轮提交的 TaskId、按 `NeedCount - Count` 计算剩余次数、或者输出“跳过 N 个需其它入口任务”。这些看起来更严格/更安全,但已经被现场结果证明会和原项目语义偏离,导致误判未完成。
|
||||
- 如果修改了激励任务,一定补三类 VM 回归:① 非最后项已完成但最后项未完成时仍失败;② 回查最后项完成时即成功,即使前面提交过的条目仍显示未完成;③ 看起来像其它入口/签到的条目只要未完成且有 `TaskId`,也按上游循环提交一次,不做本地过滤。
|
||||
- 可选任务如果被签名网关返回 `url 不在允许的接口列表内`,应标记为跳过 `⏭`,不要污染基础签到成功率。
|
||||
- 大咖荐书必须用 base62 token,并带 `senderId` 拉取具体会话;单次 `pullmessage` 或十进制 token 容易导致“参数错误”。
|
||||
|
||||
### 14.5 Autman JS 兼容性要作为上线前检查
|
||||
|
||||
- Autman 内置 JS 运行时不一定等同当前 Node。即使 Node `--check` 通过,也要避免不必要的新语法。
|
||||
- QDReader token 计算需要 `BigInt`,但不要写 `0n` / `62n` 这类 BigInt 字面量;使用 `BigInt(0)` / `BigInt(62)`,降低旧解析器失败风险。
|
||||
- 需要同时检查并同步两份维护副本:`plugins/qdreader/qdreader_sign_autman.js` 和 `js/qdreader_sign_autman.js`。
|
||||
|
||||
### 14.6 每次修复必须落地验证
|
||||
|
||||
最小验证集:
|
||||
|
||||
```bash
|
||||
cd /Users/chick/.Hermes/workspace/autman-plugins
|
||||
node --check plugins/qdreader/qdreader_sign_autman.js
|
||||
node --check js/qdreader_sign_autman.js
|
||||
node tests/qdreader_plugin.test.mjs
|
||||
git diff --check
|
||||
cmp -s plugins/qdreader/qdreader_sign_autman.js js/qdreader_sign_autman.js
|
||||
```
|
||||
|
||||
推送后还要验证 Gitea raw:
|
||||
|
||||
- raw 正式插件存在并包含新版本号。
|
||||
- raw `js/` 副本存在并与正式插件一致。
|
||||
- raw README/文档包含任务矩阵、执行时间窗口和关键接口。
|
||||
- raw 插件中不再包含 `0n` / `62n` 等 BigInt 字面量。
|
||||
|
||||
### 14.7 给 BOSS 的最终回复要包含什么
|
||||
|
||||
- 明确下一次 cron 时间,例如“每天 07:01”。
|
||||
- 明确默认只跑安全签到 + 领取奖励;可选任务需要在 `启点账号 → 账号任务配置` 按账号开启。
|
||||
- 明确查询结果从归档判断,不要把清空近期日志误解释成没运行。
|
||||
- 给出测试、commit、push、raw 验证结果,避免只说“应该可以”。
|
||||
|
||||
### 10.6 QDReader 原项目接口转写:字段大小写必须逐项核对
|
||||
|
||||
2026-05 复核启点激励任务时发现:原脚本能完成、插件不能完成的根因不是任务流程,而是 `finishWatchAdv` 的 POST payload 被转写错了字段名。上游提交字段为:
|
||||
|
||||
```text
|
||||
taskId=<TaskId>&BanId=0&BanMessage=&CaptchaAId=&CaptchaURL=&CaptchaType=0&Challenge=&Gt=&NewCaptcha=0&Offline=0&PhoneNumber=&SessionKey=
|
||||
```
|
||||
|
||||
插件曾误写成:
|
||||
|
||||
```text
|
||||
TaskId=<TaskId>&BanId=0&BanMessage=&Challenge=&CaptchaType=0&Fee=&Gif=&Gt=&NewCaptcha=0&Offline=0&PhoneTicket=&SessionKey=
|
||||
```
|
||||
|
||||
注意点:
|
||||
|
||||
- `taskId` 是小写 `t`,不是 `TaskId`。
|
||||
- 必须保留上游的空字段 `CaptchaAId`、`CaptchaURL`、`PhoneNumber`。
|
||||
- 不要凭字段含义替换成 `Fee`、`Gif`、`PhoneTicket`。
|
||||
- 复刻上游混淆脚本时,除流程外还要用测试断言实际请求 `body` 的完整字符串,避免只断言 URL/次数导致参数漂移漏检。
|
||||
- QDReader 激励/每日任务都共用 `finishWatchAdv`,修改 payload 时必须覆盖两类任务测试。
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
//[open_source: true]
|
||||
//[create_at: 2026-05-23 00:00:00]
|
||||
//[title: Autman 定时运行测试]
|
||||
//[author: Hermes]
|
||||
//[service: BOSS]
|
||||
//[class: 工具类]
|
||||
// [version: 1.0.0]
|
||||
// [platform: web,qq,wx,tg,tb,fs,we]
|
||||
// [public: false]
|
||||
// [price: 0]
|
||||
// [disable: false]
|
||||
// [description: 用于验证 Autman 当前版本定时插件入口、运行上下文和可用参数。论坛与现有插件约定使用 cron 头注;手动发送“定时测试”可交互返回诊断参数;定时每 5 分钟触发一次并打印 console.log。]
|
||||
// [rule: ^(定时测试|cron测试|task测试|autman定时测试)(.*)$]
|
||||
// [admin: true]
|
||||
// [priority: 1000]
|
||||
// [cron: */5 * * * *]
|
||||
|
||||
var PLUGIN_NAME = "Autman 定时运行测试"
|
||||
var PLUGIN_VERSION = "1.0.0"
|
||||
|
||||
function safeCall(label, fn) {
|
||||
try {
|
||||
if (typeof fn !== "function") return { ok: false, value: "<not function>" }
|
||||
var v = fn()
|
||||
if (v === undefined) return { ok: true, value: "<undefined>" }
|
||||
if (v === null) return { ok: true, value: "<null>" }
|
||||
return { ok: true, value: String(v) }
|
||||
} catch (e) {
|
||||
return { ok: false, value: "ERROR: " + (e && e.message ? e.message : String(e)) }
|
||||
}
|
||||
}
|
||||
|
||||
function hasFn(name) {
|
||||
try { return typeof this[name] === "function" } catch (e) { return false }
|
||||
}
|
||||
|
||||
function valueOf(name) {
|
||||
try {
|
||||
var v = this[name]
|
||||
if (typeof v === "function") return "<function>"
|
||||
if (v === undefined) return "<undefined>"
|
||||
if (v === null) return "<null>"
|
||||
if (typeof v === "object") return JSON.stringify(v)
|
||||
return String(v)
|
||||
} catch (e) {
|
||||
return "ERROR: " + (e && e.message ? e.message : String(e))
|
||||
}
|
||||
}
|
||||
|
||||
function callInfo(param) {
|
||||
return safeCall("call(" + param + ")", function () {
|
||||
if (typeof call !== "function") return "<call not function>"
|
||||
var f = call(param)
|
||||
if (typeof f !== "function") return "<call result not function>"
|
||||
if (param === "timeFormat") return f("yyyy-MM-dd hh:mm:ss")
|
||||
return f()
|
||||
}).value
|
||||
}
|
||||
|
||||
function getContentCompat() {
|
||||
var r = safeCall("GetContent", function () { return GetContent() })
|
||||
if (r.ok && r.value !== "<not function>" && r.value !== "<undefined>" && r.value !== "<null>") return r.value
|
||||
r = safeCall("getContent", function () { return getContent() })
|
||||
if (r.ok && r.value !== "<not function>" && r.value !== "<undefined>" && r.value !== "<null>") return r.value
|
||||
return ""
|
||||
}
|
||||
|
||||
function detectImType() {
|
||||
var r = safeCall("ImType", function () { return ImType() })
|
||||
if (r.ok && r.value !== "<not function>" && r.value !== "<undefined>" && r.value !== "<null>" && r.value !== "") return r.value
|
||||
r = safeCall("GetImType", function () { return GetImType() })
|
||||
if (r.ok && r.value !== "<not function>" && r.value !== "<undefined>" && r.value !== "<null>" && r.value !== "") return r.value
|
||||
return ""
|
||||
}
|
||||
|
||||
function isCronRun(content, imType) {
|
||||
var t = String(imType || "").toLowerCase()
|
||||
if (t === "cron" || t === "task") return true
|
||||
// 只作兜底:部分版本定时触发没有消息内容。
|
||||
return !content
|
||||
}
|
||||
|
||||
function replyText(text) {
|
||||
try { if (typeof sendText === "function") { sendText(text); return } } catch (e) {}
|
||||
try { if (typeof sender !== "undefined" && sender && typeof sender.reply === "function") { sender.reply(text); return } } catch (e2) {}
|
||||
try { if (typeof Sender !== "undefined" && Sender && typeof Sender.Reply === "function") { Sender.Reply(text); return } } catch (e3) {}
|
||||
try { if (typeof console !== "undefined" && console && typeof console.log === "function") console.log(text) } catch (e4) {}
|
||||
}
|
||||
|
||||
function buildReport() {
|
||||
var content = getContentCompat()
|
||||
var imType = detectImType()
|
||||
var cronRun = isCronRun(content, imType)
|
||||
var lines = []
|
||||
lines.push("【" + PLUGIN_NAME + " v" + PLUGIN_VERSION + "】")
|
||||
lines.push("触发类型: " + (cronRun ? "定时触发" : "交互触发"))
|
||||
lines.push("调度头注: // [cron: */5 * * * *]")
|
||||
lines.push("说明: 当前论坛/仓库约定使用 cron;task 仅作为兼容诊断值检测。")
|
||||
lines.push("")
|
||||
lines.push("-- 基础上下文 --")
|
||||
lines.push("content: " + JSON.stringify(content))
|
||||
lines.push("ImType/GetImType: " + JSON.stringify(imType))
|
||||
lines.push("GetUserID: " + safeCall("GetUserID", function () { return GetUserID() }).value)
|
||||
lines.push("GetChatID: " + safeCall("GetChatID", function () { return GetChatID() }).value)
|
||||
lines.push("isAdmin: " + safeCall("isAdmin", function () { return isAdmin() }).value)
|
||||
lines.push("")
|
||||
lines.push("-- call() 信息 --")
|
||||
lines.push("name: " + callInfo("name"))
|
||||
lines.push("version: " + callInfo("version"))
|
||||
lines.push("port: " + callInfo("port"))
|
||||
lines.push("machineId: " + callInfo("machineId"))
|
||||
lines.push("time: " + callInfo("timeFormat"))
|
||||
lines.push("")
|
||||
lines.push("-- 函数存在性 --")
|
||||
lines.push("cron API: " + (hasFn("cron") ? "function" : valueOf("cron")))
|
||||
lines.push("GetContent: " + (hasFn("GetContent") ? "function" : valueOf("GetContent")))
|
||||
lines.push("getContent: " + (hasFn("getContent") ? "function" : valueOf("getContent")))
|
||||
lines.push("ImType: " + (hasFn("ImType") ? "function" : valueOf("ImType")))
|
||||
lines.push("GetImType: " + (hasFn("GetImType") ? "function" : valueOf("GetImType")))
|
||||
lines.push("sendText: " + (hasFn("sendText") ? "function" : valueOf("sendText")))
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
function main() {
|
||||
var report = buildReport()
|
||||
// 关键:交互运行或定时运行时,都先写 console.log,方便在 Autman 日志里确认 main 是否被调用及上下文参数。
|
||||
try { if (typeof console !== "undefined" && console && typeof console.log === "function") console.log(report) } catch (e) {}
|
||||
replyText(report)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -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 <QX抓到的cookie>",
|
||||
"直接签到:启点签到",
|
||||
"指定签到:启点签到 <uid>",
|
||||
"【配置】",
|
||||
"签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API,
|
||||
"定时:每日 00:01 只归档并清空日志;每日 07:01 只自动签到;QX 只负责抓 cookie。"
|
||||
].join("\n")
|
||||
}
|
||||
function stripCommand(text) {
|
||||
var s = String(text || "")
|
||||
if (/^启点(?:签到|读书)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:签到|读书)\s*/i, ""))
|
||||
if (/^启点(?:归档|日志归档)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:归档|日志归档)\s*/i, ""))
|
||||
if (/^启点(?:查询)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:查询)\s*/i, ""))
|
||||
if (/^启点(?:账号)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:账号)\s*/i, ""))
|
||||
if (/^启点(?:ck|CK|cookie|Cookie)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:ck|CK|cookie|Cookie)\s*/i, ""))
|
||||
return trim(s.replace(/^qdreader\s*/i, ""))
|
||||
}
|
||||
function commandName(text) {
|
||||
if (/^启点(?:签到|读书)(?:\s|$)/i.test(text)) return "sign"
|
||||
if (/^启点(?:归档|日志归档)(?:\s|$)/i.test(text)) return "archive"
|
||||
if (/^qdreader\s+sign(?:\s|$)/i.test(text)) return "sign"
|
||||
if (/^qdreader\s+archive(?:\s|$)/i.test(text)) return "archive"
|
||||
if (/^启点(?:查询)(?:\s|$)/i.test(text)) return "status"
|
||||
if (/^启点(?:账号)(?:\s|$)/i.test(text)) return "account"
|
||||
return "ck"
|
||||
}
|
||||
function qs(obj) {
|
||||
var parts = []
|
||||
for (var k in (obj || {})) {
|
||||
if (!obj.hasOwnProperty(k)) continue
|
||||
var v = obj[k]
|
||||
if (v === undefined || v === null) continue
|
||||
if (Array.isArray(v)) { for (var i = 0; i < v.length; i++) parts.push(encodeURIComponent(k + "[" + i + "]") + "=" + encodeURIComponent(String(v[i]))) }
|
||||
else if (typeof v === "object") { for (var sk in v) if (v.hasOwnProperty(sk)) parts.push(encodeURIComponent(k + "[" + sk + "]") + "=" + encodeURIComponent(String(v[sk]))) }
|
||||
else parts.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(v)))
|
||||
}
|
||||
return parts.join("&")
|
||||
}
|
||||
function normalizeCookie(cookie) {
|
||||
var jar = parseCookieString(cookie)
|
||||
var session = jar.sessionyw || jar.session || jar.sessionKey || ""
|
||||
if (session) jar.sessionyw = session
|
||||
var appid = jar.appId || jar.appid || ""
|
||||
if (appid) jar.appId = appid
|
||||
var guid = jar.ywguid || jar.gwguid || jar.guid || ""
|
||||
var qdh = jar.qdh || jar.QDH || ""
|
||||
var qdheader = jar.QDHeader || jar.qdheader || jar.QDHEADER || ""
|
||||
return { jar: jar, cookie: cookieToString(jar), qdh: qdh, qdheader: qdheader, uid: extractUid(cookie), guid: guid, qimei: jar.qimei || jar.QIMEI || "", qimei36: jar.qimei36 || jar.QIMEI36 || "" }
|
||||
}
|
||||
function httpReq(opt) {
|
||||
if (!opt.headers) opt.headers = {}
|
||||
if (typeof request === "function") return request(opt)
|
||||
if (typeof $request === "function") return $request(opt)
|
||||
throw new Error("当前 Autman 环境没有 request/$request 方法,无法发起签到请求")
|
||||
}
|
||||
function parseResp(res) {
|
||||
if (res === undefined || res === null) return null
|
||||
var body = null
|
||||
if (res && typeof res === "object") {
|
||||
if (res.body !== undefined) body = res.body
|
||||
else if (res.data !== undefined) body = res.data
|
||||
else if (res.content !== undefined) body = res.content
|
||||
else if (res.responseText !== undefined) body = res.responseText
|
||||
else if (res.statusCode !== undefined || res.status !== undefined || res.headers !== undefined) body = ""
|
||||
else body = res
|
||||
} else {
|
||||
body = res
|
||||
}
|
||||
if (typeof body === "string") return safeJsonParse(body, body)
|
||||
return body
|
||||
}
|
||||
function requestWithRetry(opt, retryCount) {
|
||||
var lastErr = null
|
||||
var attempts = Math.max(1, (retryCount || 0) + 1)
|
||||
for (var i = 0; i < attempts; i++) {
|
||||
try {
|
||||
return httpReq(opt)
|
||||
} catch (e) {
|
||||
lastErr = e
|
||||
debug("QDReader request retry " + (i + 1) + "/" + attempts + " failed: " + (e && e.message ? e.message : String(e)))
|
||||
}
|
||||
}
|
||||
throw lastErr || new Error("请求失败")
|
||||
}
|
||||
function postJson(url, data) {
|
||||
var retry = getInt(RETRY_KEY, 1, 0, 3)
|
||||
return parseResp(requestWithRetry({ url: url, method: "post", dataType: "json", timeOut: 10000, headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify(data) }, retry))
|
||||
}
|
||||
function normalizeSignHeaders(signed) {
|
||||
if (typeof signed === "string") signed = safeJsonParse(signed, {})
|
||||
if (!signed) throw new Error("签名网关无返回")
|
||||
var code = signed.code !== undefined ? signed.code : (signed.retcode !== undefined ? signed.retcode : signed.status)
|
||||
var ok = code === undefined || code === 0 || code === "0" || code === 200 || code === "200" || code === "ok" || code === true
|
||||
var data = signed.data || signed.result || signed.headers || signed
|
||||
var headers = data && (data.headers || data.header || data)
|
||||
if (!ok || !isObject(headers)) throw new Error("签名失败:" + JSON.stringify(signed).slice(0, 500))
|
||||
return headers
|
||||
}
|
||||
function qidianRequest(path, method, data, cookie) {
|
||||
method = String(method || "GET").toUpperCase()
|
||||
var info = normalizeCookie(cookie)
|
||||
if (!info.uid) throw new Error("Cookie 缺少 uid/QDHeader")
|
||||
var signApi = String(cfgGet(SIGN_API_KEY, DEFAULT_SIGN_API)).replace(/\/+$/, "")
|
||||
var payload = { qdh: info.qdh, qdheader: info.qdheader, url: QIDIAN_BASE + path, method: method, data: data || {}, uid: info.uid, guid: info.guid, qimei: info.qimei, qimei36: info.qimei36 }
|
||||
var headers = normalizeSignHeaders(postJson(signApi + "/sign", payload))
|
||||
headers.Cookie = info.cookie
|
||||
if (!headers["User-Agent"]) headers["User-Agent"] = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/QDReaderiOS/5.9.456/728/QDReaderAppstore"
|
||||
var req = { url: QIDIAN_BASE + path, method: lower(method), dataType: "json", timeOut: 10000, headers: headers }
|
||||
if (method === "GET") {
|
||||
var query = qs(data || {})
|
||||
if (query) req.url += (req.url.indexOf("?") >= 0 ? "&" : "?") + query
|
||||
} else {
|
||||
req.headers["Content-Type"] = req.headers["Content-Type"] || "application/x-www-form-urlencoded"
|
||||
req.body = qs(data || {})
|
||||
}
|
||||
var retry = getInt(RETRY_KEY, 1, 0, 3)
|
||||
var out = parseResp(requestWithRetry(req, retry))
|
||||
if (typeof out === "string") out = safeJsonParse(out, out)
|
||||
return out
|
||||
}
|
||||
function resultCode(obj) { return obj && (obj.Result !== undefined ? obj.Result : obj.Code !== undefined ? obj.Code : obj.code !== undefined ? obj.code : obj.retcode) }
|
||||
function resultOk(obj) {
|
||||
var code = resultCode(obj)
|
||||
var status = obj && (obj.status || obj.Status)
|
||||
return code === undefined || code === 0 || code === "0" || code === 200 || code === "200" || status === "ok" || status === "success"
|
||||
}
|
||||
function resultMsg(obj) {
|
||||
if (!obj) return ""
|
||||
return obj.Message || obj.Msg || obj.message || obj.msg || obj.toast || (obj.data && (obj.data.message || obj.data.msg || obj.data.toast)) || ""
|
||||
}
|
||||
function nowIso() { try { return new Date().toISOString() } catch (e) { return String(new Date()) } }
|
||||
function setLastResult(uid, result) {
|
||||
var obj = cleanStringMap(safeJsonParse(cfgGet(LAST_RESULT_KEY, "{}"), {}))
|
||||
obj[String(uid)] = JSON.stringify(result || {})
|
||||
cfgSet(LAST_RESULT_KEY, JSON.stringify(obj))
|
||||
}
|
||||
function getLastResults() {
|
||||
var raw = cleanStringMap(safeJsonParse(cfgGet(LAST_RESULT_KEY, "{}"), {}))
|
||||
var out = {}
|
||||
for (var uid in raw) if (raw.hasOwnProperty(uid)) out[uid] = safeJsonParse(raw[uid], { msg: raw[uid] })
|
||||
return out
|
||||
}
|
||||
function setLastRunLog(log) { cfgSet(LAST_RUN_KEY, JSON.stringify(log || {})) }
|
||||
function getLastRunLog() { return safeJsonParse(cfgGet(LAST_RUN_KEY, "{}"), {}) || {} }
|
||||
function getArchiveStore() { return safeJsonParse(cfgGet(ARCHIVE_KEY, "{}"), {}) || {} }
|
||||
function setArchiveStore(obj) { cfgSet(ARCHIVE_KEY, JSON.stringify(obj || {})) }
|
||||
function localDateKey(iso) {
|
||||
var d = iso ? new Date(iso) : new Date()
|
||||
if (!d || isNaN(d.getTime())) d = new Date()
|
||||
var y = d.getFullYear()
|
||||
var m = String(d.getMonth() + 1); if (m.length < 2) m = "0" + m
|
||||
var day = String(d.getDate()); if (day.length < 2) day = "0" + day
|
||||
return y + "-" + m + "-" + day
|
||||
}
|
||||
function archiveRunLog(reason) {
|
||||
var run = getLastRunLog()
|
||||
var archive = getArchiveStore()
|
||||
var archived = false
|
||||
var key = ""
|
||||
if (run && run.finishedAt && run.total !== undefined) {
|
||||
key = localDateKey(run.finishedAt)
|
||||
archive[key] = run
|
||||
archive[key].archivedAt = nowIso()
|
||||
archive[key].archiveReason = reason || "daily-rollover"
|
||||
archived = true
|
||||
}
|
||||
setArchiveStore(archive)
|
||||
cfgSet(LAST_RESULT_KEY, "{}")
|
||||
cfgSet(LAST_RUN_KEY, "{}")
|
||||
return { archived: archived, key: key, archive: archive, run: run || {} }
|
||||
}
|
||||
function latestArchiveForVisible(uids) {
|
||||
var archive = getArchiveStore()
|
||||
var keys = []
|
||||
for (var k in archive) if (archive.hasOwnProperty(k)) keys.push(k)
|
||||
keys.sort()
|
||||
for (var i = keys.length - 1; i >= 0; i--) {
|
||||
var log = archive[keys[i]] || {}
|
||||
var items = log.items || []
|
||||
var map = {}
|
||||
for (var j = 0; j < items.length; j++) map[String(items[j].uid)] = items[j]
|
||||
if (!uids || !uids.length) return { key: keys[i], log: log, itemMap: map }
|
||||
for (var u = 0; u < uids.length; u++) if (map[String(uids[u])]) return { key: keys[i], log: log, itemMap: map }
|
||||
}
|
||||
return null
|
||||
}
|
||||
function cronMode() {
|
||||
var d = new Date()
|
||||
var h = d.getHours ? d.getHours() : 7
|
||||
// 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 <QX抓到的完整cookie>", uids: [] }
|
||||
var me = currentOwnerId()
|
||||
if (!me) return { ok: false, text: "无法识别当前用户,已拒绝保存CK,避免错误绑定。请确认插件在聊天会话中触发;诊断: " + currentUserDebug(), uids: [] }
|
||||
var lines = []
|
||||
var savedUids = []
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var cookie = entries[i].cookie
|
||||
if (cookie.indexOf("=") < 0 || cookie.indexOf(";") < 0) return { ok: false, text: "未识别到 Cookie。请发送:启点ck {\"uid\":\"cookie\"} 或 启点ck <QX抓到的完整cookie>", uids: savedUids }
|
||||
var parsedUid = extractUid(cookie)
|
||||
var uid = entries[i].uid || parsedUid
|
||||
if (!uid) return { ok: false, text: "无法从 Cookie 识别 uid。请确认 QX 抓的是启点读书福利中心 getlogininfo 请求,Cookie 内应包含 QDHeader 或 uid。", uids: savedUids }
|
||||
var isNew = !store[uid]
|
||||
var oldOwner = bind[uid] || ""
|
||||
if (!store[uid] && oldOwner && oldOwner !== me) {
|
||||
delete bind[uid]
|
||||
oldOwner = ""
|
||||
lines.push("检测到 uid: " + uid + " 只有旧绑定没有CK,已自动清理残留绑定")
|
||||
}
|
||||
if (store[uid] && oldOwner && oldOwner !== me && canRepairBinding(oldOwner, me)) {
|
||||
lines.push("检测到 uid: " + uid + " 旧绑定来自历史兜底标识 " + maskUid(oldOwner) + ",已修复为当前用户")
|
||||
oldOwner = ""
|
||||
}
|
||||
if (store[uid] && oldOwner && me && oldOwner !== me) {
|
||||
lines.push("无权更新 uid: " + uid + "(该账号已绑定其他用户)")
|
||||
continue
|
||||
}
|
||||
var changed = store[uid] !== cookie
|
||||
store[uid] = cookie
|
||||
bind[uid] = me
|
||||
savedUids.push(uid)
|
||||
lines.push("启点读书CK" + (isNew ? "新增" : (changed ? "更新" : "未变化")) + "成功")
|
||||
lines.push("uid: " + uid)
|
||||
if (entries.length > 1 && parsedUid && parsedUid !== uid) lines.push("提示: JSON uid 与 Cookie 解析 uid 不一致,已按 JSON uid 保存")
|
||||
}
|
||||
setStore(store); setBindStore(bind)
|
||||
lines.push(me ? ("绑定用户: " + maskUid(me)) : "绑定用户: 未识别")
|
||||
lines.push("账号数: " + visibleUids(store, bind, false).length)
|
||||
lines.push("cookie: " + maskCookie(entries[0].cookie))
|
||||
lines.push("签到:发送 启点签到")
|
||||
return { ok: savedUids.length > 0, text: lines.join("\n"), uids: savedUids }
|
||||
}
|
||||
function main() {
|
||||
var text = contentText()
|
||||
var isCronRun = !text
|
||||
var cmd = isCronRun ? cronMode() : commandName(text)
|
||||
var arg = isCronRun ? "" : trim(stripCommand(text).replace(/^(sign|archive)\s*/i, ""))
|
||||
if (!arg && cmd !== "sign" && cmd !== "archive" && cmd !== "account" && cmd !== "status") { reply(usage()); return }
|
||||
if (/^(帮助|help|h)$/i.test(arg)) { reply(usage()); return }
|
||||
|
||||
var store = getStore()
|
||||
var bind = getBindStore()
|
||||
if (cmd === "archive" || /^(归档|日志归档|archive)(?:\s+|$)/i.test(arg)) {
|
||||
reply(formatArchiveReply(archiveRunLog(isCronRun ? "midnight-archive" : "manual-archive")))
|
||||
return
|
||||
}
|
||||
if (cmd === "sign" || /^(签到|sign|run)(?:\s+|$)/i.test(arg)) {
|
||||
var target = trim(arg.replace(/^(签到|sign|run)\s*/i, ""))
|
||||
var out = signAll(target, isCronRun, isCronRun ? "cron" : "manual")
|
||||
if (!(isCronRun && getBool(CRON_SILENT_KEY, false) && out === "还没有保存启点读书CK")) reply(out)
|
||||
return
|
||||
}
|
||||
if (cmd === "account") { accountMenu(store, bind); return }
|
||||
if (cmd === "status") { queryMenu(); return }
|
||||
if (cmd === "ck" && /^(查询|状态|status|list|账号|管理)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;快速提交CK:启点ck <cookie>"); return }
|
||||
var delMsg = handleDelete(arg, store, bind)
|
||||
if (delMsg !== null) { reply(delMsg); return }
|
||||
if (cmd === "ck") {
|
||||
var save = handleSaveCookie(arg, store, bind)
|
||||
if (save.ok && save.uids.length === 1) save.text += maybePromptRemark(save.uids[0], store)
|
||||
reply(save.text)
|
||||
return
|
||||
}
|
||||
var fallbackSave = handleSaveCookie(arg, store, bind)
|
||||
reply(fallbackSave.text)
|
||||
}
|
||||
main()
|
||||
+700
-112
@@ -4,17 +4,17 @@
|
||||
//[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) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”;定时签到使用独立 cron `1 7 * * *`;日志归档由 `qdreader_archive_autman.js` 独立 cron `1 0 * * *` 执行,避免 Autman 对逗号 hour cron 兼容不一致导致定时不触发;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。]
|
||||
//[rule: ^启点(账号|查询|ck|CK|cookie|Cookie|签到|读书|归档|日志归档)(.*)$]
|
||||
//[rule: ^qdreader(.*)$]
|
||||
//[admin: true]
|
||||
//[priority: 600]
|
||||
//[cron: 1 7 * * *]
|
||||
// [version: 2.15.7]
|
||||
// [platform: web,qq,wx,tg,tb,fs,we]
|
||||
// [public: false]
|
||||
// [price: 0]
|
||||
// [disable: false]
|
||||
// [description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”;定时使用 `1 7 * * *` 每日 07:01 签到;完全体可选任务按上游原项目解密流程执行;每次签到运行结束后立即归档并清空最近运行日志,不再依赖单独归档定时;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。]
|
||||
// [rule: ^启点(账号|查询|ck|CK|cookie|Cookie|签到|读书|归档|日志归档)(.*)$]
|
||||
// [rule: ^qdreader(.*)$]
|
||||
// [admin: true]
|
||||
// [priority: 600]
|
||||
// [cron: 1 7 * * *]
|
||||
|
||||
|
||||
var BUCKET = "hermes_qidian"
|
||||
@@ -24,12 +24,28 @@ 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 TASK_PREF_KEY = "qdreader_task_pref_json"
|
||||
var ADV_JOB_ENABLE_KEY = "qdreader_adv_job_enable"
|
||||
var EXTRA_ADV_JOB_ENABLE_KEY = "qdreader_extra_adv_job_enable"
|
||||
var LOTTERY_ENABLE_KEY = "qdreader_lottery_enable"
|
||||
var WEEKLY_EXCHANGE_ENABLE_KEY = "qdreader_weekly_exchange_enable"
|
||||
var CHAPTER_CARD_ENABLE_KEY = "qdreader_chapter_card_enable"
|
||||
var MESSAGE_BOX_ENABLE_KEY = "qdreader_message_box_enable"
|
||||
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"
|
||||
var QD_TASKS = [
|
||||
{ key: "reward", label: "领取奖励", handler: "receiveGifts", defaultEnabled: true, builtin: true },
|
||||
{ key: "adv", label: "激励任务", handler: "adv", settingKey: ADV_JOB_ENABLE_KEY },
|
||||
{ key: "extraAdv", label: "每日任务", handler: "daily", settingKey: EXTRA_ADV_JOB_ENABLE_KEY },
|
||||
{ key: "lottery", label: "每日抽奖", handler: "lottery", settingKey: LOTTERY_ENABLE_KEY },
|
||||
{ key: "weeklyExchange", label: "每周兑换", handler: "exchange", settingKey: WEEKLY_EXCHANGE_ENABLE_KEY, sundayOnly: true },
|
||||
{ key: "chapterCard", label: "章节卡片", handler: "chapterCard", settingKey: CHAPTER_CARD_ENABLE_KEY },
|
||||
{ key: "messageBox", label: "大咖荐书", handler: "messageBox", settingKey: MESSAGE_BOX_ENABLE_KEY }
|
||||
]
|
||||
|
||||
function defined(name) { try { return typeof this[name] !== "undefined" } catch (e) { return false } }
|
||||
function globalValue(name) { try { return this[name] } catch (e) { return undefined } }
|
||||
@@ -75,6 +91,22 @@ function reply(msg) {
|
||||
try { if (typeof push === "function") { push({ content: out }); return } } catch (e) {}
|
||||
try { console.log(out) } catch (e) {}
|
||||
}
|
||||
function pushToUser(userId, content) {
|
||||
userId = trim(userId || "")
|
||||
content = typeof content === "string" ? content : JSON.stringify(content)
|
||||
if (!userId || !content) return false
|
||||
if (typeof push !== "function") return false
|
||||
var payloads = [
|
||||
{ imType: "wx", userID: userId, content: content },
|
||||
{ imType: "we", userID: userId, content: content },
|
||||
{ imType: "web", userID: userId, content: content },
|
||||
{ userID: userId, content: content }
|
||||
]
|
||||
for (var i = 0; i < payloads.length; i++) {
|
||||
try { push(payloads[i]); return true } catch (e) {}
|
||||
}
|
||||
return false
|
||||
}
|
||||
function debug(msg) { try { if (typeof Debug === "function") Debug(String(msg)); else console.log(String(msg)) } catch (e) {} }
|
||||
|
||||
function cleanStringMap(obj) {
|
||||
@@ -248,19 +280,29 @@ function maskUid(uid) {
|
||||
return uid.slice(0, 2) + "***" + uid.slice(-2)
|
||||
}
|
||||
|
||||
function runtimeImType() {
|
||||
var im = ""
|
||||
try { if (typeof ImType === "function") im = ImType() || "" } catch (e) {}
|
||||
try { if (!im && typeof GetImType === "function") im = GetImType() || "" } catch (e2) {}
|
||||
return lower(trim(im))
|
||||
}
|
||||
function isSchedulerImType(im) {
|
||||
im = lower(trim(im))
|
||||
return im === "cron" || im === "task" || im === "fake"
|
||||
}
|
||||
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) {}
|
||||
if (isSchedulerImType(runtimeImType())) return ""
|
||||
try { if (typeof getContent === "function") s = getContent() || "" } catch (e3) {}
|
||||
try { if (!s && typeof GetContent === "function") s = GetContent() || "" } catch (e4) {}
|
||||
try { if (!s && typeof sender !== "undefined" && sender && sender.getContent) s = sender.getContent() || "" } catch (e5) {}
|
||||
try { if (!s && typeof sender !== "undefined" && sender && sender.getMessage) s = sender.getMessage() || "" } catch (e6) {}
|
||||
try { if (!s && typeof message !== "undefined") s = String(message) } catch (e7) {}
|
||||
return trim(s)
|
||||
}
|
||||
function usage() {
|
||||
return [
|
||||
"启点读书签到插件 v2.5.6",
|
||||
"启点读书签到插件 v2.15.6",
|
||||
"【一级菜单】",
|
||||
"账号管理:启点账号",
|
||||
"执行查询:启点查询",
|
||||
@@ -268,9 +310,11 @@ function usage() {
|
||||
"快速提交CK:启点ck <QX抓到的cookie>",
|
||||
"直接签到:启点签到",
|
||||
"指定签到:启点签到 <uid>",
|
||||
"账号任务:启点账号 → 账号详情/任务配置",
|
||||
"【配置】",
|
||||
"签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API,
|
||||
"定时:每日 00:01 只归档并清空日志;每日 07:01 只自动签到;QX 只负责抓 cookie。"
|
||||
"完全体任务:领取奖励固定执行;激励/每日/抽奖/兑换/章节卡/大咖荐书按账号单独管理,默认关闭。",
|
||||
"定时:每日 07:01 自动执行签到链路;QX 只负责抓 cookie。"
|
||||
].join("\n")
|
||||
}
|
||||
function stripCommand(text) {
|
||||
@@ -324,11 +368,11 @@ 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 = ""
|
||||
if (res.body !== undefined && res.body !== null && String(res.body) !== "") body = res.body
|
||||
else if (res.data !== undefined && res.data !== null && String(res.data) !== "") body = res.data
|
||||
else if (res.content !== undefined && res.content !== null && String(res.content) !== "") body = res.content
|
||||
else if (res.responseText !== undefined && res.responseText !== null && String(res.responseText) !== "") body = res.responseText
|
||||
else if (res.statusCode !== undefined || res.status !== undefined || res.headers !== undefined) body = res.data !== undefined ? res.data : ""
|
||||
else body = res
|
||||
} else {
|
||||
body = res
|
||||
@@ -368,11 +412,12 @@ function qidianRequest(path, method, data, cookie) {
|
||||
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 targetUrl = /^https?:\/\//i.test(path) ? path : QIDIAN_BASE + path
|
||||
var payload = { qdh: info.qdh, qdheader: info.qdheader, url: targetUrl, method: lower(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 }
|
||||
var req = { url: targetUrl, 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
|
||||
@@ -385,17 +430,67 @@ function qidianRequest(path, method, data, cookie) {
|
||||
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 resultCode(obj) {
|
||||
obj = responseEnvelope(obj)
|
||||
if (!obj) return undefined
|
||||
var keys = ["Result", "result", "Code", "code", "retcode", "RetCode"]
|
||||
for (var i = 0; i < keys.length; i++) if (obj[keys[i]] !== undefined) return obj[keys[i]]
|
||||
var data = obj.Data || obj.data || obj.ResultData || obj.resultData
|
||||
if (data && typeof data === "object") for (var j = 0; j < keys.length; j++) if (data[keys[j]] !== undefined) return data[keys[j]]
|
||||
return undefined
|
||||
}
|
||||
function resultOk(obj) {
|
||||
var code = resultCode(obj)
|
||||
var status = obj && (obj.status || obj.Status)
|
||||
var env = responseEnvelope(obj)
|
||||
var code = resultCode(env)
|
||||
var status = env && (env.status || env.Status || env.statusCode || env.StatusCode)
|
||||
if (typeof status === "number" && status >= 400) return false
|
||||
if (String(status) === "401" || String(status) === "403") return false
|
||||
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 isLoginExpiredMsg(msg) {
|
||||
msg = String(msg || "")
|
||||
return /登录状态已失效|登录已失效|登录状态.*过期|登录.*过期|用户未登录|未登录|请登录|重新登录|Cookie.*失效|CK.*失效|cookie.*expired|login.*expired|not\s*login/i.test(msg)
|
||||
}
|
||||
function nowIso() { try { return new Date().toISOString() } catch (e) { return String(new Date()) } }
|
||||
function resultMsg(obj) {
|
||||
obj = responseEnvelope(obj)
|
||||
if (!obj) return ""
|
||||
var status = obj.status || obj.Status || obj.statusCode || obj.StatusCode
|
||||
if (String(status) === "401") return "登录状态已失效,请更新 Cookie"
|
||||
if (String(status) === "403") return "接口鉴权失败,请更新 Cookie"
|
||||
var msg = obj.Message || obj.Msg || obj.message || obj.msg || obj.error || obj.toast || (obj.data && (obj.data.message || obj.data.msg || obj.data.error || obj.data.toast || obj.data.Message || obj.data.Msg)) || (obj.Data && (obj.Data.message || obj.Data.msg || obj.Data.error || obj.Data.toast || obj.Data.Message || obj.Data.Msg)) || ""
|
||||
if (isLoginExpiredMsg(msg)) return "登录状态已失效,请更新 Cookie"
|
||||
return msg
|
||||
}
|
||||
function isCookieExpiredResult(obj) {
|
||||
var env = responseEnvelope(obj)
|
||||
if (!env) return false
|
||||
var status = env.status || env.Status || env.statusCode || env.StatusCode
|
||||
if (String(status) === "401") return true
|
||||
var rawMsg = env.Message || env.Msg || env.message || env.msg || env.error || env.toast || (env.data && (env.data.message || env.data.msg || env.data.error || env.data.toast || env.data.Message || env.data.Msg)) || (env.Data && (env.Data.message || env.Data.msg || env.Data.error || env.Data.toast || env.Data.Message || env.Data.Msg)) || ""
|
||||
return isLoginExpiredMsg(rawMsg)
|
||||
}
|
||||
function successLikeMsg(msg) {
|
||||
msg = String(msg || "")
|
||||
return /今日已签到|已经签到|已签到|重复签到|已领取|已经领取|已完成|完成|无可领取|无礼包|暂无可领取/.test(msg)
|
||||
}
|
||||
function taskResultOk(obj) {
|
||||
return resultOk(obj) || successLikeMsg(resultMsg(obj))
|
||||
}
|
||||
function isWhitelistBlockedError(msg) {
|
||||
msg = String(msg || "")
|
||||
return /url\s*不在允许的接口列表内|不在允许的接口列表|参数异常.*允许的接口/.test(msg)
|
||||
}
|
||||
function pad2(n) { n = String(n); return n.length < 2 ? "0" + n : n }
|
||||
function beijingDate(dateLike) {
|
||||
var d = dateLike ? new Date(dateLike) : new Date()
|
||||
if (!d || isNaN(d.getTime())) d = new Date()
|
||||
return new Date(d.getTime() + 8 * 60 * 60 * 1000)
|
||||
}
|
||||
function beijingTime(dateLike) {
|
||||
var d = beijingDate(dateLike)
|
||||
return d.getUTCFullYear() + "-" + pad2(d.getUTCMonth() + 1) + "-" + pad2(d.getUTCDate()) + " " + pad2(d.getUTCHours()) + ":" + pad2(d.getUTCMinutes()) + ":" + pad2(d.getUTCSeconds())
|
||||
}
|
||||
function nowIso() { return beijingTime() }
|
||||
function setLastResult(uid, result) {
|
||||
var obj = cleanStringMap(safeJsonParse(cfgGet(LAST_RESULT_KEY, "{}"), {}))
|
||||
obj[String(uid)] = JSON.stringify(result || {})
|
||||
@@ -412,12 +507,11 @@ 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
|
||||
var s = String(iso || "")
|
||||
var m0 = s.match(/^(\d{4}-\d{2}-\d{2})/)
|
||||
if (m0) return m0[1]
|
||||
var d = beijingDate(iso)
|
||||
return d.getUTCFullYear() + "-" + pad2(d.getUTCMonth() + 1) + "-" + pad2(d.getUTCDate())
|
||||
}
|
||||
function archiveRunLog(reason) {
|
||||
var run = getLastRunLog()
|
||||
@@ -452,12 +546,7 @@ function latestArchiveForVisible(uids) {
|
||||
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"
|
||||
// Cron only runs the signing branch. Archive is performed immediately after each sign run.
|
||||
return "sign"
|
||||
}
|
||||
function formatArchiveReply(ret) {
|
||||
@@ -476,17 +565,429 @@ function sourceName(source) {
|
||||
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 getTaskPrefs() { return safeJsonParse(cfgGet(TASK_PREF_KEY, "{}"), {}) || {} }
|
||||
function setTaskPrefs(obj) { return cfgSet(TASK_PREF_KEY, JSON.stringify(obj || {})) }
|
||||
function accountTaskPrefs(uid) {
|
||||
var all = getTaskPrefs()
|
||||
var prefs = all[String(uid)]
|
||||
return isObject(prefs) ? prefs : {}
|
||||
}
|
||||
function signAll(targetUid, allUsers, source) {
|
||||
function taskEnabledForUid(task, uid) {
|
||||
if (task.defaultEnabled || task.builtin) return true
|
||||
if (task.sundayOnly) { var d = beijingDate(); if (d.getUTCDay() !== 0) return false }
|
||||
var prefs = accountTaskPrefs(uid)
|
||||
return prefs[task.key] === true || prefs[task.key] === "true" || prefs[task.key] === 1 || prefs[task.key] === "1"
|
||||
}
|
||||
function taskEnabled(task) { return taskEnabledForUid(task, "") }
|
||||
function taskStatusLines(uid) {
|
||||
var lines = ["启点账号任务开关:" + (uid ? maskUid(uid) : "未选择账号")]
|
||||
var prefs = uid ? accountTaskPrefs(uid) : {}
|
||||
for (var i = 0; i < QD_TASKS.length; i++) {
|
||||
var t = QD_TASKS[i]
|
||||
var enabled = t.builtin || t.defaultEnabled || prefs[t.key] === true || prefs[t.key] === "true" || prefs[t.key] === 1 || prefs[t.key] === "1"
|
||||
var suffix = t.builtin ? "(固定)" : ""
|
||||
if (t.sundayOnly) suffix += "(仅周日执行)"
|
||||
lines.push((i + 1) + ". " + t.label + ":" + (enabled ? "开启" : "关闭") + suffix)
|
||||
}
|
||||
lines.push("回复:序号 on/off 切换,例如 2 on;all on 一键开启;all off 一键关闭;q 退出")
|
||||
return lines.join("\n")
|
||||
}
|
||||
function taskMenuForUid(uid) {
|
||||
if (!uid) return reply("未选择账号,已退出")
|
||||
reply(taskStatusLines(uid))
|
||||
var op = listenInput(60000)
|
||||
if (!op || /^(q|退出|取消)$/i.test(op)) return reply("已退出")
|
||||
var all = getTaskPrefs()
|
||||
var prefs = isObject(all[String(uid)]) ? all[String(uid)] : {}
|
||||
var allMatch = op.match(/^(all|全部|所有|一键|全开|全关)\s*(on|off|开启|关闭|启用|禁用|1|0)?$/i)
|
||||
if (allMatch) {
|
||||
var enableAll = /^(全开)$/i.test(allMatch[1]) || /^(on|开启|启用|1)$/i.test(allMatch[2] || "")
|
||||
if (/^(全关)$/i.test(allMatch[1])) enableAll = false
|
||||
if (!allMatch[2] && /^(all|全部|所有|一键)$/i.test(allMatch[1])) return reply("请指定 all on 或 all off")
|
||||
for (var ai = 0; ai < QD_TASKS.length; ai++) {
|
||||
var at = QD_TASKS[ai]
|
||||
if (at.builtin || !at.settingKey) continue
|
||||
prefs[at.key] = !!enableAll
|
||||
}
|
||||
all[String(uid)] = prefs
|
||||
setTaskPrefs(all)
|
||||
return reply("已" + (enableAll ? "一键开启" : "一键关闭") + "账号 " + maskUid(uid) + " 的全部可选任务\n" + taskStatusLines(uid))
|
||||
}
|
||||
var m = op.match(/^(\d+)\s*(on|off|开启|关闭|启用|禁用|1|0)?$/i)
|
||||
if (!m) return reply("未识别选项,已退出")
|
||||
var idx = parseInt(m[1], 10) - 1
|
||||
if (idx < 0 || idx >= QD_TASKS.length) return reply("编号无效,已退出")
|
||||
var task = QD_TASKS[idx]
|
||||
if (task.builtin || !task.settingKey) return reply(task.label + " 为固定任务,不能关闭")
|
||||
var currently = prefs[task.key] === true || prefs[task.key] === "true" || prefs[task.key] === 1 || prefs[task.key] === "1"
|
||||
var enable = m[2] ? /^(on|开启|启用|1)$/i.test(m[2]) : !currently
|
||||
prefs[task.key] = !!enable
|
||||
all[String(uid)] = prefs
|
||||
setTaskPrefs(all)
|
||||
return reply(task.label + "已" + (enable ? "开启" : "关闭") + "(账号 " + maskUid(uid) + ")\n" + taskStatusLines(uid))
|
||||
}
|
||||
function qdGet(path, data, cookie) { return qidianRequest(path, "GET", data || {}, cookie) }
|
||||
function qdPost(path, data, cookie) { return qidianRequest(path, "POST", data || {}, cookie) }
|
||||
function firstArray() {
|
||||
for (var i = 0; i < arguments.length; i++) if (Array.isArray(arguments[i])) return arguments[i]
|
||||
return []
|
||||
}
|
||||
function nested(obj, path) {
|
||||
var cur = obj
|
||||
for (var i = 0; i < path.length; i++) {
|
||||
if (!cur || cur[path[i]] === undefined || cur[path[i]] === null) return undefined
|
||||
cur = cur[path[i]]
|
||||
}
|
||||
return cur
|
||||
}
|
||||
function responseEnvelope(obj) {
|
||||
if (!obj || typeof obj !== "object") return obj
|
||||
if ((obj.statusCode !== undefined || obj.status !== undefined || obj.headers !== undefined) && obj.data && typeof obj.data === "object") return obj.data
|
||||
return obj
|
||||
}
|
||||
function payloadData(obj) {
|
||||
obj = responseEnvelope(obj)
|
||||
var data = obj && (obj.Data || obj.data || obj.ResultData || obj.resultData)
|
||||
if (data && typeof data === "object" && (data.Data || data.data || data.ResultData || data.resultData) && (data.Result !== undefined || data.result !== undefined || data.Code !== undefined || data.code !== undefined || data.Message !== undefined || data.message !== undefined)) return data.Data || data.data || data.ResultData || data.resultData
|
||||
return data || {}
|
||||
}
|
||||
function looksLikeVideoTask(task) { return isObject(task) && !!taskIdOf(task) && (task["IsFinished"] !== undefined || task["IsReceived"] !== undefined || task["Title"] !== undefined || task["TaskName"] !== undefined) }
|
||||
function taskArrayScore(list, kind) {
|
||||
if (!Array.isArray(list) || !list.length) return 0
|
||||
var score = 0
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var t = list[i] || {}
|
||||
if (!looksLikeVideoTask(t)) continue
|
||||
var title = taskTitle(t)
|
||||
score += 2
|
||||
if (kind === "daily" && (title === "完成1个广告任务得奖励" || title === "完成3个广告任务得奖励" || t["IsReceived"] !== undefined)) score += 5
|
||||
if (kind === "adv" && (t["IsFinished"] !== undefined || /激励|碎片|福利|广告/.test(title))) score += 3
|
||||
}
|
||||
return score
|
||||
}
|
||||
function findTaskListByShape(root, kind) {
|
||||
var best = []
|
||||
var bestScore = 0
|
||||
var seen = []
|
||||
function walk(node, depth) {
|
||||
if (!node || depth > 8) return
|
||||
if (Array.isArray(node)) {
|
||||
var score = taskArrayScore(node, kind)
|
||||
if (score > bestScore) { bestScore = score; best = node }
|
||||
for (var i = 0; i < node.length; i++) walk(node[i], depth + 1)
|
||||
return
|
||||
}
|
||||
if (!isObject(node) || seen.indexOf(node) >= 0) return
|
||||
seen.push(node)
|
||||
for (var k in node) walk(node[k], depth + 1)
|
||||
}
|
||||
walk(root, 0)
|
||||
return best
|
||||
}
|
||||
function advTaskList(main) {
|
||||
return firstArray(
|
||||
nested(main, ["Data", "DailyBenefitModule", "TaskList"]),
|
||||
nested(main, ["data", "DailyBenefitModule", "TaskList"]),
|
||||
nested(main, ["Data", "AdvTaskList"]),
|
||||
nested(main, ["data", "AdvTaskList"]),
|
||||
findTaskListByShape(payloadData(main), "adv")
|
||||
)
|
||||
}
|
||||
function dailyTaskList(main) {
|
||||
return firstArray(
|
||||
nested(main, ["Data", "VideoRewardTab", "TaskList"]),
|
||||
nested(main, ["data", "VideoRewardTab", "TaskList"]),
|
||||
nested(main, ["Data", "TaskList"]),
|
||||
nested(main, ["data", "TaskList"]),
|
||||
findTaskListByShape(payloadData(main), "daily")
|
||||
)
|
||||
}
|
||||
function surpriseBenefit(data) {
|
||||
return data.SurpriseBenefit || data.SurpriseTask || data.SurpriseBenefitTask || null
|
||||
}
|
||||
function makeVideoPayload(taskId) {
|
||||
return { taskId: String(taskId || ""), BanId: "0", BanMessage: "", CaptchaAId: "", CaptchaURL: "", CaptchaType: "0", Challenge: "", Gt: "", NewCaptcha: "0", Offline: "0", PhoneNumber: "", SessionKey: "" }
|
||||
}
|
||||
function field(obj, names, def) {
|
||||
for (var i = 0; i < names.length; i++) if (obj && obj[names[i]] !== undefined && obj[names[i]] !== null) return obj[names[i]]
|
||||
return def
|
||||
}
|
||||
function taskTitle(task) { return String(field(task, ["Title", "TaskName", "Name", "TaskTitle", "Desc", "Description"], "")) }
|
||||
function taskIdOf(task) { return field(task, ["TaskId", "TaskID", "taskId", "TaskIDStr", "Id", "id"], "") }
|
||||
function taskFinished(task) { return Number(field(task, ["IsFinished", "Finished", "Status", "status"], 0)) === 1 }
|
||||
function taskIdKey(id) { return String(id === undefined || id === null ? "" : id) }
|
||||
function taskRunCount(task, def) {
|
||||
var n = Number(field(task, ["NeedCount", "needCount", "Count", "count", "TotalCount", "totalCount", "Times", "times", "Limit", "limit"], def || 1))
|
||||
if (!n || isNaN(n) || n < 1) n = def || 1
|
||||
if (n > 10) n = 10
|
||||
return n
|
||||
}
|
||||
function taskAccepted(res) { return (taskResultOk(res) || Number(resultCode(res)) === 9) && !riskConf(res) }
|
||||
function riskConf(res) {
|
||||
res = responseEnvelope(res)
|
||||
var data = res && (res.Data || res.data || res.ResultData || res.resultData)
|
||||
return data && (data.RiskConf || data.riskConf || data.Risk || data.risk)
|
||||
}
|
||||
function riskBlockedMsg(res) {
|
||||
var rc = riskConf(res)
|
||||
if (!rc) return ""
|
||||
var url = rc.CaptchaURL || rc.captchaUrl || rc.Url || rc.url || ""
|
||||
return url ? "触发风控验证,任务未确认完成" : "触发风控验证,任务未确认完成"
|
||||
}
|
||||
function finishVideoTask(taskOrId, cookie, repeat) {
|
||||
var tid = typeof taskOrId === "object" ? taskIdOf(taskOrId || {}) : taskOrId
|
||||
var n = repeat || (typeof taskOrId === "object" ? taskRunCount(taskOrId, 1) : 1)
|
||||
var ok = 0
|
||||
var last = null
|
||||
if (!tid) return { ok: 0, total: 0, last: null }
|
||||
for (var i = 0; i < n; i++) {
|
||||
last = qdPost("/argus/api/v1/video/adv/finishWatch", makeVideoPayload(tid), cookie)
|
||||
if (taskAccepted(last)) ok++
|
||||
else break
|
||||
}
|
||||
return { ok: ok, total: n, last: last, taskId: tid }
|
||||
}
|
||||
function makeBase62Token(num) {
|
||||
var alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
var n = BigInt(num)
|
||||
var zero = BigInt(0)
|
||||
var base = BigInt(62)
|
||||
if (n === zero) return "0"
|
||||
var out = ""
|
||||
while (n > zero) { out = alphabet[Number(n % base)] + out; n = n / base }
|
||||
return out
|
||||
}
|
||||
function doReceiveGifts(cookie) {
|
||||
var res = qdPost("/argus/api/v3/checkin/receivegifts", { GameId: "0" }, cookie)
|
||||
var ok = taskResultOk(res)
|
||||
return { name: "领取奖励", ok: !!ok, msg: resultMsg(res) || (ok ? "领取成功" : "领取失败"), path: "/argus/api/v3/checkin/receivegifts" }
|
||||
}
|
||||
function doAdvTask(cookie) {
|
||||
var main = qdGet("/argus/api/v2/video/adv/mainPage", {}, cookie)
|
||||
if (!taskResultOk(main)) return { name: "激励任务", ok: false, msg: resultMsg(main) || "获取任务列表失败", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
var tasks = advTaskList(main)
|
||||
if (!tasks.length) return { name: "激励任务", ok: false, msg: "任务列表为空", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
if (taskFinished(tasks[tasks.length - 1])) return { name: "激励任务", ok: true, msg: "任务已完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
var submittedCount = 0
|
||||
var riskSubmitted = false
|
||||
for (var i = 0; i < tasks.length; i++) {
|
||||
var t = tasks[i] || {}
|
||||
if (taskFinished(t)) continue
|
||||
var tid = taskIdOf(t)
|
||||
if (!tid) continue
|
||||
var fr = finishVideoTask(tid, cookie, 1)
|
||||
submittedCount += fr.ok
|
||||
if (riskConf(fr.last)) riskSubmitted = true
|
||||
if (fr.ok < fr.total) return { name: "激励任务", ok: false, msg: riskBlockedMsg(fr.last) || resultMsg(fr.last) || ("第 " + (i + 1) + " 个任务失败"), path: "/argus/api/v1/video/adv/finishWatch" }
|
||||
}
|
||||
var verify = qdGet("/argus/api/v2/video/adv/mainPage", {}, cookie)
|
||||
var after = advTaskList(verify)
|
||||
var finished = !!(after && after.length && after.every(function (t) { return taskFinished(t) }))
|
||||
if (finished) return { name: "激励任务", ok: true, msg: "任务已完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
if (submittedCount > 0 && riskSubmitted) return { name: "激励任务", ok: false, msg: "触发风控验证,任务未确认完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
return { name: "激励任务", ok: false, msg: submittedCount ? "部分任务未完成" : "未找到可提交任务", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
}
|
||||
function taskReceived(task) {
|
||||
return Number(field(task, ["IsReceived", "Received", "Status", "status"], 0)) === 1
|
||||
}
|
||||
function doDailyTask(cookie) {
|
||||
var main = qdGet("/argus/api/v2/video/adv/mainPage", {}, cookie)
|
||||
if (!taskResultOk(main)) return { name: "每日任务", ok: false, msg: resultMsg(main) || "获取任务列表失败", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
var data = payloadData(main)
|
||||
var done = 0
|
||||
var riskSubmitted = false
|
||||
var pre = surpriseBenefit(data)
|
||||
if (pre && !taskFinished(pre) && taskIdOf(pre)) {
|
||||
var pr = finishVideoTask(taskIdOf(pre), cookie, 1)
|
||||
done += pr.ok
|
||||
if (riskConf(pr.last)) riskSubmitted = true
|
||||
if (pr.ok < pr.total) return { name: "每日任务", ok: false, msg: riskBlockedMsg(pr.last) || resultMsg(pr.last) || "惊喜福利任务失败", path: "/argus/api/v1/video/adv/finishWatch" }
|
||||
}
|
||||
var tasks = dailyTaskList(main)
|
||||
if (!tasks.length) return { name: "每日任务", ok: false, msg: "任务列表为空", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
var oneAd = null
|
||||
var target = null
|
||||
for (var i = 0; i < tasks.length; i++) {
|
||||
var title = taskTitle(tasks[i])
|
||||
if (!oneAd && title === "完成1个广告任务得奖励") oneAd = tasks[i]
|
||||
if (!target && title === "完成3个广告任务得奖励") target = tasks[i]
|
||||
}
|
||||
if (oneAd && !taskReceived(oneAd) && taskIdOf(oneAd)) {
|
||||
var ar = finishVideoTask(taskIdOf(oneAd), cookie, 1)
|
||||
done += ar.ok
|
||||
if (riskConf(ar.last)) riskSubmitted = true
|
||||
if (ar.ok < ar.total) return { name: "每日任务", ok: false, msg: riskBlockedMsg(ar.last) || resultMsg(ar.last) || "完成1个广告任务失败", path: "/argus/api/v1/video/adv/finishWatch" }
|
||||
}
|
||||
if (!target) return { name: "每日任务", ok: false, msg: "未找到每日任务任务", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
if (taskReceived(target)) return { name: "每日任务", ok: true, msg: "任务已完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
var tid = taskIdOf(target)
|
||||
if (!tid) return { name: "每日任务", ok: false, msg: "每日任务缺少 TaskId", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
var tr = finishVideoTask(tid, cookie, 3)
|
||||
done += tr.ok
|
||||
if (riskConf(tr.last)) riskSubmitted = true
|
||||
if (tr.ok < tr.total) return { name: "每日任务", ok: false, msg: riskBlockedMsg(tr.last) || resultMsg(tr.last) || "每日任务广告失败", path: "/argus/api/v1/video/adv/finishWatch" }
|
||||
var verify = qdGet("/argus/api/v2/video/adv/mainPage", {}, cookie)
|
||||
var after = dailyTaskList(verify)
|
||||
var finished = false
|
||||
for (var v = 0; v < after.length; v++) if (taskTitle(after[v]) === "完成3个广告任务得奖励" && taskReceived(after[v])) { finished = true; break }
|
||||
if (finished) return { name: "每日任务", ok: true, msg: "任务已完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
if (done > 0 && riskSubmitted) return { name: "每日任务", ok: false, msg: "触发风控验证,任务未确认完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
return { name: "每日任务", ok: false, msg: "任务未完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
}
|
||||
function doLotteryTask(cookie) {
|
||||
var detail = qdGet("/argus/api/v2/checkin/detail", {}, cookie)
|
||||
if (!taskResultOk(detail)) return { name: "每日抽奖", ok: false, msg: resultMsg(detail) || "获取抽奖信息失败", path: "/argus/api/v2/checkin/detail" }
|
||||
var data = detail.Data || detail.data || {}
|
||||
var times = Number(data.HasVideoUrge || 0) + Number(data.LotteryCount || data.LotteryChance || data.ChanceCount || 0)
|
||||
if (!times) return { name: "每日抽奖", ok: true, msg: "无可用抽奖次数", path: "/argus/api/v2/checkin/detail" }
|
||||
times = Math.min(times, 10)
|
||||
var okCount = 0
|
||||
for (var i = 0; i < times; i++) { var r = qdPost("/argus/api/v2/checkin/lottery", { GameId: "0", IsVideo: i < Number(data.HasVideoUrge || 0) ? "1" : "0" }, cookie); if (taskResultOk(r)) okCount++ }
|
||||
return { name: "每日抽奖", ok: okCount > 0, msg: "成功 " + okCount + "/" + times + " 次抽奖", path: "/argus/api/v2/checkin/lottery" }
|
||||
}
|
||||
function doWeeklyExchange(cookie) {
|
||||
var page = qdGet("/argus/api/v3/checkin/checkinexchangepage", {}, cookie)
|
||||
if (!taskResultOk(page)) return { name: "每周兑换", ok: false, msg: resultMsg(page) || "获取兑换页失败", path: "/argus/api/v3/checkin/checkinexchangepage" }
|
||||
var data = page.Data || page.data || {}
|
||||
var balance = Number(data.Balance || data.Score || 0)
|
||||
if (balance <= 0) return { name: "每周兑换", ok: false, msg: "余额不足(" + balance + ")", path: "/argus/api/v3/checkin/checkinexchangepage" }
|
||||
var goods = firstArray(data.Goods, data.GoodsList, data.GiftList, data.List, data.Items)
|
||||
if (!goods.length) return { name: "每周兑换", ok: false, msg: "章节卡列表为空", path: "/argus/api/v3/checkin/checkinexchangepage" }
|
||||
var chosen = null
|
||||
for (var i = 0; i < goods.length; i++) {
|
||||
var price = Number(goods[i].GoodsScore || goods[i].NeedScore || goods[i].Price || goods[i].Score || 0)
|
||||
if (price > 0 && price <= balance && (!chosen || price > Number(chosen.GoodsScore || chosen.NeedScore || chosen.Price || chosen.Score || 0))) chosen = goods[i]
|
||||
}
|
||||
if (!chosen) return { name: "每周兑换", ok: false, msg: "余额不足(" + balance + ")", path: "/argus/api/v3/checkin/checkinexchangepage" }
|
||||
var gid = chosen.GoodsId || chosen.Id || chosen.GoodsID
|
||||
var priceText = Number(chosen.GoodsScore || chosen.NeedScore || chosen.Price || chosen.Score || 0)
|
||||
var cardCount = chosen.ChapterCardCount || chosen.CardCount || chosen.Count || ""
|
||||
var r = qdPost("/argus/api/v3/checkin/exchangegoods", { goodId: gid }, cookie)
|
||||
var code = Number(r && (r.result !== undefined ? r.result : (r.Result !== undefined ? r.Result : (r.code !== undefined ? r.code : r.Code))))
|
||||
if (code === -220000) return { name: "每周兑换", ok: false, msg: "未到兑换时间", path: "/argus/api/v3/checkin/exchangegoods" }
|
||||
var ok = taskResultOk(r)
|
||||
var successMsg = cardCount ? ("消耗 " + priceText + " 点兑换 " + cardCount + " 点章节卡") : "兑换成功"
|
||||
return { name: "每周兑换", ok: ok, msg: resultMsg(r) || (ok ? successMsg : "兑换失败"), path: "/argus/api/v3/checkin/exchangegoods" }
|
||||
}
|
||||
function doChapterCard(cookie) {
|
||||
var res = qdGet("/argus/api/v1/readtime/scoremall/chaptercardwithbook", {}, cookie)
|
||||
var ok = taskResultOk(res)
|
||||
var cards = firstArray(nested(res, ["Data", "Cards"]), nested(res, ["data", "Cards"]), nested(res, ["Data", "ChapterCards"]), nested(res, ["data", "ChapterCards"]))
|
||||
return { name: "章节卡片", ok: !!ok, msg: ok ? (cards.length ? ("章节卡 " + cards.length + " 张") : "暂无章节卡") : (resultMsg(res) || "查询失败"), path: "/argus/api/v1/readtime/scoremall/chaptercardwithbook" }
|
||||
}
|
||||
function messageTokenFromCookie(cookie) {
|
||||
var jar = parseCookieString(cookie)
|
||||
var uid = extractUid(cookie)
|
||||
var raw = uid ? ((BigInt(uid) * BigInt(4) & BigInt("0x7ffffffffffffffc")) ^ BigInt("0x113a4f")) : BigInt(0)
|
||||
return raw ? makeBase62Token(raw) : (uid || jar.ywguid || "")
|
||||
}
|
||||
function recentCreateTime(v) {
|
||||
if (!v) return false
|
||||
var t = typeof v === "string" ? new Date(v).getTime() : Number(v)
|
||||
if (!t || isNaN(t)) return false
|
||||
if (t < 1000000000000) t = t * 1000
|
||||
var now = (new Date()).getTime()
|
||||
return now - t >= 0 && now - t <= 24 * 60 * 60 * 1000
|
||||
}
|
||||
function beijingHour(dateLike) { return beijingDate(dateLike).getUTCHours() }
|
||||
function messageBoxWindowLabel() { return "全天" }
|
||||
function messageBoxAllowedNow() {
|
||||
return true
|
||||
}
|
||||
function orderIdFromText(s) {
|
||||
var m = String(s || "").match(/orderId=([a-f0-9]+)/i)
|
||||
return m ? m[1] : ""
|
||||
}
|
||||
function doMessageBox(cookie) {
|
||||
var token = messageTokenFromCookie(cookie)
|
||||
if (!token) return { name: "大咖荐书", ok: true, msg: "缺少消息 token,已跳过", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" }
|
||||
var baseParams = { appappend: token, categoryIds: "0", pg: "1", pz: "10" }
|
||||
var res = qdGet("https://magev6.if.qidian.com/argus/api/v2/message/pullmessage", baseParams, cookie)
|
||||
if (!taskResultOk(res)) return { name: "大咖荐书", ok: false, msg: resultMsg(res) || "获取消息列表失败", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" }
|
||||
var groups = firstArray(nested(res, ["Data", "UserList"]), nested(res, ["data", "UserList"]), nested(res, ["Data", "List"]), nested(res, ["data", "List"]), nested(res, ["Data", "MsgList"]), nested(res, ["data", "MsgList"]))
|
||||
if (!groups.length) return { name: "大咖荐书", ok: true, msg: "无消息", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" }
|
||||
var senders = []
|
||||
for (var i = 0; i < groups.length; i++) if (Number(field(groups[i], ["RedPoint", "redPoint"], 0)) === 1 && field(groups[i], ["FromId", "fromId", "SenderId", "senderId", "FromUserId", "fromUserId"], "")) senders.push(groups[i])
|
||||
if (!senders.length) return { name: "大咖荐书", ok: true, msg: "无未读消息", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" }
|
||||
var handled = 0
|
||||
for (var s = 0; s < senders.length; s++) {
|
||||
var sid = field(senders[s], ["FromId", "fromId", "SenderId", "senderId", "FromUserId", "fromUserId"], "")
|
||||
var msgRes = qdGet("https://magev6.if.qidian.com/argus/api/v2/message/pullmessage", { appappend: token, categoryIds: "0", fromId: sid, pg: "1", pz: "10" }, cookie)
|
||||
if (!taskResultOk(msgRes)) continue
|
||||
var msgs = firstArray(nested(msgRes, ["Data", "MsgList"]), nested(msgRes, ["data", "MsgList"]), nested(msgRes, ["Data", "List"]), nested(msgRes, ["data", "List"]), nested(msgRes, ["Data", "Messages"]), nested(msgRes, ["data", "Messages"]))
|
||||
for (var j = 0; j < msgs.length; j++) {
|
||||
if (!recentCreateTime(field(msgs[j], ["CreateTime", "createTime"], ""))) continue
|
||||
var orderId = orderIdFromText(field(msgs[j], ["ActionUrl", "actionUrl", "RefUrl", "refUrl", "Content", "content", "Link", "link", "Url", "url"], ""))
|
||||
if (!orderId) continue
|
||||
var detail = qdGet("/argus/api/v1/interaction/rewarddetail", { orderId: orderId, refresh: "0" }, cookie)
|
||||
if (!taskResultOk(detail)) continue
|
||||
var data = detail.Data || detail.data || {}
|
||||
var chance = Number(data.ChanceCount || 0)
|
||||
var bookStatus = Number(data.BookStatus || 0)
|
||||
var bookId = data.BookId || ""
|
||||
if (data.Status !== undefined && Number(data.Status) !== 1) continue
|
||||
if (chance <= 0 && bookStatus !== 0 && bookStatus !== 1 && bookStatus !== 2) continue
|
||||
if (chance > 0) { var cb = qdPost("/argus/api/v1/interaction/rewardcallback", { orderId: orderId, checkRisk: "1" }, cookie); if (!riskConf(cb) && (taskResultOk(cb) || String(resultCode(cb)) === "0")) handled++; continue }
|
||||
if (bookStatus === 0 || bookStatus === 1) { if (!bookId) continue; var add = qdPost("/argus/api/v1/interaction/rewardaddshelf", { orderId: orderId, bookId: bookId }, cookie); if (!riskConf(add) && (taskResultOk(add) || String(resultCode(add)) === "0")) handled++; continue }
|
||||
if (bookStatus === 2) { var rc = qdPost("/argus/api/v1/interaction/rewardcallback", { orderId: orderId, checkRisk: "1" }, cookie); if (riskConf(rc) || (!taskResultOk(rc) && String(resultCode(rc)) !== "0")) continue; var r2 = qdPost("/argus/api/v1/interaction/rewardcallback", { orderId: orderId, checkRisk: "0" }, cookie); if (!riskConf(r2) && (taskResultOk(r2) || String(resultCode(r2)) === "0")) handled++ }
|
||||
}
|
||||
}
|
||||
return { name: "大咖荐书", ok: true, msg: handled ? ("成功处理 " + handled + " 个荐书") : "无待处理荐书", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" }
|
||||
}
|
||||
function doExtraTask(task, cookie) {
|
||||
if (task.handler === "receiveGifts") return doReceiveGifts(cookie)
|
||||
if (task.handler === "adv") return doAdvTask(cookie)
|
||||
if (task.handler === "daily") return doDailyTask(cookie)
|
||||
if (task.handler === "lottery") return doLotteryTask(cookie)
|
||||
if (task.handler === "exchange") return doWeeklyExchange(cookie)
|
||||
if (task.handler === "chapterCard") return doChapterCard(cookie)
|
||||
if (task.handler === "messageBox") return doMessageBox(cookie)
|
||||
var data = task.body || task.params || {}
|
||||
var res = qidianRequest(task.path, task.method || "GET", data, cookie)
|
||||
var ok = taskResultOk(res)
|
||||
var msg = resultMsg(res) || (ok ? "完成" : ("失败" + (resultCode(res) !== undefined ? " code=" + resultCode(res) : "")))
|
||||
return { name: task.label, ok: !!ok, msg: String(msg || ""), path: task.path }
|
||||
}
|
||||
function doTaskChain(uid, cookie, source, runId) {
|
||||
var startedAt = nowIso()
|
||||
var details = []
|
||||
var checkin = qidianRequest(CHECKIN_PATH, "POST", {}, cookie)
|
||||
if (isCookieExpiredResult(checkin)) {
|
||||
var expiredMsg = resultMsg(checkin) || "登录状态已失效,请更新 Cookie"
|
||||
var expiredDetail = { name: "签到结果", ok: false, msg: expiredMsg, path: CHECKIN_PATH, cookieExpired: true }
|
||||
var expiredResult = { uid: String(uid), ok: false, msg: expiredMsg, time: nowIso(), startedAt: startedAt, source: source || "manual", runId: runId || "", tasks: [expiredDetail], cookieExpired: true }
|
||||
setLastResult(uid, expiredResult)
|
||||
return { uid: uid, ok: false, msg: expiredMsg, raw: checkin, log: expiredResult, cookieExpired: true }
|
||||
}
|
||||
var ok = taskResultOk(checkin)
|
||||
var msg = resultMsg(checkin) || (ok ? "签到请求成功" : ("签到失败" + (resultCode(checkin) !== undefined ? " code=" + resultCode(checkin) : "")))
|
||||
details.push({ name: "签到结果", ok: !!ok, msg: String(msg || ""), path: CHECKIN_PATH })
|
||||
for (var i = 0; i < QD_TASKS.length; i++) {
|
||||
var task = QD_TASKS[i]
|
||||
if (!taskEnabledForUid(task, uid)) continue
|
||||
try { details.push(doExtraTask(task, cookie)) }
|
||||
catch (e) {
|
||||
var tmsg = e && e.message ? e.message : String(e)
|
||||
if (isWhitelistBlockedError(tmsg)) details.push({ name: task.label, ok: true, skipped: true, msg: "签名网关暂不支持该接口,已跳过", rawMsg: tmsg, path: task.path })
|
||||
else details.push({ name: task.label, ok: false, msg: tmsg, path: task.path })
|
||||
}
|
||||
}
|
||||
var failed = []
|
||||
for (var j = 0; j < details.length; j++) if (!details[j].ok && !details[j].skipped) failed.push(details[j].name + ":" + details[j].msg)
|
||||
var finalOk = failed.length === 0
|
||||
var detailMsg = []
|
||||
for (var k = 0; k < details.length; k++) detailMsg.push(details[k].name + (details[k].skipped ? "⏭" : (details[k].ok ? "✅" : "❌")) + (details[k].msg ? "(" + displayMsg(details[k].msg) + ")" : ""))
|
||||
var finalMsg = (finalOk ? "完全体任务完成" : "部分任务失败") + ":" + detailMsg.join(";")
|
||||
var result = { uid: String(uid), ok: !!finalOk, msg: String(finalMsg || ""), time: nowIso(), startedAt: startedAt, source: source || "manual", runId: runId || "", tasks: details }
|
||||
setLastResult(uid, result)
|
||||
return { uid: uid, ok: finalOk, msg: finalMsg, raw: checkin, log: result }
|
||||
}
|
||||
function doCheckin(uid, cookie, source, runId) {
|
||||
return doTaskChain(uid, cookie, source, runId)
|
||||
}
|
||||
function signAll(targetUid, allUsers, source, archiveAfterRun) {
|
||||
source = source || (allUsers ? "cron" : "manual")
|
||||
if (archiveAfterRun === undefined) archiveAfterRun = true
|
||||
var startedAt = nowIso()
|
||||
var runId = startedAt.replace(/[-:.TZ]/g, "").slice(0, 14) + "_" + source
|
||||
var store = getStore()
|
||||
@@ -497,46 +998,100 @@ function signAll(targetUid, allUsers, source) {
|
||||
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: [] })
|
||||
if (archiveAfterRun) archiveRunLog(source + "-after-sign")
|
||||
return emptyMsg
|
||||
}
|
||||
var okCount = 0
|
||||
var failCount = 0
|
||||
var items = []
|
||||
var lines = ["启点读书签到结果:"]
|
||||
var accountReplies = []
|
||||
var accountNotices = []
|
||||
for (var i = 0; i < uids.length; i++) {
|
||||
var uid = uids[i]
|
||||
var prefix = "启点读书签到结果:\n"
|
||||
var suffix = ""
|
||||
if (remarks[uid]) suffix = "(" + remarks[uid] + ")"
|
||||
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)
|
||||
var msg = prefix + (r.ok ? "✅ " : "❌ ") + maskUid(uid) + suffix + ":" + r.msg
|
||||
accountReplies.push(msg)
|
||||
accountNotices.push({ uid: String(uid), owner: bind[String(uid)] || "", msg: 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 errMsg = prefix + "❌ " + maskUid(uid) + suffix + ":" + emsg
|
||||
accountReplies.push(errMsg)
|
||||
accountNotices.push({ uid: String(uid), owner: bind[String(uid)] || "", msg: errMsg })
|
||||
}
|
||||
}
|
||||
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")
|
||||
var tail = []
|
||||
tail.push("汇总:成功 " + okCount + " / 失败 " + failCount + " / 总计 " + uids.length)
|
||||
tail.push("记录时间:" + finishedAt + "(" + sourceName(source) + ")")
|
||||
var archiveLine = ""
|
||||
if (archiveAfterRun) {
|
||||
var ar = archiveRunLog(source + "-after-sign")
|
||||
if (ar && ar.archived) { archiveLine = "日志已归档:" + ar.key; tail.push(archiveLine) }
|
||||
}
|
||||
if (allUsers && source === "cron") {
|
||||
for (var pi = 0; pi < accountNotices.length; pi++) {
|
||||
var notice = accountNotices[pi]
|
||||
var body = notice.msg + "\n" + tail.join("\n")
|
||||
if (!pushToUser(notice.owner, body)) debug("QDReader cron push skipped for uid " + notice.uid + ": empty owner or push unavailable")
|
||||
}
|
||||
return tail.join("\n")
|
||||
}
|
||||
if (accountReplies.length > 1) {
|
||||
for (var ai = 0; ai < accountReplies.length; ai++) reply(accountReplies[ai])
|
||||
return tail.join("\n")
|
||||
}
|
||||
return accountReplies[0] + "\n" + tail.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 shortTime(iso) {
|
||||
if (!iso) return "未知时间"
|
||||
var s = String(iso)
|
||||
var m = s.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})/)
|
||||
if (m) return m[1] + " " + m[2]
|
||||
return beijingTime(iso).slice(0, 16)
|
||||
}
|
||||
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 displayMsg(msg) {
|
||||
msg = trim(msg || "无结果")
|
||||
if (!msg) return "无结果"
|
||||
msg = msg.replace(/\s+/g, " ")
|
||||
if (msg.length > 60) msg = msg.slice(0, 57) + "..."
|
||||
return msg
|
||||
}
|
||||
function accountName(uid, remarks) {
|
||||
return maskUid(uid) + (remarks && remarks[uid] ? "[" + remarks[uid] + "]" : "")
|
||||
}
|
||||
function compactTaskMsg(msg) {
|
||||
msg = displayMsg(msg)
|
||||
msg = msg.replace(/^完全体任务完成:/, "")
|
||||
msg = msg.replace(/^部分任务失败:/, "")
|
||||
return msg
|
||||
}
|
||||
function formatResultLine(uid, r, remarks, emptyText) {
|
||||
var name = accountName(uid, remarks)
|
||||
if (!r) return "- " + name + ":⚪ " + (emptyText || "暂无执行记录")
|
||||
var status = r.ok ? "✅" : "❌"
|
||||
var line = "- " + name + ":" + status + " " + compactTaskMsg(r.msg)
|
||||
if (r.time) line += "\n 时间:" + shortTime(r.time)
|
||||
return line
|
||||
}
|
||||
function formatRunSummary(log, label, key) {
|
||||
log = log || {}
|
||||
var lines = []
|
||||
lines.push("批次:" + (key || log.runId || "最近") + "(" + (label || "最近记录") + ")")
|
||||
lines.push("时间:" + shortTime(log.finishedAt) + " | 来源:" + sourceName(log.source))
|
||||
lines.push("统计:成功 " + (log.okCount || 0) + " / 失败 " + (log.failCount || 0) + " / 总计 " + (log.total || 0))
|
||||
return lines
|
||||
}
|
||||
function queryStatus(targetUid) {
|
||||
var store = getStore()
|
||||
@@ -550,20 +1105,22 @@ function queryStatus(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]
|
||||
var lines = [targetUid ? "启点查询|单账号" : "启点查询|最近签到"]
|
||||
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 + "(发送启点查询可按归档判断昨日签到)")
|
||||
lines = lines.concat(formatRunSummary(arun, "归档", archiveInfo.key))
|
||||
lines.push("明细:")
|
||||
for (var ai = 0; ai < uids.length; ai++) lines.push(formatResultLine(uids[ai], archiveInfo.itemMap[String(uids[ai])], remarks, "归档中暂无该账号记录"))
|
||||
return lines.join("\n")
|
||||
}
|
||||
if (!targetUid && run && run.finishedAt) lines = lines.concat(formatRunSummary(run, "最近", ""))
|
||||
else if (run && run.finishedAt) lines.push("时间:" + shortTime(run.finishedAt) + " | 来源:" + sourceName(run.source))
|
||||
else lines.push("批次:暂无")
|
||||
lines.push("明细:")
|
||||
for (var i = 0; i < uids.length; i++) lines.push(formatResultLine(uids[i], last[uids[i]], remarks, "暂无执行记录"))
|
||||
if (archiveInfo) lines.push("归档:" + archiveInfo.key)
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
@@ -571,17 +1128,21 @@ 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 accountMenuText(store, bind) {
|
||||
var uids = visibleUids(store || getStore(), bind || getBindStore(), false)
|
||||
var remarks = remarksForStore(store || {})
|
||||
var lines = ["启点账号管理", "账号列表如下:"]
|
||||
if (!uids.length) lines.push("暂无账号,请回复 0 添加/更新账号")
|
||||
else {
|
||||
for (var i = 0; i < uids.length; i++) {
|
||||
var uid = uids[i]
|
||||
lines.push((i + 1) + "、" + maskUid(uid) + (remarks[uid] ? "[" + remarks[uid] + "]" : ""))
|
||||
}
|
||||
}
|
||||
lines.push("0、添加/更新账号")
|
||||
lines.push("-删除账号")
|
||||
lines.push("q、退出")
|
||||
return lines.join("\n")
|
||||
}
|
||||
function queryMenuText() {
|
||||
return [
|
||||
@@ -589,6 +1150,7 @@ function queryMenuText() {
|
||||
"1. 查看最近签到记录",
|
||||
"2. 查询单个账号最近记录",
|
||||
"3. 立即执行签到",
|
||||
"说明:附加任务开关在 启点账号 → 账号任务配置 中按账号管理",
|
||||
"q. 退出",
|
||||
"请回复序号"
|
||||
].join("\n")
|
||||
@@ -650,29 +1212,48 @@ function accountDetailText(uid, store, bind) {
|
||||
"uid: " + maskUid(uid),
|
||||
"备注: " + (remarks[uid] || "未设置"),
|
||||
"cookie: " + maskCookie(store[uid]),
|
||||
formatStatusLine(uid, last, remarks),
|
||||
taskStatusLines(uid).replace(/\n回复:[\s\S]*$/, ""),
|
||||
formatResultLine(uid, last[uid], remarks, "暂无执行记录"),
|
||||
"修改备注请回复新备注名,发送 清空 可删除备注,发送 q 退出"
|
||||
].join("\n")
|
||||
}
|
||||
function accountConfigText(uid, store, bind) {
|
||||
var detail = accountDetailText(uid, store, bind)
|
||||
if (/^(无权|未找到)/.test(detail)) return detail
|
||||
return detail + "\n\n账号操作:\n1、修改备注\n2、任务配置\n3、立即签到该账号\n-删除该账号\nq、返回/退出"
|
||||
}
|
||||
function accountConfigMenu(uid, store, bind) {
|
||||
if (!uid) return reply("未选择账号,已退出")
|
||||
var cfg = accountConfigText(uid, store, bind)
|
||||
reply(cfg)
|
||||
if (/^(无权|未找到)/.test(cfg)) return
|
||||
var op = listenInput(60000)
|
||||
if (!op || /^(q|退出|取消|返回)$/i.test(op)) return reply("已退出")
|
||||
if (/^(1|备注|改名|名称)$/i.test(op)) {
|
||||
reply("请回复新备注名;发送 清空 可删除备注;q 退出")
|
||||
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] + ")" : ""))
|
||||
}
|
||||
if (/^(2|任务|配置|开关)$/i.test(op)) return taskMenuForUid(uid)
|
||||
if (/^(3|签到|执行|立即)$/i.test(op)) return reply(signAll(uid, false, "menu"))
|
||||
if (/^-1$/.test(op) || /^(删除|delete|del|rm)$/i.test(op)) return reply(handleDelete("删除 " + uid, store, bind))
|
||||
return reply("未识别选项,已退出")
|
||||
}
|
||||
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] + ")" : ""))
|
||||
return accountConfigMenu(uid, store, bind)
|
||||
}
|
||||
function accountMenu(store, bind) {
|
||||
reply(accountMenuText())
|
||||
var uids = visibleUids(store, bind, false)
|
||||
reply(accountMenuText(store, bind))
|
||||
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)) {
|
||||
if (/^(0|a|add|添加|新增|更新)$/i.test(op)) {
|
||||
reply("请在60秒内发送 QX 抓到的完整 Cookie(q:退出)")
|
||||
var ck = listenInput(60000)
|
||||
if (!ck || /^(q|退出|取消)$/i.test(ck)) return reply("已退出")
|
||||
@@ -680,13 +1261,18 @@ function accountMenu(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))
|
||||
var n = parseInt(op, 10)
|
||||
if (!isNaN(n) && String(n) === op.replace(/^\+/, "")) {
|
||||
if (n > 0) {
|
||||
if (n > uids.length) return reply("编号无效,已退出")
|
||||
return accountConfigMenu(uids[n - 1], store, bind)
|
||||
}
|
||||
if (n < 0) {
|
||||
var idx = Math.abs(n)
|
||||
if (idx > uids.length) return reply("编号无效,已退出")
|
||||
return reply(handleDelete("删除 " + uids[idx - 1], store, bind))
|
||||
}
|
||||
}
|
||||
if (/^(5|清空|全部)$/i.test(op)) return reply(handleDelete("删除 all", store, bind))
|
||||
return reply("未识别选项,已退出")
|
||||
}
|
||||
function queryMenu() {
|
||||
@@ -712,13 +1298,15 @@ function handleDelete(arg, store, bind) {
|
||||
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)
|
||||
var allPrefs = getTaskPrefs(); for (var p = 0; p < uids.length; p++) delete allPrefs[uids[p]]
|
||||
setStore(store); setBindStore(bind); setRemarkStore(remarks); setTaskPrefs(allPrefs)
|
||||
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)
|
||||
var prefs = getTaskPrefs(); delete prefs[duid]
|
||||
setStore(store); setBindStore(bind); setRemarkStore(remarks); setTaskPrefs(prefs)
|
||||
return "已删除 uid: " + duid
|
||||
}
|
||||
function normalizeCookieInput(arg) {
|
||||
@@ -801,7 +1389,7 @@ function main() {
|
||||
}
|
||||
if (cmd === "account") { accountMenu(store, bind); return }
|
||||
if (cmd === "status") { queryMenu(); return }
|
||||
if (cmd === "ck" && /^(查询|状态|status|list|账号|管理)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;快速提交CK:启点ck <cookie>"); return }
|
||||
if (cmd === "ck" && /^(查询|状态|status|list|账号|管理|任务|配置)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;任务配置在 启点账号 → 账号任务配置;快速提交CK:启点ck <cookie>"); return }
|
||||
var delMsg = handleDelete(arg, store, bind)
|
||||
if (delMsg !== null) { reply(delMsg); return }
|
||||
if (cmd === "ck") {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
//[open_source: true]
|
||||
//[create_at: 2026-05-23 00:00:00]
|
||||
//[title: Autman 定时运行测试]
|
||||
//[author: Hermes]
|
||||
//[service: BOSS]
|
||||
//[class: 工具类]
|
||||
// [version: 1.0.0]
|
||||
// [platform: web,qq,wx,tg,tb,fs,we]
|
||||
// [public: false]
|
||||
// [price: 0]
|
||||
// [disable: false]
|
||||
// [description: 用于验证 Autman 当前版本定时插件入口、运行上下文和可用参数。论坛与现有插件约定使用 cron 头注;手动发送“定时测试”可交互返回诊断参数;定时每 5 分钟触发一次并打印 console.log。]
|
||||
// [rule: ^(定时测试|cron测试|task测试|autman定时测试)(.*)$]
|
||||
// [admin: true]
|
||||
// [priority: 1000]
|
||||
// [cron: */5 * * * *]
|
||||
|
||||
var PLUGIN_NAME = "Autman 定时运行测试"
|
||||
var PLUGIN_VERSION = "1.0.0"
|
||||
|
||||
function safeCall(label, fn) {
|
||||
try {
|
||||
if (typeof fn !== "function") return { ok: false, value: "<not function>" }
|
||||
var v = fn()
|
||||
if (v === undefined) return { ok: true, value: "<undefined>" }
|
||||
if (v === null) return { ok: true, value: "<null>" }
|
||||
return { ok: true, value: String(v) }
|
||||
} catch (e) {
|
||||
return { ok: false, value: "ERROR: " + (e && e.message ? e.message : String(e)) }
|
||||
}
|
||||
}
|
||||
|
||||
function hasFn(name) {
|
||||
try { return typeof this[name] === "function" } catch (e) { return false }
|
||||
}
|
||||
|
||||
function valueOf(name) {
|
||||
try {
|
||||
var v = this[name]
|
||||
if (typeof v === "function") return "<function>"
|
||||
if (v === undefined) return "<undefined>"
|
||||
if (v === null) return "<null>"
|
||||
if (typeof v === "object") return JSON.stringify(v)
|
||||
return String(v)
|
||||
} catch (e) {
|
||||
return "ERROR: " + (e && e.message ? e.message : String(e))
|
||||
}
|
||||
}
|
||||
|
||||
function callInfo(param) {
|
||||
return safeCall("call(" + param + ")", function () {
|
||||
if (typeof call !== "function") return "<call not function>"
|
||||
var f = call(param)
|
||||
if (typeof f !== "function") return "<call result not function>"
|
||||
if (param === "timeFormat") return f("yyyy-MM-dd hh:mm:ss")
|
||||
return f()
|
||||
}).value
|
||||
}
|
||||
|
||||
function getContentCompat() {
|
||||
var r = safeCall("GetContent", function () { return GetContent() })
|
||||
if (r.ok && r.value !== "<not function>" && r.value !== "<undefined>" && r.value !== "<null>") return r.value
|
||||
r = safeCall("getContent", function () { return getContent() })
|
||||
if (r.ok && r.value !== "<not function>" && r.value !== "<undefined>" && r.value !== "<null>") return r.value
|
||||
return ""
|
||||
}
|
||||
|
||||
function detectImType() {
|
||||
var r = safeCall("ImType", function () { return ImType() })
|
||||
if (r.ok && r.value !== "<not function>" && r.value !== "<undefined>" && r.value !== "<null>" && r.value !== "") return r.value
|
||||
r = safeCall("GetImType", function () { return GetImType() })
|
||||
if (r.ok && r.value !== "<not function>" && r.value !== "<undefined>" && r.value !== "<null>" && r.value !== "") return r.value
|
||||
return ""
|
||||
}
|
||||
|
||||
function isCronRun(content, imType) {
|
||||
var t = String(imType || "").toLowerCase()
|
||||
if (t === "cron" || t === "task") return true
|
||||
// 只作兜底:部分版本定时触发没有消息内容。
|
||||
return !content
|
||||
}
|
||||
|
||||
function replyText(text) {
|
||||
try { if (typeof sendText === "function") { sendText(text); return } } catch (e) {}
|
||||
try { if (typeof sender !== "undefined" && sender && typeof sender.reply === "function") { sender.reply(text); return } } catch (e2) {}
|
||||
try { if (typeof Sender !== "undefined" && Sender && typeof Sender.Reply === "function") { Sender.Reply(text); return } } catch (e3) {}
|
||||
try { if (typeof console !== "undefined" && console && typeof console.log === "function") console.log(text) } catch (e4) {}
|
||||
}
|
||||
|
||||
function buildReport() {
|
||||
var content = getContentCompat()
|
||||
var imType = detectImType()
|
||||
var cronRun = isCronRun(content, imType)
|
||||
var lines = []
|
||||
lines.push("【" + PLUGIN_NAME + " v" + PLUGIN_VERSION + "】")
|
||||
lines.push("触发类型: " + (cronRun ? "定时触发" : "交互触发"))
|
||||
lines.push("调度头注: // [cron: */5 * * * *]")
|
||||
lines.push("说明: 当前论坛/仓库约定使用 cron;task 仅作为兼容诊断值检测。")
|
||||
lines.push("")
|
||||
lines.push("-- 基础上下文 --")
|
||||
lines.push("content: " + JSON.stringify(content))
|
||||
lines.push("ImType/GetImType: " + JSON.stringify(imType))
|
||||
lines.push("GetUserID: " + safeCall("GetUserID", function () { return GetUserID() }).value)
|
||||
lines.push("GetChatID: " + safeCall("GetChatID", function () { return GetChatID() }).value)
|
||||
lines.push("isAdmin: " + safeCall("isAdmin", function () { return isAdmin() }).value)
|
||||
lines.push("")
|
||||
lines.push("-- call() 信息 --")
|
||||
lines.push("name: " + callInfo("name"))
|
||||
lines.push("version: " + callInfo("version"))
|
||||
lines.push("port: " + callInfo("port"))
|
||||
lines.push("machineId: " + callInfo("machineId"))
|
||||
lines.push("time: " + callInfo("timeFormat"))
|
||||
lines.push("")
|
||||
lines.push("-- 函数存在性 --")
|
||||
lines.push("cron API: " + (hasFn("cron") ? "function" : valueOf("cron")))
|
||||
lines.push("GetContent: " + (hasFn("GetContent") ? "function" : valueOf("GetContent")))
|
||||
lines.push("getContent: " + (hasFn("getContent") ? "function" : valueOf("getContent")))
|
||||
lines.push("ImType: " + (hasFn("ImType") ? "function" : valueOf("ImType")))
|
||||
lines.push("GetImType: " + (hasFn("GetImType") ? "function" : valueOf("GetImType")))
|
||||
lines.push("sendText: " + (hasFn("sendText") ? "function" : valueOf("sendText")))
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
function main() {
|
||||
var report = buildReport()
|
||||
// 关键:交互运行或定时运行时,都先写 console.log,方便在 Autman 日志里确认 main 是否被调用及上下文参数。
|
||||
try { if (typeof console !== "undefined" && console && typeof console.log === "function") console.log(report) } catch (e) {}
|
||||
replyText(report)
|
||||
}
|
||||
|
||||
main()
|
||||
+73
-31
@@ -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 7 * * *`:每日 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: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
|
||||
```
|
||||
|
||||
## 当前交互设计
|
||||
@@ -36,18 +32,7 @@ https://gitea.chickliu.fun/Hermes/autman-plugins/raw/branch/main/js/qdreader_arc
|
||||
启点查询
|
||||
```
|
||||
|
||||
`启点账号` 只负责账号管理:
|
||||
|
||||
```text
|
||||
启点账号管理
|
||||
1. 查看账号
|
||||
2. 添加/更新账号
|
||||
3. 账号详情/修改备注
|
||||
4. 删除账号
|
||||
5. 清空账号
|
||||
q. 退出
|
||||
请回复序号
|
||||
```
|
||||
`启点账号` 负责账号管理和单账号任务配置:
|
||||
|
||||
`启点查询` 只负责最后执行日志和执行动作,不做实时接口查询:
|
||||
|
||||
@@ -60,6 +45,36 @@ q. 退出
|
||||
请回复序号
|
||||
```
|
||||
|
||||
`启点账号` 内的“账号任务配置”负责完整任务模块开关,开关直接写入对应账号:
|
||||
|
||||
```text
|
||||
启点账号管理
|
||||
1. 查看账号
|
||||
2. 添加/更新账号
|
||||
3. 账号详情/修改备注
|
||||
4. 账号任务配置
|
||||
5. 删除账号
|
||||
6. 清空账号
|
||||
q. 退出
|
||||
请回复序号
|
||||
```
|
||||
|
||||
选择账号后显示该账号自己的任务开关:
|
||||
|
||||
```text
|
||||
启点账号任务开关:12***45
|
||||
1. 领取奖励:开启(固定)
|
||||
2. 激励任务:关闭
|
||||
3. 每日任务:关闭
|
||||
4. 每日抽奖:关闭
|
||||
5. 每周兑换:关闭(仅周日执行)
|
||||
6. 章节卡片:关闭
|
||||
7. 大咖荐书:关闭
|
||||
回复:序号 on/off 切换,例如 2 on;all on 一键开启;all off 一键关闭;q 退出
|
||||
```
|
||||
|
||||
默认只执行签到 + 领取奖励;其余任务按上游完全体模块接入但默认关闭,避免广告/抽奖/兑换类接口在未确认前自动运行。可用 `all on` / `all off` 对选中账号一键开启/关闭全部可选任务;开启后只对该账号生效,签到链路会按账号读取自己的任务配置,单个模块失败会记录为该账号“部分任务失败”,不会暴露 Cookie。
|
||||
|
||||
### 快速通道
|
||||
|
||||
```text
|
||||
@@ -71,7 +86,7 @@ qdreader sign
|
||||
qdreader archive
|
||||
```
|
||||
|
||||
`启点ck` 只保留快速提交 Cookie,不再承担查询/导出/批量导入等管理功能。
|
||||
`启点ck` 只保留快速提交 Cookie,不再承担查询/导出/批量导入等管理功能。`启点任务` 入口已取消,附加任务开关统一放在 `启点账号` 的单账号配置里。
|
||||
|
||||
## 账号与备注
|
||||
|
||||
@@ -99,12 +114,19 @@ hermes_qidian
|
||||
qdreader_cookie_json # uid -> full cookie
|
||||
qdreader_user_bind_json # uid -> sender.getUserID(),用户绑定索引
|
||||
qdreader_remark_json # uid -> 备注名
|
||||
qdreader_last_result_json # uid -> 单账号当日/当前批次结果 JSON 字符串(每日零点归档前清空)
|
||||
qdreader_last_run_json # 当日/当前批次日志、来源和汇总(每日零点归档前清空)
|
||||
qdreader_last_result_json # uid -> 单账号当前批次结果 JSON 字符串(签到运行后归档并清空)
|
||||
qdreader_last_run_json # 当前批次日志、来源和汇总(签到运行后归档并清空)
|
||||
qdreader_archive_json # yyyy-mm-dd -> 已归档批次日志,用于判断历史签到结果
|
||||
qdreader_sign_api # 签名网关
|
||||
qdreader_retry_count # 请求失败重试次数
|
||||
qdreader_cron_silent # 定时无账号静默
|
||||
qdreader_adv_job_enable # 激励任务开关,默认 false
|
||||
qdreader_extra_adv_job_enable # 每日任务开关,默认 false
|
||||
qdreader_lottery_enable # 每日抽奖开关,默认 false
|
||||
qdreader_weekly_exchange_enable # 每周兑换开关,默认 false
|
||||
qdreader_chapter_card_enable # 章节卡片开关,默认 false
|
||||
qdreader_message_box_enable # 旧全局大咖荐书开关,v2.7 起不再作为运行依据
|
||||
qdreader_task_pref_json # uid -> { taskKey: boolean },每个账号自己的附加任务开关
|
||||
```
|
||||
|
||||
读取顺序:
|
||||
@@ -134,12 +156,22 @@ set(key, value)
|
||||
- 同时提取 `qdh/QDHeader/guid/qimei/qimei36` 供签名网关使用,其中 `qdh` 只取 Cookie 的 `qdh/QDH`,不再用 `QDHeader` 顶替,避免签名网关 `decodeQDHAuto` 解码失败。
|
||||
2. `POST <qdreader_sign_api>/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`,包括触发来源、开始/结束时间、成功/失败/总数和每账号结果;即使账号执行异常也会记录失败日志。
|
||||
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 表达式兼容不一致。
|
||||
9. 汇总每个账号签到结果。
|
||||
4. “今日已签到/已经签到/重复签到”等签到响应按成功处理,避免重复执行时误计失败。
|
||||
5. 按账号读取 `qdreader_task_pref_json` 后追加完全体任务链路:
|
||||
- 固定执行领取奖励:`POST /argus/api/v3/checkin/receivegifts`;
|
||||
- 可选激励任务:严格按上游原项目控制流执行:`GET /argus/api/v2/video/adv/mainPage` 读取 `Data.VideoRewardTab.TaskList`;若最后一项已完成则直接视为完成;否则按顺序对每个未完成且带 `TaskId` 的条目各调用一次 `POST /argus/api/v1/video/adv/finishWatch`;上游返回 `result=9` 表示视频完成已受理;最后再次拉取 `mainPage`,只按回查后最后一项是否完成判断激励任务是否完成。这里不做本地推测过滤、不按已提交 TaskId 单独验收、不输出“跳过其它入口任务”。
|
||||
- 可选每日任务:`GET /argus/api/v3/checkin/packagelist`,先处理惊喜福利/前置任务,再按标题精确选择“每日任务”执行三次 `finishWatch` 并回查完成状态;
|
||||
- 可选每日抽奖:先 `GET /argus/api/v2/checkin/detail`,有次数时 `POST /argus/api/v2/checkin/lottery`;
|
||||
- 可选每周兑换:`GET /argus/api/v3/checkin/checkinexchangepage`,可兑换时 `POST /argus/api/v3/checkin/exchangegoods`,仅北京时间周日执行;
|
||||
- 可选章节卡片:`GET /argus/api/v1/readtime/scoremall/chaptercardwithbook`;
|
||||
- 可选大咖荐书:按上游逻辑用 `UserId` 生成 base62 消息 token,先 `pullmessage` 拉会话列表,再带 `senderId` 拉消息、提取最近消息里的 `orderId`,随后调用 `/argus/api/v2/recommend/getrewarddetail` 和 `rewardcallback/addbookshelf` 处理奖励;不是只请求一次 `pullmessage`。
|
||||
6. 对签名网关返回 `url 不在允许的接口列表内` 的可选任务,记为“已跳过”而不是账号失败,用于兼容网关白名单尚未覆盖的上游接口。
|
||||
7. 写入单账号当日结果 `qdreader_last_result_json`,包括 `ok/msg/time/source/runId/tasks`。
|
||||
8. 写入当日批次日志 `qdreader_last_run_json`,包括触发来源、开始/结束时间、成功/失败/总数和每账号结果;即使账号执行异常也会记录失败日志。
|
||||
9. 插件内只有一个 cron:`1 7 * * *`。
|
||||
10. 每日 07:01 触发时执行签到链路。
|
||||
11. 签到运行结束后,马上把本次 `qdreader_last_run_json` 按完成日期写入 `qdreader_archive_json`,再清空 `qdreader_last_result_json` / `qdreader_last_run_json`;手动签到同样会立即归档。
|
||||
12. 汇总每个账号签到/完整任务结果,并在回复中提示归档日期。
|
||||
|
||||
## 查询数据来源
|
||||
|
||||
@@ -148,17 +180,27 @@ set(key, value)
|
||||
```text
|
||||
qdreader_last_run_json # 当日/当前批次
|
||||
qdreader_last_result_json # 每个 uid 当日/当前批次结果
|
||||
qdreader_archive_json # 每日零点归档后的历史批次
|
||||
qdreader_archive_json # 每次签到后立即归档的历史批次
|
||||
```
|
||||
|
||||
因此“查询”语义是“最近签到记录/最后执行日志”。刚添加但从未执行过的账号会显示“暂无执行记录”。如果当日日志已被零点任务清空,`启点查询` 会回退到最近归档,根据归档中的账号结果判断昨日/历史签到情况并反馈给用户。
|
||||
因此“查询”语义是“最近签到记录/最后执行日志”。刚添加但从未执行过的账号会显示“暂无执行记录”。因为签到完成后最近日志会立即归档并清空,`启点查询` 会回退到最近归档,根据归档中的账号结果判断最近签到情况并反馈给用户。查询输出会加工成“批次/来源/汇总/结果明细”结构,时间压缩到分钟,过长的接口错误/原始响应会截断,避免刷屏。所有日志时间和归档日期统一按北京时间计算,避免 UTC 跨日导致日期错位。
|
||||
|
||||
## 本地验证
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
|
||||
## v2.10.1 现场失败修正
|
||||
|
||||
- `finishWatch` 的上游/签名网关响应可能使用小写 `result=9`,现在和 `Result=9` 一样按“已受理,需回查”处理,避免激励任务误报 `任务 ... 失败`。
|
||||
- 大咖荐书继续使用上游 base62 token,不再回退十进制 token;若线上仍返回“参数错误”,优先确认 Autman 已重新导入本版本,而不是旧版只请求一次 `pullmessage`。
|
||||
|
||||
|
||||
## v2.10.2 现场失败修正
|
||||
|
||||
- `finishWatch` 兼容响应码嵌在 `data/Data.result` 的网关包装形态,避免空 message 时继续误报具体任务失败。
|
||||
- 大咖荐书恢复执行时间窗口限制:仅北京时间 20:00-22:00 执行,其他时间标记为跳过,不再调用消息接口导致“参数错误”。
|
||||
|
||||
@@ -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 <QX抓到的cookie>",
|
||||
"直接签到:启点签到",
|
||||
"指定签到:启点签到 <uid>",
|
||||
"【配置】",
|
||||
"签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API,
|
||||
"定时:每日 00:01 只归档并清空日志;每日 07:01 只自动签到;QX 只负责抓 cookie。"
|
||||
].join("\n")
|
||||
}
|
||||
function stripCommand(text) {
|
||||
var s = String(text || "")
|
||||
if (/^启点(?:签到|读书)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:签到|读书)\s*/i, ""))
|
||||
if (/^启点(?:归档|日志归档)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:归档|日志归档)\s*/i, ""))
|
||||
if (/^启点(?:查询)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:查询)\s*/i, ""))
|
||||
if (/^启点(?:账号)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:账号)\s*/i, ""))
|
||||
if (/^启点(?:ck|CK|cookie|Cookie)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:ck|CK|cookie|Cookie)\s*/i, ""))
|
||||
return trim(s.replace(/^qdreader\s*/i, ""))
|
||||
}
|
||||
function commandName(text) {
|
||||
if (/^启点(?:签到|读书)(?:\s|$)/i.test(text)) return "sign"
|
||||
if (/^启点(?:归档|日志归档)(?:\s|$)/i.test(text)) return "archive"
|
||||
if (/^qdreader\s+sign(?:\s|$)/i.test(text)) return "sign"
|
||||
if (/^qdreader\s+archive(?:\s|$)/i.test(text)) return "archive"
|
||||
if (/^启点(?:查询)(?:\s|$)/i.test(text)) return "status"
|
||||
if (/^启点(?:账号)(?:\s|$)/i.test(text)) return "account"
|
||||
return "ck"
|
||||
}
|
||||
function qs(obj) {
|
||||
var parts = []
|
||||
for (var k in (obj || {})) {
|
||||
if (!obj.hasOwnProperty(k)) continue
|
||||
var v = obj[k]
|
||||
if (v === undefined || v === null) continue
|
||||
if (Array.isArray(v)) { for (var i = 0; i < v.length; i++) parts.push(encodeURIComponent(k + "[" + i + "]") + "=" + encodeURIComponent(String(v[i]))) }
|
||||
else if (typeof v === "object") { for (var sk in v) if (v.hasOwnProperty(sk)) parts.push(encodeURIComponent(k + "[" + sk + "]") + "=" + encodeURIComponent(String(v[sk]))) }
|
||||
else parts.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(v)))
|
||||
}
|
||||
return parts.join("&")
|
||||
}
|
||||
function normalizeCookie(cookie) {
|
||||
var jar = parseCookieString(cookie)
|
||||
var session = jar.sessionyw || jar.session || jar.sessionKey || ""
|
||||
if (session) jar.sessionyw = session
|
||||
var appid = jar.appId || jar.appid || ""
|
||||
if (appid) jar.appId = appid
|
||||
var guid = jar.ywguid || jar.gwguid || jar.guid || ""
|
||||
var qdh = jar.qdh || jar.QDH || ""
|
||||
var qdheader = jar.QDHeader || jar.qdheader || jar.QDHEADER || ""
|
||||
return { jar: jar, cookie: cookieToString(jar), qdh: qdh, qdheader: qdheader, uid: extractUid(cookie), guid: guid, qimei: jar.qimei || jar.QIMEI || "", qimei36: jar.qimei36 || jar.QIMEI36 || "" }
|
||||
}
|
||||
function httpReq(opt) {
|
||||
if (!opt.headers) opt.headers = {}
|
||||
if (typeof request === "function") return request(opt)
|
||||
if (typeof $request === "function") return $request(opt)
|
||||
throw new Error("当前 Autman 环境没有 request/$request 方法,无法发起签到请求")
|
||||
}
|
||||
function parseResp(res) {
|
||||
if (res === undefined || res === null) return null
|
||||
var body = null
|
||||
if (res && typeof res === "object") {
|
||||
if (res.body !== undefined) body = res.body
|
||||
else if (res.data !== undefined) body = res.data
|
||||
else if (res.content !== undefined) body = res.content
|
||||
else if (res.responseText !== undefined) body = res.responseText
|
||||
else if (res.statusCode !== undefined || res.status !== undefined || res.headers !== undefined) body = ""
|
||||
else body = res
|
||||
} else {
|
||||
body = res
|
||||
}
|
||||
if (typeof body === "string") return safeJsonParse(body, body)
|
||||
return body
|
||||
}
|
||||
function requestWithRetry(opt, retryCount) {
|
||||
var lastErr = null
|
||||
var attempts = Math.max(1, (retryCount || 0) + 1)
|
||||
for (var i = 0; i < attempts; i++) {
|
||||
try {
|
||||
return httpReq(opt)
|
||||
} catch (e) {
|
||||
lastErr = e
|
||||
debug("QDReader request retry " + (i + 1) + "/" + attempts + " failed: " + (e && e.message ? e.message : String(e)))
|
||||
}
|
||||
}
|
||||
throw lastErr || new Error("请求失败")
|
||||
}
|
||||
function postJson(url, data) {
|
||||
var retry = getInt(RETRY_KEY, 1, 0, 3)
|
||||
return parseResp(requestWithRetry({ url: url, method: "post", dataType: "json", timeOut: 10000, headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify(data) }, retry))
|
||||
}
|
||||
function normalizeSignHeaders(signed) {
|
||||
if (typeof signed === "string") signed = safeJsonParse(signed, {})
|
||||
if (!signed) throw new Error("签名网关无返回")
|
||||
var code = signed.code !== undefined ? signed.code : (signed.retcode !== undefined ? signed.retcode : signed.status)
|
||||
var ok = code === undefined || code === 0 || code === "0" || code === 200 || code === "200" || code === "ok" || code === true
|
||||
var data = signed.data || signed.result || signed.headers || signed
|
||||
var headers = data && (data.headers || data.header || data)
|
||||
if (!ok || !isObject(headers)) throw new Error("签名失败:" + JSON.stringify(signed).slice(0, 500))
|
||||
return headers
|
||||
}
|
||||
function qidianRequest(path, method, data, cookie) {
|
||||
method = String(method || "GET").toUpperCase()
|
||||
var info = normalizeCookie(cookie)
|
||||
if (!info.uid) throw new Error("Cookie 缺少 uid/QDHeader")
|
||||
var signApi = String(cfgGet(SIGN_API_KEY, DEFAULT_SIGN_API)).replace(/\/+$/, "")
|
||||
var payload = { qdh: info.qdh, qdheader: info.qdheader, url: QIDIAN_BASE + path, method: method, data: data || {}, uid: info.uid, guid: info.guid, qimei: info.qimei, qimei36: info.qimei36 }
|
||||
var headers = normalizeSignHeaders(postJson(signApi + "/sign", payload))
|
||||
headers.Cookie = info.cookie
|
||||
if (!headers["User-Agent"]) headers["User-Agent"] = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/QDReaderiOS/5.9.456/728/QDReaderAppstore"
|
||||
var req = { url: QIDIAN_BASE + path, method: lower(method), dataType: "json", timeOut: 10000, headers: headers }
|
||||
if (method === "GET") {
|
||||
var query = qs(data || {})
|
||||
if (query) req.url += (req.url.indexOf("?") >= 0 ? "&" : "?") + query
|
||||
} else {
|
||||
req.headers["Content-Type"] = req.headers["Content-Type"] || "application/x-www-form-urlencoded"
|
||||
req.body = qs(data || {})
|
||||
}
|
||||
var retry = getInt(RETRY_KEY, 1, 0, 3)
|
||||
var out = parseResp(requestWithRetry(req, retry))
|
||||
if (typeof out === "string") out = safeJsonParse(out, out)
|
||||
return out
|
||||
}
|
||||
function resultCode(obj) { return obj && (obj.Result !== undefined ? obj.Result : obj.Code !== undefined ? obj.Code : obj.code !== undefined ? obj.code : obj.retcode) }
|
||||
function resultOk(obj) {
|
||||
var code = resultCode(obj)
|
||||
var status = obj && (obj.status || obj.Status)
|
||||
return code === undefined || code === 0 || code === "0" || code === 200 || code === "200" || status === "ok" || status === "success"
|
||||
}
|
||||
function resultMsg(obj) {
|
||||
if (!obj) return ""
|
||||
return obj.Message || obj.Msg || obj.message || obj.msg || obj.toast || (obj.data && (obj.data.message || obj.data.msg || obj.data.toast)) || ""
|
||||
}
|
||||
function nowIso() { try { return new Date().toISOString() } catch (e) { return String(new Date()) } }
|
||||
function setLastResult(uid, result) {
|
||||
var obj = cleanStringMap(safeJsonParse(cfgGet(LAST_RESULT_KEY, "{}"), {}))
|
||||
obj[String(uid)] = JSON.stringify(result || {})
|
||||
cfgSet(LAST_RESULT_KEY, JSON.stringify(obj))
|
||||
}
|
||||
function getLastResults() {
|
||||
var raw = cleanStringMap(safeJsonParse(cfgGet(LAST_RESULT_KEY, "{}"), {}))
|
||||
var out = {}
|
||||
for (var uid in raw) if (raw.hasOwnProperty(uid)) out[uid] = safeJsonParse(raw[uid], { msg: raw[uid] })
|
||||
return out
|
||||
}
|
||||
function setLastRunLog(log) { cfgSet(LAST_RUN_KEY, JSON.stringify(log || {})) }
|
||||
function getLastRunLog() { return safeJsonParse(cfgGet(LAST_RUN_KEY, "{}"), {}) || {} }
|
||||
function getArchiveStore() { return safeJsonParse(cfgGet(ARCHIVE_KEY, "{}"), {}) || {} }
|
||||
function setArchiveStore(obj) { cfgSet(ARCHIVE_KEY, JSON.stringify(obj || {})) }
|
||||
function localDateKey(iso) {
|
||||
var d = iso ? new Date(iso) : new Date()
|
||||
if (!d || isNaN(d.getTime())) d = new Date()
|
||||
var y = d.getFullYear()
|
||||
var m = String(d.getMonth() + 1); if (m.length < 2) m = "0" + m
|
||||
var day = String(d.getDate()); if (day.length < 2) day = "0" + day
|
||||
return y + "-" + m + "-" + day
|
||||
}
|
||||
function archiveRunLog(reason) {
|
||||
var run = getLastRunLog()
|
||||
var archive = getArchiveStore()
|
||||
var archived = false
|
||||
var key = ""
|
||||
if (run && run.finishedAt && run.total !== undefined) {
|
||||
key = localDateKey(run.finishedAt)
|
||||
archive[key] = run
|
||||
archive[key].archivedAt = nowIso()
|
||||
archive[key].archiveReason = reason || "daily-rollover"
|
||||
archived = true
|
||||
}
|
||||
setArchiveStore(archive)
|
||||
cfgSet(LAST_RESULT_KEY, "{}")
|
||||
cfgSet(LAST_RUN_KEY, "{}")
|
||||
return { archived: archived, key: key, archive: archive, run: run || {} }
|
||||
}
|
||||
function latestArchiveForVisible(uids) {
|
||||
var archive = getArchiveStore()
|
||||
var keys = []
|
||||
for (var k in archive) if (archive.hasOwnProperty(k)) keys.push(k)
|
||||
keys.sort()
|
||||
for (var i = keys.length - 1; i >= 0; i--) {
|
||||
var log = archive[keys[i]] || {}
|
||||
var items = log.items || []
|
||||
var map = {}
|
||||
for (var j = 0; j < items.length; j++) map[String(items[j].uid)] = items[j]
|
||||
if (!uids || !uids.length) return { key: keys[i], log: log, itemMap: map }
|
||||
for (var u = 0; u < uids.length; u++) if (map[String(uids[u])]) return { key: keys[i], log: log, itemMap: map }
|
||||
}
|
||||
return null
|
||||
}
|
||||
function cronMode() {
|
||||
var d = new Date()
|
||||
var h = d.getHours ? d.getHours() : 7
|
||||
// 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 <QX抓到的完整cookie>", uids: [] }
|
||||
var me = currentOwnerId()
|
||||
if (!me) return { ok: false, text: "无法识别当前用户,已拒绝保存CK,避免错误绑定。请确认插件在聊天会话中触发;诊断: " + currentUserDebug(), uids: [] }
|
||||
var lines = []
|
||||
var savedUids = []
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var cookie = entries[i].cookie
|
||||
if (cookie.indexOf("=") < 0 || cookie.indexOf(";") < 0) return { ok: false, text: "未识别到 Cookie。请发送:启点ck {\"uid\":\"cookie\"} 或 启点ck <QX抓到的完整cookie>", uids: savedUids }
|
||||
var parsedUid = extractUid(cookie)
|
||||
var uid = entries[i].uid || parsedUid
|
||||
if (!uid) return { ok: false, text: "无法从 Cookie 识别 uid。请确认 QX 抓的是启点读书福利中心 getlogininfo 请求,Cookie 内应包含 QDHeader 或 uid。", uids: savedUids }
|
||||
var isNew = !store[uid]
|
||||
var oldOwner = bind[uid] || ""
|
||||
if (!store[uid] && oldOwner && oldOwner !== me) {
|
||||
delete bind[uid]
|
||||
oldOwner = ""
|
||||
lines.push("检测到 uid: " + uid + " 只有旧绑定没有CK,已自动清理残留绑定")
|
||||
}
|
||||
if (store[uid] && oldOwner && oldOwner !== me && canRepairBinding(oldOwner, me)) {
|
||||
lines.push("检测到 uid: " + uid + " 旧绑定来自历史兜底标识 " + maskUid(oldOwner) + ",已修复为当前用户")
|
||||
oldOwner = ""
|
||||
}
|
||||
if (store[uid] && oldOwner && me && oldOwner !== me) {
|
||||
lines.push("无权更新 uid: " + uid + "(该账号已绑定其他用户)")
|
||||
continue
|
||||
}
|
||||
var changed = store[uid] !== cookie
|
||||
store[uid] = cookie
|
||||
bind[uid] = me
|
||||
savedUids.push(uid)
|
||||
lines.push("启点读书CK" + (isNew ? "新增" : (changed ? "更新" : "未变化")) + "成功")
|
||||
lines.push("uid: " + uid)
|
||||
if (entries.length > 1 && parsedUid && parsedUid !== uid) lines.push("提示: JSON uid 与 Cookie 解析 uid 不一致,已按 JSON uid 保存")
|
||||
}
|
||||
setStore(store); setBindStore(bind)
|
||||
lines.push(me ? ("绑定用户: " + maskUid(me)) : "绑定用户: 未识别")
|
||||
lines.push("账号数: " + visibleUids(store, bind, false).length)
|
||||
lines.push("cookie: " + maskCookie(entries[0].cookie))
|
||||
lines.push("签到:发送 启点签到")
|
||||
return { ok: savedUids.length > 0, text: lines.join("\n"), uids: savedUids }
|
||||
}
|
||||
function main() {
|
||||
var text = contentText()
|
||||
var isCronRun = !text
|
||||
var cmd = isCronRun ? cronMode() : commandName(text)
|
||||
var arg = isCronRun ? "" : trim(stripCommand(text).replace(/^(sign|archive)\s*/i, ""))
|
||||
if (!arg && cmd !== "sign" && cmd !== "archive" && cmd !== "account" && cmd !== "status") { reply(usage()); return }
|
||||
if (/^(帮助|help|h)$/i.test(arg)) { reply(usage()); return }
|
||||
|
||||
var store = getStore()
|
||||
var bind = getBindStore()
|
||||
if (cmd === "archive" || /^(归档|日志归档|archive)(?:\s+|$)/i.test(arg)) {
|
||||
reply(formatArchiveReply(archiveRunLog(isCronRun ? "midnight-archive" : "manual-archive")))
|
||||
return
|
||||
}
|
||||
if (cmd === "sign" || /^(签到|sign|run)(?:\s+|$)/i.test(arg)) {
|
||||
var target = trim(arg.replace(/^(签到|sign|run)\s*/i, ""))
|
||||
var out = signAll(target, isCronRun, isCronRun ? "cron" : "manual")
|
||||
if (!(isCronRun && getBool(CRON_SILENT_KEY, false) && out === "还没有保存启点读书CK")) reply(out)
|
||||
return
|
||||
}
|
||||
if (cmd === "account") { accountMenu(store, bind); return }
|
||||
if (cmd === "status") { queryMenu(); return }
|
||||
if (cmd === "ck" && /^(查询|状态|status|list|账号|管理)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;快速提交CK:启点ck <cookie>"); return }
|
||||
var delMsg = handleDelete(arg, store, bind)
|
||||
if (delMsg !== null) { reply(delMsg); return }
|
||||
if (cmd === "ck") {
|
||||
var save = handleSaveCookie(arg, store, bind)
|
||||
if (save.ok && save.uids.length === 1) save.text += maybePromptRemark(save.uids[0], store)
|
||||
reply(save.text)
|
||||
return
|
||||
}
|
||||
var fallbackSave = handleSaveCookie(arg, store, bind)
|
||||
reply(fallbackSave.text)
|
||||
}
|
||||
main()
|
||||
@@ -4,17 +4,17 @@
|
||||
//[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) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”;定时签到使用独立 cron `1 7 * * *`;日志归档由 `qdreader_archive_autman.js` 独立 cron `1 0 * * *` 执行,避免 Autman 对逗号 hour cron 兼容不一致导致定时不触发;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。]
|
||||
//[rule: ^启点(账号|查询|ck|CK|cookie|Cookie|签到|读书|归档|日志归档)(.*)$]
|
||||
//[rule: ^qdreader(.*)$]
|
||||
//[admin: true]
|
||||
//[priority: 600]
|
||||
//[cron: 1 7 * * *]
|
||||
// [version: 2.15.7]
|
||||
// [platform: web,qq,wx,tg,tb,fs,we]
|
||||
// [public: false]
|
||||
// [price: 0]
|
||||
// [disable: false]
|
||||
// [description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”;定时使用 `1 7 * * *` 每日 07:01 签到;完全体可选任务按上游原项目解密流程执行;每次签到运行结束后立即归档并清空最近运行日志,不再依赖单独归档定时;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。]
|
||||
// [rule: ^启点(账号|查询|ck|CK|cookie|Cookie|签到|读书|归档|日志归档)(.*)$]
|
||||
// [rule: ^qdreader(.*)$]
|
||||
// [admin: true]
|
||||
// [priority: 600]
|
||||
// [cron: 1 7 * * *]
|
||||
|
||||
|
||||
var BUCKET = "hermes_qidian"
|
||||
@@ -24,12 +24,28 @@ 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 TASK_PREF_KEY = "qdreader_task_pref_json"
|
||||
var ADV_JOB_ENABLE_KEY = "qdreader_adv_job_enable"
|
||||
var EXTRA_ADV_JOB_ENABLE_KEY = "qdreader_extra_adv_job_enable"
|
||||
var LOTTERY_ENABLE_KEY = "qdreader_lottery_enable"
|
||||
var WEEKLY_EXCHANGE_ENABLE_KEY = "qdreader_weekly_exchange_enable"
|
||||
var CHAPTER_CARD_ENABLE_KEY = "qdreader_chapter_card_enable"
|
||||
var MESSAGE_BOX_ENABLE_KEY = "qdreader_message_box_enable"
|
||||
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"
|
||||
var QD_TASKS = [
|
||||
{ key: "reward", label: "领取奖励", handler: "receiveGifts", defaultEnabled: true, builtin: true },
|
||||
{ key: "adv", label: "激励任务", handler: "adv", settingKey: ADV_JOB_ENABLE_KEY },
|
||||
{ key: "extraAdv", label: "每日任务", handler: "daily", settingKey: EXTRA_ADV_JOB_ENABLE_KEY },
|
||||
{ key: "lottery", label: "每日抽奖", handler: "lottery", settingKey: LOTTERY_ENABLE_KEY },
|
||||
{ key: "weeklyExchange", label: "每周兑换", handler: "exchange", settingKey: WEEKLY_EXCHANGE_ENABLE_KEY, sundayOnly: true },
|
||||
{ key: "chapterCard", label: "章节卡片", handler: "chapterCard", settingKey: CHAPTER_CARD_ENABLE_KEY },
|
||||
{ key: "messageBox", label: "大咖荐书", handler: "messageBox", settingKey: MESSAGE_BOX_ENABLE_KEY }
|
||||
]
|
||||
|
||||
function defined(name) { try { return typeof this[name] !== "undefined" } catch (e) { return false } }
|
||||
function globalValue(name) { try { return this[name] } catch (e) { return undefined } }
|
||||
@@ -75,6 +91,22 @@ function reply(msg) {
|
||||
try { if (typeof push === "function") { push({ content: out }); return } } catch (e) {}
|
||||
try { console.log(out) } catch (e) {}
|
||||
}
|
||||
function pushToUser(userId, content) {
|
||||
userId = trim(userId || "")
|
||||
content = typeof content === "string" ? content : JSON.stringify(content)
|
||||
if (!userId || !content) return false
|
||||
if (typeof push !== "function") return false
|
||||
var payloads = [
|
||||
{ imType: "wx", userID: userId, content: content },
|
||||
{ imType: "we", userID: userId, content: content },
|
||||
{ imType: "web", userID: userId, content: content },
|
||||
{ userID: userId, content: content }
|
||||
]
|
||||
for (var i = 0; i < payloads.length; i++) {
|
||||
try { push(payloads[i]); return true } catch (e) {}
|
||||
}
|
||||
return false
|
||||
}
|
||||
function debug(msg) { try { if (typeof Debug === "function") Debug(String(msg)); else console.log(String(msg)) } catch (e) {} }
|
||||
|
||||
function cleanStringMap(obj) {
|
||||
@@ -248,19 +280,29 @@ function maskUid(uid) {
|
||||
return uid.slice(0, 2) + "***" + uid.slice(-2)
|
||||
}
|
||||
|
||||
function runtimeImType() {
|
||||
var im = ""
|
||||
try { if (typeof ImType === "function") im = ImType() || "" } catch (e) {}
|
||||
try { if (!im && typeof GetImType === "function") im = GetImType() || "" } catch (e2) {}
|
||||
return lower(trim(im))
|
||||
}
|
||||
function isSchedulerImType(im) {
|
||||
im = lower(trim(im))
|
||||
return im === "cron" || im === "task" || im === "fake"
|
||||
}
|
||||
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) {}
|
||||
if (isSchedulerImType(runtimeImType())) return ""
|
||||
try { if (typeof getContent === "function") s = getContent() || "" } catch (e3) {}
|
||||
try { if (!s && typeof GetContent === "function") s = GetContent() || "" } catch (e4) {}
|
||||
try { if (!s && typeof sender !== "undefined" && sender && sender.getContent) s = sender.getContent() || "" } catch (e5) {}
|
||||
try { if (!s && typeof sender !== "undefined" && sender && sender.getMessage) s = sender.getMessage() || "" } catch (e6) {}
|
||||
try { if (!s && typeof message !== "undefined") s = String(message) } catch (e7) {}
|
||||
return trim(s)
|
||||
}
|
||||
function usage() {
|
||||
return [
|
||||
"启点读书签到插件 v2.5.6",
|
||||
"启点读书签到插件 v2.15.6",
|
||||
"【一级菜单】",
|
||||
"账号管理:启点账号",
|
||||
"执行查询:启点查询",
|
||||
@@ -268,9 +310,11 @@ function usage() {
|
||||
"快速提交CK:启点ck <QX抓到的cookie>",
|
||||
"直接签到:启点签到",
|
||||
"指定签到:启点签到 <uid>",
|
||||
"账号任务:启点账号 → 账号详情/任务配置",
|
||||
"【配置】",
|
||||
"签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API,
|
||||
"定时:每日 00:01 只归档并清空日志;每日 07:01 只自动签到;QX 只负责抓 cookie。"
|
||||
"完全体任务:领取奖励固定执行;激励/每日/抽奖/兑换/章节卡/大咖荐书按账号单独管理,默认关闭。",
|
||||
"定时:每日 07:01 自动执行签到链路;QX 只负责抓 cookie。"
|
||||
].join("\n")
|
||||
}
|
||||
function stripCommand(text) {
|
||||
@@ -324,11 +368,11 @@ 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 = ""
|
||||
if (res.body !== undefined && res.body !== null && String(res.body) !== "") body = res.body
|
||||
else if (res.data !== undefined && res.data !== null && String(res.data) !== "") body = res.data
|
||||
else if (res.content !== undefined && res.content !== null && String(res.content) !== "") body = res.content
|
||||
else if (res.responseText !== undefined && res.responseText !== null && String(res.responseText) !== "") body = res.responseText
|
||||
else if (res.statusCode !== undefined || res.status !== undefined || res.headers !== undefined) body = res.data !== undefined ? res.data : ""
|
||||
else body = res
|
||||
} else {
|
||||
body = res
|
||||
@@ -368,11 +412,12 @@ function qidianRequest(path, method, data, cookie) {
|
||||
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 targetUrl = /^https?:\/\//i.test(path) ? path : QIDIAN_BASE + path
|
||||
var payload = { qdh: info.qdh, qdheader: info.qdheader, url: targetUrl, method: lower(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 }
|
||||
var req = { url: targetUrl, 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
|
||||
@@ -385,17 +430,67 @@ function qidianRequest(path, method, data, cookie) {
|
||||
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 resultCode(obj) {
|
||||
obj = responseEnvelope(obj)
|
||||
if (!obj) return undefined
|
||||
var keys = ["Result", "result", "Code", "code", "retcode", "RetCode"]
|
||||
for (var i = 0; i < keys.length; i++) if (obj[keys[i]] !== undefined) return obj[keys[i]]
|
||||
var data = obj.Data || obj.data || obj.ResultData || obj.resultData
|
||||
if (data && typeof data === "object") for (var j = 0; j < keys.length; j++) if (data[keys[j]] !== undefined) return data[keys[j]]
|
||||
return undefined
|
||||
}
|
||||
function resultOk(obj) {
|
||||
var code = resultCode(obj)
|
||||
var status = obj && (obj.status || obj.Status)
|
||||
var env = responseEnvelope(obj)
|
||||
var code = resultCode(env)
|
||||
var status = env && (env.status || env.Status || env.statusCode || env.StatusCode)
|
||||
if (typeof status === "number" && status >= 400) return false
|
||||
if (String(status) === "401" || String(status) === "403") return false
|
||||
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 isLoginExpiredMsg(msg) {
|
||||
msg = String(msg || "")
|
||||
return /登录状态已失效|登录已失效|登录状态.*过期|登录.*过期|用户未登录|未登录|请登录|重新登录|Cookie.*失效|CK.*失效|cookie.*expired|login.*expired|not\s*login/i.test(msg)
|
||||
}
|
||||
function nowIso() { try { return new Date().toISOString() } catch (e) { return String(new Date()) } }
|
||||
function resultMsg(obj) {
|
||||
obj = responseEnvelope(obj)
|
||||
if (!obj) return ""
|
||||
var status = obj.status || obj.Status || obj.statusCode || obj.StatusCode
|
||||
if (String(status) === "401") return "登录状态已失效,请更新 Cookie"
|
||||
if (String(status) === "403") return "接口鉴权失败,请更新 Cookie"
|
||||
var msg = obj.Message || obj.Msg || obj.message || obj.msg || obj.error || obj.toast || (obj.data && (obj.data.message || obj.data.msg || obj.data.error || obj.data.toast || obj.data.Message || obj.data.Msg)) || (obj.Data && (obj.Data.message || obj.Data.msg || obj.Data.error || obj.Data.toast || obj.Data.Message || obj.Data.Msg)) || ""
|
||||
if (isLoginExpiredMsg(msg)) return "登录状态已失效,请更新 Cookie"
|
||||
return msg
|
||||
}
|
||||
function isCookieExpiredResult(obj) {
|
||||
var env = responseEnvelope(obj)
|
||||
if (!env) return false
|
||||
var status = env.status || env.Status || env.statusCode || env.StatusCode
|
||||
if (String(status) === "401") return true
|
||||
var rawMsg = env.Message || env.Msg || env.message || env.msg || env.error || env.toast || (env.data && (env.data.message || env.data.msg || env.data.error || env.data.toast || env.data.Message || env.data.Msg)) || (env.Data && (env.Data.message || env.Data.msg || env.Data.error || env.Data.toast || env.Data.Message || env.Data.Msg)) || ""
|
||||
return isLoginExpiredMsg(rawMsg)
|
||||
}
|
||||
function successLikeMsg(msg) {
|
||||
msg = String(msg || "")
|
||||
return /今日已签到|已经签到|已签到|重复签到|已领取|已经领取|已完成|完成|无可领取|无礼包|暂无可领取/.test(msg)
|
||||
}
|
||||
function taskResultOk(obj) {
|
||||
return resultOk(obj) || successLikeMsg(resultMsg(obj))
|
||||
}
|
||||
function isWhitelistBlockedError(msg) {
|
||||
msg = String(msg || "")
|
||||
return /url\s*不在允许的接口列表内|不在允许的接口列表|参数异常.*允许的接口/.test(msg)
|
||||
}
|
||||
function pad2(n) { n = String(n); return n.length < 2 ? "0" + n : n }
|
||||
function beijingDate(dateLike) {
|
||||
var d = dateLike ? new Date(dateLike) : new Date()
|
||||
if (!d || isNaN(d.getTime())) d = new Date()
|
||||
return new Date(d.getTime() + 8 * 60 * 60 * 1000)
|
||||
}
|
||||
function beijingTime(dateLike) {
|
||||
var d = beijingDate(dateLike)
|
||||
return d.getUTCFullYear() + "-" + pad2(d.getUTCMonth() + 1) + "-" + pad2(d.getUTCDate()) + " " + pad2(d.getUTCHours()) + ":" + pad2(d.getUTCMinutes()) + ":" + pad2(d.getUTCSeconds())
|
||||
}
|
||||
function nowIso() { return beijingTime() }
|
||||
function setLastResult(uid, result) {
|
||||
var obj = cleanStringMap(safeJsonParse(cfgGet(LAST_RESULT_KEY, "{}"), {}))
|
||||
obj[String(uid)] = JSON.stringify(result || {})
|
||||
@@ -412,12 +507,11 @@ 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
|
||||
var s = String(iso || "")
|
||||
var m0 = s.match(/^(\d{4}-\d{2}-\d{2})/)
|
||||
if (m0) return m0[1]
|
||||
var d = beijingDate(iso)
|
||||
return d.getUTCFullYear() + "-" + pad2(d.getUTCMonth() + 1) + "-" + pad2(d.getUTCDate())
|
||||
}
|
||||
function archiveRunLog(reason) {
|
||||
var run = getLastRunLog()
|
||||
@@ -452,12 +546,7 @@ function latestArchiveForVisible(uids) {
|
||||
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"
|
||||
// Cron only runs the signing branch. Archive is performed immediately after each sign run.
|
||||
return "sign"
|
||||
}
|
||||
function formatArchiveReply(ret) {
|
||||
@@ -476,17 +565,429 @@ function sourceName(source) {
|
||||
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 getTaskPrefs() { return safeJsonParse(cfgGet(TASK_PREF_KEY, "{}"), {}) || {} }
|
||||
function setTaskPrefs(obj) { return cfgSet(TASK_PREF_KEY, JSON.stringify(obj || {})) }
|
||||
function accountTaskPrefs(uid) {
|
||||
var all = getTaskPrefs()
|
||||
var prefs = all[String(uid)]
|
||||
return isObject(prefs) ? prefs : {}
|
||||
}
|
||||
function signAll(targetUid, allUsers, source) {
|
||||
function taskEnabledForUid(task, uid) {
|
||||
if (task.defaultEnabled || task.builtin) return true
|
||||
if (task.sundayOnly) { var d = beijingDate(); if (d.getUTCDay() !== 0) return false }
|
||||
var prefs = accountTaskPrefs(uid)
|
||||
return prefs[task.key] === true || prefs[task.key] === "true" || prefs[task.key] === 1 || prefs[task.key] === "1"
|
||||
}
|
||||
function taskEnabled(task) { return taskEnabledForUid(task, "") }
|
||||
function taskStatusLines(uid) {
|
||||
var lines = ["启点账号任务开关:" + (uid ? maskUid(uid) : "未选择账号")]
|
||||
var prefs = uid ? accountTaskPrefs(uid) : {}
|
||||
for (var i = 0; i < QD_TASKS.length; i++) {
|
||||
var t = QD_TASKS[i]
|
||||
var enabled = t.builtin || t.defaultEnabled || prefs[t.key] === true || prefs[t.key] === "true" || prefs[t.key] === 1 || prefs[t.key] === "1"
|
||||
var suffix = t.builtin ? "(固定)" : ""
|
||||
if (t.sundayOnly) suffix += "(仅周日执行)"
|
||||
lines.push((i + 1) + ". " + t.label + ":" + (enabled ? "开启" : "关闭") + suffix)
|
||||
}
|
||||
lines.push("回复:序号 on/off 切换,例如 2 on;all on 一键开启;all off 一键关闭;q 退出")
|
||||
return lines.join("\n")
|
||||
}
|
||||
function taskMenuForUid(uid) {
|
||||
if (!uid) return reply("未选择账号,已退出")
|
||||
reply(taskStatusLines(uid))
|
||||
var op = listenInput(60000)
|
||||
if (!op || /^(q|退出|取消)$/i.test(op)) return reply("已退出")
|
||||
var all = getTaskPrefs()
|
||||
var prefs = isObject(all[String(uid)]) ? all[String(uid)] : {}
|
||||
var allMatch = op.match(/^(all|全部|所有|一键|全开|全关)\s*(on|off|开启|关闭|启用|禁用|1|0)?$/i)
|
||||
if (allMatch) {
|
||||
var enableAll = /^(全开)$/i.test(allMatch[1]) || /^(on|开启|启用|1)$/i.test(allMatch[2] || "")
|
||||
if (/^(全关)$/i.test(allMatch[1])) enableAll = false
|
||||
if (!allMatch[2] && /^(all|全部|所有|一键)$/i.test(allMatch[1])) return reply("请指定 all on 或 all off")
|
||||
for (var ai = 0; ai < QD_TASKS.length; ai++) {
|
||||
var at = QD_TASKS[ai]
|
||||
if (at.builtin || !at.settingKey) continue
|
||||
prefs[at.key] = !!enableAll
|
||||
}
|
||||
all[String(uid)] = prefs
|
||||
setTaskPrefs(all)
|
||||
return reply("已" + (enableAll ? "一键开启" : "一键关闭") + "账号 " + maskUid(uid) + " 的全部可选任务\n" + taskStatusLines(uid))
|
||||
}
|
||||
var m = op.match(/^(\d+)\s*(on|off|开启|关闭|启用|禁用|1|0)?$/i)
|
||||
if (!m) return reply("未识别选项,已退出")
|
||||
var idx = parseInt(m[1], 10) - 1
|
||||
if (idx < 0 || idx >= QD_TASKS.length) return reply("编号无效,已退出")
|
||||
var task = QD_TASKS[idx]
|
||||
if (task.builtin || !task.settingKey) return reply(task.label + " 为固定任务,不能关闭")
|
||||
var currently = prefs[task.key] === true || prefs[task.key] === "true" || prefs[task.key] === 1 || prefs[task.key] === "1"
|
||||
var enable = m[2] ? /^(on|开启|启用|1)$/i.test(m[2]) : !currently
|
||||
prefs[task.key] = !!enable
|
||||
all[String(uid)] = prefs
|
||||
setTaskPrefs(all)
|
||||
return reply(task.label + "已" + (enable ? "开启" : "关闭") + "(账号 " + maskUid(uid) + ")\n" + taskStatusLines(uid))
|
||||
}
|
||||
function qdGet(path, data, cookie) { return qidianRequest(path, "GET", data || {}, cookie) }
|
||||
function qdPost(path, data, cookie) { return qidianRequest(path, "POST", data || {}, cookie) }
|
||||
function firstArray() {
|
||||
for (var i = 0; i < arguments.length; i++) if (Array.isArray(arguments[i])) return arguments[i]
|
||||
return []
|
||||
}
|
||||
function nested(obj, path) {
|
||||
var cur = obj
|
||||
for (var i = 0; i < path.length; i++) {
|
||||
if (!cur || cur[path[i]] === undefined || cur[path[i]] === null) return undefined
|
||||
cur = cur[path[i]]
|
||||
}
|
||||
return cur
|
||||
}
|
||||
function responseEnvelope(obj) {
|
||||
if (!obj || typeof obj !== "object") return obj
|
||||
if ((obj.statusCode !== undefined || obj.status !== undefined || obj.headers !== undefined) && obj.data && typeof obj.data === "object") return obj.data
|
||||
return obj
|
||||
}
|
||||
function payloadData(obj) {
|
||||
obj = responseEnvelope(obj)
|
||||
var data = obj && (obj.Data || obj.data || obj.ResultData || obj.resultData)
|
||||
if (data && typeof data === "object" && (data.Data || data.data || data.ResultData || data.resultData) && (data.Result !== undefined || data.result !== undefined || data.Code !== undefined || data.code !== undefined || data.Message !== undefined || data.message !== undefined)) return data.Data || data.data || data.ResultData || data.resultData
|
||||
return data || {}
|
||||
}
|
||||
function looksLikeVideoTask(task) { return isObject(task) && !!taskIdOf(task) && (task["IsFinished"] !== undefined || task["IsReceived"] !== undefined || task["Title"] !== undefined || task["TaskName"] !== undefined) }
|
||||
function taskArrayScore(list, kind) {
|
||||
if (!Array.isArray(list) || !list.length) return 0
|
||||
var score = 0
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var t = list[i] || {}
|
||||
if (!looksLikeVideoTask(t)) continue
|
||||
var title = taskTitle(t)
|
||||
score += 2
|
||||
if (kind === "daily" && (title === "完成1个广告任务得奖励" || title === "完成3个广告任务得奖励" || t["IsReceived"] !== undefined)) score += 5
|
||||
if (kind === "adv" && (t["IsFinished"] !== undefined || /激励|碎片|福利|广告/.test(title))) score += 3
|
||||
}
|
||||
return score
|
||||
}
|
||||
function findTaskListByShape(root, kind) {
|
||||
var best = []
|
||||
var bestScore = 0
|
||||
var seen = []
|
||||
function walk(node, depth) {
|
||||
if (!node || depth > 8) return
|
||||
if (Array.isArray(node)) {
|
||||
var score = taskArrayScore(node, kind)
|
||||
if (score > bestScore) { bestScore = score; best = node }
|
||||
for (var i = 0; i < node.length; i++) walk(node[i], depth + 1)
|
||||
return
|
||||
}
|
||||
if (!isObject(node) || seen.indexOf(node) >= 0) return
|
||||
seen.push(node)
|
||||
for (var k in node) walk(node[k], depth + 1)
|
||||
}
|
||||
walk(root, 0)
|
||||
return best
|
||||
}
|
||||
function advTaskList(main) {
|
||||
return firstArray(
|
||||
nested(main, ["Data", "DailyBenefitModule", "TaskList"]),
|
||||
nested(main, ["data", "DailyBenefitModule", "TaskList"]),
|
||||
nested(main, ["Data", "AdvTaskList"]),
|
||||
nested(main, ["data", "AdvTaskList"]),
|
||||
findTaskListByShape(payloadData(main), "adv")
|
||||
)
|
||||
}
|
||||
function dailyTaskList(main) {
|
||||
return firstArray(
|
||||
nested(main, ["Data", "VideoRewardTab", "TaskList"]),
|
||||
nested(main, ["data", "VideoRewardTab", "TaskList"]),
|
||||
nested(main, ["Data", "TaskList"]),
|
||||
nested(main, ["data", "TaskList"]),
|
||||
findTaskListByShape(payloadData(main), "daily")
|
||||
)
|
||||
}
|
||||
function surpriseBenefit(data) {
|
||||
return data.SurpriseBenefit || data.SurpriseTask || data.SurpriseBenefitTask || null
|
||||
}
|
||||
function makeVideoPayload(taskId) {
|
||||
return { taskId: String(taskId || ""), BanId: "0", BanMessage: "", CaptchaAId: "", CaptchaURL: "", CaptchaType: "0", Challenge: "", Gt: "", NewCaptcha: "0", Offline: "0", PhoneNumber: "", SessionKey: "" }
|
||||
}
|
||||
function field(obj, names, def) {
|
||||
for (var i = 0; i < names.length; i++) if (obj && obj[names[i]] !== undefined && obj[names[i]] !== null) return obj[names[i]]
|
||||
return def
|
||||
}
|
||||
function taskTitle(task) { return String(field(task, ["Title", "TaskName", "Name", "TaskTitle", "Desc", "Description"], "")) }
|
||||
function taskIdOf(task) { return field(task, ["TaskId", "TaskID", "taskId", "TaskIDStr", "Id", "id"], "") }
|
||||
function taskFinished(task) { return Number(field(task, ["IsFinished", "Finished", "Status", "status"], 0)) === 1 }
|
||||
function taskIdKey(id) { return String(id === undefined || id === null ? "" : id) }
|
||||
function taskRunCount(task, def) {
|
||||
var n = Number(field(task, ["NeedCount", "needCount", "Count", "count", "TotalCount", "totalCount", "Times", "times", "Limit", "limit"], def || 1))
|
||||
if (!n || isNaN(n) || n < 1) n = def || 1
|
||||
if (n > 10) n = 10
|
||||
return n
|
||||
}
|
||||
function taskAccepted(res) { return (taskResultOk(res) || Number(resultCode(res)) === 9) && !riskConf(res) }
|
||||
function riskConf(res) {
|
||||
res = responseEnvelope(res)
|
||||
var data = res && (res.Data || res.data || res.ResultData || res.resultData)
|
||||
return data && (data.RiskConf || data.riskConf || data.Risk || data.risk)
|
||||
}
|
||||
function riskBlockedMsg(res) {
|
||||
var rc = riskConf(res)
|
||||
if (!rc) return ""
|
||||
var url = rc.CaptchaURL || rc.captchaUrl || rc.Url || rc.url || ""
|
||||
return url ? "触发风控验证,任务未确认完成" : "触发风控验证,任务未确认完成"
|
||||
}
|
||||
function finishVideoTask(taskOrId, cookie, repeat) {
|
||||
var tid = typeof taskOrId === "object" ? taskIdOf(taskOrId || {}) : taskOrId
|
||||
var n = repeat || (typeof taskOrId === "object" ? taskRunCount(taskOrId, 1) : 1)
|
||||
var ok = 0
|
||||
var last = null
|
||||
if (!tid) return { ok: 0, total: 0, last: null }
|
||||
for (var i = 0; i < n; i++) {
|
||||
last = qdPost("/argus/api/v1/video/adv/finishWatch", makeVideoPayload(tid), cookie)
|
||||
if (taskAccepted(last)) ok++
|
||||
else break
|
||||
}
|
||||
return { ok: ok, total: n, last: last, taskId: tid }
|
||||
}
|
||||
function makeBase62Token(num) {
|
||||
var alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
var n = BigInt(num)
|
||||
var zero = BigInt(0)
|
||||
var base = BigInt(62)
|
||||
if (n === zero) return "0"
|
||||
var out = ""
|
||||
while (n > zero) { out = alphabet[Number(n % base)] + out; n = n / base }
|
||||
return out
|
||||
}
|
||||
function doReceiveGifts(cookie) {
|
||||
var res = qdPost("/argus/api/v3/checkin/receivegifts", { GameId: "0" }, cookie)
|
||||
var ok = taskResultOk(res)
|
||||
return { name: "领取奖励", ok: !!ok, msg: resultMsg(res) || (ok ? "领取成功" : "领取失败"), path: "/argus/api/v3/checkin/receivegifts" }
|
||||
}
|
||||
function doAdvTask(cookie) {
|
||||
var main = qdGet("/argus/api/v2/video/adv/mainPage", {}, cookie)
|
||||
if (!taskResultOk(main)) return { name: "激励任务", ok: false, msg: resultMsg(main) || "获取任务列表失败", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
var tasks = advTaskList(main)
|
||||
if (!tasks.length) return { name: "激励任务", ok: false, msg: "任务列表为空", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
if (taskFinished(tasks[tasks.length - 1])) return { name: "激励任务", ok: true, msg: "任务已完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
var submittedCount = 0
|
||||
var riskSubmitted = false
|
||||
for (var i = 0; i < tasks.length; i++) {
|
||||
var t = tasks[i] || {}
|
||||
if (taskFinished(t)) continue
|
||||
var tid = taskIdOf(t)
|
||||
if (!tid) continue
|
||||
var fr = finishVideoTask(tid, cookie, 1)
|
||||
submittedCount += fr.ok
|
||||
if (riskConf(fr.last)) riskSubmitted = true
|
||||
if (fr.ok < fr.total) return { name: "激励任务", ok: false, msg: riskBlockedMsg(fr.last) || resultMsg(fr.last) || ("第 " + (i + 1) + " 个任务失败"), path: "/argus/api/v1/video/adv/finishWatch" }
|
||||
}
|
||||
var verify = qdGet("/argus/api/v2/video/adv/mainPage", {}, cookie)
|
||||
var after = advTaskList(verify)
|
||||
var finished = !!(after && after.length && after.every(function (t) { return taskFinished(t) }))
|
||||
if (finished) return { name: "激励任务", ok: true, msg: "任务已完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
if (submittedCount > 0 && riskSubmitted) return { name: "激励任务", ok: false, msg: "触发风控验证,任务未确认完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
return { name: "激励任务", ok: false, msg: submittedCount ? "部分任务未完成" : "未找到可提交任务", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
}
|
||||
function taskReceived(task) {
|
||||
return Number(field(task, ["IsReceived", "Received", "Status", "status"], 0)) === 1
|
||||
}
|
||||
function doDailyTask(cookie) {
|
||||
var main = qdGet("/argus/api/v2/video/adv/mainPage", {}, cookie)
|
||||
if (!taskResultOk(main)) return { name: "每日任务", ok: false, msg: resultMsg(main) || "获取任务列表失败", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
var data = payloadData(main)
|
||||
var done = 0
|
||||
var riskSubmitted = false
|
||||
var pre = surpriseBenefit(data)
|
||||
if (pre && !taskFinished(pre) && taskIdOf(pre)) {
|
||||
var pr = finishVideoTask(taskIdOf(pre), cookie, 1)
|
||||
done += pr.ok
|
||||
if (riskConf(pr.last)) riskSubmitted = true
|
||||
if (pr.ok < pr.total) return { name: "每日任务", ok: false, msg: riskBlockedMsg(pr.last) || resultMsg(pr.last) || "惊喜福利任务失败", path: "/argus/api/v1/video/adv/finishWatch" }
|
||||
}
|
||||
var tasks = dailyTaskList(main)
|
||||
if (!tasks.length) return { name: "每日任务", ok: false, msg: "任务列表为空", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
var oneAd = null
|
||||
var target = null
|
||||
for (var i = 0; i < tasks.length; i++) {
|
||||
var title = taskTitle(tasks[i])
|
||||
if (!oneAd && title === "完成1个广告任务得奖励") oneAd = tasks[i]
|
||||
if (!target && title === "完成3个广告任务得奖励") target = tasks[i]
|
||||
}
|
||||
if (oneAd && !taskReceived(oneAd) && taskIdOf(oneAd)) {
|
||||
var ar = finishVideoTask(taskIdOf(oneAd), cookie, 1)
|
||||
done += ar.ok
|
||||
if (riskConf(ar.last)) riskSubmitted = true
|
||||
if (ar.ok < ar.total) return { name: "每日任务", ok: false, msg: riskBlockedMsg(ar.last) || resultMsg(ar.last) || "完成1个广告任务失败", path: "/argus/api/v1/video/adv/finishWatch" }
|
||||
}
|
||||
if (!target) return { name: "每日任务", ok: false, msg: "未找到每日任务任务", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
if (taskReceived(target)) return { name: "每日任务", ok: true, msg: "任务已完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
var tid = taskIdOf(target)
|
||||
if (!tid) return { name: "每日任务", ok: false, msg: "每日任务缺少 TaskId", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
var tr = finishVideoTask(tid, cookie, 3)
|
||||
done += tr.ok
|
||||
if (riskConf(tr.last)) riskSubmitted = true
|
||||
if (tr.ok < tr.total) return { name: "每日任务", ok: false, msg: riskBlockedMsg(tr.last) || resultMsg(tr.last) || "每日任务广告失败", path: "/argus/api/v1/video/adv/finishWatch" }
|
||||
var verify = qdGet("/argus/api/v2/video/adv/mainPage", {}, cookie)
|
||||
var after = dailyTaskList(verify)
|
||||
var finished = false
|
||||
for (var v = 0; v < after.length; v++) if (taskTitle(after[v]) === "完成3个广告任务得奖励" && taskReceived(after[v])) { finished = true; break }
|
||||
if (finished) return { name: "每日任务", ok: true, msg: "任务已完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
if (done > 0 && riskSubmitted) return { name: "每日任务", ok: false, msg: "触发风控验证,任务未确认完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
return { name: "每日任务", ok: false, msg: "任务未完成", path: "/argus/api/v2/video/adv/mainPage" }
|
||||
}
|
||||
function doLotteryTask(cookie) {
|
||||
var detail = qdGet("/argus/api/v2/checkin/detail", {}, cookie)
|
||||
if (!taskResultOk(detail)) return { name: "每日抽奖", ok: false, msg: resultMsg(detail) || "获取抽奖信息失败", path: "/argus/api/v2/checkin/detail" }
|
||||
var data = detail.Data || detail.data || {}
|
||||
var times = Number(data.HasVideoUrge || 0) + Number(data.LotteryCount || data.LotteryChance || data.ChanceCount || 0)
|
||||
if (!times) return { name: "每日抽奖", ok: true, msg: "无可用抽奖次数", path: "/argus/api/v2/checkin/detail" }
|
||||
times = Math.min(times, 10)
|
||||
var okCount = 0
|
||||
for (var i = 0; i < times; i++) { var r = qdPost("/argus/api/v2/checkin/lottery", { GameId: "0", IsVideo: i < Number(data.HasVideoUrge || 0) ? "1" : "0" }, cookie); if (taskResultOk(r)) okCount++ }
|
||||
return { name: "每日抽奖", ok: okCount > 0, msg: "成功 " + okCount + "/" + times + " 次抽奖", path: "/argus/api/v2/checkin/lottery" }
|
||||
}
|
||||
function doWeeklyExchange(cookie) {
|
||||
var page = qdGet("/argus/api/v3/checkin/checkinexchangepage", {}, cookie)
|
||||
if (!taskResultOk(page)) return { name: "每周兑换", ok: false, msg: resultMsg(page) || "获取兑换页失败", path: "/argus/api/v3/checkin/checkinexchangepage" }
|
||||
var data = page.Data || page.data || {}
|
||||
var balance = Number(data.Balance || data.Score || 0)
|
||||
if (balance <= 0) return { name: "每周兑换", ok: false, msg: "余额不足(" + balance + ")", path: "/argus/api/v3/checkin/checkinexchangepage" }
|
||||
var goods = firstArray(data.Goods, data.GoodsList, data.GiftList, data.List, data.Items)
|
||||
if (!goods.length) return { name: "每周兑换", ok: false, msg: "章节卡列表为空", path: "/argus/api/v3/checkin/checkinexchangepage" }
|
||||
var chosen = null
|
||||
for (var i = 0; i < goods.length; i++) {
|
||||
var price = Number(goods[i].GoodsScore || goods[i].NeedScore || goods[i].Price || goods[i].Score || 0)
|
||||
if (price > 0 && price <= balance && (!chosen || price > Number(chosen.GoodsScore || chosen.NeedScore || chosen.Price || chosen.Score || 0))) chosen = goods[i]
|
||||
}
|
||||
if (!chosen) return { name: "每周兑换", ok: false, msg: "余额不足(" + balance + ")", path: "/argus/api/v3/checkin/checkinexchangepage" }
|
||||
var gid = chosen.GoodsId || chosen.Id || chosen.GoodsID
|
||||
var priceText = Number(chosen.GoodsScore || chosen.NeedScore || chosen.Price || chosen.Score || 0)
|
||||
var cardCount = chosen.ChapterCardCount || chosen.CardCount || chosen.Count || ""
|
||||
var r = qdPost("/argus/api/v3/checkin/exchangegoods", { goodId: gid }, cookie)
|
||||
var code = Number(r && (r.result !== undefined ? r.result : (r.Result !== undefined ? r.Result : (r.code !== undefined ? r.code : r.Code))))
|
||||
if (code === -220000) return { name: "每周兑换", ok: false, msg: "未到兑换时间", path: "/argus/api/v3/checkin/exchangegoods" }
|
||||
var ok = taskResultOk(r)
|
||||
var successMsg = cardCount ? ("消耗 " + priceText + " 点兑换 " + cardCount + " 点章节卡") : "兑换成功"
|
||||
return { name: "每周兑换", ok: ok, msg: resultMsg(r) || (ok ? successMsg : "兑换失败"), path: "/argus/api/v3/checkin/exchangegoods" }
|
||||
}
|
||||
function doChapterCard(cookie) {
|
||||
var res = qdGet("/argus/api/v1/readtime/scoremall/chaptercardwithbook", {}, cookie)
|
||||
var ok = taskResultOk(res)
|
||||
var cards = firstArray(nested(res, ["Data", "Cards"]), nested(res, ["data", "Cards"]), nested(res, ["Data", "ChapterCards"]), nested(res, ["data", "ChapterCards"]))
|
||||
return { name: "章节卡片", ok: !!ok, msg: ok ? (cards.length ? ("章节卡 " + cards.length + " 张") : "暂无章节卡") : (resultMsg(res) || "查询失败"), path: "/argus/api/v1/readtime/scoremall/chaptercardwithbook" }
|
||||
}
|
||||
function messageTokenFromCookie(cookie) {
|
||||
var jar = parseCookieString(cookie)
|
||||
var uid = extractUid(cookie)
|
||||
var raw = uid ? ((BigInt(uid) * BigInt(4) & BigInt("0x7ffffffffffffffc")) ^ BigInt("0x113a4f")) : BigInt(0)
|
||||
return raw ? makeBase62Token(raw) : (uid || jar.ywguid || "")
|
||||
}
|
||||
function recentCreateTime(v) {
|
||||
if (!v) return false
|
||||
var t = typeof v === "string" ? new Date(v).getTime() : Number(v)
|
||||
if (!t || isNaN(t)) return false
|
||||
if (t < 1000000000000) t = t * 1000
|
||||
var now = (new Date()).getTime()
|
||||
return now - t >= 0 && now - t <= 24 * 60 * 60 * 1000
|
||||
}
|
||||
function beijingHour(dateLike) { return beijingDate(dateLike).getUTCHours() }
|
||||
function messageBoxWindowLabel() { return "全天" }
|
||||
function messageBoxAllowedNow() {
|
||||
return true
|
||||
}
|
||||
function orderIdFromText(s) {
|
||||
var m = String(s || "").match(/orderId=([a-f0-9]+)/i)
|
||||
return m ? m[1] : ""
|
||||
}
|
||||
function doMessageBox(cookie) {
|
||||
var token = messageTokenFromCookie(cookie)
|
||||
if (!token) return { name: "大咖荐书", ok: true, msg: "缺少消息 token,已跳过", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" }
|
||||
var baseParams = { appappend: token, categoryIds: "0", pg: "1", pz: "10" }
|
||||
var res = qdGet("https://magev6.if.qidian.com/argus/api/v2/message/pullmessage", baseParams, cookie)
|
||||
if (!taskResultOk(res)) return { name: "大咖荐书", ok: false, msg: resultMsg(res) || "获取消息列表失败", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" }
|
||||
var groups = firstArray(nested(res, ["Data", "UserList"]), nested(res, ["data", "UserList"]), nested(res, ["Data", "List"]), nested(res, ["data", "List"]), nested(res, ["Data", "MsgList"]), nested(res, ["data", "MsgList"]))
|
||||
if (!groups.length) return { name: "大咖荐书", ok: true, msg: "无消息", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" }
|
||||
var senders = []
|
||||
for (var i = 0; i < groups.length; i++) if (Number(field(groups[i], ["RedPoint", "redPoint"], 0)) === 1 && field(groups[i], ["FromId", "fromId", "SenderId", "senderId", "FromUserId", "fromUserId"], "")) senders.push(groups[i])
|
||||
if (!senders.length) return { name: "大咖荐书", ok: true, msg: "无未读消息", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" }
|
||||
var handled = 0
|
||||
for (var s = 0; s < senders.length; s++) {
|
||||
var sid = field(senders[s], ["FromId", "fromId", "SenderId", "senderId", "FromUserId", "fromUserId"], "")
|
||||
var msgRes = qdGet("https://magev6.if.qidian.com/argus/api/v2/message/pullmessage", { appappend: token, categoryIds: "0", fromId: sid, pg: "1", pz: "10" }, cookie)
|
||||
if (!taskResultOk(msgRes)) continue
|
||||
var msgs = firstArray(nested(msgRes, ["Data", "MsgList"]), nested(msgRes, ["data", "MsgList"]), nested(msgRes, ["Data", "List"]), nested(msgRes, ["data", "List"]), nested(msgRes, ["Data", "Messages"]), nested(msgRes, ["data", "Messages"]))
|
||||
for (var j = 0; j < msgs.length; j++) {
|
||||
if (!recentCreateTime(field(msgs[j], ["CreateTime", "createTime"], ""))) continue
|
||||
var orderId = orderIdFromText(field(msgs[j], ["ActionUrl", "actionUrl", "RefUrl", "refUrl", "Content", "content", "Link", "link", "Url", "url"], ""))
|
||||
if (!orderId) continue
|
||||
var detail = qdGet("/argus/api/v1/interaction/rewarddetail", { orderId: orderId, refresh: "0" }, cookie)
|
||||
if (!taskResultOk(detail)) continue
|
||||
var data = detail.Data || detail.data || {}
|
||||
var chance = Number(data.ChanceCount || 0)
|
||||
var bookStatus = Number(data.BookStatus || 0)
|
||||
var bookId = data.BookId || ""
|
||||
if (data.Status !== undefined && Number(data.Status) !== 1) continue
|
||||
if (chance <= 0 && bookStatus !== 0 && bookStatus !== 1 && bookStatus !== 2) continue
|
||||
if (chance > 0) { var cb = qdPost("/argus/api/v1/interaction/rewardcallback", { orderId: orderId, checkRisk: "1" }, cookie); if (!riskConf(cb) && (taskResultOk(cb) || String(resultCode(cb)) === "0")) handled++; continue }
|
||||
if (bookStatus === 0 || bookStatus === 1) { if (!bookId) continue; var add = qdPost("/argus/api/v1/interaction/rewardaddshelf", { orderId: orderId, bookId: bookId }, cookie); if (!riskConf(add) && (taskResultOk(add) || String(resultCode(add)) === "0")) handled++; continue }
|
||||
if (bookStatus === 2) { var rc = qdPost("/argus/api/v1/interaction/rewardcallback", { orderId: orderId, checkRisk: "1" }, cookie); if (riskConf(rc) || (!taskResultOk(rc) && String(resultCode(rc)) !== "0")) continue; var r2 = qdPost("/argus/api/v1/interaction/rewardcallback", { orderId: orderId, checkRisk: "0" }, cookie); if (!riskConf(r2) && (taskResultOk(r2) || String(resultCode(r2)) === "0")) handled++ }
|
||||
}
|
||||
}
|
||||
return { name: "大咖荐书", ok: true, msg: handled ? ("成功处理 " + handled + " 个荐书") : "无待处理荐书", path: "https://magev6.if.qidian.com/argus/api/v2/message/pullmessage" }
|
||||
}
|
||||
function doExtraTask(task, cookie) {
|
||||
if (task.handler === "receiveGifts") return doReceiveGifts(cookie)
|
||||
if (task.handler === "adv") return doAdvTask(cookie)
|
||||
if (task.handler === "daily") return doDailyTask(cookie)
|
||||
if (task.handler === "lottery") return doLotteryTask(cookie)
|
||||
if (task.handler === "exchange") return doWeeklyExchange(cookie)
|
||||
if (task.handler === "chapterCard") return doChapterCard(cookie)
|
||||
if (task.handler === "messageBox") return doMessageBox(cookie)
|
||||
var data = task.body || task.params || {}
|
||||
var res = qidianRequest(task.path, task.method || "GET", data, cookie)
|
||||
var ok = taskResultOk(res)
|
||||
var msg = resultMsg(res) || (ok ? "完成" : ("失败" + (resultCode(res) !== undefined ? " code=" + resultCode(res) : "")))
|
||||
return { name: task.label, ok: !!ok, msg: String(msg || ""), path: task.path }
|
||||
}
|
||||
function doTaskChain(uid, cookie, source, runId) {
|
||||
var startedAt = nowIso()
|
||||
var details = []
|
||||
var checkin = qidianRequest(CHECKIN_PATH, "POST", {}, cookie)
|
||||
if (isCookieExpiredResult(checkin)) {
|
||||
var expiredMsg = resultMsg(checkin) || "登录状态已失效,请更新 Cookie"
|
||||
var expiredDetail = { name: "签到结果", ok: false, msg: expiredMsg, path: CHECKIN_PATH, cookieExpired: true }
|
||||
var expiredResult = { uid: String(uid), ok: false, msg: expiredMsg, time: nowIso(), startedAt: startedAt, source: source || "manual", runId: runId || "", tasks: [expiredDetail], cookieExpired: true }
|
||||
setLastResult(uid, expiredResult)
|
||||
return { uid: uid, ok: false, msg: expiredMsg, raw: checkin, log: expiredResult, cookieExpired: true }
|
||||
}
|
||||
var ok = taskResultOk(checkin)
|
||||
var msg = resultMsg(checkin) || (ok ? "签到请求成功" : ("签到失败" + (resultCode(checkin) !== undefined ? " code=" + resultCode(checkin) : "")))
|
||||
details.push({ name: "签到结果", ok: !!ok, msg: String(msg || ""), path: CHECKIN_PATH })
|
||||
for (var i = 0; i < QD_TASKS.length; i++) {
|
||||
var task = QD_TASKS[i]
|
||||
if (!taskEnabledForUid(task, uid)) continue
|
||||
try { details.push(doExtraTask(task, cookie)) }
|
||||
catch (e) {
|
||||
var tmsg = e && e.message ? e.message : String(e)
|
||||
if (isWhitelistBlockedError(tmsg)) details.push({ name: task.label, ok: true, skipped: true, msg: "签名网关暂不支持该接口,已跳过", rawMsg: tmsg, path: task.path })
|
||||
else details.push({ name: task.label, ok: false, msg: tmsg, path: task.path })
|
||||
}
|
||||
}
|
||||
var failed = []
|
||||
for (var j = 0; j < details.length; j++) if (!details[j].ok && !details[j].skipped) failed.push(details[j].name + ":" + details[j].msg)
|
||||
var finalOk = failed.length === 0
|
||||
var detailMsg = []
|
||||
for (var k = 0; k < details.length; k++) detailMsg.push(details[k].name + (details[k].skipped ? "⏭" : (details[k].ok ? "✅" : "❌")) + (details[k].msg ? "(" + displayMsg(details[k].msg) + ")" : ""))
|
||||
var finalMsg = (finalOk ? "完全体任务完成" : "部分任务失败") + ":" + detailMsg.join(";")
|
||||
var result = { uid: String(uid), ok: !!finalOk, msg: String(finalMsg || ""), time: nowIso(), startedAt: startedAt, source: source || "manual", runId: runId || "", tasks: details }
|
||||
setLastResult(uid, result)
|
||||
return { uid: uid, ok: finalOk, msg: finalMsg, raw: checkin, log: result }
|
||||
}
|
||||
function doCheckin(uid, cookie, source, runId) {
|
||||
return doTaskChain(uid, cookie, source, runId)
|
||||
}
|
||||
function signAll(targetUid, allUsers, source, archiveAfterRun) {
|
||||
source = source || (allUsers ? "cron" : "manual")
|
||||
if (archiveAfterRun === undefined) archiveAfterRun = true
|
||||
var startedAt = nowIso()
|
||||
var runId = startedAt.replace(/[-:.TZ]/g, "").slice(0, 14) + "_" + source
|
||||
var store = getStore()
|
||||
@@ -497,46 +998,100 @@ function signAll(targetUid, allUsers, source) {
|
||||
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: [] })
|
||||
if (archiveAfterRun) archiveRunLog(source + "-after-sign")
|
||||
return emptyMsg
|
||||
}
|
||||
var okCount = 0
|
||||
var failCount = 0
|
||||
var items = []
|
||||
var lines = ["启点读书签到结果:"]
|
||||
var accountReplies = []
|
||||
var accountNotices = []
|
||||
for (var i = 0; i < uids.length; i++) {
|
||||
var uid = uids[i]
|
||||
var prefix = "启点读书签到结果:\n"
|
||||
var suffix = ""
|
||||
if (remarks[uid]) suffix = "(" + remarks[uid] + ")"
|
||||
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)
|
||||
var msg = prefix + (r.ok ? "✅ " : "❌ ") + maskUid(uid) + suffix + ":" + r.msg
|
||||
accountReplies.push(msg)
|
||||
accountNotices.push({ uid: String(uid), owner: bind[String(uid)] || "", msg: 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 errMsg = prefix + "❌ " + maskUid(uid) + suffix + ":" + emsg
|
||||
accountReplies.push(errMsg)
|
||||
accountNotices.push({ uid: String(uid), owner: bind[String(uid)] || "", msg: errMsg })
|
||||
}
|
||||
}
|
||||
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")
|
||||
var tail = []
|
||||
tail.push("汇总:成功 " + okCount + " / 失败 " + failCount + " / 总计 " + uids.length)
|
||||
tail.push("记录时间:" + finishedAt + "(" + sourceName(source) + ")")
|
||||
var archiveLine = ""
|
||||
if (archiveAfterRun) {
|
||||
var ar = archiveRunLog(source + "-after-sign")
|
||||
if (ar && ar.archived) { archiveLine = "日志已归档:" + ar.key; tail.push(archiveLine) }
|
||||
}
|
||||
if (allUsers && source === "cron") {
|
||||
for (var pi = 0; pi < accountNotices.length; pi++) {
|
||||
var notice = accountNotices[pi]
|
||||
var body = notice.msg + "\n" + tail.join("\n")
|
||||
if (!pushToUser(notice.owner, body)) debug("QDReader cron push skipped for uid " + notice.uid + ": empty owner or push unavailable")
|
||||
}
|
||||
return tail.join("\n")
|
||||
}
|
||||
if (accountReplies.length > 1) {
|
||||
for (var ai = 0; ai < accountReplies.length; ai++) reply(accountReplies[ai])
|
||||
return tail.join("\n")
|
||||
}
|
||||
return accountReplies[0] + "\n" + tail.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 shortTime(iso) {
|
||||
if (!iso) return "未知时间"
|
||||
var s = String(iso)
|
||||
var m = s.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})/)
|
||||
if (m) return m[1] + " " + m[2]
|
||||
return beijingTime(iso).slice(0, 16)
|
||||
}
|
||||
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 displayMsg(msg) {
|
||||
msg = trim(msg || "无结果")
|
||||
if (!msg) return "无结果"
|
||||
msg = msg.replace(/\s+/g, " ")
|
||||
if (msg.length > 60) msg = msg.slice(0, 57) + "..."
|
||||
return msg
|
||||
}
|
||||
function accountName(uid, remarks) {
|
||||
return maskUid(uid) + (remarks && remarks[uid] ? "[" + remarks[uid] + "]" : "")
|
||||
}
|
||||
function compactTaskMsg(msg) {
|
||||
msg = displayMsg(msg)
|
||||
msg = msg.replace(/^完全体任务完成:/, "")
|
||||
msg = msg.replace(/^部分任务失败:/, "")
|
||||
return msg
|
||||
}
|
||||
function formatResultLine(uid, r, remarks, emptyText) {
|
||||
var name = accountName(uid, remarks)
|
||||
if (!r) return "- " + name + ":⚪ " + (emptyText || "暂无执行记录")
|
||||
var status = r.ok ? "✅" : "❌"
|
||||
var line = "- " + name + ":" + status + " " + compactTaskMsg(r.msg)
|
||||
if (r.time) line += "\n 时间:" + shortTime(r.time)
|
||||
return line
|
||||
}
|
||||
function formatRunSummary(log, label, key) {
|
||||
log = log || {}
|
||||
var lines = []
|
||||
lines.push("批次:" + (key || log.runId || "最近") + "(" + (label || "最近记录") + ")")
|
||||
lines.push("时间:" + shortTime(log.finishedAt) + " | 来源:" + sourceName(log.source))
|
||||
lines.push("统计:成功 " + (log.okCount || 0) + " / 失败 " + (log.failCount || 0) + " / 总计 " + (log.total || 0))
|
||||
return lines
|
||||
}
|
||||
function queryStatus(targetUid) {
|
||||
var store = getStore()
|
||||
@@ -550,20 +1105,22 @@ function queryStatus(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]
|
||||
var lines = [targetUid ? "启点查询|单账号" : "启点查询|最近签到"]
|
||||
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 + "(发送启点查询可按归档判断昨日签到)")
|
||||
lines = lines.concat(formatRunSummary(arun, "归档", archiveInfo.key))
|
||||
lines.push("明细:")
|
||||
for (var ai = 0; ai < uids.length; ai++) lines.push(formatResultLine(uids[ai], archiveInfo.itemMap[String(uids[ai])], remarks, "归档中暂无该账号记录"))
|
||||
return lines.join("\n")
|
||||
}
|
||||
if (!targetUid && run && run.finishedAt) lines = lines.concat(formatRunSummary(run, "最近", ""))
|
||||
else if (run && run.finishedAt) lines.push("时间:" + shortTime(run.finishedAt) + " | 来源:" + sourceName(run.source))
|
||||
else lines.push("批次:暂无")
|
||||
lines.push("明细:")
|
||||
for (var i = 0; i < uids.length; i++) lines.push(formatResultLine(uids[i], last[uids[i]], remarks, "暂无执行记录"))
|
||||
if (archiveInfo) lines.push("归档:" + archiveInfo.key)
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
@@ -571,17 +1128,21 @@ 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 accountMenuText(store, bind) {
|
||||
var uids = visibleUids(store || getStore(), bind || getBindStore(), false)
|
||||
var remarks = remarksForStore(store || {})
|
||||
var lines = ["启点账号管理", "账号列表如下:"]
|
||||
if (!uids.length) lines.push("暂无账号,请回复 0 添加/更新账号")
|
||||
else {
|
||||
for (var i = 0; i < uids.length; i++) {
|
||||
var uid = uids[i]
|
||||
lines.push((i + 1) + "、" + maskUid(uid) + (remarks[uid] ? "[" + remarks[uid] + "]" : ""))
|
||||
}
|
||||
}
|
||||
lines.push("0、添加/更新账号")
|
||||
lines.push("-删除账号")
|
||||
lines.push("q、退出")
|
||||
return lines.join("\n")
|
||||
}
|
||||
function queryMenuText() {
|
||||
return [
|
||||
@@ -589,6 +1150,7 @@ function queryMenuText() {
|
||||
"1. 查看最近签到记录",
|
||||
"2. 查询单个账号最近记录",
|
||||
"3. 立即执行签到",
|
||||
"说明:附加任务开关在 启点账号 → 账号任务配置 中按账号管理",
|
||||
"q. 退出",
|
||||
"请回复序号"
|
||||
].join("\n")
|
||||
@@ -650,29 +1212,48 @@ function accountDetailText(uid, store, bind) {
|
||||
"uid: " + maskUid(uid),
|
||||
"备注: " + (remarks[uid] || "未设置"),
|
||||
"cookie: " + maskCookie(store[uid]),
|
||||
formatStatusLine(uid, last, remarks),
|
||||
taskStatusLines(uid).replace(/\n回复:[\s\S]*$/, ""),
|
||||
formatResultLine(uid, last[uid], remarks, "暂无执行记录"),
|
||||
"修改备注请回复新备注名,发送 清空 可删除备注,发送 q 退出"
|
||||
].join("\n")
|
||||
}
|
||||
function accountConfigText(uid, store, bind) {
|
||||
var detail = accountDetailText(uid, store, bind)
|
||||
if (/^(无权|未找到)/.test(detail)) return detail
|
||||
return detail + "\n\n账号操作:\n1、修改备注\n2、任务配置\n3、立即签到该账号\n-删除该账号\nq、返回/退出"
|
||||
}
|
||||
function accountConfigMenu(uid, store, bind) {
|
||||
if (!uid) return reply("未选择账号,已退出")
|
||||
var cfg = accountConfigText(uid, store, bind)
|
||||
reply(cfg)
|
||||
if (/^(无权|未找到)/.test(cfg)) return
|
||||
var op = listenInput(60000)
|
||||
if (!op || /^(q|退出|取消|返回)$/i.test(op)) return reply("已退出")
|
||||
if (/^(1|备注|改名|名称)$/i.test(op)) {
|
||||
reply("请回复新备注名;发送 清空 可删除备注;q 退出")
|
||||
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] + ")" : ""))
|
||||
}
|
||||
if (/^(2|任务|配置|开关)$/i.test(op)) return taskMenuForUid(uid)
|
||||
if (/^(3|签到|执行|立即)$/i.test(op)) return reply(signAll(uid, false, "menu"))
|
||||
if (/^-1$/.test(op) || /^(删除|delete|del|rm)$/i.test(op)) return reply(handleDelete("删除 " + uid, store, bind))
|
||||
return reply("未识别选项,已退出")
|
||||
}
|
||||
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] + ")" : ""))
|
||||
return accountConfigMenu(uid, store, bind)
|
||||
}
|
||||
function accountMenu(store, bind) {
|
||||
reply(accountMenuText())
|
||||
var uids = visibleUids(store, bind, false)
|
||||
reply(accountMenuText(store, bind))
|
||||
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)) {
|
||||
if (/^(0|a|add|添加|新增|更新)$/i.test(op)) {
|
||||
reply("请在60秒内发送 QX 抓到的完整 Cookie(q:退出)")
|
||||
var ck = listenInput(60000)
|
||||
if (!ck || /^(q|退出|取消)$/i.test(ck)) return reply("已退出")
|
||||
@@ -680,13 +1261,18 @@ function accountMenu(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))
|
||||
var n = parseInt(op, 10)
|
||||
if (!isNaN(n) && String(n) === op.replace(/^\+/, "")) {
|
||||
if (n > 0) {
|
||||
if (n > uids.length) return reply("编号无效,已退出")
|
||||
return accountConfigMenu(uids[n - 1], store, bind)
|
||||
}
|
||||
if (n < 0) {
|
||||
var idx = Math.abs(n)
|
||||
if (idx > uids.length) return reply("编号无效,已退出")
|
||||
return reply(handleDelete("删除 " + uids[idx - 1], store, bind))
|
||||
}
|
||||
}
|
||||
if (/^(5|清空|全部)$/i.test(op)) return reply(handleDelete("删除 all", store, bind))
|
||||
return reply("未识别选项,已退出")
|
||||
}
|
||||
function queryMenu() {
|
||||
@@ -712,13 +1298,15 @@ function handleDelete(arg, store, bind) {
|
||||
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)
|
||||
var allPrefs = getTaskPrefs(); for (var p = 0; p < uids.length; p++) delete allPrefs[uids[p]]
|
||||
setStore(store); setBindStore(bind); setRemarkStore(remarks); setTaskPrefs(allPrefs)
|
||||
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)
|
||||
var prefs = getTaskPrefs(); delete prefs[duid]
|
||||
setStore(store); setBindStore(bind); setRemarkStore(remarks); setTaskPrefs(prefs)
|
||||
return "已删除 uid: " + duid
|
||||
}
|
||||
function normalizeCookieInput(arg) {
|
||||
@@ -801,7 +1389,7 @@ function main() {
|
||||
}
|
||||
if (cmd === "account") { accountMenu(store, bind); return }
|
||||
if (cmd === "status") { queryMenu(); return }
|
||||
if (cmd === "ck" && /^(查询|状态|status|list|账号|管理)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;快速提交CK:启点ck <cookie>"); return }
|
||||
if (cmd === "ck" && /^(查询|状态|status|list|账号|管理|任务|配置)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;任务配置在 启点账号 → 账号任务配置;快速提交CK:启点ck <cookie>"); return }
|
||||
var delMsg = handleDelete(arg, store, bind)
|
||||
if (delMsg !== null) { reply(delMsg); return }
|
||||
if (cmd === "ck") {
|
||||
|
||||
+1215
-98
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user