@tomflow/chatloop-extension 0.3.47 → 0.3.48
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 +8 -7
- package/bin/cli.mjs +1 -1
- package/bin/native-host.mjs +1 -5
- package/extension/actions/clean.js +4 -10
- package/extension/actions/common.js +24 -1
- package/extension/actions/continue.js +10 -64
- package/extension/actions/dom.js +2 -25
- package/extension/actions/handoff.js +11 -50
- package/extension/actions/text.js +2 -25
- package/extension/background.js +36 -446
- package/extension/loop/core.js +3 -16
- package/extension/loop/resources.js +7 -13
- package/extension/manifest.json +1 -1
- package/extension/popup.html +0 -1
- package/extension/popup.js +1 -1
- package/lib/bridge.mjs +1 -10
- package/lib/session-file.mjs +1 -1
- package/package.json +1 -1
- package/extension/acceptance.js +0 -641
- package/extension/actions/clock.js +0 -29
- package/extension/clock.html +0 -4
- package/lib/mcp-tools.json +0 -26
package/extension/acceptance.js
DELETED
|
@@ -1,641 +0,0 @@
|
|
|
1
|
-
// Read-only acceptance inventory and strictly scoped test-tab lifecycle.
|
|
2
|
-
(() => {
|
|
3
|
-
const q = new URLSearchParams(location.search);
|
|
4
|
-
const modes = ["audit", "newTest", "cleanup", "cleanupVerified", "cleanupStaleMachine", "cleanupTestHome", "cleanupFailedStartHome", "cleanupBlockedE2E"].filter(k => q.has(k));
|
|
5
|
-
if (!modes.length) return; // Ordinary popup remains unchanged.
|
|
6
|
-
const runKey = "chat-loop.action.run.v1";
|
|
7
|
-
const ownedKey = "chat-loop.acceptance.owned.v1";
|
|
8
|
-
const resourcesKey = "chat-loop.resources.v1";
|
|
9
|
-
const extensionOrigin = chrome.runtime.getURL("").replace(/\/$/, "");
|
|
10
|
-
const status = document.getElementById("status");
|
|
11
|
-
const detail = document.getElementById("detail");
|
|
12
|
-
const isChat = url => /^https:\/\/chatgpt\.com\/(?:c\/[0-9a-f-]{12,}|g\/[^/]+\/c\/[0-9a-f-]{12,})?$/i.test(url || "");
|
|
13
|
-
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
14
|
-
async function report() {
|
|
15
|
-
if (modes.length !== 1 || [...q.keys()].some(k => !modes.includes(k)))
|
|
16
|
-
throw Error("INVALID_ACCEPTANCE_PARAMS");
|
|
17
|
-
const mode = modes[0], own = await chrome.tabs.getCurrent();
|
|
18
|
-
if (!own?.id || !own.url?.startsWith(chrome.runtime.getURL("popup.html")))
|
|
19
|
-
throw Error("INVALID_ACCEPTANCE_PAGE");
|
|
20
|
-
const [state, allBeforeCleanup] = await Promise.all([
|
|
21
|
-
chrome.storage.local.get([runKey, ownedKey, resourcesKey]), chrome.tabs.query({})
|
|
22
|
-
]);
|
|
23
|
-
// Only an old read-only acceptance audit page may be automatically reaped.
|
|
24
|
-
// These are diagnostics, not Chat conversations or formal Action pages.
|
|
25
|
-
// Verify id, exact URL and inactive/unpinned state before closing.
|
|
26
|
-
const removedStaleAudits = [];
|
|
27
|
-
for (const item of allBeforeCleanup) {
|
|
28
|
-
if (item.id === own.id || item.active || item.pinned
|
|
29
|
-
|| item.url !== chrome.runtime.getURL("popup.html?audit=1")) continue;
|
|
30
|
-
const live = await chrome.tabs.get(item.id).catch(() => null);
|
|
31
|
-
if (live && !live.active && !live.pinned && live.url === item.url) {
|
|
32
|
-
await chrome.tabs.remove(item.id);
|
|
33
|
-
removedStaleAudits.push(item.id);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const all = removedStaleAudits.length ? await chrome.tabs.query({}) : allBeforeCleanup;
|
|
37
|
-
const run = state[runKey] || null;
|
|
38
|
-
const owned = Array.isArray(state[ownedKey]) ? state[ownedKey] : [];
|
|
39
|
-
const resourceState = state[resourcesKey] || {};
|
|
40
|
-
const resourceActions = all.filter(t => t.url?.startsWith(extensionOrigin + "/")
|
|
41
|
-
&& /\/(?:start|continue|handoff|status|text|dom|screenshot|reload|clean|clock)\.html(?:\?|$)/.test(t.url)).length;
|
|
42
|
-
const resourceChats = Array.isArray(resourceState.entries)
|
|
43
|
-
? resourceState.entries.filter(e => e.kind === "chat"
|
|
44
|
-
&& all.some(t => t.id === e.id)).length : 0;
|
|
45
|
-
const audit = {at: new Date().toISOString(), mode,
|
|
46
|
-
resources: {policy:{actionLimit:3,ownedChatLimit:2},
|
|
47
|
-
created:resourceState.created || 0,observed:resourceState.observed || 0,
|
|
48
|
-
closed:resourceState.closed || 0,denied:resourceState.denied || 0,
|
|
49
|
-
peakActions:resourceState.peakActions || 0,
|
|
50
|
-
peakChats:resourceState.peakChats || 0,
|
|
51
|
-
liveActions:resourceActions,liveOwnedChats:resourceChats,
|
|
52
|
-
lastDenial:resourceState.lastDenial || null,
|
|
53
|
-
lastReaped:resourceState.lastReaped || null},
|
|
54
|
-
removedStaleAuditTabs: removedStaleAudits,
|
|
55
|
-
version: chrome.runtime.getManifest().version, totalTabs: all.length,
|
|
56
|
-
otherTabCount: all.filter(t => !isChat(t.url) && !t.url?.startsWith(extensionOrigin)).length,
|
|
57
|
-
chatTabs: all.filter(t => isChat(t.url)).map(t => ({
|
|
58
|
-
id: t.id, windowId: t.windowId, url: t.url, title: t.title, active: t.active,
|
|
59
|
-
pinned: t.pinned
|
|
60
|
-
})),
|
|
61
|
-
extensionTabs: all.filter(t => t.url?.startsWith(extensionOrigin) && t.id !== own.id)
|
|
62
|
-
.map(t => ({id: t.id, windowId: t.windowId, url: t.url, title: t.title})),
|
|
63
|
-
ownedTestTabs: owned,
|
|
64
|
-
messageEvidence: {}, actionEvidence: {},
|
|
65
|
-
run: run ? Object.fromEntries(["phase", "action", "reason", "tabId", "page",
|
|
66
|
-
"sourceTabId", "sourcePage", "attemptedNewTabId", "lastStartFailureCode", "newUserKey",
|
|
67
|
-
"sessionId", "projectId", "generation",
|
|
68
|
-
"userKey", "assistantKey", "startedAt", "deadlineAt", "handoffPending",
|
|
69
|
-
"handoffPendingAt", "handoffMode", "testSetup", "timedTest", "testTurnIndex",
|
|
70
|
-
"clockAdvancedAt", "clockAdvanceMinutes", "clockRunningProof",
|
|
71
|
-
"wallTestHours", "wallHoldAt", "wallReleaseAt", "wallReleasedAt",
|
|
72
|
-
"wallTestMinutes", "wallAdjustedAt", "wallOriginalReleaseAt",
|
|
73
|
-
"countMode", "countValue", "countHandoffs", "countLastSendAt", "countReleaseAt",
|
|
74
|
-
"lastFaultProof"]
|
|
75
|
-
.filter(k => run[k] !== undefined).map(k => [k, run[k]])) : null
|
|
76
|
-
};
|
|
77
|
-
if (run?.parseEvidence && owned.some(item => item.id === run.tabId || item.id === run.sourceTabId)) {
|
|
78
|
-
audit.run.parseEvidence = run.parseEvidence;
|
|
79
|
-
}
|
|
80
|
-
// Read-only diagnosis of a failed pre-send test home, never a send or cleanup.
|
|
81
|
-
if (run?.phase === 'BLOCKED' && /^cl-intent-e2e-[a-z0-9-]+$/i.test(run.projectId || '')
|
|
82
|
-
&& Number.isInteger(run.attemptedNewTabId)) {
|
|
83
|
-
const candidate=all.find(t=>t.id===run.attemptedNewTabId
|
|
84
|
-
&& t.url==='https://chatgpt.com/');
|
|
85
|
-
if (candidate) {
|
|
86
|
-
try {
|
|
87
|
-
const [p]=await chrome.scripting.executeScript({target:{tabId:candidate.id},
|
|
88
|
-
func:()=>{
|
|
89
|
-
const visible=e=>!!e?.getClientRects?.().length;
|
|
90
|
-
const mains=[...document.querySelectorAll('main')].filter(visible);
|
|
91
|
-
const editor=document.querySelector('[contenteditable="true"][data-composer-markdown]')
|
|
92
|
-
||document.querySelector('#prompt-textarea[contenteditable="true"]');
|
|
93
|
-
return {pathname:location.pathname,mainCount:mains.length,
|
|
94
|
-
readyState:document.readyState,
|
|
95
|
-
editorExists:!!editor,editorVisible:visible(editor),
|
|
96
|
-
editorBlank:editor?!(editor.innerText||editor.textContent||'').trim():null,
|
|
97
|
-
hasOtherEditable:!!document.querySelector('[contenteditable="true"]'),
|
|
98
|
-
hasTextarea:!!document.querySelector('textarea'),
|
|
99
|
-
hasTextbox:!!document.querySelector('[role="textbox"]'),
|
|
100
|
-
hasChallengeFrame:!!document.querySelector('iframe[src*="challenge"],iframe[src*="captcha"]'),
|
|
101
|
-
mainHasMessages:mains.some(m=>!!m.querySelector('[data-chatgpt-search-message-ids],[data-message-author-role]'))};
|
|
102
|
-
}});
|
|
103
|
-
audit.failedStartHome=p?.result||{status:'NO_PROBE_RESULT'};
|
|
104
|
-
} catch {audit.failedStartHome={status:'PROBE_UNAVAILABLE'};}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// Inspect only this extension's own open action pages: a failed test setup
|
|
108
|
-
// must expose a readable failure code, never trigger a second send.
|
|
109
|
-
for (const tab of audit.extensionTabs) {
|
|
110
|
-
if (!/\/(?:continue|handoff|clock|screenshot|dom|text|reload)\.html(?:\?|$)/.test(tab.url)) continue;
|
|
111
|
-
try {
|
|
112
|
-
const [read] = await chrome.scripting.executeScript({target: {tabId: tab.id},
|
|
113
|
-
func: () => ({status: document.getElementById("status")?.innerText || "",
|
|
114
|
-
details: (document.getElementById("details")?.innerText || "").slice(0, 300)})});
|
|
115
|
-
audit.actionEvidence[tab.id] = read?.result || {status: "UNAVAILABLE"};
|
|
116
|
-
} catch {audit.actionEvidence[tab.id] = {status: "PROBE_UNAVAILABLE"};}
|
|
117
|
-
}
|
|
118
|
-
// Inspect only the active run and tests created by this extension. Browser
|
|
119
|
-
// message evidence is read-only; this page never types into ChatGPT.
|
|
120
|
-
const inspectedFailedHandoff = owned.some(item => item.id === run?.tabId || item.id === run?.sourceTabId)
|
|
121
|
-
? run?.attemptedNewTabId : null;
|
|
122
|
-
const inspectIds = [...new Set([run?.tabId, run?.sourceTabId, inspectedFailedHandoff,
|
|
123
|
-
...owned.map(item => item.id)])]
|
|
124
|
-
.filter(id => audit.chatTabs.some(tab => tab.id === id));
|
|
125
|
-
for (const id of inspectIds) {
|
|
126
|
-
try {
|
|
127
|
-
const [read] = await chrome.scripting.executeScript({
|
|
128
|
-
target: {tabId: id},
|
|
129
|
-
func: () => {
|
|
130
|
-
const mains = [...document.querySelectorAll("main")]
|
|
131
|
-
.filter(node => node.getClientRects().length);
|
|
132
|
-
if (mains.length !== 1) return {status: "MAIN_NOT_UNIQUE"};
|
|
133
|
-
const main = mains[0];
|
|
134
|
-
let rows = [...main.querySelectorAll(
|
|
135
|
-
"[data-chatgpt-search-unit-key][data-chatgpt-search-message-ids]")];
|
|
136
|
-
if (!rows.length) rows = [...main.querySelectorAll(
|
|
137
|
-
"[data-message-author-role]")];
|
|
138
|
-
const role = node => node.getAttribute("data-message-author-role")
|
|
139
|
-
|| node.getAttribute("data-chatgpt-search-unit-key")?.split(":").at(-1);
|
|
140
|
-
const idOf = node => node.getAttribute("data-message-id")
|
|
141
|
-
|| node.getAttribute("data-chatgpt-search-message-ids")?.trim().split(/\s+/)[0];
|
|
142
|
-
const users = rows.filter(node => role(node) === "user").map(idOf);
|
|
143
|
-
const assistants = rows.filter(node => role(node) === "assistant").map(idOf);
|
|
144
|
-
const last = rows.filter(node => role(node) === "assistant").at(-1);
|
|
145
|
-
const editor = document.querySelector('[contenteditable="true"][data-composer-markdown]')
|
|
146
|
-
|| document.querySelector('#prompt-textarea[contenteditable="true"]');
|
|
147
|
-
const editorVisible = !!editor?.getClientRects().length;
|
|
148
|
-
const draft = (editor?.innerText || editor?.textContent || "").trim();
|
|
149
|
-
const restoredTimedTestDraft = draft.includes("LOOP-V037-FAST-3C1H")
|
|
150
|
-
&& draft.includes("【原始任务用户消息】") && draft.includes("【最近轮次证据】")
|
|
151
|
-
&& draft.length >= 450 && draft.length <= 8192
|
|
152
|
-
&& (draft.startsWith("你正在接手上一 Chat 尚未完成的同一个任务")
|
|
153
|
-
|| (draft.startsWith("你正在接手上一 Chat 未完成的任务。旧 Chat 的网页历史仍可核实")
|
|
154
|
-
&& draft.endsWith("是否跨 Chat 由扩展决定,不要主动要求 Handoff。")));
|
|
155
|
-
const text = last?.querySelector(".markdown")?.innerText || last?.innerText || "";
|
|
156
|
-
const marker = text.trim().split(/\r?\n/).at(-1)?.trim();
|
|
157
|
-
return {status: "OK", url: location.href,
|
|
158
|
-
editorVisible, restoredTimedTestDraft, draftLength: draft.length,
|
|
159
|
-
draftPreview: (editor?.innerText || editor?.textContent || "").trim().slice(0, 160),
|
|
160
|
-
draftSuffix: draft.slice(-200), testMarker: draft.includes("LOOP-V037-FAST-3C1H"),
|
|
161
|
-
userCount: users.length, assistantCount: assistants.length,
|
|
162
|
-
userIds: users, assistantIds: assistants,
|
|
163
|
-
lastAssistantStatus: /^\[CHAT_LOOP_STATUS:(?:CONTINUE|DONE|BLOCKED|HANDOFF)\]$/.test(marker)
|
|
164
|
-
? marker : null};
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
audit.messageEvidence[id] = read?.result || {status: "EMPTY_RESULT"};
|
|
168
|
-
// Only dedicated tests created by this diagnostic may expose their
|
|
169
|
-
// harmless assistant text, for verifying strict status parsing.
|
|
170
|
-
const ownedAcceptance = owned.some(item => item.id === id
|
|
171
|
-
|| (item.id === run?.sourceTabId && id === run?.tabId));
|
|
172
|
-
// The existing read-only Audit may also inspect the CURRENT, exactly
|
|
173
|
-
// matched Worker-owned Chat. It drives the Observer's existing poll,
|
|
174
|
-
// never types into a conversation or creates an Action. Never persist
|
|
175
|
-
// unapproved user reply text for ordinary project sessions.
|
|
176
|
-
const currentOwner = run?.tabId === id
|
|
177
|
-
&& audit.messageEvidence[id]?.url === "https://chatgpt.com" + run.page;
|
|
178
|
-
if (ownedAcceptance || currentOwner) {
|
|
179
|
-
const proof = await chrome.tabs.sendMessage(id,
|
|
180
|
-
{type: "CHAT_LOOP_TURN_VERIFY_V1"}).catch(() => null);
|
|
181
|
-
audit.messageEvidence[id].observerStatus = proof?.status || "UNAVAILABLE";
|
|
182
|
-
const observed = await chrome.tabs.sendMessage(id,
|
|
183
|
-
{type: "CHAT_LOOP_TURN_OBSERVE_V1", poll: true}).catch(() => null);
|
|
184
|
-
audit.messageEvidence[id].turnObservation = observed ? {
|
|
185
|
-
status: observed.status, reason: observed.reason, page: observed.page,
|
|
186
|
-
userKey: observed.userKey, eventCount: observed.eventCount,
|
|
187
|
-
observedAt: observed.observedAt} : null;
|
|
188
|
-
if (ownedAcceptance) audit.messageEvidence[id].observerReplyText =
|
|
189
|
-
typeof proof?.replyText === "string" ? proof.replyText.slice(0, 3000) : null;
|
|
190
|
-
}
|
|
191
|
-
} catch {
|
|
192
|
-
audit.messageEvidence[id] = {status: "PROBE_UNAVAILABLE"};
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (mode === "newTest") {
|
|
196
|
-
if (run && !["DONE", "BLOCKED"].includes(run.phase))
|
|
197
|
-
throw Error("ACTIVE_LOOP_OWNERSHIP");
|
|
198
|
-
// A development-only acceptance entry must never create an unbounded
|
|
199
|
-
// stack of blank/test pages. Existing user homes also consume capacity.
|
|
200
|
-
if (all.some(tab => tab.url === 'https://chatgpt.com/'
|
|
201
|
-
|| owned.some(item => item.id === tab.id)))
|
|
202
|
-
throw Error('ACCEPTANCE_TEST_HOME_LIMIT');
|
|
203
|
-
const tab = await chrome.tabs.create({url: "https://chatgpt.com/", active: true,
|
|
204
|
-
windowId: own.windowId});
|
|
205
|
-
const next = [...owned, {id: tab.id, createdAt: Date.now()}];
|
|
206
|
-
await chrome.storage.local.set({[ownedKey]: next});
|
|
207
|
-
audit.newTestTabId = tab.id;
|
|
208
|
-
audit.ownedTestTabs = next;
|
|
209
|
-
// React can hydrate after Chrome reports the tab created. Do not open
|
|
210
|
-
// Continue setup until two stable, read-only home/editor probes pass.
|
|
211
|
-
let firstReadyAt = 0;
|
|
212
|
-
const deadline = Date.now() + 26000;
|
|
213
|
-
while (Date.now() < deadline) {
|
|
214
|
-
let ready = false;
|
|
215
|
-
try {
|
|
216
|
-
const live = await chrome.tabs.get(tab.id);
|
|
217
|
-
if (live.url === "https://chatgpt.com/") {
|
|
218
|
-
const [probe] = await chrome.scripting.executeScript({
|
|
219
|
-
target: {tabId: tab.id},
|
|
220
|
-
func: () => {
|
|
221
|
-
const visible = el => !!el?.getClientRects().length;
|
|
222
|
-
const mains = [...document.querySelectorAll("main")].filter(visible);
|
|
223
|
-
const editor = document.querySelector('[contenteditable="true"][data-composer-markdown]')
|
|
224
|
-
|| document.querySelector('#prompt-textarea[contenteditable="true"]');
|
|
225
|
-
const draft = (editor?.innerText || editor?.textContent || "").trim();
|
|
226
|
-
const knownTestDraft = (draft.includes("LOOP-V037-FAST-3C1H") || (draft.startsWith("你正在接手上一 Chat 未完成的任务。旧 Chat 的网页历史仍可核实")
|
|
227
|
-
&& draft.includes("【交接参考:只读网页片段,可能截断】")
|
|
228
|
-
&& draft.includes("【原始任务用户消息】")
|
|
229
|
-
&& draft.includes("【最近轮次证据】")
|
|
230
|
-
&& draft.endsWith("是否跨 Chat 由扩展决定,不要主动要求 Handoff。")
|
|
231
|
-
&& draft.length >= 450 && draft.length <= 8192))
|
|
232
|
-
&& ((draft.startsWith("这是 Chat Loop 短轮回归专用测试")
|
|
233
|
-
&& draft.includes("第1条消息:OK1")
|
|
234
|
-
&& draft.includes("第2条消息:OK2")
|
|
235
|
-
&& draft.includes("第3条消息:OK3")
|
|
236
|
-
&& draft.endsWith("校验串:LOOP-V037-FAST-3C1H。")
|
|
237
|
-
&& draft.length >= 200 && draft.length <= 800)
|
|
238
|
-
|| (draft.startsWith("你正在接手上一 Chat 尚未完成的同一个任务")
|
|
239
|
-
&& draft.length >= 450 && (draft.length <= 2000
|
|
240
|
-
|| (draft.length <= 8192 && draft.includes("【原始任务用户消息】")
|
|
241
|
-
&& draft.includes("【最近轮次证据】"))))
|
|
242
|
-
|| (draft.startsWith("你正在接手上一 Chat 未完成的任务。旧 Chat 的网页历史仍可核实")
|
|
243
|
-
&& draft.includes("【原始任务用户消息】") && draft.includes("【最近轮次证据】")
|
|
244
|
-
&& draft.endsWith("是否跨 Chat 由扩展决定,不要主动要求 Handoff。")
|
|
245
|
-
&& draft.length >= 450 && draft.length <= 8192)
|
|
246
|
-
|| (draft.startsWith("这是 Chat Loop 的长回复与一小时边界隔离测试")
|
|
247
|
-
&& draft.includes("MODE-TIMED-1H") && draft.length >= 350 && draft.length <= 800)
|
|
248
|
-
|| (draft.startsWith("这是 Chat Loop 的计数隔离验收")
|
|
249
|
-
&& draft.includes("COUNT-86-100") && draft.endsWith("WALL-CLOCK-TEST。")
|
|
250
|
-
&& draft.length >= 200 && draft.length <= 900)
|
|
251
|
-
|| (draft.startsWith("这是 Chat Loop 的墙钟边界隔离测试")
|
|
252
|
-
&& draft.includes("MODE-WALL-10M") && draft.endsWith("WALL-CLOCK-TEST。")
|
|
253
|
-
&& draft.length >= 250 && draft.length <= 800));
|
|
254
|
-
return {ready: location.href === "https://chatgpt.com/" && mains.length === 1
|
|
255
|
-
&& visible(editor) && (!draft || knownTestDraft)
|
|
256
|
-
&& !mains[0].querySelector('[data-chatgpt-search-message-ids],[data-message-author-role]'),
|
|
257
|
-
discardTestDraft: knownTestDraft};
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
ready = probe?.result?.ready === true;
|
|
261
|
-
if (ready) audit.newTestNeedsDiscardDraft = probe.result.discardTestDraft === true;
|
|
262
|
-
}
|
|
263
|
-
} catch { /* Fail closed if the home page cannot be inspected. */ }
|
|
264
|
-
if (ready) {
|
|
265
|
-
if (firstReadyAt && Date.now() - firstReadyAt >= 2300) break;
|
|
266
|
-
if (!firstReadyAt) firstReadyAt = Date.now();
|
|
267
|
-
} else firstReadyAt = 0;
|
|
268
|
-
await sleep(500);
|
|
269
|
-
}
|
|
270
|
-
audit.newTestReady = firstReadyAt > 0 && Date.now() - firstReadyAt >= 2300;
|
|
271
|
-
} else if (mode === "cleanupStaleMachine") {
|
|
272
|
-
// Diagnostic cleanup only: never close Chat tabs, manual Actions, live
|
|
273
|
-
// machine requests, or an Action with an unknown native result.
|
|
274
|
-
if (!run || !["DONE", "BLOCKED"].includes(run.phase))
|
|
275
|
-
throw Error("ACTIVE_LOOP_CLEANUP_DENIED");
|
|
276
|
-
const names = { "text.html": "chat_text", "dom.html": "chat_dom",
|
|
277
|
-
"screenshot.html": "chat_screenshot",
|
|
278
|
-
"continue.html": "continue", "handoff.html": "handoff" };
|
|
279
|
-
audit.cleanedStaleMachinePages = [];
|
|
280
|
-
audit.protectedStaleMachinePages = [];
|
|
281
|
-
for (const candidate of audit.extensionTabs) {
|
|
282
|
-
let url;
|
|
283
|
-
try { url = new URL(candidate.url); } catch { continue; }
|
|
284
|
-
const action = names[url.pathname.slice(1)];
|
|
285
|
-
const id = url.searchParams.get("requestId");
|
|
286
|
-
if (!action || !candidate.url.startsWith(extensionOrigin + "/")
|
|
287
|
-
|| !/^[a-f0-9]{32}$/.test(id || "") || url.search !== "?requestId=" + id)
|
|
288
|
-
continue;
|
|
289
|
-
const reject = reason => audit.protectedStaleMachinePages.push({
|
|
290
|
-
id: candidate.id, reason });
|
|
291
|
-
const before = all.find(tab => tab.id === candidate.id);
|
|
292
|
-
if (!before || before.active || before.pinned) {
|
|
293
|
-
reject("ACTIVE_OR_PINNED"); continue;
|
|
294
|
-
}
|
|
295
|
-
let proof;
|
|
296
|
-
try {
|
|
297
|
-
proof = await chrome.runtime.sendNativeMessage(
|
|
298
|
-
"com.tomflow.chatloop.extension", {op: "stale-failure", id, action});
|
|
299
|
-
} catch { reject("NATIVE_PROOF_UNAVAILABLE"); continue; }
|
|
300
|
-
if (proof?.status !== "STALE_FAILURE_CONFIRMED"
|
|
301
|
-
|| proof.id !== id || proof.action !== action) {
|
|
302
|
-
reject("NO_STALE_FAILED_RECEIPT"); continue;
|
|
303
|
-
}
|
|
304
|
-
const nowRun = (await chrome.storage.local.get(runKey))[runKey];
|
|
305
|
-
if (!nowRun || !["DONE", "BLOCKED"].includes(nowRun.phase)) {
|
|
306
|
-
reject("OWNER_CHANGED"); break;
|
|
307
|
-
}
|
|
308
|
-
const live = await chrome.tabs.get(candidate.id).catch(() => null);
|
|
309
|
-
if (!live || live.url !== candidate.url || live.active || live.pinned) {
|
|
310
|
-
reject("TAB_CHANGED"); continue;
|
|
311
|
-
}
|
|
312
|
-
await chrome.tabs.remove(candidate.id);
|
|
313
|
-
audit.cleanedStaleMachinePages.push({id: candidate.id, action});
|
|
314
|
-
}
|
|
315
|
-
const left = await chrome.tabs.query({});
|
|
316
|
-
audit.after = {totalTabs: left.length,
|
|
317
|
-
chatTabs: left.filter(tab => isChat(tab.url)).length,
|
|
318
|
-
extensionTabs: left.filter(tab => tab.url?.startsWith(extensionOrigin)
|
|
319
|
-
&& tab.id !== own.id).length};
|
|
320
|
-
} else if (mode === "cleanupBlockedE2E") {
|
|
321
|
-
// One explicitly scoped test Chat only. Closing a tab preserves its
|
|
322
|
-
// canonical ChatGPT history; never close a real user or unknown Turn.
|
|
323
|
-
const tabId=Number(q.get('cleanupBlockedE2E'));
|
|
324
|
-
if(!Number.isSafeInteger(tabId)||tabId<=0 || run?.phase!=='BLOCKED'
|
|
325
|
-
||run.action!=='CONTINUE' ||run.tabId!==tabId
|
|
326
|
-
||typeof run.page!=='string'
|
|
327
|
-
||!/^\/c\/[0-9a-f-]{12,}$/i.test(run.page)
|
|
328
|
-
||!/^cl-intent-e2e-[a-z0-9-]+$/i.test(run.projectId||'')
|
|
329
|
-
||!/^user:[^\s]{8,}$/.test(run.userKey||'')
|
|
330
|
-
||!/^assistant:[^\s]{8,}$/.test(run.assistantKey||''))
|
|
331
|
-
throw Error('BLOCKED_E2E_IDENTITY_UNVERIFIED');
|
|
332
|
-
const expectedUrl='https://chatgpt.com'+run.page;
|
|
333
|
-
const source=await chrome.tabs.get(tabId).catch(()=>null);
|
|
334
|
-
if(!source||source.url!==expectedUrl||source.active||source.pinned)
|
|
335
|
-
throw Error('BLOCKED_E2E_TAB_NOT_SAFE');
|
|
336
|
-
const expectedCode=run.projectId.toUpperCase().replace(/^CL-INTENT-E2E-/, 'CL-INTENT-E2E-');
|
|
337
|
-
async function observe(){
|
|
338
|
-
const [check]=await chrome.scripting.executeScript({
|
|
339
|
-
target:{tabId},func:(code,userKey,assistantKey,expectedPage)=>{
|
|
340
|
-
if(location.origin!=='https://chatgpt.com'||location.pathname!==expectedPage)
|
|
341
|
-
return false;
|
|
342
|
-
const visible=n=>!!n?.getClientRects?.().length;
|
|
343
|
-
const mains=[...document.querySelectorAll('main')].filter(visible);
|
|
344
|
-
if(mains.length!==1)return false;
|
|
345
|
-
const modern=[...mains[0].querySelectorAll('[data-chatgpt-search-unit-key][data-chatgpt-search-message-ids]')];
|
|
346
|
-
const rows=modern.length?modern:[...mains[0].querySelectorAll('[data-message-author-role]')];
|
|
347
|
-
const role=n=>n.getAttribute('data-message-author-role')
|
|
348
|
-
||n.getAttribute('data-chatgpt-search-unit-key')?.split(':').at(-1);
|
|
349
|
-
const id=n=>n.getAttribute('data-message-id')
|
|
350
|
-
||n.getAttribute('data-chatgpt-search-message-ids')?.trim().split(/\s+/)[0];
|
|
351
|
-
const users=rows.filter(n=>role(n)==='user');
|
|
352
|
-
const assistants=rows.filter(n=>role(n)==='assistant');
|
|
353
|
-
const user=users.at(-1),assistant=assistants.at(-1);
|
|
354
|
-
const content=assistant?.querySelector('.markdown')?.innerText
|
|
355
|
-
||assistant?.innerText||assistant?.textContent||'';
|
|
356
|
-
const editor=document.querySelector('[contenteditable="true"][data-composer-markdown]')
|
|
357
|
-
||document.querySelector('#prompt-textarea[contenteditable="true"]');
|
|
358
|
-
const hasUploads=!!document.querySelector('[data-testid*="attachment-preview"],[data-testid*="image-preview"],[data-testid="composer-attachment"]');
|
|
359
|
-
const stillRunning=[...document.querySelectorAll('button[data-testid="stop-button"],button[aria-label="Stop generating"],button[aria-label="停止响应"]')].some(visible);
|
|
360
|
-
return !!(user&&assistant&&'user:'+id(user)===userKey
|
|
361
|
-
&&'assistant:'+id(assistant)===assistantKey
|
|
362
|
-
&&(user.innerText||user.textContent||'').includes(code)
|
|
363
|
-
&&content.trim().endsWith('[CHAT_LOOP_STATUS:CONTINUE]')
|
|
364
|
-
&&visible(editor)&&!(editor.innerText||editor.textContent||'').trim()
|
|
365
|
-
&&!hasUploads&&!stillRunning);
|
|
366
|
-
},args:[expectedCode,run.userKey,run.assistantKey,run.page]});
|
|
367
|
-
return check?.result===true;
|
|
368
|
-
}
|
|
369
|
-
if(!await observe())throw Error('BLOCKED_E2E_FINAL_TURN_NOT_VERIFIED');
|
|
370
|
-
await sleep(1250);
|
|
371
|
-
if(!await observe())throw Error('BLOCKED_E2E_FINAL_TURN_UNSTABLE');
|
|
372
|
-
const latest=(await chrome.storage.local.get(runKey))[runKey];
|
|
373
|
-
const same=latest?.sessionId===run.sessionId&&latest?.phase==='BLOCKED'
|
|
374
|
-
&&latest.action===run.action&&latest.tabId===tabId
|
|
375
|
-
&&latest.userKey===run.userKey&&latest.assistantKey===run.assistantKey;
|
|
376
|
-
const live=await chrome.tabs.get(tabId).catch(()=>null);
|
|
377
|
-
const actionPages=await chrome.tabs.query({});
|
|
378
|
-
if(!same||live?.url!==source.url||live.active||live.pinned
|
|
379
|
-
||actionPages.some(t=>t.url?.startsWith(chrome.runtime.getURL('continue.html?tabId='+tabId))
|
|
380
|
-
||t.url?.startsWith(chrome.runtime.getURL('handoff.html?tabId='+tabId))))
|
|
381
|
-
throw Error('BLOCKED_E2E_SOURCE_CHANGED');
|
|
382
|
-
await chrome.tabs.remove(tabId);
|
|
383
|
-
audit.cleanedBlockedE2E=tabId;
|
|
384
|
-
} else if (mode === "cleanupFailedStartHome") {
|
|
385
|
-
// Only a known failed pre-send Start home, never a Chat conversation.
|
|
386
|
-
const id=Number(q.get('cleanupFailedStartHome'));
|
|
387
|
-
if(!run || run.phase!=='BLOCKED' || run.action!=='START'
|
|
388
|
-
|| run.reason!=='START_SEND_UNVERIFIED'
|
|
389
|
-
|| !/^cl-intent-e2e-[a-z0-9-]+$/i.test(run.projectId||'')
|
|
390
|
-
|| !Number.isSafeInteger(id) || id!==run.attemptedNewTabId)
|
|
391
|
-
throw Error('FAILED_START_OWNERSHIP_UNVERIFIED');
|
|
392
|
-
const tab=await chrome.tabs.get(id).catch(()=>null);
|
|
393
|
-
if(tab?.url!=='https://chatgpt.com/' || tab.active || tab.pinned)
|
|
394
|
-
throw Error('FAILED_START_HOME_NOT_SAFE');
|
|
395
|
-
const [p]=await chrome.scripting.executeScript({target:{tabId:id},func:()=>{
|
|
396
|
-
const visible=e=>!!e?.getClientRects?.().length;
|
|
397
|
-
const mains=[...document.querySelectorAll('main')].filter(visible);
|
|
398
|
-
const editor=document.querySelector('[contenteditable="true"][data-composer-markdown]')
|
|
399
|
-
||document.querySelector('#prompt-textarea[contenteditable="true"]');
|
|
400
|
-
const noUploads=!document.querySelector('[data-testid*="attachment-preview"], [data-testid*="image-preview"], [data-testid="composer-attachment"]');
|
|
401
|
-
return location.href==='https://chatgpt.com/' && mains.length===1
|
|
402
|
-
&& visible(editor) && !(editor.innerText||editor.textContent||'').trim()
|
|
403
|
-
&& !mains[0].querySelector('[data-chatgpt-search-message-ids],[data-message-author-role]')
|
|
404
|
-
&& noUploads;
|
|
405
|
-
}});
|
|
406
|
-
if(p?.result!==true)throw Error('FAILED_START_HOME_NOT_BLANK');
|
|
407
|
-
const latest=(await chrome.storage.local.get(runKey))[runKey];
|
|
408
|
-
const live=await chrome.tabs.get(id).catch(()=>null);
|
|
409
|
-
if(latest?.phase!=='BLOCKED' || latest.sessionId!==run.sessionId
|
|
410
|
-
|| latest.attemptedNewTabId!==id || live?.url!==tab.url
|
|
411
|
-
|| live.active || live.pinned)throw Error('FAILED_START_CHANGED');
|
|
412
|
-
await chrome.tabs.remove(id);
|
|
413
|
-
audit.cleanedFailedStartHome=id;
|
|
414
|
-
} else if (mode === "cleanupTestHome") {
|
|
415
|
-
// Close only an unstarted HOME tab created by this acceptance utility.
|
|
416
|
-
// Never discard a personal draft, real conversation or unknown upload.
|
|
417
|
-
if (!run || !["DONE", "BLOCKED"].includes(run.phase))
|
|
418
|
-
throw Error("ACTIVE_LOOP_CLEANUP_DENIED");
|
|
419
|
-
const targetId = Number(q.get("cleanupTestHome"));
|
|
420
|
-
if (!Number.isSafeInteger(targetId) || targetId <= 0
|
|
421
|
-
|| !owned.some(item => item.id === targetId))
|
|
422
|
-
throw Error("TEST_HOME_NOT_OWNED");
|
|
423
|
-
const tab = await chrome.tabs.get(targetId).catch(() => null);
|
|
424
|
-
if (tab?.url !== "https://chatgpt.com/" || tab.active || tab.pinned
|
|
425
|
-
|| tab.title !== "ChatGPT")
|
|
426
|
-
throw Error("TEST_HOME_NOT_SAFE");
|
|
427
|
-
const [probe] = await chrome.scripting.executeScript({
|
|
428
|
-
target: {tabId: targetId},
|
|
429
|
-
func: () => {
|
|
430
|
-
const visible = el => !!el?.getClientRects?.().length;
|
|
431
|
-
const mains = [...document.querySelectorAll("main")].filter(visible);
|
|
432
|
-
const editor = document.querySelector('[contenteditable="true"][data-composer-markdown]')
|
|
433
|
-
|| document.querySelector('#prompt-textarea[contenteditable="true"]');
|
|
434
|
-
const draft = (editor?.innerText || editor?.textContent || "").trim();
|
|
435
|
-
const known = draft.startsWith("这是 Chat Loop 短轮回归专用测试")
|
|
436
|
-
&& draft.includes("第1条消息:OK1")
|
|
437
|
-
&& draft.includes("第2条消息:OK2")
|
|
438
|
-
&& draft.includes("第3条消息:OK3")
|
|
439
|
-
&& draft.endsWith("校验串:LOOP-V037-FAST-3C1H。")
|
|
440
|
-
&& draft.length >= 200 && draft.length <= 800;
|
|
441
|
-
const noMessages = mains.length === 1
|
|
442
|
-
&& !mains[0].querySelector('[data-chatgpt-search-message-ids],[data-message-author-role]');
|
|
443
|
-
const noUploads = !document.querySelector(
|
|
444
|
-
'[data-testid*="attachment-preview"],[data-testid*="image-preview"],[data-testid="composer-attachment"]');
|
|
445
|
-
return {safe: location.href === "https://chatgpt.com/"
|
|
446
|
-
&& noMessages && noUploads && visible(editor) && (!draft || known),
|
|
447
|
-
knownTestDraft: known, draftLength: draft.length};
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
if (probe?.result?.safe !== true) throw Error("TEST_HOME_DRAFT_UNVERIFIED");
|
|
451
|
-
const latestRun = (await chrome.storage.local.get(runKey))[runKey];
|
|
452
|
-
const live = await chrome.tabs.get(targetId).catch(() => null);
|
|
453
|
-
if (!latestRun || !["DONE", "BLOCKED"].includes(latestRun.phase)
|
|
454
|
-
|| live?.url !== tab.url || live.active || live.pinned
|
|
455
|
-
|| live.title !== tab.title)
|
|
456
|
-
throw Error("TEST_HOME_CHANGED");
|
|
457
|
-
await chrome.tabs.remove(targetId);
|
|
458
|
-
await chrome.storage.local.set({[ownedKey]:
|
|
459
|
-
owned.filter(item => item.id !== targetId)});
|
|
460
|
-
audit.cleanedTestHomeId = targetId;
|
|
461
|
-
audit.cleanedTestHomeHadKnownDraft = probe.result.knownTestDraft;
|
|
462
|
-
} else if (mode === "cleanupVerified") {
|
|
463
|
-
// User-requested surplus Chat Loop test-tab cleanup. Never close ordinary
|
|
464
|
-
// work chats, pinned/active tabs, unknown ownership, or an in-flight run.
|
|
465
|
-
// Require BOTH a dedicated test title AND a verified test-message body.
|
|
466
|
-
const testTitle = /^(?:建会话测试|完成跨Chat验收|生成跨Chat交接包|验证跨Chat交接|确认跨 Chat 交接|跨 Chat 交接验证|验证聊天续接|验证聊天交接|确认交接完成|Chat Loop 验收|Chat Loop测试.*|Chat Loop交接测试|短轮回归测试|定时交接测试|交接测试确认)$/;
|
|
467
|
-
const testBody = /(?:Chat\s*Loop|CHAT-LOOP-ACTION-001|跨\s*Chat.{0,16}交接|验证聊天交接|验证聊天续接|ChatGPT Observer)/i;
|
|
468
|
-
const inFlight = run && !["DONE", "BLOCKED"].includes(run.phase)
|
|
469
|
-
? new Set([run.tabId, run.sourceTabId, run.attemptedNewTabId])
|
|
470
|
-
: new Set();
|
|
471
|
-
audit.cleanedTestTabs = [];
|
|
472
|
-
audit.protectedTests = [];
|
|
473
|
-
for (const candidate of audit.chatTabs) {
|
|
474
|
-
if (!testTitle.test(candidate.title || "")) continue;
|
|
475
|
-
const reject = why => audit.protectedTests.push({id: candidate.id, reason: why});
|
|
476
|
-
if (candidate.active || candidate.pinned || inFlight.has(candidate.id)) {
|
|
477
|
-
reject("ACTIVE_PINNED_OR_IN_FLIGHT"); continue;
|
|
478
|
-
}
|
|
479
|
-
try {
|
|
480
|
-
const [proof] = await chrome.scripting.executeScript({
|
|
481
|
-
target: {tabId: candidate.id},
|
|
482
|
-
func: () => {
|
|
483
|
-
const main = document.querySelector("main");
|
|
484
|
-
if (!main) return {valid: false, reason: "NO_MAIN"};
|
|
485
|
-
let users = [...main.querySelectorAll('[data-message-author-role="user"]')];
|
|
486
|
-
if (!users.length) users = [...main.querySelectorAll('[data-chatgpt-search-unit-key$=":user"]')];
|
|
487
|
-
const text = users.slice(0, 3).map(n => n.innerText || n.textContent || "").join("\n");
|
|
488
|
-
return {valid: true, url: location.href, hasUser: users.length > 0,
|
|
489
|
-
sample: text.slice(0, 1800)};
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
const evidence = proof?.result;
|
|
493
|
-
if (!evidence?.valid || !evidence.hasUser || !testBody.test(evidence.sample)
|
|
494
|
-
|| evidence.url !== candidate.url) {
|
|
495
|
-
reject("TEST_MESSAGE_NOT_VERIFIED"); continue;
|
|
496
|
-
}
|
|
497
|
-
const live = await chrome.tabs.get(candidate.id).catch(() => null);
|
|
498
|
-
if (!live || live.url !== candidate.url || live.title !== candidate.title
|
|
499
|
-
|| live.active || live.pinned) {
|
|
500
|
-
reject("TAB_CHANGED_BEFORE_CLOSE"); continue;
|
|
501
|
-
}
|
|
502
|
-
await chrome.tabs.remove(candidate.id);
|
|
503
|
-
audit.cleanedTestTabs.push({id: candidate.id, url: candidate.url,
|
|
504
|
-
title: candidate.title});
|
|
505
|
-
} catch (err) {
|
|
506
|
-
reject("PROBE_OR_CLOSE_FAILED: " + (err?.message || "unknown"));
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
// Surplus ChatGPT home tabs are not historical conversations. Close only
|
|
510
|
-
// verified blank homes, or a home whose entire draft is a known exact
|
|
511
|
-
// Chat Loop acceptance test template. Never discard an arbitrary draft.
|
|
512
|
-
// Never touch a real /c/ conversation, even if its title is generic.
|
|
513
|
-
audit.cleanedEmptyHomeTabs = [];
|
|
514
|
-
audit.protectedEmptyHomeTabs = [];
|
|
515
|
-
for (const candidate of audit.chatTabs) {
|
|
516
|
-
if (candidate.url !== "https://chatgpt.com/"
|
|
517
|
-
|| candidate.title !== "ChatGPT" || candidate.active || candidate.pinned
|
|
518
|
-
|| inFlight.has(candidate.id)) continue;
|
|
519
|
-
try {
|
|
520
|
-
// An inactive unloaded home may deny script injection. Merely focus
|
|
521
|
-
// it once through the extension, retry read-only, then restore focus;
|
|
522
|
-
// failure must NOT become permission to close the unknown tab.
|
|
523
|
-
const inspectEmpty = () => {
|
|
524
|
-
const editor = document.querySelector("#prompt-textarea")
|
|
525
|
-
|| document.querySelector('[contenteditable="true"][role="textbox"]');
|
|
526
|
-
const draft = editor?.value || editor?.innerText || editor?.textContent || "";
|
|
527
|
-
const hasMessages = !!document.querySelector(
|
|
528
|
-
'main [data-message-author-role], main [data-chatgpt-search-unit-key]');
|
|
529
|
-
const hasUploads = !!document.querySelector(
|
|
530
|
-
'[data-testid*="attachment-preview"], [data-testid*="image-preview"], [data-testid="composer-attachment"]');
|
|
531
|
-
const current = draft.trim();
|
|
532
|
-
const knownTestDraft = current.includes("LOOP-V037-FAST-3C1H")
|
|
533
|
-
&& ((current.startsWith("这是 Chat Loop 短轮回归专用测试")
|
|
534
|
-
&& current.length >= 450 && current.length <= 800)
|
|
535
|
-
|| (current.startsWith("你正在接手上一 Chat 尚未完成的同一个任务")
|
|
536
|
-
&& current.length >= 450 && (current.length <= 2000
|
|
537
|
-
|| (current.length <= 8192 && current.includes("【原始任务用户消息】")
|
|
538
|
-
&& current.includes("【最近轮次证据】"))))
|
|
539
|
-
|| (current.startsWith("你正在接手上一 Chat 未完成的任务。旧 Chat 的网页历史仍可核实")
|
|
540
|
-
&& current.includes("【原始任务用户消息】") && current.includes("【最近轮次证据】")
|
|
541
|
-
&& current.endsWith("是否跨 Chat 由扩展决定,不要主动要求 Handoff。")
|
|
542
|
-
&& current.length >= 450 && current.length <= 8192)
|
|
543
|
-
|| (current.startsWith("这是 Chat Loop 的墙钟边界隔离测试")
|
|
544
|
-
&& current.includes("MODE-WALL-10M") && current.endsWith("WALL-CLOCK-TEST。")
|
|
545
|
-
&& current.length >= 250 && current.length <= 800));
|
|
546
|
-
return {url: location.href, ready: document.readyState === "complete",
|
|
547
|
-
empty: !!editor && !current && !hasMessages && !hasUploads,
|
|
548
|
-
knownTestDraft: !!editor && knownTestDraft && !hasMessages && !hasUploads,
|
|
549
|
-
hasEditor: !!editor, draftLength: current.length,
|
|
550
|
-
hasMessages, hasUploads, title: document.title,
|
|
551
|
-
inputCount: document.querySelectorAll('textarea,[contenteditable="true"]').length};
|
|
552
|
-
};
|
|
553
|
-
let proof;
|
|
554
|
-
try {
|
|
555
|
-
[proof] = await chrome.scripting.executeScript({target: {tabId: candidate.id},
|
|
556
|
-
func: inspectEmpty});
|
|
557
|
-
} catch {
|
|
558
|
-
await chrome.tabs.update(candidate.id, {active: true});
|
|
559
|
-
await sleep(1400);
|
|
560
|
-
try {
|
|
561
|
-
[proof] = await chrome.scripting.executeScript({target: {tabId: candidate.id},
|
|
562
|
-
func: inspectEmpty});
|
|
563
|
-
} finally {
|
|
564
|
-
await chrome.tabs.update(own.id, {active: true});
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
const evidence = proof?.result;
|
|
568
|
-
const live = await chrome.tabs.get(candidate.id).catch(() => null);
|
|
569
|
-
if (evidence?.ready && (evidence.empty || evidence.knownTestDraft)
|
|
570
|
-
&& evidence.url === candidate.url
|
|
571
|
-
&& live?.url === candidate.url && live.title === "ChatGPT"
|
|
572
|
-
&& !live.active && !live.pinned) {
|
|
573
|
-
await chrome.tabs.remove(candidate.id);
|
|
574
|
-
audit.cleanedEmptyHomeTabs.push(candidate.id);
|
|
575
|
-
} else audit.protectedEmptyHomeTabs.push({id: candidate.id,
|
|
576
|
-
reason: "NOT_VERIFIED_EMPTY", evidence, title: live?.title, url: live?.url});
|
|
577
|
-
} catch (err) { audit.protectedEmptyHomeTabs.push({id: candidate.id,
|
|
578
|
-
reason: "PROBE_FAILED", error: err?.message}); }
|
|
579
|
-
}
|
|
580
|
-
// Any previously orphaned read-only acceptance audit page may be closed;
|
|
581
|
-
// do not touch other extension action pages.
|
|
582
|
-
audit.cleanedAuditPages = [];
|
|
583
|
-
for (const item of audit.extensionTabs) {
|
|
584
|
-
if (!item.url?.startsWith(chrome.runtime.getURL("popup.html?audit=1"))) continue;
|
|
585
|
-
const live = await chrome.tabs.get(item.id).catch(() => null);
|
|
586
|
-
if (live && !live.active && !live.pinned && live.url === item.url) {
|
|
587
|
-
await chrome.tabs.remove(item.id);
|
|
588
|
-
audit.cleanedAuditPages.push(item.id);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
const left = await chrome.tabs.query({});
|
|
592
|
-
audit.after = {totalTabs: left.length,
|
|
593
|
-
chatTabs: left.filter(t => isChat(t.url)).map(t => ({id:t.id, title:t.title, url:t.url})),
|
|
594
|
-
extensionTabs: left.filter(t => t.url?.startsWith(extensionOrigin) && t.id !== own.id)
|
|
595
|
-
.map(t => ({id:t.id, url:t.url}))};
|
|
596
|
-
await chrome.storage.local.set({[ownedKey]: owned.filter(item =>
|
|
597
|
-
!audit.cleanedTestTabs.some(cleaned => cleaned.id === item.id))});
|
|
598
|
-
} else if (mode === "cleanup") {
|
|
599
|
-
const id = Number(q.get("cleanup"));
|
|
600
|
-
const sourceOwned = owned.some(item => item.id === run?.sourceTabId);
|
|
601
|
-
const tab = Number.isSafeInteger(id) && id > 0
|
|
602
|
-
? await chrome.tabs.get(id).catch(() => null) : null;
|
|
603
|
-
const isRunTarget = id === run?.tabId && tab?.url === "https://chatgpt.com" + run?.page;
|
|
604
|
-
const isRunSource = id === run?.sourceTabId
|
|
605
|
-
&& tab?.url === "https://chatgpt.com" + run?.sourcePage;
|
|
606
|
-
const provenByRegistry = sourceOwned && isRunTarget;
|
|
607
|
-
// Historical test pair is allowed only when both the persisted DONE
|
|
608
|
-
// identity and an unmistakable dedicated-test title match.
|
|
609
|
-
const historicalTest = run?.action === "HANDOFF" && (isRunTarget || isRunSource)
|
|
610
|
-
&& /^(验证聊天交接|确认交接完成)$/.test(tab?.title || "");
|
|
611
|
-
if (!tab || tab.pinned || run?.phase !== "DONE"
|
|
612
|
-
|| !(provenByRegistry || historicalTest))
|
|
613
|
-
throw Error("CLEANUP_TARGET_NOT_PROVEN_OWNED_DONE_CHAT");
|
|
614
|
-
await chrome.tabs.remove(id);
|
|
615
|
-
audit.cleanedTabId = id;
|
|
616
|
-
await chrome.storage.local.set({[ownedKey]:
|
|
617
|
-
owned.filter(item => item.id !== id && item.id !== run.sourceTabId)});
|
|
618
|
-
}
|
|
619
|
-
const name = "ChatLoop/acceptance/audit-" + Date.now() + ".json";
|
|
620
|
-
const payload = JSON.stringify(audit, null, 2);
|
|
621
|
-
const id = await chrome.downloads.download({
|
|
622
|
-
url: "data:application/json;charset=utf-8," + encodeURIComponent(payload),
|
|
623
|
-
filename: name, conflictAction: "uniquify", saveAs: false
|
|
624
|
-
});
|
|
625
|
-
let complete = false;
|
|
626
|
-
for (let i = 0; i < 40; i++) {
|
|
627
|
-
const found = (await chrome.downloads.search({id}))[0];
|
|
628
|
-
if (found?.state === "complete") {complete = true; break;}
|
|
629
|
-
if (found?.state === "interrupted") break;
|
|
630
|
-
await sleep(150);
|
|
631
|
-
}
|
|
632
|
-
if (!complete) throw Error("AUDIT_DOWNLOAD_UNVERIFIED");
|
|
633
|
-
status.textContent = "ACCEPTANCE_SAVED";
|
|
634
|
-
detail.textContent = name;
|
|
635
|
-
await chrome.tabs.remove(own.id);
|
|
636
|
-
}
|
|
637
|
-
report().catch(err => {
|
|
638
|
-
status.textContent = "ACCEPTANCE_BLOCKED";
|
|
639
|
-
detail.textContent = err?.message || "UNKNOWN_ERROR";
|
|
640
|
-
});
|
|
641
|
-
})();
|