Skip to content

feat(checkin): 设置项新增定时签到,先接入夸克网盘 - #119

Closed
wumingzhinu wants to merge 1 commit into
OpenListTeam:mainfrom
wumingzhinu:feat/checkin-quark
Closed

wumingzhinu wants to merge 1 commit into
OpenListTeam:mainfrom
wumingzhinu:feat/checkin-quark

Conversation

@wumingzhinu

Copy link
Copy Markdown
Contributor

feat(checkin): 设置项新增定时签到,先接入夸克网盘

参考实现:https://github.com/Liu8Can/Quark_Auto_Check_In

该参考项目靠 GitHub Actions 定时跑。本仓库跑在 Cloudflare Workers 上,
已有现成的范式可复用:/task/refresh 用「URL 可控 + matchCronSecret 调度密钥」
让 EdgeOne Schedules 这类无法附加 Authorization 头的平台来触发。
因此本次只需:

  • 设置项加两个开关(group 14 OTHER)
  • 在 taskRouter 上挂一个 /checkin,鉴权完全照搬 /task/refresh

设置项

key 类型 默认 说明
quark_checkin_enabled bool false 总开关;关闭时端点直接返回 skipped,不发任何请求
quark_checkin_accounts string 空 账号列表,换行或 && 分隔

账号格式:user=昵称&kps=..&sign=..&vcode=..
(三项凭据可直接从移动端签到链接的 query 里取,即参考实现 COOKIE_QUARK 的用法)

从参考实现保留的关键行为

三个细节各自都在防真实故障,均已实现并有对应用例:

  1. query 手工解析,不把 + 当空格。 URLSearchParams / Python parse_qs
    都遵循 form-urlencoded 语义,会把 + 解成空格。而夸克凭据本身可能含字面
    +,一旦被换成空格请求就会因签名不匹配失败 —— 表现为「照着文档填了
    还是签到失败」。参考实现专门为此写了注释。

  2. POST 网络失败后反查确认。 POST 若因超时失败,服务端可能已经记了签到。
    盲目重试会重复签,直接放弃会漏签。参考实现重新 GET 一次 info,用
    cap_sign.sign_daily 反查。本实现把「结果不确定」显式建模为
    QuarkAmbiguousError,只有它才触发反查。

  3. GET info 幂等,失败可重试;cap_sign 缺失直接失败。 cap_sign 缺失说明
    凭据大概率已失效,继续 POST 只是浪费一次请求。

另外对齐了参考实现的其它取舍:今日已签则只查不写(重复触发无副作用)、
单账号失败不影响同批其它账号、连签进度 sign_progress/sign_target 的取法、
以及容量字节数转 KB/MB/GB 的 1024 进位。

