feat(agent): audit orchestrated runs
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
package org.dromara.aihr.agent;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.AgentPlan;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.AgentResponse;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AihrAgentAuditService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public void record(AihrKnowledgePrincipal principal, AgentPlan plan, AgentResponse response,
|
||||
long durationMs, String errorCode) {
|
||||
if (principal == null || plan == null || response == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String sourceType = response.sourceSummary().isEmpty()
|
||||
? null
|
||||
: clean(response.sourceSummary().get(0).type(), 32);
|
||||
String resultRef = response.actionDraft() == null
|
||||
? null
|
||||
: clean("DRAFT:" + response.actionDraft().type(), 100);
|
||||
jdbcTemplate.update("""
|
||||
insert into aihr_agent_run
|
||||
(run_id, tenant_id, client_key, user_id, conversation_id, context_version,
|
||||
intent, tool, status, source_type, duration_ms, error_code, result_ref, create_time)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now())
|
||||
""",
|
||||
clean(response.runId(), 64), clean(principal.tenantId(), 20),
|
||||
clean(principal.clientKey(), 64), principal.userId(),
|
||||
clean(response.conversationId(), 64), response.contextVersion(),
|
||||
plan.intent().name(), plan.tool().name(), response.status().name(), sourceType,
|
||||
Math.max(0, durationMs), safeCode(errorCode), resultRef
|
||||
);
|
||||
} catch (RuntimeException ignored) {
|
||||
log.warn("agent audit write failed (details hidden)");
|
||||
}
|
||||
}
|
||||
|
||||
private static String safeCode(String value) {
|
||||
String code = clean(value, 32);
|
||||
return code != null && code.matches("[A-Za-z0-9_-]+") ? code : null;
|
||||
}
|
||||
|
||||
private static String clean(String value, int maxLength) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String cleaned = value.trim();
|
||||
return cleaned.length() <= maxLength ? cleaned : cleaned.substring(0, maxLength);
|
||||
}
|
||||
}
|
||||
+55
-9
@@ -9,6 +9,7 @@ import org.dromara.aihr.agent.AihrAgentDto.ActionDraft;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.Clarification;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.Intent;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.SourceSummary;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.QueryRequest;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgeQueryDto.QueryResponse;
|
||||
import org.dromara.aihr.knowledge.service.AihrKnowledgeDataToolService.CurrentTaskSummary;
|
||||
@@ -35,11 +36,51 @@ public class AihrAgentOrchestrator {
|
||||
private final AihrKnowledgeQueryService queryService;
|
||||
private final AihrAgentActionService actionService;
|
||||
private final AihrWebAiService webAiService;
|
||||
private final AihrAgentAuditService auditService;
|
||||
|
||||
public AgentResponse handle(AgentRequest request) {
|
||||
return execute(request, null, false);
|
||||
}
|
||||
|
||||
public AgentResponse handleMedia(AgentRequest request, MultipartFile file) {
|
||||
return execute(request, file, true);
|
||||
}
|
||||
|
||||
private AgentResponse execute(AgentRequest request, MultipartFile file, boolean media) {
|
||||
long started = System.nanoTime();
|
||||
AgentPlan plan = null;
|
||||
AihrKnowledgePrincipal principal = null;
|
||||
AgentResponse response = null;
|
||||
String errorCode = null;
|
||||
try {
|
||||
requireQuestion(request);
|
||||
AgentPlan plan = planner.plan(request.question(), false);
|
||||
var principal = principalResolver.current();
|
||||
if (media && (file == null || file.isEmpty())) {
|
||||
throw new ServiceException("附件不能为空", 400);
|
||||
}
|
||||
plan = planner.plan(request.question(), media);
|
||||
principal = principalResolver.current();
|
||||
response = media
|
||||
? executeMedia(plan, request, file, principal)
|
||||
: executeText(plan, request, principal);
|
||||
return response;
|
||||
} catch (ServiceException ex) {
|
||||
errorCode = ex.getCode() == null ? "REJECTED" : "HTTP_" + ex.getCode();
|
||||
throw ex;
|
||||
} catch (RuntimeException ex) {
|
||||
errorCode = "INTERNAL";
|
||||
throw ex;
|
||||
} finally {
|
||||
if (auditService != null && plan != null && principal != null) {
|
||||
AgentResponse audited = response == null
|
||||
? failed(plan, request, errorCode)
|
||||
: response;
|
||||
auditService.record(principal, plan, audited,
|
||||
(System.nanoTime() - started) / 1_000_000L, errorCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AgentResponse executeText(AgentPlan plan, AgentRequest request, AihrKnowledgePrincipal principal) {
|
||||
if (plan.requiresExternalConsent() && !request.externalConsent()) {
|
||||
return simple(plan, AgentStatus.NEEDS_INPUT,
|
||||
"全网查询会将脱敏后的问题发送给外部公开检索服务。",
|
||||
@@ -58,13 +99,9 @@ public class AihrAgentOrchestrator {
|
||||
};
|
||||
}
|
||||
|
||||
public AgentResponse handleMedia(AgentRequest request, MultipartFile file) {
|
||||
requireQuestion(request);
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("附件不能为空", 400);
|
||||
}
|
||||
AgentPlan plan = planner.plan(request.question(), true);
|
||||
policy.authorize(principalResolver.current(), plan, request.externalConsent());
|
||||
private AgentResponse executeMedia(AgentPlan plan, AgentRequest request, MultipartFile file,
|
||||
AihrKnowledgePrincipal principal) {
|
||||
policy.authorize(principal, plan, request.externalConsent());
|
||||
MediaMode mode = needsKnowledge(request.question())
|
||||
? MediaMode.MEDIA_WITH_KNOWLEDGE
|
||||
: MediaMode.MEDIA_ONLY;
|
||||
@@ -157,6 +194,15 @@ public class AihrAgentOrchestrator {
|
||||
);
|
||||
}
|
||||
|
||||
private static AgentResponse failed(AgentPlan plan, AgentRequest request, String errorCode) {
|
||||
AgentStatus status = "HTTP_403".equals(errorCode) ? AgentStatus.FORBIDDEN : AgentStatus.FAILED;
|
||||
return new AgentResponse(
|
||||
runId(), request == null ? null : request.conversationId(),
|
||||
request == null ? null : request.contextVersion(), plan.intent(), status, "",
|
||||
List.of(), List.of(), List.of(), null, null, null, List.of()
|
||||
);
|
||||
}
|
||||
|
||||
private static String runId() {
|
||||
return "agent_run_" + UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package org.dromara.aihr.agent;
|
||||
|
||||
import org.dromara.aihr.agent.AihrAgentDto.AgentPlan;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.AgentResponse;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.AgentStatus;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.Intent;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.ResponseStyle;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.SourceSummary;
|
||||
import org.dromara.aihr.agent.AihrAgentDto.Tool;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@Tag("dev")
|
||||
class AihrAgentAuditServiceTest {
|
||||
|
||||
@Test
|
||||
void auditStoresOnlyMinimalMetadataWithoutQuestionOrAnswer() {
|
||||
var jdbc = new CapturingJdbcTemplate(false);
|
||||
var service = new AihrAgentAuditService(jdbc);
|
||||
|
||||
service.record(principal(), plan(), response(), 23L, null);
|
||||
|
||||
String stored = jdbc.sql + Arrays.toString(jdbc.args);
|
||||
assertTrue(stored.contains("agent_run_test"));
|
||||
assertTrue(stored.contains("MY_CURRENT_TASKS"));
|
||||
assertFalse(stored.contains("敏感问题"));
|
||||
assertFalse(stored.contains("敏感答案"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void auditFailureNeverBreaksAValidResponse() {
|
||||
var service = new AihrAgentAuditService(new CapturingJdbcTemplate(true));
|
||||
|
||||
assertDoesNotThrow(() -> service.record(principal(), plan(), response(), 23L, "INTERNAL"));
|
||||
}
|
||||
|
||||
private static AihrKnowledgePrincipal principal() {
|
||||
return new AihrKnowledgePrincipal("000000", 1L, "app_user", "employee-1",
|
||||
Set.of("employee"), Set.of("P1"), "mobile");
|
||||
}
|
||||
|
||||
private static AgentPlan plan() {
|
||||
return new AgentPlan(Intent.LIVE_MY_WORK, "敏感问题", Tool.MY_CURRENT_TASKS,
|
||||
false, false, ResponseStyle.FACT);
|
||||
}
|
||||
|
||||
private static AgentResponse response() {
|
||||
return new AgentResponse("agent_run_test", "conversation_1", 1L, Intent.LIVE_MY_WORK,
|
||||
AgentStatus.COMPLETED, "敏感答案",
|
||||
List.of(new SourceSummary("LIVE_DATA", "我的待办", "2026-07-24T20:00:00+08:00")),
|
||||
List.of(), List.of(), null, null, null, List.of());
|
||||
}
|
||||
|
||||
private static final class CapturingJdbcTemplate extends JdbcTemplate {
|
||||
private final boolean fail;
|
||||
private String sql;
|
||||
private Object[] args;
|
||||
|
||||
private CapturingJdbcTemplate(boolean fail) {
|
||||
this.fail = fail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(String sql, Object... args) {
|
||||
if (fail) {
|
||||
throw new IllegalStateException("database unavailable");
|
||||
}
|
||||
this.sql = sql;
|
||||
this.args = args;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -55,6 +55,7 @@ class AihrAgentMediaTest {
|
||||
},
|
||||
query,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
+1
@@ -111,6 +111,7 @@ class AihrAgentOrchestratorTest {
|
||||
},
|
||||
query,
|
||||
actionService,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
+2
-1
@@ -72,7 +72,8 @@ class AihrAgentWebResearchTest {
|
||||
}
|
||||
},
|
||||
null,
|
||||
web
|
||||
web,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -313,3 +313,25 @@ CREATE TABLE IF NOT EXISTS `aihr_knowledge_admin_audit` (
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_aihr_admin_audit_tenant` (`tenant_id`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='知识平台管理变更审计';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_agent_run` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`run_id` varchar(64) NOT NULL COMMENT 'Agent 运行号',
|
||||
`tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '租户编号',
|
||||
`client_key` varchar(64) DEFAULT NULL COMMENT '调用客户端',
|
||||
`user_id` bigint NOT NULL COMMENT '当前登录用户',
|
||||
`conversation_id` varchar(64) DEFAULT NULL COMMENT '短会话ID',
|
||||
`context_version` bigint DEFAULT NULL COMMENT '短会话版本',
|
||||
`intent` varchar(40) NOT NULL COMMENT '受校验意图',
|
||||
`tool` varchar(50) NOT NULL COMMENT '登记工具',
|
||||
`status` varchar(30) NOT NULL COMMENT '最终状态',
|
||||
`source_type` varchar(32) DEFAULT NULL COMMENT '主要事实来源',
|
||||
`duration_ms` bigint NOT NULL DEFAULT 0 COMMENT '处理耗时毫秒',
|
||||
`error_code` varchar(32) DEFAULT NULL COMMENT '脱敏错误码',
|
||||
`result_ref` varchar(100) DEFAULT NULL COMMENT '领域结果类型引用',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_aihr_agent_run` (`run_id`),
|
||||
KEY `idx_aihr_agent_run_user` (`tenant_id`, `user_id`, `create_time`),
|
||||
KEY `idx_aihr_agent_run_status` (`tenant_id`, `status`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='数字师傅 Agent 最小运行审计';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
-- 数字师傅 Agent 最小运行审计。只保存路由和状态元数据,不保存问题、答案或模型推理。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_agent_run` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`run_id` varchar(64) NOT NULL COMMENT 'Agent 运行号',
|
||||
`tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '租户编号',
|
||||
`client_key` varchar(64) DEFAULT NULL COMMENT '调用客户端',
|
||||
`user_id` bigint NOT NULL COMMENT '当前登录用户',
|
||||
`conversation_id` varchar(64) DEFAULT NULL COMMENT '短会话ID',
|
||||
`context_version` bigint DEFAULT NULL COMMENT '短会话版本',
|
||||
`intent` varchar(40) NOT NULL COMMENT '受校验意图',
|
||||
`tool` varchar(50) NOT NULL COMMENT '登记工具',
|
||||
`status` varchar(30) NOT NULL COMMENT '最终状态',
|
||||
`source_type` varchar(32) DEFAULT NULL COMMENT '主要事实来源',
|
||||
`duration_ms` bigint NOT NULL DEFAULT 0 COMMENT '处理耗时毫秒',
|
||||
`error_code` varchar(32) DEFAULT NULL COMMENT '脱敏错误码',
|
||||
`result_ref` varchar(100) DEFAULT NULL COMMENT '领域结果类型引用',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_aihr_agent_run` (`run_id`),
|
||||
KEY `idx_aihr_agent_run_user` (`tenant_id`, `user_id`, `create_time`),
|
||||
KEY `idx_aihr_agent_run_status` (`tenant_id`, `status`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='数字师傅 Agent 最小运行审计';
|
||||
@@ -83,10 +83,11 @@ mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aih
|
||||
mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260722_broadcast_mysql8.sql
|
||||
mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260723_broadcast_targeting_mysql8.sql
|
||||
mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260722_broadcast_question_context_mysql8.sql
|
||||
mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260724_agent_orchestration_mysql8.sql
|
||||
mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/aihr_personal_knowledge_mysql8.sql
|
||||
```
|
||||
|
||||
顺序原因:场景补充依赖三张场景/Rubric 基础表;五维迁移依赖场景和 Rubric;五岗位题库之后的内容运营迁移为每个场景补齐唯一的初始 Rubric、五个稳定评分维度与训练时评分规则快照列,并只修正仍处于待训练状态的旧每日题场景编号,不重写已完成记录。岗位/SOP/任务/资格迁移只补正式数据契约,不写业务行,也不代表岗位适用范围、任务规则或资格标准已经获得 HR 确认。住宅 SOP 和 Prompt 迁移只补内置内容,不代表内容已完成业务复核。知识会话迁移只新增短期上下文表,不生成业务对话。排序规则兼容迁移先对齐本批发布表,全量排序规则迁移再统一历史 `aihr_*` 表;两者都读取目标库 `aihr_knowledge_info.tenant_id` 的实际排序规则。服务记忆迁移创建候选、统一采集、兼容项目记录和版本留痕四张表;工作上报幂等迁移随后只补原表列与唯一索引;知识分类迁移新增空间内分类表及附件成员的可空 `category_id`,不迁移、删除或扩大任何空间授权与检索范围;银城大喇叭 M0 迁移创建消息、阅读和不可变 v1 快照三表,为已存在消息补一次快照,并补发布请求键/载荷哈希、撤回原因及 `(tenant_id,published_by,publish_request_key)` 唯一约束。紧接着 M1 定向迁移补充必读与目标载荷字段,以及规则快照、命中对象快照两表;它只决定提醒/必读对象,不改变全租户公开频道的可见性。随后,消息追问迁移只给短期会话增加可空 `broadcast_message_id`;它不保存消息正文,并用于约束同一会话不能切换公司消息。历史普通会话保持 `NULL`。个人资料迁移最后创建独立 `aihr_personal_*` 表,并从当前租户既有 MinIO 配置派生私有 `personal-minio` 配置;`ruoyi-personal` bucket 必须由受控运维流程创建和验证,不能在文档或日志中输出访问密钥。它们同样对齐目标排序规则,兼容历史生产库与全新 MySQL 8 数据库。
|
||||
顺序原因:场景补充依赖三张场景/Rubric 基础表;五维迁移依赖场景和 Rubric;五岗位题库之后的内容运营迁移为每个场景补齐唯一的初始 Rubric、五个稳定评分维度与训练时评分规则快照列,并只修正仍处于待训练状态的旧每日题场景编号,不重写已完成记录。岗位/SOP/任务/资格迁移只补正式数据契约,不写业务行,也不代表岗位适用范围、任务规则或资格标准已经获得 HR 确认。住宅 SOP 和 Prompt 迁移只补内置内容,不代表内容已完成业务复核。知识会话迁移只新增短期上下文表,不生成业务对话。排序规则兼容迁移先对齐本批发布表,全量排序规则迁移再统一历史 `aihr_*` 表;两者都读取目标库 `aihr_knowledge_info.tenant_id` 的实际排序规则。服务记忆迁移创建候选、统一采集、兼容项目记录和版本留痕四张表;工作上报幂等迁移随后只补原表列与唯一索引;知识分类迁移新增空间内分类表及附件成员的可空 `category_id`,不迁移、删除或扩大任何空间授权与检索范围;银城大喇叭 M0 迁移创建消息、阅读和不可变 v1 快照三表,为已存在消息补一次快照,并补发布请求键/载荷哈希、撤回原因及 `(tenant_id,published_by,publish_request_key)` 唯一约束。紧接着 M1 定向迁移补充必读与目标载荷字段,以及规则快照、命中对象快照两表;它只决定提醒/必读对象,不改变全租户公开频道的可见性。随后,消息追问迁移只给短期会话增加可空 `broadcast_message_id`;它不保存消息正文,并用于约束同一会话不能切换公司消息。历史普通会话保持 `NULL`。Agent 迁移只新增最小运行审计表,不保存问题、答案、密钥或模型推理。个人资料迁移最后创建独立 `aihr_personal_*` 表,并从当前租户既有 MinIO 配置派生私有 `personal-minio` 配置;`ruoyi-personal` bucket 必须由受控运维流程创建和验证,不能在文档或日志中输出访问密钥。它们同样对齐目标排序规则,兼容历史生产库与全新 MySQL 8 数据库。
|
||||
|
||||
## 迁移后只读核验
|
||||
|
||||
|
||||
@@ -135,6 +135,7 @@ FROM (
|
||||
UNION ALL SELECT 'aihr_knowledge_query_log'
|
||||
UNION ALL SELECT 'aihr_knowledge_conversation'
|
||||
UNION ALL SELECT 'aihr_knowledge_admin_audit'
|
||||
UNION ALL SELECT 'aihr_agent_run'
|
||||
UNION ALL SELECT 'aihr_learning_question'
|
||||
UNION ALL SELECT 'aihr_practice_question_feedback'
|
||||
UNION ALL SELECT 'aihr_onboard_exam'
|
||||
@@ -229,6 +230,29 @@ REMOTE
|
||||
|
||||
[[ -z "$missing_work_report_columns" ]] \
|
||||
|| fail "remote work-report schema missing required columns: $(printf '%s' "$missing_work_report_columns" | tr '\n' ', ' | sed 's/, $//')"
|
||||
local agent_schema_contract
|
||||
agent_schema_contract="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_db" <<'REMOTE'
|
||||
set -euo pipefail
|
||||
db="$1"
|
||||
mysql --batch --skip-column-names --connect-timeout=5 "$db" <<'SQL'
|
||||
SELECT CONCAT(
|
||||
(SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'aihr_agent_run'), '|',
|
||||
(SELECT GROUP_CONCAT(column_name ORDER BY seq_in_index)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'aihr_agent_run'
|
||||
AND index_name = 'uk_aihr_agent_run'), '|',
|
||||
(SELECT GROUP_CONCAT(column_name ORDER BY seq_in_index)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'aihr_agent_run'
|
||||
AND index_name = 'idx_aihr_agent_run_user')
|
||||
);
|
||||
SQL
|
||||
REMOTE
|
||||
)" || fail "remote Agent audit schema check failed: $remote_ssh:$remote_db"
|
||||
|
||||
[[ "$agent_schema_contract" = "15|run_id|tenant_id,user_id,create_time" ]] \
|
||||
|| fail "remote Agent audit schema is invalid: got ${agent_schema_contract:-missing}"
|
||||
local missing_knowledge_category_columns
|
||||
missing_knowledge_category_columns="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_db" <<'REMOTE'
|
||||
set -euo pipefail
|
||||
|
||||
@@ -34,6 +34,7 @@ docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry
|
||||
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/update/aihr_20260722_broadcast_mysql8.sql"
|
||||
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/update/aihr_20260723_broadcast_targeting_mysql8.sql"
|
||||
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/update/aihr_20260722_broadcast_question_context_mysql8.sql"
|
||||
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/update/aihr_20260724_agent_orchestration_mysql8.sql"
|
||||
|
||||
docker exec wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue \
|
||||
-e "select count(*) as tables_count from information_schema.tables where table_schema = 'ry-vue';"
|
||||
|
||||
@@ -383,4 +383,15 @@ fi
|
||||
broadcast_failed_publish_index="$(mysql "$DB" -N -B -e "SELECT CONCAT(MIN(non_unique),'|',GROUP_CONCAT(column_name ORDER BY seq_in_index),'|',SUM(sub_part IS NOT NULL)) FROM information_schema.statistics WHERE table_schema='$DB' AND table_name='aihr_broadcast_message' AND index_name='uk_aihr_broadcast_message_publish_request';")"
|
||||
test "$broadcast_failed_publish_index" = '1|tenant_id,publish_request_key,published_by|1'
|
||||
|
||||
echo "PASS: AIHR legacy schema migrations are idempotent, including knowledge-space categories, practice evidence, knowledge feedback, candidate interview review, position/SOP qualification contracts, assistant capture, work-report idempotency, broadcast publish/withdraw audit with M1 targeting defaults and target-table contracts, v1 snapshots, and trusted broadcast question context"
|
||||
agent_migration="$ROOT_DIR/backend/script/sql/update/aihr_20260724_agent_orchestration_mysql8.sql"
|
||||
test -f "$agent_migration"
|
||||
mysql "$DB" < "$agent_migration"
|
||||
mysql "$DB" < "$agent_migration"
|
||||
agent_columns="$(mysql "$DB" -N -B -e "SELECT GROUP_CONCAT(column_name ORDER BY ordinal_position) FROM information_schema.columns WHERE table_schema='$DB' AND table_name='aihr_agent_run';")"
|
||||
test "$agent_columns" = 'id,run_id,tenant_id,client_key,user_id,conversation_id,context_version,intent,tool,status,source_type,duration_ms,error_code,result_ref,create_time'
|
||||
agent_run_unique="$(mysql "$DB" -N -B -e "SELECT CONCAT(MIN(non_unique),'|',GROUP_CONCAT(column_name ORDER BY seq_in_index)) FROM information_schema.statistics WHERE table_schema='$DB' AND table_name='aihr_agent_run' AND index_name='uk_aihr_agent_run';")"
|
||||
test "$agent_run_unique" = '0|run_id'
|
||||
agent_user_index="$(mysql "$DB" -N -B -e "SELECT GROUP_CONCAT(column_name ORDER BY seq_in_index) FROM information_schema.statistics WHERE table_schema='$DB' AND table_name='aihr_agent_run' AND index_name='idx_aihr_agent_run_user';")"
|
||||
test "$agent_user_index" = 'tenant_id,user_id,create_time'
|
||||
|
||||
echo "PASS: AIHR legacy schema migrations are idempotent, including Agent run audit, knowledge-space categories, practice evidence, knowledge feedback, candidate interview review, position/SOP qualification contracts, assistant capture, work-report idempotency, broadcast publish/withdraw audit with M1 targeting defaults and target-table contracts, v1 snapshots, and trusted broadcast question context"
|
||||
|
||||
Reference in New Issue
Block a user