Skill: minusgod/swagger-skill (version 1.0.1)

--- name: swagger-skill description: 智能 Swagger API 查询和调用工具。通过自然语言指令直接查询接口详情、调用 API，无需繁琐的交互步骤。 metadata: clawdbot: emoji: "🏔️" requires: bins: ["node"] --- ## 功能特性 - **一键查询**: 直接查询接口详情，自动解析参数、请求体、响应模式 - **自然语言搜索**: 根据自然语言描述找到匹配的接口（如"保存用户"、"获取数据集列表"），支持 tags 匹配 - **智能接口调用**: 根据自然语言指令自动匹配并调用相应的 API - **完整信息展示**: 自动获取并展示接口的完整信息（参数、请求体、响应、数据模式定义） - **文件上传支持**: 支持 multipart/form-data 文件上传 - **分层缓存**: 轻量索引用于列表/搜索，Map 结构 O(1) 详情查找 - **Swagger 2.0 兼容**: 同时支持 OpenAPI 3.0 和 Swagger 2.0 规范 - **灵活认证**: 支持 Token、Cookie 或无需验证的多种认证方式 ## 安装 无需手动安装依赖。首次使用时会自动检测并安装所需依赖（axios、form-data），同时自动初始化 package.json（含 `"type": "module"` 配置）。 如需手动安装，可在 skill 目录下执行： ```bash npm install ``` ## 使用方法 ### 基础使用 ```javascript import SwaggerAPISkill from './index.js'; const skill = new SwaggerAPISkill(); // 1. 加载 Swagger 规范 await skill.fetchSwaggerSpec('https://api.example.com/swagger.json'); // 2. 获取所有接口 const allAPIs = skill.getAllAPIs(); // 3. 搜索接口 const results = skill.searchAPI('获取用户信息'); // 4. 获取接口详情 const detail = skill.getAPIDetail('/users/{id}', 'GET'); // 5. 调用接口 const response = await skill.callAPI('/users', 'GET', { query: { page: 1, limit: 10 } }); // 6. 通过自然语言指令调用 const result = await skill.callAPIByInstruction('获取所有用户', { query: { page: 1 } }); ``` ## 认证方法 ### 方法 1: 使用 Token 认证 ```javascript import SwaggerAPISkill from './index.js'; const skill = new SwaggerAPISkill(); // 方式 A: 先设置 Token，再加载规范 skill.setAuthToken('your-jwt-token'); await skill.fetchSwaggerSpec('http://localhost:8090/v2/api-docs'); // 方式 B: 在加载规范时直接传入 Token await skill.fetchSwaggerSpec('http://localhost:8090/v2/api-docs', { [REDACTED_SECRET]', tokenOptions: { tokenType: 'Bearer', headerName: 'Authorization' } }); // 调用 API（会自动添加认证头） const result = await skill.callAPI('/sysUser/list', 'POST', { body: { pageNum: 1, pageSize: 10 } }); ``` ### 方法 2: 使用 Cookie 认证 ```javascript const skill = new SwaggerAPISkill(); // 方式 A: 先设置 Cookie，再加载规范 skill.setAuthCookies({ [REDACTED_SECRET]', JSESSIONID: 'your-session-id' }); await skill.fetchSwaggerSpec('http://localhost:8090/v2/api-docs'); // 方式 B: 在加载规范时直接传入 Cookie await skill.fetchSwaggerSpec('http://localhost:8090/v2/api-docs', { cookies: { [REDACTED_SECRET]', JSESSIONID: 'your-session-id' } }); ``` ### 方法 3: 无需认证 ```javascript const skill = new SwaggerAPISkill(); // 直接加载规范 await skill.fetchSwaggerSpec('http://localhost:8090/v2/api-docs'); // 调用 API const result = await skill.callAPI('/users', 'GET', { query: { page: 1, limit: 10 } }); ``` ### 方法 4: 使用 CLI 工具（推荐） ```bash node cli.js ``` 交互式 CLI 工具会引导你： 1. 输入 Swagger API 文档 URL 2. 输入认证 Token（可选） 3. 通过菜单选择操作（获取接口列表、搜索、调用等） ## API 文档 ### fetchSwaggerSpec(url, options) 获取并加载 Swagger 规范文件。 **参数:** - `url` (string): Swagger JSON URL 或 API 基础 URL - `options` (object): 可选配置 - `token` (string): JWT Token 或其他认证 Token - `cookies` (object): Cookie 对象，如 `{ token: 'xxx', JSESSIONID: 'xxx' }` - `tokenOptions` (object): Token 选项 - `tokenType` (string): Token 类型，默认为 'Bearer' - `headerName` (string): 请求头名称，默认为 'Authorization' **返回:** ```javascript { success: boolean, apiCount?: number, // 接口总数 cached?: boolean, // 仅缓存命中时返回 true error?: string } ``` ### setAuthToken(token, options) 设置认证 Token。 **参数:** - `token` (string): JWT Token 或其他认证 Token - `options` (object): 可选配置 - `tokenType` (string): Token 类型，默认为 'Bearer' - `headerName` (string): 请求头名称，默认为 'Authorization' **返回:** ```javascript { success: boolean, message: string } ``` ### setAuthCookies(cookies) 设置认证 Cookie。 **参数:** - `cookies` (object): Cookie 对象，如 `{ token: 'xxx', JSESSIONID: 'xxx' }` **返回:** ```javascript { success: boolean, message: string } ``` ### clearAuth() 清除认证信息。 **返回:** ```javascript { success: boolean, message: string } ``` ### getAllAPIs() 获取所有接口的基本信息。 **返回:** ```javascript { success: boolean, total: number, apis: Array<{ path: string, method: string, summary: string, description: string, operationId: string, tags: string[] }> } ``` ### searchAPI(query) 根据自然语言查询搜索接口。支持 summary、description、path、operationId 和 tags 匹配。 **参数:** - `query` (string): 自然语言查询字符串 **返回:** ```javascript { success: boolean, query: string, matchCount: number, results: Array<{ path: string, method: string, summary: string, description?: string, // 仅非空时返回 score: number }> } ``` ### getAPIDetail(path, method) 获取特定接口的详细信息。使用 Map O(1) 查找。 **参数:** - `path` (string): API 路径，如 `/users/{id}` - `method` (string): HTTP 方法，如 `GET`, `POST` 等 **返回:** ```javascript { success: boolean, detail?: { path: string, method: string, summary: string, description: string, parameters: Array, requestBody: object, responses: object, tags: Array }, error?: string } ``` ### getFullAPIDetail(path, method) 获取完整的接口详情，包括关联的数据模式定义。兼容 OpenAPI 3.0 和 Swagger 2.0。 **参数:** - `path` (string): API 路径 - `method` (string): HTTP 方法 **返回:** ```javascript { success: boolean, detail?: { path: string, method: string, summary: string, description: string, parameters: Array, requestBody: object, responses: object, tags: Array, relatedSchemas: object, // 关联的数据模式定义 schemaCount: number }, error?: string } ``` ### callAPI(path, method, params) 调用 API 接口。支持 JSON 请求和 multipart/form-data 文件上传。 **参数:** - `path` (string): API 路径 - `method` (string): HTTP 方法 - `params` (object): 请求参数 - `query` (object): 查询参数 - `body` (object): 请求体（JSON 或 FormData） - `headers` (object): 自定义请求头 - `isFormData` (boolean): 是否为 FormData（文件上传） **返回:** ```javascript { success: boolean, status?: number, data?: any, error?: string } ``` **示例 - JSON 请求:** ```javascript const response = await skill.callAPI('/api/users', 'POST', { body: { name: 'John', email: '[REDACTED_SECRET]' } }); ``` **示例 - 文件上传（使用 FormData）:** ```javascript import FormData from 'form-data'; import fs from 'fs'; const form = new FormData(); form.append('file', fs.createReadStream('./data.jsonl')); form.append('name', 'My Dataset'); form.append('type', 'train_data'); const response = await skill.callAPI('/api/datasets/', 'POST', { body: form, isFormData: true }); ``` ### callAPIByInstruction(instruction, params) 根据自然语言指令调用 API。 **参数:** - `instruction` (string): 自然语言指令 - `params` (object): 请求参数（同 callAPI） **返回:** ```javascript { success: boolean, instruction: string, matchedAPI?: { path: string, method: string, summary: string, matchScore: number }, result: object, error?: string } ``` ### uploadFile(path, formData, query) 文件上传方法，支持 multipart/form-data。 **参数:** - `path` (string): API 路径 - `formData` (object): 表单数据对象 - `file`: 文件内容（Buffer）或文件路径（string） - 其他字段: 表单字段（自动转换为字符串） - `query` (object): 查询参数（可选） **返回:** ```javascript { success: boolean, status?: number, data?: any, error?: string } ``` **示例:** ```javascript import SwaggerAPISkill from './index.js'; const skill = new SwaggerAPISkill(); await skill.fetchSwaggerSpec('http://localhost:8000/openapi.json'); // 方式1: 使用文件路径 const result1 = await skill.uploadFile('/api/datasets/', { file: './test_dataset.jsonl', name: 'AI知识问答对', type: 'train_data', description: '人工智能相关的问答对数据集' }); // 方式2: 使用 Buffer import fs from 'fs'; const fileBuffer = fs.readFileSync('./test_dataset.jsonl'); const result2 = await skill.uploadFile('/api/datasets/', { file: fileBuffer, name: 'AI知识问答对', type: 'train_data', description: '人工智能相关的问答对数据集' }); ``` ### getSessionId() 获取当前会话ID。 **返回:** ```javascript string // 唯一的会话ID，格式: session_timestamp_randomId ``` ### refreshSession() 刷新会话，清空所有缓存数据。 **返回:** ```javascript { success: boolean, message: string } ``` ## 缓存机制 swagger-skill 实现了分层缓存来优化性能和 token 消耗： 1. **轻量索引 (apiIndex)**: 仅存储 path/method/summary/description/operationId/tags，用于 `getAllAPIs()` 和 `searchAPI()` 2. **详情 Map (apiDetailMap)**: `"METHOD /path" → 完整详情`，用于 `getAPIDetail()` 的 O(1) 查找 3. **首次加载**: 调用 `fetchSwaggerSpec()` 时从远程获取规范并构建两层缓存 4. **后续查询**: 所有查询操作直接使用内存缓存，无需重新加载 5. **会话管理**: 调用 `refreshSession()` 可清空缓存 ## 支持的 HTTP 方法 - GET - POST - PUT - DELETE - PATCH - HEAD - OPTIONS ## 注意事项 1. 需要网络连接来获取 Swagger 规范和调用 API 2. 某些 API 可能需要身份验证，可通过 `headers` 参数传递认证信息 3. 自然语言搜索基于关键词匹配，支持 summary、description、path、operationId 和 tags 4. 路径参数需要在 `query` 参数中提供 5. **文件上传**: - 使用 `uploadFile()` 方法是最简单的方式，支持文件路径或 Buffer - 也可以使用 `callAPI()` 方法配合 FormData 对象进行更灵活的控制 - 文件上传时不需要手动设置 Content-Type，会自动设置为 multipart/form-data 6. 同时兼容 OpenAPI 3.0 (`components.schemas`) 和 Swagger 2.0 (`definitions`) ## 许可证 MIT

