Skill: diomedeak/morning-radar (version 1.0.1)

# Morning Radar - 晨间资讯简报 通用资讯自动收集与推送工具，支持自定义关键词和主题 ## 功能 - 使用百度AI搜索获取最新资讯 - 自定义关键词和搜索主题 - 自动过滤、去重、排序 - 生成Markdown格式简报 - 定时推送到飞书 ## 安装 ```bash # 1. 复制skill到OpenClaw skills目录 cp -r skills/morning-radar ~/.openclaw/skills/ # 2. 配置环境变量 export BAIDU_API_KEY="your-baidu-api-key" export FEISHU_APP_ID="your-feishu-app-id" export FEISHU_APP_SECRET="your-feishu-app-secret" export FEISHU_RECEIVER_OPEN_ID="your-open-id" # 3. （可选）自定义搜索主题 export RADAR_QUERIES="AI人工智能,科技新闻,行业动态" export RADAR_KEYWORDS="大模型,科技,创新" # 4. 设置定时任务 openclaw cron add --name "morning-radar" --schedule "0 7 * * *" --command "morning-radar" ``` ## 配置 ### 方式1：环境变量（推荐） ```bash # 必需 export BAIDU_API_KEY="your-baidu-api-key" export FEISHU_APP_ID="cli-xxxxx" export FEISHU_APP_SECRET="your-secret" export FEISHU_RECEIVER_OPEN_ID="ou-xxxxx" # 可选 - 自定义搜索 export RADAR_QUERIES="AI人工智能,科技新闻,行业动态,财经资讯" export RADAR_KEYWORDS="大模型,科技,创新,投资,市场" export RADAR_MAX_RESULTS="10" export RADAR_MAX_ITEMS="15" # 可选 - 自定义标题 export RADAR_TITLE="🌅 我的晨间资讯简报" export RADAR_SOURCE_NAME="自定义搜索" ``` ### 方式2：配置文件 复制 `config.example.json` 为 `config.json`： ```json { "baidu": { "apiKey": "your-baidu-api-key" }, "feishu": { "appId": "cli-xxxxx", "appSecret": "your-secret", "receiverOpenId": "ou-xxxxx" }, "search": { "queries": ["AI人工智能", "科技新闻", "行业动态"], "keywords": ["大模型", "科技", "创新"], "maxResults": 10 }, "output": { "title": "🌅 我的晨间资讯简报", "sourceName": "自定义搜索", "maxItems": 15 } } ``` ## 使用场景示例 ### 场景1：AI资讯（默认） ```bash export RADAR_QUERIES="AI人工智能,大模型,智能体" export RADAR_KEYWORDS="AI,大模型,Agent,OpenAI,GPT" node index.js ``` ### 场景2：财经资讯 ```bash export RADAR_QUERIES="股市行情,投资理财,宏观经济" export RADAR_KEYWORDS="A股,基金,投资,理财,市场" export RADAR_TITLE="📈 晨间财经简报" node index.js ``` ### 场景3：科技资讯 ```bash export RADAR_QUERIES="科技新闻,互联网,新产品发布" export RADAR_KEYWORDS="科技,互联网,产品,创新,发布" export RADAR_TITLE="🔬 晨间科技简报" node index.js ``` ### 场景4：综合资讯 ```bash export RADAR_QUERIES="今日热点,社会新闻,国际动态" export RADAR_KEYWORDS="热点,新闻,社会,国际" export RADAR_TITLE="📰 晨间综合简报" node index.js ``` ## 获取配置信息 ### 1. 百度API Key 1. 访问 [百度千帆平台](https://qianfan.baidubce.com) 2. 注册/登录百度账号 3. 创建应用，获取API Key ### 2. 飞书应用配置 1. 访问 [飞书开放平台](https://open.feishu.cn) 2. 创建企业自建应用 3. 获取 **App ID** 和 **App Secret** ### 3. 获取 Receiver Open ID **方式A：飞书管理后台** 1. 登录 [飞书管理后台](https://admin.feishu.cn) 2. 进入「组织架构」→「成员与部门」 3. 找到自己的账号，复制 **Open ID** **方式B：API查询** ```bash curl -X GET "https://open.feishu.cn/open-apis/contact/v3/users/me" \ -H "[REDACTED_SECRET]" ``` ## 使用 ### 手动运行 ```bash # 使用环境变量 morning-radar # 或使用配置文件 morning-radar /path/to/config.json ``` ### 定时运行 ```bash # 添加到cron（每天7:00） openclaw cron add morning-radar "0 7 * * *" # 查看定时任务 openclaw cron list # 删除定时任务 openclaw cron remove morning-radar ``` ## 输出格式 ``` 🌅 晨间资讯简报 - 2026/3/13 ━━━━━━━━━━━━━━━━━━━━━━ 1. 【来源】 标题：xxx 链接：https://xxx.com/xxx 日期：2026-03-13 摘要：一句话要点 优先级：🔴高 ... ━━━━━━━━━━━━━━━━━━━━━━ 💡 共 X 条资讯 ⏰ 每日早7点自动推送 ``` ## 环境变量清单 | 变量名 | 必需 | 默认值 | 说明 | |--------|------|--------|------| | `BAIDU_API_KEY` | ✅ | - | 百度API Key | | `FEISHU_APP_ID` | ✅ | - | 飞书App ID | | `FEISHU_APP_SECRET` | ✅ | - | 飞书App Secret | | `FEISHU_RECEIVER_OPEN_ID` | ✅ | - | 接收者Open ID | | `RADAR_QUERIES` | ❌ | AI相关 | 搜索查询，逗号分隔 | | `RADAR_KEYWORDS` | ❌ | AI关键词 | 过滤关键词，逗号分隔 | | `RADAR_MAX_RESULTS` | ❌ | 10 | 每次搜索最大结果数 | | `RADAR_MAX_ITEMS` | ❌ | 15 | 最终输出最大条数 | | `RADAR_TITLE` | ❌ | 晨间资讯简报 | 消息标题 | | `RADAR_SOURCE_NAME` | ❌ | 百度智能搜索 | 来源名称 | ## 依赖 - Node.js 18+ - OpenClaw 2025+ - 百度千帆AI搜索API Key - 飞书开放平台应用 ## 隐私说明 本Skill不会收集或存储任何用户数据。所有配置信息仅保存在用户本地。 ## 许可证 MIT

