Archived
人手改的那一手真正落得下去;界面不撒谎那几处补齐
手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的: 件上就地改 · 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名, 于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 —— 而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。 · **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。 删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)—— 「没回传就是删了」会把一次不完整的回传变成一次删表。 · **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。 · 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。 判据与门 · 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来, 人填好的目标价它照样判「客户未提供」的毛病由此消掉。 · 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。 · 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开, 于是一单没打标就进了放行门。三个数全由基座现数。 · 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。 **指向两扇打不开的门,比不指还糟。** 界面不撒谎 · 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。 · 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、 往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。 · 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。 · 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。 · 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。 A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。 五道门禁全绿(971 passed)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
This commit is contained in:
@@ -9,10 +9,12 @@ import { Pool } from 'pg';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { evaluateCondition } from '@sfeel';
|
||||
import type { Db } from '@shared/db.ts';
|
||||
import type { TaskId } from '@contracts';
|
||||
|
||||
import type { EventService, TriggerMatcher } from '@modules/event';
|
||||
import { createEventService, createEventStore } from '@modules/event';
|
||||
import type { LedgerService } from '@modules/ledger';
|
||||
import type { LoopEngineService } from '@modules/loop-engine';
|
||||
import { createDraftStore, createLedgerService, createLedgerStore, DRAFT_DDL } from '@modules/ledger';
|
||||
import type { OrgService, InFlightWorkProbe } from '@modules/org';
|
||||
import {
|
||||
@@ -39,6 +41,7 @@ import { createActivityMemoryStore, createMemoryService, createTaskMemoryStore }
|
||||
import type { ToolboxService, ToolHandler, ToolInvoker } from '@modules/toolbox';
|
||||
import {
|
||||
createSchemaValidator,
|
||||
createToolHandoffStore,
|
||||
createToolInvoker,
|
||||
createToolRegistry,
|
||||
createToolReplayStore,
|
||||
@@ -103,6 +106,12 @@ export type Container = {
|
||||
readonly registry: ActivityRegistryService;
|
||||
readonly release: ReleaseService;
|
||||
readonly task: TaskService;
|
||||
/** 案例引擎 —— 工作台按它算阶段进度;推进照旧走 task,不从这里走 */
|
||||
readonly loop: LoopEngineService;
|
||||
/** 视图闸 —— 人在件上改完数据,按同一张模版用新数据重渲一帧(`views.repush`) */
|
||||
readonly views: { repush(taskId: TaskId): Promise<boolean> };
|
||||
/** 此刻这一单的判据 —— 按当下数据现算(发卡那一刻算死的话,人补一格判据不会动) */
|
||||
readonly checks: { of(taskId: TaskId): Promise<ReadonlyArray<{ key: string; label: string; level: string; pass: boolean; settled?: boolean; rationale?: string }>> };
|
||||
readonly process: ProcessService;
|
||||
readonly trigger: TriggerService;
|
||||
readonly minute: MinuteService;
|
||||
@@ -161,6 +170,14 @@ const sfeelEvaluator = (expression: string, scope: Readonly<Record<string, unkno
|
||||
export const firstPartyToolHandlers = new Map<string, ToolHandler>(FIRST_PARTY_TOOLS);
|
||||
|
||||
export async function createContainer(config: AppConfig): Promise<Container> {
|
||||
/*
|
||||
* 广播器**先建** —— 装配期就要往它上面接线。
|
||||
*
|
||||
* 它从前是在 `return` 里现造的,于是装配期谁也拿不到它:
|
||||
* 对话流推送、视图推送、发消息这三条**全都没有广播**,
|
||||
* 前端只好靠人点刷新(用户实测报的:任务列表 / 消息气泡 / 工作区 / 对话区都不自己动)。
|
||||
*/
|
||||
const broadcaster = createBroadcaster();
|
||||
const pool = new Pool({ connectionString: config.databaseUrl, max: 10 });
|
||||
/*
|
||||
* 空闲连接被服务端掐断(重启、`pg_terminate_backend`、网络抖动)会以 'error' 事件抛到池上;
|
||||
@@ -233,6 +250,25 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
||||
events: { publish: async (envelope) => void (await event.publish(envelope)) },
|
||||
clock: systemClock,
|
||||
ids,
|
||||
/*
|
||||
* 试跑判定 —— 设计台有**两条**试跑路径,认漏一条就等于没挡:
|
||||
* 单活动试跑:起的是不挂工作流的单,标 `task.origin = 'testbed'`
|
||||
* 流程包试跑:节点任务挂在工作流上,标在**流**身上 `workflow.source->>'ref'`
|
||||
* 一条 SQL 两条都认。认不出就当生产单(宁可少挡也不误挡:误挡会让真单丢主数据)。
|
||||
*/
|
||||
simulation: {
|
||||
isSimulated: async (taskId) => {
|
||||
const res = await pool.query<{ simulated: boolean }>(
|
||||
`select exists (
|
||||
select 1 from sys.task t
|
||||
left join sys.workflow w on w.id = t.workflow_id
|
||||
where t.id = $1 and (t.origin = 'testbed' or w.source->>'ref' = 'testbed')
|
||||
) as simulated`,
|
||||
[taskId],
|
||||
);
|
||||
return res.rows[0]?.simulated === true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const instances = createLlmInstanceRepository(db);
|
||||
@@ -278,6 +314,8 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
||||
secrets,
|
||||
clock: systemClock,
|
||||
});
|
||||
/** 工具转人台(§6.2):系统里没有这件工具时,请人代办 —— 人答回的值就是这次调用的出参 */
|
||||
const toolHandoff = createToolHandoffStore(db);
|
||||
const toolInvoker = createToolInvoker({
|
||||
registry: toolRegistry,
|
||||
runtime: toolRuntime,
|
||||
@@ -334,9 +372,41 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
||||
ids,
|
||||
});
|
||||
|
||||
/*
|
||||
* **对话流与视图推送接进广播** —— `interaction.subscribe` 从前一个订阅者都没有:
|
||||
* AI 说的每一句、推的每一帧画面都广播给了空气,前端只好靠人点刷新。
|
||||
*
|
||||
* 这里只做转接,不带业务载荷:推送说「这个任务动了」,画面照旧由服务端投影重建
|
||||
* (§9.2 前端零过程状态)。收件人是谁不在信封里,所以按任务广播 ——
|
||||
* 前端按任务键失效,负责人那一屏自然重取。
|
||||
*/
|
||||
interaction.onPush((envelope) => {
|
||||
// invalidate 信封自己说目标(task / workflow / desk);其余信封都以任务为轴
|
||||
if (envelope.kind === 'invalidate') {
|
||||
broadcaster.publish({
|
||||
kind: envelope.target === 'workflow' ? 'workflow' : 'task',
|
||||
id: envelope.id,
|
||||
reason: 'invalidate',
|
||||
});
|
||||
return;
|
||||
}
|
||||
broadcaster.publish({ kind: 'task', id: envelope.taskId, reason: envelope.kind });
|
||||
});
|
||||
|
||||
const humanTool = createHumanToolService({
|
||||
cards: createCardStore(db),
|
||||
outbox: { sendElicitation: (input) => message.sendElicitation(input) },
|
||||
outbox: {
|
||||
/*
|
||||
* 跨人卡以系统消息送达对方 —— 送到了就**广播给那个人**:
|
||||
* 他的消息气泡该立刻多一条,而不是等他自己点刷新。
|
||||
* 发消息不经事件投递(那条路才有广播),所以这里得自己说一声。
|
||||
*/
|
||||
sendElicitation: async (input) => {
|
||||
const sent = await message.sendElicitation(input);
|
||||
broadcaster.publish({ kind: 'staff', id: input.staffId, reason: 'elicitation' });
|
||||
return sent;
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
append: async (input) =>
|
||||
void (await memory.append({
|
||||
@@ -346,6 +416,17 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
||||
},
|
||||
events: {
|
||||
publishHumanEvent: async (input) => {
|
||||
/*
|
||||
* 转人代办的回传**就是工具出参**:先落转人台,再抛事件。
|
||||
*
|
||||
* 顺序不能反 —— 事件会唤起下一拍,那一拍 invoke 必须立刻取到答案;
|
||||
* 先抛后写就会空跑一拍(拍里还是「等人」,而人其实已经答了)。
|
||||
* 卡不是转人卡时 `answer` 查无此行,静默无事发生。
|
||||
*/
|
||||
const values = input.payload['values'];
|
||||
if (typeof values === 'object' && values !== null && !Array.isArray(values)) {
|
||||
await toolHandoff.answer(input.cardId, values as Record<string, unknown>);
|
||||
}
|
||||
await event.publish({
|
||||
type: 'human',
|
||||
correlation: {
|
||||
@@ -461,7 +542,19 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
||||
deliverable: input.deliverable as never,
|
||||
criteria: [...input.criteria],
|
||||
actions: input.actions.map((a) => ({ ...a })),
|
||||
fields: [],
|
||||
/*
|
||||
* 可修订字段照传 —— 从前这里写死 `fields: []`,是这条链上的最后一处丢值:
|
||||
* 契约声明了、submit 传了、release 转了,到这儿被一句空数组抹平,
|
||||
* 验收报告于是只能看不能改(第 12 章③)。
|
||||
*/
|
||||
fields: (input.fields ?? []).map((f) => ({ ...f })) as never,
|
||||
/*
|
||||
* 正文块照传 —— 与 `fields` 同一个坑:这个显式对象是这条链的最后一关,
|
||||
* 少写一行,上游传了一路的东西就在这里静默落地(卡面 blocks 恒空,踩过两次)。
|
||||
*/
|
||||
...(input.blocks === undefined || input.blocks.length === 0
|
||||
? {}
|
||||
: { blocks: input.blocks.map((b) => ({ ...b })) as never }),
|
||||
},
|
||||
);
|
||||
return { id: issued.order.id };
|
||||
@@ -537,6 +630,7 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
||||
memory,
|
||||
toolbox,
|
||||
toolInvoker,
|
||||
toolHandoff,
|
||||
humanTool,
|
||||
interaction,
|
||||
message,
|
||||
@@ -581,7 +675,7 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
||||
|
||||
return {
|
||||
config,
|
||||
broadcaster: createBroadcaster(),
|
||||
broadcaster,
|
||||
pool,
|
||||
db,
|
||||
org,
|
||||
@@ -599,6 +693,10 @@ export async function createContainer(config: AppConfig): Promise<Container> {
|
||||
registry,
|
||||
release,
|
||||
task: engines.task,
|
||||
loop: engines.loop,
|
||||
/** 件是投影 —— 人在件上改完数据,得能按同一张模版用新数据重渲一帧(见 `views.repush`) */
|
||||
views: engines.views,
|
||||
checks: engines.checks,
|
||||
process: engines.process,
|
||||
trigger: engines.trigger,
|
||||
minute: deskElements.minute,
|
||||
|
||||
Reference in New Issue
Block a user