ghost-bridge 0.9.0 → 0.9.1
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.
- package/README.md +12 -4
- package/dist/cli.js +59 -0
- package/dist/server.js +157 -84
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,18 +147,26 @@ Read the next case from target cases, operate target app, then mark the case pas
|
|
|
147
147
|
```mermaid
|
|
148
148
|
flowchart LR
|
|
149
149
|
A["AI Client<br/>Claude / Codex / Cursor"]
|
|
150
|
-
|
|
150
|
+
S["Session Process<br/>dist/server.js (stdio)"]
|
|
151
|
+
B["Ghost Bridge Daemon<br/>resident WebSocket service"]
|
|
151
152
|
C["Chrome Extension<br/>background.js"]
|
|
152
153
|
D["Browser Tabs<br/>Target Sessions"]
|
|
153
154
|
|
|
154
|
-
A <-->|"stdio"|
|
|
155
|
-
|
|
155
|
+
A <-->|"stdio"| S
|
|
156
|
+
S <-->|"WebSocket (mcp-client)"| B
|
|
157
|
+
C <-->|"WebSocket"| B
|
|
156
158
|
C <-->|"CDP"| D
|
|
157
159
|
```
|
|
158
160
|
|
|
161
|
+
The WebSocket service runs as a detached daemon, independent of any MCP session:
|
|
162
|
+
|
|
163
|
+
- The first session process spawns the daemon automatically; it keeps running after that session exits
|
|
164
|
+
- If the daemon crashes, any live session detects it and respawns it automatically
|
|
165
|
+
- Stop it manually with `ghost-bridge stop` (close live MCP sessions first, otherwise they will bring it back on their next reconnect)
|
|
166
|
+
|
|
159
167
|
## Troubleshooting
|
|
160
168
|
|
|
161
|
-
If the popup shows `No Bridge` / `Not Found`, it means the Chrome extension could not find a Ghost Bridge WebSocket service on the configured port.
|
|
169
|
+
If the popup shows `No Bridge` / `Not Found`, it means the Chrome extension could not find a Ghost Bridge WebSocket service on the configured port. With the resident daemon this normally only happens before the first MCP session of the day starts, or after `ghost-bridge stop`. Starting any MCP session (or reconnecting the extension) brings the service back within seconds.
|
|
162
170
|
|
|
163
171
|
Run `ghost-bridge status` and check:
|
|
164
172
|
|
package/dist/cli.js
CHANGED
|
@@ -6010,6 +6010,7 @@ init_source();
|
|
|
6010
6010
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6011
6011
|
import path5 from "path";
|
|
6012
6012
|
import fs5 from "fs";
|
|
6013
|
+
import os7 from "os";
|
|
6013
6014
|
var __filename = fileURLToPath2(import.meta.url);
|
|
6014
6015
|
var __dirname = path5.dirname(__filename);
|
|
6015
6016
|
var packageJsonParams = JSON.parse(
|
|
@@ -6062,4 +6063,62 @@ program2.command("status").description("Check Ghost Bridge configuration status"
|
|
|
6062
6063
|
console.error(source_default.red("Error checking status:"), error);
|
|
6063
6064
|
}
|
|
6064
6065
|
});
|
|
6066
|
+
var sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6067
|
+
function isProcessAlive(pid) {
|
|
6068
|
+
try {
|
|
6069
|
+
process.kill(pid, 0);
|
|
6070
|
+
return true;
|
|
6071
|
+
} catch (e) {
|
|
6072
|
+
return e.code === "EPERM";
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
6075
|
+
function readPortInfoFile() {
|
|
6076
|
+
const portInfoFile = process.env.GHOST_BRIDGE_PORT_INFO || path5.join(os7.tmpdir(), "ghost-bridge-port.json");
|
|
6077
|
+
if (!fs5.existsSync(portInfoFile)) return null;
|
|
6078
|
+
try {
|
|
6079
|
+
const info = JSON.parse(fs5.readFileSync(portInfoFile, "utf-8"));
|
|
6080
|
+
return info && info.pid ? { file: portInfoFile, ...info } : null;
|
|
6081
|
+
} catch {
|
|
6082
|
+
return null;
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6085
|
+
function removePortInfoFile(entry) {
|
|
6086
|
+
try {
|
|
6087
|
+
const current = JSON.parse(fs5.readFileSync(entry.file, "utf-8"));
|
|
6088
|
+
if (current.pid === entry.pid) fs5.unlinkSync(entry.file);
|
|
6089
|
+
} catch {
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
program2.command("stop").description("Stop the resident ghost-bridge daemon").action(async () => {
|
|
6093
|
+
try {
|
|
6094
|
+
const entry = readPortInfoFile();
|
|
6095
|
+
if (!entry) {
|
|
6096
|
+
console.log(source_default.yellow("\u672A\u53D1\u73B0\u8FD0\u884C\u4E2D\u7684 ghost-bridge \u5E38\u9A7B\u670D\u52A1"));
|
|
6097
|
+
return;
|
|
6098
|
+
}
|
|
6099
|
+
if (!isProcessAlive(entry.pid)) {
|
|
6100
|
+
removePortInfoFile(entry);
|
|
6101
|
+
console.log(source_default.yellow(`\u7AEF\u53E3\u4FE1\u606F\u5DF2\u8FC7\u671F\uFF08PID ${entry.pid} \u4E0D\u5B58\u5728\uFF09\uFF0C\u5DF2\u6E05\u7406`));
|
|
6102
|
+
return;
|
|
6103
|
+
}
|
|
6104
|
+
console.log(source_default.blue(`\u6B63\u5728\u505C\u6B62 ghost-bridge \u5E38\u9A7B\u670D\u52A1 (PID: ${entry.pid})...`));
|
|
6105
|
+
process.kill(entry.pid, "SIGTERM");
|
|
6106
|
+
const deadline = Date.now() + 5e3;
|
|
6107
|
+
while (Date.now() < deadline && isProcessAlive(entry.pid)) {
|
|
6108
|
+
await sleepMs(200);
|
|
6109
|
+
}
|
|
6110
|
+
if (isProcessAlive(entry.pid)) {
|
|
6111
|
+
console.error(source_default.red("\u670D\u52A1\u672A\u80FD\u5728\u9884\u671F\u65F6\u95F4\u5185\u9000\u51FA\uFF0C\u8BF7\u624B\u52A8\u68C0\u67E5\u8BE5\u8FDB\u7A0B"));
|
|
6112
|
+
process.exit(1);
|
|
6113
|
+
}
|
|
6114
|
+
removePortInfoFile(entry);
|
|
6115
|
+
console.log(source_default.green("\u2705 ghost-bridge \u5E38\u9A7B\u670D\u52A1\u5DF2\u505C\u6B62"));
|
|
6116
|
+
console.log(
|
|
6117
|
+
source_default.dim("\u6CE8\u610F\uFF1A\u82E5\u4ECD\u6709\u6D3B\u8DC3\u7684 MCP \u4F1A\u8BDD\uFF0C\u4F1A\u8BDD\u4F1A\u5728\u4E0B\u6B21\u91CD\u8FDE\u65F6\u81EA\u52A8\u91CD\u65B0\u62C9\u8D77\u670D\u52A1\uFF1B\u5982\u9700\u5F7B\u5E95\u505C\u6B62\uFF0C\u8BF7\u5148\u5173\u95ED\u4F7F\u7528\u4E2D\u7684\u4F1A\u8BDD\u3002")
|
|
6118
|
+
);
|
|
6119
|
+
} catch (error) {
|
|
6120
|
+
console.error(source_default.red("Error stopping ghost-bridge:"), error);
|
|
6121
|
+
process.exit(1);
|
|
6122
|
+
}
|
|
6123
|
+
});
|
|
6065
6124
|
program2.parse();
|
package/dist/server.js
CHANGED
|
@@ -27102,8 +27102,8 @@ var Protocol = class {
|
|
|
27102
27102
|
*
|
|
27103
27103
|
* The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
|
|
27104
27104
|
*/
|
|
27105
|
-
async connect(
|
|
27106
|
-
this._transport =
|
|
27105
|
+
async connect(transport) {
|
|
27106
|
+
this._transport = transport;
|
|
27107
27107
|
const _onclose = this.transport?.onclose;
|
|
27108
27108
|
this._transport.onclose = () => {
|
|
27109
27109
|
_onclose?.();
|
|
@@ -28523,6 +28523,7 @@ import net from "net";
|
|
|
28523
28523
|
import fs2 from "fs";
|
|
28524
28524
|
import os from "os";
|
|
28525
28525
|
import path2 from "path";
|
|
28526
|
+
import { spawn } from "child_process";
|
|
28526
28527
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
28527
28528
|
|
|
28528
28529
|
// lib/version.js
|
|
@@ -28540,9 +28541,10 @@ var BASE_PORT = Number(process.env.GHOST_BRIDGE_PORT || 33333);
|
|
|
28540
28541
|
var DEFAULT_WS_TOKEN = "ghost-bridge-local";
|
|
28541
28542
|
var WS_TOKEN = process.env.GHOST_BRIDGE_TOKEN || DEFAULT_WS_TOKEN;
|
|
28542
28543
|
var RESPONSE_TIMEOUT = 8e3;
|
|
28543
|
-
var PORT_INFO_FILE = path2.join(os.tmpdir(), "ghost-bridge-port.json");
|
|
28544
|
+
var PORT_INFO_FILE = process.env.GHOST_BRIDGE_PORT_INFO || path2.join(os.tmpdir(), "ghost-bridge-port.json");
|
|
28544
28545
|
var SERVER_STARTED_AT = (/* @__PURE__ */ new Date()).toISOString();
|
|
28545
28546
|
var SERVER_ENTRY_PATH = fileURLToPath2(import.meta.url);
|
|
28547
|
+
var IS_DAEMON = process.env.GHOST_BRIDGE_DAEMON === "1";
|
|
28546
28548
|
var chromeConnection = null;
|
|
28547
28549
|
var activeConnection = null;
|
|
28548
28550
|
var actualPort = BASE_PORT;
|
|
@@ -28698,17 +28700,24 @@ function isPortAvailable(port) {
|
|
|
28698
28700
|
server2.listen(port);
|
|
28699
28701
|
});
|
|
28700
28702
|
}
|
|
28701
|
-
|
|
28702
|
-
|
|
28703
|
-
|
|
28704
|
-
|
|
28705
|
-
|
|
28706
|
-
|
|
28707
|
-
|
|
28708
|
-
|
|
28709
|
-
|
|
28703
|
+
function startWebSocketServer(port) {
|
|
28704
|
+
return new Promise((resolve, reject) => {
|
|
28705
|
+
let settled = false;
|
|
28706
|
+
const wss2 = new import_websocket_server.default({ port }, () => {
|
|
28707
|
+
if (settled) return;
|
|
28708
|
+
settled = true;
|
|
28709
|
+
wss2.removeListener("error", onError);
|
|
28710
|
+
resolve(wss2);
|
|
28711
|
+
});
|
|
28712
|
+
const onError = (err) => {
|
|
28713
|
+
if (settled) return;
|
|
28714
|
+
settled = true;
|
|
28715
|
+
reject(err);
|
|
28716
|
+
};
|
|
28717
|
+
wss2.on("error", onError);
|
|
28718
|
+
});
|
|
28710
28719
|
}
|
|
28711
|
-
async function
|
|
28720
|
+
async function acquirePortForDaemon() {
|
|
28712
28721
|
const existing = getExistingService();
|
|
28713
28722
|
if (existing) {
|
|
28714
28723
|
log(`\u68C0\u6D4B\u5230\u73B0\u6709\u670D\u52A1 (PID: ${existing.pid}, \u7AEF\u53E3: ${existing.port})\uFF0C\u9A8C\u8BC1\u4E2D...`);
|
|
@@ -28718,17 +28727,15 @@ async function initWebSocketService() {
|
|
|
28718
28727
|
const mismatch = getServiceMismatch(probe);
|
|
28719
28728
|
if (!mismatch) {
|
|
28720
28729
|
actualPort = existing.port;
|
|
28721
|
-
|
|
28722
|
-
|
|
28723
|
-
return null;
|
|
28730
|
+
log(`\u2705 \u5DF2\u6709\u5E38\u9A7B\u670D\u52A1\u5728\u8FD0\u884C\uFF0C\u65E0\u9700\u91CD\u590D\u542F\u52A8`);
|
|
28731
|
+
return { alreadyRunning: true };
|
|
28724
28732
|
}
|
|
28725
28733
|
await stopExistingService(Number(probe.pid) || existing.pid, existing.port, mismatch);
|
|
28726
28734
|
} else {
|
|
28727
|
-
|
|
28728
|
-
await stopExistingService(oldPid, existing.port, "token \u4E0D\u4E00\u81F4");
|
|
28735
|
+
await stopExistingService(Number(probe.pid) || existing.pid, existing.port, "token \u4E0D\u4E00\u81F4");
|
|
28729
28736
|
}
|
|
28730
28737
|
} else {
|
|
28731
|
-
log(`\u274C \u73B0\u6709\u670D\u52A1\u9A8C\u8BC1\u5931\u8D25\uFF0C\
|
|
28738
|
+
log(`\u274C \u73B0\u6709\u670D\u52A1\u9A8C\u8BC1\u5931\u8D25\uFF0C\u51C6\u5907\u63A5\u7BA1...`);
|
|
28732
28739
|
try {
|
|
28733
28740
|
fs2.unlinkSync(PORT_INFO_FILE);
|
|
28734
28741
|
} catch {
|
|
@@ -28742,9 +28749,8 @@ async function initWebSocketService() {
|
|
|
28742
28749
|
const mismatch = getServiceMismatch(probe);
|
|
28743
28750
|
if (!mismatch) {
|
|
28744
28751
|
actualPort = BASE_PORT;
|
|
28745
|
-
|
|
28746
|
-
|
|
28747
|
-
return null;
|
|
28752
|
+
log(`\u2705 \u56FA\u5B9A\u7AEF\u53E3\u4E0A\u5DF2\u6709\u5E38\u9A7B\u670D\u52A1\u5728\u8FD0\u884C\uFF0C\u65E0\u9700\u91CD\u590D\u542F\u52A8`);
|
|
28753
|
+
return { alreadyRunning: true };
|
|
28748
28754
|
}
|
|
28749
28755
|
await stopExistingService(Number(probe.pid), BASE_PORT, mismatch);
|
|
28750
28756
|
} else {
|
|
@@ -28755,8 +28761,34 @@ async function initWebSocketService() {
|
|
|
28755
28761
|
throw new Error(`\u56FA\u5B9A\u7AEF\u53E3 ${BASE_PORT} \u5DF2\u88AB\u5176\u4ED6\u8FDB\u7A0B\u5360\u7528\uFF0C\u8BF7\u91CA\u653E\u8BE5\u7AEF\u53E3\u6216\u901A\u8FC7 GHOST_BRIDGE_PORT \u6307\u5B9A\u5176\u4ED6\u7AEF\u53E3`);
|
|
28756
28762
|
}
|
|
28757
28763
|
}
|
|
28758
|
-
|
|
28764
|
+
let wss2;
|
|
28765
|
+
try {
|
|
28766
|
+
wss2 = await startWebSocketServer(BASE_PORT);
|
|
28767
|
+
} catch (err) {
|
|
28768
|
+
const probe = await probeExistingService(BASE_PORT);
|
|
28769
|
+
if (probe?.service === "ghost-bridge" && probe.token === WS_TOKEN && !getServiceMismatch(probe)) {
|
|
28770
|
+
actualPort = BASE_PORT;
|
|
28771
|
+
log(`\u2705 \u53E6\u4E00\u4E2A\u5E38\u9A7B\u670D\u52A1\u5DF2\u62A2\u5148\u542F\u52A8\uFF0C\u672C\u8FDB\u7A0B\u9000\u51FA`);
|
|
28772
|
+
return { alreadyRunning: true };
|
|
28773
|
+
}
|
|
28774
|
+
throw err;
|
|
28775
|
+
}
|
|
28776
|
+
log(`\u{1F680} WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 ${BASE_PORT}${WS_TOKEN ? "\uFF08\u542F\u7528 token \u6821\u9A8C\uFF09" : ""}`);
|
|
28777
|
+
actualPort = BASE_PORT;
|
|
28759
28778
|
isMainInstance = true;
|
|
28779
|
+
return { wss: wss2 };
|
|
28780
|
+
}
|
|
28781
|
+
async function runDaemon() {
|
|
28782
|
+
let acquired;
|
|
28783
|
+
try {
|
|
28784
|
+
acquired = await acquirePortForDaemon();
|
|
28785
|
+
} catch (e) {
|
|
28786
|
+
log(`\u274C daemon \u542F\u52A8\u5931\u8D25\uFF1A${e.message}`);
|
|
28787
|
+
process.exit(1);
|
|
28788
|
+
}
|
|
28789
|
+
if (acquired.alreadyRunning) {
|
|
28790
|
+
process.exit(0);
|
|
28791
|
+
}
|
|
28760
28792
|
fs2.writeFileSync(
|
|
28761
28793
|
PORT_INFO_FILE,
|
|
28762
28794
|
JSON.stringify({
|
|
@@ -28769,9 +28801,47 @@ async function initWebSocketService() {
|
|
|
28769
28801
|
}, null, 2)
|
|
28770
28802
|
);
|
|
28771
28803
|
log(`\u{1F4DD} \u7AEF\u53E3\u4FE1\u606F\u5DF2\u5199\u5165: ${PORT_INFO_FILE}`);
|
|
28772
|
-
return
|
|
28804
|
+
return acquired.wss;
|
|
28805
|
+
}
|
|
28806
|
+
async function ensureDaemonRunning() {
|
|
28807
|
+
const existing = getExistingService();
|
|
28808
|
+
if (existing) {
|
|
28809
|
+
const probe = await probeExistingService(existing.port);
|
|
28810
|
+
if (probe?.service === "ghost-bridge" && probe.token === WS_TOKEN && !getServiceMismatch(probe)) {
|
|
28811
|
+
actualPort = Number(probe.port) || existing.port;
|
|
28812
|
+
return true;
|
|
28813
|
+
}
|
|
28814
|
+
} else {
|
|
28815
|
+
const probe = await probeExistingService(BASE_PORT);
|
|
28816
|
+
if (probe?.service === "ghost-bridge" && probe.token === WS_TOKEN && !getServiceMismatch(probe)) {
|
|
28817
|
+
actualPort = BASE_PORT;
|
|
28818
|
+
return true;
|
|
28819
|
+
}
|
|
28820
|
+
}
|
|
28821
|
+
return spawnDaemonAndWait();
|
|
28822
|
+
}
|
|
28823
|
+
async function spawnDaemonAndWait(timeoutMs = 15e3) {
|
|
28824
|
+
log("\u{1F6E0} \u5E38\u9A7B\u670D\u52A1\u672A\u8FD0\u884C\uFF0C\u6B63\u5728\u542F\u52A8 daemon...");
|
|
28825
|
+
const child = spawn(process.execPath, [SERVER_ENTRY_PATH], {
|
|
28826
|
+
detached: true,
|
|
28827
|
+
stdio: "ignore",
|
|
28828
|
+
env: { ...process.env, GHOST_BRIDGE_DAEMON: "1" }
|
|
28829
|
+
});
|
|
28830
|
+
child.unref();
|
|
28831
|
+
const deadline = Date.now() + timeoutMs;
|
|
28832
|
+
while (Date.now() < deadline) {
|
|
28833
|
+
const probe = await probeExistingService(BASE_PORT);
|
|
28834
|
+
if (probe?.service === "ghost-bridge" && probe.token === WS_TOKEN && !getServiceMismatch(probe)) {
|
|
28835
|
+
actualPort = Number(probe.port) || BASE_PORT;
|
|
28836
|
+
log(`\u2705 daemon \u5DF2\u5C31\u7EEA (PID: ${probe.pid}, \u7AEF\u53E3: ${actualPort})`);
|
|
28837
|
+
return true;
|
|
28838
|
+
}
|
|
28839
|
+
await sleep(250);
|
|
28840
|
+
}
|
|
28841
|
+
log(`\u274C daemon \u672A\u80FD\u5728 ${timeoutMs}ms \u5185\u5C31\u7EEA\uFF0C\u7A0D\u540E\u5C06\u91CD\u8BD5`);
|
|
28842
|
+
return false;
|
|
28773
28843
|
}
|
|
28774
|
-
var wss = await
|
|
28844
|
+
var wss = IS_DAEMON ? await runDaemon() : null;
|
|
28775
28845
|
var wsPingInterval = null;
|
|
28776
28846
|
var WS_PING_INTERVAL_MS = 3e4;
|
|
28777
28847
|
if (wss) {
|
|
@@ -28913,22 +28983,22 @@ if (wss) {
|
|
|
28913
28983
|
}
|
|
28914
28984
|
}, WS_PING_INTERVAL_MS);
|
|
28915
28985
|
} else {
|
|
28916
|
-
|
|
28986
|
+
const ready = await ensureDaemonRunning();
|
|
28987
|
+
if (!ready) log("\u26A0\uFE0F \u5E38\u9A7B\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u5C06\u4FDD\u6301\u540E\u53F0\u91CD\u8BD5\u5E76\u81EA\u52A8\u62C9\u8D77");
|
|
28988
|
+
log(`\u{1F4E1} \u4F5C\u4E3A\u5BA2\u6237\u7AEF\u8FDE\u63A5\u5230\u5E38\u9A7B\u670D\u52A1 (\u7AEF\u53E3: ${actualPort})...`);
|
|
28917
28989
|
connectToMainInstance();
|
|
28918
28990
|
}
|
|
28919
|
-
var MAX_RECONNECT_ATTEMPTS = 10;
|
|
28920
28991
|
var RECONNECT_INTERVAL = 3e3;
|
|
28921
|
-
var
|
|
28922
|
-
var
|
|
28992
|
+
var DAEMON_CHECK_EVERY = 5;
|
|
28993
|
+
var consecutiveFailures = 0;
|
|
28923
28994
|
function connectToMainInstance() {
|
|
28924
28995
|
const url2 = new URL(`ws://localhost:${actualPort}`);
|
|
28925
28996
|
url2.searchParams.set("token", WS_TOKEN);
|
|
28926
28997
|
url2.searchParams.set("role", "mcp-client");
|
|
28927
28998
|
const ws = new import_websocket.default(url2.toString());
|
|
28928
28999
|
ws.on("open", () => {
|
|
28929
|
-
log(`\u2705 \u5DF2\u8FDE\u63A5\u5230\
|
|
28930
|
-
|
|
28931
|
-
wasEverConnected = true;
|
|
29000
|
+
log(`\u2705 \u5DF2\u8FDE\u63A5\u5230\u5E38\u9A7B\u670D\u52A1 (\u7AEF\u53E3: ${actualPort})`);
|
|
29001
|
+
consecutiveFailures = 0;
|
|
28932
29002
|
});
|
|
28933
29003
|
ws.on("message", (data) => {
|
|
28934
29004
|
try {
|
|
@@ -28943,30 +29013,25 @@ function connectToMainInstance() {
|
|
|
28943
29013
|
}
|
|
28944
29014
|
});
|
|
28945
29015
|
ws.on("close", () => {
|
|
28946
|
-
log("\u26A0\uFE0F \u4E0E\
|
|
29016
|
+
log("\u26A0\uFE0F \u4E0E\u5E38\u9A7B\u670D\u52A1\u7684\u8FDE\u63A5\u5DF2\u65AD\u5F00");
|
|
28947
29017
|
activeConnection = null;
|
|
28948
|
-
failAllPending("\u4E0E\
|
|
28949
|
-
|
|
28950
|
-
|
|
28951
|
-
|
|
28952
|
-
|
|
28953
|
-
|
|
28954
|
-
|
|
28955
|
-
|
|
28956
|
-
|
|
28957
|
-
|
|
28958
|
-
|
|
29018
|
+
failAllPending("\u4E0E\u5E38\u9A7B\u670D\u52A1\u7684\u8FDE\u63A5\u5DF2\u65AD\u5F00");
|
|
29019
|
+
consecutiveFailures++;
|
|
29020
|
+
const attempt = consecutiveFailures;
|
|
29021
|
+
const needDaemonCheck = attempt % DAEMON_CHECK_EVERY === 0;
|
|
29022
|
+
setTimeout(async () => {
|
|
29023
|
+
if (activeConnection) return;
|
|
29024
|
+
if (needDaemonCheck) {
|
|
29025
|
+
log(`\u{1F504} \u8FDE\u7EED ${attempt} \u6B21\u672A\u8FDE\u4E0A\u5E38\u9A7B\u670D\u52A1\uFF0C\u68C0\u67E5\u5E76\u5C1D\u8BD5\u62C9\u8D77...`);
|
|
29026
|
+
await ensureDaemonRunning();
|
|
29027
|
+
}
|
|
29028
|
+
if (activeConnection) return;
|
|
29029
|
+
log(`\u{1F504} \u5C1D\u8BD5\u91CD\u65B0\u8FDE\u63A5\u5230\u5E38\u9A7B\u670D\u52A1 (\u7B2C ${attempt} \u6B21)...`);
|
|
29030
|
+
connectToMainInstance();
|
|
28959
29031
|
}, RECONNECT_INTERVAL);
|
|
28960
29032
|
});
|
|
28961
29033
|
ws.on("error", (err) => {
|
|
28962
|
-
log(`\u274C \u8FDE\u63A5\
|
|
28963
|
-
if (!wasEverConnected) {
|
|
28964
|
-
reconnectAttempts++;
|
|
28965
|
-
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
28966
|
-
log(`\u274C \u65E0\u6CD5\u8FDE\u63A5\u5230\u4E3B\u5B9E\u4F8B\uFF0C\u5BA2\u6237\u7AEF\u8FDB\u7A0B\u9000\u51FA`);
|
|
28967
|
-
process.exit(0);
|
|
28968
|
-
}
|
|
28969
|
-
}
|
|
29034
|
+
log(`\u274C \u8FDE\u63A5\u5E38\u9A7B\u670D\u52A1\u5931\u8D25: ${err.message}`);
|
|
28970
29035
|
});
|
|
28971
29036
|
}
|
|
28972
29037
|
function failAllPending(message) {
|
|
@@ -29001,7 +29066,7 @@ function handleIncoming(data) {
|
|
|
29001
29066
|
else resolve(result);
|
|
29002
29067
|
}
|
|
29003
29068
|
async function askMainInstance(command, params = {}) {
|
|
29004
|
-
if (!activeConnection) throw new Error("\u672A\u8FDE\u63A5\u5230\
|
|
29069
|
+
if (!activeConnection) throw new Error("\u672A\u8FDE\u63A5\u5230\u5E38\u9A7B\u670D\u52A1");
|
|
29005
29070
|
const id = crypto.randomUUID();
|
|
29006
29071
|
const payload = { id, command, params };
|
|
29007
29072
|
return new Promise((resolve, reject) => {
|
|
@@ -29020,7 +29085,11 @@ async function askMainInstance(command, params = {}) {
|
|
|
29020
29085
|
});
|
|
29021
29086
|
}
|
|
29022
29087
|
async function askChrome(command, params = {}, options = {}) {
|
|
29023
|
-
if (!activeConnection)
|
|
29088
|
+
if (!activeConnection) {
|
|
29089
|
+
throw new Error(
|
|
29090
|
+
IS_DAEMON ? "Chrome \u672A\u8FDE\u63A5\uFF0C\u8BF7\u786E\u8BA4\u6D4F\u89C8\u5668\u5F00\u542F\u4E14\u6269\u5C55\u5DF2\u542F\u7528" : "ghost-bridge \u5E38\u9A7B\u670D\u52A1\u672A\u8FDE\u63A5\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF08\u53EF\u7528 ghost-bridge status \u68C0\u67E5\uFF09"
|
|
29091
|
+
);
|
|
29092
|
+
}
|
|
29024
29093
|
const id = crypto.randomUUID();
|
|
29025
29094
|
const payload = { id, command, params, clientId: LOCAL_CLIENT_ID };
|
|
29026
29095
|
if (WS_TOKEN) payload.token = WS_TOKEN;
|
|
@@ -29498,7 +29567,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
29498
29567
|
text: jsonText({
|
|
29499
29568
|
service: "ghost-bridge",
|
|
29500
29569
|
version: GHOST_BRIDGE_VERSION,
|
|
29501
|
-
role: isMainInstance ? "\
|
|
29570
|
+
role: isMainInstance ? "daemon (\u5E38\u9A7B WebSocket \u670D\u52A1)" : "\u4F1A\u8BDD\u5BA2\u6237\u7AEF (\u8FDE\u63A5\u5E38\u9A7B\u670D\u52A1)",
|
|
29502
29571
|
wsPort: actualPort,
|
|
29503
29572
|
wsUrl: `ws://localhost:${actualPort}`,
|
|
29504
29573
|
pid: process.pid,
|
|
@@ -29685,38 +29754,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
29685
29754
|
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
29686
29755
|
}
|
|
29687
29756
|
});
|
|
29688
|
-
|
|
29689
|
-
|
|
29690
|
-
|
|
29691
|
-
log(`\u2705 MCP server \u5DF2\u542F\u52A8 | \u89D2\u8272:
|
|
29692
|
-
log(`\u{
|
|
29693
|
-
|
|
29694
|
-
|
|
29695
|
-
|
|
29696
|
-
|
|
29697
|
-
log("\u26A0\uFE0F stdin \u5DF2\u5173\u95ED\uFF0C\u7236\u8FDB\u7A0B\u53EF\u80FD\u5DF2\u9000\u51FA\uFF0C\u6B63\u5728\u9000\u51FA...");
|
|
29698
|
-
cleanup();
|
|
29699
|
-
process.exit(0);
|
|
29700
|
-
});
|
|
29701
|
-
process.stdin.on("close", () => {
|
|
29702
|
-
log("\u26A0\uFE0F stdin \u5DF2\u5173\u95ED\uFF0C\u6B63\u5728\u9000\u51FA...");
|
|
29703
|
-
cleanup();
|
|
29704
|
-
process.exit(0);
|
|
29705
|
-
});
|
|
29706
|
-
var parentCheckTimer = setInterval(() => {
|
|
29707
|
-
try {
|
|
29708
|
-
process.kill(parentPid, 0);
|
|
29709
|
-
} catch (e) {
|
|
29710
|
-
log(`\u26A0\uFE0F \u7236\u8FDB\u7A0B (PID: ${parentPid}) \u5DF2\u4E0D\u5B58\u5728\uFF0C\u6B63\u5728\u9000\u51FA...`);
|
|
29711
|
-
clearInterval(parentCheckTimer);
|
|
29757
|
+
if (!IS_DAEMON) {
|
|
29758
|
+
const transport = new StdioServerTransport();
|
|
29759
|
+
await server.connect(transport);
|
|
29760
|
+
log(`\u2705 MCP server \u5DF2\u542F\u52A8 | \u89D2\u8272: \u4F1A\u8BDD\u5BA2\u6237\u7AEF | \u5E38\u9A7B\u670D\u52A1\u7AEF\u53E3: ${actualPort} | PID: ${process.pid} | PPID: ${process.ppid}`);
|
|
29761
|
+
log(`\u{1F4A1} \u4F7F\u7528 get_server_info \u5DE5\u5177\u67E5\u770B\u8BE6\u7EC6\u72B6\u6001`);
|
|
29762
|
+
const PARENT_CHECK_INTERVAL = 5e3;
|
|
29763
|
+
const parentPid = process.ppid;
|
|
29764
|
+
process.stdin.on("end", () => {
|
|
29765
|
+
log("\u26A0\uFE0F stdin \u5DF2\u5173\u95ED\uFF0C\u7236\u8FDB\u7A0B\u53EF\u80FD\u5DF2\u9000\u51FA\uFF0C\u6B63\u5728\u9000\u51FA...");
|
|
29712
29766
|
cleanup();
|
|
29713
29767
|
process.exit(0);
|
|
29714
|
-
}
|
|
29715
|
-
|
|
29716
|
-
|
|
29768
|
+
});
|
|
29769
|
+
process.stdin.on("close", () => {
|
|
29770
|
+
log("\u26A0\uFE0F stdin \u5DF2\u5173\u95ED\uFF0C\u6B63\u5728\u9000\u51FA...");
|
|
29771
|
+
cleanup();
|
|
29772
|
+
process.exit(0);
|
|
29773
|
+
});
|
|
29774
|
+
const parentCheckTimer = setInterval(() => {
|
|
29775
|
+
try {
|
|
29776
|
+
process.kill(parentPid, 0);
|
|
29777
|
+
} catch (e) {
|
|
29778
|
+
log(`\u26A0\uFE0F \u7236\u8FDB\u7A0B (PID: ${parentPid}) \u5DF2\u4E0D\u5B58\u5728\uFF0C\u6B63\u5728\u9000\u51FA...`);
|
|
29779
|
+
clearInterval(parentCheckTimer);
|
|
29780
|
+
cleanup();
|
|
29781
|
+
process.exit(0);
|
|
29782
|
+
}
|
|
29783
|
+
}, PARENT_CHECK_INTERVAL);
|
|
29784
|
+
parentCheckTimer.unref();
|
|
29785
|
+
} else {
|
|
29786
|
+
log(`\u2705 ghost-bridge daemon \u5DF2\u542F\u52A8 | \u7AEF\u53E3: ${actualPort} | PID: ${process.pid}`);
|
|
29787
|
+
log(`\u{1F4C4} \u7AEF\u53E3\u4FE1\u606F\u6587\u4EF6: ${PORT_INFO_FILE}`);
|
|
29788
|
+
log(`\u{1F4A1} \u505C\u6B62\u670D\u52A1: ghost-bridge stop`);
|
|
29789
|
+
}
|
|
29717
29790
|
function cleanup() {
|
|
29718
29791
|
log("\u{1F9F9} \u6B63\u5728\u6E05\u7406...");
|
|
29719
|
-
if (
|
|
29792
|
+
if (IS_DAEMON) {
|
|
29720
29793
|
try {
|
|
29721
29794
|
if (fs2.existsSync(PORT_INFO_FILE)) {
|
|
29722
29795
|
const info = JSON.parse(fs2.readFileSync(PORT_INFO_FILE, "utf-8"));
|
|
@@ -29753,7 +29826,7 @@ process.on("SIGTERM", () => {
|
|
|
29753
29826
|
process.exit(0);
|
|
29754
29827
|
});
|
|
29755
29828
|
process.on("exit", () => {
|
|
29756
|
-
if (
|
|
29829
|
+
if (IS_DAEMON) {
|
|
29757
29830
|
try {
|
|
29758
29831
|
if (fs2.existsSync(PORT_INFO_FILE)) {
|
|
29759
29832
|
const info = JSON.parse(fs2.readFileSync(PORT_INFO_FILE, "utf-8"));
|
package/extension/manifest.json
CHANGED