Update project and configurations
This commit is contained in:
473
docs/DBUS_API.md
Normal file
473
docs/DBUS_API.md
Normal file
@@ -0,0 +1,473 @@
|
||||
# 线切割控制系统 DBus 接口说明文档
|
||||
|
||||
## 基本信息
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| **服务名** | `com.wirecut.service` |
|
||||
| **对象路径** | `/com/wirecut/control` |
|
||||
| **接口名** | `com.wirecut.IControl` |
|
||||
| **总线类型** | System Bus(系统总线) |
|
||||
| **设计用途** | 供"小龙虾"等外部程序对接线切割控制系统 |
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [运动控制接口](#1-运动控制接口)
|
||||
2. [加工参数接口](#2-加工参数接口)
|
||||
3. [状态查询接口](#3-状态查询接口)
|
||||
4. [NC 文件操作接口](#4-nc-文件操作接口)
|
||||
5. [工件坐标接口](#5-工件坐标接口)
|
||||
6. [放电设置接口](#6-放电设置接口)
|
||||
7. [信号(推送通知)](#7-信号推送通知)
|
||||
8. [状态码说明](#8-状态码说明)
|
||||
9. [命令行调用示例](#9-命令行调用示例)
|
||||
10. [注意事项](#10-注意事项)
|
||||
|
||||
---
|
||||
|
||||
## 1. 运动控制接口
|
||||
|
||||
### startRun() - 启动加工
|
||||
- **描述**: 启动加工程序,等价于点击界面"运行"按钮
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```powershell
|
||||
# 启动加工
|
||||
qdbus com.wirecut.service /com/wirecut/control startRun
|
||||
```
|
||||
|
||||
### stopRun() - 停止加工
|
||||
- **描述**: 停止当前加工,等价于点击"停止"按钮
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control stopRun
|
||||
```
|
||||
|
||||
### pauseRun() - 暂停加工
|
||||
- **描述**: 暂停加工(变频),等价于点击"变频暂停"
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus mock_qdbus.py com.wirecut.service /com/wirecut/control pauseRun
|
||||
```
|
||||
|
||||
### homeAll() - 全轴回零
|
||||
- **描述**: 所有轴执行回零操作
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control homeAll
|
||||
```
|
||||
|
||||
### startKongZou() - 开始空走
|
||||
- **描述**: 启动空走模式(不放电测试)
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control startKongZou
|
||||
```
|
||||
|
||||
### stopKongZou() - 停止空走
|
||||
- **描述**: 停止空走模式
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control stopKongZou
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 加工参数接口
|
||||
|
||||
### setSpeed(speed: int) -> int - 设置加工速度
|
||||
- **描述**: 设置加工速度
|
||||
- **参数**:
|
||||
- `speed`: 速度值,单位 mm/min,范围 1-9999
|
||||
- **返回**: 实际设置的速度值
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control setSpeed 80
|
||||
```
|
||||
|
||||
### getSpeed() -> int - 获取设定速度
|
||||
- **描述**: 获取当前设定的加工速度
|
||||
- **参数**: 无
|
||||
- **返回**: 设定速度值 (mm/min)
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control getSpeed
|
||||
```
|
||||
|
||||
### setVoltage(vol: int) - 设置放电电压
|
||||
- **描述**: 设置放电电压值
|
||||
- **参数**:
|
||||
- `vol`: 电压值
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control setVoltage 90
|
||||
```
|
||||
|
||||
### setCurrent(cur: int) - 设置放电电流
|
||||
- **描述**: 设置放电电流值
|
||||
- **参数**:
|
||||
- `cur`: 电流值
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control setCurrent 5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 状态查询接口
|
||||
|
||||
### getStatus() -> QVariantMap - 获取完整状态
|
||||
- **描述**: 获取系统完整状态信息(字典格式)
|
||||
- **参数**: 无
|
||||
- **返回字段**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `running` | int | 运行状态:0=停止 1=运行中 2=暂停 |
|
||||
| `is_homed` | int | 是否已回零:0=否 1=是 |
|
||||
| `is_homing` | int | 是否正在回零:0=否 1=是 |
|
||||
| `pos_x` | double | X轴位置 (mm) |
|
||||
| `pos_y` | double | Y轴位置 (mm) |
|
||||
| `pos_z` | double | Z轴位置 (mm) |
|
||||
| `pos_u` | double | U轴位置 (mm) |
|
||||
| `pos_v` | double | V轴位置 (mm) |
|
||||
| `vol` | int | 当前电压值 |
|
||||
| `cur` | int | 当前电流值 |
|
||||
| `daohao` | int | 导号(加工段号) |
|
||||
| `speed` | int | 设定速度 (mm/min) |
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control getStatus
|
||||
```
|
||||
|
||||
### getAxisPos() -> QString - 获取各轴位置
|
||||
- **描述**: 轻量级接口,获取各轴当前位置
|
||||
- **参数**: 无
|
||||
- **返回**: `"x,y,z,u,v"` 格式字符串,保留3位小数,单位 mm
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control getAxisPos
|
||||
# 返回示例: "10.500,20.321,0.000,0.123,0.000"
|
||||
```
|
||||
|
||||
### isRunning() -> bool - 是否正在加工
|
||||
- **描述**: 查询是否处于加工状态
|
||||
- **参数**: 无
|
||||
- **返回**: true=运行中 false=已停止
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control isRunning
|
||||
```
|
||||
|
||||
### isHomed() -> bool - 是否已回零
|
||||
- **描述**: 查询是否已完成回零
|
||||
- **参数**: 无
|
||||
- **返回**: true=已回零 false=未回零
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control isHomed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. NC 文件操作接口
|
||||
|
||||
### loadNC(path: QString) -> bool - 加载 NC 文件
|
||||
- **描述**: 加载 NC 程序文件
|
||||
- **参数**:
|
||||
- `path`: NC 文件的**绝对路径**
|
||||
- **返回**: true=成功 false=失败
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control loadNC "/home/user/test.ngc"
|
||||
```
|
||||
|
||||
### getCurrentNC() -> QString - 获取当前 NC 文件
|
||||
- **描述**: 获取当前已加载的 NC 文件路径
|
||||
- **参数**: 无
|
||||
- **返回**: NC 文件路径
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control getCurrentNC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 工件坐标接口
|
||||
|
||||
### clearAxisOffset(axis: int) - 清除单轴工件坐标
|
||||
- **描述**: 清除指定轴的工件坐标偏移
|
||||
- **参数**:
|
||||
- `axis`: 轴编号:0=X 1=Y 2=Z 3=U 4=V 5=C
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
# 清除 X 轴工件坐标
|
||||
qdbus com.wirecut.service /com/wirecut/control clearAxisOffset 0
|
||||
```
|
||||
|
||||
### clearAllAxisOffset() - 清除所有轴工件坐标
|
||||
- **描述**: 清除全部6个轴的工件坐标偏移
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control clearAllAxisOffset
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 放电设置接口
|
||||
|
||||
### ShowDischargeSetting() - 打开放电设置界面
|
||||
- **描述**: 弹出放电设置窗口
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control ShowDischargeSetting
|
||||
```
|
||||
|
||||
### HideDischargeSetting() - 关闭放电设置界面
|
||||
- **描述**: 隐藏放电设置窗口
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control HideDischargeSetting
|
||||
```
|
||||
|
||||
### SetWorkpieceId(workpieceId: int) - 设置当前工件号
|
||||
- **描述**: 切换到指定工件编号
|
||||
- **参数**:
|
||||
- `workpieceId`: 工件编号,范围 0-8
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
# 切换到工件3
|
||||
qdbus com.wirecut.service /com/wirecut/control SetWorkpieceId 3
|
||||
```
|
||||
|
||||
### SetDischargePara(workpieceId, knifeId, paramType, value) - 设置放电参数
|
||||
- **描述**: 设置指定工件、指定刀号的放电参数
|
||||
- **参数**:
|
||||
- `workpieceId`: 工件编号 (0-8)
|
||||
- `knifeId`: 刀号 (1-11)
|
||||
- `paramType`: 参数类型字符串:
|
||||
- `"voltage"` / `"放电码"` - 放电码
|
||||
- `"current"` / `"跟踪值"` - 跟踪值
|
||||
- `"servo"` / `"速度"` - 速度上限
|
||||
- `value`: 参数值
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
# 设置工件0,刀号1,放电码为80
|
||||
qdbus com.wirecut.service /com/wirecut/control SetDischargePara 0 1 voltage 80
|
||||
```
|
||||
|
||||
### GetDischargePara(workpieceId, knifeId, paramType) -> int - 获取放电参数
|
||||
- **描述**: 获取指定工件、指定刀号的放电参数值
|
||||
- **参数**: 同 SetDischargePara
|
||||
- **返回**: 参数值
|
||||
|
||||
```bash
|
||||
# 获取工件0,刀号1的放电码
|
||||
qdbus com.wirecut.service /com/wirecut/control GetDischargePara 0 1 voltage
|
||||
```
|
||||
|
||||
### CopyToAllWorkpieces() - 复制到所有工件
|
||||
- **描述**: 将当前工件的放电参数复制到所有工件
|
||||
- **参数**: 无
|
||||
- **返回**: 无
|
||||
|
||||
```bash
|
||||
qdbus com.wirecut.service /com/wirecut/control CopyToAllWorkpieces
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 信号(推送通知)
|
||||
|
||||
DBus 服务会主动推送以下信号,外部程序可以监听:
|
||||
|
||||
### runStateChanged(state: int) - 运行状态变化
|
||||
- **触发时机**: 运行状态改变时
|
||||
- **参数**:
|
||||
- `state`: 0=停止 / 1=运行 / 2=暂停
|
||||
|
||||
### alarmMessage(type: int, msg: QString) - 报警/提示
|
||||
- **触发时机**: 系统产生报警或提示时
|
||||
- **参数**:
|
||||
- `type`: 0=提示 / 1=警告 / 2=错误
|
||||
- `msg`: 消息内容
|
||||
|
||||
### axisPosUpdated(x, y, z, u, v: double) - 轴位置刷新
|
||||
- **触发时机**: 每 500ms 周期推送一次
|
||||
- **参数**: 各轴当前位置 (mm)
|
||||
|
||||
### machiningFinished() - 加工完成
|
||||
- **触发时机**: 加工程序执行完成时
|
||||
- **参数**: 无
|
||||
|
||||
---
|
||||
|
||||
## 8. 状态码说明
|
||||
|
||||
### 运行状态码 (running 字段)
|
||||
|
||||
| 值 | 状态 | 说明 |
|
||||
|----|------|------|
|
||||
| 0 | 停止 | 未运行或已停止 |
|
||||
| 1 | 运行中 | 正在加工 |
|
||||
| 2 | 暂停 | 已暂停(变频) |
|
||||
|
||||
### 轴编号
|
||||
|
||||
| 值 | 轴 | 说明 |
|
||||
|----|-----|------|
|
||||
| 0 | X | X轴 |
|
||||
| 1 | Y | Y轴 |
|
||||
| 2 | Z | Z轴 |
|
||||
| 3 | U | U轴 |
|
||||
| 4 | V | V轴 |
|
||||
| 5 | C | C轴 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 命令行调用示例
|
||||
|
||||
### 完整加工流程
|
||||
|
||||
```bash
|
||||
# 1. 查看当前状态
|
||||
qdbus com.wirecut.service /com/wirecut/control getStatus
|
||||
|
||||
# 2. 加载 NC 文件(必须绝对路径)
|
||||
qdbus com.wirecut.service /com/wirecut/control loadNC "/home/pi/sample.ngc"
|
||||
|
||||
# 3. 设置加工速度
|
||||
qdbus com.wirecut.service /com/wirecut/control setSpeed 80
|
||||
|
||||
# 4. 开始加工
|
||||
qdbus com.wirecut.service /com/wirecut/control startRun
|
||||
|
||||
# 5. 实时查看轴位置
|
||||
qdbus com.wirecut.service /com/wirecut/control getAxisPos
|
||||
|
||||
# 6. 暂停加工
|
||||
qdbus com.wirecut.service /com/wirecut/control pauseRun
|
||||
|
||||
# 7. 停止加工
|
||||
qdbus com.wirecut.service /com/wirecut/control stopRun
|
||||
```
|
||||
|
||||
### 空走测试流程
|
||||
|
||||
```bash
|
||||
# 1. 回零
|
||||
qdbus com.wirecut.service /com/wirecut/control homeAll
|
||||
|
||||
# 2. 开始空走(不放电)
|
||||
qdbus com.wirecut.service /com/wirecut/control startKongZou
|
||||
|
||||
# 3. 停止空走
|
||||
qdbus com.wirecut.service /com/wirecut/control stopKongZou
|
||||
```
|
||||
|
||||
### Python 调用示例(使用 dbus-python)
|
||||
|
||||
```python
|
||||
import dbus
|
||||
|
||||
# 连接系统总线
|
||||
bus = dbus.SystemBus()
|
||||
|
||||
# 获取服务对象
|
||||
obj = bus.get_object('com.wirecut.service', '/com/wirecut/control')
|
||||
iface = dbus.Interface(obj, 'com.wirecut.IControl')
|
||||
|
||||
# 调用方法
|
||||
status = iface.getStatus()
|
||||
print("运行状态:", status['running'])
|
||||
print("X轴位置:", status['pos_x'])
|
||||
|
||||
# 加载 NC 文件
|
||||
iface.loadNC("/home/pi/test.ngc")
|
||||
|
||||
# 启动加工
|
||||
iface.startRun()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 注意事项
|
||||
|
||||
### ⚠️ 重要提醒
|
||||
|
||||
1. **CNC 软件必须运行**
|
||||
- 所有 DBus 接口只有在 CNC 主程序启动后才能使用
|
||||
- 软件未启动时调用会返回 DBus 错误
|
||||
|
||||
2. **NC 文件路径必须为绝对路径**
|
||||
- `loadNC()` 的 path 参数必须使用完整绝对路径
|
||||
- 相对路径会导致加载失败
|
||||
|
||||
3. **速度设置范围**
|
||||
- 速度值范围:1-9999 mm/min
|
||||
- 超出范围会被截断或忽略
|
||||
|
||||
4. **System Bus 权限**
|
||||
- 服务运行在 System Bus 上,可能需要 root 权限
|
||||
- 普通用户调用可能需要配置 DBus 权限规则
|
||||
|
||||
5. **线程安全**
|
||||
- 所有接口调用都是异步队列执行(QueuedConnection)
|
||||
- 调用后不会阻塞,实际执行由主界面事件循环处理
|
||||
|
||||
6. **共享内存依赖**
|
||||
- 部分参数(电压、电流、位置)依赖共享内存
|
||||
- 共享内存未就绪时返回 0 或默认值
|
||||
|
||||
---
|
||||
|
||||
## 附录:DBus 权限配置
|
||||
|
||||
如果普通用户无法调用,需要在 `/etc/dbus-1/system.d/` 下添加权限配置文件 `com.wirecut.conf`:
|
||||
|
||||
```xml
|
||||
<!DOCTYPE busconfig PUBLIC
|
||||
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||||
<busconfig>
|
||||
<policy user="pi">
|
||||
<allow own="com.wirecut.service"/>
|
||||
<allow send_destination="com.wirecut.service"/>
|
||||
<allow receive_sender="com.wirecut.service"/>
|
||||
</policy>
|
||||
<policy context="default">
|
||||
<allow send_destination="com.wirecut.service"/>
|
||||
<allow receive_sender="com.wirecut.service"/>
|
||||
</policy>
|
||||
</busconfig>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v1.0
|
||||
**生成日期**: 2026-05-20
|
||||
**对应源码**: `/usr/share/runf/wirecutdbus.h` / `wirecutdbus.cpp`
|
||||
642
docs/architecture_overview.html
Normal file
642
docs/architecture_overview.html
Normal file
@@ -0,0 +1,642 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>工业 AI 交互画布:流程总览</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f0f2f5;
|
||||
--card: #fff;
|
||||
--ink: #111827;
|
||||
--muted: #667085;
|
||||
--line: #d8dde6;
|
||||
--soft: #f1f4f8;
|
||||
--blue: #2563eb;
|
||||
--green: #059669;
|
||||
--amber: #d97706;
|
||||
--violet: #7c3aed;
|
||||
--dark: #111827;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: var(--bg); color: var(--ink); font-family: "PingFang SC", "Microsoft YaHei", Arial, sans-serif; font-size: 14px; }
|
||||
button { font: inherit; cursor: pointer; border: none; }
|
||||
|
||||
.page { max-width: 1500px; margin: 0 auto; padding: 24px 24px 60px; }
|
||||
|
||||
/* Hero */
|
||||
.hero { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 18px; flex-wrap: wrap; }
|
||||
h1 { font-size: 22px; font-weight: 800; }
|
||||
.subtitle { color: var(--muted); font-size: 13px; margin-top: 5px; line-height: 1.6; }
|
||||
|
||||
/* Layout */
|
||||
.main-grid { display: grid; grid-template-columns: 252px minmax(0, 1fr); gap: 16px; align-items: start; }
|
||||
|
||||
/* Left panel */
|
||||
.l-panel { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 18px; }
|
||||
.panel-title { font-size: 14px; font-weight: 700; margin-bottom: 12px; color: var(--ink); }
|
||||
.stack { display: grid; gap: 8px; }
|
||||
.layer { display: grid; grid-template-columns: 30px 1fr; gap: 10px; align-items: center; border: 1px solid var(--line); border-left: 4px solid var(--dark); border-radius: 7px; padding: 9px 12px; }
|
||||
.layer[data-c="blue"] { border-left-color: #2563eb; }
|
||||
.layer[data-c="cyan"] { border-left-color: #0891b2; }
|
||||
.layer[data-c="violet"] { border-left-color: #7c3aed; }
|
||||
.layer[data-c="green"] { border-left-color: #059669; }
|
||||
.layer[data-c="amber"] { border-left-color: #d97706; }
|
||||
.layer[data-c="dark"] { border-left-color: #111827; }
|
||||
.ln { display: flex; height: 24px; width: 24px; align-items: center; justify-content: center; border-radius: 50%; background: var(--soft); font-weight: 800; font-size: 12px; color: #334155; }
|
||||
.lname { font-size: 12px; font-weight: 700; }
|
||||
.ldesc { font-size: 11px; color: var(--muted); margin-top: 2px; line-height: 1.3; }
|
||||
|
||||
/* Right: flow panel */
|
||||
.r-panel { background: var(--card); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
|
||||
|
||||
/* Controls bar */
|
||||
.controls { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--line); background: #fafbfc; flex-wrap: wrap; }
|
||||
.tg { display: inline-flex; gap: 3px; padding: 3px; border: 1px solid var(--line); border-radius: 9px; background: var(--card); }
|
||||
.tbtn { background: transparent; color: #475467; padding: 8px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; transition: all .15s; white-space: nowrap; }
|
||||
.tbtn.active-flow { background: var(--dark); color: #fff; }
|
||||
.tbtn.active-ux { background: #4f46e5; color: #fff; }
|
||||
.tbtn.active-tech { background: #0d9488; color: #fff; }
|
||||
|
||||
/* View indicator bar */
|
||||
.view-bar { height: 4px; width: 100%; }
|
||||
.view-bar.ux { background: linear-gradient(90deg, #818cf8, #a78bfa); }
|
||||
.view-bar.tech { background: linear-gradient(90deg, #0d9488, #059669); }
|
||||
|
||||
/* Flow body */
|
||||
.flow-body { padding: 22px 26px 30px; overflow-x: auto; }
|
||||
.flow-meta { margin-bottom: 18px; }
|
||||
.flow-meta h2 { font-size: 17px; font-weight: 800; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.flow-meta p { font-size: 13px; color: var(--muted); margin-top: 6px; line-height: 1.6; }
|
||||
.badge { display: inline-flex; align-items: center; font-size: 11px; font-weight: 600; padding: 2px 10px; border-radius: 20px; }
|
||||
.badge-a { background: #dbeafe; color: #1d4ed8; }
|
||||
.badge-b { background: #d1fae5; color: #065f46; }
|
||||
.badge-ux { background: #ede9fe; color: #4c1d95; }
|
||||
.badge-tech { background: #ccfbf1; color: #134e4a; }
|
||||
.badge-plan { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
|
||||
.plan-banner { margin: 0 0 16px; padding: 10px 16px; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 8px; font-size: 12px; color: #78350f; line-height: 1.6; }
|
||||
|
||||
/* Legend */
|
||||
.legend { display: flex; gap: 10px; flex-wrap: wrap; padding: 9px 14px; background: var(--soft); border-radius: 8px; margin-bottom: 18px; }
|
||||
.lg { display: flex; align-items: center; gap: 5px; font-size: 11px; color: #475467; }
|
||||
.lg-dot { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
|
||||
|
||||
/* ═══ FLOWCHART BASE ═══ */
|
||||
.fc { display: flex; flex-direction: column; align-items: center; min-width: 580px; }
|
||||
|
||||
/* Nodes */
|
||||
.nd { border-radius: 8px; padding: 9px 16px; text-align: center; border: 1.5px solid var(--line); background: #fff; }
|
||||
.nd .nt { font-size: 13px; font-weight: 700; line-height: 1.4; }
|
||||
.nd .ns { font-size: 11px; color: var(--muted); margin-top: 3px; line-height: 1.35; }
|
||||
|
||||
/* Node type variants */
|
||||
.nd-start { background: var(--dark); border: none; border-radius: 22px; min-width: 180px; }
|
||||
.nd-start .nt { color: #fff; font-size: 14px; }
|
||||
.nd-start .ns { color: rgba(255,255,255,0.6); }
|
||||
|
||||
/* UX node types */
|
||||
.nd-show { background: #f5f3ff; border-color: #a78bfa; } /* 画面显示 */
|
||||
.nd-show .nt { color: #4c1d95; }
|
||||
.nd-do { background: #ecfdf5; border-color: #34d399; } /* 用户操作 */
|
||||
.nd-do .nt { color: #065f46; }
|
||||
.nd-end { background: #f0fdf4; border-color: #86efac; } /* 结果/结束 */
|
||||
.nd-end .nt { color: #14532d; }
|
||||
.nd-trig { background: #fff7ed; border-color: #fdba74; } /* 触发条件/场景 */
|
||||
.nd-trig .nt { color: #9a3412; }
|
||||
.nd-tab { background: #f0f9ff; border-color: #7dd3fc; border-style: dashed; }
|
||||
.nd-tab .nt { color: #0c4a6e; }
|
||||
|
||||
/* Tech node types */
|
||||
.nd-route { background: #fffbeb; border-color: #fbbf24; }
|
||||
.nd-route .nt { color: #78350f; }
|
||||
.nd-bert { background: #f5f3ff; border-color: #c4b5fd; }
|
||||
.nd-bert .nt { color: #5b21b6; }
|
||||
.nd-llm { background: #fff7ed; border-color: #fdba74; }
|
||||
.nd-llm .nt { color: #9a3412; }
|
||||
.nd-tool { background: #dbeafe; border-color: #60a5fa; }
|
||||
.nd-tool .nt { color: #1e3a8a; }
|
||||
.nd-kb { background: #ecfdf5; border-color: #6ee7b7; }
|
||||
.nd-kb .nt { color: #064e3b; }
|
||||
.nd-art { background: #faf5ff; border-color: #c4b5fd; border-style: dashed; }
|
||||
.nd-art .nt { color: #5b21b6; }
|
||||
.nd-snap { background: #f0fdf4; border-color: #86efac; border-style: dashed; }
|
||||
.nd-snap .nt { color: #14532d; }
|
||||
.nd-minor { background: var(--soft); border-color: var(--line); }
|
||||
.nd-minor .nt { color: #475467; }
|
||||
|
||||
/* Tags inside nodes */
|
||||
.tag { display: inline-block; font-size: 9px; font-weight: 700; padding: 2px 6px; border-radius: 3px; margin: 3px 2px 0; }
|
||||
.t-bert { background: #ede9fe; color: #5b21b6; }
|
||||
.t-llm { background: #fff7ed; color: #9a3412; }
|
||||
.t-rule { background: #dcfce7; color: #166534; }
|
||||
.t-tool { background: #dbeafe; color: #1e40af; }
|
||||
.t-art { background: #faf5ff; color: #5b21b6; }
|
||||
|
||||
/* Connectors */
|
||||
.dn { width: 2px; background: #c9d0db; height: 20px; flex-shrink: 0; }
|
||||
.dn.tall { height: 30px; }
|
||||
.arr { width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 7px solid #b0b9c8; flex-shrink: 0; }
|
||||
|
||||
/* Branch spread link */
|
||||
.slink { position: relative; width: 100%; height: 24px; flex-shrink: 0; }
|
||||
|
||||
/* Branch columns */
|
||||
.brow { display: flex; width: 100%; align-items: flex-start; }
|
||||
.bcol { display: flex; flex-direction: column; align-items: center; flex: 1; padding: 0 7px; min-width: 0; }
|
||||
|
||||
/* Branch labels */
|
||||
.blbl { padding: 4px 11px; border-radius: 20px; font-size: 11px; font-weight: 700; white-space: nowrap; }
|
||||
.bl-b { background: #dbeafe; color: #1d4ed8; border: 1px solid #93c5fd; }
|
||||
.bl-g { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
|
||||
.bl-a { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
|
||||
.bl-v { background: #ede9fe; color: #4c1d95; border: 1px solid #a78bfa; }
|
||||
.bl-gr { background: var(--soft); color: #475467; border: 1px solid var(--line); }
|
||||
.bl-hit { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
|
||||
.bl-miss { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
|
||||
|
||||
/* Notes */
|
||||
.fnote { font-size: 11px; color: #64748b; margin-top: 4px; text-align: center; line-height: 1.4; }
|
||||
.fopt { font-size: 11px; color: #6d28d9; margin-top: 4px; text-align: center; font-style: italic; }
|
||||
.fwarn { font-size: 11px; color: #b91c1c; margin-top: 4px; text-align: center; }
|
||||
|
||||
/* Divider */
|
||||
.fc-sep { width: 100%; border: none; border-top: 1px dashed var(--line); margin: 4px 0; }
|
||||
|
||||
@media (max-width: 1080px) { .main-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<header class="hero">
|
||||
<div>
|
||||
<h1>工业 AI 交互画布 · 操作流程与技术链路</h1>
|
||||
<p class="subtitle">选择视角,分别查看"用户界面交互路径"或"背后技术判断逻辑"。语音输入经过四阶段前置拦截后再进入 BERT NLU。</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="main-grid">
|
||||
<!-- Left: Architecture Layers -->
|
||||
<aside class="l-panel">
|
||||
<div class="panel-title">主链路层级</div>
|
||||
<div class="stack">
|
||||
<div class="layer" data-c="blue">
|
||||
<div class="ln">1</div>
|
||||
<div><div class="lname">输入层</div><div class="ldesc">文字 / 语音(ASR) / 点击</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="cyan">
|
||||
<div class="ln">2</div>
|
||||
<div><div class="lname">前置拦截层</div><div class="ldesc">停止词 → UI语音点击 → Slot填写 → BERT</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="violet">
|
||||
<div class="ln">3</div>
|
||||
<div><div class="lname">路由层</div><div class="ldesc">decision: execute / clarify / route_to_cloud</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="green">
|
||||
<div class="ln">4</div>
|
||||
<div><div class="lname">编排层</div><div class="ldesc">工具选择 / 知识检索 / Artifact 生成</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="amber">
|
||||
<div class="ln">5</div>
|
||||
<div><div class="lname">执行层</div><div class="ldesc">PLC / HMI / 知识库调用</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="dark">
|
||||
<div class="ln">6</div>
|
||||
<div><div class="lname">画布层</div><div class="ldesc">渲染 Artifact + 状态快照</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Right: Flow Panel -->
|
||||
<section class="r-panel">
|
||||
<div class="controls">
|
||||
<div class="tg" id="flowTabs">
|
||||
<button class="tbtn active-flow" data-flow="A">大流程 A · 工控对话</button>
|
||||
<button class="tbtn" data-flow="B">大流程 B · 调机流程 <span style="font-size:10px;opacity:.7;">🚧 下一版本</span></button>
|
||||
</div>
|
||||
<div class="tg" id="viewTabs">
|
||||
<button class="tbtn active-ux" data-view="ux">👁 交互流程</button>
|
||||
<button class="tbtn" data-view="tech">⚙️ 技术流程</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-bar ux" id="viewBar"></div>
|
||||
<div class="flow-body" id="flowBody"></div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// ─── State ──────────────────────────────────────────────────────────────
|
||||
const S = { flow: 'A', view: 'ux' };
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||||
const dn = (h = 20) => `<div class="dn" style="height:${h}px"></div>`;
|
||||
const arr = () => `<div class="arr"></div>`;
|
||||
const da = (h = 20) => dn(h) + arr();
|
||||
|
||||
function nd(cls, title, sub = '', tags = []) {
|
||||
const ts = tags.map(t => `<span class="tag t-${t}">${t.toUpperCase()}</span>`).join('');
|
||||
return `<div class="nd ${cls}"><div class="nt">${title}</div>${sub ? `<div class="ns">${sub}</div>` : ''}${ts ? `<div>${ts}</div>` : ''}</div>`;
|
||||
}
|
||||
|
||||
function blbl(text, cls) {
|
||||
return `<div class="blbl ${cls}">${text}</div>`;
|
||||
}
|
||||
|
||||
// Horizontal branch spread link: n = 2 or 3
|
||||
function slink(n) {
|
||||
if (n === 3) {
|
||||
return `<div class="slink">
|
||||
<div style="position:absolute;top:0;left:16.67%;right:16.67%;height:2px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;left:calc(16.67% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;left:calc(50% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;right:calc(16.67% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
</div>`;
|
||||
}
|
||||
return `<div class="slink">
|
||||
<div style="position:absolute;top:0;left:25%;right:25%;height:2px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;left:calc(25% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;right:calc(25% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ─── Legends ─────────────────────────────────────────────────────────────
|
||||
const legendUX = `<div class="legend">
|
||||
<div class="lg"><div class="lg-dot" style="background:#f5f3ff;border:1px solid #a78bfa;"></div> 界面展示内容</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#ecfdf5;border:1px solid #34d399;"></div> 用户可做的操作</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#f0fdf4;border:1px solid #86efac;"></div> 最终结果/结束态</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#f0f9ff;border:1px solid #7dd3fc;border-style:dashed;"></div> Tab 分页展示(不覆盖主卡)</div>
|
||||
</div>`;
|
||||
|
||||
const legendTech = `<div class="legend">
|
||||
<div class="lg"><div class="lg-dot" style="background:#fffbeb;border:1px solid #fbbf24;"></div> 路由判断节点</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#f5f3ff;border:1px solid #c4b5fd;"></div> BERT / NLU</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#fff7ed;border:1px solid #fdba74;"></div> LLM 推断</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#dbeafe;border:1px solid #60a5fa;"></div> 工具调用</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#ecfdf5;border:1px solid #6ee7b7;"></div> 知识库检索</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#faf5ff;border:1px solid #c4b5fd;border-style:dashed;"></div> Artifact 生成</div>
|
||||
</div>`;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FLOW A · UX VIEW
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
function renderA_UX() {
|
||||
return `
|
||||
<div class="flow-meta">
|
||||
<h2>大流程 A · 普通对话 <span class="badge badge-a">无调机流程</span> <span class="badge badge-ux">👁 交互流程</span></h2>
|
||||
<p>操作员从零开始发起一次输入。界面根据输入内容呈现三种不同的画布组件,操作员按提示进行确认或查阅。</p>
|
||||
</div>
|
||||
${legendUX}
|
||||
<div class="fc">
|
||||
|
||||
${nd('nd-start', '👤 操作员输入文字或语音', '当前画布无激活流程')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-trig', '系统识别输入类型,分三种场景响应', '用户感知到的是画布出现了不同内容')}
|
||||
${slink(3)}
|
||||
|
||||
<div class="brow">
|
||||
|
||||
<!-- Branch 1 -->
|
||||
<div class="bcol">
|
||||
${blbl('场景 1 · 操控设备', 'bl-b')}
|
||||
${da()}
|
||||
${nd('nd-show', '画布出现操控确认卡', '参数变更卡 或 设备动画卡')}
|
||||
${da()}
|
||||
${nd('nd-do', '操作员选择:确认或取消', '点击按钮 或 输入"确认/取消"')}
|
||||
${da()}
|
||||
${nd('nd-end', '画布显示执行结果', '成功 / 失败 / 重试')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 2 -->
|
||||
<div class="bcol">
|
||||
${blbl('场景 2 · 询问设备问题', 'bl-g')}
|
||||
${da()}
|
||||
${nd('nd-show', '画布出现知识教学卡', 'SOP / 报警处理 / 说明书内容')}
|
||||
${da()}
|
||||
${nd('nd-do', '操作员查阅内容', '可展开详情、查看步骤')}
|
||||
${da()}
|
||||
${nd('nd-end', '内容展示完毕')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 3 -->
|
||||
<div class="bcol">
|
||||
${blbl('场景 3 · 通用 / 无关', 'bl-gr')}
|
||||
${da()}
|
||||
${nd('nd-show', '对话框返回文字回答', '打招呼、天气、闲聊等内容')}
|
||||
${da()}
|
||||
${nd('nd-end', '回答完毕', '不影响工业主流程')}
|
||||
<div class="fwarn">⚠ 非核心场景</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FLOW A · TECH VIEW
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
function renderA_Tech() {
|
||||
return `
|
||||
<div class="flow-meta">
|
||||
<h2>大流程 A · 普通对话 <span class="badge badge-a">工控对话</span> <span class="badge badge-tech">⚙️ 技术流程</span></h2>
|
||||
<p>语音输入经过四阶段前置拦截(停止词→UI语音点击→Slot填写→BERT),只有前三阶段全部未命中的输入才进入 BERT NLU,再由 decision 字段驱动工具调用、知识检索或 LLM 作答。</p>
|
||||
</div>
|
||||
${legendTech}
|
||||
<div class="fc">
|
||||
|
||||
${nd('nd-start', '语音 / 文字输入', '来自 ASR 转录文本 / 直接文字')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-route', '阶段 0 · 停止词检测', '命中 cancel 词表 → 直接生成 stop_action,流程终止', ['rule'])}
|
||||
<div class="fnote">词表来自 voice_aliases.yml · cancel_words(静态构建)</div>
|
||||
${da()}
|
||||
|
||||
${nd('nd-route', '阶段 1 · UI 可见元素语音点击匹配', '优先级:waiting_confirmation affirm/deny > 当前Artifact按钮 > 全局固定操作', ['rule'])}
|
||||
${slink(2)}
|
||||
|
||||
<div class="brow">
|
||||
<div class="bcol">
|
||||
${blbl('命中 · 语音点击', 'bl-hit')}
|
||||
${da()}
|
||||
${nd('nd-tool', '生成 ActionEvent', 'actionId / artifactId / sourceText', ['rule'])}
|
||||
${da()}
|
||||
${nd('nd-snap', '画布状态机直接响应', '不调用 BERT,不产生新 Artifact')}
|
||||
</div>
|
||||
<div class="bcol">
|
||||
${blbl('未命中 · 继续', 'bl-miss')}
|
||||
${da()}
|
||||
${nd('nd-route', '阶段 1.5 · waiting_slot + inform 检测', 'session.status=waiting_slot AND 输入为数字/数值', ['rule'])}
|
||||
${slink(2)}
|
||||
<div class="brow" style="width:100%;">
|
||||
<div class="bcol">
|
||||
${blbl('命中 · 填槽', 'bl-hit')}
|
||||
${da()}
|
||||
${nd('nd-tool', 'fill_slots 接口', '直接补全当前 slot,不走 BERT', ['rule'])}
|
||||
</div>
|
||||
<div class="bcol">
|
||||
${blbl('未命中 · 进入 BERT', 'bl-miss')}
|
||||
${da()}
|
||||
${nd('nd-bert', '阶段 2 · BERT NLU(intelligent_cabin)', 'POST /api/v1/agent/chat\n返回 intent_id + decision + slots', ['bert'])}
|
||||
<div class="fnote">inference 报错直接抛出,不降级</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${da(30)}
|
||||
${nd('nd-route', 'decision 字段路由', 'execute / clarify / route_to_cloud / reject', ['rule'])}
|
||||
${slink(3)}
|
||||
|
||||
<div class="brow">
|
||||
|
||||
<!-- Branch 1 -->
|
||||
<div class="bcol">
|
||||
${blbl('execute · 设备控制域', 'bl-b')}
|
||||
${da()}
|
||||
${nd('nd-route', 'domain = machine_control\nconfidence_grade = high\nintent_id = wirecut_*')}
|
||||
${da()}
|
||||
${nd('nd-tool', '工业工具调用', 'DBus 写参 / 设备控制指令', ['tool'])}
|
||||
${da()}
|
||||
${nd('nd-art', '生成 Artifact', 'ParameterChangeArtifact\nDeviceActionArtifact', ['art'])}
|
||||
${da()}
|
||||
${nd('nd-snap', '画布渲染 + 等待 ActionEvent')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 2 -->
|
||||
<div class="bcol">
|
||||
${blbl('route_to_cloud · 知识域', 'bl-g')}
|
||||
${da()}
|
||||
${nd('nd-route', 'domain = equipment_knowledge\n或 confidence 偏低')}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 语义兜底分析', '提取检索关键词', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-kb', '知识库检索', '说明书 / SOP / 报警手册', ['tool'])}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 组织检索结果', '生成教学结构', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-art', '生成 KnowledgeLessonArtifact', '', ['art'])}
|
||||
${da()}
|
||||
${nd('nd-snap', '画布渲染')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 3 -->
|
||||
<div class="bcol">
|
||||
${blbl('reject · smalltalk / fallback', 'bl-gr')}
|
||||
${da()}
|
||||
${nd('nd-route', 'domain = smalltalk\n或无法匹配工业 domain')}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 直接作答', '', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-minor', '文字回复,不生成 Artifact')}
|
||||
<div class="fnote">不写入 ArtifactStore</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FLOW B · UX VIEW (🚧 下一版本计划中,暂未实现)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
function renderB_UX() {
|
||||
return `
|
||||
<div class="flow-meta">
|
||||
<h2>大流程 B · 调机流程中 <span class="badge badge-b">Guided Procedure 激活</span> <span class="badge badge-ux">👁 交互流程</span> <span class="badge badge-plan">🚧 下一版本 · 计划中</span></h2>
|
||||
<p>操作员确认进入调机向导后,调机流程卡占据画布主位。后续输入优先驱动当前步骤,其他内容以 Tab 或浮层展示,调机卡不被覆盖。</p>
|
||||
</div>
|
||||
<div class="plan-banner">⚠️ 此流程为下一版本设计规划,当前版本暂未实现。此图仅用于交互设计参考。</div>
|
||||
${legendUX}
|
||||
<div class="fc">
|
||||
|
||||
${nd('nd-show', '调机向导卡在画布主位展示', '步骤列表 · 当前步骤高亮 · 步骤指令可见')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-trig', '操作员输入或点击', '文字 / 语音 / 直接点击步骤按钮')}
|
||||
${slink(2)}
|
||||
|
||||
<div class="brow">
|
||||
|
||||
<!-- HIT -->
|
||||
<div class="bcol">
|
||||
${blbl('✅ 场景 1 · 操作当前步骤', 'bl-hit')}
|
||||
${da()}
|
||||
${nd('nd-do', '输入"已完成/下一步/确认"或直接点击', '等同于用语音/文字替代手点击')}
|
||||
${da()}
|
||||
${nd('nd-show', '当前步骤标记完成,流程卡更新', '进入下一步 · 调机卡不离开主位')}
|
||||
${da()}
|
||||
${nd('nd-end', '所有步骤完成,调机流程收尾', '显示操作汇总')}
|
||||
</div>
|
||||
|
||||
<!-- NOT HIT -->
|
||||
<div class="bcol">
|
||||
${blbl('🔀 场景 2 · 其他内容输入', 'bl-miss')}
|
||||
${da()}
|
||||
${nd('nd-trig', '输入与当前步骤无关', '系统识别为新的操控/知识/通用内容')}
|
||||
${slink(3)}
|
||||
|
||||
<div class="brow" style="width:100%;">
|
||||
|
||||
<div class="bcol">
|
||||
${blbl('操控工具', 'bl-b')}
|
||||
${da()}
|
||||
${nd('nd-tab', 'Tab 出现工具执行结果', '调机 | 工具结果', [])}
|
||||
${da()}
|
||||
${nd('nd-do', '操作员查看后可确认')}
|
||||
<div class="fnote">调机卡保持主位</div>
|
||||
</div>
|
||||
|
||||
<div class="bcol">
|
||||
${blbl('知识查询', 'bl-g')}
|
||||
${da()}
|
||||
${nd('nd-tab', 'Tab 出现知识卡', '调机 | 知识参考', [])}
|
||||
${da()}
|
||||
${nd('nd-do', '操作员查阅后继续流程')}
|
||||
<div class="fnote">调机卡保持主位</div>
|
||||
</div>
|
||||
|
||||
<div class="bcol">
|
||||
${blbl('通用提问', 'bl-gr')}
|
||||
${da()}
|
||||
${nd('nd-show', '临时浮层文字回答', '不占画布主位')}
|
||||
${da()}
|
||||
${nd('nd-end', '回答完毕自动收起')}
|
||||
<div class="fnote">调机卡保持主位</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FLOW B · TECH VIEW (🚧 下一版本计划中,暂未实现)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
function renderB_Tech() {
|
||||
return `
|
||||
<div class="flow-meta">
|
||||
<h2>大流程 B · 调机流程中 <span class="badge badge-b">Guided Procedure 激活</span> <span class="badge badge-tech">⚙️ 技术流程</span> <span class="badge badge-plan">🚧 下一版本 · 计划中</span></h2>
|
||||
<p>GuidedProcedure Artifact 激活后,输入路由优先做 textAliases 规则匹配(类汽车语音点击),命中则直接更新状态机;未命中再走 BERT 新意图识别,结果以 Tab 渲染,不修改主 Artifact 实例。</p>
|
||||
</div>
|
||||
<div class="plan-banner">⚠️ 此流程为下一版本设计规划,当前版本暂未实现。此图仅用于技术设计参考。</div>
|
||||
${legendTech}
|
||||
<div class="fc">
|
||||
|
||||
${nd('nd-art', 'GuidedProcedure Artifact 激活', 'currentStepId · steps[] · actions[textAliases]')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-start', '文本输入', '来自文字 / ASR / 点击')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-route', '路由层:优先匹配当前步骤 textAliases', '规则匹配为主,BERT 置信度辅助确认', ['rule', 'bert'])}
|
||||
${slink(2)}
|
||||
|
||||
<div class="brow">
|
||||
|
||||
<!-- HIT -->
|
||||
<div class="bcol">
|
||||
${blbl('命中 · textAliases 匹配成功', 'bl-hit')}
|
||||
${da()}
|
||||
${nd('nd-route', '生成 ArtifactActionEvent', 'actionId / source="text" / transcript', ['rule'])}
|
||||
${da()}
|
||||
${nd('nd-tool', '执行步骤对应工具动作', '写入测量値 / 推进步骤状态')}
|
||||
${da()}
|
||||
${nd('nd-bert', 'Reducer 更新 Artifact 状态', 'currentStepId → nextStepId\nstep.status → "completed"')}
|
||||
${da()}
|
||||
${nd('nd-snap', '快照保存(snapshotPolicy: persistent)', '步骤记录存档,支持复盘')}
|
||||
</div>
|
||||
|
||||
<!-- NOT HIT -->
|
||||
<div class="bcol">
|
||||
${blbl('未命中 · 进入新意图识别', 'bl-miss')}
|
||||
${da()}
|
||||
${nd('nd-bert', 'BERT NLU 识别新意图', '在当前调机上下文中重新分类 domain/intent', ['bert'])}
|
||||
${slink(3)}
|
||||
|
||||
<div class="brow" style="width:100%;">
|
||||
|
||||
<div class="bcol">
|
||||
${blbl('tool_call', 'bl-b')}
|
||||
${da()}
|
||||
${nd('nd-tool', '工具执行', '不修改主 Artifact', ['tool'])}
|
||||
${da()}
|
||||
${nd('nd-art', 'Tab 渲染结果', '主 Artifact 不受影响', ['art'])}
|
||||
<div class="fnote">ArtifactStore 主实例不变</div>
|
||||
</div>
|
||||
|
||||
<div class="bcol">
|
||||
${blbl('knowledge_query', 'bl-g')}
|
||||
${da()}
|
||||
${nd('nd-kb', '知识库检索\n+ LLM 组织', '', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-art', 'Tab 渲染知识卡', '主 Artifact 不受影响', ['art'])}
|
||||
<div class="fnote">ArtifactStore 主实例不变</div>
|
||||
</div>
|
||||
|
||||
<div class="bcol">
|
||||
${blbl('smalltalk', 'bl-gr')}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 直接作答', '', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-minor', '临时浮层,不写入\nArtifactStore')}
|
||||
<div class="fnote">主 Artifact 完全不受影响</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ─── Render Dispatch ──────────────────────────────────────────────────────
|
||||
const renderers = {
|
||||
A: { ux: renderA_UX, tech: renderA_Tech },
|
||||
B: { ux: renderB_UX, tech: renderB_Tech },
|
||||
};
|
||||
|
||||
function render() {
|
||||
const { flow, view } = S;
|
||||
|
||||
// Flow tabs
|
||||
document.querySelectorAll('#flowTabs .tbtn').forEach(b => {
|
||||
b.classList.toggle('active-flow', b.dataset.flow === flow);
|
||||
});
|
||||
|
||||
// View tabs
|
||||
document.querySelectorAll('#viewTabs .tbtn').forEach(b => {
|
||||
b.classList.remove('active-ux', 'active-tech');
|
||||
if (b.dataset.view === view) {
|
||||
b.classList.add(view === 'ux' ? 'active-ux' : 'active-tech');
|
||||
}
|
||||
});
|
||||
|
||||
// Color bar
|
||||
const bar = document.getElementById('viewBar');
|
||||
bar.className = `view-bar ${view}`;
|
||||
|
||||
// Content
|
||||
document.getElementById('flowBody').innerHTML = renderers[flow][view]();
|
||||
}
|
||||
|
||||
// ─── Events ───────────────────────────────────────────────────────────────
|
||||
document.getElementById('flowTabs').addEventListener('click', e => {
|
||||
const btn = e.target.closest('.tbtn');
|
||||
if (btn && btn.dataset.flow) { S.flow = btn.dataset.flow; render(); }
|
||||
});
|
||||
|
||||
document.getElementById('viewTabs').addEventListener('click', e => {
|
||||
const btn = e.target.closest('.tbtn');
|
||||
if (btn && btn.dataset.view) { S.view = btn.dataset.view; render(); }
|
||||
});
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
371
docs/bert_integration_analysis.md
Normal file
371
docs/bert_integration_analysis.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# intelligent_cabin BERT 接入 · 冲突分析与整合方案
|
||||
|
||||
> 作者:AI 分析
|
||||
> 日期:2026-05-26
|
||||
> 状态:待你确认
|
||||
|
||||
---
|
||||
|
||||
## 背景速览
|
||||
|
||||
| 侧 | 项目 | 技术栈 | 角色 |
|
||||
|---|---|---|---|
|
||||
| **Canvas(前端/编排侧)** | ai-canvas / Next.js | TypeScript + React | 画布渲染、Artifact 管理、LLM 编排、工业工具调用(DBus/PLC)|
|
||||
| **BERT 服务侧** | intelligent_cabin | Python / FastAPI | 本地 NLU:intent + slot,语音控制场景 |
|
||||
|
||||
这两个项目目前是独立的,尚未对接。
|
||||
|
||||
---
|
||||
|
||||
## 一、冲突点(需要明确决策的地方)
|
||||
|
||||
### 冲突 1:Domain 语义完全不匹配 ⚠️ 重要
|
||||
|
||||
**问题**:`intelligent_cabin/config/domain.yml` 里的 intent 全部来自车机场景(导航、空调、音乐、车窗、车灯……),与 `DBUS_API.md` 描述的线切割工控场景(电压、电流、速度、NC 加工、空走、回零)**毫无重叠**。
|
||||
|
||||
**后果**:如果直接把 BERT 服务接进来,意图识别的类别集合完全覆盖不了工控指令,识别结果几乎全是 unknown/reject。
|
||||
|
||||
**需确认**:
|
||||
- [ ] 你们会用智能车机的这套预训练模型做迁移学习,还是重新训练工控专属的 BERT 模型?
|
||||
- [ ] 还是说这个 BERT 服务只是作为基础推理框架,`domain.yml` 需要被替换成工控版 domain?
|
||||
|
||||
---
|
||||
|
||||
### 冲突 2:置信度阈值体系 vs. Canvas 路由体系
|
||||
|
||||
**Canvas 流程**(参考 `architecture_overview.html`)的技术视图里:
|
||||
```
|
||||
输入 → BERT NLU → confidence 判断 → [高置信+设备控制域] / [知识域/低置信] / [smalltalk]
|
||||
```
|
||||
|
||||
**intelligent_cabin 的路由体系**(`router.py`):
|
||||
```
|
||||
输入 → Classifier → FusionGrader →
|
||||
decision = execute / clarify / route_to_cloud / reject
|
||||
阈值:execute_score=0.55, execute_margin=0.18, route_to_cloud=0.75
|
||||
```
|
||||
|
||||
两套路由各有自己的 confidence 分级,**如果串联,会出现双重过滤**:
|
||||
- BERT 服务输出的 `score` 未必能直接映射到 Canvas 的 `confidence ≥ 阈值` 判断
|
||||
- Canvas 目前只依赖 BERT 输出的 `domain/intent/slot/confidence`,但 BERT 服务输出的是 `intent_id + score + decision`
|
||||
|
||||
**需确认**:
|
||||
- [ ] Canvas 侧是否需要直接消费 `decision` 字段(execute/clarify/route_to_cloud/reject),还是只取 `intent_id + score`,Canvas 自己做二次路由?
|
||||
|
||||
---
|
||||
|
||||
### 冲突 3:`textAliases`(大流程 B)vs. BERT 意图识别的竞争
|
||||
|
||||
**Canvas 大流程 B(调机流程中)的路由逻辑**:
|
||||
```
|
||||
输入 → 优先匹配 textAliases(如"完成"/"下一步"/"确认")
|
||||
→ 未命中 → BERT NLU 重新识别
|
||||
```
|
||||
|
||||
**intelligent_cabin** 里的 `DialogActEngine`(`dialog_act.py`)也在做相似的事情:
|
||||
```python
|
||||
affirm: ["确认", "好的", "继续", "可以", "确定"]
|
||||
cancel: ["取消", "算了", "不用了", "停止"]
|
||||
```
|
||||
|
||||
**冲突**:
|
||||
- `textAliases` 是每个 Artifact Step 私有的,属于 Canvas 状态机层面的规则匹配
|
||||
- `DialogActEngine` 是系统级对话行为分类,优先级/触发顺序未对齐
|
||||
- 如果两套都跑,"确认"这个词可能被 `DialogActEngine` 吃掉后不再传给 Canvas 的步骤推进逻辑
|
||||
|
||||
**整合建议**:
|
||||
- 大流程 B 中,textAliases 匹配应在 BERT 服务之前,Canvas 侧处理,不交给 BERT 服务
|
||||
- 把 `dialog_acts.yml` 的 `affirm/deny/cancel/modify` 从 BERT 服务中剥离,改为 Canvas 路由层的前置规则
|
||||
- 或者让 BERT 服务的 dialog_act 结果作为辅助 metadata 透传,Canvas 侧自行决策
|
||||
|
||||
---
|
||||
|
||||
### 冲突 4:slot 语义不兼容
|
||||
|
||||
**BERT 服务的 slot 体系**(按车机场景设计):
|
||||
- `temperature`:空调温度(16-30°C)
|
||||
- `destination`:导航目的地
|
||||
- `order_id`:订单号
|
||||
- `song`:歌曲名
|
||||
|
||||
**线切割工控的 slot 需求**(从 `DBUS_API.md` 推导):
|
||||
- `speed`:加工速度(mm/min,1-9999)
|
||||
- `voltage`:放电码/电压
|
||||
- `current`:跟踪值/电流
|
||||
- `workpiece_id`:工件编号(0-8)
|
||||
- `knife_id`:刀号(1-11)
|
||||
- `param_type`:参数类型(voltage/current/servo)
|
||||
- `nc_path`:NC 文件绝对路径
|
||||
- `axis`:轴编号(0-5,X/Y/Z/U/V/C)
|
||||
|
||||
两套 slot 定义**完全不同**,需要重写 `domain.yml` 和 `rewrite_engine.py` 里的工控版本。
|
||||
|
||||
---
|
||||
|
||||
### 冲突 5:rewrite_engine 的上下文改写逻辑是车机特化的
|
||||
|
||||
`rewrite_engine.py` 里写死了:
|
||||
```python
|
||||
_AC_CONTEXT_INTENTS = {"cabin_set_ac", "cabin_ac_on", "cabin_ac_off", ...}
|
||||
_AC_DEFAULT_TEMPERATURE = 24
|
||||
_AC_STEP = 2
|
||||
_AC_MIN_TEMPERATURE = 16
|
||||
_AC_MAX_TEMPERATURE = 30
|
||||
```
|
||||
|
||||
这些都是车机空调的业务逻辑,工控场景需要换成类似:
|
||||
```python
|
||||
_PARAM_CONTEXT_INTENTS = {"wirecut_set_speed", "wirecut_set_voltage", "wirecut_set_current"}
|
||||
_SPEED_STEP = 5 # mm/min
|
||||
_VOLTAGE_STEP = 5 # 放电码
|
||||
```
|
||||
|
||||
**但这恰恰也是最有价值可以复用的能力**:多轮相对调节("再快一点"→ 改写为 "速度设为 85mm/min")在工控场景同样刚需。
|
||||
|
||||
---
|
||||
|
||||
## 二、可以直接整合的能力(不需要改动或改动很小)
|
||||
|
||||
### ✅ 整合点 1:FastAPI 服务直接作为 NLU 微服务接入
|
||||
|
||||
Canvas(Next.js 侧)可以通过 HTTP 调用 `POST /api/v1/agent/chat` 或 `POST /api/v1/agent/fill-slots`,作为 Canvas 的 "感知语义层"。
|
||||
|
||||
接入位置:Canvas 的"BERT NLU 意图识别"节点(技术流程视图第 2 层)
|
||||
|
||||
```
|
||||
Canvas 输入 → HTTP POST → intelligent_cabin :8000/api/v1/agent/chat
|
||||
← {intent_id, score, slots, decision}
|
||||
Canvas 路由层根据 intent_id + score 继续分发
|
||||
```
|
||||
|
||||
### ✅ 整合点 2:分级融合决策逻辑(router.py)可以直接复用
|
||||
|
||||
`MultiStageIntentMatcher` 里的 `execute / clarify / route_to_cloud / reject` 四态决策逻辑,与 Canvas 流程中"高置信执行 / 知识域兜底 / smalltalk" 的三路分发思路高度一致,可以直接用。
|
||||
|
||||
Canvas 只需要消费 BERT 服务返回的 `decision` 字段,然后:
|
||||
- `execute` → Canvas 走工具调用路径(DBus)
|
||||
- `clarify` → Canvas 展示澄清确认卡
|
||||
- `route_to_cloud` → Canvas 走 LLM 兜底路径
|
||||
- `reject` → Canvas 走 smalltalk / fallback
|
||||
|
||||
### ✅ 整合点 3:会话状态管理(session_store.py)
|
||||
|
||||
`SessionState` 里的 `context_memory` 和 `slots` 持久化机制,可以支持工控场景的多轮短句恢复(比如 "再快一点" → 记住上次速度值)。
|
||||
|
||||
### ✅ 整合点 4:多命令拆分(planner.py)
|
||||
|
||||
现有的 `sequence workflow` 和 `conditional workflow` 对工控多命令("先回零,再开始加工")直接可用。
|
||||
|
||||
### ✅ 整合点 5:高风险确认机制(dialog_rules.py)
|
||||
|
||||
`requires_confirmation` + `confirmation_required_risk_levels` 这套机制,完全可以映射到 Canvas 流程中"操控确认卡"的场景(Canvas 大流程 A 的场景 1),直接支持危险操作二次确认。
|
||||
|
||||
---
|
||||
|
||||
## 三、需要新建或重写的工控专属配置
|
||||
|
||||
### 3.1 新建 `config/domain_wirecut.yml`
|
||||
|
||||
需要把 DBUS_API.md 里的每个接口映射成 intent:
|
||||
|
||||
| intent_id | 对应 DBus 方法 | 示例语句 | 需要 slot |
|
||||
|---|---|---|---|
|
||||
| `wirecut_start_run` | `startRun()` | "开始加工" / "启动" | 无 |
|
||||
| `wirecut_stop_run` | `stopRun()` | "停止加工" / "停机" | 无 |
|
||||
| `wirecut_pause_run` | `pauseRun()` | "暂停" / "变频暂停" | 无 |
|
||||
| `wirecut_home_all` | `homeAll()` | "全轴回零" / "回零" | 无 |
|
||||
| `wirecut_start_kongzou` | `startKongZou()` | "开始空走" | 无 |
|
||||
| `wirecut_stop_kongzou` | `stopKongZou()` | "停止空走" | 无 |
|
||||
| `wirecut_set_speed` | `setSpeed(speed)` | "速度调到80" / "加快一点" | `speed:int` |
|
||||
| `wirecut_set_voltage` | `setVoltage(vol)` | "电压设为90" | `voltage:int` |
|
||||
| `wirecut_set_current` | `setCurrent(cur)` | "电流设为5" | `current:int` |
|
||||
| `wirecut_get_status` | `getStatus()` | "查一下状态" / "当前状态" | 无 |
|
||||
| `wirecut_load_nc` | `loadNC(path)` | "加载NC文件" | `nc_path:str` |
|
||||
| `wirecut_set_discharge` | `SetDischargePara(...)` | "设置工件0刀1放电码80" | `workpiece_id, knife_id, param_type, value` |
|
||||
|
||||
### 3.2 重写 `rewrite_engine.py` 里的工控上下文改写规则
|
||||
|
||||
```python
|
||||
# 工控版示例
|
||||
_SPEED_CONTEXT_INTENTS = {"wirecut_set_speed"}
|
||||
_SPEED_STEP = 5 # mm/min 每步调节
|
||||
_SPEED_MIN = 1
|
||||
_SPEED_MAX = 9999
|
||||
|
||||
_VOLTAGE_CONTEXT_INTENTS = {"wirecut_set_voltage"}
|
||||
_VOLTAGE_STEP = 5
|
||||
|
||||
# 改写规则
|
||||
"再快一点" → "速度设为 {last_speed + 5} mm/min"
|
||||
"再慢一点" → "速度设为 {last_speed - 5} mm/min"
|
||||
"电压再高一点" → "电压设为 {last_voltage + 5}"
|
||||
```
|
||||
|
||||
### 3.3 更新 `dialog_acts.yml` 适配工控确认场景
|
||||
|
||||
当前 `dialog_acts.yml` 的 `affirm/deny/cancel` 词表可以保留,但需要补充工控场景的特化语句:
|
||||
|
||||
```yaml
|
||||
# 工控场景补充
|
||||
affirm:
|
||||
+ "执行" / "开始" / "运行" / "启动"
|
||||
cancel:
|
||||
+ "停机" / "急停" / "中止"
|
||||
inform:
|
||||
+ 纯数字(速度值/电压值/电流值的slot填写)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、关于 dialog_act 和 rewrite_engine 能否通过配置文件驱动
|
||||
|
||||
**直接回答:当前已经部分支持,但工控场景需要扩展。**
|
||||
|
||||
### 4.1 dialog_act 配置化现状
|
||||
|
||||
`dialog_act.py` 里的 `DialogActEngine` 是数据驱动的:
|
||||
```python
|
||||
@dataclass
|
||||
class DialogActEngine:
|
||||
patterns: dict[str, tuple[str, ...]] = field(default_factory=...)
|
||||
```
|
||||
|
||||
`config_loader.py` 里已经实现了从 `dialog_acts.yml` 加载:
|
||||
```python
|
||||
def _load_dialog_act_engine(self) -> DialogActEngine:
|
||||
...
|
||||
return DialogActEngine(patterns={item.act_id: tuple(item.phrases) for item in parsed.acts})
|
||||
```
|
||||
|
||||
**✅ 结论**:`dialog_acts.yml` 已经是配置文件,直接修改 `config/dialog_acts.yml` 就能修改 act 词表,**无需改代码**。
|
||||
|
||||
**但是有个缺口**:当前 `dialog_acts.yml` 只支持 `字符串包含` 匹配(`phrase in normalized`),没有支持正则或数值范围匹配。工控场景里 "85" 这样的纯数字(作为速度 slot 的 `inform`)目前是靠 Python 代码里的 `if re.search(r"\d", normalized): return "inform"` 处理的,这个逻辑是硬编码在 `dialog_act.py` 里的,暂时没有配置化。
|
||||
|
||||
**建议优化**(详见第五节)。
|
||||
|
||||
### 4.2 rewrite_engine 配置化现状
|
||||
|
||||
`rewrite_engine.py` 里的上下文改写逻辑**全部硬编码**,没有配置文件支持:
|
||||
|
||||
```python
|
||||
_AC_CONTEXT_INTENTS = {"cabin_set_ac", ...} # 硬编码
|
||||
_AC_DEFAULT_TEMPERATURE = 24 # 硬编码
|
||||
_AC_STEP = 2 # 硬编码
|
||||
```
|
||||
|
||||
**❌ 问题**:不同设备(线切割 / 激光切割 / 注塑机 / 其他)的参数名、步长、范围都不同,每台设备都要改代码是不合理的。
|
||||
|
||||
**建议优化**(详见第五节)。
|
||||
|
||||
---
|
||||
|
||||
## 五、建议优化:将 rewrite_engine 和 dialog_act 配置化
|
||||
|
||||
这是你关心的核心问题,以下是具体方案。
|
||||
|
||||
### 5.1 新增 `config/context_rewrite.yml`
|
||||
|
||||
```yaml
|
||||
# context_rewrite.yml
|
||||
param_contexts:
|
||||
- intent_ids: ["wirecut_set_speed"]
|
||||
slot_name: "speed"
|
||||
unit: "mm/min"
|
||||
step: 5
|
||||
min_value: 1
|
||||
max_value: 9999
|
||||
default_value: 80
|
||||
up_phrases: ["再快一点", "加快", "速度调高", "快一点"]
|
||||
down_phrases: ["再慢一点", "减慢", "速度调低", "慢一点"]
|
||||
rewrite_template: "速度设为 {value} mm/min"
|
||||
|
||||
- intent_ids: ["wirecut_set_voltage"]
|
||||
slot_name: "voltage"
|
||||
unit: ""
|
||||
step: 5
|
||||
min_value: 0
|
||||
max_value: 200
|
||||
default_value: 90
|
||||
up_phrases: ["电压高一点", "电压调高"]
|
||||
down_phrases: ["电压低一点", "电压调低"]
|
||||
rewrite_template: "电压设为 {value}"
|
||||
|
||||
- intent_ids: ["wirecut_set_current"]
|
||||
slot_name: "current"
|
||||
unit: ""
|
||||
step: 1
|
||||
min_value: 0
|
||||
max_value: 30
|
||||
default_value: 5
|
||||
up_phrases: ["电流大一点", "跟踪值高一点"]
|
||||
down_phrases: ["电流小一点", "跟踪值低一点"]
|
||||
rewrite_template: "电流设为 {value}"
|
||||
```
|
||||
|
||||
然后修改 `rewrite_engine.py`,从 `context_rewrite.yml` 驱动逻辑,而不是硬编码。
|
||||
|
||||
### 5.2 扩展 `config/dialog_acts.yml` 支持 inform 的数值捕获
|
||||
|
||||
```yaml
|
||||
# dialog_acts.yml 新增字段
|
||||
acts:
|
||||
- act_id: inform
|
||||
phrases: []
|
||||
# 新增:数值模式检测
|
||||
numeric_patterns: ["\\d+"] # 包含数字就认为是 inform
|
||||
```
|
||||
|
||||
并在 `dialog_act.py` 里支持 `numeric_patterns` 字段,这样 "85" / "22度" 是否算 inform 就可以通过配置控制。
|
||||
|
||||
### 5.3 `Settings`(config.py)里增加工控设备配置项
|
||||
|
||||
`app/core/config.py` 里的 `Settings` 类目前通过环境变量驱动,建议增加工控配置路径:
|
||||
|
||||
```python
|
||||
# 新增到 Settings 类
|
||||
context_rewrite_config_path: str = "config/context_rewrite.yml"
|
||||
device_domain_config_path: str = "config/domain_wirecut.yml" # 可按设备切换
|
||||
```
|
||||
|
||||
这样不同设备(线切割、激光机)只需要切换 `.env` 文件里的 `AGENT_DEVICE_DOMAIN_CONFIG_PATH` 就能换一套 domain + rewrite 配置,**不改代码**。
|
||||
|
||||
---
|
||||
|
||||
## 六、整合架构建议(整体链路)
|
||||
|
||||
```
|
||||
语音输入
|
||||
↓ ASR(前端 / 设备侧)
|
||||
文本输入
|
||||
↓
|
||||
Canvas 路由层(Next.js / TypeScript)
|
||||
├── [大流程 B 激活] textAliases 匹配 → 直接推进 Artifact 步骤(不过 BERT)
|
||||
└── [其他] HTTP → intelligent_cabin :8000
|
||||
↓
|
||||
context rewrite(工控版配置化)
|
||||
↓
|
||||
BERT NLU(工控 domain)
|
||||
↓
|
||||
fusion decision
|
||||
├── execute → Canvas 走 DBus 工具调用
|
||||
├── clarify → Canvas 渲染 Artifact 确认卡
|
||||
├── route_to_cloud → Canvas 走 LLM 兜底 + 知识库
|
||||
└── reject → Canvas smalltalk / fallback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、待你确认的清单
|
||||
|
||||
| # | 问题 | 选项 |
|
||||
|---|---|---|
|
||||
| 1 | BERT 模型是否会重训成工控版,还是继续用车机模型迁移? | A. 重训工控 BERT B. 车机→工控迁移 C. 暂时用 mock/keyword |
|
||||
| 2 | Canvas 是消费 BERT 的 `decision` 字段,还是只取 `intent_id+score` 自己路由? | A. 消费 decision B. 自路由 |
|
||||
| 3 | 大流程 B 的 textAliases 匹配,确认在 Canvas 侧做,不经过 BERT 服务? | A. 是 B. 也经过 BERT 但 BERT 优先级低 |
|
||||
| 4 | rewrite_engine 配置化的优先级?现在要做还是等工控 domain 确定后一起做? | A. 现在就做 B. 等 domain 定了再做 |
|
||||
| 5 | dialog_acts.yml 的 `inform` 数值识别是否需要配置化? | A. 配置化 B. 保持硬编码即可 |
|
||||
| 6 | 不同设备部署时,是一个 BERT 服务实例 + 不同 `.env` 配置,还是多个实例? | A. 单实例多配置 B. 多实例 |
|
||||
|
||||
---
|
||||
|
||||
*本文档由 Antigravity 生成,请确认后继续推进实现。*
|
||||
1332
docs/industrial_ai_interaction_plan.md
Normal file
1332
docs/industrial_ai_interaction_plan.md
Normal file
File diff suppressed because it is too large
Load Diff
310
docs/nlu_integration_design.md
Normal file
310
docs/nlu_integration_design.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# NLU 接入设计方案
|
||||
|
||||
> 状态:**已确认,进入实现阶段**
|
||||
> 关联文档:`bert_integration_analysis.md`、`architecture_overview.html`
|
||||
|
||||
---
|
||||
|
||||
## 第一部分:概念解释 — 两套术语怎么对应
|
||||
|
||||
### 1.1 Canvas 设计里用的词
|
||||
|
||||
在 `architecture_overview.html` 的技术流程视图里,BERT NLU 节点描述为:
|
||||
|
||||
```
|
||||
BERT NLU 意图识别
|
||||
输出 domain / intent / slot / confidence 置信度
|
||||
```
|
||||
|
||||
这是一套**面向业务语义**的描述,每个词的含义:
|
||||
|
||||
| 词 | 含义 | 例子 |
|
||||
|---|---|---|
|
||||
| `domain` | 所属业务域,意图的分组 | `machine_control`、`equipment_knowledge`、`smalltalk` |
|
||||
| `intent` | 用户想做什么,域内的细分动作 | `wirecut_set_speed`、`wirecut_start_run`、`query_alarm` |
|
||||
| `slot` | 动作的具体参数,从句子里提取的关键值 | `speed=80`、`voltage=90`、`axis=X` |
|
||||
| `confidence` | 模型对这次识别结果的置信程度,0~1 | `0.94`(高)、`0.61`(中)、`0.32`(低)|
|
||||
|
||||
Canvas 的路由逻辑就是:拿到这四个值之后,判断 `confidence ≥ 阈值 AND domain = machine_control` → 走工具调用路径。
|
||||
|
||||
---
|
||||
|
||||
### 1.2 intelligent_cabin NLU 服务里用的词
|
||||
|
||||
`intelligent_cabin` 后端分两层输出:
|
||||
|
||||
#### 层 A:JointBertNLU 的原始输出(`joint_nlu.py`)
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class JointNluResult:
|
||||
intent_id: str | None # 识别出的意图 id,如 "wirecut_set_speed"
|
||||
intent_score: float # softmax 后的概率,就是置信度,0~1
|
||||
candidates: list[JointCandidate] # Top-K 候选意图及其概率
|
||||
slots: dict[str, Any] # 从句子里提取的 slot,如 {"speed": 80}
|
||||
slot_items: list[JointSlot] # slot 在原文中的精确位置和得分
|
||||
```
|
||||
|
||||
这里的 `intent_id + intent_score + slots` 对应 Canvas 描述里的 `intent + confidence + slot`。
|
||||
`domain` 不是模型直接输出的,而是根据 `intent_id` 在 `domain.yml` 里查到的(`wirecut_set_speed` → domain `machine_control`)。
|
||||
|
||||
#### 层 B:Router / FusionGrader 的决策输出(`router.py`)
|
||||
|
||||
```python
|
||||
# MultiStageIntentMatcher._build_fusion_stage() 里
|
||||
decision = "execute" | "clarify" | "route_to_cloud" | "reject"
|
||||
```
|
||||
|
||||
这是在原始 NLU 结果基础上做的**二次路由判断**,加入了:
|
||||
- 置信度是否够高(`score ≥ execute_score_threshold=0.55`)
|
||||
- 头两名候选的分差是否足够大(`margin ≥ execute_margin_threshold=0.18`)
|
||||
- 是否有多义性(ambiguous)
|
||||
|
||||
它告诉上层"这个识别结果能不能直接执行",而不只是"模型认为是哪个意图"。
|
||||
|
||||
---
|
||||
|
||||
### 1.3 两套词汇的完整映射关系
|
||||
|
||||
```
|
||||
Canvas 的描述 ←→ intelligent_cabin 的实际字段
|
||||
─────────────────────────────────────────────────────────────
|
||||
domain ←→ intent_def.domain (从 domain.yml 查)
|
||||
intent ←→ JointNluResult.intent_id
|
||||
slot ←→ JointNluResult.slots (dict)
|
||||
confidence ←→ JointNluResult.intent_score (0~1)
|
||||
|
||||
(以上是 NLU 层的概念对应)
|
||||
|
||||
Canvas 的路由逻辑 ←→ Router 层的 decision 字段
|
||||
"高置信 + 设备控制域" ←→ decision="execute" AND domain="machine_control"
|
||||
"知识域/低置信兜底" ←→ decision="route_to_cloud" 或 domain="equipment_knowledge"
|
||||
"smalltalk" ←→ decision="reject" 或 social_router 处理
|
||||
```
|
||||
|
||||
> **关键点**:Canvas 当前用 Mock NLU(`src/lib/nlu/mock.ts`),它直接输出 `domain + intent + confidence + routeHint`。
|
||||
> 接入真实 NLU 后,两个项目**原生打通,当成一个项目**,不做兼容适配层,接口可以随时改。
|
||||
|
||||
---
|
||||
|
||||
## 第二部分:Canvas ↔ NLU 服务的统一路由方案
|
||||
|
||||
### 2.1 两个项目合并为一个项目(已确认)
|
||||
|
||||
`intelligent_cabin` 不作为独立服务做适配,而是直接作为 `ai-canvas` 的后端子模块。
|
||||
原来 `src/lib/nlu/mock.ts` 的格式**可以废弃**,不需要保持向后兼容。
|
||||
|
||||
### 2.2 真实 NLU 服务的 HTTP 响应
|
||||
|
||||
调用 `POST /api/v1/agent/chat` 后,服务返回 `ChatResponse`,与路由相关的核心字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"intent": "wirecut_set_speed",
|
||||
"domain": "machine_control",
|
||||
"decision": "execute",
|
||||
"status": "completed",
|
||||
"filled_slots": { "speed": 80 },
|
||||
"routing_debug": {
|
||||
"confidence_grade": "high",
|
||||
"stages": [
|
||||
{ "stage": "classifier", "score": 0.87, "candidates": [...] },
|
||||
{
|
||||
"stage": "fusion",
|
||||
"metadata": { "decision": "execute", "grade": "high",
|
||||
"classifier_signal": 0.87, "classifier_margin": 0.34 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> `domain` 字段需要在 intelligent_cabin 的 `ChatResponse` schema 里加上(从 `IntentDefinition.domain` 填充),改动极小。
|
||||
|
||||
### 2.3 Canvas 侧的 NluResult 类型(替换 mock.ts)
|
||||
|
||||
```typescript
|
||||
// src/lib/nlu/types.ts (新建,替换 mock.ts 里的类型)
|
||||
|
||||
export type RouteHint =
|
||||
| "tool_call" // decision=execute + machine_control 域
|
||||
| "knowledge_query" // decision=route_to_cloud 或 equipment_knowledge 域
|
||||
| "smalltalk" // decision=reject
|
||||
| "fallback";
|
||||
|
||||
export type NluResult = {
|
||||
modelVersion: string;
|
||||
domain: string; // 后端直接返回
|
||||
intent: string; // intent_id
|
||||
confidence: number; // classifier stage score
|
||||
slots: Record<string, string | number | boolean>; // filled_slots
|
||||
routeHint: RouteHint;
|
||||
decisionGrade: "high" | "medium" | "low";
|
||||
rawDecision: "execute" | "clarify" | "route_to_cloud" | "reject";
|
||||
};
|
||||
|
||||
export function mapDecisionToRouteHint(
|
||||
decision: string,
|
||||
domain: string
|
||||
): RouteHint {
|
||||
if (decision === "execute") {
|
||||
if (domain === "machine_control") return "tool_call";
|
||||
if (domain === "equipment_knowledge") return "knowledge_query";
|
||||
return "tool_call";
|
||||
}
|
||||
if (decision === "route_to_cloud") return "knowledge_query";
|
||||
if (decision === "reject") return "smalltalk";
|
||||
return "fallback"; // clarify 等待补槽,暂用 fallback
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 confidence 读取位置
|
||||
|
||||
`routing_debug.stages` 里找 `stage === "classifier"` 的记录,取其 `score` 字段。
|
||||
这是 BERT 分类器 softmax 后的原始概率,等价于 Canvas 描述里的 `confidence`。
|
||||
|
||||
---
|
||||
|
||||
## 第三部分:语音处理前置拦截链路(已定稿)
|
||||
|
||||
### 3.1 设计原则
|
||||
|
||||
- **所有当前可见 UI 组件的按钮文本都参与语音匹配**,命中则直接触发点击事件,不调 BERT
|
||||
- 调机流程(GuidedProcedure)当前**不在实现范围**,相关 1c 拦截逻辑暂不涉及
|
||||
- BERT 报错**直接抛出**,不降级,不用 Mock 兜底
|
||||
|
||||
### 3.2 四阶段处理链路
|
||||
|
||||
```
|
||||
ASR 文本输入
|
||||
│
|
||||
▼
|
||||
[阶段 0] 停止词检测 ← 静态词表,构建时嵌入
|
||||
├── 命中 cancel_words → 生成 stop_action,链路终止
|
||||
└── 未命中 ↓
|
||||
│
|
||||
▼
|
||||
[阶段 1] UI 可见元素语音点击匹配 ← 纯前端规则,<1ms
|
||||
├── 1a. session.status=waiting_confirmation 时的 affirm/deny(最高优先级)
|
||||
└── 1b. 当前可见 Artifact 按钮 text 匹配
|
||||
│ 命中任意 → 生成 ActionEvent,走 Canvas 状态机,链路终止
|
||||
│
|
||||
│ 全部未命中
|
||||
▼
|
||||
[阶段 1.5] waiting_slot + inform 检测
|
||||
├── session.status=waiting_slot && 输入为数字/数值类
|
||||
└── 命中 → 调 fill_slots 接口,链路终止
|
||||
│
|
||||
▼
|
||||
[阶段 2] BERT NLU(intelligent_cabin /api/v1/agent/chat)
|
||||
├── 报错 → 直接抛出,不降级
|
||||
├── decision=execute → 工具调用层(DBus)→ Artifact
|
||||
├── decision=clarify → 渲染补槽卡,等待 waiting_slot
|
||||
├── decision=route_to_cloud → LLM + 知识库 → KnowledgeLessonArtifact
|
||||
└── decision=reject → LLM 直接作答,不写 ArtifactStore
|
||||
```
|
||||
|
||||
### 3.3 阶段 1 内部优先级说明
|
||||
|
||||
```typescript
|
||||
// 优先级从高到低(1c 调机 textAliases 暂不实现)
|
||||
const PRIORITY_ORDER = [
|
||||
"waiting_confirmation_affirm_deny", // 1a
|
||||
"visible_artifact_button", // 1b
|
||||
];
|
||||
```
|
||||
|
||||
**为什么 1a 最高**:当高风险操作(如"开始加工")弹出确认卡时,
|
||||
操作员说"确认"应当触发确认动作,而不是响应画布上同时存在的其他按钮。
|
||||
状态(`session.status`)决定优先级,而非文本本身。
|
||||
|
||||
### 3.4 阶段 1 匹配实现(pipeline.ts 骨架)
|
||||
|
||||
```typescript
|
||||
// src/lib/nlu/pipeline.ts
|
||||
|
||||
import { AFFIRM_WORDS, CANCEL_WORDS } from "./voice-aliases.gen"; // 构建时生成
|
||||
|
||||
type ActionEvent = {
|
||||
type: "voice_click_event" | "slot_fill_event" | "stop_action";
|
||||
actionId?: string;
|
||||
artifactId?: string;
|
||||
sourceText: string;
|
||||
};
|
||||
|
||||
export async function processVoiceInput(
|
||||
asrText: string,
|
||||
session: CanvasSession
|
||||
): Promise<NluResult | ActionEvent> {
|
||||
|
||||
// 阶段 0:停止词
|
||||
const norm = normalizeVoice(asrText);
|
||||
if (CANCEL_WORDS.some(w => norm.includes(w))) {
|
||||
return { type: "stop_action", sourceText: asrText };
|
||||
}
|
||||
|
||||
// 阶段 1a:waiting_confirmation 状态的 affirm/deny
|
||||
if (session.status === "waiting_confirmation") {
|
||||
if (AFFIRM_WORDS.some(w => norm.includes(w))) {
|
||||
return { type: "voice_click_event", actionId: "confirm", sourceText: asrText };
|
||||
}
|
||||
if (CANCEL_WORDS.some(w => norm.includes(w))) {
|
||||
return { type: "voice_click_event", actionId: "cancel", sourceText: asrText };
|
||||
}
|
||||
}
|
||||
|
||||
// 阶段 1b:当前 Artifact 按钮匹配
|
||||
const voiceClick = matchVoiceToAction(asrText, session.visibleActions);
|
||||
if (voiceClick) {
|
||||
return {
|
||||
type: "voice_click_event",
|
||||
actionId: voiceClick.actionId,
|
||||
artifactId: voiceClick.artifactId,
|
||||
sourceText: asrText,
|
||||
};
|
||||
}
|
||||
|
||||
// 阶段 1.5:waiting_slot + 数值输入
|
||||
if (session.status === "waiting_slot" && isNumericInput(asrText)) {
|
||||
return { type: "slot_fill_event", sourceText: asrText };
|
||||
}
|
||||
|
||||
// 阶段 2:BERT NLU(报错直接抛出)
|
||||
const response = await callNluService(asrText, session.sessionId);
|
||||
return adaptNluResponse(response);
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 voice_aliases 配置(已确认:静态构建)
|
||||
|
||||
**词表位置**:`intelligent_cabin/config/voice_aliases.yml`(和 `dialog_acts.yml` 放在一起)
|
||||
|
||||
```yaml
|
||||
# voice_aliases.yml
|
||||
affirm_words: ["确认", "好的", "执行", "是", "对", "继续", "好", "ok"]
|
||||
cancel_words: ["取消", "算了", "不要", "不用", "停止", "停"]
|
||||
|
||||
# 工控设备别名(按 intent_id 分组,用于阶段 1b 的 Artifact voiceActions)
|
||||
intent_aliases:
|
||||
wirecut_start_run: ["开始", "启动", "加工", "跑起来"]
|
||||
wirecut_stop_run: ["停", "停机", "急停", "停止"]
|
||||
wirecut_home_all: ["回零", "归零", "回原点"]
|
||||
wirecut_pause_run: ["暂停", "变频暂停"]
|
||||
```
|
||||
|
||||
**构建时生成**:构建脚本读取 yml → 生成 `src/lib/nlu/voice-aliases.gen.ts`,
|
||||
TypeScript 侧直接 import,不需要运行时 HTTP 请求。
|
||||
|
||||
---
|
||||
|
||||
## 第四部分:下一步实现计划
|
||||
|
||||
| 步骤 | 位置 | 内容 |
|
||||
|---|---|---|
|
||||
| 1 | `intelligent_cabin` Python 侧 | `ChatResponse` schema 加 `domain` 字段,`agent_service.py` 填充 |
|
||||
| 2 | `intelligent_cabin/config/` | 创建 `voice_aliases.yml`,补充工控别名 |
|
||||
| 3 | `src/lib/nlu/` | 新建 `types.ts`,废弃 `mock.ts` 中旧类型 |
|
||||
| 4 | `src/lib/nlu/` | 新建 `pipeline.ts`,实现四阶段处理链路 |
|
||||
| 5 | `src/lib/artifacts/types.ts` | 各 Artifact 类型上加 `voiceActions` 字段 |
|
||||
| 6 | 构建配置 | 添加 yml → ts 生成脚本(`voice-aliases.gen.ts`) |
|
||||
469
docs/归档(包含调机流程).html
Normal file
469
docs/归档(包含调机流程).html
Normal file
@@ -0,0 +1,469 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>工业 AI 交互画布:流程总览</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f0f2f5;
|
||||
--card: #fff;
|
||||
--ink: #111827;
|
||||
--muted: #667085;
|
||||
--line: #d8dde6;
|
||||
--soft: #f1f4f8;
|
||||
--blue: #2563eb;
|
||||
--green: #059669;
|
||||
--amber: #d97706;
|
||||
--violet: #7c3aed;
|
||||
--dark: #111827;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: var(--bg); color: var(--ink); font-family: "PingFang SC", "Microsoft YaHei", Arial, sans-serif; font-size: 14px; }
|
||||
button { font: inherit; cursor: pointer; border: none; }
|
||||
|
||||
.page { max-width: 1500px; margin: 0 auto; padding: 24px 24px 60px; }
|
||||
|
||||
/* Hero */
|
||||
.hero { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 18px; flex-wrap: wrap; }
|
||||
h1 { font-size: 22px; font-weight: 800; }
|
||||
.subtitle { color: var(--muted); font-size: 13px; margin-top: 5px; line-height: 1.6; }
|
||||
|
||||
/* Layout */
|
||||
.main-grid { display: grid; grid-template-columns: 252px minmax(0, 1fr); gap: 16px; align-items: start; }
|
||||
|
||||
/* Left panel */
|
||||
.l-panel { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 18px; }
|
||||
.panel-title { font-size: 14px; font-weight: 700; margin-bottom: 12px; color: var(--ink); }
|
||||
.stack { display: grid; gap: 8px; }
|
||||
.layer { display: grid; grid-template-columns: 30px 1fr; gap: 10px; align-items: center; border: 1px solid var(--line); border-left: 4px solid var(--dark); border-radius: 7px; padding: 9px 12px; }
|
||||
.layer[data-c="blue"] { border-left-color: #2563eb; }
|
||||
.layer[data-c="cyan"] { border-left-color: #0891b2; }
|
||||
.layer[data-c="violet"] { border-left-color: #7c3aed; }
|
||||
.layer[data-c="green"] { border-left-color: #059669; }
|
||||
.layer[data-c="amber"] { border-left-color: #d97706; }
|
||||
.layer[data-c="dark"] { border-left-color: #111827; }
|
||||
.ln { display: flex; height: 24px; width: 24px; align-items: center; justify-content: center; border-radius: 50%; background: var(--soft); font-weight: 800; font-size: 12px; color: #334155; }
|
||||
.lname { font-size: 12px; font-weight: 700; }
|
||||
.ldesc { font-size: 11px; color: var(--muted); margin-top: 2px; line-height: 1.3; }
|
||||
|
||||
/* Right: flow panel */
|
||||
.r-panel { background: var(--card); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
|
||||
|
||||
/* Controls bar */
|
||||
.controls { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--line); background: #fafbfc; flex-wrap: wrap; }
|
||||
.tg { display: inline-flex; gap: 3px; padding: 3px; border: 1px solid var(--line); border-radius: 9px; background: var(--card); }
|
||||
.tbtn { background: transparent; color: #475467; padding: 8px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; transition: all .15s; white-space: nowrap; }
|
||||
.tbtn.active-flow { background: var(--dark); color: #fff; }
|
||||
.tbtn.active-ux { background: #4f46e5; color: #fff; }
|
||||
.tbtn.active-tech { background: #0d9488; color: #fff; }
|
||||
|
||||
/* View indicator bar */
|
||||
.view-bar { height: 4px; width: 100%; }
|
||||
.view-bar.ux { background: linear-gradient(90deg, #818cf8, #a78bfa); }
|
||||
.view-bar.tech { background: linear-gradient(90deg, #0d9488, #059669); }
|
||||
|
||||
/* Flow body */
|
||||
.flow-body { padding: 22px 26px 30px; overflow-x: auto; }
|
||||
.flow-meta { margin-bottom: 18px; }
|
||||
.flow-meta h2 { font-size: 17px; font-weight: 800; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.flow-meta p { font-size: 13px; color: var(--muted); margin-top: 6px; line-height: 1.6; }
|
||||
.badge { display: inline-flex; align-items: center; font-size: 11px; font-weight: 600; padding: 2px 10px; border-radius: 20px; }
|
||||
.badge-a { background: #dbeafe; color: #1d4ed8; }
|
||||
.badge-b { background: #d1fae5; color: #065f46; }
|
||||
.badge-ux { background: #ede9fe; color: #4c1d95; }
|
||||
.badge-tech { background: #ccfbf1; color: #134e4a; }
|
||||
|
||||
/* Legend */
|
||||
.legend { display: flex; gap: 10px; flex-wrap: wrap; padding: 9px 14px; background: var(--soft); border-radius: 8px; margin-bottom: 18px; }
|
||||
.lg { display: flex; align-items: center; gap: 5px; font-size: 11px; color: #475467; }
|
||||
.lg-dot { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
|
||||
|
||||
/* ═══ FLOWCHART BASE ═══ */
|
||||
.fc { display: flex; flex-direction: column; align-items: center; min-width: 580px; }
|
||||
|
||||
/* Nodes */
|
||||
.nd { border-radius: 8px; padding: 9px 16px; text-align: center; border: 1.5px solid var(--line); background: #fff; }
|
||||
.nd .nt { font-size: 13px; font-weight: 700; line-height: 1.4; }
|
||||
.nd .ns { font-size: 11px; color: var(--muted); margin-top: 3px; line-height: 1.35; }
|
||||
|
||||
/* Node type variants */
|
||||
.nd-start { background: var(--dark); border: none; border-radius: 22px; min-width: 180px; }
|
||||
.nd-start .nt { color: #fff; font-size: 14px; }
|
||||
.nd-start .ns { color: rgba(255,255,255,0.6); }
|
||||
|
||||
/* UX node types */
|
||||
.nd-show { background: #f5f3ff; border-color: #a78bfa; } /* 画面显示 */
|
||||
.nd-show .nt { color: #4c1d95; }
|
||||
.nd-do { background: #ecfdf5; border-color: #34d399; } /* 用户操作 */
|
||||
.nd-do .nt { color: #065f46; }
|
||||
.nd-end { background: #f0fdf4; border-color: #86efac; } /* 结果/结束 */
|
||||
.nd-end .nt { color: #14532d; }
|
||||
.nd-trig { background: #fff7ed; border-color: #fdba74; } /* 触发条件/场景 */
|
||||
.nd-trig .nt { color: #9a3412; }
|
||||
.nd-tab { background: #f0f9ff; border-color: #7dd3fc; border-style: dashed; }
|
||||
.nd-tab .nt { color: #0c4a6e; }
|
||||
|
||||
/* Tech node types */
|
||||
.nd-route { background: #fffbeb; border-color: #fbbf24; }
|
||||
.nd-route .nt { color: #78350f; }
|
||||
.nd-bert { background: #f5f3ff; border-color: #c4b5fd; }
|
||||
.nd-bert .nt { color: #5b21b6; }
|
||||
.nd-llm { background: #fff7ed; border-color: #fdba74; }
|
||||
.nd-llm .nt { color: #9a3412; }
|
||||
.nd-tool { background: #dbeafe; border-color: #60a5fa; }
|
||||
.nd-tool .nt { color: #1e3a8a; }
|
||||
.nd-kb { background: #ecfdf5; border-color: #6ee7b7; }
|
||||
.nd-kb .nt { color: #064e3b; }
|
||||
.nd-art { background: #faf5ff; border-color: #c4b5fd; border-style: dashed; }
|
||||
.nd-art .nt { color: #5b21b6; }
|
||||
.nd-snap { background: #f0fdf4; border-color: #86efac; border-style: dashed; }
|
||||
.nd-snap .nt { color: #14532d; }
|
||||
.nd-minor { background: var(--soft); border-color: var(--line); }
|
||||
.nd-minor .nt { color: #475467; }
|
||||
|
||||
/* Tags inside nodes */
|
||||
.tag { display: inline-block; font-size: 9px; font-weight: 700; padding: 2px 6px; border-radius: 3px; margin: 3px 2px 0; }
|
||||
.t-bert { background: #ede9fe; color: #5b21b6; }
|
||||
.t-llm { background: #fff7ed; color: #9a3412; }
|
||||
.t-rule { background: #dcfce7; color: #166534; }
|
||||
.t-tool { background: #dbeafe; color: #1e40af; }
|
||||
.t-art { background: #faf5ff; color: #5b21b6; }
|
||||
|
||||
/* Connectors */
|
||||
.dn { width: 2px; background: #c9d0db; height: 20px; flex-shrink: 0; }
|
||||
.dn.tall { height: 30px; }
|
||||
.arr { width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 7px solid #b0b9c8; flex-shrink: 0; }
|
||||
|
||||
/* Branch spread link */
|
||||
.slink { position: relative; width: 100%; height: 24px; flex-shrink: 0; }
|
||||
|
||||
/* Branch columns */
|
||||
.brow { display: flex; width: 100%; align-items: flex-start; }
|
||||
.bcol { display: flex; flex-direction: column; align-items: center; flex: 1; padding: 0 7px; min-width: 0; }
|
||||
|
||||
/* Branch labels */
|
||||
.blbl { padding: 4px 11px; border-radius: 20px; font-size: 11px; font-weight: 700; white-space: nowrap; }
|
||||
.bl-b { background: #dbeafe; color: #1d4ed8; border: 1px solid #93c5fd; }
|
||||
.bl-g { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
|
||||
.bl-a { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
|
||||
.bl-v { background: #ede9fe; color: #4c1d95; border: 1px solid #a78bfa; }
|
||||
.bl-gr { background: var(--soft); color: #475467; border: 1px solid var(--line); }
|
||||
.bl-hit { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
|
||||
.bl-miss { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
|
||||
|
||||
/* Notes */
|
||||
.fnote { font-size: 11px; color: #64748b; margin-top: 4px; text-align: center; line-height: 1.4; }
|
||||
.fopt { font-size: 11px; color: #6d28d9; margin-top: 4px; text-align: center; font-style: italic; }
|
||||
.fwarn { font-size: 11px; color: #b91c1c; margin-top: 4px; text-align: center; }
|
||||
|
||||
/* Divider */
|
||||
.fc-sep { width: 100%; border: none; border-top: 1px dashed var(--line); margin: 4px 0; }
|
||||
|
||||
@media (max-width: 1080px) { .main-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<header class="hero">
|
||||
<div>
|
||||
<h1>工业 AI 交互画布 · 操作流程与技术链路</h1>
|
||||
<p class="subtitle">选择视角,分别查看"用户界面交互路径"或"背后技术判断逻辑"。语音输入经过四阶段前置拦截后再进入 BERT NLU。</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="main-grid">
|
||||
<!-- Left: Architecture Layers -->
|
||||
<aside class="l-panel">
|
||||
<div class="panel-title">主链路层级</div>
|
||||
<div class="stack">
|
||||
<div class="layer" data-c="blue">
|
||||
<div class="ln">1</div>
|
||||
<div><div class="lname">输入层</div><div class="ldesc">文字 / 语音(ASR) / 点击</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="cyan">
|
||||
<div class="ln">2</div>
|
||||
<div><div class="lname">前置拦截层</div><div class="ldesc">停止词 → UI语音点击 → Slot填写 → BERT</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="violet">
|
||||
<div class="ln">3</div>
|
||||
<div><div class="lname">路由层</div><div class="ldesc">decision: execute / clarify / route_to_cloud</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="green">
|
||||
<div class="ln">4</div>
|
||||
<div><div class="lname">编排层</div><div class="ldesc">工具选择 / 知识检索 / Artifact 生成</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="amber">
|
||||
<div class="ln">5</div>
|
||||
<div><div class="lname">执行层</div><div class="ldesc">PLC / HMI / 知识库调用</div></div>
|
||||
</div>
|
||||
<div class="layer" data-c="dark">
|
||||
<div class="ln">6</div>
|
||||
<div><div class="lname">画布层</div><div class="ldesc">渲染 Artifact + 状态快照</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Right: Flow Panel -->
|
||||
<section class="r-panel">
|
||||
<div class="controls">
|
||||
<div class="tg" id="viewTabs">
|
||||
<button class="tbtn active-ux" data-view="ux">👁 交互流程</button>
|
||||
<button class="tbtn" data-view="tech">⚙️ 技术流程</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-bar ux" id="viewBar"></div>
|
||||
<div class="flow-body" id="flowBody"></div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// ─── State ────────────────────────────────────────────────────────────────
|
||||
const S = { view: 'ux' };
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||||
const dn = (h = 20) => `<div class="dn" style="height:${h}px"></div>`;
|
||||
const arr = () => `<div class="arr"></div>`;
|
||||
const da = (h = 20) => dn(h) + arr();
|
||||
|
||||
function nd(cls, title, sub = '', tags = []) {
|
||||
const ts = tags.map(t => `<span class="tag t-${t}">${t.toUpperCase()}</span>`).join('');
|
||||
return `<div class="nd ${cls}"><div class="nt">${title}</div>${sub ? `<div class="ns">${sub}</div>` : ''}${ts ? `<div>${ts}</div>` : ''}</div>`;
|
||||
}
|
||||
|
||||
function blbl(text, cls) {
|
||||
return `<div class="blbl ${cls}">${text}</div>`;
|
||||
}
|
||||
|
||||
// Horizontal branch spread link: n = 2 or 3
|
||||
function slink(n) {
|
||||
if (n === 3) {
|
||||
return `<div class="slink">
|
||||
<div style="position:absolute;top:0;left:16.67%;right:16.67%;height:2px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;left:calc(16.67% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;left:calc(50% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;right:calc(16.67% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
</div>`;
|
||||
}
|
||||
return `<div class="slink">
|
||||
<div style="position:absolute;top:0;left:25%;right:25%;height:2px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;left:calc(25% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
<div style="position:absolute;top:0;right:calc(25% - 1px);width:2px;height:24px;background:#c9d0db;"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ─── Legends ─────────────────────────────────────────────────────────────
|
||||
const legendUX = `<div class="legend">
|
||||
<div class="lg"><div class="lg-dot" style="background:#f5f3ff;border:1px solid #a78bfa;"></div> 界面展示内容</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#ecfdf5;border:1px solid #34d399;"></div> 用户可做的操作</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#f0fdf4;border:1px solid #86efac;"></div> 最终结果/结束态</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#f0f9ff;border:1px solid #7dd3fc;border-style:dashed;"></div> Tab 分页展示(不覆盖主卡)</div>
|
||||
</div>`;
|
||||
|
||||
const legendTech = `<div class="legend">
|
||||
<div class="lg"><div class="lg-dot" style="background:#fffbeb;border:1px solid #fbbf24;"></div> 路由判断节点</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#f5f3ff;border:1px solid #c4b5fd;"></div> BERT / NLU</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#fff7ed;border:1px solid #fdba74;"></div> LLM 推断</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#dbeafe;border:1px solid #60a5fa;"></div> 工具调用</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#ecfdf5;border:1px solid #6ee7b7;"></div> 知识库检索</div>
|
||||
<div class="lg"><div class="lg-dot" style="background:#faf5ff;border:1px solid #c4b5fd;border-style:dashed;"></div> Artifact 生成</div>
|
||||
</div>`;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FLOW A · UX VIEW
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
function renderA_UX() {
|
||||
return `
|
||||
<div class="flow-meta">
|
||||
<h2>大流程 A · 普通对话 <span class="badge badge-a">无调机流程</span> <span class="badge badge-ux">👁 交互流程</span></h2>
|
||||
<p>操作员从零开始发起一次输入。界面根据输入内容呈现三种不同的画布组件,操作员按提示进行确认或查阅。</p>
|
||||
</div>
|
||||
${legendUX}
|
||||
<div class="fc">
|
||||
|
||||
${nd('nd-start', '👤 操作员输入文字或语音', '当前画布无激活流程')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-trig', '系统识别输入类型,分三种场景响应', '用户感知到的是画布出现了不同内容')}
|
||||
${slink(3)}
|
||||
|
||||
<div class="brow">
|
||||
|
||||
<!-- Branch 1 -->
|
||||
<div class="bcol">
|
||||
${blbl('场景 1 · 操控设备', 'bl-b')}
|
||||
${da()}
|
||||
${nd('nd-show', '画布出现操控确认卡', '参数变更卡 或 设备动画卡')}
|
||||
${da()}
|
||||
${nd('nd-do', '操作员选择:确认或取消', '点击按钮 或 输入"确认/取消"')}
|
||||
${da()}
|
||||
${nd('nd-end', '画布显示执行结果', '成功 / 失败 / 重试')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 2 -->
|
||||
<div class="bcol">
|
||||
${blbl('场景 2 · 询问设备问题', 'bl-g')}
|
||||
${da()}
|
||||
${nd('nd-show', '画布出现知识教学卡', 'SOP / 报警处理 / 说明书内容')}
|
||||
${da()}
|
||||
${nd('nd-do', '操作员查阅内容', '可展开详情、查看步骤')}
|
||||
${da()}
|
||||
${nd('nd-end', '内容展示完毕')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 3 -->
|
||||
<div class="bcol">
|
||||
${blbl('场景 3 · 通用 / 无关', 'bl-gr')}
|
||||
${da()}
|
||||
${nd('nd-show', '对话框返回文字回答', '打招呼、天气、闲聊等内容')}
|
||||
${da()}
|
||||
${nd('nd-end', '回答完毕', '不影响工业主流程')}
|
||||
<div class="fwarn">⚠ 非核心场景</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FLOW A · TECH VIEW
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
function renderA_Tech() {
|
||||
return `
|
||||
<div class="flow-meta">
|
||||
<h2>大流程 A · 普通对话 <span class="badge badge-a">工控对话</span> <span class="badge badge-tech">⚙️ 技术流程</span></h2>
|
||||
<p>语音输入经过四阶段前置拦截(停止词→UI语音点击→Slot填写→BERT),只有前三阶段全部未命中的输入才进入 BERT NLU,再由 decision 字段驱动工具调用、知识检索或 LLM 作答。</p>
|
||||
</div>
|
||||
${legendTech}
|
||||
<div class="fc">
|
||||
|
||||
${nd('nd-start', '语音 / 文字输入', '来自 ASR 转录文本 / 直接文字')}
|
||||
${da()}
|
||||
|
||||
${nd('nd-route', '阶段 0 · 停止词检测', '命中 cancel 词表 → 直接生成 stop_action,流程终止', ['rule'])}
|
||||
<div class="fnote">词表来自 voice_aliases.yml · cancel_words(静态构建)</div>
|
||||
${da()}
|
||||
|
||||
${nd('nd-route', '阶段 1 · UI 可见元素语音点击匹配', '优先级:waiting_confirmation affirm/deny > 当前Artifact按钮 > 全局固定操作', ['rule'])}
|
||||
${slink(2)}
|
||||
|
||||
<div class="brow">
|
||||
<div class="bcol">
|
||||
${blbl('命中 · 语音点击', 'bl-hit')}
|
||||
${da()}
|
||||
${nd('nd-tool', '生成 ActionEvent', 'actionId / artifactId / sourceText', ['rule'])}
|
||||
${da()}
|
||||
${nd('nd-snap', '画布状态机直接响应', '不调用 BERT,不产生新 Artifact')}
|
||||
</div>
|
||||
<div class="bcol">
|
||||
${blbl('未命中 · 继续', 'bl-miss')}
|
||||
${da()}
|
||||
${nd('nd-route', '阶段 1.5 · waiting_slot + inform 检测', 'session.status=waiting_slot AND 输入为数字/数值', ['rule'])}
|
||||
${slink(2)}
|
||||
<div class="brow" style="width:100%;">
|
||||
<div class="bcol">
|
||||
${blbl('命中 · 填槽', 'bl-hit')}
|
||||
${da()}
|
||||
${nd('nd-tool', 'fill_slots 接口', '直接补全当前 slot,不走 BERT', ['rule'])}
|
||||
</div>
|
||||
<div class="bcol">
|
||||
${blbl('未命中 · 进入 BERT', 'bl-miss')}
|
||||
${da()}
|
||||
${nd('nd-bert', '阶段 2 · BERT NLU(intelligent_cabin)', 'POST /api/v1/agent/chat\n返回 intent_id + decision + slots', ['bert'])}
|
||||
<div class="fnote">inference 报错直接抛出,不降级</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${da(30)}
|
||||
${nd('nd-route', 'decision 字段路由', 'execute / clarify / route_to_cloud / reject', ['rule'])}
|
||||
${slink(3)}
|
||||
|
||||
<div class="brow">
|
||||
|
||||
<!-- Branch 1 -->
|
||||
<div class="bcol">
|
||||
${blbl('execute · 设备控制域', 'bl-b')}
|
||||
${da()}
|
||||
${nd('nd-route', 'domain = machine_control\nconfidence_grade = high\nintent_id = wirecut_*')}
|
||||
${da()}
|
||||
${nd('nd-tool', '工业工具调用', 'DBus 写参 / 设备控制指令', ['tool'])}
|
||||
${da()}
|
||||
${nd('nd-art', '生成 Artifact', 'ParameterChangeArtifact\nDeviceActionArtifact', ['art'])}
|
||||
${da()}
|
||||
${nd('nd-snap', '画布渲染 + 等待 ActionEvent')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 2 -->
|
||||
<div class="bcol">
|
||||
${blbl('route_to_cloud · 知识域', 'bl-g')}
|
||||
${da()}
|
||||
${nd('nd-route', 'domain = equipment_knowledge\n或 confidence 偏低')}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 语义兜底分析', '提取检索关键词', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-kb', '知识库检索', '说明书 / SOP / 报警手册', ['tool'])}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 组织检索结果', '生成教学结构', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-art', '生成 KnowledgeLessonArtifact', '', ['art'])}
|
||||
${da()}
|
||||
${nd('nd-snap', '画布渲染')}
|
||||
</div>
|
||||
|
||||
<!-- Branch 3 -->
|
||||
<div class="bcol">
|
||||
${blbl('reject · smalltalk / fallback', 'bl-gr')}
|
||||
${da()}
|
||||
${nd('nd-route', 'domain = smalltalk\n或无法匹配工业 domain')}
|
||||
${da()}
|
||||
${nd('nd-llm', 'LLM 直接作答', '', ['llm'])}
|
||||
${da()}
|
||||
${nd('nd-minor', '文字回复,不生成 Artifact')}
|
||||
<div class="fnote">不写入 ArtifactStore</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
// ─── Render Dispatch ──────────────────────────────────────────────────────
|
||||
const renderers = {
|
||||
ux: renderA_UX,
|
||||
tech: renderA_Tech,
|
||||
};
|
||||
|
||||
function render() {
|
||||
const { view } = S;
|
||||
|
||||
// View tabs
|
||||
document.querySelectorAll('#viewTabs .tbtn').forEach(b => {
|
||||
b.classList.remove('active-ux', 'active-tech');
|
||||
if (b.dataset.view === view) {
|
||||
b.classList.add(view === 'ux' ? 'active-ux' : 'active-tech');
|
||||
}
|
||||
});
|
||||
|
||||
// Color bar
|
||||
const bar = document.getElementById('viewBar');
|
||||
bar.className = `view-bar ${view}`;
|
||||
|
||||
// Content
|
||||
document.getElementById('flowBody').innerHTML = renderers[view]();
|
||||
}
|
||||
|
||||
// ─── Events ───────────────────────────────────────────────────────────────
|
||||
document.getElementById('viewTabs').addEventListener('click', e => {
|
||||
const btn = e.target.closest('.tbtn');
|
||||
if (btn && btn.dataset.view) { S.view = btn.dataset.view; render(); }
|
||||
});
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user