erlangshen 0.1.0
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.
- package/.claude/agents/equity-agent.md +26 -0
- package/.claude/agents/macro-agent.md +25 -0
- package/.claude/commands/analyze.md +40 -0
- package/.claude/commands/macro.md +29 -0
- package/.claude/settings.json +12 -0
- package/CODEX_GOAL.md +46 -0
- package/README.md +206 -0
- package/bin/cli.js +67 -0
- package/bin/erlangshen +2 -0
- package/bin/xiaoergod +2 -0
- package/frontend/index.html +700 -0
- package/knowledge/crypto_guide.md +147 -0
- package/knowledge/economic_indicators.md +125 -0
- package/knowledge/financial_glossary.md +148 -0
- package/knowledge/first_principles.md +50 -0
- package/knowledge/first_principles_deep.md +115 -0
- package/knowledge/global_markets.md +173 -0
- package/knowledge/insights.md +141 -0
- package/knowledge/market_basics.md +116 -0
- package/knowledge/memos/session_20260513_003616.json +6 -0
- package/knowledge/memos/session_20260513_003822.json +6 -0
- package/knowledge/risk_management.md +151 -0
- package/knowledge/team_context.md +42 -0
- package/knowledge/trading_strategies.md +114 -0
- package/package.json +42 -0
- package/requirements.txt +14 -0
- package/scripts/postinstall.js +188 -0
- package/scripts/preuninstall.js +22 -0
- package/src/__init__.py +4 -0
- package/src/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/agents/__init__.py +3 -0
- package/src/agents/base.py +103 -0
- package/src/agents/base_agent.py +86 -0
- package/src/agents/equity.py +136 -0
- package/src/agents/equity_agent.py +91 -0
- package/src/agents/erlang.py +165 -0
- package/src/agents/macro.py +137 -0
- package/src/agents/macro_agent.py +81 -0
- package/src/agents/multi_asset.py +147 -0
- package/src/agents/multi_asset_agent.py +87 -0
- package/src/api/__init__.py +1 -0
- package/src/api/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/api/__pycache__/server.cpython-313.pyc +0 -0
- package/src/api/cli.py +435 -0
- package/src/api/cli_enhanced.py +537 -0
- package/src/api/server.py +266 -0
- package/src/brain.py +200 -0
- package/src/cli.py +153 -0
- package/src/commands/__init__.py +3 -0
- package/src/commands/analyze.py +131 -0
- package/src/commands/macro.py +100 -0
- package/src/commands/memo.py +216 -0
- package/src/commands/portfolio.py +154 -0
- package/src/commands/report.py +228 -0
- package/src/commands/risk.py +183 -0
- package/src/commands/search.py +183 -0
- package/src/commands/stock.py +124 -0
- package/src/config.py +327 -0
- package/src/core/__init__.py +1 -0
- package/src/core/brain.py +645 -0
- package/src/core/cerebellum.py +175 -0
- package/src/core/investment_universe.py +423 -0
- package/src/core/knowledge.py +207 -0
- package/src/core/memory.py +115 -0
- package/src/hooks/__init__.py +3 -0
- package/src/hooks/session_end.py +57 -0
- package/src/hooks/session_start.py +75 -0
- package/src/knowledge/__init__.py +1 -0
- package/src/mcp/__init__.py +3 -0
- package/src/mcp/feishu.py +331 -0
- package/src/mcp/fund_tools.py +323 -0
- package/src/mcp/macro.py +452 -0
- package/src/mcp/market.py +331 -0
- package/src/mcp/registry.py +168 -0
- package/src/network/__init__.py +15 -0
- package/src/network/detector.py +125 -0
- package/src/network/proxy.py +199 -0
- package/src/network/router.py +103 -0
- package/src/prompts/__init__.py +1 -0
- package/src/prompts/analysis_framework.md +164 -0
- package/src/prompts/persona.md +65 -0
- package/src/prompts/report_template.md +144 -0
- package/src/skills/__init__.py +3 -0
- package/src/skills/framework.py +105 -0
- package/src/skills/templates.py +342 -0
- package/src/tools/__init__.py +1 -0
- package/src/tools/file_tools.py +209 -0
- package/src/tools/macro_tools.py +152 -0
- package/src/tools/market_tools.py +1172 -0
- package/src/tools/registry.py +398 -0
- package/src/tools/search_tools.py +777 -0
- package/tests/__init__.py +1 -0
- package/tests/test_erlangshen.py +140 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""
|
|
2
|
+
二郎神CLI增强版
|
|
3
|
+
参考 Claude Code 的设计理念,添加更多高级功能
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import threading
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Optional, Callable, AsyncGenerator
|
|
13
|
+
from collections import deque
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
16
|
+
|
|
17
|
+
from loguru import logger
|
|
18
|
+
from src.core.brain import Brain
|
|
19
|
+
from src.core.memory import Memory
|
|
20
|
+
from src.core.knowledge import KnowledgeBase
|
|
21
|
+
from src.tools.market_tools import MarketTools
|
|
22
|
+
from src.tools.macro_tools import MacroTools
|
|
23
|
+
from src.tools.search_tools import SearchTools
|
|
24
|
+
from src.tools.file_tools import FileTools
|
|
25
|
+
from src.agents.erlang import 二郎神
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ============================================================
|
|
29
|
+
# ANSI 颜色和格式化
|
|
30
|
+
# ============================================================
|
|
31
|
+
@dataclass
|
|
32
|
+
class C:
|
|
33
|
+
"""颜色常量"""
|
|
34
|
+
RESET = "\033[0m"
|
|
35
|
+
BOLD = "\033[1m"
|
|
36
|
+
DIM = "\033[2m"
|
|
37
|
+
ITALIC = "\033[3m"
|
|
38
|
+
|
|
39
|
+
# 前景色
|
|
40
|
+
BLACK = "\033[30m"
|
|
41
|
+
RED = "\033[31m"
|
|
42
|
+
GREEN = "\033[32m"
|
|
43
|
+
YELLOW = "\033[33m"
|
|
44
|
+
BLUE = "\033[34m"
|
|
45
|
+
MAGENTA = "\033[35m"
|
|
46
|
+
CYAN = "\033[36m"
|
|
47
|
+
WHITE = "\033[37m"
|
|
48
|
+
|
|
49
|
+
# 亮色
|
|
50
|
+
BRIGHT_RED = "\033[91m"
|
|
51
|
+
BRIGHT_GREEN = "\033[92m"
|
|
52
|
+
BRIGHT_YELLOW = "\033[93m"
|
|
53
|
+
BRIGHT_BLUE = "\033[94m"
|
|
54
|
+
BRIGHT_MAGENTA = "\033[95m"
|
|
55
|
+
BRIGHT_CYAN = "\033[96m"
|
|
56
|
+
BRIGHT_WHITE = "\033[97m"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def c(color: str, text: str) -> str:
|
|
60
|
+
"""彩色输出"""
|
|
61
|
+
return f"{color}{text}{C.RESET}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def clear_line():
|
|
65
|
+
"""清除当前行"""
|
|
66
|
+
sys.stdout.write('\r' + ' ' * 100 + '\r')
|
|
67
|
+
sys.stdout.flush()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ============================================================
|
|
71
|
+
# Spinner 增强版
|
|
72
|
+
# ============================================================
|
|
73
|
+
class Spinner:
|
|
74
|
+
"""带消息的加载动画"""
|
|
75
|
+
|
|
76
|
+
FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
77
|
+
|
|
78
|
+
def __init__(self, message: str = "处理中"):
|
|
79
|
+
self.message = message
|
|
80
|
+
self.running = False
|
|
81
|
+
self.thread: Optional[threading.Thread] = None
|
|
82
|
+
self._stop_event = threading.Event()
|
|
83
|
+
|
|
84
|
+
def start(self):
|
|
85
|
+
"""启动"""
|
|
86
|
+
self.running = True
|
|
87
|
+
self._stop_event.clear()
|
|
88
|
+
self.thread = threading.Thread(target=self._spin, daemon=True)
|
|
89
|
+
self.thread.start()
|
|
90
|
+
|
|
91
|
+
def stop(self, final_msg: Optional[str] = None):
|
|
92
|
+
"""停止"""
|
|
93
|
+
self.running = False
|
|
94
|
+
self._stop_event.set()
|
|
95
|
+
if self.thread:
|
|
96
|
+
self.thread.join(timeout=0.3)
|
|
97
|
+
clear_line()
|
|
98
|
+
if final_msg:
|
|
99
|
+
print(c(C.BRIGHT_GREEN, f"✓ {final_msg}"))
|
|
100
|
+
|
|
101
|
+
def _spin(self):
|
|
102
|
+
"""动画循环"""
|
|
103
|
+
idx = 0
|
|
104
|
+
while self.running and not self._stop_event.is_set():
|
|
105
|
+
frame = self.FRAMES[idx % len(self.FRAMES)]
|
|
106
|
+
msg = f"{frame} {self.message}..."
|
|
107
|
+
sys.stdout.write('\r' + msg)
|
|
108
|
+
sys.stdout.flush()
|
|
109
|
+
idx += 1
|
|
110
|
+
time.sleep(0.08)
|
|
111
|
+
clear_line()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ============================================================
|
|
115
|
+
# 打字机效果(流式输出)
|
|
116
|
+
# ============================================================
|
|
117
|
+
async def typewriter_stream(
|
|
118
|
+
text: str,
|
|
119
|
+
delay: float = 0.005,
|
|
120
|
+
prefix: str = "",
|
|
121
|
+
color: str = C.WHITE
|
|
122
|
+
) -> str:
|
|
123
|
+
"""
|
|
124
|
+
流式输出效果
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
text: 要输出的文本
|
|
128
|
+
delay: 每个字符的延迟
|
|
129
|
+
prefix: 每行前缀
|
|
130
|
+
color: 颜色
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
完整文本
|
|
134
|
+
"""
|
|
135
|
+
# 非TTY或文本太长,直接输出
|
|
136
|
+
if not sys.stdout.isatty() or len(text) > 3000:
|
|
137
|
+
print(c(color, text))
|
|
138
|
+
return text
|
|
139
|
+
|
|
140
|
+
result = []
|
|
141
|
+
for i, char in enumerate(text):
|
|
142
|
+
if char == '\n':
|
|
143
|
+
result.append(char)
|
|
144
|
+
print(c(color, ''.join(result)), end='', flush=True)
|
|
145
|
+
result = []
|
|
146
|
+
else:
|
|
147
|
+
result.append(char)
|
|
148
|
+
if len(result) >= 50 or i == len(text) - 1:
|
|
149
|
+
print(c(color, ''.join(result)), end='', flush=True)
|
|
150
|
+
result = []
|
|
151
|
+
await asyncio.sleep(delay)
|
|
152
|
+
|
|
153
|
+
if result:
|
|
154
|
+
print(c(color, ''.join(result)), end='', flush=True)
|
|
155
|
+
|
|
156
|
+
print() # 换行
|
|
157
|
+
return text
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ============================================================
|
|
161
|
+
# Markdown 简单渲染
|
|
162
|
+
# ============================================================
|
|
163
|
+
class MarkdownRenderer:
|
|
164
|
+
"""简单的 Markdown 渲染器"""
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def render(text: str) -> str:
|
|
168
|
+
"""渲染 Markdown 文本(简化版)"""
|
|
169
|
+
lines = text.split('\n')
|
|
170
|
+
output = []
|
|
171
|
+
|
|
172
|
+
for line in lines:
|
|
173
|
+
# 标题 ##
|
|
174
|
+
if line.startswith('## '):
|
|
175
|
+
title = line[3:]
|
|
176
|
+
output.append(f"\n{C.BRIGHT_CYAN}{C.BOLD}{title}{C.RESET}")
|
|
177
|
+
output.append(f"{C.DIM}{'─' * len(title)}{C.RESET}")
|
|
178
|
+
# 标题 #
|
|
179
|
+
elif line.startswith('# '):
|
|
180
|
+
title = line[2:]
|
|
181
|
+
output.append(f"\n{C.BRIGHT_CYAN}{C.BOLD}{title}{C.RESET}")
|
|
182
|
+
# 列表
|
|
183
|
+
elif line.strip().startswith('- ') or line.strip().startswith('* '):
|
|
184
|
+
content = line.strip()[2:]
|
|
185
|
+
output.append(f" {C.CYAN}●{C.RESET} {content}")
|
|
186
|
+
# 分隔线
|
|
187
|
+
elif line.strip() == '---':
|
|
188
|
+
output.append(f"{C.DIM}{'─' * 50}{C.RESET}")
|
|
189
|
+
# 普通文本
|
|
190
|
+
else:
|
|
191
|
+
output.append(line)
|
|
192
|
+
|
|
193
|
+
return '\n'.join(output)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ============================================================
|
|
197
|
+
# 命令历史
|
|
198
|
+
# ============================================================
|
|
199
|
+
class CommandHistory:
|
|
200
|
+
"""命令历史管理器"""
|
|
201
|
+
|
|
202
|
+
def __init__(self, max_size: int = 100):
|
|
203
|
+
self.history: deque = deque(maxlen=max_size)
|
|
204
|
+
self.current_idx: int = -1
|
|
205
|
+
|
|
206
|
+
def add(self, cmd: str):
|
|
207
|
+
"""添加命令"""
|
|
208
|
+
if cmd and (not self.history or self.history[-1] != cmd):
|
|
209
|
+
self.history.append(cmd)
|
|
210
|
+
self.current_idx = len(self.history)
|
|
211
|
+
|
|
212
|
+
def get_prev(self) -> Optional[str]:
|
|
213
|
+
"""获取上一条命令"""
|
|
214
|
+
if self.history:
|
|
215
|
+
self.current_idx = max(0, self.current_idx - 1)
|
|
216
|
+
return self.history[self.current_idx]
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
def get_next(self) -> Optional[str]:
|
|
220
|
+
"""获取下一条命令"""
|
|
221
|
+
if self.history:
|
|
222
|
+
self.current_idx = min(len(self.history), self.current_idx + 1)
|
|
223
|
+
if self.current_idx >= len(self.history):
|
|
224
|
+
return ""
|
|
225
|
+
return self.history[self.current_idx]
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ============================================================
|
|
230
|
+
# UI 组件
|
|
231
|
+
# ============================================================
|
|
232
|
+
class UI:
|
|
233
|
+
"""UI 组件库"""
|
|
234
|
+
|
|
235
|
+
# ASCII 艺术横幅
|
|
236
|
+
BANNER = f"""
|
|
237
|
+
{C.CYAN}╔══════════════════════════════════════════════════════════════════════╗
|
|
238
|
+
║{C.RESET}
|
|
239
|
+
║{C.RESET} {C.BRIGHT_CYAN}{C.BOLD} ██████╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗ {C.RESET}
|
|
240
|
+
║{C.RESET} {C.BRIGHT_CYAN}{C.BOLD} ██╔══██╗██║ ██║██╔═══██╗██╔════╝██╔═══██╗████╗ ██║ {C.RESET}
|
|
241
|
+
║{C.RESET} {C.BRIGHT_CYAN}{C.BOLD} ██████╔╝██║ ██║██║ ██║█████╗ ██║ ██║██╔██╗ ██║ {C.RESET}
|
|
242
|
+
║{C.RESET} {C.BRIGHT_CYAN}{C.BOLD} ██╔═══╝ ██║ ██║██║ ██║██╔══╝ ██║ ██║██║╚██╗██║ {C.RESET}
|
|
243
|
+
║{C.RESET} {C.BRIGHT_CYAN}{C.BOLD} ██║ ╚██████╔╝╚██████╔╝██║ ╚██████╔╝██║ ╚████║ {C.RESET}
|
|
244
|
+
║{C.RESET} {C.BRIGHT_CYAN}{C.BOLD} ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ {C.RESET}
|
|
245
|
+
║{C.RESET}
|
|
246
|
+
║{C.RESET} {C.BRIGHT_CYAN}{C.BOLD} {C.BRIGHT_YELLOW}AI 投 资 智 能 体{C.RESET}{C.BRIGHT_CYAN} {C.RESET}
|
|
247
|
+
║{C.RESET}
|
|
248
|
+
║{C.RESET} {C.DIM} 全知全能 · 天眼洞察 · 投资决策 · 风险评估 {C.RESET} {C.CYAN}║
|
|
249
|
+
╚══════════════════════════════════════════════════════════════════════╝{C.RESET}
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def banner():
|
|
254
|
+
"""显示横幅"""
|
|
255
|
+
print(UI.BANNER)
|
|
256
|
+
print(f"{C.DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{C.RESET}")
|
|
257
|
+
print(f" {C.WHITE}输入你的问题,二郎神将为你分析投资机会和风险{C.RESET}")
|
|
258
|
+
print(f"{C.DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{C.RESET}\n")
|
|
259
|
+
|
|
260
|
+
@staticmethod
|
|
261
|
+
def help_panel():
|
|
262
|
+
"""帮助面板"""
|
|
263
|
+
print(f"""
|
|
264
|
+
{C.CYAN}╭─────────────────────────────────────────────────────────────────────╮{C.RESET}
|
|
265
|
+
{C.CYAN}│{C.RESET} {C.BOLD}快捷命令{C.RESET} {C.CYAN}│{C.RESET}
|
|
266
|
+
{C.CYAN}├─────────────────────────────────────────────────────────────────────┤{C.RESET}
|
|
267
|
+
{C.CYAN}│{C.RESET} {C.GREEN}/help{C.RESET} 显示帮助信息 {C.CYAN}│{C.RESET}
|
|
268
|
+
{C.CYAN}│{C.RESET} {C.GREEN}/market{C.RESET} 查询股票行情 例: /market 000001 {C.CYAN}│{C.RESET}
|
|
269
|
+
{C.CYAN}│{C.RESET} {C.GREEN}/macro{C.RESET} 查询宏观数据 例: /macro CPI {C.CYAN}│{C.RESET}
|
|
270
|
+
{C.CYAN}│{C.RESET} {C.GREEN}/search{C.RESET} 网络搜索 例: /search A股走势 {C.CYAN}│{C.RESET}
|
|
271
|
+
{C.CYAN}│{C.RESET} {C.GREEN}/report{C.RESET} 生成报告 例: /report 周报 {C.CYAN}│{C.RESET}
|
|
272
|
+
{C.CYAN}│{C.RESET} {C.GREEN}/status{C.RESET} 查看状态 {C.CYAN}│{C.RESET}
|
|
273
|
+
{C.CYAN}│{C.RESET} {C.GREEN}/clear{C.RESET} 清除屏幕 {C.CYAN}│{C.RESET}
|
|
274
|
+
{C.CYAN}│{C.RESET} {C.RED}/quit{C.RESET} 退出程序 {C.CYAN}│{C.RESET}
|
|
275
|
+
{C.CYAN}╰─────────────────────────────────────────────────────────────────────╯{C.RESET}
|
|
276
|
+
|
|
277
|
+
{C.DIM}示例:{C.RESET}
|
|
278
|
+
{C.WHITE}→{C.RESET} 分析美联储加息对A股的影响
|
|
279
|
+
{C.WHITE}→{C.RESET} 茅台现在值得买入吗?
|
|
280
|
+
{C.WHITE}→{C.RESET} 当前应该怎么配置资产?
|
|
281
|
+
{C.WHITE}→{C.RESET} 分析半导体行业前景
|
|
282
|
+
""")
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def status_bar():
|
|
286
|
+
"""状态栏"""
|
|
287
|
+
try:
|
|
288
|
+
width = os.get_terminal_size().columns
|
|
289
|
+
except:
|
|
290
|
+
width = 80
|
|
291
|
+
bar = "─" * max(10, width - 20)
|
|
292
|
+
print(f"\n{C.DIM}┌{bar}┐{C.RESET}")
|
|
293
|
+
print(f"{C.DIM}│{C.RESET} {C.DIM}↑↓: 历史{C.RESET} | {C.DIM}Ctrl+L: 清屏{C.RESET} | {C.DIM}Ctrl+C: 中断{C.RESET} | {C.DIM}/quit: 退出{C.RESET} {C.DIM}│{C.RESET}")
|
|
294
|
+
print(f"{C.DIM}└{bar}┘{C.RESET}")
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def thinking():
|
|
298
|
+
"""思考中"""
|
|
299
|
+
return f"{C.YELLOW}⏳ 思考中...{C.RESET}"
|
|
300
|
+
|
|
301
|
+
@staticmethod
|
|
302
|
+
def success(msg: str):
|
|
303
|
+
print(c(C.BRIGHT_GREEN, f"✓ {msg}"))
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def error(msg: str):
|
|
307
|
+
print(c(C.BRIGHT_RED, f"✗ {msg}"))
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def warning(msg: str):
|
|
311
|
+
print(c(C.YELLOW, f"⚠ {msg}"))
|
|
312
|
+
|
|
313
|
+
@staticmethod
|
|
314
|
+
def info(msg: str):
|
|
315
|
+
print(c(C.CYAN, f"ℹ {msg}"))
|
|
316
|
+
|
|
317
|
+
@staticmethod
|
|
318
|
+
def section(title: str):
|
|
319
|
+
"""分节标题"""
|
|
320
|
+
print(f"\n{C.CYAN}╭{'─' * 58}╮{C.RESET}")
|
|
321
|
+
print(f"{C.CYAN}│{C.RESET}{C.BOLD}{C.BRIGHT_CYAN} {title}{C.RESET}{' ' * (52 - len(title))}{C.CYAN}│{C.RESET}")
|
|
322
|
+
print(f"{C.CYAN}╰{'─' * 58}╯{C.RESET}\n")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ============================================================
|
|
326
|
+
# 主 CLI 类
|
|
327
|
+
# ============================================================
|
|
328
|
+
class EnhancedCLI:
|
|
329
|
+
"""增强版二郎神CLI"""
|
|
330
|
+
|
|
331
|
+
def __init__(self):
|
|
332
|
+
self.erlangshen = self._init_erlangshen()
|
|
333
|
+
self.running = True
|
|
334
|
+
self.history = CommandHistory()
|
|
335
|
+
self.spinner = Spinner()
|
|
336
|
+
self.ui = UI()
|
|
337
|
+
self.md = MarkdownRenderer()
|
|
338
|
+
logger.info("Enhanced CLI initialized")
|
|
339
|
+
|
|
340
|
+
def _init_erlangshen(self) -> 二郎神:
|
|
341
|
+
"""初始化二郎神"""
|
|
342
|
+
logger.info("Initializing 二郎神...")
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
from src.config import load_config
|
|
346
|
+
config = load_config()
|
|
347
|
+
deepseek_key = config.deepseek_api_key or os.getenv("DEEPSEEK_API_KEY")
|
|
348
|
+
if deepseek_key:
|
|
349
|
+
os.environ["DEEPSEEK_API_KEY"] = deepseek_key
|
|
350
|
+
except Exception as e:
|
|
351
|
+
logger.warning(f"Config load failed: {e}")
|
|
352
|
+
|
|
353
|
+
return 二郎神(
|
|
354
|
+
brain=Brain(),
|
|
355
|
+
memory=Memory(),
|
|
356
|
+
knowledge=KnowledgeBase(),
|
|
357
|
+
tools={
|
|
358
|
+
"market_tools": MarketTools(),
|
|
359
|
+
"macro_tools": MacroTools(),
|
|
360
|
+
"search_tools": SearchTools(),
|
|
361
|
+
"file_tools": FileTools(),
|
|
362
|
+
},
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def _read_input(self, prompt: str) -> str:
|
|
366
|
+
"""读取输入(支持历史)"""
|
|
367
|
+
try:
|
|
368
|
+
import readline
|
|
369
|
+
# 配置 readline
|
|
370
|
+
readline.parse_and_bind('tab: complete')
|
|
371
|
+
readline.set_history_length(100)
|
|
372
|
+
|
|
373
|
+
# 尝试使用历史
|
|
374
|
+
for cmd in reversed(list(self.history.history)):
|
|
375
|
+
readline.add_history(cmd)
|
|
376
|
+
except:
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
return input(prompt)
|
|
380
|
+
|
|
381
|
+
async def _process_command(self, cmd: str) -> Optional[str]:
|
|
382
|
+
"""处理命令"""
|
|
383
|
+
cmd = cmd.strip()
|
|
384
|
+
if not cmd:
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
# 命令路由
|
|
388
|
+
if cmd == "/quit":
|
|
389
|
+
self.running = False
|
|
390
|
+
return c(C.CYAN, "\n👋 再见!二郎神退下了。\n")
|
|
391
|
+
|
|
392
|
+
if cmd == "/help":
|
|
393
|
+
self.ui.help_panel()
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
if cmd == "/clear":
|
|
397
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
398
|
+
self.ui.banner()
|
|
399
|
+
self.ui.help_panel()
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
if cmd == "/status":
|
|
403
|
+
stats = self.erlangshen.knowledge.stats()
|
|
404
|
+
return f"\n{self.ui.section('知识库状态')}\n{stats}"
|
|
405
|
+
|
|
406
|
+
if cmd.startswith("/market "):
|
|
407
|
+
symbol = cmd.split(maxsplit=1)[1]
|
|
408
|
+
try:
|
|
409
|
+
result = await self.erlangshen.tools["market_tools"].get_stock_price(symbol)
|
|
410
|
+
return f"\n{self.ui.section(f'📈 {symbol} 行情')}\n{result}"
|
|
411
|
+
except Exception as e:
|
|
412
|
+
return c(C.BRIGHT_RED, f"查询失败: {e}")
|
|
413
|
+
|
|
414
|
+
if cmd.startswith("/macro "):
|
|
415
|
+
indicator = cmd.split(maxsplit=1)[1]
|
|
416
|
+
try:
|
|
417
|
+
result = await self.erlangshen.tools["macro_tools"].get_macro_indicator(indicator)
|
|
418
|
+
return f"\n{self.ui.section(f'📊 {indicator} 数据')}\n{result}"
|
|
419
|
+
except Exception as e:
|
|
420
|
+
return c(C.BRIGHT_RED, f"查询失败: {e}")
|
|
421
|
+
|
|
422
|
+
if cmd.startswith("/search "):
|
|
423
|
+
query = cmd.split(maxsplit=1)[1]
|
|
424
|
+
self.ui.status_bar()
|
|
425
|
+
print(f"\n{self.ui.thinking()}")
|
|
426
|
+
self.spinner.message = "搜索中"
|
|
427
|
+
self.spinner.start()
|
|
428
|
+
try:
|
|
429
|
+
result = await self.erlangshen.tools["search_tools"].web_search(query)
|
|
430
|
+
self.spinner.stop("搜索完成")
|
|
431
|
+
return f"\n{self.ui.section(f'🔍 搜索: {query}')}\n{result}"
|
|
432
|
+
except Exception as e:
|
|
433
|
+
self.spinner.stop()
|
|
434
|
+
return c(C.BRIGHT_RED, f"搜索失败: {e}")
|
|
435
|
+
|
|
436
|
+
if cmd.startswith("/report "):
|
|
437
|
+
title = cmd.split(maxsplit=1)[1]
|
|
438
|
+
return c(C.YELLOW, f"报告功能开发中: /report {title}")
|
|
439
|
+
|
|
440
|
+
# 默认:分析查询
|
|
441
|
+
return await self._analyze(cmd)
|
|
442
|
+
|
|
443
|
+
async def _analyze(self, query: str) -> str:
|
|
444
|
+
"""执行分析"""
|
|
445
|
+
self.ui.status_bar()
|
|
446
|
+
print(f"\n{self.ui.thinking()}")
|
|
447
|
+
self.spinner.message = "二郎神正在分析"
|
|
448
|
+
self.spinner.start()
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
result = await self.erlangshen.process(query)
|
|
452
|
+
self.spinner.stop("分析完成")
|
|
453
|
+
|
|
454
|
+
# 格式化输出
|
|
455
|
+
output = self._format_result(result)
|
|
456
|
+
return output
|
|
457
|
+
except Exception as e:
|
|
458
|
+
self.spinner.stop()
|
|
459
|
+
logger.error(f"Analysis error: {e}")
|
|
460
|
+
return c(C.BRIGHT_RED, f"\n❌ 分析出错: {e}")
|
|
461
|
+
|
|
462
|
+
def _format_result(self, result: dict) -> str:
|
|
463
|
+
"""格式化分析结果"""
|
|
464
|
+
lines = []
|
|
465
|
+
|
|
466
|
+
lines.append(f"\n{self.ui.section('二郎神分析结果')}")
|
|
467
|
+
|
|
468
|
+
# 核心结论
|
|
469
|
+
if "conclusion" in result:
|
|
470
|
+
conclusion = result["conclusion"]
|
|
471
|
+
lines.append(f"{C.BOLD}📊 核心结论{C.RESET}")
|
|
472
|
+
lines.append(f"{C.DIM}{'─' * 50}{C.RESET}")
|
|
473
|
+
lines.append(self.md.render(conclusion[:800]))
|
|
474
|
+
lines.append("")
|
|
475
|
+
|
|
476
|
+
# 洞察
|
|
477
|
+
if "erlangshen_insight" in result:
|
|
478
|
+
lines.append(f"{C.BOLD}💡 二郎神洞察{C.RESET}")
|
|
479
|
+
lines.append(f"{C.DIM}{'─' * 50}{C.RESET}")
|
|
480
|
+
insight = result["erlangshen_insight"]
|
|
481
|
+
lines.append(c(C.CYAN, insight[:400]))
|
|
482
|
+
lines.append("")
|
|
483
|
+
|
|
484
|
+
# 置信度
|
|
485
|
+
if "analysis" in result:
|
|
486
|
+
analysis = result["analysis"]
|
|
487
|
+
if "confidence" in analysis:
|
|
488
|
+
conf = analysis["confidence"]
|
|
489
|
+
conf_color = C.BRIGHT_GREEN if conf > 0.7 else C.BRIGHT_YELLOW if conf > 0.4 else C.BRIGHT_RED
|
|
490
|
+
lines.append(f"{C.BOLD}🎯 置信度{C.RESET} {c(conf_color, f'{conf:.0%}')}")
|
|
491
|
+
|
|
492
|
+
lines.append(f"\n{C.DIM}{'─' * 60}{C.RESET}")
|
|
493
|
+
return "\n".join(lines)
|
|
494
|
+
|
|
495
|
+
async def run(self):
|
|
496
|
+
"""运行CLI"""
|
|
497
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
498
|
+
self.ui.banner()
|
|
499
|
+
self.ui.help_panel()
|
|
500
|
+
|
|
501
|
+
while self.running:
|
|
502
|
+
try:
|
|
503
|
+
self.ui.status_bar()
|
|
504
|
+
user_input = self._read_input(f"{C.BRIGHT_CYAN}❓ 你:{C.RESET} ")
|
|
505
|
+
|
|
506
|
+
if not user_input.strip():
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
# 添加到历史
|
|
510
|
+
self.history.add(user_input)
|
|
511
|
+
|
|
512
|
+
# 处理命令
|
|
513
|
+
result = await self._process_command(user_input)
|
|
514
|
+
if result:
|
|
515
|
+
print(result)
|
|
516
|
+
|
|
517
|
+
except KeyboardInterrupt:
|
|
518
|
+
print(c(C.YELLOW, "\n\n⚠ 使用 /quit 退出"))
|
|
519
|
+
continue
|
|
520
|
+
except EOFError:
|
|
521
|
+
break
|
|
522
|
+
except Exception as e:
|
|
523
|
+
logger.error(f"CLI error: {e}")
|
|
524
|
+
print(c(C.BRIGHT_RED, f"\n❌ 错误: {e}"))
|
|
525
|
+
|
|
526
|
+
print(c(C.CYAN, "\n👋 二郎神已退出。再见!\n"))
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
async def main():
|
|
530
|
+
"""入口"""
|
|
531
|
+
print(f"{C.DIM}启动二郎神增强版CLI...{C.RESET}")
|
|
532
|
+
cli = EnhancedCLI()
|
|
533
|
+
await cli.run()
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
if __name__ == "__main__":
|
|
537
|
+
asyncio.run(main())
|