Merge pull request #21 from yayoinoyume/main

尝试基于pansou构建了一个MCP服务
This commit is contained in:
Nobody
2025-08-26 18:43:27 +08:00
committed by GitHub
20 changed files with 9867 additions and 6 deletions

View File

@@ -2,6 +2,8 @@
PanSou是一个高性能的网盘资源搜索API服务支持TG搜索和自定义插件搜索。系统设计以性能和可扩展性为核心支持并发搜索、结果智能排序和网盘类型分类。
[//]: # (MCP服务文档: [MCP-SERVICE.md](docs/MCP-SERVICE.md))
## 特性([详见系统设计文档](docs/%E7%B3%BB%E7%BB%9F%E5%BC%80%E5%8F%91%E8%AE%BE%E8%AE%A1%E6%96%87%E6%A1%A3.md)
@@ -11,6 +13,10 @@ PanSou是一个高性能的网盘资源搜索API服务支持TG搜索和自定
- **异步插件系统**:支持通过插件扩展搜索来源,支持"尽快响应,持续处理"的异步搜索模式,解决了某些搜索源响应时间长的问题。详情参考[**插件开发指南**](docs/插件开发指南.md)
- **二级缓存**:分片内存+分片磁盘缓存机制,大幅提升重复查询速度和并发性能
## MCP 服务
PanSou 还提供了一个基于 [Model Context Protocol (MCP)](https://modelcontextprotocol.io) 的服务,可以将搜索功能集成到 Claude Desktop 等支持 MCP 的应用中。详情请参阅 [MCP 服务文档](docs/MCP-SERVICE.md)。
## 支持的网盘类型
百度网盘 (`baidu`)、阿里云盘 (`aliyun`)、夸克网盘 (`quark`)、天翼云盘 (`tianyi`)、UC网盘 (`uc`)、移动云盘 (`mobile`)、115网盘 (`115`)、PikPak (`pikpak`)、迅雷网盘 (`xunlei`)、123网盘 (`123`)、磁力链接 (`magnet`)、电驴链接 (`ed2k`)、其他 (`others`)

394
docs/MCP-SERVICE.md Normal file
View File

@@ -0,0 +1,394 @@
# PanSou MCP 服务文档
## 功能介绍
PanSou MCP 服务是一个基于 [Model Context Protocol (MCP)](https://modelcontextprotocol.io) 的工具服务,它将 PanSou 网盘搜索 API 的功能封装为可在支持 MCP 的客户端(如 Claude Desktop中直接调用的工具。
通过 PanSou MCP 服务,可以直接在 Claude 等 AI 助手中搜索网盘资源,极大地提升了获取网盘资源的便捷性。
### 核心功能
1. **搜索网盘资源 (`search_netdisk`)**:
- 支持通过关键词搜索网盘资源。
- 可指定搜索来源Telegram 频道、插件或两者结合。
- 可过滤结果,仅显示特定类型的网盘链接(如百度网盘、阿里云盘、夸克网盘等)。
- 支持强制刷新缓存以获取最新数据。
- 支持传递扩展参数给后端插件。
- 结果可按详细信息或按网盘类型分组展示。
2. **检查服务健康状态 (`check_service_health`)**:
- 检查所连接的 PanSou 后端服务是否正常运行。
- 获取后端服务的配置信息,如可用的 Telegram 频道列表和插件列表。
3. **启动后端服务 (`start_backend`)**:
- 自动启动本地的 PanSou Go 后端服务(如果尚未运行)。
- 等待服务完全启动并可用后才开始处理其他请求。
4. **获取静态资源信息 (`pansou://` URI scheme)**:
- 提供可用插件列表、可用频道列表和支持的网盘类型列表等静态信息资源。
### 架构与部署方式
PanSou MCP 服务设计为与 PanSou Go 后端服务分离,通过 HTTP API 进行通信。支持以下部署方式:
- **npx 部署 (TypeScript)**: MCP 服务基于 TypeScript 开发,编译后可以通过 `npx` 命令直接运行。它会自动连接到指定的 PanSou 后端服务。
- **Docker 部署**: 使用 Docker 容器运行 PanSou 后端服务MCP 服务通过 HTTP API 连接到容器化的后端。
---
## 安装与部署
### 前提条件
1. **Node.js**: 确保您的系统已安装 Node.js (版本 >= 18.0.0)。您可以通过在终端运行 `node -v` 来检查版本。
2. **Go**: 确保您的系统已安装 Go (版本 >= 1.18)。您可以通过在终端运行 `go version` 来检查版本。
### 部署步骤
PanSou 后端服务通常运行在 `http://localhost:8888` (默认地址)。支持以下两种部署方式:
## 方式一:源码部署
后端服务和 MCP 服务都需要从源码构建。
#### 1. 构建并启动 PanSou 后端服务 (Go)
- 确保系统已安装 Go 1.25.0 或更高版本。
- 克隆或确保已有 PanSou Go 项目源码。
- 在项目根目录下,打开终端并执行以下命令进行构建:
```bash
# Windows (PowerShell/CMD)
go build -o pansou.exe .
```
- 构建完成后,运行生成的可执行文件以启动后端服务:
```bash
# Windows
.\pansou.exe
```
服务默认将在 `http://localhost:8888` 启动。
#### 2. 构建 PanSou MCP 服务 (TypeScript)
- 确保系统已安装 Node.js (版本 >= 18.0.0)。
-`typescript` 目录下,打开终端并执行以下命令来安装依赖并构建项目:
```bash
# 安装 Node.js 依赖
npm install
# 构建 TypeScript 项目
npm run build
```
构建完成后,编译后的 JavaScript 文件将位于 `typescript/dist` 目录下。
- 确保服务成功启动。您可以通过在终端中访问 `http://localhost:8888/api/health` 来检查,应该能看到类似以下的 JSON 响应,表明服务正常:
```json
{
"status": "ok",
"plugins_enabled": true,
"channels_count": 1,
"channels": ["tgsearchers3"],
"plugin_count": 16,
"plugins": ["pansearch", "panta", ...]
}
```
#### 3. 运行 MCP 服务
构建完成后,可以通过以下方式之一运行 MCP 服务:
- **在MCP调用时自动启动** (自动启动):
直接浏览后续MCP配置的内容配置好MCP后调用时会自动启动后端服务器。
- **使用 `node` 直接运行** (手动启动):
在 PanSou 项目根目录下(包含 `typescript` 文件夹),运行:
```bash
# Windows (CMD/PowerShell)
node .\\typescript\\dist\\index.js
```
服务启动后,将默认尝试连接到 `http://localhost:8888` 的 PanSou 后端服务。
如果想要后端服务运行在不同的地址或端口上,需要通过环境变量指定:
```bash
# Windows (CMD)
set PANSOU_SERVER_URL=http://your-backend-address:port
node .\\typescript\\dist\\index.js
# Windows (PowerShell)
export PANSOU_SERVER_URL=http://your-backend-address:port
node ./typescript/dist/index.js
```
#### 4. 示例配置 Cherry Studio(版本1.5.7)
要在 Cherry Studio 中使用 PanSou MCP 服务,需要将其添加到 Cherry Studio MCP 的配置文件中。
- 找到 设置中的MCP。
- 选择 `添加服务器` 、 `从JSON导入` 。
- 加入服务配置(可以直接复制项目根目录下的 `mcp-config.json` 内容)
```json
{
"mcpServers": {
"pansou": {
"command": "node",
"args": [
"C:\\full\\path\\to\\your\\project\\typescript\\dist\\index.js"
],
"env": {
"PANSOU_SERVER_URL": "http://localhost:8888",
"REQUEST_TIMEOUT": "30",
"MAX_RESULTS": "50",
"DEFAULT_CLOUD_TYPES": "baidu,aliyun,quark,tianyi,uc,mobile,115,pikpak,xunlei,123,magnet,ed2k,others",
"AUTO_START_BACKEND": "true",
"DOCKER_MODE": "false",
"BACKEND_SHUTDOWN_DELAY": "5000",
"BACKEND_STARTUP_TIMEOUT": "30000",
"IDLE_TIMEOUT": "300000",
"ENABLE_IDLE_SHUTDOWN": "true",
"PROJECT_ROOT_PATH": "C:\\full\\path\\to\\your\\project"
}
}
}
}
```
**注意**
- 请将 `C:\\full\\path\\to\\your\\project` 替换为您项目实际的完整路径
- 如需强制指定部署模式,可修改 `DOCKER_MODE` 和 `AUTO_START_BACKEND` 参数
#### 5. 启动 MCP 服务,并在对话界面启用,开始尝试搜索
<img width="495" height="649" alt="image" src="https://github.com/user-attachments/assets/b8c72649-03e8-4f52-86ba-aa16c4cc3b7e" />
---
## 方式二Docker 部署
Docker 部署方式更加简单,无需手动构建 Go 后端服务,直接使用预构建的 Docker 镜像。
### 前提条件
1. **Docker**: 确保您的系统已安装 Docker 和 Docker Compose。
2. **Node.js**: 确保您的系统已安装 Node.js (版本 >= 18.0.0),用于运行 MCP 服务。
### 部署步骤
#### 1. 启动 Docker 后端服务
在 PanSou 项目根目录下,使用 Docker Compose 启动后端服务:
```bash
# 启动 Docker 容器
docker-compose up -d
# 检查容器状态
docker ps
# 验证服务是否正常运行
curl http://localhost:8888/api/health
```
成功启动后,您应该能看到类似以下的 JSON 响应:
```json
{
"status": "ok",
"plugins_enabled": true,
"channels_count": 5,
"channels": ["tgsearchers3", "SharePanBaidu", "yunpanxunlei", "tianyifc", "BaiduCloudDisk"],
"plugin_count": 16,
"plugins": ["pansearch", "panta", ...]
}
```
#### 2. 构建 MCP 服务
在 `typescript` 目录下构建 MCP 服务:
```bash
cd typescript
npm install
npm run build
```
#### 3. 配置 MCP 服务
```json
{
"mcpServers": {
"pansou": {
"command": "node",
"args": [
"C:\\full\\path\\to\\your\\project\\typescript\\dist\\index.js"
],
"env": {
"PANSOU_SERVER_URL": "http://localhost:8888",
"REQUEST_TIMEOUT": "30",
"MAX_RESULTS": "50",
"DEFAULT_CLOUD_TYPES": "baidu,aliyun,quark,tianyi,uc,mobile,115,pikpak,xunlei,123,magnet,ed2k,others",
"AUTO_START_BACKEND": "true",
"DOCKER_MODE": "true",
"BACKEND_SHUTDOWN_DELAY": "5000",
"BACKEND_STARTUP_TIMEOUT": "30000",
"IDLE_TIMEOUT": "300000",
"ENABLE_IDLE_SHUTDOWN": "true",
"PROJECT_ROOT_PATH": "C:\\full\\path\\to\\your\\project"
}
}
}
}
```
**智能检测机制**
当 `DOCKER_MODE` 设置为 `"false"` 或未设置时MCP 服务将自动检测部署模式:
1. **Docker 容器检测**:检查是否有运行中的 Docker 容器(名称包含 "pansou"
2. **源码部署检测**:检查是否存在 Go 可执行文件pansou.exe/main.exe
3. **服务运行检测**:检查后端服务是否已在运行
**配置模式**
- **自动模式**(推荐):使用默认配置,让服务自动检测部署方式
- **强制 Docker 模式**:设置 `"DOCKER_MODE": "true"`
- **强制源码模式**:设置 `"DOCKER_MODE": "false"` 且 `"AUTO_START_BACKEND": "true"`
- **仅连接模式**:设置 `"AUTO_START_BACKEND": "false"`(适用于手动启动的后端)
#### 4. 在 MCP 客户端中配置
将上述配置添加到您的 MCP 客户端(如 Cherry Studio记得将路径替换为实际路径。
#### 5. 测试 Docker 部署
您可以手动测试 MCP 服务是否能正确连接到 Docker 后端:
```bash
# Windows (PowerShell)
$env:DOCKER_MODE='true'
$env:PANSOU_SERVER_URL='http://localhost:8888'
node .\typescript\dist\index.js
```
成功启动后,您应该看到类似以下的输出:
```
⏱️ 空闲监控已启用,超时时间: 300 秒
🔍 检查后端服务状态...
✅ 后端服务已在运行
🚀 PanSou MCP服务器已启动
📡 服务地址: http://localhost:8888
```
### 配置文件说明
无论是源码部署后端还是Docker部署后端都可以用统一的 `mcp-config.json` 配置文件。
### 部署方式的优势
**Docker 部署**
1. **简化部署**: 无需手动构建 Go 后端服务
2. **环境隔离**: 后端服务运行在独立的容器环境中
3. **易于管理**: 可以通过 Docker Compose 轻松启动、停止和重启服务
4. **配置灵活**: 通过环境变量轻松调整服务配置
**源码部署**
1. **完全控制**: 可以自定义构建和配置
2. **开发友好**: 便于调试和开发
3. **资源效率**: 直接运行,无容器开销
**智能检测**
1. **自动适配**: 无需手动选择部署模式
2. **简化配置**: 一个配置文件适用所有场景
3. **错误减少**: 避免配置错误导致的问题
### 常见问题排查
如果 MCP 服务无法连接到 Docker 后端,请检查:
1. **容器状态**: 使用 `docker ps` 确认容器正在运行
2. **端口映射**: 确认端口 8888 已正确映射到主机
3. **健康检查**: 使用 `curl http://localhost:8888/api/health` 测试后端服务
4. **防火墙**: 确认防火墙没有阻止端口 8888
如果遇到问题,可以查看容器日志:
```bash
# 查看容器日志
docker logs pansou
# 实时查看日志
docker logs -f pansou
```
---
## 支持的参数
MCP 服务通过工具调用接收参数。以下是主要工具及其支持的参数:
### `search_netdisk` 工具
用于搜索网盘资源。
| 参数名 | 类型 | 必填 | 默认值 | 描述 |
| :-------------- | :-------------- | :--- | :------------------- | :----------------------------------------------------------- |
| `keyword` | string | 是 | - | 搜索关键词,例如 "速度与激情"、"Python教程"。 |
| `channels` | array of string | 否 | 配置默认值 | 要搜索的 Telegram 频道列表,例如 `["tgsearchers3", "another_channel"]`。 |
| `plugins` | array of string | 否 | 配置默认值或所有插件 | 要使用的搜索插件列表,例如 `["pansearch", "panta"]`。 |
| `cloud_types` | array of string | 否 | 无过滤 | 过滤结果,仅返回指定类型的网盘链接。支持的类型有:`baidu`, `aliyun`, `quark`, `tianyi`, `uc`, `mobile`, `115`, `pikpak`, `xunlei`, `123`, `magnet`, `ed2k`, `others`。 |
| `source_type` | string | 否 | `"all"` | 数据来源类型。可选值:`"all"` (全部来源), `"tg"` (仅 Telegram), `"plugin"` (仅插件)。 |
| `force_refresh` | boolean | 否 | `false` | 是否强制刷新缓存,以获取最新数据。 |
| `result_type` | string | 否 | `"merge"` | 返回结果的类型。可选值:`"all"` (返回所有结果), `"results"` (仅返回详细结果), `"merge"` (仅返回按网盘类型分组的结果)。 |
| `concurrency` | number | 否 | 自动计算 | 并发搜索的数量。 |
| `ext_params` | object | 否 | `{}` | 传递给后端插件的自定义扩展参数,例如 `{"title_en": "Fast and Furious", "is_all": true}`。 |
---
### `check_service_health` 工具
用于检查后端服务健康状态。
- **参数**: 无
---
### `start_backend` 工具
用于启动本地 PanSou 后端服务。
| 参数名 | 类型 | 必填 | 默认值 | 描述 |
| :-------------- | :------ | :--- | :------ | :----------------------------------------- |
| `force_restart` | boolean | 否 | `false` | 是否强制重启后端服务(即使它已经在运行)。 |
---
### 环境变量配置
您可以通过设置环境变量来配置 MCP 服务的行为:
| 环境变量 | 描述 | 默认值 |
| :--------------------- | :--------------------------------------------------------- | :------------------------ |
| `PANSOU_SERVER_URL` | PanSou 后端服务的 URL 地址。 | `http://localhost:8888` |
| `REQUEST_TIMEOUT` | HTTP 请求超时时间(秒)。 | `30` |
| `MAX_RESULTS` | (内部使用,限制处理结果数量) | `100` |
| `DEFAULT_CHANNELS` | 默认搜索的 Telegram 频道列表(逗号分隔)。 | `""` (使用后端默认) |
| `DEFAULT_PLUGINS` | 默认使用的搜索插件列表(逗号分隔)。 | `""` (使用后端默认或所有) |
| `DEFAULT_CLOUD_TYPES` | 默认的网盘类型过滤器(逗号分隔)。 | `""` (无过滤) |
| `AUTO_START_BACKEND` | 是否在 MCP 服务启动时自动尝试启动后端服务。 | `true` |
| `DOCKER_MODE` | 部署模式控制。设置为 `true` 强制使用 Docker 模式;设置为 `false` 或未设置时启用智能检测。智能检测将自动识别 Docker 容器、源码部署或运行中的服务。 | `false` (智能检测) |
| `PROJECT_ROOT_PATH` | PanSou 后端可执行文件所在的目录路径(用于自动启动)。 | 无 |
| `IDLE_TIMEOUT` | 空闲超时时间(毫秒),超过此时间无活动则可能关闭后端服务。 | `300000` (5分钟) |
| `ENABLE_IDLE_SHUTDOWN` | 是否启用空闲超时自动关闭后端服务。 | `true` |

6
go.mod
View File

@@ -2,18 +2,16 @@ module pansou
go 1.23.0
toolchain go1.23.11
require (
github.com/PuerkitoBio/goquery v1.8.1
github.com/bytedance/sonic v1.13.3
github.com/bytedance/sonic v1.14.0
github.com/gin-gonic/gin v1.9.1
golang.org/x/net v0.41.0
)
require (
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect

4
go.sum
View File

@@ -4,9 +4,13 @@ github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x0
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=

55
mcp-config.json Normal file
View File

@@ -0,0 +1,55 @@
{
"mcpServers": {
"pansou": {
"command": "node",
"args": [
"C:\\full\\path\\to\\your\\project\\typescript\\dist\\index.js"
],
"env": {
"PANSOU_SERVER_URL": "http://localhost:8888",
"REQUEST_TIMEOUT": "60",
"MAX_RESULTS": "50",
"DEFAULT_CLOUD_TYPES": "baidu,aliyun,quark,tianyi,uc,mobile,115,pikpak,xunlei,123,magnet,ed2k,others",
"AUTO_START_BACKEND": "true",
"DOCKER_MODE": "true",
"BACKEND_SHUTDOWN_DELAY": "5000",
"BACKEND_STARTUP_TIMEOUT": "30000",
"IDLE_TIMEOUT": "300000",
"ENABLE_IDLE_SHUTDOWN": "true",
"PROJECT_ROOT_PATH": "C:\\full\\path\\to\\your\\project"
}
}
},
"_comments": {
"description": "PanSou MCP服务统一配置文件",
"version": "2.0",
"智能模式说明": {
"自动检测": "如果DOCKER_MODE未设置或为false服务将自动检测部署模式",
"检测优先级": [
"1. 检查是否有运行中的Docker容器名称包含pansou",
"2. 检查是否存在Go可执行文件pansou.exe/main.exe",
"3. 检查后端服务是否已在运行"
],
"环境变量覆盖": "可通过环境变量强制指定模式,如 DOCKER_MODE=true"
},
"配置说明": {
"PANSOU_SERVER_URL": "后端服务地址默认http://localhost:8888",
"REQUEST_TIMEOUT": "请求超时时间默认30",
"MAX_RESULTS": "最大搜索结果数默认50",
"DEFAULT_CLOUD_TYPES": "默认搜索的网盘类型,逗号分隔",
"AUTO_START_BACKEND": "是否自动启动后端服务源码模式默认true",
"DOCKER_MODE": "是否强制使用Docker模式默认false自动检测",
"BACKEND_SHUTDOWN_DELAY": "后端服务关闭延迟毫秒默认5000",
"BACKEND_STARTUP_TIMEOUT": "后端服务启动超时毫秒默认30000",
"IDLE_TIMEOUT": "空闲超时时间毫秒默认3000005分钟",
"ENABLE_IDLE_SHUTDOWN": "是否启用空闲自动关闭默认true",
"PROJECT_ROOT_PATH": "项目根目录路径用于查找Go可执行文件"
},
"使用示例": {
"自动模式": "默认配置,自动检测部署方式",
"强制Docker模式": "设置 DOCKER_MODE=true",
"强制源码模式": "设置 DOCKER_MODE=false 且 AUTO_START_BACKEND=true",
"仅连接模式": "设置 AUTO_START_BACKEND=false适用于手动启动的后端"
}
}
}

View File

@@ -52,7 +52,7 @@ type Response struct {
// NewSuccessResponse 创建成功响应
func NewSuccessResponse(data interface{}) Response {
return Response{
Code: 0,
Code: 200,
Message: "success",
Data: data,
}
@@ -64,4 +64,4 @@ func NewErrorResponse(code int, message string) Response {
Code: code,
Message: message,
}
}
}

1046
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

5
package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.4"
}
}

6274
typescript/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

60
typescript/package.json Normal file
View File

@@ -0,0 +1,60 @@
{
"name": "@pansou/mcp-server",
"version": "1.0.0",
"description": "MCP server for PanSou netdisk search service",
"main": "dist/index.js",
"bin": {
"pansou-mcp-server": "dist/index.js"
},
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts",
"prepare": "npm run build",
"test": "jest",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix"
},
"keywords": [
"mcp",
"model-context-protocol",
"pansou",
"netdisk",
"search",
"claude"
],
"author": "PanSou Team",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/fish2018/pansou-mcp.git"
},
"homepage": "https://github.com/fish2018/pansou-mcp#readme",
"bugs": {
"url": "https://github.com/fish2018/pansou-mcp/issues"
},
"engines": {
"node": ">=18.0.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"axios": "^1.6.0",
"zod": "^3.22.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.0.0",
"jest": "^29.0.0",
"ts-jest": "^29.0.0",
"ts-node": "^10.0.0",
"typescript": "^5.0.0"
},
"files": [
"dist/**/*",
"README.md",
"LICENSE"
]
}

390
typescript/src/index.ts Normal file
View File

@@ -0,0 +1,390 @@
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListResourcesRequestSchema,
ListToolsRequestSchema,
McpError,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { loadConfig } from './utils/config.js';
import { HttpClient } from './utils/http-client.js';
import { BackendManager } from './utils/backend-manager.js';
import { searchTool, executeSearchTool } from './tools/search.js';
import { healthTool, executeHealthTool } from './tools/health.js';
import { startBackendTool, executeStartBackendTool } from './tools/start-backend.js';
/**
* PanSou MCP服务器
*/
class PanSouMCPServer {
private server: Server;
private httpClient: HttpClient;
private backendManager: BackendManager;
private config: any;
constructor() {
this.server = new Server(
{
name: 'pansou-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// 加载配置
this.config = loadConfig();
this.httpClient = new HttpClient(this.config);
this.backendManager = new BackendManager(this.config, this.httpClient);
this.setupHandlers();
this.setupProcessHandlers();
}
/**
* 设置请求处理器
*/
private setupHandlers(): void {
// 工具列表处理器
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [healthTool, startBackendTool, searchTool],
};
});
// 工具调用处理器
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// 记录活动,重置空闲计时器
this.backendManager.recordActivity();
try {
switch (name) {
case 'check_service_health':
const healthResult = await executeHealthTool(args, this.httpClient);
return {
content: [
{
type: 'text',
text: healthResult,
},
],
};
case 'start_backend':
const startResult = await executeStartBackendTool(args, this.httpClient, this.config);
return {
content: [
{
type: 'text',
text: startResult,
},
],
};
case 'search_netdisk':
const searchResult = await executeSearchTool(args, this.httpClient);
return {
content: [
{
type: 'text',
text: searchResult,
},
],
};
default:
throw new McpError(
ErrorCode.MethodNotFound,
`未知工具: ${name}`
);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`工具执行失败: ${error instanceof Error ? error.message : String(error)}`
);
}
});
// 资源列表处理器
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'pansou://plugins',
name: '可用插件列表',
description: '获取当前可用的搜索插件列表',
mimeType: 'application/json',
},
{
uri: 'pansou://channels',
name: '可用频道列表',
description: '获取当前可用的TG频道列表',
mimeType: 'application/json',
},
{
uri: 'pansou://cloud-types',
name: '支持的网盘类型',
description: '获取支持的网盘类型列表',
mimeType: 'application/json',
},
],
};
});
// 资源读取处理器
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
// 记录活动,重置空闲计时器
this.backendManager.recordActivity();
try {
switch (uri) {
case 'pansou://plugins':
return await this.getPluginsResource();
case 'pansou://channels':
return await this.getChannelsResource();
case 'pansou://cloud-types':
return await this.getCloudTypesResource();
default:
throw new McpError(
ErrorCode.InvalidRequest,
`未知资源URI: ${uri}`
);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`资源读取失败: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}
/**
* 获取插件资源
*/
private async getPluginsResource() {
try {
const healthData = await this.httpClient.checkHealth();
const plugins = {
enabled: healthData.plugins_enabled || false,
count: healthData.plugin_count || 0,
list: healthData.plugins || [],
};
return {
contents: [
{
uri: 'pansou://plugins',
mimeType: 'application/json',
text: JSON.stringify(plugins, null, 2),
},
],
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`获取插件信息失败: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* 获取频道资源
*/
private async getChannelsResource() {
try {
const healthData = await this.httpClient.checkHealth();
const channels = {
count: healthData.channels_count || 0,
list: healthData.channels || [],
};
return {
contents: [
{
uri: 'pansou://channels',
mimeType: 'application/json',
text: JSON.stringify(channels, null, 2),
},
],
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`获取频道信息失败: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* 获取网盘类型资源
*/
private async getCloudTypesResource() {
const cloudTypes = {
supported: [
'baidu', // 百度网盘
'aliyun', // 阿里云盘
'quark', // 夸克网盘
'tianyi', // 天翼云盘
'uc', // UC网盘
'mobile', // 移动云盘
'115', // 115网盘
'pikpak', // PikPak
'xunlei', // 迅雷网盘
'123', // 123网盘
'magnet', // 磁力链接
'ed2k', // 电驴链接
'others' // 其他
],
description: {
'baidu': '百度网盘',
'aliyun': '阿里云盘',
'quark': '夸克网盘',
'tianyi': '天翼云盘',
'uc': 'UC网盘',
'mobile': '移动云盘',
'115': '115网盘',
'pikpak': 'PikPak',
'xunlei': '迅雷网盘',
'123': '123网盘',
'magnet': '磁力链接',
'ed2k': '电驴链接',
'others': '其他网盘'
}
};
return {
contents: [
{
uri: 'pansou://cloud-types',
mimeType: 'application/json',
text: JSON.stringify(cloudTypes, null, 2),
},
],
};
}
/**
* 设置进程处理器
*/
private setupProcessHandlers(): void {
// 处理优雅关闭
const gracefulShutdown = async (signal: string) => {
console.error(`\n📡 收到 ${signal} 信号,正在优雅关闭...`);
if (this.config.autoStartBackend) {
// 延迟关闭后端服务
this.backendManager.scheduleShutdown();
}
// 等待一小段时间让MCP客户端处理完当前请求
setTimeout(() => {
process.exit(0);
}, 1000);
};
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
// Windows特有的关闭事件
if (process.platform === 'win32') {
process.on('SIGBREAK', () => gracefulShutdown('SIGBREAK'));
}
}
/**
* 启动服务器
*/
public async start(): Promise<void> {
// 如果启用了自动启动后端服务
if (this.config.autoStartBackend) {
console.error('🔍 检查后端服务状态...');
// 在启动阶段启用静默模式,避免输出网络错误信息
this.httpClient.setSilentMode(true);
const isRunning = await this.backendManager.isBackendRunning();
if (!isRunning) {
console.error('🚀 自动启动后端服务...');
const started = await this.backendManager.startBackend();
if (!started) {
console.error('❌ 后端服务启动失败MCP服务器将继续运行但功能可能受限');
}
} else {
console.error('✅ 后端服务已在运行');
}
// 启动完成后关闭静默模式
this.httpClient.setSilentMode(false);
}
const transport = new StdioServerTransport();
await this.server.connect(transport);
// 输出启动信息到stderr避免干扰MCP通信
console.error('🚀 PanSou MCP服务器已启动');
console.error(`📡 服务地址: ${this.config.serverUrl}`);
console.error(`⏱️ 请求超时: ${this.config.requestTimeout}ms`);
console.error(`📊 最大结果数: ${this.config.maxResults}`);
console.error(`🔧 自动启动后端: ${this.config.autoStartBackend ? '启用' : '禁用'}`);
// 空闲监控信息已在BackendManager构造函数中显示
}
}
/**
* 主函数
*/
async function main(): Promise<void> {
try {
const server = new PanSouMCPServer();
await server.start();
} catch (error) {
console.error('❌ 服务器启动失败:', error);
process.exit(1);
}
}
// 处理未捕获的异常
process.on('uncaughtException', (error) => {
console.error('❌ 未捕获的异常:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('❌ 未处理的Promise拒绝:', reason);
process.exit(1);
});
// 启动服务器
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1].endsWith('index.js')) {
main();
}
export { PanSouMCPServer };

View File

@@ -0,0 +1,115 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { HttpClient } from '../utils/http-client.js';
/**
* 健康检查工具定义
*/
export const healthTool: Tool = {
name: 'check_service_health',
description: '检查PanSou服务的健康状态获取服务信息、可用插件和频道列表',
inputSchema: {
type: 'object',
properties: {},
required: []
}
};
/**
* 执行健康检查工具
*/
export async function executeHealthTool(args: unknown, httpClient: HttpClient): Promise<string> {
try {
// 执行健康检查
const healthData = await httpClient.checkHealth();
// 格式化返回结果
return formatHealthResult(healthData, httpClient.getServerUrl());
} catch (error) {
if (error instanceof Error) {
return formatErrorResult(error.message, httpClient.getServerUrl());
}
return formatErrorResult(`健康检查失败: ${String(error)}`, httpClient.getServerUrl());
}
}
/**
* 格式化健康检查结果
*/
function formatHealthResult(healthData: any, serverUrl: string): string {
let output = `🏥 **PanSou服务健康检查**\n\n`;
// 服务基本信息
output += `🌐 **服务地址**: ${serverUrl}\n`;
output += `✅ **服务状态**: ${healthData.status === 'ok' ? '正常' : '异常'}\n\n`;
// 频道信息
output += `📺 **TG频道信息**\n`;
output += ` 📊 频道数量: ${healthData.channels_count || 0}\n`;
if (healthData.channels && healthData.channels.length > 0) {
output += ` 📋 可用频道:\n`;
healthData.channels.forEach((channel: string, index: number) => {
output += ` ${index + 1}. ${channel}\n`;
});
} else {
output += ` ⚠️ 未配置频道\n`;
}
output += '\n';
// 插件信息
output += `🔌 **插件信息**\n`;
output += ` 🔧 插件功能: ${healthData.plugins_enabled ? '已启用' : '已禁用'}\n`;
if (healthData.plugins_enabled) {
output += ` 📊 插件数量: ${healthData.plugin_count || 0}\n`;
if (healthData.plugins && healthData.plugins.length > 0) {
output += ` 📋 可用插件:\n`;
// 将插件按行显示每行最多4个
const plugins = healthData.plugins;
for (let i = 0; i < plugins.length; i += 4) {
const row = plugins.slice(i, i + 4);
output += ` ${row.map((plugin: string, idx: number) => `${i + idx + 1}. ${plugin}`).join(' ')}\n`;
}
} else {
output += ` ⚠️ 未发现可用插件\n`;
}
} else {
output += ` 插件功能已禁用\n`;
}
output += '\n';
// 功能说明
output += `💡 **功能说明**\n`;
output += ` 🔍 支持搜索多种网盘资源\n`;
output += ` 📱 支持TG频道和插件双重搜索\n`;
output += ` 🚀 支持并发搜索,提升搜索速度\n`;
output += ` 💾 支持缓存机制,避免重复请求\n`;
output += ` 🎯 支持按网盘类型过滤结果\n`;
return output;
}
/**
* 格式化错误结果
*/
function formatErrorResult(errorMessage: string, serverUrl: string): string {
let output = `❌ **PanSou服务健康检查失败**\n\n`;
output += `🌐 **服务地址**: ${serverUrl}\n`;
output += `💥 **错误信息**: ${errorMessage}\n\n`;
output += `🔧 **可能的解决方案**:\n`;
output += ` 1. 检查PanSou服务是否正在运行\n`;
output += ` 2. 确认服务地址配置是否正确\n`;
output += ` 3. 检查网络连接是否正常\n`;
output += ` 4. 查看服务日志获取更多信息\n\n`;
output += `📖 **配置说明**:\n`;
output += ` 可通过环境变量 PANSOU_SERVER_URL 配置服务地址\n`;
output += ` 默认地址: http://localhost:8888\n`;
return output;
}

View File

@@ -0,0 +1,267 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { HttpClient, SearchRequest } from '../utils/http-client.js';
import { validateCloudTypes, validateSourceType, validateResultType, SUPPORTED_CLOUD_TYPES, SOURCE_TYPES, RESULT_TYPES } from '../utils/config.js';
/**
* 搜索工具参数验证模式
*/
const SearchToolArgsSchema = z.object({
keyword: z.string().min(1, '搜索关键词不能为空'),
channels: z.array(z.string()).optional().describe('TG频道列表如: ["tgsearchers3", "xxx"]'),
plugins: z.array(z.string()).optional().describe('插件列表,如: ["pansearch", "panta"]'),
cloud_types: z.array(z.string()).optional().describe(`网盘类型过滤,支持: ${SUPPORTED_CLOUD_TYPES.join(', ')}`),
source_type: z.enum(['all', 'tg', 'plugin']).optional().default('all').describe('数据来源类型'),
force_refresh: z.boolean().optional().default(false).describe('强制刷新缓存'),
result_type: z.enum(['all', 'results', 'merge']).optional().default('merge').describe('结果类型'),
concurrency: z.number().int().positive().optional().describe('并发搜索数量'),
ext_params: z.record(z.any()).optional().describe('扩展参数,传递给插件的自定义参数')
});
export type SearchToolArgs = z.infer<typeof SearchToolArgsSchema>;
/**
* 搜索工具定义
*/
export const searchTool: Tool = {
name: 'search_netdisk',
description: '搜索网盘资源,支持多种网盘类型和搜索来源。可以搜索电影、电视剧、软件、文档等各类资源。',
inputSchema: {
type: 'object',
properties: {
keyword: {
type: 'string',
description: '搜索关键词,如:"速度与激情"、"Python教程"、"Office 2021"等'
},
channels: {
type: 'array',
items: { type: 'string' },
description: 'TG频道列表指定要搜索的Telegram频道。不指定则使用默认配置的频道'
},
plugins: {
type: 'array',
items: { type: 'string' },
description: '插件列表,指定要使用的搜索插件。不指定则使用所有可用插件'
},
cloud_types: {
type: 'array',
items: {
type: 'string',
enum: [...SUPPORTED_CLOUD_TYPES]
},
description: `网盘类型过滤,只返回指定类型的网盘链接。支持: ${SUPPORTED_CLOUD_TYPES.join(', ')}`
},
source_type: {
type: 'string',
enum: [...SOURCE_TYPES],
default: 'all',
description: '数据来源类型all(全部来源)、tg(仅Telegram)、plugin(仅插件)'
},
force_refresh: {
type: 'boolean',
default: false,
description: '强制刷新缓存,获取最新数据'
},
result_type: {
type: 'string',
enum: [...RESULT_TYPES],
default: 'merge',
description: '结果类型all(返回所有结果)、results(仅返回results)、merge(仅返回按网盘类型分组的结果)'
},
concurrency: {
type: 'number',
description: '并发搜索数量,不指定则自动计算'
},
ext_params: {
type: 'object',
description: '扩展参数,用于传递给插件的自定义参数,如: {"title_en": "Fast and Furious", "is_all": true}'
}
},
required: ['keyword']
}
};
/**
* 执行搜索工具
*/
export async function executeSearchTool(args: unknown, httpClient: HttpClient): Promise<string> {
try {
// 参数验证
const validatedArgs = SearchToolArgsSchema.parse(args);
// 验证网盘类型
let cloudTypes: string[] | undefined;
if (validatedArgs.cloud_types) {
cloudTypes = validateCloudTypes(validatedArgs.cloud_types);
}
// 验证数据来源类型
const sourceType = validateSourceType(validatedArgs.source_type);
// 验证结果类型
const resultType = validateResultType(validatedArgs.result_type);
// 检查后端服务状态
const isHealthy = await httpClient.checkHealth();
if (!isHealthy) {
throw new Error('后端服务未运行,请先启动后端服务。');
}
// 构建搜索请求
const searchRequest: SearchRequest = {
kw: validatedArgs.keyword,
channels: validatedArgs.channels,
plugins: validatedArgs.plugins,
cloud_types: cloudTypes as any,
src: sourceType,
refresh: validatedArgs.force_refresh,
res: resultType,
conc: validatedArgs.concurrency,
ext: validatedArgs.ext_params
};
// 执行搜索
const result = await httpClient.search(searchRequest);
// 格式化返回结果
return formatSearchResult(result, validatedArgs.keyword, resultType);
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
throw new Error(`参数验证失败: ${errorMessages}`);
}
if (error instanceof Error) {
console.error('搜索过程中发生错误:', {
message: error.message,
stack: error.stack,
name: error.name,
timestamp: new Date().toISOString(),
originalArgs: args
});
throw error;
}
throw new Error(`搜索失败: ${String(error)}`);
}
}
/**
* 格式化搜索结果
*/
function formatSearchResult(result: any, keyword: string, resultType: string): string {
const { total, results, merged_by_type } = result;
let output = `🔍 搜索关键词: "${keyword}"\n`;
output += `📊 找到 ${total} 个结果\n\n`;
if (resultType === 'merge' && merged_by_type) {
// 按网盘类型分组显示
output += formatMergedResults(merged_by_type);
} else if (resultType === 'results' && results) {
// 显示详细结果
output += formatDetailedResults(results);
} else if (resultType === 'all') {
// 显示所有信息
if (merged_by_type) {
output += "## 📁 按网盘类型分组\n";
output += formatMergedResults(merged_by_type);
}
if (results && results.length > 0) {
output += "\n## 📋 详细结果\n";
output += formatDetailedResults(results.slice(0, 10)); // 限制显示前10个详细结果
}
}
return output;
}
/**
* 格式化按网盘类型分组的结果
*/
function formatMergedResults(mergedByType: Record<string, any[]>): string {
let output = '';
const typeNames: Record<string, string> = {
'baidu': '🔵 百度网盘',
'aliyun': '🟠 阿里云盘',
'quark': '🟣 夸克网盘',
'tianyi': '🔴 天翼云盘',
'uc': '🟡 UC网盘',
'mobile': '🟢 移动云盘',
'115': '⚫ 115网盘',
'pikpak': '🟤 PikPak',
'xunlei': '🔶 迅雷网盘',
'123': '🟦 123网盘',
'magnet': '🧲 磁力链接',
'ed2k': '🔗 电驴链接',
'others': '📦 其他'
};
for (const [type, links] of Object.entries(mergedByType)) {
if (links && links.length > 0) {
const typeName = typeNames[type] || `📁 ${type}`;
output += `### ${typeName} (${links.length}个)\n`;
links.slice(0, 5).forEach((link: any, index: number) => {
output += `${index + 1}. **${link.note || '未知标题'}**\n`;
output += ` 🔗 链接: ${link.url}\n`;
if (link.password) {
output += ` 🔑 密码: ${link.password}\n`;
}
if (link.source) {
output += ` 📍 来源: ${link.source}\n`;
}
output += ` 📅 时间: ${new Date(link.datetime).toLocaleString('zh-CN')}\n\n`;
});
if (links.length > 5) {
output += ` ... 还有 ${links.length - 5} 个结果\n\n`;
}
}
}
return output;
}
/**
* 格式化详细结果
*/
function formatDetailedResults(results: any[]): string {
let output = '';
results.forEach((result: any, index: number) => {
output += `### ${index + 1}. ${result.title || '未知标题'}\n`;
output += `📺 频道: ${result.channel}\n`;
output += `📅 时间: ${new Date(result.datetime).toLocaleString('zh-CN')}\n`;
if (result.content && result.content !== result.title) {
const content = result.content.length > 200 ? result.content.substring(0, 200) + '...' : result.content;
output += `📝 内容: ${content}\n`;
}
if (result.tags && result.tags.length > 0) {
output += `🏷️ 标签: ${result.tags.join(', ')}\n`;
}
if (result.links && result.links.length > 0) {
output += `🔗 网盘链接:\n`;
result.links.forEach((link: any, linkIndex: number) => {
output += ` ${linkIndex + 1}. [${link.type.toUpperCase()}] ${link.url}`;
if (link.password) {
output += ` (密码: ${link.password})`;
}
output += '\n';
});
}
if (result.images && result.images.length > 0) {
output += `🖼️ 图片: ${result.images.length}\n`;
}
output += '\n';
});
return output;
}

View File

@@ -0,0 +1,131 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { BackendManager } from '../utils/backend-manager.js';
import { HttpClient } from '../utils/http-client.js';
import { Config } from '../utils/config.js';
/**
* 启动后端服务工具定义
*/
export const startBackendTool: Tool = {
name: 'start_backend',
description: '启动PanSou后端服务。如果后端服务未运行此工具将启动它并等待服务完全可用。',
inputSchema: {
type: 'object',
properties: {
force_restart: {
type: 'boolean',
description: '是否强制重启后端服务(即使已在运行)',
default: false
}
},
additionalProperties: false
}
};
/**
* 启动后端服务工具参数接口
*/
interface StartBackendArgs {
force_restart?: boolean;
}
/**
* 执行启动后端服务工具
*/
export async function executeStartBackendTool(
args: unknown,
httpClient?: HttpClient,
config?: Config
): Promise<string> {
try {
// 参数验证
const params = args as StartBackendArgs;
const forceRestart = params?.force_restart || false;
console.log('🚀 启动后端服务工具被调用');
// 如果没有提供依赖项,则创建默认实例
if (!config) {
const { loadConfig } = await import('../utils/config.js');
config = loadConfig();
}
if (!httpClient) {
const { HttpClient } = await import('../utils/http-client.js');
httpClient = new HttpClient(config);
}
// 创建后端管理器
const backendManager = new BackendManager(config, httpClient);
// 检查当前服务状态
httpClient.setSilentMode(true);
const isHealthy = await httpClient.testConnection();
httpClient.setSilentMode(false);
if (isHealthy && !forceRestart) {
return JSON.stringify({
success: true,
message: '后端服务已在运行',
status: 'already_running',
service_url: config.serverUrl
}, null, 2);
}
if (isHealthy && forceRestart) {
console.log('🔄 强制重启后端服务...');
}
console.log('🚀 正在启动后端服务...');
const started = await backendManager.startBackend();
if (!started) {
return JSON.stringify({
success: false,
message: '后端服务启动失败',
status: 'start_failed',
error: '无法启动后端服务,请检查配置和权限'
}, null, 2);
}
// 等待服务完全启动并进行健康检查
console.log('⏳ 等待服务完全启动...');
const maxRetries = 10;
let retries = 0;
while (retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
const healthy = await httpClient.testConnection();
if (healthy) {
console.log('✅ 后端服务启动成功并通过健康检查');
return JSON.stringify({
success: true,
message: '后端服务启动成功',
status: 'started',
service_url: config.serverUrl,
startup_time: `${retries + 1}`
}, null, 2);
}
retries++;
console.log(`🔍 健康检查重试 ${retries}/${maxRetries}...`);
}
return JSON.stringify({
success: false,
message: '后端服务启动超时',
status: 'timeout',
error: '服务启动后未能通过健康检查,可能需要更多时间或存在配置问题'
}, null, 2);
} catch (error) {
console.error('启动后端服务时发生错误:', error);
return JSON.stringify({
success: false,
message: '启动后端服务时发生错误',
status: 'error',
error: error instanceof Error ? error.message : String(error)
}, null, 2);
}
}

View File

@@ -0,0 +1,148 @@
/**
* 活动监控器 - 跟踪MCP工具调用活动
*/
export class ActivityMonitor {
private lastActivityTime: number;
private idleTimeout: number;
private enableIdleShutdown: boolean;
private idleTimer: NodeJS.Timeout | null = null;
private onIdleCallback: (() => void) | null = null;
constructor(idleTimeout: number = 300000, enableIdleShutdown: boolean = true) {
this.lastActivityTime = Date.now();
this.idleTimeout = idleTimeout;
this.enableIdleShutdown = enableIdleShutdown;
}
/**
* 记录活动
*/
recordActivity(): void {
this.lastActivityTime = Date.now();
this.resetIdleTimer();
}
/**
* 获取最后活动时间
*/
getLastActivityTime(): number {
return this.lastActivityTime;
}
/**
* 获取空闲时间(毫秒)
*/
getIdleTime(): number {
return Date.now() - this.lastActivityTime;
}
/**
* 检查是否空闲超时
*/
isIdleTimeout(): boolean {
return this.getIdleTime() >= this.idleTimeout;
}
/**
* 设置空闲回调函数
*/
setOnIdleCallback(callback: () => void): void {
this.onIdleCallback = callback;
this.resetIdleTimer();
}
/**
* 重置空闲计时器
*/
private resetIdleTimer(): void {
if (!this.enableIdleShutdown || !this.onIdleCallback) {
return;
}
// 清除现有计时器
if (this.idleTimer) {
clearTimeout(this.idleTimer);
}
// 设置新的计时器
this.idleTimer = setTimeout(() => {
if (this.onIdleCallback) {
console.log(`[ActivityMonitor] 检测到空闲超时 (${this.idleTimeout}ms),触发空闲回调`);
this.onIdleCallback();
}
}, this.idleTimeout);
}
/**
* 停止监控
*/
stop(): void {
if (this.idleTimer) {
clearTimeout(this.idleTimer);
this.idleTimer = null;
}
this.onIdleCallback = null;
}
/**
* 更新配置
*/
updateConfig(idleTimeout: number, enableIdleShutdown: boolean): void {
this.idleTimeout = idleTimeout;
this.enableIdleShutdown = enableIdleShutdown;
this.resetIdleTimer();
}
/**
* 获取状态信息
*/
getStatus(): {
lastActivityTime: number;
idleTime: number;
idleTimeout: number;
enableIdleShutdown: boolean;
isIdleTimeout: boolean;
} {
return {
lastActivityTime: this.lastActivityTime,
idleTime: this.getIdleTime(),
idleTimeout: this.idleTimeout,
enableIdleShutdown: this.enableIdleShutdown,
isIdleTimeout: this.isIdleTimeout()
};
}
}
// 全局活动监控器实例
let globalActivityMonitor: ActivityMonitor | null = null;
/**
* 获取全局活动监控器实例
*/
export function getActivityMonitor(): ActivityMonitor {
if (!globalActivityMonitor) {
throw new Error('活动监控器未初始化,请先调用 initializeActivityMonitor');
}
return globalActivityMonitor;
}
/**
* 初始化全局活动监控器
*/
export function initializeActivityMonitor(idleTimeout: number, enableIdleShutdown: boolean): ActivityMonitor {
if (globalActivityMonitor) {
globalActivityMonitor.stop();
}
globalActivityMonitor = new ActivityMonitor(idleTimeout, enableIdleShutdown);
return globalActivityMonitor;
}
/**
* 停止全局活动监控器
*/
export function stopActivityMonitor(): void {
if (globalActivityMonitor) {
globalActivityMonitor.stop();
globalActivityMonitor = null;
}
}

View File

@@ -0,0 +1,469 @@
import { spawn, ChildProcess } from 'child_process';
import { promises as fs } from 'fs';
import path from 'path';
import { HttpClient } from './http-client.js';
import { Config } from './config.js';
import { ActivityMonitor } from './activity-monitor.js';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
/**
* 后端服务管理器
* 负责自动启动、停止和监控PanSou Go后端服务
*/
export class BackendManager {
private process: ChildProcess | null = null;
private config: Config;
private httpClient: HttpClient;
private shutdownTimeout: NodeJS.Timeout | null = null;
private isShuttingDown = false;
private readonly SHUTDOWN_DELAY = 5000; // 5秒延迟关闭
private readonly STARTUP_TIMEOUT = 30000; // 30秒启动超时
private readonly HEALTH_CHECK_INTERVAL = 1000; // 1秒健康检查间隔
private activityMonitor: ActivityMonitor | null = null;
constructor(config: Config, httpClient: HttpClient) {
this.config = config;
this.httpClient = httpClient;
// 初始化活动监控器
if (this.config.enableIdleShutdown) {
this.activityMonitor = new ActivityMonitor(
this.config.idleTimeout,
this.config.enableIdleShutdown
);
// 设置空闲监控回调
this.activityMonitor.setOnIdleCallback(async () => {
console.error('⏰ 检测到空闲超时,自动关闭后端服务');
await this.stopBackend();
// 退出整个进程
process.exit(0);
});
console.error(`⏱️ 空闲监控已启用,超时时间: ${this.config.idleTimeout / 1000}`);
}
}
/**
* 检查后端服务是否正在运行
*/
async isBackendRunning(): Promise<boolean> {
try {
return await this.httpClient.testConnection();
} catch (error) {
return false;
}
}
/**
* 智能检测Docker容器状态
*/
private async detectDockerContainer(): Promise<boolean> {
try {
// 检查Docker是否可用
await execAsync('docker --version');
// 检查是否有运行中的pansou容器
const { stdout } = await execAsync('docker ps --format "{{.Names}}" --filter "name=pansou"');
const runningContainers = stdout.trim().split('\n').filter(name => name.includes('pansou'));
if (runningContainers.length > 0) {
console.error(`🐳 检测到运行中的Docker容器: ${runningContainers.join(', ')}`);
return true;
}
return false;
} catch (error) {
// Docker不可用或没有容器运行
return false;
}
}
/**
* 智能检测部署模式
* @returns 'docker' | 'source' | 'unknown'
*/
private async detectDeploymentMode(): Promise<'docker' | 'source' | 'unknown'> {
// 1. 首先检查是否有Docker容器运行
const hasDockerContainer = await this.detectDockerContainer();
if (hasDockerContainer) {
return 'docker';
}
// 2. 检查是否有Go可执行文件
const execPath = await this.findGoExecutable();
if (execPath) {
return 'source';
}
// 3. 检查服务是否已经在运行(可能是手动启动的)
this.httpClient.setSilentMode(true);
const isRunning = await this.isBackendRunning();
this.httpClient.setSilentMode(false);
if (isRunning) {
console.error('✅ 检测到后端服务已在运行(可能是手动启动)');
return 'source'; // 假设是源码模式
}
return 'unknown';
}
/**
* 查找Go可执行文件路径
*/
private async findGoExecutable(): Promise<string | null> {
// 优先使用配置中的项目根目录
const configProjectRoot = this.config.projectRootPath;
const possiblePaths: string[] = [];
// 如果配置了项目根目录,直接在该目录下查找
if (configProjectRoot) {
possiblePaths.push(
path.join(configProjectRoot, 'pansou.exe'),
path.join(configProjectRoot, 'main.exe')
);
} else {
// 仅在没有配置项目根目录时才使用备用路径
possiblePaths.push(
// 当前工作目录
path.join(process.cwd(), 'pansou.exe'),
path.join(process.cwd(), 'main.exe'),
// 上级目录如果MCP在子目录中
path.join(process.cwd(), '..', 'pansou.exe'),
path.join(process.cwd(), '..', 'main.exe')
);
}
console.error('🔍 查找后端可执行文件...');
if (configProjectRoot) {
console.error(`📂 使用配置的项目根目录: ${configProjectRoot}`);
} else {
console.error(`📂 当前工作目录: ${process.cwd()}`);
}
for (const execPath of possiblePaths) {
try {
await fs.access(execPath);
console.error(`✅ 找到可执行文件: ${execPath}`);
return execPath;
} catch {
// 静默跳过未找到的路径
}
}
console.error('❌ 未找到可执行文件');
return null;
}
/**
* 启动后端服务
*/
async startBackend(): Promise<boolean> {
if (this.process) {
console.error('⚠️ 后端服务已在运行中');
return true;
}
// 智能检测部署模式如果未明确配置Docker模式
let effectiveDockerMode = this.config.dockerMode;
if (!effectiveDockerMode) {
console.error('🔍 正在智能检测部署模式...');
const detectedMode = await this.detectDeploymentMode();
switch (detectedMode) {
case 'docker':
console.error('🐳 智能检测使用Docker部署模式');
effectiveDockerMode = true;
break;
case 'source':
console.error('📦 智能检测:使用源码部署模式');
effectiveDockerMode = false;
break;
case 'unknown':
console.error('❓ 无法检测部署模式,使用默认源码模式');
effectiveDockerMode = false;
break;
}
} else {
console.error(`⚙️ 使用配置指定的模式: ${effectiveDockerMode ? 'Docker' : '源码'}`);
}
// Docker模式处理
if (effectiveDockerMode) {
console.error('🐳 Docker模式已启用正在检查后端服务连接...');
// Docker模式下进行重试检查因为容器可能需要时间启动
const maxRetries = 3;
const retryDelay = 2000; // 2秒
this.httpClient.setSilentMode(true);
for (let i = 0; i < maxRetries; i++) {
const isRunning = await this.isBackendRunning();
if (isRunning) {
this.httpClient.setSilentMode(false);
console.error('✅ Docker模式下后端服务连接成功');
return true;
}
if (i < maxRetries - 1) {
console.error(`🔄 连接尝试 ${i + 1}/${maxRetries} 失败,${retryDelay/1000}秒后重试...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
this.httpClient.setSilentMode(false);
console.error('❌ Docker模式下后端服务连接失败');
console.error('请确保Docker容器正在运行');
console.error(' docker-compose up -d');
console.error('或检查Docker容器状态');
console.error(' docker ps');
return false;
}
// 源码模式:首先检查是否已有服务在运行
this.httpClient.setSilentMode(true);
const isRunning = await this.isBackendRunning();
this.httpClient.setSilentMode(false);
if (isRunning) {
console.error('✅ 检测到后端服务已在运行');
return true;
}
// 查找Go可执行文件
const execPath = await this.findGoExecutable();
if (!execPath) {
console.error('❌ 未找到PanSou后端可执行文件');
console.error('如果您使用Docker部署请在MCP配置中设置 DOCKER_MODE=true');
console.error('如果您使用源码部署,请确保在项目根目录下存在以下文件之一:');
console.error(' - pansou.exe / pansou');
console.error(' - main.exe / main');
return false;
}
console.error(`🚀 启动后端服务: ${execPath}`);
try {
// 启动Go服务
this.process = spawn(execPath, [], {
cwd: path.dirname(execPath),
stdio: ['ignore', 'pipe', 'pipe'],
detached: false,
windowsHide: true
});
// 监听进程事件
this.process.on('error', (error) => {
console.error('❌ 后端服务启动失败:', error.message);
console.error('错误详情:', error);
this.process = null;
});
this.process.on('exit', (code, signal) => {
if (!this.isShuttingDown) {
console.error(`⚠️ 后端服务意外退出 (code: ${code}, signal: ${signal})`);
}
this.process = null;
});
// 添加进程启动确认
console.error(`📋 进程PID: ${this.process.pid}`);
console.error(`📂 工作目录: ${path.dirname(execPath)}`);
console.error(`⚙️ 启动参数: ${execPath}`);
// 给进程一点时间启动
await new Promise(resolve => setTimeout(resolve, 1000));
// 捕获输出(用于调试)
if (this.process.stdout) {
this.process.stdout.on('data', (data) => {
console.error('Backend stdout:', data.toString().trim());
});
}
if (this.process.stderr) {
this.process.stderr.on('data', (data) => {
console.error('Backend stderr:', data.toString().trim());
});
}
// 等待服务启动
const started = await this.waitForBackendReady();
if (started) {
console.error('✅ 后端服务启动成功');
// 空闲监控已在构造函数中设置
return true;
} else {
console.error('❌ 后端服务启动超时');
await this.stopBackend();
return false;
}
} catch (error) {
console.error('❌ 启动后端服务时发生错误:', error);
return false;
}
}
/**
* 等待后端服务就绪
*/
private async waitForBackendReady(): Promise<boolean> {
const startTime = Date.now();
// 在等待期间启用静默模式,避免输出网络错误
const originalSilentMode = this.httpClient.isSilentMode();
this.httpClient.setSilentMode(true);
try {
while (Date.now() - startTime < this.STARTUP_TIMEOUT) {
if (await this.isBackendRunning()) {
return true;
}
// 检查进程是否还在运行
if (!this.process || this.process.killed) {
return false;
}
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, this.HEALTH_CHECK_INTERVAL));
}
return false;
} finally {
// 恢复原始静默模式状态
this.httpClient.setSilentMode(originalSilentMode);
}
}
/**
* 停止后端服务
*/
async stopBackend(): Promise<void> {
if (!this.process) {
return;
}
console.error('🛑 正在停止后端服务...');
this.isShuttingDown = true;
try {
// 尝试优雅关闭
this.process.kill('SIGTERM');
// 等待进程退出
await new Promise<void>((resolve) => {
if (!this.process) {
resolve();
return;
}
const timeout = setTimeout(() => {
// 强制杀死进程
if (this.process && !this.process.killed) {
console.error('⚠️ 强制终止后端服务');
this.process.kill('SIGKILL');
}
resolve();
}, 5000);
this.process.on('exit', () => {
clearTimeout(timeout);
resolve();
});
});
console.error('✅ 后端服务已停止');
} catch (error) {
console.error('❌ 停止后端服务时发生错误:', error);
} finally {
this.process = null;
this.isShuttingDown = false;
}
}
/**
* 延迟停止后端服务
*/
scheduleShutdown(): void {
if (this.shutdownTimeout) {
clearTimeout(this.shutdownTimeout);
}
console.error(`⏰ 将在 ${this.SHUTDOWN_DELAY / 1000} 秒后关闭后端服务`);
this.shutdownTimeout = setTimeout(async () => {
await this.stopBackend();
this.shutdownTimeout = null;
}, this.SHUTDOWN_DELAY);
}
/**
* 取消计划的关闭
*/
cancelShutdown(): void {
if (this.shutdownTimeout) {
clearTimeout(this.shutdownTimeout);
this.shutdownTimeout = null;
console.error('⏸️ 取消后端服务关闭计划');
}
}
/**
* 获取后端服务状态
*/
getStatus(): {
processRunning: boolean;
serviceReachable: boolean;
pid?: number;
} {
return {
processRunning: this.process !== null && !this.process.killed,
serviceReachable: false, // 需要异步检查
pid: this.process?.pid
};
}
/**
* 记录活动(重置空闲计时器)
*/
recordActivity(): void {
if (this.activityMonitor) {
this.activityMonitor.recordActivity();
}
}
/**
* 获取活动监控状态
*/
getActivityStatus(): any {
return this.activityMonitor ? this.activityMonitor.getStatus() : null;
}
/**
* 清理资源
*/
async cleanup(): Promise<void> {
this.cancelShutdown();
if (this.activityMonitor) {
this.activityMonitor.stop();
this.activityMonitor = null;
}
await this.stopBackend();
}
}
/**
* 创建后端管理器实例
*/
export function createBackendManager(config: Config, httpClient: HttpClient): BackendManager {
return new BackendManager(config, httpClient);
}

View File

@@ -0,0 +1,70 @@
import { z } from 'zod';
import { ConfigSchema } from './validators.js';
export type Config = z.infer<typeof ConfigSchema>;
/**
* 解析逗号分隔的字符串为数组
*/
function parseCommaSeparated(value: string | undefined): string[] {
if (!value || value.trim() === '') {
return [];
}
return value.split(',').map(item => item.trim()).filter(item => item.length > 0);
}
/**
* 从环境变量加载配置
*/
export function loadConfig(): Config {
const rawConfig = {
serverUrl: process.env.PANSOU_SERVER_URL,
requestTimeout: process.env.REQUEST_TIMEOUT ? parseInt(process.env.REQUEST_TIMEOUT) * 1000 : undefined,
maxResults: process.env.MAX_RESULTS ? parseInt(process.env.MAX_RESULTS) : undefined,
maxConcurrentRequests: process.env.MAX_CONCURRENT_REQUESTS ? parseInt(process.env.MAX_CONCURRENT_REQUESTS) : undefined,
enableCache: process.env.ENABLE_CACHE === 'true',
defaultChannels: parseCommaSeparated(process.env.DEFAULT_CHANNELS),
defaultPlugins: parseCommaSeparated(process.env.DEFAULT_PLUGINS),
defaultCloudTypes: parseCommaSeparated(process.env.DEFAULT_CLOUD_TYPES),
logLevel: process.env.LOG_LEVEL as 'error' | 'warn' | 'info' | 'debug' | undefined,
// 后端服务自动管理配置
autoStartBackend: process.env.AUTO_START_BACKEND !== 'false', // 默认为true除非明确设置为false
backendShutdownDelay: process.env.BACKEND_SHUTDOWN_DELAY ? parseInt(process.env.BACKEND_SHUTDOWN_DELAY) : undefined,
backendStartupTimeout: process.env.BACKEND_STARTUP_TIMEOUT ? parseInt(process.env.BACKEND_STARTUP_TIMEOUT) : undefined,
// 空闲超时配置
idleTimeout: process.env.IDLE_TIMEOUT ? parseInt(process.env.IDLE_TIMEOUT) : undefined,
enableIdleShutdown: process.env.ENABLE_IDLE_SHUTDOWN !== 'false', // 默认为true除非明确设置为false
// 项目根目录路径
projectRootPath: process.env.PROJECT_ROOT_PATH,
// Docker部署模式
dockerMode: process.env.DOCKER_MODE === 'true'
};
// 移除undefined值让zod使用默认值
const cleanConfig = Object.fromEntries(
Object.entries(rawConfig).filter(([_, value]) => value !== undefined)
);
try {
return ConfigSchema.parse(cleanConfig);
} catch (error) {
if (error instanceof z.ZodError) {
console.error('配置验证失败:', error.errors);
throw new Error(`配置验证失败: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`);
}
throw error;
}
}
// 从validators模块重新导出类型和验证函数
export {
SUPPORTED_CLOUD_TYPES,
SOURCE_TYPES,
RESULT_TYPES,
type CloudType,
type SourceType,
type ResultType,
validateCloudTypes,
validateSourceType,
validateResultType
} from './validators.js';

View File

@@ -0,0 +1,286 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { Config, validateCloudTypes } from './config.js';
import { CloudType, SourceType, ResultType } from './config.js';
/**
* 搜索请求参数
*/
export interface SearchRequest {
kw: string; // 搜索关键词
channels?: string[]; // 搜索的频道列表
conc?: number; // 并发搜索数量
refresh?: boolean; // 强制刷新,不使用缓存
res?: ResultType; // 结果类型
src?: SourceType; // 数据来源类型
plugins?: string[]; // 指定搜索的插件列表
cloud_types?: CloudType[]; // 指定返回的网盘类型列表
ext?: Record<string, any>; // 扩展参数
}
/**
* 网盘链接
*/
export interface Link {
type: string;
url: string;
password: string;
}
/**
* 搜索结果项
*/
export interface SearchResult {
message_id: string;
unique_id: string;
channel: string;
datetime: string;
title: string;
content: string;
links: Link[];
tags?: string[];
images?: string[];
}
/**
* 合并后的网盘链接
*/
export interface MergedLink {
url: string;
password: string;
note: string;
datetime: string;
source?: string;
images?: string[];
}
/**
* 按网盘类型分组的合并链接
*/
export type MergedLinks = Record<string, MergedLink[]>;
/**
* 搜索响应数据
*/
export interface SearchResponseData {
total: number;
results?: SearchResult[];
merged_by_type?: MergedLinks;
}
/**
* API响应格式
*/
export interface ApiResponse<T = any> {
code: number;
message: string;
data?: T;
}
/**
* 健康检查响应
*/
export interface HealthResponse {
status: string;
plugins_enabled: boolean;
channels: string[];
channels_count: number;
plugin_count?: number;
plugins?: string[];
}
/**
* HTTP客户端类
*/
export class HttpClient {
private client: AxiosInstance;
private config: Config;
private silentMode: boolean = false;
constructor(config: Config) {
this.config = config;
this.client = axios.create({
baseURL: config.serverUrl,
timeout: config.requestTimeout,
headers: {
'Content-Type': 'application/json',
'User-Agent': 'PanSou-MCP-Server/1.0.0'
}
});
// 请求拦截器
this.client.interceptors.request.use(
(config) => {
if (this.config.logLevel === 'debug') {
console.log(`[HTTP] 请求: ${config.method?.toUpperCase()} ${config.url}`);
if (config.data) {
console.log(`[HTTP] 请求数据:`, config.data);
}
}
return config;
},
(error) => {
console.error('[HTTP] 请求错误:', error);
return Promise.reject(error);
}
);
// 响应拦截器
this.client.interceptors.response.use(
(response) => {
if (this.config.logLevel === 'debug') {
console.log(`[HTTP] 响应: ${response.status} ${response.config.url}`);
}
return response;
},
(error) => {
if (!this.silentMode) {
if (error.response) {
console.error(`[HTTP] 响应错误: ${error.response.status} ${error.response.statusText}`);
if (this.config.logLevel === 'debug') {
console.error('[HTTP] 错误详情:', error.response.data);
}
} else if (error.request) {
console.error('[HTTP] 网络错误: 无法连接到服务器');
} else {
console.error('[HTTP] 请求配置错误:', error.message);
}
}
return Promise.reject(error);
}
);
}
/**
* 搜索网盘资源
*/
async search(params: SearchRequest): Promise<SearchResponseData> {
try {
// 参数验证
if (!params.kw || params.kw.trim() === '') {
throw new Error('搜索关键词不能为空');
}
// 设置默认值
const requestData: SearchRequest = {
kw: params.kw.trim(),
channels: params.channels || this.config.defaultChannels,
conc: params.conc,
refresh: params.refresh || false,
res: params.res || 'merge',
src: params.src || 'all',
plugins: params.plugins || this.config.defaultPlugins,
cloud_types: params.cloud_types ? validateCloudTypes(params.cloud_types.map(String)) : this.config.defaultCloudTypes,
ext: params.ext || {}
};
// 清理空数组
if (requestData.channels && requestData.channels.length === 0) {
delete requestData.channels;
}
if (requestData.plugins && requestData.plugins.length === 0) {
delete requestData.plugins;
}
if (requestData.cloud_types && requestData.cloud_types.length === 0) {
delete requestData.cloud_types;
}
const response: AxiosResponse<ApiResponse<SearchResponseData>> = await this.client.post('/api/search', requestData);
// 兼容不同版本的响应格式源码版本使用200Docker版本使用0
if (response.data.code !== 200 && response.data.code !== 0) {
throw new Error(response.data.message || '搜索请求失败');
}
if (!response.data.data) {
throw new Error('服务器返回数据为空');
}
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.data?.message) {
throw new Error(`搜索失败: ${error.response.data.message}`);
} else if (error.code === 'ECONNREFUSED') {
throw new Error(`无法连接到PanSou服务器 (${this.config.serverUrl})。请确保服务器正在运行。`);
} else if (error.code === 'ETIMEDOUT') {
throw new Error('请求超时,请稍后重试');
} else {
throw new Error(`网络错误: ${error.message}`);
}
}
throw error;
}
}
/**
* 检查服务健康状态
*/
async checkHealth(): Promise<HealthResponse> {
try {
const response: AxiosResponse<HealthResponse> = await this.client.get('/api/health');
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.code === 'ECONNREFUSED') {
throw new Error(`无法连接到PanSou服务器 (${this.config.serverUrl})。请确保服务器正在运行。`);
} else if (error.code === 'ETIMEDOUT') {
throw new Error('健康检查超时');
} else {
throw new Error(`健康检查失败: ${error.message}`);
}
}
throw error;
}
}
/**
* 测试连接
*/
async testConnection(): Promise<boolean> {
try {
await this.checkHealth();
return true;
} catch (error) {
return false;
}
}
/**
* 获取服务器URL
*/
getServerUrl(): string {
return this.config.serverUrl;
}
/**
* 更新配置
*/
updateConfig(newConfig: Partial<Config>): void {
this.config = { ...this.config, ...newConfig };
// 更新axios实例配置
this.client.defaults.baseURL = this.config.serverUrl;
this.client.defaults.timeout = this.config.requestTimeout;
}
/**
* 设置静默模式
*/
setSilentMode(silent: boolean): void {
this.silentMode = silent;
}
/**
* 获取静默模式状态
*/
isSilentMode(): boolean {
return this.silentMode;
}
}
/**
* 创建HTTP客户端实例
*/
export function createHttpClient(config: Config): HttpClient {
return new HttpClient(config);
}

View File

@@ -0,0 +1,102 @@
import { z } from 'zod';
/**
* 支持的网盘类型列表
*/
export const SUPPORTED_CLOUD_TYPES = [
'baidu', // 百度网盘
'aliyun', // 阿里云盘
'quark', // 夸克网盘
'tianyi', // 天翼云盘
'uc', // UC网盘
'mobile', // 移动云盘
'115', // 115网盘
'pikpak', // PikPak
'xunlei', // 迅雷网盘
'123', // 123网盘
'magnet', // 磁力链接
'ed2k', // 电驴链接
'others' // 其他
] as const;
export type CloudType = typeof SUPPORTED_CLOUD_TYPES[number];
/**
* 支持的数据来源类型
*/
export const SOURCE_TYPES = ['all', 'tg', 'plugin'] as const;
export type SourceType = typeof SOURCE_TYPES[number];
/**
* 支持的结果类型
*/
export const RESULT_TYPES = ['all', 'results', 'merge'] as const;
export type ResultType = typeof RESULT_TYPES[number];
/**
* 配置验证模式
*/
export const ConfigSchema = z.object({
serverUrl: z.string().url().default('http://localhost:8888'),
requestTimeout: z.number().positive().default(30000),
maxResults: z.number().positive().default(100),
maxConcurrentRequests: z.number().positive().default(5),
enableCache: z.boolean().default(false),
defaultChannels: z.array(z.string()).default([]),
defaultPlugins: z.array(z.string()).default([]),
defaultCloudTypes: z.array(z.enum(['baidu', 'aliyun', 'quark', 'tianyi', 'uc', 'mobile', '115', 'pikpak', 'xunlei', '123', 'magnet', 'ed2k', 'others'])).default([]),
logLevel: z.enum(['error', 'warn', 'info', 'debug']).default('info'),
// 后端服务自动管理配置
autoStartBackend: z.boolean().default(true),
backendShutdownDelay: z.number().positive().default(5000),
backendStartupTimeout: z.number().positive().default(30000),
// 空闲超时配置(毫秒)
idleTimeout: z.number().positive().default(300000), // 默认5分钟
enableIdleShutdown: z.boolean().default(true),
// 项目根目录路径
projectRootPath: z.string().optional(),
// Docker部署模式当设置为true时不会尝试启动本地进程
dockerMode: z.boolean().default(false)
});
/**
* 验证网盘类型
*/
export function validateCloudTypes(cloudTypes: string[]): CloudType[] {
const validTypes: CloudType[] = [];
const invalidTypes: string[] = [];
for (const type of cloudTypes) {
if (SUPPORTED_CLOUD_TYPES.includes(type as CloudType)) {
validTypes.push(type as CloudType);
} else {
invalidTypes.push(type);
}
}
if (invalidTypes.length > 0) {
throw new Error(`不支持的网盘类型: ${invalidTypes.join(', ')}。支持的类型: ${SUPPORTED_CLOUD_TYPES.join(', ')}`);
}
return validTypes;
}
/**
* 验证数据来源类型
*/
export function validateSourceType(sourceType: string): SourceType {
if (!SOURCE_TYPES.includes(sourceType as SourceType)) {
throw new Error(`不支持的数据来源类型: ${sourceType}。支持的类型: ${SOURCE_TYPES.join(', ')}`);
}
return sourceType as SourceType;
}
/**
* 验证结果类型
*/
export function validateResultType(resultType: string): ResultType {
if (!RESULT_TYPES.includes(resultType as ResultType)) {
throw new Error(`不支持的结果类型: ${resultType}。支持的类型: ${RESULT_TYPES.join(', ')}`);
}
return resultType as ResultType;
}

41
typescript/tsconfig.json Normal file
View File

@@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
}