From d8bfdb21fa26900e2908b45b45fce8b3dd57fa2a Mon Sep 17 00:00:00 2001 From: chenhao Date: Thu, 12 Mar 2026 20:39:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E4=BC=9A=E8=AE=AE=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E5=92=8CAPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增加实时会议的创建、追加转录和完成接口 - 更新 `MeetingDTO` 和 `MeetingVO`,添加可选字段 `summaryModelId`, `useSpkId`, `hotWords` - 增加 `RealtimeTranscriptItemDTO` 接口 - 延长HTTP客户端连接超时时间至300秒 - 优化数据库表结构,新增 `biz_prompt_template_user_config` 表 - 更新系统参数和权限配置 --- backend/design/db_schema_pgsql.sql | 109 +++++- .../controller/biz/MeetingController.java | 58 +++ .../java/com/imeeting/dto/biz/MeetingVO.java | 1 + .../imeeting/service/biz/MeetingService.java | 7 +- .../service/biz/impl/AiModelServiceImpl.java | 6 +- .../service/biz/impl/AiTaskServiceImpl.java | 13 +- .../service/biz/impl/MeetingServiceImpl.java | 338 ++++++++++++------ backend/src/main/resources/application.yml | 8 +- frontend/src/api/business/meeting.ts | 34 +- frontend/src/pages/business/AiModels.tsx | 76 ++-- frontend/src/pages/business/MeetingDetail.tsx | 3 +- frontend/src/pages/business/Meetings.tsx | 5 +- frontend/src/routes/routes.tsx | 7 +- frontend/vite.config.ts | 6 +- 14 files changed, 512 insertions(+), 159 deletions(-) diff --git a/backend/design/db_schema_pgsql.sql b/backend/design/db_schema_pgsql.sql index 031b756..d8b07de 100644 --- a/backend/design/db_schema_pgsql.sql +++ b/backend/design/db_schema_pgsql.sql @@ -4,7 +4,7 @@ -- ---------------------------- -- 0. 租户与组织 -- ---------------------------- - +CREATE EXTENSION IF NOT EXISTS vector; -- 租户表 CREATE TABLE sys_tenant ( id BIGSERIAL PRIMARY KEY, @@ -109,7 +109,7 @@ CREATE TABLE sys_permission ( perm_id BIGSERIAL PRIMARY KEY, parent_id BIGINT, name VARCHAR(100) NOT NULL, - code VARCHAR(100) NOT NULL UNIQUE, + code VARCHAR(100) NOT NULL , perm_type VARCHAR(20) NOT NULL, level INTEGER NOT NULL, path VARCHAR(255), @@ -161,13 +161,14 @@ CREATE TABLE sys_dict_item ( status SMALLINT DEFAULT 1, created_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 uk_dict_item_value ON sys_dict_item (type_code, item_value); CREATE TABLE sys_param ( - id BIGSERIAL PRIMARY KEY, + param_id BIGSERIAL PRIMARY KEY, param_key VARCHAR(100) UNIQUE NOT NULL, param_value TEXT NOT NULL, param_type VARCHAR(20) NOT NULL, @@ -179,6 +180,14 @@ CREATE TABLE sys_param ( 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. 日志 (租户隔离) -- ---------------------------- @@ -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_meeting_transcripts IS '会议转录明细表'; 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_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_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'); + diff --git a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java index 16f8c37..da914ef 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java @@ -6,6 +6,8 @@ import com.imeeting.common.RedisKeys; import com.imeeting.dto.biz.MeetingDTO; import com.imeeting.dto.biz.MeetingTranscriptVO; 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.Meeting; import com.imeeting.security.LoginUser; @@ -144,6 +146,28 @@ public class MeetingController { return ApiResponse.ok(meetingService.createMeeting(dto)); } + @PostMapping("/realtime/start") + @PreAuthorize("isAuthenticated()") + public ApiResponse 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") @PreAuthorize("isAuthenticated()") public ApiResponse>> page( @@ -296,6 +320,40 @@ public class MeetingController { return ApiResponse.ok(meetingService.getTranscripts(id)); } + @PostMapping("/{id}/realtime/transcripts") + @PreAuthorize("isAuthenticated()") + public ApiResponse appendRealtimeTranscripts(@PathVariable Long id, @RequestBody List 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 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") @PreAuthorize("isAuthenticated()") public ApiResponse updateSpeaker(@RequestBody Map params) { diff --git a/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java b/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java index 8e2a981..64335c8 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/MeetingVO.java @@ -20,6 +20,7 @@ public class MeetingVO { private List participantIds; private String tags; private String audioUrl; + private Integer duration; private String summaryContent; private Integer status; diff --git a/backend/src/main/java/com/imeeting/service/biz/MeetingService.java b/backend/src/main/java/com/imeeting/service/biz/MeetingService.java index 135f87e..9041437 100644 --- a/backend/src/main/java/com/imeeting/service/biz/MeetingService.java +++ b/backend/src/main/java/com/imeeting/service/biz/MeetingService.java @@ -3,19 +3,22 @@ package com.imeeting.service.biz; import com.baomidou.mybatisplus.extension.service.IService; import com.imeeting.common.PageResult; 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.entity.biz.Meeting; -import com.imeeting.dto.biz.MeetingTranscriptVO; - import java.util.List; public interface MeetingService extends IService { MeetingVO createMeeting(MeetingDTO dto); + MeetingVO createRealtimeMeeting(MeetingDTO dto); PageResult> pageMeetings(Integer current, Integer size, String title, Long tenantId, Long userId, String userName, String viewType, boolean isAdmin); void deleteMeeting(Long id); MeetingVO getDetail(Long id); List getTranscripts(Long meetingId); + void appendRealtimeTranscripts(Long meetingId, List items); + void completeRealtimeMeeting(Long meetingId, String audioUrl); void updateSpeakerInfo(Long meetingId, String speakerId, String newName, String label); void updateMeetingParticipants(Long meetingId, String participants); void reSummary(Long meetingId, Long summaryModelId, Long promptId); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiModelServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiModelServiceImpl.java index dcb6a44..6205b4b 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiModelServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiModelServiceImpl.java @@ -47,7 +47,7 @@ public class AiModelServiceImpl implements AiModelService { private final LlmModelMapper llmModelMapper; private final HttpClient httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(10)) + .connectTimeout(Duration.ofSeconds(300)) .build(); @Override @@ -232,8 +232,8 @@ public class AiModelServiceImpl implements AiModelService { } private String resolveModelListUrl(String providerKey, String baseUrl, String apiKey) { - if (baseUrl.contains("3050")) { - return "http://10.100.51.199:3050/api/asrconfig"; + if ("Custom".equalsIgnoreCase(providerKey)) { + return baseUrl+"/api/asrconfig"; } if ("gemini".equals(providerKey) || "google".equals(providerKey)) { if (apiKey == null || apiKey.isBlank()) { diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java index f56a287..d951f39 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java @@ -61,7 +61,8 @@ public class AiTaskServiceImpl extends ServiceImpl impleme private String uploadPath; private final HttpClient httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(10)) + .connectTimeout(Duration.ofSeconds(300)) + .version(HttpClient.Version.HTTP_1_1) .build(); @Override @@ -206,7 +207,7 @@ public class AiTaskServiceImpl extends ServiceImpl impleme updateProgress(meeting.getId(), (int)(currentPercent * 0.85), data.path("message").asText(), eta); if (currentPercent > 0 && currentPercent == lastPercent) { - if (++unchangedCount > 45) throw new RuntimeException("识别任务长时间无进度增长,自动强制超时"); + if (++unchangedCount > 300) throw new RuntimeException("识别任务长时间无进度增长,自动强制超时"); } else { unchangedCount = 0; } @@ -316,15 +317,19 @@ public class AiTaskServiceImpl extends ServiceImpl impleme this.updateById(taskRecord); 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() .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()) - .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(req))) + .POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8)) .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + log.info("LLM summary response status={}, body={}", response.statusCode(), response.body()); JsonNode respNode = objectMapper.readTree(response.body()); if (response.statusCode() == 200 && respNode.has("choices")) { diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingServiceImpl.java index b2a0607..3ee8598 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingServiceImpl.java @@ -6,43 +6,43 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.imeeting.common.PageResult; import com.imeeting.dto.biz.MeetingDTO; -import com.imeeting.dto.biz.MeetingVO; import com.imeeting.dto.biz.MeetingTranscriptVO; -import com.imeeting.entity.biz.Meeting; -import com.imeeting.entity.biz.AiTask; -import com.imeeting.entity.biz.PromptTemplate; -import com.imeeting.entity.biz.MeetingTranscript; -import com.imeeting.entity.biz.HotWord; +import com.imeeting.dto.biz.MeetingVO; +import com.imeeting.dto.biz.RealtimeTranscriptItemDTO; 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.MeetingMapper; 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.PromptTemplateService; import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.HotWordService; +import com.imeeting.service.biz.MeetingService; +import com.imeeting.service.biz.PromptTemplateService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; 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.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.Map; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Slf4j @@ -65,103 +65,57 @@ public class MeetingServiceImpl extends ServiceImpl impl @Override @Transactional(rollbackFor = Exception.class) public MeetingVO createMeeting(MeetingDTO dto) { - 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.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); + Meeting meeting = initMeeting(dto, 0); + meeting.setAudioUrl(relocateAudioUrl(meeting.getId(), dto.getAudioUrl())); this.updateById(meeting); - // ASR Task AiTask asrTask = new AiTask(); asrTask.setMeetingId(meeting.getId()); asrTask.setTaskType("ASR"); asrTask.setStatus(0); - + Map asrConfig = new HashMap<>(); asrConfig.put("asrModelId", dto.getAsrModelId()); asrConfig.put("useSpkId", dto.getUseSpkId() != null ? dto.getUseSpkId() : 1); - + List finalHotWords = dto.getHotWords(); if (finalHotWords == null || finalHotWords.isEmpty()) { finalHotWords = hotWordService.list(new LambdaQueryWrapper() - .eq(HotWord::getTenantId, meeting.getTenantId()) - .eq(HotWord::getStatus, 1)) - .stream().map(HotWord::getWord).collect(Collectors.toList()); + .eq(HotWord::getTenantId, meeting.getTenantId()) + .eq(HotWord::getStatus, 1)) + .stream() + .map(HotWord::getWord) + .collect(Collectors.toList()); } asrConfig.put("hotWords", finalHotWords); asrTask.setTaskConfig(asrConfig); aiTaskService.save(asrTask); - // SUMMARY Task - AiTask sumTask = new AiTask(); - sumTask.setMeetingId(meeting.getId()); - sumTask.setTaskType("SUMMARY"); - sumTask.setStatus(0); - - Map 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); - + createSummaryTask(meeting.getId(), dto.getSummaryModelId(), dto.getPromptId()); eventPublisher.publishEvent(new MeetingCreatedEvent(meeting.getId())); 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 public PageResult> pageMeetings(Integer current, Integer size, String title, Long tenantId, Long userId, String userName, String viewType, boolean isAdmin) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(Meeting::getTenantId, tenantId); - if (isAdmin && "all".equals(viewType)) { - // 管理员全局可见 - } else { + if (!isAdmin || !"all".equals(viewType)) { String userIdStr = String.valueOf(userId); if ("created".equals(viewType)) { wrapper.eq(Meeting::getCreatorId, userId); } else if ("involved".equals(viewType)) { wrapper.and(w -> w.apply("',' || participants || ',' LIKE '%,' || {0} || ',%'", userIdStr)) - .ne(Meeting::getCreatorId, userId); + .ne(Meeting::getCreatorId, userId); } else { wrapper.and(w -> w.eq(Meeting::getCreatorId, userId) .or() @@ -172,12 +126,12 @@ public class MeetingServiceImpl extends ServiceImpl impl if (title != null && !title.isEmpty()) { wrapper.like(Meeting::getTitle, title); } - + wrapper.orderByDesc(Meeting::getCreatedAt); Page page = this.page(new Page<>(current, size), wrapper); List vos = page.getRecords().stream().map(m -> toVO(m, false)).collect(Collectors.toList()); - + PageResult> result = new PageResult<>(); result.setTotal(page.getTotal()); result.setRecords(vos); @@ -199,9 +153,10 @@ public class MeetingServiceImpl extends ServiceImpl impl @Override public List getTranscripts(Long meetingId) { return transcriptMapper.selectList(new LambdaQueryWrapper() - .eq(MeetingTranscript::getMeetingId, meetingId) - .orderByAsc(MeetingTranscript::getStartTime)) - .stream().map(t -> { + .eq(MeetingTranscript::getMeetingId, meetingId) + .orderByAsc(MeetingTranscript::getStartTime)) + .stream() + .map(t -> { MeetingTranscriptVO vo = new MeetingTranscriptVO(); vo.setId(t.getId()); vo.setSpeakerId(t.getSpeakerId()); @@ -214,6 +169,75 @@ public class MeetingServiceImpl extends ServiceImpl impl }).collect(Collectors.toList()); } + @Override + @Transactional(rollbackFor = Exception.class) + public void appendRealtimeTranscripts(Long meetingId, List items) { + if (items == null || items.isEmpty()) { + return; + } + + Integer maxSortOrder = transcriptMapper.selectList(new LambdaQueryWrapper() + .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() + .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() + .eq(MeetingTranscript::getMeetingId, meetingId)); + if (transcriptCount <= 0) { + meeting.setStatus(4); + this.updateById(meeting); + throw new RuntimeException("未接收到可用的实时转录内容"); + } + + aiTaskService.dispatchSummaryTask(meetingId); + } + @Override @Transactional(rollbackFor = Exception.class) public void updateSpeakerInfo(Long meetingId, String speakerId, String newName, String label) { @@ -236,32 +260,19 @@ public class MeetingServiceImpl extends ServiceImpl impl @Transactional(rollbackFor = Exception.class) public void reSummary(Long meetingId, Long summaryModelId, Long promptId) { Meeting meeting = this.getById(meetingId); - if (meeting == null) throw new RuntimeException("Meeting not found"); - - AiTask sumTask = new AiTask(); - sumTask.setMeetingId(meetingId); - sumTask.setTaskType("SUMMARY"); - sumTask.setStatus(0); - - Map sumConfig = new HashMap<>(); - sumConfig.put("summaryModelId", summaryModelId); - if (promptId != null) { - PromptTemplate template = promptTemplateService.getById(promptId); - if (template != null) { - sumConfig.put("promptContent", template.getPromptContent()); - } + if (meeting == null) { + throw new RuntimeException("Meeting not found"); } - sumTask.setTaskConfig(sumConfig); - aiTaskService.save(sumTask); + createSummaryTask(meetingId, summaryModelId, promptId); meeting.setStatus(2); this.updateById(meeting); aiTaskService.dispatchSummaryTask(meetingId); } @Override - public java.util.Map getDashboardStats(Long tenantId, Long userId, boolean isAdmin) { - java.util.Map stats = new java.util.HashMap<>(); + public Map getDashboardStats(Long tenantId, Long userId, boolean isAdmin) { + Map stats = new HashMap<>(); LambdaQueryWrapper baseWrapper = new LambdaQueryWrapper().eq(Meeting::getTenantId, tenantId); if (!isAdmin) { String userIdStr = String.valueOf(userId); @@ -274,10 +285,10 @@ public class MeetingServiceImpl extends ServiceImpl impl stats.put("processingTasks", this.count(baseWrapper.clone().in(Meeting::getStatus, 1, 2))); LocalDateTime todayStart = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0); stats.put("todayNew", this.count(baseWrapper.clone().ge(Meeting::getCreatedAt, todayStart))); - + long totalFinished = this.count(baseWrapper.clone().in(Meeting::getStatus, 3, 4)); long success = this.count(baseWrapper.clone().eq(Meeting::getStatus, 3)); - stats.put("successRate", totalFinished == 0 ? 100 : (int)((double)success / totalFinished * 100)); + stats.put("successRate", totalFinished == 0 ? 100 : (int) ((double) success / totalFinished * 100)); return stats; } @@ -294,6 +305,92 @@ public class MeetingServiceImpl extends ServiceImpl impl 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 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) { MeetingVO vo = new MeetingVO(); vo.setId(meeting.getId()); @@ -304,17 +401,23 @@ public class MeetingServiceImpl extends ServiceImpl impl vo.setMeetingTime(meeting.getMeetingTime()); vo.setTags(meeting.getTags()); vo.setAudioUrl(meeting.getAudioUrl()); + vo.setDuration(resolveMeetingDuration(meeting.getId())); vo.setStatus(meeting.getStatus()); vo.setCreatedAt(meeting.getCreatedAt()); - + if (meeting.getParticipants() != null && !meeting.getParticipants().isEmpty()) { try { List 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); if (!userIds.isEmpty()) { List 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); } } catch (Exception e) { @@ -363,6 +466,27 @@ public class MeetingServiceImpl extends ServiceImpl impl } } + private Integer resolveMeetingDuration(Long meetingId) { + MeetingTranscript latestTranscript = transcriptMapper.selectOne(new LambdaQueryWrapper() + .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() + .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) { if (markdown == null || markdown.isBlank()) { return markdown; diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 3c8f665..f623705 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,14 +1,14 @@ server: - port: 8080 + port: 8081 spring: datasource: - url: jdbc:postgresql://10.100.51.199:5432/imeeting_db + url: jdbc:postgresql://192.168.1.55:5432/imeeting_db username: postgres password: postgres data: redis: - host: 10.100.51.199 + host: 192.168.1.55 port: 6379 password: unis@123 database: 15 @@ -40,7 +40,7 @@ security: secret: change-me-please-change-me-32bytes 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/ resource-prefix: /api/static/ captcha: diff --git a/frontend/src/api/business/meeting.ts b/frontend/src/api/business/meeting.ts index 08b857e..cf69633 100644 --- a/frontend/src/api/business/meeting.ts +++ b/frontend/src/api/business/meeting.ts @@ -23,9 +23,12 @@ export interface MeetingDTO { meetingTime: string; participants: string; tags: string; - audioUrl: string; + audioUrl?: string; asrModelId: number; + summaryModelId?: number; promptId: number; + useSpkId?: number; + hotWords?: string[]; } 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( + "/api/biz/meeting/realtime/start", + data + ); +}; + +export const appendRealtimeTranscripts = (meetingId: number, data: RealtimeTranscriptItemDTO[]) => { + return http.post( + `/api/biz/meeting/${meetingId}/realtime/transcripts`, + data + ); +}; + +export const completeRealtimeMeeting = (meetingId: number, data?: { audioUrl?: string }) => { + return http.post( + `/api/biz/meeting/${meetingId}/realtime/complete`, + data || {} + ); +}; + export const deleteMeeting = (id: number) => { return http.delete( `/api/biz/meeting/${id}` diff --git a/frontend/src/pages/business/AiModels.tsx b/frontend/src/pages/business/AiModels.tsx index 2be0265..1b124a3 100644 --- a/frontend/src/pages/business/AiModels.tsx +++ b/frontend/src/pages/business/AiModels.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { + AutoComplete, Button, Card, Col, @@ -82,19 +83,21 @@ const AiModels: React.FC = () => { if (!profileStr) { return false; } + const profile = JSON.parse(profileStr); return profile.isPlatformAdmin === true; }, []); useEffect(() => { - fetchData(); + void fetchData(); }, [current, size, searchName, activeType]); useEffect(() => { if (!drawerVisible || !provider) { return; } - const providerItem = providers.find((i) => i.itemValue === provider); + + const providerItem = providers.find((item) => item.itemValue === provider); const providerLabel = providerItem?.itemLabel || provider; const currentDisplayName = form.getFieldValue("modelName"); if (!editingId && (!currentDisplayName || modelNameAutoFilledRef.current)) { @@ -130,6 +133,7 @@ const AiModels: React.FC = () => { const openDrawer = (record?: AiModelVO) => { setRemoteModels([]); modelNameAutoFilledRef.current = false; + if (record) { setEditingId(record.id); form.setFieldsValue({ @@ -153,18 +157,20 @@ const AiModels: React.FC = () => { apiPath: "/v1/chat/completions", }); } + setDrawerVisible(true); }; const handleFetchRemote = async () => { - const vals = form.getFieldsValue(["provider", "baseUrl", "apiKey"]); - if (!vals.provider || !vals.baseUrl) { + const values = form.getFieldsValue(["provider", "baseUrl", "apiKey"]); + if (!values.provider || !values.baseUrl) { message.warning("请先填写提供商和 Base URL"); return; } + setFetchLoading(true); try { - const res = await getRemoteModelList(vals); + const res = await getRemoteModelList(values); const rawModels = (res as any)?.data?.data ?? (Array.isArray(res) ? res : []); const models = Array.isArray(rawModels) ? rawModels : []; setRemoteModels(models); @@ -203,7 +209,7 @@ const AiModels: React.FC = () => { message.success("新增成功"); } setDrawerVisible(false); - fetchData(); + void fetchData(); } finally { setSubmitLoading(false); } @@ -212,7 +218,7 @@ const AiModels: React.FC = () => { const handleDelete = async (record: AiModelVO) => { await deleteAiModelByType(record.id, record.modelType); message.success("删除成功"); - fetchData(); + void fetchData(); }; const columns = [ @@ -236,9 +242,9 @@ const AiModels: React.FC = () => { title: "提供商", dataIndex: "provider", key: "provider", - render: (val: string) => { - const item = providers.find((i) => i.itemValue === val); - return item ? {item.itemLabel} : val; + render: (value: string) => { + const item = providers.find((providerItem) => providerItem.itemValue === value); + return item ? {item.itemLabel} : value; }, }, { title: "模型名称(code)", dataIndex: "modelCode", key: "modelCode" }, @@ -284,7 +290,7 @@ const AiModels: React.FC = () => { placeholder="搜索模型名称" prefix={} allowClear - onPressEnter={(e) => setSearchName((e.target as HTMLInputElement).value)} + onPressEnter={(event) => setSearchName((event.target as HTMLInputElement).value)} style={{ width: 220 }} /> diff --git a/frontend/src/pages/business/Meetings.tsx b/frontend/src/pages/business/Meetings.tsx index 525335e..23272e9 100644 --- a/frontend/src/pages/business/Meetings.tsx +++ b/frontend/src/pages/business/Meetings.tsx @@ -9,6 +9,7 @@ import { AudioOutlined } from '@ant-design/icons'; 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 { getAiModelPage, getAiModelDefault, AiModelVO } from '../../api/business/aimodel'; 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 { t } = useTranslation(); const navigate = useNavigate(); + const { can } = usePermission(); const [form] = Form.useForm(); const [loading, setLoading] = useState(false); const [submitLoading, setSubmitLoading] = useState(false); @@ -456,7 +458,8 @@ const Meetings: React.FC = () => { setUploadProgress(0); setFileList([]); setCreateDrawerVisible(true); - }} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>新会议 + }} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>上传文件会议 + {can("meeting:create:realtime") && } diff --git a/frontend/src/routes/routes.tsx b/frontend/src/routes/routes.tsx index f91443f..f7d88c5 100644 --- a/frontend/src/routes/routes.tsx +++ b/frontend/src/routes/routes.tsx @@ -13,6 +13,8 @@ import SysParams from "../pages/SysParams"; import PlatformSettings from "../pages/PlatformSettings"; import Profile from "../pages/Profile"; 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 PromptTemplates from "../pages/business/PromptTemplates"; import AiModels from "../pages/business/AiModels"; @@ -25,6 +27,7 @@ import type { MenuRoute } from "../types"; export const menuRoutes: MenuRoute[] = [ { path: "/", label: "总览", element: , perm: "menu:dashboard" }, { path: "/profile", label: "个人中心", element: }, + { path: "/realtime-asr", label: "实时识别", element: , perm: "menu:meeting" }, { path: "/speaker-reg", label: "声纹注册", element: , perm: "menu:speaker" }, { path: "/tenants", label: "租户管理", element: , perm: "menu:tenants" }, { path: "/orgs", label: "组织管理", element: , perm: "menu:orgs" }, @@ -46,6 +49,8 @@ export const menuRoutes: MenuRoute[] = [ ]; export const extraRoutes = [ - { path: "/meetings/:id", element: , perm: "menu:meeting" } + { path: "/meetings/:id", element: , perm: "menu:meeting" }, + { path: "/meeting-live-create", element: , perm: "menu:meeting" }, + { path: "/meeting-live-session/:id", element: , perm: "menu:meeting" } ]; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index a8762a0..f5e2689 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,10 +4,10 @@ import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], server: { - port: 5173, + port: 5174, proxy: { - "/auth": "http://localhost:8080", - "/api": "http://localhost:8080" + "/auth": "http://localhost:8081", + "/api": "http://localhost:8081" } } });