fr-cli 2.2.5__tar.gz → 2.2.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fr_cli-2.2.5/fr_cli.egg-info → fr_cli-2.2.6}/PKG-INFO +6 -6
- {fr_cli-2.2.5 → fr_cli-2.2.6}/README.md +3 -3
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/README.md +11 -4
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/__init__.py +1 -1
- {fr_cli-2.2.5 → fr_cli-2.2.6/fr_cli.egg-info}/PKG-INFO +6 -6
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli.egg-info/SOURCES.txt +1 -14
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli.egg-info/requires.txt +2 -2
- {fr_cli-2.2.5 → fr_cli-2.2.6}/pyproject.toml +3 -3
- fr_cli-2.2.5/tests/test_agent_client.py +0 -199
- fr_cli-2.2.5/tests/test_agent_server.py +0 -199
- fr_cli-2.2.5/tests/test_ai_save_file_with_verify.py +0 -228
- fr_cli-2.2.5/tests/test_all.py +0 -917
- fr_cli-2.2.5/tests/test_auto_session.py +0 -116
- fr_cli-2.2.5/tests/test_builtins.py +0 -67
- fr_cli-2.2.5/tests/test_dataframe.py +0 -42
- fr_cli-2.2.5/tests/test_gatekeeper.py +0 -118
- fr_cli-2.2.5/tests/test_integration.py +0 -224
- fr_cli-2.2.5/tests/test_intent_classification.py +0 -268
- fr_cli-2.2.5/tests/test_launcher.py +0 -98
- fr_cli-2.2.5/tests/test_master_agent.py +0 -162
- fr_cli-2.2.5/tests/test_structured_tools.py +0 -245
- {fr_cli-2.2.5 → fr_cli-2.2.6}/LICENSE +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/MANIFEST.in +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/WEAPON.MD +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/addon/plugin.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/__init__.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/__init__.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/_utils.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/db.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/local.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/rag.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/rag_watcher_daemon.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/remote.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/spider.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/client.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/executor.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/generator.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/manager.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/master.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/master_prompt.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/remote.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/server.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/workflow.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/breakthrough/update.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/command/__init__.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/command/executor.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/command/registry.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/command/security.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/conf/config.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/conf/wizard.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/chat.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/core.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/intent.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/llm.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/recommender.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/stream.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/sysmon.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/thinking.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/gatekeeper/__init__.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/gatekeeper/daemon.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/gatekeeper/manager.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/lang/i18n.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/main.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/memory/context.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/memory/history.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/memory/session.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/repl/__init__.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/repl/commands.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/security/security.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/ui/ui.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/cron.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/dataframe.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/disk.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/fs.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/launcher.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/loader.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/mail.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/mcp.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/vision.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/web.py +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli.egg-info/dependency_links.txt +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli.egg-info/entry_points.txt +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli.egg-info/top_level.txt +0 -0
- {fr_cli-2.2.5 → fr_cli-2.2.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fr-cli
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.6
|
|
4
4
|
Summary: 凡人打字机 - 支持多模型(Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark)的终极全能终端工具
|
|
5
5
|
Author: FANREN CLI Author
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,7 +37,7 @@ Requires-Dist: pyodbc>=4.0.0; extra == "db"
|
|
|
37
37
|
Requires-Dist: oracledb>=1.3.0; extra == "db"
|
|
38
38
|
Provides-Extra: rag
|
|
39
39
|
Requires-Dist: chromadb>=0.4.0; extra == "rag"
|
|
40
|
-
Requires-Dist: sentence-transformers>=2.2.
|
|
40
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == "rag"
|
|
41
41
|
Provides-Extra: remote
|
|
42
42
|
Requires-Dist: paramiko>=3.0.0; extra == "remote"
|
|
43
43
|
Provides-Extra: spider
|
|
@@ -56,7 +56,7 @@ Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
|
56
56
|
Requires-Dist: pyodbc>=4.0.0; extra == "all"
|
|
57
57
|
Requires-Dist: oracledb>=1.3.0; extra == "all"
|
|
58
58
|
Requires-Dist: chromadb>=0.4.0; extra == "all"
|
|
59
|
-
Requires-Dist: sentence-transformers>=2.2.
|
|
59
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == "all"
|
|
60
60
|
Requires-Dist: paramiko>=3.0.0; extra == "all"
|
|
61
61
|
Requires-Dist: selenium>=4.10.0; extra == "all"
|
|
62
62
|
Requires-Dist: bypy; extra == "all"
|
|
@@ -71,11 +71,11 @@ Dynamic: license-file
|
|
|
71
71
|
|
|
72
72
|
# 凡人打字机 (fr-cli)
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
|
|
75
75
|
|
|
76
76
|
## ✨ 功能特性
|
|
77
77
|
|
|
78
|
-
- 🤖 **AI
|
|
78
|
+
- 🤖 **AI 对话**:支持多模型(智谱 GLM / DeepSeek / Kimi / 通义千问 / StepFun / MiniMax / 讯飞星火)
|
|
79
79
|
- 🧠 **MasterAgent 主控**:自我进化的 ReAct 主控 Agent,自动规划、调用工具、反思进化
|
|
80
80
|
- 🧩 **思维模式**:direct / CoT / ToT / ReAct 四种推理模式切换
|
|
81
81
|
- 📁 **文件沙盒**:安全的虚拟文件系统,支持读写/目录操作
|
|
@@ -105,7 +105,7 @@ pip install fr-cli
|
|
|
105
105
|
fr-cli
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
首次运行会引导输入当前道统的 API Key。
|
|
109
109
|
|
|
110
110
|
## 📝 使用示例
|
|
111
111
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# 凡人打字机 (fr-cli)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
|
|
4
4
|
|
|
5
5
|
## ✨ 功能特性
|
|
6
6
|
|
|
7
|
-
- 🤖 **AI
|
|
7
|
+
- 🤖 **AI 对话**:支持多模型(智谱 GLM / DeepSeek / Kimi / 通义千问 / StepFun / MiniMax / 讯飞星火)
|
|
8
8
|
- 🧠 **MasterAgent 主控**:自我进化的 ReAct 主控 Agent,自动规划、调用工具、反思进化
|
|
9
9
|
- 🧩 **思维模式**:direct / CoT / ToT / ReAct 四种推理模式切换
|
|
10
10
|
- 📁 **文件沙盒**:安全的虚拟文件系统,支持读写/目录操作
|
|
@@ -34,7 +34,7 @@ pip install fr-cli
|
|
|
34
34
|
fr-cli
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
首次运行会引导输入当前道统的 API Key。
|
|
38
38
|
|
|
39
39
|
## 📝 使用示例
|
|
40
40
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# 凡人打字机 (fr-cli)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
|
|
4
4
|
|
|
5
5
|
**🇨🇳 中文简介**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
支持:多模型 AI 对话(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)、MasterAgent 自我进化主控、思维模式切换(direct/CoT/ToT/ReAct)、文件沙盒操作、联网搜索(SSRF 防护)、图片生成与识别、邮件收发、定时任务(shlex 安全解析)、云盘集成、会话记忆、按日期自动存档、插件进化(子进程隔离)、四阶安全拦截、Shell 管道直通 AI。
|
|
8
8
|
|
|
9
9
|
**🇺🇸 English Intro**
|
|
10
10
|
|
|
11
|
-
The ultimate all-knowing terminal tool
|
|
11
|
+
The ultimate all-knowing terminal tool supporting multiple LLM providers (Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark). Supports AI chat, MasterAgent self-evolving controller, thinking modes (direct/CoT/ToT/ReAct), virtual filesystem, web search (SSRF-protected), image generation & vision, email, cron jobs (shlex-safe), cloud drive, session memory, auto date-based archiving, self-evolving plugins (subprocess-isolated), and powerful Shell piping.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -19,7 +19,7 @@ pip install fr-cli
|
|
|
19
19
|
fr-cli
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
首次运行会引导输入当前道统的 API Key。
|
|
23
23
|
|
|
24
24
|
## 🎮 使用方式
|
|
25
25
|
|
|
@@ -41,6 +41,13 @@ fr-cli
|
|
|
41
41
|
/session_load <idx> 加载存档会话
|
|
42
42
|
/mode direct|cot|tot|react 切换思维模式
|
|
43
43
|
/master on|off|status MasterAgent 主控
|
|
44
|
+
/model <模型名> 切换当前道统模型
|
|
45
|
+
/model <道统>:<模型名> 同时切换道统和模型
|
|
46
|
+
/key <key> 修改当前道统 API Key
|
|
47
|
+
/key <道统> <key> 为指定道统设置 Key
|
|
48
|
+
/providers 查看所有道统配置
|
|
49
|
+
/providers add <p> <k> [m] 添加/更新道统配置
|
|
50
|
+
/providers use <p> 切换到指定道统
|
|
44
51
|
/mcp_list 列出 MCP 服务器及工具
|
|
45
52
|
/mcp_add <名> <命令> [参数] 添加 MCP 服务器
|
|
46
53
|
/mcp_del <名> 删除 MCP 服务器
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fr-cli
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.6
|
|
4
4
|
Summary: 凡人打字机 - 支持多模型(Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark)的终极全能终端工具
|
|
5
5
|
Author: FANREN CLI Author
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,7 +37,7 @@ Requires-Dist: pyodbc>=4.0.0; extra == "db"
|
|
|
37
37
|
Requires-Dist: oracledb>=1.3.0; extra == "db"
|
|
38
38
|
Provides-Extra: rag
|
|
39
39
|
Requires-Dist: chromadb>=0.4.0; extra == "rag"
|
|
40
|
-
Requires-Dist: sentence-transformers>=2.2.
|
|
40
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == "rag"
|
|
41
41
|
Provides-Extra: remote
|
|
42
42
|
Requires-Dist: paramiko>=3.0.0; extra == "remote"
|
|
43
43
|
Provides-Extra: spider
|
|
@@ -56,7 +56,7 @@ Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
|
56
56
|
Requires-Dist: pyodbc>=4.0.0; extra == "all"
|
|
57
57
|
Requires-Dist: oracledb>=1.3.0; extra == "all"
|
|
58
58
|
Requires-Dist: chromadb>=0.4.0; extra == "all"
|
|
59
|
-
Requires-Dist: sentence-transformers>=2.2.
|
|
59
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == "all"
|
|
60
60
|
Requires-Dist: paramiko>=3.0.0; extra == "all"
|
|
61
61
|
Requires-Dist: selenium>=4.10.0; extra == "all"
|
|
62
62
|
Requires-Dist: bypy; extra == "all"
|
|
@@ -71,11 +71,11 @@ Dynamic: license-file
|
|
|
71
71
|
|
|
72
72
|
# 凡人打字机 (fr-cli)
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
|
|
75
75
|
|
|
76
76
|
## ✨ 功能特性
|
|
77
77
|
|
|
78
|
-
- 🤖 **AI
|
|
78
|
+
- 🤖 **AI 对话**:支持多模型(智谱 GLM / DeepSeek / Kimi / 通义千问 / StepFun / MiniMax / 讯飞星火)
|
|
79
79
|
- 🧠 **MasterAgent 主控**:自我进化的 ReAct 主控 Agent,自动规划、调用工具、反思进化
|
|
80
80
|
- 🧩 **思维模式**:direct / CoT / ToT / ReAct 四种推理模式切换
|
|
81
81
|
- 📁 **文件沙盒**:安全的虚拟文件系统,支持读写/目录操作
|
|
@@ -105,7 +105,7 @@ pip install fr-cli
|
|
|
105
105
|
fr-cli
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
首次运行会引导输入当前道统的 API Key。
|
|
109
109
|
|
|
110
110
|
## 📝 使用示例
|
|
111
111
|
|
|
@@ -66,17 +66,4 @@ fr_cli/weapon/loader.py
|
|
|
66
66
|
fr_cli/weapon/mail.py
|
|
67
67
|
fr_cli/weapon/mcp.py
|
|
68
68
|
fr_cli/weapon/vision.py
|
|
69
|
-
fr_cli/weapon/web.py
|
|
70
|
-
tests/test_agent_client.py
|
|
71
|
-
tests/test_agent_server.py
|
|
72
|
-
tests/test_ai_save_file_with_verify.py
|
|
73
|
-
tests/test_all.py
|
|
74
|
-
tests/test_auto_session.py
|
|
75
|
-
tests/test_builtins.py
|
|
76
|
-
tests/test_dataframe.py
|
|
77
|
-
tests/test_gatekeeper.py
|
|
78
|
-
tests/test_integration.py
|
|
79
|
-
tests/test_intent_classification.py
|
|
80
|
-
tests/test_launcher.py
|
|
81
|
-
tests/test_master_agent.py
|
|
82
|
-
tests/test_structured_tools.py
|
|
69
|
+
fr_cli/weapon/web.py
|
|
@@ -11,7 +11,7 @@ psycopg2-binary>=2.9.0
|
|
|
11
11
|
pyodbc>=4.0.0
|
|
12
12
|
oracledb>=1.3.0
|
|
13
13
|
chromadb>=0.4.0
|
|
14
|
-
sentence-transformers>=2.2.
|
|
14
|
+
sentence-transformers>=2.2.0
|
|
15
15
|
paramiko>=3.0.0
|
|
16
16
|
selenium>=4.10.0
|
|
17
17
|
bypy
|
|
@@ -44,7 +44,7 @@ watchdog>=3.0.0
|
|
|
44
44
|
|
|
45
45
|
[rag]
|
|
46
46
|
chromadb>=0.4.0
|
|
47
|
-
sentence-transformers>=2.2.
|
|
47
|
+
sentence-transformers>=2.2.0
|
|
48
48
|
|
|
49
49
|
[remote]
|
|
50
50
|
paramiko>=3.0.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fr-cli"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.6"
|
|
8
8
|
description = "凡人打字机 - 支持多模型(Zhipu/DeepSeek/Kimi/Qwen/StepFun/MiniMax/Spark)的终极全能终端工具"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -38,7 +38,7 @@ dependencies = [
|
|
|
38
38
|
[project.optional-dependencies]
|
|
39
39
|
data = ["pandas>=1.5.0", "openpyxl>=3.0.0"]
|
|
40
40
|
db = ["pymysql>=1.0.0", "psycopg2-binary>=2.9.0", "pyodbc>=4.0.0", "oracledb>=1.3.0"]
|
|
41
|
-
rag = ["chromadb>=0.4.0", "sentence-transformers>=2.2.
|
|
41
|
+
rag = ["chromadb>=0.4.0", "sentence-transformers>=2.2.0"]
|
|
42
42
|
remote = ["paramiko>=3.0.0"]
|
|
43
43
|
spider = ["selenium>=4.10.0"]
|
|
44
44
|
cloud = ["bypy", "aligo", "msal"]
|
|
@@ -46,7 +46,7 @@ monitor = ["watchdog>=3.0.0"]
|
|
|
46
46
|
all = [
|
|
47
47
|
"pandas>=1.5.0", "openpyxl>=3.0.0",
|
|
48
48
|
"pymysql>=1.0.0", "psycopg2-binary>=2.9.0", "pyodbc>=4.0.0", "oracledb>=1.3.0",
|
|
49
|
-
"chromadb>=0.4.0", "sentence-transformers>=2.2.
|
|
49
|
+
"chromadb>=0.4.0", "sentence-transformers>=2.2.0",
|
|
50
50
|
"paramiko>=3.0.0",
|
|
51
51
|
"selenium>=4.10.0",
|
|
52
52
|
"bypy", "aligo", "msal",
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Agent 客户端测试 —— 本地/远程 Agent 调用与发现
|
|
3
|
-
"""
|
|
4
|
-
import json
|
|
5
|
-
import tempfile
|
|
6
|
-
import unittest
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from unittest.mock import MagicMock, patch
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class MockState:
|
|
12
|
-
def __init__(self):
|
|
13
|
-
self.client = MagicMock()
|
|
14
|
-
self.model_name = "glm-4-flash"
|
|
15
|
-
self.lang = "zh"
|
|
16
|
-
self.executor = MagicMock()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class TestAgentRemote(unittest.TestCase):
|
|
20
|
-
"""测试远程 Agent 配置管理"""
|
|
21
|
-
|
|
22
|
-
def setUp(self):
|
|
23
|
-
self.tmpdir = tempfile.TemporaryDirectory()
|
|
24
|
-
from fr_cli.agent import remote as remote_mod
|
|
25
|
-
self._orig_file = remote_mod.REMOTE_AGENTS_FILE
|
|
26
|
-
remote_mod.REMOTE_AGENTS_FILE = Path(self.tmpdir.name) / "remote_agents.json"
|
|
27
|
-
|
|
28
|
-
def tearDown(self):
|
|
29
|
-
self.tmpdir.cleanup()
|
|
30
|
-
from fr_cli.agent import remote as remote_mod
|
|
31
|
-
remote_mod.REMOTE_AGENTS_FILE = self._orig_file
|
|
32
|
-
|
|
33
|
-
def test_add_and_list_remote_agent(self):
|
|
34
|
-
from fr_cli.agent.remote import add_remote_agent, list_remote_agents
|
|
35
|
-
add_remote_agent("data_analyst", "192.168.1.100", 8080, "tok123", "数据分析助手")
|
|
36
|
-
agents = list_remote_agents()
|
|
37
|
-
self.assertIn("data_analyst", agents)
|
|
38
|
-
self.assertEqual(agents["data_analyst"]["host"], "192.168.1.100")
|
|
39
|
-
self.assertEqual(agents["data_analyst"]["port"], 8080)
|
|
40
|
-
self.assertEqual(agents["data_analyst"]["token"], "tok123")
|
|
41
|
-
|
|
42
|
-
def test_remove_remote_agent(self):
|
|
43
|
-
from fr_cli.agent.remote import add_remote_agent, remove_remote_agent, list_remote_agents
|
|
44
|
-
add_remote_agent("test_agent", "127.0.0.1", 9090, "abc")
|
|
45
|
-
self.assertTrue(remove_remote_agent("test_agent"))
|
|
46
|
-
self.assertEqual(list_remote_agents(), {})
|
|
47
|
-
self.assertFalse(remove_remote_agent("not_exist"))
|
|
48
|
-
|
|
49
|
-
def test_get_remote_agent(self):
|
|
50
|
-
from fr_cli.agent.remote import add_remote_agent, get_remote_agent
|
|
51
|
-
add_remote_agent("web_crawler", "10.0.0.5", 17890, "secret", "爬虫助手")
|
|
52
|
-
cfg = get_remote_agent("web_crawler")
|
|
53
|
-
self.assertIsNotNone(cfg)
|
|
54
|
-
self.assertEqual(cfg["port"], 17890)
|
|
55
|
-
self.assertIsNone(get_remote_agent("not_exist"))
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class TestAgentClient(unittest.TestCase):
|
|
59
|
-
"""测试 Agent API 客户端"""
|
|
60
|
-
|
|
61
|
-
def test_discover_all_agents(self):
|
|
62
|
-
from fr_cli.agent.client import discover_all_agents
|
|
63
|
-
agents = discover_all_agents()
|
|
64
|
-
# 返回列表格式检查
|
|
65
|
-
self.assertIsInstance(agents, list)
|
|
66
|
-
for a in agents:
|
|
67
|
-
self.assertIn("name", a)
|
|
68
|
-
self.assertIn("type", a)
|
|
69
|
-
self.assertIn(a["type"], ("local", "remote"))
|
|
70
|
-
|
|
71
|
-
def test_call_local_agent_not_found(self):
|
|
72
|
-
from fr_cli.agent.client import call_agent
|
|
73
|
-
state = MockState()
|
|
74
|
-
result, err = call_agent("nonexistent_agent_xyz", state, user_input="hello")
|
|
75
|
-
self.assertIsNone(result)
|
|
76
|
-
self.assertIn("未找到", err)
|
|
77
|
-
|
|
78
|
-
@patch("fr_cli.agent.client.urllib.request.urlopen")
|
|
79
|
-
def test_call_remote_agent_success(self, mock_urlopen):
|
|
80
|
-
from fr_cli.agent.client import call_remote_agent
|
|
81
|
-
mock_resp = MagicMock()
|
|
82
|
-
mock_resp.read.return_value = json.dumps({"result": "远程分析完成", "error": None}).encode()
|
|
83
|
-
mock_urlopen.return_value.__enter__.return_value = mock_resp
|
|
84
|
-
|
|
85
|
-
cfg = {"host": "192.168.1.50", "port": 8080, "token": "tok456"}
|
|
86
|
-
result, err = call_remote_agent("data_bot", "分析销售数据", cfg)
|
|
87
|
-
self.assertEqual(result, "远程分析完成")
|
|
88
|
-
self.assertIsNone(err)
|
|
89
|
-
|
|
90
|
-
# 验证请求构造
|
|
91
|
-
req = mock_urlopen.call_args[0][0]
|
|
92
|
-
self.assertEqual(req.full_url, "http://192.168.1.50:8080/agents/data_bot/run")
|
|
93
|
-
self.assertEqual(req.headers.get("Authorization"), "Bearer tok456")
|
|
94
|
-
|
|
95
|
-
@patch("fr_cli.agent.client.urllib.request.urlopen")
|
|
96
|
-
def test_call_remote_agent_http_error(self, mock_urlopen):
|
|
97
|
-
from fr_cli.agent.client import call_remote_agent
|
|
98
|
-
from urllib.error import HTTPError
|
|
99
|
-
mock_urlopen.side_effect = HTTPError(
|
|
100
|
-
"http://test/agents/x/run", 401, "Unauthorized", {}, None
|
|
101
|
-
)
|
|
102
|
-
cfg = {"host": "test", "port": 80, "token": "bad"}
|
|
103
|
-
result, err = call_remote_agent("x", "hi", cfg)
|
|
104
|
-
self.assertIsNone(result)
|
|
105
|
-
self.assertIn("401", err)
|
|
106
|
-
|
|
107
|
-
@patch("fr_cli.agent.client.urllib.request.urlopen")
|
|
108
|
-
def test_scan_remote_host(self, mock_urlopen):
|
|
109
|
-
from fr_cli.agent.client import scan_remote_host
|
|
110
|
-
|
|
111
|
-
def _make_resp(data_dict):
|
|
112
|
-
m = MagicMock()
|
|
113
|
-
m.read.return_value = json.dumps(data_dict).encode("utf-8")
|
|
114
|
-
m.__enter__ = MagicMock(return_value=m)
|
|
115
|
-
m.__exit__ = MagicMock(return_value=False)
|
|
116
|
-
return m
|
|
117
|
-
|
|
118
|
-
def mock_response(req, timeout=None):
|
|
119
|
-
url = req.full_url
|
|
120
|
-
if "capabilities" in url:
|
|
121
|
-
return _make_resp({
|
|
122
|
-
"service": "fr-cli-agent-api",
|
|
123
|
-
"version": "2.2.0",
|
|
124
|
-
"agents": [],
|
|
125
|
-
"endpoints": {},
|
|
126
|
-
})
|
|
127
|
-
elif "agents" in url:
|
|
128
|
-
return _make_resp({
|
|
129
|
-
"agents": [{"name": "coder", "has_persona": True, "has_skills": True}],
|
|
130
|
-
})
|
|
131
|
-
return _make_resp({})
|
|
132
|
-
|
|
133
|
-
mock_urlopen.side_effect = mock_response
|
|
134
|
-
info, err = scan_remote_host("192.168.1.10", 8080, "tok")
|
|
135
|
-
self.assertIsNone(err)
|
|
136
|
-
self.assertEqual(info["service"], "fr-cli-agent-api")
|
|
137
|
-
self.assertEqual(len(info["agents"]), 1)
|
|
138
|
-
self.assertEqual(info["agents"][0]["name"], "coder")
|
|
139
|
-
|
|
140
|
-
@patch("fr_cli.agent.client.urllib.request.urlopen")
|
|
141
|
-
def test_scan_remote_host_failure(self, mock_urlopen):
|
|
142
|
-
from fr_cli.agent.client import scan_remote_host
|
|
143
|
-
from urllib.error import URLError
|
|
144
|
-
mock_urlopen.side_effect = URLError("Connection refused")
|
|
145
|
-
info, err = scan_remote_host("bad_host", 9999, "tok")
|
|
146
|
-
self.assertIsNone(info)
|
|
147
|
-
self.assertIn("Connection refused", err)
|
|
148
|
-
|
|
149
|
-
@patch("fr_cli.agent.client.urllib.request.urlopen")
|
|
150
|
-
def test_import_remote_agents(self, mock_urlopen):
|
|
151
|
-
from fr_cli.agent.client import import_remote_agents
|
|
152
|
-
import tempfile
|
|
153
|
-
from fr_cli.agent import remote as remote_mod
|
|
154
|
-
|
|
155
|
-
tmpdir = tempfile.TemporaryDirectory()
|
|
156
|
-
orig_file = remote_mod.REMOTE_AGENTS_FILE
|
|
157
|
-
remote_mod.REMOTE_AGENTS_FILE = Path(tmpdir.name) / "remote.json"
|
|
158
|
-
|
|
159
|
-
def _make_resp(data_dict):
|
|
160
|
-
m = MagicMock()
|
|
161
|
-
m.read.return_value = json.dumps(data_dict).encode("utf-8")
|
|
162
|
-
m.__enter__ = MagicMock(return_value=m)
|
|
163
|
-
m.__exit__ = MagicMock(return_value=False)
|
|
164
|
-
return m
|
|
165
|
-
|
|
166
|
-
def mock_response(req, timeout=None):
|
|
167
|
-
url = req.full_url
|
|
168
|
-
if "capabilities" in url:
|
|
169
|
-
return _make_resp({
|
|
170
|
-
"service": "fr-cli-agent-api",
|
|
171
|
-
"version": "2.2.0",
|
|
172
|
-
"agents": [],
|
|
173
|
-
})
|
|
174
|
-
elif "agents" in url:
|
|
175
|
-
return _make_resp({
|
|
176
|
-
"agents": [
|
|
177
|
-
{"name": "writer", "has_persona": True},
|
|
178
|
-
{"name": "coder", "has_persona": True},
|
|
179
|
-
],
|
|
180
|
-
})
|
|
181
|
-
return _make_resp({})
|
|
182
|
-
|
|
183
|
-
mock_urlopen.side_effect = mock_response
|
|
184
|
-
imported, errors = import_remote_agents("10.0.0.1", 9090, "abc", prefix="team")
|
|
185
|
-
self.assertEqual(imported, 2)
|
|
186
|
-
self.assertEqual(len(errors), 0)
|
|
187
|
-
|
|
188
|
-
# 验证已写入配置
|
|
189
|
-
agents = remote_mod.list_remote_agents()
|
|
190
|
-
self.assertIn("team_writer", agents)
|
|
191
|
-
self.assertIn("team_coder", agents)
|
|
192
|
-
self.assertEqual(agents["team_writer"]["host"], "10.0.0.1")
|
|
193
|
-
|
|
194
|
-
remote_mod.REMOTE_AGENTS_FILE = orig_file
|
|
195
|
-
tmpdir.cleanup()
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if __name__ == "__main__":
|
|
199
|
-
unittest.main()
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
测试 Agent HTTP 服务功能
|
|
3
|
-
"""
|
|
4
|
-
import json
|
|
5
|
-
import threading
|
|
6
|
-
import time
|
|
7
|
-
import unittest
|
|
8
|
-
from types import SimpleNamespace
|
|
9
|
-
from unittest.mock import MagicMock, patch
|
|
10
|
-
|
|
11
|
-
from fr_cli.agent.server import AgentHTTPServer, _AgentHTTPHandler
|
|
12
|
-
from fr_cli.agent.manager import create_agent_dir, delete_agent, _agent_dir
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class MockState:
|
|
16
|
-
"""轻量 mock AppState"""
|
|
17
|
-
def __init__(self):
|
|
18
|
-
self.client = MagicMock()
|
|
19
|
-
self.model_name = "glm-4-flash"
|
|
20
|
-
self.lang = "zh"
|
|
21
|
-
self.executor = MagicMock()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _wait_for_server(server, timeout=2):
|
|
25
|
-
for _ in range(int(timeout * 10)):
|
|
26
|
-
if server.is_running():
|
|
27
|
-
return True
|
|
28
|
-
time.sleep(0.1)
|
|
29
|
-
return False
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class TestAgentHTTPServer(unittest.TestCase):
|
|
33
|
-
|
|
34
|
-
def setUp(self):
|
|
35
|
-
self.state = MockState()
|
|
36
|
-
self.server = AgentHTTPServer(self.state, host="127.0.0.1", port=0)
|
|
37
|
-
|
|
38
|
-
def tearDown(self):
|
|
39
|
-
if self.server.is_running():
|
|
40
|
-
self.server.stop()
|
|
41
|
-
|
|
42
|
-
def test_start_stop(self):
|
|
43
|
-
ok, msg = self.server.start()
|
|
44
|
-
self.assertTrue(ok, msg)
|
|
45
|
-
self.assertTrue(self.server.is_running())
|
|
46
|
-
ok2, msg2 = self.server.stop()
|
|
47
|
-
self.assertTrue(ok2, msg2)
|
|
48
|
-
self.assertFalse(self.server.is_running())
|
|
49
|
-
|
|
50
|
-
def test_double_start(self):
|
|
51
|
-
self.server.start()
|
|
52
|
-
ok, msg = self.server.start()
|
|
53
|
-
self.assertFalse(ok)
|
|
54
|
-
self.assertIn("已在运行", msg)
|
|
55
|
-
self.server.stop()
|
|
56
|
-
|
|
57
|
-
def test_status(self):
|
|
58
|
-
self.assertIn("未运行", self.server.status())
|
|
59
|
-
self.server.start()
|
|
60
|
-
self.assertIn("运行中", self.server.status())
|
|
61
|
-
self.server.stop()
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class TestAgentHTTPHandler(unittest.TestCase):
|
|
65
|
-
|
|
66
|
-
@classmethod
|
|
67
|
-
def setUpClass(cls):
|
|
68
|
-
cls.state = MockState()
|
|
69
|
-
cls.server = AgentHTTPServer(cls.state, host="127.0.0.1", port=0)
|
|
70
|
-
cls.server.start()
|
|
71
|
-
_wait_for_server(cls.server)
|
|
72
|
-
cls.port = cls.server._server.server_address[1]
|
|
73
|
-
|
|
74
|
-
@classmethod
|
|
75
|
-
def tearDownClass(cls):
|
|
76
|
-
cls.server.stop()
|
|
77
|
-
|
|
78
|
-
def _request(self, method, path, data=None):
|
|
79
|
-
import http.client
|
|
80
|
-
conn = http.client.HTTPConnection("127.0.0.1", self.port)
|
|
81
|
-
body = json.dumps(data) if data else None
|
|
82
|
-
headers = {"Content-Type": "application/json"} if body else {}
|
|
83
|
-
headers["Authorization"] = f"Bearer {self.server._token}"
|
|
84
|
-
conn.request(method, path, body=body, headers=headers)
|
|
85
|
-
resp = conn.getresponse()
|
|
86
|
-
status = resp.status
|
|
87
|
-
resp_body = resp.read().decode("utf-8")
|
|
88
|
-
conn.close()
|
|
89
|
-
return status, json.loads(resp_body) if resp_body else {}
|
|
90
|
-
|
|
91
|
-
def test_health(self):
|
|
92
|
-
status, data = self._request("GET", "/health")
|
|
93
|
-
self.assertEqual(status, 200)
|
|
94
|
-
self.assertEqual(data["status"], "ok")
|
|
95
|
-
|
|
96
|
-
def test_agents_list(self):
|
|
97
|
-
status, data = self._request("GET", "/agents")
|
|
98
|
-
self.assertEqual(status, 200)
|
|
99
|
-
self.assertIn("agents", data)
|
|
100
|
-
|
|
101
|
-
def test_agent_not_found(self):
|
|
102
|
-
status, data = self._request("GET", "/agents/nonexistent_agent_12345")
|
|
103
|
-
self.assertEqual(status, 404)
|
|
104
|
-
self.assertIn("error", data)
|
|
105
|
-
|
|
106
|
-
def test_unauthorized_without_token(self):
|
|
107
|
-
import http.client
|
|
108
|
-
conn = http.client.HTTPConnection("127.0.0.1", self.port)
|
|
109
|
-
conn.request("GET", "/agents")
|
|
110
|
-
resp = conn.getresponse()
|
|
111
|
-
self.assertEqual(resp.status, 401)
|
|
112
|
-
conn.close()
|
|
113
|
-
|
|
114
|
-
def test_capabilities_endpoint(self):
|
|
115
|
-
status, data = self._request("GET", "/capabilities")
|
|
116
|
-
self.assertEqual(status, 200)
|
|
117
|
-
self.assertEqual(data["service"], "fr-cli-agent-api")
|
|
118
|
-
self.assertIn("agents", data)
|
|
119
|
-
self.assertIn("endpoints", data)
|
|
120
|
-
|
|
121
|
-
def test_cors_preflight(self):
|
|
122
|
-
import http.client
|
|
123
|
-
conn = http.client.HTTPConnection("127.0.0.1", self.port)
|
|
124
|
-
conn.request("OPTIONS", "/agents", headers={
|
|
125
|
-
"Authorization": f"Bearer {self.server._token}",
|
|
126
|
-
"Origin": "http://example.com",
|
|
127
|
-
})
|
|
128
|
-
resp = conn.getresponse()
|
|
129
|
-
self.assertEqual(resp.status, 204)
|
|
130
|
-
conn.close()
|
|
131
|
-
|
|
132
|
-
def test_ip_whitelist_block(self):
|
|
133
|
-
# 设置一个不可能匹配的 IP 白名单
|
|
134
|
-
self.server.set_ip_whitelist(["1.2.3.4"])
|
|
135
|
-
import http.client
|
|
136
|
-
conn = http.client.HTTPConnection("127.0.0.1", self.port)
|
|
137
|
-
conn.request("GET", "/health", headers={
|
|
138
|
-
"Authorization": f"Bearer {self.server._token}",
|
|
139
|
-
})
|
|
140
|
-
resp = conn.getresponse()
|
|
141
|
-
self.assertEqual(resp.status, 403)
|
|
142
|
-
conn.close()
|
|
143
|
-
# 恢复
|
|
144
|
-
self.server.set_ip_whitelist([])
|
|
145
|
-
|
|
146
|
-
def test_publish_info(self):
|
|
147
|
-
info = self.server.get_publish_info()
|
|
148
|
-
self.assertIsNotNone(info)
|
|
149
|
-
self.assertIn("url", info)
|
|
150
|
-
self.assertIn("token", info)
|
|
151
|
-
self.assertEqual(info["token"], self.server._token)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
class TestAgentHTTPRun(unittest.TestCase):
|
|
155
|
-
|
|
156
|
-
@classmethod
|
|
157
|
-
def setUpClass(cls):
|
|
158
|
-
cls.agent_name = "__test_http_agent__"
|
|
159
|
-
d = create_agent_dir(cls.agent_name)
|
|
160
|
-
(d / "agent.py").write_text("def run(ctx, **kwargs): return 'hello from ' + ctx['agent_name']", encoding="utf-8")
|
|
161
|
-
cls.state = MockState()
|
|
162
|
-
cls.server = AgentHTTPServer(cls.state, host="127.0.0.1", port=0)
|
|
163
|
-
cls.server.start()
|
|
164
|
-
_wait_for_server(cls.server)
|
|
165
|
-
cls.port = cls.server._server.server_address[1]
|
|
166
|
-
|
|
167
|
-
@classmethod
|
|
168
|
-
def tearDownClass(cls):
|
|
169
|
-
cls.server.stop()
|
|
170
|
-
delete_agent(cls.agent_name)
|
|
171
|
-
|
|
172
|
-
def _request(self, method, path, data=None):
|
|
173
|
-
import http.client
|
|
174
|
-
conn = http.client.HTTPConnection("127.0.0.1", self.port)
|
|
175
|
-
body = json.dumps(data) if data else None
|
|
176
|
-
headers = {"Content-Type": "application/json"} if body else {}
|
|
177
|
-
headers["Authorization"] = f"Bearer {self.server._token}"
|
|
178
|
-
conn.request(method, path, body=body, headers=headers)
|
|
179
|
-
resp = conn.getresponse()
|
|
180
|
-
status = resp.status
|
|
181
|
-
resp_body = resp.read().decode("utf-8")
|
|
182
|
-
conn.close()
|
|
183
|
-
return status, json.loads(resp_body) if resp_body else {}
|
|
184
|
-
|
|
185
|
-
def test_run_agent(self):
|
|
186
|
-
status, data = self._request("POST", f"/agents/{self.agent_name}/run", {"input": "hi"})
|
|
187
|
-
self.assertEqual(status, 200)
|
|
188
|
-
self.assertIn("hello from", data["result"])
|
|
189
|
-
self.assertIsNone(data["error"])
|
|
190
|
-
|
|
191
|
-
def test_get_agent_info(self):
|
|
192
|
-
status, data = self._request("GET", f"/agents/{self.agent_name}")
|
|
193
|
-
self.assertEqual(status, 200)
|
|
194
|
-
self.assertEqual(data["name"], self.agent_name)
|
|
195
|
-
self.assertFalse(data["has_workflow"])
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if __name__ == "__main__":
|
|
199
|
-
unittest.main()
|