跳转到内容

桌面客户端

桌面客户端是员工每天面对的那一层。它是一个 Electron 应用,把一个固定版本的上游 DeepSeek Harness 运行时装进原生外壳:窗口、托盘、自动更新、登录页,不需要员工安装 Node.js,也不需要执行任何命令。

它同时是产品的主入口:能力中心、连接器、自动化任务、内置浏览器、语音输入、五轨记忆、应用中心 都以面板的形式长在这个客户端里,而不是网页。

层跑在哪负责明确不负责
桌面客户端员工机器(Electron 应用)原生窗口与托盘、登录页与账户、自动更新、以及全部用户界面面板(对话、能力中心、连接器、自动化任务、浏览器、语音、记忆、应用中心)不做模型调用与计量,不保存账号凭据,不决定员工能看到什么内容
本地 Harness 服务员工机器,127.0.0.1 上的随机端口(可固定)跑固定版本的上游 DSH:会话与消息日志、文件读写、工作区、沙箱与审批、工具执行不出网做模型请求之外的判断;不持有账号、角色、额度
企业服务端客户内网一台机器(容器镜像)认证与账号、模型网关与计量计费、能力中心与审批、连接器目录、应用中心、审计、客户端分发、门户与管理后台不接触员工本机文件、不接触员工本机会话日志

这条分工决定了后面所有的边界:

  • 数据留在本机的那一半:会话、工作区文件、工具执行、沙箱状态、语音音频都在员工机器上; 服务端只看到账号、模型用量与审计事件。
  • 控制面在服务端的那一半:模型目录(可用哪些模型)、余额与额度、能力中心的内容与授权、 连接器目录、应用目录,全部由服务端决定;客户端只是把它们画出来。
  • 模型请求的路径:登录 → 拉取模型清单(/api/client/v2/config/bootstrap)→ 请求打到 服务端的 /v1/chat/completions → 服务端过限流与余额闸门 → 按模型匹配上游 provider → 计量与余额扣减同事务。上游 API Key 只存在于服务端,客户端从头到尾拿不到它。
  • 原生窗口,沙箱渲染进程: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一组 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、改端口、改用户补丁层,都需要重启应用才生效;应用不会在活的渲染代际里热换根槽
  • 一条会话 = 一段独立上下文 + 一个工作区(项目目录)。工作区决定沙箱的可写范围、 记忆的项目轨归属、以及模型能用哪些项目级技能;会话列表、搜索与恢复沿用官方 Harness 语义。
  • 模型清单由服务端下发。客户端把 <服务端>/v1 作为模型端点,模型是否可用、输入模态 (是否收图)、单价与折扣都由管理后台配置;客户端不做本地模型配置。
  • 权限是会话级事实,不是全局开关。新建会话时会把当时的默认预设钉进这条会话的日志; 之后在设置里改默认值,只影响之后新建的会话。想让某条正在用的会话改档,用输入框旁的权限 按钮或会话内命令,不要靠「新建一条会话」——同一工作区里的空白会话可能被复用,而复用不看权限。

上游的权限模型是两个旋钮:沙箱模式 + 审批策略。产品把它们捆成「预设」呈现。

档位(沙箱模式)可写范围典型用途
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" 明确不支持。
  • 未登记的键一律报错,不会被静默忽略——少写一个字母不会被当成默认值。

authMode 默认为 auto,判定顺序是先到先得:

  1. 声明了凭据字段(tokenFields)⇒ 走静态 token(请求头里用 ${字段} 引用);
  2. 否则有 streamable-http 端点 ⇒ 走 OAuth(discovery 地址默认就取那个端点本身,也可用 auth.discoveryUrl 显式指定);连接时先探测:2xx 视为公开端点,401 按 WWW-Authenticate 的声明走 RFC 9728 → RFC 8414 发现 + 动态客户端注册 + PKCE 回环回调;
  3. 都没有 ⇒ 免凭据。
情形结果
声明进程引导变量(PATH、NODE_OPTIONS、LD_PRELOAD、PYTHONPATH、BASH_ENV…)拒收。DSH_ / ELECTRON_ / PICOAIDE_ / 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/pico/connectors 列出,/api/pico/connectors/<id>/<动作> 执行 connect / cancel / auth-submit / state / disconnect / approve / deny / refresh。 除状态轮询外的每一个动作都要求持有性证明,拿不到就拒绝进入处理函数。

