@tomflow/chatloop-extension 0.3.46 → 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.
@@ -1,577 +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"].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 === "cleanupFailedStartHome") {
321
- // Only a known failed pre-send Start home, never a Chat conversation.
322
- const id=Number(q.get('cleanupFailedStartHome'));
323
- if(!run || run.phase!=='BLOCKED' || run.action!=='START'
324
- || run.reason!=='START_SEND_UNVERIFIED'
325
- || !/^cl-intent-e2e-[a-z0-9-]+$/i.test(run.projectId||'')
326
- || !Number.isSafeInteger(id) || id!==run.attemptedNewTabId)
327
- throw Error('FAILED_START_OWNERSHIP_UNVERIFIED');
328
- const tab=await chrome.tabs.get(id).catch(()=>null);
329
- if(tab?.url!=='https://chatgpt.com/' || tab.active || tab.pinned)
330
- throw Error('FAILED_START_HOME_NOT_SAFE');
331
- const [p]=await chrome.scripting.executeScript({target:{tabId:id},func:()=>{
332
- const visible=e=>!!e?.getClientRects?.().length;
333
- const mains=[...document.querySelectorAll('main')].filter(visible);
334
- const editor=document.querySelector('[contenteditable="true"][data-composer-markdown]')
335
- ||document.querySelector('#prompt-textarea[contenteditable="true"]');
336
- const noUploads=!document.querySelector('[data-testid*="attachment-preview"], [data-testid*="image-preview"], [data-testid="composer-attachment"]');
337
- return location.href==='https://chatgpt.com/' && mains.length===1
338
- && visible(editor) && !(editor.innerText||editor.textContent||'').trim()
339
- && !mains[0].querySelector('[data-chatgpt-search-message-ids],[data-message-author-role]')
340
- && noUploads;
341
- }});
342
- if(p?.result!==true)throw Error('FAILED_START_HOME_NOT_BLANK');
343
- const latest=(await chrome.storage.local.get(runKey))[runKey];
344
- const live=await chrome.tabs.get(id).catch(()=>null);
345
- if(latest?.phase!=='BLOCKED' || latest.sessionId!==run.sessionId
346
- || latest.attemptedNewTabId!==id || live?.url!==tab.url
347
- || live.active || live.pinned)throw Error('FAILED_START_CHANGED');
348
- await chrome.tabs.remove(id);
349
- audit.cleanedFailedStartHome=id;
350
- } else if (mode === "cleanupTestHome") {
351
- // Close only an unstarted HOME tab created by this acceptance utility.
352
- // Never discard a personal draft, real conversation or unknown upload.
353
- if (!run || !["DONE", "BLOCKED"].includes(run.phase))
354
- throw Error("ACTIVE_LOOP_CLEANUP_DENIED");
355
- const targetId = Number(q.get("cleanupTestHome"));
356
- if (!Number.isSafeInteger(targetId) || targetId <= 0
357
- || !owned.some(item => item.id === targetId))
358
- throw Error("TEST_HOME_NOT_OWNED");
359
- const tab = await chrome.tabs.get(targetId).catch(() => null);
360
- if (tab?.url !== "https://chatgpt.com/" || tab.active || tab.pinned
361
- || tab.title !== "ChatGPT")
362
- throw Error("TEST_HOME_NOT_SAFE");
363
- const [probe] = await chrome.scripting.executeScript({
364
- target: {tabId: targetId},
365
- func: () => {
366
- const visible = el => !!el?.getClientRects?.().length;
367
- const mains = [...document.querySelectorAll("main")].filter(visible);
368
- const editor = document.querySelector('[contenteditable="true"][data-composer-markdown]')
369
- || document.querySelector('#prompt-textarea[contenteditable="true"]');
370
- const draft = (editor?.innerText || editor?.textContent || "").trim();
371
- const known = draft.startsWith("这是 Chat Loop 短轮回归专用测试")
372
- && draft.includes("第1条消息:OK1")
373
- && draft.includes("第2条消息:OK2")
374
- && draft.includes("第3条消息:OK3")
375
- && draft.endsWith("校验串:LOOP-V037-FAST-3C1H。")
376
- && draft.length >= 200 && draft.length <= 800;
377
- const noMessages = mains.length === 1
378
- && !mains[0].querySelector('[data-chatgpt-search-message-ids],[data-message-author-role]');
379
- const noUploads = !document.querySelector(
380
- '[data-testid*="attachment-preview"],[data-testid*="image-preview"],[data-testid="composer-attachment"]');
381
- return {safe: location.href === "https://chatgpt.com/"
382
- && noMessages && noUploads && visible(editor) && (!draft || known),
383
- knownTestDraft: known, draftLength: draft.length};
384
- }
385
- });
386
- if (probe?.result?.safe !== true) throw Error("TEST_HOME_DRAFT_UNVERIFIED");
387
- const latestRun = (await chrome.storage.local.get(runKey))[runKey];
388
- const live = await chrome.tabs.get(targetId).catch(() => null);
389
- if (!latestRun || !["DONE", "BLOCKED"].includes(latestRun.phase)
390
- || live?.url !== tab.url || live.active || live.pinned
391
- || live.title !== tab.title)
392
- throw Error("TEST_HOME_CHANGED");
393
- await chrome.tabs.remove(targetId);
394
- await chrome.storage.local.set({[ownedKey]:
395
- owned.filter(item => item.id !== targetId)});
396
- audit.cleanedTestHomeId = targetId;
397
- audit.cleanedTestHomeHadKnownDraft = probe.result.knownTestDraft;
398
- } else if (mode === "cleanupVerified") {
399
- // User-requested surplus Chat Loop test-tab cleanup. Never close ordinary
400
- // work chats, pinned/active tabs, unknown ownership, or an in-flight run.
401
- // Require BOTH a dedicated test title AND a verified test-message body.
402
- const testTitle = /^(?:建会话测试|完成跨Chat验收|生成跨Chat交接包|验证跨Chat交接|确认跨 Chat 交接|跨 Chat 交接验证|验证聊天续接|验证聊天交接|确认交接完成|Chat Loop 验收|Chat Loop测试.*|Chat Loop交接测试|短轮回归测试|定时交接测试|交接测试确认)$/;
403
- const testBody = /(?:Chat\s*Loop|CHAT-LOOP-ACTION-001|跨\s*Chat.{0,16}交接|验证聊天交接|验证聊天续接|ChatGPT Observer)/i;
404
- const inFlight = run && !["DONE", "BLOCKED"].includes(run.phase)
405
- ? new Set([run.tabId, run.sourceTabId, run.attemptedNewTabId])
406
- : new Set();
407
- audit.cleanedTestTabs = [];
408
- audit.protectedTests = [];
409
- for (const candidate of audit.chatTabs) {
410
- if (!testTitle.test(candidate.title || "")) continue;
411
- const reject = why => audit.protectedTests.push({id: candidate.id, reason: why});
412
- if (candidate.active || candidate.pinned || inFlight.has(candidate.id)) {
413
- reject("ACTIVE_PINNED_OR_IN_FLIGHT"); continue;
414
- }
415
- try {
416
- const [proof] = await chrome.scripting.executeScript({
417
- target: {tabId: candidate.id},
418
- func: () => {
419
- const main = document.querySelector("main");
420
- if (!main) return {valid: false, reason: "NO_MAIN"};
421
- let users = [...main.querySelectorAll('[data-message-author-role="user"]')];
422
- if (!users.length) users = [...main.querySelectorAll('[data-chatgpt-search-unit-key$=":user"]')];
423
- const text = users.slice(0, 3).map(n => n.innerText || n.textContent || "").join("\n");
424
- return {valid: true, url: location.href, hasUser: users.length > 0,
425
- sample: text.slice(0, 1800)};
426
- }
427
- });
428
- const evidence = proof?.result;
429
- if (!evidence?.valid || !evidence.hasUser || !testBody.test(evidence.sample)
430
- || evidence.url !== candidate.url) {
431
- reject("TEST_MESSAGE_NOT_VERIFIED"); continue;
432
- }
433
- const live = await chrome.tabs.get(candidate.id).catch(() => null);
434
- if (!live || live.url !== candidate.url || live.title !== candidate.title
435
- || live.active || live.pinned) {
436
- reject("TAB_CHANGED_BEFORE_CLOSE"); continue;
437
- }
438
- await chrome.tabs.remove(candidate.id);
439
- audit.cleanedTestTabs.push({id: candidate.id, url: candidate.url,
440
- title: candidate.title});
441
- } catch (err) {
442
- reject("PROBE_OR_CLOSE_FAILED: " + (err?.message || "unknown"));
443
- }
444
- }
445
- // Surplus ChatGPT home tabs are not historical conversations. Close only
446
- // verified blank homes, or a home whose entire draft is a known exact
447
- // Chat Loop acceptance test template. Never discard an arbitrary draft.
448
- // Never touch a real /c/ conversation, even if its title is generic.
449
- audit.cleanedEmptyHomeTabs = [];
450
- audit.protectedEmptyHomeTabs = [];
451
- for (const candidate of audit.chatTabs) {
452
- if (candidate.url !== "https://chatgpt.com/"
453
- || candidate.title !== "ChatGPT" || candidate.active || candidate.pinned
454
- || inFlight.has(candidate.id)) continue;
455
- try {
456
- // An inactive unloaded home may deny script injection. Merely focus
457
- // it once through the extension, retry read-only, then restore focus;
458
- // failure must NOT become permission to close the unknown tab.
459
- const inspectEmpty = () => {
460
- const editor = document.querySelector("#prompt-textarea")
461
- || document.querySelector('[contenteditable="true"][role="textbox"]');
462
- const draft = editor?.value || editor?.innerText || editor?.textContent || "";
463
- const hasMessages = !!document.querySelector(
464
- 'main [data-message-author-role], main [data-chatgpt-search-unit-key]');
465
- const hasUploads = !!document.querySelector(
466
- '[data-testid*="attachment-preview"], [data-testid*="image-preview"], [data-testid="composer-attachment"]');
467
- const current = draft.trim();
468
- const knownTestDraft = current.includes("LOOP-V037-FAST-3C1H")
469
- && ((current.startsWith("这是 Chat Loop 短轮回归专用测试")
470
- && current.length >= 450 && current.length <= 800)
471
- || (current.startsWith("你正在接手上一 Chat 尚未完成的同一个任务")
472
- && current.length >= 450 && (current.length <= 2000
473
- || (current.length <= 8192 && current.includes("【原始任务用户消息】")
474
- && current.includes("【最近轮次证据】"))))
475
- || (current.startsWith("你正在接手上一 Chat 未完成的任务。旧 Chat 的网页历史仍可核实")
476
- && current.includes("【原始任务用户消息】") && current.includes("【最近轮次证据】")
477
- && current.endsWith("是否跨 Chat 由扩展决定,不要主动要求 Handoff。")
478
- && current.length >= 450 && current.length <= 8192)
479
- || (current.startsWith("这是 Chat Loop 的墙钟边界隔离测试")
480
- && current.includes("MODE-WALL-10M") && current.endsWith("WALL-CLOCK-TEST。")
481
- && current.length >= 250 && current.length <= 800));
482
- return {url: location.href, ready: document.readyState === "complete",
483
- empty: !!editor && !current && !hasMessages && !hasUploads,
484
- knownTestDraft: !!editor && knownTestDraft && !hasMessages && !hasUploads,
485
- hasEditor: !!editor, draftLength: current.length,
486
- hasMessages, hasUploads, title: document.title,
487
- inputCount: document.querySelectorAll('textarea,[contenteditable="true"]').length};
488
- };
489
- let proof;
490
- try {
491
- [proof] = await chrome.scripting.executeScript({target: {tabId: candidate.id},
492
- func: inspectEmpty});
493
- } catch {
494
- await chrome.tabs.update(candidate.id, {active: true});
495
- await sleep(1400);
496
- try {
497
- [proof] = await chrome.scripting.executeScript({target: {tabId: candidate.id},
498
- func: inspectEmpty});
499
- } finally {
500
- await chrome.tabs.update(own.id, {active: true});
501
- }
502
- }
503
- const evidence = proof?.result;
504
- const live = await chrome.tabs.get(candidate.id).catch(() => null);
505
- if (evidence?.ready && (evidence.empty || evidence.knownTestDraft)
506
- && evidence.url === candidate.url
507
- && live?.url === candidate.url && live.title === "ChatGPT"
508
- && !live.active && !live.pinned) {
509
- await chrome.tabs.remove(candidate.id);
510
- audit.cleanedEmptyHomeTabs.push(candidate.id);
511
- } else audit.protectedEmptyHomeTabs.push({id: candidate.id,
512
- reason: "NOT_VERIFIED_EMPTY", evidence, title: live?.title, url: live?.url});
513
- } catch (err) { audit.protectedEmptyHomeTabs.push({id: candidate.id,
514
- reason: "PROBE_FAILED", error: err?.message}); }
515
- }
516
- // Any previously orphaned read-only acceptance audit page may be closed;
517
- // do not touch other extension action pages.
518
- audit.cleanedAuditPages = [];
519
- for (const item of audit.extensionTabs) {
520
- if (!item.url?.startsWith(chrome.runtime.getURL("popup.html?audit=1"))) continue;
521
- const live = await chrome.tabs.get(item.id).catch(() => null);
522
- if (live && !live.active && !live.pinned && live.url === item.url) {
523
- await chrome.tabs.remove(item.id);
524
- audit.cleanedAuditPages.push(item.id);
525
- }
526
- }
527
- const left = await chrome.tabs.query({});
528
- audit.after = {totalTabs: left.length,
529
- chatTabs: left.filter(t => isChat(t.url)).map(t => ({id:t.id, title:t.title, url:t.url})),
530
- extensionTabs: left.filter(t => t.url?.startsWith(extensionOrigin) && t.id !== own.id)
531
- .map(t => ({id:t.id, url:t.url}))};
532
- await chrome.storage.local.set({[ownedKey]: owned.filter(item =>
533
- !audit.cleanedTestTabs.some(cleaned => cleaned.id === item.id))});
534
- } else if (mode === "cleanup") {
535
- const id = Number(q.get("cleanup"));
536
- const sourceOwned = owned.some(item => item.id === run?.sourceTabId);
537
- const tab = Number.isSafeInteger(id) && id > 0
538
- ? await chrome.tabs.get(id).catch(() => null) : null;
539
- const isRunTarget = id === run?.tabId && tab?.url === "https://chatgpt.com" + run?.page;
540
- const isRunSource = id === run?.sourceTabId
541
- && tab?.url === "https://chatgpt.com" + run?.sourcePage;
542
- const provenByRegistry = sourceOwned && isRunTarget;
543
- // Historical test pair is allowed only when both the persisted DONE
544
- // identity and an unmistakable dedicated-test title match.
545
- const historicalTest = run?.action === "HANDOFF" && (isRunTarget || isRunSource)
546
- && /^(验证聊天交接|确认交接完成)$/.test(tab?.title || "");
547
- if (!tab || tab.pinned || run?.phase !== "DONE"
548
- || !(provenByRegistry || historicalTest))
549
- throw Error("CLEANUP_TARGET_NOT_PROVEN_OWNED_DONE_CHAT");
550
- await chrome.tabs.remove(id);
551
- audit.cleanedTabId = id;
552
- await chrome.storage.local.set({[ownedKey]:
553
- owned.filter(item => item.id !== id && item.id !== run.sourceTabId)});
554
- }
555
- const name = "ChatLoop/acceptance/audit-" + Date.now() + ".json";
556
- const payload = JSON.stringify(audit, null, 2);
557
- const id = await chrome.downloads.download({
558
- url: "data:application/json;charset=utf-8," + encodeURIComponent(payload),
559
- filename: name, conflictAction: "uniquify", saveAs: false
560
- });
561
- let complete = false;
562
- for (let i = 0; i < 40; i++) {
563
- const found = (await chrome.downloads.search({id}))[0];
564
- if (found?.state === "complete") {complete = true; break;}
565
- if (found?.state === "interrupted") break;
566
- await sleep(150);
567
- }
568
- if (!complete) throw Error("AUDIT_DOWNLOAD_UNVERIFIED");
569
- status.textContent = "ACCEPTANCE_SAVED";
570
- detail.textContent = name;
571
- await chrome.tabs.remove(own.id);
572
- }
573
- report().catch(err => {
574
- status.textContent = "ACCEPTANCE_BLOCKED";
575
- detail.textContent = err?.message || "UNKNOWN_ERROR";
576
- });
577
- })();
@@ -1,29 +0,0 @@
1
- // Test-only Action page. The Worker validates owned RUNNING boundary probes or an owned completed Turn before real 20m wall shortening.
2
- (async () => {
3
- const status = document.getElementById('status');
4
- const details = document.getElementById('details');
5
- try {
6
- const q = new URLSearchParams(location.search);
7
- if ([...q.keys()].sort().join(',') !== 'minutes,tabId') throw Error('INVALID_CLOCK_PARAMS');
8
- const tabId = Number(q.get('tabId'));
9
- const minutes = Number(q.get('minutes'));
10
- if (!Number.isSafeInteger(tabId) || tabId <= 0
11
- || !Number.isSafeInteger(minutes) || minutes !== 20 && (minutes < 60 || minutes > 180))
12
- throw Error('INVALID_CLOCK_RANGE');
13
- const own = await chrome.tabs.getCurrent();
14
- if (!own?.id || own.url !== location.href) throw Error('INVALID_CLOCK_ACTION_PAGE');
15
- const result = await chrome.runtime.sendMessage({
16
- type: 'CHAT_LOOP_ACCEPTANCE_CLOCK_ADVANCE_V1', tabId, minutes
17
- });
18
- if (!['ADVANCED_WHILE_RUNNING', 'SHORTENED_AFTER_REAL_20MIN'].includes(result?.status))
19
- throw Error(result?.code || 'CLOCK_ADVANCE_UNVERIFIED');
20
- status.textContent = 'SUCCESS';
21
- details.textContent = result.status === 'SHORTENED_AFTER_REAL_20MIN'
22
- ? 'Owned wall-clock acceptance threshold shortened after real 20 minutes; existing Worker will release only one verified Continue.'
23
- : 'Observer still RUNNING; '+minutes+'m elapsed. Await the actual finished Turn.';
24
- await chrome.tabs.remove(own.id);
25
- } catch (error) {
26
- status.textContent = 'BLOCKED';
27
- details.textContent = error?.message || 'UNKNOWN';
28
- }
29
- })();
@@ -1,4 +0,0 @@
1
- <!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>Chat Loop · Controlled Clock Test</title>
2
- <style>body{font:15px system-ui,sans-serif;margin:24px;max-width:640px}#status{font-weight:bold}#details{white-space:pre-wrap}</style></head><body>
3
- <h1>Chat Loop · Acceptance Clock</h1><p id="status">VERIFYING</p><p id="details">只允许扩展拥有的验收会话:在 RUNNING 时跨越受控边界,或真实已运行20分钟后缩短墙钟测试;不直接发送消息。</p>
4
- <script src="actions/clock.js"></script></body></html>
@@ -1,26 +0,0 @@
1
- [
2
- {
3
- "name": "chat_text",
4
- "description": "Read the text of the single Chat currently owned by Chat Loop Extension. Only when the Extension has an authenticated WAITING_REPLY Owner; returns temp artifact and text, not the clipboard.",
5
- "inputSchema": {"type":"object","properties":{},"additionalProperties":false},
6
- "annotations": {"readOnlyHint":true}
7
- },
8
- {
9
- "name": "chat_dom",
10
- "description": "Read the DOM HTML of the single Chat currently owned by Chat Loop Extension. Requires authenticated Owner; writes a private OS-temp artifact.",
11
- "inputSchema": {"type":"object","properties":{},"additionalProperties":false},
12
- "annotations": {"readOnlyHint":true}
13
- },
14
- {
15
- "name": "chat_screenshot",
16
- "description": "Capture a PNG of the single Chat currently owned by Chat Loop Extension. The tool returns actual image content; this action activates the Chat tab.",
17
- "inputSchema": {"type":"object","properties":{},"additionalProperties":false},
18
- "annotations": {"readOnlyHint":false}
19
- },
20
- {
21
- "name": "reload",
22
- "description": "Request a reload of Chat Loop Extension only, not any other extension. Due to self-reload, receipt can only be ACCEPTED_UNVERIFIED.",
23
- "inputSchema": {"type":"object","properties":{},"additionalProperties":false},
24
- "annotations": {"readOnlyHint":false}
25
- }
26
- ]