Skill: wolful/hotel-manager (version 1.0.1)

--- name: hotel-manager description: 酒店管家 Skill - 管理酒店 OTA 基础信息、房价库存及订单同步。 env_vars: - OTA_CTRIP_API_KEY - OTA_CTRIP_SECRET - OTA_MEITUAN_API_KEY - OTA_MEITUAN_SECRET - INTERNAL_PMS_URL - INTERNAL_PMS_TOKEN --- # Hotel Manager Skill 本 Skill 为酒店管理人员提供多平台（OTA）统一管理方案，支持 **API 自动化** 与 **浏览器自动化** 双模式。 > **[!IMPORTANT]** > 本仓库目前提供的是 **集成架构模板**。在使用前，您需要根据特定平台的 API 文档完善 `scripts/ota_client.js` 中的协议细节。 ## 安全与隐私 (Security) 为了保护您的 API 密钥，本 Skill 采用以下安全策略： 1. **环境变量优先**：强烈建议通过环境变量配置凭证。 2. **浏览器沙箱**：UI 自动化模式利用 Agent 已有的浏览器会话，不需本地存储明文密码。 3. **安全传输**：脚本强制使用 HTTPS 请求。 ## 配置指南 ### 方法 A: 环境变量 (推荐 API 模式) 设置 `OTA_CTRIP_API_KEY` 等变量。详细列表见上方元数据。 ### 方法 B: UI 自动化模式 (无 API 时) 无需特殊 API 配置，Agent 将直接操作浏览器。参考 `scripts/browser_controller.js` 了解 SOP。 ## 核心功能 1. **OTA 基础信息管理**：同步酒店描述、设施等。 2. **日常价库调整**：支持批量 API 更新及智能体 **直接改价（浏览器模拟）**。 3. **房态控制**：快速开关特定房型，支持跨平台同步。 4. **自动接单同步**：一键开启订单抓取并推送到内部系统。 ## 操作模式 ### 1. API 模式 高速执行，适用于大型酒店连锁。逻辑位于 `scripts/ota_client.js`。 ### 2. 浏览器直接改价 (Visual Mode) 适用于无法获取 API 权限的小型客栈或特殊 OTA 平台。 - **Agent 执行逻辑**：打开对应的 OTA 后台网页 -> 自动定位改价区 -> 执行修改并截图存证。 - **详见**：`scripts/browser_controller.js`。 ## 权限说明 - **网络访问**：访问 OTA 后台及内部 API。 - **浏览器权限**：需要读写及点击操作权限。 ## 最佳实践 - **操作留痕**：UI 模式下建议 Agent 在每次改价后提供页面截图。 - **速率限制**：避免过快操作导致封禁，脚本预留了休眠策略。

path: scripts/ota_client.js
content: const https = require('https');
const config = require('./config');

/**
 * OTA Client
 * Handles communication with various OTA APIs.
 */
class OTAClient {
  constructor(platformKey) {
    this.platformKey = platformKey;
    this.platformConfig = config.get('platforms')[platformKey];
    
    if (!this.platformConfig || !this.platformConfig.apiKey) {
      console.warn(`[${platformKey}] Warning: No API configuration found. Client will run in MOCK mode.`);
    }
  }

