update doc search engine (#5386)

* update doc search engine

* custom tokenizer

* tokenizer
This commit is contained in:
Archer
2025-08-04 22:07:52 +08:00
committed by GitHub
parent 545d8150f2
commit 6a0b0b1991
25 changed files with 432 additions and 324 deletions

View File

@@ -1,4 +1 @@
NEXT_PUBLIC_SEARCH_APPKEY=
SEARCH_APPWRITEKEY=
NEXT_PUBLIC_SEARCH_APPID=
FASTGPT_HOME_DOMAIN=

View File

@@ -19,26 +19,14 @@ RUN apk add --no-cache \
fontconfig
WORKDIR /app
ARG NEXT_PUBLIC_SEARCH_APPKEY
ARG NEXT_PUBLIC_SEARCH_APPID
ARG SEARCH_APPWRITEKEY
ARG FASTGPT_HOME_DOMAIN
ENV NEXT_PUBLIC_SEARCH_APPKEY=$NEXT_PUBLIC_SEARCH_APPKEY
ENV NEXT_PUBLIC_SEARCH_APPID=$NEXT_PUBLIC_SEARCH_APPID
ENV SEARCH_APPWRITEKEY=$SEARCH_APPWRITEKEY
ENV FASTGPT_HOME_DOMAIN=$FASTGPT_HOME_DOMAIN
COPY . .
RUN npm install
RUN npm run build
# Update search index if SEARCH_APPWRITEKEY is provided
RUN if [ -n "$SEARCH_APPWRITEKEY" ]; then \
echo "SEARCH_APPWRITEKEY found, updating search index..." && \
(npm run update-index-action || echo "Search index update failed, but continuing..."); \
else \
echo "SEARCH_APPWRITEKEY not provided, skipping search index update"; \
fi
FROM base AS runner
RUN apk add --no-cache curl

View File

@@ -1,29 +1,11 @@
# FastGPT 文档
这是FastGPT的官方文档采用fumadoc框架。
## 配置文档搜索
点击[Algolia](https://dashboard.algolia.com/account/overview),进行注册账号,注册成功后需要点击页面的搜索,然后查看应用,默认会有一个应用。
![](./public/readme/algolia.png)
拥有应用后点击个人头像,点击设置,点击`API Keys`查看自己的应用id和key。
![](./public/readme/algolia2.png)
页面中的`Application ID``Search API Key``Write API KEY`就是环境变量对应的`NEXT_PUBLIC_SEARCH_APPID``NEXT_PUBLIC_SEARCH_APPKEY``SEARCH_APPWRITEKEY`
![](./public/readme/algolia3.png)
## 运行项目
要运行文档,首先需要进行环境变量配置,在文档的根目录下创建`.env.local`文件,填写以下环境变量:
```bash
SEARCH_APPWRITEKEY = #这是上面获取的Write api key
NEXT_PUBLIC_SEARCH_APPKEY = #这是上面获取的搜索key
NEXT_PUBLIC_SEARCH_APPID = #这是上面的搜索id
FASTGPT_HOME_DOMAIN = #要跳转的FastGPT项目的域名默认海外版
```

View File

@@ -1,7 +1,21 @@
import { source } from '@/lib/source';
import { enhancedTokenizer } from '@/lib/tokenizer';
import { createFromSource } from 'fumadocs-core/search/server';
export const { GET } = createFromSource(source, {
// https://docs.orama.com/open-source/supported-languages
language: 'english'
// 使用中文分词器时不能设置 language 选项
localeMap: {
en: {
language: 'english'
},
'zh-CN': {
components: {
tokenizer: enhancedTokenizer()
},
search: {
threshold: 0,
tolerance: 0
}
}
}
});

View File

@@ -1,43 +0,0 @@
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
// ✅ 设置要处理的根目录(可修改为你的文档目录)
const rootDir = path.resolve(__dirname, 'content/docs');
// ✅ 仅保留的 frontmatter 字段
const KEEP_FIELDS = ['title', 'description'];
function cleanFrontmatter(filePath) {
const raw = fs.readFileSync(filePath, 'utf-8');
const parsed = matter(raw);
// 仅保留需要的字段
const newData = {};
for (const key of KEEP_FIELDS) {
if (parsed.data[key] !== undefined) {
newData[key] = parsed.data[key];
}
}
const cleaned = matter.stringify(parsed.content, newData);
fs.writeFileSync(filePath, cleaned, 'utf-8');
console.log(`✔ Cleaned: ${path.relative(rootDir, filePath)}`);
}
function walk(dir) {
const entries = fs.readdirSync(dir);
for (const entry of entries) {
const fullPath = path.join(dir, entry);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
walk(fullPath); // 🔁 递归子目录
} else if (entry.endsWith('.mdx')) {
cleanFrontmatter(fullPath);
}
}
}
// 🚀 开始执行
walk(rootDir);

View File

@@ -1,6 +1,5 @@
'use client';
// components/CustomSearchDialog.tsx
import { liteClient } from 'algoliasearch/lite';
import { useDocsSearch } from 'fumadocs-core/search/client';
import {
SearchDialog,
@@ -15,21 +14,11 @@ import {
} from 'fumadocs-ui/components/dialog/search';
import { useI18n } from 'fumadocs-ui/contexts/i18n';
if (!process.env.NEXT_PUBLIC_SEARCH_APPID || !process.env.NEXT_PUBLIC_SEARCH_APPKEY) {
throw new Error('NEXT_PUBLIC_SEARCH_APPID and NEXT_PUBLIC_SEARCH_APPKEY are not set');
}
const client = liteClient(
process.env.NEXT_PUBLIC_SEARCH_APPID,
process.env.NEXT_PUBLIC_SEARCH_APPKEY
);
export default function CustomSearchDialog(props: SharedProps) {
const { locale } = useI18n();
const { search, setSearch, query } = useDocsSearch({
type: 'algolia',
client,
indexName: 'document',
type: 'fetch',
api: '/api/search',
locale
});

View File

@@ -8,4 +8,4 @@ title: 其他问题
## 想做多用户
开源版未支持多用户,仅商业版支持。
社区版未支持多用户,仅商业版支持。

View File

@@ -7,11 +7,11 @@ import { Alert } from '@/components/docs/Alert';
## 简介
FastGPT 商业版是基于 FastGPT 开源版的增强版本,增加了一些独有的功能。只需安装一个商业版镜像,并在开源版基础上填写对应的内网地址,即可快速使用商业版。
FastGPT 商业版是基于 FastGPT 社区版的增强版本,增加了一些独有的功能。只需安装一个商业版镜像,并在社区版基础上填写对应的内网地址,即可快速使用商业版。
## 功能差异
| | 开源版 | 商业版 | Saas 版 |
| | 社区版 | 商业版 | Saas 版 |
| ------------------------------ | ------------------------------------------ | ------ | ------- |
| **应用构建** | | | |
| 工作流编排 | ✅ | ✅ | ✅ |
@@ -89,13 +89,13 @@ FastGPT 商业版软件根据不同的部署方式,分为 3 类收费模式。
### 如何交付?
完整版应用 = 开源版镜像 + 商业版镜像
完整版应用 = 社区版镜像 + 商业版镜像
我们会提供一个商业版镜像给你使用,该镜像需要一个 License 启动。
### 二次开发如何操作?
可以修改开源版部分代码,不支持修改商业版镜像。完整版本=开源版+商业版镜像,所以是可以修改部分内容的。但是如果二开了,后续则需要自己进行代码合并升级。
可以修改社区版部分代码,不支持修改商业版镜像。完整版本=社区版+商业版镜像,所以是可以修改部分内容的。但是如果二开了,后续则需要自己进行代码合并升级。
### Sealos 运行费用

View File

@@ -5,12 +5,14 @@ description: FastGPT 配置参数介绍
由于环境变量不利于配置复杂的内容,新版 FastGPT 采用了 ConfigMap 的形式挂载配置文件,你可以在 `projects/app/data/config.json` 看到默认的配置文件。可以参考 [docker-compose 快速部署](/docs/development/docker/) 来挂载配置文件。
**开发环境下**,你需要将示例配置文件 `config.json` 复制成 `config.local.json` 文件才会生效。
**开发环境下**,你需要将示例配置文件 `config.json` 复制成 `config.local.json` 文件才会生效。
下面配置文件示例中包含了系统参数和各个模型配置:
## 4.8.20+ 版本新配置文件示例
> 从4.8.20版本开始,模型在页面中进行配置。
```json
{
"feConfigs": {
@@ -22,7 +24,8 @@ description: FastGPT 配置参数介绍
"vlmMaxProcess": 15, // 图片理解模型最大处理进程
"tokenWorkers": 50, // Token 计算线程保持数,会持续占用内存,不能设置太大。
"hnswEfSearch": 100, // 向量搜索参数,仅对 PG 和 OB 生效。越大搜索越精确但是速度越慢。设置为100有99%+精度。
"customPdfParse": { // 4.9.0 新增配置
"customPdfParse": {
// 4.9.0 新增配置
"url": "", // 自定义 PDF 解析服务地址
"key": "", // 自定义 PDF 解析服务密钥
"doc2xKey": "", // doc2x 服务密钥
@@ -57,7 +60,7 @@ description: FastGPT 配置参数介绍
#### 2. 修改 FastGPT 配置文件
开源版用户在 `config.json` 文件中添加 `systemEnv.customPdfParse.doc2xKey` 配置,并填写上申请到的 API Key。并重启服务。
社区版用户在 `config.json` 文件中添加 `systemEnv.customPdfParse.doc2xKey` 配置,并填写上申请到的 API Key。并重启服务。
商业版用户在 Admin 后台根据表单指引填写 Doc2x 服务密钥。

View File

@@ -9,7 +9,7 @@ PDF 是一个相对复杂的文件格式,在 FastGPT 内置的 pdf 解析器
市面上目前有多种解析 PDF 的方法,比如使用 [Marker](https://github.com/VikParuchuri/marker),该项目使用了 Surya 模型,基于视觉解析,可以有效提取图片、表格、公式等复杂内容。
在 `FastGPT v4.9.0` 版本中,开源版用户可以在`config.json`文件中添加`systemEnv.customPdfParse`配置,来使用 Marker 解析 PDF 文件。商业版用户直接在 Admin 后台根据表单指引填写即可。需重新拉取 Marker 镜像,接口格式已变动。
在 `FastGPT v4.9.0` 版本中,社区版用户可以在`config.json`文件中添加`systemEnv.customPdfParse`配置,来使用 Marker 解析 PDF 文件。商业版用户直接在 Admin 后台根据表单指引填写即可。需重新拉取 Marker 镜像,接口格式已变动。
## 使用教程
@@ -23,6 +23,7 @@ PDF 是一个相对复杂的文件格式,在 FastGPT 内置的 pdf 解析器
docker pull crpi-h3snc261q1dosroc.cn-hangzhou.personal.cr.aliyuncs.com/marker11/marker_images:v0.2
docker run --gpus all -itd -p 7231:7232 --name model_pdf_v2 -e PROCESSES_PER_GPU="2" crpi-h3snc261q1dosroc.cn-hangzhou.personal.cr.aliyuncs.com/marker11/marker_images:v0.2
```
### 2. 添加 FastGPT 文件配置
```json
@@ -52,7 +53,7 @@ docker run --gpus all -itd -p 7231:7232 --name model_pdf_v2 -e PROCESSES_PER_GPU
```
[Info] 2024-12-05 15:04:42 Parsing files from an external service
[Info] 2024-12-05 15:07:08 Custom file parsing is complete, time: 1316ms
[Info] 2024-12-05 15:07:08 Custom file parsing is complete, time: 1316ms
```
然后你就可以发现,通过 Marker 解析出来的 pdf 会携带图片链接:
@@ -63,14 +64,13 @@ docker run --gpus all -itd -p 7231:7232 --name model_pdf_v2 -e PROCESSES_PER_GPU
![alt text](/imgs/marker3.png)
## 效果展示
以清华的 [ChatDev Communicative Agents for Software Develop.pdf](https://arxiv.org/abs/2307.07924) 为例,展示 Marker 解析的效果:
| | | |
| --- | --- | --- |
| ![alt text](/imgs/image-11.png) | ![alt text](/imgs/image-12.png) | ![alt text](/imgs/image-13.png) |
| | | |
| ------------------------------- | ------------------------------- | ------------------------------- |
| ![alt text](/imgs/image-11.png) | ![alt text](/imgs/image-12.png) | ![alt text](/imgs/image-13.png) |
| ![alt text](/imgs/image-14.png) | ![alt text](/imgs/image-15.png) | ![alt text](/imgs/image-16.png) |
上图是分块后的结果,下图是 pdf 原文。整体图片、公式、表格都可以提取出来,效果还是杠杠的。
@@ -95,5 +95,5 @@ CUSTOM_READ_FILE_URL=http://xxxx.com/v1/parse/file
CUSTOM_READ_FILE_EXTENSION=pdf
```
* CUSTOM_READ_FILE_URL - 自定义解析服务的地址, host改成解析服务的访问地址path 不能变动。
* CUSTOM_READ_FILE_EXTENSION - 支持的文件后缀,多个文件类型,可用逗号隔开。
- CUSTOM_READ_FILE_URL - 自定义解析服务的地址, host改成解析服务的访问地址path 不能变动。
- CUSTOM_READ_FILE_EXTENSION - 支持的文件后缀,多个文件类型,可用逗号隔开。

View File

@@ -16,11 +16,12 @@ curl --location --request POST 'https://{{host}}/api/admin/initv462' \
```
初始化说明:
1. 初始化全文索引
## V4.6.2 功能介绍
1. 新增 - 全文索引(需配合 Rerank 模型,在看怎么放到开源版,模型接口比较特殊)
1. 新增 - 全文索引(需配合 Rerank 模型,在看怎么放到社区版,模型接口比较特殊)
2. 新增 - 插件来源预计4.7/4.8版本会正式使用)
3. 优化 - PDF读取
4. 优化 - docx文件读取转成 markdown 并保留其图片内容

View File

@@ -18,6 +18,7 @@ curl --location --request POST 'https://{{host}}/api/admin/initv47' \
```
脚本功能:
1. 初始化插件的 parentId
## 3. 升级 ReRank 模型
@@ -31,18 +32,17 @@ cohere的重排模型对中文不是很好感觉不如 bge 的好用,接入
```json
{
"reRankModels": [
{
"model": "rerank-multilingual-v2.0", // 这里的 model 需要对应 cohere 的模型名
"name": "检索重排", // 随意
"requestUrl": "https://api.cohere.ai/v1/rerank",
"requestAuth": "Coherer上申请的key"
}
]
"reRankModels": [
{
"model": "rerank-multilingual-v2.0", // 这里的 model 需要对应 cohere 的模型名
"name": "检索重排", // 随意
"requestUrl": "https://api.cohere.ai/v1/rerank",
"requestAuth": "Coherer上申请的key"
}
]
}
```
## V4.7 更新说明
1. 新增 - 工具调用模块可以让LLM模型根据用户意图动态的选择其他模型或插件执行。
@@ -57,7 +57,7 @@ cohere的重排模型对中文不是很好感觉不如 bge 的好用,接入
10. 优化 - 变量输入弹窗。
11. 优化 - docker 部署,自动初始化副本集。
12. 优化 - 浏览器读取文件自动推断编码,减少乱码情况。
13. 修复 - 开源版重排选不上。
13. 修复 - 社区版重排选不上。
14. 修复 - http 请求 body不使用时传入undefined。会造成部分GET请求失败
15. 新增 - 支持 http url 使用变量。
16. 修复 - 469 的提取的提示词容易造成幻觉。

View File

@@ -23,7 +23,7 @@ description: FastGPT V4.9.10 更新说明
2. 知识库预处理参数增加 “分块条件”,可控制某些情况下不进行分块处理。
3. 知识库预处理参数增加 “段落优先” 模式,可控制最大段落深度。原“长度优先”模式,不再内嵌段落优先逻辑。
4. 工作流调整为单向接入和接出,支持快速的添加下一步节点。
5. 开放飞书和语雀知识库到开源版。
5. 开放飞书和语雀知识库到社区版。
6. gemini 和 claude 最新模型预设。
## ⚙️ 优化

View File

@@ -29,6 +29,7 @@ description: FastGPT V4.9.6 更新说明
3. 连续工具调用,上下文截断异常
## 升级指南
### 1. 做好数据备份
### 2. 部署 MCP server 服务
@@ -39,15 +40,15 @@ description: FastGPT V4.9.6 更新说明
```yml
fastgpt-mcp-server:
container_name: fastgpt-mcp-server
image: ghcr.io/labring/fastgpt-mcp_server:v4.9.6
ports:
- 3005:3000
networks:
- fastgpt
restart: always
environment:
- FASTGPT_ENDPOINT=http://fastgpt:3000
container_name: fastgpt-mcp-server
image: ghcr.io/labring/fastgpt-mcp_server:v4.9.6
ports:
- 3005:3000
networks:
- fastgpt
restart: always
environment:
- FASTGPT_ENDPOINT=http://fastgpt:3000
```
#### Sealos 部署
@@ -56,14 +57,15 @@ fastgpt-mcp-server:
### 3. 修改 FastGPT 容器环境变量
#### 开源
#### 社区
修改`config.json`配置文件,增加: `"feconfigs.mcpServerProxyEndpoint": "fastgpt-mcp-server 的访问地址"` 末尾不要携带/,例如:
```json
{
"feConfigs": {
"lafEnv": "https://laf.dev",
"mcpServerProxyEndpoint": "https://mcp.fastgpt.cn"
"mcpServerProxyEndpoint": "https://mcp.fastgpt.cn"
}
}
```

338
document/lib/tokenizer.ts Normal file
View File

@@ -0,0 +1,338 @@
import { createTokenizer } from '@orama/tokenizers/mandarin';
export const enhancedTokenizer = () => {
// 整词配置 - 需要保持完整的词汇
const wholeWords = [
// 产品相关
'fastgpt',
'FastGPT',
'快速GPT',
'Saas',
'SaaS',
'云服务',
'社区版',
'商业版',
'开源版',
'企业版',
'专业版',
'智能对话系统',
'AI应用平台',
'知识库问答系统',
// 核心功能模块
'知识库',
'工作流',
'应用构建',
'对话管理',
'可视化编排',
'拖拽式设计',
'零代码搭建',
'低代码开发',
'流程编排',
// 工作流节点
'AI对话',
'知识库搜索',
'问题分类',
'内容提取',
'用户选择',
'表单输入',
'文本编辑',
'指定回复',
'文档解析',
'HTTP请求',
'真假判断',
'变量更新',
'代码运行',
'循环',
'指代消解',
'自定义反馈',
'工具调用',
// AI模型相关
'AI',
'Agent',
'LLM',
'大语言模型',
'ChatGPT',
'GPT-4',
'GPT-3.5',
'Claude',
'文心一言',
'通义千问',
'DeepSeek',
'Function Call',
'Prompt',
'提示词',
'系统提示词',
'Temperature',
'温度参数',
'Token',
'OpenAI',
'Anthropic',
'百度',
'阿里云',
'智谱AI',
'One-API',
'AI-Proxy',
'Ollama',
'Xinference',
// 技术术语
'API',
'OpenAPI',
'SSO',
'MCP',
'向量数据库',
'语义搜索',
'embedding',
'RAG',
'检索增强生成',
'PGVector',
'Milvus',
'MongoDB',
'PostgreSQL',
'Redis',
'Docker',
'Sealos',
'Nginx',
'Cloudflare',
// 数据处理
'数据集',
'Dataset',
'文档导入',
'数据处理',
'索引模型',
'重排模型',
'分块策略',
'增强处理',
'问答拆分',
'向量化',
'文本分割',
'知识结构化',
'自动索引',
'图片标注',
'OCR识别',
// 集成相关
'Webhook',
'第三方集成',
'企业微信',
'钉钉',
'DingTalk',
'飞书',
'Feishu',
'Lark',
'微信公众号',
// 业务概念
'沙盒',
'Sandbox',
'插件',
'Plugin',
'模板',
'权限管理',
'团队协作',
'用户角色',
'访问控制',
'数据安全',
'隐私保护',
// 界面元素
'工作台',
'Dashboard',
'调试预览',
'发布分享',
'对话窗口',
'聊天界面',
'引用展示',
'猜你想问',
'文件上传',
'表单配置',
'系统配置',
'模型配置',
'参数设置',
'高级编排',
'简易模式',
'专业模式',
// 搜索插件
'Bing搜索',
'Google搜索',
'SearXNG搜索',
'Doc2x插件',
// 文件格式
'PDF',
'Word',
'Excel',
'CSV',
'TXT',
'Markdown',
'JSON',
// 部署运维
'环境变量',
'config.json',
'配置文件',
'负载均衡',
'高可用',
'容灾备份',
'监控告警',
'性能优化',
'响应速度',
'并发处理',
'缓存机制',
// 多语言
'i18n',
'国际化',
'中文',
'英文',
'日文',
'多语言支持',
// 版本管理
'版本更新',
'升级指南',
'更新日志',
'兼容性',
'迁移指南'
];
// 同义词配置 - 为整词添加相关的同义词
const synonymsMap: Record<string, string[]> = {
// 产品版本相关
: ['社区版', '开源', '开源版', '免费版'],
: ['商业版', '付费版', '企业版', '专业版', '标准版', '高级版'],
: ['社区版', '开源版', '免费版'],
: ['企业版', '商业版', '付费版', '专业版'],
// 核心功能
: ['知识库', '知识体系', '数据库', '文档库', '资料库', '信息库', '语料库'],
: ['工作流', '流程', '工作流程', '业务流程', '可视化流程', '流程图', '编排', '组合'],
: ['应用构建', '应用搭建', '应用创建', '搭建', '构建', '创建应用'],
: ['对话管理', '聊天管理', '会话管理', '对话控制'],
// AI相关
AI: ['AI', '人工智能', '机器学习', '深度学习', '智能', '智能化'],
: ['大语言模型', 'LLM', '语言模型', '大模型', '生成式AI', '对话模型'],
Agent: ['Agent', '智能体', '代理', '智能助手', 'AI助理', '机器人'],
: ['提示词', 'Prompt', '指令', '命令', '提示', '指示'],
// 技术概念
API: ['API', '接口', '接口调用', '应用程序接口', '集成接口', '外部接口'],
: ['向量数据库', '向量存储', '向量索引', 'Vector Database'],
: [
'语义搜索',
'语义检索',
'智能检索',
'相似度搜索',
'向量搜索',
'检索系统',
'搜索引擎'
],
RAG: ['RAG', '检索增强生成', '检索增强', '知识检索'],
// 数据处理
: ['数据集', 'Dataset', '数据源', '数据', '资料'],
: ['文档导入', '文件导入', '数据导入', '批量导入', '上传文档'],
: ['数据处理', '文档处理', '内容处理', '文件解析', '信息提取', '结构化处理'],
: ['向量化', 'embedding', '向量转换', '特征提取'],
// 界面相关
: ['工作台', 'Dashboard', '控制台', '管理台', '操作台', '仪表板'],
: ['调试预览', '预览', '调试', '测试', '试运行'],
: ['发布分享', '发布', '分享', '部署', '上线'],
// 插件相关
: ['插件', 'Plugin', '扩展', '组件', '模块', '附加功能', '外部工具', '第三方工具'],
: ['搜索插件', '搜索工具', '搜索扩展', '网络搜索'],
// 集成相关
: ['第三方集成', '外部集成', '平台集成', '系统对接', '接口集成'],
: ['企业微信', '企微', 'WeWork'],
: ['钉钉', 'DingTalk', '阿里钉钉'],
: ['飞书', 'Feishu', 'Lark', '字节飞书'],
// 权限管理
: ['权限管理', '访问控制', '用户权限', '权限控制', '授权管理'],
: ['团队协作', '多人协作', '协同工作', '团队管理'],
: ['用户角色', '角色管理', '权限角色', '用户权限'],
// 安全相关
: ['数据安全', '信息安全', '数据保护', '安全防护'],
: ['隐私保护', '数据隐私', '信息保护', '隐私安全'],
访: ['访问控制', '权限控制', '访问权限', '安全认证'],
// 部署相关
Docker: ['Docker', '容器', '容器化', '镜像部署'],
: ['环境变量', '配置变量', '系统变量', '环境配置'],
: ['配置文件', '配置', '设置文件', 'config'],
// 性能相关
: ['性能优化', '系统优化', '速度提升', '效率优化', '性能调优'],
: ['响应速度', '响应时间', '处理速度', '执行速度'],
: ['并发处理', '并发', '多线程', '高并发'],
: ['缓存机制', '缓存', '内存缓存', '数据缓存'],
// 多语言
: ['国际化', 'i18n', '多语言', '本地化'],
: ['多语言支持', '多语言', '国际化支持', '语言切换'],
// 版本管理
: ['版本更新', '系统更新', '功能更新', '补丁更新', '版本升级'],
: ['升级指南', '更新指南', '迁移指南', '升级说明'],
: ['更新日志', '版本日志', '修改日志', '发布日志'],
// 工作流节点
AI对话: ['AI对话', 'AI聊天', '智能对话', '机器人对话'],
: ['知识库搜索', '文档搜索', '资料搜索', '内容搜索'],
: ['问题分类', '意图识别', '分类器', '问题识别'],
: ['内容提取', '信息提取', '文本提取', '数据提取'],
: ['用户选择', '选择器', '用户输入', '交互选择'],
: ['表单输入', '用户输入', '数据输入', '信息收集'],
: ['文档解析', '文件解析', '内容解析', '格式转换'],
: ['代码运行', '代码执行', '脚本执行', '沙盒执行'],
: ['工具调用', '函数调用', 'Function Call', 'API调用']
};
const baseTokenizer = createTokenizer();
return {
...baseTokenizer,
tokenize: (text: string) => {
// 先处理整词,用特殊标记保护它们
let processedText = text;
const protectedTokens = new Map();
let tokenIndex = 0;
wholeWords.forEach((word) => {
const regex = new RegExp(`\\b${word}\\b`, 'gi');
processedText = processedText.replace(regex, (match) => {
const placeholder = `__PROTECTED_TOKEN_${tokenIndex}__`;
protectedTokens.set(placeholder, match.toLowerCase());
tokenIndex++;
return placeholder;
});
});
// 用基础分词器处理
let tokens = baseTokenizer.tokenize(processedText);
// 恢复被保护的整词,并添加同义词
tokens = tokens
.map((token) => {
if (protectedTokens.has(token)) {
const originalWord = protectedTokens.get(token);
return synonymsMap[originalWord] || [originalWord];
}
return token;
})
.flat();
return [...new Set(tokens)]; // 去重
}
};
};

View File

@@ -1,92 +0,0 @@
import fs from 'fs-extra';
import path from 'path';
// 从 mdx 文件中读取 weight
async function getWeightFromFile(filePath) {
const content = await fs.readFile(filePath, 'utf-8');
const weightMatch = content.match(/weight:\s*(\d+)/);
return weightMatch ? parseInt(weightMatch[1], 10) : 0;
}
// 从 meta.json 中读取最小 weight用于子目录
async function getWeightFromMeta(dir) {
const metaPath = path.join(dir, 'meta.json');
if (!(await fs.pathExists(metaPath))) return Infinity;
try {
const meta = await fs.readJson(metaPath);
const pages = meta.pages || [];
let minWeight = Infinity;
for (const pageName of pages) {
const mdxPath = path.join(dir, `${pageName}.mdx`);
if (await fs.pathExists(mdxPath)) {
const w = await getWeightFromFile(mdxPath);
if (w < minWeight) minWeight = w;
}
}
return minWeight === Infinity ? 0 : minWeight;
} catch {
return 0;
}
}
// 主函数,返回当前目录的最小 weight
async function generateMetaRecursive(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const items = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
const subWeight = await generateMetaRecursive(fullPath);
items.push({ name: entry.name, weight: subWeight });
} else if (
entry.isFile() &&
entry.name.endsWith('.mdx') &&
!entry.name.endsWith('.en.mdx')
) {
const nameWithoutExt = entry.name.replace(/\.mdx$/, '');
const weight = await getWeightFromFile(fullPath);
items.push({ name: nameWithoutExt, weight });
}
}
// 排序 pages
items.sort((a, b) => a.weight - b.weight);
const pages = items.map((item) => item.name);
// 读取或创建 meta.json
const metaPath = path.join(dir, 'meta.json');
let meta = {
title: 'FastGPT',
description: 'FastGPT Docs',
};
if (await fs.pathExists(metaPath)) {
try {
meta = await fs.readJson(metaPath);
} catch {
console.warn(`⚠️ Failed to parse existing meta.json at ${metaPath}, using defaults.`);
}
}
meta.pages = pages;
// 写入 meta.json格式化为一行的 pages
const jsonString = JSON.stringify(meta, null, 2);
const oneLinePages = `"pages": ${JSON.stringify(pages)}`;
const finalJson = jsonString.replace(/"pages": \[[\s\S]*?\]/, oneLinePages);
await fs.writeFile(metaPath, finalJson, 'utf-8');
console.log(`✅ Updated meta.json in ${dir}`);
return items.length > 0 ? items[0].weight : 0;
}
// 启动
const targetDir = './content/docs/introduction/development/upgrading';
generateMetaRecursive(targetDir)
.then(() => console.log('🎉 All meta.json files generated/updated!'))
.catch((err) => console.error(err));

View File

@@ -1,59 +0,0 @@
import fs from 'fs-extra';
import path from 'path';
async function generateMeta(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const pages = [];
for (const entry of entries) {
if (entry.isDirectory()) {
pages.push(entry.name);
} else if (
entry.isFile() &&
entry.name.endsWith('.mdx') &&
!entry.name.endsWith('.en.mdx')
) {
const nameWithoutExt = entry.name.replace(/\.mdx$/, '');
pages.push(nameWithoutExt);
}
}
const metaPath = path.join(dir, 'meta.json');
// 使用 JSON.stringifyspaces设为2数组不换行
// 通过 replacer 参数实现“pages”数组一行
const jsonString = JSON.stringify(
{ pages },
(key, value) => {
if (key === 'pages') {
return value; // 保持pages数组原样
}
return value;
},
2
);
// 手动替换 pages 数组换行,变成一行显示
const oneLinePages = `"pages": ${JSON.stringify(pages)}`;
const finalJson = jsonString.replace(
/"pages": \[[^\]]*\]/,
oneLinePages
);
await fs.writeFile(metaPath, finalJson, 'utf-8');
console.log(`Generated meta.json in ${dir}`);
// 递归处理子目录
for (const entry of entries) {
if (entry.isDirectory()) {
await generateMeta(path.join(dir, entry.name));
}
}
}
const targetDir = './content/docs/development/openapi';
generateMeta(targetDir)
.then(() => console.log('All meta.json files generated!'))
.catch((err) => console.error(err));

