remnote-bridge 0.1.7 → 0.1.9
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 +30 -6
- package/dist/cli/commands/connect.js +31 -2
- package/dist/cli/commands/health.js +111 -1
- package/dist/cli/commands/setup.js +112 -0
- package/dist/cli/daemon/daemon.js +101 -20
- package/dist/cli/daemon/headless-browser.js +291 -0
- package/dist/cli/daemon/static-server.js +84 -0
- package/dist/cli/main.js +22 -6
- package/dist/cli/server/ws-server.js +62 -1
- package/dist/mcp/daemon-client.js +4 -1
- package/dist/mcp/instructions.js +54 -5
- package/dist/mcp/tools/infra-tools.js +39 -9
- package/package.json +5 -1
- package/remnote-plugin/dist/bridge-icon.svg +8 -0
- package/remnote-plugin/dist/bridge_widget-sandbox.js +65 -0
- package/remnote-plugin/dist/bridge_widget.js +65 -0
- package/remnote-plugin/dist/index-sandbox.css +591 -0
- package/remnote-plugin/dist/index-sandbox.js +64 -0
- package/remnote-plugin/dist/index.css +591 -0
- package/remnote-plugin/dist/index.html +9 -0
- package/remnote-plugin/dist/index.js +64 -0
- package/remnote-plugin/dist/manifest.json +22 -0
- package/skills/remnote-bridge/SKILL.md +50 -4
- package/skills/remnote-bridge/instructions/connect.md +40 -10
- package/skills/remnote-bridge/instructions/disconnect.md +1 -1
- package/skills/remnote-bridge/instructions/health.md +67 -1
- package/skills/remnote-bridge/instructions/overall.md +1 -1
- package/skills/remnote-bridge/instructions/setup.md +130 -0
package/README.md
CHANGED
|
@@ -43,12 +43,34 @@ Once connected, the AI will guide you through connecting to RemNote, loading the
|
|
|
43
43
|
|
|
44
44
|
## Quick Start
|
|
45
45
|
|
|
46
|
+
### Headless Mode (recommended for AI agents)
|
|
47
|
+
|
|
48
|
+
Zero human intervention after initial setup — ideal for automated workflows.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 1. One-time: login to RemNote in Chrome (saves credentials)
|
|
52
|
+
remnote-bridge setup
|
|
53
|
+
|
|
54
|
+
# 2. Auto-connect with headless Chrome (no browser window needed)
|
|
55
|
+
remnote-bridge connect --headless
|
|
56
|
+
|
|
57
|
+
# 3. Verify all layers are ready
|
|
58
|
+
remnote-bridge health
|
|
59
|
+
|
|
60
|
+
# 4. Use any command — done!
|
|
61
|
+
remnote-bridge search "machine learning"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Standard Mode
|
|
65
|
+
|
|
66
|
+
Requires user to manually load the plugin in RemNote.
|
|
67
|
+
|
|
46
68
|
```bash
|
|
47
|
-
# 1. Start the daemon (launches WS server + plugin
|
|
69
|
+
# 1. Start the daemon (launches WS server + plugin server)
|
|
48
70
|
remnote-bridge connect
|
|
49
71
|
|
|
50
72
|
# 2. Load the plugin in RemNote
|
|
51
|
-
# Open RemNote →
|
|
73
|
+
# Open RemNote → Plugins → Develop Your Plugin
|
|
52
74
|
# Enter: http://localhost:8080
|
|
53
75
|
|
|
54
76
|
# 3. Check system status
|
|
@@ -75,8 +97,9 @@ remnote-bridge disconnect
|
|
|
75
97
|
|
|
76
98
|
| Command | Description |
|
|
77
99
|
|:--------|:------------|
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
100
|
+
| `setup` | Launch Chrome for RemNote login, save credentials for headless mode |
|
|
101
|
+
| `connect` | Start the daemon (`--headless` for auto Chrome, default requires manual plugin load) |
|
|
102
|
+
| `health` | Check daemon/Plugin/SDK status (`--diagnose` for screenshots, `--reload` to restart Chrome) |
|
|
80
103
|
| `disconnect` | Stop the daemon and release resources |
|
|
81
104
|
|
|
82
105
|
### Read
|
|
@@ -182,14 +205,15 @@ remnote-bridge CLI
|
|
|
182
205
|
↕ WebSocket IPC
|
|
183
206
|
Daemon (long-lived process: WS server + handlers + cache)
|
|
184
207
|
↕ WebSocket
|
|
185
|
-
remnote-plugin (runs inside RemNote browser)
|
|
208
|
+
remnote-plugin (runs inside RemNote browser or headless Chrome)
|
|
186
209
|
↕
|
|
187
210
|
RemNote SDK → Knowledge Base
|
|
188
211
|
```
|
|
189
212
|
|
|
190
213
|
- **CLI commands** are stateless — each invocation is an independent OS process
|
|
191
214
|
- **Daemon** holds state: cache, WS connections, timeout timer
|
|
192
|
-
- **Plugin** runs in the browser, calls RemNote SDK on behalf of the daemon
|
|
215
|
+
- **Plugin** runs in the browser (or headless Chrome), calls RemNote SDK on behalf of the daemon
|
|
216
|
+
- **Headless mode** launches Chrome automatically using saved credentials — no browser window needed
|
|
193
217
|
- **Three safety guards** protect edits: cache existence check, optimistic concurrency detection, str_replace exact match
|
|
194
218
|
|
|
195
219
|
## Configuration
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* connect 命令
|
|
3
3
|
*
|
|
4
|
-
* 启动后台守护进程(WS Server +
|
|
4
|
+
* 启动后台守护进程(WS Server + Plugin 服务),等待 Plugin 连接。
|
|
5
5
|
* - 已在运行 → 打印提示,退出码 0
|
|
6
6
|
* - stale PID → 清理后正常启动
|
|
7
7
|
* - 启动失败 → 退出码 1
|
|
8
|
+
*
|
|
9
|
+
* 默认使用静态文件服务器 serve 预构建 plugin。
|
|
10
|
+
* --dev 模式使用 webpack-dev-server(支持 HMR)。
|
|
8
11
|
*/
|
|
9
12
|
import path from 'path';
|
|
10
13
|
import fs from 'fs';
|
|
11
14
|
import { fork } from 'child_process';
|
|
12
15
|
import { loadConfig, pidFilePath, findProjectRoot } from '../config.js';
|
|
13
16
|
import { checkDaemon } from '../daemon/pid.js';
|
|
17
|
+
import { getSetupDonePath } from '../daemon/headless-browser.js';
|
|
14
18
|
import { jsonOutput } from '../utils/output.js';
|
|
15
19
|
function isDaemonMessage(msg) {
|
|
16
20
|
return (typeof msg === 'object' &&
|
|
@@ -22,6 +26,21 @@ export async function connectCommand(options = {}) {
|
|
|
22
26
|
const projectRoot = findProjectRoot();
|
|
23
27
|
const config = loadConfig(projectRoot);
|
|
24
28
|
const pidPath = pidFilePath(projectRoot);
|
|
29
|
+
// headless 前置检查
|
|
30
|
+
if (options.headless) {
|
|
31
|
+
const setupDonePath = getSetupDonePath();
|
|
32
|
+
if (!fs.existsSync(setupDonePath)) {
|
|
33
|
+
const error = '尚未完成 setup。请先执行 `remnote-bridge setup` 登录 RemNote,然后再使用 --headless';
|
|
34
|
+
if (json) {
|
|
35
|
+
jsonOutput({ ok: false, command: 'connect', error });
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.error(error);
|
|
39
|
+
}
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
25
44
|
// 检查是否已在运行
|
|
26
45
|
const status = checkDaemon(pidPath);
|
|
27
46
|
if (status.running) {
|
|
@@ -59,6 +78,12 @@ export async function connectCommand(options = {}) {
|
|
|
59
78
|
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
60
79
|
cwd: projectRoot,
|
|
61
80
|
execArgv,
|
|
81
|
+
env: {
|
|
82
|
+
...process.env,
|
|
83
|
+
REMNOTE_BRIDGE_DEV: options.dev ? '1' : '0',
|
|
84
|
+
...(options.headless ? { REMNOTE_HEADLESS: '1' } : {}),
|
|
85
|
+
...(options.remoteDebuggingPort ? { REMNOTE_HEADLESS_REMOTE_PORT: String(options.remoteDebuggingPort) } : {}),
|
|
86
|
+
},
|
|
62
87
|
});
|
|
63
88
|
// 等待就绪信号,超时 60 秒
|
|
64
89
|
// 首次启动可能需要安装 remnote-plugin 依赖(npm install),在 Windows 上可能需要较长时间
|
|
@@ -112,13 +137,17 @@ export async function connectCommand(options = {}) {
|
|
|
112
137
|
pid: ready.pid, wsPort: ready.wsPort, devServerPort: ready.devServerPort,
|
|
113
138
|
configPort: ready.configPort,
|
|
114
139
|
timeoutMinutes: config.daemonTimeoutMinutes,
|
|
140
|
+
headless: ready.headless ?? false,
|
|
115
141
|
});
|
|
116
142
|
}
|
|
117
143
|
else {
|
|
118
144
|
console.log(`守护进程已启动(PID: ${ready.pid})`);
|
|
119
145
|
console.log(` WS Server: ws://127.0.0.1:${ready.wsPort}`);
|
|
120
|
-
console.log(`
|
|
146
|
+
console.log(` Plugin 服务: http://localhost:${ready.devServerPort}`);
|
|
121
147
|
console.log(` 配置页面: http://127.0.0.1:${ready.configPort}`);
|
|
148
|
+
if (ready.headless) {
|
|
149
|
+
console.log(` Headless Chrome: 已启动(自动加载 Plugin)`);
|
|
150
|
+
}
|
|
122
151
|
console.log(` 超时: ${config.daemonTimeoutMinutes} 分钟无 CLI 交互后自动关闭`);
|
|
123
152
|
}
|
|
124
153
|
process.exitCode = 0;
|
|
@@ -11,7 +11,19 @@ import { checkDaemon } from '../daemon/pid.js';
|
|
|
11
11
|
import { sendDaemonRequest } from '../daemon/send-request.js';
|
|
12
12
|
import { jsonOutput } from '../utils/output.js';
|
|
13
13
|
export async function healthCommand(options = {}) {
|
|
14
|
-
const { json } = options;
|
|
14
|
+
const { json, diagnose, reload } = options;
|
|
15
|
+
// --diagnose 和 --reload 不能同时使用
|
|
16
|
+
if (diagnose && reload) {
|
|
17
|
+
const error = '--diagnose 和 --reload 不能同时使用';
|
|
18
|
+
if (json) {
|
|
19
|
+
jsonOutput({ ok: false, command: 'health', error });
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.error(error);
|
|
23
|
+
}
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
15
27
|
const projectRoot = findProjectRoot();
|
|
16
28
|
const pidPath = pidFilePath(projectRoot);
|
|
17
29
|
// 先检查 PID 文件
|
|
@@ -34,6 +46,97 @@ export async function healthCommand(options = {}) {
|
|
|
34
46
|
process.exitCode = 2;
|
|
35
47
|
return;
|
|
36
48
|
}
|
|
49
|
+
// --diagnose 模式
|
|
50
|
+
if (diagnose) {
|
|
51
|
+
try {
|
|
52
|
+
const result = await sendDaemonRequest('diagnose');
|
|
53
|
+
if (!result) {
|
|
54
|
+
const error = '非 headless 模式,不支持 --diagnose';
|
|
55
|
+
if (json) {
|
|
56
|
+
jsonOutput({ ok: false, command: 'health', error });
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.error(error);
|
|
60
|
+
}
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (json) {
|
|
65
|
+
jsonOutput({ ok: true, command: 'health', mode: 'diagnose', ...result });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log('=== Headless Chrome 诊断 ===');
|
|
69
|
+
console.log(`状态: ${result.headless.status}`);
|
|
70
|
+
console.log(`Chrome 连接: ${result.headless.chromeConnected ? '是' : '否'}`);
|
|
71
|
+
console.log(`页面 URL: ${result.headless.pageUrl ?? '无'}`);
|
|
72
|
+
console.log(`重载次数: ${result.headless.reloadCount}`);
|
|
73
|
+
console.log(`Plugin 连接: ${result.pluginConnected ? '是' : '否'}`);
|
|
74
|
+
console.log(`SDK 就绪: ${result.sdkReady ? '是' : '否'}`);
|
|
75
|
+
if (result.screenshotPath) {
|
|
76
|
+
console.log(`截图: ${result.screenshotPath}`);
|
|
77
|
+
}
|
|
78
|
+
if (result.headless.lastError) {
|
|
79
|
+
console.log(`\n最近错误: ${result.headless.lastError}`);
|
|
80
|
+
}
|
|
81
|
+
if (result.headless.recentConsoleErrors.length > 0) {
|
|
82
|
+
console.log('\nConsole 错误:');
|
|
83
|
+
for (const err of result.headless.recentConsoleErrors) {
|
|
84
|
+
console.log(` ${err}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 排查建议
|
|
88
|
+
if (!result.headless.chromeConnected) {
|
|
89
|
+
console.log('\n排查建议: Chrome 已断开,尝试 `health --reload` 重载');
|
|
90
|
+
}
|
|
91
|
+
else if (!result.pluginConnected) {
|
|
92
|
+
console.log('\n排查建议: Chrome 运行中但 Plugin 未连接,可能页面加载异常,尝试 `health --reload`');
|
|
93
|
+
}
|
|
94
|
+
else if (!result.sdkReady) {
|
|
95
|
+
console.log('\n排查建议: Plugin 已连接但 SDK 未就绪,等待几秒后重试');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
101
|
+
if (json) {
|
|
102
|
+
jsonOutput({ ok: false, command: 'health', error: errorMsg });
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.error(`诊断失败: ${errorMsg}`);
|
|
106
|
+
}
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// --reload 模式
|
|
112
|
+
if (reload) {
|
|
113
|
+
try {
|
|
114
|
+
const result = await sendDaemonRequest('headless_reload');
|
|
115
|
+
if (json) {
|
|
116
|
+
jsonOutput({ ok: result.ok, command: 'health', mode: 'reload', error: result.error });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
if (result.ok) {
|
|
120
|
+
console.log('Headless Chrome 页面已重载');
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.error(`重载失败: ${result.error}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
130
|
+
if (json) {
|
|
131
|
+
jsonOutput({ ok: false, command: 'health', mode: 'reload', error: errorMsg });
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
console.error(`重载失败: ${errorMsg}`);
|
|
135
|
+
}
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
37
140
|
// 通过 WS 连接守护进程获取状态
|
|
38
141
|
let status;
|
|
39
142
|
try {
|
|
@@ -69,6 +172,7 @@ export async function healthCommand(options = {}) {
|
|
|
69
172
|
plugin: { connected: status.pluginConnected },
|
|
70
173
|
sdk: { ready: status.sdkReady },
|
|
71
174
|
timeoutRemaining: status.timeoutRemaining,
|
|
175
|
+
...(status.headless ? { headless: status.headless } : {}),
|
|
72
176
|
});
|
|
73
177
|
}
|
|
74
178
|
else {
|
|
@@ -85,6 +189,12 @@ export async function healthCommand(options = {}) {
|
|
|
85
189
|
else {
|
|
86
190
|
console.log('❌ SDK 未就绪');
|
|
87
191
|
}
|
|
192
|
+
// Headless Chrome 状态行
|
|
193
|
+
if (status.headless) {
|
|
194
|
+
const h = status.headless;
|
|
195
|
+
const icon = h.status === 'running' ? '✅' : '❌';
|
|
196
|
+
console.log(`${icon} Chrome ${h.status}${h.lastError ? ` (${h.lastError})` : ''}`);
|
|
197
|
+
}
|
|
88
198
|
console.log(`\n超时: ${formatUptime(status.timeoutRemaining)} 后自动关闭`);
|
|
89
199
|
}
|
|
90
200
|
process.exitCode = exitCode;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setup 命令
|
|
3
|
+
*
|
|
4
|
+
* 启动有界面的 Chrome(使用共享 profile 目录),让用户登录 RemNote。
|
|
5
|
+
* 登录完成后关闭 Chrome,写入 .setup-done 标记。
|
|
6
|
+
* 后续 connect --headless 使用相同 profile 实现免登录。
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import { spawn } from 'child_process';
|
|
10
|
+
import { findChromePath, hasDisplay, getDefaultProfileDir, getSetupDonePath, } from '../daemon/headless-browser.js';
|
|
11
|
+
import { jsonOutput } from '../utils/output.js';
|
|
12
|
+
export async function setupCommand(options = {}) {
|
|
13
|
+
const { json } = options;
|
|
14
|
+
const profileDir = getDefaultProfileDir();
|
|
15
|
+
const setupDonePath = getSetupDonePath();
|
|
16
|
+
// 检查桌面环境
|
|
17
|
+
if (!hasDisplay()) {
|
|
18
|
+
const error = '未检测到桌面环境(无 DISPLAY/WAYLAND_DISPLAY),setup 需要 GUI 才能登录';
|
|
19
|
+
if (json) {
|
|
20
|
+
jsonOutput({ ok: false, command: 'setup', error });
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.error(error);
|
|
24
|
+
}
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// 检查 Chrome
|
|
29
|
+
const chromePath = findChromePath();
|
|
30
|
+
if (!chromePath) {
|
|
31
|
+
const error = '未找到 Chrome/Chromium,请安装后重试';
|
|
32
|
+
if (json) {
|
|
33
|
+
jsonOutput({ ok: false, command: 'setup', error });
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
console.error(error);
|
|
37
|
+
}
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// 检查是否已完成 setup
|
|
42
|
+
if (fs.existsSync(setupDonePath)) {
|
|
43
|
+
if (json) {
|
|
44
|
+
jsonOutput({ ok: true, command: 'setup', profileDir, alreadyDone: true });
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log(`已完成 setup(profile: ${profileDir})`);
|
|
48
|
+
console.log('如需重新登录,请删除以下文件后重试:');
|
|
49
|
+
console.log(` ${setupDonePath}`);
|
|
50
|
+
}
|
|
51
|
+
process.exitCode = 0;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// 确保 profile 目录存在
|
|
55
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
56
|
+
if (!json) {
|
|
57
|
+
console.log('正在启动 Chrome...');
|
|
58
|
+
console.log('请在浏览器中登录 RemNote,完成后关闭浏览器窗口。');
|
|
59
|
+
}
|
|
60
|
+
// 启动 Chrome(非 headless,用户可见)
|
|
61
|
+
const child = spawn(chromePath, [
|
|
62
|
+
`--user-data-dir=${profileDir}`,
|
|
63
|
+
'--no-first-run',
|
|
64
|
+
'--no-default-browser-check',
|
|
65
|
+
'https://www.remnote.com',
|
|
66
|
+
], {
|
|
67
|
+
stdio: 'ignore',
|
|
68
|
+
detached: false,
|
|
69
|
+
});
|
|
70
|
+
// 等待 Chrome 退出(超时 10 分钟)
|
|
71
|
+
const exitCode = await new Promise((resolve) => {
|
|
72
|
+
const timeout = setTimeout(() => {
|
|
73
|
+
child.kill();
|
|
74
|
+
resolve(null);
|
|
75
|
+
}, 600_000);
|
|
76
|
+
child.on('exit', (code) => {
|
|
77
|
+
clearTimeout(timeout);
|
|
78
|
+
resolve(code);
|
|
79
|
+
});
|
|
80
|
+
child.on('error', (err) => {
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
resolve(-1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
if (exitCode === null) {
|
|
86
|
+
const error = 'Chrome 未在 10 分钟内关闭,setup 超时';
|
|
87
|
+
if (json) {
|
|
88
|
+
jsonOutput({ ok: false, command: 'setup', error });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.error(error);
|
|
92
|
+
}
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// 写入 .setup-done 标记
|
|
97
|
+
const doneData = {
|
|
98
|
+
completedAt: new Date().toISOString(),
|
|
99
|
+
chromePath,
|
|
100
|
+
profileDir,
|
|
101
|
+
};
|
|
102
|
+
fs.writeFileSync(setupDonePath, JSON.stringify(doneData, null, 2));
|
|
103
|
+
if (json) {
|
|
104
|
+
jsonOutput({ ok: true, command: 'setup', profileDir, alreadyDone: false });
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log('setup 完成!');
|
|
108
|
+
console.log(` profile 目录: ${profileDir}`);
|
|
109
|
+
console.log('现在可以使用 `remnote-bridge connect --headless` 启动无头连接。');
|
|
110
|
+
}
|
|
111
|
+
process.exitCode = 0;
|
|
112
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 作为 fork 子进程运行:
|
|
5
5
|
* 1. 启动 WS Server
|
|
6
|
-
* 2. 启动 webpack-dev-server
|
|
6
|
+
* 2. 启动 Plugin 服务(静态文件服务器 或 webpack-dev-server)
|
|
7
7
|
* 3. 写入 PID 文件
|
|
8
8
|
* 4. 管理自动超时关闭
|
|
9
9
|
* 5. 通过 IPC 向父进程发送 ready 信号
|
|
@@ -13,6 +13,8 @@ import fs from 'fs';
|
|
|
13
13
|
import { BridgeServer } from '../server/ws-server.js';
|
|
14
14
|
import { ConfigServer } from '../server/config-server.js';
|
|
15
15
|
import { DevServerManager } from './dev-server.js';
|
|
16
|
+
import { StaticServer } from './static-server.js';
|
|
17
|
+
import { HeadlessBrowserManager } from './headless-browser.js';
|
|
16
18
|
import { writePid, removePid } from './pid.js';
|
|
17
19
|
import { loadConfig, pidFilePath, logFilePath, findProjectRoot } from '../config.js';
|
|
18
20
|
let shutdownInProgress = false;
|
|
@@ -53,6 +55,31 @@ async function main() {
|
|
|
53
55
|
onLog: log,
|
|
54
56
|
getTimeoutRemaining,
|
|
55
57
|
defaults: cfg.defaults,
|
|
58
|
+
// Headless Chrome 回调(闭包引用 headlessBrowser,调用时读取最新值)
|
|
59
|
+
getHeadlessStatus: () => headlessBrowser?.getDiagnostics() ?? null,
|
|
60
|
+
diagnoseHeadless: async () => {
|
|
61
|
+
if (!headlessBrowser)
|
|
62
|
+
return null;
|
|
63
|
+
const diag = headlessBrowser.getDiagnostics();
|
|
64
|
+
const screenshotPath = await headlessBrowser.takeScreenshot();
|
|
65
|
+
return {
|
|
66
|
+
headless: diag,
|
|
67
|
+
screenshotPath,
|
|
68
|
+
pluginConnected: false, // 由 ws-server 填充
|
|
69
|
+
sdkReady: false, // 由 ws-server 填充
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
reloadHeadless: async () => {
|
|
73
|
+
if (!headlessBrowser)
|
|
74
|
+
return { ok: false, error: '非 headless 模式' };
|
|
75
|
+
try {
|
|
76
|
+
await headlessBrowser.manualReload();
|
|
77
|
+
return { ok: true };
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
81
|
+
}
|
|
82
|
+
},
|
|
56
83
|
});
|
|
57
84
|
srv.onCliRequest = () => resetTimeout();
|
|
58
85
|
return srv;
|
|
@@ -84,27 +111,60 @@ async function main() {
|
|
|
84
111
|
onRestart: reload,
|
|
85
112
|
onLog: log,
|
|
86
113
|
});
|
|
87
|
-
// 启动 webpack-dev-server
|
|
114
|
+
// 启动 Plugin 服务(静态文件服务器 或 webpack-dev-server)
|
|
88
115
|
// 从包安装路径计算 pluginDir(dist/cli/daemon/daemon.js → 包根/remnote-plugin)
|
|
89
116
|
const packageRoot = path.resolve(import.meta.dirname, '..', '..', '..');
|
|
90
117
|
const pluginDir = path.join(packageRoot, 'remnote-plugin');
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
const devMode = process.env.REMNOTE_BRIDGE_DEV === '1';
|
|
119
|
+
const headlessMode = process.env.REMNOTE_HEADLESS === '1';
|
|
120
|
+
const headlessRemotePort = process.env.REMNOTE_HEADLESS_REMOTE_PORT
|
|
121
|
+
? parseInt(process.env.REMNOTE_HEADLESS_REMOTE_PORT, 10)
|
|
122
|
+
: undefined;
|
|
123
|
+
// Headless Chrome 管理器(在 createServer 之前声明,闭包可引用)
|
|
124
|
+
let headlessBrowser = null;
|
|
125
|
+
const distDir = path.join(pluginDir, 'dist');
|
|
126
|
+
const distExists = fs.existsSync(path.join(distDir, 'index.html'));
|
|
127
|
+
if (!devMode && !distExists) {
|
|
128
|
+
const msg = 'Plugin dist/ 未找到。请使用 --dev 模式,或确认 remnote-bridge 包完整性。';
|
|
129
|
+
log(msg, 'error');
|
|
130
|
+
process.send?.({ type: 'error', message: msg });
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
const pluginServerLabel = devMode ? 'webpack-dev-server' : '静态文件服务器';
|
|
134
|
+
const pluginServer = devMode
|
|
135
|
+
? new DevServerManager({
|
|
136
|
+
pluginDir,
|
|
137
|
+
port: config.devServerPort,
|
|
138
|
+
onLog: log,
|
|
139
|
+
onExit: (code) => {
|
|
140
|
+
if (!shutdownInProgress && code !== 0) {
|
|
141
|
+
log('webpack-dev-server 异常退出,守护进程关闭', 'error');
|
|
142
|
+
shutdown();
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
: new StaticServer({
|
|
147
|
+
distDir,
|
|
148
|
+
port: config.devServerPort,
|
|
149
|
+
onLog: log,
|
|
150
|
+
});
|
|
102
151
|
async function shutdown() {
|
|
103
152
|
if (shutdownInProgress)
|
|
104
153
|
return;
|
|
105
154
|
shutdownInProgress = true;
|
|
106
155
|
log('开始优雅关闭...');
|
|
107
156
|
clearTimeout(timeoutTimer);
|
|
157
|
+
// 先关闭 headless Chrome
|
|
158
|
+
if (headlessBrowser) {
|
|
159
|
+
try {
|
|
160
|
+
await headlessBrowser.stop();
|
|
161
|
+
log('Headless Chrome 已关闭');
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
log(`Headless Chrome 关闭失败: ${err}`, 'error');
|
|
165
|
+
}
|
|
166
|
+
headlessBrowser = null;
|
|
167
|
+
}
|
|
108
168
|
try {
|
|
109
169
|
await server.stop();
|
|
110
170
|
log('WS Server 已关闭');
|
|
@@ -120,11 +180,11 @@ async function main() {
|
|
|
120
180
|
log(`ConfigServer 关闭失败: ${err}`, 'error');
|
|
121
181
|
}
|
|
122
182
|
try {
|
|
123
|
-
await
|
|
124
|
-
log(
|
|
183
|
+
await pluginServer.stop();
|
|
184
|
+
log(`${pluginServerLabel} 已关闭`);
|
|
125
185
|
}
|
|
126
186
|
catch (err) {
|
|
127
|
-
log(
|
|
187
|
+
log(`${pluginServerLabel} 关闭失败: ${err}`, 'error');
|
|
128
188
|
}
|
|
129
189
|
removePid(pidPath);
|
|
130
190
|
log('PID 文件已删除');
|
|
@@ -153,18 +213,38 @@ async function main() {
|
|
|
153
213
|
// ConfigServer 非关键,启动失败不阻塞
|
|
154
214
|
}
|
|
155
215
|
try {
|
|
156
|
-
|
|
157
|
-
log(
|
|
216
|
+
await pluginServer.start();
|
|
217
|
+
log(`${pluginServerLabel} 已启动 (端口 ${config.devServerPort})`);
|
|
158
218
|
}
|
|
159
219
|
catch (err) {
|
|
160
|
-
log(
|
|
220
|
+
log(`${pluginServerLabel} 启动失败: ${err}`, 'error');
|
|
161
221
|
await server.stop();
|
|
162
|
-
process.send?.({ type: 'error', message:
|
|
222
|
+
process.send?.({ type: 'error', message: `${pluginServerLabel} 启动失败: ${err}` });
|
|
163
223
|
process.exit(1);
|
|
164
224
|
}
|
|
165
225
|
// 写入 PID 文件
|
|
166
226
|
writePid(pidPath, process.pid);
|
|
167
227
|
log(`PID 文件已写入: ${pidPath} (PID: ${process.pid})`);
|
|
228
|
+
// 启动 Headless Chrome(如果启用)
|
|
229
|
+
// headless Chrome 加载 RemNote 本身(不是 plugin 静态文件),
|
|
230
|
+
// RemNote 会自动加载已配置的 dev plugin(从 localhost:8080)
|
|
231
|
+
if (headlessMode) {
|
|
232
|
+
const remNoteUrl = 'https://www.remnote.com';
|
|
233
|
+
headlessBrowser = new HeadlessBrowserManager({
|
|
234
|
+
remNoteUrl,
|
|
235
|
+
remoteDebuggingPort: headlessRemotePort,
|
|
236
|
+
onLog: log,
|
|
237
|
+
});
|
|
238
|
+
try {
|
|
239
|
+
await headlessBrowser.start();
|
|
240
|
+
log(`Headless Chrome 已启动,加载 ${remNoteUrl}`);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
// 非致命:daemon 继续运行,日志记录错误
|
|
244
|
+
log(`Headless Chrome 启动失败(非致命): ${err}`, 'error');
|
|
245
|
+
headlessBrowser = null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
168
248
|
// 启动超时计时器
|
|
169
249
|
resetTimeout();
|
|
170
250
|
// 通知父进程就绪
|
|
@@ -174,6 +254,7 @@ async function main() {
|
|
|
174
254
|
devServerPort: config.devServerPort,
|
|
175
255
|
configPort: config.configPort,
|
|
176
256
|
pid: process.pid,
|
|
257
|
+
headless: headlessMode,
|
|
177
258
|
});
|
|
178
259
|
// 断开 IPC 通道(让父进程可以退出)
|
|
179
260
|
if (process.channel) {
|