屏幕评论污染隔离与局部历史引用设计
文档性质:阶段 0A-1(§7.1.2、§7.1.3、§12.1)已实现(
utils/screen_comment_guard.py,从 PR #3201 拆出单独交付);其余各节仍是 Proposal(待审阅设计,未实现、未上线)。日期:2026-09-28,2026-10-01 修订 §7.1.2。代码核对基线:
b00286b4e728eef9abb9ac04267ec410fb5e69b5(PR #3201 的 base,main);0A-1 部分按48ac924(main)复核。已关闭的 PR #3185 的实现未进入main,本文不再以它为现状。2026-10-01 修订: 0A-1 最初在 #3201 中把命中消息整条替换为固定占位说明。拆分单独交付时改为保留首条评论:去掉来源标签,截至该评论的句末,其后的评论移出请求视图。理由与未验证项见 §7.1.2。 用户选择保留聊天中的完整原文引用;这不意味着本文新增协议、交互和资源限额已成为现行产品合同。 本文是中文设计记录,不创建没有对应实现的 API 使用指南或语言镜像。
1. 摘要与设计决策
原问题不是单纯的标签泄漏:异常回答作为历史示例进入模型后,会诱发旧评论重放或同类格式的新场景串接。已关闭的 #3185 的“请求历史投影+共享流式过滤”有利用价值,但“自然语言引用意图 → 恢复全部历史并关闭整轮过滤”的权限范围错误,不能继续靠引用关键词修补。0A-1 只交付其中的请求历史投影,不含任何本轮输出过滤;输出侧属 0A-2,未排期。
本设计采用以下边界:
- 普通生成、逐字引用、引用内容的翻译/分析是三种不同任务。
- 逐字引用由服务端读取已验证的原文快照并直接交付,不经过 LLM 重写。
- 翻译/分析只向独立生成请求开放选定原文。 其输出是有来源的生成结果,不是原文,不保证逐字完整或语义正确。
- 原始记录、用户可见交付记录、自动模型上下文分开。 完整保存不等于自动注入;隔离不等于删除。
- 用户选择的具体记录决定开放范围。 自然语言识别、模型建议、前端正文均不能直接授予全局豁免。
- 引用交付及其派生结果默认不进入后续普通生成的正文上下文。 后续继续处理它们,需要再次明确选择;普通上下文仅保留不含原文的结构化操作摘要。
执行顺序先止损、再补完整能力:阶段 0 后首先独立拆除自然语言全局豁免,不等待消息身份、引用 UI 或状态机完成。 该止损改动需要明确暴露原文引用能力的临时限制,不得继续静默截断并声称完整成功。
本方案是候选 A 的结构化实现,不是完整来源治理 B、通用相似度检测 C 或全面生成后重试 D 的合并重写。保留应用在生成/交付时已知的屏幕输入来源事实属于近期小改动,纳入阶段 1A,不必等待完整 B。 消息标识及引用来源链是实现 A 的必要基础,不扩展为全产品事件溯源。
2. 证据、已知限制与目标
2.1 证据边界
实验条件、现场时间线与原始材料索引以事故记录为准,不在本文复制用户原文或私有文件路径。
- 用户提供的现象是两次输入、三段输出合计 23 个标签,不是确认发生了 23 次独立 API 注入。
- 已有真实 API 实验复现:污染历史后既可能逐字重放,也可能编出新场景串;首次污染来源仍未确认。
- 局部选择的初步探针使用人工指定目标,不代表生产系统已经能定位自然语言指代。
- 该探针中模型正确复述 518 字原文,始终开启的输出过滤只留下 167 字;519 个二段切分位置结果一致。
- 上述 518→167 是强制开启过滤的候选实验,不是 #3185 所有引用请求的既有行为。2026-09-28 的离线对照中,对“请复述刚才的原话”,#3185 的全局豁免关闭过滤,保留 518 字;强制过滤保留 167 字。因此先移除豁免会使部分原先可完整通过的合法引用受限,必须作为已知兼容性代价报告,不能写成“没有新增回归”。
- 引用块能避免当轮语法过滤(这是 #3185 输出过滤器下的实验条件;0A-1 没有输出过滤),但也会被历史投影保留,不能单独解决下一轮传播。
- 零屏幕历史基线只有 system+user,仍出现坦克游戏角色扮演续写。因此不把普通游戏化续写/单段无图场景记为残留污染或本次修复失败;具体实时视觉判断的因果归属另行研究,不能据一个基线证明其与污染完全无关。
- 六次同期对照中,带标签历史的连续评论串及旧句逐字重放这两类目标症状均被两种投影抑制;不是“只覆盖一半”。但每格一次的结果不等于现场全部变体已治愈或事故可直接关闭。完整依据见事故记录的 E7–E15。
no_labels的当前投影确定性无操作,属于输入识别覆盖缺口;其单次生成新场景不单独证明传播失败。另有增加边界提示后的四段无标签续写观察,需保持实验条件区分。
这里已经确认的是 #3185 的全局豁免使污染重新暴露的代码路径,以及污染历史可以诱发传播的模型实验;不能据此倒推该豁免就是用户现场首次事故的触发入口。部署版本未核实时,也不能把 #3185 分支的缺陷直接称为所有线上版本正在运行的逻辑。
2.2 必须达到的目标
| 目标 | 验收含义 |
|---|---|
| 目标症状收敛 | 对本事故已复现的带标签历史,阻断连续评论串与旧句重放;不以基线已有的游戏化续写判定失败 |
| 局部开放 | 选择消息 B 不会恢复无关消息 A 的隔离正文 |
| 原文完整 | 成功的逐字引用在可见文本层与选定快照一致,不被屏幕过滤器、摘要或模型改写 |
| 防再次回灌 | 引用原文及派生结果不因保存、回调、热切换或记忆检索自动成为下一轮聊天示例 |
| 原件可恢复 | 自动隔离只改变模型视图;原始内容按用户既有保留/删除规则保存 |
| 交付可解释 | 完整、部分交付、取消、失败分开记录,不把排队音频当作已听到 |
| 局部权限 | 普通生成始终使用自己的过滤策略,引用权限不扩散到并发回调或工具轮次 |
2.3 非目标
- 不把消除通用 Qwen 幻觉/游戏化角色扮演列为本次污染修复验收条件;无标签语义重复及超出已测结构的格式模仿明确列为覆盖边界。
- 不依赖本设计解释首次污染,也不宣称重现了生产现场全部请求。
- 不无差别清空历史,不重写所有人设、事实和长期记忆。
- 不新增无限范围的历史读取工具,不重跑有副作用的工具来修复文本。
- 首期不把原生实时音频模型的音频输出改造成逐字引用协议。缺能力的客户端应明确提示,而不是走旧全局豁免。
2.4 被隔离对象的三类来源
任务分为普通生成、逐字引用、引用生成;风险数据的来源另分三类,两者不能互相替代。
| 风险来源 | 已知信息与识别边界 | 工作及优先阶段 |
|---|---|---|
| 存量异常历史 | 有已复现的异常串,也有仅结构可疑的记录;旧数据往往没有来源字段 | 按现有结构规则生成可撤销隔离视图;必要时离线分类/人工复核,不重写原文;旧记录 ID 惰性分配,不能把 ID 分配当污染判定。止损先使用现有视图,恢复与迁移随后补齐 |
| 未来屏幕相关轮次 | 应用在构建屏幕输入时知道相关事实,当前部分信息在 CHAT 归类及写历史时没有保留 | 阶段 1A 在信息压平前记录来源事实、随成功交付写入;来源可识别不等于正文有错,不因有截图就隔离整个回复 |
| 引用自身及其派生结果 | 由本方案主动提供的完整原文、翻译或分析,会形成新的可见记录 | 从首条记录起标 reference_only;交付可见,但自动上下文、摘要、记忆提取、检索回填和主动搭话候选均不取正文。所有路径验证是启用引用功能的前置门槛 |
第三类是设计自身引入的再污染风险,不是继承问题。 “引用完整且保存成功”不能单独算修复成功;若保存后的引用经摘要/记忆提取变成新示例,A 方案反而新增了一条传播路径。其验收必须从完整交付记录出发,跨存储和新会话检查,而不是只测当轮流式过滤。
3. 现有实现与必要缺口
本节文件与函数是按上述基线核对过的入口,标“0A-1”的是 0A-1 实现新增的;后续章节的类型、协议和新模块名称均为拟议名称。
| 现有位置 | 已有能力 | 本设计需要的变化 |
|---|---|---|
utils/screen_comment_guard.py::project_screen_history()(0A-1) | assistant 文本副本投影:命中链的消息只保留首条评论(去来源标签、截至句末),其后的评论移出视图,含跨消息尾部判定;不修改原件 | 接入消息/片段级上下文策略 |
同文件 screen_guard_enabled()(0A-1) | 恒开;唯一开关是运维环境变量 NEKO_SCREEN_HISTORY_GUARD,不读任何用户措辞 | 不得再引入按用户措辞开关的全局豁免(#3185 的 requests_history_reference() 未进入 main,0A-1 也不引入) |
main_logic/omni_offline_client/_tools.py::_dialog_messages_for_provider()(0A-1) | OpenAI 工具循环(含 tools 被拒后的重发)、forced-final 与 Gemini 原生工具循环/forced-final 的统一投影入口 | 只负责普通生成;不解析或制造引用授权。main 没有共享的流式输出过滤器,输出侧属 0A-2 |
_streaming.py、_lifecycle.py、_genai_support.py | 普通聊天、回调、重试、两类 provider 路径 | 普通请求统一视图;引用任务在更外层路由,不混进工具循环 |
utils/llm_client/messages.py | BaseMessage.additional_kwargs,部分序列化可保留元数据 | 完整贯通必要身份、来源和上下文策略字段;兼容 dict 分支 |
main_logic/proactive_chat/service.py、delivery.py | 构建 screen_section、持有截图/视觉配置,随后多个来源归为 CHAT | 在归类前保留已知屏幕输入事实;与交付类别分开,不从 CHAT 反推 |
main_logic/core/proactive.py::finish_proactive_delivery()、_lifecycle.py::prompt_ephemeral() | 成功交付后写历史;0A-1 为两者持久化的回复写 dialog_source="proactive"(后者覆盖问候、agent/话题回调与语音主动搭话) | 当前仅识别独立交付;阶段 1A 仍需保存屏幕输入来源事实,不把独立交付或通用 CHAT 当屏幕专属来源 |
main_logic/core/turn.py::handle_text_data() | 可分别交付显示和 TTS,检查 speech 生命周期 | 增加明确的引用交付入口;保留取消、轮次和播报所有权检查 |
main_logic/core/tts_runtime.py::_enqueue_tts_text_chunk() | 队列、重播缓存、Markdown/括号处理 | 原文朗读需要独立文字模式,不被现有语义清洗删掉原文 |
app/memory_server/routes.py::_new_dialog() | 将 recent history 渲染进系统上下文 | 在原记录仍有结构时投影,禁止先拍平再猜角色 |
main_logic/core/notify.py::_convert_cache_to_str() 与 context_append.py | 热切换、增量上下文回填 | 传播身份与策略,渲染前应用同一上下文视图 |
static/app/app-chat.js::nextReactMessageId() | 本地 UI ID | UI ID 不是持久化身份,需增加服务端 ID 映射 |
历史面板、recall_memory | 选择导出、query/time 检索 | 不等价于精确原文引用;不能未经验证复用为权限依据 |
现有记忆事件日志不是全量消息事件溯源,不能假定写入一个新事件名就有可恢复引用存储。参见记忆事件记录。
3.1 阶段 1A:在压平前保留屏幕来源事实
当前 0A-1 已增加最小独立交付标记 AIMessage.additional_kwargs.dialog_source="proactive"(finish_proactive_delivery() 与 prompt_ephemeral() 持久化的回复都写入),用于 §7.1.3 的跨消息分段。AIMessage 文件序列化/恢复保留该字段,SQL 白名单仅新增此允许值。它不说明是否有屏幕输入、是否发送截图,也不代表完整 1A 已交付;以下屏幕来源事实方案仍待实现。缺失字段的旧记录不作推断或迁移,丢失元数据的跨服务回填也不在此次保真承诺内。
service.py 在 screenshot_b64_for_phase2 存在时构建 screen_section,随后计算 phase2_use_vision;delivery.py 将非音乐/表情包回复归为 CHAT,同时仍根据 phase2_use_vision 决定 staged_screenshot。因此不是应用无法识别屏幕上下文,而是现有交付分类没有表达并保留这些信息。
近期最小改动是沿 service → delivery → finish_proactive_delivery → AIMessage.additional_kwargs 传递结构化输入来源事实,只有成功交付才写入。保留 CHAT/MUSIC/MEME 的既有交付语义,不要求把它们重定义成互斥内容来源。
- 至少区分“该轮有屏幕上下文”“视觉生成被请求”;如果记录“请求实际携带截图”,应由真实 provider 请求组装点确认,不能把截图缓存存在或模型具备视觉能力当作已发送/已读取的证明。
- 屏幕、网页、音乐可以同时参与,不能用一个
SCREEN标签再次覆盖其他来源;不从生成正文或模型自报标签推断唯一来源。 - 不保存截图/base64、窗口标题或正文副本来充当来源字段;未知值显式保留为未知,不按无屏幕处理。
- 测试覆盖纯聊天、有屏幕、混合来源、视觉回退、取消/抢占未交付;正常屏幕回复不因有来源标记就被当成污染删除。
生产者到本地历史这一段是可独立评审的小改动,不依赖稳定 ID 或新 UI。跨服务、归档、回填的完整保真属于阶段 1B;1A 完成不能被包装成这些路径也已贯通,更不能单凭来源字段宣布复读修复。
3.2 无标签缺口与来源策略的适用范围
现有 screen_chain_start() 依赖可识别来源标记;对既有 no_labels 输入返回 None,开启投影也原样放行。0A-1 的首条保留改写仍依赖同一命中条件,因此不会自动获得无标签识别能力。这个缺口是可复现的输入侧事实,不以模型是否编出单段游戏画面来判定。
阶段 1A 保留来源事实的具体收益,是让未来记录在删除/改写正文标签后仍可被识别为同一来源。阶段 1B 再保证这些事实及上下文策略贯通存储与回填;对已具备可信来源和明确 context_policy 的记录,应按元数据策略处理,而不是重新要求正文出现标签。
不能把“有屏幕输入”直接等同于“异常串/应全部删除”。需要区分来源事实与是否允许自动进入下一轮的策略;只保存字段而没有消费者规则,不能声称已补上无标签隔离。无屏幕普通聊天生成的异常内容、旧无标签记录和丢失来源的归档仍没有可用的可信分类,不得凭人设或词语猜成屏幕来源。
相应验收分开:1A 检查同一记录去掉标签后来源事实不丢;1B 检查同一可信策略不因有无标签改变处理结果;旧无标签记录继续明确未覆盖,不用新记录的成功掩盖。保留旧带标签启发式作为兼容路径,不做全量替换宣称。
4. 产品行为与任务边界
4.1 逐字引用 quote_verbatim
用户在聊天历史选择一条已交付的正文,点击“引用原文”;可以选择显示、朗读或两者。服务端验证后直接输出选定文本。无需 Qwen 请求,无工具执行,无“请模型保持原文”的软保证。
- 内容显示为原文引用卡片,来源标签是 UI 元数据,不拼接进正文或 TTS。
- 复制正文得到所选原文,不增加引号、代码围栏、时间标签或省略号。
- “不加引号、原样复述”可以满足,不要求用户用特定标点保护正文。
- 首期只引用已交付的用户/assistant 文本,不开放系统提示、隐藏推理、凭据、工具内部消息或未交付候选。
- 支持选择连续片段;整个消息默认选择全部可见正文。图片/OCR、富文本 DOM 与附件不是文本快照本身,不自动猜测其原文。
4.2 翻译、分析 translate / analyze
用户选择原文后明确操作。服务端创建独立引用生成任务,输入包含该原文、用户任务和最小必要语言配置;不恢复普通聊天中的其他隔离消息。
- 输出标为“基于所选原文生成”,不能冒充历史原话。
- 原文始终可以独立查看/复制;原文完整性与生成结果质量分别验收。
- 任务结果处在
reference_generated段内。这个段允许按任务需要处理屏幕标签,不套用普通聊天的屏幕串接截断器;并不赋予其他输出任何豁免。 - 该生成请求不提供外部动作工具,也不访问其余原始历史。不能声称它不会幻觉或误译;以输入范围、资源限额、可见来源和后续隔离控制影响。
- 不对整轮聊天关闭过滤。首期一个引用请求只执行一个引用操作,不同时执行“查询/操作外部系统+引用”。组合需求拆成显式任务,避免重复副作用。
4.3 自然语言入口与模糊指代
“复述刚才那段”“总结我们的聊天记录”可以触发选择建议,不直接触发原文放行。
- 前端展示候选来源与操作,用户确认后发送结构化选择。
- 模型可以建议候选 ID,但服务端只接受属于当前授权用户与角色的记录;模型建议不是确认。
- 语音入口采用同一待确认选择,由可验证的确认动作完成;尚未具备此交互的客户端提示改用历史选择。
- “上一条”基于候选产生时的快照定位,之后插入主动搭话不会偷偷改变目标;过期或版本变化重新确认。
- 不确认时继续普通聊天;若缺少完成原文请求的材料,明确说明不能完整引用,不从剩余上下文编造缺失原文。
这是明确的交互代价:保留原文处理能力,但不承诺任意自然语言指代都能零确认、零误判定位。不得把“无需新 UI”当作本方案前提。
4.4 首期限额(提议值,需实现前评审)
- 每个引用任务一个源消息、一个连续片段;多个来源逐次选择,首期不假装支持全历史汇总。
- 逐字显示上限为选定文本 UTF-8 编码 64 KiB,用于约束内存、传输和持久化;超过上限提供原历史查看/导出,不截断后返回成功。
- 生成任务沿用已有 provider 上下文预算与回复预算,选定原文放不下时拒绝启动并要求缩小范围,不暗中裁剪。
- 朗读仍受会话播报预算限制;预计超出时先询问是否长文本朗读。用户取消或达到硬上限返回部分播报状态,不以摘要冒充完整朗读。
- 限额是资源策略,不是污染判定阈值;均应集中定义并覆盖边界测试,不增加措辞特例。
5. 数据模型与身份
5.1 持久化身份
拟增加 message_id(服务端 UUID)、revision、content_hash。身份归属于持久化消息实例,不是文字内容,也不是 UI 节点、列表位置、speech_id 或 anti_repeat_response_id。
- 消息创建时分配身份,流式增量和最终交付使用同一 ID;仅已完成/明确部分完成的版本可被选择。
- 已交付内容版本冻结;编辑形成新 revision,旧选择不自动转向新文本。
- 相同正文的两条消息仍是两个 ID;不能仅凭 hash 合并。
- hash 为原文 UTF-8 字节的 SHA-256,只作版本一致性检查,不作鉴权。服务端必须重新计算,不相信客户端提供的正文。
- UI ID 保留供渲染使用,但通过服务端下发的身份建立关联;对缺失/冲突映射显示不可选择,不按文本相似度猜。
5.2 引用选择与快照
拟议内部结构示意:
{
"schema_version": 1,
"message_id": "server-issued-uuid",
"revision": 1,
"content_hash": "sha256-hex",
"range": {"start": 0, "end": 120},
"operation": "quote_verbatim"
}range 使用 Unicode code point 的左闭右开下标。JavaScript 需要显式从 UTF-16 转换;禁止拆开代理对。UI 选择范围应对齐 grapheme,服务端最终以原文下标验证,不做 Unicode 正规化、trim 或换行重写。
选择中的 content_hash 校验整个源版本;ReferenceSnapshot 的 hash 校验选定片段,两者不能混用。无法进行合法 UTF-8 编码的孤立代理字符应拒绝并提示修复源记录,不能在引用途中替换字符后仍报告逐字一致。
读取形成只读 ReferenceSnapshot:授权主体、角色、源身份、revision、范围、原文、快照 hash。文本只由服务端从存储取出,客户端不能通过提交 trusted=true、正文或 segment_kind 构造快照。
快照仅用于当前请求;不能存为无期限会话通行证。source 删除/权限撤回后不可新建引用;进行中的读取与删除按同一记录锁/版本检查线性化,撤回时取消未交付部分。已经发给客户端或第三方 provider 的内容无法承诺追回。
5.3 交付记录与来源链
拟议 DeliverySegment 至少包含:
| 字段 | 作用 |
|---|---|
delivery_id, segment_id, sequence | 交付、分段及重连去重 |
kind | generated_chat、verbatim_reference、reference_generated,由服务端路径指定 |
source_ref | 选定源身份、revision、范围;普通生成为空 |
text / text_hash | 用户可见记录;原文段成功时可校验完整性 |
context_policy | 普通正文按投影策略使用;两类引用正文默认 reference_only |
state | prepared、delivering、awaiting_ack、completed、partial、cancelled、failed |
delivered_range, speech_state | 文本已确认范围和音频独立状态,不混称“已播出” |
原文仍由原消息存储拥有;引用交付记录保留必要的已交付快照以准确还原历史,不建立另一个独立且无限保留的原文仓库。快照遵守相同用户删除/导出政策,来源删除时按既有删除语义清除相关副本,不能以“原件不可变”为由绕过删除。
引用的引用不会递归自动展开。显示直接来源;审计可以追踪来源链,但上下文组装不沿链取正文。
6. 请求协议与授权
以下是拟议协议,不是已有可调用 API。最终应接入现有 WebSocket 及历史访问鉴权,不另建无鉴权端口。
6.1 能力协商
前后端协商 history_reference_v1。只有主服务、记忆存储、当前客户端均支持身份和策略保真时启用。旧客户端仍可普通聊天和查看历史,不回退到全局引用豁免。
客户端发现服务端不支持时禁用结构化引用入口并说明原因;服务端收到未知引用字段应拒绝该引用请求,不能丢掉字段后当普通聊天执行。
6.2 请求示例
{
"type": "history_reference_request",
"schema_version": 1,
"request_id": "client-request-uuid",
"selection": {
"message_id": "server-issued-uuid",
"revision": 1,
"content_hash": "sha256-hex",
"range": {"start": 0, "end": 120}
},
"operation": "quote_verbatim",
"presentation": {"text": true, "speech": false}
}translate/analyze 另带任务文字/目标语言,不接受客户端声明的可信输出类型。会话、角色和权限从服务器连接上下文确定,客户端不能覆盖。
服务端按顺序验证:协议能力 → 当前用户/角色权限 → 已交付消息类型 → ID/revision/hash → 范围及限额 → 操作与播报能力 → 生成只读快照。
错误使用稳定代码并本地化:unsupported_capability、reference_unavailable、stale_reference、invalid_range、reference_too_large、speech_confirmation_required、storage_unavailable。跨角色/无权限访问不泄漏目标存在性。不匹配时不自动找一条“相似消息”替代。
同一授权作用域中的 request_id 搭配操作和选择摘要进行幂等校验;同 key 不同请求返回冲突,不重复朗读。不能把“断线后重发请求”当成第二次用户明确播放。
6.3 输出、确认与错误
拟议输出事件分为 history_reference_start、history_reference_delta、history_reference_end。所有事件携带 schema_version、request_id、delivery_id 和 segment_id;start 还携带服务端确定的 kind、来源与选定片段 hash。
{
"type": "history_reference_delta",
"schema_version": 1,
"request_id": "client-request-uuid",
"delivery_id": "server-delivery-uuid",
"segment_id": "server-segment-uuid",
"sequence": 0,
"offset": 0,
"text": "这一段是选定的原文。"
}sequence 从 0 递增,offset 使用与选择一致的 code point 单位。客户端只接受当前授权交付中的连续序列,重复序列去重,缺段请求恢复,不直接拼接制造伪完整文本。end 携带 status、实际总长度、片段 hash、错误码以及独立的 speech_state。原样引用完整接收后,客户端校验 hash 并发送 history_reference_ack(delivery/segment、最后连续 sequence、接收长度和 hash)。生成结果校验的是实际生成文本完整传输,不是翻译正确性。
服务端完成产生全文但尚无接收确认时,状态为 awaiting_ack,不能提前标 completed。ack 超时保留可恢复状态和原有交付 ID,不自动重新朗读。客户端若已显示部分文字而最终失败,保留部分内容并显示明确状态,不把部分结果无声回滚或当成完整原文。
验证前失败返回关联 request_id 的错误事件,不产生正文;交付中失败通过 end 结束当前 segment。客户端提交的 ack 只代表其接收声明,不证明用户已经阅读或听到声音。
7. 三条执行路径
普通输入 ──→ 正常上下文投影 ──→ LLM/既有工具循环 ──→ 普通共享过滤 ──→ 交付记录
选定原文 ──→ 校验并冻结快照 ─┬→ 原样引用:直接文字交付,无 LLM、无工具
└→ 翻译/分析:独立生成请求,无外部工具
两类引用结果 ──→ 可见历史(完整或明确部分状态)
└→ 普通模型上下文(只保留来源与操作占位,不自动加入正文)7.1 普通生成
project_screen_history()的职责调整为生成上下文视图,不根据用户措辞整体跳过。- 显式
reference_only优先于正文语法:即使原文在引号、代码块中,也不自动进入普通上下文。 - 无元数据的旧 assistant 文本继续采用保守结构识别;标为“疑似连续来源叙述”,不宣称已证明污染。
- 0A-1 命中异常串后,请求副本只保留链前正文和首条评论:去掉来源标签,截至首条评论的句末,第二条评论起移出视图。具体合同见 §7.1.2。
- 普通输出维持现有流式处理,保留思考剥离顺序、工具持久化、分片一致性及取消清理;
main没有共享的屏幕输出过滤器,输出侧属 0A-2。 - 开启/关闭引用功能不改变普通输出防护;不引入任何以用户措辞为输入的权限开关(#3185 的措辞豁免未进入
main),不保留隐藏兜底。唯一的开关是运维环境变量NEKO_SCREEN_HISTORY_GUARD,用于线上误判时不发版止损。
上述全局豁免拆除在 0A-1 完成,不等待引用能力建成。0A-1 仍用现有历史结构识别,不声称完成了基于稳定身份的局部引用。投影省略的可见状态与输出拦截的通知属于 0A-2;在该状态接通前,不得以文档告知代替产品告知。原件始终可通过历史查看、复制或导出获得。提示由实际省略/拦截事件触发,不再由另一套引用措辞分类器决定。
7.1.1 0A 降级决定:拒绝完整引用,不把残缺正文作为引用成功
本设计选择“拒绝完整引用+指向历史面板”,不选择“截短后仍当作引用回答成功”。 固定文案由应用交付,不要求模型自行解释缺失;拟议中文文案为:“暂时不能完整复述这段历史,请在历史面板查看或复制原文。”实现时纳入现有运行时本地化体系。
本节描述的是完整 0A 的目标行为,其中只有第一行属于 0A-1。 0A-1 只做请求侧历史投影,不含拒绝路由、应用级提示,也不含任何输出侧拦截;下表后四行均属 0A-2。0A-1 的实际行为见 §7.1.3。
必须按触发时机区分,不能把流式中断写成生成前拒绝:
| 可观察条件 | 行为 | 用户看到/听到什么及结束状态 | 适用阶段 |
|---|---|---|---|
| 本次没有隔离记录,也没有输出拦截 | 普通处理 | 不因用户提到“复述”就一律拒绝正常历史;不新增异常提示 | 0A-1 |
| 实际存在隔离记录,且生成前已路由为原文处理请求 | 不向模型发送这次原文处理任务,不生成替代正文;直接给固定拒绝和历史入口 | 明确拒绝,reference_unavailable;若会话启用 TTS,朗读应用提示一次,不朗读伪造的复述 | 0A-2 |
| 历史投影发生,但尚不能确定用户是在要求原文 | 正常任务仍可继续;在生成前显示应用级“本轮未加载部分历史正文,不提供这些内容的完整复述”提示 | context_limited 不代表整轮拒绝,也不代表已经完整处理了历史;不得仅把提示藏进模型 system 文本 | 0A-2 |
| 输出过滤器确认评论串,尚无可见/可听正文 | 停止该次回答,发应用级拒绝/停止提示;不重试生成替代原文 | screen_chain_rejected,无残缺正文冒充回答 | 0A-2 |
| 输出过滤器确认评论串,已有正文交付 | 立即停止后续正文与未播放音频,保留已经交付部分并明确标无效/不完整;给独立应用提示 | “本次回答检测到连续屏幕评论,已停止,前面的内容不是完整回复。历史原文请在历史面板查看。”状态 screen_chain_interrupted,不是引用成功 | 0A-2 |
screen_chain_rejected 与 screen_chain_interrupted 在 0A-1 不可实现,因为 0A-1 没有输出过滤器:它不观察本轮生成,也就没有“确认评论串”这个可观察条件。这两个状态码不得在 0A-1 的交付说明或验收里出现。
这些状态名称为拟议的内部/通知原因码,应复用现有状态通知通路实现。引用请求拒绝提示与一般串接中断提示分开,避免用户只在普通聊天时被错误告知“你要求引用”。拒绝/中断提示不得拼入 assistant 生成正文或待摘要尾段;保留单独的交付状态,防止下一轮把应用提示模仿成角色发言。历史入口只能打开当前用户有权访问的现有面板,不在 0A 假装已经能跳转到稳定 ID 定位的那条记录。
引用意图识别至多用于保守路由到拒绝,可以复用已有候选信号,但不能恢复任何原文或关闭任何过滤;不以扩充同义词列表证明支持所有自然语言。该路由可能误拒绝或漏识别,需单独统计和验证。实际投影/拦截提示不依赖这个识别器,因而漏识别也不能无声省略。
承诺边界(0A-2): 0A-2 不具备任意自然语言原文请求的完美识别。它也不能撤回已经流出的首条评论或已播放音频。因此“拒绝完整引用”不等于“任何引用措辞都能在首字前识别并零输出拒绝”。用户的“复述刚才那段”“你刚才说什么”等高频输入必须进入路由/提示验收;未识别时至少执行上表的上下文缺失提示,不能宣称已实现纯预拒绝。若产品要求所有此类请求都零正文输出,必须先提供可靠的明确操作信号或另行扩展设计,不能仅删布尔开关后声称满足。
7.1.3 0A-1 实际行为与已知限制
0A-1 只做两件事:拆除"用户措辞决定权限"的全局豁免,以及在请求副本里先删掉每条 assistant 消息中可识别的来源标签、再把命中链的历史改写为首条评论(截至句末)。前者是权限判定,后者是上下文投影;它不观察本轮输出,也不向用户报告缺失。
因此它只切断一条链路——历史污染 → 模型模仿 → 新串接(含逐字重放)。以下限制必须写进交付说明:
不拦本轮模型自发产出的串接。 0A-1 不观察本轮输出。如果模型在没有受污染历史的情况下自行写出多条屏幕评论,用户会完整看到。本轮的同期真实 API 对照里没有观察到这种情况(拿掉刺激源后模型不再生成串接),但那是观测结果,不是保证。
不告知用户,也不告知模型。 改写只发生在模型上下文里,应用不向用户显示任何提示,模型上下文里也没有"此处有删减"的说明。模型看到的是自己说过一条评论;用户要求复述被移出的评论时,模型可能只复述首条、泛化或自行补写,应用不保证说明原因。这不满足 §7.1.1。
不覆盖无标签评论。 成链截断需要带标签、成串、且每条评论达到
MIN_PROSE;单条或过短的带标签评论不截断,但标签照样删掉(§7.1.2 的无条件去标签)。无标签历史静默透传,请求正文不变,不能据此断言每次模型输出完全相同。英文裸标签screen comment必须接冒号(或处在两侧斜杠的标签里)才算标记,因为后接空格是普通英文语法;英文标签紧跟在 ASCII 字母、数字或下划线后面(screenshot、prescreen、keyboard/screen display)不算标记。中文标签不看前一个字符,紧跟汉字或233、OK、QwQ这类结尾都算。识别简体、繁体中文(螢幕搭話、/螢幕畫面/)和英文标签,以及方括号形态(【屏幕搭话】、[屏幕画面];后接(的是 Markdown 链接文字,不算标签)。斜杠形态只认两侧都有斜杠、且起首斜杠位于开头、空白或标点之后的写法(/屏幕画面/,与主动搭话的标签泄漏清理同形);屏幕截图/照片、照片/屏幕截图 给我这类二选一写法是普通措辞,不算标签,也不删。URL(scheme://开头,或省略协议的//主机/——主机可为域名、IPv4、方括号 IPv6 或localhost这类单段名,主机后也可直接接?、#;两种写法都可带用户名,如https://me@[::1]/——以及带路径的裸域名或 IPv4 主机如www.host.com/、10.0.0.2:8080/;到空白、收尾标点或…、~为止)、Markdown 链接目标](…)(含带空格的](<…>)) 、引用定义里以/、./、../开头或写在尖括号里的目标([1]: /a.png、[1]: <a.png>)以及mailto:、data:等无需斜杠的常见协议地址内不找标签,路径段即使写成标签形态也原样保留,也不参与成链判断。紧跟在/或\后的裸标签(中英文)与方括号标签是文件路径段(/tmp/屏幕搭话:a.png、/tmp/screen comment:a.png、C:\tmp\[屏幕画面]\b.png),正斜杠盘符路径(C:/屏幕画面/c.png)整段受保护,同样不删、不算标记。标签由模型自行生成、应用代码从不输出,变体无法枚举完整;其余写法(如📺:、画面搭话:)静默透传。恢复路径的接缝。 续期或重启后,新会话 system prompt 里的历史来自两处:memory 服务渲染的最近历史(
_new_dialog),以及 core 渲染的会话缓存(_convert_cache_to_str)。两处各自在拍平前投影,但彼此分开判断:一条链若一半落在 memory 最近历史的末尾、一半落在缓存开头,两边都不足两条,就不会被合并隔离。合并判断需要 memory 服务提供结构化的最近历史尾部,属 §8.1 第 3、4 项的后续工作。同一 prompt 里的多段缓存(下一会话上下文、缓存快照、final swap 的增量)已合并判断。中文裸标签会误判用同一形态写的功能说明。
屏幕搭话后接空白或冒号即算标记,仓库内全部取证夹具(含 #3185 的)用的都是这个形态。一段回复若两次以“屏幕搭话 ”或“屏幕搭话:”起句、各接一句完整长句来介绍功能,词法上与现场形态相同,也会被截到第一句说明并去掉标签;按位置或分隔符收窄都会同时漏掉已记录的形态(例如只认行首加冒号,会漏掉全部“屏幕搭话 ”形态)。保存的原文不变,可用下述运维开关止损。“屏幕搭话就是……”、加引号或加粗的写法不受影响。请求视图逐轮变化。 跨消息截断只看最后一次用户发言之前的那段 assistant run;第 N 轮被移出视图的消息,到第 N+1 轮不再处于尾部,会以去标签后的原文回到请求里(与
[a×7, u1, ask]实测不成链的结论一致)。因此 provider 的前缀缓存会从这段历史起失效一次。本轮工具循环追加的内容不做成链截断:已流式给用户的 assistant 文本只去掉可识别的来源标签(模型仍看到自己说过的每条评论),工具调用与结果原样发送。几处窄边界。 晚到上下文按"快照 + 增量缓存"判断,没有计入 final prime 中拼在缓存之后的回调 extras 与被动回调 prime,极少数情况下一条晚到台词会被接到实际上隔着回调文本的 run 上;用户与角色显示名相同时缓存无法区分双方,守卫不改写(fail open);带图片的 AI 消息若文本被整段移除会保留为只含图片的消息,memory 渲染可能出现一行空正文。Markdown 简写引用链接若恰好以标签命名(正文
[屏幕截图]加一行[屏幕截图]: 地址),与方括号标签在词法上无法区分,按标签删除;[屏幕截图](地址)、[屏幕截图][1]与路径段不受影响。URL 在…、~处结束,因为模型常在地址后接这两个句末标点再开始下一条评论;路径里恰好在这两个字符后出现标签的地址(https://host/照片~屏幕搭话:a.png)与"地址 + 下一条评论"在词法上无法区分,按后者处理,标签在请求视图中被删除,存储与界面不变。
命中时按类别(单条消息内的链 / 跨消息尾部)写一条 INFO 日志;运维可用环境变量 NEKO_SCREEN_HISTORY_GUARD=0 关闭投影。
0A-1 的跨消息尾部规则(2026-09-28 实测并已实现)
单条消息内的链独立检测,包括带主动交付标记的消息。链也可能摊在连续几条 assistant 消息上;先按以下位置条件选取候选段,再按内部来源标记分段:
- 该段是最后一次
user/human消息之前连续的 assistant/ai 消息; - 该段之前实际存在一条
user/human消息;段起点为 0 时不读取负索引。这个条件仅描述位置,不能证明消息是在回答该用户。
候选段中,每条内部 additional_kwargs.dialog_source="proactive" 消息都是分段边界,不参与跨消息拼接;其前后剩余连续子段分别判定,命中则该子段只保留首条评论(去标签),首条之前的正常回复原样保留,第二条评论起的消息移出视图(见 §7.1.2)。子段判定时每条消息使用独立的词法状态(上一条以“喵”结尾、未闭合的引号或 <think>、围栏与引用块都不会延续到下一条),只有链计数跨消息延续。工具循环在工具结果后原地追加的 {"role": "user"} 图片轮不算用户轮,因此同一轮后续请求(含 forced-final)仍按真实的最后一次用户轮定位候选段。标记来自成功主动交付入口,不从正文或 anti_repeat_response_id 推断。单条内部仍有多评论链时照常改写。位置规则的原始实测依据如下(并非新增来源标记的模型实验):
| 布局 | 续接 |
|---|---|
[u, a, a, ask]、[u, a×7, ask](回答用户轮的尾部) | 5/5、2/2 |
[a×7, u](段前没有用户轮的独立评论对照) | 0/3 |
[a×7, u1, ask](中间隔着用户轮) | 0/2 |
[u, a, tool, a, ask](工具结果隔开) | 0/3 |
[u, a(单条消息内两条评论), ask](原有规则即可命中) | 2/3 |
第 2 个条件只保护上述没有前置用户的对照。真实应用也会产生 [user, proactive×N, user];现在通过成功交付来源标记保护这些独立评论,真实交付 payload 测试覆盖有/无前置用户、标签和截图。AIMessage 文件及 SQL 往返测试确认标记保留,出站 payload 不含该内部字段。工具结果仍打断候选段,不跨工具边界合并。
旧记录限制: 缺失标记、未知来源或元数据已丢失时继续采用原位置规则,因此旧主动交付仍可能误隔离;不移除已证实传播的跨消息检测,也不凭 ID 差异豁免。此改动仅完成主动交付至本地历史的最小来源区分,未完成屏幕来源事实采集、无标签异常检测或全面 1A/跨服务保真。
全组共 21 次请求、9,183 tokens。结果来自单一模型、合成人设、每种形态 2–5 次请求;工具结果为伪造数据且未真实执行工具。因此这是已实现规则及其实测依据,不是对任意模型或任意消息来源的普遍保证。
§7.1.1 表格后四行均属 0A-2:两行拒绝路由(原文处理请求的生成前拒绝、context_limited 提示)与两行输出侧状态(screen_chain_rejected / screen_chain_interrupted)连同它们的过滤器。0A-2 的过滤器需要在"看到第二条就停"上保证边界正确(跨分片不漏计、不误伤合法长文本),这正是 0A-1 不做输出过滤的原因——在没有可靠的边界方案前加过滤器,代价是截断合法回答。
7.1.2 0A-1 请求视图合同:去掉所有来源标签,成链时只保留首条评论
无条件去标签(主力): 请求副本里的每条 assistant 消息都删掉全部来源标签,不要求成链,不看 MIN_PROSE;引号、代码块、<think> 里的标签也删,因为改的只是模型自己回复的请求副本,原文不动。标签后的空白和至多一个冒号一并删除。英文标签紧贴 ASCII 单词时不算(screenshot、keyboard/screen display),标签后没有分隔符的中文写法(屏幕搭话就是……、“屏幕搭话”功能)也不算。只剩标签的消息移出视图。依据:事故记录 E14(只删标签、正文全留)普通输入为 248 字、0 标签、0 旧评论命中,与保留链前前缀的结果基本一致。
成链截断(兜底): 防止话题回调逐字重放。规则如下:
对一个 assistant/ai 消息的文本(字符串或 [{"type": "text"}] 列表),只要检测到链(两条相邻的带标签评论,各自达到 MIN_PROSE 且以句末标点收尾),0A-1 在请求副本中这样改写:
- 切点是首条完整评论之后的下一个标签起点。中间夹着的短评论(不足
MIN_PROSE)不打断链,会和其后的评论一起切掉。 - 切点之前的文本保留,但其中所有来源标签都去掉,连同标签后的空白和一个冒号(
屏幕搭话 :正文不留孤立冒号)。 - 切点所在的那段文本截至最后一个句末标点(及其后的闭合引号/括号);没有句末标点的残段整段不保留。首条评论之所以算"完整",正是因为它有句末标点,所以首条评论本身总能保留下来。句末标点指
。!?!?…~;ASCII.、~后面紧跟 ASCII 字母或数字时不算(example.com、v2.0、1.5),除非紧跟的正是下一个标签(today.screen comment:)。 - 切点所在消息之后的文本全部移出视图(切点按标签所在的消息算,而不是按第二条评论写完的位置算)。
例:好呀~ 屏幕搭话:这个视频画面好漂亮,色调很温柔呢。屏幕搭话:右下角那只猫好可爱…… 在请求视图中变为 好呀~ 这个视频画面好漂亮,色调很温柔呢。
跨消息的链(§7.1.3)按同一规则处理:把候选子段当作一串文本,切点之前的消息去标签保留(首条评论之前的正常回复不变),切点所在的消息截至句末,之后的消息移出视图。
为什么不用整条占位(#3201 的原方案):
- 占位是写在 assistant 消息里的一句"系统说明",相当于让角色"自己说过"一句与人设无关、还提到 UI(历史面板)的话,出戏;方括号格式本身也是可被模仿的样本。
- 跨消息命中时,N 条消息会变成 N 条一模一样的占位,这正是本修复要消除的"重复格式样本"。
- 整条占位丢掉了全部内容:用户问"你刚才说的那只猫呢",模型无从接话。
被模仿的是"带标签、成串"的格式,而不是评论内容本身。保留首条评论(去标签)后,模型上下文里是一句完整、无标签的普通台词。
与"截断前缀"的区别: §7.1.3 和事故记录拒绝"截断到链起点、保留前缀",理由是残留的半截前缀本身就是畸形样本。这里的切点落在完整评论的句末,不会留下半句话;切点所在文本若没有句末标点,整段不保留。
未验证项(必须写进交付说明): "只删标签、正文全留"的视图在话题回调下没有测过(E14 只测了普通输入;带标签历史在回调里逐字重放了 8 条)。补测若不复现逐字重放,成链截断可以降级甚至移除。另外,0A-1 成立的前提是"无标签的单条评论不会诱发串接"。事故记录的同期对照只测了"原始历史 / 保留链前前缀 / 整条占位"三种视图;"保留链前前缀"一组(同样无标签、无占位)为 0 标签、0 旧评论命中,与本方案最接近,但没有测过保留首条评论的视图。需要用同一探针补测单条消息内成链、[u, a×7, ask] 两种布局,指标为链是否复发、追问时能否接上、输出中是否出现标签。
合同细则:
- 仅改写请求副本中的
content,保留 role、tool_calls、tool_call_id、reasoning_content 及其他协议/应用元数据。原件从不改写。 - 移出视图的消息若带
tool_calls,保留该消息、content置为空串,否则会破坏后续 tool 消息关联;不带tool_calls的直接从副本中省略。首条评论所在的消息总会保留,因此两次用户轮之间至少还有一条 assistant 消息,不会破坏角色交替。 - 普通 user/system/tool 内容、图片与未命中的 assistant 消息不改写。
- 使用浅拷贝/定向内容替换等保结构路径;不能通过已知会丢字段的
messages_to_dict()dict 分支往返重建 provider 消息。 - 改写结果不含任何来源标签,因此重复投影是无操作。
- 改写不防止模型自行编造,仅保证第二条评论起的原文和来源标签没有进入请求;模型真实接话效果必须单独验证(见上方未验证项)。
- 不含"屏幕"或"screen"字样的文本不进入词法分析,也不进入结果缓存;含这些字样的单条文本的分析结果按文本缓存(上限 1024 条)。
7.2 原样引用
- 验证选择、加载快照、分配交付 ID;快照准备失败不输出正文。
- 将快照切成有序文本段,走专用引用交付器,不经过模型、工具标记解释、屏幕串接过滤或长回答摘要。
- 仍检查当前轮次、用户取消、语音所有权及连接状态。引用不是绕过生命周期的特权。
- UI 用 literal text 渲染,不执行 Markdown/HTML、动作标记或工具命令。原文中的脚本、内部标签均仅是文字。
- 完成时验证发送文本 hash 等于选定快照 hash;记录接收确认范围。部分交付不能标完整成功。
- 写可见记录并标
reference_only;普通上下文仅出现非正文操作占位。
这里的“原文”是被选定版本的已交付正文,不是模型隐藏推理、清洗前内部流或此前已丢失的未保存内容。
7.3 引用生成
使用单独的引用任务对象和取消 token,不挂在 session 级全局开关上。
- 输入:最小系统任务要求、当前用户操作、已选原文数据;不包含其余隔离记录,不让原文成为系统指令。
- 生成:使用现有 provider 适配器,但无工具声明、不复用普通聊天的工具重试循环。正常取消可终止该请求。
- 输出:固定属于当前已验证选择的
reference_generated段;模型不能输出一个标记把普通回复改成此类型。 - 资源:明确输出上限,超限/网络失败标
partial/failed,不能宣称翻译完整。首期不加入自动校验模型或额外重试。 - 持久化:存实际可见结果及 source_ref,默认不自动送入后续普通上下文、摘要、事实提取或主动搭话候选。
- 继续讨论:用户可重新选择该生成结果。为了防回灌牺牲自动延续全文的便利,此取舍必须在交互中说明。
该隔离控制的是数据和权限范围,不证明 LLM 理解正确。生成结果可能继续包含场景串;因为用户明确发起了限定来源的处理任务,不能再用标签数量将其当作普通聊天事故直接删掉。
8. 上下文组装与持久化边界
8.1 单一策略,多处适配
拟新增纯函数策略层(例如 utils/history_context_view.py),接受带身份与来源的消息,返回正文视图及隔离原因;不做磁盘 IO、不请求模型、不识别引用意图。具体文件名在实现评审时确定。
只集中“哪些记录/片段可自动进入模型”的决策,不合并所有存储和 provider 实现。适配点必须包括:
- 普通请求、工具迭代及强制最终回复。
prompt_ephemeral()的话题回调。_new_dialog()的 recent history 渲染。_convert_cache_to_str()、late context 和 session swap 的增量回填。- 摘要、记忆提取及
recall_memory对已知引用记录的自动使用。
第 5 项不能遗漏:仅隔离下一条聊天消息,但把引用原文先总结成长期记忆再回填,会绕开所有前面的控制。首期默认不自动使用两类引用正文,保留操作占位;不依赖再生成一段“安全摘要”。
这是引用功能启用前的硬依赖:先验证 reference_only 经保存、摘要/记忆入口、检索、回调和热切换仍不自动暴露正文,再允许阶段 2/4 产生真实引用记录。不能先上线交付,再把隔离放到最终回归阶段补做。若新交付路径会产生没有来源策略的原文副本,应拒绝启用该路径。
8.2 不能拍平后补救
role | text 不是可靠的原文访问协议。记录的角色、来源及 context_policy 必须在渲染前仍然存在;不要扫描任意系统提示中的“屏幕搭话”并截断后文。
对于已经混入不透明 system/persona/记忆字符串的旧污染,无法从字符串可靠恢复边界:保持原件,明确列为恢复检查范围。不得宣称单次热切换已净化全部历史。
8.3 传播合同
BaseMessage与 OpenAI dict 转换、跨服务消息、recent history、时间归档和 session cache 必须保留身份与上下文策略。- API 发给模型时只渲染必要正文与操作占位,应用内 IDs/权限字段不默认泄漏到模型。
- 已标记引用的记录如果无法保留策略,停止该记录的自动上下文使用并报告兼容性错误;不将它降级为普通 assistant 正文。
- 新引用记录使用带版本的必填 envelope,写入/传输接收端校验
message_id、kind、source_ref、context_policy 的完整性;已协商新协议却缺字段时拒绝接收。适配器需验证存储往返结果,不能仅凭握手声称保真。旧无版本记录走旧数据策略;一旦旧链路把全部来源信息丢掉,就无法可靠从正文恢复,这也是禁止不兼容混跑的原因。 - 新旧服务混跑必须通过 capability 检查关闭新引用任务。不能默认旧二进制会尊重未知字段,也不能宣称新增元数据天然向后安全。
- 原始查看/导出读取用户可见原件,不走生成上下文投影。
9. 文本、TTS 与动作安全
9.1 文本精确性
成功的原样引用比较 Unicode 文本及 UTF-8 hash;UI 可以软换行,但复制、导出不可改变换行/标点/空白。展示来源标签与正文分离。不得把引用卡片标签再变成需要过滤的“屏幕搭话”前缀。
9.2 TTS 的有限承诺
已有 handle_text_data() 支持 UI/TTS 分流,长回答可能摘要,不能直接拿来宣称原文朗读一致。
拟增加原文朗读模式:不走长回复摘要,不通过 Markdown/括号清洗删除原文段落,按 provider 限制切句、排队、取消。TTS 的发音、标点处理和服务商规范化不能保证逐字声学一致;能承诺的是提交文本和实际提交范围可追踪,失败不伪报完整朗读。
原文中的 __interrupt__、__shutdown__、工具名称、动作语法必须作为文字数据,不能落入控制信号分支。实现前必须核查队列控制消息,采用明确的数据/控制类型区分或等价可靠适配;不能靠替换用户原文规避。
文本可以完整显示而音频失败/被用户打断,两个状态分别呈现。用户要求长文本朗读时才扩大播报预算;不能自动在后台补播取消后的段落。
9.3 交付一致性不是同一字符串塞进所有队列
统一的是同一快照、段落身份和交付状态。普通生成原有摘要语义不在本次重写;原文引用明确禁用摘要。音频排队、provider 接收、客户端播放确认分别记录,不能承诺网络中断后的声音“严格恰好一次”。
10. 状态机、并发与异常
received → validated → snapshot_ready → delivering → awaiting_ack → completed
└──────────────→ rejected
└→ partial / cancelled / failed- 原文引用的完成依赖文本交付确认;朗读结果独立。引用生成还需要 provider 正常结束,
length不能当完整结果。 - 同一用户新输入抢占旧引用:旧任务取消,不再入队其文字/音频;已交付范围保留。
- 并发主动搭话使用独立上下文策略,没有任何引用豁免;它不能取得当前选择快照。
- 源记录在验证与交付之间发生变化:冻结版本和锁内版本检查决定结果,不读取“最新”替代原文。
- 断线后按
delivery_id/sequence恢复文本;客户端去重并返回确认。音频默认不自动重放,用户明确重播才创建新播放操作。 - 进程崩溃后通过已持久化交付状态恢复/显示部分状态;未有事务支持前不能声称状态与输出原子一致,需幂等重放测试。
- 引用生成不自动重试。需要用户重试时使用新请求 ID,同时不重复原文展示或其他副作用。
- 源不存在、revision 不符、权限变化、存储失败时明确失败;不把拒绝悄悄改成普通 LLM 请求。
11. 旧历史迁移、恢复与回滚
11.1 旧记录身份
分两层迁移,不扫描全文并改写内容:
- 新写入记录立即获得服务端身份,沿交付和存储链保真。
- 旧记录被用户打开/选择时,在权威历史记录上惰性分配 ID,原文不改。分配需要记录锁和原子写入,重复执行返回相同身份。
旧存储暂不支持附加字段时,迁移清单必须绑定“源存储记录实例+版本校验”,不能只绑定 hash、可变列表下标或时间戳。重复文本、文件压缩/分片、归档搬移必须有明确映射;不能可靠映射的记录暂不提供结构化引用,仍可原始导出。具体存储格式在阶段 1 核查后决定,禁止先新增旁路库再寻找一致性策略。
11.2 已污染会话恢复
优先从有结构的原始消息重建模型视图,保留旧运行视图备份和隔离判定版本。隔离列表可以撤销;不调用现有“清空到只剩系统消息”的重复检测作为默认恢复。
恢复前检查 recent history、热切换缓存、摘要和检索回填。已污染但无来源边界的长期文本不自动擦除,向维护者提供不含原文的待检查范围。无法完成全路径隔离时不报告“恢复完成”。
11.3 回滚
- 先停止创建新引用任务,等待/取消正在交付的任务,保留已有引用查看与状态读取。
- 回滚版本必须能尊重
reference_only或由兼容读取层继续隔离;不能直接回退到会把引用记录当普通正文的旧版本。 - 不删除新元数据,不从备份覆盖用户回滚期间的新消息。
- 如只能回滚到旧二进制,应先隔离引用记录的自动回填入口并验证;做不到则阻止此降级,不能把“关闭功能开关”当作数据安全回滚。
12. 模块与实施顺序
0A 只有一次交付:0A-1。它之后没有排定的 0A 后续。
| 编号 | 范围 | 说明 |
|---|---|---|
| 0A-1 | 拆除"用户措辞决定权限"的全局豁免;命中链的 assistant 历史在请求副本中改写为首条评论(去标签、截至句末,含跨消息判定) | 已交付。从 main 交付,因此不继承 #3185 的流式过滤器 |
0A-1 不代表完整引用能力(那从阶段 2 起)。
以下条目曾被称为 0A-2。它们没有排期,不构成 0A 的一部分,也不在阶段表的任何一行:
| 未排期条目 | 来源 |
|---|---|
| 引用拒绝路由与应用级缺失提示 | §7.1.1 表格第 2、3 行 |
| "看到第二条就停"的输出过滤器及其两行状态 | §7.1.1 表格第 4、5 行;§7.1.3 末段 |
本文其余地方的「0A-2」仅用于指代 §7.1.1 的那四行,不表示一个阶段。
| 阶段 | 工作与预计范围 | 进入下一阶段的门槛 |
|---|---|---|
| 0:确认止损边界 | 确认立即取消全局豁免及过渡期引用限制;完整交互/限额评审另行推进 | 不以完整身份、UI 或状态机设计完成作为止损前提 |
| 0A-1:请求侧隔离(独立止损 PR) | 拆除措辞全局豁免;命中历史改写为首条评论、保留协议字段 | 不依赖新身份/引用 UI、历史原件入口或应用级受限提示;出站 payload 不含来源标签、第二条起的评论或残句;普通/回调/工具/重试均一致;改变用户措辞不改变权限结果 |
| 1A:保留已知输入来源(小改动) | 在 CHAT 压平前保留屏幕/视觉输入事实,经成功交付写入本地历史元数据 | 纯聊天/屏幕/混合来源/回退/取消测试通过;去掉正文标签仍保留来源;不宣称完整归档保真或来源等于污染 |
| 1B:身份与策略保真 | 消息模型、交付事件、序列化、recent/归档/session cache;引用记录的全自动消费者隔离 | 跨进程及热切换往返保真;旧记录定位测试通过;完整引用模拟记录不能经摘要、提取、检索或回调回灌 |
| 2:最小逐字引用 | 请求验证、选择 UI、服务端快照、literal 文本交付 | 先通过 1B 隔离门槛再启用;518 字反例逐字通过;无模型请求;不可越权或污染下一轮 |
| 3:语音与生命周期 | 原文 TTS 模式、取消/重连/失败、控制消息隔离 | 不执行原文中的命令;状态不伪报;正文完整且可停止 |
| 4:引用生成 | 单独翻译/分析请求、预算、来源与上下文隔离 | 无外部工具;结果不进入普通/摘要/回填;用户可继续选择结果 |
| 5:整体验收与恢复 | 已完成止损及局部引用能力的全路径回归、受影响会话恢复和降级演练 | 不再承担首次拆开关或首次补来源/引用隔离的职责;不得由终局验收拖延 0A-1 独立发布 |
12.1 0A-1 的独立范围与兼容性代价
0A-1 是第一个独立、可验证的交付单元,1A 也可单独评审;1B–4 不作为其发布依赖。0A-1 的修改范围限于措辞全局开关的调用链、命中历史的请求投影、相应回归与发布说明;不夹带新身份系统、原文读取工具或新引用 UI,也不夹带拒绝路由与输出侧拦截(属 0A-2)。
发布说明必须写出适用边界,不能把输入投影表述为通用能力。 该投影以 screen_chain_start() 命中为前提:历史无标签、评论短于 MIN_PROSE 或不成串时,检测返回 None,投影静默无操作——不报错、不降级、不产生任何提示,现象与修复前完全相同。已实测确认这一点(事故记录“修复生效前提与失效条件”)。没有这段说明,现场历史属于该形态的用户会认为修复无效,而应用侧没有任何信号可供区分。
必须验证:相同污染历史下,普通聊天、引用要求、否定引用、工具图片追加、重试和强制最终回答都不恢复全历史。测试应改变用户措辞而保持权限结果不变,不再为不同措辞设计新豁免。
0A-1 的首要机器验收是实际出站请求,不是输出标签数量: 对确实发出的每次请求,在 provider 序列化/传输边界捕获 payload,逐条确认命中消息只剩首条评论且不含来源标签,第二条起的评论没有被另一个字段/拼接路径重新放回;原始记录未改写,工具关联及不受影响消息不变。未排期的拒绝路由若实现,另加断言:该原文请求没有调用 provider。普通聊天与回调沿用事故探针同组样本;独立且未被隔离的合法评论不要求一并清空。
此处“请求不含被隔离正文”限定于被策略命中的消息/片段及其自动搬运副本,不意味着可以删除用户当前明确提供的相同文字。逐字段/来源检查是主断言,字符串包含检查只是已知样本的辅助,不应把同文不同来源误判为泄漏。
代价不是“零新增回归”:现有能被豁免并完整通过的合法原文处理会受限。0A-1 不提供新的历史原件入口或应用级受限提示,两者均不是本次发布门槛;发布说明必须披露这一限制,不能把文档说明当作已经实现的产品提示。未来实现拒绝路由或通知的版本,才需配套历史原件入口和明确受限提示,区分材料未加载与输出被抑制,不将部分回答当完整引用成功。该要求不构成已排期交付。阶段 2/4 恢复的是受结构化范围约束的能力,不恢复旧全局开关。
止损只关闭 #3185 暴露的那条放大路径,不宣称完成旧历史净化、首次污染修复或全部复读治理。若尚未确认现场部署版本,应分别描述“修复当前分支”“上线目标版本”和“用户恢复”,不能把它们合并成一个结论。
其余改动按逻辑独立提交与评审,不逐条评论创建碎片提交;完整引用协议的进一步拆分由实际 diff 和上游维护者决定。本文不授权创建分支、提交、推送或自动更新 PR。
拟新增引用协调器可以放在 main_logic/core/,纯策略放 utils/,存储访问继续由既有记忆服务负责。不要为了“统一”重写 provider、整个记忆服务或全部聊天 UI。
13. 验证矩阵与发布门槛
13.1 确定性测试
| 风险 | 必须覆盖 |
|---|---|
| 0A-1 出站请求 | 捕获普通/回调/工具/重试/强制最终的序列化 payload;来源标签、第二条起的评论与残句不存在 |
| 未排期:拒绝与中断 | 引用拒绝路由无 provider 调用;漏识别仍有实际缺失提示;流式已交付与未交付分别中断/拒绝;不将 518→167 宣称零回归或完整成功 |
| 空/短历史前缀 | 零前缀、“呼噜……”、完整正常前文均保留并接上首条评论;无句末标点的残段不保留;保留工具关联,投影幂等且原件不变 |
| 输入来源再次丢失 | 屏幕/非屏幕/混合/回退/抢占,区分输入存在、请求带图与输出语义,不再把全部来源压成 CHAT |
| 标签依赖与新旧边界 | 无标签旧历史的当前投影无操作须明示;可信来源/策略记录去掉标签后处理不变,未知来源不自动当屏幕记录 |
| 无关历史恢复 | A 污染、B 正常,只选 B;所有 provider 请求均不含 A 隔离正文 |
| 原文误删 | 完整含标签原文、无引号、代码/引号/内部控制字样;所有切分位置精确相等 |
| 目标漂移 | 新增主动消息、同文不同 ID、旧 revision、跨角色/连接、删除竞态 |
| 非法范围 | 空范围、越界、负值、emoji/组合字符、UTF-16 转换、64 KiB 边界 |
| 输出授权扩大 | 模型/客户端伪造 kind、其他请求复用快照、普通输出夹带引用标记 |
| 后续传播 | 普通聊天、回调、工具最终回复、归档、摘要、检索、新会话、late cache |
| 设计自引入污染 | 完整原文引用和引用生成结果都从实际交付记录出发,经落盘、摘要/提取、检索和新会话后仍为 reference_only;不能只测当轮或用已截断结果替代 |
| 正常数据损坏 | 用户正文、系统提示、图片、工具元数据、正常引用原件保持不变 |
| 生命周期 | 交付前/中/后取消、断线重连、进程退出、重复 request_id、源版本冲突 |
| TTS | 原文控制词不执行、无摘要替换、部分播放不标完整、不自动重复播放 |
| 兼容与降级 | 新旧服务混跑、缺元数据、回滚重读、旧记录身份迁移幂等 |
沿用现有 pytest/前端测试工具链,不因引入文档安装新框架。现有 screen guard、tool calling、provider frame、proactive staging、取消摘要测试作为回归入口,新增测试应对应上表实际风险,不只是原评论措辞变体。
13.2 真实模型与交付测试
- 0A-1 先验收输入隔离:实际出站 payload 的确定性检查通过后,再用同一组污染历史+普通输入/回调观察模型接话,不能用“响应没标签”替代请求断言。
- 捕获 payload 只能证明送入了什么,不能确定模型会怎样接话。改写后的新编场景、重复前缀、错误引用声明仍需有限真实模型实验(保留首条评论的视图尚未做真实模型对照,见 §7.1.2);离线 MockTransport 返回不属于真实 Qwen 行为证据。
- 原样引用应有 0 次模型请求,可由确定性验收提供完整性证据;仍需真实 UI/TTS 端到端测试。
- 普通聊天、回调、引用生成及其后续轮次做有限真实模型对照:旧句重放、新编评论串、错误拒绝和数据缺失为相关验收指标;普通游戏化续写/单段无图场景单独作内容质量观察,附零屏幕历史基线,不据此判本次污染修复失败。
- 精确引用后的下一轮必须用完整已交付原文作可见历史输入验证策略,不能重复使用“已被截断”的回归样本冒充通过。
- 记录请求内容是否包含不应开放的正文;不能只检查最终响应中的标签次数。
- 预先限定调用次数、token 与失败停止规则;不循环重试寻找一次成功。对其他 provider 的协议和核心路径做确定性回归,不把单模型结果外推。
- 首字、首音频、取消生效延迟、最大缓冲量、provider 调用数分别测量。本设计无“零额外延迟”的承诺。
13.3 硬性门槛
以下任一发生即不能放行:完整原文变短却返回成功;引用开放到无关消息;下一轮/回填自动拿到 reference_only 正文;取消后继续交付;原文触发动作/队列控制;未经确认重复工具副作用;元数据丢失后静默按普通消息处理。
分阶段解释:0A-1 尚不提供结构化完整引用,验收的是措辞不再改变权限结果、且出站 payload 不含隔离正文;“如实报告受限状态”需要拒绝路由与通知(未排期),不能要求 0A-1 先实现阶段 2 的逐字交付。但从阶段 2 起,上述完整性与引用自污染门槛必须全部适用,不能沿用“临时止损”理由豁免。
未排期的拒绝路由若实现,同样不允许“残缺正文+完整引用成功”的组合。生成前路由的引用请求返回拒绝,生成中已交付才触发的异常返回中断而非成功;前端、完成回调、TTS 收尾和历史提交必须表达同一结果,不能只改提示文案而仍执行原来的成功分支。
13.4 设计阶段的离线 payload 检查(2026-09-28)
本地探针复用了现有 Qwen 诊断脚本的合成历史,在内存 httpx.MockTransport 捕获 OpenAI SDK 实际序列化的 HTTP JSON。8 个候选用例通过:普通输入、回调、空前缀、短前缀、完整前缀、带工具关联的历史、引用措辞及不受影响的角色/图片。
候选采用整条文本占位;断言命中字段内容、其余字段/消息保持、原件不变,以及已知隔离评论没有进入 payload。另确认 #3185 的实现仍会产生空 content 和“呼噜……”残前缀,因此当时不能把候选结果称为该 PR 已修复;#3201 采用的正是这里的整条占位候选,0A-1 拆分交付时改为保留首条评论(§7.1.2)。
本轮网络请求与模型调用均为 0,不读取凭据、不导入应用。检查覆盖候选纯投影和 SDK 序列化,不覆盖生产工具循环、拒绝路由、UI/TTS 或真实 Qwen 接话;上述路径仍是该部分实现后的必验收项(当前未排期)。私有探针不作为公开 CI 已覆盖的替代。
其中生产工具循环一项已由 0A-1 补进公开 CI:tests/unit/test_screen_history_request_sites.py 经真实 stream_text 与工具循环,在 provider 传输边界断言 OpenAI 工具循环、tools 被拒后的重发、forced-final 以及 Gemini 原生工具循环和 forced-final 的请求都已改写;把任一调用点改回原始 messages 都会使其转红。拒绝路由、UI/TTS 与真实 Qwen 接话仍未覆盖。
通过测试只证明覆盖范围。已测带标签传播的两类目标症状均被抑制,可确认这组实验有效;事故关闭还需要产品链路验证、受影响会话恢复检查及用户现场反馈,不能用文档完成或测试数量代替。不得因未消除基线角色扮演而把有效的串接修复记成失败,也不得把这组通过外推为旧无标签记录已覆盖。
14. 可观测性、隐私与降级
默认记录操作类别、策略版本、来源解析结果、引用长度、隔离条数、provider 次数、交付状态、耗时及稳定错误码。关联 ID 使用受控日志范围,避免无界指标标签。
不默认记录原文、模型完整请求、截图、密钥或可跨用户关联的内容 hash。内容级诊断需单独开启并遵守保留/脱敏规则。逐字引用不把原文发送第三方 LLM;用户启用云 TTS 或翻译/分析仍会按其所选 provider 发送必要内容,应明确提示。
监控重点是错误截断、策略丢失、重复交付和不应发生的模型调用,不把“零标签”当唯一健康指标。功能降级只能拒绝引用并保留原始访问,不能恢复全局放行,也不能悄悄把请求当普通生成继续执行。
15. 备选方案与取舍
| 备选 | 不采用为主方案的理由 |
|---|---|
| 更多引用正则或 LLM 意图分类 | 即使意图正确,仍不能证明具体原文范围或合法输出边界 |
| 将始终开启现有过滤当作最终引用方案 | 会截断合法原文,不能作为最终完整引用能力;但可作为阶段 0A 的止损策略,须明确能力限制与实际拦截状态 |
| 给原文加引号/代码框 | 当轮和下轮均可能保留;格式不是可信来源与生命周期 |
| 让模型输出可信引用标签 | 模型输出不是授权,用户正文也能包含同名标记 |
| 只做相似度去重 | 漏掉新编场景串,也可能误伤合法复述 |
| 完整生成后校验和重试 | 增加延迟;原文引用本就不必生成,普通工具轮不能整体重跑 |
仅持久化已压平的 CHAT,再把它当屏幕专属来源 | 丢失细分信息,不能倒推出屏幕来源;不否定压平前保存已知输入来源事实,该小改动已纳入阶段 1A |
| 清空或重开会话 | 损失上下文,回填仍可能载入原污染 |
主要代价:服务端身份与策略字段贯通、显式选择交互、独立引用交付、引用正文不自动延续到普通聊天。优点是把无法可靠从文字判断的权限问题改成可验证的数据与执行路径边界;并不消除模型生成质量问题。
16. 实施前待确认事项
这些事项决定完整引用能力的接口与用户行为,在相应能力实现前确认;不是 0A-1 止损的全部前置条件。0A-1 只先确认取消全局豁免与引用暂时受限;产品告知方式属未排期部分:
- 是否接受首期一个消息/连续片段,以及模糊自然语言需要确认的交互。
- 64 KiB 显示限额、长原文朗读确认与生成预算不足时的提示。
- 现有权威历史存储如何分配并保留 ID,旧档案搬移、删除与 revision 的具体一致性实现。
- 哪些客户端首期支持引用;原生实时音频入口如何明确降级而非伪支持。
- 先独立交付 0A-1,1A 独立评审;其后身份基础、引用交付、策略迁移如何继续拆分,以及兼容读取版本的发布顺序。
完成定义: 上述取舍明确、阶段门槛通过、文档改为实际实现合同并更新入口说明之后,才能称局部引用能力已实现。当前文档及此前探针均不满足这个定义。
