# Message Hub 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 落地统一消息中枢(模型 + 本地路由表 + Web 消息中心原型),并让工作台/外壳铃铛读同一中枢。 **Architecture:** `src/common/message-hub/` 持有 Message、ChannelDelivery、RouteRule 与 `resolve()`;新原型 `message-center` 提供筛选/列表/详情/端模拟器/去处理;工作台与 `notice-bridge` 通过适配层映射为铃铛摘要。真推送与真唤端不做。 **Tech Stack:** React + TypeScript(现有 Axhub Make 原型)、Vitest(`resolve` 单测)、localStorage(可选已读)、现有 `oneos-app-shell` 铃铛 UI。 **Spec:** [docs/superpowers/specs/2026-07-22-message-hub-design.md](../specs/2026-07-22-message-hub-design.md) --- ## 文件结构(锁定) | 路径 | 职责 | |------|------| | `src/common/message-hub/types.ts` | Message / Channel / RouteRule 类型 | | `src/common/message-hub/routes.ts` | 五端 × 多系统路由表常量 | | `src/common/message-hub/resolve.ts` | `resolveMessageTarget()` | | `src/common/message-hub/resolve.test.ts` | 路由优先级与失败态单测 | | `src/common/message-hub/seed.ts` | 宽首期种子消息(含旧 notices 映射) | | `src/common/message-hub/store.ts` | 读列表、标已读、未读数(内存 + localStorage) | | `src/common/message-hub/shell-adapter.ts` | Message → `ShellNoticeItem` | | `src/common/message-hub/index.ts` | 公共导出 | | `src/prototypes/message-center/index.tsx` | 原型入口 | | `src/prototypes/message-center/MessageCenterApp.tsx` | 主 UI | | `src/prototypes/message-center/styles/index.css` | 样式 | | `src/prototypes/message-center/annotation-source.json` | 标注目录 | | `src/prototypes/message-center/.spec/route-resolve.md` | 路由判定全文 | | `src/prototypes/message-center/.spec/requirements-prd.md` | AutoPRD | | `src/resources/prd/message-center-autoprd.md` | PRD 副本 | | 修改 `oneos-app-shell/ShellNoticeCenter.tsx` | 「查看全部」→ 消息中心;详情优先于行内直跳 | | 修改 `oneos-web-workbench-new` | 铃铛数据改读 message-hub | | 修改 `nav-menu.json` | 增加「消息中心」菜单项 | --- ### Task 1: message-hub 类型 + resolve + 单测 **Files:** - Create: `src/common/message-hub/types.ts` - Create: `src/common/message-hub/resolve.ts` - Create: `src/common/message-hub/resolve.test.ts` - Create: `src/common/message-hub/index.ts`(先导出类型与 resolve) - [ ] **Step 1: 写入类型** ```ts // src/common/message-hub/types.ts export type SourceSystem = 'oneos' | 'vehicle-mid' | 'other-system' | 'external'; export type MessageClient = 'web' | 'ios' | 'android' | 'harmony' | 'wechat_oa'; export type ChannelStatus = 'pending' | 'sent' | 'failed' | 'skipped'; export type MessagePriority = 'normal' | 'high'; export type ExternalAppId = 'external-app-a' | 'external-app-b'; export type ChannelDelivery = { client: MessageClient; status: ChannelStatus; updatedAt?: string; }; export type HubMessage = { id: string; sourceSystem: SourceSystem; bizType: string; bizId: string; title: string; summary: string; detail: string; priority: MessagePriority; createdAt: string; readAt?: string; /** 角色占位;空数组表示全员可见 */ audienceRoleIds: string[]; channels: ChannelDelivery[]; /** 展示用业务标签,如「交车任务」 */ bizTag: string; }; export type RouteRule = { sourceSystem: SourceSystem; bizType: string; client: MessageClient; targetTemplate: string; label: string; externalApp?: ExternalAppId; /** 精确未命中时,再试该 client(如 harmony → android) */ fallbackClient?: MessageClient; }; export type ResolveOk = { ok: true; uri: string; label: string; externalApp?: ExternalAppId; rule: RouteRule; }; export type ResolveFail = { ok: false; reason: 'no_rule'; message: '当前端暂无可跳转目标'; }; export type ResolveResult = ResolveOk | ResolveFail; ``` - [ ] **Step 2: 写失败单测(先不实现 resolve 逻辑体)** ```ts // src/common/message-hub/resolve.test.ts import { describe, expect, it } from 'vitest'; import { resolveMessageTarget } from './resolve'; import type { HubMessage, RouteRule } from './types'; const baseMsg: HubMessage = { id: 'm1', sourceSystem: 'oneos', bizType: 'approval.arrive', bizId: 'AP-1', title: '待审', summary: '', detail: '详情', priority: 'normal', createdAt: '2026-07-22T10:00:00+08:00', audienceRoleIds: [], channels: [], bizTag: '租赁合同', }; describe('resolveMessageTarget', () => { it('精确匹配 web 规则', () => { const rules: RouteRule[] = [ { sourceSystem: 'oneos', bizType: 'approval.arrive', client: 'web', targetTemplate: '/prototypes/oneos-web-approval-todo?id={bizId}', label: '审批待办', }, ]; const r = resolveMessageTarget(baseMsg, 'web', rules); expect(r.ok).toBe(true); if (r.ok) expect(r.uri).toBe('/prototypes/oneos-web-approval-todo?id=AP-1'); }); it('harmony 可 fallback 到 android', () => { const rules: RouteRule[] = [ { sourceSystem: 'oneos', bizType: 'approval.arrive', client: 'android', targetTemplate: 'oneos://approval/{bizId}', label: '安卓审批', fallbackClient: undefined, }, { sourceSystem: 'oneos', bizType: 'approval.arrive', client: 'harmony', targetTemplate: '', label: '鸿蒙走安卓模板', fallbackClient: 'android', }, ]; // 实现约定:harmony 行若 targetTemplate 为空且带 fallbackClient,则用 fallback 的模板 const r = resolveMessageTarget(baseMsg, 'harmony', rules); expect(r.ok).toBe(true); if (r.ok) expect(r.uri).toBe('oneos://approval/AP-1'); }); it('无规则返回固定文案', () => { const r = resolveMessageTarget(baseMsg, 'wechat_oa', []); expect(r).toEqual({ ok: false, reason: 'no_rule', message: '当前端暂无可跳转目标', }); }); it('带 externalApp', () => { const msg = { ...baseMsg, sourceSystem: 'external' as const, bizType: 'order.action', bizId: 'O-9' }; const rules: RouteRule[] = [ { sourceSystem: 'external', bizType: 'order.action', client: 'web', targetTemplate: 'ext-app-a://order/{bizId}', label: '外部 App A 订单', externalApp: 'external-app-a', }, ]; const r = resolveMessageTarget(msg, 'web', rules); expect(r.ok).toBe(true); if (r.ok) { expect(r.uri).toBe('ext-app-a://order/O-9'); expect(r.externalApp).toBe('external-app-a'); } }); }); ``` - [ ] **Step 3: 跑测确认失败** Run: `npx vitest --run src/common/message-hub/resolve.test.ts` Expected: FAIL(模块不存在或函数未导出) - [ ] **Step 4: 实现 resolve** ```ts // src/common/message-hub/resolve.ts import type { HubMessage, MessageClient, ResolveResult, RouteRule } from './types'; function fillTemplate(template: string, msg: HubMessage): string { return template .replaceAll('{bizId}', encodeURIComponent(msg.bizId)) .replaceAll('{id}', encodeURIComponent(msg.id)); } function findRule( rules: RouteRule[], sourceSystem: HubMessage['sourceSystem'], bizType: string, client: MessageClient, ): RouteRule | undefined { return rules.find( (r) => r.sourceSystem === sourceSystem && r.bizType === bizType && r.client === client, ); } export function resolveMessageTarget( msg: HubMessage, client: MessageClient, rules: RouteRule[], ): ResolveResult { const primary = findRule(rules, msg.sourceSystem, msg.bizType, client); if (primary) { const useFallback = (!primary.targetTemplate || primary.targetTemplate.trim() === '') && primary.fallbackClient; if (useFallback && primary.fallbackClient) { const fb = findRule(rules, msg.sourceSystem, msg.bizType, primary.fallbackClient); if (fb?.targetTemplate) { return { ok: true, uri: fillTemplate(fb.targetTemplate, msg), label: primary.label || fb.label, externalApp: fb.externalApp ?? primary.externalApp, rule: fb, }; } } if (primary.targetTemplate?.trim()) { return { ok: true, uri: fillTemplate(primary.targetTemplate, msg), label: primary.label, externalApp: primary.externalApp, rule: primary, }; } } return { ok: false, reason: 'no_rule', message: '当前端暂无可跳转目标' }; } ``` - [ ] **Step 5: 跑测通过** Run: `npx vitest --run src/common/message-hub/resolve.test.ts` Expected: PASS(4 tests) - [ ] **Step 6: Commit**(仅当用户要求提交时执行) ```bash git add src/common/message-hub/ git commit -m "$(cat <<'EOF' feat(message-hub): add types and resolve() with vitest coverage EOF )" ``` --- ### Task 2: 路由表 + 种子消息 **Files:** - Create: `src/common/message-hub/routes.ts` - Create: `src/common/message-hub/seed.ts` - Modify: `src/common/message-hub/index.ts` - [ ] **Step 1: 写入 `ROUTE_RULES`** 至少覆盖(每个 listed bizType 需含 web/ios/android/harmony/wechat_oa 五行;wechat 无办理页可用空 template + 不配 fallback,使 resolve 失败并在 UI 显示 skipped 通道态): | sourceSystem | bizType | web 目标示例 | |--------------|---------|--------------| | oneos | approval.arrive | `/prototypes/oneos-web-approval-todo?id={bizId}` | | oneos | urge.remind | `/prototypes/oneos-web-ops`(可带 query) | | oneos | contract.expire | `/prototypes/customer-management` | | oneos | license.expire | `/prototypes/customer-management` | | oneos | bill.ready | `/prototypes/lease-business-ledger` | | oneos | h2.balance | `/prototypes/payment-records` | | oneos | system.release | (无跳转或打开版本说明占位 path) | | vehicle-mid | fault.overdue | `/prototypes/vehicle-fault-handling#page=detail&id={bizId}` | | vehicle-mid | vehicle.status | `/prototypes/vehicle-management` | | other-system | generic.notice | `/prototypes/message-center`(仅详情,无外跳也可失败) | | external | order.action | `ext-app-a://order/{bizId}` + `externalApp: external-app-a` | | external | settle.action | `ext-app-b://settle/{bizId}` + `externalApp: external-app-b` | harmony 行:`targetTemplate: ''`, `fallbackClient: 'android'`。 ios/android:`oneos://…` / `oneos-android://…` 占位 scheme。 wechat_oa:对可 H5 办理的给 `https://oa.example.com/...` 占位;纯 PC 办理给空 template。 - [ ] **Step 2: 写入 `SEED_MESSAGES`** 映射旧 `SEED_NOTICES`(保留原 id 如 `n-1`、`RELEASE_NOTICE_ID` 便于对照)+ 新增 vehicle-mid / other-system / external 各至少规格要求条数。 `audienceRoleIds` 对齐原 `roleIds`;无角色限制用 `[]`。 `channels`:web=sent,移动三端=sent 或 pending,wechat_oa 按样例 sent/skipped。 - [ ] **Step 3: 导出** ```ts // index.ts export * from './types'; export * from './resolve'; export { ROUTE_RULES } from './routes'; export { SEED_MESSAGES } from './seed'; ``` - [ ] **Step 4: 用真实 ROUTE_RULES 补一条集成断言(可选放 resolve.test)** ```ts import { ROUTE_RULES } from './routes'; import { SEED_MESSAGES } from './seed'; it('种子 approval 在 web 可解析', () => { const msg = SEED_MESSAGES.find((m) => m.bizType === 'approval.arrive'); expect(msg).toBeTruthy(); const r = resolveMessageTarget(msg!, 'web', ROUTE_RULES); expect(r.ok).toBe(true); }); ``` Run: `npx vitest --run src/common/message-hub/resolve.test.ts` → PASS - [ ] **Step 5: Commit**(用户要求时) ```bash git add src/common/message-hub/ git commit -m "$(cat <<'EOF' feat(message-hub): add route table and wide-scope seed messages EOF )" ``` --- ### Task 3: store + shell 适配 **Files:** - Create: `src/common/message-hub/store.ts` - Create: `src/common/message-hub/shell-adapter.ts` - Modify: `src/common/message-hub/index.ts` - [ ] **Step 1: store** ```ts // 要点 const STORAGE_KEY = 'oneos.message-hub.readAt.v1'; // loadSeed(): HubMessage[] — 合并 SEED 与 localStorage 中的 readAt // markRead(id): void // unreadCount(messages, roleId?): number // listForRole(messages, roleId?): HubMessage[] // sort: 未读优先 → priority high → createdAt desc ``` - [ ] **Step 2: shell-adapter** ```ts import type { ShellNoticeItem } from '../oneos-app-shell/notice-bridge'; import type { HubMessage } from './types'; import { resolveMessageTarget } from './resolve'; import { ROUTE_RULES } from './routes'; export function toShellNoticeItem(msg: HubMessage): ShellNoticeItem { const web = resolveMessageTarget(msg, 'web', ROUTE_RULES); return { id: msg.id, type: msg.priority === 'high' && msg.bizType.startsWith('urge') ? '催办提醒' : msg.bizTag, bizTag: msg.bizTag, title: msg.title, summary: msg.summary, detail: msg.detail, time: msg.createdAt.replace('T', ' ').slice(0, 16), read: !!msg.readAt, href: web.ok && web.uri.startsWith('/') ? web.uri : undefined, taskId: msg.bizId, }; } ``` 注意:铃铛点击按规格**只打开详情、不在行上直跳**;`href` 仍可供详情内「去处理」使用。`type === '催办提醒'` 用于外壳排序/标签,adapter 对 `urge.remind` 必须输出 `'催办提醒'`。 - [ ] **Step 3: 导出 store / adapter** - [ ] **Step 4: Commit**(用户要求时) --- ### Task 4: message-center 原型 UI **Files:** - Create: `src/prototypes/message-center/index.tsx` - Create: `src/prototypes/message-center/MessageCenterApp.tsx` - Create: `src/prototypes/message-center/styles/index.css` - 实现前 Read: `~/.agents/skills/ui-ux-pro-max/SKILL.md`,并跑 `python3 ~/.agents/skills/ui-ux-pro-max/scripts/search.py "admin dashboard notification inbox" --design-system -p "OneOS message-center"` 视觉对齐现有工作台,不用营销风。 - [ ] **Step 1: 入口** ```tsx /** * @name 消息中心 */ import React from 'react'; import { PrototypeAnnotationHost } from '../../common/prototype-annotation-host'; import annotationSource from './annotation-source.json'; import MessageCenterApp from './MessageCenterApp'; import './styles/index.css'; export default function MessageCenterPage() { return ( <> ); } ``` (若项目 annotation 挂载方式与 fault-handling 不同,以同目录近期原型为准。) - [ ] **Step 2: MessageCenterApp 行为清单(必须全部有)** 1. 左栏:全部/未读/已读;系统源多选或下拉;业务类型下拉;**端模拟器**五端 2. 右栏列表:未读点、bizTag、title、时间 3. 选中详情:detail、channels 触达态表、`resolve` 预览(随端模拟器变)、「去处理」 4. 去处理:`markRead` → resolve → - `uri` 以 `/` 开头:`window.location.href` 或外壳 `postMessage({ type: 'ONEOS_SHELL_NAV', href })`(与工作台 `protoNav` 一致) - 否则:模态「将打开 {externalApp} / {uri}」(不 `window.open` scheme) - fail:页面内固定文案「当前端暂无可跳转目标」 5. 触控/对比度遵循 ui-ux-pro-max;表格数字可用 tabular-nums - [ ] **Step 3: 本地预览** 打开 `/prototypes/message-center`,手测:切换端模拟器预览变化;external 消息出示意模态;无 wechat 规则时提示不可跳。 - [ ] **Step 4: Commit**(用户要求时) --- ### Task 5: 业务规格 + AutoPRD + 标注 **Files:** - Create: `src/prototypes/message-center/.spec/route-resolve.md`(判定优先级表、前置条件、数据源、用户可见结果、种子 vs 未接 API) - Create: `src/prototypes/message-center/.spec/requirements-prd.md`(用户故事:起点→运作→闭环;摘要链到 route-resolve) - Create: `src/resources/prd/message-center-autoprd.md`(与 PRD 同步) - Create/Update: `annotation-source.json` 顶层 `directory.nodes` 含「产品需求说明(PRD)」全文 - [ ] **Step 1: 按 oneos-autoprd 写 PRD**(产品语言,不写文件路径堆砌) - [ ] **Step 2: route-resolve.md 必须含** | 优先级 | 条件 | 结果 | |--------|------|------| | 1 | 精确 source+bizType+client 且 template 非空 | 填充 URI | | 2 | template 空且 fallbackClient 有对应规则 | 用 fallback URI | | 3 | 否则 | 不可跳转文案 | - [ ] **Step 3: 若存在脚本则运行** `node scripts/sync-annotation-directory.mjs`(或项目内同等命令) - [ ] **Step 4: Commit**(用户要求时) --- ### Task 6: 导航登记 **Files:** - Modify: `src/prototypes/oneos-prototype-nav/nav-menu.json`(在「工作台」旁或系统能力区增加「消息中心」→ `prototypes/message-center`) - Run: `npm run nav:sync -- --prototype message-center --note "新增统一消息中心原型"` - [ ] **Step 1: 加菜单项** - [ ] **Step 2: nav:sync** - [ ] **Step 3: 刷新原型导航页确认可见** - [ ] **Step 4: Commit**(用户要求时) --- ### Task 7: 工作台铃铛 + 外壳对接中枢 **Files:** - Modify: `src/prototypes/oneos-web-workbench-new/components/WorkbenchPage.tsx`(及 NoticePanel 数据源) - Modify: `src/prototypes/oneos-web-workbench-new/data/notices.ts` — 改为从 hub re-export 适配函数,或标记 deprecated 并改调用方 - Modify: `src/common/oneos-app-shell/ShellNoticeCenter.tsx` - 列表项点击:`onOpen` 打开详情(勿直接 `onHandle`) - 「查看全部」主按钮导航到 `/prototypes/message-center` - Modify: `.spec/notice-center.md` 注明数据源迁移至 message-hub - Update workbench AutoPRD 中通知章节(行为变更需 sync) - [ ] **Step 1: Workbench 使用 `loadMessages` + `toShellNoticeItem` / 自有列表组件读 hub** - [ ] **Step 2: postNoticesSync 仍发 `ShellNoticeItem[]`(adapter 输出)** - [ ] **Step 3: 外壳「查看全部」→ message-center** - [ ] **Step 4: 手测:原型演示嵌入工作台时铃铛未读与中枢一致;查看全部进入消息中心** - [ ] **Step 5: Commit**(用户要求时) --- ### Task 8: 验收对照 + 收尾 - [ ] **Step 1: 对照规格 §10 验收清单逐条勾选** - [ ] **Step 2: `npx vitest --run src/common/message-hub/resolve.test.ts`** - [ ] **Step 3: 更新设计文档状态旁注明「已实现原型」(若本轮做完)** - [ ] **Step 4: 停止 brainstorm companion(可选)** `/Users/sylvawong/.agents/skills/brainstorming/scripts/stop-server.sh /Users/sylvawong/oneos1.2/.superpowers/brainstorm/82675-1784712939` --- ## Spec 覆盖自检 | 规格要求 | 任务 | |----------|------| | Message / Channel / RouteRule 模型 | Task 1–2 | | 本地路由 + 优先级 + 固定失败文案 | Task 1, 5 | | 五端 × 多系统种子与外部 A/B | Task 2 | | 消息中心 UI + 端模拟器 + 去处理 | Task 4 | | 铃铛快捷 + 查看全部 | Task 7 | | 独立中枢架构 | Task 1–4 | | .spec + AutoPRD + 标注 | Task 5 | | 导航登记 | Task 6 | | 不做真推送/真唤端 | Task 4 模态示意 | 无 TBD 占位;`resolveMessageTarget` / `HubMessage` / `ROUTE_RULES` 命名全计划一致。