@tomflow/chatloop-extension 0.3.29 → 0.3.33
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 +20 -164
- package/bin/cli.mjs +53 -6
- package/bin/native-host.mjs +11 -2
- package/extension/acceptance.js +105 -3
- package/extension/actions/clean.js +120 -0
- package/extension/actions/common.js +13 -4
- package/extension/actions/continue.js +3 -1
- package/extension/actions/handoff.js +16 -3
- package/extension/actions/model-intent.js +53 -0
- package/extension/actions/start.js +145 -0
- package/extension/actions/status.js +22 -0
- package/extension/background.js +310 -13
- package/extension/clean.html +5 -0
- package/extension/manifest.json +1 -1
- package/extension/popup.html +1 -1
- package/extension/popup.js +11 -0
- package/extension/start.html +6 -0
- package/extension/status.html +6 -0
- package/lib/bridge.mjs +62 -3
- package/lib/session-file.mjs +74 -0
- package/package.json +2 -2
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Standalone Clean Action: one verified composer only. No sends, Owner mutations or tab closures.
|
|
2
|
+
const statusNode=document.getElementById('status');
|
|
3
|
+
const detailNode=document.getElementById('details');
|
|
4
|
+
const show=(status,detail)=>{statusNode.textContent=status;detailNode.textContent=detail;
|
|
5
|
+
document.body.dataset.result=status==='SUCCESS'?'success':status==='BLOCKED'?'error':'running';};
|
|
6
|
+
const isChat=url=>{try{const u=new URL(url);return u.origin==='https://chatgpt.com'
|
|
7
|
+
&& (u.pathname==='/'||/^\/(?:c\/[0-9a-f-]{12,}|g\/[^/]+\/c\/[0-9a-f-]{12,})$/i.test(u.pathname));}
|
|
8
|
+
catch{return false;}};
|
|
9
|
+
const runKey='chat-loop.action.run.v1';
|
|
10
|
+
const ownedKey='chat-loop.acceptance.owned.v1';
|
|
11
|
+
function targetAuthorized(tab,run,owned,machine,own){
|
|
12
|
+
if(!isChat(tab?.url) || !Number.isSafeInteger(tab?.id))throw Error('CLEAN_TARGET_NOT_CHATGPT');
|
|
13
|
+
if(tab.id===own.id)throw Error('CLEAN_SELF_TARGET');
|
|
14
|
+
if(machine){
|
|
15
|
+
const testHome=tab.url==='https://chatgpt.com/' && Array.isArray(owned)
|
|
16
|
+
&& owned.some(entry=>entry.id===tab.id);
|
|
17
|
+
const owner=run?.tabId===tab.id && tab.url==='https://chatgpt.com'+run?.page;
|
|
18
|
+
if(!testHome&&!owner)throw Error('CLEAN_MACHINE_TARGET_NOT_OWNED');
|
|
19
|
+
if(run && !['DONE','BLOCKED'].includes(run.phase))
|
|
20
|
+
throw Error('CLEAN_ACTIVE_LOOP_DENIED');
|
|
21
|
+
}else{
|
|
22
|
+
if(tab.windowId!==own.windowId)throw Error('CLEAN_WINDOW_CHANGED');
|
|
23
|
+
if(run?.tabId===tab.id&&!['DONE','BLOCKED'].includes(run.phase))
|
|
24
|
+
throw Error('CLEAN_ACTIVE_LOOP_DENIED');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function clear(target,machine){
|
|
28
|
+
const [proof]=await chrome.scripting.executeScript({target:{tabId:target.id},
|
|
29
|
+
func:async expected=>{
|
|
30
|
+
try{
|
|
31
|
+
if(location.origin!=='https://chatgpt.com'||location.href!==expected.url)
|
|
32
|
+
return {status:'TARGET_CHANGED'};
|
|
33
|
+
const visible=node=>!!node?.getClientRects?.().length
|
|
34
|
+
&& getComputedStyle(node).display!=='none';
|
|
35
|
+
const main=[...document.querySelectorAll('main')].filter(visible);
|
|
36
|
+
if(main.length!==1)return {status:'MAIN_UNVERIFIED'};
|
|
37
|
+
const inputs=[...document.querySelectorAll(
|
|
38
|
+
'[contenteditable="true"][data-composer-markdown],#prompt-textarea[contenteditable="true"]')]
|
|
39
|
+
.filter(visible);
|
|
40
|
+
const unique=[...new Set(inputs)];
|
|
41
|
+
if(unique.length!==1)return {status:'COMPOSER_UNVERIFIED'};
|
|
42
|
+
const input=unique[0];
|
|
43
|
+
if(input.closest('[aria-disabled="true"]')
|
|
44
|
+
|| [...document.querySelectorAll(
|
|
45
|
+
'[data-testid*="attachment-preview"],[data-testid*="image-preview"],[data-testid="composer-attachment"]')]
|
|
46
|
+
.some(visible))return {status:'COMPOSER_HAS_ATTACHMENTS'};
|
|
47
|
+
if([...document.querySelectorAll(
|
|
48
|
+
'button[data-testid="stop-button"],button[aria-label="Stop generating"],button[aria-label="停止响应"]')]
|
|
49
|
+
.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
|
+
const before=(input.innerText||input.textContent||'').trim();
|
|
54
|
+
if(before){
|
|
55
|
+
input.focus();
|
|
56
|
+
const range=document.createRange();
|
|
57
|
+
range.selectNodeContents(input);
|
|
58
|
+
const selection=window.getSelection();
|
|
59
|
+
selection.removeAllRanges();selection.addRange(range);
|
|
60
|
+
if(!document.execCommand('delete',false))return {status:'DELETE_REJECTED'};
|
|
61
|
+
}
|
|
62
|
+
await new Promise(resolve=>setTimeout(resolve,400));
|
|
63
|
+
if(location.href!==expected.url)return {status:'TARGET_CHANGED'};
|
|
64
|
+
const remaining=(input.innerText||input.textContent||'').trim();
|
|
65
|
+
if(remaining)return {status:'CLEAN_NOT_CONFIRMED'};
|
|
66
|
+
return {status:'CLEARED',hadDraft:!!before};
|
|
67
|
+
}catch{return {status:'CLEAN_UNVERIFIED'};}
|
|
68
|
+
},args:[{url:target.url,machine}]});
|
|
69
|
+
if(proof?.result?.status!=='CLEARED')throw Error(proof?.result?.status||'CLEAN_UNVERIFIED');
|
|
70
|
+
const live=await chrome.tabs.get(target.id);
|
|
71
|
+
if(!live||live.url!==target.url)throw Error('CLEAN_TARGET_CHANGED_AFTER');
|
|
72
|
+
return {status:'CLEARED',action:'clean',tabId:target.id,hadDraft:proof.result.hadDraft===true};
|
|
73
|
+
}
|
|
74
|
+
async function run(){
|
|
75
|
+
const params=new URLSearchParams(location.search);
|
|
76
|
+
const own=await chrome.tabs.getCurrent();
|
|
77
|
+
if(!own?.id||own.url!==location.href)throw Error('CLEAN_ACTION_TAB_UNVERIFIED');
|
|
78
|
+
const reqId=params.get('requestId');
|
|
79
|
+
const machine=reqId!==null;
|
|
80
|
+
if(machine?([ ...params.keys()].length!==1||!/^[a-f0-9]{32}$/.test(reqId)):
|
|
81
|
+
([...params.keys()].length!==1||!/^[1-9][0-9]*$/.test(params.get('tabId')||'')))
|
|
82
|
+
throw Error('CLEAN_PARAMETERS_INVALID');
|
|
83
|
+
const host='com.tomflow.chatloop.extension', action='clean';
|
|
84
|
+
let receipt,tabId;
|
|
85
|
+
if(machine){
|
|
86
|
+
receipt=await chrome.runtime.sendNativeMessage(host,{op:'acquire',id:reqId,action});
|
|
87
|
+
if(receipt?.status!=='OK'||receipt.id!==reqId||receipt.action!==action
|
|
88
|
+
||typeof receipt.nonce!=='string'||!Number.isSafeInteger(receipt.intent?.tabId))
|
|
89
|
+
throw Error(receipt?.code||'CLEAN_REQUEST_UNVERIFIED');
|
|
90
|
+
tabId=receipt.intent.tabId;
|
|
91
|
+
}else tabId=Number(params.get('tabId'));
|
|
92
|
+
try{
|
|
93
|
+
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);
|
|
96
|
+
show('RUNNING','已确认单个目标,清理其未发送文本。');
|
|
97
|
+
const result=await clear(tab,machine);
|
|
98
|
+
if(machine){
|
|
99
|
+
const saved=await chrome.runtime.sendNativeMessage(host,{
|
|
100
|
+
op:'complete',id:reqId,nonce:receipt.nonce,action,status:'OK',
|
|
101
|
+
payload:JSON.stringify(result)});
|
|
102
|
+
if(saved?.status!=='SAVED'||saved.id!==reqId)throw Error('CLEAN_RECEIPT_UNVERIFIED');
|
|
103
|
+
}
|
|
104
|
+
show('SUCCESS','仅清空输入框;未发送消息或修改 Chat Owner。');
|
|
105
|
+
await chrome.tabs.remove(own.id);
|
|
106
|
+
}catch(e){
|
|
107
|
+
if(machine&&receipt?.nonce){
|
|
108
|
+
try{
|
|
109
|
+
const saved=await chrome.runtime.sendNativeMessage(host,{
|
|
110
|
+
op:'complete',id:reqId,nonce:receipt.nonce,action,status:'FAILED',
|
|
111
|
+
code:String(e.message||'CLEAN_UNVERIFIED').slice(0,120)});
|
|
112
|
+
if(saved?.status==='SAVED'&&saved.id===reqId){
|
|
113
|
+
await chrome.tabs.remove(own.id).catch(()=>{});
|
|
114
|
+
}
|
|
115
|
+
}catch{}
|
|
116
|
+
}
|
|
117
|
+
throw e;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
run().catch(e=>show('BLOCKED',e.message||'CLEAN_FAILED'));
|
|
@@ -115,10 +115,19 @@ function copyText(value) {
|
|
|
115
115
|
try {
|
|
116
116
|
const req=await chrome.runtime.sendNativeMessage(nativeHost,
|
|
117
117
|
{op:"acquire",id:machineId,action});
|
|
118
|
-
if(req?.status==="OK")
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
if(req?.status==="OK") {
|
|
119
|
+
const saved = await chrome.runtime.sendNativeMessage(nativeHost,{
|
|
120
|
+
op:"complete",id:machineId,nonce:req.nonce,action,status:"FAILED",
|
|
121
|
+
code:String(error?.message||"ACTION_FAILED").slice(0,120)
|
|
122
|
+
});
|
|
123
|
+
// Machine errors already have a durable CLI receipt. They must not
|
|
124
|
+
// accumulate as abandoned tabs; manual/unverified errors remain open.
|
|
125
|
+
if(saved?.status==="SAVED" && saved.id===machineId) {
|
|
126
|
+
const own = await chrome.tabs.getCurrent();
|
|
127
|
+
if(own?.id && own.url===location.href)
|
|
128
|
+
await chrome.tabs.remove(own.id).catch(()=>{});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
122
131
|
} catch { /* CLI timeout is UNKNOWN, never success. */ }
|
|
123
132
|
}
|
|
124
133
|
function requireOwnTab(tab) {
|
|
@@ -261,4 +261,6 @@ async function run() {
|
|
|
261
261
|
: "已确认新增一次用户消息;Action 已完成,等待下一轮 Observer。");
|
|
262
262
|
await chrome.tabs.remove(own.id);
|
|
263
263
|
}
|
|
264
|
-
|
|
264
|
+
(new URLSearchParams(location.search).has("requestId")
|
|
265
|
+
? import(chrome.runtime.getURL("actions/model-intent.js")).then(m => m.queueModelIntent("continue")) : run())
|
|
266
|
+
.catch(error => show("BLOCKED", error.message || "ACTION_FAILED"));
|
|
@@ -275,11 +275,22 @@ async function run() {
|
|
|
275
275
|
}
|
|
276
276
|
if (observed?.contract !== "chat-loop.turn-observer.v1")
|
|
277
277
|
throw Error("NEW_CHAT_OBSERVER_UNAVAILABLE");
|
|
278
|
-
const message = "你正在接手上一 Chat 未完成的任务。旧 Chat 的网页历史仍可核实,但当前 Chat
|
|
278
|
+
const message = "你正在接手上一 Chat 未完成的任务。旧 Chat 的网页历史仍可核实,但当前 Chat 没有其隐式上下文。若为项目任务,先按已授权的项目 Skill 或目标仓库真实入口核实规则、进度和上一轮副作用;不要假定每个项目都有 AGENTS.md 或固定 context 目录,也不要重复未知副作用。\n\n交接资料:\n" + contextPacket
|
|
279
279
|
+ "\n\n本轮最终回复末行只能输出一个状态标记:[CHAT_LOOP_STATUS:CONTINUE]、[CHAT_LOOP_STATUS:DONE] 或 [CHAT_LOOP_STATUS:BLOCKED]。是否跨 Chat 由扩展决定,不要主动要求 Handoff。";
|
|
280
|
+
const notice=permit.modelIntent?.id
|
|
281
|
+
? "\n\n【扩展控制回执】本次交接由上一 Chat 的工具意图 "
|
|
282
|
+
+ permit.modelIntent.id + " 触发;已通过唯一 Action 发送。"
|
|
283
|
+
+ (permit.modelIntent.reason ? "\n【交接原因(待核实)】"
|
|
284
|
+
+ permit.modelIntent.reason : "") : "";
|
|
285
|
+
const checkpoint=permit.pendingPlan?.id&&typeof permit.pendingPlan.nextPrompt==="string"
|
|
286
|
+
? "\n\n【上一 Chat 暂存的下一步检查点|到期交接覆盖了 Continue】请求 "
|
|
287
|
+
+ permit.pendingPlan.id + " 原计划:\n"
|
|
288
|
+
+ permit.pendingPlan.nextPrompt
|
|
289
|
+
+ "\n【边界】这只是上一轮模型提出的计划,不代表用户新增批准。新 Chat 必须先核实任务状态、项目规则及已发生的副作用,不能盲目重复。"
|
|
290
|
+
: "";
|
|
280
291
|
result = await sendHandoffOnce(newTab.id, permit.countMode
|
|
281
292
|
? message + `\n\n【计数专用交接指令,优先于前面的历史引用】旧Chat已经完成数字${permit.countValue},本轮只加1,第一行写${permit.countValue + 1};只有到100才能在末行写 [CHAT_LOOP_STATUS:DONE],否则末行写 [CHAT_LOOP_STATUS:CONTINUE]。不能因接班而DONE。`
|
|
282
|
-
: message);
|
|
293
|
+
: message + notice + checkpoint + (permit.modelTurnNote || ""));
|
|
283
294
|
} catch (error) {result = {status: "UNVERIFIED", reason: error.message || "HANDOFF_ERROR"};}
|
|
284
295
|
const committed = await chrome.runtime.sendMessage({type: "CHAT_LOOP_ACTION_RESULT_V1", ...request,
|
|
285
296
|
result: result.status, reason: result.reason,
|
|
@@ -299,4 +310,6 @@ async function run() {
|
|
|
299
310
|
show("SUCCESS", "新 Chat 已收到交接包;旧 Chat 标签已尝试安全清理。");
|
|
300
311
|
await chrome.tabs.remove(own.id);
|
|
301
312
|
}
|
|
302
|
-
|
|
313
|
+
(new URLSearchParams(location.search).has("requestId")
|
|
314
|
+
? import(chrome.runtime.getURL("actions/model-intent.js")).then(m => m.queueModelIntent("handoff")) : run())
|
|
315
|
+
.catch(error => show("BLOCKED", error.message || "HANDOFF_ACTION_FAILED"));
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// The only model-driven entrance reuses the two existing formal Action pages.
|
|
2
|
+
// It registers a decision; it never edits ChatGPT, creates a new Chat, or sends.
|
|
3
|
+
export async function queueModelIntent(action) {
|
|
4
|
+
const params = new URLSearchParams(location.search);
|
|
5
|
+
const id = params.get('requestId');
|
|
6
|
+
if([...params.keys()].length !== 1 || !/^[a-f0-9]{32}$/.test(id||''))
|
|
7
|
+
throw Error('INVALID_MODEL_REQUEST');
|
|
8
|
+
const own = await chrome.tabs.getCurrent();
|
|
9
|
+
if(!own?.id || own.url !== location.href)
|
|
10
|
+
throw Error('MODEL_ACTION_TAB_UNVERIFIED');
|
|
11
|
+
const host='com.tomflow.chatloop.extension';
|
|
12
|
+
const auth = await chrome.runtime.sendNativeMessage(host,{op:'acquire',id,action});
|
|
13
|
+
if(auth?.status!=='OK'||auth.id!==id||auth.action!==action
|
|
14
|
+
||typeof auth.nonce!=='string'||!auth.intent)
|
|
15
|
+
throw Error(auth?.code||'MODEL_REQUEST_UNVERIFIED');
|
|
16
|
+
let outcome;
|
|
17
|
+
try {
|
|
18
|
+
const query=typeof auth.intent.intentId==='string'||auth.intent.lookup===true;
|
|
19
|
+
outcome = await chrome.runtime.sendMessage({
|
|
20
|
+
type:query?'CHAT_LOOP_MODEL_INTENT_STATUS_V1':'CHAT_LOOP_MODEL_INTENT_V1',
|
|
21
|
+
id, action:action.toUpperCase(),intent:auth.intent
|
|
22
|
+
});
|
|
23
|
+
if(outcome?.status!==(query?'STATUS':'QUEUED')
|
|
24
|
+
|| !/^[a-f0-9]{32}$/.test(outcome.intentId||'')
|
|
25
|
+
|| (query&&auth.intent.intentId&&outcome.intentId!==auth.intent.intentId)
|
|
26
|
+
|| (!query&&outcome.intentId!==id)
|
|
27
|
+
||outcome.action!==action.toUpperCase())
|
|
28
|
+
throw Error(outcome?.code||'MODEL_INTENT_DENIED');
|
|
29
|
+
const saved = await chrome.runtime.sendNativeMessage(host,{
|
|
30
|
+
op:'complete',id,nonce:auth.nonce,action,status:'OK',
|
|
31
|
+
payload:JSON.stringify(outcome)
|
|
32
|
+
});
|
|
33
|
+
if(saved?.status!=='SAVED'||saved.id!==id)
|
|
34
|
+
throw Error(saved?.code||'INTENT_RECEIPT_UNVERIFIED');
|
|
35
|
+
await chrome.tabs.remove(own.id);
|
|
36
|
+
} catch(e) {
|
|
37
|
+
// An already-queued intent is not retried. Native failure is diagnostic only.
|
|
38
|
+
try {
|
|
39
|
+
const saved=await chrome.runtime.sendNativeMessage(host,{
|
|
40
|
+
op:'complete',id,nonce:auth.nonce,action,status:'FAILED',
|
|
41
|
+
code:String(e.message||'INTENT_UNVERIFIED').slice(0,120)
|
|
42
|
+
});
|
|
43
|
+
// The CLI already has a durable failure receipt. Keep the page only
|
|
44
|
+
// when that receipt could not be verified; never resubmit the intent.
|
|
45
|
+
if(saved?.status==='SAVED'&&saved.id===id) {
|
|
46
|
+
const live=await chrome.tabs.getCurrent();
|
|
47
|
+
if(live?.id===own.id&&live.url===location.href)
|
|
48
|
+
await chrome.tabs.remove(own.id).catch(()=>{});
|
|
49
|
+
}
|
|
50
|
+
} catch {}
|
|
51
|
+
throw e;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Dedicated first-Turn Action. Only this page creates and sends the initial Web Chat.
|
|
2
|
+
// All subsequent turns are owned by the existing Observer / Continue / Handoff flow.
|
|
3
|
+
const info=document.getElementById('details'),label=document.getElementById('status');
|
|
4
|
+
const pause=ms=>new Promise(resolve=>setTimeout(resolve,ms));
|
|
5
|
+
const show=(s,d)=>{label.textContent=s;info.textContent=d;document.body.dataset.result=s==='SUCCESS'?'success':'error';};
|
|
6
|
+
const host='com.tomflow.chatloop.extension';
|
|
7
|
+
async function newChatReady(id){
|
|
8
|
+
for(let i=0;i<85;i++){
|
|
9
|
+
const tab=await chrome.tabs.get(id).catch(()=>null);
|
|
10
|
+
if(tab?.url==='https://chatgpt.com/'){
|
|
11
|
+
const [found]=await chrome.scripting.executeScript({target:{tabId:id},func:()=>{
|
|
12
|
+
const visible=e=>Boolean(e?.getClientRects?.().length);
|
|
13
|
+
const mains=[...document.querySelectorAll('main')].filter(visible);
|
|
14
|
+
const editor=document.querySelector('[contenteditable="true"][data-composer-markdown]')
|
|
15
|
+
||document.querySelector('#prompt-textarea[contenteditable="true"]');
|
|
16
|
+
return location.origin==='https://chatgpt.com'&&location.pathname==='/'&&mains.length===1
|
|
17
|
+
&&visible(editor)&&!(editor.innerText||editor.textContent||'').trim()
|
|
18
|
+
&&!mains[0].querySelector('[data-chatgpt-search-message-ids],[data-message-author-role]');
|
|
19
|
+
}}).catch(()=>[]);
|
|
20
|
+
if(found?.result===true)return;
|
|
21
|
+
if(found?.result===false&&i>=15)throw Error('NEW_CHAT_NOT_BLANK_OR_READY');
|
|
22
|
+
}
|
|
23
|
+
await pause(420);
|
|
24
|
+
}
|
|
25
|
+
throw Error('NEW_CHAT_NOT_READY');
|
|
26
|
+
}
|
|
27
|
+
async function observerReady(id){
|
|
28
|
+
let proof=await chrome.tabs.sendMessage(id,{type:'CHAT_LOOP_TURN_OBSERVE_V1'}).catch(()=>null);
|
|
29
|
+
if(proof?.contract!=='chat-loop.turn-observer.v1'){
|
|
30
|
+
await chrome.scripting.executeScript({target:{tabId:id},files:['content.js','loop/observer.js']});
|
|
31
|
+
proof=await chrome.tabs.sendMessage(id,{type:'CHAT_LOOP_TURN_OBSERVE_V1'}).catch(()=>null);
|
|
32
|
+
}
|
|
33
|
+
if(proof?.contract!=='chat-loop.turn-observer.v1')throw Error('FIRST_TURN_OBSERVER_UNAVAILABLE');
|
|
34
|
+
}
|
|
35
|
+
async function sendFirst(id,text){
|
|
36
|
+
const [clicked]=await chrome.scripting.executeScript({target:{tabId:id},
|
|
37
|
+
func:async instruction=>{
|
|
38
|
+
try{
|
|
39
|
+
const visible=e=>Boolean(e?.getClientRects?.().length);
|
|
40
|
+
if(location.origin!=='https://chatgpt.com'||location.pathname!=='/')throw Error('START_PAGE_CHANGED');
|
|
41
|
+
const mains=[...document.querySelectorAll('main')].filter(visible);
|
|
42
|
+
const editor=document.querySelector('[contenteditable="true"][data-composer-markdown]')
|
|
43
|
+
||document.querySelector('#prompt-textarea[contenteditable="true"]');
|
|
44
|
+
if(mains.length!==1||!visible(editor)||!(editor.isContentEditable)||!document.contains(editor)
|
|
45
|
+
||(editor.innerText||editor.textContent||'').trim()
|
|
46
|
+
||mains[0].querySelector('[data-chatgpt-search-message-ids],[data-message-author-role]'))
|
|
47
|
+
throw Error('START_COMPOSER_UNVERIFIED');
|
|
48
|
+
const canonical=v=>String(v||'').replace(/\s+/g,'');
|
|
49
|
+
editor.focus();
|
|
50
|
+
if(!document.execCommand('insertText',false,instruction))throw Error('START_INPUT_REJECTED');
|
|
51
|
+
let verified=false;
|
|
52
|
+
for(let i=0;i<25;i++){
|
|
53
|
+
if(location.pathname!=='/')throw Error('START_PAGE_CHANGED_DURING_INPUT');
|
|
54
|
+
if(canonical(editor.innerText)===canonical(instruction)
|
|
55
|
+
||canonical(editor.textContent)===canonical(instruction)){verified=true;break;}
|
|
56
|
+
await new Promise(r=>setTimeout(r,120));
|
|
57
|
+
}
|
|
58
|
+
if(!verified)throw Error('START_INPUT_UNVERIFIED');
|
|
59
|
+
let send;
|
|
60
|
+
for(let i=0;i<16;i++){
|
|
61
|
+
send=document.querySelector('button[data-testid="send-button"]')
|
|
62
|
+
||document.querySelector('button[data-testid="composer-send-button"]')
|
|
63
|
+
||document.querySelector('button[type="submit"][aria-label="发送"]')
|
|
64
|
+
||document.querySelector('button[aria-label="Send message"]');
|
|
65
|
+
if(send&&!send.disabled&&visible(send))break;
|
|
66
|
+
await new Promise(r=>setTimeout(r,120));
|
|
67
|
+
}
|
|
68
|
+
if(!send||send.disabled||!visible(send))throw Error('START_SEND_BUTTON_UNAVAILABLE');
|
|
69
|
+
if(location.pathname!=='/'||(canonical(editor.innerText)!==canonical(instruction)
|
|
70
|
+
&&canonical(editor.textContent)!==canonical(instruction)))
|
|
71
|
+
throw Error('START_INPUT_CHANGED');
|
|
72
|
+
send.click(); // Once only. Worker persisted PENDING before this point.
|
|
73
|
+
return {clickedOnce:true};
|
|
74
|
+
}catch(e){return {clickedOnce:false,reason:e.message||'START_INPUT_UNKNOWN'};}
|
|
75
|
+
},args:[text]});
|
|
76
|
+
if(clicked?.result?.clickedOnce!==true)
|
|
77
|
+
throw Error(clicked?.result?.reason||'START_CLICK_UNVERIFIED');
|
|
78
|
+
// Once clicked: read-only receipt, NEVER retry typing or clicking.
|
|
79
|
+
for(let i=0;i<90;i++){
|
|
80
|
+
const tab=await chrome.tabs.get(id).catch(()=>null);
|
|
81
|
+
const page=tab?.url&&new URL(tab.url).pathname;
|
|
82
|
+
if(/^\/c\/[0-9a-f-]{12,}$/i.test(page||'')){
|
|
83
|
+
const [r]=await chrome.scripting.executeScript({target:{tabId:id},func:expected=>{
|
|
84
|
+
const visible=e=>Boolean(e?.getClientRects?.().length);
|
|
85
|
+
const mains=[...document.querySelectorAll('main')].filter(visible);
|
|
86
|
+
if(mains.length!==1)return null;
|
|
87
|
+
const modern=[...mains[0].querySelectorAll('[data-chatgpt-search-unit-key][data-chatgpt-search-message-ids]')];
|
|
88
|
+
const rows=modern.length?modern:[...mains[0].querySelectorAll('[data-message-author-role]')];
|
|
89
|
+
const role=n=>n.getAttribute('data-message-author-role')
|
|
90
|
+
||n.getAttribute('data-chatgpt-search-unit-key')?.split(':').at(-1);
|
|
91
|
+
const user=rows.filter(n=>role(n)==='user').at(-1);
|
|
92
|
+
const id=user?.getAttribute('data-message-id')
|
|
93
|
+
||user?.getAttribute('data-chatgpt-search-message-ids')?.trim().split(/\s+/)[0];
|
|
94
|
+
const bubble=user?.querySelector('[data-search-result-target]')
|
|
95
|
+
||user?.querySelector('[data-user-message-bubble="true"]')||user;
|
|
96
|
+
const canonical=v=>String(v||'').replace(/\s+/g,'');
|
|
97
|
+
return id&&canonical(bubble?.innerText||bubble?.textContent)===canonical(expected)
|
|
98
|
+
?{userKey:'user:'+id}:null;
|
|
99
|
+
},args:[text]}).catch(()=>[]);
|
|
100
|
+
if(r?.result?.userKey)return {status:'SENT_CONFIRMED',newUserKey:r.result.userKey,
|
|
101
|
+
newPage:page,newTabId:id};
|
|
102
|
+
}
|
|
103
|
+
await pause(420);
|
|
104
|
+
}
|
|
105
|
+
throw Error('FIRST_SEND_RECEIPT_UNKNOWN');
|
|
106
|
+
}
|
|
107
|
+
async function run(){
|
|
108
|
+
const own=await chrome.tabs.getCurrent();
|
|
109
|
+
const q=new URLSearchParams(location.search),id=q.get('requestId');
|
|
110
|
+
if(!own?.id||own.url!==location.href||[...q.keys()].length!==1
|
|
111
|
+
||!/^[a-f0-9]{32}$/.test(id||''))throw Error('START_ACTION_SOURCE_INVALID');
|
|
112
|
+
const proof=await chrome.runtime.sendNativeMessage(host,{op:'acquire',id,action:'start'});
|
|
113
|
+
if(proof?.status!=='OK'||proof.action!=='start'||proof.id!==id
|
|
114
|
+
||typeof proof.nonce!=='string')throw Error(proof?.code||'START_NATIVE_INVALID');
|
|
115
|
+
let prepared,taken,newTab,committed;
|
|
116
|
+
try{
|
|
117
|
+
prepared=await chrome.runtime.sendMessage({type:'CHAT_LOOP_SESSION_START_PREPARE_V1',requestId:id});
|
|
118
|
+
if(prepared?.status!=='PREPARED')throw Error(prepared?.code||'START_PREPARE_DENIED');
|
|
119
|
+
taken=await chrome.runtime.sendMessage({type:'CHAT_LOOP_SESSION_START_TAKE_V1',
|
|
120
|
+
requestId:id,token:prepared.token});
|
|
121
|
+
if(taken?.status!=='OK'||taken.sessionId!==prepared.sessionId
|
|
122
|
+
||typeof taken.prompt!=='string')throw Error(taken?.code||'START_TAKE_DENIED');
|
|
123
|
+
show('RUNNING','已独占会话,正在创建唯一网页 Chat 并确认首轮发送。');
|
|
124
|
+
newTab=await chrome.tabs.create({url:'https://chatgpt.com/',active:true,windowId:own.windowId});
|
|
125
|
+
await newChatReady(newTab.id);
|
|
126
|
+
await observerReady(newTab.id);
|
|
127
|
+
const receipt=await sendFirst(newTab.id,taken.prompt);
|
|
128
|
+
committed=await chrome.runtime.sendMessage({type:'CHAT_LOOP_SESSION_START_RESULT_V1',
|
|
129
|
+
requestId:id,token:prepared.token,...receipt,result:receipt.status});
|
|
130
|
+
if(committed?.status!=='OK')throw Error(committed?.code||'START_COMMIT_UNVERIFIED');
|
|
131
|
+
const stored=await chrome.runtime.sendNativeMessage(host,{op:'complete',
|
|
132
|
+
id,nonce:proof.nonce,action:'start',status:'OK',payload:JSON.stringify(committed)});
|
|
133
|
+
if(stored?.status!=='SAVED')throw Error(stored?.code||'START_RECEIPT_NOT_STORED');
|
|
134
|
+
show('SUCCESS','唯一网页 Chat 已确认;执行由 Observer 和 Continue/Handoff 接管。');
|
|
135
|
+
await chrome.tabs.remove(own.id);
|
|
136
|
+
}catch(e){
|
|
137
|
+
if(taken&&committed?.status!=='OK')
|
|
138
|
+
await chrome.runtime.sendMessage({type:'CHAT_LOOP_SESSION_START_RESULT_V1',
|
|
139
|
+
requestId:id,token:prepared.token,result:'UNVERIFIED',newTabId:newTab?.id}).catch(()=>{});
|
|
140
|
+
await chrome.runtime.sendNativeMessage(host,{op:'complete',id,nonce:proof.nonce,
|
|
141
|
+
action:'start',status:'FAILED',code:String(e.message||'START_UNVERIFIED').slice(0,100)}).catch(()=>{});
|
|
142
|
+
throw e; // Unknown target tab is kept for inspection, never auto-retried.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
void run().catch(e=>show('BLOCKED',e.message||'START_UNVERIFIED'));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Read-only machine Status. Does not create a Chat or modify the browser Owner.
|
|
2
|
+
const host='com.tomflow.chatloop.extension';
|
|
3
|
+
async function run(){
|
|
4
|
+
const own=await chrome.tabs.getCurrent(),q=new URLSearchParams(location.search);
|
|
5
|
+
const id=q.get('requestId');
|
|
6
|
+
if(!own?.id||own.url!==location.href||[...q.keys()].length!==1
|
|
7
|
+
||!/^[a-f0-9]{32}$/.test(id||''))throw Error('INVALID_STATUS_ACTION');
|
|
8
|
+
const proof=await chrome.runtime.sendNativeMessage(host,{op:'acquire',id,action:'status'});
|
|
9
|
+
if(proof?.status!=='OK'||proof.action!=='status'||proof.id!==id)
|
|
10
|
+
throw Error(proof?.code||'STATUS_NATIVE_INVALID');
|
|
11
|
+
const state=await chrome.runtime.sendMessage({type:'CHAT_LOOP_SESSION_STATUS_V1',requestId:id});
|
|
12
|
+
const receipt=await chrome.runtime.sendNativeMessage(host,{op:'complete',id,
|
|
13
|
+
nonce:proof.nonce,action:'status',status:'OK',payload:JSON.stringify(state)});
|
|
14
|
+
if(receipt?.status!=='SAVED')throw Error(receipt?.code||'STATUS_NOT_STORED');
|
|
15
|
+
document.getElementById('status').textContent='SUCCESS';
|
|
16
|
+
document.getElementById('details').textContent='只读状态已返回本机请求;未修改执行 Chat。';
|
|
17
|
+
await chrome.tabs.remove(own.id);
|
|
18
|
+
}
|
|
19
|
+
void run().catch(e=>{
|
|
20
|
+
document.getElementById('status').textContent='UNVERIFIED';
|
|
21
|
+
document.getElementById('details').textContent=e.message||'STATUS_UNKNOWN';
|
|
22
|
+
});
|