08|允许 AI 写文档之后,授权和审核怎样才算数

流萤简历是我做的 AI 简历与求职资料工作台。这是专栏第 8 篇,从 AI 连接器的范围选择与审核入口,聊设备授权、令牌轮换和不可变修改集。

产品特写:把一个专题交给 AI,仍然知道它能做什么

在流萤里,你可以让常用 AI 整理某个专题、追加文档或修改页面结构。接入时选择资料范围、是否包含后代、读写方式和期限;需要先看结果时选择审核模式。完成后,网页保留修改记录和资料入口,成果可以继续编辑。

这张图里最重要的承诺是"后续授权由你决定"。用户复制一段指令让 AI 开始连接,不等于把整个账号交出去了;点击批准某个候选,也不等于允许 AI 以后随意更换候选内容。

本篇聚焦范围受限的资料设备连接。个人令牌调用 OpenAPI 是另一条入口,二者共用业务规则,但凭证、范围和审核语义不同。把两者混为同一种"登录",很容易误解机器到底拿到了什么权限。

终端没有登录框,身份怎样交接

设备授权适合这种场景:CLI 运行在终端,用户在浏览器确认身份与权限。终端发起设备请求,拿到设备码和授权链接,轮询结果;浏览器完成登录后批准范围,终端再交换访问凭证。

流萤还增加了设备证明。CLI 本地生成高熵随机 verifier,把它的 SHA-256 摘要作为 challenge 发给服务端;轮询时提交 verifier,服务端核对摘要。这样仅看到授权链接或人类识别码,并不足以代替原设备完成凭证交换。

当前服务对设备码、识别码和令牌保存摘要,而不是保存明文。随机值足够高熵,摘要能让数据库中的这些字段不直接成为可用凭证;它不能保护已经泄露的原始令牌,也不能代替 HTTPS。

轮询还处理 authorization_pending、slow_down、拒绝和过期。设备行锁保护一次交换,成功后标 consumed,不能再交换第二套凭证。轮询过快会延长间隔,CLI 按协议增加等待,而不是不断重试压服务。

这条流程借鉴了设备授权协议的交互,设备证明属于本站扩展。不能据此宣布流萤是一个通用兼容的 OAuth 授权服务器;文章分析的是具体实现与边界。

接入提示词,只能把任务带到授权页

产品里"复制接入指令"会携带一次性的 intent,绑定账号和建议范围。它帮助 CLI 发起正确的设备请求,减少用户手工选择的步骤,但本身不授予最终权限。

如果把 intent 当作授权,用户复制到对话的一段文本就可能变成长时间访问资料的凭证。当前实现仍要求浏览器真实会话,用户确认资料范围、模式和期限。CLI 不会用提示词替自己批准设备请求。

授权时创建新专题也在同一数据库事务里完成:专题、页面索引、历史与连接一起成功或回滚。否则可能出现授权失败但留下空专题,或者专题创建失败但连接已经指向一个不存在的范围。

这是一处很能体现产品与工程关系的设计。用户觉得自己只是"创建一个给 AI 整理的地方",系统需要把内容身份和委托权限一起建立;两个操作之间不能留一个难以解释的半成品。

scope 是上限,还要和当前权限取交集

连接 grant 由根页面、是否包含后代和 read / review / write 模式构成。scope 可以理解为用户明确圈定的机器操作范围,它是一个上限,但不是唯一条件。

每次请求仍要读取当前页面角色。用户对某页没有编辑权限,即使连接曾被授予 write,也不能继续写;用户有编辑权限,但页面在连接范围之外,CLI 同样不能写。管理员关闭外部写入时,又增加一道全局限制。

连接的后代范围也根据当前树计算。页面被移动到授权范围之外,不能继续沿旧位置访问;复制、移动和回收操作还要看来源、目标与后代,不能只检查最外层根节点。

资料权限解析关闭持链接访问,避免把"能通过一个链接阅读"转化成"能委托 AI 持续访问"。这限制了机器权限的来源,也让用户撤销邀请后能够影响后续请求。

read、review、write 还表达不同的流程。read 不能准备写入;review 可以产生候选但要先经用户审核;write 允许在其他条件满足时提交。审核不是一句写在提示词里的软约束,服务端也会拒绝未批准候选的 commit。

为什么访问令牌短期、刷新令牌单次使用

