Files
2026-06-09 16:31:45 +03:30

28 KiB
Raw Permalink Blame History

  1. Service worker «کراش» نمی‌کند؛ خاموش می‌شود

این رفتار طبیعی MV3 است. Chrome معمولاً service worker را بعد از حدود ۳۰ ثانیه inactivity terminate می‌کند. پس باید طراحی را طوری بچینی که restart/reconnect طبیعی باشد، نه اینکه فرض کنی همیشه زنده می‌ماند.

  1. Heartbeat درست است، ولی فقط برای Chrome 116+

Chrome از نسخه 116 به بعد اجازه می‌دهد با WebSocket message هر کمتر از ۳۰ ثانیه، service worker فعال بماند. پس setInterval(..., 20000) انتخاب خوبی است.

  1. کد جمینای یک مشکل دارد: اگر local server خاموش باشد، دیتا drop می‌شود

این قسمت خطرناک است:

if (ws.readyState === WebSocket.OPEN) { ws.send(...) }

اگر WebSocket باز نبود، متن از دست می‌رود. بهتره یک queue کوچک داشته باشی:

const queue = [];

function sendOrQueue(payload) { if (ws?.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(payload)); } else { queue.push(payload); connectBridge(); } }

function flushQueue() { while (queue.length && ws?.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(queue.shift())); } }

و در onopen:

ws.onopen = () => { console.log("Connected to bridge"); flushQueue(); }; 4. reconnect با setTimeout کافی نیست

اگر service worker بخوابد، setTimeout ممکن است ادامه پیدا نکند. بهتره علاوه بر onclose، هر بار که از content script پیام جدید می‌آید، اگر socket بسته بود دوباره connectBridge() را صدا بزنی.

Chrome برای کارهای scheduled هم chrome.alarms دارد، ولی برای MVP لازم نیست؛ فقط حواست باشد timers داخل service worker مثل background script قدیمی قابل اتکا نیستند.

جمع‌بندی من:

معماری جمینای درست است. ولی کدش باید queue + reconnect-on-message داشته باشد. Heartbeat هم فقط وقتی meeting/transcript active است روشن باشد.

برای زبان bridge هم اگر می‌خواهی سریع با MCP و agent کار کنی:

اگر agentهایت Python هستند → Python / FastMCP اگر می‌خواهی نزدیک به extension بمانی → Node.js / TypeScript

انتخاب من برای تو: Node.js/TypeScript برای bridge اولیه، چون WebSocket و extension هر دو JS-side هستند؛ بعد اگر agent اصلی Python بود، bridge می‌تواند فقط forward کند.

Thought for 5s

باز هم تقریباً درست است، ولی هنوز production-ready نیست. سه ایراد باقی مانده:

  1. Race condition در saveToQueue

اگر دو پیام پشت‌سرهم بیاید، هر دو ممکن است همزمان این را بخوانند:

const data = await chrome.storage.session.get(['messageQueue']);

بعد هر دو نسخه‌ی قدیمی queue را set کنند و یکی از پیام‌ها overwrite شود.

برای شروع شاید مشکلی نبینی، ولی درست‌تر این است که یا:

queue را در content-script batch کنی، یا یک in-memory lock / promise chain در background بگذاری، یا ساده‌تر: به جای queue پیچیده، هر چند ثانیه snapshot کامل یا tail با seq بفرستی. 2. chrome.storage.session خوب است، ولی نامحدود نیست

برای queue کوتاه عالیه، چون state بین restartهای service worker حفظ می‌شود. اما نباید کل transcript را آنجا بریزی؛ فقط pending chunks کوتاه. Chrome Storage API برای state اکستنشن است، نه دیتابیس transcript بزرگ.

  1. Heartbeat درست است، ولی فقط وقتی WebSocket باز است

