Compare commits
3 Commits
main
...
research-f
| Author | SHA1 | Date | |
|---|---|---|---|
| bf3274788b | |||
| d71458669a | |||
| 0d48341b73 |
@@ -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` 只包含要更新的字段,不需要提供全部字段
|
|
||||||
- 插入数据时,自增主键不需要提供
|
|
||||||
- 如果操作涉及多行,使用批量操作或循环调用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 场景 5:AI 生成表结构
|
|
||||||
|
|
||||||
当用户需要创建新表但不知道如何设计表结构时,使用 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.
@@ -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 根据自然语言生成表结构 | 安全(仅生成,不创建) |
|
|
||||||
|
|
||||||
### 三、表数据 CRUD(5 个工具)
|
|
||||||
|
|
||||||
| 工具 | 功能 | 危险等级 |
|
|
||||||
|------|------|----------|
|
|
||||||
| `query_table_data` | 查询表数据(分页) | 安全 |
|
|
||||||
| `insert_table_row` | 插入一行数据 | 中等 |
|
|
||||||
| `update_table_row` | 更新一行数据 | 中等 |
|
|
||||||
| `delete_table_rows` | 删除数据行(按主键) | **危险** |
|
|
||||||
| `export_table_excel` | 导出表数据为 Excel(base64) | 安全 |
|
|
||||||
|
|
||||||
### 四、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` 环境
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 场景 5:AI 生成表结构
|
|
||||||
|
|
||||||
当用户需要创建新表但不知道如何设计表结构时,使用 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 编码的文件内容需要提供文件名
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 场景 7:API 密钥管理
|
|
||||||
|
|
||||||
当用户需要管理 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.
@@ -1,356 +0,0 @@
|
|||||||
---
|
|
||||||
name: mcp-tool-testing
|
|
||||||
description: MCP 工具通用测试技能。自动发现、执行和组合测试任何 MCP 服务器中的工具,支持场景自动创造和风险前置询问。
|
|
||||||
version: 0.1.0
|
|
||||||
---
|
|
||||||
|
|
||||||
# MCP Tool Testing Skill
|
|
||||||
|
|
||||||
这是一个通用的 MCP(Model Context Protocol)工具测试技能。适用于任何 MCP 环境,无论是自研的还是第三方的。
|
|
||||||
|
|
||||||
## 核心原则
|
|
||||||
|
|
||||||
1. **通用性**:不假设任何具体的服务器名称、工具名称或项目结构
|
|
||||||
2. **自动发现**:通过 MCP 协议本身获取当前环境中可用的所有工具
|
|
||||||
3. **智能执行**:能判断的入参自动执行,不能判断的询问用户
|
|
||||||
4. **场景自动创造**:根据工具自动匹配并创造测试场景,无需用户指定
|
|
||||||
5. **风险前置**:有风险的操作必须询问用户确认,绝不擅作主张
|
|
||||||
6. **结果汇总**:最终只返回执行工具的入参、出参和理解
|
|
||||||
|
|
||||||
## 执行流程
|
|
||||||
|
|
||||||
### 第一步:发现可用工具
|
|
||||||
|
|
||||||
通过 MCP 的 `tools/list` 方法获取当前环境中所有可用的工具。
|
|
||||||
|
|
||||||
**获取每个工具的信息:**
|
|
||||||
- 工具名称(name)
|
|
||||||
- 工具描述(description)
|
|
||||||
- 入参 schema(inputSchema):
|
|
||||||
- 参数名称
|
|
||||||
- 参数类型(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. **五类风险**:删除、泄密、政治、似黄、生产环境影响——必须询问用户
|
|
||||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,2 +0,0 @@
|
|||||||
{
|
|
||||||
}
|
|
||||||
68
README.md
68
README.md
@@ -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
|
|
||||||
BIN
doct/模板.docx
Normal file
BIN
doct/模板.docx
Normal file
Binary file not shown.
BIN
doct/模板.pdf
Normal file
BIN
doct/模板.pdf
Normal file
Binary file not shown.
BIN
doct/首页和尾页.docx
Normal file
BIN
doct/首页和尾页.docx
Normal file
Binary file not shown.
BIN
doct/首页和尾页.pdf
Normal file
BIN
doct/首页和尾页.pdf
Normal file
Binary file not shown.
@@ -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
|
|
||||||
@@ -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/)
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
[console_scripts]
|
|
||||||
lzwcai-demp-tool-server-dify-to-mcp = src.create_mcp:run_main
|
|
||||||
@@ -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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
src
|
|
||||||
@@ -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()
|
|
||||||
@@ -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"]
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
[egg_info]
|
|
||||||
tag_build =
|
|
||||||
tag_date = 0
|
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
|
||||||
Binary file not shown.
@@ -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)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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}")
|
|
||||||
Binary file not shown.
@@ -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,
|
|
||||||
)
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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()
|
|
||||||
@@ -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}
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
@@ -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) |
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# 此文件用于确保 logs 目录被 Git 跟踪
|
|
||||||
# 日志文件会自动生成在此目录中
|
|
||||||
@@ -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'}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
[console_scripts]
|
|
||||||
lzwcai-demp-tool-server-dify-to-mcp-test = src.create_mcp:run_main
|
|
||||||
@@ -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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
src
|
|
||||||
@@ -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()
|
|
||||||
@@ -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"]
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
[egg_info]
|
|
||||||
tag_build =
|
|
||||||
tag_date = 0
|
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
|
||||||
Binary file not shown.
@@ -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)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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'
|
|
||||||
Binary file not shown.
@@ -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,
|
|
||||||
)
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
|
||||||
@@ -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("日志配置测试完成!")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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}
|
|
||||||
@@ -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() # 取消注释并配置后运行
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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")
|
|
||||||
@@ -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 注册和启动逻辑
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.12
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
"""lzwcai-mcp-agile-db MCP Server 包"""
|
|
||||||
|
|
||||||
from .server import main
|
|
||||||
|
|
||||||
__all__ = ["main"]
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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
|
|
||||||
@@ -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: 登录过期,请重新登录
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
Reference in New Issue
Block a user