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:
@@ -15,13 +15,29 @@
|
||||
*
|
||||
* DOM 与类名逐字照搬原型:皮肤是原型的原样,自造类名等于没样式。
|
||||
*/
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createContext, useContext, useEffect, useRef, useState } from 'react';
|
||||
import type { FormField, AiTag, ChatCardReply, ChatMessage, WorkOrder, WorkOrderReply } from '@contracts';
|
||||
import { EVENT_TYPE_LABEL, isRejection } from '@contracts';
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import { Btn } from '@web-shared/Btn.tsx';
|
||||
import { ChatAvatar } from '@web-shared/ChatAvatar.tsx';
|
||||
import { atClock } from '@web-shared/at.ts';
|
||||
|
||||
/**
|
||||
* 「这一下交上去了,回音还在路上」—— 卡族共用一个在办标记。
|
||||
*
|
||||
* 卡上的钮全都要走一趟服务端(交工单 / 裁决 / 选一支 / 答一张卡),而这一趟里
|
||||
* 屏上从前**什么都不变**:人按下「提交」之后只能干等,常常再按一次。
|
||||
* 逐层往下传 prop 要穿过 ChatStream → Msg → 各卡三四层,卡族又都在这一份文件里 ——
|
||||
* 用一个上下文最省事,也最不容易漏掉某一张卡(漏掉的那张就是下一次「点了没反应」)。
|
||||
*/
|
||||
const ReplyBusyContext = createContext(false);
|
||||
|
||||
/** 卡上的钮据此说「在办事」;`ChatStream` 从任务面递进来 */
|
||||
function useReplyBusy(): boolean {
|
||||
return useContext(ReplyBusyContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型章:结论 = auto,指导 = exe,判据 = acc(原型 TAG_UI 口径)。
|
||||
*
|
||||
@@ -81,6 +97,8 @@ export type TaskChatProps = {
|
||||
* 状态来自任务投影的 runtime,不是前端猜的。
|
||||
*/
|
||||
readonly working?: boolean;
|
||||
/** 卡上刚交出去的那一笔还在路上 —— 卡上的钮据此说「在办事」 */
|
||||
readonly replying?: boolean;
|
||||
onSay(text: string): void;
|
||||
onReplyCard(reply: ChatCardReply): void;
|
||||
onReplyOrder(reply: WorkOrderReply): void;
|
||||
@@ -98,6 +116,7 @@ export function ChatStream({
|
||||
orders,
|
||||
thinking,
|
||||
working = false,
|
||||
replying = false,
|
||||
onSay,
|
||||
onReplyCard,
|
||||
onReplyOrder,
|
||||
@@ -125,6 +144,7 @@ export function ChatStream({
|
||||
};
|
||||
|
||||
return (
|
||||
<ReplyBusyContext.Provider value={replying}>
|
||||
<div className="jc-tch">
|
||||
<header className="jc-tch-head">
|
||||
<span className="jc-tch-mark">
|
||||
@@ -186,12 +206,13 @@ export function ChatStream({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button className="copilot-send" disabled={text.trim() === '' || blocked} onClick={send}>
|
||||
<Btn className="copilot-send" disabled={text.trim() === '' || blocked} busy={thinking} onClick={send}>
|
||||
<Ico name="send" s={14} /> 发送
|
||||
</button>
|
||||
</Btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ReplyBusyContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -274,6 +295,7 @@ function Msg({
|
||||
orderValues?: Readonly<Record<string, string>> | undefined;
|
||||
onField: (key: string, value: string) => void;
|
||||
}) {
|
||||
const busy = useReplyBusy();
|
||||
// 人的消息:右侧品牌气泡(纯文本)
|
||||
if (m.k === 'human') {
|
||||
return (
|
||||
@@ -495,9 +517,10 @@ function Msg({
|
||||
{m.chosen === undefined ? (
|
||||
<div className="jc-branches">
|
||||
{m.options.map((o, i) => (
|
||||
<button
|
||||
<Btn
|
||||
key={o.value}
|
||||
className={'jc-branch' + (o.recommended === true ? ' rec' : '')}
|
||||
busy={busy}
|
||||
onClick={() => onReplyCard({ k: 'decision', cardId: m.id, value: o.value })}
|
||||
>
|
||||
<span className="jc-branch-n jc-mono">{String.fromCharCode(65 + i)}</span>
|
||||
@@ -514,7 +537,7 @@ function Msg({
|
||||
)}
|
||||
</span>
|
||||
<Ico name="chevron-right" s={14} />
|
||||
</button>
|
||||
</Btn>
|
||||
))}
|
||||
{m.allowCustom && (
|
||||
<CustomOption
|
||||
@@ -767,6 +790,7 @@ function ExecForm({
|
||||
orderValues?: Readonly<Record<string, string>> | undefined;
|
||||
onField: (key: string, value: string) => void;
|
||||
}) {
|
||||
const busy = useReplyBusy();
|
||||
const val = (f: FormField) => orderValues?.[f.key] ?? f.ai ?? '';
|
||||
/*
|
||||
* **必填还空着就按不下提交**(2026-08-02 用户定)——两处必填,一个门:
|
||||
@@ -842,9 +866,10 @@ function ExecForm({
|
||||
</div>
|
||||
)}
|
||||
<div className="jc-fc-ft">
|
||||
<button
|
||||
<Btn
|
||||
className="jc-btn jc-fc-go"
|
||||
disabled={!ok}
|
||||
busy={busy}
|
||||
onClick={() =>
|
||||
onReplyOrder({
|
||||
mode: 'execution',
|
||||
@@ -854,7 +879,7 @@ function ExecForm({
|
||||
}
|
||||
>
|
||||
<Ico name="check" s={13} /> {order.submitLabel ?? '提交'}
|
||||
</button>
|
||||
</Btn>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -880,6 +905,7 @@ function AcceptForm({
|
||||
*/
|
||||
orderValues?: Readonly<Record<string, string>> | undefined;
|
||||
}) {
|
||||
const busy = useReplyBusy();
|
||||
const [why, setWhy] = useState('');
|
||||
/** 选择组:恰选一口,选口即裁分支 —— 没选之前「通过」不亮 */
|
||||
const [port, setPort] = useState<string | undefined>(undefined);
|
||||
@@ -971,9 +997,10 @@ function AcceptForm({
|
||||
/>
|
||||
</div>
|
||||
<div className="jc-fc-ft">
|
||||
<button
|
||||
<Btn
|
||||
className="jc-btn jc-btn-bad"
|
||||
disabled={why.trim() === ''}
|
||||
busy={busy}
|
||||
onClick={() =>
|
||||
onReplyOrder({
|
||||
mode: 'acceptance',
|
||||
@@ -987,11 +1014,12 @@ function AcceptForm({
|
||||
}
|
||||
>
|
||||
重做
|
||||
</button>
|
||||
</Btn>
|
||||
<span className="jc-fc-sp" />
|
||||
<button
|
||||
<Btn
|
||||
className="jc-btn jc-btn-ok"
|
||||
disabled={needChoice && port === undefined}
|
||||
busy={busy}
|
||||
title={needChoice && port === undefined ? `先在上面选一个「${choice.group}」` : undefined}
|
||||
onClick={() =>
|
||||
onReplyOrder({
|
||||
@@ -1021,7 +1049,7 @@ function AcceptForm({
|
||||
}
|
||||
>
|
||||
<Ico name="circle-check" s={14} /> {order.passLabel ?? '确认通过'}
|
||||
</button>
|
||||
</Btn>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1029,6 +1057,7 @@ function AcceptForm({
|
||||
|
||||
/* 决策的收尾项:自定义口径 */
|
||||
function CustomOption({ n, onPick }: { n: string; onPick: (txt: string) => void }) {
|
||||
const busy = useReplyBusy();
|
||||
const [txt, setTxt] = useState('');
|
||||
return (
|
||||
<div className="jc-branch jc-branch-custom">
|
||||
@@ -1044,9 +1073,9 @@ function CustomOption({ n, onPick }: { n: string; onPick: (txt: string) => void
|
||||
if (e.key === 'Enter' && txt.trim() !== '') onPick(txt.trim());
|
||||
}}
|
||||
/>
|
||||
<button className="jc-btn" disabled={txt.trim() === ''} onClick={() => onPick(txt.trim())}>
|
||||
<Btn className="jc-btn" disabled={txt.trim() === ''} busy={busy} onClick={() => onPick(txt.trim())}>
|
||||
就按这个
|
||||
</button>
|
||||
</Btn>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -1068,6 +1097,7 @@ function FillForm({
|
||||
* 人核对一眼、补上它填不出的那个即可。
|
||||
* 用惰性初值而不是 `useEffect` 回填:回填会在人已经开始打字之后把他敲的字冲掉。
|
||||
*/
|
||||
const busy = useReplyBusy();
|
||||
const [vals, setVals] = useState<Record<string, string>>(() =>
|
||||
Object.fromEntries(fields.filter((f) => (f.value ?? '') !== '').map((f) => [f.key, f.value ?? ''])),
|
||||
);
|
||||
@@ -1093,9 +1123,9 @@ function FillForm({
|
||||
<button type="button" className="jc-cant-link" onClick={() => setCant(false)}>
|
||||
← 还是填吧
|
||||
</button>
|
||||
<button className="jc-btn" disabled={reason.trim() === ''} onClick={() => onCant(reason.trim())}>
|
||||
<Btn className="jc-btn" disabled={reason.trim() === ''} busy={busy} onClick={() => onCant(reason.trim())}>
|
||||
<Ico name="send" s={13} /> 让 AI 调整需求
|
||||
</button>
|
||||
</Btn>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1120,9 +1150,9 @@ function FillForm({
|
||||
<button type="button" className="jc-cant-link" onClick={() => setCant(true)}>
|
||||
没法填?告诉 AI 原因
|
||||
</button>
|
||||
<button className="jc-btn" disabled={!ok} onClick={() => onDone(vals)}>
|
||||
<Btn className="jc-btn" disabled={!ok} busy={busy} onClick={() => onDone(vals)}>
|
||||
<Ico name="check" s={13} /> 补给 AI
|
||||
</button>
|
||||
</Btn>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1130,6 +1160,7 @@ function FillForm({
|
||||
|
||||
/* 评价卡的打分器:星级 + 改进建议(建议进活动记忆) */
|
||||
function RateForm({ onDone }: { onDone: (score: number, note: string) => void }) {
|
||||
const busy = useReplyBusy();
|
||||
const [score, setScore] = useState(0);
|
||||
const [hover, setHover] = useState(0);
|
||||
const [note, setNote] = useState('');
|
||||
@@ -1164,9 +1195,9 @@ function RateForm({ onDone }: { onDone: (score: number, note: string) => void })
|
||||
/>
|
||||
</label>
|
||||
<div className="jc-cfill-acts">
|
||||
<button className="jc-btn" disabled={score === 0} onClick={() => onDone(score, note.trim())}>
|
||||
<Btn className="jc-btn" disabled={score === 0} busy={busy} onClick={() => onDone(score, note.trim())}>
|
||||
<Ico name="check" s={13} /> 提交评价
|
||||
</button>
|
||||
</Btn>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1192,6 +1223,7 @@ function EntryCard({
|
||||
* 联系人由 AI 自己写 —— 而联系人档的 `contact_code` 是它编出来的,人一眼没看过。
|
||||
* 值按段隔开(`段号.列名`):两张表都有 `name` 这一列,平铺就会互相盖掉。
|
||||
*/
|
||||
const busy = useReplyBusy();
|
||||
const groups = [
|
||||
{
|
||||
title: '',
|
||||
@@ -1290,9 +1322,9 @@ function EntryCard({
|
||||
{!done && (
|
||||
<div className="jc-cfill-acts">
|
||||
{blank.length > 0 && <span className="jc-entry-warn">还差:{blank.map((f) => f.label).join('、')}</span>}
|
||||
<button className="jc-btn" disabled={blank.length > 0} onClick={() => onDone(vals)}>
|
||||
<Btn className="jc-btn" disabled={blank.length > 0} busy={busy} onClick={() => onDone(vals)}>
|
||||
<Ico name="database" s={13} /> 确认写入
|
||||
</button>
|
||||
</Btn>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user