path: index.js
content: import { execSync } from 'child_process';
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import path from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);

function ensureDependencies() {
  const deps = ['axios', 'form-data'];
  const missing = deps.filter(dep => {
    try { require.resolve(dep); return false; } catch { return true; }
  });
  if (missing.length > 0) {
    const pkgJsonPath = path.join(__dirname, 'package.json');
    try { require.resolve(pkgJsonPath); } catch {
      execSync(`npm init -y --prefix "${__dirname}" && node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('${pkgJsonPath}','utf8'));p.type='module';fs.writeFileSync('${pkgJsonPath}',JSON.stringify(p,null,2))"`, { stdio: 'pipe' });
    }
    execSync(`npm install --prefix "${__dirname}" ${missing.join(' ')}`, { stdio: 'pipe' });
  }
}

ensureDependencies();

const axios = (await import('axios')).default;

const HTTP_METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options']);

class SwaggerAPISkill {
  constructor() {
    this.swaggerSpec = null;
    this.baseUrl = null;
    this.apiIndex = null;       // 轻量搜索索引
    this.apiDetailMap = null;   // O(1) 详情查找
    this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    this.authToken = null;
    this.authCookies = null;
    this.authHeaders = {};
  }

  getSessionId() {
    return this.sessionId;
  }

  refreshSession() {
    this.swaggerSpec = null;
    this.baseUrl = null;
    this.apiIndex = null;
    this.apiDetailMap = null;
    this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    return { success: true, message: '会话已刷新' };
  }

