云
15 小时以前 d9a5ba1de2fce6bd8c50dace505e4bf7864043ba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package cn.iocoder.yudao.module.system.controller.admin.agent;
 
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.system.framework.agent.AgentBridgeException;
import cn.iocoder.yudao.module.system.framework.agent.AgentPrincipal;
import cn.iocoder.yudao.module.system.framework.agent.SystemAgentBridgeGuard;
import cn.iocoder.yudao.module.system.service.storage.SystemAgentAttachmentRef;
import cn.iocoder.yudao.module.system.service.storage.SystemAgentImportBridgeService;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
 
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.Set;
 
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 
/** Read-only bridge for material onboarding; business writes live in the MDM module. */
@RestController
@RequestMapping("/agent/bridge")
public class SystemAgentImportBridgeController {
 
    private static final Set<String> ACTIONS = Set.of("material_import.prepare", "material_import.read",
            "material_import.review", "material_import.preflight", "material_import.commit",
            "material_import.read_receipt",
            "conversation.create", "conversation.read", "conversation.message",
            "conversation.action", "conversation.report");
 
    /** module_id → 业务导入权限;Python 以模块 ID 作为授权 resource。 */
    private static final Map<String, String> MODULE_PERMISSIONS = Map.of(
            "mdm.item", SystemAgentBridgeGuard.PERMISSION_ITEM_IMPORT,
            "srm.supplier", "srm:supplier:create",
            "mes.bom", "mes:md-item:create",
            "mes.qc-template", "mes:qc-template:create",
            "mes.mps", "mes:pro-mps:create",
            "mes.route", "mes:pro-route:create");
 
    @Resource
    private SystemAgentImportBridgeService bridgeService;
    @Resource
    private SystemAgentBridgeGuard bridgeGuard;
 
    private static final String IMPORT_PERMISSIONS_EXPR =
            "@ss.hasAnyPermissions('mdm:item:import','srm:supplier:create','mes:md-item:create',"
                    + "'mes:qc-template:create','mes:pro-mps:create','mes:pro-route:create')";
 
    @PostMapping("/import-sessions")
    @PreAuthorize(IMPORT_PERMISSIONS_EXPR)
    public CommonResult<Long> createSession() {
        return success(bridgeService.createSession());
    }
 
    @PostMapping("/import-sessions/{sessionId}/attachments")
    @PreAuthorize(IMPORT_PERMISSIONS_EXPR)
    public CommonResult<Long> bindAttachment(@PathVariable Long sessionId, @Valid @RequestBody BindRequest request) {
        return success(bridgeService.bindAttachment(sessionId, request.getBlobId()));
    }
 
    @PostMapping("/authorize")
    @PreAuthorize(IMPORT_PERMISSIONS_EXPR)
    public CommonResult<Map<String, String>> authorize(
            @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
            @Valid @RequestBody AuthorizeRequest request) {
        if (!ACTIONS.contains(request.getAction())) {
            throw new AgentBridgeException(HttpStatus.FORBIDDEN, "ACTION_FORBIDDEN", "不支持的智能体操作");
        }
        AgentPrincipal principal;
        if (request.getAction().startsWith("conversation.")) {
            String permission = MODULE_PERMISSIONS.get(request.getResource());
            principal = permission == null
                    ? bridgeGuard.requireAny(serviceToken)
                    : bridgeGuard.require(serviceToken, permission);
        } else {
            principal = bridgeGuard.require(serviceToken, SystemAgentBridgeGuard.PERMISSION_ITEM_IMPORT);
        }
        if (requiresAttachmentOwnership(request.getAction())) {
            bridgeService.requireOwnedAttachmentRef(parseId(request.getResource()));
        }
        // read/review/read_receipt 的任务 UUID 所有权由 Python checkpoint 作用域核验;本端重查当前按钮权限。
        return success(Map.of("user_id", String.valueOf(principal.userId()),
                "tenant_id", String.valueOf(principal.tenantId()),
                "policy_revision", principal.policyRevision()));
    }
 
    @GetMapping("/attachments/{attachmentId}")
    @PreAuthorize(IMPORT_PERMISSIONS_EXPR)
    public ResponseEntity<FileSystemResource> downloadAttachment(
            @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
            @PathVariable Long attachmentId) {
        bridgeGuard.requireAny(serviceToken);
        SystemAgentAttachmentRef ref = bridgeService.requireOwnedAttachmentRef(attachmentId);
        return ResponseEntity.ok().contentType(MediaType.APPLICATION_OCTET_STREAM)
                .contentLength(ref.byteSize())
                .header("X-Agent-Filename-B64", Base64.getEncoder().encodeToString(
                        ref.originalFilename().getBytes(StandardCharsets.UTF_8)))
                .body(new FileSystemResource(ref.path()));
    }
 
    private static boolean requiresAttachmentOwnership(String action) {
        return "material_import.prepare".equals(action)
                || "material_import.preflight".equals(action)
                || "material_import.commit".equals(action);
    }
 
    private Long parseId(String value) {
        try {
            return Long.valueOf(value);
        } catch (NumberFormatException ex) {
            throw new AgentBridgeException(HttpStatus.FORBIDDEN, "ATTACHMENT_FORBIDDEN", "附件不可访问");
        }
    }
 
    @Data
    public static class BindRequest {
        @NotNull
        private Long blobId;
    }
 
    @Data
    public static class AuthorizeRequest {
        @NotNull
        private String action;
        @NotNull
        private String resource;
    }
}