乐于分享
好东西不私藏

构建 AI 桌面客户端—三端统一的应用架构

构建 AI 桌面客户端—三端统一的应用架构
《如何从零构建 7×24 小时 AI Agent》
第二章:地基 —— 三端统一的应用架构

建筑的地基决定了上面能盖多高。软件也一样。本章讲五个地基层的设计思想——运行时 Adapter、声明式能力边界、分层与依赖方向、预算约束驱动的性能架构、命名空间迁移——它们不是 AI 相关的技术,但没有它们,后面所有 AI 能力都无处安放。


2.1 一套代码三端运行:运行时 Adapter

问题:多一个平台,多一倍麻烦

假设你写了一个聊天应用。React 前端,Electron 桌面壳。跑起来了,产品说:出个手机版。

最直觉的做法是什么?再开一个仓库,React Native 重写一套 UI。界面长得差不多,网络请求抄一遍,两三周出个能用的版本。

第一个月没问题。第二个月,桌面版加了"消息已读回执"。你在移动版也要加——两边各写一遍,各调一遍。第三个月,回执逻辑改了,桌面版改完,移动版忘了。用户投诉:手机上已读状态不对。

这不是 bug,是架构问题。

两套代码意味着每个功能的维护成本不是 2 倍——写代码可能只多一倍,但"记得去改第二个地方"的认知负担是指数级的。因为遗漏不会立刻报错。编译通过,测试通过,运行也不崩溃,只是行为不一致。这类 bug 往往在用户投诉后才发现,定位时间远超修复时间。

三个平台就更不用说了。三个仓库、三条构建管线、三倍的发版流程。每个功能改三遍、调三遍、测三遍。如果你是一个五人团队,这个成本也许能扛。如果你是一个人——不管你多勤奋,乘法会杀死你。

那怎么办?能不能只写一份代码,让它在不同平台上跑?

矛盾:一份代码,但底层不同

问题在于,不同平台的底层通信机制不一样。

Electron 桌面应用的前端(渲染进程)和后端(主进程)之间用 IPC(进程间通信)——这是操作系统级的通道,快,但只在本机有效。手机 App 和 Web 浏览器没有这个通道,只能用 HTTP 请求——通过网络发到服务器。

这是一个根本性的差异。你的 React 组件调用"获取对话列表"这个操作,在桌面上应该走 IPC,在手机上应该走 HTTP。如果你把这个判断写在每个组件里——

// 不要这样写functionConversationList() {if (isDesktop) {    data = ipc.invoke('getConversations')  } else {    data = fetch('/api/conversations')  }}

每个组件都要写一遍 if。50 个 API 调用点,50 个 if。改一个通信逻辑,改 50 个地方。这比两套代码还糟——你把平台差异像撒胡椒面一样撒遍了整个项目。

方案:Adapter 模式

1994 年,GoF 在《设计模式》里描述了一个叫 Adapter 的模式。原始定义比较抽象,但核心思想很简单:

把差异关在一个房间里,不让它跑出来。

具体做法是在业务层和平台层之间插入一个薄层——适配层。业务层只跟适配层的统一接口对话,不知道底层是 IPC 还是 HTTP。适配层内部做判断和转换,把请求转发到正确的通道。

业务层(React 组件)    ↓ 调用统一接口适配层(transport + api)    ↓ 内部判断平台IPC / HTTP / WebSocket

这个模式的价值不在于它多聪明——它很朴素。价值在于它划了一条线:线以上的代码永远不需要知道自己跑在什么平台上。新增一个平台,只改线以下。新增一个功能,线以下不用动。两个方向的变化互不干扰。

这就是软件设计里反复出现的一个原则:把变化的原因隔离开。平台会变(今天三个,明天可能四个),功能也会变(每周加新 API)。如果两种变化搅在一起,任何一个改动都可能波及全局。Adapter 把它们分开了。

实战:470 行撑起三个平台

回到 Halo。它需要跑在三个环境上:Electron 桌面、Capacitor 移动壳(iOS/Android)、任意浏览器(远程访问)。这三个环境的检测逻辑集中在一个文件 transport.tssrc/renderer/api/transport.ts,470 行),判断条件各不相同:

环境
怎么检测
走什么通道
Electron 桌面
window
 上有 halo 对象(preload 注入的)
IPC
Capacitor 移动
编译时注入 __CAPACITOR__ 标志
HTTP 到用户配置的服务器地址
浏览器远程
以上都不是
HTTP 到当前页面的域名

然后,api/index.ts(约 2000 行,100 多个方法)里每个 API 都是同一个写法:

// src/renderer/api/index.tslistConversations: async (spaceId: string) => {if (isElectron()) returnwindow.halo.listConversations(spaceId)return httpRequest('GET'`/api/spaces/${spaceId}/conversations`)},

上层 React 组件调 api.listConversations(spaceId),不知道也不需要知道底层走的是什么。桌面上走 IPC,手机和浏览器上走 HTTP。100 多个方法,全部同一个模式。

这就是 Adapter。差异被关在 transport.ts 和 api/index.ts 这两个文件里,项目里其他几万行 React 代码完全不知道平台的存在。

一个关键抉择:运行时检测 vs 编译时分支

Adapter 模式确定了之后,还有一个选择:什么时候判断平台?

