feat: 增加实时会议功能并优化数据库和API
- 增加实时会议的创建、追加转录和完成接口 - 更新 `MeetingDTO` 和 `MeetingVO`,添加可选字段 `summaryModelId`, `useSpkId`, `hotWords` - 增加 `RealtimeTranscriptItemDTO` 接口 - 延长HTTP客户端连接超时时间至300秒 - 优化数据库表结构,新增 `biz_prompt_template_user_config` 表 - 更新系统参数和权限配置dev_na
parent
eaed89c9ec
commit
d8bfdb21fa
|
|
@ -4,7 +4,7 @@
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- 0. 租户与组织
|
-- 0. 租户与组织
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
-- 租户表
|
-- 租户表
|
||||||
CREATE TABLE sys_tenant (
|
CREATE TABLE sys_tenant (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
@ -109,7 +109,7 @@ CREATE TABLE sys_permission (
|
||||||
perm_id BIGSERIAL PRIMARY KEY,
|
perm_id BIGSERIAL PRIMARY KEY,
|
||||||
parent_id BIGINT,
|
parent_id BIGINT,
|
||||||
name VARCHAR(100) NOT NULL,
|
name VARCHAR(100) NOT NULL,
|
||||||
code VARCHAR(100) NOT NULL UNIQUE,
|
code VARCHAR(100) NOT NULL ,
|
||||||
perm_type VARCHAR(20) NOT NULL,
|
perm_type VARCHAR(20) NOT NULL,
|
||||||
level INTEGER NOT NULL,
|
level INTEGER NOT NULL,
|
||||||
path VARCHAR(255),
|
path VARCHAR(255),
|
||||||
|
|
@ -161,13 +161,14 @@ CREATE TABLE sys_dict_item (
|
||||||
status SMALLINT DEFAULT 1,
|
status SMALLINT DEFAULT 1,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
is_deleted SMALLINT DEFAULT 0
|
is_deleted SMALLINT DEFAULT 0,
|
||||||
|
remark varchar(255)
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_dict_item_type ON sys_dict_item (type_code);
|
CREATE INDEX idx_dict_item_type ON sys_dict_item (type_code);
|
||||||
CREATE INDEX uk_dict_item_value ON sys_dict_item (type_code, item_value);
|
CREATE INDEX uk_dict_item_value ON sys_dict_item (type_code, item_value);
|
||||||
|
|
||||||
CREATE TABLE sys_param (
|
CREATE TABLE sys_param (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
param_id BIGSERIAL PRIMARY KEY,
|
||||||
param_key VARCHAR(100) UNIQUE NOT NULL,
|
param_key VARCHAR(100) UNIQUE NOT NULL,
|
||||||
param_value TEXT NOT NULL,
|
param_value TEXT NOT NULL,
|
||||||
param_type VARCHAR(20) NOT NULL,
|
param_type VARCHAR(20) NOT NULL,
|
||||||
|
|
@ -179,6 +180,14 @@ CREATE TABLE sys_param (
|
||||||
is_deleted SMALLINT DEFAULT 0
|
is_deleted SMALLINT DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE sys_role_permission (
|
||||||
|
"id" BIGSERIAL PRIMARY KEY,
|
||||||
|
"role_id" int8 NOT NULL,
|
||||||
|
"perm_id" int8 NOT NULL,
|
||||||
|
"is_deleted" int2 NOT NULL DEFAULT 0,
|
||||||
|
"created_at" timestamp(6) NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamp(6) NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- 3. 日志 (租户隔离)
|
-- 3. 日志 (租户隔离)
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
|
@ -409,6 +418,17 @@ CREATE INDEX idx_aitask_meeting ON biz_ai_tasks (meeting_id);
|
||||||
COMMENT ON TABLE biz_meetings IS '会议管理主表';
|
COMMENT ON TABLE biz_meetings IS '会议管理主表';
|
||||||
COMMENT ON TABLE biz_meeting_transcripts IS '会议转录明细表';
|
COMMENT ON TABLE biz_meeting_transcripts IS '会议转录明细表';
|
||||||
COMMENT ON TABLE biz_ai_tasks IS 'AI 任务流水日志表';
|
COMMENT ON TABLE biz_ai_tasks IS 'AI 任务流水日志表';
|
||||||
|
DROP TABLE IF EXISTS "biz_prompt_template_user_config";
|
||||||
|
CREATE TABLE "biz_prompt_template_user_config" (
|
||||||
|
"id" BIGSERIAL PRIMARY KEY,
|
||||||
|
"tenant_id" int8 NOT NULL DEFAULT 0,
|
||||||
|
"user_id" int8 NOT NULL,
|
||||||
|
"template_id" int8 NOT NULL,
|
||||||
|
"status" int2 DEFAULT 1,
|
||||||
|
"created_at" timestamp(6) NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamp(6) NOT NULL DEFAULT now(),
|
||||||
|
"is_deleted" int2 NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
|
@ -454,3 +474,84 @@ INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES
|
||||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_log_status', '操作状态', '1=成功, 0=失败');
|
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_log_status', '操作状态', '1=成功, 0=失败');
|
||||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '成功', '1', 1);
|
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '成功', '1', 1);
|
||||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '失败', '0', 2);
|
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '失败', '0', 2);
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (21, 18, '查询租户', 'sys_tenant:query', 'button', 3, NULL, NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.647439', '2026-02-25 10:09:32.647439');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (28, 19, '删除组织', 'sys:org:delete', 'button', 3, NULL, NULL, NULL, 4, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.65871', '2026-02-25 10:09:32.65871');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (22, 18, '新增租户', 'sys_tenant:create', 'button', 3, NULL, NULL, NULL, 2, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.647439', '2026-02-25 10:09:32.647439');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (23, 18, '修改租户', 'sys_tenant:update', 'button', 3, NULL, NULL, NULL, 3, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.647439', '2026-02-25 10:09:32.647439');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (24, 18, '删除租户', 'sys_tenant:delete', 'button', 3, NULL, NULL, NULL, 4, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.647439', '2026-02-25 10:09:32.647439');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (48, 3, '权限查询', 'sys:permission:list', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 16:07:04.002702', '2026-02-26 16:07:04.003701');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (13, 12, '角色权限', 'menu:role:permission', 'menu', 2, '/role-permissions', NULL, NULL, 0, 1, 1, NULL, NULL, 1, '2026-02-10 18:01:32.999774', '2026-02-11 09:41:31.952294');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (39, 12, '平台管理', 'platform', 'menu', 2, '/platform-settings', NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 14:54:49.406968', '2026-02-26 14:54:49.407968');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (25, 19, '查询组织', 'sys:org:query', 'button', 3, NULL, NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.65871', '2026-02-25 10:09:32.65871');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (26, 19, '新增组织', 'sys:org:create', 'button', 3, NULL, NULL, NULL, 2, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.65871', '2026-02-25 10:09:32.65871');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (27, 19, '修改组织', 'sys:org:update', 'button', 3, NULL, NULL, NULL, 3, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.65871', '2026-02-25 10:09:32.65871');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (3, 12, '角色管理', 'sys:role:list', 'menu', 2, '/roles', NULL, NULL, 3, 1, 1, '角色管理菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-02-10 17:24:07.484806');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (40, 3, '角色查询', 'sys:role:query', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:35:35.692367', '2026-02-26 15:35:35.693366');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (41, 3, '角色创建', 'sys:role:create', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:35:50.081581', '2026-02-26 15:35:50.081581');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (42, 3, '角色更新', 'sys:role:update', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:36:03.385343', '2026-02-26 15:36:03.385343');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (43, 3, '角色删除', 'sys:role:delete', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:36:20.451039', '2026-02-26 15:36:20.451039');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (44, 3, '角色权限列表', 'sys:role:permission:list', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:36:39.902216', '2026-02-26 15:36:39.902216');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (45, 3, '角色权限更新', 'sys:role:permission:save', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:36:53.595974', '2026-02-26 15:36:53.595974');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (2, 12, '用户管理', 'sys:user:list', 'menu', 2, '/users', NULL, NULL, 2, 1, 1, '用户管理菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-02-26 15:43:21.037142');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (29, 2, '查询用户', 'sys:user:query', 'button', 3, NULL, NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (30, 2, '新增用户', 'sys:user:create', 'button', 3, NULL, NULL, NULL, 2, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (31, 2, '修改用户', 'sys:user:update', 'button', 3, NULL, NULL, NULL, 3, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (32, 2, '删除用户', 'sys:user:delete', 'button', 3, NULL, NULL, NULL, 4, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (33, 2, '分配角色', 'sys:user:role:save', 'button', 3, NULL, NULL, NULL, 5, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (46, 2, '用户角色查询', 'sys:user:role:list', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:45:14.890567', '2026-02-26 15:45:28.553231');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (54, NULL, '热词管理', 'menu:hotword', 'menu', 1, '/hotwords', NULL, 'hotword', 11, 1, 1, NULL, NULL, 0, '2026-02-28 16:51:49.158997', '2026-02-28 16:51:49.158997');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (55, NULL, '总结模板', 'menu:prompt', 'menu', 1, '/prompts', NULL, 'prompt', 12, 1, 1, NULL, NULL, 0, '2026-02-28 17:47:51.015282', '2026-02-28 17:47:51.015282');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (56, NULL, '模型配置', 'menu:aimodel', 'menu', 1, '/aimodels', NULL, 'aimodel', 13, 1, 1, NULL, NULL, 0, '2026-03-02 09:48:27.179055', '2026-03-02 09:48:27.179055');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (57, NULL, '会议中心', 'menu:meeting', 'menu', 1, '/meetings', NULL, 'meeting', 20, 1, 1, NULL, NULL, 0, '2026-03-02 11:02:58.089065', '2026-03-02 11:02:58.089065');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (52, NULL, '测试菜单', 'test', 'directory', 1, '/role-permissions', NULL, NULL, 0, 1, 1, NULL, NULL, 1, '2026-02-27 10:39:04.576329', '2026-03-03 10:03:45.999369');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (59, NULL, '声纹注册', 'speaker', 'menu', 1, '/speaker-reg', NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-03-06 15:23:09.314321', '2026-03-06 15:23:51.715481');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (18, 12, '租户管理', 'menu:tenant', 'menu', 2, '/tenants', NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-12 14:06:13.672548', '2026-03-06 16:31:45.006699');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (58, NULL, '发起会议', 'menu:meeting:create', 'menu', 1, '/meeting-create', NULL, 'audio', 19, 1, 1, NULL, NULL, 1, '2026-03-02 16:21:47.326202', '2026-03-05 09:05:49.301092');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (4, 12, '菜单管理', 'sys:permission:list', 'menu', 2, '/permissions', NULL, NULL, 4, 1, 1, '权限管理菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-03-05 17:10:30.891258');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (5, 12, '设备管理', 'menu:devices', 'menu', 2, '/devices', NULL, NULL, 5, 0, 1, '设备管理菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-03-05 17:11:48.867451');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (15, 12, '字典管理', 'menu:dict', 'menu', 2, '/dictionaries', NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-11 13:54:56.100838', '2026-03-05 17:12:28.223844');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (17, 12, '日志管理', 'menu:log', 'menu', 2, '/logs', NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-12 09:49:02.814427', '2026-03-05 17:12:57.94561');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (38, 12, '参数管理', 'params', 'menu', 2, '/params', NULL, NULL, 2, 1, 1, NULL, NULL, 0, '2026-02-26 14:34:16.903552', '2026-03-06 16:31:51.714937');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (1, NULL, '任务监控', 'menu:dashboard', 'menu', 1, '/', NULL, NULL, 1, 1, 1, 'Dashboard 菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-03-05 18:01:31.515477');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (19, 12, '组织管理', 'sys:org:list', 'menu', 2, '/orgs', NULL, NULL, 3, 1, 1, NULL, NULL, 0, '2026-02-12 14:09:01.818807', '2026-03-06 16:32:00.114277');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (12, NULL, '系统管理', 'system', 'directory', 1, NULL, NULL, NULL, 110, 1, 1, NULL, NULL, 0, '2026-02-10 17:23:52.877017', '2026-03-06 14:00:20.182181');
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (2, 'security.token.refresh_ttl_days', '7', 'int', 1, 1, 'Refresh Token 有效期(天)', 0, '2026-02-09 09:54:21.893832', '2026-02-09 09:54:21.893832');
|
||||||
|
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (4, 'tenant.init.default.menu.codes', 'sys:user:list,sys:user:create,sys:user:query,sys:role:create,sys:user:role:save,sys:org:delete,sys:org:query,sys:role:permission:list,sys:org:update,sys:role:permission:save,sys:role:update,system,sys:user:delete,sys:user:role:list,sys:org:list,sys:role:delete,sys:role:list,sys:org:create,sys:user:update,sys:permission:list,sys:role:query', 'String', 1, 1, '新建租户时角色权限', 0, '2026-02-26 16:46:20.392789', '2026-02-26 16:46:38.137264');
|
||||||
|
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (5, 'tenant.init.default.password', '123456', 'String', 1, 1, NULL, 0, '2026-02-26 16:46:52.124755', '2026-02-26 16:46:52.124755');
|
||||||
|
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (3, 'security.captcha.enabled', 'false', 'boolean', 1, 1, '是否开启验证码', 0, '2026-02-11 02:45:31.097324', '2026-03-10 09:40:33.084368');
|
||||||
|
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (1, 'security.token.access_ttl_minutes', '120', 'int', 1, 1, 'Access Token 有效期(分钟)', 0, '2026-02-09 09:54:21.888052', '2026-03-10 10:15:39.55035');
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO sys_user ("user_id", "username", "display_name", "email", "phone", "password_hash", "status", "is_deleted", "created_at", "updated_at", "is_platform_admin", "pwd_reset_required")
|
||||||
|
VALUES (1, 'admin', '管理员', 'admin', NULL, '$2a$10$BOm1iCFj3ObfBeyQxOvjVO659vXvIRGOd4YR62r0TUHqSusWW5bFS', 1, 0, '2026-02-09 09:54:21.880637', '2026-02-28 17:57:32.63338', 't', NULL);
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (9, 'biz_hotword_category', '热词类别', 1, '语音识别纠错分类', '2026-02-28 17:08:52.362532', '2026-02-28 17:08:52.362532');
|
||||||
|
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (10, 'biz_prompt_category', '提示词分类', 1, '会议总结模板分类', '2026-02-28 17:47:50.999655', '2026-02-28 17:47:50.999655');
|
||||||
|
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (11, 'biz_ai_provider', '模型提供商', 1, 'AI 模型服务商分类', '2026-03-02 10:10:16.653182', '2026-03-02 10:10:16.653182');
|
||||||
|
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (12, 'biz_speaker_label', '发言人角色', 1, '会议发言人的身份标签', '2026-03-02 16:15:58.193117', '2026-03-02 16:15:58.193117');
|
||||||
|
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (13, 'biz_prompt_level', '提示词模板属性', 1, '用于定义提示词模板的层级属性:1-预置模板(系统或租户级),0-个人模板', '2026-03-04 10:54:30.49116', '2026-03-04 10:54:30.49116');
|
||||||
|
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (22, 'biz_hotword_category', '人名', 'person', 1, 1, NULL, '2026-02-28 17:08:52.374667', '2026-02-28 17:08:52.374667');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (23, 'biz_hotword_category', '术语', 'term', 2, 1, NULL, '2026-02-28 17:08:52.374667', '2026-02-28 17:08:52.374667');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (24, 'biz_hotword_category', '地名', 'location', 3, 1, NULL, '2026-02-28 17:08:52.374667', '2026-02-28 17:08:52.374667');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (25, 'biz_hotword_category', '通用', 'general', 4, 1, NULL, '2026-02-28 17:08:52.374667', '2026-02-28 17:08:52.374667');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (26, 'biz_prompt_category', '全文纪要', 'summary', 1, 1, NULL, '2026-02-28 17:47:51.013288', '2026-02-28 17:47:51.013288');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (27, 'biz_prompt_category', '待办提取', 'todo', 2, 1, NULL, '2026-02-28 17:47:51.013288', '2026-02-28 17:47:51.013288');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (28, 'biz_prompt_category', '访谈整理', 'interview', 3, 1, NULL, '2026-02-28 17:47:51.013288', '2026-02-28 17:47:51.013288');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (29, 'biz_prompt_category', '创意构思', 'creative', 4, 1, NULL, '2026-02-28 17:47:51.013288', '2026-02-28 17:47:51.013288');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (30, 'biz_ai_provider', '阿里云', 'Aliyun', 1, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (31, 'biz_ai_provider', 'OpenAI', 'OpenAI', 2, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (32, 'biz_ai_provider', 'Gemini', 'Gemini', 3, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (33, 'biz_ai_provider', 'DeepSeek', 'DeepSeek', 4, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (34, 'biz_ai_provider', 'Kimi', 'Kimi', 5, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (35, 'biz_ai_provider', '自定义/本地', 'Custom', 6, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (36, 'biz_speaker_label', '主持人', 'host', 1, 1, NULL, '2026-03-02 16:15:58.205277', '2026-03-02 16:15:58.205277');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (37, 'biz_speaker_label', '汇报人', 'speaker', 2, 1, NULL, '2026-03-02 16:15:58.205277', '2026-03-02 16:15:58.205277');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (38, 'biz_speaker_label', '技术专家', 'expert', 3, 1, NULL, '2026-03-02 16:15:58.205277', '2026-03-02 16:15:58.205277');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (39, 'biz_speaker_label', '客户代表', 'customer', 4, 1, NULL, '2026-03-02 16:15:58.205277', '2026-03-02 16:15:58.205277');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (40, 'biz_prompt_level', '预置模板', '1', 1, 1, '平台系统预置或租户共享预置', '2026-03-04 10:55:42.163768', '2026-03-04 10:55:42.163768');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (41, 'biz_prompt_level', '个人模板', '0', 2, 1, '个人私有模板', '2026-03-04 10:55:42.175269', '2026-03-04 10:55:42.175269');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import com.imeeting.common.RedisKeys;
|
||||||
import com.imeeting.dto.biz.MeetingDTO;
|
import com.imeeting.dto.biz.MeetingDTO;
|
||||||
import com.imeeting.dto.biz.MeetingTranscriptVO;
|
import com.imeeting.dto.biz.MeetingTranscriptVO;
|
||||||
import com.imeeting.dto.biz.MeetingVO;
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
|
import com.imeeting.dto.biz.RealtimeMeetingCompleteDTO;
|
||||||
|
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
|
||||||
import com.imeeting.entity.biz.AiTask;
|
import com.imeeting.entity.biz.AiTask;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.security.LoginUser;
|
import com.imeeting.security.LoginUser;
|
||||||
|
|
@ -144,6 +146,28 @@ public class MeetingController {
|
||||||
return ApiResponse.ok(meetingService.createMeeting(dto));
|
return ApiResponse.ok(meetingService.createMeeting(dto));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/realtime/start")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<MeetingVO> createRealtime(@RequestBody MeetingDTO dto) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
if (dto.getPromptId() != null) {
|
||||||
|
boolean enabled = promptTemplateService.isTemplateEnabledForUser(
|
||||||
|
dto.getPromptId(),
|
||||||
|
loginUser.getTenantId(),
|
||||||
|
loginUser.getUserId(),
|
||||||
|
loginUser.getIsPlatformAdmin(),
|
||||||
|
loginUser.getIsTenantAdmin()
|
||||||
|
);
|
||||||
|
if (!enabled) {
|
||||||
|
return ApiResponse.error("总结模板不可用或已被你禁用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dto.setTenantId(loginUser.getTenantId());
|
||||||
|
dto.setCreatorId(loginUser.getUserId());
|
||||||
|
dto.setCreatorName(loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername());
|
||||||
|
return ApiResponse.ok(meetingService.createRealtimeMeeting(dto));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
public ApiResponse<PageResult<List<MeetingVO>>> page(
|
public ApiResponse<PageResult<List<MeetingVO>>> page(
|
||||||
|
|
@ -296,6 +320,40 @@ public class MeetingController {
|
||||||
return ApiResponse.ok(meetingService.getTranscripts(id));
|
return ApiResponse.ok(meetingService.getTranscripts(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/realtime/transcripts")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> appendRealtimeTranscripts(@PathVariable Long id, @RequestBody List<RealtimeTranscriptItemDTO> items) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
Meeting existing = meetingService.getById(id);
|
||||||
|
if (existing == null) {
|
||||||
|
return ApiResponse.error("会议不存在");
|
||||||
|
}
|
||||||
|
if (!existing.getCreatorId().equals(loginUser.getUserId())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||||
|
return ApiResponse.error("无权写入此会议的实时转录");
|
||||||
|
}
|
||||||
|
meetingService.appendRealtimeTranscripts(id, items);
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/realtime/complete")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> completeRealtimeMeeting(@PathVariable Long id, @RequestBody(required = false) RealtimeMeetingCompleteDTO dto) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
Meeting existing = meetingService.getById(id);
|
||||||
|
if (existing == null) {
|
||||||
|
return ApiResponse.error("会议不存在");
|
||||||
|
}
|
||||||
|
if (!existing.getCreatorId().equals(loginUser.getUserId())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||||
|
return ApiResponse.error("无权结束此实时会议");
|
||||||
|
}
|
||||||
|
meetingService.completeRealtimeMeeting(id, dto != null ? dto.getAudioUrl() : null);
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
@PutMapping("/speaker")
|
@PutMapping("/speaker")
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
public ApiResponse<Boolean> updateSpeaker(@RequestBody Map<String, Object> params) {
|
public ApiResponse<Boolean> updateSpeaker(@RequestBody Map<String, Object> params) {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ public class MeetingVO {
|
||||||
private List<Long> participantIds;
|
private List<Long> participantIds;
|
||||||
private String tags;
|
private String tags;
|
||||||
private String audioUrl;
|
private String audioUrl;
|
||||||
|
private Integer duration;
|
||||||
private String summaryContent;
|
private String summaryContent;
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,22 @@ package com.imeeting.service.biz;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.imeeting.common.PageResult;
|
import com.imeeting.common.PageResult;
|
||||||
import com.imeeting.dto.biz.MeetingDTO;
|
import com.imeeting.dto.biz.MeetingDTO;
|
||||||
|
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
|
||||||
|
import com.imeeting.dto.biz.MeetingTranscriptVO;
|
||||||
import com.imeeting.dto.biz.MeetingVO;
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.entity.biz.Meeting;
|
||||||
|
|
||||||
import com.imeeting.dto.biz.MeetingTranscriptVO;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface MeetingService extends IService<Meeting> {
|
public interface MeetingService extends IService<Meeting> {
|
||||||
MeetingVO createMeeting(MeetingDTO dto);
|
MeetingVO createMeeting(MeetingDTO dto);
|
||||||
|
MeetingVO createRealtimeMeeting(MeetingDTO dto);
|
||||||
PageResult<List<MeetingVO>> pageMeetings(Integer current, Integer size, String title, Long tenantId, Long userId, String userName, String viewType, boolean isAdmin);
|
PageResult<List<MeetingVO>> pageMeetings(Integer current, Integer size, String title, Long tenantId, Long userId, String userName, String viewType, boolean isAdmin);
|
||||||
void deleteMeeting(Long id);
|
void deleteMeeting(Long id);
|
||||||
MeetingVO getDetail(Long id);
|
MeetingVO getDetail(Long id);
|
||||||
List<MeetingTranscriptVO> getTranscripts(Long meetingId);
|
List<MeetingTranscriptVO> getTranscripts(Long meetingId);
|
||||||
|
void appendRealtimeTranscripts(Long meetingId, List<RealtimeTranscriptItemDTO> items);
|
||||||
|
void completeRealtimeMeeting(Long meetingId, String audioUrl);
|
||||||
void updateSpeakerInfo(Long meetingId, String speakerId, String newName, String label);
|
void updateSpeakerInfo(Long meetingId, String speakerId, String newName, String label);
|
||||||
void updateMeetingParticipants(Long meetingId, String participants);
|
void updateMeetingParticipants(Long meetingId, String participants);
|
||||||
void reSummary(Long meetingId, Long summaryModelId, Long promptId);
|
void reSummary(Long meetingId, Long summaryModelId, Long promptId);
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
private final LlmModelMapper llmModelMapper;
|
private final LlmModelMapper llmModelMapper;
|
||||||
|
|
||||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||||
.connectTimeout(Duration.ofSeconds(10))
|
.connectTimeout(Duration.ofSeconds(300))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -232,8 +232,8 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveModelListUrl(String providerKey, String baseUrl, String apiKey) {
|
private String resolveModelListUrl(String providerKey, String baseUrl, String apiKey) {
|
||||||
if (baseUrl.contains("3050")) {
|
if ("Custom".equalsIgnoreCase(providerKey)) {
|
||||||
return "http://10.100.51.199:3050/api/asrconfig";
|
return baseUrl+"/api/asrconfig";
|
||||||
}
|
}
|
||||||
if ("gemini".equals(providerKey) || "google".equals(providerKey)) {
|
if ("gemini".equals(providerKey) || "google".equals(providerKey)) {
|
||||||
if (apiKey == null || apiKey.isBlank()) {
|
if (apiKey == null || apiKey.isBlank()) {
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,8 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
private String uploadPath;
|
private String uploadPath;
|
||||||
|
|
||||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||||
.connectTimeout(Duration.ofSeconds(10))
|
.connectTimeout(Duration.ofSeconds(300))
|
||||||
|
.version(HttpClient.Version.HTTP_1_1)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -206,7 +207,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
updateProgress(meeting.getId(), (int)(currentPercent * 0.85), data.path("message").asText(), eta);
|
updateProgress(meeting.getId(), (int)(currentPercent * 0.85), data.path("message").asText(), eta);
|
||||||
|
|
||||||
if (currentPercent > 0 && currentPercent == lastPercent) {
|
if (currentPercent > 0 && currentPercent == lastPercent) {
|
||||||
if (++unchangedCount > 45) throw new RuntimeException("识别任务长时间无进度增长,自动强制超时");
|
if (++unchangedCount > 300) throw new RuntimeException("识别任务长时间无进度增长,自动强制超时");
|
||||||
} else {
|
} else {
|
||||||
unchangedCount = 0;
|
unchangedCount = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -316,15 +317,19 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
this.updateById(taskRecord);
|
this.updateById(taskRecord);
|
||||||
|
|
||||||
String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions");
|
String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions");
|
||||||
|
String requestBody = objectMapper.writeValueAsString(req);
|
||||||
|
log.info("Sending LLM summary request to url={}, body={}", url, requestBody);
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(url))
|
.uri(URI.create(url))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
.header("Accept", "application/json")
|
||||||
.header("Authorization", "Bearer " + llmModel.getApiKey())
|
.header("Authorization", "Bearer " + llmModel.getApiKey())
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(req)))
|
.POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
log.info("LLM summary response status={}, body={}", response.statusCode(), response.body());
|
||||||
JsonNode respNode = objectMapper.readTree(response.body());
|
JsonNode respNode = objectMapper.readTree(response.body());
|
||||||
|
|
||||||
if (response.statusCode() == 200 && respNode.has("choices")) {
|
if (response.statusCode() == 200 && respNode.has("choices")) {
|
||||||
|
|
|
||||||
|
|
@ -6,43 +6,43 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.imeeting.common.PageResult;
|
import com.imeeting.common.PageResult;
|
||||||
import com.imeeting.dto.biz.MeetingDTO;
|
import com.imeeting.dto.biz.MeetingDTO;
|
||||||
import com.imeeting.dto.biz.MeetingVO;
|
|
||||||
import com.imeeting.dto.biz.MeetingTranscriptVO;
|
import com.imeeting.dto.biz.MeetingTranscriptVO;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
import com.imeeting.entity.biz.AiTask;
|
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
|
||||||
import com.imeeting.entity.biz.PromptTemplate;
|
|
||||||
import com.imeeting.entity.biz.MeetingTranscript;
|
|
||||||
import com.imeeting.entity.biz.HotWord;
|
|
||||||
import com.imeeting.entity.SysUser;
|
import com.imeeting.entity.SysUser;
|
||||||
|
import com.imeeting.entity.biz.AiTask;
|
||||||
|
import com.imeeting.entity.biz.HotWord;
|
||||||
|
import com.imeeting.entity.biz.Meeting;
|
||||||
|
import com.imeeting.entity.biz.MeetingTranscript;
|
||||||
|
import com.imeeting.entity.biz.PromptTemplate;
|
||||||
|
import com.imeeting.event.MeetingCreatedEvent;
|
||||||
|
import com.imeeting.mapper.SysUserMapper;
|
||||||
import com.imeeting.mapper.biz.AiTaskMapper;
|
import com.imeeting.mapper.biz.AiTaskMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingMapper;
|
import com.imeeting.mapper.biz.MeetingMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||||
import com.imeeting.mapper.SysUserMapper;
|
|
||||||
import com.imeeting.event.MeetingCreatedEvent;
|
|
||||||
import com.imeeting.service.biz.MeetingService;
|
|
||||||
import com.imeeting.service.biz.AiModelService;
|
import com.imeeting.service.biz.AiModelService;
|
||||||
import com.imeeting.service.biz.PromptTemplateService;
|
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import com.imeeting.service.biz.HotWordService;
|
import com.imeeting.service.biz.HotWordService;
|
||||||
|
import com.imeeting.service.biz.MeetingService;
|
||||||
|
import com.imeeting.service.biz.PromptTemplateService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.FileSystemUtils;
|
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|
@ -65,48 +65,10 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public MeetingVO createMeeting(MeetingDTO dto) {
|
public MeetingVO createMeeting(MeetingDTO dto) {
|
||||||
Meeting meeting = new Meeting();
|
Meeting meeting = initMeeting(dto, 0);
|
||||||
meeting.setTitle(dto.getTitle());
|
meeting.setAudioUrl(relocateAudioUrl(meeting.getId(), dto.getAudioUrl()));
|
||||||
meeting.setMeetingTime(dto.getMeetingTime());
|
|
||||||
meeting.setParticipants(dto.getParticipants());
|
|
||||||
meeting.setTags(dto.getTags());
|
|
||||||
meeting.setCreatorId(dto.getCreatorId());
|
|
||||||
meeting.setCreatorName(dto.getCreatorName());
|
|
||||||
meeting.setTenantId(dto.getTenantId() != null ? dto.getTenantId() : 0L);
|
|
||||||
meeting.setStatus(0);
|
|
||||||
|
|
||||||
this.save(meeting); // Save to get meeting ID
|
|
||||||
|
|
||||||
// File moving logic
|
|
||||||
String audioUrl = dto.getAudioUrl();
|
|
||||||
if (audioUrl != null && audioUrl.startsWith("/api/static/audio/")) {
|
|
||||||
try {
|
|
||||||
String fileName = audioUrl.substring(audioUrl.lastIndexOf("/") + 1);
|
|
||||||
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
|
|
||||||
Path sourcePath = Paths.get(basePath, "audio", fileName);
|
|
||||||
|
|
||||||
if (Files.exists(sourcePath)) {
|
|
||||||
String ext = "";
|
|
||||||
int dotIdx = fileName.lastIndexOf('.');
|
|
||||||
if (dotIdx > 0) {
|
|
||||||
ext = fileName.substring(dotIdx);
|
|
||||||
}
|
|
||||||
String targetDir = basePath + "meetings/" + meeting.getId();
|
|
||||||
Files.createDirectories(Paths.get(targetDir));
|
|
||||||
Path targetPath = Paths.get(targetDir, "source_audio" + ext);
|
|
||||||
Files.move(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
|
|
||||||
audioUrl = "/api/static/meetings/" + meeting.getId() + "/source_audio" + ext;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to move audio file for meeting {}", meeting.getId(), e);
|
|
||||||
throw new RuntimeException("文件处理失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
meeting.setAudioUrl(audioUrl);
|
|
||||||
this.updateById(meeting);
|
this.updateById(meeting);
|
||||||
|
|
||||||
// ASR Task
|
|
||||||
AiTask asrTask = new AiTask();
|
AiTask asrTask = new AiTask();
|
||||||
asrTask.setMeetingId(meeting.getId());
|
asrTask.setMeetingId(meeting.getId());
|
||||||
asrTask.setTaskType("ASR");
|
asrTask.setTaskType("ASR");
|
||||||
|
|
@ -121,41 +83,33 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
|
||||||
finalHotWords = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
finalHotWords = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
||||||
.eq(HotWord::getTenantId, meeting.getTenantId())
|
.eq(HotWord::getTenantId, meeting.getTenantId())
|
||||||
.eq(HotWord::getStatus, 1))
|
.eq(HotWord::getStatus, 1))
|
||||||
.stream().map(HotWord::getWord).collect(Collectors.toList());
|
.stream()
|
||||||
|
.map(HotWord::getWord)
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
asrConfig.put("hotWords", finalHotWords);
|
asrConfig.put("hotWords", finalHotWords);
|
||||||
asrTask.setTaskConfig(asrConfig);
|
asrTask.setTaskConfig(asrConfig);
|
||||||
aiTaskService.save(asrTask);
|
aiTaskService.save(asrTask);
|
||||||
|
|
||||||
// SUMMARY Task
|
createSummaryTask(meeting.getId(), dto.getSummaryModelId(), dto.getPromptId());
|
||||||
AiTask sumTask = new AiTask();
|
|
||||||
sumTask.setMeetingId(meeting.getId());
|
|
||||||
sumTask.setTaskType("SUMMARY");
|
|
||||||
sumTask.setStatus(0);
|
|
||||||
|
|
||||||
Map<String, Object> sumConfig = new HashMap<>();
|
|
||||||
sumConfig.put("summaryModelId", dto.getSummaryModelId());
|
|
||||||
if (dto.getPromptId() != null) {
|
|
||||||
PromptTemplate template = promptTemplateService.getById(dto.getPromptId());
|
|
||||||
if (template != null) {
|
|
||||||
sumConfig.put("promptContent", template.getPromptContent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sumTask.setTaskConfig(sumConfig);
|
|
||||||
aiTaskService.save(sumTask);
|
|
||||||
|
|
||||||
eventPublisher.publishEvent(new MeetingCreatedEvent(meeting.getId()));
|
eventPublisher.publishEvent(new MeetingCreatedEvent(meeting.getId()));
|
||||||
return toVO(meeting, false);
|
return toVO(meeting, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public MeetingVO createRealtimeMeeting(MeetingDTO dto) {
|
||||||
|
Meeting meeting = initMeeting(dto, 1);
|
||||||
|
createSummaryTask(meeting.getId(), dto.getSummaryModelId(), dto.getPromptId());
|
||||||
|
return toVO(meeting, false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageResult<List<MeetingVO>> pageMeetings(Integer current, Integer size, String title, Long tenantId, Long userId, String userName, String viewType, boolean isAdmin) {
|
public PageResult<List<MeetingVO>> pageMeetings(Integer current, Integer size, String title, Long tenantId, Long userId, String userName, String viewType, boolean isAdmin) {
|
||||||
LambdaQueryWrapper<Meeting> wrapper = new LambdaQueryWrapper<Meeting>()
|
LambdaQueryWrapper<Meeting> wrapper = new LambdaQueryWrapper<Meeting>()
|
||||||
.eq(Meeting::getTenantId, tenantId);
|
.eq(Meeting::getTenantId, tenantId);
|
||||||
|
|
||||||
if (isAdmin && "all".equals(viewType)) {
|
if (!isAdmin || !"all".equals(viewType)) {
|
||||||
// 管理员全局可见
|
|
||||||
} else {
|
|
||||||
String userIdStr = String.valueOf(userId);
|
String userIdStr = String.valueOf(userId);
|
||||||
if ("created".equals(viewType)) {
|
if ("created".equals(viewType)) {
|
||||||
wrapper.eq(Meeting::getCreatorId, userId);
|
wrapper.eq(Meeting::getCreatorId, userId);
|
||||||
|
|
@ -201,7 +155,8 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
|
||||||
return transcriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>()
|
return transcriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>()
|
||||||
.eq(MeetingTranscript::getMeetingId, meetingId)
|
.eq(MeetingTranscript::getMeetingId, meetingId)
|
||||||
.orderByAsc(MeetingTranscript::getStartTime))
|
.orderByAsc(MeetingTranscript::getStartTime))
|
||||||
.stream().map(t -> {
|
.stream()
|
||||||
|
.map(t -> {
|
||||||
MeetingTranscriptVO vo = new MeetingTranscriptVO();
|
MeetingTranscriptVO vo = new MeetingTranscriptVO();
|
||||||
vo.setId(t.getId());
|
vo.setId(t.getId());
|
||||||
vo.setSpeakerId(t.getSpeakerId());
|
vo.setSpeakerId(t.getSpeakerId());
|
||||||
|
|
@ -214,6 +169,75 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void appendRealtimeTranscripts(Long meetingId, List<RealtimeTranscriptItemDTO> items) {
|
||||||
|
if (items == null || items.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer maxSortOrder = transcriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>()
|
||||||
|
.eq(MeetingTranscript::getMeetingId, meetingId)
|
||||||
|
.orderByDesc(MeetingTranscript::getSortOrder)
|
||||||
|
.last("LIMIT 1"))
|
||||||
|
.stream()
|
||||||
|
.findFirst()
|
||||||
|
.map(MeetingTranscript::getSortOrder)
|
||||||
|
.orElse(0);
|
||||||
|
|
||||||
|
int nextSortOrder = maxSortOrder == null ? 0 : maxSortOrder + 1;
|
||||||
|
for (RealtimeTranscriptItemDTO item : items) {
|
||||||
|
if (item.getContent() == null || item.getContent().isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
MeetingTranscript existing = transcriptMapper.selectOne(new LambdaQueryWrapper<MeetingTranscript>()
|
||||||
|
.eq(MeetingTranscript::getMeetingId, meetingId)
|
||||||
|
.eq(MeetingTranscript::getContent, item.getContent().trim())
|
||||||
|
.eq(item.getSpeakerId() != null && !item.getSpeakerId().isBlank(), MeetingTranscript::getSpeakerId, item.getSpeakerId())
|
||||||
|
.eq(item.getStartTime() != null, MeetingTranscript::getStartTime, item.getStartTime())
|
||||||
|
.eq(item.getEndTime() != null, MeetingTranscript::getEndTime, item.getEndTime())
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (existing != null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
MeetingTranscript transcript = new MeetingTranscript();
|
||||||
|
transcript.setMeetingId(meetingId);
|
||||||
|
transcript.setSpeakerId(resolveSpeakerId(item));
|
||||||
|
transcript.setSpeakerName(resolveSpeakerName(item));
|
||||||
|
transcript.setContent(item.getContent().trim());
|
||||||
|
transcript.setStartTime(item.getStartTime());
|
||||||
|
transcript.setEndTime(item.getEndTime());
|
||||||
|
transcript.setSortOrder(nextSortOrder++);
|
||||||
|
transcriptMapper.insert(transcript);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void completeRealtimeMeeting(Long meetingId, String audioUrl) {
|
||||||
|
Meeting meeting = this.getById(meetingId);
|
||||||
|
if (meeting == null) {
|
||||||
|
throw new RuntimeException("Meeting not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioUrl != null && !audioUrl.isBlank()) {
|
||||||
|
meeting.setAudioUrl(relocateAudioUrl(meetingId, audioUrl));
|
||||||
|
this.updateById(meeting);
|
||||||
|
}
|
||||||
|
|
||||||
|
long transcriptCount = transcriptMapper.selectCount(new LambdaQueryWrapper<MeetingTranscript>()
|
||||||
|
.eq(MeetingTranscript::getMeetingId, meetingId));
|
||||||
|
if (transcriptCount <= 0) {
|
||||||
|
meeting.setStatus(4);
|
||||||
|
this.updateById(meeting);
|
||||||
|
throw new RuntimeException("未接收到可用的实时转录内容");
|
||||||
|
}
|
||||||
|
|
||||||
|
aiTaskService.dispatchSummaryTask(meetingId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void updateSpeakerInfo(Long meetingId, String speakerId, String newName, String label) {
|
public void updateSpeakerInfo(Long meetingId, String speakerId, String newName, String label) {
|
||||||
|
|
@ -236,32 +260,19 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void reSummary(Long meetingId, Long summaryModelId, Long promptId) {
|
public void reSummary(Long meetingId, Long summaryModelId, Long promptId) {
|
||||||
Meeting meeting = this.getById(meetingId);
|
Meeting meeting = this.getById(meetingId);
|
||||||
if (meeting == null) throw new RuntimeException("Meeting not found");
|
if (meeting == null) {
|
||||||
|
throw new RuntimeException("Meeting not found");
|
||||||
AiTask sumTask = new AiTask();
|
|
||||||
sumTask.setMeetingId(meetingId);
|
|
||||||
sumTask.setTaskType("SUMMARY");
|
|
||||||
sumTask.setStatus(0);
|
|
||||||
|
|
||||||
Map<String, Object> sumConfig = new HashMap<>();
|
|
||||||
sumConfig.put("summaryModelId", summaryModelId);
|
|
||||||
if (promptId != null) {
|
|
||||||
PromptTemplate template = promptTemplateService.getById(promptId);
|
|
||||||
if (template != null) {
|
|
||||||
sumConfig.put("promptContent", template.getPromptContent());
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
sumTask.setTaskConfig(sumConfig);
|
|
||||||
aiTaskService.save(sumTask);
|
|
||||||
|
|
||||||
|
createSummaryTask(meetingId, summaryModelId, promptId);
|
||||||
meeting.setStatus(2);
|
meeting.setStatus(2);
|
||||||
this.updateById(meeting);
|
this.updateById(meeting);
|
||||||
aiTaskService.dispatchSummaryTask(meetingId);
|
aiTaskService.dispatchSummaryTask(meetingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public java.util.Map<String, Object> getDashboardStats(Long tenantId, Long userId, boolean isAdmin) {
|
public Map<String, Object> getDashboardStats(Long tenantId, Long userId, boolean isAdmin) {
|
||||||
java.util.Map<String, Object> stats = new java.util.HashMap<>();
|
Map<String, Object> stats = new HashMap<>();
|
||||||
LambdaQueryWrapper<Meeting> baseWrapper = new LambdaQueryWrapper<Meeting>().eq(Meeting::getTenantId, tenantId);
|
LambdaQueryWrapper<Meeting> baseWrapper = new LambdaQueryWrapper<Meeting>().eq(Meeting::getTenantId, tenantId);
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
String userIdStr = String.valueOf(userId);
|
String userIdStr = String.valueOf(userId);
|
||||||
|
|
@ -294,6 +305,92 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
|
||||||
return this.list(wrapper).stream().map(m -> toVO(m, false)).collect(Collectors.toList());
|
return this.list(wrapper).stream().map(m -> toVO(m, false)).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createSummaryTask(Long meetingId, Long summaryModelId, Long promptId) {
|
||||||
|
AiTask sumTask = new AiTask();
|
||||||
|
sumTask.setMeetingId(meetingId);
|
||||||
|
sumTask.setTaskType("SUMMARY");
|
||||||
|
sumTask.setStatus(0);
|
||||||
|
|
||||||
|
Map<String, Object> sumConfig = new HashMap<>();
|
||||||
|
sumConfig.put("summaryModelId", summaryModelId);
|
||||||
|
if (promptId != null) {
|
||||||
|
PromptTemplate template = promptTemplateService.getById(promptId);
|
||||||
|
if (template != null) {
|
||||||
|
sumConfig.put("promptContent", template.getPromptContent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sumTask.setTaskConfig(sumConfig);
|
||||||
|
aiTaskService.save(sumTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Meeting initMeeting(MeetingDTO dto, int status) {
|
||||||
|
Meeting meeting = new Meeting();
|
||||||
|
meeting.setTitle(dto.getTitle());
|
||||||
|
meeting.setMeetingTime(dto.getMeetingTime());
|
||||||
|
meeting.setParticipants(dto.getParticipants());
|
||||||
|
meeting.setTags(dto.getTags());
|
||||||
|
meeting.setCreatorId(dto.getCreatorId());
|
||||||
|
meeting.setCreatorName(dto.getCreatorName());
|
||||||
|
meeting.setTenantId(dto.getTenantId() != null ? dto.getTenantId() : 0L);
|
||||||
|
meeting.setAudioUrl(dto.getAudioUrl());
|
||||||
|
meeting.setStatus(status);
|
||||||
|
this.save(meeting);
|
||||||
|
return meeting;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String relocateAudioUrl(Long meetingId, String audioUrl) {
|
||||||
|
if (audioUrl == null || audioUrl.isBlank()) {
|
||||||
|
return audioUrl;
|
||||||
|
}
|
||||||
|
if (!audioUrl.startsWith("/api/static/audio/")) {
|
||||||
|
return audioUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String fileName = audioUrl.substring(audioUrl.lastIndexOf("/") + 1);
|
||||||
|
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
|
||||||
|
Path sourcePath = Paths.get(basePath, "audio", fileName);
|
||||||
|
if (!Files.exists(sourcePath)) {
|
||||||
|
return audioUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ext = "";
|
||||||
|
int dotIdx = fileName.lastIndexOf('.');
|
||||||
|
if (dotIdx > 0) {
|
||||||
|
ext = fileName.substring(dotIdx);
|
||||||
|
}
|
||||||
|
Path targetDir = Paths.get(basePath, "meetings", String.valueOf(meetingId));
|
||||||
|
Files.createDirectories(targetDir);
|
||||||
|
Path targetPath = targetDir.resolve("source_audio" + ext);
|
||||||
|
Files.move(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
return "/api/static/meetings/" + meetingId + "/source_audio" + ext;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to move audio file for meeting {}", meetingId, e);
|
||||||
|
throw new RuntimeException("文件处理失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveSpeakerId(RealtimeTranscriptItemDTO item) {
|
||||||
|
if (item.getSpeakerId() != null && !item.getSpeakerId().isBlank()) {
|
||||||
|
return item.getSpeakerId();
|
||||||
|
}
|
||||||
|
return "spk_0";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveSpeakerName(RealtimeTranscriptItemDTO item) {
|
||||||
|
if (item.getSpeakerName() != null && !item.getSpeakerName().isBlank()) {
|
||||||
|
return item.getSpeakerName();
|
||||||
|
}
|
||||||
|
String speakerId = resolveSpeakerId(item);
|
||||||
|
if (speakerId.matches("\\d+")) {
|
||||||
|
SysUser user = sysUserMapper.selectById(Long.parseLong(speakerId));
|
||||||
|
if (user != null) {
|
||||||
|
return user.getDisplayName() != null ? user.getDisplayName() : user.getUsername();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return speakerId;
|
||||||
|
}
|
||||||
|
|
||||||
private MeetingVO toVO(Meeting meeting, boolean includeSummary) {
|
private MeetingVO toVO(Meeting meeting, boolean includeSummary) {
|
||||||
MeetingVO vo = new MeetingVO();
|
MeetingVO vo = new MeetingVO();
|
||||||
vo.setId(meeting.getId());
|
vo.setId(meeting.getId());
|
||||||
|
|
@ -304,17 +401,23 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
|
||||||
vo.setMeetingTime(meeting.getMeetingTime());
|
vo.setMeetingTime(meeting.getMeetingTime());
|
||||||
vo.setTags(meeting.getTags());
|
vo.setTags(meeting.getTags());
|
||||||
vo.setAudioUrl(meeting.getAudioUrl());
|
vo.setAudioUrl(meeting.getAudioUrl());
|
||||||
|
vo.setDuration(resolveMeetingDuration(meeting.getId()));
|
||||||
vo.setStatus(meeting.getStatus());
|
vo.setStatus(meeting.getStatus());
|
||||||
vo.setCreatedAt(meeting.getCreatedAt());
|
vo.setCreatedAt(meeting.getCreatedAt());
|
||||||
|
|
||||||
if (meeting.getParticipants() != null && !meeting.getParticipants().isEmpty()) {
|
if (meeting.getParticipants() != null && !meeting.getParticipants().isEmpty()) {
|
||||||
try {
|
try {
|
||||||
List<Long> userIds = Arrays.stream(meeting.getParticipants().split(","))
|
List<Long> userIds = Arrays.stream(meeting.getParticipants().split(","))
|
||||||
.map(String::trim).filter(s -> !s.isEmpty()).map(Long::valueOf).collect(Collectors.toList());
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.map(Long::valueOf)
|
||||||
|
.collect(Collectors.toList());
|
||||||
vo.setParticipantIds(userIds);
|
vo.setParticipantIds(userIds);
|
||||||
if (!userIds.isEmpty()) {
|
if (!userIds.isEmpty()) {
|
||||||
List<SysUser> users = sysUserMapper.selectBatchIds(userIds);
|
List<SysUser> users = sysUserMapper.selectBatchIds(userIds);
|
||||||
String names = users.stream().map(u -> u.getDisplayName() != null ? u.getDisplayName() : u.getUsername()).collect(Collectors.joining(", "));
|
String names = users.stream()
|
||||||
|
.map(u -> u.getDisplayName() != null ? u.getDisplayName() : u.getUsername())
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
vo.setParticipants(names);
|
vo.setParticipants(names);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
@ -363,6 +466,27 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Integer resolveMeetingDuration(Long meetingId) {
|
||||||
|
MeetingTranscript latestTranscript = transcriptMapper.selectOne(new LambdaQueryWrapper<MeetingTranscript>()
|
||||||
|
.eq(MeetingTranscript::getMeetingId, meetingId)
|
||||||
|
.isNotNull(MeetingTranscript::getEndTime)
|
||||||
|
.orderByDesc(MeetingTranscript::getEndTime)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (latestTranscript != null && latestTranscript.getEndTime() != null && latestTranscript.getEndTime() > 0) {
|
||||||
|
return latestTranscript.getEndTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
latestTranscript = transcriptMapper.selectOne(new LambdaQueryWrapper<MeetingTranscript>()
|
||||||
|
.eq(MeetingTranscript::getMeetingId, meetingId)
|
||||||
|
.isNotNull(MeetingTranscript::getStartTime)
|
||||||
|
.orderByDesc(MeetingTranscript::getStartTime)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (latestTranscript != null && latestTranscript.getStartTime() != null && latestTranscript.getStartTime() > 0) {
|
||||||
|
return latestTranscript.getStartTime();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private String stripFrontMatter(String markdown) {
|
private String stripFrontMatter(String markdown) {
|
||||||
if (markdown == null || markdown.isBlank()) {
|
if (markdown == null || markdown.isBlank()) {
|
||||||
return markdown;
|
return markdown;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8081
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://10.100.51.199:5432/imeeting_db
|
url: jdbc:postgresql://192.168.1.55:5432/imeeting_db
|
||||||
username: postgres
|
username: postgres
|
||||||
password: postgres
|
password: postgres
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
host: 10.100.51.199
|
host: 192.168.1.55
|
||||||
port: 6379
|
port: 6379
|
||||||
password: unis@123
|
password: unis@123
|
||||||
database: 15
|
database: 15
|
||||||
|
|
@ -40,7 +40,7 @@ security:
|
||||||
secret: change-me-please-change-me-32bytes
|
secret: change-me-please-change-me-32bytes
|
||||||
|
|
||||||
app:
|
app:
|
||||||
server-base-url: http://10.100.52.13:8080 # 本地应用对外暴露的 IP 和端口
|
server-base-url: http://10.100.52.13:${server.port} # 本地应用对外暴露的 IP 和端口
|
||||||
upload-path: D:/data/imeeting/uploads/
|
upload-path: D:/data/imeeting/uploads/
|
||||||
resource-prefix: /api/static/
|
resource-prefix: /api/static/
|
||||||
captcha:
|
captcha:
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,12 @@ export interface MeetingDTO {
|
||||||
meetingTime: string;
|
meetingTime: string;
|
||||||
participants: string;
|
participants: string;
|
||||||
tags: string;
|
tags: string;
|
||||||
audioUrl: string;
|
audioUrl?: string;
|
||||||
asrModelId: number;
|
asrModelId: number;
|
||||||
|
summaryModelId?: number;
|
||||||
promptId: number;
|
promptId: number;
|
||||||
|
useSpkId?: number;
|
||||||
|
hotWords?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMeetingPage = (params: {
|
export const getMeetingPage = (params: {
|
||||||
|
|
@ -47,6 +50,35 @@ export const createMeeting = (data: MeetingDTO) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface RealtimeTranscriptItemDTO {
|
||||||
|
speakerId?: string;
|
||||||
|
speakerName?: string;
|
||||||
|
content: string;
|
||||||
|
startTime?: number;
|
||||||
|
endTime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createRealtimeMeeting = (data: MeetingDTO) => {
|
||||||
|
return http.post<any, { code: string; data: MeetingVO; msg: string }>(
|
||||||
|
"/api/biz/meeting/realtime/start",
|
||||||
|
data
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appendRealtimeTranscripts = (meetingId: number, data: RealtimeTranscriptItemDTO[]) => {
|
||||||
|
return http.post<any, { code: string; data: boolean; msg: string }>(
|
||||||
|
`/api/biz/meeting/${meetingId}/realtime/transcripts`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const completeRealtimeMeeting = (meetingId: number, data?: { audioUrl?: string }) => {
|
||||||
|
return http.post<any, { code: string; data: boolean; msg: string }>(
|
||||||
|
`/api/biz/meeting/${meetingId}/realtime/complete`,
|
||||||
|
data || {}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteMeeting = (id: number) => {
|
export const deleteMeeting = (id: number) => {
|
||||||
return http.delete<any, { code: string; data: boolean; msg: string }>(
|
return http.delete<any, { code: string; data: boolean; msg: string }>(
|
||||||
`/api/biz/meeting/${id}`
|
`/api/biz/meeting/${id}`
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
AutoComplete,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Col,
|
Col,
|
||||||
|
|
@ -82,19 +83,21 @@ const AiModels: React.FC = () => {
|
||||||
if (!profileStr) {
|
if (!profileStr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = JSON.parse(profileStr);
|
const profile = JSON.parse(profileStr);
|
||||||
return profile.isPlatformAdmin === true;
|
return profile.isPlatformAdmin === true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
void fetchData();
|
||||||
}, [current, size, searchName, activeType]);
|
}, [current, size, searchName, activeType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!drawerVisible || !provider) {
|
if (!drawerVisible || !provider) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const providerItem = providers.find((i) => i.itemValue === provider);
|
|
||||||
|
const providerItem = providers.find((item) => item.itemValue === provider);
|
||||||
const providerLabel = providerItem?.itemLabel || provider;
|
const providerLabel = providerItem?.itemLabel || provider;
|
||||||
const currentDisplayName = form.getFieldValue("modelName");
|
const currentDisplayName = form.getFieldValue("modelName");
|
||||||
if (!editingId && (!currentDisplayName || modelNameAutoFilledRef.current)) {
|
if (!editingId && (!currentDisplayName || modelNameAutoFilledRef.current)) {
|
||||||
|
|
@ -130,6 +133,7 @@ const AiModels: React.FC = () => {
|
||||||
const openDrawer = (record?: AiModelVO) => {
|
const openDrawer = (record?: AiModelVO) => {
|
||||||
setRemoteModels([]);
|
setRemoteModels([]);
|
||||||
modelNameAutoFilledRef.current = false;
|
modelNameAutoFilledRef.current = false;
|
||||||
|
|
||||||
if (record) {
|
if (record) {
|
||||||
setEditingId(record.id);
|
setEditingId(record.id);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
|
|
@ -153,18 +157,20 @@ const AiModels: React.FC = () => {
|
||||||
apiPath: "/v1/chat/completions",
|
apiPath: "/v1/chat/completions",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setDrawerVisible(true);
|
setDrawerVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFetchRemote = async () => {
|
const handleFetchRemote = async () => {
|
||||||
const vals = form.getFieldsValue(["provider", "baseUrl", "apiKey"]);
|
const values = form.getFieldsValue(["provider", "baseUrl", "apiKey"]);
|
||||||
if (!vals.provider || !vals.baseUrl) {
|
if (!values.provider || !values.baseUrl) {
|
||||||
message.warning("请先填写提供商和 Base URL");
|
message.warning("请先填写提供商和 Base URL");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFetchLoading(true);
|
setFetchLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await getRemoteModelList(vals);
|
const res = await getRemoteModelList(values);
|
||||||
const rawModels = (res as any)?.data?.data ?? (Array.isArray(res) ? res : []);
|
const rawModels = (res as any)?.data?.data ?? (Array.isArray(res) ? res : []);
|
||||||
const models = Array.isArray(rawModels) ? rawModels : [];
|
const models = Array.isArray(rawModels) ? rawModels : [];
|
||||||
setRemoteModels(models);
|
setRemoteModels(models);
|
||||||
|
|
@ -203,7 +209,7 @@ const AiModels: React.FC = () => {
|
||||||
message.success("新增成功");
|
message.success("新增成功");
|
||||||
}
|
}
|
||||||
setDrawerVisible(false);
|
setDrawerVisible(false);
|
||||||
fetchData();
|
void fetchData();
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitLoading(false);
|
setSubmitLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -212,7 +218,7 @@ const AiModels: React.FC = () => {
|
||||||
const handleDelete = async (record: AiModelVO) => {
|
const handleDelete = async (record: AiModelVO) => {
|
||||||
await deleteAiModelByType(record.id, record.modelType);
|
await deleteAiModelByType(record.id, record.modelType);
|
||||||
message.success("删除成功");
|
message.success("删除成功");
|
||||||
fetchData();
|
void fetchData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
|
@ -236,9 +242,9 @@ const AiModels: React.FC = () => {
|
||||||
title: "提供商",
|
title: "提供商",
|
||||||
dataIndex: "provider",
|
dataIndex: "provider",
|
||||||
key: "provider",
|
key: "provider",
|
||||||
render: (val: string) => {
|
render: (value: string) => {
|
||||||
const item = providers.find((i) => i.itemValue === val);
|
const item = providers.find((providerItem) => providerItem.itemValue === value);
|
||||||
return item ? <Tag>{item.itemLabel}</Tag> : val;
|
return item ? <Tag>{item.itemLabel}</Tag> : value;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ title: "模型名称(code)", dataIndex: "modelCode", key: "modelCode" },
|
{ title: "模型名称(code)", dataIndex: "modelCode", key: "modelCode" },
|
||||||
|
|
@ -284,7 +290,7 @@ const AiModels: React.FC = () => {
|
||||||
placeholder="搜索模型名称"
|
placeholder="搜索模型名称"
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
allowClear
|
allowClear
|
||||||
onPressEnter={(e) => setSearchName((e.target as HTMLInputElement).value)}
|
onPressEnter={(event) => setSearchName((event.target as HTMLInputElement).value)}
|
||||||
style={{ width: 220 }}
|
style={{ width: 220 }}
|
||||||
/>
|
/>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}>
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}>
|
||||||
|
|
@ -314,9 +320,9 @@ const AiModels: React.FC = () => {
|
||||||
current,
|
current,
|
||||||
pageSize: size,
|
pageSize: size,
|
||||||
total,
|
total,
|
||||||
onChange: (p, s) => {
|
onChange: (page, pageSize) => {
|
||||||
setCurrent(p);
|
setCurrent(page);
|
||||||
setSize(s);
|
setSize(pageSize);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -349,7 +355,11 @@ const AiModels: React.FC = () => {
|
||||||
|
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item name="modelName" label="显示名称" rules={[{ required: true, message: "请输入显示名称" }]}>
|
<Form.Item
|
||||||
|
name="modelName"
|
||||||
|
label="显示名称"
|
||||||
|
rules={[{ required: true, message: "请输入显示名称" }]}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
modelNameAutoFilledRef.current = false;
|
modelNameAutoFilledRef.current = false;
|
||||||
|
|
@ -358,7 +368,11 @@ const AiModels: React.FC = () => {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item name="provider" label="提供商" rules={[{ required: true, message: "请选择提供商" }]}>
|
<Form.Item
|
||||||
|
name="provider"
|
||||||
|
label="提供商"
|
||||||
|
rules={[{ required: true, message: "请选择提供商" }]}
|
||||||
|
>
|
||||||
<Select allowClear placeholder="请选择">
|
<Select allowClear placeholder="请选择">
|
||||||
{providers.map((item) => (
|
{providers.map((item) => (
|
||||||
<Option key={item.itemValue} value={item.itemValue}>
|
<Option key={item.itemValue} value={item.itemValue}>
|
||||||
|
|
@ -384,22 +398,28 @@ const AiModels: React.FC = () => {
|
||||||
模型参数
|
模型参数
|
||||||
</Divider>
|
</Divider>
|
||||||
|
|
||||||
<Form.Item label="模型名称" required tooltip="单选,值将作为模型 code 传给后端">
|
<Form.Item
|
||||||
<Space.Compact style={{ width: "100%" }}>
|
label="模型名称"
|
||||||
<Form.Item name="modelCode" noStyle rules={[{ required: true, message: "请选择模型名称" }]}>
|
required
|
||||||
<Select
|
tooltip="可从远程列表选择,也可手动输入;值将作为模型 code 传给后端"
|
||||||
allowClear
|
|
||||||
showSearch
|
|
||||||
style={{ width: "calc(100% - 100px)" }}
|
|
||||||
placeholder="选择模型名称"
|
|
||||||
optionFilterProp="children"
|
|
||||||
>
|
>
|
||||||
{remoteModels.map((m) => (
|
<Space.Compact style={{ width: "100%" }}>
|
||||||
<Option key={m} value={m}>
|
<Form.Item
|
||||||
{m}
|
name="modelCode"
|
||||||
</Option>
|
noStyle
|
||||||
))}
|
rules={[{ required: true, message: "请输入或选择模型名称" }]}
|
||||||
</Select>
|
>
|
||||||
|
<AutoComplete
|
||||||
|
allowClear
|
||||||
|
style={{ width: "calc(100% - 100px)" }}
|
||||||
|
placeholder="可选择或自定义输入模型名称"
|
||||||
|
options={remoteModels.map((model) => ({ value: model }))}
|
||||||
|
filterOption={(inputValue, option) =>
|
||||||
|
String(option?.value || "").toLowerCase().includes(inputValue.toLowerCase())
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</AutoComplete>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button icon={<SyncOutlined spin={fetchLoading} />} onClick={handleFetchRemote} style={{ width: 100 }}>
|
<Button icon={<SyncOutlined spin={fetchLoading} />} onClick={handleFetchRemote} style={{ width: 100 }}>
|
||||||
刷新
|
刷新
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,7 @@ const MeetingDetail: React.FC = () => {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}, [meeting]);
|
}, [meeting]);
|
||||||
|
const canRetrySummary = isOwner && transcripts.length > 0 && meeting?.status !== 1 && meeting?.status !== 2;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
|
|
@ -458,7 +459,7 @@ const MeetingDetail: React.FC = () => {
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<Space>
|
<Space>
|
||||||
{isOwner && meeting.status === 3 && (
|
{canRetrySummary && (
|
||||||
<Button icon={<SyncOutlined />} type="primary" ghost onClick={() => setSummaryVisible(true)} disabled={actionLoading}>
|
<Button icon={<SyncOutlined />} type="primary" ghost onClick={() => setSummaryVisible(true)} disabled={actionLoading}>
|
||||||
重新总结
|
重新总结
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
AudioOutlined
|
AudioOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { usePermission } from '../../hooks/usePermission';
|
||||||
import { getMeetingPage, deleteMeeting, MeetingVO, getMeetingProgress, MeetingProgress, createMeeting, uploadAudio, updateMeetingParticipants } from '../../api/business/meeting';
|
import { getMeetingPage, deleteMeeting, MeetingVO, getMeetingProgress, MeetingProgress, createMeeting, uploadAudio, updateMeetingParticipants } from '../../api/business/meeting';
|
||||||
import { getAiModelPage, getAiModelDefault, AiModelVO } from '../../api/business/aimodel';
|
import { getAiModelPage, getAiModelDefault, AiModelVO } from '../../api/business/aimodel';
|
||||||
import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt';
|
import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt';
|
||||||
|
|
@ -340,6 +341,7 @@ const MeetingCardItem: React.FC<{ item: MeetingVO, config: any, fetchData: () =>
|
||||||
const Meetings: React.FC = () => {
|
const Meetings: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { can } = usePermission();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [submitLoading, setSubmitLoading] = useState(false);
|
const [submitLoading, setSubmitLoading] = useState(false);
|
||||||
|
|
@ -456,7 +458,8 @@ const Meetings: React.FC = () => {
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
setFileList([]);
|
setFileList([]);
|
||||||
setCreateDrawerVisible(true);
|
setCreateDrawerVisible(true);
|
||||||
}} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>新会议</Button>
|
}} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>上传文件会议</Button>
|
||||||
|
{can("meeting:create:realtime") && <Button icon={<AudioOutlined />} onClick={() => navigate('/meeting-live-create')} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>实时识别会议</Button>}
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import SysParams from "../pages/SysParams";
|
||||||
import PlatformSettings from "../pages/PlatformSettings";
|
import PlatformSettings from "../pages/PlatformSettings";
|
||||||
import Profile from "../pages/Profile";
|
import Profile from "../pages/Profile";
|
||||||
import SpeakerReg from "../pages/business/SpeakerReg";
|
import SpeakerReg from "../pages/business/SpeakerReg";
|
||||||
|
import RealtimeAsr from "../pages/business/RealtimeAsr";
|
||||||
|
import RealtimeAsrSession from "../pages/business/RealtimeAsrSession";
|
||||||
import HotWords from "../pages/business/HotWords";
|
import HotWords from "../pages/business/HotWords";
|
||||||
import PromptTemplates from "../pages/business/PromptTemplates";
|
import PromptTemplates from "../pages/business/PromptTemplates";
|
||||||
import AiModels from "../pages/business/AiModels";
|
import AiModels from "../pages/business/AiModels";
|
||||||
|
|
@ -25,6 +27,7 @@ import type { MenuRoute } from "../types";
|
||||||
export const menuRoutes: MenuRoute[] = [
|
export const menuRoutes: MenuRoute[] = [
|
||||||
{ path: "/", label: "总览", element: <Dashboard />, perm: "menu:dashboard" },
|
{ path: "/", label: "总览", element: <Dashboard />, perm: "menu:dashboard" },
|
||||||
{ path: "/profile", label: "个人中心", element: <Profile /> },
|
{ path: "/profile", label: "个人中心", element: <Profile /> },
|
||||||
|
{ path: "/realtime-asr", label: "实时识别", element: <RealtimeAsr />, perm: "menu:meeting" },
|
||||||
{ path: "/speaker-reg", label: "声纹注册", element: <SpeakerReg />, perm: "menu:speaker" },
|
{ path: "/speaker-reg", label: "声纹注册", element: <SpeakerReg />, perm: "menu:speaker" },
|
||||||
{ path: "/tenants", label: "租户管理", element: <Tenants />, perm: "menu:tenants" },
|
{ path: "/tenants", label: "租户管理", element: <Tenants />, perm: "menu:tenants" },
|
||||||
{ path: "/orgs", label: "组织管理", element: <Orgs />, perm: "menu:orgs" },
|
{ path: "/orgs", label: "组织管理", element: <Orgs />, perm: "menu:orgs" },
|
||||||
|
|
@ -46,6 +49,8 @@ export const menuRoutes: MenuRoute[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export const extraRoutes = [
|
export const extraRoutes = [
|
||||||
{ path: "/meetings/:id", element: <MeetingDetail />, perm: "menu:meeting" }
|
{ path: "/meetings/:id", element: <MeetingDetail />, perm: "menu:meeting" },
|
||||||
|
{ path: "/meeting-live-create", element: <RealtimeAsr />, perm: "menu:meeting" },
|
||||||
|
{ path: "/meeting-live-session/:id", element: <RealtimeAsrSession />, perm: "menu:meeting" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import react from "@vitejs/plugin-react";
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5174,
|
||||||
proxy: {
|
proxy: {
|
||||||
"/auth": "http://localhost:8080",
|
"/auth": "http://localhost:8081",
|
||||||
"/api": "http://localhost:8080"
|
"/api": "http://localhost:8081"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue