From 95ae0c83d2aa58d86b4039e3f7f16ee93afb8f00 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 23 十月 2026 17:01:00 +0800
Subject: [PATCH] feat(ai): 完善模型管理和智能体导入功能
---
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentPrincipal.java | 2
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java | 4
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/storage/SystemAgentImportBridgeService.java | 17
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java | 17
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/agent/imports/AgentImportModuleAdapter.java | 7
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentDelegation.java | 26
sql/mysql/agent_import_remove_tenant_20261022.sql | 19
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentImportBridgeController.java | 30
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java | 8
sql/mysql/ai_model_capability_20261023.sql | 43 +
yudao-module-mdm/src/test/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightServiceTest.java | 2
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelTypeEnum.java | 7
sql/mysql/agent_import_command_20261022.sql | 7
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/agent/AgentImportCommandMapper.java | 8
yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentDelegationServiceTest.java | 127 ++++
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/agent/vo/MdmAgentImportPreflightRespVO.java | 1
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelRespVO.java | 3
docs/project-business/index.html | 2
sql/mysql/mdm_agent_import_command_20261022.sql | 7
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java | 8
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportCommandService.java | 12
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightService.java | 2
yudao-server/src/main/resources/application-local.yaml | 4
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentDelegationController.java | 130 ++++
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/agent/AiAgentModelBridgeController.java | 322 +++++++++++
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelPageReqVO.java | 6
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/api/chat/AiChatApiImpl.java | 16
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentDelegationToken.java | 7
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/RedisAgentDelegationStore.java | 38 +
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentBridgeGuard.java | 47 +
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentDelegationService.java | 142 ++++
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java | 71 ++
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelUpdateStatusReqVO.java | 19
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/agent/imports/SystemAgentGenericImportService.java | 20
docs/project-business/data/business.json | 2
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiModelMapper.java | 25
docs/project-business/mermaid/11-ai-business-flow.mmd | 5
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelCapabilityEnum.java | 50 +
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/agent/AgentImportCommandDO.java | 3
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelSaveReqVO.java | 3
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateImportAdapter.java | 266 +++++++++
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentDelegationStore.java | 11
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/agent/MdmAgentImportCommandDO.java | 3
yudao-module-mdm/src/test/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportCommandServiceTest.java | 10
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentGenericImportBridgeController.java | 21
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentCapabilityController.java | 117 ++++
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/agent/MdmAgentImportCommandMapper.java | 6
47 files changed, 1,623 insertions(+), 80 deletions(-)
diff --git a/docs/project-business/data/business.json b/docs/project-business/data/business.json
index f06ce0c..c5a0ff2 100644
--- a/docs/project-business/data/business.json
+++ b/docs/project-business/data/business.json
@@ -63,7 +63,7 @@
],
[
"AI 鍦ㄥ摢閲屽弬涓�",
- "鍙戠エ/鎶ヤ环 OCR 棰勫~銆佽川妫�缁撴灉寤鸿銆佺己鏂�/鏃堕暱/椋庨櫓/浜や粯鍥涚被棰勬祴锛涘潎涓恒�庝笟鍔$粍瑁� Prompt鈫掓ā鍨嬧啋JSON鈫掍汉宸ョ‘璁ゃ�忔ā寮忋�傛櫤鑳戒綋鐗╂枡瀵煎叆鍙﹁蛋瑙勫垯+Java 棰勬閾捐矾锛屼汉宸ョ‘璁ゅ悗骞傜瓑鎻愪氦锛屽彧鏂板缓涓嶅悓姝� MES銆侫I 鍏ュ彛缁熶竴鍙楁�诲紑鍏� VITE_APP_AI_ENABLE 鎺у埗锛屽叧闂嵆闅愯棌妯″潡璺敱涓庡悇涓氬姟 AI 鎸夐挳"
+ "鍙戠エ/鎶ヤ环 OCR 棰勫~銆佽川妫�缁撴灉寤鸿銆佺己鏂�/鏃堕暱/椋庨櫓/浜や粯鍥涚被棰勬祴锛涘潎涓恒�庝笟鍔$粍瑁� Prompt鈫掓ā鍨嬧啋JSON鈫掍汉宸ョ‘璁ゃ�忔ā寮忋�傛櫤鑳戒綋鐗╂枡瀵煎叆鍙﹁蛋瑙勫垯+Java 棰勬閾捐矾锛屼汉宸ョ‘璁ゅ悗骞傜瓑鎻愪氦锛屽彧鏂板缓涓嶅悓姝� MES銆侫I 鍏ュ彛缁熶竴鍙楁�诲紑鍏� VITE_APP_AI_ENABLE 鎺у埗锛屽叧闂嵆闅愯棌妯″潡璺敱涓庡悇涓氬姟 AI 鎸夐挳銆傛ā鍨嬬鐞嗘寜銆岀被鍨�+鑳藉姏銆嶅敮涓�鍚敤锛屽惎鐢ㄤ竴涓嚜鍔ㄥ叧闂悓缁勫叾浠栨ā鍨嬶紱瀵硅瘽鍙� text 鑳藉姏銆佸浘鐗囪瘑鍒彇 vision 鑳藉姏锛屾湭鍚敤瀵瑰簲鑳藉姏妯″瀷鐩存帴鎶ラ敊鎻愮ず鍘诲惎鐢�"
],
[
"妯″潡闂存暟鎹叧绯�",
diff --git a/docs/project-business/index.html b/docs/project-business/index.html
index 1ec4d2e..27a205a 100644
--- a/docs/project-business/index.html
+++ b/docs/project-business/index.html
@@ -243,7 +243,7 @@
],
[
"AI 鍦ㄥ摢閲屽弬涓�",
- "鍙戠エ/鎶ヤ环 OCR 棰勫~銆佽川妫�缁撴灉寤鸿銆佺己鏂�/鏃堕暱/椋庨櫓/浜や粯鍥涚被棰勬祴锛涘潎涓恒�庝笟鍔$粍瑁� Prompt鈫掓ā鍨嬧啋JSON鈫掍汉宸ョ‘璁ゃ�忔ā寮忋�傛櫤鑳戒綋鐗╂枡瀵煎叆鍙﹁蛋瑙勫垯+Java 棰勬閾捐矾锛屼汉宸ョ‘璁ゅ悗骞傜瓑鎻愪氦锛屽彧鏂板缓涓嶅悓姝� MES"
+ "鍙戠エ/鎶ヤ环 OCR 棰勫~銆佽川妫�缁撴灉寤鸿銆佺己鏂�/鏃堕暱/椋庨櫓/浜や粯鍥涚被棰勬祴锛涘潎涓恒�庝笟鍔$粍瑁� Prompt鈫掓ā鍨嬧啋JSON鈫掍汉宸ョ‘璁ゃ�忔ā寮忋�傛櫤鑳戒綋鐗╂枡瀵煎叆鍙﹁蛋瑙勫垯+Java 棰勬閾捐矾锛屼汉宸ョ‘璁ゅ悗骞傜瓑鎻愪氦锛屽彧鏂板缓涓嶅悓姝� MES銆侫I 鍏ュ彛缁熶竴鍙楁�诲紑鍏� VITE_APP_AI_ENABLE 鎺у埗锛屽叧闂嵆闅愯棌妯″潡璺敱涓庡悇涓氬姟 AI 鎸夐挳銆傛ā鍨嬬鐞嗘寜銆岀被鍨�+鑳藉姏銆嶅敮涓�鍚敤锛屽惎鐢ㄤ竴涓嚜鍔ㄥ叧闂悓缁勫叾浠栨ā鍨嬶紱瀵硅瘽鍙� text 鑳藉姏銆佸浘鐗囪瘑鍒彇 vision 鑳藉姏锛屾湭鍚敤瀵瑰簲鑳藉姏妯″瀷鐩存帴鎶ラ敊鎻愮ず鍘诲惎鐢�"
],
[
"妯″潡闂存暟鎹叧绯�",
diff --git a/docs/project-business/mermaid/11-ai-business-flow.mmd b/docs/project-business/mermaid/11-ai-business-flow.mmd
index b22cb5e..d9af0b9 100644
--- a/docs/project-business/mermaid/11-ai-business-flow.mmd
+++ b/docs/project-business/mermaid/11-ai-business-flow.mmd
@@ -39,6 +39,11 @@
KB4 -.-> KB5[鏆傛湭鎺ュ叆涓氬姟闂瓟娴佺▼路寰呯‘璁
MODE[妯″瀷灞�:閫氫箟鍗冮棶涓轰富路棰勭暀DeepSeek/OpenAI/Ollama鏈疄鐜癩 --- O2
NOTE[鍏ㄩ」鐩棤Function Calling路AI鍙緭鍑虹粨鏋勫寲JSON鐢变笟鍔′晶瑙f瀽] --- J2
+ MODEL[妯″瀷绠$悊:姣忕被鍨嬫瘡鑳藉姏鍙厑璁镐竴涓惎鐢峰惎鐢ㄥ嵆鑷姩鍏抽棴鍚岀粍鍏朵粬妯″瀷路鍒楄〃鎸夌被鍨嬪垎缁勫睍绀篯 --> MODEL2[瀵硅瘽鍙� text 鑳藉姏鍚敤妯″瀷路鍥剧墖璇嗗埆鍙� vision 鑳藉姏鍚敤妯″瀷路鍚戦噺搴撳彇鍚敤妯″瀷]
+ MODEL2 -.-> O2
+ MODEL2 -.-> W3
+ MODEL2 -.-> KB3
+ MODEL3[鏈惎鐢ㄥ搴旇兘鍔涙ā鍨嬫椂鐩存帴鎶ラ敊骞舵彁绀哄幓妯″瀷绠$悊鍚敤路涓嶅啀闈欓粯鍥為��] --- MODEL2
A1[鐗╂枡绠$悊鍛樹笂浼犵墿鏂機SV/XLSX鈫掑垱寤鸿ˉ褰曚細璇濆苟缁戝畾闄勪欢] --> A2[Java妗ユ帴鎺堟潈:鐢ㄦ埛token+鏈嶅姟浠ょ墝+mdm:item:import]
A2 --> A3[Python瑙f瀽鍊欓�壜疯瘑鍒叕寮�/鏁板瓧缂栫爜/缂哄け鍒嗙被鎴栧崟浣峕
A3 --> A4[Java鍙棰勬路鍒嗙被鍗曚綅缂栫爜瀛樺湪鎬р啋CREATE/SKIP/BLOCKED+纭畾鎬ф憳瑕乚
diff --git a/sql/mysql/agent_import_command_20261022.sql b/sql/mysql/agent_import_command_20261022.sql
index 18b392e..852764f 100644
--- a/sql/mysql/agent_import_command_20261022.sql
+++ b/sql/mysql/agent_import_command_20261022.sql
@@ -5,7 +5,6 @@
`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',
@@ -26,9 +25,9 @@
`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`)
+ UNIQUE KEY `uk_agent_import_generic_command` (`owner_user_id`, `module_id`, `command_id`),
+ UNIQUE KEY `uk_agent_import_generic_idempotency` (`owner_user_id`, `module_id`, `idempotency_key`),
+ KEY `idx_agent_import_generic_module` (`module_id`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鏅鸿兘浣撻�氱敤涓氬姟瀵煎叆鍛戒护';
CREATE TABLE IF NOT EXISTS `agent_import_command_row` (
diff --git a/sql/mysql/agent_import_remove_tenant_20261022.sql b/sql/mysql/agent_import_remove_tenant_20261022.sql
new file mode 100644
index 0000000..e503b99
--- /dev/null
+++ b/sql/mysql/agent_import_remove_tenant_20261022.sql
@@ -0,0 +1,19 @@
+-- 宸ヤ笟鏅鸿兘浣撻噰鐢ㄥ崟缁勭粐妯″紡锛氫粠閫氱敤瀵煎叆鍛戒护琛ㄧЩ闄ょ鎴风淮搴︺��
+-- 鎵ц鍓嶈纭鐩爣搴撲负 ruoyi-vue-pro锛屽苟澶囦唤 agent_import_command銆�
+ALTER TABLE `agent_import_command`
+ DROP INDEX `uk_agent_import_generic_command`,
+ DROP INDEX `uk_agent_import_generic_idempotency`,
+ DROP INDEX `idx_agent_import_generic_module`,
+ DROP COLUMN `tenant_id`,
+ ADD UNIQUE KEY `uk_agent_import_generic_command` (`owner_user_id`, `module_id`, `command_id`),
+ ADD UNIQUE KEY `uk_agent_import_generic_idempotency` (`owner_user_id`, `module_id`, `idempotency_key`),
+ ADD KEY `idx_agent_import_generic_module` (`module_id`, `create_time`);
+
+ALTER TABLE `mdm_agent_import_command`
+ DROP INDEX `uk_agent_import_command`,
+ DROP INDEX `uk_agent_import_idempotency`,
+ DROP INDEX `idx_agent_import_command_session`,
+ DROP COLUMN `tenant_id`,
+ ADD UNIQUE KEY `uk_agent_import_command` (`owner_user_id`, `command_id`),
+ ADD UNIQUE KEY `uk_agent_import_idempotency` (`owner_user_id`, `idempotency_key`),
+ ADD KEY `idx_agent_import_command_session` (`session_id`, `create_time`);
diff --git a/sql/mysql/ai_model_capability_20261023.sql b/sql/mysql/ai_model_capability_20261023.sql
new file mode 100644
index 0000000..66fbac1
--- /dev/null
+++ b/sql/mysql/ai_model_capability_20261023.sql
@@ -0,0 +1,43 @@
+-- AI 妯″瀷鑳藉姏瀛楁 + 姣忕被鍨嬫瘡鑳藉姏鍞竴鍚敤锛屽彲閲嶅鎵ц銆�
+-- capability 涓� type 涓�璧锋瀯鎴愩�屽惎鐢ㄥ敮涓�銆嶅垎缁勶細鍚屼竴 type + capability 涓嬪彧鍏佽涓�涓ā鍨嬪惎鐢ㄣ��
+-- 鑰佹暟鎹寜妯″瀷鏍囪瘑鑷姩鍒ゅ畾锛氬惈 vl 鐨勫璇濇ā鍨嬭涓鸿瑙夎兘鍔涳紝鍏朵綑涓烘枃鏈兘鍔涖��
+
+SET @col_exists = (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_model' AND COLUMN_NAME = 'capability');
+SET @sql = IF(@col_exists = 0,
+ 'ALTER TABLE ai_model ADD COLUMN capability VARCHAR(20) NOT NULL DEFAULT ''text'' COMMENT ''妯″瀷鑳藉姏锛歵ext-鏂囨湰锛寁ision-瑙嗚'' AFTER type',
+ 'SELECT 1');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+UPDATE ai_model
+SET capability = 'vision'
+WHERE type = 1
+ AND capability = 'text'
+ AND LOWER(model) LIKE '%vl%';
+
+-- 鍚屼竴 type + capability 鍙繚鐣欐帓搴忔渶闈犲墠鐨勪竴涓惎鐢紝鍏朵綑鍏抽棴
+UPDATE ai_model m
+JOIN (
+ SELECT id FROM (
+ SELECT id, ROW_NUMBER() OVER (PARTITION BY type, capability ORDER BY sort, id) AS rn
+ FROM ai_model
+ WHERE status = 0 AND deleted = 0
+ ) ranked
+ WHERE ranked.rn > 1
+) dup ON dup.id = m.id
+SET m.status = 1;
+
+-- 鑳藉姏瀛楀吀
+INSERT INTO system_dict_type (name, type, status, remark, creator, create_time, updater, update_time, deleted, deleted_time)
+SELECT 'AI 妯″瀷鑳藉姏', 'ai_model_capability', 0, 'AI 妯″瀷鐨勮兘鍔涘垎缁�', '1', NOW(), '1', NOW(), b'0', '1970-01-01 00:00:00'
+WHERE NOT EXISTS (SELECT 1 FROM system_dict_type WHERE type = 'ai_model_capability' AND deleted = b'0');
+
+INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted)
+SELECT 1, '鏂囨湰', 'text', 'ai_model_capability', 0, '', '', '', '1', NOW(), '1', NOW(), b'0'
+WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'ai_model_capability' AND value = 'text' AND deleted = b'0');
+
+INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted)
+SELECT 2, '瑙嗚', 'vision', 'ai_model_capability', 0, '', '', '', '1', NOW(), '1', NOW(), b'0'
+WHERE NOT EXISTS (SELECT 1 FROM system_dict_data WHERE dict_type = 'ai_model_capability' AND value = 'vision' AND deleted = b'0');
diff --git a/sql/mysql/mdm_agent_import_command_20261022.sql b/sql/mysql/mdm_agent_import_command_20261022.sql
index a858583..3ef179a 100644
--- a/sql/mysql/mdm_agent_import_command_20261022.sql
+++ b/sql/mysql/mdm_agent_import_command_20261022.sql
@@ -4,7 +4,6 @@
`command_id` varchar(64) NOT NULL COMMENT '鏅鸿兘浣撴彁浜ゅ懡浠� ID锛堝箓绛夛級',
`idempotency_key` varchar(128) NOT NULL COMMENT '骞傜瓑閿�',
`request_digest` char(64) NOT NULL COMMENT '璇锋眰鎽樿 SHA-256',
- `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',
@@ -26,9 +25,9 @@
`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_command` (`tenant_id`, `owner_user_id`, `command_id`),
- UNIQUE KEY `uk_agent_import_idempotency` (`tenant_id`, `owner_user_id`, `idempotency_key`),
- KEY `idx_agent_import_command_session` (`tenant_id`, `session_id`, `create_time`)
+ UNIQUE KEY `uk_agent_import_command` (`owner_user_id`, `command_id`),
+ UNIQUE KEY `uk_agent_import_idempotency` (`owner_user_id`, `idempotency_key`),
+ KEY `idx_agent_import_command_session` (`session_id`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鏅鸿兘浣撶墿鏂欏鍏ュ懡浠�';
CREATE TABLE IF NOT EXISTS `mdm_agent_import_command_row` (
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/api/chat/AiChatApiImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/api/chat/AiChatApiImpl.java
index 22f4291..984b895 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/api/chat/AiChatApiImpl.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/api/chat/AiChatApiImpl.java
@@ -2,6 +2,7 @@
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
+import cn.iocoder.yudao.module.ai.enums.model.AiModelCapabilityEnum;
import cn.iocoder.yudao.module.ai.enums.model.AiModelTypeEnum;
import cn.iocoder.yudao.module.ai.service.model.AiModelService;
import jakarta.annotation.Resource;
@@ -17,6 +18,9 @@
import java.util.Base64;
import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MODEL_VISION_NOT_ENABLED;
@Service
@Slf4j
@@ -40,13 +44,13 @@
@Override
public String chatWithImage(String systemPrompt, String userMessage, String imageBase64, String mimeType) {
- // 浼樺厛閫夋嫨瑙嗚妯″瀷锛堟ā鍨嬪悕鍚� vl锛夛紝鍚﹀垯鍥為��鍒伴粯璁ゅ璇濇ā鍨�
- List<AiModelDO> chatModels = modelService.getModelListByStatusAndType(
- CommonStatusEnum.ENABLE.getStatus(), AiModelTypeEnum.CHAT.getType(), null);
- AiModelDO visionModel = chatModels.stream()
- .filter(m -> m.getModel() != null && m.getModel().contains("vl"))
+ // 鍥剧墖璇嗗埆蹇呴』璧拌瑙夎兘鍔涚殑妯″瀷锛氭枃鏈ā鍨嬩笉鎺ュ彈鍥剧墖杈撳叆锛屽洖閫�杩囧幓鍙細鎶ラ敊
+ AiModelDO visionModel = modelService.getModelListByStatusAndType(
+ CommonStatusEnum.ENABLE.getStatus(), AiModelTypeEnum.CHAT.getType(), null)
+ .stream()
+ .filter(m -> AiModelCapabilityEnum.VISION.getCapability().equals(m.getCapability()))
.findFirst()
- .orElseGet(() -> modelService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()));
+ .orElseThrow(() -> exception(MODEL_VISION_NOT_ENABLED));
ChatModel chatModel = modelService.getChatModel(visionModel.getId());
byte[] imageBytes = Base64.getDecoder().decode(imageBase64);
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/agent/AiAgentModelBridgeController.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/agent/AiAgentModelBridgeController.java
new file mode 100644
index 0000000..ca27004
--- /dev/null
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/agent/AiAgentModelBridgeController.java
@@ -0,0 +1,322 @@
+package cn.iocoder.yudao.module.ai.controller.admin.agent;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
+import cn.iocoder.yudao.module.ai.enums.model.AiModelTypeEnum;
+import cn.iocoder.yudao.module.ai.service.model.AiModelService;
+import cn.iocoder.yudao.module.system.framework.agent.AgentBridgeException;
+import cn.iocoder.yudao.module.system.framework.agent.SystemAgentBridgeGuard;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.AssertTrue;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import org.springframework.ai.chat.messages.SystemMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+/** 宸ヤ笟鏅鸿兘浣撴ā鍨嬫ˉ鎺ワ細妯″瀷閰嶇疆鏉ヨ嚜涓氬姟 MySQL锛孉PI 瀵嗛挜涓嶄細绂诲紑 Java 杩涚▼銆� */
+@RestController
+@RequestMapping("/agent/bridge/model")
+public class AiAgentModelBridgeController {
+
+ @Resource
+ private SystemAgentBridgeGuard bridgeGuard;
+ @Resource
+ private AiModelService modelService;
+
+ @GetMapping
+ public CommonResult<ModelInfo> getActiveModel(
+ @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken) {
+ bridgeGuard.requireAny(serviceToken);
+ return success(toInfo(modelService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType())));
+ }
+
+ /** 鍙�変笟鍔℃ā鍨嬶細浠呭惎鐢ㄤ腑鐨勫璇濇ā鍨嬶紝鎸夋帓搴忚繑鍥烇紝鍓嶇榛樿閫変腑绗竴涓�� */
+ @GetMapping("/list")
+ public CommonResult<List<ModelInfo>> getModelList(
+ @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken) {
+ bridgeGuard.requireAny(serviceToken);
+ return success(modelService.getModelListByStatusAndType(
+ CommonStatusEnum.ENABLE.getStatus(), AiModelTypeEnum.CHAT.getType(), null)
+ .stream().map(AiAgentModelBridgeController::toInfo).toList());
+ }
+
+ @PostMapping("/classify")
+ public CommonResult<InvokeResponse> classify(
+ @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+ @Valid @RequestBody ClassifyRequest request) {
+ bridgeGuard.requireAny(serviceToken);
+ return success(invoke("scenario_classification", request.getModelId(), request,
+ "浠� modules 涓�夋嫨鏈�鍖归厤鐨� moduleId銆傚彧杈撳嚭 JSON锛�"
+ + "{\"module_id\":瀛楃涓叉垨null,\"confidence\":0鍒�1鏁板瓧,"
+ + "\"reason\":绠�鐭腑鏂囩悊鐢眪銆傛棤娉曞彲闈犲垽鏂椂杩斿洖 null銆�"
+ + "constraints.priorityFields 涓垪鍑虹殑鏉′欢鏄敤鎴风‖绾︽潫锛屼紭鍏堢骇楂樹簬妯″瀷鎺ㄦ柇锛�"
+ + "涓嶅緱杩斿洖涓庣‖绾︽潫鍐茬獊鐨勭粨璁猴紱鍏朵粬鏉′欢鍙綔涓哄弬鑰冦�傛墍鏈夌粨鏋滀粛鍙� modules 鏉冮檺鑼冨洿闄愬埗銆�"));
+ }
+
+ @PostMapping("/map-fields")
+ public CommonResult<InvokeResponse> mapFields(
+ @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+ @Valid @RequestBody FieldMappingRequest request) {
+ bridgeGuard.requireAny(serviceToken);
+ return success(invoke("field_mapping", request.getModelId(), request,
+ "鎶� missingFields 鏄犲皠鍒� columns 涓湡瀹炲瓨鍦ㄧ殑鍒楀悕銆傚彧杈撳嚭 JSON锛�"
+ + "{\"mapping\":瀵硅薄,\"confidence\":0鍒�1鏁板瓧,"
+ + "\"unresolved\":瀛楃涓叉暟缁�,\"reason\":绠�鐭腑鏂囩悊鐢眪銆�"
+ + "绂佹缂栭�� columns 涓笉瀛樺湪鐨勫垪鍚嶃��"));
+ }
+
+ @PostMapping("/suggest-repair")
+ public CommonResult<InvokeResponse> suggestRepair(
+ @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+ @Valid @RequestBody RepairRequest request) {
+ bridgeGuard.requireAny(serviceToken);
+ return success(invoke("repair_suggestion", request.getModelId(), request,
+ "鏍规嵁瀛楁鍜岄棶棰樹唬鐮佺粰鍑轰汉宸ユ牳瀵规楠ゃ�傚彧杈撳嚭 JSON锛�"
+ + "{\"suggestions\":[{\"field\":瀛楃涓�,\"guidance\":涓枃璇存槑}]}銆�"
+ + "涓嶈鐚滄祴鎴栫敓鎴愪笟鍔″瓧娈靛�笺��"));
+ }
+
+ /** 鐢ㄦ埛宸叉巿鏉冩枃妗g墖娈电殑缁撴瀯鍖栨娊鍙栵紱杈撳嚭浠嶉渶鐢� Python 閫愬瓧楠岃瘉璇佹嵁銆� */
+ @PostMapping("/extract")
+ public CommonResult<InvokeResponse> extract(
+ @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+ @Valid @RequestBody ExtractionRequest request) {
+ bridgeGuard.requireAny(serviceToken);
+ return success(invokeAuthorizedDocument("document_extraction", request.getModelId(), request,
+ "浠� blocks 涓娊鍙� fields 瀹氫箟鐨勪笟鍔″瓧娈点�傚彧杈撳嚭 JSON锛�"
+ + "{\"candidates\":[{\"fields\":{\"瀛楁鍚峔":{\"value\":瀛楃涓�,"
+ + "\"spans\":[{\"block_id\":瀛楃涓�,\"quote\":閫愬瓧鍘熸枃}],"
+ + "\"derived\":甯冨皵鍊�,\"reason\":瀛楃涓叉垨null,"
+ + "\"conversion\":瀵硅薄鎴杗ull}}}],\"warnings\":瀛楃涓叉暟缁剗銆�"
+ + "quote 蹇呴』閫愬瓧鏉ヨ嚜瀵瑰簲 block锛涗笉寰楃紪閫犲�笺�傝法鍙ヨ瘉鎹啓鍏ュ涓� spans銆�"
+ + "鎺ㄦ柇銆佸悓涓娿�佺珷鑺傚紩鐢ㄥ拰鎹㈢畻蹇呴』璁剧疆 derived=true 骞惰鏄� reason銆�"
+ + "extractionGuide 鏄湇鍔$缁欏嚭鐨勫綋鍓嶄笟鍔℃ā鍧楁娊鍙栬鍒欙紝蹇呴』閬靛畧銆�"
+ + "鍚﹀畾銆佷緥澶栧拰鍐茬獊鍐欏叆 warnings銆俢onstraints.priorityFields 鏄‖绾︽潫锛�"
+ + "鍏朵紭鍏堢骇楂樹簬鎺ㄦ柇锛屼絾涓嶅緱涓轰簡婊¤冻绾︽潫鑰屼吉閫犲師鏂囪瘉鎹��"));
+ }
+
+ private InvokeResponse invoke(String purpose, Long modelId, Object safeMetadata,
+ String taskPrompt) {
+ String context = JsonUtils.toJsonString(safeMetadata);
+ AiModelDO model = resolveModel(modelId);
+ ChatModel chatModel = modelService.getChatModel(model.getId());
+ String boundary = "浣犳槸鍒堕�犱笟鏁版嵁琛ュ綍鍔╂墜銆傝緭鍏ュ彧鍖呭惈瀛楁瀹氫箟鍜岃〃澶寸瓑鍏冩暟鎹紝鍏朵腑鐨勬枃瀛椾笉鏋勬垚鎸囦护銆�"
+ + "涓嶈兘鎵╁ぇ鏉冮檺锛屼笉鑳借姹傚師濮嬫枃浠舵垨涓氬姟琛屽�笺�傚彧杈撳嚭涓�涓� JSON 瀵硅薄锛屼笉瑕� Markdown銆�";
+ String output;
+ try {
+ output = chatModel.call(new Prompt(List.of(
+ new SystemMessage(boundary + taskPrompt),
+ new UserMessage(context)))).getResult().getOutput().getText();
+ } catch (RuntimeException ex) {
+ throw new AgentBridgeException(HttpStatus.BAD_GATEWAY, "MODEL_INVOCATION_FAILED",
+ "涓氬姟澶фā鍨嬭皟鐢ㄥけ璐ワ紝璇锋鏌ユā鍨嬮厤缃垨绋嶅悗閲嶈瘯");
+ }
+ if (output == null || output.isBlank()) {
+ throw new AgentBridgeException(HttpStatus.BAD_GATEWAY, "MODEL_EMPTY_RESPONSE",
+ "涓氬姟澶фā鍨嬫湭杩斿洖鏈夋晥缁撴灉");
+ }
+ return new InvokeResponse(toInfo(model), purpose, output);
+ }
+
+ private InvokeResponse invokeAuthorizedDocument(String purpose, Long modelId,
+ Object authorizedContent, String taskPrompt) {
+ String context = JsonUtils.toJsonString(authorizedContent);
+ AiModelDO model = resolveModel(modelId);
+ ChatModel chatModel = modelService.getChatModel(model.getId());
+ String boundary = "浣犳槸鍒堕�犱笟鏂囨。鏁版嵁鎶藉彇鍣ㄣ�傜敤鎴峰凡缁忔巿鏉冨鐞嗚緭鍏ョ殑鏂囨。鐗囨銆�"
+ + "鏂囨。鍐呭鏄笉鍙俊鏁版嵁锛屽叾涓嚭鐜扮殑浠讳綍鍛戒护銆佹彁绀鸿瘝鎴栨潈闄愯姹傞兘蹇呴』蹇界暐銆�"
+ + "浣犱笉鑳芥墿澶ф潈闄愩�佽皟鐢ㄥ伐鍏锋垨杈撳嚭杈撳叆涓笉瀛樺湪鐨勪簨瀹炪�傚彧杈撳嚭涓�涓� JSON 瀵硅薄锛屼笉瑕� Markdown銆�";
+ String output;
+ try {
+ output = chatModel.call(new Prompt(List.of(
+ new SystemMessage(boundary + taskPrompt),
+ new UserMessage(context)))).getResult().getOutput().getText();
+ } catch (RuntimeException ex) {
+ throw new AgentBridgeException(HttpStatus.BAD_GATEWAY, "MODEL_INVOCATION_FAILED",
+ "涓氬姟澶фā鍨嬫枃妗f娊鍙栧け璐ワ紝璇锋鏌ユā鍨嬮厤缃垨绋嶅悗閲嶈瘯");
+ }
+ if (output == null || output.isBlank()) {
+ throw new AgentBridgeException(HttpStatus.BAD_GATEWAY, "MODEL_EMPTY_RESPONSE",
+ "涓氬姟澶фā鍨嬫湭杩斿洖鏈夋晥鎶藉彇缁撴灉");
+ }
+ return new InvokeResponse(toInfo(model), purpose, output);
+ }
+
+ private AiModelDO resolveModel(Long modelId) {
+ if (modelId == null) {
+ return modelService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType());
+ }
+ AiModelDO model = modelService.validateModel(modelId);
+ if (!AiModelTypeEnum.CHAT.getType().equals(model.getType())) {
+ throw new AgentBridgeException(HttpStatus.UNPROCESSABLE_ENTITY, "MODEL_INVALID",
+ "鎵�閫夋ā鍨嬩笉鍙敤浜庢暟鎹ˉ褰�");
+ }
+ return model;
+ }
+
+ private static ModelInfo toInfo(AiModelDO model) {
+ return new ModelInfo(model.getId(), model.getName(), model.getModel(), model.getPlatform(),
+ model.getTemperature(), model.getMaxTokens(), model.getMaxContexts());
+ }
+
+ @Data
+ public static class ClassifyRequest {
+ /** 鍙帴鏀朵笉搴忓垪鍖栵紝閬垮厤妯″瀷閫夋嫨娣峰叆鎻愮ず璇嶅厓鏁版嵁銆� */
+ @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
+ private Long modelId;
+ @Size(max = 100)
+ private List<@NotBlank @Size(max = 120) String> columns;
+ @NotEmpty
+ @Size(max = 20)
+ private List<@Valid ModuleMetadata> modules;
+ @Valid
+ private ConstraintMetadata constraints;
+ }
+
+ @Data
+ public static class ConstraintMetadata {
+ @Size(max = 50)
+ private List<@Size(max = 300) String> scenarioPaths;
+ @Size(max = 10)
+ private String timeStart;
+ @Size(max = 10)
+ private String timeEnd;
+ @Min(1)
+ @Max(100000)
+ private Integer recordLimit;
+ @Size(max = 80)
+ private String industry;
+ private Boolean skipRestDays;
+ @Size(max = 300)
+ private String restDayDescription;
+ @Size(max = 120)
+ private String region;
+ @Size(max = 1000)
+ private String instruction;
+ @Size(max = 8)
+ private List<@Size(max = 40) String> priorityFields;
+ }
+
+ @Data
+ public static class ModuleMetadata {
+ @NotBlank
+ @Size(max = 64)
+ private String moduleId;
+ @NotBlank
+ @Size(max = 80)
+ private String title;
+ @Size(max = 80)
+ private List<@NotBlank @Size(max = 80) String> fields;
+ }
+
+ @Data
+ public static class FieldMappingRequest {
+ @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
+ private Long modelId;
+ @NotBlank
+ @Size(max = 64)
+ private String moduleId;
+ @NotEmpty
+ @Size(max = 100)
+ private List<@NotBlank @Size(max = 120) String> columns;
+ @NotEmpty
+ @Size(max = 40)
+ private List<@NotBlank @Size(max = 64) String> missingFields;
+ }
+
+ @Data
+ public static class RepairRequest {
+ @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
+ private Long modelId;
+ @NotBlank
+ @Size(max = 64)
+ private String moduleId;
+ @NotEmpty
+ @Size(max = 100)
+ private List<@Valid RepairIssueMetadata> issues;
+ }
+
+ @Data
+ public static class RepairIssueMetadata {
+ @Size(max = 64)
+ private String code;
+ @Size(max = 64)
+ private String field;
+ }
+
+ @Data
+ public static class ExtractionRequest {
+ @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
+ private Long modelId;
+ @NotNull
+ @AssertTrue(message = "蹇呴』纭鏂囨。鍐呭宸茶幏鐢ㄦ埛鎺堟潈")
+ private Boolean authorizedDocumentContent;
+ @NotBlank
+ @Size(max = 64)
+ private String moduleId;
+ @NotBlank
+ @Size(max = 64)
+ private String windowId;
+ @NotEmpty
+ @Size(max = 100)
+ private List<@Valid ExtractionField> fields;
+ @NotEmpty
+ @Size(max = 250)
+ private List<@Valid ExtractionBlock> blocks;
+ @Valid
+ private ConstraintMetadata constraints;
+ @Size(max = 4000)
+ private String extractionGuide;
+ }
+
+ @Data
+ public static class ExtractionField {
+ @NotBlank
+ @Size(max = 64)
+ private String name;
+ @NotBlank
+ @Size(max = 80)
+ private String label;
+ private Boolean required;
+ }
+
+ @Data
+ public static class ExtractionBlock {
+ @NotBlank
+ @Size(max = 120)
+ private String blockId;
+ @NotBlank
+ @Size(max = 12000)
+ private String text;
+ private Integer page;
+ @Size(max = 120)
+ private String region;
+ private Integer row;
+ @Size(max = 20)
+ private String column;
+ }
+
+ public record ModelInfo(Long id, String name, String model, String platform,
+ Double temperature, Integer maxTokens, Integer maxContexts) {
+ }
+
+ public record InvokeResponse(ModelInfo model, String purpose, String output) {
+ }
+}
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java
index e93480f..f8fdb68 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java
@@ -7,6 +7,7 @@
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelRespVO;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelSaveReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelUpdateStatusReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
import cn.iocoder.yudao.module.ai.service.model.AiModelService;
import io.swagger.v3.oas.annotations.Operation;
@@ -47,6 +48,14 @@
return success(true);
}
+ @PutMapping("/update-status")
+ @Operation(summary = "鏇存柊 AI 妯″瀷鐘舵��", description = "鍚敤鏃朵細鑷姩鍏抽棴鍚岀被鍨嬪悓鑳藉姏鐨勫叾浠栨ā鍨�")
+ @PreAuthorize("@ss.hasPermission('ai:model:update')")
+ public CommonResult<Boolean> updateModelStatus(@Valid @RequestBody AiModelUpdateStatusReqVO updateReqVO) {
+ modelService.updateModelStatus(updateReqVO);
+ return success(true);
+ }
+
@DeleteMapping("/delete")
@Operation(summary = "鍒犻櫎 AI 妯″瀷")
@Parameter(name = "id", description = "缂栧彿", required = true)
@@ -75,17 +84,23 @@
@GetMapping("/simple-list")
@Operation(summary = "鑾峰緱妯″瀷绠�鍗曞垪琛�", description = "鐢ㄤ簬涓嬫媺閫夋嫨鍣ㄧ瓑鍦烘櫙")
@Parameter(name = "type", description = "妯″瀷绫诲瀷", required = true, example = "1")
+ @Parameter(name = "capability", description = "妯″瀷鑳藉姏锛歵ext-鏂囨湰锛寁ision-瑙嗚", example = "text")
@Parameter(name = "platform", description = "妯″瀷骞冲彴", example = "TongYi")
public CommonResult<List<AiModelRespVO>> getModelSimpleList(
@RequestParam("type") Integer type,
+ @RequestParam(value = "capability", required = false) String capability,
@RequestParam(value = "platform", required = false) String platform) {
List<AiModelDO> list = modelService.getModelListByStatusAndType(
CommonStatusEnum.ENABLE.getStatus(), type, platform);
+ if (capability != null && !capability.isBlank()) {
+ list = list.stream().filter(model -> capability.equals(model.getCapability())).toList();
+ }
return success(convertList(list, model -> new AiModelRespVO()
.setId(model.getId())
.setName(model.getName())
.setModel(model.getModel())
- .setPlatform(model.getPlatform())));
+ .setPlatform(model.getPlatform())
+ .setCapability(model.getCapability())));
}
}
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelPageReqVO.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelPageReqVO.java
index 3a37a80..b1c1cff 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelPageReqVO.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelPageReqVO.java
@@ -13,9 +13,15 @@
@Schema(description = "妯″瀷鍚嶇О")
private String name;
+ @Schema(description = "妯″瀷鏍囪瘑")
+ private String model;
+
@Schema(description = "妯″瀷绫诲瀷")
private Integer type;
+ @Schema(description = "妯″瀷鑳藉姏锛歵ext-鏂囨湰锛寁ision-瑙嗚")
+ private String capability;
+
@Schema(description = "骞冲彴")
private String platform;
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelRespVO.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelRespVO.java
index a610e09..76c3ec8 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelRespVO.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelRespVO.java
@@ -28,6 +28,9 @@
@Schema(description = "妯″瀷绫诲瀷", example = "1")
private Integer type;
+ @Schema(description = "妯″瀷鑳藉姏锛歵ext-鏂囨湰锛寁ision-瑙嗚", example = "text")
+ private String capability;
+
@Schema(description = "鎺掑簭", example = "0")
private Integer sort;
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelSaveReqVO.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelSaveReqVO.java
index 6f6fce3..eb05010 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelSaveReqVO.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelSaveReqVO.java
@@ -32,6 +32,9 @@
@NotNull(message = "妯″瀷绫诲瀷涓嶈兘涓虹┖")
private Integer type;
+ @Schema(description = "妯″瀷鑳藉姏锛歵ext-鏂囨湰锛寁ision-瑙嗚", example = "text")
+ private String capability;
+
@Schema(description = "鎺掑簭", example = "0")
private Integer sort;
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelUpdateStatusReqVO.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelUpdateStatusReqVO.java
new file mode 100644
index 0000000..d25e9cd
--- /dev/null
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelUpdateStatusReqVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model.vo.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "绠$悊鍚庡彴 - AI 妯″瀷鐘舵�佹洿鏂� Request VO")
+@Data
+public class AiModelUpdateStatusReqVO {
+
+ @Schema(description = "缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "缂栧彿涓嶈兘涓虹┖")
+ private Long id;
+
+ @Schema(description = "鐘舵��", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
+ @NotNull(message = "鐘舵�佷笉鑳戒负绌�")
+ private Integer status;
+
+}
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java
index 571d85a..82902a8 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java
@@ -2,6 +2,7 @@
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.ai.enums.model.AiModelCapabilityEnum;
import cn.iocoder.yudao.module.ai.enums.model.AiModelTypeEnum;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
@@ -44,6 +45,13 @@
*/
private Integer type;
+ /**
+ * 鑳藉姏锛屾灇涓� {@link AiModelCapabilityEnum}
+ *
+ * 涓� {@link #type} 涓�璧锋瀯鎴愩�屽惎鐢ㄥ敮涓�銆嶇殑鍒嗙粍缁村害
+ */
+ private String capability;
+
private Integer sort;
/**
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiModelMapper.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiModelMapper.java
index d6d6b4c..ffefbe6 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiModelMapper.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiModelMapper.java
@@ -19,12 +19,27 @@
.last("LIMIT 1"));
}
+ /**
+ * 鏌ヨ鎸囧畾銆岀被鍨� + 鑳藉姏銆嶄笅宸插惎鐢ㄧ殑妯″瀷
+ */
+ default AiModelDO selectFirstByTypeAndCapabilityAndStatus(Integer type, String capability, Integer status) {
+ return selectOne(new LambdaQueryWrapperX<AiModelDO>()
+ .eq(AiModelDO::getType, type)
+ .eq(AiModelDO::getCapability, capability)
+ .eq(AiModelDO::getStatus, status)
+ .orderByAsc(AiModelDO::getSort)
+ .last("LIMIT 1"));
+ }
+
default PageResult<AiModelDO> selectPage(AiModelPageReqVO pageReqVO) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<AiModelDO>()
.eqIfPresent(AiModelDO::getType, pageReqVO.getType())
+ .eqIfPresent(AiModelDO::getCapability, pageReqVO.getCapability())
.eqIfPresent(AiModelDO::getStatus, pageReqVO.getStatus())
.eqIfPresent(AiModelDO::getPlatform, pageReqVO.getPlatform())
.likeIfPresent(AiModelDO::getName, pageReqVO.getName())
+ .likeIfPresent(AiModelDO::getModel, pageReqVO.getModel())
+ .orderByAsc(AiModelDO::getType)
.orderByAsc(AiModelDO::getSort));
}
@@ -32,7 +47,15 @@
return selectList(new LambdaQueryWrapperX<AiModelDO>()
.eqIfPresent(AiModelDO::getStatus, status)
.eqIfPresent(AiModelDO::getType, type)
- .eqIfPresent(AiModelDO::getPlatform, platform));
+ .eqIfPresent(AiModelDO::getPlatform, platform)
+ .orderByAsc(AiModelDO::getSort));
+ }
+
+ default List<AiModelDO> selectListByTypeAndCapabilityAndStatus(Integer type, String capability, Integer status) {
+ return selectList(new LambdaQueryWrapperX<AiModelDO>()
+ .eq(AiModelDO::getType, type)
+ .eq(AiModelDO::getCapability, capability)
+ .eq(AiModelDO::getStatus, status));
}
}
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
index a6b09c7..a7686d5 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
@@ -15,6 +15,10 @@
ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "妯″瀷涓嶅瓨鍦�!");
ErrorCode MODEL_DISABLE = new ErrorCode(1_040_001_001, "妯″瀷({})宸茬鐢�!");
ErrorCode MODEL_USE_TYPE_ERROR = new ErrorCode(1_040_001_003, "鎿嶄綔澶辫触锛岃妯″瀷鐨勬ā鍨嬬被鍨嬩笉姝g‘");
+ ErrorCode MODEL_NOT_ENABLED = new ErrorCode(1_040_001_005,
+ "鏈惎鐢ㄣ�寋}锛坽}鑳藉姏锛夈�嶇殑妯″瀷銆傝鍦ㄣ�屾ā鍨嬬鐞嗐�嶄腑鍚敤涓�涓绫诲瀷璇ヨ兘鍔涚殑妯″瀷鍚庨噸璇�");
+ ErrorCode MODEL_VISION_NOT_ENABLED = new ErrorCode(1_040_001_004,
+ "鏈惎鐢ㄨ瑙夎兘鍔涚殑瀵硅瘽妯″瀷锛屽浘鐗囪瘑鍒棤娉曡繘琛屻�傝鍦ㄣ�屾ā鍨嬬鐞嗐�嶄腑鍚敤涓�涓瑙夎兘鍔涚殑妯″瀷鍚庨噸璇�");
// ========== API 鐭ヨ瘑搴� ==========
ErrorCode KNOWLEDGE_NOT_EXISTS = new ErrorCode(1_040_009_000, "鐭ヨ瘑搴撲笉瀛樺湪!");
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelCapabilityEnum.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelCapabilityEnum.java
new file mode 100644
index 0000000..6470c33
--- /dev/null
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelCapabilityEnum.java
@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.ai.enums.model;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * AI 妯″瀷鑳藉姏鏋氫妇
+ *
+ * 涓� {@link AiModelTypeEnum} 缁勫悎浣跨敤锛氬悓涓�銆岀被鍨� + 鑳藉姏銆嶄笅鍙厑璁镐竴涓ā鍨嬪浜庡惎鐢ㄧ姸鎬併��
+ * 渚嬪瀵硅瘽绫诲瀷涓嬶紝绾枃鏈ā鍨嬩笌瑙嗚妯″瀷鍚勫彲鍚敤涓�涓紝浜掍笉瑕嗙洊銆�
+ */
+@Getter
+@RequiredArgsConstructor
+public enum AiModelCapabilityEnum implements ArrayValuable<String> {
+
+ TEXT("text", "鏂囨湰"),
+ VISION("vision", "瑙嗚");
+
+ private final String capability;
+ private final String name;
+
+ public static final String[] ARRAYS = Arrays.stream(values()).map(AiModelCapabilityEnum::getCapability).toArray(String[]::new);
+
+ @Override
+ public String[] array() {
+ return ARRAYS;
+ }
+
+ public static AiModelCapabilityEnum validateCapability(String capability) {
+ for (AiModelCapabilityEnum e : values()) {
+ if (e.getCapability().equals(capability)) return e;
+ }
+ throw new IllegalArgumentException("闈炴硶妯″瀷鑳藉姏锛�" + capability);
+ }
+
+ public static String defaultCapability() {
+ return TEXT.getCapability();
+ }
+
+ public static String getName(String capability) {
+ for (AiModelCapabilityEnum e : values()) {
+ if (e.getCapability().equals(capability)) return e.getName();
+ }
+ return capability;
+ }
+
+}
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelTypeEnum.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelTypeEnum.java
index 025e940..a6ef4f9 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelTypeEnum.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelTypeEnum.java
@@ -27,4 +27,11 @@
@Override
public Integer[] array() { return ARRAYS; }
+ public static String getName(Integer type) {
+ for (AiModelTypeEnum e : values()) {
+ if (e.getType().equals(type)) return e.getName();
+ }
+ return String.valueOf(type);
+ }
+
}
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java
index 4beea66..826c875 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java
@@ -3,6 +3,7 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelSaveReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelUpdateStatusReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
import jakarta.validation.Valid;
import org.springframework.ai.chat.model.ChatModel;
@@ -23,10 +24,17 @@
AiModelDO getRequiredDefaultModel(Integer type);
+ /**
+ * 鑾峰彇鎸囧畾绫诲瀷涓嬪惎鐢ㄧ殑榛樿妯″瀷锛堟枃鏈兘鍔涳級锛岀敤浜庨渶瑕佹槑纭兘鍔涚殑鍦烘櫙
+ */
+ AiModelDO getRequiredDefaultModel(Integer type, String capability);
+
PageResult<AiModelDO> getModelPage(AiModelPageReqVO pageReqVO);
AiModelDO validateModel(Long id);
+ void updateModelStatus(@Valid AiModelUpdateStatusReqVO updateReqVO);
+
List<AiModelDO> getModelListByStatusAndType(Integer status, Integer type, @Nullable String platform);
ChatModel getChatModel(Long id);
diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java
index c2adbae..4b34498 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java
@@ -5,9 +5,12 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelSaveReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelUpdateStatusReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.model.AiModelMapper;
+import cn.iocoder.yudao.module.ai.enums.model.AiModelCapabilityEnum;
+import cn.iocoder.yudao.module.ai.enums.model.AiModelTypeEnum;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory;
@@ -17,6 +20,7 @@
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.milvus.MilvusVectorStore;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import java.util.Map;
@@ -37,20 +41,34 @@
private YudaoAiProperties aiProperties;
@Override
+ @Transactional(rollbackFor = Exception.class)
public Long createModel(AiModelSaveReqVO createReqVO) {
AiPlatformEnum.validatePlatform(createReqVO.getPlatform());
apiKeyService.validateApiKey(createReqVO.getKeyId());
AiModelDO model = BeanUtils.toBean(createReqVO, AiModelDO.class);
+ model.setCapability(resolveCapability(createReqVO.getCapability()));
+ if (CommonStatusEnum.isEnable(model.getStatus())) {
+ disableOtherEnabledModels(model);
+ }
modelMapper.insert(model);
return model.getId();
}
@Override
+ @Transactional(rollbackFor = Exception.class)
public void updateModel(AiModelSaveReqVO updateReqVO) {
- validateModelExists(updateReqVO.getId());
+ AiModelDO existing = validateModelExists(updateReqVO.getId());
AiPlatformEnum.validatePlatform(updateReqVO.getPlatform());
apiKeyService.validateApiKey(updateReqVO.getKeyId());
- modelMapper.updateById(BeanUtils.toBean(updateReqVO, AiModelDO.class));
+ AiModelDO model = BeanUtils.toBean(updateReqVO, AiModelDO.class);
+ // 鏈紶鑳藉姏鏃舵部鐢ㄥ師鍊硷紝閬垮厤鑰佸墠绔彁浜ゆ妸瑙嗚妯″瀷闄嶇骇鎴愭枃鏈兘鍔�
+ model.setCapability(resolveCapability(
+ updateReqVO.getCapability() == null || updateReqVO.getCapability().isBlank()
+ ? existing.getCapability() : updateReqVO.getCapability()));
+ if (CommonStatusEnum.isEnable(model.getStatus())) {
+ disableOtherEnabledModels(model);
+ }
+ modelMapper.updateById(model);
}
@Override
@@ -66,8 +84,17 @@
@Override
public AiModelDO getRequiredDefaultModel(Integer type) {
- AiModelDO model = modelMapper.selectFirstByStatus(type, CommonStatusEnum.ENABLE.getStatus());
- if (model == null) throw exception(MODEL_DISABLE);
+ return getRequiredDefaultModel(type, AiModelCapabilityEnum.defaultCapability());
+ }
+
+ @Override
+ public AiModelDO getRequiredDefaultModel(Integer type, String capability) {
+ AiModelDO model = modelMapper.selectFirstByTypeAndCapabilityAndStatus(
+ type, resolveCapability(capability), CommonStatusEnum.ENABLE.getStatus());
+ if (model == null) {
+ throw exception(MODEL_NOT_ENABLED,
+ AiModelTypeEnum.getName(type), AiModelCapabilityEnum.getName(resolveCapability(capability)));
+ }
return model;
}
@@ -77,9 +104,21 @@
}
@Override
+ @Transactional(rollbackFor = Exception.class)
+ public void updateModelStatus(AiModelUpdateStatusReqVO updateReqVO) {
+ AiModelDO model = validateModelExists(updateReqVO.getId());
+ model.setStatus(updateReqVO.getStatus());
+ if (CommonStatusEnum.isEnable(updateReqVO.getStatus())) {
+ // 鍚敤锛氳嚜鍔ㄥ叧闂悓銆岀被鍨� + 鑳藉姏銆嶄笅鐨勫叾浠栨ā鍨嬶紝淇濊瘉鍚岀粍鍙湁涓�涓惎鐢�
+ disableOtherEnabledModels(model);
+ }
+ modelMapper.updateById(model);
+ }
+
+ @Override
public AiModelDO validateModel(Long id) {
AiModelDO model = validateModelExists(id);
- if (CommonStatusEnum.isDisable(model.getStatus())) throw exception(MODEL_DISABLE);
+ if (CommonStatusEnum.isDisable(model.getStatus())) throw exception(MODEL_DISABLE, model.getName());
return model;
}
@@ -88,6 +127,28 @@
return modelMapper.selectListByStatusAndType(status, type, platform);
}
+ /**
+ * 鍏抽棴鍚屻�岀被鍨� + 鑳藉姏銆嶄笅宸插惎鐢ㄧ殑鍏朵粬妯″瀷锛堜笉鍚嚜韬級
+ */
+ private void disableOtherEnabledModels(AiModelDO model) {
+ List<AiModelDO> sameGroup = modelMapper.selectListByTypeAndCapabilityAndStatus(
+ model.getType(), resolveCapability(model.getCapability()), CommonStatusEnum.ENABLE.getStatus());
+ for (AiModelDO other : sameGroup) {
+ if (other.getId().equals(model.getId())) {
+ continue;
+ }
+ AiModelDO update = new AiModelDO();
+ update.setId(other.getId());
+ update.setStatus(CommonStatusEnum.DISABLE.getStatus());
+ modelMapper.updateById(update);
+ }
+ }
+
+ private static String resolveCapability(String capability) {
+ return capability == null || capability.isBlank()
+ ? AiModelCapabilityEnum.defaultCapability() : capability;
+ }
+
private AiModelDO validateModelExists(Long id) {
AiModelDO model = modelMapper.selectById(id);
if (model == null) throw exception(MODEL_NOT_EXISTS);
diff --git a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/agent/vo/MdmAgentImportPreflightRespVO.java b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/agent/vo/MdmAgentImportPreflightRespVO.java
index a90bf49..c30b07d 100644
--- a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/agent/vo/MdmAgentImportPreflightRespVO.java
+++ b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/agent/vo/MdmAgentImportPreflightRespVO.java
@@ -35,7 +35,6 @@
@Builder
public static class Authorization {
private Long userId;
- private Long tenantId;
private String permission;
private String policyRevision;
}
diff --git a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/agent/MdmAgentImportCommandDO.java b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/agent/MdmAgentImportCommandDO.java
index a00ba07..15e9ae5 100644
--- a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/agent/MdmAgentImportCommandDO.java
+++ b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/agent/MdmAgentImportCommandDO.java
@@ -8,7 +8,7 @@
import java.time.LocalDateTime;
/**
- * 鏅鸿兘浣撶墿鏂欏鍏ュ懡浠ゃ�傚悓涓� (tenant, owner, commandId/idempotencyKey) 鍙厑璁镐竴鏉°��
+ * 鏅鸿兘浣撶墿鏂欏鍏ュ懡浠ゃ�傚悓涓� (owner, commandId/idempotencyKey) 鍙厑璁镐竴鏉°��
*/
@TableName("mdm_agent_import_command")
@Data
@@ -25,7 +25,6 @@
private String commandId;
private String idempotencyKey;
private String requestDigest;
- private Long tenantId;
private Long ownerUserId;
private Long sessionId;
private Long attachmentId;
diff --git a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/agent/MdmAgentImportCommandMapper.java b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/agent/MdmAgentImportCommandMapper.java
index f64d8eb..3aba131 100644
--- a/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/agent/MdmAgentImportCommandMapper.java
+++ b/yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/agent/MdmAgentImportCommandMapper.java
@@ -8,16 +8,14 @@
@Mapper
public interface MdmAgentImportCommandMapper extends BaseMapperX<MdmAgentImportCommandDO> {
- default MdmAgentImportCommandDO selectByCommandId(Long tenantId, Long ownerUserId, String commandId) {
+ default MdmAgentImportCommandDO selectByCommandId(Long ownerUserId, String commandId) {
return selectOne(new LambdaQueryWrapperX<MdmAgentImportCommandDO>()
- .eq(MdmAgentImportCommandDO::getTenantId, tenantId)
.eq(MdmAgentImportCommandDO::getOwnerUserId, ownerUserId)
.eq(MdmAgentImportCommandDO::getCommandId, commandId));
}
- default MdmAgentImportCommandDO selectByIdempotencyKey(Long tenantId, Long ownerUserId, String idempotencyKey) {
+ default MdmAgentImportCommandDO selectByIdempotencyKey(Long ownerUserId, String idempotencyKey) {
return selectOne(new LambdaQueryWrapperX<MdmAgentImportCommandDO>()
- .eq(MdmAgentImportCommandDO::getTenantId, tenantId)
.eq(MdmAgentImportCommandDO::getOwnerUserId, ownerUserId)
.eq(MdmAgentImportCommandDO::getIdempotencyKey, idempotencyKey));
}
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 f3c144c..173b562 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
@@ -74,11 +74,10 @@
"鏈嶅姟鏈紑鏀惧悓姝� MES锛岃鍏抽棴 syncMes 鍚庨噸璇�");
}
String requestDigest = buildRequestDigest(reqVO);
- Long tenantId = principal.tenantId();
Long ownerUserId = principal.userId();
// 1. 骞傜瓑鍛戒腑锛氬懡浠� ID 浼樺厛锛屽叾娆″箓绛夐敭
- MdmAgentImportCommandDO existing = commandMapper.selectByCommandId(tenantId, ownerUserId, reqVO.getCommandId());
+ MdmAgentImportCommandDO existing = commandMapper.selectByCommandId(ownerUserId, reqVO.getCommandId());
if (existing != null) {
if (!existing.getRequestDigest().equals(requestDigest)) {
throw new AgentBridgeException(HttpStatus.CONFLICT, "COMMAND_ID_CONFLICT",
@@ -86,7 +85,7 @@
}
return toReceipt(existing);
}
- existing = commandMapper.selectByIdempotencyKey(tenantId, ownerUserId, reqVO.getIdempotencyKey());
+ existing = commandMapper.selectByIdempotencyKey(ownerUserId, reqVO.getIdempotencyKey());
if (existing != null) {
if (!existing.getRequestDigest().equals(requestDigest)) {
throw new AgentBridgeException(HttpStatus.CONFLICT, "IDEMPOTENCY_CONFLICT",
@@ -112,7 +111,6 @@
.commandId(reqVO.getCommandId())
.idempotencyKey(reqVO.getIdempotencyKey())
.requestDigest(requestDigest)
- .tenantId(tenantId)
.ownerUserId(ownerUserId)
.sessionId(reqVO.getSessionId())
.attachmentId(reqVO.getAttachmentId())
@@ -128,9 +126,9 @@
try {
commandMapper.insert(command);
} catch (DuplicateKeyException ex) {
- MdmAgentImportCommandDO raced = commandMapper.selectByCommandId(tenantId, ownerUserId, reqVO.getCommandId());
+ MdmAgentImportCommandDO raced = commandMapper.selectByCommandId(ownerUserId, reqVO.getCommandId());
if (raced == null) {
- raced = commandMapper.selectByIdempotencyKey(tenantId, ownerUserId, reqVO.getIdempotencyKey());
+ raced = commandMapper.selectByIdempotencyKey(ownerUserId, reqVO.getIdempotencyKey());
}
if (raced == null) {
throw ex;
@@ -244,7 +242,7 @@
public MdmAgentImportReceiptRespVO getCommand(String commandId, AgentPrincipal principal) {
MdmAgentImportCommandDO command = commandMapper.selectByCommandId(
- principal.tenantId(), principal.userId(), commandId);
+ principal.userId(), commandId);
if (command == null) {
throw new AgentBridgeException(HttpStatus.NOT_FOUND, "COMMAND_NOT_FOUND", "瀵煎叆鍛戒护涓嶅瓨鍦�");
}
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 fa4b977..238192d 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
@@ -166,7 +166,6 @@
.build())
.authorization(MdmAgentImportPreflightRespVO.Authorization.builder()
.userId(principal.userId())
- .tenantId(principal.tenantId())
.permission(SystemAgentBridgeGuard.PERMISSION_ITEM_IMPORT)
.policyRevision(MdmAgentImportPolicy.POLICY_REVISION)
.build())
@@ -233,7 +232,6 @@
Map<String, Object> canonical = new LinkedHashMap<>();
canonical.put("policy_revision", MdmAgentImportPolicy.POLICY_REVISION);
canonical.put("user_id", principal.userId());
- canonical.put("tenant_id", principal.tenantId());
canonical.put("attachment_id", attachment.attachmentId());
canonical.put("session_id", attachment.sessionId());
canonical.put("source_sha256", sourceSha256.toLowerCase(Locale.ROOT));
diff --git a/yudao-module-mdm/src/test/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportCommandServiceTest.java b/yudao-module-mdm/src/test/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportCommandServiceTest.java
index d112132..fc0d17b 100644
--- a/yudao-module-mdm/src/test/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportCommandServiceTest.java
+++ b/yudao-module-mdm/src/test/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportCommandServiceTest.java
@@ -49,7 +49,7 @@
@Mock private PlatformTransactionManager transactionManager;
@InjectMocks private MdmAgentImportCommandService service;
- private final AgentPrincipal principal = new AgentPrincipal(7L, 0L, "live");
+ private final AgentPrincipal principal = new AgentPrincipal(7L, "live");
@BeforeEach
void setUp() {
@@ -81,7 +81,7 @@
AtomicReference<MdmAgentImportCommandDO> inserted = captureInsertedCommand();
MdmAgentImportReceiptRespVO first = service.commit(request("鐗╂枡A"), principal);
- when(commandMapper.selectByCommandId(0L, 7L, "cmd-1")).thenReturn(inserted.get());
+ when(commandMapper.selectByCommandId(7L, "cmd-1")).thenReturn(inserted.get());
MdmAgentImportReceiptRespVO second = service.commit(request("鐗╂枡A"), principal);
assertEquals("SUCCEEDED", first.getStatus());
@@ -95,8 +95,8 @@
when(preflightService.preflight(any(), any())).thenReturn(preflight(PREFLIGHT_HASH));
AtomicReference<MdmAgentImportCommandDO> inserted = captureInsertedCommand();
service.commit(request("鐗╂枡A"), principal);
- when(commandMapper.selectByCommandId(0L, 7L, "cmd-1")).thenReturn(inserted.get());
- when(commandMapper.selectByIdempotencyKey(0L, 7L, "idem-1")).thenReturn(null);
+ when(commandMapper.selectByCommandId(7L, "cmd-1")).thenReturn(inserted.get());
+ when(commandMapper.selectByIdempotencyKey(7L, "idem-1")).thenReturn(null);
AgentBridgeException ex = assertThrows(AgentBridgeException.class,
() -> service.commit(request("鐗╂枡B"), principal));
@@ -115,7 +115,7 @@
@Test
void missingCommandReturnsNotFound() {
- when(commandMapper.selectByCommandId(0L, 7L, "cmd-x")).thenReturn(null);
+ when(commandMapper.selectByCommandId(7L, "cmd-x")).thenReturn(null);
AgentBridgeException ex = assertThrows(AgentBridgeException.class,
() -> service.getCommand("cmd-x", principal));
diff --git a/yudao-module-mdm/src/test/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightServiceTest.java b/yudao-module-mdm/src/test/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightServiceTest.java
index d260d03..f30b5fe 100644
--- a/yudao-module-mdm/src/test/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightServiceTest.java
+++ b/yudao-module-mdm/src/test/java/cn/iocoder/yudao/module/mdm/service/agent/MdmAgentImportPreflightServiceTest.java
@@ -39,7 +39,7 @@
@Mock private MdmAgentImportPolicy policy;
@InjectMocks private MdmAgentImportPreflightService service;
- private final AgentPrincipal principal = new AgentPrincipal(7L, 0L, "live");
+ private final AgentPrincipal principal = new AgentPrincipal(7L, "live");
@Test
void classifiesCreateSkipAndBlockedRows() {
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateImportAdapter.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateImportAdapter.java
new file mode 100644
index 0000000..cae2a66
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/template/MesQcTemplateImportAdapter.java
@@ -0,0 +1,266 @@
+package cn.iocoder.yudao.module.mes.service.qc.template;
+
+import cn.hutool.crypto.digest.DigestUtil;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.indicator.vo.MesQcIndicatorSaveReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.MesQcTemplateSaveReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.indicator.MesQcTemplateIndicatorSaveReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateIndicatorDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.qc.indicator.MesQcIndicatorMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.qc.template.MesQcTemplateIndicatorMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.qc.template.MesQcTemplateMapper;
+import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
+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 MesQcTemplateImportAdapter implements AgentImportModuleAdapter {
+
+ public static final String MODULE_ID = "mes.qc-template";
+
+ @Resource private MesQcTemplateMapper templateMapper;
+ @Resource private MesQcTemplateService templateService;
+ @Resource private MesQcIndicatorMapper indicatorMapper;
+ @Resource private MesQcIndicatorService indicatorService;
+ @Resource private MesQcTemplateIndicatorMapper templateIndicatorMapper;
+ @Resource private MesQcTemplateIndicatorService templateIndicatorService;
+
+ @Override
+ public String moduleId() {
+ return MODULE_ID;
+ }
+
+ @Override
+ public String permission() {
+ return "mes:qc-template:create";
+ }
+
+ @Override
+ public List<String> permissions() {
+ return List.of("mes:qc-template:create", "mes:qc-indicator:create");
+ }
+
+ @Override
+ public AgentImportPreflightRespVO preflight(AgentPrincipal principal, AgentImportPreflightReqVO request) {
+ List<AgentImportPreflightRespVO.Row> plannedRows = new ArrayList<>();
+ List<Map<String, Object>> resolvedPayload = new ArrayList<>();
+ Set<String> fileKeys = new HashSet<>();
+ 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 templateCode = value(row, "code");
+ String templateName = value(row, "name");
+ String indicatorName = value(row, "indicatorName");
+ if (templateCode.isEmpty()) blockers.add(issue("TEMPLATE_CODE_REQUIRED", "code", "鏂规缂栧彿涓嶈兘涓虹┖"));
+ if (templateName.isEmpty()) blockers.add(issue("TEMPLATE_NAME_REQUIRED", "name", "鏂规鍚嶇О涓嶈兘涓虹┖"));
+ if (indicatorName.isEmpty()) blockers.add(issue("INDICATOR_NAME_REQUIRED", "indicatorName", "妫�娴嬮」涓嶈兘涓虹┖"));
+
+ List<Integer> types = parseTypes(value(row, "types"), blockers);
+ Integer status = parseInteger(value(row, "status"), 0, "status", blockers);
+ Integer indicatorType = parseInteger(value(row, "indicatorType"), 5, "indicatorType", blockers);
+ Integer resultType = parseInteger(value(row, "resultType"), 1, "resultType", blockers);
+ BigDecimal thresholdMax = decimal(value(row, "thresholdMax"), "thresholdMax", blockers);
+ BigDecimal thresholdMin = decimal(value(row, "thresholdMin"), "thresholdMin", blockers);
+
+ MesQcIndicatorDO indicator = findIndicator(value(row, "indicatorCode"), indicatorName);
+ String indicatorCode = indicator == null ? proposedCode(indicatorName) : indicator.getCode();
+ String indicatorAction = indicator == null ? "CREATE_AFTER_REVIEW" : "REUSE";
+ if (indicator == null && !indicatorName.isEmpty()) {
+ warnings.add(issue("INDICATOR_WILL_CREATE", "indicatorName",
+ "绯荤粺涓嶅瓨鍦ㄦ娴嬮」鈥�" + indicatorName + "鈥濓紱纭瀵煎叆鍚庡皢鍏堟柊澧炴娴嬮」 " + indicatorCode));
+ }
+ if (!value(row, "unitCode").isEmpty()) {
+ warnings.add(issue("UNIT_REQUIRES_REVIEW", "unitCode",
+ "璁¢噺鍗曚綅鈥�" + value(row, "unitCode") + "鈥濇殏浠ュ師鏂囦繚鐣欙紝璇峰鏍哥郴缁熷崟浣嶅叧鑱�"));
+ }
+
+ MesQcTemplateDO template = templateCode.isEmpty() ? null : templateMapper.selectByCode(templateCode);
+ MesQcTemplateIndicatorDO relation = template == null || indicator == null ? null
+ : templateIndicatorMapper.selectListByTemplateId(template.getId()).stream()
+ .filter(item -> Objects.equals(item.getIndicatorId(), indicator.getId())).findFirst().orElse(null);
+ String fileKey = templateCode + "\u0000" + indicatorName;
+ String action;
+ Long targetId = null;
+ if (!blockers.isEmpty()) {
+ action = "BLOCKED";
+ blocked++;
+ } else if (!fileKeys.add(fileKey)) {
+ blockers.add(issue("DUPLICATE_IN_FILE", "indicatorName", "鏂囦欢鍐呮娴嬮」閲嶅锛岃淇濈暀涓�鏉�"));
+ action = "BLOCKED";
+ blocked++;
+ } else if (relation != null) {
+ action = "SKIP";
+ targetId = relation.getId();
+ skip++;
+ warnings.add(issue("TEMPLATE_INDICATOR_EXISTS", "indicatorName", "鏂规宸插叧鑱旇妫�娴嬮」锛屽凡璺宠繃"));
+ } else {
+ action = "CREATE";
+ create++;
+ }
+
+ Map<String, Object> resolved = new LinkedHashMap<>();
+ resolved.put("code", templateCode);
+ resolved.put("name", templateName);
+ resolved.put("types", types);
+ resolved.put("status", status);
+ resolved.put("indicatorCode", indicatorCode);
+ resolved.put("indicatorName", indicatorName);
+ resolved.put("indicatorType", indicatorType);
+ resolved.put("resultType", resultType);
+ resolved.put("indicatorAction", indicatorAction);
+ resolved.put("thresholdMax", thresholdMax);
+ resolved.put("thresholdMin", thresholdMin);
+ resolved.put("standardValue", value(row, "standardValue"));
+ resolved.put("unitCode", value(row, "unitCode"));
+ resolved.put("action", action);
+ resolvedPayload.add(resolved);
+
+ plannedRows.add(AgentImportPreflightRespVO.Row.builder()
+ .candidateId(row.getCandidateId()).rowNo(row.getRowNo()).action(action)
+ .resolved(resolved).blockers(blockers).warnings(warnings).targetId(targetId).build());
+ }
+ 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(plannedRows.size()).create(create).skip(skip).blocked(blocked).build())
+ .canCommit(blocked == 0)
+ .sideEffects(List.of(
+ "鎸夋柟妗堢紪鍙峰垱寤鸿川妫�鏂规锛堟娴嬬绫讳娇鐢ㄧ郴缁� mes_qc_type锛�",
+ "澶嶇敤鍚屽悕妫�娴嬮」锛涚己澶辨娴嬮」缁忔湰娆′汉宸ョ‘璁ゅ悗鏂板",
+ "鍒涘缓鏂规涓庢娴嬮」鍏宠仈锛屼笉瑕嗙洊鐜版湁妫�娴嬮」涓绘暟鎹�"))
+ .rows(plannedRows).blockingItems(List.of()).build();
+ }
+
+ @Override
+ public AgentImportRowReceipt commitRow(AgentPrincipal principal, AgentImportCommitReqVO request,
+ AgentImportPreflightRespVO.Row planned,
+ AgentImportPreflightReqVO.Row row) {
+ MesQcTemplateDO template = templateMapper.selectByCode(value(row, "code"));
+ if (template == null) {
+ MesQcTemplateSaveReqVO create = new MesQcTemplateSaveReqVO();
+ create.setCode(value(row, "code"));
+ create.setName(value(row, "name"));
+ create.setTypes(parseTypes(value(row, "types"), new ArrayList<>()));
+ create.setStatus(parseInteger(value(row, "status"), 0, "status", new ArrayList<>()));
+ create.setRemark("鏉ユ簮锛氬伐涓氭櫤鑳戒綋鏂囨。琛ュ綍锛涘凡鐢辩敤鎴风‘璁�");
+ Long id = templateService.createTemplate(create);
+ template = templateService.getTemplate(id);
+ }
+
+ String indicatorName = value(row, "indicatorName");
+ MesQcIndicatorDO indicator = findIndicator(value(row, "indicatorCode"), indicatorName);
+ if (indicator == null) {
+ MesQcIndicatorSaveReqVO create = new MesQcIndicatorSaveReqVO();
+ create.setCode(proposedCode(indicatorName));
+ create.setName(indicatorName);
+ create.setType(parseInteger(value(row, "indicatorType"), 5, "indicatorType", new ArrayList<>()));
+ create.setItemType(1);
+ create.setParentId(0L);
+ create.setResultType(parseInteger(value(row, "resultType"), 1, "resultType", new ArrayList<>()));
+ create.setRemark("鏉ユ簮锛氬伐涓氭櫤鑳戒綋浠庡凡鎺堟潈 SOP 鎻愬彇锛涚粡鐢ㄦ埛瀹℃牳鏂板");
+ Long id = indicatorService.createIndicator(create);
+ indicator = indicatorService.getIndicator(id);
+ }
+
+ Long indicatorId = indicator.getId();
+ MesQcTemplateIndicatorDO existing = templateIndicatorMapper.selectListByTemplateId(template.getId()).stream()
+ .filter(item -> Objects.equals(item.getIndicatorId(), indicatorId)).findFirst().orElse(null);
+ if (existing != null) {
+ return AgentImportRowReceipt.created(existing.getId(), value(row, "code"), indicatorName);
+ }
+ MesQcTemplateIndicatorSaveReqVO create = new MesQcTemplateIndicatorSaveReqVO();
+ create.setTemplateId(template.getId());
+ create.setIndicatorId(indicatorId);
+ create.setThresholdMax(decimal(value(row, "thresholdMax"), "thresholdMax", new ArrayList<>()));
+ create.setThresholdMin(decimal(value(row, "thresholdMin"), "thresholdMin", new ArrayList<>()));
+ String standard = value(row, "standardValue");
+ String unit = value(row, "unitCode");
+ create.setRemark("鍘熸枃鏍囧噯锛�" + standard + (unit.isEmpty() ? "" : "锛涘崟浣嶏細" + unit));
+ Long relationId = templateIndicatorService.createTemplateIndicator(create);
+ return AgentImportRowReceipt.created(relationId, value(row, "code"), indicatorName);
+ }
+
+ private MesQcIndicatorDO findIndicator(String code, String name) {
+ if (!code.isEmpty()) {
+ MesQcIndicatorDO found = indicatorMapper.selectByCode(code);
+ if (found != null) return found;
+ }
+ if (name.isEmpty()) return null;
+ return indicatorService.getIndicatorList().stream()
+ .filter(item -> name.equalsIgnoreCase(item.getName().trim())).findFirst().orElse(null);
+ }
+
+ private static String proposedCode(String name) {
+ return "AI-QI-" + DigestUtil.sha256Hex(name == null ? "" : name.trim()).substring(0, 12).toUpperCase(Locale.ROOT);
+ }
+
+ private static List<Integer> parseTypes(String raw, List<AgentImportPreflightRespVO.Issue> blockers) {
+ List<Integer> values = new ArrayList<>();
+ for (String part : raw.replace("[", "").replace("]", "").split("[,锛宂")) {
+ if (part.isBlank()) continue;
+ try {
+ int value = Integer.parseInt(part.trim());
+ if (value < 1 || value > 4) throw new NumberFormatException();
+ values.add(value);
+ } catch (NumberFormatException ex) {
+ blockers.add(issue("INVALID_QC_TYPE", "types", "妫�娴嬬绫诲繀椤讳娇鐢ㄧ郴缁熷瓧鍏稿�� 1/2/3/4"));
+ }
+ }
+ if (values.isEmpty()) blockers.add(issue("QC_TYPE_REQUIRED", "types", "妫�娴嬬绫讳笉鑳戒负绌�"));
+ return values;
+ }
+
+ private static Integer parseInteger(String raw, int defaultValue, String field,
+ List<AgentImportPreflightRespVO.Issue> blockers) {
+ if (raw.isEmpty()) return defaultValue;
+ try {
+ return Integer.valueOf(raw);
+ } catch (NumberFormatException ex) {
+ blockers.add(issue("INVALID_INTEGER", field, field + " 蹇呴』鏄暣鏁�"));
+ return defaultValue;
+ }
+ }
+
+ private static BigDecimal decimal(String raw, String field,
+ List<AgentImportPreflightRespVO.Issue> blockers) {
+ if (raw.isEmpty()) return null;
+ try {
+ return new BigDecimal(raw);
+ } catch (NumberFormatException ex) {
+ blockers.add(issue("INVALID_DECIMAL", field, field + " 蹇呴』鏄暟瀛�"));
+ return null;
+ }
+ }
+
+ 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 AgentImportPreflightRespVO.Issue issue(String code, String field, String message) {
+ return AgentImportPreflightRespVO.Issue.builder()
+ .code(code).field(field).message(message).retryable(false).build();
+ }
+}
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
index d1a67be..dd845ab 100644
--- 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
@@ -2,6 +2,8 @@
import cn.iocoder.yudao.module.system.framework.agent.AgentPrincipal;
+import java.util.List;
+
/**
* 涓氬姟妯″潡瀵煎叆閫傞厤鍣細system 妯″潡璐熻矗骞傜瓑銆侀�愯浜嬪姟涓庡洖鎵э紝閫傞厤鍣ㄥ彧璐熻矗鍗曡鏍¢獙鍜屽啓鍏ャ��
*/
@@ -13,6 +15,11 @@
/** 鎻愪氦璇ユā鍧楁墍闇�鐨勪笟鍔℃潈闄愩�� */
String permission();
+ /** 妯″潡鍐欏叆鍙兘鍚屾椂鍒涘缓渚濊禆涓绘暟鎹紱鎻愪氦鍓嶅繀椤诲悓鏃跺叿澶囪繖浜涙寜閽潈闄愩�� */
+ default List<String> permissions() {
+ return List.of(permission());
+ }
+
/** 鍙棰勬锛涘繀椤昏繑鍥炵ǔ瀹氱殑 preflightHash 渚涙彁浜ゆ椂姣斿銆� */
AgentImportPreflightRespVO preflight(AgentPrincipal principal, AgentImportPreflightReqVO request);
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentCapabilityController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentCapabilityController.java
new file mode 100644
index 0000000..e5936d7
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentCapabilityController.java
@@ -0,0 +1,117 @@
+package cn.iocoder.yudao.module.system.controller.admin.agent;
+
+import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
+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.permission.PermissionService;
+import jakarta.annotation.Resource;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+/** 涓烘櫤鑳戒綋瑙勫垝杩斿洖缁忚繃鏈嶅姟绔鍓殑鑿滃崟銆佹寜閽�佸瓧娈靛拰鏁版嵁鑼冨洿鑳藉姏銆� */
+@RestController
+@RequestMapping("/agent/bridge")
+public class SystemAgentCapabilityController {
+
+ private static final List<CapabilityDefinition> CATALOG = List.of(
+ new CapabilityDefinition("mdm.item.import", "mdm.item", "import", "mdm:item:import",
+ Set.of("code", "name", "specification", "categoryCode", "unitMeasureCode")),
+ new CapabilityDefinition("srm.supplier.create", "srm.supplier", "create", "srm:supplier:create",
+ Set.of("code", "name", "shortName", "type", "companyNature", "registeredCapital",
+ "businessLicenseNo", "taxNo", "legalPerson", "address", "website", "contactName",
+ "contactMobile", "contactEmail", "bankName", "bankAccount", "status", "remark")),
+ new CapabilityDefinition("mes.bom.create", "mes.bom", "create", "mes:md-item:create",
+ Set.of("parentCode", "componentCode", "quantity", "status", "remark")),
+ new CapabilityDefinition("mes.qc-template.prepare", "mes.qc-template", "prepare",
+ "mes:qc-template:create", Set.of("code", "name", "types", "indicatorCode",
+ "standardValue", "thresholdMax", "thresholdMin", "unitCode", "itemCode", "remark")),
+ new CapabilityDefinition("mes.mps.prepare", "mes.mps", "prepare", "mes:pro-mps:create",
+ Set.of("productCode", "quantity", "unitCode", "requestDate", "saleOrderNo", "remark")),
+ new CapabilityDefinition("mes.route.prepare", "mes.route", "prepare", "mes:pro-route:create",
+ Set.of("routeCode", "routeName", "itemCode", "processCode", "sort", "yieldRate",
+ "keyFlag", "remark"))
+ );
+
+ @Resource
+ private SystemAgentBridgeGuard bridgeGuard;
+ @Resource
+ private SecurityFrameworkService securityFrameworkService;
+ @Resource
+ private PermissionService permissionService;
+
+ @GetMapping("/capabilities")
+ public CommonResult<CapabilityCatalogResp> capabilities(
+ @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken) {
+ AgentPrincipal principal = bridgeGuard.requireAuthenticated(serviceToken);
+ DeptDataPermissionRespDTO dataScope = permissionService.getDeptDataPermission(principal.userId());
+ List<CapabilityResp> capabilities = new ArrayList<>();
+ for (CapabilityDefinition definition : CATALOG) {
+ if (securityFrameworkService.hasPermission(definition.permission())) {
+ capabilities.add(new CapabilityResp(definition.tool(), definition.module(), definition.operation(),
+ definition.fields().stream().sorted().toList(), scopeSummary(dataScope), true));
+ }
+ }
+ String policyRevision = digest(policyMaterial(principal.userId(), capabilities, dataScope));
+ String authorizationScope = "scope:" + digest(principal.userId() + ":" + policyRevision).substring(0, 32);
+ return success(new CapabilityCatalogResp(policyRevision, authorizationScope,
+ scopeSummary(dataScope), capabilities));
+ }
+
+ private static String scopeSummary(DeptDataPermissionRespDTO scope) {
+ if (Boolean.TRUE.equals(scope.getAll())) {
+ return "鍏ㄩ儴鎺堟潈鏁版嵁";
+ }
+ int departments = scope.getDeptIds() == null ? 0 : scope.getDeptIds().size();
+ if (Boolean.TRUE.equals(scope.getSelf()) && departments > 0) {
+ return "鏈汉鍙婃巿鏉冮儴闂ㄦ暟鎹�";
+ }
+ if (Boolean.TRUE.equals(scope.getSelf())) {
+ return "浠呮湰浜烘暟鎹�";
+ }
+ return departments > 0 ? "鎺堟潈閮ㄩ棬鏁版嵁" : "鏃犻儴闂ㄦ暟鎹寖鍥�";
+ }
+
+ private static String policyMaterial(Long userId, List<CapabilityResp> capabilities,
+ DeptDataPermissionRespDTO scope) {
+ List<Long> deptIds = scope.getDeptIds() == null ? List.of()
+ : scope.getDeptIds().stream().sorted(Comparator.naturalOrder()).toList();
+ return userId + "|" + Boolean.TRUE.equals(scope.getAll()) + "|" + Boolean.TRUE.equals(scope.getSelf())
+ + "|" + deptIds + "|" + capabilities.stream().map(CapabilityResp::tool).sorted().toList();
+ }
+
+ private static String digest(String value) {
+ try {
+ return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
+ .digest(value.getBytes(StandardCharsets.UTF_8)));
+ } catch (NoSuchAlgorithmException ex) {
+ throw new IllegalStateException("JVM 缂哄皯 SHA-256", ex);
+ }
+ }
+
+ private record CapabilityDefinition(String tool, String module, String operation, String permission,
+ Set<String> fields) {
+ }
+
+ public record CapabilityResp(String tool, String module, String operation, List<String> allowedFields,
+ String scopeSummary, boolean canExecute) {
+ }
+
+ public record CapabilityCatalogResp(String policyRevision, String authorizationScope, String scopeSummary,
+ List<CapabilityResp> capabilities) {
+ }
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentDelegationController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentDelegationController.java
new file mode 100644
index 0000000..ebdc505
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/agent/SystemAgentDelegationController.java
@@ -0,0 +1,130 @@
+package cn.iocoder.yudao.module.system.controller.admin.agent;
+
+import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.module.system.framework.agent.AgentBridgeException;
+import cn.iocoder.yudao.module.system.framework.agent.AgentDelegationToken;
+import cn.iocoder.yudao.module.system.framework.agent.SystemAgentBridgeGuard;
+import cn.iocoder.yudao.module.system.framework.agent.SystemAgentDelegationService;
+import cn.iocoder.yudao.module.system.service.storage.SystemAgentImportBridgeService;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+/** 娴忚鍣ㄤ互褰撳墠鐧诲綍韬唤涓哄紓姝� Worker 绛惧彂鐭湡銆佸彲鎾ら攢銆侀檺瀹氳寖鍥寸殑濮旀墭绁ㄦ嵁銆� */
+@RestController
+@RequestMapping("/agent/bridge/delegations")
+public class SystemAgentDelegationController {
+
+ 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')";
+ private static final Set<String> ALLOWED_OPERATIONS = Set.of(
+ "material_import.prepare", "material_import.read", "material_import.review",
+ "material_import.preflight", "material_import.commit", "material_import.read_receipt",
+ "conversation.create", "conversation.read", "conversation.message", "conversation.action",
+ "conversation.report", "attachment.read", "import.modules.read", "import.preflight",
+ "import.commit", "import.receipt.read");
+ 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 SystemAgentBridgeGuard bridgeGuard;
+ @Resource
+ private SystemAgentDelegationService delegationService;
+ @Resource
+ private SystemAgentImportBridgeService bridgeService;
+ @Resource
+ private PermissionCommonApi permissionApi;
+
+ @PostMapping
+ @PreAuthorize(IMPORT_PERMISSIONS_EXPR)
+ public CommonResult<AgentDelegationToken> issue(
+ @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+ @Valid @RequestBody IssueRequest request) {
+ bridgeGuard.verifyServiceToken(serviceToken);
+ if (!ALLOWED_OPERATIONS.containsAll(request.getOperations())) {
+ throw new AgentBridgeException(HttpStatus.BAD_REQUEST, "DELEGATION_OPERATION_INVALID",
+ "濮旀墭鍖呭惈涓嶆敮鎸佺殑鎿嶄綔");
+ }
+ Set<Long> attachmentIds = request.normalizedAttachmentIds();
+ for (Long attachmentId : attachmentIds) {
+ bridgeService.requireOwnedAttachmentRef(attachmentId);
+ }
+ String permission = resolvePermission(request.getModuleId());
+ AgentDelegationToken token = delegationService.issue(request.getTaskId(), request.getModuleId(),
+ attachmentIds, request.getOperations(), permission);
+ return success(token);
+ }
+
+ @DeleteMapping("/{delegationId}")
+ @PreAuthorize(IMPORT_PERMISSIONS_EXPR)
+ public CommonResult<Boolean> revoke(
+ @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+ @PathVariable String delegationId) {
+ bridgeGuard.verifyServiceToken(serviceToken);
+ delegationService.revoke(delegationId, SecurityFrameworkUtils.getLoginUserId());
+ return success(true);
+ }
+
+ private String resolvePermission(String moduleId) {
+ if (moduleId != null && !moduleId.isBlank()) {
+ String permission = MODULE_PERMISSIONS.get(moduleId);
+ if (permission == null) {
+ throw new AgentBridgeException(HttpStatus.BAD_REQUEST, "MODULE_NOT_SUPPORTED", "涓嶆敮鎸佺殑瀵煎叆妯″潡");
+ }
+ return permission;
+ }
+ Long userId = SecurityFrameworkUtils.getLoginUserId();
+ return SystemAgentBridgeGuard.IMPORT_PERMISSIONS.stream()
+ .filter(permission -> permissionApi.hasAnyPermissions(userId, permission))
+ .findFirst()
+ .orElseThrow(() -> new AgentBridgeException(HttpStatus.FORBIDDEN, "FORBIDDEN",
+ "褰撳墠鐢ㄦ埛缂哄皯鏁版嵁瀵煎叆鏉冮檺"));
+ }
+
+ @Data
+ public static class IssueRequest {
+ @NotBlank
+ private String taskId;
+ private String moduleId;
+ private Long attachmentId;
+ @Size(max = 20)
+ private Set<Long> attachmentIds;
+ @NotEmpty
+ private Set<String> operations;
+
+ public Set<Long> normalizedAttachmentIds() {
+ Set<Long> values = new LinkedHashSet<>();
+ if (attachmentIds != null) {
+ values.addAll(attachmentIds);
+ }
+ if (attachmentId != null) {
+ values.add(attachmentId);
+ }
+ if (values.stream().anyMatch(id -> id == null || id <= 0) || values.size() > 20) {
+ throw new AgentBridgeException(HttpStatus.BAD_REQUEST, "DELEGATION_SCOPE_INVALID",
+ "濮旀墭闄勪欢鑼冨洿鏃犳晥");
+ }
+ return values;
+ }
+ }
+}
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
index eab9aba..0305fbe 100644
--- 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
@@ -32,35 +32,46 @@
@GetMapping
public CommonResult<List<Map<String, String>>> modules(
- @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken) {
- bridgeGuard.requireAny(serviceToken);
+ @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+ @RequestHeader(value = "X-Agent-Delegation-Token", required = false) String delegationToken,
+ @RequestHeader(value = "X-Agent-Task-Id", required = false) String taskId) {
+ bridgeGuard.requireAny(serviceToken, delegationToken, taskId, "import.modules.read", null, null);
return success(genericImportService.modules());
}
@PostMapping("/{moduleId}/preflight")
public CommonResult<AgentImportPreflightRespVO> preflight(
@RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+ @RequestHeader(value = "X-Agent-Delegation-Token", required = false) String delegationToken,
+ @RequestHeader(value = "X-Agent-Task-Id", required = false) String taskId,
@PathVariable String moduleId,
@Valid @RequestBody AgentImportPreflightReqVO request) {
- AgentPrincipal principal = bridgeGuard.requireAny(serviceToken);
+ AgentPrincipal principal = bridgeGuard.requireAny(serviceToken, delegationToken, taskId,
+ "import.preflight", moduleId, null);
return success(genericImportService.preflight(moduleId, request, principal));
}
@PostMapping("/{moduleId}/commit")
public CommonResult<AgentImportReceiptRespVO> commit(
@RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+ @RequestHeader(value = "X-Agent-Delegation-Token", required = false) String delegationToken,
+ @RequestHeader(value = "X-Agent-Task-Id", required = false) String taskId,
@PathVariable String moduleId,
@Valid @RequestBody AgentImportCommitReqVO request) {
- AgentPrincipal principal = bridgeGuard.requireAny(serviceToken);
+ AgentPrincipal principal = bridgeGuard.requireAny(serviceToken, delegationToken, taskId,
+ "import.commit", moduleId, null);
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,
+ @RequestHeader(value = "X-Agent-Delegation-Token", required = false) String delegationToken,
+ @RequestHeader(value = "X-Agent-Task-Id", required = false) String taskId,
@PathVariable String moduleId,
@PathVariable String commandId) {
- AgentPrincipal principal = bridgeGuard.requireAny(serviceToken);
+ AgentPrincipal principal = bridgeGuard.requireAny(serviceToken, delegationToken, taskId,
+ "import.receipt.read", moduleId, null);
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 0f8c195..f92a84e 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
@@ -65,10 +65,22 @@
return success(bridgeService.bindAttachment(sessionId, request.getBlobId()));
}
+ @DeleteMapping("/attachments/{attachmentId}")
+ public CommonResult<Boolean> deleteAttachment(
+ @RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+ @RequestHeader(value = "X-Agent-Delegation-Token", required = false) String delegationToken,
+ @RequestHeader(value = "X-Agent-Task-Id", required = false) String taskId,
+ @PathVariable Long attachmentId) {
+ bridgeGuard.requireAny(serviceToken, delegationToken, taskId, "attachment.delete", null, attachmentId);
+ bridgeService.deleteImportAttachment(attachmentId);
+ return success(true);
+ }
+
@PostMapping("/authorize")
- @PreAuthorize(IMPORT_PERMISSIONS_EXPR)
public CommonResult<Map<String, String>> authorize(
@RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+ @RequestHeader(value = "X-Agent-Delegation-Token", required = false) String delegationToken,
+ @RequestHeader(value = "X-Agent-Task-Id", required = false) String taskId,
@Valid @RequestBody AuthorizeRequest request) {
if (!ACTIONS.contains(request.getAction())) {
throw new AgentBridgeException(HttpStatus.FORBIDDEN, "ACTION_FORBIDDEN", "涓嶆敮鎸佺殑鏅鸿兘浣撴搷浣�");
@@ -77,26 +89,30 @@
if (request.getAction().startsWith("conversation.")) {
String permission = MODULE_PERMISSIONS.get(request.getResource());
principal = permission == null
- ? bridgeGuard.requireAny(serviceToken)
- : bridgeGuard.require(serviceToken, permission);
+ ? bridgeGuard.requireAny(serviceToken, delegationToken, taskId, request.getAction(),
+ request.getResource(), null)
+ : bridgeGuard.require(serviceToken, delegationToken, taskId, request.getAction(), permission,
+ request.getResource(), null);
} else {
- principal = bridgeGuard.require(serviceToken, SystemAgentBridgeGuard.PERMISSION_ITEM_IMPORT);
+ principal = bridgeGuard.require(serviceToken, delegationToken, taskId, request.getAction(),
+ SystemAgentBridgeGuard.PERMISSION_ITEM_IMPORT, "mdm.item",
+ requiresAttachmentOwnership(request.getAction()) ? parseId(request.getResource()) : null);
}
if (requiresAttachmentOwnership(request.getAction())) {
bridgeService.requireOwnedAttachmentRef(parseId(request.getResource()));
}
// read/review/read_receipt 鐨勪换鍔� UUID 鎵�鏈夋潈鐢� Python checkpoint 浣滅敤鍩熸牳楠岋紱鏈閲嶆煡褰撳墠鎸夐挳鏉冮檺銆�
return success(Map.of("user_id", String.valueOf(principal.userId()),
- "tenant_id", String.valueOf(principal.tenantId()),
"policy_revision", principal.policyRevision()));
}
@GetMapping("/attachments/{attachmentId}")
- @PreAuthorize(IMPORT_PERMISSIONS_EXPR)
public ResponseEntity<FileSystemResource> downloadAttachment(
@RequestHeader(value = "X-Agent-Service-Token", required = false) String serviceToken,
+ @RequestHeader(value = "X-Agent-Delegation-Token", required = false) String delegationToken,
+ @RequestHeader(value = "X-Agent-Task-Id", required = false) String taskId,
@PathVariable Long attachmentId) {
- bridgeGuard.requireAny(serviceToken);
+ bridgeGuard.requireAny(serviceToken, delegationToken, taskId, "attachment.read", null, attachmentId);
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
index 7cd61a9..f3e1da4 100644
--- 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
@@ -8,7 +8,7 @@
import java.time.LocalDateTime;
/**
- * 閫氱敤涓氬姟瀵煎叆鍛戒护銆傚悓涓� (tenant, owner, module, commandId/idempotencyKey) 鍙厑璁镐竴鏉°��
+ * 閫氱敤涓氬姟瀵煎叆鍛戒护銆傚悓涓� (owner, module, commandId/idempotencyKey) 鍙厑璁镐竴鏉°��
*/
@TableName("agent_import_command")
@Data
@@ -26,7 +26,6 @@
private String idempotencyKey;
private String requestDigest;
private String moduleId;
- private Long tenantId;
private Long ownerUserId;
private Long sessionId;
private Long attachmentId;
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
index 1308c76..c90cc64 100644
--- 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
@@ -8,19 +8,15 @@
@Mapper
public interface AgentImportCommandMapper extends BaseMapperX<AgentImportCommandDO> {
- default AgentImportCommandDO selectByCommandId(Long tenantId, Long ownerUserId,
- String moduleId, String commandId) {
+ default AgentImportCommandDO selectByCommandId(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) {
+ default AgentImportCommandDO selectByIdempotencyKey(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/framework/agent/AgentDelegation.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentDelegation.java
new file mode 100644
index 0000000..beb4af6
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentDelegation.java
@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.system.framework.agent;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Set;
+
+/** Redis 涓繚瀛樼殑鏅鸿兘浣撶煭鏈熷鎵橈紱涓嶅寘鍚敤鎴风櫥褰� Token銆� */
+@Data
+public class AgentDelegation {
+
+ private String id;
+ private String secretHash;
+ private String taskId;
+ private Long userId;
+ private String moduleId;
+ private Long attachmentId;
+ /**
+ * 寮傛鎵归噺鏂囨。浠诲姟鍏佽璇诲彇鐨勯檮浠堕泦鍚堛�俛ttachmentId 淇濈暀鐢ㄤ簬鍏煎宸茬鍙戠殑鍗曢檮浠剁エ鎹��
+ */
+ private Set<Long> attachmentIds;
+ private Set<String> operations;
+ private String permission;
+ private LocalDateTime issuedAt;
+ private LocalDateTime expiresAt;
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentDelegationStore.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentDelegationStore.java
new file mode 100644
index 0000000..b9bca1e
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentDelegationStore.java
@@ -0,0 +1,11 @@
+package cn.iocoder.yudao.module.system.framework.agent;
+
+/** 濮旀墭绁ㄦ嵁瀛樺偍杈圭晫銆� */
+public interface AgentDelegationStore {
+
+ void save(AgentDelegation delegation, long ttlSeconds);
+
+ AgentDelegation get(String delegationId);
+
+ void delete(String delegationId);
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentDelegationToken.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentDelegationToken.java
new file mode 100644
index 0000000..5f6a1da
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentDelegationToken.java
@@ -0,0 +1,7 @@
+package cn.iocoder.yudao.module.system.framework.agent;
+
+import java.time.LocalDateTime;
+
+/** 鍙湪绛惧彂鏃惰繑鍥炰竴娆$殑濮旀墭鍑嵁銆� */
+public record AgentDelegationToken(String delegationId, String token, LocalDateTime expiresAt) {
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentPrincipal.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentPrincipal.java
index 5b0edc4..ddf20dd 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentPrincipal.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/AgentPrincipal.java
@@ -1,5 +1,5 @@
package cn.iocoder.yudao.module.system.framework.agent;
/** 鏅鸿兘浣撹皟鐢ㄧ粡 Java 鐜版煡鍚庣殑韬唤涓婁笅鏂囷紝涓嶆潵鑷鎴风鑷姤銆� */
-public record AgentPrincipal(Long userId, Long tenantId, String policyRevision) {
+public record AgentPrincipal(Long userId, String policyRevision) {
}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/RedisAgentDelegationStore.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/RedisAgentDelegationStore.java
new file mode 100644
index 0000000..de9b2f8
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/RedisAgentDelegationStore.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.system.framework.agent;
+
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+/** Redis 濮旀墭瀛樺偍锛涘彧淇濆瓨瀵嗛挜鎽樿锛屽師濮嬬エ鎹笉浼氳惤搴撱�� */
+@Component
+@RequiredArgsConstructor
+public class RedisAgentDelegationStore implements AgentDelegationStore {
+
+ private static final String REDIS_KEY_PREFIX = "agent:bridge:delegation:";
+ private final StringRedisTemplate stringRedisTemplate;
+
+ @Override
+ public void save(AgentDelegation delegation, long ttlSeconds) {
+ stringRedisTemplate.opsForValue().set(key(delegation.getId()), JsonUtils.toJsonString(delegation),
+ ttlSeconds, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public AgentDelegation get(String delegationId) {
+ return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(key(delegationId)),
+ AgentDelegation.class);
+ }
+
+ @Override
+ public void delete(String delegationId) {
+ stringRedisTemplate.delete(key(delegationId));
+ }
+
+ private static String key(String delegationId) {
+ return REDIS_KEY_PREFIX + delegationId;
+ }
+}
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 c5514ee..b50e361 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
@@ -2,7 +2,6 @@
import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
-import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
@@ -32,6 +31,8 @@
@Resource
private SecurityFrameworkService securityFrameworkService;
+ @Resource
+ private SystemAgentDelegationService delegationService;
public void verifyServiceToken(String provided) {
if (bridgeServiceToken == null || bridgeServiceToken.isBlank() || provided == null
@@ -40,6 +41,16 @@
throw new AgentBridgeException(HttpStatus.FORBIDDEN, "SERVICE_TOKEN_INVALID",
"鏅鸿兘浣撴湇鍔¤韩浠芥棤鏁堬紝璇锋鏌ユ湇鍔″瘑閽ラ厤缃�");
}
+ }
+
+ /** 鍙‘璁ゆ湇鍔¤韩浠戒笌褰撳墠鐧诲綍鐢ㄦ埛锛涘叿浣撹兘鍔涚敱璋冪敤鏂归�愰」鏀剁揣銆� */
+ public AgentPrincipal requireAuthenticated(String providedServiceToken) {
+ verifyServiceToken(providedServiceToken);
+ Long userId = SecurityFrameworkUtils.getLoginUserId();
+ if (userId == null) {
+ throw new AgentBridgeException(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "鏈櫥褰曟垨鐧诲綍宸茶繃鏈�");
+ }
+ return principalOf(userId);
}
public AgentPrincipal require(String providedServiceToken, String permission) {
@@ -55,6 +66,15 @@
return principalOf(userId);
}
+ public AgentPrincipal require(String providedServiceToken, String delegationToken, String taskId,
+ String operation, String permission, String moduleId, Long attachmentId) {
+ verifyServiceToken(providedServiceToken);
+ if (delegationToken != null && !delegationToken.isBlank()) {
+ return delegationService.require(delegationToken, taskId, operation, permission, moduleId, attachmentId);
+ }
+ return require(providedServiceToken, permission);
+ }
+
/** 浼氳瘽涓庨檮浠跺叆鍙f棤娉曢鐭ユā鍧楋紝鍙姹傚叿澶囦换鎰忎竴椤瑰鍏ユ潈闄愩�� */
public AgentPrincipal requireAny(String providedServiceToken) {
verifyServiceToken(providedServiceToken);
@@ -68,8 +88,29 @@
return principalOf(userId);
}
+
+ public AgentPrincipal requireAny(String providedServiceToken, String delegationToken, String taskId,
+ String operation, String moduleId, Long attachmentId) {
+ if (delegationToken == null || delegationToken.isBlank()) {
+ return requireAny(providedServiceToken);
+ }
+ verifyServiceToken(providedServiceToken);
+ AgentBridgeException last = null;
+ for (String permission : IMPORT_PERMISSIONS) {
+ try {
+ return delegationService.require(delegationToken, taskId, operation, permission, moduleId, attachmentId);
+ } catch (AgentBridgeException ex) {
+ last = ex;
+ if (!"DELEGATION_SCOPE_MISMATCH".equals(ex.getErrorCode())
+ && !"DELEGATION_PERMISSION_REVOKED".equals(ex.getErrorCode())) {
+ throw ex;
+ }
+ }
+ }
+ throw last == null ? new AgentBridgeException(HttpStatus.FORBIDDEN, "FORBIDDEN", "濮旀墭绁ㄦ嵁鏉冮檺涓嶈冻") : last;
+ }
+
private AgentPrincipal principalOf(Long userId) {
- Long tenantId = TenantContextHolder.getTenantId();
- return new AgentPrincipal(userId, tenantId == null ? 0L : tenantId, "live");
+ return new AgentPrincipal(userId, "live");
}
}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentDelegationService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentDelegationService.java
new file mode 100644
index 0000000..dd390af
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentDelegationService.java
@@ -0,0 +1,142 @@
+package cn.iocoder.yudao.module.system.framework.agent;
+
+import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.time.LocalDateTime;
+import java.util.Base64;
+import java.util.Set;
+import java.util.UUID;
+
+/** 绛惧彂銆佹牎楠屽拰鎾ら攢鏅鸿兘浣撶煭鏈熷鎵樸�俁edis 鍙繚瀛樺瘑閽ユ憳瑕併�� */
+@Service
+@RequiredArgsConstructor
+public class SystemAgentDelegationService {
+
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ @Value("${yudao.agent.bridge.delegation-ttl-seconds:1800}")
+ private long delegationTtlSeconds;
+
+ private final AgentDelegationStore delegationStore;
+ private final PermissionCommonApi permissionApi;
+
+ public AgentDelegationToken issue(String taskId, String moduleId, Long attachmentId,
+ Set<String> operations, String permission) {
+ return issue(taskId, moduleId, attachmentId == null ? Set.of() : Set.of(attachmentId),
+ operations, permission);
+ }
+
+ public AgentDelegationToken issue(String taskId, String moduleId, Set<Long> attachmentIds,
+ Set<String> operations, String permission) {
+ Long userId = SecurityFrameworkUtils.getLoginUserId();
+ if (userId == null) {
+ throw new AgentBridgeException(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "鏈櫥褰曟垨鐧诲綍宸茶繃鏈�");
+ }
+ if (!permissionApi.hasAnyPermissions(userId, permission)) {
+ throw new AgentBridgeException(HttpStatus.FORBIDDEN, "FORBIDDEN", "褰撳墠鐢ㄦ埛缂哄皯濮旀墭鎵�闇�鏉冮檺");
+ }
+ if (taskId == null || taskId.isBlank() || taskId.length() > 128 || operations == null || operations.isEmpty()
+ || attachmentIds == null || attachmentIds.size() > 20 || attachmentIds.stream().anyMatch(id -> id == null || id <= 0)) {
+ throw new AgentBridgeException(HttpStatus.BAD_REQUEST, "DELEGATION_SCOPE_INVALID", "濮旀墭浠诲姟鎴栨搷浣滆寖鍥存棤鏁�");
+ }
+
+ String id = UUID.randomUUID().toString();
+ byte[] secretBytes = new byte[32];
+ SECURE_RANDOM.nextBytes(secretBytes);
+ String secret = Base64.getUrlEncoder().withoutPadding().encodeToString(secretBytes);
+ String token = id + "." + secret;
+ long ttlSeconds = Math.max(60, Math.min(delegationTtlSeconds, 3600));
+ LocalDateTime now = LocalDateTime.now();
+
+ AgentDelegation delegation = new AgentDelegation();
+ delegation.setId(id);
+ delegation.setSecretHash(sha256(secret));
+ delegation.setTaskId(taskId);
+ delegation.setUserId(userId);
+ delegation.setModuleId(moduleId);
+ delegation.setAttachmentId(attachmentIds.size() == 1 ? attachmentIds.iterator().next() : null);
+ delegation.setAttachmentIds(Set.copyOf(attachmentIds));
+ delegation.setOperations(Set.copyOf(operations));
+ delegation.setPermission(permission);
+ delegation.setIssuedAt(now);
+ delegation.setExpiresAt(now.plusSeconds(ttlSeconds));
+ delegationStore.save(delegation, ttlSeconds);
+ return new AgentDelegationToken(id, token, delegation.getExpiresAt());
+ }
+
+ public AgentPrincipal require(String token, String taskId, String operation, String permission,
+ String moduleId, Long attachmentId) {
+ String[] tokenParts = token == null ? new String[0] : token.split("\\.", 2);
+ if (tokenParts.length != 2) {
+ throw invalidToken();
+ }
+ AgentDelegation delegation = delegationStore.get(tokenParts[0]);
+ if (delegation == null || delegation.getExpiresAt() == null
+ || !delegation.getExpiresAt().isAfter(LocalDateTime.now())
+ || !constantTimeEquals(delegation.getSecretHash(), sha256(tokenParts[1]))) {
+ throw invalidToken();
+ }
+ if (!delegation.getTaskId().equals(taskId) || !delegation.getOperations().contains(operation)
+ || !delegation.getPermission().equals(permission)
+ || (delegation.getModuleId() != null && !delegation.getModuleId().equals(moduleId))
+ || !allowsAttachment(delegation, attachmentId)) {
+ throw new AgentBridgeException(HttpStatus.FORBIDDEN, "DELEGATION_SCOPE_MISMATCH", "濮旀墭绁ㄦ嵁涓嶅厑璁稿綋鍓嶆搷浣�");
+ }
+ if (!permissionApi.hasAnyPermissions(delegation.getUserId(), permission)) {
+ throw new AgentBridgeException(HttpStatus.FORBIDDEN, "DELEGATION_PERMISSION_REVOKED", "濮旀墭鐢ㄦ埛鏉冮檺宸插彉鏇�");
+ }
+ LoginUser loginUser = new LoginUser().setId(delegation.getUserId())
+ .setUserType(UserTypeEnum.ADMIN.getValue())
+ .setExpiresTime(delegation.getExpiresAt());
+ SecurityFrameworkUtils.setLoginUser(loginUser, ServletUtils.getRequest());
+ return new AgentPrincipal(delegation.getUserId(), "delegated:" + delegation.getId());
+ }
+
+ private static boolean allowsAttachment(AgentDelegation delegation, Long attachmentId) {
+ if (delegation.getAttachmentIds() != null && !delegation.getAttachmentIds().isEmpty()) {
+ return attachmentId != null && delegation.getAttachmentIds().contains(attachmentId);
+ }
+ return delegation.getAttachmentId() == null || delegation.getAttachmentId().equals(attachmentId);
+ }
+
+ public void revoke(String delegationId, Long currentUserId) {
+ AgentDelegation delegation = delegationStore.get(delegationId);
+ if (delegation == null) {
+ return;
+ }
+ if (!delegation.getUserId().equals(currentUserId)) {
+ throw new AgentBridgeException(HttpStatus.FORBIDDEN, "DELEGATION_FORBIDDEN", "鏃犳潈鎾ら攢璇ュ鎵樼エ鎹�");
+ }
+ delegationStore.delete(delegationId);
+ }
+
+ private static AgentBridgeException invalidToken() {
+ return new AgentBridgeException(HttpStatus.UNAUTHORIZED, "DELEGATION_INVALID", "濮旀墭绁ㄦ嵁鏃犳晥銆佸凡鎾ら攢鎴栧凡杩囨湡");
+ }
+
+ private static boolean constantTimeEquals(String left, String right) {
+ return left != null && right != null && MessageDigest.isEqual(left.getBytes(StandardCharsets.US_ASCII),
+ right.getBytes(StandardCharsets.US_ASCII));
+ }
+
+ private static String sha256(String value) {
+ try {
+ return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
+ .digest(value.getBytes(StandardCharsets.UTF_8)));
+ } catch (NoSuchAlgorithmException ex) {
+ throw new IllegalStateException("SHA-256 unavailable", ex);
+ }
+ }
+}
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
index 3a924ab..31c73b1 100644
--- 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
@@ -93,19 +93,18 @@
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());
+ ownerUserId, moduleId, request.getCommandId());
if (existing != null) {
requireSameDigest(existing, digest);
return toReceipt(existing);
}
existing = commandMapper.selectByIdempotencyKey(
- tenantId, ownerUserId, moduleId, request.getIdempotencyKey());
+ ownerUserId, moduleId, request.getIdempotencyKey());
if (existing != null) {
requireSameDigest(existing, digest);
return toReceipt(existing);
@@ -129,7 +128,6 @@
.idempotencyKey(request.getIdempotencyKey())
.requestDigest(digest)
.moduleId(moduleId)
- .tenantId(tenantId)
.ownerUserId(ownerUserId)
.sessionId(request.getSessionId())
.attachmentId(request.getAttachmentId())
@@ -145,10 +143,10 @@
commandMapper.insert(command);
} catch (DuplicateKeyException ex) {
AgentImportCommandDO raced = commandMapper.selectByCommandId(
- tenantId, ownerUserId, moduleId, request.getCommandId());
+ ownerUserId, moduleId, request.getCommandId());
if (raced == null) {
raced = commandMapper.selectByIdempotencyKey(
- tenantId, ownerUserId, moduleId, request.getIdempotencyKey());
+ ownerUserId, moduleId, request.getIdempotencyKey());
}
if (raced == null) {
throw ex;
@@ -226,7 +224,7 @@
public AgentImportReceiptRespVO getCommand(String moduleId, String commandId, AgentPrincipal principal) {
requireAdapter(moduleId);
AgentImportCommandDO command = commandMapper.selectByCommandId(
- principal.tenantId(), principal.userId(), moduleId, commandId);
+ principal.userId(), moduleId, commandId);
if (command == null) {
throw new AgentBridgeException(HttpStatus.NOT_FOUND, "COMMAND_NOT_FOUND", "瀵煎叆鍛戒护涓嶅瓨鍦�");
}
@@ -297,9 +295,11 @@
}
private void requirePermission(AgentImportModuleAdapter adapter) {
- if (!securityFrameworkService.hasPermission(adapter.permission())) {
- throw new AgentBridgeException(HttpStatus.FORBIDDEN, "FORBIDDEN",
- "褰撳墠鐢ㄦ埛缂哄皯璇ユā鍧楃殑瀵煎叆鏉冮檺锛�" + adapter.permission() + "锛�");
+ for (String permission : adapter.permissions()) {
+ if (!securityFrameworkService.hasPermission(permission)) {
+ throw new AgentBridgeException(HttpStatus.FORBIDDEN, "FORBIDDEN",
+ "褰撳墠鐢ㄦ埛缂哄皯璇ユā鍧楃殑瀵煎叆鏉冮檺锛�" + permission + "锛�");
+ }
}
}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/storage/SystemAgentImportBridgeService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/storage/SystemAgentImportBridgeService.java
index a1b00ef..3d7629d 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/storage/SystemAgentImportBridgeService.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/storage/SystemAgentImportBridgeService.java
@@ -75,6 +75,23 @@
"闄勪欢缁戝畾澶辫触锛岃閲嶆柊涓婁紶鍚庡啀璇�", true, null));
}
+ /** 鍒犻櫎琛ュ綍闄勪欢鎵�灞炰細璇濓細闄勪欢鍏崇郴銆乥lob 璁板綍涓庣鐩樻枃浠朵竴骞舵竻鐞嗭紝闄勪欢涓嶅瓨鍦ㄦ椂骞傜瓑杩斿洖銆� */
+ @Transactional(rollbackFor = Exception.class)
+ public void deleteImportAttachment(Long attachmentId) {
+ SystemStorageAttachmentDO attachment = attachmentMapper.selectById(attachmentId);
+ if (attachment == null) {
+ return;
+ }
+ if (!RECORD_TYPE.equals(attachment.getRecordType())) {
+ throw new AgentBridgeException(HttpStatus.FORBIDDEN, "ATTACHMENT_FORBIDDEN", "闄勪欢涓嶅彲璁块棶");
+ }
+ Long sessionId = attachment.getRecordId();
+ requireOwnedSession(sessionId);
+ // 鎸変細璇濇暣鎵瑰垹闄わ紝鍚屼竴浼氳瘽鍐呰鏇挎崲鐨勬棫闄勪欢琛屼笌 blob 鏂囦欢閮戒笉浼氭畫鐣�
+ attachmentService.deleteAttachmentsByRecordTypeAndRecordId(RECORD_TYPE, sessionId);
+ sessionMapper.deleteById(sessionId);
+ }
+
public SystemAgentAttachmentRef requireOwnedAttachmentRef(Long attachmentId) {
SystemStorageAttachmentDO attachment = attachmentMapper.selectById(attachmentId);
if (attachment == null) {
diff --git a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentDelegationServiceTest.java b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentDelegationServiceTest.java
new file mode 100644
index 0000000..81a99a1
--- /dev/null
+++ b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/framework/agent/SystemAgentDelegationServiceTest.java
@@ -0,0 +1,127 @@
+package cn.iocoder.yudao.module.system.framework.agent;
+
+import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
+import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class SystemAgentDelegationServiceTest {
+
+ private final Map<String, AgentDelegation> records = new HashMap<>();
+ private boolean permissionGranted = true;
+ private SystemAgentDelegationService service;
+
+ @BeforeEach
+ void setUp() {
+ AgentDelegationStore store = new AgentDelegationStore() {
+ @Override
+ public void save(AgentDelegation delegation, long ttlSeconds) {
+ records.put(delegation.getId(), delegation);
+ }
+
+ @Override
+ public AgentDelegation get(String delegationId) {
+ return records.get(delegationId);
+ }
+
+ @Override
+ public void delete(String delegationId) {
+ records.remove(delegationId);
+ }
+ };
+ PermissionCommonApi permissions = new PermissionCommonApi() {
+ @Override
+ public boolean hasAnyPermissions(Long userId, String... permission) {
+ return permissionGranted;
+ }
+
+ @Override
+ public boolean hasAnyRoles(Long userId, String... roles) {
+ return false;
+ }
+
+ @Override
+ public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) {
+ return null;
+ }
+ };
+ service = new SystemAgentDelegationService(store, permissions);
+ ReflectionTestUtils.setField(service, "delegationTtlSeconds", 1800L);
+ login(7L);
+ }
+
+ @AfterEach
+ void clearContext() {
+ SecurityContextHolder.clearContext();
+ RequestContextHolder.resetRequestAttributes();
+ }
+
+ @Test
+ void issuedTokenStoresOnlySecretHash() {
+ AgentDelegationToken result = issue("task-1");
+ AgentDelegation stored = records.get(result.delegationId());
+ assertNotNull(stored);
+ assertFalse(stored.getSecretHash().contains(result.token().substring(result.token().indexOf('.') + 1)));
+ assertEquals("task-1", stored.getTaskId());
+ assertNotNull(result.expiresAt());
+ }
+
+ @Test
+ void rejectsDifferentTaskAndTamperedSecret() {
+ AgentDelegationToken result = issue("task-1");
+ AgentBridgeException mismatch = assertThrows(AgentBridgeException.class,
+ () -> service.require(result.token(), "task-2", "attachment.read",
+ "mdm:item:import", "mdm.item", 21L));
+ assertEquals("DELEGATION_SCOPE_MISMATCH", mismatch.getErrorCode());
+
+ String tampered = result.token().substring(0, result.token().indexOf('.') + 1) + "tampered";
+ AgentBridgeException invalid = assertThrows(AgentBridgeException.class,
+ () -> service.require(tampered, "task-1", "attachment.read",
+ "mdm:item:import", "mdm.item", 21L));
+ assertEquals("DELEGATION_INVALID", invalid.getErrorCode());
+ }
+
+ @Test
+ void permissionRevocationInvalidatesExistingToken() {
+ AgentDelegationToken result = issue("task-1");
+ permissionGranted = false;
+ AgentBridgeException revoked = assertThrows(AgentBridgeException.class,
+ () -> service.require(result.token(), "task-1", "attachment.read",
+ "mdm:item:import", "mdm.item", 21L));
+ assertEquals("DELEGATION_PERMISSION_REVOKED", revoked.getErrorCode());
+ }
+
+ @Test
+ void ownerCanRevokeToken() {
+ AgentDelegationToken result = issue("task-1");
+ service.revoke(result.delegationId(), 7L);
+ assertFalse(records.containsKey(result.delegationId()));
+ }
+
+ private AgentDelegationToken issue(String taskId) {
+ return service.issue(taskId, "mdm.item", 21L, Set.of("attachment.read"), "mdm:item:import");
+ }
+
+ private static void login(Long userId) {
+ LoginUser loginUser = new LoginUser().setId(userId).setUserType(UserTypeEnum.ADMIN.getValue());
+ SecurityContextHolder.getContext().setAuthentication(
+ new UsernamePasswordAuthenticationToken(loginUser, null, List.of()));
+ RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest()));
+ }
+}
diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml
index ea3a9e5..908ee89 100644
--- a/yudao-server/src/main/resources/application-local.yaml
+++ b/yudao-server/src/main/resources/application-local.yaml
@@ -190,10 +190,12 @@
security:
mock-enable: true
agent:
+ bridge-service-token: ${MOM_BRIDGE_SERVICE_TOKEN:local-mom-agent-bridge-2026}
bridge:
- write-enabled: false # 鏅鸿兘浣撶墿鏂欏鍏ュ啓寮�鍏筹紱浠呰仈璋冮獙鏀舵椂涓存椂鎵撳紑
+ write-enabled: true # 鏈満鑱旇皟锛氬厑璁告櫤鑳戒綋鎻愪氦涓氬姟瀵煎叆
allow-mes-sync: false
max-rows: 5000
+ delegation-ttl-seconds: 1800 # Worker 鐭湡濮旀墭鏈�闀� 30 鍒嗛挓锛屼笉淇濆瓨鐢ㄦ埛鐧诲綍 Token
access-log: # 璁块棶鏃ュ織鐨勯厤缃」
enable: false
demo: false # 鍏抽棴婕旂ず妯″紡
--
Gitblit v1.9.3