  setAuthToken(token, options = {}) {
    if (!token || typeof token !== 'string') {
      return { success: false, message: 'Token 必须是非空字符串' };
    }
    const tokenType = options.tokenType || 'Bearer';
    const headerName = options.headerName || 'Authorization';
    this.authToken = token;
    this.authHeaders = { ...this.authHeaders, [headerName]: `${tokenType} ${token}` };
    return { success: true, message: '认证 Token 已设置' };
  }

  setAuthCookies(cookies = {}) {
    if (!cookies || typeof cookies !== 'object' || Array.isArray(cookies)) {
      return { success: false, message: 'cookies 必须是对象' };
    }
    const entries = Object.entries(cookies).filter(([, v]) => v !== undefined && v !== null);
    if (entries.length === 0) {
      return { success: false, message: 'cookies 不能为空' };
    }
    const invalid = entries.find(([name, value]) => {
      if (!name || typeof name !== 'string') return true;
      if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') return true;
      return /[;\r\n]/.test(name) || /[\r\n]/.test(String(value));
    });
    if (invalid) {
      return { success: false, message: `无效的 Cookie 键值: ${invalid[0]}` };
    }
    this.authCookies = Object.fromEntries(entries.map(([n, v]) => [n, String(v)]));
    this.authHeaders = {
      ...this.authHeaders,
      Cookie: Object.entries(this.authCookies).map(([n, v]) => `${n}=${encodeURIComponent(v)}`).join('; ')
    };
    return { success: true, message: '认证 Cookie 已设置' };
  }

