@tomflow/chatloop-extension 0.0.0-stage → 0.3.27

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
@@ -1,3 +1,173 @@
1
- # Temporary Holding Version
1
+ # @tomflow/chatloop-extension|Chat Loop Extension
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > **当前源码 v0.3.27;正式工作窗口 10 分钟;仍是六个正式 Action。** 本包负责受控 Chrome 页面中的观察、一次性授权与动作回执;模型不能主动请求 Handoff。Chrome 安装版本、实站验收结果以 [`CURRENT`](../../context/CURRENT.md) 为准;历史版本的完整叙述已归档至 [`旧版记录`](../../work/v037-acceptance/legacy-project-notes-2026-11-04.md)。
4
+
5
+ **当前边界**:Chat ↔ MCP ↔ Mac 是任务工具通道;扩展 → Chat 是单向生命周期调度。扩展包自有 CLI 与按需 Native Messaging Host 已实现本机 IPC;Chrome 真机机器回执与远端 MCP 仍待验收。完整策略见 [`REQ-001`](../../requirements/REQ-001.md) 和 [`三端运行流程`](../../docs/15-mvp-three-end-runtime-flow.md)。
6
+
7
+ ## 1. 包与运行边界
8
+
9
+ | 组成 | 位置 | 当前职责与状态 |
10
+ | --- | --- | --- |
11
+ | Chrome 扩展 | `extension/manifest.json`(MV3,v0.3.27) | **已实现**:环境检测、Screenshot、DOM、Text、Reload |
12
+ | Eye 动作页 | `extension/{screenshot,dom,text,reload}.html` + `actions/{common,screenshot,dom,text,reload}.js` | **已实现**:每页只执行相应动作,复用底层逻辑;成功关闭、失败留页 |
13
+ | Continue 动作页 | `extension/continue.html` + `actions/continue.js` | **已实现并实测**:持单次授权才能编辑 Chat 输入框并点击一次发送;成功关闭,失败阻断 |
14
+ | ChatGPT 页面脚本 | `extension/content.js` | **已实现**:受限 ChatGPT 页面 Eye 读取;Reload 后仅对明确目标按需重新注入 |
15
+ | 扩展后台 | `extension/background.js` | **已实现并实测**:核验 Observer 事件和状态标记,持久化单循环的状态,按结果创建 Continue 或 Handoff Action 页;后台不编辑 Chat DOM;兼容 Reload |
16
+ | Popup | `extension/popup.html` + `popup.js` | **已实现**:当前网页环境检测、动作入口及脱敏的最近发送状态(无 token/正文) |
17
+ | 扩展 CLI / Native Host | `bin/cli.mjs`、`bin/native-host.mjs`,命令 `chatloop-extension` | **Node 层已实现**四个正式 Action 的机器回执通道,Chrome 真机还待验收 |
18
+ | 自驱动协议 | `extension/loop/core.js` | **已实现**:严格解析末行状态标记;不调用模型 |
19
+ | 只读轮次观察 | `extension/loop/observer.js` | **已实站验收普通 Turn**:观察完成后上报本扩展后台;不点击、不写入聊天 |
20
+ | Handoff 动作页 | `extension/handoff.html` + `actions/handoff.js` | **旧显式模式及真实 1 小时历史版本已验收**;当前 v0.3.26 是 Worker 十分钟到期后调度的正式 TIMED Handoff;当前十分钟实际实站以 CURRENT 为准,不将旧 1h 误报为新版本验收 |
21
+ | 回归测试 | `test/{eye,loop,observer,handoff-dom}.test.mjs` | **已实现**:Eye 动作及模拟 DOM 循环行为测试 |
22
+
23
+ `package.json` 与 `extension/manifest.json` 同为 **0.3.27**;npm 安装包**不会自动向 Chrome 安装或加载扩展**。
24
+
25
+ **运行事实与旧版验收**:Reload 后可通过 Worker 持久 Owner 重新绑定正在生成的 Observer;正常延时 Handoff 已在旧版真墙钟和受控时钟下取得实站回执。原始测试位于 `work/v037-acceptance/`,版本与结论以 CURRENT 为准,不把正常路径的 PASS 当作异常 E2E 已通过。Plugin→本机 CLI→扩展正式回执尚未实现。
26
+
27
+ ## 2. API:外部动作入口(已实现)
28
+
29
+ 以下是 **Chrome 内置扩展页 URL**,不是 HTTP REST API,也不是 npm CLI/MCP 命令。扩展 ID 请从已安装的 `chrome://extensions` 中读取,不得硬编码当前机器的 ID。
30
+
31
+ | URL 路径 | 可选查询参数 | 实际动作 | 成功证据 |
32
+ | --- | --- | --- | --- |
33
+ | `screenshot.html` | `?tabId=N` | 激活目标 ChatGPT 标签页,截取**可见区域** | Chrome 下载状态 `complete`,PNG 存到 `Downloads/ChatLoop/eye/` |
34
+ | `dom.html` | `?tabId=N` | 读取 `document.documentElement.outerHTML` | 将完整 HTML 写入**本机系统剪贴板** |
35
+ | `text.html` | `?tabId=N` | 读取 `main.innerText`;没有 main 时取 `body.innerText` | 将纯文本写入**本机系统剪贴板** |
36
+ | `reload.html` | 无 | 通过 service worker 关闭该动作页并 `chrome.runtime.reload()` | 扩展可重新使用;不重载其他扩展、不强制刷新聊天页 |
37
+ | `continue.html` | 自动调度 `?tabId=N&token=...`;手动验收 `?tabId=N`(多标签必须明确) | 正常 Continue:完成 Turn 后单次提交新消息;普通错误 Retry 模式:对**原请求**只点击页面自带重试一次 | 正常以新增 user ID 为回执;Retry 不新建用户消息,错误不再重试;不可信则 BLOCKED |
38
+ | `handoff.html` | 自动调度 `?tabId=N&token=...`;手动验收 `?tabId=N`(多标签必须明确) | 十分钟完整 Turn 后 TIMED Handoff;或明确容量错误下 ERROR Handoff;只读旧 Chat 快照并单次发送新 Chat | 新 Chat/user ID 与完整正文确认后才转移所有权;未知回执时 BLOCKED |
39
+
40
+ 完整 URL 形式:`chrome-extension://<EXTENSION_ID>/text.html?tabId=123`。调用者只需打开动作页,无需按按钮;成功后动作页自动关闭,失败时保留页面并显示错误码。Screenshot 会切换目标标签页到活动状态,属于明确的用户界面副作用。
41
+
42
+ **当前目录(六个正式 Action)**:所有动作页面仍位于扩展根目录,保留既有 URL;业务实现分别位于 `extension/actions/{screenshot,dom,text,reload,continue,handoff}.js`,仅 Eye 四动作共享 `actions/common.js` 的选中目标/读回/剪贴板/关闭等通用函数。Observer 在 `loop/observer.js`,Worker 在 `background.js`。扩展不再自带临时 `test/*.html` 手动测试页面;自动化测试保留在 npm 包的 `test/*.test.mjs`。
43
+
44
+ **Action 边界(强制)**:Observer 只读核验整轮 `TURN_FINISHED` 并上报;Service Worker 只调度,先持久化 `RESERVED` 再创建正式 `continue.html` / `handoff.html`;只有对应 Action 页可领取一次性许可、核实目标身份、执行一次必要网页写入并验证新 user ID。Continue 要求当前完成回复为 `CONTINUE`,Handoff 正常路径要求扩展的 **10 分钟时机**及此前已完成的 `CONTINUE`,而非模型主动 `HANDOFF`。`PENDING` 回执未知、草稿非空、身份漂移或仍在生成均不重发。DONE 停止;BLOCKED/UNKNOWN 保守阻断;会话容量类错误已实现 ERROR Handoff 的离线合约,真实 Chrome 错误交接尚待验收。没有新增服务/数据库/付费 API。
45
+
46
+ **唯一 Owner 准入(v0.3.26 修正)**:只有扩展正式 Action 明确准备并确认已发送 user 消息的当前 Chat 才具有 Loop 所有权;自动 `CHAT_LOOP_TURN_FINISHED_V1` 事件必须匹配持久 `WAITING_REPLY` Owner 的 tabId、规范化会话 URL、user ID 和最终 assistant ID。**不允许在无运行记录或旧运行 DONE/BLOCKED 时仅凭另一 Chat 的 `CONTINUE` 标记自动启动**。受控测试入口和手动明确 Action 仍能在原运行结束后授权新的 Owner;未来 Plugin 新任务启动尚未实现。Handoff 成功回执后只保留新的唯一 Owner,旧 Chat 不再参与调度。规则见 [`REQ-001`](../../requirements/REQ-001.md);v0.3.25 86→100 真实记录保持 PASS,v0.3.26 的新增 Owner 约束目前经本地回归验证,不能冒充新的真实 E2E。
47
+
48
+ **86→100 专用真实验收(v0.3.25)**:唯一专用启动 `continue.html?tabId=N&setup=1&timed=1&wall=10m&count=86`(若已确认存在扩展生成的测试草稿,在 `setup=1` 后追加 `&discardDraft=1`)。必须由正式验收工具先登记、证明目标为空且属于专用测试 Chat;普通 Continue 不受此测试节流影响。首轮 86→87,后续每次只加1;Worker 核验模型输出数字严格等于前次+1,非100只能 CONTINUE,100才 DONE。每次新 Continue 在上次实际发送确认后至少 **60 秒**才能被授权;完成早于60秒进入持久 `WAITING_INTERVAL`,一次性时钟释放,不允许补发积压次数。Chat 到独立10分钟截止时优先转 TIMED Handoff,不打断在途 Turn;Handoff 动作把已确认数字及只读进度交给新 Chat,新 Chat 第一轮继续 +1,不能因接班直接 DONE。保存字段 `countMode/countValue/countLastSendAt/countHandoffs` 限制于该验收;不使用额外服务、付费模型或新 Action。网页安全挑战和不确定发送回执仍按正常规则 BLOCKED。**v0.3.25 是否真实到100请以 CURRENT 的现场记录为准,不能用模拟测试代替。**
49
+
50
+ **轮询频率(当前 v0.3.26)**:用户要求延长的是 Observer 的 DOM 轮询,不是 Continue 发送完成后的固定等待,也不是 Handoff 计时。轮询从 900ms 经 15 秒临时调整至 **60000ms(60 秒)**,保留完整 Turn、内容稳定窗口及一次性发送的校验;Chrome Worker 每分钟保活和到期单次 Alarm 仍独立。轮询慢会增加 Turn 结束被发现的延迟(通常需跨两次采样),无法保证解决 Cloudflare 安全验证;出现挑战仍阻断。
51
+
52
+ **历史十分钟算术回归(v0.3.22–v0.3.24,非当前86→100验收)**:正式普通 Continue 提示仅做等价精简,仍要求任务完成时返回唯一 `CONTINUE/DONE/BLOCKED` 标记,不混入测试问题。10m 受控测试 Chat 使用独立算术题:首轮 `1+1`,后续 `1+3`、`2+3` 等,每个完成 Turn 只申请一条下一题;题号在 Worker 原持久运行态中随成功收件和完整 Turn 变化,不能因重复完成事件而跳号。**交接仍由真实时间节点决定**,不由题目/轮数决定;原有 `SECURITY_CHALLENGE / BLOCKED` 继续保留,改变测试文本不表示可以跳过站点安全验证。测试设计与运行结论见 `work/v037-acceptance/`、CURRENT。
53
+
54
+ **到期准备交接(当前 v0.3.26 代码契约)**:Worker 使用现有 Chrome `alarms` 同一套机制,在持久 `startedAt + windowMs` 截止时间设一次性唤醒,原每分钟 Observer 唤醒作为延迟兜底。时间节点独立持久 `handoffPending=true`/`handoffPendingAt`,不生成 Handoff Action、不编辑 Chat;真实到期后禁止**新授权** Continue,但原有 `RESERVED/PENDING` Continue 凭既有 token 继续完成发送和回执,其关联 Turn 完成后才允许 Handoff。Turn 完成事件和 Action 回执继续通过 Worker 同一串行队列,授权前复查时钟;新 Chat 确认收件才转移 Owner、清除交接准备标记并重新起钟。测试与实站状态以 CURRENT 为准,不把新增代码视为真实端到端已通过。
55
+
56
+ **十分钟调度与 Action 自闭环(当前 MVP)**:Observer 的页面计时器每 60 秒采样(临时;旧为 15 秒),Worker 使用 `chrome.alarms` 每分钟唤醒一次已在 `WAITING_REPLY` 的目标 Observer(后台节流时可能仍有延迟)。任务时间从首次观察到任务用户 Turn 开始,Worker 将 `startedAt` 持久化并在多轮 Continue 中沿用;每满十分钟且仍需继续时选择 Handoff。**时限到达仍在推理、工具运行、DOM 状态不明确时不得发送**;普通任务在十分钟以内完成且明确 CONTINUE 时立即调度下一次 Continue;仅专用 86→100 验收受 Worker 的 60 秒发送授权间隔约束,只等 Observer 真实 `TURN_FINISHED` 后作选择;DONE/BLOCKED 优先停发。Handoff 成功转移到新 Chat 后重置十分钟时钟。时间策略的需求真源见 [REQ-001](../../requirements/REQ-001.md)。
57
+
58
+ **运行状态识别口径(源码核对,2026-11-05)**:对于已有未完成 User Turn,产品层面只用三种明确结果:`RUNNING`(等待模型/工具/继续生成)、`TURN_FINISHED`(当前助手回复真实完成,不代表任务 DONE)、`ERROR`(页面或连接出现明确中止错误,Observer 当前编码为 `BLOCKED`)。另有 `IDLE`(没有新的待处理 Turn)和 `UNKNOWN`(DOM/身份/工具/完成证据不足),它们是安全保护状态,**不得合并为正常完成,也不应将无法判断伪称为已确认错误**。当已出现新 user ID 但没有相应 assistant,或有可见 Stop、`aria-busy` / 工具运行标记、未完成的 Turn 标记、正在确认文本稳定期时,保持 RUNNING。正常完成需要同一 user/assistant 身份、无 Stop/运行中工具/明确页面错误、若有 Turn 完成标记必须为 `complete`、可见 Copy、可用编辑器、内容指纹连续稳定至少 2200ms,才发一次 `TURN_FINISHED`。页面错误依可见 alert/status、conversation-error、connection-status 及中英文错误提示识别;模态框、未知工具状态、加载中、标签漂移等不具足够证据时保守 UNKNOWN。**到期 Alarm 仅持久设置 `handoffPending`,不直接发消息或 Handoff;必须等待同一真实 Turn 的 `TURN_FINISHED`,由 Worker 完成身份和状态核验后才可授权唯一 Handoff Action**,绝不凭文字静止或 Stop 按钮暂时消失执行网页副作用。
59
+
60
+ **异常与实站边界**:v0.3.24 真 Chrome 观察到 Cloudflare `role=alert` 的 `cloudflare_challenge`,Observer 明确识别并让 Worker 安全 BLOCKED,**绝不自动点安全验证按钮**。原生 Unknown error 一次 Retry 与上下文 ERROR Handoff 仍须分别实站验收。历史专用 `WALL_CLOCK_HOLD` 只证明等待后交接;现行 10m 验收必须随 Turn 连续 Continue,不能以旧 HOLD 证明真实循环。
61
+
62
+ **仍待实站验证**:真实 Chrome 已证实 `Unknown error → BLOCKED/PAGE_ERROR`;但 v0.3.16 起实现的「一次原生 Retry」和「上下文容量错误自动 Handoff」尚无完整实站 E2E,极端长工具/网络不稳定路径也未完成专项复现。Copy 按钮、2.2 秒文字稳定及 Stop 缺失也不能单独代表所有工具场景的强终态证据,证据不足时持续 `UNKNOWN` 或安全阻断,不能冒充完成。三态是业务模型,并不意味着源码只有三种内部值。
63
+
64
+ **两种打开方式**:正常由 Worker 依据时机自动创建对应 Action 页,仅 URL 传 `tabId` 和一次性 `token`,交接正文由 Handoff Action 在旧 Chat 内**只读获取**、不经 URL。历史调试也允许明确打开 `continue.html?tabId=N` 或 `handoff.html?tabId=N`,由 Worker 核实唯一目标、完成的 Turn、稳定身份证据和未处理过的当前回复,才发一次许可;手动动作只用于诊断,**不能作为生产自动 Handoff 已按时机触发的验收依据**。目标多义、草稿、旧回复或未知回执均不得发送。
65
+
66
+ **Handoff 差异与标签收尾**:Chat 的稳定身份是规范化的会话 URL 路径(当前运行状态的 `page`),Chrome `tabId` 只是本次运行时的临时定位句柄。Continue 每轮在**同一个 Chat URL** 发消息;可能短暂打开独立的 Continue Action 页,但成功后关闭,不会新建 Chat。Handoff **只有确认新 Chat 收件、Worker 回执 `OK` 且所有权已转移后**,才检查旧标签 tabId 是否仍指向原 Chat URL,匹配则关闭旧浏览器标签及当前 Handoff Action 页;不匹配或关闭失败时保留,不误关其他标签。ChatGPT 中原会话历史仍可按原 URL 再打开,不删除历史。如果新 Chat 发送或转移未确认,不能关闭旧标签。当前自动流程不接受模型主动 `HANDOFF`(旧手动诊断兼容能力不属于自动触发条件);若十分钟时间到期但当前完成回复为 `CONTINUE`(未知标记严格 BLOCKED),Action 自行核实旧 Chat 最后一轮并只读获取有界任务与近期轮次快照,原 Chat 不再发送任何内容,随后创建新 Chat。快照不保证完整覆盖非文本附件或超长历史;需要在新 Chat 通过可用工具重新核实现场,不能伪装为完整上下文。显式交接已连续十轮实站通过;TIMED 自动快照在**受控时钟设为 61 分钟后、真实完成 Turn 才触发**的实站路径也已通过(新 Chat DONE、时钟重置、旧标签关闭)。尚未以真实连续一小时的挂起/后台运行验证定时策略。
67
+
68
+ **目标选择**:对于 Screenshot/DOM/Text,`tabId` 必须是**当前 Chrome 中真实存在的正整数标签 ID**,且目标 URL 必须是 `https://chatgpt.com/*`。未指定时优先取动作页的 ChatGPT opener;否则只有全 Chrome 唯一一个 ChatGPT 候选才自动选取。多个候选报 `AMBIGUOUS_CHATGPT_TABS` 并停止,不读取其他聊天。Reload 无目标标签参数。
69
+
70
+ **动作成功不返回远端 RPC 响应**:Screenshot 的输出是 Chrome 下载的本地文件,DOM/Text 的输出是系统剪贴板;动作页关闭是完成后的界面行为,不代表 MCP 已拿到内容。对需要结果回传的未来工具须另设**明确的受控通信契约**。
71
+
72
+ **显式 Action 验收准备(v0.3.6)**:`continue.html?tabId=N&setup=1` 仅面向调用者新建的空白专用测试 Chat。Worker 两次只读核实指定标签处于 `https://chatgpt.com/`、唯一可见 main、无历史消息、无草稿;无其他活跃循环且不是上一动作目标时,才签发固定无工具测试提示的一次性许可。可显式附加 `&discardDraft=1`,只允许正式 Continue Action 清空调用者新建测试标签中恢复的草稿,并等待、核实空白状态后再准备固定测试提示;不能指向旧 Chat,也不用于正常 Continue。正文不可从 URL 或调用消息替换。仍只有正式 Continue Action 写入和点击,先持久化 PENDING,等待 ChatGPT 的本地临时会话路径变成正式 UUID 路径并确认用户 ID 后提交;失败或未知不重试。此发送仅用于建立测试首回合,**不计作 Continue 真实续接 PASS**。之后的正式 Continue/Handoff 必须各自核对真实新增消息。该入口不用于生产任务启动、不更改 Loop 策略。
73
+
74
+ ## 3. API:扩展内部消息协议(实际源码)
75
+
76
+ 这些消息仅供**本扩展内部**使用,当前不属于外部插件可直接调用的公共接口。扩展版本 `0.3.27` 与内部读操作契约版本 `chat-loop.eye.v1` 是两回事;**保留 `eye.v1` 兼容名,不为产品改名破坏已验证协议**。
77
+
78
+ | 发送方 → 接收方 | 请求 | 返回字段 |
79
+ | --- | --- | --- |
80
+ | Popup → ChatGPT content script | `{type: "CHAT_LOOP_WEB_CHECK_V1"}` | `{contract:"chat-loop.web-environment.v1", status, pageOrigin, browser, extensionVersion}` |
81
+ | 动作页 → ChatGPT content script | `{type:"CHAT_LOOP_EYE_READ_V1", mode:"dom"}` 或 `mode:"text"` | `{contract:"chat-loop.eye.v1",status:"OK",mode,url,value,length}` |
82
+ | Reload 动作页 → background service worker | `{type:"CHAT_LOOP_EYE_RELOAD_V1",tabId:ownTabId}` | `{status:"ACCEPTED"}` 或 `{status:"FAILED",code}` |
83
+ | Observer → Worker | `{type:"CHAT_LOOP_TURN_FINISHED_V1",page,userKey,assistantKey,replyText}` | `QUEUED`/`IGNORED`/`DENIED`/`BLOCKED`;只在真实完成 Turn 发出 |
84
+ | Observer → Worker | `{type:"CHAT_LOOP_TURN_BLOCKED_V1",page,userKey,reason:"PAGE_ERROR"|"CONTEXT_LIMIT"}` | `QUEUED`(一次原生 Retry / 明确容量 Handoff)或 `BLOCKED`/`IGNORED`/`DENIED`;Worker 双次核验 Owner 和错误身份 |
85
+ | Worker → Observer | `{type:"CHAT_LOOP_TURN_RETRY_MONITOR_V1",page,userKey}` | `REARMED`/`UNVERIFIED`;只在原生 Retry 的动作回执确认后重新监测同一失败请求,不发送用户消息 |
86
+ | Worker → Observer | `{type:"CHAT_LOOP_TURN_RESUME_V1",page,userKey,assistantKey,startedAt}` | `RESUMED`/`ALREADY_MONITORING`/`UNVERIFIED`;仅持久 `WAITING_REPLY` 的唯一 Owner 在 Reload/Alarm 后按消息 ID 恢复只读观察,不是新发送 |
87
+ | Continue/Handoff Action → Worker | `{type:"CHAT_LOOP_ACTION_TAKE_V1",tabId,token}` | 只接受对应内置 Action 标签与未领过的令牌;返回目标和对应续接文案或交接包 |
88
+ | Continue/Handoff Action → Worker | `{type:"CHAT_LOOP_ACTION_RESULT_V1",tabId,token,result,newUserKey,...}` | 只在 `PENDING` 且对应令牌时更新;Handoff 的成功回执还需新的 Chat tabId、page 才能转移所有权 |
89
+
90
+ `value` 为完整 DOM 字符串或纯文本,`length` 为 JavaScript 字符串长度,**不是文件字节数**。content script 在非 ChatGPT 页面或不支持的 `mode` 下返回 `{contract:"chat-loop.eye.v1",status:"UNSUPPORTED"}`。环境探针可能报告 `WEB_READY`、`BROWSER_UNVERIFIED`、`UNSUPPORTED_PAGE`;Popup 无法取得有效消息时显示 `EXTENSION_UNVERIFIED`,该状态是 Popup 的判断,不是页面脚本返回值。
91
+
92
+ **Reload 恢复逻辑**:动作页初次发送读取消息失败时,借助 `chrome.scripting.executeScript` **仅向目标 ChatGPT tabId** 注入 `content.js`,然后重试一次;脚本注册有幂等保护。不会因扩展重载主动刷新所有 ChatGPT 标签页。
93
+
94
+ ## 4. 错误与副作用约定
95
+
96
+ | 情况 | 错误/表现 | 保证 |
97
+ | --- | --- | --- |
98
+ | 未找到目标、非法 tabId、多候选 | `NO_CHATGPT_TAB`、`INVALID_TAB_ID`、`TARGET_NOT_CHATGPT_OR_NOT_FOUND`、`AMBIGUOUS_CHATGPT_TABS` | 不猜目标、不读取其他聊天 |
99
+ | 页面脚本不可用或结果不符合契约 | `CONTENT_SCRIPT_UNAVAILABLE`、`INVALID_CONTENT_RESPONSE` | 只对指定目标尝试重注入一次,失败保留动作页 |
100
+ | 纯文本为空或复制失败 | `EMPTY_CONTENT`、`CLIPBOARD_COPY_FAILED` | 不报告成功;失败时保留动作页 |
101
+ | 截图、下载或权限失败 | `TARGET_NOT_VISIBLE`、`CAPTURE_FAILED`、`DOWNLOAD_NOT_STARTED`、`DOWNLOAD_TIMEOUT`、`DOWNLOAD_INTERRUPTED`、`DOWNLOAD_FAILED`、Chrome 原始权限异常 | 只有实际下载完成才标成功 |
102
+ | 扩展重载请求未被接受 | `RELOAD_UNVERIFIED` | 不误报扩展已重载 |
103
+
104
+ 以上是**当前已显式处理的主要状态**,不是强行统一的全局错误枚举;其他浏览器异常可能直接显示原始错误。错误页不会自动关闭,便于观察证据;正常动作页会在确认为成功后自动关闭。
105
+
106
+ ## 5. 扩展 npm 包自有 CLI / Native Messaging / 系统临时目录
107
+
108
+ > **此层归 `@tomflow/chatloop-extension` 独立维护**。四个 Action、它们的 CLI 适配、HTML/CSS、原生消息回执和临时文件写入均在本包;`@tomflow/chatloop-tools` 仅代理、按命名空间暴露 MCP 工具,不复制扩展动作。Continue/Handoff 仅由原 Worker 自动授权,绝不对 Chat 开放任意执行命令。
109
+
110
+ ```bash
111
+ npm install @tomflow/chatloop-extension@0.3.27
112
+ # 在 Chrome 加载该 npm 包内的 extension/ 文件夹,并获取实际扩展 ID
113
+ chatloop-extension host install <EXTENSION_ID> # macOS 用户级注册已实现
114
+ chatloop-extension extension chat text
115
+ chatloop-extension extension chat dom
116
+ chatloop-extension extension chat screenshot
117
+ chatloop-extension extension reload
118
+ ```
119
+
120
+ 普通手动动作保持已验收行为:DOM/Text 复制到系统剪贴板,截图下载 Downloads。**机器模式**由 CLI 写入独立随机请求 ID(本机 0700 目录,文件 0600),打开同一个正式 Action 页,扩展通过 `chrome.runtime.sendNativeMessage` 向按需 Native Host 核验请求。扩展 Worker 只读核实 `WAITING_REPLY` 的**唯一受控 Chat Owner**,然后正式 Action 执行读取/截图;Native Host 把原始 UTF-8 或 PNG 字节落入系统临时目录,写 `result.json` 供发起的 CLI 等待。找不到唯一 Owner 则拒绝;绝不猜测当前活动标签。每次返回 `requestId`、状态、mimeType、字节数和本机临时文件路径,短文本还能直接返回 CLI 内容。失败、超时和结果不明不能假装完成,不能盲目重试。过期临时产物由后续 CLI 命令按 20 分钟 TTL 清理。
121
+
122
+ 系统临时目录由 **Node `os.tmpdir()`** 决定,而非固定写死:macOS 通常为 `/var/folders/.../T`,Windows 为当前用户 `%TEMP%`(典型 `C:\Users\<User>\AppData\Local\Temp`),Linux 通常为 `/tmp`。Chrome 扩展不能直接访问 OS 任意临时文件,必须由 Native Host 在宿主 OS 上读写。**截图不能只给远程 Chat 一个本机路径**:工具包应验证文件、读取 PNG 并通过 MCP `image/png` 内容交回 Chat;路径只是本机证据。Reload 的 Action 可能重载自身使回执连接消失,本版只能返回 `ACCEPTED_UNVERIFIED`,不能声明最终健康。
123
+
124
+ Chrome Native Host 配置在 macOS `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/`;Native Host 只允许用户安装时指定的**准确扩展 ID**。CLI Host 安装器目前只支持 macOS,Windows Registry 和 Linux 浏览器位置要后续单独实现与验收。**Node 层通信测试通过不等于真实 Chrome/MCP 完成;需要升级加载的扩展和工具包后验证。** 不会无提示安装扩展、执行 Continue/Handoff、操作其他 Browser Owner。
125
+
126
+ ## 6. Turn Observer + Action-boundary 自动续接|v0.3.2
127
+
128
+ **当前实际加载:** Manifest Content Scripts 和后台重注入仅包含 `content.js`(Eye)与 `loop/observer.js`(Observer);后台 Worker 独立导入纯协议解析 `loop/core.js`。原直接向输入框写入的 `loop/driver.js` 已退役删除,**任何续接消息的写入/点击只能在 `continue.html` 或 `handoff.html` Action 页发起**。云端 Plugin v0.1.4 不受本机扩展升级影响。正式 Chrome 的普通 Turn 观察、单次自动续接及 DONE 停止已分别验收。
129
+
130
+ **观察 API(扩展内部、只读):** 向明确的 ChatGPT 页面 content script 发送 `{type:"CHAT_LOOP_TURN_OBSERVE_V1"}`,同步获得最近一次采样结果 `{contract:"chat-loop.turn-observer.v1",status,reason,page,userKey,assistantKey,eventCount,observedAt}`。无当前消息正文,`page`、`userKey` 和 `assistantKey` 仅用于该页面内观察关联;不是外部 MCP 或持久任务 ID。`eventCount` 是自本次页面 content script 实例初始化以来实际派发的 `CHAT_LOOP_TURN_FINISHED_V1` 次数,供只读去重验收;**刷新或扩展重载会归零,不能当作跨会话持久计数**。Observer 不点击、输入、上传、不建立数据库、不保存聊天内容。
131
+
132
+ | 观察状态 | 源码判定与约束 |
133
+ | --- | --- |
134
+ | `IDLE` | 当前可见会话的历史基线**已可靠建立**;没有基线后新增 user 消息,或当前 Turn 已处理。切换/刷新后等待历史 DOM 异步加载期间**不能**把空页面立即认作 IDLE。 |
135
+ | `RUNNING` | 观察到新 user 消息且等待 assistant;或有可见 Stop、忙碌/工具运行信号;或最新回复内容/完成证据尚在稳定观察窗口。 |
136
+ | `TURN_FINISHED` | 仅对基线之后同一新 user 对应的**新 assistant**,同时满足标识存在、无活跃 Stop/工具、工具完成可核实、输入区域可用、最新 assistant 完成操作控件可见、文本持续 2200ms 稳定,才产生一次事件。 |
137
+ | `UNKNOWN` | 目标或消息身份不明确、DOM 缺少要件、工具结局不明确、弹窗或意外观测异常。**不推断完成**。 |
138
+ | `BLOCKED` | 检测到明确页面错误提示。只表示观察受阻,不代表业务失败。 |
139
+
140
+ **切换与启动保护:** ChatGPT 可在同一文档内保留两个 `<main>`;Observer 只选择唯一可见的主会话区域,存在多个可见区域时返回 `UNKNOWN/MAIN_AMBIGUOUS`。URL 切换或冷注入遇到空白历史消息时返回 `UNKNOWN/ATTACHING_CHAT` / `UNKNOWN/WAITING_CHAT_HYDRATION`,等待用户消息身份稳定后才建立基线;首页明确发送的首条新消息另行绑定,不误当作历史。真实 Chrome 曾复现切换历史聊天时重复完成事件,修复后通过真实切换与事件数归零复验。
141
+
142
+ **事件:** `document` 上的 `CHAT_LOOP_TURN_FINISHED_V1` `CustomEvent`,detail 只含契约、路径及 user/assistant 页面标识;同一个新 user 仅派发一次。刷新/扩展重注入从当前 DOM 重新建立基线,不把历史回复重放为结束事件;页面身份变化重设基线。**此事件会由 Observer 同步上报给后台调度器**;Observer 自身仍不发送聊天内容。
143
+
144
+ **真实 UI 证据及边界:** 2026-11-03 正式 Chrome 的消息容器实际采用 `[data-chatgpt-search-unit-key][data-chatgpt-search-message-ids]`,辅助轮次状态 `data-talvt-turn-state="complete"`;编辑器为 `[contenteditable="true"][data-composer-markdown]`,助手 Copy 控件为 `aria-label="复制"`。Observer 兼容原旧选择器。只读探针在专用测试会话实际采集 `IDLE → RUNNING(等待 assistant / Stop 生成中)→ RUNNING(稳定观察)→ TURN_FINISHED → IDLE(去重)`,并验证首页首轮与扩展 Reload 后旧消息不重放。**这是正常 Turn 的 E2E PASS,不是服务器的 Turn 完成回执**;长推理、工具执行、错误弹窗等实站特殊路径未完成真实覆盖,遇不充分证据继续返回 `UNKNOWN/BLOCKED`,不能把离线测试充作实测。正式证据与未完成范围以 [CURRENT](../../context/CURRENT.md) 为准。
145
+
146
+ 以前的 `test/observer-probe.html` / `test/run-probe.html` 仅属临时验收页,目录整理后已从发布扩展源码移除;正常运行只保留正式 Action。需要重新采集可观测证据时,应通过受控、显式的测试工具或新增经过验收的正式观察接口,不把调试页当作产品 Action。
147
+
148
+ 历史 v0.3.2 的两轮自动续接 `CONTINUE → DONE` 已有实站证据,原始描述已转至 [`旧版记录`](../../work/v037-acceptance/legacy-project-notes-2026-11-04.md)。当前的三态协议、Observer 身份去重、Reload 保护仍按本节及源码执行。
149
+
150
+ ## 7. 六个 Action 与验收工具的边界
151
+
152
+ 正式 Action **只有** `screenshot.html`、`dom.html`、`text.html`、`reload.html`、`continue.html`、`handoff.html`。`loop/observer.js` 只读观察,`background.js` 只负责判断/独占授权,网页写入只由正式 Action 页执行。
153
+
154
+ `popup.html?audit=1`、`?newTest=1`、`?cleanup=…` 和 `clock.html` 只服务隔离验收,**不是第七个业务 Action**;此前考虑拆成 `audit.html` / `test-create.html` / `test-cleanup.html` 的候选方案没有被批准,不作为当前实施任务。原始讨论保留在旧版归档。
155
+
156
+ ## 8. 安装、权限、验收与维护
157
+
158
+ **v0.3.7 真实 Chrome 连续验收辅助(不是第七个 Action)**:现有 `popup.html` 额外支持受约束的验收参数,不改变普通 Popup,也不在后台直接向 ChatGPT 发消息:
159
+ - `popup.html?audit=1`:通过扩展本身只读核对 Chrome 全标签数量、Chat 会话 URL 与 tabId、扩展 Action 临时页、Worker 阶段及当前运行关联消息的 user/assistant ID、数量和末行状态,下载 JSON 到 `~/Downloads/ChatLoop/acceptance/` 并关闭**自身**。只有由本验收页明确创建的专用测试 Chat 才可把 Observer 实际回复文字纳入本地验收证据,其他 Chat 不记录正文。
160
+ - `popup.html?newTest=1`:只有运行阶段允许时,在扩展内部创建**一张**新空白 ChatGPT 测试页,记录本次创建的 tabId;接着通过正式 `continue.html?tabId=N&setup=1&timed=1&wall=10m` 开始十分钟独立验收;其他旧验证模式仅限历史回归。后续所有 Continue 与 Handoff 的消息发送及新 Chat 创建仍只能由六 Action 中的对应正式 Action 执行。
161
+ - `popup.html?cleanupVerified=1`:专用于按用户要求清理多余**测试**标签;必须双重核对专用标题与真实测试消息、关闭前重读 URL/标题/活动/固定状态;对未知/私人草稿、加载未完成或脚本无法验证的首页一律保留;仅已核验专用测试稿可以按明确用户清理要求关闭。保存清理前后标签与逐项结果 JSON,不会删除 ChatGPT 历史会话。
162
+ - `popup.html?cleanup=N`:只允许关闭经过本次 DONE 所有权、精准 tabId/会话 URL 核对的测试标签,或已有持久 DONE 证据及明确专用测试标题的历史 Handoff 测试标签;**不提供一键关闭全部 ChatGPT**。旧 `BLOCKED`/`UNKNOWN` 只读核实,不因状态未知重发。此清理只关闭 Chrome 标签,不删除 ChatGPT 历史会话。
163
+ - 本轮本机一次性验收脚本 `work/v037-acceptance/run-ten.py` **只负责**打开上述扩展页面、读取本地下载 JSON、逐轮校验与归档;不使用 CDP/Playwright 浏览器操作、不向 ChatGPT 页面直接注入写操作、不引入 API、服务或新浏览器平台。
164
+
165
+ **已发现的实站异常防护**:长用户消息在 ChatGPT 上会附加 `显示更多` 控件,Action 的发送回执以 `[data-search-result-target]` 内的原消息精确比对,而非包含控件的整块 `innerText`,仍然只点击发送一次;若 Observer 初次报出的完成 Turn 文本未带可识别状态,仅对 **Worker 已确认发送的同一会话 URL、user ID 与 assistant ID** 进行两次间隔稳定的只读核验后才推进,无法复核则保持 `BLOCKED`,**绝不重新点击发送**。本机单元测试和十轮真实 Chrome 结果分别记录,不能混为已验收。
166
+
167
+ 1. 在现有 Chrome 打开 `chrome://extensions/` → 开启开发者模式 → 加载已解压的扩展,选择**该 npm 包里的 `extension/` 目录**;修改代码后打开 `reload.html` 重载扩展。
168
+ 2. 在 Chrome 真实打开 `https://chatgpt.com`;用 `?tabId=N` 指向明确的测试标签,依次核对剪贴板内容、PNG 文件及动作页关闭、Reload 后可再次读回。多标签未指定目标时应报错而不执行。
169
+ 3. 运行 `npm run test`(Eye + 模拟 Loop + 模拟 Observer,无额外测试依赖)和 `npm run check`。**自动化回归 PASS ≠ 实际 Chrome E2E PASS**;真实验收结果和版本以 [CURRENT](../../context/CURRENT.md) 为准。
170
+
171
+ 扩展权限来自 `manifest.json`:`tabs`、`downloads`、`clipboardWrite`、`storage`、`alarms`、`activeTab`、`scripting`、`<all_urls>`;内容脚本仅匹配 `https://chatgpt.com/*`。为支持**直接打开截图动作页**,Chrome `captureVisibleTab` 使用 `<all_urls>` 授权(或需要用户当次操作授予 `activeTab`)。这是**较宽的浏览器权限**,不能把“代码只选 ChatGPT”说成 Chrome 强制限制了权限;后续调整时要重新做真实截图验收。
172
+
173
+ DOM/Text 和截图都可能包含私人聊天数据,写入用户本机剪贴板或下载文件;当前不上传到第三方。原直接写 DOM 的 Driver 已删除;同 Chat 续接与跨 Chat 交接分别由 Worker 创建独立的 Continue/Handoff Action 执行。普通场景已实站核验,工具/断线/隐藏标签节流仍待专项核验。功能和代码边界变更应先更新本包的接口合同与测试,再更新顶层简短引用;不要建立第二份 API 真源。
package/bin/cli.mjs ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ // CLI is owned by the Extension package; local-tools delegates, never implements actions.
3
+ import {spawnSync} from 'node:child_process';
4
+ import {existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync} from 'node:fs';
5
+ import {homedir} from 'node:os';
6
+ import {dirname,join,resolve} from 'node:path';
7
+ import {fileURLToPath} from 'node:url';
8
+ import {ACTIONS,HOST_NAME,configFile,prepare,readResult,privateDir,validateExtensionId} from '../lib/bridge.mjs';
9
+ const contract='chat-loop.extension-cli.v1';
10
+ const wait=ms=>new Promise(r=>setTimeout(r,ms));
11
+ const emit=(event,command,fields={})=>process.stdout.write(JSON.stringify({contract,event,command,...fields})+'\n');
12
+ const quoted=s=>"'"+s.replaceAll("'","'\\''")+"'";
13
+ function installHost(extensionId) {
14
+ validateExtensionId(extensionId);
15
+ if(process.platform!=='darwin')throw Object.assign(Error('Native Host registration not verified on this OS'),{code:'HOST_INSTALL_UNSUPPORTED'});
16
+ const conf=configFile();privateDir(dirname(conf));
17
+ const node=process.execPath,script=fileURLToPath(new URL('./native-host.mjs',import.meta.url));
18
+ const runner=join(dirname(conf),'native-host.sh');
19
+ writeFileSync(runner,'#!/bin/sh\nexec '+quoted(node)+' '+quoted(script)+' "$@"\n',{mode:0o700});
20
+ chmodSync(runner,0o700);
21
+ const hostDir=join(homedir(),'Library','Application Support','Google','Chrome','NativeMessagingHosts');
22
+ mkdirSync(hostDir,{recursive:true});
23
+ const manifest={name:HOST_NAME,description:'Private Chat Loop Extension Native Messaging Host',
24
+ path:runner,type:'stdio',allowed_origins:['chrome-extension://'+extensionId+'/']};
25
+ const manifestPath=join(hostDir,HOST_NAME+'.json');
26
+ writeFileSync(manifestPath,JSON.stringify(manifest,null,2)+'\n',{mode:0o600});
27
+ writeFileSync(conf,JSON.stringify({extensionId,host:HOST_NAME,at:Date.now()},null,2)+'\n',{mode:0o600});
28
+ return {status:'INSTALLED',extensionId,manifestPath};
29
+ }
30
+ function configuration() {
31
+ if(!existsSync(configFile()))throw Object.assign(Error('Run host install <extension-id> first'),{code:'HOST_NOT_CONFIGURED'});
32
+ const config=JSON.parse(readFileSync(configFile(),'utf8'));
33
+ validateExtensionId(config.extensionId);
34
+ return config;
35
+ }
36
+ function openAction(url) {
37
+ if(process.platform==='darwin') {
38
+ const r=spawnSync('open',['-a','Google Chrome',url],{encoding:'utf8',timeout:5000});
39
+ if(r.error||r.status!==0)throw Object.assign(Error('Could not open formal Extension Action page'),{code:'CHROME_OPEN_FAILED'});
40
+ }else throw Object.assign(Error('Browser launcher not yet verified on this OS'),{code:'BROWSER_LAUNCH_UNSUPPORTED'});
41
+ }
42
+ async function executeAction(action) {
43
+ if(!ACTIONS.includes(action))throw Object.assign(Error('Unknown action'),{code:'UNKNOWN_ACTION'});
44
+ const config=configuration(),req=prepare(action);
45
+ const page=action==='reload'?'reload.html':action.slice(5)+'.html';
46
+ const url='chrome-extension://'+config.extensionId+'/'+page+'?requestId='+req.id;
47
+ openAction(url);
48
+ const until=Date.now()+27000;
49
+ while(Date.now()<until){
50
+ const result=readResult(req.id);
51
+ if(result){
52
+ if(result.status!=='OK')throw Object.assign(Error(result.code||'ACTION_FAILED'),{code:result.code||'ACTION_FAILED'});
53
+ if(!result.verified) return {...result,status:'ACCEPTED_UNVERIFIED'};
54
+ // The temp file is the canonical artifact. Returning small text is a CLI convenience.
55
+ if(action==='chat_text'||action==='chat_dom'){
56
+ const contents=readFileSync(result.path,'utf8');
57
+ return {...result,content:contents.length<=500000?contents:undefined,
58
+ truncated:contents.length>500000};
59
+ }
60
+ return result;
61
+ }
62
+ await wait(100);
63
+ }
64
+ throw Object.assign(Error('No verified Extension Action result; do not retry blindly'),{code:'RESULT_TIMEOUT'});
65
+ }
66
+ async function execute(argv) {
67
+ const cmd=argv.join(' ');
68
+ if(argv.length===0||cmd==='capabilities'){
69
+ return {implemented:['capabilities','host install <extension-id>','extension chat text',
70
+ 'extension chat dom','extension chat screenshot','extension reload'],
71
+ available:existsSync(configFile()),hostConfigured:existsSync(configFile())};
72
+ }
73
+ if(argv[0]==='host'&&argv[1]==='install'&&argv.length===3)return installHost(argv[2]);
74
+ if(argv[0]==='extension'&&argv[1]==='chat'&&argv.length===3 &&
75
+ ['text','dom','screenshot'].includes(argv[2]))
76
+ return executeAction('chat_'+argv[2]);
77
+ if(cmd==='extension reload')return executeAction('reload');
78
+ throw Object.assign(Error('Unknown command'),{code:'INVALID_ARGUMENT'});
79
+ }
80
+ const args=process.argv.slice(2),command=args.join(' ')||'capabilities';
81
+ emit('STARTED',command);
82
+ try{
83
+ const data=await execute(args);
84
+ emit('RESULT',command,{ok:true,value:data});
85
+ }catch(e){
86
+ emit('RESULT',command,{ok:false,error:{code:e.code||'UNKNOWN_RESULT',message:e.message}});
87
+ process.exitCode=1;
88
+ }
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ // Chrome Native Messaging: one request per process, no persistent server.
3
+ import {readRequest, finish} from '../lib/bridge.mjs';
4
+ const MAX_INPUT=48*1024*1024;
5
+ const write=obj=>{
6
+ const b=Buffer.from(JSON.stringify(obj)),len=Buffer.alloc(4);
7
+ len.writeUInt32LE(b.length);process.stdout.write(Buffer.concat([len,b]));
8
+ };
9
+ let buf=Buffer.alloc(0),handled=false;
10
+ process.stdin.on('data',chunk=>{
11
+ if(handled)return;
12
+ buf=Buffer.concat([buf,chunk]);
13
+ if(buf.length<4)return;
14
+ const n=buf.readUInt32LE();
15
+ if(n>MAX_INPUT){handled=true;write({status:'FAILED',code:'MESSAGE_TOO_LARGE'});return;}
16
+ if(buf.length<4+n)return;
17
+ handled=true;
18
+ try{
19
+ const m=JSON.parse(buf.subarray(4,4+n).toString('utf8'));
20
+ if(m.op==='acquire'){
21
+ const r=readRequest(m.id);
22
+ if(r.action!==m.action)throw Object.assign(Error('Action mismatch'),{code:'REQUEST_MISMATCH'});
23
+ write({status:'OK',id:r.id,action:r.action,nonce:r.nonce});
24
+ }else if(m.op==='complete'){
25
+ const r=finish(m);
26
+ write({status:'SAVED',id:r.id,action:r.action,bytes:r.bytes});
27
+ }else throw Object.assign(Error('Unknown operation'),{code:'INVALID_OPERATION'});
28
+ }catch(e){write({status:'FAILED',code:e.code||'NATIVE_HOST_ERROR'});}
29
+ });