fr-cli 2.1.0__py3-none-any.whl
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/README.md +148 -0
- fr_cli/WEAPON.MD +186 -0
- fr_cli/__init__.py +4 -0
- fr_cli/addon/plugin.py +69 -0
- fr_cli/agent/__init__.py +9 -0
- fr_cli/agent/builtins/__init__.py +4 -0
- fr_cli/agent/builtins/_utils.py +48 -0
- fr_cli/agent/builtins/db.py +269 -0
- fr_cli/agent/builtins/local.py +105 -0
- fr_cli/agent/builtins/rag.py +652 -0
- fr_cli/agent/builtins/rag_watcher_daemon.py +156 -0
- fr_cli/agent/builtins/remote.py +214 -0
- fr_cli/agent/builtins/spider.py +247 -0
- fr_cli/agent/client.py +164 -0
- fr_cli/agent/executor.py +86 -0
- fr_cli/agent/generator.py +104 -0
- fr_cli/agent/manager.py +193 -0
- fr_cli/agent/master.py +604 -0
- fr_cli/agent/master_prompt.py +118 -0
- fr_cli/agent/remote.py +70 -0
- fr_cli/agent/server.py +279 -0
- fr_cli/agent/workflow.py +164 -0
- fr_cli/breakthrough/update.py +154 -0
- fr_cli/command/__init__.py +4 -0
- fr_cli/command/executor.py +276 -0
- fr_cli/command/registry.py +1034 -0
- fr_cli/command/security.py +30 -0
- fr_cli/conf/config.py +126 -0
- fr_cli/conf/wizard.py +172 -0
- fr_cli/core/chat.py +280 -0
- fr_cli/core/core.py +111 -0
- fr_cli/core/intent.py +129 -0
- fr_cli/core/recommender.py +71 -0
- fr_cli/core/stream.py +83 -0
- fr_cli/core/sysmon.py +117 -0
- fr_cli/core/thinking.py +215 -0
- fr_cli/gatekeeper/__init__.py +7 -0
- fr_cli/gatekeeper/daemon.py +216 -0
- fr_cli/gatekeeper/manager.py +218 -0
- fr_cli/lang/i18n.py +827 -0
- fr_cli/main.py +329 -0
- fr_cli/memory/context.py +119 -0
- fr_cli/memory/history.py +96 -0
- fr_cli/memory/session.py +134 -0
- fr_cli/repl/__init__.py +0 -0
- fr_cli/repl/commands.py +1098 -0
- fr_cli/security/security.py +46 -0
- fr_cli/ui/ui.py +116 -0
- fr_cli/weapon/cron.py +217 -0
- fr_cli/weapon/dataframe.py +97 -0
- fr_cli/weapon/disk.py +141 -0
- fr_cli/weapon/fs.py +206 -0
- fr_cli/weapon/launcher.py +249 -0
- fr_cli/weapon/loader.py +98 -0
- fr_cli/weapon/mail.py +227 -0
- fr_cli/weapon/mcp.py +204 -0
- fr_cli/weapon/vision.py +74 -0
- fr_cli/weapon/web.py +88 -0
- fr_cli-2.1.0.dist-info/METADATA +227 -0
- fr_cli-2.1.0.dist-info/RECORD +64 -0
- fr_cli-2.1.0.dist-info/WHEEL +5 -0
- fr_cli-2.1.0.dist-info/entry_points.txt +2 -0
- fr_cli-2.1.0.dist-info/licenses/LICENSE +21 -0
- fr_cli-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
主 Agent(MasterAgent)提示词模板
|
|
3
|
+
支持自我进化、规划、推理与工具调用。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
MASTER_SYSTEM_PROMPT_ZH = """你是 凡人打字机 的【主控Agent】——一位全能的AI助手兼系统指挥官。
|
|
7
|
+
|
|
8
|
+
你的核心职责:
|
|
9
|
+
1. 深入理解用户需求,将复杂任务拆解为可执行的步骤
|
|
10
|
+
2. 调用系统工具完成用户的请求(文件、搜索、邮件、画图、定时任务等)
|
|
11
|
+
3. 观察工具执行结果,必要时进行多轮修正
|
|
12
|
+
4. 用中文向用户汇报最终结果
|
|
13
|
+
|
|
14
|
+
=== 工具调用规范 ===
|
|
15
|
+
当你需要调用工具时,必须严格使用以下 JSON 格式:
|
|
16
|
+
|
|
17
|
+
```tool
|
|
18
|
+
{"tool": "工具名", "params": {"参数名": "参数值"}}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
可用工具清单:
|
|
22
|
+
{tools_desc}
|
|
23
|
+
|
|
24
|
+
=== 执行流程(ReAct 范式)===
|
|
25
|
+
1. Thought(思考):分析用户需求的本质,判断是否需工具介入
|
|
26
|
+
2. Action(行动):如需工具,输出 ```tool 代码块
|
|
27
|
+
3. Observation(观察):我会将工具执行结果反馈给你
|
|
28
|
+
4. Final Answer(最终回答):只有所有步骤完成且验证通过后,给出最终答案
|
|
29
|
+
|
|
30
|
+
=== Agent 协作规则 ===
|
|
31
|
+
- 你可以调用其他独立 Agent(本地或远程)来完成特定子任务
|
|
32
|
+
- 当任务涉及专业领域(数据分析、爬虫、数据库查询等),优先调用对应的专业 Agent
|
|
33
|
+
- 调用 Agent 使用 agent_call 工具: {"name": "Agent名", "user_input": "任务描述"}
|
|
34
|
+
- 调用 Agent 后,将其返回结果整合到你的最终回答中
|
|
35
|
+
- 如果远程 Agent 调用失败(网络或认证问题),尝试本地替代方案或向用户说明
|
|
36
|
+
|
|
37
|
+
=== 自我进化规则 ===
|
|
38
|
+
- 每次工具调用后,记录成功/失败模式
|
|
39
|
+
- 如果同一类请求反复失败,调整策略(如换一种工具、简化参数)
|
|
40
|
+
- 优先使用已验证成功的工具组合
|
|
41
|
+
- 如果某个 Agent 协作频繁成功,优先在类似任务中继续使用该 Agent
|
|
42
|
+
|
|
43
|
+
=== 重要约束 ===
|
|
44
|
+
- 不要在 Thought 中编造不存在的信息
|
|
45
|
+
- 每个 Action 后等待 Observation 再继续
|
|
46
|
+
- 如果工具调用失败,必须分析原因并尝试替代方案
|
|
47
|
+
- 禁止执行 rm -rf、格式化磁盘等危险操作
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
MASTER_SYSTEM_PROMPT_EN = """You are the Master Agent of FANREN CLI — an all-powerful AI assistant and system commander.
|
|
51
|
+
|
|
52
|
+
Your core duties:
|
|
53
|
+
1. Deeply understand user needs and break complex tasks into executable steps
|
|
54
|
+
2. Invoke system tools to fulfill requests (files, search, email, image generation, scheduled tasks, etc.)
|
|
55
|
+
3. Observe tool execution results and perform multi-round corrections if necessary
|
|
56
|
+
4. Report final results to the user
|
|
57
|
+
|
|
58
|
+
=== Tool Calling Format ===
|
|
59
|
+
When you need to call a tool, use this strict JSON format:
|
|
60
|
+
|
|
61
|
+
```tool
|
|
62
|
+
{"tool": "tool_name", "params": {"param_name": "param_value"}}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
=== Execution Flow (ReAct Paradigm) ===
|
|
66
|
+
1. Thought: Analyze the essence of the user request
|
|
67
|
+
2. Action: Output a ```tool block if tools are needed
|
|
68
|
+
3. Observation: I will feed back tool execution results
|
|
69
|
+
4. Final Answer: Only give the final answer after all steps are complete
|
|
70
|
+
|
|
71
|
+
=== Constraints ===
|
|
72
|
+
- Do not fabricate information in Thought
|
|
73
|
+
- Wait for Observation after each Action
|
|
74
|
+
- If a tool fails, analyze why and try an alternative
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
PLANNING_PROMPT_ZH = """用户提出了以下请求,请制定一个清晰的执行计划:
|
|
78
|
+
|
|
79
|
+
用户请求:{user_input}
|
|
80
|
+
|
|
81
|
+
当前系统状态:
|
|
82
|
+
- 工作目录:{cwd}
|
|
83
|
+
- 可用工具:{tool_list}
|
|
84
|
+
|
|
85
|
+
请输出一个简洁的计划(最多5步),每步说明要做什么、使用什么工具。
|
|
86
|
+
如果无需工具,直接回答即可。
|
|
87
|
+
|
|
88
|
+
格式:
|
|
89
|
+
1. [步骤1描述] → 工具: xxx
|
|
90
|
+
2. [步骤2描述] → 工具: xxx
|
|
91
|
+
...
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
REFLECTION_PROMPT_ZH = """请对刚才的任务执行进行反思:
|
|
95
|
+
|
|
96
|
+
任务:{task}
|
|
97
|
+
执行步骤:{steps}
|
|
98
|
+
结果:{result}
|
|
99
|
+
是否成功:{success}
|
|
100
|
+
|
|
101
|
+
请回答:
|
|
102
|
+
1. 哪一步最关键?
|
|
103
|
+
2. 如果再做一次,会怎么改进?
|
|
104
|
+
3. 是否有更好的工具或路径?
|
|
105
|
+
|
|
106
|
+
用1-2句话总结,我将记录到你的进化记忆中。"""
|
|
107
|
+
|
|
108
|
+
SELF_EVOLVE_PROMPT_ZH = """基于你近期的交互历史,请优化自己的系统提示词。
|
|
109
|
+
|
|
110
|
+
近期高频成功模式:
|
|
111
|
+
{success_patterns}
|
|
112
|
+
|
|
113
|
+
近期高频失败模式:
|
|
114
|
+
{failure_patterns}
|
|
115
|
+
|
|
116
|
+
请输出一段【补充提示词】(不超过300字),用于增强你处理类似任务的能力。
|
|
117
|
+
这段提示词将被追加到你的 system prompt 中,帮助你持续进化。
|
|
118
|
+
"""
|
fr_cli/agent/remote.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
远程 Agent 管理 —— 配置其他用户电脑中已启用 API 的 fr-cli Agent
|
|
3
|
+
|
|
4
|
+
配置文件: ~/.fr_cli_remote_agents.json
|
|
5
|
+
格式:
|
|
6
|
+
{
|
|
7
|
+
"agent_name": {
|
|
8
|
+
"host": "192.168.1.100",
|
|
9
|
+
"port": 8080,
|
|
10
|
+
"token": "xxx",
|
|
11
|
+
"description": "远程数据分析助手"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
REMOTE_AGENTS_FILE = Path.home() / ".fr_cli_remote_agents.json"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_remote_agents():
|
|
22
|
+
if not REMOTE_AGENTS_FILE.exists():
|
|
23
|
+
return {}
|
|
24
|
+
try:
|
|
25
|
+
with open(REMOTE_AGENTS_FILE, "r", encoding="utf-8") as f:
|
|
26
|
+
return json.load(f)
|
|
27
|
+
except Exception:
|
|
28
|
+
return {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _save_remote_agents(data):
|
|
32
|
+
try:
|
|
33
|
+
with open(REMOTE_AGENTS_FILE, "w", encoding="utf-8") as f:
|
|
34
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def add_remote_agent(name, host, port, token, description=""):
|
|
40
|
+
"""添加远程 Agent 配置"""
|
|
41
|
+
data = _load_remote_agents()
|
|
42
|
+
data[name] = {
|
|
43
|
+
"host": host,
|
|
44
|
+
"port": int(port),
|
|
45
|
+
"token": token,
|
|
46
|
+
"description": description,
|
|
47
|
+
}
|
|
48
|
+
_save_remote_agents(data)
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def remove_remote_agent(name):
|
|
53
|
+
"""删除远程 Agent 配置"""
|
|
54
|
+
data = _load_remote_agents()
|
|
55
|
+
if name in data:
|
|
56
|
+
del data[name]
|
|
57
|
+
_save_remote_agents(data)
|
|
58
|
+
return True
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def list_remote_agents():
|
|
63
|
+
"""列出所有远程 Agent"""
|
|
64
|
+
return _load_remote_agents()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_remote_agent(name):
|
|
68
|
+
"""获取单个远程 Agent 配置"""
|
|
69
|
+
data = _load_remote_agents()
|
|
70
|
+
return data.get(name)
|
fr_cli/agent/server.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent HTTP 服务 —— 将分身能力发布为 Web API
|
|
3
|
+
供外部系统通过 REST 接口调用 Agent 的推理与执行能力。
|
|
4
|
+
使用 Python 标准库 http.server,无需额外依赖。
|
|
5
|
+
|
|
6
|
+
安全特性:
|
|
7
|
+
- 默认仅绑定 127.0.0.1(本地回环)
|
|
8
|
+
- 启动时自动生成随机 Token,所有请求需携带 Authorization: Bearer <token>
|
|
9
|
+
- CORS 限制为同源,不再开放 *
|
|
10
|
+
- 支持 IP 白名单(可选)
|
|
11
|
+
"""
|
|
12
|
+
import json
|
|
13
|
+
import secrets
|
|
14
|
+
import threading
|
|
15
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
16
|
+
from urllib.parse import urlparse
|
|
17
|
+
|
|
18
|
+
from fr_cli.agent.manager import list_agents, load_persona, load_memory, load_skills
|
|
19
|
+
from fr_cli.agent.workflow import load_workflow
|
|
20
|
+
from fr_cli.agent.executor import run_agent
|
|
21
|
+
from fr_cli.agent.workflow import run_workflow as wf_run
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _AgentHTTPHandler(BaseHTTPRequestHandler):
|
|
25
|
+
"""HTTP 请求处理器 —— 路由分发 + Token 认证 + CORS + IP 白名单"""
|
|
26
|
+
|
|
27
|
+
# 关闭默认日志输出(避免污染 CLI 界面)
|
|
28
|
+
def log_message(self, format, *args):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
def _check_auth(self):
|
|
32
|
+
"""校验请求是否携带正确的 Bearer Token"""
|
|
33
|
+
expected = getattr(self.server, "_token", None)
|
|
34
|
+
if not expected:
|
|
35
|
+
return True
|
|
36
|
+
auth = self.headers.get("Authorization", "")
|
|
37
|
+
if auth.startswith("Bearer ") and auth[7:] == expected:
|
|
38
|
+
return True
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
def _check_ip(self):
|
|
42
|
+
"""IP 白名单校验"""
|
|
43
|
+
whitelist = getattr(self.server, "_ip_whitelist", None)
|
|
44
|
+
if not whitelist:
|
|
45
|
+
return True
|
|
46
|
+
client_ip = self.client_address[0]
|
|
47
|
+
return client_ip in whitelist
|
|
48
|
+
|
|
49
|
+
def _send_cors_headers(self):
|
|
50
|
+
"""发送 CORS 响应头"""
|
|
51
|
+
allowed_origins = getattr(self.server, "_allowed_origins", [])
|
|
52
|
+
origin = self.headers.get("Origin", "")
|
|
53
|
+
if allowed_origins and origin in allowed_origins:
|
|
54
|
+
self.send_header("Access-Control-Allow-Origin", origin)
|
|
55
|
+
elif not allowed_origins:
|
|
56
|
+
# 未配置时默认只允许同源(不发送 CORS 头)
|
|
57
|
+
pass
|
|
58
|
+
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
59
|
+
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
60
|
+
|
|
61
|
+
def _send_json(self, status, data):
|
|
62
|
+
self.send_response(status)
|
|
63
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
64
|
+
self._send_cors_headers()
|
|
65
|
+
self.end_headers()
|
|
66
|
+
self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
|
|
67
|
+
|
|
68
|
+
def _read_json(self):
|
|
69
|
+
try:
|
|
70
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
71
|
+
if length:
|
|
72
|
+
body = self.rfile.read(length).decode("utf-8")
|
|
73
|
+
return json.loads(body)
|
|
74
|
+
return {}
|
|
75
|
+
except Exception:
|
|
76
|
+
return {}
|
|
77
|
+
|
|
78
|
+
def do_OPTIONS(self):
|
|
79
|
+
"""处理 CORS 预检请求"""
|
|
80
|
+
self.send_response(204)
|
|
81
|
+
self._send_cors_headers()
|
|
82
|
+
self.end_headers()
|
|
83
|
+
|
|
84
|
+
def do_GET(self):
|
|
85
|
+
if not self._check_ip():
|
|
86
|
+
self._send_json(403, {"error": "IP not allowed"})
|
|
87
|
+
return
|
|
88
|
+
if not self._check_auth():
|
|
89
|
+
self._send_json(401, {"error": "Unauthorized"})
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
parsed = urlparse(self.path)
|
|
93
|
+
path = parsed.path
|
|
94
|
+
parts = [p for p in path.split("/") if p]
|
|
95
|
+
|
|
96
|
+
# /health
|
|
97
|
+
if path == "/health":
|
|
98
|
+
self._send_json(200, {"status": "ok"})
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# /capabilities — 服务元数据与能力声明
|
|
102
|
+
if path == "/capabilities":
|
|
103
|
+
agents = list_agents()
|
|
104
|
+
self._send_json(200, {
|
|
105
|
+
"service": "fr-cli-agent-api",
|
|
106
|
+
"version": "2.1.0",
|
|
107
|
+
"agents": [
|
|
108
|
+
{
|
|
109
|
+
"name": a["name"],
|
|
110
|
+
"has_persona": a["has_persona"],
|
|
111
|
+
"has_memory": a["has_memory"],
|
|
112
|
+
"has_skills": a["has_skills"],
|
|
113
|
+
}
|
|
114
|
+
for a in agents
|
|
115
|
+
],
|
|
116
|
+
"endpoints": {
|
|
117
|
+
"list_agents": "GET /agents",
|
|
118
|
+
"agent_info": "GET /agents/<name>",
|
|
119
|
+
"agent_run": "POST /agents/<name>/run",
|
|
120
|
+
"agent_workflow": "POST /agents/<name>/workflow",
|
|
121
|
+
"capabilities": "GET /capabilities",
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
# /agents
|
|
127
|
+
if path == "/agents":
|
|
128
|
+
agents = list_agents()
|
|
129
|
+
self._send_json(200, {"agents": agents})
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# /agents/<name>
|
|
133
|
+
if len(parts) == 2 and parts[0] == "agents":
|
|
134
|
+
name = parts[1]
|
|
135
|
+
from fr_cli.agent.manager import agent_exists
|
|
136
|
+
if not agent_exists(name):
|
|
137
|
+
self._send_json(404, {"error": f"Agent not found: {name}"})
|
|
138
|
+
return
|
|
139
|
+
info = {
|
|
140
|
+
"name": name,
|
|
141
|
+
"persona": load_persona(name),
|
|
142
|
+
"memory": load_memory(name),
|
|
143
|
+
"skills": load_skills(name),
|
|
144
|
+
"has_workflow": load_workflow(name) is not None,
|
|
145
|
+
}
|
|
146
|
+
self._send_json(200, info)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
self._send_json(404, {"error": "Not found"})
|
|
150
|
+
|
|
151
|
+
def do_POST(self):
|
|
152
|
+
if not self._check_ip():
|
|
153
|
+
self._send_json(403, {"error": "IP not allowed"})
|
|
154
|
+
return
|
|
155
|
+
if not self._check_auth():
|
|
156
|
+
self._send_json(401, {"error": "Unauthorized"})
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
parsed = urlparse(self.path)
|
|
160
|
+
path = parsed.path
|
|
161
|
+
parts = [p for p in path.split("/") if p]
|
|
162
|
+
body = self._read_json()
|
|
163
|
+
|
|
164
|
+
# /agents/<name>/run
|
|
165
|
+
if len(parts) == 3 and parts[0] == "agents" and parts[2] == "run":
|
|
166
|
+
name = parts[1]
|
|
167
|
+
from fr_cli.agent.manager import agent_exists
|
|
168
|
+
if not agent_exists(name):
|
|
169
|
+
self._send_json(404, {"error": f"Agent not found: {name}"})
|
|
170
|
+
return
|
|
171
|
+
state = self.server._state
|
|
172
|
+
user_input = body.get("input", "")
|
|
173
|
+
kwargs = body.get("kwargs", {})
|
|
174
|
+
if user_input:
|
|
175
|
+
kwargs["user_input"] = user_input
|
|
176
|
+
result, error = run_agent(name, state, **kwargs)
|
|
177
|
+
resp = {"result": result, "error": error}
|
|
178
|
+
self._send_json(200 if not error else 500, resp)
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# /agents/<name>/workflow
|
|
182
|
+
if len(parts) == 3 and parts[0] == "agents" and parts[2] == "workflow":
|
|
183
|
+
name = parts[1]
|
|
184
|
+
from fr_cli.agent.manager import agent_exists
|
|
185
|
+
if not agent_exists(name):
|
|
186
|
+
self._send_json(404, {"error": f"Agent not found: {name}"})
|
|
187
|
+
return
|
|
188
|
+
state = self.server._state
|
|
189
|
+
user_input = body.get("input", "")
|
|
190
|
+
kwargs = body.get("kwargs", {})
|
|
191
|
+
final, error, steps = wf_run(name, state, user_input=user_input, **kwargs)
|
|
192
|
+
resp = {"result": final, "error": error, "steps": steps}
|
|
193
|
+
self._send_json(200 if not error else 500, resp)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
self._send_json(404, {"error": "Not found"})
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class AgentHTTPServer:
|
|
200
|
+
"""Agent HTTP 服务守护线程 —— 可启动、停止、查询状态"""
|
|
201
|
+
|
|
202
|
+
def __init__(self, state, host="127.0.0.1", port=17890):
|
|
203
|
+
self.state = state
|
|
204
|
+
self.host = host
|
|
205
|
+
self.port = port
|
|
206
|
+
self._server = None
|
|
207
|
+
self._thread = None
|
|
208
|
+
self._token = None
|
|
209
|
+
self._allowed_origins = []
|
|
210
|
+
self._ip_whitelist = []
|
|
211
|
+
|
|
212
|
+
def set_cors(self, origins):
|
|
213
|
+
"""设置允许的 CORS 来源(默认空列表=不允许跨域)"""
|
|
214
|
+
self._allowed_origins = origins or []
|
|
215
|
+
|
|
216
|
+
def set_ip_whitelist(self, ips):
|
|
217
|
+
"""设置 IP 白名单(默认空列表=不限制)"""
|
|
218
|
+
self._ip_whitelist = ips or []
|
|
219
|
+
if self._server is not None:
|
|
220
|
+
self._server._ip_whitelist = self._ip_whitelist
|
|
221
|
+
|
|
222
|
+
def start(self):
|
|
223
|
+
"""启动 HTTP 服务(后台线程)"""
|
|
224
|
+
if self.is_running():
|
|
225
|
+
return False, f"服务已在运行: http://{self.host}:{self.port}"
|
|
226
|
+
|
|
227
|
+
self._token = secrets.token_urlsafe(16)
|
|
228
|
+
self._server = HTTPServer((self.host, self.port), _AgentHTTPHandler)
|
|
229
|
+
self._server._state = self.state
|
|
230
|
+
self._server._token = self._token
|
|
231
|
+
self._server._allowed_origins = self._allowed_origins
|
|
232
|
+
self._server._ip_whitelist = self._ip_whitelist
|
|
233
|
+
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
|
|
234
|
+
self._thread.start()
|
|
235
|
+
msg = (
|
|
236
|
+
f"Agent HTTP 服务已启动: http://{self.host}:{self.port}\n"
|
|
237
|
+
f" Token: {self._token}\n"
|
|
238
|
+
f" 使用示例: curl -H 'Authorization: Bearer {self._token}' http://{self.host}:{self.port}/agents\n"
|
|
239
|
+
f" 能力声明: curl -H 'Authorization: Bearer {self._token}' http://{self.host}:{self.port}/capabilities"
|
|
240
|
+
)
|
|
241
|
+
if self.host == "127.0.0.1":
|
|
242
|
+
msg += "\n ⚠️ 当前仅绑定 127.0.0.1,外部无法访问。如需公网暴露请使用 ngrok 或修改 host。"
|
|
243
|
+
return True, msg
|
|
244
|
+
|
|
245
|
+
def stop(self):
|
|
246
|
+
"""停止 HTTP 服务"""
|
|
247
|
+
if not self.is_running():
|
|
248
|
+
return False, "服务未运行"
|
|
249
|
+
self._server.shutdown()
|
|
250
|
+
self._server.server_close()
|
|
251
|
+
self._server = None
|
|
252
|
+
self._thread = None
|
|
253
|
+
self._token = None
|
|
254
|
+
return True, "Agent HTTP 服务已停止"
|
|
255
|
+
|
|
256
|
+
def is_running(self):
|
|
257
|
+
return self._server is not None and self._thread is not None and self._thread.is_alive()
|
|
258
|
+
|
|
259
|
+
def status(self):
|
|
260
|
+
if self.is_running():
|
|
261
|
+
return f"运行中: http://{self.host}:{self.port} (Token: {self._token})"
|
|
262
|
+
return "未运行"
|
|
263
|
+
|
|
264
|
+
def get_publish_info(self):
|
|
265
|
+
"""获取对外发布的连接信息"""
|
|
266
|
+
if not self.is_running():
|
|
267
|
+
return None
|
|
268
|
+
import socket
|
|
269
|
+
hostname = socket.gethostname()
|
|
270
|
+
try:
|
|
271
|
+
local_ip = socket.getaddrinfo(hostname, None)[0][4][0]
|
|
272
|
+
except Exception:
|
|
273
|
+
local_ip = "127.0.0.1"
|
|
274
|
+
return {
|
|
275
|
+
"url": f"http://{self.host}:{self.port}",
|
|
276
|
+
"token": self._token,
|
|
277
|
+
"local_ip": local_ip,
|
|
278
|
+
"hostname": hostname,
|
|
279
|
+
}
|
fr_cli/agent/workflow.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Agent workflow engine
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
WORKFLOW_FILE = "workflow.md"
|
|
9
|
+
|
|
10
|
+
def load_workflow(name):
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from fr_cli.agent.manager import _agent_dir
|
|
13
|
+
p = _agent_dir(name) / WORKFLOW_FILE
|
|
14
|
+
if not p.exists():
|
|
15
|
+
return None
|
|
16
|
+
return p.read_text(encoding="utf-8")
|
|
17
|
+
|
|
18
|
+
def save_workflow(name, content):
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from fr_cli.agent.manager import _agent_dir
|
|
21
|
+
p = _agent_dir(name) / WORKFLOW_FILE
|
|
22
|
+
p.write_text(content, encoding="utf-8")
|
|
23
|
+
|
|
24
|
+
def parse_workflow(text):
|
|
25
|
+
steps = []
|
|
26
|
+
sections = re.split(r"\n## ", text)
|
|
27
|
+
for sec in sections[1:]:
|
|
28
|
+
lines = sec.strip().split("\n")
|
|
29
|
+
title_line = lines[0].strip()
|
|
30
|
+
m = re.match(r"步骤?(\d+)[\s:\-\.]+(.+)", title_line, re.I)
|
|
31
|
+
if m:
|
|
32
|
+
step_num = int(m.group(1))
|
|
33
|
+
step_title = m.group(2).strip()
|
|
34
|
+
else:
|
|
35
|
+
step_num = len(steps) + 1
|
|
36
|
+
step_title = title_line
|
|
37
|
+
action = ""
|
|
38
|
+
params = {}
|
|
39
|
+
in_params = False
|
|
40
|
+
for line in lines[1:]:
|
|
41
|
+
line = line.rstrip()
|
|
42
|
+
if not line:
|
|
43
|
+
continue
|
|
44
|
+
am = re.match(r"-\s+\*\*action\*\*\s*:\s*(.+)", line, re.I)
|
|
45
|
+
if am:
|
|
46
|
+
action = am.group(1).strip()
|
|
47
|
+
continue
|
|
48
|
+
if re.match(r"-\s+\*\*params\*\*\s*:", line, re.I):
|
|
49
|
+
in_params = True
|
|
50
|
+
continue
|
|
51
|
+
if in_params:
|
|
52
|
+
pm = re.match(r"\s+-\s+([\w_]+)\s*:\s*(.+)", line)
|
|
53
|
+
if pm:
|
|
54
|
+
params[pm.group(1)] = pm.group(2).strip()
|
|
55
|
+
if action:
|
|
56
|
+
steps.append({"num": step_num, "title": step_title, "action": action, "params": params})
|
|
57
|
+
steps.sort(key=lambda x: x["num"])
|
|
58
|
+
return steps
|
|
59
|
+
|
|
60
|
+
def _resolve_var(var_expr, context, step_results, user_input):
|
|
61
|
+
"""解析模板变量,如 {{step1.result}} {{user_input}}"""
|
|
62
|
+
var_expr = var_expr.strip()
|
|
63
|
+
if var_expr == "user_input":
|
|
64
|
+
return user_input or ""
|
|
65
|
+
if var_expr == "agent.persona":
|
|
66
|
+
return context.get("persona", "")
|
|
67
|
+
if var_expr == "agent.memory":
|
|
68
|
+
return context.get("memory", "")
|
|
69
|
+
if var_expr == "agent.skills":
|
|
70
|
+
return context.get("skills", "")
|
|
71
|
+
sm = re.match(r"step(\d+)\.result", var_expr, re.I)
|
|
72
|
+
if sm:
|
|
73
|
+
idx = int(sm.group(1)) - 1
|
|
74
|
+
if 0 <= idx < len(step_results):
|
|
75
|
+
return str(step_results[idx].get("result", ""))
|
|
76
|
+
sm = re.match(r"step(\d+)\.error", var_expr, re.I)
|
|
77
|
+
if sm:
|
|
78
|
+
idx = int(sm.group(1)) - 1
|
|
79
|
+
if 0 <= idx < len(step_results):
|
|
80
|
+
return str(step_results[idx].get("error", ""))
|
|
81
|
+
return "{" + var_expr + "}"
|
|
82
|
+
|
|
83
|
+
def _substitute_vars(text, context, step_results, user_input):
|
|
84
|
+
"""替换文本中的所有 {{var}} 模板变量"""
|
|
85
|
+
if not isinstance(text, str):
|
|
86
|
+
return text
|
|
87
|
+
def repl(m):
|
|
88
|
+
return _resolve_var(m.group(1), context, step_results, user_input)
|
|
89
|
+
return re.sub(r"\{\{([^}]+)\}\}", repl, text)
|
|
90
|
+
|
|
91
|
+
def run_workflow(name, state, user_input=None, **kwargs):
|
|
92
|
+
"""执行 Agent 的工作流。返回 (final_result, error, step_results)"""
|
|
93
|
+
wf_text = load_workflow(name)
|
|
94
|
+
if not wf_text:
|
|
95
|
+
return None, "工作流不存在,使用 /agent_edit <name> workflow 定义工作流", []
|
|
96
|
+
steps = parse_workflow(wf_text)
|
|
97
|
+
if not steps:
|
|
98
|
+
return None, "工作流为空或解析失败", []
|
|
99
|
+
|
|
100
|
+
persona = load_persona(name)
|
|
101
|
+
memory = load_memory(name)
|
|
102
|
+
skills = load_skills(name)
|
|
103
|
+
|
|
104
|
+
context = {
|
|
105
|
+
"persona": persona,
|
|
106
|
+
"memory": memory,
|
|
107
|
+
"skills": skills,
|
|
108
|
+
"client": state.client,
|
|
109
|
+
"model": state.model_name,
|
|
110
|
+
"lang": state.lang,
|
|
111
|
+
"executor": state.executor,
|
|
112
|
+
"state": state,
|
|
113
|
+
"agent_name": name,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
step_results = []
|
|
117
|
+
for step in steps:
|
|
118
|
+
action = step["action"]
|
|
119
|
+
params = {k: _substitute_vars(v, context, step_results, user_input) for k, v in step["params"].items()}
|
|
120
|
+
result = None
|
|
121
|
+
error = None
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
if action in ("invoke_tool", "tool"):
|
|
125
|
+
tool_name = params.pop("tool", list(params.keys())[0] if params else "")
|
|
126
|
+
tool_params = params
|
|
127
|
+
result, error = state.executor.invoke_tool(tool_name, tool_params)
|
|
128
|
+
elif action in ("execute_cmd", "cmd", "command"):
|
|
129
|
+
cmd_str = params.get("cmd", "")
|
|
130
|
+
result, error = state.executor.execute(cmd_str)
|
|
131
|
+
elif action in ("agent_call", "agent", "call_agent"):
|
|
132
|
+
target = params.get("target") or params.get("agent") or params.get("to")
|
|
133
|
+
message = params.get("message", "")
|
|
134
|
+
result, error = run_agent(target, state, pipeline_input=message, **kwargs)
|
|
135
|
+
elif action in ("ai_generate", "ai", "generate", "ask"):
|
|
136
|
+
prompt = params.get("prompt", "")
|
|
137
|
+
from fr_cli.core.stream import stream_cnt
|
|
138
|
+
msgs = [{"role": "user", "content": prompt}]
|
|
139
|
+
result, _, _ = stream_cnt(state.client, state.model_name, msgs, state.lang)
|
|
140
|
+
elif action in ("save_memory", "memory_append"):
|
|
141
|
+
mem = params.get("content", "")
|
|
142
|
+
from fr_cli.agent.manager import save_memory, load_memory
|
|
143
|
+
old = load_memory(name)
|
|
144
|
+
save_memory(name, old + "\n" + mem if old else mem)
|
|
145
|
+
result = "记忆已更新"
|
|
146
|
+
else:
|
|
147
|
+
error = f"未知动作: {action}"
|
|
148
|
+
except Exception as e:
|
|
149
|
+
error = str(e)
|
|
150
|
+
|
|
151
|
+
step_results.append({
|
|
152
|
+
"step": step["num"],
|
|
153
|
+
"title": step["title"],
|
|
154
|
+
"action": action,
|
|
155
|
+
"result": result,
|
|
156
|
+
"error": error,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if error:
|
|
160
|
+
return None, f"步骤 {step['num']} ({step['title']}) 失败: {error}", step_results
|
|
161
|
+
|
|
162
|
+
final_result = step_results[-1]["result"] if step_results else None
|
|
163
|
+
return final_result, None, step_results
|
|
164
|
+
|