  clearAuth() {
    this.authToken = null;
    this.authCookies = null;
    this.authHeaders = {};
    return { success: true, message: '认证信息已清除' };
  }

  async fetchSwaggerSpec(url, options = {}) {
    try {
      if (this.swaggerSpec && this.apiIndex) {
        return { success: true, apiCount: this.apiIndex.length, cached: true };
      }

      if (options.token) {
        const r = this.setAuthToken(options.token, options.tokenOptions);
        if (!r.success) return r;
      }
      if (options.cookies) {
        const r = this.setAuthCookies(options.cookies);
        if (!r.success) return r;
      }

      let specUrl = url;
      if (!url.match(/\/(swagger\.json|openapi\.json|api-docs|v3\/api-docs)$/)) {
        specUrl = url.endsWith('/') ? url + 'swagger.json' : url + '/swagger.json';
      }

      const response = await axios.get(specUrl, { headers: this.authHeaders });
      this.swaggerSpec = response.data;
      this.baseUrl = url.replace(/\/(swagger\.json|openapi\.json|v3\/api-docs)$/, '');
      this._buildAPICache();

      return { success: true, apiCount: this.apiIndex.length };
    } catch (error) {
      return { success: false, error: `获取 Swagger 规范失败: ${error.message}` };
    }
  }

