Compare commits
2 Commits
research-f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 635313a7ab | |||
| ba5cd4bbe1 |
451
.kilo/plans/lzwcai-agile-db-skill.md
Normal file
451
.kilo/plans/lzwcai-agile-db-skill.md
Normal file
@@ -0,0 +1,451 @@
|
||||
# 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 自动识别和加载。
|
||||
BIN
.kilo/skills/lzwcai-agile-db.zip
Normal file
BIN
.kilo/skills/lzwcai-agile-db.zip
Normal file
Binary file not shown.
1219
.kilo/skills/lzwcai-agile-db/SKILL.md
Normal file
1219
.kilo/skills/lzwcai-agile-db/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
356
.kilo/skills/mcp-tool-testing/SKILL.md
Normal file
356
.kilo/skills/mcp-tool-testing/SKILL.md
Normal file
@@ -0,0 +1,356 @@
|
||||
---
|
||||
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. **五类风险**:删除、泄密、政治、似黄、生产环境影响——必须询问用户
|
||||
343
.kilo/skills/数字员工平台数据库技能的.md
Normal file
343
.kilo/skills/数字员工平台数据库技能的.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# 数字员工平台数据库技能配置指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档介绍两种使用数字员工平台数据库技能的方法:
|
||||
- **方法一**:通过数字员工对话直接使用
|
||||
- **方法二**:通过 AI 编辑器的 Skills + MCP 搭配使用
|
||||
|
||||
---
|
||||
|
||||
## 方法一:通过数字员工对话使用
|
||||
|
||||
### 配置说明
|
||||
|
||||
此方法通过配置 MCP Server 来连接数字员工平台。
|
||||
|
||||
### MCP 配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"lzwcai-mcp-agile-db": {
|
||||
"command": "uvx",
|
||||
"type": "stdio",
|
||||
"args": [
|
||||
"lzwcai-mcp-agile-db"
|
||||
],
|
||||
"timeout": 600,
|
||||
"env": {
|
||||
"API_KEY": "eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjljMDllMjZhLWFkNzgtNGNmMi05YzQ5LWQzY2Y5NjI5MGRjYyJ9.v5ZffkvM2CnMkoWqc-Xy-0gN2oLBjyfJlm_YnCE6TWBwfKVmpuakoRXFMTiHQG8LOphv7JlEmZHcYpzwM52D0Q"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 配置中的 `API_KEY` 是数字员工平台的密钥
|
||||
- 存在单点登录问题,请使用不会过期的账号密钥
|
||||
- Skills 配置参考:`lzwcai-agile-db`
|
||||
|
||||
---
|
||||
|
||||
## 方法二:通过 AI 编辑器使用
|
||||
|
||||
### 配置说明
|
||||
|
||||
此方法通过下载 Skills 和 MCP 配置,在 AI 编辑器中搭配使用,支持直接对话操作。
|
||||
|
||||
### MCP 配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"lzwcai_mcp_agile_db": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"lzwcai-mcp-agile-db"
|
||||
],
|
||||
"env": {
|
||||
"UV_INDEX_URL": "http://192.168.2.236:3141/lzwc/dev/+simple/",
|
||||
"API_KEY": "eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjljMDllMjZhLWFkNzgtNGNmMi05YzQ5LWQzY2Y5NjI5MGRjYyJ9.v5ZffkvM2CnMkoWqc-Xy-0gN2oLBjyfJlm_YnCE6TWBwfKVmpuakoRXFMTiHQG8LOphv7JlEmZHcYpzwM52D0Q",
|
||||
"backendBaseUrl": "http://192.168.2.236:8088"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 环境变量说明
|
||||
|
||||
| 变量名 | 说明 | 示例值 |
|
||||
|--------|------|--------|
|
||||
| `UV_INDEX_URL` | Python 包索引地址 | `http://192.168.2.236:3141/lzwc/dev/+simple/` |
|
||||
| `API_KEY` | 数字员工平台 API 密钥 | (见配置示例) |
|
||||
| `backendBaseUrl` | 后端服务基础地址 | `http://192.168.2.236:8088` |
|
||||
|
||||
---
|
||||
|
||||
## 使用建议
|
||||
|
||||
1. **选择方法**:根据实际使用场景选择合适的方法
|
||||
- 数字员工对话:适合快速使用
|
||||
- AI 编辑器:适合更灵活的配置和使用
|
||||
|
||||
2. **密钥管理**:妥善保管 `API_KEY`,避免泄露
|
||||
|
||||
3. **网络配置**:确保网络可以访问配置的后端地址
|
||||
|
||||
---
|
||||
|
||||
## 相关资源
|
||||
|
||||
- Skills 目录:`lzwcai-agile-db/`
|
||||
- 详细技能文档:`lzwcai-agile-db/SKILL.md`
|
||||
|
||||
---
|
||||
|
||||
## 对话使用场景示例
|
||||
|
||||
以下是配置好技能后,您可以直接使用的对话示例:
|
||||
|
||||
### 📊 场景 1:浏览数据源和表
|
||||
|
||||
**用户:** "帮我看看有哪些数据源"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
共找到 3 个数据源:
|
||||
1. HMD产品(PostgreSQL, 运行中)
|
||||
2. 测试数据库(MySQL, 运行中)
|
||||
3. 历史数据(Oracle, 已停止)
|
||||
|
||||
请告诉我您想看哪个数据源?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**用户:** "看看HMD产品有哪些表"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
HMD产品 数据源包含以下数据库和表:
|
||||
|
||||
order_db:
|
||||
- orders (订单表, 15 个字段, 2568 条记录)
|
||||
- users (用户表, 8 个字段, 156 条记录)
|
||||
- products (商品表, 12 个字段, 892 条记录)
|
||||
|
||||
analytics_db:
|
||||
- daily_stats (日报表, 10 个字段, 365 条记录)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔍 场景 2:查询表数据
|
||||
|
||||
**用户:** "查一下users表前10条数据"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
users 表共 156 条记录,当前显示第 1-10 条:
|
||||
|
||||
| id | username | email | status | created_at |
|
||||
|----|----------|-----------------|--------|---------------------|
|
||||
| 1 | admin | admin@test.com | active | 2024-01-01 00:00:00 |
|
||||
| 2 | user1 | user1@test.com | active | 2024-01-02 10:30:00 |
|
||||
| 3 | user2 | user2@test.com | inactive| 2024-01-03 15:45:00 |
|
||||
...
|
||||
|
||||
注:默认查询测试环境,如需查询生产环境请说明。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📝 场景 3:执行 SQL 查询
|
||||
|
||||
**用户:** "统计每个地区的订单数量"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
各地区订单统计:
|
||||
|
||||
| 地区 | 订单数 |
|
||||
|------|--------|
|
||||
| 华东 | 1,250 |
|
||||
| 华南 | 980 |
|
||||
| 华北 | 756 |
|
||||
| 西南 | 432 |
|
||||
| 东北 | 289 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ➕ 场景 4:插入数据
|
||||
|
||||
**用户:** "新增一个用户,用户名是test_user,邮箱test@test.com"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
即将向 users 表插入以下数据:
|
||||
username = test_user
|
||||
email = test@test.com
|
||||
(id 自动生成)
|
||||
|
||||
确认插入吗?
|
||||
```
|
||||
|
||||
**用户:** "确认"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
✅ 已成功插入用户 test_user (ID: 157)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✏️ 场景 5:更新数据
|
||||
|
||||
**用户:** "把ID为5的用户邮箱改成new@test.com"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
即将更新 users 表 ID=5 的记录:
|
||||
email: old@test.com → new@test.com
|
||||
|
||||
确认更新吗?
|
||||
```
|
||||
|
||||
**用户:** "确认"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
✅ 已更新用户 ID=5 的邮箱为 new@test.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🗑️ 场景 6:删除数据
|
||||
|
||||
**用户:** "删除ID为10的订单"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
⚠️ 安全提醒:此操作存在删除风险。
|
||||
具体说明:数据将永久删除,不可恢复。
|
||||
|
||||
确认要删除以下记录吗?
|
||||
订单 ID=10, 订单号=ORD-2024-0010, 金额=¥1,250.00
|
||||
|
||||
请回复"确认删除"继续。
|
||||
```
|
||||
|
||||
**用户:** "确认删除"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
✅ 已成功删除订单 ID=10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🤖 场景 7:AI 生成表结构
|
||||
|
||||
**用户:** "帮我设计一个商品表,包含名称、价格、库存、分类"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
AI 已生成表结构设计:
|
||||
|
||||
表名: products (商品表)
|
||||
字段:
|
||||
- id (SERIAL, 主键) - 主键ID
|
||||
- name (VARCHAR(200), 非空) - 商品名称
|
||||
- price (DECIMAL(10,2)) - 价格
|
||||
- stock (INTEGER) - 库存数量
|
||||
- category (VARCHAR(50)) - 分类
|
||||
- created_at (TIMESTAMP) - 创建时间
|
||||
|
||||
是否需要在此基础上修改或直接创建?
|
||||
```
|
||||
|
||||
**用户:** "直接创建到order_db数据库"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
✅ 已成功创建表 products (商品表)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📥 场景 8:导入 Excel 数据
|
||||
|
||||
**用户:** "帮我导入这个Excel文件:https://example.com/data.xlsx"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
AI 已识别 Excel 文件内容:
|
||||
|
||||
表名: customers (客户表)
|
||||
字段:
|
||||
- id (SERIAL, 主键)
|
||||
- name (VARCHAR(100)) - 客户名称
|
||||
- phone (VARCHAR(20)) - 电话
|
||||
- address (TEXT) - 地址
|
||||
|
||||
预览数据(前3条):
|
||||
| id | name | phone | address |
|
||||
|----|--------|------------|----------------|
|
||||
| 1 | 张三 | 13800138000| 北京市朝阳区 |
|
||||
| 2 | 李四 | 13900139000| 上海市浦东新区 |
|
||||
| 3 | 王五 | 13700137000| 广州市天河区 |
|
||||
|
||||
共 50 条数据待导入。
|
||||
|
||||
⚠️ 安全提醒:此操作可能涉及数据安全风险。
|
||||
说明:导入的数据将写入数据库,请确认数据来源合法合规。
|
||||
请确认是否继续?
|
||||
```
|
||||
|
||||
**用户:** "继续"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
✅ 成功导入 50 条数据到 customers 表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔑 场景 9:API 密钥管理
|
||||
|
||||
**用户:** "帮我创建一个新的API密钥"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
请输入密钥名称:
|
||||
```
|
||||
|
||||
**用户:** "测试密钥"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
✅ 已创建 API 密钥:
|
||||
名称: 测试密钥
|
||||
密钥: lzwc_sk_xxxxxxxxxxxx
|
||||
状态: 启用
|
||||
过期时间: 2025-06-18
|
||||
|
||||
请妥善保管密钥,丢失后无法找回。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 重要使用提示
|
||||
|
||||
1. **环境选择:** 默认使用测试环境(`test`),操作生产环境需要明确说明"查询生产环境"或"操作生产环境"
|
||||
|
||||
2. **安全确认:** 所有写操作(增删改)都会先预览并等待您的确认
|
||||
|
||||
3. **多选原则:** 有多个数据源/数据库/表可选时,AI会列出让您选择,不会擅自做主
|
||||
|
||||
4. **分步执行:** 复杂任务会分步完成,每一步都会确认后再继续
|
||||
2
.vscode/settings.json
vendored
Normal file
2
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
68
README.md
Normal file
68
README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 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
BIN
doct/模板.docx
Binary file not shown.
BIN
doct/模板.pdf
BIN
doct/模板.pdf
Binary file not shown.
BIN
doct/首页和尾页.pdf
BIN
doct/首页和尾页.pdf
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
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
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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/)
|
||||
@@ -0,0 +1,12 @@
|
||||
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
|
||||
@@ -0,0 +1,23 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[console_scripts]
|
||||
lzwcai-demp-tool-server-dify-to-mcp = src.create_mcp:run_main
|
||||
@@ -0,0 +1,7 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
src
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,32 @@
|
||||
[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"]
|
||||
@@ -0,0 +1,4 @@
|
||||
[egg_info]
|
||||
tag_build =
|
||||
tag_date = 0
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
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.
@@ -0,0 +1,212 @@
|
||||
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)),
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,104 @@
|
||||
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}")
|
||||
@@ -0,0 +1,172 @@
|
||||
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()
|
||||
@@ -0,0 +1,318 @@
|
||||
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()
|
||||
@@ -0,0 +1,373 @@
|
||||
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.
@@ -0,0 +1,53 @@
|
||||
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.
@@ -0,0 +1,153 @@
|
||||
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()
|
||||
@@ -0,0 +1,64 @@
|
||||
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.
@@ -0,0 +1,175 @@
|
||||
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()
|
||||
@@ -0,0 +1,12 @@
|
||||
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
|
||||
@@ -0,0 +1,267 @@
|
||||
# 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) |
|
||||
@@ -0,0 +1,2 @@
|
||||
# 此文件用于确保 logs 目录被 Git 跟踪
|
||||
# 日志文件会自动生成在此目录中
|
||||
@@ -0,0 +1,156 @@
|
||||
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'}
|
||||
@@ -0,0 +1,12 @@
|
||||
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
|
||||
@@ -0,0 +1,27 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[console_scripts]
|
||||
lzwcai-demp-tool-server-dify-to-mcp-test = src.create_mcp:run_main
|
||||
@@ -0,0 +1,7 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
src
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,32 @@
|
||||
[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"]
|
||||
@@ -0,0 +1,4 @@
|
||||
[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.
@@ -0,0 +1,7 @@
|
||||
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.
@@ -0,0 +1,203 @@
|
||||
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)),
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,104 @@
|
||||
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}")
|
||||
@@ -0,0 +1,213 @@
|
||||
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()
|
||||
@@ -0,0 +1,253 @@
|
||||
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()
|
||||
@@ -0,0 +1,320 @@
|
||||
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()
|
||||
@@ -0,0 +1,342 @@
|
||||
"""
|
||||
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.
@@ -0,0 +1,53 @@
|
||||
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.
@@ -0,0 +1,400 @@
|
||||
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,552 @@
|
||||
"""
|
||||
统一日志配置模块
|
||||
|
||||
这个模块提供了整个项目的统一日志配置和管理功能,确保所有组件使用一致的日志格式和输出方式。
|
||||
|
||||
主要功能:
|
||||
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("日志配置测试完成!")
|
||||
@@ -0,0 +1,153 @@
|
||||
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()
|
||||
@@ -0,0 +1,64 @@
|
||||
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}
|
||||
@@ -0,0 +1,461 @@
|
||||
"""
|
||||
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.
@@ -0,0 +1,352 @@
|
||||
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")
|
||||
86
lzwcai_mcp_agile_db/README.md
Normal file
86
lzwcai_mcp_agile_db/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# lzwcai-mcp-agile-db
|
||||
|
||||
数据库管理平台 MCP Server,提供 34 个工具用于数据库管理、表操作、数据 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` - 授予权限(仅追加,不可撤销)
|
||||
|
||||
### 技能与工具管理
|
||||
- `add_sql_tool_to_datasource` - 把 SQL 沉淀为工具(一步到位,自动建技能+配模板+建工具,推荐入口)
|
||||
- `get_skill_by_datasource` - 获取技能信息
|
||||
- `get_skill_tools` - 获取技能工具列表
|
||||
- `create_sql_tool` - 创建 SQL 工具(需技能已存在)
|
||||
- `delete_skill_tool` - 删除技能工具
|
||||
- `update_skill_config` - 更新技能配置
|
||||
- `update_skill_tool` - 修改技能工具
|
||||
|
||||
### 数据导入
|
||||
- `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
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/.python-version
Normal file
1
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12
|
||||
0
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/README.md
Normal file
0
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/README.md
Normal file
5
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__init__.py
Normal file
5
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""lzwcai-mcp-agile-db MCP Server 包"""
|
||||
|
||||
from .server import main
|
||||
|
||||
__all__ = ["main"]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,377 @@
|
||||
2026-06-22 23:02:48 - 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-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-22 23:02:48 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
2026-06-22 23:02:48 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:73] - [客户端初始化] base_url=https://dempdemo.lzwcai.com/api, 认证方式=account:demp04
|
||||
2026-06-22 23:02:48 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:118] - [登录] POST https://dempdemo.lzwcai.com/api/login, username=demp04, loginType=user
|
||||
2026-06-22 23:02:48 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x0000020F12F4F5C0>
|
||||
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - start_tls.started ssl_context=<ssl.SSLContext object at 0x0000020F12F165D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
|
||||
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - start_tls.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x0000020F12DFD970>
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Mon, 22 Jun 2026 15:02:58 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (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'SAMEORIGIN'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
|
||||
2026-06-22 23:02:49 - httpx - INFO - [_client.py:1740] - HTTP Request: POST https://dempdemo.lzwcai.com/api/login "HTTP/1.1 200 "
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-22 23:02:49 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:161] - [API响应] HTTP 200
|
||||
2026-06-22 23:02:49 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:137] - [登录] 成功获取 token
|
||||
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||
2026-06-23 09:47:20 - 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-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
|
||||
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
|
||||
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
|
||||
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
|
||||
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
|
||||
2026-06-23 09:47:20 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
||||
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001FB167DF860>
|
||||
2026-06-23 09:47:21 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
||||
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
||||
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
||||
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8088, 认证方式=account:yy8z9
|
||||
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
|
||||
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001FB156EB260>
|
||||
2026-06-23 09:47:28 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8088/login, username=yy8z9, loginType=user
|
||||
2026-06-23 09:47:28 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8088 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-23 09:47:28 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000001FB16D14260>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(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'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Date', b'Tue, 23 Jun 2026 01:47:27 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')])
|
||||
2026-06-23 09:47:28 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8088/login "HTTP/1.1 200 "
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:216] - [API请求] GET http://192.168.2.236:8088/datasource/api_key/list
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(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'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Date', b'Tue, 23 Jun 2026 01:47:27 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')])
|
||||
2026-06-23 09:47:28 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8088/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||
"total": 21,
|
||||
"rows": [
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 11:29:46",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-06-18 11:59:25",
|
||||
"remark": null,
|
||||
"id": "37",
|
||||
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
|
||||
"apiKeyName": "盒马超市只读访问密钥",
|
||||
"enterpriseId": "1932095424144715777",
|
||||
"status": 0,
|
||||
"expireTime": "2027-06-18T11:59:25.000+08:00"
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 10:54:40",
|
||||
...
|
||||
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 09:47:33 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||
2026-06-23 09:47:33 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||
2026-06-23 09:48:33 - 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-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
|
||||
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
|
||||
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
|
||||
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
|
||||
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
|
||||
2026-06-23 09:48:33 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
||||
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001F89A81AAB0>
|
||||
2026-06-23 09:48:34 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
||||
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
||||
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
||||
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8082/api, 认证方式=account:yy8z9
|
||||
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
|
||||
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001F89A273B00>
|
||||
2026-06-23 09:48:36 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
|
||||
2026-06-23 09:48:36 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-23 09:48:36 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000001F89C2B8F20>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 01:48:35 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (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'Content-Encoding', b'gzip')])
|
||||
2026-06-23 09:48:36 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:216] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 01:48:35 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (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'Content-Encoding', b'gzip')])
|
||||
2026-06-23 09:48:36 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||
"total": 21,
|
||||
"rows": [
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 11:29:46",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-06-18 11:59:25",
|
||||
"remark": null,
|
||||
"id": "37",
|
||||
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
|
||||
"apiKeyName": "盒马超市只读访问密钥",
|
||||
"enterpriseId": "1932095424144715777",
|
||||
"status": 0,
|
||||
"expireTime": "2027-06-18T11:59:25.000+08:00"
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 10:54:40",
|
||||
...
|
||||
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 09:48:39 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||
2026-06-23 09:48:39 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||
2026-06-23 11:11:10 - 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-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
|
||||
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
|
||||
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
|
||||
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
|
||||
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
|
||||
2026-06-23 11:11:10 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
||||
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCAFE606E0>
|
||||
2026-06-23 11:11:11 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
||||
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
||||
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
||||
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8082/api, 认证方式=account:yy8z9
|
||||
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
|
||||
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 11:11:14 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCB0FBFB60>
|
||||
2026-06-23 11:11:14 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||
2026-06-23 11:11:14 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||
2026-06-23 11:11:14 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
||||
2026-06-23 11:11:14 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
|
||||
2026-06-23 11:11:14 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-23 11:11:14 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000002BCB15974D0>
|
||||
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:13 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (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'Content-Encoding', b'gzip')])
|
||||
2026-06-23 11:11:15 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
|
||||
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
|
||||
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:232] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:13 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (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'Content-Encoding', b'gzip')])
|
||||
2026-06-23 11:11:15 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
|
||||
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
||||
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||
"total": 21,
|
||||
"rows": [
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 11:29:46",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-06-18 11:59:25",
|
||||
"remark": null,
|
||||
"id": "37",
|
||||
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
|
||||
"apiKeyName": "盒马超市只读访问密钥",
|
||||
"enterpriseId": "1932095424144715777",
|
||||
"status": 0,
|
||||
"expireTime": "2027-06-18T11:59:25.000+08:00"
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 10:54:40",
|
||||
...
|
||||
2026-06-23 11:11:15 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCB0EA5D60>
|
||||
2026-06-23 11:11:27 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:232] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
|
||||
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000002BCB100F1D0>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 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')])
|
||||
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:239] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (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'Content-Encoding', b'gzip')])
|
||||
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (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'Content-Encoding', b'gzip')])
|
||||
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||
"total": 21,
|
||||
"rows": [
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 11:29:46",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-06-18 11:59:25",
|
||||
"remark": null,
|
||||
"id": "37",
|
||||
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
|
||||
"apiKeyName": "盒马超市只读访问密钥",
|
||||
"enterpriseId": "1932095424144715777",
|
||||
"status": 0,
|
||||
"expireTime": "2027-06-18T11:59:25.000+08:00"
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 10:54:40",
|
||||
...
|
||||
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 11:13:47 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||
2026-06-23 11:13:47 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||
2026-06-23 11:37:08 - 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-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-23 11:37:08 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:314] - [认证] 重新登录后仍返回 401
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
@@ -0,0 +1 @@
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:314] - [认证] 重新登录后仍返回 401
|
||||
143
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/server.py
Normal file
143
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/server.py
Normal file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
lzwcai-mcp-agile-db MCP Server
|
||||
数据库管理平台 MCP 工具服务,提供数据库管理、表/数据操作、技能、API 密钥、MQTT 同步、AI 训练等工具
|
||||
"""
|
||||
|
||||
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 模式)"""
|
||||
try:
|
||||
async with stdio_server() as streams:
|
||||
await server.run(
|
||||
streams[0],
|
||||
streams[1],
|
||||
InitializationOptions(
|
||||
server_name="lzwcai_mcp_agile_db",
|
||||
server_version="0.1.8",
|
||||
capabilities=server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
),
|
||||
),
|
||||
)
|
||||
finally:
|
||||
# 释放全局 HTTP 客户端
|
||||
if _api_client is not None:
|
||||
await _api_client.close()
|
||||
|
||||
|
||||
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()
|
||||
18
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__init__.py
Normal file
18
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
工具自动发现模块
|
||||
自动导入 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__)
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user