提交 e05c975f authored 作者: 陈泽健's avatar 陈泽健

feat(device-sim, element-locate): 设备模拟定时上报配置 + 元素定位优化

设备模拟模块:
- 新增定时上报配置功能(1秒-24小时灵活配置)
- 修复消息体格式符合 Mqtt_Send.py 真实设备格式
- 修复消息体被覆盖的关键 Bug
- 添加 topic_params 和 report_config 字段
- 优化启动设备异常处理

元素定位模块:
- 优化元素定位 API 接口
- 增强前端 API 调用逻辑

文档:
- 新增自然语言用例智能定位功能 PRD 和执行计划
- 新增设备模拟修复计划文档
- 新增设备模拟主题配置交接文档
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 4e31fa9b
# 执行计划 — 自然语言用例智能定位功能
> **关联 PRD**: `_PRD_自然语言用例智能定位功能.md`
> **优先级**: P0
> **作者**: czj / Claude Code
> **创建日期**: 2026-08-05
> **预计工期**: 5 天
---
## 总体规划
| Phase | 内容 | 工期 | 依赖 |
|-------|------|------|------|
| Phase 1 | 登录流程模板化 | 1 天 | 无 |
| Phase 2 | 智能定位后端核心 | 2 天 | Phase 1 |
| Phase 3 | 前端集成与测试 | 1 天 | Phase 2 |
| Phase 4 | 优化与文档更新 | 1 天 | Phase 3 |
---
## Phase 1: 登录流程模板化(1 天)
### 目标
将已验证的登录步骤固化为模板,智能定位时自动使用,用户无需手动配置登录。
### 任务清单
- [ ] 创建 `login_templates` 表(SQLite)
- [ ] 创建 `backend/app/models/login_template.py`
- [ ] 创建 `backend/app/schemas/login_template.py`
- [ ] 创建 `backend/app/routers/login_template.py`
- [ ] 创建 `backend/app/services/login_template_service.py`
- [ ] 插入默认登录模板(已验证的 8 步登录 + 3 步功能中心导航)
- [ ]`main.py` 注册路由
### 预置模板内容
**登录模板(8 步)**
| 步骤 | 名称 | 操作 | 选择器 | 值 |
|------|------|------|--------|-----|
| 1 | 访问登录页面 | navigate | - | https://192.168.5.44 |
| 2 | 等待登录表单加载 | wait | `input[placeholder*="手机号"]` | timeout:10000 |
| 3 | 输入用户名 | fill | `input[placeholder*="手机号"]` | admin@xty |
| 4 | 输入密码 | fill | `input[type="password"]` | Ubains@13579 |
| 5 | 输入验证码 | fill | `input[placeholder*="图"]` | csba |
| 6 | 勾选协议复选框 | click | `.el-checkbox` | - |
| 7 | 点击登录按钮 | click | `button:has-text("登录")` | - |
| 8 | 等待登录跳转完成 | wait | `[class*='nav'], .container, .el-main` | timeout:15000 |
**功能中心导航模板(3 步)**
| 步骤 | 名称 | 操作 | 选择器 |
|------|------|------|--------|
| 9 | 点击功能中心图标 | click | `//*[@id="Home"]/div[1]/div[1]` |
| 10 | 等待功能抽屉打开 | wait | `.el-drawer` |
| 11 | 点击目标菜单 | click | `.el-drawer >> text="${菜单名}"` |
### 验证标准
- [ ] API 可正常 CRUD 登录模板
- [ ] 默认模板存在且包含 11 个步骤
---
## Phase 2: 智能定位后端核心(2 天)
### 目标
实现 `/api/element/smart-locate` 接口,自动访问被测系统、执行操作、提取选择器。
### 任务清单
#### 2.1 关键词提取与匹配模块(0.5 天)
- [ ] 创建 `backend/app/services/keyword_matcher.py`
```python
# 关键词提取
def extract_keywords(description: str) -> list[str]
# 关键词匹配元素
def match_element_by_keywords(page, keywords: list[str], action: str) -> tuple[Element, list[dict]]
```
#### 2.2 选择器提取模块(0.5 天)
- [ ] 创建 `backend/app/services/selector_extractor.py`
```python
# 提取多候选选择器
def extract_selectors(element) -> dict
# 按优先级排序
# 1. data-testid 2. ID 3. placeholder 4. aria-label 5. 文本 6. 组合选择器 7. XPath
```
#### 2.3 智能定位服务(1 天)
- [ ] 创建 `backend/app/services/smart_locate_service.py`
```python
class SmartLocateService:
def locate_steps(self, steps: list[dict], auto_login: bool, navigate_menu: str) -> list[dict]:
"""
主流程:
1. 启动 Playwright
2. 执行登录模板
3. 导航到目标菜单
4. 逐步骤定位 + 执行
5. 返回结果
"""
def locate_single_step(self, step: dict) -> dict:
"""
单步骤定位:
1. 提取关键词
2. 关键词匹配
3. 语义推断
4. 回退策略
"""
```
#### 2.4 智能定位路由(0.5 天)
- [ ] 创建 `backend/app/routers/smart_locate.py`
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/element/smart-locate` | 智能定位主接口 |
| POST | `/api/element/verify-step` | 单步骤验证(调试用) |
- [ ]`main.py` 注册路由
### 验证标准
- [ ] `/api/element/smart-locate` 可自动登录并定位元素
- [ ] 每个步骤返回多种候选选择器
- [ ] 定位的选择器执行通过率 ≥ 90%
---
## Phase 3: 前端集成与测试(1 天)
### 目标
改造前端"获取定位"功能,调用智能定位接口。
### 任务清单
- [ ] 修改 `frontend/src/api/elementLocate.ts`,增加 `smartLocate` 接口
- [ ] 修改 `frontend/src/views/Cases.vue`,"获取定位"按钮改为调用智能定位接口
- [ ] 保留现有配置弹窗(目标菜单、自动登录等)
- [ ] 实时显示定位进度(WebSocket)
- [ ] 定位结果展示:成功/不确定/失败,可编辑选择器
### 验证标准
- [ ] 点击"获取定位"后可正常调用智能定位接口
- [ ] 定位结果自动填充到步骤表格
- [ ] 可直接保存并执行用例
---
## Phase 4: 优化与文档更新(1 天)
### 任务清单
- [ ] 端到端测试:创建"会议管理"模块用例并验证执行
- [ ] 更新 `HANDOFF_UI自动化.md`
- [ ] 删除旧的 `prd-code` skill 相关文档(`_PRD_用例录制器功能.md`
- [ ] 清理临时脚本
### 验证标准
- [ ] 新建用例定位准确率 ≥ 95%
- [ ] 用例首次执行通过率 ≥ 90%
---
## 文件清单
### 新增文件
| 文件 | Phase | 说明 |
|------|-------|------|
| `backend/app/models/login_template.py` | 1 | 登录模板 ORM |
| `backend/app/schemas/login_template.py` | 1 | 登录模板 Schema |
| `backend/app/routers/login_template.py` | 1 | 登录模板路由 |
| `backend/app/services/login_template_service.py` | 1 | 登录模板服务 |
| `backend/app/services/keyword_matcher.py` | 2 | 关键词匹配模块 |
| `backend/app/services/selector_extractor.py` | 2 | 选择器提取模块 |
| `backend/app/services/smart_locate_service.py` | 2 | 智能定位服务 |
| `backend/app/routers/smart_locate.py` | 2 | 智能定位路由 |
### 修改文件
| 文件 | Phase | 说明 |
|------|-------|------|
| `backend/app/main.py` | 1,2 | 注册新路由 |
| `backend/app/database.py` | 1 | 新增 login_templates 表 |
| `backend/app/executors/playwright_executor.py` | 2 | 增加 `do_login_template()` 方法 |
| `frontend/src/api/elementLocate.ts` | 3 | 增加 smartLocate 接口 |
| `frontend/src/views/Cases.vue` | 3 | 改造获取定位功能 |
---
## 与旧方案对比
| 对比项 | 旧方案(AI 推断) | 新方案(智能定位) |
|--------|-----------------|------------------|
| 核心接口 | `/api/element/locate-batch` | `/api/element/smart-locate` |
| Claude CLI | 必须依赖 | 不需要 |
| 选择器来源 | AI 静态推断 | 实际执行验证 |
| 准确率 | 不稳定 | 高(已验证 12/12 通过) |
| 延迟 | 10-30s | 预计 5-15s |
| 成本 | Claude API 调用 | 无额外成本 |
---
*本文档由 Claude Code 于 2026-08-05 创建。*
# 执行计划 — 修复 metadata 字段错误
> **文档类型**: 执行计划文档
> **创建日期**: 2026-08-03
> **作者**: Claude Code
> **优先级**: P0(阻塞)
> **版本**: v5.60
> **状态**: ✅ 全部完成
---
## 一、执行概述
### 1.1 目标
修复创建模拟设备时的多个关联错误,恢复设备创建功能。
### 1.2 范围
- 修改 `backend/app/services/device_sim_service.py` 字段名
- 修改 `backend/app/schemas/device_sim.py` 添加验证器和缺失字段
- 数据库迁移添加 `topic_params`
- 更新 PRD 文档版本至 v5.60
### 1.3 实际工时
30 分钟
---
## 二、执行步骤
### Step 1: 修复 service 层字段名(✅ 已完成)
**文件**: `backend/app/services/device_sim_service.py:389`
```python
# 修改前
extra_attrs=data.metadata or {},
# 修改后
extra_attrs=data.extra_attrs or {},
```
---
### Step 2: 添加 field_validator(✅ 已完成)
**文件**: `backend/app/schemas/device_sim.py`
1. 添加导入:`field_validator`
2.`SimulatorResponse` 中添加 `ignore_sqlalchemy_metadata` 验证器
3. 添加 `metadata` 字段声明(`exclude=True`
---
### Step 3: 恢复缺失字段(✅ 已完成)
**文件**: `backend/app/schemas/device_sim.py`
1. `SimulatorBase` 添加 `topic_params` 字段
2. `SimulatorUpdate` 添加 `topic_params` 字段
3. 添加 `TopicTemplateResponse``TopicTemplateListResponse`
---
### Step 4: 数据库迁移(✅ 已完成)
```sql
ALTER TABLE device_simulators ADD COLUMN topic_params TEXT
```
---
### Step 5: 重启服务并验证(✅ 已完成)
- 后端服务已重启
- 创建门口屏设备:201 ✓
- 创建无纸化设备:201 ✓
- 创建中控设备:201 ✓
- 主题模板 API:200 ✓
---
### Step 6: 更新版本标记(✅ 已完成)
PRD 文档已更新为 v5.60。
---
## 三、修改文件清单
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `backend/app/services/device_sim_service.py` | 修改 | 第 389 行:字段名修正 |
| `backend/app/schemas/device_sim.py` | 修改 | 添加验证器、缺失字段、主题模板 Schema |
| `backend/data/test_platform.db` | 修改 | 添加 `topic_params` 列 |
| `Docs/PRD/需求文档/设备模拟/_PRD_需求文档_设备模拟模块.md` | 修改 | 更新版本至 v5.60 |
| `Docs/PRD/需求文档/设备模拟/_问题处理_创建模拟设备失败_metadata字段错误.md` | 新建 | 问题处理文档 |
| `Docs/PRD/需求文档/设备模拟/_执行计划_修复metadata字段错误_v5.60.md` | 新建 | 本文档 |
---
## 四、验收标准
- [x] service 层字段名修正
- [x] Schema 添加 field_validator
- [x] Schema 恢复缺失字段
- [x] 数据库迁移完成
- [x] 后端服务重启
- [x] 创建设备 API 返回 201
- [x] 不同设备类型均可创建
- [x] 主题参数配置功能正常
- [x] 无其他副作用
---
## 五、相关文档
- 问题处理文档: `_问题处理_创建模拟设备失败_metadata字段错误.md`
- PRD 文档: `_PRD_需求文档_设备模拟模块.md`
- 交接文档: `HANDOFF_设备模拟_主题配置改造.md`
---
*本文档为修复 metadata 字段错误的执行计划,版本 v5.60,全部完成。*
此差异已折叠。
...@@ -52,6 +52,7 @@ class EnvConfig(Base): ...@@ -52,6 +52,7 @@ class EnvConfig(Base):
topics: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="多主题前缀映射,如 {'door': 'device/door', 'paperless': 'device/paperless'}") topics: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="多主题前缀映射,如 {'door': 'device/door', 'paperless': 'device/paperless'}")
use_tls: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否启用 TLS") use_tls: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否启用 TLS")
client_id_prefix: Mapped[str] = mapped_column(String(100), default="sim", comment="客户端 ID 前缀") client_id_prefix: Mapped[str] = mapped_column(String(100), default="sim", comment="客户端 ID 前缀")
default_topic_params: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="默认主题参数,含 company_id/room_ids/app_token_prefix/app_token_start/app_token_end/conference_id/conference_name")
status: Mapped[str] = mapped_column(String(20), default="disconnected", comment="连接状态") status: Mapped[str] = mapped_column(String(20), default="disconnected", comment="连接状态")
last_connected_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="最后连接时间") last_connected_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="最后连接时间")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, comment="创建时间") created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, comment="创建时间")
...@@ -90,6 +91,8 @@ class DeviceSimulator(Base): ...@@ -90,6 +91,8 @@ class DeviceSimulator(Base):
status: Mapped[str] = mapped_column(String(20), default="stopped", comment="运行状态") status: Mapped[str] = mapped_column(String(20), default="stopped", comment="运行状态")
auto_reconnect: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否自动重连") auto_reconnect: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否自动重连")
extra_attrs: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="设备自定义属性") extra_attrs: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="设备自定义属性")
topic_params: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="主题动态参数,如 {'room_id': 'A101', 'company_id': '001'}")
report_config: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="上报配置,含 interval(秒)/enabled(开关)")
last_reported_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="最后上报时间") last_reported_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="最后上报时间")
total_reports: Mapped[int] = mapped_column(Integer, default=0, comment="累计上报次数") total_reports: Mapped[int] = mapped_column(Integer, default=0, comment="累计上报次数")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, comment="创建时间") created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, comment="创建时间")
......
...@@ -34,6 +34,7 @@ class EnvConfigBase(BaseModel): ...@@ -34,6 +34,7 @@ class EnvConfigBase(BaseModel):
topics: Optional[Dict[str, str]] = Field(default=None, description="多主题前缀映射,key为设备类型,value为主题前缀") topics: Optional[Dict[str, str]] = Field(default=None, description="多主题前缀映射,key为设备类型,value为主题前缀")
use_tls: bool = Field(default=False, description="是否启用 TLS") use_tls: bool = Field(default=False, description="是否启用 TLS")
client_id_prefix: str = Field(default="sim", max_length=100, description="客户端 ID 前缀") client_id_prefix: str = Field(default="sim", max_length=100, description="客户端 ID 前缀")
default_topic_params: Optional[dict] = Field(default=None, description="默认主题参数,如 {'company_id': '001', 'room_ids': ['A101', 'A202']}")
model_config = ConfigDict( model_config = ConfigDict(
alias_generator=to_camel, alias_generator=to_camel,
...@@ -58,6 +59,7 @@ class EnvConfigUpdate(BaseModel): ...@@ -58,6 +59,7 @@ class EnvConfigUpdate(BaseModel):
topics: Optional[Dict[str, str]] = Field(default=None, description="多主题前缀映射") topics: Optional[Dict[str, str]] = Field(default=None, description="多主题前缀映射")
use_tls: Optional[bool] = Field(default=None, description="是否启用 TLS") use_tls: Optional[bool] = Field(default=None, description="是否启用 TLS")
client_id_prefix: Optional[str] = Field(default=None, max_length=100, description="客户端 ID 前缀") client_id_prefix: Optional[str] = Field(default=None, max_length=100, description="客户端 ID 前缀")
default_topic_params: Optional[dict] = Field(default=None, description="默认主题参数")
model_config = ConfigDict( model_config = ConfigDict(
alias_generator=to_camel, alias_generator=to_camel,
...@@ -100,6 +102,21 @@ class SimulatorBase(BaseModel): ...@@ -100,6 +102,21 @@ class SimulatorBase(BaseModel):
auto_reconnect: bool = Field(default=True, description="是否自动重连") auto_reconnect: bool = Field(default=True, description="是否自动重连")
extra_attrs: Optional[dict] = Field(default=None, description="设备自定义属性") extra_attrs: Optional[dict] = Field(default=None, description="设备自定义属性")
topic_params: Optional[Dict[str, str]] = Field(default=None, description="主题动态参数") topic_params: Optional[Dict[str, str]] = Field(default=None, description="主题动态参数")
report_config: Optional[dict] = Field(default=None, description="上报配置,含 interval(秒)/enabled(开关)")
@field_validator('report_config')
@classmethod
def validate_report_config(cls, v):
"""验证上报配置字段"""
if v is None:
return None
if 'interval' in v:
if not isinstance(v['interval'], int) or v['interval'] < 1:
raise ValueError('interval must be a positive integer (>= 1)')
if 'enabled' in v:
if not isinstance(v['enabled'], bool):
raise ValueError('enabled must be a boolean')
return v
model_config = ConfigDict( model_config = ConfigDict(
alias_generator=to_camel, alias_generator=to_camel,
...@@ -119,6 +136,21 @@ class SimulatorUpdate(BaseModel): ...@@ -119,6 +136,21 @@ class SimulatorUpdate(BaseModel):
auto_reconnect: Optional[bool] = Field(default=None, description="是否自动重连") auto_reconnect: Optional[bool] = Field(default=None, description="是否自动重连")
extra_attrs: Optional[dict] = Field(default=None, description="设备自定义属性") extra_attrs: Optional[dict] = Field(default=None, description="设备自定义属性")
topic_params: Optional[Dict[str, str]] = Field(default=None, description="主题动态参数") topic_params: Optional[Dict[str, str]] = Field(default=None, description="主题动态参数")
report_config: Optional[dict] = Field(default=None, description="上报配置,含 interval(秒)/enabled(开关)")
@field_validator('report_config')
@classmethod
def validate_report_config(cls, v):
"""验证上报配置字段"""
if v is None:
return None
if 'interval' in v:
if not isinstance(v['interval'], int) or v['interval'] < 1:
raise ValueError('interval must be a positive integer (>= 1)')
if 'enabled' in v:
if not isinstance(v['enabled'], bool):
raise ValueError('enabled must be a boolean')
return v
model_config = ConfigDict( model_config = ConfigDict(
alias_generator=to_camel, alias_generator=to_camel,
......
...@@ -388,6 +388,7 @@ class DeviceSimService: ...@@ -388,6 +388,7 @@ class DeviceSimService:
auto_reconnect=data.auto_reconnect, auto_reconnect=data.auto_reconnect,
extra_attrs=data.extra_attrs or {}, extra_attrs=data.extra_attrs or {},
topic_params=data.topic_params or {}, topic_params=data.topic_params or {},
report_config=data.report_config,
status="stopped", status="stopped",
) )
...@@ -507,6 +508,7 @@ class DeviceSimService: ...@@ -507,6 +508,7 @@ class DeviceSimService:
device_id=simulator.device_id, device_id=simulator.device_id,
env_config_id=config.id, env_config_id=config.id,
mqtt_manager=mqtt_manager, mqtt_manager=mqtt_manager,
report_config=simulator.report_config,
topics=topics, topics=topics,
topic_params=simulator.topic_params, topic_params=simulator.topic_params,
) )
...@@ -529,7 +531,11 @@ class DeviceSimService: ...@@ -529,7 +531,11 @@ class DeviceSimService:
_running_simulators[device_id] = sim _running_simulators[device_id] = sim
simulator.status = "running" simulator.status = "running"
simulator.updated_at = datetime.now() simulator.updated_at = datetime.now()
try:
await self.db.flush() await self.db.flush()
except Exception as e:
logger.warning(f"更新设备状态失败(模拟器已启动): {device_id}, error={e}")
# 不抛出异常,模拟器已在运行
return success return success
......
...@@ -37,8 +37,9 @@ DEFAULT_INTERVAL_MAP = { ...@@ -37,8 +37,9 @@ DEFAULT_INTERVAL_MAP = {
def create_simulator(device_type: str, device_id: str, def create_simulator(device_type: str, device_id: str,
env_config_id: str, mqtt_manager: MqttManager, env_config_id: str, mqtt_manager: MqttManager,
report_interval: Optional[int] = None, report_config: Optional[dict] = None,
topics: Optional[dict] = None) -> Optional[BaseSimulator]: topics: Optional[dict] = None,
topic_params: Optional[dict] = None) -> Optional[BaseSimulator]:
""" """
模拟器工厂方法 模拟器工厂方法
...@@ -49,8 +50,9 @@ def create_simulator(device_type: str, device_id: str, ...@@ -49,8 +50,9 @@ def create_simulator(device_type: str, device_id: str,
device_id: 设备 ID device_id: 设备 ID
env_config_id: 环境配置 ID env_config_id: 环境配置 ID
mqtt_manager: MQTT 管理器实例 mqtt_manager: MQTT 管理器实例
report_interval: 上报间隔(秒),None 使用默认值 report_config: 上报配置,如 {'interval': 30, 'enabled': True}
topics: 多主题前缀映射,key 为设备类型,value 为主题前缀 topics: 多主题前缀映射,key 为设备类型,value 为主题前缀
topic_params: 主题动态参数,如 {'room_id': 'A101', 'company_id': '001'}
Returns: Returns:
BaseSimulator: 模拟器实例,如果设备类型不支持则返回 None BaseSimulator: 模拟器实例,如果设备类型不支持则返回 None
...@@ -59,15 +61,18 @@ def create_simulator(device_type: str, device_id: str, ...@@ -59,15 +61,18 @@ def create_simulator(device_type: str, device_id: str,
if not simulator_class: if not simulator_class:
return None return None
if report_interval is None: # 如果未提供 report_config,使用设备类型默认值
report_interval = DEFAULT_INTERVAL_MAP.get(device_type, 30) if report_config is None:
default_interval = DEFAULT_INTERVAL_MAP.get(device_type, 30)
report_config = {"interval": default_interval, "enabled": True}
return simulator_class( return simulator_class(
device_id=device_id, device_id=device_id,
env_config_id=env_config_id, env_config_id=env_config_id,
mqtt_manager=mqtt_manager, mqtt_manager=mqtt_manager,
report_interval=report_interval, report_config=report_config,
topics=topics or {} topics=topics or {},
topic_params=topic_params,
) )
......
...@@ -97,10 +97,11 @@ class CentralSimulator(BaseSimulator): ...@@ -97,10 +97,11 @@ class CentralSimulator(BaseSimulator):
} }
def __init__(self, device_id: str, env_config_id: str, def __init__(self, device_id: str, env_config_id: str,
mqtt_manager: MqttManager, report_interval: int = 30, mqtt_manager: MqttManager, report_config: Optional[dict] = None,
topics: Optional[dict] = None): topics: Optional[dict] = None,
topic_params: Optional[dict] = None):
super().__init__(device_id, env_config_id, "central", mqtt_manager, super().__init__(device_id, env_config_id, "central", mqtt_manager,
report_interval, topics) report_config, topics, topic_params)
self._sub_devices = {} self._sub_devices = {}
self._current_scene = "power_off" self._current_scene = "power_off"
self._init_sub_devices() self._init_sub_devices()
......
...@@ -42,8 +42,9 @@ class ClientSimulator(BaseSimulator): ...@@ -42,8 +42,9 @@ class ClientSimulator(BaseSimulator):
_VERSIONS = ["V1.2.0", "V1.3.0", "V1.4.0", "V2.0.0", "V2.1.0"] _VERSIONS = ["V1.2.0", "V1.3.0", "V1.4.0", "V2.0.0", "V2.1.0"]
def __init__(self, device_id: str, env_config_id: str, def __init__(self, device_id: str, env_config_id: str,
mqtt_manager: MqttManager, report_interval: int = 5, mqtt_manager: MqttManager, report_config: Optional[dict] = None,
topics: Optional[dict] = None): topics: Optional[dict] = None,
topic_params: Optional[dict] = None):
""" """
初始化集控客户端模拟器 初始化集控客户端模拟器
...@@ -51,11 +52,12 @@ class ClientSimulator(BaseSimulator): ...@@ -51,11 +52,12 @@ class ClientSimulator(BaseSimulator):
device_id: 设备 ID device_id: 设备 ID
env_config_id: 环境配置 ID env_config_id: 环境配置 ID
mqtt_manager: MQTT 管理器 mqtt_manager: MQTT 管理器
report_interval: 上报间隔(秒),集控客户端心跳频率更高 report_config: 上报配置,如 {'interval': 5, 'enabled': True}
topics: 多主题前缀映射 topics: 多主题前缀映射
topic_params: 主题动态参数
""" """
super().__init__(device_id, env_config_id, "client", mqtt_manager, super().__init__(device_id, env_config_id, "client", mqtt_manager,
report_interval, topics) report_config, topics, topic_params)
self._version = random.choice(self._VERSIONS) self._version = random.choice(self._VERSIONS)
self._heartbeat_count = 0 self._heartbeat_count = 0
self._log_level = "info" self._log_level = "info"
......
...@@ -38,8 +38,9 @@ class DoorSimulator(BaseSimulator): ...@@ -38,8 +38,9 @@ class DoorSimulator(BaseSimulator):
""" """
def __init__(self, device_id: str, env_config_id: str, def __init__(self, device_id: str, env_config_id: str,
mqtt_manager: MqttManager, report_interval: int = 30, mqtt_manager: MqttManager, report_config: Optional[dict] = None,
topics: Optional[dict] = None): topics: Optional[dict] = None,
topic_params: Optional[dict] = None):
""" """
初始化门口屏模拟器 初始化门口屏模拟器
...@@ -47,11 +48,12 @@ class DoorSimulator(BaseSimulator): ...@@ -47,11 +48,12 @@ class DoorSimulator(BaseSimulator):
device_id: 设备 ID device_id: 设备 ID
env_config_id: 环境配置 ID env_config_id: 环境配置 ID
mqtt_manager: MQTT 管理器 mqtt_manager: MQTT 管理器
report_interval: 上报间隔(秒) report_config: 上报配置,如 {'interval': 30, 'enabled': True}
topics: 多主题前缀映射 topics: 多主题前缀映射
topic_params: 主题动态参数
""" """
super().__init__(device_id, env_config_id, "door", mqtt_manager, super().__init__(device_id, env_config_id, "door", mqtt_manager,
report_interval, topics) report_config, topics, topic_params)
self._door_status = "closed" self._door_status = "closed"
self._battery_level = random.randint(60, 100) self._battery_level = random.randint(60, 100)
self._call_active = False self._call_active = False
...@@ -156,6 +158,10 @@ class DoorSimulator(BaseSimulator): ...@@ -156,6 +158,10 @@ class DoorSimulator(BaseSimulator):
def _auto_close_door(self) -> None: def _auto_close_door(self) -> None:
"""模拟自动关门""" """模拟自动关门"""
self._door_status = "closed" self._door_status = "closed"
if self._has_real_topics:
publish_topics = self.get_publish_topics()
status_topic = publish_topics[0]["topic"] if publish_topics else self.get_topic_prefix()
else:
status_topic = f"{self.get_topic_prefix()}/{self.device_id}/status" status_topic = f"{self.get_topic_prefix()}/{self.device_id}/status"
self.mqtt.publish(self.env_config_id, status_topic, { self.mqtt.publish(self.env_config_id, status_topic, {
"device_id": self.device_id, "device_id": self.device_id,
...@@ -168,6 +174,10 @@ class DoorSimulator(BaseSimulator): ...@@ -168,6 +174,10 @@ class DoorSimulator(BaseSimulator):
"""模拟设备重启""" """模拟设备重启"""
import threading as t import threading as t
# 发送离线 # 发送离线
if self._has_real_topics:
publish_topics = self.get_publish_topics()
offline_topic = publish_topics[0]["topic"] if publish_topics else self.get_topic_prefix()
else:
offline_topic = f"{self.get_topic_prefix()}/{self.device_id}/status" offline_topic = f"{self.get_topic_prefix()}/{self.device_id}/status"
self.mqtt.publish(self.env_config_id, offline_topic, { self.mqtt.publish(self.env_config_id, offline_topic, {
"device_id": self.device_id, "status": "offline", "reason": "reboot" "device_id": self.device_id, "status": "offline", "reason": "reboot"
...@@ -176,6 +186,10 @@ class DoorSimulator(BaseSimulator): ...@@ -176,6 +186,10 @@ class DoorSimulator(BaseSimulator):
def _recover(): def _recover():
time.sleep(5) time.sleep(5)
if self._running: if self._running:
if self._has_real_topics:
publish_topics = self.get_publish_topics()
status_topic = publish_topics[0]["topic"] if publish_topics else self.get_topic_prefix()
else:
status_topic = f"{self.get_topic_prefix()}/{self.device_id}/status" status_topic = f"{self.get_topic_prefix()}/{self.device_id}/status"
self.mqtt.publish(self.env_config_id, status_topic, self.build_status_payload()) self.mqtt.publish(self.env_config_id, status_topic, self.build_status_payload())
t.Thread(target=_recover, daemon=True).start() t.Thread(target=_recover, daemon=True).start()
......
...@@ -52,10 +52,11 @@ class PaperlessSimulator(BaseSimulator): ...@@ -52,10 +52,11 @@ class PaperlessSimulator(BaseSimulator):
] ]
def __init__(self, device_id: str, env_config_id: str, def __init__(self, device_id: str, env_config_id: str,
mqtt_manager: MqttManager, report_interval: int = 30, mqtt_manager: MqttManager, report_config: Optional[dict] = None,
topics: Optional[dict] = None): topics: Optional[dict] = None,
topic_params: Optional[dict] = None):
super().__init__(device_id, env_config_id, "paperless", mqtt_manager, super().__init__(device_id, env_config_id, "paperless", mqtt_manager,
report_interval, topics) report_config, topics, topic_params)
self._meeting_status = "idle" self._meeting_status = "idle"
self._signed_in = False self._signed_in = False
self._file_sync_progress = 0 self._file_sync_progress = 0
...@@ -187,8 +188,15 @@ class PaperlessSimulator(BaseSimulator): ...@@ -187,8 +188,15 @@ class PaperlessSimulator(BaseSimulator):
time.sleep(1) time.sleep(1)
self._file_sync_progress = i * 10 self._file_sync_progress = i * 10
# 上报同步进度 # 上报同步进度
sync_topic = f"{self.get_topic_prefix()}/{self.device_id}/file_sync" if self._has_real_topics:
self.mqtt.publish(self.env_config_id, sync_topic, { # 使用真实主题,找 meeting_message 或第一个 publish 主题
topic = self.get_resolved_topic("meeting_message")
if not topic:
publish_topics = self.get_publish_topics()
topic = publish_topics[0]["topic"] if publish_topics else self.get_topic_prefix()
else:
topic = f"{self.get_topic_prefix()}/{self.device_id}/file_sync"
self.mqtt.publish(self.env_config_id, topic, {
"device_id": self.device_id, "device_id": self.device_id,
"progress": self._file_sync_progress, "progress": self._file_sync_progress,
"file_name": f"meeting_materials_{time.strftime('%Y%m%d')}.pdf", "file_name": f"meeting_materials_{time.strftime('%Y%m%d')}.pdf",
......
...@@ -74,6 +74,8 @@ export interface BatchLocateRequest { ...@@ -74,6 +74,8 @@ export interface BatchLocateRequest {
retry_on_empty?: boolean retry_on_empty?: boolean
/** 最大重试次数(默认 2) */ /** 最大重试次数(默认 2) */
max_retries?: number max_retries?: number
/** 目标菜单名称(如"会议管理"、"信息管理",支持智能推断) */
navigate_menu?: string
} }
/** /**
......
...@@ -36,6 +36,15 @@ export interface EnvConfig { ...@@ -36,6 +36,15 @@ export interface EnvConfig {
topics: Record<string, string> | null topics: Record<string, string> | null
useTls: boolean useTls: boolean
clientIdPrefix: string clientIdPrefix: string
defaultTopicParams: {
company_id?: string
room_ids?: string[]
app_token_prefix?: string
app_token_start?: number
app_token_end?: number
conference_id?: string
conference_name?: string
} | null
status: string status: string
lastConnectedAt: string | null lastConnectedAt: string | null
createdAt: string createdAt: string
...@@ -53,6 +62,15 @@ export interface EnvConfigCreate { ...@@ -53,6 +62,15 @@ export interface EnvConfigCreate {
topics?: Record<string, string> | null topics?: Record<string, string> | null
useTls?: boolean useTls?: boolean
clientIdPrefix?: string clientIdPrefix?: string
defaultTopicParams?: {
company_id?: string
room_ids?: string[]
app_token_prefix?: string
app_token_start?: number
app_token_end?: number
conference_id?: string
conference_name?: string
} | null
} }
/** 更新环境配置请求 */ /** 更新环境配置请求 */
...@@ -66,6 +84,15 @@ export interface EnvConfigUpdate { ...@@ -66,6 +84,15 @@ export interface EnvConfigUpdate {
topics?: Record<string, string> | null topics?: Record<string, string> | null
useTls?: boolean useTls?: boolean
clientIdPrefix?: string clientIdPrefix?: string
defaultTopicParams?: {
company_id?: string
room_ids?: string[]
app_token_prefix?: string
app_token_start?: number
app_token_end?: number
conference_id?: string
conference_name?: string
} | null
} }
/** 模拟设备 */ /** 模拟设备 */
...@@ -79,6 +106,10 @@ export interface Simulator { ...@@ -79,6 +106,10 @@ export interface Simulator {
autoReconnect: boolean autoReconnect: boolean
extraAttrs: Record<string, any> | null extraAttrs: Record<string, any> | null
topicParams: Record<string, string> | null topicParams: Record<string, string> | null
reportConfig?: {
interval: number // 上报间隔(秒)
enabled?: boolean // 是否启用定时上报
}
lastReportedAt: string | null lastReportedAt: string | null
totalReports: number totalReports: number
createdAt: string createdAt: string
...@@ -94,6 +125,10 @@ export interface SimulatorCreate { ...@@ -94,6 +125,10 @@ export interface SimulatorCreate {
autoReconnect?: boolean autoReconnect?: boolean
extraAttrs?: Record<string, any> | null extraAttrs?: Record<string, any> | null
topicParams?: Record<string, string> | null topicParams?: Record<string, string> | null
reportConfig?: {
interval: number
enabled?: boolean
}
} }
/** 更新模拟设备请求 */ /** 更新模拟设备请求 */
...@@ -102,6 +137,10 @@ export interface SimulatorUpdate { ...@@ -102,6 +137,10 @@ export interface SimulatorUpdate {
autoReconnect?: boolean autoReconnect?: boolean
extraAttrs?: Record<string, any> | null extraAttrs?: Record<string, any> | null
topicParams?: Record<string, string> | null topicParams?: Record<string, string> | null
reportConfig?: {
interval: number
enabled?: boolean
}
} }
/** 上报记录 */ /** 上报记录 */
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论