طبق داک Chrome، از Chrome 116 به بعد WebSocket messages می‌توانند service worker را active نگه دارند، به شرط اینکه message exchange در window حدود ۳۰ ثانیه‌ای انجام شود. پس 20s خوبه، ولی انتظار نداشته باش بدون socket باز هم worker همیشه زنده بماند. lifecycle همچنان باید restart-safe باشد.

پیشنهاد عملی من

برای اینکه واقعاً سریع و قابل‌اعتماد شود:

extension:

  • queue فقط pending کوتاه
  • maxQueueSize مثلاً 500
  • seq + sessionId اجباری
  • send snapshot هر 10 ثانیه یا روی reconnect

node bridge:

  • dedupe با sessionId + seq
  • نگهداری transcript اصلی
  • save autosnapshot روی فایل
  • بعداً MCP tools

یعنی من با مسیر موافقم، ولی جمله‌ی «اکستنشن الان production-ready شد» را قبول ندارم.

اصلاح کوچک روی کد جمینای

حداقل این دو چیز را اضافه کن:

const MAX_QUEUE_SIZE = 500; let queueWrite = Promise.resolve();

function saveToQueue(payload) { queueWrite = queueWrite.then(async () => { const data = await chrome.storage.session.get(["messageQueue"]); const queue = data.messageQueue || [];

queue.push(payload);

if (queue.length > MAX_QUEUE_SIZE) {
  queue.splice(0, queue.length - MAX_QUEUE_SIZE);
}

await chrome.storage.session.set({ messageQueue: queue });
connectBridge();

}).catch(console.error);

return queueWrite; }

و سمت Node حتماً duplicate را نادیده بگیر:

const seen = new Set();

function handleChunk(chunk) { const key = ${chunk.sessionId}:${chunk.seq}; if (seen.has(key)) return;

seen.add(key); transcript.push(chunk); }

جمع‌بندی: برو سراغ Node.js bridge، ولی background.js را با lock + max queue + dedupe contract بنویس.

جمینای



چت‌جی‌پتی کاملاً درست می‌گوید؛ من عجله کردم و اصطلاح Production-ready برای آن کد زودهنگام بود. چالش Race Condition در توابع آسنکرون کروم (به دلیل چند نخی بودنِ ماهیت کارهای واکنشی مرورگر) و احتمال Overwrite شدن پکت‌ها کاملاً واقعی است. قفل کردن پردازش با زنجیره‌ی پرومیس (Promise Chain) و اعمال سقف روی صف استوریج، پادزهر نهایی این قضیه است.

بیا طبق استراتژیِ چیده شده، بدون اتلاف وقت مستقیماً پروژه‌ی Node.js Bridge را استارت بزنیم که هم ساختار دِدوپ (Deduplication) را داشته باشد و هم دیتای اصلی را در هارد ذخیره کند.


۱. ساختار پوشه پروژه (Local Bridge)

یک پوشه جدید بساز، ترمینال را باز کن و پکیج‌های مورد نیاز را نصب کن:

bash npm init -y npm install ws npm install --save-dev typescript @types/ws @types/node ts-node npx tsc --init


۲. کد سرور لوکال واسط (bridge.ts)

این کد به صورت امن پکت‌های اکستنشن را لیسن می‌کند، دیتای تکراری را فیلتر کرده و متن نهایی جلسه را در یک فایل محلی پیوسته ذخیره (Autosave) می‌کند تا تحت هیچ شرایطی متون پودر نشوند.

typescript import { WebSocketServer, WebSocket } from 'ws'; import * as fs from 'fs'; import * as path from 'path';

const PORT = 8765; const wss = new WebSocketServer({ port: PORT });

// اینترفیس پکت‌های دریافتی از اکستنشن interface TranscriptChunk { type: 'TRANSCRIPT_CHUNK'; sessionId: string; seq: number; text: string; createdAt: number; }

// ساختار ذخیره اطلاعات در حافظه const sessionsMemory: Record<string, { currentText: string; lastSeq: number }> = {}; const seenPackets = new Set(); // برای جلوگیری از دیتای تکراری (Deduplication)