  _buildAPICache() {
    this.apiIndex = [];
    this.apiDetailMap = new Map();

    if (!this.swaggerSpec?.paths) return;

    for (const path in this.swaggerSpec.paths) {
      const pathItem = this.swaggerSpec.paths[path];
      for (const method in pathItem) {
        if (!HTTP_METHODS.has(method.toLowerCase())) continue;
        const op = pathItem[method];
        const key = `${method.toUpperCase()} ${path}`;

        this.apiIndex.push({
          path,
          method: method.toUpperCase(),
          summary: op.summary || '',
          description: op.description || '',
          operationId: op.operationId || '',
          tags: op.tags || []
        });

        this.apiDetailMap.set(key, {
          path,
          method: method.toUpperCase(),
          summary: op.summary || '',
          description: op.description || '',
          parameters: op.parameters || [],
          requestBody: op.requestBody || null,
          responses: op.responses || {},
          tags: op.tags || []
        });
      }
    }
  }

  getAllAPIs() {
    if (!this.swaggerSpec) {
      return { success: false, error: '未加载 Swagger 规范，请先调用 fetchSwaggerSpec' };
    }
    if (!this.apiIndex) this._buildAPICache();
    return {
      success: true,
      total: this.apiIndex.length,
      apis: this.apiIndex
    };
  }

  searchAPI(query) {
    if (!this.swaggerSpec) {
      return { success: false, error: '未加载 Swagger 规范' };
    }
    if (!this.apiIndex) this._buildAPICache();

    const results = [];
    const queryLower = query.toLowerCase();
    const queryWords = this._tokenizeQuery(query);

    for (const api of this.apiIndex) {
      let score = 0;
      const { summary, description, path, operationId, tags } = api;
      const pathLower = path.toLowerCase();

      // 精确匹配
      if (summary === query) { score = 100; }
      else if (description === query) { score = 80; }
      else {
        // 完整包含匹配
        if (summary.includes(query)) score += 50;
        if (description.includes(query)) score += 30;
        if (pathLower.includes(queryLower)) score += 20;
        if (operationId?.toLowerCase().includes(queryLower)) score += 10;

        // tags 匹配
        if (tags.some(t => t.toLowerCase().includes(queryLower))) score += 15;

        // 分词匹配（仅当没有完整匹配时）
        if (score < 50) {
          for (const word of queryWords) {
            const wordLower = word.toLowerCase();
            if (summary.includes(word)) score += 15;
            if (description.includes(word)) score += 10;
            if (pathLower.includes(wordLower)) score += 5;
            if (tags.some(t => t.toLowerCase().includes(wordLower))) score += 8;
          }
        }
      }

      if (score > 0) {
        const item = { path, method: api.method, summary: summary || '无描述', score: score / 100 };
        if (description) item.description = description;
        results.push(item);
      }
    }

    results.sort((a, b) => b.score - a.score);
    return {
      success: true,
      query,
      matchCount: results.length,
      results: results.slice(0, 10)
    };
  }

  getAPIDetail(path, method) {
    if (!this.swaggerSpec) {
      return { success: false, error: '未加载 Swagger 规范' };
    }
    if (!this.apiDetailMap) this._buildAPICache();

    const api = this.apiDetailMap.get(`${method.toUpperCase()} ${path}`);
    if (!api) {
      return { success: false, error: `接口 ${method.toUpperCase()} ${path} 不存在` };
    }
    return { success: true, detail: api };
  }

  async callAPI(path, method, params = {}) {
    if (!this.swaggerSpec) {
      return { success: false, error: '未加载 Swagger 规范' };
    }
    try {
      const url = this._buildURL(path, params.query);
      const isFormData = params.isFormData || (params.body instanceof FormData);
      const config = {
        method: method.toLowerCase(),
        url,
        headers: isFormData
          ? { ...this.authHeaders, ...params.headers }
          : { 'Content-Type': 'application/json', ...this.authHeaders, ...params.headers }
      };
      if (params.body) config.data = params.body;
      if (params.query) config.params = params.query;

      const response = await axios(config);
      return { success: true, status: response.status, data: response.data };
    } catch (error) {
      return {
        success: false,
        status: error.response?.status,
        error: error.message,
        data: error.response?.data || {}
      };
    }
  }

