Update AI assistant

This commit is contained in:
meswork
2026-05-28 13:45:16 +08:00
parent 9a2edb15c3
commit 318414394a
6 changed files with 1378 additions and 8 deletions

View File

@@ -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 助手界面展示为主,不新增独立页面。
- 本阶段只输出技术文档,不修改代码。

View File

@@ -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": [
{

View File

@@ -1,6 +1,6 @@
# WC-SPC 项目静态分析
生成时间2026/5/27 16:14:27
生成时间2026/5/28 10:01:21
## 结论

View File

@@ -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 = '<h4>AI\u52a9\u624b\u4f7f\u7528\u8bf4\u660e</h4>' + 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 += '<h4>\u53ef\u7ed8\u5236\u7684SPC\u56fe\u8868</h4>' + 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 += '<h4>\u6ce8\u610f\u4e8b\u9879</h4>' + 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 '<span class="ai-risk-badge ' + className + '">' + escapeHtml(level) + '</span>'
}
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 = '<h4>SPC智能预测预警结论</h4><table class="ai-result-table"><tbody>'
conclusionRows.forEach(row => { html += '<tr><th>' + escapeHtml(row.name) + '</th><td>' + row.value + '</td></tr>' })
html += '</tbody></table>'
html += '<h4>预警规则命中</h4>' + table([{ key: 'name', label: '规则' }, { key: 'hit', label: '是否命中' }, { key: 'level', label: '风险等级' }, { key: 'score', label: '分数' }, { key: 'description', label: '说明' }], analysis.rules)
html += '<h4>Cp/Cpk工序能力趋势</h4>' + 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 += '<h4>置信度说明</h4>' + table([{ key: 'reason', label: '说明' }], analysis.confidenceReasons.map(reason => ({ reason })))
html += '<h4>改进建议</h4>' + 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 '<h4>SPC\u4e3b\u8981\u53c2\u6570\u8ba1\u7b97\u7ed3\u679c</h4>' + 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 = '<h4>SPC\u4e3b\u8981\u53c2\u6570\u8ba1\u7b97\u7ed3\u679c</h4>' + 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 += '<h4>\u8ba1\u7b97\u516c\u5f0f</h4>' + 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 += '<h4>\u80fd\u529b\u5224\u65ad</h4>' + 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 = '<h4>质量数据查询前置条件</h4>' + 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 += '<h4>工位号列表</h4>' + table([{ key: 'stationNo', label: '工位号' }, { key: 'stationName', label: '工位名称' }], rows)
} else if (kind === 'position') {
html += '<h4>测量位置列表</h4>' + table([{ key: 'stationNo', label: '工位号' }, { key: 'testPosition', label: '测量位置' }], rows)
} else {
html += '<h4>测量项目/检测内容列表</h4>' + table([{ key: 'stationNo', label: '工位号' }, { key: 'testPosition', label: '测量位置' }, { key: 'testItem', label: '测量项目/检测内容' }], rows)
}
html += '<h4>下一步建议</h4>' + 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 = '<h4>\u8d28\u91cf\u6570\u636e\u67e5\u8be2\u524d\u7f6e\u6761\u4ef6</h4>' + 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 += '<h4>\u5de5\u4f4d\u53f7\u5217\u8868</h4>' + table([{ key: 'stationNo', label: '\u5de5\u4f4d\u53f7' }, { key: 'stationName', label: '\u5de5\u4f4d\u540d\u79f0' }], rows)
html += '<h4>\u4e0b\u4e00\u6b65\u5efa\u8bae</h4>' + 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: '<h4>\u8d28\u91cf\u6570\u636e\u67e5\u8be2\u524d\u7f6e\u6761\u4ef6</h4>' + 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 = '<h4>\u8d28\u91cf\u6570\u636e\u67e5\u8be2\u524d\u7f6e\u6761\u4ef6</h4>' + 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 += '<h4>\u5de5\u4f4d\u53f7\u5217\u8868</h4>' + table([{ key: 'stationNo', label: '\u5de5\u4f4d\u53f7' }, { key: 'stationName', label: '\u5de5\u4f4d\u540d\u79f0' }], rows)
} else if (kind === 'position') {
html += '<h4>\u6d4b\u91cf\u4f4d\u7f6e\u5217\u8868</h4>' + table([{ key: 'stationNo', label: '\u5de5\u4f4d\u53f7' }, { key: 'testPosition', label: '\u6d4b\u91cf\u4f4d\u7f6e' }], rows)
} else {
html += '<h4>\u6d4b\u91cf\u9879\u76ee/\u68c0\u6d4b\u5185\u5bb9\u5217\u8868</h4>' + 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 += '<h4>\u4e0b\u4e00\u6b65\u5efa\u8bae</h4>' + 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 '<h4>预测预警结果综述</h4>' + 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 = '<h4>预测预警结果综述</h4>' + table([{ key: 'name', label: '项目' }, { key: 'value', label: '内容' }], summaryRows)
html += '<h4>已触发规则</h4>' + table([{ key: 'name', label: '规则' }, { key: 'level', label: '等级' }, { key: 'score', label: '分数' }, { key: 'description', label: '说明' }], hitRules.length ? hitRules : [{ name: '无高风险规则', level: '正常', score: 0, description: '当前结果未命中高风险规则。' }])
html += '<h4>改进建议</h4>' + 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: '<h4>质量数据查询入口已定位</h4>' + 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) {
: '<h4>质量数据查询功能</h4>' + 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 = '<h4>图表输出</h4>' + 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 })

View File

@@ -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' // 福田

View File

@@ -6,7 +6,7 @@
<div class="ai-header" @mousedown="startDrag">
<div class="ai-title">
<span class="ai-dot"/>
<span v-if="!collapsed">DeepSeekV4Pro AI助手</span>
<span v-if="!collapsed">品质管理AI助手</span>
</div>
<div class="ai-actions">
<el-tooltip v-if="!collapsed" :content="'窗口尺寸:' + sizeLabel" effect="dark" placement="top">
@@ -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;