path: config.example.json
content: {
  "baidu": {
    "apiKey": "${BAIDU_API_KEY}",
    "apiUrl": "https://qianfan.baidubce.com/v2/ai_search/web_search"
  },
  "feishu": {
    "appId": "${FEISHU_APP_ID}",
    "appSecret": "${FEISHU_APP_SECRET}",
    "receiverOpenId": "${FEISHU_RECEIVER_OPEN_ID}"
  },
  "search": {
    "queries": ["${RADAR_QUERIES}"],
    "keywords": ["${RADAR_KEYWORDS}"],
    "maxResults": "${RADAR_MAX_RESULTS}"
  },
  "output": {
    "title": "${RADAR_TITLE}",
    "sourceName": "${RADAR_SOURCE_NAME}",
    "maxItems": "${RADAR_MAX_ITEMS}"
  }
}


path: index.js
content: #!/usr/bin/env node
/**
 * Morning Radar - 晨间资讯简报
 * 通用资讯自动收集与推送工具
 * 
 * 使用: node index.js [configPath]
 * 
 * 环境变量:
 *   BAIDU_API_KEY - 百度API Key
 *   FEISHU_APP_ID - 飞书App ID
 *   FEISHU_APP_SECRET - 飞书App Secret
 *   FEISHU_RECEIVER_OPEN_ID - 飞书接收者Open ID
 *   RADAR_QUERIES - 搜索查询（逗号分隔）
 *   RADAR_KEYWORDS - 过滤关键词（逗号分隔）
 *   RADAR_MAX_RESULTS - 每次搜索最大结果数（默认10）
 *   RADAR_MAX_ITEMS - 最终输出最大条数（默认15）
 *   RADAR_TITLE - 简报标题
 *   RADAR_SOURCE_NAME - 来源名称
 */

import { readFileSync, existsSync } from 'fs';
import { baiduSearch } from './lib/search.js';
import { parseResults, formatMessage } from './lib/format.js';
import { pushToFeishu } from './lib/push.js';

const CONFIG_PATH = process.argv[2] || './config.json';

// 默认配置
const DEFAULT_CONFIG = {
  search: {
    queries: ["AI人工智能", "大模型", "智能体"],
    keywords: ["AI", "大模型", "Agent", "人工智能"],
    maxResults: 10
  },
  output: {
    title: "🌅 晨间资讯简报",
    sourceName: "百度智能搜索",
    maxItems: 15
  }
};

