运营中心撤掉「数据」区;流程包首页重排:卡有层次、⋯ 管编辑与删除、新建变幽灵卡

**运营 · 数据(活动记忆四层)整个撤掉** —— 记忆的放行口还没成形,摆一个空区不如
不摆;真要做时随「数据资产」一并重开。区表、面归属、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
This commit is contained in:
2026-07-29 22:38:52 +08:00
co-authored by Claude Opus 5
parent f631b5b8bb
commit e5cfca638f
18 changed files with 392 additions and 164 deletions
+5
View File
@@ -56,6 +56,11 @@ export type ProcessService = {
instancesOf(workflowId: string): Promise<NodeInstance[]>;
/** 删一版:生效版与在途钉版都不许删(见 `ActiveVersionError` / `PinnedVersionError`) */
deleteVersion(id: string, version: number): Promise<ProcessPackage[]>;
/**
* 删整条流(全部版本)—— 生效版不拦:整条流的删除是明示动作,不是手滑;
* 但**任一版有在途单就拒**(`PinnedVersionError`)。返回剩下的全部包。
*/
deleteFlow(id: string): Promise<ProcessPackage[]>;
/**
* 改版本记录:**号与名**,只动这一版 —— 号自己定。
* 名不设闸(标签不参与流转);号设两道:不许撞同流已有号(`VersionTakenError`)、
+118
View File
@@ -1052,6 +1052,124 @@ export declare const deleteProcessVersionRoute: {
}, z.core.$strip>>;
}, z.core.$strip>;
};
/**
* 删整条流 —— 连同它的**所有版本**。货架上一条流一张卡,删的就是那张卡。
* 两道闸:任一版有在途单即拒(那些单要按它跑完);还有调起规则指着它也拒 ——
* 删了包留着规则,那规则再命中就调不动任何东西,是个哑火的配置。
*/
export declare const deleteProcessFlowRoute: {
readonly method: "DELETE";
readonly path: "/api/gov/process/:id";
readonly params: z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>;
readonly output: z.ZodObject<{
packages: z.ZodArray<z.ZodObject<{
id: z.core.$ZodBranded<z.ZodString, "ProcessPackageId", "out">;
name: z.ZodString;
version: z.ZodNumber;
state: z.ZodEnum<{
active: "active";
retired: "retired";
}>;
callInterface: z.ZodObject<{
input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
output: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, z.core.$strip>;
nodes: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
kind: z.ZodLiteral<"start">;
id: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
label: z.ZodString;
layout: z.ZodOptional<z.ZodObject<{
row: z.ZodNumber;
col: z.ZodNumber;
x: z.ZodOptional<z.ZodNumber>;
y: z.ZodOptional<z.ZodNumber>;
}, z.core.$strip>>;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"end">;
output: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
id: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
label: z.ZodString;
layout: z.ZodOptional<z.ZodObject<{
row: z.ZodNumber;
col: z.ZodNumber;
x: z.ZodOptional<z.ZodNumber>;
y: z.ZodOptional<z.ZodNumber>;
}, z.core.$strip>>;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"activity">;
activityId: z.core.$ZodBranded<z.ZodString, "ActivityId", "out">;
activityVersion: z.ZodOptional<z.ZodString>;
failure: z.ZodDefault<z.ZodDiscriminatedUnion<[z.ZodObject<{
kind: z.ZodLiteral<"terminate">;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"reroute">;
edgeId: z.ZodString;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"escalate">;
approvalNodeId: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"tolerate">;
}, z.core.$strip>], "kind">>;
message: z.ZodOptional<z.ZodString>;
id: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
label: z.ZodString;
layout: z.ZodOptional<z.ZodObject<{
row: z.ZodNumber;
col: z.ZodNumber;
x: z.ZodOptional<z.ZodNumber>;
y: z.ZodOptional<z.ZodNumber>;
}, z.core.$strip>>;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"approval">;
reject: z.ZodDiscriminatedUnion<[z.ZodObject<{
kind: z.ZodLiteral<"reopen">;
nodeId: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"terminate">;
}, z.core.$strip>], "kind">;
nudge: z.ZodOptional<z.ZodObject<{
deadline: z.ZodString;
steps: z.ZodDefault<z.ZodArray<z.ZodObject<{
after: z.ZodString;
action: z.ZodEnum<{
nudge: "nudge";
escalate: "escalate";
}>;
}, z.core.$strip>>>;
}, z.core.$strip>>;
message: z.ZodOptional<z.ZodString>;
id: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
label: z.ZodString;
layout: z.ZodOptional<z.ZodObject<{
row: z.ZodNumber;
col: z.ZodNumber;
x: z.ZodOptional<z.ZodNumber>;
y: z.ZodOptional<z.ZodNumber>;
}, z.core.$strip>>;
}, z.core.$strip>], "kind">>;
edges: z.ZodArray<z.ZodObject<{
id: z.ZodString;
from: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
to: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
condition: z.ZodOptional<z.ZodString>;
label: z.ZodOptional<z.ZodString>;
tone: z.ZodOptional<z.ZodEnum<{
ok: "ok";
bad: "bad";
}>>;
reopen: z.ZodDefault<z.ZodBoolean>;
}, z.core.$strip>>;
variables: z.ZodDefault<z.ZodArray<z.ZodObject<{
name: z.ZodString;
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, z.core.$strip>>>;
description: z.ZodOptional<z.ZodString>;
diagram: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, z.core.$strip>>;
}, z.core.$strip>;
};
/**
* 改版本记录 —— **版本号自己定**(2026-07-29 用户定)、名字随之:改哪版只动哪版。
*
+30
View File
@@ -35,6 +35,7 @@ import {
resumeTaskRoute,
staffIdSchema,
taskIdSchema,
deleteProcessFlowRoute,
deleteProcessVersionRoute,
editProcessVersionRoute,
setNodeOwnerRoute,
@@ -511,6 +512,35 @@ export function registerGovernanceOpsRoutes(app: Hono<AppEnv>, container: Contai
}
});
/**
* 删整条流:连同全部版本。两道闸 —— 在途单钉着(409,用例层);
* **还有调起规则指着它也拒**(409,这一层查):删了包留着规则,那规则就是哑火的配置。
* 反查放在路由层而不是用例层:process 不该知道 trigger 的存在(L7 不依赖它)。
*/
app.delete(deleteProcessFlowRoute.path, async (c) => {
const denied = await governProcess(c.get('account'));
if (denied) return c.json(denied, denyStatus(denied));
const id = c.req.param('id');
const rules = (await container.trigger.list()).filter((t) => t.processPackageId === id);
if (rules.length > 0) {
const body: ApiError = {
code: 'conflict',
message: `还有 ${rules.length} 条调起规则指着这条流 —— 先去触发器区改掉或删掉它们`,
};
return c.json(body, 409);
}
try {
return c.json({ packages: await container.process.deleteFlow(id) });
} catch (error) {
if (error instanceof PinnedVersionError) {
const body: ApiError = { code: 'conflict', message: error.message };
return c.json(body, 409);
}
const body: ApiError = { code: 'not-found', message: error instanceof Error ? error.message : String(error) };
return c.json(body, 404);
}
});
/** 改版本记录:号自己定、名随之 —— 撞号与「跑过单改号」409,别的版一字不动 */
app.post(editProcessVersionRoute.path, async (c) => {
const denied = await governProcess(c.get('account'));
+3
View File
@@ -30,6 +30,9 @@
与**活动装载**(unknown-activity);字段类的三条检查(边未映射 / 必填未映射 / 类型相抵)全退役。
- **版本可删,两道闸**:生效版不可删(先停用,`ActiveVersionError`)、在途钉版不可删
(那些单要按它跑完,`PinnedVersionError`)。
- **整条流可删**(`deleteFlow`,2026-07-29):连同全部版本。生效版这道闸**不设** ——
删整条流是明示动作,先停用再删只是多一步仪式;在途单那道闸照设。
「还有调起规则指着它」由**路由层**反查(process 不依赖 trigger,L7 不知道它存在)。
- **版本号与名归设计人员自己定**(2026-07-29 用户定):
· **号自己编**,不再只能系统递增 —— 两道闸:不许撞同流已有号(`VersionTakenError`)、
在途钉版不许改号(`PinnedVersionError`,那些单正按这个号找定义)。
@@ -110,6 +110,17 @@ describe('版本的删与改名', () => {
expect(h.rows).toHaveLength(2);
});
it('删整条流:所有版本一起没 —— 含生效版,这是明示动作不是手滑', async () => {
const h = harness([pkg(2, 'active'), pkg(1, 'retired')]);
expect(await h.service.deleteFlow('quote')).toEqual([]);
});
it('删整条流:任一版有在途单就拒 —— 那些单要按它跑完', async () => {
const h = harness([pkg(2, 'active'), pkg(1, 'retired')], 2);
await expect(h.service.deleteFlow('quote')).rejects.toBeInstanceOf(PinnedVersionError);
expect(h.rows, '一版都没删').toHaveLength(2);
});
it('改名只动这一版 —— 各版本正是靠名字互相区分', async () => {
const h = harness([pkg(2, 'active'), pkg(1, 'retired')]);
const after = await h.service.editVersion('quote', 1, { name: '统一报价 · 手工议价' });
+5
View File
@@ -81,6 +81,11 @@ export type ProcessService = {
/** 删一版:生效版与在途钉版都不许删(见 `ActiveVersionError` / `PinnedVersionError`) */
deleteVersion(id: string, version: number): Promise<ProcessPackage[]>;
/**
* 删整条流(全部版本)—— 生效版不拦:整条流的删除是明示动作,不是手滑;
* 但**任一版有在途单就拒**(`PinnedVersionError`)。返回剩下的全部包。
*/
deleteFlow(id: string): Promise<ProcessPackage[]>;
/**
* 改版本记录:**号与名**,只动这一版 —— 号自己定。
* 名不设闸(标签不参与流转);号设两道:不许撞同流已有号(`VersionTakenError`)、
@@ -177,6 +177,22 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
return after.filter((p) => p.id === id).sort((a, b) => b.version - a.version);
},
async deleteFlow(id) {
const all = await deps.store.listPackages();
const mine = all.filter((p) => p.id === id);
if (mine.length === 0) throw new Error(`流程不存在:${id}`);
/*
* 生效版这道闸**不设**:删整条流是明示动作,先停用再删只是多一步仪式。
* 在途单那道闸必须设 —— 定义没了那些单就再也推不动,与删单版同一条理由。
*/
for (const v of mine) {
const pinned = await deps.store.countPinnedInFlight(id, v.version);
if (pinned > 0) throw new PinnedVersionError(id, v.version, pinned);
}
for (const v of mine) await deps.store.deletePackage(id, v.version);
return deps.store.listPackages();
},
async editVersion(id, version, patch) {
const one = await deps.store.getPackage(id, version);
if (one === undefined) throw new StaleVersionError(id, version);
+2 -5
View File
@@ -34,7 +34,7 @@ import { ToolPage, ToolsHome } from '@features/gov-tool';
import { DataHome, LedgersHome, ObjPage } from '@features/gov-data';
import { ArchiveHome } from '@features/gov-archive';
import { TestbedHome } from '@features/gov-testbed';
import { CockpitHome, KnowledgeHome, MemPage } from '@features/gov-memory';
import { CockpitHome } from '@features/gov-memory';
import { ChanPage, LlmPage, ModelsHome, SystemHome } from '@features/gov-system';
/* ── URL 编解码:`#/<surface>/<zone>/<tab>[/<k>:<id>…]` ────────────────── */
@@ -298,7 +298,6 @@ function ZoneHome({ zone, tab, nav }: { zone: GovZone; tab: string; nav: Nav })
tasks: TasksHome,
people: PeopleHome,
ledgers: LedgersHome,
knowledge: KnowledgeHome,
system: SystemHome,
}[zone];
return <P tab={tab} nav={nav} />;
@@ -322,13 +321,11 @@ function Detail({ route, nav }: { route: Route; nav: Nav }) {
case 'trigger':
return <TriggerPage id={route.id} />;
case 'act':
return <ActPage id={route.id} nav={nav} />;
return <ActPage id={route.id} />;
case 'tool':
return <ToolPage id={route.id} />;
case 'obj':
return <ObjPage id={route.id} />;
case 'mem':
return <MemPage id={route.id} />;
case 'llm':
return <LlmPage id={route.id} />;
case 'chan':
+39
View File
@@ -260,6 +260,45 @@
.g-tile-f > .g-btn { margin-left: auto; height: 24px; padding: 0 9px; font-size: 11px; }
.g-tile-f .r { margin-left: auto; font-family: var(--mono); color: var(--ink-4); }
/* ── 流程卡(货架)—— 两段:头(身份 · 状态 · ⋯)+ 数据板(四格计量) ──────
从前是「头 + 芯片行 + 一大片空 + 脚注」,卡撑到 158 高全靠留白;
现在段段有内容、段间有线,高矮由内容说了算。 */
.g-fcard { position: relative; display: flex; flex-direction: column; background: var(--card); border: 1px solid var(--cline); border-radius: 12px; box-shadow: var(--cshadow); overflow: hidden; transition: border-color var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast); }
.g-fcard:hover { border-color: #c9d6e6; box-shadow: 0 5px 14px -5px rgba(19, 28, 38, .22); transform: translateY(-1px); }
/* 整面一枚透明按钮 = 进这条流;字面不吃点击,直接穿到它身上(卡里还住着 ⋯ 与输入框) */
.g-fcard > .hit { position: absolute; inset: 0; z-index: 0; border: 0; background: transparent; cursor: pointer; }
.g-fcard > .hit:focus-visible { outline: 2px solid var(--brand); outline-offset: -3px; border-radius: 12px; }
.g-fcard > .hd, .g-fcard > .plate { position: relative; z-index: 1; pointer-events: none; }
.g-fcard button, .g-fcard input { pointer-events: auto; }
.g-fcard > .hd { display: flex; align-items: center; gap: 9px; padding: 13px 12px 12px 13px; }
.g-fcard .ico { flex: none; width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; color: var(--brand-deep); background: var(--brand-wash); }
.g-fcard.off .ico { color: var(--ink-4); background: #eef1f5; }
.g-fcard .ttl { flex: 1; min-width: 0; line-height: 1.3; }
.g-fcard .ttl b { display: block; font-size: 14px; font-weight: 700; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.g-fcard .ttl em { display: block; font-style: normal; font-size: 10.5px; font-family: var(--mono); color: var(--ink-4); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.g-fcard .ttl .g-input { width: 100%; height: 25px; padding: 0 7px; font-size: 13px; }
.g-fcard .stx { flex: none; font: 600 10.5px/1 var(--mono); color: var(--ink-4); }
.g-fcard .stx.on { color: var(--ok); }
.g-fcard .mbtn { flex: none; width: 24px; height: 24px; display: grid; place-items: center; border: 1px solid transparent; border-radius: 7px; background: transparent; color: var(--ink-4); cursor: pointer; }
.g-fcard .mbtn:hover, .g-fcard .mbtn.on { border-color: var(--cline); background: #f3f6fa; color: var(--ink); }
/* 数据板:四格等宽、竖线分隔 —— 数字在上、名目在下,一行说完这条流有多大 */
.g-fcard .plate { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--line-soft); background: #fafbfd; }
.g-fcard .plate > span { display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 9px 4px 10px; border-left: 1px solid var(--line-soft); }
.g-fcard .plate > span:first-child { border-left: 0; }
.g-fcard .plate b { font: 700 15px/1 var(--mono); letter-spacing: -.02em; color: var(--ink); }
.g-fcard .plate i { font-style: normal; font-size: 10px; color: var(--ink-4); }
.g-fcard .plate > span.bad b { color: var(--danger); }
/* ⋯ 菜单:压在卡内右上角(卡是 overflow:hidden,菜单只在卡里活动) */
.g-fcard .menu { position: absolute; right: 10px; top: 41px; z-index: 3; width: 138px; display: flex; flex-direction: column; gap: 2px; padding: 5px; border: 1px solid var(--cline); border-radius: 9px; background: #fff; box-shadow: 0 10px 22px -8px rgba(19, 28, 38, .3); }
.g-fcard .menu button { display: flex; align-items: center; gap: 6px; width: 100%; height: 27px; padding: 0 8px; border: 0; border-radius: 6px; background: transparent; font: 500 12px/1 var(--sans); color: var(--ink); text-align: left; cursor: pointer; }
.g-fcard .menu button:hover { background: #f2f5f9; }
.g-fcard .menu button.bad { color: var(--danger); }
.g-fcard .menu button.bad:hover { background: #fdf2f2; }
.g-fcard .menu .q { padding: 3px 8px 5px; font-size: 11px; line-height: 1.5; color: var(--ink-3); }
/* 幽灵卡:新建住在货架里 —— 与真卡同宽同高,一眼看出「这儿还能再添一条」 */
.g-fcard.ghost { flex-direction: row; align-items: center; justify-content: center; gap: 7px; min-height: 102px; border-style: dashed; border-color: #c6d1de; background: transparent; box-shadow: none; color: var(--ink-4); font: 600 12.5px/1 var(--sans); cursor: pointer; }
.g-fcard.ghost:hover { border-color: var(--brand); background: var(--brand-wash); color: var(--brand-deep); box-shadow: none; transform: none; }
/* ============ 规则清单(触发器):整行可下钻 ============ */
.g-rules { display: flex; flex-direction: column; }
.g-rule { display: grid; grid-template-columns: 30px minmax(0, 1fr) 84px 108px 78px 18px; align-items: center; gap: 12px; width: 100%; padding: 13px 16px; border: 0; border-bottom: 1px solid var(--line-soft); background: transparent; font: inherit; text-align: left; cursor: pointer; transition: background var(--t-fast); }
@@ -15,7 +15,6 @@ import {
Card,
Chip,
Clear,
Entry,
Field,
Note,
PageHead,
@@ -39,10 +38,7 @@ type InstallReply = {
};
import { PARA } from '@web-shared/gov/tone.ts';
import type { Nav } from '@web-shared/gov/gov-route.ts';
import {
useGovActivities,
useGovMemory,
} from '@web-shared/gov/gov-api.ts';
import { useGovActivities } from '@web-shared/gov/gov-api.ts';
export type HomeProps = { tab: string; nav: Nav };
@@ -108,9 +104,8 @@ export function LibraryHome({ nav }: HomeProps) {
/* ---------------- ⑥ 工具管理 ---------------- */
export function ActPage({ id, nav }: { id: string; nav: Nav }) {
export function ActPage({ id }: { id: string }) {
const q = useGovActivities();
const memory = useGovMemory(id);
const a = (q.data?.activities ?? []).find((x) => x.activityId === id);
if (a === undefined) return <Note>活动不存在。</Note>;
return (
@@ -208,12 +203,6 @@ export function ActPage({ id, nav }: { id: string; nav: Nav }) {
{a.assetVersions.length === 0 && <Clear>尚无资产版本。</Clear>}
</div>
</Card>
<Entry
ico="layers"
title="活动记忆"
desc={`四层条目 · 当前 ${(memory.data?.entries ?? []).length} 条`}
onClick={() => nav.jump('knowledge', 'layers')}
/>
</>
);
}
+1 -1
View File
@@ -2,4 +2,4 @@
* feature `gov-memory` 唯一出口(编码规范 §9.1)。
* 记忆管理:度量仪表、活动记忆四层
*/
export { CockpitHome, KnowledgeHome, MemPage } from './internal/panel.tsx';
export { CockpitHome } from './internal/panel.tsx';
@@ -1,24 +1,19 @@
/**
* feature `gov-memory` —— 运营中心的两处:**经营驾驶舱**(度量八指标)与**知识库**(活动记忆四层)。
* feature `gov-memory` —— **经营驾驶舱**(运营首页:度量八指标 + 异常面)。
*
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
*
* 原本这两件同挂在「记忆管理」一区,是两种心智硬挤在一起:
* 看指标问的是「今天跑得怎样」,管沉淀问的是「学到了什么、放不放行」。三面拆分后各自成页。
* 「活动记忆」那一页 2026-07-29 撤销(用户定:运营中心删掉数据模块)——
* 记忆的放行口还没成形,摆一个空区不如不摆;真要做时随「数据资产」一并重开。
* 文件名仍叫 gov-memory:度量本就是留痕的投影,改名要动一串 import,等有第二处再说。
*/
import { useState } from 'react';
import { TERMINAL_STATE_LABEL } from '@contracts';
import type { TerminalState } from '@contracts';
import { Ico } from '@web-shared/Ico.tsx';
import { Chip, Clear, Kpi, NList, NRow, Note, PageHead, QueryGate, Todo } from '@web-shared/gov/primitives.tsx';
import { Clear, Kpi, Note, QueryGate, Todo } from '@web-shared/gov/primitives.tsx';
import type { Nav } from '@web-shared/gov/gov-route.ts';
import {
useGovActivities,
useGovMemory,
useGovMetrics,
useGovTasks,
} from '@web-shared/gov/gov-api.ts';
import { useGovMetrics, useGovTasks } from '@web-shared/gov/gov-api.ts';
export type HomeProps = { tab: string; nav: Nav };
@@ -165,76 +160,3 @@ export function CockpitHome({ nav }: HomeProps) {
</QueryGate>
);
}
/* ---------------- 知识库 ---------------- */
/** 活动记忆四层 + 候选增量的放行口 —— 记忆的终点是成为模型,那道门在这里 */
export function KnowledgeHome({ nav }: HomeProps) {
const activities = useGovActivities();
const [picked, setPicked] = useState<string | undefined>(undefined);
const memory = useGovMemory(picked);
return (
<>
<div className="g-picker">
{(activities.data?.activities ?? []).map((a) => (
<button
key={a.activityId}
className={'g-pick' + (picked === a.activityId ? ' on' : '')}
onClick={() => setPicked(a.activityId)}
>
{a.name}
</button>
))}
</div>
<NList>
{(memory.data?.entries ?? []).map((e) => (
<NRow
key={e.id}
tone={e.incrementState === undefined ? 'brand' : 'warn'}
ico="layers"
title={e.statement}
sub={`${e.layer} · ${e.createdAt.slice(0, 16).replace('T', ' ')}`}
cols={[{ k: '证据', v: e.evidenceCount }]}
right={
e.incrementState !== undefined ? (
<Chip tone="warn">候选 · {e.incrementState}</Chip>
) : (
<Chip tone="ok">已成条目</Chip>
)
}
onClick={() => nav.push({ k: 'mem', id: e.id })}
/>
))}
{picked === undefined && <Clear>选一个活动,看它这些年学到了什么。</Clear>}
{picked !== undefined && (memory.data?.entries ?? []).length === 0 && (
<Clear>这个活动还没有记忆条目 —— 任务落终态封卷、蒸馏钩子跑过之后,沉淀会出现在这里。</Clear>
)}
</NList>
</>
);
}
/* ---------------- 记忆条目详情 ---------------- */
export function MemPage({ id }: { id: string }) {
const activities = useGovActivities();
const first = (activities.data?.activities ?? [])[0]?.activityId;
const memory = useGovMemory(first);
const e = (memory.data?.entries ?? []).find((x) => x.id === id);
if (e === undefined) return <Note>记忆条目需从对应活动进入。</Note>;
return (
<PageHead
ico="layers"
kicker={`活动记忆 · ${e.layer}`}
title={e.statement}
chips={
<>
<Chip ico="link">{e.evidenceCount} 条证据</Chip>
{e.incrementState !== undefined && <Chip tone="warn">候选增量 · {e.incrementState}</Chip>}
</>
}
desc="写入唯蒸馏,条目必挂证据链;记忆不载权力——强制力只经候选增量 + 回放 + 人放行进入业务模型。"
/>
);
}
@@ -19,6 +19,7 @@ import type { KeyboardEvent } from 'react';
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
import type { ClosureReport, ProcessPackage, ProcessNode } from '@contracts';
import {
deleteProcessFlowRoute,
deleteProcessVersionRoute,
processPackageSchema,
editProcessVersionRoute,
@@ -110,8 +111,6 @@ function OverviewHome() {
unit="%"
sub="不闭合不得激活"
/>
{/* 网关已废除:分支 = 条件出边,不再是一种要素 */}
<Kpi ico="boxes" label="编排要素" value="四种" sub="活动 · 审批 · 触发器 · 流程包" />
</div>
</QueryGate>
);
@@ -151,6 +150,11 @@ function FlowShelf({ nav }: { nav: Nav }) {
const [nfName, setNfName] = useState('');
const [nfId, setNfId] = useState('');
const [nfError, setNfError] = useState<string | undefined>(undefined);
/** 卡上的 ⋯ 菜单:开在哪张卡上;`drop` = 那张卡的菜单已翻到删除确认 */
const [menu, setMenu] = useState<{ id: string; drop: boolean } | null>(null);
/** 就地改名:改的是卡面显示的**那一版**(名字是版本的属性) */
const [renaming, setRenaming] = useState<string | null>(null);
const [cardErr, setCardErr] = useState<string | undefined>(undefined);
// 标识进 URL 和触发器规则里引用,所以收紧:小写字母开头,只许小写/数字/连字符
const idOk = /^[a-z][a-z0-9-]{1,40}$/.test(nfId);
const idTaken = flows.some((f) => f.id === nfId);
@@ -180,13 +184,41 @@ function FlowShelf({ nav }: { nav: Nav }) {
onError: (e: unknown) => setNfError((e as { message?: string }).message ?? '建流程失败'),
});
/** 卡上改名 —— 落在卡面那一版上(整条流的名字就是它生效版的名字) */
const rename = useMutation({
mutationFn: (input: { id: string; version: number; name: string }) =>
call<{ packages: ProcessPackage[] }>(editProcessVersionRoute, {
params: { id: input.id, version: String(input.version) },
body: { name: input.name },
}),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] });
setRenaming(null);
},
onError: (e: unknown) => setCardErr((e as { message?: string }).message ?? '改名失败'),
});
/** 删整条流 —— 在途单钉着、还有调起规则指着,服务端都会挡回来(409),判词照抄 */
const dropFlow = useMutation({
mutationFn: (id: string) =>
call<{ packages: ProcessPackage[] }>(deleteProcessFlowRoute, { params: { id } }),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] });
setMenu(null);
},
onError: (e: unknown) => {
setCardErr((e as { message?: string }).message ?? '删不掉');
setMenu(null);
},
});
return (
<QueryGate query={q}>
{creating ? (
{creating && (
<Card ico="workflow" title="新建流程" extra="建出来就是 v1 并即刻生效 —— 一张「开始 → 结束」的最小图,进详情再画">
<div className="g-newflow">
<Field k="流程名称" hint="人读的名字,如「统一报价」">
<input className="g-input" value={nfName} placeholder="如:统一报价" onChange={(e) => setNfName(e.target.value)} />
<input className="g-input" autoFocus value={nfName} placeholder="如:统一报价" onChange={(e) => setNfName(e.target.value)} />
</Field>
<Field k="标识" hint="进 URL 与触发器规则里引用 —— 小写字母开头,只许小写 / 数字 / 连字符">
<input
@@ -214,9 +246,8 @@ function FlowShelf({ nav }: { nav: Nav }) {
/>
</div>
</Card>
) : (
<Acts items={[{ label: '新建流程', ico: 'plus', primary: true, onClick: () => setCreating(true) }]} />
)}
{cardErr !== undefined && <Note tone="warn">{cardErr}</Note>}
<div className="g-shelf">
{flows.map((f) => {
@@ -227,50 +258,110 @@ function FlowShelf({ nav }: { nav: Nav }) {
activity: shown.nodes.filter((n) => n.kind === 'activity').length,
approval: shown.nodes.filter((n) => n.kind === 'approval').length,
};
const open = menu?.id === f.id;
return (
<button
key={f.id}
className={'g-tile t-' + (f.active !== undefined ? 'ok' : 'mute')}
onClick={() => nav.push({ k: 'pkg', id: f.id })}
>
<span className="g-tile-h">
<span className="g-tile-i">
<Ico name="workflow" s={16} />
/*
* 卡是个 div,整面盖一枚透明的「进这条流」按钮 —— 卡里还要住 ⋯ 菜单
* 与改名输入框,按钮套按钮是非法 DOM。字面部分不吃点击,直接穿到底下那枚。
*/
<div key={f.id} className={'g-fcard' + (f.active === undefined ? ' off' : '')}>
<button className="hit" aria-label={`打开 ${shown.name}`} onClick={() => nav.push({ k: 'pkg', id: f.id })} />
<div className="hd">
<span className="ico">
<Ico name="workflow" s={15} />
</span>
<span className="g-tile-t">
<b>{shown.name}</b>
<div className="ttl">
{renaming === f.id ? (
<input
className="g-input"
autoFocus
maxLength={40}
defaultValue={shown.name}
onClick={(e) => e.stopPropagation()}
onBlur={() => setRenaming(null)}
onKeyDown={(e) => {
e.stopPropagation();
const val = (e.target as HTMLInputElement).value.trim();
if (e.key === 'Enter' && val !== '') rename.mutate({ id: f.id, version: shown.version, name: val });
if (e.key === 'Escape') setRenaming(null);
}}
/>
) : (
<b>{shown.name}</b>
)}
<em>{f.id}</em>
</div>
<span className={'stx' + (f.active === undefined ? '' : ' on')}>
{f.active === undefined ? '已下线' : `生效 v${f.active.version}`}
</span>
{f.active !== undefined ? (
<St tone="ok" ico="circle-check">
生效 v{f.active.version}
</St>
) : (
<St tone="warn" ico="pause">
已下线
</St>
)}
</span>
<span className="g-tile-m">
<Chip tone="auto" ico="git-commit">
活动 {c.activity}
</Chip>
<Chip tone="warn" ico="shield-check">
审批 {c.approval}
</Chip>
<Chip tone="mute" ico="git-branch">
边 {shown.edges.length}
</Chip>
</span>
<span className="g-tile-f">
<St tone={ec.closed === ec.total ? 'ok' : 'danger'} ico={ec.closed === ec.total ? 'circle-check' : 'alert-circle'}>
边 {ec.closed}/{ec.total}
</St>
<span className="r">{f.versions.length} 个版本</span>
</span>
</button>
<button
className={'mbtn' + (open ? ' on' : '')}
aria-label="更多"
onClick={(e) => {
e.stopPropagation();
setCardErr(undefined);
setMenu(open ? null : { id: f.id, drop: false });
}}
>
<Ico name="more-horizontal" s={14} />
</button>
</div>
{/* 数据板:四格等宽、竖线分隔 —— 一行说完这条流有多大、闭合没有 */}
<div className="plate">
<span>
<b>{c.activity}</b>
<i>活动</i>
</span>
<span>
<b>{c.approval}</b>
<i>审批</i>
</span>
<span className={ec.closed === ec.total ? '' : 'bad'}>
<b>
{ec.closed}/{ec.total}
</b>
<i>边闭合</i>
</span>
<span>
<b>{f.versions.length}</b>
<i>版本</i>
</span>
</div>
{open && (
<div className="menu">
{menu.drop ? (
<>
<span className="q">删掉整条流?{f.versions.length} 个版本一起没</span>
<button onClick={() => setMenu({ id: f.id, drop: false })}>算了</button>
<button className="bad" disabled={dropFlow.isPending} onClick={() => dropFlow.mutate(f.id)}>
删除
</button>
</>
) : (
<>
<button
onClick={() => {
setRenaming(f.id);
setMenu(null);
}}
>
<Ico name="pen-line" s={12} /> 编辑
</button>
<button className="bad" onClick={() => setMenu({ id: f.id, drop: true })}>
<Ico name="trash-2" s={12} /> 删除
</button>
</>
)}
</div>
)}
</div>
);
})}
{/* 幽灵卡:新建就住货架里 —— 与真卡同一尺寸,一眼看出「这儿还能再添一条」 */}
<button className="g-fcard ghost" onClick={() => setCreating(true)}>
<Ico name="plus" s={18} />
新建流程
</button>
</div>
</QueryGate>
);
@@ -79,7 +79,8 @@ describe('legacyGovTarget —— 旧 #/gov 深链一条不死', () => {
it('已删的区落到对应新家', () => {
expect(legacyGovTarget('#/gov/memory/metrics')).toBe('/ops#/ops/cockpit/dash');
expect(legacyGovTarget('#/gov/memory/layers')).toBe('/ops#/ops/knowledge/layers');
// 活动记忆那一区 2026-07-29 整个撤了(运营 · 数据)—— 旧链接落运营首页,不给白屏
expect(legacyGovTarget('#/gov/memory/layers')).toBe('/ops#/ops/cockpit/dash');
});
/*
+2 -13
View File
@@ -17,7 +17,6 @@ export type Route =
| { k: 'act'; id: string }
| { k: 'tool'; id: string }
| { k: 'obj'; id: string }
| { k: 'mem'; id: string }
| { k: 'llm'; id: string }
| { k: 'chan'; id: string };
@@ -25,7 +24,8 @@ export type Route =
* 十一区 —— 分属两个台面(见 `surfaces.ts`):
* 设计中心定义企业(战略 → 方法 → 能力 → 对象),运营中心看企业跑。
* 触发器不再独立成区:定义并入 workflows,启停并入 tasks;
* 记忆一分为二:度量升为 cockpit 首页,活动记忆归 knowledge。
* 记忆只剩度量:升为 cockpit 首页。活动记忆那一区(运营 · 数据)2026-07-29 撤销 ——
* 记忆的放行口未建成形,摆一个空区不如不摆;真要做时随「数据资产」一并重开。
*/
export const GOV_ZONES = [
/*
@@ -49,7 +49,6 @@ export const GOV_ZONES = [
'tasks',
'people',
'ledgers',
'knowledge',
'system',
] as const;
export type GovZone = (typeof GOV_ZONES)[number];
@@ -69,7 +68,6 @@ export const ZONES: ReadonlyArray<{ key: GovZone; label: string; icon: string }>
{ key: 'tasks', label: '任务', icon: 'list-checks' },
{ key: 'people', label: '员工', icon: 'users' },
{ key: 'ledgers', label: '台账', icon: 'table' },
{ key: 'knowledge', label: '数据', icon: 'layers' },
{ key: 'system', label: '系统', icon: 'settings' },
];
@@ -93,12 +91,6 @@ export const ZM: Readonly<Record<GovZone, { title: string; desc: string; accent:
accent: 'linear-gradient(140deg,#245bdb,#16307a)',
desc: '行数据 · 版本 · 引用——域的结构在设计中心改,这里做运营动作。域即页签,随数据域发布生长:设计侧一发布新域,台账里自动多一个域页签。',
},
knowledge: {
title: '数据',
icon: 'layers',
accent: 'linear-gradient(140deg,#f14ba9,#c92d86)',
desc: '运行沉淀的数据资产:活动记忆四层 + 技能资产 + 候选增量的回放与放行。记忆的终点是成为模型——候选增量必经回放与人放行,这里是那道人工放行口。',
},
runs: {
title: '工作流',
icon: 'activity',
@@ -204,7 +196,6 @@ export const TABS: Readonly<Record<GovZone, ReadonlyArray<{ key: string; label:
{ key: 'groups', label: '工作组', ico: 'users' },
],
ledgers: [], // 动态:页签即数据域(短名呈现),由壳层按已发布域生成
knowledge: [{ key: 'layers', label: '活动记忆', ico: 'layers' }],
system: [{ key: 'chan', label: '通道实例', ico: 'mail' }],
};
@@ -232,8 +223,6 @@ export function routeLabel(r: Route): string {
return '工具';
case 'obj':
return '数据对象';
case 'mem':
return '活动记忆';
case 'llm':
return 'LLM 实例';
case 'chan':
+2 -3
View File
@@ -24,10 +24,10 @@ export const SURFACE_META: Readonly<Record<Surface, { path: string; brand: strin
},
};
/** 区归属:设计九区 + 运营七区,恰好铺满 `GOV_ZONES`(由测试守住) */
/** 区归属:设计九区 + 运营六区,恰好铺满 `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', 'knowledge', 'system'],
ops: ['cockpit', 'runs', 'tasks', 'people', 'ledgers', 'system'],
};
/**
@@ -72,7 +72,6 @@ const LEGACY_MOVES: Readonly<Record<string, string>> = {
'testbed/inbound': 'design/testbed/run',
'system/llm': 'design/models/llm',
memory: 'ops/cockpit/dash',
'memory/layers': 'ops/knowledge/layers',
};
/** 运营首页 —— 认不出的旧链接一律落这儿,不给白屏 */
+12
View File
@@ -238,6 +238,18 @@ export const deleteProcessVersionRoute = {
output: z.object({ packages: z.array(processPackageSchema) }),
} as const satisfies RouteContract;
/**
* 删整条流 —— 连同它的**所有版本**。货架上一条流一张卡,删的就是那张卡。
* 两道闸:任一版有在途单即拒(那些单要按它跑完);还有调起规则指着它也拒 ——
* 删了包留着规则,那规则再命中就调不动任何东西,是个哑火的配置。
*/
export const deleteProcessFlowRoute = {
method: 'DELETE',
path: '/api/gov/process/:id',
params: z.object({ id: z.string() }),
output: z.object({ packages: z.array(processPackageSchema) }),
} as const satisfies RouteContract;
/**
* 改版本记录 —— **版本号自己定**(2026-07-29 用户定)、名字随之:改哪版只动哪版。
*
+1
View File
@@ -57,6 +57,7 @@ const GLYPHS: Record<string, React.ReactNode> = {
paperclip: (<path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" />),
"building-2": (<><path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z" /><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2" /><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2" /><path d="M10 6h4" /><path d="M10 10h4" /><path d="M10 14h4" /><path d="M10 18h4" /></>),
"panel-left-open": (<><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M9 3v18" /><path d="m14 9 3 3-3 3" /></>),
"more-horizontal": (<><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></>),
"trash-2": (<><path d="M3 6h18" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /><line x1="10" x2="10" y1="11" y2="17" /><line x1="14" x2="14" y1="11" y2="17" /></>),
calculator: (<><rect width="16" height="20" x="4" y="2" rx="2" /><line x1="8" x2="16" y1="6" y2="6" /><line x1="16" x2="16" y1="14" y2="18" /><path d="M16 10h.01" /><path d="M12 10h.01" /><path d="M8 10h.01" /><path d="M12 14h.01" /><path d="M8 14h.01" /><path d="M12 18h.01" /><path d="M8 18h.01" /></>),
/* —— 治理中心扩充图标 —— */