侧边栏「更多」→「自动化任务」打开官方 Schedule 任务中心(0.2.1 起替换自研 cron)。 任务由 Host 调度、绑定到创建它的会话;到点把提示词发回该会话。

字段说明
名称必填,纯空白视为缺失(界面与模型面用同一条判据)
时间规则单次 / 固定间隔 / 每日 / 每周 / 5 段 cron;时区按创建时的本地时区记录
提示词到点发给原会话的内容;它是数据,不是命令行
会话固定绑定创建任务的会话,不能跨会话投递
状态活跃 / 已结束;可在任务中心查看、编辑、删除
执行记录触发时间、投递消息 id、最近一次投递;超出保留范围的旧记录会清理
  • 到点由 Host 进程投递;窗口关闭但应用仍在运行时照常触发;应用完全退出期间不触发。
  • 下次启动后,属于当前登录账号的到期任务会被补触发(不是全部历史)。
  • 任务目录、历史、更新与删除都按当前 Pico 账号(server + username)过滤: 其他账号的任务不可见、不可改,也不会被投递;未归属的历史任务在账号环境下同样不可见。
  • 应用完全退出或账号未登录期间不会向他人会话投递。

模型可以调用四个工具:schedule_create / schedule_list / schedule_update / schedule_delete。 边界与界面同源:按账号过滤(看不到的任务等同于不存在);用户始终能在界面上看到并管理这些任务。

Agent 驱动的浏览器是一个独立的 OS 窗口,不是主窗口里的 iframe。

  • 多标签:每个标签是一个 WebContentsView,跑在按用户分区的持久 partition 里 (persist:agent-browser-<用户>),所以会话存储、登录态与主窗口互相隔离。标签上限 16。
  • 地址栏:工具栏提供 URL 输入、后退 / 前进 / 刷新、关闭标签。
  • 创建即隐藏:窗口建出来是隐藏的,只有用户动作才显示;它也不会自己弹到前台—— 所有会抢焦点的动作都要先过一道「窗口可见 + 未最小化 + 有焦点」的闸门。

这是这一层最容易被误设计的交互,因此写死成规则:

  • 只有右下角的「我来操作」按钮能取得控制权;取得之后同一个胶囊变成「交给 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/pico/wasm-apps/open;平台侧唯一入口 POST /api/client/v2/apps/wasm/:app_id/request,带员工令牌转发执行。
  • 窗口隔离:应用窗口跑在按用户分区的 session 里;导航、子帧导航与 302 重定向 共用同一个 origin 判据——跨应用与外站一律拦、同应用放行。
  • 保留面:应用页面里 /__picoaide/ 前缀是宿主保留命名空间,目前只有 /__picoaide/ai/chat 一条路径存在(由客户端本地处理,绝不转发平台);其余一律 404。

应用配置 picoaide.app.json 的 access 决定谁能打开:

值语义
public匿名可达
login(缺省)必须登录
whitelist名单制;名单上限 2000,access=whitelist 时名单必须非空,否则发布被拒

平台只把身份与访问模式注入帧,名单比对由应用自己读取自己的配置并判定——平台不替应用做名单决策。

  • 每个应用有自己的数据库;作者可以在应用中心查看只读的表结构与行数据。
  • 「允许 AI 读取此应用的数据」是一条独立授权,记账键是 用户 ⊕ 服务端地址 ⊕ 应用:换账号、换服务端都会重新问一次;旧版本的授权记录一律 按未授权处理,升级后需要重新授权一次。
  • 应用面板里的请求体上限 1 MiB、响应体上限 1 MiB、请求头最多 24 个、 单个头值 8 KiB、单次请求超时 75 秒。

这是这一层的核心设计之一:客户端的一切外部信息来源,都是它当前登录的那台服务端。

安装包和升级清单都来自那台服务器

