桌面客户端
桌面客户端是员工每天面对的那一层。它是一个 Electron 应用,把一个固定版本的上游 DeepSeek Harness 运行时装进原生外壳:窗口、托盘、自动更新、登录页,不需要员工安装 Node.js,也不需要执行任何命令。
它同时是产品的主入口:能力中心、连接器、自动化任务、内置浏览器、语音输入、五轨记忆、应用中心 都以面板的形式长在这个客户端里,而不是网页。
三层里这一层负责什么
Section titled “三层里这一层负责什么”| 层 | 跑在哪 | 负责 | 明确不负责 |
|---|---|---|---|
| 桌面客户端 | 员工机器(Electron 应用) | 原生窗口与托盘、登录页与账户、自动更新、以及全部用户界面面板(对话、能力中心、连接器、自动化任务、浏览器、语音、记忆、应用中心) | 不做模型调用与计量,不保存账号凭据,不决定员工能看到什么内容 |
| 本地 Harness 服务 | 员工机器,127.0.0.1 上的随机端口(可固定) | 跑固定版本的上游 DSH:会话与消息日志、文件读写、工作区、沙箱与审批、工具执行 | 不出网做模型请求之外的判断;不持有账号、角色、额度 |
| 企业服务端 | 客户内网一台机器(容器镜像) | 认证与账号、模型网关与计量计费、能力中心与审批、连接器目录、应用中心、审计、客户端分发、门户与管理后台 | 不接触员工本机文件、不接触员工本机会话日志 |
这条分工决定了后面所有的边界:
- 数据留在本机的那一半:会话、工作区文件、工具执行、沙箱状态、语音音频都在员工机器上; 服务端只看到账号、模型用量与审计事件。
- 控制面在服务端的那一半:模型目录(可用哪些模型)、余额与额度、能力中心的内容与授权、 连接器目录、应用目录,全部由服务端决定;客户端只是把它们画出来。
- 模型请求的路径:登录 → 拉取模型清单(
/api/client/v2/config/bootstrap)→ 请求打到 服务端的/v1/chat/completions→ 服务端过限流与余额闸门 → 按模型匹配上游 provider → 计量与余额扣减同事务。上游 API Key 只存在于服务端,客户端从头到尾拿不到它。
主界面与托盘
Section titled “主界面与托盘”- 原生窗口,沙箱渲染进程:
contextIsolation: true、nodeIntegration: false、sandbox: true; 渲染进程只加载本地回环 origin 的页面,没有 Electron IPC 桥、没有裸 Electron API。 - 本地 Web 端口:默认
0(由系统随机分配,避免端口冲突),可配置为0–65535的固定端口; 服务只监听127.0.0.1。 - 端口变更 = 一次有序重启:改端口会先让当前 Cordis 树完整 dispose,再重新拉起 Electron, 不做活体热替换(根槽、原生材质、Loader 行都不在活的渲染代际里换)。
- 单实例:重复启动聚焦已有实例,不会开出第二个进程。
- 关闭 ≠ 退出:关闭窗口只是隐藏,Host 树继续跑(自动化任务照常触发);退出走托盘「退出」、
系统退出、
SIGINT/SIGTERM—— 先请求有序销毁,5 秒后或再次请求则强制退出。
托盘菜单是有序贡献点拼出来的,顺序固定为:打开产品 → 工具组 → Profile 组 → 状态组 → 退出。
| 分组 | 条目 | 行为 |
|---|---|---|
| 顶层 | 打开 <产品名> | 显示并聚焦主窗口 |
工具(tools,order 20) | 导出诊断信息… | 生成并定位 diagnostics-*.zip |
Profile(profiles) | —— | 产品不注册任何条目:没有 profile 切换、没有「打开 DSH 终端」、没有模式切换 |
状态(status,order 10) | 检查更新 / 有新版本 vX / 正在下载 vX / 安装更新 vX | 同一行随状态改文案;只有安装包真的下载并校验通过后才出现「安装更新」子项 |
| 顶层 | 退出 | 请求有序销毁后退出 |
- 日志写在 Electron 的 user-data 目录下
logs/:dsh-YYYY-MM-DD.log另加dsh-YYYY-MM-DD.error.log(只收 warning 与 error);单文件上限 10 MiB,整个目录上限 200 MiB,启动时清理 7 天前的日志;目录0700、文件0600。 - 导出诊断信息…(托盘 → 工具组):先弹一个说明隐私边界的原生确认框,确认后在旁边的
diagnostics/目录生成diagnostics-<时间戳>-<uuid>.zip,并在系统文件管理器中定位。 打包进压缩包的证据上限 50 MiB(日志与 Crashpad.dmp共用这个额度),另含system-info.txt(Desktop / Electron / Node / 平台 / 架构版本)。 - 日志里的可识别凭据会被脱敏,但本地路径、工作区 ID、会话 ID、提示词与工具输出仍可能存在 —— 对外上传前必须自己过一遍。
- 闪退到起不来时:直接运行安装后的程序并加
--export-diagnostics,它不启动 Host, 只打印诊断 ZIP 的绝对路径。 - 深链:
<scheme>://app/<应用 id>打开一个应用,<scheme>://auth?...处理登录回调; scheme 由随包的渠道内容决定(见部署文档的渠道章节)。
Profile 与高级模式
Section titled “Profile 与高级模式”| 概念 | 说明 |
|---|---|
| Profile | 一组 DSH bundle、依赖与 patch 的组合。产品固定运行 desktop profile:托盘里没有切换器,应用里也没有选择器。上游把该名字保留给 Electron 应用:dsh --profile desktop 与 dsh plugin --profile desktop 都会被 CLI 直接拒绝(profile "desktop" is managed exclusively by the Electron application) |
| 展示模式 | 固定 advanced。dsh-desktop.mode 这个设置项仍被接受,但只为向后兼容,读出来永远是 advanced |
| 高级模式做了什么 | 在不改上游 Web carrier 的前提下,禁用官方 ui-layout 行、保留官方 ui-sidebar 与 ui-conversation 行,由桌面包自持 layout 服务与根槽占用,只负责边框几何与原生材质:macOS 用透明 hidden-inset 标题栏与原生 sidebar vibrancy,Windows 用隐藏标题栏 + Mica,Linux 用标准系统窗框(没有 Mica,布局仍是高级模式) |
| 重启边界 | 增删 profile bundle、改端口、改用户补丁层,都需要重启应用才生效;应用不会在活的渲染代际里热换根槽 |
会话与工作区
Section titled “会话与工作区”- 一条会话 = 一段独立上下文 + 一个工作区(项目目录)。工作区决定沙箱的可写范围、 记忆的项目轨归属、以及模型能用哪些项目级技能;会话列表、搜索与恢复沿用官方 Harness 语义。
- 模型清单由服务端下发。客户端把
<服务端>/v1作为模型端点,模型是否可用、输入模态 (是否收图)、单价与折扣都由管理后台配置;客户端不做本地模型配置。 - 权限是会话级事实,不是全局开关。新建会话时会把当时的默认预设钉进这条会话的日志; 之后在设置里改默认值,只影响之后新建的会话。想让某条正在用的会话改档,用输入框旁的权限 按钮或会话内命令,不要靠「新建一条会话」——同一工作区里的空白会话可能被复用,而复用不看权限。
权限分档与工具审批
Section titled “权限分档与工具审批”上游的权限模型是两个旋钮:沙箱模式 + 审批策略。产品把它们捆成「预设」呈现。
| 档位(沙箱模式) | 可写范围 | 典型用途 |
|---|---|---|
read-only | 不写任何东西 | 查代码、查资料、做评审 |
workspace-write | 只写会话工作区 + 会话私有临时目录;读不受限 | 日常改代码(默认档) |
danger-full-access | 不施加文件效果约束 | 装依赖、动工作区外的路径 |
默认预设表里有两项:workspace-write(= workspace-write + 审批 ask)与
danger-full-access(= danger-full-access + 审批 never);custom 与 auto 是保留名,
分别表示「旋钮值不对应任何预设」与「审查集成派生」。新建会话的默认预设就是
workspace-write。
平台的落地手段不同:Linux 走内核的 Landlock 与进程托管,Windows 走目录 ACL 与受限令牌, macOS 走 Seatbelt。
- 审批策略
ask时,模型调用有风险的工具(写文件、跑命令、操作浏览器、上传)会先弹确认; 这是「AI 只提议、人决定」的第一道闸门。 - 授权是一次性的:批准的是这一次调用,没有「总是允许」。
- 沙箱拒绝与审批拒绝都会作为工具错误回到模型,模型看得见「被谁拒了、为什么」。
Windows 上「工作区内修改」的前置条件
Section titled “Windows 上「工作区内修改」的前置条件”workspace-write 在 Windows 上要先给工作区根目录写一条 DACL(capability write ACE +
Low 强制性标签)。写 DACL 需要调用者拥有该目录或持有 WRITE_DAC;拿不到就 fail-closed:
- 表现是该工作区的每一条命令都起不来(连只读的列目录也一样,因为授权发生在命令启动之前), 只能逐条用「完全权限」提权绕过;
- 报错会明说这是目录 DACL 的问题,而不是代码或命令的问题;
- 三条出路:① 换成本账户在本地 NTFS 上用普通(非管理员)窗口新建的目录;
② 管理员执行
takeown /F "<目录>" /R /D Y+icacls "<目录>" /grant "<用户>:(OI)(CI)F"后重启客户端;③ 该会话改用「完全权限」。 - 「只读」与「完全权限」两档不经过这一步,所以不会撞上这个错误。网盘、映射网络驱动器、 exFAT/FAT32 上 ① ② 都无效。
侧边栏单入口,回答两个问题:有什么内容可以用,以及它从哪里来。
能力中心├── 我的 本地创作 + 上传状态(技能 / 智能体)└── 市场 授权制商城 + 组织共享库的合并视图(技能 / 智能体)- 顶部只有来源维度的两个 Tab(我的 / 市场);「组织」以来源徽章出现在市场视图内 (来源徽章:市场 / 组织 / 本地);另有类型筛选(全部 / 技能 / 智能体)与搜索框。
- 多版本归并:同名内容用
{kind}:{name}复合键合并成一张卡,展开可看历史版本; 市场与组织同名时由服务端合并为一条权威行(市场优先)。 - 质量徽章:
official(官方)与featured(精选)由管理员在审核时标记,互斥; 官方内容普通员工不能上传新版(按钮置灰)。 - 状态徽章:已安装 / 可更新到 vX / 审核中 / 未通过(带原因)。
- 分区独立错误态:一个接口失败只影响对应分区,该分区显示「重试」,其余照常。
| 操作 | 行为与拒绝条件 |
|---|---|
| 安装 | 从市场 / 组织安装技能或智能体包;与本地同名时弹覆盖确认。本地同名内容已被改动过则宿主回 409 LOCAL_CONTENT |
| 更新 | 已安装且有更高的已批准版本时显示「更新到 vX」,同样走安装确认 |
| 卸载 | 确认后卸载。两种情况会拒绝:① 同名内容还存在于优先级更高的技能根(项目 / 用户)⇒ 422 RESIDUE,需要先处理那一份;② 本地内容已被改动 ⇒ 409 LOCAL_CONTENT,显式覆盖后才删 |
| 上传共享 | 把本地技能 / 智能体打包上传(归档安全校验在客户端与服务端两侧都做),进入 pending 等管理员审核;可重新上传新版本。服务端对同名占用回 409 NAME_TAKEN |
| 查看状态 | 「我的」分区展示自己上传内容的审核状态(审核中 / 已共享 / 未通过 + 原因) |
分发与信任模型——两条来源走的是同一套「双门制」:
- 市场:管理员上架,再按用户或部门授权后才可见可装;分级词只有免费版 / 专业版(
price.tier)。 - 组织:员工上传 →
pending→ 管理员 approve / reject(reject 必填原因)→ 批准之后仍须授权 才可见可装。管理员恒可全量。 - 本地:自己创作的技能 / 智能体,只在本机可见,不经审核。
连接器把外部系统以 MCP(Model Context Protocol) 接进来。对外契约收敛成**一条标准 MCP 配置
- 标题说明**——只有传输与端点也能跑:
{ "mcpServers": { "example-mcp": { "type": "streamableHttp", "url": "https://mcp.example.com/mcp" } } }type接受streamableHttp/http/stdio(省略则按url或command推断);stdio用command/args/env。type: "sse"明确不支持。- 未登记的键一律报错,不会被静默忽略——少写一个字母不会被当成默认值。
认证模式:默认自动
Section titled “认证模式:默认自动”authMode 默认为 auto,判定顺序是先到先得:
- 声明了凭据字段(
tokenFields)⇒ 走静态 token(请求头里用${字段}引用); - 否则有 streamable-http 端点 ⇒ 走 OAuth(discovery 地址默认就取那个端点本身,也可用
auth.discoveryUrl显式指定);连接时先探测:2xx 视为公开端点,401 按WWW-Authenticate的声明走 RFC 9728 → RFC 8414 发现 + 动态客户端注册 + PKCE 回环回调; - 都没有 ⇒ 免凭据。
会被拒绝的东西
Section titled “会被拒绝的东西”| 情形 | 结果 |
|---|---|
声明进程引导变量(PATH、NODE_OPTIONS、LD_PRELOAD、PYTHONPATH、BASH_ENV…) | 拒收。DSH_ / ELECTRON_ / ZEOKEEL_ / GIT_CONFIG_KEY_ / GIT_CONFIG_VALUE_ 按前缀整族拒收 |
命令钩子族(GIT_SSH_COMMAND、GIT_ASKPASS、BROWSER、LESSOPEN、PERL5OPT、JAVA_TOOL_OPTIONS…) | 拒收:名字看起来是”配置”,值是会被执行的命令模板 |
PAGER / EDITOR / GIT_EDITOR 一类选择器 | 允许但值受形状闸门约束:只接受单个程序名形状(不允许空白与 shell 元字符),且 basename 不能是 sh / python 之类解释器 |
| 出站目标解析到内网、保留段、回环或云元数据地址 | 拒发。解析失败一律 fail-closed(错误码 resolution-failed / resolution-timeout / resolution-empty) |
本地 stdio 连接器首次运行 | 需要本地确认:命令、参数、环境变量逐条披露给你看 |
- 凭据落在按账号 + 服务端地址分作用域的目录里(
<数据根>/users/<账号>/connectors), 目录0700、文件0600,原子写、拒收符号链接。 - 令牌续期有三条触发点:MCP SDK 自己的 401 自愈、后台心跳(到期前 60 秒)、面板上的手动刷新。 一个连接器同时只有一个刷新主人,避免同一个一次性 refresh token 被出示两次而被授权服务器 判定重放、吊销整条授权。
- 失败语义:死授权(
invalid_grant/invalid_client)⇒ 行状态变成「需要重新授权」并给出明确文案; 5xx 视为瞬时故障、保留旧凭据;任何情况下都不会在后台替你打开浏览器。 - 行状态有五种:未连接 / 连接中 / 已连接 / 需要重新授权 / 错误。
连接器面板是本地回环路由:GET /api/zeokeel/connectors 列出,/api/zeokeel/connectors/<id>/<动作>
执行 connect / cancel / auth-submit / state / disconnect / approve / deny / refresh。
除状态轮询外的每一个动作都要求持有性证明,拿不到就拒绝进入处理函数。
侧边栏「更多」→「自动化任务」打开官方 Schedule 任务中心(0.2.1 起替换自研 cron)。 任务由 Host 调度、绑定到创建它的会话;到点把提示词发回该会话。
| 字段 | 说明 |
|---|---|
| 名称 | 必填,纯空白视为缺失(界面与模型面用同一条判据) |
| 时间规则 | 单次 / 固定间隔 / 每日 / 每周 / 5 段 cron;时区按创建时的本地时区记录 |
| 提示词 | 到点发给原会话的内容;它是数据,不是命令行 |
| 会话 | 固定绑定创建任务的会话,不能跨会话投递 |
| 状态 | 活跃 / 已结束;可在任务中心查看、编辑、删除 |
| 执行记录 | 触发时间、投递消息 id、最近一次投递;超出保留范围的旧记录会清理 |
执行与账号隔离
Section titled “执行与账号隔离”- 到点由 Host 进程投递;窗口关闭但应用仍在运行时照常触发;应用完全退出期间不触发。
- 下次启动后,属于当前登录账号的到期任务会被补触发(不是全部历史)。
- 任务目录、历史、更新与删除都按当前 ZeoKeel 账号(server + username)过滤: 其他账号的任务不可见、不可改,也不会被投递;未归属的历史任务在账号环境下同样不可见。
- 应用完全退出或账号未登录期间不会向他人会话投递。
模型可以调用四个工具:schedule_create / schedule_list / schedule_update / schedule_delete。
边界与界面同源:按账号过滤(看不到的任务等同于不存在);用户始终能在界面上看到并管理这些任务。
Agent 驱动的浏览器是一个独立的 OS 窗口,不是主窗口里的 iframe。
- 多标签:每个标签是一个
WebContentsView,跑在按用户分区的持久 partition 里 (persist:agent-browser-<用户>),所以会话存储、登录态与主窗口互相隔离。标签上限 16。 - 地址栏:工具栏提供 URL 输入、后退 / 前进 / 刷新、关闭标签。
- 创建即隐藏:窗口建出来是隐藏的,只有用户动作才显示;它也不会自己弹到前台—— 所有会抢焦点的动作都要先过一道「窗口可见 + 未最小化 + 有焦点」的闸门。
控制权:一个按钮,双向
Section titled “控制权:一个按钮,双向”这是这一层最容易被误设计的交互,因此写死成规则:
- 只有右下角的「我来操作」按钮能取得控制权;取得之后同一个胶囊变成「交给 AI」, 由它交回。
- 蒙版的空白处、活动面板、
Esc都不会改变控制权——避免”随手点一下就把浏览器从 AI 手里抢走”。 - 用户拿着控制权时,模型调用浏览器工具会收到一句明确的拒绝:
「用户正在操作浏览器(我来操作)——请在浏览器窗口点「交给 AI」交还控制权后重试」,
并且
browser_list_tabs的control字段会如实回报状态:controlled/busy/busyTool/awaitingRelease/awaitingReleaseTool。 侧边栏入口也会点亮提示,因为交还按钮只在浏览器窗口里,用户回到聊天窗口原本是没有任何提示的。 - 控制权不会比窗口活得久:关闭浏览器、销毁窗口、切换会话或分区都会复位控制权与提示。
预算序关系(这是硬约束,不是调参)
Section titled “预算序关系(这是硬约束,不是调参)”工具在 browser_* 上注册了一个协同预算,上游的 timeout 策略会在预算到点时替换整条结果——
所以任何内部等待都必须显著短于它,否则模型只会看到笼统的 tool call timed out,
真正的原因(用户拿着控制权、池子满了、页面没加载完)永远送不到。
| 量 | 值 |
|---|---|
browser_* 工具预算 | 30 秒 |
| 用户闸门等待 | 10 秒(预留给闸门打开后真正要跑的那一段 20 秒) |
browser_wait_for 条件等待上限 | 40 秒(注册预算 = 10 + 40 + 5 秒余量) |
| 开页前等空闲标签槽位 | 5 秒(超时抛出可执行的「标签已满,先关一个」) |
| 导航加载竞速上限 | 14 秒(= 30 − 10 − 5 − 1;到点只表示”这次不再等”,页面继续在后台加载) |
| 排队等前一个操作时保留的工作时间 | ≥ 1 秒 |
| 单帧索引等待 / 稳定等待 | 500 毫秒 + 150 毫秒 |
共 31 个工具,按用途分组:
| 分组 | 工具 |
|---|---|
| 标签与导航 | browser_open browser_navigate browser_reload browser_go_back browser_go_forward browser_list_tabs browser_switch_tab browser_close_tab |
| 交互 | browser_click browser_type browser_select browser_press browser_scroll browser_fill_form browser_upload_file |
| 读取 | browser_get_snapshot browser_get_text browser_screenshot browser_wait_for |
| 脚本 | browser_eval |
| 书签与历史 | browser_bookmarks_add browser_bookmarks_list browser_bookmarks_remove browser_history_search |
| 下载 | browser_download browser_downloads_list browser_downloads_remove |
| 控制权与凭据 | browser_takeover browser_fill_credentials browser_credentials_list |
| 清理 | browser_clear_data |
| 项 | 值 |
|---|---|
| 单次下载上限 | 100 MiB,超限直接拒绝;其余下载先询问保存位置 |
| 快照元素数 | 200 项 |
| 单次文本读取 | 32 KiB |
| 每个元素在快照里的文本 | 80 字符 |
| 历史条数 / 时间窗 | 5000 条 / 90 天 |
| 书签条数 | 500 |
| 下载记录条数 | 200 |
| 下载目录 | <数据根>/downloads |
操作账本(op log)记录每一次导航、点击与下载,可审计。落盘的 URL 会对敏感查询键做脱敏, 但页面内容、你填过的明文凭据不会进账本——需要凭据的操作走专门的凭据工具。
关闭语义:用户关窗只是隐藏;只有 Agent 的 browser_close 才真正销毁窗口。
输入框右侧的麦克风按钮把说话变成输入框里的文字,默认开启。
- 在本机识别:SenseVoiceSmall(整段识别,非流式)+ Silero VAD(分段)跑在客户端的 本地子进程里。音频不出机器,不经任何云端识别服务,也没有云端回退——离线可用。
- 模型从哪来:默认随客户端分发(权重就在安装包里,装上即用、零网络)。只有渠道显式
关掉(
desktop.speech_bundle_model: false)时才回落到「首次使用时下载 int8 量化模型 (约 228 MB)到<数据根>/speech-to-text/sensevoice」,此时点麦克风会进入准备面 (阶段 / 进度 / 失败原因,可取消、可重试)。 - “只有认证代理能出公网”的网络:模型下载与转写子进程都走直连(见下面的出站策略),
所以这类网络需要在渠道包里预置模型目录或指定内网镜像
(
desktop.speech_model_dir/desktop.speech_vad_path/desktop.speech_model_origin)。 - 权限:首次录音向系统申请麦克风;只放行本应用主窗口主框架的纯音频请求 (内置浏览器与应用窗口各走自己的权限面,不受影响)。macOS 还要先过系统授权框。
- 语言:中 / 英 / 日 / 韩 / 粤自动识别。
- 边界:转写只落到输入框(可以先改再发),不写入会话、不自动发送; 单次录音最长 120 秒、上限 4 MiB。
产品内置(vendored 社区插件 dsh-memory-evolve)的跨会话长期记忆:
| 轨道 | 内容 | 落盘 |
|---|---|---|
| 用户档案 | 你的稳定偏好与事实 | USER.md |
| 全局事实 | 环境 / 工具 / 惯例等跨项目事实 | MEMORY.md |
| 项目关键记忆 | 当前项目的关键长期记忆,自动注入上下文,可按 git 分支过滤 | projects/<项目>/KEY.md |
| 项目日志 | 当前项目的会话日志,按需读取 | projects/<项目>/MEMORY.md |
| 每日日志 | 每日工作日志,按需读取 | daily/YYYY-MM-DD.md |
- 确认制:AI 提议的关键记忆、待办、技能全部先进待确认队列,你采纳才写入。 会真实改变 AI 行为输入的写入,决定权在你。
- 归档:主轨 ↔ 归档文件可双向移动。归档后的条目不再注入上下文,随时可以移回。
归档文件与主轨同目录:
MEMORY-archive.md、USER-archive.md、projects/<项目>/KEY-archive.md。 - 隔离:项目轨按会话工作目录切分,关键记忆还可按 git 分支限定; 换项目、隔天继续时直接问 AI「查一下记忆」即可衔接上下文。
- 边界:关键记忆需要一条带工作目录的会话(拿不到就拒绝写入);写入走「临时文件 + rename」 的原子路径,且会拒收指向仓库外的符号链接——这些是记忆仓库可能被当作共享目录时的硬要求。
- 除了记忆,这个插件还提供四轨待办(life / work / project / daily)与技能自我进化能力; 这些能力默认多数关闭,需要时在插件的设置页打开。
员工自建的 WASM 应用:服务端执行、客户端独立窗口打开的小程序。
- 为什么是客户端专属:应用窗口就是客户端里的一个独立原生窗口,用自定义协议
<scheme>://<应用 id>加载,页面里所有对外请求都经客户端加信封后转发给平台。 因此服务器不需要为应用准备任何公网入口或证书,也不存在”应用子域”这一层。 - 打开路径:客户端本机路由
POST /api/zeokeel/wasm-apps/open;平台侧唯一入口POST /api/client/v2/apps/wasm/:app_id/request,带员工令牌转发执行。 - 窗口隔离:应用窗口跑在按用户分区的 session 里;导航、子帧导航与 302 重定向 共用同一个 origin 判据——跨应用与外站一律拦、同应用放行。
- 保留面:应用页面里
/__zeokeel/前缀是宿主保留命名空间,目前只有/__zeokeel/ai/chat一条路径存在(由客户端本地处理,绝不转发平台);其余一律 404。
应用配置 zeokeel.app.json 的 access 决定谁能打开:
| 值 | 语义 |
|---|---|
public | 匿名可达 |
login(缺省) | 必须登录 |
whitelist | 名单制;名单上限 2000,access=whitelist 时名单必须非空,否则发布被拒 |
平台只把身份与访问模式注入帧,名单比对由应用自己读取自己的配置并判定——平台不替应用做名单决策。
数据与 AI
Section titled “数据与 AI”- 每个应用有自己的数据库;作者可以在应用中心查看只读的表结构与行数据。
- 「允许 AI 读取此应用的数据」是一条独立授权,记账键是 用户 ⊕ 服务端地址 ⊕ 应用:换账号、换服务端都会重新问一次;旧版本的授权记录一律 按未授权处理,升级后需要重新授权一次。
- 应用面板里的请求体上限 1 MiB、响应体上限 1 MiB、请求头最多 24 个、 单个头值 8 KiB、单次请求超时 75 秒。
客户端与它登录的服务端绑定
Section titled “客户端与它登录的服务端绑定”这是这一层的核心设计之一:客户端的一切外部信息来源,都是它当前登录的那台服务端。
安装包和升级清单都来自那台服务器
Section titled “安装包和升级清单都来自那台服务器”- 客户端只请求
GET /api/client/v2/updates/manifest拿清单,安装包也从同一台服务端的/updates/client/*取。它不访问任何分发服务器、不访问 GitHub、也不需要知道自己属于哪个渠道。 - 渠道身份由服务端在结构上决定:镜像里装的是哪个渠道的内容,服务端就发哪个渠道的清单与安装包。 客户端不携带渠道身份,也就不存在”客户端拿错渠道的清单、把自己洗成另一个渠道”这一类 版本错配——那是最严重的一类事故(品牌丢失、数据根被换掉、会话看起来”全没了”)。
- 客户端仍会做一次对账:清单里的
channel_id必须存在,并且在拿到服务端/api/client/v2/channel自报的渠道 id 后必须精确相等。不一致直接判清单非法、不重试—— 配置出错的镜像当场暴露,而不是静默放行。
同一台服务端还决定了别的东西
Section titled “同一台服务端还决定了别的东西”模型目录与输入模态、能力中心的内容与授权、连接器目录、应用目录、账号与余额、审计——都来自这台服务端。 换服务端或换账号 = 换了一整套上下文,所以客户端会整份丢弃上一台服务端的在飞状态:
- 在飞的检查与安装包下载立即作废(连”失败”都不记到新服务端头上);
- 已经下载好、等待安装的安装包被撤回;
- 把安装包交给平台安装器之前会再复检一次归属(当前源的清单 + 该版本的 SHA-256 + 平台容器魔数), 复检不过就撤掉待安装状态,绝不把一份来路不明的二进制交给安装器。什么都不装的失败,比装错轻。
没连服务端时
Section titled “没连服务端时”没有更新源,也就不做任何外发检查——单机使用场景下客户端不发一个更新请求。 这不是”检查失败”,是”还没有可问的对象”,界面文案也与网络错误区分开。
- 设置:命名空间就是 profile 里的条目 id(语言、主题、自动化任务、连接器、浏览器、
语音准备面、关于与更新等各自一行)。上游已退役旧的
settings.yaml单体文档:它只在启动时 被导入一次然后改名。 - 账户页:当前账号、服务端地址、余额与用量(来自
/api/client/v2/auth/usage:balance_money/balance_enabled/balance_monthly/balance_mode、本月与今日的 用量与费用、以及是否管理员)。从未入账的账号不渲染余额行(balance_activated为 false)。 员工唯一可花的钱就是账户余额。 - 修改密码:
- 只有本地账号显示内联改密表单;LDAP / OIDC 用户由企业 IdP 管理密码,界面只给提示;
- 新密码至少 10 位,且不能与旧密码相同;
- 改密成功后服务端会吊销该账号的全部令牌,本地会话随之清除,需要重新登录;
- 管理员重置密码后可强制下次登录改密,服务端会以
403 PASSWORD_CHANGE_REQUIRED拦住其它请求。
- 退出登录:
POST /api/zeokeel/auth/logout。退出后更新源消失(见上一节),各面板按新的会话身份重新解析。
什么时候检查
Section titled “什么时候检查”- 打包版启动 60 秒后首次检查,之后每 6 小时一次。
- 托盘「检查更新…」与「设置 → 关于」可手动检查。手动检查即使已是最新也给出结果, 失败也会如实告诉你”稍后重试”,而不是静默。
检查与传输各自有界重试
Section titled “检查与传输各自有界重试”| 操作 | 缺省退避 | 尝试次数 | 是否抖动 |
|---|---|---|---|
| 清单检查(后台) | 2 / 8 / 20 秒 | 3 | 是(确定性抖动 0.25,避免所有客户端同一毫秒重试) |
| 安装包传输 | 2 / 8 / 20 / 30 / 30 秒 | 5 | 是 |
| 清单检查(手动) | —— | 1 | 让用户对着”正在检查更新…”等一分半,比直接说”失败了,再点一次”更糟 |
只重试瞬时故障:请求抛错、5xx/408、空响应、超长响应、哈希不符(=被截断)、续传时的连接中断。 不重试的情况:4xx、“服务端没有这个平台的包”、“安装包格式不对”、用户取消, 以及清单结构 / 渠道不符(重试改变不了结果)。退避期间界面显示「第 n/N 次 / 还有几秒」, 倒计时按绝对截止时刻现算,所以它真的会走。
清单是安全边界,任何结构不符都直接判非法:
schema必须为 1;channel_id必须存在且(拿到服务端自报值后)精确相等;- 下载地址必须是绝对 https;
sha256必须是 64 位小写十六进制;安装包按它做流式校验;- 落盘前还检查平台容器魔数(macOS DMG 尾部
koly、Windows PE 头、Linux AppImage 在偏移 8 处的AI\x02);单份安装包上限 1 GiB。
任何一步不通过都不安装,失败保持当前版本。
- 后台静默下载:发现新版本就开始传,全程不弹对话框;传输是续传(未完成的字节留在
.partial,旁边记一份 sidecar:来源 URL、SHA-256、已收字节、验证器),重试不浪费已下的字节。 - 传输有界:60 秒没有任何字节进展即判定停滞(按可重试的网络故障处理,保留续传链); 另有一个 attempt 级绝对预算兜住”一直在慢慢发但永远发不完”的对端。
- 下载完成的安装包可复用:状态记在
state.json(downloadedVersion/downloadedPath, 上限 4 KiB、0600),重启或换源后会先按清单 SHA-256 + 魔数校验磁盘上那份完成件, 命中就直接进入「可安装」,不发第二次下载请求。 - 安装一律由用户显式触发(托盘「安装更新」/ 会话头部徽标 / 设置-关于)。产品不做静默重启安装。
各平台行为:
- Windows:拉起下载好的 NSIS 安装器,然后请求有序退出;
- macOS:打开下载好的 DMG,提示你把「应用程序」里的旧版本替换掉;
- Linux:
chmod +x并提示你替换当前 AppImage(AppImage 没有静默自安装)。
失败时的表现
Section titled “失败时的表现”lastError 的每个类别都有明确文案,不会把不同原因压成同一句「网络不可达」:
| 类别 | 含义 |
|---|---|
network | 网络不可达(重试预算已用尽) |
not-signed-in | 尚未登录,没有可问的服务端 |
server-unavailable | 服务端能连上,但它给不出安全的下载地址(部署未配对外 https 地址) |
release-missing | 清单里没有这个平台的安装包 |
checksum-mismatch / invalid-artifact | 校验不一致 / 安装包格式不对 |
storage | 本地永久失败(磁盘满、权限、只读挂载)——重试不会成功 |
unsupported | 该平台没有安装包约定 |
平台资产与签名
Section titled “平台资产与签名”只出三个平台的安装包:Windows x64 的 NSIS 安装器、macOS Apple 芯片的 DMG、Linux x64 的 AppImage(企业交付面不含 deb)。
- macOS 正式版已签名并公证;
- Windows 安装器与 Linux AppImage 未签名——Windows SmartScreen 可能提示「未知发布者」;
- 更新链路校验的是下载容器的完整性,不是发布者身份。企业侧的静默部署通道是 MDM / GPO / SCCM 直接推安装包,不需要改客户端代码。
插件管理与随包运行时
Section titled “插件管理与随包运行时”应用固定运行 desktop profile,托盘里没有「打开 DSH 终端 / 切换 Profile / 模式切换」入口,
CLI 也拒绝操作这个 profile。第三方插件通过 profile 的用户补丁层加入:
编辑数据根下的 cordis.patch.yml,按 Loader patch 语法追加一行(应用每次启动都会合并该层):
- insert: - id: my-plugin name: my-plugin-package插件改动后需要重启应用才进入 Loader 组合。数据根随渠道不同(见部署文档的渠道章节)。
客户端随包分发三样运行时,都在应用的 resources/runtimes/ 下、不进 asar:
| 运行时 | 用途 |
|---|---|
| Node.js 24(LTS) | agent 写 / 跑 JS;也是 plugin_manager 跑 pnpm 的宿主 |
| pnpm 11 | 上游「把能力打成 bundle,再用 plugin_manager 安装」这条路径的包管理器 |
| CPython 3.12 | agent 写 / 跑 Python;自带 pip |
规则只有一条,但很重要:应用启动时把 <resources>/runtimes/bin 前置到应用自己的 PATH,
只影响应用派生的子进程(agent 的 shell 命令、MCP stdio 服务、打包插件用的 pnpm),
不改系统环境。Python 的 pip install 目标与 .pyc 缓存被重定向进应用数据根,
不写进安装目录。于是 agent 可以直接写代码并运行 node / python3,
上游的 bundle 安装路径也不再因为缺少包管理器而失败。
从 registry 安装依赖需要出网;离线环境仍可安装工作区里的本地 bundle。
出站策略:默认不使用系统代理
Section titled “出站策略:默认不使用系统代理”客户端默认禁止使用任何代理:宿主机配置了系统代理、代理环境变量、PAC/WPAD 自动发现,
或者给了 --proxy-server,客户端一律直连。
| 层 | 开关 | 语义 |
|---|---|---|
| 真实进程环境变量 | ZEOKEEL_ALLOW_SYSTEM_PROXY=1 | 允许使用宿主机代理;显式 0 / false 也生效(双向开关) |
| 渠道包 | desktop.allow_system_proxy: true | 允许(只认严格布尔 true) |
| 缺省 | —— | 禁止 |
判定顺序是先到先得(环境变量 > 渠道包 > 默认禁止)。写在数据根 .env 分层里的开关结构上太晚:
Chromium 的开关必须在 app.whenReady() 之前 append,而 .env 分层要到 start() 才加载——
这种情况启动日志里会明说它被忽略了,而不是静默失效。
覆盖范围与代价:
- 一次启动期开关覆盖默认 session 与全部后建分区(内置浏览器、应用窗口),并压过宿主机显式
传入的
--proxy-server;逐个 session 设直连是不够的,分区仍会走代理。 - 主进程的 Node 栈默认直连;宿主机若显式设了
NODE_USE_ENV_PROXY,全局 dispatcher 会被换成直连 Agent。 - agent 的子进程(
curl/git/ MCP stdio)由父进程环境派生,所以代理名会被显式删除。 - 代价(有意接受):内置浏览器也失去系统代理。在”只有经代理才能出公网”的办公网里, 这类部署必须打开上面的退路。
- 管不到:L3/TUN、透明代理、VPN、出口设备。
- 窗口消失了:先看系统托盘——关闭窗口不是退出。
- 应用持续闪退:用
--export-diagnostics运行安装后的程序(见「主界面与托盘」), 它不启动 Host,直接给出诊断 ZIP 的绝对路径。 - 插件没有出现:确认补丁写在当前渠道的数据根下,并重启应用。
- 更新没有提示:后台失败是静默的;用托盘「检查更新…」看真实结论, 它会区分”已是最新""没有可问的服务端""服务端给不出下载地址”。
- 下载卡住:60 秒无字节进展会判停滞并按可重试处理;看到「第 n/N 次 / 还有几秒」说明在退避重试。
- 固定端口冲突:把端口改回
0(随机)或换一个空闲端口;改端口会触发一次有序重启。 - 工作区内修改报目录权限错(Windows):见「权限分档与工具审批」里的三条出路。
- 浏览器工具一直超时:先看浏览器窗口右下角——如果用户拿着控制权,模型侧收到的是明确的 「请交还控制权」而不是超时;确认控制权状态再排查页面本身。
- 开发者调试:调试端口(9223)被残留实例占用会导致复用错误的实例——排查前先清掉旧实例 (用 bracket 技巧避免误杀调用它的 shell)。