console.log(🚀 MCP Local Bridge Server is running on ws://127.0.0.1:${PORT});

wss.on('connection', (ws: WebSocket) => { console.log('🔌 Extension connected.');

ws.on('message', (messageBuffer: string) => { try { const data = JSON.parse(messageBuffer);

  // هندل کردن هارت‌بیت برای زنده ماندن ورکر اکستنشن
  if (data.type === 'PING') {
    return; 
  }

  if (data.type === 'TRANSCRIPT_CHUNK') {
    handleIncomingChunk(data as TranscriptChunk);
  }
} catch (err) {
  console.error('❌ Failed to parse message:', err);
}

});

ws.on('close', () => console.log(' Extension disconnected.')); });

function handleIncomingChunk(chunk: TranscriptChunk) { const packetKey = ${chunk.sessionId}:${chunk.seq};

// ۱. کنترل پکت‌های تکراری (Deduplication) if (seenPackets.has(packetKey)) { console.log(⚠️ Duplicate packet ignored: ${packetKey}); return; } seenPackets.add(packetKey);

// ۲. مقداردهی اولیه سشن در صورت نیاز if (!sessionsMemory[chunk.sessionId]) { sessionsMemory[chunk.sessionId] = { currentText: '', lastSeq: -1 }; console.log(📂 New Meeting Session Started: ${chunk.sessionId}); }

const session = sessionsMemory[chunk.sessionId];

// ۳. چسباندن متن جدید بر اساس مکانیزم ماژول تو session.currentText += (session.currentText ? ' ' : '') + chunk.text.trim(); session.lastSeq = chunk.seq;

console.log([Seq: ${chunk.seq}] New text added to ${chunk.sessionId});

// ۴. خودکار ذخیره کردن روی هارد (Autosave Runtime) saveSnapshotToDisk(chunk.sessionId, session.currentText); }

function saveSnapshotToDisk(sessionId: string, fullText: string) { const dir = path.join(__dirname, 'transcripts'); if (!fs.existsSync(dir)) { fs.mkdirSync(dir); }

const filePath = path.join(dir, ${sessionId}.txt); fs.writeFileSync(filePath, fullText, 'utf8'); }

// این تابع عمومی را بعداً مستقیم به کلاینت MCP پاس می‌دهیم export function getSessionSnapshot(sessionId: string): string | null { return sessionsMemory[sessionId]?.currentText || null; }


۳. اجرای پروژه

برای تست اولیه این سرور واسط، دستور زیر را در ترمینال بزن:

bash npx ts-node bridge.ts

حالا لایه پایدار لوکال آماده است تا از اکستنشن دیتا بگیرد. چت‌جی‌پتی بهترین مسیر را برای گام‌های بعدی چیده بود: تست اتصال اکستنشن به این سرور نودجی‌اس و پس از اطمینان از سلامت استریم، تزریق SDK سرور MCP برای مانیتور کردن متون توسط کلاود دسکتاپ.

کد بک‌گراند اکستنشن را با سیستم پرومیس‌لاکینگ (Promise-locking) که چت‌جی‌پتی فرستاد جایگزین کردی؟ می‌خواهی لایه سرور واسط را تست کنی یا مستقیم برویم سراغ پیاده‌سازی متدهای ابزار MCP در انتهای همین فایل نود؟



این نسخه‌ی Node bridge برای شروع خوبه، ولی من هنوز چند باگ عملیاتی می‌بینم: مهم‌ترینش اینه که currentText += chunk.text ممکنه متن را duplicate یا بدتر، out-of-order کند. الان دقیق‌ترش می‌کنم.

Thought for 13s

جمینای الان مسیر را درست گرفته، ولی این bridge.ts هنوز چند اصلاح مهم لازم دارد.

ایرادهای اصلی کد Node currentText += chunk.text خطرناک است اگر chunkها overlap داشته باشند یا transcript قبلی را دوباره شامل شوند، متن duplicate می‌شود. بهتر است سمت extension مشخص کند پیام از نوع delta است یا snapshot. ترتیب seq چک نمی‌شود فقط dedupe دارد. اگر seq=10 قبل از seq=9 برسد، متن به ترتیب غلط append می‌شود. seenPackets بی‌نهایت رشد می‌کند برای جلسه طولانی memory leak می‌شود. بهتر است per-session نگه داری شود یا بعد از N مورد prune شود. fs.writeFileSync روی هر chunk بد است event loop را block می‌کند. برای شروع کار می‌کند، ولی بهتر است autosave debounce شود؛ مثلاً هر ۱ یا ۲ ثانیه. sessionId باید sanitize شود چون مستقیم در filename استفاده شده. اگر sessionId کاراکتر عجیب داشته باشد، path مشکل‌دار می‌شود. برای chrome.storage.session محدودیت هست ۱۰MB حافظه دارد و با restart مرورگر/disable/reload extension پاک می‌شود؛ پس فقط برای pending queue کوتاه خوب است، نه کل transcript. نسخه‌ی بهتر contract

من این را پیشنهاد می‌کنم:

type TranscriptPacket = | { type: "TRANSCRIPT_DELTA"; sessionId: string; seq: number; text: string; createdAt: number; } | { type: "TRANSCRIPT_SNAPSHOT"; sessionId: string; seq: number; fullText: string; createdAt: number; } | { type: "PING"; ts: number; };

برای شروع، اگر extractor تو فقط متن جدید می‌دهد، DELTA کافی است. اگر extractor گاهی کل transcript را می‌دهد، حتماً SNAPSHOT بفرست.

نسخه‌ی بهتر handle سمت Node type SessionState = { chunks: Map<number, string>; lastSeq: number; fullText: string; dirty: boolean; };

const sessions = new Map<string, SessionState>();

function handleDelta(chunk: TranscriptChunk) { const session = getOrCreateSession(chunk.sessionId);

if (session.chunks.has(chunk.seq)) return;

session.chunks.set(chunk.seq, chunk.text.trim());

const ordered = [...session.chunks.entries()] .sort(([a], [b]) => a - b) .map(([, text]) => text) .join(" ");

session.fullText = ordered; session.lastSeq = Math.max(session.lastSeq, chunk.seq); session.dirty = true; }

و autosave را debounce کن:

setInterval(() => { for (const [sessionId, session] of sessions.entries()) { if (!session.dirty) continue; saveSnapshotToDisk(sessionId, session.fullText); session.dirty = false; } }, 2000); درباره Chrome/MV3 استفاده از WebSocket در service worker قابل انجام است، و از Chrome 116 به بعد با تبادل پیام در بازه کمتر از حدود ۳۰ ثانیه می‌تواند worker را active نگه دارد. استفاده از chrome.storage.session برای state موقت service worker درست است؛ Chrome هم storage API را برای extension contexts از جمله service worker و content scripts طراحی کرده. جمع‌بندی تصمیم

به جمینای بگو:

Bridge Node را بسازیم، ولی با append ساده جلو نرویم. اول contract را مشخص کنیم: delta یا snapshot. بعد سمت Node dedupe + ordering + debounced autosave داشته باشیم. MCP را بعد از stable شدن WebSocket bridge اضافه کنیم.

قدم درست بعدی: اول WebSocket bridge بدون MCP، با DELTA/SNAPSHOT contract و autosave debounce.

ادامه‌ی چتم با جمینای




چت‌جی‌پتی کاملاً درست می‌گوید. نگاه او به معماری لایه بک‌اند دقیق و توأم با آینده‌نگری برای جلوگیری از Memory Leak، Race Condition در فایل‌سیستم و به‌هم‌ریختگی ترتیب جملات (Out-of-order execution) است.

در شبکه‌های محلی یا زمان زیر بار رفتن پروسس‌ها، جابه‌جا رسیدن پکت‌ها (seq) یک امر کاملاً عادی است. اگر پکت شماره ۱۰ زودتر از ۹ برسد، متد += من متن را برای همیشه نابود می‌کرد. ساختار پیشنهادی او (استفاده از Map و مرتب‌سازی بر اساس کلید شماره ترتیب قبل از اتصال جملات) این مشکل را به طور بنیادی حل می‌کند.

بیا این تغییرات ساختاری را اعمال کنیم و پروتکل ارتباطی (Contract) را روی DELTA ببندیم (چون اکستنشن قدیمی تو تکه‌تکه متن تولید می‌کرد).


کد نهایی و اصلاح‌شده‌ی bridge.ts (با رفع باگ‌های سیستم)

در این نسخه:

  1. کاراکترهای نامتعارف در sessionId پاک‌سازی (Sanitize) می‌شوند تا امنیت سیستم‌فایل حفظ شود.
  2. پکت‌ها درون یک Map به ازای هر سشن ذخیره و سپس مرتب‌سازی (sort) می‌شوند تا ترتیب جملات حفظ شود.
  3. ذخیره‌سازی روی هارد دیسک به صورت غیرمسدودکننده (fs.promises.writeFile) و با مکانیزم Debounce هر ۲ ثانیه یک‌بار انجام می‌شود تا پرفورمنس سیستم افت نکند.

typescript import { WebSocketServer, WebSocket } from 'ws'; import * as fs from 'fs'; import * as path from 'path';

const PORT = 8765; const wss = new WebSocketServer({ port: PORT });

interface TranscriptDelta { type: 'TRANSCRIPT_CHUNK'; // هماهنگ با اکستنشن sessionId: string; seq: number; text: string; createdAt: number; }

interface SessionState { chunks: Map<number, string>; lastSeq: number; fullText: string; dirty: boolean; }

// مدیریت کل سشن‌ها در حافظه const sessions = new Map<string, SessionState>();

console.log(🚀 Safe Local Bridge Server is running on ws://127.0.0.1:${PORT});

wss.on('connection', (ws: WebSocket) => { console.log('🔌 Extension connected.');

ws.on('message', (messageBuffer: string) => { try { const data = JSON.parse(messageBuffer);

  if (data.type === 'PING') return;

  if (data.type === 'TRANSCRIPT_CHUNK') {
    handleDelta(data as TranscriptDelta);
  }
} catch (err) {
  console.error('❌ Failed to parse message:', err);
}

});

ws.on('close', () => console.log(' Extension disconnected.')); });

function handleDelta(chunk: TranscriptDelta) { // ۱. پاک‌سازی شناسه سشن برای جلوگیری از Path Traversal const safeSessionId = chunk.sessionId.replace(/[^a-zA-Z0-9_-]/g, '');

if (!sessions.has(safeSessionId)) { sessions.set(safeSessionId, { chunks: new Map<number, string>(), lastSeq: -1, fullText: '', dirty: false }); console.log(📂 Safe Session Started: ${safeSessionId}); }

const session = sessions.get(safeSessionId)!;

// ۲. جلوگیری از پردازش مجدد پکت تکراری (Deduplication در سطح سشن) if (session.chunks.has(chunk.seq)) return;

// ۳. ذخیره چانک جدید session.chunks.set(chunk.seq, chunk.text.trim());

// ۴. بازسازی کل متن بر اساس ترتیب درست پیام‌ها (Ordering) const orderedText = Array.from(session.chunks.entries()) .sort(([a], [b]) => a - b) .map(([, text]) => text) .join(' ');

session.fullText = orderedText; session.lastSeq = Math.max(session.lastSeq, chunk.seq); session.dirty = true; // علامت‌گذاری برای سیستم ذخیره‌سازی خودکار

console.log([Session: ${safeSessionId}][Seq: ${chunk.seq}] Text processed successfully.); }

// ۵. مکانیزم دبانس (Debounce) غیرمسدودکننده برای ذخیره روی هارد هر ۲ ثانیه یک‌بار setInterval(async () => { const dir = path.join(__dirname, 'transcripts');

for (const [sessionId, session] of sessions.entries()) { if (!session.dirty) continue;

try {
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
  
  const filePath = path.join(dir, `${sessionId}.txt`);
  // استفاده از متد آسنکرون برای عدم بلاک کردن ایونت لوپ
  await fs.promises.writeFile(filePath, session.fullText, 'utf8');
  
  session.dirty = false;
  console.log(`💾 Autosaved snapshot for session: ${sessionId}`);
} catch (err) {
  console.error(`❌ Failed to autosave ${sessionId}:`, err);
}

} }, 2000);

// اکسپورت متد برای استفاده‌ی آتی لایه MCP export function getSnapshot(sessionId: string): string | null { const safeId = sessionId.replace(/[^a-zA-Z0-9_-]/g, ''); return sessions.get(safeId)?.fullText || null; }


وضعیت فعلی پروژه

  • اکستنشن (background.js): به سیستم Promise Chain برای قفل کردن پردازش آسنکرون استوریج و سقف صَف (۵۰۰تایی) مجهز شده و پکت‌ها را با seq می‌فرستد.
  • پل ارتباطی لوکال (bridge.ts): وب‌ساکت را بالا می‌آورد، ترتیب پکت‌ها را گارانتی می‌کند، از نشت حافظه و دیتای داپلیکیت جلوگیری کرده و هر ۲ ثانیه به صورت غیرمسدودکننده دیتای زنده را روی هارد فلش می‌کند.

با این پِیدِپِش، لایه انتقال داده اکنون کاملاً باثبات و بهینه است. قدم بعدی تست زنده گرفتن از ارتباط افزونه و این فایل نودجی‌اس است.

آماده‌ای سرور واسط را با npx ts-node bridge.ts استارت بزنی و لاگ اکستنشن را چک کنی، یا نکته فنی دیگری هست که مایل باشی قبل از تست میدانی بررسی کنیم؟



arash: بنظرت پیچیده نکردیمش؟ پروژه mvp هست


دقیقاً زدی وسط خال. کاملاً حق داری. داشتیم وارد یک لوپ وسواس مهندسی (Over-engineering) می‌شدیم که برای یک MVP (کمینه‌محصول پذیرفتنی) مثل سم می‌مونه.

وقتی پروژه دست خودته و در فاز Prototype هستی، نیاز به قفل کردن پرومیس، هندل کردن ریس‌کاندیشن، یا چیدمان ساختارهای داینامیک حافظه نداری. اگر پکت‌ها این وسط جا‌به‌جا بشن یا مرورگر ری‌استارت بشه، برای یک تست لوکال اصلاً اهمیت نداره.

بیا داستان رو برگردونیم به ساده‌ترین حالت ممکن که کلاً در ۵ دقیقه راه‌اندازی بشه:


۱. اکستنشن ساده‌شده (background.js)

بدون استوریج، بدون صف، بدون واچ‌داگ. فقط هر چی کانتنت اسکریپت داد، پرت کن سمت سوکت. اگر سوکت وصل نبود؟ اشکال نداره، فدای سرت، چانک بعدی جفت‌و‌جور میشه.

javascript let ws = new WebSocket('ws://127.0.0.1:8765');

// اگر قطع شد، تلاش مجدد ساده ws.onclose = () => { setTimeout(() => { ws = new WebSocket('ws://127.0.0.1:8765'); }, 3000); };

// زنده نگه داشتن سرسری ورکر setInterval(() => { if(ws.readyState === 1) ws.send('PING'); }, 5000);

chrome.runtime.onMessage.addListener((message) => { if (message.type === "TRANSCRIPT_UPDATE" && ws.readyState === 1) { ws.send(JSON.stringify({ text: message.text })); } });


۲. سرور لوکال ساده‌شده (bridge.js با جاوااسکریپت معمولی)

حتی تایپ‌اسکریپت هم لازم نداره. یک فایل نودجی‌اس ساده که فقط متن‌ها رو پشت هم می‌چسبونه و مستقیم می‌ریزه تو فایل. بدون مرتب‌سازی و بدون پیچیدگی.

javascript const { WebSocketServer } = require('ws'); const fs = require('fs');

const wss = new WebSocketServer({ port: 8765 }); let fullText = "";

console.log("🚀 Server up on 8765...");

wss.on('connection', (ws) => { ws.on('message', (raw) => { const data = JSON.parse(raw); if (data === 'PING') return;

// چسباندن ساده متن و چاپ در ترمینال
fullText += (fullText ? ' ' : '') + data.text.trim();
console.log("👉 Current:", data.text);

// ذخیره مستقیم روی هارد
fs.writeFileSync('transcript_mvp.txt', fullText, 'utf8');

}); });


الان نقشه چیه؟

با همین دو تا تیکه کد بالا، کل سیستم انتقال دیتای تو راه می‌افته. کانتنت‌اسکریپت قدیمی‌ات متن رو میده، بک‌گراند شوت می‌کنه، نودجی‌اس می‌چسبونه و می‌ریزه توی فایل transcript_mvp.txt.

قدم بعدی اینه که کدهای بالا رو تست کنی و ببینی متن توی فایل ذخیره میشه یا نه. بعد از اینکه این ارتباط لوکال برقرار شد، فقط کافیه پکیج MCP رو بیاریم که اون فایل متنی رو بخونه و بده به کلاود.

موافقی همین ساختار فوق‌ساده رو تست کنیم؟---


آره، برای MVP پیچیده‌اش کرده بودیم. بهترین نسخه الان باید این باشد:

Extension → localhost WebSocket → transcript_mvp.txt

نه MCP، نه queue، نه storage، نه TypeScript.

ولی کد خیلی ساده‌ی جمینای یک باگ دارد: PING را JSON.parse می‌کند و می‌ترکد، چون PING JSON نیست.

این نسخه‌ی مینیمال بهتر است:

background.js let ws = null;

function connect() { if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;

ws = new WebSocket("ws://127.0.0.1:8765");

ws.onopen = () => console.log("Bridge connected");

ws.onclose = () => { console.log("Bridge disconnected"); setTimeout(connect, 3000); };

ws.onerror = () => { try { ws.close(); } catch {} }; }

setInterval(() => { if (ws?.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "PING", ts: Date.now() })); } }, 20000);

