Compare commits

154 Commits

Author SHA1 Message Date
baiyanyun
91f68576dd 补齐测试配置与上传资源 2026-06-29 20:02:05 +08:00
baiyanyun
f84dc1d44e 吸收数字员工工作台入口重构收口 2026-06-29 19:53:34 +08:00
baiyanyun
d0b7223b53 吸收数字员工高风险缺口安全壳总收口 2026-06-15 23:33:11 +08:00
baiyanyun
3a41eb40d0 吸收数字员工日报物理事实发送记录预览 2026-06-15 17:20:42 +08:00
baiyanyun
fd06f3bce3 吸收数字员工服务技能记忆审计收口 2026-06-15 16:21:46 +08:00
baiyanyun
de33710423 吸收数字员工资产通知日报业务收口 2026-06-15 14:59:29 +08:00
baiyanyun
87b440e93f 吸收数字员工策略治理审批路由收口 2026-06-15 14:40:41 +08:00
baiyanyun
fd3b149172 吸收数字员工管理端治理策略版本预览 2026-06-15 14:01:10 +08:00
baiyanyun
ff7576f33d 吸收数字员工管理端服务恢复安全壳预览 2026-06-15 11:05:02 +08:00
baiyanyun
053a321ec1 吸收数字员工管理端技能安装权限预览 2026-06-15 10:51:48 +08:00
baiyanyun
6ad88cb6c4 吸收数字员工管理端记忆策略预览 2026-06-15 10:40:32 +08:00
baiyanyun
e57971488c 吸收数字员工管理端远程策略编辑安全壳预览 2026-06-15 10:24:41 +08:00
baiyanyun
642521035b 吸收数字员工管理端日报审批裁决回写预览 2026-06-15 09:54:56 +08:00
baiyanyun
b64f8e35c3 吸收数字员工管理端通知订阅策略预览 2026-06-15 09:45:52 +08:00
baiyanyun
a62b749ef2 吸收数字员工管理端诊断修复预览 2026-06-15 09:14:32 +08:00
baiyanyun
cc55fd819f 吸收数字员工跨设备调度派发策略冲突预览 2026-06-14 20:59:41 +08:00
baiyanyun
39371861f3 吸收数字员工跨设备审批策略冲突预览 2026-06-14 20:20:25 +08:00
baiyanyun
c01c699410 吸收数字员工管理端审批策略预览 2026-06-14 19:54:45 +08:00
baiyanyun
89eec8afc1 吸收数字员工管理端审批裁决策略预览 2026-06-14 18:49:08 +08:00
baiyanyun
8b651e4ae1 吸收数字员工管理端审计归档账单预览 2026-06-14 13:21:51 +08:00
baiyanyun
dd6c10f44b 吸收数字员工管理端产物跨设备聚合预览 2026-06-14 11:53:16 +08:00
baiyanyun
797ffeee82 吸收数字员工管理端记忆合并候选预览 2026-06-13 16:36:54 +08:00
baiyanyun
4fdc95da75 吸收数字员工管理端技能版本权限预览 2026-06-13 16:28:17 +08:00
baiyanyun
b9f49d2c0e 吸收数字员工管理端服务恢复运营预览 2026-06-13 16:18:01 +08:00
baiyanyun
7ea963dc9a 吸收数字员工跨设备路由干预冲突预览 2026-06-13 15:58:10 +08:00
baiyanyun
5ab6bc0fd0 吸收数字员工管理端步骤调度预览 2026-06-13 15:38:30 +08:00
baiyanyun
c9d8aa3b93 吸收数字员工管理端协作运营收口 2026-06-13 15:08:14 +08:00
baiyanyun
95838dba04 吸收数字员工管理端治理执行安全壳 2026-06-13 14:56:54 +08:00
baiyanyun
f08b0c39d9 吸收数字员工管理端策略调度预览 2026-06-13 14:42:16 +08:00
baiyanyun
de501c0252 吸收数字员工管理端记忆技能审计预览 2026-06-13 14:21:16 +08:00
baiyanyun
b6b0a9dcb3 吸收数字员工管理端治理命令预览 2026-06-13 14:07:40 +08:00
baiyanyun
d5025c0ebc 吸收数字员工管理端日报运营预览 2026-06-13 14:00:53 +08:00
baiyanyun
d86f82a4c5 吸收数字员工管理端审计诊断预览 2026-06-13 13:54:18 +08:00
baiyanyun
b0f0b95c2a 吸收数字员工管理端产物生命周期预览 2026-06-13 13:42:50 +08:00
baiyanyun
1daf58a94c 吸收数字员工管理端通知运营预览 2026-06-13 00:03:42 +08:00
baiyanyun
479e2e209d 吸收数字员工管理端路由治理预览 2026-06-12 23:47:20 +08:00
baiyanyun
ca746366f6 吸收数字员工管理端服务运营预览 2026-06-12 23:30:31 +08:00
baiyanyun
8ed4e0ce3b 吸收数字员工管理端审批治理预览 2026-06-12 23:03:58 +08:00
baiyanyun
bd7d7b4913 吸收数字员工发送账单审计外壳 2026-06-12 22:48:27 +08:00
baiyanyun
26fbf51fc8 吸收数字员工业务事实物化查询 2026-06-12 22:24:38 +08:00
baiyanyun
8a5d5d6474 吸收数字员工跨设备策略冲突视图 2026-06-12 21:48:33 +08:00
baiyanyun
b3f21adecb 吸收数字员工管理端低风险动作闭环 2026-06-12 21:17:51 +08:00
baiyanyun
be901ee112 吸收数字员工管理端运营台前端 2026-06-12 20:18:30 +08:00
baiyanyun
48ac9bdf19 吸收数字员工管理端运营台后端 2026-06-12 20:07:55 +08:00
baiyanyun
1a8e0fb51f 吸收数字员工运营预览台 2026-06-12 19:57:38 +08:00
baiyanyun
871bd715c4 合并账号菜单与数字员工吸收 2026-06-12 16:33:56 +08:00
baiyanyun
6323987a0d 收口账号菜单侧栏导航 2026-06-12 16:33:18 +08:00
baiyanyun
81d2797035 吸收数字员工审批冲突与产物业务闭环 2026-06-12 16:29:01 +08:00
baiyanyun
fffa0ebe50 吸收数字员工管理端业务查询模型 2026-06-12 11:21:43 +08:00
baiyanyun
9011b91dba 吸收数字员工诊断成本审计 2026-06-12 10:52:35 +08:00
baiyanyun
3a159e2f5a 吸收数字员工治理审计补齐 2026-06-12 10:33:46 +08:00
baiyanyun
e473b5c147 吸收数字员工通知权限引导 2026-06-12 10:16:02 +08:00
baiyanyun
e57c94c19d 吸收数字员工审批跨端冲突可视化 2026-06-11 21:53:08 +08:00
baiyanyun
84d34236e8 吸收数字员工风险策略本地管理 2026-06-11 21:14:29 +08:00
baiyanyun
245a210b25 吸收数字员工系统桌面通知 2026-06-11 20:53:46 +08:00
baiyanyun
8278fdc81c 吸收数字员工通知跨端同步 2026-06-11 20:27:10 +08:00
baiyanyun
4c862ef4b7 吸收数字员工入口路由干预回写 2026-06-11 18:31:41 +08:00
baiyanyun
d292c34579 吸收数字员工入口路由策略下发 2026-06-11 17:44:16 +08:00
baiyanyun
43418d0453 吸收数字员工多步审批跨端回写 2026-06-11 10:44:12 +08:00
baiyanyun
e9c76c59ca 吸收数字员工风险策略远端下发 2026-06-11 09:51:14 +08:00
baiyanyun
8ab3d73fb1 吸收数字员工技能远端策略下发 2026-06-11 09:04:08 +08:00
baiyanyun
751ce45619 吸收数字员工PlanStep管理端强租约 2026-06-10 23:59:54 +08:00
baiyanyun
c400367d22 吸收数字员工PlanStep远端策略拉取 2026-06-10 23:15:51 +08:00
baiyanyun
6f2ccdf783 吸收数字员工PlanStep本地软租约 2026-06-10 22:29:52 +08:00
baiyanyun
909f11dae5 吸收数字员工PlanStep跨任务编排执行 2026-06-10 21:00:59 +08:00
baiyanyun
a6e0246418 吸收数字员工PlanStep阻塞处理入口 2026-06-10 20:37:11 +08:00
baiyanyun
d679639175 吸收数字员工PlanStep依赖图派发策略 2026-06-10 18:41:33 +08:00
baiyanyun
3ac350eb27 吸收数字员工审批历史订阅策略 2026-06-10 18:22:53 +08:00
baiyanyun
5b78189c9c 吸收数字员工通知回复任务输入 2026-06-10 17:49:14 +08:00
baiyanyun
1271e4967b 吸收数字员工管理端通知视图 2026-06-10 17:05:57 +08:00
baiyanyun
5b2f14123e 吸收数字员工通知订阅策略 2026-06-10 15:56:34 +08:00
baiyanyun
13e48df63d 吸收数字员工管理端事件视图 2026-06-10 15:16:23 +08:00
baiyanyun
4f489f402a 吸收数字员工诊断审计观测 2026-06-10 14:49:49 +08:00
baiyanyun
914ed81f19 吸收数字员工入口路由观测 2026-06-10 11:43:48 +08:00
baiyanyun
d12db03216 吸收数字员工步骤自动调度 2026-06-10 11:23:22 +08:00
baiyanyun
047651e404 吸收数字员工审批历史联动 2026-06-10 10:46:28 +08:00
baiyanyun
ed2f19d257 吸收数字员工产物生命周期治理 2026-06-10 10:23:47 +08:00
baiyanyun
362f397e37 吸收数字员工产物访问治理 2026-06-10 10:00:34 +08:00
baiyanyun
cc36d8dcaf 吸收数字员工技能调用审计 2026-06-10 09:24:02 +08:00
baiyanyun
91540dec0c 吸收数字员工安全服务重启入口 2026-06-09 23:10:01 +08:00
baiyanyun
0d2008b35c 吸收数字员工日报产物闭环 2026-06-09 22:16:03 +08:00
baiyanyun
ba806f779e 吸收数字员工业务查询视图 2026-06-09 21:25:03 +08:00
baiyanyun
d1ae9421c4 吸收数字员工诊断审计投影 2026-06-09 19:24:28 +08:00
baiyanyun
755a8f041d 吸收数字员工入口路由动作映射 2026-06-09 19:05:15 +08:00
baiyanyun
23c3577ef4 吸收数字员工通知投影 2026-06-09 18:33:01 +08:00
baiyanyun
dc8cd09535 吸收数字员工审批时间线 2026-06-09 17:33:00 +08:00
baiyanyun
5160a73254 吸收数字员工产物交付状态 2026-06-09 16:47:14 +08:00
baiyanyun
4d8b8e4ad5 吸收数字员工技能策略开关 2026-06-09 16:28:18 +08:00
baiyanyun
9ad7881378 吸收数字员工治理动作入口 2026-06-09 15:54:49 +08:00
baiyanyun
702ea05d4d 吸收数字员工入口路由决策 2026-06-09 15:32:38 +08:00
baiyanyun
fd130f1f60 吸收数字员工步骤依赖调度 2026-06-09 14:18:07 +08:00
baiyanyun
ddd39396f7 吸收数字员工长期记忆闭环 2026-06-09 13:49:11 +08:00
baiyanyun
ef8252e2dd 吸收数字员工步骤治理闭环 2026-06-09 11:31:29 +08:00
baiyanyun
5ebc0a640b 吸收数字员工周期调度闭环 2026-06-09 11:06:25 +08:00
baiyanyun
f35d945d9c 吸收数字员工计划步骤依赖图 2026-06-09 10:21:12 +08:00
baiyanyun
e4715bbabb 梳理数字员工剩余吸收能力 2026-06-09 09:48:12 +08:00
baiyanyun
726369f5a9 沉淀数字员工角色偏好状态 2026-06-09 09:34:39 +08:00
baiyanyun
2f9af0b173 修复数字员工同步业务失败识别 2026-06-09 09:24:53 +08:00
baiyanyun
eb8ea9974f docs(client): refresh digital fallback status 2026-06-09 08:50:46 +08:00
baiyanyun
16eb8c691e chore(client): remove digital demo scenario residue 2026-06-09 08:49:45 +08:00
baiyanyun
48e5830b9e feat(client): remove demo skill fallback 2026-06-09 08:48:11 +08:00
baiyanyun
451cda7aa6 feat(client): materialize restarted plans 2026-06-08 18:58:39 +08:00
baiyanyun
913c6da8f3 feat(client): project local GUI and file tools 2026-06-08 18:44:45 +08:00
baiyanyun
38e5bdbc04 feat(client): project acp skills to digital catalog 2026-06-08 18:31:03 +08:00
baiyanyun
e4652ace1c feat(client): remove demo mission fallback 2026-06-08 18:19:36 +08:00
baiyanyun
820b1b6f13 feat(client): control task agents from digital deck 2026-06-08 18:06:11 +08:00
baiyanyun
76a82f6203 chore(client): add digital favicon 2026-06-08 17:48:38 +08:00
baiyanyun
3bab6af63c feat(client): define digital sync contract 2026-06-08 17:10:38 +08:00
baiyanyun
ff25c2fef7 feat(client): project plan and task agents 2026-06-08 11:57:47 +08:00
baiyanyun
710bf72353 feat(client): project managed services to digital employee 2026-06-08 11:40:03 +08:00
baiyanyun
8dc9ce73b9 chore(client): add digital employee check 2026-06-08 10:36:19 +08:00
baiyanyun
700f6a40b8 feat(client): show digital artifact sources 2026-06-08 10:31:36 +08:00
baiyanyun
f4af77cb2b feat(client): reply digital approvals to acp 2026-06-08 10:23:06 +08:00
baiyanyun
ee51fcdbeb feat(client): enrich digital skill catalog 2026-06-07 20:04:38 +08:00
baiyanyun
0f0bffe695 feat(client): load local digital plan templates 2026-06-07 19:53:24 +08:00
baiyanyun
e37024bdbe feat(client): persist digital governance events 2026-06-07 18:59:59 +08:00
baiyanyun
5e76828afe fix(client): ensure digital employee schema 2026-06-07 18:39:19 +08:00
baiyanyun
6deea42de1 docs(client): note digital employee database location 2026-06-07 18:24:32 +08:00
baiyanyun
09d65701a3 fix(client): migrate digital storage keys 2026-06-07 18:21:24 +08:00
baiyanyun
fa5522e32e fix(client): localize digital fallback missions 2026-06-07 18:09:39 +08:00
baiyanyun
757858a9aa fix(client): limit workday decisions to approvals 2026-06-07 17:56:37 +08:00
baiyanyun
aee8af0877 fix(client): sync digital approval decisions 2026-06-07 17:52:20 +08:00
baiyanyun
b07f133ed6 feat(client): project digital employee runtime into UI 2026-06-07 17:46:07 +08:00
baiyanyun
8ff1c57b83 fix(backend): validate digital sync client key header 2026-06-07 11:33:59 +08:00
baiyanyun
bf25bc5a47 feat: filter digital sync links by device 2026-06-07 11:24:01 +08:00
baiyanyun
029ca6e7f6 feat: mask digital sync client keys 2026-06-07 11:14:16 +08:00
baiyanyun
7eb1498412 feat: summarize digital sync records 2026-06-07 02:25:37 +08:00
baiyanyun
2bf9192fc5 feat(client): export digital sync diagnostics 2026-06-07 02:22:37 +08:00
baiyanyun
df2ab51e70 fix(client): omit client key from sync record links 2026-06-07 02:20:30 +08:00
baiyanyun
f8564b631d test(client): cover digital employee sync retries 2026-06-07 02:16:04 +08:00
baiyanyun
bdcb5c6c14 feat(client): show digital sync credential gaps 2026-06-07 02:05:12 +08:00
baiyanyun
6b033da313 feat: auto open digital sync records 2026-06-07 02:02:21 +08:00
baiyanyun
38ece7e3d3 feat(client): summarize digital sync failures 2026-06-07 01:51:15 +08:00
baiyanyun
cef3fe4a43 feat(client): track digital sync retry history 2026-06-07 01:42:39 +08:00
baiyanyun
646901b5fa feat: deep link digital sync failures 2026-06-07 01:35:08 +08:00
baiyanyun
508406ff10 feat(client): add digital sync retry details 2026-06-07 01:17:07 +08:00
baiyanyun
0672e82fc7 feat(client): show digital sync failure details 2026-06-07 01:05:49 +08:00
baiyanyun
ac1a6fd0e6 feat(client): show digital employee sync errors 2026-06-07 00:53:28 +08:00
baiyanyun
e2a91d88e8 feat: add digital employee sync records page 2026-06-07 00:37:15 +08:00
baiyanyun
dc14418963 feat(backend): expose digital employee sync records 2026-06-05 18:40:44 +08:00
baiyanyun
17475bf0d7 feat(client): add manual digital employee sync 2026-06-05 18:24:04 +08:00
baiyanyun
b7aecad092 feat(client): back off digital employee sync retries 2026-06-05 18:19:32 +08:00
baiyanyun
35d2cb5ea5 feat(client): show digital employee sync status 2026-06-05 17:25:46 +08:00
baiyanyun
ab1d022905 feat: add digital employee management sync endpoint 2026-06-05 17:21:48 +08:00
baiyanyun
b9132c421d feat(client): sync digital employee outbox 2026-06-05 17:11:10 +08:00
baiyanyun
f8048724c4 feat(client): mirror agent events to digital employee 2026-06-05 17:00:44 +08:00
baiyanyun
512c7ab095 feat(client): record computer tasks for digital employee 2026-06-05 16:52:19 +08:00
baiyanyun
2373e0b5f3 feat(client): add digital employee sync schema 2026-06-05 16:43:23 +08:00
baiyanyun
40151c3902 feat(client): persist digital employee snapshots 2026-06-05 16:27:15 +08:00
baiyanyun
15a3285ed5 feat(client): bridge digital employee status 2026-06-05 15:59:48 +08:00
baiyanyun
31b4b5a281 feat(client): add qimingclaw digital adapter 2026-06-05 15:45:55 +08:00
baiyanyun
2eb9a99da3 feat(client): vendor digital employee source 2026-06-05 15:02:42 +08:00
baiyanyun
c6fdff20e8 fix(client): show embedded digital employee directly 2026-06-04 23:08:41 +08:00
baiyanyun
2c3f2c8c54 feat(client): add offline digital employee overview 2026-06-04 19:42:37 +08:00
229 changed files with 83667 additions and 1401 deletions

View File

@@ -0,0 +1,9 @@
---
name:
description:
license:
---
# Processing Guide
## Overview

View File

@@ -0,0 +1,4 @@
{
"description": "Skill template",
"config": {}
}

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env python3
# Helper script for this skill
if __name__ == "__main__":
print("Process script placeholder")

View File

@@ -0,0 +1,3 @@
# API Documentation
Add interface descriptions related to skills here.

View File

@@ -104,6 +104,10 @@
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-bill-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<profiles>
<profile>

View File

@@ -0,0 +1,105 @@
package com.xspaceagi.agent.core.adapter.application;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchPageDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPageDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRequestDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordRequestDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordResultDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBillingEstimateDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordPageDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto;
import java.util.Date;
import java.util.List;
public interface DigitalEmployeeSyncApplicationService {
DigitalEmployeeSyncResponseDto syncOutbox(DigitalEmployeeSyncRequestDto request);
DigitalEmployeeSyncRecordPageDto queryRecords(
String clientKey,
String deviceId,
String entityType,
String entityId,
String outboxId,
Long pageNo,
Long pageSize
);
DigitalEmployeeBusinessViewPageDto queryBusinessViews(
String entityType,
String clientKey,
String deviceId,
String entityId,
String status,
String syncStatus,
Date updatedFrom,
Date updatedTo,
Long pageNo,
Long pageSize
);
DigitalEmployeePlanBusinessViewDetailDto queryPlanBusinessViewDetail(
String planId,
String clientKey,
String deviceId
);
DigitalEmployeeAdminWorkbenchPageDto queryAdminWorkbench(
String view,
String clientKey,
String deviceId,
String entityType,
String entityId,
String status,
String kind,
String category,
String level,
String priority,
Date updatedFrom,
Date updatedTo,
Long pageNo,
Long pageSize
);
DigitalEmployeePolicySnapshotPageDto queryAdminPolicySnapshots(
String clientKey,
String deviceId,
String status,
String policyKind,
Date updatedFrom,
Date updatedTo,
Long pageNo,
Long pageSize
);
DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request);
DigitalEmployeeAdminActionPageDto queryPendingAdminActions(
String clientKey,
String deviceId,
Long pageNo,
Long pageSize
);
DigitalEmployeeAdminActionResultDto markAdminActionResult(
Long actionId,
String status,
List<String> reasonCodes
);
DigitalEmployeeAdminRecordResultDto recordDailyReportSendResult(DigitalEmployeeAdminRecordRequestDto request);
DigitalEmployeeBillingEstimateDto queryBillingEstimates(
String clientKey,
String deviceId,
Date updatedFrom,
Date updatedTo
);
DigitalEmployeeAdminRecordResultDto recordAuditArchive(DigitalEmployeeAdminRecordRequestDto request);
}

