feat: 新增小红书模块后端(Agent/任务/发布/分析/技能/AI)及 SQL 种子与 RBAC GUID 修复脚本
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
42b41e68c7
commit
af53c48f7f
181
db.sql
181
db.sql
@ -1028,4 +1028,185 @@ CREATE TABLE `t_warehouse_address` (
|
||||
KEY `idx_wa_type` (`WarehouseType`)
|
||||
) COMMENT='仓库地址表';
|
||||
|
||||
-- 小红书(LIKE AI → OMS)模块,详见 sql/migrate_t_xhs.sql 与设计文档 docs/小红书模块设计-063001.md
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_agent` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID → t_customer.Id',
|
||||
`DeviceId` VARCHAR(64) NOT NULL COMMENT 'Agent 本机设备标识',
|
||||
`DeviceName` VARCHAR(128) NULL COMMENT '用户自定义名称',
|
||||
`AgentVersion` VARCHAR(32) NULL COMMENT 'Agent 版本号',
|
||||
`BridgePort` INT NOT NULL DEFAULT 9333 COMMENT 'Bridge WebSocket 端口',
|
||||
`Status` VARCHAR(16) NOT NULL DEFAULT 'offline' COMMENT 'online/offline',
|
||||
`EngineRunning` TINYINT NOT NULL DEFAULT 0 COMMENT 'Bridge 引擎是否运行',
|
||||
`ExtensionConnected` TINYINT NOT NULL DEFAULT 0 COMMENT 'Chrome 扩展是否连上 Bridge',
|
||||
`XhsLoggedIn` TINYINT NOT NULL DEFAULT 0 COMMENT '小红书是否已登录',
|
||||
`LastHeartbeatTime` DATETIME NULL COMMENT '最后心跳时间',
|
||||
`LastError` VARCHAR(512) NULL COMMENT '最近一次 Agent 上报错误摘要',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_agent_device` (`DeviceId`),
|
||||
KEY `idx_xhs_agent_customer` (`CustomerId`, `Status`)
|
||||
) COMMENT='小红书本地 Agent 设备表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_task` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`AgentId` INT NULL COMMENT '指定执行的 Agent;NULL 表示任意在线 Agent',
|
||||
`TaskType` VARCHAR(32) NOT NULL COMMENT '任务类型',
|
||||
`ToolName` VARCHAR(64) NULL COMMENT 'LIKE AI 工具名',
|
||||
`Priority` INT NOT NULL DEFAULT 0 COMMENT '越大越优先',
|
||||
`Status` VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/claimed/running/succeeded/failed/cancelled',
|
||||
`ArgumentsJson` JSON NOT NULL COMMENT '工具参数 JSON',
|
||||
`ProgressText` VARCHAR(512) NULL COMMENT '执行过程摘要',
|
||||
`ResultJson` JSON NULL COMMENT '成功时结构化结果',
|
||||
`ErrorMessage` VARCHAR(1024) NULL COMMENT '失败原因',
|
||||
`Source` VARCHAR(32) NOT NULL DEFAULT 'web' COMMENT 'web/schedule/skill',
|
||||
`SourceRefId` VARCHAR(64) NULL COMMENT '来源关联 ID',
|
||||
`ScheduledTime` DATETIME NULL COMMENT '计划执行时间',
|
||||
`ClaimedAt` DATETIME NULL COMMENT 'Agent 认领时间',
|
||||
`StartedAt` DATETIME NULL COMMENT '开始执行',
|
||||
`FinishedAt` DATETIME NULL COMMENT '结束时间',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_xhs_task_poll` (`CustomerId`, `Status`, `ScheduledTime`, `Priority`),
|
||||
KEY `idx_xhs_task_agent` (`AgentId`, `Status`),
|
||||
KEY `idx_xhs_task_source` (`Source`, `SourceRefId`)
|
||||
) COMMENT='小红书 Agent 任务队列表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_publish_record` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`TaskId` INT NULL COMMENT '关联 t_xhs_task.Id',
|
||||
`ToolName` VARCHAR(64) NOT NULL COMMENT 'publish_content / publish_with_video',
|
||||
`Title` VARCHAR(256) NOT NULL COMMENT '笔记标题',
|
||||
`ContentPreview` VARCHAR(512) NULL COMMENT '正文摘要',
|
||||
`ContentFull` TEXT NULL COMMENT '完整正文',
|
||||
`MediaJson` JSON NULL COMMENT '图片/视频 URL 列表',
|
||||
`TagsJson` JSON NULL COMMENT '标签数组',
|
||||
`Visibility` VARCHAR(32) NULL COMMENT '可见范围',
|
||||
`Status` VARCHAR(16) NOT NULL DEFAULT 'draft' COMMENT 'draft/scheduled/publishing/succeeded/failed/cancelled',
|
||||
`Success` TINYINT NULL COMMENT '最终是否成功',
|
||||
`ResultSummary` VARCHAR(1024) NULL COMMENT 'Agent 回传结果摘要',
|
||||
`ScheduledTime` DATETIME NULL COMMENT '定时发布时间',
|
||||
`PublishedTime` DATETIME NULL COMMENT '实际发布完成时间',
|
||||
`SkillId` INT NULL COMMENT '若来自技能卡片',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_xhs_publish_customer_time` (`CustomerId`, `CreateTime`),
|
||||
KEY `idx_xhs_publish_status_sched` (`CustomerId`, `Status`, `ScheduledTime`),
|
||||
KEY `idx_xhs_publish_task` (`TaskId`)
|
||||
) COMMENT='小红书发布记录表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_analysis_session` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`SessionName` VARCHAR(128) NULL COMMENT '用户备注名',
|
||||
`AnalysisType` VARCHAR(32) NOT NULL COMMENT 'home_feed/search/my_feeds/feed_detail',
|
||||
`Keyword` VARCHAR(128) NULL COMMENT '搜索关键词',
|
||||
`TaskId` INT NULL COMMENT '采集任务 ID',
|
||||
`FeedsJson` JSON NULL COMMENT '采集笔记列表',
|
||||
`SelectedFeedId` VARCHAR(64) NULL COMMENT '当前选中笔记 ID',
|
||||
`AiAnalysisMarkdown` MEDIUMTEXT NULL COMMENT 'AI 分析结果',
|
||||
`SkillId` INT NULL COMMENT '技能配方 ID',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_xhs_analysis_customer` (`CustomerId`, `CreateTime`)
|
||||
) COMMENT='小红书内容分析会话表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_skill` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID;0 表示系统预置',
|
||||
`ExternalId` VARCHAR(32) NOT NULL COMMENT '对外稳定 ID',
|
||||
`Title` VARCHAR(256) NOT NULL COMMENT '技能名称',
|
||||
`SkillType` VARCHAR(32) NOT NULL DEFAULT 'ai_create' COMMENT '技能类型',
|
||||
`PresetId` VARCHAR(64) NULL COMMENT '内置预设标识',
|
||||
`RecipeJson` JSON NOT NULL COMMENT '配方 JSON',
|
||||
`SortOrder` INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_skill_external` (`CustomerId`, `ExternalId`),
|
||||
KEY `idx_xhs_skill_customer` (`CustomerId`, `SortOrder`)
|
||||
) COMMENT='小红书技能卡片表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_human_config` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`BehaviorJson` JSON NOT NULL COMMENT 'human_behavior 配置',
|
||||
`GuardJson` JSON NOT NULL COMMENT 'human_guard 配置',
|
||||
`Version` INT NOT NULL DEFAULT 1 COMMENT '配置版本',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_human_customer` (`CustomerId`)
|
||||
) COMMENT='小红书拟人行为与防封配置表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_ai_provider` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID;0 表示全局',
|
||||
`ProviderKey` VARCHAR(32) NOT NULL COMMENT 'ai_chatgpt / ai_zhipu 等',
|
||||
`ProviderType` VARCHAR(32) NOT NULL COMMENT 'chatgpt/doubao/deepseek/zhipu',
|
||||
`DisplayName` VARCHAR(64) NOT NULL COMMENT '展示名',
|
||||
`BaseUrl` VARCHAR(512) NOT NULL COMMENT 'API Base URL',
|
||||
`ApiKeyEnc` VARCHAR(512) NULL COMMENT 'API Key(暂明文,后续可加密)',
|
||||
`ApiSecretEnc` VARCHAR(512) NULL COMMENT 'Secret/模型名',
|
||||
`IsDefault` TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认',
|
||||
`Enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_ai_provider` (`CustomerId`, `ProviderKey`),
|
||||
KEY `idx_xhs_ai_customer` (`CustomerId`, `Enabled`)
|
||||
) COMMENT='小红书 AI 提供商配置表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_media` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`FileName` VARCHAR(256) NOT NULL COMMENT '原始文件名',
|
||||
`StoragePath` VARCHAR(512) NOT NULL COMMENT '相对路径',
|
||||
`PublicUrl` VARCHAR(512) NOT NULL COMMENT '对外 HTTPS URL',
|
||||
`MimeType` VARCHAR(64) NULL COMMENT 'MIME',
|
||||
`FileSizeBytes` BIGINT NULL COMMENT '字节数',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_xhs_media_customer` (`CustomerId`, `CreateTime`)
|
||||
) COMMENT='小红书创作上传媒体表';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
45
sql/diagnose_rbac_bad_guids.sql
Normal file
45
sql/diagnose_rbac_bad_guids.sql
Normal file
@ -0,0 +1,45 @@
|
||||
-- RBAC 库:诊断所有 CHAR(36) / GUID 字段中的非法值(如 "0")
|
||||
-- 症状:登录 / get-menu-tree → Could not parse CHAR(36) value as Guid: 0
|
||||
--
|
||||
-- ⚠️ 必须在 API 实际连接的 RBAC 库执行
|
||||
-- 本地前端 dev(localhost:5173)默认代理到 http://api.nanafa.com/admin → 修本机库无效
|
||||
|
||||
SET @GUID_RE = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$';
|
||||
|
||||
SELECT 'menu bad rows' AS section, m.`Id`, m.`Name`, m.`Route`, m.`PId`, m.`CreateUserId`, m.`UpdateUserId`, m.`ConcurrencyStamp`
|
||||
FROM `menu` m
|
||||
WHERE (m.`PId` IS NOT NULL AND m.`PId` NOT REGEXP @GUID_RE)
|
||||
OR (m.`CreateUserId` IS NOT NULL AND m.`CreateUserId` NOT REGEXP @GUID_RE)
|
||||
OR (m.`UpdateUserId` IS NOT NULL AND m.`UpdateUserId` NOT REGEXP @GUID_RE)
|
||||
OR (m.`ConcurrencyStamp` IS NOT NULL AND m.`ConcurrencyStamp` NOT REGEXP @GUID_RE);
|
||||
|
||||
SELECT 'role bad rows' AS section, r.`Id`, r.`Code`, r.`Name`, r.`CreateUserId`, r.`UpdateUserId`, r.`ConcurrencyStamp`
|
||||
FROM `role` r
|
||||
WHERE (r.`CreateUserId` IS NOT NULL AND CAST(r.`CreateUserId` AS CHAR) NOT REGEXP @GUID_RE)
|
||||
OR (r.`UpdateUserId` IS NOT NULL AND CAST(r.`UpdateUserId` AS CHAR) NOT REGEXP @GUID_RE)
|
||||
OR (r.`ConcurrencyStamp` IS NOT NULL AND r.`ConcurrencyStamp` NOT REGEXP @GUID_RE);
|
||||
|
||||
SELECT 'user bad rows' AS section, u.`Id`, u.`Account`, u.`OrganizationId`, u.`ConcurrencyStamp`
|
||||
FROM `user` u
|
||||
WHERE (u.`OrganizationId` IS NOT NULL AND u.`OrganizationId` <> '' AND u.`OrganizationId` NOT REGEXP @GUID_RE)
|
||||
OR (u.`ConcurrencyStamp` IS NOT NULL AND u.`ConcurrencyStamp` NOT REGEXP @GUID_RE);
|
||||
|
||||
SELECT 'user_role bad rows' AS section, ur.`Id`, ur.`UserId`, ur.`RoleId`
|
||||
FROM `user_role` ur
|
||||
WHERE (ur.`UserId` IS NOT NULL AND ur.`UserId` NOT REGEXP @GUID_RE)
|
||||
OR (ur.`RoleId` IS NOT NULL AND ur.`RoleId` NOT REGEXP @GUID_RE);
|
||||
|
||||
SELECT 'role_permission bad RoleId' AS section, rp.`Id`, rp.`RoleId`, rp.`PermissionCode`
|
||||
FROM `role_permission` rp
|
||||
WHERE rp.`RoleId` IS NOT NULL AND rp.`RoleId` NOT REGEXP @GUID_RE;
|
||||
|
||||
SELECT 'organization bad rows' AS section, o.`Id`, o.`Name`, o.`ParentId`, o.`LeaderId`
|
||||
FROM `organization` o
|
||||
WHERE (o.`ParentId` IS NOT NULL AND o.`ParentId` <> '' AND o.`ParentId` NOT REGEXP @GUID_RE)
|
||||
OR (o.`LeaderId` IS NOT NULL AND o.`LeaderId` <> '' AND o.`LeaderId` NOT REGEXP @GUID_RE)
|
||||
OR (o.`ConcurrencyStamp` IS NOT NULL AND o.`ConcurrencyStamp` NOT REGEXP @GUID_RE);
|
||||
|
||||
-- 快速查 menu 里最常见的脏值(勿只查 Id 列)
|
||||
SELECT 'menu CreateUserId=0' AS hint, COUNT(*) AS cnt FROM `menu` WHERE CAST(`CreateUserId` AS CHAR) = '0';
|
||||
SELECT 'menu UpdateUserId=0' AS hint, COUNT(*) AS cnt FROM `menu` WHERE CAST(`UpdateUserId` AS CHAR) = '0';
|
||||
SELECT 'menu PId=0' AS hint, COUNT(*) AS cnt FROM `menu` WHERE CAST(`PId` AS CHAR) = '0';
|
||||
86
sql/fix_rbac_bad_guids.sql
Normal file
86
sql/fix_rbac_bad_guids.sql
Normal file
@ -0,0 +1,86 @@
|
||||
-- RBAC 库:修复 GUID 脏数据(menu / role / user / organization 等)
|
||||
-- 与 diagnose_rbac_bad_guids.sql 配套;修复后重新跑诊断应无结果
|
||||
--
|
||||
-- ⚠️ 在 API 实际连接的 RBAC 库执行(api.nanafa.com 对应库,不是本机 oms 除非 API 连本机)
|
||||
|
||||
SET @GUID_RE = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$';
|
||||
SET @ROOT_PID = '00000000-0000-0000-0000-000000000000';
|
||||
|
||||
SET @ADMIN_USER_ID = (
|
||||
SELECT `Id` FROM `user`
|
||||
WHERE `Account` = 'admin' AND (`IsDeleted` = 0 OR `IsDeleted` IS NULL)
|
||||
LIMIT 1
|
||||
);
|
||||
SET @ADMIN_USER_ID = IFNULL(
|
||||
@ADMIN_USER_ID,
|
||||
(SELECT `Id` FROM `user` WHERE `IsDeleted` = 0 OR `IsDeleted` IS NULL ORDER BY `CreateTime` LIMIT 1)
|
||||
);
|
||||
|
||||
SELECT @ADMIN_USER_ID AS admin_user_id_used;
|
||||
|
||||
-- ========== menu ==========
|
||||
UPDATE `menu`
|
||||
SET `CreateUserId` = @ADMIN_USER_ID
|
||||
WHERE `CreateUserId` IS NOT NULL AND `CreateUserId` NOT REGEXP @GUID_RE;
|
||||
|
||||
UPDATE `menu`
|
||||
SET `UpdateUserId` = @ADMIN_USER_ID
|
||||
WHERE `UpdateUserId` IS NOT NULL AND `UpdateUserId` NOT REGEXP @GUID_RE;
|
||||
|
||||
UPDATE `menu`
|
||||
SET `PId` = @ROOT_PID
|
||||
WHERE `PId` IS NOT NULL AND `PId` NOT REGEXP @GUID_RE;
|
||||
|
||||
UPDATE `menu`
|
||||
SET `ConcurrencyStamp` = UUID()
|
||||
WHERE `ConcurrencyStamp` IS NOT NULL AND `ConcurrencyStamp` NOT REGEXP @GUID_RE;
|
||||
|
||||
-- 小红书种子菜单(精确 Id)
|
||||
UPDATE `menu`
|
||||
SET `CreateUserId` = @ADMIN_USER_ID,
|
||||
`UpdateUserId` = @ADMIN_USER_ID
|
||||
WHERE `Id` IN (
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567801',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567802',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567803',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567804',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567805',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567806',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567807',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567808'
|
||||
);
|
||||
|
||||
-- ========== role(若列为 CHAR(36) GUID)==========
|
||||
UPDATE `role`
|
||||
SET `CreateUserId` = @ADMIN_USER_ID
|
||||
WHERE `CreateUserId` IS NOT NULL AND CAST(`CreateUserId` AS CHAR) NOT REGEXP @GUID_RE;
|
||||
|
||||
UPDATE `role`
|
||||
SET `UpdateUserId` = @ADMIN_USER_ID
|
||||
WHERE `UpdateUserId` IS NOT NULL AND CAST(`UpdateUserId` AS CHAR) NOT REGEXP @GUID_RE;
|
||||
|
||||
UPDATE `role`
|
||||
SET `ConcurrencyStamp` = UUID()
|
||||
WHERE `ConcurrencyStamp` IS NOT NULL AND `ConcurrencyStamp` NOT REGEXP @GUID_RE;
|
||||
|
||||
-- ========== user ==========
|
||||
UPDATE `user`
|
||||
SET `OrganizationId` = NULL
|
||||
WHERE `OrganizationId` IS NOT NULL
|
||||
AND `OrganizationId` <> ''
|
||||
AND `OrganizationId` NOT REGEXP @GUID_RE;
|
||||
|
||||
UPDATE `user`
|
||||
SET `ConcurrencyStamp` = UUID()
|
||||
WHERE `ConcurrencyStamp` IS NOT NULL AND `ConcurrencyStamp` NOT REGEXP @GUID_RE;
|
||||
|
||||
-- ========== organization ==========
|
||||
UPDATE `organization`
|
||||
SET `ParentId` = NULL
|
||||
WHERE `ParentId` IS NOT NULL AND `ParentId` <> '' AND `ParentId` NOT REGEXP @GUID_RE;
|
||||
|
||||
UPDATE `organization`
|
||||
SET `LeaderId` = NULL
|
||||
WHERE `LeaderId` IS NOT NULL AND `LeaderId` <> '' AND `LeaderId` NOT REGEXP @GUID_RE;
|
||||
|
||||
-- 修复后请再执行 diagnose_rbac_bad_guids.sql 确认无脏行
|
||||
56
sql/fix_xhs_menu_bad_guids.sql
Normal file
56
sql/fix_xhs_menu_bad_guids.sql
Normal file
@ -0,0 +1,56 @@
|
||||
-- 修复 menu 表 CreateUserId / UpdateUserId 非法 GUID(如 "0")
|
||||
-- 症状:登录或 GET /rbac/menu/get-menu-tree → Could not parse CHAR(36) value as Guid: 0
|
||||
--
|
||||
-- ⚠️ 在 RBAC 库执行(不是 OMS 业务库 fake_simple_admin)
|
||||
-- ⚠️ WHERE 中禁止写 CreateUserId = 0(整数),会触发 1292 GUID 转数字错误
|
||||
|
||||
-- 1) 管理员 GUID:优先 admin 账号,否则取第一个用户
|
||||
SET @ADMIN_USER_ID = (
|
||||
SELECT `Id` FROM `user`
|
||||
WHERE `Account` = 'admin' AND (`IsDeleted` = 0 OR `IsDeleted` IS NULL)
|
||||
LIMIT 1
|
||||
);
|
||||
SET @ADMIN_USER_ID = IFNULL(
|
||||
@ADMIN_USER_ID,
|
||||
(SELECT `Id` FROM `user` WHERE `IsDeleted` = 0 OR `IsDeleted` IS NULL ORDER BY `CreateTime` LIMIT 1)
|
||||
);
|
||||
|
||||
SELECT @ADMIN_USER_ID AS admin_user_id_used;
|
||||
|
||||
-- 2) 诊断:修复前查看脏数据
|
||||
SELECT `Id`, `Name`, `Route`, `CreateUserId`, `UpdateUserId`
|
||||
FROM `menu`
|
||||
WHERE (`CreateUserId` IS NOT NULL AND `CreateUserId` NOT REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')
|
||||
OR (`UpdateUserId` IS NOT NULL AND `UpdateUserId` NOT REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$');
|
||||
|
||||
-- 3) 小红书种子菜单(按 Id 精确修复)
|
||||
UPDATE `menu`
|
||||
SET `CreateUserId` = @ADMIN_USER_ID,
|
||||
`UpdateUserId` = @ADMIN_USER_ID
|
||||
WHERE `Id` IN (
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567801',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567802',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567803',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567804',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567805',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567806',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567807',
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567808'
|
||||
);
|
||||
|
||||
-- 4) 兜底:全表非法 GUID(字符串比较,勿用 = 0)
|
||||
UPDATE `menu`
|
||||
SET `CreateUserId` = @ADMIN_USER_ID
|
||||
WHERE `CreateUserId` IS NOT NULL
|
||||
AND `CreateUserId` NOT REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$';
|
||||
|
||||
UPDATE `menu`
|
||||
SET `UpdateUserId` = @ADMIN_USER_ID
|
||||
WHERE `UpdateUserId` IS NOT NULL
|
||||
AND `UpdateUserId` NOT REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$';
|
||||
|
||||
-- 5) 修复后应无结果
|
||||
SELECT `Id`, `Name`, `CreateUserId`, `UpdateUserId`
|
||||
FROM `menu`
|
||||
WHERE (`CreateUserId` IS NOT NULL AND `CreateUserId` NOT REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')
|
||||
OR (`UpdateUserId` IS NOT NULL AND `UpdateUserId` NOT REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$');
|
||||
219
sql/install_xhs_module.sql
Normal file
219
sql/install_xhs_module.sql
Normal file
@ -0,0 +1,219 @@
|
||||
-- 小红书模块:建表 + 业务种子数据(在 OMS 库 fake_simple_admin / oms 执行)
|
||||
-- 执行顺序:本文件 → seed_xhs_rbac_menu.sql(RBAC 库 menu 表)→ seed_xhs_role_permissions.sql
|
||||
--
|
||||
-- Navicat:选中 OMS 业务库后运行本文件
|
||||
|
||||
-- ========== 1. 建表 migrate_t_xhs.sql ==========
|
||||
-- (内容与 migrate_t_xhs.sql 相同,便于一键安装)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_agent` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID → t_customer.Id',
|
||||
`DeviceId` VARCHAR(64) NOT NULL COMMENT 'Agent 本机设备标识',
|
||||
`DeviceName` VARCHAR(128) NULL COMMENT '用户自定义名称',
|
||||
`AgentVersion` VARCHAR(32) NULL COMMENT 'Agent 版本号',
|
||||
`BridgePort` INT NOT NULL DEFAULT 9333 COMMENT 'Bridge WebSocket 端口',
|
||||
`Status` VARCHAR(16) NOT NULL DEFAULT 'offline' COMMENT 'online/offline',
|
||||
`EngineRunning` TINYINT NOT NULL DEFAULT 0 COMMENT 'Bridge 引擎是否运行',
|
||||
`ExtensionConnected` TINYINT NOT NULL DEFAULT 0 COMMENT 'Chrome 扩展是否连上 Bridge',
|
||||
`XhsLoggedIn` TINYINT NOT NULL DEFAULT 0 COMMENT '小红书是否已登录',
|
||||
`LastHeartbeatTime` DATETIME NULL COMMENT '最后心跳时间',
|
||||
`LastError` VARCHAR(512) NULL COMMENT '最近一次 Agent 上报错误摘要',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_agent_device` (`DeviceId`),
|
||||
KEY `idx_xhs_agent_customer` (`CustomerId`, `Status`)
|
||||
) COMMENT='小红书本地 Agent 设备表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_task` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`AgentId` INT NULL COMMENT '指定执行的 Agent;NULL 表示任意在线 Agent',
|
||||
`TaskType` VARCHAR(32) NOT NULL COMMENT '任务类型',
|
||||
`ToolName` VARCHAR(64) NULL COMMENT 'LIKE AI 工具名',
|
||||
`Priority` INT NOT NULL DEFAULT 0 COMMENT '越大越优先',
|
||||
`Status` VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/claimed/running/succeeded/failed/cancelled',
|
||||
`ArgumentsJson` JSON NOT NULL COMMENT '工具参数 JSON',
|
||||
`ProgressText` VARCHAR(512) NULL COMMENT '执行过程摘要',
|
||||
`ResultJson` JSON NULL COMMENT '成功时结构化结果',
|
||||
`ErrorMessage` VARCHAR(1024) NULL COMMENT '失败原因',
|
||||
`Source` VARCHAR(32) NOT NULL DEFAULT 'web' COMMENT 'web/schedule/skill',
|
||||
`SourceRefId` VARCHAR(64) NULL COMMENT '来源关联 ID',
|
||||
`ScheduledTime` DATETIME NULL COMMENT '计划执行时间',
|
||||
`ClaimedAt` DATETIME NULL COMMENT 'Agent 认领时间',
|
||||
`StartedAt` DATETIME NULL COMMENT '开始执行',
|
||||
`FinishedAt` DATETIME NULL COMMENT '结束时间',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_xhs_task_poll` (`CustomerId`, `Status`, `ScheduledTime`, `Priority`),
|
||||
KEY `idx_xhs_task_agent` (`AgentId`, `Status`),
|
||||
KEY `idx_xhs_task_source` (`Source`, `SourceRefId`)
|
||||
) COMMENT='小红书 Agent 任务队列表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_publish_record` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`TaskId` INT NULL COMMENT '关联 t_xhs_task.Id',
|
||||
`ToolName` VARCHAR(64) NOT NULL COMMENT 'publish_content / publish_with_video',
|
||||
`Title` VARCHAR(256) NOT NULL COMMENT '笔记标题',
|
||||
`ContentPreview` VARCHAR(512) NULL COMMENT '正文摘要',
|
||||
`ContentFull` TEXT NULL COMMENT '完整正文',
|
||||
`MediaJson` JSON NULL COMMENT '图片/视频 URL 列表',
|
||||
`TagsJson` JSON NULL COMMENT '标签数组',
|
||||
`Visibility` VARCHAR(32) NULL COMMENT '可见范围',
|
||||
`Status` VARCHAR(16) NOT NULL DEFAULT 'draft' COMMENT 'draft/scheduled/publishing/succeeded/failed/cancelled',
|
||||
`Success` TINYINT NULL COMMENT '最终是否成功',
|
||||
`ResultSummary` VARCHAR(1024) NULL COMMENT 'Agent 回传结果摘要',
|
||||
`ScheduledTime` DATETIME NULL COMMENT '定时发布时间',
|
||||
`PublishedTime` DATETIME NULL COMMENT '实际发布完成时间',
|
||||
`SkillId` INT NULL COMMENT '若来自技能卡片',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_xhs_publish_customer_time` (`CustomerId`, `CreateTime`),
|
||||
KEY `idx_xhs_publish_status_sched` (`CustomerId`, `Status`, `ScheduledTime`),
|
||||
KEY `idx_xhs_publish_task` (`TaskId`)
|
||||
) COMMENT='小红书发布记录表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_analysis_session` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`SessionName` VARCHAR(128) NULL COMMENT '用户备注名',
|
||||
`AnalysisType` VARCHAR(32) NOT NULL COMMENT 'home_feed/search/my_feeds/feed_detail',
|
||||
`Keyword` VARCHAR(128) NULL COMMENT '搜索关键词',
|
||||
`TaskId` INT NULL COMMENT '采集任务 ID',
|
||||
`FeedsJson` JSON NULL COMMENT '采集笔记列表',
|
||||
`SelectedFeedId` VARCHAR(64) NULL COMMENT '当前选中笔记 ID',
|
||||
`AiAnalysisMarkdown` MEDIUMTEXT NULL COMMENT 'AI 分析结果',
|
||||
`SkillId` INT NULL COMMENT '技能配方 ID',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_xhs_analysis_customer` (`CustomerId`, `CreateTime`)
|
||||
) COMMENT='小红书内容分析会话表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_skill` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID;0 表示系统预置',
|
||||
`ExternalId` VARCHAR(32) NOT NULL COMMENT '对外稳定 ID',
|
||||
`Title` VARCHAR(256) NOT NULL COMMENT '技能名称',
|
||||
`SkillType` VARCHAR(32) NOT NULL DEFAULT 'ai_create' COMMENT '技能类型',
|
||||
`PresetId` VARCHAR(64) NULL COMMENT '内置预设标识',
|
||||
`RecipeJson` JSON NOT NULL COMMENT '配方 JSON',
|
||||
`SortOrder` INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_skill_external` (`CustomerId`, `ExternalId`),
|
||||
KEY `idx_xhs_skill_customer` (`CustomerId`, `SortOrder`)
|
||||
) COMMENT='小红书技能卡片表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_human_config` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`BehaviorJson` JSON NOT NULL COMMENT 'human_behavior 配置',
|
||||
`GuardJson` JSON NOT NULL COMMENT 'human_guard 配置',
|
||||
`Version` INT NOT NULL DEFAULT 1 COMMENT '配置版本',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_human_customer` (`CustomerId`)
|
||||
) COMMENT='小红书拟人行为与防封配置表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_ai_provider` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID;0 表示全局',
|
||||
`ProviderKey` VARCHAR(32) NOT NULL COMMENT 'ai_chatgpt / ai_zhipu 等',
|
||||
`ProviderType` VARCHAR(32) NOT NULL COMMENT 'chatgpt/doubao/deepseek/zhipu',
|
||||
`DisplayName` VARCHAR(64) NOT NULL COMMENT '展示名',
|
||||
`BaseUrl` VARCHAR(512) NOT NULL COMMENT 'API Base URL',
|
||||
`ApiKeyEnc` VARCHAR(512) NULL COMMENT 'API Key(暂明文,后续可加密)',
|
||||
`ApiSecretEnc` VARCHAR(512) NULL COMMENT 'Secret/模型名',
|
||||
`IsDefault` TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认',
|
||||
`Enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_ai_provider` (`CustomerId`, `ProviderKey`),
|
||||
KEY `idx_xhs_ai_customer` (`CustomerId`, `Enabled`)
|
||||
) COMMENT='小红书 AI 提供商配置表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_media` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`FileName` VARCHAR(256) NOT NULL COMMENT '原始文件名',
|
||||
`StoragePath` VARCHAR(512) NOT NULL COMMENT '相对路径',
|
||||
`PublicUrl` VARCHAR(512) NOT NULL COMMENT '对外 HTTPS URL',
|
||||
`MimeType` VARCHAR(64) NULL COMMENT 'MIME',
|
||||
`FileSizeBytes` BIGINT NULL COMMENT '字节数',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_xhs_media_customer` (`CustomerId`, `CreateTime`)
|
||||
) COMMENT='小红书创作上传媒体表';
|
||||
|
||||
-- ========== 2. 种子 seed_xhs_skills + seed_xhs_ai_providers ==========
|
||||
|
||||
INSERT INTO `t_xhs_skill` (
|
||||
`CustomerId`, `ExternalId`, `Title`, `SkillType`, `PresetId`, `RecipeJson`, `SortOrder`,
|
||||
`IsDeleted`, `CreateUserId`, `CreateTime`
|
||||
) VALUES
|
||||
(0, '5a9d3c012d50', '数字型/疑问型 · 物流知识 · 图文', 'ai_create', NULL,
|
||||
'{"formula":["emotion","utility","visual","social"],"title_types":["number","question"],"body":{"domain":"物流知识","content_type":"干货教程","emotion":"生活场景","social":"知识炫耀"},"image_modes":["cover_impact"],"video_modes":[],"notes":"讲解9610的简易申报和汇总申报,退税,以及申报要求"}',
|
||||
1, 0, 0, NOW()),
|
||||
(0, '7dd58bfe4770', '效率工具 · 干货教程(新号)', 'ai_create', 'efficiency_tutorial',
|
||||
'{"formula":["emotion","utility","visual","social"],"title_types":["number","question"],"body":{"domain":"效率工具","content_type":"干货教程","emotion":"焦虑共鸣","social":"价值传递"},"image_modes":["cover_impact","step_cards"],"video_modes":[],"notes":"话题示例:笔记工具提升效率"}',
|
||||
2, 0, 0, NOW()),
|
||||
(0, '3f6ed9f62afd', '职场发展 · 经验分享', 'ai_create', 'career_experience',
|
||||
'{"formula":["emotion","utility","social"],"title_types":["story","compare"],"body":{"domain":"职场发展","content_type":"经验分享","emotion":"理想生活","social":"身份标签"},"image_modes":["cover_impact","lifestyle"],"video_modes":[],"notes":"话题示例:职场人际关系"}',
|
||||
3, 0, 0, NOW()),
|
||||
(0, '61a4d4febada', '情感共鸣 · 真实场景', 'ai_create', 'emotional_relatable',
|
||||
'{"formula":["emotion","social"],"title_types":["story","question"],"body":{"domain":"情感共鸣","content_type":"经验分享","emotion":"生活场景","social":"情感支持"},"image_modes":["lifestyle"],"video_modes":["vlog_clip"],"notes":"让读者觉得这也太真实了"}',
|
||||
4, 0, 0, NOW()),
|
||||
(0, 'ec62cc03eea9', '避坑指南 · 观点表达', 'ai_create', 'pitfall_opinion',
|
||||
'{"formula":["emotion","utility","visual","social"],"title_types":["number"],"body":{"domain":"职场发展","content_type":"避坑指南","emotion":"吐槽宣泄","social":"观点表达"},"image_modes":["info_chart"],"video_modes":["talking_head"],"notes":"话题示例:避坑指南"}',
|
||||
5, 0, 0, NOW())
|
||||
ON DUPLICATE KEY UPDATE `Title` = VALUES(`Title`), `RecipeJson` = VALUES(`RecipeJson`);
|
||||
|
||||
INSERT INTO `t_xhs_ai_provider` (
|
||||
`CustomerId`, `ProviderKey`, `ProviderType`, `DisplayName`, `BaseUrl`,
|
||||
`ApiKeyEnc`, `ApiSecretEnc`, `IsDefault`, `Enabled`,
|
||||
`IsDeleted`, `CreateUserId`, `CreateTime`
|
||||
) VALUES
|
||||
(0, 'ai_zhipu', 'zhipu', '智谱 AI', 'https://open.bigmodel.cn/api/paas/v4', NULL, 'glm-4-flash', 1, 0, 0, 0, NOW()),
|
||||
(0, 'ai_deepseek', 'deepseek', 'DeepSeek', 'https://api.deepseek.com/v1', NULL, 'deepseek-chat', 0, 0, 0, 0, NOW()),
|
||||
(0, 'ai_chatgpt', 'chatgpt', 'OpenAI', 'https://api.openai.com/v1', NULL, 'gpt-4o-mini', 0, 0, 0, 0, NOW())
|
||||
ON DUPLICATE KEY UPDATE `DisplayName` = VALUES(`DisplayName`), `BaseUrl` = VALUES(`BaseUrl`);
|
||||
181
sql/migrate_t_xhs.sql
Normal file
181
sql/migrate_t_xhs.sql
Normal file
@ -0,0 +1,181 @@
|
||||
-- 小红书(LIKE AI → OMS)模块表结构
|
||||
-- 设计文档:E:\oms0414\docs\小红书模块设计-063001.md
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_agent` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID → t_customer.Id',
|
||||
`DeviceId` VARCHAR(64) NOT NULL COMMENT 'Agent 本机设备标识',
|
||||
`DeviceName` VARCHAR(128) NULL COMMENT '用户自定义名称',
|
||||
`AgentVersion` VARCHAR(32) NULL COMMENT 'Agent 版本号',
|
||||
`BridgePort` INT NOT NULL DEFAULT 9333 COMMENT 'Bridge WebSocket 端口',
|
||||
`Status` VARCHAR(16) NOT NULL DEFAULT 'offline' COMMENT 'online/offline',
|
||||
`EngineRunning` TINYINT NOT NULL DEFAULT 0 COMMENT 'Bridge 引擎是否运行',
|
||||
`ExtensionConnected` TINYINT NOT NULL DEFAULT 0 COMMENT 'Chrome 扩展是否连上 Bridge',
|
||||
`XhsLoggedIn` TINYINT NOT NULL DEFAULT 0 COMMENT '小红书是否已登录',
|
||||
`LastHeartbeatTime` DATETIME NULL COMMENT '最后心跳时间',
|
||||
`LastError` VARCHAR(512) NULL COMMENT '最近一次 Agent 上报错误摘要',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_agent_device` (`DeviceId`),
|
||||
KEY `idx_xhs_agent_customer` (`CustomerId`, `Status`)
|
||||
) COMMENT='小红书本地 Agent 设备表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_task` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`AgentId` INT NULL COMMENT '指定执行的 Agent;NULL 表示任意在线 Agent',
|
||||
`TaskType` VARCHAR(32) NOT NULL COMMENT '任务类型',
|
||||
`ToolName` VARCHAR(64) NULL COMMENT 'LIKE AI 工具名',
|
||||
`Priority` INT NOT NULL DEFAULT 0 COMMENT '越大越优先',
|
||||
`Status` VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/claimed/running/succeeded/failed/cancelled',
|
||||
`ArgumentsJson` JSON NOT NULL COMMENT '工具参数 JSON',
|
||||
`ProgressText` VARCHAR(512) NULL COMMENT '执行过程摘要',
|
||||
`ResultJson` JSON NULL COMMENT '成功时结构化结果',
|
||||
`ErrorMessage` VARCHAR(1024) NULL COMMENT '失败原因',
|
||||
`Source` VARCHAR(32) NOT NULL DEFAULT 'web' COMMENT 'web/schedule/skill',
|
||||
`SourceRefId` VARCHAR(64) NULL COMMENT '来源关联 ID',
|
||||
`ScheduledTime` DATETIME NULL COMMENT '计划执行时间',
|
||||
`ClaimedAt` DATETIME NULL COMMENT 'Agent 认领时间',
|
||||
`StartedAt` DATETIME NULL COMMENT '开始执行',
|
||||
`FinishedAt` DATETIME NULL COMMENT '结束时间',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_xhs_task_poll` (`CustomerId`, `Status`, `ScheduledTime`, `Priority`),
|
||||
KEY `idx_xhs_task_agent` (`AgentId`, `Status`),
|
||||
KEY `idx_xhs_task_source` (`Source`, `SourceRefId`)
|
||||
) COMMENT='小红书 Agent 任务队列表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_publish_record` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`TaskId` INT NULL COMMENT '关联 t_xhs_task.Id',
|
||||
`ToolName` VARCHAR(64) NOT NULL COMMENT 'publish_content / publish_with_video',
|
||||
`Title` VARCHAR(256) NOT NULL COMMENT '笔记标题',
|
||||
`ContentPreview` VARCHAR(512) NULL COMMENT '正文摘要',
|
||||
`ContentFull` TEXT NULL COMMENT '完整正文',
|
||||
`MediaJson` JSON NULL COMMENT '图片/视频 URL 列表',
|
||||
`TagsJson` JSON NULL COMMENT '标签数组',
|
||||
`Visibility` VARCHAR(32) NULL COMMENT '可见范围',
|
||||
`Status` VARCHAR(16) NOT NULL DEFAULT 'draft' COMMENT 'draft/scheduled/publishing/succeeded/failed/cancelled',
|
||||
`Success` TINYINT NULL COMMENT '最终是否成功',
|
||||
`ResultSummary` VARCHAR(1024) NULL COMMENT 'Agent 回传结果摘要',
|
||||
`ScheduledTime` DATETIME NULL COMMENT '定时发布时间',
|
||||
`PublishedTime` DATETIME NULL COMMENT '实际发布完成时间',
|
||||
`SkillId` INT NULL COMMENT '若来自技能卡片',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_xhs_publish_customer_time` (`CustomerId`, `CreateTime`),
|
||||
KEY `idx_xhs_publish_status_sched` (`CustomerId`, `Status`, `ScheduledTime`),
|
||||
KEY `idx_xhs_publish_task` (`TaskId`)
|
||||
) COMMENT='小红书发布记录表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_analysis_session` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`SessionName` VARCHAR(128) NULL COMMENT '用户备注名',
|
||||
`AnalysisType` VARCHAR(32) NOT NULL COMMENT 'home_feed/search/my_feeds/feed_detail',
|
||||
`Keyword` VARCHAR(128) NULL COMMENT '搜索关键词',
|
||||
`TaskId` INT NULL COMMENT '采集任务 ID',
|
||||
`FeedsJson` JSON NULL COMMENT '采集笔记列表',
|
||||
`SelectedFeedId` VARCHAR(64) NULL COMMENT '当前选中笔记 ID',
|
||||
`AiAnalysisMarkdown` MEDIUMTEXT NULL COMMENT 'AI 分析结果',
|
||||
`SkillId` INT NULL COMMENT '技能配方 ID',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_xhs_analysis_customer` (`CustomerId`, `CreateTime`)
|
||||
) COMMENT='小红书内容分析会话表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_skill` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID;0 表示系统预置',
|
||||
`ExternalId` VARCHAR(32) NOT NULL COMMENT '对外稳定 ID',
|
||||
`Title` VARCHAR(256) NOT NULL COMMENT '技能名称',
|
||||
`SkillType` VARCHAR(32) NOT NULL DEFAULT 'ai_create' COMMENT '技能类型',
|
||||
`PresetId` VARCHAR(64) NULL COMMENT '内置预设标识',
|
||||
`RecipeJson` JSON NOT NULL COMMENT '配方 JSON',
|
||||
`SortOrder` INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_skill_external` (`CustomerId`, `ExternalId`),
|
||||
KEY `idx_xhs_skill_customer` (`CustomerId`, `SortOrder`)
|
||||
) COMMENT='小红书技能卡片表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_human_config` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`BehaviorJson` JSON NOT NULL COMMENT 'human_behavior 配置',
|
||||
`GuardJson` JSON NOT NULL COMMENT 'human_guard 配置',
|
||||
`Version` INT NOT NULL DEFAULT 1 COMMENT '配置版本',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_human_customer` (`CustomerId`)
|
||||
) COMMENT='小红书拟人行为与防封配置表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_ai_provider` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID;0 表示全局',
|
||||
`ProviderKey` VARCHAR(32) NOT NULL COMMENT 'ai_chatgpt / ai_zhipu 等',
|
||||
`ProviderType` VARCHAR(32) NOT NULL COMMENT 'chatgpt/doubao/deepseek/zhipu',
|
||||
`DisplayName` VARCHAR(64) NOT NULL COMMENT '展示名',
|
||||
`BaseUrl` VARCHAR(512) NOT NULL COMMENT 'API Base URL',
|
||||
`ApiKeyEnc` VARCHAR(512) NULL COMMENT 'API Key(暂明文,后续可加密)',
|
||||
`ApiSecretEnc` VARCHAR(512) NULL COMMENT 'Secret/模型名',
|
||||
`IsDefault` TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认',
|
||||
`Enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_ai_provider` (`CustomerId`, `ProviderKey`),
|
||||
KEY `idx_xhs_ai_customer` (`CustomerId`, `Enabled`)
|
||||
) COMMENT='小红书 AI 提供商配置表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_media` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`FileName` VARCHAR(256) NOT NULL COMMENT '原始文件名',
|
||||
`StoragePath` VARCHAR(512) NOT NULL COMMENT '相对路径',
|
||||
`PublicUrl` VARCHAR(512) NOT NULL COMMENT '对外 HTTPS URL',
|
||||
`MimeType` VARCHAR(64) NULL COMMENT 'MIME',
|
||||
`FileSizeBytes` BIGINT NULL COMMENT '字节数',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_xhs_media_customer` (`CustomerId`, `CreateTime`)
|
||||
) COMMENT='小红书创作上传媒体表';
|
||||
36
sql/seed_xhs_ai_providers.sql
Normal file
36
sql/seed_xhs_ai_providers.sql
Normal file
@ -0,0 +1,36 @@
|
||||
-- 小红书 AI 提供商模板(CustomerId=0 全局,需填入 ApiKeyEnc / ApiSecretEnc 后启用)
|
||||
-- ApiSecretEnc 字段存模型名,如 glm-4-flash / deepseek-chat / gpt-4o-mini
|
||||
--
|
||||
-- 若报 1146 Table doesn't exist:请先执行 migrate_t_xhs.sql
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_ai_provider` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID;0 表示全局',
|
||||
`ProviderKey` VARCHAR(32) NOT NULL COMMENT 'ai_chatgpt / ai_zhipu 等',
|
||||
`ProviderType` VARCHAR(32) NOT NULL COMMENT 'chatgpt/doubao/deepseek/zhipu',
|
||||
`DisplayName` VARCHAR(64) NOT NULL COMMENT '展示名',
|
||||
`BaseUrl` VARCHAR(512) NOT NULL COMMENT 'API Base URL',
|
||||
`ApiKeyEnc` VARCHAR(512) NULL COMMENT 'API Key(暂明文,后续可加密)',
|
||||
`ApiSecretEnc` VARCHAR(512) NULL COMMENT 'Secret/模型名',
|
||||
`IsDefault` TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认',
|
||||
`Enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_ai_provider` (`CustomerId`, `ProviderKey`),
|
||||
KEY `idx_xhs_ai_customer` (`CustomerId`, `Enabled`)
|
||||
) COMMENT='小红书 AI 提供商配置表';
|
||||
|
||||
INSERT INTO `t_xhs_ai_provider` (
|
||||
`CustomerId`, `ProviderKey`, `ProviderType`, `DisplayName`, `BaseUrl`,
|
||||
`ApiKeyEnc`, `ApiSecretEnc`, `IsDefault`, `Enabled`,
|
||||
`IsDeleted`, `CreateUserId`, `CreateTime`
|
||||
) VALUES
|
||||
(0, 'ai_zhipu', 'zhipu', '智谱 AI', 'https://open.bigmodel.cn/api/paas/v4', NULL, 'glm-4-flash', 1, 0, 0, 0, NOW()),
|
||||
(0, 'ai_deepseek', 'deepseek', 'DeepSeek', 'https://api.deepseek.com/v1', NULL, 'deepseek-chat', 0, 0, 0, 0, NOW()),
|
||||
(0, 'ai_chatgpt', 'chatgpt', 'OpenAI', 'https://api.openai.com/v1', NULL, 'gpt-4o-mini', 0, 0, 0, 0, NOW())
|
||||
ON DUPLICATE KEY UPDATE `DisplayName` = VALUES(`DisplayName`), `BaseUrl` = VALUES(`BaseUrl`);
|
||||
90
sql/seed_xhs_rbac_menu.sql
Normal file
90
sql/seed_xhs_rbac_menu.sql
Normal file
@ -0,0 +1,90 @@
|
||||
-- 小红书模块 RBAC 菜单种子(Fake.Rbac menu 表)
|
||||
-- 设计文档:E:\oms0414\docs\小红书模块设计-063001.md
|
||||
--
|
||||
-- 注意:本环境 menu.CreateUserId / UpdateUserId 为 CHAR(36) GUID,不可写整数 0
|
||||
-- 若已误插入导致 get-menu-tree 报错,先执行 fix_xhs_menu_bad_guids.sql
|
||||
|
||||
SET @ADMIN_USER_ID = (
|
||||
SELECT `Id` FROM `user`
|
||||
WHERE `Account` = 'admin' AND (`IsDeleted` = 0 OR `IsDeleted` IS NULL)
|
||||
LIMIT 1
|
||||
);
|
||||
SET @ADMIN_USER_ID = IFNULL(
|
||||
@ADMIN_USER_ID,
|
||||
'08de4226-3fd4-4a5c-83e5-70e164b15f2f'
|
||||
);
|
||||
SET @ROOT_PID = '00000000-0000-0000-0000-000000000000';
|
||||
|
||||
-- 一级:小红书
|
||||
INSERT INTO `menu` (
|
||||
`Id`, `PId`, `Name`, `PermissionCode`, `Type`, `Icon`, `Route`, `Component`, `Order`,
|
||||
`IsHidden`, `IsCached`, `Description`, `IsDeleted`,
|
||||
`ConcurrencyStamp`, `CreateUserId`, `CreateTime`, `UpdateUserId`, `UpdateTime`
|
||||
) VALUES (
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567801',
|
||||
@ROOT_PID,
|
||||
'小红书',
|
||||
'Xhs.Agent.Query',
|
||||
1,
|
||||
'icon-fire',
|
||||
'/xhs',
|
||||
NULL,
|
||||
4,
|
||||
0, 0, 'LIKE AI 小红书自动化', 0,
|
||||
UUID(), @ADMIN_USER_ID, NOW(), @ADMIN_USER_ID, NOW()
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
`Name` = VALUES(`Name`),
|
||||
`CreateUserId` = VALUES(`CreateUserId`),
|
||||
`UpdateUserId` = VALUES(`UpdateUserId`);
|
||||
|
||||
INSERT INTO `menu` (
|
||||
`Id`, `PId`, `Name`, `PermissionCode`, `Type`, `Icon`, `Route`, `Component`, `Order`,
|
||||
`IsHidden`, `IsCached`, `Description`, `IsDeleted`,
|
||||
`ConcurrencyStamp`, `CreateUserId`, `CreateTime`, `UpdateUserId`, `UpdateTime`
|
||||
) VALUES
|
||||
('a1b2c3d4-e5f6-7890-abcd-ef1234567802', 'a1b2c3d4-e5f6-7890-abcd-ef1234567801', '启动服务', 'Xhs.Agent.Query', 1, NULL, '/xhs/startService', 'views/xhs/startService/index', 1, 0, 1, NULL, 0, UUID(), @ADMIN_USER_ID, NOW(), @ADMIN_USER_ID, NOW()),
|
||||
('a1b2c3d4-e5f6-7890-abcd-ef1234567803', 'a1b2c3d4-e5f6-7890-abcd-ef1234567801', '内容分析', 'Xhs.Analysis.Use', 1, NULL, '/xhs/analysis', 'views/xhs/analysis/index', 2, 0, 1, NULL, 0, UUID(), @ADMIN_USER_ID, NOW(), @ADMIN_USER_ID, NOW()),
|
||||
('a1b2c3d4-e5f6-7890-abcd-ef1234567804', 'a1b2c3d4-e5f6-7890-abcd-ef1234567801', '爆款创作', 'Xhs.Creation.Use', 1, NULL, '/xhs/creation', 'views/xhs/creation/index', 3, 0, 1, NULL, 0, UUID(), @ADMIN_USER_ID, NOW(), @ADMIN_USER_ID, NOW()),
|
||||
('a1b2c3d4-e5f6-7890-abcd-ef1234567805', 'a1b2c3d4-e5f6-7890-abcd-ef1234567801', '发布记录', 'Xhs.Publish.Query', 1, NULL, '/xhs/publishHistory', 'views/xhs/publishHistory/index', 4, 0, 1, NULL, 0, UUID(), @ADMIN_USER_ID, NOW(), @ADMIN_USER_ID, NOW()),
|
||||
('a1b2c3d4-e5f6-7890-abcd-ef1234567806', 'a1b2c3d4-e5f6-7890-abcd-ef1234567801', '技能卡片', 'Xhs.Skills.Query', 1, NULL, '/xhs/skills', 'views/xhs/skills/index', 5, 0, 1, NULL, 0, UUID(), @ADMIN_USER_ID, NOW(), @ADMIN_USER_ID, NOW()),
|
||||
('a1b2c3d4-e5f6-7890-abcd-ef1234567807', 'a1b2c3d4-e5f6-7890-abcd-ef1234567801', '拟人设置', 'Xhs.HumanSettings.Save', 1, NULL, '/xhs/humanSettings', 'views/xhs/humanSettings/index', 6, 0, 1, NULL, 0, UUID(), @ADMIN_USER_ID, NOW(), @ADMIN_USER_ID, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`Name` = VALUES(`Name`),
|
||||
`Route` = VALUES(`Route`),
|
||||
`CreateUserId` = VALUES(`CreateUserId`),
|
||||
`UpdateUserId` = VALUES(`UpdateUserId`);
|
||||
|
||||
-- AI 助手:挂在「系统管理」下(Route=/system);若不存在则挂根节点
|
||||
SET @SYSTEM_PID = (
|
||||
SELECT `Id` FROM `menu`
|
||||
WHERE `Route` = '/system' AND `IsDeleted` = 0
|
||||
ORDER BY `Order`
|
||||
LIMIT 1
|
||||
);
|
||||
SET @AI_ASSISTANT_PID = IFNULL(@SYSTEM_PID, @ROOT_PID);
|
||||
|
||||
INSERT INTO `menu` (
|
||||
`Id`, `PId`, `Name`, `PermissionCode`, `Type`, `Icon`, `Route`, `Component`, `Order`,
|
||||
`IsHidden`, `IsCached`, `Description`, `IsDeleted`,
|
||||
`ConcurrencyStamp`, `CreateUserId`, `CreateTime`, `UpdateUserId`, `UpdateTime`
|
||||
) VALUES (
|
||||
'a1b2c3d4-e5f6-7890-abcd-ef1234567808',
|
||||
@AI_ASSISTANT_PID,
|
||||
'AI 助手',
|
||||
'Xhs.Ai.Use',
|
||||
1,
|
||||
NULL,
|
||||
'/system/xhsAiAssistant',
|
||||
'views/system/xhsAiAssistant/index',
|
||||
99,
|
||||
0, 1, '小红书 AI 助手(系统菜单)', 0,
|
||||
UUID(), @ADMIN_USER_ID, NOW(), @ADMIN_USER_ID, NOW()
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
`Name` = VALUES(`Name`),
|
||||
`PId` = VALUES(`PId`),
|
||||
`Route` = VALUES(`Route`),
|
||||
`Component` = VALUES(`Component`),
|
||||
`CreateUserId` = VALUES(`CreateUserId`),
|
||||
`UpdateUserId` = VALUES(`UpdateUserId`);
|
||||
|
||||
-- 权限分配见 sql/seed_xhs_role_permissions.sql(或重启 API 由 XhsRolePermissionSeedContributor 自动合并)
|
||||
49
sql/seed_xhs_role_permissions.sql
Normal file
49
sql/seed_xhs_role_permissions.sql
Normal file
@ -0,0 +1,49 @@
|
||||
-- 为客户角色(Code=Customer)批量追加小红书权限(幂等,可重复执行)
|
||||
-- RBAC 库执行;role_permission.CreateUserId 为 INT,可写 0
|
||||
|
||||
INSERT INTO `role_permission` (`Id`, `RoleId`, `PermissionCode`, `CreateUserId`, `CreateTime`)
|
||||
SELECT UUID(), r.`Id`, p.`Code`, 0, NOW()
|
||||
FROM `role` r
|
||||
CROSS JOIN (
|
||||
SELECT 'Xhs.Agent.Query' AS `Code` UNION ALL
|
||||
SELECT 'Xhs.Agent.Control' UNION ALL
|
||||
SELECT 'Xhs.Analysis.Use' UNION ALL
|
||||
SELECT 'Xhs.Creation.Use' UNION ALL
|
||||
SELECT 'Xhs.Publish.Query' UNION ALL
|
||||
SELECT 'Xhs.Publish.Cancel' UNION ALL
|
||||
SELECT 'Xhs.Skills.Query' UNION ALL
|
||||
SELECT 'Xhs.Skills.Save' UNION ALL
|
||||
SELECT 'Xhs.Skills.Run' UNION ALL
|
||||
SELECT 'Xhs.HumanSettings.Save' UNION ALL
|
||||
SELECT 'Xhs.Ai.Use' UNION ALL
|
||||
SELECT 'Xhs.Ai.Save'
|
||||
) p
|
||||
WHERE r.`Code` = 'Customer'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `role_permission` rp
|
||||
WHERE rp.`RoleId` = r.`Id` AND rp.`PermissionCode` = p.`Code`
|
||||
);
|
||||
|
||||
-- 超级管理员(可选,便于内测)
|
||||
INSERT INTO `role_permission` (`Id`, `RoleId`, `PermissionCode`, `CreateUserId`, `CreateTime`)
|
||||
SELECT UUID(), r.`Id`, p.`Code`, 0, NOW()
|
||||
FROM `role` r
|
||||
CROSS JOIN (
|
||||
SELECT 'Xhs.Agent.Query' AS `Code` UNION ALL
|
||||
SELECT 'Xhs.Agent.Control' UNION ALL
|
||||
SELECT 'Xhs.Analysis.Use' UNION ALL
|
||||
SELECT 'Xhs.Creation.Use' UNION ALL
|
||||
SELECT 'Xhs.Publish.Query' UNION ALL
|
||||
SELECT 'Xhs.Publish.Cancel' UNION ALL
|
||||
SELECT 'Xhs.Skills.Query' UNION ALL
|
||||
SELECT 'Xhs.Skills.Save' UNION ALL
|
||||
SELECT 'Xhs.Skills.Run' UNION ALL
|
||||
SELECT 'Xhs.HumanSettings.Save' UNION ALL
|
||||
SELECT 'Xhs.Ai.Use' UNION ALL
|
||||
SELECT 'Xhs.Ai.Save'
|
||||
) p
|
||||
WHERE r.`Code` = 'Admin'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `role_permission` rp
|
||||
WHERE rp.`RoleId` = r.`Id` AND rp.`PermissionCode` = p.`Code`
|
||||
);
|
||||
47
sql/seed_xhs_skills.sql
Normal file
47
sql/seed_xhs_skills.sql
Normal file
@ -0,0 +1,47 @@
|
||||
-- 小红书系统预置技能卡片(CustomerId=0)
|
||||
-- 来源:LIKE AI .xhs_bridge/skills.json
|
||||
--
|
||||
-- 若报 1146 Table doesn't exist:请先执行 migrate_t_xhs.sql(建全部 t_xhs_* 表)
|
||||
-- 或仅缺本表时,下方 CREATE TABLE 会自动创建 t_xhs_skill
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_xhs_skill` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID;0 表示系统预置',
|
||||
`ExternalId` VARCHAR(32) NOT NULL COMMENT '对外稳定 ID',
|
||||
`Title` VARCHAR(256) NOT NULL COMMENT '技能名称',
|
||||
`SkillType` VARCHAR(32) NOT NULL DEFAULT 'ai_create' COMMENT '技能类型',
|
||||
`PresetId` VARCHAR(64) NULL COMMENT '内置预设标识',
|
||||
`RecipeJson` JSON NOT NULL COMMENT '配方 JSON',
|
||||
`SortOrder` INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_xhs_skill_external` (`CustomerId`, `ExternalId`),
|
||||
KEY `idx_xhs_skill_customer` (`CustomerId`, `SortOrder`)
|
||||
) COMMENT='小红书技能卡片表';
|
||||
|
||||
INSERT INTO `t_xhs_skill` (
|
||||
`CustomerId`, `ExternalId`, `Title`, `SkillType`, `PresetId`, `RecipeJson`, `SortOrder`,
|
||||
`IsDeleted`, `CreateUserId`, `CreateTime`
|
||||
) VALUES
|
||||
(0, '5a9d3c012d50', '数字型/疑问型 · 物流知识 · 图文', 'ai_create', NULL,
|
||||
'{"formula":["emotion","utility","visual","social"],"title_types":["number","question"],"body":{"domain":"物流知识","content_type":"干货教程","emotion":"生活场景","social":"知识炫耀"},"image_modes":["cover_impact"],"video_modes":[],"notes":"讲解9610的简易申报和汇总申报,退税,以及申报要求"}',
|
||||
1, 0, 0, NOW()),
|
||||
(0, '7dd58bfe4770', '效率工具 · 干货教程(新号)', 'ai_create', 'efficiency_tutorial',
|
||||
'{"formula":["emotion","utility","visual","social"],"title_types":["number","question"],"body":{"domain":"效率工具","content_type":"干货教程","emotion":"焦虑共鸣","social":"价值传递"},"image_modes":["cover_impact","step_cards"],"video_modes":[],"notes":"话题示例:笔记工具提升效率"}',
|
||||
2, 0, 0, NOW()),
|
||||
(0, '3f6ed9f62afd', '职场发展 · 经验分享', 'ai_create', 'career_experience',
|
||||
'{"formula":["emotion","utility","social"],"title_types":["story","compare"],"body":{"domain":"职场发展","content_type":"经验分享","emotion":"理想生活","social":"身份标签"},"image_modes":["cover_impact","lifestyle"],"video_modes":[],"notes":"话题示例:职场人际关系"}',
|
||||
3, 0, 0, NOW()),
|
||||
(0, '61a4d4febada', '情感共鸣 · 真实场景', 'ai_create', 'emotional_relatable',
|
||||
'{"formula":["emotion","social"],"title_types":["story","question"],"body":{"domain":"情感共鸣","content_type":"经验分享","emotion":"生活场景","social":"情感支持"},"image_modes":["lifestyle"],"video_modes":["vlog_clip"],"notes":"让读者觉得这也太真实了"}',
|
||||
4, 0, 0, NOW()),
|
||||
(0, 'ec62cc03eea9', '避坑指南 · 观点表达', 'ai_create', 'pitfall_opinion',
|
||||
'{"formula":["emotion","utility","visual","social"],"title_types":["number"],"body":{"domain":"职场发展","content_type":"避坑指南","emotion":"吐槽宣泄","social":"观点表达"},"image_modes":["info_chart"],"video_modes":["talking_head"],"notes":"话题示例:避坑指南"}',
|
||||
5, 0, 0, NOW())
|
||||
ON DUPLICATE KEY UPDATE `Title` = VALUES(`Title`), `RecipeJson` = VALUES(`RecipeJson`);
|
||||
|
||||
533
src/Oms.Application/Dtos/Xhs/XhsDtos.cs
Normal file
533
src/Oms.Application/Dtos/Xhs/XhsDtos.cs
Normal file
@ -0,0 +1,533 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
|
||||
namespace Oms.Application.Dtos.Xhs;
|
||||
|
||||
public class XhsPageInput
|
||||
{
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
public int PageSize { get; set; } = 30;
|
||||
}
|
||||
|
||||
public class XhsAgentRegisterInput
|
||||
{
|
||||
[Required]
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
|
||||
public string? DeviceName { get; set; }
|
||||
|
||||
public string? AgentVersion { get; set; }
|
||||
|
||||
public int BridgePort { get; set; } = 9333;
|
||||
}
|
||||
|
||||
public class XhsAgentRegisterOutput
|
||||
{
|
||||
public int AgentId { get; set; }
|
||||
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
|
||||
public int HumanConfigVersion { get; set; }
|
||||
}
|
||||
|
||||
public class XhsAgentHeartbeatInput
|
||||
{
|
||||
[Required]
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
|
||||
public bool EngineRunning { get; set; }
|
||||
|
||||
public bool ExtensionConnected { get; set; }
|
||||
|
||||
public bool XhsLoggedIn { get; set; }
|
||||
|
||||
public string? LastError { get; set; }
|
||||
}
|
||||
|
||||
public class XhsAgentHeartbeatOutput
|
||||
{
|
||||
public bool Ok { get; set; } = true;
|
||||
|
||||
public int HumanConfigVersion { get; set; }
|
||||
|
||||
public DateTime ServerTime { get; set; }
|
||||
}
|
||||
|
||||
public class XhsAgentListInput : XhsPageInput;
|
||||
|
||||
public class XhsAgentOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
|
||||
public string? DeviceName { get; set; }
|
||||
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
public bool EngineRunning { get; set; }
|
||||
|
||||
public bool ExtensionConnected { get; set; }
|
||||
|
||||
public bool XhsLoggedIn { get; set; }
|
||||
|
||||
public DateTime? LastHeartbeatTime { get; set; }
|
||||
|
||||
public string? AgentVersion { get; set; }
|
||||
|
||||
public int BridgePort { get; set; }
|
||||
|
||||
public string? LastError { get; set; }
|
||||
}
|
||||
|
||||
public class XhsAgentControlInput
|
||||
{
|
||||
public int AgentId { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Action { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class XhsAgentControlOutput
|
||||
{
|
||||
public int TaskId { get; set; }
|
||||
}
|
||||
|
||||
public class XhsTaskClaimInput
|
||||
{
|
||||
[Required]
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
|
||||
public int Limit { get; set; } = 1;
|
||||
}
|
||||
|
||||
public class XhsTaskClaimItemOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string TaskType { get; set; } = string.Empty;
|
||||
|
||||
public string? ToolName { get; set; }
|
||||
|
||||
public object? Arguments { get; set; }
|
||||
|
||||
public int HumanConfigVersion { get; set; }
|
||||
}
|
||||
|
||||
public class XhsTaskClaimOutput
|
||||
{
|
||||
public List<XhsTaskClaimItemOutput> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
public class XhsTaskReportInput
|
||||
{
|
||||
public int TaskId { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
public string? ProgressText { get; set; }
|
||||
|
||||
public object? Result { get; set; }
|
||||
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class XhsTaskListInput : XhsPageInput
|
||||
{
|
||||
public string? Status { get; set; }
|
||||
|
||||
public string? TaskType { get; set; }
|
||||
|
||||
public string? SourceRefId { get; set; }
|
||||
}
|
||||
|
||||
public class XhsTaskOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string TaskType { get; set; } = string.Empty;
|
||||
|
||||
public string? ToolName { get; set; }
|
||||
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
public string? ProgressText { get; set; }
|
||||
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
public DateTime? FinishedAt { get; set; }
|
||||
}
|
||||
|
||||
public class XhsTaskCancelInput
|
||||
{
|
||||
public int TaskId { get; set; }
|
||||
}
|
||||
|
||||
public class XhsHumanConfigOutput
|
||||
{
|
||||
public object? Behavior { get; set; }
|
||||
|
||||
public object? Guard { get; set; }
|
||||
|
||||
public int Version { get; set; }
|
||||
}
|
||||
|
||||
public class XhsHumanConfigSaveInput
|
||||
{
|
||||
public object? Behavior { get; set; }
|
||||
|
||||
public object? Guard { get; set; }
|
||||
}
|
||||
|
||||
public class XhsHumanConfigPullInput
|
||||
{
|
||||
[Required]
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
|
||||
public int KnownVersion { get; set; }
|
||||
}
|
||||
|
||||
public class XhsHumanConfigPullOutput
|
||||
{
|
||||
public bool Unchanged { get; set; }
|
||||
|
||||
public object? Behavior { get; set; }
|
||||
|
||||
public object? Guard { get; set; }
|
||||
|
||||
public int Version { get; set; }
|
||||
}
|
||||
|
||||
public class XhsPublishListInput : XhsPageInput
|
||||
{
|
||||
public string? Status { get; set; }
|
||||
|
||||
public string? Keyword { get; set; }
|
||||
}
|
||||
|
||||
public class XhsPublishRecordOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string ToolName { get; set; } = string.Empty;
|
||||
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
public string? ContentPreview { get; set; }
|
||||
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
public bool? Success { get; set; }
|
||||
|
||||
public string? ResultSummary { get; set; }
|
||||
|
||||
public DateTime? ScheduledTime { get; set; }
|
||||
|
||||
public DateTime? PublishedTime { get; set; }
|
||||
|
||||
public DateTime CreateTime { get; set; }
|
||||
}
|
||||
|
||||
public class XhsPublishCancelInput
|
||||
{
|
||||
public int PublishRecordId { get; set; }
|
||||
}
|
||||
|
||||
public class XhsPublishRetryInput
|
||||
{
|
||||
public int PublishRecordId { get; set; }
|
||||
|
||||
public int? AgentId { get; set; }
|
||||
}
|
||||
|
||||
public class XhsCreationPublishInput
|
||||
{
|
||||
public int? AgentId { get; set; }
|
||||
|
||||
[Required]
|
||||
public string ToolName { get; set; } = "publish_content";
|
||||
|
||||
[Required]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
public List<string>? MediaUrls { get; set; }
|
||||
|
||||
public List<string>? Tags { get; set; }
|
||||
|
||||
public string? Visibility { get; set; }
|
||||
|
||||
public DateTime? ScheduledTime { get; set; }
|
||||
|
||||
public int? SkillId { get; set; }
|
||||
}
|
||||
|
||||
public class XhsCreationPublishOutput
|
||||
{
|
||||
public int PublishRecordId { get; set; }
|
||||
|
||||
public int? TaskId { get; set; }
|
||||
}
|
||||
|
||||
public class XhsSkillListInput
|
||||
{
|
||||
public int? CustomerId { get; set; }
|
||||
}
|
||||
|
||||
public class XhsSkillOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
public string ExternalId { get; set; } = string.Empty;
|
||||
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
public string SkillType { get; set; } = string.Empty;
|
||||
|
||||
public string? PresetId { get; set; }
|
||||
|
||||
public object? Recipe { get; set; }
|
||||
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public class XhsSkillSaveInput
|
||||
{
|
||||
public int? Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
public string SkillType { get; set; } = "ai_create";
|
||||
|
||||
public string? PresetId { get; set; }
|
||||
|
||||
public object? Recipe { get; set; }
|
||||
}
|
||||
|
||||
public class XhsAiProviderOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string ProviderKey { get; set; } = string.Empty;
|
||||
|
||||
public string ProviderType { get; set; } = string.Empty;
|
||||
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
|
||||
public string? ApiKey { get; set; }
|
||||
|
||||
public string? ApiSecret { get; set; }
|
||||
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
public class XhsAiProviderSaveInput
|
||||
{
|
||||
[Required]
|
||||
public string ProviderKey { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string ProviderType { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
|
||||
public string? ApiKey { get; set; }
|
||||
|
||||
public string? ApiSecret { get; set; }
|
||||
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
public class XhsAiChatInput
|
||||
{
|
||||
[Required]
|
||||
public string ProviderKey { get; set; } = string.Empty;
|
||||
|
||||
public string? Model { get; set; }
|
||||
|
||||
[Required]
|
||||
public List<XhsAiChatMessageInput> Messages { get; set; } = [];
|
||||
|
||||
public string Scene { get; set; } = "assistant";
|
||||
}
|
||||
|
||||
public class XhsAiChatMessageInput
|
||||
{
|
||||
public string Role { get; set; } = "user";
|
||||
|
||||
public string Content { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class XhsAiChatOutput
|
||||
{
|
||||
public string Content { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class XhsAnalysisStartInput
|
||||
{
|
||||
[Required]
|
||||
public string AnalysisType { get; set; } = string.Empty;
|
||||
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
public object? Filters { get; set; }
|
||||
|
||||
public int? AgentId { get; set; }
|
||||
|
||||
public int? SkillId { get; set; }
|
||||
}
|
||||
|
||||
public class XhsAnalysisStartOutput
|
||||
{
|
||||
public int SessionId { get; set; }
|
||||
|
||||
public int TaskId { get; set; }
|
||||
}
|
||||
|
||||
public class XhsAnalysisListInput : XhsPageInput;
|
||||
|
||||
public class XhsAnalysisSessionOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string? SessionName { get; set; }
|
||||
|
||||
public string AnalysisType { get; set; } = string.Empty;
|
||||
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
public int? TaskId { get; set; }
|
||||
|
||||
public DateTime CreateTime { get; set; }
|
||||
}
|
||||
|
||||
public class XhsAnalysisDetailInput
|
||||
{
|
||||
public int SessionId { get; set; }
|
||||
}
|
||||
|
||||
public class XhsAnalysisDetailOutput : XhsAnalysisSessionOutput
|
||||
{
|
||||
public object? Feeds { get; set; }
|
||||
|
||||
public string? AiAnalysisMarkdown { get; set; }
|
||||
|
||||
public string? TaskStatus { get; set; }
|
||||
}
|
||||
|
||||
public class XhsAnalysisRunAiInput
|
||||
{
|
||||
public int SessionId { get; set; }
|
||||
|
||||
public int? SkillId { get; set; }
|
||||
|
||||
[Required]
|
||||
public string ProviderKey { get; set; } = string.Empty;
|
||||
|
||||
public List<string>? SelectedFeedIds { get; set; }
|
||||
}
|
||||
|
||||
public class XhsAnalysisRunAiOutput
|
||||
{
|
||||
public string Markdown { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class XhsAnalysisInteractInput
|
||||
{
|
||||
public int? AgentId { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Action { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string FeedId { get; set; } = string.Empty;
|
||||
|
||||
public string? XsecToken { get; set; }
|
||||
|
||||
public string? Content { get; set; }
|
||||
}
|
||||
|
||||
public class XhsAnalysisInteractOutput
|
||||
{
|
||||
public int TaskId { get; set; }
|
||||
}
|
||||
|
||||
public class XhsSkillRunInput
|
||||
{
|
||||
public int SkillId { get; set; }
|
||||
|
||||
public int? AgentId { get; set; }
|
||||
|
||||
public string RunMode { get; set; } = "analyze_only";
|
||||
|
||||
public string? ProviderKey { get; set; }
|
||||
|
||||
public string? TopicOverride { get; set; }
|
||||
|
||||
public List<string>? MediaUrls { get; set; }
|
||||
}
|
||||
|
||||
public class XhsSkillRunOutput
|
||||
{
|
||||
public List<int> TaskIds { get; set; } = [];
|
||||
|
||||
public int? SessionId { get; set; }
|
||||
|
||||
public int? PublishRecordId { get; set; }
|
||||
|
||||
public string? GeneratedTitle { get; set; }
|
||||
|
||||
public string? GeneratedContent { get; set; }
|
||||
|
||||
public List<string>? GeneratedTags { get; set; }
|
||||
}
|
||||
|
||||
public class XhsCreationGenerateInput
|
||||
{
|
||||
public int? SkillId { get; set; }
|
||||
|
||||
[Required]
|
||||
public string ProviderKey { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Topic { get; set; } = string.Empty;
|
||||
|
||||
public string? ExtraNotes { get; set; }
|
||||
}
|
||||
|
||||
public class XhsCreationGenerateOutput
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
public List<string> Tags { get; set; } = [];
|
||||
}
|
||||
|
||||
public class XhsMediaUploadOutput
|
||||
{
|
||||
public int MediaId { get; set; }
|
||||
|
||||
public string PublicUrl { get; set; } = string.Empty;
|
||||
}
|
||||
@ -4,6 +4,8 @@ using Fake.Security.Claims;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Oms.Application.Integrations.SmallBagTrack;
|
||||
using Oms.Application.Integrations.Itfx;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Application.Workers;
|
||||
using Oms.Domain;
|
||||
|
||||
namespace Oms.Application;
|
||||
@ -26,5 +28,7 @@ public class OmsApplicationModule : FakeModule
|
||||
});
|
||||
context.Services.AddOmsSmallBagTrack(context.Services.GetConfiguration());
|
||||
context.Services.AddOmsItfxIntegration(context.Services.GetConfiguration());
|
||||
context.Services.AddXhsLlmClient();
|
||||
context.Services.AddHostedService<XhsScheduledPublishWorker>();
|
||||
}
|
||||
}
|
||||
|
||||
207
src/Oms.Application/Services/Xhs/XhsAgentService.cs
Normal file
207
src/Oms.Application/Services/Xhs/XhsAgentService.cs
Normal file
@ -0,0 +1,207 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Dtos.Xhs;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.XhsAggregate.Consts;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
using Oms.Domain.XhsAggregate.Repositories;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Oms.Application.Services.Xhs;
|
||||
|
||||
[ApiExplorerSettings(GroupName = "OMS-Xhs")]
|
||||
public class XhsAgentService : ApplicationService
|
||||
{
|
||||
private IXhsAgentRepository AgentRepository => LazyServiceProvider.GetRequiredService<IXhsAgentRepository>();
|
||||
private IXhsTaskRepository TaskRepository => LazyServiceProvider.GetRequiredService<IXhsTaskRepository>();
|
||||
private IXhsHumanConfigRepository HumanConfigRepository => LazyServiceProvider.GetRequiredService<IXhsHumanConfigRepository>();
|
||||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||||
|
||||
[HttpPost("/xhs/agent/register")]
|
||||
public async Task<XhsAgentRegisterOutput> RegisterAsync(XhsAgentRegisterInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var deviceId = input.DeviceId.Trim();
|
||||
if (string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
throw new BusinessException("deviceId 不能为空");
|
||||
}
|
||||
|
||||
var humanVersion = await EnsureHumanConfigVersionAsync(customerId);
|
||||
var agent = await AgentRepository.GetByDeviceIdAsync(deviceId);
|
||||
if (agent == null)
|
||||
{
|
||||
agent = new XhsAgent
|
||||
{
|
||||
CustomerId = customerId,
|
||||
DeviceId = deviceId,
|
||||
DeviceName = NormalizeOptional(input.DeviceName),
|
||||
AgentVersion = NormalizeOptional(input.AgentVersion),
|
||||
BridgePort = input.BridgePort > 0 ? input.BridgePort : 9333,
|
||||
Status = XhsAgentStatusCodes.Online,
|
||||
LastHeartbeatTime = DateTime.Now
|
||||
};
|
||||
await AgentRepository.InsertAsync(agent);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (agent.CustomerId != customerId)
|
||||
{
|
||||
throw new BusinessException("设备已绑定其他客户");
|
||||
}
|
||||
|
||||
agent.DeviceName = NormalizeOptional(input.DeviceName) ?? agent.DeviceName;
|
||||
agent.AgentVersion = NormalizeOptional(input.AgentVersion) ?? agent.AgentVersion;
|
||||
agent.BridgePort = input.BridgePort > 0 ? input.BridgePort : agent.BridgePort;
|
||||
agent.Status = XhsAgentStatusCodes.Online;
|
||||
agent.LastHeartbeatTime = DateTime.Now;
|
||||
await AgentRepository.UpdateAsync(agent);
|
||||
}
|
||||
|
||||
return new XhsAgentRegisterOutput
|
||||
{
|
||||
AgentId = agent.Id,
|
||||
CustomerId = customerId,
|
||||
DeviceId = agent.DeviceId,
|
||||
HumanConfigVersion = humanVersion
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/agent/heartbeat")]
|
||||
public async Task<XhsAgentHeartbeatOutput> HeartbeatAsync(XhsAgentHeartbeatInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var agent = await GetOwnedAgentByDeviceAsync(input.DeviceId.Trim(), customerId);
|
||||
agent.EngineRunning = input.EngineRunning;
|
||||
agent.ExtensionConnected = input.ExtensionConnected;
|
||||
agent.XhsLoggedIn = input.XhsLoggedIn;
|
||||
agent.LastError = NormalizeOptional(input.LastError);
|
||||
agent.Status = XhsAgentStatusCodes.Online;
|
||||
agent.LastHeartbeatTime = DateTime.Now;
|
||||
await AgentRepository.UpdateAsync(agent);
|
||||
|
||||
var humanVersion = await EnsureHumanConfigVersionAsync(customerId);
|
||||
return new XhsAgentHeartbeatOutput
|
||||
{
|
||||
Ok = true,
|
||||
HumanConfigVersion = humanVersion,
|
||||
ServerTime = DateTime.Now
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/agent/list")]
|
||||
public async Task<OmsPageResult<XhsAgentOutput>> ListAsync(XhsAgentListInput input)
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
||||
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
||||
|
||||
Expression<Func<XhsAgent, bool>> predicate = customerId.HasValue
|
||||
? x => x.CustomerId == customerId.Value
|
||||
: x => true;
|
||||
|
||||
var items = await AgentRepository.GetPagedListAsync(
|
||||
predicate,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
new Dictionary<string, bool> { [nameof(XhsAgent.Id)] = false });
|
||||
var total = await AgentRepository.CountAsync(predicate);
|
||||
|
||||
return new OmsPageResult<XhsAgentOutput>
|
||||
{
|
||||
Items = items.Select(MapAgent).ToList(),
|
||||
Total = total
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/agent/control")]
|
||||
public async Task<XhsAgentControlOutput> ControlAsync(XhsAgentControlInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var agent = await GetOwnedAgentByIdAsync(input.AgentId, customerId);
|
||||
var task = XhsTaskFactory.CreateControlTask(customerId, agent.Id, input.Action.Trim());
|
||||
await TaskRepository.InsertAsync(task);
|
||||
return new XhsAgentControlOutput { TaskId = task.Id };
|
||||
}
|
||||
|
||||
private async Task<int> RequireCustomerIdAsync()
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
if (!customerId.HasValue)
|
||||
{
|
||||
throw new BusinessException("请使用客户账号操作,或指定客户上下文");
|
||||
}
|
||||
|
||||
return customerId.Value;
|
||||
}
|
||||
|
||||
private async Task<XhsAgent> GetOwnedAgentByDeviceAsync(string deviceId, int customerId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
throw new BusinessException("deviceId 不能为空");
|
||||
}
|
||||
|
||||
var agent = await AgentRepository.GetByDeviceIdAsync(deviceId);
|
||||
if (agent == null || agent.CustomerId != customerId)
|
||||
{
|
||||
throw new BusinessException("Agent 设备不存在");
|
||||
}
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
private async Task<XhsAgent> GetOwnedAgentByIdAsync(int agentId, int customerId)
|
||||
{
|
||||
var agent = await AgentRepository.FirstOrDefaultAsync(x => x.Id == agentId);
|
||||
if (agent == null || agent.CustomerId != customerId)
|
||||
{
|
||||
throw new BusinessException("Agent 不存在");
|
||||
}
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
private async Task<int> EnsureHumanConfigVersionAsync(int customerId)
|
||||
{
|
||||
var config = await HumanConfigRepository.GetByCustomerIdAsync(customerId);
|
||||
if (config != null)
|
||||
{
|
||||
return config.Version;
|
||||
}
|
||||
|
||||
config = new XhsHumanConfig
|
||||
{
|
||||
CustomerId = customerId,
|
||||
BehaviorJson = XhsHumanDefaults.BehaviorJson,
|
||||
GuardJson = XhsHumanDefaults.GuardJson,
|
||||
Version = 1
|
||||
};
|
||||
await HumanConfigRepository.InsertAsync(config);
|
||||
return config.Version;
|
||||
}
|
||||
|
||||
private static XhsAgentOutput MapAgent(XhsAgent agent)
|
||||
{
|
||||
var online = XhsHelper.IsAgentOnline(agent);
|
||||
return new XhsAgentOutput
|
||||
{
|
||||
Id = agent.Id,
|
||||
DeviceId = agent.DeviceId,
|
||||
DeviceName = agent.DeviceName,
|
||||
Status = online ? XhsAgentStatusCodes.Online : XhsAgentStatusCodes.Offline,
|
||||
EngineRunning = agent.EngineRunning,
|
||||
ExtensionConnected = agent.ExtensionConnected,
|
||||
XhsLoggedIn = agent.XhsLoggedIn,
|
||||
LastHeartbeatTime = agent.LastHeartbeatTime,
|
||||
AgentVersion = agent.AgentVersion,
|
||||
BridgePort = agent.BridgePort,
|
||||
LastError = agent.LastError
|
||||
};
|
||||
}
|
||||
|
||||
private static string? NormalizeOptional(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
137
src/Oms.Application/Services/Xhs/XhsAiService.cs
Normal file
137
src/Oms.Application/Services/Xhs/XhsAiService.cs
Normal file
@ -0,0 +1,137 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Dtos.Xhs;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
using Oms.Domain.XhsAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Services.Xhs;
|
||||
|
||||
[ApiExplorerSettings(GroupName = "OMS-Xhs")]
|
||||
public class XhsAiService : ApplicationService
|
||||
{
|
||||
private const string SecretMask = "******";
|
||||
|
||||
private IXhsAiProviderRepository AiProviderRepository => LazyServiceProvider.GetRequiredService<IXhsAiProviderRepository>();
|
||||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||||
|
||||
[HttpPost("/xhs/ai/provider/list")]
|
||||
public async Task<OmsItemsResult<XhsAiProviderOutput>> ListProvidersAsync()
|
||||
{
|
||||
var customerId = await ResolveAiCustomerIdAsync();
|
||||
var items = await AiProviderRepository.GetListAsync(x => x.CustomerId == customerId || x.CustomerId == 0);
|
||||
return new OmsItemsResult<XhsAiProviderOutput>
|
||||
{
|
||||
Items = items.Select(x => MapProvider(x, maskSecret: true)).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/ai/provider/save")]
|
||||
public async Task<XhsAiProviderOutput> SaveProviderAsync(XhsAiProviderSaveInput input)
|
||||
{
|
||||
var customerId = await ResolveAiCustomerIdAsync();
|
||||
var providerKey = input.ProviderKey.Trim();
|
||||
var entity = await AiProviderRepository.GetByProviderKeyAsync(customerId, providerKey);
|
||||
if (entity == null)
|
||||
{
|
||||
entity = new XhsAiProvider { CustomerId = customerId, ProviderKey = providerKey };
|
||||
}
|
||||
|
||||
entity.ProviderType = input.ProviderType.Trim();
|
||||
entity.DisplayName = input.DisplayName.Trim();
|
||||
entity.BaseUrl = input.BaseUrl.Trim();
|
||||
entity.IsDefault = input.IsDefault;
|
||||
entity.Enabled = input.Enabled;
|
||||
|
||||
if (!ShouldKeepExistingSecret(input.ApiKey))
|
||||
{
|
||||
entity.ApiKeyEnc = NormalizeOptional(input.ApiKey);
|
||||
}
|
||||
|
||||
if (!ShouldKeepExistingSecret(input.ApiSecret))
|
||||
{
|
||||
entity.ApiSecretEnc = NormalizeOptional(input.ApiSecret);
|
||||
}
|
||||
|
||||
if (entity.Id > 0)
|
||||
{
|
||||
await AiProviderRepository.UpdateAsync(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
await AiProviderRepository.InsertAsync(entity);
|
||||
}
|
||||
|
||||
if (input.IsDefault)
|
||||
{
|
||||
var others = await AiProviderRepository.GetListAsync(x =>
|
||||
x.CustomerId == customerId && x.Id != entity.Id && x.IsDefault);
|
||||
foreach (var other in others)
|
||||
{
|
||||
other.IsDefault = false;
|
||||
await AiProviderRepository.UpdateAsync(other);
|
||||
}
|
||||
}
|
||||
|
||||
return MapProvider(entity, maskSecret: true);
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/ai/chat")]
|
||||
public async Task<XhsAiChatOutput> ChatAsync(XhsAiChatInput input)
|
||||
{
|
||||
var customerId = await ResolveAiCustomerIdAsync();
|
||||
var provider = await ResolveProviderAsync(customerId, input.ProviderKey.Trim());
|
||||
var llm = LazyServiceProvider.GetRequiredService<XhsLlmClient>();
|
||||
var content = await llm.ChatCompletionAsync(
|
||||
provider,
|
||||
input.Messages.Select(x => (x.Role, x.Content)).ToList(),
|
||||
input.Model);
|
||||
return new XhsAiChatOutput { Content = content };
|
||||
}
|
||||
|
||||
internal async Task<XhsAiProvider> ResolveProviderAsync(int customerId, string providerKey)
|
||||
{
|
||||
var provider = await AiProviderRepository.GetByProviderKeyAsync(customerId, providerKey);
|
||||
if (provider == null || !provider.Enabled)
|
||||
{
|
||||
provider = await AiProviderRepository.GetByProviderKeyAsync(0, providerKey);
|
||||
}
|
||||
|
||||
if (provider == null || !provider.Enabled)
|
||||
{
|
||||
throw new BusinessException($"AI 提供商 {providerKey} 未配置或未启用");
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
private async Task<int> ResolveAiCustomerIdAsync()
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
return customerId ?? 0;
|
||||
}
|
||||
|
||||
private static XhsAiProviderOutput MapProvider(XhsAiProvider entity, bool maskSecret) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
ProviderKey = entity.ProviderKey,
|
||||
ProviderType = entity.ProviderType,
|
||||
DisplayName = entity.DisplayName,
|
||||
BaseUrl = entity.BaseUrl,
|
||||
ApiKey = maskSecret ? MaskSecret(entity.ApiKeyEnc) : entity.ApiKeyEnc,
|
||||
ApiSecret = maskSecret ? MaskSecret(entity.ApiSecretEnc) : entity.ApiSecretEnc,
|
||||
IsDefault = entity.IsDefault,
|
||||
Enabled = entity.Enabled
|
||||
};
|
||||
|
||||
private static string? MaskSecret(string? secret) =>
|
||||
string.IsNullOrWhiteSpace(secret) ? secret : SecretMask;
|
||||
|
||||
private static bool ShouldKeepExistingSecret(string? secret) =>
|
||||
string.IsNullOrWhiteSpace(secret) || string.Equals(secret.Trim(), SecretMask, StringComparison.Ordinal);
|
||||
|
||||
private static string? NormalizeOptional(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
276
src/Oms.Application/Services/Xhs/XhsAnalysisService.cs
Normal file
276
src/Oms.Application/Services/Xhs/XhsAnalysisService.cs
Normal file
@ -0,0 +1,276 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Dtos.Xhs;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.XhsAggregate.Consts;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
using Oms.Domain.XhsAggregate.Repositories;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Oms.Application.Services.Xhs;
|
||||
|
||||
[ApiExplorerSettings(GroupName = "OMS-Xhs")]
|
||||
public class XhsAnalysisService : ApplicationService
|
||||
{
|
||||
private IXhsAnalysisSessionRepository AnalysisSessionRepository => LazyServiceProvider.GetRequiredService<IXhsAnalysisSessionRepository>();
|
||||
private IXhsTaskRepository TaskRepository => LazyServiceProvider.GetRequiredService<IXhsTaskRepository>();
|
||||
private IXhsSkillRepository SkillRepository => LazyServiceProvider.GetRequiredService<IXhsSkillRepository>();
|
||||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||||
|
||||
[HttpPost("/xhs/analysis/start")]
|
||||
public async Task<XhsAnalysisStartOutput> StartAsync(XhsAnalysisStartInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
return await StartSessionAsync(customerId, input);
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/analysis/list")]
|
||||
public async Task<OmsPageResult<XhsAnalysisSessionOutput>> ListAsync(XhsAnalysisListInput input)
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
||||
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
||||
|
||||
Expression<Func<XhsAnalysisSession, bool>> predicate = x =>
|
||||
!customerId.HasValue || x.CustomerId == customerId.Value;
|
||||
|
||||
var items = await AnalysisSessionRepository.GetPagedListAsync(
|
||||
predicate,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
new Dictionary<string, bool> { [nameof(XhsAnalysisSession.CreateTime)] = false });
|
||||
var total = await AnalysisSessionRepository.CountAsync(predicate);
|
||||
|
||||
return new OmsPageResult<XhsAnalysisSessionOutput>
|
||||
{
|
||||
Items = items.Select(MapSession).ToList(),
|
||||
Total = total
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/analysis/detail")]
|
||||
public async Task<XhsAnalysisDetailOutput> DetailAsync(XhsAnalysisDetailInput input)
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
var session = await AnalysisSessionRepository.FirstOrDefaultAsync(x => x.Id == input.SessionId);
|
||||
if (session == null || (customerId.HasValue && session.CustomerId != customerId.Value))
|
||||
{
|
||||
throw new BusinessException("分析会话不存在");
|
||||
}
|
||||
|
||||
string? taskStatus = null;
|
||||
if (session.TaskId is > 0)
|
||||
{
|
||||
var task = await TaskRepository.FirstOrDefaultAsync(x => x.Id == session.TaskId);
|
||||
taskStatus = task?.Status;
|
||||
}
|
||||
|
||||
return new XhsAnalysisDetailOutput
|
||||
{
|
||||
Id = session.Id,
|
||||
SessionName = session.SessionName,
|
||||
AnalysisType = session.AnalysisType,
|
||||
Keyword = session.Keyword,
|
||||
TaskId = session.TaskId,
|
||||
CreateTime = session.CreateTime,
|
||||
Feeds = OmsDocHelper.DeserializeJsonObject(session.FeedsJson),
|
||||
AiAnalysisMarkdown = session.AiAnalysisMarkdown,
|
||||
TaskStatus = taskStatus
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/analysis/run-ai")]
|
||||
public async Task<XhsAnalysisRunAiOutput> RunAiAsync(XhsAnalysisRunAiInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var session = await AnalysisSessionRepository.FirstOrDefaultAsync(x => x.Id == input.SessionId);
|
||||
if (session == null || session.CustomerId != customerId)
|
||||
{
|
||||
throw new BusinessException("分析会话不存在");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(session.FeedsJson))
|
||||
{
|
||||
throw new BusinessException("请先完成笔记采集");
|
||||
}
|
||||
|
||||
var aiService = LazyServiceProvider.GetRequiredService<XhsAiService>();
|
||||
var provider = await aiService.ResolveProviderAsync(customerId, input.ProviderKey.Trim());
|
||||
var llm = LazyServiceProvider.GetRequiredService<XhsLlmClient>();
|
||||
|
||||
var skillHint = string.Empty;
|
||||
if (input.SkillId is > 0)
|
||||
{
|
||||
var skill = await SkillRepository.FirstOrDefaultAsync(x => x.Id == input.SkillId);
|
||||
skillHint = skill == null ? string.Empty : XhsHelper.ExtractRecipeNotes(skill.RecipeJson) ?? skill.Title;
|
||||
}
|
||||
|
||||
var feedsText = XhsHelper.SerializeFeedsForPrompt(OmsDocHelper.DeserializeJsonObject(session.FeedsJson));
|
||||
var userPrompt =
|
||||
"你是小红书内容分析助手。请基于以下采集到的笔记数据,输出 Markdown 格式的分析报告,包含:\n" +
|
||||
"1. 爆款共性(标题、封面、结构)\n" +
|
||||
"2. 可借鉴的选题角度\n" +
|
||||
"3. 3 条可执行的创作建议\n\n" +
|
||||
$"会话类型:{session.AnalysisType}\n" +
|
||||
$"关键词:{session.Keyword ?? "无"}\n" +
|
||||
$"技能参考:{skillHint}\n\n" +
|
||||
$"采集数据:\n{feedsText}";
|
||||
|
||||
var markdown = await llm.ChatCompletionAsync(
|
||||
provider,
|
||||
[("system", "你擅长小红书爆款分析与内容策划。"), ("user", userPrompt)]);
|
||||
|
||||
session.AiAnalysisMarkdown = markdown;
|
||||
await AnalysisSessionRepository.UpdateAsync(session);
|
||||
|
||||
return new XhsAnalysisRunAiOutput { Markdown = markdown };
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/analysis/interact")]
|
||||
public async Task<XhsAnalysisInteractOutput> InteractAsync(XhsAnalysisInteractInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var (toolName, args) = BuildInteractTask(input);
|
||||
|
||||
var task = new XhsTask
|
||||
{
|
||||
CustomerId = customerId,
|
||||
AgentId = input.AgentId,
|
||||
TaskType = XhsTaskTypeCodes.Interact,
|
||||
ToolName = toolName,
|
||||
ArgumentsJson = OmsDocHelper.SerializeJson(args) ?? "{}",
|
||||
Source = XhsTaskSourceCodes.Web,
|
||||
SourceRefId = input.FeedId,
|
||||
Status = XhsTaskStatusCodes.Pending,
|
||||
Priority = 4
|
||||
};
|
||||
await TaskRepository.InsertAsync(task);
|
||||
|
||||
return new XhsAnalysisInteractOutput { TaskId = task.Id };
|
||||
}
|
||||
|
||||
internal async Task<XhsAnalysisStartOutput> StartSessionAsync(int customerId, XhsAnalysisStartInput input)
|
||||
{
|
||||
var analysisType = input.AnalysisType.Trim();
|
||||
var (taskType, toolName, args) = BuildBrowseTask(analysisType, input);
|
||||
|
||||
var session = new XhsAnalysisSession
|
||||
{
|
||||
CustomerId = customerId,
|
||||
SessionName = BuildSessionName(analysisType, input.Keyword),
|
||||
AnalysisType = analysisType,
|
||||
Keyword = NormalizeOptional(input.Keyword),
|
||||
SkillId = input.SkillId
|
||||
};
|
||||
await AnalysisSessionRepository.InsertAsync(session);
|
||||
|
||||
var task = new XhsTask
|
||||
{
|
||||
CustomerId = customerId,
|
||||
AgentId = input.AgentId,
|
||||
TaskType = taskType,
|
||||
ToolName = toolName,
|
||||
ArgumentsJson = OmsDocHelper.SerializeJson(args) ?? "{}",
|
||||
Source = XhsTaskSourceCodes.Web,
|
||||
SourceRefId = session.Id.ToString(),
|
||||
Status = XhsTaskStatusCodes.Pending,
|
||||
Priority = 3
|
||||
};
|
||||
await TaskRepository.InsertAsync(task);
|
||||
|
||||
session.TaskId = task.Id;
|
||||
await AnalysisSessionRepository.UpdateAsync(session);
|
||||
|
||||
return new XhsAnalysisStartOutput
|
||||
{
|
||||
SessionId = session.Id,
|
||||
TaskId = task.Id
|
||||
};
|
||||
}
|
||||
|
||||
private static (string ToolName, object Args) BuildInteractTask(XhsAnalysisInteractInput input)
|
||||
{
|
||||
var action = input.Action.Trim().ToLowerInvariant();
|
||||
return action switch
|
||||
{
|
||||
"post_comment" => ("post_comment_to_feed", new
|
||||
{
|
||||
feed_id = input.FeedId,
|
||||
xsec_token = input.XsecToken ?? string.Empty,
|
||||
content = input.Content ?? string.Empty
|
||||
}),
|
||||
"like" => ("like_feed", new
|
||||
{
|
||||
feed_id = input.FeedId,
|
||||
xsec_token = input.XsecToken ?? string.Empty
|
||||
}),
|
||||
"favorite" => ("favorite_feed", new
|
||||
{
|
||||
feed_id = input.FeedId,
|
||||
xsec_token = input.XsecToken ?? string.Empty
|
||||
}),
|
||||
_ => throw new BusinessException($"不支持的互动动作: {input.Action}")
|
||||
};
|
||||
}
|
||||
|
||||
private static (string TaskType, string ToolName, object Args) BuildBrowseTask(
|
||||
string analysisType,
|
||||
XhsAnalysisStartInput input)
|
||||
{
|
||||
return analysisType switch
|
||||
{
|
||||
XhsAnalysisTypeCodes.HomeFeed => (
|
||||
XhsTaskTypeCodes.Browse,
|
||||
"list_feeds",
|
||||
new { }),
|
||||
XhsAnalysisTypeCodes.Search => (
|
||||
XhsTaskTypeCodes.Browse,
|
||||
"search_feeds",
|
||||
new
|
||||
{
|
||||
keyword = input.Keyword ?? string.Empty,
|
||||
filters = input.Filters
|
||||
}),
|
||||
XhsAnalysisTypeCodes.MyFeeds => (
|
||||
XhsTaskTypeCodes.Browse,
|
||||
"my_account_feeds",
|
||||
new { }),
|
||||
_ => throw new BusinessException($"不支持的 analysisType: {analysisType}")
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildSessionName(string analysisType, string? keyword) =>
|
||||
analysisType switch
|
||||
{
|
||||
XhsAnalysisTypeCodes.Search => $"搜索:{keyword}",
|
||||
XhsAnalysisTypeCodes.HomeFeed => "首页推荐",
|
||||
XhsAnalysisTypeCodes.MyFeeds => "我的笔记",
|
||||
_ => analysisType
|
||||
};
|
||||
|
||||
private async Task<int> RequireCustomerIdAsync()
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
if (!customerId.HasValue)
|
||||
{
|
||||
throw new BusinessException("请使用客户账号操作");
|
||||
}
|
||||
|
||||
return customerId.Value;
|
||||
}
|
||||
|
||||
private static XhsAnalysisSessionOutput MapSession(XhsAnalysisSession session) => new()
|
||||
{
|
||||
Id = session.Id,
|
||||
SessionName = session.SessionName,
|
||||
AnalysisType = session.AnalysisType,
|
||||
Keyword = session.Keyword,
|
||||
TaskId = session.TaskId,
|
||||
CreateTime = session.CreateTime
|
||||
};
|
||||
|
||||
private static string? NormalizeOptional(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
130
src/Oms.Application/Services/Xhs/XhsCreationService.cs
Normal file
130
src/Oms.Application/Services/Xhs/XhsCreationService.cs
Normal file
@ -0,0 +1,130 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Xhs;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.XhsAggregate.Consts;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
using Oms.Domain.XhsAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Services.Xhs;
|
||||
|
||||
[ApiExplorerSettings(GroupName = "OMS-Xhs")]
|
||||
public class XhsCreationService : ApplicationService
|
||||
{
|
||||
private IXhsPublishRecordRepository PublishRecordRepository => LazyServiceProvider.GetRequiredService<IXhsPublishRecordRepository>();
|
||||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||||
|
||||
[HttpPost("/xhs/creation/publish")]
|
||||
public async Task<XhsCreationPublishOutput> PublishAsync(XhsCreationPublishInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var toolName = string.IsNullOrWhiteSpace(input.ToolName) ? "publish_content" : input.ToolName.Trim();
|
||||
var scheduled = input.ScheduledTime.HasValue && input.ScheduledTime.Value > DateTime.Now;
|
||||
|
||||
var record = new XhsPublishRecord
|
||||
{
|
||||
CustomerId = customerId,
|
||||
ToolName = toolName,
|
||||
Title = input.Title.Trim(),
|
||||
ContentFull = input.Content.Trim(),
|
||||
ContentPreview = XhsHelper.BuildContentPreview(input.Content),
|
||||
MediaJson = OmsDocHelper.SerializeJson(input.MediaUrls),
|
||||
TagsJson = OmsDocHelper.SerializeJson(input.Tags),
|
||||
Visibility = string.IsNullOrWhiteSpace(input.Visibility) ? "公开可见" : input.Visibility.Trim(),
|
||||
ScheduledTime = input.ScheduledTime,
|
||||
SkillId = input.SkillId,
|
||||
Status = scheduled ? XhsPublishStatusCodes.Scheduled : XhsPublishStatusCodes.Draft
|
||||
};
|
||||
await PublishRecordRepository.InsertAsync(record);
|
||||
|
||||
int? taskId = null;
|
||||
if (!scheduled)
|
||||
{
|
||||
var publishService = LazyServiceProvider.GetRequiredService<XhsPublishService>();
|
||||
var task = await publishService.CreatePublishTaskAsync(record, input.AgentId);
|
||||
taskId = task.Id;
|
||||
record.TaskId = task.Id;
|
||||
record.Status = XhsPublishStatusCodes.Publishing;
|
||||
await PublishRecordRepository.UpdateAsync(record);
|
||||
}
|
||||
|
||||
return new XhsCreationPublishOutput
|
||||
{
|
||||
PublishRecordId = record.Id,
|
||||
TaskId = taskId
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/creation/generate")]
|
||||
public async Task<XhsCreationGenerateOutput> GenerateAsync(XhsCreationGenerateInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var aiService = LazyServiceProvider.GetRequiredService<XhsAiService>();
|
||||
var provider = await aiService.ResolveProviderAsync(customerId, input.ProviderKey.Trim());
|
||||
var llm = LazyServiceProvider.GetRequiredService<XhsLlmClient>();
|
||||
|
||||
var skillHint = string.Empty;
|
||||
if (input.SkillId is > 0)
|
||||
{
|
||||
var skillRepo = LazyServiceProvider.GetRequiredService<IXhsSkillRepository>();
|
||||
var skill = await skillRepo.FirstOrDefaultAsync(x => x.Id == input.SkillId);
|
||||
if (skill != null)
|
||||
{
|
||||
skillHint = XhsHelper.ExtractRecipeNotes(skill.RecipeJson) ?? skill.Title;
|
||||
}
|
||||
}
|
||||
|
||||
var userPrompt =
|
||||
"请为小红书创作一篇笔记,严格按 JSON 返回,不要 markdown 代码块:\n" +
|
||||
"{\"title\":\"标题20字内\",\"content\":\"正文800字内,分段\",\"tags\":[\"标签1\",\"标签2\"]}\n\n" +
|
||||
$"主题:{input.Topic.Trim()}\n" +
|
||||
$"补充说明:{input.ExtraNotes ?? skillHint}";
|
||||
|
||||
var raw = await llm.ChatCompletionAsync(
|
||||
provider,
|
||||
[("system", "你是小红书爆款创作助手,输出必须是合法 JSON。"), ("user", userPrompt)]);
|
||||
|
||||
var parsed = OmsDocHelper.DeserializeJson<Dictionary<string, object>>(ExtractJsonObject(raw));
|
||||
if (parsed == null)
|
||||
{
|
||||
throw new BusinessException("AI 返回无法解析,请重试");
|
||||
}
|
||||
|
||||
return new XhsCreationGenerateOutput
|
||||
{
|
||||
Title = parsed.TryGetValue("title", out var title) ? title?.ToString() ?? input.Topic : input.Topic,
|
||||
Content = parsed.TryGetValue("content", out var content) ? content?.ToString() ?? raw : raw,
|
||||
Tags = ParseStringList(parsed.GetValueOrDefault("tags"))
|
||||
};
|
||||
}
|
||||
|
||||
private static List<string> ParseStringList(object? value)
|
||||
{
|
||||
var json = OmsDocHelper.SerializeJson(value);
|
||||
return OmsDocHelper.DeserializeStringList(json) ?? [];
|
||||
}
|
||||
|
||||
private static string ExtractJsonObject(string text)
|
||||
{
|
||||
var start = text.IndexOf('{');
|
||||
var end = text.LastIndexOf('}');
|
||||
if (start >= 0 && end > start)
|
||||
{
|
||||
return text[start..(end + 1)];
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private async Task<int> RequireCustomerIdAsync()
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
if (!customerId.HasValue)
|
||||
{
|
||||
throw new BusinessException("请使用客户账号操作");
|
||||
}
|
||||
|
||||
return customerId.Value;
|
||||
}
|
||||
}
|
||||
105
src/Oms.Application/Services/Xhs/XhsHumanConfigService.cs
Normal file
105
src/Oms.Application/Services/Xhs/XhsHumanConfigService.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Xhs;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
using Oms.Domain.XhsAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Services.Xhs;
|
||||
|
||||
[ApiExplorerSettings(GroupName = "OMS-Xhs")]
|
||||
public class XhsHumanConfigService : ApplicationService
|
||||
{
|
||||
private IXhsHumanConfigRepository HumanConfigRepository => LazyServiceProvider.GetRequiredService<IXhsHumanConfigRepository>();
|
||||
private IXhsAgentRepository AgentRepository => LazyServiceProvider.GetRequiredService<IXhsAgentRepository>();
|
||||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||||
|
||||
[HttpPost("/xhs/human-config/get")]
|
||||
public async Task<XhsHumanConfigOutput> GetAsync()
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var config = await GetOrCreateConfigAsync(customerId);
|
||||
return MapOutput(config);
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/human-config/save")]
|
||||
public async Task<XhsHumanConfigOutput> SaveAsync(XhsHumanConfigSaveInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var config = await GetOrCreateConfigAsync(customerId);
|
||||
config.BehaviorJson = OmsDocHelper.SerializeJson(input.Behavior) ?? XhsHumanDefaults.BehaviorJson;
|
||||
config.GuardJson = OmsDocHelper.SerializeJson(input.Guard) ?? XhsHumanDefaults.GuardJson;
|
||||
config.Version += 1;
|
||||
await HumanConfigRepository.UpdateAsync(config);
|
||||
return MapOutput(config);
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/human-config/pull")]
|
||||
public async Task<XhsHumanConfigPullOutput> PullAsync(XhsHumanConfigPullInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
_ = await GetOwnedAgentByDeviceAsync(input.DeviceId.Trim(), customerId);
|
||||
var config = await GetOrCreateConfigAsync(customerId);
|
||||
if (config.Version == input.KnownVersion)
|
||||
{
|
||||
return new XhsHumanConfigPullOutput { Unchanged = true, Version = config.Version };
|
||||
}
|
||||
|
||||
return new XhsHumanConfigPullOutput
|
||||
{
|
||||
Unchanged = false,
|
||||
Behavior = OmsDocHelper.DeserializeJsonObject(config.BehaviorJson),
|
||||
Guard = OmsDocHelper.DeserializeJsonObject(config.GuardJson),
|
||||
Version = config.Version
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<XhsHumanConfig> GetOrCreateConfigAsync(int customerId)
|
||||
{
|
||||
var config = await HumanConfigRepository.GetByCustomerIdAsync(customerId);
|
||||
if (config != null)
|
||||
{
|
||||
return config;
|
||||
}
|
||||
|
||||
config = new XhsHumanConfig
|
||||
{
|
||||
CustomerId = customerId,
|
||||
BehaviorJson = XhsHumanDefaults.BehaviorJson,
|
||||
GuardJson = XhsHumanDefaults.GuardJson,
|
||||
Version = 1
|
||||
};
|
||||
await HumanConfigRepository.InsertAsync(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
private async Task<int> RequireCustomerIdAsync()
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
if (!customerId.HasValue)
|
||||
{
|
||||
throw new BusinessException("请使用客户账号操作");
|
||||
}
|
||||
|
||||
return customerId.Value;
|
||||
}
|
||||
|
||||
private async Task<XhsAgent> GetOwnedAgentByDeviceAsync(string deviceId, int customerId)
|
||||
{
|
||||
var agent = await AgentRepository.GetByDeviceIdAsync(deviceId);
|
||||
if (agent == null || agent.CustomerId != customerId)
|
||||
{
|
||||
throw new BusinessException("Agent 设备不存在");
|
||||
}
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
private static XhsHumanConfigOutput MapOutput(XhsHumanConfig config) => new()
|
||||
{
|
||||
Behavior = OmsDocHelper.DeserializeJsonObject(config.BehaviorJson),
|
||||
Guard = OmsDocHelper.DeserializeJsonObject(config.GuardJson),
|
||||
Version = config.Version
|
||||
};
|
||||
}
|
||||
71
src/Oms.Application/Services/Xhs/XhsMediaService.cs
Normal file
71
src/Oms.Application/Services/Xhs/XhsMediaService.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Xhs;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
using Oms.Domain.XhsAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Services.Xhs;
|
||||
|
||||
[ApiExplorerSettings(GroupName = "OMS-Xhs")]
|
||||
public class XhsMediaService : ApplicationService
|
||||
{
|
||||
private IXhsMediaRepository MediaRepository => LazyServiceProvider.GetRequiredService<IXhsMediaRepository>();
|
||||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||||
private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService<IWebHostEnvironment>();
|
||||
|
||||
[HttpPost("/xhs/media/upload")]
|
||||
public async Task<XhsMediaUploadOutput> UploadAsync(IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
throw new BusinessException("请选择文件");
|
||||
}
|
||||
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var uploadsRoot = Path.Combine(WebHostEnvironment.WebRootPath ?? "wwwroot", "uploads", "xhs", customerId.ToString());
|
||||
Directory.CreateDirectory(uploadsRoot);
|
||||
|
||||
var ext = Path.GetExtension(file.FileName);
|
||||
var storedName = $"{Guid.NewGuid():N}{ext}";
|
||||
var fullPath = Path.Combine(uploadsRoot, storedName);
|
||||
await using (var stream = File.Create(fullPath))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
var relativePath = Path.Combine("uploads", "xhs", customerId.ToString(), storedName).Replace('\\', '/');
|
||||
var publicUrl = $"/{relativePath}";
|
||||
|
||||
var media = new XhsMedia
|
||||
{
|
||||
CustomerId = customerId,
|
||||
FileName = file.FileName,
|
||||
StoragePath = relativePath,
|
||||
PublicUrl = publicUrl,
|
||||
MimeType = file.ContentType,
|
||||
FileSizeBytes = file.Length
|
||||
};
|
||||
await MediaRepository.InsertAsync(media);
|
||||
|
||||
return new XhsMediaUploadOutput
|
||||
{
|
||||
MediaId = media.Id,
|
||||
PublicUrl = publicUrl
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<int> RequireCustomerIdAsync()
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
if (!customerId.HasValue)
|
||||
{
|
||||
throw new BusinessException("请使用客户账号操作");
|
||||
}
|
||||
|
||||
return customerId.Value;
|
||||
}
|
||||
}
|
||||
163
src/Oms.Application/Services/Xhs/XhsPublishService.cs
Normal file
163
src/Oms.Application/Services/Xhs/XhsPublishService.cs
Normal file
@ -0,0 +1,163 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Dtos.Xhs;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.XhsAggregate.Consts;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
using Oms.Domain.XhsAggregate.Repositories;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Oms.Application.Services.Xhs;
|
||||
|
||||
[ApiExplorerSettings(GroupName = "OMS-Xhs")]
|
||||
public class XhsPublishService : ApplicationService
|
||||
{
|
||||
private IXhsPublishRecordRepository PublishRecordRepository => LazyServiceProvider.GetRequiredService<IXhsPublishRecordRepository>();
|
||||
private IXhsTaskRepository TaskRepository => LazyServiceProvider.GetRequiredService<IXhsTaskRepository>();
|
||||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||||
|
||||
[HttpPost("/xhs/publish/list")]
|
||||
public async Task<OmsPageResult<XhsPublishRecordOutput>> ListAsync(XhsPublishListInput input)
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
||||
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
||||
var status = input.Status?.Trim();
|
||||
var keyword = input.Keyword?.Trim();
|
||||
|
||||
Expression<Func<XhsPublishRecord, bool>> predicate = x =>
|
||||
(!customerId.HasValue || x.CustomerId == customerId.Value) &&
|
||||
(string.IsNullOrWhiteSpace(status) || x.Status == status) &&
|
||||
(string.IsNullOrWhiteSpace(keyword) || x.Title.Contains(keyword));
|
||||
|
||||
var items = await PublishRecordRepository.GetPagedListAsync(
|
||||
predicate,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
new Dictionary<string, bool> { [nameof(XhsPublishRecord.CreateTime)] = false });
|
||||
var total = await PublishRecordRepository.CountAsync(predicate);
|
||||
|
||||
return new OmsPageResult<XhsPublishRecordOutput>
|
||||
{
|
||||
Items = items.Select(MapRecord).ToList(),
|
||||
Total = total
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/publish/cancel")]
|
||||
public async Task CancelAsync(XhsPublishCancelInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var record = await GetOwnedRecordAsync(input.PublishRecordId, customerId);
|
||||
if (record.Status != XhsPublishStatusCodes.Scheduled)
|
||||
{
|
||||
throw new BusinessException("仅定时发布可取消");
|
||||
}
|
||||
|
||||
record.Status = XhsPublishStatusCodes.Cancelled;
|
||||
await PublishRecordRepository.UpdateAsync(record);
|
||||
|
||||
if (record.TaskId is > 0)
|
||||
{
|
||||
var task = await TaskRepository.FirstOrDefaultAsync(x => x.Id == record.TaskId);
|
||||
if (task != null && task.Status is XhsTaskStatusCodes.Pending or XhsTaskStatusCodes.Claimed)
|
||||
{
|
||||
task.Status = XhsTaskStatusCodes.Cancelled;
|
||||
task.FinishedAt = DateTime.Now;
|
||||
await TaskRepository.UpdateAsync(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/publish/retry")]
|
||||
public async Task<XhsCreationPublishOutput> RetryAsync(XhsPublishRetryInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var record = await GetOwnedRecordAsync(input.PublishRecordId, customerId);
|
||||
if (record.Status != XhsPublishStatusCodes.Failed)
|
||||
{
|
||||
throw new BusinessException("仅失败记录可重试");
|
||||
}
|
||||
|
||||
var task = await CreatePublishTaskAsync(record, input.AgentId);
|
||||
record.Status = XhsPublishStatusCodes.Publishing;
|
||||
record.TaskId = task.Id;
|
||||
record.Success = null;
|
||||
record.ResultSummary = null;
|
||||
await PublishRecordRepository.UpdateAsync(record);
|
||||
|
||||
return new XhsCreationPublishOutput
|
||||
{
|
||||
PublishRecordId = record.Id,
|
||||
TaskId = task.Id
|
||||
};
|
||||
}
|
||||
|
||||
internal async Task<XhsTask> CreatePublishTaskAsync(
|
||||
XhsPublishRecord record,
|
||||
int? agentId,
|
||||
string? source = null)
|
||||
{
|
||||
var args = new Dictionary<string, object?>
|
||||
{
|
||||
["title"] = record.Title,
|
||||
["content"] = record.ContentFull ?? record.ContentPreview ?? string.Empty,
|
||||
["images"] = OmsDocHelper.DeserializeStringList(record.MediaJson),
|
||||
["tags"] = OmsDocHelper.DeserializeStringList(record.TagsJson),
|
||||
["visibility"] = record.Visibility ?? "公开可见"
|
||||
};
|
||||
|
||||
var task = new XhsTask
|
||||
{
|
||||
CustomerId = record.CustomerId,
|
||||
AgentId = agentId,
|
||||
TaskType = XhsTaskTypeCodes.Publish,
|
||||
ToolName = record.ToolName,
|
||||
ArgumentsJson = OmsDocHelper.SerializeJson(args) ?? "{}",
|
||||
Source = source ?? XhsTaskSourceCodes.Web,
|
||||
SourceRefId = record.Id.ToString(),
|
||||
Status = XhsTaskStatusCodes.Pending,
|
||||
Priority = 5
|
||||
};
|
||||
await TaskRepository.InsertAsync(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
private async Task<int> RequireCustomerIdAsync()
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
if (!customerId.HasValue)
|
||||
{
|
||||
throw new BusinessException("请使用客户账号操作");
|
||||
}
|
||||
|
||||
return customerId.Value;
|
||||
}
|
||||
|
||||
private async Task<XhsPublishRecord> GetOwnedRecordAsync(int id, int customerId)
|
||||
{
|
||||
var record = await PublishRecordRepository.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (record == null || record.CustomerId != customerId)
|
||||
{
|
||||
throw new BusinessException("发布记录不存在");
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
private static XhsPublishRecordOutput MapRecord(XhsPublishRecord record) => new()
|
||||
{
|
||||
Id = record.Id,
|
||||
ToolName = record.ToolName,
|
||||
Title = record.Title,
|
||||
ContentPreview = record.ContentPreview,
|
||||
Status = record.Status,
|
||||
Success = record.Success,
|
||||
ResultSummary = record.ResultSummary,
|
||||
ScheduledTime = record.ScheduledTime,
|
||||
PublishedTime = record.PublishedTime,
|
||||
CreateTime = record.CreateTime
|
||||
};
|
||||
}
|
||||
200
src/Oms.Application/Services/Xhs/XhsSkillService.cs
Normal file
200
src/Oms.Application/Services/Xhs/XhsSkillService.cs
Normal file
@ -0,0 +1,200 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Dtos.Xhs;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.XhsAggregate.Consts;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
using Oms.Domain.XhsAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Services.Xhs;
|
||||
|
||||
[ApiExplorerSettings(GroupName = "OMS-Xhs")]
|
||||
public class XhsSkillService : ApplicationService
|
||||
{
|
||||
private IXhsSkillRepository SkillRepository => LazyServiceProvider.GetRequiredService<IXhsSkillRepository>();
|
||||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||||
|
||||
[HttpPost("/xhs/skill/list")]
|
||||
public async Task<OmsItemsResult<XhsSkillOutput>> ListAsync(XhsSkillListInput input)
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, input.CustomerId);
|
||||
var listCustomerId = customerId ?? 0;
|
||||
|
||||
var items = await SkillRepository.GetListAsync(x =>
|
||||
x.CustomerId == listCustomerId || x.CustomerId == 0);
|
||||
items = items.OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToList();
|
||||
|
||||
return new OmsItemsResult<XhsSkillOutput>
|
||||
{
|
||||
Items = items.Select(MapSkill).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/skill/save")]
|
||||
public async Task<XhsSkillOutput> SaveAsync(XhsSkillSaveInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
XhsSkill entity;
|
||||
if (input.Id is > 0)
|
||||
{
|
||||
entity = await SkillRepository.FirstOrDefaultAsync(x => x.Id == input.Id.Value)
|
||||
?? throw new BusinessException("技能不存在");
|
||||
if (entity.CustomerId != customerId)
|
||||
{
|
||||
throw new BusinessException("无权修改该技能");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
entity = new XhsSkill
|
||||
{
|
||||
CustomerId = customerId,
|
||||
ExternalId = Guid.NewGuid().ToString("N")[..12]
|
||||
};
|
||||
}
|
||||
|
||||
entity.Title = input.Title.Trim();
|
||||
entity.SkillType = string.IsNullOrWhiteSpace(input.SkillType) ? "ai_create" : input.SkillType.Trim();
|
||||
entity.PresetId = string.IsNullOrWhiteSpace(input.PresetId) ? null : input.PresetId.Trim();
|
||||
entity.RecipeJson = OmsDocHelper.SerializeJson(input.Recipe) ?? "{}";
|
||||
|
||||
if (input.Id is > 0)
|
||||
{
|
||||
await SkillRepository.UpdateAsync(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SkillRepository.InsertAsync(entity);
|
||||
}
|
||||
|
||||
return MapSkill(entity);
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/skill/delete")]
|
||||
public async Task DeleteAsync(IdInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var entity = await SkillRepository.FirstOrDefaultAsync(x => x.Id == input.Id);
|
||||
if (entity == null || entity.CustomerId != customerId)
|
||||
{
|
||||
throw new BusinessException("技能不存在");
|
||||
}
|
||||
|
||||
await SkillRepository.DeleteAsync(entity);
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/skill/run")]
|
||||
public async Task<XhsSkillRunOutput> RunAsync(XhsSkillRunInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var entity = await SkillRepository.FirstOrDefaultAsync(x => x.Id == input.SkillId);
|
||||
if (entity == null || (entity.CustomerId != customerId && entity.CustomerId != 0))
|
||||
{
|
||||
throw new BusinessException("技能不存在");
|
||||
}
|
||||
|
||||
var runMode = string.IsNullOrWhiteSpace(input.RunMode) ? "analyze_only" : input.RunMode.Trim();
|
||||
var topic = input.TopicOverride?.Trim()
|
||||
?? XhsHelper.ExtractRecipeNotes(entity.RecipeJson)
|
||||
?? entity.Title;
|
||||
|
||||
if (runMode == "analyze_only")
|
||||
{
|
||||
var analysisService = LazyServiceProvider.GetRequiredService<XhsAnalysisService>();
|
||||
var started = await analysisService.StartSessionAsync(
|
||||
customerId,
|
||||
new XhsAnalysisStartInput
|
||||
{
|
||||
AnalysisType = XhsAnalysisTypeCodes.Search,
|
||||
Keyword = topic,
|
||||
AgentId = input.AgentId,
|
||||
SkillId = entity.Id
|
||||
});
|
||||
|
||||
return new XhsSkillRunOutput
|
||||
{
|
||||
SessionId = started.SessionId,
|
||||
TaskIds = [started.TaskId]
|
||||
};
|
||||
}
|
||||
|
||||
if (runMode is "create_only" or "create_and_publish")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.ProviderKey))
|
||||
{
|
||||
throw new BusinessException("create 模式需指定 providerKey");
|
||||
}
|
||||
|
||||
var creationService = LazyServiceProvider.GetRequiredService<XhsCreationService>();
|
||||
var generated = await creationService.GenerateAsync(new XhsCreationGenerateInput
|
||||
{
|
||||
SkillId = entity.Id,
|
||||
ProviderKey = input.ProviderKey.Trim(),
|
||||
Topic = topic,
|
||||
ExtraNotes = XhsHelper.ExtractRecipeNotes(entity.RecipeJson)
|
||||
});
|
||||
|
||||
if (runMode == "create_only")
|
||||
{
|
||||
return new XhsSkillRunOutput
|
||||
{
|
||||
GeneratedTitle = generated.Title,
|
||||
GeneratedContent = generated.Content,
|
||||
GeneratedTags = generated.Tags
|
||||
};
|
||||
}
|
||||
|
||||
var mediaUrls = input.MediaUrls ?? [];
|
||||
if (mediaUrls.Count == 0)
|
||||
{
|
||||
throw new BusinessException("create_and_publish 需至少上传一张图片");
|
||||
}
|
||||
|
||||
var published = await creationService.PublishAsync(new XhsCreationPublishInput
|
||||
{
|
||||
AgentId = input.AgentId,
|
||||
Title = generated.Title,
|
||||
Content = generated.Content,
|
||||
Tags = generated.Tags,
|
||||
MediaUrls = mediaUrls,
|
||||
SkillId = entity.Id
|
||||
});
|
||||
|
||||
return new XhsSkillRunOutput
|
||||
{
|
||||
GeneratedTitle = generated.Title,
|
||||
GeneratedContent = generated.Content,
|
||||
GeneratedTags = generated.Tags,
|
||||
PublishRecordId = published.PublishRecordId,
|
||||
TaskIds = published.TaskId is > 0 ? [published.TaskId.Value] : []
|
||||
};
|
||||
}
|
||||
|
||||
throw new BusinessException($"未知 runMode: {runMode}");
|
||||
}
|
||||
|
||||
private async Task<int> RequireCustomerIdAsync()
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
if (!customerId.HasValue)
|
||||
{
|
||||
throw new BusinessException("请使用客户账号操作");
|
||||
}
|
||||
|
||||
return customerId.Value;
|
||||
}
|
||||
|
||||
private static XhsSkillOutput MapSkill(XhsSkill skill) => new()
|
||||
{
|
||||
Id = skill.Id,
|
||||
CustomerId = skill.CustomerId,
|
||||
ExternalId = skill.ExternalId,
|
||||
Title = skill.Title,
|
||||
SkillType = skill.SkillType,
|
||||
PresetId = skill.PresetId,
|
||||
Recipe = OmsDocHelper.DeserializeJsonObject(skill.RecipeJson),
|
||||
SortOrder = skill.SortOrder
|
||||
};
|
||||
}
|
||||
274
src/Oms.Application/Services/Xhs/XhsTaskService.cs
Normal file
274
src/Oms.Application/Services/Xhs/XhsTaskService.cs
Normal file
@ -0,0 +1,274 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Dtos.Xhs;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.XhsAggregate.Consts;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
using Oms.Domain.XhsAggregate.Repositories;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Oms.Application.Services.Xhs;
|
||||
|
||||
[ApiExplorerSettings(GroupName = "OMS-Xhs")]
|
||||
public class XhsTaskService : ApplicationService
|
||||
{
|
||||
private IXhsAgentRepository AgentRepository => LazyServiceProvider.GetRequiredService<IXhsAgentRepository>();
|
||||
private IXhsTaskRepository TaskRepository => LazyServiceProvider.GetRequiredService<IXhsTaskRepository>();
|
||||
private IXhsHumanConfigRepository HumanConfigRepository => LazyServiceProvider.GetRequiredService<IXhsHumanConfigRepository>();
|
||||
private IXhsPublishRecordRepository PublishRecordRepository => LazyServiceProvider.GetRequiredService<IXhsPublishRecordRepository>();
|
||||
private IXhsAnalysisSessionRepository AnalysisSessionRepository => LazyServiceProvider.GetRequiredService<IXhsAnalysisSessionRepository>();
|
||||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||||
|
||||
[HttpPost("/xhs/task/claim")]
|
||||
public async Task<XhsTaskClaimOutput> ClaimAsync(XhsTaskClaimInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var agent = await GetOwnedAgentByDeviceAsync(input.DeviceId.Trim(), customerId);
|
||||
var limit = input.Limit <= 0 ? 1 : Math.Min(input.Limit, 5);
|
||||
var tasks = await TaskRepository.ClaimPendingAsync(customerId, agent.Id, limit);
|
||||
var humanVersion = await GetHumanConfigVersionAsync(customerId);
|
||||
|
||||
return new XhsTaskClaimOutput
|
||||
{
|
||||
Items = tasks.Select(x => new XhsTaskClaimItemOutput
|
||||
{
|
||||
Id = x.Id,
|
||||
TaskType = x.TaskType,
|
||||
ToolName = x.ToolName,
|
||||
Arguments = OmsDocHelper.DeserializeJsonObject(x.ArgumentsJson),
|
||||
HumanConfigVersion = humanVersion
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/task/report")]
|
||||
public async Task ReportAsync(XhsTaskReportInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var task = await TaskRepository.FirstOrDefaultAsync(x => x.Id == input.TaskId);
|
||||
if (task == null || task.CustomerId != customerId)
|
||||
{
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
|
||||
var status = input.Status.Trim().ToLowerInvariant();
|
||||
task.ProgressText = NormalizeOptional(input.ProgressText);
|
||||
task.ErrorMessage = NormalizeOptional(input.ErrorMessage);
|
||||
|
||||
if (status == XhsTaskStatusCodes.Running)
|
||||
{
|
||||
task.Status = XhsTaskStatusCodes.Running;
|
||||
task.StartedAt ??= DateTime.Now;
|
||||
}
|
||||
else if (status == XhsTaskStatusCodes.Succeeded)
|
||||
{
|
||||
task.Status = XhsTaskStatusCodes.Succeeded;
|
||||
task.ResultJson = OmsDocHelper.SerializeJson(input.Result);
|
||||
task.FinishedAt = DateTime.Now;
|
||||
}
|
||||
else if (status == XhsTaskStatusCodes.Failed)
|
||||
{
|
||||
task.Status = XhsTaskStatusCodes.Failed;
|
||||
task.FinishedAt = DateTime.Now;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new BusinessException("无效的任务状态");
|
||||
}
|
||||
|
||||
await TaskRepository.UpdateAsync(task);
|
||||
await SyncPublishRecordAsync(task, status, input);
|
||||
await SyncAnalysisSessionAsync(task, status, input);
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/task/list")]
|
||||
public async Task<OmsPageResult<XhsTaskOutput>> ListAsync(XhsTaskListInput input)
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
||||
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
||||
var status = input.Status?.Trim();
|
||||
var taskType = input.TaskType?.Trim();
|
||||
var sourceRefId = input.SourceRefId?.Trim();
|
||||
|
||||
Expression<Func<XhsTask, bool>> predicate = x =>
|
||||
(!customerId.HasValue || x.CustomerId == customerId.Value) &&
|
||||
(string.IsNullOrWhiteSpace(status) || x.Status == status) &&
|
||||
(string.IsNullOrWhiteSpace(taskType) || x.TaskType == taskType) &&
|
||||
(string.IsNullOrWhiteSpace(sourceRefId) || x.SourceRefId == sourceRefId);
|
||||
|
||||
var items = await TaskRepository.GetPagedListAsync(
|
||||
predicate,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
new Dictionary<string, bool> { [nameof(XhsTask.Id)] = false });
|
||||
var total = await TaskRepository.CountAsync(predicate);
|
||||
|
||||
return new OmsPageResult<XhsTaskOutput>
|
||||
{
|
||||
Items = items.Select(MapTask).ToList(),
|
||||
Total = total
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/xhs/task/cancel")]
|
||||
public async Task CancelAsync(XhsTaskCancelInput input)
|
||||
{
|
||||
var customerId = await RequireCustomerIdAsync();
|
||||
var task = await TaskRepository.FirstOrDefaultAsync(x => x.Id == input.TaskId);
|
||||
if (task == null || task.CustomerId != customerId)
|
||||
{
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
|
||||
if (task.Status is not (XhsTaskStatusCodes.Pending or XhsTaskStatusCodes.Claimed))
|
||||
{
|
||||
throw new BusinessException("当前状态不可取消");
|
||||
}
|
||||
|
||||
task.Status = XhsTaskStatusCodes.Cancelled;
|
||||
task.FinishedAt = DateTime.Now;
|
||||
await TaskRepository.UpdateAsync(task);
|
||||
}
|
||||
|
||||
private async Task SyncAnalysisSessionAsync(XhsTask task, string status, XhsTaskReportInput input)
|
||||
{
|
||||
if (task.TaskType is not (XhsTaskTypeCodes.Browse or XhsTaskTypeCodes.FeedDetail))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(task.SourceRefId) ||
|
||||
!int.TryParse(task.SourceRefId, out var sessionId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var session = await AnalysisSessionRepository.FirstOrDefaultAsync(x => x.Id == sessionId);
|
||||
if (session == null || session.CustomerId != task.CustomerId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (status == XhsTaskStatusCodes.Succeeded)
|
||||
{
|
||||
session.FeedsJson = OmsDocHelper.SerializeJson(input.Result);
|
||||
await AnalysisSessionRepository.UpdateAsync(session);
|
||||
}
|
||||
else if (status == XhsTaskStatusCodes.Failed)
|
||||
{
|
||||
session.FeedsJson = OmsDocHelper.SerializeJson(new
|
||||
{
|
||||
error = task.ErrorMessage ?? task.ProgressText ?? "采集失败"
|
||||
});
|
||||
await AnalysisSessionRepository.UpdateAsync(session);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SyncPublishRecordAsync(XhsTask task, string status, XhsTaskReportInput input)
|
||||
{
|
||||
if (task.TaskType != XhsTaskTypeCodes.Publish || string.IsNullOrWhiteSpace(task.SourceRefId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(task.SourceRefId, out var publishId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var record = await PublishRecordRepository.FirstOrDefaultAsync(x => x.Id == publishId);
|
||||
if (record == null || record.CustomerId != task.CustomerId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
record.TaskId = task.Id;
|
||||
if (status == XhsTaskStatusCodes.Running)
|
||||
{
|
||||
record.Status = XhsPublishStatusCodes.Publishing;
|
||||
}
|
||||
else if (status == XhsTaskStatusCodes.Succeeded)
|
||||
{
|
||||
record.Status = XhsPublishStatusCodes.Succeeded;
|
||||
record.Success = true;
|
||||
record.PublishedTime = DateTime.Now;
|
||||
record.ResultSummary = ExtractResultSummary(input.Result) ?? task.ProgressText;
|
||||
}
|
||||
else if (status == XhsTaskStatusCodes.Failed)
|
||||
{
|
||||
record.Status = XhsPublishStatusCodes.Failed;
|
||||
record.Success = false;
|
||||
record.ResultSummary = task.ErrorMessage ?? task.ProgressText;
|
||||
}
|
||||
|
||||
await PublishRecordRepository.UpdateAsync(record);
|
||||
}
|
||||
|
||||
private static string? ExtractResultSummary(object? result)
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (result is string text)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
var json = OmsDocHelper.SerializeJson(result);
|
||||
var obj = OmsDocHelper.DeserializeJson<Dictionary<string, object>>(json);
|
||||
if (obj != null && obj.TryGetValue("summary", out var summary))
|
||||
{
|
||||
return summary?.ToString();
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
private async Task<int> RequireCustomerIdAsync()
|
||||
{
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(CustomerRepository, CurrentUser, null);
|
||||
if (!customerId.HasValue)
|
||||
{
|
||||
throw new BusinessException("请使用客户账号操作");
|
||||
}
|
||||
|
||||
return customerId.Value;
|
||||
}
|
||||
|
||||
private async Task<XhsAgent> GetOwnedAgentByDeviceAsync(string deviceId, int customerId)
|
||||
{
|
||||
var agent = await AgentRepository.GetByDeviceIdAsync(deviceId);
|
||||
if (agent == null || agent.CustomerId != customerId)
|
||||
{
|
||||
throw new BusinessException("Agent 设备不存在");
|
||||
}
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
private async Task<int> GetHumanConfigVersionAsync(int customerId)
|
||||
{
|
||||
var config = await HumanConfigRepository.GetByCustomerIdAsync(customerId);
|
||||
return config?.Version ?? 1;
|
||||
}
|
||||
|
||||
private static XhsTaskOutput MapTask(XhsTask task) => new()
|
||||
{
|
||||
Id = task.Id,
|
||||
TaskType = task.TaskType,
|
||||
ToolName = task.ToolName,
|
||||
Status = task.Status,
|
||||
ProgressText = task.ProgressText,
|
||||
ErrorMessage = task.ErrorMessage,
|
||||
CreateTime = task.CreateTime,
|
||||
FinishedAt = task.FinishedAt
|
||||
};
|
||||
|
||||
private static string? NormalizeOptional(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
82
src/Oms.Application/Shared/XhsHelper.cs
Normal file
82
src/Oms.Application/Shared/XhsHelper.cs
Normal file
@ -0,0 +1,82 @@
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// 小红书模块通用辅助。
|
||||
/// </summary>
|
||||
public static class XhsHelper
|
||||
{
|
||||
public const int OnlineThresholdSeconds = 90;
|
||||
|
||||
public static bool IsAgentOnline(XhsAgent agent, DateTime? now = null)
|
||||
{
|
||||
if (!string.Equals(agent.Status, "online", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (agent.LastHeartbeatTime == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var current = now ?? DateTime.Now;
|
||||
return (current - agent.LastHeartbeatTime.Value).TotalSeconds <= OnlineThresholdSeconds;
|
||||
}
|
||||
|
||||
public static string BuildContentPreview(string? content, int maxLen = 200)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var text = content.Trim();
|
||||
return text.Length <= maxLen ? text : text[..maxLen];
|
||||
}
|
||||
|
||||
public static string? ExtractRecipeNotes(string? recipeJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(recipeJson))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(recipeJson);
|
||||
if (doc.RootElement.TryGetProperty("notes", out var notes))
|
||||
{
|
||||
var text = notes.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return text.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.RootElement.TryGetProperty("body", out var body) &&
|
||||
body.TryGetProperty("domain", out var domain))
|
||||
{
|
||||
return domain.GetString()?.Trim();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string SerializeFeedsForPrompt(object? feeds)
|
||||
{
|
||||
if (feeds == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var json = OmsDocHelper.SerializeJson(feeds) ?? string.Empty;
|
||||
return json.Length <= 12000 ? json : json[..12000];
|
||||
}
|
||||
}
|
||||
47
src/Oms.Application/Shared/XhsHumanDefaults.cs
Normal file
47
src/Oms.Application/Shared/XhsHumanDefaults.cs
Normal file
@ -0,0 +1,47 @@
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// LIKE AI 拟人配置默认值(与 human.py / human_guard.py 内置默认对齐的简化版)。
|
||||
/// </summary>
|
||||
public static class XhsHumanDefaults
|
||||
{
|
||||
public const string BehaviorJson = """
|
||||
{
|
||||
"default_max_attempts": 500,
|
||||
"human_delay": { "min_ms": 300, "max_ms": 700 },
|
||||
"home_browse_plan": {
|
||||
"scroll_lo": 16, "scroll_hi": 51,
|
||||
"click_lo": 2, "click_hi": 8
|
||||
},
|
||||
"search_browse_plan": {
|
||||
"scroll_lo": 13, "scroll_hi": 39,
|
||||
"click_lo": 1, "click_hi": 5
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
public const string GuardJson = """
|
||||
{
|
||||
"publish": {
|
||||
"max_per_day": 2,
|
||||
"min_interval_hours": 6,
|
||||
"pre_delay_min_sec": 60,
|
||||
"pre_delay_max_sec": 180
|
||||
},
|
||||
"browse": {
|
||||
"min_interval_sec": 25,
|
||||
"max_interval_sec": 45,
|
||||
"max_per_hour": 20
|
||||
},
|
||||
"interact": {
|
||||
"min_interval_sec": 45,
|
||||
"max_per_day": 10
|
||||
},
|
||||
"quiet_hours": {
|
||||
"enabled": true,
|
||||
"start": "02:00",
|
||||
"end": "07:00"
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
118
src/Oms.Application/Shared/XhsLlmClient.cs
Normal file
118
src/Oms.Application/Shared/XhsLlmClient.cs
Normal file
@ -0,0 +1,118 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Fake.Application;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// OpenAI 兼容 Chat Completions 客户端(智谱 / DeepSeek / 豆包等)。
|
||||
/// </summary>
|
||||
public class XhsLlmClient
|
||||
{
|
||||
public const string HttpClientName = nameof(XhsLlmClient);
|
||||
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public XhsLlmClient(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public async Task<string> ChatCompletionAsync(
|
||||
XhsAiProvider provider,
|
||||
IReadOnlyList<(string Role, string Content)> messages,
|
||||
string? modelOverride = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(provider.ApiKeyEnc))
|
||||
{
|
||||
throw new BusinessException($"请先配置 {provider.DisplayName} 的 API Key");
|
||||
}
|
||||
|
||||
var model = modelOverride ?? provider.ApiSecretEnc;
|
||||
if (string.IsNullOrWhiteSpace(model))
|
||||
{
|
||||
throw new BusinessException($"请先配置 {provider.DisplayName} 的模型名(ApiSecret 字段)");
|
||||
}
|
||||
|
||||
var url = BuildChatUrl(provider.BaseUrl);
|
||||
var payload = new
|
||||
{
|
||||
model,
|
||||
messages = messages.Select(x => new { role = x.Role, content = x.Content }).ToList(),
|
||||
temperature = 0.7
|
||||
};
|
||||
|
||||
var client = _httpClientFactory.CreateClient(HttpClientName);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", provider.ApiKeyEnc.Trim());
|
||||
request.Content = JsonContent.Create(payload);
|
||||
|
||||
using var response = await client.SendAsync(request, cancellationToken);
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new BusinessException($"AI 请求失败 ({(int)response.StatusCode}): {TrimError(body)}");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var root = doc.RootElement;
|
||||
if (root.TryGetProperty("choices", out var choices) &&
|
||||
choices.GetArrayLength() > 0 &&
|
||||
choices[0].TryGetProperty("message", out var message) &&
|
||||
message.TryGetProperty("content", out var content))
|
||||
{
|
||||
return content.GetString()?.Trim() ?? string.Empty;
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("error", out var error) &&
|
||||
error.TryGetProperty("message", out var errorMessage))
|
||||
{
|
||||
throw new BusinessException(errorMessage.GetString() ?? "AI 返回错误");
|
||||
}
|
||||
|
||||
throw new BusinessException("AI 响应格式无法解析");
|
||||
}
|
||||
|
||||
private static string BuildChatUrl(string baseUrl)
|
||||
{
|
||||
var url = baseUrl.Trim().TrimEnd('/');
|
||||
if (url.EndsWith("/chat/completions", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return url;
|
||||
}
|
||||
|
||||
if (url.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return url + "/chat/completions";
|
||||
}
|
||||
|
||||
return url + "/v1/chat/completions";
|
||||
}
|
||||
|
||||
private static string TrimError(string body)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return "空响应";
|
||||
}
|
||||
|
||||
return body.Length <= 300 ? body : body[..300];
|
||||
}
|
||||
}
|
||||
|
||||
public static class XhsLlmServiceExtensions
|
||||
{
|
||||
public static IServiceCollection AddXhsLlmClient(this IServiceCollection services)
|
||||
{
|
||||
services.AddHttpClient(XhsLlmClient.HttpClientName, client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromMinutes(3);
|
||||
});
|
||||
services.AddTransient<XhsLlmClient>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
36
src/Oms.Application/Shared/XhsTaskFactory.cs
Normal file
36
src/Oms.Application/Shared/XhsTaskFactory.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Fake.Application;
|
||||
using Oms.Domain.XhsAggregate.Consts;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Agent 远程控制动作 → 任务定义。
|
||||
/// </summary>
|
||||
public static class XhsTaskFactory
|
||||
{
|
||||
public static XhsTask CreateControlTask(int customerId, int agentId, string action)
|
||||
{
|
||||
var (taskType, toolName, args) = action switch
|
||||
{
|
||||
XhsAgentControlActions.EngineStart => (XhsTaskTypeCodes.EngineStart, null, "{}"),
|
||||
XhsAgentControlActions.EngineStop => (XhsTaskTypeCodes.EngineStop, null, "{}"),
|
||||
XhsAgentControlActions.BrowserLogin => (XhsTaskTypeCodes.BrowserLogin, "browser_login", "{}"),
|
||||
XhsAgentControlActions.ClearCookies => (XhsTaskTypeCodes.ClearCookies, "delete_cookies", "{}"),
|
||||
XhsAgentControlActions.CheckLogin => (XhsTaskTypeCodes.CheckLogin, "check_login_status", "{}"),
|
||||
_ => throw new BusinessException($"未知控制动作: {action}")
|
||||
};
|
||||
|
||||
return new XhsTask
|
||||
{
|
||||
CustomerId = customerId,
|
||||
AgentId = agentId,
|
||||
TaskType = taskType,
|
||||
ToolName = toolName,
|
||||
ArgumentsJson = args,
|
||||
Source = XhsTaskSourceCodes.Web,
|
||||
Priority = 10,
|
||||
Status = XhsTaskStatusCodes.Pending
|
||||
};
|
||||
}
|
||||
}
|
||||
85
src/Oms.Application/Workers/XhsScheduledPublishWorker.cs
Normal file
85
src/Oms.Application/Workers/XhsScheduledPublishWorker.cs
Normal file
@ -0,0 +1,85 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Services.Xhs;
|
||||
using Oms.Domain.XhsAggregate.Consts;
|
||||
using Oms.Domain.XhsAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Workers;
|
||||
|
||||
/// <summary>
|
||||
/// 扫描到期定时发布记录并创建 Agent 任务。
|
||||
/// </summary>
|
||||
public class XhsScheduledPublishWorker : BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan PollInterval = TimeSpan.FromMinutes(1);
|
||||
private const int BatchSize = 10;
|
||||
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<XhsScheduledPublishWorker> _logger;
|
||||
|
||||
public XhsScheduledPublishWorker(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<XhsScheduledPublishWorker> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessDueAsync(stoppingToken);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogError(ex, "小红书定时发布调度异常");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(PollInterval, stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessDueAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var publishRecordRepository = scope.ServiceProvider.GetRequiredService<IXhsPublishRecordRepository>();
|
||||
var publishService = scope.ServiceProvider.GetRequiredService<XhsPublishService>();
|
||||
|
||||
var dueRecords = await publishRecordRepository.GetDueScheduledAsync(BatchSize, cancellationToken);
|
||||
foreach (var record in dueRecords)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (record.Status != XhsPublishStatusCodes.Scheduled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
record.Status = XhsPublishStatusCodes.Publishing;
|
||||
await publishRecordRepository.UpdateAsync(record);
|
||||
|
||||
var task = await publishService.CreatePublishTaskAsync(record, agentId: null, XhsTaskSourceCodes.Schedule);
|
||||
record.TaskId = task.Id;
|
||||
await publishRecordRepository.UpdateAsync(record);
|
||||
|
||||
_logger.LogInformation(
|
||||
"小红书定时发布已派发 publishRecordId={PublishRecordId} taskId={TaskId}",
|
||||
record.Id,
|
||||
task.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/Oms.Domain/XhsAggregate/Consts/XhsCodes.cs
Normal file
65
src/Oms.Domain/XhsAggregate/Consts/XhsCodes.cs
Normal file
@ -0,0 +1,65 @@
|
||||
namespace Oms.Domain.XhsAggregate.Consts;
|
||||
|
||||
public static class XhsAgentStatusCodes
|
||||
{
|
||||
public const string Online = "online";
|
||||
public const string Offline = "offline";
|
||||
}
|
||||
|
||||
public static class XhsTaskStatusCodes
|
||||
{
|
||||
public const string Pending = "pending";
|
||||
public const string Claimed = "claimed";
|
||||
public const string Running = "running";
|
||||
public const string Succeeded = "succeeded";
|
||||
public const string Failed = "failed";
|
||||
public const string Cancelled = "cancelled";
|
||||
}
|
||||
|
||||
public static class XhsTaskTypeCodes
|
||||
{
|
||||
public const string EngineStart = "engine_start";
|
||||
public const string EngineStop = "engine_stop";
|
||||
public const string BrowserLogin = "browser_login";
|
||||
public const string ClearCookies = "clear_cookies";
|
||||
public const string CheckLogin = "check_login";
|
||||
public const string Browse = "browse";
|
||||
public const string FeedDetail = "feed_detail";
|
||||
public const string Interact = "interact";
|
||||
public const string Publish = "publish";
|
||||
public const string SkillPipeline = "skill_pipeline";
|
||||
}
|
||||
|
||||
public static class XhsTaskSourceCodes
|
||||
{
|
||||
public const string Web = "web";
|
||||
public const string Schedule = "schedule";
|
||||
public const string Skill = "skill";
|
||||
}
|
||||
|
||||
public static class XhsPublishStatusCodes
|
||||
{
|
||||
public const string Draft = "draft";
|
||||
public const string Scheduled = "scheduled";
|
||||
public const string Publishing = "publishing";
|
||||
public const string Succeeded = "succeeded";
|
||||
public const string Failed = "failed";
|
||||
public const string Cancelled = "cancelled";
|
||||
}
|
||||
|
||||
public static class XhsAgentControlActions
|
||||
{
|
||||
public const string EngineStart = "engine_start";
|
||||
public const string EngineStop = "engine_stop";
|
||||
public const string BrowserLogin = "browser_login";
|
||||
public const string ClearCookies = "clear_cookies";
|
||||
public const string CheckLogin = "check_login";
|
||||
}
|
||||
|
||||
public static class XhsAnalysisTypeCodes
|
||||
{
|
||||
public const string HomeFeed = "home_feed";
|
||||
public const string Search = "search";
|
||||
public const string MyFeeds = "my_feeds";
|
||||
public const string FeedDetail = "feed_detail";
|
||||
}
|
||||
32
src/Oms.Domain/XhsAggregate/Entities/XhsAgent.cs
Normal file
32
src/Oms.Domain/XhsAggregate/Entities/XhsAgent.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using Oms.Domain.Shared;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.Domain.XhsAggregate.Entities;
|
||||
|
||||
[SugarTable("t_xhs_agent")]
|
||||
public class XhsAgent : OmsAuditedAggregateRoot
|
||||
{
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
|
||||
public string? DeviceName { get; set; }
|
||||
|
||||
public string? AgentVersion { get; set; }
|
||||
|
||||
public int BridgePort { get; set; } = 9333;
|
||||
|
||||
public string Status { get; set; } = "offline";
|
||||
|
||||
public bool EngineRunning { get; set; }
|
||||
|
||||
public bool ExtensionConnected { get; set; }
|
||||
|
||||
public bool XhsLoggedIn { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public DateTime? LastHeartbeatTime { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? LastError { get; set; }
|
||||
}
|
||||
28
src/Oms.Domain/XhsAggregate/Entities/XhsAiProvider.cs
Normal file
28
src/Oms.Domain/XhsAggregate/Entities/XhsAiProvider.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using Oms.Domain.Shared;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.Domain.XhsAggregate.Entities;
|
||||
|
||||
[SugarTable("t_xhs_ai_provider")]
|
||||
public class XhsAiProvider : OmsAuditedAggregateRoot
|
||||
{
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
public string ProviderKey { get; set; } = string.Empty;
|
||||
|
||||
public string ProviderType { get; set; } = string.Empty;
|
||||
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? ApiKeyEnc { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? ApiSecretEnc { get; set; }
|
||||
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
33
src/Oms.Domain/XhsAggregate/Entities/XhsAnalysisSession.cs
Normal file
33
src/Oms.Domain/XhsAggregate/Entities/XhsAnalysisSession.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using Oms.Domain.Shared;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.Domain.XhsAggregate.Entities;
|
||||
|
||||
[SugarTable("t_xhs_analysis_session")]
|
||||
public class XhsAnalysisSession : OmsAuditedAggregateRoot
|
||||
{
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? SessionName { get; set; }
|
||||
|
||||
public string AnalysisType { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public int? TaskId { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDataType = "json", IsNullable = true)]
|
||||
public string? FeedsJson { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? SelectedFeedId { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDataType = "mediumtext", IsNullable = true)]
|
||||
public string? AiAnalysisMarkdown { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public int? SkillId { get; set; }
|
||||
}
|
||||
18
src/Oms.Domain/XhsAggregate/Entities/XhsHumanConfig.cs
Normal file
18
src/Oms.Domain/XhsAggregate/Entities/XhsHumanConfig.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using Oms.Domain.Shared;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.Domain.XhsAggregate.Entities;
|
||||
|
||||
[SugarTable("t_xhs_human_config")]
|
||||
public class XhsHumanConfig : OmsAuditedAggregateRoot
|
||||
{
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDataType = "json")]
|
||||
public string BehaviorJson { get; set; } = "{}";
|
||||
|
||||
[SugarColumn(ColumnDataType = "json")]
|
||||
public string GuardJson { get; set; } = "{}";
|
||||
|
||||
public int Version { get; set; } = 1;
|
||||
}
|
||||
22
src/Oms.Domain/XhsAggregate/Entities/XhsMedia.cs
Normal file
22
src/Oms.Domain/XhsAggregate/Entities/XhsMedia.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Oms.Domain.Shared;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.Domain.XhsAggregate.Entities;
|
||||
|
||||
[SugarTable("t_xhs_media")]
|
||||
public class XhsMedia : OmsAuditedAggregateRoot
|
||||
{
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
public string StoragePath { get; set; } = string.Empty;
|
||||
|
||||
public string PublicUrl { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? MimeType { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public long? FileSizeBytes { get; set; }
|
||||
}
|
||||
49
src/Oms.Domain/XhsAggregate/Entities/XhsPublishRecord.cs
Normal file
49
src/Oms.Domain/XhsAggregate/Entities/XhsPublishRecord.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using Oms.Domain.Shared;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.Domain.XhsAggregate.Entities;
|
||||
|
||||
[SugarTable("t_xhs_publish_record")]
|
||||
public class XhsPublishRecord : OmsAuditedAggregateRoot
|
||||
{
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public int? TaskId { get; set; }
|
||||
|
||||
public string ToolName { get; set; } = string.Empty;
|
||||
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? ContentPreview { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDataType = "text", IsNullable = true)]
|
||||
public string? ContentFull { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDataType = "json", IsNullable = true)]
|
||||
public string? MediaJson { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDataType = "json", IsNullable = true)]
|
||||
public string? TagsJson { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? Visibility { get; set; }
|
||||
|
||||
public string Status { get; set; } = "draft";
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public bool? Success { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? ResultSummary { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public DateTime? ScheduledTime { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public DateTime? PublishedTime { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public int? SkillId { get; set; }
|
||||
}
|
||||
24
src/Oms.Domain/XhsAggregate/Entities/XhsSkill.cs
Normal file
24
src/Oms.Domain/XhsAggregate/Entities/XhsSkill.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using Oms.Domain.Shared;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.Domain.XhsAggregate.Entities;
|
||||
|
||||
[SugarTable("t_xhs_skill")]
|
||||
public class XhsSkill : OmsAuditedAggregateRoot
|
||||
{
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
public string ExternalId { get; set; } = string.Empty;
|
||||
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
public string SkillType { get; set; } = "ai_create";
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? PresetId { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDataType = "json")]
|
||||
public string RecipeJson { get; set; } = "{}";
|
||||
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
51
src/Oms.Domain/XhsAggregate/Entities/XhsTask.cs
Normal file
51
src/Oms.Domain/XhsAggregate/Entities/XhsTask.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using Oms.Domain.Shared;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.Domain.XhsAggregate.Entities;
|
||||
|
||||
[SugarTable("t_xhs_task")]
|
||||
public class XhsTask : OmsAuditedAggregateRoot
|
||||
{
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public int? AgentId { get; set; }
|
||||
|
||||
public string TaskType { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? ToolName { get; set; }
|
||||
|
||||
public int Priority { get; set; }
|
||||
|
||||
public string Status { get; set; } = "pending";
|
||||
|
||||
[SugarColumn(ColumnDataType = "json")]
|
||||
public string ArgumentsJson { get; set; } = "{}";
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? ProgressText { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDataType = "json", IsNullable = true)]
|
||||
public string? ResultJson { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
public string Source { get; set; } = "web";
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string? SourceRefId { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public DateTime? ScheduledTime { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public DateTime? ClaimedAt { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public DateTime? StartedAt { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public DateTime? FinishedAt { get; set; }
|
||||
}
|
||||
42
src/Oms.Domain/XhsAggregate/Repositories/IXhsRepositories.cs
Normal file
42
src/Oms.Domain/XhsAggregate/Repositories/IXhsRepositories.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using Fake.Domain.Repositories;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
|
||||
namespace Oms.Domain.XhsAggregate.Repositories;
|
||||
|
||||
public interface IXhsAgentRepository : IRepository<XhsAgent>
|
||||
{
|
||||
Task<XhsAgent?> GetByDeviceIdAsync(string deviceId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IXhsTaskRepository : IRepository<XhsTask>
|
||||
{
|
||||
Task<List<XhsTask>> ClaimPendingAsync(
|
||||
int customerId,
|
||||
int agentId,
|
||||
int limit,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IXhsPublishRecordRepository : IRepository<XhsPublishRecord>
|
||||
{
|
||||
Task<List<XhsPublishRecord>> GetDueScheduledAsync(int limit, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IXhsAnalysisSessionRepository : IRepository<XhsAnalysisSession>;
|
||||
|
||||
public interface IXhsSkillRepository : IRepository<XhsSkill>
|
||||
{
|
||||
Task<XhsSkill?> GetByExternalIdAsync(int customerId, string externalId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IXhsHumanConfigRepository : IRepository<XhsHumanConfig>
|
||||
{
|
||||
Task<XhsHumanConfig?> GetByCustomerIdAsync(int customerId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IXhsAiProviderRepository : IRepository<XhsAiProvider>
|
||||
{
|
||||
Task<XhsAiProvider?> GetByProviderKeyAsync(int customerId, string providerKey, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IXhsMediaRepository : IRepository<XhsMedia>;
|
||||
96
src/Oms.SqlSugarCore/Xhs/XhsRepositories.cs
Normal file
96
src/Oms.SqlSugarCore/Xhs/XhsRepositories.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Oms.Domain.XhsAggregate.Consts;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
using Oms.Domain.XhsAggregate.Repositories;
|
||||
using Oms.SqlSugarCore.Repositories;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.SqlSugarCore.Xhs;
|
||||
|
||||
public class XhsAgentRepository : OmsSqlSugarRepository<XhsAgent>, IXhsAgentRepository, IScopedDependency
|
||||
{
|
||||
public Task<XhsAgent?> GetByDeviceIdAsync(string deviceId, CancellationToken cancellationToken = default) =>
|
||||
FirstOrDefaultAsync(x => x.DeviceId == deviceId, cancellationToken);
|
||||
}
|
||||
|
||||
public class XhsTaskRepository : OmsSqlSugarRepository<XhsTask>, IXhsTaskRepository, IScopedDependency
|
||||
{
|
||||
public async Task<List<XhsTask>> ClaimPendingAsync(
|
||||
int customerId,
|
||||
int agentId,
|
||||
int limit,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var db = await GetDbContextAsync();
|
||||
var query = db.Queryable<XhsTask>()
|
||||
.Where(x => x.CustomerId == customerId)
|
||||
.Where(x => x.Status == XhsTaskStatusCodes.Pending)
|
||||
.Where(x => x.AgentId == null || x.AgentId == agentId)
|
||||
.Where(x => x.ScheduledTime == null || x.ScheduledTime <= now)
|
||||
.OrderBy(x => x.Priority, OrderByType.Desc)
|
||||
.OrderBy(x => x.Id, OrderByType.Asc)
|
||||
.Take(limit);
|
||||
|
||||
var tasks = await query.ToListAsync(cancellationToken);
|
||||
if (tasks.Count == 0)
|
||||
{
|
||||
return tasks;
|
||||
}
|
||||
|
||||
var claimedAt = DateTime.Now;
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
task.Status = XhsTaskStatusCodes.Claimed;
|
||||
task.AgentId = agentId;
|
||||
task.ClaimedAt = claimedAt;
|
||||
}
|
||||
|
||||
await db.Updateable(tasks).ExecuteCommandAsync(cancellationToken);
|
||||
return tasks;
|
||||
}
|
||||
}
|
||||
|
||||
public class XhsPublishRecordRepository
|
||||
: OmsSqlSugarRepository<XhsPublishRecord>, IXhsPublishRecordRepository, IScopedDependency
|
||||
{
|
||||
public async Task<List<XhsPublishRecord>> GetDueScheduledAsync(int limit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var db = await GetDbContextAsync();
|
||||
return await db.Queryable<XhsPublishRecord>()
|
||||
.Where(x => x.Status == XhsPublishStatusCodes.Scheduled)
|
||||
.Where(x => x.ScheduledTime != null && x.ScheduledTime <= now)
|
||||
.OrderBy(x => x.ScheduledTime, OrderByType.Asc)
|
||||
.Take(limit)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class XhsAnalysisSessionRepository
|
||||
: OmsSqlSugarRepository<XhsAnalysisSession>, IXhsAnalysisSessionRepository, IScopedDependency;
|
||||
|
||||
public class XhsSkillRepository : OmsSqlSugarRepository<XhsSkill>, IXhsSkillRepository, IScopedDependency
|
||||
{
|
||||
public Task<XhsSkill?> GetByExternalIdAsync(int customerId, string externalId, CancellationToken cancellationToken = default) =>
|
||||
FirstOrDefaultAsync(x => x.CustomerId == customerId && x.ExternalId == externalId, cancellationToken);
|
||||
}
|
||||
|
||||
public class XhsHumanConfigRepository
|
||||
: OmsSqlSugarRepository<XhsHumanConfig>, IXhsHumanConfigRepository, IScopedDependency
|
||||
{
|
||||
public Task<XhsHumanConfig?> GetByCustomerIdAsync(int customerId, CancellationToken cancellationToken = default) =>
|
||||
FirstOrDefaultAsync(x => x.CustomerId == customerId, cancellationToken);
|
||||
}
|
||||
|
||||
public class XhsAiProviderRepository
|
||||
: OmsSqlSugarRepository<XhsAiProvider>, IXhsAiProviderRepository, IScopedDependency
|
||||
{
|
||||
public Task<XhsAiProvider?> GetByProviderKeyAsync(
|
||||
int customerId,
|
||||
string providerKey,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
FirstOrDefaultAsync(x => x.CustomerId == customerId && x.ProviderKey == providerKey, cancellationToken);
|
||||
}
|
||||
|
||||
public class XhsMediaRepository : OmsSqlSugarRepository<XhsMedia>, IXhsMediaRepository, IScopedDependency;
|
||||
63
src/SimpleAdmin.Api/Auth/XhsRolePermissionSeedContributor.cs
Normal file
63
src/SimpleAdmin.Api/Auth/XhsRolePermissionSeedContributor.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using Fake.Data.Seeding;
|
||||
using Fake.Rbac.Application.Services;
|
||||
|
||||
namespace SimpleAdmin.Api.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// 启动时为 Customer / Admin 角色追加小红书权限(幂等合并,不覆盖已有权限)。
|
||||
/// </summary>
|
||||
public class XhsRolePermissionSeedContributor(RoleService roleService) : IDataSeedContributor
|
||||
{
|
||||
public const string AdminRoleCode = "Admin";
|
||||
|
||||
public static readonly string[] CustomerPermissions =
|
||||
[
|
||||
"Xhs.Agent.Query",
|
||||
"Xhs.Agent.Control",
|
||||
"Xhs.Analysis.Use",
|
||||
"Xhs.Creation.Use",
|
||||
"Xhs.Publish.Query",
|
||||
"Xhs.Publish.Cancel",
|
||||
"Xhs.Skills.Query",
|
||||
"Xhs.Skills.Save",
|
||||
"Xhs.Skills.Run",
|
||||
"Xhs.HumanSettings.Save",
|
||||
"Xhs.Ai.Use",
|
||||
"Xhs.Ai.Save"
|
||||
];
|
||||
|
||||
public async Task SeedAsync()
|
||||
{
|
||||
var roles = await roleService.GetAllRolesAsync();
|
||||
var customer = roles.FirstOrDefault(x =>
|
||||
string.Equals(x.Code, CustomerRoleSeedContributor.CustomerRoleCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (customer != null)
|
||||
{
|
||||
await MergePermissionsAsync(customer.Id, CustomerPermissions);
|
||||
}
|
||||
|
||||
var admin = roles.FirstOrDefault(x =>
|
||||
string.Equals(x.Code, AdminRoleCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (admin != null)
|
||||
{
|
||||
await MergePermissionsAsync(admin.Id, CustomerPermissions);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MergePermissionsAsync(Guid roleId, IEnumerable<string> requiredPermissions)
|
||||
{
|
||||
var existing = await roleService.GetRolePermissionsAsync(roleId);
|
||||
var merged = existing
|
||||
.Concat(requiredPermissions)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (merged.Count == existing.Count &&
|
||||
requiredPermissions.All(p => existing.Contains(p, StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await roleService.AssignPermissionsAsync(roleId, merged);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user