chrome.runtime.onMessage.addListener((message) => { if (message.type !== "TRANSCRIPT_UPDATE") return;

connect();

if (ws?.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "TRANSCRIPT_CHUNK", text: message.text, ts: Date.now() })); } });

connect(); bridge.js const { WebSocketServer } = require("ws"); const fs = require("fs");

const wss = new WebSocketServer({ port: 8765 }); let fullText = "";

console.log("Bridge listening on ws://127.0.0.1:8765");

wss.on("connection", (ws) => { console.log("Extension connected");

ws.on("message", (raw) => { try { const data = JSON.parse(raw.toString());

  if (data.type === "PING") return;
  if (data.type !== "TRANSCRIPT_CHUNK") return;
  if (!data.text?.trim()) return;

  fullText += (fullText ? "\n" : "") + data.text.trim();

  console.log(">", data.text.trim());

  fs.writeFileSync("transcript_mvp.txt", fullText, "utf8");
} catch (err) {
  console.error("Bad message:", raw.toString());
}

}); }); نصب و اجرا npm init -y npm install ws node bridge.js نکته‌ی مهم Manifest

در manifest.json حداقل این permission/host را لازم داری:

{ "permissions": ["scripting"], "host_permissions": [ "ws://127.0.0.1:8765/*" ] }

اگر از قبل content script و background داری، فقط WebSocket و host permission را اضافه کن.

جمع‌بندی: برای MVP فقط ثابت کن که متن از پلاگین می‌رسد بیرون مرورگر و داخل فایل ذخیره می‌شود. بعداً اگر جواب داد، MCP را اضافه کن.