Skill: uupt-mcp/uu-paotui (version 1.0.0)

--- name: uupt-delivery description: >- UU跑腿同城配送服务。支持跑腿配送和帮忙服务两种订单类型，包括订单询价、发单下单、查询订单、取消订单、骑手实时追踪。当用户表达任何与"送"、"取"、"寄"、"跑腿"、"发单"、"配送"、"帮忙"、"帮我"、"代取号"、"代排队"、"搬东西"等配送或帮忙需求时使用此skill。 version: 1.0.6 metadata: openclaw: requires: env: [] bins: - node - python3 homepage: https://open.uupt.com install: - kind: node package: axios bins: [] - kind: python package: requests bins: [] --- # UU跑腿同城配送服务 Skill UU跑腿同城配送服务为用户提供便捷的同城即时配送能力和现场帮忙服务，包括订单询价、发单、订单管理和跑男实时追踪等功能。 ## 功能特性 - 📱 手机号一键注册（首次使用自动引导） - 💰 订单询价（计算配送/帮忙服务费用） - 📦 创建跑腿配送订单（从A地到B地） - 🤝 创建帮忙服务订单（在指定地点获得现场协助） - 💳 在线支付（余额不足时提供支付链接，支持微信/支付宝） - 📋 查询订单详情 - ❌ 取消订单 - 🏃 跑男实时位置追踪 ## 运行环境 本 skill 同时提供 **Node.js** 和 **Python** 两种版本，Agent 自动检测可用环境。 | 环境 | 依赖安装 | 脚本入口 | |------|---------|---------| | Node.js | `npm install` | `scripts/*.js`、`index.js` | | Python | `pip install -r requirements.txt` | `uupt_delivery.py` | > **命令格式约定**：下文命令示例默认使用 Node.js 版本，Python 版本只需将脚本路径换为 `python uupt_delivery.py <command>`，参数名中的 `--camelCase` 换为 `--kebab-case`（如 `--fromAddress` → `--from-address`），参数含义完全相同，不再重复列出。 ## 触发条件与场景判断 收到用户请求后，先判断场景。Agent 需智能识别**跑腿配送(SEND)** vs **帮忙服务(HELP)**： | 用户表达 | 识别为 | 判断依据 | |---------|--------|---------| | "从A送到B"、"把X寄到Y"、"配送" | 跑腿配送(SEND) | 两个不同地点之间的物品传递 | | "帮我在X地点..."、"帮我搬/扔/装..." | 帮忙服务(HELP) | 只有一个地点，跑男在现场提供协助 | | "帮我买个X送到Y" | 跑腿配送(SEND) | 本质是A到B的配送，用了"帮"字 | | "帮我在医院挂个号"、"帮我在餐厅取号" | 帮忙服务(HELP) | 在现场执行特定任务，不涉及物品配送 | **判断原则**：核心是从A到B传递物品 → 配送；核心是在某地点提供现场协助 → 帮忙。 **六大场景**： | 场景 | 触发条件 | 所需信息 | |------|---------|---------| | 场景零：首次注册 | 执行脚本输出 `[REGISTRATION_REQUIRED]` | 手机号 / 开发者凭证 | | 场景一：订单询价 | 用户想知道费用 | 地址信息（配送需起止地址，帮忙只需地点） | | 场景二：创建订单 | 用户确认发单 | priceToken、收件人电话、帮忙内容(note) | | 场景三：查询订单 | 用户想看订单状态 | 订单编号 | | 场景四：取消订单 | 用户要取消订单 | 订单编号 | | 场景五：跑男追踪 | 用户想看骑手位置 | 订单编号 | --- ## 场景零：首次注册 当执行任何脚本输出 `[REGISTRATION_REQUIRED]` 时自动触发。 ### Step 1: 询问凭证 ``` 首次使用需要配置认证信息，请问您是否已有 UU跑腿开放平台的凭证（appId、appSecret、openId）？ - A: 已有凭证，直接配置 - B: 没有凭证，通过手机号注册 ``` ### Step 1A: 开发者模式（A） 请用户提供 `appId`、`appSecret`、`openId`，写入 `config.json`： ```json { "appId": "...", "appSecret": "...", "openId": "..." } ``` 保存后告知「配置完成！」，**立即继续执行用户最初要求的功能**。 ### Step 2: 手机号注册（B） 询问手机号，发送短信验证码： ```bash node scripts/register.js --mobile="用户手机号" ``` 处理结果： - `[SMS_SENT]` → 验证码已发送，进入 Step 3 - `[IMAGE_CAPTCHA_REQUIRED]` → 输出包含 `IMAGE_DATA=data:image/png;base64,...`，将 base64 图片展示给用户识别数字后重试： ```bash node scripts/register.js --mobile="手机号" --imageCode="用户输入的数字" ``` ### Step 3: 输入验证码完成授权 ```bash node scripts/register.js --mobile="手机号" --smsCode="用户输入的验证码" ``` 处理结果： - `[REGISTRATION_SUCCESS]` → 注册成功，openId 已保存，**立即继续执行用户最初的功能** - `[REGISTRATION_FAILED]` → 从 Step 2 重试（无需重新输入手机号），最多 3 次 --- ## 场景一：订单询价 计算配送/帮忙服务费用，用户可只询价不发单。 ### 执行步骤 1. 判断订单类型（配送 vs 帮忙） 2. 获取地址：配送需起止地址，帮忙只需地点 3. 执行询价脚本，如输出 `[REGISTRATION_REQUIRED]` 则进入场景零后重试 ### 命令 **跑腿配送：** ```bash node scripts/order-price.js --fromAddress="起始地址" --toAddress="目的地址" --cityName="郑州市" ``` **帮忙服务：** ```bash node scripts/order-price.js --fromAddress="帮忙地点" --toAddress="帮忙地点" --orderType="help" ``` | 参数 | 说明 | 必填 | |------|------|------| | `--fromAddress` | 起始地址（帮忙时为帮忙地点） | 是 | | `--toAddress` | 目的地址（帮忙时为帮忙地点） | 是 | | `--cityName` | 城市名称（需带"市"字，默认"郑州市"） | 否 | | `--orderType` | `send`=配送(默认)，`help`=帮忙 | 否 | ### 回复模板 ``` 💰 {跑腿配送/帮忙服务}费用查询结果： {起点/服务地点}：{fromAddress} {终点（仅配送）：{toAddress}} 预估费用：{price/100} 元 📝 如需下单，请提供收件人电话{帮忙订单加：和具体帮忙内容}。 ``` > 返回包含 `priceToken` 和价格信息（单位：分，展示时除以 100 转元）。 --- ## 场景二：创建订单（发单） 用户明确要发单时，**询价后直接创建订单，无需二次确认**。 ### 订单类型对比 | 维度 | 跑腿配送(SEND) | 帮忙服务(HELP) | |------|---------------|---------------| | 核心行为 | 物品从A送到B | 跑男在现场提供协助 | | 地址 | 起始 ≠ 目的 | 起始 = 目的（同一地点） | | 必填参数 | fromAddress, toAddress, receiverPhone | fromAddress, receiverPhone, **note** | | 帮忙场景 | - | 帮取号、帮搬装、帮扔杂物、其他协助 | ### 执行步骤 1. 获取必要信息（地址、收件人电话、帮忙内容） 2. 调用询价接口获取 priceToken（参照场景一命令） 3. **立即创建订单**，不询问确认 4. 处理返回结果 ### 创建订单命令 ```bash # 跑腿配送 node scripts/create-order.js --priceToken="xxx" --receiverPhone="13800138000" # 帮忙服务（必须带 --note） node scripts/create-order.js --priceToken="xxx" --receiverPhone="13800138000" --note="帮忙内容描述" # 微信渠道：追加 --channel="wechat" 生成二维码 ``` | 参数 | 说明 | 必填 | |------|------|------| | `--priceToken` | 询价返回的 token | 是 | | `--receiverPhone` | 收件人手机号 | 是 | | `--channel` | 渠道（wechat/feishu/dingtalk 等） | 否 | | `--note` | 帮忙内容描述 | 帮忙必填 | ### 返回结果处理 **情况一：余额充足（订单创建成功）** ``` 订单创建成功！ 订单编号：{order_code} {帮忙订单：帮忙内容：{note} | 服务地点：{fromAddress}} 配送费用：{price/100} 元 骑手正在接单中，请保持电话畅通。 ``` **情况二：余额不足（`[PAYMENT_REQUIRED]` / `[RECHARGE_REQUIRED]`）** **帮帮订单**（`[RECHARGE_REQUIRED]`）：帮帮订单暂不支持第三方支付，需提示用户充值： 关键输出：`ORDER_CODE`、`NOTE`、`HELP_ORDER_NO_THIRD_PARTY_PAY`。 ``` 帮帮订单暂不支持第三方支付。 订单编号：{order_code} 帮忙内容：{note} 请通过 UU跑腿商户端或联系客服为账户充值后重新下单。 ``` **跑腿配送**（`[PAYMENT_REQUIRED]`）： 关键输出：`ORDER_CODE`、`PAYMENT_URL`、`QRCODE_FILE`（仅 `--channel="wechat"` 时）。 **微信渠道**（链接无法直接打开，必须发二维码图片）： ``` message(action=send, channel="wechat", path="{QRCODE_FILE}", message="请扫码支付 {price/100} 元") ``` **其他渠道**：直接发送支付链接 `{PAYMENT_URL}`（支持微信/支付宝）。 用户返回后询问支付状态，确认后查询订单详情： ```bash node scripts/order-detail.js --orderCode="{order_code}" ``` ### 完整流程示例 ``` 用户：帮我从金水区送到二七区德化街，电话 13800138000 Agent：询价 → 创建订单 → 余额充足则返回成功，不足则引导支付 用户：帮我在郑州人民医院挂个号 Agent：识别帮忙服务 → 询价 → 获取电话 → 创建订单（带 --note） ``` --- ## 场景三：查询订单详情 ```bash node scripts/order-detail.js --orderCode="UU123456789" ``` | 参数 | 说明 | 必填 | |------|------|------| | `--orderCode` | 订单编号 | 是 | 回复模板： ``` 📋 订单详情： 订单编号：{order_code} | 状态：{status} 起点：{from_address} | 终点：{to_address} 配送费：{price/100} 元 骑手：{driver_name} {driver_phone} ``` --- ## 场景四：取消订单 ```bash node scripts/cancel-order.js --orderCode="UU123456789" --reason="取消原因（可选）" ``` | 参数 | 说明 | 必填 | |------|------|------| | `--orderCode` | 订单编号 | 是 | | `--reason` | 取消原因 | 否 | --- ## 场景五：跑男实时追踪 ```bash node scripts/driver-track.js --orderCode="UU123456789" ``` | 参数 | 说明 | 必填 | |------|------|------| | `--orderCode` | 订单编号 | 是 | 回复模板： ``` 跑男实时位置： 骑手：{driver_name} | 电话：{driver_phone} 当前位置：{current_location} | 预计送达：{estimated_time} ``` --- ## 配置管理 配置分为两层，优先级：**环境变量 > config.json > defaults.json**。 | 文件 | 内容 | 说明 | |------|------|------| | `defaults.json` | appId、appSecret、apiUrl | 内置凭证，**请勿修改** | | `config.json` | openId（或完整凭证） | 注册后自动生成或手动创建 | ### 环境变量 | 变量 | 说明 | |------|------| | `UUPT_APP_ID` | 应用 ID | | `UUPT_APP_SECRET` | 应用密钥 | | `UUPT_OPEN_ID` | 用户唯一标识 | | `UUPT_API_URL` | API 地址（可选，默认生产环境） | ### API 环境 | 环境 | URL | |------|-----| | 生产环境 | `https://api-open.uupt.com/openapi/v3/` | | 测试环境 | `http://api-open.test.uupt.com/openapi/v3/` | --- ## 在代码中使用 ### Node.js ```javascript const { orderPrice, createOrder, orderDetail, cancelOrder, driverTrack } = require('./index'); // 配送询价 const price = await orderPrice({ fromAddress: '...', toAddress: '...', cityName: '郑州市' }); // 帮忙询价 const helpPrice = await orderPrice({ fromAddress: '...', orderType: 'help' }); // 创建订单 const order = await createOrder({ priceToken: price.body.priceToken, receiverPhone: '13800138000' }); // 帮忙订单（带 note） const helpOrder = await createOrder({ priceToken: helpPrice.body.priceToken, receiverPhone: '13800138000', note: '帮忙内容' }); // 余额不足检测 if (order.body.orderUrl) console.log('支付链接:', order.body.orderUrl); // 查询订单 const detail = await orderDetail({ orderCode: order.body.orderCode }); ``` ### Python ```python from uupt_delivery import order_price, create_order, order_detail, cancel_order, driver_track # 配送询价 price = order_price(from_address='...', to_address='...', city_name='郑州市') # 帮忙询价 help_price = order_price(from_address='...', order_type='help') # 创建订单 order = create_order(price_token=price['body']['priceToken'], receiver_phone='13800138000') # 帮忙订单（带 note） help_order = create_order(price_token=help_price['body']['priceToken'], receiver_phone='13800138000', note='帮忙内容') # 余额不足检测 if order['body'].get('orderUrl'): print('支付链接:', order['body']['orderUrl']) # 查询订单 detail = order_detail(order_code=order['body']['orderCode']) ``` --- ## 注意事项 - **首次使用**：需通过手机号验证获取授权，之后无需重复。注册失败自动重试，最多 3 次（无需重新输入手机号） - **图片验证码**：短信发送时若返回 `[IMAGE_CAPTCHA_REQUIRED]`，展示 base64 图片给用户识别后重试 - **询价有效期**：priceToken 有时效性，建议获取后尽快创建订单 - **价格单位**：API 返回的价格单位是分，展示时除以 100 转换为元 - **地址完整性**：地址越完整配送越准确。未指定城市默认"郑州市" - **余额不足**：`[PAYMENT_REQUIRED]` 时，微信渠道用 `message` 发送二维码图片附件，其他渠道发送支付链接；`[RECHARGE_REQUIRED]` 时，帮帮订单暂不支持第三方支付，提示用户充值后重新下单 - **帮忙订单**：必须传 `--note` 参数，fromAddress = toAddress；务必先确认帮忙内容再下单 - **配置文件**：`defaults.json` 为内置凭证，请勿修改或删除 ## 相关链接 - [UU跑腿开放平台](https://open.uupt.com) - [API 文档](https://open.uupt.com/docs)

