' +
+ '
伴侣已暂停
' +
+ '
头脑风暴伴侣已停止。' +
+ '请让你的编程 Agent 重新启动它——本页面会自动重新连接。
';
+ if (document.body) document.body.appendChild(el);
+ }
+
+ function connect() {
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
+ setStatus(everConnected ? 'reconnecting' : 'connecting');
+ ws = new WebSocket(websocketUrl());
+
+ ws.onopen = () => {
+ const recovered = tombstoneShown;
+ everConnected = true;
+ disconnectedSince = null;
+ reconnectDelay = MIN_RECONNECT_MS;
+ tombstoneShown = false;
+ setStatus('connected');
+ eventQueue.forEach(e => ws.send(JSON.stringify(e)));
+ eventQueue = [];
+ // Recovered from a tombstoned outage (e.g. the server restarted on the same
+ // port) — reload through the keyed bootstrap when possible so the cookie is
+ // refreshed before the visible URL returns to bare /.
+ if (recovered) reloadAfterRecovery();
+ };
+
+ ws.onmessage = (msg) => {
+ let data;
+ try { data = JSON.parse(msg.data); } catch (e) { return; }
+ if (data.type === 'reload') window.location.reload();
+ };
+
+ ws.onclose = () => {
+ ws = null;
+ if (disconnectedSince === null) disconnectedSince = Date.now();
+ if (Date.now() - disconnectedSince >= TOMBSTONE_AFTER_MS) {
+ setStatus('disconnected');
+ showTombstone();
+ } else {
+ setStatus('reconnecting');
+ }
+ reconnectTimer = setTimeout(connect, reconnectDelay);
+ reconnectDelay = nextReconnectDelay(reconnectDelay, MAX_RECONNECT_MS);
+ };
+
+ // Let onclose own reconnection so we don't schedule it twice.
+ ws.onerror = () => { try { ws.close(); } catch (e) {} };
+ }
+
+ function sendEvent(event) {
+ event.timestamp = Date.now();
+ if (ws && ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify(event));
+ } else {
+ eventQueue.push(event);
+ }
+ }
+
+ // Capture clicks on choice elements
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-choice]');
+ if (!target) return;
+
+ sendEvent({
+ type: 'click',
+ text: target.textContent.trim(),
+ choice: target.dataset.choice,
+ id: target.id || null
+ });
+
+ });
+
+ // Frame UI: selection tracking
+ window.selectedChoice = null;
+
+ window.toggleSelect = function(el) {
+ const container = el.closest('.options') || el.closest('.cards');
+ const multi = container && container.dataset.multiselect !== undefined;
+ if (container && !multi) {
+ container.querySelectorAll('.option, .card').forEach(o => o.classList.remove('selected'));
+ }
+ if (multi) {
+ el.classList.toggle('selected');
+ } else {
+ el.classList.add('selected');
+ }
+ window.selectedChoice = el.dataset.choice;
+ };
+
+ // Expose API for explicit use
+ window.brainstorm = {
+ send: sendEvent,
+ choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata })
+ };
+
+ connect();
+})();
diff --git a/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/server.cjs b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/server.cjs
new file mode 100644
index 00000000..cc6c2aee
--- /dev/null
+++ b/plugins/codex/plugins/superpowers-zh/skills/brainstorming/scripts/server.cjs
@@ -0,0 +1,723 @@
+const crypto = require('crypto');
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+
+// ========== WebSocket Protocol (RFC 6455) ==========
+
+const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
+const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
+const MAX_FRAME_PAYLOAD_BYTES = 10 * 1024 * 1024;
+
+function computeAcceptKey(clientKey) {
+ return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
+}
+
+function encodeFrame(opcode, payload) {
+ const fin = 0x80;
+ const len = payload.length;
+ let header;
+
+ if (len < 126) {
+ header = Buffer.alloc(2);
+ header[0] = fin | opcode;
+ header[1] = len;
+ } else if (len < 65536) {
+ header = Buffer.alloc(4);
+ header[0] = fin | opcode;
+ header[1] = 126;
+ header.writeUInt16BE(len, 2);
+ } else {
+ header = Buffer.alloc(10);
+ header[0] = fin | opcode;
+ header[1] = 127;
+ header.writeBigUInt64BE(BigInt(len), 2);
+ }
+
+ return Buffer.concat([header, payload]);
+}
+
+function decodeFrame(buffer) {
+ if (buffer.length < 2) return null;
+
+ const secondByte = buffer[1];
+ const opcode = buffer[0] & 0x0F;
+ const masked = (secondByte & 0x80) !== 0;
+ let payloadLen = secondByte & 0x7F;
+ let offset = 2;
+
+ if (!masked) throw new Error('Client frames must be masked');
+
+ if (payloadLen === 126) {
+ if (buffer.length < 4) return null;
+ payloadLen = buffer.readUInt16BE(2);
+ offset = 4;
+ } else if (payloadLen === 127) {
+ if (buffer.length < 10) return null;
+ const extendedLen = buffer.readBigUInt64BE(2);
+ if (extendedLen > BigInt(MAX_FRAME_PAYLOAD_BYTES)) {
+ throw new Error('WebSocket frame payload exceeds maximum allowed size');
+ }
+ payloadLen = Number(extendedLen);
+ offset = 10;
+ }
+
+ if (payloadLen > MAX_FRAME_PAYLOAD_BYTES) {
+ throw new Error('WebSocket frame payload exceeds maximum allowed size');
+ }
+
+ const maskOffset = offset;
+ const dataOffset = offset + 4;
+ const totalLen = dataOffset + payloadLen;
+ if (buffer.length < totalLen) return null;
+
+ const mask = buffer.slice(maskOffset, dataOffset);
+ const data = Buffer.alloc(payloadLen);
+ for (let i = 0; i < payloadLen; i++) {
+ data[i] = buffer[dataOffset + i] ^ mask[i % 4];
+ }
+
+ return { opcode, payload: data, bytesConsumed: totalLen };
+}
+
+// ========== Configuration ==========
+
+const PORT_FILE = process.env.BRAINSTORM_PORT_FILE || null;
+const randomPort = () => 49152 + Math.floor(Math.random() * 16383);
+// Prefer an explicit port, else the port this session last bound (so a restart
+// reuses it and an already-open browser tab reconnects), else a random high port.
+function preferredPort() {
+ if (process.env.BRAINSTORM_PORT) return Number(process.env.BRAINSTORM_PORT);
+ if (PORT_FILE) {
+ try {
+ const p = Number(fs.readFileSync(PORT_FILE, 'utf-8').trim());
+ if (Number.isInteger(p) && p > 1023 && p < 65536) return p;
+ } catch (e) { /* no prior port recorded */ }
+ }
+ return randomPort();
+}
+let PORT = preferredPort();
+const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1';
+const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST);
+const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
+const CONTENT_DIR = path.join(SESSION_DIR, 'content');
+const STATE_DIR = path.join(SESSION_DIR, 'state');
+const SUPERPOWERS_VERSION = readSuperpowersVersion();
+const SUPERPOWERS_BRAND_IMAGE_URL = 'https://primeradiant.com/brand/superpowers-visual-brainstorming-logo.png';
+const TELEMETRY_DISABLE_ENV_VARS = [
+ 'SUPERPOWERS_DISABLE_TELEMETRY',
+ 'DISABLE_TELEMETRY',
+ 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'
+];
+const SUPERPOWERS_TELEMETRY_DISABLED = TELEMETRY_DISABLE_ENV_VARS.some(name => isTruthyEnv(process.env[name]));
+let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null;
+
+// Per-session secret key. The companion is reachable by any local browser tab
+// and, when bound to a non-loopback host, by any host that can route to it.
+// The key authenticates the real client uniformly across loopback, tunnel, and
+// remote binds — and defeats DNS rebinding — where a Host/Origin allowlist
+// cannot. It rides the served URL as ?key= and is mirrored into a cookie on
+// first load so same-origin subresources and the WebSocket carry it for free.
+// Persisted alongside the port (BRAINSTORM_TOKEN_FILE) so a restart keeps the
+// same key and an already-open tab's cookie still validates.
+const TOKEN_FILE = process.env.BRAINSTORM_TOKEN_FILE || null;
+function generateToken() {
+ return crypto.randomBytes(32).toString('hex');
+}
+
+function chmodOwnerOnly(file) {
+ try { fs.chmodSync(file, 0o600); } catch (e) { /* best effort */ }
+}
+
+function initialToken() {
+ if (process.env.BRAINSTORM_TOKEN) {
+ return { value: process.env.BRAINSTORM_TOKEN, source: 'env' };
+ }
+ if (TOKEN_FILE) {
+ try {
+ const t = fs.readFileSync(TOKEN_FILE, 'utf-8').trim();
+ if (/^[0-9a-f]{32,}$/i.test(t)) {
+ chmodOwnerOnly(TOKEN_FILE);
+ return { value: t, source: 'file' };
+ }
+ } catch (e) { /* no prior token recorded */ }
+ }
+ return { value: generateToken(), source: 'generated' };
+}
+
+const tokenInfo = initialToken();
+let TOKEN = tokenInfo.value;
+let tokenSource = tokenInfo.source;
+let COOKIE_NAME = 'brainstorm-key-' + PORT; // refined to the actual bound port in onListen
+
+const MIME_TYPES = {
+ '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
+ '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml'
+};
+
+// ========== Templates and Constants ==========
+
+function waitingPage() {
+ return renderBranding(`
+
+