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 += '' }) + html += '
' + escapeHtml(row.name) + '' + row.value + '
' + 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 @@
- DeepSeekV4Pro AI助手 + 品质管理AI助手
@@ -81,7 +81,8 @@ export default { }, messages: [welcomeMessage()], messageId: 2, - chartInstances: {} + chartInstances: {}, + lastSpcWarning: null } }, computed: { @@ -204,8 +205,19 @@ export default { this.question = '' this.loading = true this.scrollToBottom() - axios.post(this.getBaseUrl() + '/api/assistant/chat', { question: content }).then(response => { + axios.post(this.getBaseUrl() + '/api/assistant/chat', { + question: content, + context: { + lastSpcWarning: this.lastSpcWarning + } + }).then(response => { const payload = response.data || {} + if (payload.data && payload.data.analysis) { + this.lastSpcWarning = { + analysis: payload.data.analysis, + updatedAt: new Date().toISOString() + } + } const message = { id: this.messageId++, role: 'assistant', @@ -414,6 +426,42 @@ export default { font-weight: 600; } +.ai-risk-badge { + display: inline-flex; + align-items: center; + min-width: 48px; + height: 22px; + padding: 0 8px; + border-radius: 3px; + font-size: 12px; + font-weight: 600; + line-height: 22px; +} + +.ai-risk-normal { + color: #166534; + background: #dcfce7; + border: 1px solid #86efac; +} + +.ai-risk-watch { + color: #854d0e; + background: #fef9c3; + border: 1px solid #fde68a; +} + +.ai-risk-warning { + color: #9a3412; + background: #ffedd5; + border: 1px solid #fdba74; +} + +.ai-risk-critical { + color: #b91c1c; + background: #fee2e2; + border: 1px solid #fca5a5; +} + .ai-detail-table-wrap { width: 100%; overflow-x: auto;