path: config.example.json
content: appId: 你的appId（可选，不填则使用默认值）
appSecret: 你的appSecret（可选，不填则使用默认值）
openId: 你的openId（必填，或通过注册流程自动获取）

path: defaults.json
content: appId: 04dde0c0ba93455c8a6c6d46734f7180
appSecret: 47888e576b454a23a1f4a34abaaac95d
apiUrl: https://api-open.uupt.com/openapi/v3/

path: index.js
content: const fs = require('fs');
const path = require('path');
const axios = require('axios');
const crypto = require('crypto');

// 配置文件路径
const CONFIG_FILE = path.join(__dirname, 'config.json');
const DEFAULTS_FILE = path.join(__dirname, 'defaults.json');

/**
 * 读取配置文件
 */
function readConfig() {
  try {
    if (fs.existsSync(CONFIG_FILE)) {
      const data = fs.readFileSync(CONFIG_FILE, 'utf8');
      return JSON.parse(data);
    }
  } catch (error) {
    console.error('读取配置文件失败:', error.message);
  }
  return {};
}

/**
 * 读取预制默认配置
 */
function readDefaults() {
  try {
    if (fs.existsSync(DEFAULTS_FILE)) {
      const data = fs.readFileSync(DEFAULTS_FILE, 'utf8');
      return JSON.parse(data);
    }
  } catch (error) {
    console.error('读取默认配置失败:', error.message);
  }
  return {};
}

/**
 * 保存配置文件（合并写入）
 */
function saveConfig(config) {
  try {
    const existing = readConfig();
    const merged = { ...existing, ...config };
    fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), 'utf8');
    console.log('配置已保存到:', CONFIG_FILE);
    return true;
  } catch (error) {
    console.error('保存配置文件失败:', error.message);
    return false;
  }
}

/**
 * 获取配置（优先级：环境变量 > config.json > defaults.json）
 */
function getConfig() {
  const defaults = readDefaults();
  const config = readConfig();
  
  return {
    appId: process.env.UUPT_APP_ID || config.appId || defaults.appId || null,
    appSecret: process.env.UUPT_APP_SECRET || config.appSecret || defaults.appSecret || null,
    openId: process.env.UUPT_OPEN_ID || config.openId || defaults.openId || null,
    apiUrl: process.env.UUPT_API_URL || config.apiUrl || defaults.apiUrl || 'https://api-open.uupt.com/openapi/v3/'
  };
}

/**
 * 检查并确保配置完整
 */
function ensureConfig() {
  const config = getConfig();
  
  if (!config.appId || !config.appSecret) {
    console.log('\n[FATAL] 缺少应用凭证，请确认 defaults.json 文件完整');
    throw new Error('[FATAL] 缺少应用凭证 (appId/appSecret)，请确认 defaults.json 文件存在且内容完整');
  }
  
  if (!config.openId) {
    console.log('\n[REGISTRATION_REQUIRED]');
    console.log('尚未注册，请先完成手机号验证获取授权。');
    console.log('请运行注册脚本: node scripts/register.js --mobile="您的手机号"');
    throw new Error('[REGISTRATION_REQUIRED] 尚未注册，请先完成手机号验证获取授权');
  }
  
  return config;
}

/**
 * 生成 MD5 签名
 */
function generateMd5(input) {
  return crypto.createHash('md5').update(input, 'utf8').digest('hex').toUpperCase();
}

/**
 * 发送 API 请求（需要 openId 的业务接口）
 */