编译时分支是一种做法。C/C++ 里的 #ifdef 就是这个思路——编译时决定走哪条路,打出不同的产物。桌面版编译出 bundle-desktop.js,移动版编译出 bundle-mobile.js,各走各的。

好处是运行时零开销,坏处是三份产物。三条构建管线、三个 CI 配置、三个版本号。构建配置是 Electron 项目里最脆弱的部分——改一个 Vite 配置就可能炸掉整条线。三份配置意味着三倍的炸裂概率。

运行时检测是另一种做法。只编译一份代码,运行时通过 if 判断当前环境。多了一个 if 的开销(可以忽略),但只维护一份产物、一条构建管线、一个版本号。

Halo 选了运行时检测。原因很具体:一个人加 AI 的团队,维护三条构建管线是不可承受的。运行时多一个 if,换来的是只需要管一份代码——这个交换在 Halo 的约束下是不需要犹豫的。

但这个选择不是在所有场景下都成立。如果三个平台的 UI 差异很大(比如手表和桌面),共享代码的收益就小了,编译分支可能更合适。如果团队有专门的构建工程师,三条管线的成本可以被分摊。Adapter 的"运行时 vs 编译时"不是对错问题,是团队规模和平台差异程度决定的取舍。

代价:差异处理集中在适配层

一套代码跑三个平台,不意味着三个平台的行为完全一样。差异是真实存在的——它们只是被集中管理了,而不是消失了。

举两个例子。移动端用户需要手动填入桌面端的 IP 地址才能连接。但"添加服务器"这个流程有一个微妙需求:用户填了地址,系统要先测试连通性,通了才持久化;取消则丢弃。transport 层用一个临时变量和一个持久变量解决这个问题——设计上很轻,但不处理的话,用户取消连接后会莫名连到一个错误地址。

再比如令牌过期。同样是 401 错误,移动端没有服务器渲染的登录页,需要触发一个 DOM 事件把用户导航回连接页面;浏览器端有服务器兜底,直接刷新让服务器重定向到登录页。两种行为都在同一个 httpRequest 函数里按环境判断,上层调用者不需要知道。

这种细节是 Adapter 模式最容易出问题的地方。宏观架构画得再清楚,一个 401 处理不对,用户就卡在白屏上。模式给你的是结构——怎么在结构里处理每一个边界条件,仍然是工程活。

为什么这是本书最值得记住的架构决策

渲染层共享带来的最大价值不是省了多少代码——470 行 transport 加 2000 行 API,不到 2500 行撑起三端通信。真正的价值是每个功能只写一次

新增一个 API,改适配层的两个文件加主进程 handler,三个文件。如果是三套代码,要在三个仓库各做一遍——写三次、调三次、发三次版。对一个人加 AI 的团队来说,这种乘法是致命的。约束逼出了更简单的架构。

而且这个决策有一个当时没预料到的长远回报:Halo 后来做远程访问功能时,因为渲染层已经和传输层解耦,加 HTTP 支持几乎是零成本——transport 层加一个 else 分支就行。如果当初选了编译分支,远程访问意味着新增第三条构建管线和第三个仓库。

好的架构决策,当时看是"够用就好",回头看是"提前关闭了一类问题"。


2.2 IPC 安全桥:声明式能力边界

问题:谁能调什么

假设你在做一个桌面应用,前端是 Web 技术(HTML/CSS/JS),后端是 Node.js。前端想读一个文件,怎么办?直接调 fs.readFile()

如果你用的是 Electron,默认配置下前端(渲染进程)确实可以直接调用 Node.js 的所有 API——文件系统、网络、子进程,什么都行。开发起来非常顺畅。

但想一下:如果你的应用会加载外部网页呢?一个 AI 浏览器功能,会在渲染进程里打开任意 URL。如果渲染进程能直接调 Node.js API,那任何一个被注入了恶意脚本的网页,都能通过你的应用读取用户的文件系统。一个 XSS 漏洞就能变成完整的系统入侵。

这不是假设性的风险。业界太多 Electron 应用把 nodeIntegration 设成 true(允许渲染进程直接调 Node.js),出过安全事故。Electron 官方文档在安全章节的第一条建议就是:关掉它。

关掉之后,渲染进程和 Node.js 之间就有了一道墙。渲染进程不能直接调用任何 Node.js API。那合法的操作怎么办?——比如"获取对话列表"这种你自己的业务逻辑?

你需要在墙上开洞。问题是:怎么开,在哪开,谁来管?

两种开洞方式

方式一:在每个调用点开。 渲染进程要调什么,就在那个地方单独处理权限。组件 A 需要读文件,就在组件 A 里写权限检查;组件 B 需要发消息,就在组件 B 里写另一个检查。

问题是:权限检查散落在整个项目里。你有 100 个组件,就有 100 个潜在的权限漏洞点。新来的开发者(或 AI)加了一个组件,忘了写权限检查?没有任何机制能提醒你。更糟糕的是,"漏了一个检查"不会报错——功能正常工作,安全缺口静默存在。

方式二:在一个地方声明所有能力。 写一份清单,列出渲染进程能做的所有事情。不在清单上的,做不了。清单就是能力的边界。

Electron 提供了实现第二种方式的机制:contextBridge。它要求你在一个叫 preload 的脚本里,显式声明渲染进程能调用的每一个方法,然后把它们注入到 window 对象上。没声明的方法,渲染进程连函数名都看不到。

