Skill: juan-xin-cai/google-deep-research (version 1.0.2)

--- name: deep-research description: "使用 Gemini Deep Research Agent 进行自主深度研究，生成带引用的详细研究报告。当用户需要深度调研话题、生成研究报告、或需要多轮搜索和分析时使用。触发词：深度研究、deep research、帮我调研、研究一下、写研究报告、deep dive、详细分析。" metadata: openclaw: primaryEnv: GEMINI_API_KEY homepage: https://www.npmjs.com/package/@google/genai --- # Deep Research Agent 使用 Gemini Deep Research Pro 执行自主多步搜索-阅读-分析的深度研究，通常需要 5-20 分钟完成。 ## 依赖来源 - 首选 SDK 来源：npm 包 `@google/genai` - 可选兼容模式：通过 `GOOGLE_GENAI_SDK_PATH` 显式指定一个本地 SDK 路径 不要依赖某台开发机上的隐式 bundled SDK 路径，也不要在技能里写死个人机器目录。 ## 工作流程 ### 1. 确认研究主题 - 与用户确认研究主题和范围 - 如果话题模糊，帮助用户明确研究方向和关注点 - 将用户需求转化为清晰的英文或中文研究查询 ### 2. 准备 API Key 必须显式提供： ```bash export GEMINI_API_KEY="<api_key>" ``` 不要自动读取本地 `openclaw.json` 或扫描多个默认配置位置。发布型技能应让密钥来源保持明确、可审查、可控。 ### 3. 执行研究 **提醒用户**：深度研究通常需要 5-20 分钟，请耐心等待。 脚本与 `SKILL.md` 位于同一目录。执行时先把脚本路径解析为当前技能目录下的 `deep-research.mjs`，不要写死开发机绝对路径。 运行脚本： ```bash GEMINI_API_KEY="<api_key>" node <skill-dir>/deep-research.mjs "<研究主题>" ``` 可选参数： - `--timeout <seconds>` — 超时时间，默认 3600 秒（1 小时） - `--no-stream` — 使用轮询模式（如果流式出错可尝试） 可选 SDK 路径覆盖： ```bash GOOGLE_GENAI_SDK_PATH="/path/to/@google/genai/dist/node/index.mjs" \ GEMINI_API_KEY="<api_key>" \ node <skill-dir>/deep-research.mjs "<研究主题>" ``` **重要**：由于研究耗时较长，使用 exec 工具时设置足够的超时时间（至少 1200 秒）。 ### 4. 后续追问 研究完成后，脚本会输出 Interaction ID。如果用户需要追问或深入某个方面： ```bash GEMINI_API_KEY="<api_key>" node <skill-dir>/deep-research.mjs --follow-up <interaction_id> "<追加问题>" ``` ### 5. 呈现结果 - stdout 输出为 Markdown 格式的研究报告 - 保持原始 Markdown 格式呈现给用户 - 报告包含引用来源链接 - 如果用户需要，可以将报告保存为文件 ## 输出说明 - **stdout** — 最终研究报告（Markdown 格式） - **stderr** — 进度信息： - 🔬 研究开始 - 📊 状态更新 - 💭 思考摘要（Agent 的中间思考过程） - ✅ 研究完成 + 耗时 - 📎 Interaction ID（用于后续追问） ## 错误处理 | 退出码 | 含义 | 处理方式 | |--------|------|----------| | 0 | 成功 | 正常呈现报告 | | 1 | 参数错误 | 检查研究主题是否提供 | | 2 | 环境或 API 错误 | 检查 `GEMINI_API_KEY`、`@google/genai` 安装和配额 | | 3 | 超时 | 建议缩小研究范围或增加超时 | ## 安全 - 不要将 API Key 直接展示给用户 - 使用环境变量传递 API Key，不要把真实密钥写进技能文件 - 不要在未获用户确认前读取本地配置中的潜在密钥

path: deep-research.mjs
content: #!/usr/bin/env node