当前访问凭证最长 15 分钟,同时不超过连接到期时间。短期凭证减少单个 access 泄露后的时间窗口;连接暂停、撤销和到期仍由数据库状态实时控制,不能只依赖 token 自身过期。

刷新凭证每次交换都会轮换,并把旧值标为 consumed。如果旧刷新凭证再次出现,服务端把整个连接撤销。它表达一种判断:同一份本应只使用一次的秘密又被拿来交换,可能存在重放或并发使用。

重放处理有个事务细节。代码在事务里更新撤销状态后返回错误数据,再由外层映射响应;若直接在同一事务里 throw,让撤销跟着回滚,安全动作就会被错误处理抵消。

下面摘出 refreshToken() 中的重用分支,省略已获取的连接和 token 查询:

ts 复制代码
if (token.consumed) {
  await tx.update(t.flowingConnections)
    .set({ status: "revoked", accessHash: null })
    .where(eq(t.flowingConnections.id, lookup.connectionId));
  return { oauthError: "invalid_grant" };
}

这里的 return 使事务可以提交撤销状态,外层再把结果变成错误响应。如果在更新后抛异常,默认事务回滚就可能把本来要执行的撤销一起撤掉。

正常的多进程也可能制造类似重放。两个 CLI 进程同时发现凭证快过期,若都用同一 refresh 交换,第二个会触发重用检测。客户端用文件锁串行刷新,拿到锁后重新读凭证;若别的进程已换过 access,就直接使用新值。

这个设计把恢复责任分给两端。服务端对重复秘密保持严格,客户端避免把正常并发伪装成攻击。系统密钥环承担本地秘密存储,配置文件只保存站点与连接编号;密钥环不可用时明确失败,显式注入的只读凭证文件是另一种运行模式,不自动回退成明文。

有了授权,为什么还要准备修改集

授权解决"你能改什么",没有表达"这次具体要改成什么"。用户要求 AI 整理十个页面,直接发送十次独立保存,会出现前几页成功、后几页失败,审核也只能看一段模糊的操作描述。

修改集把一次任务冻结成一个候选:操作列表、基础 revision、前后差异、权限依赖、到期时间与状态。当前同一所有者最多 20 个操作,要么一起提交,要么回滚。

prepare 会在当前资料上应用操作,生成 diff 并记录结果。它尚未把候选写进正文,但已经定义了用户准备审核的实际内容。复制得到的新页面与块身份也在候选阶段固定,不能到提交时重新随机生成另一套对象。

图中的 Stop 是说明性终点,表示此次不能继续提交,不是数据库里新增了一个统一的 stop 状态。实际返回可能是过期或版本冲突,需要重新读取再准备。

本地 --dry-run 也要区分。它只检查请求结构并给出稳定幂等键,不请求服务端、不生成远端候选、不确认当前权限。终端显示一次漂亮预览,不能证明用户已经批准或服务端已经接受。

"批准"与"直接应用",实际是两条路径

当前网页提供普通审核,以及显式直接应用候选的路径。普通 reviewCliChange() 只把 review 改为 approved 或 rejected,之后由 CLI 提交同一 changeId。

另一条 applyWebChange() 在用户明确请求网页直接应用时,沿共享的提交实现完成写入。它不是"批准后顺便偷偷执行",而是一个不同的产品动作,调用时仍要检查连接、权限、版本和额度。

这个区别值得专门写,因为"批准"和"应用"在文案里很相近,数据动作却不同。技术文章如果只读旧文档,可能把产品现有入口讲错;分析要同时核对 controller 的分发和 service 的实际执行。

CLI 的 commit --wait 最多等待 30 分钟,拒绝、过期与超时都有明确结果。等待不能替用户批准,超时也不应改成新候选自动继续。原任务的身份和用户看到的内容要始终能对应。

审核过了,提交时为什么还要再检查

设想版本 40 上准备了候选,用户查看后批准;等待提交时,网页已经把原文改到版本 41。如果继续把旧候选覆盖上去,用户批准的是旧上下文中的修改,实际影响却发生在另一个基础上。

当前提交核对 baseRevision 与当前整库 revision。变化以后返回 VERSION_CONFLICT,重新读取并准备新候选。它没有通过"拿最新版本号包装旧正文"绕开检查,也不会把旧批准搬到内容已经变化的新候选上。