这就是声明式能力边界——用一份白名单定义"能做什么",而不是用散落各处的黑名单防止"不能做什么"。安全领域的经验是:白名单几乎总是比黑名单安全。黑名单的问题是你得预见所有攻击方式,漏一个就输了;白名单只需要确保清单是完整的。

实战:954 行白名单

在 Halo 里,src/preload/index.ts 有 954 行,声明了大约 215 个方法。这个文件的内容极其机械:

// src/preload/index.tsconst api: HaloAPI = {  listConversations: (spaceId) =>    ipcRenderer.invoke('conversation:list', spaceId),  sendMessage: (request) =>    ipcRenderer.invoke('agent:send-message', request),// ... 还有两百多个}contextBridge.exposeInMainWorld('halo', api)

每一行做的事情都一样:把一个 ipcRenderer.invoke 调用包一层,暴露到 window.halo 上。没有业务逻辑,没有条件判断,纯声明。

这 954 行就是渲染进程的完整能力边界。如果你想知道渲染进程能做什么、不能做什么,读这一个文件就够了。不在这个文件里的操作,渲染进程做不到——不是"不应该做",是"做不到",API 入口根本不存在。

代价:N 文件同步

声明式能力边界的设计很清晰,但它带来了一个工程上的痛苦:每加一个功能,要同步改多个文件。

新增一个从渲染进程到主进程的调用,需要改三个文件:

1. src/main/ipc/<module>.ts    — 主进程 handler(实际执行逻辑)2. src/preload/index.ts         — 安全桥声明(白名单注册)3. src/renderer/api/index.ts    — 渲染层调用(业务层入口)

如果这个功能还要支持远程访问(手机或浏览器通过 HTTP 调用),还需要改第四个文件:HTTP 路由。

少改一处会怎样?编译不会报错,TypeScript 不会警告,运行时也不崩溃。它只是静默失效——你点一个按钮,什么都没发生,没有错误日志,没有异常弹窗。第一次碰到这种 bug,定位花了 40 分钟,最后发现只是 preload 里漏了一行声明。

这是一个典型的"多文件协同一致性"问题。解决思路不止一条,而且它们构成一个从轻到重的光谱:

Level 1:规范约束。 把"三文件同步"写成规范,放在团队成员(或 AI)每次写代码时能看到的地方。好处是零基础设施成本;代价是依赖纪律,漏了不报错。

Level 2:共享接口 + 编译器拦截。 VS Code 用的就是这种方案。它在公共层(common/)定义一个 TypeScript 接口(比如 IMyService),服务端和客户端各写一个适配器——服务端的 IServerChannel 把 IPC 调用映射到真实方法,客户端的 Proxy 类把方法调用转发到 IPC 通道。两端都被同一个接口约束。改了方法签名但忘了更新另一端?TypeScript 编译直接报错。代价是每个服务多一层抽象(Channel + Proxy),样板代码更多;收益是**从"靠纪律发现遗漏"升级到"靠编译器发现遗漏"**。而且同一个接口可以透明切换底层传输——Electron IPC、Node Socket、浏览器 MessagePort 都走同一套代码,这就是 VS Code 的 Remote 架构能那么干净的原因之一。

Level 3:代码生成。 写一个生成器,从一份定义文件自动生成 handler、preload 声明、渲染层调用。好处是彻底消除遗漏和样板代码;代价是生成器本身要维护,生成时机要管理。gRPC 的 .proto → 生成客户端/服务端代码就是这个思路。

在 Halo 里选了 Level 1——新增 IPC 的频率大约一周一两个,215 个方法的规模还不到值得引入 Channel/Proxy 抽象的地步。规范写在 CLAUDE.md(项目指令文件,AI 每次启动时自动读取)里:"新增 IPC 必须同步修改三个文件"。AI 看见规范就会遵守,漏改的频率从大概三次里错一次降到了几乎不再犯。

如果 Halo 的 IPC 方法数量增长到 500+ 或者有多人协作,Level 2 就值得迁移了——用共享接口换掉现在三个文件各自独立声明的状态。这是一个"什么时候引入抽象层"的判断题,不是"该不该用"的对错题。判断标准很具体:当"漏改导致的调试时间"开始超过"维护抽象层的时间"时,就是升级的信号。

模块组织:按业务切,不按技术切

主进程那边,handler 分散在 src/main/ipc/ 下 27 个文件里,每个文件对应一个业务领域——对话、配置、浏览器、远程访问、App 管理等。

这个组织方式影响的不只是代码整洁度,还有 AI 协作的效率。当你对 AI 说"加一个对话相关的 handler",它知道去 conversation.ts 里找。如果所有 handler 堆在一个文件里,AI 要在几千行代码里定位插入点,出错概率翻倍。模块边界是给人看的,也是给 AI 看的。

所有 handler 遵循同一个响应格式 { success, data?, error? }。渲染层用统一的方式处理成功和失败,不用每个调用点写不同的错误解析。这个约定写在提示词里,AI 从第一天起就没偏离过。

局部重复 vs 全局 DRY

HTTP 路由和 IPC handler 几乎调用同一个 service 层,只是入口不同——IPC 通过 event 对象传参,HTTP 通过 req/res。这种近乎 1:1 的重复,第一反应是抽一个 controller 层去重。

但 IPC 和 HTTP 的签名不同,强行统一需要一个适配层,适配层又引入新的间接性。最终的代码量可能差不多,但可读性下降了——你需要理解适配层才能理解调用链。

