Skip to content

Commit 88d1e32

Browse files
committed
B4: 后台任务可视化,与运行时配置合并为「任务与配置」页
问题:TaskRunner 跑着 6 类循环,但除了失败时一行 warning,没有任何地方能看到 「上次什么时候跑的、结果如何」,也没有端点暴露。调研确认项目内不存在后台任务页 (无 TasksPage / 无 /api/tasks,git 全历史与文档均无)——这是新增而非找回。 后端 - 新增 tasks/status.py:TASK_SPECS 任务清单 + TaskStatusStore 进程内运行态。 刻意不落库:任务状态回答的是「这次进程活着的时候谁跑过」,重启归零比编造一条 重启前记录更诚实,也省掉新表 + 保留期清理 + 老库迁移(无 schema 变更)。 - TaskRunner._guarded 增加 key:返回 None 表示本轮 no-op(签到未到点 / 活跃上报 未启用),不入账——否则页面会显示「签到刚刚跑过」而当天其实一次都没签; 异常入账 last_error,否则「一直在失败」会显示成「尚未执行」。 - TaskRunner.task_status():周期与开关取当前生效值(热更后刷新即变),非装配快照; 未装配的任务不出现在清单里。 - HotSetting 增 task 归属字段并透出到 snapshot();新增 GET /api/tasks(admin), 带 server_time 供前端用服务端时钟算「距今多久」;runner 缺失时返回空列表不 500。 前端 - 导航与页头「运行时配置」→「任务与配置」;SettingsPage 重组为任务卡片区 (运行态 + 该任务的配置项,复用 SettingRow)+「网关与调度」区;归属由后端下发, 前端不硬编码 key 列表。 - 新增 useTasks(refetchInterval 30s)、display.formatAgo / taskReportLabel。 文档 - PROPOSAL Q38(含 Q11/Q34 措辞)、TECHNICAL §3.12 + §3.8/§6.1/目录树、 README 中英、NOTICE 增列 ithtelab/workbuddy-manager。 质量 - 后端 ruff + pytest 100% 行/分支覆盖(1328 passed);前端 tsc + vitest(158) + build。 - 生产实例实测 /api/tasks:6 任务、周期取 DB 覆盖值(探测 30 分钟)、启动首轮入账。
1 parent 774447f commit 88d1e32

18 files changed

Lines changed: 1096 additions & 86 deletions

‎NOTICE‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,10 @@ Copyright (c) 2026 Hongsheng Luo (Robbs)
2525
- xueyue33/codebuddy2api - https://github.com/xueyue33/codebuddy2api
2626
- Sliverkiss/traework2api - https://github.com/Sliverkiss/traework2api
2727

28+
- workbuddy-manager - https://github.com/ithtelab/workbuddy-manager
29+
(提供后台任务可视化页面的设计参考:任务清单 + 最近一次运行结果 + 自动刷新;
30+
其核心教训——容器重建即丢、需要采集落库——正是本项目选择「进程内运行态、
31+
不新增表」并明确「重启归零」的对照依据)
32+
2833
本项目的代码为独立实现,不复制上述项目的源代码。
2934
上游服务的协议细节来自对客户端行为的观察,不属于上述项目的版权范围。

