新增插件: - terminal: 持久交互式终端,纯标准库,全平台,13个测试 - operit_bridge: Operit AI工具格式桥接,47个脚本288个工具,12个测试 其他: - plugins/operit/: 47个Operit官方脚本 - 更新plugin_registry.json/plugins_config.py - 更新about.txt/CHANGELOG.md至v5.4.2
1443 lines
52 KiB
TypeScript
1443 lines
52 KiB
TypeScript
/*
|
|
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
|
|
}
|
|
]
|
|
},
|
|
]
|
|
}*/
|
|
|
|
// Baidu Map API Type Definitions
|
|
namespace BaiduMap {
|
|
export interface Point {
|
|
x: string;
|
|
y: string;
|
|
}
|
|
|
|
export interface TicketInfo {
|
|
title?: string;
|
|
price?: string;
|
|
marketprice?: string;
|
|
sold_info?: string;
|
|
bookingTimeTag?: string[];
|
|
ticket_type_name?: string;
|
|
}
|
|
|
|
export interface RankingInfo {
|
|
ranking?: string;
|
|
rank_s?: string;
|
|
score?: number;
|
|
short_name?: string;
|
|
ranking_type?: string;
|
|
}
|
|
|
|
export interface EventNotice {
|
|
title?: string;
|
|
content?: string;
|
|
img_url?: string;
|
|
start_end?: string;
|
|
}
|
|
|
|
export interface BangdanHead {
|
|
ranking_show?: RankingInfo[];
|
|
}
|
|
|
|
export interface DetailInfo {
|
|
point?: Point;
|
|
guoke_geo?: {
|
|
geo: string;
|
|
};
|
|
tag?: string;
|
|
overall_rating?: string;
|
|
comment_num?: string;
|
|
price?: string;
|
|
shop_hours_simple?: string;
|
|
photo_num?: string;
|
|
indoor_map?: string;
|
|
aoi_src_id?: string;
|
|
navi_update_time?: string;
|
|
official_url?: string;
|
|
is_reservable?: string;
|
|
dk_ticket?: TicketInfo;
|
|
bangdan_head?: BangdanHead;
|
|
event_notice?: EventNotice;
|
|
areaid?: string;
|
|
entrance_price?: string;
|
|
free?: string;
|
|
}
|
|
|
|
export interface Ext {
|
|
detail_info?: DetailInfo;
|
|
geo?: string;
|
|
}
|
|
|
|
export interface BusinessTimeInfo {
|
|
common?: string;
|
|
festival?: string;
|
|
}
|
|
|
|
export interface BusinessTimeData {
|
|
business_time_text?: BusinessTimeInfo;
|
|
common?: BusinessTimeSchedule[];
|
|
festival?: Record<string, BusinessTimeSchedule[]>;
|
|
}
|
|
|
|
export interface BusinessTimeSchedule {
|
|
date?: string;
|
|
season?: string;
|
|
special?: string;
|
|
weeks?: Record<string, TimeSlot[]>;
|
|
}
|
|
|
|
export interface TimeSlot {
|
|
time?: string;
|
|
type?: string;
|
|
}
|
|
|
|
export interface BusinessTime {
|
|
data?: BusinessTimeData[];
|
|
}
|
|
|
|
export interface ContentItem {
|
|
uid?: string;
|
|
name?: string;
|
|
alias?: string;
|
|
addr?: string;
|
|
area_name?: string;
|
|
tel?: string;
|
|
cla?: [number, string][];
|
|
ext?: Ext;
|
|
geo?: string;
|
|
geodata?: unknown;
|
|
guoke_geo?: { geo: string };
|
|
geo_type?: number;
|
|
x?: string;
|
|
y?: string;
|
|
point?: Point;
|
|
di_tag?: string;
|
|
std_tag?: string;
|
|
overall_rating?: string;
|
|
business_time?: BusinessTime;
|
|
shop_hours_simple?: string;
|
|
pano?: string | number;
|
|
area_code?: string | number;
|
|
city_id?: string | number;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface ApiResponse {
|
|
content?: ContentItem[];
|
|
current_city?: {
|
|
code?: string | number;
|
|
};
|
|
result?: {
|
|
code?: string | number;
|
|
city_id?: string | number;
|
|
};
|
|
[key: string]: unknown;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 门票信息接口
|
|
*/
|
|
interface TicketInfoResult {
|
|
title?: string;
|
|
price?: string;
|
|
market_price?: string;
|
|
sold_info?: string;
|
|
booking_tags: string[];
|
|
ticket_type?: string;
|
|
}
|
|
|
|
/**
|
|
* 排行榜信息接口
|
|
*/
|
|
interface RankingResult {
|
|
name?: string;
|
|
rank?: string;
|
|
score?: number;
|
|
short_name?: string;
|
|
type?: string;
|
|
}
|
|
|
|
/**
|
|
* 活动事件接口
|
|
*/
|
|
interface EventResult {
|
|
title?: string;
|
|
content?: string;
|
|
img_url?: string;
|
|
start_end?: string;
|
|
}
|
|
|
|
/**
|
|
* 详细开放时间接口
|
|
*/
|
|
interface OpeningHoursDetail {
|
|
common_hours?: string;
|
|
festival_hours?: string;
|
|
detailed_schedule: BaiduMap.BusinessTimeSchedule[];
|
|
festival_schedule: Record<string, BaiduMap.BusinessTimeSchedule[]>;
|
|
}
|
|
|
|
/**
|
|
* AOI (Area of Interest) item structure returned from search.
|
|
*/
|
|
interface AoiDataItem {
|
|
uid: string;
|
|
name: string;
|
|
address: string;
|
|
area_name?: string;
|
|
phone?: string;
|
|
tags?: string;
|
|
detail_type?: string;
|
|
rating?: number;
|
|
comment_count?: number;
|
|
price?: string;
|
|
ticket_info?: TicketInfoResult;
|
|
opening_hours?: string;
|
|
opening_hours_detail?: OpeningHoursDetail;
|
|
shop_hours_simple?: string;
|
|
photo_count?: number;
|
|
has_street_view?: boolean;
|
|
street_view_info?: { pid: string; heading: number };
|
|
has_indoor_map?: boolean;
|
|
rankings: RankingResult[];
|
|
events: EventResult[];
|
|
type: string;
|
|
has_geo_data: boolean;
|
|
center: {
|
|
lng: number;
|
|
lat: number;
|
|
};
|
|
detail_url: string;
|
|
additional_info: Record<string, unknown>;
|
|
raw_data?: Partial<BaiduMap.ContentItem>;
|
|
}
|
|
|
|
/**
|
|
* Result structure for the `search_aoi` function.
|
|
*/
|
|
interface SearchAoiResult {
|
|
success: boolean;
|
|
message?: string;
|
|
keyword: string;
|
|
city_name?: string;
|
|
total: number;
|
|
aois: AoiDataItem[];
|
|
boundary?: AoiBoundaryResult;
|
|
}
|
|
|
|
/**
|
|
* Result structure for the `get_aoi_boundary` function.
|
|
*/
|
|
interface AoiBoundaryResult {
|
|
success: boolean;
|
|
message?: string;
|
|
uid: string;
|
|
name?: string;
|
|
address?: string;
|
|
center?: {
|
|
lng: number;
|
|
lat: number;
|
|
};
|
|
boundary?: { lng: number, lat: number }[];
|
|
point_count?: number;
|
|
raw_data?: Partial<BaiduMap.ContentItem>;
|
|
}
|
|
|
|
/**
|
|
* Location information structure.
|
|
*/
|
|
interface LocationInfo {
|
|
lat: number;
|
|
lng: number;
|
|
address?: string;
|
|
}
|
|
|
|
/**
|
|
* Route details for a specific transport mode.
|
|
*/
|
|
interface RouteDetails {
|
|
estimated_distance: string;
|
|
estimated_duration: string;
|
|
transport_mode: "driving" | "walking" | "transit";
|
|
navigation_url: string;
|
|
suggestion: string;
|
|
}
|
|
|
|
/**
|
|
* Destination information structure.
|
|
*/
|
|
interface DestinationInfo {
|
|
name: string;
|
|
address: string;
|
|
location: LocationInfo;
|
|
}
|
|
|
|
/**
|
|
* Route planning result structure.
|
|
*/
|
|
interface RouteResult {
|
|
success: boolean;
|
|
message?: string;
|
|
current_location?: LocationInfo;
|
|
destination?: DestinationInfo;
|
|
all_routes?: {
|
|
driving: RouteDetails;
|
|
walking: RouteDetails;
|
|
transit: RouteDetails;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Test results structure.
|
|
*/
|
|
interface BaiduMapTestResults {
|
|
aoi_search?: {
|
|
success: boolean;
|
|
total?: number;
|
|
error?: string;
|
|
};
|
|
advanced_feature?: {
|
|
success: boolean;
|
|
uid?: string;
|
|
point_count?: number;
|
|
error?: string;
|
|
};
|
|
route_planning?: {
|
|
success: boolean;
|
|
destination?: string;
|
|
distance?: string;
|
|
duration?: string;
|
|
error?: string;
|
|
};
|
|
route_planning_all_modes?: {
|
|
success: boolean;
|
|
destination?: string;
|
|
routes?: {
|
|
driving: RouteDetails;
|
|
walking: RouteDetails;
|
|
transit: RouteDetails;
|
|
};
|
|
error?: string;
|
|
};
|
|
}
|
|
|
|
|
|
const baiduMap = (function () {
|
|
// 常用城市编码
|
|
const CITY_CODES = {
|
|
"北京": "131",
|
|
"上海": "289",
|
|
"广州": "257",
|
|
"深圳": "340",
|
|
"杭州": "179",
|
|
"南京": "315",
|
|
"武汉": "218",
|
|
"成都": "75",
|
|
"重庆": "132",
|
|
"西安": "233",
|
|
"全国": "1" // 默认值
|
|
};
|
|
|
|
// 请求头配置
|
|
const 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/'
|
|
};
|
|
|
|
// 日志级别配置
|
|
const LOG_LEVELS = {
|
|
NONE: 0, // 不输出任何日志
|
|
ERROR: 1, // 只输出错误信息
|
|
WARN: 2, // 输出警告和错误
|
|
INFO: 3, // 输出普通信息、警告和错误
|
|
DEBUG: 4, // 输出调试信息、普通信息、警告和错误
|
|
TRACE: 5 // 输出所有日志,包括跟踪信息
|
|
};
|
|
|
|
// 默认日志级别
|
|
let currentLogLevel = LOG_LEVELS.INFO;
|
|
|
|
/**
|
|
* 设置日志级别
|
|
*/
|
|
function setLogLevel(level: number): void {
|
|
if (level >= LOG_LEVELS.NONE && level <= LOG_LEVELS.TRACE) {
|
|
currentLogLevel = level;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 统一的日志输出函数
|
|
*/
|
|
function logger(level: number, message: string, data?: unknown): void {
|
|
if (level > currentLogLevel) return;
|
|
|
|
const levelNames = {
|
|
[LOG_LEVELS.ERROR]: '[ERROR]',
|
|
[LOG_LEVELS.WARN]: '[WARN]',
|
|
[LOG_LEVELS.INFO]: '[INFO]',
|
|
[LOG_LEVELS.DEBUG]: '[DEBUG]',
|
|
[LOG_LEVELS.TRACE]: '[TRACE]'
|
|
};
|
|
|
|
const levelName = levelNames[level] || '[UNKNOWN]';
|
|
const timestamp = new Date().toISOString();
|
|
const logMessage = `${timestamp} ${levelName} ${message}`;
|
|
|
|
console.log(logMessage);
|
|
}
|
|
|
|
/**
|
|
* 对中文进行URL编码
|
|
*/
|
|
function encodeURIComponentSafe(str: string): string {
|
|
try {
|
|
return encodeURIComponent(str);
|
|
} catch (e) {
|
|
logger(LOG_LEVELS.ERROR, `编码失败:`, e);
|
|
return str;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 使用OkHttp发起GET请求
|
|
*/
|
|
async function httpGet(url: string): Promise<any> {
|
|
try {
|
|
const client = createHttpClient();
|
|
const response = await client.get(url, HEADERS);
|
|
|
|
if (!response.isSuccessful()) {
|
|
throw new Error(`请求失败: ${response.statusCode} ${response.statusMessage}`);
|
|
}
|
|
|
|
try {
|
|
const jsonResponse = response.json();
|
|
return jsonResponse;
|
|
} catch (e) {
|
|
logger(LOG_LEVELS.ERROR, `解析JSON失败:`, e);
|
|
return response.content;
|
|
}
|
|
} catch (e) {
|
|
logger(LOG_LEVELS.ERROR, `网络请求错误:`, e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 搜索百度地图AOI信息
|
|
* 参考 https://blog.csdn.net/Jacey_cai/article/details/131524758
|
|
*/
|
|
async function search_aoi(params: {
|
|
keyword: string;
|
|
city_name?: string; // 城市名,如"北京"
|
|
}): Promise<SearchAoiResult> {
|
|
try {
|
|
// 参数处理
|
|
const keyword = params.keyword;
|
|
if (!keyword) {
|
|
throw new Error("关键词不能为空");
|
|
}
|
|
|
|
// 处理城市编码 - 改进版
|
|
let cityCode = "1"; // 默认全国
|
|
|
|
if (params.city_name) {
|
|
// 使用动态获取城市编码功能
|
|
cityCode = await getCityCode(params.city_name);
|
|
logger(LOG_LEVELS.DEBUG, `城市 "${params.city_name}" 对应的编码:`, cityCode);
|
|
}
|
|
|
|
// 构建URL - 使用搜索API
|
|
const encodedKeyword = encodeURIComponentSafe(keyword);
|
|
const url = `https://map.baidu.com/?newmap=1&qt=s&da_src=searchBox.button&wd=${encodedKeyword}&c=${cityCode}`;
|
|
|
|
logger(LOG_LEVELS.INFO, `搜索AOI: ${keyword}, 城市名称: ${params.city_name || '全国'}`);
|
|
|
|
// 发起请求
|
|
const result = await httpGet(url) as BaiduMap.ApiResponse;
|
|
|
|
// 根据实际结构,直接使用content数组
|
|
const dataContent = result?.content || [];
|
|
|
|
// 如果没有找到任何内容数据结构,返回空结果
|
|
if (!dataContent || dataContent.length === 0) {
|
|
logger(LOG_LEVELS.INFO, `搜索结果为空或格式不符合预期`);
|
|
return {
|
|
success: true,
|
|
keyword: keyword,
|
|
city_name: params.city_name,
|
|
total: 0,
|
|
aois: [] as AoiDataItem[]
|
|
};
|
|
}
|
|
|
|
// 解析content数组中的每个元素
|
|
const potentialAois: AoiDataItem[] = dataContent.map((item: BaiduMap.ContentItem) => {
|
|
// ==========基本信息==========
|
|
const uid = item.uid || "";
|
|
const name = item.name || (item.alias) || "";
|
|
const address = item.addr || "";
|
|
const area_name = item.area_name;
|
|
const phone = item.tel;
|
|
|
|
// ==========分类信息==========
|
|
let type = "";
|
|
if (item.cla && Array.isArray(item.cla) && item.cla.length > 0) {
|
|
type = item.cla.map(cla => cla[1]).join(", ");
|
|
}
|
|
const detail_type = item.std_tag;
|
|
const tags = item.di_tag;
|
|
|
|
// ==========评分和评论==========
|
|
let rating: number | undefined = undefined;
|
|
let comment_count: number | undefined = undefined;
|
|
|
|
// 从多个位置提取评分
|
|
const overallRating = item.overall_rating || item.ext?.detail_info?.overall_rating;
|
|
if (overallRating) {
|
|
rating = parseFloat(overallRating);
|
|
}
|
|
|
|
// 从多个位置提取评论数
|
|
const commentNum = item.ext?.detail_info?.comment_num;
|
|
if (commentNum) {
|
|
comment_count = parseInt(commentNum, 10);
|
|
}
|
|
|
|
// ==========价格信息==========
|
|
let price: string | undefined = undefined;
|
|
let ticket_info: TicketInfoResult | undefined = undefined;
|
|
|
|
// 从detail_info提取价格
|
|
if (item.ext?.detail_info?.price) {
|
|
price = item.ext.detail_info.price;
|
|
}
|
|
|
|
// 提取门票信息
|
|
if (item.ext?.detail_info?.dk_ticket) {
|
|
const ticketData = item.ext.detail_info.dk_ticket;
|
|
ticket_info = {
|
|
title: ticketData.title,
|
|
price: ticketData.price,
|
|
market_price: ticketData.marketprice,
|
|
sold_info: ticketData.sold_info,
|
|
booking_tags: ticketData.bookingTimeTag || [],
|
|
ticket_type: ticketData.ticket_type_name
|
|
};
|
|
}
|
|
|
|
// ==========开放时间==========
|
|
let opening_hours: string | undefined = undefined;
|
|
let opening_hours_detail: OpeningHoursDetail | undefined = undefined;
|
|
|
|
if (item.business_time?.data?.[0]) {
|
|
const timeData = item.business_time.data[0];
|
|
opening_hours = timeData.business_time_text?.common;
|
|
|
|
// 详细开放时间信息
|
|
opening_hours_detail = {
|
|
common_hours: timeData.business_time_text?.common,
|
|
festival_hours: timeData.business_time_text?.festival,
|
|
detailed_schedule: timeData.common || [],
|
|
festival_schedule: timeData.festival || {}
|
|
};
|
|
}
|
|
|
|
// ==========排行榜信息==========
|
|
let rankings: RankingResult[] = [];
|
|
if (item.ext?.detail_info?.bangdan_head?.ranking_show) {
|
|
rankings = item.ext.detail_info.bangdan_head.ranking_show.map((rank: BaiduMap.RankingInfo) => ({
|
|
name: rank.ranking,
|
|
rank: rank.rank_s,
|
|
score: rank.score,
|
|
short_name: rank.short_name,
|
|
type: rank.ranking_type
|
|
}));
|
|
}
|
|
|
|
// ==========活动事件==========
|
|
let events: EventResult[] = [];
|
|
if (item.ext?.detail_info?.event_notice) {
|
|
events.push({
|
|
title: item.ext.detail_info.event_notice.title,
|
|
content: item.ext.detail_info.event_notice.content,
|
|
img_url: item.ext.detail_info.event_notice.img_url,
|
|
start_end: item.ext.detail_info.event_notice.start_end
|
|
});
|
|
}
|
|
|
|
// ==========照片和街景==========
|
|
const shop_hours_simple = item.shop_hours_simple;
|
|
const photo_count = item.ext?.detail_info?.photo_num ? parseInt(item.ext.detail_info.photo_num, 10) : undefined;
|
|
const has_indoor_map = item.ext?.detail_info?.indoor_map === '1';
|
|
|
|
let has_street_view = false;
|
|
let street_view_info: { pid: string; heading: number } | undefined = undefined;
|
|
if (typeof item.pano === 'string' && item.pano) {
|
|
const firstPano = item.pano.split(';')[0];
|
|
const panoParts = firstPano.split(',');
|
|
if (panoParts.length >= 2 && panoParts[0]) {
|
|
has_street_view = true;
|
|
street_view_info = {
|
|
pid: panoParts[0],
|
|
heading: parseInt(panoParts[1] || '0', 10)
|
|
};
|
|
}
|
|
}
|
|
|
|
// ==========坐标信息==========
|
|
let lng = 0, lat = 0;
|
|
if (item.ext && item.ext.detail_info && item.ext.detail_info.point) {
|
|
lng = parseFloat(item.ext.detail_info.point.x);
|
|
lat = parseFloat(item.ext.detail_info.point.y);
|
|
} else if (item.x && item.y) {
|
|
lng = parseFloat(item.x);
|
|
lat = parseFloat(item.y);
|
|
} else if (item.point && item.point.x && item.point.y) {
|
|
lng = parseFloat(item.point.x);
|
|
lat = parseFloat(item.point.y);
|
|
}
|
|
|
|
// ==========地理数据==========
|
|
const hasGeoData = !!(
|
|
(item.geo && item.geo.length > 0) ||
|
|
(item.ext && item.ext.detail_info && item.ext.detail_info.guoke_geo && item.ext.detail_info.guoke_geo.geo) ||
|
|
item.geo_type == 2
|
|
);
|
|
|
|
// ==========其他附加信息==========
|
|
const additional_info: Record<string, unknown> = {};
|
|
if (item.ext?.detail_info?.aoi_src_id) additional_info['aoi_src_id'] = item.ext.detail_info.aoi_src_id;
|
|
if (item.ext?.detail_info?.navi_update_time) additional_info['navi_update_time'] = item.ext.detail_info.navi_update_time;
|
|
if (item.ext?.detail_info?.official_url) additional_info['official_url'] = item.ext.detail_info.official_url;
|
|
if (item.ext?.detail_info?.is_reservable) additional_info['is_reservable'] = item.ext.detail_info.is_reservable === '1';
|
|
if (item.ext?.detail_info?.areaid) additional_info['area_id'] = item.ext.detail_info.areaid;
|
|
if (item.ext?.detail_info?.entrance_price) additional_info['entrance_price'] = item.ext.detail_info.entrance_price;
|
|
if (item.ext?.detail_info?.free) additional_info['is_free'] = item.ext.detail_info.free;
|
|
|
|
// 构建详情URL
|
|
const detailUrl = uid ? `https://map.baidu.com/?qt=ext&uid=${uid}` : "";
|
|
|
|
return {
|
|
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: type,
|
|
has_geo_data: hasGeoData,
|
|
center: {
|
|
lng: lng,
|
|
lat: lat
|
|
},
|
|
detail_url: detailUrl,
|
|
additional_info: additional_info,
|
|
// 只在DEBUG模式下包含简化的原始数据
|
|
...(currentLogLevel >= LOG_LEVELS.DEBUG ? {
|
|
raw_data: {
|
|
uid: item.uid,
|
|
name: item.name,
|
|
addr: item.addr,
|
|
x: item.x,
|
|
y: item.y,
|
|
geo_type: item.geo_type
|
|
}
|
|
} : {})
|
|
} as AoiDataItem;
|
|
});
|
|
|
|
logger(LOG_LEVELS.DEBUG, `找到${potentialAois.length}个AOI结果`);
|
|
|
|
return {
|
|
success: true,
|
|
keyword: keyword,
|
|
city_name: params.city_name,
|
|
total: potentialAois.length,
|
|
aois: potentialAois
|
|
};
|
|
} catch (error) {
|
|
logger(LOG_LEVELS.ERROR, `[search_aoi] 错误:`, error);
|
|
logger(LOG_LEVELS.ERROR, `错误堆栈:`, error.stack);
|
|
|
|
return {
|
|
success: false,
|
|
message: `搜索AOI失败: ${error.message}`,
|
|
keyword: params.keyword,
|
|
city_name: params.city_name,
|
|
total: 0,
|
|
aois: [] as AoiDataItem[]
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取AOI边界坐标
|
|
*/
|
|
async function get_aoi_boundary(params: { uid: string }): Promise<AoiBoundaryResult> {
|
|
try {
|
|
const uid = params.uid;
|
|
if (!uid) {
|
|
throw new Error("AOI的UID不能为空");
|
|
}
|
|
|
|
// 构建URL - 使用百度地图地点详情接口
|
|
const url = `https://map.baidu.com/?qt=ext&uid=${uid}`;
|
|
|
|
logger(LOG_LEVELS.INFO, `获取AOI边界: ${uid}`);
|
|
|
|
// 发起请求
|
|
const result = await httpGet(url) as BaiduMap.ApiResponse;
|
|
|
|
// 记录完整响应以便调试
|
|
logger(LOG_LEVELS.DEBUG, `AOI边界结果结构:`, Object.keys(result || {}));
|
|
|
|
// 检查是否有内容数据
|
|
if (!result || !result.content || (Array.isArray(result.content) && result.content.length === 0)) {
|
|
return {
|
|
success: false,
|
|
message: "未找到AOI边界数据",
|
|
uid: uid
|
|
};
|
|
}
|
|
|
|
// content在新的API中可能是数组,也可能是对象
|
|
const content = (Array.isArray(result.content) ? result.content[0] : result.content) as BaiduMap.ContentItem;
|
|
|
|
if (!content) {
|
|
return {
|
|
success: false,
|
|
message: "AOI内容数据为空",
|
|
uid: uid
|
|
};
|
|
}
|
|
|
|
// 尝试多种可能的地理数据结构
|
|
let geoData: string | ({ x?: string | number; y?: string | number; } | (string | number)[])[] | undefined = content.geo;
|
|
if (!geoData && content.ext && content.ext.geo) {
|
|
geoData = content.ext.geo;
|
|
} else if (!geoData && content.geodata) {
|
|
geoData = content.geodata as string | ({ x?: string | number; y?: string | number; } | (string | number)[])[];
|
|
} else if (!geoData && content.guoke_geo && content.guoke_geo.geo) {
|
|
geoData = content.guoke_geo.geo;
|
|
}
|
|
|
|
let boundary: Array<{ lng: number, lat: number }> = [];
|
|
|
|
if (typeof geoData === 'string') {
|
|
// 解析百度地图的geo字符串格式
|
|
// 格式: "4|12957496.191084,4826145.210198;12958713.912140,4828039.128871|1-12957512.1751599,4827980.8872594,..."
|
|
try {
|
|
const parts = geoData.split('|');
|
|
if (parts.length >= 3) {
|
|
// 第三部分包含详细的边界坐标
|
|
const boundaryPart = parts[2];
|
|
const coordinatePairs = boundaryPart.split(',');
|
|
|
|
// 每两个数字组成一个坐标点
|
|
for (let i = 0; i < coordinatePairs.length - 1; i += 2) {
|
|
const lng = parseFloat(coordinatePairs[i]);
|
|
const lat = parseFloat(coordinatePairs[i + 1]);
|
|
|
|
if (!isNaN(lng) && !isNaN(lat)) {
|
|
boundary.push({ lng, lat });
|
|
}
|
|
}
|
|
|
|
logger(LOG_LEVELS.DEBUG, `从geo字符串解析到${boundary.length}个边界点`);
|
|
}
|
|
} catch (e) {
|
|
logger(LOG_LEVELS.ERROR, `解析geo字符串失败:`, e);
|
|
}
|
|
} else if (Array.isArray(geoData)) {
|
|
// 尝试解析数组形式的边界
|
|
boundary = geoData.map(point => ({
|
|
lng: parseFloat(String((Array.isArray(point) ? point[0] : point.x) || 0)),
|
|
lat: parseFloat(String((Array.isArray(point) ? point[1] : point.y) || 0))
|
|
}));
|
|
logger(LOG_LEVELS.DEBUG, `解析到${boundary.length}个边界点`);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
uid: uid,
|
|
name: content.name || "",
|
|
address: content.addr || "",
|
|
center: {
|
|
lng: parseFloat(content.x || (content.point && content.point.x) || '0'),
|
|
lat: parseFloat(content.y || (content.point && content.point.y) || '0')
|
|
},
|
|
boundary: boundary,
|
|
point_count: boundary.length,
|
|
// 只在DEBUG模式下包含简化的原始数据
|
|
...(currentLogLevel >= LOG_LEVELS.DEBUG ? {
|
|
raw_data: {
|
|
uid: content.uid,
|
|
name: content.name,
|
|
addr: content.addr,
|
|
x: content.x,
|
|
y: content.y,
|
|
geo_type: content.geo_type
|
|
}
|
|
} : {})
|
|
};
|
|
} catch (error) {
|
|
logger(LOG_LEVELS.ERROR, `[get_aoi_boundary] 错误:`, error);
|
|
logger(LOG_LEVELS.ERROR, `错误堆栈:`, error.stack);
|
|
|
|
return {
|
|
success: false,
|
|
message: `获取AOI边界失败: ${error.message}`,
|
|
uid: params.uid
|
|
};
|
|
}
|
|
}
|
|
|
|
function wrap(coreFunction: (params: any) => Promise<any>) {
|
|
return async (params: any) => {
|
|
const result = await coreFunction(params);
|
|
complete(result);
|
|
return result;
|
|
};
|
|
}
|
|
/**
|
|
* 格式化AOI搜索结果为结构化文本
|
|
*/
|
|
function formatAoiResultAsText(aoiResult: SearchAoiResult): string {
|
|
if (!aoiResult.success) {
|
|
return `AOI搜索失败: ${aoiResult.message || '未知错误'}`;
|
|
}
|
|
|
|
let output = `=== AOI搜索结果 ===\n`;
|
|
output += `搜索关键词: ${aoiResult.keyword}\n`;
|
|
output += `搜索城市: ${aoiResult.city_name || '全国'}\n`;
|
|
output += `找到结果: ${aoiResult.total} 个\n\n`;
|
|
|
|
if (aoiResult.aois && aoiResult.aois.length > 0) {
|
|
aoiResult.aois.forEach((aoi, index) => {
|
|
output += `--- 结果 ${index + 1} ---\n`;
|
|
output += `名称: ${aoi.name}\n`;
|
|
output += `类型: ${aoi.type || '未知'}\n`;
|
|
output += `地址: ${aoi.address}\n`;
|
|
|
|
if (aoi.area_name) {
|
|
output += `所属区域: ${aoi.area_name}\n`;
|
|
}
|
|
|
|
if (aoi.phone) {
|
|
output += `联系电话: ${aoi.phone}\n`;
|
|
}
|
|
|
|
if (aoi.tags) {
|
|
output += `标签: ${aoi.tags}\n`;
|
|
}
|
|
|
|
if (aoi.rating !== undefined) {
|
|
output += `评分: ${aoi.rating}/5.0`;
|
|
if (aoi.comment_count !== undefined) {
|
|
output += ` (${aoi.comment_count}条评论)`;
|
|
}
|
|
output += `\n`;
|
|
}
|
|
|
|
if (aoi.price) {
|
|
output += `价格信息: ${aoi.price}\n`;
|
|
}
|
|
|
|
if (aoi.ticket_info) {
|
|
output += `门票信息:\n`;
|
|
if (aoi.ticket_info.title) output += ` - 门票名称: ${aoi.ticket_info.title}\n`;
|
|
if (aoi.ticket_info.price) output += ` - 门票价格: ${aoi.ticket_info.price}\n`;
|
|
if (aoi.ticket_info.market_price) output += ` - 市场价: ${aoi.ticket_info.market_price}\n`;
|
|
if (aoi.ticket_info.sold_info) output += ` - 销售信息: ${aoi.ticket_info.sold_info}\n`;
|
|
}
|
|
|
|
if (aoi.opening_hours) {
|
|
output += `开放时间: ${aoi.opening_hours}\n`;
|
|
} else if (aoi.shop_hours_simple) {
|
|
output += `当前状态: ${aoi.shop_hours_simple}\n`;
|
|
}
|
|
|
|
if (aoi.opening_hours_detail) {
|
|
const detail = aoi.opening_hours_detail;
|
|
if (detail.common_hours) {
|
|
output += `常规时间: ${detail.common_hours}\n`;
|
|
}
|
|
if (detail.festival_hours) {
|
|
output += `节假日时间: ${detail.festival_hours}\n`;
|
|
}
|
|
}
|
|
|
|
if (aoi.photo_count) {
|
|
output += `照片数量: ${aoi.photo_count} 张\n`;
|
|
}
|
|
|
|
const features: string[] = [];
|
|
if (aoi.has_street_view) features.push('街景');
|
|
if (aoi.has_indoor_map) features.push('室内地图');
|
|
if (aoi.has_geo_data) features.push('边界数据');
|
|
if (features.length > 0) {
|
|
output += `可用功能: ${features.join(', ')}\n`;
|
|
}
|
|
|
|
if (aoi.rankings && aoi.rankings.length > 0) {
|
|
output += `排行榜信息:\n`;
|
|
aoi.rankings.slice(0, 3).forEach(rank => {
|
|
if (rank.name && rank.rank) {
|
|
output += ` - ${rank.name}: ${rank.rank}`;
|
|
if (rank.score) output += ` (评分: ${rank.score})`;
|
|
output += `\n`;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (aoi.events && aoi.events.length > 0 && aoi.events[0].title) {
|
|
output += `当前活动:\n`;
|
|
aoi.events.forEach(event => {
|
|
if (event.title) {
|
|
output += ` - ${event.title}`;
|
|
if (event.start_end) output += ` (${event.start_end})`;
|
|
output += `\n`;
|
|
if (event.content) output += ` ${event.content}\n`;
|
|
}
|
|
});
|
|
}
|
|
|
|
output += `坐标: 经度 ${aoi.center.lng}, 纬度 ${aoi.center.lat}\n`;
|
|
output += `详情链接: ${aoi.detail_url}\n`;
|
|
|
|
if (Object.keys(aoi.additional_info).length > 0) {
|
|
output += `其他信息: `;
|
|
const info: string[] = [];
|
|
for (const [key, value] of Object.entries(aoi.additional_info)) {
|
|
if (key === 'is_free' && value === 2) {
|
|
info.push('收费景点');
|
|
} else if (key === 'is_free' && value === 1) {
|
|
info.push('免费景点');
|
|
} else if (key === 'area_id') {
|
|
info.push(`区域ID: ${value}`);
|
|
} else {
|
|
info.push(`${key}: ${value}`);
|
|
}
|
|
}
|
|
output += info.join(', ') + `\n`;
|
|
}
|
|
|
|
output += `\n`;
|
|
});
|
|
|
|
if (aoiResult.boundary) {
|
|
output += `=== 边界信息 ===\n`;
|
|
output += `边界点数: ${aoiResult.boundary.point_count || 0}\n`;
|
|
if (aoiResult.boundary.center) {
|
|
output += `中心坐标: 经度 ${aoiResult.boundary.center.lng}, 纬度 ${aoiResult.boundary.center.lat}\n`;
|
|
}
|
|
output += `\n`;
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* 格式化路线规划结果为结构化文本
|
|
*/
|
|
function formatRouteResultAsText(routeResult: RouteResult): string {
|
|
if (!routeResult.success) {
|
|
return `路线规划失败: ${routeResult.message || '未知错误'}`;
|
|
}
|
|
|
|
let output = `=== 路线规划结果 ===\n`;
|
|
|
|
if (routeResult.current_location) {
|
|
output += `当前位置: 经度 ${routeResult.current_location.lng}, 纬度 ${routeResult.current_location.lat}\n`;
|
|
if (routeResult.current_location.address) {
|
|
output += `当前地址: ${routeResult.current_location.address}\n`;
|
|
}
|
|
}
|
|
|
|
if (routeResult.destination) {
|
|
output += `目的地: ${routeResult.destination.name}\n`;
|
|
output += `目的地址: ${routeResult.destination.address}\n`;
|
|
output += `目的坐标: 经度 ${routeResult.destination.location.lng}, 纬度 ${routeResult.destination.location.lat}\n`;
|
|
}
|
|
|
|
output += `\n`;
|
|
|
|
if (routeResult.all_routes) {
|
|
output += `=== 所有交通方式 ===\n`;
|
|
|
|
// 驾车路线
|
|
if (routeResult.all_routes.driving) {
|
|
const driving = routeResult.all_routes.driving;
|
|
output += `🚗 驾车路线:\n`;
|
|
output += ` 距离: ${driving.estimated_distance}\n`;
|
|
output += ` 时间: ${driving.estimated_duration}\n`;
|
|
output += ` 建议: ${driving.suggestion}\n\n`;
|
|
}
|
|
|
|
// 步行路线
|
|
if (routeResult.all_routes.walking) {
|
|
const walking = routeResult.all_routes.walking;
|
|
output += `🚶 步行路线:\n`;
|
|
output += ` 距离: ${walking.estimated_distance}\n`;
|
|
output += ` 时间: ${walking.estimated_duration}\n`;
|
|
output += ` 建议: ${walking.suggestion}\n\n`;
|
|
}
|
|
|
|
// 公交路线
|
|
if (routeResult.all_routes.transit) {
|
|
const transit = routeResult.all_routes.transit;
|
|
output += `🚌 公共交通:\n`;
|
|
output += ` 距离: ${transit.estimated_distance}\n`;
|
|
output += ` 时间: ${transit.estimated_duration}\n`;
|
|
output += ` 建议: ${transit.suggestion}\n\n`;
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
|
|
/**
|
|
* 测试格式化函数
|
|
*/
|
|
async function main(): Promise<string> {
|
|
let output = "";
|
|
output += "========== 百度地图格式化函数测试 ==========\n\n";
|
|
|
|
try {
|
|
// 测试AOI搜索格式化
|
|
output += "[1] 测试AOI搜索格式化...\n";
|
|
const aoiResult = await search_aoi({
|
|
keyword: "长安大学",
|
|
city_name: "西安"
|
|
});
|
|
output += formatAoiResultAsText(aoiResult) + "\n";
|
|
|
|
await Tools.System.sleep(1000);
|
|
|
|
// 测试路径规划格式化
|
|
output += "[3] 测试路径规划格式化...\n";
|
|
const routeResult = await planRoute({
|
|
destination: "长安大学",
|
|
city_name: "西安"
|
|
});
|
|
output += formatRouteResultAsText(routeResult) + "\n";
|
|
|
|
output += "========== 格式化测试完成 ==========\n";
|
|
|
|
logger(LOG_LEVELS.INFO, output);
|
|
return output;
|
|
} catch (error) {
|
|
return `测试过程中发生错误: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取用户当前位置
|
|
* 使用系统提供的位置API获取当前位置坐标
|
|
*/
|
|
async function getCurrentLocation(): Promise<{ lng: number, lat: number } | undefined> {
|
|
try {
|
|
logger(LOG_LEVELS.INFO, "正在获取用户当前位置...");
|
|
const locationResult = await Tools.System.getLocation();
|
|
|
|
if (!locationResult) {
|
|
logger(LOG_LEVELS.ERROR, "获取位置失败:", "未知错误");
|
|
return undefined;
|
|
}
|
|
|
|
// 获取成功,返回经纬度
|
|
return {
|
|
lng: locationResult.longitude,
|
|
lat: locationResult.latitude
|
|
};
|
|
} catch (error) {
|
|
logger(LOG_LEVELS.ERROR, "获取位置出错:", error.message);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 将百度墨卡托坐标转换为WGS-84坐标系下的经纬度(近似)
|
|
* @param mercatorLng 墨卡托x坐标
|
|
* @param mercatorLat 墨卡托y坐标
|
|
* @returns {{lng: number, lat: number}} WGS-84经纬度
|
|
*/
|
|
function convertMercatorToLatLng(mercatorLng: number, mercatorLat: number): { lng: number, lat: number } {
|
|
const lng = (mercatorLng / 20037508.34) * 180;
|
|
let lat = (mercatorLat / 20037508.34) * 180;
|
|
lat = (180 / Math.PI) * (2 * Math.atan(Math.exp(lat * Math.PI / 180)) - Math.PI / 2);
|
|
// 此转换未处理BD-09到WGS-84的偏移,但对于消除大的距离计算错误已经足够
|
|
return { lng: lng, lat: lat };
|
|
}
|
|
|
|
/**
|
|
* 智能路线规划
|
|
* 高级封装函数,根据用户当前位置和目的地名称,提供导航信息
|
|
*/
|
|
async function planRoute(params: {
|
|
destination: string; // 目的地名称
|
|
city_name?: string; // 城市名称,辅助目的地查找
|
|
}): Promise<RouteResult> {
|
|
try {
|
|
// 步骤 1: 获取当前位置和搜索目的地(所有模式通用)
|
|
const currentLocation = await getCurrentLocation();
|
|
if (!currentLocation) {
|
|
return { success: false, message: "无法获取当前位置信息" };
|
|
}
|
|
|
|
const searchResults = await search_aoi({
|
|
keyword: params.destination,
|
|
city_name: params.city_name
|
|
});
|
|
|
|
if (!searchResults.success || !searchResults.aois || searchResults.aois.length === 0) {
|
|
return {
|
|
success: false,
|
|
message: `未能找到目的地: ${params.destination}`,
|
|
current_location: currentLocation
|
|
};
|
|
}
|
|
|
|
const destination = searchResults.aois[0];
|
|
const destLocation = destination.center;
|
|
|
|
if (!destLocation || !destLocation.lng || !destLocation.lat) {
|
|
return {
|
|
success: false,
|
|
message: `目的地坐标信息无效: ${params.destination}`,
|
|
current_location: currentLocation
|
|
};
|
|
}
|
|
|
|
const destLatLng = convertMercatorToLatLng(destLocation.lng, destLocation.lat);
|
|
logger(LOG_LEVELS.DEBUG, `目的地 "${destination.name}" 墨卡托坐标:`, destLocation);
|
|
logger(LOG_LEVELS.DEBUG, `转换后的经纬度:`, destLatLng);
|
|
|
|
const distance = calculateDistance(
|
|
currentLocation.lat,
|
|
currentLocation.lng,
|
|
destLatLng.lat,
|
|
destLatLng.lng
|
|
);
|
|
|
|
// 帮助函数,用于计算特定交通方式的路线详情
|
|
const getRouteDetailsForMode = async (mode: "driving" | "walking" | "transit"): Promise<RouteDetails> => {
|
|
let cityCode = "1";
|
|
if (params.city_name) {
|
|
cityCode = await getCityCode(params.city_name);
|
|
}
|
|
const navUrl = `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: `${(distance / 1000).toFixed(2)}公里`,
|
|
estimated_duration: estimateDuration(distance, mode),
|
|
transport_mode: mode,
|
|
navigation_url: navUrl,
|
|
suggestion: getSuggestion(distance, mode)
|
|
};
|
|
};
|
|
|
|
// 并发获取所有交通方式的路线
|
|
logger(LOG_LEVELS.INFO, "获取所有模式的路线规划...");
|
|
const modes: ("driving" | "walking" | "transit")[] = ["driving", "walking", "transit"];
|
|
const allRoutesPromises = modes.map(mode => getRouteDetailsForMode(mode));
|
|
const routesResults = await Promise.all(allRoutesPromises);
|
|
|
|
const 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
|
|
};
|
|
} catch (error) {
|
|
logger(LOG_LEVELS.ERROR, `[planRoute] 错误:`, error);
|
|
return {
|
|
success: false,
|
|
message: `路线规划失败: ${error.message}`
|
|
};
|
|
}
|
|
}
|
|
|
|
// 工具函数:计算两点间距离(米)
|
|
function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
|
|
const R = 6371000; // 地球半径,单位米
|
|
const dLat = (lat2 - lat1) * Math.PI / 180;
|
|
const dLng = (lng2 - lng1) * Math.PI / 180;
|
|
const 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);
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
return R * c;
|
|
}
|
|
|
|
// 工具函数:格式化时间为可读字符串
|
|
function formatDuration(minutes: number): string {
|
|
if (minutes < 60) {
|
|
return `约${Math.ceil(minutes)}分钟`;
|
|
}
|
|
const hours = Math.floor(minutes / 60);
|
|
const remainingMinutes = Math.ceil(minutes % 60);
|
|
if (remainingMinutes === 0) {
|
|
return `约${hours}小时`;
|
|
}
|
|
return `约${hours}小时${remainingMinutes}分钟`;
|
|
}
|
|
|
|
// 工具函数:估算行程时间
|
|
function estimateDuration(distance: number, mode: string): string {
|
|
let minutes = 0;
|
|
const distanceInKm = distance / 1000;
|
|
|
|
switch (mode) {
|
|
case "walking":
|
|
// 步行速度约 4 km/h (15 min/km)
|
|
minutes = distanceInKm * 15;
|
|
break;
|
|
case "transit":
|
|
// 公共交通,分段估算
|
|
if (distanceInKm < 10) { // 市内
|
|
minutes = distanceInKm * 4; // 15 km/h
|
|
} else if (distanceInKm < 100) {
|
|
minutes = distanceInKm * 2; // 30 km/h
|
|
} else { // 长途
|
|
minutes = distanceInKm * 1; // 60 km/h
|
|
}
|
|
break;
|
|
case "driving":
|
|
default:
|
|
// 驾车,分段估算
|
|
if (distanceInKm < 10) { // 市区
|
|
minutes = distanceInKm * 2; // 30 km/h
|
|
} else if (distanceInKm < 100) {
|
|
minutes = distanceInKm * 1.2; // 50 km/h
|
|
} else { // 高速
|
|
minutes = distanceInKm * 0.75; // 80 km/h
|
|
}
|
|
break;
|
|
}
|
|
return formatDuration(minutes);
|
|
}
|
|
|
|
// 工具函数:根据距离和交通方式给出建议
|
|
function getSuggestion(distance: number, mode: string): string {
|
|
if (distance < 500) {
|
|
return "目的地非常近,步行即可到达";
|
|
} else if (distance < 2000) {
|
|
return "距离适中,可步行或乘坐短途交通工具";
|
|
} else if (distance < 5000) {
|
|
return "距离较远,建议使用公共交通工具";
|
|
} else {
|
|
return "目的地较远,建议驾车或使用公共交通工具";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 创建HTTP客户端
|
|
* 使用OkHttp库实现网络请求
|
|
*/
|
|
function createHttpClient() {
|
|
return OkHttp.newBuilder()
|
|
.connectTimeout(10000)
|
|
.readTimeout(30000)
|
|
.writeTimeout(15000)
|
|
.followRedirects(true)
|
|
.build();
|
|
}
|
|
|
|
/**
|
|
* 获取城市编码
|
|
* @param cityName 城市名称,如"杭州"、"南京"等
|
|
* @returns 返回城市编码字符串,如果找不到返回默认编码"1"(全国)
|
|
*/
|
|
async function getCityCode(cityName: string): Promise<string> {
|
|
try {
|
|
logger(LOG_LEVELS.INFO, `开始查询城市编码: ${cityName}`);
|
|
|
|
// 如果已在本地映射表中,直接返回
|
|
if (CITY_CODES[cityName]) {
|
|
logger(LOG_LEVELS.DEBUG, `本地映射表中找到城市"${cityName}"的编码:`, CITY_CODES[cityName]);
|
|
return CITY_CODES[cityName];
|
|
}
|
|
|
|
// 使用百度地图API查询城市信息
|
|
const encodedCityName = encodeURIComponentSafe(cityName);
|
|
const url = `https://map.baidu.com/?newmap=1&qt=s&wd=${encodedCityName}&c=1`;
|
|
|
|
logger(LOG_LEVELS.INFO, `发送请求获取城市编码:`, url);
|
|
const result = await httpGet(url) as BaiduMap.ApiResponse;
|
|
|
|
// 尝试从不同路径提取城市编码
|
|
let cityCode = "1"; // 默认值
|
|
|
|
// 路径1: current_city.code
|
|
if (result && result.current_city && result.current_city.code) {
|
|
cityCode = result.current_city.code.toString();
|
|
logger(LOG_LEVELS.DEBUG, `从current_city中找到城市编码:`, cityCode);
|
|
}
|
|
// 路径2: content[].area_code 或 city_id
|
|
else if (result && result.content && Array.isArray(result.content) && result.content.length > 0) {
|
|
for (const item of result.content) {
|
|
if (item.area_code) {
|
|
cityCode = item.area_code.toString();
|
|
logger(LOG_LEVELS.DEBUG, `从content[].area_code中找到城市编码:`, cityCode);
|
|
break;
|
|
} else if (item.city_id) {
|
|
cityCode = item.city_id.toString();
|
|
logger(LOG_LEVELS.DEBUG, `从content[].city_id中找到城市编码:`, cityCode);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// 路径3: result_code
|
|
else if (result && result.result && result.result.code) {
|
|
cityCode = result.result.code.toString();
|
|
logger(LOG_LEVELS.DEBUG, `从result.code中找到城市编码:`, cityCode);
|
|
}
|
|
// 路径4: result.city_id
|
|
else if (result && result.result && result.result.city_id) {
|
|
cityCode = result.result.city_id.toString();
|
|
logger(LOG_LEVELS.DEBUG, `从result.city_id中找到城市编码:`, cityCode);
|
|
}
|
|
|
|
// 如果没找到,尝试第二个API端点
|
|
if (cityCode === "1") {
|
|
// 使用城市搜索API
|
|
const secondUrl = `https://map.baidu.com/?qt=cur&wd=${encodedCityName}`;
|
|
logger(LOG_LEVELS.DEBUG, `未找到编码,尝试第二个API端点:`, secondUrl);
|
|
|
|
try {
|
|
// 增加延迟避免请求过快
|
|
await Tools.System.sleep(500);
|
|
|
|
const secondResult = await httpGet(secondUrl);
|
|
if (secondResult && secondResult.current_city && secondResult.current_city.code) {
|
|
cityCode = secondResult.current_city.code.toString();
|
|
logger(LOG_LEVELS.DEBUG, `从第二个API获取到城市编码:`, cityCode);
|
|
}
|
|
} catch (e) {
|
|
logger(LOG_LEVELS.ERROR, `第二个API请求失败:`, e);
|
|
}
|
|
}
|
|
|
|
// 如果仍未找到,使用默认值
|
|
if (cityCode === "1") {
|
|
logger(LOG_LEVELS.INFO, `未能找到城市"${cityName}"的编码,使用默认编码"1"(全国)`);
|
|
} else {
|
|
// 找到编码后,可以临时添加到CITY_CODES中供本次会话使用
|
|
CITY_CODES[cityName] = cityCode;
|
|
logger(LOG_LEVELS.DEBUG, `已将城市"${cityName}"的编码${cityCode}添加到临时映射表`);
|
|
}
|
|
|
|
return cityCode;
|
|
} catch (error) {
|
|
logger(LOG_LEVELS.ERROR, `获取城市编码失败:`, error);
|
|
return "1"; // 出错时返回默认编码(全国)
|
|
}
|
|
}
|
|
|
|
return {
|
|
|
|
// 格式化工具函数
|
|
formatAoiResultAsText: wrap(async (param: any) => formatAoiResultAsText(await search_aoi(param))),
|
|
formatRouteResultAsText: wrap(async (param: any) => formatRouteResultAsText(await planRoute(param))),
|
|
|
|
// 测试函数
|
|
main: wrap(main),
|
|
};
|
|
})();
|
|
|
|
// 格式化工具函数导出
|
|
exports.search_aoi = baiduMap.formatAoiResultAsText;
|
|
exports.planRoute = baiduMap.formatRouteResultAsText;
|
|
|
|
// 测试函数导出
|
|
exports.main = baiduMap.main; |