RAG笔记(2)ragflow-web二开

记录一下 ragflow-web 二开的过程。

基础知识

  1. Umi 以路由为基础,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。
  2. 总结一下 Outlet 的主要特点:
    路由嵌套:
  • 允许在父路由组件中渲染子路由组件
  • 实现路由的层级结构
    布局复用:
  • 可以在多个页面之间共享相同的布局
  • 只需要改变 Outlet 位置的内容
    权限控制:
  • 可以在渲染子路由之前进行权限验证
  • 根据条件决定是否渲染子路由内容
    组件复用:
  • 避免重复编写相同的布局代码
  • 提高代码的可维护性
    这是 UmiJS 框架提供的一个非常实用的功能,特别适合构建具有复杂路由结构的应用程序。

前端架构

  1. 前端使用 react + umi 框架,经多次实验,已定版本 umi4.4.4;
  2. TypeScript 作为开发语言,Tailwind CSS 作为样式解决方案;
  3. 组件使用 Ant Design,可作定制化修改;
  4. ESLint 和 Prettier 用于代码规范,Jest 用于单元测试;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    ├── docs/                                # 文档,上级目录中 copy 过来,保证项目正常运行
    ├── dist/ # npm run build 之后打包的文件
    ├── public/ # 静态资源目录
    ├── package.json # 项目依赖和脚本配置
    ├── tsconfig.json # typeScript 配置文件
    ├── tailwind.config.js 和 tailwind.css # Tailwind CSS 配置和样式文件
    ├── .umirc.ts # UmiJS 框架的配置文件
    ├── .env # 运行时环境配置
    ├── typings.d.ts # 全局变量/模块声明
    ├── docs/ # 文档
    ├── src/
    │ ├── .umi/ # dev 时的临时文件目录,比如入口文件、路由等,都会被临时生成到这里
    │ ├── .umi-production/ # build 时的临时文件目录,比如入口文件、路由等,都会被临时生成到这里
    │ ├── pages/ # 页面组件目录
    │ │ ├── agent/ # Agent 流程图 UI(Canvas + DSL 构建)
    │ │ ├── chat/ # 聊天界面:配置、消息流、Prompt 管理
    │ │ ├── dataset/ # 数据集:文档上传、解析、标签设置
    │ │ ├── add-knowledge/ # 添加知识:文档→结构化→知识图谱
    │ │ ├── flow/ # Flow 模式 UI,与 Agent 机制相似
    │ └── layouts/ # 布局组件目录
    │ │ ├── index.tsx # umi会根据路由进行匹配,匹配到的组件都会放到Outlet中渲染
    │ ├── components/ # 通用组件库:表单、Modal、预览器等
    │ ├── hooks/ # 与后端服务配合的数据处理逻辑封装
    │ └── utils/ # 工具函数
    │ └── assets/ # 静态资源文件,img
    │ └── locales/ # 国际化文件,当前只保留中文
    │ └── theme/ # 主题相关文件
    │ └── wrapper/ # 权限包裹组件(临时注释可避开验证)
    │ └── less/ # 全局样式文件
    │ └── app.tsx # 应用入口文件
    │ └── routes.ts # 路由配置文件

前端代码修改思路

框架修改

  1. .umirc.ts作为基础框架,可设置系统配置、全局路由、接口代理、插件。
  2. 最终路由导入到 umi 配置文件中。若需要修改,在routers.ts 定义路由,pages 文件夹下新建页面组件;(umi支持约定式路由和配置式路由,这里选择配置式)
  3. 若需要修改后端接口代理,添加 proxy
  4. 若需要在入口文件index.html (运行时umi自动生成)中添加静态文件,可在 public 中引入,然后在 headScripts 中添加文件名;
  5. 配置修改:
  6. Logo: 直接修改 public/logo.svg
  7. 标题:conf.json 中修改
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    import path from 'path';
    import TerserPlugin from 'terser-webpack-plugin';
    import { defineConfig } from 'umi';
    import { appName } from './src/conf.json';
    import routes from './src/routes';

    export default defineConfig({
    title: appName,
    outputPath: 'dist',
    alias: { '@parent': path.resolve(__dirname, './') },
    npmClient: 'npm',
    base: '/',
    routes,
    publicPath: '/',
    esbuildMinifyIIFE: true,
    icons: {},
    hash: true,
    favicons: ['/logo.svg'],
    clickToComponent: {},
    history: {
    type: 'browser',
    },
    headScripts: [
    { src: '/config.js' }
    ],
    plugins: [
    '@react-dev-inspector/umi4-plugin',
    '@umijs/plugins/dist/tailwindcss',
    ],
    jsMinifier: 'none',
    lessLoader: {
    modifyVars: {
    hack: `true; @import "~@/less/index.less";`,
    },
    },
    devtool: 'source-map',
    copy: [
    { from: 'src/conf.json', to: 'dist/conf.json' },
    { from: 'node_modules/monaco-editor/min/vs/', to: 'dist/vs/' },
    ],
    proxy: [
    {
    context: ['/api', '/v1'],
    target: 'http://127.0.0.1:9380/',
    changeOrigin: true,
    ws: true,
    logger: console,
    // pathRewrite: { '^/v1': '/v1' },
    },
    ],

    chainWebpack(memo, args) {
    memo.module.rule('markdown').test(/\.md$/).type('asset/source');
    memo.optimization.minimizer('terser').use(TerserPlugin); // Fixed the issue that the page displayed an error after packaging lexical with terser
    return memo;
    },
    tailwindcss: {},
    });

