remnote-bridge 0.1.7 → 0.1.8

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 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 dev server)
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 → Settings → Plugins → Add Local Plugin
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
- | `connect` | Start the daemon process (WS server + plugin dev server) |
79
- | `health` | Check daemon, Plugin, and SDK status |
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 + webpack-dev-server),等待 Plugin 连接。
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(` webpack-dev-server: http://localhost:${ready.devServerPort}`);
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 devServer = new DevServerManager({
92
- pluginDir,
93
- port: config.devServerPort,
94
- onLog: log,
95
- onExit: (code) => {
96
- if (!shutdownInProgress && code !== 0) {
97
- log('webpack-dev-server 异常退出,守护进程关闭', 'error');
98
- shutdown();
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 devServer.stop();
124
- log('webpack-dev-server 已关闭');
183
+ await pluginServer.stop();
184
+ log(`${pluginServerLabel} 已关闭`);
125
185
  }
126
186
  catch (err) {
127
- log(`webpack-dev-server 关闭失败: ${err}`, 'error');
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
- devServer.start();
157
- log(`webpack-dev-server 启动中 (端口 ${config.devServerPort})`);
216
+ await pluginServer.start();
217
+ log(`${pluginServerLabel} 已启动 (端口 ${config.devServerPort})`);
158
218
  }
159
219
  catch (err) {
160
- log(`webpack-dev-server 启动失败: ${err}`, 'error');
220
+ log(`${pluginServerLabel} 启动失败: ${err}`, 'error');
161
221
  await server.stop();
162
- process.send?.({ type: 'error', message: `webpack-dev-server 启动失败: ${err}` });
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) {