"局部重复但一目了然"和"全局 DRY 但需要理解间接层",是软件设计中反复出现的取舍。DRY 原则(Don't Repeat Yourself)是对的,但它的适用前提是"重复的部分真的是同一个东西"。IPC handler 和 HTTP route 看起来像同一个东西,但它们的变化原因不同——IPC 可能要处理 Electron 特有的 event 对象,HTTP 可能要处理中间件和认证。它们碰巧现在长得一样,不代表以后也一样。DRY 消除的应该是"同一个知识的重复",不是"碰巧相似的代码的重复"。


2.3 分层与依赖方向:为什么坏代码不会拖垮整栋楼

问题:一个文件改坏了,半个项目跟着崩

假设你在做一个中等规模的项目——十几个模块,几万行代码。你改了数据库模块里的一个查询函数,结果 UI 页面崩了。

你困惑了一下,然后发现:UI 组件直接 import 了数据库模块的一个内部函数。你改了那个函数的返回格式,UI 组件没跟着改,崩了。

这不是你的错,是代码结构的错。UI 层不应该直接依赖数据库层。 它们之间应该隔着一个 service 层——UI 调 service,service 调数据库。这样你改数据库的内部实现,只要 service 的接口没变,UI 就不会受影响。

这就是"分层"——软件架构里最古老、最基本的组织原则之一。几乎每本架构书都会讲,但它在 AI 编程时代变得比以往更重要。原因很简单:

AI 写出坏代码是常态。

不是偶尔,是常态。一个函数里逻辑绕弯、一个模块内重复实现、一个变量名起得莫名其妙——这些事天天发生。你不可能靠 review 全部拦下,量太大了。

那怎么办?答案不是"让 AI 写出更好的代码"(这取决于模型能力,你控制不了),而是让坏代码的影响范围可控

设计思想:单向依赖 = 风险隔离

分层的核心不是"把代码分成几个文件夹"——那是形式。核心是依赖方向的约束:上层可以依赖下层,下层不能依赖上层。

UI 层(渲染进程)  ↓ 可以调用Service 层(业务逻辑)  ↓ 可以调用Platform 层(数据库、文件系统)  ↓ 可以调用Shared 层(类型定义、常量)

箭头只能朝下,不能朝上。Shared 层不知道 Platform 层的存在,Platform 层不知道 Service 层的存在。这个约束有两个直接后果:

后果一:损坏被隔离在局部。 如果 AI 在 Platform 层写了一段烂代码,它只可能影响 Platform 层本身和调用它的 Service 层。UI 层不受影响——因为 UI 层不直接依赖 Platform 层。就像一栋楼的承重结构:一角被砸坏,受力被楼板和承重墙分摊到局部,整栋楼不会倾倒。

后果二:懒加载和代码拆分成为可能。 这一点经常被忽略。如果两个模块互相依赖(A import B,B 也 import A),打包工具没法把它们拆成独立的 chunk——它们被绑死在一起。你想对 A 做 dynamic import() 实现懒加载?做不到,因为 B 已经在启动时把 A 拉进来了。循环依赖是性能优化的天敌。 我在在线文档系统里见过这个问题——几个核心模块互相引用,导致主 bundle 怎么拆都拆不小,首屏加载时间降不下去。根源不是打包配置的问题,是依赖方向的问题。

所以分层不是"代码整洁"的审美追求,它是两个非常实际的工程需求的前提:风险隔离和性能优化。

实战:哪些层是干净的,哪些不是

一个真实项目的分层不会是完美的。看一下 Halo 的实际情况:

src/shared/      → 0 个外部依赖。完全干净。src/renderer/    → 0 个对 src/main/ 的依赖。靠 Electron 进程模型硬隔离。src/sdk/         → 0 个对 main/renderer 的依赖。完全独立。src/worker/      → 0 个对 Electron 的依赖。纯 Node.js 进程。

这四个层的边界是硬的——不是靠 lint 规则,是靠进程隔离和构建配置。renderer/ 和 main/ 在不同的进程里运行,物理上就不可能互相 import。worker/ 的代码注释里写着"不得导入 Electron"——因为它跑在独立的子进程里,导入 Electron 会直接崩溃。

但在 src/main/ 内部,情况没那么干净:

  • services/ 和 apps/ 存在双向依赖。 apps 依赖 services 是正常的(57 处 import)。但 services 也反向依赖了 apps(12 处 import)。这意味着你没法把 apps 模块独立拆出去做懒加载——它和 services 绑在一起了。
  • **platform/ 反向依赖了 services/**(7 处 import)。platform 应该是更底层的基础设施,它不应该知道 services 的存在。

这不是"Halo 的代码多好"的展示——恰恰相反,这是一个真实项目里"80% 干净 + 20% 务实妥协"的典型状态。那 20% 的不干净有具体代价:apps 和 services 的循环依赖让数字员工系统没法被独立懒加载,启动时必须一起加载。如果依赖方向是干净的,数字员工系统可以推迟到用户第一次使用时再加载——这正是下一节(2.4)讲的性能优化手段的前提。

分层对 AI 编程为什么特别重要

传统团队开发里,分层的价值是"代码可维护性"——一个有经验的工程师会自觉遵守依赖方向,偶尔犯错同事也能在 review 里拦住。

AI 编程里,分层的价值变成了损伤控制

AI 不会"自觉遵守"依赖方向。你不在提示词里明确写"这个模块不能 import services/",它就可能从 platform 层直接调 service 层的函数——因为那是完成任务最快的路径。AI 优化的是"当前任务完成",不是"长期架构健康"。

这意味着两件事:

第一,依赖方向必须写在 AI 能看到的地方。 不是写在架构文档的某个角落,而是写在 AI 每次启动时自动加载的项目指令文件里。"src/shared/ 不得 import 任何其他层""platform/ 不得 import services/"——这种规则越具体越好。AI 遵守明确的禁令比遵守模糊的原则可靠得多。

第二,进程级别的硬隔离比 lint 规则更可靠。 Halo 的 renderer/ 和 main/ 之间零违规,不是因为 AI 更守规矩,是因为进程模型让违规在物理上不可能。如果你的架构允许用进程或构建配置来强制执行层级边界,优先用它们——它们不依赖任何人(或 AI)的纪律。

第一章里提到过一个判断:"给 AI 设计合理的模块层次和依赖方向,是纪律能落地的前提。" 本节展开了这个判断的技术含义。分层不保证 AI 写出好代码——但它保证坏代码的爆炸半径是有限的。


2.4 首屏性能:预算、懒加载、Worker 隔离

问题:为什么你的应用越来越慢

假设你在做一个 Electron 桌面应用。第一个月,功能不多,启动飞快——0.5 秒。你很满意。

第三个月,加了用户系统、文件管理、通知中心。启动变成 2 秒。你想"还好,可以接受"。

半年后,加了插件系统、AI 功能、自动更新、数据库初始化。启动变成 5 秒。用户开始投诉。你想优化,打开启动流程的代码——几十个初始化模块排在 app.on('ready') 回调里,互相之间有隐含的依赖关系,没人知道哪个能安全移走。

你不敢动。

这个过程我在大型在线文档系统里亲眼见过。几十个初始化模块塞进启动回调,功能堆到后面,启动时间从 800ms 膨胀到 4 秒。不是因为哪一个模块特别慢——每个模块单独看都只要几十毫秒。问题是没有人在功能加入时问过一个问题:"这个东西是首屏必须的吗?"

VS Code 也面临同样的问题。它的解决方案是把启动拆成 5 个 Phase——Phase 0 是窗口壳,Phase 1 是核心编辑器,Phase 2 以后才是插件系统。这套 Phase 模型后来成了 Electron 应用启动优化的标准参考。

但无论是 VS Code 的 5 阶段还是其他任何分阶段方案,背后的思想是同一个:性能不是事后优化的结果,而是事前设定的约束。

设计思想:预算约束

"优化"和"约束"是两种完全不同的思维方式。

优化是事后的。功能先上,等慢了再找瓶颈、做 profiling、逐个优化。问题是:等你发现慢的时候,技术债已经堆了一层又一层,优化的成本远高于当初做对的成本。而且优化往往是局部的——你优化了一个模块,下个月又加了三个新模块,启动时间又回去了。

约束是事前的。在动第一行代码之前就设定一个预算——比如"启动时间不超过 500ms"——然后每个新功能加入时都必须在预算内。超了,不是"以后优化",而是"现在就不能放在启动阶段"。

这和家庭财务的道理一样。"月底看看花了多少,超了就少花点"是优化思维;"月初设好预算,每笔支出都要在预算内"是约束思维。哪个更有效,不言自明。

预算约束的价值不在于数字本身——500ms、300ms、1 秒都可以,取决于你的产品。价值在于它强迫每个新功能在加入时回答一个问题:你是首屏必须的吗?

大部分功能的答案是"不是"。

实战:三阶段启动

Halo 把启动拆成三个阶段,写在 src/main/bootstrap/ 下。

阶段一:Essential(500ms 红线)。 只注册首屏必须的 9 个 IPC handler。顺序有讲究——Config 必须第一个,因为后面的服务可能读配置。这 9 个服务决定了用户打开应用后第一眼看到的东西:空间列表、对话列表、发送消息的能力。

这个约束写在代码注释里:

// src/main/bootstrap/essential.ts// GUIDELINES://   - Each service here directly impacts startup time//   - Total initialization should be < 500ms//   - New additions require architecture review

三行注释,但它形成了一道心理屏障——任何人(包括 AI)想往首屏塞新东西,都会先看到这三行。Remote Access 想进来过,被拒了:远程访问不是首屏必须的。AI Browser 想进来过,也被拒了:浏览器功能可以等用户第一次点击时再初始化。

阶段二:Extended(窗口显示后)。 窗口已经画出来了,用户已经能看到界面了。这时候开始注册非首屏功能:远程访问、AI 浏览器、搜索、健康监控等十几个模块。关键特征是只注册,不执行重逻辑。AI Browser 的初始化是懒加载的——注册 handler 时什么都不做,等用户第一次用到浏览器功能时才真正启动。

最重的活——数据库、调度器、记忆系统、App Runtime——全部丢进一个后台异步函数,UI 不等它。

阶段三:Background(后台执行)。 数据库初始化完成后,调度器、App 管理器、App Runtime 按依赖顺序启动。这里有一个关键的顺序约束:调度器必须最后启动。原因是调度器一旦启动就会触发定时任务,如果事件路由还没注册完,定时事件会被触发但没有监听器接收——然后丢失。

"先订阅,后发布"——在消息系统设计里这是常识,但在启动流程里很容易忘。急着让系统跑起来,忘了有些组件还没准备好接收。

Push + Pull:一个实践中发现的状态同步问题

阶段二完成后,需要通知渲染进程"扩展服务已就绪"。最直觉的做法是发一个事件:

sendToRenderer('bootstrap:extended-ready', { timestamp, duration })

开发时,每次改一行 React 代码,Vite 会热重载渲染进程,但主进程不重启。这个事件在 3 秒前就发过了,新的渲染进程没收到。结果:HMR 之后整个 App Store 面板变灰——渲染进程认为扩展服务还没就绪。

解决方案是同时用两种机制:事件推送(Push)和状态查询(Pull)。主进程发完事件后还维护一个布尔标志位,渲染进程启动时主动查一下——如果已经 ready 就不等事件了。

VS Code 用了更复杂的 Barrier + LifecyclePhase 状态机来解决同样的问题。两个变量和一个 IPC handler 对单窗口应用足够。方案的复杂度应该匹配问题的复杂度。

分层懒加载:同一个原则的多层应用

"推迟到需要时再加载"这个原则不只适用于服务初始化,数据加载也一样。

Halo 的对话存储有一个设计:AI 的思考链(thinking)单独存成 .thoughts.json,不跟消息本体混在一起。原因是思考链占整个对话文件大小的约 97%——一条消息可能只有 200 字,但思考过程可能有 8000 字。如果每次列出对话列表时都把思考链加载进内存,首屏就别想快了。

所以对话数据也分三层:列表只读 index(极轻量),进入对话读 messages(中等),展开思考过程才读 thoughts(重量级)。

同一个思维模型——按需加载,而不是预先加载——从服务层贯穿到数据层。你在自己的项目里也可以用同样的判断:这个数据/服务/模块,用户此刻真的需要看到吗?如果不需要,推迟。

重 I/O 剥离主线程:Worker 隔离

分层解决了"什么时候加载"的问题,但还有一个问题:"加载过程本身如果很重怎么办?"

文件系统监控就是这类问题。桌面应用需要监控工作空间里的文件变化(新增、修改、删除),这涉及大量的文件系统调用。如果在主进程的事件循环里做这件事,每次文件扫描都会阻塞 UI 响应——用户点一个按钮,要等文件扫描完才有反应。

解决方案是把重 I/O 操作剥离到独立进程。Halo 有一个 file-watcher worker(src/worker/file-watcher/),通过 child_process.fork() 运行在独立的 OS 进程里,和主进程通过消息通信。主进程的事件循环完全不受文件扫描的影响。

这个模式的关键约束是:worker 进程不能导入 Electron。它是一个纯 Node.js 进程,只做计算和 I/O,通过类型化的消息协议和主进程交换数据。这种隔离确保了 worker 崩溃不会拖垮主进程——主进程检测到 worker 崩溃后会自动重启它。

什么时候该用 Worker?一个简单的判断标准:如果一个操作可能阻塞事件循环超过 50ms,把它移出去。 50ms 是一帧的时间(按 20fps 算)。超过这个时间,用户能感知到界面卡顿。

这些手段的共同思想

三阶段启动、懒加载、Worker 隔离——这些手段表面上各不相同,但底层是同一个思想:把"什么时候做"和"做什么"分开设计。

不加约束的默认行为是"启动时全部做完"。这在功能少的时候没问题,功能多了就是灾难。分层启动把"做什么"拆成了几批,按优先级分配到不同时间点。懒加载进一步推迟到"用户真正需要时"。Worker 隔离甚至把"在哪里做"也分开了——重活丢到另一个进程,主进程只负责协调。

这套思维方式不限于 Electron。Web 应用的 code splitting、移动端的按需加载、后端服务的延迟初始化——本质上都在回答同一个问题:这件事必须现在做吗?必须在这里做吗?


2.5 SQLite 命名空间迁移:并行开发的版本隔离

问题:两个人同时改数据库

假设你的项目用了 SQLite。你和同事各自在做一个新功能,都需要给数据库加新表。

你们用的是 Rails 风格的全局迁移:每次改 schema 就写一个迁移文件,文件名带版本号(001、002、003...),启动时按顺序执行。

你写了 003 号迁移,加了一张 tasks 表。同事写了 003 号迁移,加了一张 logs 表。你们都在自己的分支上开发,各自的代码跑得好好的。合并代码的时候——版本号撞了。两个 003,系统不知道先跑哪个。

这是一个协调问题。通常的解决方式是:约定由一个人分配版本号,或者用时间戳代替序号。但不管怎么约定,核心矛盾不变——全局版本号要求所有开发者在一条线上排队。

如果你的"开发者"是几个并行运行的 AI 实例呢?它们互相不知道对方的存在,不能在 Slack 上喊一声"003 我用了"。

设计思想:版本隔离

解决版本号冲突有两条路。

路一:每个模块独立数据库。 调度器用 scheduler.db,App 管理器用 apps.db,各管各的版本号,互不干扰。好处是隔离彻底;代价是每个模块都要写一遍数据库打开、PRAGMA 配置、连接管理的样板代码。如果你有五个模块,就是五份几乎一样的基础设施代码。

路二:共享数据库,但版本号按模块隔离。 所有模块共用一个 .db 文件,但每个模块有自己的版本号——调度器的 v3 和 App 管理器的 v3 是两个东西,互不干扰。模块共享连接管理、PRAGMA 配置、事务机制,但各自追踪自己的 schema 版本。

路二就是"命名空间迁移"。它在 Rails 的全局迁移和微服务各自独立数据库之间找了一个中间点:共享基础设施,隔离版本演进。

这个方案的适用条件很明确:多个模块需要持久化,模块之间偶尔需要事务保证,但 schema 演进是独立的。 如果模块之间有大量 JOIN 查询(紧耦合),共享数据库但隔离版本反而碍事——不如用同一套迁移。如果模块之间完全无关(松耦合),独立数据库更干净。命名空间迁移适合中间地带。

实战:一张元表,四个命名空间

用一张 _migrations 表管理所有命名空间的版本:

CREATETABLEIFNOTEXISTS _migrations (  namespace  TEXT PRIMARY KEY,versionINTEGERNOTNULLDEFAULT0,  applied_at INTEGERNOTNULL)

三列,每个命名空间一行记录。scheduler 版本 1,app_manager 版本 3,app_runtime 版本 3,store-cache 版本 1。不是每次迁移一行——那样要扫描历史。只关心"现在到了第几版"。

每个消费模块注册自己的迁移序列:

const migrations = [  { version: 1, up: (db) => { /* 建初始表 */ } },  { version: 2, up: (db) => { /* 加索引 */ } },  { version: 3, up: (db) => { /* 改字段 */ } },]dbManager.runMigrations('app_manager', migrations)