/**
 * deep-research.mjs — Gemini Deep Research helper script
 *
 * Runs the Gemini Interactions API with the deep-research agent and streams
 * research progress to stderr, final Markdown report to stdout.
 *
 * Distribution rules:
 * - Requires GEMINI_API_KEY from the environment
 * - Uses @google/genai from npm by default
 * - Optionally accepts an explicit GOOGLE_GENAI_SDK_PATH override
 * - Does NOT auto-read local OpenClaw config files
 *
 * Usage:
 *   node deep-research.mjs "research topic"
 *   node deep-research.mjs "research topic" --timeout 1800
 *   node deep-research.mjs --follow-up <interaction_id> "follow-up question"
 *   node deep-research.mjs "research topic" --no-stream
 *
 * Exit codes: 0 = success, 1 = arg error, 2 = setup/API error, 3 = timeout
 */

import { pathToFileURL } from 'node:url';
import path from 'node:path';

// ─── SDK resolution ────────────────────────────────────────────────────────
async function loadGoogleGenAI() {
  const candidates = [process.env.GOOGLE_GENAI_SDK_PATH?.trim(), '@google/genai'].filter(Boolean);
  const errors = [];

  for (const candidate of candidates) {
    try {
      const specifier =
        candidate.startsWith('/') || candidate.startsWith('.')
          ? pathToFileURL(path.resolve(candidate)).href
          : candidate;
      const mod = await import(specifier);
      if (mod?.GoogleGenAI) {
        return mod.GoogleGenAI;
      }
      errors.push(`${candidate}: GoogleGenAI export not found`);
    } catch (err) {
      errors.push(`${candidate}: ${err.message}`);
    }
  }

  throw new Error(
    `Unable to load @google/genai. Install it from npm, or set GOOGLE_GENAI_SDK_PATH explicitly. Tried: ${candidates.join(', ')}${
      errors.length ? `\n${errors.join('\n')}` : ''
    }`,
  );
}

const GoogleGenAI = await loadGoogleGenAI();

// ─── Argument parsing ───────────────────────────────────────────────────────
const argv = process.argv.slice(2);
let query = null;
let timeout = 3600;
let followUpId = null;
let useStream = true;

for (let i = 0; i < argv.length; i++) {
  switch (argv[i]) {
    case '--timeout':
      timeout = parseInt(argv[++i], 10);
      if (Number.isNaN(timeout) || timeout <= 0) {
        console.error('Error: --timeout must be a positive integer (seconds).');
        process.exit(1);
      }
      break;
    case '--follow-up':
      followUpId = argv[++i];
      if (!followUpId) {
        console.error('Error: --follow-up requires an interaction ID.');
        process.exit(1);
      }
      break;
    case '--no-stream':
      useStream = false;
      break;
    case '--help':
      printUsage();
      process.exit(0);
      break;
    default:
      if (!query && !argv[i].startsWith('-')) {
        query = argv[i];
      }
      break;
  }
}

function printUsage() {
  console.error(
    `Usage: node deep-research.mjs "research topic" [options]

Options:
  --timeout <seconds>           Max wait time (default: 3600)
  --follow-up <interaction_id>  Continue a previous research session
  --no-stream                   Use polling instead of streaming
  --help                        Show this help

Environment:
  GEMINI_API_KEY                Required Gemini API key
  GOOGLE_GENAI_SDK_PATH         Optional explicit @google/genai SDK path

Exit codes:
  0  Success
  1  Argument error
  2  Setup or API error
  3  Timeout`,
  );
}

if (!query) {
  console.error('Error: Research topic is required.\n');
  printUsage();
  process.exit(1);
}

const [REDACTED_SECRET]);
if (!apiKey) {
  console.error('Error: GEMINI_API_KEY environment variable is required.');
  process.exit(2);
}

// ─── Initialize ─────────────────────────────────────────────────────────────
const client = new GoogleGenAI({ apiKey });
const startTime = Date.now();

const timeoutId = setTimeout(() => {
  console.error(`\n⏰ Research timed out after ${timeout}s.`);
  process.exit(3);
}, timeout * 1000);

