travel-agent-cli 0.2.0 → 0.2.2
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 +6 -6
- package/package.json +2 -2
- package/python/agents/__init__.py +19 -0
- package/python/agents/analysis_agent.py +234 -0
- package/python/agents/base.py +377 -0
- package/python/agents/collector_agent.py +304 -0
- package/python/agents/manager_agent.py +251 -0
- package/python/agents/planning_agent.py +161 -0
- package/python/agents/product_agent.py +672 -0
- package/python/agents/report_agent.py +172 -0
- package/python/analyzers/__init__.py +10 -0
- package/python/analyzers/hot_score.py +123 -0
- package/python/analyzers/ranker.py +225 -0
- package/python/analyzers/route_planner.py +86 -0
- package/python/cli/commands.py +254 -0
- package/python/collectors/__init__.py +14 -0
- package/python/collectors/ota/ctrip.py +120 -0
- package/python/collectors/ota/fliggy.py +152 -0
- package/python/collectors/weibo.py +235 -0
- package/python/collectors/wenlv.py +155 -0
- package/python/collectors/xiaohongshu.py +170 -0
- package/python/config/__init__.py +30 -0
- package/python/config/models.py +119 -0
- package/python/config/prompts.py +105 -0
- package/python/config/settings.py +172 -0
- package/python/export/__init__.py +6 -0
- package/python/export/report.py +192 -0
- package/python/main.py +632 -0
- package/python/pyproject.toml +51 -0
- package/python/scheduler/tasks.py +77 -0
- package/python/tools/fliggy_mcp.py +553 -0
- package/python/tools/flyai_tools.py +251 -0
- package/python/tools/mcp_tools.py +412 -0
- package/python/utils/__init__.py +9 -0
- package/python/utils/http.py +73 -0
- package/python/utils/storage.py +288 -0
- package/scripts/postinstall.js +59 -65
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""HTTP 工具模块"""
|
|
2
|
+
import httpx
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from config.settings import get_settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_client() -> httpx.AsyncClient:
|
|
9
|
+
"""获取配置好的 HTTP 客户端"""
|
|
10
|
+
settings = get_settings()
|
|
11
|
+
|
|
12
|
+
headers = {
|
|
13
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
14
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
|
15
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# 新版 httpx 使用 transport 来配置代理
|
|
19
|
+
if settings.http_proxy or settings.https_proxy:
|
|
20
|
+
# httpx 使用 mounts 来配置代理
|
|
21
|
+
proxy_url = settings.https_proxy or settings.http_proxy
|
|
22
|
+
if proxy_url:
|
|
23
|
+
return httpx.AsyncClient(
|
|
24
|
+
timeout=httpx.Timeout(settings.request_timeout),
|
|
25
|
+
proxies=proxy_url,
|
|
26
|
+
headers=headers,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return httpx.AsyncClient(
|
|
30
|
+
timeout=httpx.Timeout(settings.request_timeout),
|
|
31
|
+
headers=headers,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def fetch_with_retry(
|
|
36
|
+
url: str,
|
|
37
|
+
method: str = "GET",
|
|
38
|
+
headers: Optional[Dict[str, str]] = None,
|
|
39
|
+
params: Optional[Dict[str, Any]] = None,
|
|
40
|
+
data: Optional[Dict[str, Any]] = None,
|
|
41
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
42
|
+
) -> Optional[httpx.Response]:
|
|
43
|
+
"""带重试的 HTTP 请求"""
|
|
44
|
+
settings = get_settings()
|
|
45
|
+
|
|
46
|
+
async with get_client() as client:
|
|
47
|
+
for attempt in range(settings.max_retries):
|
|
48
|
+
try:
|
|
49
|
+
if method == "GET":
|
|
50
|
+
response = await client.get(url, headers=headers, params=params)
|
|
51
|
+
elif method == "POST":
|
|
52
|
+
response = await client.post(
|
|
53
|
+
url, headers=headers, params=params, data=data, json=json_data
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
raise ValueError(f"不支持的 HTTP 方法:{method}")
|
|
57
|
+
|
|
58
|
+
if response.status_code == 200:
|
|
59
|
+
return response
|
|
60
|
+
elif response.status_code == 429:
|
|
61
|
+
# rate limited, wait and retry
|
|
62
|
+
await asyncio.sleep(settings.request_delay * (attempt + 1) * 2)
|
|
63
|
+
else:
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
|
|
66
|
+
except httpx.HTTPError as e:
|
|
67
|
+
if attempt < settings.max_retries - 1:
|
|
68
|
+
await asyncio.sleep(settings.request_delay * (attempt + 1))
|
|
69
|
+
else:
|
|
70
|
+
print(f"请求失败 {url}: {e}")
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
return None
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""数据存储工具"""
|
|
2
|
+
import aiosqlite
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, List, Dict, Any
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from config.models import (
|
|
9
|
+
SocialPost,
|
|
10
|
+
WenlvInfo,
|
|
11
|
+
FlightInfo,
|
|
12
|
+
HotelInfo,
|
|
13
|
+
DestinationRecommendation,
|
|
14
|
+
)
|
|
15
|
+
from config.settings import get_settings
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Storage:
|
|
19
|
+
"""SQLite 数据存储"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, db_path: Optional[str] = None):
|
|
22
|
+
settings = get_settings()
|
|
23
|
+
self.db_path = db_path or settings.database_path
|
|
24
|
+
self._initialized = False
|
|
25
|
+
|
|
26
|
+
async def ensure_init(self):
|
|
27
|
+
"""确保数据库已初始化"""
|
|
28
|
+
if not self._initialized:
|
|
29
|
+
await self.init_db()
|
|
30
|
+
self._initialized = True
|
|
31
|
+
|
|
32
|
+
async def init_db(self):
|
|
33
|
+
"""初始化数据库表"""
|
|
34
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
35
|
+
# 社交媒体帖子表
|
|
36
|
+
await db.execute(
|
|
37
|
+
"""
|
|
38
|
+
CREATE TABLE IF NOT EXISTS social_posts (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
source TEXT NOT NULL,
|
|
41
|
+
title TEXT,
|
|
42
|
+
content TEXT,
|
|
43
|
+
author TEXT,
|
|
44
|
+
author_id TEXT,
|
|
45
|
+
images TEXT,
|
|
46
|
+
likes INTEGER DEFAULT 0,
|
|
47
|
+
comments INTEGER DEFAULT 0,
|
|
48
|
+
shares INTEGER DEFAULT 0,
|
|
49
|
+
tags TEXT,
|
|
50
|
+
url TEXT,
|
|
51
|
+
published_at TIMESTAMP,
|
|
52
|
+
collected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
53
|
+
)
|
|
54
|
+
"""
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# 文旅信息表
|
|
58
|
+
await db.execute(
|
|
59
|
+
"""
|
|
60
|
+
CREATE TABLE IF NOT EXISTS wenlv_info (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
source TEXT NOT NULL,
|
|
63
|
+
title TEXT,
|
|
64
|
+
content TEXT,
|
|
65
|
+
region TEXT,
|
|
66
|
+
url TEXT,
|
|
67
|
+
info_type TEXT,
|
|
68
|
+
published_at TIMESTAMP,
|
|
69
|
+
collected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
70
|
+
)
|
|
71
|
+
"""
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# 航班信息表
|
|
75
|
+
await db.execute(
|
|
76
|
+
"""
|
|
77
|
+
CREATE TABLE IF NOT EXISTS flight_info (
|
|
78
|
+
id TEXT PRIMARY KEY,
|
|
79
|
+
departure_city TEXT,
|
|
80
|
+
arrival_city TEXT,
|
|
81
|
+
price REAL,
|
|
82
|
+
currency TEXT DEFAULT 'CNY',
|
|
83
|
+
airline TEXT,
|
|
84
|
+
departure_time TEXT,
|
|
85
|
+
travel_time TEXT,
|
|
86
|
+
platform TEXT DEFAULT 'fliggy',
|
|
87
|
+
collected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
88
|
+
)
|
|
89
|
+
"""
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# 酒店信息表
|
|
93
|
+
await db.execute(
|
|
94
|
+
"""
|
|
95
|
+
CREATE TABLE IF NOT EXISTS hotel_info (
|
|
96
|
+
id TEXT PRIMARY KEY,
|
|
97
|
+
name TEXT,
|
|
98
|
+
destination TEXT,
|
|
99
|
+
price_per_night REAL,
|
|
100
|
+
currency TEXT DEFAULT 'CNY',
|
|
101
|
+
rating REAL,
|
|
102
|
+
stars INTEGER,
|
|
103
|
+
platform TEXT DEFAULT 'fliggy',
|
|
104
|
+
url TEXT,
|
|
105
|
+
collected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
106
|
+
)
|
|
107
|
+
"""
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# 推荐结果表
|
|
111
|
+
await db.execute(
|
|
112
|
+
"""
|
|
113
|
+
CREATE TABLE IF NOT EXISTS recommendations (
|
|
114
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
115
|
+
name TEXT,
|
|
116
|
+
rank INTEGER,
|
|
117
|
+
score REAL,
|
|
118
|
+
reason TEXT,
|
|
119
|
+
estimated_cost TEXT,
|
|
120
|
+
suggested_days INTEGER,
|
|
121
|
+
best_time TEXT,
|
|
122
|
+
highlights TEXT,
|
|
123
|
+
flight_info_id TEXT,
|
|
124
|
+
hotel_info_id TEXT,
|
|
125
|
+
generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
126
|
+
FOREIGN KEY (flight_info_id) REFERENCES flight_info(id),
|
|
127
|
+
FOREIGN KEY (hotel_info_id) REFERENCES hotel_info(id)
|
|
128
|
+
)
|
|
129
|
+
"""
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
await db.commit()
|
|
133
|
+
|
|
134
|
+
async def save_post(self, post: SocialPost):
|
|
135
|
+
"""保存社交媒体帖子"""
|
|
136
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
137
|
+
await db.execute(
|
|
138
|
+
"""
|
|
139
|
+
INSERT OR REPLACE INTO social_posts
|
|
140
|
+
(id, source, title, content, author, author_id, images, likes, comments, shares, tags, url, published_at, collected_at)
|
|
141
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
142
|
+
""",
|
|
143
|
+
(
|
|
144
|
+
post.id,
|
|
145
|
+
post.source.value,
|
|
146
|
+
post.title,
|
|
147
|
+
post.content,
|
|
148
|
+
post.author,
|
|
149
|
+
post.author_id,
|
|
150
|
+
json.dumps(post.images),
|
|
151
|
+
post.likes,
|
|
152
|
+
post.comments,
|
|
153
|
+
post.shares,
|
|
154
|
+
json.dumps(post.tags),
|
|
155
|
+
post.url,
|
|
156
|
+
post.published_at,
|
|
157
|
+
post.collected_at,
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
await db.commit()
|
|
161
|
+
|
|
162
|
+
async def save_wenlv(self, info: WenlvInfo):
|
|
163
|
+
"""保存文旅信息"""
|
|
164
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
165
|
+
await db.execute(
|
|
166
|
+
"""
|
|
167
|
+
INSERT OR REPLACE INTO wenlv_info
|
|
168
|
+
(id, source, title, content, region, url, info_type, published_at, collected_at)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
170
|
+
""",
|
|
171
|
+
(
|
|
172
|
+
info.id,
|
|
173
|
+
info.source.value,
|
|
174
|
+
info.title,
|
|
175
|
+
info.content,
|
|
176
|
+
info.region,
|
|
177
|
+
info.url,
|
|
178
|
+
info.info_type,
|
|
179
|
+
info.published_at,
|
|
180
|
+
info.collected_at,
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
await db.commit()
|
|
184
|
+
|
|
185
|
+
async def save_flight(self, flight: FlightInfo):
|
|
186
|
+
"""保存航班信息"""
|
|
187
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
188
|
+
await db.execute(
|
|
189
|
+
"""
|
|
190
|
+
INSERT OR REPLACE INTO flight_info
|
|
191
|
+
(id, departure_city, arrival_city, price, currency, airline, departure_time, travel_time, platform)
|
|
192
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
193
|
+
""",
|
|
194
|
+
(
|
|
195
|
+
flight.id,
|
|
196
|
+
flight.departure_city,
|
|
197
|
+
flight.arrival_city,
|
|
198
|
+
flight.price,
|
|
199
|
+
flight.currency,
|
|
200
|
+
flight.airline,
|
|
201
|
+
flight.departure_time,
|
|
202
|
+
flight.travel_time,
|
|
203
|
+
flight.platform,
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
await db.commit()
|
|
207
|
+
|
|
208
|
+
async def save_hotel(self, hotel: HotelInfo):
|
|
209
|
+
"""保存酒店信息"""
|
|
210
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
211
|
+
await db.execute(
|
|
212
|
+
"""
|
|
213
|
+
INSERT OR REPLACE INTO hotel_info
|
|
214
|
+
(id, name, destination, price_per_night, currency, rating, stars, platform, url)
|
|
215
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
216
|
+
""",
|
|
217
|
+
(
|
|
218
|
+
hotel.id,
|
|
219
|
+
hotel.name,
|
|
220
|
+
hotel.destination,
|
|
221
|
+
hotel.price_per_night,
|
|
222
|
+
hotel.currency,
|
|
223
|
+
hotel.rating,
|
|
224
|
+
hotel.stars,
|
|
225
|
+
hotel.platform,
|
|
226
|
+
hotel.url,
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
await db.commit()
|
|
230
|
+
|
|
231
|
+
async def get_recent_posts(
|
|
232
|
+
self, source: Optional[str] = None, limit: int = 100
|
|
233
|
+
) -> List[SocialPost]:
|
|
234
|
+
"""获取最近的帖子"""
|
|
235
|
+
await self.ensure_init()
|
|
236
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
237
|
+
db.row_factory = aiosqlite.Row
|
|
238
|
+
query = "SELECT * FROM social_posts ORDER BY collected_at DESC LIMIT ?"
|
|
239
|
+
if source:
|
|
240
|
+
query = (
|
|
241
|
+
"SELECT * FROM social_posts WHERE source = ? ORDER BY collected_at DESC LIMIT ?"
|
|
242
|
+
)
|
|
243
|
+
cursor = await db.execute(query, (source, limit))
|
|
244
|
+
else:
|
|
245
|
+
cursor = await db.execute(query, (limit,))
|
|
246
|
+
|
|
247
|
+
rows = await cursor.fetchall()
|
|
248
|
+
return [
|
|
249
|
+
SocialPost(
|
|
250
|
+
id=row["id"],
|
|
251
|
+
source=row["source"],
|
|
252
|
+
title=row["title"],
|
|
253
|
+
content=row["content"],
|
|
254
|
+
author=row["author"],
|
|
255
|
+
author_id=row["author_id"],
|
|
256
|
+
images=json.loads(row["images"] or "[]"),
|
|
257
|
+
likes=row["likes"],
|
|
258
|
+
comments=row["comments"],
|
|
259
|
+
shares=row["shares"],
|
|
260
|
+
tags=json.loads(row["tags"] or "[]"),
|
|
261
|
+
url=row["url"],
|
|
262
|
+
published_at=row["published_at"],
|
|
263
|
+
)
|
|
264
|
+
for row in rows
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
async def get_recent_wenlv(self, limit: int = 100) -> List[WenlvInfo]:
|
|
268
|
+
"""获取最近的文旅信息"""
|
|
269
|
+
await self.ensure_init()
|
|
270
|
+
async with aiosqlite.connect(self.db_path) as db:
|
|
271
|
+
db.row_factory = aiosqlite.Row
|
|
272
|
+
cursor = await db.execute(
|
|
273
|
+
"SELECT * FROM wenlv_info ORDER BY collected_at DESC LIMIT ?", (limit,)
|
|
274
|
+
)
|
|
275
|
+
rows = await cursor.fetchall()
|
|
276
|
+
return [
|
|
277
|
+
WenlvInfo(
|
|
278
|
+
id=row["id"],
|
|
279
|
+
source=row["source"],
|
|
280
|
+
title=row["title"],
|
|
281
|
+
content=row["content"],
|
|
282
|
+
region=row["region"],
|
|
283
|
+
url=row["url"],
|
|
284
|
+
info_type=row["info_type"],
|
|
285
|
+
published_at=row["published_at"],
|
|
286
|
+
)
|
|
287
|
+
for row in rows
|
|
288
|
+
]
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,104 +1,98 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* npm 安装后执行的脚本
|
|
4
|
-
*
|
|
4
|
+
* 安装 Python 依赖
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { execSync } = require('child_process');
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
const
|
|
11
|
+
const packagePath = path.join(__dirname, '..');
|
|
12
|
+
const pythonDir = path.join(packagePath, 'python');
|
|
13
13
|
|
|
14
14
|
console.log('╭───────────────────────────────────────────────────────────────╮');
|
|
15
|
-
console.log('│ travel-agent-cli
|
|
15
|
+
console.log('│ travel-agent-cli 正在安装 Python 依赖... │');
|
|
16
16
|
console.log('╰───────────────────────────────────────────────────────────────╯\n');
|
|
17
17
|
|
|
18
18
|
// 检查 Python
|
|
19
|
-
function
|
|
19
|
+
function findPython() {
|
|
20
20
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return true;
|
|
21
|
+
execSync('python3 --version', { stdio: 'ignore' });
|
|
22
|
+
return 'python3';
|
|
24
23
|
} catch (e) {
|
|
25
24
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return true;
|
|
25
|
+
execSync('python --version', { stdio: 'ignore' });
|
|
26
|
+
return 'python';
|
|
29
27
|
} catch (e2) {
|
|
30
|
-
|
|
31
|
-
console.error('\n请先安装 Python 3.10+:');
|
|
32
|
-
console.error(' macOS: brew install python@3.10');
|
|
33
|
-
console.error(' Linux: sudo apt install python3.10');
|
|
34
|
-
console.error(' Windows: https://www.python.org/downloads/');
|
|
35
|
-
return false;
|
|
28
|
+
return null;
|
|
36
29
|
}
|
|
37
30
|
}
|
|
38
31
|
}
|
|
39
32
|
|
|
40
|
-
//
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} catch (e2) {
|
|
52
|
-
console.error('✗ pip: 未找到');
|
|
53
|
-
console.error(' 安装:https://pip.pypa.io/en/stable/installation/');
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
33
|
+
// 安装依赖
|
|
34
|
+
function installDependencies() {
|
|
35
|
+
const pythonCmd = findPython();
|
|
36
|
+
|
|
37
|
+
if (!pythonCmd) {
|
|
38
|
+
console.error('\n⚠ 警告:未找到 Python');
|
|
39
|
+
console.error('请先安装 Python 3.10+');
|
|
40
|
+
console.error(' macOS: brew install python@3.10');
|
|
41
|
+
console.error(' Linux: sudo apt install python3.10');
|
|
42
|
+
console.error(' Windows: https://www.python.org/downloads/');
|
|
43
|
+
return false;
|
|
56
44
|
}
|
|
57
|
-
}
|
|
58
45
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
46
|
+
console.log(`使用 Python: ${pythonCmd}\n`);
|
|
47
|
+
|
|
48
|
+
// 检查 pyproject.toml 是否存在
|
|
49
|
+
const pyprojectPath = path.join(pythonDir, 'pyproject.toml');
|
|
50
|
+
if (!fs.existsSync(pyprojectPath)) {
|
|
51
|
+
console.error(`⚠ 未找到 pyproject.toml`);
|
|
52
|
+
return false;
|
|
64
53
|
}
|
|
65
|
-
console.log(`○ 虚拟环境:未创建(首次运行会自动创建)`);
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
54
|
|
|
69
|
-
// 检查 Playwright
|
|
70
|
-
function checkPlaywright() {
|
|
71
55
|
try {
|
|
72
|
-
|
|
73
|
-
console.log(
|
|
56
|
+
// 使用 pip install -e 安装可编辑模式
|
|
57
|
+
console.log('正在安装 Python 依赖...');
|
|
58
|
+
execSync(`${pythonCmd} -m pip install -e "${pythonDir}"`, {
|
|
59
|
+
stdio: 'inherit',
|
|
60
|
+
env: { ...process.env, PIP_NO_INPUT: '1' }
|
|
61
|
+
});
|
|
62
|
+
console.log('\n✓ Python 依赖安装完成\n');
|
|
74
63
|
return true;
|
|
75
64
|
} catch (e) {
|
|
76
|
-
console.
|
|
65
|
+
console.error('\n⚠ Python 依赖安装失败');
|
|
66
|
+
console.error('\n手动安装:');
|
|
67
|
+
console.error(` ${pythonCmd} -m pip install -e "${pythonDir}"`);
|
|
77
68
|
return false;
|
|
78
69
|
}
|
|
79
70
|
}
|
|
80
71
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
checkVenv();
|
|
85
|
-
checkPlaywright();
|
|
72
|
+
// 主函数
|
|
73
|
+
function main() {
|
|
74
|
+
const success = installDependencies();
|
|
86
75
|
|
|
87
|
-
|
|
88
|
-
console.log('
|
|
89
|
-
console.log('
|
|
76
|
+
if (success) {
|
|
77
|
+
console.log('╭───────────────────────────────────────────────────────────────╮');
|
|
78
|
+
console.log('│ 安装完成!快速开始: │');
|
|
79
|
+
console.log('╰───────────────────────────────────────────────────────────────╯\n');
|
|
80
|
+
|
|
81
|
+
console.log('1. 初始化配置:');
|
|
82
|
+
console.log(' travel-agent config --init\n');
|
|
90
83
|
|
|
91
|
-
console.log('
|
|
92
|
-
console.log('
|
|
84
|
+
console.log('2. 配置 LLM API Key:');
|
|
85
|
+
console.log(' 编辑 .env 文件,填入你的 API Key\n');
|
|
93
86
|
|
|
94
|
-
console.log('
|
|
95
|
-
console.log('
|
|
87
|
+
console.log('3. 查看支持的模型:');
|
|
88
|
+
console.log(' travel-agent model list\n');
|
|
96
89
|
|
|
97
|
-
console.log('
|
|
98
|
-
console.log(' travel-agent
|
|
90
|
+
console.log('4. 运行工作流:');
|
|
91
|
+
console.log(' travel-agent run -k "海岛游"\n');
|
|
99
92
|
|
|
100
|
-
console.log('
|
|
101
|
-
console.log(' travel-agent
|
|
93
|
+
console.log('更多帮助:');
|
|
94
|
+
console.log(' travel-agent --help\n');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
102
97
|
|
|
103
|
-
|
|
104
|
-
console.log(' travel-agent --help\n');
|
|
98
|
+
main();
|