feat: support chat share and playback

This commit is contained in:
alan.cl
2026-02-26 17:33:45 +08:00
parent f5ead2684d
commit 9fc6c92298
11 changed files with 1719 additions and 27 deletions

View File

@@ -0,0 +1,15 @@
-- From 0.7.x to 0.8.0, we have the following changes:
USE dbgpt;
-- share_links, Store conversation share link tokens
CREATE TABLE `share_links` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'Primary key',
`token` varchar(64) NOT NULL COMMENT 'Unique random share token',
`conv_uid` varchar(255) NOT NULL COMMENT 'The conversation uid being shared',
`created_by` varchar(255) DEFAULT NULL COMMENT 'User who created the share link',
`gmt_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation time',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_share_token` (`token`),
KEY `ix_share_links_token` (`token`),
KEY `ix_share_links_conv_uid` (`conv_uid`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Conversation share link table';

View File

@@ -0,0 +1,637 @@
-- You can change `dbgpt` to your actual metadata database name in your `.env` file
-- eg. `LOCAL_DB_NAME=dbgpt`
CREATE
DATABASE IF NOT EXISTS dbgpt;
use dbgpt;
-- For alembic migration tool
CREATE TABLE IF NOT EXISTS `alembic_version`
(
version_num VARCHAR(32) NOT NULL,
CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)
) DEFAULT CHARSET=utf8mb4 ;
CREATE TABLE IF NOT EXISTS `knowledge_space`
(
`id` int NOT NULL AUTO_INCREMENT COMMENT 'auto increment id',
`name` varchar(100) NOT NULL COMMENT 'knowledge space name',
`vector_type` varchar(50) NOT NULL COMMENT 'vector type',
`domain_type` varchar(50) NOT NULL COMMENT 'domain type',
`desc` varchar(500) NOT NULL COMMENT 'description',
`owner` varchar(100) DEFAULT NULL COMMENT 'owner',
`context` TEXT DEFAULT NULL COMMENT 'context argument',
`gmt_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'created time',
`gmt_modified` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'update time',
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) COMMENT 'index:idx_name'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='knowledge space table';
CREATE TABLE IF NOT EXISTS `knowledge_document`
(
`id` int NOT NULL AUTO_INCREMENT COMMENT 'auto increment id',
`doc_name` varchar(100) NOT NULL COMMENT 'document path name',
`doc_type` varchar(50) NOT NULL COMMENT 'doc type',
`doc_token` varchar(100) NULL COMMENT 'doc token',
`space` varchar(50) NOT NULL COMMENT 'knowledge space',
`chunk_size` int NOT NULL COMMENT 'chunk size',
`last_sync` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'last sync time',
`status` varchar(50) NOT NULL COMMENT 'status TODO,RUNNING,FAILED,FINISHED',
`content` LONGTEXT NOT NULL COMMENT 'knowledge embedding sync result',
`result` TEXT NULL COMMENT 'knowledge content',
`questions` TEXT NULL COMMENT 'document related questions',
`vector_ids` LONGTEXT NULL COMMENT 'vector_ids',
`summary` LONGTEXT NULL COMMENT 'knowledge summary',
`gmt_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'created time',
`gmt_modified` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'update time',
PRIMARY KEY (`id`),
KEY `idx_doc_name` (`doc_name`) COMMENT 'index:idx_doc_name'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='knowledge document table';
CREATE TABLE IF NOT EXISTS `document_chunk`
(
`id` int NOT NULL AUTO_INCREMENT COMMENT 'auto increment id',
`doc_name` varchar(100) NOT NULL COMMENT 'document path name',
`doc_type` varchar(50) NOT NULL COMMENT 'doc type',
`document_id` int NOT NULL COMMENT 'document parent id',
`content` longtext NOT NULL COMMENT 'chunk content',
`questions` text NULL COMMENT 'chunk related questions',
`meta_info` text NOT NULL COMMENT 'metadata info',
`gmt_created` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'created time',
`gmt_modified` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'update time',
PRIMARY KEY (`id`),
KEY `idx_document_id` (`document_id`) COMMENT 'index:document_id'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='knowledge document chunk detail';
CREATE TABLE IF NOT EXISTS `connect_config`
(
`id` int NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`db_type` varchar(255) NOT NULL COMMENT 'db type',
`db_name` varchar(255) NOT NULL COMMENT 'db name',
`db_path` varchar(255) DEFAULT NULL COMMENT 'file db path',
`db_host` varchar(255) DEFAULT NULL COMMENT 'db connect host(not file db)',
`db_port` varchar(255) DEFAULT NULL COMMENT 'db cnnect port(not file db)',
`db_user` varchar(255) DEFAULT NULL COMMENT 'db user',
`db_pwd` varchar(255) DEFAULT NULL COMMENT 'db password',
`comment` text COMMENT 'db comment',
`sys_code` varchar(128) DEFAULT NULL COMMENT 'System code',
`user_name` varchar(255) DEFAULT NULL COMMENT 'user name',
`user_id` varchar(255) DEFAULT NULL COMMENT 'user id',
`gmt_created` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'Record creation time',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Record update time',
`ext_config` text COMMENT 'Extended configuration, json format',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_db` (`db_name`),
KEY `idx_q_db_type` (`db_type`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT 'Connection confi';
CREATE TABLE IF NOT EXISTS `chat_history`
(
`id` int NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`conv_uid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Conversation record unique id',
`chat_mode` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Conversation scene mode',
`summary` longtext COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Conversation record summary',
`user_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'interlocutor',
`messages` text COLLATE utf8mb4_unicode_ci COMMENT 'Conversation details',
`message_ids` text COLLATE utf8mb4_unicode_ci COMMENT 'Message id list, split by comma',
`app_code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'App unique code',
`sys_code` varchar(128) DEFAULT NULL COMMENT 'System code',
`gmt_created` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'created time',
`gmt_modified` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'update time',
UNIQUE KEY `conv_uid` (`conv_uid`),
PRIMARY KEY (`id`),
KEY `idx_chat_his_app_code` (`app_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT 'Chat history';
CREATE TABLE IF NOT EXISTS `chat_history_message`
(
`id` int NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`conv_uid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Conversation record unique id',
`index` int NOT NULL COMMENT 'Message index',
`round_index` int NOT NULL COMMENT 'Round of conversation',
`message_detail` longtext COLLATE utf8mb4_unicode_ci COMMENT 'Message details, json format',
`gmt_created` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'created time',
`gmt_modified` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'update time',
UNIQUE KEY `message_uid_index` (`conv_uid`, `index`),
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT 'Chat history message';
CREATE TABLE IF NOT EXISTS `chat_feed_back`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`conv_uid` varchar(128) DEFAULT NULL COMMENT 'Conversation ID',
`conv_index` int(4) DEFAULT NULL COMMENT 'Round of conversation',
`score` int(1) DEFAULT NULL COMMENT 'Score of user',
`ques_type` varchar(32) DEFAULT NULL COMMENT 'User question category',
`question` longtext DEFAULT NULL COMMENT 'User question',
`knowledge_space` varchar(128) DEFAULT NULL COMMENT 'Knowledge space name',
`messages` longtext DEFAULT NULL COMMENT 'The details of user feedback',
`message_id` varchar(255) NULL COMMENT 'Message id',
`feedback_type` varchar(50) NULL COMMENT 'Feedback type like or unlike',
`reason_types` varchar(255) NULL COMMENT 'Feedback reason categories',
`remark` text NULL COMMENT 'Feedback remark',
`user_code` varchar(128) NULL COMMENT 'User code',
`user_name` varchar(128) DEFAULT NULL COMMENT 'User name',
`gmt_created` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'created time',
`gmt_modified` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'update time',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_conv` (`conv_uid`,`conv_index`),
KEY `idx_conv` (`conv_uid`,`conv_index`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='User feedback table';
CREATE TABLE IF NOT EXISTS `my_plugin`
(
`id` int NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`tenant` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'user tenant',
`user_code` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'user code',
`user_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'user name',
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'plugin name',
`file_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'plugin package file name',
`type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'plugin type',
`version` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'plugin version',
`use_count` int DEFAULT NULL COMMENT 'plugin total use count',
`succ_count` int DEFAULT NULL COMMENT 'plugin total success count',
`sys_code` varchar(128) DEFAULT NULL COMMENT 'System code',
`gmt_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'plugin install time',
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='User plugin table';
CREATE TABLE IF NOT EXISTS `plugin_hub`
(
`id` int NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'plugin name',
`description` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'plugin description',
`author` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'plugin author',
`email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'plugin author email',
`type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'plugin type',
`version` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'plugin version',
`storage_channel` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'plugin storage channel',
`storage_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'plugin download url',
`download_param` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'plugin download param',
`gmt_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'plugin upload time',
`installed` int DEFAULT NULL COMMENT 'plugin already installed count',
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Plugin Hub table';
CREATE TABLE IF NOT EXISTS `prompt_manage`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`chat_scene` varchar(100) DEFAULT NULL COMMENT 'Chat scene',
`sub_chat_scene` varchar(100) DEFAULT NULL COMMENT 'Sub chat scene',
`prompt_type` varchar(100) DEFAULT NULL COMMENT 'Prompt type: common or private',
`prompt_name` varchar(256) DEFAULT NULL COMMENT 'prompt name',
`prompt_code` varchar(256) DEFAULT NULL COMMENT 'prompt code',
`content` longtext COMMENT 'Prompt content',
`input_variables` varchar(1024) DEFAULT NULL COMMENT 'Prompt input variables(split by comma))',
`response_schema` text DEFAULT NULL COMMENT 'Prompt response schema',
`model` varchar(128) DEFAULT NULL COMMENT 'Prompt model name(we can use different models for different prompt)',
`prompt_language` varchar(32) DEFAULT NULL COMMENT 'Prompt language(eg:en, zh-cn)',
`prompt_format` varchar(32) DEFAULT 'f-string' COMMENT 'Prompt format(eg: f-string, jinja2)',
`prompt_desc` varchar(512) DEFAULT NULL COMMENT 'Prompt description',
`user_code` varchar(128) DEFAULT NULL COMMENT 'User code',
`user_name` varchar(128) DEFAULT NULL COMMENT 'User name',
`sys_code` varchar(128) DEFAULT NULL COMMENT 'System code',
`gmt_created` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'created time',
`gmt_modified` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'update time',
PRIMARY KEY (`id`),
UNIQUE KEY `prompt_name_uiq` (`prompt_name`, `sys_code`, `prompt_language`, `model`),
KEY `gmt_created_idx` (`gmt_created`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Prompt management table';
CREATE TABLE IF NOT EXISTS `gpts_conversations` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`conv_id` varchar(255) NOT NULL COMMENT 'The unique id of the conversation record',
`user_goal` text NOT NULL COMMENT 'User''s goals content',
`gpts_name` varchar(255) NOT NULL COMMENT 'The gpts name',
`state` varchar(255) DEFAULT NULL COMMENT 'The gpts state',
`max_auto_reply_round` int(11) NOT NULL COMMENT 'max auto reply round',
`auto_reply_count` int(11) NOT NULL COMMENT 'auto reply count',
`user_code` varchar(255) DEFAULT NULL COMMENT 'user code',
`sys_code` varchar(255) DEFAULT NULL COMMENT 'system app ',
`created_at` datetime DEFAULT NULL COMMENT 'create time',
`updated_at` datetime DEFAULT NULL COMMENT 'last update time',
`team_mode` varchar(255) NULL COMMENT 'agent team work mode',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gpts_conversations` (`conv_id`),
KEY `idx_gpts_name` (`gpts_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT="gpt conversations";
CREATE TABLE IF NOT EXISTS `gpts_instance` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`gpts_name` varchar(255) NOT NULL COMMENT 'Current AI assistant name',
`gpts_describe` varchar(2255) NOT NULL COMMENT 'Current AI assistant describe',
`resource_db` text COMMENT 'List of structured database names contained in the current gpts',
`resource_internet` text COMMENT 'Is it possible to retrieve information from the internet',
`resource_knowledge` text COMMENT 'List of unstructured database names contained in the current gpts',
`gpts_agents` varchar(1000) DEFAULT NULL COMMENT 'List of agents names contained in the current gpts',
`gpts_models` varchar(1000) DEFAULT NULL COMMENT 'List of llm model names contained in the current gpts',
`language` varchar(100) DEFAULT NULL COMMENT 'gpts language',
`user_code` varchar(255) NOT NULL COMMENT 'user code',
`sys_code` varchar(255) DEFAULT NULL COMMENT 'system app code',
`created_at` datetime DEFAULT NULL COMMENT 'create time',
`updated_at` datetime DEFAULT NULL COMMENT 'last update time',
`team_mode` varchar(255) NOT NULL COMMENT 'Team work mode',
`is_sustainable` tinyint(1) NOT NULL COMMENT 'Applications for sustainable dialogue',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gpts` (`gpts_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT="gpts instance";
CREATE TABLE `gpts_messages` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`conv_id` varchar(255) NOT NULL COMMENT 'The unique id of the conversation record',
`sender` varchar(255) NOT NULL COMMENT 'Who speaking in the current conversation turn',
`receiver` varchar(255) NOT NULL COMMENT 'Who receive message in the current conversation turn',
`model_name` varchar(255) DEFAULT NULL COMMENT 'message generate model',
`rounds` int(11) NOT NULL COMMENT 'dialogue turns',
`is_success` int(4) NULL DEFAULT 0 COMMENT 'agent message is success',
`app_code` varchar(255) NOT NULL COMMENT 'Current AI assistant code',
`app_name` varchar(255) NOT NULL COMMENT 'Current AI assistant name',
`content` text COMMENT 'Content of the speech',
`current_goal` text COMMENT 'The target corresponding to the current message',
`context` text COMMENT 'Current conversation context',
`review_info` text COMMENT 'Current conversation review info',
`action_report` longtext COMMENT 'Current conversation action report',
`resource_info` text DEFAULT NULL COMMENT 'Current conversation resource info',
`role` varchar(255) DEFAULT NULL COMMENT 'The role of the current message content',
`created_at` datetime DEFAULT NULL COMMENT 'create time',
`updated_at` datetime DEFAULT NULL COMMENT 'last update time',
PRIMARY KEY (`id`),
KEY `idx_q_messages` (`conv_id`,`rounds`,`sender`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT="gpts message";
CREATE TABLE `gpts_plans` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`conv_id` varchar(255) NOT NULL COMMENT 'The unique id of the conversation record',
`sub_task_num` int(11) NOT NULL COMMENT 'Subtask number',
`sub_task_title` varchar(255) NOT NULL COMMENT 'subtask title',
`sub_task_content` text NOT NULL COMMENT 'subtask content',
`sub_task_agent` varchar(255) DEFAULT NULL COMMENT 'Available agents corresponding to subtasks',
`resource_name` varchar(255) DEFAULT NULL COMMENT 'resource name',
`rely` varchar(255) DEFAULT NULL COMMENT 'Subtask dependencieslike: 1,2,3',
`agent_model` varchar(255) DEFAULT NULL COMMENT 'LLM model used by subtask processing agents',
`retry_times` int(11) DEFAULT NULL COMMENT 'number of retries',
`max_retry_times` int(11) DEFAULT NULL COMMENT 'Maximum number of retries',
`state` varchar(255) DEFAULT NULL COMMENT 'subtask status',
`result` longtext COMMENT 'subtask result',
`created_at` datetime DEFAULT NULL COMMENT 'create time',
`updated_at` datetime DEFAULT NULL COMMENT 'last update time',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_sub_task` (`conv_id`,`sub_task_num`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT="gpt plan";
-- dbgpt.dbgpt_serve_flow definition
CREATE TABLE `dbgpt_serve_flow` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'Auto increment id',
`uid` varchar(128) NOT NULL COMMENT 'Unique id',
`dag_id` varchar(128) DEFAULT NULL COMMENT 'DAG id',
`name` varchar(128) DEFAULT NULL COMMENT 'Flow name',
`flow_data` longtext COMMENT 'Flow data, JSON format',
`user_name` varchar(128) DEFAULT NULL COMMENT 'User name',
`sys_code` varchar(128) DEFAULT NULL COMMENT 'System code',
`gmt_created` datetime DEFAULT NULL COMMENT 'Record creation time',
`gmt_modified` datetime DEFAULT NULL COMMENT 'Record update time',
`flow_category` varchar(64) DEFAULT NULL COMMENT 'Flow category',
`description` varchar(512) DEFAULT NULL COMMENT 'Flow description',
`state` varchar(32) DEFAULT NULL COMMENT 'Flow state',
`error_message` varchar(512) NULL comment 'Error message',
`source` varchar(64) DEFAULT NULL COMMENT 'Flow source',
`source_url` varchar(512) DEFAULT NULL COMMENT 'Flow source url',
`version` varchar(32) DEFAULT NULL COMMENT 'Flow version',
`define_type` varchar(32) null comment 'Flow define type(json or python)',
`label` varchar(128) DEFAULT NULL COMMENT 'Flow label',
`editable` int DEFAULT NULL COMMENT 'Editable, 0: editable, 1: not editable',
`variables` text DEFAULT NULL COMMENT 'Flow variables, JSON format',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_uid` (`uid`),
KEY `ix_dbgpt_serve_flow_sys_code` (`sys_code`),
KEY `ix_dbgpt_serve_flow_uid` (`uid`),
KEY `ix_dbgpt_serve_flow_dag_id` (`dag_id`),
KEY `ix_dbgpt_serve_flow_user_name` (`user_name`),
KEY `ix_dbgpt_serve_flow_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- dbgpt.dbgpt_serve_file definition
CREATE TABLE `dbgpt_serve_file` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'Auto increment id',
`bucket` varchar(255) NOT NULL COMMENT 'Bucket name',
`file_id` varchar(255) NOT NULL COMMENT 'File id',
`file_name` varchar(256) NOT NULL COMMENT 'File name',
`file_size` int DEFAULT NULL COMMENT 'File size',
`storage_type` varchar(32) NOT NULL COMMENT 'Storage type',
`storage_path` varchar(512) NOT NULL COMMENT 'Storage path',
`uri` varchar(512) NOT NULL COMMENT 'File URI',
`custom_metadata` text DEFAULT NULL COMMENT 'Custom metadata, JSON format',
`file_hash` varchar(128) DEFAULT NULL COMMENT 'File hash',
`user_name` varchar(128) DEFAULT NULL COMMENT 'User name',
`sys_code` varchar(128) DEFAULT NULL COMMENT 'System code',
`gmt_created` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'Record creation time',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Record update time',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_bucket_file_id` (`bucket`, `file_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- dbgpt.dbgpt_serve_variables definition
CREATE TABLE `dbgpt_serve_variables` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'Auto increment id',
`key` varchar(128) NOT NULL COMMENT 'Variable key',
`name` varchar(128) DEFAULT NULL COMMENT 'Variable name',
`label` varchar(128) DEFAULT NULL COMMENT 'Variable label',
`value` text DEFAULT NULL COMMENT 'Variable value, JSON format',
`value_type` varchar(32) DEFAULT NULL COMMENT 'Variable value type(string, int, float, bool)',
`category` varchar(32) DEFAULT 'common' COMMENT 'Variable category(common or secret)',
`encryption_method` varchar(32) DEFAULT NULL COMMENT 'Variable encryption method(fernet, simple, rsa, aes)',
`salt` varchar(128) DEFAULT NULL COMMENT 'Variable salt',
`scope` varchar(32) DEFAULT 'global' COMMENT 'Variable scope(global,flow,app,agent,datasource,flow_priv,agent_priv, ""etc)',
`scope_key` varchar(256) DEFAULT NULL COMMENT 'Variable scope key, default is empty, for scope is "flow_priv", the scope_key is dag id of flow',
`enabled` int DEFAULT 1 COMMENT 'Variable enabled, 0: disabled, 1: enabled',
`description` text DEFAULT NULL COMMENT 'Variable description',
`user_name` varchar(128) DEFAULT NULL COMMENT 'User name',
`sys_code` varchar(128) DEFAULT NULL COMMENT 'System code',
`gmt_created` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'Record creation time',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Record update time',
PRIMARY KEY (`id`),
KEY `ix_your_table_name_key` (`key`),
KEY `ix_your_table_name_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `dbgpt_serve_model` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'Auto increment id',
`host` varchar(255) NOT NULL COMMENT 'The model worker host',
`port` int NOT NULL COMMENT 'The model worker port',
`model` varchar(255) NOT NULL COMMENT 'The model name',
`provider` varchar(255) NOT NULL COMMENT 'The model provider',
`worker_type` varchar(255) NOT NULL COMMENT 'The worker type',
`params` text NOT NULL COMMENT 'The model parameters, JSON format',
`enabled` int DEFAULT 1 COMMENT 'Whether the model is enabled, if it is enabled, it will be started when the system starts, 1 is enabled, 0 is disabled',
`worker_name` varchar(255) DEFAULT NULL COMMENT 'The worker name',
`description` text DEFAULT NULL COMMENT 'The model description',
`user_name` varchar(128) DEFAULT NULL COMMENT 'User name',
`sys_code` varchar(128) DEFAULT NULL COMMENT 'System code',
`gmt_created` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'Record creation time',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Record update time',
PRIMARY KEY (`id`),
KEY `idx_user_name` (`user_name`),
KEY `idx_sys_code` (`sys_code`),
UNIQUE KEY `uk_model_provider_type` (`model`, `provider`, `worker_type`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Model persistence table';
-- dbgpt.gpts_app definition
CREATE TABLE `gpts_app` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`app_code` varchar(255) NOT NULL COMMENT 'Current AI assistant code',
`app_name` varchar(255) NOT NULL COMMENT 'Current AI assistant name',
`app_describe` varchar(2255) NOT NULL COMMENT 'Current AI assistant describe',
`language` varchar(100) NOT NULL COMMENT 'gpts language',
`team_mode` varchar(255) NOT NULL COMMENT 'Team work mode',
`team_context` text COMMENT 'The execution logic and team member content that teams with different working modes rely on',
`user_code` varchar(255) DEFAULT NULL COMMENT 'user code',
`sys_code` varchar(255) DEFAULT NULL COMMENT 'system app code',
`created_at` datetime DEFAULT NULL COMMENT 'create time',
`updated_at` datetime DEFAULT NULL COMMENT 'last update time',
`icon` varchar(1024) DEFAULT NULL COMMENT 'app icon, url',
`published` varchar(64) DEFAULT 'false' COMMENT 'Has it been published?',
`param_need` text DEFAULT NULL COMMENT 'Parameter information supported by the application',
`admins` text DEFAULT NULL COMMENT 'administrator',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gpts_app` (`app_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `gpts_app_collection` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`app_code` varchar(255) NOT NULL COMMENT 'Current AI assistant code',
`user_code` int(11) NOT NULL COMMENT 'user code',
`sys_code` varchar(255) NULL COMMENT 'system app code',
`created_at` datetime DEFAULT NULL COMMENT 'create time',
`updated_at` datetime DEFAULT NULL COMMENT 'last update time',
PRIMARY KEY (`id`),
KEY `idx_app_code` (`app_code`),
KEY `idx_user_code` (`user_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT="gpt collections";
-- dbgpt.gpts_app_detail definition
CREATE TABLE `gpts_app_detail` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`app_code` varchar(255) NOT NULL COMMENT 'Current AI assistant code',
`app_name` varchar(255) NOT NULL COMMENT 'Current AI assistant name',
`agent_name` varchar(255) NOT NULL COMMENT ' Agent name',
`node_id` varchar(255) NOT NULL COMMENT 'Current AI assistant Agent Node id',
`resources` text COMMENT 'Agent bind resource',
`prompt_template` text COMMENT 'Agent bind template',
`llm_strategy` varchar(25) DEFAULT NULL COMMENT 'Agent use llm strategy',
`llm_strategy_value` text COMMENT 'Agent use llm strategy value',
`created_at` datetime DEFAULT NULL COMMENT 'create time',
`updated_at` datetime DEFAULT NULL COMMENT 'last update time',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gpts_app_agent_node` (`app_name`,`agent_name`,`node_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- For deploy model cluster of DB-GPT(StorageModelRegistry)
CREATE TABLE IF NOT EXISTS `dbgpt_cluster_registry_instance` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Auto increment id',
`model_name` varchar(128) NOT NULL COMMENT 'Model name',
`host` varchar(128) NOT NULL COMMENT 'Host of the model',
`port` int(11) NOT NULL COMMENT 'Port of the model',
`weight` float DEFAULT 1.0 COMMENT 'Weight of the model',
`check_healthy` tinyint(1) DEFAULT 1 COMMENT 'Whether to check the health of the model',
`healthy` tinyint(1) DEFAULT 0 COMMENT 'Whether the model is healthy',
`enabled` tinyint(1) DEFAULT 1 COMMENT 'Whether the model is enabled',
`prompt_template` varchar(128) DEFAULT NULL COMMENT 'Prompt template for the model instance',
`last_heartbeat` datetime DEFAULT NULL COMMENT 'Last heartbeat time of the model instance',
`user_name` varchar(128) DEFAULT NULL COMMENT 'User name',
`sys_code` varchar(128) DEFAULT NULL COMMENT 'System code',
`gmt_created` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'Record creation time',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Record update time',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_model_instance` (`model_name`, `host`, `port`, `sys_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Cluster model instance table, for registering and managing model instances';
-- dbgpt.recommend_question definition
CREATE TABLE `recommend_question` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time',
`gmt_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'last update time',
`app_code` varchar(255) NOT NULL COMMENT 'Current AI assistant code',
`question` text DEFAULT NULL COMMENT 'question',
`user_code` varchar(255) NOT NULL COMMENT 'user code',
`sys_code` varchar(255) NULL COMMENT 'system app code',
`valid` varchar(10) DEFAULT 'true' COMMENT 'is it effectivetrue/false',
`chat_mode` varchar(255) DEFAULT NULL COMMENT 'Conversation scene modechat_knowledge...',
`params` text DEFAULT NULL COMMENT 'question param',
`is_hot_question` varchar(10) DEFAULT 'false' COMMENT 'Is it a popular recommendation question?',
PRIMARY KEY (`id`),
KEY `idx_rec_q_app_code` (`app_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT="AI application related recommendation issues";
-- dbgpt.user_recent_apps definition
CREATE TABLE `user_recent_apps` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time',
`gmt_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'last update time',
`app_code` varchar(255) NOT NULL COMMENT 'AI assistant code',
`last_accessed` timestamp NULL DEFAULT NULL COMMENT 'User recent usage time',
`user_code` varchar(255) DEFAULT NULL COMMENT 'user code',
`sys_code` varchar(255) DEFAULT NULL COMMENT 'system app code',
PRIMARY KEY (`id`),
KEY `idx_user_r_app_code` (`app_code`),
KEY `idx_last_accessed` (`last_accessed`),
KEY `idx_user_code` (`user_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='User recently used apps';
-- dbgpt.dbgpt_serve_dbgpts_my definition
CREATE TABLE `dbgpt_serve_dbgpts_my` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`name` varchar(255) NOT NULL COMMENT 'plugin name',
`user_code` varchar(255) DEFAULT NULL COMMENT 'user code',
`user_name` varchar(255) DEFAULT NULL COMMENT 'user name',
`file_name` varchar(255) NOT NULL COMMENT 'plugin package file name',
`type` varchar(255) DEFAULT NULL COMMENT 'plugin type',
`version` varchar(255) DEFAULT NULL COMMENT 'plugin version',
`use_count` int DEFAULT NULL COMMENT 'plugin total use count',
`succ_count` int DEFAULT NULL COMMENT 'plugin total success count',
`sys_code` varchar(128) DEFAULT NULL COMMENT 'System code',
`gmt_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'plugin install time',
`gmt_modified` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'update time',
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`, `user_name`),
KEY `ix_my_plugin_sys_code` (`sys_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- dbgpt.dbgpt_serve_dbgpts_hub definition
CREATE TABLE `dbgpt_serve_dbgpts_hub` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`name` varchar(255) NOT NULL COMMENT 'plugin name',
`description` varchar(255) NULL COMMENT 'plugin description',
`author` varchar(255) DEFAULT NULL COMMENT 'plugin author',
`email` varchar(255) DEFAULT NULL COMMENT 'plugin author email',
`type` varchar(255) DEFAULT NULL COMMENT 'plugin type',
`version` varchar(255) DEFAULT NULL COMMENT 'plugin version',
`storage_channel` varchar(255) DEFAULT NULL COMMENT 'plugin storage channel',
`storage_url` varchar(255) DEFAULT NULL COMMENT 'plugin download url',
`download_param` varchar(255) DEFAULT NULL COMMENT 'plugin download param',
`gmt_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'plugin upload time',
`gmt_modified` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'update time',
`installed` int DEFAULT NULL COMMENT 'plugin already installed count',
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- dbgpt.evaluate_manage definition
CREATE TABLE `evaluate_manage` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`evaluate_code` varchar(256) NOT NULL COMMENT 'evaluate unique code',
`scene_key` varchar(100) DEFAULT NULL COMMENT 'scene key',
`scene_value` varchar(256) DEFAULT NULL COMMENT 'scene value',
`context` text DEFAULT NULL COMMENT 'context',
`evaluate_metrics` varchar(599) DEFAULT NULL COMMENT 'evaluate metrics',
`datasets_name` varchar(256) DEFAULT NULL COMMENT 'datasets name',
`datasets` text DEFAULT NULL COMMENT 'datasets content',
`storage_type` varchar(256) DEFAULT NULL COMMENT 'result storage type',
`parallel_num` int DEFAULT NULL COMMENT 'execute parallel thread number',
`state` VARCHAR(100) DEFAULT NULL COMMENT 'execute state',
`result` text DEFAULT NULL COMMENT 'evaluate result',
`log_info` text DEFAULT NULL COMMENT 'evaluate error log',
`average_score` text DEFAULT NULL COMMENT 'metrics average score',
`user_id` varchar(100) DEFAULT NULL COMMENT 'user id',
`user_name` varchar(128) DEFAULT NULL COMMENT 'user name',
`sys_code` varchar(128) DEFAULT NULL COMMENT 'system code',
`gmt_create` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'benchmark create time',
`gmt_modified` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'benchmark finish time',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_evaluate` (`evaluate_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- dbgpt.benchmark_summary definition
CREATE TABLE `benchmark_summary` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'autoincrement id',
`round_id` int NOT NULL COMMENT 'task round id',
`output_path` varchar(512) NULL COMMENT 'output file path',
`right` int DEFAULT NULL COMMENT 'right number',
`wrong` int DEFAULT NULL COMMENT 'wrong number',
`failed` int DEFAULT NULL COMMENT 'failed number',
`exception` int DEFAULT NULL COMMENT 'exception number',
`llm_code` varchar(256) DEFAULT NULL COMMENT 'benchmark llm code',
`evaluate_code` varchar(256) DEFAULT NULL COMMENT 'benchmark evaluate code',
`gmt_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'benchmark create time',
`gmt_modified` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'benchmark finish time',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- dbgpt.share_links definition
CREATE TABLE `share_links` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'Primary key',
`token` varchar(64) NOT NULL COMMENT 'Unique random share token',
`conv_uid` varchar(255) NOT NULL COMMENT 'The conversation uid being shared',
`created_by` varchar(255) DEFAULT NULL COMMENT 'User who created the share link',
`gmt_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation time',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_share_token` (`token`),
KEY `ix_share_links_token` (`token`),
KEY `ix_share_links_conv_uid` (`conv_uid`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Conversation share link table';
CREATE
DATABASE IF NOT EXISTS EXAMPLE_1;
use EXAMPLE_1;
CREATE TABLE IF NOT EXISTS `users`
(
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(50) NOT NULL COMMENT '密码',
`email` varchar(50) NOT NULL COMMENT '邮箱',
`phone` varchar(20) DEFAULT NULL COMMENT '电话',
PRIMARY KEY (`id`),
KEY `idx_username` (`username`) COMMENT '索引:按用户名查询'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='聊天用户表';
INSERT INTO users (username, password, email, phone)
VALUES ('user_1', 'password_1', 'user_1@example.com', '12345678901');
INSERT INTO users (username, password, email, phone)
VALUES ('user_2', 'password_2', 'user_2@example.com', '12345678902');
INSERT INTO users (username, password, email, phone)
VALUES ('user_3', 'password_3', 'user_3@example.com', '12345678903');
INSERT INTO users (username, password, email, phone)
VALUES ('user_4', 'password_4', 'user_4@example.com', '12345678904');
INSERT INTO users (username, password, email, phone)
VALUES ('user_5', 'password_5', 'user_5@example.com', '12345678905');
INSERT INTO users (username, password, email, phone)
VALUES ('user_6', 'password_6', 'user_6@example.com', '12345678906');
INSERT INTO users (username, password, email, phone)
VALUES ('user_7', 'password_7', 'user_7@example.com', '12345678907');
INSERT INTO users (username, password, email, phone)
VALUES ('user_8', 'password_8', 'user_8@example.com', '12345678908');
INSERT INTO users (username, password, email, phone)
VALUES ('user_9', 'password_9', 'user_9@example.com', '12345678909');
INSERT INTO users (username, password, email, phone)
VALUES ('user_10', 'password_10', 'user_10@example.com', '12345678900');
INSERT INTO users (username, password, email, phone)
VALUES ('user_11', 'password_11', 'user_11@example.com', '12345678901');
INSERT INTO users (username, password, email, phone)
VALUES ('user_12', 'password_12', 'user_12@example.com', '12345678902');
INSERT INTO users (username, password, email, phone)
VALUES ('user_13', 'password_13', 'user_13@example.com', '12345678903');
INSERT INTO users (username, password, email, phone)
VALUES ('user_14', 'password_14', 'user_14@example.com', '12345678904');
INSERT INTO users (username, password, email, phone)
VALUES ('user_15', 'password_15', 'user_15@example.com', '12345678905');
INSERT INTO users (username, password, email, phone)
VALUES ('user_16', 'password_16', 'user_16@example.com', '12345678906');
INSERT INTO users (username, password, email, phone)
VALUES ('user_17', 'password_17', 'user_17@example.com', '12345678907');
INSERT INTO users (username, password, email, phone)
VALUES ('user_18', 'password_18', 'user_18@example.com', '12345678908');
INSERT INTO users (username, password, email, phone)
VALUES ('user_19', 'password_19', 'user_19@example.com', '12345678909');
INSERT INTO users (username, password, email, phone)
VALUES ('user_20', 'password_20', 'user_20@example.com', '12345678900');

View File

@@ -6,6 +6,7 @@ from dbgpt.storage.chat_history.chat_history_db import (
ChatHistoryMessageEntity,
)
from dbgpt_app.openapi.api_v1.feedback.feed_back_db import ChatFeedBackEntity
from dbgpt_app.share.models import ShareLinkEntity
from dbgpt_serve.agent.app.recommend_question.recommend_question import (
RecommendQuestionEntity,
)
@@ -38,4 +39,5 @@ _MODELS = [
RecommendQuestionEntity,
FlowVariableEntity,
BenchmarkSummaryEntity,
ShareLinkEntity,
]

View File

@@ -13,6 +13,7 @@ from fastapi import APIRouter, Body, Depends, File, Query, UploadFile
from fastapi.responses import StreamingResponse
from dbgpt._private.config import Config
from dbgpt._private.pydantic import BaseModel as _BaseModel
from dbgpt.agent.resource.tool.base import tool
from dbgpt.agent.skill.manage import get_skill_manager
from dbgpt.component import ComponentType
@@ -26,6 +27,7 @@ from dbgpt_app.openapi.api_view_model import (
from dbgpt_serve.utils.auth import UserRequest, get_user_from_headers
from dbgpt_serve.datasource.manages import ConnectorManager
router = APIRouter()
CFG = Config()
logger = logging.getLogger(__name__)
@@ -621,7 +623,7 @@ async def _react_agent_stream(
database_context = f"""
## 数据库信息
- 数据库名: {database_name}
- 可用表: {', '.join(table_names)}
- 可用表: {", ".join(table_names)}
- 表结构:
{table_info}
- 使用 'sql_query' 工具执行 SQL 查询
@@ -632,9 +634,7 @@ async def _react_agent_stream(
f"(tables: {', '.join(table_names)})"
)
except Exception as e:
logger.warning(
f"Failed to load database connector: {e}", exc_info=e
)
logger.warning(f"Failed to load database connector: {e}", exc_info=e)
database_context = f"""
## 数据库
- 警告: 加载数据库 '{database_name}' 失败。错误: {str(e)}
@@ -1050,7 +1050,7 @@ print(json.dumps(summary, ensure_ascii=False))
@tool(
description=(
"对用户选择的数据库执行 SQL 查询(仅支持 SELECT"
"参数: {\"sql\": \"SELECT 语句\"}"
'参数: {"sql": "SELECT 语句"}'
)
)
def sql_query(sql: str) -> str:
@@ -1071,8 +1071,15 @@ print(json.dumps(summary, ensure_ascii=False))
sql_stripped = sql.strip().rstrip(";")
sql_upper = sql_stripped.upper().lstrip()
forbidden = [
"INSERT", "UPDATE", "DELETE", "DROP",
"ALTER", "TRUNCATE", "CREATE", "GRANT", "REVOKE",
"INSERT",
"UPDATE",
"DELETE",
"DROP",
"ALTER",
"TRUNCATE",
"CREATE",
"GRANT",
"REVOKE",
]
for kw in forbidden:
if sql_upper.startswith(kw):
@@ -1102,9 +1109,7 @@ print(json.dumps(summary, ensure_ascii=False))
# result[0] = column names, result[1:] = data rows
columns = result[0]
col_names = [
str(c[0]) if isinstance(c, tuple) else str(c) for c in columns
]
col_names = [str(c[0]) if isinstance(c, tuple) else str(c) for c in columns]
rows = result[1:]
# Build markdown table
@@ -1112,21 +1117,13 @@ print(json.dumps(summary, ensure_ascii=False))
separator = "| " + " | ".join(["---"] * len(col_names)) + " |"
md_rows = []
for row in rows[:50]:
md_rows.append(
"| "
+ " | ".join(str(v) for v in row)
+ " |"
)
md_rows.append("| " + " | ".join(str(v) for v in row) + " |")
table = "\n".join([header, separator] + md_rows)
if len(rows) > 50:
table += f"\n\n(仅显示前 50 行,共 {len(rows)} 行)"
return json.dumps(
{
"chunks": [
{"output_type": "markdown", "content": table}
]
},
{"chunks": [{"output_type": "markdown", "content": table}]},
ensure_ascii=False,
)
except Exception as e:
@@ -2481,6 +2478,119 @@ Action Input: 工具参数的 JSON 格式
yield _sse_event({"type": "done"})
# ---------------------------------------------------------------------------
# Share link APIs
# ---------------------------------------------------------------------------
class ShareCreateRequest(_BaseModel):
"""Request body for creating a share link."""
conv_uid: str
class ShareCreateResponse(_BaseModel):
"""Response body for share link creation."""
token: str
conv_uid: str
share_url: str
class ShareConvResponse(_BaseModel):
"""Public payload returned when viewing a shared conversation."""
conv_uid: str
token: str
messages: list # list[{role, context, order}]
def _get_share_dao():
"""Lazily instantiate the ShareLinkDao (avoids import-time side-effects)."""
from dbgpt_app.share.models import ShareLinkDao
return ShareLinkDao()
def _get_conversation_service():
"""Return the ConversationServe Service component."""
from dbgpt_serve.conversation.service.service import Service
from dbgpt_serve.conversation.config import SERVE_SERVICE_COMPONENT_NAME
return CFG.SYSTEM_APP.get_component(SERVE_SERVICE_COMPONENT_NAME, Service)
@router.post("/v1/chat/share", response_model=Result)
async def create_share_link(
body: ShareCreateRequest = Body(),
user_token: UserRequest = Depends(get_user_from_headers),
):
"""Create (or return existing) share link for a conversation.
The returned ``share_url`` is a relative path that the client should
prepend with the current host to form an absolute URL.
"""
dao = _get_share_dao()
created_by = user_token.user_id if user_token else None
entity = dao.create_share(conv_uid=body.conv_uid, created_by=created_by)
if entity is None:
return Result.failed(msg="Failed to create share link")
return Result.succ(
ShareCreateResponse(
token=entity.token,
conv_uid=entity.conv_uid,
share_url=f"/share/{entity.token}",
)
)
@router.get("/v1/chat/share/{token}", response_model=Result)
async def get_share_conversation(token: str):
"""Public endpoint — no authentication required.
Returns the full conversation history for the given share token so that the
replay page can reconstruct and animate the session.
"""
dao = _get_share_dao()
link = dao.get_by_token(token)
if link is None:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Share link not found")
service = _get_conversation_service()
from dbgpt_serve.conversation.api.schemas import ServeRequest
history = service.get_history_messages(ServeRequest(conv_uid=link.conv_uid))
messages = [
{"role": m.role, "context": m.context, "order": m.order}
for m in (history or [])
]
return Result.succ(
ShareConvResponse(
conv_uid=link.conv_uid,
token=token,
messages=messages,
)
)
@router.delete("/v1/chat/share/{token}", response_model=Result)
async def delete_share_link(
token: str,
user_token: UserRequest = Depends(get_user_from_headers),
):
"""Revoke a share link. Only the owner (or any authenticated user) may delete."""
dao = _get_share_dao()
deleted = dao.delete_by_token(token)
if not deleted:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Share link not found")
return Result.succ({"deleted": True, "token": token})
@router.post("/v1/chat/react-agent")
async def chat_react_agent(
dialogue: ConversationVo = Body(),

View File

@@ -0,0 +1 @@
"""Share link sub-package."""

View File

@@ -0,0 +1,114 @@
"""Share link database models and DAO."""
import secrets
from datetime import datetime
from typing import Optional
from sqlalchemy import Column, DateTime, Integer, String, UniqueConstraint
from dbgpt.storage.metadata import BaseDao, Model
class ShareLinkEntity(Model):
"""Share link entity — maps a random token to a conversation."""
__tablename__ = "share_links"
__table_args__ = (UniqueConstraint("token", name="uk_share_token"),)
id = Column(Integer, primary_key=True, autoincrement=True, comment="Primary key")
token = Column(
String(64),
unique=False, # enforced by UniqueConstraint above
nullable=False,
index=True,
comment="Unique random share token",
)
conv_uid = Column(
String(255),
nullable=False,
index=True,
comment="The conversation uid being shared",
)
created_by = Column(
String(255),
nullable=True,
comment="User who created the share link",
)
gmt_created = Column(
DateTime,
default=datetime.now,
comment="Creation time",
)
class ShareLinkDao(BaseDao):
"""DAO for share_links table."""
def create_share(
self, conv_uid: str, created_by: Optional[str] = None
) -> Optional[ShareLinkEntity]:
"""Create or return an existing share link for a conversation.
If a share link already exists for this ``conv_uid``, it is returned
as-is so that repeat clicks always yield the same URL.
"""
# Check for an existing link first (read-only session)
existing = self.get_by_conv_uid(conv_uid)
if existing is not None:
return existing
token = secrets.token_urlsafe(32)
entity = ShareLinkEntity(
token=token,
conv_uid=conv_uid,
created_by=created_by,
gmt_created=datetime.now(),
)
with self.session() as session:
session.add(entity)
session.flush()
# Eagerly load all columns before the session closes
session.refresh(entity)
# Detach-safe: copy values into a plain object
result = ShareLinkEntity(
id=entity.id,
token=entity.token,
conv_uid=entity.conv_uid,
created_by=entity.created_by,
gmt_created=entity.gmt_created,
)
return result
def get_by_token(self, token: str) -> Optional[ShareLinkEntity]:
"""Retrieve a share link by token."""
with self.session(commit=False) as session:
row = session.query(ShareLinkEntity).filter_by(token=token).first()
if row is None:
return None
return ShareLinkEntity(
id=row.id,
token=row.token,
conv_uid=row.conv_uid,
created_by=row.created_by,
gmt_created=row.gmt_created,
)
def get_by_conv_uid(self, conv_uid: str) -> Optional[ShareLinkEntity]:
"""Retrieve a share link by conversation uid."""
with self.session(commit=False) as session:
row = session.query(ShareLinkEntity).filter_by(conv_uid=conv_uid).first()
if row is None:
return None
return ShareLinkEntity(
id=row.id,
token=row.token,
conv_uid=row.conv_uid,
created_by=row.created_by,
gmt_created=row.gmt_created,
)
def delete_by_token(self, token: str) -> bool:
"""Delete a share link by token. Returns True if a row was deleted."""
with self.session() as session:
rows = session.query(ShareLinkEntity).filter_by(token=token).delete()
return rows > 0

View File

@@ -80,6 +80,7 @@ export interface ManusLeftPanelProps {
onArtifactClick?: (artifact: ArtifactItem) => void;
onArtifactDownload?: (artifact: ArtifactItem) => void;
onViewAllFiles?: () => void;
onShare?: () => void;
isCollapsed?: boolean;
onExpand?: () => void;
attachedFile?: {
@@ -364,7 +365,7 @@ const StepCard: React.FC<{
onClick: () => void;
}> = memo(({ step, isActive, onClick }) => {
const [isVisible, setIsVisible] = useState(false);
const detailLine = step.detail ? step.detail.split('\n')[0] : '';
const detailLine = step.description ? step.description.split('\n')[0] : '';
React.useEffect(() => {
const timer = setTimeout(() => setIsVisible(true), 50);
@@ -680,6 +681,7 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
onArtifactClick,
onArtifactDownload,
onViewAllFiles,
onShare,
isCollapsed,
onExpand,
attachedFile,
@@ -839,7 +841,7 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
)}
{artifacts && artifacts.length > 0 && (
<div className='mt-5 px-1'>
<div className='mt-5 px-1 pb-8'>
<div className='flex flex-wrap gap-3'>
{artifacts
.filter(a => a.type === 'file' || a.type === 'html')
@@ -866,7 +868,7 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
)}
</div>
<div className='flex items-center gap-1.5 mt-4'>
<div className='flex items-center gap-1.5 mt-5'>
<CheckOutlined className='text-xs text-emerald-500' />
<span className='text-sm text-emerald-600 dark:text-emerald-400 font-medium'></span>
</div>
@@ -877,8 +879,10 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
{modelName && (
<div className='px-4 py-2 border-t border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-900/50'>
<div className='flex items-center justify-between text-[10px] text-gray-400'>
<span>Model: {modelName}</span>
{isWorking && <span className='animate-pulse'>Processing...</span>}
<span>{`Model: ${modelName}`}</span>
<div className='flex items-center gap-2'>
{isWorking && <span className='animate-pulse'>Processing...</span>}
</div>
</div>
</div>
)}

View File

@@ -23,6 +23,7 @@ import {
FileTextOutlined,
FolderOpenOutlined,
LeftOutlined,
LinkOutlined,
LoadingOutlined,
PlayCircleOutlined,
SearchOutlined,
@@ -74,6 +75,7 @@ export interface ManusRightPanelProps {
outputs: ExecutionOutput[];
isRunning?: boolean;
onRerun?: () => void;
onShare?: () => void;
terminalTitle?: string;
onCollapse?: () => void;
isCollapsed?: boolean;
@@ -888,6 +890,7 @@ const ManusRightPanel: React.FC<ManusRightPanelProps> = ({
outputs,
isRunning,
onRerun,
onShare,
terminalTitle = 'DB-GPT 的电脑',
onCollapse,
artifacts,
@@ -1065,6 +1068,20 @@ const ManusRightPanel: React.FC<ManusRightPanelProps> = ({
</Tooltip>
)}
{onShare && (
<Tooltip title='分享此对话'>
<Button
type='text'
size='small'
icon={<LinkOutlined />}
onClick={onShare}
className='text-blue-500 hover:text-blue-600'
>
</Button>
</Tooltip>
)}
</div>
</div>

View File

@@ -85,12 +85,12 @@ function LayoutWrapper({ children }: { children: React.ReactNode }) {
handleAuth();
}, []);
if (!isLogin) {
if (!isLogin && !router.pathname.startsWith('/share')) {
return null;
}
const renderContent = () => {
if (router.pathname.includes('mobile')) {
if (router.pathname.includes('mobile') || router.pathname.startsWith('/share')) {
return <>{children}</>;
}
return (

View File

@@ -31,6 +31,7 @@ import {
FileOutlined,
FilePptOutlined,
FileTextOutlined,
LinkOutlined,
PieChartOutlined,
PlusOutlined,
ReadOutlined,
@@ -1844,6 +1845,25 @@ const Playground: NextPage = () => {
}
};
// Share current conversation — create share link and copy to clipboard
const handleShare = async () => {
if (!conversationId) {
message.warning('请先开始一段对话再分享');
return;
}
try {
const res: any = await axios.post('/api/v1/chat/share', { conv_uid: conversationId });
const shareUrl = res?.data?.share_url;
if (!shareUrl) throw new Error('No share URL returned');
const fullUrl = `${window.location.origin}${shareUrl}`;
await navigator.clipboard.writeText(fullUrl);
message.success('分享链接已复制到剪贴板!');
} catch (e) {
console.error('Failed to create share link', e);
message.error('创建分享链接失败,请稍后重试');
}
};
const _QuickAction = ({ icon, text, onClick }: { icon: any; text: string; onClick?: () => void }) => (
<div
onClick={onClick}
@@ -2270,6 +2290,7 @@ const Playground: NextPage = () => {
isRunning={isRunning}
onCollapse={() => setRightPanelCollapsed(true)}
onRerun={() => {}}
onShare={!loading && !!conversationId ? handleShare : undefined}
terminalTitle='DB-GPT 的电脑'
artifacts={artifacts.filter(a => a.messageId === activeViewMsg?.id)}
onArtifactClick={artifact => {

771
web/pages/share/[token].tsx Normal file
View File

@@ -0,0 +1,771 @@
/**
* /share/[token] — 对话回放页面
*
* 从后端获取共享对话数据,逐步动画展示每一轮的思考→行动→输出过程。
* 无需登录,只读,不可继续对话。
*/
import ManusLeftPanel, {
ArtifactItem,
ThinkingSection,
ExecutionStep as ManusExecutionStep,
StepType,
} from '@/new-components/chat/content/ManusLeftPanel';
import ManusRightPanel, {
ExecutionOutput as ManusExecutionOutput,
ActiveStepInfo,
PanelView,
} from '@/new-components/chat/content/ManusRightPanel';
import {
LinkOutlined,
PauseCircleOutlined,
PlayCircleOutlined,
ReloadOutlined,
StepForwardOutlined,
} from '@ant-design/icons';
import { Button, Tooltip, message } from 'antd';
import { GetServerSideProps, NextPage } from 'next';
import Head from 'next/head';
import { useCallback, useEffect, useRef, useState } from 'react';
// ---------------------------------------------------------------------------
// Types mirroring index.tsx
// ---------------------------------------------------------------------------
interface RawStep {
id: string;
title?: string;
detail?: string;
thought?: string;
action?: string;
action_input?: any;
outputs?: Array<{ output_type: string; content: any }>;
status?: string;
}
interface RawPayload {
version: number;
type: string;
final_content: string;
steps: RawStep[];
generated_images?: string[];
}
interface ParsedMessage {
role: 'human' | 'view';
context: string;
order: number;
payload?: RawPayload; // only for view messages with react-agent payload
}
/** A fully prepared "round" ready for the replay engine */
interface ReplayRound {
humanText: string;
steps: ManusExecutionStep[];
outputs: Record<string, ManusExecutionOutput[]>;
stepThoughts: Record<string, string>;
finalContent: string;
artifacts: ArtifactItem[];
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const getStepType = (title?: string): StepType => {
const lower = (title || '').toLowerCase();
if (lower.includes('load_skill') || lower.includes('load skill')) return 'skill';
if (lower.includes('sql_query') || lower.includes('sql query') || lower.includes('sql')) return 'sql';
if (lower.includes('read') || lower.includes('load')) return 'read';
if (lower.includes('edit')) return 'edit';
if (lower.includes('write') || lower.includes('save')) return 'write';
if (lower.includes('bash') || lower.includes('execute') || lower.includes('command')) return 'bash';
if (lower.includes('grep') || lower.includes('search')) return 'grep';
if (lower.includes('glob') || lower.includes('find')) return 'glob';
if (lower.includes('html')) return 'html';
if (lower.includes('python') || lower.includes('code')) return 'python';
if (lower.includes('skill')) return 'skill';
if (lower.includes('task')) return 'task';
return 'other';
};
/** Extract ArtifactItem[] from a round's steps+outputs (mirrors index.tsx buildArtifactsFromExecution) */
function buildArtifacts(roundId: string, steps: ManusExecutionStep[], outputs: Record<string, ManusExecutionOutput[]>, finalContent: string): ArtifactItem[] {
const artifacts: ArtifactItem[] = [];
const now = Date.now();
const seenCodeHashes = new Set<string>();
steps.forEach(step => {
const stepOutputs = outputs[step.id] || [];
stepOutputs.forEach((output, oIdx) => {
if (output.output_type === 'code') {
const codeStr = String(output.content || '').trim();
const hash = codeStr.slice(0, 200);
if (codeStr && !seenCodeHashes.has(hash)) {
seenCodeHashes.add(hash);
artifacts.push({
id: `${roundId}-code-${step.id}-${oIdx}`,
type: 'code',
name: `code_${oIdx + 1}.py`,
content: codeStr,
createdAt: now,
downloadable: true,
});
}
} else if (output.output_type === 'file') {
artifacts.push({
id: `${roundId}-file-${step.id}-${oIdx}`,
type: 'file',
name: output.content?.name || output.content?.file_name || 'File',
content: output.content,
createdAt: now,
downloadable: true,
size: output.content?.size,
});
} else if (output.output_type === 'html') {
const htmlContent = typeof output.content === 'string'
? output.content
: output.content?.content || output.content?.html || String(output.content);
const htmlTitle = output.content?.title || 'Report';
artifacts.push({
id: `${roundId}-html-${step.id}-${oIdx}`,
type: 'html',
name: `${htmlTitle}.html`,
content: htmlContent,
createdAt: now,
downloadable: true,
});
} else if (output.output_type === 'image') {
const imgUrl = typeof output.content === 'string'
? output.content
: output.content?.url || output.content?.src || String(output.content);
const imgName = imgUrl.split('/').pop() || `image_${oIdx}.png`;
artifacts.push({
id: `${roundId}-img-${step.id}-${oIdx}`,
type: 'image',
name: imgName.replace(/^[a-f0-9]{8}_/, ''),
content: imgUrl,
createdAt: now,
downloadable: true,
});
}
});
});
// Also scan final_content for file references (e.g. "已保存到 xxx.xlsx")
const fileRefRegex = /[\w\u4e00-\u9fa5_\-]+\.(xlsx?|csv|pdf|png|jpg|jpeg|gif|html?|txt|zip|json)/gi;
const matches = finalContent.matchAll(fileRefRegex);
for (const m of matches) {
const name = m[0];
if (!artifacts.some(a => a.name.toLowerCase() === name.toLowerCase())) {
artifacts.push({
id: `${roundId}-fileref-${name}`,
type: 'file',
name,
content: { name },
createdAt: now,
downloadable: true,
});
}
}
return artifacts;
}
/** Build ReplayRound[] from raw messages returned by the share API */
function buildReplayRounds(rawMessages: Array<{ role: string; context: string; order: number }>): ReplayRound[] {
const rounds: ReplayRound[] = [];
let pendingHuman: string | null = null;
for (const msg of rawMessages) {
if (msg.role === 'human') {
pendingHuman = msg.context;
} else if (msg.role === 'view') {
let payload: RawPayload | null = null;
try {
const parsed = JSON.parse(msg.context);
if (parsed?.version === 1 && parsed?.type === 'react-agent') {
payload = parsed as RawPayload;
}
} catch {
/* ignore */
}
if (!payload) continue;
const steps: ManusExecutionStep[] = [];
const outputs: Record<string, ManusExecutionOutput[]> = {};
const stepThoughts: Record<string, string> = {};
(payload.steps || []).forEach((s, idx) => {
const stepId = s.id || `step-${idx}`;
// Filter terminate steps
if ((s.detail || '').toLowerCase().includes('action: terminate')) return;
steps.push({
id: stepId,
type: getStepType(s.title),
title: s.title || `Step ${idx + 1}`,
subtitle: (s.detail || '').split('\n')[0]?.slice(0, 80),
description: s.detail || undefined,
status: s.status === 'failed' ? 'error' : 'completed',
});
const stepOutputs: ManusExecutionOutput[] = [];
// Prepend code from action_input for code_interpreter steps
if (s.action === 'code_interpreter' && s.action_input) {
try {
const inp = typeof s.action_input === 'string' ? JSON.parse(s.action_input) : s.action_input;
if (inp?.code) stepOutputs.push({ output_type: 'code', content: inp.code });
} catch { /* ignore */ }
}
if (Array.isArray(s.outputs)) {
s.outputs.forEach(o => stepOutputs.push({ output_type: o.output_type as any, content: o.content }));
}
outputs[stepId] = stepOutputs;
if (s.thought) stepThoughts[stepId] = s.thought;
});
const artifacts = buildArtifacts(`round-${rounds.length}`, steps, outputs, payload.final_content || '');
rounds.push({
humanText: pendingHuman || '',
steps,
outputs,
stepThoughts,
finalContent: payload.final_content || '',
artifacts,
});
pendingHuman = null;
}
}
return rounds;
}
/** Build ThinkingSection[] from a round's steps (same grouping logic as index.tsx) */
function buildSections(steps: ManusExecutionStep[]): ThinkingSection[] {
if (steps.length === 0) return [];
const thinkSteps = steps.filter(s =>
s.title?.toLowerCase().includes('think') || s.title?.toLowerCase().includes('plan'),
);
const skillSteps = steps.filter(s => s.title?.toLowerCase().includes('skill'));
const otherSteps = steps.filter(
s =>
!s.title?.toLowerCase().includes('think') &&
!s.title?.toLowerCase().includes('plan') &&
!s.title?.toLowerCase().includes('skill'),
);
const sections: ThinkingSection[] = [];
if (thinkSteps.length > 0) sections.push({ id: 'section-think', title: '分析与规划', isCompleted: true, steps: thinkSteps });
if (skillSteps.length > 0) sections.push({ id: 'section-skill', title: '技能加载', isCompleted: true, steps: skillSteps });
if (otherSteps.length > 0) sections.push({ id: 'section-execution', title: '数据处理与执行', isCompleted: true, steps: otherSteps });
if (sections.length === 0) sections.push({ id: 'section-main', title: '执行过程', isCompleted: true, steps });
return sections;
}
// ---------------------------------------------------------------------------
// Replay speed config
// ---------------------------------------------------------------------------
const SPEED_OPTIONS = [
{ label: '0.5×', value: 0.5 },
{ label: '1×', value: 1 },
{ label: '2×', value: 2 },
{ label: '4×', value: 4 },
];
/** Delays (ms) per event at 1× speed */
const BASE_DELAYS = {
beforeStep: 600, // pause before revealing a step
afterStep: 400, // pause after a step appears before showing outputs
betweenOutputs: 300,
beforeFinal: 700,
betweenRounds: 1000,
};
// ---------------------------------------------------------------------------
// useReplayEngine hook
// ---------------------------------------------------------------------------
/** Represents the visible state at any instant during replay */
interface ReplayState {
/** Which round index is currently showing (0-based) */
roundIndex: number;
/** Steps revealed so far in the current round (all previous rounds are fully shown) */
visibleStepCount: number;
/** The step currently "active" (highlighted in the right panel) */
activeStepId: string | null;
/** Whether we are showing the final summary for the current round */
showFinalForRound: boolean;
/** Index of the last fully-finished round */
completedRoundIndex: number;
/** Whether replay has finished */
done: boolean;
}
function useReplayEngine(rounds: ReplayRound[], speed: number, autoPlay = false) {
const [state, setState] = useState<ReplayState>({
roundIndex: 0,
visibleStepCount: 0,
activeStepId: null,
showFinalForRound: false,
completedRoundIndex: -1,
done: false,
});
const [playing, setPlaying] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const stateRef = useRef(state);
stateRef.current = state;
const playingRef = useRef(playing);
playingRef.current = playing;
const speedRef = useRef(speed);
speedRef.current = speed;
const delay = useCallback((ms: number) => {
return new Promise<void>(resolve => {
timerRef.current = setTimeout(resolve, ms / speedRef.current);
});
}, []);
/** Core replay loop — runs as an async generator stepping through all events */
const runLoop = useCallback(async () => {
for (let ri = stateRef.current.roundIndex; ri < rounds.length; ri++) {
const round = rounds[ri];
// Restore already-finished rounds instantly
if (ri < stateRef.current.roundIndex) continue;
for (let si = stateRef.current.visibleStepCount; si < round.steps.length; si++) {
if (!playingRef.current) return; // paused
const step = round.steps[si];
await delay(BASE_DELAYS.beforeStep);
if (!playingRef.current) return;
// Reveal the step
setState(prev => ({
...prev,
roundIndex: ri,
visibleStepCount: si + 1,
activeStepId: step.id,
showFinalForRound: false,
}));
await delay(BASE_DELAYS.afterStep);
if (!playingRef.current) return;
}
// Show final summary
await delay(BASE_DELAYS.beforeFinal);
if (!playingRef.current) return;
setState(prev => ({
...prev,
roundIndex: ri,
visibleStepCount: round.steps.length,
activeStepId: round.steps.length > 0 ? round.steps[round.steps.length - 1].id : null,
showFinalForRound: true,
completedRoundIndex: ri,
}));
if (ri < rounds.length - 1) {
await delay(BASE_DELAYS.betweenRounds);
if (!playingRef.current) return;
// Move to next round
setState(prev => ({
...prev,
roundIndex: ri + 1,
visibleStepCount: 0,
activeStepId: null,
showFinalForRound: false,
}));
}
}
// Replay complete
setState(prev => ({ ...prev, done: true }));
setPlaying(false);
}, [rounds, delay]);
const play = useCallback(() => {
if (stateRef.current.done) return;
setPlaying(true);
}, []);
const pause = useCallback(() => {
setPlaying(false);
if (timerRef.current) clearTimeout(timerRef.current);
}, []);
// Start/resume loop when playing becomes true
useEffect(() => {
if (playing) {
runLoop();
}
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [playing, runLoop]);
// Auto-play on mount when rounds are ready
useEffect(() => {
if (autoPlay && rounds.length > 0) {
setPlaying(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
/** Jump to a specific round (skips to its end instantly) */
const jumpToRound = useCallback((ri: number) => {
pause();
setState({
roundIndex: ri,
visibleStepCount: rounds[ri]?.steps.length ?? 0,
activeStepId: rounds[ri]?.steps.slice(-1)[0]?.id ?? null,
showFinalForRound: true,
completedRoundIndex: ri,
done: ri === rounds.length - 1,
});
}, [pause, rounds]);
/** Restart replay from the beginning */
const restart = useCallback(() => {
pause();
setState({
roundIndex: 0,
visibleStepCount: 0,
activeStepId: null,
showFinalForRound: false,
completedRoundIndex: -1,
done: false,
});
// Small delay to let state settle before starting loop
setTimeout(() => setPlaying(true), 50);
}, [pause]);
return { state, playing, play, pause, jumpToRound, restart };
}
// ---------------------------------------------------------------------------
// Page component
// ---------------------------------------------------------------------------
interface SharePageProps {
token: string;
messages?: Array<{ role: string; context: string; order: number }>;
firstQuestion?: string;
error?: string;
}
const SharePage: NextPage<SharePageProps> = ({ token, messages: initMessages, firstQuestion, error: initError }) => {
const [fetchError] = useState<string | null>(initError || null);
const [speed, setSpeed] = useState(1);
const [rightPanelView, setRightPanelView] = useState<PanelView>('execution');
const rounds = initMessages ? buildReplayRounds(initMessages) : [];
const { state, playing, play, pause, jumpToRound, restart } = useReplayEngine(rounds, speed, true);
// -------------------------------------------------------------------------
// Derive display data from replay state
// -------------------------------------------------------------------------
/** For each round index, compute what's visible */
const getVisibleRoundData = (ri: number) => {
const round = rounds[ri];
if (!round) return null;
const isCurrentRound = ri === state.roundIndex;
const isCompletedRound = ri < state.roundIndex || (ri === state.roundIndex && state.showFinalForRound);
const visibleSteps: ManusExecutionStep[] = isCurrentRound
? round.steps.slice(0, state.visibleStepCount).map((s, idx) => ({
...s,
status: idx < state.visibleStepCount - 1 ? 'completed' : (playing ? 'running' : 'completed'),
}))
: isCompletedRound
? round.steps.map(s => ({ ...s, status: 'completed' as const }))
: [];
const sections = buildSections(visibleSteps);
const showFinal = isCompletedRound || (isCurrentRound && state.showFinalForRound);
const activeStepId = isCurrentRound ? state.activeStepId : (isCompletedRound ? round.steps.slice(-1)[0]?.id ?? null : null);
return { round, visibleSteps, sections, showFinal, activeStepId };
};
// Active round for the right panel
const activeRoundData = getVisibleRoundData(state.roundIndex);
const activeStepId = state.activeStepId;
const rightPanelOutputs: ManusExecutionOutput[] = (() => {
if (!activeRoundData || !activeStepId) return [];
return activeRoundData.round.outputs[activeStepId] || [];
})();
const rightPanelActiveStep: ActiveStepInfo | null = (() => {
if (!activeRoundData || !activeStepId) return null;
const step = activeRoundData.round.steps.find(s => s.id === activeStepId);
if (!step) return null;
return {
id: step.id,
type: step.type,
title: step.title || '',
subtitle: step.subtitle,
detail: step.description,
status: playing ? 'running' : 'completed',
};
})();
// Progress: total steps across all rounds
const totalSteps = rounds.reduce((acc, r) => acc + r.steps.length, 0);
const completedSteps = rounds.slice(0, state.roundIndex).reduce((acc, r) => acc + r.steps.length, 0) + state.visibleStepCount;
const progressPercent = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
message.success('链接已复制');
} catch {
message.error('复制失败');
}
};
// -------------------------------------------------------------------------
// Render
// -------------------------------------------------------------------------
if (fetchError || rounds.length === 0) {
return (
<div className='flex items-center justify-center h-screen bg-white dark:bg-[#111217]'>
<div className='text-center space-y-3'>
<p className='text-2xl font-semibold text-gray-700 dark:text-gray-200'></p>
<p className='text-gray-400'>{fetchError || '没有可回放的对话内容'}</p>
</div>
</div>
);
}
return (
<>
<Head>
<title>{firstQuestion ? `${firstQuestion.slice(0, 60)}${firstQuestion.length > 60 ? '…' : ''} · DB-GPT 回放` : 'DB-GPT 对话回放'}</title>
</Head>
<div className='flex flex-col h-screen bg-white dark:bg-[#111217] overflow-hidden'>
{/* ---------------------------------------------------------------- */}
{/* Top bar */}
{/* ---------------------------------------------------------------- */}
<div className='flex items-center justify-between px-5 py-2.5 border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-[#1a1b1e] flex-shrink-0'>
<div className='flex items-center gap-3 min-w-0'>
{/* Logo / brand */}
<span className='font-bold text-base text-gray-800 dark:text-white flex-shrink-0'>DB-GPT</span>
<div className='w-px h-4 bg-gray-200 dark:bg-gray-700 flex-shrink-0' />
<span className='text-xs font-medium text-blue-500 bg-blue-50 dark:bg-blue-500/10 px-2 py-0.5 rounded-full flex-shrink-0'></span>
{firstQuestion && (
<span className='text-sm text-gray-500 dark:text-gray-400 truncate max-w-[400px]' title={firstQuestion}>
{firstQuestion}
</span>
)}
</div>
{/* Playback controls */}
<div className='flex items-center gap-2'>
{/* Speed selector */}
<div className='flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5'>
{SPEED_OPTIONS.map(opt => (
<button
key={opt.value}
onClick={() => setSpeed(opt.value)}
className={`px-2.5 py-1 rounded-md text-xs font-medium transition-colors ${
speed === opt.value
? 'bg-white dark:bg-gray-700 text-gray-800 dark:text-white shadow-sm'
: 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
{opt.label}
</button>
))}
</div>
{/* Divider */}
<div className='w-px h-5 bg-gray-200 dark:bg-gray-700' />
{/* Play / Pause */}
{state.done ? (
<Button
icon={<ReloadOutlined />}
onClick={restart}
>
</Button>
) : playing ? (
<Button
icon={<PauseCircleOutlined />}
onClick={pause}
>
</Button>
) : (
<Button
type='primary'
icon={<PlayCircleOutlined />}
onClick={play}
>
{completedSteps === 0 ? '开始回放' : '继续'}
</Button>
)}
{/* Skip to end */}
{!state.done && (
<Tooltip title='跳到最后一步'>
<Button
icon={<StepForwardOutlined />}
onClick={() => jumpToRound(rounds.length - 1)}
/>
</Tooltip>
)}
{/* Divider */}
<div className='w-px h-5 bg-gray-200 dark:bg-gray-700' />
{/* Copy share link — blue to match the UI theme */}
<Tooltip title='复制分享链接,任何人可通过此链接回放对话'>
<Button
icon={<LinkOutlined />}
onClick={handleCopyLink}
style={{ color: '#3b82f6', borderColor: '#3b82f6' }}
>
</Button>
</Tooltip>
</div>
</div>
{/* ---------------------------------------------------------------- */}
{/* Progress bar */}
{/* ---------------------------------------------------------------- */}
<div className='px-5 py-2 border-b border-gray-100 dark:border-gray-800/60 bg-gray-50/50 dark:bg-transparent flex-shrink-0'>
<div className='flex items-center gap-3'>
<div className='flex-1'>
<div className='h-1 bg-gray-200 dark:bg-gray-800 rounded-full overflow-hidden'>
<div
className='h-full bg-blue-500 rounded-full transition-all duration-500 ease-out'
style={{ width: `${progressPercent}%` }}
/>
</div>
</div>
<span className='text-xs text-gray-400 whitespace-nowrap tabular-nums'>
{completedSteps} / {totalSteps}
</span>
{/* Round selector tabs */}
{rounds.length > 1 && (
<div className='flex items-center gap-1'>
{rounds.map((round, ri) => (
<Tooltip key={ri} title={round.humanText ? round.humanText.slice(0, 40) : `${ri + 1}`}>
<button
onClick={() => jumpToRound(ri)}
className={`w-5 h-5 rounded-full text-[10px] font-bold transition-colors ${
ri === state.roundIndex
? 'bg-blue-500 text-white'
: ri <= state.completedRoundIndex
? 'bg-emerald-100 dark:bg-emerald-900/40 text-emerald-600 dark:text-emerald-400'
: 'bg-gray-200 dark:bg-gray-700 text-gray-400'
}`}
>
{ri + 1}
</button>
</Tooltip>
))}
</div>
)}
</div>
</div>
{/* ---------------------------------------------------------------- */}
{/* Main content: left (steps) + right (output) */}
{/* ---------------------------------------------------------------- */}
<div className='flex-1 flex overflow-hidden'>
{/* Left panel — all rounds stacked */}
<div className='w-[42%] min-w-[360px] border-r border-gray-200 dark:border-gray-800 flex flex-col overflow-y-auto'>
{rounds.map((_, ri) => {
const data = getVisibleRoundData(ri);
if (!data) return null;
const isCurrentRound = ri === state.roundIndex;
const isPastRound = ri < state.roundIndex;
const isFutureRound = ri > state.roundIndex;
// Future rounds are hidden until the replay reaches them
if (isFutureRound) return null;
const isCollapsed = isPastRound;
return (
<ManusLeftPanel
key={ri}
sections={data.sections}
activeStepId={isCurrentRound ? data.activeStepId : undefined}
onStepClick={undefined /* read-only */}
isWorking={isCurrentRound && playing && !state.showFinalForRound}
userQuery={data.round.humanText}
assistantText={data.showFinal ? data.round.finalContent : undefined}
stepThoughts={data.round.stepThoughts}
artifacts={data.showFinal ? data.round.artifacts : []}
onViewAllFiles={data.showFinal ? () => setRightPanelView('files') : undefined}
isCollapsed={isCollapsed}
onExpand={() => jumpToRound(ri)}
/>
);
})}
{/* Pending placeholder for rounds not yet reached */}
{rounds.length > 1 && state.roundIndex < rounds.length - 1 && (
<div className='px-4 py-3 text-xs text-gray-400 text-center animate-pulse'>
{rounds.length - state.roundIndex - 1} ...
</div>
)}
{/* Bottom breathing room */}
<div className='flex-shrink-0 h-8' />
</div>
{/* Right panel — outputs of the active step */}
<div className='flex-1 overflow-hidden'>
<ManusRightPanel
activeStep={rightPanelActiveStep}
outputs={rightPanelOutputs}
isRunning={playing && !!activeStepId}
artifacts={activeRoundData?.round.artifacts ?? []}
panelView={rightPanelView}
onPanelViewChange={setRightPanelView}
/>
</div>
</div>
</div>
</>
);
};
export const getServerSideProps: GetServerSideProps = async context => {
const { token } = context.params as { token: string };
const apiBase = process.env.API_BASE_URL ?? 'http://127.0.0.1:5670';
try {
const res = await fetch(`${apiBase}/api/v1/chat/share/${token}`);
if (!res.ok) {
return { props: { token, error: `分享链接无效或已过期 (${res.status})` } };
}
const json = await res.json();
const messages = json?.data?.messages ?? null;
if (!Array.isArray(messages)) {
return { props: { token, error: '数据格式错误' } };
}
const firstQuestion: string = messages.find((m: any) => m.role === 'human')?.context ?? '';
return { props: { token, messages, firstQuestion } };
} catch (e: any) {
return { props: { token, error: e?.message || '加载失败' } };
}
};
export default SharePage;