Commit f8fd1ee5 by luoqi

feat(引导): 「最忙的那位」补上「另有 N 位也超时效」;修一处注释与 SQL 打架

**① 只报最忙一位,分不出两种相反的局面**(产品定):
  · 只有他一个人超 → 该给他少分点 / 改派几个
  · 全队都超       → 该减少这批 / 延长时效
  同一句话、相反的处置 —— 而本节点唯一的选项是「整批时效改成 N 天」,
  碰上第一种局面它本身就是错的(为一个人的负载去延长整批时效)。
⇒ why 里补「另有 N 位也超过 X 天;每位分完之后要打几天,确认单上逐位都写着」。
️ 只加一个数 + 一句引路, 不在引导里铺开每个人:确认单每行已经有「约 N 天」,
  分布本来就在眼皮底下(引导节点的职责是点出要他定的事,不是展示数据)。
️ 只有一个人超时那半句**不出现** ——  不制造无谓噪音。
📌 加在**工具产出的数据**里(`daily_overload` 的 why), 没动提示词 ——
  模型从返回值里直接读,不需要被提醒(工具返回值 > 提示词)。

**② 团队面板那段注释与 SQL 打架**:注释把「超期」和「在手」并列写成"与窗口无关",
  而 SQL 里是 `assignment_expires_at >= since` —— **SQL 是对的**:
  一年前过期的单报上来对主管没意义,他此刻能处置的只有近期这批。
  ⇒ 改注释、 别照注释去掉 SQL 的窗口,并把理由写在旁边。

新增 2 条测试(多人超时 → 报数并引到确认单;只有一人超 → 那半句不出现),1298 全绿。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent dac5e781
......@@ -378,6 +378,18 @@ export function computeSignals(p: AssignmentProposal, extra: Signal[] = []): Sig
// 主管把它改成 20 之后,常量算出来的天数和卡片上每一行都对不上。
if (heaviest && heaviest.loadAfter > p.dailyCalls * d) {
const needDays = Math.ceil(heaviest.loadAfter / p.dailyCalls);
/**
* 🔴 **超时效的一共几位**(2026-08-16 产品定)。只报最忙那一位分不出两种相反的局面:
* · 只有他一个人超 → 该做的是**给他少分点 / 改派几个**;
* · 全队都超 → 该做的是**减少这批 / 延长时效**。
* 同一句话、相反的处置 —— 而这条唯一给的选项是「整批时效改成 N 天」,
* 碰上第一种局面它本身就是错的(为一个人的负载去延长整批时效)。
* ⚠️ 只加一个数,⛔ 不在这里铺开每个人:确认单上每位客服那一行已经有「约 N 天」,
* 分布本来就在眼皮底下 ⇒ 这里一句话把他引过去看就够(引导节点的职责是
* **点出要他定的事**,不是展示数据)。
* ⚠️ 只有他一个人超时这半句**不出现** —— ⛔ 不制造无谓的噪音。
*/
const overCount = p.byAgent.filter((a) => a.loadAfter > p.dailyCalls * d).length;
out.push({
key: 'daily_overload',
severity: SEV.WRONG_EXPECTATION,
......@@ -389,7 +401,10 @@ export function computeSignals(p: AssignmentProposal, extra: Signal[] = []): Sig
// ⚠️ 「按每天 15 通算」这个前提要写出来:它是评审当天的口头经验值,不是实测。
why:
`其中本批 ${heaviest.count} 条、原本在手 ${heaviest.inHandBefore} 条;` +
`按每人每天 ${p.dailyCalls} 通算需要 ${needDays} 天,本批时效定的是 ${d} 天。`,
`按每人每天 ${p.dailyCalls} 通算需要 ${needDays} 天,本批时效定的是 ${d} 天。` +
(overCount > 1
? `另有 ${overCount - 1} 位也超过 ${d} 天;每位分完之后要打几天,确认单上逐位都写着。`
: ''),
defaultLabel: `不处理 = 就按 ${d} 天发,到期没打完的自动落回池子,下批还能再分`,
/**
* 🔴 **只留带数的那一个**(2026-08-15 产品定)。
......
......@@ -834,10 +834,15 @@ export class PlanAssignmentService {
* 回答文档里那三个问题的第三个:「团队现在什么状态」。
*
* ── 两种时间性混在一张表里,必须说清 ────────────────────────────
* · **在手 / 超期** —— **此刻**的状态(手上还压着多少、其中多少过了时效),与窗口无关;
* · **完成 / 退回 / 已处置 / 没动** —— **窗口内**发生的事(近 7 天 / 近 30 天)。
* · **在手** —— **此刻**手上还压着多少(`status='assigned'`),与窗口无关;
* · **超期 / 完成 / 退回 / 已处置 / 没动** —— **窗口内**的事(近 7 天 / 近 30 天)。
* ⚠️ 界面上必须写明这件事,否则主管会把「在手 62」当成"这 7 天分了 62 条"。
*
* 🔴 **超期也是带窗口的**(2026-08-16 修注释:原文把它和「在手」并列写成"与窗口无关",
* 与下面那条 SQL 对不上 —— SQL 里有 `assignment_expires_at >= since`,是**对的**)。
* 理由:一年前过期的单报上来对主管没有意义,他此刻能处置的只有近期这批。
* ⛔ 别照注释把 SQL 的窗口去掉。
*
* ⚠️ 「超期」必须带上**且没约下次回访**这半句,与 detail 的 agentStats 同一条判据:
* 到期回收器刻意跳过 snoozedUntil 在未来的单(客服约了 6/10 回访,那之前不能收走),
* 不加守卫会把"打了电话、约好下次"的人显示成"压着单没动" —— 干得最好的那个被指责。
......
......@@ -765,3 +765,39 @@ describe('没分到的那几位 —— 「为什么没轮到」要答得上来',
expect(facts([])).not.toHaveProperty('他们现在手上有');
});
});
/**
* 🔴 「最忙的那位」要报**一共几位超时效**(2026-08-16 产品定)。
* 只报最忙一位,主管分不出"个别人忙"和"全队都忙" —— 而这两种局面的处置相反:
* 前者该给他少分点/改派,后者该减量/延时效。而本节点唯一的选项是「整批时效改成 N 天」,
* 碰上前者它本身就是错的。
*/
describe('引导节点 · 最忙的那位要说清是一个人还是一片', () => {
const over = (name: string, loadAfter: number) => ({
userId: name,
name,
count: 5,
inHandBefore: loadAfter - 5,
loadAfter,
});
test('🔴 多人超时效 → 报「另有 N 位」,并把他引到确认单看逐位明细', () => {
// 时效 1 天 × 每天 15 通 = 15 条封顶;三个人都超
const s = computeSignals(
proposal({
expiresInDays: 1,
byAgent: [over('王强', 45), over('李莉', 30), over('赵敏', 20), over('周涛', 9)],
}),
).find((x) => x.key === 'daily_overload')!;
expect(s.title).toContain('王强'); // 最忙的那位仍然点名
expect(s.why).toContain('另有 2 位也超过 1 天'); // 3 位超 → 除最忙外还有 2 位
expect(s.why).toContain('确认单上逐位都写着');
});
test('⛔ 只有一个人超 → 那半句不出现(不制造无谓噪音)', () => {
const s = computeSignals(
proposal({ expiresInDays: 1, byAgent: [over('王强', 45), over('周涛', 9)] }),
).find((x) => x.key === 'daily_overload')!;
expect(s.why).not.toContain('另有');
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment