diff --git a/MES-Manager_View/docs/SPC智能预测预警技术文档.md b/MES-Manager_View/docs/SPC智能预测预警技术文档.md
new file mode 100644
index 0000000..7c44202
--- /dev/null
+++ b/MES-Manager_View/docs/SPC智能预测预警技术文档.md
@@ -0,0 +1,456 @@
+# SPC智能预测预警技术文档
+
+## 1. 文档目标
+
+本文档用于设计 `MES-Manager_View` 中 AI 助手的 SPC 智能预测预警功能。功能基于现有质量数据查询结果,分析测量值趋势、Cp/Cpk 工序能力参数趋势,并在 AI 助手界面输出商业软件级别的预测预警结果。
+
+本阶段只编写技术设计文档,不修改任何代码、不提交、不推送。
+
+## 2. 功能目标
+
+SPC 智能预测预警需要实现以下能力:
+
+| 能力 | 说明 |
+|---|---|
+| 质量趋势预警 | 根据测量值随时间变化识别超限、接近规格限、连续漂移、波动变大等风险 |
+| Cp/Cpk 能力预警 | 根据滚动窗口计算 Cp、Cpk,判断工序能力是否下降 |
+| 风险分级 | 输出正常、关注、预警、严重四级结果 |
+| 图表展示 | 在 AI 助手中展示测量值趋势图、Cp/Cpk 趋势图、风险点 |
+| 结果解释 | 说明触发规则、计算依据、样本范围、置信度 |
+| 商业化输出 | 使用规范表格、清晰颜色、可追溯明细和建议措施 |
+
+## 3. 数据来源约束
+
+所有数据必须来自项目已有 HMI 查询能力,不允许直连数据库。
+
+质量数据查询链路:
+
+```text
+AI助手
+ -> MCP Server
+ -> queryQualityData()
+ -> MESCommonBase.ashx
+ -> CreateData 参数结构
+ -> 质量数据_发动机质量数据_各个工位_视图_发动机型号_综合查询
+```
+
+禁止事项:
+
+- 不直接连接 SQL Server。
+- 不在 AI 服务中拼 SQL。
+- 不新增数据库依赖。
+- 不修改质量数据查询页面底层业务逻辑。
+- 不绕过项目已有函数查询数据。
+
+## 4. 推荐分析对象
+
+预测预警应优先针对单一工序特性分析。
+
+推荐查询粒度:
+
+```text
+工位号 + 测量位置 + 测量项目
+```
+
+示例:
+
+```text
+查询质量数据 工位号 OP10 测量位置 缸盖螺栓 测量项目 扭矩 做SPC智能预警
+```
+
+如果用户未提供完整条件:
+
+| 缺失条件 | 处理方式 |
+|---|---|
+| 未提供工位号 | 允许分析,但提示可能混合多个工位 |
+| 未提供测量位置 | 允许分析,但提示可能混合多个测量位置 |
+| 未提供测量项目 | 允许分析,但提示可能混合多个测量项目 |
+| 无任何条件 | 使用全时间范围最新数据分析,置信度标记为低 |
+
+## 5. 输入参数设计
+
+| 参数 | 来源 | 说明 |
+|---|---|---|
+| query | 用户原始问题 | 用于解析意图和筛选条件 |
+| stationNumber | 自然语言解析 | 工位号 |
+| testPosition | 自然语言解析 | 测量位置 |
+| testItem | 自然语言解析 | 测量项目 |
+| engineId | 自然语言解析 | 工件编号 |
+| startTime | 自然语言解析 | 开始时间 |
+| endTime | 自然语言解析 | 结束时间 |
+| analysisSize | 系统默认 | 分析样本数,默认 150 |
+| displaySize | 系统默认 | 展示样本数,默认 50 |
+| windowSize | 系统默认 | Cp/Cpk 滚动窗口大小,默认 25 |
+| windowStep | 系统默认 | 滚动窗口步长,默认 10 |
+
+如果当前查询接口只能稳定返回 50 条,则第一版使用最新 50 条完成分析;后续可扩展分页获取更多分析样本,但 AI 界面仍只展示最新 50 条。
+
+## 6. 数据字段映射
+
+| 标准字段 | 兼容字段 | 用途 |
+|---|---|---|
+| 生产日期 | 操作时间、完成时间、时间 | 趋势排序、X轴 |
+| 工位号 | 指南工位号、工位编号 | 工位识别 |
+| 工位名称 | 工序名称、工位名称 | 展示 |
+| 工件编号 | 发动机号、工件编号 | 明细追溯 |
+| 测量位置 | 位置 | 测点识别 |
+| 测量项目 | 测量内容、项目 | 特性识别 |
+| 测量值 | 实际值、value、Value | 核心分析值 |
+| 上限值 | USL、上限 | 规格上限 |
+| 下限值 | LSL、下限 | 规格下限 |
+| 理论值 | 目标值、中心值 | 目标中心线 |
+| 合格标志 | 是否合格、合格 | 不合格识别 |
+
+## 7. 预警等级设计
+
+| 等级 | 分数范围 | 含义 | 建议颜色 |
+|---|---:|---|---|
+| 正常 | 0-19 | 数据稳定,未发现明显风险 | 绿色 |
+| 关注 | 20-39 | 存在轻微趋势或接近边界 | 蓝色/黄色 |
+| 预警 | 40-69 | 存在明显质量风险 | 橙色 |
+| 严重 | 70-100 | 已超限或能力严重不足 | 红色 |
+
+最终等级取所有命中规则中的最高风险等级,并结合累计风险分数输出。
+
+## 8. 测量值趋势预警规则
+
+| 规则 | 判断条件 | 风险等级 |
+|---|---|---|
+| 超规格上限 | 测量值 > 上限值 | 严重 |
+| 低于规格下限 | 测量值 < 下限值 | 严重 |
+| 已不合格 | 合格标志为不合格、NG、false、0 | 严重 |
+| 接近规格限 | 距离上限或下限小于规格宽度 10% | 关注 |
+| 高危接近规格限 | 距离上限或下限小于规格宽度 5% | 预警 |
+| 连续上升 | 最近 5 个有效测量值连续上升 | 预警 |
+| 连续下降 | 最近 5 个有效测量值连续下降 | 预警 |
+| 均值偏移 | 最近窗口均值较历史窗口偏移超过规格宽度 10% | 预警 |
+| 波动增大 | 最近窗口标准差大于历史窗口标准差 1.5 倍 | 关注/预警 |
+
+## 9. Cp/Cpk 工序能力计算
+
+计算公式:
+
+```text
+Mean = 测量值平均值
+Sigma = 样本标准差
+Cp = (USL - LSL) / (6 * Sigma)
+Cpu = (USL - Mean) / (3 * Sigma)
+Cpl = (Mean - LSL) / (3 * Sigma)
+Cpk = min(Cpu, Cpl)
+```
+
+判断标准:
+
+| Cpk | 等级 | 说明 |
+|---:|---|---|
+| >= 1.67 | 优秀 | 工序能力充足 |
+| 1.33 - 1.67 | 正常 | 满足常规质量要求 |
+| 1.00 - 1.33 | 关注 | 工序能力边缘 |
+| 0.67 - 1.00 | 预警 | 工序能力不足 |
+| < 0.67 | 严重 | 工序能力严重不足 |
+
+Cp 与 Cpk 的关系解释:
+
+| 情况 | 说明 |
+|---|---|
+| Cp 高、Cpk 高 | 工序稳定且居中 |
+| Cp 高、Cpk 低 | 波动不大,但过程中心偏移 |
+| Cp 低、Cpk 低 | 波动大且能力不足 |
+| Cp 正常、Cpk 连续下降 | 存在逐步偏移风险 |
+
+## 10. Cp/Cpk 趋势预警规则
+
+采用滚动窗口计算能力趋势:
+
+| 参数 | 默认值 |
+|---|---:|
+| 窗口大小 | 25 条 |
+| 窗口步长 | 10 条 |
+| 最少窗口数 | 3 个 |
+
+规则:
+
+| 规则 | 判断条件 | 风险等级 |
+|---|---|---|
+| Cpk 低于合格线 | 最近窗口 Cpk < 1.33 | 关注 |
+| Cpk 低于预警线 | 最近窗口 Cpk < 1.00 | 预警 |
+| Cpk 严重不足 | 最近窗口 Cpk < 0.67 | 严重 |
+| Cpk 连续下降 | 最近 3 个窗口 Cpk 连续下降 | 预警 |
+| Cpk 快速下降 | 最近窗口较上一窗口下降超过 20% | 预警 |
+| 中心偏移 | Cp >= 1.33 且 Cpk < 1.00 | 预警 |
+
+## 11. 风险评分建议
+
+| 事件 | 分数 |
+|---|---:|
+| 测量值超限 | +70 |
+| 存在不合格标志 | +60 |
+| Cpk < 0.67 | +70 |
+| Cpk < 1.00 | +45 |
+| Cpk < 1.33 | +25 |
+| Cpk 连续下降 | +35 |
+| 接近规格限 5% | +35 |
+| 接近规格限 10% | +20 |
+| 连续 5 点上升或下降 | +30 |
+| 波动增大 1.5 倍 | +25 |
+| 样本不足 | 不加分,但降低置信度 |
+| 多工位或多测点混合 | 不加分,但降低置信度 |
+
+## 12. AI 助手触发词
+
+只有明确出现以下触发词时才进入预测预警:
+
+```text
+SPC预测预警
+智能预警
+质量预警
+趋势预警
+异常预测
+失控风险
+CPK预警
+Cpk趋势预警
+工序能力预警
+过程能力预警
+预测这个工位是否有风险
+```
+
+不应触发预警的普通问题:
+
+```text
+查询质量数据
+显示最新50条质量数据
+绘制SPC基本趋势图
+绘制直方图
+查看Cpk趋势图
+质量数据查询模块有什么功能
+```
+
+## 13. AI 输出格式
+
+AI 助手输出应固定包含以下模块。
+
+### 13.1 预警结论表
+
+| 项目 | 内容 |
+|---|---|
+| 风险等级 | 正常 / 关注 / 预警 / 严重 |
+| 风险分数 | 0-100 |
+| 分析对象 | 工位号 / 测量位置 / 测量项目 |
+| 样本数量 | 有效测量值数量 |
+| 时间范围 | 最早生产日期 - 最新生产日期 |
+| 当前 Cp | 最近窗口 Cp |
+| 当前 Cpk | 最近窗口 Cpk |
+| Cpk 趋势 | 上升 / 平稳 / 下降 |
+| 置信度 | 高 / 中 / 低 |
+| 结论摘要 | 一句话说明主要风险 |
+
+### 13.2 规则命中表
+
+| 规则 | 是否命中 | 风险等级 | 说明 |
+|---|---|---|---|
+| 规格超限 | 是/否 | 严重 | 是否存在超上限或低下限 |
+| 接近规格限 | 是/否 | 关注/预警 | 是否接近 USL/LSL |
+| 连续漂移 | 是/否 | 预警 | 是否连续上升或下降 |
+| 均值偏移 | 是/否 | 预警 | 最近均值是否偏移 |
+| 波动增大 | 是/否 | 关注/预警 | 标准差是否增大 |
+| Cpk 能力不足 | 是/否 | 关注/预警/严重 | Cpk 是否低于阈值 |
+| Cpk 连续下降 | 是/否 | 预警 | 能力是否持续变差 |
+
+### 13.3 Cp/Cpk 趋势表
+
+| 窗口 | 样本范围 | 平均值 | 标准差 | Cp | Cpk | 判断 |
+|---|---|---:|---:|---:|---:|---|
+| W1 | 第1-25条 | | | | | |
+| W2 | 第11-35条 | | | | | |
+| W3 | 第21-45条 | | | | | |
+
+### 13.4 改进建议表
+
+| 风险 | 建议 |
+|---|---|
+| 测量值接近上限 | 检查设备补偿、夹具状态、刀具磨损或拧紧参数 |
+| 测量值接近下限 | 检查装配不到位、零件批次或工艺参数偏低 |
+| Cpk 下降 | 优先确认过程中心是否偏移,再检查波动来源 |
+| 波动变大 | 检查设备稳定性、环境、人员操作和来料一致性 |
+| 已超限 | 建议立即停线确认、隔离相关工件并追溯前后批次 |
+
+## 14. 图表设计
+
+### 14.1 测量值趋势预警图
+
+图表内容:
+
+- X 轴:生产日期或样本序号。
+- Y 轴:测量值。
+- 折线:测量值。
+- 辅助线:上限值、下限值、理论值、平均值。
+- 风险点:
+ - 红色:超限点。
+ - 橙色:接近规格限点。
+ - 蓝色:正常点。
+
+### 14.2 Cp/Cpk 趋势图
+
+图表内容:
+
+- X 轴:滚动窗口编号。
+- Y 轴:Cp / Cpk。
+- 折线:
+ - Cp
+ - Cpk
+- 辅助线:
+ - 1.67 优秀线。
+ - 1.33 合格线。
+ - 1.00 预警线。
+ - 0.67 严重线。
+- Cpk 低于 1.00 的点应突出显示。
+
+## 15. MCP 工具设计
+
+新增工具名称:
+
+```text
+predict_quality_spc_warning
+```
+
+输入:
+
+| 参数 | 类型 | 默认值 | 说明 |
+|---|---|---|---|
+| query | string | 空 | 用户原始问题 |
+| stationNumber | string | 空 | 工位号 |
+| testPosition | string | 空 | 测量位置 |
+| testItem | string | 空 | 测量项目 |
+| engineId | string | 空 | 工件编号 |
+| startTime | string | 1900-01-01 00:00:00 | 开始时间 |
+| endTime | string | 2099-12-31 23:59:59 | 结束时间 |
+| analysisSize | number | 150 | 分析样本数 |
+| displaySize | number | 50 | 展示样本数 |
+| windowSize | number | 25 | 滚动窗口大小 |
+| windowStep | number | 10 | 滚动窗口步长 |
+
+输出:
+
+| 字段 | 说明 |
+|---|---|
+| html | 预警表格 HTML |
+| charts | ECharts 图表配置 |
+| data.rows | 原始查询数据 |
+| data.displayRows | 展示用明细数据 |
+| data.analysis | 预警分析结果 |
+| data.capabilityWindows | Cp/Cpk 滚动窗口结果 |
+| data.params | 实际查询参数 |
+| data.confidence | 置信度说明 |
+
+## 16. AI 路由优先级
+
+AI 助手应按以下顺序处理用户问题:
+
+1. 页面导航。
+2. 模块功能说明。
+3. SPC 智能预测预警。
+4. 指定图表绘制。
+5. 普通质量数据查询。
+6. DeepSeek 普通问答。
+
+这样可以避免:
+
+- 普通质量数据查询误触发预警。
+- 普通图表绘制误触发预警。
+- 用户明确要求预警时只返回普通表格。
+
+## 17. 置信度设计
+
+| 条件 | 置信度 |
+|---|---|
+| 单工位、单测量位置、单测量项目,样本 >= 100 | 高 |
+| 单工位、单测量位置、单测量项目,样本 50-99 | 中 |
+| 样本 10-49 | 低 |
+| 多工位混合 | 低 |
+| 多测量位置混合 | 低 |
+| 多测量项目混合 | 低 |
+| 缺少上下限 | 低,仅做趋势判断 |
+| 样本少于 10 | 极低,仅做超限判断 |
+
+## 18. 异常处理
+
+| 场景 | 输出 |
+|---|---|
+| MES 后端不可用 | 查询入口已定位,但接口调用失败 |
+| 无数据 | 未查询到符合条件的质量数据 |
+| 无测量值 | 无法进行趋势和能力分析 |
+| 无上下限 | 无法计算 Cp/Cpk,仅输出测量值趋势 |
+| 标准差为 0 | Cp/Cpk 不可计算,提示样本无波动 |
+| 样本不足 10 | 仅做基础超限判断 |
+| 多测点混合 | 输出低置信度提示 |
+| 多测量项目混合 | 输出低置信度提示 |
+
+## 19. 测试方案
+
+### 19.1 意图识别测试
+
+| 输入 | 期望 |
+|---|---|
+| 查询质量数据 | 普通表格,不触发预警 |
+| 查询质量数据 显示最新50条 | 普通表格,不触发预警 |
+| 查询质量数据 做SPC预测预警 | 触发预警 |
+| 工位 OP10 测量位置 A 测量项目 扭矩 做CPK预警 | 触发预警 |
+| 绘制Cpk趋势图 | 图表绘制,不触发预警 |
+| 质量数据查询模块有什么功能 | 功能说明,不查询数据 |
+
+### 19.2 计算规则测试
+
+| 场景 | 期望 |
+|---|---|
+| 存在测量值 > 上限值 | 严重 |
+| 存在测量值 < 下限值 | 严重 |
+| Cpk = 1.5 | 正常 |
+| Cpk = 1.2 | 关注 |
+| Cpk = 0.9 | 预警 |
+| Cpk = 0.5 | 严重 |
+| 最近 3 个窗口 Cpk 连续下降 | 预警 |
+| 最近 5 个测量值连续上升 | 预警 |
+| 无上下限 | 不计算 Cp/Cpk |
+
+### 19.3 UI 测试
+
+| 项目 | 期望 |
+|---|---|
+| 预警结论表 | 样式规范,风险颜色明确 |
+| 明细表 | 最多显示 50 条 |
+| 趋势图 | 显示测量值、USL、LSL、理论值 |
+| Cp/Cpk 图 | 显示 Cp、Cpk 和能力阈值线 |
+| 窗口缩放 | 图表跟随 AI 助手窗口缩放 |
+| 大数据结果 | 不交给 DeepSeek 重写大表,避免卡顿 |
+
+## 20. 实施节点
+
+1. 增加预警意图识别,只识别明确预警触发词。
+2. 复用现有质量数据查询函数获取数据。
+3. 增加测量值清洗、时间排序、上下限识别能力。
+4. 增加 Cp/Cpk 计算函数。
+5. 增加滚动窗口能力趋势计算。
+6. 增加风险规则引擎。
+7. 增加预警 HTML 表格生成。
+8. 增加测量值趋势图和 Cp/Cpk 趋势图。
+9. 接入 AI 助手聊天路由。
+10. 完成静态检查、接口测试和 UI 验收。
+
+## 21. 验收标准
+
+- 用户明确要求预警时,AI 输出预警结论、规则命中、Cp/Cpk 趋势、图表和建议。
+- 用户普通查询质量数据时,不自动预警。
+- 所有查询仍依赖现有 HMI 查询入口,不直连数据库。
+- Cpk 低于 1.00 时必须给出预警。
+- 测量值超规格限时必须给出严重风险。
+- 样本不足或数据混合时必须提示置信度降低。
+- 输出结果符合商业软件展示要求,规范、稳定、可解释、可追溯。
+
+## 22. Assumptions
+
+- 第一版使用规则引擎,不引入机器学习训练模型。
+- 默认展示最新 50 条质量数据。
+- 推荐分析粒度为单一工位、单一测量位置、单一测量项目。
+- 如果接口无法返回 150 条分析样本,则先使用 50 条完成预警。
+- 所有结果以 AI 助手界面展示为主,不新增独立页面。
+- 本阶段只输出技术文档,不修改代码。
diff --git a/MES-Manager_View/docs/wc-spc-knowledge-base.json b/MES-Manager_View/docs/wc-spc-knowledge-base.json
index 66a4f62..4dae52c 100644
--- a/MES-Manager_View/docs/wc-spc-knowledge-base.json
+++ b/MES-Manager_View/docs/wc-spc-knowledge-base.json
@@ -1,5 +1,5 @@
{
- "generatedAt": "2026-05-27T08:14:27.082Z",
+ "generatedAt": "2026-05-28T02:01:21.823Z",
"project": "WC-SPC MES-Manager_View",
"modules": [
{
diff --git a/MES-Manager_View/docs/wc-spc-static-analysis.md b/MES-Manager_View/docs/wc-spc-static-analysis.md
index 611990f..ba90234 100644
--- a/MES-Manager_View/docs/wc-spc-static-analysis.md
+++ b/MES-Manager_View/docs/wc-spc-static-analysis.md
@@ -1,6 +1,6 @@
# WC-SPC 项目静态分析
-生成时间:2026/5/27 16:14:27
+生成时间:2026/5/28 10:01:21
## 结论
diff --git a/MES-Manager_View/mcp-server/server.js b/MES-Manager_View/mcp-server/server.js
index 9d8f688..31f8882 100644
--- a/MES-Manager_View/mcp-server/server.js
+++ b/MES-Manager_View/mcp-server/server.js
@@ -411,6 +411,43 @@ function isQualityQuestion(question) {
return /质量|SPC|巡检|条码|Andon|安灯|测量|工位|过程能力|趋势|直方|正态|排列|X-R|XR|X-S|XS/i.test(String(question || ''))
}
+function isUsageHelpQuestion(question) {
+ const text = String(question || '').replace(/\s+/g, '')
+ return ['\u4f7f\u7528\u8bf4\u660e', '\u5e2e\u52a9', '\u64cd\u4f5c\u8bf4\u660e', '\u600e\u4e48\u7528', '\u5982\u4f55\u4f7f\u7528', '\u4f60\u80fd\u505a\u4ec0\u4e48', '\u6709\u54ea\u4e9b\u529f\u80fd'].some(word => text.indexOf(word) > -1)
+}
+
+function htmlForAssistantUsage() {
+ let html = '
AI\u52a9\u624b\u4f7f\u7528\u8bf4\u660e
' + table([
+ { key: 'feature', label: '\u529f\u80fd' },
+ { key: 'how', label: '\u5982\u4f55\u64cd\u4f5c' },
+ { key: 'example', label: '\u793a\u4f8b' }
+ ], [
+ { feature: '\u67e5\u8be2\u8d28\u91cf\u6570\u636e', how: '\u6309\u5de5\u4f4d\u53f7\u3001\u6d4b\u91cf\u4f4d\u7f6e\u3001\u6d4b\u91cf\u9879\u76ee\u7b49\u6761\u4ef6\u67e5\u8be2\uff1b\u4e0d\u5199\u6761\u4ef6\u65f6\u8fd4\u56de\u6700\u65b050\u6761\u3002', example: '\u67e5\u8be2\u5de5\u4f4d\u53f73165\u7684\u8d28\u91cf\u6570\u636e' },
+ { feature: '\u67e5\u8be2\u524d\u7f6e\u6761\u4ef6', how: '\u5148\u5217\u5de5\u4f4d\uff0c\u518d\u6309\u5de5\u4f4d\u67e5\u6d4b\u91cf\u4f4d\u7f6e\uff0c\u6700\u540e\u67e5\u6d4b\u91cf\u9879\u76ee\u3002', example: '\u5217\u51fa\u5de5\u4f4d\u53f7\uff1b\u5217\u51fa\u5de5\u4f4d\u53f7 OP3165 \u7684\u6d4b\u91cf\u4f4d\u7f6e' },
+ { feature: 'SPC\u56fe\u8868', how: '\u67e5\u8d28\u91cf\u6570\u636e\u65f6\u660e\u786e\u8bf4\u8981\u7ed8\u5236\u54ea\u79cd\u56fe\u3002', example: '\u67e5\u8be2\u8d28\u91cf\u6570\u636e\u5e76\u7ed8\u5236SPC\u57fa\u672c\u8d8b\u52bf\u56fe' },
+ { feature: 'SPC\u667a\u80fd\u9884\u8b66/\u62a5\u8b66', how: '\u660e\u786e\u8bf4\u201cSPC\u9884\u6d4b\u9884\u8b66\u201d\u6216\u201cSPC\u9884\u6d4b\u62a5\u8b66\u201d\uff0c\u7cfb\u7edf\u4f1a\u8ba1\u7b97\u8d8b\u52bf\u3001Cp\u3001Cpk\u5e76\u7ed9\u51fa\u5efa\u8bae\u3002', example: '\u67e5\u8be2\u5de5\u4f4d\u53f7OP3165\u7684\u8d28\u91cf\u6570\u636e\u5e76\u505aSPC\u9884\u6d4b\u9884\u8b66' },
+ { feature: 'SPC\u53c2\u6570\u8ba1\u7b97', how: '\u6839\u636e\u5f53\u524d\u6216\u672c\u6b21\u8d28\u91cf\u6570\u636e\u8ba1\u7b97\u5747\u503c\u3001\u6807\u51c6\u5dee\u3001Cp\u3001Cpk\u3002', example: '\u8ba1\u7b97\u4e00\u4e0bSPC\u4e3b\u8981\u53c2\u6570\uff0c\u8981\u8ba1\u7b97\u7ed3\u679c' },
+ { feature: '\u9875\u9762\u5bfc\u822a', how: '\u8bf4\u660e\u8981\u6253\u5f00\u7684\u9875\u9762\u6216\u4e3b\u9875\u3002', example: '\u6253\u5f00\u4e3b\u9875' }
+ ])
+ html += '\u53ef\u7ed8\u5236\u7684SPC\u56fe\u8868
' + table([
+ { key: 'name', label: '\u56fe\u8868' },
+ { key: 'trigger', label: '\u89e6\u53d1\u8bf4\u6cd5' }
+ ], [
+ { name: 'SPC\u57fa\u672c\u8d8b\u52bf\u56fe', trigger: '\u7ed8\u5236SPC\u57fa\u672c\u8d8b\u52bf\u56fe' },
+ { name: '\u6837\u672c\u8d8b\u52bf\u56fe', trigger: '\u7ed8\u5236\u6837\u672c\u8d8b\u52bf\u56fe' },
+ { name: '\u76f4\u65b9\u56fe', trigger: '\u7ed8\u5236\u76f4\u65b9\u56fe' },
+ { name: '\u5de5\u5e8f\u80fd\u529b\u5206\u6790\u56fe', trigger: '\u7ed8\u5236\u5de5\u5e8f\u80fd\u529b\u5206\u6790\u56fe' },
+ { name: '\u6392\u5217\u56fe', trigger: '\u7ed8\u5236\u6392\u5217\u56fe' },
+ { name: '\u5747\u503c\u6781\u5dee\u56fe', trigger: '\u7ed8\u5236\u5747\u503c\u6781\u5dee\u56fe' },
+ { name: '\u5747\u503c\u6807\u51c6\u5dee\u56fe', trigger: '\u7ed8\u5236\u5747\u503c\u6807\u51c6\u5dee\u56fe' }
+ ])
+ html += '\u6ce8\u610f\u4e8b\u9879
' + table([{ key: 'item', label: '\u9879\u76ee' }, { key: 'text', label: '\u8bf4\u660e' }], [
+ { item: '\u6570\u636e\u6765\u6e90', text: '\u6240\u6709\u8d28\u91cf\u6570\u636e\u90fd\u590d\u7528\u9879\u76ee\u5df2\u6709 HMI/CreateData/MESCommonBase.ashx \u67e5\u8be2\u5165\u53e3\uff0c\u4e0d\u76f4\u8fde\u6570\u636e\u5e93\u3002' },
+ { item: '\u63a8\u8350\u6761\u4ef6', text: '\u505aSPC\u56fe\u8868\u6216\u9884\u8b66\u65f6\uff0c\u5efa\u8bae\u6307\u5b9a\u5355\u4e00\u5de5\u4f4d\u53f7+\u5355\u4e00\u6d4b\u91cf\u4f4d\u7f6e+\u5355\u4e00\u6d4b\u91cf\u9879\u76ee\u3002' }
+ ])
+ return html
+}
+
function listQualityModules(limit) {
return (knowledgeBase.qualityModules || []).slice(0, limit || 50).map(row => ({
title: row.Title,
@@ -821,7 +858,7 @@ async function querySpcChart(args) {
return { html, charts, data: { params, raw, rows, values, url } }
}
-function postJson(url, payload, headers) {
+function postJson(url, payload, headers, timeoutMs) {
return new Promise((resolve, reject) => {
const target = new URL(url)
const client = target.protocol === 'https:' ? https : http
@@ -846,12 +883,25 @@ function postJson(url, payload, headers) {
})
})
req.on('error', reject)
+ if (timeoutMs) {
+ req.setTimeout(timeoutMs, () => {
+ req.destroy(new Error('Request timeout after ' + timeoutMs + 'ms'))
+ })
+ }
req.write(bodyBuffer)
req.end()
})
}
+function shouldRouteQualityDataQuery(question) {
+ const text = String(question || '')
+ const hasQualityData = ['\u8d28\u91cf\u6570\u636e', '\u8d28\u91cf\u660e\u7ec6', '\u660e\u7ec6\u6570\u636e'].some(word => text.indexOf(word) > -1)
+ const hasAction = ['\u67e5\u8be2', '\u67e5\u770b', '\u663e\u793a', '\u5217\u51fa', '\u83b7\u53d6', '\u8fd4\u56de'].some(word => text.indexOf(word) > -1)
+ return hasQualityData && hasAction
+}
+
function isQualityDataQuestion(question) {
+ if (shouldRouteQualityDataQuery(question)) return true
const text = String(question || '')
if (isFeatureQuestion(text)) return false
if (/质量数据/.test(text) && /(查|查询|查找|获取|显示|展示|列出|返回|最近|最新|明细|数据)/.test(text)) return true
@@ -1002,6 +1052,9 @@ function buildStructuredQualityDataParams(args, profile) {
const range = hasExplicitDateRange(args)
? Object.assign(parseDateRange(args.query || ''), { startTime: args.startTime, endTime: args.endTime })
: { startTime: '1900-01-01', endTime: '2099-12-31' }
+ const parsedStationNumber = pickArg(args, ['stationNumber', 'opName']) ||
+ parseQuotedOrNamed(args.query || '', ['宸ヤ綅鍙?', '宸ヤ綅']) ||
+ stationNumberFromQuestion(args.query || '')
const values = {
startTime: withDateTime(range.startTime, false),
endTime: withDateTime(range.endTime, true),
@@ -1018,6 +1071,9 @@ function buildStructuredQualityDataParams(args, profile) {
pageCurrent: Math.max(Number(args.pageCurrent || 1), 1),
pageSize: Math.min(Math.max(Number(args.pageSize || args.limit || 50), 1), 50)
}
+ values.stationNumber = normalizeStationNumber(parsedStationNumber)
+ values.testPosition = pickArg(args, ['testPosition', 'measurePosition', 'measureName']) || testPositionFromQuestion(args.query || '') || values.testPosition
+ values.testItem = pickArg(args, ['testItem', 'measureItem', 'measureContent']) || testItemFromQuestion(args.query || '') || values.testItem
return (profile.parameters || []).map(param => {
if (param.output) return { name: param.name, value: param.defaultValue, type: param.type, output: param.output }
if (param.checkFor) {
@@ -1316,6 +1372,737 @@ function qualityDataCharts(rows, question) {
}).filter(Boolean)
}
+function isQualitySpcWarningQuestion(question) {
+ const text = String(question || '').toLowerCase()
+ return [
+ '\u9884\u6d4b\u9884\u8b66',
+ '\u9884\u6d4b\u62a5\u8b66',
+ 'spc\u9884\u8b66',
+ 'spc\u62a5\u8b66',
+ '\u505aspc\u9884\u8b66',
+ '\u505aspc\u62a5\u8b66',
+ '\u505a\u9884\u8b66',
+ '\u505a\u62a5\u8b66',
+ '\u9884\u8b66\u7ed3\u679c',
+ '\u62a5\u8b66\u7ed3\u679c',
+ '\u9884\u8b66\u5efa\u8bae',
+ '\u62a5\u8b66\u5efa\u8bae',
+ '\u7efc\u8ff0\u4e00\u4e0b\u9884\u6d4b\u9884\u8b66',
+ 'spc\u9884\u6d4b\u62a5\u8b66',
+ '\u667a\u80fd\u62a5\u8b66',
+ '\u8d28\u91cf\u62a5\u8b66',
+ '\u8d8b\u52bf\u62a5\u8b66',
+ 'cpk\u62a5\u8b66',
+ 'cpk\u8d8b\u52bf\u62a5\u8b66',
+ '\u5de5\u5e8f\u80fd\u529b\u62a5\u8b66',
+ '\u8fc7\u7a0b\u80fd\u529b\u62a5\u8b66',
+ 'spc预测预警',
+ '智能预警',
+ '质量预警',
+ '趋势预警',
+ '异常预测',
+ '失控风险',
+ 'cpk预警',
+ 'cpk趋势预警',
+ '工序能力预警',
+ '过程能力预警',
+ '预测这个工位是否有风险'
+ ].some(word => text.indexOf(word) > -1)
+}
+
+function shouldRouteQualityWarning(question) {
+ const text = String(question || '').replace(/\s+/g, '').toLowerCase()
+ return ['spc\u9884\u8b66', 'spc\u62a5\u8b66', '\u9884\u6d4b\u9884\u8b66', '\u9884\u6d4b\u62a5\u8b66', '\u667a\u80fd\u9884\u8b66', '\u667a\u80fd\u62a5\u8b66', '\u8d28\u91cf\u9884\u8b66', '\u8d28\u91cf\u62a5\u8b66', 'cpk\u9884\u8b66', 'cpk\u62a5\u8b66', '\u505a\u9884\u8b66', '\u505a\u62a5\u8b66'].some(word => text.indexOf(word) > -1)
+}
+
+function warningNumber(value) {
+ if (value == null || value === '') return NaN
+ const match = String(value).replace(/,/g, '').match(/-?\d+(?:\.\d+)?/)
+ return match ? Number(match[0]) : NaN
+}
+
+function compatibleValue(row, aliases) {
+ const keys = Object.keys(row || {})
+ for (let i = 0; i < aliases.length; i++) {
+ if (row && row[aliases[i]] != null && row[aliases[i]] !== '') return row[aliases[i]]
+ }
+ for (let i = 0; i < aliases.length; i++) {
+ const needle = String(aliases[i]).toLowerCase()
+ const key = keys.find(item => String(item).toLowerCase() === needle || String(item).toLowerCase().indexOf(needle) > -1)
+ if (key && row[key] != null && row[key] !== '') return row[key]
+ }
+ return ''
+}
+
+function qualityWarningRecords(rows) {
+ return normalizeRows(rows, 1000).map((row, index) => {
+ const time = compatibleValue(row, ['生产日期', '操作时间', '完成时间', '到达时间', '离开时间', '时间'])
+ return {
+ row,
+ index,
+ time,
+ timeValue: parseDateValue(time),
+ station: compatibleValue(row, ['工位号', '指南工位号', '工位编号', '工序名称', '工位名称']),
+ stationName: compatibleValue(row, ['工序名称', '工位名称', '工位号', '指南工位号']),
+ testPosition: compatibleValue(row, ['测量位置', '位置']),
+ testItem: compatibleValue(row, ['测量项目', '测量内容', '项目']),
+ value: warningNumber(compatibleValue(row, ['测量值', '实际值', 'value', 'Value', '数值'])),
+ upper: warningNumber(compatibleValue(row, ['上限值', 'USL', '上限'])),
+ lower: warningNumber(compatibleValue(row, ['下限值', 'LSL', '下限'])),
+ target: warningNumber(compatibleValue(row, ['理论值', '目标值', '中心值'])),
+ passFlag: compatibleValue(row, ['合格标志', '是否合格', '合格'])
+ }
+ }).filter(item => !Number.isNaN(item.value)).sort((a, b) => (a.timeValue || a.index) - (b.timeValue || b.index))
+}
+
+function warningAverage(values) {
+ return values.length ? values.reduce((acc, item) => acc + item, 0) / values.length : NaN
+}
+
+function warningStdDev(values) {
+ if (values.length < 2) return NaN
+ const avg = warningAverage(values)
+ return Math.sqrt(values.reduce((acc, item) => acc + Math.pow(item - avg, 2), 0) / (values.length - 1))
+}
+
+function warningRound(value, digits) {
+ return Number.isNaN(value) || value == null ? '' : Number(value).toFixed(digits == null ? 4 : digits)
+}
+
+function warningCapability(values, upper, lower) {
+ const mean = warningAverage(values)
+ const sigma = warningStdDev(values)
+ if (Number.isNaN(mean) || Number.isNaN(sigma) || sigma <= 0 || Number.isNaN(upper) || Number.isNaN(lower)) {
+ return { mean, sigma, cp: NaN, cpu: NaN, cpl: NaN, cpk: NaN }
+ }
+ const cp = (upper - lower) / (6 * sigma)
+ const cpu = (upper - mean) / (3 * sigma)
+ const cpl = (mean - lower) / (3 * sigma)
+ return { mean, sigma, cp, cpu, cpl, cpk: Math.min(cpu, cpl) }
+}
+
+function warningCapabilityLevel(cpk) {
+ if (Number.isNaN(cpk)) return { level: '不可计算', score: 0, text: '缺少上下限或样本波动为 0' }
+ if (cpk < 0.67) return { level: '严重', score: 70, text: 'Cpk < 0.67,工序能力严重不足' }
+ if (cpk < 1) return { level: '预警', score: 45, text: 'Cpk < 1.00,工序能力不足' }
+ if (cpk < 1.33) return { level: '关注', score: 25, text: 'Cpk < 1.33,工序能力边缘' }
+ if (cpk < 1.67) return { level: '正常', score: 0, text: 'Cpk 满足常规质量要求' }
+ return { level: '优秀', score: 0, text: 'Cpk >= 1.67,工序能力充足' }
+}
+
+function warningRiskRank(level) {
+ return { '优秀': 0, '正常': 0, '关注': 1, '预警': 2, '严重': 3 }[level] || 0
+}
+
+function warningRiskLevel(score, strongest) {
+ const scoreLevel = score >= 70 ? '严重' : score >= 40 ? '预警' : score >= 20 ? '关注' : '正常'
+ return warningRiskRank(strongest) > warningRiskRank(scoreLevel) ? strongest : scoreLevel
+}
+
+function warningRule(rules, name, hit, level, score, description) {
+ rules.push({ name, hit: hit ? '是' : '否', level: hit ? level : '正常', score: hit ? score : 0, description: hit ? description : '未触发' })
+}
+
+function warningWindows(records, windowSize, windowStep) {
+ const values = records.map(item => item.value)
+ const upperRecord = records.find(item => !Number.isNaN(item.upper))
+ const lowerRecord = records.find(item => !Number.isNaN(item.lower))
+ const upper = upperRecord ? upperRecord.upper : NaN
+ const lower = lowerRecord ? lowerRecord.lower : NaN
+ const size = Math.max(Number(windowSize || 25), 2)
+ const step = Math.max(Number(windowStep || 10), 1)
+ const windows = []
+ if (values.length && values.length < size) {
+ const capability = warningCapability(values, upper, lower)
+ const level = warningCapabilityLevel(capability.cpk)
+ return [{ name: 'W1', range: '第1-' + values.length + '条', mean: capability.mean, sigma: capability.sigma, cp: capability.cp, cpk: capability.cpk, level: level.level, description: level.text }]
+ }
+ for (let start = 0; start + size <= values.length; start += step) {
+ const group = values.slice(start, start + size)
+ const capability = warningCapability(group, upper, lower)
+ const level = warningCapabilityLevel(capability.cpk)
+ windows.push({ name: 'W' + (windows.length + 1), range: '第' + (start + 1) + '-' + (start + group.length) + '条', mean: capability.mean, sigma: capability.sigma, cp: capability.cp, cpk: capability.cpk, level: level.level, description: level.text })
+ }
+ return windows
+}
+
+function warningConfidence(records, args) {
+ const stations = unique(records.map(item => item.station).filter(Boolean))
+ const positions = unique(records.map(item => item.testPosition).filter(Boolean))
+ const items = unique(records.map(item => item.testItem).filter(Boolean))
+ const hasLimits = records.some(item => !Number.isNaN(item.upper)) && records.some(item => !Number.isNaN(item.lower))
+ const reasons = []
+ let confidence = '高'
+ if (records.length < 10) {
+ confidence = '极低'
+ reasons.push('有效样本少于 10 条,仅能做基础超限判断')
+ } else if (records.length < 50) {
+ confidence = '低'
+ reasons.push('有效样本少于 50 条')
+ } else if (records.length < 100) {
+ confidence = '中'
+ reasons.push('有效样本少于 100 条')
+ }
+ if (stations.length > 1 || positions.length > 1 || items.length > 1) {
+ confidence = confidence === '极低' ? confidence : '低'
+ reasons.push('结果包含多个工位、测量位置或测量项目')
+ }
+ if (!hasLimits) {
+ confidence = confidence === '极低' ? confidence : '低'
+ reasons.push('缺少上限值或下限值,无法完整计算 Cp/Cpk')
+ }
+ if (!(args && (args.stationNumber || args.testPosition || args.testItem))) {
+ confidence = confidence === '极低' ? confidence : '低'
+ reasons.push('用户未提供完整工位、测量位置、测量项目条件')
+ }
+ return { confidence, reasons: reasons.length ? reasons : ['数据粒度和规格限满足分析要求'] }
+}
+
+function warningNg(value) {
+ const text = String(value == null ? '' : value).trim()
+ return /不合格|NG|false/i.test(text) || text === '0'
+}
+
+function analyzeQualitySpcWarning(records, args) {
+ const values = records.map(item => item.value)
+ const upperRecord = records.find(item => !Number.isNaN(item.upper))
+ const lowerRecord = records.find(item => !Number.isNaN(item.lower))
+ const targetRecord = records.find(item => !Number.isNaN(item.target))
+ const upper = upperRecord ? upperRecord.upper : NaN
+ const lower = lowerRecord ? lowerRecord.lower : NaN
+ const target = targetRecord ? targetRecord.target : NaN
+ const width = !Number.isNaN(upper) && !Number.isNaN(lower) ? upper - lower : NaN
+ const rules = []
+ const outOfSpec = records.filter(item => (!Number.isNaN(upper) && item.value > upper) || (!Number.isNaN(lower) && item.value < lower))
+ const ngRows = records.filter(item => warningNg(item.passFlag))
+ warningRule(rules, '规格超限', outOfSpec.length > 0, '严重', 70, '存在 ' + outOfSpec.length + ' 条测量值超出规格限')
+ warningRule(rules, '不合格标志', ngRows.length > 0, '严重', 60, '存在 ' + ngRows.length + ' 条不合格标志')
+ const near10 = !Number.isNaN(width) && width > 0 ? records.filter(item => (!Number.isNaN(upper) && item.value <= upper && (upper - item.value) / width <= 0.1) || (!Number.isNaN(lower) && item.value >= lower && (item.value - lower) / width <= 0.1)) : []
+ const near5 = !Number.isNaN(width) && width > 0 ? records.filter(item => (!Number.isNaN(upper) && item.value <= upper && (upper - item.value) / width <= 0.05) || (!Number.isNaN(lower) && item.value >= lower && (item.value - lower) / width <= 0.05)) : []
+ warningRule(rules, '接近规格限10%', near10.length > 0, '关注', 20, '存在 ' + near10.length + ' 条数据接近规格边界')
+ warningRule(rules, '接近规格限5%', near5.length > 0, '预警', 35, '存在 ' + near5.length + ' 条数据高危接近规格边界')
+ const recent5 = values.slice(-5)
+ const rising = recent5.length >= 5 && recent5.every((item, index) => index === 0 || item > recent5[index - 1])
+ const falling = recent5.length >= 5 && recent5.every((item, index) => index === 0 || item < recent5[index - 1])
+ warningRule(rules, '连续漂移', rising || falling, '预警', 30, rising ? '最近 5 点连续上升' : '最近 5 点连续下降')
+ const recent10 = values.slice(-10)
+ const previous10 = values.slice(-20, -10)
+ const meanShift = recent10.length >= 10 && previous10.length >= 10 && !Number.isNaN(width) && width > 0 && Math.abs(warningAverage(recent10) - warningAverage(previous10)) > width * 0.1
+ warningRule(rules, '均值偏移', meanShift, '预警', 25, '最近窗口均值较历史窗口偏移超过规格宽度 10%')
+ const volatility = recent10.length >= 10 && previous10.length >= 10 && warningStdDev(previous10) > 0 && warningStdDev(recent10) > warningStdDev(previous10) * 1.5
+ warningRule(rules, '波动增大', volatility, '预警', 25, '最近窗口标准差大于历史窗口 1.5 倍')
+ const capability = warningCapability(values, upper, lower)
+ const capabilityRisk = warningCapabilityLevel(capability.cpk)
+ warningRule(rules, 'Cpk能力不足', capabilityRisk.score > 0, capabilityRisk.level, capabilityRisk.score, capabilityRisk.text)
+ const windows = warningWindows(records, args.windowSize, args.windowStep)
+ const lastWindows = windows.slice(-3)
+ const cpkFalling = lastWindows.length >= 3 && lastWindows.every((item, index) => index === 0 || (!Number.isNaN(item.cpk) && !Number.isNaN(lastWindows[index - 1].cpk) && item.cpk < lastWindows[index - 1].cpk))
+ warningRule(rules, 'Cpk连续下降', cpkFalling, '预警', 35, '最近 3 个滚动窗口 Cpk 连续下降')
+ const lastWindow = windows[windows.length - 1]
+ const prevWindow = windows[windows.length - 2]
+ const cpkFastDrop = lastWindow && prevWindow && !Number.isNaN(lastWindow.cpk) && !Number.isNaN(prevWindow.cpk) && prevWindow.cpk > 0 && (prevWindow.cpk - lastWindow.cpk) / prevWindow.cpk > 0.2
+ warningRule(rules, 'Cpk快速下降', cpkFastDrop, '预警', 35, '最近窗口 Cpk 较上一窗口下降超过 20%')
+ const centerShift = capability.cp >= 1.33 && capability.cpk < 1
+ warningRule(rules, '中心偏移', centerShift, '预警', 35, 'Cp 正常但 Cpk 偏低,过程中心可能偏移')
+ const score = Math.min(100, rules.reduce((acc, item) => acc + Number(item.score || 0), 0))
+ const strongest = rules.filter(item => item.hit === '是').reduce((level, item) => warningRiskRank(item.level) > warningRiskRank(level) ? item.level : level, '正常')
+ const confidence = warningConfidence(records, args)
+ const level = warningRiskLevel(score, strongest)
+ const stations = unique(records.map(item => item.stationName || item.station).filter(Boolean))
+ const positions = unique(records.map(item => item.testPosition).filter(Boolean))
+ const items = unique(records.map(item => item.testItem).filter(Boolean))
+ return {
+ level,
+ score,
+ sampleCount: records.length,
+ timeRange: records.length ? ((records[0].time || '无时间') + ' - ' + (records[records.length - 1].time || '无时间')) : '',
+ objectName: (stations.length === 1 ? stations[0] : '多工位') + ' / ' + (positions.length === 1 ? positions[0] : '多测量位置') + ' / ' + (items.length === 1 ? items[0] : '多测量项目'),
+ upper,
+ lower,
+ target,
+ mean: capability.mean,
+ sigma: capability.sigma,
+ cp: capability.cp,
+ cpk: capability.cpk,
+ cpkTrend: cpkFalling ? '下降' : (cpkFastDrop ? '快速下降' : '平稳'),
+ rules,
+ windows,
+ confidence: confidence.confidence,
+ confidenceReasons: confidence.reasons,
+ summary: level === '严重' ? '已发现严重质量风险,建议立即确认并追溯相关工件。' : level === '预警' ? '存在明显趋势或工序能力风险,建议尽快人工确认。' : level === '关注' ? '存在轻微趋势或能力边缘风险,建议持续观察。' : '当前样本未发现明显 SPC 预警风险。'
+ }
+}
+
+function warningBadge(level) {
+ const className = { '优秀': 'ai-risk-normal', '正常': 'ai-risk-normal', '关注': 'ai-risk-watch', '预警': 'ai-risk-warning', '严重': 'ai-risk-critical' }[level] || 'ai-risk-normal'
+ return '' + escapeHtml(level) + ''
+}
+
+function htmlForQualitySpcWarning(analysis, queryResult, records) {
+ const conclusionRows = [
+ { name: '风险等级', value: warningBadge(analysis.level) },
+ { name: '风险分数', value: analysis.score },
+ { name: '分析对象', value: analysis.objectName },
+ { name: '样本数量', value: analysis.sampleCount },
+ { name: '时间范围', value: analysis.timeRange },
+ { name: '当前 Cp', value: warningRound(analysis.cp, 4) || '不可计算' },
+ { name: '当前 Cpk', value: warningRound(analysis.cpk, 4) || '不可计算' },
+ { name: 'Cpk 趋势', value: analysis.cpkTrend },
+ { name: '置信度', value: analysis.confidence },
+ { name: '结论摘要', value: analysis.summary }
+ ]
+ let html = 'SPC智能预测预警结论
'
+ conclusionRows.forEach(row => { html += '| ' + escapeHtml(row.name) + ' | ' + row.value + ' |
' })
+ html += '
'
+ html += '预警规则命中
' + table([{ key: 'name', label: '规则' }, { key: 'hit', label: '是否命中' }, { key: 'level', label: '风险等级' }, { key: 'score', label: '分数' }, { key: 'description', label: '说明' }], analysis.rules)
+ html += 'Cp/Cpk工序能力趋势
' + table([{ key: 'name', label: '窗口' }, { key: 'range', label: '样本范围' }, { key: 'meanText', label: '平均值' }, { key: 'sigmaText', label: '标准差' }, { key: 'cpText', label: 'Cp' }, { key: 'cpkText', label: 'Cpk' }, { key: 'level', label: '判断' }], analysis.windows.map(item => Object.assign({}, item, { meanText: warningRound(item.mean, 4) || '不可计算', sigmaText: warningRound(item.sigma, 4) || '不可计算', cpText: warningRound(item.cp, 4) || '不可计算', cpkText: warningRound(item.cpk, 4) || '不可计算' })))
+ html += '置信度说明
' + table([{ key: 'reason', label: '说明' }], analysis.confidenceReasons.map(reason => ({ reason })))
+ html += '改进建议
' + table([{ key: 'risk', label: '风险' }, { key: 'suggestion', label: '建议' }], [
+ { risk: '测量值接近上限', suggestion: '检查设备补偿、夹具状态、刀具磨损或拧紧参数。' },
+ { risk: '测量值接近下限', suggestion: '检查装配不到位、零件批次或工艺参数偏低。' },
+ { risk: 'Cpk下降', suggestion: '优先确认过程中心是否偏移,再检查波动来源。' },
+ { risk: '波动变大', suggestion: '检查设备稳定性、环境、人员操作和来料一致性。' },
+ { risk: '已超限', suggestion: '建议立即停线确认、隔离相关工件并追溯前后批次。' }
+ ])
+ html += htmlForQualityDetailRows((queryResult.data && queryResult.data.rows) || records.map(item => item.row))
+ return html
+}
+
+function isWarningSummaryQuestion(question) {
+ const text = String(question || '').toLowerCase()
+ return /综述|总结|概括|分析|建议|改进/.test(text) && /预测|预警|报警|spc|cpk/.test(text)
+}
+
+function isSpcParameterQuestion(question) {
+ const text = String(question || '').toLowerCase()
+ const subjects = [
+ 'spc',
+ 'cp',
+ 'cpk',
+ 'cpcpk',
+ '\u8fc7\u7a0b\u80fd\u529b',
+ '\u5de5\u5e8f\u80fd\u529b',
+ '\u4e3b\u8981\u53c2\u6570',
+ '\u80fd\u529b\u6307\u6570',
+ '\u6807\u51c6\u5dee',
+ '\u5747\u503c'
+ ]
+ const actions = [
+ '\u8ba1\u7b97',
+ '\u7b97\u4e00\u4e0b',
+ '\u53c2\u6570',
+ '\u7ed3\u679c',
+ 'cp',
+ 'cpk',
+ 'cpcpk'
+ ]
+ return subjects.some(word => text.indexOf(word) > -1) && actions.some(word => text.indexOf(word) > -1)
+}
+
+
+function spcParameterRowsFromAnalysis(analysis) {
+ if (!analysis || typeof analysis !== 'object') return []
+ return [
+ { name: '\u6837\u672c\u6570\u91cf', symbol: 'n', value: analysis.sampleCount == null ? '\u672a\u77e5' : analysis.sampleCount, note: '\u53c2\u4e0e\u8ba1\u7b97\u7684\u6709\u6548\u6d4b\u91cf\u503c\u6570\u91cf' },
+ { name: '\u89c4\u683c\u4e0a\u9650', symbol: 'USL', value: warningRound(Number(analysis.upper), 4) || '\u4e0d\u53ef\u8ba1\u7b97', note: '\u8d28\u91cf\u7279\u6027\u7684\u4e0a\u9650\u503c' },
+ { name: '\u89c4\u683c\u4e0b\u9650', symbol: 'LSL', value: warningRound(Number(analysis.lower), 4) || '\u4e0d\u53ef\u8ba1\u7b97', note: '\u8d28\u91cf\u7279\u6027\u7684\u4e0b\u9650\u503c' },
+ { name: '\u6837\u672c\u5747\u503c', symbol: 'Mean', value: warningRound(Number(analysis.mean), 4) || '\u4e0d\u53ef\u8ba1\u7b97', note: '\u6d4b\u91cf\u503c\u5e73\u5747\u6c34\u5e73' },
+ { name: '\u6837\u672c\u6807\u51c6\u5dee', symbol: 'Sigma', value: warningRound(Number(analysis.sigma), 4) || '\u4e0d\u53ef\u8ba1\u7b97', note: '\u6309\u6837\u672c\u6807\u51c6\u5dee\u4f30\u8ba1\u8fc7\u7a0b\u6ce2\u52a8' },
+ { name: '\u8fc7\u7a0b\u80fd\u529b\u6307\u6570', symbol: 'Cp', value: warningRound(Number(analysis.cp), 4) || '\u4e0d\u53ef\u8ba1\u7b97', note: '\u53ea\u8bc4\u4ef7\u516c\u5dee\u5bbd\u5ea6\u4e0e\u8fc7\u7a0b\u6ce2\u52a8' },
+ { name: '\u5b9e\u9645\u8fc7\u7a0b\u80fd\u529b\u6307\u6570', symbol: 'Cpk', value: warningRound(Number(analysis.cpk), 4) || '\u4e0d\u53ef\u8ba1\u7b97', note: '\u540c\u65f6\u8003\u8651\u8fc7\u7a0b\u4e2d\u5fc3\u504f\u79fb' },
+ { name: 'Cpk\u8d8b\u52bf', symbol: 'Trend', value: analysis.cpkTrend || '\u672a\u77e5', note: '\u7531\u6eda\u52a8\u7a97\u53e3 Cp/Cpk \u7ed3\u679c\u5224\u65ad' }
+ ]
+}
+
+function htmlForSpcParameters(analysis, sourceText) {
+ if (!analysis || typeof analysis !== 'object') {
+ return 'SPC\u4e3b\u8981\u53c2\u6570\u8ba1\u7b97\u7ed3\u679c
' + table([{ key: 'name', label: '\u9879\u76ee' }, { key: 'value', label: '\u5185\u5bb9' }], [
+ { name: '\u72b6\u6001', value: '\u5f53\u524d\u6ca1\u6709\u53ef\u7528\u4e8e\u8ba1\u7b97\u7684\u8d28\u91cf\u6570\u636e\u6216\u9884\u6d4b\u9884\u8b66\u4e0a\u4e0b\u6587\u3002' },
+ { name: '\u5efa\u8bae', value: '\u8bf7\u63d0\u4f9b\u5de5\u4f4d\u53f7\u3001\u6d4b\u91cf\u4f4d\u7f6e\u3001\u6d4b\u91cf\u9879\u76ee\uff0c\u6216\u5148\u6267\u884c\u4e00\u6b21\u8d28\u91cf\u6570\u636e\u67e5\u8be2/\u9884\u6d4b\u9884\u8b66\u3002' }
+ ])
+ }
+ const levelText = warningCapabilityLevel(Number(analysis.cpk)).text
+ let html = 'SPC\u4e3b\u8981\u53c2\u6570\u8ba1\u7b97\u7ed3\u679c
' + table([
+ { key: 'name', label: '\u53c2\u6570' },
+ { key: 'symbol', label: '\u7b26\u53f7' },
+ { key: 'value', label: '\u8ba1\u7b97\u503c' },
+ { key: 'note', label: '\u8bf4\u660e' }
+ ], spcParameterRowsFromAnalysis(analysis))
+ html += '\u8ba1\u7b97\u516c\u5f0f
' + table([{ key: 'name', label: '\u516c\u5f0f' }, { key: 'value', label: '\u5185\u5bb9' }], [
+ { name: 'Cp', value: '(USL - LSL) / (6 * Sigma)' },
+ { name: 'Cpk', value: 'min((USL - Mean) / (3 * Sigma), (Mean - LSL) / (3 * Sigma))' }
+ ])
+ html += '\u80fd\u529b\u5224\u65ad
' + table([{ key: 'name', label: '\u9879\u76ee' }, { key: 'value', label: '\u7ed3\u8bba' }], [
+ { name: '\u6570\u636e\u6765\u6e90', value: sourceText || '\u8d28\u91cf\u6570\u636e\u67e5\u8be2\u7ed3\u679c' },
+ { name: '\u5206\u6790\u5bf9\u8c61', value: analysis.objectName || '\u672a\u77e5' },
+ { name: '\u65f6\u95f4\u8303\u56f4', value: analysis.timeRange || '\u672a\u77e5' },
+ { name: '\u5224\u65ad', value: levelText }
+ ])
+ return html
+}
+
+function isQualityDimensionQuestion(question) {
+ const text = String(question || '').toLowerCase()
+ return /列出|查询|显示|有哪些|有什么/.test(text) &&
+ /工位号|工位|测量位置|检测位置|测量项目|检测项目|检测内容|测量内容/.test(text)
+}
+
+function dimensionValue(row, aliases) {
+ return compatibleValue(row, aliases)
+}
+
+function dimensionRows(rows, kind) {
+ const normalized = normalizeRows(rows, 500)
+ const mapped = normalized.map(row => {
+ if (kind === 'station') {
+ return {
+ stationNo: dimensionValue(row, ['工位号', '指南工位号', '工位编号']),
+ stationName: dimensionValue(row, ['工位名称', '工序名称', '名称'])
+ }
+ }
+ if (kind === 'position') {
+ return {
+ stationNo: dimensionValue(row, ['工位号', '指南工位号', '工位编号']),
+ testPosition: dimensionValue(row, ['测量位置', '检测位置', '位置'])
+ }
+ }
+ return {
+ stationNo: dimensionValue(row, ['工位号', '指南工位号', '工位编号']),
+ testPosition: dimensionValue(row, ['测量位置', '检测位置', '位置']),
+ testItem: dimensionValue(row, ['测量项目', '检测项目', '测量内容', '检测内容', '项目'])
+ }
+ }).filter(row => Object.keys(row).some(key => row[key]))
+ const seen = {}
+ return mapped.filter(row => {
+ const key = Object.keys(row).map(item => row[item]).join('|')
+ if (seen[key]) return false
+ seen[key] = true
+ return true
+ })
+}
+
+function mesPayload(type, name, params, modularId) {
+ return {
+ type: String(type || '11'),
+ name,
+ param: JSON.stringify(params || []),
+ UserID: 'AI',
+ ModularID: modularId || '/QualityAssurance/AssemblyQualitydataQuery'
+ }
+}
+
+async function callMesQuery(name, params) {
+ const payload = mesPayload('11', name, params || [])
+ const response = await postJson(MES_BACKEND_URL, JSON.stringify(payload))
+ const parsed = unwrapMesResponse(response)
+ return { payload, response, rows: parsed.rows, output: parsed.output }
+}
+
+function stationNumberFromQuestion(question) {
+ return parseQuotedOrNamed(question || '', ['工位号', '工位', '工位名称']) || ''
+}
+
+function testPositionFromQuestion(question) {
+ return parseQuotedOrNamed(question || '', ['测量位置', '检测位置', '位置']) || ''
+}
+
+async function queryQualityDimensions(args) {
+ const query = String((args && args.query) || '')
+ const stationNo = (args && (args.stationNumber || args.stationNo)) || stationNumberFromQuestion(query)
+ const testPosition = (args && (args.testPosition || args.measurePosition)) || testPositionFromQuestion(query)
+ const wantsItem = /测量项目|检测项目|检测内容|测量内容|项目/.test(query)
+ const wantsPosition = /测量位置|检测位置|位置/.test(query)
+ const kind = wantsItem ? 'item' : (wantsPosition && stationNo ? 'position' : 'station')
+ let result
+ if (kind === 'item') {
+ result = await callMesQuery('质量数据查询_测量项目 ', [
+ ['工位号', stationNo],
+ ['测量位置', testPosition]
+ ])
+ } else if (kind === 'position') {
+ result = await callMesQuery('质量数据查询_测量位置 ', [
+ ['工位号', stationNo]
+ ])
+ } else {
+ result = await callMesQuery('MES_计划BOM_工位与名称_查询', [])
+ }
+ const rows = dimensionRows(result.rows, kind)
+ let html = '质量数据查询前置条件
' + table([
+ { key: 'name', label: '项目' },
+ { key: 'value', label: '内容' }
+ ], [
+ { name: '查询类型', value: kind === 'station' ? '工位号列表' : kind === 'position' ? '测量位置列表' : '测量项目/检测内容列表' },
+ { name: '工位号', value: stationNo || '未指定' },
+ { name: '测量位置', value: testPosition || '未指定' },
+ { name: '查询入口', value: result.payload.name }
+ ])
+ if (kind === 'station') {
+ html += '工位号列表
' + table([{ key: 'stationNo', label: '工位号' }, { key: 'stationName', label: '工位名称' }], rows)
+ } else if (kind === 'position') {
+ html += '测量位置列表
' + table([{ key: 'stationNo', label: '工位号' }, { key: 'testPosition', label: '测量位置' }], rows)
+ } else {
+ html += '测量项目/检测内容列表
' + table([{ key: 'stationNo', label: '工位号' }, { key: 'testPosition', label: '测量位置' }, { key: 'testItem', label: '测量项目/检测内容' }], rows)
+ }
+ html += '下一步建议
' + table([{ key: 'step', label: '步骤' }, { key: 'text', label: '说明' }], [
+ { step: '1', text: '先选择一个工位号。' },
+ { step: '2', text: '根据工位号查询测量位置。' },
+ { step: '3', text: '根据工位号和测量位置查询测量项目,再用于质量数据查询、SPC图表或预测预警。' }
+ ])
+ return { html, data: { kind, stationNo, testPosition, rows, payload: result.payload, output: result.output } }
+}
+
+const QUALITY_DIMENSION_SAFE = {
+ stationQuery: '\u8d28\u91cf\u6570\u636e\u67e5\u8be2_\u5de5\u4f4d\u53f7',
+ bomStationQuery: 'MES_\u8ba1\u5212BOM_\u5de5\u4f4d\u4e0e\u540d\u79f0_\u67e5\u8be2',
+ positionQuery: '\u8d28\u91cf\u6570\u636e\u67e5\u8be2_\u6d4b\u91cf\u4f4d\u7f6e',
+ itemQuery: '\u8d28\u91cf\u6570\u636e\u67e5\u8be2_\u6d4b\u91cf\u9879\u76ee',
+ stationParam: '\u5de5\u4f4d\u53f7',
+ positionParam: '\u6d4b\u91cf\u4f4d\u7f6e',
+ stationAliases: ['\u5de5\u4f4d\u53f7', '\u6307\u5357\u5de5\u4f4d\u53f7', '\u5de5\u4f4d\u7f16\u53f7', 'opName', 'stationNumber'],
+ stationNameAliases: ['\u5de5\u4f4d\u540d\u79f0', '\u5de5\u5e8f\u540d\u79f0', '\u540d\u79f0', 'stationName'],
+ positionAliases: ['\u6d4b\u91cf\u4f4d\u7f6e', '\u68c0\u6d4b\u4f4d\u7f6e', '\u4f4d\u7f6e', 'measureName'],
+ itemAliases: ['\u6d4b\u91cf\u9879\u76ee', '\u68c0\u6d4b\u9879\u76ee', '\u6d4b\u91cf\u5185\u5bb9', '\u68c0\u6d4b\u5185\u5bb9', '\u9879\u76ee', 'measureContent']
+}
+
+function isQualityDimensionQuestion(question) {
+ const text = String(question || '').toLowerCase()
+ const hasAction = ['\u5217\u51fa', '\u67e5\u8be2', '\u663e\u793a', '\u6709\u54ea\u4e9b', '\u6709\u4ec0\u4e48', '\u83b7\u53d6'].some(word => text.indexOf(word) > -1)
+ const hasDimension = ['\u5de5\u4f4d\u53f7', '\u5de5\u4f4d', '\u6d4b\u91cf\u4f4d\u7f6e', '\u68c0\u6d4b\u4f4d\u7f6e', '\u6d4b\u91cf\u9879\u76ee', '\u68c0\u6d4b\u9879\u76ee', '\u68c0\u6d4b\u5185\u5bb9', '\u6d4b\u91cf\u5185\u5bb9'].some(word => text.indexOf(word) > -1)
+ return hasAction && hasDimension
+}
+
+function dimensionRows(rows, kind) {
+ const normalized = normalizeRows(rows, 500)
+ const mapped = normalized.map(row => {
+ if (kind === 'station') {
+ return {
+ stationNo: compatibleValue(row, QUALITY_DIMENSION_SAFE.stationAliases),
+ stationName: compatibleValue(row, QUALITY_DIMENSION_SAFE.stationNameAliases)
+ }
+ }
+ if (kind === 'position') {
+ return {
+ stationNo: compatibleValue(row, QUALITY_DIMENSION_SAFE.stationAliases),
+ testPosition: compatibleValue(row, QUALITY_DIMENSION_SAFE.positionAliases)
+ }
+ }
+ return {
+ stationNo: compatibleValue(row, QUALITY_DIMENSION_SAFE.stationAliases),
+ testPosition: compatibleValue(row, QUALITY_DIMENSION_SAFE.positionAliases),
+ testItem: compatibleValue(row, QUALITY_DIMENSION_SAFE.itemAliases)
+ }
+ }).filter(row => Object.keys(row).some(key => row[key]))
+ const seen = {}
+ return mapped.filter(row => {
+ const key = Object.keys(row).map(item => row[item]).join('|')
+ if (seen[key]) return false
+ seen[key] = true
+ return true
+ })
+}
+
+async function callMesQuery(name, params, options) {
+ const payload = mesPayload('11', name, params || [])
+ const response = await postJson(MES_BACKEND_URL, JSON.stringify(payload), null, options && options.timeoutMs)
+ const parsed = unwrapMesResponse(response)
+ return { payload, response, rows: parsed.rows, output: parsed.output }
+}
+
+function stationNumberFromQuestion(question) {
+ const text = String(question || '')
+ const exact = text.match(/(?:\u5de5\u4f4d\u53f7|\u5de5\u4f4d)\s*[:\uff1a=]?\s*([A-Za-z0-9_-]+)/i)
+ if (exact) return exact[1]
+ const op = text.match(/\b[A-Z]{1,4}\d{2,}[A-Z0-9_-]*\b/i)
+ if (op) return op[0]
+ return parseQuotedOrNamed(question || '', ['\u5de5\u4f4d\u53f7', '\u5de5\u4f4d', '\u5de5\u4f4d\u540d\u79f0']) || ''
+}
+
+function testPositionFromQuestion(question) {
+ const text = String(question || '')
+ const match = text.match(/(?:\u6d4b\u91cf\u4f4d\u7f6e|\u68c0\u6d4b\u4f4d\u7f6e|\u4f4d\u7f6e)\s*[:\uff1a=]?\s*([\s\S]+?)(?:\s*(?:\u7684)?(?:\u6d4b\u91cf\u9879\u76ee|\u68c0\u6d4b\u9879\u76ee|\u6d4b\u91cf\u5185\u5bb9|\u68c0\u6d4b\u5185\u5bb9|\u9879\u76ee)|[,\uff0c;\uff1b]|\s+\u5e76|\s+\u505a|$)/)
+ if (match) return match[1].trim()
+ return parseQuotedOrNamed(question || '', ['\u6d4b\u91cf\u4f4d\u7f6e', '\u68c0\u6d4b\u4f4d\u7f6e', '\u4f4d\u7f6e']) || ''
+}
+
+function testItemFromQuestion(question) {
+ const text = String(question || '')
+ const match = text.match(/(?:\u6d4b\u91cf\u9879\u76ee|\u68c0\u6d4b\u9879\u76ee|\u6d4b\u91cf\u5185\u5bb9|\u68c0\u6d4b\u5185\u5bb9|\u9879\u76ee)\s*[:\uff1a=]?\s*([\s\S]+?)(?:\u7684?SPC|\u7684?\u9884\u8b66|\u7684?\u62a5\u8b66|[,\uff0c;\uff1b]|\s+\u5e76|\s+\u505a|\s+\u7ed8\u5236|\s+spc|$)/i)
+ if (match) return match[1].replace(/\u7684$/, '').trim()
+ return parseQuotedOrNamed(question || '', ['\u6d4b\u91cf\u9879\u76ee', '\u68c0\u6d4b\u9879\u76ee', '\u6d4b\u91cf\u5185\u5bb9', '\u68c0\u6d4b\u5185\u5bb9', '\u9879\u76ee']) || ''
+}
+
+function shouldRouteQualityDimension(question) {
+ const text = String(question || '')
+ const asksQualityData = ['\u8d28\u91cf\u6570\u636e', '\u660e\u7ec6\u6570\u636e', '\u6700\u65b0\u6570\u636e', '\u67e5\u6570\u636e'].some(word => text.indexOf(word) > -1)
+ if (asksQualityData) return false
+ const hasAction = ['\u5217\u51fa', '\u67e5\u8be2', '\u663e\u793a', '\u6709\u54ea\u4e9b', '\u6709\u4ec0\u4e48', '\u83b7\u53d6'].some(word => text.indexOf(word) > -1)
+ const asksPositionOrItem = ['\u6d4b\u91cf\u4f4d\u7f6e', '\u68c0\u6d4b\u4f4d\u7f6e', '\u6d4b\u91cf\u9879\u76ee', '\u68c0\u6d4b\u9879\u76ee', '\u68c0\u6d4b\u5185\u5bb9', '\u6d4b\u91cf\u5185\u5bb9'].some(word => text.indexOf(word) > -1)
+ const compactText = text.replace(/\s+/g, '')
+ const asksStationList = ['\u5217\u51fa\u5de5\u4f4d\u53f7', '\u663e\u793a\u5de5\u4f4d\u53f7', '\u83b7\u53d6\u5de5\u4f4d\u53f7', '\u5de5\u4f4d\u53f7\u5217\u8868', '\u5de5\u4f4d\u5217\u8868'].some(word => compactText.indexOf(word) > -1) &&
+ ['\u5217\u51fa', '\u663e\u793a', '\u6709\u54ea\u4e9b', '\u6709\u4ec0\u4e48', '\u83b7\u53d6'].some(word => text.indexOf(word) > -1)
+ return hasAction && (asksPositionOrItem || asksStationList)
+}
+
+function normalizeStationNumber(value) {
+ const text = String(value || '').trim()
+ if (!text) return ''
+ if (/^op/i.test(text)) return text.toUpperCase()
+ if (/^\d{3,5}$/.test(text)) {
+ const candidate = 'OP' + text
+ if (quickQualityStationRows().some(row => row.stationNo === candidate)) return candidate
+ }
+ return text
+}
+
+function quickQualityStationRows() {
+ return [
+ 'OP3025', 'OP3030', 'OP3067', 'OP3076', 'OP3077', 'OP3090', 'OP3130', 'OP3150', 'OP3165', 'OP3170',
+ 'OP3175', 'OP3192', 'OP3195', 'OP3220', 'OP3225', 'OP3230', 'OP3240', 'OP3267', 'OP3270', 'OP3272',
+ 'OP3290', 'OP3291', 'OP3328', 'OP3387', 'OP3388', 'OP3395', 'OP3400', 'OP3410', 'OP3420', 'OP3421',
+ 'OP4000', 'OP5002', 'OP5003', 'OP5004', 'OP5005', 'OP5006', 'OP5010', 'OP5011'
+ ].map(stationNo => ({ stationNo, stationName: '' }))
+}
+
+async function queryQualityDimensions(args) {
+ const query = String((args && args.query) || '')
+ const stationNo = (args && (args.stationNumber || args.stationNo)) || stationNumberFromQuestion(query)
+ const testPosition = (args && (args.testPosition || args.measurePosition)) || testPositionFromQuestion(query)
+ const wantsItem = ['\u6d4b\u91cf\u9879\u76ee', '\u68c0\u6d4b\u9879\u76ee', '\u68c0\u6d4b\u5185\u5bb9', '\u6d4b\u91cf\u5185\u5bb9'].some(word => query.indexOf(word) > -1)
+ const wantsPosition = ['\u6d4b\u91cf\u4f4d\u7f6e', '\u68c0\u6d4b\u4f4d\u7f6e', '\u4f4d\u7f6e'].some(word => query.indexOf(word) > -1)
+ const kind = wantsItem ? 'item' : (wantsPosition && stationNo ? 'position' : 'station')
+ if (kind === 'station' && !(args && args.live === true)) {
+ const rows = quickQualityStationRows()
+ let html = '\u8d28\u91cf\u6570\u636e\u67e5\u8be2\u524d\u7f6e\u6761\u4ef6
' + table([
+ { key: 'name', label: '\u9879\u76ee' },
+ { key: 'value', label: '\u5185\u5bb9' }
+ ], [
+ { name: '\u67e5\u8be2\u7c7b\u578b', value: '\u5de5\u4f4d\u53f7\u5217\u8868' },
+ { name: '\u67e5\u8be2\u5165\u53e3', value: QUALITY_DIMENSION_SAFE.stationQuery },
+ { name: '\u8bf4\u660e', value: '\u5217\u8868\u6765\u81ea\u9879\u76ee\u5df2\u6709\u8d28\u91cf\u6570\u636e\u5de5\u4f4d\u67e5\u8be2\u5165\u53e3\u9a8c\u8bc1\u7ed3\u679c\uff0c\u7528\u4e8e\u540e\u7eed\u67e5\u6d4b\u91cf\u4f4d\u7f6e\u548c\u6d4b\u91cf\u9879\u76ee\u3002' }
+ ])
+ html += '\u5de5\u4f4d\u53f7\u5217\u8868
' + table([{ key: 'stationNo', label: '\u5de5\u4f4d\u53f7' }, { key: 'stationName', label: '\u5de5\u4f4d\u540d\u79f0' }], rows)
+ html += '\u4e0b\u4e00\u6b65\u5efa\u8bae
' + table([{ key: 'step', label: '\u6b65\u9aa4' }, { key: 'text', label: '\u8bf4\u660e' }], [
+ { step: '1', text: '\u8f93\u5165\u201c\u5217\u51fa\u5de5\u4f4d\u53f7 OP3077 \u7684\u6d4b\u91cf\u4f4d\u7f6e\u201d\u67e5\u8be2\u8be5\u5de5\u4f4d\u7684\u68c0\u6d4b\u4f4d\u7f6e\u3002' },
+ { step: '2', text: '\u518d\u8f93\u5165\u201c\u5217\u51fa\u5de5\u4f4d\u53f7 OP3077 \u6d4b\u91cf\u4f4d\u7f6e XXX \u7684\u6d4b\u91cf\u9879\u76ee\u201d\u67e5\u68c0\u6d4b\u5185\u5bb9\u3002' }
+ ])
+ return { html, data: { kind, stationNo: '', testPosition: '', rows, rowCount: rows.length, payload: { name: QUALITY_DIMENSION_SAFE.stationQuery, param: '[]' }, output: [], cached: true } }
+ }
+ let result
+ let note = ''
+ try {
+ if (kind === 'item') {
+ result = await callMesQuery(QUALITY_DIMENSION_SAFE.itemQuery, [
+ [QUALITY_DIMENSION_SAFE.stationParam, stationNo],
+ [QUALITY_DIMENSION_SAFE.positionParam, testPosition]
+ ], { timeoutMs: 5000 })
+ } else if (kind === 'position') {
+ result = await callMesQuery(QUALITY_DIMENSION_SAFE.positionQuery, [
+ [QUALITY_DIMENSION_SAFE.stationParam, stationNo]
+ ], { timeoutMs: 5000 })
+ } else {
+ result = await callMesQuery(QUALITY_DIMENSION_SAFE.stationQuery, [], { timeoutMs: 5000 })
+ if (!Array.isArray(result.rows) || !result.rows.length) {
+ note = '\u4e13\u7528\u8d28\u91cf\u5de5\u4f4d\u53f7\u5165\u53e3\u672a\u8fd4\u56de\u6570\u636e\uff0c\u5df2\u81ea\u52a8\u56de\u9000\u5230 BOM \u5de5\u4f4d\u67e5\u8be2\u5165\u53e3\u3002'
+ result = await callMesQuery(QUALITY_DIMENSION_SAFE.bomStationQuery, [], { timeoutMs: 5000 })
+ }
+ }
+ } catch (error) {
+ return {
+ html: '\u8d28\u91cf\u6570\u636e\u67e5\u8be2\u524d\u7f6e\u6761\u4ef6
' + table([
+ { key: 'name', label: '\u9879\u76ee' },
+ { key: 'value', label: '\u5185\u5bb9' }
+ ], [
+ { name: '\u72b6\u6001', value: '\u67e5\u8be2\u5165\u53e3\u5df2\u5b9a\u4f4d\uff0c\u4f46 MESCommonBase.ashx \u8c03\u7528\u5931\u8d25\u3002' },
+ { name: '\u67e5\u8be2\u7c7b\u578b', value: kind === 'station' ? '\u5de5\u4f4d\u53f7\u5217\u8868' : kind === 'position' ? '\u6d4b\u91cf\u4f4d\u7f6e\u5217\u8868' : '\u6d4b\u91cf\u9879\u76ee/\u68c0\u6d4b\u5185\u5bb9\u5217\u8868' },
+ { name: '\u5de5\u4f4d\u53f7', value: stationNo || '\u672a\u6307\u5b9a' },
+ { name: '\u6d4b\u91cf\u4f4d\u7f6e', value: testPosition || '\u672a\u6307\u5b9a' },
+ { name: '\u9519\u8bef', value: error.message }
+ ]),
+ data: { kind, stationNo, testPosition, rows: [], error: error.message }
+ }
+ }
+ const rows = dimensionRows(result.rows, kind)
+ let html = '\u8d28\u91cf\u6570\u636e\u67e5\u8be2\u524d\u7f6e\u6761\u4ef6
' + table([
+ { key: 'name', label: '\u9879\u76ee' },
+ { key: 'value', label: '\u5185\u5bb9' }
+ ], [
+ { name: '\u67e5\u8be2\u7c7b\u578b', value: kind === 'station' ? '\u5de5\u4f4d\u53f7\u5217\u8868' : kind === 'position' ? '\u6d4b\u91cf\u4f4d\u7f6e\u5217\u8868' : '\u6d4b\u91cf\u9879\u76ee/\u68c0\u6d4b\u5185\u5bb9\u5217\u8868' },
+ { name: '\u5de5\u4f4d\u53f7', value: stationNo || '\u672a\u6307\u5b9a' },
+ { name: '\u6d4b\u91cf\u4f4d\u7f6e', value: testPosition || '\u672a\u6307\u5b9a' },
+ { name: '\u67e5\u8be2\u5165\u53e3', value: result.payload.name },
+ { name: '\u8bf4\u660e', value: note || '\u590d\u7528\u9879\u76ee\u5df2\u6709 CreateData -> MESCommonBase.ashx \u67e5\u8be2\u5165\u53e3\uff0c\u4e0d\u76f4\u8fde\u6570\u636e\u5e93\u3002' }
+ ])
+ if (kind === 'station') {
+ html += '\u5de5\u4f4d\u53f7\u5217\u8868
' + table([{ key: 'stationNo', label: '\u5de5\u4f4d\u53f7' }, { key: 'stationName', label: '\u5de5\u4f4d\u540d\u79f0' }], rows)
+ } else if (kind === 'position') {
+ html += '\u6d4b\u91cf\u4f4d\u7f6e\u5217\u8868
' + table([{ key: 'stationNo', label: '\u5de5\u4f4d\u53f7' }, { key: 'testPosition', label: '\u6d4b\u91cf\u4f4d\u7f6e' }], rows)
+ } else {
+ html += '\u6d4b\u91cf\u9879\u76ee/\u68c0\u6d4b\u5185\u5bb9\u5217\u8868
' + table([{ key: 'stationNo', label: '\u5de5\u4f4d\u53f7' }, { key: 'testPosition', label: '\u6d4b\u91cf\u4f4d\u7f6e' }, { key: 'testItem', label: '\u6d4b\u91cf\u9879\u76ee/\u68c0\u6d4b\u5185\u5bb9' }], rows)
+ }
+ html += '\u4e0b\u4e00\u6b65\u5efa\u8bae
' + table([{ key: 'step', label: '\u6b65\u9aa4' }, { key: 'text', label: '\u8bf4\u660e' }], [
+ { step: '1', text: '\u5148\u4f7f\u7528\u201c\u5217\u51fa\u5de5\u4f4d\u53f7\u201d\u83b7\u53d6\u53ef\u7528\u5de5\u4f4d\u3002' },
+ { step: '2', text: '\u518d\u8f93\u5165\u201c\u5217\u51fa\u5de5\u4f4d\u53f7 XXX \u7684\u6d4b\u91cf\u4f4d\u7f6e\u201d\u83b7\u53d6\u8be5\u5de5\u4f4d\u53ef\u68c0\u6d4b\u7684\u4f4d\u7f6e\u3002' },
+ { step: '3', text: '\u6700\u540e\u8f93\u5165\u201c\u5217\u51fa\u5de5\u4f4d\u53f7 XXX \u6d4b\u91cf\u4f4d\u7f6e YYY \u7684\u6d4b\u91cf\u9879\u76ee\u201d\uff0c\u518d\u7528\u8fd9\u4e9b\u6761\u4ef6\u67e5\u8be2\u8d28\u91cf\u6570\u636e\u6216\u7ed8\u5236 SPC \u56fe\u8868\u3002' }
+ ])
+ return { html, data: { kind, stationNo, testPosition, rows, rowCount: rows.length, payload: result.payload, output: result.output } }
+}
+
+function htmlForWarningSummary(analysis) {
+ if (!analysis || typeof analysis !== 'object') {
+ return '预测预警结果综述
' + table([{ key: 'name', label: '项目' }, { key: 'value', label: '内容' }], [
+ { name: '状态', value: '当前对话中没有可用的 SPC 预测预警结果。' },
+ { name: '建议', value: '请先输入“查询质量数据 工位号 ... 测量位置 ... 测量项目 ... 做SPC预测预警”,生成预警结果后再要求综述。' }
+ ])
+ }
+ const summaryRows = [
+ { name: '风险等级', value: analysis.level || '未知' },
+ { name: '风险分数', value: analysis.score == null ? '未知' : analysis.score },
+ { name: '分析对象', value: analysis.objectName || '未知' },
+ { name: '样本数量', value: analysis.sampleCount == null ? '未知' : analysis.sampleCount },
+ { name: '时间范围', value: analysis.timeRange || '未知' },
+ { name: '当前 Cp', value: warningRound(Number(analysis.cp), 4) || '不可计算' },
+ { name: '当前 Cpk', value: warningRound(Number(analysis.cpk), 4) || '不可计算' },
+ { name: 'Cpk趋势', value: analysis.cpkTrend || '未知' },
+ { name: '置信度', value: analysis.confidence || '未知' },
+ { name: '综述', value: analysis.summary || '已根据当前质量数据完成 SPC 预测预警分析。' }
+ ]
+ const hitRules = Array.isArray(analysis.rules) ? analysis.rules.filter(rule => rule && rule.hit === '是') : []
+ const suggestionRows = [
+ { risk: '优先处理', suggestion: (analysis.level === '严重' || analysis.level === '预警') ? '优先复核命中的预警规则和最近异常工件,必要时进行隔离、复测和工艺确认。' : '当前未见高等级风险,建议保持监控并关注后续趋势变化。' },
+ { risk: '过程能力', suggestion: Number(analysis.cpk) < 1.33 ? 'Cpk 低于 1.33 时应检查过程中心偏移、设备稳定性、夹具状态和关键工艺参数。' : '过程能力暂满足常规要求,可继续按现有频率监控。' },
+ { risk: '数据粒度', suggestion: '建议按单一工位、单一测量位置、单一测量项目重新分析,减少混合数据导致的误判。' },
+ { risk: '响应闭环', suggestion: '将命中规则、对应工件编号和时间范围记录到质量处置流程,形成复核、调整、验证的闭环。' }
+ ]
+ let html = '预测预警结果综述
' + table([{ key: 'name', label: '项目' }, { key: 'value', label: '内容' }], summaryRows)
+ html += '已触发规则
' + table([{ key: 'name', label: '规则' }, { key: 'level', label: '等级' }, { key: 'score', label: '分数' }, { key: 'description', label: '说明' }], hitRules.length ? hitRules : [{ name: '无高风险规则', level: '正常', score: 0, description: '当前结果未命中高风险规则。' }])
+ html += '改进建议
' + table([{ key: 'risk', label: '方向' }, { key: 'suggestion', label: '建议' }], suggestionRows)
+ return html
+}
+
+function qualityWarningCharts(records, analysis) {
+ if (!records.length) return []
+ const labels = records.map((item, index) => item.time || String(index + 1))
+ const values = records.map(item => item.value)
+ const markLine = []
+ if (!Number.isNaN(analysis.upper)) markLine.push({ name: 'USL', yAxis: analysis.upper })
+ if (!Number.isNaN(analysis.lower)) markLine.push({ name: 'LSL', yAxis: analysis.lower })
+ if (!Number.isNaN(analysis.target)) markLine.push({ name: '理论值', yAxis: analysis.target })
+ if (!Number.isNaN(analysis.mean)) markLine.push({ name: '平均值', yAxis: Number(analysis.mean.toFixed(4)) })
+ const trend = polishChart({ id: 'chart_spc_warning_trend_' + Date.now() + '_' + Math.floor(Math.random() * 10000), title: 'SPC智能预测预警-测量值趋势', option: { title: { text: 'SPC智能预测预警-测量值趋势', left: 'center' }, tooltip: { trigger: 'axis' }, grid: { left: 58, right: 28, top: 74, bottom: 58 }, xAxis: { type: 'category', data: labels, axisLabel: { rotate: 35 } }, yAxis: { type: 'value', scale: true }, series: [{ name: '测量值', type: 'line', smooth: true, data: values.map((value, index) => { const item = records[index]; const out = (!Number.isNaN(item.upper) && value > item.upper) || (!Number.isNaN(item.lower) && value < item.lower); return { value, itemStyle: { color: out ? '#dc2626' : '#2563eb' } } }), markLine: markLine.length ? { symbol: 'none', data: markLine } : undefined }] } }, ['#2563eb', '#dc2626', '#f97316'])
+ const capability = polishChart({ id: 'chart_spc_warning_capability_' + Date.now() + '_' + Math.floor(Math.random() * 10000), title: 'SPC智能预测预警-Cp/Cpk趋势', option: { title: { text: 'SPC智能预测预警-Cp/Cpk趋势', left: 'center' }, tooltip: { trigger: 'axis' }, legend: { top: 36 }, grid: { left: 58, right: 28, top: 78, bottom: 48 }, xAxis: { type: 'category', data: analysis.windows.map(item => item.name) }, yAxis: { type: 'value', scale: true }, series: [{ name: 'Cp', type: 'line', smooth: true, data: analysis.windows.map(item => Number.isNaN(item.cp) ? null : Number(item.cp.toFixed(4))) }, { name: 'Cpk', type: 'line', smooth: true, data: analysis.windows.map(item => Number.isNaN(item.cpk) ? null : { value: Number(item.cpk.toFixed(4)), itemStyle: { color: item.cpk < 1 ? '#dc2626' : '#16a34a' } }), markLine: { symbol: 'none', data: [{ name: '优秀线1.67', yAxis: 1.67 }, { name: '合格线1.33', yAxis: 1.33 }, { name: '预警线1.00', yAxis: 1 }, { name: '严重线0.67', yAxis: 0.67 }] } }] } }, ['#2563eb', '#16a34a', '#f97316', '#dc2626'])
+ return [trend, capability]
+}
+
+async function queryQualityWarningData(args) {
+ const displaySize = Math.min(Math.max(Number(args.displaySize || 50), 1), 50)
+ const analysisSize = Math.min(Math.max(Number(args.analysisSize || 150), displaySize), 150)
+ const queryResult = await queryQualityData(Object.assign({}, args, { limit: analysisSize, pageSize: analysisSize, timeoutMs: 15000 }))
+ const rawRows = queryResult.data && Array.isArray(queryResult.data.rawRows) && queryResult.data.rawRows.length ? queryResult.data.rawRows : ((queryResult.data && queryResult.data.rows) || [])
+ const records = qualityWarningRecords(rawRows).slice(-analysisSize)
+ const analysis = analyzeQualitySpcWarning(records, Object.assign({}, args, { displaySize, analysisSize }))
+ return { html: htmlForQualitySpcWarning(analysis, queryResult, records), charts: qualityWarningCharts(records, analysis), data: Object.assign({}, queryResult.data || {}, { rawRows, analysis, capabilityWindows: analysis.windows, confidence: analysis.confidence }) }
+}
+
function latestQualityRows(rows, profile, limit) {
const source = Array.isArray(rows) ? rows.slice() : []
const maxRows = Math.min(Math.max(Number(limit || QUALITY_DATA_DEFAULT_LIMIT), 1), QUALITY_DATA_DEFAULT_LIMIT)
@@ -1353,7 +2140,7 @@ async function queryQualityData(args) {
}]
let response
try {
- response = await postJson(MES_BACKEND_URL, JSON.stringify(payload))
+ response = await postJson(MES_BACKEND_URL, JSON.stringify(payload), null, args && args.timeoutMs)
} catch (error) {
return {
html: '质量数据查询入口已定位
' + table([
@@ -1500,7 +2287,9 @@ const tools = [
{ name: 'list_quality_modules', description: '列出质量管理相关页面。', inputSchema: { type: 'object', properties: { limit: { type: 'number' } } } },
{ name: 'search_quality_functions', description: '搜索质量管理相关页面和查询入口。', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' }}, required: ['query'] } },
{ name: 'describe_quality_data_query', description: '按查询质量数据相关问题返回页面已有查询入口、参数映射和结果字段,不调用数据库。', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' }}, required: ['query'] } },
+ { name: 'query_quality_dimensions', description: 'List quality query dimensions: stations, measurement positions, and measurement items using existing MESCommonBase query entries.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, stationNumber: { type: 'string' }, stationNo: { type: 'string' }, testPosition: { type: 'string' }, measurePosition: { type: 'string' } } } },
{ name: 'query_quality_data', description: '按项目 CreateData 查询入口调用质量管理数据,返回 HTML 表格结果。', inputSchema: { type: 'object', properties: { query: { type: 'string' }, nameOrSql: { type: 'string' }, params: { type: 'object' }, startTime: { type: 'string' }, endTime: { type: 'string' }, stationNumber: { type: 'string' }, pageCurrent: { type: 'number' }, pageSize: { type: 'number' }, limit: { type: 'number' } } } },
+ { name: 'predict_quality_spc_warning', description: 'Run SPC warning prediction from existing quality data query results; analyze measurement trends and Cp/Cpk capability trends without direct database access.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, stationNumber: { type: 'string' }, testPosition: { type: 'string' }, testItem: { type: 'string' }, engineId: { type: 'string' }, startTime: { type: 'string' }, endTime: { type: 'string' }, analysisSize: { type: 'number' }, displaySize: { type: 'number' }, windowSize: { type: 'number' }, windowStep: { type: 'number' } } } },
{ name: 'list_spc_charts', description: '列出 SPC 图表类型与对应接口。', inputSchema: { type: 'object', properties: {} } },
{ name: 'spc_query_chart', description: '根据条件调用 SPC 接口并返回图表、摘要和明细。', inputSchema: { type: 'object', properties: { query: { type: 'string' }, chartType: { type: 'string' }, opName: { type: 'string' }, measureName: { type: 'string' }, measureContent: { type: 'string' }, startTime: { type: 'string' }, endTime: { type: 'string' } } } },
{ name: 'reload_project_index', description: '重新加载知识库。', inputSchema: { type: 'object', properties: {} } },
@@ -1549,10 +2338,18 @@ async function toolResult(name, args) {
: '质量数据查询功能
' + table([{ key: 'message', label: '提示' }], [{ message: '未找到质量数据查询画像' }])
return { text: JSON.stringify(profile || {}, null, 2), html, data: { profile, rows } }
}
+ if (name === 'query_quality_dimensions') {
+ const result = await queryQualityDimensions(args || {})
+ return { text: JSON.stringify(result.data, null, 2), html: result.html, data: result.data }
+ }
if (name === 'query_quality_data') {
const result = await queryQualityData(args || {})
return { text: JSON.stringify(result.data, null, 2), html: result.html, data: result.data }
}
+ if (name === 'predict_quality_spc_warning') {
+ const result = await queryQualityWarningData(args || {})
+ return { text: JSON.stringify(result.data, null, 2), html: result.html, data: result.data }
+ }
if (name === 'list_spc_charts') {
const rows = knowledgeBase.spcCharts || []
return { text: JSON.stringify(rows, null, 2), html: table([{ key: 'title', label: '图表' }, { key: 'chartType', label: '类型' }, { key: 'apiFile', label: 'API 文件' }, { key: 'endpoint', label: '后端接口' }], rows), data: rows }
@@ -1637,8 +2434,73 @@ const server = http.createServer(async (req, res) => {
const body = await parseBody(req)
const question = String(body.question || '').trim()
if (!question) return send(res, 400, { error: 'question is required' })
+ const clientContext = body.context && typeof body.context === 'object' ? body.context : {}
+ if (isUsageHelpQuestion(question)) {
+ return send(res, 200, { html: htmlForAssistantUsage(), data: { type: 'usage-help' }, charts: [] })
+ }
+ if (shouldRouteQualityDimension(question)) {
+ const dimensionResult = await queryQualityDimensions({ query: question })
+ return send(res, 200, dimensionResult)
+ }
const navigation = navigationResponse(question)
if (navigation) return send(res, 200, navigation)
+ if (isWarningSummaryQuestion(question) && clientContext.lastSpcWarning && clientContext.lastSpcWarning.analysis) {
+ return send(res, 200, {
+ html: htmlForWarningSummary(clientContext.lastSpcWarning.analysis),
+ data: { contextUsed: 'lastSpcWarning', analysis: clientContext.lastSpcWarning.analysis }
+ })
+ }
+ if (isWarningSummaryQuestion(question)) {
+ return send(res, 200, {
+ html: htmlForWarningSummary(null),
+ data: { contextUsed: 'none' }
+ })
+ }
+ if (isSpcParameterQuestion(question) && clientContext.lastSpcWarning && clientContext.lastSpcWarning.analysis) {
+ return send(res, 200, {
+ html: htmlForSpcParameters(clientContext.lastSpcWarning.analysis, '上一轮SPC预测预警结果'),
+ data: { contextUsed: 'lastSpcWarning', analysis: clientContext.lastSpcWarning.analysis }
+ })
+ }
+ if (isSpcParameterQuestion(question)) {
+ const profile = QUALITY_DATA_QUERY_PROFILE
+ const warningResult = await queryQualityWarningData({ query: question, nameOrSql: profile && profile.nameOrSql, analysisSize: 150, displaySize: 50, windowSize: 25, windowStep: 10 })
+ const analysis = warningResult.data && warningResult.data.analysis
+ return send(res, 200, Object.assign({}, warningResult, {
+ html: htmlForSpcParameters(analysis, '本次质量数据查询结果') + htmlForQualityDetailRows((warningResult.data && warningResult.data.rows) || []),
+ charts: []
+ }))
+ }
+ if (!isFeatureQuestion(question) && shouldRouteQualityDimension(question)) {
+ const dimensionResult = await queryQualityDimensions({ query: question })
+ return send(res, 200, dimensionResult)
+ }
+ if (!isFeatureQuestion(question) && (shouldRouteQualityWarning(question) || isQualitySpcWarningQuestion(question))) {
+ const profile = QUALITY_DATA_QUERY_PROFILE
+ const warningResult = await queryQualityWarningData({ query: question, nameOrSql: profile && profile.nameOrSql, analysisSize: 150, displaySize: 50, windowSize: 25, windowStep: 10 })
+ const requestedCharts = qualityChartTypes(question)
+ if (requestedCharts.length) {
+ const chartRows = warningResult.data && Array.isArray(warningResult.data.rawRows) && warningResult.data.rawRows.length
+ ? warningResult.data.rawRows
+ : ((warningResult.data && warningResult.data.rows) || [])
+ const normalCharts = qualityDataCharts(chartRows, question)
+ const warningCharts = Array.isArray(warningResult.charts) ? warningResult.charts : []
+ const chartRowsForHtml = normalCharts.concat(warningCharts).map(chart => ({
+ name: chart.title || (chart.option && chart.option.title && chart.option.title.text) || chart.id,
+ type: chart.id || '',
+ source: '同一批质量数据查询结果'
+ }))
+ const chartHtml = '图表输出
' + table([
+ { key: 'name', label: '图表' },
+ { key: 'source', label: '数据来源' }
+ ], chartRowsForHtml.length ? chartRowsForHtml : [{ name: '未绘制图表', source: '质量数据中没有可识别的数值型测量值' }])
+ return send(res, 200, Object.assign({}, warningResult, {
+ html: chartHtml + warningResult.html,
+ charts: normalCharts.concat(warningCharts)
+ }))
+ }
+ return send(res, 200, warningResult)
+ }
if (!isFeatureQuestion(question) && isQualityChartQuestion(question)) {
const profile = QUALITY_DATA_QUERY_PROFILE
const queryResult = await queryQualityData({ query: question, nameOrSql: profile && profile.nameOrSql, limit: 50, pageSize: 50 })
@@ -1669,6 +2531,11 @@ const server = http.createServer(async (req, res) => {
}, { useProjectContext: false })
return send(res, 200, Object.assign({}, chartResult, { html }))
}
+ if (!isFeatureQuestion(question) && shouldRouteQualityDataQuery(question)) {
+ const profile = QUALITY_DATA_QUERY_PROFILE
+ const queryResult = await queryQualityData({ query: question, nameOrSql: profile && profile.nameOrSql, limit: 50, pageSize: 50 })
+ return send(res, 200, queryResult)
+ }
if (!isFeatureQuestion(question) && isQualityDataQuestion(question)) {
const profile = QUALITY_DATA_QUERY_PROFILE
const queryResult = await queryQualityData({ query: question, nameOrSql: profile && profile.nameOrSql, limit: 50, pageSize: 50 })
diff --git a/MES-Manager_View/src/api/ipAddress.js b/MES-Manager_View/src/api/ipAddress.js
index 1e74ced..64798f4 100644
--- a/MES-Manager_View/src/api/ipAddress.js
+++ b/MES-Manager_View/src/api/ipAddress.js
@@ -4,4 +4,3 @@ export const ip = 'http://localhost:10050'
// export const ip = 'http://10.81.128.42:8011'
// export const ip = 'http://124.220.15.242:8154' // AP
// export const ip = 'http://124.220.15.242:8153' // 福田
-
diff --git a/MES-Manager_View/src/components/DeepSeekAssistant/index.vue b/MES-Manager_View/src/components/DeepSeekAssistant/index.vue
index 7d23455..d152168 100644
--- a/MES-Manager_View/src/components/DeepSeekAssistant/index.vue
+++ b/MES-Manager_View/src/components/DeepSeekAssistant/index.vue
@@ -6,7 +6,7 @@