@roll-agent/browser-use-agent 0.14.0 → 0.16.0

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.
@@ -0,0 +1,30 @@
1
+ export type NativeReloadController = {
2
+ evaluateJson<T = unknown>(expression: string): Promise<T>;
3
+ reload(options?: {
4
+ readonly url?: string;
5
+ readonly ignoreCache?: boolean;
6
+ readonly timeoutMs?: number;
7
+ }): Promise<void>;
8
+ };
9
+ export type ReloadNativePageOptions = {
10
+ readonly url: string;
11
+ readonly ignoreCache?: boolean;
12
+ readonly timeoutMs?: number;
13
+ readonly pollMs?: number;
14
+ readonly now?: () => number;
15
+ readonly delay?: (ms: number) => Promise<void>;
16
+ readonly createToken?: () => string;
17
+ readonly onReloadSent?: () => void;
18
+ };
19
+ /**
20
+ * Reloads the current native page via CDP `Page.reload` and waits until the
21
+ * document has actually been swapped.
22
+ *
23
+ * A reload keeps the same URL, so the navigate ready-poll (which anchors on a
24
+ * URL change) would falsely report readiness while the stale document is still
25
+ * `readyState === "complete"`. Instead we tag the live document with a window
26
+ * sentinel, trigger the reload, and only return once that sentinel is gone
27
+ * (window globals are wiped when the document is replaced) and the fresh
28
+ * document has reached an interactive/complete ready state.
29
+ */
30
+ export declare function reloadNativePageAndWaitForSwap(controller: NativeReloadController, options: ReloadNativePageOptions): Promise<void>;
@@ -9,6 +9,22 @@ export type ZhipinNativePagePortOptions = {
9
9
  readonly target: BrowserInspectablePage;
10
10
  readonly controller: NativeCdpController;
11
11
  };