async function postRequest(bizParams, apiPath) {
  const config = ensureConfig();
  const timestamp = Math.floor(Date.now() / 1000);
  const bizJson = JSON.stringify(bizParams);
  
  const signStr = bizJson + config.appSecret + timestamp;
  const sign = generateMd5(signStr);
  
  const payload = {
    openId: config.openId,
    timestamp: timestamp,
    biz: bizJson,
    sign: sign
  };
  
  const url = config.apiUrl + apiPath;
  
  try {
    console.log(`🔄 正在请求: ${apiPath}...`);
    
    const response = await axios.post(url, payload, {
      headers: {
        'X-App-Id': config.appId,
        'Content-Type': 'application/json'
      }
    });
    
    if (response.status === 200) {
      console.log('✅ 请求成功\n');
      return response.data;
    } else {
      console.error('❌ 请求失败:', response.status);
      return null;
    }
  } catch (error) {
    console.error('❌ 请求异常:', error.message);
    return null;
  }
}

/**
 * 发送无需 openId 的 API 请求（用于注册/授权接口）
 */
async function postUnauthorizedRequest(bizParams, apiPath) {
  const config = getConfig();
  
  if (!config.appId || !config.appSecret) {
    throw new Error('[FATAL] 缺少应用凭证 (appId/appSecret)，请确认 defaults.json 文件存在且内容完整');
  }
  
  const timestamp = Math.floor(Date.now() / 1000);
  const bizJson = JSON.stringify(bizParams);
  
  const signStr = bizJson + config.appSecret + timestamp;
  const sign = generateMd5(signStr);
  
  const payload = {
    timestamp: timestamp,
    biz: bizJson,
    sign: sign
  };
  
  const url = config.apiUrl + apiPath;
  
  try {
    console.log(`🔄 正在请求: ${apiPath}...`);
    
    const response = await axios.post(url, payload, {
      headers: {
        'X-App-Id': config.appId,
        'Content-Type': 'application/json'
      }
    });
    
    if (response.status === 200) {
      console.log('✅ 请求成功\n');
      return response.data;
    } else {
      console.error('❌ 请求失败:', response.status);
      return null;
    }
  } catch (error) {
    console.error('❌ 请求异常:', error.message);
    return null;
  }
}

/**
 * 获取用户公网 IP
 * 使用多个备用服务，提高成功率
 */
async function getPublicIp() {
  const ipServices = [
    { url: 'https://httpbin.org/ip', extract: (data) => data.origin },
    { url: 'https://ipinfo.io/json', extract: (data) => data.ip },
    { url: 'https://api64.ipify.org?format=json', extract: (data) => data.ip },
    { url: 'https://api.ipify.org?format=json', extract: (data) => data.ip }
  ];

  for (const service of ipServices) {
    try {
      const response = await axios.get(service.url, { timeout: 5000 });
      const ip = service.extract(response.data);
      if (ip) {
        // 处理可能的逗号分隔的多个IP
        const cleanIp = ip.split(',')[0].trim();
        return cleanIp;
      }
    } catch (error) {
      console.log(`[IP查询] ${service.url} 失败: ${error.message}`);
      continue;
    }
  }

  console.error('[错误] 所有IP查询服务均不可用');
  return '';
}

/**
 * 发送短信验证码
 * @param {Object} params
 * @param {string} params.userMobile - 用户手机号（必填）
 * @param {string} params.userIp - 用户公网 IP（必填）
 * @param {string} params.imageCode - 图片验证码（可选）
 */
async function sendSmsCode(params) {
  const { userMobile, userIp, imageCode } = params;
  
  if (!userMobile) {
    throw new Error('手机号为必填项');
  }
  if (!userIp) {
    throw new Error('用户公网 IP 为必填项');
  }
  
  const biz = {
    userMobile: userMobile,
    userIp: userIp,
    imageCode: imageCode || ''
  };
  
  console.log('📱 正在发送短信验证码...');
  return await postUnauthorizedRequest(biz, 'user/unauthorized/sendSmsCode');
}

/**
 * 商户授权（获取 openId）
 * @param {Object} params
 * @param {string} params.userMobile - 用户手机号（必填）
 * @param {string} params.userIp - 用户公网 IP（必填）
 * @param {string} params.smsCode - 短信验证码（必填）
 */
async function auth(params) {
  const { userMobile, userIp, smsCode } = params;
  
  if (!userMobile) {
    throw new Error('手机号为必填项');
  }
  if (!userIp) {
    throw new Error('用户公网 IP 为必填项');
  }
  if (!smsCode) {
    throw new Error('短信验证码为必填项');
  }
  
  const biz = {
    userMobile: userMobile,
    userIp: userIp,
    smsCode: smsCode,
    cityName: '郑州市',
    countyName: ''
  };
  
  console.log('🔐 正在进行商户授权...');
  const result = await postUnauthorizedRequest(biz, 'user/unauthorized/auth');
  
  if (result && result.body && result.body.openId) {
    saveConfig({ openId: result.body.openId });
    console.log('✅ 授权成功，openId 已保存');
  }
  
  return result;
}

/**
 * 订单询价
 * @param {Object} params - 询价参数
 * @param {string} params.fromAddress - 起始地址（必填，帮忙订单时为帮忙地点）
 * @param {string} params.toAddress - 目的地址（必填，帮忙订单时与fromAddress相同）
 * @param {string} params.cityName - 城市名称（可选，默认郑州市）
 * @param {string} [params.orderType='send'] - 订单类型，'send'为跑腿配送，'help'为帮忙服务
 */
async function orderPrice(params) {
  const { fromAddress, toAddress, cityName = '郑州市', orderType = 'send' } = params;
  
  if (!fromAddress || !toAddress) {
    throw new Error('起始地址和目的地址为必填项');
  }
  
  // 确保城市名带"市"
  let city = cityName;
  if (city && !city.endsWith('市')) {
    city = city + '市';
  }
  
  const isHelp = orderType && orderType.toLowerCase() === 'help';
  
  const biz = {
    fromAddress: fromAddress,
    toAddress: isHelp ? fromAddress : toAddress,
    sendType: isHelp ? 'HELP' : 'SEND',
    cityName: city,
    specialChannel: 2
  };
  
  if (isHelp) {
    biz.goodsType = 'ALLHELP';
  }
  
  const typeLabel = isHelp ? '帮忙服务' : '配送';
  console.log(`💰 正在查询${typeLabel}价格...`);
  return await postRequest(biz, 'order/orderPrice');
}

/**
 * 创建订单
 * @param {Object} params - 订单参数
 * @param {string} params.priceToken - 询价返回的 token（必填）
 * @param {string} params.receiverPhone - 收件人电话（必填）
 * @param {string} [params.channel] - 聊天渠道（wechat 渠道 specialChannel=4，其他渠道=2）
 * @param {string} [params.note] - 帮忙内容描述（帮忙订单时必填，描述具体需要跑男提供的帮助服务）
 */
async function createOrder(params) {
  const { priceToken, receiverPhone, channel, note } = params;
  
  if (!priceToken) {
    throw new Error('priceToken 为必填项，请先调用订单询价接口');
  }
  
  if (!receiverPhone) {
    throw new Error('收件人电话为必填项');
  }
  
  // 微信渠道 specialChannel=4，其他渠道=2
  const isWechat = channel && channel.toLowerCase() === 'wechat';
  const specialChannel = isWechat ? 4 : 2;
  
  const biz = {
    priceToken: priceToken,
    receiver_phone: receiverPhone,
    pushType: 'OPEN_ORDER',
    payType: 'BALANCE_PAY',
    specialChannel: specialChannel,
    specialType: 'NOT_NEED_WARM'
  };
  
  if (note) {
    biz.note = note;
  }
  
  console.log('📦 正在创建订单...');
  return await postRequest(biz, 'order/addOrder');
}

/**
 * 查询订单详情
 * @param {Object} params - 查询参数
 * @param {string} params.orderCode - 订单编号（必填）
 */
async function orderDetail(params) {
  const { orderCode } = params;
  
  if (!orderCode) {
    throw new Error('订单编号为必填项');
  }
  
  const biz = {
    order_code: orderCode
  };
  
  console.log('📋 正在查询订单详情...');
  return await postRequest(biz, 'order/orderDetail');
}

/**
 * 取消订单
 * @param {Object} params - 取消参数
 * @param {string} params.orderCode - 订单编号（必填）
 * @param {string} params.reason - 取消原因（可选）
 */
async function cancelOrder(params) {
  const { orderCode, reason } = params;
  
  if (!orderCode) {
    throw new Error('订单编号为必填项');
  }
  
  const biz = {
    order_code: orderCode,
    reason: reason || ''
  };
  
  console.log('❌ 正在取消订单...');
  return await postRequest(biz, 'order/cancelOrder');
}

/**
 * 跑男实时追踪
 * @param {Object} params - 追踪参数
 * @param {string} params.orderCode - 订单编号（必填）
 */
async function driverTrack(params) {
  const { orderCode } = params;
  
  if (!orderCode) {
    throw new Error('订单编号为必填项');
  }
  
  const biz = {
    order_code: orderCode
  };
  
  console.log('🏃 正在查询跑男信息...');
  return await postRequest(biz, 'order/driverTrack');
}

/**
 * 格式化价格（分转元）
 */
function formatPrice(priceInFen) {
  return (priceInFen / 100).toFixed(2);
}

// 导出函数
module.exports = {
  readConfig,
  readDefaults,
  saveConfig,
  getConfig,
  ensureConfig,
  postUnauthorizedRequest,
  getPublicIp,
  sendSmsCode,
  auth,
  orderPrice,
  createOrder,
  orderDetail,
  cancelOrder,
  driverTrack,
  formatPrice
};

