#!/usr/bin/env node // Usage: node scripts/feishu-send-image.mjs // target: "user:open_id_xxx" or "oc_xxx" (chat_id) import { readFileSync, writeFileSync, existsSync } from 'fs'; import { resolve } from 'path'; import { homedir } from 'os'; const API = 'https://open.feishu.cn/open-apis'; const TOKEN_CACHE = '/tmp/feishu-token.json'; const [target, imagePath] = process.argv.slice(2); if (!target || !imagePath) { console.error('Usage: node scripts/feishu-send-image.mjs '); process.exit(1); } // --- credentials (env vars preferred, fallback to openclaw.json) --- let appId = process.env.FEISHU_APP_ID; let appSecret = process.env.FEISHU_APP_SECRET; if (!appId || !appSecret) { try { const configPath = resolve(homedir(), '.openclaw/openclaw.json'); const config = JSON.parse(readFileSync(configPath, 'utf8')); const accounts = config.channels?.feishu?.accounts ?? {}; const acctName = process.env.FEISHU_ACCOUNT || Object.keys(accounts)[0]; const acct = accounts[acctName]; appId = acct?.appId; appSecret = acct?.appSecret; } catch {} } if (!appId || !appSecret) { console.error('Missing credentials. Set FEISHU_APP_ID + FEISHU_APP_SECRET env vars, or configure ~/.openclaw/openclaw.json'); process.exit(1); } // --- tenant_access_token (cached) --- async function getToken() { if (existsSync(TOKEN_CACHE)) { try { const cached = JSON.parse(readFileSync(TOKEN_CACHE, 'utf8')); if (cached.token && cached.expiresAt > Date.now() + 60_000) return cached.token; } catch {} } const res = await fetch(`${API}/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 res.json(); if (data.code !== 0) throw new Error(`token error: ${data.msg}`); const token = data.tenant_access_token; writeFileSync(TOKEN_CACHE, JSON.stringify({ token, expiresAt: Date.now() + data.expire * 1000 })); return token; } // --- upload image --- async function uploadImage(token, filePath) { const absPath = resolve(filePath); const bytes = readFileSync(absPath); const blob = new Blob([bytes]); const form = new FormData(); form.append('image_type', 'message'); form.append('image', blob, absPath.split('/').pop()); const res = await fetch(`${API}/im/v1/images`, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: form, }); const data = await res.json(); if (data.code !== 0) throw new Error(`upload error: ${data.code} ${data.msg}`); return data.data.image_key; } // --- send message --- async function sendImage(token, target, imageKey) { let receiveIdType, receiveId; if (target.startsWith('user:')) { receiveIdType = 'open_id'; receiveId = target.slice(5); } else { receiveIdType = 'chat_id'; receiveId = target; } const res = await fetch(`${API}/im/v1/messages?receive_id_type=${receiveIdType}`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ receive_id: receiveId, msg_type: 'image', content: JSON.stringify({ image_key: imageKey }) }), }); const data = await res.json(); if (data.code !== 0) throw new Error(`send error: ${data.code} ${data.msg}`); return data.data.message_id; } // --- main --- try { const token = await getToken(); console.log('token ok'); const imageKey = await uploadImage(token, imagePath); console.log('uploaded:', imageKey); const msgId = await sendImage(token, target, imageKey); console.log('sent:', msgId); } catch (e) { console.error(e.message); process.exit(1); }