runMigrations 查当前版本,只跑未执行的迁移,全部在一个事务里完成。中间任何一步失败,整个事务回滚,版本停留在执行前。

为什么事务性不是可选的

事务在这里不是"最佳实践",是必需品。

SQLite 不支持 ALTER COLUMN——改一个字段类型,唯一的办法是建新表、复制数据、删旧表、改名。这四步如果中间断了(比如断电),数据库就停在一个不一致的中间状态:旧表删了,新表还没改名。事务把这四步变成原子操作——要么全成功,要么全回滚,不存在中间状态。

better-sqlite3 的 db.transaction() 返回一个函数,不是启动一个事务。调用这个函数才真正执行。内部任何一步抛异常,整个事务回滚。这个 API 设计本身就值得学习——它让事务的边界在代码里一目了然。

损坏恢复:可用性优先于数据完整性

数据库可能损坏。断电、磁盘错误、或者开发阶段的 schema 实验,都可能导致 .db 文件无法打开。

多数应用的做法是报错退出。但对桌面应用来说,启动崩溃是不可接受的——用户打不开应用,什么都做不了。所以数据库初始化做了一件事:打开后立刻查一次 sqlite_master,如果查询失败(文件损坏),把损坏的文件改名备份,然后建一个全新的空数据库。

丢数据?是的。调度任务、运行日志、安装记录全没了。但 App 的定义存在 YAML 文件里(不在数据库),安装信息可以重建。相比之下,不能启动就不能恢复,能启动至少能重建。

