Compare commits

..

2 Commits

Author SHA1 Message Date
d71458669a ```
docs: 删除项目文档和配置文件

移除主 README.md 文件,包含项目概述、安装指南和配置示例

移除 lzwcai_mcp_sqlexecutor 模块的完整文档、配置文件和初始化文件

删除 businessQueries.json 配置模板和 .python-version 版本指定文件

更新 .gitignore 文件以清理 Python 缓存文件
```
2026-06-11 18:56:15 +08:00
0d48341b73 docs(agile-db): 移除过时的 AgileDB 技能实现计划文档
移除 .kilo/plans/lzwcai-agile-db-skill.md 文件,该文件包含
AgileDB 数据库操作技能的详细实现计划,包括数据源管理、
数据库与表管理、表数据操作和 SQL 执行等场景化工作流指导。
同时移除对应的技能压缩包文件。
2026-06-11 18:55:33 +08:00
376 changed files with 0 additions and 48852 deletions

View File

@@ -1,451 +0,0 @@
# lzwcai-agile-db Skill 实现计划
## 目标
创建一个 Kilo skill 文件,为 AI Agent 提供 AgileDB 数据库操作的场景化工作流指导。
## Skill 文件路径
`.kilo/skills/lzwcai-agile-db/SKILL.md`
## Skill 内容
以下为完整的 SKILL.md 内容:
```markdown
# lzwcai-agile-db
AgileDB 数据库操作技能。为 AI Agent 提供数据源浏览、表数据 CRUD、SQL 执行和 AI 生成表结构的场景化工作流。
## 可用 MCP 工具
本 skill 基于 `lzwcai_mcp_agile_db` MCP Server提供以下工具
### 数据源管理
| 工具 | 功能 |
|------|------|
| `list_datasources` | 获取数据源列表 |
| `get_datasource_detail` | 获取数据源详情(含数据库、表结构) |
| `create_datasource` | 创建外部数据源 |
| `update_datasource` | 更新数据源 |
| `toggle_datasource_status` | 启用/停用数据源 |
| `delete_datasource` | 删除数据源 |
### 数据库与表管理
| 工具 | 功能 |
|------|------|
| `list_databases` | 获取数据源下的数据库列表 |
| `list_tables` | 获取数据库下的表列表 |
| `get_table_detail` | 获取表结构详情(字段、类型、主键) |
| `create_table` | 创建新表 |
| `alter_table` | 修改表结构 |
| `generate_table_by_description` | AI 根据自然语言生成表结构 |
### 表数据操作
| 工具 | 功能 |
|------|------|
| `query_table_data` | 查询表数据(分页) |
| `insert_table_row` | 插入一行数据 |
| `update_table_row` | 更新一行数据 |
| `delete_table_rows` | 删除数据行(按主键) |
| `export_table_excel` | 导出表数据为 Excel |
### SQL 执行
| 工具 | 功能 |
|------|------|
| `execute_sql` | 执行原生 SQL 查询 |
---
## 场景 1浏览数据源
当用户想要了解有哪些数据源、数据库或表时,使用此流程。
### 工作流程
```
用户请求: "有哪些数据源?" / "看看 XX 数据源有哪些表?"
1. 调用 list_datasources()
2. 展示数据源列表(名称、类型、状态、数据库数、表数)
3. 用户选择数据源后,调用 get_datasource_detail(datasourceId="xx")
4. 展示数据库列表和实时表结构
```
### 示例
**用户**: "帮我看看有哪些数据源"
```
调用: list_datasources()
返回: [{id: "1", name: "订单系统", type: "builtin", status: 0, databases: 3, tables: 12}, ...]
回复: 共找到 3 个数据源:
- 订单系统内置运行中3 个库12 张表)
- 用户中心(外部 MySQL运行中1 个库5 张表)
- 报表系统(外部 PostgreSQL已停止
```
**用户**: "看看订单系统有哪些表"
```
调用: get_datasource_detail(datasourceId="1")
返回: {datasource: {...}, databases: [{name: "order_db", tables: [...]}]}
回复: 订单系统包含以下数据库:
order_db:
- orders (订单表, 15 个字段)
- order_items (订单明细, 10 个字段)
- users (用户表, 8 个字段)
```
### 注意事项
- 数据源状态:`0`=运行中,`1`=已停止
- 数据源类型:`builtin`=内置,`external`=外部
- 如果用户只说"查一下数据库",先调用 `list_datasources()` 再引导选择
---
## 场景 2查询表数据
当用户想要查看某张表的具体数据记录时,使用此流程。
### 工作流程
```
用户请求: "查一下 users 表的数据" / "看看前 20 条订单"
1. 如果不知道 tableId先浏览数据源找到目标表
2. 调用 query_table_data(tableId="xx", pageNum=1, pageSize=10, target="prod")
3. 展示数据(表格形式,包含字段名和值)
4. 如果数据量大,提示用户可翻页或调整 pageSize
```
### 示例
**用户**: "查一下 users 表前 10 条数据"
```
调用: query_table_data(tableId="5", pageNum=1, pageSize=10)
返回: {
columns: [{name: "id", type: "INTEGER"}, {name: "username", type: "VARCHAR"}, ...],
data: [{id: 1, username: "admin", email: "admin@test.com"}, ...],
total: 156
}
回复: users 表共 156 条记录,当前显示第 1-10 条:
| id | username | email | created_at |
|----|----------|-----------------|---------------------|
| 1 | admin | admin@test.com | 2024-01-15 10:30:00 |
| 2 | user1 | user1@test.com | 2024-01-16 14:20:00 |
...
```
### 注意事项
- `target` 参数:`prod`=生产环境,`test`=测试环境,默认 `prod`
- 如果用户未指定数量,默认 `pageSize=10`
- 数据返回格式为对象数组(已自动转换表头)
- 如果用户说"翻页",增加 `pageNum` 参数
---
## 场景 3执行 SQL
当用户需要执行自定义 SQL 查询时,使用此流程。
### 工作流程
```
用户请求: "帮我执行 SQL..." / "查一下订单数大于 100 的用户"
1. 如果用户直接提供 SQL直接使用
如果用户提供自然语言需求,先转换为 SQL
2. 调用 execute_sql(datasourceId="xx", executableSql="SELECT ...")
3. 展示查询结果
4. 如果 SQL 执行失败,展示错误信息并建议修正
```
### 示例
**用户**: "统计每个地区的订单数量"
```
转换为 SQL: SELECT region, COUNT(*) as order_count FROM orders GROUP BY region ORDER BY order_count DESC
调用: execute_sql(
datasourceId="1",
executableSql="SELECT region, COUNT(*) as order_count FROM orders GROUP BY region ORDER BY order_count DESC"
)
返回: {
columns: [{name: "region"}, {name: "order_count"}],
data: [{region: "华东", order_count: 1250}, {region: "华南", order_count: 980}, ...]
}
回复: 各地区订单统计:
| 地区 | 订单数 |
|------|--------|
| 华东 | 1,250 |
| 华南 | 980 |
| 华北 | 756 |
```
### 注意事项
- `datasourceId` 必须提供,如果用户未指定,先询问或使用最近使用的数据源
- `sqlTemplate` 可选,用于模板化查询
- `parameters` 可选,用于参数化查询(防止 SQL 注入)
- 执行危险操作DELETE/DROP/UPDATE**必须**向用户确认
- 如果查询结果超过 100 行,建议用户使用 `query_table_data` 代替
---
## 场景 4增删改数据
当用户需要修改表中的数据时,使用此流程。
### 4.1 插入数据
```
用户请求: "新增一个用户,用户名是 test_user"
1. 先调用 get_table_detail(tableId="xx") 了解表结构
2. 确认必填字段(非空字段、无默认值字段)
3. 调用 insert_table_row(tableId="xx", data={...})
4. 确认插入成功
```
**示例**:
**用户**: "新增一个用户,用户名 test_user邮箱 test@test.com"
```
调用: get_table_detail(tableId="5")
返回: {columns: [{name: "id", isPrimaryKey: true, isAutoIncrement: true}, {name: "username", isNullable: false}, ...]}
调用: insert_table_row(
tableId="5",
data={"username": "test_user", "email": "test@test.com"}
)
回复: 已成功插入用户 test_user (ID 自动生成)
```
### 4.2 更新数据
```
用户请求: "把用户 test_user 的邮箱改成 new@test.com"
1. 确认主键字段和要更新的值
2. 调用 update_table_row(tableId="xx", primaryKey={主键}, data={要更新的字段})
3. 确认更新成功
```
**示例**:
**用户**: "把 ID 为 5 的用户邮箱改成 new@test.com"
```
调用: update_table_row(
tableId="5",
primaryKey={"id": 5},
data={"email": "new@test.com"}
)
回复: 已更新用户 ID=5 的邮箱为 new@test.com
```
### 4.3 删除数据
```
用户请求: "删除用户 test_user"
1. ⚠️ 危险操作!先确认删除范围和影响
2. 向用户展示将要删除的数据,请求确认
3. 用户确认后,调用 delete_table_rows(tableId="xx", primaryKeys=[{主键}])
4. 确认删除成功
```
**示例**:
**用户**: "删除 ID 为 10 的订单"
```
回复: ⚠️ 确认要删除以下记录吗?
订单 ID=10, 订单号=ORD-2024-0010, 金额=¥1,250.00
此操作不可恢复。请回复"确认删除"继续。
用户: "确认删除"
调用: delete_table_rows(
tableId="3",
primaryKeys=[{"id": 10}]
)
回复: 已成功删除订单 ID=10
```
### 注意事项
- **删除操作必须二次确认**
- `primaryKey` 必须是对象格式,如 `{"id": 1}`
- `data` 只包含要更新的字段,不需要提供全部字段
- 插入数据时,自增主键不需要提供
- 如果操作涉及多行,使用批量操作或循环调用
---
## 场景 5AI 生成表结构
当用户需要创建新表但不知道如何设计表结构时,使用 AI 辅助生成。
### 工作流程
```
用户请求: "我需要一个用户表" / "帮我设计一个订单系统的表结构"
1. 调用 generate_table_by_description(requirement="用户描述")
2. 展示 AI 生成的表结构(表名、字段、类型、注释)
3. 用户确认后,调用 create_table(connectionId="xx", databaseName="xx", tableName="xx", columns=[...])
4. 确认创建成功
```
### 示例
**用户**: "帮我设计一个商品表,包含名称、价格、库存、分类"
```
调用: generate_table_by_description(
requirement="我需要一个商品表,包含商品名称、价格、库存、分类,用于电商系统"
)
返回: {
tableName: "products",
tableComment: "商品表",
columns: [
{columnName: "id", columnType: "SERIAL", isPrimaryKey: true, isAutoIncrement: true, columnComment: "主键ID"},
{columnName: "name", columnType: "VARCHAR", columnLength: 200, isNullable: false, columnComment: "商品名称"},
{columnName: "price", columnType: "DECIMAL", columnLength: 10, columnComment: "价格"},
{columnName: "stock", columnType: "INTEGER", columnComment: "库存数量"},
{columnName: "category_id", columnType: "INTEGER", columnComment: "分类ID"},
{columnName: "created_at", columnType: "TIMESTAMP", defaultValue: "NOW()", columnComment: "创建时间"}
]
}
回复: AI 已生成表结构设计:
表名: products (商品表)
字段:
- id (SERIAL, 主键, 自增) - 主键ID
- name (VARCHAR(200), 非空) - 商品名称
- price (DECIMAL(10)) - 价格
- stock (INTEGER) - 库存数量
- category_id (INTEGER) - 分类ID
- created_at (TIMESTAMP, 默认NOW()) - 创建时间
是否需要在此基础上修改或直接创建?
用户: "直接创建到 order_db 数据库"
调用: create_table(
connectionId="1",
databaseName="order_db",
tableName="products",
tableComment="商品表",
columns=[...] // 使用 AI 生成的 columns
)
回复: 已成功创建表 products (商品表)
```
### 注意事项
- `requirement` 参数应尽可能详细,包含业务场景和字段需求
- AI 生成的表结构可能需要用户调整(如字段长度、类型)
- 创建表前需要确认 `connectionId` 和 `databaseName`
- 常用字段类型:`VARCHAR`, `INTEGER`, `SERIAL`(自增主键), `DECIMAL`, `TIMESTAMP`, `TEXT`, `BOOLEAN`
- 如果用户描述模糊,先引导用户补充细节
---
## 最佳实践
### 1. 始终确认环境
- 默认使用 `prod`(生产环境)
- 如果用户提到"测试"或"测试环境",使用 `target="test"`
- 执行危险操作前,明确告知用户当前环境
### 2. 提供上下文
- 展示数据时,包含字段名和类型
- 执行操作后,告知影响的行数或具体变化
- 错误时,提供清晰的错误信息和建议
### 3. 分步引导
- 如果用户请求不完整(如未指定数据源),先引导补充信息
- 复杂操作分步执行,每步确认后继续
- 对于多表操作,先展示整体计划
### 4. 数据展示
- 表格数据使用 Markdown 表格格式
- 长文本截断显示(最多 100 字符)
- 数字格式化(千位分隔符、货币符号)
- 时间格式化为可读格式
### 5. 错误处理
- API 错误:展示错误信息,建议重试或检查参数
- SQL 错误:展示 SQL 错误位置,建议修正
- 连接错误:检查数据源状态,建议启用或重新配置
---
## 常用参数参考
### 数据源类型
- `builtin` - 内置 PostgreSQL
- `external` - 外部数据库MySQL/PostgreSQL/Oracle/SQL Server 等)
### 数据源状态
- `0` - 运行中
- `1` - 已停止
### 环境
- `prod` - 生产环境(默认)
- `test` - 测试环境
### 常用字段类型
| 类型 | 用途 | 示例 |
|------|------|------|
| `SERIAL` | 自增主键 | `id SERIAL PRIMARY KEY` |
| `VARCHAR(n)` | 短文本 | `username VARCHAR(50)` |
| `TEXT` | 长文本 | `description TEXT` |
| `INTEGER` | 整数 | `stock INTEGER` |
| `DECIMAL(m,n)` | 精确小数 | `price DECIMAL(10,2)` |
| `TIMESTAMP` | 时间戳 | `created_at TIMESTAMP` |
| `BOOLEAN` | 布尔值 | `is_active BOOLEAN` |
---
## 快速开始
如果用户说"帮我查一下数据库",按以下步骤操作:
1. 调用 `list_datasources()` 获取数据源列表
2. 展示列表,让用户选择或默认第一个运行中的数据源
3. 调用 `get_datasource_detail(datasourceId="xx")` 获取数据库和表信息
4. 引导用户选择要操作的表
5. 根据用户意图调用相应的工具(查询/执行 SQL/增删改)
```
## 实施步骤
1. 创建目录 `.kilo/skills/lzwcai-agile-db/`
2. 创建文件 `SKILL.md` 并写入上述内容
3. 验证 skill 文件格式正确
## 验证
创建完成后skill 应该可以被 Kilo 自动识别和加载。

Binary file not shown.

View File

@@ -1,893 +0,0 @@
---
name: lzwcai-agile-db
description: AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据库操作工作流指导,适合零基础用户。
version: 0.2.0
---
# lzwcai-agile-db
AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据库操作工作流指导,适合零基础用户。
## 完整工具清单33 个工具)
本 skill 基于 `lzwcai_mcp_agile_db` MCP Server共提供 33 个工具,分为 9 大类:
### 一、数据源管理6 个工具)
| 工具 | 功能 | 危险等级 |
|------|------|----------|
| `list_datasources` | 获取数据源列表 | 安全 |
| `get_datasource_detail` | 获取数据源详情(含数据库、表结构) | 安全 |
| `create_datasource` | 创建外部数据源连接 | 安全 |
| `update_datasource` | 更新数据源连接信息 | 中等 |
| `toggle_datasource_status` | 启用/停用数据源 | 中等 |
| `delete_datasource` | 删除数据源 | **危险** |
### 二、数据库与表管理6 个工具)
| 工具 | 功能 | 危险等级 |
|------|------|----------|
| `list_databases` | 获取数据源下的数据库列表 | 安全 |
| `list_tables` | 获取数据源下的表列表 | 安全 |
| `get_table_detail` | 获取表结构详情(字段、类型、主键) | 安全 |
| `create_table` | 创建新表 | **危险** |
| `alter_table` | 修改表结构 | **危险** |
| `generate_table_by_description` | AI 根据自然语言生成表结构 | 安全(仅生成,不创建) |
### 三、表数据 CRUD5 个工具)
| 工具 | 功能 | 危险等级 |
|------|------|----------|
| `query_table_data` | 查询表数据(分页) | 安全 |
| `insert_table_row` | 插入一行数据 | 中等 |
| `update_table_row` | 更新一行数据 | 中等 |
| `delete_table_rows` | 删除数据行(按主键) | **危险** |
| `export_table_excel` | 导出表数据为 Excelbase64 | 安全 |
### 四、SQL 执行1 个工具)
| 工具 | 功能 | 危险等级 |
|------|------|----------|
| `execute_sql` | 执行原生 SQL 查询 | 中等/危险 |
### 五、数据导入2 个工具)
| 工具 | 功能 | 危险等级 |
|------|------|----------|
| `preview_import_data` | 上传 Excel 文件AI 智能识别并预览 | 安全 |
| `confirm_import_data` | 确认导入 AI 识别后的数据 | **危险** |
### 六、表订阅1 个工具)
| 工具 | 功能 | 危险等级 |
|------|------|----------|
| `toggle_table_subscription` | 切换表的订阅状态 | 中等 |
### 七、API 密钥管理6 个工具)
| 工具 | 功能 | 危险等级 |
|------|------|----------|
| `list_api_keys` | 获取 API 密钥列表 | 安全 |
| `create_api_key` | 创建新的 API 密钥 | 中等 |
| `toggle_api_key_status` | 启用/禁用 API 密钥 | 中等 |
| `delete_api_key` | 删除 API 密钥 | **危险** |
| `get_api_key_permissions` | 查看指定密钥的权限配置 | 安全 |
| `grant_api_key_permissions` | 批量为 API 密钥授予权限 | **危险** |
### 八、技能与工具管理6 个工具)
| 工具 | 功能 | 危险等级 |
|------|------|----------|
| `get_skill_by_datasource` | 根据数据源获取技能信息 | 安全 |
| `get_skill_tools` | 获取技能下的工具列表 | 安全 |
| `create_skill` | 为数据源创建技能 | 中等 |
| `create_sql_tool` | 将 SQL 查询创建为可复用工具 | 中等 |
| `delete_skill_tool` | 删除技能下的工具 | **危险** |
| `update_skill_config` | 更新技能配置 | 中等 |
---
## 核心概念说明
### 数据源是什么?
数据源 = 一个数据库连接。它可以是:
- `builtin`:内置 PostgreSQL 数据库(系统自带)
- `external`外部数据库MySQL、PostgreSQL、Oracle、SQL Server、达梦等
### 数据源状态
- `0` = 运行中(正常)
- `1` = 已停止(不可用)
### 环境参数 `target`
- `prod` = 生产环境(正式数据,默认值)
- `test` = 测试环境(测试数据)
### 主键 `primaryKey`
主键是唯一标识一行数据的字段。例如 `{"id": 1}` 表示删除/更新 id=1 的那行数据。
### `connectionId` vs `datasourceId`
- `datasourceId`:用于数据源列表、数据库列表、表列表等浏览类操作
- `connectionId`:用于创建表、修改表、数据导入等需要直接操作连接的操作
- 两者通常指向同一个东西,但 API 设计不同,请按照工具说明使用
---
## ⚠️ 安全确认原则(必须遵守)
### 一、执行工具前的风险评估
当执行以下类型的操作时,**必须先询问用户确认**,不得擅作主张:
1. **删除操作**:删除数据源、删除表数据、删除 API 密钥、删除技能工具等
- 说明:此操作不可恢复,数据将永久丢失
2. **泄密风险操作**:导出包含敏感数据的表、创建 API 密钥、查看密钥详情等
- 说明:可能导致敏感信息泄露,需确认用户授权
3. **政治敏感操作**:涉及政治相关数据的查询、修改、删除等
- 说明:可能涉及合规风险,需确认用户意图
4. **疑似违规内容**:涉及色情、暴力、违法等内容的操作
- 说明:可能违反法律法规,必须拒绝执行并告知用户
#### 确认格式
执行上述操作前,必须使用以下格式向用户确认:
```
⚠️ 安全提醒:此操作存在 [具体风险类型] 风险。
具体说明:[说明可能的后果]
请确认是否继续?(回复"确认"继续,或取消操作)
```
**只有在用户明确确认后才能继续执行。**
---
### 二、多步骤场景必须逐步确认
当完成任务需要调用多个工具时(如:先查询表结构 → 插入数据 → 确认结果),**不得一次性自动连续执行**
1. **每完成一步后暂停**,向用户展示当前步骤的结果
2. **询问用户是否继续下一步**,等待确认后再执行
3. **不要假设用户的意图**,即使用户的请求看似明确,也需要分步确认
**示例**
```
用户:"帮我新增一个用户"
❌ 错误做法:自动调用 get_table_detail → insert_table_row → 返回结果(一次性执行完)
✅ 正确做法:
1. 调用 get_table_detail 了解表结构
2. 展示必填字段清单,询问用户:"请提供以下必填字段的值:..."
3. 用户回复后,展示将要插入的数据预览
4. 询问:"确认插入以上数据?"
5. 用户确认后才调用 insert_table_row
```
---
### 三、遇到多项选择时必须询问用户
当执行任务过程中遇到需要选择的场景时,**AI 不得擅自做主选择**
1. **列出所有可选方案**,说明每个方案的优缺点或适用场景
2. **等待用户明确选择**,不要猜测用户意图
3. **不要默认选择第一个**或看似最合理的选项
#### 常见需要询问的选择场景:
- 有多个数据源可选时
- 有多个数据库可选时
- 有多个表可选时
- 有多种操作方式可选时(如:用 SQL 查询 vs 用表数据查询工具)
- 有多种字段类型可选时
- 用户描述模糊,存在多种理解方式时
**示例**
```
用户:"查一下订单数据"
❌ 错误做法:直接选择第一个数据源和第一个订单表进行查询
✅ 正确做法:
"找到以下数据源包含订单相关表:
1. HMD产品 → order_db → orders 表156 条记录)
2. 测试数据源 → test_db → test_orders 表10 条记录)
请问您想查询哪个?"
```
---
## 场景 1浏览数据源新手入门第一步
当用户想了解系统里有哪些数据库或表时,使用此流程。
### 工作流程
```
用户请求: "有哪些数据源?" / "看看 XX 数据源有哪些表?" / "帮我查一下数据库"
1. 调用 list_datasources()
2. 展示数据源列表(名称、类型、状态、数据库数、表数)
3. 用户选择数据源后,调用 get_datasource_detail(datasourceId="xx")
4. 展示数据库列表和实时表结构
```
### 示例
**用户**: "帮我看看有哪些数据源"
```
调用: list_datasources()
返回: {
"total": 14,
"rows": [
{"id": "58", "datasourceName": "HMD产品", "host": "host.docker.internal", "port": 5432, "status": 0, ...},
...
]
}
回复: 共找到 14 个数据源:
1. HMD产品PostgreSQL, host.docker.internal:5432, 运行中)
2. ...
请告诉我你想看哪个数据源?
```
**用户**: "看看 HMD 产品有哪些表"
```
调用: get_datasource_detail(datasourceId="58")
返回: {
"detail": {...},
"config": {...},
"structure": {
"databases": [
{"name": "order_db", "tables": [
{"tableName": "orders", "columns": [...]},
...
]}
]
}
}
回复: HMD产品 数据源包含以下数据库和表:
order_db:
- orders (订单表, 15 个字段)
- users (用户表, 8 个字段)
```
### 注意事项
- 数据源类型为 `builtin` 表示系统内置数据库类型为具体的数据库引擎名称mysql、postgresql 等)表示外部数据源
- 如果用户只说"查一下数据库",先调用 `list_datasources()` 再引导选择
- 已停止的数据源status=1无法访问其结构
---
## 场景 2查询表数据
当用户想要查看某张表的具体数据记录时,使用此流程。
### 工作流程
```
用户请求: "查一下 users 表的数据" / "看看前 20 条订单"
1. 如果不知道 tableId先浏览数据源找到目标表
2. 调用 query_table_data(tableId="xx", pageNum=1, pageSize=10, target="prod")
3. 展示数据(表格形式,包含字段名和值)
4. 如果数据量大,提示用户可翻页或调整 pageSize
```
### 示例
**用户**: "查一下 users 表前 10 条数据"
```
调用: query_table_data(tableId="5", pageNum=1, pageSize=10)
返回: {
"columns": [{"name": "id", "type": "INTEGER"}, {"name": "username", "type": "VARCHAR"}, ...],
"data": [{"id": 1, "username": "admin", "email": "admin@test.com"}, ...],
"total": 156
}
回复: users 表共 156 条记录,当前显示第 1-10 条:
| id | username | email | created_at |
|----|----------|-----------------|---------------------|
| 1 | admin | admin@test.com | 2024-01-15 10:30:00 |
| 2 | user1 | user1@test.com | 2024-01-16 14:20:00 |
...
```
### 注意事项
- `target` 参数:`prod`=生产环境(默认),`test`=测试环境
- 如果用户未指定数量,默认 `pageSize=10`
- 如果用户说"翻页",增加 `pageNum` 参数
- 如果用户想看更多数据,可以增大 `pageSize`(最大根据 API 限制)
- 数据返回格式为对象数组
---
## 场景 3执行 SQL
当用户需要执行自定义 SQL 查询时,使用此流程。
### 工作流程
```
用户请求: "帮我执行 SQL..." / "查一下订单数大于 100 的用户"
1. 如果用户直接提供 SQL直接使用
如果用户提供自然语言需求,先转换为 SQL
2. 调用 execute_sql(datasourceId="xx", executableSql="SELECT ...")
3. 展示查询结果
4. 如果 SQL 执行失败,展示错误信息并建议修正
```
### 示例
**用户**: "统计每个地区的订单数量"
```
转换为 SQL: SELECT region, COUNT(*) as order_count FROM orders GROUP BY region ORDER BY order_count DESC
调用: execute_sql(
datasourceId="58",
executableSql="SELECT region, COUNT(*) as order_count FROM orders GROUP BY region ORDER BY order_count DESC"
)
返回: {
"columns": [{"name": "region"}, {"name": "order_count"}],
"data": [{"region": "华东", "order_count": 1250}, {"region": "华南", "order_count": 980}, ...]
}
回复: 各地区订单统计:
| 地区 | 订单数 |
|------|--------|
| 华东 | 1,250 |
| 华南 | 980 |
| 华北 | 756 |
```
### 注意事项
- `datasourceId` 必须提供,如果用户未指定,先询问
- `sqlTemplate` 可选,用于模板化查询
- `parameters` 可选,用于参数化查询(防止 SQL 注入)
- **执行危险操作DELETE/DROP/TRUNCATE必须向用户确认**
- 如果查询结果超过 100 行,建议用户使用 `query_table_data` 代替
- 对于只读查询SELECT可以直接执行对于写操作INSERT/UPDATE/DELETE必须二次确认
---
## 场景 4增删改数据
当用户需要修改表中的数据时,使用此流程。
### 4.1 插入数据
```
用户请求: "新增一个用户,用户名是 test_user"
1. 先调用 get_table_detail(tableId="xx") 了解表结构
2. 确认必填字段(非空字段、无默认值字段)
3. 调用 insert_table_row(tableId="xx", data={...})
4. 确认插入成功
```
**示例**:
**用户**: "新增一个用户,用户名 test_user邮箱 test@test.com"
```
调用: get_table_detail(tableId="5")
返回: {columns: [{name: "id", isPrimaryKey: true, isAutoIncrement: true}, {name: "username", isNullable: false}, ...]}
调用: insert_table_row(
tableId="5",
data={"username": "test_user", "email": "test@test.com"}
)
回复: 已成功插入用户 test_user (ID 自动生成)
```
### 4.2 更新数据
```
用户请求: "把用户 test_user 的邮箱改成 new@test.com"
1. 确认主键字段和要更新的值
2. 调用 update_table_row(tableId="xx", primaryKey={主键}, data={要更新的字段})
3. 确认更新成功
```
**示例**:
**用户**: "把 ID 为 5 的用户邮箱改成 new@test.com"
```
调用: update_table_row(
tableId="5",
primaryKey={"id": 5},
data={"email": "new@test.com"}
)
回复: 已更新用户 ID=5 的邮箱为 new@test.com
```
### 4.3 删除数据
```
用户请求: "删除用户 test_user"
1. ⚠️ 安全提醒:此操作存在删除风险。
说明:数据将永久删除,不可恢复。
请确认是否继续?
2. ⚠️ 危险操作!先确认删除范围和影响
3. 向用户展示将要删除的数据,请求确认
4. 用户确认后,调用 delete_table_rows(tableId="xx", primaryKeys=[{主键}])
5. 确认删除成功
```
**示例**:
**用户**: "删除 ID 为 10 的订单"
```
回复: ⚠️ 确认要删除以下记录吗?
订单 ID=10, 订单号=ORD-2024-0010, 金额=¥1,250.00
此操作不可恢复。请回复"确认删除"继续。
用户: "确认删除"
调用: delete_table_rows(
tableId="3",
primaryKeys=[{"id": 10}]
)
回复: 已成功删除订单 ID=10
```
**注意:如果删除内容涉及敏感数据(如用户隐私、政治相关内容、疑似违规内容),必须额外说明后果并再次确认。**
### 4.4 导出表数据为 Excel
```
用户请求: "把 users 表数据导出成 Excel"
1. 调用 export_table_excel(tableId="xx", target="prod")
2. 返回 base64 编码的 Excel 文件内容
3. 提示用户解码 base64 获取文件
```
**示例**:
```
调用: export_table_excel(tableId="5")
返回: {
"success": true,
"file_base64": "UEsDBBQAAAAI...",
"message": "Excel 文件已导出,请解码 base64 内容获取文件"
}
回复: 已成功导出 users 表数据为 Excel 文件
文件内容已 base64 编码,请解码后保存为 .xlsx 文件
```
### 注意事项
- **删除操作必须二次确认**
- `primaryKey` 必须是对象格式,如 `{"id": 1}`
- `primaryKeys` 是数组格式,如 `[{"id": 1}, {"id": 2}]`
- `data` 只包含要更新的字段,不需要提供全部字段
- 插入数据时,自增主键不需要提供
- 如果操作涉及多行,使用批量操作或循环调用
- 增删改操作默认作用于 `prod` 环境
---
## 场景 5AI 生成表结构
当用户需要创建新表但不知道如何设计表结构时,使用 AI 辅助生成。
### 工作流程
```
用户请求: "我需要一个用户表" / "帮我设计一个订单系统的表结构"
1. 调用 generate_table_by_description(requirement="用户描述")
2. 展示 AI 生成的表结构(表名、字段、类型、注释)
3. 用户确认后,调用 create_table(connectionId="xx", databaseName="xx", tableName="xx", columns=[...])
4. 确认创建成功
```
### 示例
**用户**: "帮我设计一个商品表,包含名称、价格、库存、分类"
```
调用: generate_table_by_description(
requirement="我需要一个商品表,包含商品名称、价格、库存、分类,用于电商系统"
)
返回: {
"tableName": "products",
"tableComment": "商品表",
"columns": [
{"columnName": "id", "columnType": "SERIAL", "isPrimaryKey": true, "isAutoIncrement": true, "columnComment": "主键ID"},
{"columnName": "name", "columnType": "VARCHAR", "columnLength": 200, "isNullable": false, "columnComment": "商品名称"},
{"columnName": "price", "columnType": "DECIMAL", "columnLength": 10, "columnComment": "价格"},
{"columnName": "stock", "columnType": "INTEGER", "columnComment": "库存数量"},
{"columnName": "category_id", "columnType": "INTEGER", "columnComment": "分类ID"},
{"columnName": "created_at", "columnType": "TIMESTAMP", "defaultValue": "NOW()", "columnComment": "创建时间"}
]
}
回复: AI 已生成表结构设计:
表名: products (商品表)
字段:
- id (SERIAL, 主键, 自增) - 主键ID
- name (VARCHAR(200), 非空) - 商品名称
- price (DECIMAL(10)) - 价格
- stock (INTEGER) - 库存数量
- category_id (INTEGER) - 分类ID
- created_at (TIMESTAMP, 默认NOW()) - 创建时间
是否需要在此基础上修改或直接创建?
用户: "直接创建到 order_db 数据库"
调用: create_table(
connectionId="58",
databaseName="order_db",
tableName="products",
tableComment="商品表",
columns=[...] // 使用 AI 生成的 columns
)
回复: 已成功创建表 products (商品表)
```
### 修改表结构alter_table
如果用户需要修改已有表的结构:
```
用户请求: "给 users 表加一个 phone 字段"
1. 确认表结构和要修改的内容
2. 构建 operations 数组(支持多种变更类型)
3. 调用 alter_table(connectionId="xx", databaseName="xx", tableName="xx", operations=[...])
4. 确认修改成功
```
**可用的 operations 类型**
- `ADD_COLUMN`:添加字段
- `DROP_COLUMN`:删除字段
- `RENAME_COLUMN`:重命名字段
- `ALTER_COLUMN_TYPE`:修改字段类型
- `SET_NOT_NULL`:设置为非空
- `DROP_NOT_NULL`:取消非空约束
- `SET_DEFAULT`:设置默认值
- `DROP_DEFAULT`:删除默认值
### 注意事项
- `requirement` 参数应尽可能详细,包含业务场景和字段需求
- AI 生成的表结构可能需要用户调整(如字段长度、类型)
- 创建表前需要确认 `connectionId``databaseName`
- 常用字段类型:`VARCHAR`, `INTEGER`, `SERIAL`(自增主键), `DECIMAL`, `TIMESTAMP`, `TEXT`, `BOOLEAN`
- 如果用户描述模糊,先引导用户补充细节
---
## 场景 6数据导入Excel 到数据库)
当用户需要从 Excel 文件导入数据到数据库时,使用此流程。
### 工作流程
```
用户请求: "帮我导入这个 Excel 文件" / "把表格数据导入数据库"
1. 用户将 Excel 文件转为 base64 编码
2. 调用 preview_import_data(connectionId="xx", file_base64="...", file_name="data.xlsx", target="test")
3. 展示 AI 识别的表结构和数据预览
4. ⚠️ 安全提醒:此操作可能涉及数据安全风险。
说明:导入的数据将写入数据库,请确认数据来源合法合规,不包含敏感信息、政治内容或违规内容。
请确认是否继续?
5. 用户确认无误后,调用 confirm_import_data(connectionId="xx", data={...}, target="test")
6. 确认导入成功
```
### 注意事项
- 文件大小限制:< 500KB
- 支持格式:.xlsx / .xls
- 导入前默认使用 `test` 环境(安全做法)
- 如果用户要导入到正式环境,必须二次确认
- base64 编码的文件内容需要提供文件名
---
## 场景 7API 密钥管理
当用户需要管理 API 密钥时,使用此流程。
### 7.1 查看密钥列表
```
调用: list_api_keys()
返回: {
"total": 6,
"rows": [
{"id": "7", "apiKeyName": "AWINBEXT", "apiKey": "Lb8Lg...", "status": 0, "expireTime": "2027-06-06..."},
...
]
}
```
### 7.2 创建新密钥
```
调用: create_api_key(apiKeyName="新密钥名称")
返回: 包含新创建的密钥信息
```
### 7.3 启用/禁用密钥
```
调用: toggle_api_key_status(id="7", status=0) // 0=启用, 1=禁用
```
### 7.4 删除密钥
```
⚠️ 删除前确认:
⚠️ 安全提醒:此操作存在删除风险。
说明API 密钥删除后不可恢复,依赖该密钥的服务将失效。
请确认是否继续?
回复: 确认要删除 API 密钥 "AWINBEXT" 吗?此操作不可恢复。
调用: delete_api_key(id="7")
```
### 7.5 查看密钥权限
```
调用: get_api_key_permissions(apiKeyId="7")
```
### 7.6 批量授权
```
调用: grant_api_key_permissions(
apiKeyId="7",
batchDatas=[
{
"connectionId": "58",
"permissionLevel": "connection",
"permissionType": "read,write"
},
{
"connectionId": "58",
"permissionLevel": "database",
"databaseName": "order_db",
"permissionType": "read"
}
]
)
```
权限级别说明:
- `connection`:数据源级别权限
- `database`:数据库级别权限
- `table`:表级别权限
---
## 场景 8技能与工具管理
当用户需要创建和管理自定义技能时,使用此流程。
### 8.1 查看数据源关联的技能
```
调用: get_skill_by_datasource(datasourceId="58")
```
### 8.2 创建技能
```
调用: create_skill(datasourceId="58", name="订单查询技能", description="用于订单数据的常用查询")
```
### 8.3 查看技能下的工具
```
调用: get_skill_tools(skillId="xx")
```
### 8.4 将 SQL 创建为可复用工具
```
调用: create_sql_tool(
skillId="xx",
tableIds=["5"],
suggestions=[{
"name": "查询活跃用户",
"businessDescription": "查询所有状态为活跃的用户",
"sqlTemplate": "SELECT * FROM users WHERE status = #{status}",
"sqlParams": {"status": {"type": "string", "default": "active"}},
"resultType": "list",
"businessScenario": "用于查看当前活跃用户列表"
}]
)
```
### 8.5 删除技能工具
```
⚠️ 删除前确认:
⚠️ 安全提醒:此操作存在删除风险。
说明:技能工具删除后不可恢复。
请确认是否继续?
调用: delete_skill_tool(skillToolId="xx")
```
### 8.6 更新技能配置
```
调用: update_skill_config(
datasourceId="58",
configTemplate='{"mcpServer": "..."}' // JSON 字符串
)
```
---
## 场景 9表订阅管理
```
用户请求: "订阅 orders 表" / "取消订阅 users 表"
调用: toggle_table_subscription(
configId="数据库配置ID",
tableName="orders",
isSubscribe=true // true=订阅, false=取消订阅
)
```
---
## 最佳实践
### 1. 安全第一
#### 通用安全确认规则
- 执行任何工具前,评估是否存在删除、泄密、政治敏感、似黄等风险
- 存在风险时,**必须询问用户确认**,说明后果,不得擅作主张
- 用户未明确确认前,不得执行
- 执行任何写操作INSERT/UPDATE/DELETE先确认环境prod vs test
- 删除操作必须向用户展示将要删除的数据并二次确认
- 危险操作DELETE/DROP/TRUNCATE/UPDATE 影响多行)前必须明确告知用户风险
- 数据导入时默认使用 `test` 环境
### 2. 分步引导
- 新手可能不知道 `datasourceId``tableId` 等参数,先通过列表工具引导获取
- 复杂操作分步执行,每步确认后继续
- 如果用户请求不完整(如未指定数据源),先引导补充信息
### 3. 数据展示
- 表格数据使用 Markdown 表格格式
- 长文本截断显示(最多 100 字符)
- 数字格式化(千位分隔符、货币符号)
- 时间格式化为可读格式
- 展示数据时包含字段名和类型
### 4. 错误处理
- API 错误:展示错误信息,建议重试或检查参数
- SQL 错误:展示 SQL 错误位置,建议修正
- 连接错误:检查数据源状态,建议启用或重新配置
- "登录过期":提示用户检查 `API_KEY` 环境变量
### 5. 沟通方式
- 向小白用户解释时,避免使用技术术语(如 JSON Schema、SERIAL 等),用通俗语言
- 操作完成后告知结果(成功/失败/影响行数)
- 如果用户操作成功,给出明确的反馈信息
---
## 常用参数参考
### 数据源类型
- `builtin` - 内置 PostgreSQL
- `external` - 外部数据库MySQL/PostgreSQL/Oracle/SQL Server/达梦等)
### 数据源状态
- `0` - 运行中
- `1` - 已停止
### 环境
- `prod` - 生产环境(默认)
- `test` - 测试环境
### 常用字段类型
| 类型 | 用途 | 示例 |
|------|------|------|
| `SERIAL` | 自增主键 | `id SERIAL PRIMARY KEY` |
| `VARCHAR(n)` | 短文本 | `username VARCHAR(50)` |
| `TEXT` | 长文本 | `description TEXT` |
| `INTEGER` | 整数 | `stock INTEGER` |
| `DECIMAL(m,n)` | 精确小数 | `price DECIMAL(10,2)` |
| `TIMESTAMP` | 时间戳 | `created_at TIMESTAMP` |
| `BOOLEAN` | 布尔值 | `is_active BOOLEAN` |
### 表结构变更操作类型alter_table
| 类型 | 用途 |
|------|------|
| `ADD_COLUMN` | 添加字段 |
| `DROP_COLUMN` | 删除字段 |
| `RENAME_COLUMN` | 重命名字段 |
| `ALTER_COLUMN_TYPE` | 修改字段类型 |
| `SET_NOT_NULL` | 设置为非空 |
| `DROP_NOT_NULL` | 取消非空约束 |
| `SET_DEFAULT` | 设置默认值 |
| `DROP_DEFAULT` | 删除默认值 |
### 权限级别API 密钥授权)
| 级别 | 范围 |
|------|------|
| `connection` | 整个数据源 |
| `database` | 指定数据库 |
| `table` | 指定表 |
---
## 快速开始
如果用户说"帮我查一下数据库",按以下步骤操作:
1. 调用 `list_datasources()` 获取数据源列表
2. 展示列表,让用户选择或默认第一个运行中的数据源
3. 调用 `get_datasource_detail(datasourceId="xx")` 获取数据库和表信息
4. 引导用户选择要操作的表
5. 根据用户意图调用相应的工具:
- 想看数据 → `query_table_data`
- 想执行 SQL → `execute_sql`
- 想新增数据 → `insert_table_row`
- 想修改数据 → `update_table_row`
- 想删除数据 → `delete_table_rows`(需确认)
- 想导出 Excel → `export_table_excel`
- 想创建表 → `generate_table_by_description` + `create_table`
- 想导入 Excel → `preview_import_data` + `confirm_import_data`