// 如果直接运行此文件，显示帮助信息
if (require.main === module) {
  console.log(`
🚚 UU跑腿同城配送服务
支持跑腿配送(SEND)和帮忙服务(HELP)两种订单类型。

可用命令:
  node scripts/register.js       - 手机号注册/获取授权
  node scripts/order-price.js    - 订单询价（支持跑腿配送和帮忙服务）
  node scripts/create-order.js   - 创建订单
  node scripts/order-detail.js   - 查询订单详情
  node scripts/cancel-order.js   - 取消订单
  node scripts/driver-track.js   - 跑男实时追踪

首次使用:
  运行任何命令时会自动检测是否需要注册。
  如需手动注册: node scripts/register.js --mobile="您的手机号"
`);
}


path: package.json
content: {
  "name": "uupt-delivery-skill",
  "version": "1.0.0",
  "description": "UU跑腿同城配送服务，提供订单询价、发单、订单管理、跑男追踪等功能",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "register": "node scripts/register.js",
    "price": "node scripts/order-price.js",
    "create": "node scripts/create-order.js",
    "detail": "node scripts/order-detail.js",
    "cancel": "node scripts/cancel-order.js",
    "track": "node scripts/driver-track.js"
  },
  "keywords": [
    "uupt",
    "delivery",
    "courier",
    "order",
    "tracking"
  ],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "axios": "^1.6.0"
  }
}


path: uupt_delivery.py
content: #!/usr/bin/env python3
"""
UU跑腿同城配送服务 Agent Skill (Python 版本)
提供订单询价、发单、订单查询、取消订单、跑男追踪等功能。
支持跑腿配送(SEND)和帮忙服务(HELP)两种订单类型。

用法：
    python uupt_delivery.py register --mobile="手机号" [--sms-code="验证码"]
    python uupt_delivery.py price --from-address="起始地址" --to-address="目的地址" [--city="城市名"] [--order-type="send|help"]
    python uupt_delivery.py create --price-[REDACTED_SECRET]" --receiver-phone="收件人电话" [--note="帮忙内容"]
    python uupt_delivery.py detail --order-code="订单编号"
    python uupt_delivery.py cancel --order-code="订单编号" [--reason="取消原因"]
    python uupt_delivery.py track --order-code="订单编号"

配置方式：
    1. 预制配置：defaults.json（appId、appSecret，随 Skill 分发）
    2. 用户配置：config.json（openId，注册后自动保存）
    3. 环境变量：UUPT_OPEN_ID（可选覆盖）
"""

import sys
import os
import json
import hashlib
import time
import argparse
import tempfile
from pathlib import Path
from urllib.parse import quote

try:
    import requests
except ImportError:
    print("[错误] 缺少 requests 库，请运行: pip install requests")
    sys.exit(1)

# 配置文件路径
CONFIG_FILE = Path(__file__).parent / "config.json"
DEFAULTS_FILE = Path(__file__).parent / "defaults.json"

# 默认 API 地址
DEFAULT_API_URL = "https://api-open.uupt.com/openapi/v3/"


def read_config() -> dict:
    """读取配置文件"""
    try:
        if CONFIG_FILE.exists():
            with open(CONFIG_FILE, "r", encoding="utf-8") as f:
                return json.load(f)
    except Exception as e:
        print(f"[警告] 读取配置文件失败: {e}")
    return {}


def read_defaults() -> dict:
    """读取预制默认配置"""
    try:
        if DEFAULTS_FILE.exists():
            with open(DEFAULTS_FILE, "r", encoding="utf-8") as f:
                return json.load(f)
    except Exception as e:
        print(f"[警告] 读取默认配置失败: {e}")
    return {}


def save_config(config: dict) -> bool:
    """保存配置文件（合并写入）"""
    try:
        existing = read_config()
        merged = {**existing, **config}
        with open(CONFIG_FILE, "w", encoding="utf-8") as f:
            json.dump(merged, f, indent=2, ensure_ascii=False)
        print(f"[成功] 配置已保存到: {CONFIG_FILE}")
        return True
    except Exception as e:
        print(f"[错误] 保存配置文件失败: {e}")
        return False


def get_config() -> dict:
    """获取配置（优先级：环境变量 > config.json > defaults.json）"""
    defaults = read_defaults()
    config = read_config()
    return {
        "app_id": os.environ.get("UUPT_APP_ID") or config.get("appId") or defaults.get("appId"),
        "app_secret": os.environ.get("UUPT_APP_SECRET") or config.get("appSecret") or defaults.get("appSecret"),
        "open_id": os.environ.get("UUPT_OPEN_ID") or config.get("openId") or defaults.get("openId"),
        "api_url": os.environ.get("UUPT_API_URL") or config.get("apiUrl") or defaults.get("apiUrl") or DEFAULT_API_URL,
    }


def ensure_config() -> dict:
    """检查并确保配置完整"""
    config = get_config()
    
    if not config["app_id"] or not config["app_secret"]:
        print("\n[FATAL] 缺少应用凭证，请确认 defaults.json 文件完整")
        raise ValueError("[FATAL] 缺少应用凭证 (appId/appSecret)，请确认 defaults.json 文件存在且内容完整")
    
    if not config["open_id"]:
        print("\n[REGISTRATION_REQUIRED]")
        print("尚未注册，请先完成手机号验证获取授权。")
        print('请运行注册脚本: python uupt_delivery.py register --mobile="您的手机号"')
        raise ValueError("[REGISTRATION_REQUIRED] 尚未注册，请先完成手机号验证获取授权")
    
    return config


def generate_md5(text: str) -> str:
    """生成 MD5 签名"""
    return hashlib.md5(text.encode("utf-8")).hexdigest().upper()


def post_request(biz_params: dict, api_path: str) -> dict:
    """发送 API 请求（需要 openId 的业务接口）"""
    config = ensure_config()
    timestamp = int(time.time())
    biz_json = json.dumps(biz_params, ensure_ascii=False, separators=(",", ":"))
    
    sign_str = biz_json + config["app_secret"] + str(timestamp)
    sign = generate_md5(sign_str)
    
    payload = {
        "openId": config["open_id"],
        "timestamp": timestamp,
        "biz": biz_json,
        "sign": sign,
    }
    
    url = config["api_url"] + api_path
    headers = {
        "X-App-Id": config["app_id"],
        "Content-Type": "application/json",
    }
    
    print(f"[请求] 正在请求: {api_path}...")
    
    try:
        response = requests.post(url, json=payload, headers=headers, timeout=30)
        if response.status_code == 200:
            print("[成功] 请求成功\n")
            return response.json()
        else:
            print(f"[错误] 请求失败: HTTP {response.status_code}")
            return {"error": f"HTTP {response.status_code}"}
    except requests.RequestException as e:
        print(f"[错误] 请求异常: {e}")
        return {"error": str(e)}


def post_unauthorized_request(biz_params: dict, api_path: str) -> dict:
    """发送无需 openId 的 API 请求（用于注册/授权接口）"""
    config = get_config()
    
    if not config["app_id"] or not config["app_secret"]:
        raise ValueError("[FATAL] 缺少应用凭证 (appId/appSecret)，请确认 defaults.json 文件存在且内容完整")
    
    timestamp = int(time.time())
    biz_json = json.dumps(biz_params, ensure_ascii=False, separators=(",", ":"))
    
    sign_str = biz_json + config["app_secret"] + str(timestamp)
    sign = generate_md5(sign_str)
    
    payload = {
        "timestamp": timestamp,
        "biz": biz_json,
        "sign": sign,
    }
    
    url = config["api_url"] + api_path
    headers = {
        "X-App-Id": config["app_id"],
        "Content-Type": "application/json",
    }
    
    print(f"[请求] 正在请求: {api_path}...")
    
    try:
        response = requests.post(url, json=payload, headers=headers, timeout=30)
        if response.status_code == 200:
            print("[成功] 请求成功\n")
            return response.json()
        else:
            print(f"[错误] 请求失败: HTTP {response.status_code}")
            return {"error": f"HTTP {response.status_code}"}
    except requests.RequestException as e:
        print(f"[错误] 请求异常: {e}")
        return {"error": str(e)}


def get_public_ip() -> str:
    """获取用户公网 IP
    使用多个备用服务，提高成功率
    """
    ip_services = [
        {"url": "https://httpbin.org/ip", "extract": lambda d: d.get("origin")},
        {"url": "https://ipinfo.io/json", "extract": lambda d: d.get("ip")},
        {"url": "https://api64.ipify.org?format=json", "extract": lambda d: d.get("ip")},
        {"url": "https://api.ipify.org?format=json", "extract": lambda d: d.get("ip")},
    ]

    for service in ip_services:
        try:
            response = requests.get(service["url"], timeout=5)
            ip = service["extract"](response.json())
            if ip:
                # 处理可能的逗号分隔的多个IP
                clean_ip = ip.split(",")[0].strip()
                return clean_ip
        except Exception as e:
            print(f"[IP查询] {service['url']} 失败: {e}")
            continue

    print("[错误] 所有IP查询服务均不可用")
    return ""