View File

@@ -11,7 +11,6 @@
"dependencies": {
"@orama/orama": "^3.1.11",
"@orama/tokenizers": "^3.1.11",
"algoliasearch": "^5.34.0",
"fast-glob": "^3.3.3",
"fs-extra": "^11.3.0",
"fumadocs-core": "15.6.3",
@@ -47,6 +46,8 @@
"resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.34.0.tgz",
"integrity": "sha512-d6ardhDtQsnMpyr/rPrS3YuIE9NYpY4rftkC7Ap9tyuhZ/+V3E/LH+9uEewPguKzVqduApdwJzYq2k+vAXVEbQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@algolia/client-common": "5.34.0",
"@algolia/requester-browser-xhr": "5.34.0",
@@ -62,6 +63,8 @@
"resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.34.0.tgz",
"integrity": "sha512-WXIByjHNA106JO1Dj6b4viSX/yMN3oIB4qXr2MmyEmNq0MgfuPfPw8ayLRIZPa9Dp27hvM3G8MWJ4RG978HYFw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@algolia/client-common": "5.34.0",
"@algolia/requester-browser-xhr": "5.34.0",
@@ -77,6 +80,8 @@
"resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.34.0.tgz",
"integrity": "sha512-JeN1XJLZIkkv6yK0KT93CIXXk+cDPUGNg5xeH4fN9ZykYFDWYRyqgaDo+qvg4RXC3WWkdQ+hogQuuCk4Y3Eotw==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 14.0.0"
}
@@ -86,6 +91,8 @@
"resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.34.0.tgz",
"integrity": "sha512-gdFlcQa+TWXJUsihHDlreFWniKPFIQ15i5oynCY4m9K3DCex5g5cVj9VG4Hsquxf2t6Y0yv8w6MvVTGDO8oRLw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@algolia/client-common": "5.34.0",
"@algolia/requester-browser-xhr": "5.34.0",
@@ -101,6 +108,8 @@
"resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.34.0.tgz",
"integrity": "sha512-g91NHhIZDkh1IUeNtsUd8V/ZxuBc2ByOfDqhCkoQY3Z/mZszhpn3Czn6AR5pE81fx793vMaiOZvQVB5QttArkQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@algolia/client-common": "5.34.0",
"@algolia/requester-browser-xhr": "5.34.0",
@@ -116,6 +125,8 @@
"resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.34.0.tgz",
"integrity": "sha512-cvRApDfFrlJ3Vcn37U4Nd/7S6T8cx7FW3mVLJPqkkzixv8DQ/yV+x4VLirxOtGDdq3KohcIbIGWbg1QuyOZRvQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@algolia/client-common": "5.34.0",
"@algolia/requester-browser-xhr": "5.34.0",
@@ -131,6 +142,8 @@
"resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.34.0.tgz",
"integrity": "sha512-m9tK4IqJmn+flEPRtuxuHgiHmrKV0su5fuVwVpq8/es4DMjWMgX1a7Lg1PktvO8AbKaTp9kTtBAPnwXpuCwmEg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@algolia/client-common": "5.34.0",
"@algolia/requester-browser-xhr": "5.34.0",
@@ -146,6 +159,8 @@
"resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.34.0.tgz",
"integrity": "sha512-2rxy4XoeRtIpzxEh5u5UgDC5HY4XbNdjzNgFx1eDrfFkSHpEVjirtLhISMy2N5uSFqYu1uUby5/NC1Soq8J7iw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@algolia/client-common": "5.34.0",
"@algolia/requester-browser-xhr": "5.34.0",
@@ -161,6 +176,8 @@
"resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.34.0.tgz",
"integrity": "sha512-OJiDhlJX8ZdWAndc50Z6aUEW/YmnhFK2ul3rahMw5/c9Damh7+oY9SufoK2LimJejy+65Qka06YPG29v2G/vww==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@algolia/client-common": "5.34.0",
"@algolia/requester-browser-xhr": "5.34.0",
@@ -176,6 +193,8 @@
"resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.34.0.tgz",
"integrity": "sha512-fzNQZAdVxu/Gnbavy8KW5gurApwdYcPW6+pjO7Pw8V5drCR3eSqnOxSvp79rhscDX8ezwqMqqK4F3Hsq+KpRzg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@algolia/client-common": "5.34.0",
"@algolia/requester-browser-xhr": "5.34.0",
@@ -191,6 +210,8 @@
"resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.34.0.tgz",
"integrity": "sha512-gEI0xjzA/xvMpEdYmgQnf6AQKllhgKRtnEWmwDrnct+YPIruEHlx1dd7nRJTy/33MiYcCxkB4khXpNrHuqgp3Q==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@algolia/client-common": "5.34.0"
},
@@ -203,6 +224,8 @@
"resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.34.0.tgz",
"integrity": "sha512-5SwGOttpbACT4jXzfSJ3mnTcF46SVNSnZ1JjxC3qBa3qKi4U0CJGzuVVy3L798u8dG5H0SZ2MAB5v7180Gnqew==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@algolia/client-common": "5.34.0"
},
@@ -215,6 +238,8 @@
"resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.34.0.tgz",
"integrity": "sha512-409XlyIyEXrxyGjWxd0q5RASizHSRVUU0AXPCEdqnbcGEzbCgL1n7oYI8YxzE/RqZLha+PNwWCcTVn7EE5tyyQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@algolia/client-common": "5.34.0"
},
@@ -1466,7 +1491,6 @@
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/@orama/tokenizers/-/tokenizers-3.1.11.tgz",
"integrity": "sha512-fwULrEdbP5/83gFjaX1X/l7lzdD7LxBT8YbAzcY89BmXjJcJETU/5qckp4ZNDMhRRjJUSGKH4bAXHsm6yu+ZPw==",
"license": "Apache-2.0",
"dependencies": {
"@orama/orama": "3.1.11"
},
@@ -2723,6 +2747,8 @@
"resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.34.0.tgz",
"integrity": "sha512-wioVnf/8uuG8Bmywhk5qKIQ3wzCCtmdvicPRb0fa3kKYGGoewfgDqLEaET1MV2NbTc3WGpPv+AgauLVBp1nB9A==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@algolia/client-abtesting": "5.34.0",
"@algolia/client-analytics": "5.34.0",