  async callAPIByInstruction(instruction, params = {}) {
    const searchResult = this.searchAPI(instruction);
    if (!searchResult.success || searchResult.results.length === 0) {
      return {
        success: false,
        instruction,
        error: `未找到匹配的 API: ${instruction}`,
        suggestion: '请尝试使用其他关键词或查看 getAllAPIs() 获取所有可用接口'
      };
    }

    const topMatch = searchResult.results[0];
    const result = await this.callAPI(topMatch.path, topMatch.method, params);
    return {
      success: result.success,
      instruction,
      matchedAPI: { path: topMatch.path, method: topMatch.method, summary: topMatch.summary, matchScore: topMatch.score },
      result
    };
  }

  async uploadFile(path, formData = {}, query = null) {
    if (!this.swaggerSpec) {
      return { success: false, error: '未加载 Swagger 规范' };
    }
    try {
      const FormDataLib = (await import('form-data')).default;
      const fs = (await import('fs')).default;
      const form = new FormDataLib();

      for (const [key, value] of Object.entries(formData)) {
        if (key === 'file' && typeof value === 'string') {
          form.append(key, fs.createReadStream(value));
        } else if (key === 'file' && Buffer.isBuffer(value)) {
          form.append(key, value, 'file');
        } else {
          form.append(key, String(value));
        }
      }

      const url = this._buildURL(path, query);
      const config = {
        method: 'post',
        url,
        data: form,
        headers: { ...form.getHeaders(), ...this.authHeaders }
      };
      if (query) config.params = query;

      const response = await axios(config);
      return { success: true, status: response.status, data: response.data };
    } catch (error) {
      return {
        success: false,
        status: error.response?.status,
        error: error.message,
        data: error.response?.data || {}
      };
    }
  }

  getFullAPIDetail(path, method) {
    if (!this.swaggerSpec) {
      return { success: false, error: '未加载 Swagger 规范' };
    }

    const basicDetail = this.getAPIDetail(path, method);
    if (!basicDetail.success) return basicDetail;

    const detail = basicDetail.detail;
    const schemas = this.swaggerSpec.components?.schemas
      || this.swaggerSpec.definitions
      || {};

    const relatedSchemas = {};
    const schemaNames = new Set();

    // 从 requestBody 提取 $ref (OpenAPI 3.0)
    if (detail.requestBody?.content?.['application/json']?.schema?.$ref) {
      schemaNames.add(detail.requestBody.content['application/json'].schema.$ref.split('/').pop());
    }

    // 从 parameters 提取 $ref (Swagger 2.0 body 参数)
    (detail.parameters || []).forEach(param => {
      if (param.schema?.$ref) {
        schemaNames.add(param.schema.$ref.split('/').pop());
      }
    });

    // 从 responses 提取 $ref (兼容 OpenAPI 3.0 和 Swagger 2.0)
    Object.values(detail.responses || {}).forEach(response => {
      const ref = response.content?.['application/json']?.schema?.$ref
        || response.schema?.$ref;
      if (ref) schemaNames.add(ref.split('/').pop());
    });

    const collectSchemas = (schemaName, visited = new Set()) => {
      if (visited.has(schemaName) || !schemas[schemaName]) return;
      visited.add(schemaName);
      const schema = schemas[schemaName];
      relatedSchemas[schemaName] = schema;

      if (schema.properties) {
        Object.values(schema.properties).forEach(prop => {
          if (prop.$ref) collectSchemas(prop.$ref.split('/').pop(), visited);
          else if (prop.items?.$ref) collectSchemas(prop.items.$ref.split('/').pop(), visited);
        });
      }
      if (schema.anyOf) {
        schema.anyOf.forEach(item => {
          if (item.$ref) collectSchemas(item.$ref.split('/').pop(), visited);
        });
      }
      if (schema.items?.$ref) {
        collectSchemas(schema.items.$ref.split('/').pop(), visited);
      }
    };

    schemaNames.forEach(name => collectSchemas(name));

    return {
      success: true,
      detail: { ...detail, relatedSchemas, schemaCount: Object.keys(relatedSchemas).length }
    };
  }

  // Private helpers

  _buildURL(path, queryParams = {}) {
    if (!queryParams || Object.keys(queryParams).length === 0) {
      return this.baseUrl + path;
    }
    return this.baseUrl + path.replace(/\{(\w+)\}/g, (_, key) => queryParams[key] ?? `{${key}}`);
  }