这是一个可用性 vs 数据完整性的取舍。不同的产品会做不同的选择——银行系统选数据完整性,桌面应用选可用性。关键不是选哪个,是明确知道自己选了什么、放弃了什么

336 行,不多不少

database-manager.ts 全文 336 行。没有 ORM,没有查询构建器。每个消费模块拿到数据库连接,自己写 SQL,自己定义迁移序列。

为什么不用 ORM?数据模型不复杂——四个命名空间加起来不到十张表。ORM 的抽象层在查询调试时反而增加间接性。而且 better-sqlite3 是同步 API(这是它快的原因之一),ORM 大多假设异步,适配层又是一层复杂度。

这是一个工程判断:抽象层的价值只在问题规模足够大时才显现。 10 张表用 ORM,你花在理解 ORM 行为上的时间可能比写 SQL 还多。100 张表就不一样了——ORM 的类型安全和查询构建在大规模下能防止大量低级错误。判断标准不是"ORM 好不好",而是"你的问题规模是否值得引入这层抽象"。


本章回顾

这一章讲了五个设计思想,每一个都可以脱离 Halo 独立应用到你自己的项目里。

运行时 Adapter(2.1):把平台差异封装在一个薄层里,业务代码不知道自己跑在什么平台上。核心原则是"把变化的原因隔离开"。运行时检测 vs 编译时分支的选择取决于团队规模和平台差异程度。