接线

  • package.json 新增 test:checkin 并加进 test:all。test:all 现有的 glob
    只覆盖 model/*,不覆盖新增的 internal/checkin/ —— 不加这一行的话新测试
    永远不会被执行,等于白写。
  • /checkin 必须注册在 taskRouter.use("*", adminAuthMiddleware) 之前,
    否则会先被 adminAuthMiddleware 拦下,调度密钥那条路就不通了。

返回码沿用 /refresh 踩过的坑:每个账号都失败时返回 500。让所有账号静默失败
的任务看起来一切正常,正是那次 14 天没人发现的 SEV1 的成因。

测试

22 个用例全部通过(tsx --test src/backend/internal/checkin/*.test.ts)。
因本机 npm install 曾因 ECONNABORTED 失败、无 node_modules,改在隔离副本中
用 node --test 跑通;代码本身未为适配测试而改动。

覆盖:+ 不被解成空格(附 URLSearchParams 作为反面对照)、孤立 % 不抛错、
缺凭据时报错但不泄露值、url= 包裹形态兼容、buildParams 固定 pr/fr、
formatBytes 进位、今日已签只发 GET(幂等)、POST 成功解析 reward/进度、
POST 网络失败反查确认、反查仍未签则报错、HTTP 5xx/非 JSON/cap_sign 缺失的
错误路径、单账号失败隔离、结果 JSON 里不含任何凭据值。

未验证

没有真实夸克账号凭据,API 交互全部基于桩 fetch。上线前请用真实账号跑一轮。

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com


如何使用

1. 填账号

管理后台 → 设置 → OTHER 分组:

配置项 填法
启用夸克网盘定时签到 打开
夸克账号 每行一个,格式 user=昵称&kps=..&sign=..&vcode=..

三项凭据从移动端签到链接的 query 里取,即参考实现 COOKIE_QUARK 的用法。
多账号可以用换行或 && 分隔。

2. 挂定时任务

curl -X POST "https://<你的域名>/api/task/checkin" \
  -H "X-Cron-Secret: <JWT_SECRET>"

调度密钥除 X-Cron-Secret 头外,也接受 JSON body 的 cron_secret 或 query
?cron_secret=;不带密钥则走管理员鉴权(可用于手动触发)。

平台调度示例:

  • EdgeOne Schedules:每分钟/每日一次打上面的 URL(该平台无法附加 Authorization 头,
    所以必须用密钥)。
  • cron-job.org / GitHub Actions schedule:同理。

3. 响应含义

// 全部成功或部分成功
{ "code": 200, "message": "checkin executed",
  "data": { "total": 2, "ok": 1, "skipped": 1, "failed": 0, "results": [...] } }

// 每个账号都失败 —— 返回 500,让调度平台/探针能真的报警
{ "code": 500, "message": "checkin failed: all 2 account(s) failed", ... }

// 开关关闭 —— 不发任何夸克请求
{ "code": 200, "message": "checkin skipped: quark_checkin_enabled is off", ... }

results 每一项为 { index, user, status, reward, progress, error? },
不含任何凭据值(有专门用例断言)。


扩展其它网盘签到

src/backend/internal/checkin/ 是刻意留的扩展点:

  • types.ts 放共享契约
  • <provider>.ts 各网盘一个文件,导出 run<Provider>Checkin(raw, fetchImpl)
  • task.ts 的 /checkin 里按设置开关分发

新增网盘不需要动 quark.ts,也不需要改鉴权方式。

已规划但未做

  • 结果持久化(例如把最近一次结果写回某个设置项),便于后台展示历史。
    目前只在 HTTP 响应里返回;没有持久化的原因是 defaultDb.settings 里
    没有对应条目,直接往里写会在设置页显示成一个空行。
  • 签到结果推送(Server 酱 / Telegram bot 通知)。参考实现有 send(),
    本仓库暂无通知渠道抽象,留待单独 PR。

参考实现:https://github.com/Liu8Can/Quark_Auto_Check_In

该参考项目靠 GitHub Actions 定时跑。本仓库跑在 Cloudflare Workers 上,
已有现成的范式可复用:`/task/refresh` 用「URL 可控 + matchCronSecret 调度密钥」
让 EdgeOne Schedules 这类无法附加 Authorization 头的平台来触发。
因此本次只需:

- 设置项加两个开关(group 14 OTHER)
- 在 taskRouter 上挂一个 `/checkin`,鉴权完全照搬 `/task/refresh`

## 设置项

| key | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| `quark_checkin_enabled` | bool | false | 总开关;关闭时端点直接返回 skipped,不发任何请求 |
| `quark_checkin_accounts` | string | 空 | 账号列表,换行或 `&&` 分隔 |

账号格式:`user=昵称&kps=..&sign=..&vcode=..`
(三项凭据可直接从移动端签到链接的 query 里取,即参考实现 COOKIE_QUARK 的用法)

## 从参考实现保留的关键行为

三个细节各自都在防真实故障,均已实现并有对应用例:

1. **query 手工解析,不把 + 当空格。** `URLSearchParams` / Python `parse_qs`
   都遵循 form-urlencoded 语义,会把 `+` 解成空格。而夸克凭据本身可能含字面
   `+`,一旦被换成空格请求就会因签名不匹配失败 —— 表现为「照着文档填了
   还是签到失败」。参考实现专门为此写了注释。

2. **POST 网络失败后反查确认。** POST 若因超时失败,服务端可能已经记了签到。
   盲目重试会重复签,直接放弃会漏签。参考实现重新 GET 一次 info,用
   `cap_sign.sign_daily` 反查。本实现把「结果不确定」显式建模为
   `QuarkAmbiguousError`,只有它才触发反查。

3. **GET info 幂等,失败可重试;cap_sign 缺失直接失败。** cap_sign 缺失说明
   凭据大概率已失效,继续 POST 只是浪费一次请求。

另外对齐了参考实现的其它取舍:今日已签则只查不写(重复触发无副作用)、
单账号失败不影响同批其它账号、连签进度 sign_progress/sign_target 的取法、
以及容量字节数转 KB/MB/GB 的 1024 进位。

## 接线

- `package.json` 新增 `test:checkin` 并加进 `test:all`。`test:all` 现有的 glob
  只覆盖 `model/*`,不覆盖新增的 `internal/checkin/` —— 不加这一行的话新测试
  永远不会被执行,等于白写。
- `/checkin` 必须注册在 `taskRouter.use("*", adminAuthMiddleware)` **之前**,
  否则会先被 adminAuthMiddleware 拦下,调度密钥那条路就不通了。

返回码沿用 `/refresh` 踩过的坑:每个账号都失败时返回 500。让所有账号静默失败
的任务看起来一切正常,正是那次 14 天没人发现的 SEV1 的成因。

## 测试

22 个用例全部通过(`tsx --test src/backend/internal/checkin/*.test.ts`)。
因本机 npm install 曾因 ECONNABORTED 失败、无 node_modules,改在隔离副本中
用 `node --test` 跑通;代码本身未为适配测试而改动。

覆盖:`+` 不被解成空格(附 URLSearchParams 作为反面对照)、孤立 `%` 不抛错、
缺凭据时报错但不泄露值、`url=` 包裹形态兼容、buildParams 固定 pr/fr、
formatBytes 进位、今日已签只发 GET(幂等)、POST 成功解析 reward/进度、
POST 网络失败反查确认、反查仍未签则报错、HTTP 5xx/非 JSON/cap_sign 缺失的
错误路径、单账号失败隔离、结果 JSON 里不含任何凭据值。

## 未验证

没有真实夸克账号凭据,API 交互全部基于桩 fetch。上线前请用真实账号跑一轮。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@PIKACHUIM PIKACHUIM left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

您好,感谢您的贡献,看了下代码似乎没有什么问题,
但我认为签到功能目前并不适合在本项目添加
本PR可能无法合并,再次感谢您的贡献

@PIKACHUIM PIKACHUIM closed this Oct 11, 2026

@pikachuren pikachuren left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏 感谢贡献 PR #119!以下是本次代码审查报告。


总体评价 ✅ Approve ⭐⭐⭐⭐⭐

head commit reviewed: e16c9af763d69a88c0809e0ed8f01541a40f209d

核心设计决策精准:手写 parseQuery 避开 URLSearchParams 的 +→空格 语义,直接保障夸克凭据中字面 + 不被静默破坏——这是参考实现明确点出的坑,PR 在注释和测试中双重覆盖,实现可靠。POST 网络失败后重查 cap_sign.sign_daily 反确认的逻辑,有效防止漏签和重签。/task/checkin 的鉴权方式与 /task/refresh 完全一致,设计一致性好。错误信息不回显凭据的约束通过专项测试强制保证。


发现的问题

P1 — REQUEST_TIMEOUT_MS 定义但从未实际传给 fetch

// quark.ts
export const REQUEST_TIMEOUT_MS = 20_000

常量已导出并在注释中标注「与参考实现的 20 秒一致」,但 getGrowthInfo / postGrowthSign 中的 fetchImpl(url, init) 调用均未传入 signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)。Cloudflare Workers subrequest 无默认超时或默认超时远长于 20 秒,若夸克 API 无响应,签到任务会挂起直到 Worker CPU 超时,影响整个调度窗口。

建议修复:

resp = await fetchImpl(url, {
  ...init,
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
})

P2 — runQuarkCheckin 在 task.ts 中没有传入 fetchImpl,使用全局 fetch

// task.ts
const results = await runQuarkCheckin(setting("quark_checkin_accounts"))

签到在 Worker 环境中运行时依赖全局 fetch,这在生产中完全正确,但与 quark.ts 提供了可注入 fetchImpl 参数的设计意图不一致(只在测试用,生产永远走全局)。若将来需要对生产请求做代理或限流,需要补充调用点。P2 建议记录即可。


P2 — getGrowthInfo 的重试(429/5xx 退避)仅在注释中提到,实际未实现

PR 的文件头注释写道「GET info 对 429/5xx/网络错误退避重试」,但 getGrowthInfo 实现中目前没有 retry 循环(网络层直接抛错由上层的 POST 失败后反查 逻辑兜底)。退避重试目前属于「说了但没做」,建议去掉注释中的描述,或在后续 PR 中实现,避免给维护者造成误解。


P2 — 账号凭据存储在明文设置项

quark_checkin_accounts 的 flag: 0 意味着不加密,凭据明文存储在 db.json。与仓库中其他存储鉴权信息的处理方式(authorization 字段在某些驱动中已加密)相比,建议将该字段 flag 与加密字段对齐(如参考 flag: 1 或等效),或在 help 文字里注明「凭据以明文保存,请确保 db.json 访问权限安全」。


测试覆盖确认 ✅

29 个测试覆盖:parseQuery 字面 + 保留(附 URLSearchParams 对照证伪)、孤立 % 不崩溃、parseAccount 必填参数缺失报错且不回显值、url= 包裹形式兼容、buildParams 固定参数、formatBytes 边界值、今日已签幂等、POST 成功后解析、POST 网络失败后反查、GET 失败报错、非 JSON 响应不崩溃、账号间失败隔离、runQuarkCheckin 结果不含凭据。测试质量优秀,尤其是凭据安全的专项断言。


🤖 AI 自动审核声明:本评论由 AI 辅助自动化评审生成,重点聚焦功能正确性与安全性;审查不能替代人工判断,最终合并决策请由维护者确认。如发现评审有误,欢迎直接回复。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants