Files
JX-BPOS/apps/web/src/shared/gov/surfaces.ts
T
ChaiHaoandClaude Opus 5 e5cfca638f 运营中心撤掉「数据」区;流程包首页重排:卡有层次、⋯ 管编辑与删除、新建变幽灵卡
**运营 · 数据(活动记忆四层)整个撤掉** —— 记忆的放行口还没成形,摆一个空区不如
不摆;真要做时随「数据资产」一并重开。区表、面归属、shell 映射、KnowledgeHome /
MemPage、活动详情里那枚入口一并清干净;旧深链 `#/gov/memory/layers` 落运营首页
(测试跟着改口径,不留死链)。运营中心自此六区。

**流程包首页**:
  · 「编排要素:四种」这枚计数删了 —— 网关早废,它是句没人用的口号;
  · 卡片重做。从前是「头 + 芯片行 + 一大片空 + 脚注」,158 高全靠留白撑;
    现在两段:头(图标 · 名 · 标识 · 生效版 · ⋯)+ **数据板**(活动/审批/边闭合/版本
    四格等宽、竖线分隔)。段段有内容、段间有线,高矮由内容说了算 —— 实测 111 高;
  · 卡上 **⋯ 菜单**:编辑(就地改名,落在卡面那一版上)/ 删除(整条流,菜单原地翻成
    确认,不另弹窗);
  · **新建按钮变幽灵卡**:虚线卡住在货架末位,与真卡同宽同高(111/111)。

删整条流是新能力:`DELETE /api/gov/process/:id` + `process.deleteFlow`。
生效版这道闸不设(明示动作,不是手滑),在途单那道照设(PinnedVersionError → 409);
**「还有调起规则指着它」由路由层反查** —— process 是 L7,不该知道 trigger 的存在。

实测(丁子乔账号):改名往返、删除被 2 条调起规则如实挡回、幽灵卡建 `tmp-drop`
再删掉整条流,库里回到只剩 unified-quote v4,试跑数据已清。五闸绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-29 22:38:52 +08:00

107 lines
4.1 KiB
TypeScript

/**
* 三面骨架的唯一事实源(docs/产品设计.md §1 / §6)。
*
* 产品分三面:设计中心定义企业、运营中心看企业跑、工作台在企业里干活。
* 区归属、入口亮灯、旧链接改写都从这里出 —— 壳层只消费,不各写各的。
*/
import type { GovernanceFunction, Session } from '@contracts';
import { GOV_ZONES } from './gov-route.ts';
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: '检索运行:工作流 / 任务 / 员工 / 台账…',
},
};
/** 区归属:设计九区 + 运营六区,恰好铺满 `GOV_ZONES`(由测试守住) */
export const SURFACE_ZONES: Readonly<Record<Surface, readonly GovZone[]>> = {
design: ['blueprint', 'triggers', 'workflows', 'library', 'tools', 'data', 'archive', 'models', 'testbed'],
ops: ['cockpit', 'runs', 'tasks', 'people', 'ledgers', 'system'],
};
/**
* 亮灯功能集 —— 授权模型不动(仍是九功能 × 读/写/治),这里只说「哪个功能属哪一面」。
*
* `trigger` 与 `data` 两面都算:触发器**定义归设计、启停归运营**;
* 数据**结构归设计、行数据归运营**。同一功能的两种动作各归其位,不拆授权。
*/
export const SURFACE_FNS: Readonly<Record<Surface, readonly GovernanceFunction[]>> = {
design: ['process', 'activity', 'tool', 'data', 'trigger', 'system'],
ops: ['task', 'staff', 'memory', 'system', 'trigger', 'data'],
};
/**
* 台面入口是否亮灯。
*
* 判据是「持有该面任一功能的**任意**授权」—— 动作蕴含 govern ⊃ write ⊃ read,
* 有 grant 即至少可读,入口就该看得见;进去之后动作仍逐功能校验。
*/
export function canSeeSurface(session: Session | undefined, surface: Surface): boolean {
if (session === undefined) return false;
const fns = SURFACE_FNS[surface];
return session.grants.some((g) => g.object.kind === 'governance' && fns.includes(g.object.function));
}
export function surfaceOfZone(zone: GovZone): Surface {
return SURFACE_ZONES.design.includes(zone) ? 'design' : 'ops';
}
function isGovZone(value: string): value is GovZone {
return (GOV_ZONES as readonly string[]).includes(value);
}
/**
* 已删区的改嫁表:键是 `区` 或 `区/页签`,值是新台面下的 `台面/区/页签`。
* 精确到页签的先匹配 —— 记忆的两个旧页签各归其位(度量升驾驶舱,活动记忆归知识库)。
*/
const LEGACY_MOVES: Readonly<Record<string, string>> = {
'tasks/board': 'ops/runs/board',
'tasks/trg': 'ops/runs/conf',
'workflows/rules': 'design/triggers/rules',
'testbed/inbound': 'design/testbed/run',
'system/llm': 'design/models/llm',
memory: 'ops/cockpit/dash',
};
/** 运营首页 —— 认不出的旧链接一律落这儿,不给白屏 */
const OPS_HOME = 'ops/cockpit/dash';
function toUrl(place: string): string {
const head = place.split('/')[0];
const surface: Surface = head === 'design' ? 'design' : 'ops';
return `${SURFACE_META[surface].path}#/${place}`;
}
/**
* 旧 `#/gov/<区>/…` 深链 → 新台面的完整 URL(pathname + hash)。
* 非 gov 哈希返回 `undefined`(不是我们的事,别乱改人家的地址)。
*/
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 toUrl(OPS_HOME);
const moved = LEGACY_MOVES[`${zone}/${parts[2] ?? ''}`] ?? LEGACY_MOVES[zone];
if (moved !== undefined) return toUrl(moved);
if (!isGovZone(zone)) return toUrl(OPS_HOME);
// 仍在的区:原样带着页签与下钻栈搬到新台面
return toUrl([surfaceOfZone(zone), ...parts.slice(1)].join('/'));
}