function parseEnvList(value, defaultList) {
  if (!value) return defaultList;
  return value.split(',').map(s => s.trim()).filter(s => s);
}

function loadConfig() {
  // 从环境变量读取配置
  const envConfig = {
    baidu: {
      [REDACTED_SECRET],
      apiUrl: "https://qianfan.baidubce.com/v2/ai_search/web_search"
    },
    feishu: {
      appId: process.env.FEISHU_APP_ID,
      appSecret: process.env.FEISHU_APP_SECRET,
      receiverOpenId: process.env.FEISHU_RECEIVER_OPEN_ID
    },
    search: {
      queries: parseEnvList(process.env.RADAR_QUERIES, DEFAULT_CONFIG.search.queries),
      keywords: parseEnvList(process.env.RADAR_KEYWORDS, DEFAULT_CONFIG.search.keywords),
      maxResults: parseInt(process.env.RADAR_MAX_RESULTS) || DEFAULT_CONFIG.search.maxResults
    },
    output: {
      title: process.env.RADAR_TITLE || DEFAULT_CONFIG.output.title,
      sourceName: process.env.RADAR_SOURCE_NAME || DEFAULT_CONFIG.output.sourceName,
      maxItems: parseInt(process.env.RADAR_MAX_ITEMS) || DEFAULT_CONFIG.output.maxItems
    }
  };
  
  // 检查环境变量是否完整
  const hasEnvConfig = envConfig.baidu.apiKey && 
                       envConfig.feishu.appId && 
                       envConfig.feishu.appSecret && 
                       envConfig.feishu.receiverOpenId;
  
  if (hasEnvConfig) {
    console.log("✓ 使用环境变量配置");
    return envConfig;
  }
  
  // 尝试从配置文件读取
  if (existsSync(CONFIG_PATH)) {
    try {
      console.log(`✓ 使用配置文件: ${CONFIG_PATH}`);
      const content = readFileSync(CONFIG_PATH, 'utf8');
      const fileConfig = JSON.parse(content);
      
      // 合并配置
      return {
        baidu: { ...envConfig.baidu, ...fileConfig.baidu },
        feishu: { ...envConfig.feishu, ...fileConfig.feishu },
        search: { ...DEFAULT_CONFIG.search, ...fileConfig.search },
        output: { ...DEFAULT_CONFIG.output, ...fileConfig.output }
      };
    } catch (e) {
      console.error(`❌ 加载配置文件失败: ${e.message}`);
    }
  }
  
  console.error("❌ 错误: 未找到配置");
  console.error("   请设置环境变量，或创建 config.json 文件");
  console.error("   详见 SKILL.md");
  process.exit(1);
}

async function main() {
  console.log("🌅 晨间雷达启动...\n");
  
  const config = loadConfig();
  
  // 验证配置
  if (!config.baidu?.apiKey) {
    console.error("❌ 错误: 未配置百度API Key");
    process.exit(1);
  }
  if (!config.feishu?.appId || !config.feishu?.appSecret) {
    console.error("❌ 错误: 未配置飞书应用信息");
    process.exit(1);
  }
  if (!config.feishu?.receiverOpenId) {
    console.error("❌ 错误: 未配置飞书接收者Open ID");
    process.exit(1);
  }
  
  console.log(`📋 搜索主题: ${config.search.queries.join(', ')}`);
  console.log(`🔑 过滤关键词: ${config.search.keywords.join(', ')}\n`);
  
  const allResults = [];
  
  // 执行搜索
  for (const query of config.search.queries) {
    console.log(`🔍 搜索: ${query}...`);
    const results = await baiduSearch(config.baidu.apiKey, query, config.search.maxResults);
    console.log(`   ✓ 获取 ${results.length} 条结果`);
    
    const articles = parseResults(results, config.search.keywords);
    allResults.push(...articles);
  }
  
  // 去重并限制数量
  const seen = new Set();
  const uniqueResults = allResults.filter(item => {
    const key = item.title.toLowerCase();
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  }).slice(0, config.output.maxItems);
  
  // 排序
  uniqueResults.sort((a, b) => {
    const order = { "高": 3, "中": 2, "低": 1 };
    return order[b.priority] - order[a.priority];
  });
  
  console.log(`\n✅ 共 ${uniqueResults.length} 条相关资讯`);
  
  // 格式化消息
  const message = formatMessage(uniqueResults, config.output);
  
  // 推送到飞书
  console.log("📤 推送到飞书...");
  await pushToFeishu(config.feishu, message);
  
  console.log("✅ 完成！");
}

