From d9a5ba1de2fce6bd8c50dace505e4bf7864043ba Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 22 十月 2026 15:01:07 +0800
Subject: [PATCH] feat(mdm): 新增智能体通用业务导入功能

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesBomImportAdapter.java                                   |  277 ++++++++
 yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/md/item/MesBomImportAdapterTest.java                               |  174 +++++
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportModuleAdapter.java                      |   23 
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentImportBridgeController.java        |   37 
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentBridgeGuard.java                          |   29 
 sql/mysql/system_agent_data_entry_menu_20261022.sql                                                                                   |   23 
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportPreflightReqVO.java                     |   46 +
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/agent/imports/SystemAgentGenericImportService.java           |  454 ++++++++++++++
 sql/mysql/agent_import_command_20261022.sql                                                                                           |   57 +
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/agent/AgentImportCommandRowDO.java                    |   36 +
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportCommitReqVO.java                        |   24 
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportRowReceipt.java                         |   12 
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/agent/AgentImportCommandMapper.java                        |   28 
 yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierImportAdapter.java                             |  217 +++++++
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/agent/AgentImportCommandDO.java                       |   44 +
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportPreflightRespVO.java                    |   65 ++
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/agent/AgentImportCommandRowMapper.java                     |   15 
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportReceiptRespVO.java                      |   57 +
 yudao-module-srm/src/test/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierImportAdapterTest.java                         |  121 +++
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentGenericImportBridgeController.java |   66 ++
 yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportCommandService.java                            |    6 
 yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightService.java                          |   10 
 22 files changed, 1,809 insertions(+), 12 deletions(-)

