feat: add WeChat QR code login to config-server
Add in-app WeChat scanning via Tencent iLink API: - server.js: QR code PNG renderer, WeChat API proxy, plugin auto-install - index.html: WeChat channel card with QR display and polling UI - Windows-Start.bat: auto-install WeChat plugin on startup - Windows-Install.bat: copy WeChat plugin during installation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -322,6 +322,16 @@ input:focus { border-color: #ff6b35; }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-card channel-card" style="grid-column: 1 / -1; border-color: #07c160;" onclick="startWeChatLogin(event)">
|
||||
<h4>💬 微信 <span class="tag" style="background:rgba(7,193,96,0.2);color:#07c160;">个人微信</span></h4>
|
||||
<p>扫码接入个人微信,用微信聊天控制 AI</p>
|
||||
<div class="channel-form" id="form-wechat" onclick="event.stopPropagation()" style="max-height:none;overflow:visible;">
|
||||
<div id="wechat-qr" style="text-align:center;padding:16px;">
|
||||
<p style="color:#888;font-size:0.85em;">点击此卡片开始扫码</p>
|
||||
</div>
|
||||
<div id="wechat-status" style="text-align:center;color:#888;font-size:0.85em;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -519,6 +529,96 @@ function toggleChannel(card, type) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- WeChat login ---
|
||||
var wechatSession = null;
|
||||
var wechatPolling = false;
|
||||
|
||||
async function startWeChatLogin(e) {
|
||||
var form = document.getElementById('form-wechat');
|
||||
// If form is already open and showing QR, don't restart
|
||||
if (form.classList.contains('open') && wechatPolling) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle form open
|
||||
var card = form.closest('.channel-card');
|
||||
if (!form.classList.contains('open')) {
|
||||
form.classList.add('open');
|
||||
card.classList.add('selected');
|
||||
}
|
||||
|
||||
var qrDiv = document.getElementById('wechat-qr');
|
||||
var statusDiv = document.getElementById('wechat-status');
|
||||
qrDiv.innerHTML = '<p style="color:#888;">正在获取二维码...</p>';
|
||||
statusDiv.textContent = '';
|
||||
|
||||
try {
|
||||
var res = await fetch('/api/wechat/start', { method: 'POST' });
|
||||
var data = await res.json();
|
||||
if (data.error) throw new Error(data.error);
|
||||
|
||||
wechatSession = data.sessionKey;
|
||||
qrDiv.innerHTML = '<img src="' + data.qrcodeUrl + '" style="width:200px;height:200px;image-rendering:pixelated;border-radius:8px;">';
|
||||
statusDiv.textContent = '请用微信扫描上方二维码';
|
||||
statusDiv.style.color = '#07c160';
|
||||
|
||||
// Start polling
|
||||
wechatPolling = true;
|
||||
pollWeChatStatus();
|
||||
} catch (err) {
|
||||
qrDiv.innerHTML = '<p style="color:#f44336;">获取二维码失败</p>';
|
||||
statusDiv.textContent = err.message;
|
||||
statusDiv.style.color = '#f44336';
|
||||
}
|
||||
}
|
||||
|
||||
async function pollWeChatStatus() {
|
||||
if (!wechatSession || !wechatPolling) return;
|
||||
|
||||
try {
|
||||
var res = await fetch('/api/wechat/status?session=' + encodeURIComponent(wechatSession));
|
||||
var data = await res.json();
|
||||
|
||||
var qrDiv = document.getElementById('wechat-qr');
|
||||
var statusDiv = document.getElementById('wechat-status');
|
||||
|
||||
if (data.status === 'confirmed') {
|
||||
wechatPolling = false;
|
||||
qrDiv.innerHTML = '<div style="font-size:3em;">✔</div>';
|
||||
statusDiv.innerHTML = '<span style="color:#4caf50;font-weight:600;">微信连接成功!</span><br><span style="font-size:0.8em;">账号: ' +
|
||||
(data.accountId || '') + (data.pluginInstalled ? '' : '<br>⚠️ 插件未安装,请重启 Gateway') + '<br>需重启 Gateway 生效</span>';
|
||||
statusDiv.style.color = '#4caf50';
|
||||
showToast('微信连接成功!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'refreshed') {
|
||||
qrDiv.innerHTML = '<img src="' + data.qrcodeUrl + '" style="width:200px;height:200px;image-rendering:pixelated;border-radius:8px;">';
|
||||
statusDiv.textContent = '二维码已刷新,请重新扫码';
|
||||
statusDiv.style.color = '#ff6b35';
|
||||
} else if (data.status === 'scaned') {
|
||||
statusDiv.textContent = '已扫码,请在手机上确认...';
|
||||
statusDiv.style.color = '#07c160';
|
||||
} else if (data.status === 'expired') {
|
||||
wechatPolling = false;
|
||||
qrDiv.innerHTML = '<p style="color:#888;">二维码已过期,<a href="#" onclick="startWeChatLogin(event);return false;" style="color:#ff6b35;">重新获取</a></p>';
|
||||
statusDiv.textContent = data.message || '';
|
||||
statusDiv.style.color = '#f44336';
|
||||
return;
|
||||
} else if (data.status === 'error') {
|
||||
wechatPolling = false;
|
||||
qrDiv.innerHTML = '<p style="color:#f44336;">连接失败</p>';
|
||||
statusDiv.textContent = data.message || 'Unknown error';
|
||||
statusDiv.style.color = '#f44336';
|
||||
return;
|
||||
}
|
||||
// wait -> continue polling
|
||||
setTimeout(pollWeChatStatus, 1500);
|
||||
} catch (err) {
|
||||
setTimeout(pollWeChatStatus, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Save channels and open dashboard ---
|
||||
async function saveChannelsAndOpen() {
|
||||
var channels = {};
|
||||
|
||||
@@ -2,10 +2,306 @@
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { deflateSync } = require('zlib');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const PORT = 18788;
|
||||
const CONFIG_PATH = path.join(__dirname, '../data/.openclaw/openclaw.json');
|
||||
|
||||
// ── WeChat Login State ──────────────────────────────────────────────────────
|
||||
const DEFAULT_WECHAT_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
||||
const DEFAULT_ILINK_BOT_TYPE = '3';
|
||||
const ACTIVE_LOGIN_TTL_MS = 5 * 60000;
|
||||
const QR_POLL_TIMEOUT_MS = 35000;
|
||||
const MAX_QR_REFRESH_COUNT = 3;
|
||||
|
||||
// Resolve ~/.openclaw/ directory
|
||||
const OPENCLAW_DIR = process.env.OPENCLAW_STATE_DIR ||
|
||||
path.join(process.env.USERPROFILE || process.env.HOME || require('os').homedir(), '.openclaw');
|
||||
const WECHAT_STATE_DIR = path.join(OPENCLAW_DIR, 'openclaw-weixin');
|
||||
const WECHAT_ACCOUNTS_DIR = path.join(WECHAT_STATE_DIR, 'accounts');
|
||||
const WECHAT_ACCOUNT_INDEX_FILE = path.join(WECHAT_STATE_DIR, 'accounts.json');
|
||||
|
||||
// Plugin source on USB
|
||||
const USB_PLUGIN_DIR = path.join(__dirname, '../app/extensions/openclaw-weixin');
|
||||
const INSTALLED_PLUGIN_DIR = path.join(OPENCLAW_DIR, 'extensions', 'openclaw-weixin');
|
||||
|
||||
const activeLogins = new Map();
|
||||
|
||||
// ── QR Code PNG Renderer (pure Node.js, no external deps) ───────────────────
|
||||
|
||||
function getQrRenderDeps() {
|
||||
// Try to load QR lib from openclaw's bundled qrcode-terminal
|
||||
const corePath = path.join(__dirname, '../app/core/node_modules');
|
||||
const candidates = [
|
||||
path.join(corePath, 'qrcode-terminal/vendor/QRCode/index.js'),
|
||||
path.join(corePath, 'openclaw/node_modules/qrcode-terminal/vendor/QRCode/index.js'),
|
||||
];
|
||||
const errCandidates = [
|
||||
path.join(corePath, 'qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js'),
|
||||
path.join(corePath, 'openclaw/node_modules/qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js'),
|
||||
];
|
||||
for (let i = 0; i < candidates.length; i++) {
|
||||
if (fs.existsSync(candidates[i])) {
|
||||
return { QRCode: require(candidates[i]), QRErrorCorrectLevel: require(errCandidates[i]) };
|
||||
}
|
||||
}
|
||||
// Fallback: try WeChat plugin's own node_modules
|
||||
const pluginQr = path.join(USB_PLUGIN_DIR, 'node_modules/qrcode-terminal/vendor/QRCode/index.js');
|
||||
const pluginQrErr = path.join(USB_PLUGIN_DIR, 'node_modules/qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js');
|
||||
if (fs.existsSync(pluginQr)) {
|
||||
return { QRCode: require(pluginQr), QRErrorCorrectLevel: require(pluginQrErr) };
|
||||
}
|
||||
throw new Error('QR code library not found');
|
||||
}
|
||||
|
||||
function createQrMatrix(input) {
|
||||
const { QRCode, QRErrorCorrectLevel } = getQrRenderDeps();
|
||||
const qr = new QRCode(-1, QRErrorCorrectLevel.L);
|
||||
qr.addData(input);
|
||||
qr.make();
|
||||
return qr;
|
||||
}
|
||||
|
||||
function fillPixel(buf, x, y, width, r, g, b, a) {
|
||||
const idx = (y * width + x) * 4;
|
||||
buf[idx] = r; buf[idx + 1] = g; buf[idx + 2] = b; buf[idx + 3] = (a === undefined ? 255 : a);
|
||||
}
|
||||
|
||||
const CRC_TABLE = (function() {
|
||||
const table = new Uint32Array(256);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
let c = i;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
table[i] = c >>> 0;
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
function crc32(buf) {
|
||||
let crc = 0xffffffff;
|
||||
for (let i = 0; i < buf.length; i++) crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function pngChunk(type, data) {
|
||||
const typeBuf = Buffer.from(type, 'ascii');
|
||||
const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0);
|
||||
const crc = crc32(Buffer.concat([typeBuf, data]));
|
||||
const crcBuf = Buffer.alloc(4); crcBuf.writeUInt32BE(crc, 0);
|
||||
return Buffer.concat([len, typeBuf, data, crcBuf]);
|
||||
}
|
||||
|
||||
function encodePngRgba(buffer, width, height) {
|
||||
const stride = width * 4;
|
||||
const raw = Buffer.alloc((stride + 1) * height);
|
||||
for (let row = 0; row < height; row++) {
|
||||
const offset = row * (stride + 1);
|
||||
raw[offset] = 0;
|
||||
buffer.copy(raw, offset + 1, row * stride, row * stride + stride);
|
||||
}
|
||||
const compressed = deflateSync(raw);
|
||||
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4);
|
||||
ihdr[8] = 8; ihdr[9] = 6;
|
||||
return Buffer.concat([signature, pngChunk('IHDR', ihdr), pngChunk('IDAT', compressed), pngChunk('IEND', Buffer.alloc(0))]);
|
||||
}
|
||||
|
||||
function renderQrPngDataUrl(input) {
|
||||
const scale = 6, margin = 4;
|
||||
const qr = createQrMatrix(input);
|
||||
const modules = qr.getModuleCount();
|
||||
const size = (modules + margin * 2) * scale;
|
||||
const buf = Buffer.alloc(size * size * 4, 255);
|
||||
for (let row = 0; row < modules; row++) {
|
||||
for (let col = 0; col < modules; col++) {
|
||||
if (!qr.isDark(row, col)) continue;
|
||||
const sx = (col + margin) * scale, sy = (row + margin) * scale;
|
||||
for (let y = 0; y < scale; y++) for (let x = 0; x < scale; x++)
|
||||
fillPixel(buf, sx + x, sy + y, size, 0, 0, 0, 255);
|
||||
}
|
||||
}
|
||||
return 'data:image/png;base64,' + encodePngRgba(buf, size, size).toString('base64');
|
||||
}
|
||||
|
||||
// ── WeChat API helpers ──────────────────────────────────────────────────────
|
||||
|
||||
async function fetchWeChatQrCode(apiBaseUrl) {
|
||||
const base = apiBaseUrl.endsWith('/') ? apiBaseUrl : apiBaseUrl + '/';
|
||||
const url = base + 'ilink/bot/get_bot_qrcode?bot_type=' + encodeURIComponent(DEFAULT_ILINK_BOT_TYPE);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => '');
|
||||
throw new Error('Failed to fetch QR: ' + response.status + ' ' + body);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async function pollWeChatQrStatus(apiBaseUrl, qrcode) {
|
||||
const base = apiBaseUrl.endsWith('/') ? apiBaseUrl : apiBaseUrl + '/';
|
||||
const url = base + 'ilink/bot/get_qrcode_status?qrcode=' + encodeURIComponent(qrcode);
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), QR_POLL_TIMEOUT_MS);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: { 'iLink-App-ClientVersion': '1' },
|
||||
signal: controller.signal
|
||||
});
|
||||
clearTimeout(timer);
|
||||
const text = await response.text();
|
||||
if (!response.ok) throw new Error('Poll failed: ' + response.status + ' ' + text);
|
||||
return JSON.parse(text);
|
||||
} catch (err) {
|
||||
clearTimeout(timer);
|
||||
if (err.name === 'AbortError') return { status: 'wait' };
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAccountId(raw) {
|
||||
return String(raw).toLowerCase().replace(/[^a-z0-9._-]/g, '-');
|
||||
}
|
||||
|
||||
async function saveWeChatAccount(rawAccountId, payload) {
|
||||
const accountId = normalizeAccountId(rawAccountId);
|
||||
fs.mkdirSync(WECHAT_ACCOUNTS_DIR, { recursive: true });
|
||||
const filePath = path.join(WECHAT_ACCOUNTS_DIR, accountId + '.json');
|
||||
const data = {
|
||||
token: payload.token.trim(),
|
||||
savedAt: new Date().toISOString(),
|
||||
};
|
||||
if (payload.baseUrl) data.baseUrl = payload.baseUrl.trim();
|
||||
if (payload.userId) data.userId = payload.userId.trim();
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
|
||||
// Update account index
|
||||
let accounts = [];
|
||||
try { accounts = JSON.parse(fs.readFileSync(WECHAT_ACCOUNT_INDEX_FILE, 'utf-8')); } catch {}
|
||||
if (!Array.isArray(accounts)) accounts = [];
|
||||
if (!accounts.includes(accountId)) {
|
||||
accounts.push(accountId);
|
||||
fs.mkdirSync(WECHAT_STATE_DIR, { recursive: true });
|
||||
fs.writeFileSync(WECHAT_ACCOUNT_INDEX_FILE, JSON.stringify(accounts, null, 2));
|
||||
}
|
||||
return accountId;
|
||||
}
|
||||
|
||||
function ensureWeChatPluginInstalled() {
|
||||
if (!fs.existsSync(USB_PLUGIN_DIR) || !fs.existsSync(path.join(USB_PLUGIN_DIR, 'openclaw.plugin.json'))) {
|
||||
return { installed: false, warning: 'WeChat plugin not found on USB' };
|
||||
}
|
||||
if (fs.existsSync(path.join(INSTALLED_PLUGIN_DIR, 'openclaw.plugin.json'))) {
|
||||
return { installed: true };
|
||||
}
|
||||
// Copy from USB to ~/.openclaw/extensions/
|
||||
const extDir = path.join(OPENCLAW_DIR, 'extensions');
|
||||
fs.mkdirSync(extDir, { recursive: true });
|
||||
copyDirSync(USB_PLUGIN_DIR, INSTALLED_PLUGIN_DIR);
|
||||
return { installed: fs.existsSync(path.join(INSTALLED_PLUGIN_DIR, 'openclaw.plugin.json')) };
|
||||
}
|
||||
|
||||
function copyDirSync(src, dest) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const s = path.join(src, entry.name);
|
||||
const d = path.join(dest, entry.name);
|
||||
if (entry.isDirectory()) copyDirSync(s, d);
|
||||
else fs.copyFileSync(s, d);
|
||||
}
|
||||
}
|
||||
|
||||
// ── WeChat login session management ─────────────────────────────────────────
|
||||
|
||||
async function handleWeChatStart() {
|
||||
const sessionKey = crypto.randomUUID();
|
||||
const apiBaseUrl = DEFAULT_WECHAT_BASE_URL;
|
||||
const qrResponse = await fetchWeChatQrCode(apiBaseUrl);
|
||||
const qrDataUrl = renderQrPngDataUrl(qrResponse.qrcode_img_content);
|
||||
|
||||
activeLogins.set(sessionKey, {
|
||||
sessionKey,
|
||||
qrcode: qrResponse.qrcode,
|
||||
qrcodeUrl: qrDataUrl,
|
||||
startedAt: Date.now(),
|
||||
apiBaseUrl,
|
||||
});
|
||||
|
||||
return { sessionKey, qrcodeUrl: qrDataUrl };
|
||||
}
|
||||
|
||||
async function handleWeChatStatus(sessionKey) {
|
||||
const login = activeLogins.get(sessionKey);
|
||||
if (!login) return { status: 'expired', message: 'No active session' };
|
||||
if (Date.now() - login.startedAt > ACTIVE_LOGIN_TTL_MS) {
|
||||
activeLogins.delete(sessionKey);
|
||||
return { status: 'expired', message: 'Session expired' };
|
||||
}
|
||||
|
||||
const result = await pollWeChatQrStatus(login.apiBaseUrl, login.qrcode);
|
||||
|
||||
if (result.status === 'expired') {
|
||||
// Try to refresh QR code
|
||||
if (!login.refreshCount) login.refreshCount = 1;
|
||||
login.refreshCount++;
|
||||
if (login.refreshCount > MAX_QR_REFRESH_COUNT) {
|
||||
activeLogins.delete(sessionKey);
|
||||
return { status: 'expired', message: 'QR expired too many times' };
|
||||
}
|
||||
const refreshed = await fetchWeChatQrCode(login.apiBaseUrl);
|
||||
const newQr = renderQrPngDataUrl(refreshed.qrcode_img_content);
|
||||
login.qrcode = refreshed.qrcode;
|
||||
login.qrcodeUrl = newQr;
|
||||
login.startedAt = Date.now();
|
||||
return { status: 'refreshed', qrcodeUrl: newQr };
|
||||
}
|
||||
|
||||
if (result.status === 'confirmed') {
|
||||
activeLogins.delete(sessionKey);
|
||||
if (!result.ilink_bot_id || !result.bot_token) {
|
||||
return { status: 'error', message: 'Server did not return credentials' };
|
||||
}
|
||||
|
||||
// 1. Install plugin
|
||||
const pluginResult = ensureWeChatPluginInstalled();
|
||||
|
||||
// 2. Save account
|
||||
const accountId = await saveWeChatAccount(result.ilink_bot_id, {
|
||||
token: result.bot_token,
|
||||
baseUrl: result.baseurl,
|
||||
userId: result.ilink_user_id,
|
||||
});
|
||||
|
||||
// 3. Update openclaw.json to enable the plugin
|
||||
try {
|
||||
const configRaw = fs.existsSync(CONFIG_PATH) ? fs.readFileSync(CONFIG_PATH, 'utf-8') : '{}';
|
||||
const config = JSON.parse(configRaw);
|
||||
if (!config.plugins) config.plugins = {};
|
||||
if (!config.plugins.entries) config.plugins.entries = {};
|
||||
config.plugins.entries['openclaw-weixin'] = { enabled: true };
|
||||
const dir = path.dirname(CONFIG_PATH);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
||||
} catch (e) {
|
||||
console.error('Failed to update config:', e.message);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'confirmed',
|
||||
accountId,
|
||||
pluginInstalled: pluginResult.installed,
|
||||
message: 'WeChat connected! Restart Gateway to activate.',
|
||||
};
|
||||
}
|
||||
|
||||
return { status: result.status };
|
||||
}
|
||||
|
||||
function handleWeChatCancel(sessionKey) {
|
||||
if (sessionKey) activeLogins.delete(sessionKey);
|
||||
else activeLogins.clear();
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// CORS headers
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
@@ -18,6 +314,68 @@ const server = http.createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: WeChat start login
|
||||
if (req.url === '/api/wechat/start' && req.method === 'POST') {
|
||||
handleWeChatStart()
|
||||
.then(result => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(result));
|
||||
})
|
||||
.catch(err => {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err.message }));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// API: WeChat poll status
|
||||
if (req.url && req.url.startsWith('/api/wechat/status') && req.method === 'GET') {
|
||||
const urlObj = new URL(req.url, 'http://localhost');
|
||||
const session = urlObj.searchParams.get('session');
|
||||
if (!session) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Missing session parameter' }));
|
||||
return;
|
||||
}
|
||||
handleWeChatStatus(session)
|
||||
.then(result => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(result));
|
||||
})
|
||||
.catch(err => {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err.message }));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// API: WeChat cancel
|
||||
if (req.url === '/api/wechat/cancel' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk);
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const data = body ? JSON.parse(body) : {};
|
||||
handleWeChatCancel(data.session);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err.message }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// API: WeChat plugin status
|
||||
if (req.url === '/api/wechat/plugin-status' && req.method === 'GET') {
|
||||
const hasPlugin = fs.existsSync(path.join(USB_PLUGIN_DIR, 'openclaw.plugin.json'));
|
||||
const installed = fs.existsSync(path.join(INSTALLED_PLUGIN_DIR, 'openclaw.plugin.json'));
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ hasPlugin, installed }));
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Get config
|
||||
if (req.url === '/api/config' && req.method === 'GET') {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user