@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/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
|
|
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
|
-
|
|
7
|
+
**运行状态与手动操作**:首轮发送前新 Chat 未就绪时 Worker 记录安全白名单内的失败码并 BLOCKED,绝不补发未知用户消息。Popup 常规环境检测和用户明确点击 Clean 仍可用,且 Clean 必须经过 Worker 同一个 Action 配额准入。原 `popup.html?audit=1`、`newTest` 与各种隐藏测试清理参数已从运行入口移除;升级时遗留的精确 audit 页仅在非活动且到期后由原有资源管理器清理。
|
|
8
8
|
|
|
9
|
-
|
|
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
|
|
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
|
|
|
@@ -50,4 +48,7 @@ Clean 只清空输入框,不发送,不更改 Owner;必须指定精确标
|
|
|
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 正常创建。
|
|
52
50
|
|
|
53
|
-
|
|
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.\
|
|
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.',
|
package/bin/native-host.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(!
|
|
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(
|
|
95
|
-
targetAuthorized(tab,state[runKey],
|
|
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.
|
|
35
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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",
|
|
234
|
-
? "测试首回合准备(不计作 Continue 验收):核对指定空白测试 Chat,申请一次固定测试消息许可。"
|
|
235
|
-
: "手动打开 Continue:核对当前已完成的 Turn,并申请单次发送许可。");
|
|
182
|
+
show("VERIFYING", "手动打开 Continue:核对当前已完成的 Turn,并申请单次发送许可。");
|
|
236
183
|
const prepared = await chrome.runtime.sendMessage({
|
|
237
|
-
type:
|
|
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
|
}
|
package/extension/actions/dom.js
CHANGED
|
@@ -1,25 +1,2 @@
|
|
|
1
|
-
// DOM Action:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
310
|
-
|
|
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:
|
|
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);
|