feat(workbench): persist operational workflows and events
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# 15173 联调 UI Keycloak 客户端
|
||||
|
||||
2026-09-23 已在 `tjwater` realm 验证独立 public client
|
||||
`tjwater-integration-ui`。该客户端只用于本机 15173 联调页面,不替代或修改
|
||||
现有 `next-tjwater` 客户端。
|
||||
|
||||
已验证配置:
|
||||
|
||||
- Standard Authorization Code Flow:开启。
|
||||
- PKCE:强制 `S256`。
|
||||
- Implicit Flow、Direct Access Grants、Service Accounts:关闭。
|
||||
- Public client:开启,不保存客户端密钥。
|
||||
- Full Scope Allowed:关闭;未授予 realm-management 或系统管理员角色。
|
||||
- Redirect URI:`http://127.0.0.1:15173/*`、`http://localhost:15173/*`。
|
||||
- Web Origin:`http://127.0.0.1:15173`、`http://localhost:15173`。
|
||||
|
||||
可使用以下幂等脚本复核或恢复同一配置;管理员密码仅从隐藏提示或进程环境
|
||||
读取,不会写入文件:
|
||||
|
||||
```bash
|
||||
conda run -n server python scripts/provision_integration_ui_client.py
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
# 工况工作进程与外部动作适配
|
||||
|
||||
工况执行不依赖浏览器保活。API 保存任务及审批事实,工作进程扫描持久化队列,再向 Agent 提交后端分配的执行请求编号。相同请求编号重试复用同一执行,不能根据轮询时间生成新的编号。
|
||||
|
||||
## 调查任务队列
|
||||
|
||||
当前产品分为定时任务、调查任务、预约任务。定时任务保留前端 mock 数据,不调用后端计划创建或到期调度;调查任务读取数据库并进入本工作队列;预约任务读取数据库中的外围工单投影,以外部同步进度为准。已有后端 schedule API 暂不在当前产品主路径使用。
|
||||
|
||||
从后端仓库运行:
|
||||
|
||||
```bash
|
||||
conda run -n server python scripts/run_operational_worker.py --once
|
||||
conda run -n server python scripts/run_operational_worker.py
|
||||
```
|
||||
|
||||
部署环境提供以下变量,不将实际值写入仓库。工作进程也读取后端仓库已忽略的 `.env`,已存在的进程环境变量优先:
|
||||
|
||||
| 变量 | 用途 |
|
||||
| --- | --- |
|
||||
| `OPERATIONAL_WORKER_PROJECT_ID` | 唯一授权项目 UUID |
|
||||
| `OPERATIONAL_WORKER_SERVER_URL` | 主后端地址,默认本机 8000 |
|
||||
| `OPERATIONAL_WORKER_AGENT_URL` | Agent 地址,默认本机 8787 |
|
||||
| `OPERATIONAL_WORKER_RECONCILE_SECONDS` | SSE 中断或漏通知时的对账间隔,默认 30 秒 |
|
||||
| `OPERATIONAL_WORKER_MAX_CONCURRENCY` | 同时投递的 run 上限,默认 4,可设置 1–32 |
|
||||
| `OPERATIONAL_WORKER_BEARER` | 已具有该项目权限的产品身份,仅用于受控开发或短期运行 |
|
||||
| `OPERATIONAL_WORKER_TOKEN_URL` | 可选 OAuth 客户端凭据取令牌地址 |
|
||||
| `OPERATIONAL_WORKER_CLIENT_ID`、`OPERATIONAL_WORKER_CLIENT_SECRET` | 长期运行身份;不配置固定 Bearer 时使用,并按有效期刷新令牌 |
|
||||
| `OPERATIONAL_WORKER_TOKEN_REFRESH_SECONDS` | OAuth 缓存上限,默认 300 秒,可设置 30–600 秒;同时受令牌 expires_in 限制,应短于后端允许的最大令牌年龄 |
|
||||
|
||||
工作身份必须能通过现有产品身份与项目成员权限校验。配置令牌获取不等于自动创建产品用户、项目成员或授权。权限错误会保留任务用于后续重试,不会绕过认证。
|
||||
|
||||
长期 OAuth 身份由身份管理员创建支持 `client_credentials` 的专用机密客户端;服务账号令牌应具有后端配置要求的 audience、UUID `sub` 和 `preferred_username`。再由产品管理员把该 `sub` 绑定到启用的产品用户及目标项目成员关系。只赋予本次调查所需项目权限,不授予系统管理员或设备命令权限。当前项目成员策略同时包含工单审批/执行权限,不能将其误称为已经细分的专用最小权限服务角色。
|
||||
|
||||
本机联调配置示意(密钥由管理员通过安全配置填入,禁止粘贴到聊天):
|
||||
|
||||
```dotenv
|
||||
OPERATIONAL_WORKER_PROJECT_ID=324ff7ee-c2d4-5184-a84b-48fb14a2933e
|
||||
OPERATIONAL_WORKER_SERVER_URL=http://127.0.0.1:8000
|
||||
OPERATIONAL_WORKER_AGENT_URL=http://127.0.0.1:18787
|
||||
OPERATIONAL_WORKER_TOKEN_URL=https://<identity-host>/realms/<realm>/protocol/openid-connect/token
|
||||
OPERATIONAL_WORKER_CLIENT_ID=<dedicated-service-client>
|
||||
OPERATIONAL_WORKER_CLIENT_SECRET=<inject-securely>
|
||||
OPERATIONAL_ACTION_PERIPHERAL_URL=http://127.0.0.1:8011
|
||||
OPERATIONAL_DATA_PERIPHERAL_URL=http://127.0.0.1:8011
|
||||
```
|
||||
|
||||
选择 OAuth 时不要同时配置 `OPERATIONAL_WORKER_BEARER`,否则固定 Bearer 优先。不要配置设备命令网关进行本次合成联调。
|
||||
|
||||
工作进程先恢复过期执行租约,再使用 keyset 游标扫描持久化 run 队列;`operational.run.queued` 事件会立即唤醒扫描,连接中断时每 30 秒对账。Agent 正在执行时每 20 秒续租,租约为 60 秒;进程中断后由后端分配新的执行代次,旧 run 和旧会话不能继续覆盖新一轮结果。网络错误及 5xx 从 5 秒指数退避到 5 分钟,确定性 4xx 终止该 run;连续 10 次投递失败产生运维通知。
|
||||
|
||||
Agent 正常结束必须调用 run `complete`。同一执行代次缺少最终计划或报告时,Server 将 run 以 `ARTIFACT_INCOMPLETE` 明确失败,不依赖租约超时猜测完成状态。心跳只增加 run revision,不改变任务业务 revision。
|
||||
|
||||
## 事件日志与清理
|
||||
|
||||
`resources/sql/015_workbench_events_and_task_runs.sql` 在每个项目 BizDB 中增加 `domain_event_log` 和 `operational_task_runs`。事件与业务投影在同一事务提交,`LISTEN/NOTIFY` 只负责唤醒;浏览器与 worker 始终按持久游标补读。每个 API 进程对活跃项目最多保留一条 LISTEN 连接,无订阅者 60 秒后关闭。
|
||||
|
||||
事件默认保留 7 天。定时任务先执行 dry-run,再执行清理;脚本从 Metadata DB 解析所有活跃项目路由,并在发现多个项目共享同一 BizDB 时拒绝运行:
|
||||
|
||||
```bash
|
||||
conda run -n server python scripts/cleanup_workbench_events.py --dry-run
|
||||
conda run -n server python scripts/cleanup_workbench_events.py
|
||||
```
|
||||
|
||||
浏览器使用 `GET /api/v1/workbench/sync-snapshot` 建立原子快照,并以该游标连接 `GET /api/v1/workbench/events`。游标早于保留窗口、超前或对应事件已全部清理时返回 `reset_required`,客户端必须重建快照。SSE 需要 Bearer 与 `X-Project-Id`,且 `CORS_ALLOWED_ORIGINS` 必须配置为明确来源。
|
||||
|
||||
## 外围样板工单同步
|
||||
|
||||
```bash
|
||||
conda run -n server python scripts/sync_peripheral_work_orders.py --once
|
||||
conda run -n server python scripts/sync_peripheral_work_orders.py
|
||||
```
|
||||
|
||||
使用相同工作身份,并配置 `OPERATIONAL_ACTION_PERIPHERAL_URL`。同步器读取分页工单,将外围版本投影到 `external_work_order` 任务。合成来源在摘要和证据中明确标注;工单完成只表示该外部任务完成,不代表其他调查自动完成。
|
||||
|
||||
水质投诉和爆管上报类型的外围工单同时创建关联调查,即使当时没有浏览器打开也会入队。两类事件从外围样板测试面板模拟上报;工况页负责展示调查。调查中创建的外围工单通过回执编号关联,后续反馈送回原调查,由 Agent 重新分析。
|
||||
|
||||
## 旧调查迁移
|
||||
|
||||
`scripts/import_agent_investigation.py <session_id> --dry-run` 只检查旧合成水质调查能否转换。去掉 `--dry-run` 后使用上述产品工作身份导入后端任务、流程、证据及阶段报告;不会修改原 Agent 文件,也不会覆盖已存在的任务计划。应在启用队列工作进程前导入,避免旧调查被当作新的待执行任务领取。
|
||||
|
||||
## 调查数据读取
|
||||
|
||||
`OPERATIONAL_DATA_SERVER_URL` 默认本机 8000;外围样板数据读取需配置 `OPERATIONAL_DATA_PERIPHERAL_URL`。查询只允许预定义的数据源、最多 20 个设备和 24 小时时间范围,响应有大小限制。每次查询保存内容摘要对应的数据快照;缺失、拒绝、超限或未配置都返回明确错误,不生成正常数据结论。截断结果必须保留不完整标记。
|
||||
|
||||
## 人工确认后的动作
|
||||
|
||||
- `OPERATIONAL_ACTION_PERIPHERAL_URL`:使用既有 `/api/sample/v1/work-orders` 创建合成外围工单。成功回执说明工单已创建,不表示现场工作已经完成。
|
||||
- `OPERATIONAL_ACTION_COMMAND_GATEWAY_URL`:可选设备命令网关。未配置时明确返回不支持执行,不能生成成功回执。
|
||||
- `OPERATIONAL_ACTION_TIMEOUT_SECONDS`:外部请求超时,默认 10 秒。
|
||||
|
||||
命令网关契约:`POST /api/v1/commands` 接收 `request_id`、`project_id`、`task_id`、`action_id`、`action_version`、`approved_by`、`approved_at` 与类型化 `command`。网关必须按 `request_id` 幂等处理,并在执行前验证目标设备当前状态、前置条件和命令有效期。
|
||||
|
||||
回执包含 `request_id`、`project_id`、`receipt_id`、`status`(`accepted` / `running` / `completed` / `failed`)、`submitted_at`、可空 `completed_at` 和 `detail`。`completed` 必须有完成时间。网关回执查询路径为 `GET /api/v1/commands/requests/{request_id}`。
|
||||
|
||||
请求和回执时间必须包含时区。网关地址只来自服务端配置,Agent 或用户动作内容不能指定任意目的地址。产品 Bearer 仅在当前请求内传递,不进入任务、会话、提示词和日志。没有回执的网络故障保留为 `unknown`,不能当作失败后直接重新下发。
|
||||
|
||||
## 验证边界
|
||||
|
||||
适配器和工作进程测试使用隔离 HTTP transport 验证权限前提、幂等身份、丢响应恢复、项目隔离、接受与完成的区别。它们不等于生产设备网关或现场执行验收;接入具体网关后还需以其真实回执验证执行链。
|
||||
|
||||
可从后端仓库复现聚焦验证:
|
||||
|
||||
```bash
|
||||
conda run -n server python -m pytest tests/unit/test_operational_action_adapters.py tests/unit/test_operational_worker.py tests/unit/test_peripheral_task_sync.py tests/unit/test_operational_data.py tests/unit/test_import_agent_investigation.py -q
|
||||
OPERATIONAL_TASKS_STORAGE_TEST=1 conda run -n server python -m pytest tests/integration/test_operational_tasks_storage.py -q
|
||||
```
|
||||
|
||||
第二条需要当前配置的 PostgreSQL 可连接及测试 DDL 权限。测试在强制回滚事务中创建表结构和隔离项目数据,结束后不保留这些修改;不代替正式迁移。外部工单调用仍使用隔离 HTTP transport,不会操作实际工单系统。该测试覆盖持久化报告、反馈重新排队、旧会话拒绝、人工确认与丢响应回执核对,而非真实模型推理质量。
|
||||
@@ -0,0 +1,54 @@
|
||||
# 工况任务后端契约
|
||||
|
||||
`resources/sql/014_operational_tasks.sql` 提供项目隔离的统一工况任务存储。所有接口均位于 `/api/v1`,使用产品 Bearer 身份和 `X-Project-Id`;任务正文不会接受客户端提供的项目 ID。
|
||||
|
||||
## 三类任务
|
||||
|
||||
- 定时任务:当前产品继续使用前端 mock。`scheduled_flow` 及后端计划接口为已有保留能力,不接当前前端、不由当前 worker 调度或执行。
|
||||
- `external_work_order`:外围预约、维修等工单的只读任务投影。`source.kind + source.external_id` 在项目内唯一,进度以外部版本反馈为准。
|
||||
- `event_investigation`:投诉、爆管上报或人工发起的调查。Agent 查询受控数据,发布执行图、证据引用和报告;现场反馈会创建新的执行 generation,让 Agent 重新分析。
|
||||
|
||||
创建请求使用 `request_id` 幂等。任务执行使用独立的 `run_id + execution_request_id`:Agent 必须领取当前 run,并通过 run 级心跳维持 60 秒租约。正常结束必须显式 complete;缺少同代最终计划或报告会以 `ARTIFACT_INCOMPLETE` 失败。`POST /operational-tasks/leases/recover` 会处理过期执行,连续三代超时后停止自动重试。
|
||||
|
||||
## 主要接口
|
||||
|
||||
| 方法和路径 | 作用 |
|
||||
| --- | --- |
|
||||
| `POST /operational-tasks` | 幂等创建任务 |
|
||||
| `GET /operational-tasks` | 按类型、状态分页读取任务 |
|
||||
| `GET /operational-tasks/{task_id}` | 读取当前计划、报告、证据摘要、审批动作和当前用户权限 |
|
||||
| `GET /operational-tasks/{task_id}/agent-context` | 读取 Agent 任务上下文及当前身份允许的数据源 |
|
||||
| `POST /operational-tasks/{task_id}/runs/{run_id}/claim` | 使用当前执行请求号领取 queued run |
|
||||
| `POST /operational-tasks/{task_id}/runs/{run_id}/heartbeat` | 续期当前 Agent run,不递增任务业务 revision |
|
||||
| `POST /operational-tasks/{task_id}/runs/{run_id}/complete` | 校验同代最终产物并显式结束 run |
|
||||
| `POST /operational-tasks/{task_id}/runs/{run_id}/fail` | 使用稳定失败码显式终止 run |
|
||||
| `POST /operational-tasks/{task_id}/data-query` | 查询固定白名单数据源,同时保存有界原始快照并返回证据 ID |
|
||||
| `POST /operational-tasks/{task_id}/agent-artifacts` | 追加证据、计划快照和报告修订 |
|
||||
| `POST /operational-tasks/{task_id}/human-evidence` | 保存用户补充信息并重新排队分析 |
|
||||
| `POST /operational-tasks/{task_id}/external-feedback` | 幂等同步稳定外部事件流的单调版本 |
|
||||
| `POST /operational-tasks/{task_id}/actions` | 提议需要人工确认的动作 |
|
||||
| `POST .../actions/{action_id}/decision` | 有对应审批权限的用户确认或拒绝 |
|
||||
| `POST .../actions/{action_id}/execute` | 重新校验执行权限后通过类型化适配器提交 |
|
||||
| `POST .../actions/{action_id}/reconcile` | 仅使用固定执行请求号查询不确定或已提交动作的回执 |
|
||||
| `GET /operational-task-notifications` | 返回当前用户可见、按用户区分已读状态的通知 |
|
||||
|
||||
报告采用不可变修订,正文只保存当前结论、结论边界、建议、证据引用和待确认事项。详细的 Agent 工具轨迹和模型内部推理不进入报告。系统数据只能通过受控 `data-query` 形成证据,外围反馈和人工补充也有各自入口;Agent 不能自行声明这三类证据。合成演练证据始终标记为 `synthetic`。
|
||||
|
||||
## 人工确认与执行真实性
|
||||
|
||||
动作分为 `create_work_order` 和 `issue_command`,审批状态与执行状态分别保存。每次状态变化都会写入不可变动作版本;首次提交生成固定 `execution_request_id`,后续回执核对不改变它。
|
||||
|
||||
工单审批和执行可授予项目成员。设备命令审批、执行只授予具有系统管理身份且同时能进入项目的人员。普通查看者只能看任务及其 `permissions`,不能从前端角色名称推断或绕过服务端权限。
|
||||
|
||||
`approved` 只表示允许提交。`submitted` 需要外部接收回执,`succeeded` 还需要完成时间和完成回执。未配置适配器返回 `unsupported`,响应丢失返回 `unknown`;这两种情况都不会被记录为执行成功。外部适配器及 worker 的部署方式见 `docs/operational-task-workers.md`。
|
||||
|
||||
## 数据库部署
|
||||
|
||||
对每个项目 BizDB 应用:
|
||||
|
||||
```bash
|
||||
psql "$PROJECT_BIZ_DSN" -v ON_ERROR_STOP=1 -f resources/sql/014_operational_tasks.sql
|
||||
psql "$PROJECT_BIZ_DSN" -v ON_ERROR_STOP=1 -f resources/sql/015_workbench_events_and_task_runs.sql
|
||||
```
|
||||
|
||||
迁移不会创建或修改外围系统的业务表,也不授予 `PUBLIC` 权限。生产运行前需按部署身份显式授予新表权限。
|
||||
@@ -13,3 +13,9 @@
|
||||
- `/api/sample/v1/demo-data`
|
||||
|
||||
演示数据通过 `scripts/seed_peripheral_sample.py` 写入 `tjwater_v2` 的 `peripheral_sample` schema。当前固定窗口为 2026-09-17,所有记录标记为 `synthetic`,并使用 `DEMO-` 前缀。
|
||||
|
||||
## 水质投诉 Agent 演示
|
||||
|
||||
`POST /api/sample/v1/work-orders` 可创建 `work_order_type=water_quality_complaint` 的合成投诉事件,或 `work_order_type=burst_report` 的合成爆管上报。二者由外围同步器转换为统一事件调查任务,再由后端 worker 可靠交给 Agent;浏览器不再负责启动调查。事件和执行请求 ID 均由稳定业务身份确定,回执恢复或重试不会重复创建任务。
|
||||
|
||||
样板服务只保存工单,不直接调用 Agent。`scripts/sync_peripheral_work_orders.py` 使用受认证的后台身份同步外部版本并创建调查任务,`scripts/run_operational_worker.py` 再将持久化的待执行任务交给 Agent,因此浏览器关闭后流程仍可继续。
|
||||
|
||||
@@ -15,6 +15,6 @@ DDL 位于 `resources/sql/008_workbench_alert_events.sql`。任务通过 `POST /
|
||||
- 每个子任务包含一个或多个有序 `stages`,`sequence` 从 1 连续递增,阶段编号在子任务内唯一,最多只有一个 `current` 阶段。
|
||||
- 阶段名称不写死为固定枚举。演示数据使用派发、执行、复令,正式工单适配器可以按业务定义其他阶段。
|
||||
|
||||
演示数据可以先运行 `scripts/seed_peripheral_sample.py`,再运行 `scripts/seed_workbench_work_orders.py` 投影。工作台会显示一张主工单,三项外围核查分别作为子任务展示。
|
||||
预约任务演示数据通过 `scripts/seed_workbench_work_orders.py` 直接写入。工作台会显示三张原有 mock 工单,分别处于派发、执行和复令阶段;每张工单都保留自己的子任务与有序阶段。脚本可重复执行,并会先清理旧的外围合成工单投影。外围种子脚本仍会恢复被软删除的固定演示资源。
|
||||
|
||||
工作台运行账号对工单表只需 `SELECT`,同步账号使用独立受控写权限;对告警事件表需要 `SELECT, INSERT`,对告警状态表需要 `SELECT, INSERT, UPDATE`。
|
||||
|
||||
@@ -7,4 +7,4 @@
|
||||
- `workbench.alert_events` 保存任务显式推送的事件,`workbench.alerts` 保存当前状态。分析结果、测点值和缺测状态不会自动变成告警。
|
||||
- 每个可选分区独立读取并返回 `ready`、`not_configured` 或 `degraded`,一个分区失败不会污染其他分区。
|
||||
|
||||
演示工单由 `scripts/seed_peripheral_sample.py` 写入 `peripheral_sample`,再由 `scripts/seed_workbench_work_orders.py` 将三项外围核查组合成一张主工单和三个子任务,写入 `workbench.work_orders`。当前固定演示日期为 2026-09-17。
|
||||
预约任务演示数据由 `scripts/seed_workbench_work_orders.py` 直接写入 `workbench.work_orders`,与外围样板的 synthetic 测试数据相互独立。种子集 `workbench-demo-orders@1` 恢复前端原有的三张 mock 工单,分别覆盖派发、执行和复令状态;脚本会在同一事务中删除旧的“外围合成数据核查”投影并幂等写入新记录。当前固定演示日期为 2026-09-17。外围种子脚本仍可重复执行,并会恢复曾被软删除的固定演示工单和测试数据。
|
||||
|
||||
Reference in New Issue
Block a user