travel-agent-cli 0.2.0 → 0.2.1

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 (36) 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
@@ -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
+ ]