声明式能力边界(2.2):用一份白名单定义所有能力,而不是在每个调用点做权限检查。白名单几乎总是比黑名单安全。多文件同步的工程代价,用规范约束、共享接口还是代码生成来解决,取决于变更频率和项目规模。

分层与依赖方向(2.3):单向依赖不是代码整洁的审美追求,它是风险隔离和性能优化的前提。循环依赖让懒加载和代码拆分做不了;在 AI 编程时代,分层更是损伤控制机制——坏代码被困在局部,不会跨层传染。

预算约束(2.4):性能不是事后优化,是事前约束。一个写在代码注释里的数字(500ms),比任何 profiling 工具都能更早地防止性能退化。同一个"推迟到需要时再做"的原则,从服务初始化延伸到数据加载和进程隔离。

命名空间迁移(2.5):多个独立演进的模块共享一个数据库时,用命名空间隔离版本号,避免全局协调的开销。事务性迁移不是可选的"最佳实践",在 SQLite 的 ALTER 限制下是必需品。

这五个思想会反复出现在后续章节。第六章的调度器建立在命名空间迁移上,第八章的 AI 浏览器通过能力边界暴露功能,第十一章的 Remote Access 依赖三端 Adapter,第十三章的 AI 编程方法论建立在分层提供的风险隔离之上。地基不出彩,但上层每一层都站在它上面。

