travel-agent-cli 0.2.8 → 0.3.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/bin/cli.js +40 -12
- package/package.json +1 -1
- package/python/interactive.py +328 -0
- package/python/main.py +47 -0
- package/python/pyproject.toml +4 -1
package/bin/cli.js
CHANGED
|
@@ -64,20 +64,24 @@ function run() {
|
|
|
64
64
|
travel-agent <command> [options]
|
|
65
65
|
|
|
66
66
|
可用命令:
|
|
67
|
-
run
|
|
68
|
-
analyze
|
|
69
|
-
agents
|
|
70
|
-
status
|
|
71
|
-
model
|
|
72
|
-
config
|
|
73
|
-
|
|
67
|
+
run 运行完整工作流(数据采集 → 分析评分 → 路线规划 → 报告生成)
|
|
68
|
+
analyze 分析旅行产品可行性(市场调研 → 资源评估 → 生成报告)
|
|
69
|
+
agents 查看 Agent 团队信息
|
|
70
|
+
status 显示系统状态(版本、模型、API 配置)
|
|
71
|
+
model 管理 LLM 模型配置(list/status/use)
|
|
72
|
+
config 管理配置(--show/--init)
|
|
73
|
+
flyai FlyAI 旅行搜索(航班/酒店/景点)
|
|
74
|
+
interactive 启动交互式 CLI(推荐)
|
|
75
|
+
help 显示帮助信息
|
|
74
76
|
|
|
75
77
|
示例:
|
|
76
|
-
travel-agent
|
|
77
|
-
travel-agent
|
|
78
|
-
travel-agent
|
|
79
|
-
travel-agent
|
|
80
|
-
travel-agent
|
|
78
|
+
travel-agent interactive # 启动交互式 CLI(推荐)
|
|
79
|
+
travel-agent run -k "海岛游" # 搜索海岛游推荐
|
|
80
|
+
travel-agent analyze "北欧极光" # 分析极光旅行产品
|
|
81
|
+
travel-agent flyai "5 天日本行程规划" # FlyAI 旅行搜索
|
|
82
|
+
travel-agent agents # 查看 Agent 团队
|
|
83
|
+
travel-agent model list # 查看支持的模型
|
|
84
|
+
travel-agent --help # 显示帮助
|
|
81
85
|
|
|
82
86
|
选项:
|
|
83
87
|
-h, --help 显示帮助信息
|
|
@@ -108,6 +112,30 @@ function run() {
|
|
|
108
112
|
process.exit(1);
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
// 检查 interactive 命令
|
|
116
|
+
const interactivePy = path.join(pythonDir, 'interactive.py');
|
|
117
|
+
if (args[0] === 'interactive') {
|
|
118
|
+
if (!fs.existsSync(interactivePy)) {
|
|
119
|
+
console.error(`错误:未找到 interactive.py 文件:${interactivePy}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const pyArgs = [interactivePy, ...args.slice(1)];
|
|
123
|
+
const child = spawn(pythonCmd, pyArgs, {
|
|
124
|
+
stdio: 'inherit',
|
|
125
|
+
cwd: pythonDir
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
child.on('error', (err) => {
|
|
129
|
+
console.error('执行失败:', err.message);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
child.on('exit', (code) => {
|
|
134
|
+
process.exit(code || 0);
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
111
139
|
// 检查主文件是否存在
|
|
112
140
|
if (!fs.existsSync(mainPy)) {
|
|
113
141
|
console.error(`错误:未找到 main.py 文件:${mainPy}`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""travel-agent 交互式 CLI 入口
|
|
3
|
+
|
|
4
|
+
基于 Textual 框架实现的终端用户界面(TUI)
|
|
5
|
+
提供类似 Claude Code 的交互式体验
|
|
6
|
+
"""
|
|
7
|
+
import asyncio
|
|
8
|
+
from textual.app import App, ComposeResult
|
|
9
|
+
from textual.widgets import Header, Footer, Static, Input, RichLog
|
|
10
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
11
|
+
from textual.screen import Screen
|
|
12
|
+
from textual.binding import Binding
|
|
13
|
+
from textual import work
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.markdown import Markdown
|
|
16
|
+
from rich.text import Text
|
|
17
|
+
|
|
18
|
+
from main import run_command
|
|
19
|
+
from config.settings import get_settings
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ChatOutput(Static):
|
|
23
|
+
"""聊天输出区域"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.messages = []
|
|
28
|
+
|
|
29
|
+
def compose(self) -> ComposeResult:
|
|
30
|
+
yield Static(id="chat-content")
|
|
31
|
+
|
|
32
|
+
def add_message(self, role: str, content: str):
|
|
33
|
+
"""添加消息到聊天记录"""
|
|
34
|
+
self.messages.append((role, content))
|
|
35
|
+
self._update_display()
|
|
36
|
+
|
|
37
|
+
def _update_display(self):
|
|
38
|
+
"""更新显示内容"""
|
|
39
|
+
content_widget = self.query_one("#chat-content", Static)
|
|
40
|
+
lines = []
|
|
41
|
+
|
|
42
|
+
for role, content in self.messages:
|
|
43
|
+
if role == "user":
|
|
44
|
+
lines.append(f"[bold blue]> {content}[/bold blue]")
|
|
45
|
+
else:
|
|
46
|
+
lines.append(f"{content}")
|
|
47
|
+
lines.append("")
|
|
48
|
+
|
|
49
|
+
content_widget.update("\n".join(lines))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CommandPalette(Screen):
|
|
53
|
+
"""命令选择面板(按 / 唤起)"""
|
|
54
|
+
|
|
55
|
+
BINDINGS = [
|
|
56
|
+
Binding("escape", "close", "关闭", show=True),
|
|
57
|
+
Binding("up", "navigate_up", "上", show=False),
|
|
58
|
+
Binding("down", "navigate_down", "下", show=False),
|
|
59
|
+
Binding("enter", "select", "选择", show=True),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
COMMANDS = [
|
|
63
|
+
("run", "运行完整工作流", "/run"),
|
|
64
|
+
("flyai", "FlyAI 旅行搜索", "/flyai <查询>"),
|
|
65
|
+
("analyze", "产品可行性分析", "/analyze <话题>"),
|
|
66
|
+
("model", "模型管理", "/model list|status|use"),
|
|
67
|
+
("config", "配置管理", "/config --show|--init"),
|
|
68
|
+
("status", "系统状态", "/status"),
|
|
69
|
+
("agents", "Agent 团队", "/agents"),
|
|
70
|
+
("help", "帮助信息", "/help"),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
def __init__(self):
|
|
74
|
+
super().__init__()
|
|
75
|
+
self.selected_index = 0
|
|
76
|
+
|
|
77
|
+
def compose(self) -> ComposeResult:
|
|
78
|
+
yield Static(self._render_commands(), id="command-list")
|
|
79
|
+
|
|
80
|
+
def _render_commands(self) -> str:
|
|
81
|
+
"""渲染命令列表"""
|
|
82
|
+
lines = ["[bold]可用命令[/bold]\n"]
|
|
83
|
+
for i, (cmd, desc, usage) in enumerate(self.COMMANDS):
|
|
84
|
+
marker = "→ " if i == self.selected_index else " "
|
|
85
|
+
lines.append(f"{marker}[cyan]/{cmd}[/cyan] - {desc}")
|
|
86
|
+
lines.append(f" [dim]{usage}[/dim]")
|
|
87
|
+
lines.append("")
|
|
88
|
+
return "\n".join(lines)
|
|
89
|
+
|
|
90
|
+
def on_key(self, event):
|
|
91
|
+
"""处理键盘事件"""
|
|
92
|
+
if event.key == "up":
|
|
93
|
+
self.selected_index = (self.selected_index - 1) % len(self.COMMANDS)
|
|
94
|
+
elif event.key == "down":
|
|
95
|
+
self.selected_index = (self.selected_index + 1) % len(self.COMMANDS)
|
|
96
|
+
elif event.key == "enter":
|
|
97
|
+
self.dismiss(self.COMMANDS[self.selected_index][0])
|
|
98
|
+
elif event.key == "escape":
|
|
99
|
+
self.dismiss(None)
|
|
100
|
+
|
|
101
|
+
self.query_one("#command-list", Static).update(self._render_commands())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TravelAgentApp(App):
|
|
105
|
+
"""travel-agent 交互式应用"""
|
|
106
|
+
|
|
107
|
+
TITLE = "travel-agent"
|
|
108
|
+
SUB_TITLE = "AI 驱动的旅行目的地推荐"
|
|
109
|
+
|
|
110
|
+
CSS = """
|
|
111
|
+
Screen {
|
|
112
|
+
background: $surface;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#main-container {
|
|
116
|
+
height: 100%;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#chat-output {
|
|
120
|
+
height: 1fr;
|
|
121
|
+
border: solid $primary;
|
|
122
|
+
margin: 1 1 0 1;
|
|
123
|
+
padding: 1;
|
|
124
|
+
background: $surface;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#input-area {
|
|
128
|
+
height: auto;
|
|
129
|
+
max-height: 3;
|
|
130
|
+
margin: 1;
|
|
131
|
+
padding: 1;
|
|
132
|
+
background: $primary-background;
|
|
133
|
+
border: solid $primary;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#command-palette {
|
|
137
|
+
width: 60;
|
|
138
|
+
height: 20;
|
|
139
|
+
align: center middle;
|
|
140
|
+
background: $surface;
|
|
141
|
+
border: solid $primary;
|
|
142
|
+
padding: 1 2;
|
|
143
|
+
}
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
BINDINGS = [
|
|
147
|
+
Binding("ctrl+q", "quit", "退出", show=True),
|
|
148
|
+
Binding("/", "command_palette", "命令", show=True),
|
|
149
|
+
Binding("ctrl+h", "toggle_help", "帮助", show=True),
|
|
150
|
+
Binding("ctrl+g", "submit_input", "提交", show=True),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
def __init__(self):
|
|
154
|
+
super().__init__()
|
|
155
|
+
self.messages = []
|
|
156
|
+
self.command_history = []
|
|
157
|
+
self.history_index = -1
|
|
158
|
+
self.chat_output = None
|
|
159
|
+
self.input_widget = None
|
|
160
|
+
|
|
161
|
+
def compose(self) -> ComposeResult:
|
|
162
|
+
yield Header(show_clock=True)
|
|
163
|
+
with Container(id="main-container"):
|
|
164
|
+
with Vertical():
|
|
165
|
+
yield Static("", id="chat-output")
|
|
166
|
+
yield Input(placeholder="输入消息或 / 命令...", id="input-area")
|
|
167
|
+
yield Footer()
|
|
168
|
+
|
|
169
|
+
def on_mount(self) -> None:
|
|
170
|
+
"""应用启动时"""
|
|
171
|
+
self.chat_output = self.query_one("#chat-output", Static)
|
|
172
|
+
self.input_widget = self.query_one("#input-area", Input)
|
|
173
|
+
self.input_widget.focus()
|
|
174
|
+
|
|
175
|
+
# 显示欢迎信息
|
|
176
|
+
self._add_welcome_message()
|
|
177
|
+
|
|
178
|
+
def _add_welcome_message(self):
|
|
179
|
+
"""显示欢迎信息"""
|
|
180
|
+
welcome = """
|
|
181
|
+
欢迎使用 travel-agent 交互式 CLI!
|
|
182
|
+
|
|
183
|
+
可用命令:
|
|
184
|
+
/run - 运行完整工作流
|
|
185
|
+
/flyai - FlyAI 旅行搜索
|
|
186
|
+
/analyze - 产品可行性分析
|
|
187
|
+
/model - 模型管理
|
|
188
|
+
/config - 配置管理
|
|
189
|
+
/status - 系统状态
|
|
190
|
+
/agents - 查看 Agent 团队
|
|
191
|
+
|
|
192
|
+
按 / 打开命令面板,Ctrl+Q 退出
|
|
193
|
+
"""
|
|
194
|
+
self.messages.append(("system", welcome))
|
|
195
|
+
self._update_chat_output()
|
|
196
|
+
|
|
197
|
+
def _update_chat_output(self):
|
|
198
|
+
"""更新聊天输出"""
|
|
199
|
+
if not self.chat_output:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
lines = []
|
|
203
|
+
for role, content in self.messages:
|
|
204
|
+
if role == "user":
|
|
205
|
+
lines.append(f"[bold blue]> {content}[/bold blue]")
|
|
206
|
+
elif role == "system":
|
|
207
|
+
lines.append(f"[dim]{content}[/dim]")
|
|
208
|
+
else:
|
|
209
|
+
lines.append(f"{content}")
|
|
210
|
+
lines.append("")
|
|
211
|
+
|
|
212
|
+
self.chat_output.update("\n".join(lines))
|
|
213
|
+
|
|
214
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
215
|
+
"""处理输入提交"""
|
|
216
|
+
user_input = event.value.strip()
|
|
217
|
+
if not user_input:
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
# 添加到历史记录
|
|
221
|
+
self.command_history.append(user_input)
|
|
222
|
+
self.history_index = len(self.command_history)
|
|
223
|
+
|
|
224
|
+
# 显示用户消息
|
|
225
|
+
self.messages.append(("user", user_input))
|
|
226
|
+
self._update_chat_output()
|
|
227
|
+
|
|
228
|
+
# 清空输入框
|
|
229
|
+
event.input.value = ""
|
|
230
|
+
|
|
231
|
+
# 处理命令
|
|
232
|
+
self._handle_command(user_input)
|
|
233
|
+
|
|
234
|
+
@work(exclusive=True)
|
|
235
|
+
async def _handle_command(self, command: str):
|
|
236
|
+
"""处理命令(异步工作)"""
|
|
237
|
+
try:
|
|
238
|
+
# 显示处理中状态
|
|
239
|
+
self.messages.append(("system", "正在处理..."))
|
|
240
|
+
self._update_chat_output()
|
|
241
|
+
|
|
242
|
+
if command.startswith("/"):
|
|
243
|
+
# 处理斜杠命令
|
|
244
|
+
result = await self._handle_slash_command(command[1:])
|
|
245
|
+
else:
|
|
246
|
+
# 默认使用 flyai 处理自然语言
|
|
247
|
+
result = await self._handle_natural_language(command)
|
|
248
|
+
|
|
249
|
+
# 移除处理中状态
|
|
250
|
+
self.messages = [(r, c) for r, c in self.messages if c != "正在处理..."]
|
|
251
|
+
|
|
252
|
+
# 显示结果
|
|
253
|
+
self.messages.append(("assistant", result))
|
|
254
|
+
self._update_chat_output()
|
|
255
|
+
|
|
256
|
+
except Exception as e:
|
|
257
|
+
self.messages = [(r, c) for r, c in self.messages if c != "正在处理..."]
|
|
258
|
+
self.messages.append(("system", f"[red]错误:{e}[/red]"))
|
|
259
|
+
self._update_chat_output()
|
|
260
|
+
|
|
261
|
+
async def _handle_slash_command(self, command: str) -> str:
|
|
262
|
+
"""处理斜杠命令"""
|
|
263
|
+
parts = command.split(maxsplit=1)
|
|
264
|
+
cmd_name = parts[0]
|
|
265
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
result = await run_command(cmd_name, args)
|
|
269
|
+
return result
|
|
270
|
+
except Exception as e:
|
|
271
|
+
return f"命令执行失败:{e}"
|
|
272
|
+
|
|
273
|
+
async def _handle_natural_language(self, query: str) -> str:
|
|
274
|
+
"""处理自然语言查询(默认使用 flyai)"""
|
|
275
|
+
try:
|
|
276
|
+
result = await run_command("flyai", query)
|
|
277
|
+
return result
|
|
278
|
+
except Exception as e:
|
|
279
|
+
return f"查询失败:{e}"
|
|
280
|
+
|
|
281
|
+
def action_quit(self):
|
|
282
|
+
"""退出应用"""
|
|
283
|
+
self.exit()
|
|
284
|
+
|
|
285
|
+
def action_command_palette(self):
|
|
286
|
+
"""打开命令面板"""
|
|
287
|
+
def handle_result(selected):
|
|
288
|
+
if selected:
|
|
289
|
+
self.input_widget.value = f"/{selected}"
|
|
290
|
+
self.input_widget.focus()
|
|
291
|
+
|
|
292
|
+
self.push_screen(CommandPalette(), handle_result)
|
|
293
|
+
|
|
294
|
+
def action_submit_input(self):
|
|
295
|
+
"""提交输入"""
|
|
296
|
+
if self.input_widget:
|
|
297
|
+
self.input_widget._submit()
|
|
298
|
+
|
|
299
|
+
def action_toggle_help(self):
|
|
300
|
+
"""切换帮助显示"""
|
|
301
|
+
help_text = """
|
|
302
|
+
快捷键:
|
|
303
|
+
Ctrl+G - 提交输入
|
|
304
|
+
/ - 打开命令面板
|
|
305
|
+
Ctrl+Q - 退出应用
|
|
306
|
+
↑/↓ - 历史命令导航(在输入框中)
|
|
307
|
+
|
|
308
|
+
命令:
|
|
309
|
+
/run - 运行完整工作流
|
|
310
|
+
/flyai - FlyAI 旅行搜索
|
|
311
|
+
/analyze - 产品可行性分析
|
|
312
|
+
/model - 模型管理
|
|
313
|
+
/config - 配置管理
|
|
314
|
+
/status - 系统状态
|
|
315
|
+
/agents - 查看 Agent 团队
|
|
316
|
+
"""
|
|
317
|
+
self.messages.append(("system", help_text))
|
|
318
|
+
self._update_chat_output()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def main():
|
|
322
|
+
"""主入口"""
|
|
323
|
+
app = TravelAgentApp()
|
|
324
|
+
app.run()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
if __name__ == "__main__":
|
|
328
|
+
main()
|
package/python/main.py
CHANGED
|
@@ -409,6 +409,53 @@ def _parse_args(args: List[str], command_name: str = "") -> dict:
|
|
|
409
409
|
return result
|
|
410
410
|
|
|
411
411
|
|
|
412
|
+
# =============================================================================
|
|
413
|
+
# 交互式 CLI 辅助函数
|
|
414
|
+
# =============================================================================
|
|
415
|
+
|
|
416
|
+
async def run_command(command_name: str, args_string: str = "") -> str:
|
|
417
|
+
"""运行命令并返回结果(供交互式 CLI 调用)
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
command_name: 命令名称(不含 /)
|
|
421
|
+
args_string: 参数字符串
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
命令输出结果
|
|
425
|
+
"""
|
|
426
|
+
from io import StringIO
|
|
427
|
+
from rich.console import Console
|
|
428
|
+
|
|
429
|
+
# 保存原始 console
|
|
430
|
+
global console
|
|
431
|
+
original_console = console
|
|
432
|
+
|
|
433
|
+
# 创建捕获输出的 console
|
|
434
|
+
output_buffer = StringIO()
|
|
435
|
+
console = Console(file=output_buffer, force_terminal=True, width=100)
|
|
436
|
+
|
|
437
|
+
try:
|
|
438
|
+
cmd = registry.get(command_name)
|
|
439
|
+
if not cmd:
|
|
440
|
+
return f"未知命令:{command_name}"
|
|
441
|
+
|
|
442
|
+
# 解析参数
|
|
443
|
+
args = args_string.split() if args_string else []
|
|
444
|
+
command_args = _parse_args(args, command_name)
|
|
445
|
+
|
|
446
|
+
# 执行命令
|
|
447
|
+
await cmd.handler(**command_args)
|
|
448
|
+
|
|
449
|
+
# 返回捕获的输出
|
|
450
|
+
return output_buffer.getvalue()
|
|
451
|
+
|
|
452
|
+
except Exception as e:
|
|
453
|
+
return f"命令执行失败:{e}"
|
|
454
|
+
finally:
|
|
455
|
+
# 恢复原始 console
|
|
456
|
+
console = original_console
|
|
457
|
+
|
|
458
|
+
|
|
412
459
|
# =============================================================================
|
|
413
460
|
# 辅助函数
|
|
414
461
|
# =============================================================================
|
package/python/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "travel-agent"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "AI-powered travel destination recommendation agent"
|
|
9
9
|
authors = [{ name = "Your Name", email = "your.email@example.com" }]
|
|
10
10
|
readme = "README.md"
|
|
@@ -23,6 +23,9 @@ dependencies = [
|
|
|
23
23
|
"httpx>=0.26.0",
|
|
24
24
|
"aiosqlite>=0.19.0",
|
|
25
25
|
"rich>=13.7.0",
|
|
26
|
+
"textual>=0.50.0",
|
|
27
|
+
"markdown-it-py>=3.0.0",
|
|
28
|
+
"pygments>=2.17.0",
|
|
26
29
|
]
|
|
27
30
|
|
|
28
31
|
[project.optional-dependencies]
|