样式修改

  1. 若需要改变前端布局,修改 layouts 文件夹下的内容;header为头部导航,right-toolbar 暂时不用;
  2. app.tsx 中可修改 ant-design 主题相关或 token 级别组件相关。字体使用 inter,在assets/inter 中可见倒入的字体。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <ConfigProvider
    theme={{
    token: {
    fontFamily: 'Inter',
    colorPrimary: '#165DFF',
    },
    }}
    locale={locale}
    >
    <SidebarProvider>
  3. src/theme 中进行系统相关的样式变量配置。

接口修改

  1. 本系统所有接口都在utils/api.ts中集合,interface/request 中定义接口参数的类型,services 中进行 url、method 封装(blob 相关也在这里配置),最后在 hooks 中作为方法调用。
  2. 若使用之前定义的格式,则需要避开装好的接口调用方式,因为原来的接口设计的不太好,请求顺利但是业务逻辑错误会返回400,在这个系统就获取不到返回的数据,所以只能自己hack。这里测试下来可以直接在 hooks 中使用 fetch 来获取完整的接口输出。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    export const useLogin = () => {
    const { t } = useTranslation();

    const {
    data,
    isPending: loading,
    mutateAsync,
    } = useMutation({
    mutationKey: ['login'],
    mutationFn: async (params: {
    account: string;
    hashed_password: string;
    }) => {
    const response = await fetch(
    'accountApi/v1/ai-platform-control-center/sign-in-password',
    {
    method: 'POST',
    headers: {
    'Content-Type': 'application/json',
    },
    body: JSON.stringify(params),
    },
    );

    const res = await response.json();

    if (res?.jw_token) {
    authorizationUtil.setItems({
    Token: res?.jw_token,
    Authorization: res?.jw_token,
    });
    return res;
    } else if (res?.trial_times) {
    if (res?.trial_times !== 0) {
    message.error(
    '密码错误,您还有' + res?.trial_times + '次尝试的机会。',
    );
    }
    }
    return false;
    },
    });

    return { data, loading, login: mutateAsync };
    };

权限

系统设置

  1. 在左侧菜单渲染层面,通过用户名=admin进行比对,只有admin时才可以渲染出系统设置菜单。
  2. 可能需要 access 来设定直接输入 URL 层面的跳转,目前查阅资源结论是升级到 @umi/max 比较好,但是考虑到后期还需要移植新 feature,直接改框架成本较高,暂时搁置。

模型设置跳转

  1. Ragflow 原有逻辑:
  2. 个人账号未设置模型时,创建知识库、点击知识库之后会跳转;
  3. 个人账号未设置模型时,创建 Agent、点击 Agent之后会跳转;
  4. 用了navigate(/***)。猜测是到了路由为 /knowledge/dataset 页面之后进行的判断,该页面 pages/add-knowledge/knowledge-dataset 由 knowledge-file 和 knowledge-chunk 两个组件组成。
  5. 具体是在弹窗的ok之后跳转的,跳转逻辑在 hooks/user-setting-hooks 的 useFetchTenantInfo 方法中,useFetchTenantInfo(true) 会跳转 /user-setting/model,当前采用将跳转直接注释,若后续有问题需要整体查看一下。

web 端交互

搜索模块

  1. hooks/logic-hooks.ts中,send 方法请求 /v1/conversation/ask,目前 response 中的 data 为具体内容,若引入深度思考,需要后端对回答进行参数明晰。(大概在215行)。大模型回答可搜索 answer 关键字。
  2. response 中参数如下:
  3. doc_aggs 为当前切片数组,用分页实现,第一次会返回12条数据,不明原因(看页面展示就用到了10条数据)。但是同时也调用了 /v1/chunk/retrieval_test 返回10条数据,应该用的是这里的数据。
  4. 后面点击分页之后,请求 /v1/chunk/retrieval_test 都是返回当前页数的10条数据。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    answer: 'sse不断新增的完整回答',
    reference: {
    chunks: [], // 切片
    doc_aggs: [
    {
    doc_name: '文件名',
    doc_id: '' // 根据id获取文件图片-一连串查询接口
    }
    ],
    total: 29
    }
    }

    问答助手模块

  5. pages/chat/chat-container/index.tsx 为聊天内容代码,pages/chat/markdown-content/index.tsx 为回答展示。
    /v1/conversation/completion 为大模型回答,answer 参数为回答的具体内容,其中包裹的内容为深度思考,在 markdown 里面是按照html直接渲染的,样式已做了修改。
  6. components/message-item/index.tsx 为单条信息的代码,可设置 showLoudspeaker 为 false 关闭语音播放按钮。具体按钮组代码在 components/message-item/group-button 中。
    [图片]
  7. components/message-item/index.tsx 中用户回答可携带上传成功的文件;助理回答可携带知识库参考来源;MarkdownContent 为回答内容;
  8. components/message-input/index.tsx 为输入框代码;用户对输入框的操作在 pages/chat/hooks.ts 中;发送代码在 src/hooks/logic-hooks.ts 的 useSendMessageWithSse

前后端可能遇到的问题

  1. 登录时原始返回 authorization、userInfo 和 token,authorization 应该是网关相关,后面的接口都会用到,若用户无法认证,系统会自动跳转登录页面;和统一登录有所差别,开发时需特殊处理;

附录
ragflow
ragflow-plus
umi
深入理解 UMI 框架
qiankun+umi+antd
自定义umi微前端
HTML to JSX
定制主题


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!