#!/usr/bin/env node 'use strict'; /** * 把 bridge 的工具 manifest 注册到 LibreDesk(Admin → AI → Tools)。 * * 鉴权二选一: * 1. .env 配了 LIBREDESK_API_KEY/SECRET → 用 API key * 2. 否则用 System 账号登录(LIBREDESK_SYSTEM_USER_PASSWORD)拿 session + CSRF * * 用法:node scripts/register-tools.js */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); function loadEnv(file) { const out = {}; for (const line of fs.readFileSync(file, 'utf8').split('\n')) { const t = line.trim(); if (!t || t.startsWith('#')) continue; const eq = t.indexOf('='); if (eq < 0) continue; let v = t.slice(eq + 1).trim(); if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1); out[t.slice(0, eq).trim()] = v; } return out; } function createClient(baseURL) { const cookies = new Map(); let csrf = ''; let authHeader = ''; const cookieHeader = () => [...cookies.entries()].map(([k, v]) => `${k}=${v}`).join('; '); function store(res) { const list = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : [res.headers.get('set-cookie')].filter(Boolean); for (const c of list) { const [pair] = c.split(';'); const eq = pair.indexOf('='); if (eq > 0) { const k = pair.slice(0, eq).trim(); const v = pair.slice(eq + 1).trim(); cookies.set(k, v); if (k === 'csrf_token') csrf = v; } } } async function req(method, p, body) { const headers = { Accept: 'application/json' }; if (authHeader) headers.Authorization = authHeader; else if (cookies.size) headers.Cookie = cookieHeader(); if (body !== undefined) headers['Content-Type'] = 'application/json'; if (!authHeader && csrf && method !== 'GET') headers['X-CSRFTOKEN'] = csrf; const res = await fetch(baseURL + p, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) }); store(res); const text = await res.text(); let json = null; try { json = text ? JSON.parse(text) : null; } catch { /* ignore */ } if (!res.ok) throw new Error(`${method} ${p} -> HTTP ${res.status}: ${(json && json.message) || text.slice(0, 300)}`); return json; } return { setAuth: (h) => { authHeader = h; }, login: (email, password) => req('POST', '/api/v1/auth/login', { email, password }), get: (p) => req('GET', p), post: (p, b) => req('POST', p, b), put: (p, b) => req('PUT', p, b), }; } async function main() { const env = loadEnv(path.join(ROOT, '.env')); const base = (env.LIBREDESK_BASE_URL || 'http://192.168.31.61:9000').replace(/\/+$/, ''); const toolsKey = env.TOOLS_SHARED_KEY; if (!toolsKey) throw new Error('TOOLS_SHARED_KEY 未配置'); const c = createClient(base); if (env.LIBREDESK_API_KEY && env.LIBREDESK_API_SECRET) { c.setAuth(`token ${env.LIBREDESK_API_KEY}:${env.LIBREDESK_API_SECRET}`); console.log('使用 API key 鉴权'); } else { await c.login(env.LIBREDESK_SYSTEM_EMAIL || 'System', env.LIBREDESK_SYSTEM_USER_PASSWORD); console.log('使用 System 会话鉴权'); } const manifestRes = await fetch('http://127.0.0.1:4100/tools/manifest', { headers: { 'X-Tools-Key': toolsKey } }); if (!manifestRes.ok) throw new Error(`bridge manifest 不可用(HTTP ${manifestRes.status}),先启动全栈`); const manifest = await manifestRes.json(); const listJson = await c.get('/api/v1/ai/tools'); const existing = Array.isArray(listJson) ? listJson : (listJson.data || []); const byName = new Map(existing.map((t) => [t.name, t])); let failed = 0; for (const tool of manifest.tools) { const body = { name: tool.name, description: tool.description, url: tool.url, // http://bridge:4100/tools/(容器网络内可达) method: 'POST', auth: { headers: [{ key: 'X-Tools-Key', value: toolsKey }] }, parameters: tool.parameters, enabled: true, copilot_enabled: true, generate_reply_enabled: true, requires_agent_approval: false, requires_verification: Boolean(tool.requires_verification), }; const current = byName.get(tool.name); try { if (current) { await c.put(`/api/v1/ai/tools/${current.id}`, body); console.log(` ✓ ${tool.name} (updated)`); } else { await c.post('/api/v1/ai/tools', body); console.log(` ✓ ${tool.name} (created)`); } } catch (err) { failed += 1; console.error(` ✗ ${tool.name}: ${err.message}`); } } if (failed) { console.error('\n部分工具注册失败,回退到 Admin → AI → Tools 手工创建。'); process.exitCode = 1; } } main().catch((err) => { console.error('[register-tools]', err.message); process.exit(1); });