‎PROPOSAL.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
| Q8 | 协议出口 | v1 仅 OpenAI;v1.1 加 Anthropic |
1919
| Q9 | 存储 | SQLite,schema 重新设计,凭证入加密列 |
2020
| Q10 | 配额 | 不做配额,只做按人统计 |
21-
| Q11 | 前端范围 | Dashboard / 凭证 / API Key / 统计 / Playground / 运行时配置 / 登录(Q34 推翻「无设置页」:13 项可热更配置走管理台,其余配置仍走 env) |
21+
| Q11 | 前端范围 | Dashboard / 凭证 / API Key / 统计 / Playground / 任务与配置 / 登录(Q34 推翻「无设置页」:13 项可热更配置走管理台,其余配置仍走 env;Q38 把该页与后台任务合并,页名「任务与配置」) |
2222
| Q12 | 调度策略 | 统一健康度 + 到期积分指标 + 冷却状态机,保留手动 pin |
2323
| Q13 | Anthropic | v1.1,架构预留中立事件层 |
2424
| Q14 | 测试 | 全量 100%(行 + 分支),契约测试优先 |
@@ -39,10 +39,11 @@
3939
| Q31 | 到期积分排序 | 两级字典序:主窗口(36h)+ 次窗口(7 天)内到期积分总量依次做排序键(均 env 可配);落库到期阶梯而非单一日期 |
4040
| Q32 | Responses 出口 | v1 只做 `chat/completions` 子集:`POST /v1/responses` 与 chat 共用同一 executor,出口 translator 可注入;`include`/`store`/`previous_response_id` 按 Codex CLI 实测取舍(见 TECHNICAL §3.7) |
4141
| Q33 | 凭证暂停语义 | 复用现有 `enabled`(不新增 `manual_disabled` 列):`enabled=0` 实测已只摘对话流量,后台任务(签到/刷新/成长/探测)只认 `disabled`;UI 文案统一为「暂停/取消暂停」以区别于系统禁用后的「恢复」(见计划 B3.1 实测收窄) |
42-
| Q34 | 运行时配置热更 | **推翻 Q11 的「无设置页」**:新增 `runtime_settings` 表 + `RuntimeSettings` 覆盖层 + 管理台第 7 页「运行时配置」。白名单 13 项(默认模型 / 模型黑名单 / 到期两个窗口 / 会话粘性 TTL / 成长不可逆开关 / 成长与探测周期 / 两个节流窗口 / CB 聊天最小间隔 / 活跃上报开关与时点)改完立即生效,无需重启;**DB 覆盖值优先于 .env**,UI 与日志明示,可「恢复默认」清掉覆盖。启动期项(密钥 / 端口 / 数据目录 / 上游白名单)不进白名单——它们决定进程如何启动,运行期改只会让内存与磁盘静默分叉 |
42+
| Q34 | 运行时配置热更 | **推翻 Q11 的「无设置页」**:新增 `runtime_settings` 表 + `RuntimeSettings` 覆盖层 + 管理台第 7 页(Q38 后更名为「任务与配置」,与后台任务运行态同页)。白名单 13 项(默认模型 / 模型黑名单 / 到期两个窗口 / 会话粘性 TTL / 成长不可逆开关 / 成长与探测周期 / 两个节流窗口 / CB 聊天最小间隔 / 活跃上报开关与时点)改完立即生效,无需重启;**DB 覆盖值优先于 .env**,UI 与日志明示,可「恢复默认」清掉覆盖。启动期项(密钥 / 端口 / 数据目录 / 上游白名单)不进白名单——它们决定进程如何启动,运行期改只会让内存与磁盘静默分叉 |
4343
| Q35 | token 到期展示与预警 | `credentials` 增列 `token_expires_at` / `token_issued_at`(`SCHEMA_VERSION` 10→11)。到期时间优先取凭证显式 `expires_at`,缺失/非法时回落到 access token 的 **JWT `exp`**;签发时间取 JWT `iat`(新增渠道中立的 `provider/token_expiry.py`)——**实测 CodeBuddy 的 token 响应(OAuth 登录与刷新)不带任何到期字段**,只看 `expires_at` 会恒为 0,既让管理台看不到到期、也让 `needs_refresh` 永不触发(token 过期即被 401 硬禁用,且 revive 不自愈)。两边都取不到时为 0 = 未知,**不猜本地 TTL**。展示为独立「token 剩余」列,只给剩余时间;**进度条与「最后续期」最初的设计已移除**(两渠道 token 寿命 55 天 vs 14 天,同一条无可比性;`iat` 需二次推理才有意义,不值一行),`iat` 仍落库供诊断。老库不批量回填:列表读到时按需从密文派生,写回后走列值 |
4444
| Q36 | 积分变动流水 | 新增 `credit_events` 表(`SCHEMA_VERSION` 11→12),在额度探测写回的**同一事务**里比对余额、只增记一条。**计划原文要求 `source` 标注来源(签到/成长/对话),但实测三类证据都拿不到真实归因**:diff 只能看到区间净变化,这段区间里签到、成长领取与对话消耗可能同时发生;`growth_events` 无积分快照;上游接口本就不打日志。故 `source` **改为只表达归因已知度**——`observed`(常规探测区间)/ `sync`(首次建立基线),另加 `window_start` 记录变化覆盖时段,前端文案一律说「净变化」而非「签到 +N」。余额未变不记(避免每轮 0 行淹没);任一端未知仍记但 `delta` 为空(「变未知」是该追的异常,绝不量化成 0)。保留期与 `usage_events` 一致(90 天) |
4545
| Q37 | 池健康与多 Key 出口 | `GET /healthz` 返回 `{status, service, version, credentials:{total,ready,cooling,paused,disabled}}`(保留 `GET /health` 作纯存活探针):`ready` 复用调度器的 `Candidate.is_selectable` 口径,五类互斥且合计 = total——**计划原文只列 4 类**,但项目已明确区分「系统禁用」与「用户暂停」(见 Q33/B3.1),少一类会让计数对不上,故补 `paused`。`api_keys` 增 `provider_binding`(`codebuddy`/`trae`/空=自动)与 `allowed_ips`(逗号分隔 IP/CIDR,空=不限制),`SCHEMA_VERSION` 12→13;`deps.api_key_user` 升级为返回 `ApiKeyPrincipal`(用户名 + 绑定),并**在鉴权当场**判定来源 IP。IP 白名单**默认不信 `X-Forwarded-For`**(客户端可写),仅 `TRUST_PROXY=true` 时采信,且取 XFF **最后一个**条目(`$proxy_add_x_forwarded_for` 语义下那是紧邻受信代理所见地址)——因此只适用于「本服务前恰好一层受信反代」的部署。绑定渠道在 `executor` 收窄候选上游:模型归属别家渠道时 400 并给出实际归属,目录未就绪时保守放行。**不做**每 Key 配额/多租户(与 Q10 冲突) |
46+
| Q38 | 后台任务可视化(「任务与配置」页) | 管理台原「运行时配置」页与后台任务**合并**为一页:13 项配置按 `HotSetting.task` 归属进任务卡片(卡片 = 运行态 + 该任务的可热更项),无归属的进「网关与调度」区;新增 `GET /api/tasks`(admin)下发 6 类任务的运行态,前端 30s 自动刷新。**运行态只存进程内、不落库**(`tasks/status.py`):任务状态是「本进程内谁跑过」,重启归零比编造一条重启前记录更诚实,也省掉新表 + 保留期清理 + 老库迁移;因此**无 schema 变更**。**no-op 轮次不入账**(返回 `None` 表示未到点/未开启)——否则签到会显示成「刚刚跑过」而当天其实没签;异常入账(`last_error`),否则「一直在失败」会显示成「尚未执行」。周期与开关取**当前生效值**(热更后刷新即变),不是装配快照 |
4647