  _tokenizeQuery(query) {
    const tokens = [];
    let word = '';
    for (let i = 0; i < query.length; i++) {
      const code = query.charCodeAt(i);
      const isChinese = code >= 0x4e00 && code <= 0x9fff;
      const isAlphaNum = (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95;
      if (isChinese) {
        if (word) { tokens.push(word); word = ''; }
        tokens.push(query[i]);
      } else if (isAlphaNum) {
        word += query[i];
      } else {
        if (word) { tokens.push(word); word = ''; }
      }
    }
    if (word) tokens.push(word);
    return tokens;
  }
}

export default SwaggerAPISkill;


path: cli.js
content: #!/usr/bin/env node

import SwaggerAPISkill from './index.js';
import readline from 'readline';

const skill = new SwaggerAPISkill();
let swaggerUrl = null;
let token = null;
let cookies = null;

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

function prompt(question) {
  return new Promise(resolve => {
    rl.question(question, resolve);
  });
}

async function main() {
  console.log('\n╔════════════════════════════════════════╗');
  console.log('║     Swagger API Skill CLI 工具         ║');
  console.log('╚════════════════════════════════════════╝\n');

  // 获取 Swagger URL
  swaggerUrl = await prompt('请输入 Swagger API 文档 URL (如: http://localhost:8090/v2/api-docs): ');
  if (!swaggerUrl.trim()) {
    console.error('❌ URL 不能为空');
    rl.close();
    return;
  }

  // 获取 Token（可选）
  const tokenInput = await prompt('请输入认证 Token (可选，按 Enter 跳过): ');
  if (tokenInput.trim()) {
    [REDACTED_SECRET]);
    console.log('✓ Token 已设置\n');
  } else {
    console.log('✓ 未设置 Token\n');
  }

  // 获取 Cookie（可选，JSON 格式）
  const cookieInput = await prompt('请输入认证 Cookie (JSON 格式，可选，按 Enter 跳过): ');
  if (cookieInput.trim()) {
    try {
      const parsed = JSON.parse(cookieInput);
      const cookieResult = skill.setAuthCookies(parsed);
      if (!cookieResult.success) {
        console.error(`❌ Cookie 设置失败: ${cookieResult.message}`);
        rl.close();
        return;
      }
      cookies = parsed;
      console.log('✓ Cookie 已设置\n');
    } catch (e) {
      console.error('❌ Cookie JSON 格式错误');
      rl.close();
      return;
    }
  } else if (!token) {
    console.log('✓ 无需认证\n');
  }

  // 加载 Swagger 规范
  console.log('正在加载 Swagger 规范...');
  const fetchOptions = {};
  if (token) {
    fetchOptions.token = token;
    fetchOptions.tokenOptions = {
      tokenType: 'Bearer',
      headerName: 'Authorization'
    };
  }
  if (cookies) {
    fetchOptions.cookies = cookies;
  }
  const specResult = await skill.fetchSwaggerSpec(swaggerUrl, fetchOptions);

  if (!specResult.success) {
    console.error('❌ 加载失败:', specResult.error);
    rl.close();
    return;
  }

  console.log(`✓ 加载成功，共找到 ${specResult.apiCount} 个接口\n`);

  // 交互菜单
  await showMenu();

  rl.close();
}

async function showMenu() {
  while (true) {
    console.log('\n═══════════════════════════════════════');
    console.log('请选择操作:');
    console.log('1. 获取所有接口列表');
    console.log('2. 搜索接口');
    console.log('3. 获取接口详情');
    console.log('4. 调用接口');
    console.log('5. 刷新会话');
    console.log('6. 退出');
    console.log('═══════════════════════════════════════');

    const choice = await prompt('\n请输入选项 (1-6): ');

    switch (choice.trim()) {
      case '1':
        await listAllAPIs();
        break;
      case '2':
        await searchAPIs();
        break;
      case '3':
        await getAPIDetail();
        break;
      case '4':
        await callAPI();
        break;
      case '5':
        await refreshSession();
        break;
      case '6':
        console.log('\n👋 再见！');
        return;
      default:
        console.log('❌ 无效的选项');
    }
  }
}

