Native Shell
Native Shell: AI-generated UI, native in your app, on every platform. Apps on iOS, Android, macOS, Windows and Linux, GPUI apps and web pages load signed, sandboxed TSX modules at run time and draw th
小克 —— Claude Code 一键配置工具,接入 DeepSeek 模型。Windows / macOS 客户端下载,Linux 服务器一条命令安装,注册即送 ¥3 体验额度。
Translation pending
Developer toolsPython · ShellGitHub @Qy-115200OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
跟 AI 说一句话,Claude Code 就装好了 —— Windows / macOS / Linux 三平台的 Claude Code 一键安装、配置与用量管理助手
Translation pending
Taken from the repository README
Loading the full README.
Read the full README ↗Linux / 服务器没有图形界面,也是一行装完:
curl -fsSL https://aikaui.com/install.sh | sh
装 Claude Code 本可以五分钟结束,但国内用户实际要过这几关:装 Node(MSI 安装包还经常 以 1603 失败)、npm 全局目录不在 PATH 里、地区限制、中转地址写在哪、VSCode 插件、 第一次启动该用哪个工作目录……每一步都能卡住一个不太折腾电脑的人。
小克把这件事交给一个会自己看环境、会调工具的 Agent:它检测、它下载、它写配置、它问你 「以后在哪个文件夹里干活」,装完把 VSCode 或终端打开。每一步都用大白话解释一句,要装东西 之前先问过你,装到一半不耐烦了可以点「中断」。
它同时是个能聊的助手:查余额、看用量图表、扫码充值,都在同一个窗口里。窗口左边一条导航栏, 右边三个页面:💬 对话、📊 用量信息 和 🔐 账户信息。
claude、怎么写 settings.json、凭证存在哪,
都能在仓库里读到,没有藏在安装包里的黑盒步骤。api_key 存本机(Windows 走 DPAPI,POSIX 是 0600
文件),本机密钥和设备指纹一起做「同一台机器找回同一个号」的双验证。装 Claude Code(一键完成)
底部一个「一键安装 Claude Code」按钮,或者直接说"帮我装 Claude Code"。Agent 按固定顺序 调前四个工具:领 token → 绕过地区限制 → 写配置 → 建桌面快捷方式,然后问清楚工作目录, 最后二选一收尾:装好 Claude 的 VSCode 插件并用 VSCode 打开(默认),或者开一个新终端。 中途每一步都会用大白话跟你解释一句,要装东西之前会先问过你。
装到一半不耐烦了可以点「中断」(2026-10-31 加)
装 winget 要下 58MB、走官方安装包那条路要下 240MB,几分钟很正常。输入框右边有一个 「中断」按钮,平时是灰的,Agent 一开工就亮起来。点了会同时做三件事:
agent_bus.cancel_event)—— Agent 循环每走一步、每个下载循环
每 1MB 都会看一眼,看见就停;kill_active_children(),Windows 上 taskkill /F /T),
不然 winget / powershell / 安装包能自顾自跑几分钟,按钮就成了摆设;已经装到一半的东西不会回滚,点完的提示里也这么写。下一轮任务开工时牌子会自动摘掉
(agent_worker / 服务器版每轮调 clear_cancel())。服务器版对应的是连按两次 Ctrl-C
(第一次只是「上膛」),同一个牌子、同一套收尾。
装完不自动关窗口(2026-10-31)。以前安装流程走完会留 10 秒,然后助手自己退出;
用户明确说不要 —— 装完正是最想回头问两句的时候("这个面板怎么用""项目放哪"),
助手自己关掉最招人烦。install_done 这个事件还在(它仍然是"这一轮真把用户领进门了"
的唯一判据),只是界面拿到它什么都不做。关窗口只有两个办法:点右上角 X,或直接说再见。
顺带的好处:助手常驻之后,PyInstaller 报的
Failed to remove temporary directory: …\_MEI… 也不会再被"自己先退出、VSCode 还占着目录"触发。
先定工作目录,再启动
Claude Code 只看得到你给它的一个文件夹。所以启动之前 Agent 必须先问清楚"以后在哪个
文件夹里干活"(ask_user 给三个选项:桌面新建一个 / 用户主目录 / 自己指定路径),
再把这个目录传给 open_vscode(directory=…) / launch_claude(directory=…) 当 cwd。目录不存在会建出来,越界(跑到
用户主目录外面)会被退回主目录并在日志里说明原因 —— 跟 read_file / write_file 是同一条规则。
启动完还会把这件事再讲一遍:一个项目一个文件夹,换项目就在 VSCode 里打开那个文件夹(或在那个文件夹里重新启动)。
装完接到 VSCode:检测 → 问 → 装 / 装插件 → 打开
装完 Claude Code、定好工作目录之后,Agent 先 check_vscode() 拿事实,再按情况只问一次:
| 情况 | 问什么 | 同意之后 |
|---|---|---|
| 没有 VSCode | 先讲一句好处(改了什么在编辑器里直接看到、能一键撤销),问要不要装 | install_vscode() → install_claude_extension() → open_vscode() |
| 有 VSCode 没插件 | 问要不要装 Claude 插件 | install_claude_extension() → open_vscode() |
| 都有 | 不问 | 直接 open_vscode() |
用户说"不用",就退回 launch_claude() 开终端,之后任何一轮都不再提 VSCode。
两个入口只能开一个,绝不都开。
打开 VSCode 时关掉「工作区信任」门禁(2026-10-31)。助手是替用户新建的目录,
VSCode 一定把它当成"未信任",先弹一个「Do you trust the authors of the files in this
folder? … Creating a terminal process requires executing code」。在用户点「信任」之前,
扩展跑在受限模式里:Claude 面板出不来、连终端都开不了 —— 用户看到的现象是"插件装了却没开"。
所以 open_vscode() 一律带 --disable-workspace-trust(VSCODE_NO_TRUST_FLAG):
只对这一次打开的那个窗口生效,不改任何全局设置,用户之后自己打开别的文件夹照样会被问。
带开关就得走 CLI(直接起 Code.exe 时参数会被转手给已运行的实例,开关不生效)。
装 VSCode 本身:Windows 走 winget --scope user(不弹 UAC),macOS 走 brew install --cask,
Linux 只打开官网下载页(apt/snap 要 sudo,助手不替你提权)。装不上就打开下载页并如实说明;
插件市场连不上也一样,照样用 VSCode 打开,让用户在扩展面板里手动装。
机器上没有 winget 时先把它装上(ensure_winget(),2026-10-30 加)。原因很实在:
Windows 10/11 的家庭版精简镜像、老 LTSC、服务器版都可能不带 App Installer,
走到 install_vscode() 就直接死在「这台机器上没有 winget」,用户被卡在装到一半。
platform_support/windows.py 里)。不走 GitHub:2026-10-30 实测国内直连
objects.githubusercontent.com 是 0 字节,而 winget 的 msixbundle 有 200MB+。
这里只取 bundle 里 x64 的那一份,内容与微软发布的完全一致,没有做任何改动。Microsoft.UI.Xaml.2.8 和 Microsoft.VCLibs.140.00.UWPDesktop,微软把它们单独放在
DesktopAppInstaller_Dependencies.zip 里。漏了这一步,主包可能装不上。
两个包(6.7MB + 5.1MB,sha256 钉在 WINGET_DEPS 里)下到
%TEMP%\aikaui-winget-deps\,大小和 sha256 都对得上就复用、不再走网络。
依赖装不上不直接放弃(机器上可能本来就有),主包照样试;两个都失败才报两句。
Add-AppxPackage 的 0x80073D06 / 0x80073CFB / 0x80073D02(更高版本已装 / 同版本
已装 / 资源包已注册)都算成功 —— 重装依赖时天天撞见,不认它就是误报。Add-AppxPackage 的参数名只能是 -Path(2026-10-31 查清,这是那个老 bug 的真正原因)。
它没有 -LiteralPath;写上它 PowerShell 当场抛
ParameterBindingException / NamedParameterNotFound, Microsoft.Windows.Appx.PackageManager.Commands.AddAppxPackageCommand,整条命令一句都没执行。
表现就是用户日志里的「Add-AppxPackage 返回 1」,然后一路退到 240MB 的官方安装包 ——
走得通,但慢得多。-Path 认通配符,所以路径里真带 [ ] ` 时会先
Set-Location -LiteralPath <目录> 再只传文件名。+ ~~~~ 位置标记,真正有信息的第一行
Deployment failed with HRESULT: 0x8007… 反被丢了;而且硬按 UTF-8 解码,
中文路径 张三 在用户日志里变成 ????(中文 Windows 的 PowerShell 写 GBK)。
现在 _summarize_powershell_error():先去掉纯装饰行,保留第一行,再补一条带
0x8… 的、一条 CategoryInfo、一条 FullyQualifiedErrorId;解码走
smart_decode()(UTF-8 → 本地编码)。%PUBLIC%\aikaui\(纯 ASCII 路径)
再装 —— AppX 部署对非 ASCII 路径有过 IO 报错,ASCII 副本是绕开它的最省事办法。
_ascii_copy() 只在原路径含非 ASCII 时才做,装完(无论成败)删掉副本。
两次都失败就报两段的错误。Add-AppxPackage(用户级、不弹 UAC、不改系统策略);装不上就认。
2026-10-31 删掉了「解压便携版」那条路:便携版没注册成 AppX 包,源索引永远打不开,
winget install 必然 0x8A15004B。以前这里会解压一份、打一行「已装好 winget
(便携版,解压即用)」,下一句紧接着就是「读不到软件源,改用官方安装包」——
白等十几秒,日志还自相矛盾。现在直接返回失败原因,install_vscode() 跳第 3 条。Get-AppxPackage Microsoft.DesktopAppInstaller 可能注册着,但版本是 1.0.30251.0 ——
winget 出现之前的那版,包里根本没有 winget.exe(winget 要 1.11 以上才带)。
这时候日志里会多一句「太老…在 Microsoft Store「库 → 获取更新」里升级它能一劳永逸」,
而不是只有一句看不懂的「装不上」。这是用户真机上撞到的情形。winget 装上了不等于能用(2026-10-30 第二个坑)。刚装好的 winget 没有源索引,
winget install 会当场返回 0x8A15004B(SourceOpenFailedSuggestion)——
用户在真机上撞到的就是这个:包下完了、winget 也装上了,然后报
winget 返回 2316632139。所以 install_vscode() 现在是三条路依次试:
winget install。失败时只有看着像源的问题(SourceOpenFailed / source)才
winget source update,不行再 source reset --force + update,然后重试一次。
超时之类的失败不重试 —— 那等于把上限翻倍。ensure_winget() 装上(上面那 58MB),再走第 1 条。
ensure_winget() 失败(Add-AppxPackage 两次都没成)就直接跳第 3 条 ——
不再解压便携版,理由见上。update.code.visualstudio.com/latest/win32-x64-user/stable,约 240MB),
/VERYSILENT /MERGETASKS=!runcode 静默装,退出码 3010(要求重启)也算成功。
这条和下载页给用户的是同一个文件,签名也是微软的。
下载走 6 路分段(2026-10-31 加,见下),240MB 从 9 分钟变一分半。三条都不行才打开下载页,并把每一步卡在哪如实说出来(on_note 会往对话里打行)。
240MB 那个安装包用 6 路并发下(_download_parallel(),2026-10-31 加)。
起因是用户的原话「直接下载微软官方的安装包花的时间太久了」:微软那个 CDN 单条连接
只有 ~450KB/s,240MB 要 9 分钟。先问一次
update.code.visualstudio.com/api/update/win32-x64-user/stable/latest(几百字节的 JSON)
拿到带版本号的真实地址和 sha256hash,再用 Range 把它切成 6 段并发下。
本机实测 2.64MB/s、90.9 秒下完 251,590,472 字节、sha256 对得上(快 6 倍)。
分段拿不到(代理拦了 Range、服务器不支持)就退回单连接那条老路 —— 慢,但通;
退回去之后仍然按 API 给的 sha256 校验,因为"某一段少了一截"的文件头照样是 MZ。
用户点中断时不会偷偷再下一遍。
find_winget(verify=True) 的顺序是 PATH → 执行别名 → 最后问一次 AppX 注册表,
前面那几个候选手到之后都会真跑一次 --version:WindowsApps 下的执行别名可能是空壳
(文件在、包没为这个用户装,Popen 直接 WinError 3),不验就会拿一个跑不起来的路径去装东西。
便携版那条候选删掉了(2026-10-31):老的解压目录就算还在,拿它装东西也必然失败。
最后那道 _winget_from_appx_registration()(2026-10-31 加)是不再靠猜路径的兜底:
问系统 (Get-AppxPackage Microsoft.DesktopAppInstaller).InstallLocation 拿到包真正装在哪,
再用那里的 winget.exe。加它的原因很实在 —— 用户机器上 winget 明明是好的
(Get-AppxPackage 报 1.29.380 / Ok,别名跑 --version 回 v1.29.380),我们却判定
「没有 winget」,于是白下 58MB、白装一遍(还失败)、再白下 240MB。上面那些候选全靠猜:
PATH 最前面那条 WindowsApps 可能属于另一个账户(本机就是,Administrator 的空壳),
%USERPROFILE% 也可能不是你以为的那个目录(本机 USERNAME 是中文名,profile 却是
C:\Users\abc)。问 AppX 注册表则没有「猜」这回事。
open_vscode() 不只是开窗:它把 Claude 面板也一起打开。VSCode 命令行没有 --command
这种东西,所以走的是插件自己注册的 URI 入口 vscode://anthropic.claude-code/open
(插件 extension.js 里的 registerUriHandler 接到它就执行 claude-vscode.primaryEditor.open)。
两个细节:① 只在 --list-extensions 里确认插件存在时才发链接 —— 否则 VSCode 会弹一个
"无法打开链接"的报错框;② 本流程刚装完插件会先开一个新窗口(已开着的窗口是在装插件
之前加载扩展的,里面没有它),等 4 秒让插件宿主注册好 URI 处理器再发。
面板没能自动打开时,返回里会写清楚,让用户点左边活动栏的 Claude 图标 —— 不会假装成功。
检测不依赖 PATH:Windows 先查注册表(App Paths → Applications\Code.exe → vscode 协议),
再查常见目录,最后才是 which。这样装在 D:\vs code 这种非默认位置也找得到。
CLI 一律用安装目录里的 bin\code.cmd(Code.exe --list-extensions 是空输出),
Code.exe 只用来开窗;所有 VSCode 子进程都先过 clean_env() 摘掉
ELECTRON_RUN_AS_NODE / VSCODE_*(带着它们 Code.exe <目录> 会当 node 脚本跑完就退,
不报错也不弹窗)。
对话式的「小克」
不装东西的时候它就是个普通助手,问它什么都行。系统提示词里写死了:用户只是在打招呼或 提问时,不要自作主张去装东西。
📊 用量信息页
跟钱有关的东西只有这一页:余额(大数字,低于 ¥2 变橙、低于 ¥1 变红)、今日/本月消耗、 两张按时间分布的 token 用量柱状图(上 flash、下 pro)、右上角的「充值」按钮。数据每 30 秒 自动刷一次,这一页也有「刷新」按钮。
两张图各画在 tk.Canvas 上,上下叠着:今日按小时、本月按天(服务端按北京时间分桶),
单色(刻度写在轴线下面,颜色不承担区分职责,也就不需要图例)、柱顶直接标数值、
鼠标悬停出提示框。时间桶多时只每隔几个画一个刻度(从最后一根往回数,保证当前小时 /
今天一定有标签)。卡片标题就是模型名,右上角写这一期间的合计,所以不靠悬停也能
知道每个模型花了多少。模型按名字里的 flash / pro 挑,某个模型当期没用量也留着
那张空图("暂无数据"),不会两张图变成同一个模型。色板跑过对比度校验
(#3987e5 在卡片底色 #1f1f1f 上 ≥3:1)。
后端
/api/usage的today/month里各带一个series_by_model({模型: [按小时 / 按天的序列]})。两张图吃series_by_model;老服务端只给series/by_model时,客户端会退化成每个模型一根总量柱子,并注明 "服务器暂未按模型拆分时间",不会白屏 —— 详见下面「服务端接口」一节。余额不足时点服务器给的
aikaui://recharge链接,助手先显示这一页(让人看清钱花在 哪了),数据回来后再自动弹充值窗;充值窗停在主窗右边,不把图表整个盖住。
余额卡下面那行账户码只露前 4 位(完整凭证在「🔐 账户信息」页):这一页是用户最容易 截图发给别人的地方。
🔐 账户信息页
账户码和账户密钥在这里,默认打码(CC-D************ / ************************)。
每行有「👁 显示」按钮,点一下才看明文;再点变回打码,切走再回来也是打码 —— 明文状态
不做任何持久化。旁边是「复制」,只写系统剪贴板,不写日志(日志是用户会截图的地方)。
两串的分工:账户码"找到哪个账号",账户密钥"证明这个账号是你的"。换设备登录要两样 一起填(见下面「换设备登录要两样」)。
下面一行红字是这一页存在的理由:
⚠️ 重要:这是您找回账户和余额的唯一凭证,请截图保存在安全的地方。切勿发给他人,否则余额将被盗用。
打码只是"别被路过的人一眼看走",不是加密:明文就躺在本地凭证里。所以那句警告是必需品, 不是装饰。
余额见底只问一句,不主动弹充值页
余额跌破 ¥1 时,程序不会自己打开充值窗口,只在窗口底部冒一条暗琥珀色的 "⚠️ 余额只剩 ¥x.xx,是否立即充值?"(服务器真的说"余额已用完"时同理)。点「立即充值」 才弹充值窗,点「暂不」就收起来,并提示"需要充值随时点左边「用量信息」→「充值」"。 一次会话只问一次(服务器明确报余额用完时那次除外)。
402 有两种,不能混成一句「余额已用完」
new-api 在请求开始前按「上下文 + max_tokens」做预扣费估算,估算值超过剩余额度就拒绝, 不等实际生成完再算。所以一次很大的请求(图片、长文件、大 max_tokens)完全可能被拒, 而余额还有几毛钱 —— 2026-10-28 就因此让用户充了不必要充的钱。两种情况的处置完全不同:
服务器回的 code | 真实含义 | 界面怎么说 | 要不要劝充值 |
|---|---|---|---|
insufficient_user_quota | 余额真的没了 | 「账户余额已用完」 | 要 |
quota_exceeded_for_request | 余额还在,这次请求的预估花费超了 | 「这次请求的预估花费超过了剩余余额(还剩 ¥0.75)」 | 不要,先让他把请求改小 |
llm.insufficient_balance() 返回 {"kind", "text", "remaining"},agent_core 只对
kind == "empty" 发 ("recharge", …) 事件。代理还会把真实剩余额度放在 remaining
(单位:元)里 —— 只要它大于 0,就算 code 还是老的那几个,也按第二种处理。
老代理只发一句中文、没有这些字段,那就按第一种走(和 v1.4.8 以前行为一致),
所以两边谁先上线都不会坏。
ask_user 选项交互
Agent 需要你拿主意时,不是把 "1. xxx 2. xxx" 打在日志里,而是渲染成可点的选项行: 点一下就直接当答案提交(没有"先选、再点确定"那一步),也可以 ↑↓ 移动选中、回车提交, 或者直接打序号。Esc 跳到自由输入。最后一行永远是「✏️ 输入点别的」。
关闭窗口 = 退出程序
三个平台都不做托盘。点右上角 X 就是真的退出,不会留一个找不到的常驻进程。 (以前 Windows 上"关窗缩到托盘"的行为已删除。)
字体是一处定义的
界面字体按平台挑:Windows 用 Microsoft YaHei UI(设置面板用的那一款,比普通雅黑字距更紧)、
macOS 用 PingFang SC、Linux 用 Noto Sans CJK SC;数字和订单号走等宽,
这样 token 数、金额、日志列与列之间是对齐的。字体是运行时按优先级问 Tk 要的,
一个都找不到才退到系统兜底字体。字号全在 ui/fonts.py 一张表里,改一处整个界面跟着变。
需要 Python 3(开发环境用的是 3.12.4),Windows / macOS / Linux 都行。
pip install -r requirements.txt
python main.py
首次运行会自动注册一个账号(服务器要求做一次 PoW 人机校验,通常一两秒),
凭证存到 ~/.claude-agent/identity.json。之后每次启动都直接用这份本地凭证,不再联网注册。
离线测试(不需要联网,也不需要 GUI):
python -m unittest discover -s tests
服务器上没有图形界面怎么办? 用服务器版(见下一节),它不需要 $DISPLAY。
服务器上没有 X11 / Wayland,python main.py 一进来就 import customtkinter,
ctk.CTk() 会直接抛 TclError。所以 Linux 发的是另一份产物:一个纯终端程序。
它和桌面版跑的是同一套 Agent 循环(agent_core.run_one_task),只是把界面换成了
终端对话:装 Node → 装 claude → 领账号 → 写配置这几件事由模型来决定顺序、怎么补救、
什么时候该问你,不再是一条写死的流水线。唯一的确定性环节是账号引导——模型客户端
本身就要 identity.json 才能说话,所以那几步由代码问、代码办。
终端里的约定:
↑/↓ 选、回车确认,Esc 跳到「自己打字」;Ctrl-C 要按两次:第一次只是「上膛」,2 秒内再按一次才中断。
已经开始的下载/安装不会被杀掉(子进程是独立会话),它会自己在后台跑完;claude;管道/容器里(--yes)不会,只打印怎么启动。一条命令装好(脚本会自动下 latest.json 里 linux-x64
那一条,校验 sha256,不需要 root):
curl -fsSL https://aikaui.com/install.sh | sh
装到 ~/.local/lib/claude-code-assistant/,软链成 ~/.local/bin/claude-code-assistant,
并往 ~/.bashrc / ~/.profile 写一段带标记的 PATH(# >>> aikaui >>>,重跑不会重复写)。
装完会跑一次 --version:跑不起来就退 4,不会留下一个"装上了但用不了"的状态。
装完直接启动(模型会一步步做,需要你拿主意的时候它才问):
claude-code-assistant # 首次运行:注册账号 → 写 ~/.claude.json → 提示怎么用
claude-code-assistant --account-code <账户码> --account-key <账户密钥> # 用已有账号绑这台机器
claude-code-assistant --yes # 全默认,适合写进 Dockerfile / cloud-init(需要模型可达)
claude-code-assistant --dry-run # 只打印要做什么,不下载、不写任何文件
claude-code-assistant --repair # 只对账配置:跟 identity.json 不一致就补齐(见下)
claude-code-assistant --balance # 查余额和用量
claude-code-assistant --recharge 20 # 充 20 元:在终端里打印付款二维码
| 参数 | 作用 |
|---|---|
--dir PATH | claude 的工作目录,默认 $HOME |
--account-code / --account-key | 绑已有账号(比在新机器上再注册一个更常见) |
--new-account | 上次注册没走完、留下了旧账户码时,明确放弃它重新注册 |
--node-version / --claude-version | 钉版本;默认 Node 取 LTS、claude 取 latest |
--yes / --force / --quiet | 不提问(提问时自动选第一项)/ 已装也重装 / 只输出错误;--yes 现在需要模型可达,见下 |
--repair | 只检查并修复 ~/.claude.json 和 ~/.claude/settings.json,不装 Node、不装 claude、不注册账号 |
--balance | 只查余额和用量(今日 / 本月 token),不装东西 |
--recharge 金额 | 创建充值订单并打印二维码;--channel wechat 可指定微信(默认支付宝) |
--allow-root | 1.4.4 起 root 默认就能装,这个参数只为兼容老脚本保留 |
--repair / --balance / --recharge 是"只做一件事"的模式,互相不能组合,
也不能和 --account-code / --new-account 一起用(那是安装路径的事)。
余额和充值(服务器版)。没有图形界面,所以付款走终端:
server/billing_console.py 把
server/api.py 拿到的数据变成几行字,二维码用 segno 画成方块 ——
黑字白底(深色终端里不这么做,黑白是反的,有些扫码器不认)。画不出来不影响充值:
没装 segno、或者服务器没给二维码内容时,退回打印 pay_url 和二维码图片地址。
余额跌破 ¥1(和桌面版同一条线)时,--balance 和装完最后那屏都会把两条路摆出来:
claude-code-assistant --recharge 20 —— 就地充,二维码直接打在终端里;~/.claude-agent/identity.json 的 account_key,只填进桌面版,别外传)。账户码、金额、订单号可以打;api_key / account_key 的值任何一行都不打印,
提示里只出现 <密钥> 这样的占位符。
它是分两层跑的: 上面这几件"装什么、怎么装"交给模型(工具在
server/agent_tools.py,提示词在
server/prompts_server.py),模型的顺序大概率是这四步;
--dry-run 打印的就是这条确定性参考路线(不联网、不调模型,退出码照旧)。
模型能用的 12 个工具:ensure_node / install_claude / configure_claude /
finish_install(验收闸门:Node、claude、账号、两份配置都在才算装好)、
bash / read_file / write_file / ask_user、query_balance / query_usage /
query_pay_methods / create_recharge。没有 VSCode、快捷方式、aikaui:// 协议、
login_with_account_code —— 账户密钥不进模型的上下文。
bash 有护栏:不许 sudo、不许 apt/dnf/yum/apk/snap install、不许自己 npm install
(那几条坑在 install_claude 里已经处理好了,让模型现编就是事故重演)、
不许读写 ~/.claude-agent/(里面有 api_key);子进程一律 stdin=/dev/null,
否则它会抢走终端的按键,用户看到的是"打字没反应"。
四步的细节(也就是那四个工具的实现在做什么):
~/.local 不可写,都会在下载任何
东西之前就失败(退出码 3)。root 不拦,只警告一句:东西全落在 /root 下,换个人用不了。node 主版本 ≥ 22 就复用;否则从 cdn.npmmirror.com 下
node-v<ver>-linux-<arch>.tar.xz,对 SHASUMS256.txt 校验,装到 ~/.local/node-v<ver>
再软链成 ~/.local/node。全程不 sudo。npm install -g --prefix ~/.local @anthropic-ai/claude-code --include=optional --allow-scripts=@anthropic-ai/claude-code。
--include=optional 不能省:真二进制在 optionalDependencies 的平台子包里。
--allow-scripts 也不能省:npm ≥ 11 默认不跑依赖的安装脚本,只警告一句
npm warn install-scripts … not yet covered by allowScripts,而这个包正是靠
postinstall 把二进制拷到 bin/。脚本一被拦,装完就是「added 1 package」加一个
500 字节的占位。参数写裸包名(npm 的匹配规则允许,钉版本会随发版失效);
npm 10 不认这个参数,但它只当普通配置忽略,不会报错。
另外 npm_env() 把所有继承来的 npm_config_* 全部清掉再显式设一遍,
防用户 .npmrc 里的 ignore-scripts=true / omit=optional。
原生包还是没下来时(132MB 断在半路),单独 npm install -g 平台子包并
自动重试 3 次;如果它其实已经在树里、只是没拷到位,就只跑一次
install.cjs,不重复下载。--account-code 走
bind_account();都没有才 register_device()(PoW 进度在终端里画一行)。
最后 bypass_region_check() + set_model_config() 写 ~/.claude.json 和
~/.claude/settings.json。模型连不上就直说连不上。 这是用户明确要的:不假装装好了、也不含糊成"安装失败"。
server/agent_install.py 按原因分开说:
| 原因 | 打出来的话 | 退出码 |
|---|---|---|
| 本机没有账号凭证 | 本机没有账号凭证,模型用不了 | 4 |
| 余额不足 | 账户余额不足(+ 充值命令) | 4 |
| 连不上模型服务器 | 连不上模型服务器(+ 先确认能打开 aikaui.com) | 1 |
| 模型服务器返回看不懂的内容 | 等一会儿重跑;一直这样请联系我们 | 1 |
| 跑满 40 步还没装完 | Agent 跑满了步数上限还没装完 | 1 |
| 用户按了两次 Ctrl-C | 已中断 | 130 |
配置自愈。两份配置里的 ANTHROPIC_* 是我们写的,但用户重装 / 升级 Claude Code、
上次写盘中断、服务端轮换 token,都会让它们跟 ~/.claude-agent/identity.json 对不上。
桌面版每次启动、服务器版每次 --repair 都会对一次账
(server/claude_config.py 的 check_and_repair):
ANTHROPIC_BASE_URL 与 identity 一致(或一个受管键都没有)
才算我们的;指向别处的不自动改(那是用户自己的东西),但也不算配好了 ——
桌面版会拦下来(见下面「别人的中转」),服务器版 --repair 报成失败(退出码 7),
加 --force 才换成我们的;env 不是对象、路径是软链 → 跳过并说明);<文件名>.aikaui-bak;
settings.json 顶层的 model 不碰 —— 那是用户在 /model 里的选择;<文件名>.aikaui-bak 的定义是"装我们之前的样子",不是"上一次写入前的样子":
apply()(安装 / 换账号时那次覆盖)和 check_and_repair()(自动修复)
都只在备份还不存在时才写它。这样用户原来那套别家中转(连同它自己的 token)
永远留在备份里 —— 管家给的选项写着「换成我们的(原来的会备份,随时能换回)」,
这条承诺就得对后来每一次自动修复都成立。
别人的中转:拒绝使用,除非换成我们的。 这个客户端只配合 aikaui 中转工作 ——
终端要是还指着别处,我们既收不到那台机器的用量,也不该继续发它对话额度。
server/relay_gate.py 只看两个文件的 env.ANTHROPIC_BASE_URL:
| 情况 | 判定 |
|---|---|
| 没写 base / 只写了 token | 不算(还没接别家) |
与 identity 的 api_base 规范化后相同(带不带 /v1 都算) | 我们的 |
| 是别的地址 | 拦 —— 桌面版弹窗只给「换成 aikaui 中转」和「退出」两条路 |
文件坏了 / 是软链 / env 不是对象 | 判断不了就不拦,交给 --repair 报 |
apply()),原配置随时能换回;--recharge 那条只付钱、不碰终端配置,不拦;--repair 发现还指着别处就退出 7。退出码。两个程序的码各有各的含义,脚本化部署时靠它判断,别只 grep 输出:
install.sh(下载和落地这一段):
| 码 | 含义 |
|---|---|
| 0 | 装好了 |
| 1 | 拉不到 latest.json、下载失败,或清单里缺字段 |
| 2 | 被前置检查拒绝:root、HOME 为空或 /、缺 tar/awk/curl |
| 3 | 包不对:非 https、大小不符、sha256 不符、tar 读不了、包里有 ../、包内没有可执行文件 |
| 4 | 装上了但 --version 跑不起来(glibc / 架构不兼容) |
claude-code-assistant(Agent 装 + 终端对话):
| 码 | 含义 |
|---|---|
| 0 | 装好了(--dry-run / --balance / --recharge / --repair 成功也走这个码) |
| 1 | 没归类的失败:模型连不上、模型服务器返回看不懂的内容、跑满步数还没装完、装完但核验没过 |
| 2 | 参数不对(argparse) |
| 3 | 环境不支持 / ~/.local 不可写,或没有 TTY 又没给 --yes |
| 4 | 账号:注册失败、绑账号失败、本地留着未完成注册的旧账户码却没给 --new-account(--repair 时是"本机没有账号") |
| 5 | Node 装不上(镜像挂了、校验失败) |
| 6 | npm 装不上(网络、权限、包缺原生二进制) |
| 7 | 配置写不进 ~/.claude.json / ~/.claude/settings.json;--repair 遇到坏文件、写入校验失败,或者配置还指向别家中转(加 --force 才换成我们的) |
| 130 | 用户在终端里按了两次 Ctrl-C |
★ 5/6/7 现在是 Agent 调工具时拿到的失败:它自己会重试、换镜像、问你要不要继续, 这几条只在"它也解决不了、最后收场"时才成为退出码。所以脚本化部署仍然可以按码判断。
装完 install.sh 会直接进向导,并把向导的退出码原样透传出来 ——
所以 curl … | sh 返回 5/6/7 时,问题在向导那一步,照第二张表查。
没有 TTY 的时候(CI、ssh host cmd、被别的脚本调用):不阻塞、不假装成功。
DISPLAY 在就试着用 x-terminal-emulator/gnome-terminal 之类把向导开在真终端里,
否则打一行提示 + 写 ~/.claude-agent/cli.log,退 3。
--yes 可以完全绕过这个分支,但语义变了:它现在进的是 Agent 模式,
所以要求 ① 这台机器已经有账号(没有的话 --account-code/--account-key 一起给,
或者先在真终端里跑一次),② 模型服务连得上。两条缺一条就会照实报错退出
(4 / 1),不会退回去走那条老流水线。管道里没有主循环,所以:
[自动] 选了:… 留痕;claude(那是给人用的交互界面),只打印 cd <工作目录> && claude。install.sh 认这几个环境变量(都是给测试和特殊部署用的,正常安装不用管):
AIKAUI_SITE(换站点,默认 https://aikaui.com)、AIKAUI_LIB(换安装目录)、
AIKAUI_NO_RUN(只装不启动)、AIKAUI_ALLOW_ROOT=1(1.4.4 起不需要了:root 默认就装,只打一行警告;
留着只为兼容老文档。向导那边照旧传 --allow-root,钉老版本时才用得上)。
root:容器里常常只有 root,所以不拦。代价是 Node、claude 和配置全落在 /root 下,
换个人用不了 —— 警告里直说这一点。
边界:一台机器一个账号 —— 同一个 $HOME 下的所有用户共用一份凭证,本版不做多租户。
Windows / macOS 用 agent.spec,Linux 用 server.spec
(两份的入口不同:main.py / server_main.py)。文件名都用 ASCII,CI 和命令行都能直接引用:
python -m PyInstaller agent.spec --noconfirm --clean # Windows / macOS
python -m PyInstaller server.spec --noconfirm --clean # Linux(服务器版)
| 平台 | 产物 | 说明 |
|---|---|---|
| Windows | dist/ClaudeCode安装助手.exe | 单文件、无控制台;先跑 python build_tools/make_version_info.py 才有文件版本信息 |
| macOS | dist/ClaudeCodeAssistant.app | 未签名;Info.plist 里声明了 aikaui:// |
| Linux | dist/ClaudeCodeAssistant | 单文件、有控制台(服务器版);发行时和 agent.png 一起打 tar.gz |
.spec 里的入口只写了 main.py / server_main.py,PyInstaller 会顺着 import 图自己把
ui/、server/、platform_support/ 收进去,不用手工维护模块清单。要手工列的数据文件只有图标
(Windows 的 agent.ico、macOS/Linux 的 agent.png)和 macOS 的 agent.icns。
server.spec 另外做了几件和 agent.spec 不同的事,改的时候别照抄漏掉:
console=True。GUI 版没有控制台是有意的;服务器版没有控制台等于把所有报错吞掉。excludes 里点名 tkinter / _tkinter / customtkinter / PIL / Pillow /
ui / images / deps / bus / prompts / updater / notify /
platform_support.windows|macos。
这不是省体积,是保证界面库进不来 —— tests/test_imports.py 会断言
import server_main 之后这些模块(连同 openai / pydantic / agent_core)不在
sys.modules 里,所以"不小心 import 了 ui.something"会在测试阶段就红。
bus 和 prompts 容易漏:agent_core 里确实写着 from prompts import …,
只是挪进了 agent_worker()(服务器版不调它);bus 则是把 tkinter 拖进包的那根线。
排掉它们的前提就是这两句只在函数里 —— 所以 tests/test_updater_linux_payload.py
连"是不是写在函数里"一起断言。openai / pydantic /
pydantic_core / httpx2 / httpcore2 / anyio / jiter / sniffio 的数据文件和
子模块都要收,包从 26MB 涨到 36.5MB(tar.gz 35.9MB,2026-10-28 量的)。
涨的空间基本就是模型栈:openai + pydantic + httpx2。注意 openai 3.x 依赖的是
httpx2 / httpcore2,不是 httpx / httpcore —— 写错名字会静默跳过,
到用户机器上才 ModuleNotFoundError。
★ 收这些包用 _collect(),不用 collect_all():后者默认
include_py_files=True,会把每个包的 .py 再作为数据文件放一份(PYZ 里已经有
字节码了)。桌面版靠这一条从 35.9MB 降到 23.6MB,服务器版同理。改这里时别贪方便
换回 collect_all。helpers 不能排除:server/term_render.py 从它取 norm_options / resolve_option
(桌面版和终端版的选项语义靠同一个来源对齐)。它只 import math / re。hiddenimports 点名 agent_bus / agent_core / llm / server.agent_install /
server.agent_tools / / / /
/ / / 。这些全是 的( 不该把模型栈拖起来),静态分析跟不到,漏一个就是运行时崩。
是解 Node 的 用的(PyInstaller 不会自动收 C 扩展),
是 按平台延迟 import 的。体积(2026-10-28 量的 Windows 单文件):23.6MB,改之前 35.9MB,少了 12.3MB(−34%)。
省下来的是打包方式,功能一个没减,细节和"哪些不能动"写在 agent.spec 文件头:
不用 collect_all(源码会重复进包)、只声明真的用到的 win32com.client、排掉已经坏了的
win10toast → pkg_resources → setuptools 那条链、去掉 Pillow 用不到的 _avif / _imagingcms
解码器、optimize=2(等价 python -OO)。tcl/tk(5.9MB)、libcrypto/libssl(2.4MB)、
openai 的资源模块(5.9MB)都没动 —— 那是拿功能换体积。
想再量一遍,或者怀疑哪个包又变胖了:
python build_tools/analyze_archive.py dist/ClaudeCode安装助手.exe
它按顶层包归总,comp MB 才是能省下来的量(uncomp MB 是解压后的大小);PYZ 里那
几千个模块压缩是整体的,单列一行,拆不到单个包。
CI:打 v* tag(或手动触发)时 .github/workflows/build.yml
在四个 runner 上并行构建(windows-x64 / macos-arm64 / macos-x64 / linux-x64)。矩阵每一行带
spec:,Linux 那行指向 server.spec;构建完还有一个 Linux smoke test 步骤跑
./dist/ClaudeCodeAssistant --version 和 --help —— 这两条能过就说明没有把 tkinter 拖进来、
lzma 也在,省得下载下来才发现跑不起来。
产物有两个去处:GitHub Release(打 tag 时 release job 挂上去,本机用
python build_tools/fetch_release.py v1.0.1 --dir release 取回来 —— 装了 gh 的话
gh release download v1.0.1 --dir release 等价)和 Actions artifact(保留 30 天)。
CI 不部署到服务器 —— 见上面「发布新版本」里的分工表和原因;Actions 的摘要页也会
写清楚「线上还没变,部署在本机跑」。
macOS 两个包都没有签名:首次打开要右键 →「打开」,
或者终端跑 xattr -dr com.apple.quarantine /Applications/ClaudeCodeAssistant.app。
程序不弹窗、不打断:后台静默检查 → 静默下载 → 下次启动时换上新版本。
用量页底部有一行 v1.0.0,就是当前跑着的版本。
客户端怎么工作(updater.py)
https://aikaui.com/latest.json(网络失败就退回上次缓存的清单)。schema_version: 1、https 地址、64 位十六进制 SHA-256、
size <= 300 MB,而且版本必须比当前新、当前版本不低于 min_version。~/.claude-agent/updates/staged/<版本>/,边写边算哈希;
对不上就删掉 .part,绝不替换。main.py 最前面):
拿文件锁 → 把旧文件改名为 .old-<pid> → 放入新文件 → 失败就换回去 →
用原来的参数重新拉起自己 → sys.exit(0)。新进程打一行「已自动更新到 vX」。failed.json、
删掉暂存,不再拿它骚扰用户。更新失败永远只是"这次没更新成",不会让程序起不来。状态文件都在 ~/.claude-agent/updates/:manifest.json(清单缓存)、pending.json
(待安装)、applied.json(更新成功的提示,只提示一次)、failed.json、update.log。
macOS:从"下载"目录直接打开的 app 会被 App Translocation 放到只读的随机路径,
替换会失败(只记日志)。把 app 拖进 /Applications 再运行即可正常更新。
发布新版本
改 config.py 里的 APP_VERSION,跑一遍 python -m unittest discover -s tests,提交。
打注释 tag(必须与 APP_VERSION 一致,CI 会校验),正文写这版改了什么 ——
整个正文会变成 GitHub Release 的说明和 latest.json 里的 notes,只维护这一份:
git tag -a v1.0.1 -F - <<'EOF'
一句话说这版改了什么
下面几行可以写细节
EOF
git push origin v1.0.1
CI 在四个 runner 上并行构建,然后由 release job 把四个产物挂到 GitHub Release 上。
CI 不部署——见下面那张分工表。Actions 摘要页会写「线上还没变」。
在本机部署。先把产物取回来(本机没装 gh CLI,用仓库里的脚本;装了的话
gh release download v1.0.1 --dir release 等价),然后:
python build_tools/fetch_release.py v1.0.1 --dir release
DEPLOY_HOST=… DEPLOY_USER=root DEPLOY_SSH_KEY=… \
NOTES="$(git tag -l --format='%(contents)' v1.0.1)" \
bash build_tools/deploy_downloads.sh 1.0.1 ./release
加 DRY_RUN=1 只生成清单、用客户端自己的 updater 验一遍,不碰服务器。
为什么 CI 不部署: GitHub runner 到我们那台服务器(49.233.72.225)的上行只有 ~17KB/s —— 2026-10-28 实测,136MB 传了十几分钟才走 11MB,按那个速度要两小时。 本机到同一台机器约 5MB/s,几十秒就完。所以分工是:
| 谁 | 干什么 | 为什么是它 |
|---|---|---|
| CI | 四个平台的构建 + 挂 GitHub Release | macOS 的包只能在 macOS runner 上打 |
| 本机 | deploy_downloads.sh 推到 nginx + COS | 上行快;而且部署变成显式动作,不会"打个 tag 就改了生产" |
deploy_downloads.sh 的执行顺序是刻意的:传包到 nginx → 传包到 COS → 两边 HEAD 验大小 →
才原子换 latest.json → 更新 install.sh → 改两个页面的版本号 → 最后以客户端视角再确认一遍。
任何一步没验过就退出,所以清单永远不会指向一个取不到的文件。
中间被打断也安全:线上清单还是旧的,只是多了一个半截的版本目录,删掉重跑即可。
releases/winget/<版本>/AppInstaller_x64.msix 不归发版管(2026-10-30 加)。
它是客户端「没有 winget 时自动装一个」用的包,版本固定、内容永久不变,已经在
releases/winget/1.11.430/ 下,发新版本不用动它。要换 winget 版本才跑一次
build_tools/deploy_winget.sh <msix> <版本>,然后把 platform_support/windows.py
里的三个常量(WINGET_VERSION / WINGET_MSIX_SIZE / WINGET_MSIX_SHA256)一起改掉 ——
脚本结尾会把该填的值打出来。忘了改常量的后果是客户端 sha256 校验不过,走下载页兜底。
发布时有三个容易踩的坑,脚本已经处理好了,但你要知道它在干什么:
aikaui.com(nginx 的
/var/www/aikaui/releases/),而 latest.json 里给自动更新用的 url 指向 dl.aikaui.com
(腾讯云 COS + CDN)。脚本两边都传、两边都 HEAD 验大小;任何一边取不到就直接退出,
不换清单。只传一边的后果不是报错,而是客户端悄悄 404 —— 只会在 update.log 里留一行,
6 小时后再试一次。index.html 和 download/index.html 各有 4 条
/releases/<版本>/… 的链接,只换 latest.json 的话,新用户从下载页拿到的还是旧包。
脚本在换完清单之后用正则把这两页的版本号改成新的(备份成 *.bak-<时间戳>,
同目录 mv 原子替换)。用正则而不是替换某个固定字符串,所以不用每次改脚本。install.sh 和版本号无关,但每次发版都要重传。 官网那行
curl -fsSL https://aikaui.com/install.sh | sh 拉的是服务器上的一个固定文件,
它自己去读 latest.json,所以没有版本号要改 —— 也正因如此,改了脚本却忘了传,
线上还是老脚本,比发错包更难发现。脚本先 sh -n 语法校验,再 scp 成 .tmp 后 mv,
最后 curl 回来 cmp 一次字节一致,不一致就退出。顺序是刻意的:传包 → 两边都验 → 才换 latest.json → 再改下载页。
清单永远不会指向一个取不到的文件。DRY_RUN=1 只生成清单并用客户端自己的
updater.py 验一遍(形状、sha256、size、min_version、已是最新时不再提示),不连服务器。
老用户注意:
v1.0.0是第一个带 updater 的版本。更早安装的版本里面没有更新代码, 必须手动装一次v1.0.0,之后才能自动更新。
agent_demo/
│
├── main.py 182 程序入口:先换新版本 → 建窗口 → 别家中转就拦下 → 起线程(含配置自愈)→ 事件循环
├── server_main.py 618 服务器版入口:环境自检 → Node → claude → 账号 → 配置(--repair 只对账,--balance / --recharge 只查账收钱)
│
├── config.py 98 唯一的常量源:版本、服务器地址、凭证、路径、配色、更新参数、MAX_STEPS
├── updater.py 660 自动更新:清单校验 / 静默下载 / 下次启动替换自己(不 import tkinter)
├── deps.py 19 可选依赖探测:Pillow 在不在
├── helpers.py 240 数字金额格式化 + 用量取数(按模型 / 时间序列)+ 刻度 + 文字宽度估算 + 选项清洗 + 凭证打码
├── images.py 138 窗口图标(打包资源优先,缺了现画)+ 付款二维码下载
├── notify.py 26 桌面通知薄封装(各平台实现在 platform_support)
├── bus.py 112 线程间队列 + poll_queue(每 100ms 把消息翻译成界面操作)
├── llm.py 126 大模型客户端(走 aikaui.com 代理,用本机账号的 api_key)
├── agent_core.py 285 Agent 主循环:run_one_task / agent_worker / balance_worker
├── prompts.py 614 系统提示词(按平台给安装指令 + 工作目录 + VSCode 询问分支 + 余额分寸)
│
├── platform_support/ 三平台差异(调用方只 import 这个包)
│ ├── __init__.py 181 按 sys.platform 延迟加载并转发
│ ├── common.py 230 safe_cwd(HOME 越界检查)、find_claude、clean_env、跑子进程抓输出、VSCode 值解析
│ ├── windows.py 1524 注册表协议 / .lnk / cmd / WinRT toast / VSCode 探测 + winget 安装(含"没 winget 就装一个"、问 AppX 注册表定位、240MB 安装包 6 路分段下)/ 指纹原始值(主板+CPU、MachineGuid)/ claude 探测(含便携 Node 的 %USERPROFILE%\nodejs 和 npm 全局前缀)
│ ├── macos.py 467 Info.plist 协议 + Apple Event / .command 快捷方式 / Terminal.app / VSCode 探测 + brew 安装 / 指纹原始值(IOPlatformUUID)
│ └── linux.py 387 .desktop + xdg-mime / 终端探测 / notify-send / VSCode 探测(安装只开下载页)/ 指纹原始值(machine-id + DMI,容器里带 hostname)
│
├── server/ 服务端与工具层(不 import 界面)
│ ├── ui_bridge.py 31 GUI 注入桥:set_ui / get_ui
│ ├── cli.py 335 服务器版共用的地基:退出码、子进程封装、sha256、安全解包、shell PATH 块、HttpClient
│ ├── console_ui.py 128 终端里的进度实现(实现 ui_bridge 那几个钩子,不阻塞、没 tty 也不卡住)
│ ├── node_setup.py 346 Node 检测 / 选 LTS / 从 npmmirror 下载校验 / 解到 ~/.local/node / 软链命令
│ ├── claude_setup.py 462 npm install -g --prefix ~/.local(清掉继承的 npm_config_*,显式 --include=optional + --allow-scripts;原生包没下来时单独补装,断网自己重试)
│ ├── identity.py 581 设备身份、PoW 注册、本地凭证、换设备登录、_ensure_account_code / _ensure_user_id、pending_recovery
│ ├── device_key.py 68 本机密钥:首次注册生成 32 字节随机数,存在比 identity.json 深的地方,**只随注册请求**发给服务器(服务器只存 sha256,用来认出"这台机器来过")
│ ├── fingerprint.py 186 设备指纹:按来源优先取(board+cpu / MachineGuid / IOPlatformUUID / machine-id+dmi / device_id)→ SHA-256 前 32 位(不是凭据,注册时上报,服务器按它还回旧账号)
│ ├── vault.py 268 凭证的"第二份":账户码存注册表(DPAPI 加密)/ 配置目录;本机密钥也存在这里,但是另一个槽位
│ ├── pow.py 128 PoW 求解(进度 / 取消 / 90 秒超时),和服务器 pow.js 逐位对齐
│ ├── api.py 504 余额 / 用量 / 充值 HTTP 层(永远不抛异常)
│ ├── claude_config.py 495 把身份写进两份配置 + 事后对账自愈(坏文件不覆盖、外家中转不碰)
│ ├── relay_gate.py 132 别家中转的判定:有 base 且不是我们的就拦住(不 import 界面,可离线测)
│ ├── billing_console.py 226 服务器版的措辞层:余额/用量排版、终端二维码、装完能敲的命令(不联网、不 import 界面)
│ ├── tools_fixed.py 148 五个固定工具 + aikaui:// 协议注册(转发 platform_support)
│ ├── tools_general.py 125 bash / read_file / write_file / ask_user(解码和杀进程树在 platform_support.common)
│ ├── tools_vscode.py 246 check_vscode / install_vscode / install_claude_extension / open_vscode(连 Claude 面板一起开)
│ └── tool_specs.py 386 工具说明书:tools[] + tool_map(19 个工具)
│
├── ui/ 界面层
│ ├── fonts.py 179 全界面字体表:按平台选字体 + 字号常量 + 画布缩放
│ ├── gui.py 31 class GUI(WindowMixin, OptionsMixin, RechargeMixin, UsageMixin, LoginMixin, AccountMixin, GateMixin)
│ ├── window.py 521 WindowMixin:窗口壳、左侧导航、对话页、底部询问条、余额回流
│ ├── usage.py 625 UsageMixin:用量信息页(余额卡 + 两张按模型的时间分布图 + 充值入口)
│ ├── account.py 272 AccountMixin:账户信息页(账户码 / 密钥打码、眼睛、复制、防泄露红字)
│ ├── options.py 219 OptionsMixin:ask_user 选项面板(点击 / ↑↓ / 回车 / Esc)
│ ├── recharge.py 627 RechargeMixin:充值弹窗两页 + 创单 + 付款后刷新 + 只办充值时的靠右定位
│ ├── login.py 323 LoginMixin:换设备登录弹窗(账户码 + 账户密钥;也承担"本地凭证丢了"时的自动找回)
│ └── gate.py 156 GateMixin:检测到别家中转时的模态窗(换成我们的 / 退出)
│
├── build_tools/
│ ├── make_version_info.py 80 生成 Windows 文件版本信息(build/version_info.txt)
│ ├── make_manifest.py 113 四个产物 → latest.json(校验版本、算 sha256 / size)
│ ├── fetch_release.py 从 GitHub Release 取回四个产物(本机没有 gh CLI)
│ ├── analyze_archive.py 按顶层包归总产物,量体积时看谁最占地方
│ ├── deploy_downloads.sh 发布:传 nginx + 传 COS → 两边验大小 → 换清单 → 改下载页版本号
│ └── deploy_winget.sh 单独发那份 App Installer(换 winget 版本时才跑)
├── tests/ 离线单元测试(stdlib unittest)
│ ├── test_version.py 版本解析 / 比较 / 清单校验
│ ├── test_download.py 本地假服务器:下载、哈希、404、缓存回退
│ ├── test_apply.py 备份、回滚、失败上限、锁、更新提示
│ ├── test_pow.py PoW 求解(和服务器 pow.js 的测试向量对齐)
│ ├── test_register_flow.py 本地假服务器说真契约:注册、不重注册、不泄密、指纹字段、fingerprint_required、is_recovered、保险箱不被动静冲掉
│ ├── test_credentials.py 打码、设备指纹(各来源 / 占位符 / 老公式冻结 / 失败兜底)、保险箱往返
│ ├── test_ui_messages.py poll_queue 把 recovery / recovered 送到对的界面方法
│ ├── test_bash_timeout.py bash 工具的超时与杀干净
│ ├── test_platform_support.py 三平台抽象层
│ ├── test_prompts.py 提示词按平台切换 + VSCode 询问分支的关键句
│ ├── test_vscode.py VSCode 探测 / 安装 / 打开(三平台,全程打桩)
│ ├── test_winget.py 没 winget 时自动装:sha256 校验、依赖包先装、失败报错要能看(HRESULT / 中文不乱码 / 重试一次)、不再解压便携版、问 AppX 注册表、老版本要点名、240MB 6 路分段(拼序 / 接不到段 / 中断 / 校验)
│ ├── test_cancel.py 「中断」:牌子语义、子进程登记/清理、下载到一半能停
│ ├── test_tools_vscode.py 四个 VSCode 工具的返回值、失败归类、Claude 面板什么时候发链接
│ ├── test_agent_core.py install_done 什么时候触发("这一轮真装完了"的判定)
│ ├── test_tool_specs.py tools[] 和 tool_map 一一对上
│ ├── test_server_cli.py 服务器版:参数、退出码、--dry-run 不落地、无 TTY 不阻塞、不重复注册、密钥不进报错
│ ├── test_billing_console.py 余额/充值措辞:读不懂的余额不编成 0、二维码每行等宽、没装 segno 就退回链接、密钥不出现
│ ├── test_claude_config.py 配置自愈:没 diff 不写、坏文件不覆盖、外家中转不碰、顶层 model 不动、日志不含 token
│ ├── test_relay_gate.py 别家中转一律拦:带不带 /v1 都认得出、坏文件不拦、切换留备份、文字不含 token
│ ├── test_node_setup.py Node 选版本 / 资产名 / SHASUMS / 校验失败 / PATH 块只写一次
│ ├── test_claude_setup.py npm argv 精确 / --allow-scripts 带裸包名 / npm_config_* 被清掉 / 失败映射到 6 / 原生包没下来时补装、已下过的不再下一遍、断网重试
│ ├── test_console_ui.py 终端进度条:管道里不打 \r、静默模式、流坏了不抛
│ ├── test_install_sh.py 真跑 sh:假 curl + 真 tar,装到哪、校验失败退几、包里有 ../ 会不会接
│ ├── test_updater_linux_payload.py server.spec / CI 矩阵 / updater 还认得这个包
│ └── test_imports.py 工具层导入不依赖 Windows;服务器版入口不拖界面
├── packaging/
│ ├── install.sh 322 官网那一行 `curl … | sh` 拉的就是它(POSIX sh,上线前 sh -n 校验)
│ └── claude-code-assistant.desktop Linux 桌面启动器(服务器版不再发它,留着给手动装桌面版的人)
├── collab/ 两台机器上两个 Agent 的协作规约与实现(PROTOCOL.md / collab.py)
├── docs/ 公共文档用的截图(README 顶部那张)
├── .github/workflows/build.yml 四平台 CI 构建 + tag 时发布
│
├── agent.spec PyInstaller 打包配置(Windows / macOS,入口 main.py)
├── server.spec 105 PyInstaller 打包配置(Linux 服务器版,入口 server_main.py)
├── requirements.txt 第三方依赖(按平台带 marker)
├── agent.ico / agent.png / agent.icns 窗口 / 应用图标
├── 文件执行或调用顺序.md 一次对话里各模块的出场顺序
├── LICENSE MIT
└── README.md
main.py ─→ ui/ ─→ agent_core ─→ llm / server/ ─→ config
依赖是单向的,全图无环:
config.py 不依赖任何本地模块,是所有常量的唯一来源。ui/)按职责拆成六个 mixin,拼成 GUI;六个 mixin 方法名互不重复,
也没有一个调 super(),所以 MRO 顺序不影响行为。server/)不 import 界面。| 线程 | 起点 | 周期 | 干什么 |
|---|---|---|---|
| 主线程 | mainloop() | poll_queue 每 100ms | 唯一能碰 tkinter 的地方 |
| Agent 线程 | agent_core.py | 阻塞在 user_input_queue.get() | 跑模型、调工具 |
| 余额线程 | agent_core.py | 每 30 秒 | 查余额 / 用量 → 丢进 msg_queue |
| 协议线程 | main.py | 一次性 | 注册 aikaui://,注册完就结束 |
线程之间没有任何直接调用,全靠 bus.py 里那四个东西连着:msg_queue(工作线程 → 界面)、
user_input_queue(界面 → Agent)、input_event + input_holder(ask_user 专用的
"闹钟 + 信箱")。
铁律:非主线程绝不碰控件,一律往 msg_queue 丢消息,由主线程在 poll_queue 里更新界面。
poll_queue 会分发的消息种类:log、balance、busy、recharge、recharge_result、
recharge_methods、recharge_qr、recharge_paid、login_result、recovery、recovered、
ask、progress_start、pow_progress、progress_stop。其中 recharge(服务器说余额不足)
不弹窗,只调 offer_recharge() 在底部问一句。
一次对话里文件和线程的完整流转,见 文件执行或调用顺序.md。
1. _ui 的晚绑定。
工具层拿界面对象必须走 server/ui_bridge.py 的 get_ui():
u = get_ui()
if u:
u.ui_log("...")
不能 from server.ui_bridge import _ui —— 那样会在 import 的那一刻把值(通常是 None)
拷进来,之后 set_ui(gui) 改的是 ui_bridge 里的 _ui,你那份永远还是 None。
2. 两个同名的"刷新余额"。
ui/recharge.py 和 ui/usage.py 都用到了,别搞混:
from agent_core import refresh_balance # 查完丢进队列,刷用量页
from server.api import refresh_balance as fetch_balance # 查完返回 dict,判断到没到账
ui/usage.py 的「刷新」按钮也是起一条 daemon 线程跑 agent_core.refresh_balance ——
网络请求不能落在主线程上,不然界面会卡住。
3. 六个 mixin 的方法名不能撞。
GUI 靠 MRO 把六个 mixin 拼起来,谁都不调 super(),所以一旦两个 mixin 定义了同名方法,
排在后面的那个会被静默吃掉。加方法前先在全目录搜一下同名。
4. _ensure_account_code / _ensure_user_id 必须留在 server/identity.py。
server/api.py 要用它们,而它们内部会回调 register_device()。挪进 api.py 就变成
api ↔ identity 双向依赖,直接循环导入。
5. tk.Canvas 上的东西不会被 CTk 缩放。
字体全在 ui/fonts.py 一处定义:控件用 font(S_BODY),画布用 spec(S_CANVAS),
别在别处写 ctk.CTkFont(size=11) 或 font=("微软雅黑", 11) 这种字面量。特别是画布——
ctk.CTkFont 的 size 是像素(还会按屏幕 DPI 再放大一遍,这台机器 150% 就是 ×1.5),
而 create_text 老实按 Tk 的规矩解释:正数是磅、负数是像素,1 磅只有 1.33 像素。
所以 spec() 传的是负数;同时画布上的长度(留白、柱宽、提示框)也得乘 canvas_scale(),
不然图上的字永远比旁边的控件小一圈。文字宽度用 helpers._text_width(text, fonts.csize(S_AXIS))
估算 —— 字号口径必须和 spec() 一致,否则提示框会量错、把文字切掉。
| 文件 | 内容 |
|---|---|
~/.claude-agent/identity.json | 本机凭证:device_id、api_key、account_key、api_base、model、fast_model、user_id、account_code、balance、fingerprint(取到时),以及最近一笔充值订单 last_order。POSIX 上权限是 0600 |
HKCU\Software\aikaui\ClaudeCodeAssistant | 凭证的第二份(只有 account_code / account_key / user_id),值用 DPAPI 加密 —— 只有同一个 Windows 用户、同一台机器解得开。macOS 在 ~/Library/Application Support/aikaui/credential.json,Linux 在 ~/.config/aikaui/credential.json(都 0600) |
同一个注册表键下的值 m / 凭证目录里的 .state | 本机密钥(server/device_key.py),和上面那份分开存:vault.clear() 不会动它。它不进 identity.json、不进界面、不进日志,只在注册请求里发一次 |
~/.claude.json | Claude Code 的配置,set_model_config 往它的 env 里写中转地址、token 和各档位模型映射 |
~/.claude/settings.json | Claude Code 的用户级设置,set_model_config 会把同一套 env 也写进去(优先级更高),并把 model 钉成 flash,其它键原样保留 |
set_model_config 写进去的映射分两层:
| 档位 | 环境变量 | 指向 |
|---|---|---|
| 默认 | ANTHROPIC_MODEL / ANTHROPIC_SMALL_FAST_MODEL / ANTHROPIC_DEFAULT_MODEL | flash |
| 默认 | ANTHROPIC_DEFAULT_SONNET_MODEL / ANTHROPIC_DEFAULT_HAIKU_MODEL | flash |
| 显式 | ANTHROPIC_DEFAULT_OPUS_MODEL | pro |
外加 ~/.claude/settings.json 的 "model" 也钉成 flash(写完整模型名而不是
haiku 这类别名,可以直接绕开别名解析)。
ANTHROPIC_DEFAULT_{SONNET,OPUS,HAIKU}_MODEL 不是"优先级较低的默认值",而是给
sonnet / opus / haiku 这几个别名做重定向的。Claude Code 在用户没明确指定模型时
走的是 sonnet 档,所以 ANTHROPIC_DEFAULT_SONNET_MODEL 一旦指向强模型,用户什么
都没选也会打到贵的那个上 —— 这就是"指定了 flash 却偷偷调用 pro"的原因。
同理,只要 ANTHROPIC_DEFAULT_OPUS_MODEL 指向 pro,任何请求到 opus 档的地方
(子 agent、plan 模式等)都会落到 pro;这是留给用户自己 /model 选 opus 的通道,
不想要可以把它也改成 flash。
device_id 是主机名 + MAC + 架构 的 sha256 前 16 位,同一台机器每次算出来都一样。
它现在只是本机标识(bind-account 用它去重),不再是身份凭证 —— 服务器只按
api_key / account_code 认人,所以同一个 device_id 换不出任何东西。
同一个 account_code(形如 CC-XXXXXX-XXXXXX)可以换设备找回账号,「🔐 账户信息」页把它和
account_key 都列出来(默认打码,点眼睛看明文,带复制和红字警告);「用量信息」页只露前 4 位。
服务端还回一个 account_key(64 位 hex,只回一次),存在 identity.json 里,目前只用来核对
账号归属,接口并不需要它。
注册不是幂等的:新契约下"注册"= 用 PoW 开一个新账号,所以只要本地有凭证, 客户端一次请求都不会再发(重复注册会开新号、把余额丢掉)。
本机凭证被删 / 被清理工具带走:注册或换设备登录成功时,账户码和账户密钥会另存一份到
server/vault.py(Windows 注册表 + DPAPI)。下次启动发现 identity.json 不在了、
但保险箱里还有凭证,程序不注册新号,而是弹出「换设备登录」窗口把两串都预填好,
由用户点一下「登录」确认 —— 恢复走的是现成的 POST /api/bind-account。
不替用户自动确认:共用电脑上前一个人删了凭证,后一个人一开机就被"恢复"进别人的账号,
等于把余额送给陌生人。
换了机器 / 重装了系统:只能靠用户自己存下来的账户码 + 账户密钥(这也是「🔐 账户信息」页 要红字提醒"截图保存"的原因)。
同一台机器上再注册一次(2026-10-31 起):/api/auto-register 会拿设备指纹去找有没有
属于这台机器的账号,有就直接把那个账号还回来(响应 is_recovered: true,界面上会弹
「账号已自动恢复」)。这是为了堵住"删掉 identity.json 再注册一次就多领一份赠送额度"。
★ 这条只发 api_key + account_code,不发 account_key:指纹不是秘密(见下一节),
撞号或泄露的风险只能靠"不交主人凭证"来兜底。所以保险箱里的密钥客户端自己续上,
identity.py 里专门处理了"恢复时别拿空值冲掉旧密钥"。
2026-11-01 起这条路前面还多加了一道本机密钥(见「本机密钥」一节):它比指纹强,
命中时优先按它恢复 —— 删掉 ~/.claude-agent 删不掉它,所以"清配置刷号"这一步就断了。
服务端 POST /api/bind-account 要 {account_code, account_key, device_id}:
SHA-256。
比对时 trim + 大小写不敏感(那串 hex 大小写不携带信息,用户手抄容易错),
用 timingSafeEqual 比,不按字节早退。三种回答分得很清楚,别混着说:
| 服务端 | 含义 | 界面上 |
|---|---|---|
404 account_code not found | 码查不到 | 让用户核对账户码 |
403 account_key_required | 码对、但没给密钥 | 光标跳到密钥框 |
403 account_key_invalid | 密钥不对 | 强调"整串复制,别只抄一段" |
老账号(2026-10-24 之前注册的)没有密钥,服务端对它们只认码 —— 那批账号的密钥 从来没下发过,现发一把会让"只知道码的人先登录把真用户锁在外面"。所以客户端不把密钥 设成必填,空着照样发,由服务端决定。
注册时随请求发一个 32 位十六进制的设备指纹(server/fingerprint.py)。取值按顺序,
第一个拿到的赢:
| 来源 | 平台 | 取的是什么 |
|---|---|---|
board+cpu | Windows | SHA-256(主板序列号 | ProcessorId)[:32] —— 老公式,一个字都不能改(线上已入库的指纹还认它) |
machine-guid | Windows | 主板序列号是占位串 / 虚拟机时,退到 HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid(winreg 读,不起 PowerShell) |
platform-uuid / platform-serial | macOS | ioreg -rd1 -c IOPlatformExpertDevice 的 IOPlatformUUID(读不到才用硬件序列号) |
machine-id+dmi | Linux | /etc/machine-id + DMI 的 product_uuid / board_serial;容器里再加 hostname(同宿主机同镜像的两个容器前三个完全一样,不加就撞号) |
device-id | 全平台 | 兜底:主机名 + MAC + 架构。弱,但保证不会空 |
原始值的读取全在 platform_support/{windows,macos,linux}.py 的 fingerprint_parts();
哈希和"选哪个来源"在 server/fingerprint.py 的纯函数 derive() 里,测试不碰系统。
指纹连同来源一起上报(fingerprint_src),只进服务端日志 —— 两个人"撞"到同一个账号时,
得能一眼看出是哪个来源撞的。
它不是凭据:本机任何程序都读得到,随手也能往接口里传任意字符串;ProcessorId 对同型号
CPU 是常量(CPUID 特性位 + 型号签名,不是序列号);不少 OEM 主板 / 虚拟机的序列号是
Default string、To be filled by O.E.M. 这类占位符 —— 这些值一律当"没有",退到下一个来源。
服务端拿它做两件事:同一指纹再注册就还回原来的账号(见上一节第 3 条),以及
同一指纹 24 小时内最多 5 次新注册(POW_FP_HARD,和 IP 硬阈值一个数量级)。
恢复本身不计入这个配额(审计记 outcome='recovered',配额只数 'registered')。
上线顺序:客户端先发(指纹永不空、始终上报),服务端随后。服务端第一版
FINGERPRINT_REQUIRED=0(缺指纹照旧放行,兼容 1.5.5 及更老的客户端),等日志里确认 指纹齐全了再翻成1硬拒。
真正能证明"这账号是我的"的,只有账户码 + 账户密钥。
指纹能挡住"清配置刷号",但它有个弱点:不保密,本机随便一个程序都读得到,改一改还能 伪造。所以 2026-11-01 加了一串客户端自己生成的密钥:
server/device_key.py)。
它只随 /api/auto-register 发一次,和 device_id 一起上去;服务器只存 SHA-256
(cc_accounts.device_key_hash + device_id),日志里连哈希都只打前 8 位。identity.json 深一层 —— Windows 是 HKCU\Software\aikaui\ClaudeCodeAssistant
下的另一个值(值名 m,DPAPI 加密),macOS / Linux 是凭证目录里一个 0600 的 .state。
不在 identity.json 里、不进界面、不进日志、不进任何给模型看的文本。
删掉 ~/.claude-agent 不会把它一起删掉;vault.clear()(换账号)也不动它。SHA-256(device_key) 先查有没有账号 ——
命中就还回原账号(和指纹恢复同一个响应形状,只给 api_key + account_code),
优先级高于指纹(密钥比指纹强)。没命中再看指纹,都没命中才建新号。
闸门和指纹一样在「挑战校验 + PoW + 消费挑战」之后,不然就成了不花算力就能枚举
"这台机器有没有账号"的入口。.state,或客服帮忙清)。
它抬高的是刷号成本,不是不可伪造的证明。所以命中后依旧不发 account_key。服务端开关 DEVICE_KEY_REQUIRED 默认 0(1.5.6 及更老的客户端不认这个字段,硬拒会让它们
一装就注册不了),等 1.5.7 铺开、日志里确认密钥齐全之后再翻成 1。注册风控白名单
(cc_settings.ip_whitelist)同样跳过这个开关。
统一走 https://aikaui.com。X-Agent-Key 只在注册链路里发(它打在 exe 里,
是公开的"敲门砖",不是秘密);聊天用注册拿到的专属 api_key。
| 接口 | 用途 |
|---|---|
GET /api/challenge | 取一个 PoW 挑战(带 X-Agent-Key) |
POST /api/auto-register | 提交 {challenge, nonce, fingerprint, fingerprint_src?},换回账号与专属 api_key;指纹已对应到账号时直接还回那个账号(is_recovered: true,不发 account_key) |
POST /api/bind-account | 用 {account_code, account_key, device_id} 换设备登录(不需要 X-Agent-Key;老账号 account_key 可省) |
GET /api/balance | 查余额(按 account_code 查) |
GET /api/usage | 查 token 消耗 |
POST /api/create-order | 创建充值订单,返回付款二维码 |
POST /api/chat/completions | 助手自己聊天,只认 Authorization: Bearer <用户 api_key> |
aikaui://recharge?user_id=N | 自定义协议:支付页付完款靠它把本程序唤起来 |
POST /api/issue-token 已经下线(恒返回 410),客户端不再调用。
/api/challenge 给一个 32 位 hex 的挑战(绑定来源 IP、120 秒过期、只能用一次),
客户端要在本地找一个 nonce,使 SHA256(challenge + nonce) 的前 difficulty 个二进制位
全为 0,再交给 /api/auto-register。难度平时是 16,同一出口注册太频繁会抬到 22。
求解在 server/pow.py,跑在 Agent 线程里:能报进度(已算多少次 / 约百分之几)、能取消
(进度条上有个「取消」)、单次 90 秒硬超时,一次注册最多重来 3 次(每次都重新取挑战)。
失败原因会照实带到界面上:注册关闭、网络太频繁、认不出 IP,三种情况用户该做的事不一样。
提交时一定带 fingerprint(除非连 device_id 都拿不到,那是极端情况,见上面
「设备指纹是什么、不是什么」)。服务端还是老版本、不认识这个字段而整条请求 400 时,
客户端会去掉字段再试一轮 —— 兼容老服务端用;但服务端明确回 fingerprint_required
时不重试(丢掉指纹只会失败得更难看),直接如实报错让用户升级。
server/api.py 里的所有函数永远不抛异常 —— 失败就返回 (None, 错误说明) 或
{"error": "..."},错误原文会被翻译成大白话带到界面上。余额查不到、支付平台挂了,
都不会把程序搞崩。
/api/usage 的响应形状(后端已按这个返回)today / month 两块里各带一个 by_model、一个 series(总量序列)和一个
series_by_model(按模型拆开的时间序列,两张图读这个)。
序列按北京时间分桶:今日是 0 点到当前小时,本月是 1 号到今天。
{"today": {"tokens": 11785016, "yuan": "2.22",
"by_model": {"deepseek-flash": {"tokens": 11785016, "yuan": "2.22"}},
"series": [{"label": "00:00", "tokens": 0, "yuan": "0"},
{"label": "01:00", "tokens": 1234, "yuan": "0.01"}],
"series_by_model": {
"deepseek-flash": [{"label": "00:00", "tokens": 0, "yuan": "0"},
{"label": "01:00", "tokens": 1234, "yuan": "0.01"}],
"deepseek-v4-pro": [{"label": "00:00", "tokens": 0, "yuan": "0"}]}},
"month": {"tokens": 48647024, "yuan": "13.44",
"by_model": {"deepseek-flash": {"tokens": 44580889, "yuan": "9.81"},
"deepseek-v4-pro": {"tokens": 4066135, "yuan": "3.64"}},
"series_by_model": {
"deepseek-flash": [{"label": "10-01", "tokens": 100, "yuan": "0"}],
"deepseek-v4-pro": [{"label": "10-01", "tokens": 0, "yuan": "0"}]}}}
改动这个接口时守四条:by_model 里各模型的 tokens 加起来等于同级的 tokens;
series 里各桶的 tokens 加起来也等于同级的 tokens;series_by_model 里每个模型的
桶加起来等于该模型在 by_model 里的 tokens;老的 tokens / yuan 字段保留原样
(余额卡和两个消耗小格读的是它们)。
客户端解析做得很松:helpers._norm_by_model 认 by_model / models / by_model_tokens,
值可以是 {模型: token数}、{模型: {"tokens":…, "yuan":…}},也可以是
[{"model": …, "tokens": …}, …];helpers._norm_series 认 series / by_hour / by_day
(series_by_model 里的每一段也走它)。按模型序列缺失时那两张图退回每个模型一根
总量柱子并注明"服务器暂未按模型拆分时间";连模型数据都没有时退回总量的单根柱子 +
"暂无数据",不会白屏。
顺带一提:这里有两个 Agent 在干活 —— 一个在作者的 Windows 电脑上,一个在服务器上。
它们不共享会话记忆,靠 git 当共享外存:认领任务、开自己的分支、推上去、用 ask / done
交接。规约写在 collab/PROTOCOL.md,实现是
collab/collab.py,连"每次开工自动看一眼对端有没有新消息"的 hook
都配好了。这套东西本身就是个可以抄的样本:让两个不会互相记得的 Agent 不靠自觉、
靠机制把活接上。
「没 winget 就自动装」这一步,真机上还没成功过一次。
能确定的是:包从 CDN 下得下来(实测 58MB / 11 秒 / sha256 对得上)、
Add-AppxPackage 的参数绑定不再报错、分支逻辑全有离线测试。
用户的机器走到的是 DeploymentError / IOException(部署进程已排队,然后 IO 失败),
这属于 AppX 部署本机故障,不是我们能绕的:1.5.4 那次的 ParameterBindingException
已经修掉,剩下的这类失败会如实报出来、隔 2 秒换 ASCII 路径再试一次,还不行就退到
240MB 官方安装包,不会卡住。手上没有一台"干净、没有 App Installer"的机器,
所以这条路没跑过完整成功一遍。
用户那台机器上的 winget 是"本来就装不上"的(2026-10-31 查清)。
Get-AppxPackage Microsoft.DesktopAppInstaller 有结果,但版本是 1.0.30251.0 ——
winget 出现之前的老版本,包里没有 winget.exe,winget 自然也不在 PATH 里
(用户那边报的是 CommandNotFoundException,这是正确的,不是环境坏了)。
我们下的 1.11.430 想把它升级上去,Add-AppxPackage 在部署阶段 IO 失败。
这种事在用户态没有可靠的绕法(Remove-AppxPackage 动的是系统预置包,要管理员权限,
而且会把双击安装 msix 的能力一起弄没)—— 所以这事停在这里,交给用户在
Microsoft Store 里更新「应用安装程序」。装不上也不会卡:走 240MB 官方安装包。
Add-AppxPackage 返回 1 的原因查清了(2026-10-31)。不是缺依赖,是参数名写错:
Add-AppxPackage 没有 -LiteralPath,写上它 PowerShell 抛 ParameterBindingException、
一句都不执行。10-30 那次的日志只剩「返回 1: and」,所以当时猜成了缺框架包;
10-31 的日志留了三行,FullyQualifiedErrorId: NamedParameterNotFound,…AddAppxPackageCommand
才把它点破。现在用 -Path(带通配符字符时先 Set-Location -LiteralPath 再传文件名)。
1.5.4 之后用户那边的报错换成了 DeploymentError / IOException ——
说明参数这关过去了,卡在 AppX 部署本身(本机 WindowsApps\Deleted 下有删不掉的残留、
ClipSVC 停着,都是仓库受损的迹象)。
便携版 winget 装不了东西(2026-10-31 确认)。它不是 AppX 包,源索引永远打不开,
winget install 必然 0x8A15004B。所以 2026-10-31 起不再解压便携版(以前解压完
打一行「已装好」、下一行就说「读不到软件源」,白等十几秒还自相矛盾):
ensure_winget() 装不上就返回失败原因,install_vscode() 直接走官方安装包。
find_winget 不再靠猜路径(2026-10-31)。用户机器上 winget 是好的,我们却报了
「没有 winget」:PATH 里第一条 WindowsApps 属于别的账户(空壳别名),%USERPROFILE%
和实际 profile 目录也可能不是一个(本机 USERNAME 是中文名,profile 是 C:\Users\abc)。
现在最后一道是问 Get-AppxPackage Microsoft.DesktopAppInstaller 的 InstallLocation。
这条在这台机器上验证过(能准确拿到 1.29.380 的包目录),但它依赖 PowerShell 能起得来;
起不来就当没有,行为退回以前。
新装的 winget 源索引是空的,2026-10-30 用户在真机上撞到 0x8A15004B。
已经加了 / 重试和「直接下微软安装包」那条兜底,
但(本机的 winget 本来就是装好的、源索引也在)。
下次再遇到,日志里会多出「刷新源索引后再试一次」这些行,能直接看出走到哪一步。
find_winget() 直接命中。server.prompts_serverserver.term_chatserver.term_keysserver.term_renderlzma_lzmaplatform_support.linux--balancelzma.tar.xzplatform_support.linuxplatform_support/__init__.pysource updatesource reset --force「中断」不回滚。 点中断只会停下正在跑的东西,装了一半的 winget / VSCode 还在,
再跑一次会接着用(find_winget / find_vscode 都会先探测)。另外被子进程自己
start_new_session 出去的孙进程(极少数)可能杀不干净,taskkill /T 能盖住绝大多数。
Linux 发的是服务器版,不是桌面版。 从发这一版起,linux-x64 那个包没有图形界面:
老的 Linux 桌面用户被自动更新换上之后,双击图标(如果他们手动装过 .desktop)
或者在终端里直接跑,看到的都是一个终端向导而不是窗口。这是"一个包只服务一类机器"
的代价,换来的是服务器上真的能用。要在 Linux 桌面上用界面版,得自己
python main.py(源码)或自己用 agent.spec 打包。
服务器版一台机器一个账号。 凭证在 $HOME 下,同一个 $HOME 的多个用户共用一份;
不做多租户,也不支持同一台机器上切换多个账户码。
三平台功能一致,但细节依赖系统。Windows 的快捷方式是 .lnk、协议写注册表;
macOS 的协议写在 Info.plist 里、链接靠 pyobjc 收 Apple Event、快捷方式是桌面的
Claude Code.command(双击由 Terminal 执行;脚本会自己补 PATH、出错时停住等按键,
见 platform_support/macos.py 的 _SHORTCUT_TEMPLATE);
Linux 的协议靠 .desktop + xdg-mime、快捷方式也是 .desktop。桌面图标和终端启动
各平台的实现见 platform_support/;Linux 没装终端模拟器时会明确报失败,不会假装成功。
macOS 包没有签名。首次打开要右键 →「打开」,或 xattr -dr com.apple.quarantine;
从 Downloads 直接运行时 App Translocation 会让"覆盖自身"失败(更新会在下次启动时跳过)。
文件操作限制在用户主目录内。read_file / write_file / launch_claude 的 directory
都会做路径越界检查,越界直接拒绝或退回主目录。
余额是个"测试大数"。当前这个测试账号服务器给的余额是 8 位数的量级,看起来不像真钱; 界面对它没有任何问题(不溢出、不上色),只是读起来怪。
create-order 的响应字段名(img / qrcode / pay_url)是按接口文档实现的。
代码对字段缺失做了容错(只给 qrcode 不给 pay_url 也能跑),但没有跟真实响应核对过。
如果扫码页显示「二维码没加载出来」,先去确认这几个字段名。
httpx2 / httpcore2 不是笔误,是这一套 openai 真的要的。 本机装的是 openai 3.15.0,
它内部写的是 import httpx2(标准版 openai 才是 httpx),而这两个包跟同名去尾数的
版本是两套不同的东西。所以 .spec 里那两行 collect_all 必须留着 —— 换成别的 openai
版本时要跟着换成 httpx / httpcore,否则打出来的 exe 一联网就崩。