@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.
package/README.md CHANGED
@@ -2,13 +2,11 @@
2
2
 
3
3
  ## P0 资源生命周期(Extension Worker 唯一仲裁)
4
4
 
5
- `extension/loop/resources.js` 在既有 Worker 内维护持久资源索引,不引入第二浏览器驱动。正式 Action 同时最多 3 页,已登记执行 Chat 同时最多 2 页(Owner 与 Handoff successor);新建前拒绝超限。已存在的普通 Chat、未知发送、BLOCKED 或带草稿页面不自动关闭。只对精确登记且测试 projectId 为 `cl-intent-e2e-*` 的 DONE Chat 执行空草稿、非激活、URL 未变化的 TTL 回收。用户真实任务的 Chat 不会自动删除;其配额满时安全拒绝新建,需用户确认归属后清理。机器只读 Action 最多 3 分钟后有界回收;写入 Action 仅凭匹配终态与 token 回收。资源创建/关闭/峰值/拒绝写入同一 `chat-loop.resources.v1`,Acceptance Audit 读取该记录;未知收据不补发。`ACTION_RESULT_TIMEOUT_UNVERIFIED` 为安全 BLOCKED,不是自动重试。
5
+ `extension/loop/resources.js` 在既有 Worker 内维护持久资源索引,不引入第二浏览器驱动。正式 Action 同时最多 3 页,已登记执行 Chat 同时最多 2 页(Owner 与 Handoff successor);新建前拒绝超限。已存在的普通 Chat、未知发送、BLOCKED 或带草稿页面不自动关闭。只对精确登记且测试 projectId 为 `cl-intent-e2e-*` 的 DONE Chat 执行空草稿、非激活、URL 未变化的 TTL 回收。用户真实任务的 Chat 不会自动删除;其配额满时安全拒绝新建,需用户确认归属后清理。机器只读 Action 最多 3 分钟后有界回收;写入 Action 仅凭匹配终态与 token 回收。资源创建/关闭/峰值/拒绝写入唯一 `chat-loop.resources.v1` 供 Worker 回溯;未知收据不补发。`ACTION_RESULT_TIMEOUT_UNVERIFIED` 为安全 BLOCKED,不是自动重试。
6
6
 
7
- **失败 Start 诊断(本地 0.3.45)**:首轮发送前若新 Chat 主页未就绪,Worker 保持 BLOCKED/START_SEND_UNVERIFIED;新增仅审计字段 `lastStartFailureCode` 记录安全白名单里的未就绪原因,`popup.html?audit=1` 的失败页探针只提供 DOM 就绪状态和控件存在布尔值,不读草稿/正文/密钥。它不证明页面可自动绕过挑战,也不能授权重发或清理未确认草稿。
7
+ **运行状态与手动操作**:首轮发送前新 Chat 未就绪时 Worker 记录安全白名单内的失败码并 BLOCKED,绝不补发未知用户消息。Popup 常规环境检测和用户明确点击 Clean 仍可用,且 Clean 必须经过 Worker 同一个 Action 配额准入。原 `popup.html?audit=1`、`newTest` 与各种隐藏测试清理参数已从运行入口移除;升级时遗留的精确 audit 页仅在非活动且到期后由原有资源管理器清理。
8
8
 
9
- **手动入口限额**:Popup 的 Clean 点击也必须经 Worker 同一 Action 配额准入;开发验收 New Test 一次最多保留一个已登记测试/首页,不能从旁路无限创建标签。
10
-
11
- **验收边界**:源码测试不等于已安装 Chrome 实测。安装 npm 新包并 Reload 后须由正式 Extension Audit 验证 `resources` 字段及资源前后/峰值;未见该字段即表示仍在运行旧扩展。
9
+ **验收边界**:离线源码回归不等于已安装 Chrome 真机通过;正式安装包的 Owner、资源配额及受控发送需要独立实测,不能用旧版证据替代。
12
10
 
13
11
  Chrome 扩展(9 个独立 Action,含 Status / Start)及其独立 CLI / Native Messaging Host。执行前先看 Help:
14
12
 
@@ -40,7 +38,7 @@ Run: chatloop-extension extension <command> --help
40
38
  {"projectId":"new-distinct-test","prompt":"新的独立任务","confirmed":true,"replaceBlockedSessionId":"11111111-1111-4111-a111-111111111111"}