View File

@@ -0,0 +1,33 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Data
@Builder
public class DigitalEmployeeAdminActionDto {
private Long id;
@JsonProperty("client_key_masked")
private String clientKeyMasked;
@JsonProperty("device_id")
private String deviceId;
@JsonProperty("entity_type")
private String entityType;
@JsonProperty("entity_id")
private String entityId;
private String action;
private String status;
@JsonProperty("reason_codes")
private List<String> reasonCodes;
@JsonProperty("request_payload")
private Map<String, Object> requestPayload;
@JsonProperty("created_at")
private Date createdAt;
@JsonProperty("processed_at")
private Date processedAt;
}

View File

@@ -0,0 +1,18 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class DigitalEmployeeAdminActionPageDto {
private Long total;
@JsonProperty("page_no")
private Long pageNo;
@JsonProperty("page_size")
private Long pageSize;
private List<DigitalEmployeeAdminActionDto> records;
}

View File

@@ -0,0 +1,23 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.Map;
@Data
public class DigitalEmployeeAdminActionRequestDto {
@JsonProperty("sync_record_id")
private Long syncRecordId;
@JsonProperty("client_key")
private String clientKey;
@JsonProperty("device_id")
private String deviceId;
@JsonProperty("entity_type")
private String entityType;
@JsonProperty("entity_id")
private String entityId;
private String action;
@JsonProperty("request_payload")
private Map<String, Object> requestPayload;
}

View File

@@ -0,0 +1,17 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class DigitalEmployeeAdminActionResultDto {
private Boolean ok;
private String status;
@JsonProperty("reason_codes")
private List<String> reasonCodes;
private DigitalEmployeeAdminActionDto action;
}

View File

@@ -0,0 +1,33 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.Map;
@Data
public class DigitalEmployeeAdminRecordRequestDto {
@JsonAlias("sync_record_id")
private Long syncRecordId;
@JsonAlias("client_key")
private String clientKey;
@JsonAlias("device_id")
private String deviceId;
@JsonAlias("entity_type")
private String entityType;
@JsonAlias("entity_id")
private String entityId;
private String status;
private String endpoint;
@JsonAlias("remote_id")
private String remoteId;
@JsonAlias("archive_location")
private String archiveLocation;
@JsonAlias("archive_summary")
private Map<String, Object> archiveSummary;
private String reason;
@JsonProperty("request_payload")
@JsonAlias("requestPayload")
private Map<String, Object> requestPayload;
}

View File

@@ -0,0 +1,17 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class DigitalEmployeeAdminRecordResultDto {
private Boolean ok;
private String status;
@JsonProperty("reason_codes")
private List<String> reasonCodes;
private DigitalEmployeeAdminActionDto record;
}

View File

@@ -0,0 +1,18 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class DigitalEmployeeAdminWorkbenchPageDto {
private Long total;
@JsonProperty("page_no")
private Long pageNo;
@JsonProperty("page_size")
private Long pageSize;
private List<DigitalEmployeeAdminWorkbenchRowDto> records;
}

View File

@@ -0,0 +1,364 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Data
@Builder
public class DigitalEmployeeAdminWorkbenchRowDto {
private String view;
@JsonProperty("sync_record_id")
private Long syncRecordId;
@JsonProperty("sync_record_url")
private String syncRecordUrl;
@JsonProperty("entity_type")
private String entityType;
@JsonProperty("entity_id")
private String entityId;
private String operation;
private String title;
private String status;
private String kind;
private String category;
private String level;
private String priority;
private String summary;
@JsonProperty("plan_id")
private String planId;
@JsonProperty("task_id")
private String taskId;
@JsonProperty("run_id")
private String runId;
@JsonProperty("step_id")
private String stepId;
@JsonProperty("dependency_status")
private String dependencyStatus;
@JsonProperty("lease_state")
private String leaseState;
@JsonProperty("lease_source")
private String leaseSource;
@JsonProperty("dispatch_status")
private String dispatchStatus;
@JsonProperty("dispatch_policy_key")
private String dispatchPolicyKey;
@JsonProperty("orchestration_id")
private String orchestrationId;
@JsonProperty("trigger_source")
private String triggerSource;
@JsonProperty("approval_id")
private String approvalId;
@JsonProperty("approval_policy_id")
private String approvalPolicyId;
@JsonProperty("conflict_policy_id")
private String conflictPolicyId;
@JsonProperty("decision_guard_status")
private String decisionGuardStatus;
@JsonProperty("batch_accept_remote_disabled")
private Boolean batchAcceptRemoteDisabled;
@JsonProperty("auto_decision_disabled")
private Boolean autoDecisionDisabled;
@JsonProperty("policy_reason")
private String policyReason;
@JsonProperty("governance_policy_id")
private String governancePolicyId;
@JsonProperty("policy_guard_status")
private String policyGuardStatus;
@JsonProperty("hard_interrupt_disabled")
private Boolean hardInterruptDisabled;
@JsonProperty("artifact_id")
private String artifactId;
@JsonProperty("artifact_graph_id")
private String artifactGraphId;
@JsonProperty("artifact_group_id")
private String artifactGroupId;
@JsonProperty("group_key")
private String groupKey;
@JsonProperty("related_artifact_ids")
private List<String> relatedArtifactIds;
@JsonProperty("artifact_count")
private Long artifactCount;
@JsonProperty("device_count")
private Long deviceCount;
@JsonProperty("version_count")
private Long versionCount;
@JsonProperty("latest_artifact_id")
private String latestArtifactId;
@JsonProperty("version_key")
private String versionKey;
@JsonProperty("cleanup_request_id")
private String cleanupRequestId;
@JsonProperty("cleanup_eligible_count")
private Long cleanupEligibleCount;
@JsonProperty("batch_cleanup_disabled")
private Boolean batchCleanupDisabled;
@JsonProperty("retention_conflict_status")
private String retentionConflictStatus;
@JsonProperty("cleanup_conflict_status")
private String cleanupConflictStatus;
@JsonProperty("remote_delete_disabled")
private Boolean remoteDeleteDisabled;
@JsonProperty("notification_id")
private String notificationId;
@JsonProperty("collaboration_thread_id")
private String collaborationThreadId;
@JsonProperty("subscription_policy_id")
private String subscriptionPolicyId;
@JsonProperty("subscription_conflict_status")
private String subscriptionConflictStatus;
@JsonProperty("notification_policy_id")
private String notificationPolicyId;
@JsonProperty("subscription_status")
private String subscriptionStatus;
@JsonProperty("desktop_enabled")
private Boolean desktopEnabled;
@JsonProperty("minimum_level")
private String minimumLevel;
private String channel;
@JsonProperty("delivery_status")
private String deliveryStatus;
@JsonProperty("delivery_attempt_id")
private String deliveryAttemptId;
@JsonProperty("send_request_id")
private String sendRequestId;
@JsonProperty("real_push_disabled")
private Boolean realPushDisabled;
@JsonProperty("conflict_status")
private String conflictStatus;
@JsonProperty("report_id")
private String reportId;
@JsonProperty("daily_report_fact_id")
private String dailyReportFactId;
@JsonProperty("daily_report_table")
private String dailyReportTable;
@JsonProperty("daily_report_materialized")
private Boolean dailyReportMaterialized;
@JsonProperty("daily_report_physical_fact_status")
private String dailyReportPhysicalFactStatus;
@JsonProperty("send_record_id")
private String sendRecordId;
@JsonProperty("send_channel")
private String sendChannel;
@JsonProperty("send_request_status")
private String sendRequestStatus;
@JsonProperty("send_record_status")
private String sendRecordStatus;
@JsonProperty("formal_daily_report_page_disabled")
private Boolean formalDailyReportPageDisabled;
@JsonProperty("external_send_disabled")
private Boolean externalSendDisabled;
@JsonProperty("third_party_channel_disabled")
private Boolean thirdPartyChannelDisabled;
@JsonProperty("approval_decision_status")
private String approvalDecisionStatus;
@JsonProperty("advanced_query_supported")
private Boolean advancedQuerySupported;
@JsonProperty("aggregation_scope")
private String aggregationScope;
@JsonProperty("materialized_source")
private String materializedSource;
@JsonProperty("daily_report_approval_id")
private String dailyReportApprovalId;
@JsonProperty("daily_report_approval_action")
private String dailyReportApprovalAction;
@JsonProperty("daily_report_approval_actor")
private String dailyReportApprovalActor;
@JsonProperty("daily_report_approval_reason")
private String dailyReportApprovalReason;
@JsonProperty("daily_report_approval_recorded_at")
private String dailyReportApprovalRecordedAt;
@JsonProperty("route_decision_id")
private String routeDecisionId;
@JsonProperty("service_id")
private String serviceId;
@JsonProperty("service_lease_id")
private String serviceLeaseId;
@JsonProperty("lease_holder")
private String leaseHolder;
@JsonProperty("lease_expires_at")
private String leaseExpiresAt;
@JsonProperty("restart_record_id")
private String restartRecordId;
@JsonProperty("restart_count")
private Long restartCount;
@JsonProperty("last_restart_at")
private String lastRestartAt;
@JsonProperty("recovery_policy_id")
private String recoveryPolicyId;
@JsonProperty("auto_recovery_status")
private String autoRecoveryStatus;
@JsonProperty("service_start_disabled")
private Boolean serviceStartDisabled;
@JsonProperty("service_stop_disabled")
private Boolean serviceStopDisabled;
@JsonProperty("lease_takeover_disabled")
private Boolean leaseTakeoverDisabled;
@JsonProperty("auto_recovery_disabled")
private Boolean autoRecoveryDisabled;
@JsonProperty("recovery_action_request_id")
private String recoveryActionRequestId;
@JsonProperty("start_request_id")
private String startRequestId;
@JsonProperty("stop_request_id")
private String stopRequestId;
@JsonProperty("lease_takeover_request_id")
private String leaseTakeoverRequestId;
@JsonProperty("auto_recovery_request_id")
private String autoRecoveryRequestId;
@JsonProperty("start_service_disabled")
private Boolean startServiceDisabled;
@JsonProperty("stop_service_disabled")
private Boolean stopServiceDisabled;
@JsonProperty("memory_id")
private String memoryId;
@JsonProperty("memory_key")
private String memoryKey;
@JsonProperty("governance_status")
private String governanceStatus;
@JsonProperty("merge_candidate_count")
private Long mergeCandidateCount;
@JsonProperty("duplicate_memory_ids")
private List<String> duplicateMemoryIds;
@JsonProperty("latest_memory_id")
private String latestMemoryId;
@JsonProperty("last_governance_action")
private String lastGovernanceAction;
@JsonProperty("last_reviewed_at")
private String lastReviewedAt;
@JsonProperty("memory_policy_id")
private String memoryPolicyId;
@JsonProperty("memory_policy_status")
private String memoryPolicyStatus;
@JsonProperty("merge_policy_id")
private String mergePolicyId;
@JsonProperty("merge_action_disabled")
private Boolean mergeActionDisabled;
@JsonProperty("remote_memory_policy_disabled")
private Boolean remoteMemoryPolicyDisabled;
@JsonProperty("skill_id")
private String skillId;
@JsonProperty("skill_version")
private String skillVersion;
@JsonProperty("install_status")
private String installStatus;
@JsonProperty("permission_scope")
private String permissionScope;
@JsonProperty("policy_source")
private String policySource;
@JsonProperty("policy_status")
private String policyStatus;
@JsonProperty("skill_install_request_id")
private String skillInstallRequestId;
@JsonProperty("skill_upgrade_request_id")
private String skillUpgradeRequestId;
@JsonProperty("permission_policy_id")
private String permissionPolicyId;
@JsonProperty("permission_edit_request_id")
private String permissionEditRequestId;
@JsonProperty("install_action_disabled")
private Boolean installActionDisabled;
@JsonProperty("upgrade_action_disabled")
private Boolean upgradeActionDisabled;
@JsonProperty("permission_edit_disabled")
private Boolean permissionEditDisabled;
@JsonProperty("tool_name")
private String toolName;
@JsonProperty("server_id")
private String serverId;
@JsonProperty("audit_archive_id")
private String auditArchiveId;
@JsonProperty("archive_status")
private String archiveStatus;
@JsonProperty("archive_location")
private String archiveLocation;
@JsonProperty("billing_estimate_id")
private String billingEstimateId;
@JsonProperty("billing_source")
private String billingSource;
private Boolean estimated;
@JsonProperty("diagnostic_id")
private String diagnosticId;
@JsonProperty("diagnostic_kind")
private String diagnosticKind;
@JsonProperty("issue_code")
private String issueCode;
@JsonProperty("repair_action")
private String repairAction;
@JsonProperty("repair_status")
private String repairStatus;
@JsonProperty("repair_reason")
private String repairReason;
@JsonProperty("safe_repair_disabled")
private Boolean safeRepairDisabled;
@JsonProperty("billing_actual_disabled")
private Boolean billingActualDisabled;
@JsonProperty("sandbox_archive_file_disabled")
private Boolean sandboxArchiveFileDisabled;
@JsonProperty("auto_repair_execution_disabled")
private Boolean autoRepairExecutionDisabled;
@JsonProperty("repair_approval_required")
private Boolean repairApprovalRequired;
@JsonProperty("recommended_action")
private String recommendedAction;
@JsonProperty("policy_kind")
private String policyKind;
@JsonProperty("policy_key")
private String policyKey;
@JsonProperty("policy_edit_request_id")
private String policyEditRequestId;
@JsonProperty("policy_edit_status")
private String policyEditStatus;
@JsonProperty("policy_edit_reason")
private String policyEditReason;
@JsonProperty("merge_request_id")
private String mergeRequestId;
@JsonProperty("memory_merge_request_id")
private String memoryMergeRequestId;
@JsonProperty("remote_memory_policy_request_id")
private String remoteMemoryPolicyRequestId;
@JsonProperty("merge_recommendation")
private String mergeRecommendation;
@JsonProperty("remote_policy_edit_disabled")
private Boolean remotePolicyEditDisabled;
@JsonProperty("auto_merge_disabled")
private Boolean autoMergeDisabled;
@JsonProperty("approval_policy_edit_disabled")
private Boolean approvalPolicyEditDisabled;
@JsonProperty("route_policy_edit_disabled")
private Boolean routePolicyEditDisabled;
@JsonProperty("route_recovery_disabled")
private Boolean routeRecoveryDisabled;
@JsonProperty("high_risk_execution_disabled")
private Boolean highRiskExecutionDisabled;
@JsonProperty("schedule_id")
private String scheduleId;
@JsonProperty("job_id")
private String jobId;
@JsonProperty("revision_id")
private String revisionId;
@JsonProperty("revision_fact_id")
private String revisionFactId;
@JsonProperty("revision_status")
private String revisionStatus;
@JsonProperty("revision_materialized")
private Boolean revisionMaterialized;
@JsonProperty("base_revision_id")
private String baseRevisionId;
@JsonProperty("changed_fields")
private List<String> changedFields;
@JsonProperty("occurred_at")
private String occurredAt;
@JsonProperty("synced_at")
private Date syncedAt;
@JsonProperty("client_key_masked")
private String clientKeyMasked;
@JsonProperty("device_id")
private String deviceId;
@JsonProperty("business_view")
private Map<String, Object> businessView;
private Map<String, Object> payload;
}

View File

@@ -0,0 +1,47 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Data
@Builder
public class DigitalEmployeeBillingEstimateDto {
@JsonProperty("total_tokens")
private Long totalTokens;
@JsonProperty("input_tokens")
private Long inputTokens;
@JsonProperty("output_tokens")
private Long outputTokens;
@JsonProperty("estimated_cost_usd")
private BigDecimal estimatedCostUsd;
@JsonProperty("request_count")
private Long requestCount;
private Boolean estimated;
@JsonProperty("by_model")
private Map<String, ModelEstimate> byModel;
@JsonProperty("updated_at")
private Date updatedAt;
@JsonProperty("source_records")
private List<Long> sourceRecords;
@Data
@Builder
public static class ModelEstimate {
@JsonProperty("total_tokens")
private Long totalTokens;
@JsonProperty("input_tokens")
private Long inputTokens;
@JsonProperty("output_tokens")
private Long outputTokens;
@JsonProperty("estimated_cost_usd")
private BigDecimal estimatedCostUsd;
@JsonProperty("request_count")
private Long requestCount;
}
}

View File

@@ -0,0 +1,18 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class DigitalEmployeeBusinessViewPageDto {
private Long total;
@JsonProperty("page_no")
private Long pageNo;
@JsonProperty("page_size")
private Long pageSize;
private List<DigitalEmployeeBusinessViewRowDto> records;
}

View File

@@ -0,0 +1,68 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.Date;
import java.util.Map;
@Data
@Builder
public class DigitalEmployeeBusinessViewRowDto {
@JsonProperty("sync_record_id")
private Long syncRecordId;
@JsonProperty("sync_record_url")
private String syncRecordUrl;
@JsonProperty("entity_type")
private String entityType;
@JsonProperty("entity_id")
private String entityId;
private String operation;
private String title;
private String status;
private String summary;
@JsonProperty("plan_id")
private String planId;
@JsonProperty("task_id")
private String taskId;
@JsonProperty("run_id")
private String runId;
private String kind;
@JsonProperty("occurred_at")
private String occurredAt;
@JsonProperty("started_at")
private String startedAt;
@JsonProperty("finished_at")
private String finishedAt;
@JsonProperty("duration_ms")
private Long durationMs;
@JsonProperty("sync_status")
private String syncStatus;
@JsonProperty("client_key_masked")
private String clientKeyMasked;
@JsonProperty("device_id")
private String deviceId;
@JsonProperty("synced_at")
private Date syncedAt;
private Date modified;
private Date created;
@JsonProperty("business_view")
private Map<String, Object> businessView;
private Map<String, Object> payload;
@JsonProperty("task_count")
private Integer taskCount;
@JsonProperty("run_count")
private Integer runCount;
@JsonProperty("latest_event_at")
private String latestEventAt;
@JsonProperty("last_event_message")
private String lastEventMessage;
@JsonProperty("advanced_query_supported")
private Boolean advancedQuerySupported;
@JsonProperty("aggregation_scope")
private String aggregationScope;
@JsonProperty("materialized_source")
private String materializedSource;
private Boolean materialized;
}

View File

@@ -0,0 +1,17 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class DigitalEmployeePlanBusinessViewDetailDto {
private DigitalEmployeeBusinessViewRowDto plan;
private List<DigitalEmployeeBusinessViewRowDto> tasks;
private List<DigitalEmployeeBusinessViewRowDto> runs;
private List<DigitalEmployeeBusinessViewRowDto> events;
private List<DigitalEmployeeBusinessViewRowDto> artifacts;
private List<DigitalEmployeeBusinessViewRowDto> approvals;
}

View File

@@ -0,0 +1,18 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class DigitalEmployeePolicySnapshotPageDto {
private Long total;
@JsonProperty("page_no")
private Long pageNo;
@JsonProperty("page_size")
private Long pageSize;
private List<DigitalEmployeePolicySnapshotRowDto> records;
}

View File