  /**
   * Internal generic request handler
   */
  async _request(method, path, data) {
    if (!this.platformConfig || !this.platformConfig.apiKey) {
      console.log(`[MOCK][${this.platformKey}] ${method} ${path}`, data || '');
      return { status: 'mock_success' };
    }

    // Example of real network behavior scaffolding
    return new Promise((resolve, reject) => {
      const options = {
        hostname: new URL(this.platformConfig.endpoint).hostname,
        path: path,
        method: method,
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${this.platformConfig.apiKey}`
        }
      };

      const req = https.request(options, (res) => {
        let body = '';
        res.on('data', (chunk) => body += chunk);
        res.on('end', () => resolve(JSON.parse(body)));
      });

      req.on('error', (e) => reject(e));
      if (data) req.write(JSON.stringify(data));
      req.end();
    });
  }

  async updatePrice(roomTypeId, date, price) {
    return this._request('POST', '/update-price', { roomTypeId, date, price });
  }

  async updateInventory(roomTypeId, date, count) {
    return this._request('POST', '/update-inventory', { roomTypeId, date, count });
  }

  async setRoomStatus(roomTypeId, date, isOpen) {
    return this._request('POST', '/set-status', { roomTypeId, date, isOpen });
  }

  async fetchNewOrders() {
    if (!this.platformConfig || !this.platformConfig.apiKey) {
      return [{ id: `MOCK_ORD_${Date.now()}`, guest: 'Sample Guest', total: 0 }];
    }
    return this._request('GET', '/orders');
  }
}

module.exports = OTAClient;


path: scripts/order_sync.js
content: const OTAClient = require('./ota_client');
const config = require('./config');

class OrderSyncService {
  constructor() {
    this.clients = Object.keys(config.get('platforms')).map(key => new OTAClient(key));
    this.internalPms = config.get('internalPms');
  }

  async syncOrders() {
    console.log(`[${new Date().toISOString()}] Starting order sync...`);
    
    for (const client of this.clients) {
      try {
        const orders = await client.fetchNewOrders();
        for (const order of orders) {
          await this.pushToInternalSystem(order, client.platformKey);
        }
      } catch (err) {
        console.error(`Error fetching orders from ${client.platformKey}:`, err.message);
      }
    }
  }

  async pushToInternalSystem(order, platformKey) {
    if (!this.internalPms.apiUrl) {
      console.log(`[MOCK][Sync] Pushing order ${order.id} from ${platformKey} to internal system...`);
      return { success: true };
    }

    console.log(`[Sync] Pushing order ${order.id} from ${platformKey} to ${this.internalPms.apiUrl}...`);
    // Real implementation would use fetch/https to push to internalPms.apiUrl
    return { success: true };
  }

  startAutoSync(intervalMs = 60000) {
    console.log(`Order sync service started (Interval: ${intervalMs}ms)`);
    setInterval(() => this.syncOrders(), intervalMs);
    this.syncOrders();
  }
}

// Example usage
if (require.main === module) {
  const service = new OrderSyncService();
  service.startAutoSync(300000); // Sync every 5 mins
}

module.exports = OrderSyncService;


path: scripts/config.js
content: /**
 * Config Loader
 * Safeguards credentials by prioritizing Environment Variables over local JSON files.
 */

const fs = require('fs');
const path = require('path');

class Config {
  constructor() {
    this.values = {};
    this.load();
  }

  load() {
    // 1. Try to load from environment variables (Highest priority)
    this.values = {
      platforms: {
        ctrip: {
          [REDACTED_SECRET],
          [REDACTED_SECRET],
          endpoint: process.env.OTA_CTRIP_ENDPOINT || 'https://api.ctrip.com/hotel/v1'
        },
        meituan: {
          [REDACTED_SECRET],
          [REDACTED_SECRET],
          endpoint: process.env.OTA_MEITUAN_ENDPOINT || 'https://api.meituan.com/hotel/v1'
        }
      },
      internalPms: {
        apiUrl: process.env.INTERNAL_PMS_URL,
        [REDACTED_SECRET]
      }
    };

    // 2. Try to load from a local config file if not fully provided by ENV
    const configPath = process.env.HOTEL_MANAGER_CONFIG_PATH || path.join(__dirname, '../resources/ota_config.json');
    if (fs.existsSync(configPath)) {
      try {
        const fileContent = fs.readFileSync(configPath, 'utf8');
        const fileConfig = JSON.parse(fileContent);
        this.merge(fileConfig);
      } catch (err) {
        console.warn(`Warning: Could not parse config file at ${configPath}: ${err.message}`);
      }
    }
  }

  merge(fileConfig) {
    // Deep merge logic simplified for this skill
    if (fileConfig.platforms) {
      fileConfig.platforms.forEach(p => {
        const key = this.mapKey(p.name);
        if (key && this.values.platforms[key]) {
          this.values.platforms[key].[REDACTED_SECRET] || p.api_key;
          this.values.platforms[key].[REDACTED_SECRET] || p.secret;
          this.values.platforms[key].endpoint = this.values.platforms[key].endpoint || p.api_endpoint;
        }
      });
    }
    if (fileConfig.internal_pms) {
      this.values.internalPms.apiUrl = this.values.internalPms.apiUrl || fileConfig.internal_pms.api_url;
      this.values.internalPms.[REDACTED_SECRET] || fileConfig.internal_pms.token;
    }
  }

  mapKey(name) {
    if (name.includes('携程')) return 'ctrip';
    if (name.includes('美团')) return 'meituan';
    return null;
  }

  get(key) {
    return this.values[key];
  }
}

module.exports = new Config();


path: scripts/price_manager.js
content: const OTAClient = require('./ota_client');
const config = require('./config');

class PriceManager {
  constructor() {
    this.clients = Object.keys(config.get('platforms')).map(key => new OTAClient(key));
  }

  /**
   * Update prices across all platforms with a base price.
   */
  async updateUnifiedPrice(roomTypeId, date, basePrice) {
    console.log(`--- Starting Unified Price Update for ${roomTypeId} on ${date} (Base: ${basePrice}) ---`);
    
    const results = await Promise.all(this.clients.map(async client => {
      // Example logic: Different platforms might have different markups
      let platformPrice = basePrice;
      if (client.platformKey === 'ctrip') platformPrice += 10;
      if (client.platformKey === 'meituan') platformPrice -= 5;
      
      return await client.updatePrice(roomTypeId, date, platformPrice);
    }));

    console.log('--- Unified Update Finished ---');
    return results;
  }
}

// Example usage
if (require.main === module) {
  const manager = new PriceManager();
  manager.updateUnifiedPrice('RM_001', '2024-05-01', 500);
}

module.exports = PriceManager;


path: scripts/browser_controller.js
content: /**
 * Browser Controller - UI Automation SOP
 * 
 * 这个脚本定义了在没有 API 的情况下，如何通过浏览器访问 OTA 后台进行操作。
 * 它可以被支持浏览器工具的 Agent (如 Antigravity 或 openclaw) 直接解析执行。
 */

const PLATFORM_URLS = {
  ctrip: 'https://ebooking.ctrip.com/',
  meituan: 'https://eb.meituan.com/hotel/',
  fliggy: 'https://b.fliggy.com/'
};

class BrowserController {
  /**
   * 引导 Agent 执行改价流程的描述 (Prompt-based Instructions)
   */
  async getPriceUpdateSOP(platform, roomType, date, newPrice) {
    return {
      target_url: PLATFORM_URLS[platform],
      steps: [
        `1. 导航到 ${PLATFORM_URLS[platform]} 并确保已登录。`,
        `2. 在侧边栏寻找“价格维护”或“房态房价”菜单并点击。`,
        `3. 在日期筛选器中选择 ${date}。`,
        `4. 定位到房型名称为 "${roomType}" 的行。`,
        `5. 找到对应的价格输入框，将其值修改为 "${newPrice}"。`,
        `6. 点击页面底部的“保存”或“提交”按钮。`,
        `7. 截图并确认页面显示“修改成功”字样。`
      ],
      selectors: {
        // 预设一些常见的选择器（示例，需根据实际 DOM 调整）
        price_input: `input[data-room="${roomType}"][data-date="${date}"]`,
        save_button: 'button#btn-save-all'
      }
    };
  }
}

module.exports = new BrowserController();


path: examples/update_price.md
content: # 如何使用 Hotel Manager Skill 进行统一调价

本示例展示如何通过脚本一次性调整所有 OTA 平台的五一节假日价格。

## 场景
由于“五一”劳动节即将来临，酒店决定将所有房型的基础价格提高 200 元。

## 步骤

### 1. 确认配置文件
确保 `resources/ota_config.json` 中已填入正确的 API Key。

### 2. 执行调价脚本
在终端中运行以下命令：

```bash
# 假设房型 ID 为 "deluxe_king"，日期为 "2024-05-01"，基准价为 800
node scripts/price_manager.js
```

> 注意：生产环境下，您应通过命令行参数或外部调用传递具体数值。

### 3. 查看输出日志
您将看到类似以下内容的日志：
```text
--- Starting Unified Price Update for deluxe_king on 2024-05-01 (Base: 800) ---
[携程] Updating price for deluxe_king on 2024-05-01 to 810...
[美团] Updating price for deluxe_king on 2024-05-01 to 800...
[飞猪] Updating price for deluxe_king on 2024-05-01 to 795...
--- Unified Update Finished ---
```

## 自动接单设置
若要开启自动同步订单至内部 PMS，请运行：
```bash
node scripts/order_sync.js
```
该服务会每隔 5 分钟轮询一次新订单。


path: resources/ota_config.example.json
content: {
  "platforms": [
    {
      "name": "携程",
      "api_endpoint": "https://api.ctrip.com/hotel/v1",
      "api_key": "YOUR_API_KEY_HERE",
      "secret": "YOUR_SECRET_HERE"
    },
    {
      "name": "美团",
      "api_endpoint": "https://api.meituan.com/hotel/v1",
      "api_key": "YOUR_API_KEY_HERE",
      "secret": "YOUR_SECRET_HERE"
    }
  ],
  "internal_pms": {
    "api_url": "YOUR_PMS_URL_HERE",
    "token": "YOUR_TOKEN_HERE"
  }
}


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