Files
OneOS1.2/docs/superpowers/plans/2026-07-22-message-hub.md

19 KiB
Raw Blame History

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 原型、Vitestresolve 单测、localStorage可选已读、现有 oneos-app-shell 铃铛 UI。

Spec: docs/superpowers/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: 写入类型

// 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 逻辑体)
// 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
// 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: PASS4 tests

  • Step 6: Commit(仅当用户要求提交时执行)
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/androidoneos://… / oneos-android://… 占位 scheme。
wechat_oa对可 H5 办理的给 https://oa.example.com/... 占位;纯 PC 办理给空 template。

  • Step 2: 写入 SEED_MESSAGES

映射旧 SEED_NOTICES(保留原 id 如 n-1RELEASE_NOTICE_ID 便于对照)+ 新增 vehicle-mid / other-system / external 各至少规格要求条数。
audienceRoleIds 对齐原 roleIds;无角色限制用 []
channelsweb=sent移动三端=sent 或 pendingwechat_oa 按样例 sent/skipped。

  • Step 3: 导出
// index.ts
export * from './types';
export * from './resolve';
export { ROUTE_RULES } from './routes';
export { SEED_MESSAGES } from './seed';
  • Step 4: 用真实 ROUTE_RULES 补一条集成断言(可选放 resolve.test
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(用户要求时)
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

// 要点
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
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: 入口

/**
 * @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 (
    <>
      <MessageCenterApp />
      <PrototypeAnnotationHost source={annotationSource as any} />
    </>
  );
}

(若项目 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 12
本地路由 + 优先级 + 固定失败文案 Task 1, 5
五端 × 多系统种子与外部 A/B Task 2
消息中心 UI + 端模拟器 + 去处理 Task 4
铃铛快捷 + 查看全部 Task 7
独立中枢架构 Task 14
.spec + AutoPRD + 标注 Task 5
导航登记 Task 6
不做真推送/真唤端 Task 4 模态示意

无 TBD 占位;resolveMessageTarget / HubMessage / ROUTE_RULES 命名全计划一致。