def send_sms_code(user_mobile: str, user_ip: str, image_code: str = "") -> dict:
    """发送短信验证码"""
    if not user_mobile:
        raise ValueError("手机号为必填项")
    if not user_ip:
        raise ValueError("用户公网 IP 为必填项")
    
    biz = {
        "userMobile": user_mobile,
        "userIp": user_ip,
        "imageCode": image_code or "",
    }
    
    print("[注册] 正在发送短信验证码...")
    return post_unauthorized_request(biz, "user/unauthorized/sendSmsCode")


def user_auth(user_mobile: str, user_ip: str, sms_code: str) -> dict:
    """商户授权（获取 openId）"""
    if not user_mobile:
        raise ValueError("手机号为必填项")
    if not user_ip:
        raise ValueError("用户公网 IP 为必填项")
    if not sms_code:
        raise ValueError("短信验证码为必填项")
    
    biz = {
        "userMobile": user_mobile,
        "userIp": user_ip,
        "smsCode": sms_code,
        "cityName": "郑州市",
        "countyName": "",
    }
    
    print("[注册] 正在进行商户授权...")
    result = post_unauthorized_request(biz, "user/unauthorized/auth")
    
    if result and result.get("body") and result["body"].get("openId"):
        save_config({"openId": result["body"]["openId"]})
        print("[成功] 授权成功，openId 已保存")
    
    return result


def format_price(price_in_fen: int) -> str:
    """格式化价格（分转元）"""
    return f"{price_in_fen / 100:.2f}"


# ============ 业务功能 ============

def order_price(from_address: str, to_address: str, city_name: str = "郑州市", order_type: str = "send") -> dict:
    """订单询价
    
    Args:
        from_address: 起始地址（帮忙订单时为帮忙地点）
        to_address: 目的地址（帮忙订单时与from_address相同）
        city_name: 城市名称
        order_type: 订单类型，"send"为跑腿配送，"help"为帮忙服务
    """
    if not from_address or not to_address:
        raise ValueError("起始地址和目的地址为必填项")
    
    # 确保城市名带"市"
    if city_name and not city_name.endswith("市"):
        city_name = city_name + "市"
    
    is_help = order_type.lower() == "help"
    
    biz = {
        "fromAddress": from_address,
        "toAddress": from_address if is_help else to_address,
        "sendType": "HELP" if is_help else "SEND",
        "cityName": city_name,
        "specialChannel": 2,
    }
    
    if is_help:
        biz["goodsType"] = "ALLHELP"
    
    type_label = "帮忙服务" if is_help else "配送"
    print(f"[询价] 正在查询{type_label}价格...")
    return post_request(biz, "order/orderPrice")


def create_order(price_token: str, receiver_phone: str, channel: str = "", note: str = "") -> dict:
    """创建订单
    
    Args:
        price_token: 询价返回的 token
        receiver_phone: 收件人电话
        channel: 聊天渠道（wechat 渠道 specialChannel=4，其他渠道=2）
        note: 帮忙内容描述（帮忙订单时必填，用于描述具体需要跑男提供的帮助服务）
    """
    if not price_token:
        raise ValueError("priceToken 为必填项，请先调用订单询价接口")
    if not receiver_phone:
        raise ValueError("收件人电话为必填项")
    
    # 微信渠道 specialChannel=4，其他渠道=2
    is_wechat = channel.lower() == 'wechat' if channel else False
    special_channel = 4 if is_wechat else 2
    
    biz = {
        "priceToken": price_token,
        "receiver_phone": receiver_phone,
        "pushType": "OPEN_ORDER",
        "payType": "BALANCE_PAY",
        "specialChannel": special_channel,
        "specialType": "NOT_NEED_WARM",
    }
    
    if note:
        biz["note"] = note
    
    print("[下单] 正在创建订单...")
    return post_request(biz, "order/addOrder")


def order_detail(order_code: str) -> dict:
    """查询订单详情"""
    if not order_code:
        raise ValueError("订单编号为必填项")
    
    biz = {"order_code": order_code}
    
    print("[查询] 正在查询订单详情...")
    return post_request(biz, "order/orderDetail")


def cancel_order(order_code: str, reason: str = "") -> dict:
    """取消订单"""
    if not order_code:
        raise ValueError("订单编号为必填项")
    
    biz = {
        "order_code": order_code,
        "reason": reason or "",
    }
    
    print("[取消] 正在取消订单...")
    return post_request(biz, "order/cancelOrder")


def driver_track(order_code: str) -> dict:
    """跑男实时追踪"""
    if not order_code:
        raise ValueError("订单编号为必填项")
    
    biz = {"order_code": order_code}
    
    print("[追踪] 正在查询跑男信息...")
    return post_request(biz, "order/driverTrack")


# ============ 结果格式化 ============

def format_price_result(result: dict) -> None:
    """格式化询价结果"""
    print("[结果] 询价结果:")
    print(json.dumps(result, indent=2, ensure_ascii=False))

    if result.get("body"):
        data = result["body"]
        print("\n[价格] 价格摘要:")
        # 优先使用 needPayMoney，其次使用 totalMoney
        price = data.get("needPayMoney", data.get("totalMoney", 0))
        print(f"   预估费用: {format_price(price)} 元")
        if data.get("distance"):
            print(f"   配送距离: {data['distance'] / 1000:.2f} 公里")
        if data.get("priceToken"):
            print(f"   priceToken: {data['priceToken']}")
            print("\n[提示] 使用此 priceToken 创建订单")


def format_create_result(result: dict, channel: str = "", note: str = "") -> None:
    """格式化创建订单结果
    
    Args:
        result: API 返回结果
        channel: 聊天渠道（只有 wechat 渠道才生成二维码图片）
        note: 帮忙内容描述（有值表示帮忙订单，余额不足时提示充值而非第三方支付）
    """
    print("[结果] 创建结果:")
    print(json.dumps(result, indent=2, ensure_ascii=False))
    
    if result.get("body"):
        data = result["body"]
        # 检查是否需要支付（余额不足）
        if data.get("orderUrl"):
            payment_url = data["orderUrl"]
            order_code = data["orderCode"]
            
            # 帮忙订单（有 note 参数）：暂不支持第三方支付，提示用户充值
            if note:
                print("\n[警告] 帮忙订单暂不支持第三方支付")
                print(f"   订单编号: {order_code}")
                print("\n[提示] 请通过 UU跑腿APP或联系客服为账户充值后重新下单")
                
                print("\n[RECHARGE_REQUIRED]")
                print(f"ORDER_CODE={order_code}")
                print(f"NOTE={note}")
                print("HELP_ORDER_NO_THIRD_PARTY_PAY=帮忙订单暂不支持第三方支付，需要充值后重新下单")
                
                print("\n   充值完成后，请重新发起下单")
            else:
                # 跑腿配送订单：引导第三方支付
                print("\n[警告] 账户余额不足，需要完成支付")
                print(f"   订单编号: {order_code}")
                
                # 检查是否为微信渠道，只有微信渠道才生成二维码图片
                is_wechat_channel = channel.lower() == 'wechat' if channel else False
                
                if is_wechat_channel:
                    # 微信渠道：生成二维码图片
                    qrcode_url = f"https://api.qrserver.com/v1/create-qr-code/?size=300x300&data={quote(payment_url, safe='')}"
                    
                    try:
                        script_dir = os.path.dirname(os.path.abspath(__file__))
                        qr_file_name = "payment_qrcode.png"
                        qr_file_path = os.path.join(script_dir, qr_file_name)
                        
                        response = requests.get(qrcode_url, timeout=10)
                        response.raise_for_status()
                        
                        with open(qr_file_path, 'wb') as f:
                            f.write(response.content)
                        
                        print("\n[支付] 支付信息：")
                        print(f"   支付链接: {payment_url}")
                        print(f"   二维码图片: {qr_file_path}")
                        
                        print("\n[PAYMENT_REQUIRED]")
                        print(f"ORDER_CODE={order_code}")
                        print(f"PAYMENT_URL={payment_url}")
                        print(f"QRCODE_FILE={qr_file_path}")
                    except Exception as e:
                        print(f"   下载二维码失败: {e}")
                        
                        print("\n[支付] 支付信息：")
                        print(f"   支付链接: {payment_url}")
                        
                        print("\n[PAYMENT_REQUIRED]")
                        print(f"ORDER_CODE={order_code}")
                        print(f"PAYMENT_URL={payment_url}")
                else:
                    # 其他渠道：只输出支付链接
                    print("\n[支付] 支付信息：")
                    print(f"   支付链接: {payment_url}")
                    
                    print("\n[PAYMENT_REQUIRED]")
                    print(f"ORDER_CODE={order_code}")
                    print(f"PAYMENT_URL={payment_url}")
                
                print("\n   支付完成后，订单将自动生效")
        else:
            print("\n[成功] 订单创建成功!")
            print(f"   订单编号: {data['orderCode']}")
            print("\n[提示] 使用订单编号可查询订单详情或跟踪跑男位置")


def get_order_status_text(state: int) -> str:
    """订单状态码映射"""
    status_map = {
        1: '下单成功',
        3: '骑手已接单',
        4: '骑手已到达',
        5: '骑手已取件',
        6: '骑手送达中',
        10: '已完成',
        11: '已取消',
        20: '异常订单'
    }
    return status_map.get(state, f'未知状态({state})')


