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
fr_cli/core/thinking.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
思维引擎 —— 造化推演
|
|
3
|
+
支持 CoT(思维链)、ToT(思维树)、ReAct(推理+行动)三种思维模式。
|
|
4
|
+
在最终回答前,让大模型按照人类思维范式进行问题拆解和自我反馈验证。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
COT_PROMPT_ZH = """你是一个深度思考专家。用户提出了以下问题,请严格按照以下步骤进行完整的思维推演:
|
|
8
|
+
|
|
9
|
+
步骤1 — 问题拆解:将用户问题分解为若干个独立的子问题,明确每个子问题的核心诉求。
|
|
10
|
+
步骤2 — 信息评估:列出已知信息、缺失信息、以及可能需要验证的假设。
|
|
11
|
+
步骤3 — 路径探索:针对每个子问题,提出至少两种解决思路,并简要说明各自的优劣。
|
|
12
|
+
步骤4 — 方案筛选:综合评估后,选择最优解决路径,并说明选择理由。
|
|
13
|
+
步骤5 — 自我验证:以批判者视角审视你的推理链,检查是否存在逻辑漏洞、过度推断或遗漏的边界情况。
|
|
14
|
+
步骤6 — 风险兜底:如果最优方案失败,备选方案是什么?
|
|
15
|
+
|
|
16
|
+
要求:
|
|
17
|
+
- 每个步骤必须独立成段,标题明确
|
|
18
|
+
- 推理要具体、可执行,不要空洞的概括
|
|
19
|
+
- 最后用一个简短的"结论摘要"总结核心思路
|
|
20
|
+
|
|
21
|
+
用户问题:{question}
|
|
22
|
+
|
|
23
|
+
请开始深度思考:"""
|
|
24
|
+
|
|
25
|
+
COT_PROMPT_EN = """You are a deep-thinking expert. The user has asked the following question. Please conduct a complete chain-of-thought reasoning strictly following these steps:
|
|
26
|
+
|
|
27
|
+
Step 1 — Problem Decomposition: Break the user's question into independent sub-problems, clarifying the core need of each.
|
|
28
|
+
Step 2 — Information Assessment: List known facts, missing information, and assumptions that need verification.
|
|
29
|
+
Step 3 — Path Exploration: For each sub-problem, propose at least two solution approaches with brief pros/cons.
|
|
30
|
+
Step 4 — Solution Selection: Choose the optimal path with justification.
|
|
31
|
+
Step 5 — Self-Verification: Review your reasoning chain critically for logical gaps, over-inference, or overlooked edge cases.
|
|
32
|
+
Step 6 — Risk Mitigation: If the optimal solution fails, what is the fallback plan?
|
|
33
|
+
|
|
34
|
+
Requirements:
|
|
35
|
+
- Each step must be a separate paragraph with a clear heading
|
|
36
|
+
- Reasoning must be concrete and actionable, not vague summaries
|
|
37
|
+
- End with a brief "Conclusion Summary"
|
|
38
|
+
|
|
39
|
+
User Question: {question}
|
|
40
|
+
|
|
41
|
+
Begin deep thinking:"""
|
|
42
|
+
|
|
43
|
+
TOT_PROMPT_ZH = """你是一个战略思维专家。请使用思维树(Tree of Thought)方法对用户问题进行系统性分析。
|
|
44
|
+
|
|
45
|
+
思维树构建规则:
|
|
46
|
+
1. 【根节点】提炼问题的核心目标(用一句话概括)
|
|
47
|
+
2. 【第一层分支】生成至少3个不同的解决策略(标注为 策略A、策略B、策略C)
|
|
48
|
+
3. 【第二层分支】对每个策略进行至少两层深入展开(子策略/具体步骤)
|
|
49
|
+
4. 【节点评估】对每个叶子节点从以下维度评分(1-5分):
|
|
50
|
+
- 可行性(技术/资源是否允许)
|
|
51
|
+
- 准确性(是否能真正解决问题)
|
|
52
|
+
- 效率(时间/成本开销)
|
|
53
|
+
5. 【路径选择】选择总分最高的完整路径作为最优方案
|
|
54
|
+
6. 【反向验证】假设最优路径因某个节点失败而中断,从该节点的父节点出发,选择次优分支作为备选
|
|
55
|
+
7. 【收敛总结】用一句话总结最终推荐的行动方案
|
|
56
|
+
|
|
57
|
+
要求:
|
|
58
|
+
- 以树状层级格式输出(使用缩进或 ├── 符号)
|
|
59
|
+
- 评分必须给出具体理由,不能只有数字
|
|
60
|
+
- 最后明确标注"推荐方案"和"备选方案"
|
|
61
|
+
|
|
62
|
+
用户问题:{question}
|
|
63
|
+
|
|
64
|
+
请构建思维树:"""
|
|
65
|
+
|
|
66
|
+
TOT_PROMPT_EN = """You are a strategic thinking expert. Please conduct a systematic analysis of the user's question using the Tree of Thought methodology.
|
|
67
|
+
|
|
68
|
+
Tree Construction Rules:
|
|
69
|
+
1. [Root Node] Distill the core objective in one sentence
|
|
70
|
+
2. [Level-1 Branches] Generate at least 3 distinct strategies (labeled Strategy A, B, C)
|
|
71
|
+
3. [Level-2 Branches] Expand each strategy at least two levels deep (sub-strategies / concrete steps)
|
|
72
|
+
4. [Node Evaluation] Score each leaf node on (1-5):
|
|
73
|
+
- Feasibility (technical/resource constraints)
|
|
74
|
+
- Accuracy (does it truly solve the problem)
|
|
75
|
+
- Efficiency (time/cost overhead)
|
|
76
|
+
5. [Path Selection] Choose the highest-scoring complete path as optimal
|
|
77
|
+
6. [Backtracking] If the optimal path fails at any node, select the next-best branch from the parent node as fallback
|
|
78
|
+
7. [Convergence] Summarize the final recommended action in one sentence
|
|
79
|
+
|
|
80
|
+
Requirements:
|
|
81
|
+
- Output in tree hierarchy format (use indentation or ├── symbols)
|
|
82
|
+
- Scores must include rationale, not just numbers
|
|
83
|
+
- Clearly label "Recommended Plan" and "Fallback Plan"
|
|
84
|
+
|
|
85
|
+
User Question: {question}
|
|
86
|
+
|
|
87
|
+
Construct the thought tree:"""
|
|
88
|
+
|
|
89
|
+
REACT_SYSTEM_ENHANCEMENT_ZH = """
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
【ReAct 推演模式已激活】
|
|
93
|
+
在回答用户问题时,请严格遵循 ReAct(Reasoning + Acting)范式:
|
|
94
|
+
|
|
95
|
+
1. Thought(观察与思考):
|
|
96
|
+
- 先观察用户问题的核心诉求
|
|
97
|
+
- 明确当前已掌握的信息和缺失的信息
|
|
98
|
+
- 思考下一步应该采取什么行动来获取信息或解决问题
|
|
99
|
+
- 每轮思考后进行自我检查:"我的推理是否存在漏洞?"
|
|
100
|
+
|
|
101
|
+
2. Action(行动):
|
|
102
|
+
- 如果需要调用工具,使用标准格式:【调用:tool_name({"参数": "值"})】
|
|
103
|
+
- 如果信息足够,直接给出答案
|
|
104
|
+
- 如果信息不足,明确说明需要什么额外信息
|
|
105
|
+
|
|
106
|
+
3. Observation(观察结果):
|
|
107
|
+
- 工具执行结果会自动反馈给你
|
|
108
|
+
- 基于新信息更新你的思考
|
|
109
|
+
|
|
110
|
+
4. Final Answer(最终答案):
|
|
111
|
+
- 只有当所有子问题都解决、所有验证都通过后,才给出最终答案
|
|
112
|
+
- 最终答案前必须包含一行:"✅ 验证通过,以下是最终结论:"
|
|
113
|
+
|
|
114
|
+
重要:
|
|
115
|
+
- 不要在 Thought 中编造不存在的信息
|
|
116
|
+
- 每个 Action 后等待 Observation 再继续
|
|
117
|
+
- 如果某个路径失败,必须回溯并尝试替代方案
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
REACT_SYSTEM_ENHANCEMENT_EN = """
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
[ReAct Mode Activated]
|
|
124
|
+
When answering the user's question, strictly follow the ReAct (Reasoning + Acting) paradigm:
|
|
125
|
+
|
|
126
|
+
1. Thought (Observation & Reasoning):
|
|
127
|
+
- Observe the core intent of the user's question
|
|
128
|
+
- Identify what information is known and what is missing
|
|
129
|
+
- Decide the next action to acquire information or solve the problem
|
|
130
|
+
- After each round of reasoning, self-check: "Does my reasoning have any flaws?"
|
|
131
|
+
|
|
132
|
+
2. Action:
|
|
133
|
+
- If a tool is needed, use the standard format: 【调用:tool_name({"param": "value"})】
|
|
134
|
+
- If information is sufficient, provide the answer directly
|
|
135
|
+
- If information is insufficient, clearly state what additional info is needed
|
|
136
|
+
|
|
137
|
+
3. Observation:
|
|
138
|
+
- Tool execution results will be fed back to you automatically
|
|
139
|
+
- Update your reasoning based on new information
|
|
140
|
+
|
|
141
|
+
4. Final Answer:
|
|
142
|
+
- Only provide the final answer after all sub-problems are resolved and all checks pass
|
|
143
|
+
- Must include a line before the final answer: "✅ Verification passed. Final conclusion:"
|
|
144
|
+
|
|
145
|
+
Important:
|
|
146
|
+
- Do not fabricate information in Thought
|
|
147
|
+
- Wait for Observation after each Action
|
|
148
|
+
- If a path fails, backtrack and try an alternative
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ThinkingEngine:
|
|
153
|
+
"""思维引擎 —— 支持 CoT / ToT / ReAct / direct 四种模式"""
|
|
154
|
+
|
|
155
|
+
MODES = ["direct", "cot", "tot", "react"]
|
|
156
|
+
|
|
157
|
+
def __init__(self):
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def is_valid_mode(mode):
|
|
162
|
+
return mode in ThinkingEngine.MODES
|
|
163
|
+
|
|
164
|
+
def analyze(self, state, user_input, mode, intent, lang="zh"):
|
|
165
|
+
"""
|
|
166
|
+
根据思维模式对用户问题进行预处理分析。
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
reasoning_text (str or None): 对于 cot/tot,返回思维推演文本;
|
|
170
|
+
对于 react,返回 system prompt 增强片段;
|
|
171
|
+
对于 direct,返回 None。
|
|
172
|
+
"""
|
|
173
|
+
if mode == "direct" or mode not in self.MODES:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
if mode == "cot":
|
|
177
|
+
return self._run_cot(state, user_input, lang)
|
|
178
|
+
elif mode == "tot":
|
|
179
|
+
return self._run_tot(state, user_input, lang)
|
|
180
|
+
elif mode == "react":
|
|
181
|
+
return self._get_react_enhancement(lang)
|
|
182
|
+
|
|
183
|
+
def _run_cot(self, state, user_input, lang):
|
|
184
|
+
"""执行思维链推演(额外一次 LLM 调用)"""
|
|
185
|
+
from fr_cli.core.stream import stream_cnt
|
|
186
|
+
|
|
187
|
+
prompt_template = COT_PROMPT_ZH if lang == "zh" else COT_PROMPT_EN
|
|
188
|
+
prompt = prompt_template.format(question=user_input)
|
|
189
|
+
messages = [{"role": "user", "content": prompt}]
|
|
190
|
+
|
|
191
|
+
txt, _, _ = stream_cnt(
|
|
192
|
+
state.client, state.model_name, messages, lang,
|
|
193
|
+
custom_prefix="", max_tokens=2048, silent=True
|
|
194
|
+
)
|
|
195
|
+
return txt.strip() if txt else None
|
|
196
|
+
|
|
197
|
+
def _run_tot(self, state, user_input, lang):
|
|
198
|
+
"""执行思维树推演(额外一次 LLM 调用)"""
|
|
199
|
+
from fr_cli.core.stream import stream_cnt
|
|
200
|
+
|
|
201
|
+
prompt_template = TOT_PROMPT_ZH if lang == "zh" else TOT_PROMPT_EN
|
|
202
|
+
prompt = prompt_template.format(question=user_input)
|
|
203
|
+
messages = [{"role": "user", "content": prompt}]
|
|
204
|
+
|
|
205
|
+
txt, _, _ = stream_cnt(
|
|
206
|
+
state.client, state.model_name, messages, lang,
|
|
207
|
+
custom_prefix="", max_tokens=2048, silent=True
|
|
208
|
+
)
|
|
209
|
+
return txt.strip() if txt else None
|
|
210
|
+
|
|
211
|
+
def _get_react_enhancement(self, lang):
|
|
212
|
+
"""获取 ReAct system prompt 增强片段(不额外调用 LLM)"""
|
|
213
|
+
if lang == "zh":
|
|
214
|
+
return REACT_SYSTEM_ENHANCEMENT_ZH
|
|
215
|
+
return REACT_SYSTEM_ENHANCEMENT_EN
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gatekeeper 守护进程 —— 后台结界主宰
|
|
3
|
+
负责在主进程退出后继续维持核心服务运转。
|
|
4
|
+
支持:Agent HTTP 服务、全局定时任务(shell/agent)、配置热重载。
|
|
5
|
+
|
|
6
|
+
启动方式(不应由用户直接调用):
|
|
7
|
+
python -m fr_cli.gatekeeper.daemon
|
|
8
|
+
|
|
9
|
+
停止方式:
|
|
10
|
+
创建 ~/.fr_cli_gatekeeper.stop 标记文件,守护进程检测到后自行退出。
|
|
11
|
+
"""
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
import json
|
|
16
|
+
import signal
|
|
17
|
+
import atexit
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# 确保项目根目录在 Python 路径中
|
|
21
|
+
_project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
22
|
+
if _project_root not in sys.path:
|
|
23
|
+
sys.path.insert(0, _project_root)
|
|
24
|
+
|
|
25
|
+
PID_FILE = Path.home() / ".fr_cli_gatekeeper.pid"
|
|
26
|
+
STOP_FILE = Path.home() / ".fr_cli_gatekeeper.stop"
|
|
27
|
+
DAEMON_CONFIG_FILE = Path.home() / ".fr_cli_gatekeeper.json"
|
|
28
|
+
|
|
29
|
+
# 配置热重载间隔(秒)
|
|
30
|
+
RELOAD_INTERVAL = 30
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _write_pid(pid):
|
|
34
|
+
try:
|
|
35
|
+
with open(PID_FILE, "w", encoding="utf-8") as f:
|
|
36
|
+
f.write(str(pid))
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _clear_stop_marker():
|
|
42
|
+
if STOP_FILE.exists():
|
|
43
|
+
try:
|
|
44
|
+
STOP_FILE.unlink()
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _cleanup():
|
|
50
|
+
_clear_stop_marker()
|
|
51
|
+
if PID_FILE.exists():
|
|
52
|
+
try:
|
|
53
|
+
PID_FILE.unlink()
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _setup_signal_handlers():
|
|
59
|
+
def _sigterm_handler(signum, frame):
|
|
60
|
+
_cleanup()
|
|
61
|
+
sys.exit(0)
|
|
62
|
+
|
|
63
|
+
signal.signal(signal.SIGTERM, _sigterm_handler)
|
|
64
|
+
if hasattr(signal, "SIGINT"):
|
|
65
|
+
signal.signal(signal.SIGINT, _sigterm_handler)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _load_daemon_config():
|
|
69
|
+
if DAEMON_CONFIG_FILE.exists():
|
|
70
|
+
try:
|
|
71
|
+
with open(DAEMON_CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
72
|
+
return json.load(f)
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
return {}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _reload_cron_jobs(cron_mgr, daemon_cfg, state):
|
|
79
|
+
"""热重载定时任务配置:对比当前运行任务与配置文件,增删同步。"""
|
|
80
|
+
cfg_jobs = daemon_cfg.get("cron_jobs", [])
|
|
81
|
+
# 过滤出 shell 类型的全局定时任务
|
|
82
|
+
shell_jobs = [j for j in cfg_jobs if j.get("job_type", "shell") == "shell"]
|
|
83
|
+
cron_mgr.sync_jobs(shell_jobs, lang=daemon_cfg.get("lang", "zh"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _reload_agent_crons(cron_mgr, daemon_cfg, state):
|
|
87
|
+
"""热重载 Agent 分身定时任务:对比当前运行任务与配置文件,增删同步。"""
|
|
88
|
+
import copy
|
|
89
|
+
cfg_jobs = daemon_cfg.get("agent_crons", [])
|
|
90
|
+
# 深拷贝避免修改原始配置 dict,防止无限重载循环
|
|
91
|
+
cfg_jobs = copy.deepcopy(cfg_jobs)
|
|
92
|
+
for j in cfg_jobs:
|
|
93
|
+
j["job_type"] = "agent"
|
|
94
|
+
cron_mgr.sync_jobs(cfg_jobs, lang=daemon_cfg.get("lang", "zh"), state=state)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _init_services(daemon_cfg):
|
|
98
|
+
"""初始化并启动核心子系统"""
|
|
99
|
+
from fr_cli.conf.config import load_config
|
|
100
|
+
from fr_cli.core.core import AppState
|
|
101
|
+
from fr_cli.agent.server import AgentHTTPServer
|
|
102
|
+
from fr_cli.weapon.cron import CronManager
|
|
103
|
+
|
|
104
|
+
# 加载用户主配置(不触发交互式向导)
|
|
105
|
+
cfg = load_config()
|
|
106
|
+
state = AppState(cfg)
|
|
107
|
+
|
|
108
|
+
services = {
|
|
109
|
+
"state": state,
|
|
110
|
+
"agent_server": None,
|
|
111
|
+
"cron_manager": None,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# 启动 Agent HTTP 服务
|
|
115
|
+
agent_port = daemon_cfg.get("agent_server_port")
|
|
116
|
+
if agent_port:
|
|
117
|
+
try:
|
|
118
|
+
agent_port = int(agent_port)
|
|
119
|
+
agent_server = AgentHTTPServer(state, port=agent_port)
|
|
120
|
+
ok, msg = agent_server.start()
|
|
121
|
+
if ok:
|
|
122
|
+
services["agent_server"] = agent_server
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
# 初始化 CronManager 并恢复所有定时任务
|
|
127
|
+
cron_mgr = CronManager()
|
|
128
|
+
|
|
129
|
+
# 恢复 shell 类型全局定时任务
|
|
130
|
+
cron_jobs = daemon_cfg.get("cron_jobs", [])
|
|
131
|
+
for job in cron_jobs:
|
|
132
|
+
try:
|
|
133
|
+
jtype = job.get("job_type", "shell")
|
|
134
|
+
if jtype == "shell":
|
|
135
|
+
cron_mgr.add_job(
|
|
136
|
+
cmd=job["cmd"],
|
|
137
|
+
interval=job["interval"],
|
|
138
|
+
lang=job.get("lang", "zh"),
|
|
139
|
+
job_type="shell",
|
|
140
|
+
)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
# 恢复 Agent 分身定时任务(需要 AppState)
|
|
145
|
+
agent_crons = daemon_cfg.get("agent_crons", [])
|
|
146
|
+
for job in agent_crons:
|
|
147
|
+
try:
|
|
148
|
+
cron_mgr.add_job(
|
|
149
|
+
cmd=job.get("cmd", job.get("agent_name", "")),
|
|
150
|
+
interval=job["interval"],
|
|
151
|
+
lang=job.get("lang", "zh"),
|
|
152
|
+
job_type="agent",
|
|
153
|
+
agent_name=job.get("agent_name"),
|
|
154
|
+
agent_input=job.get("agent_input", ""),
|
|
155
|
+
state=state,
|
|
156
|
+
)
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
services["cron_manager"] = cron_mgr
|
|
161
|
+
return services
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def run_daemon():
|
|
165
|
+
"""守护进程主循环"""
|
|
166
|
+
_clear_stop_marker()
|
|
167
|
+
_write_pid(os.getpid())
|
|
168
|
+
atexit.register(_cleanup)
|
|
169
|
+
_setup_signal_handlers()
|
|
170
|
+
|
|
171
|
+
daemon_cfg = _load_daemon_config()
|
|
172
|
+
services = _init_services(daemon_cfg)
|
|
173
|
+
cron_mgr = services["cron_manager"]
|
|
174
|
+
state = services["state"]
|
|
175
|
+
|
|
176
|
+
last_reload = time.time()
|
|
177
|
+
|
|
178
|
+
# 主循环:定期检查停止标记 + 热重载配置
|
|
179
|
+
while True:
|
|
180
|
+
time.sleep(2)
|
|
181
|
+
|
|
182
|
+
if STOP_FILE.exists():
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
# 每 RELOAD_INTERVAL 秒热重载一次配置
|
|
186
|
+
if time.time() - last_reload >= RELOAD_INTERVAL:
|
|
187
|
+
last_reload = time.time()
|
|
188
|
+
new_cfg = _load_daemon_config()
|
|
189
|
+
if new_cfg != daemon_cfg:
|
|
190
|
+
daemon_cfg = new_cfg
|
|
191
|
+
_reload_cron_jobs(cron_mgr, daemon_cfg, state)
|
|
192
|
+
_reload_agent_crons(cron_mgr, daemon_cfg, state)
|
|
193
|
+
|
|
194
|
+
# 停止所有子服务
|
|
195
|
+
agent_server = services.get("agent_server")
|
|
196
|
+
if agent_server and getattr(agent_server, "is_running", lambda: False)():
|
|
197
|
+
try:
|
|
198
|
+
agent_server.stop()
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
# 取消所有定时任务
|
|
203
|
+
cron_mgr = services.get("cron_manager")
|
|
204
|
+
if cron_mgr:
|
|
205
|
+
for job in list(cron_mgr.jobs):
|
|
206
|
+
if job.get("timer"):
|
|
207
|
+
try:
|
|
208
|
+
job["timer"].cancel()
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
_cleanup()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
run_daemon()
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gatekeeper 管理器 —— 结界主宰
|
|
3
|
+
在主进程中控制守护进程的启动、停止与状态查询。
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import json
|
|
9
|
+
import signal
|
|
10
|
+
import subprocess
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
PID_FILE = Path.home() / ".fr_cli_gatekeeper.pid"
|
|
14
|
+
STOP_FILE = Path.home() / ".fr_cli_gatekeeper.stop"
|
|
15
|
+
DAEMON_CONFIG_FILE = Path.home() / ".fr_cli_gatekeeper.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GatekeeperManager:
|
|
19
|
+
"""守护进程管理器"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def _daemon_script_path():
|
|
26
|
+
return Path(__file__).with_name("daemon.py")
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def _read_pid():
|
|
30
|
+
if PID_FILE.exists():
|
|
31
|
+
try:
|
|
32
|
+
return int(PID_FILE.read_text(encoding="utf-8").strip())
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _is_pid_alive(pid):
|
|
39
|
+
"""跨平台检测进程是否存活"""
|
|
40
|
+
try:
|
|
41
|
+
if sys.platform == "win32":
|
|
42
|
+
import ctypes
|
|
43
|
+
kernel32 = ctypes.windll.kernel32
|
|
44
|
+
handle = kernel32.OpenProcess(1, False, pid)
|
|
45
|
+
if handle:
|
|
46
|
+
kernel32.CloseHandle(handle)
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
else:
|
|
50
|
+
os.kill(pid, 0)
|
|
51
|
+
return True
|
|
52
|
+
except (OSError, ProcessLookupError):
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _cleanup_files():
|
|
57
|
+
for f in (PID_FILE, STOP_FILE):
|
|
58
|
+
if f.exists():
|
|
59
|
+
try:
|
|
60
|
+
f.unlink()
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def save_daemon_config(cfg):
|
|
66
|
+
"""保存守护进程配置供下次启动使用"""
|
|
67
|
+
try:
|
|
68
|
+
with open(DAEMON_CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
69
|
+
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
return False, str(e)
|
|
72
|
+
return True, "配置已保存"
|
|
73
|
+
|
|
74
|
+
def is_running(self):
|
|
75
|
+
pid = self._read_pid()
|
|
76
|
+
if pid and self._is_pid_alive(pid):
|
|
77
|
+
return True
|
|
78
|
+
# 残留文件清理
|
|
79
|
+
if PID_FILE.exists():
|
|
80
|
+
self._cleanup_files()
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
def start(self):
|
|
84
|
+
"""启动守护进程"""
|
|
85
|
+
if self.is_running():
|
|
86
|
+
pid = self._read_pid()
|
|
87
|
+
return False, f"Gatekeeper 守护进程已在运行 (PID: {pid})"
|
|
88
|
+
|
|
89
|
+
self._cleanup_files()
|
|
90
|
+
daemon_script = self._daemon_script_path()
|
|
91
|
+
if not daemon_script.exists():
|
|
92
|
+
return False, f"守护进程脚本不存在: {daemon_script}"
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
kwargs = {}
|
|
96
|
+
if sys.platform == "win32":
|
|
97
|
+
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
98
|
+
|
|
99
|
+
proc = subprocess.Popen(
|
|
100
|
+
[sys.executable, str(daemon_script)],
|
|
101
|
+
stdout=subprocess.DEVNULL,
|
|
102
|
+
stderr=subprocess.DEVNULL,
|
|
103
|
+
stdin=subprocess.DEVNULL,
|
|
104
|
+
close_fds=True,
|
|
105
|
+
**kwargs
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# 等待 PID 文件写入
|
|
109
|
+
for _ in range(10):
|
|
110
|
+
time.sleep(0.3)
|
|
111
|
+
pid = self._read_pid()
|
|
112
|
+
if pid and self._is_pid_alive(pid):
|
|
113
|
+
return True, f"Gatekeeper 守护进程已启动 (PID: {pid})"
|
|
114
|
+
if proc.poll() is not None:
|
|
115
|
+
return False, "守护进程启动后立即退出,请检查配置。"
|
|
116
|
+
|
|
117
|
+
return True, f"Gatekeeper 守护进程已启动 (PID: {proc.pid})"
|
|
118
|
+
except Exception as e:
|
|
119
|
+
return False, f"启动失败: {e}"
|
|
120
|
+
|
|
121
|
+
def stop(self):
|
|
122
|
+
"""停止守护进程"""
|
|
123
|
+
pid = self._read_pid()
|
|
124
|
+
if not pid:
|
|
125
|
+
self._cleanup_files()
|
|
126
|
+
return False, "Gatekeeper 守护进程未运行。"
|
|
127
|
+
|
|
128
|
+
if not self._is_pid_alive(pid):
|
|
129
|
+
self._cleanup_files()
|
|
130
|
+
return False, "Gatekeeper 守护进程未运行(已清理残留状态)。"
|
|
131
|
+
|
|
132
|
+
# 写入停止标记
|
|
133
|
+
try:
|
|
134
|
+
STOP_FILE.write_text("1", encoding="utf-8")
|
|
135
|
+
except Exception as e:
|
|
136
|
+
return False, f"发送停止信号失败: {e}"
|
|
137
|
+
|
|
138
|
+
# 等待进程自行退出
|
|
139
|
+
for _ in range(15):
|
|
140
|
+
if not self._is_pid_alive(pid):
|
|
141
|
+
self._cleanup_files()
|
|
142
|
+
return True, "Gatekeeper 守护进程已停止。"
|
|
143
|
+
time.sleep(0.5)
|
|
144
|
+
|
|
145
|
+
# 强制终止
|
|
146
|
+
try:
|
|
147
|
+
if sys.platform == "win32":
|
|
148
|
+
os.kill(pid, signal.CTRL_BREAK_EVENT)
|
|
149
|
+
else:
|
|
150
|
+
os.kill(pid, signal.SIGTERM)
|
|
151
|
+
except ProcessLookupError:
|
|
152
|
+
pass
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
# 再等待一次
|
|
157
|
+
for _ in range(5):
|
|
158
|
+
if not self._is_pid_alive(pid):
|
|
159
|
+
self._cleanup_files()
|
|
160
|
+
return True, "Gatekeeper 守护进程已停止。"
|
|
161
|
+
time.sleep(0.5)
|
|
162
|
+
|
|
163
|
+
self._cleanup_files()
|
|
164
|
+
return True, "Gatekeeper 守护进程已强制停止。"
|
|
165
|
+
|
|
166
|
+
def status(self):
|
|
167
|
+
"""查询守护进程状态"""
|
|
168
|
+
pid = self._read_pid()
|
|
169
|
+
if not pid:
|
|
170
|
+
return "Gatekeeper 守护进程未运行。"
|
|
171
|
+
if self._is_pid_alive(pid):
|
|
172
|
+
return f"Gatekeeper 守护进程运行中 (PID: {pid})"
|
|
173
|
+
self._cleanup_files()
|
|
174
|
+
return "Gatekeeper 守护进程未运行(已清理残留状态)。"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_manager():
|
|
178
|
+
return GatekeeperManager()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def read_daemon_config():
|
|
182
|
+
"""读取当前守护进程配置"""
|
|
183
|
+
if DAEMON_CONFIG_FILE.exists():
|
|
184
|
+
try:
|
|
185
|
+
with open(DAEMON_CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
186
|
+
return json.load(f)
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
return {}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def sync_gatekeeper_cron_jobs(cron_jobs=None, agent_crons=None, append=False):
|
|
193
|
+
"""同步定时任务配置到 gatekeeper 配置文件。
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
cron_jobs: shell 类型任务列表,每项为 dict
|
|
197
|
+
agent_crons: Agent 类型任务列表,每项为 dict
|
|
198
|
+
append: 是否追加模式(False 则替换对应字段)
|
|
199
|
+
"""
|
|
200
|
+
cfg = read_daemon_config()
|
|
201
|
+
if cron_jobs is not None and not append:
|
|
202
|
+
cfg["cron_jobs"] = cron_jobs
|
|
203
|
+
elif cron_jobs is not None and append:
|
|
204
|
+
existing = cfg.get("cron_jobs", [])
|
|
205
|
+
cfg["cron_jobs"] = existing + [j for j in cron_jobs if j not in existing]
|
|
206
|
+
|
|
207
|
+
if agent_crons is not None and not append:
|
|
208
|
+
cfg["agent_crons"] = agent_crons
|
|
209
|
+
elif agent_crons is not None and append:
|
|
210
|
+
existing = cfg.get("agent_crons", [])
|
|
211
|
+
cfg["agent_crons"] = existing + [j for j in agent_crons if j not in existing]
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
with open(DAEMON_CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
215
|
+
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
|
216
|
+
return True
|
|
217
|
+
except Exception:
|
|
218
|
+
return False
|