@gecho-ai/gecho-bridge 1.1.26 → 1.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@gecho-ai/gecho-bridge",
3
- "version": "1.1.26",
3
+ "version": "1.1.28",
4
4
  "description": "TikTok Search Tool for OpenClaw"
5
5
  }
@@ -33854,7 +33854,7 @@ var require_package = __commonJS({
33854
33854
  "package.json"(exports2, module2) {
33855
33855
  module2.exports = {
33856
33856
  name: "@gecho-ai/gecho-bridge",
33857
- version: "1.1.26",
33857
+ version: "1.1.28",
33858
33858
  description: "TikTok Search and Insight tools for OpenClaw, connecting LLMs to TikTok via Chrome Extension.",
33859
33859
  main: "mcp-client.js",
33860
33860
  bin: {
@@ -33868,6 +33868,7 @@ var require_package = __commonJS({
33868
33868
  ".mcp.json",
33869
33869
  "openclaw.plugin.json",
33870
33870
  "skills",
33871
+ "distribution-skills",
33871
33872
  "README.md",
33872
33873
  "README.zh.md",
33873
33874
  "qywx.jpg"
@@ -34017,12 +34018,19 @@ function requestShutdown() {
34017
34018
  });
34018
34019
  }
34019
34020
  function startServiceDetached() {
34020
- const child = spawn("node", [SERVICE_PATH], {
34021
- detached: true,
34022
- stdio: "ignore"
34023
- // 静默启动,不占用当前终端
34021
+ const isWindows = process.platform === "win32";
34022
+ const child = spawn(process.execPath, [SERVICE_PATH], {
34023
+ // On Windows, detached creates a separate process group. It is unnecessary
34024
+ // after unref() and has proved unreliable for the browser launched by this
34025
+ // service when the parent is a desktop MCP host. Keep the Unix behaviour.
34026
+ detached: !isWindows,
34027
+ stdio: "ignore",
34028
+ windowsHide: true,
34029
+ // Make this equivalent to running `node server.js` from the package.
34030
+ cwd: __dirname
34024
34031
  });
34025
34032
  child.unref();
34033
+ return child;
34026
34034
  }
34027
34035
  async function waitForServiceDown() {
34028
34036
  let retries = 10;
package/dist/server.cjs CHANGED
@@ -3640,7 +3640,7 @@ var require_package = __commonJS({
3640
3640
  "package.json"(exports2, module2) {
3641
3641
  module2.exports = {
3642
3642
  name: "@gecho-ai/gecho-bridge",
3643
- version: "1.1.26",
3643
+ version: "1.1.28",
3644
3644
  description: "TikTok Search and Insight tools for OpenClaw, connecting LLMs to TikTok via Chrome Extension.",
3645
3645
  main: "mcp-client.js",
3646
3646
  bin: {
@@ -3654,6 +3654,7 @@ var require_package = __commonJS({
3654
3654
  ".mcp.json",
3655
3655
  "openclaw.plugin.json",
3656
3656
  "skills",
3657
+ "distribution-skills",
3657
3658
  "README.md",
3658
3659
  "README.zh.md",
3659
3660
  "qywx.jpg"
@@ -3699,9 +3700,10 @@ var { WebSocketServer } = require_ws();
3699
3700
  var http = require("http");
3700
3701
  var fs = require("fs");
3701
3702
  var path = require("path");
3703
+ var { spawn } = require("child_process");
3702
3704
  var packageJson = require_package();
3703
- var WS_PORT = 18792;
3704
- var HTTP_PORT = 18793;
3705
+ var WS_PORT = Number(process["env"].GECHO_WS_PORT || 18792);
3706
+ var HTTP_PORT = Number(process["env"].GECHO_HTTP_PORT || 18793);
3705
3707
  var SERVICE_PROTOCOL_VERSION = 2;
3706
3708
  var SERVICE_STARTED_AT = Date.now();
3707
3709
  var DEFAULT_DATA_DIR = path.join(__dirname, "data");
@@ -3709,14 +3711,32 @@ var JOBS_DIR = process["env"].GECHO_DATA_DIR || DEFAULT_DATA_DIR;
3709
3711
  var JOBS_STORE_PATH = path.join(JOBS_DIR, ".async_jobs.json");
3710
3712
  var JOB_DETAILS_DIR = path.join(JOBS_DIR, "jobs");
3711
3713
  var REQUEST_INDEX_PATH = path.join(JOBS_DIR, ".async_request_index.json");
3714
+ var BROWSER_CONNECTION_PATH = path.join(JOBS_DIR, ".browser_connection.json");
3712
3715
  var JOB_TTL_MS = Number(process["env"].GECHO_JOB_TTL_MS || 3 * 24 * 60 * 60 * 1e3);
3713
3716
  var MAX_PERSISTED_JOBS = Number(process["env"].GECHO_MAX_PERSISTED_JOBS || 2e3);
3714
3717
  var CLEANUP_INTERVAL_MS = Number(process["env"].GECHO_CLEANUP_INTERVAL_MS || 10 * 60 * 1e3);
3718
+ var EXTENSION_CONNECT_TIMEOUT_MS = Math.max(1e3, Number(process["env"].GECHO_EXTENSION_CONNECT_TIMEOUT_MS || 3e4));
3719
+ var EXTENSION_CONNECT_POLL_MS = 500;
3720
+ var EXTENSION_READY_GRACE_MS = Math.max(0, Number(process["env"].GECHO_EXTENSION_READY_GRACE_MS || 1500));
3721
+ var AUTO_LAUNCH_BROWSER = process["env"].GECHO_AUTO_LAUNCH_BROWSER !== "0";
3722
+ var AUTO_LAUNCH_BROWSER_DRY_RUN = process["env"].GECHO_AUTO_LAUNCH_BROWSER_DRY_RUN === "1";
3723
+ var AUTO_LAUNCH_BROWSER_COOLDOWN_MS = Math.max(0, Number(process["env"].GECHO_AUTO_LAUNCH_BROWSER_COOLDOWN_MS || 1e4));
3715
3724
  var extensionSocket = null;
3725
+ var wss = null;
3726
+ var lastExtensionConnectedAt = 0;
3727
+ var lastBrowserConnection = null;
3728
+ var browserLaunchPromise = null;
3729
+ var lastBrowserLaunchAt = 0;
3716
3730
  var pendingRequests = /* @__PURE__ */ new Map();
3717
3731
  var requestIdCounter = 1;
3718
3732
  var shuttingDown = false;
3719
3733
  var lastDispatchedRequestId = "";
3734
+ var BRIDGE_TRACE_LIMIT = 100;
3735
+ var bridgeTrace = [];
3736
+ function traceBridgeEvent(event, details = {}) {
3737
+ bridgeTrace.push({ at: Date.now(), event, ...details });
3738
+ if (bridgeTrace.length > BRIDGE_TRACE_LIMIT) bridgeTrace.shift();
3739
+ }
3720
3740
  var asyncJobs = /* @__PURE__ */ new Map();
3721
3741
  var requestIndex = /* @__PURE__ */ new Map();
3722
3742
  var ASYNC_ATTEMPT_TIMEOUT_MS = 36e4;
@@ -3738,6 +3758,27 @@ function ensureJobDetailsDirReady() {
3738
3758
  function getJobDetailPath(jobId) {
3739
3759
  return path.join(JOB_DETAILS_DIR, `${jobId}.json`);
3740
3760
  }
3761
+ function writeFileAtomic(targetPath, content) {
3762
+ const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
3763
+ fs.writeFileSync(tmpPath, content, "utf8");
3764
+ try {
3765
+ fs.renameSync(tmpPath, targetPath);
3766
+ } catch (e) {
3767
+ if (e && (e.code === "EEXIST" || e.code === "EPERM")) {
3768
+ try {
3769
+ fs.unlinkSync(targetPath);
3770
+ } catch (_ignore) {
3771
+ }
3772
+ fs.renameSync(tmpPath, targetPath);
3773
+ } else {
3774
+ try {
3775
+ fs.unlinkSync(tmpPath);
3776
+ } catch (_ignore) {
3777
+ }
3778
+ throw e;
3779
+ }
3780
+ }
3781
+ }
3741
3782
  function enqueueWrite(task, label = "write_task") {
3742
3783
  writeQueue = writeQueue.then(async () => {
3743
3784
  await task();
@@ -3768,6 +3809,267 @@ async function writeFileAtomicAsync(targetPath, content) {
3768
3809
  }
3769
3810
  }
3770
3811
  }
3812
+ function getOpenExtensionSocket() {
3813
+ if (extensionSocket && extensionSocket.readyState === 1) {
3814
+ return extensionSocket;
3815
+ }
3816
+ if (wss) {
3817
+ for (const socket of wss.clients) {
3818
+ if (socket.readyState === 1) {
3819
+ extensionSocket = socket;
3820
+ return socket;
3821
+ }
3822
+ }
3823
+ }
3824
+ return null;
3825
+ }
3826
+ function isExtensionConnected() {
3827
+ return !!getOpenExtensionSocket();
3828
+ }
3829
+ function normalizeBrowserName(value) {
3830
+ const normalized = String(value || "").trim().toLowerCase();
3831
+ if (normalized === "edge" || normalized === "microsoft-edge" || normalized === "msedge") return "edge";
3832
+ if (normalized === "chrome" || normalized === "google-chrome") return "chrome";
3833
+ return "";
3834
+ }
3835
+ function detectBrowserFromUserAgent(userAgent) {
3836
+ const value = String(userAgent || "");
3837
+ if (/\bEdg\//i.test(value)) return "edge";
3838
+ if (/\bChrome\//i.test(value) && !/\b(?:OPR|Edg)\//i.test(value)) return "chrome";
3839
+ return "";
3840
+ }
3841
+ function loadLastBrowserConnection() {
3842
+ try {
3843
+ if (!fs.existsSync(BROWSER_CONNECTION_PATH)) return null;
3844
+ const parsed = JSON.parse(fs.readFileSync(BROWSER_CONNECTION_PATH, "utf8") || "{}");
3845
+ const browser = normalizeBrowserName(parsed?.browser);
3846
+ if (!browser) return null;
3847
+ return {
3848
+ browser,
3849
+ detectedAt: Number(parsed.detectedAt || 0) || 0
3850
+ };
3851
+ } catch (e) {
3852
+ console.warn(`Failed to load last browser connection: ${e.message}`);
3853
+ return null;
3854
+ }
3855
+ }
3856
+ function persistLastBrowserConnection(browser) {
3857
+ const normalized = normalizeBrowserName(browser);
3858
+ if (!normalized) return;
3859
+ lastBrowserConnection = { browser: normalized, detectedAt: Date.now() };
3860
+ try {
3861
+ ensureJobsDirReady();
3862
+ writeFileAtomic(BROWSER_CONNECTION_PATH, JSON.stringify(lastBrowserConnection));
3863
+ } catch (e) {
3864
+ console.warn(`Failed to persist last browser connection: ${e.message}`);
3865
+ }
3866
+ }
3867
+ function findFirstExistingPath(paths) {
3868
+ return paths.find((candidate) => {
3869
+ try {
3870
+ return !!candidate && fs.existsSync(candidate);
3871
+ } catch (_e) {
3872
+ return false;
3873
+ }
3874
+ }) || "";
3875
+ }
3876
+ function findFirstCommand(commands) {
3877
+ const pathEntries = String(process["env"].PATH || "").split(path.delimiter).filter(Boolean);
3878
+ const extensions = process.platform === "win32" ? String(process["env"].PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";") : [""];
3879
+ for (const command of commands) {
3880
+ for (const entry of pathEntries) {
3881
+ for (const extension of extensions) {
3882
+ const executable = path.join(entry, process.platform === "win32" ? `${command}${extension}` : command);
3883
+ if (fs.existsSync(executable)) return executable;
3884
+ }
3885
+ }
3886
+ }
3887
+ return "";
3888
+ }
3889
+ function getWindowsBrowserExecutable(browser) {
3890
+ const normalized = normalizeBrowserName(browser);
3891
+ const executable = normalized === "edge" ? "msedge.exe" : "chrome.exe";
3892
+ const vendorPath = normalized === "edge" ? ["Microsoft", "Edge", "Application"] : ["Google", "Chrome", "Application"];
3893
+ const roots = [
3894
+ process["env"].LOCALAPPDATA,
3895
+ process["env"].PROGRAMFILES,
3896
+ process["env"]["PROGRAMFILES(X86)"]
3897
+ ].filter(Boolean);
3898
+ return findFirstExistingPath(roots.map((root) => path.join(root, ...vendorPath, executable)));
3899
+ }
3900
+ function isBrowserInstalled(browser) {
3901
+ const normalized = normalizeBrowserName(browser);
3902
+ if (!normalized) return false;
3903
+ if (process.platform === "darwin") {
3904
+ const appName = normalized === "edge" ? "Microsoft Edge.app" : "Google Chrome.app";
3905
+ return !!findFirstExistingPath([
3906
+ path.join("/Applications", appName),
3907
+ path.join("/System/Applications", appName)
3908
+ ]);
3909
+ }
3910
+ if (process.platform === "win32") {
3911
+ return !!getWindowsBrowserExecutable(normalized);
3912
+ }
3913
+ return !!findFirstCommand(
3914
+ normalized === "edge" ? ["microsoft-edge", "microsoft-edge-stable"] : ["google-chrome", "google-chrome-stable"]
3915
+ );
3916
+ }
3917
+ function getBrowserToLaunch() {
3918
+ const configured = normalizeBrowserName(process["env"].GECHO_BROWSER);
3919
+ if (configured) return configured;
3920
+ if (lastBrowserConnection?.browser) return lastBrowserConnection.browser;
3921
+ if (isBrowserInstalled("chrome")) return "chrome";
3922
+ if (isBrowserInstalled("edge")) return "edge";
3923
+ return "";
3924
+ }
3925
+ function getBrowserLaunchSpec(browser) {
3926
+ const normalized = normalizeBrowserName(browser);
3927
+ const appName = normalized === "edge" ? "Microsoft Edge" : "Google Chrome";
3928
+ if (!normalized) return null;
3929
+ if (process.platform === "darwin") {
3930
+ return {
3931
+ command: "open",
3932
+ args: ["-a", appName, "--args", "--new-window", "about:blank"]
3933
+ };
3934
+ }
3935
+ if (process.platform === "win32") {
3936
+ const browserPath = getWindowsBrowserExecutable(normalized);
3937
+ if (browserPath) {
3938
+ return {
3939
+ command: browserPath,
3940
+ args: ["--new-window", "about:blank"]
3941
+ };
3942
+ }
3943
+ const executable2 = normalized === "edge" ? "msedge" : "chrome";
3944
+ return {
3945
+ command: "cmd.exe",
3946
+ args: ["/d", "/s", "/c", "start", "", executable2, "--new-window", "about:blank"]
3947
+ };
3948
+ }
3949
+ const executable = findFirstCommand(
3950
+ normalized === "edge" ? ["microsoft-edge", "microsoft-edge-stable"] : ["google-chrome", "google-chrome-stable"]
3951
+ ) || (normalized === "edge" ? "microsoft-edge" : "google-chrome");
3952
+ return {
3953
+ command: executable,
3954
+ args: ["--new-window", "about:blank"]
3955
+ };
3956
+ }
3957
+ async function launchBrowserForExtension() {
3958
+ if (!AUTO_LAUNCH_BROWSER) {
3959
+ return { attempted: false, launched: false, reason: "disabled" };
3960
+ }
3961
+ const browser = getBrowserToLaunch();
3962
+ const spec = getBrowserLaunchSpec(browser);
3963
+ if (!spec) {
3964
+ return { attempted: false, launched: false, reason: "browser_unknown" };
3965
+ }
3966
+ if (browserLaunchPromise) return browserLaunchPromise;
3967
+ if (Date.now() - lastBrowserLaunchAt < AUTO_LAUNCH_BROWSER_COOLDOWN_MS) {
3968
+ return { attempted: false, launched: false, browser, reason: "cooldown" };
3969
+ }
3970
+ lastBrowserLaunchAt = Date.now();
3971
+ browserLaunchPromise = new Promise((resolve) => {
3972
+ if (AUTO_LAUNCH_BROWSER_DRY_RUN) {
3973
+ console.log(`\u{1F9EA} Browser launch dry run: ${browser}`);
3974
+ resolve({ attempted: true, launched: true, browser, dryRun: true });
3975
+ return;
3976
+ }
3977
+ let child;
3978
+ try {
3979
+ child = spawn(spec.command, spec.args, {
3980
+ // A detached Windows process starts in a separate process group. That
3981
+ // extra isolation is not needed for a GUI browser after unref(), and
3982
+ // can prevent an extension service worker from reconnecting when the
3983
+ // bridge itself was started by a desktop MCP host.
3984
+ detached: process.platform !== "win32",
3985
+ stdio: "ignore",
3986
+ windowsHide: true
3987
+ });
3988
+ } catch (e) {
3989
+ resolve({ attempted: true, launched: false, browser, reason: e.message });
3990
+ return;
3991
+ }
3992
+ child.once("error", (e) => {
3993
+ resolve({ attempted: true, launched: false, browser, reason: e.message });
3994
+ });
3995
+ child.once("spawn", () => {
3996
+ child.unref();
3997
+ console.log(`\u{1F310} Started ${browser} while waiting for the extension connection`);
3998
+ resolve({ attempted: true, launched: true, browser });
3999
+ });
4000
+ });
4001
+ try {
4002
+ return await browserLaunchPromise;
4003
+ } finally {
4004
+ browserLaunchPromise = null;
4005
+ }
4006
+ }
4007
+ function waitForExtensionConnection(timeoutMs = EXTENSION_CONNECT_TIMEOUT_MS) {
4008
+ return new Promise((resolve) => {
4009
+ if (isExtensionConnected()) return resolve(true);
4010
+ let waited = 0;
4011
+ const checkTimer = setInterval(() => {
4012
+ waited += EXTENSION_CONNECT_POLL_MS;
4013
+ if (isExtensionConnected()) {
4014
+ clearInterval(checkTimer);
4015
+ resolve(true);
4016
+ } else if (waited >= timeoutMs) {
4017
+ clearInterval(checkTimer);
4018
+ resolve(false);
4019
+ }
4020
+ }, EXTENSION_CONNECT_POLL_MS);
4021
+ });
4022
+ }
4023
+ async function waitForExtensionReadyGracePeriod() {
4024
+ const remainingMs = Math.max(0, EXTENSION_READY_GRACE_MS - (Date.now() - lastExtensionConnectedAt));
4025
+ if (remainingMs === 0) return isExtensionConnected();
4026
+ traceBridgeEvent("extension_ready_grace_wait_started", { remainingMs });
4027
+ await new Promise((resolve) => setTimeout(resolve, remainingMs));
4028
+ const connected = isExtensionConnected();
4029
+ traceBridgeEvent("extension_ready_grace_wait_finished", { connected });
4030
+ return connected;
4031
+ }
4032
+ async function ensureExtensionConnection(action) {
4033
+ if (isExtensionConnected()) {
4034
+ traceBridgeEvent("extension_already_connected", { action });
4035
+ return { connected: true, launch: { attempted: false, launched: false, reason: "already_connected" } };
4036
+ }
4037
+ traceBridgeEvent("extension_connection_wait_started", { action });
4038
+ const launch = await launchBrowserForExtension();
4039
+ console.log(
4040
+ `\u23F3 Extension not connected yet. Waiting up to ${EXTENSION_CONNECT_TIMEOUT_MS / 1e3}s (action: ${action}, browser: ${launch.browser || "unknown"})`
4041
+ );
4042
+ const connected = await waitForExtensionConnection();
4043
+ const ready = connected && await waitForExtensionReadyGracePeriod();
4044
+ traceBridgeEvent("extension_connection_wait_finished", {
4045
+ action,
4046
+ connected: ready,
4047
+ browser: launch.browser || "",
4048
+ launchReason: launch.reason || ""
4049
+ });
4050
+ return { connected: ready, launch };
4051
+ }
4052
+ function extensionConnectionError(connection) {
4053
+ const launch = connection?.launch || {};
4054
+ const details = [];
4055
+ if (launch.reason === "browser_unknown") {
4056
+ details.push("Bridge has not identified a browser yet. Open Edge or Chrome with the Gecho extension once, then retry.");
4057
+ } else if (launch.reason === "disabled") {
4058
+ details.push("Automatic browser launch is disabled (GECHO_AUTO_LAUNCH_BROWSER=0).");
4059
+ } else if (launch.reason === "cooldown") {
4060
+ details.push(`${launch.browser} was started recently; it did not reconnect in time.`);
4061
+ } else if (launch.attempted && !launch.launched) {
4062
+ details.push(`Failed to start ${launch.browser || "the saved browser"}: ${launch.reason || "unknown error"}`);
4063
+ } else if (launch.attempted) {
4064
+ details.push(`${launch.browser} was started, but its extension did not connect in time.`);
4065
+ }
4066
+ return [
4067
+ "Extension not connected.",
4068
+ ...details,
4069
+ "Check that the Gecho extension is installed, enabled, and signed in for that browser profile."
4070
+ ].join(" ");
4071
+ }
4072
+ lastBrowserConnection = loadLastBrowserConnection();
3771
4073
  function sanitizeJobForDisk(job = {}) {
3772
4074
  const copy = { ...job };
3773
4075
  delete copy.data;
@@ -4197,17 +4499,19 @@ function hydrateJobWithDataFromFile(job) {
4197
4499
  return job;
4198
4500
  }
4199
4501
  }
4200
- function runAsyncAttempt({ jobId, action, params, payload, attempt }) {
4201
- if (!extensionSocket || extensionSocket.readyState !== 1) {
4502
+ async function runAsyncAttempt({ jobId, action, params, payload, attempt }) {
4503
+ const connection = await ensureExtensionConnection(action);
4504
+ if (!connection.connected) {
4202
4505
  updateJob(jobId, {
4203
4506
  status: "error",
4204
4507
  stage: "extension_disconnected",
4205
- error: "Extension not connected during async dispatch"
4508
+ error: extensionConnectionError(connection)
4206
4509
  });
4207
4510
  return;
4208
4511
  }
4209
4512
  const requestId = `${jobId}:a${attempt}`;
4210
4513
  lastDispatchedRequestId = requestId;
4514
+ traceBridgeEvent("action_dispatched", { action, requestId, async: true });
4211
4515
  requestIndex.set(requestId, { jobId, attempt, updatedAt: Date.now() });
4212
4516
  schedulePersistRequestIndex();
4213
4517
  appendJobEvent(jobId, "attempt_started", { attempt, requestId });
@@ -4290,14 +4594,19 @@ function gracefulShutdown(reason) {
4290
4594
  }
4291
4595
  }
4292
4596
  }
4293
- var wss = new WebSocketServer({ port: WS_PORT, host: "127.0.0.1" });
4294
- wss.on("connection", (ws) => {
4295
- console.log("\u2705 Chrome Extension connected to Service Layer");
4597
+ wss = new WebSocketServer({ port: WS_PORT, host: "127.0.0.1" });
4598
+ wss.on("connection", (ws, request) => {
4599
+ const browser = detectBrowserFromUserAgent(request?.headers?.["user-agent"]);
4600
+ if (browser) persistLastBrowserConnection(browser);
4601
+ console.log(`\u2705 Browser extension connected to Service Layer${browser ? ` (${browser})` : ""}`);
4296
4602
  extensionSocket = ws;
4603
+ lastExtensionConnectedAt = Date.now();
4604
+ traceBridgeEvent("extension_connected", { browser, clientCount: wss.clients.size });
4297
4605
  ws.on("message", (message) => {
4298
4606
  try {
4299
4607
  const parsed = JSON.parse(message);
4300
4608
  if (parsed.method === "action_progress" && parsed.requestId) {
4609
+ traceBridgeEvent("extension_progress", { requestId: parsed.requestId });
4301
4610
  const pending = pendingRequests.get(parsed.requestId);
4302
4611
  if (!pending) {
4303
4612
  console.warn(`WARN action_progress requestId not found: ${parsed.requestId}`);
@@ -4317,6 +4626,7 @@ wss.on("connection", (ws) => {
4317
4626
  }
4318
4627
  }
4319
4628
  if (parsed.method === "action_result" && parsed.requestId) {
4629
+ traceBridgeEvent("extension_result", { requestId: parsed.requestId });
4320
4630
  console.log(`\u{1F4E9} Received result from extension (ID: ${parsed.requestId})`);
4321
4631
  const pending = pendingRequests.get(parsed.requestId);
4322
4632
  if (pending) {
@@ -4349,8 +4659,13 @@ wss.on("connection", (ws) => {
4349
4659
  }
4350
4660
  });
4351
4661
  ws.on("close", () => {
4352
- console.log("\u274C Chrome Extension disconnected");
4353
- extensionSocket = null;
4662
+ console.log("\u274C Browser extension disconnected");
4663
+ traceBridgeEvent("extension_disconnected", { clientCount: wss.clients.size });
4664
+ if (extensionSocket === ws) extensionSocket = null;
4665
+ });
4666
+ ws.on("error", (error) => {
4667
+ traceBridgeEvent("extension_socket_error", { message: error.message });
4668
+ console.warn(`Browser extension WebSocket error: ${error.message}`);
4354
4669
  });
4355
4670
  });
4356
4671
  loadAsyncJobsFromDisk();
@@ -4378,7 +4693,14 @@ var server = http.createServer(async (req, res) => {
4378
4693
  servicePath: __filename,
4379
4694
  startedAt: SERVICE_STARTED_AT,
4380
4695
  sourceMtimeMs: getServiceSourceMtimeMs(),
4381
- dataDir: JOBS_DIR
4696
+ dataDir: JOBS_DIR,
4697
+ extensionConnected: isExtensionConnected(),
4698
+ lastConnectedBrowser: lastBrowserConnection?.browser || null,
4699
+ autoBrowserLaunch: {
4700
+ enabled: AUTO_LAUNCH_BROWSER,
4701
+ browser: getBrowserToLaunch() || null
4702
+ },
4703
+ bridgeTrace: bridgeTrace.slice(-30)
4382
4704
  }));
4383
4705
  }
4384
4706
  if (req.method === "POST" && req.url === "/shutdown") {
@@ -4431,29 +4753,10 @@ var server = http.createServer(async (req, res) => {
4431
4753
  res.statusCode = 400;
4432
4754
  return res.end(JSON.stringify({ error: "Missing action" }));
4433
4755
  }
4434
- if (!extensionSocket || extensionSocket.readyState !== 1) {
4435
- console.log(`\u23F3 Extension not connected yet. Waiting for connection... (action: ${action})`);
4436
- const maxWaitMs = 15e3;
4437
- const checkIntervalMs = 500;
4438
- let waited = 0;
4439
- await new Promise((resolve) => {
4440
- const checkTimer = setInterval(() => {
4441
- waited += checkIntervalMs;
4442
- if (extensionSocket && extensionSocket.readyState === 1) {
4443
- clearInterval(checkTimer);
4444
- resolve();
4445
- } else if (waited >= maxWaitMs) {
4446
- clearInterval(checkTimer);
4447
- resolve();
4448
- }
4449
- }, checkIntervalMs);
4450
- });
4451
- if (!extensionSocket || extensionSocket.readyState !== 1) {
4452
- res.statusCode = 503;
4453
- return res.end(JSON.stringify({
4454
- error: "Extension not connected. Chrome \u63D2\u4EF6\u672A\u8FDE\u63A5\u3002\n\u8BF7\u68C0\u67E5\uFF1A\n1. Chrome \u662F\u5426\u4FDD\u6301\u6253\u5F00\u72B6\u6001\n2. Gecho TikTok Bridge \u63D2\u4EF6\u662F\u5426\u4ECD\u5728\u8FD0\u884C\uFF08\u6269\u5C55\u56FE\u6807\u662F\u5426\u4EAE\u7740\uFF09\n3. TikTok tab \u662F\u5426\u6D3B\u8DC3\uFF08\u6700\u597D\u5728 tiktok.com \u9875\u9762\u4E0A\uFF09\n\u5982\u679C\u63D2\u4EF6\u521A\u542F\u52A8\uFF0C\u8BF7\u7B49\u5F85\u51E0\u79D2\u540E\u518D\u8BD5\u3002"
4455
- }));
4456
- }
4756
+ const connection = await ensureExtensionConnection(action);
4757
+ if (!connection.connected) {
4758
+ res.statusCode = 503;
4759
+ return res.end(JSON.stringify({ error: extensionConnectionError(connection) }));
4457
4760
  }
4458
4761
  const jobId = `job-${Date.now()}-${requestIdCounter++}`;
4459
4762
  console.log(`\u{1F680} Dispatching ASYNC action: [${action}], jobId: ${jobId}`);
@@ -4509,33 +4812,15 @@ var server = http.createServer(async (req, res) => {
4509
4812
  res.statusCode = 400;
4510
4813
  return res.end(JSON.stringify({ error: "Missing action" }));
4511
4814
  }
4512
- if (!extensionSocket || extensionSocket.readyState !== 1) {
4513
- console.log(`\u23F3 Extension not connected yet. Waiting for connection... (action: ${action})`);
4514
- const maxWaitMs = 15e3;
4515
- const checkIntervalMs = 500;
4516
- let waited = 0;
4517
- await new Promise((resolve) => {
4518
- const checkTimer = setInterval(() => {
4519
- waited += checkIntervalMs;
4520
- if (extensionSocket && extensionSocket.readyState === 1) {
4521
- clearInterval(checkTimer);
4522
- resolve();
4523
- } else if (waited >= maxWaitMs) {
4524
- clearInterval(checkTimer);
4525
- resolve();
4526
- }
4527
- }, checkIntervalMs);
4528
- });
4529
- if (!extensionSocket || extensionSocket.readyState !== 1) {
4530
- res.statusCode = 503;
4531
- return res.end(JSON.stringify({
4532
- error: "Extension not connected. Chrome \u63D2\u4EF6\u672A\u8FDE\u63A5\u3002\n\u8BF7\u68C0\u67E5\uFF1A\n1. Chrome \u662F\u5426\u4FDD\u6301\u6253\u5F00\u72B6\u6001\n2. Gecho TikTok Bridge \u63D2\u4EF6\u662F\u5426\u4ECD\u5728\u8FD0\u884C\uFF08\u6269\u5C55\u56FE\u6807\u662F\u5426\u4EAE\u7740\uFF09\n3. TikTok tab \u662F\u5426\u6D3B\u8DC3\uFF08\u6700\u597D\u5728 tiktok.com \u9875\u9762\u4E0A\uFF09\n\u5982\u679C\u63D2\u4EF6\u521A\u542F\u52A8\uFF0C\u8BF7\u7B49\u5F85\u51E0\u79D2\u540E\u518D\u8BD5\u3002"
4533
- }));
4534
- }
4815
+ const connection = await ensureExtensionConnection(action);
4816
+ if (!connection.connected) {
4817
+ res.statusCode = 503;
4818
+ return res.end(JSON.stringify({ error: extensionConnectionError(connection) }));
4535
4819
  }
4536
4820
  console.log(`\u{1F680} Dispatching action: [${action}]`);
4537
4821
  const requestId = `svc-${Date.now()}-${requestIdCounter++}`;
4538
4822
  lastDispatchedRequestId = requestId;
4823
+ traceBridgeEvent("action_dispatched", { action, requestId, async: false });
4539
4824
  const { action: _a, ...params } = payload;
4540
4825
  const result = await new Promise((resolve) => {
4541
4826
  const timeoutId = setTimeout(() => {