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:
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 时刻一律本地 —— **差八小时的时刻比没有时刻更坏**,它看起来像一个可信的事实。
|
||||
*
|
||||
* 实测栽过:后端存 UTC,前端满屏 `at.slice(5, 16)` 直接摆上去,
|
||||
* 于是一封 08-01 04:18 到的信,工单上写着「派发 07-31 20:18」、气泡写着「20:18」;
|
||||
* 人拿它去核 SLA、去跟客户对「我们几点收到的」,一核一个错。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { atClock, atFull, atMinute } from '../at.ts';
|
||||
|
||||
const two = (n: number): string => String(n).padStart(2, '0');
|
||||
|
||||
describe('ISO → 本地时刻', () => {
|
||||
const iso = '2026-07-31T20:18:33.000Z';
|
||||
const at = new Date(iso);
|
||||
|
||||
it('atMinute 给「月-日 时:分」,按本地时区', () => {
|
||||
expect(atMinute(iso)).toBe(
|
||||
`${two(at.getMonth() + 1)}-${two(at.getDate())} ${two(at.getHours())}:${two(at.getMinutes())}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('atClock 给「时:分」', () => {
|
||||
expect(atClock(iso)).toBe(`${two(at.getHours())}:${two(at.getMinutes())}`);
|
||||
});
|
||||
|
||||
it('atFull 带上年', () => {
|
||||
expect(atFull(iso)).toBe(
|
||||
`${at.getFullYear()}-${two(at.getMonth() + 1)}-${two(at.getDate())} ${two(at.getHours())}:${two(at.getMinutes())}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('**不是照搬 UTC 那几个字符** —— 时区不为零时两者必不相同', () => {
|
||||
const offset = new Date(iso).getTimezoneOffset();
|
||||
if (offset !== 0) expect(atMinute(iso)).not.toBe(iso.slice(5, 16).replace('T', ' '));
|
||||
});
|
||||
|
||||
it('不是时刻就留空,不摆一个假的', () => {
|
||||
for (const bad of ['', '这不是时刻', 'undefined']) {
|
||||
expect(atMinute(bad)).toBe('');
|
||||
expect(atClock(bad)).toBe('');
|
||||
expect(atFull(bad)).toBe('');
|
||||
}
|
||||
});
|
||||
|
||||
it('秒不进屏 —— 同一分钟内的两个时刻摆出来一模一样', () => {
|
||||
expect(atMinute('2026-08-01T04:18:01.000Z')).toBe(atMinute('2026-08-01T04:18:59.999Z'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 时刻的摆法 —— **屏上一律本地时刻**。
|
||||
*
|
||||
* 后端存的是 UTC ISO 串。从前满屏都是 `at.slice(5, 16).replace('T', ' ')` ——
|
||||
* 把 UTC 那八个字符直接摆上去,于是**每一个时刻都差着时区**:
|
||||
* 一封 08-01 04:18 到的信,工单上写着「派发 07-31 20:18」,对话气泡写着「20:18」。
|
||||
* 差八小时的时刻比没有时刻更坏 —— 它看起来像一个可信的事实,人拿它去核 SLA、
|
||||
* 去跟客户对「我们几点收到的」,一核一个错(实测在来件那一行栽过一次)。
|
||||
*
|
||||
* 只做一件事:ISO → 本地。**不做「几分钟前」这类相对时间** ——
|
||||
* 相对时间读着舒服,可它随打开的时刻变,截图发给同事就对不上了。
|
||||
*/
|
||||
|
||||
const two = (n: number): string => String(n).padStart(2, '0');
|
||||
|
||||
/** `月-日 时:分` —— 工单、气泡、日志里那一小行 */
|
||||
export function atMinute(iso: string): string {
|
||||
const at = new Date(iso);
|
||||
if (Number.isNaN(at.getTime())) return '';
|
||||
return `${two(at.getMonth() + 1)}-${two(at.getDate())} ${two(at.getHours())}:${two(at.getMinutes())}`;
|
||||
}
|
||||
|
||||
/** `时:分` —— 同一天之内的密集条目(对话流) */
|
||||
export function atClock(iso: string): string {
|
||||
const at = new Date(iso);
|
||||
if (Number.isNaN(at.getTime())) return '';
|
||||
return `${two(at.getHours())}:${two(at.getMinutes())}`;
|
||||
}
|
||||
|
||||
/** `年-月-日 时:分` —— 详情页那种要说清哪一天的地方 */
|
||||
export function atFull(iso: string): string {
|
||||
const at = new Date(iso);
|
||||
if (Number.isNaN(at.getTime())) return '';
|
||||
return `${at.getFullYear()}-${two(at.getMonth() + 1)}-${two(at.getDate())} ${two(at.getHours())}:${two(at.getMinutes())}`;
|
||||
}
|
||||
@@ -2,7 +2,22 @@
|
||||
* 数据域短名 —— 字典命中给人读名,字典外回落键尾段:轨上永远有个能认的名字。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { domainShort, sortDomains } from '../domains.ts';
|
||||
import { DOMAIN_SHORT, domainShort, sortDomains } from '../domains.ts';
|
||||
|
||||
/** 库里现有的全部数据域(2026-07-31 实测 `/api/gov/data/objects`) */
|
||||
const LIVE_DOMAINS = [
|
||||
'customer',
|
||||
'intel',
|
||||
'project',
|
||||
'quote_alternative',
|
||||
'quote_archive',
|
||||
'quote_intake',
|
||||
'quote_negotiate',
|
||||
'quote_pricing',
|
||||
'quote_sourcing',
|
||||
'shared',
|
||||
'supplier',
|
||||
];
|
||||
|
||||
describe('domainShort', () => {
|
||||
it('已知域给 2-3 字人读名', () => {
|
||||
@@ -12,20 +27,51 @@ describe('domainShort', () => {
|
||||
expect(domainShort('supplier')).toBe('供应商');
|
||||
});
|
||||
|
||||
it('未知域回落到键的最后一段,超长截 4 字符', () => {
|
||||
it('六个活动域跟着活动叫', () => {
|
||||
expect(domainShort('quote_intake')).toBe('受理');
|
||||
expect(domainShort('quote_alternative')).toBe('选型');
|
||||
expect(domainShort('quote_sourcing')).toBe('寻源');
|
||||
expect(domainShort('quote_pricing')).toBe('定价');
|
||||
expect(domainShort('quote_negotiate')).toBe('议价');
|
||||
expect(domainShort('quote_archive')).toBe('归档');
|
||||
expect(domainShort('shared')).toBe('共享');
|
||||
});
|
||||
|
||||
/*
|
||||
* 这条是用户提的硬要求:数据域**必须都显示中文**。
|
||||
* 从前 `quote_*` 一个都没登记,治理台上蹦出 alte / sour / pric 这样的英文残词。
|
||||
* 拿库里真实存在的域逐个过一遍 —— 漏登记一个,这条就红。
|
||||
*/
|
||||
it('库里现有的每一个域都有中文短名 —— 一个都不许漏', () => {
|
||||
const missing = LIVE_DOMAINS.filter((d) => DOMAIN_SHORT[d] === undefined);
|
||||
expect(missing).toEqual([]);
|
||||
for (const d of LIVE_DOMAINS) {
|
||||
expect(domainShort(d)).toMatch(/^[一-龥]+$/);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* 字典外回落到**完整尾段**,不再截 4 个字符 ——
|
||||
* `alternative` 截成 `alte` 不是「能认的名字」,而且把「还没登记」伪装成了「它就叫这个」。
|
||||
*/
|
||||
it('未知域回落到键的最后一段,整段留着不截', () => {
|
||||
expect(domainShort('warehouse_ops')).toBe('ops');
|
||||
expect(domainShort('some_verylongtail')).toBe('very');
|
||||
expect(domainShort('some_verylongtail')).toBe('verylongtail');
|
||||
expect(domainShort('solo')).toBe('solo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortDomains —— 按经营视角排,不按字母', () => {
|
||||
it('已知域按业务序(项目最前),未知域殿后', () => {
|
||||
expect(sortDomains(['supplier', 'intel', 'zzz_custom', 'project'])).toEqual([
|
||||
'project',
|
||||
'intel',
|
||||
'supplier',
|
||||
it('先按一单走下来的次序摆业务域,沉淀域在后,未知域殿后', () => {
|
||||
expect(sortDomains(['shared', 'quote_pricing', 'zzz_custom', 'quote_intake'])).toEqual([
|
||||
'quote_intake',
|
||||
'quote_pricing',
|
||||
'shared',
|
||||
'zzz_custom',
|
||||
]);
|
||||
});
|
||||
|
||||
it('退役占位槽留下的 project 压在已知域最后', () => {
|
||||
expect(sortDomains(['project', 'quote_intake', 'customer'])).toEqual(['quote_intake', 'customer', 'project']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,14 +6,50 @@
|
||||
* 字典之外的域回落到键的最后一段(截 4 字符)—— 新域没配短名时,先有个能认的名字。
|
||||
*/
|
||||
export const DOMAIN_SHORT: Readonly<Record<string, string>> = {
|
||||
project: '项目',
|
||||
/* 沉淀资产:跨单积累的事实 */
|
||||
customer: '客户',
|
||||
intel: '情报',
|
||||
supplier: '供应商',
|
||||
shared: '共享',
|
||||
market: '市场',
|
||||
/* 每一单的流转单据 —— `project` 是占位槽时代的统一域,活动转正后各归各的 */
|
||||
project: '项目',
|
||||
/*
|
||||
* 真活动包各自的域(2026-07-31 补齐)。
|
||||
*
|
||||
* 从前这几个一个都没登记,全落到「截键尾 4 字符」的兜底上:
|
||||
* `quote_alternative` 显示成 **alte**、`quote_sourcing` 成 **sour**、
|
||||
* `quote_pricing` 成 **pric** —— 一屏中文里蹦出几个没意义的英文残词。
|
||||
* 短名跟着活动叫,与设计中心活动库里那六个名字对得上(受理 / 选型 / 寻源 / 定价 / 议价 / 归档)。
|
||||
*/
|
||||
quote_intake: '受理',
|
||||
quote_alternative: '选型',
|
||||
quote_sourcing: '寻源',
|
||||
quote_pricing: '定价',
|
||||
quote_negotiate: '议价',
|
||||
quote_archive: '归档',
|
||||
};
|
||||
|
||||
/** 业务序:项目(每天看的)→ 客户 → 情报 → 供应商 —— 按经营视角排,不按字母 */
|
||||
const DOMAIN_ORDER = ['project', 'customer', 'intel', 'supplier'];
|
||||
/**
|
||||
* 业务序 —— 按经营视角排,不按字母。
|
||||
* 先按**一单走下来的次序**摆六个业务域(受理→选型→寻源→定价→议价→归档),
|
||||
* 再摆跨单沉淀的资产域;`project` 是退役占位槽留下的,压在最后。
|
||||
*/
|
||||
const DOMAIN_ORDER = [
|
||||
'quote_intake',
|
||||
'quote_alternative',
|
||||
'quote_sourcing',
|
||||
'quote_pricing',
|
||||
'quote_negotiate',
|
||||
'quote_archive',
|
||||
'shared',
|
||||
/* 主数据四域:谁在买 → 为什么买 → 从哪买 → 买什么 */
|
||||
'customer',
|
||||
'project',
|
||||
'supplier',
|
||||
'market',
|
||||
'intel',
|
||||
];
|
||||
|
||||
export function sortDomains(domains: readonly string[]): string[] {
|
||||
return [...domains].sort((a, b) => {
|
||||
@@ -30,6 +66,14 @@ export function sortDomains(domains: readonly string[]): string[] {
|
||||
export function domainShort(domain: string): string {
|
||||
const hit = DOMAIN_SHORT[domain];
|
||||
if (hit !== undefined) return hit;
|
||||
const tail = domain.split('_').at(-1) ?? domain;
|
||||
return tail.length <= 4 ? tail : tail.slice(0, 4);
|
||||
/*
|
||||
* 字典外的域**回落到键的完整尾段,不再截字符**(2026-07-31 改)。
|
||||
*
|
||||
* 从前截 4 个字符,`quote_alternative` 出来是 `alte`、`quote_pricing` 是 `pric` ——
|
||||
* 那不是「能认的名字」,是一串看不出所以然的残词,而且它把「这个域还没登记短名」
|
||||
* 这件事伪装成了「它就叫这个」。整段留着,至少一眼看得出是哪个域、也看得出没配中文。
|
||||
*
|
||||
* 新加数据域时请一并往 `DOMAIN_SHORT` 里登记中文短名 —— 这条兜底是**提醒**,不是归宿。
|
||||
*/
|
||||
return domain.split('_').at(-1) ?? domain;
|
||||
}
|
||||
|
||||
@@ -172,7 +172,15 @@ export const ZM: Readonly<Record<GovZone, { title: string; desc: string; accent:
|
||||
*/
|
||||
export const TABS: Readonly<Record<GovZone, ReadonlyArray<{ key: string; label: string; ico: string }>>> = {
|
||||
blueprint: [{ key: 'map', label: '企业蓝图', ico: 'route' }],
|
||||
testbed: [{ key: 'run', label: '仿真试跑', ico: 'flask-conical' }],
|
||||
/*
|
||||
* 测试台三格 —— 从前是一格加一个「两档旋钮」,旋钮长在内容里,别处跳不进来、
|
||||
* 刷新也记不住试的是哪一台。改成页签:试什么、看什么由地址说了算。
|
||||
*/
|
||||
testbed: [
|
||||
{ key: 'run', label: '流程', ico: 'workflow' },
|
||||
{ key: 'act', label: '活动', ico: 'package' },
|
||||
{ key: 'log', label: '日志', ico: 'scroll-text' },
|
||||
],
|
||||
triggers: [{ key: 'rules', label: '调起规则', ico: 'zap' }],
|
||||
archive: [{ key: 'docs', label: '资料', ico: 'library' }],
|
||||
models: [{ key: 'llm', label: '模型实例', ico: 'cpu' }],
|
||||
|
||||
@@ -40,7 +40,8 @@ export const APR_ST: Readonly<Record<'pending' | 'passed' | 'rejected', ToneIco
|
||||
|
||||
export const SIDE_ST: Readonly<Record<string, ToneIco>> = {
|
||||
机器产出: { tone: 'auto', ico: 'bot' },
|
||||
人产出: { tone: 'brand', ico: 'user-round' },
|
||||
员工动作: { tone: 'brand', ico: 'user-round' },
|
||||
人裁决: { tone: 'decision', ico: 'shield-check' },
|
||||
世界等待: { tone: 'wait', ico: 'hourglass' },
|
||||
系统等待: { tone: 'wait', ico: 'hourglass' },
|
||||
触发事件: { tone: 'brand', ico: 'zap' },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user