搜索/构建从多个来源收集活动事件、并在多个宿主界面中显示的应用

构建从多个来源收集活动事件、并在多个宿主界面中显示的应用

用于构建从多种工具收集事件、并同时在编辑器扩展和独立服务器中显示的应用。它让分层依赖保持单向,把每个来源都转成同一种事件类型,接入新来源时无需改动其他代码,并安全地保存状态。

要使用这个AI规则
## 让依赖保持单向,各宿主界面之间互不导入 让依赖始终按照命名分层单向流动。`core` 层只存放协议与接口定义,没有任何运行时副作用。运行时层只依赖 `core`。每个宿主界面——例如一个编辑器扩展适配器和一个独立的命令行服务器——都依赖 `core` 加运行时,但界面之间彼此绝不互相引入。明确声明允许的依赖方向并强制执行,因为哪怕只有一处跨边界的引用,也会悄悄把两个本应各自独立、可替换的界面耦合在一起。 ### 为什么这条规则站得住脚 一个不含依赖的契约层,能让每个界面、每个集成都针对同一套类型编译,而不必引入宿主专属的代码。新界面只是组合共享的组件,绝不会横向伸手去动另一个界面的内部实现。一旦你无法在不修改现有界面的前提下添加新界面,说明分层已经被打破了。 ## 从一份协议文件生成类型化绑定,并在 CI 中检查差异 让一份机器可读的契约成为线路协议的唯一真相来源,再由它生成带类型的绑定代码,而不是手写。把生成出来的文件当作只读文件:加上自动生成的标注,绝不手动修改。在持续集成中加入一个步骤,重新生成绑定代码,一旦出现任何非空差异就让构建失败,这样契约和代码就永远不会出现偏差。 ### 锁定生成器版本 把契约版本锁定在代码生成器实际支持的范围内。如果 schema 版本比生成器能处理的更新,生成器可能会在没有明显报错的情况下,悄悄把生成的类型降级为一个宽松的万能类型,等于抹掉了所有类型安全保障。记录清楚为什么锁定这个版本,只有在工具链跟上之后才重新考虑升级。 ## 在单一枢纽中把每个来源转成同一种事件类型 让所有活动都通过单一枢纽聚合。不管来源是什么——通过 HTTP 推送的事件,还是从日志和记录文件中解析出的行——在进入运行时之前,都要先把它们统一规范化为同一种规范事件类型。下游代码只根据事件的类型(kind)来分发处理,绝不依赖特定来源的动作名或工具名,这样新增一个来源就不会波及运行时或界面。 ### 让事件集合保持精简且封闭 定义一个精简且封闭的事件类型集合:会话开始与结束、工作开始与结束、子任务生命周期、进度、以及权限请求。每条摄取路径唯一的职责,就是产出这些规范事件。枢纽之后的一切代码都与来源无关,即便有新来源接入也能保持稳定。 ## 把通信细节藏在一个传输接口之后 把底层通信藏在一个接口面很小的单一传输接口之后:发送消息、订阅收到的消息、暴露一个就绪 promise 和连接状态、以及释放资源。每种环境各实现一次——比如嵌入式界面用进程内消息通道,独立界面用会自动重连的 socket——而不同实现之间的切换只放在唯一一个工厂函数里完成。下游的一切代码都只依赖这个接口,永远不知道实际用的是哪种传输方式。 ### 韧性设计放在具体实现里 给基于网络的实现加上指数退避重试机制,以及一个在重连后会自动清空的发送队列。进程内的实现则可以在整个生命周期里都报告为已连接。无论用的是哪种实现,调用方观察到的行为都完全一致,因此任何功能代码里都不会出现针对特定传输方式的分支判断。 ## 把每个集成作为提供者接入,不改动其他代码 把每一个外部集成都建模为实现同一个接口的 provider:把原始事件规范化为标准事件、安装/卸载/查询自己的钩子、格式化状态、以及声明哪些动作名可以豁免特定规则。把诸如轮询兜底、扩展的多方参与模式这类可选能力设计成可选方法,这样简单的集成就能保持精简。 ### 检验边界是否干净的标准 新增一个集成,理应只是新增一个实现该接口的子目录,对运行时、界面或任何现有 provider 都零改动。如果新增一个来源却不得不去改别处的代码,说明这个边界正在渗漏,需要在更多集成堆到这条裂缝上之前尽快收紧。 ## 两个宿主界面共享同一个运行时核心和同一个存储 让两个宿主界面都组合使用同一个运行时核心;唯一有差异的是持久化命名空间、传输方式,以及任何宿主专属的适配器。所有状态变更都要经过同一个 store,由它拥有数据、发出带类型的变更事件,并作为唯一的真相来源。让一个广播层订阅这些事件,并在那里统一把它们转换为要发出去的协议消息——除此之外没有任何模块可以直接调用传输方法。 ### 让宿主接缝保持窄小 在运行时与宿主之间,只暴露唯一一个回调接口,用来处理少数真正与宿主相关的事情:启动任务、打开文件夹、导入导出。正是这道接缝保持窄小,才让两个界面能够原样共享同一个核心,因此只有在确实存在宿主差异的必要时,才去拓宽它。 ## 优先使用推送信号,并保留文件轮询作为后备 当存在快速可靠的信号时,优先使用它,同时保留一个较慢的兜底方案,用于该信号缺失的情况。基于 HTTP 的事件回调推送通道,能提供即时且有序的信号;当推送未安装时,轮询文件就是兜底方案。要给两者加上门控,避免重复计数:用一个按条目记录的已送达标记,再加一个全局开关,共同决定兜底方案的计时器是否运行。 ### 把「读取内容」和「推导状态」分开 有些数据可能只能从兜底路径获得——比如细粒度的进度、状态文本——所以即使在推送模式下,也要让轮询继续为这些数据运行;但一旦确认某条目的推送已经生效,就要抑制兜底方案里那些用于推导状态的计时器(比如权限提示、空闲检测)。把「读取内容」和「推导状态」这两件事分开处理,正是让两种模式能够并存而不冲突的关键。 ## 按宿主界面分开保存状态,确认副本后再清除旧数据 给持久化状态按界面分别设置命名空间,这样并行运行的两个界面就不会互相覆盖对方的数据。写文件要原子化——先写入临时文件,再重命名——并标记出自己写入的内容,避免跨进程的监听器把它误判为外部变更而重新读取。让文件监听搭配轮询作为备份,因为原生的文件监听在部分平台上并不可靠。 ### 清除之前先验证 迁移旧状态时,要先验证再清除:先写入新位置,再读回来核对,确认无误之后才删除旧的键。只要还有内容尚未迁移完成,就要显示一条不阻塞操作的警告,而不是想当然地认为已经成功。在磁盘上的副本被确认之前,绝不能删除作为真相来源的原始数据。 ## 把常量集中在一处,并用 lint 规则强制执行 把每一个魔法数字和字符串——时间常量、标识符、协议字面量、样式 token——都集中放到少数几个常量模块里,每一层对应一个,绝不直接写死在代码里。直接写死的值会逐渐产生偏差、出现重复,还会隐藏后续重构时容易踩到的耦合关系。 ### 用 lint 强制执行,而不是靠自觉 用设置为错误级别的自定义 lint 规则来支撑这条策略,让违规直接阻止合并:禁止在 token 文件之外直接写死颜色和样式字面量、要求使用共享的阴影与间距 token、要求引用共享的字体定义。由机器强制执行的规则,不会因为贡献者更替而失效;只写在文档里的约定则不然。 ## 分层测试,对经常变动的代码使用端到端测试 采用分层测试策略,每一层用对应合适的工具。快速的单元测试和集成测试覆盖纯逻辑部分:状态变更、事件路由、解析、迁移、安装过程的原子性。端到端测试则驱动真实的界面,断言用户可见的结果,而不是内部实现细节。 ### 对开放贡献而言,优先选择稳定的接缝 对于贡献者经常改动的代码,比如界面内部实现,优先使用能钉住用户可见行为的端到端测试,而不是绑定内部实现的单元测试,这样重构时贡献者就不必在完成功能的同时还要重写内部测试。让每个测试都保持隔离——各自拥有独立的临时目录、工作区和数据目录——避免状态在测试之间泄漏;并且要像检查生成代码的偏差一样,在持续集成中检查任何自动生成的测试清单是否出现偏差。 ## 跳过坏输入而不是崩溃,并给每行日志加上标签 优雅降级:捕获并记录错误,但绝不能让错误导致宿主崩溃。遇到格式错误的输入行,直接静默跳过,而不是中断整条数据流。当资源或素材缺失时,记录一条警告,用空值或兜底值继续执行,而不是让整个操作失败。 ### 让日志便于检索 给每一行日志都加上一个用方括号标注子系统名称的上下文标签,这样即便是扁平的控制台输出流也依然可以检索。要有意识地判断自己是否需要集中式的错误上报或遥测;如果决定不需要,就把这个决定明确写下来,并保持本地日志格式一致,避免以后的阅读者猜来猜去。 ## 安全读取不断追加的日志文件,并为异步测试留足时间 这些规则说起来轻巧,一旦忘记再重新学一遍,代价却很高。 ### 读取只追加写入的文件 给任何原生文件监听器都搭配一个短间隔的周期性轮询,并把尚未写完的部分行保留在缓冲区里跨多次读取拼接,因为读取时可能正好碰上文件被写到一半。会话被清空或重置时,往往会开始写一个全新的文件,而旧文件只是停止更新,因此要通过内容变化来判断文件是否已经切换,而不是想当然地认为同一个句柄会一直增长下去。 ### 测试中的时间预算 当你要在一条异步流程的末尾断言结果时,要为每一个阶段的延迟都留出预算——触发信号与推导信号之间的间隔、计时器延迟,以及进程间通信加上渲染带来的额外损耗。如果超时时间比这些预算加起来的总和还短,那这个测试从设计上就注定是不稳定的。 ### 渲染与批处理 把高频更新的动画状态放在命令式的 store 里,而不是放进框架自身的状态管理中;并且让「工作完成」这类状态转换稍微延迟一下,这样界面的批处理机制就不会把短暂出现的活动状态从用户眼前隐藏掉。