4748
## 2. 目标与非目标
4849

@@ -247,6 +248,9 @@ v1 只接 OpenAI 出口,但上游 SSE 解析到「中立事件」这一步独
247248
- **token 到期**(Q35):`credentials.token_expires_at`(显式 `expires_at` 优先,缺失回落 access token 的 JWT `exp`;0 = 未知)与 `token_issued_at`(JWT `iat`,即最后续期;0 = 未知,仅落库供诊断)。展示层只给「token 剩余」一列——进度条与「最后续期」已移除(两渠道寿命 55 天 vs 14 天无可比量纲;`iat` 需与剩余天数一起看才有意义,不值一行)。派生逻辑在渠道中立的 `provider/token_expiry.py`,**不猜本地 TTL**
248249
- **积分流水**(Q36):`credit_events` 记两次额度探测之间的净变化(含 `window_start` 覆盖区间与归因已知度 `source`);**不是动作归因**——上游不打日志,diff 分不出分数是谁加的。保留期同 `usage_events`(90 天)
249250
- 签到去重与模型列表缓存均进程内实现,不进库
251+
- **后台任务运行态**(Q38)同样进程内、**不建表**:「上次运行 / 最近结果」只描述本进程,
252+
重启归零是诚实语义;落库要新表 + 保留期清理 + 老库迁移,而跨重启的历史价值有限
253+
(业务留痕已有 `growth_events` / `credit_events` / `usage_events`)
250254

251255
DDL 以 [src/db/schema.sql](../src/db/schema.sql) 为准,补充实现细节见 [TECHNICAL.md §7](TECHNICAL.md)。
252256