12
+ export type ZhipinNativeReloadOptions = {
13
+ readonly url: string;
14
+ readonly ignoreCache?: boolean;
15
+ readonly onReloadSent?: () => void;
16
+ };
17
+ export declare const ZHIPIN_CHAT_RELOAD_SKIPPED_REASONS: readonly ["not_chat_page"];
18
+ export type ZhipinChatReloadSkippedReason = (typeof ZHIPIN_CHAT_RELOAD_SKIPPED_REASONS)[number];
19
+ export type ZhipinChatReloadTarget = {
20
+ readonly ok: true;
21
+ readonly url: string;
22
+ } | {
23
+ readonly ok: false;
24
+ readonly url: string;
25
+ readonly skippedReason: ZhipinChatReloadSkippedReason;
26
+ readonly error: string;
27
+ };
12
28
  export type ReadNativeChatCandidatesOptions = {
13
29
  readonly targetCount?: number;
14
30
  readonly autoScroll?: boolean;
@@ -176,6 +192,8 @@ export declare class ZhipinNativePagePort {
176
192
  waitForSelector(selector: string, timeoutMs?: number): Promise<boolean>;
177
193
  evaluateJson<T = unknown>(expression: string): Promise<T>;
178
194
  bringToFront(): Promise<void>;
195
+ inspectChatReloadTarget(): Promise<ZhipinChatReloadTarget>;
196
+ reload(options: ZhipinNativeReloadOptions): Promise<void>;
179
197
  isChatSurfaceOpen(): Promise<boolean>;
180
198
  waitForChatSurface(timeoutMs?: number): Promise<boolean>;
181
199
  isRecommendSurfaceOpen(): Promise<boolean>;
@@ -13,6 +13,7 @@ export declare const BrowserInstanceConfigSchema: z.ZodEffects<z.ZodObject<{
13
13
  sessionsDir: z.ZodOptional<z.ZodString>;
14
14
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
15
15
  profileName: z.ZodOptional<z.ZodString>;
16
+ profileColor: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
16
17
  windowBounds: z.ZodOptional<z.ZodObject<{
17
18
  x: z.ZodOptional<z.ZodNumber>;
18
19
  y: z.ZodOptional<z.ZodNumber>;
@@ -37,6 +38,7 @@ export declare const BrowserInstanceConfigSchema: z.ZodEffects<z.ZodObject<{
37
38
  userDataDir: string;
38
39
  headless?: boolean | undefined;
39
40
  profileName?: string | undefined;
41
+ profileColor?: string | undefined;
40
42
  cdpUrl?: string | undefined;
41
43
  cdpPort?: number | undefined;
42
44
  executablePath?: string | undefined;
@@ -55,6 +57,7 @@ export declare const BrowserInstanceConfigSchema: z.ZodEffects<z.ZodObject<{
55
57
  mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
56
58
  headless?: boolean | undefined;
57
59
  profileName?: string | undefined;
60
+ profileColor?: string | undefined;
58
61
  cdpUrl?: string | undefined;
59
62
  cdpHost?: string | undefined;
60
63
  cdpPort?: number | undefined;
@@ -77,6 +80,7 @@ export declare const BrowserInstanceConfigSchema: z.ZodEffects<z.ZodObject<{
77
80
  userDataDir: string;
78
81
  headless?: boolean | undefined;
79
82
  profileName?: string | undefined;
83
+ profileColor?: string | undefined;
80
84
  cdpUrl?: string | undefined;
81
85
  cdpPort?: number | undefined;
82
86
  executablePath?: string | undefined;
@@ -95,6 +99,7 @@ export declare const BrowserInstanceConfigSchema: z.ZodEffects<z.ZodObject<{
95
99
  mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
96
100
  headless?: boolean | undefined;
97
101
  profileName?: string | undefined;
102
+ profileColor?: string | undefined;
98
103
  cdpUrl?: string | undefined;
99
104
  cdpHost?: string | undefined;
100
105
  cdpPort?: number | undefined;
@@ -126,6 +131,7 @@ export declare const BrowserInstancesConfigSchema: z.ZodEffects<z.ZodObject<{
126
131
  sessionsDir: z.ZodOptional<z.ZodString>;
127
132
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
128
133
  profileName: z.ZodOptional<z.ZodString>;
134
+ profileColor: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
129
135
  windowBounds: z.ZodOptional<z.ZodObject<{
130
136
  x: z.ZodOptional<z.ZodNumber>;
131
137
  y: z.ZodOptional<z.ZodNumber>;
@@ -150,6 +156,7 @@ export declare const BrowserInstancesConfigSchema: z.ZodEffects<z.ZodObject<{
150
156
  userDataDir: string;
151
157
  headless?: boolean | undefined;
152
158
  profileName?: string | undefined;
159
+ profileColor?: string | undefined;
153
160
  cdpUrl?: string | undefined;
154
161
  cdpPort?: number | undefined;
155
162
  executablePath?: string | undefined;
@@ -168,6 +175,7 @@ export declare const BrowserInstancesConfigSchema: z.ZodEffects<z.ZodObject<{
168
175
  mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
169
176
  headless?: boolean | undefined;
170
177
  profileName?: string | undefined;
178
+ profileColor?: string | undefined;
171
179
  cdpUrl?: string | undefined;
172
180
  cdpHost?: string | undefined;
173
181
  cdpPort?: number | undefined;
@@ -190,6 +198,7 @@ export declare const BrowserInstancesConfigSchema: z.ZodEffects<z.ZodObject<{
190
198
  userDataDir: string;
191
199
  headless?: boolean | undefined;
192
200
  profileName?: string | undefined;
201
+ profileColor?: string | undefined;
193
202
  cdpUrl?: string | undefined;
194
203
  cdpPort?: number | undefined;
195
204
  executablePath?: string | undefined;
@@ -208,6 +217,7 @@ export declare const BrowserInstancesConfigSchema: z.ZodEffects<z.ZodObject<{
208
217
  mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
209
218
  headless?: boolean | undefined;
210
219
  profileName?: string | undefined;
220
+ profileColor?: string | undefined;
211
221
  cdpUrl?: string | undefined;
212
222
  cdpHost?: string | undefined;
213
223
  cdpPort?: number | undefined;
@@ -232,6 +242,7 @@ export declare const BrowserInstancesConfigSchema: z.ZodEffects<z.ZodObject<{
232
242
  userDataDir: string;
233
243
  headless?: boolean | undefined;
234
244
  profileName?: string | undefined;
245
+ profileColor?: string | undefined;
235
246
  cdpUrl?: string | undefined;
236
247
  cdpPort?: number | undefined;
237
248
  executablePath?: string | undefined;
@@ -253,6 +264,7 @@ export declare const BrowserInstancesConfigSchema: z.ZodEffects<z.ZodObject<{
253
264
  mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
254
265
  headless?: boolean | undefined;
255
266
  profileName?: string | undefined;
267
+ profileColor?: string | undefined;
256
268
  cdpUrl?: string | undefined;
257
269
  cdpHost?: string | undefined;
258
270
  cdpPort?: number | undefined;
@@ -278,6 +290,7 @@ export declare const BrowserInstancesConfigSchema: z.ZodEffects<z.ZodObject<{
278
290
  userDataDir: string;
279
291
  headless?: boolean | undefined;
280
292
  profileName?: string | undefined;
293
+ profileColor?: string | undefined;
281
294
  cdpUrl?: string | undefined;
282
295
  cdpPort?: number | undefined;
283
296
  executablePath?: string | undefined;
@@ -299,6 +312,7 @@ export declare const BrowserInstancesConfigSchema: z.ZodEffects<z.ZodObject<{
299
312
  mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
300
313
  headless?: boolean | undefined;
301
314
  profileName?: string | undefined;
315
+ profileColor?: string | undefined;
302
316
  cdpUrl?: string | undefined;
303
317
  cdpHost?: string | undefined;
304
318
  cdpPort?: number | undefined;
@@ -0,0 +1,28 @@
1
+ import { getContextManager, getRuntime } from "../runtime-holder.ts";
2
+ import { toNativePageInfo } from "../page-info.ts";
3
+ import { reloadNativePageAndWaitForSwap } from "../native-reload.ts";
4
+ type ReloadActiveTabDeps = {
5
+ readonly getContextManager: typeof getContextManager;
6
+ readonly getRuntime: typeof getRuntime;
7
+ readonly toNativePageInfo: typeof toNativePageInfo;
8
+ readonly reloadNativePageAndWaitForSwap: typeof reloadNativePageAndWaitForSwap;
9
+ };
10
+ export declare function setReloadActiveTabDepsForTests(override: Partial<ReloadActiveTabDeps> | undefined): void;
11
+ export declare const browserReloadActiveTab: import("@roll-agent/sdk").ToolDefinition<{
12
+ ignoreCache?: boolean | undefined;
13
+ browserActionApproval?: {
14
+ id: string;
15
+ } | undefined;
16
+ }, {
17
+ success: boolean;
18
+ reloaded: boolean;
19
+ page: {
20
+ pageId: string;
21
+ url: string;
22
+ title: string;
23
+ boundPlatform: "zhipin" | "yupao" | null;
24
+ detectedPlatform: "zhipin" | "yupao" | null;
25
+ isSelectedForPlatform: boolean;
26
+ };
27
+ }>;
28
+ export {};
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ import { type BrowserInstanceStopResult } from "../browser-instance-pool.ts";
3
+ declare const BrowserStopInputSchema: z.ZodEffects<z.ZodObject<{
4
+ browserInstance: z.ZodOptional<z.ZodString>;
5
+ browserInstances: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
6
+ all: z.ZodOptional<z.ZodBoolean>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ browserInstance?: string | undefined;
9
+ browserInstances?: string[] | undefined;
10
+ all?: boolean | undefined;
11
+ }, {
12
+ browserInstance?: string | undefined;
13
+ browserInstances?: string[] | undefined;
14
+ all?: boolean | undefined;
15
+ }>, {
16
+ browserInstance?: string | undefined;
17
+ browserInstances?: string[] | undefined;
18
+ all?: boolean | undefined;
19
+ }, {
20
+ browserInstance?: string | undefined;
21
+ browserInstances?: string[] | undefined;
22
+ all?: boolean | undefined;
23
+ }>;
24
+ declare const BrowserStopOutputSchema: z.ZodObject<{
25
+ ok: z.ZodBoolean;
26
+ stopped: z.ZodNumber;
27
+ results: z.ZodArray<z.ZodObject<{
28
+ browserInstance: z.ZodString;
29
+ status: z.ZodEnum<["stopped", "not_running", "not_found", "failed"]>;
30
+ mode: z.ZodOptional<z.ZodEnum<["managed-cdp", "remote-cdp", "existing-session"]>>;
31
+ message: z.ZodOptional<z.ZodString>;
32
+ }, "strip", z.ZodTypeAny, {
33
+ status: "not_found" | "failed" | "stopped" | "not_running";
34
+ browserInstance: string;
35
+ message?: string | undefined;
36
+ mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
37
+ }, {
38
+ status: "not_found" | "failed" | "stopped" | "not_running";
39
+ browserInstance: string;
40
+ message?: string | undefined;
41
+ mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
42
+ }>, "many">;
43
+ }, "strip", z.ZodTypeAny, {
44
+ stopped: number;
45
+ ok: boolean;
46
+ results: {
47
+ status: "not_found" | "failed" | "stopped" | "not_running";
48
+ browserInstance: string;
49
+ message?: string | undefined;
50
+ mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
51
+ }[];
52
+ }, {
53
+ stopped: number;
54
+ ok: boolean;
55
+ results: {
56
+ status: "not_found" | "failed" | "stopped" | "not_running";
57
+ browserInstance: string;
58
+ message?: string | undefined;
59
+ mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
60
+ }[];
61
+ }>;
62
+ type BrowserStopInput = z.infer<typeof BrowserStopInputSchema>;
63
+ type BrowserStopOutput = z.infer<typeof BrowserStopOutputSchema>;
64
+ export declare const browserStop: import("@roll-agent/sdk").ToolDefinition<{
65
+ browserInstance?: string | undefined;
66
+ browserInstances?: string[] | undefined;
67
+ all?: boolean | undefined;
68
+ }, {
69
+ stopped: number;
70
+ ok: boolean;
71
+ results: {
72
+ status: "not_found" | "failed" | "stopped" | "not_running";
73
+ browserInstance: string;
74
+ message?: string | undefined;
75
+ mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
76
+ }[];
77
+ }>;
78
+ export declare function resolveBrowserStopTargets(input: BrowserStopInput, availableInstances: readonly string[]): readonly string[];
79
+ export declare function createBrowserStopOutput(results: readonly BrowserInstanceStopResult[]): BrowserStopOutput;
80
+ export {};
@@ -1,18 +1,25 @@
1
1
  import { NativeVisualActivitySession } from "../native-visual-activity-session.ts";
2
2
  import { openZhipinNativePagePort } from "../pages/zhipin/native-page.ts";
3
- import type { ZhipinNativePagePort } from "../pages/zhipin/native-page.ts";
4
- import { getContextManager } from "../runtime-holder.ts";
3
+ import { type ZhipinNativePagePort } from "../pages/zhipin/native-page.ts";
4
+ import { getContextManager, getRuntime } from "../runtime-holder.ts";
5
5
  type NativeVisualActivitySessionLike = Pick<NativeVisualActivitySession, "begin" | "highlightSelector" | "previewMouseMotion" | "succeed" | "fail">;
6
6
  type ZhipinOpenChatPageDeps = {
7
7
  readonly getContextManager: typeof getContextManager;
8
+ readonly getRuntime: typeof getRuntime;
8
9
  readonly openNativePagePort: typeof openZhipinNativePagePort;
9
10
  readonly createNativeVisualActivitySession: (page: ZhipinNativePagePort) => NativeVisualActivitySessionLike;
10
11
  };
11
12
  export declare function setZhipinOpenChatPageDepsForTests(override: Partial<ZhipinOpenChatPageDeps> | undefined): void;
12
- export declare const zhipinOpenChatPage: import("@roll-agent/sdk").ToolDefinition<{}, {
13
+ export declare const zhipinOpenChatPage: import("@roll-agent/sdk").ToolDefinition<{
14
+ browserActionApproval?: {
15
+ id: string;
16
+ } | undefined;
17
+ forceReload?: boolean | undefined;
18
+ }, {
13
19
  success: boolean;
14
20
  alreadyOnChat: boolean;
15
21
  usedSidebarClick: boolean;
22
+ usedReload: boolean;
16
23
  chatReady: boolean;
17
24
  error?: string | undefined;
18
25
  page?: {
@@ -23,5 +30,6 @@ export declare const zhipinOpenChatPage: import("@roll-agent/sdk").ToolDefinitio
23
30
  detectedPlatform: "zhipin" | "yupao" | null;
24
31
  isSelectedForPlatform: boolean;
25
32
  } | undefined;
33
+ reloadSkippedReason?: "not_chat_page" | undefined;
26
34
  }>;
27
35
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roll-agent/browser-use-agent",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -47,7 +47,7 @@
47
47
  "zod": "^3.25.76",
48
48
  "@roll-agent/reply-authority-client": "0.1.2",
49
49
  "@roll-agent/sdk": "0.2.0",
50
- "@roll-agent/browser": "0.7.0"
50
+ "@roll-agent/browser": "0.9.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "^22.0.0"
@@ -31,9 +31,12 @@ optional:
31
31
  purpose: browser-use 工具级业务策略 JSON;可按 exact tool name 配置 log、deny、confirm
32
32
  default: '{"approvalTtlMs":300000,"tools":{}}'
33
33
  example: '{"approvalTtlMs":300000,"tools":{"zhipin_send_prepared_reply":{"policy":"confirm"}}}'
34
+ - name: BROWSER_PROFILE_COLOR
35
+ purpose: legacy 单浏览器运行时的 Chrome profile 颜色,格式为 `#RRGGBB`;配置了 `browser.instances` 时由每个实例的 profileColor 接管,此 env 会被忽略
36
+ example: "#2563EB"
34
37
  - name: BROWSER_INSTANCES_JSON
35
- purpose: Roll core 注入的多浏览器实例声明 JSON;由 `browser.instances` 派生,包含每个实例的 profile、CDP 端口、session 目录、可选 profileName/windowBounds 和招聘事件归因 ID;orchestrator 不应手工拼接,手工配置时必须保持 cdpPort/userDataDir 唯一
36
- example: '{"defaultInstance":"boss-a","instances":{"boss-a":{"platform":"zhipin","mode":"managed-cdp","cdpPort":9222,"userDataDir":"/tmp/roll-browser/profiles/boss-a","profileName":"boss-a","windowBounds":{"x":0,"y":0,"width":680,"height":1000},"trackingAgentId":"zhipin-boss-a"}}}'
38
+ purpose: Roll core 注入的多浏览器实例声明 JSON;由 `browser.instances` 派生,包含每个实例的 profile、CDP 端口、session 目录、可选 profileName/profileColor/windowBounds 和招聘事件归因 ID;orchestrator 不应手工拼接,手工配置时必须保持 cdpPort/userDataDir 唯一
39
+ example: '{"defaultInstance":"boss-a","instances":{"boss-a":{"platform":"zhipin","mode":"managed-cdp","cdpPort":9222,"userDataDir":"/tmp/roll-browser/profiles/boss-a","profileName":"boss-a","profileColor":"#2563EB","windowBounds":{"x":0,"y":0,"width":680,"height":1000},"trackingAgentId":"zhipin-boss-a"}}}'
37
40
  - name: BROWSER_VISUAL_CURSOR
38
41
  purpose: 在可见浏览器页面内显示 browser-use 的页内虚拟指针和点击波纹;默认开启,设为 `"false"` 可关闭
39
42
  default: "true"
@@ -68,7 +68,7 @@
68
68
  `jobRef` 规则:
69
69
 
70
70
  - `jobRef` 格式如 `@j1`,只来自 `zhipin_list_recommend_jobs()` 输出。
71
- - `jobRef` 只对最近一次岗位下拉快照有效;筛选、搜索、刷新或页面重开后必须重新读取。
71
+ - `jobRef` 只对最近一次岗位下拉快照有效;筛选、搜索、刷新、页面 reload 或页面重开后必须重新读取。
72
72
  - orchestrator 不要自行构造 `jobRef`。
73
73
  - `jobRef` 不是安全边界,只是降低编排认知负担;真实 DOM 点击仍由工具解析到 `value` / `index` 后执行。
74
74
  - 默认不要传 `forceClick:true`;只有需要重新点击已选中的岗位项时才使用。
@@ -191,7 +191,7 @@ recommend-list -> 推荐牛人列表,默认向下滚动,去重主键 candid
191
191
 
192
192
  1. 上层 orchestrator 优先把 `candidateRef` 传给 `zhipin_say_hello({ candidateRefs })` 或 `zhipin_open_resume({ candidateRef })`。
193
193
  2. `indices` / `index` 只作为当前 DOM 快照兜底。
194
- 3. 筛选、滚动加载、搜索、刷新或页面重开后必须重新调用 `zhipin_get_candidate_list`,不要复用旧 `candidateRef`。
194
+ 3. 筛选、滚动加载、搜索、刷新、页面 reload 或页面重开后必须重新调用 `zhipin_get_candidate_list`,不要复用旧 `candidateRef`。
195
195
  4. 不要由 orchestrator 自己构造 `@c1`;只使用 tool 输出中的 `candidateRef`。
196
196
  5. 聊天消息列表没有 `candidateRef`;聊天回复链路继续使用 `conversationId` / `candidateId`。
197
197
  6. 调 `zhipin_say_hello` 前先过滤 `buttonText:"打招呼"`;`buttonText` 为空通常表示已打过招呼,不应重复点击。
@@ -202,3 +202,16 @@ recommend-list -> 推荐牛人列表,默认向下滚动,去重主键 candid
202
202
  - 如果 `candidateRef` 对应的 `candidateId` / `name` 与当前 DOM 不一致,工具会返回 `success:false` 并提示“候选人引用已过期”。
203
203
  - 收到过期提示后,重新执行推荐候选人链路的第 3 步,再提交新的 `candidateRefs`。
204
204
  - 同一快照内可以一次提交多个 `candidateRefs` 连续打招呼;如果 BOSS 在点击后重排列表,工具会拒绝过期 ref,orchestrator 应刷新列表后只重试剩余目标。
205
+
206
+ ## 长跑 tab 的 reload recovery
207
+
208
+ Chrome 全天不关、同一沟通 tab 连续跑多批任务时,前端 SPA 状态会在同一 renderer 内累积,常见症状是 `.geek-item.selected` 选中态丢失、依赖「当前选中聊天」的工具(如 `zhipin_exchange_wechat`)失败,且失败率随运行时长上升。
209
+
210
+ recovery 优先级与边界:
211
+
212
+ 1. `zhipin_open_chat_page({ forceReload: true })`:只对当前沟通页执行 native CDP `Page.reload`,等价手动 F5,清空当前 document 的 DOM 与页面内 SPA 状态,保留 Chrome 窗口与 profile 登录态;reload 后返回 `usedReload: true` 与 `chatReady`。如果实时页面已不是沟通页,会跳过 reload 并返回 `reloadSkippedReason`。
213
+ 2. 通用 `browser_reload_active_tab`:对当前 tracked native page 做同样的 reload,用于非沟通页或不需要确认聊天就绪的场景。
214
+ 3. 普通 `zhipin_open_chat_page()`(不带 forceReload)在已处于沟通页时只返回 `alreadyOnChat: true`,**不会**卸载 document,无法清状态。
215
+ 4. `roll browser stop` 才能回收 renderer 进程内存,但会关闭浏览器窗口;reload 只清 document 级状态,**不保证** renderer 把内存归还 OS。
216
+
217
+ reload / 页面重开后,所有 `@eN` / `candidateRef` / `jobRef` 一律失效,必须重新 `browser_snapshot` 或重新读列表后再继续。