Compare commits

..
2 Commits
Author SHA1 Message Date
xcsandClaude Opus 5 e1d8d86b16 合并 origin/mvp-foundation:运营中心收口(四区建设中占位 · L2 文案与收口)+ 治理台告警钮暂隐(shanqin 两条)
CI / ci (push) Failing after 10s
主支这边一条:实例时间线第 9 步的结果跟第 8 步走(已放行的单不再永远停在 pending);
远端两条是队友的前端:运营中心四区(驾驶舱 / 工作流 / 台账 / 系统)摆建设中占位、
L2「启动活动」改「创建活动」、沙箱区块隐藏、行内终止撤掉收口 L3、负责人小签隐藏、
文案定名活动列表 / 活动号;治理台顶栏告警钮暂隐(SHOW_ALERTS 开关,计数逻辑保留)。

两边文件零重叠(后端 activity-ops + API 文档 vs 前端 gov-shell / panel + 需求文档),自动合并无冲突。
五道门禁绿:2260 测试通过、23 跳过(无模型密钥 / 无真信箱的惯常项)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
2026-08-28 10:51:08 +08:00
xcsandClaude Opus 5 c1137fb09a fix(ops): 实例时间线第 9 步的结果跟第 8 步走 —— 已放行的单不再永远停在 pending
放行那一步(T_release,人工激活)**没有自己的收步事件** —— 人签字后 `submit` 直接把整单落终态,
计划项本身从没被 finish 过。于是一单已 `released` 的详情里第 9 步永远是 `pending`,
而它前面拟落账那一步早就 `done`:同一张单上两句话互相打脸。
`followRes` 纯函数在服务端交出详情之前把第 9 步的 `res` 与 `at` 跟上第 8 步(2026-08-28 用户定,`at` 也跟);
**用时不编** —— 那一步没有自己的起止痕;两步有一步不在(别的活动步数不同)就什么都不动。
《API文档-活动模块》§ 时间线补一行口径。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
2026-08-28 10:50:16 +08:00
4 changed files with 45 additions and 11 deletions
@@ -2,7 +2,7 @@
import { describe, expect, it } from 'vitest';
import { parseCmmn } from '@modules/loop-engine';
import type { ItemRuntime } from '@modules/loop-engine';
import { stepsOf } from '../activity-ops/instance-steps.ts';
import { followRes, stepsOf } from '../activity-ops/instance-steps.ts';
const XML = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL" id="d">
@@ -84,3 +84,18 @@ describe('stepsOf', () => {
expect(steps[1]?.res).toBe('current');
});
});
describe('followRes:第 9 步跟第 8 步(放行那一步没有自己的收步事件)', () => {
const step = (seq: number, res: 'done' | 'current' | 'pending' | 'fail') => ({ seq, name: `S${seq}`, by: 'agent' as const, byName: 'llm', at: null, durationMs: null, res, note: '' });
it('已放行的单:第 8 步 done → 第 9 步也 done、时刻跟第 8 步;别的字段不动', () => {
const steps = [...Array.from({ length: 7 }, (_, i) => step(i + 1, 'done')), { ...step(8, 'done'), at: '2026-08-27T14:30:26.080Z' }, { ...step(9, 'pending'), by: 'human' as const, byName: '柴浩' }];
const out = followRes(steps, { seq: 9, from: 8 });
expect(out[8]).toEqual({ ...step(9, 'done'), at: '2026-08-27T14:30:26.080Z', by: 'human', byName: '柴浩' });
expect(out.slice(0, 8)).toEqual(steps.slice(0, 8));
});
it('第 8 步 fail / pending 时第 9 步照样跟;步数不够 9 的活动什么都不动', () => {
expect(followRes([step(1, 'done'), ...Array.from({ length: 7 }, (_, i) => step(i + 2, 'pending')), step(9, 'pending')].map((s) => (s.seq === 8 ? { ...s, res: 'fail' as const } : s)), { seq: 9, from: 8 })[8]?.res).toBe('fail');
const seven = Array.from({ length: 7 }, (_, i) => step(i + 1, 'done'));
expect(followRes(seven, { seq: 9, from: 8 })).toEqual(seven);
});
});
@@ -99,3 +99,16 @@ export function stepsOf(f: StepFacts): InstanceStep[] {
};
});
}
/**
* 让某一步的结果**跟随**前面那一步(2026-08-28 用户定:详情接口交给前端之前,第 9 步的 res 跟第 8 步走)。
*
* 为什么会需要:放行那一步(T_release,人工激活)没有自己的收步事件 —— 人签字后 `submit` 直接把整单落终态,
* 计划项本身从没被 finish 过,于是一单已 `released` 的详情里第 9 步永远是 `pending`,而它前面拟落账那一步早就 `done`。
* 改 `res` 与 `at`(2026-08-28 用户定,`at` 也跟);用时不编(那一步没有自己的起止痕)。两步有一步不在(别的活动步数不同)就什么都不动。
*/
export function followRes(steps: readonly InstanceStep[], rule: { readonly seq: number; readonly from: number }): InstanceStep[] {
const source = steps.find((s) => s.seq === rule.from);
if (source === undefined) return [...steps];
return steps.map((s) => (s.seq === rule.seq ? { ...s, res: source.res, at: source.at } : s));
}
+8 -3
View File
@@ -41,7 +41,7 @@ import { fieldSpecsOf, launchParamsOf, validateLaunchParams } from './launch-par
import type { FieldSpec, LaunchIssue, OptionsSource } from './launch-params.ts';
import { instanceStateOf, verdictFactOf } from './instance-state.ts';
import type { StateVerdict } from './instance-state.ts';
import { stepsOf } from './instance-steps.ts';
import { followRes, stepsOf } from './instance-steps.ts';
import { applyInstanceFilters, inputFilesOf, inputSummaryOf, outputSummaryOf, pageOf, statsOf } from './summaries.ts';
import type { InstanceQuery } from './summaries.ts';
import { LEDGER_TEMPLATE, injectHeightScript } from './view-html.ts';
@@ -54,6 +54,8 @@ import type { ActivityOpsStore, TaskRow } from './store.ts';
export const LAUNCH_CLAIM_STALE_MS = 60_000;
/** combo 候选内联上限(文档 §4:本期一律内联;更大的表以后走检索接口) */
export const OPTIONS_CAP = 500;
/** 详情时间线交给前端之前:第 9 步(放行)的结果跟第 8 步(拟落账)走 —— 见 `followRes` 的注释(2026-08-28 用户定) */
export const STEP_FOLLOW_RULE = { seq: 9, from: 8 } as const;
/** 输入 / 产出文件的存取 —— app 里现成的两段(`upload-store.ts` / `asset-archive.ts`),按接口注入好替身 */
export type InstanceFileStore = {
@@ -387,7 +389,8 @@ export function createActivityOps(deps: ActivityOpsDeps): ActivityOps {
deps.files.listArchived(taskId),
]);
const llm = llmOf(manifest);
const steps = stepsOf({
const steps = followRes(
stepsOf({
items: loop?.parsed.items ?? [],
runtimeItems: loop?.runtime.items ?? [],
intents: new Map((loop?.model.steps ?? []).map((s) => [s.item, s.intent])),
@@ -395,7 +398,9 @@ export function createActivityOps(deps: ActivityOpsDeps): ActivityOps {
state: verdict.state,
ownerName: summary.owner.name,
llm,
});
}),
STEP_FOLLOW_RULE,
);
const startedMs = Date.parse(row.startedAt);
const finishedMs = row.finishedAt === null ? undefined : Date.parse(row.finishedAt);
const durationMs = finishedMs !== undefined && Number.isFinite(startedMs) && Number.isFinite(finishedMs) && finishedMs >= startedMs ? finishedMs - startedMs : null;
+1
View File
@@ -196,6 +196,7 @@
| `input.files` | `InstanceFile[]` | 是 | 输入文件元数据(可为空数组) |
| `output.files` | `InstanceFile[]` | 是 | 产出文件元数据;没产出是 `[]`,摆「暂无产出」 |
| `steps` | `InstanceStep[]` | 是 | 时间线 |
| | | | **第 9 步的 `res` 与 `at` 跟第 8 步走**(服务端交出前改好,2026-08-28 定):放行那一步没有自己的收步事件,不这么做已放行的单第 9 步永远 `pending`;用时不编 |
注意:详情里**没有** `input`/`output` 那两句摘要字符串(它们是清单行的字段),`input`/`output` 在详情里是上面的结构体。