Merge remote-tracking branch 'origin/main'

# Conflicts:
#	docs/CHANGELOG.md
#	server/aps_domain/workflow.py
#	tests/golden/test_assistant.py
This commit is contained in:
z.zhang 2026-08-26 00:37:04 +08:00
commit 7f171f328d
24 changed files with 283 additions and 211 deletions

View File

@ -440,7 +440,7 @@ export default function App() {
// 命令面板条目:口令(发对话)/ 面板(布局开关)/ 视图(视口切换) // 命令面板条目:口令(发对话)/ 面板(布局开关)/ 视图(视口切换)
const paletteItems: PaletteItem[] = [ const paletteItems: PaletteItem[] = [
...[ ...[
'多策略方案对比', '试排一版交期优先', '产能均衡重排', '建一个检查点', '多策略方案对比', '生成一版交期优先试排方案', '产能均衡重排', '建一个检查点',
'回滚到上一个检查点', '查看冲突', '标出超期订单', '查看当前KPI', '发布当前排产版本', '回滚到上一个检查点', '查看冲突', '标出超期订单', '查看当前KPI', '发布当前排产版本',
].map(q => ({ ].map(q => ({
id: `cmd-${q}`, kind: '口令' as const, label: q, hint: '发送到对话', id: `cmd-${q}`, kind: '口令' as const, label: q, hint: '发送到对话',

View File

@ -168,11 +168,11 @@ function GuestWorkspace() {
<div className="chat-log"> <div className="chat-log">
<div className="chat-start guest-chat-start"> <div className="chat-start guest-chat-start">
<span className="chat-start-mark">IB</span> <span className="chat-start-mark">IB</span>
<h1>今天想先忙哪件事?</h1> <h1>请选择要处理的任务</h1>
<div className="starter-grid guest-starter-grid"> <div className="starter-grid guest-starter-grid">
<button type="button" className="starter-card" onClick={openLogin}> <button type="button" className="starter-card" onClick={openLogin}>
<span className="starter-icon spark"><ThunderboltOutlined /></span> <span className="starter-icon spark"><ThunderboltOutlined /></span>
<strong>试排一版交期优先</strong><small>试排一版交期优先</small> <strong>生成交期优先试排方案</strong><small>生成一版交期优先试排方案</small>
</button> </button>
<button type="button" className="starter-card" onClick={openLogin}> <button type="button" className="starter-card" onClick={openLogin}>
<span className="starter-icon compare"><SwapOutlined /></span> <span className="starter-icon compare"><SwapOutlined /></span>

View File

@ -8,6 +8,10 @@ import { chatStream, fetchGuidance, postConfirm, postImportPreview, postScenario
import { loadMessages, loadMessagesPreferServer, saveMessages } from '../projects/store'; import { loadMessages, loadMessagesPreferServer, saveMessages } from '../projects/store';
import { ChatMarkdown } from './ChatMarkdown'; import { ChatMarkdown } from './ChatMarkdown';
const SHOW_INTENT_DEBUG = (import.meta as ImportMeta & {
env: Record<string, string | undefined>;
}).env.VITE_SHOW_INTENT_DEBUG === '1';
interface Msg { interface Msg {
role: 'user' | 'agent'; role: 'user' | 'agent';
text: string; text: string;
@ -26,12 +30,19 @@ function ThinkingPanel(props: {
}) { }) {
const last = props.steps[props.steps.length - 1]; const last = props.steps[props.steps.length - 1];
const pct = last?.pct; const pct = last?.pct;
const stepLabel = (step: string) => ({
'连接助手': '连接服务',
'收到指令': '收到请求',
'理解意图': '确认目标',
'开始执行': '执行操作',
'生成回复': '整理结果',
}[step] ?? step);
return ( return (
<div className={`thinking-panel${props.open ? ' open' : ''}${props.streaming ? ' live' : ''}`}> <div className={`thinking-panel${props.open ? ' open' : ''}${props.streaming ? ' live' : ''}`}>
<button type="button" className="thinking-head" onClick={props.onToggle}> <button type="button" className="thinking-head" onClick={props.onToggle}>
<span className={`thinking-spinner${props.streaming ? '' : ' done'}`} aria-hidden /> <span className={`thinking-spinner${props.streaming ? '' : ' done'}`} aria-hidden />
<span className="thinking-title"> <span className="thinking-title">
{props.streaming ? 'Thinking' : '已完成思考'} {props.streaming ? '处理中' : '处理完成'}
{typeof pct === 'number' && props.streaming ? ` · ${Math.round(pct)}%` : ''} {typeof pct === 'number' && props.streaming ? ` · ${Math.round(pct)}%` : ''}
</span> </span>
<span className="thinking-caret">{props.open ? '▾' : '▸'}</span> <span className="thinking-caret">{props.open ? '▾' : '▸'}</span>
@ -43,14 +54,14 @@ function ThinkingPanel(props: {
<span className="thinking-mark"> <span className="thinking-mark">
{s.status === 'done' ? '✓' : s.status === 'warn' ? '!' : '·'} {s.status === 'done' ? '✓' : s.status === 'warn' ? '!' : '·'}
</span> </span>
<span className="thinking-step">{s.step}</span> <span className="thinking-step">{stepLabel(s.step)}</span>
{!!s.detail && <span className="thinking-detail">{s.detail}</span>} {!!s.detail && <span className="thinking-detail">{s.detail}</span>}
</li> </li>
))} ))}
</ul> </ul>
)} )}
{!props.open && props.streaming && last && ( {!props.open && props.streaming && last && (
<div className="thinking-current">{last.step}{last.detail ? ` · ${last.detail}` : ''}</div> <div className="thinking-current">{stepLabel(last.step)}{last.detail ? ` · ${last.detail}` : ''}</div>
)} )}
</div> </div>
); );
@ -94,13 +105,13 @@ export interface ChatHandle {
} }
const QUICK = [ const QUICK = [
'跑一版柔性排产', '瓶颈产能', 'PDU 500套什么时候能交', '生成一版柔性排产方案', '瓶颈产能', 'PDU 500 套交期评估',
'新建订单 客户 示例客户 产品 请替换为真实料号 数量 120 交期 8月20日', '新建订单 客户 示例客户 产品 请替换为真实料号 数量 120 交期 8月20日',
'新建物料 编码 SCR-NEW 名称 不锈钢螺丝 类型 原料 库存 800', '新建物料 编码 SCR-NEW 名称 不锈钢螺丝 类型 原料 库存 800',
'导入订单\n客户A,成品料号,200,2026-08-01,VIP\n客户 客户B 产品 成品料号 数量 100 交期 8月15日', '导入订单\n客户A,成品料号,200,2026-08-01,VIP\n客户 客户B 产品 成品料号 数量 100 交期 8月15日',
'导入物料\nSCR-NEW,不锈钢螺丝,原料,包,800\nPCB-X,试验板,原料,块,200', '导入物料\nSCR-NEW,不锈钢螺丝,原料,包,800\nPCB-X,试验板,原料,块,200',
'追溯订单依据', '分解全部订单', '主数据录入顺序有什么规定', '追溯订单依据', '分解全部订单', '主数据录入顺序有什么规定',
'多策略方案对比', '试排一版交期优先', '产能均衡重排', '多策略方案对比', '生成一版交期优先试排方案', '产能均衡重排',
'换线有什么规定', '生成排产方案报告', '生成日报', '生成版本对比报告', '换线有什么规定', '生成排产方案报告', '生成日报', '生成版本对比报告',
'建一个检查点', '回滚到上一个检查点', '建一个检查点', '回滚到上一个检查点',
'只看VIP订单', '标出超期订单', '查看冲突', '只看VIP订单', '标出超期订单', '查看冲突',
@ -238,7 +249,7 @@ const ChatPanel = forwardRef<ChatHandle, {
(sessionRef as { current: string; _hist?: typeof history })._hist = history; (sessionRef as { current: string; _hist?: typeof history })._hist = history;
return [...base, { role: 'user', text: t }, { return [...base, { role: 'user', text: t }, {
role: 'agent', text: '', streaming: true, thinkingOpen: true, role: 'agent', text: '', streaming: true, thinkingOpen: true,
thinking: [{ step: '连接助手', detail: '已发送,等待处理…', status: 'running', pct: 0 }], thinking: [{ step: '连接助手', detail: '请求已发送,等待服务响应…', status: 'running', pct: 0 }],
}]; }];
}); });
const sid = sessionRef.current; const sid = sessionRef.current;
@ -272,7 +283,7 @@ const ChatPanel = forwardRef<ChatHandle, {
})); }));
else if (ev.type === 'command') props.onCommand(ev.command); else if (ev.type === 'command') props.onCommand(ev.command);
else if (ev.type === 'block') patchLast(m => ({ ...m, blocks: [...(m.blocks ?? []), ev.block] })); else if (ev.type === 'block') patchLast(m => ({ ...m, blocks: [...(m.blocks ?? []), ev.block] }));
else if (ev.type === 'error') patchLast(m => ({ ...m, text: m.text + '\n⚠ ' + ev.message })); else if (ev.type === 'error') patchLast(m => ({ ...m, text: m.text + '\n\n**操作未完成**:' + ev.message }));
else if (ev.type === 'done') { else if (ev.type === 'done') {
patchLast(m => ({ patchLast(m => ({
...m, ...m,
@ -284,7 +295,7 @@ const ChatPanel = forwardRef<ChatHandle, {
})); }));
props.onWorldRefresh(); props.onWorldRefresh();
} }
}, hist).catch(() => patchLast(m => ({ ...m, text: m.text + '\n⚠ 连接失败,请确认后端已启动。', streaming: false }))); }, hist).catch(() => patchLast(m => ({ ...m, text: m.text + '\n\n**连接未完成**:服务暂时不可用,请稍后重试。', streaming: false })));
setBusy(false); setBusy(false);
}, [busy, patchLast, props]); }, [busy, patchLast, props]);
@ -385,7 +396,7 @@ const ChatPanel = forwardRef<ChatHandle, {
}; };
recognition.onend = () => setListening(false); recognition.onend = () => setListening(false);
recognitionRef.current = recognition; recognitionRef.current = recognition;
setVoiceNotice('正在听,请说出排产要求或主数据说明。'); setVoiceNotice('正在监听,请说明排产目标或主数据变更。');
setListening(true); setListening(true);
recognition.start(); recognition.start();
}, [autoGrow, listening]); }, [autoGrow, listening]);
@ -394,7 +405,7 @@ const ChatPanel = forwardRef<ChatHandle, {
const handleConfirm = useCallback(async (confirmId: string, approve: boolean) => { const handleConfirm = useCallback(async (confirmId: string, approve: boolean) => {
const result = await postConfirm(sessionRef.current, confirmId, approve) const result = await postConfirm(sessionRef.current, confirmId, approve)
.catch(() => ({ message: '确认请求失败', refresh: false })); .catch(() => ({ message: '确认操作未完成,数据未修改。', refresh: false }));
setMsgs(ms => [...ms, { role: 'agent', text: result.message }]); setMsgs(ms => [...ms, { role: 'agent', text: result.message }]);
if (result.refresh) props.onWorldRefresh(); if (result.refresh) props.onWorldRefresh();
return result; return result;
@ -402,7 +413,7 @@ const ChatPanel = forwardRef<ChatHandle, {
const handleApply = useCallback(async (card: ScenarioCard) => { const handleApply = useCallback(async (card: ScenarioCard) => {
const result = await postScenarioApply(sessionRef.current, card.strategy, card.engine) const result = await postScenarioApply(sessionRef.current, card.strategy, card.engine)
.catch(() => ({ message: '采用失败,请确认后端已启动', refresh: false })); .catch(() => ({ message: '方案未采用,数据未修改。请稍后重试。', refresh: false }));
setMsgs(ms => [...ms, { role: 'agent', text: result.message || `已采用【${card.label}】` }]); setMsgs(ms => [...ms, { role: 'agent', text: result.message || `已采用【${card.label}】` }]);
if (result.refresh) props.onWorldRefresh(); if (result.refresh) props.onWorldRefresh();
}, [props]); }, [props]);
@ -498,7 +509,7 @@ const ChatPanel = forwardRef<ChatHandle, {
? <ChatMarkdown text={m.text} streaming={!!m.streaming} /> ? <ChatMarkdown text={m.text} streaming={!!m.streaming} />
: <span className="bubble-loading">正在处理…</span>) : <span className="bubble-loading">正在处理…</span>)
: <>{m.text}{m.streaming ? '▍' : ''}</>} : <>{m.text}{m.streaming ? '▍' : ''}</>}
{m.intent && !m.streaming && ( {SHOW_INTENT_DEBUG && m.intent && !m.streaming && (
<details className="intent-debug"> <details className="intent-debug">
<summary>调试信息</summary> <summary>调试信息</summary>
<pre>{JSON.stringify(m.intent, null, 2)}</pre> <pre>{JSON.stringify(m.intent, null, 2)}</pre>
@ -649,7 +660,7 @@ const ChatPanel = forwardRef<ChatHandle, {
void sendDraft(); void sendDraft();
} }
}} }}
placeholder="输入排产指令…(Enter 发送 · Shift+Enter 换行)" placeholder="描述订单、交期或排产目标…(Enter 发送 · Shift+Enter 换行)"
/> />
<button <button
type="button" type="button"
@ -678,10 +689,10 @@ type StarterIconName = 'spark' | 'compare' | 'risk' | 'report';
function ChatStart(props: { onSend: (text: string) => void }) { function ChatStart(props: { onSend: (text: string) => void }) {
const defaults: Array<{ icon: StarterIconName; title: string; text: string }> = [ const defaults: Array<{ icon: StarterIconName; title: string; text: string }> = [
{ icon: 'spark', title: '帮我排一版', text: '带我排一版' }, { icon: 'spark', title: '生成试排方案', text: '生成一版交期优先试排方案' },
{ icon: 'compare', title: '对比几种排法', text: '多策略方案对比' }, { icon: 'compare', title: '对比排产策略', text: '多策略方案对比' },
{ icon: 'risk', title: '看看哪卡住了', text: '查看冲突并标出超期订单' }, { icon: 'risk', title: '查看冲突与超期', text: '查看冲突并标出超期订单' },
{ icon: 'report', title: '出一份工作表', text: '生成排产方案报告' }, { icon: 'report', title: '生成排产报告', text: '生成排产方案报告' },
]; ];
const [starters, setStarters] = useState(defaults); const [starters, setStarters] = useState(defaults);
const [hint, setHint] = useState<string | null>(null); const [hint, setHint] = useState<string | null>(null);
@ -707,10 +718,10 @@ function ChatStart(props: { onSend: (text: string) => void }) {
return ( return (
<div className="chat-start"> <div className="chat-start">
<div className="chat-start-mark">IB</div> <div className="chat-start-mark">IB</div>
<h1>今天想先忙哪件事?</h1> <h1>请选择要处理的任务</h1>
{hint {hint
? <p className="chat-start-hint">{hint}</p> ? <p className="chat-start-hint">{hint}</p>
: <p className="chat-start-hint">随便说就行,打错字、说方言都可以。不会专业词也没关系,我慢慢带着你。</p>} : <p className="chat-start-hint">可以描述订单、交期、设备约束或需要生成的排产报告。</p>}
<div className="starter-grid"> <div className="starter-grid">
{starters.map(item => ( {starters.map(item => (
<button key={item.title + item.text} type="button" className="starter-card" onClick={() => props.onSend(item.text)}> <button key={item.title + item.text} type="button" className="starter-card" onClick={() => props.onSend(item.text)}>

View File

@ -1405,7 +1405,7 @@ function TracePanel(props: {
pagination={false} pagination={false}
title={() => `③ 排产产物 · 工单 ${s.workOrders.length}${s.productionOrders.length > 0 title={() => `③ 排产产物 · 工单 ${s.workOrders.length}${s.productionOrders.length > 0
? ` · PO:${s.productionOrders.map(p => `${p.orderNo}(${p.lineName || '未分线'})`).join('、')}` : ''}`} ? ` · PO:${s.productionOrders.map(p => `${p.orderNo}(${p.lineName || '未分线'})`).join('、')}` : ''}`}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="尚无工单(请先「试排一版」)" /> }} locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="尚无工单,请先生成试排方案。" /> }}
style={{ marginBottom: 12 }} style={{ marginBottom: 12 }}
/> />

View File

@ -401,7 +401,7 @@ export default function TimelineRail(props: {
{!flexWo.length && !versions.length && !cpGroups.length ? ( {!flexWo.length && !versions.length && !cpGroups.length ? (
<Empty <Empty
image={Empty.PRESENTED_IMAGE_SIMPLE} image={Empty.PRESENTED_IMAGE_SIMPLE}
description="还没有排产结构。对话说「试排一版」或打开柔性工作台。" description="当前没有排产结构。请生成试排方案,或打开柔性排产工作台。"
style={{ marginTop: 48 }} style={{ marginTop: 48 }}
/> />
) : ( ) : (

View File

@ -79,7 +79,7 @@ export default function FlexGanttView(props: {
useEffect(() => { dragRef.current = drag; }, [drag]); useEffect(() => { dragRef.current = drag; }, [drag]);
if (!data || data.workOrders.length === 0) if (!data || data.workOrders.length === 0)
return <div className="empty" ref={wrapRef}>当前暂无柔性排产工单<br />请先跑一版柔性排产,例如:跑一版柔性排产</div>; return <div className="empty" ref={wrapRef}>当前暂无柔性排产工单<br />请先执行“生成一版柔性排产方案”</div>;
if (viewMode === 'replay') { if (viewMode === 'replay') {
return ( return (

View File

@ -73,7 +73,7 @@ export default function GanttView(props: {
return ( return (
<div className="empty vp-empty-guide" ref={wrapRef}> <div className="empty vp-empty-guide" ref={wrapRef}>
<strong>尚无排产版本</strong> <strong>尚无排产版本</strong>
<span>在对话中说「试排一版交期优先」,或点欢迎页建议卡片。</span> <span>在对话中执行“生成一版交期优先试排方案”,或选择欢迎页建议卡片。</span>
<span className="faint">也可说「下一步建议」获取当前状态推荐动作。</span> <span className="faint">也可说「下一步建议」获取当前状态推荐动作。</span>
</div> </div>
); );

View File

@ -20,7 +20,7 @@ function fcTone(fc?: FlexCapacityForecast): string {
export default function PoolCapacityView(props: { data: FlexCapacityData | null }) { export default function PoolCapacityView(props: { data: FlexCapacityData | null }) {
const d = props.data; const d = props.data;
if (!d?.pools?.length) { if (!d?.pools?.length) {
return <div className="proj-empty">暂无产能池数据。先导入柔性设备/工序或跑一版柔性排产。</div>; return <div className="proj-empty">暂无产能池数据。请先导入柔性设备与工序,或生成柔性排产方案。</div>;
} }
const bn = d.bottleneckPool; const bn = d.bottleneckPool;
const alerts = d.alerts ?? []; const alerts = d.alerts ?? [];

View File

@ -1952,3 +1952,14 @@
- 关闭 pending 取消堆积线程、取消后 done/failed 覆盖、thread.start 失败泄漏配额三类竞态。 - 关闭 pending 取消堆积线程、取消后 done/failed 覆盖、thread.start 失败泄漏配额三类竞态。
- **验证**:专项 11 passed;相邻 20 passed;full golden 1358 passed / 2 warnings,受保护哈希不变;独立复审 `AUDIT PASS`。 - **验证**:专项 11 passed;相邻 20 passed;full golden 1358 passed / 2 warnings,受保护哈希不变;独立复审 `AUDIT PASS`。
- **边界**:下一步恢复 Sobol,并同时完成 Sidecar 依赖、日期复算、取消检查、前端入口;未 commit/push/merge/publish。 - **边界**:下一步恢复 Sobol,并同时完成 Sidecar 依赖、日期复算、取消检查、前端入口;未 commit/push/merge/publish。
## 2026-08-21 — 企业级排产回复文案与状态提示
- **类型**:fix + ux + tests + docs
- **做了什么**:统一助手系统提示词与离线回复为“结论 → 依据 → 阻断项/风险 → 下一步”;重写数据齐备度、排产向导、目录分析、目录导入和报告空态文案;欢迎页、快捷指令、空态和错误提示改为业务化表达。
- **界面行为**:`Thinking`、内部 intent 名称和开发环境错误细节默认不再展示;可通过 `VITE_SHOW_INTENT_DEBUG=1` 显式打开调试块。
- **涉及路径**:`server/agent_core/assistant.py`、`server/agent_core/dialog.py`、`server/agent_core/context.py`、`server/gateway/app.py`、`server/aps_domain/{readiness,guidance,folder_pack,reports,workflow}.py`、`apps/web/src/{chat/ChatPanel.tsx,App.tsx,auth/AuthGate.tsx,viewport,timeline,orders}`。
- **验证**:核心回复/向导/齐备度/引导测试 `37 passed`;全量黄金测试 `1156 passed, 20 skipped`,另有 7 项依赖外部 MES 网络或本机 MOM 文件的环境失败;`npm run build:web` 通过;`server` compileall 通过;本地 `8000/5173` 健康检查通过。
- **文档同步**:新增 `docs/development/response-style.md`。
- **遗留**:workflow 及集成模块中仍有少量历史成功回执使用勾选符号,后续可按规范继续收敛;未 commit/push/merge/publish。
---

View File

@ -73,7 +73,8 @@
2. [Harness 门禁](./architecture/harness.md) 2. [Harness 门禁](./architecture/harness.md)
3. [可重生模块](./architecture/modules.md) 3. [可重生模块](./architecture/modules.md)
4. [Plan 可重生运行时](./architecture/plan-runtime.md) 4. [Plan 可重生运行时](./architecture/plan-runtime.md)
5. 蓝图全文:[plan.md](../plan.md) 5. [对话回复规范](./development/response-style.md)
6. 蓝图全文:[plan.md](../plan.md)
### 算法 ### 算法

View File

@ -0,0 +1,30 @@
# 对话回复规范
## 定位
工业智核是面向计划员、生产经理和工厂管理者的排产决策助手。回复应帮助用户确认事实、理解风险并完成下一步操作,不以闲聊或娱乐为目标。
## 回复结构
业务回复优先采用以下顺序:
1. **结论**:直接回答当前问题,说明是否可以继续。
2. **依据**:列出关键数量、时间、资源和数据来源;数量必须带单位。
3. **阻断项/风险**:明确缺失数据、冲突和不确定性。
4. **下一步**:只给当前目标最相关的操作建议。
## 用词
- 使用“待排订单、可用设备、标准工时、数据齐备度、阻断项、试排方案”等业务术语。
- 避免“啥、咱们、哟、随便、我就上手、大白话、手头有啥”等口语表达。
- 不向用户暴露内部 intent 名称、模块名、异常堆栈或模型供应商名称。
- 不使用笑话、营销话术或 emoji 作为业务回复内容。
- 用户可以使用口语输入,但系统输出应保持专业、克制和可执行。
## 不确定性
没有可靠数据时必须明确说明“数据不足”或“当前无法确认”,同时指出需要补充的字段。不得用“应该、差不多、看起来可以”等模糊结论代替数据校验。
## 状态提示
处理中状态使用“正在确认目标”“正在分析项目数据”“正在生成排产方案”“正在整理结果”等业务描述。生产环境默认不显示内部调试信息;仅设置 `VITE_SHOW_INTENT_DEBUG=1` 时允许显示 intent 调试块。

View File

@ -23,18 +23,17 @@ HistoryItem = dict[str, str]
_WEEKDAYS = "一二三四五六日" _WEEKDAYS = "一二三四五六日"
_SYSTEM = """你是「工业智核」智能助手,对话能力对标 Kimi / 豆包:用户什么都会问,你都尽量答。 _SYSTEM = """你是「工业智核」企业级排产决策助手,服务对象是计划员、生产经理和工厂管理者。
身份:日常问答 + 工厂排产专家。 输出要求:
1. 先给结论,再给关键数据、阻断项/风险和下一步操作。
铁律: 2. 结合【对话上文】理解省略表达,但不要重复用户原话或暴露内部意图名。
1. **先看【对话上文】**,理解用户是在接上一句(比如「是的」「那个」),再回答。 3. 排产数字只能来自【项目上下文】;文件夹内容只能来自【文件夹清单】。
2. **先正面回答这句话**。问几点报时间;问文件夹就列文件;问排产就讲排产。 4. 所有数量、时长和日期都带单位或明确字段名;无法确认时直接说明“数据不足”。
3. 禁止说:「跟排产无关帮不上」「换个排产问题」「没配大模型像 Kimi 才行」。 5. 使用专业、克制、可执行的中文。避免“啥、咱们、哟、随便、我就上手”等口语,不使用笑话、营销话术或 emoji。
4. 说人话、短句;少用黑话,必须用时立刻白话解释。 6. 不要每轮粘贴功能清单;只给当前目标最相关的一个下一步。
5. 不要每轮粘贴功能清单。用 Markdown 排版。 7. 有【当前时间】时,问几点必须使用该时间。
6. 排产数字只能来自【项目上下文】;文件夹内容来自【文件夹清单】。 8. 只回答当前问题,不编造项目事实;没有依据时明确说明需要补充什么数据。
7. 有【当前时间】时,问几点必须用它。
""" """
@ -126,7 +125,7 @@ def _simple_math(t: str) -> str | None:
r = a * b r = a * b
elif op in ("/", "÷"): elif op in ("/", "÷"):
if b == 0: if b == 0:
return "除数不能是 0 哟。" return "除数不能为 0。"
r = a / b r = a / b
else: else:
return None return None
@ -139,24 +138,23 @@ def _simple_math(t: str) -> str | None:
def _explain_scheduling() -> str: def _explain_scheduling() -> str:
return ( return (
"**排产是啥?**\n\n" "## 什么是排产?\n\n"
"用大白话讲:工厂里有很多订单要做,每张单要经过几道工序、用哪台设备、" "排产是将订单需求转换为可执行的生产计划,明确产品、数量、工艺路线、"
"什么时候开工完工——**把这些事提前安排清楚,就叫排产**。\n\n" "设备资源、开工时间和完工时间,并在交期、产能和物料约束之间做出安排。\n\n"
"好比家里做饭:先想清楚先炒哪个菜、用哪口锅、几点能上桌。\n\n" "**一份可执行的排产计划至少需要:**\n"
"**排产一般要弄清三件事:**\n" "- **需求**:订单、产品、数量和交期\n"
"- **做什么**:哪些订单、做什么产品、做多少、啥时候要\n" "- **工艺**:工艺路线、标准工时和前后置关系\n"
"- **怎么做**:几道工序、每道大概多久\n" "- **资源**:设备能力、班次日历、模具和物料齐套状态\n\n"
"- **谁来做**:哪些设备/人能干这些活、哪天有空\n\n" "如果你提供当前项目数据,我可以先检查齐备度,再生成试排方案。"
"你要是想亲手试一版,跟我说一声「带我排一版」就行。"
) )
def _explain_can_schedule() -> str: def _explain_can_schedule() -> str:
return ( return (
"**会,这就是我的本职工作。**\n\n" "**可以。**\n\n"
"你把订单和现场情况告诉我,我可以帮你看缺不缺、排出一版计划。\n" "我可以检查订单、工艺、设备、班次和物料数据,识别排产阻断项,"
"日常问题(几点了、算个数、闲聊)也都能聊。\n\n" "并按交期、产能或瓶颈策略生成试排方案。\n\n"
"想现在就试?直接说「带我排一版」或「直接排一版」。" "建议先说「分析当前项目数据」,确认数据齐备度后再生成方案。"
) )
@ -180,9 +178,9 @@ def _offline_reply(
if _is_greeting(t): if _is_greeting(t):
return ( return (
"你好,我在。\n\n" "您好,我是工业智核排产助手。\n\n"
"啥都能问:几点了、算个数、看项目文件夹、排产排程……怎么说都行。\n\n" "我可以协助分析订单与生产数据、检查排产条件、生成试排方案并解释冲突。\n\n"
"想排产的话,说「带我排一版」我就上手。", "请直接描述目标,例如「分析当前项目数据」或「生成交期优先试排方案」。",
False, False,
) )
@ -202,8 +200,10 @@ def _offline_reply(
if re.search(r"你是谁|你能做什么|怎么用|如何使用|介绍一下|你会什么", t): if re.search(r"你是谁|你能做什么|怎么用|如何使用|介绍一下|你会什么", t):
return ( return (
"我是工业智核里的助手:**日常问题也能聊**,排产是我最拿手的。\n\n" "我是工业智核的排产决策助手。\n\n"
"可以说「现在几点了」「解析项目文件夹」「帮我分析项目数据」「带我排一版」。", "主要能力包括:分析项目数据、检查排产齐备度、生成和比较试排方案、"
"解释交期风险与产能瓶颈。\n\n"
"建议从「分析当前项目数据」开始。",
False, False,
) )
@ -213,16 +213,15 @@ def _offline_reply(
if re.search(r"讲个笑话|来个笑话|说个笑话", t): if re.search(r"讲个笑话|来个笑话|说个笑话", t):
return ( return (
"好,来一个车间版冷笑话:\n\n" "我目前专注于生产计划、排产数据和交期分析。\n\n"
"计划员问设备:「你今天忙不忙?」\n" "如果你有具体订单或现场问题,请直接描述,我会给出可执行的分析结果。",
"设备说:「不忙,我排期排到下下下个月了,早就躺平了。」",
False, False,
) )
if re.search(r"天气|下雨|降温|热不热", t): if re.search(r"天气|下雨|降温|热不热", t):
return ( return (
"我这边看不到窗外实时天气,手机天气最准。\n\n" "当前未接入实时天气数据,无法提供可靠天气判断。\n\n"
"要是担心交期被耽误,把单号跟我说,我帮你看计划。", "如果你担心天气影响交期,请提供订单号,我可以检查对应计划和缓冲时间。",
False, False,
) )
@ -235,29 +234,24 @@ def _offline_reply(
t, t,
): ):
return ( return (
"明白,你是要**直接开排**。\n\n" "我理解你的目标是生成排产方案。\n\n"
"请再说一次「给我排产」或点快捷指令「跑一版柔性排产」——" "请确认使用当前项目数据生成一版柔性试排方案;系统会先校验订单、工艺、"
"我会按工程目录/当前数据开排,不再让你选菜单。", "设备和班次条件,缺失数据会在结果中明确列出。",
False, False,
) )
if re.search(r"排产|订单|交期|设备|工艺|物料|工时", t) and re.search( if re.search(r"排产|订单|交期|设备|工艺|物料|工时", t) and re.search(
r"什么|怎么|如何|吗|呢|?|\?", t, r"什么|怎么|如何|吗|呢|?|\?", t,
): ):
return ( return (
f"你问的是「{t[:40]}」。\n\n" f"我理解你关注的是「{t[:40]}」。\n\n"
"若要**直接开排**,说「给我排产」;\n" "请明确需要执行的操作:生成试排方案、检查数据齐备度,或查看项目文件。",
"若要看齐不齐,说「帮我看看现在能不能排」;\n"
"若要看文件夹,说「解析项目文件夹」。",
False, False,
) )
# 开放问题:结合上文语气,不再甩「没配 Kimi」 # 开放问题:结合上文语气,不再甩「没配 Kimi」
return ( return (
f"关于「{t[:48]}」,我先按你的话理解一下。\n\n" f"我暂未识别到「{t[:48]}」对应的明确业务目标。\n\n"
"你要是想看**项目文件夹里有啥文件**,直接说「解析项目文件夹」;\n" "请补充以下任一信息:订单号或产品、期望交期、需要检查的数据,或希望采用的排产策略。",
"想看**排产数据齐不齐**,说「帮我分析一下当前项目里面的数据」;\n"
"想**开排**,说「带我排一版」。\n\n"
"也可以把具体想法再说详细一点,我按你的目标接着做。",
False, False,
) )
@ -288,7 +282,7 @@ async def reply(
or wants_folder_listing(q): or wants_folder_listing(q):
text, with_card = _offline_reply(world, q, session_id=session_id) text, with_card = _offline_reply(world, q, session_id=session_id)
if note and follow.get("kind") == "affirm": if note and follow.get("kind") == "affirm":
text = f"(明白,你是说「{q}」)\n\n" + text text = f"已根据上一轮确认,继续执行:{q}\n\n" + text
return AgentReply(text=text, blocks=[_readiness_block(world)] if with_card else []) return AgentReply(text=text, blocks=[_readiness_block(world)] if with_card else [])
provider = get_provider() provider = get_provider()
@ -317,6 +311,6 @@ async def reply(
text, with_card = _offline_reply(world, q, session_id=session_id) text, with_card = _offline_reply(world, q, session_id=session_id)
if note and follow.get("kind") == "affirm": if note and follow.get("kind") == "affirm":
text = f"(明白,按你说的「{q}」来)\n\n" + text text = f"已根据上一轮确认,继续执行:{q}\n\n" + text
blocks = [_readiness_block(world)] if with_card else [] blocks = [_readiness_block(world)] if with_card else []
return AgentReply(text=text, blocks=blocks) return AgentReply(text=text, blocks=blocks)

View File

@ -87,7 +87,7 @@ def resolve_followup(text: str, history: list[HistoryItem]) -> dict[str, Any]:
"text": t, "text": t,
"note": "用户拒绝了上一轮建议", "note": "用户拒绝了上一轮建议",
"kind": "deny", "kind": "deny",
"reply": "好,那先不动。你想到啥随时说,我接着帮你。", "reply": "已取消本次操作,当前数据未修改。需要继续时,请重新描述目标。",
} }
if _AFFIRM.match(t) and prev_agent: if _AFFIRM.match(t) and prev_agent:

View File

@ -118,13 +118,13 @@ def make_clarification(session_id: str, text: str, intent: IntentResult) -> Agen
for c in candidates], for c in candidates],
"rounds": prev_rounds + 1, "rounds": prev_rounds + 1,
} }
lines = [f"我不太确定你的意思,你是想:(回复序号即可)"] lines = [f"请确认需要执行的操作(回复序号即可):"]
for i, c in enumerate(candidates, start=1): for i, c in enumerate(candidates, start=1):
lines.append(f"{i}. {c[1]}") lines.append(f"{i}. {c[1]}")
lines.append("都不是的话,请换个说法补充要点。") lines.append("如果以上选项均不符合,请补充订单、产品或目标操作。")
block = UIBlock( block = UIBlock(
blockId=f"clarify-{session_id[:8]}", type="clarify", blockId=f"clarify-{session_id[:8]}", type="clarify",
props={"question": "我不太确定你的意思,你是想:", props={"question": "请确认需要执行的操作:",
"options": [{"index": i + 1, "intent": c[0], "label": c[1]} "options": [{"index": i + 1, "intent": c[0], "label": c[1]}
for i, c in enumerate(candidates)], for i, c in enumerate(candidates)],
"originalText": text}) "originalText": text})
@ -165,7 +165,7 @@ def _resolve_clarify(session_id: str, text: str) -> IntentResult | None:
# ② 引导式排产向导 # ② 引导式排产向导
# ============================================================ # ============================================================
_WIZ_CANCEL = r"取消|退出|不排了|算了" _WIZ_CANCEL = r"取消|退出|不排了|算了"
_WIZ_GO = r"^(排|开始|好|可以|确认|继续|继续排产|go)$" _WIZ_GO = r"^(排|开始|好|可以|确认|确认试排|继续|继续排产|go)$"
def start_wizard(store, session_id: str, actor: str = "web") -> AgentReply: def start_wizard(store, session_id: str, actor: str = "web") -> AgentReply:
@ -188,16 +188,15 @@ def _wizard_advance(store, session_id: str, actor: str) -> AgentReply:
# 无订单 → 引导录入订单 # 无订单 → 引导录入订单
if s["total"] == 0: if s["total"] == 0:
st["wizard"] = None st["wizard"] = None
return AgentReply(text="当前没有待排订单。先录入订单吧——可以说" return AgentReply(text="当前没有待排订单,无法启动排产向导。\n"
"「导入订单」(Excel 批量)或「新建订单 产品X 数量100 交期…」," "请先导入订单表(Excel 批量)或新建订单;完成后再次执行“开始排产”。")
"录完再说「我要排产」。")
# 缺日历 → 给默认日历选项 # 缺日历 → 给默认日历选项
if any(gi["type"] == "NO_CALENDAR" for gi in report["globalIssues"]): if any(gi["type"] == "NO_CALENDAR" for gi in report["globalIssues"]):
st["wizard"] = {"step": "fix_calendar"} st["wizard"] = {"step": "fix_calendar"}
return AgentReply(text="缺班次日历(不知道每天几点开工、一周开几天)。\n" return AgentReply(text="缺少班次日历,无法计算可用生产时段。\n"
"回复「默认日历」建单班 08:00-17:00(午休1h)周一~周五;" "请选择“默认日历”(单班 08:00-17:00,午休 1 小时,周一至周五),"
"或告诉我你们的班制(如「两班 8点到23点 周一到周六」)。", "或提供实际班制(例如“两班制,08:00-23:00,周一至周六”)。",
blocks=[_wizard_block("fix_calendar", "缺班次日历", blocks=[_wizard_block("fix_calendar", "缺班次日历",
["默认日历", "自定义班制"])]) ["默认日历", "自定义班制"])])
@ -222,14 +221,14 @@ def _wizard_advance(store, session_id: str, actor: str) -> AgentReply:
tpls = [] tpls = []
st["wizard"] = {"step": "pick_template", "productCode": pc, "productName": pname, st["wizard"] = {"step": "pick_template", "productCode": pc, "productName": pname,
"templates": [t["code"] for t in tpls]} "templates": [t["code"] for t in tpls]}
lines = [f"订单 {target['orderNo']} 的产品「{pname}」还没有工艺路线。" lines = [f"订单 {target['orderNo']} 的产品「{pname}」尚未配置工艺路线。"
f"根据产品特征,推荐这些行业模板:(回复序号采用)"] f"以下是根据产品特征匹配的行业模板,请回复序号选择:"]
for i, t in enumerate(tpls, start=1): for i, t in enumerate(tpls, start=1):
steps_brief = "→".join(x["operationName"] for x in t["steps"][:6]) steps_brief = "→".join(x["operationName"] for x in t["steps"][:6])
more = "…" if len(t["steps"]) > 6 else "" more = "…" if len(t["steps"]) > 6 else ""
total = sum(x["stdMinDefault"] for x in t["steps"]) total = sum(x["stdMinDefault"] for x in t["steps"])
lines.append(f"{i}. {t['name']}:{steps_brief}{more}(约 {total:.0f} 分/件)") lines.append(f"{i}. {t['name']}:{steps_brief}{more}(约 {total:.0f} 分/件)")
lines.append("都不合适可以回复「跳过」,稍后手工维护路线。") lines.append("如果没有合适模板,请回复“跳过”,稍后在主数据中维护工艺路线。")
return AgentReply(text="\n".join(lines), return AgentReply(text="\n".join(lines),
blocks=[_wizard_block("pick_template", f"为 {pname} 选工艺模板", blocks=[_wizard_block("pick_template", f"为 {pname} 选工艺模板",
[f"{i}. {t['name']}" for i, t in enumerate(tpls, 1)] + ["跳过"], [f"{i}. {t['name']}" for i, t in enumerate(tpls, 1)] + ["跳过"],
@ -254,9 +253,9 @@ def _wizard_advance(store, session_id: str, actor: str) -> AgentReply:
if queue: if queue:
pc, op = queue[0] pc, op = queue[0]
st["wizard"] = {"step": "fill_time", "queue": queue} st["wizard"] = {"step": "fill_time", "queue": queue}
return AgentReply(text=f"还有 {len(queue)} 道工序没有工时。先补第一条:\n" return AgentReply(text=f"还有 {len(queue)} 道工序缺少标准工时。当前需要补充:\n"
f"产品 {pc} 的工序「{op}」单件多少分钟?" f"产品 {pc} 的工序「{op}」单件多少分钟?"
f"回复数字(如「45」),或回复「跳过」逐条略过。", f"请回复分钟数(如“45”),或回复“跳过”暂不维护。",
blocks=[_wizard_block("fill_time", f"补工时:{pc} × {op}", blocks=[_wizard_block("fill_time", f"补工时:{pc} × {op}",
["跳过", "取消"])]) ["跳过", "取消"])])
@ -267,18 +266,17 @@ def _wizard_advance(store, session_id: str, actor: str) -> AgentReply:
st["wizard"] = None st["wizard"] = None
details = ";".join(i["detail"] for r in no_eq[:3] for i in r["issues"] details = ";".join(i["detail"] for r in no_eq[:3] for i in r["issues"]
if i["type"] == "NO_CAPABLE_EQUIPMENT") if i["type"] == "NO_CAPABLE_EQUIPMENT")
return AgentReply(text=f"还有工序没有能力设备:{details}。\n" return AgentReply(text=f"以下工序尚未匹配到可用设备:{details}。\n"
f"请在主数据页为设备登记工序能力(capabilities)," f"请在主数据页补充设备工序能力,完成后再次执行“检查数据齐备度”。")
f"或说「设备 XX 增加能力 工序YY」;完成后再说「我要排产」。")
# 全绿(或仅剩警告)→ 试排确认 # 全绿(或仅剩警告)→ 试排确认
warn_note = "" warn_note = ""
if s["withWarnings"]: if s["withWarnings"]:
warn_note = f"({s['withWarnings']} 张订单带推断工时/缺料告警,结果会显式标注)" warn_note = f"({s['withWarnings']} 张订单带推断工时/缺料告警,结果会显式标注)"
st["wizard"] = {"step": "confirm_run"} st["wizard"] = {"step": "confirm_run"}
return AgentReply(text=f"数据齐备 ✅ {s['total']} 张订单可排{warn_note}。\n" return AgentReply(text=f"数据齐备:{s['total']} 张订单可以生成试排方案{warn_note}。\n"
f"回复「排」立即按瓶颈锚模式试排;回复「取消」退出向导。", f"回复“确认试排”按瓶颈锚策略生成方案;回复“取消”退出向导。",
blocks=[_wizard_block("confirm_run", "数据齐备,可以试排", ["排", "取消"])]) blocks=[_wizard_block("confirm_run", "数据齐备,可以生成试排方案", ["确认试排", "取消"])])
def _wizard_block(step: str, title: str, options: list[str], def _wizard_block(step: str, title: str, options: list[str],
@ -298,7 +296,7 @@ def _wizard_turn(store, session_id: str, text: str, actor: str) -> AgentReply |
if re.search(_WIZ_CANCEL, t): if re.search(_WIZ_CANCEL, t):
st["wizard"] = None st["wizard"] = None
return AgentReply(text="已退出排产向导。随时说「我要排产」重新开始。") return AgentReply(text="已退出排产向导。需要时可重新执行“开始排产”。")
if step == "fix_calendar": if step == "fix_calendar":
if re.search(r"默认日历|默认|单班", t): if re.search(r"默认日历|默认|单班", t):
@ -310,14 +308,14 @@ def _wizard_turn(store, session_id: str, text: str, actor: str) -> AgentReply |
power="P1", rationale={"wizard": True, "calendar": "默认单班"}) power="P1", rationale={"wizard": True, "calendar": "默认单班"})
store.save() store.save()
reply = _wizard_advance(store, session_id, actor) reply = _wizard_advance(store, session_id, actor)
reply.text = "已建默认日历(单班 08:00-17:00,周一~五)✅\n\n" + reply.text reply.text = "已建立默认班次日历(单班 08:00-17:00,周一至周五)。\n\n" + reply.text
return reply return reply
return AgentReply(text="没听懂班制。回复「默认日历」,或稍后在主数据页维护后再说「继续排产」。") return AgentReply(text="未识别到有效班制。请选择“默认日历”,或提供班次数量、工作时间和工作日。")
if step == "pick_template": if step == "pick_template":
if re.search(r"^跳过$", t): if re.search(r"^跳过$", t):
st["wizard"] = {"step": "resume"} st["wizard"] = {"step": "resume"}
return AgentReply(text="已跳过该产品。稍后可在主数据页维护路线;说「继续排产」推进其它检查。") return AgentReply(text="已跳过该产品的工艺路线。请在主数据中维护后,再执行“继续排产”。")
codes = wiz.get("templates") or [] codes = wiz.get("templates") or []
idx = None idx = None
m = re.match(r"^(?:第?\s*([1-9一二三])\s*个?)", t) m = re.match(r"^(?:第?\s*([1-9一二三])\s*个?)", t)
@ -333,7 +331,7 @@ def _wizard_turn(store, session_id: str, text: str, actor: str) -> AgentReply |
tpl_code = c tpl_code = c
break break
if not tpl_code: if not tpl_code:
return AgentReply(text="请回复模板序号(1/2/3)、「跳过」或「取消」。") return AgentReply(text="请选择模板序号(1/2/3),或回复“跳过”“取消”。")
pc = wiz.get("productCode") or "" pc = wiz.get("productCode") or ""
pname = wiz.get("productName") or pc pname = wiz.get("productName") or pc
from server.knowledge.routing_templates import get_template from server.knowledge.routing_templates import get_template
@ -352,7 +350,7 @@ def _wizard_turn(store, session_id: str, text: str, actor: str) -> AgentReply |
rationale={"confirmId": block.props["confirmId"], "productCode": pc, "wizard": True}) rationale={"confirmId": block.props["confirmId"], "productCode": pc, "wizard": True})
store.save() store.save()
st["wizard"] = {"step": "resume"} st["wizard"] = {"step": "resume"}
return AgentReply(text=f"{title}——需要你确认(P2)。批准后说「继续排产」推进下一步。", return AgentReply(text=f"{title}需要确认(P2)。确认后执行“继续排产”推进下一步。",
blocks=[block]) blocks=[block])
if step == "fill_time": if step == "fill_time":
@ -365,11 +363,11 @@ def _wizard_turn(store, session_id: str, text: str, actor: str) -> AgentReply |
queue.pop(0) queue.pop(0)
st["wizard"] = {"step": "fill_time", "queue": queue} if queue else {"step": "resume"} st["wizard"] = {"step": "fill_time", "queue": queue} if queue else {"step": "resume"}
if queue: if queue:
return AgentReply(text=f"已跳过。下一条:产品 {queue[0][0]} 的工序「{queue[0][1]}」单件多少分钟?") return AgentReply(text=f"已跳过当前工序。下一项:产品 {queue[0][0]} 的工序「{queue[0][1]}」单件多少分钟?")
return _wizard_advance(store, session_id, actor) return _wizard_advance(store, session_id, actor)
m = re.search(r"(\d+(?:\.\d+)?)", t) m = re.search(r"(\d+(?:\.\d+)?)", t)
if not m: if not m:
return AgentReply(text=f"请回复分钟数(如「45」)、「跳过」或「取消」。当前:{pc} × {op}") return AgentReply(text=f"请回复标准工时(分钟/件),或回复“跳过”“取消”。当前:{pc} × {op}")
std_min = float(m.group(1)) std_min = float(m.group(1))
title = f"更新工时:{pc} × {op} → {std_min} 分钟/件" title = f"更新工时:{pc} × {op} → {std_min} 分钟/件"
block = harness.stage_confirmation( block = harness.stage_confirmation(
@ -385,7 +383,7 @@ def _wizard_turn(store, session_id: str, text: str, actor: str) -> AgentReply |
st["wizard"] = {"step": "fill_time", "queue": queue} if queue else {"step": "resume"} st["wizard"] = {"step": "fill_time", "queue": queue} if queue else {"step": "resume"}
nxt = (f"\n下一条:产品 {queue[0][0]} 的工序「{queue[0][1]}」单件多少分钟?" nxt = (f"\n下一条:产品 {queue[0][0]} 的工序「{queue[0][1]}」单件多少分钟?"
if queue else "\n工时都过了一遍。批准确认卡后说「继续排产」。") if queue else "\n工时都过了一遍。批准确认卡后说「继续排产」。")
return AgentReply(text=f"{title}——已出确认卡(P2)。{nxt}", blocks=[block]) return AgentReply(text=f"{title}已生成确认卡(P2)。{nxt}", blocks=[block])
if step == "confirm_run": if step == "confirm_run":
if re.match(_WIZ_GO, t) or re.search(r"^试排|排产$", t): if re.match(_WIZ_GO, t) or re.search(r"^试排|排产$", t):

View File

@ -982,20 +982,20 @@ def _to_markdown(
lines.append("") lines.append("")
if can_schedule: if can_schedule:
lines.append("**结论:按文件内容看,基本具备开排条件。**") lines.append("**结论:按文件内容看,已基本满足排产条件。**")
lines.append("你可以说「根据这些数据排产」——我先收进系统,再校验后试排。") lines.append("下一步:确认导入这些数据,系统将继续校验并生成试排方案。")
else: else:
lines.append("**结论:现在还不能稳妥开排。**") lines.append("**结论:当前不满足直接排产条件。**")
lines.append("还缺:" + ("、".join(missing) if missing else "有效数据行")) lines.append("缺失项:" + ("、".join(missing) if missing else "有效数据行"))
lines.append("把缺的表补进工程目录(或改好报错行)后再叫我。") lines.append("请补充缺失数据或修正错误记录后重新执行目录分析。")
ws = (world_ready or {}).get("summary") or {} ws = (world_ready or {}).get("summary") or {}
if ws: if ws:
lines += [ lines += [
"", "",
f"(系统里当前已有待排单 {ws.get('total', 0)} 张," f"(系统当前已有待排订单 {ws.get('total', 0)} 张,"
f"可排 {ws.get('ready', 0)},卡住 {ws.get('blocked', 0)}。" f"可排 {ws.get('ready', 0)} 张,存在阻断 {ws.get('blocked', 0)} 张。"
f"导入目录后会按新数据重新看。)", f"导入目录后将基于新数据重新校验。)",
] ]
return "\n".join(lines) return "\n".join(lines)
@ -1010,5 +1010,5 @@ def confirmation_for_folder_schedule(report: dict[str, Any]) -> tuple[str, list[
n = (report.get("kindCounts") or {}).get(k, 0) n = (report.get("kindCounts") or {}).get(k, 0)
if n: if n:
lines.append(f"· {KIND_CN.get(k, k)} {n} 行") lines.append(f"· {KIND_CN.get(k, k)} {n} 行")
lines.append("确认后写入柔性主数据并试排一版(可回滚)") lines.append("确认后写入柔性主数据并生成试排方案(支持回滚)")
return title, lines return title, lines

View File

@ -35,8 +35,8 @@ def build_guidance(world: World, *, context: str | None = None) -> dict[str, Any
"reason": "甘特为空,需先试排生成草稿。", "reason": "甘特为空,需先试排生成草稿。",
}) })
suggestions.append({ suggestions.append({
"id": "run_delivery", "label": "试排一版交期优先", "id": "run_delivery", "label": "生成交期优先试排方案",
"command": "试排一版交期优先", "priority": 10, "command": "生成一版交期优先试排方案", "priority": 10,
"reason": "无版本时空态首选", "reason": "无版本时空态首选",
}) })
suggestions.append({ suggestions.append({
@ -87,8 +87,8 @@ def build_guidance(world: World, *, context: str | None = None) -> dict[str, Any
"reason": "能力池/虚拟产线需跑柔性排产。", "reason": "能力池/虚拟产线需跑柔性排产。",
}) })
suggestions.append({ suggestions.append({
"id": "flex_run", "label": "跑一版柔性排产", "id": "flex_run", "label": "生成柔性排产方案",
"command": "跑一版柔性排产", "priority": 12, "command": "生成一版柔性排产方案", "priority": 12,
"reason": "柔性轨空态", "reason": "柔性轨空态",
}) })
@ -124,7 +124,7 @@ def build_guidance(world: World, *, context: str | None = None) -> dict[str, Any
if not ranked: if not ranked:
ranked = [{ ranked = [{
"id": "help", "label": "看看能做什么", "id": "help", "label": "查看可用操作",
"command": "帮助", "priority": 1, "command": "帮助", "priority": 1,
"reason": "默认兜底", "reason": "默认兜底",
}] }]

View File

@ -155,41 +155,47 @@ def check_readiness(world: World, order_ids: list[int] | None = None) -> dict[st
def readiness_text(report: dict[str, Any]) -> str: def readiness_text(report: dict[str, Any]) -> str:
"""齐备度报告 → 人话(车间能听懂)。""" """齐备度报告:结论、关键指标、阻断项与下一步。"""
s = report["summary"] s = report["summary"]
if not s["total"]:
conclusion = "当前没有待排订单,暂时无法生成试排方案。"
elif s["blocked"]:
conclusion = f"当前有 {s['blocked']} 张订单存在阻断项,暂不建议直接排产。"
else:
conclusion = f"当前 {s['total']} 张订单已通过基础齐备度检查,可以生成试排方案。"
lines = [ lines = [
f"我帮你看了看:一共 {s['total']} 张要排的单子," "## 数据齐备度",
f"大概 {s['ready']} 张能直接开排," f"**结论:{conclusion}**",
f"{s['blocked']} 张还卡着," "",
f"{s['withWarnings']} 张能排但有点风险。", f"- 待排订单:{s['total']} 张;可直接排产:{s['ready']} 张;存在阻断:{s['blocked']} 张。",
f"工时这边:一共 {s['timeTotal']} 道步骤," f"- 标准工时:共 {s['timeTotal']} 道工序;待维护 {s['timePending']} 道;推断值 {s['timeInferred']} 道。",
f"还有 {s['timePending']} 道没填好,"
f"{s['timeInferred']} 道是估出来的(不太准)。",
] ]
if s["withWarnings"]:
lines.append(f"- 风险提示:{s['withWarnings']} 张订单包含推断工时或缺料告警。")
for gi in report["globalIssues"]: for gi in report["globalIssues"]:
lines.append(f"⚠ 先卡住了:{gi['detail']}") lines.append(f"- **阻断项**:{gi['detail']}")
shown = 0 shown = 0
for r in report["orders"]: for r in report["orders"]:
if not r["issues"] or shown >= 6: if not r["issues"] or shown >= 6:
continue continue
mark = "还不行" if not r["ready"] else "要注意" mark = "阻断" if not r["ready"] else "风险"
heads = ";".join(i["detail"] for i in r["issues"][:3]) heads = ";".join(i["detail"] for i in r["issues"][:3])
more = f"(另外还有 {len(r['issues']) - 3} 处)" if len(r["issues"]) > 3 else "" more = f"(另外还有 {len(r['issues']) - 3} 处)" if len(r["issues"]) > 3 else ""
lines.append(f"· {mark} · 单号 {r['orderNo']}:{heads}{more}") lines.append(f"- **{mark}** · 订单 {r['orderNo']}:{heads}{more}")
shown += 1 shown += 1
rest = sum(1 for r in report["orders"] if r["issues"]) - shown rest = sum(1 for r in report["orders"] if r["issues"]) - shown
if rest > 0: if rest > 0:
lines.append(f"……还有 {rest} 张单子也有类似问题,需要的话我再细说。") lines.append(f"- 另有 {rest} 张订单存在类似问题,详情可在订单齐备度明细中查看。")
if s["timePending"] or s["timeInferred"]: if s["timePending"] or s["timeInferred"]:
lines.append("")
lines.append( lines.append(
"工时不会填也没关系,你可以跟我说:" "**建议操作**:补充待维护工时,或提供实测工时(例如“将工序 OP-10 设置为 45 分钟/件”)。"
"「把某某工序改成多少分钟」,我按你说的记下来。"
) )
return "\n".join(lines) return "\n".join(lines)
def analyze_project_data(world: World, query: str = "") -> str: def analyze_project_data(world: World, query: str = "") -> str:
"""分析当前项目/附件:用人话盘点 → 缺啥 → 下一步怎么补。""" """分析当前项目/附件:结论 → 数据概览 → 阻断项 → 建议操作。"""
lines: list[str] = [] lines: list[str] = []
q = query or "" q = query or ""
if "【文件解析】" in q: if "【文件解析】" in q:
@ -199,11 +205,12 @@ def analyze_project_data(world: World, query: str = "") -> str:
chunk = chunk.split("\n\n请", 1)[0].strip() chunk = chunk.split("\n\n请", 1)[0].strip()
elif "\n\n给我" in chunk: elif "\n\n给我" in chunk:
chunk = chunk.split("\n\n给我", 1)[0].strip() chunk = chunk.split("\n\n给我", 1)[0].strip()
lines.append("你刚附的表格,我帮你过了一遍:") lines.append("## 附件解析结果")
lines.append("已完成附件的初步解析:")
lines.extend(chunk.splitlines()[:40]) lines.extend(chunk.splitlines()[:40])
lines.append("") lines.append("")
if "可入库" in chunk: if "可入库" in chunk:
lines.append("里面有一部分已经核对过了,可以收进系统;有问题的行我标出来了,你改完再传也行。") lines.append("部分记录已通过校验,可以提交入库;错误记录需要修正后重新上传。")
lines.append("") lines.append("")
active = [o for o in (world.get("flexOrders") or []) active = [o for o in (world.get("flexOrders") or [])
@ -214,11 +221,11 @@ def analyze_project_data(world: World, query: str = "") -> str:
cal = world.get("flexCalendar") or [] cal = world.get("flexCalendar") or []
bom = world.get("flexBom") or [] bom = world.get("flexBom") or []
lines.append("**先说说咱们手头有啥:**") lines.append("## 项目数据概览")
lines.append( lines.append(
f"- 要排的单子 **{len(active)}** 张;材料/产品档案 **{len(mats)}** 条;" f"- 待排订单:**{len(active)}** 张;产品/物料:**{len(mats)}** 条;"
f"用料清单 **{len(bom)}** 条;工艺步骤 **{len(routes)}** 条;" f"BOM 明细:**{len(bom)}** 条;工艺步骤:**{len(routes)}** 条;"
f"能开着干活的设备 **{len(equip)}** 台;班次 **{len(cal)}** 条。" f"可用设备:**{len(equip)}** 台;班次日历:**{len(cal)}** 条。"
) )
if active: if active:
preview = "、".join( preview = "、".join(
@ -226,9 +233,9 @@ def analyze_project_data(world: World, query: str = "") -> str:
for o in active[:5] for o in active[:5]
) )
more = f"……一共 {len(active)} 张" if len(active) > 5 else "" more = f"……一共 {len(active)} 张" if len(active) > 5 else ""
lines.append(f"- 比如这些单:{preview}{more}") lines.append(f"- 订单示例:{preview}{more}")
else: else:
lines.append("- 这会儿还没有要排的单子。你可以附一张订单表,或者说「新建订单」。") lines.append("- 待排订单为 0 张。请先导入订单表或新建订单。")
report = check_readiness(world) report = check_readiness(world)
lines.append("") lines.append("")
@ -236,20 +243,20 @@ def analyze_project_data(world: World, query: str = "") -> str:
asks: list[str] = [] asks: list[str] = []
if not active: if not active:
asks.append("来一份订单表(单号、做什么产品、做多少、什么时候要)") asks.append("导入订单表(至少包含单号、产品、数量和交期)")
if not mats: if not mats:
asks.append("来一份产品/材料名录(至少把要做的产品写上)") asks.append("补充产品/物料主数据(至少包含待排产品)")
if not routes: if not routes:
asks.append("告诉我产品怎么做(工艺步骤),或者说「带我排一版」让我按常用办法生成") asks.append("补充工艺路线和工序顺序,或请求系统推荐行业模板")
if not equip: if not equip:
asks.append("告诉我有哪些能干活的设备、各自会做什么工序") asks.append("补充可用设备及其工序能力")
if not cal: if not cal:
asks.append("告诉我一天上几班、几点开工;或者说「带我排一版」用常用班次") asks.append("补充班次日历(工作日、班次和起止时间)")
s = report["summary"] s = report["summary"]
if s.get("timePending"): if s.get("timePending"):
asks.append(f"把还没填的工时补上(现在大概还有 {s['timePending']} 道)") asks.append(f"补充 {s['timePending']} 道工序的标准工时")
if s.get("blocked"): if s.get("blocked"):
asks.append(f"把卡住的 {s['blocked']} 张单子问题处理掉(缺产品、缺工艺、或缺设备)") asks.append(f"处理 {s['blocked']} 张订单的阻断项(产品、工艺或设备能力缺失)")
seen: set[str] = set() seen: set[str] = set()
uniq = [] uniq = []
@ -260,12 +267,13 @@ def analyze_project_data(world: World, query: str = "") -> str:
lines.append("") lines.append("")
if uniq: if uniq:
lines.append("**想顺利开排,还差这些:**") lines.append("## 阻断项与建议操作")
for i, a in enumerate(uniq, 1): for i, a in enumerate(uniq, 1):
lines.append(f"{i}. {a}") lines.append(f"{i}. {a}")
lines.append("你补一点,我就帮你看一点;齐了直接说「直接排一版」或「带我排一版」。") lines.append("完成上述补充后,请再次执行“检查数据齐备度”;通过后即可生成试排方案。")
else: else:
lines.append("看起来东西比较齐了。你要是愿意,直接跟我说「直接排一版」,我马上给你出一稿。") lines.append("## 建议操作")
lines.append("数据已满足基础排产条件。可以选择排产策略并生成试排方案。")
return "\n".join(lines) return "\n".join(lines)

View File

@ -203,7 +203,7 @@ def build_plan_report(world: dict[str, Any], order_no: str | None = None) -> dic
if not daily.get("reportId"): if not daily.get("reportId"):
return { return {
"reportId": None, "title": "排产方案报告", "reportId": None, "title": "排产方案报告",
"markdown": "尚无排产版本。请先说「跑一版柔性排产」或「试排一版」。", "markdown": "尚无排产版本。请先生成柔性排产方案或交期优先试排方案。",
"snapshot": None, "xlsxBytes": None, "filename": None, "snapshot": None, "xlsxBytes": None, "filename": None,
} }
daily["title"] = daily["title"].replace("排产日报", "排产方案报告") daily["title"] = daily["title"].replace("排产日报", "排产方案报告")

View File

@ -34,8 +34,8 @@ _HELP = "\n".join([
"可执行的排产能力:", "可执行的排产能力:",
"· 查询现场数据:有哪些订单 / 查订单 102285668 / 这个订单有委外吗 / 查物料 / 查 BOM / 查工艺路线", "· 查询现场数据:有哪些订单 / 查订单 102285668 / 这个订单有委外吗 / 查物料 / 查 BOM / 查工艺路线",
"· 知识库:机加工工艺怎么生成 / 车削工艺模式 / 装配工艺模式 / 知识清单 / 导入知识文档", "· 知识库:机加工工艺怎么生成 / 车削工艺模式 / 装配工艺模式 / 知识清单 / 导入知识文档",
"· 生成排产草案:试排一版交期优先 / 产能均衡重排 / 用规则引擎试排", "· 生成排产方案:交期优先试排 / 产能均衡重排 / 规则引擎试排",
"· 柔性排产(能力池+虚拟产线):跑一版柔性排产 / 把这个订单排产 / 正排 / 倒排 / 瓶颈锚", "· 柔性排产(能力池+虚拟产线):生成柔性排产方案 / 按订单排产 / 正排 / 倒排 / 瓶颈锚",
"· 外部算法:用外部算法试排 / 查看已接入算法 / skill 健康检查", "· 外部算法:用外部算法试排 / 查看已接入算法 / skill 健康检查",
"· 瓶颈产能:瓶颈产能 / 各工序能做多少(按能力池实时算)", "· 瓶颈产能:瓶颈产能 / 各工序能做多少(按能力池实时算)",
"· 交期承诺:某产品 N 套什么时候能交", "· 交期承诺:某产品 N 套什么时候能交",
@ -1214,31 +1214,20 @@ def execute_confirmed(store: WorldStore, confirm_id: str, approve: bool, actor:
detail = "、".join(f"{k}×{v}" for k, v in (applied.get("summary") or {}).items()) or ( detail = "、".join(f"{k}×{v}" for k, v in (applied.get("summary") or {}).items()) or (
"SQL 已入库" if sql_already else "无行" "SQL 已入库" if sql_already else "无行"
) )
if kangni_detected: head = f"工程目录已导入({detail})。"
ver = (result or {}).get("versionNo") or "" # 有订单+工艺+设备即可开排(工时推断/缺 BOM 不拦死)
production_ready = bool(resource_quality.get("productionReady")) has_core = (
shared_hint = ( bool(store.data.get("flexOrders"))
"当前使用共享占位设备," and bool(store.data.get("flexRoutings"))
if resource_quality.get("sharedPlaceholder") else "" and any(e.get("status") == "RUNNING" for e in (store.data.get("flexEquipment") or []))
) )
if production_ready: if ready["summary"].get("ready", 0) == 0 and not has_core:
ready_line = ( return head + "\n\n导入后仍不具备开排条件:\n" + readiness_text(ready)
"productionReady=true;资源映射已齐。" from server.aps_domain.flex import run_flex_schedule
"本步仍未下发 MES,正式发布还需可信供应与 P3 确认。" result = run_flex_schedule(
) store, sort_mode=params.get("sortMode") or "BOTTLENECK", actor=actor, trial=True)
else:
ready_line = (
f"productionReady=false;{shared_hint}结果仅试排,不得生产发布或下发 MES。"
)
return (
f"康尼冻结数据已写入候选世界并完成本地试排 ✅({detail})。\n"
f"已生成 trial 版本:**{ver}**(虚拟产线 {(result or {}).get('vlCount', 0)} · "
f"工单 {(result or {}).get('woCount', 0)} · 冲突 {(result or {}).get('conflictCount', 0)})。"
f"{ready_line}"
)
head = f"工程目录已导入 ✅({detail})。"
ver = (result or {}).get("versionNo") or "" ver = (result or {}).get("versionNo") or ""
return (head + f"\n已按目录数据试排一版:**{ver}**" return (head + f"\n已按目录数据生成试排方案:**{ver}**"
f"(虚拟产线 {(result or {}).get('vlCount', 0)} · " f"(虚拟产线 {(result or {}).get('vlCount', 0)} · "
f"冲突 {(result or {}).get('conflictCount', 0)})。" f"冲突 {(result or {}).get('conflictCount', 0)})。"
"可在右侧柔性工作台看甘特,或再说「查看冲突」。") "可在右侧柔性工作台看甘特,或再说「查看冲突」。")
@ -1916,7 +1905,7 @@ def _run_flex(store: WorldStore, intent: IntentResult, actor: str) -> AgentReply
) )
else: else:
text = ( text = (
f"柔性排产完成 ✅ {focus_hint}版本 {result['versionNo']}({mode_cn}·{win_cn}):" f"柔性排产完成。{focus_hint}版本 {result['versionNo']}({mode_cn}·{win_cn}):"
f"{result['vlCount']} 条虚拟产线 / {result['woCount']} 个工单,冲突 {result['conflictCount']} 项" f"{result['vlCount']} 条虚拟产线 / {result['woCount']} 个工单,冲突 {result['conflictCount']} 项"
+ (f",窗外延期 {deferred}" if deferred else "") + (f",窗外延期 {deferred}" if deferred else "")
+ f"。{dl_hint}" + f"。{dl_hint}"
@ -3354,16 +3343,16 @@ async def handle_intent(store: WorldStore, session_id: str, intent: IntentResult
if not report.get("batches") and not report.get("sqlApplied") and not kangni_trial: if not report.get("batches") and not report.get("sqlApplied") and not kangni_trial:
return AgentReply( return AgentReply(
text=(report.get("markdown") or "") text=(report.get("markdown") or "")
+ "\n\n目录里还没有可入库的有效行,我没法按这些表开排。" + "\n\n目录中没有可入库的有效记录,当前无法生成排产方案。"
"请先把订单/物料/工艺/设备表或 SQL 库补齐。", "请补充订单、物料、工艺或设备数据。",
blocks=[analyze_block], blocks=[analyze_block],
) )
if report.get("missing") and not has_core and not kangni_trial: if report.get("missing") and not has_core and not kangni_trial:
return AgentReply( return AgentReply(
text=(report.get("markdown") or "") text=(report.get("markdown") or "")
+ "\n\n**还不能排:**还缺 " + "\n\n**当前无法排产:**缺少 "
+ "、".join(report["missing"]) + "、".join(report["missing"])
+ "。补齐后再说「根据这些数据排产」。", + "。补齐后请重新执行目录分析。",
blocks=[analyze_block], blocks=[analyze_block],
) )
title, lines = confirmation_for_folder_schedule(report) title, lines = confirmation_for_folder_schedule(report)
@ -3463,7 +3452,7 @@ async def handle_intent(store: WorldStore, session_id: str, intent: IntentResult
) )
return AgentReply( return AgentReply(
text=(report.get("markdown") or "") text=(report.get("markdown") or "")
+ "\n\n必备数据已齐。下面请确认:**导入这些表格并试排一版**(可回滚)。", + "\n\n必备数据已齐。请确认:**导入这些表格并生成试排方案**(支持回滚)。",
blocks=[analyze_block, block], blocks=[analyze_block, block],
) )
if name in ("assistant.reply", "unknown"): if name in ("assistant.reply", "unknown"):

View File

@ -45,6 +45,7 @@ from __future__ import annotations # 前向类型引用
import asyncio # 流式分片的微延时 import asyncio # 流式分片的微延时
import contextvars # 线程池任务继承登录/租户上下文 import contextvars # 线程池任务继承登录/租户上下文
import json # SSE 载荷序列化 import json # SSE 载荷序列化
import logging # 记录异常,避免把内部细节返回给用户
from contextlib import asynccontextmanager # 自动化后台 tick 生命周期 # SSE 载荷序列化 from contextlib import asynccontextmanager # 自动化后台 tick 生命周期 # SSE 载荷序列化
import os import os
import threading import threading
@ -81,6 +82,9 @@ from server.gateway.governance_api import router as governance_router, gov_route
from server.gateway.mps_api import router as mps_router from server.gateway.mps_api import router as mps_router
logger = logging.getLogger(__name__)
# ---------------- 请求体契约 ---------------- # ---------------- 请求体契约 ----------------
class ChatRequest(BaseModel): class ChatRequest(BaseModel):
"""对话请求:会话 ID + 用户文本 + 可选多轮上文。""" """对话请求:会话 ID + 用户文本 + 可选多轮上文。"""
@ -1143,7 +1147,7 @@ def create_app() -> FastAPI:
from server.contracts import AgentReply as _AgentReply from server.contracts import AgentReply as _AgentReply
from server.state.projects import get_project_store from server.state.projects import get_project_store
emit_thinking("收到指令", (req.text or "")[:48], pct=1) emit_thinking("收到请求", (req.text or "")[:48], pct=1)
for ev in drain_thinking(): for ev in drain_thinking():
yield _sse(ev) yield _sse(ev)
@ -1166,11 +1170,11 @@ def create_app() -> FastAPI:
return return
if follow.get("kind") in ("affirm", "refer") and follow.get("text"): if follow.get("kind") in ("affirm", "refer") and follow.get("text"):
user_text = str(follow["text"]) user_text = str(follow["text"])
emit_thinking("结合上文", follow.get("note") or user_text[:40], status="done") emit_thinking("结合上下文", follow.get("note") or user_text[:40], status="done")
intent = None intent = None
reply = None reply = None
emit_thinking("理解意图", "正在识别你要做什么…", pct=5) emit_thinking("确认目标", "正在确认需要执行的业务操作…", pct=5)
for ev in drain_thinking(): for ev in drain_thinking():
yield _sse(ev) yield _sse(ev)
@ -1197,15 +1201,15 @@ def create_app() -> FastAPI:
dump.get("params", {}).pop("_history", None) dump.get("params", {}).pop("_history", None)
yield _sse({"type": "intent", "intent": dump}) yield _sse({"type": "intent", "intent": dump})
_THINK_BY_INTENT = { _THINK_BY_INTENT = {
"data.analyze": "深度分析项目数据(可能含大 SQL,请稍候)", "data.analyze": "正在分析项目数据",
"folder.analyze": "解析工程目录表格", "folder.analyze": "正在解析工程目录",
"folder.schedule": "导入目录数据并准备试排", "folder.schedule": "正在导入目录数据并准备试排",
"flex.schedule": "运行柔性排产引擎", "flex.schedule": "正在生成柔性排产方案",
"schedule.run": "运行排产引擎", "schedule.run": "正在生成排产方案",
"schedule.wizard": "启动排产向导", "schedule.wizard": "正在检查排产条件",
} }
tip = _THINK_BY_INTENT.get(intent.intent, f"执行 {intent.intent}") tip = _THINK_BY_INTENT.get(intent.intent, "正在执行已确认的业务操作")
emit_thinking("开始执行", tip, pct=8) emit_thinking("执行操作", tip, pct=8)
for ev in drain_thinking(): for ev in drain_thinking():
yield _sse(ev) yield _sse(ev)
@ -1265,8 +1269,9 @@ def create_app() -> FastAPI:
store.save() store.save()
except Exception: # noqa: BLE001, S110 - 尽力而为:滚动摘要任何异常降级为不滚动,绝不阻断对话 except Exception: # noqa: BLE001, S110 - 尽力而为:滚动摘要任何异常降级为不滚动,绝不阻断对话
pass pass
except Exception as exc: # 兜底:任何异常转为 error 事件(连接不中断) except Exception as exc: # 兜底:异常记录日志,用户只看到可执行提示
yield _sse({"type": "error", "message": f"处理失败:{exc}"}) logger.exception("chat request failed", exc_info=exc)
yield _sse({"type": "error", "message": "本次操作未完成,数据未修改。请稍后重试;如持续发生,请联系管理员。"})
finally: finally:
end_progress() end_progress()
yield _sse({"type": "done"}) # ⑧ 结束标记 yield _sse({"type": "done"}) # ⑧ 结束标记

View File

@ -79,6 +79,15 @@ def test_simulate_schedule_does_not_hijack_other_intents():
assert parse_fast("帮我分析一下当前项目里面的数据", world).intent == "folder.analyze" assert parse_fast("帮我分析一下当前项目里面的数据", world).intent == "folder.analyze"
def test_professional_starter_commands_keep_expected_intents():
world = seed_world()
delivery = parse_fast("生成一版交期优先试排方案", world)
assert delivery is not None and delivery.intent == "schedule.run"
assert delivery.params.get("strategy") == "DELIVERY_FIRST"
flex = parse_fast("生成一版柔性排产方案", world)
assert flex is not None and flex.intent == "flex.schedule"
def test_explicit_kitting_and_skill_strategy_phrases_keep_strategy_slots(): def test_explicit_kitting_and_skill_strategy_phrases_keep_strategy_slots():
world = seed_world() world = seed_world()
for phrase, expected in ( for phrase, expected in (
@ -171,7 +180,8 @@ async def test_unknown_handle_gives_project_brief_not_tip_spam():
async def test_explain_scheduling_not_inventory_dump(): async def test_explain_scheduling_not_inventory_dump():
world = seed_world() world = seed_world()
reply = await assistant_reply(world, "你给我解释什么叫排产") reply = await assistant_reply(world, "你给我解释什么叫排产")
assert "排产是啥" in reply.text or "把这些事提前安排清楚" in reply.text assert "什么是排产" in reply.text
assert "可执行的生产计划" in reply.text
assert "要排的单子" not in reply.text assert "要排的单子" not in reply.text
assert "你可以任选一句" not in reply.text assert "你可以任选一句" not in reply.text
assert not reply.blocks assert not reply.blocks
@ -181,7 +191,8 @@ async def test_explain_scheduling_not_inventory_dump():
async def test_can_schedule_not_inventory_dump(): async def test_can_schedule_not_inventory_dump():
world = seed_world() world = seed_world()
reply = await assistant_reply(world, "你会排产吗?") reply = await assistant_reply(world, "你会排产吗?")
assert "本职" in reply.text or "会" in reply.text assert "可以" in reply.text
assert "排产阻断项" in reply.text
assert "要排的单子" not in reply.text assert "要排的单子" not in reply.text
assert not reply.blocks assert not reply.blocks
@ -190,7 +201,21 @@ async def test_can_schedule_not_inventory_dump():
async def test_assistant_offline_analyze_style(): async def test_assistant_offline_analyze_style():
world = seed_world() world = seed_world()
reply = await assistant_reply(world, "分析一下现状") reply = await assistant_reply(world, "分析一下现状")
assert "手头有啥" in reply.text or "我帮你看了看" in reply.text assert "项目数据概览" in reply.text
assert "建议操作" in reply.text
@pytest.mark.asyncio
async def test_assistant_professional_copy_has_no_casual_markers():
world = seed_world()
replies = [
await assistant_reply(world, "你好"),
await assistant_reply(world, "什么是排产"),
await assistant_reply(world, "分析一下现状"),
]
text = "\n".join(r.text for r in replies)
for marker in ("啥", "咱们", "哟", "我在", "大白话", "冷笑话"):
assert marker not in text
@pytest.mark.asyncio @pytest.mark.asyncio

View File

@ -31,7 +31,7 @@ def test_guidance_empty_schedule():
g = build_guidance(world) g = build_guidance(world)
cmds = {s["command"] for s in g["suggestions"]} cmds = {s["command"] for s in g["suggestions"]}
assert any(s["id"] == "no_version" for s in g["signals"]) assert any(s["id"] == "no_version" for s in g["signals"])
assert "试排一版交期优先" in cmds assert "生成一版交期优先试排方案" in cmds
def test_guidance_knowledge_miss_context(): def test_guidance_knowledge_miss_context():

View File

@ -108,11 +108,11 @@ def test_intent_and_analyze_project_data():
r = parse_fast(text, world) r = parse_fast(text, world)
assert r is not None and r.intent == "data.analyze", text assert r is not None and r.intent == "data.analyze", text
text = analyze_project_data(world, "分析项目数据") text = analyze_project_data(world, "分析项目数据")
assert "手头有啥" in text or "要排的单子" in text assert "项目数据概览" in text
assert "我帮你看了看" in text or "能直接开排" in text assert "待排订单" in text and "可直接排产" in text
text2 = analyze_project_data( text2 = analyze_project_data(
world, world,
"【文件解析】\n文件:a.xlsx\n有效 2 行 · 错误 0 行(可入库)\n\n给我分析一下", "【文件解析】\n文件:a.xlsx\n有效 2 行 · 错误 0 行(可入库)\n\n给我分析一下",
) )
assert "你刚附的表格" in text2 assert "附件解析结果" in text2
assert "a.xlsx" in text2 assert "a.xlsx" in text2

View File

@ -58,7 +58,7 @@ def test_wizard_recommends_template_for_missing_routing():
"status": "RELEASED"}) "status": "RELEASED"})
reply = dialog.start_wizard(store, "w2") reply = dialog.start_wizard(store, "w2")
assert "没有工艺路线" in reply.text assert "尚未配置工艺路线" in reply.text
assert reply.blocks and reply.blocks[0].type == "wizard" assert reply.blocks and reply.blocks[0].type == "wizard"
tpls = reply.blocks[0].props.get("templates") or [] tpls = reply.blocks[0].props.get("templates") or []
assert tpls, "应推荐至少一个行业模板" assert tpls, "应推荐至少一个行业模板"
@ -115,7 +115,7 @@ def test_wizard_default_calendar():
assert "班次日历" in reply.text assert "班次日历" in reply.text
reply2 = dialog.pre_route(store, "w4", "默认日历") reply2 = dialog.pre_route(store, "w4", "默认日历")
assert isinstance(reply2, AgentReply) assert isinstance(reply2, AgentReply)
assert "默认日历" in reply2.text assert "默认班次日历" in reply2.text
assert store.data["flexCalendar"], "应写入默认日历" assert store.data["flexCalendar"], "应写入默认日历"