41
39
  ```
42
40
 
43
- `PAGE_ERROR` 且无原生 Retry 时,Worker 仅观察最多四次;只有原用户消息 ID 未变且连续两次证明错误消失才继续观察;若回查时已经识别到同一 Turn 的可靠完成事件或已发出完成事件,则让已排队的完成回执自然结项,避免 4 秒回查窗口错误覆盖 DONE。其余情况依旧 BLOCKED。正式 `popup.html?audit=1` Audit 包含最近一条真实已核验故障的 `run.lastFaultProof`(不含正文);若仅有页面告警而无此字段,不能宣称 Worker 实际进入了恢复分支。**不能把当前网页错误消失当成原消息未发送的证据**。
41
+ `PAGE_ERROR` 且无原生 Retry 时,Worker 最多四次只读核实同一用户消息;只有同一 Turn 的错误消失并连续稳定,才恢复观察;仍有错误、身份变化或回执不明时 BLOCKED。`lastFaultProof` 存在当前 Owner 的持久状态中,不会把当前网页错误消失推断成“可以重发”。
44
42
 
45
43
  Clean 只清空输入框,不发送,不更改 Owner;必须指定精确标签页 ID 且验证目标。Chrome 已安装扩展不随 npm 包自动安装。本地源码与已发布 npm 版本可能不同,真实结果请看 [CURRENT](../../context/CURRENT.md)。
46
44
 
@@ -49,3 +47,8 @@ Clean 只清空输入框,不发送,不更改 Owner;必须指定精确标
49
47
  **新页准备超时策略(0.3.45)**:Start 新 Chat 编辑器只读等待约 218 秒,Handoff 新 Chat 只读等待约 234 秒,均保持一次新建/一次发送且不跳过 ChatGPT 网页验证。Start CLI 最多等待 310 秒,MCP Tools 转发最多 330 秒;Worker 对 Start/Handoff 的未确认资源等待 330 秒,其它 Action 维持 180 秒。窗口到期仍无确定回执时保留 BLOCKED,禁止自动重发。
50
48
 
51
49
  **0.3.46 Chrome pendingUrl 修复**:Worker 创建正式 Action 时,以自己刚发起的受控 Extension URL 与 Chrome 返回的数值 tabId 登记资源,不能依赖 `tabs.create` 立即返回最终 `tab.url`(它可能仅有 `pendingUrl`)。Action 自身仍需验证真实 URL、Owner、token,任何未确认发送都不得重试;代码回归覆盖过渡 tab.url 缺失但单次 Action 正常创建。
50
+
51
+ **下一源码候选 0.3.48(未发行)**:Worker 内正式串行消息类型统一以显式路由表派发;各处理器自身的来源、一次性凭证和 Owner 身份校验保持不变。未重构或发布网页发送行为,真实已发行版仍见 CURRENT。
52
+
53
+
54
+ **本次源码级熵减(仅候选、未发行)**:删除仅用于旧手动验收的 Popup 脚本、Clock Action、计数/模拟墙钟状态及其专属测试。正式 Start、Continue、Handoff 与 Observer/Owner、十分钟真实时钟、错误隔离、配额与已登记测试 Chat 的安全回收保持实现。继续/交接只能由独立 Action 单次发送,用户未发送草稿不允许清除;过往隐藏验收 URL 不再可调用。确切最新测试数量及未验证项只看 [CURRENT](../../context/CURRENT.md)。
package/bin/cli.mjs CHANGED
@@ -11,7 +11,7 @@ const HELP={
11
11
  '': 'Usage: chatloop-extension <command> [options]\nCommands: capabilities, host, extension\nRun: chatloop-extension <command> --help',
12
12
  host: 'Usage: chatloop-extension host install <extension-id>\nRegisters the local Chrome Native Messaging Host (macOS).',
13
13
  extension: 'Usage: chatloop-extension extension <command>\nCommands: chat text | chat dom | chat screenshot | reload | clean <tab-id> | continue | handoff | status | start\nRun: chatloop-extension extension <command> --help',
14
- 'extension clean':'Usage: chatloop-extension extension clean <tab-id>\nClear only the verified ChatGPT composer. Never send or close a Chat.\nRequires exact tab id of a registered test page or the sole completed Chat Loop Owner.',
14
+ 'extension clean':'Usage: chatloop-extension extension clean <tab-id>\nClear only the verified ChatGPT composer. Never send or close a Chat.\nMachine Clean requires the exact tab id of a terminal Chat Loop Owner; popup Clean requires a user-selected ChatGPT tab.',
15
15
  'extension continue':'Usage: chatloop-extension extension continue (JSON intent on stdin)\nSubmit next-turn text or query receipt; does not send directly.',
16
16
  'extension handoff':'Usage: chatloop-extension extension handoff (JSON intent on stdin)\nRequest early handoff or query receipt; does not transfer directly.',
17
17
  'extension status':'Usage: chatloop-extension extension status\nRead-only, verified active Web Chat status.',
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Chrome Native Messaging: one request per process, no persistent server.
3
- import {readRequest, finish, staleFailureConfirmed} from '../lib/bridge.mjs';
3
+ import {readRequest, finish} from '../lib/bridge.mjs';
4
4
  import {readSession,writeSession} from '../lib/session-file.mjs';
5
5
  const MAX_INPUT=48*1024*1024;
6
6
  const write=obj=>{
@@ -29,10 +29,6 @@ process.stdin.on('data',chunk=>{
29
29
  write({status:'OK',session:readSession()});
30
30
  }else if(m.op==='session-sync'){
31
31
  write({status:'OK',session:writeSession(m.session,m.replaceBlockedSessionId)});
32
- }else if(m.op==='stale-failure'){
33
- const confirmed=staleFailureConfirmed(m.id,m.action);
34
- write({status:confirmed?'STALE_FAILURE_CONFIRMED':'NOT_ELIGIBLE',
35
- id:m.id,action:m.action});
36
32
  }else throw Object.assign(Error('Unknown operation'),{code:'INVALID_OPERATION'});
37
33
  }catch(e){write({status:'FAILED',code:e.code||'NATIVE_HOST_ERROR'});}
38
34
  });
@@ -7,15 +7,12 @@ const isChat=url=>{try{const u=new URL(url);return u.origin==='https://chatgpt.c
7
7
  && (u.pathname==='/'||/^\/(?:c\/[0-9a-f-]{12,}|g\/[^/]+\/c\/[0-9a-f-]{12,})$/i.test(u.pathname));}
8
8
  catch{return false;}};
9
9
  const runKey='chat-loop.action.run.v1';
10
- const ownedKey='chat-loop.acceptance.owned.v1';
11
- function targetAuthorized(tab,run,owned,machine,own){
10
+ function targetAuthorized(tab,run,machine,own){
12
11
  if(!isChat(tab?.url) || !Number.isSafeInteger(tab?.id))throw Error('CLEAN_TARGET_NOT_CHATGPT');
13
12
  if(tab.id===own.id)throw Error('CLEAN_SELF_TARGET');
14
13
  if(machine){
15
- const testHome=tab.url==='https://chatgpt.com/' && Array.isArray(owned)
16
- && owned.some(entry=>entry.id===tab.id);
17
14
  const owner=run?.tabId===tab.id && tab.url==='https://chatgpt.com'+run?.page;
18
- if(!testHome&&!owner)throw Error('CLEAN_MACHINE_TARGET_NOT_OWNED');
15
+ if(!owner)throw Error('CLEAN_MACHINE_TARGET_NOT_OWNED');
19
16
  if(run && !['DONE','BLOCKED'].includes(run.phase))
20
17
  throw Error('CLEAN_ACTIVE_LOOP_DENIED');
21
18
  }else{
@@ -47,9 +44,6 @@ async function clear(target,machine){
47
44
  if([...document.querySelectorAll(
48
45
  'button[data-testid="stop-button"],button[aria-label="Stop generating"],button[aria-label="停止响应"]')]
49
46
  .some(visible))return {status:'TURN_STILL_RUNNING'};
50
- if(expected.machine&&expected.url==='https://chatgpt.com/'
51
- && main[0].querySelector('[data-chatgpt-search-message-ids],[data-message-author-role]'))
52
- return {status:'TEST_HOME_HAS_MESSAGES'};
53
47
  const before=(input.innerText||input.textContent||'').trim();
54
48
  if(before){
55
49
  input.focus();
@@ -91,8 +85,8 @@ async function run(){
91
85
  }else tabId=Number(params.get('tabId'));
92
86
  try{
93
87
  const tab=await chrome.tabs.get(tabId);
94
- const state=await chrome.storage.local.get([runKey,ownedKey]);
95
- targetAuthorized(tab,state[runKey],state[ownedKey],machine,own);
88
+ const state=await chrome.storage.local.get(runKey);
89
+ targetAuthorized(tab,state[runKey],machine,own);
96
90
  show('RUNNING','已确认单个目标,清理其未发送文本。');
97
91
  const result=await clear(tab,machine);
98
92
  if(machine){
@@ -134,6 +134,29 @@ function copyText(value) {
134
134
  if (!tab?.id) throw Error("ACTION_TAB_UNVERIFIED");
135
135
  return tab;
136
136
  }
137
+ // Text and DOM share the same verified read path. Their separate pages
138
+ // select a fixed mode and clipboard settling delay; no extra executor.
139
+ async function runReadAction(mode, clipboardMs) {
140
+ const action="chat_"+mode;
141
+ try {
142
+ const machine=await machineBinding(action);
143
+ const own=requireOwnTab(await chrome.tabs.getCurrent());
144
+ const target=machine ? await machineTarget(machine) : await findTarget(own);
145
+ show("RUNNING", mode+" → ChatGPT tab "+target.id);
146
+ const value=await readChat(target,mode);
147
+ if(!value.length)throw Error("EMPTY_CONTENT");
148
+ if(machine)await machineResult(machine,value);
149
+ else {
150
+ copyText(value);
151
+ await new Promise(resolve=>setTimeout(resolve,clipboardMs));
152
+ }
153
+ await done(own,mode,target);
154
+ }catch(error){
155
+ await machineFailure(action,error);
156
+ fail(error.message || "UNEXPECTED_ERROR",
157
+ "操作未完成;动作页已保留,未对其他 ChatGPT 标签页进行操作。");
158
+ }
159
+ }
137
160
  function done(ownTab, action, target) {
138
161
  show("SUCCESS", action + " completed for tab " + target.id);
139
162
  document.body.dataset.result = "success";
@@ -141,6 +164,6 @@ function copyText(value) {
141
164
  }
142
165
  globalThis.ChatLoopActionCommonV1 = Object.freeze({
143
166
  show, fail, findTarget, readChat, copyText, closeAction, done, requireOwnTab,
144
- machineBinding, machineTarget, machineResult, machineFailure
167
+ machineBinding, machineTarget, machineResult, machineFailure, runReadAction
145
168
  });
146
169
  })();
@@ -31,13 +31,9 @@ async function sendOnce(target, text) {
31
31
  || node?.closest('[data-testid^="conversation-turn-"]')?.getAttribute("data-testid");
32
32
  const user = roles.filter(node => role(node) === "user").at(-1);
33
33
  const answer = roles.filter(node => role(node) === "assistant").at(-1);
34
- if (expected.testSetup) {
35
- if (expected.page !== "/" || roles.length) throw Error("TEST_CHAT_NOT_EMPTY");
36
- } else if ("user:" + idOf(user) !== expected.userKey
37
- || "assistant:" + idOf(answer) !== expected.assistantKey) {
34
+ if ("user:" + idOf(user) !== expected.userKey
35
+ || "assistant:" + idOf(answer) !== expected.assistantKey)
38
36
  throw Error("TURN_IDENTITY_CHANGED");
39
- }
40
- if (!expected.testSetup) {
41
37
  const scope = answer.closest('[data-testid^="conversation-turn-"]')
42
38
  || answer.closest('[data-talvt-turn-state]') || answer;
43
39
  const replyText = (answer.querySelector('.markdown')?.innerText || answer.innerText || '').trim();
@@ -52,50 +48,13 @@ async function sendOnce(target, text) {
52
48
  throw Error("GENERATION_STILL_RUNNING");
53
49
  if (![...scope.querySelectorAll('button[aria-label="复制"],button[aria-label="Copy"],button[data-testid="copy-turn-action-button"]')].some(visible))
54
50
  throw Error("FINAL_UI_EVIDENCE_MISSING");
55
- }
56
51
  let input = document.querySelector('[contenteditable="true"][data-composer-markdown]')
57
52
  || document.querySelector('#prompt-textarea[contenteditable="true"]')
58
53
  || document.querySelector('[data-testid="composer-input"][contenteditable="true"]');
59
54
  if (!visible(input) || input.closest('[aria-disabled="true"]'))
60
55
  throw Error("COMPOSER_UNAVAILABLE");
61
- if ((input.innerText || input.textContent || '').trim()) {
62
- const draft = (input.innerText || input.textContent || '').trim();
63
- if (!expected.testSetup || !expected.discardDraft
64
- || !(draft.includes("LOOP-V037-FAST-3C1H") || (draft.startsWith("你正在接手上一 Chat 未完成的任务。旧 Chat 的网页历史仍可核实")
65
- && draft.includes("【交接参考:只读网页片段,可能截断】")
66
- && draft.includes("【原始任务用户消息】")
67
- && draft.includes("【最近轮次证据】")
68
- && draft.endsWith("是否跨 Chat 由扩展决定,不要主动要求 Handoff。")
69
- && draft.length >= 450 && draft.length <= 8192))
70
- || !((draft.startsWith("这是 Chat Loop 短轮回归专用测试")
71
- && draft.length >= 150 && draft.length <= 800)
72
- || (draft.startsWith("你正在接手上一 Chat 尚未完成的同一个任务")
73
- && draft.length >= 450 && (draft.length <= 2000
74
- || (draft.length <= 8192 && draft.includes("【原始任务用户消息】")
75
- && draft.includes("【最近轮次证据】"))))
76
- || (draft.startsWith("你正在接手上一 Chat 未完成的任务。旧 Chat 的网页历史仍可核实")
77
- && draft.includes("【原始任务用户消息】") && draft.includes("【最近轮次证据】")
78
- && draft.endsWith("是否跨 Chat 由扩展决定,不要主动要求 Handoff。")
79
- && draft.length >= 450 && draft.length <= 8192)
80
- || (draft.startsWith("这是 Chat Loop 的长回复与一小时边界隔离测试")
81
- && draft.includes("MODE-TIMED-1H") && draft.length >= 350 && draft.length <= 800)
82
- || (draft.startsWith("这是 Chat Loop 的墙钟边界隔离测试")
83
- && draft.includes("MODE-WALL-10M") && draft.endsWith("WALL-CLOCK-TEST。")
84
- && draft.length >= 250 && draft.length <= 800)))
85
- throw Error("COMPOSER_NOT_EMPTY");
86
- // Explicit test preparation on the caller-created empty Chat only.
87
- // Never clear the source Chat or use this branch for normal continuation.
88
- input.focus();
89
- const range = document.createRange();
90
- range.selectNodeContents(input);
91
- const selection = window.getSelection();
92
- selection.removeAllRanges();
93
- selection.addRange(range);
94
- if (!document.execCommand("delete", false)) throw Error("TEST_DRAFT_CLEAR_REJECTED");
95
- await new Promise(resolve => setTimeout(resolve, 500));
96
- if ((input.innerText || input.textContent || '').trim())
97
- throw Error("TEST_DRAFT_CLEAR_UNVERIFIED");
98
- }
56
+ if ((input.innerText || input.textContent || '').trim())
57
+ throw Error("COMPOSER_NOT_EMPTY");
99
58
  input.focus();
100
59
  if (!document.execCommand("insertText", false, instruction)) throw Error("INPUT_NOT_ACCEPTED");
101
60
  // DOM writes can precede React composer state. Do not click a stale
@@ -124,9 +83,7 @@ async function sendOnce(target, text) {
124
83
  && canonical(input.textContent) !== canonical(instruction)) throw Error("INPUT_CHANGED_BEFORE_SEND");
125
84
  button.click(); // Once, after durable PENDING commit in the worker. Never retry.
126
85
  for (let n = 0; n < 32; n++) {
127
- if (expected.testSetup ? location.pathname !== "/"
128
- && !/^\/c\/(?:[0-9a-f-]{12,}|local-chatgpt%3A[0-9a-f-]{36})$/i.test(location.pathname)
129
- : location.pathname !== expected.page) throw Error("CHAT_PAGE_CHANGED_AFTER_SEND");
86
+ if (location.pathname !== expected.page) throw Error("CHAT_PAGE_CHANGED_AFTER_SEND");
130
87
  const visibleMain = [...document.querySelectorAll("main")].filter(visible);
131
88
  if (visibleMain.length !== 1) throw Error("CHAT_MAIN_CHANGED_AFTER_SEND");
132
89
  const rows = [...visibleMain[0].querySelectorAll(
@@ -140,10 +97,6 @@ async function sendOnce(target, text) {
140
97
  if (newId && 'user:' + newId !== expected.userKey
141
98
  && (canonical(message?.innerText) === canonical(instruction)
142
99
  || canonical(message?.textContent) === canonical(instruction))) {
143
- if (expected.testSetup && !/^\/c\/[0-9a-f-]{12,}$/i.test(location.pathname)) {
144
- await new Promise(resolve => setTimeout(resolve, 250));
145
- continue;
146
- }
147
100
  return {status: "SENT_CONFIRMED", newUserKey: 'user:' + newId, newPage: location.pathname};
148
101
  }
149
102
  await new Promise(resolve => setTimeout(resolve, 250));
@@ -217,24 +170,18 @@ async function run() {
217
170
  const raw = params.get("tabId") || "";
218
171
  let token = params.get("token") || "";
219
172
  let tabId = Number(raw);
220
- const setup = params.get("setup") === "1";
221
- if (params.has("setup") && (!setup || !raw || token)) throw Error("TEST_SETUP_PARAMETERS_INVALID");
222
- if (params.has("wall") && (!setup || params.get("timed") !== "1"
223
- || !["1", "2", "10m"].includes(params.get("wall")))) throw Error("TEST_WALL_PARAMETERS_INVALID");
224
- if (params.has("count") && (!setup || params.get("count") !== "86"
225
- || params.get("wall") !== "10m" || params.get("timed") !== "1"))
226
- throw Error("COUNT_TEST_PARAMETERS_INVALID");
173
+ if ([...params.keys()].some(k => k !== "tabId" && k !== "token")
174
+ || [...params.keys()].length !== new Set(params.keys()).size)
175
+ throw Error("ACTION_PARAMETERS_INVALID");
227
176
  if (raw && !/^[1-9][0-9]*$/.test(raw)
228
177
  || token && !/^[0-9a-f-]{36}$/.test(token)
229
178
  || token && !raw) throw Error("ACTION_PARAMETERS_INVALID");
230
179
  const own = await chrome.tabs.getCurrent();
231
180
  if (!own?.id || own.url !== location.href) throw Error("ACTION_TAB_UNVERIFIED");
232
181
  if (!token) {
233
- show("VERIFYING", setup
234
- ? "测试首回合准备(不计作 Continue 验收):核对指定空白测试 Chat,申请一次固定测试消息许可。"
235
- : "手动打开 Continue:核对当前已完成的 Turn,并申请单次发送许可。");
182
+ show("VERIFYING", "手动打开 Continue:核对当前已完成的 Turn,并申请单次发送许可。");
236
183
  const prepared = await chrome.runtime.sendMessage({
237
- type: setup ? "CHAT_LOOP_ACTION_TEST_PREPARE_V1" : "CHAT_LOOP_ACTION_MANUAL_PREPARE_V1", action: "CONTINUE",
184
+ type: "CHAT_LOOP_ACTION_MANUAL_PREPARE_V1", action: "CONTINUE",
238
185
  ...(raw ? {tabId} : {})
239
186
  });
240
187
  if (prepared?.status !== "PREPARED")
@@ -257,7 +204,6 @@ async function run() {
257
204
  && result.status === "RETRY_CLICKED") || final?.status !== "OK")
258
205
  throw Error(result.reason || final?.code || "ACTION_RESULT_NOT_COMMITTED");
259
206
  show("SUCCESS", permit.retryMode === "NATIVE" ? "已点击原消息的网页重试一次;没有发送新用户消息。"
260
- : setup ? "测试首回合已确认;等待正式 Continue 与 Handoff 独立执行。"
261
207
  : "已确认新增一次用户消息;Action 已完成,等待下一轮 Observer。");
262
208
  await chrome.tabs.remove(own.id);
263
209
  }
@@ -1,25 +1,2 @@
1
- // DOM Action: read one selected ChatGPT tab and copy its content.
2
- (() => {
3
- const {show, fail, findTarget, readChat, copyText, done, requireOwnTab,
4
- machineBinding, machineTarget, machineResult, machineFailure}
5
- = globalThis.ChatLoopActionCommonV1;
6
- let machine;
7
- async function run() {
8
- machine = await machineBinding("chat_dom");
9
- const own = requireOwnTab(await chrome.tabs.getCurrent());
10
- const target = machine ? await machineTarget(machine) : await findTarget(own);
11
- show("RUNNING", "dom → ChatGPT tab " + target.id);
12
- const value = await readChat(target, "dom");
13
- if (!value.length) throw Error("EMPTY_CONTENT");
14
- if(machine) await machineResult(machine,value);
15
- else copyText(value);
16
- // Let Chrome publish selections to macOS pasteboard before closing.
17
- if(!machine) await new Promise(resolve => setTimeout(resolve, 1000));
18
- await done(own, "dom", target);
19
- }
20
- run().catch(async error => {
21
- await machineFailure("chat_dom",error);
22
- fail(error.message || "UNEXPECTED_ERROR",
23
- "操作未完成;动作页已保留,未对其他 ChatGPT 标签页进行操作。");
24
- });
25
- })();
1
+ // DOM Action: independent entrypoint; shared verified read path.
2
+ void globalThis.ChatLoopActionCommonV1.runReadAction('dom',1000);
@@ -89,56 +89,22 @@ async function verifyOldChat(target, summary, mode = "EXPLICIT") {
89
89
  || execution.result.snapshot.length < 120)) throw Error("SOURCE_SNAPSHOT_UNVERIFIED");
90
90
  return execution.result.snapshot || summary;
91
91
  }
92
- async function waitForNewChat(tabId, allowKnownTestDraftCleanup = false) {
92
+ // Read-only readiness of the new, strictly blank Chat home. Never clear a
93
+ // restored human draft, even if its text resembles a historical test prompt.
94
+ async function waitForNewChat(tabId) {
93
95
  for (let attempt = 0; attempt < 520; attempt++) {
94
96
  const tab = await chrome.tabs.get(tabId);
95
97
  if (tab.url?.startsWith("https://chatgpt.com/")) {
96
98
  try {
97
- const [check] = await chrome.scripting.executeScript({target: {tabId}, func: allowTestClear => {
99
+ const [check] = await chrome.scripting.executeScript({target: {tabId}, func: () => {
98
100
  if (location.origin !== "https://chatgpt.com" || location.pathname !== "/") return false;
99
101
  const input = document.querySelector('[contenteditable="true"][data-composer-markdown]')
100
102
  || document.querySelector('#prompt-textarea[contenteditable="true"]');
101
- if (!input?.getClientRects?.().length) return false;
102
- const draft = (input.innerText || input.textContent || '').trim();
103
- if (!draft) return true;
104
- // An acceptance-only new Chat may restore the exact prior Chat Loop
105
- // test instruction. Never discard a real user's unsent draft.
106
- if (!allowTestClear || !(draft.includes("LOOP-V037-FAST-3C1H") || (draft.startsWith("你正在接手上一 Chat 未完成的任务。旧 Chat 的网页历史仍可核实")
107
- && draft.includes("【交接参考:只读网页片段,可能截断】")
108
- && draft.includes("【原始任务用户消息】")
109
- && draft.includes("【最近轮次证据】")
110
- && draft.endsWith("是否跨 Chat 由扩展决定,不要主动要求 Handoff。")
111
- && draft.length >= 450 && draft.length <= 8192))
112
- || !((draft.startsWith("这是 Chat Loop 短轮回归专用测试")
113
- && draft.length >= 150 && draft.length <= 800)
114
- || (draft.startsWith("你正在接手上一 Chat 尚未完成的同一个任务")
115
- && draft.length >= 450 && (draft.length <= 2000
116
- || (draft.length <= 8192 && draft.includes("【原始任务用户消息】")
117
- && draft.includes("【最近轮次证据】"))))
118
- || (draft.startsWith("你正在接手上一 Chat 未完成的任务。旧 Chat 的网页历史仍可核实")
119
- && draft.includes("【原始任务用户消息】") && draft.includes("【最近轮次证据】")
120
- && draft.endsWith("是否跨 Chat 由扩展决定,不要主动要求 Handoff。")
121
- && draft.length >= 450 && draft.length <= 8192)
122
- || (draft.startsWith("这是 Chat Loop 的长回复与一小时边界隔离测试")
123
- && draft.includes("MODE-TIMED-1H") && draft.length >= 350 && draft.length <= 800)
124
- || (draft.startsWith("这是 Chat Loop 的计数隔离验收")
125
- && draft.includes("COUNT-86-100") && draft.endsWith("WALL-CLOCK-TEST。")
126
- && draft.length >= 200 && draft.length <= 900)
127
- || (draft.startsWith("这是 Chat Loop 的墙钟边界隔离测试")
128
- && draft.includes("MODE-WALL-10M") && draft.endsWith("WALL-CLOCK-TEST。")
129
- && draft.length >= 250 && draft.length <= 800))) return false;
130
- input.focus();
131
- const range = document.createRange();
132
- range.selectNodeContents(input);
133
- const selection = window.getSelection();
134
- selection.removeAllRanges();
135
- selection.addRange(range);
136
- if (!document.execCommand("delete", false)) return false;
137
- return !(input.innerText || input.textContent || '').trim() ? "CLEARED_TEST_DRAFT" : false;
138
- }, args: [allowKnownTestDraftCleanup]});
103
+ return !!input?.getClientRects?.().length
104
+ && !(input.innerText || input.textContent || "").trim();
105
+ }});
139
106
  if (check?.result === true) return;
140
- if (check?.result === "CLEARED_TEST_DRAFT") {await delay(600);continue;}
141
- } catch { /* Only retry a read-only readiness probe, never the send. */ }
107
+ } catch { /* Retry only the read-only readiness probe. */ }
142
108
  }
143
109
  await delay(450);
144
110
  }
@@ -264,10 +230,6 @@ async function run() {
264
230
  // Opening a new Chat is itself a side effect; never create another on failure.
265
231
  newTab = await chrome.tabs.create({url: "https://chatgpt.com/", active: true,
266
232
  windowId: source.windowId, openerTabId: own.id});
267
- const registry = (await chrome.storage.local.get("chat-loop.acceptance.owned.v1"))[
268
- "chat-loop.acceptance.owned.v1"];
269
- const dedicatedTest = Array.isArray(registry)
270
- && registry.some(item => item.id === request.tabId);
271
233
  // Chrome may initially report about:blank while the requested home URL loads.
272
234
  let homeUrlReady=false;
273
235
  for(let i=0;i<40;i++){
@@ -279,7 +241,7 @@ async function run() {
279
241
  const registered=await chrome.runtime.sendMessage({type:'CHAT_LOOP_RESOURCE_CHAT_V1',
280
242
  action:'handoff',stage:'register',tabId:newTab.id});
281
243
  if(registered?.status!=='OK')throw Error(registered?.code||'RESOURCE_REGISTER_UNVERIFIED');
282
- await waitForNewChat(newTab.id, dedicatedTest);
244
+ await waitForNewChat(newTab.id);
283
245
  // The observer must exist before the first user click on the new home page.
284
246
  // A successful send without an observer cannot be automatically continued.
285
247
  let observed;
@@ -306,9 +268,8 @@ async function run() {
306
268
  + permit.pendingPlan.nextPrompt
307
269
  + "\n【边界】这只是上一轮模型提出的计划,不代表用户新增批准。新 Chat 必须先核实任务状态、项目规则及已发生的副作用,不能盲目重复。"
308
270
  : "";
309
- result = await sendHandoffOnce(newTab.id, permit.countMode
310
- ? message + `\n\n【计数专用交接指令,优先于前面的历史引用】旧Chat已经完成数字${permit.countValue},本轮只加1,第一行写${permit.countValue + 1};只有到100才能在末行写 [CHAT_LOOP_STATUS:DONE],否则末行写 [CHAT_LOOP_STATUS:CONTINUE]。不能因接班而DONE。`
311
- : message + notice + checkpoint + (permit.modelTurnNote || ""));
271
+ result = await sendHandoffOnce(newTab.id,
272
+ message + notice + checkpoint + (permit.modelTurnNote || ""));
312
273
  } catch (error) {result = {status: "UNVERIFIED", reason: error.message || "HANDOFF_ERROR"};}
313
274
  const committed = await chrome.runtime.sendMessage({type: "CHAT_LOOP_ACTION_RESULT_V1", ...request,
314
275
  result: result.status, reason: result.reason,
@@ -1,25 +1,2 @@
1
- // TEXT Action: read one selected ChatGPT tab and copy its content.
2
- (() => {
3
- const {show, fail, findTarget, readChat, copyText, done, requireOwnTab,
4
- machineBinding, machineTarget, machineResult, machineFailure}
5
- = globalThis.ChatLoopActionCommonV1;
6
- let machine;
7
- async function run() {
8
- machine = await machineBinding("chat_text");
9
- const own = requireOwnTab(await chrome.tabs.getCurrent());
10
- const target = machine ? await machineTarget(machine) : await findTarget(own);
11
- show("RUNNING", "text → ChatGPT tab " + target.id);
12
- const value = await readChat(target, "text");
13
- if (!value.length) throw Error("EMPTY_CONTENT");
14
- if(machine) await machineResult(machine,value);
15
- else copyText(value);
16
- // Let Chrome publish selections to macOS pasteboard before closing.
17
- if(!machine) await new Promise(resolve => setTimeout(resolve, 250));
18
- await done(own, "text", target);
19
- }
20
- run().catch(async error => {
21
- await machineFailure("chat_text",error);
22
- fail(error.message || "UNEXPECTED_ERROR",
23
- "操作未完成;动作页已保留,未对其他 ChatGPT 标签页进行操作。");
24
- });
25
- })();
1
+ // TEXT Action: independent entrypoint; shared verified read path.
2
+ void globalThis.ChatLoopActionCommonV1.runReadAction('text',250);