feat: professionalize assistant responses
This commit is contained in:
parent
ef7256f12c
commit
887a2317e1
|
|
@ -438,7 +438,7 @@ export default function App() {
|
|||
// 命令面板条目:口令(发对话)/ 面板(布局开关)/ 视图(视口切换)
|
||||
const paletteItems: PaletteItem[] = [
|
||||
...[
|
||||
'多策略方案对比', '试排一版交期优先', '产能均衡重排', '建一个检查点',
|
||||
'多策略方案对比', '生成一版交期优先试排方案', '产能均衡重排', '建一个检查点',
|
||||
'回滚到上一个检查点', '查看冲突', '标出超期订单', '查看当前KPI', '发布当前排产版本',
|
||||
].map(q => ({
|
||||
id: `cmd-${q}`, kind: '口令' as const, label: q, hint: '发送到对话',
|
||||
|
|
|
|||
|
|
@ -168,11 +168,11 @@ function GuestWorkspace() {
|
|||
<div className="chat-log">
|
||||
<div className="chat-start guest-chat-start">
|
||||
<span className="chat-start-mark">IB</span>
|
||||
<h1>今天想先忙哪件事?</h1>
|
||||
<h1>请选择要处理的任务</h1>
|
||||
<div className="starter-grid guest-starter-grid">
|
||||
<button type="button" className="starter-card" onClick={openLogin}>
|
||||
<span className="starter-icon spark"><ThunderboltOutlined /></span>
|
||||
<strong>试排一版交期优先</strong><small>试排一版交期优先</small>
|
||||
<strong>生成交期优先试排方案</strong><small>生成一版交期优先试排方案</small>
|
||||
</button>
|
||||
<button type="button" className="starter-card" onClick={openLogin}>
|
||||
<span className="starter-icon compare"><SwapOutlined /></span>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ import { chatStream, fetchGuidance, postConfirm, postImportPreview, postScenario
|
|||
import { loadMessages, loadMessagesPreferServer, saveMessages } from '../projects/store';
|
||||
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 {
|
||||
role: 'user' | 'agent';
|
||||
text: string;
|
||||
|
|
@ -26,12 +30,19 @@ function ThinkingPanel(props: {
|
|||
}) {
|
||||
const last = props.steps[props.steps.length - 1];
|
||||
const pct = last?.pct;
|
||||
const stepLabel = (step: string) => ({
|
||||
'连接助手': '连接服务',
|
||||
'收到指令': '收到请求',
|
||||
'理解意图': '确认目标',
|
||||
'开始执行': '执行操作',
|
||||
'生成回复': '整理结果',
|
||||
}[step] ?? step);
|
||||
return (
|
||||
<div className={`thinking-panel${props.open ? ' open' : ''}${props.streaming ? ' live' : ''}`}>
|
||||
<button type="button" className="thinking-head" onClick={props.onToggle}>
|
||||
<span className={`thinking-spinner${props.streaming ? '' : ' done'}`} aria-hidden />
|
||||
<span className="thinking-title">
|
||||
{props.streaming ? 'Thinking' : '已完成思考'}
|
||||
{props.streaming ? '处理中' : '处理完成'}
|
||||
{typeof pct === 'number' && props.streaming ? ` · ${Math.round(pct)}%` : ''}
|
||||
</span>
|
||||
<span className="thinking-caret">{props.open ? '▾' : '▸'}</span>
|
||||
|
|
@ -43,14 +54,14 @@ function ThinkingPanel(props: {
|
|||
<span className="thinking-mark">
|
||||
{s.status === 'done' ? '✓' : s.status === 'warn' ? '!' : '·'}
|
||||
</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>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{!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>
|
||||
);
|
||||
|
|
@ -94,13 +105,13 @@ export interface ChatHandle {
|
|||
}
|
||||
|
||||
const QUICK = [
|
||||
'跑一版柔性排产', '瓶颈产能', 'PDU 500套什么时候能交',
|
||||
'生成一版柔性排产方案', '瓶颈产能', 'PDU 500 套交期评估',
|
||||
'新建订单 客户 示例客户 产品 请替换为真实料号 数量 120 交期 8月20日',
|
||||
'新建物料 编码 SCR-NEW 名称 不锈钢螺丝 类型 原料 库存 800',
|
||||
'导入订单\n客户A,成品料号,200,2026-08-01,VIP\n客户 客户B 产品 成品料号 数量 100 交期 8月15日',
|
||||
'导入物料\nSCR-NEW,不锈钢螺丝,原料,包,800\nPCB-X,试验板,原料,块,200',
|
||||
'追溯订单依据', '分解全部订单', '主数据录入顺序有什么规定',
|
||||
'多策略方案对比', '试排一版交期优先', '产能均衡重排',
|
||||
'多策略方案对比', '生成一版交期优先试排方案', '产能均衡重排',
|
||||
'换线有什么规定', '生成排产方案报告', '生成日报', '生成版本对比报告',
|
||||
'建一个检查点', '回滚到上一个检查点',
|
||||
'只看VIP订单', '标出超期订单', '查看冲突',
|
||||
|
|
@ -238,7 +249,7 @@ const ChatPanel = forwardRef<ChatHandle, {
|
|||
(sessionRef as { current: string; _hist?: typeof history })._hist = history;
|
||||
return [...base, { role: 'user', text: t }, {
|
||||
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;
|
||||
|
|
@ -272,7 +283,7 @@ const ChatPanel = forwardRef<ChatHandle, {
|
|||
}));
|
||||
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 === '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') {
|
||||
patchLast(m => ({
|
||||
...m,
|
||||
|
|
@ -284,7 +295,7 @@ const ChatPanel = forwardRef<ChatHandle, {
|
|||
}));
|
||||
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);
|
||||
}, [busy, patchLast, props]);
|
||||
|
||||
|
|
@ -385,7 +396,7 @@ const ChatPanel = forwardRef<ChatHandle, {
|
|||
};
|
||||
recognition.onend = () => setListening(false);
|
||||
recognitionRef.current = recognition;
|
||||
setVoiceNotice('正在听,请说出排产要求或主数据说明。');
|
||||
setVoiceNotice('正在监听,请说明排产目标或主数据变更。');
|
||||
setListening(true);
|
||||
recognition.start();
|
||||
}, [autoGrow, listening]);
|
||||
|
|
@ -394,7 +405,7 @@ const ChatPanel = forwardRef<ChatHandle, {
|
|||
|
||||
const handleConfirm = useCallback(async (confirmId: string, approve: boolean) => {
|
||||
const result = await postConfirm(sessionRef.current, confirmId, approve)
|
||||
.catch(() => ({ message: '确认请求失败', refresh: false }));
|
||||
.catch(() => ({ message: '确认操作未完成,数据未修改。', refresh: false }));
|
||||
setMsgs(ms => [...ms, { role: 'agent', text: result.message }]);
|
||||
if (result.refresh) props.onWorldRefresh();
|
||||
return result;
|
||||
|
|
@ -402,7 +413,7 @@ const ChatPanel = forwardRef<ChatHandle, {
|
|||
|
||||
const handleApply = useCallback(async (card: ScenarioCard) => {
|
||||
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}】` }]);
|
||||
if (result.refresh) props.onWorldRefresh();
|
||||
}, [props]);
|
||||
|
|
@ -498,7 +509,7 @@ const ChatPanel = forwardRef<ChatHandle, {
|
|||
? <ChatMarkdown text={m.text} streaming={!!m.streaming} />
|
||||
: <span className="bubble-loading">正在处理…</span>)
|
||||
: <>{m.text}{m.streaming ? '▍' : ''}</>}
|
||||
{m.intent && !m.streaming && (
|
||||
{SHOW_INTENT_DEBUG && m.intent && !m.streaming && (
|
||||
<details className="intent-debug">
|
||||
<summary>调试信息</summary>
|
||||
<pre>{JSON.stringify(m.intent, null, 2)}</pre>
|
||||
|
|
@ -640,7 +651,7 @@ const ChatPanel = forwardRef<ChatHandle, {
|
|||
void sendDraft();
|
||||
}
|
||||
}}
|
||||
placeholder="输入排产指令…(Enter 发送 · Shift+Enter 换行)"
|
||||
placeholder="描述订单、交期或排产目标…(Enter 发送 · Shift+Enter 换行)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -669,10 +680,10 @@ type StarterIconName = 'spark' | 'compare' | 'risk' | 'report';
|
|||
|
||||
function ChatStart(props: { onSend: (text: string) => void }) {
|
||||
const defaults: Array<{ icon: StarterIconName; title: string; text: string }> = [
|
||||
{ icon: 'spark', title: '帮我排一版', text: '带我排一版' },
|
||||
{ icon: 'compare', title: '对比几种排法', text: '多策略方案对比' },
|
||||
{ icon: 'risk', title: '看看哪卡住了', text: '查看冲突并标出超期订单' },
|
||||
{ icon: 'report', title: '出一份工作表', text: '生成排产方案报告' },
|
||||
{ icon: 'spark', title: '生成试排方案', text: '生成一版交期优先试排方案' },
|
||||
{ icon: 'compare', title: '对比排产策略', text: '多策略方案对比' },
|
||||
{ icon: 'risk', title: '查看冲突与超期', text: '查看冲突并标出超期订单' },
|
||||
{ icon: 'report', title: '生成排产报告', text: '生成排产方案报告' },
|
||||
];
|
||||
const [starters, setStarters] = useState(defaults);
|
||||
const [hint, setHint] = useState<string | null>(null);
|
||||
|
|
@ -698,10 +709,10 @@ function ChatStart(props: { onSend: (text: string) => void }) {
|
|||
return (
|
||||
<div className="chat-start">
|
||||
<div className="chat-start-mark">IB</div>
|
||||
<h1>今天想先忙哪件事?</h1>
|
||||
<h1>请选择要处理的任务</h1>
|
||||
{hint
|
||||
? <p className="chat-start-hint">{hint}</p>
|
||||
: <p className="chat-start-hint">随便说就行,打错字、说方言都可以。不会专业词也没关系,我慢慢带着你。</p>}
|
||||
: <p className="chat-start-hint">可以描述订单、交期、设备约束或需要生成的排产报告。</p>}
|
||||
<div className="starter-grid">
|
||||
{starters.map(item => (
|
||||
<button key={item.title + item.text} type="button" className="starter-card" onClick={() => props.onSend(item.text)}>
|
||||
|
|
|
|||
|
|
@ -1405,7 +1405,7 @@ function TracePanel(props: {
|
|||
pagination={false}
|
||||
title={() => `③ 排产产物 · 工单 ${s.workOrders.length}${s.productionOrders.length > 0
|
||||
? ` · 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 }}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -401,7 +401,7 @@ export default function TimelineRail(props: {
|
|||
{!flexWo.length && !versions.length && !cpGroups.length ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="还没有排产结构。对话说「试排一版」或打开柔性工作台。"
|
||||
description="当前没有排产结构。请生成试排方案,或打开柔性排产工作台。"
|
||||
style={{ marginTop: 48 }}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -475,4 +475,4 @@ function fmtRange(start: string, end: string): string {
|
|||
const s = start?.slice(5, 16) ?? '';
|
||||
const e = end?.slice(11, 16) ?? '';
|
||||
return e ? `${s} → ${e}` : s;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export default function FlexGanttView(props: {
|
|||
useEffect(() => { dragRef.current = drag; }, [drag]);
|
||||
|
||||
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') {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export default function GanttView(props: {
|
|||
return (
|
||||
<div className="empty vp-empty-guide" ref={wrapRef}>
|
||||
<strong>尚无排产版本</strong>
|
||||
<span>在对话中说「试排一版交期优先」,或点欢迎页建议卡片。</span>
|
||||
<span>在对话中执行“生成一版交期优先试排方案”,或选择欢迎页建议卡片。</span>
|
||||
<span className="faint">也可说「下一步建议」获取当前状态推荐动作。</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ function fcTone(fc?: FlexCapacityForecast): string {
|
|||
export default function PoolCapacityView(props: { data: FlexCapacityData | null }) {
|
||||
const d = props.data;
|
||||
if (!d?.pools?.length) {
|
||||
return <div className="proj-empty">暂无产能池数据。先导入柔性设备/工序或跑一版柔性排产。</div>;
|
||||
return <div className="proj-empty">暂无产能池数据。请先导入柔性设备与工序,或生成柔性排产方案。</div>;
|
||||
}
|
||||
const bn = d.bottleneckPool;
|
||||
const alerts = d.alerts ?? [];
|
||||
|
|
|
|||
|
|
@ -1748,3 +1748,14 @@
|
|||
|
||||
- **类型**:chore
|
||||
- **做了什么**:`poc/workbench.html` 静态左说右动验证
|
||||
## 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。
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -73,7 +73,8 @@
|
|||
2. [Harness 门禁](./architecture/harness.md)
|
||||
3. [可重生模块](./architecture/modules.md)
|
||||
4. [Plan 可重生运行时](./architecture/plan-runtime.md)
|
||||
5. 蓝图全文:[plan.md](../plan.md)
|
||||
5. [对话回复规范](./development/response-style.md)
|
||||
6. 蓝图全文:[plan.md](../plan.md)
|
||||
|
||||
### 算法
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
# 对话回复规范
|
||||
|
||||
## 定位
|
||||
|
||||
工业智核是面向计划员、生产经理和工厂管理者的排产决策助手。回复应帮助用户确认事实、理解风险并完成下一步操作,不以闲聊或娱乐为目标。
|
||||
|
||||
## 回复结构
|
||||
|
||||
业务回复优先采用以下顺序:
|
||||
|
||||
1. **结论**:直接回答当前问题,说明是否可以继续。
|
||||
2. **依据**:列出关键数量、时间、资源和数据来源;数量必须带单位。
|
||||
3. **阻断项/风险**:明确缺失数据、冲突和不确定性。
|
||||
4. **下一步**:只给当前目标最相关的操作建议。
|
||||
|
||||
## 用词
|
||||
|
||||
- 使用“待排订单、可用设备、标准工时、数据齐备度、阻断项、试排方案”等业务术语。
|
||||
- 避免“啥、咱们、哟、随便、我就上手、大白话、手头有啥”等口语表达。
|
||||
- 不向用户暴露内部 intent 名称、模块名、异常堆栈或模型供应商名称。
|
||||
- 不使用笑话、营销话术或 emoji 作为业务回复内容。
|
||||
- 用户可以使用口语输入,但系统输出应保持专业、克制和可执行。
|
||||
|
||||
## 不确定性
|
||||
|
||||
没有可靠数据时必须明确说明“数据不足”或“当前无法确认”,同时指出需要补充的字段。不得用“应该、差不多、看起来可以”等模糊结论代替数据校验。
|
||||
|
||||
## 状态提示
|
||||
|
||||
处理中状态使用“正在确认目标”“正在分析项目数据”“正在生成排产方案”“正在整理结果”等业务描述。生产环境默认不显示内部调试信息;仅设置 `VITE_SHOW_INTENT_DEBUG=1` 时允许显示 intent 调试块。
|
||||
|
|
@ -23,18 +23,17 @@ HistoryItem = dict[str, str]
|
|||
|
||||
_WEEKDAYS = "一二三四五六日"
|
||||
|
||||
_SYSTEM = """你是「工业智核」智能助手,对话能力对标 Kimi / 豆包:用户什么都会问,你都尽量答。
|
||||
_SYSTEM = """你是「工业智核」企业级排产决策助手,服务对象是计划员、生产经理和工厂管理者。
|
||||
|
||||
身份:日常问答 + 工厂排产专家。
|
||||
|
||||
铁律:
|
||||
1. **先看【对话上文】**,理解用户是在接上一句(比如「是的」「那个」),再回答。
|
||||
2. **先正面回答这句话**。问几点报时间;问文件夹就列文件;问排产就讲排产。
|
||||
3. 禁止说:「跟排产无关帮不上」「换个排产问题」「没配大模型像 Kimi 才行」。
|
||||
4. 说人话、短句;少用黑话,必须用时立刻白话解释。
|
||||
5. 不要每轮粘贴功能清单。用 Markdown 排版。
|
||||
6. 排产数字只能来自【项目上下文】;文件夹内容来自【文件夹清单】。
|
||||
7. 有【当前时间】时,问几点必须用它。
|
||||
输出要求:
|
||||
1. 先给结论,再给关键数据、阻断项/风险和下一步操作。
|
||||
2. 结合【对话上文】理解省略表达,但不要重复用户原话或暴露内部意图名。
|
||||
3. 排产数字只能来自【项目上下文】;文件夹内容只能来自【文件夹清单】。
|
||||
4. 所有数量、时长和日期都带单位或明确字段名;无法确认时直接说明“数据不足”。
|
||||
5. 使用专业、克制、可执行的中文。避免“啥、咱们、哟、随便、我就上手”等口语,不使用笑话、营销话术或 emoji。
|
||||
6. 不要每轮粘贴功能清单;只给当前目标最相关的一个下一步。
|
||||
7. 有【当前时间】时,问几点必须使用该时间。
|
||||
8. 只回答当前问题,不编造项目事实;没有依据时明确说明需要补充什么数据。
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -126,7 +125,7 @@ def _simple_math(t: str) -> str | None:
|
|||
r = a * b
|
||||
elif op in ("/", "÷"):
|
||||
if b == 0:
|
||||
return "除数不能是 0 哟。"
|
||||
return "除数不能为 0。"
|
||||
r = a / b
|
||||
else:
|
||||
return None
|
||||
|
|
@ -139,24 +138,23 @@ def _simple_math(t: str) -> str | None:
|
|||
|
||||
def _explain_scheduling() -> str:
|
||||
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:
|
||||
return (
|
||||
"**会,这就是我的本职工作。**\n\n"
|
||||
"你把订单和现场情况告诉我,我可以帮你看缺不缺、排出一版计划。\n"
|
||||
"日常问题(几点了、算个数、闲聊)也都能聊。\n\n"
|
||||
"想现在就试?直接说「带我排一版」或「直接排一版」。"
|
||||
"**可以。**\n\n"
|
||||
"我可以检查订单、工艺、设备、班次和物料数据,识别排产阻断项,"
|
||||
"并按交期、产能或瓶颈策略生成试排方案。\n\n"
|
||||
"建议先说「分析当前项目数据」,确认数据齐备度后再生成方案。"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -180,9 +178,9 @@ def _offline_reply(
|
|||
|
||||
if _is_greeting(t):
|
||||
return (
|
||||
"你好,我在。\n\n"
|
||||
"啥都能问:几点了、算个数、看项目文件夹、排产排程……怎么说都行。\n\n"
|
||||
"想排产的话,说「带我排一版」我就上手。",
|
||||
"您好,我是工业智核排产助手。\n\n"
|
||||
"我可以协助分析订单与生产数据、检查排产条件、生成试排方案并解释冲突。\n\n"
|
||||
"请直接描述目标,例如「分析当前项目数据」或「生成交期优先试排方案」。",
|
||||
False,
|
||||
)
|
||||
|
||||
|
|
@ -202,8 +200,10 @@ def _offline_reply(
|
|||
|
||||
if re.search(r"你是谁|你能做什么|怎么用|如何使用|介绍一下|你会什么", t):
|
||||
return (
|
||||
"我是工业智核里的助手:**日常问题也能聊**,排产是我最拿手的。\n\n"
|
||||
"可以说「现在几点了」「解析项目文件夹」「帮我分析项目数据」「带我排一版」。",
|
||||
"我是工业智核的排产决策助手。\n\n"
|
||||
"主要能力包括:分析项目数据、检查排产齐备度、生成和比较试排方案、"
|
||||
"解释交期风险与产能瓶颈。\n\n"
|
||||
"建议从「分析当前项目数据」开始。",
|
||||
False,
|
||||
)
|
||||
|
||||
|
|
@ -213,16 +213,15 @@ def _offline_reply(
|
|||
|
||||
if re.search(r"讲个笑话|来个笑话|说个笑话", t):
|
||||
return (
|
||||
"好,来一个车间版冷笑话:\n\n"
|
||||
"计划员问设备:「你今天忙不忙?」\n"
|
||||
"设备说:「不忙,我排期排到下下下个月了,早就躺平了。」",
|
||||
"我目前专注于生产计划、排产数据和交期分析。\n\n"
|
||||
"如果你有具体订单或现场问题,请直接描述,我会给出可执行的分析结果。",
|
||||
False,
|
||||
)
|
||||
|
||||
if re.search(r"天气|下雨|降温|热不热", t):
|
||||
return (
|
||||
"我这边看不到窗外实时天气,手机天气最准。\n\n"
|
||||
"要是担心交期被耽误,把单号跟我说,我帮你看计划。",
|
||||
"当前未接入实时天气数据,无法提供可靠天气判断。\n\n"
|
||||
"如果你担心天气影响交期,请提供订单号,我可以检查对应计划和缓冲时间。",
|
||||
False,
|
||||
)
|
||||
|
||||
|
|
@ -235,29 +234,24 @@ def _offline_reply(
|
|||
t,
|
||||
):
|
||||
return (
|
||||
"明白,你是要**直接开排**。\n\n"
|
||||
"请再说一次「给我排产」或点快捷指令「跑一版柔性排产」——"
|
||||
"我会按工程目录/当前数据开排,不再让你选菜单。",
|
||||
"我理解你的目标是生成排产方案。\n\n"
|
||||
"请确认使用当前项目数据生成一版柔性试排方案;系统会先校验订单、工艺、"
|
||||
"设备和班次条件,缺失数据会在结果中明确列出。",
|
||||
False,
|
||||
)
|
||||
if re.search(r"排产|订单|交期|设备|工艺|物料|工时", t) and re.search(
|
||||
r"什么|怎么|如何|吗|呢|?|\?", t,
|
||||
):
|
||||
return (
|
||||
f"你问的是「{t[:40]}」。\n\n"
|
||||
"若要**直接开排**,说「给我排产」;\n"
|
||||
"若要看齐不齐,说「帮我看看现在能不能排」;\n"
|
||||
"若要看文件夹,说「解析项目文件夹」。",
|
||||
f"我理解你关注的是「{t[:40]}」。\n\n"
|
||||
"请明确需要执行的操作:生成试排方案、检查数据齐备度,或查看项目文件。",
|
||||
False,
|
||||
)
|
||||
|
||||
# 开放问题:结合上文语气,不再甩「没配 Kimi」
|
||||
return (
|
||||
f"关于「{t[:48]}」,我先按你的话理解一下。\n\n"
|
||||
"你要是想看**项目文件夹里有啥文件**,直接说「解析项目文件夹」;\n"
|
||||
"想看**排产数据齐不齐**,说「帮我分析一下当前项目里面的数据」;\n"
|
||||
"想**开排**,说「带我排一版」。\n\n"
|
||||
"也可以把具体想法再说详细一点,我按你的目标接着做。",
|
||||
f"我暂未识别到「{t[:48]}」对应的明确业务目标。\n\n"
|
||||
"请补充以下任一信息:订单号或产品、期望交期、需要检查的数据,或希望采用的排产策略。",
|
||||
False,
|
||||
)
|
||||
|
||||
|
|
@ -288,7 +282,7 @@ async def reply(
|
|||
or wants_folder_listing(q):
|
||||
text, with_card = _offline_reply(world, q, session_id=session_id)
|
||||
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 [])
|
||||
|
||||
provider = get_provider()
|
||||
|
|
@ -317,6 +311,6 @@ async def reply(
|
|||
|
||||
text, with_card = _offline_reply(world, q, session_id=session_id)
|
||||
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 []
|
||||
return AgentReply(text=text, blocks=blocks)
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ def resolve_followup(text: str, history: list[HistoryItem]) -> dict[str, Any]:
|
|||
"text": t,
|
||||
"note": "用户拒绝了上一轮建议",
|
||||
"kind": "deny",
|
||||
"reply": "好,那先不动。你想到啥随时说,我接着帮你。",
|
||||
"reply": "已取消本次操作,当前数据未修改。需要继续时,请重新描述目标。",
|
||||
}
|
||||
|
||||
if _AFFIRM.match(t) and prev_agent:
|
||||
|
|
|
|||
|
|
@ -118,13 +118,13 @@ def make_clarification(session_id: str, text: str, intent: IntentResult) -> Agen
|
|||
for c in candidates],
|
||||
"rounds": prev_rounds + 1,
|
||||
}
|
||||
lines = [f"我不太确定你的意思,你是想:(回复序号即可)"]
|
||||
lines = [f"请确认需要执行的操作(回复序号即可):"]
|
||||
for i, c in enumerate(candidates, start=1):
|
||||
lines.append(f"{i}. {c[1]}")
|
||||
lines.append("都不是的话,请换个说法补充要点。")
|
||||
lines.append("如果以上选项均不符合,请补充订单、产品或目标操作。")
|
||||
block = UIBlock(
|
||||
blockId=f"clarify-{session_id[:8]}", type="clarify",
|
||||
props={"question": "我不太确定你的意思,你是想:",
|
||||
props={"question": "请确认需要执行的操作:",
|
||||
"options": [{"index": i + 1, "intent": c[0], "label": c[1]}
|
||||
for i, c in enumerate(candidates)],
|
||||
"originalText": text})
|
||||
|
|
@ -165,7 +165,7 @@ def _resolve_clarify(session_id: str, text: str) -> IntentResult | None:
|
|||
# ② 引导式排产向导
|
||||
# ============================================================
|
||||
_WIZ_CANCEL = r"取消|退出|不排了|算了"
|
||||
_WIZ_GO = r"^(排|开始|好|可以|确认|继续|继续排产|go)$"
|
||||
_WIZ_GO = r"^(排|开始|好|可以|确认|确认试排|继续|继续排产|go)$"
|
||||
|
||||
|
||||
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:
|
||||
st["wizard"] = None
|
||||
return AgentReply(text="当前没有待排订单。先录入订单吧——可以说"
|
||||
"「导入订单」(Excel 批量)或「新建订单 产品X 数量100 交期…」,"
|
||||
"录完再说「我要排产」。")
|
||||
return AgentReply(text="当前没有待排订单,无法启动排产向导。\n"
|
||||
"请先导入订单表(Excel 批量)或新建订单;完成后再次执行“开始排产”。")
|
||||
|
||||
# 缺日历 → 给默认日历选项
|
||||
if any(gi["type"] == "NO_CALENDAR" for gi in report["globalIssues"]):
|
||||
st["wizard"] = {"step": "fix_calendar"}
|
||||
return AgentReply(text="缺班次日历(不知道每天几点开工、一周开几天)。\n"
|
||||
"回复「默认日历」建单班 08:00-17:00(午休1h)周一~周五;"
|
||||
"或告诉我你们的班制(如「两班 8点到23点 周一到周六」)。",
|
||||
return AgentReply(text="缺少班次日历,无法计算可用生产时段。\n"
|
||||
"请选择“默认日历”(单班 08:00-17:00,午休 1 小时,周一至周五),"
|
||||
"或提供实际班制(例如“两班制,08:00-23:00,周一至周六”)。",
|
||||
blocks=[_wizard_block("fix_calendar", "缺班次日历",
|
||||
["默认日历", "自定义班制"])])
|
||||
|
||||
|
|
@ -222,14 +221,14 @@ def _wizard_advance(store, session_id: str, actor: str) -> AgentReply:
|
|||
tpls = []
|
||||
st["wizard"] = {"step": "pick_template", "productCode": pc, "productName": pname,
|
||||
"templates": [t["code"] for t in tpls]}
|
||||
lines = [f"订单 {target['orderNo']} 的产品「{pname}」还没有工艺路线。"
|
||||
f"根据产品特征,推荐这些行业模板:(回复序号采用)"]
|
||||
lines = [f"订单 {target['orderNo']} 的产品「{pname}」尚未配置工艺路线。"
|
||||
f"以下是根据产品特征匹配的行业模板,请回复序号选择:"]
|
||||
for i, t in enumerate(tpls, start=1):
|
||||
steps_brief = "→".join(x["operationName"] for x in t["steps"][:6])
|
||||
more = "…" if len(t["steps"]) > 6 else ""
|
||||
total = sum(x["stdMinDefault"] for x in t["steps"])
|
||||
lines.append(f"{i}. {t['name']}:{steps_brief}{more}(约 {total:.0f} 分/件)")
|
||||
lines.append("都不合适可以回复「跳过」,稍后手工维护路线。")
|
||||
lines.append("如果没有合适模板,请回复“跳过”,稍后在主数据中维护工艺路线。")
|
||||
return AgentReply(text="\n".join(lines),
|
||||
blocks=[_wizard_block("pick_template", f"为 {pname} 选工艺模板",
|
||||
[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:
|
||||
pc, op = queue[0]
|
||||
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"回复数字(如「45」),或回复「跳过」逐条略过。",
|
||||
f"请回复分钟数(如“45”),或回复“跳过”暂不维护。",
|
||||
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
|
||||
details = ";".join(i["detail"] for r in no_eq[:3] for i in r["issues"]
|
||||
if i["type"] == "NO_CAPABLE_EQUIPMENT")
|
||||
return AgentReply(text=f"还有工序没有能力设备:{details}。\n"
|
||||
f"请在主数据页为设备登记工序能力(capabilities),"
|
||||
f"或说「设备 XX 增加能力 工序YY」;完成后再说「我要排产」。")
|
||||
return AgentReply(text=f"以下工序尚未匹配到可用设备:{details}。\n"
|
||||
f"请在主数据页补充设备工序能力,完成后再次执行“检查数据齐备度”。")
|
||||
|
||||
# 全绿(或仅剩警告)→ 试排确认
|
||||
warn_note = ""
|
||||
if s["withWarnings"]:
|
||||
warn_note = f"({s['withWarnings']} 张订单带推断工时/缺料告警,结果会显式标注)"
|
||||
st["wizard"] = {"step": "confirm_run"}
|
||||
return AgentReply(text=f"数据齐备 ✅ {s['total']} 张订单可排{warn_note}。\n"
|
||||
f"回复「排」立即按瓶颈锚模式试排;回复「取消」退出向导。",
|
||||
blocks=[_wizard_block("confirm_run", "数据齐备,可以试排", ["排", "取消"])])
|
||||
return AgentReply(text=f"数据齐备:{s['total']} 张订单可以生成试排方案{warn_note}。\n"
|
||||
f"回复“确认试排”按瓶颈锚策略生成方案;回复“取消”退出向导。",
|
||||
blocks=[_wizard_block("confirm_run", "数据齐备,可以生成试排方案", ["确认试排", "取消"])])
|
||||
|
||||
|
||||
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):
|
||||
st["wizard"] = None
|
||||
return AgentReply(text="已退出排产向导。随时说「我要排产」重新开始。")
|
||||
return AgentReply(text="已退出排产向导。需要时可重新执行“开始排产”。")
|
||||
|
||||
if step == "fix_calendar":
|
||||
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": "默认单班"})
|
||||
store.save()
|
||||
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 AgentReply(text="没听懂班制。回复「默认日历」,或稍后在主数据页维护后再说「继续排产」。")
|
||||
return AgentReply(text="未识别到有效班制。请选择“默认日历”,或提供班次数量、工作时间和工作日。")
|
||||
|
||||
if step == "pick_template":
|
||||
if re.search(r"^跳过$", t):
|
||||
st["wizard"] = {"step": "resume"}
|
||||
return AgentReply(text="已跳过该产品。稍后可在主数据页维护路线;说「继续排产」推进其它检查。")
|
||||
return AgentReply(text="已跳过该产品的工艺路线。请在主数据中维护后,再执行“继续排产”。")
|
||||
codes = wiz.get("templates") or []
|
||||
idx = None
|
||||
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
|
||||
break
|
||||
if not tpl_code:
|
||||
return AgentReply(text="请回复模板序号(1/2/3)、「跳过」或「取消」。")
|
||||
return AgentReply(text="请选择模板序号(1/2/3),或回复“跳过”“取消”。")
|
||||
pc = wiz.get("productCode") or ""
|
||||
pname = wiz.get("productName") or pc
|
||||
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})
|
||||
store.save()
|
||||
st["wizard"] = {"step": "resume"}
|
||||
return AgentReply(text=f"{title}——需要你确认(P2)。批准后说「继续排产」推进下一步。",
|
||||
return AgentReply(text=f"{title}需要确认(P2)。确认后执行“继续排产”推进下一步。",
|
||||
blocks=[block])
|
||||
|
||||
if step == "fill_time":
|
||||
|
|
@ -365,11 +363,11 @@ def _wizard_turn(store, session_id: str, text: str, actor: str) -> AgentReply |
|
|||
queue.pop(0)
|
||||
st["wizard"] = {"step": "fill_time", "queue": queue} if queue else {"step": "resume"}
|
||||
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)
|
||||
m = re.search(r"(\d+(?:\.\d+)?)", t)
|
||||
if not m:
|
||||
return AgentReply(text=f"请回复分钟数(如「45」)、「跳过」或「取消」。当前:{pc} × {op}")
|
||||
return AgentReply(text=f"请回复标准工时(分钟/件),或回复“跳过”“取消”。当前:{pc} × {op}")
|
||||
std_min = float(m.group(1))
|
||||
title = f"更新工时:{pc} × {op} → {std_min} 分钟/件"
|
||||
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"}
|
||||
nxt = (f"\n下一条:产品 {queue[0][0]} 的工序「{queue[0][1]}」单件多少分钟?"
|
||||
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 re.match(_WIZ_GO, t) or re.search(r"^试排|排产$", t):
|
||||
|
|
|
|||
|
|
@ -635,20 +635,20 @@ def _to_markdown(
|
|||
|
||||
lines.append("")
|
||||
if can_schedule:
|
||||
lines.append("**结论:按文件内容看,基本具备开排条件。**")
|
||||
lines.append("你可以说「根据这些数据排产」——我先收进系统,再校验后试排。")
|
||||
lines.append("**结论:按文件内容看,已基本满足排产条件。**")
|
||||
lines.append("下一步:确认导入这些数据,系统将继续校验并生成试排方案。")
|
||||
else:
|
||||
lines.append("**结论:现在还不能稳妥开排。**")
|
||||
lines.append("还缺:" + ("、".join(missing) if missing else "有效数据行"))
|
||||
lines.append("把缺的表补进工程目录(或改好报错行)后再叫我。")
|
||||
lines.append("**结论:当前不满足直接排产条件。**")
|
||||
lines.append("缺失项:" + ("、".join(missing) if missing else "有效数据行"))
|
||||
lines.append("请补充缺失数据或修正错误记录后重新执行目录分析。")
|
||||
|
||||
ws = (world_ready or {}).get("summary") or {}
|
||||
if ws:
|
||||
lines += [
|
||||
"",
|
||||
f"(系统里当前已有待排单 {ws.get('total', 0)} 张,"
|
||||
f"可排 {ws.get('ready', 0)},卡住 {ws.get('blocked', 0)}。"
|
||||
f"导入目录后会按新数据重新看。)",
|
||||
f"(系统当前已有待排订单 {ws.get('total', 0)} 张,"
|
||||
f"可排 {ws.get('ready', 0)} 张,存在阻断 {ws.get('blocked', 0)} 张。"
|
||||
f"导入目录后将基于新数据重新校验。)",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
|
@ -663,5 +663,5 @@ def confirmation_for_folder_schedule(report: dict[str, Any]) -> tuple[str, list[
|
|||
n = (report.get("kindCounts") or {}).get(k, 0)
|
||||
if n:
|
||||
lines.append(f"· {KIND_CN.get(k, k)} {n} 行")
|
||||
lines.append("确认后写入柔性主数据并试排一版(可回滚)")
|
||||
lines.append("确认后写入柔性主数据并生成试排方案(支持回滚)")
|
||||
return title, lines
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ def build_guidance(world: World, *, context: str | None = None) -> dict[str, Any
|
|||
"reason": "甘特为空,需先试排生成草稿。",
|
||||
})
|
||||
suggestions.append({
|
||||
"id": "run_delivery", "label": "试排一版交期优先",
|
||||
"command": "试排一版交期优先", "priority": 10,
|
||||
"id": "run_delivery", "label": "生成交期优先试排方案",
|
||||
"command": "生成一版交期优先试排方案", "priority": 10,
|
||||
"reason": "无版本时空态首选",
|
||||
})
|
||||
suggestions.append({
|
||||
|
|
@ -87,8 +87,8 @@ def build_guidance(world: World, *, context: str | None = None) -> dict[str, Any
|
|||
"reason": "能力池/虚拟产线需跑柔性排产。",
|
||||
})
|
||||
suggestions.append({
|
||||
"id": "flex_run", "label": "跑一版柔性排产",
|
||||
"command": "跑一版柔性排产", "priority": 12,
|
||||
"id": "flex_run", "label": "生成柔性排产方案",
|
||||
"command": "生成一版柔性排产方案", "priority": 12,
|
||||
"reason": "柔性轨空态",
|
||||
})
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ def build_guidance(world: World, *, context: str | None = None) -> dict[str, Any
|
|||
|
||||
if not ranked:
|
||||
ranked = [{
|
||||
"id": "help", "label": "看看能做什么",
|
||||
"id": "help", "label": "查看可用操作",
|
||||
"command": "帮助", "priority": 1,
|
||||
"reason": "默认兜底",
|
||||
}]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""齐备度报告 → 人话(车间能听懂)。"""
|
||||
"""齐备度报告:结论、关键指标、阻断项与下一步。"""
|
||||
s = report["summary"]
|
||||
if not s["total"]:
|
||||
conclusion = "当前没有待排订单,暂时无法生成试排方案。"
|
||||
elif s["blocked"]:
|
||||
conclusion = f"当前有 {s['blocked']} 张订单存在阻断项,暂不建议直接排产。"
|
||||
else:
|
||||
conclusion = f"当前 {s['total']} 张订单已通过基础齐备度检查,可以生成试排方案。"
|
||||
lines = [
|
||||
f"我帮你看了看:一共 {s['total']} 张要排的单子,"
|
||||
f"大概 {s['ready']} 张能直接开排,"
|
||||
f"{s['blocked']} 张还卡着,"
|
||||
f"{s['withWarnings']} 张能排但有点风险。",
|
||||
f"工时这边:一共 {s['timeTotal']} 道步骤,"
|
||||
f"还有 {s['timePending']} 道没填好,"
|
||||
f"{s['timeInferred']} 道是估出来的(不太准)。",
|
||||
"## 数据齐备度",
|
||||
f"**结论:{conclusion}**",
|
||||
"",
|
||||
f"- 待排订单:{s['total']} 张;可直接排产:{s['ready']} 张;存在阻断:{s['blocked']} 张。",
|
||||
f"- 标准工时:共 {s['timeTotal']} 道工序;待维护 {s['timePending']} 道;推断值 {s['timeInferred']} 道。",
|
||||
]
|
||||
if s["withWarnings"]:
|
||||
lines.append(f"- 风险提示:{s['withWarnings']} 张订单包含推断工时或缺料告警。")
|
||||
for gi in report["globalIssues"]:
|
||||
lines.append(f"⚠ 先卡住了:{gi['detail']}")
|
||||
lines.append(f"- **阻断项**:{gi['detail']}")
|
||||
shown = 0
|
||||
for r in report["orders"]:
|
||||
if not r["issues"] or shown >= 6:
|
||||
continue
|
||||
mark = "还不行" if not r["ready"] else "要注意"
|
||||
mark = "阻断" if not r["ready"] else "风险"
|
||||
heads = ";".join(i["detail"] for i in r["issues"][:3])
|
||||
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
|
||||
rest = sum(1 for r in report["orders"] if r["issues"]) - shown
|
||||
if rest > 0:
|
||||
lines.append(f"……还有 {rest} 张单子也有类似问题,需要的话我再细说。")
|
||||
lines.append(f"- 另有 {rest} 张订单存在类似问题,详情可在订单齐备度明细中查看。")
|
||||
if s["timePending"] or s["timeInferred"]:
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"工时不会填也没关系,你可以跟我说:"
|
||||
"「把某某工序改成多少分钟」,我按你说的记下来。"
|
||||
"**建议操作**:补充待维护工时,或提供实测工时(例如“将工序 OP-10 设置为 45 分钟/件”)。"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def analyze_project_data(world: World, query: str = "") -> str:
|
||||
"""分析当前项目/附件:用人话盘点 → 缺啥 → 下一步怎么补。"""
|
||||
"""分析当前项目/附件:结论 → 数据概览 → 阻断项 → 建议操作。"""
|
||||
lines: list[str] = []
|
||||
q = query or ""
|
||||
if "【文件解析】" in q:
|
||||
|
|
@ -199,11 +205,12 @@ def analyze_project_data(world: World, query: str = "") -> str:
|
|||
chunk = chunk.split("\n\n请", 1)[0].strip()
|
||||
elif "\n\n给我" in chunk:
|
||||
chunk = chunk.split("\n\n给我", 1)[0].strip()
|
||||
lines.append("你刚附的表格,我帮你过了一遍:")
|
||||
lines.append("## 附件解析结果")
|
||||
lines.append("已完成附件的初步解析:")
|
||||
lines.extend(chunk.splitlines()[:40])
|
||||
lines.append("")
|
||||
if "可入库" in chunk:
|
||||
lines.append("里面有一部分已经核对过了,可以收进系统;有问题的行我标出来了,你改完再传也行。")
|
||||
lines.append("部分记录已通过校验,可以提交入库;错误记录需要修正后重新上传。")
|
||||
lines.append("")
|
||||
|
||||
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 []
|
||||
bom = world.get("flexBom") or []
|
||||
|
||||
lines.append("**先说说咱们手头有啥:**")
|
||||
lines.append("## 项目数据概览")
|
||||
lines.append(
|
||||
f"- 要排的单子 **{len(active)}** 张;材料/产品档案 **{len(mats)}** 条;"
|
||||
f"用料清单 **{len(bom)}** 条;工艺步骤 **{len(routes)}** 条;"
|
||||
f"能开着干活的设备 **{len(equip)}** 台;班次 **{len(cal)}** 条。"
|
||||
f"- 待排订单:**{len(active)}** 张;产品/物料:**{len(mats)}** 条;"
|
||||
f"BOM 明细:**{len(bom)}** 条;工艺步骤:**{len(routes)}** 条;"
|
||||
f"可用设备:**{len(equip)}** 台;班次日历:**{len(cal)}** 条。"
|
||||
)
|
||||
if active:
|
||||
preview = "、".join(
|
||||
|
|
@ -226,9 +233,9 @@ def analyze_project_data(world: World, query: str = "") -> str:
|
|||
for o in active[:5]
|
||||
)
|
||||
more = f"……一共 {len(active)} 张" if len(active) > 5 else ""
|
||||
lines.append(f"- 比如这些单:{preview}{more}")
|
||||
lines.append(f"- 订单示例:{preview}{more}")
|
||||
else:
|
||||
lines.append("- 这会儿还没有要排的单子。你可以附一张订单表,或者说「新建订单」。")
|
||||
lines.append("- 待排订单为 0 张。请先导入订单表或新建订单。")
|
||||
|
||||
report = check_readiness(world)
|
||||
lines.append("")
|
||||
|
|
@ -236,20 +243,20 @@ def analyze_project_data(world: World, query: str = "") -> str:
|
|||
|
||||
asks: list[str] = []
|
||||
if not active:
|
||||
asks.append("来一份订单表(单号、做什么产品、做多少、什么时候要)")
|
||||
asks.append("导入订单表(至少包含单号、产品、数量和交期)")
|
||||
if not mats:
|
||||
asks.append("来一份产品/材料名录(至少把要做的产品写上)")
|
||||
asks.append("补充产品/物料主数据(至少包含待排产品)")
|
||||
if not routes:
|
||||
asks.append("告诉我产品怎么做(工艺步骤),或者说「带我排一版」让我按常用办法生成")
|
||||
asks.append("补充工艺路线和工序顺序,或请求系统推荐行业模板")
|
||||
if not equip:
|
||||
asks.append("告诉我有哪些能干活的设备、各自会做什么工序")
|
||||
asks.append("补充可用设备及其工序能力")
|
||||
if not cal:
|
||||
asks.append("告诉我一天上几班、几点开工;或者说「带我排一版」用常用班次")
|
||||
asks.append("补充班次日历(工作日、班次和起止时间)")
|
||||
s = report["summary"]
|
||||
if s.get("timePending"):
|
||||
asks.append(f"把还没填的工时补上(现在大概还有 {s['timePending']} 道)")
|
||||
asks.append(f"补充 {s['timePending']} 道工序的标准工时")
|
||||
if s.get("blocked"):
|
||||
asks.append(f"把卡住的 {s['blocked']} 张单子问题处理掉(缺产品、缺工艺、或缺设备)")
|
||||
asks.append(f"处理 {s['blocked']} 张订单的阻断项(产品、工艺或设备能力缺失)")
|
||||
|
||||
seen: set[str] = set()
|
||||
uniq = []
|
||||
|
|
@ -260,12 +267,13 @@ def analyze_project_data(world: World, query: str = "") -> str:
|
|||
|
||||
lines.append("")
|
||||
if uniq:
|
||||
lines.append("**想顺利开排,还差这些:**")
|
||||
lines.append("## 阻断项与建议操作")
|
||||
for i, a in enumerate(uniq, 1):
|
||||
lines.append(f"{i}. {a}")
|
||||
lines.append("你补一点,我就帮你看一点;齐了直接说「直接排一版」或「带我排一版」。")
|
||||
lines.append("完成上述补充后,请再次执行“检查数据齐备度”;通过后即可生成试排方案。")
|
||||
else:
|
||||
lines.append("看起来东西比较齐了。你要是愿意,直接跟我说「直接排一版」,我马上给你出一稿。")
|
||||
lines.append("## 建议操作")
|
||||
lines.append("数据已满足基础排产条件。可以选择排产策略并生成试排方案。")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ def build_plan_report(world: dict[str, Any], order_no: str | None = None) -> dic
|
|||
if not daily.get("reportId"):
|
||||
return {
|
||||
"reportId": None, "title": "排产方案报告",
|
||||
"markdown": "尚无排产版本。请先说「跑一版柔性排产」或「试排一版」。",
|
||||
"markdown": "尚无排产版本。请先生成柔性排产方案或交期优先试排方案。",
|
||||
"snapshot": None, "xlsxBytes": None, "filename": None,
|
||||
}
|
||||
daily["title"] = daily["title"].replace("排产日报", "排产方案报告")
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ _HELP = "\n".join([
|
|||
"可执行的排产能力:",
|
||||
"· 查询现场数据:有哪些订单 / 查订单 102285668 / 这个订单有委外吗 / 查物料 / 查 BOM / 查工艺路线",
|
||||
"· 知识库:机加工工艺怎么生成 / 车削工艺模式 / 装配工艺模式 / 知识清单 / 导入知识文档",
|
||||
"· 生成排产草案:试排一版交期优先 / 产能均衡重排 / 用规则引擎试排",
|
||||
"· 柔性排产(能力池+虚拟产线):跑一版柔性排产 / 把这个订单排产 / 正排 / 倒排 / 瓶颈锚",
|
||||
"· 生成排产方案:交期优先试排 / 产能均衡重排 / 规则引擎试排",
|
||||
"· 柔性排产(能力池+虚拟产线):生成柔性排产方案 / 按订单排产 / 正排 / 倒排 / 瓶颈锚",
|
||||
"· 外部算法:用外部算法试排 / 查看已接入算法 / skill 健康检查",
|
||||
"· 瓶颈产能:瓶颈产能 / 各工序能做多少(按能力池实时算)",
|
||||
"· 交期承诺:某产品 N 套什么时候能交",
|
||||
|
|
@ -884,7 +884,7 @@ 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 (
|
||||
"SQL 已入库" if sql_already else "无行"
|
||||
)
|
||||
head = f"工程目录已导入 ✅({detail})。"
|
||||
head = f"工程目录已导入({detail})。"
|
||||
# 有订单+工艺+设备即可开排(工时推断/缺 BOM 不拦死)
|
||||
has_core = (
|
||||
bool(store.data.get("flexOrders"))
|
||||
|
|
@ -897,7 +897,7 @@ def execute_confirmed(store: WorldStore, confirm_id: str, approve: bool, actor:
|
|||
result = run_flex_schedule(
|
||||
store, sort_mode=params.get("sortMode") or "BOTTLENECK", actor=actor, trial=True)
|
||||
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('conflictCount', 0)})。"
|
||||
"可在右侧柔性工作台看甘特,或再说「查看冲突」。")
|
||||
|
|
@ -1542,7 +1542,7 @@ def _run_flex(store: WorldStore, intent: IntentResult, actor: str) -> AgentReply
|
|||
)
|
||||
else:
|
||||
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",窗外延期 {deferred}" if deferred else "")
|
||||
+ f"。{dl_hint}"
|
||||
|
|
@ -2965,16 +2965,16 @@ async def handle_intent(store: WorldStore, session_id: str, intent: IntentResult
|
|||
if not report.get("batches") and not report.get("sqlApplied"):
|
||||
return AgentReply(
|
||||
text=(report.get("markdown") or "")
|
||||
+ "\n\n目录里还没有可入库的有效行,我没法按这些表开排。"
|
||||
"请先把订单/物料/工艺/设备表或 SQL 库补齐。",
|
||||
+ "\n\n目录中没有可入库的有效记录,当前无法生成排产方案。"
|
||||
"请补充订单、物料、工艺或设备数据。",
|
||||
blocks=[analyze_block],
|
||||
)
|
||||
if report.get("missing") and not has_core:
|
||||
return AgentReply(
|
||||
text=(report.get("markdown") or "")
|
||||
+ "\n\n**还不能排:**还缺 "
|
||||
+ "\n\n**当前无法排产:**缺少 "
|
||||
+ "、".join(report["missing"])
|
||||
+ "。补齐后再说「根据这些数据排产」。",
|
||||
+ "。补齐后请重新执行目录分析。",
|
||||
blocks=[analyze_block],
|
||||
)
|
||||
title, lines = confirmation_for_folder_schedule(report)
|
||||
|
|
@ -2997,7 +2997,7 @@ async def handle_intent(store: WorldStore, session_id: str, intent: IntentResult
|
|||
store.save()
|
||||
return AgentReply(
|
||||
text=(report.get("markdown") or "")
|
||||
+ "\n\n必备数据已齐。下面请确认:**导入这些表格并试排一版**(可回滚)。",
|
||||
+ "\n\n必备数据已齐。请确认:**导入这些表格并生成试排方案**(支持回滚)。",
|
||||
blocks=[analyze_block, block],
|
||||
)
|
||||
if name in ("assistant.reply", "unknown"):
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ from __future__ import annotations # 前向类型引用
|
|||
import asyncio # 流式分片的微延时
|
||||
import contextvars # 线程池任务继承登录/租户上下文
|
||||
import json # SSE 载荷序列化
|
||||
import logging # 记录异常,避免把内部细节返回给用户
|
||||
from contextlib import asynccontextmanager # 自动化后台 tick 生命周期 # SSE 载荷序列化
|
||||
import os
|
||||
import uuid # 会话 ID 生成
|
||||
|
|
@ -77,6 +78,9 @@ from server.auth.providers import AuthError, get_auth_provider
|
|||
from server.gateway.plan_api import router as plan_router
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------- 请求体契约 ----------------
|
||||
class ChatRequest(BaseModel):
|
||||
"""对话请求:会话 ID + 用户文本 + 可选多轮上文。"""
|
||||
|
|
@ -1048,7 +1052,7 @@ def create_app() -> FastAPI:
|
|||
from server.contracts import AgentReply as _AgentReply
|
||||
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():
|
||||
yield _sse(ev)
|
||||
|
||||
|
|
@ -1071,11 +1075,11 @@ def create_app() -> FastAPI:
|
|||
return
|
||||
if follow.get("kind") in ("affirm", "refer") and follow.get("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
|
||||
reply = None
|
||||
emit_thinking("理解意图", "正在识别你要做什么…", pct=5)
|
||||
emit_thinking("确认目标", "正在确认需要执行的业务操作…", pct=5)
|
||||
for ev in drain_thinking():
|
||||
yield _sse(ev)
|
||||
|
||||
|
|
@ -1102,15 +1106,15 @@ def create_app() -> FastAPI:
|
|||
dump.get("params", {}).pop("_history", None)
|
||||
yield _sse({"type": "intent", "intent": dump})
|
||||
_THINK_BY_INTENT = {
|
||||
"data.analyze": "深度分析项目数据(可能含大 SQL,请稍候)",
|
||||
"folder.analyze": "解析工程目录表格",
|
||||
"folder.schedule": "导入目录数据并准备试排",
|
||||
"flex.schedule": "运行柔性排产引擎",
|
||||
"schedule.run": "运行排产引擎",
|
||||
"schedule.wizard": "启动排产向导",
|
||||
"data.analyze": "正在分析项目数据",
|
||||
"folder.analyze": "正在解析工程目录",
|
||||
"folder.schedule": "正在导入目录数据并准备试排",
|
||||
"flex.schedule": "正在生成柔性排产方案",
|
||||
"schedule.run": "正在生成排产方案",
|
||||
"schedule.wizard": "正在检查排产条件",
|
||||
}
|
||||
tip = _THINK_BY_INTENT.get(intent.intent, f"执行 {intent.intent}")
|
||||
emit_thinking("开始执行", tip, pct=8)
|
||||
tip = _THINK_BY_INTENT.get(intent.intent, "正在执行已确认的业务操作")
|
||||
emit_thinking("执行操作", tip, pct=8)
|
||||
for ev in drain_thinking():
|
||||
yield _sse(ev)
|
||||
|
||||
|
|
@ -1170,8 +1174,9 @@ def create_app() -> FastAPI:
|
|||
store.save()
|
||||
except Exception: # noqa: BLE001, S110 - 尽力而为:滚动摘要任何异常降级为不滚动,绝不阻断对话
|
||||
pass
|
||||
except Exception as exc: # 兜底:任何异常转为 error 事件(连接不中断)
|
||||
yield _sse({"type": "error", "message": f"处理失败:{exc}"})
|
||||
except Exception as exc: # 兜底:异常记录日志,用户只看到可执行提示
|
||||
logger.exception("chat request failed", exc_info=exc)
|
||||
yield _sse({"type": "error", "message": "本次操作未完成,数据未修改。请稍后重试;如持续发生,请联系管理员。"})
|
||||
finally:
|
||||
end_progress()
|
||||
yield _sse({"type": "done"}) # ⑧ 结束标记
|
||||
|
|
|
|||
|
|
@ -79,6 +79,15 @@ def test_simulate_schedule_does_not_hijack_other_intents():
|
|||
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"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_what_time_is_it():
|
||||
world = seed_world()
|
||||
|
|
@ -103,7 +112,8 @@ async def test_unknown_handle_gives_project_brief_not_tip_spam():
|
|||
async def test_explain_scheduling_not_inventory_dump():
|
||||
world = seed_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
|
||||
|
|
@ -113,7 +123,8 @@ async def test_explain_scheduling_not_inventory_dump():
|
|||
async def test_can_schedule_not_inventory_dump():
|
||||
world = seed_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 reply.blocks
|
||||
|
||||
|
|
@ -122,7 +133,21 @@ async def test_can_schedule_not_inventory_dump():
|
|||
async def test_assistant_offline_analyze_style():
|
||||
world = seed_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
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ def test_guidance_empty_schedule():
|
|||
g = build_guidance(world)
|
||||
cmds = {s["command"] for s in g["suggestions"]}
|
||||
assert any(s["id"] == "no_version" for s in g["signals"])
|
||||
assert "试排一版交期优先" in cmds
|
||||
assert "生成一版交期优先试排方案" in cmds
|
||||
|
||||
|
||||
def test_guidance_knowledge_miss_context():
|
||||
|
|
|
|||
|
|
@ -108,11 +108,11 @@ def test_intent_and_analyze_project_data():
|
|||
r = parse_fast(text, world)
|
||||
assert r is not None and r.intent == "data.analyze", text
|
||||
text = analyze_project_data(world, "分析项目数据")
|
||||
assert "手头有啥" in text or "要排的单子" in text
|
||||
assert "我帮你看了看" in text or "能直接开排" in text
|
||||
assert "项目数据概览" in text
|
||||
assert "待排订单" in text and "可直接排产" in text
|
||||
text2 = analyze_project_data(
|
||||
world,
|
||||
"【文件解析】\n文件:a.xlsx\n有效 2 行 · 错误 0 行(可入库)\n\n给我分析一下",
|
||||
)
|
||||
assert "你刚附的表格" in text2
|
||||
assert "附件解析结果" in text2
|
||||
assert "a.xlsx" in text2
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ def test_wizard_recommends_template_for_missing_routing():
|
|||
"status": "RELEASED"})
|
||||
|
||||
reply = dialog.start_wizard(store, "w2")
|
||||
assert "没有工艺路线" in reply.text
|
||||
assert "尚未配置工艺路线" in reply.text
|
||||
assert reply.blocks and reply.blocks[0].type == "wizard"
|
||||
tpls = reply.blocks[0].props.get("templates") or []
|
||||
assert tpls, "应推荐至少一个行业模板"
|
||||
|
|
@ -115,7 +115,7 @@ def test_wizard_default_calendar():
|
|||
assert "班次日历" in reply.text
|
||||
reply2 = dialog.pre_route(store, "w4", "默认日历")
|
||||
assert isinstance(reply2, AgentReply)
|
||||
assert "默认日历" in reply2.text
|
||||
assert "默认班次日历" in reply2.text
|
||||
assert store.data["flexCalendar"], "应写入默认日历"
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue