Skip to content

[PR] Unified Announcement Panel: Built-in Registry + Local JSON Files + Remote Dynamic Sources (Done in One Pass) | 统一公告面:内置注册表 + 本地 JSON 文件 + 远程动态源(一次做完) - #5575

Merged
ranxianglei merged 16 commits into
masterfrom
2026-11-03_unified-notices-surface
Nov 4, 2026

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

把「web 面板上到底能不能弹公告」这件事一次做完:内置公告统一注册表 + 本地 JSON 文件自动渲染 + 远程动态公告源,外加完整文档和 CI 校验。这是之前 #5554(P1)和 #5570(P2)的合集——那两个已关闭,全部改动以线性历史收进本 PR(master d244fff 之上仅 4 个提交)。

用户看到什么

  1. 内置一次性公告走同一条渲染循环:绿底新功能横幅(大上下文增量解析那条)、关键缺陷横幅不变;文案进 i18n 表、死 key 有测试守卫。
  2. 本地文件:往配置目录丢 announcements/*.json(默认 ~/.config/billion-context/announcements/)即渲染成横幅。记录支持中英双语、7 类 kind→颜色推导、npm 风格版本定向(>=a.b.c <x.y.z/^/~/[lo hi]/|| 列表)、percent 灰度分桶(按公告 id 加盐,与业务灰度互不相关)、时间窗、按类型优先级排序、三种关闭模式(persistent=按内容寻址、改内容再弹一次 / instance=仅本进程 / always=无关闭按钮)、CTA 按钮(仅 http(s))。坏文件只废掉它自己。
  3. 远程源:本仓库 master 的 announcements/remote.json(CI 校验的官方文档,当前空列表),约每 3 分钟拉取;上一份有效公告的快照跨重启/断网存活;坏推送被拒绝并继续显示旧值;防火墙环境用 "announcementsUrl" / BILI_ANNOUNCEMENTS_URL 指中继镜像。整条链路 fail-open:拉不到/坏了只影响公告面,绝不影响模型流量。
  4. 同名覆盖规则:本地文件 > 远程源 > 内置。合并时修了一个测试抓到的真 bug:此前 local 没压掉 remote 同 id 条目(只压了内置),现已补上并有钉测。

config surface(新增面,按 AGENTS 要求显式申报)

  • 配置文件顶层新键 announcementsUrl(string,默认缺省 = 官方 GitHub raw 源)+ 环境变量 BILI_ANNOUNCEMENTS_URL(env > file > 默认三级,走 loadOptions 既有机制)。
  • 选择理由:现有两个同族监视器 advisoryUrl / releaseNotesUrl 就是这个形状(URL 覆盖键 + BILI_ 前缀 env),完全复用既有模式,没有发明新配置形态;本地/远程公告本身不是配置面(内容 ≠ 配置),不需要额外键。

验证

Closes #5548 的实现部分(设计稿在 issue 内评论);替代并关闭 #5554、#5570。

ework-agent added 4 commits November 3, 2026 10:09
…er loop (#5548 P1)

Every built-in web notice now lives as ONE entry in src/notices.ts (id, tone,
i18n key set, server-side active predicate). The web client renders whatever
the overview reports through a single generic loop over
#notices-banner-region; the per-notice builders/banners/wiring added one by
one in #5079, #5469 and the retired #5547 are gone. Adding a 4th notice is a
table row + its i18n keys + its store allowlist id — no admin/client/page
edits.

- new src/notices.ts: NoticeEntry registry + never-throwing visibleNotices()
- admin.ts: overview gains notices[] (replaces the two bespoke payload
  fields); new GET /__bili/notices serves the same array
- client.ts: bili_pckNoticeBanner/bili_wmNoticeBanner collapse into one
  bili_noticeBanner({tone,on,desc,hint,dismiss}); one loop wires
  .notice-dismiss buttons against POST /__bili/notices/dismiss
- page.ts: two static banner divs -> single #notices-banner-region
- i18n: per-notice dismissed toasts merge into one notices.dismissed key
  (zh/en); web-i18n dead-key guard learns to scan src/notices.ts (registry
  strings count as references)
- tests: new tests/notice-registry.test.ts (registry invariants, both
  locales, fresh-install visibility order, dismiss-hides-only-one)

Behavior is deliberately unchanged (P1 of the approved #5548 design: P2 adds
local JSON announcements + full schema, P3 remote GitHub raw + CI gate).
… get a banner (#5548 P2)

Owners can now post notices by writing one JSON file per announcement into
~/.config/billion-context/announcements/*.json (the dir follows
BILI_CONFIG_FILE relocation). The web panel renders them through the same
single banner loop as built-ins (P1 registry); no code change needed ever
again for owner-side announcements.

Schema (whitelist copy, extra fields dropped not fatal; caps title<=120 /
desc<=600 / hint<=300, file<=64KB, bad file skips itself only):
- id [a-z0-9-]{1,64} + kind 7-class (new-feature/upgrade-advisory/warning/
  suggestion/incident/deprecation/other); tone auto-derived (ok/warn/info) or
  overridden; priority defaults per kind (incident 90 .. other 30)
- versions: semver-ish AND list (exact, >=a <b, ^, ~, [lo hi] || [x y]); this
  build must match every element; unparseable build version hides versioned
  records (never over-nag)
- percent 0-100 grey bucketing: sha256(id|install-id) last two bytes %100,
  salted per announcement so buckets do not correlate with feature rollouts
- since/until ISO windows; publishedAt metadata
- dismiss modes: persistent (store key id@sha256(canonical record)[:16] so
  edited content re-nags exactly once), instance (process memory only,
  restart re-shows), always (no close button rendered; store writes accepted
  but ignored — immune by construction)
- ctaLabel+ctaUrl pair (http(s) only, <=200 chars), rendered as
  target=_blank rel=noopener anchor

Merge/sort contract: local (B) shadows builtin (A) on equal id — tested that
a shadowed id renders exactly once as source=local — then priority desc with
stable order (builtins first among ties). mergedNotices() feeds both
/__bili/notices and the overview field; the dismissal endpoint routes by the
record's declared mode before touching persistent state.

Store (src/pck-notice.ts): hashed keys id@16hex now admissible alongside the
two builtin ids; write is bounded to 200 keys with builtin ids protected from
trimming (regression-tested at 210+ hash keys).

Client: bili_pickLoc picks zh-CN/en/any-value off the wire locale objects with
the t() fallback chain; banner builder gains optional CTA anchor and
hide-dismiss (all content through escapeHtml — zero new XSS surface). i18n:
one shared notices.dismiss_btn label added (zh/en parity checked by suite).

Evidence: typecheck clean, build clean, unused-exports gate clean; 51/51
targeted green (new tests/local-announcements.test.ts = 10 tests, every gate
asserted both directions per #3617; plus notice-registry,
wm-fastpath-intro-notice, issue5079-pck-default-off incl. live-proxy e2e,
web-i18n dead-key guard, web-update-banner, web-relay-warn).
…S merge, docs and CI lint (#5548)

Remote announcements: master announcements/remote.json polled on the shared
~3-minute watcher cadence (same throttle/dispatcher machinery as advisories),
last-known-good snapshot survives restarts and outages, malformed pushes are
rejected, installs behind firewalls override the URL via announcementsUrl /
BILI_ANNOUNCEMENTS_URL. Merge order fixed so equal ids shadow builtin < remote
< local (a test caught the missing local-over-remote leg). Docs: config
reference seeds regenerated (en+zh+site), README en/zh sections, AGENTS KDD#16,
plus a CI lint job guarding the official feed document.
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-11-03_unified-notices-surface (70dd6cf)

Latest build — install this exact version

npm install -g billion-context@0.0.8039-pr.5575.e1a309a

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
70dd6cf billion-context@0.0.8039-pr.5575.e1a309a build
94d711b billion-context@0.0.8033-pr.5575.92a3fd1 build
656de81 billion-context@0.0.8015-pr.5575.402e1ea build
9b396e9 billion-context@0.0.8011-pr.5575.44ef73f build
bbbf857 billion-context@0.0.8006-pr.5575.857508e build
b0972ab billion-context@0.0.8004-pr.5575.b6c9394 build
c0f09bf billion-context@0.0.7957-pr.5575.118b96a build
c07703f billion-context@0.0.7952-pr.5575.f1ff6aa build
15a4852 billion-context@0.0.7951-pr.5575.80d9051 build
2f91f63 billion-context@0.0.7944-pr.5575.dc4d886 build

Download artifact

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

Install instructions are refreshed on each push.

@ranxianglei ranxianglei changed the title 统一公告面:内置注册表 + 本地 JSON 文件 + 远程动态源(一次做完) [PR] Unified Announcement Panel: Built-in Registry + Local JSON Files + Remote Dynamic Sources (Done in One Pass) | 统一公告面:内置注册表 + 本地 JSON 文件 + 远程动态源(一次做完) Nov 3, 2026
ework-agent and others added 4 commits November 3, 2026 10:57
…ile/feed notices (#5548)

Live smoke caught it: the persistent path stored the bare id, which the
store allowlist rejects ('unknown notice'), so every local/remote dismissal
400'd while built-ins kept working. Handler now resolves the visible record
(local shadows remote) and stores id@contentHash16 — exactly the key the
visibility gate filters on; built-in ids fall through unchanged. Unit pin:
bare id rejected, hash key hides, edited content re-nags once.
…sUrl (#5548 review fix)

Owner review: the bare 'announcements' prefix would collide with the future
npm release-notes channel (releaseNotesUrl / notes prefix); scope it to the
remote feed explicitly. Env var BILI_ANNOUNCEMENTS_URL unchanged. Also
registers BILI_ANNOUNCEMENTS_URL in LIFECYCLE.md §2.7 — its absence failed
lifecycle-ledger-guard on this branch.
The content-addressed dismiss fix (77c0e05) unit-pins the resolver helper
and store directly, so a regression of the endpoint's own glue — passing the
resolved value into dismissUpgradeNotice — would slip past that pin. Drive the
real admin route (mock req/res, loopback-gated) across persistent LOCAL,
built-in, unknown, and persistent REMOTE-feed cases; a revert to bare-id
dismissal fails here instead of in production.

Reviewer supplement for PR #5575.
Rename to follow the repo issue-numbered test convention (an internal
mirror number slipped in by mistake); the test pins the #5548 unified-
notice dismissal route. No behavior change.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重:不重复。 本 PR 是 #5548(统一公告面)的唯一实现,已替代并关闭 #5554/#5570;检索 open/closed 无其它条目在做同一件事。评审期间该分支上有两个并发提交已先行落地(缺陷修复 77c0e05、配置改名 2f91f63),我已把它们纳入评审基线,未另开重复 issue/PR。

结论先行: 这是功能 PR(内置注册表 + 本地 JSON 文件 + 远程动态源,一次做完)。核心实现正确、fail-open 链路干净、diff 整洁、config-surface 按规范申报到位。唯一实质缺陷是「持久型本地/远程公告点『关闭』一直失败」——我独立定位到根因后确认你已在 77c0e05 修掉(修法与我一致且更紧:按可见性解析内容寻址值)。我补了一个真正打真实路由的端到端回归测试钉住它(已推 c07703f)。当前无阻塞项。

结构化八问:

  1. 是什么问题 —— 统一公告面新功能;评审中发现一个真实交互缺陷:持久型(默认模式)的本地文件公告与远程 feed 公告,点关闭会失败、横幅关不掉。
  2. 什么原因导致 —— 可见性门按内容寻址值(id@sha256前缀)从关闭存储里过滤记录,但关闭端点把网页发来的裸 id 直接写进同一存储;裸 id 既不在内置白名单也不匹配哈希形状 → 被拒(400 unknown notice)→ 门永远命中不到 → 反复弹。网页只发裸 id 是协议设计,缺的是端点侧的换算。
  3. 影响范围与严重程度 —— 仅影响往配置目录丢 announcements/*.json、或收到持久型远程 feed 条目且想关掉它的用户;内置一次性公告、instance/always 模式不受影响。因是未发布新功能,属发布前抓到的交互缺陷(体验瑕疵级,非数据丢失/不可用)。
  4. 怎么修复的 —— 端点写存储前先从「当前可见记录」解析出与门一致的内容寻址值再写入(本地优先于远程),内置与未知 id 走原裸 id 路径不变;我另加端到端测试覆盖 持久本地 / 内置 / 未知 / 持久远程 四形态。
  5. 是否有回归问题 —— 逐项核过既有行为均保持:内置 PCK / WM-intro 关闭仍走裸白名单、未知 id 仍 400、instance/always 提前返回逻辑、本地>远程>内置同名覆盖、版本/percent/时间窗门、远程 feed 拉取与 last-known-good 快照 fail-open、config-surface 覆盖账。未见回归。
  6. 是否破坏缓存 —— 不涉及模型/上下文缓存。公告侧唯一的「缓存」是远程 feed 的 last-known-good 快照,本次改动不触碰其读写路径。(旁注:关闭存储 200 条上限按字典序而非时间序裁最旧,是既有设计小瑕疵、非本次引入,仅极端多公告时可能误裁旧键。)
  7. 是否改了设计方向 —— 没有转向,就是按既定设计把三源收进一条渲染循环 + B>A>S 影子规则做完;配置键改名是把命名收窄到「远程 feed」(避免与未来 release-notes 通道撞 announcements 前缀),形状不变。
  8. 是否引入新配置 —— 是,显式申报:顶层新键 remoteAnnouncementsUrl(string,缺省 = 官方 raw 源)+ 环境变量 BILI_ANNOUNCEMENTS_URL(env>file>默认,走 loadOptions 既有三级)。本地/远程公告本身是内容不是配置,不加键。

行为变更披露(规范强制):

  • 新增配置面:remoteAnnouncementsUrl + BILI_ANNOUNCEMENTS_URL(见 Q8)。
  • 配置键改名 announcementsUrl → remoteAnnouncementsUrl:旧名从未随任何版本发布,无用户需迁移。
  • 关闭存储写入值:由本 PR 初版的裸 id 改为内容寻址 id@hash——这是修复本身,非对外行为回退。

两个非阻塞小观察(可不改,留你定夺): mergedNotices 注释里的 B/A/S 字母标注与代码中的 builtin/local/remote 对不齐(纯注释);合并列表里内置公告被固定 priority=50 而非按其 kind 取值(当前两条内置都低优先级,暂无实际影响,但将来若加高严重度内置会被压低排序)。

判定:可合并 —— 等推送头 c07703f 上正在跑的 test 分片转绿即可(当前 0 失败 / 12 待跑,其余 success/skipped)。我已本地定向验证 typecheck 干净 + 相关套件全绿。我推的两个 commit(15a4852 端到端测试、c07703f 按仓库惯例把该测试文件改名为 tests/issue5548-notice-dismiss-endpoint.test.ts)均为纯增量,不改你的源码逻辑。

ework-agent and others added 8 commits November 3, 2026 12:27
The validator imports parseRemoteDoc from src/remote-notices.ts, whose
module graph reaches acp-kernel through src modules; without a built
kernel/dist the file:./kernel link resolves to a missing index.js and
the job dies with ERR_MODULE_NOT_FOUND before any linting happens.
Same prerequisite as the main test matrix (ci.yml static/test legs).
…ref in test header, eviction comment accuracy

@ranxianglei ranxianglei left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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

(GitHub rejects the APPROVE event on the author's own PR, so this carries as a COMMENT review — the verdict below stands.)

Verdict: APPROVE — full end-to-end read of all 28 files (+1746/−107) incl. the five new test files; CI green on head (mirror cache: 30 checks, 0 failed); no kernel/ touch, no wire/model-traffic impact.

结构化八问

  1. 是什么问题:三个公告面各自生长(内置引导提示、升级提示、#5548 设计的远程 feed),各有各的渲染/关闭逻辑;此前拆分期还留下「本地文件只压内置、不压同 id 远程条目」的 merge bug,且关闭回执在统一存储激活时仍写 JSON 状态文件。本 PR 把它们收敛成一个循环:一套 schema、一个渲染器、一份回执存储。
  2. 什么原因导致的:每个面各长了一条 fetch/dismiss/merge 路径,而不是共享「什么该显示」的单一投影——shadowing bug 和回执双写都是这个分裂的直接后果。
  3. 影响范围与严重程度:仅 web UI banner 区 + loopback-only /__bili admin 路由;零模型流量、零 wire、零 kernel。属 UX 功能 + 存储卫生,无可用性影响。
  4. 怎么修复的:schema 只定义一次(src/announcements.ts)三源逐字共用;mergedNotices() 做 local > remote > builtin 影子 + 稳定优先级排序;回执每进程一次性从统一存储(kv namespace notices)水合,遗留文件键恰好迁移一次;远程拉取走 ~3 分钟 watcher 节奏、fail-open,last-known-good snapshot + throttle 文件兜底;渲染对每个动态值(含 CTA href 属性)走 house-standard escapeHtml,CTA 锚点 target=_blank rel=noopener noreferrer。
  5. 回归审查(diff 触碰的既有行为逐项清点):
    • overview payload:pckNotice{active} / wmNotice{active} 对象移除,改为 notices[] 数组——有意变更,#5548 线程已披露;唯一消费者是仓内 web client,同一 diff 内同步更新(grep 复核:这两个字段无其他读取者)。
    • POST /__bili/notices/dismiss:可接受 id 从内置 id 扩到 内置 ∪ 内容寻址 id@hash——严格超集,旧调用形状照旧工作(新 endpoint e2e 钉住)。
    • upgrade-notices.json:统一存储实例上降级为 pre-hydration 读 fallback + 一次性导入源(键恰好迁移一次,notice-unified-store.test.ts 钉住);legacy-only 实例仍是唯一载体,纯文件路径保留(内存缓存永不接管)。既有关闭数据两条路都保得住。
    • pck/wm 提示的可见性谓词不变(同样的 routing 门 + 同样的 dismiss 查询),只是背后的存储搬了家。
    • i18n:删 2 个死键、加 2 个共享键;dead-key 扫描扩到 src/notices.ts。
    • package.json version 未动;无 lockfile churn;diff 无跑题文件。
  6. 是否破坏缓存:不涉及——模型流量零接触;唯一持久化状态是关闭回执本身。
  7. 是否改了设计方向:没有——既有面板内的 UI 面收敛。配置面纪律合规:新键在 config-surface 段声明,CONFIGURATION.md en+zh、updates.yaml seed、LIFECYCLE.md 全镜像。
  8. 新配置:remoteAnnouncementsUrl(默认本仓 master announcements/remote.json)+ env BILI_ANNOUNCEMENTS_URL(env > file > default,同 advisoryUrl 模式);可选 <config dir>/announcements/*.json 无需任何配置。纯增量,无迁移。

测试有效性

真测试、断言有牙:#3617 双向门(matcher 矩阵含负例行;cap 测试垫到 210 条断言 ≤200 且内置受保护;failure ladder 模拟重启;endpoint 测试驱动真实路由处理器;unified-store 测试钉死 BILI_STORAGE_UNIFIED=full 并证明回执跨模拟重启存活)。hermetic 临时目录、listen(0) 端口(#360)、无环境运气依赖。

非阻塞观察(不要求动作)

  • legacy-only 安装(无统一 sidecar)在文件超限淘汰时按字母序而非 FIFO——除非该类实例累积 >198 个非内置哈希键否则不可达;真 FIFO 需要文件格式加时间戳,超出本 PR 范围。
  • 渲染循环不设设计稿里的 ≤5 条头部上限(溢出列表方案未落地)——实际暴露面很小:feed 由 owner 维护且过 CI lint,本地文件是用户自有的。
  • PR 正文仍写着改名前的配置键名 announcementsUrl(2f91f63 已改名 remoteAnnouncementsUrl)——纯措辞,reviewer 不动 PR 正文。

已顺手修(仅注释/文档,零行为变化)

  • README.md:539 残留旧键名 "announcementsUrl" → "remoteAnnouncementsUrl"
  • tests/issue5548-notice-dismiss-endpoint.test.ts:9 头注释引用了 PR #5577(那是另一个已关闭的 PR)→ 更正为 #5575
  • src/pck-notice.ts 淘汰注释声称 oldest-first,代码实为字母序淘汰(flat 文件没有时间戳)——注释改回与代码事实一致

commit 70dd6cf

结论:可合并。 CI 绿、发现项全部就地修掉或有意识留档、未发现回归。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

PR #5575(统一公告面)review 完成:结论 APPROVE——全量通读 28 个文件(+1746/−107)含 5 个新测试文件,CI 绿(head 94d711b,镜像缓存 30 checks 0 failed),无 kernel/wire/模型流量影响。GitHub 禁止作者账号 approve 自己的 PR,故以 COMMENT review 形式发布同一结论:#5575 (review) 。已顺手修 3 处注释/文档(README 残留旧配置键名、测试头注释误引 #5577、淘汰策略注释与代码事实不符),commit 70dd6cf 已推 PR 分支,docs-only,CI 重跑中。

@ranxianglei

Copy link
Copy Markdown
Owner Author

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

新 head 70dd6cff6 复审:与上轮已审 head(94d711bef)的 diff = 仅上一轮顺手修的 fix(notice) 注释/文档 commit(3 文件,+4/−3,零行为变化),无 kernel/。该 head CI 全绿(52 checks:38 success / 14 skipped / 0 failed——镜像缓存快照落后约 12 分钟,已单次核实)。结论不变:APPROVE。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

PR #5575 新 head 70dd6cf 复审完成:delta = 仅上轮自己的 docs-only fix commit(3 文件 +4/−3,零行为变化),无 kernel;该 head CI 全绿(52 checks:38 success / 14 skipped / 0 failed,镜像缓存落后已单次核实)。结论不变 APPROVE,PR 线程已留短评论。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

(说明:上一条是本 bot 自己发的复审评论被同步层镜像回来,非新消息,无额外动作。)当前状态:PR #5575 已审完、结论 APPROVE、CI 全绿(head 70dd6cf),等 owner 合并即可。

@ranxianglei
ranxianglei merged commit c29b118 into master Nov 4, 2026
52 checks passed
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

📦 Merged Build Published

This merge to master was published as 0.0.1018-master.c29b118 (rolling master tag).

npm install -g billion-context@0.0.1018-master.c29b118  # 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

Labels

None yet

Projects

None yet

1 participant