- 新增 operit_runtime.py: 跨平台 Python 运行时 - 实现 Tools.System/Files/Net/UI/Memory/FFmpeg/Workflow/Chat/SoftwareSettings/Tasker - 实现 OkHttp 链式 HTTP 客户端兼容层 - 实现 complete()/exports/console 全局对象 - 实现 OperitScriptLoader 脚本加载器 - 支持 Windows/macOS/Linux 三平台 - 新增 operit_py/ 目录: 47 个 Python 翻译脚本 - 搜索类: duckduckgo, google_search, tavily, zhipu_search, various_search, crossref - 绘图类: openai_draw, qwen_draw, xai_draw, minimax_draw, siliconflow_draw, zhipu_draw, nanobanana_draw - 媒体类: ffmpeg, file_converter, douyin_download, bilibili_tools, jmcomic - 文件类: extended_file_tools, hex_editor, reader, beeimg_image_uploader - 网络类: extended_http_tools, network_test - 自动化类: browser, automatic_ui_base, automatic_ui_subagent, automatic_bilibili_assistant, automatic_xiaohongshu_assistant, automatic_baidu_map_assistant - 系统类: system_tools, super_admin, daily_life, operit_editor - 对话类: ai_chat, extended_chat, qq_intelligent - 工作流类: workflow, vflow_trigger, tasker - 工具类: time, code_runner, quick_start, extended_memory_tools, baidu_map, 12306, pdf_vision_parser - 测试结果: 47/47 语法检查通过, 38 脚本加载成功, 280 个工具注册 - Android 专属功能已降级处理(保留接口,返回不支持提示)
802 lines
34 KiB
Python
802 lines
34 KiB
Python
# METADATA
|
|
# {
|
|
# "name": "baidu_map",
|
|
# "display_name": { "zh": "百度地图工具", "en": "Baidu Map Tools" },
|
|
# "description": { "zh": "百度地图工具集合,提供AOI(兴趣区域)数据获取接口。通过调用百度地图API,支持按地理范围查询AOI边界坐标,基于位置的路线规划,助力地理信息系统应用开发和空间数据分析。", "en": "A Baidu Maps toolkit that provides AOI (Area of Interest) data access. It supports querying AOI boundary coordinates by geographic range and location-based route planning, useful for GIS development and spatial data analysis." },
|
|
# "enabledByDefault": true,
|
|
# "category": "Map",
|
|
# "tools": [
|
|
# { "name": "search_aoi", "description": { "zh": "搜索百度地图兴趣区域(AOI)信息", "en": "Search AOI (Area of Interest) information from Baidu Maps." }, "parameters": [ { "name": "keyword", "description": { "zh": "搜索关键词,如商场、小区名称等", "en": "Search keyword, e.g. mall name, residential community name, etc." }, "type": "string", "required": true }, { "name": "city_name", "description": { "zh": "城市名称,如'北京',默认全国范围", "en": "City name, e.g. '北京'. Defaults to nationwide." }, "type": "string", "required": false } ] },
|
|
# { "name": "planRoute", "description": { "zh": "智能路线规划,从当前位置到指定目的地,并发返回驾车、步行、公交三种方式的路线规划。", "en": "Smart route planning from current location to a destination, returning driving/walking/transit plans in parallel." }, "parameters": [ { "name": "destination", "description": { "zh": "目的地名称", "en": "Destination name." }, "type": "string", "required": true }, { "name": "city_name", "description": { "zh": "城市名称,辅助目的地查找", "en": "City name to help resolve the destination (optional)." }, "type": "string", "required": false } ] }
|
|
# ]
|
|
# }
|
|
|
|
import json
|
|
import math
|
|
import time
|
|
import asyncio
|
|
from datetime import datetime
|
|
from urllib.parse import quote, unquote
|
|
|
|
Error = Exception
|
|
|
|
# 常用城市编码
|
|
CITY_CODES = {
|
|
"北京": "131",
|
|
"上海": "289",
|
|
"广州": "257",
|
|
"深圳": "340",
|
|
"杭州": "179",
|
|
"南京": "315",
|
|
"武汉": "218",
|
|
"成都": "75",
|
|
"重庆": "132",
|
|
"西安": "233",
|
|
"全国": "1" # 默认值
|
|
}
|
|
|
|
# 请求头配置
|
|
HEADERS = {
|
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
'Host': 'map.baidu.com',
|
|
'Pragma': 'no-cache',
|
|
'Sec-Fetch-Dest': 'document',
|
|
'Sec-Fetch-Mode': 'navigate',
|
|
'Sec-Fetch-Site': 'none',
|
|
'Sec-Fetch-User': '?1',
|
|
'Upgrade-Insecure-Requests': '1',
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0',
|
|
'sec-ch-ua': '"Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99"',
|
|
'sec-ch-ua-mobile': '?0',
|
|
'sec-ch-ua-platform': '"Windows"',
|
|
'Referer': 'https://map.baidu.com/'
|
|
}
|
|
|
|
# 日志级别配置
|
|
LOG_LEVELS = {
|
|
'NONE': 0,
|
|
'ERROR': 1,
|
|
'WARN': 2,
|
|
'INFO': 3,
|
|
'DEBUG': 4,
|
|
'TRACE': 5
|
|
}
|
|
|
|
# 默认日志级别
|
|
currentLogLevel = LOG_LEVELS['INFO']
|
|
|
|
|
|
def setLogLevel(level):
|
|
global currentLogLevel
|
|
if level >= LOG_LEVELS['NONE'] and level <= LOG_LEVELS['TRACE']:
|
|
currentLogLevel = level
|
|
|
|
|
|
def logger(level, message, data=None):
|
|
if level > currentLogLevel:
|
|
return
|
|
levelNames = {
|
|
LOG_LEVELS['ERROR']: '[ERROR]',
|
|
LOG_LEVELS['WARN']: '[WARN]',
|
|
LOG_LEVELS['INFO']: '[INFO]',
|
|
LOG_LEVELS['DEBUG']: '[DEBUG]',
|
|
LOG_LEVELS['TRACE']: '[TRACE]'
|
|
}
|
|
levelName = levelNames.get(level, '[UNKNOWN]')
|
|
timestamp = datetime.now().isoformat()
|
|
logMessage = f"{timestamp} {levelName} {message}"
|
|
console.log(logMessage)
|
|
if data is not None:
|
|
console.log(data)
|
|
|
|
|
|
def encodeURIComponentSafe(s):
|
|
try:
|
|
return quote(s, safe='')
|
|
except Exception as e:
|
|
logger(LOG_LEVELS['ERROR'], "编码失败:", e)
|
|
return s
|
|
|
|
|
|
def createHttpClient():
|
|
return OkHttp.newBuilder() \
|
|
.connectTimeout(10000) \
|
|
.readTimeout(30000) \
|
|
.writeTimeout(15000) \
|
|
.followRedirects(True) \
|
|
.build()
|
|
|
|
|
|
async def httpGet(url):
|
|
try:
|
|
client = createHttpClient()
|
|
response = await client.newRequest().url(url).method("GET").headers(HEADERS).build().execute()
|
|
if not response.isSuccessful():
|
|
raise Error(f"请求失败: {response.statusCode}")
|
|
try:
|
|
return json.loads(response.content)
|
|
except Exception as e:
|
|
logger(LOG_LEVELS['ERROR'], "解析JSON失败:", e)
|
|
return response.content
|
|
except Exception as e:
|
|
logger(LOG_LEVELS['ERROR'], "网络请求错误:", e)
|
|
raise e
|
|
|
|
|
|
async def getCityCode(cityName):
|
|
try:
|
|
logger(LOG_LEVELS['INFO'], f"开始查询城市编码: {cityName}")
|
|
if cityName in CITY_CODES:
|
|
logger(LOG_LEVELS['DEBUG'], f"本地映射表中找到城市\"{cityName}\"的编码:", CITY_CODES[cityName])
|
|
return CITY_CODES[cityName]
|
|
|
|
encodedCityName = encodeURIComponentSafe(cityName)
|
|
url = f"https://map.baidu.com/?newmap=1&qt=s&wd={encodedCityName}&c=1"
|
|
logger(LOG_LEVELS['INFO'], "发送请求获取城市编码:", url)
|
|
result = await httpGet(url)
|
|
|
|
cityCode = "1"
|
|
|
|
if result and result.get("current_city") and result["current_city"].get("code"):
|
|
cityCode = str(result["current_city"]["code"])
|
|
logger(LOG_LEVELS['DEBUG'], "从current_city中找到城市编码:", cityCode)
|
|
elif result and result.get("content") and isinstance(result["content"], list) and len(result["content"]) > 0:
|
|
for item in result["content"]:
|
|
if item.get("area_code"):
|
|
cityCode = str(item["area_code"])
|
|
logger(LOG_LEVELS['DEBUG'], "从content[].area_code中找到城市编码:", cityCode)
|
|
break
|
|
elif item.get("city_id"):
|
|
cityCode = str(item["city_id"])
|
|
logger(LOG_LEVELS['DEBUG'], "从content[].city_id中找到城市编码:", cityCode)
|
|
break
|
|
elif result and result.get("result") and result["result"].get("code"):
|
|
cityCode = str(result["result"]["code"])
|
|
logger(LOG_LEVELS['DEBUG'], "从result.code中找到城市编码:", cityCode)
|
|
elif result and result.get("result") and result["result"].get("city_id"):
|
|
cityCode = str(result["result"]["city_id"])
|
|
logger(LOG_LEVELS['DEBUG'], "从result.city_id中找到城市编码:", cityCode)
|
|
|
|
if cityCode == "1":
|
|
secondUrl = f"https://map.baidu.com/?qt=cur&wd={encodedCityName}"
|
|
logger(LOG_LEVELS['DEBUG'], "未找到编码,尝试第二个API端点:", secondUrl)
|
|
try:
|
|
await Tools.System.sleep(500)
|
|
secondResult = await httpGet(secondUrl)
|
|
if secondResult and secondResult.get("current_city") and secondResult["current_city"].get("code"):
|
|
cityCode = str(secondResult["current_city"]["code"])
|
|
logger(LOG_LEVELS['DEBUG'], "从第二个API获取到城市编码:", cityCode)
|
|
except Exception as e:
|
|
logger(LOG_LEVELS['ERROR'], "第二个API请求失败:", e)
|
|
|
|
if cityCode == "1":
|
|
logger(LOG_LEVELS['INFO'], f"未能找到城市\"{cityName}\"的编码,使用默认编码\"1\"(全国)")
|
|
else:
|
|
CITY_CODES[cityName] = cityCode
|
|
logger(LOG_LEVELS['DEBUG'], f"已将城市\"{cityName}\"的编码{cityCode}添加到临时映射表")
|
|
|
|
return cityCode
|
|
except Exception as error:
|
|
logger(LOG_LEVELS['ERROR'], "获取城市编码失败:", error)
|
|
return "1"
|
|
|
|
|
|
async def search_aoi(params):
|
|
try:
|
|
keyword = params.get("keyword", "")
|
|
if not keyword:
|
|
raise Error("关键词不能为空")
|
|
|
|
cityCode = "1"
|
|
if params.get("city_name"):
|
|
cityCode = await getCityCode(params["city_name"])
|
|
logger(LOG_LEVELS['DEBUG'], f"城市 \"{params['city_name']}\" 对应的编码:", cityCode)
|
|
|
|
encodedKeyword = encodeURIComponentSafe(keyword)
|
|
url = f"https://map.baidu.com/?newmap=1&qt=s&da_src=searchBox.button&wd={encodedKeyword}&c={cityCode}"
|
|
logger(LOG_LEVELS['INFO'], f"搜索AOI: {keyword}, 城市名称: {params.get('city_name', '全国')}")
|
|
|
|
result = await httpGet(url)
|
|
dataContent = result.get("content", []) if result else []
|
|
|
|
if not dataContent or len(dataContent) == 0:
|
|
logger(LOG_LEVELS['INFO'], "搜索结果为空或格式不符合预期")
|
|
return {
|
|
"success": True,
|
|
"keyword": keyword,
|
|
"city_name": params.get("city_name"),
|
|
"total": 0,
|
|
"aois": []
|
|
}
|
|
|
|
potentialAois = []
|
|
for item in dataContent:
|
|
uid = item.get("uid", "")
|
|
name = item.get("name", "") or item.get("alias", "")
|
|
address = item.get("addr", "")
|
|
area_name = item.get("area_name")
|
|
phone = item.get("tel")
|
|
|
|
typeStr = ""
|
|
if item.get("cla") and isinstance(item["cla"], list) and len(item["cla"]) > 0:
|
|
typeStr = ", ".join([cla[1] for cla in item["cla"]])
|
|
detail_type = item.get("std_tag")
|
|
tags = item.get("di_tag")
|
|
|
|
rating = None
|
|
comment_count = None
|
|
overallRating = item.get("overall_rating") or (item.get("ext", {}).get("detail_info", {}) or {}).get("overall_rating")
|
|
if overallRating:
|
|
rating = float(overallRating)
|
|
commentNum = (item.get("ext", {}).get("detail_info", {}) or {}).get("comment_num")
|
|
if commentNum:
|
|
comment_count = int(commentNum)
|
|
|
|
price = None
|
|
ticket_info = None
|
|
detail_info = (item.get("ext", {}) or {}).get("detail_info", {}) or {}
|
|
if detail_info.get("price"):
|
|
price = detail_info["price"]
|
|
if detail_info.get("dk_ticket"):
|
|
ticketData = detail_info["dk_ticket"]
|
|
ticket_info = {
|
|
"title": ticketData.get("title"),
|
|
"price": ticketData.get("price"),
|
|
"market_price": ticketData.get("marketprice"),
|
|
"sold_info": ticketData.get("sold_info"),
|
|
"booking_tags": ticketData.get("bookingTimeTag", []),
|
|
"ticket_type": ticketData.get("ticket_type_name")
|
|
}
|
|
|
|
opening_hours = None
|
|
opening_hours_detail = None
|
|
business_time = item.get("business_time", {}) or {}
|
|
if business_time.get("data") and len(business_time["data"]) > 0:
|
|
timeData = business_time["data"][0]
|
|
btt = timeData.get("business_time_text", {}) or {}
|
|
opening_hours = btt.get("common")
|
|
opening_hours_detail = {
|
|
"common_hours": btt.get("common"),
|
|
"festival_hours": btt.get("festival"),
|
|
"detailed_schedule": timeData.get("common", []),
|
|
"festival_schedule": timeData.get("festival", {})
|
|
}
|
|
|
|
rankings = []
|
|
bangdan_head = detail_info.get("bangdan_head", {}) or {}
|
|
if bangdan_head.get("ranking_show"):
|
|
rankings = [{
|
|
"name": rank.get("ranking"),
|
|
"rank": rank.get("rank_s"),
|
|
"score": rank.get("score"),
|
|
"short_name": rank.get("short_name"),
|
|
"type": rank.get("ranking_type")
|
|
} for rank in bangdan_head["ranking_show"]]
|
|
|
|
events = []
|
|
if detail_info.get("event_notice"):
|
|
en = detail_info["event_notice"]
|
|
events.append({
|
|
"title": en.get("title"),
|
|
"content": en.get("content"),
|
|
"img_url": en.get("img_url"),
|
|
"start_end": en.get("start_end")
|
|
})
|
|
|
|
shop_hours_simple = item.get("shop_hours_simple")
|
|
photo_count = int(detail_info["photo_num"]) if detail_info.get("photo_num") else None
|
|
has_indoor_map = detail_info.get("indoor_map") == '1'
|
|
|
|
has_street_view = False
|
|
street_view_info = None
|
|
pano = item.get("pano")
|
|
if isinstance(pano, str) and pano:
|
|
firstPano = pano.split(';')[0]
|
|
panoParts = firstPano.split(',')
|
|
if len(panoParts) >= 2 and panoParts[0]:
|
|
has_street_view = True
|
|
street_view_info = {
|
|
"pid": panoParts[0],
|
|
"heading": int(panoParts[1] or '0')
|
|
}
|
|
|
|
lng = 0
|
|
lat = 0
|
|
if detail_info.get("point"):
|
|
lng = float(detail_info["point"].get("x", 0))
|
|
lat = float(detail_info["point"].get("y", 0))
|
|
elif item.get("x") and item.get("y"):
|
|
lng = float(item["x"])
|
|
lat = float(item["y"])
|
|
elif item.get("point") and item["point"].get("x") and item["point"].get("y"):
|
|
lng = float(item["point"]["x"])
|
|
lat = float(item["point"]["y"])
|
|
|
|
hasGeoData = bool(
|
|
(item.get("geo") and len(item["geo"]) > 0) or
|
|
(detail_info.get("guoke_geo") and detail_info["guoke_geo"].get("geo")) or
|
|
item.get("geo_type") == 2
|
|
)
|
|
|
|
additional_info = {}
|
|
if detail_info.get("aoi_src_id"):
|
|
additional_info["aoi_src_id"] = detail_info["aoi_src_id"]
|
|
if detail_info.get("navi_update_time"):
|
|
additional_info["navi_update_time"] = detail_info["navi_update_time"]
|
|
if detail_info.get("official_url"):
|
|
additional_info["official_url"] = detail_info["official_url"]
|
|
if detail_info.get("is_reservable"):
|
|
additional_info["is_reservable"] = detail_info["is_reservable"] == '1'
|
|
if detail_info.get("areaid"):
|
|
additional_info["area_id"] = detail_info["areaid"]
|
|
if detail_info.get("entrance_price"):
|
|
additional_info["entrance_price"] = detail_info["entrance_price"]
|
|
if detail_info.get("free"):
|
|
additional_info["is_free"] = detail_info["free"]
|
|
|
|
detailUrl = f"https://map.baidu.com/?qt=ext&uid={uid}" if uid else ""
|
|
|
|
aoiItem = {
|
|
"uid": uid,
|
|
"name": name,
|
|
"address": address,
|
|
"area_name": area_name,
|
|
"phone": phone,
|
|
"tags": tags,
|
|
"detail_type": detail_type,
|
|
"rating": rating,
|
|
"comment_count": comment_count,
|
|
"price": price,
|
|
"ticket_info": ticket_info,
|
|
"opening_hours": opening_hours,
|
|
"opening_hours_detail": opening_hours_detail,
|
|
"shop_hours_simple": shop_hours_simple,
|
|
"photo_count": photo_count,
|
|
"has_street_view": has_street_view,
|
|
"street_view_info": street_view_info,
|
|
"has_indoor_map": has_indoor_map,
|
|
"rankings": rankings,
|
|
"events": events,
|
|
"type": typeStr,
|
|
"has_geo_data": hasGeoData,
|
|
"center": {"lng": lng, "lat": lat},
|
|
"detail_url": detailUrl,
|
|
"additional_info": additional_info
|
|
}
|
|
if currentLogLevel >= LOG_LEVELS['DEBUG']:
|
|
aoiItem["raw_data"] = {
|
|
"uid": item.get("uid"),
|
|
"name": item.get("name"),
|
|
"addr": item.get("addr"),
|
|
"x": item.get("x"),
|
|
"y": item.get("y"),
|
|
"geo_type": item.get("geo_type")
|
|
}
|
|
potentialAois.append(aoiItem)
|
|
|
|
logger(LOG_LEVELS['DEBUG'], f"找到{len(potentialAois)}个AOI结果")
|
|
return {
|
|
"success": True,
|
|
"keyword": keyword,
|
|
"city_name": params.get("city_name"),
|
|
"total": len(potentialAois),
|
|
"aois": potentialAois
|
|
}
|
|
except Exception as error:
|
|
logger(LOG_LEVELS['ERROR'], "[search_aoi] 错误:", error)
|
|
import traceback
|
|
logger(LOG_LEVELS['ERROR'], "错误堆栈:", traceback.format_exc())
|
|
return {
|
|
"success": False,
|
|
"message": f"搜索AOI失败: {error}",
|
|
"keyword": params.get("keyword"),
|
|
"city_name": params.get("city_name"),
|
|
"total": 0,
|
|
"aois": []
|
|
}
|
|
|
|
|
|
async def get_aoi_boundary(params):
|
|
try:
|
|
uid = params.get("uid", "")
|
|
if not uid:
|
|
raise Error("AOI的UID不能为空")
|
|
|
|
url = f"https://map.baidu.com/?qt=ext&uid={uid}"
|
|
logger(LOG_LEVELS['INFO'], f"获取AOI边界: {uid}")
|
|
result = await httpGet(url)
|
|
logger(LOG_LEVELS['DEBUG'], "AOI边界结果结构:", list(result.keys()) if result else {})
|
|
|
|
if not result or not result.get("content") or (isinstance(result.get("content"), list) and len(result["content"]) == 0):
|
|
return {"success": False, "message": "未找到AOI边界数据", "uid": uid}
|
|
|
|
content = result["content"][0] if isinstance(result["content"], list) else result["content"]
|
|
if not content:
|
|
return {"success": False, "message": "AOI内容数据为空", "uid": uid}
|
|
|
|
geoData = content.get("geo")
|
|
if not geoData and content.get("ext") and content["ext"].get("geo"):
|
|
geoData = content["ext"]["geo"]
|
|
elif not geoData and content.get("geodata"):
|
|
geoData = content["geodata"]
|
|
elif not geoData and content.get("guoke_geo") and content["guoke_geo"].get("geo"):
|
|
geoData = content["guoke_geo"]["geo"]
|
|
|
|
boundary = []
|
|
if isinstance(geoData, str):
|
|
try:
|
|
parts = geoData.split('|')
|
|
if len(parts) >= 3:
|
|
boundaryPart = parts[2]
|
|
coordinatePairs = boundaryPart.split(',')
|
|
i = 0
|
|
while i < len(coordinatePairs) - 1:
|
|
lng = float(coordinatePairs[i])
|
|
lat = float(coordinatePairs[i + 1])
|
|
if not (math.isnan(lng) or math.isnan(lat)):
|
|
boundary.append({"lng": lng, "lat": lat})
|
|
i += 2
|
|
logger(LOG_LEVELS['DEBUG'], f"从geo字符串解析到{len(boundary)}个边界点")
|
|
except Exception as e:
|
|
logger(LOG_LEVELS['ERROR'], "解析geo字符串失败:", e)
|
|
elif isinstance(geoData, list):
|
|
for point in geoData:
|
|
if isinstance(point, list):
|
|
boundary.append({"lng": float(point[0] or 0), "lat": float(point[1] or 0)})
|
|
else:
|
|
boundary.append({"lng": float(point.get("x", 0) or 0), "lat": float(point.get("y", 0) or 0)})
|
|
logger(LOG_LEVELS['DEBUG'], f"解析到{len(boundary)}个边界点")
|
|
|
|
resultObj = {
|
|
"success": True,
|
|
"uid": uid,
|
|
"name": content.get("name", ""),
|
|
"address": content.get("addr", ""),
|
|
"center": {
|
|
"lng": float(content.get("x") or (content.get("point", {}) or {}).get("x") or '0'),
|
|
"lat": float(content.get("y") or (content.get("point", {}) or {}).get("y") or '0')
|
|
},
|
|
"boundary": boundary,
|
|
"point_count": len(boundary)
|
|
}
|
|
if currentLogLevel >= LOG_LEVELS['DEBUG']:
|
|
resultObj["raw_data"] = {
|
|
"uid": content.get("uid"),
|
|
"name": content.get("name"),
|
|
"addr": content.get("addr"),
|
|
"x": content.get("x"),
|
|
"y": content.get("y"),
|
|
"geo_type": content.get("geo_type")
|
|
}
|
|
return resultObj
|
|
except Exception as error:
|
|
logger(LOG_LEVELS['ERROR'], "[get_aoi_boundary] 错误:", error)
|
|
import traceback
|
|
logger(LOG_LEVELS['ERROR'], "错误堆栈:", traceback.format_exc())
|
|
return {"success": False, "message": f"获取AOI边界失败: {error}", "uid": params.get("uid")}
|
|
|
|
|
|
def formatAoiResultAsText(aoiResult):
|
|
if not aoiResult.get("success"):
|
|
return f"AOI搜索失败: {aoiResult.get('message', '未知错误')}"
|
|
|
|
output = "=== AOI搜索结果 ===\n"
|
|
output += f"搜索关键词: {aoiResult['keyword']}\n"
|
|
output += f"搜索城市: {aoiResult.get('city_name', '全国')}\n"
|
|
output += f"找到结果: {aoiResult['total']} 个\n\n"
|
|
|
|
if aoiResult.get("aois") and len(aoiResult["aois"]) > 0:
|
|
for index, aoi in enumerate(aoiResult["aois"]):
|
|
output += f"--- 结果 {index + 1} ---\n"
|
|
output += f"名称: {aoi['name']}\n"
|
|
output += f"类型: {aoi.get('type', '未知')}\n"
|
|
output += f"地址: {aoi['address']}\n"
|
|
if aoi.get("area_name"):
|
|
output += f"所属区域: {aoi['area_name']}\n"
|
|
if aoi.get("phone"):
|
|
output += f"联系电话: {aoi['phone']}\n"
|
|
if aoi.get("tags"):
|
|
output += f"标签: {aoi['tags']}\n"
|
|
if aoi.get("rating") is not None:
|
|
output += f"评分: {aoi['rating']}/5.0"
|
|
if aoi.get("comment_count") is not None:
|
|
output += f" ({aoi['comment_count']}条评论)"
|
|
output += "\n"
|
|
if aoi.get("price"):
|
|
output += f"价格信息: {aoi['price']}\n"
|
|
if aoi.get("ticket_info"):
|
|
output += "门票信息:\n"
|
|
ti = aoi["ticket_info"]
|
|
if ti.get("title"):
|
|
output += f" - 门票名称: {ti['title']}\n"
|
|
if ti.get("price"):
|
|
output += f" - 门票价格: {ti['price']}\n"
|
|
if ti.get("market_price"):
|
|
output += f" - 市场价: {ti['market_price']}\n"
|
|
if ti.get("sold_info"):
|
|
output += f" - 销售信息: {ti['sold_info']}\n"
|
|
if aoi.get("opening_hours"):
|
|
output += f"开放时间: {aoi['opening_hours']}\n"
|
|
elif aoi.get("shop_hours_simple"):
|
|
output += f"当前状态: {aoi['shop_hours_simple']}\n"
|
|
if aoi.get("opening_hours_detail"):
|
|
detail = aoi["opening_hours_detail"]
|
|
if detail.get("common_hours"):
|
|
output += f"常规时间: {detail['common_hours']}\n"
|
|
if detail.get("festival_hours"):
|
|
output += f"节假日时间: {detail['festival_hours']}\n"
|
|
if aoi.get("photo_count"):
|
|
output += f"照片数量: {aoi['photo_count']} 张\n"
|
|
features = []
|
|
if aoi.get("has_street_view"):
|
|
features.append("街景")
|
|
if aoi.get("has_indoor_map"):
|
|
features.append("室内地图")
|
|
if aoi.get("has_geo_data"):
|
|
features.append("边界数据")
|
|
if len(features) > 0:
|
|
output += f"可用功能: {', '.join(features)}\n"
|
|
if aoi.get("rankings") and len(aoi["rankings"]) > 0:
|
|
output += "排行榜信息:\n"
|
|
for rank in aoi["rankings"][:3]:
|
|
if rank.get("name") and rank.get("rank"):
|
|
output += f" - {rank['name']}: {rank['rank']}"
|
|
if rank.get("score"):
|
|
output += f" (评分: {rank['score']})"
|
|
output += "\n"
|
|
if aoi.get("events") and len(aoi["events"]) > 0 and aoi["events"][0].get("title"):
|
|
output += "当前活动:\n"
|
|
for event in aoi["events"]:
|
|
if event.get("title"):
|
|
output += f" - {event['title']}"
|
|
if event.get("start_end"):
|
|
output += f" ({event['start_end']})"
|
|
output += "\n"
|
|
if event.get("content"):
|
|
output += f" {event['content']}\n"
|
|
output += f"坐标: 经度 {aoi['center']['lng']}, 纬度 {aoi['center']['lat']}\n"
|
|
output += f"详情链接: {aoi['detail_url']}\n"
|
|
if aoi.get("additional_info") and len(aoi["additional_info"]) > 0:
|
|
output += "其他信息: "
|
|
info = []
|
|
for key, value in aoi["additional_info"].items():
|
|
if key == 'is_free' and value == 2:
|
|
info.append("收费景点")
|
|
elif key == 'is_free' and value == 1:
|
|
info.append("免费景点")
|
|
elif key == 'area_id':
|
|
info.append(f"区域ID: {value}")
|
|
else:
|
|
info.append(f"{key}: {value}")
|
|
output += ", ".join(info) + "\n"
|
|
output += "\n"
|
|
if aoiResult.get("boundary"):
|
|
output += "=== 边界信息 ===\n"
|
|
output += f"边界点数: {aoiResult['boundary'].get('point_count', 0)}\n"
|
|
if aoiResult["boundary"].get("center"):
|
|
output += f"中心坐标: 经度 {aoiResult['boundary']['center']['lng']}, 纬度 {aoiResult['boundary']['center']['lat']}\n"
|
|
output += "\n"
|
|
return output
|
|
|
|
|
|
def formatRouteResultAsText(routeResult):
|
|
if not routeResult.get("success"):
|
|
return f"路线规划失败: {routeResult.get('message', '未知错误')}"
|
|
output = "=== 路线规划结果 ===\n"
|
|
if routeResult.get("current_location"):
|
|
output += f"当前位置: 经度 {routeResult['current_location']['lng']}, 纬度 {routeResult['current_location']['lat']}\n"
|
|
if routeResult["current_location"].get("address"):
|
|
output += f"当前地址: {routeResult['current_location']['address']}\n"
|
|
if routeResult.get("destination"):
|
|
output += f"目的地: {routeResult['destination']['name']}\n"
|
|
output += f"目的地址: {routeResult['destination']['address']}\n"
|
|
output += f"目的坐标: 经度 {routeResult['destination']['location']['lng']}, 纬度 {routeResult['destination']['location']['lat']}\n"
|
|
output += "\n"
|
|
if routeResult.get("all_routes"):
|
|
output += "=== 所有交通方式 ===\n"
|
|
if routeResult["all_routes"].get("driving"):
|
|
driving = routeResult["all_routes"]["driving"]
|
|
output += "驾车路线:\n"
|
|
output += f" 距离: {driving['estimated_distance']}\n"
|
|
output += f" 时间: {driving['estimated_duration']}\n"
|
|
output += f" 建议: {driving['suggestion']}\n\n"
|
|
if routeResult["all_routes"].get("walking"):
|
|
walking = routeResult["all_routes"]["walking"]
|
|
output += "步行路线:\n"
|
|
output += f" 距离: {walking['estimated_distance']}\n"
|
|
output += f" 时间: {walking['estimated_duration']}\n"
|
|
output += f" 建议: {walking['suggestion']}\n\n"
|
|
if routeResult["all_routes"].get("transit"):
|
|
transit = routeResult["all_routes"]["transit"]
|
|
output += "公共交通:\n"
|
|
output += f" 距离: {transit['estimated_distance']}\n"
|
|
output += f" 时间: {transit['estimated_duration']}\n"
|
|
output += f" 建议: {transit['suggestion']}\n\n"
|
|
return output
|
|
|
|
|
|
async def getCurrentLocation():
|
|
try:
|
|
logger(LOG_LEVELS['INFO'], "正在获取用户当前位置...")
|
|
locationResult = await Tools.System.getLocation()
|
|
if not locationResult:
|
|
logger(LOG_LEVELS['ERROR'], "获取位置失败:", "未知错误")
|
|
return None
|
|
return {"lng": locationResult.longitude, "lat": locationResult.latitude}
|
|
except Exception as error:
|
|
logger(LOG_LEVELS['ERROR'], "获取位置出错:", str(error))
|
|
return None
|
|
|
|
|
|
def convertMercatorToLatLng(mercatorLng, mercatorLat):
|
|
lng = (mercatorLng / 20037508.34) * 180
|
|
lat = (mercatorLat / 20037508.34) * 180
|
|
lat = (180 / math.pi) * (2 * math.atan(math.exp(lat * math.pi / 180)) - math.pi / 2)
|
|
return {"lng": lng, "lat": lat}
|
|
|
|
|
|
def calculateDistance(lat1, lng1, lat2, lng2):
|
|
R = 6371000
|
|
dLat = (lat2 - lat1) * math.pi / 180
|
|
dLng = (lng2 - lng1) * math.pi / 180
|
|
a = math.sin(dLat / 2) * math.sin(dLat / 2) + \
|
|
math.cos(lat1 * math.pi / 180) * math.cos(lat2 * math.pi / 180) * \
|
|
math.sin(dLng / 2) * math.sin(dLng / 2)
|
|
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
|
return R * c
|
|
|
|
|
|
def formatDuration(minutes):
|
|
if minutes < 60:
|
|
return f"约{math.ceil(minutes)}分钟"
|
|
hours = int(minutes / 60)
|
|
remainingMinutes = math.ceil(minutes % 60)
|
|
if remainingMinutes == 0:
|
|
return f"约{hours}小时"
|
|
return f"约{hours}小时{remainingMinutes}分钟"
|
|
|
|
|
|
def estimateDuration(distance, mode):
|
|
minutes = 0
|
|
distanceInKm = distance / 1000
|
|
if mode == "walking":
|
|
minutes = distanceInKm * 15
|
|
elif mode == "transit":
|
|
if distanceInKm < 10:
|
|
minutes = distanceInKm * 4
|
|
elif distanceInKm < 100:
|
|
minutes = distanceInKm * 2
|
|
else:
|
|
minutes = distanceInKm * 1
|
|
else: # driving
|
|
if distanceInKm < 10:
|
|
minutes = distanceInKm * 2
|
|
elif distanceInKm < 100:
|
|
minutes = distanceInKm * 1.2
|
|
else:
|
|
minutes = distanceInKm * 0.75
|
|
return formatDuration(minutes)
|
|
|
|
|
|
def getSuggestion(distance, mode):
|
|
if distance < 500:
|
|
return "目的地非常近,步行即可到达"
|
|
elif distance < 2000:
|
|
return "距离适中,可步行或乘坐短途交通工具"
|
|
elif distance < 5000:
|
|
return "距离较远,建议使用公共交通工具"
|
|
else:
|
|
return "目的地较远,建议驾车或使用公共交通工具"
|
|
|
|
|
|
async def planRoute(params):
|
|
try:
|
|
currentLocation = await getCurrentLocation()
|
|
if not currentLocation:
|
|
return {"success": False, "message": "无法获取当前位置信息"}
|
|
|
|
searchResults = await search_aoi({
|
|
"keyword": params["destination"],
|
|
"city_name": params.get("city_name")
|
|
})
|
|
|
|
if not searchResults.get("success") or not searchResults.get("aois") or len(searchResults["aois"]) == 0:
|
|
return {
|
|
"success": False,
|
|
"message": f"未能找到目的地: {params['destination']}",
|
|
"current_location": currentLocation
|
|
}
|
|
|
|
destination = searchResults["aois"][0]
|
|
destLocation = destination["center"]
|
|
if not destLocation or not destLocation.get("lng") or not destLocation.get("lat"):
|
|
return {
|
|
"success": False,
|
|
"message": f"目的地坐标信息无效: {params['destination']}",
|
|
"current_location": currentLocation
|
|
}
|
|
|
|
destLatLng = convertMercatorToLatLng(destLocation["lng"], destLocation["lat"])
|
|
logger(LOG_LEVELS['DEBUG'], f"目的地 \"{destination['name']}\" 墨卡托坐标:", destLocation)
|
|
logger(LOG_LEVELS['DEBUG'], "转换后的经纬度:", destLatLng)
|
|
|
|
distance = calculateDistance(currentLocation["lat"], currentLocation["lng"], destLatLng["lat"], destLatLng["lng"])
|
|
|
|
async def getRouteDetailsForMode(mode):
|
|
cityCode = "1"
|
|
if params.get("city_name"):
|
|
cityCode = await getCityCode(params["city_name"])
|
|
navUrl = f"https://api.map.baidu.com/direction?origin={currentLocation['lat']},{currentLocation['lng']}&destination={destLatLng['lat']},{destLatLng['lng']}&mode={mode}®ion={cityCode}&output=html"
|
|
return {
|
|
"estimated_distance": f"{(distance / 1000):.2f}公里",
|
|
"estimated_duration": estimateDuration(distance, mode),
|
|
"transport_mode": mode,
|
|
"navigation_url": navUrl,
|
|
"suggestion": getSuggestion(distance, mode)
|
|
}
|
|
|
|
logger(LOG_LEVELS['INFO'], "获取所有模式的路线规划...")
|
|
modes = ["driving", "walking", "transit"]
|
|
routesResults = await asyncio.gather(*[getRouteDetailsForMode(mode) for mode in modes])
|
|
|
|
all_routes = {
|
|
"driving": routesResults[0],
|
|
"walking": routesResults[1],
|
|
"transit": routesResults[2]
|
|
}
|
|
|
|
return {
|
|
"success": True,
|
|
"current_location": currentLocation,
|
|
"destination": {
|
|
"name": destination["name"],
|
|
"address": destination["address"],
|
|
"location": destLatLng
|
|
},
|
|
"all_routes": all_routes
|
|
}
|
|
except Exception as error:
|
|
logger(LOG_LEVELS['ERROR'], "[planRoute] 错误:", error)
|
|
return {"success": False, "message": f"路线规划失败: {error}"}
|
|
|
|
|
|
async def main():
|
|
output = ""
|
|
output += "========== 百度地图格式化函数测试 ==========\n\n"
|
|
try:
|
|
output += "[1] 测试AOI搜索格式化...\n"
|
|
aoiResult = await search_aoi({"keyword": "长安大学", "city_name": "西安"})
|
|
output += formatAoiResultAsText(aoiResult) + "\n"
|
|
await Tools.System.sleep(1000)
|
|
output += "[3] 测试路径规划格式化...\n"
|
|
routeResult = await planRoute({"destination": "长安大学", "city_name": "西安"})
|
|
output += formatRouteResultAsText(routeResult) + "\n"
|
|
output += "========== 格式化测试完成 ==========\n"
|
|
logger(LOG_LEVELS['INFO'], output)
|
|
return output
|
|
except Exception as error:
|
|
return f"测试过程中发生错误: {error}"
|
|
|
|
|
|
def wrap(coreFunction):
|
|
async def wrapped(params):
|
|
result = await coreFunction(params)
|
|
complete(result)
|
|
return result
|
|
return wrapped
|
|
|
|
|
|
# 导出
|
|
async def _search_aoi_wrapper(param):
|
|
return formatAoiResultAsText(await search_aoi(param))
|
|
|
|
async def _planRoute_wrapper(param):
|
|
return formatRouteResultAsText(await planRoute(param))
|
|
|
|
exports.search_aoi = wrap(_search_aoi_wrapper)
|
|
exports.planRoute = wrap(_planRoute_wrapper)
|
|
exports.main = wrap(main)
|