Langchain-Chatchat/frontend/contributing/Basic/Feature-Development.zh-CN.md
imClumsyPanda 995c5e300e
Pre-release v0.3.0 (#4159)
* publish 0.2.10 (#2797)

新功能:
- 优化 PDF 文件的 OCR,过滤无意义的小图片 by @liunux4odoo #2525
- 支持 Gemini 在线模型 by @yhfgyyf #2630
- 支持 GLM4 在线模型 by @zRzRzRzRzRzRzR
- elasticsearch更新https连接 by @xldistance #2390
- 增强对PPT、DOC知识库文件的OCR识别 by @596192804 #2013
- 更新 Agent 对话功能 by @zRzRzRzRzRzRzR
- 每次创建对象时从连接池获取连接,避免每次执行方法时都新建连接 by @Lijia0 #2480
- 实现 ChatOpenAI 判断token有没有超过模型的context上下文长度 by @glide-the
- 更新运行数据库报错和项目里程碑 by @zRzRzRzRzRzRzR #2659
- 更新配置文件/文档/依赖 by @imClumsyPanda @zRzRzRzRzRzRzR
- 添加日文版 readme by @eltociear #2787

修复:
- langchain 更新后,PGVector 向量库连接错误 by @HALIndex #2591
- Minimax's model worker 错误 by @xyhshen 
- ES库无法向量检索.添加mappings创建向量索引 by MSZheng20 #2688

* Update README.md

* Add files via upload

* Update README.md

* 修复PDF旋转的BUG

* Support Chroma

* perf delete unused import

* 忽略测试代码

* 更新文件

* API前端丢失问题解决

* 更新了chromadb的打印的符号

* autodl代号错误

* Update README.md

* Update README.md

* Update README.md

* 修复milvus相关bug

* 支持星火3.5模型

* 修复es 知识库查询bug (#2848)

* 修复es 知识库查询bug (#2848)

* 更新zhipuai请求方式

* 增加对 .htm 扩展名的显式支持

* 更新readme

* Docker镜像制作与K8S YAML部署操作说明 (#2892)

* Dev (#2280)

* 修复Azure 不设置Max token的bug

* 重写agent

1. 修改Agent实现方式,支持多参数,仅剩 ChatGLM3-6b和 OpenAI GPT4 支持,剩余模型将在暂时缺席Agent功能
2. 删除agent_chat 集成到llm_chat中
3. 重写大部分工具,适应新Agent

* 更新架构

* 删除web_chat,自动融合

* 移除所有聊天,都变成Agent控制

* 更新配置文件

* 更新配置模板和提示词

* 更改参数选择bug

* 修复模型选择的bug

* 更新一些内容

* 更新多模态 语音 视觉的内容

1. 更新本地模型语音 视觉多模态功能并设置了对应工具

* 支持多模态Grounding

1. 美化了chat的代码
2. 支持视觉工具输出Grounding任务
3. 完善工具调用的流程

* 支持XPU,修改了glm3部分agent

* 添加 qwen agent

* 对其ChatGLM3-6B与Qwen-14B

* fix callback handler

* 更新Agent工具返回

* fix: LLMChain no output when no tools selected

* 跟新了langchain 0.1.x需要的依赖和修改的代码

* 更新chatGLM3 langchain0.1.x Agent写法

* 按照 langchain 0.1 重写 qwen agent

* 修复 callback 无效的问题

* 添加文生图工具

* webui 支持文生图

* 集成openai plugins插件

* 删除fastchat的配置

* 增加openai插件

* 集成openai plugins插件

* 更新模型执行列表和今晚修改的内容

* 集成openai_plugins/imitater插件

* 集成openai_plugins/imitater插件

* 集成openai_plugins/imitater插件

* 减少错误的显示

* 标准配置

* vllm参数配置

* 增加智谱插件

* 删除本地fschat配置

* 删除本地fschat配置,pydantic升级到2

* 删除本地fschat workers

* openai-plugins-list.json

* 升级agent,pydantic升级到2

* fix model_config是系统关键词问题

* embeddings模块集成openai plugins插件,使用统一api调用

* loom模型服务update_store更新逻辑

* 集成LOOM在线embedding业务

* 本地知识库搜索字段修改

* 知识库在线api接入点配置在线api接入点配置更新逻辑

* Update model_config.py.example

* 修改模型配置方式,所有模型以 openai 兼容框架的形式接入,chatchat 自身不再加载模型。
改变 Embeddings 模型改为使用框架 API,不再手动加载,删除自定义 Embeddings Keyword 代码
修改依赖文件,移除 torch transformers 等重依赖
暂时移出对 loom 的集成

后续:
1、优化目录结构
2、检查合并中有无被覆盖的 0.2.10 内容

* move document_loaders & text_splitter under server

* make torch & transformers optional
import pydantic Model & Field from langchain.pydantic_v1 instead of pydantic.v1

* - pydantic 限定为 v1,并统一项目中所有 pydantic 导入路径,为以后升级 v2 做准备
- 重构 api.py:
    - 按模块划分为不同的 router
    - 添加 openai 兼容的转发接口,项目默认使用该接口以实现模型负载均衡
    - 添加 /tools 接口,可以获取/调用编写的 agent tools
    - 移除所有 EmbeddingFuncAdapter,统一改用 get_Embeddings
    - 待办:
        - /chat/chat 接口改为 openai 兼容
        - 添加 /chat/kb_chat 接口,openai 兼容
        - 改变 ntlk/knowledge_base/logs 等数据目录位置

* 移除 llama-index 依赖;修复 /v1/models 错误

* 原因:windows下启动失败提示补充python-multipart包 (#3184)

改动:requirements添加python-multipart==0.0.9
版本:0.0.9  Requires: Python >=3.8

Co-authored-by: XuCai <liangxc@akulaku.com>

* 添加 xinference 本地模型和自定义模型配置 UI: streamlit run model_loaders/xinference_manager.py

* update xinference manager ui

* fix merge conflict

* model_config 中补充 oneapi 默认在线模型;/v1/models 接口支持 oneapi 平台,统一返回模型列表

* 重写 calculate 工具

* 调整根目录结构,kb/logs/media/nltk_data 移动到专用数据目录(可配置,默认 data)。注意知识库文件要做相应移动

* update kb_config.py.example

* 优化 ES 知识库
- 开发者
    - get_OpenAIClient 的 local_wrap 默认值改为 False,避免 API 服务未启动导致其它功能受阻(如Embeddings)
    - 修改 ES 知识库服务:
	- 检索策略改为 ApproxRetrievalStrategy
	- 设置 timeout 为 60, 避免文档过多导致 ConnecitonTimeout Error
    - 修改 LocalAIEmbeddings,使用多线程进行  embed_texts,效果不明显,瓶颈可能主要在提供 Embedding 的服务器上

* 修复glm3 agent被注释的agent会话文本结构解析代码
看起来输出的文本占位符如下,目前解析代码是有问题的
Thought <|assistant|> Action\r
```python
tool_call(action_input)
```<|observation|>

* make qwen agent work with langchain>=0.1 (#3228)

* make xinference model manager support xinference 0.9.x

* 使用多进程提高导入知识库的速度 (#3276)

* xinference的代码

先传 我后面来改

* Delete server/xinference directory

* Create khazic

* diiii

diii

* Revert "xinference的代码"

* fix markdown header split (#1825) (#3324)

* dify model_providers configuration
This module provides the interface for invoking and authenticating various models, and offers Dify a unified information and credentials form rule for model providers.

* fix merge conflict: langchain Embeddings not imported in server.utils

* 添加 react 编写的新版 WEBUI (#3417)

* feat:提交前端代码

* feat:提交logo样式切换

* feat:替换avatar、部分位置icon、chatchat相关说明、git链接、Wiki链接、关于、设置、反馈与建议等功能,关闭lobehub自检更新功能

* fix:移除多余代码

---------

Co-authored-by: liunux4odoo <41217877+liunux4odoo@users.noreply.github.com>

* model_providers bootstrap

* model_providers bootstrap

* update to pydantic v2 (#3486)

* 使用poetry管理项目

* 使用poetry管理项目

* dev分支解决pydantic版本冲突问题,增加ollama配置,支持ollama会话和向量接口 (#3508)

* dev分支解决pydantic版本冲突问题,增加ollama配置,支持ollama会话和向量接口
1、因dev版本的pydantic升级到了v2版本,由于在class History(BaseModel)中使用了from server.pydantic_v1,而fastapi的引用已变为pydantic的v2版本,所以fastapi用v2版本去校验用v1版本定义的对象,当会话历史histtory不为空的时候,会报错:TypeError: BaseModel.validate() takes 2 positional arguments but 3 were given。经测试,解方法为在class History(BaseModel)中也使用v2版本即可;
2、配置文件参照其它平台配置,增加了ollama平台相关配置,会话模型用户可根据实际情况自行添加,向量模型目前支持nomic-embed-text(必须升级ollama到0.1.29以上)。
3、因ollama官方只在会话部分对openai api做了兼容,向量api暂未适配,好在langchain官方库支持OllamaEmbeddings,因而在get_Embeddings方法中添加了相关支持代码。

* 修复 pydantic 升级到 v2 后 DocumentWithVsID 和 /v1/embeddings 兼容性问题

---------

Co-authored-by: srszzw <srszzw@163.com>
Co-authored-by: liunux4odoo <liunux@qq.com>

* 对python的要求降级到py38

* fix bugs; make poetry using tsinghua mirror of pypi

* update gitignore; remove unignored files

* update wiki sub module

* 20240326

* 20240326

* qqqq

* 删除历史文件

* 移动项目模块

* update .gitignore; fix model version error in api_schemas

* 封装ModelManager

* - 重写 tool 部分: (#3553)

- 简化 tool 的定义方式
    - 所有 tool 和 tool_config 支持热加载
    - 修复:json_schema_extra warning

* 使用yaml加载用户配置适配器

* 格式化代码

* 格式化

* 优化工具定义;添加 openai 兼容的统一 chat 接口 (#3570)

- 修复:
    - Qwen Agent 的 OutputParser 不再抛出异常,遇到非 COT 文本直接返回
    - CallbackHandler 正确处理工具调用信息

- 重写 tool 定义方式:
    - 添加 regist_tool 简化 tool 定义:
        - 可以指定一个用户友好的名称
        - 自动将函数的 __doc__ 作为 tool.description
	- 支持用 Field 定义参数,不再需要额外定义 ModelSchema
        - 添加 BaseToolOutput 封装 tool	返回结果,以便同时获取原始值、给LLM的字符串值
        - 支持工具热加载(有待测试)

- 增加 openai 兼容的统一 chat 接口,通过 tools/tool_choice/extra_body 不同参数组合支持:
    - Agent 对话
    - 指定工具调用(如知识库RAG)
    - LLM 对话

- 根据后端功能更新 webui

* 修复:search_local_knowledge_base 工具返回值错误;/tools 路由错误;webui 中“正在思考”一直显示 (#3571)

* 添加 openai 兼容的 files 接口 (#3573)

* 使用BootstrapWebBuilder适配RESTFulOpenAIBootstrapBaseWeb加载

* 格式化和代码检查说明

* 模型列表适配

* make format

* chat_completions接口报文适配

* make format

* xinference 插件示例

* 一些默认参数

* exec path fix

* 解决ollama部署的qwen,执行agent,返回的json格式不正确问题。

* provider_configuration.py
查询所有的平台信息,包含计费策略和配置schema_validators(参数必填信息校验规则)
/workspaces/current/model-providers
查询平台模型分类的详细默认信息,包含了模型类型,模型参数,模型状态
workspaces/current/models/model-types/{model_type}

* 开发手册

* 兼容model_providers,集成webui及API中平台配置的初始化 (#3625)

* provider_configuration init of MODEL_PLATFORMS

* 开发手册

* 兼容model_providers,集成webui及API中平台配置的初始化

* Dev model providers (#3628)


* gemini 初始化参数问题

* gemini 同步工具调用

* embedding convert endpoint

* 修复 --api -w命令

* /v1/models 接口返回值由 List[Model] 改为 {'data': List[Model]},兼容最新版 xinference

* 3.8兼容 (#3769)

* 增加使用说明

* 3.8兼容性配置

* fix

* formater

* 不同平台兼容测试用例

* embedding兼容

* 增加日志信息

* pip源仓库设置,一些版本问题,启动说明  配置说明 (#3854)

* 仓库设置,一些版本问题

* pip源仓库设置,一些版本问题,启动说明

* 配置说明

* 泛型标记错误 (#3855)

* 仓库设置,一些版本问题

* pip源仓库设置,一些版本问题,启动说明

* 配置说明

* 发布的依赖信息

* 泛型标记错误

* 泛型标记错误

* CICD github action build publish pypi、Release Tag (#3886)

* 测试用例

* CICD 流程

* CICD 流程

* CICD 流程

* 一些agent数据处理的问题,model_runtime模块的说明文档 (#3943)

* 一些agent数据出来的问题

* Changes:
- Translated and updated the Model Runtime documentation to reflect the latest changes and features.
- Clarified the decoupling benefits of the Model Runtime module from the Chatchat service.
- Removed outdated information regarding the model configuration storage module.
- Detailed the retained functionalities post-removal of the Dify configuration page.
- Provided a comprehensive overview of the Model Runtime's three-layered structure.
- Included the status of the `fetch-from-remote` feature and its non-implementation in Dify.
- Added instructions for custom service provider model capabilities.

* - 新功能 (#3944)

- streamlit 更新到 1.34,webui 支持 Dialog 操作
    - streamlit-chatbox 更新到 1.1.12,更好的多会话支持
- 开发者
    - 在 API 中增加项目图片路由(/img/{file_name}),方便前端使用

* 修改包名

* 修改包信息

* ollama配置解析问题

* 用户配置动态加载 (#3951)

* version = "0.3.0.20240506"

* version = "0.3.0.20240506"

* version = "0.3.0.20240506"

* version = "0.3.0.20240506"

* 启动说明

* 一些bug

* 修复了一些配置重载的bug

* 配置的加载行为修改

* 配置的加载行为修改

* agent代码优化

* ollama 代码升级,使用openai协议

* 支持deepseek客户端

* contributing (#4043)

* 添加了贡献说明 docs/contributing,包含了一些代码仓库说明和开发规范,以及在model_providers下面编写了一些单元测试的示例

* 关于providers的配置说明

* python3.8兼容

* python3.8兼容

* ollama兼容

* ollama兼容

* 一些兼容 pydantic<3,>=1.9.0  的代码,

* 一些兼容 pydantic<3,>=1.9.0 model_config 的代码,

* make format

* test

* 更新版本

* get_img_base64

* get_img_base64

* get_img_base64

* get_img_base64

* get_img_base64

* 统一模型类型编码

* 向量处理问题

* 优化目录结构 (#4058)

* 优化目录结构

* 修改一些测试问题

---------

Co-authored-by: glide-the <2533736852@qq.com>

* repositories

* 调整日志

* 调整日志zdf

* 增加可选依赖extras

* feat:Added some documentation. (#4085)

* feat:Added some documentation.

* feat:Added some documentation.

* feat:Added some documentation.

---------

Co-authored-by: yuehuazhang <yuehuazhang@tencent.com>

* fix code.md typos

* fix chatchat-server/pyproject.toml typos

* feat:README (#4118)

Co-authored-by: yuehuazhang <yuehuazhang@tencent.com>

* 初始化数据库集成model_providers

* 关闭守护进程

* 1、修改知识库列表接口,返回全量属性字段,同时修改受影响的相关代码。 (#4119)

2、run_in_process_pool改为run_in_thread_pool,解决兼容性问题。
3、poetry配置文件修复。

* 动态更新Prompt中的知识库描述信息,使大模型更容易判断使用哪个知识库。 (#4121)

* 1、修改知识库列表接口,返回全量属性字段,同时修改受影响的相关代码。
2、run_in_process_pool改为run_in_thread_pool,解决兼容性问题。
3、poetry配置文件修复。

* 1、动态更新Prompt中的知识库描述信息,使大模型更容易判断使用哪个知识库。

* fix: 补充 xinference 配置信息 (#4123)

* feat:README

* feat:补充 xinference 平台 llm 和 embedding 模型配置.

---------

Co-authored-by: yuehuazhang <yuehuazhang@tencent.com>

* 知识库工具的下拉列表改为动态获取,不必重启服务。 (#4126)

* 1、知识库工具的下拉列表改为动态获取,不必重启服务。

* update README and imgs

* update README and imgs

* update README and imgs

* update README and imgs

* 修改安装说明描述问题

* make formater

* 更新版本"0.3.0.20240606

* Update code.md

* 优化知识库相关功能 (#4153)

- 新功能
    - pypi 包新增 chatchat-kb 命令脚本,对应 init_database.py 功能

- 开发者
    - _model_config.py 中默认包含 xinference 配置项
    - 所有涉及向量库的操作,前置检查当前 Embed 模型是否可用
    - /knowledge_base/create_knowledge_base 接口增加 kb_info 参数
    - /knowledge_base/list_files 接口返回所有数据库字段,而非文件名称列表
    - 修正 xinference 模型管理脚本

* 消除警告

* 一些依赖问题

* 增加text2sql工具,支持特定表、智能判定表,支持对表名进行额外说明 (#4154)

* 1、增加text2sql工具,支持特定表、智能判定表,支持对表名进行额外说明

* 支持SQLAlchemy大部分数据库、新增read-only模式,提高安全性、增加text2sql使用建议 (#4155)

* 1、修改text2sql连接配置,支持SQLAlchemy大部分数据库;
2、新增read-only模式,若有数据库写保护需求,会从大模型判断、SQLAlchemy拦截器两个层面进行写拦截,提高安全性;
3、增加text2sql使用建议;

* dotenv

* dotenv 配置

* 用户工作空间操作 (#4156)

工作空间的配置预设,提供ConfigBasic建造方法产生实例。
  该类的实例对象用于存储工作空间的配置信息,如工作空间的路径等
  工作空间的配置信息存储在用户的家目录下的.config/chatchat/workspace/workspace_config.json文件中。
  注意:不存在则读取默认

提供了操作入口
指令` chatchat-config` 工作空间配置

options:
```
  -h, --help            show this help message and exit
  -v {true,false}, --verbose {true,false}
                        是否开启详细日志
  -d DATA, --data DATA  数据存放路径
  -f FORMAT, --format FORMAT
                        日志格式
  --clear               清除配置
```

* 配置路径问题

* fix faiss_cache bug

* Feature(File RAG): add file_rag in chatchat-server, add ensemble retriever and vectorstore retriever.

* Feature(File RAG): add file_rag in chatchat-server, add ensemble retriever and vectorstore retriever.

* fix xinference manager bug

* Fix(File RAG): use jieba instead of cutword

* Fix(File RAG): update kb_doc_api.py

* 工作空间的配置预设,提供ConfigBasic建造 实例。 (#4158)

- ConfigWorkSpace接口说明
```text

ConfigWorkSpace是一个配置工作空间的抽象类,提供基础的配置信息存储和读取功能。
提供ConfigFactory建造方法产生实例。
该类的实例对象用于存储工作空间的配置信息,如工作空间的路径等
工作空间的配置信息存储在用户的家目录下的.chatchat/workspace/workspace_config.json文件中。
注意:不存在则读取默认
```

* 编写配置说明

* 编写配置说明

---------

Co-authored-by: liunux4odoo <41217877+liunux4odoo@users.noreply.github.com>
Co-authored-by: glide-the <2533736852@qq.com>
Co-authored-by: tonysong <tonysong@digitalgd.com.cn>
Co-authored-by: songpb <songpb@gmail.com>
Co-authored-by: showmecodett <showmecodett@gmail.com>
Co-authored-by: zR <2448370773@qq.com>
Co-authored-by: zqt <1178747941@qq.com>
Co-authored-by: zqt996 <67185303+zqt996@users.noreply.github.com>
Co-authored-by: fengyaojie <fengyaojie@xdf.cn>
Co-authored-by: Hans WAN <hanswan@tom.com>
Co-authored-by: thinklover <thinklover@gmail.com>
Co-authored-by: liunux4odoo <liunux@qq.com>
Co-authored-by: xucailiang <74602715+xucailiang@users.noreply.github.com>
Co-authored-by: XuCai <liangxc@akulaku.com>
Co-authored-by: dignfei <913015993@qq.com>
Co-authored-by: Leb <khazzz1c@gmail.com>
Co-authored-by: Sumkor <sumkor@foxmail.com>
Co-authored-by: panhong <381500590@qq.com>
Co-authored-by: srszzw <741992282@qq.com>
Co-authored-by: srszzw <srszzw@163.com>
Co-authored-by: yuehua-s <41819795+yuehua-s@users.noreply.github.com>
Co-authored-by: yuehuazhang <yuehuazhang@tencent.com>
2024-06-10 22:48:35 +08:00

26 KiB
Raw Blame History

LobeChat 功能开发完全指南

本文档旨在指导开发者了解如何在 LobeChat 中开发一块完整的功能需求。

我们将以 sessionGroup 的实现为示例: feat: add session group manager 通过以下六个主要部分来阐述完整的实现流程:

  1. 数据模型 / 数据库定义
  2. Service 实现 / Model 实现
  3. 前端数据流 Store 实现
  4. UI 实现与 action 绑定
  5. 数据迁移
  6. 数据导入导出

一、数据库部分

为了实现 Session Group 功能,首先需要在数据库层面定义相关的数据模型和索引。

定义一个新的 sessionGroup 表,分 3 步:

1. 建立数据模型 schema

src/database/schema/sessionGroup.ts 中定义 DB_SessionGroup 的数据模型:

import { z } from 'zod';

export const DB_SessionGroupSchema = z.object({
  name: z.string(),
  sort: z.number().optional(),
});

export type DB_SessionGroup = z.infer<typeof DB_SessionGroupSchema>;

2. 创建数据库索引

由于要新增一个表,所以需要在在数据库 Schema 中,为 sessionGroup 表添加索引。

src/database/core/schema.ts 中添加 dbSchemaV4:

// ... 前面的一些实现

// ************************************** //
// ******* Version 3 - 2023-12-06 ******* //
// ************************************** //
// - Added `plugin` table

export const dbSchemaV3 = {
  ...dbSchemaV2,
  plugins:
    '&identifier, type, manifest.type, manifest.meta.title, manifest.meta.description, manifest.meta.author, createdAt, updatedAt',
};

+ // ************************************** //
+ // ******* Version 4 - 2024-01-21 ******* //
+ // ************************************** //
+ // - Added `sessionGroup` table

+ export const dbSchemaV4 = {
+   ...dbSchemaV3,
+   sessionGroups: '&id, name, sort, createdAt, updatedAt',
+   sessions: '&id, type, group, pinned, meta.title, meta.description, meta.tags, createdAt, updatedAt',
};

[!Note]

除了 sessionGroups 外,此处也修改了 sessions 的定义,原因是存在数据迁移的情况。但由于本节只关注 schema 定义,不展开数据迁移部分实现,详情可见第五节。

[!Important]

如果你不了解为何此处需要创建索引,以及不了解此处的 schema 的定义语法。你可能需要提前了解下 Dexie.js 相关的基础知识,可以查阅 📘 本地数据库 部分了解相关内容。

3. 在本地 DB 中加入 sessionGroups 表

扩展本地数据库类以包含新的 sessionGroups 表:


import { dbSchemaV1, dbSchemaV2, dbSchemaV3, dbSchemaV4 } from './schemas';

interface LobeDBSchemaMap {
  files: DB_File;
  messages: DB_Message;
  plugins: DB_Plugin;
+ sessionGroups: DB_SessionGroup;
  sessions: DB_Session;
  topics: DB_Topic;
}

// Define a local DB
export class LocalDB extends Dexie {
  public files: LobeDBTable<'files'>;
  public sessions: LobeDBTable<'sessions'>;
  public messages: LobeDBTable<'messages'>;
  public topics: LobeDBTable<'topics'>;
  public plugins: LobeDBTable<'plugins'>;
+ public sessionGroups: LobeDBTable<'sessionGroups'>;

  constructor() {
    super(LOBE_CHAT_LOCAL_DB_NAME);
    this.version(1).stores(dbSchemaV1);
    this.version(2).stores(dbSchemaV2);
    this.version(3).stores(dbSchemaV3);
+   this.version(4).stores(dbSchemaV4);

    this.files = this.table('files');
    this.sessions = this.table('sessions');
    this.messages = this.table('messages');
    this.topics = this.table('topics');
    this.plugins = this.table('plugins');
+   this.sessionGroups = this.table('sessionGroups');
  }
}

如此一来,你就可以通过在 Application -> Storage -> IndexedDB 中查看到 LOBE_CHAT_DB 里的 sessionGroups 表了。

二、Model 与 Service 部分

定义 Model

在构建 LobeChat 应用时Model 负责与数据库的交互,它定义了如何读取、插入、更新和删除数据库的数据,定义具体的业务逻辑。

src/database/model/sessionGroup.ts 中定义 SessionGroupModel

import { BaseModel } from '@/database/core';
import { DB_SessionGroup, DB_SessionGroupSchema } from '@/database/schemas/sessionGroup';
import { nanoid } from '@/utils/uuid';

class _SessionGroupModel extends BaseModel {
  constructor() {
    super('sessions', DB_SessionGroupSchema);
  }

  async create(name: string, sort?: number, id = nanoid()) {
    return this._add({ name, sort }, id);
  }

  // ... 其他 CRUD 方法的实现
}

export const SessionGroupModel = new _SessionGroupModel();

Service 实现

在 LobeChat 中Service 层主要负责与后端服务进行通信,封装业务逻辑,并提供数据给前端的其他层使用。SessionService 是一个专门处理与会话Session相关业务逻辑的服务类它封装了创建会话、查询会话、更新会话等操作。

为了保持代码的可维护性和可扩展性,我们将会话分组相关的服务逻辑放在 SessionService 中,这样可以使会话领域的业务逻辑保持内聚。当业务需求增加或变化时,我们可以更容易地在这个领域内进行修改和扩展。

SessionService 通过调用 SessionGroupModel 的方法来实现对会话分组的管理。 在 sessionService 中实现 Session Group 相关的请求逻辑:

class SessionService {
  // ... 省略 session 业务逻辑

  // ************************************** //
  // ***********  SessionGroup  *********** //
  // ************************************** //

  async createSessionGroup(name: string, sort?: number) {
    const item = await SessionGroupModel.create(name, sort);
    if (!item) {
      throw new Error('session group create Error');
    }

    return item.id;
  }

  // ... 其他 SessionGroup 相关的实现
}

三、Store Action 部分

在 LobeChat 应用中Store 是用于管理应用前端状态的模块。其中的 Action 是触发状态更新的函数,通常会调用服务层的方法来执行实际的数据处理操作,然后更新 Store 中的状态。我们采用了 zustand 作为 Store 模块的底层依赖,对于状态管理的详细实践介绍,可以查阅 📘 状态管理最佳实践

sessionGroup CRUD

会话组的 CRUD 操作是管理会话组数据的核心行为。在 src/store/session/slice/sessionGroup 中,我们将实现与会话组相关的状态逻辑,包括添加、删除、更新会话组及其排序。

以下是 action.ts 文件中需要实现的 SessionGroupAction 接口方法:

export interface SessionGroupAction {
  // 增加会话组
  addSessionGroup: (name: string) => Promise<string>;
  // 删除会话组
  removeSessionGroup: (id: string) => Promise<void>;
  // 更新会话的会话组 ID
  updateSessionGroupId: (sessionId: string, groupId: string) => Promise<void>;
  // 更新会话组名称
  updateSessionGroupName: (id: string, name: string) => Promise<void>;
  // 更新会话组排序
  updateSessionGroupSort: (items: SessionGroupItem[]) => Promise<void>;
}

addSessionGroup 方法为例,我们首先调用 sessionServicecreateSessionGroup 方法来创建新的会话组,然后使用 refreshSessions 方法来刷新 sessions 状态:

export const createSessionGroupSlice: StateCreator<
  SessionStore,
  [['zustand/devtools', never]],
  [],
  SessionGroupAction
> = (set, get) => ({
  // 实现添加会话组的逻辑
  addSessionGroup: async (name) => {
    // 调用服务层的 createSessionGroup 方法并传入会话组名称
    const id = await sessionService.createSessionGroup(name);
    // 调用 get 方法获取当前的 Store 状态并执行 refreshSessions 方法刷新会话数据
    await get().refreshSessions();
    // 返回新创建的会话组 ID
    return id;
  },
  // ... 其他 action 实现
});

通过以上的实现,我们可以确保在添加新的会话组后,应用的状态会及时更新,且相关的组件会收到最新的状态并重新渲染。这种方式提高了数据流的可预测性和可维护性,同时也简化了组件之间的通信。

Sessions 分组逻辑改造

本次需求改造需要对 Sessions 进行升级,从原来的单一列表变成了三个不同的分组:pinnedSessions(置顶列表)、customSessionGroups(自定义分组)和 defaultSessions(默认列表)。

为了处理这些分组,我们需要改造 useFetchSessions 的实现逻辑。以下是关键的改动点:

  1. 使用 sessionService.getSessionsWithGroup 方法负责调用后端接口来获取分组后的会话数据;
  2. 将获取后的数据保存为三到不同的状态字段中:pinnedSessionscustomSessionGroupsdefaultSessions

useFetchSessions 方法

该方法在 createSessionSlice 中定义,如下所示:

export const createSessionSlice: StateCreator<
  SessionStore,
  [['zustand/devtools', never]],
  [],
  SessionAction
> = (set, get) => ({
  // ... 其他方法
  useFetchSessions: () =>
    useSWR<ChatSessionList>(FETCH_SESSIONS_KEY, sessionService.getSessionsWithGroup, {
      onSuccess: (data) => {
        set(
          {
            customSessionGroups: data.customGroup,
            defaultSessions: data.default,
            isSessionsFirstFetchFinished: true,
            pinnedSessions: data.pinned,
            sessions: data.all,
          },
          false,
          n('useFetchSessions/onSuccess', data),
        );
      },
    }),
});

在成功获取数据后,我们使用 set 方法来更新 customSessionGroupsdefaultSessionspinnedSessionssessions 状态。这将保证状态与最新的会话数据同步。

getSessionsWithGroup

使用 sessionService.getSessionsWithGroup 方法负责调用后端接口 SessionModel.queryWithGroups()

class SessionService {
  // ... 其他 SessionGroup 相关的实现

  async getSessionsWithGroup(): Promise<ChatSessionList> {
    return SessionModel.queryWithGroups();
  }
}

SessionModel.queryWithGroups 方法

此方法是 sessionService.getSessionsWithGroup 调用的核心方法,它负责查询和组织会话数据,代码如下:

class _SessionModel extends BaseModel {
  // ... 其他方法

  /**
   * 查询会话数据,并根据会话组将会话分类。
   * @returns {Promise<ChatSessionList>} 返回一个对象,其中包含所有会话以及分为不同组的会话列表。
   */
  async queryWithGroups(): Promise<ChatSessionList> {
    // 查询会话组数据
    const groups = await SessionGroupModel.query();
    // 根据会话组ID查询自定义会话组
    const customGroups = await this.queryByGroupIds(groups.map((item) => item.id));
    // 查询默认会话列表
    const defaultItems = await this.querySessionsByGroupId(SessionDefaultGroup.Default);
    // 查询置顶的会话
    const pinnedItems = await this.getPinnedSessions();

    // 查询所有会话
    const all = await this.query();
    // 组合并返回所有会话及其分组信息
    return {
      all, // 包含所有会话的数组
      customGroup: groups.map((group) => ({ ...group, children: customGroups[group.id] })), // 自定义分组
      default: defaultItems, // 默认会话列表
      pinned: pinnedItems, // 置顶会话列表
    };
  }
}

方法 queryWithGroups 首先查询所有会话组,然后基于这些组的 ID 查询自定义会话组,同时查询默认和固定的会话。最后,它返回一个包含所有会话和按组分类的会话列表对象。

sessions selectors 调整

由于 sessions 中关于分组的逻辑发生了变化,因此我们需要调整 sessions 的 selectors 逻辑,以确保它们能够正确地处理新的数据结构。

原有的 selectors:

// 默认分组
const defaultSessions = (s: SessionStore): LobeSessions => s.sessions;

// 置顶分组
const pinnedSessionList = (s: SessionStore) =>
  defaultSessions(s).filter((s) => s.group === SessionGroupDefaultKeys.Pinned);

// 未置顶分组
const unpinnedSessionList = (s: SessionStore) =>
  defaultSessions(s).filter((s) => s.group === SessionGroupDefaultKeys.Default);

修改后:

const defaultSessions = (s: SessionStore): LobeSessions => s.defaultSessions;
const pinnedSessions = (s: SessionStore): LobeSessions => s.pinnedSessions;
const customSessionGroups = (s: SessionStore): CustomSessionGroup[] => s.customSessionGroups;

由于在 UI 中的取数全部是通过 useSessionStore(sessionSelectors.defaultSessions) 这样的写法实现的,因此我们只需要修改 defaultSessions 的选择器实现,即可完成数据结构的变更。 UI 层的取数代码完全不用变更,可以大大降低重构的成本和风险。

![Important]

如果你对 Selectors 的概念和功能不太了解,可以查阅 📘 数据存储取数模块 部分了解相关内容。

四、UI 部分

在 UI 组件中绑定 Store Action 实现交互逻辑,例如 CreateGroupModal

const CreateGroupModal = () => {
  // ... 其他逻辑

  const [updateSessionGroup, addCustomGroup] = useSessionStore((s) => [
    s.updateSessionGroupId,
    s.addSessionGroup,
  ]);

  return (
    <Modal
      onOk={async () => {
        // ... 其他逻辑
        const groupId = await addCustomGroup(name);
        await updateSessionGroup(sessionId, groupId);
      }}
    >
      {/* ... */}
    </Modal>
  );
};

五、数据迁移

在软件开发过程中,数据迁移是一个不可避免的问题,尤其是当现有的数据结构无法满足新的业务需求时。对于本次 SessionGroup 的迭代,我们需要处理 sessiongroup 字段的迁移,这是一个典型的数据迁移案例。

旧数据结构的问题

在旧的数据结构中,group 字段被用来标记会话是否为 pinned(置顶)或属于某个 default(默认)分组。但是当需要支持多个会话分组时,原有的数据结构就显得不够灵活了。

例如:

before   pin:  group = abc
after    pin:  group = pinned
after  unpin:  group = default

从上述示例中可以看出,一旦会话从置顶状态(pinned)取消置顶(unpingroup 字段将无法恢复为原来的 abc 值。这是因为我们没有一个独立的字段来维护置顶状态。因此,我们引入了一个新的字段 pinned 来表示会话是否被置顶,而 group 字段将仅用于标识会话分组。

迁移策略

本次迁移的核心逻辑只有一条:

  • 当用户的 group 字段为 pinned 时,将其 pinned 字段置为 true,同时将 group 设为 default;

但 LobeChat 中的数据迁移通常涉及到 配置文件迁移数据库迁移 两个部分。所以上述逻辑会需要分别在两块实现迁移。

配置文件迁移

对于配置文件迁移我们建议先于数据库迁移进行因为配置文件迁移通常更容易进行测试和验证。LobeChat 的文件迁移配置位于 src/migrations/index.ts 文件中,其中定义了配置文件迁移的各个版本及对应的迁移脚本。

// 当前最新的版本号
- export const CURRENT_CONFIG_VERSION = 2;
+ export const CURRENT_CONFIG_VERSION = 3;

// 历史记录版本升级模块
const ConfigMigrations = [
+ /**
+ * 2024.01.22
+  * from `group = pinned` to `pinned:true`
+  */
+ MigrationV2ToV3,
  /**
   * 2023.11.27
   * 从单 key 数据库转换为基于 dexie 的关系型结构
   */
  MigrationV1ToV2,
  /**
   * 2023.07.11
   * just the first version, Nothing to do
   */
  MigrationV0ToV1,
];

本次的配置文件迁移逻辑定义在 src/migrations/FromV2ToV3/index.ts 中,简化如下:

export class MigrationV2ToV3 implements Migration {
  // 指定从该版本开始向上升级
  version = 2;

  migrate(data: MigrationData<V2ConfigState>): MigrationData<V3ConfigState> {
    const { sessions } = data.state;

    return {
      ...data,
      state: {
        ...data.state,
        sessions: sessions.map((s) => this.migrateSession(s)),
      },
    };
  }

  migrateSession = (session: V2Session): V3Session => {
    return {
      ...session,
      group: 'default',
      pinned: session.group === 'pinned',
    };
  };
}

可以看到迁移的实现非常简单。但重要的是,我们需要保证迁移的正确性,因此需要编写对应的测试用例 src/migrations/FromV2ToV3/migrations.test.ts

import { MigrationData, VersionController } from '@/migrations/VersionController';

import { MigrationV1ToV2 } from '../FromV1ToV2';
import inputV1Data from '../FromV1ToV2/fixtures/input-v1-session.json';
import inputV2Data from './fixtures/input-v2-session.json';
import outputV3DataFromV1 from './fixtures/output-v3-from-v1.json';
import outputV3Data from './fixtures/output-v3.json';
import { MigrationV2ToV3 } from './index';

describe('MigrationV2ToV3', () => {
  let migrations;
  let versionController: VersionController<any>;

  beforeEach(() => {
    migrations = [MigrationV2ToV3];
    versionController = new VersionController(migrations, 3);
  });

  it('should migrate data correctly through multiple versions', () => {
    const data: MigrationData = inputV2Data;

    const migratedData = versionController.migrate(data);

    expect(migratedData.version).toEqual(outputV3Data.version);
    expect(migratedData.state.sessions).toEqual(outputV3Data.state.sessions);
    expect(migratedData.state.topics).toEqual(outputV3Data.state.topics);
    expect(migratedData.state.messages).toEqual(outputV3Data.state.messages);
  });

  it('should work correct from v1 to v3', () => {
    const data: MigrationData = inputV1Data;

    versionController = new VersionController([MigrationV2ToV3, MigrationV1ToV2], 3);

    const migratedData = versionController.migrate(data);

    expect(migratedData.version).toEqual(outputV3DataFromV1.version);
    expect(migratedData.state.sessions).toEqual(outputV3DataFromV1.state.sessions);
    expect(migratedData.state.topics).toEqual(outputV3DataFromV1.state.topics);
    expect(migratedData.state.messages).toEqual(outputV3DataFromV1.state.messages);
  });
});

单测需要使用 fixtures 来固定测试数据,测试用例包含了两个部分的验证逻辑: 1 单次迁移v2 -> v3和 2 完整迁移v1 -> v3的正确性。

[!Important]

配置文件的版本号可能与数据库版本号不一致,因为数据库版本的更新不总是伴随数据结构的变化(如新增表或字段),而配置文件的版本更新则通常涉及到数据迁移。

数据库迁移

数据库迁移则需要在 LocalDB 类中实施,该类定义在 src/database/core/db.ts 文件中。迁移过程涉及到为 sessions 表的每条记录添加新的 pinned 字段,并重置 group 字段:

export class LocalDB extends Dexie {
  public files: LobeDBTable<'files'>;
  public sessions: LobeDBTable<'sessions'>;
  public messages: LobeDBTable<'messages'>;
  public topics: LobeDBTable<'topics'>;
  public plugins: LobeDBTable<'plugins'>;
  public sessionGroups: LobeDBTable<'sessionGroups'>;

  constructor() {
    super(LOBE_CHAT_LOCAL_DB_NAME);
    this.version(1).stores(dbSchemaV1);
    this.version(2).stores(dbSchemaV2);
    this.version(3).stores(dbSchemaV3);
    this.version(4)
      .stores(dbSchemaV4)
+     .upgrade((trans) => this.upgradeToV4(trans));

    this.files = this.table('files');
    this.sessions = this.table('sessions');
    this.messages = this.table('messages');
    this.topics = this.table('topics');
    this.plugins = this.table('plugins');
    this.sessionGroups = this.table('sessionGroups');
  }

+  /**
+   * 2024.01.22
+   *
+   * DB V3 to V4
+   * from `group = pinned` to `pinned:true`
+   */
+  upgradeToV4 = async (trans: Transaction) => {
+    const sessions = trans.table('sessions');
+    await sessions.toCollection().modify((session) => {
+      // translate boolean to number
+      session.pinned = session.group === 'pinned' ? 1 : 0;
+      session.group = 'default';
+    });
+  };
}

以上就是我们的数据迁移策略。在进行迁移时,务必确保迁移脚本的正确性,并通过充分的测试验证迁移结果。

六、数据导入导出

在 LobeChat 中,数据导入导出功能是为了确保用户可以在不同设备之间迁移他们的数据。这包括会话、话题、消息和设置等数据。在本次的 Session Group 功能实现中,我们也需要对数据导入导出进行处理,以确保当完整导出的数据在其他设备上可以一模一样恢复。

数据导入导出的核心实现在 src/service/config.tsConfigService 中,其中的关键方法如下:

方法名称 描述
importConfigState 导入配置数据
exportAgents 导出所有助理数据
exportSessions 导出所有会话数据
exportSingleSession 导出单个会话数据
exportSingleAgent 导出单个助理数据
exportSettings 导出设置数据
exportAll 导出所有数据

数据导出

在 LobeChat 中,当用户选择导出数据时,会将当前的会话、话题、消息和设置等数据打包成一个 JSON 文件并提供给用户下载。这个 JSON 文件的标准结构如下:

{
  "exportType": "sessions",
  "state": {
    "sessions": [],
    "topics": [],
    "messages": []
  },
  "version": 3
}

其中:

  • exportType 标识导出数据的类型,目前有 sessionsagentsettingsall 四种;
  • state 存储实际的数据,不同 exportType 的数据类型也不同;
  • version 标识数据的版本。

在 Session Group 功能实现中,我们需要在 state 字段中添加 sessionGroups 数据。这样,当用户导出数据时,他们的 Session Group 数据也会被包含在内。

以导出 sessions 为例,导出数据的相关实现代码修改如下:

class ConfigService {
  // ... 省略其他

  exportSessions = async () => {
    const sessions = await sessionService.getSessions();
+   const sessionGroups = await sessionService.getSessionGroups();
    const messages = await messageService.getAllMessages();
    const topics = await topicService.getAllTopics();

-   const config = createConfigFile('sessions', { messages, sessions, topics });
+   const config = createConfigFile('sessions', { messages, sessionGroups, sessions, topics });

    exportConfigFile(config, 'sessions');
  };
}

数据导入

数据导入的功能是通过 ConfigService.importConfigState 来实现的。当用户选择导入数据时,他们需要提供一个由 符合上述结构规范的 JSON 文件。importConfigState 方法接受配置文件的数据,并将其导入到应用中。

在 Session Group 功能实现中,我们需要在导入数据的过程中处理 sessionGroups 数据。这样,当用户导入数据时,他们的 Session Group 数据也会被正确地导入。

以下是 importConfigState 中导入实现的变更代码:

class ConfigService {
  // ... 省略其他代码

+ importSessionGroups = async (sessionGroups: SessionGroupItem[]) => {
+   return sessionService.batchCreateSessionGroups(sessionGroups);
+ };

  importConfigState = async (config: ConfigFile): Promise<ImportResults | undefined> => {
    switch (config.exportType) {
      case 'settings': {
        await this.importSettings(config.state.settings);

        break;
      }

      case 'agents': {
+       const sessionGroups = await this.importSessionGroups(config.state.sessionGroups);

        const data = await this.importSessions(config.state.sessions);
        return {
+         sessionGroups: this.mapImportResult(sessionGroups),
          sessions: this.mapImportResult(data),
        };
      }

      case 'all': {
        await this.importSettings(config.state.settings);

+       const sessionGroups = await this.importSessionGroups(config.state.sessionGroups);

        const [sessions, messages, topics] = await Promise.all([
          this.importSessions(config.state.sessions),
          this.importMessages(config.state.messages),
          this.importTopics(config.state.topics),
        ]);

        return {
          messages: this.mapImportResult(messages),
+         sessionGroups: this.mapImportResult(sessionGroups),
          sessions: this.mapImportResult(sessions),
          topics: this.mapImportResult(topics),
        };
      }

      case 'sessions': {
+       const sessionGroups = await this.importSessionGroups(config.state.sessionGroups);

        const [sessions, messages, topics] = await Promise.all([
          this.importSessions(config.state.sessions),
          this.importMessages(config.state.messages),
          this.importTopics(config.state.topics),
        ]);

        return {
          messages: this.mapImportResult(messages),
+         sessionGroups: this.mapImportResult(sessionGroups),
          sessions: this.mapImportResult(sessions),
          topics: this.mapImportResult(topics),
        };
      }
    }
  };
}

上述修改的一个要点是先进行 sessionGroup 的导入,因为如果先导入 session 时,如果没有在当前数据库中查到相应的 SessionGroup Id那么这个 session 的 group 会兜底修改为默认值。这样就无法正确地将 sessionGroup 的 ID 与 session 进行关联。

以上就是 LobeChat Session Group 功能在数据导入导出部分的实现。通过这种方式,我们可以确保用户的 Session Group 数据在导入导出过程中能够被正确地处理。

总结

以上就是 LobeChat Session Group 功能的完整实现流程。开发者可以参考本文档进行相关功能的开发和测试。