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.
Files changed (84) hide show
  1. {fr_cli-2.2.5/fr_cli.egg-info → fr_cli-2.2.6}/PKG-INFO +6 -6
  2. {fr_cli-2.2.5 → fr_cli-2.2.6}/README.md +3 -3
  3. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/README.md +11 -4
  4. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/__init__.py +1 -1
  5. {fr_cli-2.2.5 → fr_cli-2.2.6/fr_cli.egg-info}/PKG-INFO +6 -6
  6. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli.egg-info/SOURCES.txt +1 -14
  7. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli.egg-info/requires.txt +2 -2
  8. {fr_cli-2.2.5 → fr_cli-2.2.6}/pyproject.toml +3 -3
  9. fr_cli-2.2.5/tests/test_agent_client.py +0 -199
  10. fr_cli-2.2.5/tests/test_agent_server.py +0 -199
  11. fr_cli-2.2.5/tests/test_ai_save_file_with_verify.py +0 -228
  12. fr_cli-2.2.5/tests/test_all.py +0 -917
  13. fr_cli-2.2.5/tests/test_auto_session.py +0 -116
  14. fr_cli-2.2.5/tests/test_builtins.py +0 -67
  15. fr_cli-2.2.5/tests/test_dataframe.py +0 -42
  16. fr_cli-2.2.5/tests/test_gatekeeper.py +0 -118
  17. fr_cli-2.2.5/tests/test_integration.py +0 -224
  18. fr_cli-2.2.5/tests/test_intent_classification.py +0 -268
  19. fr_cli-2.2.5/tests/test_launcher.py +0 -98
  20. fr_cli-2.2.5/tests/test_master_agent.py +0 -162
  21. fr_cli-2.2.5/tests/test_structured_tools.py +0 -245
  22. {fr_cli-2.2.5 → fr_cli-2.2.6}/LICENSE +0 -0
  23. {fr_cli-2.2.5 → fr_cli-2.2.6}/MANIFEST.in +0 -0
  24. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/WEAPON.MD +0 -0
  25. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/addon/plugin.py +0 -0
  26. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/__init__.py +0 -0
  27. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/__init__.py +0 -0
  28. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/_utils.py +0 -0
  29. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/db.py +0 -0
  30. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/local.py +0 -0
  31. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/rag.py +0 -0
  32. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/rag_watcher_daemon.py +0 -0
  33. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/remote.py +0 -0
  34. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/builtins/spider.py +0 -0
  35. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/client.py +0 -0
  36. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/executor.py +0 -0
  37. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/generator.py +0 -0
  38. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/manager.py +0 -0
  39. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/master.py +0 -0
  40. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/master_prompt.py +0 -0
  41. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/remote.py +0 -0
  42. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/server.py +0 -0
  43. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/agent/workflow.py +0 -0
  44. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/breakthrough/update.py +0 -0
  45. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/command/__init__.py +0 -0
  46. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/command/executor.py +0 -0
  47. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/command/registry.py +0 -0
  48. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/command/security.py +0 -0
  49. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/conf/config.py +0 -0
  50. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/conf/wizard.py +0 -0
  51. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/chat.py +0 -0
  52. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/core.py +0 -0
  53. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/intent.py +0 -0
  54. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/llm.py +0 -0
  55. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/recommender.py +0 -0
  56. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/stream.py +0 -0
  57. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/sysmon.py +0 -0
  58. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/core/thinking.py +0 -0
  59. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/gatekeeper/__init__.py +0 -0
  60. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/gatekeeper/daemon.py +0 -0
  61. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/gatekeeper/manager.py +0 -0
  62. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/lang/i18n.py +0 -0
  63. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/main.py +0 -0
  64. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/memory/context.py +0 -0
  65. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/memory/history.py +0 -0
  66. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/memory/session.py +0 -0
  67. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/repl/__init__.py +0 -0
  68. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/repl/commands.py +0 -0
  69. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/security/security.py +0 -0
  70. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/ui/ui.py +0 -0
  71. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/cron.py +0 -0
  72. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/dataframe.py +0 -0
  73. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/disk.py +0 -0
  74. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/fs.py +0 -0
  75. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/launcher.py +0 -0
  76. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/loader.py +0 -0
  77. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/mail.py +0 -0
  78. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/mcp.py +0 -0
  79. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/vision.py +0 -0
  80. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli/weapon/web.py +0 -0
  81. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli.egg-info/dependency_links.txt +0 -0
  82. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli.egg-info/entry_points.txt +0 -0
  83. {fr_cli-2.2.5 → fr_cli-2.2.6}/fr_cli.egg-info/top_level.txt +0 -0
  84. {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.5
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.5; extra == "rag"
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.5; extra == "all"
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
- 基于智谱 AI (ZhipuAI/GLM) 的终极全能终端工具。
74
+ 支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
75
75
 
76
76
  ## ✨ 功能特性
77
77
 
78
- - 🤖 **AI 对话**:基于 GLM-4 系列模型的智能对话
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
- 首次运行会引导输入智谱 API Key。
108
+ 首次运行会引导输入当前道统的 API Key。
109
109
 
110
110
  ## 📝 使用示例
111
111
 
@@ -1,10 +1,10 @@
1
1
  # 凡人打字机 (fr-cli)
2
2
 
3
- 基于智谱 AI (ZhipuAI/GLM) 的终极全能终端工具。
3
+ 支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
4
4
 
5
5
  ## ✨ 功能特性
6
6
 
7
- - 🤖 **AI 对话**:基于 GLM-4 系列模型的智能对话
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
- 首次运行会引导输入智谱 API Key。
37
+ 首次运行会引导输入当前道统的 API Key。
38
38
 
39
39
  ## 📝 使用示例
40
40
 
@@ -1,14 +1,14 @@
1
1
  # 凡人打字机 (fr-cli)
2
2
 
3
- 基于智谱 AI (ZhipuAI / GLM) 的终极全能终端工具。
3
+ 支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
4
4
 
5
5
  **🇨🇳 中文简介**
6
6
 
7
- 支持:AI 智能对话、MasterAgent 自我进化主控、思维模式切换(direct/CoT/ToT/ReAct)、文件沙盒操作、联网搜索(SSRF 防护)、图片生成与识别、邮件收发、定时任务(shlex 安全解析)、云盘集成、会话记忆、按日期自动存档、插件进化(子进程隔离)、四阶安全拦截、Shell 管道直通 AI。
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 based on Zhipu AI. 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.
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
- 首次运行会引导输入智谱 API Key。
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,4 +1,4 @@
1
1
  """
2
2
  凡人打字机 - 基于智谱AI的终极全能终端工具
3
3
  """
4
- __version__ = "2.2.5"
4
+ __version__ = "2.2.6"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fr-cli
3
- Version: 2.2.5
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.5; extra == "rag"
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.5; extra == "all"
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
- 基于智谱 AI (ZhipuAI/GLM) 的终极全能终端工具。
74
+ 支持多模型(智谱/DeepSeek/Kimi/Qwen/StepFun/MiniMax/讯飞星火)的终极全能终端工具。
75
75
 
76
76
  ## ✨ 功能特性
77
77
 
78
- - 🤖 **AI 对话**:基于 GLM-4 系列模型的智能对话
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
- 首次运行会引导输入智谱 API Key。
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.5
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.5
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.5"
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.5"]
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.5",
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()