feat: add digital employee management sync endpoint
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
package com.xspaceagi.agent.core.adapter.application;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto;
|
||||
|
||||
public interface DigitalEmployeeSyncApplicationService {
|
||||
DigitalEmployeeSyncResponseDto syncOutbox(DigitalEmployeeSyncRequestDto request);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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;
|
||||
private Map<String, Object> payload;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.xspaceagi.agent.core.adapter.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord;
|
||||
|
||||
public interface DigitalEmployeeSyncRecordRepository extends IService<DigitalEmployeeSyncRecord> {
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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 payload;
|
||||
private Date syncedAt;
|
||||
private Date modified;
|
||||
private Date created;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.xspaceagi.agent.core.application.service;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncAckDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncItemDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto;
|
||||
import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeSyncRecordRepository;
|
||||
import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmployeeSyncApplicationService {
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncRecordRepository digitalEmployeeSyncRecordRepository;
|
||||
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
@DSTransactional
|
||||
public DigitalEmployeeSyncResponseDto syncOutbox(DigitalEmployeeSyncRequestDto request) {
|
||||
List<DigitalEmployeeSyncAckDto> acked = new ArrayList<>();
|
||||
if (request == null || CollectionUtils.isEmpty(request.getItems())) {
|
||||
return DigitalEmployeeSyncResponseDto.builder().acked(acked).build();
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
for (DigitalEmployeeSyncItemDto item : request.getItems()) {
|
||||
if (item == null || item.getOutboxId() == null || item.getEntityType() == null || item.getEntityId() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DigitalEmployeeSyncRecord existing = digitalEmployeeSyncRecordRepository.getOne(
|
||||
new QueryWrapper<>(DigitalEmployeeSyncRecord.builder()
|
||||
.clientKey(request.getClientKey())
|
||||
.outboxId(item.getOutboxId())
|
||||
.build()),
|
||||
false
|
||||
);
|
||||
|
||||
DigitalEmployeeSyncRecord record = existing == null ? new DigitalEmployeeSyncRecord() : existing;
|
||||
record.setTenantId(RequestContext.get().getTenantId());
|
||||
record.setUserId(RequestContext.get().getUserId());
|
||||
record.setClientKey(request.getClientKey());
|
||||
record.setDeviceId(request.getDeviceId());
|
||||
record.setOutboxId(item.getOutboxId());
|
||||
record.setEntityType(item.getEntityType());
|
||||
record.setEntityId(item.getEntityId());
|
||||
record.setOperation(item.getOperation());
|
||||
record.setPayload(toJson(item.getPayload()));
|
||||
record.setSyncedAt(now);
|
||||
record.setModified(now);
|
||||
if (record.getId() == null) {
|
||||
record.setCreated(now);
|
||||
digitalEmployeeSyncRecordRepository.save(record);
|
||||
} else {
|
||||
digitalEmployeeSyncRecordRepository.updateById(record);
|
||||
}
|
||||
|
||||
acked.add(DigitalEmployeeSyncAckDto.builder()
|
||||
.outboxId(item.getOutboxId())
|
||||
.entityType(item.getEntityType())
|
||||
.entityId(item.getEntityId())
|
||||
.remoteId(String.valueOf(record.getId()))
|
||||
.build());
|
||||
}
|
||||
|
||||
return DigitalEmployeeSyncResponseDto.builder().acked(acked).build();
|
||||
}
|
||||
|
||||
private String toJson(Object payload) {
|
||||
if (payload == null) {
|
||||
return "{}";
|
||||
}
|
||||
try {
|
||||
return objectMapper.writeValueAsString(payload);
|
||||
} catch (JsonProcessingException e) {
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.xspaceagi.agent.core.infra.repository;
|
||||
|
||||
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.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DigitalEmployeeSyncRecordRepositoryImpl
|
||||
extends ServiceImpl<DigitalEmployeeSyncRecordMapper, DigitalEmployeeSyncRecord>
|
||||
implements DigitalEmployeeSyncRecordRepository {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.xspaceagi.agent.web.ui.controller;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import jakarta.annotation.Resource;
|
||||
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.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/digital-employee/sync")
|
||||
public class DigitalEmployeeSyncController {
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
|
||||
|
||||
@RequestMapping(path = "/outbox", method = RequestMethod.POST)
|
||||
public ReqResult<DigitalEmployeeSyncResponseDto> syncOutbox(@RequestBody DigitalEmployeeSyncRequestDto request) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.syncOutbox(request));
|
||||
}
|
||||
}
|
||||
23
qiming-backend/sql/update-20260605.sql
Normal file
23
qiming-backend/sql/update-20260605.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
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_user_time` (`_tenant_id`, `user_id`, `synced_at`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci
|
||||
COMMENT = '数字员工客户端同步记录';
|
||||
@@ -16,6 +16,7 @@ interface DigitalEmployeeSyncConfig {
|
||||
enabled: boolean;
|
||||
endpoint: string | null;
|
||||
clientKey: string | null;
|
||||
authToken: string | null;
|
||||
batchSize: number;
|
||||
}
|
||||
|
||||
@@ -96,7 +97,7 @@ export async function flushDigitalEmployeeSyncOutbox(): Promise<DigitalEmployeeS
|
||||
if (syncing) return getDigitalEmployeeSyncStatus();
|
||||
|
||||
const config = resolveSyncConfig();
|
||||
if (!config.enabled || !config.endpoint || !config.clientKey) {
|
||||
if (!config.enabled || !config.endpoint || !config.clientKey || !config.authToken) {
|
||||
lastSyncError = null;
|
||||
return getDigitalEmployeeSyncStatus();
|
||||
}
|
||||
@@ -167,6 +168,7 @@ function resolveSyncConfig(): DigitalEmployeeSyncConfig {
|
||||
enabled: !disabled,
|
||||
endpoint,
|
||||
clientKey: readClientKey(serverHost),
|
||||
authToken: readAuthToken(serverHost),
|
||||
batchSize,
|
||||
};
|
||||
}
|
||||
@@ -190,6 +192,21 @@ function readClientKey(serverHost: string | null): string | null {
|
||||
return typeof globalKey === "string" && globalKey.trim() ? globalKey : null;
|
||||
}
|
||||
|
||||
function readAuthToken(serverHost: string | null): string | null {
|
||||
const oneShotToken = readSetting("auth.token");
|
||||
if (typeof oneShotToken === "string" && oneShotToken.trim()) {
|
||||
return oneShotToken;
|
||||
}
|
||||
if (serverHost) {
|
||||
const domain = serverHost.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
||||
const domainToken = readSetting(`auth.tokens.${domain}`);
|
||||
if (typeof domainToken === "string" && domainToken.trim()) {
|
||||
return domainToken;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPendingOutbox(limit: number): OutboxRow[] {
|
||||
const db = getDb();
|
||||
if (!db) return [];
|
||||
@@ -217,7 +234,7 @@ async function postOutbox(
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${config.clientKey}`,
|
||||
Authorization: `Bearer ${config.authToken}`,
|
||||
"X-Qiming-Client-Key": config.clientKey!,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -455,13 +455,23 @@ window.QimingClawBridge.digital.flushSync()
|
||||
|
||||
当前边界:
|
||||
|
||||
- 后端接口尚未真正确认,因此客户端先实现可配置 endpoint 和兼容 ack 协议。
|
||||
- qiming-backend 已补最小接收端,先以通用同步记录表承接客户端 outbox。
|
||||
- worker 只消费 outbox,不直接侵入 Plan / Task / Run / Event 写入链路。
|
||||
- 管理端返回格式建议包含 `acked[]`,每项带 `outbox_id`、`entity_type`、`entity_id`、`remote_id`。
|
||||
- 管理端返回格式包含 `acked[]`,每项带 `outbox_id`、`entity_type`、`entity_id`、`remote_id`。
|
||||
- 当前接口沿用管理端登录态鉴权,客户端必须能读取到登录 ticket/token 才会推送;没有 token 时保持本地 outbox 等待。
|
||||
|
||||
后端最小落点:
|
||||
|
||||
```text
|
||||
qiming-backend/sql/update-20260605.sql
|
||||
qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/.../DigitalEmployeeSyncController.java
|
||||
qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/.../DigitalEmployeeSyncApplicationServiceImpl.java
|
||||
qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/.../DigitalEmployeeSyncRecord.java
|
||||
```
|
||||
|
||||
下一步:
|
||||
|
||||
- 在 qiming-backend 落地 `/api/digital-employee/sync/outbox`。
|
||||
- 在有 Maven 的环境验证 qiming-backend 编译与接口启动。
|
||||
- 数字员工页面读取 `getSyncStatus()`,展示联动状态。
|
||||
- 增加 outbox 重试退避策略,避免接口长期不可用时频繁请求。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user