travel-agent-tools 1.0.0__tar.gz

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 (27) hide show
  1. travel_agent_tools-1.0.0/PKG-INFO +84 -0
  2. travel_agent_tools-1.0.0/README.md +63 -0
  3. travel_agent_tools-1.0.0/config/__init__.py +14 -0
  4. travel_agent_tools-1.0.0/config/settings.py +45 -0
  5. travel_agent_tools-1.0.0/mcp_server/__init__.py +69 -0
  6. travel_agent_tools-1.0.0/pyproject.toml +50 -0
  7. travel_agent_tools-1.0.0/server.py +130 -0
  8. travel_agent_tools-1.0.0/setup.cfg +4 -0
  9. travel_agent_tools-1.0.0/templates/__init__.py +1 -0
  10. travel_agent_tools-1.0.0/templates/checklist.html +36 -0
  11. travel_agent_tools-1.0.0/templates/photo_spots.html +36 -0
  12. travel_agent_tools-1.0.0/templates/share_card.html +45 -0
  13. travel_agent_tools-1.0.0/templates/travel_guide.html +66 -0
  14. travel_agent_tools-1.0.0/tools/__init__.py +211 -0
  15. travel_agent_tools-1.0.0/tools/booking_link/__init__.py +116 -0
  16. travel_agent_tools-1.0.0/tools/data_collector/__init__.py +143 -0
  17. travel_agent_tools-1.0.0/tools/html_generator/__init__.py +78 -0
  18. travel_agent_tools-1.0.0/tools/map_mcp/__init__.py +214 -0
  19. travel_agent_tools-1.0.0/tools/pdf_generator/__init__.py +127 -0
  20. travel_agent_tools-1.0.0/tools/spatial_mcp/__init__.py +175 -0
  21. travel_agent_tools-1.0.0/tools/ticket_steps/__init__.py +83 -0
  22. travel_agent_tools-1.0.0/travel_agent_tools.egg-info/PKG-INFO +84 -0
  23. travel_agent_tools-1.0.0/travel_agent_tools.egg-info/SOURCES.txt +25 -0
  24. travel_agent_tools-1.0.0/travel_agent_tools.egg-info/dependency_links.txt +1 -0
  25. travel_agent_tools-1.0.0/travel_agent_tools.egg-info/entry_points.txt +2 -0
  26. travel_agent_tools-1.0.0/travel_agent_tools.egg-info/requires.txt +6 -0
  27. travel_agent_tools-1.0.0/travel_agent_tools.egg-info/top_level.txt +5 -0
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: travel-agent-tools
3
+ Version: 1.0.0
4
+ Summary: 旅行特工队 - 工具层:面向智能旅行规划的 MCP 工具集(华为ICT创新赛)
5
+ Author: Travel Agent Team
6
+ License: MIT
7
+ Keywords: mcp,travel,agent,huawei,agentarts
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: requests>=2.31.0
16
+ Requires-Dist: Jinja2>=3.1.2
17
+ Requires-Dist: reportlab>=4.0.4
18
+ Requires-Dist: beautifulsoup4>=4.12.3
19
+ Requires-Dist: loguru>=0.7.2
20
+ Requires-Dist: python-dotenv>=1.0.1
21
+
22
+ # 旅行特工队 - 工具层(travel-agent-tools)
23
+
24
+ > 华为 ICT 创新赛参赛作品 · 智果(AgentArts)平台 MCP 工具集
25
+
26
+ 面向年轻群体的智能旅行规划系统 **工具层**,通过 MCP(Model Context Protocol)协议对外提供
27
+ 7 大类共 **30 个工具**,可直接接入智果(AgentArts)智能体平台。
28
+
29
+ ## 工具清单
30
+
31
+ | 模块 | 工具 |
32
+ |---|---|
33
+ | 地图 MCP | `search_poi` `planning_route` `geocode_address` `reverse_geocode` `search_around` |
34
+ | 空间计算 MCP | `calculate_distance` `estimate_commute_time` `optimize_route_order` `find_enroute_attractions` `get_pois_center` `split_pois_by_day` |
35
+ | 数据采集 | `fetch_reviews` `clean_reviews` `analyze_rating` `prioritize_pois` `summarize_reviews` |
36
+ | HTML 生成 | `generate_travel_guide_html` `generate_checklist_html` `generate_photo_spots_html` `generate_share_card_html` |
37
+ | PDF 生成 | `generate_checklist_pdf` `generate_itinerary_pdf` `generate_photo_guide_pdf` |
38
+ | 接口跳转 | `generate_booking_links` `generate_countdown` `aggregate_booking_status` `generate_calendar_reminder` |
39
+ | 抢票步骤 | `generate_ticket_steps` `generate_recommended_platform` `generate_ticket_tips` |
40
+
41
+ > 未配置 `AMAP_API_KEY` 时,地图类工具自动降级为示例数据,服务仍可启动与联调。
42
+
43
+ ## 本地部署
44
+
45
+ ```bash
46
+ # 1. 安装依赖(Python 3.10+)
47
+ pip install -r requirements.txt
48
+
49
+ # 2. 配置环境变量
50
+ copy .env.example .env
51
+ # 编辑 .env,填入高德地图 API Key(https://lbs.amap.com/ 申请)
52
+
53
+ # 3. 验证
54
+ python server.py --list
55
+ python server.py --test "search_poi:keyword=故宫,city=北京"
56
+
57
+ # 4. 启动
58
+ python server.py --http --port 8000 # HTTP 模式(测试)
59
+ python server.py # stdio 模式(MCP 标准,智果平台用)
60
+ ```
61
+
62
+ HTTP 模式端点:`GET /`(健康检查)、`GET /tools`(工具列表)、`POST /tools/call`(调用工具)。
63
+
64
+ ## 智果(AgentArts)平台部署
65
+
66
+ 1. 登录 https://www.huaweicloud.com/product/agentarts.html 智果平台控制台(需完成认证)
67
+ 2. 「连接器管理」→「新建连接器」:类型 MCP Server,运行环境 Python 3.10+
68
+ 3. 上传 `travel-agent-tools.zip` 代码包,启动命令:`python server.py`
69
+ 4. 环境变量:`AMAP_API_KEY`(必填)、`MCP_SERVER_NAME=travel-agent-tools`、`LOG_LEVEL=INFO`
70
+ 5. 资源规格:开发测试 0.5核1GB 即可(本项目为轻量 CPU 应用)
71
+ 6. 部署 → 测试连接 → 在智能体中绑定连接器并勾选工具
72
+
73
+ ## 扩展开发
74
+
75
+ 1. 在 `tools/<模块>/` 实现新函数
76
+ 2. 在 `tools/__init__.py` 的 `HANDLERS` 与 `TOOLS` 中注册
77
+ 3. 重新打包部署到智果平台
78
+
79
+ ## 技术栈
80
+
81
+ Python 3.10+ · MCP(JSON-RPC over stdio) · 高德地图 API · Jinja2 · ReportLab · BeautifulSoup4 · Haversine · Loguru
82
+
83
+ ---
84
+ 版本 v1.0.0 · 华为 ICT 创新赛 - 旅行特工队
@@ -0,0 +1,63 @@
1
+ # 旅行特工队 - 工具层(travel-agent-tools)
2
+
3
+ > 华为 ICT 创新赛参赛作品 · 智果(AgentArts)平台 MCP 工具集
4
+
5
+ 面向年轻群体的智能旅行规划系统 **工具层**,通过 MCP(Model Context Protocol)协议对外提供
6
+ 7 大类共 **30 个工具**,可直接接入智果(AgentArts)智能体平台。
7
+
8
+ ## 工具清单
9
+
10
+ | 模块 | 工具 |
11
+ |---|---|
12
+ | 地图 MCP | `search_poi` `planning_route` `geocode_address` `reverse_geocode` `search_around` |
13
+ | 空间计算 MCP | `calculate_distance` `estimate_commute_time` `optimize_route_order` `find_enroute_attractions` `get_pois_center` `split_pois_by_day` |
14
+ | 数据采集 | `fetch_reviews` `clean_reviews` `analyze_rating` `prioritize_pois` `summarize_reviews` |
15
+ | HTML 生成 | `generate_travel_guide_html` `generate_checklist_html` `generate_photo_spots_html` `generate_share_card_html` |
16
+ | PDF 生成 | `generate_checklist_pdf` `generate_itinerary_pdf` `generate_photo_guide_pdf` |
17
+ | 接口跳转 | `generate_booking_links` `generate_countdown` `aggregate_booking_status` `generate_calendar_reminder` |
18
+ | 抢票步骤 | `generate_ticket_steps` `generate_recommended_platform` `generate_ticket_tips` |
19
+
20
+ > 未配置 `AMAP_API_KEY` 时,地图类工具自动降级为示例数据,服务仍可启动与联调。
21
+
22
+ ## 本地部署
23
+
24
+ ```bash
25
+ # 1. 安装依赖(Python 3.10+)
26
+ pip install -r requirements.txt
27
+
28
+ # 2. 配置环境变量
29
+ copy .env.example .env
30
+ # 编辑 .env,填入高德地图 API Key(https://lbs.amap.com/ 申请)
31
+
32
+ # 3. 验证
33
+ python server.py --list
34
+ python server.py --test "search_poi:keyword=故宫,city=北京"
35
+
36
+ # 4. 启动
37
+ python server.py --http --port 8000 # HTTP 模式(测试)
38
+ python server.py # stdio 模式(MCP 标准,智果平台用)
39
+ ```
40
+
41
+ HTTP 模式端点:`GET /`(健康检查)、`GET /tools`(工具列表)、`POST /tools/call`(调用工具)。
42
+
43
+ ## 智果(AgentArts)平台部署
44
+
45
+ 1. 登录 https://www.huaweicloud.com/product/agentarts.html 智果平台控制台(需完成认证)
46
+ 2. 「连接器管理」→「新建连接器」:类型 MCP Server,运行环境 Python 3.10+
47
+ 3. 上传 `travel-agent-tools.zip` 代码包,启动命令:`python server.py`
48
+ 4. 环境变量:`AMAP_API_KEY`(必填)、`MCP_SERVER_NAME=travel-agent-tools`、`LOG_LEVEL=INFO`
49
+ 5. 资源规格:开发测试 0.5核1GB 即可(本项目为轻量 CPU 应用)
50
+ 6. 部署 → 测试连接 → 在智能体中绑定连接器并勾选工具
51
+
52
+ ## 扩展开发
53
+
54
+ 1. 在 `tools/<模块>/` 实现新函数
55
+ 2. 在 `tools/__init__.py` 的 `HANDLERS` 与 `TOOLS` 中注册
56
+ 3. 重新打包部署到智果平台
57
+
58
+ ## 技术栈
59
+
60
+ Python 3.10+ · MCP(JSON-RPC over stdio) · 高德地图 API · Jinja2 · ReportLab · BeautifulSoup4 · Haversine · Loguru
61
+
62
+ ---
63
+ 版本 v1.0.0 · 华为 ICT 创新赛 - 旅行特工队
@@ -0,0 +1,14 @@
1
+ from .settings import ( # noqa: F401
2
+ AMAP_API_KEY,
3
+ BASE_DIR,
4
+ HTML_DIR,
5
+ LOG_LEVEL,
6
+ MCP_SERVER_NAME,
7
+ OUTPUT_DIR,
8
+ PDF_DIR,
9
+ SERVER_NAME,
10
+ SERVER_VERSION,
11
+ TEMPLATES_DIR,
12
+ ensure_output_dirs,
13
+ has_amap_key,
14
+ )
@@ -0,0 +1,45 @@
1
+ """配置管理:统一读取环境变量与项目路径。"""
2
+ import os
3
+ from pathlib import Path
4
+
5
+ from dotenv import load_dotenv
6
+
7
+ BASE_DIR = Path(__file__).resolve().parent.parent
8
+
9
+ # 加载项目根目录下的 .env(若存在)
10
+ load_dotenv(BASE_DIR / ".env")
11
+
12
+ # ========== 环境变量 ==========
13
+ # 高德地图 API Key(必填)申请地址: https://lbs.amap.com/
14
+ AMAP_API_KEY = os.getenv("AMAP_API_KEY", "").strip()
15
+ # 服务名称(智果平台连接器标识)
16
+ MCP_SERVER_NAME = os.getenv("MCP_SERVER_NAME", "travel-agent-tools")
17
+ # 日志级别: DEBUG / INFO / WARNING / ERROR
18
+ LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
19
+
20
+ # ========== 目录 ==========
21
+ TEMPLATES_DIR = BASE_DIR / "templates"
22
+ OUTPUT_DIR = BASE_DIR / "output"
23
+ HTML_DIR = OUTPUT_DIR / "html"
24
+ PDF_DIR = OUTPUT_DIR / "pdf"
25
+
26
+
27
+ def ensure_output_dirs() -> None:
28
+ """确保输出目录存在;站点目录只读时(如 uvx 安装运行)回退到当前工作目录。"""
29
+ global OUTPUT_DIR, HTML_DIR, PDF_DIR
30
+ try:
31
+ for d in (HTML_DIR, PDF_DIR):
32
+ d.mkdir(parents=True, exist_ok=True)
33
+ except OSError:
34
+ base = Path.cwd() / "output"
35
+ OUTPUT_DIR, HTML_DIR, PDF_DIR = base, base / "html", base / "pdf"
36
+ for d in (HTML_DIR, PDF_DIR):
37
+ d.mkdir(parents=True, exist_ok=True)
38
+
39
+
40
+ def has_amap_key() -> bool:
41
+ return bool(AMAP_API_KEY)
42
+
43
+
44
+ SERVER_NAME = MCP_SERVER_NAME
45
+ SERVER_VERSION = "1.0.0"
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ """MCP 服务核心:JSON-RPC 2.0 请求分发(stdio / HTTP 共用)。"""
4
+ import json
5
+
6
+ from config.settings import SERVER_NAME, SERVER_VERSION
7
+ from tools import call_tool, list_tools
8
+
9
+ PROTOCOL_VERSION = "2024-11-05"
10
+
11
+
12
+ def handle_request(request: dict) -> dict | None:
13
+ """处理一条 JSON-RPC 2.0 请求,返回响应;通知类请求返回 None。"""
14
+ method = request.get("method", "")
15
+ req_id = request.get("id")
16
+
17
+ # 通知(无 id)不需要响应
18
+ if req_id is None:
19
+ return None
20
+
21
+ if method == "initialize":
22
+ return {"jsonrpc": "2.0", "id": req_id, "result": {
23
+ "protocolVersion": PROTOCOL_VERSION,
24
+ "capabilities": {"tools": {}},
25
+ "serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
26
+ }}
27
+
28
+ if method == "ping":
29
+ return {"jsonrpc": "2.0", "id": req_id, "result": {}}
30
+
31
+ if method == "tools/list":
32
+ return {"jsonrpc": "2.0", "id": req_id, "result": {"tools": list_tools()}}
33
+
34
+ if method == "tools/call":
35
+ params = request.get("params") or {}
36
+ name = params.get("name", "")
37
+ arguments = params.get("arguments") or {}
38
+ try:
39
+ result_text = _call(name, arguments)
40
+ return {"jsonrpc": "2.0", "id": req_id, "result": {
41
+ "content": [{"type": "text", "text": result_text}],
42
+ "isError": False,
43
+ }}
44
+ except Exception as e: # noqa: BLE001
45
+ return {"jsonrpc": "2.0", "id": req_id, "result": {
46
+ "content": [{"type": "text", "text": f"工具调用失败: {e}"}],
47
+ "isError": True,
48
+ }}
49
+
50
+ return {"jsonrpc": "2.0", "id": req_id,
51
+ "error": {"code": -32601, "message": f"Method not found: {method}"}} if req_id is not None else None
52
+
53
+
54
+ def _call(name: str, arguments: dict) -> str:
55
+ from tools import call_tool as _ct
56
+ result = _ct(name, arguments)
57
+ return json.dumps(result, ensure_ascii=False, default=str, indent=2)
58
+
59
+
60
+ def handle_raw(raw: str) -> str | None:
61
+ """处理一行原始 JSON 文本,返回响应文本或 None。"""
62
+ try:
63
+ request = json.loads(raw)
64
+ except json.JSONDecodeError:
65
+ req_id = None
66
+ return json.dumps({"jsonrpc": "2.0", "id": req_id,
67
+ "error": {"code": -32700, "message": "Parse error"}})
68
+ resp = handle_request(request)
69
+ return json.dumps(resp, ensure_ascii=False) if resp is not None else None
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "travel-agent-tools"
7
+ version = "1.0.0"
8
+ description = "旅行特工队 - 工具层:面向智能旅行规划的 MCP 工具集(华为ICT创新赛)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Travel Agent Team" }]
13
+ keywords = ["mcp", "travel", "agent", "huawei", "agentarts"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ ]
21
+ dependencies = [
22
+ "requests>=2.31.0",
23
+ "Jinja2>=3.1.2",
24
+ "reportlab>=4.0.4",
25
+ "beautifulsoup4>=4.12.3",
26
+ "loguru>=0.7.2",
27
+ "python-dotenv>=1.0.1",
28
+ ]
29
+
30
+ [project.scripts]
31
+ travel-agent-tools = "server:main"
32
+
33
+ [tool.setuptools]
34
+ py-modules = ["server"]
35
+ packages = [
36
+ "config",
37
+ "mcp_server",
38
+ "templates",
39
+ "tools",
40
+ "tools.map_mcp",
41
+ "tools.spatial_mcp",
42
+ "tools.data_collector",
43
+ "tools.html_generator",
44
+ "tools.pdf_generator",
45
+ "tools.booking_link",
46
+ "tools.ticket_steps",
47
+ ]
48
+
49
+ [tool.setuptools.package-data]
50
+ templates = ["*.html"]
@@ -0,0 +1,130 @@
1
+ """旅行特工队 - 工具层 MCP 服务入口。
2
+
3
+ 用法:
4
+ python server.py # stdio 模式(MCP 标准模式,智果平台默认)
5
+ python server.py --http --port 8000 # HTTP 模式(推荐用于本地测试)
6
+ python server.py --list # 列出所有可用工具
7
+ python server.py --test "search_poi:keyword=故宫,city=北京" # 测试某个工具
8
+ """
9
+ import argparse
10
+ import json
11
+ import sys
12
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
13
+
14
+ # 确保中文输出不乱码(Windows / 部分容器默认非 UTF-8)
15
+ for stream in (sys.stdout, sys.stderr):
16
+ try:
17
+ stream.reconfigure(encoding="utf-8")
18
+ except Exception: # noqa: BLE001
19
+ pass
20
+
21
+ from loguru import logger
22
+
23
+ from config.settings import LOG_LEVEL, SERVER_NAME, SERVER_VERSION
24
+ from mcp_server import handle_raw
25
+ from tools import call_tool_json, list_tools
26
+
27
+ logger.remove()
28
+ logger.add(sys.stderr, level=LOG_LEVEL)
29
+
30
+
31
+ def run_list() -> None:
32
+ tools = list_tools()
33
+ print(f"共 {len(tools)} 个可用工具:\n")
34
+ for t in tools:
35
+ print(f" - {t['name']}: {t['description']}")
36
+
37
+
38
+ def run_test(spec: str) -> None:
39
+ """spec 格式: 工具名:参数1=值1,参数2=值2"""
40
+ if ":" not in spec:
41
+ name, raw_args = spec, ""
42
+ else:
43
+ name, raw_args = spec.split(":", 1)
44
+ arguments: dict = {}
45
+ for kv in filter(None, raw_args.split(",")):
46
+ if "=" in kv:
47
+ k, v = kv.split("=", 1)
48
+ try:
49
+ v = int(v)
50
+ except ValueError:
51
+ try:
52
+ v = float(v)
53
+ except ValueError:
54
+ pass
55
+ arguments[k.strip()] = v
56
+ print(f"调用工具 {name},参数: {arguments}\n")
57
+ print(call_tool_json(name, arguments))
58
+
59
+
60
+ def run_stdio() -> None:
61
+ """MCP 标准模式:stdin 读取 JSON-RPC 请求,stdout 输出响应(日志全部走 stderr)。"""
62
+ logger.info(f"{SERVER_NAME} v{SERVER_VERSION} 已启动(stdio 模式)")
63
+ for line in sys.stdin:
64
+ line = line.strip()
65
+ if not line:
66
+ continue
67
+ resp = handle_raw(line)
68
+ if resp is not None:
69
+ sys.stdout.write(resp + "\n")
70
+ sys.stdout.flush()
71
+
72
+
73
+ def run_http(port: int) -> None:
74
+ class Handler(BaseHTTPRequestHandler):
75
+ def _send(self, code: int, obj) -> None:
76
+ body = json.dumps(obj, ensure_ascii=False, default=str).encode("utf-8")
77
+ self.send_response(code)
78
+ self.send_header("Content-Type", "application/json; charset=utf-8")
79
+ self.send_header("Content-Length", str(len(body)))
80
+ self.end_headers()
81
+ self.wfile.write(body)
82
+
83
+ def do_GET(self) -> None: # noqa: N802
84
+ if self.path == "/":
85
+ self._send(200, {"status": "ok", "server": SERVER_NAME,
86
+ "version": SERVER_VERSION, "tools": len(list_tools())})
87
+ elif self.path == "/tools":
88
+ self._send(200, {"tools": list_tools()})
89
+ else:
90
+ self._send(404, {"error": "not found, try / or /tools"})
91
+
92
+ def do_POST(self) -> None: # noqa: N802
93
+ if self.path != "/tools/call":
94
+ self._send(404, {"error": "not found, use POST /tools/call"})
95
+ return
96
+ try:
97
+ length = int(self.headers.get("Content-Length", 0))
98
+ payload = json.loads(self.rfile.read(length) or b"{}")
99
+ result = call_tool_json(payload.get("name", ""), payload.get("arguments") or {})
100
+ self._send(200, json.loads(result))
101
+ except Exception as e: # noqa: BLE001
102
+ self._send(400, {"success": False, "error": str(e)})
103
+
104
+ def log_message(self, *args) -> None: # 静默默认访问日志
105
+ logger.debug(args)
106
+
107
+ logger.info(f"{SERVER_NAME} v{SERVER_VERSION} HTTP 模式启动,端口 {port}")
108
+ ThreadingHTTPServer(("0.0.0.0", port), Handler).serve_forever()
109
+
110
+
111
+ def main() -> None:
112
+ parser = argparse.ArgumentParser(description="旅行特工队 - 工具层 MCP 服务")
113
+ parser.add_argument("--http", action="store_true", help="以 HTTP 模式启动(默认 stdio)")
114
+ parser.add_argument("--port", type=int, default=8000, help="HTTP 端口,默认 8000")
115
+ parser.add_argument("--list", action="store_true", help="列出所有可用工具")
116
+ parser.add_argument("--test", type=str, metavar="SPEC", help='测试工具,如 "search_poi:keyword=故宫,city=北京"')
117
+ args = parser.parse_args()
118
+
119
+ if args.list:
120
+ run_list()
121
+ elif args.test:
122
+ run_test(args.test)
123
+ elif args.http:
124
+ run_http(args.port)
125
+ else:
126
+ run_stdio()
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ # 模板资源包(随 PyPI 包分发,供 HTML 生成工具使用)
@@ -0,0 +1,36 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ title }}</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: "PingFang SC", "Microsoft YaHei", sans-serif; background: #f5f6fa; color: #2f3542; padding: 24px; }
10
+ .wrap { max-width: 680px; margin: 0 auto; }
11
+ h1 { text-align: center; margin: 16px 0 4px; }
12
+ .sub { text-align: center; color: #888; margin-bottom: 24px; }
13
+ .cat { background: #fff; border-radius: 12px; padding: 20px; margin-bottom: 14px; box-shadow: 0 2px 8px rgba(0,0,0,.06); }
14
+ .cat h2 { font-size: 18px; margin-bottom: 12px; color: #ff5e57; }
15
+ .item { display: flex; align-items: flex-start; gap: 10px; padding: 6px 0; line-height: 1.5; }
16
+ .box { width: 18px; height: 18px; border: 2px solid #b0b3ba; border-radius: 4px; flex: none; margin-top: 2px; }
17
+ .item .note { color: #888; font-size: 13px; }
18
+ footer { text-align: center; color: #999; font-size: 13px; padding: 24px 0; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <div class="wrap">
23
+ <h1>☑ {{ title }}</h1>
24
+ {% if destination %}<p class="sub">目的地:{{ destination }}</p>{% endif %}
25
+ {% for c in categories %}
26
+ <div class="cat">
27
+ <h2>{{ c.get('name', '') }}</h2>
28
+ {% for it in c.get('items', []) %}
29
+ <div class="item"><span class="box"></span><div>{{ it.get('name', it) if it is mapping else it }}{% if it is mapping and it.get('note') %}<br><span class="note">{{ it['note'] }}</span>{% endif %}</div></div>
30
+ {% endfor %}
31
+ </div>
32
+ {% endfor %}
33
+ <footer>旅行特工队 · 打印或截图打卡 {{ generated_at }}</footer>
34
+ </div>
35
+ </body>
36
+ </html>
@@ -0,0 +1,36 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ destination }} 拍照指南</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: "PingFang SC", "Microsoft YaHei", sans-serif; background: #1e272e; color: #f5f6fa; padding: 24px; }
10
+ .wrap { max-width: 720px; margin: 0 auto; }
11
+ h1 { text-align: center; margin: 16px 0; }
12
+ .card { background: #2f3640; border-radius: 14px; padding: 20px; margin-bottom: 16px; border-left: 4px solid #ff9f43; }
13
+ .card h2 { font-size: 19px; margin-bottom: 10px; }
14
+ .tag { display: inline-block; background: #ff9f43; color: #1e272e; border-radius: 999px; padding: 3px 12px; font-size: 13px; margin-right: 8px; margin-bottom: 8px; }
15
+ .card p { line-height: 1.7; color: #d2dae2; font-size: 14px; }
16
+ .card p b { color: #ffb27a; }
17
+ footer { text-align: center; color: #777; font-size: 13px; padding: 24px 0; }
18
+ </style>
19
+ </head>
20
+ <body>
21
+ <div class="wrap">
22
+ <h1>📷 {{ destination or '旅行' }} 拍照指南</h1>
23
+ {% for s in spots %}
24
+ <div class="card">
25
+ <h2>{{ s.get('name', s) if s is mapping else s }}</h2>
26
+ {% if s is mapping %}
27
+ {% if s.get('best_time') %}<span class="tag">⏰ {{ s['best_time'] }}</span>{% endif %}
28
+ {% if s.get('angle') %}<p><b>机位建议:</b>{{ s['angle'] }}</p>{% endif %}
29
+ {% if s.get('tip') %}<p><b>小贴士:</b>{{ s['tip'] }}</p>{% endif %}
30
+ {% endif %}
31
+ </div>
32
+ {% endfor %}
33
+ <footer>旅行特工队 · 出片率拉满 {{ generated_at }}</footer>
34
+ </div>
35
+ </body>
36
+ </html>
@@ -0,0 +1,45 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ destination }} 旅行分享卡片</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { background: #2f3542; display: flex; justify-content: center; padding: 32px 16px; font-family: "PingFang SC", "Microsoft YaHei", sans-serif; }
10
+ .card { width: 420px; max-width: 100%; background: linear-gradient(160deg, #ff5e57 0%, #ff9f43 60%, #ffd32a 100%); border-radius: 24px; padding: 36px 28px; color: #fff; box-shadow: 0 16px 48px rgba(0,0,0,.35); }
11
+ .brand { font-size: 13px; opacity: .85; letter-spacing: 2px; }
12
+ h1 { font-size: 34px; margin: 10px 0 4px; }
13
+ .sub { font-size: 15px; opacity: .92; margin-bottom: 20px; }
14
+ .stats { display: flex; gap: 10px; margin-bottom: 20px; }
15
+ .stat { flex: 1; background: rgba(255,255,255,.2); backdrop-filter: blur(4px); border-radius: 12px; padding: 12px 8px; text-align: center; }
16
+ .stat b { display: block; font-size: 18px; }
17
+ .stat span { font-size: 12px; opacity: .9; }
18
+ .quote { background: rgba(255,255,255,.16); border-radius: 12px; padding: 14px 16px; font-size: 15px; line-height: 1.6; margin-bottom: 20px; }
19
+ .routes { font-size: 13.5px; line-height: 1.9; margin-bottom: 8px; }
20
+ .routes li { margin-left: 18px; }
21
+ .author { text-align: right; font-size: 14px; opacity: .9; margin-top: 18px; }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <div class="card">
26
+ <div class="brand">TRAVEL AGENT · 旅行特工队</div>
27
+ <h1>{{ destination }}</h1>
28
+ <div class="sub">{{ days }} 天 {{ travelers }} 人的美好旅程</div>
29
+ <div class="stats">
30
+ <div class="stat"><b>{{ days }}</b><span>天数</span></div>
31
+ <div class="stat"><b>{{ travelers }}</b><span>同行人数</span></div>
32
+ <div class="stat"><b>¥{{ budget }}</b><span>预算</span></div>
33
+ </div>
34
+ {% if highlight_text %}<div class="quote">“{{ highlight_text }}”</div>{% endif %}
35
+ {% if daily_routes %}
36
+ <ul class="routes">
37
+ {% for d in daily_routes %}
38
+ <li>Day {{ d.get('day', loop.index) }}:{% if d.get('title') %}{{ d['title'] }}{% elif d.get('pois') %}{{ d['pois'] | join(' → ') }}{% endif %}</li>
39
+ {% endfor %}
40
+ </ul>
41
+ {% endif %}
42
+ <div class="author">—— {{ author }}</div>
43
+ </div>
44
+ </body>
45
+ </html>
@@ -0,0 +1,66 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ destination }} · {{ days }} 日旅行攻略</title>
7
+ <style>
8
+ :root { --primary: #ff5e57; --dark: #2f3542; --light: #f5f6fa; }
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
+ body { font-family: "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--light); color: var(--dark); }
11
+ .hero { background: linear-gradient(135deg, #ff5e57 0%, #ff9f43 100%); color: #fff; padding: 48px 24px; text-align: center; }
12
+ .hero h1 { font-size: 32px; margin-bottom: 12px; }
13
+ .meta { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; margin-top: 16px; }
14
+ .meta span { background: rgba(255,255,255,.22); padding: 6px 14px; border-radius: 999px; font-size: 14px; }
15
+ .container { max-width: 860px; margin: 0 auto; padding: 24px; }
16
+ h2 { margin: 32px 0 16px; padding-left: 12px; border-left: 4px solid var(--primary); font-size: 22px; }
17
+ .highlights li, .tips li { margin: 8px 0 8px 20px; line-height: 1.6; }
18
+ .day-card { background: #fff; border-radius: 12px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 10px rgba(0,0,0,.06); }
19
+ .day-card h3 { color: var(--primary); margin-bottom: 8px; }
20
+ .day-card ul { margin-left: 20px; line-height: 1.8; }
21
+ .note { color: #57606f; font-size: 14px; margin-top: 8px; }
22
+ footer { text-align: center; color: #999; font-size: 13px; padding: 32px 0; }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <div class="hero">
27
+ <h1>{{ destination }} · {{ days }} 日游攻略</h1>
28
+ <div class="meta">
29
+ <span>📅 出发:{{ start_date or '待定' }}</span>
30
+ <span>👥 {{ travelers }} 人</span>
31
+ <span>💰 预算 ¥{{ budget }}</span>
32
+ </div>
33
+ </div>
34
+ <div class="container">
35
+ {% if highlights %}
36
+ <h2>✨ 行程亮点</h2>
37
+ <ul class="highlights">{% for h in highlights %}<li>{{ h }}</li>{% endfor %}</ul>
38
+ {% endif %}
39
+
40
+ {% if daily_routes %}
41
+ <h2>🗺 每日路线</h2>
42
+ {% for d in daily_routes %}
43
+ <div class="day-card">
44
+ <h3>Day {{ d.get('day', loop.index) }}{% if d.get('title') %} · {{ d['title'] }}{% endif %}</h3>
45
+ <ul>
46
+ {% for p in d.get('pois', []) %}
47
+ {% if p is mapping %}
48
+ <li>{% if p.get('time') %}{{ p['time'] }} {% endif %}{{ p.get('name', '') }}{% if p.get('note') %} —— {{ p['note'] }}{% endif %}</li>
49
+ {% else %}
50
+ <li>{{ p }}</li>
51
+ {% endif %}
52
+ {% endfor %}
53
+ </ul>
54
+ {% if d.get('note') %}<p class="note">💡 {{ d['note'] }}</p>{% endif %}
55
+ </div>
56
+ {% endfor %}
57
+ {% endif %}
58
+
59
+ {% if tips %}
60
+ <h2>💡 实用贴士</h2>
61
+ <ul class="tips">{% for t in tips %}<li>{{ t }}</li>{% endfor %}</ul>
62
+ {% endif %}
63
+ </div>
64
+ <footer>由 旅行特工队 · 工具层 生成 · {{ generated_at }}</footer>
65
+ </body>
66
+ </html>