async function listAllAPIs() {
  console.log('\n📋 所有接口列表:\n');
  const allAPIs = skill.getAllAPIs();
  allAPIs.apis.forEach((api, index) => {
    console.log(`${index + 1}. [${api.method.toUpperCase()}] ${api.path}`);
    if (api.summary) console.log(`   📝 ${api.summary}`);
  });
  console.log(`\n共 ${allAPIs.total} 个接口`);
}

async function searchAPIs() {
  const query = await prompt('\n请输入搜索关键词: ');
  if (!query.trim()) {
    console.log('❌ 关键词不能为空');
    return;
  }

  const results = skill.searchAPI(query);
  if (results.matchCount === 0) {
    console.log(`\n❌ 未找到匹配 "${query}" 的接口`);
    return;
  }

  console.log(`\n🔍 搜索结果 (共 ${results.matchCount} 个):\n`);
  results.results.forEach((result, index) => {
    console.log(`${index + 1}. [${result.method.toUpperCase()}] ${result.path}`);
    console.log(`   📝 ${result.summary || result.description || '无描述'}`);
    console.log(`   匹配度: ${(result.score * 100).toFixed(2)}%\n`);
  });
}

async function getAPIDetail() {
  const path = await prompt('\n请输入接口路径 (如: /users/{id}): ');
  const method = await prompt('请输入 HTTP 方法 (GET/POST/PUT/DELETE 等): ');

  if (!path.trim() || !method.trim()) {
    console.log('❌ 路径和方法不能为空');
    return;
  }

  const detail = skill.getAPIDetail(path.trim(), method.trim().toUpperCase());
  if (!detail.success) {
    console.log(`❌ ${detail.error}`);
    return;
  }

  console.log('\n📄 接口详情:\n');
  console.log(JSON.stringify(detail.detail, null, 2));
}

async function callAPI() {
  const path = await prompt('\n请输入接口路径: ');
  const method = await prompt('请输入 HTTP 方法: ');

  if (!path.trim() || !method.trim()) {
    console.log('❌ 路径和方法不能为空');
    return;
  }

  const queryStr = await prompt('请输入查询参数 (JSON 格式，可选): ');
  const bodyStr = await prompt('请输入请求体 (JSON 格式，可选): ');

  const params = {};
  if (queryStr.trim()) {
    try {
      params.query = JSON.parse(queryStr);
    } catch (e) {
      console.log('❌ 查询参数 JSON 格式错误');
      return;
    }
  }
  if (bodyStr.trim()) {
    try {
      params.body = JSON.parse(bodyStr);
    } catch (e) {
      console.log('❌ 请求体 JSON 格式错误');
      return;
    }
  }

  console.log('\n正在调用接口...');
  const result = await skill.callAPI(path.trim(), method.trim().toUpperCase(), params);

  if (result.success) {
    console.log('\n✓ 调用成功\n');
    console.log('响应数据:');
    console.log(JSON.stringify(result.data, null, 2));
  } else {
    console.log(`\n❌ 调用失败: ${result.error}`);
  }
}

async function refreshSession() {
  const result = skill.refreshSession();
  console.log(`\n✓ ${result.message}`);
  console.log(`新会话ID: ${skill.getSessionId()}`);

  // 重新加载 Swagger 规范
  console.log('\n正在重新加载 Swagger 规范...');
  const fetchOptions = {};
  if (token) {
    fetchOptions.token = token;
    fetchOptions.tokenOptions = {
      tokenType: 'Bearer',
      headerName: 'Authorization'
    };
  }
  if (cookies) {
    fetchOptions.cookies = cookies;
  }
  const specResult = await skill.fetchSwaggerSpec(swaggerUrl, fetchOptions);

  if (specResult.success) {
    console.log(`✓ 重新加载成功，共 ${specResult.apiCount} 个接口`);
  } else {
    console.log(`❌ 重新加载失败: ${specResult.error}`);
  }
}

main().catch(console.error);


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