- commit
- 2b010d4
- parent
- 7c7e4c5
- author
- im_wower
- date
- 2026-03-29 04:08:02 +0800 CST
fix: reload open ai tabs after extension restart
2 files changed,
+185,
-6
+172,
-2
1@@ -46,6 +46,8 @@ const TRACKED_TAB_REFRESH_DELAY = 150;
2 const SHELL_RUNTIME_HEALTHCHECK_INTERVAL = 30_000;
3 const CONTROL_STATUS_BODY_LIMIT = 12_000;
4 const FINAL_MESSAGE_RELAY_CACHE_LIMIT = 20;
5+const STARTUP_OPEN_AI_TAB_RELOAD_DELAY = 2_500;
6+const SESSION_CONTROLLER_BOOT_MARKER_KEY = "baaFirefox.controllerSessionBootMarker";
7 const CONTENT_SCRIPT_INJECTION_FILES = ["delivery-adapters.js", "content-script.js"];
8 const PAGE_INTERCEPTOR_INJECTION_FILES = ["page-interceptor.js"];
9 const WS_RECONNECT_DELAY = 3_000;
10@@ -199,6 +201,7 @@ const state = {
11 lastControlFailureKey: "",
12 trackedTabRefreshTimer: null,
13 shellRuntimeTimer: null,
14+ startupOpenAiTabReloadTimer: null,
15 shellRuntimeLastHealthCheckAt: 0,
16 trackedTabRefreshRunning: false,
17 trackedTabRefreshQueued: false,
18@@ -278,6 +281,14 @@ function normalizeSavedControlBaseUrl(value) {
19 return DEFAULT_CONTROL_BASE_URL;
20 }
21
22+function getRuntimeOrigin() {
23+ try {
24+ return trimToNull(new URL(browser.runtime.getURL("controller.html")).origin);
25+ } catch (_) {
26+ return null;
27+ }
28+}
29+
30 function normalizeSavedWsUrl(value) {
31 void value;
32 return DEFAULT_WS_URL;
33@@ -450,6 +461,7 @@ function createDefaultControllerRuntimeState(overrides = {}) {
34 lastReloadAt: 0,
35 lastAction: null,
36 lastActionAt: 0,
37+ extensionOrigin: null,
38 ...overrides
39 };
40 }
41@@ -466,7 +478,8 @@ function cloneControllerRuntimeState(value) {
42 lastReadyAt: Number(value.lastReadyAt) || 0,
43 lastReloadAt: Number(value.lastReloadAt) || 0,
44 lastAction: trimToNull(value.lastAction),
45- lastActionAt: Number(value.lastActionAt) || 0
46+ lastActionAt: Number(value.lastActionAt) || 0,
47+ extensionOrigin: trimToNull(value.extensionOrigin)
48 });
49 }
50
51@@ -5153,6 +5166,16 @@ async function queryPlatformTabs(platform) {
52 return sortTabsByRecency(await browser.tabs.query({ url: PLATFORMS[platform].urlPatterns }));
53 }
54
55+async function queryOpenBusinessPlatformTabs(platform) {
56+ return dedupeTabsById(await queryPlatformTabs(platform)).filter((tab) => {
57+ if (!Number.isInteger(tab?.id) || !trimToNull(tab.url)) {
58+ return false;
59+ }
60+
61+ return !isPlatformShellUrl(platform, tab.url || "", { allowFallback: false });
62+ });
63+}
64+
65 async function findPlatformTab(platform) {
66 const tabs = await queryPlatformTabs(platform);
67 if (tabs.length === 0) return null;
68@@ -5296,6 +5319,148 @@ async function reinjectAllOpenPlatformTabs(options = {}) {
69 return results;
70 }
71
72+function describeStartupOpenAiTabReloadDecision(reason) {
73+ switch (reason) {
74+ case "initial_install":
75+ return "首次安装";
76+ case "extension_reload":
77+ return "插件重载";
78+ case "browser_startup":
79+ return "浏览器启动";
80+ case "controller_reopen":
81+ return "controller 页面重新打开";
82+ case "session_storage_unavailable":
83+ return "session 存储不可用";
84+ default:
85+ return "未命中自动刷新条件";
86+ }
87+}
88+
89+async function resolveStartupOpenAiTabReloadPlan(previousExtensionOrigin = null) {
90+ const currentExtensionOrigin = getRuntimeOrigin();
91+ const sessionStorage = browser.storage?.session;
92+ let firstControllerInBrowserSession = true;
93+ let sessionStorageAvailable = false;
94+
95+ if (sessionStorage?.get && sessionStorage?.set) {
96+ sessionStorageAvailable = true;
97+
98+ try {
99+ const saved = await sessionStorage.get(SESSION_CONTROLLER_BOOT_MARKER_KEY);
100+ const existingMarker = trimToNull(saved?.[SESSION_CONTROLLER_BOOT_MARKER_KEY]);
101+ firstControllerInBrowserSession = !existingMarker;
102+
103+ if (!existingMarker) {
104+ await sessionStorage.set({
105+ [SESSION_CONTROLLER_BOOT_MARKER_KEY]: currentExtensionOrigin || String(Date.now())
106+ });
107+ }
108+ } catch (error) {
109+ firstControllerInBrowserSession = true;
110+ addLog(
111+ "warn",
112+ `无法读取 controller session 标记,自动刷新将退化为基于扩展 origin 的判断:${error instanceof Error ? error.message : String(error)}`,
113+ false
114+ );
115+ }
116+ }
117+
118+ const normalizedPreviousOrigin = trimToNull(previousExtensionOrigin);
119+ const initialInstall = !normalizedPreviousOrigin;
120+ const sameBrowserSession = !!currentExtensionOrigin && currentExtensionOrigin === normalizedPreviousOrigin;
121+ const shouldReload = firstControllerInBrowserSession && (initialInstall || sameBrowserSession);
122+ let reason = "unknown";
123+
124+ if (shouldReload) {
125+ reason = initialInstall ? "initial_install" : "extension_reload";
126+ } else if (!firstControllerInBrowserSession) {
127+ reason = "controller_reopen";
128+ } else if (!sessionStorageAvailable) {
129+ reason = "session_storage_unavailable";
130+ } else {
131+ reason = "browser_startup";
132+ }
133+
134+ return {
135+ shouldReload,
136+ reason,
137+ previousExtensionOrigin: normalizedPreviousOrigin,
138+ currentExtensionOrigin
139+ };
140+}
141+
142+async function reloadOpenBusinessPlatformTabs(options = {}) {
143+ const source = trimToNull(options.source) || "startup";
144+ const candidates = [];
145+
146+ for (const platform of PLATFORM_ORDER) {
147+ const tabs = await queryOpenBusinessPlatformTabs(platform);
148+ for (const tab of tabs) {
149+ candidates.push({
150+ platform,
151+ tabId: tab.id,
152+ url: trimToNull(tab.url)
153+ });
154+ }
155+ }
156+
157+ if (candidates.length === 0) {
158+ addLog("info", `无需刷新已打开的 AI 页面,来源 ${source}`, false);
159+ return [];
160+ }
161+
162+ const reloaded = [];
163+ for (const candidate of candidates) {
164+ try {
165+ await browser.tabs.reload(candidate.tabId);
166+ reloaded.push(candidate);
167+ addLog(
168+ "info",
169+ `已自动刷新 ${platformLabel(candidate.platform)} 标签页 ${candidate.tabId},URL ${candidate.url || "-"},来源 ${source}`,
170+ false
171+ );
172+ } catch (error) {
173+ addLog(
174+ "warn",
175+ `自动刷新 ${platformLabel(candidate.platform)} 标签页 ${candidate.tabId} 失败,URL ${candidate.url || "-"}:${error instanceof Error ? error.message : String(error)}`,
176+ false
177+ );
178+ }
179+ }
180+
181+ addLog("info", `已自动刷新 ${reloaded.length}/${candidates.length} 个 AI 业务标签页,来源 ${source}`, false);
182+ return reloaded;
183+}
184+
185+function scheduleStartupOpenAiTabReload(plan = null) {
186+ clearTimeout(state.startupOpenAiTabReloadTimer);
187+
188+ if (!plan?.shouldReload) {
189+ addLog("info", `跳过已打开 AI 页面自动刷新:${describeStartupOpenAiTabReloadDecision(plan?.reason)}`, false);
190+ return false;
191+ }
192+
193+ addLog(
194+ "info",
195+ `检测到${describeStartupOpenAiTabReloadDecision(plan.reason)},将在 ${STARTUP_OPEN_AI_TAB_RELOAD_DELAY}ms 后自动刷新已打开的 AI 业务页`,
196+ false
197+ );
198+
199+ state.startupOpenAiTabReloadTimer = setTimeout(() => {
200+ reloadOpenBusinessPlatformTabs({
201+ source: plan.reason
202+ }).catch((error) => {
203+ addLog(
204+ "error",
205+ `自动刷新已打开 AI 页面失败:${error instanceof Error ? error.message : String(error)}`,
206+ false
207+ );
208+ });
209+ }, STARTUP_OPEN_AI_TAB_RELOAD_DELAY);
210+
211+ return true;
212+}
213+
214 async function setTrackedTab(platform, tab) {
215 state.trackedTabs[platform] = tab ? tab.id : null;
216 state.actualTabs[platform] = buildActualTabSnapshot(
217@@ -7251,6 +7416,8 @@ async function init() {
218 restoreFinalMessageRelayCache(saved[CONTROLLER_STORAGE_KEYS.finalMessageRelayCache]);
219 state.claudeState = loadClaudeState(saved[CONTROLLER_STORAGE_KEYS.claudeState]);
220 state.controllerRuntime = loadControllerRuntimeState(saved[CONTROLLER_STORAGE_KEYS.controllerRuntime]);
221+ const previousExtensionOrigin = trimToNull(state.controllerRuntime.extensionOrigin);
222+ const startupOpenAiTabReloadPlan = await resolveStartupOpenAiTabReloadPlan(previousExtensionOrigin);
223 if (needsStatusReset) {
224 state.lastHeaders = createPlatformMap(() => ({}));
225 state.credentialCapturedAt = createPlatformMap(() => 0);
226@@ -7288,7 +7455,8 @@ async function init() {
227 tabId: current?.id ?? null,
228 ready: true,
229 status: "ready",
230- lastReadyAt: Date.now()
231+ lastReadyAt: Date.now(),
232+ extensionOrigin: startupOpenAiTabReloadPlan.currentExtensionOrigin
233 });
234
235 if (Number.isInteger(current?.id)) {
236@@ -7323,12 +7491,14 @@ async function init() {
237 restartShellRuntimeHealthTimer(SHELL_RUNTIME_HEALTHCHECK_INTERVAL);
238 await prepareStartupControlState();
239 refreshControlPlaneState({ source: "startup", silent: true }).catch(() => {});
240+ scheduleStartupOpenAiTabReload(startupOpenAiTabReloadPlan);
241 }
242
243 window.addEventListener("beforeunload", () => {
244 clearTimeout(state.controlRefreshTimer);
245 clearTimeout(state.trackedTabRefreshTimer);
246 clearTimeout(state.shellRuntimeTimer);
247+ clearTimeout(state.startupOpenAiTabReloadTimer);
248 closeWsConnection();
249 });
250
+13,
-4
1@@ -2,7 +2,7 @@
2
3 ## 状态
4
5-- 当前状态:`待开始`
6+- 当前状态:`已完成`
7 - 规模预估:`S`
8 - 依赖任务:无
9 - 建议执行者:`均可`
10@@ -100,21 +100,30 @@
11
12 ### 开始执行
13
14-- 执行者:
15-- 开始时间:
16+- 执行者:`Codex (GPT-5)`
17+- 开始时间:`2026-03-29 03:45:00 CST`
18 - 状态变更:`待开始` → `进行中`
19
20 ### 完成摘要
21
22-- 完成时间:
23+- 完成时间:`2026-03-29 04:07:14 CST`
24 - 状态变更:`进行中` → `已完成`
25 - 修改了哪些文件:
26+ - `plugins/baa-firefox/controller.js`
27+ - `tasks/T-BUG-031.md`
28 - 核心实现思路:
29+ - 在 `controller.js` 启动初始化阶段增加一次性判定:结合 `browser.storage.session` 的本次浏览器会话标记与持久化的扩展 `origin`,区分“插件安装/重载”与“浏览器普通启动 / controller 页面重开”。
30+ - 仅在判定为安装或重载时,延迟 `2500ms` 查询 Claude / ChatGPT / Gemini 已打开标签页,并只刷新非 `#baa-shell` 的业务页面。
31+ - 为跳过、无需刷新、逐 tab 刷新成功 / 失败都增加日志,日志包含平台、tab ID、URL 和来源。
32 - 跑了哪些测试:
33+ - `node --check plugins/baa-firefox/controller.js`
34
35 ### 执行过程中遇到的问题
36
37 > 记录执行过程中遇到的阻塞、环境问题、临时绕过方案等。合并时由合并者判断是否需要修复或建新任务。
38
39+- 未发现阻塞问题。
40+
41 ### 剩余风险
42
43+- 该实现依赖 Firefox 扩展侧 `browser.storage.session` 与持久化的扩展 `origin` 来区分“插件重载”和“浏览器重启”。如果未来 Firefox 在这两处行为上发生变化,可能需要调整判定逻辑。