Skip to content

[PR] feat(#4906): Panel blog auto-pulls new posts from GitHub master | feat(#4906): 面板博客自动从 GitHub master 拉新文章 - #4917

Merged
ranxianglei merged 8 commits into
masterfrom
2026-10-27_web-blog-github-refresh
Oct 27, 2026
Merged

ranxianglei merged 8 commits into
masterfrom
2026-10-27_web-blog-github-refresh

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Closes #4906

改了什么

面板博客(/__bili/blog)以前只显示随 npm 包冻结的那批文章,master 上新写的博客要等发版 + self-update 才看得到。现在会自动从 GitHub master 拉最新的博客文章、长文和图片:

  • 后台悄悄拉,全部下完才整体替换展示内容(不会渲染半截页)。
  • 无论有没有人访问,每个活着的进程最多 1 小时自动拉一轮(首次触碰后启动每小时自驱链,句柄 unref,不占事件循环);有人打开页面/状态访问时还会顺带检查一次。
  • 拉不到(断网/GitHub 出错)就继续显示之前的好内容或包内文章,页面角落标 offline 状态,不影响面板其他功能。
  • 纯公开 API、不存任何凭据、没有新配置项;1 小时节流 + 单飞去重,不会变成爬虫。
  • dsh 内嵌的 bili 设置面板里新增「博客」tab 按钮,位置紧跟「版本」之后;点击直接进入同一篇博客页(iframe 复用既有 #/blog hash 路由,零新增数据通道;旧构建的内嵌页面不认识该路由时自行回落默认页,不会白屏)。

触发点

  1. 每小时自驱链:进程第一次碰博客机制(任何一条路)后开始计时,之后每 1h 强制拉一轮——零流量时内容也不会老于 ~1h。
  2. 打开 /__bili/blog 顺带检查一次(页面本身立刻返回旧内容,不等网络)。
  3. CLI 状态路径冷启动强拉一次;本进程已有 master 快照后不再重复。

实现要点

  • 新文件 src/web/blog-refresh.ts:拉取引擎。单飞去重 + TTL 节流 + 单文件 5s 超时 + 总量/文件数/图片数预算,注册前全量校验,失败只落 "offline" 标记,任何异常都不会抛出 HTTP 层。website/articles.json 的原文随轮下载并计入预算,远端长文(en+zh 对)正常进 feed;清单缺失/畸形自动降级为 posts-only。
  • src/web/blogfeed.ts:展示层改成「远端 master 快照优先、包内内容兜底」,按文章 id 合并去重;图片路由先查内存缓存再落盘。两个纯函数(相对路径解析带越界防护、markdown 图片引用收集)保证下载集合和渲染丢图判定一致。
  • src/server/admin.ts / src/plugin.ts:上面各触发点的一行级接入;offline 标记在面板博客页可见。
  • src/agent/dsh-native-client.ts(owner 跟进):PAGE_IDS 在 updates 后追加 blog + zh/en 各一条 i18n(博客/Blog);tab 栏只改 iframe hash,点击不触发任何网络行为。

测试与验证

  • 新增 tests/web-blog-refresh.test.ts(mock 时钟覆盖每小时自驱链的 ~1h/~2h 强制轮、禁用态不起链、articles 清单 happy/degrade、超尺寸预算 fail-soft 等)。本地博客相关三个测试文件共 29/29 全过。
  • 本地已过:tsc ✓ · unused-exports gate ✓ · test-import-graph ✓ · npm run build(含 dist-external/dist-annotations)✓
  • 分支先后两次合入 master(b8f7d05e、e4cc6857/[PR] fix(hermes): tool_search still binds session identity and model budget when deferring plugin tools (/acp panel + 500K wi | fix(hermes): tool_search 延迟插件工具时仍绑定会话身份与模型预算(/acp 面板 + 500K 窗口) #4856),与并行评审修复(offline 标记展示、articles 清单保留)无冲突地叠在一起,每次合并后全部门禁重跑通过。
  • dsh tab:tests/dsh-settings-ui.test.ts 断言同步更新(6 个 tab、中文标签「博客」、按钮顺序)。tab 相关断言本地全过;该文件另有 8 个宿主侧 inject 测试在本机失败——用 stash 验证过不带我的改动也一样失败(本分支基线就如此),且上一版 tip 的 CI 全绿,属本机并发环境因素,以本次推送后的 CI 判定为准。
  • 本机整仓测试套件这次没跑干净:同一台机器上另一个会话正在活跃使用机器全局状态目录 ~/.local/state/billion-context/(bili.log/prefix-affinity 等实时增长),并发压力下大量与本改动无关的测试出现 15s 就绪超时——把本次改动从状态路径临时摘掉后同样复现,确认是并发争用而非代码问题。完整矩阵交给 CI 判定;CI 若出现相关失败我会跟进修掉。

Devlog

devlog/2026-10-27_web-blog-github-refresh/:REQ.md(问题与验收,含 owner 追加的"零流量也 ≤1h"要求)、DESIGN.md(模块划分、数据流、关键取舍)。

@ranxianglei ranxianglei changed the title feat(#4906): 面板博客自动从 GitHub master 拉新文章 [PR] feat(#4906): Panel blog auto-pulls new posts from GitHub master | feat(#4906): 面板博客自动从 GitHub master 拉新文章 Oct 27, 2026
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-27_web-blog-github-refresh (b1f2bd2)

Latest build — install this exact version

npm install -g billion-context@0.0.6411-pr.4917.2cb0381

This exact version is immutable and reproducible. The rolling pr tag tracks the newest test build across all PRs.

Recent builds (commit → version)

commit version build
b1f2bd2 billion-context@0.0.6411-pr.4917.2cb0381 build
dab98b6 billion-context@0.0.6392-pr.4917.d8f67e9 build
fd00218 billion-context@0.0.6389-pr.4917.46dde54 build
c78701b billion-context@0.0.6387-pr.4917.eb18138 build
b8f7d05 billion-context@0.0.6380-pr.4917.521822c build
a7b0962 billion-context@0.0.6378-pr.4917.8c8b1ac build

Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr4917.tgz
npm install -g package

Install instructions are refreshed on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

已把 origin/master 合入本分支并推送:干净自动合并、无冲突(mergeable=true)。推送后 head=b8f7d05e8,base=e4cc68575;当前 mergeable_state=blocked 是等待评审/CI,非冲突。behind 已清零。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

查重: 与 PR #4915 重复(同机制)——两个 PR 都是 #4906「面板博客从 GitHub master 自动拉新文章」的实现,且都改动 src/web/blogfeed.ts + src/server/admin.ts,合入任何一个后另一个必然冲突。按先来后到 #4915 在先;但两者方案细节不同(#4915 = live overlay 单篇回退 + 图片改写 raw.githubusercontent;#4917 = master 快照整体替换 + 面板 asset 路由走内存缓存),我先把 #4917 审完给出质量对比,由 owner 定保留哪个、关哪个——agent 无关闭权,两个都不动。

标签: pr + p2 + scope/ui(功能增强非 bug;面板界面域;p2 对齐母 issue #4906 的定级)。

开始审核:先读 diff 全文 + 定向跑新增测试 + 查 CI 状态,结论按八问体例回复。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

查重结论:与 PR #4915 重复(同一需求 #4906、同一机制——面板博客从 GitHub master 拉新内容)。按先来后到 #4915 先到,但两个 PR 都改了 src/web/blogfeed.ts + src/server/admin.ts,互斥,只能合一个,由 owner 拍板。本 PR 审核已完成,审核中修复了 1 个功能缺陷 + 3 个小问题,5 个标注 commit 已推到 PR 分支(HEAD fd0021867)。对比与建议见文末。

结构化八问

1. 是什么问题:不是缺陷,是 #4906 的功能请求——面板博客 /__bili/blog 只显示随 npm 包冻结的那批文章,master 上新写的要等发版 + self-update。本 PR 让面板自动从 GitHub master 拉最新的博客文章和图片。

2. 什么原因导致的(机制):访问驱动拉取,无常驻定时器——打开 /__bili/blog 顺带触发一次(fire-and-forget,页面立刻返回旧内容)+ CLI 状态路径冷启动时拉一次;单飞去重 + 1 小时 TTL(匿名 API ~60 次/小时/IP)+ 单文件 5s 超时 + 字节/文件数/图片数预算;拉取失败只落 "offline" 标记,继续显示上次好内容或包内文章,任何异常不出 HTTP 层。

3. 影响范围与严重程度:新增行为,无存量可用性影响。波及面:所有打开面板博客页的用户;失败时降级为包内内容 + 页面 offline 提示,不影响面板其他功能。风险面:bili 进程新增对 api.github.com / raw.githubusercontent.com 的匿名出站(≤1 轮/小时/进程 + 冷启动一轮),与 changelog 模块既有节奏一致;内存持有 ≤8MB 图片快照直到下一成功轮替换。

4. 怎么修复的(审核结论 + 审核中所做):方向成立(changelog 模块的第二实例:公开 API、无凭据、无配置项、fail-soft)。审核发现并已修:

  • 功能缺陷(有运行时复现):articles manifest 被内联解析后丢弃、未存入 files map,而渲染侧以 files.get("website/articles.json") 为门 → 远端长文文章下载了却永远不渲染,且原 12 个测试无一断言长文渲染(共享路由默认分支恰好 404 掩盖)。修法:manifest 文本存入 files map(带预算记账),降级语义不变;补 3 个回归测试(长文 en+zh 渲染 happy-path / manifest 损坏降为 posts-only / 超预算文档 fail-soft 保住上次好快照)。
  • offline 标记面板不可见:PR 描述称「页面角落标 offline」,但 client.ts loadBlog 从不读 data.error → 实现提示卡 + i18n(对齐 changelog 的 changelog_offline 先例)。
  • plugin.ts 触发点注释块重复一份 → 删;bl.sub 与 README(en/zh)「随安装包内置」措辞已过时 → 更新。
    本地验证:typecheck ✓ · build ✓ · 定向测试 web-blog-refresh 15/15 + web-blogfeed 8/8 ✓。整仓矩阵交 CI 判定(审核时段 GitHub API 限流,未能确认 check-runs 状态)。

5. 是否有回归问题:逐项审过触碰路径的既有行为,无意回归为零:

既有行为 结论
getBlogFeed 消费方(仅 handleBlogGet + 测试):合并语义 = 远端按 id 优先、包内独有 id 存活、排序比较器不变 保持 + 有意变更已披露(下节);另由返回缓存数组引用改为每次新数组,消除别名隐患
handleBlogAssetGet:safeSegments 在远端 Map 查找前已拒绝 ../NUL/空段/坏百分号编码,磁盘路径与 root 前缀门不变 保持
collectPosts→postsFromPairs 重构:配对/slug/日期/双语标题逻辑逐行等价,web-blogfeed.test.ts 钉住 保持
sortItems 抽取:比较器相同 保持
admin.ts/plugin.ts 接线:fire-and-forget + 内部守卫永不抛,状态路径热身后完全不走网络 纯增量
client.ts loadBlog:offlineNote 仅在 data.error 存在时渲染(此前不可能出现) 在线时零视觉差异
markdown.ts / changelog.ts / kernel / 压缩管道 未触碰

6. 是否破坏缓存:不涉及。博客 feed 是进程内内存快照,无跨进程缓存/重试/增量游标/去重键;模型侧 prefix-cache 机制零接触。

7. 是否改了设计方向:无架构转向。确立的是「面板静态内容可被 GitHub master 远端覆盖」模式的第二实例(先例 changelog #3719)。与 #4915 有一处有意语义差异需 owner 知悉:#4917 是 floor-merge(master 上删除的文章仍留在面板,包内版本永久兜底);#4915 是权威合并(master 删除即从面板消失)。两者都说得通,floor-merge 更保守(已发布内容不因 master 瞬时状态从用户面板消失)。

8. 是否引入了新的配置:无。无新 env/config/CLI(符合 #4906 验收口径)。

行为变更披露(旧 → 新,均有意)

  1. GET /__bili/blog 响应可携带 error:"offline"(此前该字段声明存在但永空)——additive。
  2. 面板博客页新增 offline 提示卡(此前不存在)——仅在拉取失败时出现。
  3. 博客内容源:恒为包内冻结副本 → 在线时远端 master 快照按 id 优先(文章+长文,图片经 /__bili/blog-assets 内存优先),失败回退包内。
  4. bili 新增 GitHub 匿名出站(≤1 轮/小时/进程 + 冷启动一轮,UA billion-context-blog-refresh)。
  5. 文案:面板 bl.sub 副标题与 README FAQ(en/zh)更新。

#4917 vs #4915 对比(owner 拍板用)

维度 #4917 blog-refresh #4915 blog-live
刷新范围 posts + 长文(manifest,审核中修好) 仅 posts(显式声明 articles 留包内)
合并语义 floor-merge:master 删除不消失,包内兜底 权威合并:master 删除即消失;单文件失败保包内 twin
图片策略 代理下载到内存(≤8MB/轮),asset 路由内存优先再落盘——浏览器无法直连 GitHub 也能看图 改写为 raw.githubusercontent.com 绝对 URL,浏览器直连 CDN;代理零带宽零内存
首屏延迟 fire-and-forget,页面立刻返回旧内容(偏离 changelog 先例,体验更好) 冷进程首次请求 await 网络轮(最多 ~15s 预算)——与 changelog 先例逐字一致
防护预算 每文档 512K / 每图 2M / 整轮 8M 字节 + 200 文件 + 50 图 整轮 15s wall-clock AbortController + 50 文件
offline 标记 原未落地,审核中已补(提示卡可见) 字段声明为 additive、面板暂不显示(注释明说是有意对齐 changelog 契约)
文档 README 原未更新,审核中已补 一开始就更新了 en/zh
本地实测 定向套件 15/15 + 8/8,含本次补的 3 例 其自身套件 10/10(临时 worktree 实跑)
PR 状态 mergeable_state: behind mergeable_state: blocked

建议:保留 #4917——修完后功能完整(覆盖长文)、首屏不阻塞、图片走代理对浏览器出不去 GitHub 的场景更稳、字节预算更细;#4915 可标 superseded。若 owner 更看重与 changelog 先例逐字一致 + 权威合并语义,选 #4915 也成立(floor-merge → 权威合并的移植量很小:丢弃不在远端清单里的包内 id 即可,我可以代做)。关哪个、留哪个请 owner 定——agent 不关单。

收工判定:代码层面本 PR 已就绪(缺陷已修、回归已清点、行为变更已披露),待 CI 全矩阵绿 + owner 在两个 PR 间拍板后可合。

ework-agent and others added 8 commits October 27, 2026 14:39
Access-driven overlay refresh for the /__bili/blog feed (#4587 serves
content frozen at release time): a background round downloads the live
master tree (website/blog/posts/*.md + articles.json entries + referenced
images) from the public GitHub APIs and registers it as the overlay the
feed/asset routes prefer over the bundled bytes; on failure the last good
overlay (or bundled floor) stays up with an "offline" marker.

- src/web/blog-refresh.ts (new): fail-soft round engine — anonymous public
  API only, no credentials/config/env surface; single-flight coalescing,
  1h TTL between rounds (anonymous rate limit ~60 req/h/IP), per-file
  timeouts, size/file-count budgets, atomic registration.
- src/web/blogfeed.ts: remote-overlay seams — pure resolveRepoRel /
  collectImageTargets helpers, buildRemoteBlogPosts, registerRemoteBlogFeed,
  getRemoteBlogAsset; getBlogFeed merges remote-wins per id over the
  bundled floor, asset route checks the in-memory map before disk.
- src/server/admin.ts: GET /__bili/blog fires a fire-and-forget kick after
  answering (response always instant).
- src/plugin.ts: /acp status path warms once while COLD (no master snapshot
  in-process yet) — same cadence class as warmChangelogOnce; warm processes
  keep the status path network-free.

tests: web-blog-refresh.test.ts (12 cases: merge priority, asset split,
image-404 churn, offline degradation, TTL/single-flight, warmer cold/warm
semantics); devlog REQ+DESIGN under devlog/2026-10-27_web-blog-github-refresh.
… articles render

runRound parsed website/articles.json inline to learn which docs to
download but discarded the text; buildRemoteBlogPosts gates its article
section on files.get(MANIFEST_REL), which was therefore always undefined —
master articles were fetched (bandwidth spent) yet never appeared in the
panel feed. Store the raw manifest into the files map under MANIFEST_REL
(accounted against the round budget) so the section renders; regression
tests pin both the happy path (en+zh pair via slug/title/title_zh) and the
malformed-manifest degrade (posts-only round, no throw).
The refresh failure marker (error:"offline" in GET /__bili/blog) was
invisible: loadBlog never read data.error, so the advertised corner
offline state did not exist. Render a small notice card above the list,
empty state and detail view when the feed carries an error (changelog
precedent: sys.changelog_offline), with bl.offline i18n keys. Also refresh
the bl.sub copy that still claimed content is only shipped inside the
install.
Owner review flagged the access-driven-only cadence: with no traffic a
served process would show stale content indefinitely. Every process now
runs a lazy hourly chain — the first touch of the module (the /__bili/blog
route, the status-path warm, or any round) arms a repeating force-kick at
ROUND_TTL_MS intervals:

- content served by any long-lived process ages out to ~1h even with zero
  further visits;
- the handle is unref'd so short-lived CLIs exit normally and tests never
  inherit a live timer (_resetBlogRefreshForTest clears it);
- force lets a failed round retry on the hour instead of slipping a full
  window (a failing round costs ~1 anonymous contents-API request);
- disabled modules never arm.

tests: 2 new cases under mocked clock (~1h/~2h forced rounds without
traffic) + disabled-no-arm; devlog REQ/DESIGN updated.
The embedded bili settings UI drives its iframe by hash route; PAGE_IDS
never listed "blog", so the master-pulled post feed had no entry point
in dsh's own panel even though the framed page (#/blog) already exists.
Owner follow-up: expose it as a sixth tab right after 版本/Versions.

- src/agent/dsh-native-client.ts: append "blog" to PAGE_IDS (after
  "updates") + PAGE_KEYS/i18n entries (博客 / Blog). Clicking only flips
  the iframe hash — no new data channel. Older embedded frames without
  the #/blog route fall back to their default page inside the frame.
- tests/dsh-settings-ui.test.ts: button counts 6→7, zh label list gains
  博客, action-button indices shift by one.
@ranxianglei
ranxianglei force-pushed the 2026-10-27_web-blog-github-refresh branch from dab98b6 to b1f2bd2 Compare October 27, 2026 06:39
@ranxianglei
ranxianglei merged commit d4c3355 into master Oct 27, 2026
51 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

📦 Merged Build Published

This merge to master was published as 0.0.875-master.d4c3355 (rolling master tag).

npm install -g billion-context@0.0.875-master.d4c3355  # this exact build
npm install -g billion-context@master  # always the newest merged build

npm view billion-context@master prints the current version at any time.

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

Projects

None yet

1 participant