bota
bota.chat ai wrapper unlimited, can read file, tts, transcript, web search, up-to-date
11 Oktober 20261252 file
bota.jsjavascript · 1097 baris
#!/usr/bin/env node/** * Bota.chat API Wrapper (ESM) * Fully automatic anonymous auth → chat → stream response * Output: clean JSON with important fields only * * Usage: * node bota.js "pesan kamu" * node bota.js "pesan" --model default-model --web-search * node bota.js --file path/to/doc.md "jelaskan file ini" * import { Bota } from './bota.js' */import { readFileSync, writeFileSync, existsSync } from 'node:fs';import { basename, extname } from 'node:path';import { fileURLToPath } from 'node:url';import { dirname, join } from 'node:path';const __dirname = dirname(fileURLToPath(import.meta.url));const BASE = 'https://bota.chat';const FRAME_ID = 'x8OA7Azs4hBy';const SITE_ID = 53;const UA = 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';// ─── Helpers ───────────────────────────────────────────────function headers(token = null, extra = {}) { const h = { Accept: 'application/json', 'Content-Type': 'application/json', Origin: BASE, Referer: `${BASE}/`, 'User-Agent': UA, 'Accept-Language': 'id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7', ...extra, }; if (token) h['x-anon-token'] = token; return h;}async function req(method, path, { body, token, formData } = {}) { const url = path.startsWith('http') ? path : `${BASE}${path}`; const opts = { method, headers: headers(token) }; if (formData) { // Let fetch set multipart boundary delete opts.headers['Content-Type']; opts.body = formData; } else if (body) { opts.body = JSON.stringify(body); } const res = await fetch(url, opts); const ct = res.headers.get('content-type') || ''; // SSE stream if (ct.includes('text/event-stream') || ct.includes('event-stream')) { return { ok: res.ok, status: res.status, stream: res.body }; } // Some proxies strip content-type; peek if body starts like SSE // But we can only read body once — so for stream endpoint force stream if ok if (url.includes('/frame/stream') && res.ok) { return { ok: true, status: res.status, stream: res.body }; } const text = await res.text(); let data = null; try { data = JSON.parse(text); } catch { data = text; } return { ok: res.ok, status: res.status, data };}// ─── SSE Parser ────────────────────────────────────────────async function parseSSE(readableStream, onEvent) { const reader = readableStream.getReader(); const decoder = new TextDecoder(); let buffer = ''; const events = []; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split('\n\n'); buffer = parts.pop() || ''; for (const part of parts) { const line = part .split('\n') .find((l) => l.startsWith('data: ')); if (!line) continue; try { const json = JSON.parse(line.slice(6)); events.push(json); if (onEvent) onEvent(json); } catch { // skip malformed } } } return events;}// ─── Core Class ────────────────────────────────────────────export class Bota { constructor(opts = {}) { this.token = opts.token || null; this.publicId = opts.publicId || null; this.chatId = opts.chatId || null; this.locale = opts.locale || 'en'; this.currency = opts.currency || 'USD'; this.model = opts.model || 'default-model'; this.verbose = opts.verbose || false; this.autoRotate = opts.autoRotate !== false; // default ON this.maxRotate = opts.maxRotate ?? Infinity; // unlimited by default this._rotateCount = 0; this._sessionFile = opts.sessionFile || join(__dirname, '.bota-session.json'); this._cookies = {}; // name -> value this._xsrf = null; } log(...args) { if (this.verbose) console.error('[bota]', ...args); } /** Load saved session if exists */ loadSession() { if (!existsSync(this._sessionFile)) return false; try { const s = JSON.parse(readFileSync(this._sessionFile, 'utf8')); this.token = s.token; this.publicId = s.publicId; this.chatId = s.chatId || null; this.log('session loaded', this.publicId || ''); return true; } catch { return false; } } /** Persist token + chat */ saveSession() { try { writeFileSync( this._sessionFile, JSON.stringify( { token: this.token, publicId: this.publicId, chatId: this.chatId, savedAt: new Date().toISOString(), }, null, 2 ) ); } catch (e) { this.log('saveSession skip:', e.message); } } _cookieHeader() { return Object.entries(this._cookies) .map(([k, v]) => `${k}=${v}`) .join('; '); } _ingestSetCookies(res) { const list = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : []; // fallback single header const single = res.headers.get('set-cookie'); const all = list.length ? list : single ? [single] : []; for (const c of all) { const [nv] = c.split(';'); const eq = nv.indexOf('='); if (eq < 0) continue; const name = nv.slice(0, eq).trim(); const val = nv.slice(eq + 1).trim(); this._cookies[name] = val; if (name === 'XSRF-TOKEN') { try { this._xsrf = decodeURIComponent(val); } catch { this._xsrf = val; } } } } /** * Bootstrap Laravel session cookies (needed for TTS / some APIs) */ async ensureBrowserSession() { if (this._xsrf && this._cookies['framesuite-session']) return; await this.ensureAuth(); if (!this.chatId) await this.ensureChat(); const url = `${BASE}/__fs/f/${FRAME_ID}?c=${this.chatId || ''}&inside=1&fs_locale=${this.locale}`; this.log('bootstrap session cookies...'); try { const res = await fetch(url, { method: 'GET', headers: { Accept: 'text/html, application/json', 'User-Agent': UA, Origin: BASE, Referer: `${BASE}/`, 'x-anon-token': this.token, }, redirect: 'manual', }); this._ingestSetCookies(res); // also set anon cookie from our token if (this.token) this._cookies['anonymous_user_token'] = this.token; this.log('cookies', Object.keys(this._cookies).join(',')); } catch (e) { this.log('session bootstrap fail:', e.message); } } /** Clear session & force new guest */ clearSession() { this.token = null; this.publicId = null; this.chatId = null; this._cookies = {}; this._xsrf = null; try { if (existsSync(this._sessionFile)) { writeFileSync(this._sessionFile, '{}'); } } catch {} } // ── Auth ──────────────────────────────────────────────── async ensureAuth() { if (this.token) { const me = await this.getMe(); if (me?.success) return me; this.log('token invalid, rotating guest...'); this.clearSession(); } return this.anonLogin(); } /** * Force new anonymous guest (rotate) */ async rotateGuest() { this._rotateCount++; // optional soft cap only if user explicitly set a finite maxRotate if (Number.isFinite(this.maxRotate) && this._rotateCount > this.maxRotate) { throw new Error(`Max guest rotate reached (${this.maxRotate})`); } this.log(`rotating guest #${this._rotateCount}...`); this.clearSession(); // jitter + backoff biar tidak kena rate-limit server const backoff = Math.min(8000, 300 + this._rotateCount * 200 + Math.random() * 500); await sleep(backoff); return this.anonLogin(); } async anonLogin() { this.log('anonymous login...'); // randomize fingerprint a bit each rotate const screens = ['360x812', '390x844', '412x915', '375x812', '414x896']; const screen = screens[Math.floor(Math.random() * screens.length)]; const langs = ['id-ID', 'en-US', 'id', 'en']; const lang = langs[Math.floor(Math.random() * langs.length)]; const { ok, data, status } = await req( 'POST', `/__fs/f/${FRAME_ID}/anon?fs_locale=${this.locale}`, { body: { screen, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Jakarta', navigator_language: lang, ui_locale: this.locale, }, } ); if (!ok || !data?.success) { throw new Error(`Anon login failed (${status}): ${JSON.stringify(data)}`); } this.token = data.anonymous_token; this.publicId = data.public_id; this.chatId = null; // always new chat after rotate this.saveSession(); this.log('got token', this.token.slice(0, 8) + '...', 'id=', this.publicId); return data; } async getMe() { const { data, status } = await req( 'GET', `/__fs/f/${FRAME_ID}/me?fs_locale=${this.locale}`, { token: this.token } ); if (status === 401 || data?.success === false) return { success: false }; return data; } // ── Chat ──────────────────────────────────────────────── async createChat() { this.log('creating chat...'); const { ok, data, status } = await req( 'POST', `/__fs/f/${FRAME_ID}/chat?fs_locale=${this.locale}`, { token: this.token, body: { site_id: SITE_ID, agent_open: false }, } ); if (!ok || !data?.success) { // auth/credit problem → rotate if (this.autoRotate && (status === 401 || status === 402 || status === 429 || status === 403)) { await this.rotateGuest(); return this.createChat(); } throw new Error(`Create chat failed (${status}): ${JSON.stringify(data)}`); } this.chatId = data.chat.hash_id; this.saveSession(); this.log('chat_id =', this.chatId); return data.chat; } async ensureChat() { if (this.chatId) return { hash_id: this.chatId }; return this.createChat(); } async listChats() { const { data } = await req('GET', `/__fs/f/${FRAME_ID}/chats?fs_locale=${this.locale}`, { token: this.token, }); return data; } /** Detect paywall / limit errors from stream or JSON body */ _isLimitError(errMsg = '', data = null) { const s = (errMsg + ' ' + JSON.stringify(data || {})).toLowerCase(); return ( s.includes('credit') || s.includes('paywall') || s.includes('limit') || s.includes('quota') || s.includes('balance') || s.includes('unauthorized') || s.includes('401') || s.includes('402') || s.includes('403') || s.includes('429') || s.includes('forbidden') || s.includes('rate') ); } // ── Upload ────────────────────────────────────────────── async upload(filePath) { if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`); await this.ensureAuth(); const buf = readFileSync(filePath); const name = basename(filePath); const form = new FormData(); const blob = new Blob([buf], { type: mimeFromExt(extname(name)) }); form.append('files[]', blob, name); this.log('uploading', name); const { ok, data, status } = await req( 'POST', `/__fs/f/${FRAME_ID}/upload?fs_locale=${this.locale}`, { token: this.token, formData: form } ); if (!ok || !data?.success) { if (this.autoRotate && this._isLimitError('', data) || status === 401) { await this.rotateGuest(); return this.upload(filePath); } throw new Error(`Upload failed (${status}): ${JSON.stringify(data)}`); } // Normalize to format expected by stream endpoint return (data.files || []).map((f) => ({ id: f.id, name: f.name, type: 'file', mime: f.mime, size: f.size, url: f.url, path: f.path, file: {}, uploading: false, })); } // ── Stream / Chat ─────────────────────────────────────── /** * Send message and collect full response * Auto-rotates guest on credit/auth failure */ async chat(message, opts = {}) { this._rotateCount = 0; // reset per top-level call return this._chatOnce(message, opts); } async _chatOnce(message, opts = {}) { await this.ensureAuth(); await this.ensureChat(); // Attachments from previous guest won't work after rotate — re-upload if needed let attachments = opts.attachments || []; const payload = { chat_id: this.chatId, locale: this.locale, paywall_currency: this.currency, message, model: opts.model || this.model, image_mode: !!opts.imageMode, video_mode: !!opts.videoMode, diagram_mode: !!opts.diagramMode, plot_mode: !!opts.plotMode, web_search: !!opts.webSearch, voice_mode: !!opts.voiceMode, hidden: false, attachments, vision_capable: opts.visionCapable !== false, tools_capable: opts.toolsCapable !== false, image_gen_capable: !!opts.imageGenCapable, from_suggestion: false, agent_open: false, }; this.log('streaming...', message.slice(0, 60)); const res = await req( 'POST', `/__fs/api/frame/stream?fs_locale=${this.locale}`, { token: this.token, body: payload } ); // Hard HTTP failure → maybe rotate if (!res.ok) { const errStr = JSON.stringify(res.data); if (this.autoRotate && this._isLimitError(errStr, res.data)) { this.log('limit/auth error, rotating...'); await this.rotateGuest(); // drop attachments (belong to old guest); caller can re-pass file path via opts._filePath if (opts._filePath) { attachments = await this.upload(opts._filePath); opts = { ...opts, attachments }; } else { opts = { ...opts, attachments: [] }; } return this._chatOnce(message, opts); } throw new Error(`Stream failed (HTTP ${res.status}): ${errStr}`); } if (!res.stream) { const errStr = JSON.stringify(res.data || {}).slice(0, 400); if (this.autoRotate && this._isLimitError(errStr, res.data)) { this.log('paywall in body, rotating...'); await this.rotateGuest(); if (opts._filePath) { opts = { ...opts, attachments: await this.upload(opts._filePath) }; } else { opts = { ...opts, attachments: [] }; } return this._chatOnce(message, opts); } throw new Error(`Stream failed (HTTP ${res.status}, no SSE): ${errStr}`); } const stream = res.stream; const result = { success: true, chat_id: this.chatId, public_id: this.publicId, message: '', message_id: null, sources: [], tools: [], reasoning: null, suggestions: [], timing: null, billing: null, model: null, credits: null, new_balance: null, rotated: this._rotateCount > 0, rotate_count: this._rotateCount, }; const chunks = []; let currentTool = null; let streamError = null; await parseSSE(stream, (ev) => { // server-side error events if (ev.error || ev.paywall || ev.limit_reached) { streamError = ev; return; } if (ev.agent_thinking) { this.log('thinking...'); } if (ev.tool_step) { const t = ev.tool_step; if (t.status === 'running' && t.args) { currentTool = { id: t.id, name: t.name, args: t.args, status: 'running', }; this.log(`tool: ${t.name}`, t.args?.query || ''); } if (t.status === 'done') { const tool = { id: t.id, name: t.name, args: currentTool?.args || t.args, status: 'done', duration_ms: t.duration_ms, sources_count: t.sources?.length || 0, }; result.tools.push(tool); if (t.sources) { for (const s of t.sources) { result.sources.push({ url: s.url, title: s.title, snippet: (s.content || '').slice(0, 300), }); } } currentTool = null; } } if (ev.agent_reasoning) { result.reasoning = ev.agent_reasoning; } if (ev.chunk) { chunks.push(ev.chunk); if (this.verbose) process.stderr.write(ev.chunk); } if (ev.done) { result.message_id = ev.message_id ?? null; result.suggestions = ev.suggestions || []; result.timing = ev.timing || null; result.billing = ev.billing || null; result.model = ev.timing?.model || null; result.credits = ev.timing?.credits ?? null; result.new_balance = ev.new_balance ?? null; if (ev.sources?.length) { result.sources = ev.sources.map((s) => ({ url: s.url, title: s.title, snippet: (s.content || s.snippet || '').slice(0, 300), })); } if (this.verbose) process.stderr.write('\n'); } }); // If stream itself signalled paywall/limit if (streamError && this.autoRotate && this._isLimitError('', streamError)) { this.log('stream paywall event, rotating...'); await this.rotateGuest(); if (opts._filePath) { opts = { ...opts, attachments: await this.upload(opts._filePath) }; } else { opts = { ...opts, attachments: [] }; } return this._chatOnce(message, opts); } result.message = chunks.join(''); // Empty response after "success" can also mean limit — rotate once if ( !result.message && !result.message_id && this.autoRotate ) { this.log('empty response, trying rotate...'); await this.rotateGuest(); if (opts._filePath) { opts = { ...opts, attachments: await this.upload(opts._filePath) }; } else { opts = { ...opts, attachments: [] }; } return this._chatOnce(message, opts); } // dedupe sources by url const seen = new Set(); result.sources = result.sources.filter((s) => { if (!s.url || seen.has(s.url)) return false; seen.add(s.url); return true; }); return result; } // ── TTS / Voice ────────────────────────────────────────── /** * Text → speech (WAV buffer) * @param {string} text * @param {{ messageId?: number|null }} [opts] * @returns {Promise<Buffer>} */ async tts(text, opts = {}) { await this.ensureAuth(); if (!this.chatId) await this.ensureChat(); await this.ensureBrowserSession(); const body = { text: String(text || '').replace(/\*\*/g, '').slice(0, 5000), chat_id: this.chatId, }; if (opts.messageId != null) body.message_id = opts.messageId; this.log('tts...', body.text.slice(0, 50)); const url = `${BASE}/__fs/api/frame/tts?fs_locale=${this.locale}`; const hdrs = { Accept: '*/*', 'Content-Type': 'application/json', Origin: BASE, Referer: `${BASE}/?c=${this.chatId}&inside=1`, 'User-Agent': UA, 'x-anon-token': this.token, }; if (this._xsrf) { hdrs['X-XSRF-TOKEN'] = this._xsrf; hdrs['X-CSRF-TOKEN'] = this._xsrf; } const cookie = this._cookieHeader(); if (cookie) hdrs['Cookie'] = cookie; const res = await fetch(url, { method: 'POST', headers: hdrs, body: JSON.stringify(body), }); this._ingestSetCookies(res); if (!res.ok) { const t = await res.text().catch(() => ''); // refresh session once then retry if (res.status === 401 || res.status === 419) { this._cookies = {}; this._xsrf = null; await this.ensureBrowserSession(); const res2 = await fetch(url, { method: 'POST', headers: { ...hdrs, 'X-XSRF-TOKEN': this._xsrf || '', 'X-CSRF-TOKEN': this._xsrf || '', Cookie: this._cookieHeader(), 'x-anon-token': this.token, }, body: JSON.stringify(body), }); if (res2.ok) { const buf2 = Buffer.from(await res2.arrayBuffer()); this.log('tts ok', buf2.length, 'bytes'); return buf2; } const t2 = await res2.text().catch(() => ''); throw new Error(`TTS failed (HTTP ${res2.status}): ${t2.slice(0, 200)}`); } throw new Error(`TTS failed (HTTP ${res.status}): ${t.slice(0, 200)}`); } const buf = Buffer.from(await res.arrayBuffer()); if (buf.length < 44) { throw new Error('TTS returned empty/invalid audio'); } this.log('tts ok', buf.length, 'bytes'); return buf; } /** * Speech (WAV/base64 or file path) * @param {string|Buffer} audio path ke .wav ATAU Buffer * @returns {Promise<string>} */ async transcribe(audio) { await this.ensureAuth(); let buf; if (typeof audio === 'string') { if (!existsSync(audio)) throw new Error(`Audio file not found: ${audio}`); buf = readFileSync(audio); } else if (Buffer.isBuffer(audio)) { buf = audio; } else { throw new Error('transcribe() butuh path string atau Buffer'); } const b64 = buf.toString('base64'); this.log('transcribe...', buf.length, 'bytes'); await this.ensureBrowserSession(); const url = `${BASE}/__fs/api/frame/transcribe?fs_locale=${this.locale}`; const hdrs = { Accept: '*/*', 'Content-Type': 'application/json', Origin: BASE, Referer: `${BASE}/?c=${this.chatId || ''}&inside=1`, 'User-Agent': UA, 'x-anon-token': this.token, }; if (this._xsrf) { hdrs['X-XSRF-TOKEN'] = this._xsrf; hdrs['X-CSRF-TOKEN'] = this._xsrf; } const cookie = this._cookieHeader(); if (cookie) hdrs['Cookie'] = cookie; const raw = await fetch(url, { method: 'POST', headers: hdrs, body: JSON.stringify({ audio: b64 }), }); this._ingestSetCookies(raw); const ct = raw.headers.get('content-type') || ''; let res; if (ct.includes('event-stream') || url.includes('transcribe')) { // always try SSE parse if body is stream-like if (ct.includes('event-stream')) { res = { ok: raw.ok, status: raw.status, stream: raw.body }; } else { const textBody = await raw.text(); // SSE-looking? if (textBody.includes('data: ')) { // fake parse const chunks = []; for (const line of textBody.split('\n')) { if (line.startsWith('data: ')) { try { const j = JSON.parse(line.slice(6)); if (j.chunk) chunks.push(j.chunk); if (j.text) chunks.push(j.text); } catch {} } } res = { ok: raw.ok, status: raw.status, data: { text: chunks.join('') } }; } else { try { res = { ok: raw.ok, status: raw.status, data: JSON.parse(textBody) }; } catch { res = { ok: raw.ok, status: raw.status, data: textBody }; } } } } else { const textBody = await raw.text(); try { res = { ok: raw.ok, status: raw.status, data: JSON.parse(textBody) }; } catch { res = { ok: raw.ok, status: raw.status, data: textBody }; } } // bisa SSE atau JSON let text = ''; if (res.stream) { const chunks = []; await parseSSE(res.stream, (ev) => { if (ev.chunk) chunks.push(ev.chunk); if (ev.text) chunks.push(ev.text); if (ev.transcript) chunks.push(ev.transcript); }); text = chunks.join(''); } else if (res.data) { if (typeof res.data === 'string') text = res.data; else text = res.data.text || res.data.transcript || res.data.message || ''; // kadang data adalah gabungan event di string if (!text && typeof res.data === 'object') { text = JSON.stringify(res.data); } } text = (text || '').trim(); this.log('transcribed:', text.slice(0, 80)); if (!text) throw new Error('Transcribe kosong / gagal'); return text; } /** * Kirim voice file → dapat balasan teks (+ opsional TTS wav) * @param {string} audioPath path .wav * @param {object} opts sama seperti chat(), plus tts: true|path */ async chatVoice(audioPath, opts = {}) { const transcript = await this.transcribe(audioPath); const result = await this.chat(transcript, { ...opts, voiceMode: true, }); result.transcript = transcript; if (opts.tts) { const outPath = typeof opts.tts === 'string' ? opts.tts : `bota-reply-${result.message_id || Date.now()}.wav`; try { const wav = await this.tts(result.message, { messageId: result.message_id }); writeFileSync(outPath, wav); result.tts_file = outPath; result.tts_bytes = wav.length; this.log('saved tts →', outPath); } catch (e) { result.tts_error = e.message; this.log('tts error:', e.message); } } return result; } /** * Chat teks + simpan balasan jadi wav */ async chatWithTts(message, opts = {}) { const result = await this.chat(message, opts); const outPath = typeof opts.tts === 'string' ? opts.tts : opts.ttsFile || `bota-reply-${result.message_id || Date.now()}.wav`; try { const wav = await this.tts(result.message, { messageId: result.message_id }); writeFileSync(outPath, wav); result.tts_file = outPath; result.tts_bytes = wav.length; } catch (e) { result.tts_error = e.message; } return result; } /** * One-shot: force fresh guest every time */ async ask(message, opts = {}) { this.clearSession(); this._rotateCount = 0; return this.chat(message, opts); }}function sleep(ms) { return new Promise((r) => setTimeout(r, ms));}// ─── Utils ─────────────────────────────────────────────────function mimeFromExt(ext) { const map = { '.md': 'text/markdown', '.txt': 'text/plain', '.pdf': 'application/pdf', '.json': 'application/json', '.html': 'text/html', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.gif': 'image/gif', }; return map[ext.toLowerCase()] || 'application/octet-stream';}// ─── CLI ───────────────────────────────────────────────────function parseArgs(argv) { const args = { message: '', model: 'default-model', webSearch: false, voiceMode: false, imageMode: false, file: null, voiceIn: null, // path audio input (.wav) tts: false, // true | path output wav newChat: false, verbose: false, session: true, help: false, }; const positional = []; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === '--help' || a === '-h') args.help = true; else if (a === '--web-search' || a === '-w') args.webSearch = true; else if (a === '--voice') args.voiceMode = true; else if (a === '--image') args.imageMode = true; else if (a === '--new' || a === '-n') args.newChat = true; else if (a === '--verbose' || a === '-v') args.verbose = true; else if (a === '--no-session') args.session = false; else if (a === '--tts') { // --tts atau --tts out.wav const next = argv[i + 1]; if (next && !next.startsWith('-')) { args.tts = argv[++i]; } else { args.tts = true; } } else if (a === '--voice-in' || a === '--mic' || a === '--audio') args.voiceIn = argv[++i]; else if (a === '--model' || a === '-m') args.model = argv[++i]; else if (a === '--file' || a === '-f') args.file = argv[++i]; else if (!a.startsWith('-')) positional.push(a); } args.message = positional.join(' ').trim(); return args;}function printHelp() { console.log(`Bota.chat CLI WrapperUsage: node bota.js "pesan kamu di sini" node bota.js "siapa presiden RI?" --web-search node bota.js --file README.md "ringkas dokumen ini" node bota.js "halo" --tts # balasan + simpan wav node bota.js "halo" --tts jawab.wav node bota.js --voice-in rekaman.wav --tts # kirim voice, dapat voiceOptions: -m, --model <name> Model (default: default-model) -w, --web-search Aktifkan web search -f, --file <path> Upload file lalu tanya --voice-in <wav> Kirim file audio (transcribe → chat) --tts [path.wav] Balasan AI di-TTS ke file wav -n, --new Buat chat baru (ignore session) -v, --verbose Log progress ke stderr --no-session Jangan simpan/load session -h, --help BantuanOutput: JSON ke stdout (hanya field penting)`);}async function main() { const args = parseArgs(process.argv.slice(2)); if (args.help || (!args.message && !args.file && !args.voiceIn)) { printHelp(); process.exit(args.help ? 0 : 1); } const client = new Bota({ verbose: args.verbose, model: args.model, }); if (args.session && !args.newChat) { client.loadSession(); } if (args.newChat) { client.chatId = null; } try { await client.ensureAuth(); let attachments = []; if (args.file) { attachments = await client.upload(args.file); if (!args.message) { args.message = `Tolong analisis / jelaskan file: ${basename(args.file)}`; } } await client.ensureChat(); let result; if (args.voiceIn) { // kirim voice → transcribe → chat → optional tts result = await client.chatVoice(args.voiceIn, { model: args.model, webSearch: args.webSearch, imageMode: args.imageMode, attachments, tts: args.tts, }); } else if (args.tts) { result = await client.chatWithTts(args.message, { model: args.model, webSearch: args.webSearch, voiceMode: args.voiceMode, imageMode: args.imageMode, attachments, _filePath: args.file || null, tts: args.tts, }); } else { result = await client.chat(args.message, { model: args.model, webSearch: args.webSearch, voiceMode: args.voiceMode, imageMode: args.imageMode, attachments, _filePath: args.file || null, }); } // Output clean JSON only const out = { success: result.success, chat_id: result.chat_id, public_id: result.public_id, message_id: result.message_id, model: result.model, message: result.message, transcript: result.transcript || undefined, tts_file: result.tts_file || undefined, tts_bytes: result.tts_bytes || undefined, tts_error: result.tts_error || undefined, sources: result.sources, tools: (result.tools || []).map((t) => ({ name: t.name, query: t.args?.query || null, duration_ms: t.duration_ms, })), rotated: result.rotated || false, rotate_count: result.rotate_count || 0, timing: result.timing ? { total_ms: result.timing.total, credits: result.timing.credits, tools: result.timing.tools, } : null, billing: result.billing ? { prompt_tokens: result.billing.prompt_tokens, completion_tokens: result.billing.completion_tokens, total_tokens: result.billing.total_tokens, cost_usd: result.billing.cost_usd, } : null, new_balance: result.new_balance, suggestions: result.suggestions, }; console.log(JSON.stringify(out, null, 2)); } catch (err) { console.log( JSON.stringify( { success: false, error: err.message, }, null, 2 ) ); process.exit(1); }}// Run CLI if executed directlyconst isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];if (isMain) { main();}README.mdmarkdown · 88 baris
Komentar
Masuk untuk ikut berkomentar.
Belum ada komentar.