‎README.en.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ upstream channels, with a shared credential pool, unified scheduling, and per-us
2828
- **Per-credential pause**: the admin credential menu's "Pause" removes one credential from *chat traffic only* — quota probing, token refresh, daily check-in, growth-center and activity-report tasks keep running (they only honor the system hard-disable `disabled`). Unlike "Disabled", which means the upstream rejected the session and requires re-login plus "Restore"
2929
- **Pool health endpoint**: `GET /healthz` reports `{status, service, version, credentials:{total,ready,cooling,paused,disabled}}` (unauthenticated) so monitors can alert when the pool is exhausted (`ready=0`, i.e. alive but unusable) — the bare `GET /health` stays as a pure liveness probe. The five buckets are mutually exclusive and sum to `total`, using the scheduler's own "selectable" definition
3030
- **Per-key routing policy**: an API key can be bound to one provider (`provider_binding`) and/or restricted to source IPs (`allowed_ips`, comma-separated IP/CIDR, empty = unrestricted). A bound key that requests a model owned by the other provider gets a 400 naming the real owner instead of silently re-routing or wasting an upstream call. Source IPs are checked at auth time; `X-Forwarded-For` is **ignored by default** (clients can forge it) and only honored with `TRUST_PROXY=true`, where the *last* XFF entry is used — so that flag fits exactly one trusted reverse proxy in front. No per-key quotas / multi-tenancy
31-
- **Runtime-configurable settings**: 13 settings (default model, model blocklist, both expiry windows, conversation-sticky TTL, irreversible growth actions, growth/probe intervals, both pacer bounds, CodeBuddy chat interval, activity-report toggle/hour) can be changed from the admin UI's *Runtime settings* page and take effect immediately — no restart. **DB overrides win over `.env`**; the page marks each row as "DB override" and offers "Reset to default" to fall back to `.env`. Startup-only knobs (`APP_SECRET`, `PORT`, `DATA_DIR`, allowlists) are deliberately excluded
31+
- **Runtime-configurable settings & background-task view** (*Tasks & Settings* page): 13 settings (default model, model blocklist, both expiry windows, conversation-sticky TTL, irreversible growth actions, growth/probe intervals, both pacer bounds, CodeBuddy chat interval, activity-report toggle/hour) can be changed from the admin UI and take effect immediately — no restart. **DB overrides win over `.env`**; the page marks each row as "DB override" and offers "Reset to default" to fall back to `.env`. Startup-only knobs (`APP_SECRET`, `PORT`, `DATA_DIR`, allowlists) are deliberately excluded. The same page shows the **background tasks** (quota probe / token pre-refresh / daily check-in / growth center / activity report / detail retention): their interval, last run, latest result and error — each task's own settings are grouped into its card, everything else sits under "Gateway & scheduling". Task state is **in-process only** (`GET /api/tasks`, admin, auto-refreshed every 30s): it shows what ran *since this process started* and **resets on restart** — nothing is persisted and no history is kept. A scheduled wake-up that has nothing to do (check-in already done today, activity report window not open) is *not* counted as a run; otherwise the card would claim "checked in just now" on a day with no check-in at all. Intervals and toggles show the **currently effective** values, so a hot-reload is visible on the next refresh
3232
- **Token-expiry visibility**: each credential has a dedicated "token remaining" column, red-flagged below `TOKEN_EXPIRY_WARNING_SECONDS`. The expiry comes from the credential's explicit `expires_at` and **falls back to the access token's JWT `exp`** — CodeBuddy's token responses carry no expiry at all (measured), so without the fallback the value is always 0 and CodeBuddy tokens would never pre-refresh (only hard-disabled on a 401). Both sources missing → `—`, never guessed. The JWT `iat` (last renewal) is still persisted to `credentials.token_issued_at` for diagnostics but is not shown: it only means something alongside the remaining days, which is not worth a table row
3333
- **Credit-change log**: the per-credential "credit record" drawer lists the **net change between consecutive quota probes** (`credit_events`, same 90-day retention as usage detail). It deliberately does **not** attribute changes to check-in / growth / chat: upstream logs nothing for those calls, so a diff cannot tell who added the points. The UI says "net change", never "check-in +5". First probe only records a baseline (`sync`); unchanged balances are skipped; a balance going *unknown* still records a row with no delta, because that is an anomaly worth chasing rather than "no change"
3434

