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,254 @@
|
|
|
1
|
+
"""旅行 Agent CLI - 参考 Claude Code 设计模式
|
|
2
|
+
|
|
3
|
+
命令设计原则:
|
|
4
|
+
1. 简洁的命令定义,包含清晰的描述
|
|
5
|
+
2. 支持别名(aliases)
|
|
6
|
+
3. 参数提示(argumentHint)
|
|
7
|
+
4. 即时执行(immediate)vs 延迟执行
|
|
8
|
+
5. 支持交互模式和非交互模式
|
|
9
|
+
6. 输出格式化(使用 rich 库)
|
|
10
|
+
"""
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Callable, Awaitable, Optional, List, Dict, Any, Union
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CommandMode(Enum):
|
|
17
|
+
"""命令执行模式"""
|
|
18
|
+
INTERACTIVE = "interactive" # 交互模式(默认)
|
|
19
|
+
NON_INTERACTIVE = "non-interactive" # 非交互模式(脚本化)
|
|
20
|
+
BOTH = "both" # 两种模式都支持
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CommandType(Enum):
|
|
24
|
+
"""命令类型"""
|
|
25
|
+
PROMPT = "prompt" # 触发 LLM 对话
|
|
26
|
+
LOCAL = "local" # 本地执行
|
|
27
|
+
LOCAL_JSX = "local-jsx" # 本地执行带 UI 组件
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class CommandArg:
|
|
32
|
+
"""命令参数定义"""
|
|
33
|
+
name: str
|
|
34
|
+
description: str
|
|
35
|
+
required: bool = False
|
|
36
|
+
default: Any = None
|
|
37
|
+
type_hint: str = "TEXT"
|
|
38
|
+
choices: Optional[List[str]] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Command:
|
|
43
|
+
"""命令定义(参考 Claude Code Command 接口)
|
|
44
|
+
|
|
45
|
+
示例:
|
|
46
|
+
run_command = Command(
|
|
47
|
+
name="run",
|
|
48
|
+
aliases=["start", "execute"],
|
|
49
|
+
description="运行完整工作流",
|
|
50
|
+
argument_hint="<keyword>",
|
|
51
|
+
immediate=True,
|
|
52
|
+
supports_non_interactive=True,
|
|
53
|
+
handler=run_workflow,
|
|
54
|
+
args=[
|
|
55
|
+
CommandArg("keyword", "搜索关键词", default="旅行"),
|
|
56
|
+
CommandArg("top_n", "推荐数量", type_hint="INTEGER", default=10),
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
"""
|
|
60
|
+
name: str
|
|
61
|
+
description: str
|
|
62
|
+
handler: Callable[..., Awaitable[Any]]
|
|
63
|
+
|
|
64
|
+
# 命令元数据
|
|
65
|
+
aliases: List[str] = field(default_factory=list)
|
|
66
|
+
argument_hint: Optional[str] = None # 参数提示,如 "[options]" 或 "<prompt>"
|
|
67
|
+
immediate: bool = False # 是否立即执行(不等待确认)
|
|
68
|
+
supports_non_interactive: bool = True # 是否支持非交互模式
|
|
69
|
+
hidden: bool = False # 是否在帮助中隐藏
|
|
70
|
+
|
|
71
|
+
# 参数定义
|
|
72
|
+
args: List[CommandArg] = field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
# 执行模式
|
|
75
|
+
mode: CommandMode = CommandMode.BOTH
|
|
76
|
+
|
|
77
|
+
# 短描述(用于帮助列表,长描述的截取版)
|
|
78
|
+
short_description: Optional[str] = None
|
|
79
|
+
|
|
80
|
+
def __post_init__(self):
|
|
81
|
+
if self.short_description is None:
|
|
82
|
+
# 截取 description 的前 60 个字符作为短描述
|
|
83
|
+
self.short_description = self.description[:60] + (
|
|
84
|
+
"..." if len(self.description) > 60 else ""
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class CommandRegistry:
|
|
89
|
+
"""命令注册表(参考 Claude Code 的命令系统)
|
|
90
|
+
|
|
91
|
+
使用示例:
|
|
92
|
+
registry = CommandRegistry()
|
|
93
|
+
|
|
94
|
+
# 注册命令
|
|
95
|
+
@registry.command(
|
|
96
|
+
name="run",
|
|
97
|
+
aliases=["start"],
|
|
98
|
+
description="运行完整工作流",
|
|
99
|
+
argument_hint="[options]",
|
|
100
|
+
)
|
|
101
|
+
async def run_workflow(keyword: str = "旅行"):
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
# 获取命令
|
|
105
|
+
cmd = registry.get("run")
|
|
106
|
+
|
|
107
|
+
# 列出所有命令
|
|
108
|
+
for cmd in registry.list_commands():
|
|
109
|
+
print(f" {cmd.name}: {cmd.short_description}")
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self):
|
|
113
|
+
self._commands: Dict[str, Command] = {}
|
|
114
|
+
|
|
115
|
+
def command(
|
|
116
|
+
self,
|
|
117
|
+
name: str,
|
|
118
|
+
aliases: Optional[List[str]] = None,
|
|
119
|
+
description: str = "",
|
|
120
|
+
argument_hint: Optional[str] = None,
|
|
121
|
+
immediate: bool = False,
|
|
122
|
+
supports_non_interactive: bool = True,
|
|
123
|
+
):
|
|
124
|
+
"""装饰器:注册命令
|
|
125
|
+
|
|
126
|
+
使用示例:
|
|
127
|
+
@registry.command(
|
|
128
|
+
name="run",
|
|
129
|
+
description="运行完整工作流",
|
|
130
|
+
argument_hint="[options]",
|
|
131
|
+
)
|
|
132
|
+
async def run_workflow(args):
|
|
133
|
+
...
|
|
134
|
+
"""
|
|
135
|
+
def decorator(func: Callable[..., Awaitable[Any]]) -> Command:
|
|
136
|
+
cmd = Command(
|
|
137
|
+
name=name,
|
|
138
|
+
description=description,
|
|
139
|
+
handler=func,
|
|
140
|
+
aliases=aliases or [],
|
|
141
|
+
argument_hint=argument_hint,
|
|
142
|
+
immediate=immediate,
|
|
143
|
+
supports_non_interactive=supports_non_interactive,
|
|
144
|
+
)
|
|
145
|
+
self.register(cmd)
|
|
146
|
+
return cmd
|
|
147
|
+
return decorator
|
|
148
|
+
|
|
149
|
+
def register(self, cmd: Command) -> None:
|
|
150
|
+
"""注册命令"""
|
|
151
|
+
# 注册主名称
|
|
152
|
+
self._commands[cmd.name] = cmd
|
|
153
|
+
|
|
154
|
+
# 注册别名
|
|
155
|
+
for alias in cmd.aliases:
|
|
156
|
+
if alias in self._commands:
|
|
157
|
+
raise ValueError(f"命令别名 '{alias}' 已存在")
|
|
158
|
+
self._commands[alias] = cmd
|
|
159
|
+
|
|
160
|
+
def get(self, name: str) -> Optional[Command]:
|
|
161
|
+
"""获取命令"""
|
|
162
|
+
return self._commands.get(name)
|
|
163
|
+
|
|
164
|
+
def list_commands(self, include_hidden: bool = False) -> List[Command]:
|
|
165
|
+
"""列出所有命令"""
|
|
166
|
+
cmds = list(self._commands.values())
|
|
167
|
+
# 去重(因为别名指向同一个 Command 对象)
|
|
168
|
+
seen = set()
|
|
169
|
+
unique = []
|
|
170
|
+
for cmd in cmds:
|
|
171
|
+
if cmd.name not in seen:
|
|
172
|
+
seen.add(cmd.name)
|
|
173
|
+
if not cmd.hidden or include_hidden:
|
|
174
|
+
unique.append(cmd)
|
|
175
|
+
return unique
|
|
176
|
+
|
|
177
|
+
def has(self, name: str) -> bool:
|
|
178
|
+
"""检查命令是否存在"""
|
|
179
|
+
return name in self._commands
|
|
180
|
+
|
|
181
|
+
def execute(
|
|
182
|
+
self,
|
|
183
|
+
name: str,
|
|
184
|
+
args: Optional[List[str]] = None,
|
|
185
|
+
**kwargs
|
|
186
|
+
) -> Awaitable[Any]:
|
|
187
|
+
"""执行命令"""
|
|
188
|
+
cmd = self.get(name)
|
|
189
|
+
if not cmd:
|
|
190
|
+
raise ValueError(f"未知命令:{name}")
|
|
191
|
+
return cmd.handler(*args or [], **kwargs)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# =============================================================================
|
|
195
|
+
# 输出格式化(参考 Claude Code print.ts)
|
|
196
|
+
# =============================================================================
|
|
197
|
+
|
|
198
|
+
class OutputFormatter:
|
|
199
|
+
"""输出格式化工具"""
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def format_command_list(commands: List[Command]) -> str:
|
|
203
|
+
"""格式化命令列表输出"""
|
|
204
|
+
lines = []
|
|
205
|
+
|
|
206
|
+
# 分组:常用命令和其他命令
|
|
207
|
+
common_commands = [c for c in commands if c.immediate]
|
|
208
|
+
other_commands = [c for c in commands if not c.immediate and not c.hidden]
|
|
209
|
+
|
|
210
|
+
if common_commands:
|
|
211
|
+
lines.append("常用命令:")
|
|
212
|
+
for cmd in sorted(common_commands, key=lambda x: x.name):
|
|
213
|
+
hint = f" {cmd.argument_hint}" if cmd.argument_hint else ""
|
|
214
|
+
lines.append(f" {cmd.name}{hint}")
|
|
215
|
+
lines.append(f" {cmd.short_description}")
|
|
216
|
+
lines.append("")
|
|
217
|
+
|
|
218
|
+
if other_commands:
|
|
219
|
+
lines.append("其他命令:")
|
|
220
|
+
for cmd in sorted(other_commands, key=lambda x: x.name):
|
|
221
|
+
hint = f" {cmd.argument_hint}" if cmd.argument_hint else ""
|
|
222
|
+
aliases_str = f" (别名:{', '.join(cmd.aliases)})" if cmd.aliases else ""
|
|
223
|
+
lines.append(f" {cmd.name}{hint}{aliases_str}")
|
|
224
|
+
lines.append(f" {cmd.short_description}")
|
|
225
|
+
|
|
226
|
+
return "\n".join(lines)
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def format_command_help(cmd: Command) -> str:
|
|
230
|
+
"""格式化单个命令的帮助信息"""
|
|
231
|
+
lines = []
|
|
232
|
+
|
|
233
|
+
# 命令名称和参数提示
|
|
234
|
+
header = cmd.name
|
|
235
|
+
if cmd.argument_hint:
|
|
236
|
+
header += f" {cmd.argument_hint}"
|
|
237
|
+
if cmd.aliases:
|
|
238
|
+
header += f" (别名:{', '.join(cmd.aliases)})"
|
|
239
|
+
|
|
240
|
+
lines.append(f"用法:{header}")
|
|
241
|
+
lines.append("")
|
|
242
|
+
lines.append(cmd.description)
|
|
243
|
+
lines.append("")
|
|
244
|
+
|
|
245
|
+
# 参数列表
|
|
246
|
+
if cmd.args:
|
|
247
|
+
lines.append("参数:")
|
|
248
|
+
for arg in cmd.args:
|
|
249
|
+
req = "必填" if arg.required else "可选"
|
|
250
|
+
default = f" [默认:{arg.default}]" if arg.default is not None else ""
|
|
251
|
+
lines.append(f" {arg.name} ({arg.type_hint}, {req}){default}")
|
|
252
|
+
lines.append(f" {arg.description}")
|
|
253
|
+
|
|
254
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""数据采集模块"""
|
|
2
|
+
from collectors.xiaohongshu import XiaohongshuCollector
|
|
3
|
+
from collectors.weibo import WeiboCollector
|
|
4
|
+
from collectors.wenlv import WenlvCollector
|
|
5
|
+
from collectors.ota.fliggy import FliggyCollector
|
|
6
|
+
from collectors.ota.ctrip import CtripCollector
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"XiaohongshuCollector",
|
|
10
|
+
"WeiboCollector",
|
|
11
|
+
"WenlvCollector",
|
|
12
|
+
"FliggyCollector",
|
|
13
|
+
"CtripCollector",
|
|
14
|
+
]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""携程数据采集器
|
|
2
|
+
|
|
3
|
+
爬取携程平台的机票和酒店价格
|
|
4
|
+
参考:知乎 2025 爬虫教程
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
from config.models import FlightInfo, HotelInfo
|
|
11
|
+
from utils.storage import Storage
|
|
12
|
+
from utils.http import get_client
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CtripCollector:
|
|
16
|
+
"""携程 OTA 采集器"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.storage = Storage()
|
|
20
|
+
self.base_url = "https://www.ctrip.com"
|
|
21
|
+
|
|
22
|
+
async def search_flights(
|
|
23
|
+
self,
|
|
24
|
+
departure_city: str,
|
|
25
|
+
arrival_city: str,
|
|
26
|
+
departure_date: Optional[str] = None,
|
|
27
|
+
) -> List[FlightInfo]:
|
|
28
|
+
"""搜索机票价格"""
|
|
29
|
+
print(f"携程搜索机票:{departure_city} -> {arrival_city}")
|
|
30
|
+
|
|
31
|
+
# 返回示例数据
|
|
32
|
+
return self._get_sample_flights(departure_city, arrival_city)
|
|
33
|
+
|
|
34
|
+
async def search_hotels(
|
|
35
|
+
self,
|
|
36
|
+
destination: str,
|
|
37
|
+
check_in: Optional[str] = None,
|
|
38
|
+
check_out: Optional[str] = None,
|
|
39
|
+
) -> List[HotelInfo]:
|
|
40
|
+
"""搜索酒店价格"""
|
|
41
|
+
print(f"携程搜索酒店:{destination}")
|
|
42
|
+
|
|
43
|
+
# 返回示例数据
|
|
44
|
+
return self._get_sample_hotels(destination)
|
|
45
|
+
|
|
46
|
+
def _get_sample_flights(
|
|
47
|
+
self, departure: str, arrival: str
|
|
48
|
+
) -> List[FlightInfo]:
|
|
49
|
+
"""示例航班数据"""
|
|
50
|
+
samples = [
|
|
51
|
+
{
|
|
52
|
+
"id": f"ctrip_flight_{departure}_{arrival}_001",
|
|
53
|
+
"departure_city": departure,
|
|
54
|
+
"arrival_city": arrival,
|
|
55
|
+
"price": 750 + hash(departure + arrival) % 500,
|
|
56
|
+
"airline": "中国国航",
|
|
57
|
+
"departure_time": "07:30",
|
|
58
|
+
"travel_time": "2h 30m",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": f"ctrip_flight_{departure}_{arrival}_002",
|
|
62
|
+
"departure_city": departure,
|
|
63
|
+
"arrival_city": arrival,
|
|
64
|
+
"price": 680 + hash(departure + arrival) % 400,
|
|
65
|
+
"airline": "海南航空",
|
|
66
|
+
"departure_time": "12:00",
|
|
67
|
+
"travel_time": "2h 40m",
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
return [
|
|
72
|
+
FlightInfo(
|
|
73
|
+
id=s["id"],
|
|
74
|
+
departure_city=s["departure_city"],
|
|
75
|
+
arrival_city=s["arrival_city"],
|
|
76
|
+
price=s["price"],
|
|
77
|
+
airline=s.get("airline"),
|
|
78
|
+
departure_time=s.get("departure_time"),
|
|
79
|
+
travel_time=s.get("travel_time"),
|
|
80
|
+
platform="ctrip",
|
|
81
|
+
)
|
|
82
|
+
for s in samples
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
def _get_sample_hotels(self, destination: str) -> List[HotelInfo]:
|
|
86
|
+
"""示例酒店数据"""
|
|
87
|
+
samples = [
|
|
88
|
+
{
|
|
89
|
+
"id": f"ctrip_hotel_{destination}_001",
|
|
90
|
+
"name": f"{destination}希尔顿酒店",
|
|
91
|
+
"destination": destination,
|
|
92
|
+
"price_per_night": 600 + hash(destination) % 300,
|
|
93
|
+
"rating": 4.7,
|
|
94
|
+
"stars": 5,
|
|
95
|
+
"url": f"https://www.ctrip.com/hotel/{destination}_001",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": f"ctrip_hotel_{destination}_002",
|
|
99
|
+
"name": f"{destination}亚朵酒店",
|
|
100
|
+
"destination": destination,
|
|
101
|
+
"price_per_night": 350 + hash(destination) % 200,
|
|
102
|
+
"rating": 4.6,
|
|
103
|
+
"stars": 4,
|
|
104
|
+
"url": f"https://www.ctrip.com/hotel/{destination}_002",
|
|
105
|
+
},
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
return [
|
|
109
|
+
HotelInfo(
|
|
110
|
+
id=s["id"],
|
|
111
|
+
name=s["name"],
|
|
112
|
+
destination=s["destination"],
|
|
113
|
+
price_per_night=s["price_per_night"],
|
|
114
|
+
rating=s.get("rating"),
|
|
115
|
+
stars=s.get("stars"),
|
|
116
|
+
platform="ctrip",
|
|
117
|
+
url=s.get("url"),
|
|
118
|
+
)
|
|
119
|
+
for s in samples
|
|
120
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""飞猪数据采集器
|
|
2
|
+
|
|
3
|
+
爬取飞猪平台的机票和酒店价格
|
|
4
|
+
参考:RealDataAPI, 知乎 2025 爬虫教程
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
from typing import List, Optional, Dict
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
|
|
10
|
+
from config.models import FlightInfo, HotelInfo
|
|
11
|
+
from utils.storage import Storage
|
|
12
|
+
from utils.http import get_client
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FliggyCollector:
|
|
16
|
+
"""飞猪 OTA 采集器"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.storage = Storage()
|
|
20
|
+
self.base_url = "https://www.fliggy.com"
|
|
21
|
+
|
|
22
|
+
async def search_flights(
|
|
23
|
+
self,
|
|
24
|
+
departure_city: str,
|
|
25
|
+
arrival_city: str,
|
|
26
|
+
departure_date: Optional[str] = None,
|
|
27
|
+
) -> List[FlightInfo]:
|
|
28
|
+
"""搜索机票价格
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
departure_city: 出发城市
|
|
32
|
+
arrival_city: 到达城市
|
|
33
|
+
departure_date: 出发日期,格式 YYYY-MM-DD
|
|
34
|
+
|
|
35
|
+
注意:飞猪有严格反爬,需要处理:
|
|
36
|
+
1. 动态加载(使用 Playwright)
|
|
37
|
+
2. 验证码
|
|
38
|
+
3. Cookie 验证
|
|
39
|
+
"""
|
|
40
|
+
# 简化示例,实际需要集成 Playwright
|
|
41
|
+
print(f"搜索机票:{departure_city} -> {arrival_city}")
|
|
42
|
+
|
|
43
|
+
# 返回示例数据
|
|
44
|
+
return self._get_sample_flights(departure_city, arrival_city)
|
|
45
|
+
|
|
46
|
+
async def search_hotels(
|
|
47
|
+
self,
|
|
48
|
+
destination: str,
|
|
49
|
+
check_in: Optional[str] = None,
|
|
50
|
+
check_out: Optional[str] = None,
|
|
51
|
+
price_min: Optional[int] = None,
|
|
52
|
+
price_max: Optional[int] = None,
|
|
53
|
+
) -> List[HotelInfo]:
|
|
54
|
+
"""搜索酒店价格"""
|
|
55
|
+
print(f"搜索酒店:{destination}")
|
|
56
|
+
|
|
57
|
+
# 返回示例数据
|
|
58
|
+
return self._get_sample_hotels(destination)
|
|
59
|
+
|
|
60
|
+
def _get_sample_flights(
|
|
61
|
+
self, departure: str, arrival: str
|
|
62
|
+
) -> List[FlightInfo]:
|
|
63
|
+
"""示例航班数据"""
|
|
64
|
+
samples = [
|
|
65
|
+
{
|
|
66
|
+
"id": f"fliggy_flight_{departure}_{arrival}_001",
|
|
67
|
+
"departure_city": departure,
|
|
68
|
+
"arrival_city": arrival,
|
|
69
|
+
"price": 800 + hash(departure + arrival) % 500,
|
|
70
|
+
"airline": "中国国航",
|
|
71
|
+
"departure_time": "08:00",
|
|
72
|
+
"travel_time": "2h 30m",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": f"fliggy_flight_{departure}_{arrival}_002",
|
|
76
|
+
"departure_city": departure,
|
|
77
|
+
"arrival_city": arrival,
|
|
78
|
+
"price": 650 + hash(departure + arrival) % 400,
|
|
79
|
+
"airline": "南方航空",
|
|
80
|
+
"departure_time": "14:30",
|
|
81
|
+
"travel_time": "2h 45m",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": f"fliggy_flight_{departure}_{arrival}_003",
|
|
85
|
+
"departure_city": departure,
|
|
86
|
+
"arrival_city": arrival,
|
|
87
|
+
"price": 1200 + hash(departure + arrival) % 600,
|
|
88
|
+
"airline": "东方航空",
|
|
89
|
+
"departure_time": "19:00",
|
|
90
|
+
"travel_time": "2h 20m",
|
|
91
|
+
},
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
return [
|
|
95
|
+
FlightInfo(
|
|
96
|
+
id=s["id"],
|
|
97
|
+
departure_city=s["departure_city"],
|
|
98
|
+
arrival_city=s["arrival_city"],
|
|
99
|
+
price=s["price"],
|
|
100
|
+
airline=s.get("airline"),
|
|
101
|
+
departure_time=s.get("departure_time"),
|
|
102
|
+
travel_time=s.get("travel_time"),
|
|
103
|
+
platform="fliggy",
|
|
104
|
+
)
|
|
105
|
+
for s in samples
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
def _get_sample_hotels(self, destination: str) -> List[HotelInfo]:
|
|
109
|
+
"""示例酒店数据"""
|
|
110
|
+
samples = [
|
|
111
|
+
{
|
|
112
|
+
"id": f"fliggy_hotel_{destination}_001",
|
|
113
|
+
"name": f"{destination}国际大酒店",
|
|
114
|
+
"destination": destination,
|
|
115
|
+
"price_per_night": 500 + hash(destination) % 300,
|
|
116
|
+
"rating": 4.8,
|
|
117
|
+
"stars": 5,
|
|
118
|
+
"url": f"https://www.fliggy.com/hotel/{destination}_001",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": f"fliggy_hotel_{destination}_002",
|
|
122
|
+
"name": f"{destination}精品民宿",
|
|
123
|
+
"destination": destination,
|
|
124
|
+
"price_per_night": 280 + hash(destination) % 200,
|
|
125
|
+
"rating": 4.5,
|
|
126
|
+
"stars": None,
|
|
127
|
+
"url": f"https://www.fliggy.com/hotel/{destination}_002",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"id": f"fliggy_hotel_{destination}_003",
|
|
131
|
+
"name": f"{destination}经济酒店",
|
|
132
|
+
"destination": destination,
|
|
133
|
+
"price_per_night": 150 + hash(destination) % 100,
|
|
134
|
+
"rating": 4.2,
|
|
135
|
+
"stars": 3,
|
|
136
|
+
"url": f"https://www.fliggy.com/hotel/{destination}_003",
|
|
137
|
+
},
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
return [
|
|
141
|
+
HotelInfo(
|
|
142
|
+
id=s["id"],
|
|
143
|
+
name=s["name"],
|
|
144
|
+
destination=s["destination"],
|
|
145
|
+
price_per_night=s["price_per_night"],
|
|
146
|
+
rating=s.get("rating"),
|
|
147
|
+
stars=s.get("stars"),
|
|
148
|
+
platform="fliggy",
|
|
149
|
+
url=s.get("url"),
|
|
150
|
+
)
|
|
151
|
+
for s in samples
|
|
152
|
+
]
|