Change rules and verification¶
Effect service execution¶
服务工作流使用目录固定的 effect 4.0.0 包。在修改这些工作流时,请阅读已安装包中的源码和参考资料。Electron 主进程、账户 Worker、站点路由 Worker、Signal 服务和共享团队客户端使用相同版本。框架路由、IPC 校验、UI 渲染和事件投递保持其原生接口。
Effect 组合领域操作和类型化失败。Promise 接口保留在框架和 SDK 边界。这些接口使用 Effect.result 解包预期失败;缺陷和中断仍可能 reject。现有的公共错误映射和密钥脱敏仍由每个边界负责。
长期存在的服务图拥有其托管运行时,并在关闭时将其释放。Provider 发现机制在桌面端 teardown 注册表中注册释放操作。每个远程对等方拥有其运行时,直到对等方释放。Signal 拥有一个进程运行时。Worker 服务依赖属于某个请求或调用;它们不得在全局运行时中保留请求绑定。响应流和 waitUntil 任务保留其框架生命周期。
构造函数注入的服务每个异步方法暴露一个 Effect 操作。服务端口接受 Effects,调用者直接组合它们。没有成对的 Promise 外观。延迟值共享待处理结果;信号量保持操作顺序;拥有的作用域保留后台纤程直到清理。同步 SQLite 事务保持同步。必须在回滚或关闭前完成的变更使用显式中断边界。
渲染器和移动应用工作流不在本次迁移范围内。它们的原生回调执行共享客户端 Effects 并保持现有的 UI 和事件行为。Electron 窗口管理、HTTP 路由、IPC 处理器、SDK 回调注册以及启动/teardown 钩子仍然是框架代码。它们调用 Effect 服务边界并等待资源释放。
隔离的代理数据库主机从已安装的包中导入 Effect。它保留其独立的进程、SQL 授权器和冻结的行协议。管理器仍然可以终止阻塞在同步 SQLite 工作中的进程。桌面包随主机一起解包 Effect,包验证检查该依赖存在于其中。
资源属于获取它们的操作。对文件句柄、流、临时文件、许可和待处理回调使用 finalizer。仅将取消转发给支持它的适配器。取消不会使数据库写入或远程变更安全地重放。将领域重试和恢复规则保留在现有的所有者处。
Change rules¶
- 只有当类型跨越进程或应用边界时,才将其放入
packages/contracts。 - 当所有消费者必须使用相同的限制或语法时,将验证规则放在契约旁边。
- 将 provider 特定的负载保留在 provider 适配器内部。在摄入时进行转换。
- 将数据库架构更改保留在架构模块中,并添加迁移测试。
- 保持 Electron 入口点小巧。新功能使用服务或专注的 IPC 输入模块。
- 不要添加第二个 linter 或 formatter。Biome 及其仓库拥有的 anti-slop 插件是唯一的仓库 lint 和格式工具。
- 将渲染器状态放在该领域特性目录内的领域上下文模块中,
src/renderer/src/features/<domain>/<domain>-context.tsx,位于读取它的逻辑、视图和测试旁边,并按生命周期放置:属于一个团队服务器的状态放在app-providers.tsx中的键控作用域内,其他所有状态放在其上方。服务器切换会丢弃并重建该作用域,因此它是唯一的按服务器 teardown——位于该边界错误一侧的信号要么在不应存活时存活于切换中,要么在不应死亡时死亡于切换中,任何 setter 列表都无法修复。上下文只能向下通过use*()访问另一个上下文,遵循app-providers.tsx的嵌套顺序,或通过 provider prop;写入多个领域的命令位于叶子上下文或挂载在所有领域之下的桥接组件中。视图、上下文和存储不读取window.openbot。它们调用<domain>-port.ts模块,该模块列出其领域发出的桥接调用,或调用者可替换的窄 API 对象(conversation-runtime.ts、provider-key-api.ts)。循环被noImportCycles拒绝,因此向上的边必须是import type。 优先选择上下文内每个关注点一个 store,而不是每个字段一个 signal:并行的 signal 行才是让一个屏幕同时处于加载中、已加载和出错状态的原因。 - 从需要它们的最小组件读取这些上下文。窗格调用其渲染领域的
use*(),别无其他;WorkspaceShell只读取决定哪个窗格渲染的内容,并在两个组件本会重复派生同一值时将值作为 prop 传递。一个组装另一个组件 props 的组件正是上次 god controller 重新长出来的方式。 - 不要在没有移除条件和该条件的测试时添加临时兼容路径。已发布的 Team API 协议适配器默认是永久性的,并遵循以下策略。
- 通过
@openbot/logging(ts-logLogger)记录日志,绝不使用裸的console.*——Biome 的noConsole在src、scripts和packages中强制执行此规则。biome.json中noConsole覆盖排除的 remote-desktop 构建配方脚本除外。.github/workflows/remote-desktop-runtime.yml中的Require a recipe version bump步骤列出了它们:对它们的任何编辑,无论外观性还是非外观性,都强制remoteDesktop.recipeVersion提升并触发完整原生运行时重建,因此它们的日志记录在配方因真实原因变更之前保持冻结。每一行都带时间戳、前缀和密钥脱敏,且脱敏覆盖作为单个字符串传递的序列化负载,而不仅仅是结构化参数。模式无法找到没有标签的密钥,例如错误文本中的 token,因此加载密钥的 store 将值传递给registerSecretValue,之后的每一行都掩码该确切值。provider 凭据存储和 MCP 存储(针对看起来像密钥的 header 和环境变量名)都这样做。默认写入info及以上级别;OPENBOT_LOG_LEVEL更改阈值。机器可读的 stdout(管道 JSON、标签、harness URL)使用带// Machine-readable:注释的process.stdout.write。开发自动化(scripts/dev-automation、bun run dev:automation)通过其远程调试 CDP 端口驱动已在运行的 dev 应用,从不启动第二个实例、播种或重置 dev 配置文件。因为多个 worktree 并排运行 dev,每个实例将其 worktree、配置文件、渲染器端口和调试端口发布到 dev 运行时目录中的注册表(src/main/development-runtime-directory.ts中的devRuntimeDirectory()、scripts/dev-automation/instance-registry.ts)。该目录不遵循TMPDIR,因为TMPDIR可能因 shell 而异,所以每个命令都读取栈写入的注册表。dev 主机将其测试客户端连接交给测试客户端的文件也在那里,每个栈一个。自动化解析其运行的 worktree 的记录,在驱动页面之前验证渲染器端口和window.openbotpreload 桥,并拒绝对仅推断出的实例执行click或type。其旁边的第二个注册表(scripts/dev-automation/stack-registry.ts)记录整个 dev 栈持有的每个端口和 pid,包括 Storybook,而scripts/dev-automation/port-allocation.ts将读取、选择和发布串行化在一个机器范围锁后面:探测端口并在几秒后绑定它是一个检查后使用的模式,因此两个同时启动的 runner 过去都会赢得 5173 和无后缀的OpenBot Dev配置文件。该锁的所有权是代际而非路径:获取它意味着通过独占创建创建下一个编号文件,因此谁拥有它由内核原子的步骤决定,而不是由删除决定。一旦锁路径存在,它就一直存在,没有任何东西释放它:释放通过一次rename到同一路径将内容替换为 released 标记,而持有者已死亡的锁在其所在位置被取代。分配器请求它看到的最高代际加一,因此任何释放路径的操作——删除它或将其重命名到一边——都让该编号在已针对其制定计划的同时再次被分配出去。仍在运行的持有者永远不会被越过,无论它持有多久。读取注册表是相同的形式并遵循相同的规则:进程已消失的记录在每次读取中都被过滤掉,因此其端口从那一刻起是空闲的,但判定它的读取者永远不会删除该文件——管理器可能在此期间用分离的子进程重新发布了它,而更新分支上的兄弟 worktree 写入的记录是此 checkout 完全无法解析的。bun run dev:forget才是移除记录的方式,因为开发人员请求它是一个决定而非猜测。它丢弃其忘记的栈的实例记录,而 pid 不是身份:实例记录存储在其 pid 下,因此回收该 pid 的应用会覆盖拥有它的应用的记录。每条记录都标注日期,因此 worktree 和记录的启动时间决定它是谁的,而活动实例永远不会是已死亡栈的。bun run dev:status和bun run dev:stop(scripts/dev-stack.ts)读取这些 pid 而不是匹配进程名称,这正是让停止一个 worktree 的栈不影响其他栈的原因。每个 dev 窗口保持可访问:pages列出目标,--page=<target-id|url-substring>驱动其中任何一个,因此应用窗口是默认而非限制。页面 URL 只能通过describeTarget到达诊断和快照文档。
SQLite 迁移历史从冻结的版本 8 兼容基线开始。保持基线模式不变,按数字顺序追加所有后续迁移,并更新用于新数据库的独立最新模式。切勿删除或重写可能已发布的迁移。
启动时,聊天恢复功能会读取每个会话线程中所有已保存的提供商会话,包括升级或提供商变更后产生的非活动会话。它使用每个会话的提供商,将消息合并到 SQLite 中,而不会激活旧会话。读取失败时会报告错误,保留已保存的消息,并可在提供商连接或应用重启后重试。
必需验证¶
运行范围最小的相关测试,并对更改的文件执行 lint。预提交钩子会运行 check:staged、check:ui,以及对暂存文件所影响的每个项目执行类型检查;CI 运行其余检查。有关本地规则,请参阅 AGENTS.md, Checks;有关每个 CI 任务覆盖的内容,请参阅 检查设计说明。
Storybook CI 任务使用 OPENBOT_STORYBOOK_CHECK=true 构建所有 stories。这会跳过 Solid 的自动属性文档分析。该任务检查编译且不发布其输出。本地 Storybook 保留此分析。两条路径都使用同一个 Solid 编译器插件。
浏览器冒烟检查还支持 --scenario=wait-deadlines。这些检查会等待标签页的操作队列清空后再测量新的截止时间。超时的调用可能在 CDP 命令仍需完成时就返回,而该清理过程不在下一次操作的截止时间范围内。
每个 TypeScript 项目都会在其配置旁边写入自身被忽略的 .tsbuildinfo 缓存。每个工作树启动时都没有缓存。首次检查会创建这些文件;后续检查会重用它们并检查已变更的输入。删除缓存文件可强制进行全新检查。新的 CI 检出同样从无缓存开始,除非 CI 任务恢复了缓存。聚合命令会并行处理所有项目。项目作用域和编译器 worker 设置保持不变。
对打包、原生模块或 Electron 安全性的更改,还要求运行相应的 macOS 和 Windows 包验证命令。实时提供商和团队冒烟测试使用隔离的临时数据,并且由于可能需要本地凭据,因此需要手动运行。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 openbo/openbot