@@ -0,0 +1,72 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Data
@Builder
public class DigitalEmployeePolicySnapshotRowDto {
@JsonProperty("policy_kind")
private String policyKind;
@JsonProperty("policy_key")
private String policyKey;
private String status;
private String summary;
@JsonProperty("client_count")
private Integer clientCount;
@JsonProperty("device_count")
private Integer deviceCount;
@JsonProperty("conflict_keys")
private List<String> conflictKeys;
@JsonProperty("latest_synced_at")
private Date latestSyncedAt;
@JsonProperty("recommended_resolution")
private String recommendedResolution;
@JsonProperty("route_decision_count")
private Integer routeDecisionCount;
@JsonProperty("blocked_count")
private Integer blockedCount;
@JsonProperty("approval_required_count")
private Integer approvalRequiredCount;
@JsonProperty("missing_context_count")
private Integer missingContextCount;
@JsonProperty("not_mapped_count")
private Integer notMappedCount;
@JsonProperty("intervention_conflict_status")
private String interventionConflictStatus;
@JsonProperty("approval_policy_id")
private String approvalPolicyId;
@JsonProperty("conflict_policy_id")
private String conflictPolicyId;
@JsonProperty("decision_guard_status")
private String decisionGuardStatus;
@JsonProperty("batch_accept_remote_disabled")
private Boolean batchAcceptRemoteDisabled;
@JsonProperty("auto_decision_disabled")
private Boolean autoDecisionDisabled;
@JsonProperty("policy_reason")
private String policyReason;
@JsonProperty("dispatch_policy_key")
private String dispatchPolicyKey;
@JsonProperty("max_per_sweep")
private Long maxPerSweep;
@JsonProperty("auto_dispatch_ready_steps")
private Boolean autoDispatchReadySteps;
@JsonProperty("catch_up_on_startup")
private Boolean catchUpOnStartup;
@JsonProperty("max_run_history")
private Long maxRunHistory;
@JsonProperty("sync_record_url")
private String syncRecordUrl;
@JsonProperty("client_key_masked")
private String clientKeyMasked;
@JsonProperty("device_id")
private String deviceId;
@JsonProperty("business_view")
private Map<String, Object> businessView;
}

View File

@@ -0,0 +1,18 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class DigitalEmployeeSyncAckDto {
@JsonProperty("outbox_id")
private String outboxId;
@JsonProperty("entity_type")
private String entityType;
@JsonProperty("entity_id")
private String entityId;
@JsonProperty("remote_id")
private String remoteId;
}

View File

@@ -0,0 +1,24 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonAlias;
import lombok.Data;
import java.util.Map;
@Data
public class DigitalEmployeeSyncItemDto {
@JsonAlias("outbox_id")
private String outboxId;
@JsonAlias("entity_type")
private String entityType;
@JsonAlias("entity_id")
private String entityId;
private String operation;
@JsonAlias("contract_version")
private String contractVersion;
@JsonAlias("entity_summary")
private Map<String, Object> entitySummary;
@JsonAlias("business_view")
private Map<String, Object> businessView;
private Map<String, Object> payload;
}

View File

@@ -0,0 +1,35 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.Date;
@Data
@Builder
public class DigitalEmployeeSyncRecordDto {
private Long id;
@JsonProperty("client_key")
private String clientKey;
@JsonProperty("device_id")
private String deviceId;
@JsonProperty("outbox_id")
private String outboxId;
@JsonProperty("entity_type")
private String entityType;
@JsonProperty("entity_id")
private String entityId;
private String operation;
@JsonProperty("contract_version")
private String contractVersion;
@JsonProperty("entity_summary")
private String entitySummary;
@JsonProperty("business_view")
private String businessView;
private String payload;
@JsonProperty("synced_at")
private Date syncedAt;
private Date modified;
private Date created;
}

View File

@@ -0,0 +1,18 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class DigitalEmployeeSyncRecordPageDto {
private Long total;
@JsonProperty("page_no")
private Long pageNo;
@JsonProperty("page_size")
private Long pageSize;
private List<DigitalEmployeeSyncRecordDto> records;
}

View File

@@ -0,0 +1,15 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import com.fasterxml.jackson.annotation.JsonAlias;
import lombok.Data;
import java.util.List;
@Data
public class DigitalEmployeeSyncRequestDto {
@JsonAlias("client_key")
private String clientKey;
@JsonAlias("device_id")
private String deviceId;
private List<DigitalEmployeeSyncItemDto> items;
}

View File

@@ -0,0 +1,12 @@
package com.xspaceagi.agent.core.adapter.dto.digital;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class DigitalEmployeeSyncResponseDto {
private List<DigitalEmployeeSyncAckDto> acked;
}

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.agent.core.adapter.repository;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeAdminAction;
public interface DigitalEmployeeAdminActionRepository extends IService<DigitalEmployeeAdminAction> {
IPage<DigitalEmployeeAdminAction> queryActions(
Long tenantId,
String clientKey,
String deviceId,
String status,
long pageNo,
long pageSize
);
}

View File

@@ -0,0 +1,8 @@
package com.xspaceagi.agent.core.adapter.repository;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeArtifactLifecycle;
public interface DigitalEmployeeArtifactLifecycleRepository extends IService<DigitalEmployeeArtifactLifecycle> {
void upsertBySyncRecord(DigitalEmployeeArtifactLifecycle lifecycle);
}

View File

@@ -0,0 +1,24 @@
package com.xspaceagi.agent.core.adapter.repository;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeBusinessFact;
import java.util.Date;
public interface DigitalEmployeeBusinessFactRepository extends IService<DigitalEmployeeBusinessFact> {
void upsertBySyncRecord(DigitalEmployeeBusinessFact fact);
IPage<DigitalEmployeeBusinessFact> queryFacts(
Long tenantId,
String clientKey,
String deviceId,
String entityType,
String entityId,
Date updatedFrom,
Date updatedTo,
String syncStatus,
long pageNo,
long pageSize
);
}

View File

@@ -0,0 +1,8 @@
package com.xspaceagi.agent.core.adapter.repository;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeDailyReportFact;
public interface DigitalEmployeeDailyReportFactRepository extends IService<DigitalEmployeeDailyReportFact> {
void upsertBySyncRecord(DigitalEmployeeDailyReportFact fact);
}

View File

@@ -0,0 +1,33 @@
package com.xspaceagi.agent.core.adapter.repository;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord;
import java.util.Date;
public interface DigitalEmployeeSyncRecordRepository extends IService<DigitalEmployeeSyncRecord> {
IPage<DigitalEmployeeSyncRecord> queryRecords(
Long tenantId,
String clientKey,
String deviceId,
String entityType,
String entityId,
String outboxId,
long pageNo,
long pageSize
);
IPage<DigitalEmployeeSyncRecord> queryBusinessRecords(
Long tenantId,
String clientKey,
String deviceId,
String entityType,
String entityId,
Date updatedFrom,
Date updatedTo,
String syncStatus,
long pageNo,
long pageSize
);
}

View File

@@ -0,0 +1,36 @@
package com.xspaceagi.agent.core.adapter.repository.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@TableName("digital_employee_admin_action")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DigitalEmployeeAdminAction {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField(value = "_tenant_id")
private Long tenantId;
private Long userId;
private String clientKey;
private String deviceId;
private String entityType;
private String entityId;
private String action;
private String status;
private String reasonCodes;
private String requestPayload;
private Date processedAt;
private Date modified;
private Date created;
}

View File

@@ -0,0 +1,41 @@
package com.xspaceagi.agent.core.adapter.repository.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@TableName("digital_employee_artifact_lifecycle")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DigitalEmployeeArtifactLifecycle {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField(value = "_tenant_id")
private Long tenantId;
private Long syncRecordId;
private String clientKey;
private String deviceId;
private String artifactId;
private String planId;
private String taskId;
private String runId;
private String status;
private String retentionStatus;
private String cleanupStatus;
private String deletedAt;
private String versionKey;
private String source;
private String businessView;
private Date syncedAt;
private Date modified;
private Date created;
}

View File

@@ -0,0 +1,50 @@
package com.xspaceagi.agent.core.adapter.repository.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@TableName("digital_employee_business_fact")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DigitalEmployeeBusinessFact {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField(value = "_tenant_id")
private Long tenantId;
private Long userId;
private Long syncRecordId;
private String clientKey;
private String deviceId;
private String outboxId;
private String entityType;
private String entityId;
private String operation;
private String title;
private String status;
private String summary;
private String planId;
private String taskId;
private String runId;
private String kind;
private String category;
private String occurredAt;
private String startedAt;
private String finishedAt;
private Long durationMs;
private String syncStatus;
private String businessView;
private String payload;
private Date syncedAt;
private Date modified;
private Date created;
}

View File

@@ -0,0 +1,46 @@
package com.xspaceagi.agent.core.adapter.repository.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@TableName("digital_employee_daily_report_fact")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DigitalEmployeeDailyReportFact {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField(value = "_tenant_id")
private Long tenantId;
private Long syncRecordId;
private String clientKey;
private String deviceId;
private String reportId;
private String dailyReportFactId;
private String planId;
private String status;
private String physicalFactStatus;
private String deliveryStatus;
private String approvalStatus;
private String approvalDecisionStatus;
private String confirmedAt;
private String reportDate;
private String sendRecordId;
private String sendChannel;
private String sendRequestStatus;
private String sendRecordStatus;
private String materializedSource;
private String businessView;
private Date syncedAt;
private Date modified;
private Date created;
}

View File

@@ -0,0 +1,38 @@
package com.xspaceagi.agent.core.adapter.repository.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@TableName("digital_employee_sync_record")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DigitalEmployeeSyncRecord {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField(value = "_tenant_id")
private Long tenantId;
private Long userId;
private String clientKey;
private String deviceId;
private String outboxId;
private String entityType;
private String entityId;
private String operation;
private String contractVersion;
private String entitySummary;
private String businessView;
private String payload;
private Date syncedAt;
private Date modified;
private Date created;
}

View File

@@ -0,0 +1,9 @@
package com.xspaceagi.agent.core.infra.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeAdminAction;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DigitalEmployeeAdminActionMapper extends BaseMapper<DigitalEmployeeAdminAction> {
}

View File

@@ -0,0 +1,9 @@
package com.xspaceagi.agent.core.infra.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeArtifactLifecycle;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DigitalEmployeeArtifactLifecycleMapper extends BaseMapper<DigitalEmployeeArtifactLifecycle> {
}

View File

@@ -0,0 +1,9 @@
package com.xspaceagi.agent.core.infra.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeBusinessFact;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DigitalEmployeeBusinessFactMapper extends BaseMapper<DigitalEmployeeBusinessFact> {
}

View File

@@ -0,0 +1,9 @@
package com.xspaceagi.agent.core.infra.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeDailyReportFact;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DigitalEmployeeDailyReportFactMapper extends BaseMapper<DigitalEmployeeDailyReportFact> {
}

View File

@@ -0,0 +1,9 @@
package com.xspaceagi.agent.core.infra.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DigitalEmployeeSyncRecordMapper extends BaseMapper<DigitalEmployeeSyncRecord> {
}

View File

@@ -0,0 +1,36 @@
package com.xspaceagi.agent.core.infra.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeAdminActionRepository;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeAdminAction;
import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeAdminActionMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@Service
public class DigitalEmployeeAdminActionRepositoryImpl
extends ServiceImpl<DigitalEmployeeAdminActionMapper, DigitalEmployeeAdminAction>
implements DigitalEmployeeAdminActionRepository {
@Override
public IPage<DigitalEmployeeAdminAction> queryActions(
Long tenantId,
String clientKey,
String deviceId,
String status,
long pageNo,
long pageSize
) {
LambdaQueryWrapper<DigitalEmployeeAdminAction> queryWrapper = new LambdaQueryWrapper<DigitalEmployeeAdminAction>()
.eq(tenantId != null, DigitalEmployeeAdminAction::getTenantId, tenantId)
.eq(StringUtils.isNotBlank(clientKey), DigitalEmployeeAdminAction::getClientKey, clientKey)
.eq(StringUtils.isNotBlank(deviceId), DigitalEmployeeAdminAction::getDeviceId, deviceId)
.eq(StringUtils.isNotBlank(status), DigitalEmployeeAdminAction::getStatus, status)
.orderByAsc(DigitalEmployeeAdminAction::getCreated)
.orderByAsc(DigitalEmployeeAdminAction::getId);
return page(new Page<>(pageNo, pageSize), queryWrapper);
}
}

View File

@@ -0,0 +1,29 @@
package com.xspaceagi.agent.core.infra.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeArtifactLifecycleRepository;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeArtifactLifecycle;
import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeArtifactLifecycleMapper;
import org.springframework.stereotype.Service;
@Service
public class DigitalEmployeeArtifactLifecycleRepositoryImpl
extends ServiceImpl<DigitalEmployeeArtifactLifecycleMapper, DigitalEmployeeArtifactLifecycle>
implements DigitalEmployeeArtifactLifecycleRepository {
@Override
public void upsertBySyncRecord(DigitalEmployeeArtifactLifecycle lifecycle) {
if (lifecycle == null || lifecycle.getTenantId() == null || lifecycle.getSyncRecordId() == null) return;
DigitalEmployeeArtifactLifecycle existing = getOne(new LambdaQueryWrapper<DigitalEmployeeArtifactLifecycle>()
.eq(DigitalEmployeeArtifactLifecycle::getTenantId, lifecycle.getTenantId())
.eq(DigitalEmployeeArtifactLifecycle::getSyncRecordId, lifecycle.getSyncRecordId()), false);
if (existing == null) {
save(lifecycle);
} else {
lifecycle.setId(existing.getId());
lifecycle.setCreated(existing.getCreated());
updateById(lifecycle);
}
}
}

View File

@@ -0,0 +1,61 @@
package com.xspaceagi.agent.core.infra.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeBusinessFactRepository;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeBusinessFact;
import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeBusinessFactMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class DigitalEmployeeBusinessFactRepositoryImpl
extends ServiceImpl<DigitalEmployeeBusinessFactMapper, DigitalEmployeeBusinessFact>
implements DigitalEmployeeBusinessFactRepository {
@Override
public void upsertBySyncRecord(DigitalEmployeeBusinessFact fact) {
if (fact == null || fact.getTenantId() == null || fact.getSyncRecordId() == null) return;
DigitalEmployeeBusinessFact existing = getOne(new LambdaQueryWrapper<DigitalEmployeeBusinessFact>()
.eq(DigitalEmployeeBusinessFact::getTenantId, fact.getTenantId())
.eq(DigitalEmployeeBusinessFact::getSyncRecordId, fact.getSyncRecordId()), false);
if (existing == null) {
save(fact);
} else {
fact.setId(existing.getId());
fact.setCreated(existing.getCreated());
updateById(fact);
}
}
@Override
public IPage<DigitalEmployeeBusinessFact> queryFacts(
Long tenantId,
String clientKey,
String deviceId,
String entityType,
String entityId,
Date updatedFrom,
Date updatedTo,
String syncStatus,
long pageNo,
long pageSize
) {
LambdaQueryWrapper<DigitalEmployeeBusinessFact> queryWrapper = new LambdaQueryWrapper<DigitalEmployeeBusinessFact>()
.eq(tenantId != null, DigitalEmployeeBusinessFact::getTenantId, tenantId)
.eq(StringUtils.isNotBlank(clientKey), DigitalEmployeeBusinessFact::getClientKey, clientKey)
.eq(StringUtils.isNotBlank(deviceId), DigitalEmployeeBusinessFact::getDeviceId, deviceId)
.eq(StringUtils.isNotBlank(entityType), DigitalEmployeeBusinessFact::getEntityType, entityType)
.eq(StringUtils.isNotBlank(entityId), DigitalEmployeeBusinessFact::getEntityId, entityId)
.eq(StringUtils.isNotBlank(syncStatus), DigitalEmployeeBusinessFact::getSyncStatus, syncStatus)
.ge(updatedFrom != null, DigitalEmployeeBusinessFact::getSyncedAt, updatedFrom)
.le(updatedTo != null, DigitalEmployeeBusinessFact::getSyncedAt, updatedTo)
.orderByDesc(DigitalEmployeeBusinessFact::getSyncedAt)
.orderByDesc(DigitalEmployeeBusinessFact::getId);
return page(new Page<>(pageNo, pageSize), queryWrapper);
}
}

View File

@@ -0,0 +1,29 @@
package com.xspaceagi.agent.core.infra.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeDailyReportFactRepository;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeDailyReportFact;
import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeDailyReportFactMapper;
import org.springframework.stereotype.Service;
@Service
public class DigitalEmployeeDailyReportFactRepositoryImpl
extends ServiceImpl<DigitalEmployeeDailyReportFactMapper, DigitalEmployeeDailyReportFact>
implements DigitalEmployeeDailyReportFactRepository {
@Override
public void upsertBySyncRecord(DigitalEmployeeDailyReportFact fact) {
if (fact == null || fact.getTenantId() == null || fact.getSyncRecordId() == null) return;
DigitalEmployeeDailyReportFact existing = getOne(new LambdaQueryWrapper<DigitalEmployeeDailyReportFact>()
.eq(DigitalEmployeeDailyReportFact::getTenantId, fact.getTenantId())
.eq(DigitalEmployeeDailyReportFact::getSyncRecordId, fact.getSyncRecordId()), false);
if (existing == null) {
save(fact);
} else {
fact.setId(existing.getId());
fact.setCreated(existing.getCreated());
updateById(fact);
}
}
}

View File

@@ -0,0 +1,68 @@
package com.xspaceagi.agent.core.infra.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeSyncRecordRepository;
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord;
import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeSyncRecordMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class DigitalEmployeeSyncRecordRepositoryImpl
extends ServiceImpl<DigitalEmployeeSyncRecordMapper, DigitalEmployeeSyncRecord>
implements DigitalEmployeeSyncRecordRepository {
@Override
public IPage<DigitalEmployeeSyncRecord> queryRecords(
Long tenantId,
String clientKey,
String deviceId,
String entityType,
String entityId,
String outboxId,
long pageNo,
long pageSize
) {
LambdaQueryWrapper<DigitalEmployeeSyncRecord> queryWrapper = new LambdaQueryWrapper<DigitalEmployeeSyncRecord>()
.eq(tenantId != null, DigitalEmployeeSyncRecord::getTenantId, tenantId)
.eq(StringUtils.isNotBlank(clientKey), DigitalEmployeeSyncRecord::getClientKey, clientKey)
.eq(StringUtils.isNotBlank(deviceId), DigitalEmployeeSyncRecord::getDeviceId, deviceId)
.eq(StringUtils.isNotBlank(entityType), DigitalEmployeeSyncRecord::getEntityType, entityType)
.eq(StringUtils.isNotBlank(entityId), DigitalEmployeeSyncRecord::getEntityId, entityId)
.eq(StringUtils.isNotBlank(outboxId), DigitalEmployeeSyncRecord::getOutboxId, outboxId)
.orderByDesc(DigitalEmployeeSyncRecord::getSyncedAt)
.orderByDesc(DigitalEmployeeSyncRecord::getId);
return page(new Page<>(pageNo, pageSize), queryWrapper);
}
@Override
public IPage<DigitalEmployeeSyncRecord> queryBusinessRecords(
Long tenantId,
String clientKey,
String deviceId,
String entityType,
String entityId,
Date updatedFrom,
Date updatedTo,
String syncStatus,
long pageNo,
long pageSize
) {
LambdaQueryWrapper<DigitalEmployeeSyncRecord> queryWrapper = new LambdaQueryWrapper<DigitalEmployeeSyncRecord>()
.eq(tenantId != null, DigitalEmployeeSyncRecord::getTenantId, tenantId)
.eq(StringUtils.isNotBlank(clientKey), DigitalEmployeeSyncRecord::getClientKey, clientKey)
.eq(StringUtils.isNotBlank(deviceId), DigitalEmployeeSyncRecord::getDeviceId, deviceId)
.eq(StringUtils.isNotBlank(entityType), DigitalEmployeeSyncRecord::getEntityType, entityType)
.eq(StringUtils.isNotBlank(entityId), DigitalEmployeeSyncRecord::getEntityId, entityId)
.ge(updatedFrom != null, DigitalEmployeeSyncRecord::getSyncedAt, updatedFrom)
.le(updatedTo != null, DigitalEmployeeSyncRecord::getSyncedAt, updatedTo)
.orderByDesc(DigitalEmployeeSyncRecord::getSyncedAt)
.orderByDesc(DigitalEmployeeSyncRecord::getId);
return page(new Page<>(pageNo, pageSize), queryWrapper);
}
}

View File