diff --git a/sql/mysql/agent_import_command_20261022.sql b/sql/mysql/agent_import_command_20261022.sql
new file mode 100644
index 0000000..18b392e
--- /dev/null
+++ b/sql/mysql/agent_import_command_20261022.sql
@@ -0,0 +1,57 @@
+-- 閫氱敤涓氬姟瀵煎叆骞傜瓑鍛戒护涓庨�愯鍥炴墽锛堜緵搴斿晢銆丅OM 绛夛級銆傛墽琛屽墠澶囦唤鐩爣搴撱��
+CREATE TABLE IF NOT EXISTS `agent_import_command` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭',
+  `command_id` varchar(64) NOT NULL COMMENT '鏅鸿兘浣撴彁浜ゅ懡浠� ID锛堝箓绛夛級',
+  `idempotency_key` varchar(128) NOT NULL COMMENT '骞傜瓑閿�',
+  `request_digest` char(64) NOT NULL COMMENT '璇锋眰鎽樿 SHA-256',
+  `module_id` varchar(64) NOT NULL COMMENT '涓氬姟妯″潡 ID锛屽 srm.supplier',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '绉熸埛 ID',
+  `owner_user_id` bigint NOT NULL COMMENT '鎻愪氦鐢ㄦ埛 ID',
+  `session_id` bigint DEFAULT NULL COMMENT '琛ュ綍浼氳瘽 ID',
+  `attachment_id` bigint NOT NULL COMMENT '闄勪欢 ID',
+  `source_sha256` char(64) NOT NULL COMMENT '闄勪欢鍐呭鎽樿',
+  `preflight_hash` char(64) NOT NULL COMMENT '棰勬璁″垝鎽樿',
+  `proposal_hash` varchar(128) DEFAULT NULL COMMENT '鏅鸿兘浣撴柟妗堟憳瑕�',
+  `status` varchar(20) NOT NULL DEFAULT 'RUNNING' COMMENT 'RUNNING/SUCCEEDED/PARTIAL_FAILED/FAILED/UNKNOWN',
+  `total_count` int NOT NULL DEFAULT 0 COMMENT '鎬昏鏁�',
+  `success_count` int NOT NULL DEFAULT 0 COMMENT '鎴愬姛琛屾暟',
+  `failed_count` int NOT NULL DEFAULT 0 COMMENT '澶辫触琛屾暟',
+  `skipped_count` int NOT NULL DEFAULT 0 COMMENT '璺宠繃琛屾暟',
+  `error_code` varchar(64) DEFAULT NULL COMMENT '鍛戒护绾ч敊璇爜',
+  `error_message` varchar(512) DEFAULT NULL COMMENT '鍛戒护绾ч敊璇俊鎭�',
+  `lease_expires_at` datetime DEFAULT NULL COMMENT '鎵ц绉熺害鍒版湡鏃堕棿',
+  `creator` varchar(64) DEFAULT NULL,
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updater` varchar(64) DEFAULT NULL,
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  `deleted` bit(1) NOT NULL DEFAULT b'0',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_agent_import_generic_command` (`tenant_id`, `owner_user_id`, `module_id`, `command_id`),
+  UNIQUE KEY `uk_agent_import_generic_idempotency` (`tenant_id`, `owner_user_id`, `module_id`, `idempotency_key`),
+  KEY `idx_agent_import_generic_module` (`tenant_id`, `module_id`, `create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鏅鸿兘浣撻�氱敤涓氬姟瀵煎叆鍛戒护';
+
+CREATE TABLE IF NOT EXISTS `agent_import_command_row` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭',
+  `command_id` varchar(64) NOT NULL COMMENT '鎻愪氦鍛戒护 ID',
+  `module_id` varchar(64) NOT NULL COMMENT '涓氬姟妯″潡 ID',
+  `candidate_id` varchar(64) NOT NULL COMMENT '瀵煎叆琛屾爣璇�',
+  `source_row` int DEFAULT NULL COMMENT '婧愭枃浠惰鍙�',
+  `code` varchar(64) DEFAULT NULL COMMENT '涓氬姟缂栫爜',
+  `name` varchar(255) DEFAULT NULL COMMENT '涓氬姟鍚嶇О',
+  `action` varchar(16) NOT NULL COMMENT 'CREATE/SKIP',
+  `status` varchar(16) NOT NULL COMMENT 'SUCCESS/FAILED/SKIPPED/UNKNOWN',
+  `target_item_id` bigint DEFAULT NULL COMMENT '鍐欏叆鍚庣殑鐩爣涓婚敭',
+  `target_version` varchar(64) DEFAULT NULL COMMENT '鐩爣鐗堟湰锛堥鐣欙級',
+  `error_code` varchar(64) DEFAULT NULL COMMENT '琛岀骇閿欒鐮�',
+  `error_message` varchar(512) DEFAULT NULL COMMENT '琛岀骇閿欒淇℃伅',
+  `retryable` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍙噸璇�',
+  `creator` varchar(64) DEFAULT NULL,
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updater` varchar(64) DEFAULT NULL,
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  `deleted` bit(1) NOT NULL DEFAULT b'0',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_agent_import_generic_row` (`command_id`, `candidate_id`),
+  KEY `idx_agent_import_generic_row` (`command_id`, `source_row`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鏅鸿兘浣撻�氱敤涓氬姟瀵煎叆閫愯鍥炴墽';
diff --git a/sql/mysql/system_agent_data_entry_menu_20261022.sql b/sql/mysql/system_agent_data_entry_menu_20261022.sql
new file mode 100644
index 0000000..3b6558c
--- /dev/null
+++ b/sql/mysql/system_agent_data_entry_menu_20261022.sql
@@ -0,0 +1,23 @@
+-- 妯″潡鏁版嵁琛ュ綍鑿滃崟琛ヤ竵锛屽彲閲嶅鎵ц銆�
+-- 椤剁骇鐩綍鈥滄暟鎹ˉ褰曗�� + 瀛愯彍鍗曗�滄ā鍧楁暟鎹ˉ褰曗�濓紝鍓嶇缁勪欢 agent/dataEntry/index銆�
+-- 浠呰秴绾х鐞嗗憳鑷姩鍙锛涘叾浠栬鑹茶鍦ㄨ鑹茬鐞嗕腑鍕鹃�夎繖涓や釜鑿滃崟銆�
+
+INSERT INTO system_menu (id,name,permission,type,sort,parent_id,path,icon,component,component_name,status,visible,keep_alive,always_show,creator,create_time,updater,update_time,deleted)
+SELECT next_id.id,'鏁版嵁琛ュ綍','',1,2,0,'/agent','ep:magic-stick','',NULL,0,b'1',b'0',b'1','1',NOW(),'1',NOW(),b'0'
+FROM (SELECT COALESCE(MAX(id),0)+1 AS id FROM system_menu) next_id
+WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE parent_id=0 AND path='/agent' AND deleted=b'0');
+
+INSERT INTO system_menu (id,name,permission,type,sort,parent_id,path,icon,component,component_name,status,visible,keep_alive,always_show,creator,create_time,updater,update_time,deleted)
+SELECT next_id.id,'妯″潡鏁版嵁琛ュ綍','',2,1,parent.id,'data-entry','ep:chat-dot-round','agent/dataEntry/index','AgentDataEntry',0,b'1',b'0',b'1','1',NOW(),'1',NOW(),b'0'
+FROM system_menu parent CROSS JOIN (SELECT COALESCE(MAX(id),0)+1 AS id FROM system_menu) next_id
+WHERE parent.parent_id=0 AND parent.path='/agent' AND parent.deleted=b'0'
+  AND NOT EXISTS (SELECT 1 FROM system_menu WHERE component='agent/dataEntry/index' AND deleted=b'0');
+
+UPDATE system_menu SET status=0,visible=b'1',deleted=b'0',updater='1',update_time=NOW()
+WHERE (component='agent/dataEntry/index' OR (parent_id=0 AND path='/agent')) AND deleted<>b'0';
+
+INSERT INTO system_role_menu (role_id,menu_id,creator,create_time,updater,update_time,deleted)
+SELECT r.id,m.id,'1',NOW(),'1',NOW(),b'0'
+FROM system_role r JOIN system_menu m ON m.deleted=b'0' AND (m.component='agent/dataEntry/index' OR (m.parent_id=0 AND m.path='/agent'))
+WHERE r.code='super_admin' AND r.deleted=b'0'
+  AND NOT EXISTS (SELECT 1 FROM system_role_menu rm WHERE rm.role_id=r.id AND rm.menu_id=m.id);
diff --git a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportCommandService.java b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportCommandService.java
index 70c8839..f3c144c 100644
--- a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportCommandService.java
+++ b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportCommandService.java
@@ -69,6 +69,10 @@
             throw new AgentBridgeException(HttpStatus.SERVICE_UNAVAILABLE, "WRITE_DISABLED",
                     "鏅鸿兘浣撳啓鍏ュ紑鍏虫湭鎵撳紑锛屾彁浜ゅ凡鎷掔粷");
         }
+        if (Boolean.TRUE.equals(reqVO.getSyncMes()) && !policy.isAllowMesSync()) {
+            throw new AgentBridgeException(HttpStatus.UNPROCESSABLE_ENTITY, "MES_SYNC_DISABLED",
+                    "鏈嶅姟鏈紑鏀惧悓姝� MES锛岃鍏抽棴 syncMes 鍚庨噸璇�");
+        }
         String requestDigest = buildRequestDigest(reqVO);
         Long tenantId = principal.tenantId();
         Long ownerUserId = principal.userId();
@@ -189,7 +193,7 @@
                                 return;
                             }
                             MdmItemSaveReqVO saveReqVO = resolution.saveReqVO();
-                            saveReqVO.setSyncMes(false); // 鏈樁娈靛浐瀹氫笉鍚屾 MES
+                            saveReqVO.setSyncMes(Boolean.TRUE.equals(reqVO.getSyncMes()));
                             Long itemId = itemService.createItem(saveReqVO);
                             commandRowMapper.insert(baseRow(command.getCommandId(), row)
                                     .action("CREATE").status("SUCCESS").targetItemId(itemId)
diff --git a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightService.java b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightService.java
index db69105..fa4b977 100644
--- a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightService.java
+++ b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightService.java
@@ -71,9 +71,9 @@
 
         boolean syncMesRequested = Boolean.TRUE.equals(reqVO.getSyncMes());
         List<MdmAgentImportPreflightRespVO.Issue> blockingItems = new ArrayList<>();
-        if (syncMesRequested) {
+        if (syncMesRequested && !policy.isAllowMesSync()) {
             blockingItems.add(issue("MES_SYNC_DISABLED", "syncMes",
-                    "鏈樁娈典笉寮�鏀惧悓姝� MES锛岃鍏抽棴 syncMes 鍚庨噸璇�", false));
+                    "鏈嶅姟鏈紑鏀惧悓姝� MES锛岃鍏抽棴 syncMes 鍚庨噸璇�", false));
         }
 
         // 4. 鏂囦欢鍐呴噸澶嶇紪鐮侊細鎵�鏈夐噸澶嶈涓�璧烽樆鏂紝閬垮厤鈥滈琛屽缓銆佹琛岃烦鈥濈殑璇
@@ -182,7 +182,11 @@
                         .blocked(blockedCount)
                         .build())
                 .canCommit(canCommit)
-                .sideEffects(canCommit ? List.of("mdm_item.create:" + createCount) : List.of())
+                .sideEffects(canCommit
+                        ? (syncMesRequested
+                        ? List.of("mdm_item.create:" + createCount, "mes_md_item.sync:" + createCount)
+                        : List.of("mdm_item.create:" + createCount))
+                        : List.of())
                 .rows(rows)
                 .blockingItems(blockingItems)
                 .build();
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesBomImportAdapter.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesBomImportAdapter.java
new file mode 100644
index 0000000..f8b35d0
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesBomImportAdapter.java
@@ -0,0 +1,277 @@
+package cn.iocoder.yudao.module.mes.service.md.item;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.bom.MesMdProductBomSaveReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdProductBomDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdProductBomMapper;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportCommitReqVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportModuleAdapter;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportPreflightReqVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportPreflightRespVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportRowReceipt;
+import cn.iocoder.yudao.module.system.framework.agent.AgentBridgeException;
+import cn.iocoder.yudao.module.system.framework.agent.AgentPrincipal;
+import cn.iocoder.yudao.module.system.service.agent.imports.SystemAgentGenericImportService;
+import jakarta.annotation.Resource;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.*;
+
+/**
+ * BOM 瀵煎叆锛氱埗/瀛愮墿鏂欏繀椤诲凡鍚屾鍒� MES锛涘凡瀛樺湪鐖跺瓙缁勫悎璺宠繃锛屽彧鏂板缓涓嶆洿鏂般��
+ */
+@Component
+public class MesBomImportAdapter implements AgentImportModuleAdapter {
+
+    public static final String MODULE_ID = "mes.bom";
+    private static final String PERMISSION = "mes:md-item:create";
+
+    @Resource
+    private MesMdItemService itemService;
+    @Resource
+    private MesMdProductBomService productBomService;
+    @Resource
+    private MesMdProductBomMapper productBomMapper;
+
+    @Override
+    public String moduleId() {
+        return MODULE_ID;
+    }
+
+    @Override
+    public String permission() {
+        return PERMISSION;
+    }
+
+    @Override
+    public AgentImportPreflightRespVO preflight(AgentPrincipal principal, AgentImportPreflightReqVO request) {
+        // 1. 鐜版湁 BOM 鍥句笌鐖跺瓙缁勫悎绱㈠紩锛岀敤浜庤烦杩囦笌鐜娴�
+        Map<Long, Set<Long>> graph = new HashMap<>();
+        Map<String, Long> pairs = new HashMap<>();
+        for (MesMdProductBomDO bom : productBomMapper.selectAll()) {
+            graph.computeIfAbsent(bom.getItemId(), key -> new HashSet<>()).add(bom.getBomItemId());
+            pairs.put(pairKey(bom.getItemId(), bom.getBomItemId()), bom.getId());
+        }
+
+        Map<String, Optional<MesMdItemDO>> itemCache = new HashMap<>();
+        Set<String> filePairs = new HashSet<>();
+        List<AgentImportPreflightRespVO.Row> rows = new ArrayList<>();
+        List<Map<String, Object>> resolvedPayload = new ArrayList<>();
+        int create = 0;
+        int skip = 0;
+        int blocked = 0;
+
+        for (AgentImportPreflightReqVO.Row row : request.getRows()) {
+            List<AgentImportPreflightRespVO.Issue> blockers = new ArrayList<>();
+            List<AgentImportPreflightRespVO.Issue> warnings = new ArrayList<>();
+            String parentCode = value(row, "parentCode");
+            String componentCode = value(row, "componentCode");
+            if (parentCode.isEmpty()) {
+                blockers.add(issue("PARENT_CODE_REQUIRED", "parentCode", "鐖堕」鐗╂枡缂栫爜涓嶈兘涓虹┖"));
+            }
+            if (componentCode.isEmpty()) {
+                blockers.add(issue("COMPONENT_CODE_REQUIRED", "componentCode", "瀛愰」鐗╂枡缂栫爜涓嶈兘涓虹┖"));
+            }
+            BigDecimal quantity = parseQuantity(row.getValues().get("quantity"), blockers);
+            Integer status = parseStatus(row.getValues().get("status"), blockers);
+
+            MesMdItemDO parent = parentCode.isEmpty() ? null : lookup(itemCache, parentCode);
+            MesMdItemDO component = componentCode.isEmpty() ? null : lookup(itemCache, componentCode);
+            if (!parentCode.isEmpty() && parent == null) {
+                blockers.add(issue("MES_ITEM_NOT_SYNCED", "parentCode",
+                        "鐖堕」鐗╂枡缂栫爜 " + parentCode + " 鍦� MES 鐗╂枡涓笉瀛樺湪锛岃鍏堝鍏ュ苟鍚屾鐗╂枡"));
+            }
+            if (!componentCode.isEmpty() && component == null) {
+                blockers.add(issue("MES_ITEM_NOT_SYNCED", "componentCode",
+                        "瀛愰」鐗╂枡缂栫爜 " + componentCode + " 鍦� MES 鐗╂枡涓笉瀛樺湪锛岃鍏堝鍏ュ苟鍚屾鐗╂枡"));
+            }
+
+            String action;
+            Long targetId = null;
+            if (!blockers.isEmpty()) {
+                action = "BLOCKED";
+                blocked++;
+            } else {
+                String key = pairKey(parent.getId(), component.getId());
+                if (parent.getId().equals(component.getId())) {
+                    blockers.add(issue("BOM_SELF_REFERENCE", "componentCode", "鐖堕」涓庡瓙椤逛笉鑳芥槸鍚屼竴鐗╂枡"));
+                    action = "BLOCKED";
+                    blocked++;
+                } else if (pairs.containsKey(key)) {
+                    action = "SKIP";
+                    targetId = pairs.get(key);
+                    skip++;
+                    warnings.add(issue("BOM_EXISTS", "componentCode",
+                            "璇ョ埗瀛愮墿鏂欑粍鍚堝凡瀛樺湪 BOM锛屽凡璺宠繃涓斾笉浼氭洿鏂扮敤閲�"));
+                } else if (filePairs.contains(key)) {
+                    blockers.add(issue("DUPLICATE_IN_FILE", "componentCode",
+                            "鏂囦欢鍐呯埗椤逛笌瀛愰」缁勫悎閲嶅锛岃鏍稿疄鐢ㄩ噺鐗堟湰"));
+                    action = "BLOCKED";
+                    blocked++;
+                } else if (reaches(graph, component.getId(), parent.getId())) {
+                    blockers.add(issue("BOM_CYCLE", "componentCode",
+                            "鏂板鍚� BOM 灏嗗舰鎴愬惊鐜紩鐢紝璇疯皟鏁寸埗瀛愬叧绯�"));
+                    action = "BLOCKED";
+                    blocked++;
+                } else {
+                    action = "CREATE";
+                    create++;
+                    filePairs.add(key);
+                    graph.computeIfAbsent(parent.getId(), item -> new HashSet<>()).add(component.getId());
+                }
+            }
+
+            Map<String, Object> resolved = new LinkedHashMap<>();
+            resolved.put("candidate_id", row.getCandidateId());
+            resolved.put("parent_item_id", parent == null ? null : parent.getId());
+            resolved.put("component_item_id", component == null ? null : component.getId());
+            resolved.put("quantity", quantity);
+            resolved.put("status", status);
+            resolved.put("action", action);
+            resolved.put("target_id", targetId);
+            resolvedPayload.add(resolved);
+
+            Map<String, Object> resolvedValues = new LinkedHashMap<>();
+            resolvedValues.put("parentCode", parentCode);
+            resolvedValues.put("componentCode", componentCode);
+            resolvedValues.put("quantity", quantity);
+            resolvedValues.put("status", status);
+            rows.add(AgentImportPreflightRespVO.Row.builder()
+                    .candidateId(row.getCandidateId())
+                    .rowNo(row.getRowNo())
+                    .action(action)
+                    .resolved(resolvedValues)
+                    .blockers(blockers)
+                    .warnings(warnings)
+                    .targetId(targetId)
+                    .build());
+        }
+
+        int total = rows.size();
+        return AgentImportPreflightRespVO.builder()
+                .moduleId(MODULE_ID)
+                .preflightHash(SystemAgentGenericImportService.preflightHash(MODULE_ID, request, resolvedPayload))
+                .source(AgentImportPreflightRespVO.Source.builder()
+                        .attachmentId(request.getAttachmentId())
+                        .sessionId(request.getSessionId())
+                        .sourceSha256(request.getSourceSha256())
+                        .proposalRevision(request.getProposalRevision())
+                        .build())
+                .summary(AgentImportPreflightRespVO.Summary.builder()
+                        .total(total).create(create).skip(skip).blocked(blocked).build())
+                .canCommit(blocked == 0)
+                .sideEffects(List.of("鍒涘缓 BOM 鐖跺瓙鐢ㄦ枡鍏崇郴", "涓嶆洿鏂板凡瀛樺湪鐨� BOM"))
+                .rows(rows)
+                .blockingItems(List.of())
+                .build();
+    }
+
+    @Override
+    public AgentImportRowReceipt commitRow(AgentPrincipal principal, AgentImportCommitReqVO request,
+                                           AgentImportPreflightRespVO.Row planned,
+                                           AgentImportPreflightReqVO.Row row) {
+        String parentCode = value(row, "parentCode");
+        String componentCode = value(row, "componentCode");
+        MesMdItemDO parent = itemService.getItemByCode(parentCode);
+        MesMdItemDO component = itemService.getItemByCode(componentCode);
+        if (parent == null || component == null) {
+            throw new AgentBridgeException(HttpStatus.CONFLICT, "MES_ITEM_NOT_SYNCED",
+                    "鐗╂枡鍦� MES 涓凡涓嶅瓨鍦紝璇烽噸鏂板悓姝ュ悗閲嶈瘯", true, null);
+        }
+        BigDecimal quantity = new BigDecimal(value(row, "quantity"));
+        if (quantity.signum() <= 0) {
+            throw new AgentBridgeException(HttpStatus.UNPROCESSABLE_ENTITY, "INVALID_QUANTITY",
+                    "鐢ㄩ噺蹇呴』澶т簬 0", false, null);
+        }
+
+        MesMdProductBomSaveReqVO saveReqVO = new MesMdProductBomSaveReqVO();
+        saveReqVO.setItemId(parent.getId());
+        saveReqVO.setBomItemId(component.getId());
+        saveReqVO.setQuantity(quantity);
+        saveReqVO.setStatus(parseStatus(row.getValues().get("status"), null));
+        saveReqVO.setRemark(StrUtil.blankToDefault(row.getValues().get("remark"), null));
+
+        Long id = productBomService.createProductBom(saveReqVO);
+        return AgentImportRowReceipt.created(id, parentCode, componentCode);
+    }
+
+    private MesMdItemDO lookup(Map<String, Optional<MesMdItemDO>> cache, String code) {
+        return cache.computeIfAbsent(code,
+                key -> Optional.ofNullable(itemService.getItemByCode(key))).orElse(null);
+    }
+
+    /** 娌垮凡鏈夛紙鍚湰娆″凡鎺ュ彈鐨勶級鐖跺瓙杈逛粠 start 鍑哄彂锛屽垽鏂兘鍚﹀埌杈� target銆� */
+    private static boolean reaches(Map<Long, Set<Long>> graph, Long start, Long target) {
+        Deque<Long> stack = new ArrayDeque<>();
+        Set<Long> visited = new HashSet<>();
+        stack.push(start);
+        while (!stack.isEmpty()) {
+            Long node = stack.pop();
+            if (!visited.add(node)) {
+                continue;
+            }
+            if (node.equals(target)) {
+                return true;
+            }
+            for (Long next : graph.getOrDefault(node, Set.of())) {
+                stack.push(next);
+            }
+        }
+        return false;
+    }
+
+    private static String pairKey(Long parentId, Long componentId) {
+        return parentId + ":" + componentId;
+    }
+
+    private static AgentImportPreflightRespVO.Issue issue(String code, String field, String message) {
+        return AgentImportPreflightRespVO.Issue.builder()
+                .code(code).field(field).message(message).retryable(false).build();
+    }
+
+    private static String value(AgentImportPreflightReqVO.Row row, String key) {
+        String raw = row.getValues() == null ? null : row.getValues().get(key);
+        return raw == null ? "" : raw.trim();
+    }
+
+    private static BigDecimal parseQuantity(String raw, List<AgentImportPreflightRespVO.Issue> blockers) {
+        if (StrUtil.isBlank(raw)) {
+            if (blockers != null) {
+                blockers.add(issue("INVALID_QUANTITY", "quantity", "鐢ㄩ噺蹇呴』鏄ぇ浜� 0 鐨勬暟瀛�"));
+            }
+            return null;
+        }
+        try {
+            BigDecimal quantity = new BigDecimal(raw.trim());
+            if (quantity.signum() <= 0) {
+                if (blockers != null) {
+                    blockers.add(issue("INVALID_QUANTITY", "quantity", "鐢ㄩ噺蹇呴』澶т簬 0"));
+                }
+                return quantity;
+            }
+            return quantity;
+        } catch (NumberFormatException ex) {
+            if (blockers != null) {
+                blockers.add(issue("INVALID_QUANTITY", "quantity", "鐢ㄩ噺蹇呴』鏄ぇ浜� 0 鐨勬暟瀛�"));
+            }
+            return null;
+        }
+    }
+
+    private static Integer parseStatus(String raw, List<AgentImportPreflightRespVO.Issue> blockers) {
+        if (StrUtil.isBlank(raw)) {
+            return 0;
+        }
+        String value = raw.trim();
+        if ("0".equals(value) || "1".equals(value)) {
+            return Integer.valueOf(value);
+        }
+        if (blockers != null) {
+            blockers.add(issue("INVALID_STATUS", "status", "鐘舵�佸彧鑳芥槸 0锛堝惎鐢級鎴� 1锛堢鐢級"));
+        }
+        return 0;
+    }
+}
diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/md/item/MesBomImportAdapterTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/md/item/MesBomImportAdapterTest.java
new file mode 100644
index 0000000..12f8d23
--- /dev/null
+++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/md/item/MesBomImportAdapterTest.java
@@ -0,0 +1,174 @@
+package cn.iocoder.yudao.module.mes.service.md.item;
+
+import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.bom.MesMdProductBomSaveReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdProductBomDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdProductBomMapper;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportCommitReqVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportPreflightReqVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportPreflightRespVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportRowReceipt;
+import cn.iocoder.yudao.module.system.framework.agent.AgentPrincipal;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.math.BigDecimal;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+class MesBomImportAdapterTest {
+
+    private static final AgentPrincipal PRINCIPAL = new AgentPrincipal(1L, 1L, "live");
+
+    private MesBomImportAdapter adapter;
+    private MesMdItemService itemService;
+    private MesMdProductBomService productBomService;
+    private MesMdProductBomMapper productBomMapper;
+
+    @BeforeEach
+    void setUp() {
+        adapter = new MesBomImportAdapter();
+        itemService = mock(MesMdItemService.class);
+        productBomService = mock(MesMdProductBomService.class);
+        productBomMapper = mock(MesMdProductBomMapper.class);
+        ReflectionTestUtils.setField(adapter, "itemService", itemService);
+        ReflectionTestUtils.setField(adapter, "productBomService", productBomService);
+        ReflectionTestUtils.setField(adapter, "productBomMapper", productBomMapper);
+        when(itemService.getItemByCode("P")).thenReturn(item(1L, "P"));
+        when(itemService.getItemByCode("C")).thenReturn(item(2L, "C"));
+        when(itemService.getItemByCode("A")).thenReturn(item(3L, "A"));
+        when(itemService.getItemByCode("X")).thenReturn(null);
+        when(productBomMapper.selectAll()).thenReturn(List.of());
+    }
+
+    @Test
+    void preflightCreatesWhenItemsAreSynced() {
+        AgentImportPreflightRespVO plan = adapter.preflight(PRINCIPAL, request(row("r1", "P", "C", "2")));
+
+        assertEquals("CREATE", plan.getRows().get(0).getAction());
+        assertEquals(1, plan.getSummary().getCreate());
+        assertTrue(plan.getCanCommit());
+        assertNotNull(plan.getPreflightHash());
+    }
+
+    @Test
+    void existingCombinationIsSkippedWithWarning() {
+        when(productBomMapper.selectAll()).thenReturn(List.of(bom(50L, 1L, 2L)));
+
+        AgentImportPreflightRespVO plan = adapter.preflight(PRINCIPAL, request(row("r1", "P", "C", "2")));
+
+        assertEquals("SKIP", plan.getRows().get(0).getAction());
+        assertEquals(50L, plan.getRows().get(0).getTargetId());
+        assertEquals(1, plan.getSummary().getSkip());
+        assertTrue(plan.getRows().get(0).getWarnings().stream()
+                .anyMatch(item -> "BOM_EXISTS".equals(item.getCode())));
+    }
+
+    @Test
+    void unsyncedComponentBlocksRow() {
+        AgentImportPreflightRespVO plan = adapter.preflight(PRINCIPAL, request(row("r1", "P", "X", "1")));
+
+        assertEquals("BLOCKED", plan.getRows().get(0).getAction());
+        assertTrue(plan.getRows().get(0).getBlockers().stream()
+                .anyMatch(item -> "MES_ITEM_NOT_SYNCED".equals(item.getCode())));
+        assertFalse(plan.getCanCommit());
+    }
+
+    @Test
+    void selfReferenceBlocksRow() {
+        AgentImportPreflightRespVO plan = adapter.preflight(PRINCIPAL, request(row("r1", "P", "P", "1")));
+
+        assertEquals("BLOCKED", plan.getRows().get(0).getAction());
+        assertTrue(plan.getRows().get(0).getBlockers().stream()
+                .anyMatch(item -> "BOM_SELF_REFERENCE".equals(item.getCode())));
+    }
+
+    @Test
+    void duplicateCombinationInFileBlocksSecondRow() {
+        AgentImportPreflightRespVO plan = adapter.preflight(PRINCIPAL, request(
+                row("r1", "P", "C", "1"),
+                row("r2", "P", "C", "2")));
+
+        assertEquals("CREATE", plan.getRows().get(0).getAction());
+        assertEquals("BLOCKED", plan.getRows().get(1).getAction());
+        assertTrue(plan.getRows().get(1).getBlockers().stream()
+                .anyMatch(item -> "DUPLICATE_IN_FILE".equals(item.getCode())));
+    }
+
+    @Test
+    void cycleAgainstExistingGraphBlocksRow() {
+        when(productBomMapper.selectAll()).thenReturn(List.of(bom(60L, 3L, 1L)));
+
+        AgentImportPreflightRespVO plan = adapter.preflight(PRINCIPAL, request(row("r1", "P", "A", "1")));
+
+        assertEquals("BLOCKED", plan.getRows().get(0).getAction());
+        assertTrue(plan.getRows().get(0).getBlockers().stream()
+                .anyMatch(item -> "BOM_CYCLE".equals(item.getCode())));
+    }
+
+    @Test
+    void invalidQuantityBlocksRow() {
+        AgentImportPreflightRespVO plan = adapter.preflight(PRINCIPAL, request(row("r1", "P", "C", "0")));
+
+        assertEquals("BLOCKED", plan.getRows().get(0).getAction());
+        assertTrue(plan.getRows().get(0).getBlockers().stream()
+                .anyMatch(item -> "INVALID_QUANTITY".equals(item.getCode())));
+    }
+
+    @Test
+    void commitCreatesBomWithResolvedItemIds() {
+        when(productBomService.createProductBom(any())).thenReturn(88L);
+
+        AgentImportRowReceipt receipt = adapter.commitRow(PRINCIPAL, new AgentImportCommitReqVO(),
+                AgentImportPreflightRespVO.Row.builder().candidateId("r1").action("CREATE").build(),
+                row("r1", "P", "C", "2"));
+
+        assertEquals(88L, receipt.targetId());
+        assertEquals("P", receipt.code());
+        verify(productBomService).createProductBom(any(MesMdProductBomSaveReqVO.class));
+    }
+
+    private static MesMdItemDO item(Long id, String code) {
+        MesMdItemDO item = new MesMdItemDO();
+        item.setId(id);
+        item.setCode(code);
+        return item;
+    }
+
+    private static MesMdProductBomDO bom(Long id, Long itemId, Long bomItemId) {
+        MesMdProductBomDO bom = new MesMdProductBomDO();
+        bom.setId(id);
+        bom.setItemId(itemId);
+        bom.setBomItemId(bomItemId);
+        bom.setQuantity(BigDecimal.ONE);
+        return bom;
+    }
+
+    private static AgentImportPreflightReqVO request(AgentImportPreflightReqVO.Row... rows) {
+        AgentImportPreflightReqVO request = new AgentImportPreflightReqVO();
+        request.setAttachmentId(1L);
+        request.setSourceSha256("a".repeat(64));
+        request.setProposalRevision("0");
+        request.setRows(List.of(rows));
+        return request;
+    }
+
+    private static AgentImportPreflightReqVO.Row row(String candidateId, String parent, String component,
+                                                      String quantity) {
+        Map<String, String> values = new LinkedHashMap<>();
+        values.put("parentCode", parent);
+        values.put("componentCode", component);
+        values.put("quantity", quantity);
+        AgentImportPreflightReqVO.Row row = new AgentImportPreflightReqVO.Row();
+        row.setCandidateId(candidateId);
+        row.setRowNo(1);
+        row.setValues(values);
+        return row;
+    }
+}
diff --git a/yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierImportAdapter.java b/yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierImportAdapter.java
new file mode 100644
index 0000000..01b9ded
--- /dev/null
+++ b/yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierImportAdapter.java
@@ -0,0 +1,217 @@
+package cn.iocoder.yudao.module.srm.service.supplier;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.srm.controller.admin.supplier.vo.SrmSupplierSaveReqVO;
+import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierDO;
+import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierMapper;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportCommitReqVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportModuleAdapter;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportPreflightReqVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportPreflightRespVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportRowReceipt;
+import cn.iocoder.yudao.module.system.framework.agent.AgentPrincipal;
+import cn.iocoder.yudao.module.system.service.agent.imports.SystemAgentGenericImportService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.*;
+
+/**
+ * 渚涘簲鍟嗘。妗堝鍏ワ細宸插瓨鍦ㄧ紪鐮佽烦杩囧苟缁欒鍛婏紝鍙柊寤轰笉鏇存柊銆�
+ */
+@Component
+public class SrmSupplierImportAdapter implements AgentImportModuleAdapter {
+
+    public static final String MODULE_ID = "srm.supplier";
+    private static final String PERMISSION = "srm:supplier:create";
+
+    @Resource
+    private SrmSupplierService supplierService;
+    @Resource
+    private SrmSupplierMapper supplierMapper;
+
+    @Override
+    public String moduleId() {
+        return MODULE_ID;
+    }
+
+    @Override
+    public String permission() {
+        return PERMISSION;
+    }
+
+    @Override
+    public AgentImportPreflightRespVO preflight(AgentPrincipal principal, AgentImportPreflightReqVO request) {
+        // 1. 鍏堢粺璁℃枃浠跺唴閲嶅缂栫爜锛屽叏閮ㄦ爣涓洪樆鏂紝閬垮厤鏇跨敤鎴烽�夌増鏈�
+        Map<String, Integer> codeCounts = new HashMap<>();
+        for (AgentImportPreflightReqVO.Row row : request.getRows()) {
+            String code = value(row, "code");
+            if (!code.isEmpty()) {
+                codeCounts.merge(code, 1, Integer::sum);
+            }
+        }
+
+        List<AgentImportPreflightRespVO.Row> rows = new ArrayList<>();
+        List<Map<String, Object>> resolvedPayload = new ArrayList<>();
+        int create = 0;
+        int skip = 0;
+        int blocked = 0;
+        for (AgentImportPreflightReqVO.Row row : request.getRows()) {
+            List<AgentImportPreflightRespVO.Issue> blockers = new ArrayList<>();
+            List<AgentImportPreflightRespVO.Issue> warnings = new ArrayList<>();
+            String code = value(row, "code");
+            String name = value(row, "name");
+            if (code.isEmpty()) {
+                blockers.add(issue("CODE_REQUIRED", "code", "渚涘簲鍟嗙紪鐮佷笉鑳戒负绌�"));
+            }
+            if (name.isEmpty()) {
+                blockers.add(issue("NAME_REQUIRED", "name", "渚涘簲鍟嗗悕绉颁笉鑳戒负绌�"));
+            }
+            if (!code.isEmpty() && codeCounts.getOrDefault(code, 0) > 1) {
+                blockers.add(issue("DUPLICATE_CODE_IN_FILE", "code",
+                        "鏂囦欢鍐呭瓨鍦ㄩ噸澶嶇殑渚涘簲鍟嗙紪鐮侊細" + code));
+            }
+            Integer status = parseStatus(row.getValues().get("status"), blockers);
+            BigDecimal registeredCapital = parseDecimal(row.getValues().get("registeredCapital"), blockers);
+
+            Long targetId = null;
+            String action;
+            SrmSupplierDO existing = code.isEmpty() ? null : supplierMapper.selectByCode(code);
+            if (!blockers.isEmpty()) {
+                action = "BLOCKED";
+                blocked++;
+            } else if (existing != null) {
+                action = "SKIP";
+                targetId = existing.getId();
+                skip++;
+                warnings.add(issue("SUPPLIER_EXISTS", "code",
+                        "渚涘簲鍟嗙紪鐮佸凡瀛樺湪锛屽凡璺宠繃涓斾笉浼氭洿鏂扮幇鏈夋。妗�"));
+            } else {
+                action = "CREATE";
+                create++;
+            }
+
+            Map<String, Object> resolved = new LinkedHashMap<>();
+            resolved.put("candidate_id", row.getCandidateId());
+            resolved.put("code", code);
+            resolved.put("name", name);
+            resolved.put("status", status);
+            resolved.put("action", action);
+            resolved.put("target_id", targetId);
+            resolvedPayload.add(resolved);
+
+            Map<String, Object> resolvedValues = new LinkedHashMap<>();
+            resolvedValues.put("code", code);
+            resolvedValues.put("name", name);
+            resolvedValues.put("status", status);
+            resolvedValues.put("registeredCapital", registeredCapital);
+            rows.add(AgentImportPreflightRespVO.Row.builder()
+                    .candidateId(row.getCandidateId())
+                    .rowNo(row.getRowNo())
+                    .action(action)
+                    .resolved(resolvedValues)
+                    .blockers(blockers)
+                    .warnings(warnings)
+                    .targetId(targetId)
+                    .build());
+        }
+
+        int total = rows.size();
+        return AgentImportPreflightRespVO.builder()
+                .moduleId(MODULE_ID)
+                .preflightHash(SystemAgentGenericImportService.preflightHash(MODULE_ID, request, resolvedPayload))
+                .source(source(request))
+                .summary(AgentImportPreflightRespVO.Summary.builder()
+                        .total(total).create(create).skip(skip).blocked(blocked).build())
+                .canCommit(blocked == 0)
+                .sideEffects(List.of("鍒涘缓渚涘簲鍟嗘。妗�", "涓嶆洿鏂板凡瀛樺湪鐨勪緵搴斿晢"))
+                .rows(rows)
+                .blockingItems(List.of())
+                .build();
+    }
+
+    @Override
+    public AgentImportRowReceipt commitRow(AgentPrincipal principal, AgentImportCommitReqVO request,
+                                           AgentImportPreflightRespVO.Row planned,
+                                           AgentImportPreflightReqVO.Row row) {
+        Map<String, String> values = row.getValues();
+        SrmSupplierSaveReqVO saveReqVO = new SrmSupplierSaveReqVO();
+        saveReqVO.setCode(value(row, "code"));
+        saveReqVO.setName(value(row, "name"));
+        saveReqVO.setShortName(blankToNull(values.get("shortName")));
+        saveReqVO.setType(blankToNull(values.get("type")));
+        saveReqVO.setCompanyNature(blankToNull(values.get("companyNature")));
+        saveReqVO.setRegisteredCapital(decimalOrNull(values.get("registeredCapital")));
+        saveReqVO.setBusinessLicenseNo(blankToNull(values.get("businessLicenseNo")));
+        saveReqVO.setTaxNo(blankToNull(values.get("taxNo")));
+        saveReqVO.setLegalPerson(blankToNull(values.get("legalPerson")));
+        saveReqVO.setAddress(blankToNull(values.get("address")));
+        saveReqVO.setWebsite(blankToNull(values.get("website")));
+        saveReqVO.setContactName(blankToNull(values.get("contactName")));
+        saveReqVO.setContactMobile(blankToNull(values.get("contactMobile")));
+        saveReqVO.setContactEmail(blankToNull(values.get("contactEmail")));
+        saveReqVO.setBankName(blankToNull(values.get("bankName")));
+        saveReqVO.setBankAccount(blankToNull(values.get("bankAccount")));
+        saveReqVO.setStatus(parseStatus(values.get("status"), null));
+        saveReqVO.setRemark(blankToNull(values.get("remark")));
+
+        Long id = supplierService.createSupplier(saveReqVO);
+        return AgentImportRowReceipt.created(id, saveReqVO.getCode(), saveReqVO.getName());
+    }
+
+    private static AgentImportPreflightRespVO.Source source(AgentImportPreflightReqVO request) {
+        return AgentImportPreflightRespVO.Source.builder()
+                .attachmentId(request.getAttachmentId())
+                .sessionId(request.getSessionId())
+                .sourceSha256(request.getSourceSha256())
+                .proposalRevision(request.getProposalRevision())
+                .build();
+    }
+
+    private static AgentImportPreflightRespVO.Issue issue(String code, String field, String message) {
+        return AgentImportPreflightRespVO.Issue.builder()
+                .code(code).field(field).message(message).retryable(false).build();
+    }
+
+    private static String value(AgentImportPreflightReqVO.Row row, String key) {
+        String raw = row.getValues() == null ? null : row.getValues().get(key);
+        return raw == null ? "" : raw.trim();
+    }
+
+    private static Integer parseStatus(String raw, List<AgentImportPreflightRespVO.Issue> blockers) {
+        if (StrUtil.isBlank(raw)) {
+            return 0;
+        }
+        String value = raw.trim();
+        if ("0".equals(value) || "1".equals(value)) {
+            return Integer.valueOf(value);
+        }
+        if (blockers != null) {
+            blockers.add(issue("INVALID_STATUS", "status", "鐘舵�佸彧鑳芥槸 0锛堝惎鐢級鎴� 1锛堢鐢級"));
+        }
+        return 0;
+    }
+
+    private static BigDecimal parseDecimal(String raw, List<AgentImportPreflightRespVO.Issue> blockers) {
+        if (StrUtil.isBlank(raw)) {
+            return null;
+        }
+        try {
+            return new BigDecimal(raw.trim());
+        } catch (NumberFormatException ex) {
+            if (blockers != null) {
+                blockers.add(issue("INVALID_DECIMAL", "registeredCapital", "娉ㄥ唽璧勯噾蹇呴』鏄暟瀛�"));
+            }
+            return null;
+        }
+    }
+
+    private static BigDecimal decimalOrNull(String raw) {
+        return StrUtil.isBlank(raw) ? null : new BigDecimal(raw.trim());
+    }
+
+    private static String blankToNull(String raw) {
+        return StrUtil.isBlank(raw) ? null : raw.trim();
+    }
+}
diff --git a/yudao-module-srm/src/test/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierImportAdapterTest.java b/yudao-module-srm/src/test/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierImportAdapterTest.java
new file mode 100644
index 0000000..f7ec076
--- /dev/null
+++ b/yudao-module-srm/src/test/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierImportAdapterTest.java
@@ -0,0 +1,121 @@
+package cn.iocoder.yudao.module.srm.service.supplier;
+
+import cn.iocoder.yudao.module.srm.controller.admin.supplier.vo.SrmSupplierSaveReqVO;
+import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierDO;
+import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierMapper;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportCommitReqVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportPreflightReqVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportPreflightRespVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportRowReceipt;
+import cn.iocoder.yudao.module.system.framework.agent.AgentPrincipal;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+class SrmSupplierImportAdapterTest {
+
+    private static final AgentPrincipal PRINCIPAL = new AgentPrincipal(1L, 1L, "live");
+
+    private SrmSupplierImportAdapter adapter;
+    private SrmSupplierService supplierService;
+    private SrmSupplierMapper supplierMapper;
+
+    @BeforeEach
+    void setUp() {
+        adapter = new SrmSupplierImportAdapter();
+        supplierService = mock(SrmSupplierService.class);
+        supplierMapper = mock(SrmSupplierMapper.class);
+        ReflectionTestUtils.setField(adapter, "supplierService", supplierService);
+        ReflectionTestUtils.setField(adapter, "supplierMapper", supplierMapper);
+    }
+
+    @Test
+    void preflightMarksCreateSkipAndBlockedRows() {
+        SrmSupplierDO existing = new SrmSupplierDO();
+        existing.setId(7L);
+        when(supplierMapper.selectByCode("SUP-OLD")).thenReturn(existing);
+        when(supplierMapper.selectByCode("SUP-NEW")).thenReturn(null);
+
+        AgentImportPreflightRespVO plan = adapter.preflight(PRINCIPAL, request(
+                row("r1", "SUP-NEW", "鏂颁緵搴斿晢"),
+                row("r2", "SUP-OLD", "鏃т緵搴斿晢"),
+                row("r3", "", "缂虹紪鐮�")));
+
+        assertEquals("CREATE", plan.getRows().get(0).getAction());
+        assertEquals("SKIP", plan.getRows().get(1).getAction());
+        assertEquals(7L, plan.getRows().get(1).getTargetId());
+        assertEquals("BLOCKED", plan.getRows().get(2).getAction());
+        assertEquals(1, plan.getSummary().getCreate());
+        assertEquals(1, plan.getSummary().getSkip());
+        assertEquals(1, plan.getSummary().getBlocked());
+        assertFalse(plan.getCanCommit());
+        assertNotNull(plan.getPreflightHash());
+        assertTrue(plan.getRows().get(1).getWarnings().stream()
+                .anyMatch(item -> "SUPPLIER_EXISTS".equals(item.getCode())));
+    }
+
+    @Test
+    void duplicateCodeInFileBlocksAllOccurrences() {
+        when(supplierMapper.selectByCode(any())).thenReturn(null);
+
+        AgentImportPreflightRespVO plan = adapter.preflight(PRINCIPAL, request(
+                row("r1", "SUP-DUP", "渚涘簲鍟咥"),
+                row("r2", "SUP-DUP", "渚涘簲鍟咮")));
+
+        assertEquals("BLOCKED", plan.getRows().get(0).getAction());
+        assertEquals("BLOCKED", plan.getRows().get(1).getAction());
+        assertEquals(2, plan.getSummary().getBlocked());
+        assertFalse(plan.getCanCommit());
+    }
+
+    @Test
+    void preflightHashIsStableForSameInput() {
+        when(supplierMapper.selectByCode(any())).thenReturn(null);
+        AgentImportPreflightReqVO request = request(row("r1", "SUP-NEW", "鏂颁緵搴斿晢"));
+
+        assertEquals(adapter.preflight(PRINCIPAL, request).getPreflightHash(),
+                adapter.preflight(PRINCIPAL, request).getPreflightHash());
+    }
+
+    @Test
+    void commitCreatesSupplierAndReturnsReceipt() {
+        when(supplierService.createSupplier(any())).thenReturn(99L);
+        AgentImportPreflightReqVO.Row row = row("r1", "SUP-NEW", "鏂颁緵搴斿晢");
+
+        AgentImportRowReceipt receipt = adapter.commitRow(PRINCIPAL, new AgentImportCommitReqVO(),
+                AgentImportPreflightRespVO.Row.builder().candidateId("r1").action("CREATE").build(), row);
+
+        assertEquals("CREATE", receipt.action());
+        assertEquals(99L, receipt.targetId());
+        assertEquals("SUP-NEW", receipt.code());
+        verify(supplierService).createSupplier(any(SrmSupplierSaveReqVO.class));
+    }
+
+    private static AgentImportPreflightReqVO request(AgentImportPreflightReqVO.Row... rows) {
+        AgentImportPreflightReqVO request = new AgentImportPreflightReqVO();
+        request.setAttachmentId(1L);
+        request.setSourceSha256("a".repeat(64));
+        request.setProposalRevision("0");
+        request.setRows(List.of(rows));
+        return request;
+    }
+
+    private static AgentImportPreflightReqVO.Row row(String candidateId, String code, String name) {
+        Map<String, String> values = new LinkedHashMap<>();
+        values.put("code", code);
+        values.put("name", name);
+        AgentImportPreflightReqVO.Row row = new AgentImportPreflightReqVO.Row();
+        row.setCandidateId(candidateId);
+        row.setRowNo(1);
+        row.setValues(values);
+        return row;
+    }
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportCommitReqVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportCommitReqVO.java
new file mode 100644
index 0000000..e634843
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportCommitReqVO.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.system.api.agent.imports;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 閫氱敤涓氬姟瀵煎叆鎻愪氦璇锋眰锛氬湪棰勬璇锋眰涓婅拷鍔犲箓绛変笌璁″垝鎽樿銆�
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AgentImportCommitReqVO extends AgentImportPreflightReqVO {
+
+    @NotBlank(message = "鍛戒护缂栧彿涓嶈兘涓虹┖")
+    private String commandId;
+
+    @NotBlank(message = "骞傜瓑閿笉鑳戒负绌�")
+    private String idempotencyKey;
+
+    @NotBlank(message = "棰勬鎽樿涓嶈兘涓虹┖")
+    private String preflightHash;
+
+    private String proposalHash;
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportModuleAdapter.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportModuleAdapter.java
new file mode 100644
index 0000000..d1a67be
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportModuleAdapter.java
@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.system.api.agent.imports;
+
+import cn.iocoder.yudao.module.system.framework.agent.AgentPrincipal;
+
+/**
+ * 涓氬姟妯″潡瀵煎叆閫傞厤鍣細system 妯″潡璐熻矗骞傜瓑銆侀�愯浜嬪姟涓庡洖鎵э紝閫傞厤鍣ㄥ彧璐熻矗鍗曡鏍¢獙鍜屽啓鍏ャ��
+ */
+public interface AgentImportModuleAdapter {
+
+    /** 涓� Python 渚� ModuleSpec.module_id 瀵归綈锛屽 srm.supplier銆� */
+    String moduleId();
+
+    /** 鎻愪氦璇ユā鍧楁墍闇�鐨勪笟鍔℃潈闄愩�� */
+    String permission();
+
+    /** 鍙棰勬锛涘繀椤昏繑鍥炵ǔ瀹氱殑 preflightHash 渚涙彁浜ゆ椂姣斿銆� */
+    AgentImportPreflightRespVO preflight(AgentPrincipal principal, AgentImportPreflightReqVO request);
+
+    /** 鍗曡鍐欏叆锛涘け璐ユ椂鎶涘嚭寮傚父锛岀敱缂栨帓灞傚啓鍏� FAILED 鍥炴墽銆� */
+    AgentImportRowReceipt commitRow(AgentPrincipal principal, AgentImportCommitReqVO request,
+                                    AgentImportPreflightRespVO.Row planned,
+                                    AgentImportPreflightReqVO.Row row);
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportPreflightReqVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportPreflightReqVO.java
new file mode 100644
index 0000000..bf7ec7b
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportPreflightReqVO.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.system.api.agent.imports;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 閫氱敤涓氬姟瀵煎叆棰勬璇锋眰銆傚彧璇伙紝涓嶅啓涓氬姟琛ㄣ��
+ */
+@Data
+public class AgentImportPreflightReqVO {
+
+    @NotNull(message = "闄勪欢缂栧彿涓嶈兘涓虹┖")
+    private Long attachmentId;
+
+    private Long sessionId;
+
+    @NotBlank(message = "闄勪欢鎽樿涓嶈兘涓虹┖")
+    private String sourceSha256;
+
+    private String proposalRevision;
+
+    /** 妯″潡绾ч�夐」锛屽 syncMes锛涙湭璇嗗埆鐨勯敭鐢遍�傞厤鍣ㄥ拷鐣ャ�� */
+    private Map<String, Object> options;
+
+    @NotEmpty(message = "瀵煎叆琛屼笉鑳戒负绌�")
+    @Valid
+    private List<Row> rows;
+
+    @Data
+    public static class Row {
+
+        @NotBlank(message = "琛屾爣璇嗕笉鑳戒负绌�")
+        private String candidateId;
+
+        private Integer rowNo;
+
+        @NotEmpty(message = "琛屾暟鎹笉鑳戒负绌�")
+        private Map<String, String> values;
+    }
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportPreflightRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportPreflightRespVO.java
new file mode 100644
index 0000000..83cf2ad
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportPreflightRespVO.java
@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.module.system.api.agent.imports;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 閫氱敤涓氬姟瀵煎叆棰勬鍝嶅簲锛屽瓧娈典笌 Python 绔� GenericPreflightResponse 瀵归綈銆�
+ */
+@Data
+@Builder
+public class AgentImportPreflightRespVO {
+
+    private String moduleId;
+    private String preflightHash;
+    private Source source;
+    private Map<String, Object> policy;
+    private Summary summary;
+    private Boolean canCommit;
+    private List<String> sideEffects;
+    private List<Row> rows;
+    private List<Issue> blockingItems;
+
+    @Data
+    @Builder
+    public static class Source {
+        private Long attachmentId;
+        private Long sessionId;
+        private String sourceSha256;
+        private String proposalRevision;
+    }
+
+    @Data
+    @Builder
+    public static class Summary {
+        private Integer total;
+        private Integer create;
+        private Integer skip;
+        private Integer blocked;
+    }
+
+    @Data
+    @Builder
+    public static class Row {
+        private String candidateId;
+        private Integer rowNo;
+        /** CREATE / SKIP / BLOCKED */
+        private String action;
+        private Map<String, Object> resolved;
+        private List<Issue> blockers;
+        private List<Issue> warnings;
+        private Long targetId;
+    }
+
+    @Data
+    @Builder
+    public static class Issue {
+        private String code;
+        private String field;
+        private String message;
+        private Boolean retryable;
+    }
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportReceiptRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportReceiptRespVO.java
new file mode 100644
index 0000000..5bcf372
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportReceiptRespVO.java
@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.system.api.agent.imports;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 閫氱敤涓氬姟瀵煎叆鍥炴墽锛屽瓧娈典笌 Python 绔� CommitReceipt 瀵归綈銆�
+ */
+@Data
+@Builder
+public class AgentImportReceiptRespVO {
+
+    private String commandId;
+    private String idempotencyKey;
+    private String status;
+    private Long attachmentId;
+    private Long sessionId;
+    private String sourceSha256;
+    private String preflightHash;
+    private String proposalHash;
+    private Summary summary;
+    private String errorCode;
+    private String errorMessage;
+    private List<Row> rows;
+    private LocalDateTime createTime;
+    private LocalDateTime completedTime;
+
+    @Data
+    @Builder
+    public static class Summary {
+        private Integer total;
+        private Integer success;
+        private Integer failed;
+        private Integer skipped;
+        private Integer unknown;
+    }
+
+    @Data
+    @Builder
+    public static class Row {
+        private String candidateId;
+        private Integer rowNo;
+        private String code;
+        private String name;
+        private String action;
+        /** SUCCESS / FAILED / SKIPPED / UNKNOWN */
+        private String status;
+        private Long targetItemId;
+        private String targetVersion;
+        private String errorCode;
+        private String errorMessage;
+        private Boolean retryable;
+    }
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportRowReceipt.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportRowReceipt.java
new file mode 100644
index 0000000..ce74ca9
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportRowReceipt.java
@@ -0,0 +1,12 @@
+package cn.iocoder.yudao.module.system.api.agent.imports;
+
+/**
+ * 鍗曡鍐欏叆鎴愬姛鍚庣殑鏈�灏忓洖鎵с��
+ */
+public record AgentImportRowReceipt(String action, Long targetId, String targetVersion,
+                                    String code, String name) {
+
+    public static AgentImportRowReceipt created(Long targetId, String code, String name) {
+        return new AgentImportRowReceipt("CREATE", targetId, null, code, name);
+    }
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentGenericImportBridgeController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentGenericImportBridgeController.java
new file mode 100644
index 0000000..eab9aba
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentGenericImportBridgeController.java
@@ -0,0 +1,66 @@
+package cn.iocoder.yudao.module.system.controller.admin.agent;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportCommitReqVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportPreflightReqVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportPreflightRespVO;
+import cn.iocoder.yudao.module.system.api.agent.imports.AgentImportReceiptRespVO;
+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.agent.imports.SystemAgentGenericImportService;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+/**
+ * 閫氱敤涓氬姟瀵煎叆妗ユ帴锛氶妫�銆佹彁浜や笌鍥炴墽銆�
+ * 鍏蜂綋涓氬姟瑙勫垯鐢卞悇妯″潡鐨� {@code AgentImportModuleAdapter} 瀹炵幇锛屾湰绔彧鍋氶壌鏉冧笌缂栨帓鍏ュ彛銆�
+ */
+@RestController
+@RequestMapping("/agent/bridge/imports")
+public class SystemAgentGenericImportBridgeController {
+
+    @Resource
+    private SystemAgentGenericImportService genericImportService;
+    @Resource
+    private SystemAgentBridgeGuard bridgeGuard;
+
+    @GetMapping
+    public CommonResult<List<Map<String, String>>> modules(
+            @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken) {
+        bridgeGuard.requireAny(serviceToken);
+        return success(genericImportService.modules());
+    }
+
+    @PostMapping("/{moduleId}/preflight")
+    public CommonResult<AgentImportPreflightRespVO> preflight(
+            @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+            @PathVariable String moduleId,
+            @Valid @RequestBody AgentImportPreflightReqVO request) {
+        AgentPrincipal principal = bridgeGuard.requireAny(serviceToken);
+        return success(genericImportService.preflight(moduleId, request, principal));
+    }
+
+    @PostMapping("/{moduleId}/commit")
+    public CommonResult<AgentImportReceiptRespVO> commit(
+            @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+            @PathVariable String moduleId,
+            @Valid @RequestBody AgentImportCommitReqVO request) {
+        AgentPrincipal principal = bridgeGuard.requireAny(serviceToken);
+        return success(genericImportService.commit(moduleId, request, principal));
+    }
+
+    @GetMapping("/{moduleId}/commands/{commandId}")
+    public CommonResult<AgentImportReceiptRespVO> getCommand(
+            @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+            @PathVariable String moduleId,
+            @PathVariable String commandId) {
+        AgentPrincipal principal = bridgeGuard.requireAny(serviceToken);
+        return success(genericImportService.getCommand(moduleId, commandId, principal));
+    }
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentImportBridgeController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentImportBridgeController.java
index bbba5e3..0f8c195 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentImportBridgeController.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentImportBridgeController.java
@@ -31,33 +31,56 @@
 
     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");
+            "material_import.read_receipt",
+            "conversation.create", "conversation.read", "conversation.message",
+            "conversation.action", "conversation.report");
+
+    /** module_id 鈫� 涓氬姟瀵煎叆鏉冮檺锛汸ython 浠ユā鍧� 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("@ss.hasPermission('mdm:item:import')")
+    @PreAuthorize(IMPORT_PERMISSIONS_EXPR)
     public CommonResult<Long> createSession() {
         return success(bridgeService.createSession());
     }
 
     @PostMapping("/import-sessions/{sessionId}/attachments")
-    @PreAuthorize("@ss.hasPermission('mdm:item:import')")
+    @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("@ss.hasPermission('mdm:item:import')")
+    @PreAuthorize(IMPORT_PERMISSIONS_EXPR)
     public CommonResult<Map<String, String>> authorize(
             @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
             @Valid @RequestBody AuthorizeRequest request) {
-        AgentPrincipal principal = bridgeGuard.require(serviceToken, SystemAgentBridgeGuard.PERMISSION_ITEM_IMPORT);
         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()));
@@ -69,11 +92,11 @@
     }
 
     @GetMapping("/attachments/{attachmentId}")
-    @PreAuthorize("@ss.hasPermission('mdm:item:import')")
+    @PreAuthorize(IMPORT_PERMISSIONS_EXPR)
     public ResponseEntity<FileSystemResource> downloadAttachment(
             @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
             @PathVariable Long attachmentId) {
-        bridgeGuard.require(serviceToken, SystemAgentBridgeGuard.PERMISSION_ITEM_IMPORT);
+        bridgeGuard.requireAny(serviceToken);
         SystemAgentAttachmentRef ref = bridgeService.requireOwnedAttachmentRef(attachmentId);
         return ResponseEntity.ok().contentType(MediaType.APPLICATION_OCTET_STREAM)
                 .contentLength(ref.byteSize())
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/agent/AgentImportCommandDO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/agent/AgentImportCommandDO.java
new file mode 100644
index 0000000..7cd61a9
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/agent/AgentImportCommandDO.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.system.dal.dataobject.agent;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * 閫氱敤涓氬姟瀵煎叆鍛戒护銆傚悓涓� (tenant, owner, module, commandId/idempotencyKey) 鍙厑璁镐竴鏉°��
+ */
+@TableName("agent_import_command")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AgentImportCommandDO extends BaseDO {
+
+    @TableId
+    private Long id;
+
+    private String commandId;
+    private String idempotencyKey;
+    private String requestDigest;
+    private String moduleId;
+    private Long tenantId;
+    private Long ownerUserId;
+    private Long sessionId;
+    private Long attachmentId;
+    private String sourceSha256;
+    private String preflightHash;
+    private String proposalHash;
+    private String status;
+    private Integer totalCount;
+    private Integer successCount;
+    private Integer failedCount;
+    private Integer skippedCount;
+    private String errorCode;
+    private String errorMessage;
+    private LocalDateTime leaseExpiresAt;
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/agent/AgentImportCommandRowDO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/agent/AgentImportCommandRowDO.java
new file mode 100644
index 0000000..50a9053
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/agent/AgentImportCommandRowDO.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.system.dal.dataobject.agent;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 閫氱敤涓氬姟瀵煎叆閫愯鍥炴墽銆�
+ */
+@TableName("agent_import_command_row")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AgentImportCommandRowDO extends BaseDO {
+
+    @TableId
+    private Long id;
+
+    private String commandId;
+    private String moduleId;
+    private String candidateId;
+    private Integer sourceRow;
+    private String code;
+    private String name;
+    private String action;
+    private String status;
+    private Long targetItemId;
+    private String targetVersion;
+    private String errorCode;
+    private String errorMessage;
+    private Boolean retryable;
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/agent/AgentImportCommandMapper.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/agent/AgentImportCommandMapper.java
new file mode 100644
index 0000000..1308c76
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/agent/AgentImportCommandMapper.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.system.dal.mysql.agent;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.system.dal.dataobject.agent.AgentImportCommandDO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AgentImportCommandMapper extends BaseMapperX<AgentImportCommandDO> {
+
+    default AgentImportCommandDO selectByCommandId(Long tenantId, Long ownerUserId,
+                                                   String moduleId, String commandId) {
+        return selectOne(new LambdaQueryWrapperX<AgentImportCommandDO>()
+                .eq(AgentImportCommandDO::getTenantId, tenantId)
+                .eq(AgentImportCommandDO::getOwnerUserId, ownerUserId)
+                .eq(AgentImportCommandDO::getModuleId, moduleId)
+                .eq(AgentImportCommandDO::getCommandId, commandId));
+    }
+
+    default AgentImportCommandDO selectByIdempotencyKey(Long tenantId, Long ownerUserId,
+                                                        String moduleId, String idempotencyKey) {
+        return selectOne(new LambdaQueryWrapperX<AgentImportCommandDO>()
+                .eq(AgentImportCommandDO::getTenantId, tenantId)
+                .eq(AgentImportCommandDO::getOwnerUserId, ownerUserId)
+                .eq(AgentImportCommandDO::getModuleId, moduleId)
+                .eq(AgentImportCommandDO::getIdempotencyKey, idempotencyKey));
+    }
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/agent/AgentImportCommandRowMapper.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/agent/AgentImportCommandRowMapper.java
new file mode 100644
index 0000000..870e6a9
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/agent/AgentImportCommandRowMapper.java
@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.system.dal.mysql.agent;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.system.dal.dataobject.agent.AgentImportCommandRowDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface AgentImportCommandRowMapper extends BaseMapperX<AgentImportCommandRowDO> {
+
+    default List<AgentImportCommandRowDO> selectListByCommandId(String commandId) {
+        return selectList(AgentImportCommandRowDO::getCommandId, commandId);
+    }
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentBridgeGuard.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentBridgeGuard.java
index f79576d..c5514ee 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentBridgeGuard.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentBridgeGuard.java
@@ -10,12 +10,22 @@
 
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
+import java.util.List;
 
 /** 妗ユ帴缁熶竴閴存潈锛氭湇鍔′护鐗� + 褰撳墠鐧诲綍鐢ㄦ埛 + 涓氬姟鏉冮檺锛岀己涓�涓嶅彲銆� */
 @Component
 public class SystemAgentBridgeGuard {
 
     public static final String PERMISSION_ITEM_IMPORT = "mdm:item:import";
+
+    /** 鍚勪笟鍔℃ā鍧楃殑瀵煎叆鏉冮檺锛涗細璇濆垱寤轰笌闄勪欢璇诲彇鍙渶鍛戒腑鍏朵腑浠绘剰涓�椤广�� */
+    public static final List<String> IMPORT_PERMISSIONS = List.of(
+            PERMISSION_ITEM_IMPORT,
+            "srm:supplier:create",
+            "mes:md-item:create",
+            "mes:qc-template:create",
+            "mes:pro-mps:create",
+            "mes:pro-route:create");
 
     @Value("${yudao.agent.bridge-service-token:}")
     private String bridgeServiceToken;
@@ -40,8 +50,25 @@
         }
         if (!securityFrameworkService.hasPermission(permission)) {
             throw new AgentBridgeException(HttpStatus.FORBIDDEN, "FORBIDDEN",
-                    "褰撳墠鐢ㄦ埛缂哄皯鐗╂枡瀵煎叆鏉冮檺锛�" + permission + "锛�");
+                    "褰撳墠鐢ㄦ埛缂哄皯鏁版嵁瀵煎叆鏉冮檺锛�" + permission + "锛�");
         }
+        return principalOf(userId);
+    }
+
+    /** 浼氳瘽涓庨檮浠跺叆鍙f棤娉曢鐭ユā鍧楋紝鍙姹傚叿澶囦换鎰忎竴椤瑰鍏ユ潈闄愩�� */
+    public AgentPrincipal requireAny(String providedServiceToken) {
+        verifyServiceToken(providedServiceToken);
+        Long userId = SecurityFrameworkUtils.getLoginUserId();
+        if (userId == null) {
+            throw new AgentBridgeException(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "鏈櫥褰曟垨鐧诲綍宸茶繃鏈�");
+        }
+        if (!securityFrameworkService.hasAnyPermissions(IMPORT_PERMISSIONS.toArray(String[]::new))) {
+            throw new AgentBridgeException(HttpStatus.FORBIDDEN, "FORBIDDEN", "褰撳墠鐢ㄦ埛缂哄皯鏁版嵁瀵煎叆鏉冮檺");
+        }
+        return principalOf(userId);
+    }
+
+    private AgentPrincipal principalOf(Long userId) {
         Long tenantId = TenantContextHolder.getTenantId();
         return new AgentPrincipal(userId, tenantId == null ? 0L : tenantId, "live");
     }
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/agent/imports/SystemAgentGenericImportService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/agent/imports/SystemAgentGenericImportService.java
new file mode 100644
index 0000000..3a924ab
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/agent/imports/SystemAgentGenericImportService.java
@@ -0,0 +1,454 @@
+package cn.iocoder.yudao.module.system.service.agent.imports;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
+import cn.iocoder.yudao.module.system.api.agent.imports.*;
+import cn.iocoder.yudao.module.system.dal.dataobject.agent.AgentImportCommandDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.agent.AgentImportCommandRowDO;
+import cn.iocoder.yudao.module.system.dal.mysql.agent.AgentImportCommandMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.agent.AgentImportCommandRowMapper;
+import cn.iocoder.yudao.module.system.framework.agent.AgentBridgeException;
+import cn.iocoder.yudao.module.system.framework.agent.AgentPrincipal;
+import cn.iocoder.yudao.module.system.service.storage.SystemAgentImportBridgeService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.Resource;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.support.TransactionTemplate;
+import org.springframework.validation.annotation.Validated;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.*;
+
+/**
+ * 閫氱敤涓氬姟瀵煎叆缂栨帓锛氶�傞厤鍣ㄥ彧璐熻矗鍗曡鏍¢獙/鍐欏叆锛屾湰绫荤粺涓�澶勭悊鏉冮檺銆佸箓绛夈�侀�愯浜嬪姟涓庡洖鎵с��
+ */
+@Service
+@Validated
+public class SystemAgentGenericImportService {
+
+    public static final String STATUS_RUNNING = "RUNNING";
+    public static final String STATUS_SUCCEEDED = "SUCCEEDED";
+    public static final String STATUS_PARTIAL_FAILED = "PARTIAL_FAILED";
+    public static final String STATUS_FAILED = "FAILED";
+    public static final String STATUS_UNKNOWN = "UNKNOWN";
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final Duration LEASE_DURATION = Duration.ofMinutes(5);
+
+    @Resource
+    private ObjectProvider<AgentImportModuleAdapter> adapterProvider;
+    @Resource
+    private SecurityFrameworkService securityFrameworkService;
+    @Resource
+    private SystemAgentImportBridgeService attachmentService;
+    @Resource
+    private AgentImportCommandMapper commandMapper;
+    @Resource
+    private AgentImportCommandRowMapper commandRowMapper;
+    @Resource
+    private PlatformTransactionManager transactionManager;
+
+    @Value("${yudao.agent.bridge.write-enabled:false}")
+    private boolean writeEnabled;
+
+    @Value("${yudao.agent.bridge.max-rows:5000}")
+    private int maxRows;
+
+    public List<Map<String, String>> modules() {
+        return adapters().values().stream()
+                .map(adapter -> Map.of("moduleId", adapter.moduleId(), "permission", adapter.permission()))
+                .toList();
+    }
+
+    public AgentImportPreflightRespVO preflight(String moduleId, AgentImportPreflightReqVO request,
+                                                AgentPrincipal principal) {
+        AgentImportModuleAdapter adapter = requireAdapter(moduleId);
+        requirePermission(adapter);
+        validateAttachment(request.getAttachmentId(), request.getSourceSha256());
+        validateRows(request);
+        AgentImportPreflightRespVO result = adapter.preflight(principal, request);
+        validatePlan(result, request);
+        return result;
+    }
+
+    public AgentImportReceiptRespVO commit(String moduleId, AgentImportCommitReqVO request,
+                                           AgentPrincipal principal) {
+        if (!writeEnabled) {
+            throw new AgentBridgeException(HttpStatus.SERVICE_UNAVAILABLE, "WRITE_DISABLED",
+                    "鏅鸿兘浣撳啓鍏ュ紑鍏虫湭鎵撳紑锛屾彁浜ゅ凡鎷掔粷");
+        }
+        AgentImportModuleAdapter adapter = requireAdapter(moduleId);
+        requirePermission(adapter);
+        validateAttachment(request.getAttachmentId(), request.getSourceSha256());
+        validateRows(request);
+
+        Long tenantId = principal.tenantId();
+        Long ownerUserId = principal.userId();
+        String digest = requestDigest(moduleId, request);
+
+        // 1. 骞傜瓑鍛戒腑锛氬懡浠� ID 浼樺厛锛屽叾娆″箓绛夐敭
+        AgentImportCommandDO existing = commandMapper.selectByCommandId(
+                tenantId, ownerUserId, moduleId, request.getCommandId());
+        if (existing != null) {
+            requireSameDigest(existing, digest);
+            return toReceipt(existing);
+        }
+        existing = commandMapper.selectByIdempotencyKey(
+                tenantId, ownerUserId, moduleId, request.getIdempotencyKey());
+        if (existing != null) {
+            requireSameDigest(existing, digest);
+            return toReceipt(existing);
+        }
+
+        // 2. 閲嶆柊棰勬骞舵瘮瀵硅鍒掓憳瑕侊紝闃叉纭鍚庝富鏁版嵁鍙樺寲
+        AgentImportPreflightRespVO plan = adapter.preflight(principal, request);
+        validatePlan(plan, request);
+        if (!StrUtil.equalsIgnoreCase(request.getPreflightHash(), plan.getPreflightHash())) {
+            throw new AgentBridgeException(HttpStatus.CONFLICT, "PLAN_STALE",
+                    "棰勬璁″垝宸插彉鍖栵紝璇烽噸鏂伴妫�骞剁‘璁�", false, Map.of("preflight", plan));
+        }
+        if (!Boolean.TRUE.equals(plan.getCanCommit())) {
+            throw new AgentBridgeException(HttpStatus.UNPROCESSABLE_ENTITY, "PREFLIGHT_BLOCKED",
+                    "棰勬瀛樺湪闃绘柇椤癸紝涓嶈兘鎻愪氦", false, Map.of("preflight", plan));
+        }
+
+        // 3. 鐧昏鍛戒护锛堝敮涓�閿厹浣忓苟鍙戦噸澶嶆彁浜わ級
+        AgentImportCommandDO command = AgentImportCommandDO.builder()
+                .commandId(request.getCommandId())
+                .idempotencyKey(request.getIdempotencyKey())
+                .requestDigest(digest)
+                .moduleId(moduleId)
+                .tenantId(tenantId)
+                .ownerUserId(ownerUserId)
+                .sessionId(request.getSessionId())
+                .attachmentId(request.getAttachmentId())
+                .sourceSha256(request.getSourceSha256().toLowerCase(Locale.ROOT))
+                .preflightHash(request.getPreflightHash().toLowerCase(Locale.ROOT))
+                .proposalHash(request.getProposalHash())
+                .status(STATUS_RUNNING)
+                .totalCount(request.getRows().size())
+                .successCount(0).failedCount(0).skippedCount(0)
+                .leaseExpiresAt(LocalDateTime.now().plus(LEASE_DURATION))
+                .build();
+        try {
+            commandMapper.insert(command);
+        } catch (DuplicateKeyException ex) {
+            AgentImportCommandDO raced = commandMapper.selectByCommandId(
+                    tenantId, ownerUserId, moduleId, request.getCommandId());
+            if (raced == null) {
+                raced = commandMapper.selectByIdempotencyKey(
+                        tenantId, ownerUserId, moduleId, request.getIdempotencyKey());
+            }
+            if (raced == null) {
+                throw ex;
+            }
+            requireSameDigest(raced, digest);
+            return toReceipt(raced);
+        }
+
+        // 4. 閫愯鐙珛浜嬪姟鎵ц锛涘崟琛屽け璐ヤ笉褰卞搷鍏朵粬琛�
+        int success = 0;
+        int failed = 0;
+        int skipped = 0;
+        TransactionTemplate rowTx = new TransactionTemplate(transactionManager);
+        try {
+            for (int index = 0; index < request.getRows().size(); index++) {
+                AgentImportPreflightReqVO.Row row = request.getRows().get(index);
+                AgentImportPreflightRespVO.Row planned = plan.getRows().get(index);
+                if ("SKIP".equals(planned.getAction())) {
+                    commandRowMapper.insert(baseRow(moduleId, request.getCommandId(), row)
+                            .action("SKIP").status("SKIPPED")
+                            .targetItemId(planned.getTargetId()).retryable(false).build());
+                    skipped++;
+                    continue;
+                }
+                if (!"CREATE".equals(planned.getAction())) {
+                    commandRowMapper.insert(baseRow(moduleId, request.getCommandId(), row)
+                            .action("CREATE").status("FAILED")
+                            .errorCode("PREFLIGHT_BLOCKED").errorMessage("璇ヨ棰勬鏈�氳繃").retryable(false).build());
+                    failed++;
+                    continue;
+                }
+                try {
+                    AgentImportRowReceipt receipt = rowTx.execute(status ->
+                            adapter.commitRow(principal, request, planned, row));
+                    if (receipt == null) {
+                        throw new IllegalStateException("閫傞厤鍣ㄦ湭杩斿洖鍥炴墽");
+                    }
+                    commandRowMapper.insert(baseRow(moduleId, request.getCommandId(), row)
+                            .action(StrUtil.blankToDefault(receipt.action(), "CREATE"))
+                            .status("SUCCESS")
+                            .targetItemId(receipt.targetId())
+                            .targetVersion(receipt.targetVersion())
+                            .code(StrUtil.blankToDefault(receipt.code(), row.getValues().get("code")))
+                            .name(StrUtil.blankToDefault(receipt.name(), row.getValues().get("name")))
+                            .retryable(false).build());
+                    success++;
+                } catch (Exception ex) {
+                    recordFailure(moduleId, request.getCommandId(), row, ex);
+                    failed++;
+                }
+            }
+        } catch (Exception ex) {
+            command.setStatus(STATUS_UNKNOWN);
+            command.setErrorCode("COMMIT_INTERRUPTED");
+            command.setErrorMessage(trim(ex.getMessage(), 512));
+            command.setSuccessCount(success);
+            command.setFailedCount(failed);
+            command.setSkippedCount(skipped);
+            command.setUpdateTime(LocalDateTime.now());
+            commandMapper.updateById(command);
+            return toReceipt(command);
+        }
+
+        command.setSuccessCount(success);
+        command.setFailedCount(failed);
+        command.setSkippedCount(skipped);
+        command.setStatus(failed == 0 ? STATUS_SUCCEEDED
+                : (success + skipped == 0 ? STATUS_FAILED : STATUS_PARTIAL_FAILED));
+        command.setLeaseExpiresAt(LocalDateTime.now());
+        command.setUpdateTime(LocalDateTime.now());
+        commandMapper.updateById(command);
+        return toReceipt(command);
+    }
+
+    public AgentImportReceiptRespVO getCommand(String moduleId, String commandId, AgentPrincipal principal) {
+        requireAdapter(moduleId);
+        AgentImportCommandDO command = commandMapper.selectByCommandId(
+                principal.tenantId(), principal.userId(), moduleId, commandId);
+        if (command == null) {
+            throw new AgentBridgeException(HttpStatus.NOT_FOUND, "COMMAND_NOT_FOUND", "瀵煎叆鍛戒护涓嶅瓨鍦�");
+        }
+        return toReceipt(command);
+    }
+
+    private void recordFailure(String moduleId, String commandId, AgentImportPreflightReqVO.Row row,
+                               Exception ex) {
+        String errorCode = "ROW_WRITE_FAILED";
+        boolean retryable = true;
+        String message = ex.getMessage();
+        if (ex instanceof AgentBridgeException bridge) {
+            errorCode = bridge.getErrorCode();
+            retryable = bridge.isRetryable();
+            message = bridge.getReason();
+        } else if (ex instanceof ServiceException service) {
+            errorCode = "ROW_WRITE_REJECTED";
+            retryable = false;
+            message = service.getMessage();
+        }
+        commandRowMapper.insert(baseRow(moduleId, commandId, row)
+                .action("CREATE").status("FAILED")
+                .errorCode(errorCode).errorMessage(trim(message, 512)).retryable(retryable).build());
+    }
+
+    private AgentImportPreflightRespVO.Row requirePlanRow(AgentImportPreflightRespVO plan, int index) {
+        if (plan.getRows() == null || index >= plan.getRows().size()) {
+            throw new AgentBridgeException(HttpStatus.BAD_GATEWAY, "ADAPTER_CONTRACT_INVALID",
+                    "閫傞厤鍣ㄨ繑鍥炵殑棰勬琛屾暟涓庤姹備笉涓�鑷�");
+        }
+        return plan.getRows().get(index);
+    }
+
+    private void validatePlan(AgentImportPreflightRespVO plan, AgentImportPreflightReqVO request) {
+        if (plan == null || StrUtil.isBlank(plan.getPreflightHash())) {
+            throw new AgentBridgeException(HttpStatus.BAD_GATEWAY, "ADAPTER_CONTRACT_INVALID",
+                    "閫傞厤鍣ㄦ湭杩斿洖棰勬鎽樿");
+        }
+        if (plan.getRows() == null || plan.getRows().size() != request.getRows().size()) {
+            throw new AgentBridgeException(HttpStatus.BAD_GATEWAY, "ADAPTER_CONTRACT_INVALID",
+                    "閫傞厤鍣ㄨ繑鍥炵殑棰勬琛屾暟涓庤姹備笉涓�鑷�");
+        }
+        for (int index = 0; index < plan.getRows().size(); index++) {
+            if (!request.getRows().get(index).getCandidateId()
+                    .equals(plan.getRows().get(index).getCandidateId())) {
+                throw new AgentBridgeException(HttpStatus.BAD_GATEWAY, "ADAPTER_CONTRACT_INVALID",
+                        "閫傞厤鍣ㄨ繑鍥炵殑棰勬琛岄『搴忎笌璇锋眰涓嶄竴鑷�");
+            }
+        }
+    }
+
+    private void validateAttachment(Long attachmentId, String sourceSha256) {
+        String actual = attachmentService.sha256OfAttachment(attachmentId);
+        if (!StrUtil.equalsIgnoreCase(actual, sourceSha256)) {
+            throw new AgentBridgeException(HttpStatus.CONFLICT, "SOURCE_CHANGED",
+                    "闄勪欢鍐呭宸插彉鍖栵紝璇烽噸鏂颁笂浼犲悗鍐嶈瘯", true, null);
+        }
+    }
+
+    private void validateRows(AgentImportPreflightReqVO request) {
+        if (request.getRows() == null || request.getRows().isEmpty()) {
+            throw new AgentBridgeException(HttpStatus.UNPROCESSABLE_ENTITY, "ROWS_EMPTY", "娌℃湁鍙鍏ョ殑鏁版嵁琛�");
+        }
+        if (request.getRows().size() > maxRows) {
+            throw new AgentBridgeException(HttpStatus.UNPROCESSABLE_ENTITY, "ROWS_TOO_MANY",
+                    "鍗曟瀵煎叆瓒呰繃 " + maxRows + " 琛岋紝璇锋媶鍒嗘枃浠�");
+        }
+    }
+
+    private void requirePermission(AgentImportModuleAdapter adapter) {
+        if (!securityFrameworkService.hasPermission(adapter.permission())) {
+            throw new AgentBridgeException(HttpStatus.FORBIDDEN, "FORBIDDEN",
+                    "褰撳墠鐢ㄦ埛缂哄皯璇ユā鍧楃殑瀵煎叆鏉冮檺锛�" + adapter.permission() + "锛�");
+        }
+    }
+
+    private AgentImportModuleAdapter requireAdapter(String moduleId) {
+        AgentImportModuleAdapter adapter = adapters().get(moduleId);
+        if (adapter == null) {
+            throw new AgentBridgeException(HttpStatus.NOT_FOUND, "MODULE_NOT_SUPPORTED",
+                    "涓嶆敮鎸佺殑瀵煎叆妯″潡锛�" + moduleId);
+        }
+        return adapter;
+    }
+
+    private Map<String, AgentImportModuleAdapter> adapters() {
+        Map<String, AgentImportModuleAdapter> result = new LinkedHashMap<>();
+        for (AgentImportModuleAdapter adapter : adapterProvider) {
+            result.put(adapter.moduleId(), adapter);
+        }
+        return result;
+    }
+
+    private static AgentImportCommandRowDO.AgentImportCommandRowDOBuilder baseRow(
+            String moduleId, String commandId, AgentImportPreflightReqVO.Row row) {
+        return AgentImportCommandRowDO.builder()
+                .commandId(commandId)
+                .moduleId(moduleId)
+                .candidateId(row.getCandidateId())
+                .sourceRow(row.getRowNo())
+                .code(row.getValues().get("code"))
+                .name(row.getValues().get("name"));
+    }
+
+    private static void requireSameDigest(AgentImportCommandDO command, String digest) {
+        if (!command.getRequestDigest().equals(digest)) {
+            throw new AgentBridgeException(HttpStatus.CONFLICT, "IDEMPOTENCY_CONFLICT",
+                    "鍚屼竴鍛戒护鎴栧箓绛夐敭鎼哄甫浜嗕笉鍚岀殑璇锋眰鍐呭锛岃妫�鏌ヤ换鍔$姸鎬佸悗閲嶈瘯");
+        }
+    }
+
+    private static String requestDigest(String moduleId, AgentImportCommitReqVO request) {
+        Map<String, Object> canonical = new LinkedHashMap<>();
+        canonical.put("module_id", moduleId);
+        canonical.put("attachment_id", request.getAttachmentId());
+        canonical.put("session_id", request.getSessionId());
+        canonical.put("source_sha256", StrUtil.nullToEmpty(request.getSourceSha256()).toLowerCase(Locale.ROOT));
+        canonical.put("preflight_hash", StrUtil.nullToEmpty(request.getPreflightHash()).toLowerCase(Locale.ROOT));
+        canonical.put("proposal_hash", StrUtil.nullToEmpty(request.getProposalHash()));
+        canonical.put("proposal_revision", StrUtil.nullToEmpty(request.getProposalRevision()));
+        canonical.put("options", request.getOptions() == null ? Map.of() : new TreeMap<>(request.getOptions()));
+        List<Map<String, Object>> rows = new ArrayList<>();
+        for (AgentImportPreflightReqVO.Row row : request.getRows()) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("candidate_id", row.getCandidateId());
+            item.put("row_no", row.getRowNo());
+            item.put("values", row.getValues() == null ? Map.of() : new TreeMap<>(row.getValues()));
+            rows.add(item);
+        }
+        canonical.put("rows", rows);
+        try {
+            byte[] payload = OBJECT_MAPPER.writeValueAsBytes(canonical);
+            return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(payload));
+        } catch (NoSuchAlgorithmException ex) {
+            throw new IllegalStateException("SHA-256 涓嶅彲鐢�", ex);
+        } catch (JsonProcessingException ex) {
+            throw new IllegalStateException("璇锋眰鎽樿鐢熸垚澶辫触", ex);
+        }
+    }
+
+    /**
+     * 閫傞厤鍣ㄨ绠楅妫�鎽樿鐨勭粺涓�鍏ュ彛锛岄伩鍏嶅悇妯″潡鎽樿鍙e緞婕傜Щ銆�
+     */
+    public static String preflightHash(String moduleId, AgentImportPreflightReqVO request,
+                                       Object resolvedPayload) {
+        Map<String, Object> canonical = new LinkedHashMap<>();
+        canonical.put("module_id", moduleId);
+        canonical.put("attachment_id", request.getAttachmentId());
+        canonical.put("source_sha256", StrUtil.nullToEmpty(request.getSourceSha256()).toLowerCase(Locale.ROOT));
+        canonical.put("proposal_revision", StrUtil.nullToEmpty(request.getProposalRevision()));
+        canonical.put("options", request.getOptions() == null ? Map.of() : new TreeMap<>(request.getOptions()));
+        List<Map<String, Object>> rows = new ArrayList<>();
+        for (AgentImportPreflightReqVO.Row row : request.getRows()) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("candidate_id", row.getCandidateId());
+            item.put("row_no", row.getRowNo());
+            item.put("values", row.getValues() == null ? Map.of() : new TreeMap<>(row.getValues()));
+            rows.add(item);
+        }
+        canonical.put("rows", rows);
+        canonical.put("resolved", resolvedPayload);
+        try {
+            byte[] payload = OBJECT_MAPPER.writeValueAsBytes(canonical);
+            return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(payload));
+        } catch (NoSuchAlgorithmException ex) {
+            throw new IllegalStateException("SHA-256 涓嶅彲鐢�", ex);
+        } catch (JsonProcessingException ex) {
+            throw new IllegalStateException("棰勬鎽樿鐢熸垚澶辫触", ex);
+        }
+    }
+
+    private AgentImportReceiptRespVO toReceipt(AgentImportCommandDO command) {
+        String status = command.getStatus();
+        if (STATUS_RUNNING.equals(status) && command.getLeaseExpiresAt() != null
+                && command.getLeaseExpiresAt().isBefore(LocalDateTime.now())) {
+            status = STATUS_UNKNOWN;
+        }
+        List<AgentImportCommandRowDO> rowDOs = commandRowMapper.selectListByCommandId(command.getCommandId());
+        List<AgentImportReceiptRespVO.Row> rows = rowDOs.stream()
+                .map(row -> AgentImportReceiptRespVO.Row.builder()
+                        .candidateId(row.getCandidateId())
+                        .rowNo(row.getSourceRow())
+                        .code(row.getCode())
+                        .name(row.getName())
+                        .action(row.getAction())
+                        .status(row.getStatus())
+                        .targetItemId(row.getTargetItemId())
+                        .targetVersion(row.getTargetVersion())
+                        .errorCode(row.getErrorCode())
+                        .errorMessage(row.getErrorMessage())
+                        .retryable(row.getRetryable())
+                        .build())
+                .toList();
+        int success = (int) rowDOs.stream().filter(row -> "SUCCESS".equals(row.getStatus())).count();
+        int failed = (int) rowDOs.stream().filter(row -> "FAILED".equals(row.getStatus())).count();
+        int skipped = (int) rowDOs.stream().filter(row -> "SKIPPED".equals(row.getStatus())).count();
+        int unknown = (int) rowDOs.stream().filter(row -> "UNKNOWN".equals(row.getStatus())).count();
+        return AgentImportReceiptRespVO.builder()
+                .commandId(command.getCommandId())
+                .idempotencyKey(command.getIdempotencyKey())
+                .status(status)
+                .attachmentId(command.getAttachmentId())
+                .sessionId(command.getSessionId())
+                .sourceSha256(command.getSourceSha256())
+                .preflightHash(command.getPreflightHash())
+                .proposalHash(command.getProposalHash())
+                .summary(AgentImportReceiptRespVO.Summary.builder()
+                        .total(command.getTotalCount())
+                        .success(success).failed(failed).skipped(skipped).unknown(unknown)
+                        .build())
+                .errorCode(command.getErrorCode())
+                .errorMessage(command.getErrorMessage())
+                .rows(rows)
+                .createTime(command.getCreateTime())
+                .completedTime(STATUS_RUNNING.equals(command.getStatus()) ? null : command.getUpdateTime())
+                .build();
+    }
+
+    private static String trim(String value, int maxLength) {
+        if (value == null) {
+            return null;
+        }
+        return value.length() <= maxLength ? value : value.substring(0, maxLength);
+    }
+}

--
Gitblit v1.9.3