main().catch(e => {
  console.error(`\n❌ 错误: ${e.message}`);
  process.exit(1);
});


path: package.json
content: {
  "name": "morning-radar",
  "version": "1.0.0",
  "description": "晨间AI资讯自动收集与推送工具",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node index.js",
    "test": "node test.js"
  },
  "keywords": [
    "openclaw",
    "skill",
    "ai",
    "news",
    "feishu",
    "baidu"
  ],
  "author": "",
  "license": "MIT",
  "dependencies": {},
  "engines": {
    "node": ">=18.0.0"
  }
}


path: skill.json
content: {
  "name": "morning-radar",
  "version": "1.0.0",
  "description": "通用资讯自动收集与推送工具，支持百度AI搜索和飞书推送",
  "author": "",
  "license": "MIT",
  "type": "skill",
  "entry": "index.js",
  "runtime": "node",
  "requires": {
    "node": ">=18.0.0",
    "openclaw": ">=2025.0.0"
  },
  "env": {
    "required": [
      {
        "name": "BAIDU_API_KEY",
        "description": "百度千帆AI搜索API Key",
        "example": "bce-v3/ALTAK-xxxxx"
      },
      {
        "name": "FEISHU_APP_ID",
        "description": "飞书应用App ID",
        "example": "cli_xxxxxxxxxx"
      },
      {
        "name": "FEISHU_APP_SECRET",
        "description": "飞书应用App Secret",
        "example": "xxxxxxxxxx"
      },
      {
        "name": "FEISHU_RECEIVER_OPEN_ID",
        "description": "飞书接收者Open ID",
        "example": "ou_xxxxxxxxxx"
      }
    ],
    "optional": [
      {
        "name": "RADAR_QUERIES",
        "description": "搜索查询主题，逗号分隔",
        "default": "AI人工智能,大模型,智能体",
        "example": "科技新闻,财经资讯,行业动态"
      },
      {
        "name": "RADAR_KEYWORDS",
        "description": "过滤关键词，逗号分隔",
        "default": "AI,大模型,Agent,人工智能",
        "example": "科技,投资,市场,创新"
      },
      {
        "name": "RADAR_TITLE",
        "description": "简报标题",
        "default": "🌅 晨间资讯简报",
        "example": "📈 财经晨间简报"
      },
      {
        "name": "RADAR_SOURCE_NAME",
        "description": "来源名称",
        "default": "百度智能搜索",
        "example": "综合搜索"
      },
      {
        "name": "RADAR_MAX_RESULTS",
        "description": "每次搜索最大结果数",
        "default": "10",
        "example": "15"
      },
      {
        "name": "RADAR_MAX_ITEMS",
        "description": "最终输出最大条数",
        "default": "15",
        "example": "20"
      }
    ]
  },
  "config": {
    "files": ["config.json"],
    "optional": true
  },
  "permissions": {
    "network": ["qianfan.baidubce.com", "open.feishu.cn"],
    "filesystem": "read-only"
  },
  "install": {
    "steps": [
      "复制skill到OpenClaw skills目录",
      "配置必需的环境变量（BAIDU_API_KEY, FEISHU_APP_ID, FEISHU_APP_SECRET, FEISHU_RECEIVER_OPEN_ID）",
      "（可选）配置搜索主题和关键词环境变量",
      "（可选）设置定时任务"
    ],
    "note": "需要百度千帆AI搜索API Key和飞书开放平台应用"
  },
  "usage": {
    "command": "morning-radar [configPath]",
    "examples": [
      {
        "command": "morning-radar",
        "description": "使用环境变量配置运行"
      },
      {
        "command": "morning-radar ./my-config.json",
        "description": "使用指定配置文件运行"
      }
    ]
  },
  "cron": {
    "supported": true,
    "example": "0 7 * * *",
    "description": "建议每天早上7点运行"
  },
  "tags": ["news", "search", "feishu", "baidu", "push", "daily"],
  "homepage": "",
  "repository": ""
}


path: lib/format.js
content: /**
 * 格式化模块
 */

export function checkRelevance(title, content, keywords) {
  const text = `${title} ${content}`.toLowerCase();
  for (const keyword of keywords) {
    if (text.includes(keyword.toLowerCase())) return true;
  }
  return false;
}