// ─── Build request ──────────────────────────────────────────────────────────
console.error(`🔬 Starting deep research: "${query}"`);
if (followUpId) {
  console.error(`📎 Following up on interaction: ${followUpId}`);
}

const params = {
  agent: 'deep-research-pro-preview-12-2025',
  input: query,
  background: true,
  stream: useStream,
  agent_config: {
    type: 'deep-research',
    thinking_summaries: 'auto',
  },
  ...(followUpId && { previous_interaction_id: followUpId }),
};

// ─── Execute ────────────────────────────────────────────────────────────────
try {
  if (useStream) {
    await runStreaming(params);
  } else {
    await runPolling(params);
  }
} catch (err) {
  console.error(`\n❌ API Error: ${err.message}`);
  if (err.status) console.error(`   HTTP Status: ${err.status}`);
  process.exit(2);
} finally {
  clearTimeout(timeoutId);
}

// ─── Streaming mode ─────────────────────────────────────────────────────────
async function runStreaming(params) {
  const stream = await client.interactions.create(params);
  let interactionId = null;

  for await (const event of stream) {
    switch (event.event_type) {
      case 'interaction.start':
        interactionId = event.interaction?.id;
        console.error(`📊 Research started (ID: ${interactionId})`);
        break;

      case 'interaction.status_update':
        console.error(`📊 Status: ${event.status}`);
        break;

      case 'content.delta':
        handleDelta(event);
        break;

      case 'content.start':
      case 'content.stop':
        break;

      case 'interaction.complete': {
        const elapsed = formatElapsed(Date.now() - startTime);
        console.error(`\n✅ Research complete (${elapsed})`);
        if (interactionId) {
          console.error(`📎 Interaction ID: ${interactionId}`);
        }
        break;
      }

      case 'error':
        console.error(`\n❌ Error event: ${JSON.stringify(event)}`);
        break;
    }
  }
}

function handleDelta(event) {
  const delta = event.delta;
  if (!delta) return;

  if (delta.type === 'text') {
    process.stdout.write(delta.text || '');
  } else if (delta.type === 'thought_summary') {
    const text = delta.content?.text || '';
    if (text) {
      console.error(`💭 ${text}`);
    }
  }
}

// ─── Polling mode ───────────────────────────────────────────────────────────
async function runPolling(params) {
  const interaction = await client.interactions.create({
    ...params,
    stream: false,
  });
  const id = interaction.id;
  console.error(`📊 Research created (ID: ${id})`);

  const POLL_INTERVAL = 15_000;

  while (true) {
    await sleep(POLL_INTERVAL);

    const updated = await client.interactions.get(id);
    const status = updated.status;
    const elapsed = formatElapsed(Date.now() - startTime);
    console.error(`📊 Status: ${status} (${elapsed})`);

    if (status === 'completed') {
      outputInteractionContent(updated);
      console.error(`\n✅ Research complete (${elapsed})`);
      console.error(`📎 Interaction ID: ${id}`);
      return;
    }

    if (status === 'failed' || status === 'cancelled' || status === 'incomplete') {
      console.error(`\n❌ Research ${status}.`);
      process.exit(2);
    }
  }
}

function outputInteractionContent(interaction) {
  const output = interaction.output || interaction.outputs || [];
  for (const item of Array.isArray(output) ? output : [output]) {
    if (typeof item === 'string') {
      process.stdout.write(item);
    } else if (item?.text) {
      process.stdout.write(item.text);
    } else if (item?.content) {
      const parts = Array.isArray(item.content) ? item.content : [item.content];
      for (const part of parts) {
        if (part?.text) process.stdout.write(part.text);
      }
    }
  }
}

// ─── Utilities ──────────────────────────────────────────────────────────────
function formatElapsed(ms) {
  const totalSec = Math.floor(ms / 1000);
  const m = Math.floor(totalSec / 60);
  const s = totalSec % 60;
  return m > 0 ? `${m}m ${s}s` : `${s}s`;
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}


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