View File

@@ -4,8 +4,6 @@
"private": true,
"scripts": {
"build": "next build",
"update-index-action": "node ./update-index.mjs",
"update-index": "node --env-file=.env.local ./update-index.mjs",
"dev": "next dev --turbo",
"start": "next start",
"postinstall": "fumadocs-mdx"
@@ -13,7 +11,6 @@
"dependencies": {
"@orama/orama": "^3.1.11",
"@orama/tokenizers": "^3.1.11",
"algoliasearch": "^5.34.0",
"fast-glob": "^3.3.3",
"fs-extra": "^11.3.0",
"fumadocs-core": "15.6.3",

View File

@@ -1,28 +0,0 @@
import { algoliasearch } from 'algoliasearch';
import { sync } from 'fumadocs-core/search/algolia';
import * as fs from 'node:fs';
async function main() {
const content = fs.readFileSync('.next/server/app/static.json.body');
// now you can pass it to `sync`
/** @type {import('fumadocs-core/search/algolia').DocumentRecord[]} **/
const records = JSON.parse(content.toString());
if (!process.env.NEXT_PUBLIC_SEARCH_APPID || !process.env.SEARCH_APPWRITEKEY || !process.env.SEARCH_APPWRITEKEY) {
console.log('NEXT_PUBLIC_SEARCH_APPID or SEARCH_APPWRITEKEY is not set');
return;
}
const client = algoliasearch(
process.env.NEXT_PUBLIC_SEARCH_APPID || '',
process.env.SEARCH_APPWRITEKEY || ''
);
void sync(client, {
indexName: 'document',
documents: records
});
}
main();