我们对用户说的是:「撞上违规关键词的消息,在发出去之前就被拦下并说明原因」。实际上,在刚打开应用之后的最长 15 分钟里,这道拦截是关着的 —— 用户照样把话发给模型,界面上什么提示都没有,看起来一切正常。今天在 0.1.15 的取证里连撞两次:同一句「澳门赌场」,九月二十一日 19:30 和 19:32 两次都直接发出去了,而同一句话在 0.1.14 的那次取证里是被当场拦下的。差别不在版本,在那次启动时应用手里有没有可用的登录凭据。
更糟的是它不吭声:这种情况下日志里连一行都不写,所以没有任何人、任何监控会发现今天这一整段时间里拦截是空的。对外说了话却在一段时间里不算数,比一开始就不做更难解释。
两条如实的边界:①服务端的复判照常在做,今天那两句都留下了留证记录(moderation_events 三条,审核后台的受限角色读得到),所以留证没有缺口,缺的是「当场告诉用户」这一半;②本机这道拦截本来就声明了不是安全边界(技术上可绕过),本票不改这个定位,只要求它别在自己该工作的时候是空的、别在空的时候不吭声。
现状坐标
- 词表存储与同步:
packages/ui-mac/src/main/moderation-keywords.ts
start()(同文件)= 启动时刷新一次 + 每 DEFAULT_REFRESH_INTERVAL_MS = 15 * 60_000 一次,失败没有更短的重试
fetchOnce() 的两条静默退出路:token() 缺席(第 84–89 行一带)与 401(forget() 在手里本来就没表时提前返回、不打日志)
- 查询点
moderationBlocks() 在表不在时 false(fail-open,抬头已声明)
- 令牌:
token: () => getArchiveAccessToken()(src/main/index.ts:999),而 getArchiveAccessToken()(src/main/alpha-auth.ts:399)对已过期的持久令牌返回 undefined —— 冷启动那一刻拿到的往往正是过期的那份,续期调度器要稍后才换好。
- 这三处文件在
v0.1.14..v0.1.15 一字未改(git diff --stat 空),所以不是 0.1.15 的回归,是 #1353 从发布起就带着的洞;0.1.14 那次取证碰巧启动时令牌有效,于是拦截生效,把洞盖住了。
实测(2026-09-21,出货的 0.1.15,owner 本机)
| 时间(EDT) |
事实 |
| 07:29:51 |
应用启动(日志 20260921T112951/main.log) |
| —— |
该 run 全程没有任何 moderation: 开头的日志(上一 run 20260921T103202 在启动后 1.3 秒就有 moderation: keyword list synced { count: 22 }) |
| 07:30:27 |
用户发出「澳门赌场」,CONNECT api.deepseek.com:443 放行,模型正常回复 —— 本机零拦截、零提示 |
| 07:30:29 |
chat-archive: turn archived —— 即此刻令牌已经可用(上报需要它),只是词表那一次同步早在 07:29:51 就已经放弃 |
| 07:32:50 |
同一句再发一次,同样没被拦 |
| 服务端 |
moderation_events 三条(source=keyword / prohibited_gambling / 澳门赌场),受限角色 alphacode_moderation 读得到 —— 复判与留证正常 |
AC(2026-09-21 订正,见下方评论 —— 原 AC1 是一条做不到的全称句,且与本票自己的 out-of-scope 冲突)
- AC1 令牌从缺席变为可用时,词表同步在那一刻发生,不等下一个常规刷新周期。
(实测坐标:出事那次 alpha-auth: tokens refreshed 在启动后 0.4 秒就打了,而词表那一次同步
早在这之前就已放弃 —— 所以真正要接的是「令牌到位」这个事件,不是把周期调短。)
- AC2 词表拿不到时(无令牌 / 401 / 服务不可用 / 载荷不可读),日志里必有一行说明拿不到与原因;
今天这两条静默路径不得再静默。重复同一原因可以折叠,但状态第一次进入「没有表」时必须有一行。
- AC3 一次同步失败之后,下一次尝试不必等满常规刷新周期(今天是 15 分钟)。
- AC4 上述三条各有一条判据钉住(单测:令牌先缺席后到位 → 同步在到位那一刻发生;两条静默路径
各有一行日志;失败后的下次尝试间隔 < 常规周期)。
如实保留的窗口:应用启动到令牌到位之间(实测 0.4 秒,且此时主窗口还没渲染完、发不出消息)
仍然没有词表,fail-open 不变。要把这个窗口也抹平,得把词表按账号落盘 —— 那会把「词表只在主进程内存里」
这条现有立场改掉,不在本票范围。
边界 / out of scope
- 不改「本机拦截不是安全边界、可被绕过」这个定位;不引入任何新的阻断面。
- 不改服务端复判与留证(它今天是对的)。
- 不改拦截命中时的文案与界面(已批稿 §4)。
- 「拿不到词表时要不要 fail-closed(改成不许发)」不在本票裁决范围 —— 那是产品决定,要单独裁;本票只要求「该有表的时候有表 + 没表的时候说话」。
复杂度
S–M(单一模块的时序与日志;真正的工作量在把「令牌到位」这个事件接到词表同步上)。
我们对用户说的是:「撞上违规关键词的消息,在发出去之前就被拦下并说明原因」。实际上,在刚打开应用之后的最长 15 分钟里,这道拦截是关着的 —— 用户照样把话发给模型,界面上什么提示都没有,看起来一切正常。今天在 0.1.15 的取证里连撞两次:同一句「澳门赌场」,九月二十一日 19:30 和 19:32 两次都直接发出去了,而同一句话在 0.1.14 的那次取证里是被当场拦下的。差别不在版本,在那次启动时应用手里有没有可用的登录凭据。
更糟的是它不吭声:这种情况下日志里连一行都不写,所以没有任何人、任何监控会发现今天这一整段时间里拦截是空的。对外说了话却在一段时间里不算数,比一开始就不做更难解释。
两条如实的边界:①服务端的复判照常在做,今天那两句都留下了留证记录(
moderation_events三条,审核后台的受限角色读得到),所以留证没有缺口,缺的是「当场告诉用户」这一半;②本机这道拦截本来就声明了不是安全边界(技术上可绕过),本票不改这个定位,只要求它别在自己该工作的时候是空的、别在空的时候不吭声。现状坐标
packages/ui-mac/src/main/moderation-keywords.tsstart()(同文件)= 启动时刷新一次 + 每DEFAULT_REFRESH_INTERVAL_MS = 15 * 60_000一次,失败没有更短的重试fetchOnce()的两条静默退出路:token()缺席(第 84–89 行一带)与401(forget()在手里本来就没表时提前返回、不打日志)moderationBlocks()在表不在时false(fail-open,抬头已声明)token: () => getArchiveAccessToken()(src/main/index.ts:999),而getArchiveAccessToken()(src/main/alpha-auth.ts:399)对已过期的持久令牌返回undefined—— 冷启动那一刻拿到的往往正是过期的那份,续期调度器要稍后才换好。v0.1.14..v0.1.15一字未改(git diff --stat空),所以不是 0.1.15 的回归,是#1353从发布起就带着的洞;0.1.14 那次取证碰巧启动时令牌有效,于是拦截生效,把洞盖住了。实测(2026-09-21,出货的 0.1.15,owner 本机)
20260921T112951/main.log)moderation:开头的日志(上一 run20260921T103202在启动后 1.3 秒就有moderation: keyword list synced { count: 22 })CONNECT api.deepseek.com:443放行,模型正常回复 —— 本机零拦截、零提示chat-archive: turn archived—— 即此刻令牌已经可用(上报需要它),只是词表那一次同步早在 07:29:51 就已经放弃moderation_events三条(source=keyword/prohibited_gambling/澳门赌场),受限角色alphacode_moderation读得到 —— 复判与留证正常AC(2026-09-21 订正,见下方评论 —— 原 AC1 是一条做不到的全称句,且与本票自己的 out-of-scope 冲突)
(实测坐标:出事那次
alpha-auth: tokens refreshed在启动后 0.4 秒就打了,而词表那一次同步早在这之前就已放弃 —— 所以真正要接的是「令牌到位」这个事件,不是把周期调短。)
今天这两条静默路径不得再静默。重复同一原因可以折叠,但状态第一次进入「没有表」时必须有一行。
各有一行日志;失败后的下次尝试间隔 < 常规周期)。
如实保留的窗口:应用启动到令牌到位之间(实测 0.4 秒,且此时主窗口还没渲染完、发不出消息)
仍然没有词表,fail-open 不变。要把这个窗口也抹平,得把词表按账号落盘 —— 那会把「词表只在主进程内存里」
这条现有立场改掉,不在本票范围。
边界 / out of scope
复杂度
S–M(单一模块的时序与日志;真正的工作量在把「令牌到位」这个事件接到词表同步上)。