Section titled “安装包和升级清单都来自那台服务器”
  • 客户端只请求 GET /api/client/v2/updates/manifest 拿清单,安装包也从同一台服务端的 /updates/client/* 取。它不访问任何分发服务器、不访问 GitHub、也不需要知道自己属于哪个渠道。
  • 渠道身份由服务端在结构上决定:镜像里装的是哪个渠道的内容,服务端就发哪个渠道的清单与安装包。 客户端不携带渠道身份,也就不存在”客户端拿错渠道的清单、把自己洗成另一个渠道”这一类 版本错配——那是最严重的一类事故(品牌丢失、数据根被换掉、会话看起来”全没了”)。
  • 客户端仍会做一次对账:清单里的 channel_id 必须存在,并且在拿到服务端 /api/client/v2/channel 自报的渠道 id 后必须精确相等。不一致直接判清单非法、不重试—— 配置出错的镜像当场暴露,而不是静默放行。

同一台服务端还决定了别的东西

Section titled “同一台服务端还决定了别的东西”

模型目录与输入模态、能力中心的内容与授权、连接器目录、应用目录、账号与余额、审计——都来自这台服务端。 换服务端或换账号 = 换了一整套上下文,所以客户端会整份丢弃上一台服务端的在飞状态:

  • 在飞的检查与安装包下载立即作废(连”失败”都不记到新服务端头上);
  • 已经下载好、等待安装的安装包被撤回;
  • 把安装包交给平台安装器之前会再复检一次归属(当前源的清单 + 该版本的 SHA-256 + 平台容器魔数), 复检不过就撤掉待安装状态,绝不把一份来路不明的二进制交给安装器。什么都不装的失败,比装错轻。

没有更新源,也就不做任何外发检查——单机使用场景下客户端不发一个更新请求。 这不是”检查失败”,是”还没有可问的对象”,界面文案也与网络错误区分开。

  • 设置:命名空间就是 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/pico/auth/logout。退出后更新源消失(见上一节),各面板按新的会话身份重新解析。
  • 打包版启动 60 秒后首次检查,之后每 6 小时一次。
  • 托盘「检查更新…」与「设置 → 关于」可手动检查。手动检查即使已是最新也给出结果, 失败也会如实告诉你”稍后重试”,而不是静默。
操作缺省退避尝试次数是否抖动
清单检查(后台)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 没有静默自安装)。

lastError 的每个类别都有明确文案,不会把不同原因压成同一句「网络不可达」:

类别含义
network网络不可达(重试预算已用尽)
not-signed-in尚未登录,没有可问的服务端
server-unavailable服务端能连上,但它给不出安全的下载地址(部署未配对外 https 地址)
release-missing清单里没有这个平台的安装包
checksum-mismatch / invalid-artifact校验不一致 / 安装包格式不对
storage本地永久失败(磁盘满、权限、只读挂载)——重试不会成功
unsupported该平台没有安装包约定

只出三个平台的安装包:Windows x64 的 NSIS 安装器、macOS Apple 芯片的 DMG、Linux x64 的 AppImage(企业交付面不含 deb)。

  • macOS 正式版已签名并公证;
  • Windows 安装器与 Linux AppImage 未签名——Windows SmartScreen 可能提示「未知发布者」;
  • 更新链路校验的是下载容器的完整性,不是发布者身份。企业侧的静默部署通道是 MDM / GPO / SCCM 直接推安装包,不需要改客户端代码。

应用固定运行 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.12agent 写 / 跑 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,客户端一律直连。

层开关语义
真实进程环境变量PICOAI_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)。
  • 快速开始:从拿到服务端地址到发出第一条消息。
  • 系统架构:三层的进程、数据与接口边界。
  • 企业管控面:模型、额度、能力中心审批、连接器目录、应用中心这些服务端侧的配置。
  • 应用中心:自建应用的配置契约、发布链路、运行时边界与失败行为。
  • 安全与合规:密钥、令牌、沙箱、出站边界与审计的实现细节与残留限制。
  • 客户端分发与升级:服务端如何下发安装包与升级清单。
  • 渠道与白标:数据根、深链 scheme、名称与素材是怎么随包决定的。
  • 部署总览:镜像、证书、备份与回滚。
  • 插件开发:自己写一个插件装进客户端。
  • API 参考:客户端调用的服务端端点。
  • 常见问题。