forked from ChaiHao/BPOS-MVP
基线:鉴权会话+工作组+工作台三区修复+编排工作区+产品设计文档
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -0,0 +1,819 @@
|
||||
# 三面一体(设计中心 / 运营中心 / 工作台)实施计划
|
||||
|
||||
> **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:** 按 `docs/产品设计.md` 把治理中心拆成设计中心(`/design`)与运营中心(`/ops`)两个台面,实现到**编排层**为止 —— 壳、两张首页、触发器归位、台账动态位、工作组管理;活动内部(活动开发、契约深化)不在本计划内。
|
||||
|
||||
**Architecture:** 纯前端信息架构重排 + 三处小后端增量(触发器 upsert 路由、度量路由补齐八指标、工作组读写路由 —— 全部是既有模块用例的 HTTP 面,零模块语义改动)。壳层由一份 surface 配置驱动:`surfaces.ts` 定静态区表与权限亮灯,`gov-shell` 参数化成两壳,台账导航项按已发布数据域运行时生成。
|
||||
|
||||
**Tech Stack:** React 19 + Vite + TanStack Query(前端零过程状态,服务端投影);Hono + zod 契约(`packages/contracts/src/api/governance.ts`);vitest(纯逻辑单测);皮肤复用 `console.css` 的 `g-*` 类,**不新造类名**。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 五门必须全绿才算一个任务完成:`pnpm typecheck && pnpm lint && pnpm depcruise && pnpm api:check && pnpm test`(api:check 报契约变更时:同步 CONTRACT.md → `pnpm api:check --update` → `git add api-report`)。
|
||||
- 编码规范 §12.5 禁用:`any`、`as unknown as`、`@ts-ignore`、domain 层 IO、全局可变状态、删测试、改门禁配置。
|
||||
- `exactOptionalPropertyTypes: true`:显式赋 `undefined` 的可选属性要写成 `field?: T | undefined`。
|
||||
- ESM 相对导入必须带扩展名(`./x.ts` / `./x.tsx`);web 别名:`@contracts` `@web-shared` `@features` `@ui`。
|
||||
- 不新增任何 npm 依赖。
|
||||
- 治理台界面只用 `console.css` 已有的 `g-*` 类(自造类名 = 没样式);共享件从 `@web-shared/gov/primitives.tsx` 取(Chip/St/Note/Sec/Kpi/Card/Rows/NList/NRow/Entry/PageHead/Todo/Clear/QueryGate/Prop/Field/Seg/ListEdit/Acts)。
|
||||
- 空态文案用**用户视角**(「还没有…,会有什么」),不写「还没接」。
|
||||
- 授权模型不动:九治理功能 × 读/写/治;台面入口按「持有该面任一功能的任意授权」亮灯(动作蕴含 govern ⊃ write ⊃ read,有任意 grant 即至少可读)。
|
||||
- 提交信息:中文一句话,风格同 `git log`(如「三面拆分:…」),结尾加
|
||||
`Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>` 与 `Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m`。
|
||||
- 服务地址:API `:8787`(`pnpm --filter @app/api dev`),Web `:5273`(`cd apps/web && npx vite --port 5273 --strictPort`;5173 被别的项目占用)。测试账号 `Chaihao` / `ch19900614`。
|
||||
- 浏览器验证一律先 `POST /api/login` 拿 cookie(HttpOnly 会话;curl 用 `-c/-b /tmp/ch.cookie`)。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 基线入库
|
||||
|
||||
工作区里有约 90 个未提交文件(鉴权、工作组、工作台修复、编排工作区、产品设计文档等)。**先整体入库一次**,否则后续每个任务的提交都会混入无关变更,评审无从做起。
|
||||
|
||||
**Files:**
|
||||
- Modify: 无(纯 git 操作)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: 干净的工作区 —— 后续任务的 `git add` 只含本任务文件。
|
||||
|
||||
- [ ] **Step 1: 确认五门全绿(基线必须是好的)**
|
||||
|
||||
Run: `pnpm typecheck && pnpm lint && pnpm depcruise && pnpm test`
|
||||
Expected: 全部通过(api:check 已在上一轮 `--update` 并 `git add`,若再报差异则先 `pnpm api:check --update && git add api-report`)。
|
||||
|
||||
- [ ] **Step 2: 整体提交**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "基线:鉴权会话+工作组+工作台三区修复+编排工作区+产品设计文档
|
||||
|
||||
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||||
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 确认干净**
|
||||
|
||||
Run: `git status --porcelain | wc -l`
|
||||
Expected: `0`
|
||||
|
||||
---
|
||||
|
||||
### Task 2: surface 配置与纯逻辑(surfaces.ts)
|
||||
|
||||
三面骨架的唯一事实源:区归属、权限亮灯、旧链接改写 —— 全部纯函数,先测后写。
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/web/src/shared/gov/surfaces.ts`
|
||||
- Create: `apps/web/src/shared/gov/__tests__/surfaces.test.ts`
|
||||
- Modify: `apps/web/src/shared/gov/gov-route.ts`(GovZone 增减 + TABS/ZM 相应条目)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `gov-route.ts` 的 `GovZone`、`ZONES`、`TABS`、`ZM`;`@contracts` 的 `Session`、`GovernanceFunction`。
|
||||
- Produces(后续任务按名引用):
|
||||
```ts
|
||||
export type Surface = 'design' | 'ops';
|
||||
export const SURFACE_META: Readonly<Record<Surface, { path: string; brand: string; searchHint: string }>>;
|
||||
export const SURFACE_ZONES: Readonly<Record<Surface, readonly GovZone[]>>;
|
||||
export const SURFACE_FNS: Readonly<Record<Surface, readonly GovernanceFunction[]>>;
|
||||
export function canSeeSurface(session: Session | undefined, surface: Surface): boolean;
|
||||
export function surfaceOfZone(zone: GovZone): Surface;
|
||||
/** 旧 #/gov/<zone>/… 深链 → 新台面完整 URL(pathname+hash);非 gov 哈希返回 undefined */
|
||||
export function legacyGovTarget(hash: string): string | undefined;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 改 `gov-route.ts` 的区表(surfaces 的类型依赖它,先动它)**
|
||||
|
||||
在 `gov-route.ts`:
|
||||
1. `GOV_ZONES` 改为:
|
||||
```ts
|
||||
export const GOV_ZONES = [
|
||||
'blueprint', 'workflows', 'library', 'tools', 'data', // 设计中心
|
||||
'cockpit', 'tasks', 'people', 'ledgers', 'knowledge', 'system', // 运营中心
|
||||
] as const;
|
||||
```
|
||||
(删 `'triggers'` 与 `'memory'` —— 触发器定义并入 workflows,处置并入 tasks;度量升驾驶舱,活动记忆归知识库。)
|
||||
2. `ZONES` 数组同步(label/icon):`blueprint 蓝图/route`、`cockpit 驾驶舱/gauge`、`ledgers 台账/database`、`knowledge 知识库/layers`;其余沿用;删 triggers/memory 两行。
|
||||
3. `ZM` 增四条、删两条(desc 从 `docs/产品设计.md` 二、三节的定位文字取;`ledgers` 的 desc:「行数据 · 版本 · 引用 —— 域的 schema 在设计中心改,这里做运营动作」)。
|
||||
4. `TABS` 增删:
|
||||
```ts
|
||||
blueprint: [{ key: 'map', label: '企业蓝图', ico: 'route' }],
|
||||
cockpit: [{ key: 'dash', label: '驾驶舱', ico: 'gauge' }],
|
||||
knowledge: [{ key: 'layers', label: '活动记忆', ico: 'layers' }],
|
||||
ledgers: [], // 动态:页签即数据域,由壳层运行时生成
|
||||
workflows: [ …原三条…, { key: 'rules', label: '调起规则', ico: 'zap' } ],
|
||||
tasks: [ …原四条…, { key: 'trg', label: '起单开关', ico: 'zap' } ],
|
||||
```
|
||||
(`TABS` 类型放宽为 `ReadonlyArray<…>` 可空数组即可,`readPlace` 对空 tabs 的兜底在 Task 4 处理。)
|
||||
|
||||
- [ ] **Step 2: 写失败测试**
|
||||
|
||||
`apps/web/src/shared/gov/__tests__/surfaces.test.ts`:
|
||||
```ts
|
||||
/**
|
||||
* 三面骨架的唯一事实源 —— 区归属、亮灯、旧链接改写。
|
||||
* 这里守的是「配置自洽」:每个区恰属一面;亮灯只看该面功能;旧深链一条不死。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { Session } from '@contracts';
|
||||
import { sessionSchema } from '@contracts';
|
||||
import { GOV_ZONES } from '../gov-route.ts';
|
||||
import { SURFACE_ZONES, canSeeSurface, legacyGovTarget } from '../surfaces.ts';
|
||||
|
||||
/** 经 schema 构造合法会话 —— §12.5 禁 `as unknown as`,测试同样遵守 */
|
||||
const sessionWith = (fns: string[]): Session =>
|
||||
sessionSchema.parse({
|
||||
staff: { id: 's-1', account: 'a', name: 'n', positionId: 'p-1', state: 'active' },
|
||||
position: { id: 'p-1', code: 'PM', name: '项目经理' },
|
||||
grants: fns.map((fn) => ({
|
||||
positionId: 'p-1',
|
||||
object: { kind: 'governance', function: fn },
|
||||
action: 'read',
|
||||
})),
|
||||
});
|
||||
|
||||
describe('SURFACE_ZONES', () => {
|
||||
it('每个区恰属一面,不重不漏', () => {
|
||||
const all = [...SURFACE_ZONES.design, ...SURFACE_ZONES.ops];
|
||||
expect([...all].sort()).toEqual([...GOV_ZONES].sort());
|
||||
expect(new Set(all).size).toBe(all.length);
|
||||
});
|
||||
it('设计面没有任何运行态区', () => {
|
||||
for (const z of ['tasks', 'cockpit', 'ledgers'] as const) {
|
||||
expect(SURFACE_ZONES.design).not.toContain(z);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('canSeeSurface —— 入口按读权亮灯', () => {
|
||||
it('持有该面任一功能的授权即亮', () => {
|
||||
expect(canSeeSurface(sessionWith(['process']), 'design')).toBe(true);
|
||||
expect(canSeeSurface(sessionWith(['task']), 'ops')).toBe(true);
|
||||
});
|
||||
it('只有另一面的授权不亮', () => {
|
||||
expect(canSeeSurface(sessionWith(['task']), 'design')).toBe(false);
|
||||
});
|
||||
it('trigger 与 data 两面都算(定义归设计、处置与行数据归运营)', () => {
|
||||
expect(canSeeSurface(sessionWith(['trigger']), 'design')).toBe(true);
|
||||
expect(canSeeSurface(sessionWith(['trigger']), 'ops')).toBe(true);
|
||||
expect(canSeeSurface(sessionWith(['data']), 'ops')).toBe(true);
|
||||
});
|
||||
it('无会话不亮', () => {
|
||||
expect(canSeeSurface(undefined, 'design')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacyGovTarget —— 旧 #/gov 深链一条不死', () => {
|
||||
it('设计态区改写到 /design', () => {
|
||||
expect(legacyGovTarget('#/gov/workflows/pkgs/pkg:unified-quote%404')).toBe(
|
||||
'/design#/design/workflows/pkgs/pkg:unified-quote%404',
|
||||
);
|
||||
});
|
||||
it('运行态区改写到 /ops', () => {
|
||||
expect(legacyGovTarget('#/gov/tasks/board')).toBe('/ops#/ops/tasks/board');
|
||||
});
|
||||
it('已删的区落到对应新家:triggers→设计·workflows/rules,memory→运营·cockpit', () => {
|
||||
expect(legacyGovTarget('#/gov/triggers/rules')).toBe('/design#/design/workflows/rules');
|
||||
expect(legacyGovTarget('#/gov/memory/metrics')).toBe('/ops#/ops/cockpit/dash');
|
||||
expect(legacyGovTarget('#/gov/memory/layers')).toBe('/ops#/ops/knowledge/layers');
|
||||
});
|
||||
it('裸 #/gov 落运营首页;非 gov 哈希不管', () => {
|
||||
expect(legacyGovTarget('#/gov')).toBe('/ops#/ops/cockpit/dash');
|
||||
expect(legacyGovTarget('#/whatever')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
- [ ] **Step 3: 跑测试确认失败**
|
||||
|
||||
Run: `pnpm exec vitest run apps/web/src/shared/gov 2>&1 | tail -5`
|
||||
Expected: FAIL(`surfaces.ts` 不存在 / gov-route 缺新区)。
|
||||
|
||||
- [ ] **Step 4: 写实现 `surfaces.ts`**
|
||||
|
||||
```ts
|
||||
/**
|
||||
* 三面骨架的唯一事实源(docs/产品设计.md §1/§6)。
|
||||
* 区归属、权限亮灯、旧链接改写都从这里出 —— 壳层只消费,不各写各的。
|
||||
*/
|
||||
import type { Session } from '@contracts';
|
||||
import type { GovZone } from './gov-route.ts';
|
||||
|
||||
export type Surface = 'design' | 'ops';
|
||||
|
||||
export const SURFACE_META: Readonly<Record<Surface, { path: string; brand: string; searchHint: string }>> = {
|
||||
design: { path: '/design', brand: '设计中心', searchHint: '检索定义:流程包 / 活动 / 工具 / 数据域…' },
|
||||
ops: { path: '/ops', brand: '运营中心', searchHint: '检索运行:任务 / 工作流 / 员工 / 台账…' },
|
||||
};
|
||||
|
||||
export const SURFACE_ZONES: Readonly<Record<Surface, readonly GovZone[]>> = {
|
||||
design: ['blueprint', 'workflows', 'library', 'tools', 'data'],
|
||||
ops: ['cockpit', 'tasks', 'people', 'ledgers', 'knowledge', 'system'],
|
||||
};
|
||||
|
||||
/**
|
||||
* 亮灯功能集:trigger 与 data 两面都算 ——
|
||||
* 触发器「定义归设计、处置归运营」;数据「schema 归设计、行数据归运营」。
|
||||
*/
|
||||
export const SURFACE_FNS: Readonly<Record<Surface, readonly string[]>> = {
|
||||
design: ['process', 'activity', 'tool', 'data', 'trigger'],
|
||||
ops: ['task', 'staff', 'memory', 'system', 'trigger', 'data'],
|
||||
};
|
||||
|
||||
export function canSeeSurface(session: Session | undefined, surface: Surface): boolean {
|
||||
if (session === undefined) return false;
|
||||
// 动作蕴含 govern ⊃ write ⊃ read:持有任意动作的 grant 即至少可读,入口即亮
|
||||
return session.grants.some(
|
||||
(g) => g.object.kind === 'governance' && (SURFACE_FNS[surface] as readonly string[]).includes(g.object.function),
|
||||
);
|
||||
}
|
||||
|
||||
export function surfaceOfZone(zone: GovZone): Surface {
|
||||
return (SURFACE_ZONES.design as readonly string[]).includes(zone) ? 'design' : 'ops';
|
||||
}
|
||||
|
||||
/** 已删区的搬迁表:旧深链按内容归新家 */
|
||||
const LEGACY_ZONE: Readonly<Record<string, { surface: Surface; zone: string; tab?: string }>> = {
|
||||
triggers: { surface: 'design', zone: 'workflows', tab: 'rules' },
|
||||
memory: { surface: 'ops', zone: 'cockpit', tab: 'dash' },
|
||||
};
|
||||
|
||||
export function legacyGovTarget(hash: string): string | undefined {
|
||||
const parts = hash.replace(/^#\/?/, '').split('/').filter((s) => s !== '');
|
||||
if (parts[0] !== 'gov') return undefined;
|
||||
const zone = parts[1];
|
||||
if (zone === undefined) return '/ops#/ops/cockpit/dash';
|
||||
|
||||
const moved = LEGACY_ZONE[zone];
|
||||
if (moved !== undefined) {
|
||||
// memory 的两个旧页签各归其位
|
||||
const tab = zone === 'memory' && parts[2] === 'layers' ? undefined : moved.tab;
|
||||
const target = zone === 'memory' && parts[2] === 'layers' ? { surface: 'ops' as const, zone: 'knowledge', tab: 'layers' } : { ...moved, tab };
|
||||
return `${SURFACE_META[target.surface].path}#/${target.surface}/${target.zone}/${target.tab ?? ''}`.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
const surface = surfaceOfZone(zone as GovZone);
|
||||
const rest = parts.slice(1).join('/');
|
||||
return `${SURFACE_META[surface].path}#/${surface}/${rest}`;
|
||||
}
|
||||
```
|
||||
(实现时按测试逐条对齐 —— 特别是 `#/gov/memory/layers` 与 `#/gov/triggers/rules` 两条的期望输出。)
|
||||
|
||||
- [ ] **Step 5: 跑测试确认通过**
|
||||
|
||||
Run: `pnpm exec vitest run apps/web/src/shared/gov 2>&1 | tail -5`
|
||||
Expected: PASS(全部用例)。
|
||||
随后 `pnpm typecheck` —— gov-route 改动会把还引用 `triggers`/`memory` 区的旧代码打红(gov-shell / PkgPage 的 `nav.jump('triggers', …)` 等)。**本任务只修类型层面**:PkgPage 里 `nav.jump('triggers', 'rules', …)` 改为 `nav.jump('workflows', 'rules', …)`;gov-shell 的 TriggersHome/MemoryHome 引用暂时改挂到新区键(`knowledge`→MemoryHome、`cockpit`→MemoryHome,Task 5/8 再真正拆)。tsc 必须归零。
|
||||
|
||||
- [ ] **Step 6: 提交**
|
||||
|
||||
```bash
|
||||
git add apps/web/src/shared/gov/surfaces.ts apps/web/src/shared/gov/__tests__/surfaces.test.ts \
|
||||
apps/web/src/shared/gov/gov-route.ts apps/web/src/features/gov-process/internal/panel.tsx \
|
||||
apps/web/src/app/gov-shell.tsx
|
||||
git commit -m "三面拆分①:surface 配置与区表 —— 归属/亮灯/旧链改写皆纯函数先测后写"
|
||||
```
|
||||
(此后每个提交都带 Global Constraints 里的两行 footer,下略。)
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 网页标签三章 + 入口路由(/design、/ops、/gov 跳转)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/app/tab-identity.ts`
|
||||
- Modify: `apps/web/src/app/main.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `legacyGovTarget`、`SURFACE_META`(Task 2)。
|
||||
- Produces: `applyTabIdentity(surface: 'desk' | Surface): void`;`main.tsx` 导出的常量 `surface: 'desk' | Surface`(Gate 组件与 Task 4 的壳选择用)。
|
||||
|
||||
- [ ] **Step 1: `tab-identity.ts` 扩为三章**
|
||||
|
||||
签名 `applyTabIdentity(surface: 'desk' | 'design' | 'ops')`。新增两枚 SVG(与现有 `DESK_ICON`/`GOV_ICON` 同构;`GOV_ICON` 弃用删除):
|
||||
```ts
|
||||
/** 设计中心:深靛底 + 路网(定义企业的流转) */
|
||||
const DESIGN_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="8" fill="#232b6e"/>
|
||||
<g transform="translate(4,4)" fill="none" stroke="#9db1ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/>
|
||||
<path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15" stroke="#fff"/>
|
||||
</g>
|
||||
</svg>`;
|
||||
/** 运营中心:深色底 + 仪表(看企业今天跑得怎样) */
|
||||
const OPS_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="8" fill="#141b2e"/>
|
||||
<g transform="translate(4,4)" fill="none" stroke="#7fe0c3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m12 14 4-4" stroke="#fff"/>
|
||||
<path d="M3.34 19a10 10 0 1 1 17.32 0"/>
|
||||
</g>
|
||||
</svg>`;
|
||||
```
|
||||
标题:`JCAI 工作台` / `JCAI 设计中心` / `JCAI 运营中心`。
|
||||
|
||||
- [ ] **Step 2: `main.tsx` 入口判台**
|
||||
|
||||
替换现有 `isGovernance` 段:
|
||||
```ts
|
||||
import { legacyGovTarget, SURFACE_META } from '@web-shared/gov/surfaces.ts';
|
||||
import type { Surface } from '@web-shared/gov/surfaces.ts';
|
||||
|
||||
/** 旧 /gov 链接一条不死:带着深链改写到新台面(replace,不留历史) */
|
||||
const legacy = window.location.pathname.startsWith('/gov')
|
||||
? (legacyGovTarget(window.location.hash) ?? '/ops#/ops/cockpit/dash')
|
||||
: undefined;
|
||||
if (legacy !== undefined) window.location.replace(legacy);
|
||||
|
||||
const surface: 'desk' | Surface = window.location.pathname.startsWith(SURFACE_META.design.path)
|
||||
? 'design'
|
||||
: window.location.pathname.startsWith(SURFACE_META.ops.path)
|
||||
? 'ops'
|
||||
: 'desk';
|
||||
applyTabIdentity(surface);
|
||||
```
|
||||
样式装载:`surface === 'desk'` 装 `proto.vendor.css + app.css`,否则装 `theme.css + console.css`(两台治理面同一套皮肤)。`Gate` 里 `isGovernance` 相应改为 `surface !== 'desk'`,`GovShell` 暂以 `<GovShell />` 原样渲染(Task 4 传 surface)。
|
||||
|
||||
- [ ] **Step 3: 验证**
|
||||
|
||||
```bash
|
||||
lsof -ti:8787 >/dev/null || (nohup pnpm --filter @app/api dev >/tmp/api.log 2>&1 &)
|
||||
lsof -ti:5273 >/dev/null || (cd apps/web && nohup npx vite --port 5273 --strictPort >/tmp/web.log 2>&1 &)
|
||||
sleep 8
|
||||
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:5273/design # 200
|
||||
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:5273/ops # 200
|
||||
```
|
||||
浏览器(agent-browser)验证:开 `/design` 标签题为「JCAI 设计中心」、`/ops` 为「JCAI 运营中心」;开 `http://localhost:5273/gov#/gov/workflows/pkgs` 应落到 `/design#/design/workflows/pkgs`。
|
||||
Run: `pnpm typecheck && pnpm lint`,Expected: 通过。
|
||||
|
||||
- [ ] **Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add apps/web/src/app/tab-identity.ts apps/web/src/app/main.tsx
|
||||
git commit -m "三面拆分②:三章标签与入口路由,/gov 深链整链改写"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 壳参数化 —— 两套左轨、动态台账项、顶栏互跳亮灯
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/app/gov-shell.tsx`
|
||||
- Modify: `apps/web/src/app/main.tsx`(把 `surface` 传给壳)
|
||||
- Modify: `apps/web/src/app/app-shell.tsx`(工作台轨底加两面入口,按亮灯)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `SURFACE_META / SURFACE_ZONES / canSeeSurface`(Task 2);`useGovObjects`(域清单);`sessionRoute` + `queryKeys.session`(亮灯与轨底身份)。
|
||||
- Produces: `GovShell({ surface }: { surface: Surface })`;hash 格式 `#/<surface>/<zone>/<tab>/<k>:<id>…`;`Place.tab` 对 `ledgers` 区 = 域名。
|
||||
|
||||
- [ ] **Step 1: `readPlace`/`writePlace` 带 surface**
|
||||
|
||||
```ts
|
||||
function readPlace(surface: Surface): Place {
|
||||
const raw = window.location.hash.replace(/^#\/?/, '');
|
||||
const parts = raw.split('/').filter((s) => s !== '');
|
||||
// parts[0] 是台面名;区限定在本台面的区表内,越界落回本台首区
|
||||
const zones = SURFACE_ZONES[surface];
|
||||
const zone = (zones.includes(parts[1] as GovZone) ? parts[1] : zones[0]) as GovZone;
|
||||
const tabs = TABS[zone];
|
||||
// ledgers 的页签是数据域(动态):不校验,原样收;静态区仍校验并兜底到首签
|
||||
const tab = zone === 'ledgers' ? (parts[2] ?? '') : (tabs.find((t) => t.key === parts[2])?.key ?? tabs[0]?.key ?? '');
|
||||
…stack 解析不变…
|
||||
}
|
||||
const writePlace = (surface: Surface, p: Place): string =>
|
||||
['#', surface, p.zone, p.tab, ...p.stack.map((r) => `${r.k}:${encodeURIComponent(r.id)}`)].join('/');
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 壳按 surface 渲染**
|
||||
|
||||
`GovShell({ surface })`:
|
||||
- 左轨:`SURFACE_ZONES[surface]` 过滤 `ZONES`;品牌字 `SURFACE_META[surface].brand`;轨章图标 design=`route`、ops=`gauge`。
|
||||
- **ledgers 动态项**(仅 ops):`useGovObjects()` 取 `objects`,`domains = [...new Set(objects.map(o => o.domain))]`;左轨在 `ledgers` 位置渲染一枚分组小标题「台账」+ 每域一个按钮(`goZone` 改为 `go({zone:'ledgers', tab: domain, stack: []})`,高亮条件 `zone==='ledgers' && tab===domain`)。域为空时渲染一条灰字「随数据域发布自动生成」。
|
||||
- 顶栏:`工作台` 链接保留;新增另一面按钮(design 壳给「运营中心」→ `/ops`,ops 壳给「设计中心」→ `/design`),以 `canSeeSurface(session, …)` 决定渲染;`告警` 按钮仅 ops 壳渲染。session 取法:`useQuery({ queryKey: queryKeys.session, queryFn: () => call<Session>(sessionRoute) })`。
|
||||
- 轨底身份:删除 `account === 'fengbing'` 硬编码(现存 bug),改用上面的 session(`session.data?.staff.name`)。
|
||||
- 面包屑首项:`治理台` 改为 `SURFACE_META[surface].brand`。
|
||||
- `ZoneHome` 映射:`blueprint: BlueprintHome`(Task 6 前先渲染 `<Note>企业蓝图建设中</Note>` 的内联占位组件)、`cockpit/knowledge: MemoryHome`(Task 5 拆)、`ledgers: LedgersHome`(Task 10 前内联占位)、其余不变;`triggers` 分支删除。
|
||||
- `Detail`:不动(所有 `Route.k` 仍有效;`trigger` 详情页仍可从两面进)。
|
||||
|
||||
- [ ] **Step 3: 工作台入口亮灯**
|
||||
|
||||
`app-shell.tsx` 轨底(`jc-rail-foot` 内、身份钮上方)加:
|
||||
```tsx
|
||||
{canSeeSurface(session.data, 'design') && (
|
||||
<a className="jc-nav-user" href="/design" title="设计中心"><span className="jc-nav-ava">设</span><span className="jc-nav-name">设计</span></a>
|
||||
)}
|
||||
{canSeeSurface(session.data, 'ops') && (
|
||||
<a className="jc-nav-user" href="/ops" title="运营中心"><span className="jc-nav-ava">运</span><span className="jc-nav-name">运营</span></a>
|
||||
)}
|
||||
```
|
||||
(复用 `jc-nav-user` 皮肤,不造新类。)
|
||||
|
||||
- [ ] **Step 4: 验证**
|
||||
|
||||
浏览器:
|
||||
1. `/ops` 登录柴浩(TEST 岗九功能全 govern)→ 左轨 = 驾驶舱/任务/员工/台账(报价域、共享域)/知识库/系统;顶栏见「设计中心」「工作台」。
|
||||
2. `/design` → 左轨 = 蓝图/流程包/活动库/工具箱/数据域;无告警钮。
|
||||
3. 点台账「报价域」→ hash 变 `#/ops/ledgers/quote_intake…`(域名),再点「共享域」切换。
|
||||
4. 工作台 `/` 轨底出现 设计/运营 两枚入口。
|
||||
Run: `pnpm typecheck && pnpm lint && pnpm exec vitest run apps/web`,Expected: 通过。
|
||||
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add apps/web/src/app/gov-shell.tsx apps/web/src/app/main.tsx apps/web/src/app/app-shell.tsx
|
||||
git commit -m "三面拆分③:壳参数化两台,台账随域动态生成,顶栏互跳按权亮灯"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 驾驶舱(运营首页)+ 度量路由补齐八指标
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/contracts/src/api/governance.ts`(`govMetricsRoute` 输出补三字段)
|
||||
- Modify: `apps/api/src/app/routes/governance-ops.ts`(metrics handler 补映射)
|
||||
- Modify: `apps/web/src/shared/gov/gov-api.ts`(`usePkgReports` 自 gov-process 面板上移并更名 `useGovPkgReports` —— 驾驶舱的未闭合草稿数用它;实现体见 Task 6 Step 1 的代码块,**在本任务完成上移**,Task 6 只消费)
|
||||
- Modify: `apps/web/src/features/gov-process/internal/panel.tsx`(改引上移后的 hook,删本地副本)
|
||||
- Modify: `apps/web/src/features/gov-memory/internal/panel.tsx`(拆出 CockpitHome / KnowledgeHome)
|
||||
- Modify: `apps/web/src/features/gov-memory/index.ts`(出口)
|
||||
- Modify: `apps/web/src/app/gov-shell.tsx`(cockpit/knowledge 挂新组件)
|
||||
- Modify: `api-report/`(api:check --update)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `container.memory.metrics`(已计算八指标,现路由只暴露六)、`useGovTasks`(异常工作流数)、`useGovPackages` + `validateProcessRoute`(未闭合草稿数)。
|
||||
- Produces: `govMetricsRoute.output` 增 `breakpoint: z.number()`, `unitCost: z.object({ tokens: z.number(), minutes: z.number(), humanCalls: z.number() })`, `terminalMix: z.record(z.string(), z.number())`;组件 `CockpitHome({ tab, nav }: HomeProps)`、`KnowledgeHome({ tab, nav }: HomeProps)`。
|
||||
|
||||
- [ ] **Step 1: 契约与 handler**
|
||||
|
||||
`govMetricsRoute.output` 补三字段(全部 memory 模块已算,handler 里从 `metrics` 结果透传;对照 `packages/contracts/src/api/workbench.ts` 的 `reportMetricsSchema` 字段名保持一致)。跑 `pnpm api:check` 确认只报此处差异 → 同步 `apps/api/src/modules/memory/CONTRACT.md` 影响半径一句 → `pnpm api:check --update && git add api-report`。
|
||||
|
||||
- [ ] **Step 2: 拆 gov-memory 面板**
|
||||
|
||||
`CockpitHome` = 原 metrics 页签内容 + 顶部加一排 KPI:
|
||||
```tsx
|
||||
<div className="g-kpis">
|
||||
<Kpi ico="alert-triangle" tone="danger" label="异常工作流" value={abnormal} sub="失败走向未消化,待人工处置" />
|
||||
<Kpi ico="git-merge" tone="warn" label="未闭合草稿" value={draftBad} sub="边不闭合不得发布" />
|
||||
<Kpi ico="activity" tone="brand" label="在途任务" value={running} sub="运行中 + 等待中" />
|
||||
</div>
|
||||
```
|
||||
`abnormal` 自 `useGovTasks().data.workflows.filter(w=>w.state==='abnormal').length`;`draftBad` 自 `useGovPackages` + `useGovPkgReports`(本任务上移到 gov-api.ts);`running` 自 tasks。八指标卡沿用原 metrics 渲染,补 `断点率/单位成本/终态分布` 三卡(数据来自 Step 1 扩的路由)。点「异常工作流」卡 → `nav.jump('tasks','todo')`。
|
||||
`KnowledgeHome` = 原 layers 页签内容原样搬入。原 `MemoryHome` 删除。
|
||||
**死信卡不做**(无查询数据源),在 Step 4 记入 debt。
|
||||
|
||||
- [ ] **Step 3: 验证**
|
||||
|
||||
curl:`curl -s -b /tmp/ch.cookie http://localhost:8787/api/gov/metrics | python3 -m json.tool` → 九个键齐(六旧 + 三新)。
|
||||
浏览器:`/ops` 开屏即驾驶舱(KPI 排 + 八指标);知识库页签只剩活动记忆。
|
||||
Run: 五门,Expected: 全绿。
|
||||
|
||||
- [ ] **Step 4: 记欠账 + 提交**
|
||||
|
||||
`docs/debt.md` 追加:「驾驶舱·死信卡:死信目前只送治理者消息要素,无计数查询面 —— 驾驶舱首版不含死信卡,补上需给 system_message 一个按 kind 计数的治理路由」。
|
||||
```bash
|
||||
git add packages/contracts apps/api/src/app/routes/governance-ops.ts apps/api/src/modules/memory/CONTRACT.md \
|
||||
api-report apps/web/src/features/gov-memory apps/web/src/app/gov-shell.tsx docs/debt.md
|
||||
git commit -m "三面拆分④:驾驶舱首页 —— 度量路由补齐八指标,异常/草稿/在途三卡"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 企业蓝图(设计首页)
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/web/src/features/gov-blueprint/index.ts`
|
||||
- Create: `apps/web/src/features/gov-blueprint/internal/panel.tsx`
|
||||
- Modify: `apps/web/src/app/gov-shell.tsx`(blueprint 挂 BlueprintHome)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `useGovPackages / useGovActivities / useGovTools / useGovObjects / useGovTriggers` + `useGovPkgReports(packages)`。
|
||||
- Produces: `useGovPkgReports(packages: readonly ProcessPackage[]): UseQueryResult<Map<string, ClosureReport>>`;`BlueprintHome({ tab, nav }: HomeProps)`。
|
||||
|
||||
- [ ] **Step 1: 确认 hook 已上移(Task 5 完成);若按乱序执行先补上移**
|
||||
|
||||
`gov-api.ts` 的 `useGovPkgReports`(实现体即原 gov-process 面板里的 `usePkgReports`,原地删除并改引用):
|
||||
```ts
|
||||
export const useGovPkgReports = (packages: readonly ProcessPackage[]): UseQueryResult<Map<string, ClosureReport>> =>
|
||||
useQuery({
|
||||
queryKey: ['gov', 'process-reports', packages.map((p) => `${p.id}@${p.version}`).join(',')],
|
||||
enabled: packages.length > 0,
|
||||
queryFn: async () =>
|
||||
new Map(
|
||||
await Promise.all(
|
||||
packages.map(
|
||||
async (p) => [`${p.id}@${p.version}`, await call<ClosureReport>(validateProcessRoute, { body: { pkg: p } })] as const,
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: BlueprintHome**
|
||||
|
||||
因果链一屏(全部用既有 `g-*` 件):
|
||||
```tsx
|
||||
export function BlueprintHome({ nav }: HomeProps) {
|
||||
const pkgs = useGovPackages(); const acts = useGovActivities();
|
||||
const tools = useGovTools(); const objs = useGovObjects(); const trgs = useGovTriggers();
|
||||
const reports = useGovPkgReports(pkgs.data?.packages ?? []);
|
||||
const drafts = (pkgs.data?.packages ?? []).filter((p) => p.state !== 'published');
|
||||
const draftBad = drafts.filter((p) => reports.data?.get(`${p.id}@${p.version}`)?.publishable === false);
|
||||
const violations = [...(reports.data?.values() ?? [])].flatMap((r) => r.violations);
|
||||
const domains = [...new Set((objs.data?.objects ?? []).map((o) => o.domain))];
|
||||
return (
|
||||
<>
|
||||
<Note>战略 → 方法 → 能力 → 对象:流程包决定企业以哪些流转运转,活动库决定每件事怎么干,工具箱决定系统能对世界做什么,数据域决定企业运营什么。设计中心没有任何运行态数字。</Note>
|
||||
<div className="g-kpis">
|
||||
<Kpi ico="workflow" tone="brand" label="流程包" value={pkgs.data?.packages.length ?? 0} sub={`草稿 ${drafts.length} · 未闭合 ${draftBad.length}`} />
|
||||
<Kpi ico="boxes" tone="collab" label="活动" value={acts.data?.activities.length ?? 0} sub="一个活动 = 一个任务的定义" />
|
||||
<Kpi ico="wrench" tone="auto" label="工具" value={tools.data?.tools.length ?? 0} sub="读类自由调用 · 写类登记意向" />
|
||||
<Kpi ico="database" tone="ok" label="数据域" value={domains.length} sub="发布即在运营中心长出台账位" />
|
||||
</div>
|
||||
<Sec ico="flag" title="待补清单" extra="闭合校验与契约缺口聚在这里,点进即改">
|
||||
{violations.length === 0 ? (
|
||||
<Clear>无待补 —— 全部流程包闭合校验通过。</Clear>
|
||||
) : (
|
||||
<div className="g-todo">
|
||||
{draftBad.map((p) => (
|
||||
<Todo key={`${p.id}@${p.version}`} tone="warn" ico="git-merge"
|
||||
title={`${p.name} v${p.version} · 闭合校验未过`}
|
||||
desc={(reports.data?.get(`${p.id}@${p.version}`)?.violations ?? []).map((v) => v.message).join(';')}
|
||||
acts={[{ label: '去编排', ico: 'route', onClick: () => nav.push({ k: 'pkg', id: `${p.id}@${p.version}` }) }]} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Sec>
|
||||
<Sec ico="zap" title="调起规则" extra={`${(trgs.data?.triggers ?? []).length} 条 —— 定义随流程包,处置在运营中心`}>
|
||||
<div className="g-entries">
|
||||
<Entry ico="zap" title="查看全部调起规则" desc="周期 / 事件 / 登记三类 · 条件用 S-FEEL 子集" onClick={() => nav.jump('workflows', 'rules')} />
|
||||
</div>
|
||||
</Sec>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
(工具/活动的 output 字段名以 `GovToolsOutput`/`GovActivitiesOutput` 实际为准 —— 写前 `grep listToolsRoute -A10` 校对。)
|
||||
|
||||
- [ ] **Step 3: 验证 + 提交**
|
||||
|
||||
浏览器:`/design` 开屏即蓝图(四卡 + 待补清单;v5 草稿若仍未发布应出现在待补或草稿计数里)。五门全绿。
|
||||
```bash
|
||||
git add apps/web/src/shared/gov/gov-api.ts apps/web/src/features/gov-blueprint apps/web/src/features/gov-process/internal/panel.tsx apps/web/src/app/gov-shell.tsx
|
||||
git commit -m "三面拆分⑤:企业蓝图首页 —— 四库因果链与待补清单"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 触发器 upsert 路由(后端)
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/contracts/src/api/governance.ts`
|
||||
- Modify: `apps/api/src/app/routes/governance-ops.ts`
|
||||
- Modify: `api-report/`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `container.trigger.upsert(config: TriggerConfig)`(模块出口已有,播种在用)。
|
||||
- Produces:
|
||||
```ts
|
||||
export const upsertTriggerRoute = {
|
||||
method: 'PUT',
|
||||
path: '/api/gov/triggers',
|
||||
body: z.object({ trigger: triggerConfigSchema }),
|
||||
output: z.object({ trigger: triggerConfigSchema }),
|
||||
} as const satisfies RouteContract;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 契约 + handler**
|
||||
|
||||
governance-ops.ts(紧挨 `toggleTriggerRoute`,复用 `governTrigger` 守卫):
|
||||
```ts
|
||||
/** 调起规则的定义面(设计中心):新建 / 改规则 / 改装配 —— 启停仍走 toggle */
|
||||
app.put(upsertTriggerRoute.path, async (c) => {
|
||||
const denied = await governTrigger(c.get('account'));
|
||||
if (denied) return c.json(denied, denyStatus(denied));
|
||||
const parsed = upsertTriggerRoute.body.safeParse(await c.req.json());
|
||||
if (!parsed.success) {
|
||||
const body: ApiError = { code: 'validation-failed', message: '触发器不合 schema', detail: parsed.error.issues };
|
||||
return c.json(body, 400);
|
||||
}
|
||||
return c.json({ trigger: await container.trigger.upsert(parsed.data.trigger) });
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: curl 验证**
|
||||
|
||||
```bash
|
||||
curl -s -b /tmp/ch.cookie -X PUT http://localhost:8787/api/gov/triggers \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"trigger":{"id":"trg-rfq-manual","name":"人工登记起单(统一报价)","enabled":true,"processPackageId":"unified-quote","mapping":[],"spec":{"kind":"registration"}}}'
|
||||
```
|
||||
Expected: 200 + trigger 回显;错 schema 给 400;无权给 403。
|
||||
|
||||
- [ ] **Step 3: api:check + 提交**
|
||||
|
||||
CONTRACT.md:trigger 模块面未变(路由在 app 层),无需动;`pnpm api:check --update && git add api-report`。五门全绿。
|
||||
```bash
|
||||
git add packages/contracts apps/api/src/app/routes/governance-ops.ts api-report
|
||||
git commit -m "三面拆分⑥:触发器 upsert 路由 —— 定义面的 HTTP 口"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 调起规则(设计侧)—— workflows/rules 页签 + 包详情可编辑
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/features/gov-trigger/internal/panel.tsx`(TriggersHome 改造为 `TriggerRulesHome` 定义面:列表 + 编辑表单;`TriggerPage` 保留)
|
||||
- Modify: `apps/web/src/features/gov-trigger/index.ts`
|
||||
- Modify: `apps/web/src/features/gov-process/internal/panel.tsx`(WorkflowsHome 增 `tab === 'rules'` 分支渲染 `TriggerRulesHome`;PkgPage「调起它的触发器」段加「编辑」进入行内表单)
|
||||
- Modify: `apps/web/src/app/gov-shell.tsx`(TriggersHome 引用清理)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `upsertTriggerRoute`(Task 7)、`useGovTriggers`、`toggleTriggerRoute`(启停不在此面,只读展示状态)。
|
||||
- Produces: `TriggerRulesHome({ nav }: { nav: Nav })` —— 供 workflows 区 rules 页签与蓝图入口复用。
|
||||
|
||||
- [ ] **Step 1: TriggerRulesHome(定义面)**
|
||||
|
||||
列表(NList/NRow):名称 / 三类章 / 规则原文 / 目标包;行点开 `nav.push({k:'trigger', id})`。行尾「编辑」展开行内表单(Prop/Field 版式):
|
||||
- `name` 文本;`processPackageId` 下拉(useGovPackages 的 id 去重);
|
||||
- `spec` 按 kind 三分支:registration 无参;event 给 `eventType`(data/time/human/control 下拉)+ `rule` textarea;periodic-calendar 给 `calendar` 文本;periodic-data 给 domain/object/dateField/offset/filter 五格(domain/object 下拉自 useGovObjects);
|
||||
- `mapping` 用 `ListEdit` 编辑 `target=source.path` 形式的行,提交时解析为 `{target, source:{from:'initiation', path}}`(定义面先支持起单载荷映射这一种 —— 其余 source 类型显示只读,注明「在流程包边映射里维护」);
|
||||
- 保存:`call(upsertTriggerRoute, { body: { trigger } })` → 失效 `['gov','triggers']`;`AskBand` 确认带(影响:此后命中规则即按新定义起单;可再改;即时生效)。
|
||||
**启停开关不出现在此面**(那是运营的事,Task 9)。
|
||||
|
||||
- [ ] **Step 2: 接线**
|
||||
|
||||
WorkflowsHome:`if (tab === 'rules') return <TriggerRulesHome nav={nav} />;`。
|
||||
PkgPage「调起它的触发器」段:每行加「编辑规则」按钮 → `nav.jump('workflows', 'rules')`(编辑集中在 rules 页签,包详情不重复摆表单);无绑定时的 Note 文案追加「在调起规则页签新建」。
|
||||
|
||||
- [ ] **Step 3: 验证 + 提交**
|
||||
|
||||
浏览器:`/design` → 流程包 → 调起规则:改 `trg-rfq-manual` 的 name 存盘,刷新仍在;`curl …/api/gov/triggers` 回显新名。工作台起单验证不受影响(`POST /api/triggers/trg-rfq-manual/register` 仍 200)。五门全绿。
|
||||
```bash
|
||||
git add apps/web/src/features/gov-trigger apps/web/src/features/gov-process/internal/panel.tsx apps/web/src/app/gov-shell.tsx
|
||||
git commit -m "三面拆分⑦:调起规则并入流程包区 —— 触发器定义可编辑"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: 起单开关(运营侧)—— tasks/trg 页签
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/features/gov-trigger/internal/panel.tsx`(新增 `TriggerOpsTab`:启停 + 状态)
|
||||
- Modify: `apps/web/src/features/gov-trigger/index.ts`
|
||||
- Modify: `apps/web/src/features/gov-task/internal/panel.tsx`(TasksHome 增 `tab === 'trg'` 分支)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `useGovTriggers`、`toggleTriggerRoute`(既有:`POST /api/gov/triggers/:id/toggle`,body `{enabled}` —— 写前 grep 确认参数形状)。
|
||||
- Produces: `TriggerOpsTab({ nav }: { nav: Nav })`。
|
||||
|
||||
- [ ] **Step 1: TriggerOpsTab**
|
||||
|
||||
NList 每触发器一行:三类章 / 规则原文 / 目标包 / `St` 状态章;行尾「启用/停用」按钮走 `AskBand` 确认(影响文案照原型:「触发器不入流程包结构…此后不再起新单;已起的照常跑完」),`useMutation` → toggle → 失效 `['gov','triggers']`。行点开仍 `nav.push({k:'trigger', id})`。「编辑规则」入口按 `canSeeSurface(session,'design')` 亮灯,点了整页跳 `/design#/design/workflows/rules`。
|
||||
|
||||
- [ ] **Step 2: 验证 + 提交**
|
||||
|
||||
浏览器:`/ops` → 任务 → 起单开关:停用 `trg-rfq-manual` → 工作台登记起单应被拒(`fireByRegistration` 回 `skipped:'disabled'`);再启用恢复。五门全绿。
|
||||
```bash
|
||||
git add apps/web/src/features/gov-trigger apps/web/src/features/gov-task/internal/panel.tsx
|
||||
git commit -m "三面拆分⑧:起单开关进运营·任务区 —— 定义与处置各归其位"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: 台账动态位 + 数据域定义面瘦身
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/features/gov-data/internal/panel.tsx`(拆三件:`DataHome` 定义面、`LedgersHome` 运营面、`ObjPage` 按来源分呈现)
|
||||
- Modify: `apps/web/src/features/gov-data/index.ts`
|
||||
- Modify: `apps/web/src/app/gov-shell.tsx`(ledgers 挂 `LedgersHome`,替换 Task 4 的占位)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `useGovObjects`(域/对象清单)、`useGovLedger(domain, object)`(列 / 主键 / 行 / 总数)。
|
||||
- Produces: `LedgersHome({ domain, nav }: { domain: string; nav: Nav })` —— 壳层以 `tab` 为域名传入;`DataHome` 保持 `HomeProps` 签名。
|
||||
|
||||
- [ ] **Step 1: LedgersHome(运营面)**
|
||||
|
||||
按传入 `domain` 过滤对象,每对象一张 `NRow`(名称 / 行数徽 —— 行数取 `useGovLedger` 的 `total`,懒加载:仅当前域的对象查询);点行 `nav.push({k:'obj', id: `${domain}.${object}`})` 进 `ObjPage` 看行数据(现 ObjPage 已是行浏览,复用)。空域文案:「这个域还没有对象 —— 对象随活动安装物化」。
|
||||
|
||||
- [ ] **Step 2: DataHome 瘦身(定义面)**
|
||||
|
||||
设计侧只留定义信息:域分组 → 对象卡(列结构 `columns`、主键、被哪些活动声明 —— 声明反查暂无查询面,此卡先列 `columns/primaryKey`,行数与行浏览入口**移除**);卡底加一行灰字「行数据在运营中心 · 台账」。`ObjPage` 从设计侧不再可达(设计侧对象卡不下钻行数据)。
|
||||
|
||||
- [ ] **Step 3: 验证 + 提交**
|
||||
|
||||
浏览器:`/ops` 台账·报价域 → 对象行数与行浏览正常;`/design` 数据域 → 只见结构,无行数据入口。五门全绿。
|
||||
```bash
|
||||
git add apps/web/src/features/gov-data apps/web/src/app/gov-shell.tsx
|
||||
git commit -m "三面拆分⑨:台账动态位入运营,数据域瘦身为纯定义面"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 11: 工作组管理(员工区新页签 + 读写路由)
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/contracts/src/api/governance.ts`(两条路由)
|
||||
- Modify: `apps/api/src/app/routes/governance-ops.ts`(handler,app 层组合 org 出口)
|
||||
- Modify: `apps/web/src/features/gov-staff/internal/panel.tsx`(PeopleHome 增 `tab === 'groups'`)
|
||||
- Modify: `apps/web/src/shared/gov/gov-route.ts`(people TABS 增 `{ key: 'groups', label: '工作组', ico: 'users' }`)
|
||||
- Modify: `api-report/`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `container.org.listStaff() / workGroupOf(staffId) / saveWorkGroup({id?, name, ownerId, members})`(均在 org 出口);`workGroupSchema`(@contracts)。
|
||||
- Produces:
|
||||
```ts
|
||||
export const listWorkGroupsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/work-groups',
|
||||
output: z.object({ groups: z.array(workGroupSchema.extend({ ownerName: z.string() })) }),
|
||||
} as const satisfies RouteContract;
|
||||
export const saveWorkGroupRoute = {
|
||||
method: 'PUT',
|
||||
path: '/api/gov/work-groups',
|
||||
body: z.object({ ownerId: staffIdSchema, name: z.string().min(1), members: z.record(z.string(), staffIdSchema) }),
|
||||
output: z.object({ group: workGroupSchema }),
|
||||
} as const satisfies RouteContract;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: handler(守卫 `guard('staff', 'read'/'govern')`)**
|
||||
|
||||
```ts
|
||||
app.get(listWorkGroupsRoute.path, async (c) => {
|
||||
const denied = await readStaff(c.get('account'));
|
||||
if (denied) return c.json(denied, denyStatus(denied));
|
||||
const { staff } = await container.org.listStaff();
|
||||
const groups = [];
|
||||
for (const s of staff) {
|
||||
const g = await container.org.workGroupOf(s.id);
|
||||
if (g !== undefined) groups.push({ ...g, ownerName: s.name });
|
||||
}
|
||||
return c.json({ groups });
|
||||
});
|
||||
|
||||
app.put(saveWorkGroupRoute.path, async (c) => {
|
||||
const denied = await governStaff(c.get('account'));
|
||||
if (denied) return c.json(denied, denyStatus(denied));
|
||||
const parsed = saveWorkGroupRoute.body.safeParse(await c.req.json());
|
||||
if (!parsed.success) {
|
||||
const body: ApiError = { code: 'validation-failed', message: '工作组不合 schema', detail: parsed.error.issues };
|
||||
return c.json(body, 400);
|
||||
}
|
||||
// 一人一组:已有组即更新(id 复用),没有即新建 —— 与 org.workGroupOf 的取组口径一致
|
||||
const existing = await container.org.workGroupOf(parsed.data.ownerId);
|
||||
const group = await container.org.saveWorkGroup({
|
||||
...(existing === undefined ? {} : { id: existing.id }),
|
||||
name: parsed.data.name,
|
||||
ownerId: parsed.data.ownerId,
|
||||
members: parsed.data.members,
|
||||
});
|
||||
return c.json({ group });
|
||||
});
|
||||
```
|
||||
(`readStaff`/`governStaff` 守卫若不存在,按文件内既有 `guard('staff', …)` 工厂补两行。)
|
||||
|
||||
- [ ] **Step 2: 界面(people/groups 页签)**
|
||||
|
||||
列表:每组一张 `Card`(组名 · 组长 ownerName · 成员表 `Rows`:岗位 code → 员工名,员工名经 `useGovStaff` 的 id→name 映射)。编辑:选组长(下拉全员)→ 逐岗位下拉选员工(岗位集 = `useGovStaff().positions` 的 code;可留空 = 该岗位不绑定)→ `AskBand` 确认(影响:「仅新起的工作流;在途不受影响 —— 绑定是起单时的快照」)→ PUT → 失效 `['gov','work-groups']` 键(新增 hook `useGovWorkGroups` 于 gov-api.ts)。空态:「还没有工作组 —— 建一个,起单时整条流可以按组绑定到人」。
|
||||
|
||||
- [ ] **Step 3: 验证 + 提交**
|
||||
|
||||
curl:GET 应见播种的 `wg-solo-ai`/`wg-solo-chaihao`;PUT 改柴浩组名后 GET 回显。浏览器:`/ops` 员工 → 工作组页签可看可改;改完用柴浩起一单,`sys.workflow.work_group` 快照应含新成员表。`pnpm api:check --update && git add api-report`(org 模块 CONTRACT.md 影响半径补一句:app 层新增工作组读写路由)。五门全绿。
|
||||
```bash
|
||||
git add packages/contracts apps/api/src/app/routes/governance-ops.ts apps/api/src/modules/org/CONTRACT.md \
|
||||
api-report apps/web/src/features/gov-staff apps/web/src/shared/gov/gov-route.ts apps/web/src/shared/gov/gov-api.ts
|
||||
git commit -m "三面拆分⑩:工作组管理落员工区 —— ADR-011 欠账清账"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 12: 收口 —— 走查、门禁、文档对账
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/debt.md`(核销已清项:工作组界面;新增项见各任务)
|
||||
- Modify: `docs/产品设计.md`(如实现与设计有出入,改设计或记偏差 —— 文档与实现不许各说各话)
|
||||
|
||||
**Interfaces:** 无新接口。
|
||||
|
||||
- [ ] **Step 1: 全链走查(浏览器,柴浩账号)**
|
||||
|
||||
1. `/` 工作台:轨底两枚入口 → 进 `/ops`;
|
||||
2. `/ops` 驾驶舱开屏 → 任务/员工/台账×2/知识库/系统逐区开一遍,无空白页、无 `(无)`;
|
||||
3. 起单开关停/启验证一轮;
|
||||
4. `/design` 蓝图开屏 → 流程包(编排工作区仍可用:v5 草稿点开、属性卡、编辑芯片)→ 调起规则改名 → 活动库/工具箱/数据域逐区开;
|
||||
5. 旧链接 `/gov#/gov/workflows/pkgs`、`/gov#/gov/memory/layers` 各自落对;
|
||||
6. 工作台起一单 → `/ops` 驾驶舱在途 +1。
|
||||
|
||||
- [ ] **Step 2: 五门 + 文档**
|
||||
|
||||
Run: `pnpm gen:tiers && pnpm typecheck && pnpm lint && pnpm depcruise && pnpm api:check && pnpm test`
|
||||
Expected: 全绿(api:check 无未入库差异)。
|
||||
`docs/debt.md`:划掉「工作组无治理台界面」;确认「驾驶舱死信卡」「声明反查无查询面」两条在列。
|
||||
|
||||
- [ ] **Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add docs
|
||||
git commit -m "三面拆分⑪:收口 —— 走查通过,债务台账对齐"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自审记录(写完计划后跑过)
|
||||
|
||||
- **规格覆盖**:产品设计.md §5 前端改动清单五项 → Task 3/4(壳与标签)、5/6(两首页)、7/8/9(触发器归位)、10(台账+数据域)、11(工作组);§6 权限亮灯 → Task 2/4;§1 `/gov` 跳转 → Task 3。驾驶舱「死信卡」为已声明偏差(Task 5 Step 4 记 debt)。
|
||||
- **占位扫描**:无 TBD;两处「写前 grep 校对」是对既有代码形状的核对动作,不是留白。
|
||||
- **类型一致性**:`canSeeSurface(session, surface)` 三处消费(Task 4 壳、Task 4 工作台、Task 9 编辑入口)同签名;`useGovPkgReports` 的**上移动作归 Task 5**,Task 6 只消费 —— 前向依赖已消除。
|
||||
Reference in New Issue
Block a user