def format_detail_result(result: dict, order_code: str) -> None:
    """格式化订单详情结果"""
    print("[结果] 订单详情:")
    print(json.dumps(result, indent=2, ensure_ascii=False))

    if result.get("body"):
        data = result["body"]
        print("\n[详情] 订单摘要:")
        print(f"   订单编号: {data.get('orderCode', order_code)}")
        print(f"   订单状态: {get_order_status_text(data.get('state', 0))}")
        if data.get("orderPrice"):
            print(f"   配送费用: {format_price(data['orderPrice'])} 元")
        if data.get("fromAddress"):
            print(f"   起点地址: {data['fromAddress']}")
        if data.get("toAddress"):
            print(f"   终点地址: {data['toAddress']}")
        if data.get("driverName"):
            print(f"   骑手姓名: {data['driverName']}")
            print(f"   骑手电话: {data.get('driverMobile', '-')}")


def format_cancel_result(result: dict, order_code: str, reason: str) -> None:
    """格式化取消订单结果"""
    print("[结果] 取消结果:")
    print(json.dumps(result, indent=2, ensure_ascii=False))
    
    if result.get("code") in [0, "0"]:
        print("\n[成功] 订单已取消")
        print(f"   订单编号: {order_code}")
        if reason:
            print(f"   取消原因: {reason}")


def format_track_result(result: dict) -> None:
    """格式化跑男追踪结果"""
    print("[结果] 跑男信息:")
    print(json.dumps(result, indent=2, ensure_ascii=False))
    
    if result.get("body"):
        data = result["body"]
        print("\n[骑手] 跑男摘要:")
        if data.get("driver_name"):
            print(f"   骑手姓名: {data['driver_name']}")
            print(f"   联系电话: {data.get('driver_phone', '-')}")
        if data.get("longitude") and data.get("latitude"):
            print(f"   当前位置: {data['longitude']}, {data['latitude']}")
        if data.get("distance"):
            print(f"   距离目的地: {data['distance']} 米")


# ============ 命令行入口 ============

def print_usage():
    """打印使用说明"""
    usage = """
UU跑腿同城配送服务 (Python 版本)
支持跑腿配送(SEND)和帮忙服务(HELP)两种订单类型。

用法:
  python uupt_delivery.py <命令> [参数]

命令:
  register  手机号注册/获取授权
  price     订单询价（支持跑腿配送和帮忙服务）
  create    创建订单
  detail    查询订单详情
  cancel    取消订单
  track     跑男实时追踪

示例:
  python uupt_delivery.py register --mobile="13800138000"
  python uupt_delivery.py register --mobile="13800138000" --sms-code="123456"
  # 跑腿配送询价
  python uupt_delivery.py price --from-address="郑州市金水区农业路" --to-address="郑州市二七区德化街"
  # 帮忙服务询价
  python uupt_delivery.py price --from-address="郑州市金水区农业路" --order-type="help"
  python uupt_delivery.py create --price-token="xxx" --receiver-phone="13800138000"
  python uupt_delivery.py create --price-token="xxx" --receiver-phone="13800138000" --note="帮我搬一箱矿泉水到3楼"
  python uupt_delivery.py detail --order-code="UU123456789"
  python uupt_delivery.py cancel --order-code="UU123456789" --reason="用户改变主意"
  python uupt_delivery.py track --order-code="UU123456789"

首次使用:
  运行任何命令时会自动检测是否需要注册。
  如需手动注册: python uupt_delivery.py register --mobile="您的手机号"
    """.strip()
    print(usage)


def main():
    if len(sys.argv) < 2 or sys.argv[1] in ["-h", "--help"]:
        print_usage()
        sys.exit(0)
    
    command = sys.argv[1].lower()
    
    parser = argparse.ArgumentParser(add_help=False)
    
    try:
        if command == "register":
            parser.add_argument("--mobile", required=True, help="用户手机号")
            parser.add_argument("--sms-code", default="", help="短信验证码")
            parser.add_argument("--image-code", default="", help="图片验证码")
            parser.add_argument("--ip", default="", help="手动指定公网IP")
            args = parser.parse_args(sys.argv[2:])
            
            # 获取公网 IP
            user_ip = args.ip
            if not user_ip:
                print("[注册] 正在获取公网 IP...")
                user_ip = get_public_ip()
                if not user_ip:
                    print("[错误] 无法自动获取公网 IP，请使用 --ip 参数手动指定")
                    sys.exit(1)
                print(f"   公网 IP: {user_ip}")
            
            if args.sms_code:
                # 第二步：完成授权
                print("\n[注册] 正在完成商户授权...")
                auth_result = user_auth(args.mobile, user_ip, args.sms_code)
                
                if auth_result and auth_result.get("body") and auth_result["body"].get("openId"):
                    print("\n[REGISTRATION_SUCCESS]")
                    print(f"[成功] 注册成功！openId 已保存到配置文件。")
                    print(f"   openId: {auth_result['body']['openId']}")
                else:
                    print("\n[REGISTRATION_FAILED]")
                    print("[错误] 授权失败")
                    if auth_result:
                        print(f"   错误码: {auth_result.get('code', '-')}")
                        print(f"   错误信息: {auth_result.get('msg', '-')}")
                    print("\n[提示] 请重新发送验证码后重试")
                    sys.exit(1)
            else:
                # 第一步：发送短信验证码
                print("\n[注册] 正在发送短信验证码...")
                sms_result = send_sms_code(args.mobile, user_ip, args.image_code)
                
                if not sms_result:
                    print("[错误] 发送验证码失败，请稍后重试")
                    sys.exit(1)
                
                # 检查是否需要图片验证码
                if str(sms_result.get("code", "")) == "88100106":
                    print("\n[IMAGE_CAPTCHA_REQUIRED]")
                    if sms_result.get("msg"):
                        print(f"IMAGE_DATA=data:image/png;base64,{sms_result['msg']}")
                    print("\n[警告] 需要图片验证码，请识别图片中的数字后重新运行:")
                    print(f'   python uupt_delivery.py register --mobile="{args.mobile}" --image-code="图片中的数字"')
                    sys.exit(2)
                
                if str(sms_result.get("code", "")) == "1":
                    print("\n[SMS_SENT]")
                    print("[成功] 验证码已发送，请查看手机短信。")
                    print("\n[提示] 收到验证码后，请运行:")
                    print(f'   python uupt_delivery.py register --mobile="{args.mobile}" --sms-code="收到的验证码"')
                else:
                    print(f"\n[错误] 发送验证码失败: {sms_result.get('msg', '未知错误')}")
                    sys.exit(1)
        
        elif command == "price":
            parser.add_argument("--from-address", required=True, help="起始地址（帮忙订单时为帮忙地点）")
            parser.add_argument("--to-address", default="", help="目的地址（帮忙订单时无需填写，自动使用起始地址）")
            parser.add_argument("--city", default="郑州市", help="城市名称")
            parser.add_argument("--order-type", default="send", help="订单类型: send=跑腿配送, help=帮忙服务")
            args = parser.parse_args(sys.argv[2:])
            
            # 帮忙订单时 to_address 使用 from_address 的值
            to_addr = args.to_address if args.to_address else args.from_address
            result = order_price(args.from_address, to_addr, args.city, args.order_type)
            format_price_result(result)
            
        elif command == "create":
            parser.add_argument("--price-token", required=True, help="询价返回的token")
            parser.add_argument("--receiver-phone", required=True, help="收件人电话")
            parser.add_argument("--channel", default="", help="聊天渠道（如 wechat、feishu、dingtalk 等）")
            parser.add_argument("--note", default="", help="帮忙内容描述（帮忙订单时必填，描述具体需要跑男提供的帮助服务）")
            args = parser.parse_args(sys.argv[2:])
            
            result = create_order(args.price_token, args.receiver_phone, args.channel, args.note)
            format_create_result(result, args.channel, args.note)
            
        elif command == "detail":
            parser.add_argument("--order-code", required=True, help="订单编号")
            args = parser.parse_args(sys.argv[2:])
            
            result = order_detail(args.order_code)
            format_detail_result(result, args.order_code)
            
        elif command == "cancel":
            parser.add_argument("--order-code", required=True, help="订单编号")
            parser.add_argument("--reason", default="", help="取消原因")
            args = parser.parse_args(sys.argv[2:])
            
            result = cancel_order(args.order_code, args.reason)
            format_cancel_result(result, args.order_code, args.reason)
            
        elif command == "track":
            parser.add_argument("--order-code", required=True, help="订单编号")
            args = parser.parse_args(sys.argv[2:])
            
            result = driver_track(args.order_code)
            format_track_result(result)
            
        else:
            print(f"[错误] 未知命令: {command}")
            print("   支持的命令: register, price, create, detail, cancel, track")
            print("   使用 -h 查看帮助")
            sys.exit(1)
            
    except ValueError as e:
        print(f"[错误] 参数错误: {e}")
        sys.exit(1)
    except KeyboardInterrupt:
        print("\n已取消。")
        sys.exit(0)


if __name__ == "__main__":
    main()


path: scripts/cancel-order.js
content: #!/usr/bin/env node