‎README.md‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ curl http://127.0.0.1:8000/v1/user/balance -H "Authorization: Bearer sk-你的ke
188188
## 后台任务
189189

190190
额度探测、token 预刷新、每日签到、成长中心、明细清理由 `TaskRunner` 自动调度(失败互不影响),周期见 TECHNICAL.md §6.1。
191+
每类任务最近一次执行的时间、结果与错误可在管理台**「任务与配置」页**查看(与可热更配置同页,30 秒自动刷新);
192+
该运行态只存在于**本次进程**内,重启归零。
191193

192194
成长中心仅 CodeBuddy 有:自动领取 Buddy 旅行礼物、派 Buddy 出发、领取新任务与任务奖、
193195
断登补登、连登奖励兑换、开盲盒、能量开 Buddy 盲盒。Buddy 旅行 1–4 小时回来一次,
@@ -250,9 +252,9 @@ CodeBuddy 成长中心的「连登天数 / 活跃地图」按日统计客户端
250252
| `AUTO_CONTINUE_MAX` | `10` | 上游以 `finish_reason=length` 截断时同凭证自动续写的最多次数;`0` 关闭(见 TECHNICAL.md §3.4) |
251253
| `HOST` / `PORT` | `127.0.0.1` / `8000` | 监听地址与端口(compose 默认 `0.0.0.0`,`PORT` 同时决定宿主机映射端口) |
252254

253-
### 管理台热更(运行时配置)
255+
### 管理台热更(「任务与配置」页)
254256

255-
上表中带「可热更」语义的 13 项可以不改 `.env`、不重启,直接在管理台「运行时配置」页修改:
257+
上表中带「可热更」语义的 13 项可以不改 `.env`、不重启,直接在管理台「任务与配置」页修改:
256258

257259
`DEFAULT_MODEL`、`MODEL_BLOCKLIST`、`QUOTA_EXPIRY_WINDOW_SECONDS`、`QUOTA_EXPIRY_SECONDARY_WINDOW_SECONDS`、`CONVERSATION_STICKY_SECONDS`、`GROWTH_IRREVERSIBLE_ACTIONS`、`GROWTH_INTERVAL_MINUTES`、`QUOTA_PROBE_MINUTES`、`CODEBUDDY_CHAT_MIN_INTERVAL`、`PACER_MIN_SECONDS`、`PACER_MAX_SECONDS`、`ACTIVITY_REPORT_ENABLED`、`ACTIVITY_REPORT_HOUR`。
258260

@@ -262,6 +264,12 @@ CodeBuddy 成长中心的「连登天数 / 活跃地图」按日统计客户端
262264
- 值存 `runtime_settings` 表(纯 key/value),新增可热更项不需要迁移;白名单外的 key、非法类型/越界值在写入前被拒,读取时坏行跳过并记警告。
263265
- 启动期项(`APP_SECRET` / `PORT` / `DATA_DIR` / `USERS_FILE` / 上游端点白名单)**不在**白名单,改它们仍需重启:它们决定进程如何启动,运行期变更只会让内存与磁盘静默分叉。
264266
- 接口:`GET /api/settings` 读快照,`PUT /api/settings` 写(admin + CSRF),body 形如 `{"values": {"QUOTA_PROBE_MINUTES 对应的 key": 15}}`,传 `null` 表示恢复默认。
267+
- 该页同时展示**后台任务运行态**:6 类任务(额度探测 / token 预刷新 / 每日签到 / 成长中心 /
268+
活跃上报 / 明细清理)的周期、上次执行时间、最近一轮结果与错误。配置项按所属任务分组
269+
进卡片,其余(默认模型、黑名单、到期窗口、节流等)在「网关与调度」区。
270+
- 运行态是**进程内**的(`GET /api/tasks`,admin,页面每 30 秒自动刷新):只显示「本次启动以来」
271+
跑过没有,**重启归零**,不落库也不保留历史。未到点/未开启的轮次不算一次执行——
272+
否则签到会显示成「刚刚跑过」,而当天其实一次都没签。
265273

266274
## 部署注意
267275

0 commit comments

Comments
 (0)