Binary file not shown.

View File

@@ -1,356 +0,0 @@
---
name: mcp-tool-testing
description: MCP 工具通用测试技能。自动发现、执行和组合测试任何 MCP 服务器中的工具,支持场景自动创造和风险前置询问。
version: 0.1.0
---
# MCP Tool Testing Skill
这是一个通用的 MCPModel Context Protocol工具测试技能。适用于任何 MCP 环境,无论是自研的还是第三方的。
## 核心原则
1. **通用性**:不假设任何具体的服务器名称、工具名称或项目结构
2. **自动发现**:通过 MCP 协议本身获取当前环境中可用的所有工具
3. **智能执行**:能判断的入参自动执行,不能判断的询问用户
4. **场景自动创造**:根据工具自动匹配并创造测试场景,无需用户指定
5. **风险前置**:有风险的操作必须询问用户确认,绝不擅作主张
6. **结果汇总**:最终只返回执行工具的入参、出参和理解
## 执行流程
### 第一步:发现可用工具
通过 MCP 的 `tools/list` 方法获取当前环境中所有可用的工具。
**获取每个工具的信息:**
- 工具名称name
- 工具描述description
- 入参 schemainputSchema
- 参数名称
- 参数类型string, integer, boolean, object, array, enum 等)
- 是否必填required
- 默认值default
- 枚举值enum
- 参数描述description
### 第二步:分析工具并分类
根据工具名称和描述,自动对工具进行分类:
1. **查询类**list_xxx, get_xxx, query_xxx, search_xxx, find_xxx
2. **创建类**create_xxx, add_xxx, insert_xxx, new_xxx
3. **更新类**update_xxx, edit_xxx, modify_xxx, alter_xxx
4. **删除类**delete_xxx, remove_xxx, drop_xxx
5. **执行类**execute_xxx, run_xxx, call_xxx, invoke_xxx
6. **状态类**toggle_xxx, enable_xxx, disable_xxx, start_xxx, stop_xxx
7. **其他**:无法归类的工具
### 第三步:执行单个工具
对每个工具,按以下策略执行:
#### 入参判断逻辑
```
对于每个必填参数required 中的参数):
1. schema 有 default 值 → 使用该默认值
2. schema 有 enum 且只有一个值 → 使用该值
3. schema 有 enum 且有多个值 → 使用第一个,或询问用户
4. 参数名是常见的通用类型:
- pageNum, page, offset → 使用 1
- pageSize, limit, size → 使用 10
- target, environment → 使用 "prod" 或第一个 enum 值
- status, enabled → 使用 0 或 true
- name, title → 使用测试名称如 "test_item"
- id, xxxId → 需要从其他工具查询获取,或询问用户
5. 可以从之前执行的工具结果中获取 → 使用前一个工具的输出
6. 以上都不满足 → 询问用户
对于非必填参数:
- 一般不传,使用服务器默认值
- 如果需要测试特定功能,可以传一个合理值
```
#### 询问用户的标准
当遇到以下情况时必须询问:
**1. 参数不明确**
- 参数是必填的,且值完全不明确
- 例如:某个特定的业务 ID、密钥、路径等
**2. 参数有多个合理选项且无法自动判断**
- 例如enum 有 ["mysql", "postgresql", "oracle"],不知道测试用哪个
**3. 参数涉及敏感信息**
- 例如password, token, apiKey, secret 等
**4. 高风险操作必须询问(绝不擅作主张)**
以下类型的工具/操作在执行前必须先询问用户确认:
- **删除类**delete_xxx, remove_xxx, drop_xxx, destroy_xxx
- 风险:数据丢失,不可恢复
- 询问内容:确认是否要执行删除操作,指定删除目标或使用测试数据
- **泄密风险类**export_xxx, dump_xxx, download_xxx, get_all_xxx大量数据
- 风险:可能导出敏感业务数据、用户信息、密钥等
- 询问内容:确认是否要导出数据,是否使用脱敏测试数据
- **政治敏感类**:涉及政府、政策、领导人等相关内容的工具
- 风险:可能触及政治敏感话题
- 询问内容:确认测试方向和范围
- **似黄/色情风险类**:涉及用户生成内容、图片、文本审核等相关工具
- 风险:可能接触到不当内容
- 询问内容:确认是否使用安全的测试数据
- **生产环境操作类**:影响生产环境数据的工具
- 风险:可能影响真实业务
- 询问内容:确认是否在测试环境执行,或使用只读模式
**5. 更新/修改类操作需要确认**
- 例如update_xxx, modify_xxx 可能改变真实数据
**高风险操作询问格式:**
```
⚠️ 高风险操作提醒
工具:[工具名]
风险类型:[删除/泄密/政治敏感/似黄/生产环境影响]
具体风险:[描述可能的风险]
请选择:
1. 确认执行,使用测试数据
2. 确认执行,使用真实数据(我知道风险)
3. 跳过此工具
4. 其他指示
```
**普通询问格式:**
```
工具:[工具名]
参数:[参数名](类型:[类型],必填:是/否)
用途:[从 description 中提取]
请选择:
1. [建议选项1如果有]
2. [建议选项2如果有]
3. 提供自定义值
或者告诉我使用什么值。
```
#### 执行顺序策略
1. **先执行无参数或少参数的查询工具**
- 如 list_xxx、get_all_xxx 等
- 这些工具通常不需要太多参数,可以直接执行
- 执行结果可以为后续工具提供 ID 等参数
2. **利用查询结果作为后续工具的入参**
- 例如list_datasources 返回的 id 用于 get_datasource_detail
- 例如list_tables 返回的 tableId 用于 query_table_data
3. **最后执行创建/修改/删除类工具**
- 这些通常需要更多上下文参数
- 删除操作要特别小心,确认是测试数据
### 第四步:场景自动创造与搭配测试
这是本技能的核心能力:根据当前所有可用工具,自动创造有意义的测试场景,无需用户指定。
#### 场景识别规则
根据工具的名称、描述和参数,自动匹配以下 8 种场景模式:
**1. 查询链路场景**
- 匹配规则:存在 list_xxx + get_xxx_detail/xxx_detail + query/get_data 类工具
- 创造方式list 获取列表 → 取第一条的 ID → get detail 获取详情 → 用详情中的 ID 查询关联数据
- 示例list_datasources → get_datasource_detail(id) → list_tables(datasourceId) → get_table_detail(tableId)
**2. 完整 CRUD 场景**
- 匹配规则:存在 create_xxx + list/query_xxx + update_xxx + delete_xxx 类工具
- 创造方式create 创建测试数据 → list 确认存在 → update 修改 → list 确认修改 → delete 删除 → list 确认删除
- 示例create_api_key → list_api_keys → toggle_api_key_status → list_api_keys → delete_api_key → list_api_keys
**3. 状态变更验证场景**
- 匹配规则:存在 get/list_xxx + toggle/enable/disable/start/stop_xxx 类工具
- 创造方式get 初始状态 → toggle 变更状态 → get 验证状态已变更 → toggle 恢复 → get 验证恢复
- 示例get_datasource_detail → toggle_datasource_status(status=1) → get_datasource_detail → toggle_datasource_status(status=0) → get_datasource_detail
**4. 参数传递链路场景**
- 匹配规则:工具 A 的输出字段与工具 B 的输入参数名匹配或语义相关
- 创造方式:执行 A → 从结果提取字段 → 作为 B 的输入 → 执行 B → 继续传递给 C
- 示例list_datasources 返回 datasourceId → get_datasource_detail(datasourceId) 返回 connectionId → execute_sql(connectionId, sql)
**5. 批量操作场景**
- 匹配规则:存在接受 array 类型参数的工具(如批量删除、批量授权)
- 创造方式list 获取多条记录 → 提取 IDs 数组 → 传递给批量操作工具 → 验证结果
- 示例list_api_keys → 提取 ids → grant_api_key_permissions(batchDatas=[{...}])
**6. 条件分支场景**
- 匹配规则:工具支持不同枚举参数或可选参数,产生不同行为
- 创造方式:同一工具传不同参数 → 对比输出差异
- 示例query_table_data(tableId, target="prod") vs query_table_data(tableId, target="test")
**7. 异常处理场景**
- 匹配规则:任何工具
- 创造方式:传入无效 ID、空参数、错误类型 → 验证错误处理是否合理
- 示例get_datasource_detail(datasourceId="invalid") → 验证返回错误信息
**8. 跨服务器场景**(当有多个 MCP 服务器时)
- 匹配规则:不同服务器的工具之间存在业务关联
- 创造方式服务器A的输出 → 作为服务器B的输入
- 示例IoT 获取设备列表 → Dify workflow 处理设备数据 → SQL 执行存储结果
#### 场景自动创造流程
```
1. 收集所有工具,建立工具字典
- key: 工具名
- value: {description, inputSchema, outputSample, category}
2. 构建参数依赖图
- 分析每个工具的输入参数名(如 datasourceId, tableId, apiKeyId
- 分析每个工具的输出字段(从执行结果中提取)
- 建立 "输出字段 → 输入参数" 的映射关系
3. 匹配场景模式
- 遍历上述 8 种场景规则
- 对每种规则,检查当前工具集是否满足匹配条件
- 满足则创造具体场景实例
4. 场景优先级排序
- P0: 查询链路(最基础,最先执行)
- P1: CRUD 完整链路(验证完整生命周期)
- P2: 状态变更验证(验证状态管理)
- P3: 参数传递链路(验证工具间协作)
- P4: 批量操作、条件分支、异常处理
- P5: 跨服务器场景(最复杂,最后执行)
5. 执行场景
- 按优先级依次执行
- 前一步的输出自动传递给下一步
- 任何一步失败则标记场景失败,继续下一个场景
- 场景中包含高风险操作时,执行前询问用户
```
#### 场景执行记录格式
```markdown
### 场景:[场景名称]
- **触发规则**[匹配的场景模式,如 "查询链路场景"]
- **涉及工具**工具A → 工具B → 工具C
- **执行过程**
| 步骤 | 工具 | 入参 | 出参摘要 | 状态 |
|------|------|------|---------|------|
| 1 | list_xxx | {} | 返回 5 条记录 | ✅ |
| 2 | get_xxx | {id: "从步骤1获取"} | 返回详情 | ✅ |
| 3 | query_xxx | {xxxId: "从步骤2获取"} | 返回结果 | ✅ |
- **数据流**步骤1.id → 步骤2.id → 步骤2.connectionId → 步骤3.connectionId
- **场景结果**:✅ 全部成功 / ❌ 步骤N失败原因...
- **理解**[对这个场景的整体理解,工具间如何协作]
```
**场景测试要点:**
- 记录完整的数据流转过程
- 验证工具之间的兼容性
- 检查数据格式是否一致
- 场景创造过程不需要用户干预,自动完成
- 只在遇到无法判断的参数或高风险操作时才询问用户
### 第五步:生成报告
最终只返回以下内容:
```markdown
# MCP 工具测试报告
## 工具列表
共发现 [N] 个工具:
| 序号 | 工具名 | 描述 | 必填参数 | 分类 |
|------|--------|------|---------|------|
## 工具执行结果
### 1. [工具名]
- **描述**[工具描述]
- **入参**`{JSON}`
- **出参**`{JSON 摘要}`
- **状态**:✅ 成功 / ❌ 失败 / ⚠️ 跳过(原因:...
- **理解**[你对这个工具功能的简要理解]
### 2. [工具名]
...
## 场景测试
### 场景1[场景名称]
- **流程**工具A → 工具B → 工具C
- **数据流**[描述数据如何在工具间传递]
- **结果**[最终结果摘要]
- **状态**:✅ 成功 / ❌ 失败
## 总结
| 指标 | 数量 |
|------|------|
| 发现工具总数 | N |
| 成功执行 | N |
| 执行失败 | N |
| 跳过(需用户提供信息) | N |
| 高风险操作(已询问用户) | N |
| 场景测试数 | N |
## 待确认事项
列出需要用户提供的参数或信息:
1. [工具名] 的 [参数名][说明]
2. ...
```
## 特殊处理
### 动态工具
有些服务器启动时从外部加载工具定义(如从 API、配置文件、工作流引擎等
1. 通过 MCP 协议获取实际可用的工具列表
2. 对动态获取的工具同样按照上述流程测试
3. 记录工具的来源信息(如果有)
### 需要认证/配置的服务器
某些 MCP 服务器需要特定的环境变量、API Key、Token 等:
1. 检查服务器是否正常运行
2. 如果启动失败,记录错误信息并跳过
3. 如果运行正常但工具调用失败(如 401询问用户提供认证信息
### 失败处理
- **网络超时**:记录错误,标记为失败,继续下一个
- **参数错误**:检查是否需要补充参数,或询问用户
- **服务器未运行**:记录原因,跳过该服务器的所有工具
- **工具不存在**:可能工具定义已变更,重新获取工具列表
### 输出处理
- 输出过长时适当截断(如超过 2000 字符)
- 保留关键信息:状态码、主要数据、错误信息
- 对于列表类输出,显示前几条和总数
## 注意事项
1. **保持参数一致性**:同一个 ID 在多个工具中保持相同
2. **风险前置**:高风险操作必须先询问用户,绝不擅作主张
3. **记录完整**:每个工具的入参、出参都要记录
4. **及时询问**:遇到不明确的参数不要猜测,询问用户
5. **场景优先**:优先测试有意义的场景组合,而不是孤立地测试每个工具
6. **自动创造**:场景不需要用户指定,根据工具自动匹配和创造
7. **最终输出**:只返回入参、出参和理解,不需要冗长的过程描述
8. **五类风险**:删除、泄密、政治、似黄、生产环境影响——必须询问用户

View File

@@ -1,2 +0,0 @@
{
}

View File

@@ -1,68 +0,0 @@
# lzwcai-mcp-server-package
MCP (Model Context Protocol) 服务器工具集,为 AI 助手提供企业级业务能力扩展。
## 📦 包含模块
| 模块 | 版本 | 说明 |
|------|------|------|
| [lzwcai-mcp-iot](./lzwcai_mcp_iot) | 0.3.3 | IoT 设备控制服务器,支持设备查询、定位和控制 |
| [lzwcai-mcp-sqlexecutor](./lzwcai_mcp_sqlexecutor) | 0.1.8 | SQL 查询执行服务器,支持动态工具生成 |
| [lzwcai-mcp-api-converter](./lzwcai_mcp_api_converter) | 0.1.30 | API 转换服务器,将业务 API 转换为 MCP 工具 |
| [lzwcai-demp-tool-server-dify-to-mcp](./lzwcai_demp_tool_server_dify_to_mcp) | 0.1.4 | Dify 集成工具,将 Dify 模型部署到 MCP |
| [lzwcai-demp-tool-server-dify-to-mcp-test](./lzwcai_demp_tool_server_dify_to_mcp_test) | 0.1.0 | Dify 集成工具测试版 |
## 🚀 快速安装
```bash
# IoT 设备控制
pip install lzwcai-mcp-iot
# SQL 查询执行
pip install lzwcai-mcp-sqlexecutor
# API 转换器
pip install lzwcai-mcp-api-converter
# Dify 集成
pip install lzwcai-demp-tool-server-dify-to-mcp
```
## <20> 打包 与发布
```bash
# 进入子模块目录
cd lzwcai_mcp_iot
# 使用 uv 打包
uv build
# 上传到管理端技能广场
# 将 dist/ 目录下的 .tar.gz 文件上传至技能广场
```
## 🔧 MCP 客户端配置示例
```json
{
"mcpServers": {
"iot": {
"command": "lzwcai-mcp-iot"
},
"sql": {
"command": "lzwcai-mcp-sqlexecutor"
},
"api": {
"command": "lzwcai-mcp-api-converter"
}
}
}
```
## 📄 许可证
专有软件 - 版权所有 © LZWCAI开发团队
## 📧 联系方式
- 邮箱dev@lzwcai.com

View File

@@ -1,12 +0,0 @@
Metadata-Version: 2.4
Name: lzwcai-demp-tool-server-dify-to-mcp
Version: 0.1.0
Summary: 这是一个Dify to MCP的集成工具通过Dify的API接口将Dify的模型部署到MCP平台并进行推理。
Requires-Python: >=3.10
Requires-Dist: httpx>=0.28.1
Requires-Dist: mcp>=1.1.2
Requires-Dist: omegaconf>=2.3.0
Requires-Dist: pip>=24.3.1
Requires-Dist: python-dotenv>=1.0.1
Requires-Dist: requests
Requires-Dist: pypinyin>=0.54.0

View File

@@ -1,37 +0,0 @@
# lzwcai-mcp-server-package
#### 介绍
lzwcai-mcp-server-package
#### 软件架构
软件架构说明
#### 安装教程
1. xxxx
2. xxxx
3. xxxx
#### 使用说明
1. xxxx
2. xxxx
3. xxxx
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

View File

@@ -1,12 +0,0 @@
Metadata-Version: 2.4
Name: lzwcai-demp-tool-server-dify-to-mcp
Version: 0.1.4
Summary: 这是一个Dify to MCP的集成工具通过Dify的API接口将Dify的模型部署到MCP平台并进行推理。
Requires-Python: >=3.10
Requires-Dist: httpx>=0.28.1
Requires-Dist: mcp>=1.1.2
Requires-Dist: omegaconf>=2.3.0
Requires-Dist: pip>=24.3.1
Requires-Dist: python-dotenv>=1.0.1
Requires-Dist: requests
Requires-Dist: pypinyin>=0.54.0

View File

@@ -1,23 +0,0 @@
README.md
pyproject.toml
setup.cfg
lzwcai_demp_tool_server_dify_to_mcp.egg-info/PKG-INFO
lzwcai_demp_tool_server_dify_to_mcp.egg-info/SOURCES.txt
lzwcai_demp_tool_server_dify_to_mcp.egg-info/dependency_links.txt
lzwcai_demp_tool_server_dify_to_mcp.egg-info/entry_points.txt
lzwcai_demp_tool_server_dify_to_mcp.egg-info/requires.txt
lzwcai_demp_tool_server_dify_to_mcp.egg-info/top_level.txt
src/__init__.py
src/create_mcp.py
src/create_mcp_util.py
src/chat/__init__.py
src/chat/chat_server.py
src/completion/completion_server.py
src/completion/test.py
src/core/__init__.py
src/core/core_server.py
src/difyTaskCall/task_instance.py
src/utils/tool_translation.py
src/utils/translator.py
src/workflow/__init__.py
src/workflow/workflow_server.py

View File

@@ -1,2 +0,0 @@
[console_scripts]
lzwcai-demp-tool-server-dify-to-mcp = src.create_mcp:run_main

View File

@@ -1,7 +0,0 @@
httpx>=0.28.1
mcp>=1.1.2
omegaconf>=2.3.0
pip>=24.3.1
python-dotenv>=1.0.1
requests
pypinyin>=0.54.0

View File

@@ -1,61 +0,0 @@
#!/usr/bin/env python3
"""
主入口文件
用于启动 Dify MCP 服务器,并配置命令行参数
"""
import os
import sys
# Mock 配置参数
def setup_mock_arguments():
"""
设置模拟命令行参数
这些参数可以根据实际需求进行修改
"""
# 默认配置
default_config = {
"base_url": "http://192.168.2.236:3001/v1",
"app_sks": ["app-YFHByB4whARWVqXN2LcuPudq"],
"mode_type": "workflow",
"transport": "stdio"
}
# 如果没有提供命令行参数,则添加默认参数
if len(sys.argv) == 1:
sys.argv.extend([
"--base-url", default_config["base_url"],
"--app-sks", *default_config["app_sks"],
"--mode-type", default_config["mode_type"]
])
return default_config
def main():
"""
主函数:设置命令行参数并启动服务器
"""
# 设置模拟命令行参数
config = setup_mock_arguments()
# 导入并运行 MCP 服务器
try:
from src.create_mcp import run_main
# 获取传输模式
transport_mode = config.get("transport", "stdio")
# 运行服务器(不输出额外信息,避免干扰 STDIO 通信)
run_main(transport=transport_mode)
except ImportError as e:
print(f"[ERROR] 导入错误: {e}", file=sys.stderr)
print("请确保已正确安装所有依赖包", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"[ERROR] 运行错误: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -1,32 +0,0 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "lzwcai-demp-tool-server-dify-to-mcp"
version = "0.1.4"
description = "这是一个Dify to MCP的集成工具通过Dify的API接口将Dify的模型部署到MCP平台并进行推理。"
requires-python = ">=3.10"
dependencies = [
"httpx>=0.28.1",
"mcp>=1.1.2",
"omegaconf>=2.3.0",
"pip>=24.3.1",
"python-dotenv>=1.0.1",
"requests",
"pypinyin>=0.54.0",
]
[tool.setuptools]
packages = {find = {where = ["."], include = ["src*"]}}
include-package-data = true
[project.scripts]
lzwcai-demp-tool-server-dify-to-mcp = "src.create_mcp:run_main"
[tool.hatch.build.targets.wheel]
packages = ["src"]
[tool.setuptools.package-data]
"*" = ["*.env"]
"src" = ["**/*.env"]

View File

@@ -1,4 +0,0 @@
[egg_info]
tag_build =
tag_date = 0

View File

@@ -1,7 +0,0 @@
class ChatDifyAPI:
def __init__(self, base_url: str, app_sks: str):
self.base_url = base_url
self.app_sks = app_sks
def process_task(self, task_id: str, **kwargs):
pass

View File

@@ -1,212 +0,0 @@
import requests
from abc import ABC
import logging
import json
import re
import pypinyin
logger = logging.getLogger(__name__)
def pinyin_to_camel(pinyin):
"""
将拼音列表转换为驼峰命名
例如: ['ni', 'hao', 'a'] -> 'NiHaoA'
所有非字母数字字符会被替换为下划线
"""
# 使用正则表达式将所有非字母数字字符(包括所有标点符号)替换为下划线
cleaned = re.sub(r'[^\w\s]', '_', pinyin)
# 将空格也替换为下划线
cleaned = re.sub(r'\s+', '_', cleaned)
# 移除连续的下划线并去除首尾下划线
cleaned = re.sub(r'_+', '_', cleaned).strip('_')
# 转换为拼音并生成驼峰命名
pinyin_list = pypinyin.lazy_pinyin(cleaned)
return "tool_" + "".join(word.capitalize() for word in pinyin_list)
class CompletionDifyAPI(ABC):
def __init__(self, base_url: str, dify_app_sks: list, user="pp666"):
# dify configs
self.dify_base_url = base_url
self.dify_app_sks = dify_app_sks
self.user = user
# dify app infos
dify_app_infos = []
dify_app_params = []
dify_app_metas = []
for key in self.dify_app_sks:
dify_app_infos.append(self.get_app_info(key))
dify_app_params.append(self.get_app_parameters(key))
dify_app_metas.append(self.get_app_meta(key))
self.dify_app_infos = dify_app_infos
self.dify_app_params = dify_app_params
self.dify_app_metas = dify_app_metas
self.dify_app_names = [x["name"] for x in dify_app_infos]
def chat_message(
self,
api_key,
inputs={},
response_mode="streaming",
conversation_id=None,
userId="pp666",
files=None,
):
url = f"{self.dify_base_url}/completion-messages"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": userId,
}
if conversation_id:
data["conversation_id"] = conversation_id
if response_mode == "streaming":
response = requests.post(url, headers=headers, json=data, stream=True)
# 处理流式响应
full_answer = ""
for line in response.iter_lines():
if line:
# 跳过 "data:" 前缀
decoded_line = line.decode("utf-8")
if decoded_line.startswith("data:"):
try:
json_str = decoded_line[5:].strip()
data = json.loads(json_str)
if data.get("event") == "message" and "answer" in data:
# 累积完整答案
full_answer += data["answer"]
# 这里也可以选择处理每个部分响应,例如返回生成器
# yield data
except json.JSONDecodeError:
logger.warning(f"无法解析JSON数据: {decoded_line}")
# 创建一个符合非流式响应格式的结果
response_data = {"answer": full_answer}
# 处理可能包含代码块的数据
processed_data = self.process_answer_code_block(response_data)
return processed_data
else:
response = requests.post(url, headers=headers, json=data)
response_data = response.json()
# 处理可能包含代码块的数据
processed_data = self.process_answer_code_block(response_data)
return processed_data
def upload_file(self, api_key, file_path, user="pp666"):
url = f"{self.dify_base_url}/files/upload"
headers = {"Authorization": f"Bearer {api_key}"}
files = {"file": open(file_path, "rb")}
data = {"user": user}
response = requests.post(url, headers=headers, files=files, data=data)
response.raise_for_status()
return response.json()
def stop_response(self, api_key, task_id, user="pp666"):
url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {"user": user}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
def get_app_info(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/info"
headers = {"Authorization": f"Bearer {api_key}"}
# params = {"user": user}
response = requests.get(url, headers=headers)
response.raise_for_status()
response_map = response.json()
# 翻译工具名称
from src.utils.tool_translation import TranslationService
tool_name = response_map.get("name")
if tool_name:
# translated_name = TranslationService.translate_tool_name(tool_name)
translated_name = pinyin_to_camel(tool_name)
response_map["name"] = translated_name
# 翻译工具描述
# tool_description = response_map.get("description")
# if tool_description:
# translated_description = TranslationService.translate_tool_description(
# tool_description
# )
# response_map["description"] = (
# f"{tool_description} ({translated_description})"
# )
return response_map
def get_app_parameters(self, api_key, user="pp666"):
return {
"user_input_form": [
{"string": {"variable": "query", "label": "查询内容", "required": True}}
]
}
def get_app_meta(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/meta"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
@staticmethod
def process_answer_code_block(data):
try:
# 获取answer字段
answer = data.get("answer", "")
# 构造符合workflow_finished格式的输出
formatted_response = [
{"event": "workflow_finished", "data": {"outputs": {"result": answer}}}
]
# 尝试处理可能的代码块
if answer.startswith("```") and answer.endswith("```"):
try:
# 移除代码块标记并解析JSON
code_content = answer.strip("```").strip()
json_data = json.loads(code_content)
# 如果包含description字段用它替换answer
if "description" in json_data:
formatted_response[0]["data"]["outputs"]["result"] = json_data[
"description"
]
except json.JSONDecodeError:
# 如果不是有效的JSON保留原始代码块内容
pass
return formatted_response
except Exception as e:
logger.warning(f"处理答案代码块时出错: {str(e)}")
# 发生错误时返回符合格式的基础响应
return [
{
"event": "workflow_finished",
"data": {
"outputs": {
"error": str(e),
"fallback": data.get("answer", str(data)),
}
},
}
]

View File

@@ -1,104 +0,0 @@
import requests
from abc import ABC
import logging
import json
logger = logging.getLogger(__name__)
res = {
"event": "message",
"task_id": "49c9ea1b-7b43-475b-a680-d769fb238a45",
"id": "432ab98e-5e36-4a29-abe5-e01281c3678c",
"message_id": "432ab98e-5e36-4a29-abe5-e01281c3678c",
"mode": "completion",
"answer": '```\n{\n "description": "该API的具体功能描述暂时不明确因为提供的API信息 \'今天打老虎啊按时啊啊\' 并不是有效的API名称或描述。请提供正确的API名称和相关输入输出信息以便我能为其补充完善的API描述。"\n}\n```',
"metadata": {
"usage": {
"prompt_tokens": 73,
"prompt_unit_price": "0.0",
"prompt_price_unit": "0.0",
"prompt_price": "0.0",
"completion_tokens": 61,
"completion_unit_price": "0.0",
"completion_price_unit": "0.0",
"completion_price": "0.0",
"total_tokens": 134,
"total_price": "0.0",
"currency": "USD",
"latency": 1.896302318200469,
}
},
"created_at": 1747233054,
}
def process_answer_code_block(data):
try:
# 获取answer字段
answer = data.get("answer", "")
# 检查answer是否是代码块格式
if answer.startswith("```") and answer.endswith("```"):
# 移除代码块标记并解析JSON
code_content = answer.strip("```").strip()
json_data = json.loads(code_content)
# 获取description字段
if "description" in json_data:
return json_data["description"]
# 如果不是预期格式则返回原始answer
return data.get("answer", data)
except Exception as e:
logger.warning(f"处理答案代码块时出错: {str(e)}")
# 发生错误时返回原始数据
return data.get("answer", data)
def chat_message_test(
api_key,
inputs={},
response_mode="blocking",
conversation_id=None,
userId="pp666",
files=None,
):
url = "https://ops.lzwcai.com/v1/completion-messages"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": userId,
}
if conversation_id:
data["conversation_id"] = conversation_id
if response_mode == "streaming":
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
return response
else:
response = requests.post(url, headers=headers, json=data)
return response.json()
if __name__ == "__main__":
print("开始执行主程序")
try:
print("准备调用chat_message方法")
res = chat_message_test(
api_key="app-Ppemii3c0ROPoLvRwskgZ7Il",
inputs={"query": "今天打老虎啊按时啊啊"},
response_mode="streaming",
userId="abc-123",
)
print("chat_message方法调用完成")
# 打印响应内容
print("响应内容:", res)
# print(process_answer_code_block(res))
except Exception as e:
print(f"执行过程中出现错误: {e}")

View File

@@ -1,172 +0,0 @@
import requests
from abc import ABC
import logging
import json
import re
import pypinyin
logger = logging.getLogger(__name__)
def pinyin_to_camel(pinyin):
"""
将拼音列表转换为驼峰命名
例如: ['ni', 'hao', 'a'] -> 'NiHaoA'
所有非字母数字字符会被替换为下划线
"""
# 使用正则表达式将所有非字母数字字符(包括所有标点符号)替换为下划线
cleaned = re.sub(r'[^\w\s]', '_', pinyin)
# 将空格也替换为下划线
cleaned = re.sub(r'\s+', '_', cleaned)
# 移除连续的下划线并去除首尾下划线
cleaned = re.sub(r'_+', '_', cleaned).strip('_')
# 转换为拼音并生成驼峰命名
pinyin_list = pypinyin.lazy_pinyin(cleaned)
return "tool_" + "".join(word.capitalize() for word in pinyin_list)
class DifyAPI(ABC):
def __init__(self, base_url: str, dify_app_sks: list, user="pp666"):
# dify configs
self.dify_base_url = base_url
self.dify_app_sks = dify_app_sks
self.user = user
# dify app infos
dify_app_infos = []
dify_app_params = []
dify_app_metas = []
for key in self.dify_app_sks:
dify_app_infos.append(self.get_app_info(key))
dify_app_params.append(self.get_app_parameters(key))
dify_app_metas.append(self.get_app_meta(key))
print("dify_app_params", dify_app_params)
self.dify_app_infos = dify_app_infos
self.dify_app_params = dify_app_params
self.dify_app_metas = dify_app_metas
self.dify_app_names = [x["name"] for x in dify_app_infos]
def chat_message(
self,
api_key,
inputs={},
response_mode="streaming",
conversation_id=None,
user="pp666",
files=None,
):
url = f"{self.dify_base_url}/workflows/run"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": user,
}
logger.info("Sending data to Dify API: %s", data)
logger.info("Sending headers to Dify API: %s", headers)
logger.info("Sending url to Dify API: %s", url)
if conversation_id:
data["conversation_id"] = conversation_id
if files:
files_data = []
for file_info in files:
file_path = file_info.get("path")
transfer_method = file_info.get("transfer_method")
if transfer_method == "local_file":
files_data.append(("file", open(file_path, "rb")))
elif transfer_method == "remote_url":
pass
response = requests.post(
url,
headers=headers,
data=data,
files=files_data,
stream=response_mode == "streaming",
)
else:
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
response.raise_for_status()
if response_mode == "streaming":
for line in response.iter_lines():
if line:
if line.startswith(b"data:"):
try:
json_data = json.loads(line[5:].decode("utf-8"))
yield json_data
except json.JSONDecodeError:
print(f"Error decoding JSON: {line}")
else:
return response.json()
def upload_file(self, api_key, file_path, user="pp666"):
url = f"{self.dify_base_url}/files/upload"
headers = {"Authorization": f"Bearer {api_key}"}
files = {"file": open(file_path, "rb")}
data = {"user": user}
response = requests.post(url, headers=headers, files=files, data=data)
response.raise_for_status()
return response.json()
def stop_response(self, api_key, task_id, user="pp666"):
url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {"user": user}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
def get_app_info(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/info"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
from src.utils.tool_translation import TranslationService
response_map = response.json()
# 翻译工具名称
# tool_name = response_map.get("name")
# translated_name = TranslationService.translate_tool_name(tool_name)
# response_map["name"] = translated_name
# # 翻译工具描述
# tool_description = response_map.get("description")
# translated_description = TranslationService.translate_tool_description(
# tool_description
# )
# response_map["description"] = translated_description
tool_name = response_map.get("name")
if tool_name:
# translated_name = TranslationService.translate_tool_name(tool_name)
translated_name = pinyin_to_camel(tool_name)
response_map["name"] = translated_name
return response_map
def get_app_parameters(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/parameters"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
def get_app_meta(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/meta"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()

View File

@@ -1,318 +0,0 @@
import asyncio
import json
import os
import logging
import argparse
from abc import ABC
import mcp.server.stdio
import mcp.types as types
import requests
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
from omegaconf import OmegaConf
# from src.workflow.workflow_server import WorkflowDifyAPI
from src.difyTaskCall.task_instance import TaskInstance
# 配置日志记录
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
def parse_arguments():
parser = argparse.ArgumentParser(description="Dify MCP服务器配置")
parser.add_argument(
"--base-url",
type=str,
help="API基础URL",
default="http://192.168.11.24:3001/v1",
)
parser.add_argument(
"--app-sks",
nargs="+",
help="应用秘钥列表",
default=["app-d7s00CJ2NY4LJzUEiZsVDnPN"],
)
parser.add_argument(
"--mode-type",
type=str,
help="Dify应用模式类型 (workflow, chat, completion)",
default="workflow",
choices=["workflow", "chat", "completion"],
)
return parser.parse_args()
def get_app_info(base_url=None, app_sks=None, mode_type=None):
# 获取命令行参数
args = parse_arguments()
# 命令行参数优先,其次是函数参数,最后是默认值
if args.base_url is not None:
base_url = args.base_url
if base_url is None:
base_url = "http://192.168.11.24:3001/v1"
if args.app_sks is not None:
app_sks = args.app_sks
if app_sks is None:
app_sks = ["app-d7s00CJ2NY4LJzUEiZsVDnPN"]
# 确保 app_sks 始终是列表类型
if isinstance(app_sks, str):
# 如果是字符串,转换为列表
app_sks = [app_sks]
if args.mode_type is not None:
mode_type = args.mode_type
if mode_type is None:
mode_type = "workflow"
return base_url, app_sks, mode_type
# return "https://dempdify.lzwcai.com/v1", ["app-X6wAy5nkvWB3hR69cgvIjC3r"], "workflow"
# 初始化服务器和Dify API
base_url, dify_app_sks, dify_app_mode_type = get_app_info()
server = Server("dify_mcp_server")
task_instance = TaskInstance(base_url, dify_app_sks, dify_app_mode_type)
dify_api = task_instance.get_task_instance(dify_app_mode_type)
def process_user_input_form(user_input_form):
"""
处理Dify应用的用户输入表单转换为JSON Schema格式
参数:
user_input_form: Dify应用的用户输入表单配置
返回:
处理后的inputSchema字典
"""
inputSchema = dict(
type="object",
properties={},
required=[],
)
property_num = len(user_input_form)
if property_num > 0:
for j in range(property_num):
param = user_input_form[j]
param_type = list(param.keys())[0]
param_info = param[param_type]
property_name = param_info["variable"]
# 根据不同控件类型处理
if param_type == "text-input":
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
}
if "default" in param_info:
inputSchema["properties"][property_name]["default"] = param_info[
"default"
]
elif param_type == "paragraph":
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
"format": "paragraph",
}
if "default" in param_info:
inputSchema["properties"][property_name]["default"] = param_info[
"default"
]
elif param_type == "select":
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
"enum": param_info["options"],
}
if "default" in param_info:
inputSchema["properties"][property_name]["default"] = param_info[
"default"
]
elif param_type == "file_upload":
# 文件上传控件处理
file_type_schema = {
"type": "object",
"description": param_info["label"],
"properties": {
"file_url": {"type": "string", "description": "文件URL"},
"file_name": {"type": "string", "description": "文件名称"},
},
"required": ["file_url"],
}
# 处理图片上传配置
if "image" in param_info and param_info["image"]["enabled"]:
image_config = param_info["image"]
file_type_schema["properties"]["type"] = {
"type": "string",
"description": "文件类型支持png、jpg、jpeg、webp、gif",
"enum": ["png", "jpg", "jpeg", "webp", "gif"],
}
# 处理数量限制
number_limits = image_config.get("number_limits", 3)
if number_limits > 1:
# 如果允许多个文件,则使用数组
inputSchema["properties"][property_name] = {
"type": "array",
"description": param_info["label"],
"items": file_type_schema,
"maxItems": number_limits,
}
else:
# 如果只允许单个文件
inputSchema["properties"][property_name] = file_type_schema
else:
# 如果没有特定的图片配置,使用一般文件配置
inputSchema["properties"][property_name] = file_type_schema
else:
# 默认处理为字符串类型
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
}
# 处理必填字段
if param_info.get("required", False):
inputSchema["required"].append(property_name)
# 添加必填的userId参数支持数字或字符串类型
# inputSchema["properties"]["userId"] = dict(
# oneOf=[{"type": "number"}, {"type": "string"}],
# description="您的员工ID用于识别您的员工身份",
# )
# inputSchema["required"].append("userId")
return inputSchema
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
列出可用的工具
返回:
工具列表每个工具都使用JSON Schema验证其参数
"""
tools = []
tool_names = dify_api.dify_app_names
tool_infos = dify_api.dify_app_infos
tool_params = dify_api.dify_app_params
tool_num = len(tool_names)
for i in range(tool_num):
# 加载每个工具的应用信息
app_info = tool_infos[i]
# 加载每个工具的应用参数
app_param = tool_params[i]
# 处理用户输入表单
inputSchema = process_user_input_form(app_param["user_input_form"])
tools.append(
types.Tool(
name=app_info["name"],
description=app_info["description"],
inputSchema=inputSchema,
)
)
return tools
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
调用工具处理请求
参数:
name: 工具名称
arguments: 工具参数
返回:
处理结果列表
"""
tool_names = dify_api.dify_app_names
if name in tool_names:
tool_idx = tool_names.index(name)
tool_sk = dify_api.dify_app_sks[tool_idx]
responses = dify_api.chat_message(
tool_sk,
inputs=arguments,
userId=arguments.get("userId", "pp666"),
)
for res in responses:
if res["event"] == "workflow_finished":
outputs = res["data"]["outputs"]
mcp_out = []
for _, v in outputs.items():
mcp_out.append(types.TextContent(type="text", text=v))
return mcp_out
else:
raise ValueError(f"Unknown tool: {name}")
def run_main(transport="stdio"):
"""
主函数使用stdin/stdout流运行服务器
"""
if transport == "stdio":
import anyio
from mcp.server.stdio import stdio_server
async def arun():
async with stdio_server() as streams:
await server.run(
streams[0],
streams[1],
InitializationOptions(
server_name="dify_mcp_server",
server_version="0.0.6",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
anyio.run(arun)
else:
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Mount, Route
sse = SseServerTransport("/messages/")
async def handle_sse(request):
async with sse.connect_sse(
request.scope, request.receive, request._send
) as streams:
await server.run(
streams[0], streams[1], server.create_initialization_options()
)
return Response()
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse, methods=["GET"]),
Mount("/messages/", app=sse.handle_post_message),
],
)
import uvicorn
uvicorn.run(starlette_app, host="0.0.0.0", port=8080, log_level="info")
if __name__ == "__main__":
run_main()