/**
 * 取消订单脚本
 * 用法: node cancel-order.js --orderCode="订单编号" [--reason="取消原因"]
 */

const { cancelOrder } = require('../index');

// 解析命令行参数
function parseArgs() {
  const args = {};
  process.argv.slice(2).forEach(arg => {
    const match = arg.match(/^--(\w+)=(.+)$/);
    if (match) {
      args[match[1]] = match[2].replace(/^["']|["']$/g, '');
    }
  });
  return args;
}

async function main() {
  const args = parseArgs();
  
  if (!args.orderCode) {
    console.log(`
❌ 取消订单

用法:
  node cancel-order.js --orderCode="订单编号" [--reason="取消原因"]

参数:
  --orderCode  订单编号（必填）
  --reason     取消原因（可选）

示例:
  node cancel-order.js --orderCode="UU123456789"
  node cancel-order.js --orderCode="UU123456789" --reason="用户改变主意"

注意:
  - 已接单的订单可能会收取取消费用
  - 已完成的订单无法取消
`);
    process.exit(1);
  }
  
  try {
    const result = await cancelOrder({
      orderCode: args.orderCode,
      reason: args.reason
    });
    
    if (result) {
      console.log('📊 取消结果:');
      console.log(JSON.stringify(result, null, 2));
      
      if (result.code === 0 || result.code === '0') {
        console.log('\n✅ 订单已取消');
        console.log(`   订单编号: ${args.orderCode}`);
        if (args.reason) {
          console.log(`   取消原因: ${args.reason}`);
        }
      }
    }
  } catch (error) {
    console.error('执行失败:', error.message);
    process.exit(1);
  }
}

main();


path: scripts/create-order.js
content: #!/usr/bin/env node

/**
 * 创建订单脚本
 * 用法: node create-order.js --priceToken="询价token" --receiverPhone="收件人电话" [--channel="渠道"] [--note="帮忙内容"]
 */

const { createOrder, formatPrice } = require('../index');
const axios = require('axios');
const fs = require('fs');
const path = require('path');

// 解析命令行参数
function parseArgs() {
  const args = {};
  process.argv.slice(2).forEach(arg => {
    const match = arg.match(/^--(\w+)=(.+)$/);
    if (match) {
      args[match[1]] = match[2].replace(/^["']|["']$/g, '');
    }
  });
  return args;
}

async function main() {
  const args = parseArgs();
  
  if (!args.priceToken || !args.receiverPhone) {
    console.log(`
📦 创建订单

用法:
  node create-order.js --priceToken="询价token" --receiverPhone="收件人电话" [--channel="渠道"] [--note="帮忙内容"]

参数:
  --priceToken     询价接口返回的 token（必填）
  --receiverPhone  收件人手机号（必填）
  --channel        聊天渠道（可选，如 wechat、feishu、dingtalk 等）
  --note           帮忙内容描述（帮忙订单时必填，描述具体需要跑男提供的帮助服务）

示例:
  # 跑腿配送
  node create-order.js --priceToken="abc123xyz" --receiverPhone="13800138000"
  node create-order.js --priceToken="abc123xyz" --receiverPhone="13800138000" --channel="wechat"
  # 帮忙服务
  node create-order.js --priceToken="abc123xyz" --receiverPhone="13800138000" --note="帮我搬一箱矿泉水到3楼"

注意:
  - priceToken 有时效性，请在询价后尽快创建订单
  - 如账户余额不足，将返回支付链接，用户可点击选择微信/支付宝支付
  - 帮忙订单务必填写 --note 参数描述具体帮忙内容
`);
    process.exit(1);
  }
  
  try {
    const result = await createOrder({
      priceToken: args.priceToken,
      receiverPhone: args.receiverPhone,
      channel: args.channel || '',
      note: args.note || ''
    });
    
    if (result) {
      console.log('📊 创建结果:');
      console.log(JSON.stringify(result, null, 2));
      
      if (result && result.body) {
        const data = result.body;
        // 检查是否需要支付（余额不足）
        if (data.orderUrl) {
          const paymentUrl = data.orderUrl;
          const orderCode = data.orderCode;
          
          // 帮帮订单（有 --note 参数）：暂不支持第三方支付，提示用户充值
          if (args.note) {
            console.log('\n⚠️  帮帮订单暂不支持第三方支付');
            console.log(`   订单编号: ${orderCode}`);
            console.log('\n💡 请通过 UU跑腿APP或联系客服为账户充值后重新下单');
            
            console.log('\n[RECHARGE_REQUIRED]');
            console.log(`ORDER_CODE=${orderCode}`);
            console.log("NOTE=${args.note}");
            console.log('HELP_ORDER_NO_THIRD_PARTY_PAY=帮帮订单暂不支持第三方支付，需要充值后重新下单');
            
            console.log('\n   充值完成后，请重新发起下单');
          } else {
            // 跑腿配送订单：引导第三方支付
            console.log('\n⚠️  账户余额不足，需要完成支付');
            console.log(`   订单编号: ${orderCode}`);
            
            // 检查是否为微信渠道，只有微信渠道才生成二维码图片
            const isWechatChannel = args.channel && args.channel.toLowerCase() === 'wechat';
            
            if (isWechatChannel) {
              // 微信渠道：生成二维码图片
              const qrcodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(paymentUrl)}`;
              
              try {
                const projectRoot = path.dirname(__dirname);
                const qrFileName = 'payment_qrcode.png';
                const qrFilePath = path.join(projectRoot, qrFileName);
                
                const response = await axios.get(qrcodeUrl, { responseType: 'arraybuffer', timeout: 10000 });
                fs.writeFileSync(qrFilePath, response.data);
                
                console.log('\n💳 支付信息：');
                console.log(`   支付链接: ${paymentUrl}`);
                console.log(`   二维码图片: ${qrFilePath}`);
                
                // 输出特殊标记，供 Agent 识别
                console.log('\n[PAYMENT_REQUIRED]');
                console.log(`ORDER_CODE=${orderCode}`);
                console.log(`PAYMENT_URL=${paymentUrl}`);
                console.log(`QRCODE_FILE=${qrFilePath}`);
              } catch (downloadErr) {
                console.error('   下载二维码失败:', downloadErr.message);
                
                console.log('\n💳 支付信息：');
                console.log(`   支付链接: ${paymentUrl}`);
                
                console.log('\n[PAYMENT_REQUIRED]');
                console.log(`ORDER_CODE=${orderCode}`);
                console.log(`PAYMENT_URL=${paymentUrl}`);
              }
            } else {
              // 其他渠道：只输出支付链接
              console.log('\n💳 支付信息：');
              console.log(`   支付链接: ${paymentUrl}`);
              
              console.log('\n[PAYMENT_REQUIRED]');
              console.log(`ORDER_CODE=${orderCode}`);
              console.log(`PAYMENT_URL=${paymentUrl}`);
            }
            
            console.log('\n   支付完成后，订单将自动生效');
          }
        } else {
          console.log('\n✅ 订单创建成功!');
          console.log(`   订单编号: ${data.orderCode}`);
          console.log('\n💡 提示: 使用订单编号可查询订单详情或跟踪跑男位置');
        }
      }
    }
  } catch (error) {
    console.error('执行失败:', error.message);
    process.exit(1);
  }
}

main();


path: scripts/driver-track.js
content: #!/usr/bin/env node

/**
 * 跑男实时追踪脚本
 * 用法: node driver-track.js --orderCode="订单编号"
 */

const { driverTrack } = require('../index');

// 解析命令行参数
function parseArgs() {
  const args = {};
  process.argv.slice(2).forEach(arg => {
    const match = arg.match(/^--(\w+)=(.+)$/);
    if (match) {
      args[match[1]] = match[2].replace(/^["']|["']$/g, '');
    }
  });
  return args;
}

async function main() {
  const args = parseArgs();
  
  if (!args.orderCode) {
    console.log(`
🏃 跑男实时追踪

用法:
  node driver-track.js --orderCode="订单编号"

参数:
  --orderCode  订单编号（必填）

示例:
  node driver-track.js --orderCode="UU123456789"

说明:
  - 返回跑男的实时位置和状态信息
  - 需要订单已被接单才能查看跑男信息
`);
    process.exit(1);
  }
  
  try {
    const result = await driverTrack({
      orderCode: args.orderCode
    });
    
    if (result) {
      console.log('📊 跑男信息:');
      console.log(JSON.stringify(result, null, 2));
      
      if (result.data) {
        const data = result.data;
        console.log('\n🏃 跑男摘要:');
        if (data.driver_name) {
          console.log(`   骑手姓名: ${data.driver_name}`);
          console.log(`   联系电话: ${data.driver_phone || '-'}`);
        }
        if (data.longitude && data.latitude) {
          console.log(`   当前位置: ${data.longitude}, ${data.latitude}`);
        }
        if (data.distance) {
          console.log(`   距离目的地: ${data.distance} 米`);
        }
      }
    }
  } catch (error) {
    console.error('执行失败:', error.message);
    process.exit(1);
  }
}

main();


path: scripts/order-detail.js
content: #!/usr/bin/env node

/**
 * 查询订单详情脚本
 * 用法: node order-detail.js --orderCode="订单编号"
 */

const { orderDetail, formatPrice } = require('../index');

// 订单状态码映射
function getOrderStatusText(state) {
  const statusMap = {
    1: '下单成功',
    3: '骑手已接单',
    4: '骑手已到达',
    5: '骑手已取件',
    6: '骑手送达中',
    10: '已完成',
    11: '已取消',
    20: '异常订单'
  };
  return statusMap[state] || `未知状态(${state})`;
}

// 解析命令行参数
function parseArgs() {
  const args = {};
  process.argv.slice(2).forEach(arg => {
    const match = arg.match(/^--(\w+)=(.+)$/);
    if (match) {
      args[match[1]] = match[2].replace(/^["']|["']$/g, '');
    }
  });
  return args;
}

async function main() {
  const args = parseArgs();
  
  if (!args.orderCode) {
    console.log(`
📋 查询订单详情

用法:
  node order-detail.js --orderCode="订单编号"

参数:
  --orderCode  订单编号（必填）

示例:
  node order-detail.js --orderCode="UU123456789"
`);
    process.exit(1);
  }
  
  try {
    const result = await orderDetail({
      orderCode: args.orderCode
    });
    
    if (result) {
      console.log('📊 订单详情:');
      console.log(JSON.stringify(result, null, 2));
      
      if (result.body) {
        const data = result.body;
        console.log('\n📋 订单摘要:');
        console.log(`   订单编号: ${data.orderCode || args.orderCode}`);
        console.log(`   订单状态: ${getOrderStatusText(data.state)}`);
        if (data.orderPrice) {
          console.log(`   配送费用: ${formatPrice(data.orderPrice)} 元`);
        }
        if (data.fromAddress) {
          console.log(`   起点地址: ${data.fromAddress}`);
        }
        if (data.toAddress) {
          console.log(`   终点地址: ${data.toAddress}`);
        }
        if (data.driverName) {
          console.log(`   骑手姓名: ${data.driverName}`);
          console.log(`   骑手电话: ${data.driverMobile || '-'}`);
        }
      }
    }
  } catch (error) {
    console.error('执行失败:', error.message);
    process.exit(1);
  }
}

main();


path: scripts/order-price.js
content: #!/usr/bin/env node

/**
 * 订单询价脚本
 * 用法: node order-price.js --fromAddress="起始地址" --toAddress="目的地址" [--cityName="城市名"] [--orderType="send|help"]
 */

const { orderPrice, formatPrice } = require('../index');

// 解析命令行参数
function parseArgs() {
  const args = {};
  process.argv.slice(2).forEach(arg => {
    const match = arg.match(/^--(\w+)=(.+)$/);
    if (match) {
      args[match[1]] = match[2].replace(/^["']|["']$/g, '');
    }
  });
  return args;
}

async function main() {
  const args = parseArgs();
  
  if (!args.fromAddress || !args.toAddress) {
    console.log(`
🚚 订单询价

用法:
  node order-price.js --fromAddress="起始地址" --toAddress="目的地址" [--cityName="城市名"] [--orderType="send|help"]

参数:
  --fromAddress  起始地址（必填，要求完整地址信息。帮忙订单时填写帮忙地点）
  --toAddress    目的地址（必填，要求完整地址信息。帮忙订单时与fromAddress相同）
  --cityName     城市名称（可选，默认郑州市，需要带"市"字）
  --orderType    订单类型（可选，send=跑腿配送，help=帮忙服务，默认send）

示例:
  # 跑腿配送询价
  node order-price.js --fromAddress="郑州市金水区农业路经三路交叉口" --toAddress="郑州市二七区德化街100号"
  node order-price.js --fromAddress="北京市朝阳区三里屯" --toAddress="北京市海淀区中关村" --cityName="北京市"
  # 帮忙服务询价
  node order-price.js --fromAddress="郑州市金水区农业路经三路交叉口" --orderType="help"
`);
    process.exit(1);
  }
  
  try {
    const result = await orderPrice({
      fromAddress: args.fromAddress,
      toAddress: args.toAddress,
      cityName: args.cityName,
      orderType: args.orderType
    });
    
    if (result) {
      console.log('📊 询价结果:');
      console.log(JSON.stringify(result, null, 2));
      
      // 如果有价格信息，格式化显示
      if (result.body) {
        console.log('\n💰 价格摘要:');
        // 优先使用 needPayMoney，其次使用 totalMoney
        const price = result.body.needPayMoney !== undefined ? result.body.needPayMoney : (result.body.totalMoney || 0);
        console.log(`   预估费用: ${formatPrice(price)} 元`);
        if (result.body.distance) {
          console.log(`   配送距离: ${(result.body.distance / 1000).toFixed(2)} 公里`);
        }
        if (result.body.priceToken) {
          console.log(`   priceToken: ${result.body.priceToken}`);
          console.log('\n💡 提示: 使用此 priceToken 创建订单');
        }
      }
    }
  } catch (error) {
    console.error('执行失败:', error.message);
    process.exit(1);
  }
}

main();


path: scripts/register.js
content: #!/usr/bin/env node

/**
 * 手机号注册/获取授权脚本
 * 用法:
 *   发送验证码: node register.js --mobile="手机号"
 *   完成授权:   node register.js --mobile="手机号" --smsCode="验证码"
 *   带图片验证: node register.js --mobile="手机号" --imageCode="图片验证码"
 */

const { getPublicIp, sendSmsCode, auth, getConfig } = require('../index');

// 解析命令行参数
function parseArgs() {
  const args = {};
  process.argv.slice(2).forEach(arg => {
    const match = arg.match(/^--(\w+)=(.+)$/);
    if (match) {
      args[match[1]] = match[2].replace(/^["']|["']$/g, '');
    }
  });
  return args;
}

async function main() {
  const args = parseArgs();
  
  if (!args.mobile) {
    console.log(`
📱 手机号注册/获取授权

用法:
  Step 1 - 发送验证码:
    node register.js --mobile="手机号"

  Step 2 - 完成授权:
    node register.js --mobile="手机号" --smsCode="短信验证码"

参数:
  --mobile     用户手机号（必填）
  --smsCode    短信验证码（第二步必填）
  --imageCode  图片验证码（当需要时）
  --ip         手动指定公网IP（可选，默认自动获取）

示例:
  node register.js --mobile="13800138000"
  node register.js --mobile="13800138000" --smsCode="123456"
  node register.js --mobile="13800138000" --imageCode="8523"
`);
    process.exit(1);
  }
  
  try {
    // 获取公网 IP
    let userIp = args.ip;
    if (!userIp) {
      console.log('🌐 正在获取公网 IP...');
      userIp = await getPublicIp();
      if (!userIp) {
        console.error('❌ 无法自动获取公网 IP，请使用 --ip 参数手动指定');
        process.exit(1);
      }
      console.log(`   公网 IP: ${userIp}`);
    }
    
    // 如果提供了短信验证码，执行授权流程
    if (args.smsCode) {
      console.log('\n🔐 正在完成商户授权...');
      const authResult = await auth({
        userMobile: args.mobile,
        userIp: userIp,
        smsCode: args.smsCode
      });
      
      if (authResult && authResult.body && authResult.body.openId) {
        console.log('\n[REGISTRATION_SUCCESS]');
        console.log(`✅ 注册成功！openId 已保存到配置文件。`);
        console.log(`   openId: ${authResult.body.openId}`);
      } else {
        console.log('\n[REGISTRATION_FAILED]');
        console.error('❌ 授权失败');
        if (authResult) {
          console.error(`   错误码: ${authResult.code}`);
          console.error(`   错误信息: ${authResult.msg}`);
        }
        console.log('\n💡 请重新发送验证码后重试');
        process.exit(1);
      }
      return;
    }
    
    // 否则，发送短信验证码
    console.log('\n📱 正在发送短信验证码...');
    const smsResult = await sendSmsCode({
      userMobile: args.mobile,
      userIp: userIp,
      imageCode: args.imageCode || ''
    });
    
    if (!smsResult) {
      console.error('❌ 发送验证码失败，请稍后重试');
      process.exit(1);
    }
    
    // 检查是否需要图片验证码
    if (smsResult.code === 88100106 || smsResult.code === '88100106') {
      console.log('\n[IMAGE_CAPTCHA_REQUIRED]');
      // msg 字段包含 base64 图片数据
      if (smsResult.msg) {
        console.log(`IMAGE_DATA=data:image/png;base64,${smsResult.msg}`);
      }
      console.log('\n⚠️ 需要图片验证码，请识别图片中的数字后重新运行:');
      console.log(`   node register.js --mobile="${args.mobile}" --imageCode="图片中的数字"`);
      process.exit(2);
    }
    
    if (smsResult.code === 1 || smsResult.code === '1') {
      console.log('\n[SMS_SENT]');
      console.log('✅ 验证码已发送，请查看手机短信。');
      console.log('\n📩 收到验证码后，请运行:');
      console.log(`   node register.js --mobile="${args.mobile}" --smsCode="收到的验证码"`);
    } else {
      console.error(`\n❌ 发送验证码失败: ${smsResult.msg || '未知错误'}`);
      process.exit(1);
    }
    
  } catch (error) {
    console.error('执行失败:', error.message);
    process.exit(1);
  }
}

main();


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal:  | SkillSpector: suspicious (CRITICAL)