权限同样可能变化。prepare、读取差异、审核和 commit 都重新检查当前授权。候选保存的依赖还包括复制来源与受影响后代,不能只看最终输出页面;用户失去来源权限后,差异里的 before 也不应该继续可见。

提交会重新验证操作,再把固定差异中的 after 作为实际候选内容使用。这样可以复查结构与权限,同时避免重新计算复制 ID 或内容导致"审的是 A,写的是 B"。规则验证和候选不变需要同时成立。

幂等键、行锁与事务,怎么配合

prepare 的幂等键限定在连接内,服务端保存请求摘要。相同键与相同请求返回原候选,键相同但请求不同返回 IDEMPOTENCY_CONFLICT。网络响应丢失时,客户端能找回原来的 changeId,而不制造第二份待审核任务。

commit 对修改集行加锁。一个请求已经 committed,重复请求返回原 result,不再执行一遍操作。正文、历史、页面索引、写入计数和修改集状态在同一事务里提交,失败整体回滚。

这保证的是已完成数据库事务中的副作用不会因重复提交再次应用,不是抽象地承诺任意网络系统都有"exactly once"。客户端仍可能不知道响应有没有到,幂等结果让它能够消除这段不确定。

只在本地用一个 isSubmitting 布尔值,没有办法约束另一台机器或另一个 Node 进程。真正负责的边界仍是服务端状态与数据库事务;界面禁用按钮只是减少误触。

恢复动作,要和错误的含义对应

VERSION_CONFLICT 需要读取新内容并合并意图,生成新候选;BLOCK_CONFLICT 需要重新定位块;REVIEW_REQUIRED 需要展示原审核链接;SCOPE_DENIED 则应停止访问,不能换个根 ID 绕过。

候选过期意味着当前批准上下文不能继续使用,不是把时间戳改大就能恢复。连接撤销同样需要重新由用户授权,CLI 不应该因为"任务还没做完"就扩大权限。

对 AI 来说,最重要的指南不是只有成功示例,而是失败后如何保留任务材料与用户控制。明确错误码、完整链接和稳定候选能让宿主把需要用户处理的步骤讲清楚,而不是反复试命令。

怎样验证用户看到的,真的就是最终提交的

可以准备一个带复制、移动和块修改的候选,记录页面和块 ID,再走审核提交,确认最终内容与差异一致。用户批准后先让另一请求改 revision,旧 commit 应被拒绝,正文和额度都不应部分变化。

权限测试则在 prepare 后撤销成员,分别请求差异、审核和提交,都应按当前权限阻止。刷新测试用同一个旧 refresh 重放,确认撤销状态真的提交;两个正常 CLI 进程并发刷新,应通过本地锁复用轮换结果。

此外,检查关闭外部写入、连接暂停、到期与跨范围移动。每个场景都要观察数据库与可见内容,不能仅凭 UI 按钮灰了就判断权限成立。

可以从流萤 AI 连接器选一个占位专题,先用只读或审核模式体验自己的 AI 如何参与资料整理。自动化越接近真实写入,越需要让权限、候选和最终结果保持可解释的关系。下一篇把视线移回文档内部,看看 Mermaid 与 HTML 内容块怎样既可编辑又可预览。

相关推荐
浪遏19 分钟前
05|网页、分享页与 CLI,为什么必须走同一条保存路径
架构
浪遏22 分钟前
02|功能越来越多,代码怎样还能按业务找到
架构
阿卡好可爱4 小时前
Lua 热更新不生效:改完文件线上仍是旧逻辑,package.loaded 与 upvalue 重绑定详解
数据结构·分布式·后端·架构·lua
数字孪生视频孪生5 小时前
在线重构,实时同步:视频动态三维重建打造 “常更常新” 的数字底座
大数据·人工智能·重构·架构·空间计算
Decay__5 小时前
01_SQLServer 体系架构全解 —— 实例进程、关系引擎、SQLOS 与事务日志深入
运维·数据库·sql·mysql·架构
mldong6 小时前
同一个节点,同一个人,办两次:jeeflow 的已办撤回
后端·架构
hpoenixf6 小时前
Deep Research:从资源升级到研究交付,我们怎么实现,又测出了什么?
架构
hpoenixf6 小时前
失败已经确定,为什么还返回不了?清理与结果结算的责任边界
架构
Erishen18 小时前
Lume A2A 实战:让 Lume 应用接入分布式 Agent 协作网络
架构·开源·agent