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.
Files changed (37) hide show
  1. package/bin/cli.js +6 -6
  2. package/package.json +2 -2
  3. package/python/agents/__init__.py +19 -0
  4. package/python/agents/analysis_agent.py +234 -0
  5. package/python/agents/base.py +377 -0
  6. package/python/agents/collector_agent.py +304 -0
  7. package/python/agents/manager_agent.py +251 -0
  8. package/python/agents/planning_agent.py +161 -0
  9. package/python/agents/product_agent.py +672 -0
  10. package/python/agents/report_agent.py +172 -0
  11. package/python/analyzers/__init__.py +10 -0
  12. package/python/analyzers/hot_score.py +123 -0
  13. package/python/analyzers/ranker.py +225 -0
  14. package/python/analyzers/route_planner.py +86 -0
  15. package/python/cli/commands.py +254 -0
  16. package/python/collectors/__init__.py +14 -0
  17. package/python/collectors/ota/ctrip.py +120 -0
  18. package/python/collectors/ota/fliggy.py +152 -0
  19. package/python/collectors/weibo.py +235 -0
  20. package/python/collectors/wenlv.py +155 -0
  21. package/python/collectors/xiaohongshu.py +170 -0
  22. package/python/config/__init__.py +30 -0
  23. package/python/config/models.py +119 -0
  24. package/python/config/prompts.py +105 -0
  25. package/python/config/settings.py +172 -0
  26. package/python/export/__init__.py +6 -0
  27. package/python/export/report.py +192 -0
  28. package/python/main.py +632 -0
  29. package/python/pyproject.toml +51 -0
  30. package/python/scheduler/tasks.py +77 -0
  31. package/python/tools/fliggy_mcp.py +553 -0
  32. package/python/tools/flyai_tools.py +251 -0
  33. package/python/tools/mcp_tools.py +412 -0
  34. package/python/utils/__init__.py +9 -0
  35. package/python/utils/http.py +73 -0
  36. package/python/utils/storage.py +288 -0
  37. 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
+ ]
@@ -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 projectRoot = path.join(__dirname, '..');
12
- const venvPath = path.join(projectRoot, 'venv');
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 checkPython() {
19
+ function findPython() {
20
20
  try {
21
- const version = execSync('python3 --version', { encoding: 'utf8' }).trim();
22
- console.log(`✓ Python: ${version}`);
23
- return true;
21
+ execSync('python3 --version', { stdio: 'ignore' });
22
+ return 'python3';
24
23
  } catch (e) {
25
24
  try {
26
- const version = execSync('python --version', { encoding: 'utf8' }).trim();
27
- console.log(`✓ Python: ${version}`);
28
- return true;
25
+ execSync('python --version', { stdio: 'ignore' });
26
+ return 'python';
29
27
  } catch (e2) {
30
- console.error('✗ Python: 未找到');
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
- // 检查 pip
41
- function checkPip() {
42
- try {
43
- const version = execSync('pip3 --version', { encoding: 'utf8' }).trim();
44
- console.log(`✓ pip: 已安装`);
45
- return true;
46
- } catch (e) {
47
- try {
48
- const version = execSync('pip --version', { encoding: 'utf8' }).trim();
49
- console.log(`✓ pip: 已安装`);
50
- return true;
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
- function checkVenv() {
61
- if (fs.existsSync(venvPath)) {
62
- console.log(`✓ 虚拟环境:已存在`);
63
- return true;
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
- execSync('python3 -m playwright --version', { encoding: 'utf8', stdio: 'ignore' });
73
- console.log(`✓ Playwright: 已安装`);
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.log(`○ Playwright: 未安装(按需使用)`);
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
- console.log('依赖检查:\n');
82
- checkPython();
83
- checkPip();
84
- checkVenv();
85
- checkPlaywright();
72
+ // 主函数
73
+ function main() {
74
+ const success = installDependencies();
86
75
 
87
- console.log('\n╭───────────────────────────────────────────────────────────────╮');
88
- console.log('│ 快速开始 │');
89
- console.log('╰───────────────────────────────────────────────────────────────╯\n');
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('1. 初始化配置:');
92
- console.log(' travel-agent config --init\n');
84
+ console.log('2. 配置 LLM API Key:');
85
+ console.log(' 编辑 .env 文件,填入你的 API Key\n');
93
86
 
94
- console.log('2. 配置 LLM API Key:');
95
- console.log(' 编辑 .env 文件,填入你的 API Key\n');
87
+ console.log('3. 查看支持的模型:');
88
+ console.log(' travel-agent model list\n');
96
89
 
97
- console.log('3. 查看支持的模型:');
98
- console.log(' travel-agent model list\n');
90
+ console.log('4. 运行工作流:');
91
+ console.log(' travel-agent run -k "海岛游"\n');
99
92
 
100
- console.log('4. 运行工作流:');
101
- console.log(' travel-agent run -k "海岛游"\n');
93
+ console.log('更多帮助:');
94
+ console.log(' travel-agent --help\n');
95
+ }
96
+ }
102
97
 
103
- console.log('更多帮助:');
104
- console.log(' travel-agent --help\n');
98
+ main();