@@ -0,0 +1,77 @@
package com.xspaceagi.agent.web.ui.controller;
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPageDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRequestDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST;
@RestController
@RequestMapping("/api/digital-employee/admin/actions")
public class DigitalEmployeeAdminActionController {
private static final String CLIENT_KEY_HEADER = "X-Qiming-Client-Key";
@Resource
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
@RequestMapping(method = RequestMethod.POST)
public ReqResult<DigitalEmployeeAdminActionResultDto> createAction(
@RequestBody(required = false) DigitalEmployeeAdminActionRequestDto request
) {
return ReqResult.success(digitalEmployeeSyncApplicationService.createAdminAction(request));
}
@RequestMapping(path = "/pending", method = RequestMethod.GET)
public ReqResult<DigitalEmployeeAdminActionPageDto> pendingActions(
@RequestHeader(name = CLIENT_KEY_HEADER, required = false) String headerClientKey,
@RequestParam(name = "client_key", required = false) String clientKey,
@RequestParam(name = "device_id", required = false) String deviceId,
@RequestParam(name = "page_no", required = false) Long pageNo,
@RequestParam(name = "page_size", required = false) Long pageSize
) {
String normalizedClientKey = StringUtils.defaultIfBlank(clientKey, headerClientKey);
if (StringUtils.isBlank(normalizedClientKey)) {
return ReqResult.create("0001", "客户端 Key 缺失", null);
}
return ReqResult.success(digitalEmployeeSyncApplicationService.queryPendingAdminActions(
normalizedClientKey.trim(),
deviceId,
pageNo,
pageSize
));
}
@RequestMapping(path = "/{actionId}/result", method = RequestMethod.POST)
public ReqResult<DigitalEmployeeAdminActionResultDto> markResult(
@PathVariable("actionId") Long actionId,
@RequestBody(required = false) Map<String, Object> body
) {
String status = body == null ? null : String.valueOf(body.get("status"));
List<String> reasonCodes = reasonCodes(body == null ? null : body.get("reason_codes"));
return ReqResult.success(digitalEmployeeSyncApplicationService.markAdminActionResult(actionId, status, reasonCodes));
}
private List<String> reasonCodes(Object value) {
if (value instanceof List<?> list) {
return list.stream().map(String::valueOf).toList();
}
return List.of();
}
}

View File

