Archived
E04 修到零被拒线,harness 补七刀:跨进程租约与广播桥、原子写、引用墙、对话区按序号排
活动侧(E04 2.1.0 → 2.1.9,I01 1.2.2): - 覆盖闭合成为机检:基座新增 distinct 读数,「计划盖住的客户数 = 候选行数」钉进 S3 收步 / S4 哨兵 / 放行 L2;人加的候选客户不再被整单无视 - 「关键人待识别」占位句式全链退役:查无联系人先发数据录入卡收人;契约新增 refersTo, 计划/回执的 contact_code 只许引用真实存在的联系人(安装期核引用目标) - 台账更新确认摆全落库面:客户档案/联系人档案两张落库预览(touched: 数据源, 正式现值 + 草稿叠加),可补改、可加行(自增主键由基座续号);姓名升必填 - 更新明细:update_key 位置键 + 「同一处只留一条」L2;依据写人话不带机器键; 标题旁不摆单号;判据与文案不夹字段名;行标识去掉单标识那一段 - 回执骨架 = 计划行数钉进 S3 收步与 S4 哨兵;重做分支不许删计划凑数 - 阶段刻度对齐文档 7 步;起单样例预填满 基座侧: - 两个后端连同一个库:task-claim 跨进程租约(迁移 0030)+ Postgres LISTEN/NOTIFY 广播桥;对话区按任务内记忆序号排,不按墙上时钟;并卡「刚说过话」按记忆序列判紧邻 - 多行 write 整批先验后写(dryRun),被拒 = 一行都没落 - 决策卡意见与本单草稿字面比对,归属以观察进轨迹(note-anchors) - 括号栈补「补闭括号」两形(少一个 } / 行对象没关直接跟下一行) - 件上加行:空着的自增主键直进续号,不从别的行借号;未落下的行点名回给前端 - 人答复落库那一刻就把 runtime 翻 running(task.wake),两块同帧变脸 - 试跑单可落真库(land_master,迁移 0031,默认开);数据域按活动分组; JSON 列不再是 [object Object];按钮键感 + 在办进度条 + 防抖 - 报价链 A1–A7 从系统卸载(只卸不删源码),种子只装 E04/I01 契约变更(api-report 已刷新):FieldDeclaration.distinct / refersTo, objectDeclaration.file,landMaster,unkeyed,assetFiles,ledgerByActivityRoute。 1700 测 / typecheck / lint / depcruise / api:check 绿。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EEJX8Z7CHejvwmDQyPQuBX
This commit is contained in:
+179
-26
@@ -42,21 +42,45 @@ function rowsOf<T>(result: unknown): T[] {
|
||||
return Array.isArray(rows) ? (rows as T[]) : [];
|
||||
}
|
||||
import type { AppEnv } from '../http-env.ts';
|
||||
import { coerceToColumn, missingReadings } from '../wiring/engines.ts';
|
||||
import { coerceToColumn, missingReadings, rowLabel } from '../wiring/engines.ts';
|
||||
import { orderChat } from '../chat-order.ts';
|
||||
|
||||
|
||||
/** 一张表收上来的样子:`{ 台账列名: 值 }`,一行一条 —— 主键列在里面,但只用来认行 */
|
||||
type EditedRow = Readonly<Record<string, string>>;
|
||||
|
||||
/** 写回的收成:改了几行、新增几行、撤了几行、哪几格归不了型 */
|
||||
type WriteBack = { rows: number; added: number; dropped: string[]; bad: string[] };
|
||||
type WriteBack = {
|
||||
rows: number;
|
||||
added: number;
|
||||
/**
|
||||
* **人新增的是哪几行**(主键,2026-08-24 柴浩实测补):从前只有个数「新增 1 行」——
|
||||
* 模型看不见这一屏,只看得见轨迹;不点名,它就守着自己记忆里的「识别出 1 家」
|
||||
* 把人加的那家从头无视到尾(计划/回执/收口一行都没它)。撤行早就点名了
|
||||
* (「撤掉的是人定的,别再写回去」),加行是同一条道理的另一半。
|
||||
*/
|
||||
addedKeys: string[];
|
||||
dropped: string[];
|
||||
bad: string[];
|
||||
/**
|
||||
* **认不出是哪一行、因而没落下的那几行**(2026-08-24 用户实测:
|
||||
* 「用编辑新增了一个客户,推进到下一步直接就把我手动添加的这一行遗漏掉了」)。
|
||||
*
|
||||
* 从前这里是一句 `continue` —— 行悄悄没了,而接口照旧回 `{ok:true}`:
|
||||
* 人看到「保存成功」,东西却不在。**保存说成功,就得真的都存下了**;
|
||||
* 存不下的必须点名说出来(是哪张表、缺哪几列钥匙),让他补得上。
|
||||
*/
|
||||
unkeyed: Array<{ address: string; keys: string[]; hint: string }>;
|
||||
};
|
||||
|
||||
const nothing = (): WriteBack => ({ rows: 0, added: 0, dropped: [], bad: [] });
|
||||
const nothing = (): WriteBack => ({ rows: 0, added: 0, addedKeys: [], dropped: [], bad: [], unkeyed: [] });
|
||||
const merge = (into: WriteBack, one: WriteBack): void => {
|
||||
into.rows += one.rows;
|
||||
into.added += one.added;
|
||||
into.addedKeys.push(...one.addedKeys);
|
||||
into.dropped.push(...one.dropped);
|
||||
into.bad.push(...one.bad);
|
||||
into.unkeyed.push(...one.unkeyed);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -86,6 +110,8 @@ export function resolveRowKey(input: {
|
||||
cells: Readonly<Record<string, string>>;
|
||||
drafts: ReadonlyArray<{ key: Record<string, unknown> }>;
|
||||
types: Readonly<Record<string, string>>;
|
||||
/** 声明了 `auto` 的主键列 —— 见下面那条「空着的自增列不许从别的行借号」 */
|
||||
autos?: readonly string[];
|
||||
}): Record<string, string> | undefined {
|
||||
const { keys, cells, drafts, types } = input;
|
||||
const key: Record<string, string> = {};
|
||||
@@ -95,7 +121,16 @@ export function resolveRowKey(input: {
|
||||
}
|
||||
if (Object.keys(key).length === keys.length) return key;
|
||||
|
||||
const known = findKnownRow(drafts, keys, key);
|
||||
/*
|
||||
* **空着的自增列不是身份线索,是「给我续个号」**(2026-08-24 确认屏补录联系人逼出):
|
||||
* 人加的新行联系人编码本来就该空着(自增列不问人),可 findKnownRow 拿着只有
|
||||
* customer_code 的半把钥匙,会把这一行错配到该客户**已有的草稿联系人行**上 ——
|
||||
* 编码从那行借来,「新增」当场变「覆盖」:模型刚建的那位被人加的这位顶掉,
|
||||
* 痕还写着「改了 1 行」。声明了 auto 的列,件上摆着却空着 → 一律不许从别的行
|
||||
* 借值,留给续号那条路(writeEditedRows 的 mint)。
|
||||
*/
|
||||
const barred = new Set((input.autos ?? []).filter((k) => k in cells && (cells[k] ?? '').trim() === ''));
|
||||
const known = barred.size > 0 ? undefined : findKnownRow(drafts, keys, key);
|
||||
const isSeq = (column: string): boolean => types[column] === 'integer' || types[column] === 'number';
|
||||
/*
|
||||
* **序号型主键不走「全表一致」这条路**(2026-08-23 浏览器实测逮到的):
|
||||
@@ -105,7 +140,19 @@ export function resolveRowKey(input: {
|
||||
*/
|
||||
for (const k of keys) {
|
||||
if (key[k] !== undefined) continue;
|
||||
if (barred.has(k)) continue;
|
||||
if (known === undefined && isSeq(k)) continue;
|
||||
/*
|
||||
* **件上摆着、他却空着的那一列,不许替他编一个值**(2026-08-24 用户实测的第二面)。
|
||||
*
|
||||
* 「全表一致」这条路是给**件上压根没摆**的主键用的(如案号 `round_ref`:
|
||||
* 一单一号,全表当然一致,基座补它天经地义)。可如果那一列就在件上、人能填、
|
||||
* 他没填 —— 补出来的值是**替他决定这一行是谁**:表里恰好只有一行时,
|
||||
* 新加的那行会拿到老行的客户编码,**当场把老行覆盖掉**(屏上两行,库里一行)。
|
||||
* 序号型主键 2026-08-23 已经因为同一个原因单独绕开过 —— 那是这条规律的一个特例,
|
||||
* 现在按「他填得了吗」统一判:填得了就必须他填,基座只补他碰不到的那几列。
|
||||
*/
|
||||
if (known === undefined && k in cells) continue;
|
||||
const v =
|
||||
known === undefined
|
||||
? (() => {
|
||||
@@ -133,14 +180,24 @@ export function resolveRowKey(input: {
|
||||
async function writeEditedRows(
|
||||
container: Container,
|
||||
taskId: TaskId,
|
||||
declared: { domain: string; object: string; fields: ReadonlyArray<{ name: string; type: string; primaryKey: boolean }> },
|
||||
declared: { domain: string; object: string; caseKey?: string | undefined; fields: ReadonlyArray<{ name: string; type: string; primaryKey: boolean; auto?: boolean | undefined }> },
|
||||
lines: readonly EditedRow[],
|
||||
): Promise<WriteBack> {
|
||||
const { domain, object } = declared;
|
||||
const keys = declared.fields.filter((f) => f.primaryKey).map((f) => f.name);
|
||||
const out = nothing();
|
||||
// 这一单现有的行 —— 三处要用:补主键(件上没摆全的那几列)、认出哪几行真改了、认出哪几行是新增
|
||||
// 本单草稿行 —— 补主键用(件上没摆全的那几列按它认)
|
||||
const drafts = (await container.ledger.taskDrafts(taskId)).filter((d) => d.domain === domain && d.object === object);
|
||||
/*
|
||||
* **「真改了吗」要对着叠加后的行比,不能只对草稿**(2026-08-24 落库预览逼出来的):
|
||||
* 预览回显的是正式现值 + 草稿的叠加 —— 人一格没动按下保存,回传里带着的是回显值;
|
||||
* 只对草稿比的话,草稿里没有的那几列(如 Itelma 既有的客户类型)全算「改了」,
|
||||
* 痕上写「改了 2 行」而人一个字没动(日志撒谎),还平白多出一笔草稿。
|
||||
* 「这一行是不是新增」同理:正式表里有、草稿里没有的行,不是新增。
|
||||
*/
|
||||
const page = await container.ledger
|
||||
.taskView(taskId, { address: { domain, object }, where: {}, orderBy: [], limit: 500, offset: 0 })
|
||||
.catch(() => ({ rows: [] as Array<Record<string, unknown>> }));
|
||||
const types = Object.fromEntries(declared.fields.map((f) => [f.name, f.type]));
|
||||
for (const cells of lines) {
|
||||
/*
|
||||
@@ -150,7 +207,13 @@ async function writeEditedRows(
|
||||
* 凑主键这一段抽成了纯函数 `resolveRowKey`(有测):三类主键三种凑法,
|
||||
* 尤其序号型那一手不补,人在件上加的行会被静默丢掉。
|
||||
*/
|
||||
const key = resolveRowKey({ keys, cells, drafts, types });
|
||||
const key = resolveRowKey({
|
||||
keys,
|
||||
cells,
|
||||
drafts,
|
||||
types,
|
||||
autos: declared.fields.filter((f) => f.primaryKey && f.auto === true).map((f) => f.name),
|
||||
});
|
||||
/*
|
||||
* **人手改的值与 AI 写的值过同一道归型门**(`coerceToColumn`)。
|
||||
*
|
||||
@@ -169,12 +232,73 @@ async function writeEditedRows(
|
||||
if (coerced.kind === 'value') patch[name] = coerced.value;
|
||||
else if (coerced.kind === 'reject') out.bad.push(`${name}=${value}`);
|
||||
}
|
||||
if (key === undefined) continue;
|
||||
const was = drafts.find((d) => keys.every((k) => String(d.key[k] ?? '') === key[k]));
|
||||
if (!rowChanged(was === undefined ? undefined : { ...was.key, ...was.patch }, patch)) continue;
|
||||
if (key === undefined) {
|
||||
const filled = Object.entries(cells).filter(([, v]) => v.trim() !== '');
|
||||
/*
|
||||
* **自增主键空着不是「他没填」,是「不用他填」**(2026-08-24 用户在台账更新确认
|
||||
* 屏上补录联系人逼出来的):声明标了 `auto` 的主键(联系人编码)本就不该问人 ——
|
||||
* 录入卡那条路 2026-08-02 就立了这条规矩,件上「加一行」是同一件事的另一个入口。
|
||||
* 其余主键他都填了、缺的恰好全是 auto 列 → 由基座按声明的续号规则铸
|
||||
* (同组其余主键值-两位序号,序号数正式版 + 本单草稿,与录入卡同一段 autoKeys)。
|
||||
*/
|
||||
const autoCols = keys.filter((k) => declared.fields.find((f) => f.name === k)?.auto === true);
|
||||
const blankKeys = keys.filter((k) => (cells[k] ?? '').trim() === '');
|
||||
const mintable =
|
||||
filled.length > 0 &&
|
||||
autoCols.length > 0 &&
|
||||
blankKeys.length > 0 &&
|
||||
blankKeys.every((k) => autoCols.includes(k)) &&
|
||||
keys.filter((k) => !autoCols.includes(k)).every((k) => (cells[k] ?? '').trim() !== '');
|
||||
if (mintable) {
|
||||
const given = Object.fromEntries(keys.filter((k) => !autoCols.includes(k)).map((k) => [k, (cells[k] ?? '').trim()]));
|
||||
const minted = await autoKeys(container, taskId, declared as LedgerDeclaration, given);
|
||||
const newKey = { ...given, ...minted };
|
||||
if (keys.every((k) => (newKey[k] ?? '') !== '')) {
|
||||
await container.ledger.draftWrite({ taskId, address: { domain, object }, key: newKey, patch });
|
||||
out.added += 1;
|
||||
out.addedKeys.push(rowLabel(newKey, declared.caseKey));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* **认不出这一行,就得说出来** —— 从前这里是一句 `continue`:人在件上加了一行、
|
||||
* 按了保存、接口回 `{ok:true}`,而那一行根本没落下(2026-08-24 用户实测)。
|
||||
* 键凑不齐只有一种情形:主键那几格他没填(件上摆着、标着必填,但空着)。
|
||||
* 空行不算 —— 那是「加一行」点出来还没动的骨架,人自己也知道它是空的。
|
||||
*/
|
||||
if (filled.length > 0) {
|
||||
/*
|
||||
* **只点名他填得了的那几列** —— 件上摆着、却空着的主键格。
|
||||
* 压根不在件上的主键(如案号 `round_ref`,由基座按全表一致补)不许出现在这句话里:
|
||||
* 那是基座的活,写进去等于把人指向一扇他打不开的门(拒回指错方向比不指更糟)。
|
||||
*/
|
||||
// 自增列不点名 —— 人填不了它(拒回指向打不开的门比不指更糟)
|
||||
const missing = keys.filter(
|
||||
(k) => k in cells && (cells[k] ?? '').trim() === '' && declared.fields.find((f) => f.name === k)?.auto !== true,
|
||||
);
|
||||
// 人读名优先(件上摆的是「客户编号」,不是 customer_code)—— 取不到就退列名
|
||||
const label = (name: string): string => {
|
||||
const found = declared.fields.find((f) => f.name === name) as { label?: string } | undefined;
|
||||
return found?.label ?? name;
|
||||
};
|
||||
out.unkeyed.push({
|
||||
address: `${domain}.${object}`,
|
||||
keys: missing,
|
||||
hint:
|
||||
missing.length > 0
|
||||
? `这一行没能落下 —— ${missing.map(label).join('、')}是它的钥匙,填上才存得住`
|
||||
: '这一行没能落下 —— 认不出它是哪一行(主键这几格件上没摆出来),请报给开发',
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const was = page.rows.find((r) => keys.every((k) => String((r as Record<string, unknown>)[k] ?? '') === key[k]));
|
||||
if (!rowChanged(was as Record<string, unknown> | undefined, patch)) continue;
|
||||
await container.ledger.draftWrite({ taskId, address: { domain, object }, key, patch });
|
||||
if (was === undefined) out.added += 1;
|
||||
else out.rows += 1;
|
||||
if (was === undefined) {
|
||||
out.added += 1;
|
||||
out.addedKeys.push(rowLabel(key, declared.caseKey));
|
||||
} else out.rows += 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -192,7 +316,7 @@ async function writeEditedRows(
|
||||
async function dropEditedRows(
|
||||
container: Container,
|
||||
taskId: TaskId,
|
||||
declared: { domain: string; object: string; fields: ReadonlyArray<{ name: string; primaryKey: boolean }> },
|
||||
declared: { domain: string; object: string; caseKey?: string | undefined; fields: ReadonlyArray<{ name: string; primaryKey: boolean }> },
|
||||
lines: readonly EditedRow[],
|
||||
): Promise<WriteBack> {
|
||||
const { domain, object } = declared;
|
||||
@@ -208,7 +332,7 @@ async function dropEditedRows(
|
||||
const one = hit.length === 1 ? hit[0] : undefined;
|
||||
if (one === undefined) continue;
|
||||
const gone = await container.ledger.draftDrop({ taskId, address: { domain, object }, key: one.key });
|
||||
if (gone) out.dropped.push(Object.values(one.key).map(String).join('/'));
|
||||
if (gone) out.dropped.push(rowLabel(one.key, declared.caseKey));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -483,23 +607,35 @@ export function parseRowKey(raw: string, keys: readonly string[]): Record<string
|
||||
*/
|
||||
async function noteEdits(container: Container, taskId: TaskId, got: WriteBack): Promise<number> {
|
||||
const touched = got.rows + got.added + got.dropped.length;
|
||||
if (touched === 0 && got.bad.length === 0) return touched;
|
||||
// 一行都没落下也要留痕 —— 那正是最该说出来的一次(从前这里把 unkeyed 一并吞了)
|
||||
if (touched === 0 && got.bad.length === 0 && got.unkeyed.length === 0) return touched;
|
||||
const parts: string[] = [];
|
||||
if (got.rows > 0) parts.push(`改了 ${got.rows} 行`);
|
||||
if (got.added > 0) parts.push(`新增 ${got.added} 行`);
|
||||
/*
|
||||
* **加的行要点名**(2026-08-24 柴浩实测):只写「新增 1 行」,模型守着轨迹里
|
||||
* 「识别出 1 家」把人加的那家从计划到收口全程无视 —— 这条痕是它下一拍的行动指南,
|
||||
* 得说清加的是谁、且与既有的行同等作数。
|
||||
*/
|
||||
if (got.added > 0) parts.push(`新增 ${got.added} 行(${got.addedKeys.slice(0, 5).join('、')})`);
|
||||
if (got.dropped.length > 0) parts.push(`撤掉 ${got.dropped.length} 行(${got.dropped.slice(0, 5).join('、')})`);
|
||||
if (got.bad.length > 0) parts.push(`${got.bad.length} 格填的值与列型对不上,已跳过(${got.bad.slice(0, 3).join('、')})`);
|
||||
// 没落下的行要在日志里点名 —— 下一拍模型看的是这条痕,人回看也是它
|
||||
if (got.unkeyed.length > 0) {
|
||||
parts.push(`${got.unkeyed.length} 行没落下(${got.unkeyed.map((u) => u.hint).slice(0, 2).join(';')})`);
|
||||
}
|
||||
await container.memory.append({
|
||||
taskId,
|
||||
kind: 'human-input',
|
||||
title:
|
||||
`就地修订成果:${parts.join(';')}` +
|
||||
(got.dropped.length === 0 ? '' : ' —— 撤掉的是人定的,别再写回去;条目数跟着改对'),
|
||||
(got.dropped.length === 0 ? '' : ' —— 撤掉的是人定的,别再写回去;条目数跟着改对') +
|
||||
(got.added === 0 ? '' : ' —— 新增的行是人定的,与既有的行同等作数:后续每一步按台账现读的全表办,别按你先前识别的行数办'),
|
||||
detail: {
|
||||
rows: got.rows,
|
||||
...(got.added === 0 ? {} : { added: got.added }),
|
||||
...(got.added === 0 ? {} : { added: got.added, addedKeys: got.addedKeys }),
|
||||
...(got.dropped.length === 0 ? {} : { dropped: got.dropped }),
|
||||
...(got.bad.length === 0 ? {} : { rejected: got.bad }),
|
||||
...(got.unkeyed.length === 0 ? {} : { unkeyed: got.unkeyed }),
|
||||
},
|
||||
// 版本戳:这条不由活动产出,与引擎那几条同一口径
|
||||
stamp: { contractVersion: '1', businessModelVersion: '1', skillVersions: [] },
|
||||
@@ -529,9 +665,9 @@ async function applyViewTableEdits(
|
||||
tables: Readonly<Record<string, readonly EditedRow[]>>;
|
||||
drops?: Readonly<Record<string, readonly EditedRow[]>> | undefined;
|
||||
},
|
||||
): Promise<number> {
|
||||
): Promise<{ rows: number; unkeyed: WriteBack['unkeyed'] }> {
|
||||
const declarations = await declarationsOf(container, taskId);
|
||||
if (declarations === undefined) return 0;
|
||||
if (declarations === undefined) return { rows: 0, unkeyed: [] };
|
||||
const got = nothing();
|
||||
/*
|
||||
* **先撤后写** —— 顺序有意义:人可能把一行删掉、又在同一次保存里加回一行同主键的。
|
||||
@@ -543,13 +679,15 @@ async function applyViewTableEdits(
|
||||
for (const { declared, lines } of writesOf(declarations, payload.tables)) {
|
||||
merge(got, await writeEditedRows(container, taskId, declared, lines));
|
||||
}
|
||||
return noteEdits(container, taskId, got);
|
||||
return { rows: await noteEdits(container, taskId, got), unkeyed: got.unkeyed };
|
||||
}
|
||||
|
||||
/** 台账声明的最小形状 —— 这一段只用到这几样 */
|
||||
type LedgerDeclaration = {
|
||||
domain: string;
|
||||
object: string;
|
||||
/** 单标识列 —— 摆给人看的行标识里去掉它(rowLabel) */
|
||||
caseKey?: string | undefined;
|
||||
fields: ReadonlyArray<{
|
||||
name: string;
|
||||
type: string;
|
||||
@@ -990,8 +1128,8 @@ export function registerDeskRoutes(app: Hono<AppEnv>, container: Container): voi
|
||||
}
|
||||
}
|
||||
|
||||
// 对话流 = 时间线 + 对话卡族,按时间全序 —— 与任务记忆同源、不同筛选
|
||||
const merged: ChatMessage[] = [...chat, ...cards].sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : 0));
|
||||
// 对话流 = 时间线 + 对话卡族,**按任务内序号排**(时钟跨机器会偏,序号不会;见 chat-order.ts)
|
||||
const merged: ChatMessage[] = orderChat([...chat, ...cards], await container.memory.history(taskId));
|
||||
|
||||
// 视图区:活动推的 HTML 优先(它是活动按局面组织的完整一屏);
|
||||
// 尚无推送时给一份进度概览,不留空屏
|
||||
@@ -1191,10 +1329,16 @@ export function registerDeskRoutes(app: Hono<AppEnv>, container: Container): voi
|
||||
const body: ApiError = { code: 'conflict', message: '这一单已收尾,改不动了' };
|
||||
return c.json(body, 409);
|
||||
}
|
||||
const rows = await applyViewTableEdits(container, taskId, parsed.data);
|
||||
const saved = await applyViewTableEdits(container, taskId, parsed.data);
|
||||
// 数据变了,件跟着变 —— 算出来的那些(类徽 / 分布条 / 去向)由这一次重渲重新求值
|
||||
if (rows > 0) await container.views.repush(taskId);
|
||||
return c.json({ ok: true, rows });
|
||||
if (saved.rows > 0) await container.views.repush(taskId);
|
||||
/*
|
||||
* **有行没落下就如实说** —— 保存说成功、东西却不在,是这一屏最伤人的一种谎
|
||||
* (2026-08-24 用户实测:件上加的那个客户推到下一步就不见了)。
|
||||
* 仍回 200:别的格子是真落下了,不该因为一行没钥匙把整次保存判死;
|
||||
* 但把没落下的那几行摆回去,前端据此红字点名,人补上主键再存一次。
|
||||
*/
|
||||
return c.json({ ok: true, rows: saved.rows, ...(saved.unkeyed.length === 0 ? {} : { unkeyed: saved.unkeyed }) });
|
||||
});
|
||||
|
||||
/** 回传:与后端回传校验**同 schema** —— 前端提交校验用的也是它 */
|
||||
@@ -1267,6 +1411,14 @@ export function registerDeskRoutes(app: Hono<AppEnv>, container: Container): voi
|
||||
* 仍守铁律②:写的是草稿层,放行那一刻才并进正式版。
|
||||
*/
|
||||
await applyBoundTableEdits(container, outcome.taskId, outcome.order, parsed.data);
|
||||
/*
|
||||
* **返回之前就把这单标成「在办事」**(2026-08-25 用户:「右边聊天框先有反应,中间的
|
||||
* html 过一会儿才变成 AI 正在推进中,看上去很不流畅」)。
|
||||
* 从前 runtime 要等派发器投递、推进器抢到租约、开跑前那一刻才翻成 running ——
|
||||
* 前端 onSuccess 那一次重取抓到的还是 awaiting-event:对话区(卡状态已落)
|
||||
* 先变,工作区(读 runtime)要等下一次推送。两块该读同一份事实。
|
||||
*/
|
||||
await container.task.wake(outcome.taskId).catch(() => false);
|
||||
// 人答完了就接着跑,别让他对着不动的屏幕等两轮轮询(见 workers/pulse.ts)
|
||||
void pulse();
|
||||
return c.json({ ok: true, producesCompletion: outcome.producesCompletion });
|
||||
@@ -1388,6 +1540,7 @@ export function registerDeskRoutes(app: Hono<AppEnv>, container: Container): voi
|
||||
* 眼睛正盯着屏幕。等两轮轮询(实测 6.8 秒)那段时间对话区一动不动,
|
||||
* 人以为没点上,会再点一次。见 `workers/pulse.ts`。
|
||||
*/
|
||||
await container.task.wake(cardTaskId).catch(() => false); // 与工单回传同一条理:返回前先标「在办」
|
||||
void pulse();
|
||||
return c.json({ ok: true });
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user