remnote-bridge 0.1.8 → 0.1.10
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/dist/cli/commands/connect.js +3 -1
- package/dist/cli/commands/disconnect.js +5 -0
- package/dist/cli/daemon/headless-browser.js +88 -0
- package/dist/cli/daemon/static-server.js +4 -1
- package/dist/mcp/instructions.js +2 -0
- package/package.json +1 -1
- package/remnote-plugin/dist/index-sandbox.js +26 -26
- package/remnote-plugin/dist/index.js +26 -26
- package/remnote-plugin/src/services/breadcrumb.ts +2 -1
- package/remnote-plugin/src/services/read-context.ts +2 -2
- package/remnote-plugin/src/services/read-globe.ts +2 -1
- package/remnote-plugin/src/services/read-tree.ts +2 -2
- package/remnote-plugin/src/services/rem-builder.ts +53 -3
- package/remnote-plugin/src/services/search.ts +2 -1
- package/skills/remnote-bridge/SKILL.md +2 -0
|
@@ -14,7 +14,7 @@ import fs from 'fs';
|
|
|
14
14
|
import { fork } from 'child_process';
|
|
15
15
|
import { loadConfig, pidFilePath, findProjectRoot } from '../config.js';
|
|
16
16
|
import { checkDaemon } from '../daemon/pid.js';
|
|
17
|
-
import { getSetupDonePath } from '../daemon/headless-browser.js';
|
|
17
|
+
import { getSetupDonePath, cleanupOrphanChrome } from '../daemon/headless-browser.js';
|
|
18
18
|
import { jsonOutput } from '../utils/output.js';
|
|
19
19
|
function isDaemonMessage(msg) {
|
|
20
20
|
return (typeof msg === 'object' &&
|
|
@@ -28,6 +28,8 @@ export async function connectCommand(options = {}) {
|
|
|
28
28
|
const pidPath = pidFilePath(projectRoot);
|
|
29
29
|
// headless 前置检查
|
|
30
30
|
if (options.headless) {
|
|
31
|
+
// 清理上次可能残留的孤儿 Chrome 进程
|
|
32
|
+
cleanupOrphanChrome(json ? undefined : (msg) => console.log(msg));
|
|
31
33
|
const setupDonePath = getSetupDonePath();
|
|
32
34
|
if (!fs.existsSync(setupDonePath)) {
|
|
33
35
|
const error = '尚未完成 setup。请先执行 `remnote-bridge setup` 登录 RemNote,然后再使用 --headless';
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { pidFilePath, findProjectRoot } from '../config.js';
|
|
9
9
|
import { checkDaemon, removePid } from '../daemon/pid.js';
|
|
10
|
+
import { cleanupOrphanChrome } from '../daemon/headless-browser.js';
|
|
10
11
|
import { jsonOutput } from '../utils/output.js';
|
|
11
12
|
const WAIT_TIMEOUT_MS = 10_000;
|
|
12
13
|
const POLL_INTERVAL_MS = 200;
|
|
@@ -49,6 +50,8 @@ export async function disconnectCommand(options = {}) {
|
|
|
49
50
|
const exited = await waitForExit(pid, WAIT_TIMEOUT_MS);
|
|
50
51
|
if (exited) {
|
|
51
52
|
removePid(pidPath);
|
|
53
|
+
// 清理可能残留的孤儿 headless Chrome
|
|
54
|
+
cleanupOrphanChrome(json ? undefined : (msg) => console.log(msg));
|
|
52
55
|
if (json) {
|
|
53
56
|
jsonOutput({ ok: true, command: 'disconnect', wasRunning: true, pid, forced: false });
|
|
54
57
|
}
|
|
@@ -65,6 +68,8 @@ export async function disconnectCommand(options = {}) {
|
|
|
65
68
|
// 可能已退出
|
|
66
69
|
}
|
|
67
70
|
removePid(pidPath);
|
|
71
|
+
// daemon 被强杀后 Chrome 更可能成为孤儿,务必清理
|
|
72
|
+
cleanupOrphanChrome(json ? undefined : (msg) => console.log(msg));
|
|
68
73
|
if (json) {
|
|
69
74
|
jsonOutput({ ok: true, command: 'disconnect', wasRunning: true, pid, forced: true });
|
|
70
75
|
}
|
|
@@ -58,6 +58,87 @@ export function getDefaultProfileDir() {
|
|
|
58
58
|
export function getSetupDonePath() {
|
|
59
59
|
return path.join(getDefaultProfileDir(), '.setup-done');
|
|
60
60
|
}
|
|
61
|
+
// ── Chrome PID 文件管理(孤儿进程清理) ──
|
|
62
|
+
const HEADLESS_PID_FILENAME = '.headless-pid';
|
|
63
|
+
function getHeadlessPidPath() {
|
|
64
|
+
return path.join(os.homedir(), '.remnote-bridge', HEADLESS_PID_FILENAME);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 将 Chrome 进程 PID 写入文件,供孤儿清理使用。
|
|
68
|
+
*/
|
|
69
|
+
function writeHeadlessPid(pid) {
|
|
70
|
+
const filePath = getHeadlessPidPath();
|
|
71
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
72
|
+
fs.writeFileSync(filePath, String(pid), 'utf-8');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 删除 Chrome PID 文件。
|
|
76
|
+
*/
|
|
77
|
+
function removeHeadlessPid() {
|
|
78
|
+
try {
|
|
79
|
+
fs.unlinkSync(getHeadlessPidPath());
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// 文件不存在则忽略
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 清理孤儿 headless Chrome 进程。
|
|
87
|
+
* 读取 PID 文件,如果对应进程仍在运行则 kill,最后删除 PID 文件。
|
|
88
|
+
* 在 disconnect 和 connect --headless 启动前调用。
|
|
89
|
+
*/
|
|
90
|
+
export function cleanupOrphanChrome(onLog) {
|
|
91
|
+
const pidPath = getHeadlessPidPath();
|
|
92
|
+
let pidStr;
|
|
93
|
+
try {
|
|
94
|
+
pidStr = fs.readFileSync(pidPath, 'utf-8').trim();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return; // 无 PID 文件,无需清理
|
|
98
|
+
}
|
|
99
|
+
const pid = parseInt(pidStr, 10);
|
|
100
|
+
if (isNaN(pid)) {
|
|
101
|
+
removeHeadlessPid();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// 检查进程是否仍在运行
|
|
105
|
+
try {
|
|
106
|
+
process.kill(pid, 0); // 不发信号,仅检查存在性
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// 进程已不存在,清理 PID 文件即可
|
|
110
|
+
removeHeadlessPid();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// 进程仍在运行 → kill 它
|
|
114
|
+
onLog?.(`发现孤儿 headless Chrome 进程 (PID: ${pid}),正在清理...`);
|
|
115
|
+
try {
|
|
116
|
+
process.kill(pid, 'SIGTERM');
|
|
117
|
+
// 给 Chrome 1 秒优雅退出
|
|
118
|
+
const start = Date.now();
|
|
119
|
+
while (Date.now() - start < 1000) {
|
|
120
|
+
try {
|
|
121
|
+
process.kill(pid, 0);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
break; // 已退出
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 如果还没退出,强杀
|
|
128
|
+
try {
|
|
129
|
+
process.kill(pid, 0);
|
|
130
|
+
process.kill(pid, 'SIGKILL');
|
|
131
|
+
onLog?.(`孤儿 Chrome 进程 (PID: ${pid}) 已强制终止`);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
onLog?.(`孤儿 Chrome 进程 (PID: ${pid}) 已清理`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// kill 失败,可能已退出
|
|
139
|
+
}
|
|
140
|
+
removeHeadlessPid();
|
|
141
|
+
}
|
|
61
142
|
const MAX_AUTO_RELOAD = 5;
|
|
62
143
|
const AUTO_RELOAD_DELAY_MS = 10_000;
|
|
63
144
|
const CONSOLE_ERROR_BUFFER_SIZE = 20;
|
|
@@ -142,6 +223,12 @@ export class HeadlessBrowserManager {
|
|
|
142
223
|
timeout: 60_000,
|
|
143
224
|
});
|
|
144
225
|
this.status = 'running';
|
|
226
|
+
// 记录 Chrome PID 供孤儿清理使用
|
|
227
|
+
const chromePid = this.browser.process()?.pid;
|
|
228
|
+
if (chromePid) {
|
|
229
|
+
writeHeadlessPid(chromePid);
|
|
230
|
+
this.log(`[headless] Chrome PID: ${chromePid}`);
|
|
231
|
+
}
|
|
145
232
|
this.log(`[headless] Chrome 已启动,页面: ${this.options.remNoteUrl}`);
|
|
146
233
|
}
|
|
147
234
|
catch (err) {
|
|
@@ -181,6 +268,7 @@ export class HeadlessBrowserManager {
|
|
|
181
268
|
catch (err) {
|
|
182
269
|
this.log(`[headless] 关闭时出错: ${err}`, 'warn');
|
|
183
270
|
}
|
|
271
|
+
removeHeadlessPid();
|
|
184
272
|
this.log('[headless] Chrome 已关闭');
|
|
185
273
|
}
|
|
186
274
|
/**
|
|
@@ -52,7 +52,10 @@ export class StaticServer {
|
|
|
52
52
|
}
|
|
53
53
|
const ext = path.extname(filePath);
|
|
54
54
|
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
55
|
-
res.writeHead(200, {
|
|
55
|
+
res.writeHead(200, {
|
|
56
|
+
'Content-Type': contentType,
|
|
57
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
58
|
+
});
|
|
56
59
|
res.end(data);
|
|
57
60
|
});
|
|
58
61
|
});
|
package/dist/mcp/instructions.js
CHANGED
|
@@ -103,6 +103,8 @@ disconnect → 关闭 daemon,清空所有缓存
|
|
|
103
103
|
|
|
104
104
|
标准模式每次 connect 后都需要用户手动操作 RemNote。Headless 模式通过 setup(一次性)+ headless Chrome 实现自动连接,后续 connect 无需用户介入。
|
|
105
105
|
|
|
106
|
+
**⚠️ 模式选择建议**:日常使用推荐**标准模式**。Headless 模式下 Chrome 在后台运行,**无法感知用户正在 RemNote 中浏览和操作的界面**(\\\`read_context\\\` 返回的是 headless Chrome 的上下文,而非用户的浏览器)。只有在全自动化场景(CI/CD、定时任务、批量操作等无需与用户界面交互的场景)才建议使用 Headless 模式。
|
|
107
|
+
|
|
106
108
|
#### 首次使用(setup)
|
|
107
109
|
|
|
108
110
|
\\\`setup\\\` 会弹出 Chrome 窗口,用户需要完成两件事:
|