@@ -0,0 +1,67 @@
package com.xspaceagi.agent.web.ui.controller;
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST;
@RestController
@RequestMapping("/api/digital-employee/admin/policies")
public class DigitalEmployeeAdminPolicyController {
@Resource
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
@RequestMapping(path = "/snapshots", method = RequestMethod.GET)
public ReqResult<DigitalEmployeePolicySnapshotPageDto> querySnapshots(
@RequestParam(name = "client_key", required = false) String clientKey,
@RequestParam(name = "device_id", required = false) String deviceId,
@RequestParam(name = "status", required = false) String status,
@RequestParam(name = "policy_kind", required = false) String policyKind,
@RequestParam(name = "updated_from", required = false) String updatedFrom,
@RequestParam(name = "updated_to", required = false) String updatedTo,
@RequestParam(name = "page_no", required = false) Long pageNo,
@RequestParam(name = "page_size", required = false) Long pageSize
) {
return ReqResult.success(digitalEmployeeSyncApplicationService.queryAdminPolicySnapshots(
clientKey,
deviceId,
status,
policyKind,
parseDate(updatedFrom),
parseDate(updatedTo),
pageNo,
pageSize
));
}
private Date parseDate(String value) {
if (StringUtils.isBlank(value)) return null;
String trimmed = value.trim();
try {
return Date.from(Instant.parse(trimmed));
} catch (Exception ignored) {
try {
LocalDateTime localDateTime = LocalDateTime.parse(trimmed, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
} catch (Exception ignoredAgain) {
return null;
}
}
}
}

View File

@@ -0,0 +1,78 @@
package com.xspaceagi.agent.web.ui.controller;
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordRequestDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminRecordResultDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBillingEstimateDto;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST;
@RestController
@RequestMapping("/api/digital-employee/admin")
public class DigitalEmployeeAdminRecordController {
@Resource
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
@RequestMapping(path = "/daily-reports/send-records", method = RequestMethod.POST)
public ReqResult<DigitalEmployeeAdminRecordResultDto> recordDailyReportSendResult(
@RequestBody(required = false) DigitalEmployeeAdminRecordRequestDto request
) {
return ReqResult.success(digitalEmployeeSyncApplicationService.recordDailyReportSendResult(request));
}
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
@RequestMapping(path = "/billing/estimates", method = RequestMethod.GET)
public ReqResult<DigitalEmployeeBillingEstimateDto> queryBillingEstimates(
@RequestParam(name = "client_key", required = false) String clientKey,
@RequestParam(name = "device_id", required = false) String deviceId,
@RequestParam(name = "updated_from", required = false) String updatedFrom,
@RequestParam(name = "updated_to", required = false) String updatedTo
) {
return ReqResult.success(digitalEmployeeSyncApplicationService.queryBillingEstimates(
clientKey,
deviceId,
parseDate(updatedFrom),
parseDate(updatedTo)
));
}
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
@RequestMapping(path = "/audit-archives", method = RequestMethod.POST)
public ReqResult<DigitalEmployeeAdminRecordResultDto> recordAuditArchive(
@RequestBody(required = false) DigitalEmployeeAdminRecordRequestDto request
) {
return ReqResult.success(digitalEmployeeSyncApplicationService.recordAuditArchive(request));
}
private Date parseDate(String value) {
if (StringUtils.isBlank(value)) return null;
String trimmed = value.trim();
try {
return Date.from(Instant.parse(trimmed));
} catch (Exception ignored) {
try {
LocalDateTime localDateTime = LocalDateTime.parse(trimmed, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
} catch (Exception ignoredAgain) {
return null;
}
}
}
}

View File

@@ -0,0 +1,80 @@
package com.xspaceagi.agent.web.ui.controller;
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenchPageDto;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST;
@RestController
@RequestMapping("/api/digital-employee/admin/workbench")
public class DigitalEmployeeAdminWorkbenchController {
@Resource
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
@RequestMapping(path = "/{view}", method = RequestMethod.GET)
public ReqResult<DigitalEmployeeAdminWorkbenchPageDto> queryWorkbench(
@PathVariable("view") String view,
@RequestParam(name = "client_key", required = false) String clientKey,
@RequestParam(name = "device_id", required = false) String deviceId,
@RequestParam(name = "entity_type", required = false) String entityType,
@RequestParam(name = "entity_id", required = false) String entityId,
@RequestParam(name = "status", required = false) String status,
@RequestParam(name = "kind", required = false) String kind,
@RequestParam(name = "category", required = false) String category,
@RequestParam(name = "level", required = false) String level,
@RequestParam(name = "priority", required = false) String priority,
@RequestParam(name = "updated_from", required = false) String updatedFrom,
@RequestParam(name = "updated_to", required = false) String updatedTo,
@RequestParam(name = "page_no", required = false) Long pageNo,
@RequestParam(name = "page_size", required = false) Long pageSize
) {
return ReqResult.success(digitalEmployeeSyncApplicationService.queryAdminWorkbench(
view,
clientKey,
deviceId,
entityType,
entityId,
status,
kind,
category,
level,
priority,
parseDate(updatedFrom),
parseDate(updatedTo),
pageNo,
pageSize
));
}
private Date parseDate(String value) {
if (StringUtils.isBlank(value)) return null;
String trimmed = value.trim();
try {
return Date.from(Instant.parse(trimmed));
} catch (Exception ignored) {
try {
LocalDateTime localDateTime = LocalDateTime.parse(trimmed, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
} catch (Exception ignoredAgain) {
return null;
}
}
}
}

View File

@@ -0,0 +1,177 @@
package com.xspaceagi.agent.web.ui.controller;
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST;
@RestController
@RequestMapping("/api/digital-employee")
public class DigitalEmployeeBusinessViewController {
@Resource
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
@RequestMapping(path = "/plans", method = RequestMethod.GET)
public ReqResult<DigitalEmployeeBusinessViewPageDto> queryPlans(
@RequestParam(name = "client_key", required = false) String clientKey,
@RequestParam(name = "device_id", required = false) String deviceId,
@RequestParam(name = "entity_id", required = false) String entityId,
@RequestParam(name = "status", required = false) String status,
@RequestParam(name = "sync_status", required = false) String syncStatus,
@RequestParam(name = "updated_from", required = false) String updatedFrom,
@RequestParam(name = "updated_to", required = false) String updatedTo,
@RequestParam(name = "page_no", required = false) Long pageNo,
@RequestParam(name = "page_size", required = false) Long pageSize
) {
return ReqResult.success(query("plan", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize));
}
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
@RequestMapping(path = "/plans/{plan_id}", method = RequestMethod.GET)
public ReqResult<DigitalEmployeePlanBusinessViewDetailDto> queryPlanDetail(
@PathVariable("plan_id") String planId,
@RequestParam(name = "client_key", required = false) String clientKey,
@RequestParam(name = "device_id", required = false) String deviceId
) {
return ReqResult.success(digitalEmployeeSyncApplicationService.queryPlanBusinessViewDetail(planId, clientKey, deviceId));
}
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
@RequestMapping(path = "/tasks", method = RequestMethod.GET)
public ReqResult<DigitalEmployeeBusinessViewPageDto> queryTasks(
@RequestParam(name = "client_key", required = false) String clientKey,
@RequestParam(name = "device_id", required = false) String deviceId,
@RequestParam(name = "entity_id", required = false) String entityId,
@RequestParam(name = "status", required = false) String status,
@RequestParam(name = "sync_status", required = false) String syncStatus,
@RequestParam(name = "updated_from", required = false) String updatedFrom,
@RequestParam(name = "updated_to", required = false) String updatedTo,
@RequestParam(name = "page_no", required = false) Long pageNo,
@RequestParam(name = "page_size", required = false) Long pageSize
) {
return ReqResult.success(query("task", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize));
}
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
@RequestMapping(path = "/runs", method = RequestMethod.GET)
public ReqResult<DigitalEmployeeBusinessViewPageDto> queryRuns(
@RequestParam(name = "client_key", required = false) String clientKey,
@RequestParam(name = "device_id", required = false) String deviceId,
@RequestParam(name = "entity_id", required = false) String entityId,
@RequestParam(name = "status", required = false) String status,
@RequestParam(name = "sync_status", required = false) String syncStatus,
@RequestParam(name = "updated_from", required = false) String updatedFrom,
@RequestParam(name = "updated_to", required = false) String updatedTo,
@RequestParam(name = "page_no", required = false) Long pageNo,
@RequestParam(name = "page_size", required = false) Long pageSize
) {
return ReqResult.success(query("run", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize));
}
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
@RequestMapping(path = "/events", method = RequestMethod.GET)
public ReqResult<DigitalEmployeeBusinessViewPageDto> queryEvents(
@RequestParam(name = "client_key", required = false) String clientKey,
@RequestParam(name = "device_id", required = false) String deviceId,
@RequestParam(name = "entity_id", required = false) String entityId,
@RequestParam(name = "status", required = false) String status,
@RequestParam(name = "sync_status", required = false) String syncStatus,
@RequestParam(name = "updated_from", required = false) String updatedFrom,
@RequestParam(name = "updated_to", required = false) String updatedTo,
@RequestParam(name = "page_no", required = false) Long pageNo,
@RequestParam(name = "page_size", required = false) Long pageSize
) {
return ReqResult.success(query("event", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize));
}
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
@RequestMapping(path = "/artifacts", method = RequestMethod.GET)
public ReqResult<DigitalEmployeeBusinessViewPageDto> queryArtifacts(
@RequestParam(name = "client_key", required = false) String clientKey,
@RequestParam(name = "device_id", required = false) String deviceId,
@RequestParam(name = "entity_id", required = false) String entityId,
@RequestParam(name = "status", required = false) String status,
@RequestParam(name = "sync_status", required = false) String syncStatus,
@RequestParam(name = "updated_from", required = false) String updatedFrom,
@RequestParam(name = "updated_to", required = false) String updatedTo,
@RequestParam(name = "page_no", required = false) Long pageNo,
@RequestParam(name = "page_size", required = false) Long pageSize
) {
return ReqResult.success(query("artifact", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize));
}
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
@RequestMapping(path = "/approvals", method = RequestMethod.GET)
public ReqResult<DigitalEmployeeBusinessViewPageDto> queryApprovals(
@RequestParam(name = "client_key", required = false) String clientKey,
@RequestParam(name = "device_id", required = false) String deviceId,
@RequestParam(name = "entity_id", required = false) String entityId,
@RequestParam(name = "status", required = false) String status,
@RequestParam(name = "sync_status", required = false) String syncStatus,
@RequestParam(name = "updated_from", required = false) String updatedFrom,
@RequestParam(name = "updated_to", required = false) String updatedTo,
@RequestParam(name = "page_no", required = false) Long pageNo,
@RequestParam(name = "page_size", required = false) Long pageSize
) {
return ReqResult.success(query("approval", clientKey, deviceId, entityId, status, syncStatus, updatedFrom, updatedTo, pageNo, pageSize));
}
private DigitalEmployeeBusinessViewPageDto query(
String entityType,
String clientKey,
String deviceId,
String entityId,
String status,
String syncStatus,
String updatedFrom,
String updatedTo,
Long pageNo,
Long pageSize
) {
return digitalEmployeeSyncApplicationService.queryBusinessViews(
entityType,
clientKey,
deviceId,
entityId,
status,
syncStatus,
parseDate(updatedFrom),
parseDate(updatedTo),
pageNo,
pageSize
);
}
private Date parseDate(String value) {
if (StringUtils.isBlank(value)) return null;
String trimmed = value.trim();
try {
return Date.from(Instant.parse(trimmed));
} catch (Exception ignored) {
try {
LocalDateTime localDateTime = LocalDateTime.parse(trimmed, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
} catch (Exception ignoredAgain) {
return null;
}
}
}
}

View File

@@ -0,0 +1,72 @@
package com.xspaceagi.agent.web.ui.controller;
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordPageDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST;
@RestController
@RequestMapping("/api/digital-employee/sync")
public class DigitalEmployeeSyncController {
private static final String CLIENT_KEY_HEADER = "X-Qiming-Client-Key";
@Resource
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
@RequestMapping(path = "/outbox", method = RequestMethod.POST)
public ReqResult<DigitalEmployeeSyncResponseDto> syncOutbox(
@RequestHeader(name = CLIENT_KEY_HEADER, required = false) String headerClientKey,
@RequestBody(required = false) DigitalEmployeeSyncRequestDto request
) {
if (StringUtils.isBlank(headerClientKey)) {
return ReqResult.create("0001", "客户端 Key header 缺失", null);
}
if (request == null) {
request = new DigitalEmployeeSyncRequestDto();
}
String normalizedHeaderClientKey = headerClientKey.trim();
if (StringUtils.isBlank(request.getClientKey())) {
request.setClientKey(normalizedHeaderClientKey);
} else if (!StringUtils.equals(request.getClientKey().trim(), normalizedHeaderClientKey)) {
return ReqResult.create("0001", "客户端 Key header 与请求体不一致", null);
} else {
request.setClientKey(request.getClientKey().trim());
}
return ReqResult.success(digitalEmployeeSyncApplicationService.syncOutbox(request));
}
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST)
@RequestMapping(path = "/records", method = RequestMethod.GET)
public ReqResult<DigitalEmployeeSyncRecordPageDto> queryRecords(
@RequestParam(name = "client_key", required = false) String clientKey,
@RequestParam(name = "device_id", required = false) String deviceId,
@RequestParam(name = "entity_type", required = false) String entityType,
@RequestParam(name = "entity_id", required = false) String entityId,
@RequestParam(name = "outbox_id", required = false) String outboxId,
@RequestParam(name = "page_no", required = false) Long pageNo,
@RequestParam(name = "page_size", required = false) Long pageSize
) {
return ReqResult.success(digitalEmployeeSyncApplicationService.queryRecords(
clientKey,
deviceId,
entityType,
entityId,
outboxId,
pageNo,
pageSize
));
}
}

View File

@@ -417,6 +417,16 @@ public enum ResourceEnum {
CONTENT_SKILL_QUERY_DETAIL(ResourceTypeEnum.OPERATION, "content_skill_query_detail", "查询详情", "content_skill"),
CONTENT_SKILL_DELETE(ResourceTypeEnum.OPERATION, "content_skill_delete", "删除", "content_skill"),
// 2级模块 - 数字员工同步记录
CONTENT_DIGITAL_EMPLOYEE_SYNC(ResourceTypeEnum.MODULE, "content_digital_employee_sync", "数字员工同步记录", "content_manage"),
CONTENT_DIGITAL_EMPLOYEE_SYNC_QUERY_LIST(ResourceTypeEnum.OPERATION, "content_digital_employee_sync_query_list", "查询列表", "content_digital_employee_sync"),
// 2级模块 - 数字员工运营台
CONTENT_DIGITAL_EMPLOYEE_OPS(ResourceTypeEnum.MODULE, "content_digital_employee_ops", "数字员工运营台", "content_manage"),
CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST(ResourceTypeEnum.OPERATION, "content_digital_employee_ops_query_list", "查询列表", "content_digital_employee_ops"),
// ================== 多语言管理模块 ==================
I18N_LANG_MANAGE(ResourceTypeEnum.MODULE, "i18n_lang_manage", "多语言管理模块", "root"),
I18N_LANG_QUERY(ResourceTypeEnum.OPERATION, "i18n_lang_query", "查询", "i18n_lang_manage"),

View File

@@ -0,0 +1,24 @@
CREATE TABLE `digital_employee_sync_record`
(
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`_tenant_id` BIGINT DEFAULT NULL COMMENT '租户ID',
`user_id` BIGINT DEFAULT NULL COMMENT '用户ID',
`client_key` VARCHAR(128) NOT NULL COMMENT '客户端配置Key',
`device_id` VARCHAR(128) DEFAULT NULL COMMENT '设备ID',
`outbox_id` VARCHAR(255) NOT NULL COMMENT '客户端outbox ID',
`entity_type` VARCHAR(32) NOT NULL COMMENT '实体类型plan/task/run/event/artifact/approval',
`entity_id` VARCHAR(255) NOT NULL COMMENT '客户端实体ID',
`operation` VARCHAR(32) NOT NULL COMMENT '操作类型',
`payload` JSON DEFAULT NULL COMMENT '实体快照',
`synced_at` DATETIME NOT NULL COMMENT '最近同步时间',
`modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_client_outbox` (`client_key`, `outbox_id`),
KEY `idx_client_entity` (`client_key`, `entity_type`, `entity_id`),
KEY `idx_tenant_client_entity_time` (`_tenant_id`, `client_key`, `entity_type`, `entity_id`, `synced_at`),
KEY `idx_tenant_user_time` (`_tenant_id`, `user_id`, `synced_at`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci
COMMENT = '数字员工客户端同步记录';

View File

@@ -0,0 +1,75 @@
-- Add digital employee sync records menu and resource.
-- Idempotent for existing tenants: creates menu/resource rows, binds menu to resource,
-- and grants super_admin default access. Other roles can be granted this menu
-- from the menu permission management UI.
INSERT INTO sys_resource (`parent_id`, `code`, `name`, `description`, `source`, `type`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
SELECT parent.id, 'content_digital_employee_sync', '数字员工同步记录', '数字员工同步记录', 1, 1, 90, 1, parent.`_tenant_id`, 0, 'system', 1
FROM sys_resource parent
WHERE parent.`code` = 'content_manage' AND parent.`yn` = 1
AND NOT EXISTS (
SELECT 1 FROM sys_resource r
WHERE r.`_tenant_id` = parent.`_tenant_id` AND r.`code` = 'content_digital_employee_sync' AND r.`yn` = 1
);
INSERT INTO sys_resource (`parent_id`, `code`, `name`, `description`, `source`, `type`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
SELECT parent.id, 'content_digital_employee_sync_query_list', '查询列表', '查询数字员工同步记录', 1, 2, 1, 1, parent.`_tenant_id`, 0, 'system', 1
FROM sys_resource parent
WHERE parent.`code` = 'content_digital_employee_sync' AND parent.`yn` = 1
AND NOT EXISTS (
SELECT 1 FROM sys_resource r
WHERE r.`_tenant_id` = parent.`_tenant_id` AND r.`code` = 'content_digital_employee_sync_query_list' AND r.`yn` = 1
);
INSERT INTO sys_menu (`parent_id`, `code`, `name`, `description`, `source`, `path`, `open_type`, `icon`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
SELECT parent.id, 'content_digital_employee_sync', '数字员工同步记录', '数字员工同步记录', 1, '/system/content/content-digital-employee-sync', 1, '', 90, 1, parent.`_tenant_id`, 0, 'system', 1
FROM sys_menu parent
WHERE parent.`code` = 'content_manage' AND parent.`yn` = 1
AND NOT EXISTS (
SELECT 1 FROM sys_menu m
WHERE m.`_tenant_id` = parent.`_tenant_id` AND m.`code` = 'content_digital_employee_sync' AND m.`yn` = 1
);
INSERT INTO sys_menu_resource (`menu_id`, `resource_id`, `resource_bind_type`, `_tenant_id`, `creator_id`, `creator`, `yn`)
SELECT m.id, r.id, 1, m.`_tenant_id`, 0, 'system', 1
FROM sys_menu m
JOIN sys_resource r ON r.`_tenant_id` = m.`_tenant_id` AND r.`code` = 'content_digital_employee_sync' AND r.`yn` = 1
WHERE m.`code` = 'content_digital_employee_sync' AND m.`yn` = 1
AND NOT EXISTS (
SELECT 1 FROM sys_menu_resource mr
WHERE mr.`_tenant_id` = m.`_tenant_id` AND mr.`menu_id` = m.id AND mr.`resource_id` = r.id AND mr.`yn` = 1
);
UPDATE sys_menu_resource mr
JOIN sys_menu m ON m.id = mr.menu_id AND m.`_tenant_id` = mr.`_tenant_id` AND m.`code` = 'content_digital_employee_sync' AND m.`yn` = 1
JOIN sys_resource r ON r.id = mr.resource_id AND r.`_tenant_id` = mr.`_tenant_id` AND r.`code` = 'content_digital_employee_sync' AND r.`yn` = 1
SET mr.`resource_bind_type` = 1,
mr.`modified` = CURRENT_TIMESTAMP
WHERE mr.`yn` = 1;
INSERT INTO sys_role_menu (`role_id`, `menu_id`, `menu_bind_type`, `resource_tree_json`, `_tenant_id`, `creator_id`, `creator`, `yn`)
SELECT role.id,
menu.id,
1,
JSON_ARRAY(JSON_OBJECT('id', resource.id, 'code', resource.`code`, 'resourceBindType', 1)),
menu.`_tenant_id`,
0,
'system',
1
FROM sys_role role
JOIN sys_menu menu ON menu.`_tenant_id` = role.`_tenant_id` AND menu.`code` = 'content_digital_employee_sync' AND menu.`yn` = 1
JOIN sys_resource resource ON resource.`_tenant_id` = role.`_tenant_id` AND resource.`code` = 'content_digital_employee_sync' AND resource.`yn` = 1
WHERE role.`code` = 'super_admin' AND role.`yn` = 1
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu rm
WHERE rm.`_tenant_id` = role.`_tenant_id` AND rm.`role_id` = role.id AND rm.`menu_id` = menu.id AND rm.`yn` = 1
);
UPDATE sys_role_menu rm
JOIN sys_role role ON role.id = rm.role_id AND role.`_tenant_id` = rm.`_tenant_id` AND role.`code` = 'super_admin' AND role.`yn` = 1
JOIN sys_menu menu ON menu.id = rm.menu_id AND menu.`_tenant_id` = rm.`_tenant_id` AND menu.`code` = 'content_digital_employee_sync' AND menu.`yn` = 1
JOIN sys_resource resource ON resource.`_tenant_id` = rm.`_tenant_id` AND resource.`code` = 'content_digital_employee_sync' AND resource.`yn` = 1
SET rm.`menu_bind_type` = 1,
rm.`resource_tree_json` = JSON_ARRAY(JSON_OBJECT('id', resource.id, 'code', resource.`code`, 'resourceBindType', 1)),
rm.`modified` = CURRENT_TIMESTAMP
WHERE rm.`yn` = 1;

View File

@@ -0,0 +1,193 @@
ALTER TABLE `digital_employee_sync_record`
ADD COLUMN `contract_version` VARCHAR(128) DEFAULT NULL COMMENT '客户端同步契约版本' AFTER `operation`,
ADD COLUMN `entity_summary` JSON DEFAULT NULL COMMENT '客户端实体摘要' AFTER `contract_version`,
ADD COLUMN `business_view` JSON DEFAULT NULL COMMENT '客户端业务视图投影' AFTER `entity_summary`;
CREATE TABLE IF NOT EXISTS `digital_employee_admin_action` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`_tenant_id` BIGINT DEFAULT NULL COMMENT '租户 ID',
`user_id` BIGINT DEFAULT NULL COMMENT '创建用户 ID',
`client_key` VARCHAR(128) DEFAULT NULL COMMENT '客户端 Key',
`device_id` VARCHAR(128) DEFAULT NULL COMMENT '设备 ID',
`entity_type` VARCHAR(64) DEFAULT NULL COMMENT '实体类型',
`entity_id` VARCHAR(128) DEFAULT NULL COMMENT '实体 ID',
`action` VARCHAR(64) DEFAULT NULL COMMENT '管理端动作',
`status` VARCHAR(32) DEFAULT NULL COMMENT '状态 pending/applied/rejected/failed',
`reason_codes` JSON DEFAULT NULL COMMENT '原因码',
`request_payload` JSON DEFAULT NULL COMMENT '请求附加参数',
`processed_at` DATETIME DEFAULT NULL COMMENT '客户端处理时间',
`modified` DATETIME DEFAULT NULL COMMENT '更新时间',
`created` DATETIME DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_de_admin_action_pending` (`_tenant_id`, `client_key`, `device_id`, `status`, `created`),
KEY `idx_de_admin_action_entity` (`_tenant_id`, `entity_type`, `entity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字员工管理端远程动作意图';
CREATE TABLE IF NOT EXISTS `digital_employee_business_fact` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`_tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`user_id` BIGINT DEFAULT NULL COMMENT '用户ID',
`sync_record_id` BIGINT NOT NULL COMMENT '同步记录ID',
`client_key` VARCHAR(255) DEFAULT NULL COMMENT '客户端Key',
`device_id` VARCHAR(255) DEFAULT NULL COMMENT '设备ID',
`outbox_id` VARCHAR(255) DEFAULT NULL COMMENT '客户端outbox ID',
`entity_type` VARCHAR(64) NOT NULL COMMENT '实体类型',
`entity_id` VARCHAR(255) NOT NULL COMMENT '实体ID',
`operation` VARCHAR(64) DEFAULT NULL COMMENT '操作',
`title` VARCHAR(512) DEFAULT NULL COMMENT '标题',
`status` VARCHAR(128) DEFAULT NULL COMMENT '业务状态',
`summary` VARCHAR(1024) DEFAULT NULL COMMENT '摘要',
`plan_id` VARCHAR(255) DEFAULT NULL COMMENT '计划ID',
`task_id` VARCHAR(255) DEFAULT NULL COMMENT '任务ID',
`run_id` VARCHAR(255) DEFAULT NULL COMMENT '运行ID',
`kind` VARCHAR(255) DEFAULT NULL COMMENT '类型',
`category` VARCHAR(128) DEFAULT NULL COMMENT '分类',
`occurred_at` VARCHAR(64) DEFAULT NULL COMMENT '发生时间',
`started_at` VARCHAR(64) DEFAULT NULL COMMENT '开始时间',
`finished_at` VARCHAR(64) DEFAULT NULL COMMENT '结束时间',
`duration_ms` BIGINT DEFAULT NULL COMMENT '耗时毫秒',
`sync_status` VARCHAR(128) DEFAULT NULL COMMENT '同步状态',
`business_view` JSON DEFAULT NULL COMMENT '业务视图快照',
`payload` JSON DEFAULT NULL COMMENT '安全payload快照',
`synced_at` DATETIME DEFAULT NULL COMMENT '同步时间',
`modified` DATETIME NOT NULL COMMENT '修改时间',
`created` DATETIME NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_de_business_fact_record` (`_tenant_id`, `sync_record_id`),
KEY `idx_de_business_fact_entity` (`_tenant_id`, `entity_type`, `entity_id`),
KEY `idx_de_business_fact_plan` (`_tenant_id`, `plan_id`, `entity_type`),
KEY `idx_de_business_fact_client` (`_tenant_id`, `client_key`, `device_id`, `synced_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字员工业务事实物化表';
CREATE TABLE IF NOT EXISTS `digital_employee_artifact_lifecycle` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`_tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`sync_record_id` BIGINT NOT NULL COMMENT '同步记录ID',
`client_key` VARCHAR(255) DEFAULT NULL COMMENT '客户端Key',
`device_id` VARCHAR(255) DEFAULT NULL COMMENT '设备ID',
`artifact_id` VARCHAR(255) NOT NULL COMMENT '产物ID',
`plan_id` VARCHAR(255) DEFAULT NULL COMMENT '计划ID',
`task_id` VARCHAR(255) DEFAULT NULL COMMENT '任务ID',
`run_id` VARCHAR(255) DEFAULT NULL COMMENT '运行ID',
`status` VARCHAR(128) DEFAULT NULL COMMENT '状态',
`retention_status` VARCHAR(128) DEFAULT NULL COMMENT '保留状态',
`cleanup_status` VARCHAR(128) DEFAULT NULL COMMENT '清理状态',
`deleted_at` VARCHAR(64) DEFAULT NULL COMMENT '删除时间',
`version_key` VARCHAR(255) DEFAULT NULL COMMENT '版本Key',
`source` VARCHAR(255) DEFAULT NULL COMMENT '来源',
`business_view` JSON DEFAULT NULL COMMENT '业务视图快照',
`synced_at` DATETIME DEFAULT NULL COMMENT '同步时间',
`modified` DATETIME NOT NULL COMMENT '修改时间',
`created` DATETIME NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_de_artifact_lifecycle_record` (`_tenant_id`, `sync_record_id`),
KEY `idx_de_artifact_lifecycle_artifact` (`_tenant_id`, `artifact_id`),
KEY `idx_de_artifact_lifecycle_plan` (`_tenant_id`, `plan_id`, `task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字员工产物生命周期物化表';
CREATE TABLE IF NOT EXISTS `digital_employee_daily_report_fact` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`_tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`sync_record_id` BIGINT NOT NULL COMMENT '同步记录ID',
`client_key` VARCHAR(255) DEFAULT NULL COMMENT '客户端Key',
`device_id` VARCHAR(255) DEFAULT NULL COMMENT '设备ID',
`report_id` VARCHAR(255) NOT NULL COMMENT '日报ID',
`daily_report_fact_id` VARCHAR(255) DEFAULT NULL COMMENT '日报事实ID',
`plan_id` VARCHAR(255) DEFAULT NULL COMMENT '计划ID',
`status` VARCHAR(128) DEFAULT NULL COMMENT '状态',
`physical_fact_status` VARCHAR(128) DEFAULT NULL COMMENT '物理事实状态',
`delivery_status` VARCHAR(128) DEFAULT NULL COMMENT '交付状态',
`approval_status` VARCHAR(128) DEFAULT NULL COMMENT '审批状态',
`approval_decision_status` VARCHAR(128) DEFAULT NULL COMMENT '审批裁决状态',
`confirmed_at` VARCHAR(64) DEFAULT NULL COMMENT '确认时间',
`report_date` VARCHAR(64) DEFAULT NULL COMMENT '日报日期',
`send_record_id` VARCHAR(255) DEFAULT NULL COMMENT '发送记录ID',
`send_channel` VARCHAR(128) DEFAULT NULL COMMENT '发送渠道',
`send_request_status` VARCHAR(128) DEFAULT NULL COMMENT '发送请求状态',
`send_record_status` VARCHAR(128) DEFAULT NULL COMMENT '发送记录状态',
`materialized_source` VARCHAR(128) DEFAULT NULL COMMENT '物化来源',
`business_view` JSON DEFAULT NULL COMMENT '业务视图快照',
`synced_at` DATETIME DEFAULT NULL COMMENT '同步时间',
`modified` DATETIME NOT NULL COMMENT '修改时间',
`created` DATETIME NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_de_daily_report_fact_record` (`_tenant_id`, `sync_record_id`),
KEY `idx_de_daily_report_fact_report` (`_tenant_id`, `report_id`),
KEY `idx_de_daily_report_fact_client` (`_tenant_id`, `client_key`, `device_id`, `synced_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字员工日报事实物化表';
-- Add digital employee admin operations workbench menu and resource.
-- Idempotent for existing tenants: creates menu/resource rows, binds menu to resource,
-- and grants super_admin default access. Other roles can be granted this menu
-- from the menu permission management UI.
INSERT INTO sys_resource (`parent_id`, `code`, `name`, `description`, `source`, `type`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
SELECT parent.id, 'content_digital_employee_ops', '数字员工运营台', '数字员工运营台', 1, 1, 91, 1, parent.`_tenant_id`, 0, 'system', 1
FROM sys_resource parent
WHERE parent.`code` = 'content_manage' AND parent.`yn` = 1
AND NOT EXISTS (
SELECT 1 FROM sys_resource r
WHERE r.`_tenant_id` = parent.`_tenant_id` AND r.`code` = 'content_digital_employee_ops' AND r.`yn` = 1
);
INSERT INTO sys_resource (`parent_id`, `code`, `name`, `description`, `source`, `type`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
SELECT parent.id, 'content_digital_employee_ops_query_list', '查询列表', '查询数字员工运营台', 1, 2, 1, 1, parent.`_tenant_id`, 0, 'system', 1
FROM sys_resource parent
WHERE parent.`code` = 'content_digital_employee_ops' AND parent.`yn` = 1
AND NOT EXISTS (
SELECT 1 FROM sys_resource r
WHERE r.`_tenant_id` = parent.`_tenant_id` AND r.`code` = 'content_digital_employee_ops_query_list' AND r.`yn` = 1
);
INSERT INTO sys_menu (`parent_id`, `code`, `name`, `description`, `source`, `path`, `open_type`, `icon`, `sort_index`, `status`, `_tenant_id`, `creator_id`, `creator`, `yn`)
SELECT parent.id, 'content_digital_employee_ops', '数字员工运营台', '数字员工运营台', 1, '/system/content/content-digital-employee-ops', 1, '', 91, 1, parent.`_tenant_id`, 0, 'system', 1
FROM sys_menu parent
WHERE parent.`code` = 'content_manage' AND parent.`yn` = 1
AND NOT EXISTS (
SELECT 1 FROM sys_menu m
WHERE m.`_tenant_id` = parent.`_tenant_id` AND m.`code` = 'content_digital_employee_ops' AND m.`yn` = 1
);
INSERT INTO sys_menu_resource (`menu_id`, `resource_id`, `resource_bind_type`, `_tenant_id`, `creator_id`, `creator`, `yn`)
SELECT m.id, r.id, 1, m.`_tenant_id`, 0, 'system', 1
FROM sys_menu m
JOIN sys_resource r ON r.`_tenant_id` = m.`_tenant_id` AND r.`code` = 'content_digital_employee_ops' AND r.`yn` = 1
WHERE m.`code` = 'content_digital_employee_ops' AND m.`yn` = 1
AND NOT EXISTS (
SELECT 1 FROM sys_menu_resource mr
WHERE mr.`_tenant_id` = m.`_tenant_id` AND mr.`menu_id` = m.id AND mr.`resource_id` = r.id AND mr.`yn` = 1
);
UPDATE sys_menu_resource mr
JOIN sys_menu m ON m.id = mr.menu_id AND m.`_tenant_id` = mr.`_tenant_id` AND m.`code` = 'content_digital_employee_ops' AND m.`yn` = 1
JOIN sys_resource r ON r.id = mr.resource_id AND r.`_tenant_id` = mr.`_tenant_id` AND r.`code` = 'content_digital_employee_ops' AND r.`yn` = 1
SET mr.`resource_bind_type` = 1,
mr.`modified` = CURRENT_TIMESTAMP
WHERE mr.`yn` = 1;
INSERT INTO sys_role_menu (`role_id`, `menu_id`, `menu_bind_type`, `resource_tree_json`, `_tenant_id`, `creator_id`, `creator`, `yn`)
SELECT role.id,
menu.id,
1,
JSON_ARRAY(JSON_OBJECT('id', resource.id, 'code', resource.`code`, 'resourceBindType', 1)),
menu.`_tenant_id`,
0,
'system',
1
FROM sys_role role
JOIN sys_menu menu ON menu.`_tenant_id` = role.`_tenant_id` AND menu.`code` = 'content_digital_employee_ops' AND menu.`yn` = 1
JOIN sys_resource resource ON resource.`_tenant_id` = role.`_tenant_id` AND resource.`code` = 'content_digital_employee_ops' AND resource.`yn` = 1
WHERE role.`code` = 'super_admin' AND role.`yn` = 1
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu rm
WHERE rm.`_tenant_id` = role.`_tenant_id` AND rm.`role_id` = role.id AND rm.`menu_id` = menu.id AND rm.`yn` = 1
);
UPDATE sys_role_menu rm
JOIN sys_role role ON role.id = rm.role_id AND role.`_tenant_id` = rm.`_tenant_id` AND role.`code` = 'super_admin' AND role.`yn` = 1
JOIN sys_menu menu ON menu.id = rm.menu_id AND menu.`_tenant_id` = rm.`_tenant_id` AND menu.`code` = 'content_digital_employee_ops' AND menu.`yn` = 1
JOIN sys_resource resource ON resource.`_tenant_id` = rm.`_tenant_id` AND resource.`code` = 'content_digital_employee_ops' AND resource.`yn` = 1
SET rm.`menu_bind_type` = 1,
rm.`resource_tree_json` = JSON_ARRAY(JSON_OBJECT('id', resource.id, 'code', resource.`code`, 'resourceBindType', 1)),
rm.`modified` = CURRENT_TIMESTAMP
WHERE rm.`yn` = 1;

View File

@@ -33,7 +33,8 @@
"serve": "http-server ./dist",
"setup": "max setup",
"start": "npm run dev",
"test": "vitest",
"test": "vitest run",
"test:watch": "vitest",
"verify-commit": "max verify-commit"
},
"lint-staged": {
@@ -140,14 +141,15 @@
"cross-env": "^7.0.3",
"esbuild": "^0.27.0",
"husky": "^9.1.7",
"jsdom": "^27.3.0",
"jest-axe": "^10.0.0",
"jsdom": "^22.1.0",
"lint-staged": "^13.2.0",
"prettier": "^2.8.7",
"prettier-plugin-organize-imports": "^3.2.2",
"prettier-plugin-packagejson": "^2.4.3",
"standard-version": "^9.5.0",
"typescript": "^5.0.3",
"vitest": "^4.0.15"
"vitest": "^0.34.6"
},
"packageManager": "pnpm@10.27.0",
"engines": {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,477 @@
import { XProTable } from '@/components/ProComponents';
import WorkspaceLayout from '@/components/WorkspaceLayout';
import { SUCCESS_CODE } from '@/constants/codes.constants';
import { apiDigitalEmployeeSyncRecordList } from '@/services/systemManage';
import type { DigitalEmployeeSyncRecordInfo } from '@/types/interfaces/systemManage';
import type {
ActionType,
FormInstance,
ProColumns,
} from '@ant-design/pro-components';
import { Button, Drawer, message, Space, Tag, Typography } from 'antd';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useModel, useSearchParams } from 'umi';
const ENTITY_TYPE_OPTIONS = {
plan: { text: 'Plan' },
task: { text: 'Task' },
run: { text: 'Run' },
event: { text: 'Event' },
artifact: { text: 'Artifact' },
approval: { text: 'Approval' },
};
const ENTITY_TYPE_TONE: Record<string, string> = {
plan: 'blue',
task: 'geekblue',
run: 'purple',
event: 'cyan',
artifact: 'green',
approval: 'orange',
};
const STATUS_TONE: Record<string, string> = {
running: 'processing',
pending: 'warning',
completed: 'success',
success: 'success',
synced: 'success',
failed: 'error',
error: 'error',
cancelled: 'default',
};
interface PayloadObject {
[key: string]: unknown;
}
interface SyncBusinessSummary {
title: string;
description: string;
status?: string;
statusTone: string;
meta: string[];
}
function formatPayload(payload?: string): string {
if (!payload) return '{}';
try {
return JSON.stringify(JSON.parse(payload), null, 2);
} catch {
return payload;
}
}
function parsePayload(payload?: string): unknown {
if (!payload) return null;
try {
return JSON.parse(payload);
} catch {
return null;
}
}
function isPayloadObject(value: unknown): value is PayloadObject {
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
}
function valueAsString(value: unknown): string {
if (typeof value === 'string') return value.trim();
if (typeof value === 'number' || typeof value === 'boolean')
return String(value);
return '';
}
function pickString(payload: PayloadObject, keys: string[]): string {
for (const key of keys) {
const value = valueAsString(payload[key]);
if (value) return value;
}
return '';
}
function compactSummaryText(value: string, maxLength = 96): string {
const text = value.replace(/\s+/g, ' ').trim();
return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
}
function maskClientKey(value?: string): string {
const key = value?.trim();
if (!key) return '--';
if (key.length <= 4) return '****';
if (key.length <= 10) return `${key.slice(0, 2)}****${key.slice(-2)}`;
return `${key.slice(0, 4)}...${key.slice(-4)}`;
}
function entityLabel(entityType: string): string {
return (
ENTITY_TYPE_OPTIONS[entityType as keyof typeof ENTITY_TYPE_OPTIONS]?.text ||
entityType ||
'Entity'
);
}
function statusTone(status?: string): string {
return status ? STATUS_TONE[status.toLowerCase()] || 'default' : 'default';
}
function buildBusinessSummary(
record: DigitalEmployeeSyncRecordInfo,
): SyncBusinessSummary {
const payload = parsePayload(record.payload);
const entity = entityLabel(record.entity_type);
if (!isPayloadObject(payload)) {
return {
title: `${entity} ${record.entity_id}`,
description: 'Payload 暂无法解析,查看详情可读取原始内容。',
status: record.operation,
statusTone: 'default',
meta: [record.outbox_id],
};
}
const status = pickString(payload, ['status', 'currentStatus', 'kind']);
const title =
pickString(payload, ['title', 'name', 'message', 'objective']) ||
`${entity} ${record.entity_id}`;
const description = compactSummaryText(
pickString(payload, ['objective', 'summary', 'message', 'uri']) ||
record.outbox_id,
);
const meta = [
pickString(payload, ['planId', 'plan_id']) &&
`Plan ${pickString(payload, ['planId', 'plan_id'])}`,
pickString(payload, ['taskId', 'task_id']) &&
`Task ${pickString(payload, ['taskId', 'task_id'])}`,
pickString(payload, ['runId', 'run_id']) &&
`Run ${pickString(payload, ['runId', 'run_id'])}`,
pickString(payload, ['assignedSkillId']) &&
`Skill ${pickString(payload, ['assignedSkillId'])}`,
pickString(payload, ['occurred_at', 'started_at', 'finished_at']),
].filter((item): item is string => Boolean(item));
return {
title: compactSummaryText(title, 72),
description,
status: status || record.operation,
statusTone: statusTone(status),
meta: meta.slice(0, 3),
};
}
function renderBusinessSummary(record: DigitalEmployeeSyncRecordInfo) {
const summary = buildBusinessSummary(record);
return (
<div style={{ minWidth: 0 }}>
<Space size={6} wrap>
<Typography.Text strong ellipsis style={{ maxWidth: 260 }}>
{summary.title}
</Typography.Text>
{summary.status && (
<Tag color={summary.statusTone}>{summary.status}</Tag>
)}
</Space>
<Typography.Text
type="secondary"
ellipsis
style={{ display: 'block', maxWidth: 330, fontSize: 12 }}
>
{summary.description}
</Typography.Text>
{summary.meta.length > 0 && (
<Space size={[4, 4]} wrap style={{ marginTop: 4 }}>
{summary.meta.map((item) => (
<Tag key={item} color="default" style={{ marginInlineEnd: 0 }}>
{item}
</Tag>
))}
</Space>
)}
</div>
);
}
function readSearchValue(
params: URLSearchParams,
key: string,
): string | undefined {
const value = params.get(key)?.trim();
return value || undefined;
}
const DigitalEmployeeSync: React.FC = () => {
const { hasPermission } = useModel('menuModel');
const actionRef = useRef<ActionType>();
const formRef = useRef<FormInstance>();
const autoOpenedOutboxRef = useRef<string | null>(null);
const location = useLocation();
const [, setSearchParams] = useSearchParams();
const [payloadRecord, setPayloadRecord] =
useState<DigitalEmployeeSyncRecordInfo | null>(null);
const formattedPayload = useMemo(
() => (payloadRecord ? formatPayload(payloadRecord.payload) : ''),
[payloadRecord],
);
const formInitialValues = useMemo(() => {
const params = new URLSearchParams(location.search || '');
const values = {
client_key: readSearchValue(params, 'client_key'),
device_id: readSearchValue(params, 'device_id'),
entity_type: readSearchValue(params, 'entity_type'),
entity_id: readSearchValue(params, 'entity_id'),
outbox_id: readSearchValue(params, 'outbox_id'),
};
return Object.values(values).some(Boolean) ? values : undefined;
}, [location.search]);
const handleReset = useCallback(() => {
formRef.current?.resetFields();
formRef.current?.setFieldsValue({
client_key: undefined,
device_id: undefined,
entity_type: undefined,
entity_id: undefined,
outbox_id: undefined,
});
actionRef.current?.setPageInfo?.({ current: 1, pageSize: 15 });
autoOpenedOutboxRef.current = null;
if (location.search) {
setSearchParams(new URLSearchParams());
return;
}
actionRef.current?.reload();
}, [location.search, setSearchParams]);
useEffect(() => {
autoOpenedOutboxRef.current = null;
}, [location.search]);
useEffect(() => {
const state = location.state as any;
if (state?._t) {
handleReset();
}
}, [location.state, handleReset]);
const columns: ProColumns<DigitalEmployeeSyncRecordInfo>[] = useMemo(
() => [
{
title: '客户端 Key',
dataIndex: 'client_key',
width: 190,
ellipsis: true,
renderText: (value) => maskClientKey(value),
fieldProps: {
placeholder: '输入客户端 Key',
allowClear: true,
},
},
{
title: '设备 ID',
dataIndex: 'device_id',
width: 150,
ellipsis: true,
fieldProps: {
placeholder: '输入设备 ID',
allowClear: true,
},
renderText: (value) => value || '--',
},
{
title: '实体类型',
dataIndex: 'entity_type',
width: 120,
valueType: 'select',
valueEnum: ENTITY_TYPE_OPTIONS,
fieldProps: {
placeholder: '选择实体类型',
allowClear: true,
},
render: (_, record) => (
<Tag color={ENTITY_TYPE_TONE[record.entity_type] || 'default'}>
{record.entity_type || '--'}
</Tag>
),
},
{
title: '实体 ID',
dataIndex: 'entity_id',
width: 180,
ellipsis: true,
fieldProps: {
placeholder: '输入实体 ID',
allowClear: true,
},
},
{
title: '业务摘要',
dataIndex: 'payload',
width: 360,
hideInSearch: true,
render: (_, record) => renderBusinessSummary(record),
},
{
title: 'Outbox ID',
dataIndex: 'outbox_id',
width: 220,
ellipsis: true,
fieldProps: {
placeholder: '输入 Outbox ID',
allowClear: true,
},
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
width: 90,
hideInSearch: true,
renderText: (value) => value || '--',
},
{
title: '同步时间',
dataIndex: 'synced_at',
align: 'center',
width: 170,
hideInSearch: true,
valueType: 'dateTime',
},
{
title: '创建时间',
dataIndex: 'created',
align: 'center',
width: 170,
hideInSearch: true,
valueType: 'dateTime',
},
{
title: '详情',
valueType: 'option',
fixed: 'right',
align: 'center',
width: 90,
render: (_, record) => (
<Button type="link" onClick={() => setPayloadRecord(record)}>
</Button>
),
},
],
[],
);
const request = async (params: {
pageSize?: number;
current?: number;
client_key?: string;
device_id?: string;
entity_type?: string;
entity_id?: string;
outbox_id?: string;
}) => {
const response = await apiDigitalEmployeeSyncRecordList({
pageNo: params.current || 1,
pageSize: params.pageSize || 15,
clientKey: params.client_key,
deviceId: params.device_id,
entityType: params.entity_type,
entityId: params.entity_id,
outboxId: params.outbox_id,
});
if (response.code !== SUCCESS_CODE) {
message.error(response.message || '查询数字员工同步记录失败');
}
const records = response.data?.records || [];
const outboxId = params.outbox_id?.trim();
const matchedRecord = outboxId
? records.find((record) => record.outbox_id === outboxId)
: null;
if (
response.code === SUCCESS_CODE &&
outboxId &&
matchedRecord &&
autoOpenedOutboxRef.current !== outboxId
) {
autoOpenedOutboxRef.current = outboxId;
setPayloadRecord(matchedRecord);
}
return {
data: records,
total: response.data?.total || 0,
success: response.code === SUCCESS_CODE,
};
};
return (
<WorkspaceLayout title="数字员工同步记录" hideScroll>
<XProTable<DigitalEmployeeSyncRecordInfo>
key={location.pathname + (location.search || '')}
actionRef={actionRef}
formRef={formRef}
rowKey="id"
columns={columns}
request={request}
onReset={handleReset}
form={
formInitialValues ? { initialValues: formInitialValues } : undefined
}
showQueryButtons={hasPermission(
'content_digital_employee_sync_query_list',
)}
/>
<Drawer
width={720}
title="同步详情"
open={Boolean(payloadRecord)}
onClose={() => setPayloadRecord(null)}
>
{payloadRecord && (
<>
<div style={{ marginBottom: 16 }}>
{renderBusinessSummary(payloadRecord)}
</div>
<Typography.Paragraph>
<Typography.Text strong> Key</Typography.Text>
{maskClientKey(payloadRecord.client_key)}
</Typography.Paragraph>
<Typography.Paragraph>
<Typography.Text strong> ID</Typography.Text>
{payloadRecord.device_id || '--'}
</Typography.Paragraph>
<Typography.Paragraph>
<Typography.Text strong></Typography.Text>
{payloadRecord.entity_type} / {payloadRecord.entity_id}
</Typography.Paragraph>
<Typography.Paragraph>
<Typography.Text strong>Outbox</Typography.Text>
{payloadRecord.outbox_id}
</Typography.Paragraph>
<Typography.Paragraph copyable={{ text: formattedPayload }}>
<pre
style={{
maxHeight: 'calc(100vh - 260px)',
overflow: 'auto',
margin: 0,
padding: 16,
background: '#f6f8fa',
border: '1px solid #e5e7eb',
borderRadius: 6,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{formattedPayload}
</pre>
</Typography.Paragraph>
</>
)}
</Drawer>
</WorkspaceLayout>
);
};
export default DigitalEmployeeSync;

View File

@@ -351,6 +351,18 @@ const routes = [
name: getRouteLabel('PC.Routes.contentSkill'),
component: '@/pages/SystemManagement/Content/Skill',
},
{
path: 'content-digital-employee-sync',
name: '数字员工同步记录',
component:
'@/pages/SystemManagement/Content/DigitalEmployeeSync',
},
{
path: 'content-digital-employee-ops',
name: '数字员工运营台',
component:
'@/pages/SystemManagement/Content/DigitalEmployeeOps',
},
],
},
{

View File

@@ -53,6 +53,10 @@ const MENU_ICON_MAP: Record<string, string> = {
system_log_query: 'icons-nav-log',
// 内容管理
content_manage: 'icons-nav-cube',
// 数字员工同步记录
content_digital_employee_sync: 'icons-nav-robot',
// 数字员工运营台
content_digital_employee_ops: 'icons-nav-robot',
// 支付与收益(开发者)
dev_payment_earnings: 'icons-nav-my-earnings',
// 订阅与积分(管理员)

View File

@@ -7,7 +7,19 @@ import type { Page, RequestResponse } from '@/types/interfaces/request';
import type {
AccessStatsResult,
AddSystemUserParams,
DigitalEmployeeAdminActionResult,
DigitalEmployeeAdminActionSubmitParams,
DigitalEmployeeAdminRecordResult,
DigitalEmployeeAdminRecordSubmitParams,
ConversationStatsResult,
DigitalEmployeeBillingEstimate,
DigitalEmployeeBillingEstimateParams,
DigitalEmployeeAdminWorkbenchListParams,
DigitalEmployeeAdminWorkbenchPage,
DigitalEmployeePolicySnapshotListParams,
DigitalEmployeePolicySnapshotPage,
DigitalEmployeeSyncRecordListParams,
DigitalEmployeeSyncRecordPage,
ModelConfigDto,
NotifyMessageSendParams,
OnlineMonitorInfo,
@@ -264,6 +276,142 @@ export async function apiSystemResourceAgentDelete(data: {
});
}
// 查询数字员工同步记录
export async function apiDigitalEmployeeSyncRecordList(
data: DigitalEmployeeSyncRecordListParams,
): Promise<RequestResponse<DigitalEmployeeSyncRecordPage>> {
return request('/api/digital-employee/sync/records', {
method: 'GET',
params: {
client_key: data.clientKey,
device_id: data.deviceId,
entity_type: data.entityType,
entity_id: data.entityId,
outbox_id: data.outboxId,
page_no: data.pageNo,
page_size: data.pageSize,
},
});
}
// 查询数字员工正式运营台
export async function apiDigitalEmployeeAdminWorkbenchList(
data: DigitalEmployeeAdminWorkbenchListParams,
): Promise<RequestResponse<DigitalEmployeeAdminWorkbenchPage>> {
return request(`/api/digital-employee/admin/workbench/${data.view}`, {
method: 'GET',
params: {
client_key: data.clientKey,
device_id: data.deviceId,
entity_type: data.entityType,
entity_id: data.entityId,
status: data.status,
kind: data.kind,
category: data.category,
level: data.level,
priority: data.priority,
updated_from: data.updatedFrom,
updated_to: data.updatedTo,
page_no: data.pageNo,
page_size: data.pageSize,
},
});
}
// 查询数字员工跨设备策略快照冲突
export async function apiDigitalEmployeeAdminPolicySnapshots(
data: DigitalEmployeePolicySnapshotListParams,
): Promise<RequestResponse<DigitalEmployeePolicySnapshotPage>> {
return request('/api/digital-employee/admin/policies/snapshots', {
method: 'GET',
params: {
client_key: data.clientKey,
device_id: data.deviceId,
status: data.status,
policy_kind: data.policyKind,
updated_from: data.updatedFrom,
updated_to: data.updatedTo,
page_no: data.pageNo,
page_size: data.pageSize,
},
});
}
// 记录数字员工管理端低风险动作意图
export async function apiDigitalEmployeeAdminActionSubmit(
data: DigitalEmployeeAdminActionSubmitParams,
): Promise<RequestResponse<DigitalEmployeeAdminActionResult>> {
return request('/api/digital-employee/admin/actions', {
method: 'POST',
data: {
sync_record_id: data.syncRecordId,
client_key: data.clientKey,
device_id: data.deviceId,
entity_type: data.entityType,
entity_id: data.entityId,
action: data.action,
request_payload: data.requestPayload,
},
});
}
// 记录数字员工日报发送结果
export async function apiDigitalEmployeeAdminDailyReportSendRecord(
data: DigitalEmployeeAdminRecordSubmitParams,
): Promise<RequestResponse<DigitalEmployeeAdminRecordResult>> {
return request('/api/digital-employee/admin/daily-reports/send-records', {
method: 'POST',
data: {
sync_record_id: data.syncRecordId,
client_key: data.clientKey,
device_id: data.deviceId,
entity_type: data.entityType,
entity_id: data.entityId,
status: data.status,
endpoint: data.endpoint,
remote_id: data.remoteId,
reason: data.reason,
request_payload: data.requestPayload,
},
});
}
// 查询数字员工账单估算
export async function apiDigitalEmployeeAdminBillingEstimates(
data: DigitalEmployeeBillingEstimateParams,
): Promise<RequestResponse<DigitalEmployeeBillingEstimate>> {
return request('/api/digital-employee/admin/billing/estimates', {
method: 'GET',
params: {
client_key: data.clientKey,
device_id: data.deviceId,
updated_from: data.updatedFrom,
updated_to: data.updatedTo,
},
});
}
// 记录数字员工审计归档摘要
export async function apiDigitalEmployeeAdminAuditArchive(
data: DigitalEmployeeAdminRecordSubmitParams,
): Promise<RequestResponse<DigitalEmployeeAdminRecordResult>> {
return request('/api/digital-employee/admin/audit-archives', {
method: 'POST',
data: {
sync_record_id: data.syncRecordId,
client_key: data.clientKey,
device_id: data.deviceId,
entity_type: data.entityType,
entity_id: data.entityId,
status: data.status,
archive_location: data.archiveLocation,
archive_summary: data.archiveSummary,
reason: data.reason,
request_payload: data.requestPayload,
},
});
}
// 查询网页应用列表
export async function apiSystemResourceWebappList(
data: SystemWebappListParams,

View File

@@ -516,6 +516,430 @@ export interface SystemAgentListParams extends SystemPaginationParams {
// 智能体列表分页响应
export type SystemAgentPage = SystemPageResult<SystemAgentInfo>;
// 数字员工同步记录
export interface DigitalEmployeeSyncRecordInfo {
id: number;
client_key: string;
device_id?: string;
outbox_id: string;
entity_type:
| 'plan'
| 'task'
| 'run'
| 'event'
| 'artifact'
| 'approval'
| string;
entity_id: string;
operation: string;
payload: string;
synced_at: string;
modified: string;
created: string;
}
// 数字员工同步记录查询参数
export interface DigitalEmployeeSyncRecordListParams
extends SystemPaginationParams {
clientKey?: string;
deviceId?: string;
entityType?: string;
entityId?: string;
outboxId?: string;
}
// 数字员工同步记录分页响应
export interface DigitalEmployeeSyncRecordPage {
total: number;
page_no: number;
page_size: number;
records: DigitalEmployeeSyncRecordInfo[];
}
export type DigitalEmployeeAdminWorkbenchView =
| 'business'
| 'interventions'
| 'approval_governance'
| 'approval_policies'
| 'service_operations'
| 'artifact_lifecycle'
| 'notifications'
| 'notification_policies'
| 'audits'
| 'daily_reports'
| 'governance_commands'
| 'route_governance'
| 'policy_conflicts'
| 'memory_governance'
| 'memory_policies'
| 'skill_audits'
| 'policy_center'
| 'scheduler_operations'
| 'plan_step_dispatch'
| 'plan_governance'
| 'plan_revisions'
| 'governance_policies'
| 'plan_revision_materialization';
export interface DigitalEmployeeAdminWorkbenchRow {
view: DigitalEmployeeAdminWorkbenchView | string;
sync_record_id: number;
sync_record_url?: string;
entity_type?: string;
entity_id?: string;
operation?: string;
title?: string;
status?: string;
kind?: string;
category?: string;
level?: string;
priority?: string;
summary?: string;
plan_id?: string;
task_id?: string;
run_id?: string;
step_id?: string;
dependency_status?: string;
lease_state?: string;
lease_source?: string;
dispatch_status?: string;
dispatch_policy_key?: string;
max_per_sweep?: number;
auto_dispatch_ready_steps?: boolean;
catch_up_on_startup?: boolean;
max_run_history?: number;
orchestration_id?: string;
trigger_source?: string;
approval_id?: string;
approval_policy_id?: string;
conflict_policy_id?: string;
decision_guard_status?: string;
batch_accept_remote_disabled?: boolean;
auto_decision_disabled?: boolean;
policy_reason?: string;
governance_policy_id?: string;
policy_guard_status?: string;
hard_interrupt_disabled?: boolean;
artifact_id?: string;
artifact_graph_id?: string;
artifact_group_id?: string;
group_key?: string;
related_artifact_ids?: string[];
artifact_count?: number;
device_count?: number;
version_count?: number;
latest_artifact_id?: string;
version_key?: string;
cleanup_request_id?: string;
cleanup_eligible_count?: number;
batch_cleanup_disabled?: boolean;
retention_conflict_status?: string;
cleanup_conflict_status?: string;
remote_delete_disabled?: boolean;
notification_id?: string;
collaboration_thread_id?: string;
subscription_policy_id?: string;
subscription_conflict_status?: string;
notification_policy_id?: string;
subscription_status?: string;
desktop_enabled?: boolean;
minimum_level?: string;
channel?: string;
delivery_status?: string;
delivery_attempt_id?: string;
send_request_id?: string;
real_push_disabled?: boolean;
conflict_status?: string;
report_id?: string;
daily_report_fact_id?: string;
daily_report_table?: string;
daily_report_materialized?: boolean;
daily_report_physical_fact_status?: string;
send_record_id?: string;
send_channel?: string;
send_request_status?: string;
send_record_status?: string;
formal_daily_report_page_disabled?: boolean;
external_send_disabled?: boolean;
third_party_channel_disabled?: boolean;
approval_decision_status?: string;
advanced_query_supported?: boolean;
aggregation_scope?: string;
materialized_source?: string;
daily_report_approval_id?: string;
daily_report_approval_action?: string;
daily_report_approval_actor?: string;
daily_report_approval_reason?: string;
daily_report_approval_recorded_at?: string;
route_decision_id?: string;
service_id?: string;
service_lease_id?: string;
lease_holder?: string;
lease_expires_at?: string;
restart_record_id?: string;
restart_count?: number;
last_restart_at?: string;
recovery_policy_id?: string;
auto_recovery_status?: string;
service_start_disabled?: boolean;
service_stop_disabled?: boolean;
lease_takeover_disabled?: boolean;
auto_recovery_disabled?: boolean;
recovery_action_request_id?: string;
start_request_id?: string;
stop_request_id?: string;
lease_takeover_request_id?: string;
auto_recovery_request_id?: string;
start_service_disabled?: boolean;
stop_service_disabled?: boolean;
memory_id?: string;
memory_key?: string;
governance_status?: string;
merge_candidate_count?: number;
duplicate_memory_ids?: string[];
latest_memory_id?: string;
last_governance_action?: string;
last_reviewed_at?: string;
memory_policy_id?: string;
memory_policy_status?: string;
merge_policy_id?: string;
merge_action_disabled?: boolean;
remote_memory_policy_disabled?: boolean;
skill_id?: string;
skill_version?: string;
install_status?: string;
permission_scope?: string;
policy_source?: string;
policy_status?: string;
skill_install_request_id?: string;
skill_upgrade_request_id?: string;
permission_policy_id?: string;
permission_edit_request_id?: string;
install_action_disabled?: boolean;
upgrade_action_disabled?: boolean;
permission_edit_disabled?: boolean;
tool_name?: string;
server_id?: string;
audit_archive_id?: string;
archive_status?: string;
archive_location?: string;
billing_estimate_id?: string;
billing_source?: string;
estimated?: boolean;
diagnostic_id?: string;
diagnostic_kind?: string;
issue_code?: string;
repair_action?: string;
repair_status?: string;
repair_reason?: string;
safe_repair_disabled?: boolean;
billing_actual_disabled?: boolean;
sandbox_archive_file_disabled?: boolean;
auto_repair_execution_disabled?: boolean;
repair_approval_required?: boolean;
recommended_action?: string;
policy_kind?: string;
policy_key?: string;
policy_edit_request_id?: string;
policy_edit_status?: string;
policy_edit_reason?: string;
merge_request_id?: string;
memory_merge_request_id?: string;
remote_memory_policy_request_id?: string;
merge_recommendation?: string;
remote_policy_edit_disabled?: boolean;
auto_merge_disabled?: boolean;
approval_policy_edit_disabled?: boolean;
route_policy_edit_disabled?: boolean;
route_recovery_disabled?: boolean;
high_risk_execution_disabled?: boolean;
schedule_id?: string;
job_id?: string;
revision_id?: string;
revision_fact_id?: string;
revision_status?: string;
revision_materialized?: boolean;
base_revision_id?: string;
changed_fields?: string[];
conflict_keys?: string[];
recommended_resolution?: string;
route_decision_count?: number;
blocked_count?: number;
approval_required_count?: number;
missing_context_count?: number;
not_mapped_count?: number;
intervention_conflict_status?: string;
occurred_at?: string;
synced_at?: string;
latest_synced_at?: string;
materialized?: boolean;
client_key_masked?: string;
device_id?: string;
business_view?: Record<string, unknown>;
payload?: Record<string, unknown>;
}
export interface DigitalEmployeeAdminWorkbenchListParams
extends SystemPaginationParams {
view: DigitalEmployeeAdminWorkbenchView;
clientKey?: string;
deviceId?: string;
entityType?: string;
entityId?: string;
status?: string;
kind?: string;
category?: string;
level?: string;
priority?: string;
updatedFrom?: string;
updatedTo?: string;
}
export interface DigitalEmployeeAdminWorkbenchPage {
total: number;
page_no: number;
page_size: number;
records: DigitalEmployeeAdminWorkbenchRow[];
}
export interface DigitalEmployeePolicySnapshotRow {
policy_kind?: string;
policy_key?: string;
status?: string;
summary?: string;
client_count?: number;
device_count?: number;
conflict_keys?: string[];
latest_synced_at?: string;
recommended_resolution?: string;
route_decision_count?: number;
blocked_count?: number;
approval_required_count?: number;
missing_context_count?: number;
not_mapped_count?: number;
intervention_conflict_status?: string;
memory_policy_id?: string;
memory_policy_status?: string;
merge_policy_id?: string;
merge_action_disabled?: boolean;
remote_memory_policy_disabled?: boolean;
approval_policy_id?: string;
conflict_policy_id?: string;
decision_guard_status?: string;
batch_accept_remote_disabled?: boolean;
auto_decision_disabled?: boolean;
policy_reason?: string;
policy_edit_request_id?: string;
policy_edit_status?: string;
policy_edit_reason?: string;
merge_request_id?: string;
merge_recommendation?: string;
remote_policy_edit_disabled?: boolean;
auto_merge_disabled?: boolean;
approval_policy_edit_disabled?: boolean;
route_policy_edit_disabled?: boolean;
route_recovery_disabled?: boolean;
high_risk_execution_disabled?: boolean;
dispatch_policy_key?: string;
max_per_sweep?: number;
auto_dispatch_ready_steps?: boolean;
catch_up_on_startup?: boolean;
max_run_history?: number;
sync_record_url?: string;
client_key_masked?: string;
device_id?: string;
business_view?: Record<string, unknown>;
}
export interface DigitalEmployeePolicySnapshotListParams
extends SystemPaginationParams {
clientKey?: string;
deviceId?: string;
status?: string;
policyKind?: string;
updatedFrom?: string;
updatedTo?: string;
}
export interface DigitalEmployeePolicySnapshotPage {
total: number;
page_no: number;
page_size: number;
records: DigitalEmployeePolicySnapshotRow[];
}
export interface DigitalEmployeeAdminActionSubmitParams {
syncRecordId?: number;
clientKey?: string;
deviceId?: string;
entityType?: string;
entityId?: string;
action: string;
requestPayload?: Record<string, unknown>;
}
export interface DigitalEmployeeAdminActionResult {
ok: boolean;
status?: string;
reason_codes?: string[];
action?: {
id: number;
status: string;
action: string;
entity_type?: string;
entity_id?: string;
};
}
export interface DigitalEmployeeAdminRecordSubmitParams {
syncRecordId?: number;
clientKey?: string;
deviceId?: string;
entityType?: string;
entityId?: string;
status?: string;
endpoint?: string;
remoteId?: string;
archiveLocation?: string;
archiveSummary?: Record<string, unknown>;
reason?: string;
requestPayload?: Record<string, unknown>;
}
export interface DigitalEmployeeAdminRecordResult {
ok: boolean;
status?: string;
reason_codes?: string[];
record?: DigitalEmployeeAdminActionResult['action'];
}
export interface DigitalEmployeeBillingEstimateParams {
clientKey?: string;
deviceId?: string;
updatedFrom?: string;
updatedTo?: string;
}
export interface DigitalEmployeeBillingModelEstimate {
total_tokens?: number;
input_tokens?: number;
output_tokens?: number;
estimated_cost_usd?: number | string;
request_count?: number;
}
export interface DigitalEmployeeBillingEstimate {
total_tokens?: number;
input_tokens?: number;
output_tokens?: number;
estimated_cost_usd?: number | string;
request_count?: number;
estimated?: boolean;
by_model?: Record<string, DigitalEmployeeBillingModelEstimate>;
updated_at?: string;
source_records?: number[];
}
// 网页应用信息
export type SystemWebappInfo = SystemResourceInfo;

View File

@@ -1,17 +1,25 @@
/**
* Axe smoke coverage for low-level shared UI components.
*/
import {
ActionMenu,
CopyButton,
CopyIconButton,
MenuListItem,
SecondMenuItem,
} from '@/components/base';
import ActionMenu from '@/components/base/ActionMenu';
import CopyButton from '@/components/base/CopyButton';
import CopyIconButton from '@/components/base/CopyIconButton';
import MenuListItem from '@/components/base/MenuListItem';
import SecondMenuItem from '@/components/base/SecondMenuItem';
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
import { describe, expect, it, vi } from 'vitest';
vi.mock('@/components/base/SvgIcon', () => ({
default: ({ name }: { name: string }) => (
<span aria-hidden="true" data-icon={name} />
),
}));
vi.mock('@/services/i18nRuntime', () => ({
dict: (key: string) => key,
}));
const assertNoViolations = async (element: HTMLElement) => {
const results = await axe(element);
expect(results.violations).toEqual([]);

View File

@@ -1,4 +1,52 @@
import '@testing-library/jest-dom';
import { TextDecoder, TextEncoder } from 'node:util';
Object.assign(globalThis, { TextDecoder, TextEncoder });
if (typeof window !== 'undefined') {
Object.assign(window, { TextDecoder, TextEncoder });
}
class MemoryStorage implements Storage {
private readonly store = new Map<string, string>();
get length() {
return this.store.size;
}
clear() {
this.store.clear();
}
getItem(key: string) {
return this.store.get(key) ?? null;
}
key(index: number) {
return Array.from(this.store.keys())[index] ?? null;
}
removeItem(key: string) {
this.store.delete(key);
}
setItem(key: string, value: string) {
this.store.set(key, String(value));
}
}
const ensureStorage = (name: 'localStorage' | 'sessionStorage') => {
const current = globalThis[name];
if (current) return;
Object.defineProperty(globalThis, name, {
configurable: true,
value: new MemoryStorage(),
});
};
ensureStorage('localStorage');
ensureStorage('sessionStorage');
// antd 响应式依赖 matchMedia测试环境补齐
if (typeof window !== 'undefined' && !window.matchMedia) {

View File

@@ -11,6 +11,11 @@ export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
environmentOptions: {
jsdom: {
url: 'http://localhost/',
},
},
setupFiles: './tests/setupTests.ts',
exclude: [...configDefaults.exclude, 'tests/**/*[Vv]2*.test.{ts,tsx}'],
alias: {

View File

@@ -0,0 +1,3 @@
node_modules/
dist/
*.tsbuildinfo

View File

@@ -0,0 +1,24 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="./logo.png" />
<title>数字员工</title>
<script>
window.__QIMINGCLAW_EMBEDDED_DIGITAL__ = true;
try {
window.localStorage.setItem("qimingclaw_digital_token", "qimingclaw_embedded_digital_employee");
window.localStorage.removeItem("zeroclaw_token");
window.sessionStorage.setItem("qimingclaw_digital_logged_in", "1");
window.sessionStorage.removeItem("sgrobot_logged_in");
} catch (error) {
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
{
"name": "@qiming-ai/sgrobot-digital-embedded",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"build": "tsc -b && vite build",
"dev": "vite"
},
"dependencies": {
"animejs": "^4.4.1",
"lucide-react": "^0.468.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.1.1",
"remark-gfm": "^4.0.1"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.0",
"@types/node": "^25.3.0",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"tailwindcss": "^4.0.0",
"typescript": "~5.7.2",
"vite": "^6.0.7"
}
}

View File

@@ -0,0 +1,13 @@
# qimingclaw embedded patches
This directory records qimingclaw-only adaptations around the sgRobot digital employee UI.
- `src/main.tsx` mounts the digital employee page directly at `#/digital`.
- `src/lib/basePath.ts` fixes static assets under `/sgrobot-digital`.
- `src/lib/tauri.ts` disables sgRobot/Tauri gateway detection inside the Electron webview.
- `src/lib/qimingclawAdapter.ts` provides the first qimingclaw-native digital employee API projection.
- `src/lib/api.ts` calls the qimingclaw adapter before falling back to normal HTTP fetch.
- `index.html` injects a local token so copied sgRobot pages do not show the pairing screen.
- `window.QimingClawBridge.digital.getSnapshot()` is read when the page runs inside qimingclaw's Electron webview.
Run `npm run sync:sgrobot-digital` from `crates/agent-electron-client` after changing the upstream sgRobot digital UI.

View File

@@ -0,0 +1,7 @@
# Digital Employee Static Assets
This directory is owned by the React digital employee frontend.
- `avatars/` keeps the original digital employee character images used by the home page `Avatar3D` stage.
- Do not place the legacy Vue standalone application bundle here.
- If an avatar file is renamed, update `src/pages/digital/CommandDeck.tsx` in the same change.

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@@ -0,0 +1,29 @@
import Gem from './Gem';
import Stage from './Stage';
interface Avatar3DProps {
avatarSrc: string;
altText?: string;
statusLabel?: string;
}
export default function Avatar3D({ avatarSrc, altText = '数字员工', statusLabel }: Avatar3DProps) {
return (
<div className="de-avatar-container" aria-hidden="true">
<div className="de-avatar-glass-space">
<div className="de-avatar-roof">
<div className="de-avatar-roof-face" />
<div className="de-avatar-roof-glow" />
</div>
<div className="de-avatar-glass-left" />
<div className="de-avatar-glass-right" />
<Gem className="de-gem-1" />
<Gem className="de-gem-2" />
<Gem className="de-gem-3" />
</div>
{statusLabel && <div className="de-avatar-state-label">{statusLabel}</div>}
<img src={avatarSrc} alt={altText} className="de-avatar-img" />
<Stage />
</div>
);
}

View File

@@ -0,0 +1,15 @@
import type { ReactNode } from 'react';
interface CardTitleBarProps {
title: string;
children?: ReactNode;
}
export default function CardTitleBar({ title, children }: CardTitleBarProps) {
return (
<div className="de-card-title-bar">
<span className="de-card-title-bar-text">{title}</span>
{children}
</div>
);
}

View File

@@ -0,0 +1,686 @@
import { useEffect, useMemo, useState } from 'react';
import {
buildDemoFlowEvents,
buildDemoPlanMessages,
buildDemoRuns,
buildDemoTasks,
getDemoMissions,
} from '@/pages/digital/demoScenario';
import {
displayBackendMessage,
displayBusinessText,
displayEventKind,
displayStatus,
type Mission,
} from '@/lib/consoleDataAdapter';
import { getDebugPlanFlow, getDebugTasksForPlan, getPlanMessages, type PlanMessage } from '@/lib/api';
import type { DebugRun, DebugTask, FlowResponse } from '@/types/api';
import StatusBadge from './StatusBadge';
interface FlowDetailPanelProps {
mission: Mission;
onClose: () => void;
}
function stepVariant(status: string): 'info' | 'success' | 'warning' | 'danger' {
switch (status) {
case 'succeeded': case 'completed': case 'Succeeded': return 'success';
case 'failed': return 'danger';
case 'Failed': return 'danger';
case 'running': case 'active': case 'Running': return 'info';
default: return 'warning';
}
}
function fmtTime(ts?: string | null): string {
if (!ts) return '未记录';
return new Date(ts).toLocaleString('zh-CN', { hour12: false });
}
function missionPeriodLabel(mission: Mission): string {
const text = `${mission.id} ${mission.title} ${mission.objective}`.toLowerCase();
if (text.includes('month') || text.includes('monthly') || text.includes('月')) return '月度任务';
if (text.includes('week') || text.includes('weekly') || text.includes('周')) return '周任务';
if (text.includes('临时') || text.includes('adhoc') || text.includes('hotlist') || text.includes('monitor')) return '临时任务';
return '日常任务';
}
function isDone(status: string): boolean {
return ['succeeded', 'success', 'finished', 'complete', 'completed'].includes(status.toLowerCase());
}
function isFailed(status: string): boolean {
return ['failed', 'error', 'cancelled', 'canceled', 'aborted', 'rejected', 'deadletter'].includes(status.toLowerCase());
}
function progressText(mission: Mission): string {
const total = mission.steps.length;
if (total === 0) return displayStatus(mission.status);
const done = mission.steps.filter((step) => isDone(step.status)).length;
const failed = mission.steps.filter((step) => isFailed(step.status)).length;
const running = mission.steps.filter((step) => ['running', 'active', 'queued', 'leased'].includes(step.status.toLowerCase())).length;
const parts = [`已完成 ${done}/${total}`];
if (failed > 0) parts.push(`失败 ${failed}`);
if (running > 0 && failed === 0) parts.push(`处理中 ${running}`);
return parts.join('');
}
function businessTimelineMessage(message: string): string {
return message
.replace(/^启动计划:/, '接收委托:')
.replace(/^准备步骤:/, '准备工作:')
.replace(/^创建任务:/, '安排工作:')
.replace(/^任务失败:/, '工作失败:')
.replace(/^开始处理:/, '开始处理:')
.replace(/^完成任务:/, '完成工作:')
.replace(/^调用技能:/, '数字员工处理:')
.replace(/^执行开始:/, '开始执行:')
.replace(/^执行完成:/, '执行完成:')
.replace(/^执行失败:/, '执行失败:')
.replace(/,将使用[^,。]+/, '')
.replace(/,分配给[^,。]+/, '');
}
function isFailureMessage(message: string): boolean {
return /失败|failed|error|timeout|unavailable/i.test(message);
}
function businessKindLabel(kind: string, message = ''): string {
if (isFailureMessage(message)) return '失败';
if (kind === 'artifact') return '产物';
if (kind === 'task_summary') return '结果';
if (kind === 'task_running' || kind === 'run') return '处理中';
if (kind === 'task_done' || kind === 'run_done') return '完成';
return '过程';
}
function messageRoleLabel(role: string): string {
if (role === 'user') return '用户';
if (role === 'assistant') return '数字员工';
if (role === 'system') return '系统';
return '消息';
}
type DetailEvent = {
event_id: string;
kind: string;
occurred_at: string | null;
message: string;
};
type ApprovalSummary = {
id: string;
status: string;
note: string;
requestedAt: string | null;
decidedAt: string | null;
decidedBy: string | null;
runId: string;
taskId: string | null;
planId: string | null;
targetKind: string;
targetId: string;
};
function skillLabel(skillId: string | null | undefined): string {
return displayBusinessText(skillId || '', skillId || '自动化技能');
}
function toolBusinessMessage(toolName: string, success: string, elapsedMs: string): string {
const result = success === 'true' ? '成功' : '失败';
if (toolName.includes('zhihu-hotlist') && toolName.includes('extract_hotlist')) {
return `调用技能:知乎热榜采集,结果:${result},耗时 ${elapsedMs}ms`;
}
if (toolName.includes('office-export-xlsx') && toolName.includes('export_xlsx')) {
return `调用技能Excel 导出,结果:${result},耗时 ${elapsedMs}ms`;
}
return `调用技能:${toolName},结果:${result},耗时 ${elapsedMs}ms`;
}
function valueText(value: unknown): string {
return typeof value === 'string' ? value : '';
}
function eventTime(event: DetailEvent): string {
return event.occurred_at || '';
}
function earliestTime(values: Array<string | null | undefined>): string | null {
return values
.filter((value): value is string => Boolean(value))
.sort((a, b) => a.localeCompare(b))[0] || null;
}
function latestByResultSubject(events: DetailEvent[]): DetailEvent[] {
const bySubject = new Map<string, DetailEvent>();
for (const event of events) {
const subject = event.message.split(',结果:')[0]?.split(',状态:')[0] || event.message;
const current = bySubject.get(subject);
if (!current || eventTime(event).localeCompare(eventTime(current)) > 0) {
bySubject.set(subject, event);
}
}
return Array.from(bySubject.values());
}
function approvalSummariesFromRuns(runs: DebugRun[]): ApprovalSummary[] {
const byId = new Map<string, ApprovalSummary>();
for (const run of runs) {
for (const approval of run.approvals || []) {
const id = valueText(approval.approval_id) || `approval-${run.run_id}`;
byId.set(id, {
id,
status: valueText(approval.status) || 'requested',
note: valueText(approval.note) || '等待人工审批',
requestedAt: valueText(approval.requested_at) || null,
decidedAt: valueText(approval.decided_at) || null,
decidedBy: valueText(approval.decided_by) || null,
runId: valueText(approval.run_id) || run.run_id,
taskId: valueText(approval.task_id) || run.task_id || null,
planId: valueText(approval.plan_id) || run.plan_id || null,
targetKind: valueText(approval.target_kind) || 'task',
targetId: valueText(approval.target_id) || run.task_id || run.run_id,
});
}
}
return Array.from(byId.values()).sort((a, b) => (b.requestedAt || '').localeCompare(a.requestedAt || ''));
}
function objectValue(value: unknown): Record<string, unknown> | null {
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
function normalizeOutputPath(value: string): string {
return value.replace(/\\\\/g, '\\');
}
function buildOperationalTimeline(
mission: Mission,
tasks: DebugTask[],
runs: DebugRun[],
messages: PlanMessage[],
flowEvents?: FlowResponse['events'],
): DetailEvent[] {
const events: DetailEvent[] = [];
const add = (kind: string, occurredAt: string | null | undefined, message: string, id: string) => {
if (!message.trim()) return;
events.push({ event_id: id, kind, occurred_at: occurredAt ?? null, message });
};
const addOutputFacts = (rawOutput: string, occurredAt: string | null | undefined, idPrefix: string, sourceName: string) => {
let parsedOutputPath = false;
try {
const parsed = objectValue(JSON.parse(rawOutput));
if (parsed) {
const label = typeof parsed.label === 'string' ? parsed.label.trim() : '';
const value = parsed.value;
if (label && value !== undefined && value !== null) {
add('task_summary', occurredAt, `${label}${String(value)}`, `metric-${idPrefix}-${sourceName}`);
}
const outputPath = typeof parsed.output_path === 'string' ? parsed.output_path.trim() : '';
if (outputPath) {
parsedOutputPath = true;
add('artifact', occurredAt, `产物生成:${normalizeOutputPath(outputPath)}`, `artifact-${idPrefix}-${sourceName}`);
}
}
} catch {
// Some execution messages are human-readable text. Regex extraction below handles those.
}
const rowCountMatch = rawOutput.match(/"row_count"\s*:\s*(\d+)/);
if (rowCountMatch?.[1]) {
add('task_summary', occurredAt, `数据处理完成:共 ${rowCountMatch[1]} 条记录`, `rows-${idPrefix}-${sourceName}`);
}
const pathMatch = rawOutput.match(/"output_path"\s*:\s*"([^"]+\.(?:xlsx|xls|csv|json|html|pdf|png|jpg|jpeg|txt))"/i);
const outputPath = pathMatch?.[1];
if (outputPath && !parsedOutputPath) {
add('artifact', occurredAt, `产物生成:${normalizeOutputPath(outputPath)}`, `artifact-regex-${idPrefix}-${sourceName}`);
}
};
add('plan', mission.createdAt || mission.updatedAt, `启动计划:${mission.title},目标:${mission.objective}`, `plan-${mission.id}`);
if (tasks.length === 0) {
for (const step of mission.steps) {
const skills = step.preferredSkills.map(skillLabel).join('、') || '自动化技能';
add('task', mission.updatedAt || mission.createdAt, `准备步骤:${step.title},将使用 ${skills}`, `step-${step.id}`);
}
}
for (const task of tasks) {
const skill = skillLabel(task.assigned_skill_id);
const taskRuns = runs.filter((run) => run.task_id === task.task_id);
const firstRunAt = earliestTime(taskRuns.map((run) => run.created_at));
const taskCreatedAt = earliestTime([task.created_at, task.started_at, firstRunAt]);
add('task', taskCreatedAt, `创建任务:${task.title || '子任务'},分配给 ${skill}`, `task-create-${task.task_id}`);
if (task.started_at) add('task_running', task.started_at, `开始处理:${task.title || '子任务'}`, `task-start-${task.task_id}`);
if (task.finished_at) {
const taskDoneVerb = isFailed(task.status) ? '任务失败' : '完成任务';
const errorDetail = isFailed(task.status) && task.error_message ? `,原因:${task.error_message}` : '';
add('task_done', task.finished_at, `${taskDoneVerb}${task.title || '子任务'},状态:${displayStatus(task.status)}${errorDetail}`, `task-finish-${task.task_id}`);
}
}
for (const run of runs) {
const task = tasks.find((item) => item.task_id === run.task_id);
const taskTitle = task?.title || '任务执行';
const runSkill = skillLabel(run.skill_id || task?.assigned_skill_id);
const hasSessionToolForTask = messages.some((message) => (message.content || '').includes(`[Task ${run.task_id}] tool_call`));
if (!hasSessionToolForTask) {
add('skill_tool', run.created_at, `调用技能:${runSkill},处理:${taskTitle}`, `run-skill-${run.run_id}`);
}
add('run', run.created_at, `执行开始:${taskTitle},第 ${run.attempt_no || 1}`, `run-start-${run.run_id}`);
if (run.completed_at) {
const runDoneVerb = isFailed(run.lifecycle) ? '执行失败' : '执行完成';
add('run_done', run.completed_at, `${runDoneVerb}${taskTitle},结果:${displayStatus(run.lifecycle)}`, `run-done-${run.run_id}`);
}
for (const [index, runEvent] of (run.events || []).entries()) {
addOutputFacts(
valueText(runEvent.message),
runEvent.occurred_at || run.completed_at || run.created_at,
`run-${run.run_id}-${runEvent.event_id || index}`,
run.skill_id || task?.assigned_skill_id || 'run',
);
}
for (const [index, artifact] of (run.artifacts || []).entries()) {
const uri = valueText(artifact.uri);
const name = valueText(artifact.name) || uri;
if (uri || name) {
add('artifact', run.completed_at || run.created_at, `产物生成:${name}${uri && uri !== name ? `${uri}` : ''}`, `run-artifact-${run.run_id}-${index}`);
}
}
}
for (const message of messages) {
const content = message.content || '';
const toolMatch = content.match(/\[Task ([^\]]+)\] tool_call name=([^\s]+) success=(true|false) elapsed_ms=(\d+) args=(.*?) output=(.*)$/s);
if (toolMatch) {
const taskId = toolMatch[1] || '';
const toolName = toolMatch[2] || '';
const success = toolMatch[3] || 'false';
const elapsedMs = toolMatch[4] || '0';
const output = toolMatch[6] || '';
add('skill_tool', message.created_at, toolBusinessMessage(toolName, success, elapsedMs), `tool-${taskId}-${message.created_at}-${toolName}`);
addOutputFacts(output, message.created_at, `message-${message.created_at}`, toolName);
continue;
}
const hotlistCount = content.match(/共采集到\s*\**\s*(\d+)\s*条/);
if (hotlistCount?.[1]) {
add('task_summary', message.created_at, `采集完成:获得 ${hotlistCount[1]} 条知乎热榜数据`, `summary-hotlist-${message.created_at}`);
continue;
}
if (content.includes('[Task ') && content.includes('Excel')) {
add('task_summary', message.created_at, '导出完成Excel 文件已生成', `summary-excel-${message.created_at}`);
}
}
if (events.length <= 1) {
for (const event of flowEvents || []) {
add(event.kind, event.occurred_at, displayBackendMessage(event.message, event.message), event.event_id);
}
}
return events
.sort((a, b) => (a.occurred_at || '').localeCompare(b.occurred_at || ''))
.slice(-40);
}
export default function FlowDetailPanel({ mission, onClose }: FlowDetailPanelProps) {
const [tasks, setTasks] = useState<DebugTask[]>([]);
const [runs, setRuns] = useState<DebugRun[]>([]);
const [flow, setFlow] = useState<FlowResponse | null>(null);
const [loading, setLoading] = useState(true);
const [sessionMessages, setSessionMessages] = useState<PlanMessage[]>([]);
const [sessionOpen, setSessionOpen] = useState(false);
const [advancedOpen, setAdvancedOpen] = useState(false);
const [sessionLoading, setSessionLoading] = useState(false);
const [sessionError, setSessionError] = useState<string | null>(null);
const planId = mission.sourceIds.planId || mission.id;
useEffect(() => {
let cancelled = false;
const refresh = () => {
setLoading(true);
Promise.allSettled([
getDebugTasksForPlan(planId),
getDebugPlanFlow(planId),
getPlanMessages(planId),
]).then(([tasksResult, flowResult, messagesResult]) => {
if (cancelled) return;
const demoMission = getDemoMissions().find((item) => item.id === planId);
const fallbackTasks = demoMission ? buildDemoTasks([demoMission]) : [];
const fallbackRuns = fallbackTasks.length > 0 ? buildDemoRuns(fallbackTasks) : [];
if (tasksResult.status === 'fulfilled') {
setTasks(tasksResult.value.tasks && tasksResult.value.tasks.length > 0 ? tasksResult.value.tasks : fallbackTasks);
setRuns(tasksResult.value.runs && tasksResult.value.runs.length > 0 ? tasksResult.value.runs : fallbackRuns);
} else {
setTasks(fallbackTasks);
setRuns(fallbackRuns);
}
if (flowResult.status === 'fulfilled' && flowResult.value?.events && flowResult.value.events.length > 0) {
setFlow(flowResult.value);
} else {
setFlow(demoMission ? { events: buildDemoFlowEvents(demoMission) } as FlowResponse : null);
}
if (messagesResult.status === 'fulfilled' && messagesResult.value.messages.length > 0) {
setSessionMessages(messagesResult.value.messages);
} else if (demoMission) {
setSessionMessages(buildDemoPlanMessages(demoMission));
} else {
setSessionMessages([]);
}
}).finally(() => {
if (!cancelled) setLoading(false);
});
};
refresh();
const timer = window.setInterval(refresh, 3000);
return () => {
cancelled = true;
window.clearInterval(timer);
};
}, [planId]);
useEffect(() => {
setSessionMessages([]);
setSessionOpen(false);
setAdvancedOpen(false);
setSessionError(null);
}, [planId]);
const loadSessionMessages = async () => {
if (sessionLoading) return;
setSessionOpen(true);
setSessionLoading(true);
setSessionError(null);
try {
const data = await getPlanMessages(planId);
if (data.messages && data.messages.length > 0) {
setSessionMessages(data.messages);
} else {
const demoMission = getDemoMissions().find((item) => item.id === planId);
setSessionMessages(demoMission ? buildDemoPlanMessages(demoMission) : []);
}
} catch (err) {
const demoMission = getDemoMissions().find((item) => item.id === planId);
if (demoMission) {
setSessionMessages(buildDemoPlanMessages(demoMission));
setSessionError(null);
} else {
setSessionMessages([]);
setSessionError(err instanceof Error ? err.message : '读取会话失败');
}
} finally {
setSessionLoading(false);
}
};
const runsByTask = useMemo(() => {
const grouped = new Map<string, DebugRun[]>();
for (const run of runs) {
const list = grouped.get(run.task_id) || [];
list.push(run);
grouped.set(run.task_id, list);
}
for (const list of grouped.values()) {
list.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));
}
return grouped;
}, [runs]);
const eventTimeline = useMemo(
() => buildOperationalTimeline(mission, tasks, runs, sessionMessages, flow?.events),
[flow?.events, mission, runs, sessionMessages, tasks],
);
const businessTimeline = useMemo(
() => eventTimeline
.filter((event) => event.kind !== 'skill_tool')
.slice(-12),
[eventTimeline],
);
const approvalSummaries = useMemo(
() => approvalSummariesFromRuns(runs),
[runs],
);
const resultEvents = useMemo(
() => {
const facts = eventTimeline.filter((event) => ['artifact', 'task_summary'].includes(event.kind));
const taskResults = eventTimeline.filter((event) => event.kind === 'task_done');
const runResults = latestByResultSubject(eventTimeline.filter((event) => event.kind === 'run_done'));
const terminal = taskResults.length > 0 ? taskResults : runResults;
return [...facts, ...terminal]
.sort((a, b) => eventTime(a).localeCompare(eventTime(b)))
.slice(-6);
},
[eventTimeline],
);
const latestEventAt = useMemo(
() => eventTimeline.reduce<string | null>((latest, event) => {
if (!event.occurred_at) return latest;
if (!latest || event.occurred_at.localeCompare(latest) > 0) return event.occurred_at;
return latest;
}, null),
[eventTimeline],
);
const displayUpdatedAt = mission.updatedAt || latestEventAt;
const openAdvanced = () => {
const nextOpen = !advancedOpen;
setAdvancedOpen(nextOpen);
if (nextOpen && !sessionOpen) {
void loadSessionMessages();
}
};
return (
<div className="de-drawer" role="dialog" aria-label={mission.title}>
<div className="de-drawer-head">
<div className="de-drawer-title-group">
<span className="de-drawer-eyebrow"></span>
<h3>{mission.title}</h3>
</div>
<button className="de-btn-secondary" onClick={onClose} aria-label="关闭"></button>
</div>
<div className="de-drawer-status-line">
<StatusBadge variant={stepVariant(mission.status)} label={displayStatus(mission.status)} />
<span className="de-drawer-meta">{missionPeriodLabel(mission)}</span>
<span className="de-drawer-meta">{progressText(mission)}</span>
</div>
<div className="de-drawer-timeline">
<section className="de-detail-section">
<div className="de-detail-section-head">
<h4></h4>
</div>
<div className="de-detail-kv">
<span></span><strong>{mission.objective || '未填写'}</strong>
<span></span><strong>{missionPeriodLabel(mission)}</strong>
<span></span><strong>{progressText(mission)}</strong>
<span></span><strong>{fmtTime(mission.createdAt)}</strong>
<span></span><strong>{displayStatus(mission.status)}</strong>
<span></span><strong>{fmtTime(displayUpdatedAt)}</strong>
</div>
</section>
{approvalSummaries.length > 0 ? (
<section className="de-detail-section">
<div className="de-detail-section-head">
<h4></h4>
<span className="de-chain-plan-id">{approvalSummaries.length} </span>
</div>
<div className="de-event-list">
{approvalSummaries.map((approval) => (
<div key={`approval-${approval.id}`} className="de-event-row">
<strong>{displayStatus(approval.status)}</strong>
<span>
{approval.note}
{' '}
{approval.taskId ? `Task ${approval.taskId}` : `Target ${approval.targetKind}:${approval.targetId}`}
{approval.runId ? ` / Run ${approval.runId}` : ''}
{approval.decidedBy ? ` / ${approval.decidedBy}` : ''}
</span>
<time>{fmtTime(approval.decidedAt || approval.requestedAt)}</time>
</div>
))}
</div>
</section>
) : null}
<section className="de-detail-section de-execution-chain">
<div className="de-detail-section-head">
<h4></h4>
<span className="de-chain-plan-id">{fmtTime(displayUpdatedAt || mission.createdAt)}</span>
</div>
{loading && businessTimeline.length === 0 ? (
<p className="de-empty-text">...</p>
) : businessTimeline.length === 0 ? (
<p className="de-empty-text"></p>
) : (
<div className="de-chain-list">
{businessTimeline.map((event) => (
<div key={`chain-${event.event_id}`} className={`de-chain-row de-chain-row--${event.kind} ${isFailureMessage(event.message) ? 'de-chain-row--danger' : ''}`}>
<strong>{businessKindLabel(event.kind, event.message)}</strong>
<span>{businessTimelineMessage(event.message)}</span>
<time>{fmtTime(event.occurred_at)}</time>
</div>
))}
</div>
)}
</section>
<section className="de-detail-section">
<h4></h4>
{resultEvents.length > 0 ? (
<div className="de-event-list">
{[...resultEvents].reverse().map((event) => (
<div key={`result-${event.event_id}`} className={`de-event-row ${isFailureMessage(event.message) ? 'de-event-row--danger' : ''}`}>
<strong>{businessKindLabel(event.kind, event.message)}</strong>
<span>{businessTimelineMessage(event.message)}</span>
<time>{fmtTime(event.occurred_at)}</time>
</div>
))}
</div>
) : (
<p className="de-empty-text"></p>
)}
</section>
<section className="de-detail-section">
<h4></h4>
{loading ? <p className="de-empty-text">...</p> : null}
{mission.steps.length === 0 ? (
<p className="de-empty-text"></p>
) : (
mission.steps.map((step, i) => (
<div key={step.id} className="de-step-node">
<div className={`de-step-dot de-step-dot--${step.status}`} />
{i < mission.steps.length - 1 && <div className="de-step-line" />}
<div className="de-step-content">
<strong>{step.title}</strong>
<span>{displayStatus(step.status)}</span>
</div>
</div>
))
)}
</section>
<section className="de-detail-section">
<div className="de-detail-section-head">
<h4></h4>
<button className="de-btn-secondary de-btn-sm" onClick={openAdvanced} disabled={sessionLoading}>
{advancedOpen ? '收起' : '展开'}
</button>
</div>
{advancedOpen ? (
<>
<section className="de-detail-section">
<div className="de-detail-section-head">
<h4></h4>
<span className="de-chain-plan-id"></span>
</div>
{eventTimeline.length > 0 ? (
<div className="de-event-list">
{eventTimeline.slice(-20).reverse().map((event) => (
<div key={event.event_id} className={`de-event-row ${isFailureMessage(event.message) ? 'de-event-row--danger' : ''}`}>
<strong>{displayEventKind(event.kind)}</strong>
<span>{event.message}</span>
<time>{fmtTime(event.occurred_at)}</time>
</div>
))}
</div>
) : (
<p className="de-empty-text"></p>
)}
</section>
<section className="de-detail-section">
<div className="de-detail-section-head">
<h4></h4>
<button className="de-btn-secondary de-btn-sm" onClick={loadSessionMessages} disabled={sessionLoading}>
</button>
</div>
{sessionError ? (
<p className="de-empty-text">{sessionError}</p>
) : sessionLoading ? (
<p className="de-empty-text">...</p>
) : sessionMessages.length === 0 ? (
<p className="de-empty-text"></p>
) : (
<div className="de-session-message-list">
{sessionMessages.map((message, index) => (
<div key={`${message.role}-${index}`} className={`de-session-message de-session-message--${message.role === 'user' ? 'user' : 'agent'}`}>
<div className="de-session-message-role">{messageRoleLabel(message.role)}</div>
<p>{displayBackendMessage(message.content, message.content)}</p>
</div>
))}
</div>
)}
</section>
<section className="de-detail-section">
<h4></h4>
{tasks.length === 0 ? (
<p className="de-empty-text"></p>
) : (
tasks.map((task) => {
const taskRuns = runsByTask.get(task.task_id) || [];
const latestRun = taskRuns[0];
return (
<div key={task.task_id} className="de-task-run-card">
<div className="de-task-run-head">
<span>{task.title || '子任务'}</span>
<StatusBadge variant={stepVariant(task.status)} label={displayStatus(task.status)} />
</div>
<div className="de-task-run-meta">
<span>skill_id{task.assigned_skill_id || '未分配'}</span>
<span>{fmtTime(task.started_at)}</span>
<span>{fmtTime(task.finished_at)}</span>
</div>
<div className="de-task-run-meta">
<span>task_id{task.task_id}</span>
<span>{taskRuns.length} </span>
{latestRun ? <span>{displayStatus(latestRun.lifecycle)}</span> : null}
</div>
{isFailed(task.status) && task.error_message ? (
<div className="de-task-run-error">
<strong></strong>{task.error_message}
</div>
) : null}
</div>
);
})
)}
</section>
</>
) : (
<p className="de-empty-text"></p>
)}
</section>
</div>
</div>
);
}

View File

@@ -0,0 +1,7 @@
interface GemProps {
className?: string;
}
export default function Gem({ className = '' }: GemProps) {
return <div className={`de-gem ${className}`} aria-hidden="true" />;
}

View File

@@ -0,0 +1,15 @@
import type { ReactNode, CSSProperties } from 'react';
interface GlassCardProps {
children: ReactNode;
className?: string;
style?: CSSProperties;
}
export default function GlassCard({ children, className = '', style }: GlassCardProps) {
return (
<div className={`de-card ${className}`} style={style}>
{children}
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More