下一章进入本书技术深度最深的区域:AI 引擎。一个经过 65 次迭代的自研 Agent 引擎,积累了大量关于"如何让 AI 可靠地完成工作"的工程经验。

完结


《如何从零构建 7×24 小时 AI Agent 》

一本讲 AI Agent 系统设计思想的开源技术书。从问题出发教设计原则,用 30 万行真实代码做案例。

🌐 在线阅读 · 💬 反馈与讨论

https://github.com/openkursar-flynn/build-ai-agent-platform

这本书讲什么

不是 AI 入门,不造概念,不是源码导读。

本书像《设计模式》一样,用一个真实项目(30 万行 TypeScript,开源 AI Agent 平台)作为贯穿全书的案例,讲 AI Agent 系统从引擎到调度到浏览器自动化的设计思想。每一节从一个具体问题开始——"你的应用要跑三个平台怎么办" "Agent如何持续保持记忆"等——引出设计原则,再用真实代码验证。

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-05-27 18:36:41 HTTP/1.1 GET : https://www.yeyulingfeng.com/a/669850.html
  2. 运行时间 : 0.096507s [ 吞吐率:10.36req/s ] 内存消耗:4,827.13kb 文件加载:145
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=fe6173819283e9ef239e13152df33761
  1. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_static.php ( 6.05 KB )
  7. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/ralouphie/getallheaders/src/getallheaders.php ( 1.60 KB )
  10. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  11. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  12. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  13. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  14. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  15. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  16. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  17. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  18. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  19. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions_include.php ( 0.16 KB )
  21. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions.php ( 5.54 KB )
  22. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  23. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  24. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  25. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/provider.php ( 0.19 KB )
  26. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  27. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  28. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  29. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/common.php ( 0.03 KB )
  30. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  32. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/alipay.php ( 3.59 KB )
  33. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  34. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/app.php ( 0.95 KB )
  35. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cache.php ( 0.78 KB )
  36. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/console.php ( 0.23 KB )
  37. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cookie.php ( 0.56 KB )
  38. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/database.php ( 2.48 KB )
  39. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/filesystem.php ( 0.61 KB )
  40. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/lang.php ( 0.91 KB )
  41. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/log.php ( 1.35 KB )
  42. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/middleware.php ( 0.19 KB )
  43. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/route.php ( 1.89 KB )
  44. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/session.php ( 0.57 KB )
  45. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/trace.php ( 0.34 KB )
  46. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/view.php ( 0.82 KB )
  47. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/event.php ( 0.25 KB )
  48. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  49. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/service.php ( 0.13 KB )
  50. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/AppService.php ( 0.26 KB )
  51. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  52. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  53. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  54. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  55. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  56. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/services.php ( 0.14 KB )
  57. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  58. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  59. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  60. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  61. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  62. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  63. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  64. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  65. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  66. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  67. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  68. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  69. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  70. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  71. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  72. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  73. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  74. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  75. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  76. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  77. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  78. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  79. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  80. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  81. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  82. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  83. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  84. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  85. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  86. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  87. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/Request.php ( 0.09 KB )
  88. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  89. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/middleware.php ( 0.25 KB )
  90. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  91. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  92. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  93. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  94. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  95. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  96. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  97. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  98. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  99. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  100. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  101. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  102. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  103. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/route/app.php ( 3.94 KB )
  104. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  105. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  106. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Index.php ( 9.87 KB )
  108. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/BaseController.php ( 2.05 KB )
  109. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  110. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  111. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  112. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  113. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  114. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  115. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  116. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  117. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  118. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  119. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  120. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  121. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  122. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  123. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  124. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  125. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  126. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  127. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  128. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  129. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  130. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  131. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  132. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  133. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  134. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  135. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Es.php ( 3.30 KB )
  136. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  137. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  138. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  139. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  140. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  141. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  142. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  143. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  144. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/runtime/temp/c935550e3e8a3a4c27dd94e439343fdf.php ( 31.50 KB )
  145. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000689s ] mysql:host=127.0.0.1;port=3306;dbname=wenku;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001233s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000394s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000352s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000825s ]
  6. SELECT * FROM `set` [ RunTime:0.000256s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000821s ]
  8. SELECT * FROM `article` WHERE `id` = 669850 LIMIT 1 [ RunTime:0.000665s ]
  9. UPDATE `article` SET `lasttime` = 1779878201 WHERE `id` = 669850 [ RunTime:0.001204s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.000300s ]
  11. SELECT * FROM `article` WHERE `id` < 669850 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000561s ]
  12. SELECT * FROM `article` WHERE `id` > 669850 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000597s ]
  13. SELECT * FROM `article` WHERE `id` < 669850 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.001518s ]
  14. SELECT * FROM `article` WHERE `id` < 669850 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.001020s ]
  15. SELECT * FROM `article` WHERE `id` < 669850 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.001255s ]
0.098160s