#!/usr/bin/env node 'use strict'; // 匿名 Live chat:AI 问邮箱 -> 发 OTP -> 读 163 邮箱取码 -> 回码 -> 验证 -> 查订单 const { execFileSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const env = {}; for (const line of fs.readFileSync(path.join(ROOT, '.env'), 'utf8').split('\n')) { if (!line.includes('=') || line.trim().startsWith('#')) continue; const i = line.indexOf('='); env[line.slice(0, i).trim()] = line.slice(i + 1).trim(); } const BASE = (env.LIBREDESK_BASE_URL || '').replace(/\/+$/, ''); const INBOX = env.LIBREDESK_INBOX_UUID; const EMAIL = process.env.OTP_EMAIL || env.SMTP_USER; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function adminCookie() { const r = await fetch(BASE + '/api/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'System', password: env.LIBREDESK_SYSTEM_USER_PASSWORD }), }); return r.headers.getSetCookie().map((c) => c.split(';')[0]).join('; '); } async function adminMessages(cookie, uuid) { const r = await fetch(`${BASE}/api/v1/conversations/${uuid}/messages`, { headers: { Cookie: cookie } }); if (!r.ok) return []; const body = await r.json(); const msgs = (body.data && (body.data.results || body.data.messages)) || []; return msgs.slice().sort((a, b) => Number(a.id) - Number(b.id)); } async function waitReply(cookie, uuid, afterId, timeoutMs = 90000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { await sleep(3000); const msgs = await adminMessages(cookie, uuid); const replies = msgs.filter((m) => m.type === 'outgoing' && m.sender_type === 'agent' && !m.private && Number(m.id) > afterId); if (replies.length) return replies[replies.length - 1]; } return null; } async function sendWidget(uuid, token, message) { const r = await fetch(`${BASE}/api/v1/widget/chat/conversations/${uuid}/message`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, 'X-Libredesk-Inbox-ID': INBOX }, body: JSON.stringify({ message }), }); if (!r.ok) throw new Error(`widget send failed: ${r.status} ${(await r.text()).slice(0, 200)}`); } function readOtpaCode() { const py = String.raw` import json, imaplib, email, re from email.header import decode_header imaplib.Commands['ID']=('AUTH',) cfg=json.load(open('/home/dev/code/crm/crm-ai-demo/bridge/mail.config.json')) imap=cfg['imap']; auth=imap['auth'] M=imaplib.IMAP4_SSL(imap['host'], imap['port']); M.login(auth['user'],auth['pass']) M._simple_command('ID','("name" "pycrm" "version" "1.0" "vendor" "python")'); M.select('INBOX') typ,data=M.search(None,'ALL'); ids=data[0].split()[-10:] for i in reversed(ids): typ,md=M.fetch(i,'(RFC822)'); msg=email.message_from_bytes(md[0][1]) subj=str(email.header.make_header(decode_header(msg['Subject'] or ''))) if 'verification code' in subj.lower(): body='' if msg.is_multipart(): for part in msg.walk(): if part.get_content_type()=='text/plain': body=part.get_payload(decode=True).decode('utf-8','ignore'); break else: body=msg.get_payload(decode=True).decode('utf-8','ignore') m=re.search(r'\b(\d{6})\b', body) if m: print(m.group(1)); break `; return execFileSync('python3', ['-c', py], { encoding: 'utf8' }).trim(); } async function main() { const cookie = await adminCookie(); const init = await fetch(`${BASE}/api/v1/widget/chat/conversations/init`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Libredesk-Inbox-ID': INBOX }, body: JSON.stringify({ message: 'I want to check my order status' }), }); const j = await init.json(); if (!init.ok) throw new Error(`init failed: ${JSON.stringify(j).slice(0, 200)}`); const uuid = j.data.conversation.uuid; const token = j.data.session_token; console.log('1) conversation', uuid); let msgs = await adminMessages(cookie, uuid); let lastId = msgs.length ? Number(msgs[msgs.length - 1].id) : 0; const ask = await waitReply(cookie, uuid, lastId); console.log('2) AI:', ask && ask.text_content); await sendWidget(uuid, token, EMAIL); console.log('3) sent email', EMAIL); msgs = await adminMessages(cookie, uuid); lastId = Number(msgs[msgs.length - 1].id); const sent = await waitReply(cookie, uuid, lastId); console.log('4) AI:', sent && sent.text_content); const code = readOtpaCode(); if (!code) throw new Error('OTP code not found in mailbox'); console.log('5) code from mailbox', code); await sendWidget(uuid, token, code); msgs = await adminMessages(cookie, uuid); lastId = Number(msgs[msgs.length - 1].id); const final = await waitReply(cookie, uuid, lastId, 120000); console.log('6) AI:', final && final.text_content); if (!final) throw new Error('no final reply'); console.log('\nCONVERSATION', uuid); } main().catch((err) => { console.error('[e2e-anon-otp]', err.message); process.exit(1); });