export function assessPriority(title, content) {
  const text = `${title} ${content}`.toLowerCase();
  const highKeywords = ["发布", "推出", "融资", "收购", "重大突破", "上市", "合作"];
  for (const kw of highKeywords) {
    if (text.includes(kw)) return "高";
  }
  const mediumKeywords = ["更新", "升级", "应用", "案例", "报告", "分析"];
  for (const kw of mediumKeywords) {
    if (text.includes(kw)) return "中";
  }
  return "低";
}

export function parseResults(results, keywords) {
  const articles = [];
  const today = new Date().toISOString().split('T')[0];
  const seen = new Set();
  
  for (const item of results) {
    if (seen.has(item.url) || item.title.length < 10) continue;
    seen.add(item.url);
    
    if (!checkRelevance(item.title, item.snippet, keywords)) continue;
    
    let source = "";
    try {
      source = new URL(item.url).hostname.replace(/^www\./, '');
    } catch (e) {
      source = "未知来源";
    }
    
    articles.push({
      title: item.title,
      source,
      url: item.url,
      date: item.date || today,
      summary: truncate(item.snippet, 50) || "待验证",
      priority: assessPriority(item.title, item.snippet)
    });
  }
  
  return articles;
}

export function formatMessage(items, outputConfig) {
  const today = new Date().toLocaleDateString('zh-CN');
  const title = outputConfig?.title || "🌅 晨间资讯简报";
  
  let msg = `${title} - ${today}\n`;
  msg += `━━━━━━━━━━━━━━━━━━━━━━\n\n`;
  
  if (items.length === 0) {
    msg += `⚠️ 今日暂无匹配资讯\n\n`;
  } else {
    items.forEach((item, i) => {
      const priority = item.priority === "高" ? "🔴高" : item.priority === "中" ? "🟡中" : "🟢低";
      msg += `${i + 1}. 【${item.source}】\n`;
      msg += `标题：${item.title}\n`;
      msg += `链接：${item.url}\n`;
      msg += `日期：${item.date}\n`;
      msg += `摘要：${item.summary}\n`;
      msg += `优先级：${priority}\n\n`;
    });
  }
  
  msg += `━━━━━━━━━━━━━━━━━━━━━━\n`;
  msg += `💡 共 ${items.length} 条资讯\n`;
  msg += `⏰ 每日早7点自动推送\n`;
  
  return msg;
}

function truncate(str, maxLen) {
  if (!str) return "";
  if (str.length <= maxLen) return str;
  return str.substring(0, maxLen - 2) + "..";
}


path: lib/push.js
content: /**
 * 飞书推送模块
 */

export async function getTenantToken(appId, appSecret) {
  const response = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ app_id: appId, app_secret: appSecret })
  });
  
  const data = await response.json();
  if (data.code !== 0) {
    throw new Error(`获取token失败: ${data.msg}`);
  }
  return data.tenant_access_token;
}

export async function sendMessage(token, openId, message) {
  const response = await fetch("https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      receive_id: openId,
      msg_type: "text",
      content: JSON.stringify({ text: message })
    })
  });
  
  const data = await response.json();
  if (data.code !== 0) {
    throw new Error(`发送消息失败: ${data.msg}`);
  }
  return data;
}

export async function pushToFeishu(config, message) {
  const token = await getTenantToken(config.appId, config.appSecret);
  await sendMessage(token, config.receiverOpenId, message);
}


path: lib/search.js
content: /**
 * 百度搜索模块
 */

export async function baiduSearch(apiKey, query, count = 10) {
  const url = "https://qianfan.baidubce.com/v2/ai_search/web_search";
  
  try {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        messages: [{ role: "user", content: query }],
        search_source: "baidu_search_v2",
        resource_type_filter: [{ type: "web", top_k: count }],
        search_recency_filter: "week"
      })
    });
    
    if (!response.ok) {
      throw new Error(`API错误: ${response.status}`);
    }
    
    const data = await response.json();
    
    const results = [];
    if (data.references) {
      for (const item of data.references) {
        results.push({
          title: item.title || "",
          url: item.url || "",
          snippet: item.content || item.snippet || "",
          date: item.date || ""
        });
      }
    }
    
    return results;
  } catch (e) {
    console.error(`搜索失败: ${e.message}`);
    return [];
  }
}


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