View File

@@ -1,373 +0,0 @@
import json
from typing import Dict, List, Any, Tuple, Optional
import logging
from pathlib import Path
# 常量定义
DEFAULT_NUMBER_LIMITS = 3 # 默认文件数量限制
def process_user_input_form(
user_input_form: List[Dict[str, Any]],
) -> Tuple[Dict[str, Any], List[str]]:
"""
处理Dify用户输入表单生成对应的JSON Schema properties和required列表
参数:
user_input_form: Dify应用的用户输入表单配置
返回:
properties: 表单字段的properties字典
required: 必填字段列表
"""
properties = {}
required = []
if not user_input_form:
return properties, required
for param in user_input_form:
try:
# 直接获取字典的第一个键,而不是通过list转换
param_type = next(iter(param))
param_info = param[param_type]
property_name = param_info["variable"]
properties[property_name] = {
"type": param_type,
"description": param_info["label"],
}
if param_info.get("required", False):
required.append(property_name)
except (KeyError, StopIteration) as e:
logging.warning(f"处理用户输入表单项时出错: {e}, 跳过此项")
continue
return properties, required
def process_file_upload(file_upload: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""
处理Dify文件上传配置生成对应的JSON Schema properties
设计为通用实现,支持任意文件类型配置
参数:
file_upload: Dify应用的文件上传配置
返回:
file_properties: 文件上传的properties字典
"""
# 检查是否存在文件上传配置
if not file_upload:
return {}
# 收集所有启用的文件类型信息
enabled_types = []
max_items = 0
supported_transfer_methods = set()
file_type_configs = {}
for file_type, config in file_upload.items():
if not config.get("enabled", False):
continue
enabled_types.append(file_type)
number_limits = config.get("number_limits", DEFAULT_NUMBER_LIMITS)
max_items += number_limits
type_transfer_methods = config.get("transfer_methods", [])
supported_transfer_methods.update(type_transfer_methods)
# 存储每种文件类型的详细配置
file_type_configs[file_type] = {
"number_limits": number_limits,
"transfer_methods": type_transfer_methods,
}
# 如果没有启用的文件类型,返回空字典
if not enabled_types:
return {}
# 构建文件项的JSON Schema
file_item_schema = _build_file_item_schema(
enabled_types, supported_transfer_methods, file_type_configs
)
# 构建最终的files属性
file_properties = {
"files": {
"type": "array",
"items": file_item_schema,
"description": "支持多种文件类型的文件列表",
"maxItems": max_items,
}
}
return file_properties
def _build_file_item_schema(
enabled_types: List[str],
supported_transfer_methods: set,
file_type_configs: Dict[str, Dict[str, Any]],
) -> Dict[str, Any]:
"""
构建文件项的JSON Schema
参数:
enabled_types: 启用的文件类型列表
supported_transfer_methods: 支持的传输方式集合
file_type_configs: 每种文件类型的配置信息
返回:
file_item_schema: 文件项的JSON Schema
"""
file_item_schema = {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": enabled_types,
"description": "文件类型",
},
"transfer_method": {
"type": "string",
"enum": list(supported_transfer_methods),
"description": "传输方式",
},
},
}
# 添加条件属性
if "remote_url" in supported_transfer_methods:
file_item_schema["properties"]["url"] = {
"type": "string",
"format": "uri",
"description": "文件URL (当传输方式为remote_url时使用)",
}
if "local_file" in supported_transfer_methods:
file_item_schema["properties"]["upload_file_id"] = {
"type": "string",
"description": "上传文件ID (当传输方式为local_file时使用)",
}
# 添加条件验证逻辑
file_item_schema["allOf"] = []
# 为每种文件类型添加验证规则
for file_type, type_config in file_type_configs.items():
type_methods = type_config["transfer_methods"]
# 基本验证
file_item_schema["allOf"].append(
{
"if": {"properties": {"type": {"const": file_type}}},
"then": {
"properties": {
"transfer_method": {
"enum": type_methods,
"description": f"{file_type}类型支持的传输方式: {', '.join(type_methods)}",
}
}
},
}
)
# 为每种传输方法添加类型特定的验证
_add_transfer_method_validations(file_item_schema, file_type, type_methods)
return file_item_schema
def _add_transfer_method_validations(
file_item_schema: Dict[str, Any], file_type: str, type_methods: List[str]
) -> None:
"""
为每种传输方法添加类型特定的验证
参数:
file_item_schema: 文件项JSON Schema
file_type: 文件类型
type_methods: 该类型支持的传输方法列表
"""
for method in type_methods:
if method == "remote_url":
file_item_schema["allOf"].append(
{
"if": {
"properties": {
"type": {"const": file_type},
"transfer_method": {"const": "remote_url"},
},
"required": ["type", "transfer_method"],
},
"then": {"required": ["url"]},
}
)
elif method == "local_file":
file_item_schema["allOf"].append(
{
"if": {
"properties": {
"type": {"const": file_type},
"transfer_method": {"const": "local_file"},
},
"required": ["type", "transfer_method"],
},
"then": {"required": ["upload_file_id"]},
}
)
def convert_dify_params_to_schema(tool_params: Dict[str, Any]) -> Dict[str, Any]:
"""
将用户输入表单和文件上传配置组合成新的结构
参数:
tool_params: 包含user_input_form和file_upload的参数字典
返回:
组合后的结构
"""
# 参数验证
if not isinstance(tool_params, dict):
raise TypeError("tool_params 必须是字典类型")
# 处理用户输入表单
properties, required = process_user_input_form(
tool_params.get("user_input_form", [])
)
# 处理文件上传配置
file_properties = process_file_upload(tool_params.get("file_upload"))
# 创建新的结构
result = {
"inputs": {"type": "object", "properties": properties, "required": required},
# "userId": {
# "oneOf": [{"type": "number"}, {"type": "string"}],
# "description": "您的员工ID用于识别您的员工身份",
# "required": True,
# },
}
# 如果有文件上传配置添加files字段
if file_properties:
result["files"] = file_properties.get("files", {})
return result
def finalize_schema_structure(mock_result: Dict[str, Any]) -> Dict[str, Any]:
"""
将mock_result构建为符合要求的map_mock_result格式
参数:
mock_result: 通过convert_dify_params_to_schema函数获取的结果
返回:
构建后的map_mock_result字典
"""
# 确定required字段
# required_fields = ["userId"]
required_fields = []
# 只有当inputs的required有值时才添加inputs到顶层required
if (
mock_result.get("inputs", {}).get("required")
and len(mock_result["inputs"]["required"]) > 0
):
required_fields.append("inputs")
# 如果有文件上传也可以考虑添加files到required
if "files" in mock_result:
# 可以根据需求决定是否将files添加到required
# required_fields.append("files")
pass
return {
"type": "object",
"properties": mock_result,
"required": required_fields,
}
def create_json_file(data: Dict[str, Any], filename: str = "output.json") -> None:
"""
将数据保存为JSON文件
参数:
data: 要保存的数据
filename: 保存的文件名
"""
try:
# 获取mock数据
mock_result = convert_dify_params_to_schema(data)
# 使用封装的方法构建map_mock_result
map_mock_result = finalize_schema_structure(mock_result)
# 确保目标目录存在
output_path = Path(filename)
output_path.parent.mkdir(parents=True, exist_ok=True)
# 将结果写入JSON文件
with open(filename, "w", encoding="utf-8") as f:
json.dump(map_mock_result, f, ensure_ascii=False, indent=4)
logging.info(f"已成功将数据保存至 {filename}")
print(f"已成功将数据保存至 {filename}")
except Exception as e:
error_msg = f"保存JSON文件时出错: {e}"
logging.error(error_msg)
raise IOError(error_msg)
if __name__ == "__main__":
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
# 示例数据
api_data = {
"opening_statement": "",
"suggested_questions": [],
"suggested_questions_after_answer": {"enabled": False},
"speech_to_text": {"enabled": False},
"text_to_speech": {"enabled": False, "language": "", "voice": ""},
"retriever_resource": {"enabled": False},
"annotation_reply": {"enabled": False},
"more_like_this": {"enabled": False},
"user_input_form": [
{
"paragraph": {
"label": "文案内容",
"max_length": 33024,
"options": [],
"required": True,
"type": "paragraph",
"variable": "content",
}
}
],
"sensitive_word_avoidance": {"enabled": False},
"file_upload": {
"image": {
"enabled": False,
"number_limits": 3,
"transfer_methods": ["local_file", "remote_url"],
}
},
"system_parameters": {"image_file_size_limit": "10"},
}
try:
create_json_file(api_data)
except Exception as e:
logging.error(f"程序执行失败: {e}")

View File

@@ -1,53 +0,0 @@
from abc import ABC
# class WorkflowDifyAPI和chatDifyApi和completionDifyApi
# dify_app_mode_type workflow, chat, completion
class TaskInstance(ABC):
def __init__(self, base_url, dify_app_sks, dify_app_mode_type):
self.base_url = base_url
self.dify_app_sks = dify_app_sks
self.dify_app_mode_type = dify_app_mode_type
def get_task_instance(self, task_id: str):
"""
根据dify_app_mode_type返回相应的API实例
Args:
task_id: 任务ID
Returns:
返回对应的API实例
Raises:
ValueError: 当dify_app_mode_type无效时抛出异常
"""
from src.workflow.workflow_server import WorkflowDifyAPI
from src.completion.completion_server import CompletionDifyAPI
from src.chat.chat_server import ChatDifyAPI
# 使用字典映射提高代码灵活性和可维护性
api_classes = {
"workflow": WorkflowDifyAPI,
"chat": ChatDifyAPI,
"completion": CompletionDifyAPI,
}
# 检查mode_type是否有效
if self.dify_app_mode_type.lower() not in api_classes:
supported_types = ", ".join(api_classes.keys())
raise ValueError(
f"不支持的dify_app_mode_type: {self.dify_app_mode_type},支持的类型: {supported_types}"
)
# 获取对应的API类
api_class = api_classes[self.dify_app_mode_type.lower()]
# 这里假设所有API类都接受相同的参数集
# 如果各API类构造函数参数不同需要针对每种类型单独处理
return api_class(
self.base_url,
self.dify_app_sks,
)

View File

@@ -1,153 +0,0 @@
import os
import sys
from typing import Dict, Any, Optional
# 导入翻译函数
from .translator import translate
class TranslationService:
"""翻译服务类,用于处理各种翻译需求"""
@staticmethod
def create_prompt(
content: str,
target_lang: str,
use_case: str,
style: str,
prompt_type: str = "general",
keep_terms_desc: str = "核心术语",
) -> str:
"""
创建翻译提示
Args:
content: 待翻译内容
target_lang: 目标语言
use_case: 使用场景
style: 翻译风格
prompt_type: 提示类型,可选值: "general", "tool_name", "tool_description"
keep_terms_desc: 保留术语的描述
Returns:
str: 格式化的翻译提示
"""
if prompt_type == "tool_name":
keep_terms_desc = "核心术语(如 小写,词语需要用下划线连接)"
elif prompt_type == "tool_description":
keep_terms_desc = "核心术语(这是一段话)"
return f"""
角色:专业本地化翻译专家
任务:将以下内容翻译为{target_lang}(目标用途:{use_case}
要求:
1. 仅返回译文,不含解释或原文;
2. 保留{keep_terms_desc}
3. 符合{style}风格;
4. 特殊符号保持原样。
示例输出格式:
Translated Text
待翻译内容:
{content}
"""
@staticmethod
def translate_text(
content: str,
target_lang: str,
use_case: str = "",
style: str = "正式且符合技术品牌调性",
prompt_type: str = "general",
) -> Dict[str, Any]:
"""
翻译文本
Args:
content: 待翻译内容
target_lang: 目标语言
use_case: 使用场景,默认为空
style: 翻译风格,默认为"正式且符合技术品牌调性"
prompt_type: 提示类型,可选值: "general", "tool_name", "tool_description"
Returns:
Dict: 包含翻译结果的字典
"""
prompt = TranslationService.create_prompt(
content=content,
target_lang=target_lang,
use_case=use_case,
style=style,
prompt_type=prompt_type,
)
try:
result = translate(prompt, target_lang)
return result
except Exception as e:
print(f"翻译出错: {str(e)}")
return {"translated_text": "", "error": str(e)}
@staticmethod
def translate_tool_name(
name: str,
target_lang: str = "英语",
use_case: str = "工具名称",
style: str = "正式且符合技术品牌调性,大模型能理解",
) -> str:
"""
翻译工具名称的便捷方法
Returns:
str: 翻译后的工具名称
"""
result = TranslationService.translate_text(
content=name,
target_lang=target_lang,
use_case=use_case,
style=style,
prompt_type="tool_name",
)
return result.get("translated_text", "")
@staticmethod
def translate_tool_description(
description: str,
target_lang: str = "英语",
use_case: str = "工具描述",
style: str = "正式且符合技术品牌调性,大模型能理解",
) -> str:
"""
翻译工具描述的便捷方法
Returns:
str: 翻译后的工具描述
"""
result = TranslationService.translate_text(
content=description,
target_lang=target_lang,
use_case=use_case,
style=style,
prompt_type="tool_description",
)
return result.get("translated_text", "")
def translation_example():
"""翻译功能使用示例"""
# 示例1: 翻译工具名称
tool_name = "万川AI新媒体平台【测试环境】"
translated_name = TranslationService.translate_tool_name(tool_name)
print(f"工具名称翻译: {translated_name}")
# 示例2: 翻译工具描述
description = "21日辛柏青发布讣告宣布妻子朱媛媛抗癌五年后离世。此前在一次路演现场当观众问及朱媛媛时辛柏青2秒停顿藏着"
translated_desc = TranslationService.translate_tool_description(description)
print(f"工具描述翻译: {translated_desc}")
if __name__ == "__main__":
translation_example()

View File

@@ -1,64 +0,0 @@
import os
import requests
import json
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
# ========== 模型相关 ==========
# 从.env文件获取模型API配置
BASE_URL = os.getenv("BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1")
API_KEY = os.getenv("OPENAI_API_KEY", "sk-c5a912a6bc8e4c9cbdbdf68232352a03")
TEMPERATURE = float(os.getenv("MODEL_TEMPERATURE", "0.7"))
def translate(content, target_language):
"""
翻译文本内容到目标语言
:param content: 要翻译的内容
:param target_language: 目标语言,如'en'(英语), 'zh'(中文), 'ja'(日语), 'fr'(法语)等
:return: 翻译后的内容,如果翻译失败则返回原文和错误信息
"""
if not content or not target_language:
return {"error": "内容或目标语言不能为空", "translated_text": content}
# 确保API密钥已设置
if not API_KEY:
return {"error": "API密钥未设置请检查.env文件", "translated_text": content}
try:
# 构建API请求头
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}",
}
# 构建翻译提示
prompt = f"请将以下内容翻译成{target_language},只返回翻译结果,不要包含任何解释或原文:\n\n{content}"
# 构建API请求体
data = {
"model": "qwen-max", # 使用通义千问模型,可以根据实际需要更改
"messages": [{"role": "user", "content": prompt}],
"temperature": TEMPERATURE,
}
# 发送API请求
response = requests.post(
f"{BASE_URL}/chat/completions", headers=headers, json=data
)
# 解析响应
if response.status_code == 200:
result = response.json()
translated_text = result["choices"][0]["message"]["content"].strip()
return {"translated_text": translated_text}
else:
error_message = (
f"翻译失败,状态码: {response.status_code}, 响应: {response.text}"
)
return {"error": error_message, "translated_text": content}
except Exception as e:
return {"error": f"翻译过程中发生错误: {str(e)}", "translated_text": content}

View File

@@ -1,175 +0,0 @@
import requests
from abc import ABC
import logging
import json
import re
logger = logging.getLogger(__name__)
import pypinyin
def pinyin_to_camel(pinyin):
"""
将拼音列表转换为驼峰命名
例如: ['ni', 'hao', 'a'] -> 'NiHaoA'
所有非字母数字字符会被替换为下划线
"""
# 使用正则表达式将所有非字母数字字符(包括所有标点符号)替换为下划线
cleaned = re.sub(r'[^\w\s]', '_', pinyin)
# 将空格也替换为下划线
cleaned = re.sub(r'\s+', '_', cleaned)
# 移除连续的下划线并去除首尾下划线
cleaned = re.sub(r'_+', '_', cleaned).strip('_')
# 转换为拼音并生成驼峰命名
pinyin_list = pypinyin.lazy_pinyin(cleaned)
return "tool_" + "".join(word.capitalize() for word in pinyin_list)
class WorkflowDifyAPI(ABC):
def __init__(self, base_url: str, dify_app_sks: list, user="pp666"):
# dify configs
self.dify_base_url = base_url
self.dify_app_sks = dify_app_sks
self.user = user
# dify app infos
dify_app_infos = []
dify_app_params = []
dify_app_metas = []
for key in self.dify_app_sks:
dify_app_infos.append(self.get_app_info(key))
dify_app_params.append(self.get_app_parameters(key))
dify_app_metas.append(self.get_app_meta(key))
self.dify_app_infos = dify_app_infos
self.dify_app_params = dify_app_params
self.dify_app_metas = dify_app_metas
self.dify_app_names = [x["name"] for x in dify_app_infos]
def chat_message(
self,
api_key,
inputs={},
response_mode="streaming",
conversation_id=None,
userId="pp666",
files=None,
):
url = f"{self.dify_base_url}/workflows/run"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": userId,
}
logger.info("Sending data to Dify API: %s", data)
logger.info("Sending headers to Dify API: %s", headers)
logger.info("Sending url to Dify API: %s", url)
if conversation_id:
data["conversation_id"] = conversation_id
if files:
files_data = []
for file_info in files:
file_path = file_info.get("path")
transfer_method = file_info.get("transfer_method")
if transfer_method == "local_file":
files_data.append(("file", open(file_path, "rb")))
elif transfer_method == "remote_url":
pass
response = requests.post(
url,
headers=headers,
data=data,
files=files_data,
stream=response_mode == "streaming",
)
else:
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
response.raise_for_status()
if response_mode == "streaming":
for line in response.iter_lines():
if line:
if line.startswith(b"data:"):
try:
json_data = json.loads(line[5:].decode("utf-8"))
yield json_data
except json.JSONDecodeError:
print(f"Error decoding JSON: {line}")
else:
return response.json()
def upload_file(self, api_key, file_path, user="pp666"):
url = f"{self.dify_base_url}/files/upload"
headers = {"Authorization": f"Bearer {api_key}"}
files = {"file": open(file_path, "rb")}
data = {"user": user}
response = requests.post(url, headers=headers, files=files, data=data)
response.raise_for_status()
return response.json()
def stop_response(self, api_key, task_id, user="pp666"):
url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {"user": user}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
def get_app_info(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/info"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
from src.utils.tool_translation import TranslationService
response_map = response.json()
# 翻译工具名称
tool_name = response_map.get("name")
if tool_name:
# translated_name = TranslationService.translate_tool_name(tool_name)
translated_name = pinyin_to_camel(tool_name)
response_map["name"] = translated_name
# 翻译工具描述
# tool_description = response_map.get("description")
# if tool_description:
# translated_description = TranslationService.translate_tool_description(
# tool_description
# )
# response_map["description"] = (
# f"{tool_description} ({translated_description})"
# )
return response_map
def get_app_parameters(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/parameters"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
def get_app_meta(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/meta"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()

View File

@@ -1,12 +0,0 @@
Metadata-Version: 2.4
Name: lzwcai-demp-tool-server-dify-to-mcp-test
Version: 0.0.15
Summary: 这是一个Dify to MCP的集成工具通过Dify的API接口将Dify的模型部署到MCP平台并进行推理。
Requires-Python: >=3.10
Requires-Dist: httpx>=0.28.1
Requires-Dist: mcp>=1.1.2
Requires-Dist: omegaconf>=2.3.0
Requires-Dist: pip>=24.3.1
Requires-Dist: python-dotenv>=1.0.1
Requires-Dist: requests
Requires-Dist: pypinyin>=0.54.0

View File

@@ -1,267 +0,0 @@
# Dify Workflow API 文档
## 概述
Workflow 应用无会话支持,适合用于翻译、文章写作、总结等 AI 场景。
**Base URL:** `http://192.168.2.236:3001/v1`
## 认证
所有 API 请求需在 HTTP Header 中包含 API-Key:
```
Authorization: Bearer {API_KEY}
```
## 1. 执行 Workflow
**接口:** `POST /workflows/run`
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| inputs | object | ✓ | 允许传入 App 定义的各变量值。inputs 参数包含了多组键值对,每组的键对应一个特定变量,每组的值则是该变量的具体值 |
| response_mode | string | ✓ | 返回响应模式:`streaming`(流式,推荐)或`blocking`(阻塞,Cloudflare 限制 100 秒超时) |
| user | string | ✓ | 用户标识,用于定义终端用户的身份,方便检索、统计。需保证用户标识在应用内唯一 |
### 文件列表类型变量
| 字段 | 类型 | 说明 |
|------|------|------|
| type | string | 文件类型:document/image/audio/video/custom |
| transfer_method | string | 传递方式:remote_url(图片地址)或local_file(上传文件) |
| url | string | 图片地址(仅当传递方式为 remote_url 时) |
| upload_file_id | string | 上传文件 ID(仅当传递方式为 local_file 时) |
**支持的文件类型:**
- **document**: TXT, MD, PDF, HTML, XLSX, DOCX, CSV, PPTX, XML, EPUB
- **image**: JPG, PNG, GIF, WEBP, SVG
- **audio**: MP3, WAV, M4A, WEBM, AMR
- **video**: MP4, MOV, MPEG
### 响应格式
#### CompletionResponse(阻塞模式)
| 字段 | 类型 | 说明 |
|------|------|------|
| workflow_run_id | string | workflow 执行 ID |
| task_id | string | 任务 ID,用于请求跟踪和停止响应接口 |
| data.id | string | workflow 执行 ID |
| data.workflow_id | string | 关联 Workflow ID |
| data.status | string | 执行状态:running/succeeded/failed/stopped |
| data.outputs | json | 可选,输出内容 |
| data.error | string | 可选,错误原因 |
| data.elapsed_time | float | 可选,耗时(秒) |
| data.total_tokens | int | 可选,总使用 tokens |
| data.total_steps | int | 总步数,默认 0 |
| data.created_at | timestamp | 开始时间 |
| data.finished_at | timestamp | 结束时间 |
#### ChunkCompletionResponse(流式模式)
**事件类型:**
**1. workflow_started**
| 字段 | 类型 | 说明 |
|------|------|------|
| task_id | string | 任务 ID |
| workflow_run_id | string | workflow 执行 ID |
| event | string | 固定为 workflow_started |
| data.id | string | workflow 执行 ID |
| data.workflow_id | string | 关联 Workflow ID |
| data.sequence_number | int | 自增序号,从 1 开始 |
| data.created_at | timestamp | 开始时间 |
**2. node_started**
| 字段 | 类型 | 说明 |
|------|------|------|
| data.node_id | string | 节点 ID |
| data.node_type | string | 节点类型 |
| data.title | string | 节点名称 |
| data.index | int | 执行序号 |
| data.predecessor_node_id | string | 前置节点 ID |
| data.inputs | object | 节点中所有使用到的前置节点变量内容 |
**3. node_finished**
| 字段 | 类型 | 说明 |
|------|------|------|
| data.node_id | string | 节点 ID |
| data.status | string | 执行状态:running/succeeded/failed/stopped |
| data.outputs | json | 可选,输出内容 |
| data.error | string | 可选,错误原因 |
| data.elapsed_time | float | 可选,耗时(秒) |
| data.execution_metadata.total_tokens | int | 可选,总使用 tokens |
| data.execution_metadata.total_price | decimal | 可选,总费用 |
| data.execution_metadata.currency | string | 可选,货币(USD/RMB) |
**4. workflow_finished**
| 字段 | 类型 | 说明 |
|------|------|------|
| data.status | string | 执行状态:running/succeeded/failed/stopped |
| data.outputs | json | 可选,输出内容 |
| data.error | string | 可选,错误原因 |
| data.total_tokens | int | 可选,总使用 tokens |
| data.finished_at | timestamp | 结束时间 |
**5. tts_message** - TTS 音频流事件(Mp3 格式,base64 编码)
| 字段 | 类型 | 说明 |
|------|------|------|
| task_id | string | 任务 ID |
| message_id | string | 消息唯一 ID |
| audio | string | Base64 编码的音频内容 |
| created_at | int | 创建时间戳 |
**6. tts_message_end** - TTS 音频流结束
**7. ping** - 每 10 秒心跳保活
### 错误码
| 状态码 | 错误码 | 说明 |
|--------|--------|------|
| 400 | invalid_param | 传入参数异常 |
| 400 | app_unavailable | App 配置不可用 |
| 400 | provider_not_initialize | 无可用模型凭据配置 |
| 400 | provider_quota_exceeded | 模型调用额度不足 |
| 400 | workflow_request_error | workflow 执行失败 |
| 500 | - | 服务内部异常 |
---
## 2. 查询 Workflow 执行状态
**接口:** `GET /workflows/run/:workflow_run_id`
### 响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | workflow 执行 ID |
| workflow_id | string | 关联的 Workflow ID |
| status | string | 执行状态:running/succeeded/failed/stopped |
| inputs | json | 任务输入内容 |
| outputs | json | 任务输出内容 |
| error | string | 错误原因 |
| total_steps | int | 任务执行总步数 |
| total_tokens | int | 任务执行总 tokens |
| created_at | timestamp | 任务开始时间 |
| finished_at | timestamp | 任务结束时间 |
| elapsed_time | float | 耗时(秒) |
---
## 3. 停止 Workflow 执行
**接口:** `POST /workflows/tasks/:task_id/stop`
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| user | string | ✓ | 用户标识,必须和发送消息接口传入 user 保持一致 |
---
## 4. 上传文件
**接口:** `POST /files/upload`
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file | file | ✓ | 要上传的文件 |
| user | string | ✓ | 用户标识 |
### 响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | uuid | ID |
| name | string | 文件名 |
| size | int | 文件大小(byte) |
| extension | string | 文件后缀 |
| mime_type | string | 文件 mime-type |
| created_by | uuid | 上传人 ID |
| created_at | timestamp | 上传时间 |
### 错误码
| 状态码 | 错误码 | 说明 |
|--------|--------|------|
| 400 | no_file_uploaded | 必须提供文件 |
| 413 | file_too_large | 文件太大 |
| 415 | unsupported_file_type | 不支持的文件类型 |
| 503 | s3_connection_failed | 无法连接到 S3 服务 |
---
## 5. 获取 Workflow 日志
**接口:** `GET /workflows/logs`
### 查询参数
| 参数 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| keyword | string | 关键字 | - |
| status | string | 执行状态:succeeded/failed/stopped | - |
| page | int | 当前页码 | 1 |
| limit | int | 每页条数 | 20 |
### 响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| page | int | 当前页码 |
| limit | int | 每页条数 |
| total | int | 总条数 |
| has_more | bool | 是否还有更多数据 |
| data[].workflow_run.id | string | 标识 |
| data[].workflow_run.status | string | 执行状态 |
| data[].workflow_run.elapsed_time | float | 耗时(秒) |
| data[].workflow_run.total_tokens | int | 消耗的 token 数量 |
---
## 6. 获取应用信息
**接口:** `GET /info`
### 响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| name | string | 应用名称 |
| description | string | 应用描述 |
| tags | array[string] | 应用标签 |
---
## 7. 获取应用参数
**接口:** `GET /parameters`
### 响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| user_input_form[].text-input.label | string | 控件展示标签名 |
| user_input_form[].text-input.variable | string | 控件 ID |
| user_input_form[].text-input.required | bool | 是否必填 |
| user_input_form[].text-input.default | string | 默认值 |
| file_upload.image.enabled | bool | 是否开启 |
| file_upload.image.number_limits | int | 图片数量限制,默认 3 |
| file_upload.image.transfer_methods | array[string] | 传递方式:remote_url/local_file |
| system_parameters.file_size_limit | int | 文档上传大小限制(MB) |
| system_parameters.image_file_size_limit | int | 图片文件上传大小限制(MB) |
| system_parameters.audio_file_size_limit | int | 音频文件上传大小限制(MB) |
| system_parameters.video_file_size_limit | int | 视频文件上传大小限制(MB) |

View File

@@ -1,2 +0,0 @@
# 此文件用于确保 logs 目录被 Git 跟踪
# 日志文件会自动生成在此目录中

View File

@@ -1,156 +0,0 @@
2026-01-19 16:05:23 - src.utils.logger_config - INFO - [logger_config.py:213] - ================================================================================
2026-01-19 16:05:23 - src.utils.logger_config - INFO - [logger_config.py:214] - 日志系统初始化完成 - 2026-01-19 16:05:23
2026-01-19 16:05:23 - src.utils.logger_config - INFO - [logger_config.py:215] - 日志级别: INFO
2026-01-19 16:05:23 - src.utils.logger_config - INFO - [logger_config.py:216] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_demp_tool_server_dify_to_mcp_test\lzwcai_demp_tool_server_dify_to_mcp_test\logs\lzwcai_demp_tool_server_dify_to_mcp_test.log
2026-01-19 16:05:23 - src.utils.logger_config - INFO - [logger_config.py:217] - 控制台输出: False
2026-01-19 16:05:23 - src.utils.logger_config - INFO - [logger_config.py:218] - 文件输出: True
2026-01-19 16:05:23 - src.utils.logger_config - INFO - [logger_config.py:219] - 文件轮转: 最大10MB, 保留5个备份
2026-01-19 16:05:23 - src.utils.logger_config - INFO - [logger_config.py:220] - ================================================================================
2026-01-19 16:05:23 - __main__ - INFO - [main.py:50] - ================================================================================
2026-01-19 16:05:23 - __main__ - INFO - [main.py:51] - Dify MCP 服务器启动
2026-01-19 16:05:23 - __main__ - INFO - [main.py:52] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_demp_tool_server_dify_to_mcp_test\lzwcai_demp_tool_server_dify_to_mcp_test\logs\lzwcai_demp_tool_server_dify_to_mcp_test.log
2026-01-19 16:05:23 - __main__ - INFO - [main.py:53] - ================================================================================
2026-01-19 16:05:25 - src.workflow.workflow_server - INFO - [workflow_server.py:322] - 调用 /parameters API: http://192.168.11.24:3001/v1/parameters
2026-01-19 16:05:25 - src.workflow.workflow_server - INFO - [workflow_server.py:323] - 请求头: {'Authorization': 'Bearer app-RM7IGxvdq48ixu1JXFiUZh76'}
2026-01-19 16:05:25 - src.workflow.workflow_server - INFO - [workflow_server.py:324] - 请求参数: {'user': 'pp666'}
2026-01-19 16:05:25 - src.workflow.workflow_server - INFO - [workflow_server.py:328] - /parameters API 响应状态码: 200
2026-01-19 16:05:25 - src.workflow.workflow_server - INFO - [workflow_server.py:333] - /parameters API 响应数据: {'opening_statement': '', 'suggested_questions': [], 'suggested_questions_after_answer': {'enabled': False}, 'speech_to_text': {'enabled': False}, 'text_to_speech': {'enabled': False, 'voice': '', 'language': ''}, 'retriever_resource': {'enabled': True}, 'annotation_reply': {'enabled': False}, 'more_like_this': {'enabled': False}, 'user_input_form': [{'file': {'variable': 'contact_file', 'label': 'contact_file', 'type': 'file', 'max_length': 4096, 'required': True, 'options': [], 'allowed_file_upload_methods': ['remote_url'], 'allowed_file_types': ['document'], 'allowed_file_extensions': []}}], 'sensitive_word_avoidance': {'enabled': False}, 'file_upload': {'image': {'enabled': False, 'number_limits': 3, 'transfer_methods': ['local_file', 'remote_url']}, 'enabled': False, 'allowed_file_types': ['image'], 'allowed_file_extensions': ['.JPG', '.JPEG', '.PNG', '.GIF', '.WEBP', '.SVG'], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'number_limits': 3, 'fileUploadConfig': {'file_size_limit': 15, 'batch_count_limit': 5, 'image_file_size_limit': 10, 'video_file_size_limit': 100, 'audio_file_size_limit': 50, 'workflow_file_upload_limit': 10}}, 'system_parameters': {'image_file_size_limit': 10, 'video_file_size_limit': 100, 'audio_file_size_limit': 50, 'file_size_limit': 15, 'workflow_file_upload_limit': 10}}
2026-01-19 16:05:25 - __main__ - INFO - [main.py:68] - 传输模式: stdio
2026-01-19 16:05:25 - __main__ - INFO - [main.py:69] - 配置参数: {'base_url': 'http://192.168.11.24:3001/v1', 'app_sks': ['app-RM7IGxvdq48ixu1JXFiUZh76'], 'mode_type': 'workflow', 'transport': 'stdio'}
2026-01-19 16:05:26 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest
2026-01-19 16:05:26 - src.create_mcp - INFO - [create_mcp.py:104] - 工具 tool_HeTongShenChaGongZuoLiu 的 parameters 数据: {'opening_statement': '', 'suggested_questions': [], 'suggested_questions_after_answer': {'enabled': False}, 'speech_to_text': {'enabled': False}, 'text_to_speech': {'enabled': False, 'voice': '', 'language': ''}, 'retriever_resource': {'enabled': True}, 'annotation_reply': {'enabled': False}, 'more_like_this': {'enabled': False}, 'user_input_form': [{'file': {'variable': 'contact_file', 'label': 'contact_file', 'type': 'file', 'max_length': 4096, 'required': True, 'options': [], 'allowed_file_upload_methods': ['remote_url'], 'allowed_file_types': ['document'], 'allowed_file_extensions': []}}], 'sensitive_word_avoidance': {'enabled': False}, 'file_upload': {'image': {'enabled': False, 'number_limits': 3, 'transfer_methods': ['local_file', 'remote_url']}, 'enabled': False, 'allowed_file_types': ['image'], 'allowed_file_extensions': ['.JPG', '.JPEG', '.PNG', '.GIF', '.WEBP', '.SVG'], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'number_limits': 3, 'fileUploadConfig': {'file_size_limit': 15, 'batch_count_limit': 5, 'image_file_size_limit': 10, 'video_file_size_limit': 100, 'audio_file_size_limit': 50, 'workflow_file_upload_limit': 10}}, 'system_parameters': {'image_file_size_limit': 10, 'video_file_size_limit': 100, 'audio_file_size_limit': 50, 'file_size_limit': 15, 'workflow_file_upload_limit': 10}}
2026-01-19 16:05:26 - src.create_mcp - INFO - [create_mcp.py:110] - 工具 tool_HeTongShenChaGongZuoLiu 提取的文件字段: [{'variable': 'contact_file', 'label': 'contact_file', 'required': True, 'max_length': 4096, 'allowed_file_types': ['document'], 'allowed_file_upload_methods': ['remote_url'], 'allowed_file_extensions': [], 'is_list': False}]
2026-01-19 16:06:14 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type CallToolRequest
2026-01-19 16:06:14 - src.create_mcp - INFO - [create_mcp.py:144] - 工具 tool_HeTongShenChaGongZuoLiu 的文件字段信息: [{'variable': 'contact_file', 'label': 'contact_file', 'required': True, 'max_length': 4096, 'allowed_file_types': ['document'], 'allowed_file_upload_methods': ['remote_url'], 'allowed_file_extensions': [], 'is_list': False}]
2026-01-19 16:06:14 - src.create_mcp - INFO - [create_mcp.py:145] - 工具 tool_HeTongShenChaGongZuoLiu 调用前的 arguments: {'contact_file': [{'url': 'http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx'}]}
2026-01-19 16:06:14 - src.create_mcp_utils - INFO - [create_mcp_utils.py:64] - 工具 tool_HeTongShenChaGongZuoLiu 的文件字段变量名: {'contact_file'}
2026-01-19 16:06:14 - src.create_mcp_utils - INFO - [create_mcp_utils.py:73] - 工具 tool_HeTongShenChaGongZuoLiu: 发现文件字段 contact_file值: [{'url': 'http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx'}]
2026-01-19 16:06:14 - src.create_mcp_utils - INFO - [create_mcp_utils.py:168] - 工具 tool_HeTongShenChaGongZuoLiu: 处理文件列表: [{'url': 'http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx'}]
2026-01-19 16:06:14 - src.create_mcp_utils - INFO - [create_mcp_utils.py:85] - 工具 tool_HeTongShenChaGongZuoLiu: 准备处理文件列表: [{'url': 'http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx'}]
2026-01-19 16:06:14 - src.create_mcp_utils - INFO - [create_mcp_utils.py:322] - 从URL http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx 提取的文件扩展名: docx
2026-01-19 16:06:14 - src.create_mcp_utils - INFO - [create_mcp_utils.py:333] - URL http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx 匹配文件类型: document
2026-01-19 16:06:14 - src.create_mcp_utils - INFO - [create_mcp_utils.py:96] - 工具 tool_HeTongShenChaGongZuoLiu: 自动识别文件类型为 document
2026-01-19 16:06:14 - src.workflow.workflow_server - INFO - [workflow_server.py:242] - 开始上传远程文件: http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx
2026-01-19 16:06:14 - src.utils.upload_file - INFO - [upload_file.py:400] - 开始处理文件: http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx
2026-01-19 16:06:14 - src.utils.upload_file - INFO - [upload_file.py:86] - 正在下载文件: http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx (尝试 1/3)
2026-01-19 16:06:14 - src.utils.upload_file - INFO - [upload_file.py:100] - 文件下载成功: C:\Users\HiWin10\AppData\Local\Temp\eb25b2b233a8719f680d4b0b9c5a80e7.docx (大小: 20445 字节)
2026-01-19 16:06:14 - src.utils.upload_file - INFO - [upload_file.py:189] - 准备上传文件: C:\Users\HiWin10\AppData\Local\Temp\eb25b2b233a8719f680d4b0b9c5a80e7.docx (大小: 0.02 MB)
2026-01-19 16:06:14 - src.utils.upload_file - INFO - [upload_file.py:220] - 文件上传成功: eb25b2b233a8719f680d4b0b9c5a80e7.docx (ID: e2ec6720-9cf9-4a64-aa31-102e787a343b)
2026-01-19 16:06:14 - src.utils.upload_file - INFO - [upload_file.py:417] - 已清理临时文件: C:\Users\HiWin10\AppData\Local\Temp\eb25b2b233a8719f680d4b0b9c5a80e7.docx
2026-01-19 16:06:14 - src.workflow.workflow_server - INFO - [workflow_server.py:255] - 文件上传成功 - ID: e2ec6720-9cf9-4a64-aa31-102e787a343b, 名称: eb25b2b233a8719f680d4b0b9c5a80e7.docx, 大小: 20445 bytes
2026-01-19 16:06:14 - src.workflow.workflow_server - INFO - [workflow_server.py:274] - 文件预处理完成,共处理 1 个文件
2026-01-19 16:06:14 - src.create_mcp_utils - INFO - [create_mcp_utils.py:111] - 工具 tool_HeTongShenChaGongZuoLiu: 文件预处理完成,成功上传 1 个文件
2026-01-19 16:06:14 - src.create_mcp_utils - INFO - [create_mcp_utils.py:219] - 工具 tool_HeTongShenChaGongZuoLiu: 字段 contact_file 是 file 类型,输出单个对象
2026-01-19 16:06:14 - src.create_mcp_utils - INFO - [create_mcp_utils.py:221] - 工具 tool_HeTongShenChaGongZuoLiu: 字段 contact_file 最终值: {'transfer_method': 'local_file', 'type': 'document', 'upload_file_id': 'e2ec6720-9cf9-4a64-aa31-102e787a343b'}
2026-01-19 16:06:14 - src.create_mcp - INFO - [create_mcp.py:149] - 工具 tool_HeTongShenChaGongZuoLiu 处理后的 arguments: {'contact_file': {'transfer_method': 'local_file', 'type': 'document', 'upload_file_id': 'e2ec6720-9cf9-4a64-aa31-102e787a343b'}}
2026-01-19 16:06:14 - src.workflow.workflow_server - INFO - [workflow_server.py:131] - Sending data to Dify API: {'inputs': {'contact_file': {'transfer_method': 'local_file', 'type': 'document', 'upload_file_id': 'e2ec6720-9cf9-4a64-aa31-102e787a343b'}}, 'response_mode': 'streaming', 'user': 'pp666'}
2026-01-19 16:06:14 - src.workflow.workflow_server - INFO - [workflow_server.py:132] - Sending headers to Dify API: {'Authorization': 'Bearer app-RM7IGxvdq48ixu1JXFiUZh76', 'Content-Type': 'application/json'}
2026-01-19 16:06:14 - src.workflow.workflow_server - INFO - [workflow_server.py:133] - Sending url to Dify API: http://192.168.11.24:3001/v1/workflows/run
2026-01-19 16:06:14 - src.workflow.workflow_server - INFO - [workflow_server.py:148] - Response1:{'inputs': {'contact_file': {'transfer_method': 'local_file', 'type': 'document', 'upload_file_id': 'e2ec6720-9cf9-4a64-aa31-102e787a343b'}}, 'response_mode': 'streaming', 'user': 'pp666'} 400 BAD REQUEST
2026-01-19 16:06:14 - src.workflow.workflow_server - ERROR - [workflow_server.py:152] - API request failed with status 400
2026-01-19 16:06:14 - src.workflow.workflow_server - ERROR - [workflow_server.py:153] - Response content: {"code": "invalid_param", "message": "File validation failed for file: eb25b2b233a8719f680d4b0b9c5a80e7.docx", "status": 400}
2026-01-19 16:06:14 - src.workflow.workflow_server - ERROR - [workflow_server.py:154] - Request data: {'inputs': {'contact_file': {'transfer_method': 'local_file', 'type': 'document', 'upload_file_id': 'e2ec6720-9cf9-4a64-aa31-102e787a343b'}}, 'response_mode': 'streaming', 'user': 'pp666'}
2026-01-19 16:06:14 - src.create_mcp - ERROR - [create_mcp.py:178] - 工具 tool_HeTongShenChaGongZuoLiu 调用 Dify API 失败: [400] invalid_param: File validation failed for file: eb25b2b233a8719f680d4b0b9c5a80e7.docx
2026-01-19 16:21:31 - src.utils.logger_config - INFO - [logger_config.py:213] - ================================================================================
2026-01-19 16:21:31 - src.utils.logger_config - INFO - [logger_config.py:214] - 日志系统初始化完成 - 2026-01-19 16:21:31
2026-01-19 16:21:31 - src.utils.logger_config - INFO - [logger_config.py:215] - 日志级别: INFO
2026-01-19 16:21:31 - src.utils.logger_config - INFO - [logger_config.py:216] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_demp_tool_server_dify_to_mcp_test\lzwcai_demp_tool_server_dify_to_mcp_test\logs\lzwcai_demp_tool_server_dify_to_mcp_test.log
2026-01-19 16:21:31 - src.utils.logger_config - INFO - [logger_config.py:217] - 控制台输出: False
2026-01-19 16:21:31 - src.utils.logger_config - INFO - [logger_config.py:218] - 文件输出: True
2026-01-19 16:21:31 - src.utils.logger_config - INFO - [logger_config.py:219] - 文件轮转: 最大10MB, 保留5个备份
2026-01-19 16:21:31 - src.utils.logger_config - INFO - [logger_config.py:220] - ================================================================================
2026-01-19 16:21:31 - __main__ - INFO - [main.py:50] - ================================================================================
2026-01-19 16:21:31 - __main__ - INFO - [main.py:51] - Dify MCP 服务器启动
2026-01-19 16:21:31 - __main__ - INFO - [main.py:52] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_demp_tool_server_dify_to_mcp_test\lzwcai_demp_tool_server_dify_to_mcp_test\logs\lzwcai_demp_tool_server_dify_to_mcp_test.log
2026-01-19 16:21:31 - __main__ - INFO - [main.py:53] - ================================================================================
2026-01-19 16:21:33 - src.workflow.workflow_server - INFO - [workflow_server.py:322] - 调用 /parameters API: http://192.168.11.24:3001/v1/parameters
2026-01-19 16:21:33 - src.workflow.workflow_server - INFO - [workflow_server.py:323] - 请求头: {'Authorization': 'Bearer app-RM7IGxvdq48ixu1JXFiUZh76'}
2026-01-19 16:21:33 - src.workflow.workflow_server - INFO - [workflow_server.py:324] - 请求参数: {'user': 'pp666'}
2026-01-19 16:21:33 - src.workflow.workflow_server - INFO - [workflow_server.py:328] - /parameters API 响应状态码: 200
2026-01-19 16:21:33 - src.workflow.workflow_server - INFO - [workflow_server.py:333] - /parameters API 响应数据: {'opening_statement': '', 'suggested_questions': [], 'suggested_questions_after_answer': {'enabled': False}, 'speech_to_text': {'enabled': False}, 'text_to_speech': {'enabled': False, 'voice': '', 'language': ''}, 'retriever_resource': {'enabled': True}, 'annotation_reply': {'enabled': False}, 'more_like_this': {'enabled': False}, 'user_input_form': [{'file': {'variable': 'contact_file', 'label': 'contact_file', 'type': 'file', 'max_length': 4096, 'required': True, 'options': [], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'allowed_file_types': ['document'], 'allowed_file_extensions': []}}], 'sensitive_word_avoidance': {'enabled': False}, 'file_upload': {'image': {'enabled': False, 'number_limits': 3, 'transfer_methods': ['local_file', 'remote_url']}, 'enabled': False, 'allowed_file_types': ['image'], 'allowed_file_extensions': ['.JPG', '.JPEG', '.PNG', '.GIF', '.WEBP', '.SVG'], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'number_limits': 3, 'fileUploadConfig': {'file_size_limit': 15, 'batch_count_limit': 5, 'image_file_size_limit': 10, 'video_file_size_limit': 100, 'audio_file_size_limit': 50, 'workflow_file_upload_limit': 10}}, 'system_parameters': {'image_file_size_limit': 10, 'video_file_size_limit': 100, 'audio_file_size_limit': 50, 'file_size_limit': 15, 'workflow_file_upload_limit': 10}}
2026-01-19 16:21:33 - __main__ - INFO - [main.py:68] - 传输模式: stdio
2026-01-19 16:21:33 - __main__ - INFO - [main.py:69] - 配置参数: {'base_url': 'http://192.168.11.24:3001/v1', 'app_sks': ['app-RM7IGxvdq48ixu1JXFiUZh76'], 'mode_type': 'workflow', 'transport': 'stdio'}
2026-01-19 16:21:37 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest
2026-01-19 16:21:37 - src.create_mcp - INFO - [create_mcp.py:104] - 工具 tool_HeTongShenChaGongZuoLiu 的 parameters 数据: {'opening_statement': '', 'suggested_questions': [], 'suggested_questions_after_answer': {'enabled': False}, 'speech_to_text': {'enabled': False}, 'text_to_speech': {'enabled': False, 'voice': '', 'language': ''}, 'retriever_resource': {'enabled': True}, 'annotation_reply': {'enabled': False}, 'more_like_this': {'enabled': False}, 'user_input_form': [{'file': {'variable': 'contact_file', 'label': 'contact_file', 'type': 'file', 'max_length': 4096, 'required': True, 'options': [], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'allowed_file_types': ['document'], 'allowed_file_extensions': []}}], 'sensitive_word_avoidance': {'enabled': False}, 'file_upload': {'image': {'enabled': False, 'number_limits': 3, 'transfer_methods': ['local_file', 'remote_url']}, 'enabled': False, 'allowed_file_types': ['image'], 'allowed_file_extensions': ['.JPG', '.JPEG', '.PNG', '.GIF', '.WEBP', '.SVG'], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'number_limits': 3, 'fileUploadConfig': {'file_size_limit': 15, 'batch_count_limit': 5, 'image_file_size_limit': 10, 'video_file_size_limit': 100, 'audio_file_size_limit': 50, 'workflow_file_upload_limit': 10}}, 'system_parameters': {'image_file_size_limit': 10, 'video_file_size_limit': 100, 'audio_file_size_limit': 50, 'file_size_limit': 15, 'workflow_file_upload_limit': 10}}
2026-01-19 16:21:37 - src.create_mcp - INFO - [create_mcp.py:110] - 工具 tool_HeTongShenChaGongZuoLiu 提取的文件字段: [{'variable': 'contact_file', 'label': 'contact_file', 'required': True, 'max_length': 4096, 'allowed_file_types': ['document'], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'allowed_file_extensions': [], 'is_list': False}]
2026-01-19 16:21:46 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type CallToolRequest
2026-01-19 16:21:46 - src.create_mcp - INFO - [create_mcp.py:144] - 工具 tool_HeTongShenChaGongZuoLiu 的文件字段信息: [{'variable': 'contact_file', 'label': 'contact_file', 'required': True, 'max_length': 4096, 'allowed_file_types': ['document'], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'allowed_file_extensions': [], 'is_list': False}]
2026-01-19 16:21:46 - src.create_mcp - INFO - [create_mcp.py:145] - 工具 tool_HeTongShenChaGongZuoLiu 调用前的 arguments: {'contact_file': [{'url': 'http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx'}]}
2026-01-19 16:21:46 - src.create_mcp_utils - INFO - [create_mcp_utils.py:64] - 工具 tool_HeTongShenChaGongZuoLiu 的文件字段变量名: {'contact_file'}
2026-01-19 16:21:46 - src.create_mcp_utils - INFO - [create_mcp_utils.py:73] - 工具 tool_HeTongShenChaGongZuoLiu: 发现文件字段 contact_file值: [{'url': 'http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx'}]
2026-01-19 16:21:46 - src.create_mcp_utils - INFO - [create_mcp_utils.py:168] - 工具 tool_HeTongShenChaGongZuoLiu: 处理文件列表: [{'url': 'http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx'}]
2026-01-19 16:21:46 - src.create_mcp_utils - INFO - [create_mcp_utils.py:85] - 工具 tool_HeTongShenChaGongZuoLiu: 准备处理文件列表: [{'url': 'http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx'}]
2026-01-19 16:21:46 - src.create_mcp_utils - INFO - [create_mcp_utils.py:322] - 从URL http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx 提取的文件扩展名: docx
2026-01-19 16:21:46 - src.create_mcp_utils - INFO - [create_mcp_utils.py:333] - URL http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx 匹配文件类型: document
2026-01-19 16:21:46 - src.create_mcp_utils - INFO - [create_mcp_utils.py:96] - 工具 tool_HeTongShenChaGongZuoLiu: 自动识别文件类型为 document
2026-01-19 16:21:46 - src.workflow.workflow_server - INFO - [workflow_server.py:242] - 开始上传远程文件: http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx
2026-01-19 16:21:46 - src.utils.upload_file - INFO - [upload_file.py:400] - 开始处理文件: http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx
2026-01-19 16:21:46 - src.utils.upload_file - INFO - [upload_file.py:86] - 正在下载文件: http://192.168.11.24:9000/lzwcai/upload/2026-01-19/eb25b2b233a8719f680d4b0b9c5a80e7/eb25b2b233a8719f680d4b0b9c5a80e7.docx (尝试 1/3)
2026-01-19 16:21:46 - src.utils.upload_file - INFO - [upload_file.py:100] - 文件下载成功: C:\Users\HiWin10\AppData\Local\Temp\eb25b2b233a8719f680d4b0b9c5a80e7.docx (大小: 20445 字节)
2026-01-19 16:21:46 - src.utils.upload_file - INFO - [upload_file.py:189] - 准备上传文件: C:\Users\HiWin10\AppData\Local\Temp\eb25b2b233a8719f680d4b0b9c5a80e7.docx (大小: 0.02 MB)
2026-01-19 16:21:46 - src.utils.upload_file - INFO - [upload_file.py:220] - 文件上传成功: eb25b2b233a8719f680d4b0b9c5a80e7.docx (ID: c09a25e0-9d92-4b8d-b098-a59d6be10921)
2026-01-19 16:21:46 - src.utils.upload_file - INFO - [upload_file.py:417] - 已清理临时文件: C:\Users\HiWin10\AppData\Local\Temp\eb25b2b233a8719f680d4b0b9c5a80e7.docx
2026-01-19 16:21:46 - src.workflow.workflow_server - INFO - [workflow_server.py:255] - 文件上传成功 - ID: c09a25e0-9d92-4b8d-b098-a59d6be10921, 名称: eb25b2b233a8719f680d4b0b9c5a80e7.docx, 大小: 20445 bytes
2026-01-19 16:21:46 - src.workflow.workflow_server - INFO - [workflow_server.py:274] - 文件预处理完成,共处理 1 个文件
2026-01-19 16:21:46 - src.create_mcp_utils - INFO - [create_mcp_utils.py:111] - 工具 tool_HeTongShenChaGongZuoLiu: 文件预处理完成,成功上传 1 个文件
2026-01-19 16:21:46 - src.create_mcp_utils - INFO - [create_mcp_utils.py:219] - 工具 tool_HeTongShenChaGongZuoLiu: 字段 contact_file 是 file 类型,输出单个对象
2026-01-19 16:21:46 - src.create_mcp_utils - INFO - [create_mcp_utils.py:221] - 工具 tool_HeTongShenChaGongZuoLiu: 字段 contact_file 最终值: {'transfer_method': 'local_file', 'type': 'document', 'upload_file_id': 'c09a25e0-9d92-4b8d-b098-a59d6be10921'}
2026-01-19 16:21:46 - src.create_mcp - INFO - [create_mcp.py:149] - 工具 tool_HeTongShenChaGongZuoLiu 处理后的 arguments: {'contact_file': {'transfer_method': 'local_file', 'type': 'document', 'upload_file_id': 'c09a25e0-9d92-4b8d-b098-a59d6be10921'}}
2026-01-19 16:21:46 - src.workflow.workflow_server - INFO - [workflow_server.py:131] - Sending data to Dify API: {'inputs': {'contact_file': {'transfer_method': 'local_file', 'type': 'document', 'upload_file_id': 'c09a25e0-9d92-4b8d-b098-a59d6be10921'}}, 'response_mode': 'streaming', 'user': 'pp666'}
2026-01-19 16:21:46 - src.workflow.workflow_server - INFO - [workflow_server.py:132] - Sending headers to Dify API: {'Authorization': 'Bearer app-RM7IGxvdq48ixu1JXFiUZh76', 'Content-Type': 'application/json'}
2026-01-19 16:21:46 - src.workflow.workflow_server - INFO - [workflow_server.py:133] - Sending url to Dify API: http://192.168.11.24:3001/v1/workflows/run
2026-01-19 16:21:46 - src.workflow.workflow_server - INFO - [workflow_server.py:148] - Response1:{'inputs': {'contact_file': {'transfer_method': 'local_file', 'type': 'document', 'upload_file_id': 'c09a25e0-9d92-4b8d-b098-a59d6be10921'}}, 'response_mode': 'streaming', 'user': 'pp666'} 200 OK
2026-01-19 22:09:39 - src.utils.logger_config - INFO - [logger_config.py:213] - ================================================================================
2026-01-19 22:09:39 - src.utils.logger_config - INFO - [logger_config.py:214] - 日志系统初始化完成 - 2026-01-19 22:09:39
2026-01-19 22:09:39 - src.utils.logger_config - INFO - [logger_config.py:215] - 日志级别: INFO
2026-01-19 22:09:39 - src.utils.logger_config - INFO - [logger_config.py:216] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_demp_tool_server_dify_to_mcp_test\lzwcai_demp_tool_server_dify_to_mcp_test\logs\lzwcai_demp_tool_server_dify_to_mcp_test.log
2026-01-19 22:09:39 - src.utils.logger_config - INFO - [logger_config.py:217] - 控制台输出: False
2026-01-19 22:09:39 - src.utils.logger_config - INFO - [logger_config.py:218] - 文件输出: True
2026-01-19 22:09:39 - src.utils.logger_config - INFO - [logger_config.py:219] - 文件轮转: 最大10MB, 保留5个备份
2026-01-19 22:09:39 - src.utils.logger_config - INFO - [logger_config.py:220] - ================================================================================
2026-01-19 22:09:39 - __main__ - INFO - [main.py:50] - ================================================================================
2026-01-19 22:09:39 - __main__ - INFO - [main.py:51] - Dify MCP 服务器启动
2026-01-19 22:09:39 - __main__ - INFO - [main.py:52] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_demp_tool_server_dify_to_mcp_test\lzwcai_demp_tool_server_dify_to_mcp_test\logs\lzwcai_demp_tool_server_dify_to_mcp_test.log
2026-01-19 22:09:39 - __main__ - INFO - [main.py:53] - ================================================================================
2026-01-19 22:09:54 - src.workflow.workflow_server - INFO - [workflow_server.py:322] - 调用 /parameters API: http://192.167.30.8:3001/v1/parameters
2026-01-19 22:09:54 - src.workflow.workflow_server - INFO - [workflow_server.py:323] - 请求头: {'Authorization': 'Bearer app-0dlxPuFY8HBXo6PpoeStQCjA'}
2026-01-19 22:09:54 - src.workflow.workflow_server - INFO - [workflow_server.py:324] - 请求参数: {'user': 'pp666'}
2026-01-19 22:09:55 - src.utils.logger_config - INFO - [logger_config.py:213] - ================================================================================
2026-01-19 22:09:55 - src.utils.logger_config - INFO - [logger_config.py:214] - 日志系统初始化完成 - 2026-01-19 22:09:55
2026-01-19 22:09:55 - src.utils.logger_config - INFO - [logger_config.py:215] - 日志级别: INFO
2026-01-19 22:09:55 - src.utils.logger_config - INFO - [logger_config.py:216] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_demp_tool_server_dify_to_mcp_test\lzwcai_demp_tool_server_dify_to_mcp_test\logs\lzwcai_demp_tool_server_dify_to_mcp_test.log
2026-01-19 22:09:55 - src.utils.logger_config - INFO - [logger_config.py:217] - 控制台输出: False
2026-01-19 22:09:55 - src.utils.logger_config - INFO - [logger_config.py:218] - 文件输出: True
2026-01-19 22:09:55 - src.utils.logger_config - INFO - [logger_config.py:219] - 文件轮转: 最大10MB, 保留5个备份
2026-01-19 22:09:55 - src.utils.logger_config - INFO - [logger_config.py:220] - ================================================================================
2026-01-19 22:09:55 - __main__ - INFO - [main.py:50] - ================================================================================
2026-01-19 22:09:55 - __main__ - INFO - [main.py:51] - Dify MCP 服务器启动
2026-01-19 22:09:55 - __main__ - INFO - [main.py:52] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_demp_tool_server_dify_to_mcp_test\lzwcai_demp_tool_server_dify_to_mcp_test\logs\lzwcai_demp_tool_server_dify_to_mcp_test.log
2026-01-19 22:09:55 - __main__ - INFO - [main.py:53] - ================================================================================
2026-01-19 22:10:01 - src.utils.logger_config - INFO - [logger_config.py:213] - ================================================================================
2026-01-19 22:10:01 - src.utils.logger_config - INFO - [logger_config.py:214] - 日志系统初始化完成 - 2026-01-19 22:10:01
2026-01-19 22:10:01 - src.utils.logger_config - INFO - [logger_config.py:215] - 日志级别: INFO
2026-01-19 22:10:01 - src.utils.logger_config - INFO - [logger_config.py:216] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_demp_tool_server_dify_to_mcp_test\lzwcai_demp_tool_server_dify_to_mcp_test\logs\lzwcai_demp_tool_server_dify_to_mcp_test.log
2026-01-19 22:10:01 - src.utils.logger_config - INFO - [logger_config.py:217] - 控制台输出: False
2026-01-19 22:10:01 - src.utils.logger_config - INFO - [logger_config.py:218] - 文件输出: True
2026-01-19 22:10:01 - src.utils.logger_config - INFO - [logger_config.py:219] - 文件轮转: 最大10MB, 保留5个备份
2026-01-19 22:10:01 - src.utils.logger_config - INFO - [logger_config.py:220] - ================================================================================
2026-01-19 22:10:01 - __main__ - INFO - [main.py:50] - ================================================================================
2026-01-19 22:10:01 - __main__ - INFO - [main.py:51] - Dify MCP 服务器启动
2026-01-19 22:10:01 - __main__ - INFO - [main.py:52] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_demp_tool_server_dify_to_mcp_test\lzwcai_demp_tool_server_dify_to_mcp_test\logs\lzwcai_demp_tool_server_dify_to_mcp_test.log
2026-01-19 22:10:01 - __main__ - INFO - [main.py:53] - ================================================================================
2026-01-19 22:10:06 - src.workflow.workflow_server - INFO - [workflow_server.py:322] - 调用 /parameters API: http://192.167.30.8:3001/v1/parameters
2026-01-19 22:10:06 - src.workflow.workflow_server - INFO - [workflow_server.py:323] - 请求头: {'Authorization': 'Bearer app-0dlxPuFY8HBXo6PpoeStQCjA'}
2026-01-19 22:10:06 - src.workflow.workflow_server - INFO - [workflow_server.py:324] - 请求参数: {'user': 'pp666'}
2026-01-19 22:10:10 - src.workflow.workflow_server - INFO - [workflow_server.py:328] - /parameters API 响应状态码: 200
2026-01-19 22:10:10 - src.workflow.workflow_server - INFO - [workflow_server.py:333] - /parameters API 响应数据: {'opening_statement': '', 'suggested_questions': [], 'suggested_questions_after_answer': {'enabled': False}, 'speech_to_text': {'enabled': False}, 'text_to_speech': {'enabled': False, 'voice': '', 'language': ''}, 'retriever_resource': {'enabled': True}, 'annotation_reply': {'enabled': False}, 'more_like_this': {'enabled': False}, 'user_input_form': [{'paragraph': {'variable': 'contentText', 'label': '订单内容', 'type': 'paragraph', 'max_length': 48000, 'required': False, 'options': [], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'allowed_file_types': ['image', 'document'], 'allowed_file_extensions': []}}, {'file': {'variable': 'contentFile', 'label': '文件内容', 'type': 'file', 'max_length': 5, 'required': False, 'options': [], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'allowed_file_types': ['document', 'image'], 'allowed_file_extensions': []}}], 'sensitive_word_avoidance': {'enabled': False}, 'file_upload': {'image': {'enabled': False, 'number_limits': 3, 'transfer_methods': ['local_file', 'remote_url']}, 'enabled': False, 'allowed_file_types': ['image'], 'allowed_file_extensions': ['.JPG', '.JPEG', '.PNG', '.GIF', '.WEBP', '.SVG'], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'number_limits': 3, 'fileUploadConfig': {'file_size_limit': 15, 'batch_count_limit': 5, 'image_file_size_limit': 10, 'video_file_size_limit': 100, 'audio_file_size_limit': 50, 'workflow_file_upload_limit': 10}}, 'system_parameters': {'image_file_size_limit': 10, 'video_file_size_limit': 100, 'audio_file_size_limit': 50, 'file_size_limit': 15, 'workflow_file_upload_limit': 10}}
2026-01-19 22:10:10 - src.workflow.workflow_server - INFO - [workflow_server.py:322] - 调用 /parameters API: http://192.167.30.8:3001/v1/parameters
2026-01-19 22:10:10 - src.workflow.workflow_server - INFO - [workflow_server.py:328] - /parameters API 响应状态码: 200
2026-01-19 22:10:10 - src.workflow.workflow_server - INFO - [workflow_server.py:323] - 请求头: {'Authorization': 'Bearer app-0dlxPuFY8HBXo6PpoeStQCjA'}
2026-01-19 22:10:10 - src.workflow.workflow_server - INFO - [workflow_server.py:333] - /parameters API 响应数据: {'opening_statement': '', 'suggested_questions': [], 'suggested_questions_after_answer': {'enabled': False}, 'speech_to_text': {'enabled': False}, 'text_to_speech': {'enabled': False, 'voice': '', 'language': ''}, 'retriever_resource': {'enabled': True}, 'annotation_reply': {'enabled': False}, 'more_like_this': {'enabled': False}, 'user_input_form': [{'paragraph': {'variable': 'contentText', 'label': '订单内容', 'type': 'paragraph', 'max_length': 48000, 'required': False, 'options': [], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'allowed_file_types': ['image', 'document'], 'allowed_file_extensions': []}}, {'file': {'variable': 'contentFile', 'label': '文件内容', 'type': 'file', 'max_length': 5, 'required': False, 'options': [], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'allowed_file_types': ['document', 'image'], 'allowed_file_extensions': []}}], 'sensitive_word_avoidance': {'enabled': False}, 'file_upload': {'image': {'enabled': False, 'number_limits': 3, 'transfer_methods': ['local_file', 'remote_url']}, 'enabled': False, 'allowed_file_types': ['image'], 'allowed_file_extensions': ['.JPG', '.JPEG', '.PNG', '.GIF', '.WEBP', '.SVG'], 'allowed_file_upload_methods': ['local_file', 'remote_url'], 'number_limits': 3, 'fileUploadConfig': {'file_size_limit': 15, 'batch_count_limit': 5, 'image_file_size_limit': 10, 'video_file_size_limit': 100, 'audio_file_size_limit': 50, 'workflow_file_upload_limit': 10}}, 'system_parameters': {'image_file_size_limit': 10, 'video_file_size_limit': 100, 'audio_file_size_limit': 50, 'file_size_limit': 15, 'workflow_file_upload_limit': 10}}
2026-01-19 22:10:10 - src.workflow.workflow_server - INFO - [workflow_server.py:324] - 请求参数: {'user': 'pp666'}
2026-01-19 22:10:13 - __main__ - INFO - [main.py:68] - 传输模式: stdio
2026-01-19 22:10:13 - __main__ - INFO - [main.py:69] - 配置参数: {'base_url': 'http://192.167.30.8:3001/v1', 'app_sks': ['app-0dlxPuFY8HBXo6PpoeStQCjA'], 'mode_type': 'workflow', 'transport': 'stdio'}
2026-01-19 22:10:17 - __main__ - INFO - [main.py:68] - 传输模式: stdio
2026-01-19 22:10:17 - __main__ - INFO - [main.py:69] - 配置参数: {'base_url': 'http://192.167.30.8:3001/v1', 'app_sks': ['app-0dlxPuFY8HBXo6PpoeStQCjA'], 'mode_type': 'workflow', 'transport': 'stdio'}

View File

@@ -1,12 +0,0 @@
Metadata-Version: 2.4
Name: lzwcai-demp-tool-server-dify-to-mcp-test
Version: 0.1.2
Summary: 这是一个Dify to MCP的集成工具通过Dify的API接口将Dify的模型部署到MCP平台并进行推理。
Requires-Python: >=3.10
Requires-Dist: httpx>=0.28.1
Requires-Dist: mcp>=1.1.2
Requires-Dist: omegaconf>=2.3.0
Requires-Dist: pip>=24.3.1
Requires-Dist: python-dotenv>=1.0.1
Requires-Dist: requests
Requires-Dist: pypinyin>=0.54.0

View File

@@ -1,27 +0,0 @@
README.md
pyproject.toml
setup.cfg
lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/PKG-INFO
lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/SOURCES.txt
lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/dependency_links.txt
lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/entry_points.txt
lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/requires.txt
lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/top_level.txt
src/__init__.py
src/create_mcp.py
src/create_mcp_update.py
src/create_mcp_utils.py
src/chat/__init__.py
src/chat/chat_server.py
src/completion/completion_server.py
src/completion/test.py
src/core/__init__.py
src/core/core_server.py
src/difyTaskCall/task_instance.py
src/utils/dify_workflow_schema.py
src/utils/logger_config.py
src/utils/tool_translation.py
src/utils/translator.py
src/utils/upload_file.py
src/workflow/__init__.py
src/workflow/workflow_server.py

View File

@@ -1,2 +0,0 @@
[console_scripts]
lzwcai-demp-tool-server-dify-to-mcp-test = src.create_mcp:run_main

View File

@@ -1,7 +0,0 @@
httpx>=0.28.1
mcp>=1.1.2
omegaconf>=2.3.0
pip>=24.3.1
python-dotenv>=1.0.1
requests
pypinyin>=0.54.0

View File

@@ -1,86 +0,0 @@
#!/usr/bin/env python3
"""
主入口文件
用于启动 Dify MCP 服务器,并配置命令行参数
"""
import os
import sys
import logging
# 导入日志配置
from src.utils.logger_config import setup_logging, get_logger
# Mock 配置参数
def setup_mock_arguments():
"""
设置模拟命令行参数
这些参数可以根据实际需求进行修改
"""
# 默认配置
default_config = {
"base_url": "http://192.168.2.236:3001/v1",
"app_sks": ["app-IfJayK9uu5oTo54Rpr2AS7wl"],
"mode_type": "workflow",
"transport": "stdio"
}
# 如果没有提供命令行参数,则添加默认参数
if len(sys.argv) == 1:
sys.argv.extend([
"--base-url", default_config["base_url"],
"--app-sks", *default_config["app_sks"],
"--mode-type", default_config["mode_type"]
])
return default_config
def main():
"""
主函数:设置命令行参数并启动服务器
"""
# 初始化日志系统MCP模式下禁用控制台输出避免干扰stdio通信
try:
log_file_path = setup_logging(
log_level=logging.INFO,
console_output=False, # MCP模式下禁用控制台输出
file_output=True
)
logger = get_logger(__name__)
logger.info("=" * 80)
logger.info("Dify MCP 服务器启动")
logger.info(f"日志文件: {log_file_path}")
logger.info("=" * 80)
except Exception as e:
# 如果日志初始化失败使用stderr输出错误
print(f"[ERROR] 日志系统初始化失败: {e}", file=sys.stderr)
# 设置模拟命令行参数
config = setup_mock_arguments()
# 导入并运行 MCP 服务器
try:
from src.create_mcp import run_main
# 获取传输模式
transport_mode = config.get("transport", "stdio")
logger.info(f"传输模式: {transport_mode}")
logger.info(f"配置参数: {config}")
# 运行服务器(不输出额外信息,避免干扰 STDIO 通信)
run_main(transport=transport_mode)
except ImportError as e:
logger.error(f"导入错误: {e}", exc_info=True)
print(f"[ERROR] 导入错误: {e}", file=sys.stderr)
print("请确保已正确安装所有依赖包", file=sys.stderr)
sys.exit(1)
except Exception as e:
logger.error(f"运行错误: {e}", exc_info=True)
print(f"[ERROR] 运行错误: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -1,32 +0,0 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "lzwcai-demp-tool-server-dify-to-mcp-test"
version = "0.1.2"
description = "这是一个Dify to MCP的集成工具通过Dify的API接口将Dify的模型部署到MCP平台并进行推理。"
requires-python = ">=3.10"
dependencies = [
"httpx>=0.28.1",
"mcp>=1.1.2",
"omegaconf>=2.3.0",
"pip>=24.3.1",
"python-dotenv>=1.0.1",
"requests",
"pypinyin>=0.54.0",
]
[tool.setuptools]
packages = {find = {where = ["."], include = ["src*"]}}
include-package-data = true
[project.scripts]
lzwcai-demp-tool-server-dify-to-mcp-test = "src.create_mcp:run_main"
[tool.hatch.build.targets.wheel]
packages = ["src"]
[tool.setuptools.package-data]
"*" = ["*.env"]
"src" = ["**/*.env"]

View File

@@ -1,4 +0,0 @@
[egg_info]
tag_build =
tag_date = 0

View File

@@ -1,7 +0,0 @@
class ChatDifyAPI:
def __init__(self, base_url: str, app_sks: str):
self.base_url = base_url
self.app_sks = app_sks
def process_task(self, task_id: str, **kwargs):
pass

View File

@@ -1,203 +0,0 @@
import requests
from abc import ABC
import logging
import json
import pypinyin
from src.utils.logger_config import get_logger
logger = get_logger(__name__)
def pinyin_to_camel(pinyin):
"""
将拼音列表转换为驼峰命名
例如: ['ni', 'hao', 'a'] -> 'NiHaoA'
"""
pinyin_list = pypinyin.lazy_pinyin(pinyin)
return "tool_" + "".join(word.capitalize() for word in pinyin_list)
class CompletionDifyAPI(ABC):
def __init__(self, base_url: str, dify_app_sks: list, user="pp666"):
# dify configs
self.dify_base_url = base_url
self.dify_app_sks = dify_app_sks
self.user = user
# dify app infos
dify_app_infos = []
dify_app_params = []
dify_app_metas = []
for key in self.dify_app_sks:
dify_app_infos.append(self.get_app_info(key))
dify_app_params.append(self.get_app_parameters(key))
dify_app_metas.append(self.get_app_meta(key))
self.dify_app_infos = dify_app_infos
self.dify_app_params = dify_app_params
self.dify_app_metas = dify_app_metas
self.dify_app_names = [x["name"] for x in dify_app_infos]
def chat_message(
self,
api_key,
inputs={},
response_mode="streaming",
conversation_id=None,
userId="pp666",
files=None,
):
url = f"{self.dify_base_url}/completion-messages"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": userId,
}
if conversation_id:
data["conversation_id"] = conversation_id
if response_mode == "streaming":
response = requests.post(url, headers=headers, json=data, stream=True)
# 处理流式响应
full_answer = ""
for line in response.iter_lines():
if line:
# 跳过 "data:" 前缀
decoded_line = line.decode("utf-8")
if decoded_line.startswith("data:"):
try:
json_str = decoded_line[5:].strip()
data = json.loads(json_str)
if data.get("event") == "message" and "answer" in data:
# 累积完整答案
full_answer += data["answer"]
# 这里也可以选择处理每个部分响应,例如返回生成器
# yield data
except json.JSONDecodeError:
logger.warning(f"无法解析JSON数据: {decoded_line}")
# 创建一个符合非流式响应格式的结果
response_data = {"answer": full_answer}
# 处理可能包含代码块的数据
processed_data = self.process_answer_code_block(response_data)
return processed_data
else:
response = requests.post(url, headers=headers, json=data)
response_data = response.json()
# 处理可能包含代码块的数据
processed_data = self.process_answer_code_block(response_data)
return processed_data
def upload_file(self, api_key, file_path, user="pp666"):
url = f"{self.dify_base_url}/files/upload"
headers = {"Authorization": f"Bearer {api_key}"}
files = {"file": open(file_path, "rb")}
data = {"user": user}
response = requests.post(url, headers=headers, files=files, data=data)
response.raise_for_status()
return response.json()
def stop_response(self, api_key, task_id, user="pp666"):
url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {"user": user}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
def get_app_info(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/info"
headers = {"Authorization": f"Bearer {api_key}"}
# params = {"user": user}
response = requests.get(url, headers=headers)
response.raise_for_status()
response_map = response.json()
# 翻译工具名称
from src.utils.tool_translation import TranslationService
tool_name = response_map.get("name")
if tool_name:
# translated_name = TranslationService.translate_tool_name(tool_name)
translated_name = pinyin_to_camel(tool_name)
response_map["name"] = translated_name
# 翻译工具描述
# tool_description = response_map.get("description")
# if tool_description:
# translated_description = TranslationService.translate_tool_description(
# tool_description
# )
# response_map["description"] = (
# f"{tool_description} ({translated_description})"
# )
return response_map
def get_app_parameters(self, api_key, user="pp666"):
return {
"user_input_form": [
{"string": {"variable": "query", "label": "查询内容", "required": True}}
]
}
def get_app_meta(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/meta"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
@staticmethod
def process_answer_code_block(data):
try:
# 获取answer字段
answer = data.get("answer", "")
# 构造符合workflow_finished格式的输出
formatted_response = [
{"event": "workflow_finished", "data": {"outputs": {"result": answer}}}
]
# 尝试处理可能的代码块
if answer.startswith("```") and answer.endswith("```"):
try:
# 移除代码块标记并解析JSON
code_content = answer.strip("```").strip()
json_data = json.loads(code_content)
# 如果包含description字段用它替换answer
if "description" in json_data:
formatted_response[0]["data"]["outputs"]["result"] = json_data[
"description"
]
except json.JSONDecodeError:
# 如果不是有效的JSON保留原始代码块内容
pass
return formatted_response
except Exception as e:
logger.warning(f"处理答案代码块时出错: {str(e)}")
# 发生错误时返回符合格式的基础响应
return [
{
"event": "workflow_finished",
"data": {
"outputs": {
"error": str(e),
"fallback": data.get("answer", str(data)),
}
},
}
]

View File

@@ -1,104 +0,0 @@
import requests
from abc import ABC
import logging
import json
logger = logging.getLogger(__name__)
res = {
"event": "message",
"task_id": "49c9ea1b-7b43-475b-a680-d769fb238a45",
"id": "432ab98e-5e36-4a29-abe5-e01281c3678c",
"message_id": "432ab98e-5e36-4a29-abe5-e01281c3678c",
"mode": "completion",
"answer": '```\n{\n "description": "该API的具体功能描述暂时不明确因为提供的API信息 \'今天打老虎啊按时啊啊\' 并不是有效的API名称或描述。请提供正确的API名称和相关输入输出信息以便我能为其补充完善的API描述。"\n}\n```',
"metadata": {
"usage": {
"prompt_tokens": 73,
"prompt_unit_price": "0.0",
"prompt_price_unit": "0.0",
"prompt_price": "0.0",
"completion_tokens": 61,
"completion_unit_price": "0.0",
"completion_price_unit": "0.0",
"completion_price": "0.0",
"total_tokens": 134,
"total_price": "0.0",
"currency": "USD",
"latency": 1.896302318200469,
}
},
"created_at": 1747233054,
}
def process_answer_code_block(data):
try:
# 获取answer字段
answer = data.get("answer", "")
# 检查answer是否是代码块格式
if answer.startswith("```") and answer.endswith("```"):
# 移除代码块标记并解析JSON
code_content = answer.strip("```").strip()
json_data = json.loads(code_content)
# 获取description字段
if "description" in json_data:
return json_data["description"]
# 如果不是预期格式则返回原始answer
return data.get("answer", data)
except Exception as e:
logger.warning(f"处理答案代码块时出错: {str(e)}")
# 发生错误时返回原始数据
return data.get("answer", data)
def chat_message_test(
api_key,
inputs={},
response_mode="blocking",
conversation_id=None,
userId="pp666",
files=None,
):
url = "https://ops.lzwcai.com/v1/completion-messages"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": userId,
}
if conversation_id:
data["conversation_id"] = conversation_id
if response_mode == "streaming":
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
return response
else:
response = requests.post(url, headers=headers, json=data)
return response.json()
if __name__ == "__main__":
print("开始执行主程序")
try:
print("准备调用chat_message方法")
res = chat_message_test(
api_key="app-Ppemii3c0ROPoLvRwskgZ7Il",
inputs={"query": "今天打老虎啊按时啊啊"},
response_mode="streaming",
userId="abc-123",
)
print("chat_message方法调用完成")
# 打印响应内容
print("响应内容:", res)
# print(process_answer_code_block(res))
except Exception as e:
print(f"执行过程中出现错误: {e}")

View File

@@ -1,213 +0,0 @@
import requests
from abc import ABC
import logging
import json
# 导入 pypinyin 用于中文转拼音
try:
import pypinyin
except ImportError:
pypinyin = None
logging.warning("pypinyin 模块未安装,将使用简化的命名方式")
logger = logging.getLogger(__name__)
def pinyin_to_camel(pinyin):
"""
将中文名称转换为工具名称
处理逻辑:
1. 如果安装了 pypinyin将中文转换为拼音然后转为驼峰命名
2. 如果未安装 pypinyin将所有非字母数字字符替换为下划线
3. 所有符号都会被替换成下划线
示例:
"你好啊" -> "tool_NiHaoA" (有pypinyin)
"测试-工具" -> "tool_测试_工具" (无pypinyin)
"Hello World!" -> "tool_Hello_World_" (无pypinyin)
Args:
pinyin: 输入的字符串(可能包含中文、英文、符号等)
Returns:
str: 格式化后的工具名称,以 "tool_" 开头
"""
import re
if pypinyin is None:
# 如果 pypinyin 未安装,使用简化的命名方式
# 将所有非字母数字字符(包括空格、符号等)替换为下划线
cleaned = re.sub(r'[^\w]', '_', str(pinyin))
# 移除连续的下划线
cleaned = re.sub(r'_+', '_', cleaned)
# 移除首尾的下划线
cleaned = cleaned.strip('_')
return "tool_" + cleaned if cleaned else "tool_unnamed"
# 使用 pypinyin 转换中文为拼音
pinyin_list = pypinyin.lazy_pinyin(pinyin)
# 处理每个拼音单词
processed_words = []
for word in pinyin_list:
# 将所有非字母数字字符替换为下划线
cleaned_word = re.sub(r'[^\w]', '_', word)
# 移除连续的下划线
cleaned_word = re.sub(r'_+', '_', cleaned_word)
# 移除首尾的下划线
cleaned_word = cleaned_word.strip('_')
if cleaned_word:
# 首字母大写(驼峰命名)
processed_words.append(cleaned_word.capitalize())
# 拼接所有单词
result = "".join(processed_words) if processed_words else "Unnamed"
return "tool_" + result
class DifyAPI(ABC):
def __init__(self, base_url: str, dify_app_sks: list, user="pp666"):
# dify configs
self.dify_base_url = base_url
self.dify_app_sks = dify_app_sks
self.user = user
# dify app infos
dify_app_infos = []
dify_app_params = []
dify_app_metas = []
for key in self.dify_app_sks:
dify_app_infos.append(self.get_app_info(key))
dify_app_params.append(self.get_app_parameters(key))
dify_app_metas.append(self.get_app_meta(key))
logger.info(f"Dify应用参数: {dify_app_params}")
self.dify_app_infos = dify_app_infos
self.dify_app_params = dify_app_params
self.dify_app_metas = dify_app_metas
self.dify_app_names = [x["name"] for x in dify_app_infos]
def chat_message(
self,
api_key,
inputs={},
response_mode="streaming",
conversation_id=None,
user="pp666",
files=None,
):
url = f"{self.dify_base_url}/workflows/run"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": user,
}
logger.info("Sending data to Dify API: %s", data)
logger.info("Sending headers to Dify API: %s", headers)
logger.info("Sending url to Dify API: %s", url)
if conversation_id:
data["conversation_id"] = conversation_id
if files:
files_data = []
for file_info in files:
file_path = file_info.get("path")
transfer_method = file_info.get("transfer_method")
if transfer_method == "local_file":
files_data.append(("file", open(file_path, "rb")))
elif transfer_method == "remote_url":
pass
response = requests.post(
url,
headers=headers,
data=data,
files=files_data,
stream=response_mode == "streaming",
)
else:
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
response.raise_for_status()
if response_mode == "streaming":
for line in response.iter_lines():
if line:
if line.startswith(b"data:"):
try:
json_data = json.loads(line[5:].decode("utf-8"))
yield json_data
except json.JSONDecodeError:
logger.error(f"JSON解码错误: {line}")
else:
return response.json()
def upload_file(self, api_key, file_path, user="pp666"):
url = f"{self.dify_base_url}/files/upload"
headers = {"Authorization": f"Bearer {api_key}"}
files = {"file": open(file_path, "rb")}
data = {"user": user}
response = requests.post(url, headers=headers, files=files, data=data)
response.raise_for_status()
return response.json()
def stop_response(self, api_key, task_id, user="pp666"):
url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {"user": user}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
def get_app_info(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/info"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
from src.utils.tool_translation import TranslationService
response_map = response.json()
# 翻译工具名称
# tool_name = response_map.get("name")
# translated_name = TranslationService.translate_tool_name(tool_name)
# response_map["name"] = translated_name
# # 翻译工具描述
# tool_description = response_map.get("description")
# translated_description = TranslationService.translate_tool_description(
# tool_description
# )
# response_map["description"] = translated_description
tool_name = response_map.get("name")
if tool_name:
# translated_name = TranslationService.translate_tool_name(tool_name)
translated_name = pinyin_to_camel(tool_name)
response_map["name"] = translated_name
return response_map
def get_app_parameters(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/parameters"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
def get_app_meta(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/meta"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()

View File

@@ -1,253 +0,0 @@
import asyncio
import json
import os
import logging
import argparse
from abc import ABC
import mcp.server.stdio
import mcp.types as types
import requests
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
from omegaconf import OmegaConf
# from src.workflow.workflow_server import WorkflowDifyAPI
from src.difyTaskCall.task_instance import TaskInstance
from src.utils.dify_workflow_schema import process_user_input_form, extract_file_fields
from src.create_mcp_utils import process_file_arguments
from src.utils.logger_config import get_logger
from src.workflow.workflow_server import DifyAPIError
# 使用统一的日志配置
logger = get_logger(__name__)
def parse_arguments():
parser = argparse.ArgumentParser(description="Dify MCP服务器配置")
parser.add_argument(
"--base-url",
type=str,
help="API基础URL",
default="http://192.168.2.236:3001/v1",
)
parser.add_argument(
"--app-sks",
nargs="+",
help="应用秘钥列表",
default=["app-RBS0TuYEnqm8Q1cRQingkuhf"],
)
parser.add_argument(
"--mode-type",
type=str,
help="Dify应用模式类型 (workflow, chat, completion)",
default="workflow",
choices=["workflow", "chat", "completion"],
)
return parser.parse_args()
def get_app_info(base_url=None, app_sks=None, mode_type=None):
# 获取命令行参数
args = parse_arguments()
# 命令行参数优先,其次是函数参数,最后是默认值
if args.base_url is not None:
base_url = args.base_url
if base_url is None:
base_url = "http://192.168.2.236:3001/v1"
if args.app_sks is not None:
app_sks = args.app_sks
if app_sks is None:
app_sks = ["app-RBS0TuYEnqm8Q1cRQingkuhf"]
if args.mode_type is not None:
mode_type = args.mode_type
if mode_type is None:
mode_type = "workflow"
return base_url, app_sks, mode_type
# 初始化服务器和Dify API
base_url, dify_app_sks, dify_app_mode_type = get_app_info()
server = Server("dify_mcp_server")
task_instance = TaskInstance(base_url, dify_app_sks, dify_app_mode_type)
dify_api = task_instance.get_task_instance(dify_app_mode_type)
# 创建工具
file_config = {
"file_fields": {}, # 字典key为工具名称value为该工具的文件字段列表
"file_type_dicts": {} # 字典key为工具名称value为该工具的文件类型字典
}
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
列出可用的工具
返回:
工具列表每个工具都使用JSON Schema验证其参数
"""
tools = []
tool_names = dify_api.dify_app_names
tool_infos = dify_api.dify_app_infos
tool_params = dify_api.dify_app_params
tool_num = len(tool_names)
for i in range(tool_num):
# 加载每个工具的应用信息
app_info = tool_infos[i]
# 加载每个工具的应用参数
app_param = tool_params[i]
# 记录 parameters API 返回的原始数据
logger.info(f"工具 {app_info['name']} 的 parameters 数据: {app_param}")
# 处理用户输入表单
inputSchema = process_user_input_form(app_param["user_input_form"])
# 提取所有文件字段并存储到全局字典中
tool_file_fields = extract_file_fields(app_param["user_input_form"])
logger.info(f"工具 {app_info['name']} 提取的文件字段: {tool_file_fields}")
file_config["file_fields"][app_info["name"]] = tool_file_fields
tools.append(
types.Tool(
name=app_info["name"],
description=app_info["description"],
inputSchema=inputSchema,
)
)
return tools
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
调用工具处理请求
参数:
name: 工具名称
arguments: 工具参数
返回:
处理结果列表
"""
tool_names = dify_api.dify_app_names
if name in tool_names:
tool_idx = tool_names.index(name)
tool_sk = dify_api.dify_app_sks[tool_idx]
# 获取当前工具的文件字段信息
current_tool_file_fields = file_config["file_fields"].get(name, [])
logger.info(f"工具 {name} 的文件字段信息: {current_tool_file_fields}")
logger.info(f"工具 {name} 调用前的 arguments: {arguments}")
# 使用工具函数处理文件字段
processed_arguments = process_file_arguments(arguments, current_tool_file_fields, dify_api, name)
logger.info(f"工具 {name} 处理后的 arguments: {processed_arguments}")
try:
responses = dify_api.chat_message(
tool_sk,
inputs=processed_arguments,
)
# 初始化 outputs 变量,避免未定义错误
outputs = {}
for res in responses:
if res["event"] == "workflow_finished":
outputs = res["data"]["outputs"]
break # 找到 workflow_finished 事件后退出循环
# 构建 MCP 输出
mcp_out = []
if outputs:
for _, v in outputs.items():
mcp_out.append(types.TextContent(type="text", text=str(v)))
else:
# 如果没有获取到 outputs返回错误信息
logger.warning(f"工具 {name} 未获取到 workflow_finished 事件或 outputs 为空")
mcp_out.append(types.TextContent(type="text", text="工具执行完成,但未返回输出结果"))
return mcp_out
except DifyAPIError as e:
# 捕获 Dify API 错误,直接返回给用户
logger.error(f"工具 {name} 调用 Dify API 失败: {e}")
error_message = f"API调用失败: {e.message}"
return [types.TextContent(type="text", text=error_message)]
except requests.exceptions.RequestException as e:
# 捕获网络请求错误
logger.error(f"工具 {name} 网络请求失败: {e}")
error_message = f"网络请求失败: {str(e)}"
return [types.TextContent(type="text", text=error_message)]
except Exception as e:
# 捕获其他未知错误
logger.error(f"工具 {name} 执行时发生未知错误: {e}", exc_info=True)
error_message = f"工具执行失败: {str(e)}"
return [types.TextContent(type="text", text=error_message)]
else:
raise ValueError(f"Unknown tool: {name}")
def run_main(transport="stdio"):
"""
主函数使用stdin/stdout流运行服务器
"""
if transport == "stdio":
import anyio
from mcp.server.stdio import stdio_server
async def arun():
async with stdio_server() as streams:
await server.run(
streams[0],
streams[1],
InitializationOptions(
server_name="dify_mcp_server",
server_version="0.0.6",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
anyio.run(arun)
else:
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Mount, Route
sse = SseServerTransport("/messages/")
async def handle_sse(request):
async with sse.connect_sse(
request.scope, request.receive, request._send
) as streams:
await server.run(
streams[0], streams[1], server.create_initialization_options()
)
return Response()
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse, methods=["GET"]),
Mount("/messages/", app=sse.handle_post_message),
],
)
import uvicorn
uvicorn.run(starlette_app, host="0.0.0.0", port=8080, log_level="info")
if __name__ == "__main__":
run_main()

View File

@@ -1,320 +0,0 @@
import asyncio
import json
import os
import logging
import argparse
from abc import ABC
import mcp.server.stdio
import mcp.types as types
import requests
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
from omegaconf import OmegaConf
# from src.workflow.workflow_server import WorkflowDifyAPI
from src.difyTaskCall.task_instance import TaskInstance
from src.utils.dify_workflow_schema import process_user_input_form
# 配置日志记录
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
def parse_arguments():
parser = argparse.ArgumentParser(description="Dify MCP服务器配置")
parser.add_argument(
"--base-url",
type=str,
help="API基础URL",
default="http://192.168.2.236:3001/v1",
)
parser.add_argument(
"--app-sks",
nargs="+",
help="应用秘钥列表",
default=["app-XaRWpeL2Yfdguc5ul3ScXvPE"],
)
parser.add_argument(
"--mode-type",
type=str,
help="Dify应用模式类型 (workflow, chat, completion)",
default="workflow",
choices=["workflow", "chat", "completion"],
)
return parser.parse_args()
def get_app_info(base_url=None, app_sks=None, mode_type=None):
# 获取命令行参数
args = parse_arguments()
# 命令行参数优先,其次是函数参数,最后是默认值
if args.base_url is not None:
base_url = args.base_url
if base_url is None:
base_url = "http://192.168.2.236:3001/v1"
if args.app_sks is not None:
app_sks = args.app_sks
if app_sks is None:
app_sks = ["app-XaRWpeL2Yfdguc5ul3ScXvPE"]
if args.mode_type is not None:
mode_type = args.mode_type
if mode_type is None:
mode_type = "workflow"
return base_url, app_sks, mode_type
# 初始化服务器和Dify API
base_url, dify_app_sks, dify_app_mode_type = get_app_info()
server = Server("dify_mcp_server")
task_instance = TaskInstance(base_url, dify_app_sks, dify_app_mode_type)
dify_api = task_instance.get_task_instance(dify_app_mode_type)
def process_user_input_form1(user_input_form):
"""
处理Dify应用的用户输入表单转换为JSON Schema格式
参数:
user_input_form: Dify应用的用户输入表单配置
返回:
处理后的inputSchema字典
"""
inputSchema = dict(
type="object",
properties={},
required=[],
)
property_num = len(user_input_form)
if property_num > 0:
for j in range(property_num):
param = user_input_form[j]
param_type = list(param.keys())[0]
param_info = param[param_type]
property_name = param_info["variable"]
# 根据不同控件类型处理
if param_type == "text-input":
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
}
if "default" in param_info:
inputSchema["properties"][property_name]["default"] = param_info[
"default"
]
elif param_type == "paragraph":
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
"format": "paragraph",
}
if "default" in param_info:
inputSchema["properties"][property_name]["default"] = param_info[
"default"
]
elif param_type == "select":
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
"enum": param_info["options"],
}
if "default" in param_info:
inputSchema["properties"][property_name]["default"] = param_info[
"default"
]
elif param_type == "file_upload":
# 文件上传控件处理
file_type_schema = {
"type": "object",
"description": param_info["label"],
"properties": {
"file_url": {"type": "string", "description": "文件URL"},
"file_name": {"type": "string", "description": "文件名称"},
},
"required": ["file_url"],
}
# 处理图片上传配置
if "image" in param_info and param_info["image"]["enabled"]:
image_config = param_info["image"]
file_type_schema["properties"]["type"] = {
"type": "string",
"description": "文件类型支持png、jpg、jpeg、webp、gif",
"enum": ["png", "jpg", "jpeg", "webp", "gif"],
}
# 处理数量限制
number_limits = image_config.get("number_limits", 3)
if number_limits > 1:
# 如果允许多个文件,则使用数组
inputSchema["properties"][property_name] = {
"type": "array",
"description": param_info["label"],
"items": file_type_schema,
"maxItems": number_limits,
}
else:
# 如果只允许单个文件
inputSchema["properties"][property_name] = file_type_schema
else:
# 如果没有特定的图片配置,使用一般文件配置
inputSchema["properties"][property_name] = file_type_schema
else:
# 默认处理为字符串类型
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
}
# 处理必填字段
if param_info.get("required", False):
inputSchema["required"].append(property_name)
# 添加必填的userId参数支持数字或字符串类型
inputSchema["properties"]["userId"] = dict(
oneOf=[{"type": "number"}, {"type": "string"}],
description="您的员工ID用于识别您的员工身份",
)
inputSchema["required"].append("userId")
return inputSchema
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
列出可用的工具
返回:
工具列表每个工具都使用JSON Schema验证其参数
"""
tools = []
tool_names = dify_api.dify_app_names
tool_infos = dify_api.dify_app_infos
tool_params = dify_api.dify_app_params
tool_num = len(tool_names)
for i in range(tool_num):
# 加载每个工具的应用信息
app_info = tool_infos[i]
# 加载每个工具的应用参数
app_param = tool_params[i]
# 处理用户输入表单
inputSchema = process_user_input_form(app_param["user_input_form"])
tools.append(
types.Tool(
name=app_info["name"],
description=app_info["description"],
inputSchema=inputSchema,
)
)
return tools
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
调用工具处理请求
参数:
name: 工具名称
arguments: 工具参数
返回:
处理结果列表
"""
tool_names = dify_api.dify_app_names
if name in tool_names:
tool_idx = tool_names.index(name)
tool_sk = dify_api.dify_app_sks[tool_idx]
# 提取files参数并创建不包含files的inputs对象
files = arguments.get("files", None) if arguments else None
inputs = {k: v for k, v in (arguments or {}).items() if k != "files"}
responses = dify_api.chat_message(
tool_sk,
inputs=inputs,
userId=arguments.get("userId", "pp666") if arguments else "pp666",
files=files,
)
for res in responses:
if res["event"] == "workflow_finished":
outputs = res["data"]["outputs"]
mcp_out = []
for _, v in outputs.items():
mcp_out.append(types.TextContent(type="text", text=v))
return mcp_out
else:
raise ValueError(f"Unknown tool: {name}")
def run_main(transport="stdio"):
"""
主函数使用stdin/stdout流运行服务器
"""
if transport == "stdio":
import anyio
from mcp.server.stdio import stdio_server
async def arun():
async with stdio_server() as streams:
await server.run(
streams[0],
streams[1],
InitializationOptions(
server_name="dify_mcp_server",
server_version="0.0.6",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
anyio.run(arun)
else:
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Mount, Route
sse = SseServerTransport("/messages/")
async def handle_sse(request):
async with sse.connect_sse(
request.scope, request.receive, request._send
) as streams:
await server.run(
streams[0], streams[1], server.create_initialization_options()
)
return Response()
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse, methods=["GET"]),
Mount("/messages/", app=sse.handle_post_message),
],
)
import uvicorn
uvicorn.run(starlette_app, host="0.0.0.0", port=8080, log_level="info")
if __name__ == "__main__":
run_main()

View File

@@ -1,342 +0,0 @@
"""
MCP创建工具的辅助函数模块
包含文件字段处理、参数预处理等功能
"""
import logging
import re
from urllib.parse import urlparse
import os
from src.utils.logger_config import get_logger
logger = get_logger(__name__)
file_type_details = {
"document": {
"extensions": "TXT, MD, MARKDOWN, PDF, HTML, XLSX, XLS, DOCX, CSV, EML, MSG, PPTX, PPT, XML, EPUB",
"description": "文档文件"
},
"image": {
"extensions": "JPG, JPEG, PNG, GIF, WEBP, SVG",
"description": "图片文件"
},
"audio": {
"extensions": "MP3, M4A, WAV, WEBM, AMR",
"description": "音频文件"
},
"video": {
"extensions": "MP4, MOV, MPEG, MPGA",
"description": "视频文件"
},
"custom": {
"extensions": "",
"description": "其他文件类型"
}
}
def process_file_arguments(arguments, current_tool_file_fields, dify_api, tool_name):
"""
处理arguments中的文件类型字段
Args:
arguments (dict): 工具调用的参数字典
current_tool_file_fields (list): 当前工具的文件字段信息列表
数据结构: [{'variable': 'txt', 'label': '输入', 'required': True, 'max_length': 48,
'allowed_file_types': ['document'], 'allowed_file_upload_methods': ['local_file', 'remote_url'],
'allowed_file_extensions': [], 'is_list': False}]
dify_api: Dify API实例包含file_parameter_pretreatment方法
tool_name (str): 工具名称,用于日志记录
Returns:
dict: 处理后的arguments字典
Raises:
Exception: 当文件处理过程中发生严重错误时抛出
"""
if not arguments or not current_tool_file_fields:
logger.info(f"工具 {tool_name}: 无需处理文件字段 (arguments={bool(arguments)}, file_fields={len(current_tool_file_fields) if current_tool_file_fields else 0})")
return arguments
# 创建文件字段变量名到字段信息的映射,用于快速查找
file_field_map = {field['variable']: field for field in current_tool_file_fields}
file_field_variables = set(file_field_map.keys())
logger.info(f"工具 {tool_name} 的文件字段变量名: {file_field_variables}")
# 创建arguments的副本避免修改原始数据
processed_arguments = arguments.copy()
# 处理arguments中的文件字段
for arg_name, arg_value in arguments.items():
# 检查参数名是否是文件字段
if arg_name in file_field_variables:
logger.info(f"工具 {tool_name}: 发现文件字段 {arg_name},值: {arg_value}")
# 检查参数值是否包含文件信息
files_to_process = _extract_files_from_argument(arg_value, arg_name, tool_name)
if not files_to_process:
logger.info(f"工具 {tool_name}: 字段 {arg_name} 不是文件格式,跳过处理")
continue
# 调用文件预处理方法
try:
logger.info(f"工具 {tool_name}: 准备处理文件列表: {files_to_process}")
# 为每个文件对象添加必要的字段
for file_obj in files_to_process:
# 设置传输方式为 remote_url从URL下载并上传
if 'transfer_method' not in file_obj:
file_obj['transfer_method'] = 'remote_url'
# 自动识别文件类型
if 'type' not in file_obj and 'url' in file_obj:
file_obj['type'] = get_file_type_from_url(file_obj['url'])
logger.info(f"工具 {tool_name}: 自动识别文件类型为 {file_obj['type']}")
# 调用文件预处理下载文件并上传到Dify获取upload_file_id
processed_files = dify_api.file_parameter_pretreatment(files_to_process)
if not processed_files or len(processed_files) == 0:
logger.error(f"工具 {tool_name}: 文件预处理失败,未返回有效结果")
continue
# 过滤掉上传失败的文件
valid_files = [f for f in processed_files if f.get('upload_file_id') and not f.get('upload_error')]
if not valid_files:
logger.error(f"工具 {tool_name}: 所有文件上传失败")
continue
logger.info(f"工具 {tool_name}: 文件预处理完成,成功上传 {len(valid_files)} 个文件")
# 获取当前字段的配置信息,判断是单文件还是多文件
field_config = file_field_map.get(arg_name, {})
is_list = field_config.get('is_list', False)
# 更新processed_arguments中的值
_update_processed_argument(processed_arguments, arg_name, arg_value, valid_files, tool_name, is_list)
except Exception as e:
logger.error(f"工具 {tool_name}: 处理文件字段 {arg_name} 时发生错误: {str(e)}")
# 继续执行,不中断整个流程
continue
return processed_arguments
def _extract_files_from_argument(arg_value, arg_name, tool_name):
"""
从参数值中提取文件信息
Args:
arg_value: 参数值可以是字符串URL、文件对象或文件列表
arg_name (str): 参数名称
tool_name (str): 工具名称
Returns:
list: 文件列表如果不是文件格式则返回None
"""
# 情况1: 单个字符串URL
if isinstance(arg_value, str):
# 检查是否是有效的URL
if arg_value.startswith(('http://', 'https://')):
file_obj = {'url': arg_value}
logger.info(f"工具 {tool_name}: 将字符串URL转换为文件对象: {file_obj}")
return [file_obj]
else:
logger.warning(f"工具 {tool_name}: 字符串不是有效的URL: {arg_value}")
return None
# 情况2: 单个文件对象
if isinstance(arg_value, dict) and _is_file_object(arg_value):
files_to_process = [arg_value]
logger.info(f"工具 {tool_name}: 处理单个文件对象: {files_to_process}")
return files_to_process
# 情况3: 文件列表
elif isinstance(arg_value, list) and len(arg_value) > 0:
# 检查列表中是否包含文件对象或URL字符串
valid_files = []
for item in arg_value:
if isinstance(item, dict) and _is_file_object(item):
valid_files.append(item)
elif isinstance(item, str) and item.startswith(('http://', 'https://')):
valid_files.append({'url': item})
if valid_files:
logger.info(f"工具 {tool_name}: 处理文件列表: {valid_files}")
return valid_files
return None
def _is_file_object(obj):
"""
判断对象是否是文件对象
Args:
obj (dict): 要检查的对象
Returns:
bool: 如果是文件对象返回True否则返回False
"""
if not isinstance(obj, dict):
return False
# 检查是否包含文件对象的关键字段
file_indicators = ['type', 'transfer_method', 'url', 'upload_file_id', 'file_url', 'file_name']
return any(key in obj for key in file_indicators)
def _update_processed_argument(processed_arguments, arg_name, original_value, processed_files, tool_name, is_list=False):
"""
更新处理后的参数值
根据字段类型决定输出格式:
- file-list (is_list=True): 输出列表格式 []
- file (is_list=False): 输出对象格式 {}
Args:
processed_arguments (dict): 处理后的参数字典
arg_name (str): 参数名称
original_value: 原始参数值可以是字符串URL、文件对象或文件列表
processed_files (list): 处理后的文件对象列表
tool_name (str): 工具名称
is_list (bool): 是否为多文件类型 (file-list=True, file=False)
"""
if not processed_files:
logger.warning(f"工具 {tool_name}: 文件处理后为空,保持原值")
return
if is_list:
# file-list 类型:使用完整列表
processed_arguments[arg_name] = processed_files
logger.info(f"工具 {tool_name}: 字段 {arg_name} 是 file-list 类型,输出 {len(processed_files)} 个文件")
else:
# file 类型:只取第一个文件对象
processed_arguments[arg_name] = processed_files[0]
logger.info(f"工具 {tool_name}: 字段 {arg_name} 是 file 类型,输出单个对象")
logger.info(f"工具 {tool_name}: 字段 {arg_name} 最终值: {processed_arguments[arg_name]}")
def validate_file_field_configuration(file_fields, tool_name):
"""
验证文件字段配置的有效性
Args:
file_fields (list): 文件字段配置列表
tool_name (str): 工具名称
Returns:
bool: 配置是否有效
"""
if not file_fields:
return True
for field in file_fields:
if not isinstance(field, dict):
logger.warning(f"工具 {tool_name}: 文件字段配置不是字典格式: {field}")
return False
if 'variable' not in field or not field['variable']:
logger.warning(f"工具 {tool_name}: 文件字段缺少variable字段: {field}")
return False
return True
def get_file_field_summary(file_fields, tool_name):
"""
获取文件字段的摘要信息
Args:
file_fields (list): 文件字段配置列表
tool_name (str): 工具名称
Returns:
dict: 文件字段摘要信息
"""
if not file_fields:
return {
'count': 0,
'variables': [],
'required_count': 0,
'optional_count': 0
}
variables = [field.get('variable', '') for field in file_fields if field.get('variable')]
required_count = sum(1 for field in file_fields if field.get('required', False))
optional_count = len(file_fields) - required_count
summary = {
'count': len(file_fields),
'variables': variables,
'required_count': required_count,
'optional_count': optional_count
}
logger.info(f"工具 {tool_name} 文件字段摘要: {summary}")
return summary
def get_file_type_from_url(url):
"""
根据URL地址返回文件类型
Args:
url (str): 文件的URL地址
Returns:
str: 文件类型 ('document', 'image', 'audio', 'video', 'custom')
Examples:
>>> get_file_type_from_url("http://example.com/file.pdf")
'document'
>>> get_file_type_from_url("http://example.com/image.jpg")
'image'
>>> get_file_type_from_url("http://example.com/video.mp4")
'video'
"""
if not url or not isinstance(url, str):
logger.warning(f"无效的URL: {url}")
return 'custom'
try:
# 解析URL获取路径
parsed_url = urlparse(url)
path = parsed_url.path
# 从路径中提取文件扩展名
file_extension = os.path.splitext(path)[1].lower().lstrip('.')
# 如果没有扩展名,尝试从查询参数中提取
if not file_extension:
# 使用正则表达式匹配常见的文件扩展名模式
extension_pattern = r'\.([a-zA-Z0-9]{2,5})(?:\?|$|&)'
match = re.search(extension_pattern, url)
if match:
file_extension = match.group(1).lower()
logger.info(f"从URL {url} 提取的文件扩展名: {file_extension}")
# 根据扩展名判断文件类型
for file_type, details in file_type_details.items():
if file_type == 'custom':
continue
# 将支持的扩展名转换为小写列表
supported_extensions = [ext.strip().lower() for ext in details['extensions'].split(',')]
if file_extension in supported_extensions:
logger.info(f"URL {url} 匹配文件类型: {file_type}")
return file_type
# 如果没有匹配到任何类型返回custom
logger.info(f"URL {url} 未匹配到已知文件类型返回custom")
return 'custom'
except Exception as e:
logger.error(f"解析URL {url} 时发生错误: {str(e)}")
return 'custom'

View File

@@ -1,53 +0,0 @@
from abc import ABC
# class WorkflowDifyAPI和chatDifyApi和completionDifyApi
# dify_app_mode_type workflow, chat, completion
class TaskInstance(ABC):
def __init__(self, base_url, dify_app_sks, dify_app_mode_type):
self.base_url = base_url
self.dify_app_sks = dify_app_sks
self.dify_app_mode_type = dify_app_mode_type
def get_task_instance(self, task_id: str):
"""
根据dify_app_mode_type返回相应的API实例
Args:
task_id: 任务ID
Returns:
返回对应的API实例
Raises:
ValueError: 当dify_app_mode_type无效时抛出异常
"""
from src.workflow.workflow_server import WorkflowDifyAPI
from src.completion.completion_server import CompletionDifyAPI
from src.chat.chat_server import ChatDifyAPI
# 使用字典映射提高代码灵活性和可维护性
api_classes = {
"workflow": WorkflowDifyAPI,
"chat": ChatDifyAPI,
"completion": CompletionDifyAPI,
}
# 检查mode_type是否有效
if self.dify_app_mode_type.lower() not in api_classes:
supported_types = ", ".join(api_classes.keys())
raise ValueError(
f"不支持的dify_app_mode_type: {self.dify_app_mode_type},支持的类型: {supported_types}"
)
# 获取对应的API类
api_class = api_classes[self.dify_app_mode_type.lower()]
# 这里假设所有API类都接受相同的参数集
# 如果各API类构造函数参数不同需要针对每种类型单独处理
return api_class(
self.base_url,
self.dify_app_sks,
)

View File

@@ -1,400 +0,0 @@
import json
import logging
# 获取日志器
logger = logging.getLogger(__name__)
data={
"user_input_form": [
{
"file": {
"variable": "files",
"label": "files",
"type": "file",
"max_length": 48,
"required": True,
"options": [],
"allowed_file_upload_methods": [
"local_file",
"remote_url"
],
"allowed_file_types": [
"image",
"document"
],
"allowed_file_extensions": []
}
}
],
}
data2={
"user_input_form": [
{
"paragraph": {
"label": "产品名称",
"max_length": 33024,
"options": [],
"required": True,
"type": "paragraph",
"variable": "name"
}
},
{
"paragraph": {
"label": "补充描述",
"max_length": 33024,
"options": [],
"required": False,
"type": "paragraph",
"variable": "desc"
}
}
],
}
data3={
"user_input_form": [
{
"file": {
"allowed_file_extensions": [],
"allowed_file_types": [
"document","image"
],
"allowed_file_upload_methods": [
"local_file",
"remote_url"
],
"label": "输入",
"max_length": 48,
"options": [],
"required": True,
"type": "file",
"variable": "txt"
}
}
]
}
def generate_file_type_description(allowed_file_types):
"""
根据allowed_file_types生成文件类型描述
参数:
allowed_file_types (list): 允许的文件类型列表
返回:
str: 生成的文件类型描述
"""
# 定义各种文件类型的具体格式和中文描述
file_type_details = {
"document": {
"extensions": "TXT, MD, MARKDOWN, PDF, HTML, XLSX, XLS, DOCX, CSV, EML, MSG, PPTX, PPT, XML, EPUB",
"description": "文档文件"
},
"image": {
"extensions": "JPG, JPEG, PNG, GIF, WEBP, SVG",
"description": "图片文件"
},
"audio": {
"extensions": "MP3, M4A, WAV, WEBM, AMR",
"description": "音频文件"
},
"video": {
"extensions": "MP4, MOV, MPEG, MPGA",
"description": "视频文件"
},
"custom": {
"extensions": "",
"description": "其他文件类型"
}
}
if not allowed_file_types or len(allowed_file_types) == 0:
return "请提供有效的文件URL地址"
# 生成描述
descriptions = []
all_extensions = []
for file_type in allowed_file_types:
if file_type in file_type_details:
detail = file_type_details[file_type]
if detail["extensions"]:
descriptions.append(f"{detail['description']}({detail['extensions']})")
all_extensions.extend(detail["extensions"].split(", "))
else:
descriptions.append(detail["description"])
if descriptions:
if len(descriptions) == 1:
return f"请提供{descriptions[0]}的URL地址"
else:
return f"请提供文件URL地址支持的文件类型{' | '.join(descriptions)}"
else:
return "请提供有效的文件URL地址"
def process_user_input_form(user_input_form):
"""
处理Dify应用的用户输入表单转换为JSON Schema格式
支持的控件类型:
- text-input (object): 文本输入控件
* label (string): 控件展示标签名
* variable (string): 控件 ID
* required (bool): 是否必填
* default (string): 默认值
- paragraph (object): 段落文本输入控件
* label (string): 控件展示标签名
* variable (string): 控件 ID
* required (bool): 是否必填
* default (string): 默认值
- select (object): 下拉控件
* label (string): 控件展示标签名
* variable (string): 控件 ID
* required (bool): 是否必填
* default (string): 默认值
* options (array[string]): 选项值
- file (object): 文件上传控件 (支持复杂的文件处理逻辑)
参数:
user_input_form (array[object]): 用户输入表单配置
返回:
dict: 处理后的inputSchema字典符合JSON Schema规范
"""
# 初始化基础schema结构
inputSchema = {
"type": "object",
"properties": {},
"required": [],
}
# 如果没有用户输入表单配置,跳过处理
if not user_input_form or len(user_input_form) == 0:
pass
else:
# 遍历处理每个表单控件
for form_item in user_input_form:
# 检查form_item是否为None或空
if not form_item or not isinstance(form_item, dict):
continue
# 获取控件类型和配置信息
control_type = list(form_item.keys())[0]
logger.debug(f"处理控件类型: {control_type}")
control_config = form_item[control_type]
# 检查control_config是否为None
if not control_config or not isinstance(control_config, dict):
continue
# 提取控件基础属性
variable = control_config.get("variable", "")
label = control_config.get("label", "")
required = control_config.get("required", False)
default_value = control_config.get("default")
# 跳过没有variable的无效控件
if not variable:
continue
# 根据控件类型进行相应处理
property_schema = None
if control_type == "text-input":
# 文本输入控件处理
property_schema = {
"type": "string",
"description": label or f"文本输入字段: {variable}",
}
# 设置默认值
if default_value is not None:
property_schema["default"] = str(default_value)
elif control_type == "paragraph":
# 段落文本输入控件处理
property_schema = {
"type": "string",
"description": label or f"段落文本字段: {variable}",
"format": "textarea", # 标识为多行文本输入
}
# 设置默认值
if default_value is not None:
property_schema["default"] = str(default_value)
elif control_type == "select":
# 下拉控件处理
options = control_config.get("options", [])
property_schema = {
"type": "string",
"description": label or f"下拉选择字段: {variable}",
}
# 设置选项枚举值
if options and len(options) > 0:
property_schema["enum"] = options
# 设置默认值
if default_value is not None:
property_schema["default"] = str(default_value)
elif control_type in ["file", "file-list"]:
# 文件上传控件处理 - 简化版本仅支持remote_url
# 获取允许的文件类型
allowed_file_types = control_config.get("allowed_file_types", [])
# 生成动态的URL描述
url_description = generate_file_type_description(allowed_file_types)
file_schema = {
"type": "object",
"description": label or f"文件上传字段: {variable}",
"properties": {
"url": {
"type": "string",
"description": url_description
}
},
"required": ["url"]
}
# 判断是单文件还是多文件
# file-list 类型或 max_length > 1 都视为多文件
actual_type = control_config.get("type", control_type)
max_length = control_config.get("max_length", 1)
is_multi_file = actual_type == "file-list" or max_length > 1
if is_multi_file:
# 多文件上传场景
property_schema = {
"type": "array",
"description": label or f"多文件上传字段: {variable}",
"items": file_schema,
"maxItems": max_length,
"minItems": 1 if required else 0
}
else:
# 单文件上传场景
property_schema = file_schema
else:
# 未知控件类型的默认处理
property_schema = {
"type": "string",
"description": label or f"未知类型字段: {variable} (类型: {control_type})",
}
if default_value is not None:
property_schema["default"] = str(default_value)
# 将处理后的属性添加到schema中
if property_schema:
inputSchema["properties"][variable] = property_schema
# 处理必填字段约束
if required:
inputSchema["required"].append(variable)
return inputSchema
def extract_file_fields(user_input_form):
"""
从用户输入表单中提取所有type为file的字段信息
参数:
user_input_form (array[object]): 用户输入表单配置
返回:
list: 包含所有file类型字段信息的列表每个元素包含
- variable (str): 字段变量名
- label (str): 字段标签
- required (bool): 是否必填
- max_length (int): 最大文件数量
- allowed_file_types (list): 允许的文件类型
- allowed_file_upload_methods (list): 允许的上传方式
- allowed_file_extensions (list): 允许的文件扩展名
- is_list (bool): 是否为多文件类型 (file-list=True, file=False)
"""
file_fields = []
# 如果没有用户输入表单配置,返回空列表
if not user_input_form or len(user_input_form) == 0:
return file_fields
# 遍历处理每个表单控件
for form_item in user_input_form:
# 检查form_item是否为None或空
if not form_item or not isinstance(form_item, dict):
continue
# 获取控件类型和配置信息
control_type = list(form_item.keys())[0]
control_config = form_item[control_type]
# 检查control_config是否为None
if not control_config or not isinstance(control_config, dict):
continue
# 只处理type为file或file-list的字段
if control_type in ["file", "file-list"] or control_config.get("type") in ["file", "file-list"]:
# 判断是否为多文件类型
# 优先使用 control_config 中的 type其次使用 control_type
actual_type = control_config.get("type", control_type)
is_list = actual_type == "file-list"
# 提取文件字段的详细信息
file_field_info = {
"variable": control_config.get("variable", ""),
"label": control_config.get("label", ""),
"required": control_config.get("required", False),
"max_length": control_config.get("max_length", 1),
"allowed_file_types": control_config.get("allowed_file_types", []),
"allowed_file_upload_methods": control_config.get("allowed_file_upload_methods", []),
"allowed_file_extensions": control_config.get("allowed_file_extensions", []),
"is_list": is_list # 新增:标识是否为多文件类型
}
# 只添加有效的字段必须有variable
if file_field_info["variable"]:
file_fields.append(file_field_info)
return file_fields
if __name__ == "__main__":
# run_main()
result = process_user_input_form(data3["user_input_form"])
print("开始生成 Schema...", result)
# 保存到当前目录下的JSON文件
output_file = "process_user_input_form_output.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
# print(f"结果已保存到: {output_file}")
# # 测试新的extract_file_fields方法
# print("\n=== 测试 extract_file_fields 方法 ===")
# # 测试data包含file字段
# file_fields_data = extract_file_fields(data["user_input_form"])
# print("data中的file字段:", file_fields_data)
# # 测试data2不包含file字段
# file_fields_data2 = extract_file_fields(data2["user_input_form"])
# print("data2中的file字段:", file_fields_data2)
# 测试data3包含file字段
# file_fields_data3 = extract_file_fields(data3["user_input_form"])
# print("data3中的file字段:", file_fields_data3)

View File

@@ -1,552 +0,0 @@
"""
统一日志配置模块
这个模块提供了整个项目的统一日志配置和管理功能,确保所有组件使用一致的日志格式和输出方式。
主要功能:
1. 统一的日志格式配置
2. 支持控制台和文件双重输出
3. 日志文件轮转管理
4. MCP模式下的特殊处理禁用控制台输出
5. 便捷的日志器获取接口
6. 丰富的日志工具函数
设计特点:
- 单例模式确保配置一致性
- 支持动态配置调整
- 异常安全的编码处理
- 详细的调试信息记录
作者: lzwcai
版本: 1.0.0
"""
import logging
import logging.handlers
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
class LoggerConfig:
"""
日志配置管理器
这个类采用单例模式管理整个项目的日志配置。
它提供了统一的日志格式、文件轮转、编码处理等功能。
主要特性:
- 单例模式:确保全局日志配置一致
- 双重输出:同时支持控制台和文件输出
- 文件轮转:自动管理日志文件大小和数量
- 编码安全:正确处理中文字符
- MCP兼容支持MCP模式下的特殊需求
配置参数:
DEFAULT_LOG_LEVEL: 默认日志级别INFO
DEFAULT_LOG_FORMAT: 日志格式模板
DEFAULT_DATE_FORMAT: 时间格式
LOG_FILE_NAME: 日志文件名
MAX_LOG_SIZE: 单个日志文件最大大小10MB
BACKUP_COUNT: 保留的备份文件数量5个
"""
# ==================== 默认配置常量 ====================
# 默认日志级别INFO级别平衡了信息量和性能
DEFAULT_LOG_LEVEL = logging.INFO
# 默认日志格式:包含时间、模块名、级别、文件位置、消息内容
DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s"
# 默认时间格式:标准的年-月-日 时:分:秒格式
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
# ==================== 日志文件配置 ====================
# 日志文件名:使用项目名称作为前缀
LOG_FILE_NAME = "lzwcai_demp_tool_server_dify_to_mcp_test.log"
# 单个日志文件最大大小10MB
MAX_LOG_SIZE = 10 * 1024 * 1024 # 10MB
# 保留的备份文件数量5个总共约50MB的日志存储
BACKUP_COUNT = 5
# ==================== 单例模式状态 ====================
# 初始化标志:确保只初始化一次
_initialized = False
# 日志文件路径:记录当前使用的日志文件路径
_log_file_path = None
@classmethod
def setup_logging(
cls,
log_level: int = DEFAULT_LOG_LEVEL,
log_file: Optional[str] = None,
console_output: bool = True,
file_output: bool = True
) -> str:
"""
设置项目统一日志配置
这是日志系统的核心初始化方法,负责配置整个项目的日志输出。
采用单例模式,确保在整个应用生命周期中只初始化一次。
配置流程:
1. 检查是否已经初始化(单例模式)
2. 确定日志文件路径(自动或手动指定)
3. 创建必要的目录结构
4. 配置根日志器和处理器
5. 设置日志格式化器
6. 添加控制台和文件处理器
7. 记录初始化信息
特殊处理:
- MCP模式下通常禁用控制台输出避免干扰stdio通信
- Windows系统下的UTF-8编码处理
- 日志文件的自动轮转管理
参数:
log_level: 日志级别DEBUG, INFO, WARNING, ERROR, CRITICAL
log_file: 日志文件路径None时使用默认路径
console_output: 是否输出到控制台MCP模式下通常为False
file_output: 是否输出到文件通常为True
返回:
str: 实际使用的日志文件路径
注意事项:
- 这个方法是线程安全的
- 重复调用会直接返回已配置的路径
- 日志文件会自动创建必要的目录
"""
# 单例模式检查:如果已经初始化,直接返回
if cls._initialized:
return cls._log_file_path
# ==================== 日志文件路径配置 ====================
if log_file is None:
# 自动确定日志文件路径:项目根目录/logs/ + 默认文件名
project_root = cls._get_project_root()
logs_dir = project_root / "logs"
logs_dir.mkdir(parents=True, exist_ok=True)
log_file = logs_dir / cls.LOG_FILE_NAME
else:
# 使用指定的日志文件路径
log_file = Path(log_file)
# 确保日志目录存在(递归创建)
log_file.parent.mkdir(parents=True, exist_ok=True)
cls._log_file_path = str(log_file)
# ==================== 根日志器配置 ====================
# 配置根日志器,这样可以捕获所有模块的日志
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
# 清除根日志器上现有的处理器,避免重复配置
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# ==================== 日志格式化器 ====================
# 创建统一的日志格式化器
formatter = logging.Formatter(
fmt=cls.DEFAULT_LOG_FORMAT, # 日志格式模板
datefmt=cls.DEFAULT_DATE_FORMAT # 时间格式
)
# ==================== 控制台处理器配置 ====================
if console_output:
# 控制台输出处理器支持彩色输出和UTF-8编码
import io
# 处理Windows系统的编码问题
if hasattr(sys.stdout, 'buffer'):
# 在Windows上强制使用UTF-8编码避免中文乱码
# errors='replace'确保即使有编码问题也不会崩溃
console_stream = io.TextIOWrapper(
sys.stdout.buffer,
encoding='utf-8',
errors='replace'
)
else:
# Unix/Linux系统通常默认支持UTF-8
console_stream = sys.stdout
# 创建控制台处理器
console_handler = logging.StreamHandler(console_stream)
console_handler.setLevel(log_level)
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
# ==================== 文件处理器配置 ====================
if file_output:
# 文件输出处理器,支持自动轮转
file_handler = logging.handlers.RotatingFileHandler(
filename=cls._log_file_path, # 日志文件路径
maxBytes=cls.MAX_LOG_SIZE, # 单文件最大大小
backupCount=cls.BACKUP_COUNT, # 备份文件数量
encoding='utf-8' # 文件编码
)
file_handler.setLevel(log_level)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# ==================== 初始化完成标记 ====================
# 标记为已初始化,防止重复配置
cls._initialized = True
# ==================== 记录初始化信息 ====================
# 获取当前模块的日志器并记录初始化信息
logger = logging.getLogger(__name__)
logger.info("=" * 80)
logger.info(f"日志系统初始化完成 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
logger.info(f"日志级别: {logging.getLevelName(log_level)}")
logger.info(f"日志文件: {cls._log_file_path}")
logger.info(f"控制台输出: {console_output}")
logger.info(f"文件输出: {file_output}")
logger.info(f"文件轮转: 最大{cls.MAX_LOG_SIZE // (1024*1024)}MB, 保留{cls.BACKUP_COUNT}个备份")
logger.info("=" * 80)
return cls._log_file_path
@classmethod
def _get_project_root(cls) -> Path:
"""
获取项目根目录
这个方法通过向上遍历目录树来查找项目根目录。
它会寻找常见的项目标识文件来确定根目录位置。
查找策略:
1. 从当前文件所在目录开始向上查找
2. 寻找项目标识文件pyproject.toml, setup.py, main.py
3. 找到任一标识文件的目录即为项目根目录
4. 如果都找不到,使用当前文件的上级目录作为备选
返回:
Path: 项目根目录的路径对象
注意事项:
- 这个方法假设项目结构相对标准
- 在特殊的部署环境中可能需要调整
- 备选方案确保总是返回有效路径
"""
# 从当前文件向上查找项目根目录
current_path = Path(__file__).parent
# 向上遍历目录树
while current_path.parent != current_path: # 避免到达文件系统根目录
# 检查常见的项目标识文件
if (current_path / "pyproject.toml").exists() or \
(current_path / "setup.py").exists() or \
(current_path / "main.py").exists():
return current_path
current_path = current_path.parent
# 备选方案:如果找不到标识文件,使用预设的相对路径
# 这个路径基于当前的项目结构utils -> src -> 项目根
return Path(__file__).parent.parent.parent
@classmethod
def get_logger(cls, name: str) -> logging.Logger:
"""
获取配置好的日志器
这是获取日志器的标准方法,确保返回的日志器使用统一的配置。
如果日志系统尚未初始化,会自动进行初始化。
参数:
name: 日志器名称,通常使用模块的 __name__ 变量
返回:
logging.Logger: 配置好的日志器实例
使用示例:
logger = LoggerConfig.get_logger(__name__)
logger.info("这是一条信息日志")
特性:
- 自动初始化首次调用时自动配置日志系统MCP模式下禁用控制台输出
- 层次化命名支持Python日志器的层次化命名
- 统一配置:所有日志器使用相同的格式和输出配置
"""
# 检查是否已初始化,未初始化则使用默认配置初始化
# 重要在MCP模式下禁用控制台输出避免干扰stdio通信
if not cls._initialized:
cls.setup_logging(console_output=False, file_output=True)
# 返回指定名称的日志器
return logging.getLogger(name)
# ==================== 日志工具方法 ====================
@classmethod
def log_function_entry(cls, logger: logging.Logger, func_name: str, **kwargs):
"""
记录函数入口日志
用于调试和性能分析,记录函数被调用时的参数信息。
通常在DEBUG级别输出不会影响生产环境的性能。
参数:
logger: 日志器实例
func_name: 函数名称
**kwargs: 函数参数(键值对形式)
使用示例:
LoggerConfig.log_function_entry(logger, "process_data", user_id=123, action="login")
"""
args_str = ", ".join([f"{k}={v}" for k, v in kwargs.items()])
logger.debug(f"进入函数 {func_name}({args_str})")
@classmethod
def log_function_exit(cls, logger: logging.Logger, func_name: str, result=None):
"""
记录函数出口日志
与log_function_entry配对使用记录函数执行完成和返回值。
有助于跟踪函数执行流程和调试返回值问题。
参数:
logger: 日志器实例
func_name: 函数名称
result: 函数返回值(可选)
使用示例:
LoggerConfig.log_function_exit(logger, "process_data", result={"status": "success"})
"""
if result is not None:
logger.debug(f"退出函数 {func_name},返回值: {result}")
else:
logger.debug(f"退出函数 {func_name}")
@classmethod
def log_api_request(cls, logger: logging.Logger, method: str, url: str, **kwargs):
"""
记录API请求日志
标准化API请求的日志记录包含HTTP方法、URL和请求参数。
有助于API调用的监控和调试。
参数:
logger: 日志器实例
method: HTTP方法GET, POST, PUT, DELETE等
url: 请求URL
**kwargs: 请求参数(可选)
使用示例:
LoggerConfig.log_api_request(logger, "POST", "https://api.example.com/users",
headers={"Authorization": "Bearer xxx"})
"""
logger.info(f"API请求 - {method} {url}")
if kwargs:
logger.debug(f"请求参数: {kwargs}")
@classmethod
def log_api_response(cls, logger: logging.Logger, status_code: int, response_time: float = None):
"""
记录API响应日志
记录API响应的状态码和响应时间用于性能监控和问题诊断。
参数:
logger: 日志器实例
status_code: HTTP状态码
response_time: 响应时间(秒,可选)
使用示例:
LoggerConfig.log_api_response(logger, 200, 0.156)
"""
if response_time:
logger.info(f"API响应 - 状态码: {status_code}, 响应时间: {response_time:.3f}s")
else:
logger.info(f"API响应 - 状态码: {status_code}")
@classmethod
def log_error_with_context(cls, logger: logging.Logger, error: Exception, context: str = ""):
"""
记录带上下文的错误日志
提供丰富的错误信息记录,包含异常类型、错误消息、上下文信息和详细堆栈。
这是错误处理的标准方法。
参数:
logger: 日志器实例
error: 异常对象
context: 错误发生的上下文描述(可选)
使用示例:
try:
risky_operation()
except Exception as e:
LoggerConfig.log_error_with_context(logger, e, "处理用户请求时")
"""
if context:
logger.error(f"错误发生在 {context}: {type(error).__name__}: {str(error)}")
else:
logger.error(f"错误: {type(error).__name__}: {str(error)}")
# 记录详细的异常堆栈信息仅在DEBUG级别显示
logger.debug("错误详情:", exc_info=True)
# ==================== 便捷函数 ====================
def get_logger(name: str) -> logging.Logger:
"""
获取日志器的便捷函数
这是LoggerConfig.get_logger的简化版本提供更简洁的调用方式。
推荐在模块级别使用这个函数获取日志器。
参数:
name: 日志器名称,通常使用 __name__
返回:
logging.Logger: 配置好的日志器实例
使用示例:
logger = get_logger(__name__)
"""
return LoggerConfig.get_logger(name)
def setup_logging(**kwargs) -> str:
"""
设置日志的便捷函数
这是LoggerConfig.setup_logging的简化版本支持所有相同的参数。
参数:
**kwargs: 传递给LoggerConfig.setup_logging的所有参数
返回:
str: 日志文件路径
使用示例:
log_file = setup_logging(log_level=logging.DEBUG, console_output=False)
"""
return LoggerConfig.setup_logging(**kwargs)
# ==================== 装饰器 ====================
def log_function_calls(logger: Optional[logging.Logger] = None):
"""
函数调用日志装饰器
这个装饰器自动记录函数的调用和返回,包括参数和返回值。
主要用于调试和性能分析在生产环境中通常设置为DEBUG级别。
特性:
- 自动记录函数入口和出口
- 记录函数参数kwargs
- 记录返回值
- 自动处理异常并记录错误上下文
- 支持自定义日志器或自动获取
参数:
logger: 可选的日志器实例None时自动获取函数所在模块的日志器
返回:
装饰器函数
使用示例:
@log_function_calls()
def process_user_data(user_id, action="login"):
# 函数实现
return {"status": "success"}
# 或者指定日志器
@log_function_calls(logger=my_logger)
def another_function():
pass
注意事项:
- 会记录所有kwargs参数注意不要记录敏感信息
- 返回值也会被记录,大对象可能影响性能
- 异常会被重新抛出,不会被吞掉
"""
def decorator(func):
def wrapper(*args, **kwargs):
nonlocal logger
# 如果没有提供日志器,自动获取函数所在模块的日志器
if logger is None:
logger = get_logger(func.__module__)
func_name = func.__name__
# 记录函数入口只记录kwargs避免记录过多信息
LoggerConfig.log_function_entry(logger, func_name, **kwargs)
try:
# 执行原函数
result = func(*args, **kwargs)
# 记录函数出口和返回值
LoggerConfig.log_function_exit(logger, func_name, result)
return result
except Exception as e:
# 记录异常信息并重新抛出
LoggerConfig.log_error_with_context(logger, e, f"函数 {func_name}")
raise
return wrapper
return decorator
# ==================== 测试代码 ====================
if __name__ == "__main__":
"""
日志配置测试代码
这个测试代码演示了日志系统的基本功能,包括:
1. 日志系统初始化
2. 不同级别的日志输出
3. 日志文件路径获取
4. 装饰器功能测试
运行方式:
python -m src.utils.logger_config
"""
# 初始化日志系统DEBUG级别同时输出到控制台和文件
log_file = setup_logging(log_level=logging.DEBUG)
test_logger = get_logger(__name__)
test_logger.info("开始测试日志配置...")
# 测试不同级别的日志输出
test_logger.debug("这是一个调试消息 - 用于开发调试")
test_logger.info("这是一个信息消息 - 记录重要信息")
test_logger.warning("这是一个警告消息 - 提醒注意事项")
test_logger.error("这是一个错误消息 - 记录错误情况")
# 测试工具方法
LoggerConfig.log_api_request(test_logger, "GET", "https://api.example.com/test")
LoggerConfig.log_api_response(test_logger, 200, 0.123)
# 测试装饰器
@log_function_calls()
def test_function(param1, param2="default"):
"""测试函数"""
return {"result": "success", "param1": param1}
# 调用测试函数
result = test_function("test_value", param2="custom")
# 输出日志文件位置
test_logger.info(f"日志文件位置: {log_file}")
test_logger.info("日志配置测试完成!")

View File

@@ -1,153 +0,0 @@
import os
import sys
from typing import Dict, Any, Optional
# 导入翻译函数
from .translator import translate
class TranslationService:
"""翻译服务类,用于处理各种翻译需求"""
@staticmethod
def create_prompt(
content: str,
target_lang: str,
use_case: str,
style: str,
prompt_type: str = "general",
keep_terms_desc: str = "核心术语",
) -> str:
"""
创建翻译提示
Args:
content: 待翻译内容
target_lang: 目标语言
use_case: 使用场景
style: 翻译风格
prompt_type: 提示类型,可选值: "general", "tool_name", "tool_description"
keep_terms_desc: 保留术语的描述
Returns:
str: 格式化的翻译提示
"""
if prompt_type == "tool_name":
keep_terms_desc = "核心术语(如 小写,词语需要用下划线连接)"
elif prompt_type == "tool_description":
keep_terms_desc = "核心术语(这是一段话)"
return f"""
角色:专业本地化翻译专家
任务:将以下内容翻译为{target_lang}(目标用途:{use_case}
要求:
1. 仅返回译文,不含解释或原文;
2. 保留{keep_terms_desc}
3. 符合{style}风格;
4. 特殊符号保持原样。
示例输出格式:
Translated Text
待翻译内容:
{content}
"""
@staticmethod
def translate_text(
content: str,
target_lang: str,
use_case: str = "",
style: str = "正式且符合技术品牌调性",
prompt_type: str = "general",
) -> Dict[str, Any]:
"""
翻译文本
Args:
content: 待翻译内容
target_lang: 目标语言
use_case: 使用场景,默认为空
style: 翻译风格,默认为"正式且符合技术品牌调性"
prompt_type: 提示类型,可选值: "general", "tool_name", "tool_description"
Returns:
Dict: 包含翻译结果的字典
"""
prompt = TranslationService.create_prompt(
content=content,
target_lang=target_lang,
use_case=use_case,
style=style,
prompt_type=prompt_type,
)
try:
result = translate(prompt, target_lang)
return result
except Exception as e:
print(f"翻译出错: {str(e)}")
return {"translated_text": "", "error": str(e)}
@staticmethod
def translate_tool_name(
name: str,
target_lang: str = "英语",
use_case: str = "工具名称",
style: str = "正式且符合技术品牌调性,大模型能理解",
) -> str:
"""
翻译工具名称的便捷方法
Returns:
str: 翻译后的工具名称
"""
result = TranslationService.translate_text(
content=name,
target_lang=target_lang,
use_case=use_case,
style=style,
prompt_type="tool_name",
)
return result.get("translated_text", "")
@staticmethod
def translate_tool_description(
description: str,
target_lang: str = "英语",
use_case: str = "工具描述",
style: str = "正式且符合技术品牌调性,大模型能理解",
) -> str:
"""
翻译工具描述的便捷方法
Returns:
str: 翻译后的工具描述
"""
result = TranslationService.translate_text(
content=description,
target_lang=target_lang,
use_case=use_case,
style=style,
prompt_type="tool_description",
)
return result.get("translated_text", "")
def translation_example():
"""翻译功能使用示例"""
# 示例1: 翻译工具名称
tool_name = "万川AI新媒体平台【测试环境】"
translated_name = TranslationService.translate_tool_name(tool_name)
print(f"工具名称翻译: {translated_name}")
# 示例2: 翻译工具描述
description = "21日辛柏青发布讣告宣布妻子朱媛媛抗癌五年后离世。此前在一次路演现场当观众问及朱媛媛时辛柏青2秒停顿藏着"
translated_desc = TranslationService.translate_tool_description(description)
print(f"工具描述翻译: {translated_desc}")
if __name__ == "__main__":
translation_example()

View File

@@ -1,64 +0,0 @@
import os
import requests
import json
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
# ========== 模型相关 ==========
# 从.env文件获取模型API配置
BASE_URL = os.getenv("BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1")
API_KEY = os.getenv("OPENAI_API_KEY", "sk-c5a912a6bc8e4c9cbdbdf68232352a03")
TEMPERATURE = float(os.getenv("MODEL_TEMPERATURE", "0.7"))
def translate(content, target_language):
"""
翻译文本内容到目标语言
:param content: 要翻译的内容
:param target_language: 目标语言,如'en'(英语), 'zh'(中文), 'ja'(日语), 'fr'(法语)等
:return: 翻译后的内容,如果翻译失败则返回原文和错误信息
"""
if not content or not target_language:
return {"error": "内容或目标语言不能为空", "translated_text": content}
# 确保API密钥已设置
if not API_KEY:
return {"error": "API密钥未设置请检查.env文件", "translated_text": content}
try:
# 构建API请求头
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}",
}
# 构建翻译提示
prompt = f"请将以下内容翻译成{target_language},只返回翻译结果,不要包含任何解释或原文:\n\n{content}"
# 构建API请求体
data = {
"model": "qwen-max", # 使用通义千问模型,可以根据实际需要更改
"messages": [{"role": "user", "content": prompt}],
"temperature": TEMPERATURE,
}
# 发送API请求
response = requests.post(
f"{BASE_URL}/chat/completions", headers=headers, json=data
)
# 解析响应
if response.status_code == 200:
result = response.json()
translated_text = result["choices"][0]["message"]["content"].strip()
return {"translated_text": translated_text}
else:
error_message = (
f"翻译失败,状态码: {response.status_code}, 响应: {response.text}"
)
return {"error": error_message, "translated_text": content}
except Exception as e:
return {"error": f"翻译过程中发生错误: {str(e)}", "translated_text": content}

View File

@@ -1,461 +0,0 @@
"""
Dify文件上传工具 - 优化版
主要功能:
- 从URL下载文件并自动上传到Dify API
- 支持常见文件类型JPG、PNG、GIF、PDF、DOCX、TXT等
- 自动处理文件大小检查、MIME类型识别、临时文件清理
使用方法:
from upload_file import upload_file_from_url
result = upload_file_from_url(
file_url="http://example.com/image.jpg",
base_url="http://192.168.2.236:3001/v1",
api_key="app-QdfDKqHAI3dlB6tvnibuh6rv"
)
file_id = result['id'] # 获取上传后的文件ID
"""
import os
import tempfile
import requests
import logging
from urllib.parse import urlparse, unquote
from typing import Optional
# 获取模块级别的logger避免影响全局日志配置
logger = logging.getLogger(__name__)
# 常用MIME类型映射
MIME_TYPE_MAP = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
'.pdf': 'application/pdf',
'.txt': 'text/plain',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
}
def download_file_from_url(
url: str,
download_dir: Optional[str] = None,
filename: Optional[str] = None,
timeout: int = 30,
max_retries: int = 3
) -> str:
"""
从URL下载文件到本地并返回文件路径
Args:
url: 文件的URL地址
download_dir: 下载目录如果为None则使用系统临时目录
filename: 指定文件名如果为None则从URL中提取
timeout: 请求超时时间(秒)
max_retries: 最大重试次数
Returns:
str: 下载后的本地文件路径
Raises:
Exception: 下载失败时抛出异常
"""
# 设置下载目录
if download_dir is None:
download_dir = tempfile.gettempdir()
# 确保下载目录存在
os.makedirs(download_dir, exist_ok=True)
# 提取文件名
if filename is None:
filename = _extract_filename_from_url(url)
# 构建完整的文件路径
file_path = os.path.join(download_dir, filename)
# 下载文件(带重试机制)
for attempt in range(max_retries):
try:
logger.info(f"正在下载文件: {url} (尝试 {attempt + 1}/{max_retries})")
# 发送GET请求下载文件
response = requests.get(url, timeout=timeout, stream=True)
response.raise_for_status()
# 写入文件
with open(file_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
# 验证文件是否下载成功
if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
logger.info(f"文件下载成功: {file_path} (大小: {os.path.getsize(file_path)} 字节)")
return file_path
else:
raise Exception("下载的文件为空或不存在")
except requests.exceptions.Timeout:
error_msg = f"请求超时 (超过 {timeout} 秒)"
logger.warning(f"{error_msg}, 尝试 {attempt + 1}/{max_retries}")
if attempt == max_retries - 1:
raise Exception(f"下载失败:{error_msg}")
except requests.exceptions.RequestException as e:
error_msg = f"请求异常: {str(e)}"
logger.warning(f"{error_msg}, 尝试 {attempt + 1}/{max_retries}")
if attempt == max_retries - 1:
raise Exception(f"下载失败:{error_msg}")
except Exception as e:
error_msg = f"下载过程中发生错误: {str(e)}"
logger.warning(f"{error_msg}, 尝试 {attempt + 1}/{max_retries}")
if attempt == max_retries - 1:
raise Exception(f"下载失败:{error_msg}")
raise Exception("下载失败:已达到最大重试次数")
def _extract_filename_from_url(url: str) -> str:
"""
从URL中提取文件名
Args:
url: 文件URL
Returns:
str: 提取的文件名
"""
try:
# 解析URL
parsed_url = urlparse(url)
path = unquote(parsed_url.path)
# 从路径中提取文件名
filename = os.path.basename(path)
# 如果没有找到文件名或文件名为空,使用默认名称
if not filename or filename == '/':
filename = "downloaded_file"
# 移除查询参数(如果文件名中包含)
if '?' in filename:
filename = filename.split('?')[0]
return filename
except Exception as e:
logger.warning(f"无法从URL提取文件名: {str(e)}, 使用默认文件名")
return "downloaded_file"
def upload_file_to_dify(
file_path: str,
base_url: str,
api_key: str,
user: str = "default_user",
verify_ssl: bool = False
) -> dict:
"""
上传文件到Dify API
Args:
file_path: 本地文件路径
base_url: Dify API基础URL (例如: http://192.168.2.236:3001/v1)
api_key: API密钥
user: 用户标识
verify_ssl: 是否验证SSL证书
Returns:
dict: 上传响应结果
Raises:
Exception: 上传失败时抛出异常
"""
# 检查文件是否存在
if not os.path.exists(file_path):
raise FileNotFoundError(f"文件不存在: {file_path}")
# 检查文件大小
file_size = os.path.getsize(file_path)
file_size_mb = file_size / (1024 * 1024)
logger.info(f"准备上传文件: {file_path} (大小: {file_size_mb:.2f} MB)")
# 检查文件大小是否超过限制
if file_size_mb > 10:
logger.warning(f"文件大小 {file_size_mb:.2f} MB 可能超过服务器限制 (10 MB)")
upload_url = f"{base_url}/files/upload"
headers = {"Authorization": f"Bearer {api_key}"}
try:
with open(file_path, "rb") as f:
# 获取文件扩展名和MIME类型
file_ext = os.path.splitext(file_path)[1].lower()
mime_type = MIME_TYPE_MAP.get(file_ext, "application/octet-stream")
# 构建文件上传数据
files = {"file": (os.path.basename(file_path), f, mime_type)}
data = {"user": user}
# 发送上传请求
response = requests.post(
upload_url,
headers=headers,
files=files,
data=data,
verify=verify_ssl
)
# 检查响应
if response.status_code == 201:
result = response.json()
logger.info(f"文件上传成功: {result.get('name', 'unknown')} (ID: {result.get('id', 'unknown')})")
return result
else:
error_msg = f"上传失败 (状态码: {response.status_code}): {response.text}"
logger.error(error_msg)
raise requests.exceptions.HTTPError(error_msg)
except requests.exceptions.RequestException as e:
logger.error(f"上传文件请求失败: {str(e)}")
raise
except Exception as e:
logger.error(f"上传文件失败: {str(e)}")
raise
def check_app_config(base_url: str, api_key: str):
"""
检查Dify应用的文件上传配置
Args:
base_url: Dify API基础URL
api_key: API密钥
"""
try:
# 获取应用参数配置
config_url = f"{base_url}/parameters"
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.get(config_url, headers=headers, verify=False)
response.raise_for_status()
config = response.json()
logger.info(f"应用配置获取成功")
# 检查文件上传配置
file_upload = config.get("file_upload", {})
if file_upload.get("enabled", False):
logger.info("✓ 文件上传功能已启用")
logger.info(f" - 允许的文件类型: {file_upload.get('allowed_file_types', [])}")
logger.info(f" - 允许的文件扩展名: {file_upload.get('allowed_file_extensions', [])}")
logger.info(f" - 文件大小限制: {file_upload.get('fileUploadConfig', {}).get('image_file_size_limit', 'N/A')} MB")
else:
logger.warning("✗ 文件上传功能未启用")
return config
except Exception as e:
logger.error(f"检查应用配置失败: {str(e)}")
return None
def test_download_and_upload():
"""
测试下载和上传功能的示例
"""
# 测试用的文件URL
file_url = "http://192.168.2.236:9000/lzwcai/upload/2025-07-29/34b28da03f3c43b0921ba1b76857bbc0/34b28da03f3c43b0921ba1b76857bbc0.JPG?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20250729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250729T075242Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=80f58d37c3bd52fb2b25efa36b5df0d73c8da7c773e7a7066dd6563710c619d6"
# 上传API配置
base_url = "http://192.168.2.236:3001/v1"
upload_url = f"{base_url}/files/upload"
api_key = "app-QdfDKqHAI3dlB6tvnibuh6rv"
headers = {"Authorization": f"Bearer {api_key}"}
user = "abc-123"
try:
logger.info("开始测试下载和上传流程...")
# 0. 检查应用配置
logger.info("检查Dify应用配置...")
config = check_app_config(base_url, api_key)
if not config:
logger.error("无法获取应用配置,终止测试")
return
# 1. 下载文件
logger.info(f"正在下载文件: {file_url}")
file_path = download_file_from_url(file_url)
logger.info(f"文件下载成功,保存路径: {file_path}")
# 2. 上传文件
logger.info(f"正在上传文件到: {upload_url}")
# 检查文件大小
file_size = os.path.getsize(file_path)
file_size_mb = file_size / (1024 * 1024)
logger.info(f"文件大小: {file_size_mb:.2f} MB")
# 检查文件大小是否超过限制通常为10MB对于图片
if file_size_mb > 10:
logger.warning(f"文件大小 {file_size_mb:.2f} MB 可能超过服务器限制")
with open(file_path, "rb") as f:
# 获取文件扩展名来确定MIME类型
file_ext = os.path.splitext(file_path)[1].lower()
mime_type = "image/jpeg" if file_ext in ['.jpg', '.jpeg'] else "application/octet-stream"
# 构建文件上传数据指定正确的MIME类型
files = {"file": (os.path.basename(file_path), f, mime_type)}
data = {"user": user}
# 不要在headers中设置Content-Type让requests自动处理multipart/form-data
upload_headers = headers.copy()
if "Content-Type" in upload_headers:
del upload_headers["Content-Type"]
# 添加SSL验证跳过选项用于自签名证书
response = requests.post(upload_url, headers=upload_headers, files=files, data=data, verify=False)
# 打印详细的响应信息用于调试
logger.info(f"响应状态码: {response.status_code}")
logger.info(f"响应头: {response.headers}")
logger.info(f"响应内容: {response.text}")
response.raise_for_status()
result = response.json()
logger.info(f"文件上传成功,响应: {result}")
# 3. 清理临时文件
try:
os.remove(file_path)
logger.info(f"已清理临时文件: {file_path}")
except Exception as e:
logger.warning(f"清理临时文件失败: {str(e)}")
return result
except Exception as e:
logger.error(f"测试失败: {str(e)}")
raise
def upload_file_from_url(
file_url: str,
base_url: str,
api_key: str,
user: str = "default_user",
verify_ssl: bool = False
) -> dict:
"""
从URL下载文件并上传到Dify API - 一站式解决方案
这是一个优化后的方法只需要提供URL地址、base_url和api_key就能自动完成下载和上传。
支持常见的文件类型JPG、PNG、GIF、PDF、DOCX、TXT等。
自动处理文件大小检查、MIME类型识别、临时文件清理等。
Args:
file_url: 要下载的文件URL
base_url: Dify API基础URL (例如: http://192.168.2.236:3001/v1)
api_key: API密钥 (例如: app-QdfDKqHAI3dlB6tvnibuh6rv)
user: 用户标识 (可选,默认为 default_user)
verify_ssl: 是否验证SSL证书 (可选,默认为 False)
Returns:
dict: 上传成功后的结果包含文件ID、名称、大小等信息
示例: {
'id': 'a239b623-40a8-482c-859f-bb8368d5b1fe',
'name': 'example.jpg',
'size': 495240,
'extension': 'jpg',
'mime_type': 'image/jpeg',
'created_by': '92c4b250-e0e7-4123-900d-f5c2187679a2',
'created_at': 1753777420
}
使用示例:
result = upload_file_from_url(
file_url="http://example.com/image.jpg",
base_url="http://192.168.2.236:3001/v1",
api_key="app-QdfDKqHAI3dlB6tvnibuh6rv"
)
file_id = result['id'] # 获取上传后的文件ID
Raises:
Exception: 下载或上传失败时抛出异常
"""
temp_file_path = None
try:
logger.info(f"开始处理文件: {file_url}")
# 1. 下载文件到临时目录
temp_file_path = download_file_from_url(file_url)
# 2. 上传文件到Dify (复用已有的上传函数)
result = upload_file_to_dify(temp_file_path, base_url, api_key, user, verify_ssl)
return result
except Exception as e:
logger.error(f"处理文件失败: {str(e)}")
raise
finally:
# 清理临时文件
if temp_file_path and os.path.exists(temp_file_path):
try:
os.remove(temp_file_path)
logger.info(f"已清理临时文件: {temp_file_path}")
except Exception as e:
logger.warning(f"清理临时文件失败: {str(e)}")
def test_upload_functionality():
"""
测试文件上传功能的示例
注意:这个函数包含示例配置,实际使用时请替换为真实的配置
"""
# 示例配置 - 实际使用时请替换为真实的配置
file_url = "https://example.com/test-image.jpg" # 替换为实际的文件URL
base_url = "http://localhost:3001/v1" # 替换为实际的Dify API地址
api_key = "your-api-key-here" # 替换为实际的API密钥
try:
logger.info("=== 开始测试文件上传功能 ===")
# 检查应用配置
logger.info("检查Dify应用配置...")
config = check_app_config(base_url, api_key)
if not config:
logger.warning("无法获取应用配置,但继续测试上传功能")
# 调用上传方法
result = upload_file_from_url(file_url, base_url, api_key)
logger.info("=== 上传成功!结果如下 ===")
logger.info(f"文件ID: {result.get('id')}")
logger.info(f"文件名: {result.get('name')}")
logger.info(f"文件大小: {result.get('size')} 字节")
logger.info(f"文件类型: {result.get('mime_type')}")
logger.info(f"扩展名: {result.get('extension')}")
return result
except Exception as e:
logger.error(f"测试失败: {str(e)}")
raise
if __name__ == "__main__":
# 运行测试 - 注意需要先配置正确的URL和API密钥
print("警告:测试函数包含示例配置,请先修改为实际配置后再运行")
# test_upload_functionality() # 取消注释并配置后运行

View File

@@ -1,352 +0,0 @@
import requests
from abc import ABC
import logging
import json
from src.utils.logger_config import get_logger
# 导入 pypinyin 用于中文转拼音
try:
import pypinyin
except ImportError:
pypinyin = None
logging.warning("pypinyin 模块未安装,将使用简化的命名方式")
logger = get_logger(__name__)
class DifyAPIError(Exception):
"""Dify API 错误异常类"""
def __init__(self, status_code: int, error_code: str, message: str, request_data: dict = None):
self.status_code = status_code
self.error_code = error_code
self.message = message
self.request_data = request_data
super().__init__(self.message)
def __str__(self):
return f"[{self.status_code}] {self.error_code}: {self.message}"
def to_dict(self):
return {
"status_code": self.status_code,
"error_code": self.error_code,
"message": self.message
}
def pinyin_to_camel(pinyin):
"""
将中文名称转换为工具名称
处理逻辑:
1. 如果安装了 pypinyin将中文转换为拼音然后转为驼峰命名
2. 如果未安装 pypinyin将所有非字母数字字符替换为下划线
3. 所有符号都会被替换成下划线
示例:
"你好啊" -> "tool_NiHaoA" (有pypinyin)
"测试-工具" -> "tool_测试_工具" (无pypinyin)
"Hello World!" -> "tool_Hello_World_" (无pypinyin)
Args:
pinyin: 输入的字符串(可能包含中文、英文、符号等)
Returns:
str: 格式化后的工具名称,以 "tool_" 开头
"""
import re
if pypinyin is None:
# 如果 pypinyin 未安装,使用简化的命名方式
# 将所有非字母数字字符(包括空格、符号等)替换为下划线
cleaned = re.sub(r'[^\w]', '_', str(pinyin))
# 移除连续的下划线
cleaned = re.sub(r'_+', '_', cleaned)
# 移除首尾的下划线
cleaned = cleaned.strip('_')
return "tool_" + cleaned if cleaned else "tool_unnamed"
# 使用 pypinyin 转换中文为拼音
pinyin_list = pypinyin.lazy_pinyin(pinyin)
# 处理每个拼音单词
processed_words = []
for word in pinyin_list:
# 将所有非字母数字字符替换为下划线
cleaned_word = re.sub(r'[^\w]', '_', word)
# 移除连续的下划线
cleaned_word = re.sub(r'_+', '_', cleaned_word)
# 移除首尾的下划线
cleaned_word = cleaned_word.strip('_')
if cleaned_word:
# 首字母大写(驼峰命名)
processed_words.append(cleaned_word.capitalize())
# 拼接所有单词
result = "".join(processed_words) if processed_words else "Unnamed"
return "tool_" + result
class WorkflowDifyAPI(ABC):
def __init__(self, base_url: str, dify_app_sks: list, user="pp666"):
# dify configs
self.dify_base_url = base_url
self.dify_app_sks = dify_app_sks
self.user = user
# dify app infos
dify_app_infos = []
dify_app_params = []
dify_app_metas = []
for key in self.dify_app_sks:
dify_app_infos.append(self.get_app_info(key))
dify_app_params.append(self.get_app_parameters(key))
dify_app_metas.append(self.get_app_meta(key))
self.dify_app_infos = dify_app_infos
self.dify_app_params = dify_app_params
self.dify_app_metas = dify_app_metas
self.dify_app_names = [x["name"] for x in dify_app_infos]
def chat_message(
self,
api_key,
inputs={},
response_mode="streaming",
conversation_id=None,
userId="pp666",
files=None,
):
url = f"{self.dify_base_url}/workflows/run"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": userId,
}
logger.info("Sending data to Dify API: %s", data)
logger.info("Sending headers to Dify API: %s", headers)
logger.info("Sending url to Dify API: %s", url)
if conversation_id:
data["conversation_id"] = conversation_id
if files:
files_data = self.file_parameter_pretreatment(files)
if files_data and len(files_data) > 0:
data["inputs"]["files"] = files_data[0]
# For workflow API, we send files data in the JSON payload, not as multipart files
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
else:
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
logger.info(f"Response1:{data} {response.status_code} {response.reason}")
# Add debugging for error responses
if response.status_code != 200:
logger.error(f"API request failed with status {response.status_code}")
logger.error(f"Response content: {response.text}")
logger.error(f"Request data: {data}")
# 解析错误响应并抛出带有详细信息的异常
try:
error_data = response.json()
error_message = error_data.get("message", response.text)
error_code = error_data.get("code", "unknown_error")
except json.JSONDecodeError:
error_message = response.text
error_code = "unknown_error"
raise DifyAPIError(
status_code=response.status_code,
error_code=error_code,
message=error_message,
request_data=data
)
if response_mode == "streaming":
def stream_generator():
for line in response.iter_lines():
if line:
if line.startswith(b"data:"):
try:
json_data = json.loads(line[5:].decode("utf-8"))
yield json_data
except json.JSONDecodeError:
logger.error(f"Error decoding JSON: {line}")
return stream_generator()
else:
return response.json()
def upload_file(self, api_key, file_path, user="pp666"):
url = f"{self.dify_base_url}/files/upload"
headers = {"Authorization": f"Bearer {api_key}"}
data = {"user": user}
with open(file_path, "rb") as f:
files = {"file": f}
response = requests.post(url, headers=headers, files=files, data=data)
response.raise_for_status()
return response.json()
def upload_file_remote_url(self, file_url):
from src.utils.upload_file import upload_file_from_url
base_url = self.dify_base_url
api_key = self.dify_app_sks[0]
return upload_file_from_url(file_url, base_url, api_key)
def file_parameter_pretreatment(self, files):
"""
文件参数预处理方法
传入的"files"数据结构是这样的: [
{
"type": "image",
"transfer_method": "remote_url",
"url": "http://example.com/image.jpg"
}
]
处理逻辑:
1. 遍历files列表中的每个文件对象
2. 对于transfer_method为"remote_url"的文件调用upload_file_remote_url方法
3. 将返回的对象的id字段存入原对象的upload_file_id字段
4. 设置transfer_method为"local_file"因为已经上传到Dify服务器
5. 返回处理好的files列表
Args:
files (list): 文件列表每个元素包含type、transfer_method、url等字段
Returns:
list: 处理后的文件列表每个文件对象包含upload_file_id和transfer_method字段
"""
if not files or not isinstance(files, list):
logger.warning("文件参数为空或格式不正确")
return files
processed_files = []
for file_obj in files:
# 创建文件对象的副本,避免修改原始数据
processed_file = file_obj.copy()
# 检查是否需要处理远程URL文件
if (processed_file.get("transfer_method") == "remote_url" and
processed_file.get("url")):
try:
logger.info(f"开始上传远程文件: {processed_file['url']}")
# 调用upload_file_remote_url方法下载文件并上传到Dify
upload_result = self.upload_file_remote_url(processed_file["url"])
# 将返回的对象的id存入upload_file_id字段
if upload_result and "id" in upload_result:
processed_file["upload_file_id"] = upload_result["id"]
# 修改transfer_method为local_file因为文件已经上传到Dify服务器
processed_file["transfer_method"] = "local_file"
# 移除url字段因为已经不需要了
processed_file.pop("url", None)
logger.info(f"文件上传成功 - ID: {upload_result['id']}, "
f"名称: {upload_result.get('name', 'N/A')}, "
f"大小: {upload_result.get('size', 'N/A')} bytes")
else:
logger.error(f"文件上传失败未获取到有效的文件ID响应: {upload_result}")
processed_file["upload_error"] = "未获取到有效的文件ID"
except Exception as e:
logger.error(f"文件上传过程中发生错误: {str(e)}", exc_info=True)
# 记录错误信息,但继续处理其他文件
processed_file["upload_error"] = str(e)
elif processed_file.get("transfer_method") == "local_file":
# 如果已经是local_file确保有upload_file_id
if not processed_file.get("upload_file_id"):
logger.warning("local_file类型的文件缺少upload_file_id字段")
processed_files.append(processed_file)
logger.info(f"文件预处理完成,共处理 {len(processed_files)} 个文件")
return processed_files
def stop_response(self, api_key, task_id, user="pp666"):
url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {"user": user}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
def get_app_info(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/info"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
response_map = response.json()
# 翻译工具名称
tool_name = response_map.get("name")
if tool_name:
# translated_name = TranslationService.translate_tool_name(tool_name)
translated_name = pinyin_to_camel(tool_name)
response_map["name"] = translated_name
# 翻译工具描述
# tool_description = response_map.get("description")
# if tool_description:
# translated_description = TranslationService.translate_tool_description(
# tool_description
# )
# response_map["description"] = (
# f"{tool_description} ({translated_description})"
# )
return response_map
def get_app_parameters(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/parameters"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
logger.info(f"调用 /parameters API: {url}")
logger.info(f"请求头: {headers}")
logger.info(f"请求参数: {params}")
response = requests.get(url, headers=headers, params=params)
logger.info(f"/parameters API 响应状态码: {response.status_code}")
response.raise_for_status()
response_data = response.json()
logger.info(f"/parameters API 响应数据: {response_data}")
return response_data
def get_app_meta(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/meta"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
if __name__ == "__main__":
dify_api = WorkflowDifyAPI(
"https://ops.lzwcai.com/v1",
["app-ZmLuBlRmViseUdOonqLyNSku", "app-AHjfp8k4nawQSJi0us8x3J5Q"],
)
dify_api.upload_file_remote_url("http://192.168.2.236:9000/lzwcai/upload/2025-07-29/34b28da03f3c43b0921ba1b76857bbc0/34b28da03f3c43b0921ba1b76857bbc0.JPG?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20250729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250729T075242Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=80f58d37c3bd52fb2b25efa36b5df0d73c8da7c773e7a7066dd6563710c619d6")

View File

@@ -1,85 +0,0 @@
# lzwcai-mcp-agile-db
数据库管理平台 MCP Server提供 33 个工具用于数据库管理、表操作、数据 CRUD、API 密钥管理、技能与工具管理等。
## 环境变量
| 变量名 | 必填 | 说明 |
|--------|------|------|
| `API_KEY` | 是 | 数据库管理平台的 API 密钥(格式: `Bearer <token>` |
| `backendBaseUrl` | 否 | 数据库管理平台后端地址(默认 `http://lzwcai-demp-corp-manager:8086` |
## 安装
```bash
pip install -e .
```
## 运行
```bash
# 设置环境变量
export API_KEY="Bearer your-token"
export backendBaseUrl="https://dempdemo.lzwcai.com" # 可选
# 运行 MCP Server
lzwcai-mcp-agile-db
```
## 工具列表
### 数据源管理
- `list_datasources` - 获取数据源列表
- `get_datasource_detail` - 获取数据源详情
- `create_datasource` - 创建数据源
- `update_datasource` - 更新数据源
- `toggle_datasource_status` - 启用/停用数据源
- `delete_datasource` - 删除数据源
### 数据库与表管理
- `list_databases` - 获取数据库列表
- `list_tables` - 获取表列表
- `get_table_detail` - 获取表详情
- `create_table` - 创建表
- `alter_table` - 修改表结构
- `generate_table_by_description` - 通过自然语言生成表结构
### 表数据 CRUD
- `query_table_data` - 查询表数据
- `insert_table_row` - 插入行数据
- `update_table_row` - 更新行数据
- `delete_table_rows` - 删除行数据
- `export_table_excel` - 导出 Excel
### API 密钥管理
- `list_api_keys` - 获取密钥列表
- `create_api_key` - 创建密钥
- `toggle_api_key_status` - 启用/禁用密钥
- `delete_api_key` - 删除密钥
- `get_api_key_permissions` - 查看密钥权限
- `grant_api_key_permissions` - 授予权限
### 技能与工具管理
- `get_skill_by_datasource` - 获取技能信息
- `get_skill_tools` - 获取技能工具列表
- `create_skill` - 创建技能
- `create_sql_tool` - 创建 SQL 工具
- `delete_skill_tool` - 删除技能工具
- `update_skill_config` - 更新技能配置
### 数据导入
- `preview_import_data` - 预览导入数据
- `confirm_import_data` - 确认导入数据
### 表订阅与 SQL 执行
- `toggle_table_subscription` - 切换表订阅
- `execute_sql` - 执行 SQL 查询
## 架构
- `tools/_base.py` - 工具注册装饰器和基类
- `tools/*.py` - 工具实现文件
- `utils/api_client.py` - 统一 HTTP 客户端
- `utils/env_config.py` - 环境变量配置
- `utils/logger_config.py` - 日志配置
- `server.py` - MCP Server 注册和启动逻辑

View File

@@ -1,5 +0,0 @@
"""lzwcai-mcp-agile-db MCP Server 包"""
from .server import main
__all__ = ["main"]

View File

@@ -1,202 +0,0 @@
2026-06-11 09:30:49 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
2026-06-11 09:30:49 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
2026-06-11 09:30:49 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-11 09:30:49 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:124] - ==================================================
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:125] - lzwcai-mcp-agile-db MCP Server 启动
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - 已注册工具数量: 33
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:131] - ==================================================
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:133] - 开始运行 MCP Server (stdio 模式)
2026-06-11 09:30:49 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-11 09:30:49 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
2026-06-11 09:30:50 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x0000025426747BF0>
2026-06-11 09:30:50 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2026-06-11 09:30:50 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
2026-06-11 09:30:50 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
2026-06-11 09:30:50 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-11 09:33:24 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
2026-06-11 09:33:24 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
2026-06-11 09:33:24 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-11 09:33:24 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:124] - ==================================================
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:125] - lzwcai-mcp-agile-db MCP Server 启动
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - 已注册工具数量: 33
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:131] - ==================================================
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:133] - 开始运行 MCP Server (stdio 模式)
2026-06-11 09:33:24 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-11 09:33:24 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
2026-06-11 09:33:25 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x0000020155C7D190>
2026-06-11 09:33:25 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2026-06-11 09:33:25 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
2026-06-11 09:33:25 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
2026-06-11 09:33:25 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:48] - [客户端初始化] base_url=https://dempdemo.lzwcai.com
2026-06-11 09:33:25 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 33 个工具
2026-06-11 09:33:25 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-11 09:33:45 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x0000020155844410>
2026-06-11 09:33:45 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
2026-06-11 09:33:45 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
2026-06-11 09:33:45 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_datasources
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:100] - [API请求] GET https://dempdemo.lzwcai.com/api/datasource/connection/list
2026-06-11 09:33:46 - httpx - DEBUG - [_config.py:82] - load_ssl_context verify=True cert=None trust_env=True http2=False
2026-06-11 09:33:46 - httpx - DEBUG - [_config.py:148] - load_verify_locations cafile='D:\\anaconda3\\Library\\ssl\\cacert.pem'
2026-06-11 09:33:46 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
2026-06-11 09:33:46 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x0000020155E9D8E0>
2026-06-11 09:33:46 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context=<ssl.SSLContext object at 0x0000020155E740D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
2026-06-11 09:33:46 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x0000020155D4CFE0>
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request=<Request [b'GET']>
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request=<Request [b'GET']>
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request=<Request [b'GET']>
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Thu, 11 Jun 2026 01:33:40 GMT'), (b'Content-Type', b'application/json;charset=utf-8'), (b'Content-Length', b'51'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
2026-06-11 09:33:46 - httpx - INFO - [_client.py:1038] - HTTP Request: GET https://dempdemo.lzwcai.com/api/datasource/connection/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request=<Request [b'GET']>
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:91] - [API错误] 登录过期,请重新登录
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.server - ERROR - [server.py:96] - 工具执行失败: list_datasources, 错误: 登录过期,请重新登录
Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\server.py", line 84, in handle_call_tool
result = await tool_instance.execute(arguments or {})
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\tools\datasources.py", line 30, in execute
return self.client.get("/api/datasource/connection/list", params=params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\utils\api_client.py", line 102, in get
return self._handle_response(response, url)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\utils\api_client.py", line 92, in _handle_response
raise Exception(error_msg)
Exception: 登录过期,请重新登录
2026-06-11 09:33:46 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-11 09:39:21 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
2026-06-11 09:39:21 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
2026-06-11 09:39:21 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-11 09:39:21 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-11 09:39:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:48] - [客户端初始化] base_url=https://dempdemo.lzwcai.com
2026-06-11 09:39:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:100] - [API请求] GET https://dempdemo.lzwcai.com/api/datasource/connection/list
2026-06-11 09:39:21 - httpx - DEBUG - [_config.py:82] - load_ssl_context verify=True cert=None trust_env=True http2=False
2026-06-11 09:39:21 - httpx - DEBUG - [_config.py:148] - load_verify_locations cafile='C:\\Users\\HiWin10\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\certifi\\cacert.pem'
2026-06-11 09:39:21 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
2026-06-11 09:39:21 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x0000027729C00710>
2026-06-11 09:39:21 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context=<ssl.SSLContext object at 0x0000027729DFE9D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
2026-06-11 09:39:21 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x0000027729C23170>
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request=<Request [b'GET']>
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request=<Request [b'GET']>
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request=<Request [b'GET']>
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Thu, 11 Jun 2026 01:39:15 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
2026-06-11 09:39:21 - httpx - INFO - [_client.py:1038] - HTTP Request: GET https://dempdemo.lzwcai.com/api/datasource/connection/list "HTTP/1.1 200 "
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request=<Request [b'GET']>
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete
2026-06-11 09:39:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200
2026-06-11 09:39:52 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
2026-06-11 09:39:52 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
2026-06-11 09:39:52 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-11 09:39:52 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:124] - ==================================================
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:125] - lzwcai-mcp-agile-db MCP Server 启动
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - 已注册工具数量: 33
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:131] - ==================================================
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:133] - 开始运行 MCP Server (stdio 模式)
2026-06-11 09:39:52 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-11 09:39:52 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
2026-06-11 09:39:53 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002B57D06C860>
2026-06-11 09:39:53 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2026-06-11 09:39:53 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
2026-06-11 09:39:53 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
2026-06-11 09:39:53 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:48] - [客户端初始化] base_url=https://dempdemo.lzwcai.com
2026-06-11 09:39:53 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 33 个工具
2026-06-11 09:39:53 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-11 09:40:08 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002B57D188A40>
2026-06-11 09:40:08 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
2026-06-11 09:40:08 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_datasources
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:100] - [API请求] GET https://dempdemo.lzwcai.com/api/datasource/connection/list
2026-06-11 09:40:08 - httpx - DEBUG - [_config.py:82] - load_ssl_context verify=True cert=None trust_env=True http2=False
2026-06-11 09:40:08 - httpx - DEBUG - [_config.py:148] - load_verify_locations cafile='D:\\anaconda3\\Library\\ssl\\cacert.pem'
2026-06-11 09:40:08 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
2026-06-11 09:40:08 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x000002B57D28C560>
2026-06-11 09:40:08 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context=<ssl.SSLContext object at 0x000002B57D2642D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
2026-06-11 09:40:08 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x000002B57CF80410>
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request=<Request [b'GET']>
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request=<Request [b'GET']>
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request=<Request [b'GET']>
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Thu, 11 Jun 2026 01:40:02 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
2026-06-11 09:40:08 - httpx - INFO - [_client.py:1038] - HTTP Request: GET https://dempdemo.lzwcai.com/api/datasource/connection/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request=<Request [b'GET']>
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_datasources
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
"total": 14,
"rows": [
{
"createBy": "",
"createTime": "2026-06-10 16:47:43",
"updateBy": "",
"updateTime": "2026-06-10 16:47:43",
"remark": "设备报价管理系统包含设备基础信息、预设方案模板、报价单主表和明细表四个核心数据对象,支持从设备参数管理到整套产线方案配置的完整报价流程,为销售部门提供标准化报价服务,实现快速方案生成和精准成本核算。",
"id": "58",
"enterpriseId": "1937166012193443842",
"deptId": "1171",
"userId": "292",
"host": "host.docker.internal",
"port": 5432,
"datasourceName": "HMD产品",
"database...
2026-06-11 09:40:08 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-11 09:40:55 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002B57D0617C0>
2026-06-11 09:40:55 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
2026-06-11 09:40:55 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
2026-06-11 09:40:55 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
2026-06-11 09:40:55 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:100] - [API请求] GET https://dempdemo.lzwcai.com/api/datasource/api_key/list
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - close.started
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - close.complete
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x000002B57D2623C0>
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context=<ssl.SSLContext object at 0x000002B57D2642D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x000002B57D2626F0>
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request=<Request [b'GET']>
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request=<Request [b'GET']>
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request=<Request [b'GET']>
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Thu, 11 Jun 2026 01:40:49 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
2026-06-11 09:40:56 - httpx - INFO - [_client.py:1038] - HTTP Request: GET https://dempdemo.lzwcai.com/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request=<Request [b'GET']>
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete
2026-06-11 09:40:56 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200
2026-06-11 09:40:56 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
2026-06-11 09:40:56 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
"total": 6,
"rows": [
{
"createBy": "",
"createTime": "2026-06-06 15:10:31",
"updateBy": "",
"updateTime": "2026-06-06 15:10:31",
"remark": null,
"id": "7",
"apiKey": "Lb8LgEJ7eBUU8QMifKUJvo9w6YLAotbKJ-w1DKU8ZrU",
"apiKeyName": "AWINBEXT",
"enterpriseId": "1937166012193443842",
"status": 0,
"expireTime": "2027-06-06T15:10:32.000+08:00"
},
{
"createBy": "",
"createTime": "2026-05-25 14:47:11",
"u...
2026-06-11 09:40:56 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent

View File

@@ -1,15 +0,0 @@
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:91] - [API错误] 登录过期,请重新登录
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.server - ERROR - [server.py:96] - 工具执行失败: list_datasources, 错误: 登录过期,请重新登录
Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\server.py", line 84, in handle_call_tool
result = await tool_instance.execute(arguments or {})
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\tools\datasources.py", line 30, in execute
return self.client.get("/api/datasource/connection/list", params=params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\utils\api_client.py", line 102, in get
return self._handle_response(response, url)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\utils\api_client.py", line 92, in _handle_response
raise Exception(error_msg)
Exception: 登录过期,请重新登录

View File

@@ -1,138 +0,0 @@
#!/usr/bin/env python3
"""
lzwcai-mcp-agile-db MCP Server
数据库管理平台 MCP 工具服务,提供 33 个工具用于数据库管理、表操作、API 密钥管理等
"""
import json
import logging
import anyio
import mcp.types as types
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
from .utils.logger_config import setup_system_logging, get_logger
from .utils.api_client import AgileDBAPIClient
from .tools._base import get_registered_tools
# 初始化日志系统
setup_system_logging(app_name="lzwcai_mcp_agile_db", log_level=logging.DEBUG)
logger = get_logger(__name__)
# 初始化 MCP Server
server = Server("lzwcai_mcp_agile_db")
# 全局 API 客户端
_api_client: AgileDBAPIClient = None
def get_api_client() -> AgileDBAPIClient:
"""获取或创建 API 客户端"""
global _api_client
if _api_client is None:
_api_client = AgileDBAPIClient()
return _api_client
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""列出所有可用工具"""
logger.info("收到 ListTools 请求")
tools = []
for tool_cls in get_registered_tools():
instance = tool_cls(get_api_client())
tool_def = instance.to_tool_def()
tools.append(
types.Tool(
name=tool_def["name"],
description=tool_def["description"],
inputSchema=tool_def["inputSchema"],
)
)
logger.info(f"ListTools 响应: 返回 {len(tools)} 个工具")
return tools
@server.call_tool()
async def handle_call_tool(
name: str,
arguments: dict | None,
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""调用工具"""
logger.info(f"收到 CallTool 请求: name={name}")
# 查找对应的工具类
tool_cls = None
for cls in get_registered_tools():
if cls.name == name:
tool_cls = cls
break
if tool_cls is None:
logger.error(f"未找到工具: {name}")
raise ValueError(f"未知工具: {name}")
# 创建工具实例并执行
client = get_api_client()
tool_instance = tool_cls(client)
try:
result = await tool_instance.execute(arguments or {})
logger.info(f"工具执行成功: {name}")
logger.debug(f"工具返回结果: {json.dumps(result, ensure_ascii=False, indent=2)[:500]}...")
return [
types.TextContent(
type="text",
text=json.dumps(result, ensure_ascii=False, indent=2),
)
]
except Exception as e:
logger.error(f"工具执行失败: {name}, 错误: {e}", exc_info=True)
return [
types.TextContent(
type="text",
text=json.dumps({"error": str(e), "tool_name": name}, ensure_ascii=False, indent=2),
)
]
async def run_server():
"""运行 MCP Server (stdio 模式)"""
async with stdio_server() as streams:
await server.run(
streams[0],
streams[1],
InitializationOptions(
server_name="lzwcai_mcp_agile_db",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
def main():
"""主入口"""
logger.info("=" * 50)
logger.info("lzwcai-mcp-agile-db MCP Server 启动")
# 导入所有工具模块(触发装饰器注册)
from . import tools # noqa: F401
logger.info(f"已注册工具数量: {len(get_registered_tools())}")
logger.info("=" * 50)
logger.info("开始运行 MCP Server (stdio 模式)")
anyio.run(run_server)
if __name__ == "__main__":
main()

View File

@@ -1,18 +0,0 @@
"""
工具自动发现模块
自动导入 tools/ 目录下所有工具模块,触发 @register_tool 装饰器注册
"""
import importlib
import pkgutil
from pathlib import Path
# 获取当前包路径
_package_path = Path(__file__).parent
# 遍历所有 Python 文件(排除 __init__.py 和 _base.py
for _, module_name, _ in pkgutil.iter_modules([str(_package_path)]):
if module_name.startswith("_"):
continue
# 动态导入模块,触发装饰器
importlib.import_module(f".{module_name}", __package__)

Some files were not shown because too many files have changed in this diff Show More