本页目录(74 节)

Spark CLI 与桌面联动

Spark CLI(包名 @spark/agent,命令 spark)完整参考:18 个子命令与参数解析的真实边界(所有 flag 共用一张解析表,与本命令无关的 flag 会被静默忽略)、三种输出格式的区别(--json 给事件流、--output-format json 才给一行结果对象)、完整退出码表(取消是 130、用法错误是 2、update 有专属 0/1/2/3/4)、会话账本的组织方式与 -c/-r 的差别、TUI 的七个准入条件与 9 条公开斜杠命令、图片附件的格式与限额。桌面联动部分讲清本地回环桥的发现与选主、bridge 描述文件协议、三个真实端点、哪些渠道会被桥接(只有 Anthropic 与显式标记 responses 的 OpenAI 渠道),以及桥的安全模型与凭据链路。最后是 ~/.spark 与项目级 .spark 的数据落点、配置分层合并规则、各子命令的真实行为与 28 行排查对照。

阅读约 26 分钟最后核对 2026-09-19

Spark CLI 是桌面应用之外的第二入口:一个终端程序,名叫 spark,包名 @spark/agent。 它自带交互式 TUI、一次性任务模式与一组维护命令,并且与桌面应用共用 ~/.spark 数据根—— 你在桌面里配好的渠道,CLI 能直接用,靠的是桌面进程里一个只监听回环地址的本地桥。

这一页讲清三件事:CLI 自己的命令面(参数、输出格式、退出码、会话、TUI), 桌面联动那条链路的真实协议与边界,以及 ~/.spark 与项目级 .spark 里到底存了什么。 凡是容易被想当然写错的地方都单独标出来了。

1. 这是什么

1.1 包与入口

项实际值
包名@spark/agent
安装后的命令spark(bin 映射到 ./dist/cli/main.js)
Node 要求engines.node >= 22.14.0
源码根仓库里的 spark-engine/
自带依赖Ink + React(TUI)、zod、ajv、smol-toml、@modelcontextprotocol/sdk

它不是桌面应用的包装脚本,而是一套独立的引擎:SDK、事件账本、工具运行时、MCP 客户端、 TUI 都在这个包里,桌面应用的 Spark 执行器复用的是同一套内核。

1.2 与桌面端的关系

两边通过三个共享面协作,理解这三个面基本就理解了「桌面联动」:

共享面路径 / 形式作用
数据根~/.spark(SPARK_HOME 可覆盖)配置、会话账本、账号凭据、桥描述文件、更新状态
本地桥~/.spark/hosts/sparkwork/bridge-<id>.json让 CLI 用桌面已配置的渠道与凭据发请求
宿主技能目录~/.claude/skills、~/.codex/skills、~/.agents/skills、~/.spark/skills两边都扫描这四处,CLI 侧同一份技能可见

注意一个反直觉的例外:长期记忆不在 ~/.spark 下,而在 ~/.spark-agent(见 第 12 章)。所以「两边共用数据根」这句话对配置和会话成立,对记忆不成立。

1.3 三套容易混淆的「CLI」

仓库里还有两个名字里带 CLI 的东西,它们是不同的对象,不要混写:

名称是什么怎么区分
spark CLI本文档的主角,独立安装的终端程序命令是 spark,包名 @spark/agent
「本机 AI 工具」渠道桌面里的两个内置 Provider(local-cli / local-codex-cli),把模型调用转给你机器上已登录的 Claude Code / Codex CLI没有 Key、没有 Endpoint,属于 Provider 而非程序
spark serve只是存根,不启动任何服务固定打印一句「M1 kernel slice / M3 才有」,退出码 2
spark serve # spark serve is not part of the M1 kernel slice; the versioned App Server lands in M3. # 退出码 2

另外 spark-engine 这个目录名指的是引擎源码包,不是命令名——命令始终是 spark。

2. 安装、卸载与 PATH

2.1 三种安装路径的真实差别

这三条路经常被当成同一件事,其实职责完全不同:

方式下载做什么
install.sh / install.ps1会下载 tarball → 校验 sha256 → npm install -g;只有 npm 全局 bin 不在 PATH 时才回落去链 ~/.spark/bin
npm install -g @spark/agent会标准 npm 安装,shim 落在 npm 全局 bin
spark install不会只把当前正在运行的这份代码的入口链到 --bin 目录

最后一条最容易被误解。它从运行中的代码向上找包根(最多 8 层),入口常量是 dist/cli/main.js; 在源码树里直接跑会因为没构建而报 is missing dist/cli/main.js; build it with npm run build。

2.2 spark install 做了什么

  • launcher 形态分平台:POSIX 建符号链接指向 <包>/dist/cli/main.js;Windows 写一个文本 shim,内容固定为三行(@echo off、rem @spark/agent launcher、node "<entry>" %*)。
  • 写入是原子的:先写 .spark-launcher-<pid>.tmp,Windows 上把旧文件改名成 .bak 再换新,失败回滚。
  • 不校验 PATH,只警告:成功后提示 Verify with: spark doctor,PATH 不在只给一条 NOTE。
  • 报错一律退出码 2,不是「安装失败=3」。

一个容易踩的细节:--bin 的默认值是 join(defaultSparkHome(), 'bin'), 而 defaultSparkHome() 读 SPARK_HOME。帮助文本只写 default ~/.spark/bin, 没提环境变量——所以你改过 SPARK_HOME 时,默认 launcher 目录会跟着变。

2.3 卸载的三种粒度

命令删除范围保留
spark uninstall只删 launcher 文件npm 包、~/.spark 全部保留
spark uninstall --packagelauncher + npm 全局 shim + 包目录本身~/.spark 的配置 / 会话 / 缓存永不删除
npm uninstall -g @spark/agentnpm 侧PATH 上的 launcher 会变成悬空链接

--package 的所有权校验是保守的:只会删「能证明属于 spark」的文件——POSIX 要求符号链接且目标落在包目录内, Windows 要读文件内容匹配包目录或标记。无法证明的条目会打印exists but does not provably belong to spark; left untouched 并原样留着。 成功恒定返回 0,包括什么都没删到的情况。

但它卸不掉 npm link 装的版本:更新路径在发现目标目录本身是符号链接时会明确拒绝, 需要先 npm unlink -g @spark/agent。

2.4 PATH 与「外来 launcher」

「外来 launcher」指的是 PATH 上那个 spark 不是我们装的:判定要求它要么不是符号链接, 要么指向的包名不是 @spark/agent。此时 spark install 会拒绝并提示加 --force;spark uninstall 同样拒绝,让你手工处理。

PATH 探测按 PATH 顺序逐目录找,悬空符号链接也算候选(会明确提示 broken link)。 多份 launcher 同时存在时,靠前者会遮蔽靠后者,doctor 会给出WARNING: <path> shadows a spark launcher later on PATH.

3. 命令面与参数解析

3.1 子命令清单

子命令是一张硬编码白名单,共 18 个:

serve models doctor update upgrade install uninstall init login logout whoami config mcp memory plan skills todo sessions

分发不是框架,而是 main() 里一串手写分支。这带来一个必须知道的行为:positionals[0] 一旦命中这些字面量就直接走子命令, 所以 spark update 永远不会被当成任务名;反过来 spark hello update 会被拼成任务提示 "hello update"。

3.2 「全局 flag」是文档概念,不是解析概念

所有 flag 都定义在同一张 parseArgs.options 里,任何子命令在语法上都能接受任何 flag。strict: true 只拒绝未定义的 flag(比如 --nope 会报错并退出 2), 不拒绝「定义了但与本命令无关」的 flag——后者被静默忽略。

# 这两个都不会报错: spark -p "分析日志" --check # --check 被静默丢弃,任务照常跑 spark todo list --scope user # --scope 只映射给 memory,这里无效

同一个 flag 复用到多个子命令的地方,是最容易记错的一类:

flag实际接收者
--limitmemory(1–100)、todo(1–100)、skills(1–200)——三处区间不同
--bodymemory save 与 plan set|append
--global / --projectconfig 与 mcp(帮助文本只写了 config)
--scope只有 memory
--session只有 plan

3.3 任务提示词的三种来源

优先级是 -p/--prompt > 位置参数 > 管道 stdin:

spark "重构这个文件" # 位置参数按空格拼成完整提示词 spark -p "重构这个文件" # 与上一行等价 cat notes.md | spark # stdin 非 TTY 时才读管道

-p 一旦给出,位置参数被静默忽略。没有提示词、又不是交互 TTY 时报No task was provided. Pass a prompt, pipe stdin, or run spark in an interactive TTY. 并退出 2。

3.4 --resume 的空值哨兵

node:util 的 parseArgs 拒绝无值 flag,所以裸 --resume 会被预处理: 在它后面插入一个空串 '' 当哨兵,下游把哨兵解释成「打开 TUI 会话选择器」。

spark --resume # TUI 里弹选择器(需要 TTY) spark -r session_... # 直接恢复指定会话 spark -c # 继续当前目录最近一次会话

副作用值得一提:预处理是「后面跟任何以 - 开头的 token 就插空串」, 所以 spark -r -p "x" 会被改写成 -r '' -p x——-r 吃掉了哨兵,-p 正常生效。

4. 输出格式

4.1 三态语义

这里有两个长得像但语义不同的开关,混用是这一块最常见的错误:

写法输出什么含流式 delta?
(默认)人类可读文本是,直接写 stdout
--json事实事件 NDJSON(无 delta)否
--output-format stream-json事实事件 + {"type":"delta",…} 记录是
--output-format json恰好一行最终结果对象否

所以「--json 就是机器可读的最终结果」是错的——它给的是事件流。 想要一行结果对象,只能用 --output-format json。

--json 与 --output-format text / json 冲突报错, 但与 --output-format stream-json 是合法组合,且此时行为等同纯 stream-json。

4.2 text 模式的 stdout / stderr 分工

这是脚本化调用时最实用的一条:

  • stdout:只有模型的正文输出。
  • stderr:工具调用、权限请求、失败、取消等全部「过程事实」。

如果模型一个字面 delta 都没给,会在 assistant.completed 之后补打整段文本。 这意味着 spark -p "..." > answer.txt 拿到的就是干净的答案。

4.3 --output-format json 的字段

{"type":"result","sessionId":"session_…","turnId":"turn_…", "status":"completed","message":"…","terminal":{"type":"turn.completed",…}}

status 取值与终态事件一致,message 在没有文本时是 null。 该模式下 delta 被显式关闭,内容只来自 assistant.completed。

4.4 一个容易踩的副作用

--output-format 会顺手打开所有子命令的 JSON 输出:

spark models --output-format json # 输出 JSON spark models --output-format text # 输出表格

不需要 JSON 的子命令上带了这个 flag,输出形态会被意外改掉。

5. 退出码

退出码是脚本化调用最依赖的东西,也是文档里最容易写错的部分。分三组记。

5.1 一次性任务

码条件
0终态事件是 turn.completed
1其它终态(含 turn.failed)
130终态是 turn.cancelled(例如你按了 Ctrl+C)

取消是 130 而不是 1,与 Unix 的 SIGINT 约定一致; 另有 EPIPE 被单独吞成 0(写管道时下游提前关闭不算失败)。

5.2 用法错误统一是 2

参数解析失败会打印错误 + 整篇帮助并退出 2。触发条件包括:未知 flag、--output-format 非法、--json 冲突、--permission-mode 非法、--effort 非法、--continue 与 --resume 同给、--global 与 --project 同给、--dangerously-skip-permissions 与显式非 bypass 模式冲突。

除此之外,下面这些也走 2:配置加载失败、模型无法解析、图片读取失败、裸 --resume 但没有 TTY。

5.3 子命令自带的分支

「用法错误是 2、找不到/未登录是 1」是大致规律,但有真实的不一致:

命令退出 1 的情况退出 2 的情况
config get键未设置用法错误、未知子命令
config unset—键不存在也是 2(与 get 相反)
mcp remove名字不存在用法错误
mcp status任一服务器启动失败用法错误、变量缺失
skills read找不到技能,或 --limit 非法未知子命令、参数个数不对
todo removeid 不存在id 格式非法、用法错误
planPlanStoreError(非法 sessionId、超限)无会话、缺 --body
whoami未登录、会话过期未知子命令

plan show / plan clear 在没有计划时仍返回 0, 只打印 No plan exists for session …;logout 在未登录时也返回 0。

6. 会话与账本

6.1 会话存在哪里

会话不是散落的文件,而是一份按项目目录归档的事件账本:

~/.spark/projects/<编码后的项目目录>/<sessionId>/events.jsonl

目录名由两段拼成,这个规则值得记住,因为你去磁盘找文件时只能靠它:

  • 把绝对路径里所有非 [a-zA-Z0-9._-] 的字符换成 -,去掉首尾 -,取最后 96 个字符(空了就用 root);
  • 再接一个 - 和绝对路径 sha256 的前 12 位十六进制。

事件按行追加,每行一个 JSON。同目录下可并存多个会话,互不干扰。

6.2 三个入口的差别

命令选中的会话找不到时
spark -c当前目录最近更新的主会话(排除 subagent)不报错:打印提示并开新会话,退出 0
spark -r <id>按完整 id 精确匹配(包含 subagent 会话)报 Session not found: … + 最近 5 条提示,退出 2
spark --resumeTUI 里手动选没有 TTY 时报错退出 2

最容易踩的坑:spark sessions 列表里显示的是8 位短 id(去掉 session_ 前缀后取前 8 个字符), 而 --resume 只接受完整 id。直接把列表里的短 id 粘进去一定失败。 完整 id 要用 spark sessions --json 拿,或在 TUI 里用 /status 看。

6.3 恢复时的权限语义

会话会把当时的权限模式记进账本。恢复时:

  • 显式给了 --permission-mode → 覆盖账本里的模式;
  • 没给 → 沿用会话原有的模式,而不是当前配置里的默认值。

另外两条硬约束:恢复 subagent 会话会被明确拒绝; 恢复一个带未终止 turn 的会话时,会先补写一条 turn.failed,把断掉的轮次收尾。

7. TUI 与 plain REPL

7.1 什么情况下会进 TUI

进入 TUI 需要同时满足七个条件,并且没有提示词:

--plain 未给 且 --json 未给 且 --output-format 是 text 且 stdin 与 stdout 都是 TTY 且 CI != 'true' 且 TERM != 'dumb'

注意 --output-format json / stream-json 会让内部 json 标志为真, 从而永久关闭 TUI——这是「加了输出格式就不进界面了」的原因。

还有一类输入永远不会进 TUI:-i/--image 只对一次性任务有效, 带子命令或没有提示词时都会先被拦掉(见 第 8 章)。

7.2 斜杠命令

公开的是这 9 条(/help 显示的就是它们):

命令作用
/help显示命令与快捷键
/status会话 id、排队 turn、事件数
/model切换模型或配置本地渠道
/perm选择权限策略
/effort选择推理强度
/update检查并安装新版本(--check 仅检查)
/sessions选择并切换到历史会话
/clear开启全新会话
/exit退出(Ctrl+C 两次同效)

还有一条未公开的 /quit:它和 /exit 走同一个分支, 但不在命令表里——不出现在 Tab 补全和 /help 中,也不占用保留名, 所以项目里的 .spark/commands/quit.md 可以把 /quit 抢注成自定义命令。 未知命令会提示 未知命令: … · 输入 /help 查看命令。

/model、/perm、/effort、/clear、/sessions在有 turn 正在跑时只提示不执行,不会打断进行中的工作。

7.3 快捷键

按键行为
Esc有草稿时先清空草稿;草稿为空则中断当前 turn
Shift+Tab在 manual ↔ auto 之间循环(永远不会切到 bypass)
Ctrl+O开关思考流展示
Ctrl+E展开/折叠折叠区
Ctrl+U / Ctrl+W行编辑快捷删除
Shift+Enter / \+Enter换行而不提交
↑ / ↓历史输入
Tab命令补全
Ctrl+C按两次退出

Shift+Tab 到不了 bypass 是刻意的:完全访问权限必须在 /perm 里 经过二次确认才能开启。另外 Ctrl+V 粘贴图片只出现在欢迎框提示里,不在 /help 的快捷键表中。

7.4 权限模式与推理强度

权限模式只有三个值:

值含义
manual每次工具调用按策略询问
auto按规则自动放行低风险操作
bypass完全放行,仅用于受控环境

默认值不是帮助文本写的 manual。实际链是 「显式 flag → [permissions].mode → /perm 持久化过的 agent.permission_mode → manual」。 TUI 里 /perm 的选择会写回全局配置,所以下一次启动仍然生效。

spark --permission-mode bypass "跑一遍测试" # stderr 会打印: # DANGER: permission bypass is active; registered tools may execute without approval.

这条警告只在一次性任务与 plain REPL 路径打印,TUI 路径不打印。--dangerously-skip-permissions 是 --permission-mode bypass 的别名, 但和显式给出的非 bypass 模式同时出现会报错。

推理强度五值 off | low | medium | high | max,默认链同样是 「flag → 持久化偏好 → high」。off 关闭思考;max 在 Anthropic 上是 65536 token 的思考预算。

7.5 --plain 到底是什么

帮助文本写的是「Disable color and terminal redraw」,但实现里它选的是行式 REPL: stdin/stdout 都是 TTY 且没有提示词时进入,横幅是 spark plain REPL · /exit 退出。

  • 只识别 /exit 与 /quit,其它 /xxx 一律当任务文本发给模型;
  • 该路径完全忽略 --json 与 --output-format;
  • 配一次性提示词或配子命令时,--plain 没有任何效果。

8. 图片附件

-i/--image 把图片附加到一次性任务的提示词上,可重复出现:

spark -p "分析这张截图的问题" -i shot.png -i arch.jpg

8.1 格式与限额

项实际值
支持格式PNG / JPEG / WEBP / GIF
判定依据文件头魔数,不是扩展名
单轮张数最多 20 张
单张大小最多 20 MiB
合计大小最多 50 MiB

校验在读取阶段完成,整批失败就整体拒绝(不会只跳过坏的那张)。 因为按魔数判断,把 PNG 改名成 notes.bin 也能通过;反过来把一个文本文件改名成 .png 会被拒。

8.2 两条约束

  • 只能配一次性任务:图片在模型解析之前就读取校验,而 TUI 分支在此之前就已经被排除。
  • 不能配子命令:spark models -i a.png 会报 --image only applies to a task prompt, not to models. 并退出 2。

图片不会以 base64 形式写进事件账本——账本里只记制品引用,请求体里才是 data URL。 这也意味着账本可以安全地按行传阅,不会夹带图片内容。

9. 桌面联动:本地桥

「CLI 与桌面共用 ~/.spark,所以能看到同一批渠道」这句话只说对了一半。 真正让 CLI 用上桌面渠道的,是桌面主进程里跑的一个只监听回环地址的本地 HTTP 服务。 CLI 从不直接读桌面的数据库,也不直接读系统钥匙串——它只和这个桥说话。

9.1 桥是什么

项实际值
实现位置Electron 主进程内的 node:http 服务器
绑定地址127.0.0.1 + 系统分配的临时端口(listen(0))
端口每次启动都变,没有固定端口
发现方式只靠描述文件里的 endpoint 字段
是否可关闭不能:没有设置项、没有 IPC 开关,随应用启动无条件尝试创建

端口不固定意味着你无法靠 curl 127.0.0.1:某端口 直接调试——必须先读描述文件拿到 endpoint。 如果启动时绑定失败,只记一条 log.warn,整进程不会重试。

9.2 描述文件

~/.spark/hosts/sparkwork/bridge-<instanceId>.json

目录权限 0700、文件 0600,用「同目录临时文件 + fsync + rename」原子写入。 内容共 7 个字段:

{ "schemaVersion": 1, "host": "sparkwork", "instanceId": "<uuid>", "endpoint": "http://127.0.0.1:<port>", "token": "<32 字节随机数的 base64url>", "pid": 12345, "startedAt": "2026-09-19T…Z" }

token 在每次进程启动时重新生成,不做轮换,只存在于描述文件与内存里。 CLI 侧用严格的 zod schema 校验同一组字段——多一个字段整份描述会被判无效。

两侧的文件名正则不等价,这是最容易写错的一处。桌面端接受 bridge.json(单实例时代的遗留名,会被 GC 清理), 而 CLI 侧的正则要求必须有 -<id> 段, 所以 bridge.json 对 CLI 来说等于不存在。

9.3 发现与选主

CLI 每次开始任务时都重新发现一遍,流程是:

  1. 扫 ~/.spark/hosts/sparkwork/ 下匹配 bridge-<id>.json 的普通文件;
  2. 按文件 mtime 倒序取前 8 个作为候选;
  3. 逐个校验权限(非 Windows 要求 0600 或更严)、大小(≤ 64 KiB)、endpoint 必须是回环 HTTP且不带凭据/路径/查询/fragment;
  4. 带 token 请求 /v1/catalog;
  5. 成功的候选里,取 startedAt 最大的那个作为胜出者。

探测超时分两档:pid 存活给 2000 ms,pid 已消失只给 250 ms—— 后者是防「端口复用巧合应答」拖慢启动。

多开桌面应用是支持的:每个实例写自己的描述文件,退出时只删自己那份; 启动时还会清理「能证明已死」的残留(判定依据是 kill(pid, 0) 返回 ESRCH, EPERM 视为存活)。解析失败的垃圾文件不会阻塞启动,交给 CLI 侧跳过。

9.4 桥的全部端点

只有三条有效路由,没有「凭据端点」这种设计:

方法路径返回
GET/v1/health{"ok":true,"schemaVersion":1,"host":"sparkwork"}
GET/v1/catalog模型目录:revision / generatedAt / defaultRoute? / routes[]
POST/v1/proxy/<providerId>/v1/messages代理到上游 Anthropic Messages
POST/v1/proxy/<providerId>/v1/responses代理到上游 OpenAI Responses

revision 是 sha256(JSON.stringify({defaultRoute, routes})),routes 里含渠道显示名,所以改一个渠道的名字也会换 revision。

/v1/health 也要 token(鉴权在路由分发之前), 而且它在整个仓库里没有任何调用方——是一条定义了但没人用的路由。

鉴权头有两种写法,两种都必须支持:authorization: Bearer <token>或 x-api-key: <token>。原因是 CLI 的 Anthropic 路由把 token 放进 x-api-key、 OpenAI 路由放进 authorization。只实现一种会出现「Anthropic 渠道全部 401」。

10. 哪些渠道会被桥接

这是本章最容易写错的部分。桥不会把桌面里的全部渠道暴露给 CLI, 筛选规则相当窄。

10.1 协议判定规则

渠道必须命中下面两条之一,否则根本不出现在目录里:

渠道类型条件桥协议
Anthropicprovider === 'anthropic'anthropic-messages
OpenAIprovider === 'openai' 且 codexApiKind === 'responses'openai-responses

也就是说:OpenAI 渠道必须显式标记为 Responses API 才会进桥。 普通走 /chat/completions 的渠道在桌面里能用,但在 CLI 里彻底看不到。 这一点很反直觉,因为桌面端自己两种协议都支持。

按内置预设统计:Anthropic 系全部可桥接(约 16 条),OpenAI 加 codexApiKind: 'responses' 的只有 5 条, 其余约 32 条 OpenAI 兼容渠道(含通义千问、Google Gemini、DeepSeek 这类未标记 codexApiKind 的预设) 都不进桥。想知道某个渠道能不能用,看它有没有显式的 codexApiKind: 'responses' 即可。

10.2 排除集

除协议外,还有三类过滤:

  • enabled === false 的渠道不出现;
  • modelType === 'image' 的渠道不出现;
  • 下面四个 id 被硬编码排除。
local-cli local-codex-cli claude-auto-router codex-auto-router

这个排除集是承重的,不是冗余: 前两个是「本机 AI 工具」渠道,没有 Key 也没有 Endpoint, 但它们有非空的模型名——如果不排除,桥会真的生成sparkwork:local-cli:claude cli 这样的路由,然后在调用时返回 503。 后两个是运行时合成的「自动路由」profile,本身模型名为空。

另外,被时段策略临时禁用的渠道/模型也会从目录里消失——目录是每次请求现算的,不是启动时快照。

10.3 上游地址如何推导(含一处不一致)

桥拿到 providerId 后,按下表推导真正的上游地址:

协议规则
anthropic-messages已以 /v1/messages 结尾 → 原样;以 /v1 结尾 → 补 /messages;否则补 /v1/messages
openai-responses已以 /responses 结尾 → 原样;以 /chat/completions 结尾 → 换成 /responses;最后一段是 /vN 形式 → 补 /responses;以 /v1 结尾 → 补 /responses;否则补 /v1/responses

/vN 规则这条曾有一处真实缺陷:桌面端一直有这条规则, 而桥的实现里此前没有,导致因为地址以 /vN 结尾而被自动标成responses 的编程端点(如 …/api/coding/paas/v4)会被拼成…/paas/v4/v1/responses——同一个渠道「桌面里能用、CLI 里报错」。 该缺陷已在桥侧修复:桥的推导与桌面端 getOpenAiResponsesEndpoint 逐分支对齐,并用回归测试锁定(覆盖…/paas/v4 与 …/coding/v3 两类端点)。如果你还在旧版本上遇到这个症状,升级桌面端后重启 CLI 即可。

10.4 在 spark models 里长什么样

每行格式固定是「选中标记 + 模型名 + 渠道名 + 协议 + 来源」:

* gpt-5.5 OpenAI openai-responses [sparkwork] claude-sonnet-4-5 Claude anthropic-messages [sparkwork] my-local local anthropic-messages [local]

来源只有 local 与 sparkwork 两个值, 终端里不出现「bridge」这个词——看到 [sparkwork] 就是指它来自桌面桥。 路由 id 的内部格式是 sparkwork:<providerId>:<model>, 但列表和 TUI 显示时会还原成裸模型名。--model 两种都接受: 既可以直接给路由 id,也可以给唯一的模型名;名字重复时报Model X exists in multiple SparkWork providers; use one of: …。

目录里桥路由排在本地配置的模型前面,所以打开 /model 时 光标默认落在桥路由上。但默认选中项另有优先级链:

--model > SPARK_MODEL > 项目层 [agent].model > 全局 [agent].model > 桌面的 defaultRoute

只要 CLI 侧存过任何本地默认,桌面的 defaultRoute 就轮不到——这是刻意的「sticky」设计。

最后一个必须知道的后果:桌面没开时没有回退,只有报错。 如果你之前用 TUI 选过一个桥路由,它会被写进 ~/.spark/config.toml 的 [agent].model; 关掉桌面再启动 CLI,就会撞上Model <id> is not defined locally or available from SparkWork。 解法是重开桌面,或在 TUI 里换回本地模型。

11. 桥的安全模型

这部分的边界值得单独讲,因为它决定了「谁能读到你的 Key」。

11.1 三道约束

约束实现
只监听回环服务端只 bind 127.0.0.1;客户端还会拒绝任何非回环 endpoint
随机 token32 字节随机数,每次启动重新生成,随描述文件传递
文件权限目录 0700、文件 0600;CLI 侧反向校验,权限过松直接拒绝

没有 CORS / Origin 检查,也没有 Host 头校验—— 防护完全建立在「回环 + 随机 token + 文件权限」之上。这意味着同一台机器上的其他进程 只要能读到描述文件,就能用你的渠道额度。

11.2 请求头白名单

桥转发到上游的客户端头是一张 6 项白名单,其余一律丢弃:

anthropic-version anthropic-beta openai-organization openai-project x-opencode-session user-agent

x-opencode-session 的存在有个具体原因:某些网关要求每个会话带稳定的会话头, 缺了直接返回 400。桥生成不出这个值(它必须跨请求稳定且与会话一一对应),只能原样透传。 桥对头值从不记录,日志里只有「这个头在不在」的布尔值。

客户端没带 User-Agent 时,兜底值是 sparkwork-cli-bridge, 这样上游不会看到 Node 的默认 UA。

11.3 体积与流式

项上限
请求体32 MiB(超出返回 413 request_too_large)
未完成的 SSE 累积缓冲8 MiB

请求体是全量缓冲成字符串再转发,即使 stream:true 也不做背压转发。

流式响应按完整 SSE 帧转发,帧边界取 \n\n 与 \r\n\r\n 中较早的一个。 上游中途断流时,半截 JSON 会被丢弃,然后追加一个协议原生形状的错误帧—— Anthropic 用 {type:"error", error:{type:"bridge_stream_error"…}}, OpenAI 用 {code:"bridge_stream_error"…}——这样下游解析器不会看到两个事件被粘在一起。 连接 EOF 处那个「天然完整」的尾帧还要再验一次能否解析成合法 JSON 或 [DONE]。

11.4 凭据是怎么用的

凭据链路是:URL 里的 providerId → 桌面 SQLite 里的渠道行 → 系统钥匙串。 桥每次代理请求现取一次凭据,不缓存;取到空串就返回 503。

但那个 503 的错误码永远到不了调用方。桥的通用错误处理规则是「状态码 < 500 时回传原始消息,否则统一返回bridge_request_failed」,而凭据缺失恰好是 503。 所以 CLI 侧只会看到 {"error":"bridge_request_failed"}; 想知道到底是「凭据没了」还是别的,得去翻桌面端日志。

凭据只写进上游请求的 x-api-key(Anthropic)或 authorization: Bearer(OpenAI), 且会覆盖客户端传来的同名头——调用方无法用自己的值顶替。 回传给调用方的响应头也只是一张固定白名单(content-type、retry-after、request-id 及两个厂商的 request-id)。 目录响应里不含任何密钥。

另外,桥本身不做 sk- 之类的密钥模式脱敏,只做控制字符清洗与 1024 字符截断。 正常路径上密钥不会出现在响应体里,但如果上游把密钥回显到错误文本里,桥不会主动擦掉。

12. 数据落点与配置分层

12.1 ~/.spark 里到底有什么

全局根的解析是 SPARK_HOME ?? ~/.spark。真实落盘项:

路径内容权限
config.toml全局配置层文件 0600 / 目录 0700
credentials.json账号会话(含 token / refreshToken)0600
projects/<编码目录>/<sessionId>/events.jsonl会话账本—
artifacts/<sha256 前 2 位>/<sha256>工具结果制品—
settings.jsonHooks 的 user 作用域配置—
hosts/sparkwork/桌面桥描述文件目录目录 0700
update-check.json更新提示的最近检查时间与版本—
update.lock更新并发锁—
bin/spark install 的默认 launcher 目录—

12.2 项目级 .spark

以 process.cwd() 为根,不会向上查找父目录:

路径内容
.spark/config.toml项目配置层,覆盖全局
.spark/todos.jsonspark todo 的任务清单
.spark/plans/<sessionId>.mdspark plan 的会话计划
.spark/settings.json / settings.local.json项目级 Hooks 配置
.spark/skills项目级技能

12.3 记忆是那个例外

长期记忆不在 ~/.spark 下,而在:

~/.spark-agent/memory/<scope>/

默认根是 SPARK_AGENT_HOME ?? ~/.spark-agent, 而 CLI 构造记忆存储时不传自定义 home, 所以设置 SPARK_HOME 对记忆完全无效。这是一个非常容易踩的坑: 你以为把 SPARK_HOME 指到别处就隔离了全部状态,实际记忆还留在原处。

项目作用域的记忆目录名是 .spark-agent/memory(注意是 .spark-agent,不是 .spark), 而且它会逐级向上查找第一个已存在的目录——这一点与 todo / plan 只认当前目录的行为不同。

12.4 配置分层与合并

两层合并规则是「全局在下、项目在上,项目赢」,实现为对象递归合并、数组与标量整体替换。 真实的 section 与 key 只有这些:

section键
[agent]model、permission_mode、reasoning_effort、failover、max_retries、retry_initial_delay_ms、retry_max_delay_ms、retry_jitter_ratio
[providers.<id>]protocol、base_url、api_key_env
[models.<id>]provider、model、context_window、max_tokens、capabilities
[permissions]mode、allow、deny、ask
[tools]enabled、disabled(二者互斥)
[mcp] / [mcp.servers.<name>]startup_timeout_ms;enabled、command、args、env、cwd、url、headers
[memory]enabled、max_inject_tokens、agent_id
[platform]server_url、web_login_url
[update]base_url、version、enabled

根 schema 是 strict 的:写一个不存在的键不是被静默丢弃,而是报错退出 2。 所以 spark config set foo bar 会失败。

表级递归合并有一个隐蔽的坑。如果全局写了 mcp.servers.x.url、项目层写了 mcp.servers.x.command, 合并结果是两者共存,而 schema 明确禁止 command 与 url 同时出现。 于是所有读配置的命令都会退出 2。修复只能用 spark config unset(写前校验的是合并后的结果)。

13. 子命令逐个说

13.1 login / logout / whoami

登录走的是浏览器 + PKCE + 轮询,不是设备码,也不是本地回调:

  1. 生成 state、codeVerifier(各 32 字节随机数的 hex),codeChallenge = sha256(verifier);
  2. 拼出 ?desktop=1&state=…&challenge=… 的登录页并打开浏览器;
  3. 轮询服务端 GET /auth/desktop/poll?state=,间隔 2 秒、总超时 300 秒;
  4. 成功后一次性 POST /auth/desktop/exchange 换回会话。

state 与 verifier 只存在进程内存里,交换成功前不写盘。--no-browser 只打印登录链接,适合远程或容器环境。 轮询间隔与超时没有对应的命令行开关(它们只是测试注入口)。

spark login --no-browser # 打印链接,自己在浏览器打开 spark whoami # 显示当前账号与服务器 spark logout # 删除本地凭据

credentials.json 的结构是{version, serverUrl, session:{token, refreshToken, userId}, account?, updatedAt}, 写入时 0600;如果加载时发现权限被放宽,会自动 chmod 回 0600。 文件损坏或字段不符会直接报错,不会静默登出。

spark logout 只删本地文件,不调用任何服务端吊销接口。CLI 使用的服务端端点里根本没有 logout / revoke, 所以「登出后旧 token 在服务端是否还有效」在代码层面无法确认——按仍然有效来处理更安全。

whoami 用的是存储里的 serverUrl(不是当前配置里的), 两者不一致只在 stderr 提示。会话过期时会清空凭据并退出 1。

13.2 config

子命令行为
spark config / list列出生效配置,每行 key = value [scope]
get <key>text 模式打印原始值;--json 给完整条目
set <key> <value>默认写全局层;--project 写项目层
unset <key>从默认(全局)层删除;键不存在退出 2
path打印全局与项目的配置文件路径,并标注是否已创建

两个必须注意的口径:

  • config list 只反映显式写过的键,schema 默认值不出现;
  • config get 读的是合并结果,看不到 schema 默认值—— 比如配置文件里没写 agent.max_retries,get 会说「not set」,尽管运行时默认是 2。

取值解析规则:以 [ / { 开头按 JSON 解析成数组或对象;true/false 转布尔;纯数字转 number;带引号会剥掉引号。

13.3 mcp

stdio 与 http 两种服务器的配置形状:

[mcp.servers.filesystem] command = "npx" args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] env = { TOKEN = "abc" } cwd = "/tmp" [mcp.servers.remote] url = "https://example.com/mcp" headers = { Authorization = "Bearer xyz" }

schema 强制几条约束,违反会直接退出 2:

  • command 与 url 必须二选一;
  • args / env / cwd 只能配 command;
  • headers 只能配 url;
  • 服务名要求 ^[A-Za-z0-9._:-]{1,96}$。

spark mcp add 写的是整表替换:同名服务器的原有字段会被清掉,不是字段级合并。 要临时关闭某个服务器,只能spark config set mcp.servers.<name>.enabled false,add 本身不提供关闭选项。

spark mcp status 会真的把每个启用的服务器拉起来: 串行启动、每个 15 秒超时,读注册表里 mcp__<name>__ 前缀的工具,然后关闭。 任一失败即退出 1。所以它是真实连接测试,不是读配置文件。 因为要展开 ${VAR},环境变量缺失会报错退出 2,而 mcp list 不展开变量所以不会。

13.4 memory

子命令作用
list列出记忆摘要(不含正文)
search <q>按摘要检索
recall <id>读取一条完整记忆(含正文);会写盘——命中计数 +1
save写入一条记忆,需要 --scope

--type 与 --scope 是两个不同域的枚举,这点最容易搞混:

枚举取值
--scopeuser | project | agent
--typeuser | feedback | project | reference ——没有 agent

不指定 --type 时按 scope 推导:project scope 落成 type: project,其余(含 agent)都落成 type: user。 所以「agent 作用域的记忆类型是 agent」是错的。

几条实用口径:

  • --confidence 取值范围 0–1(含端点);--limit 是 1–100;
  • 同名条目是覆盖而非新增(按 name 在目标作用域内查,命中就沿用原 id 重写);
  • list 的排序不是 user→project→agent,而是按类型优先级 feedback < user < project < reference,再按更新时间倒序;
  • 手写 .md 放进目录不会被识别——id 必须匹配 ^(usr|prj|agt)_…,否则整条跳过;
  • [memory].enabled = false 不阻止 CLI 读写,它只影响提示词注入。

13.5 plan

会话计划是会话私有的 Markdown,落在 <cwd>/.spark/plans/<sessionId>.md, 与项目级的 todo 刻意分开。CLI 与模型工具共用同一个 store, 所以 spark plan show 读到的就是模型写下的计划。

子命令行为
show(别名 read)打印当前计划;没有则打印提示并退出 0
set(别名 write)整体覆写,需要 --body
append读出现有计划,用空行拼接后整体写回
clear删除该文件;文件不存在仍退出 0

不写 --session 时默认用该项目下更新时间最新的会话; 一条会话都没有才报错退出 2。sessionId 有格式校验(只允许 [A-Za-z0-9._-]、≤128、不含 ..), 单文件上限 256 KB。

有个校验缺口:plan set <任意合法 id> 不校验该会话是否存在, 会直接给你创建一个 .spark/plans/<id>.md。

13.6 skills

技能从四个宿主根发现,再加上每个祖先目录下的同名四兄弟(越深越优先):

~/.claude/skills ~/.codex/skills ~/.agents/skills ~/.spark/skills

spark skills list [query] 按 name / description / id 做子串匹配, 默认上限 200 条(超出会被截断且不提示);--limit 允许 1–200。

spark skills read <id-or-name> 打印完整的 SKILL.md 正文,--json 时正文字段名是 instructions。 目录扫描时每个技能只读前 16 KB 解析 frontmatter,整文件上限 256 KB; 再次加载会校验 name 与目录清单一致,不一致报 changed_document。

桌面里装的技能在 CLI 里看不到。桌面的托管技能目录是应用数据目录下的 skills/, CLI 侧完全不引用它;两边只共享上面那四个宿主目录。

13.7 todo

任务是单个 JSON 文件 <cwd>/.spark/todos.json, 结构是 {version:1, items:[{id,title,status,priority,notes,createdAt,updatedAt}]}, 最多 1000 条。id 形如 todo_<uuid>。

枚举取值
--statuspending | in_progress | completed | cancelled
--prioritylow | normal | high

两条容易错的:

  • todo list 不传 --limit 是「全量」,不是默认 10 或 20;传了则必须在 1–100 之间。
  • todo clear 只删已完成与已取消;要连待办一起清掉必须加 --all。

列表排序是按状态分组(进行中 → 待办 → 已完成 → 已取消),再按优先级降序、更新时间降序。add 的 --title 与位置参数互斥;标题禁止换行、最长 240 字符。

13.8 未公开的别名

下面这些等价写法不在 --help 里,但确实是同一分支:

写法等价于
spark config(无子命令)spark config list
spark mcp(无子命令)spark mcp list
spark memory addspark memory save
spark plan read / writespark plan show / set
spark todo deletespark todo remove

另一类不一致是「位置参数被静默忽略」:spark mcp list <x>、spark config path <x>、spark memory list <x>都不会报错;而 spark todo list <x> 反而会报用法错误退出 2。

14. 版本与更新

14.1 从哪里取版本

发布源与版本的解析顺序是两条独立的链:

项优先级(高 → 低)
发布源--base → SPARK_RELEASE_BASE → SPARK_INSTALL_BASE → 全局 [update] base_url → 内置地址
固定版本--target → SPARK_INSTALL_VERSION → [update] version

两个容易踩的坑。第一,install.sh 只认 SPARK_INSTALL_BASE(不认 SPARK_RELEASE_BASE), 所以设了后者的机器上,spark update 与 install.sh 会走两个不同的源。 第二,SPARK_INSTALL_VERSION / [update] version 一旦设置就会永久钉死后续每次更新, 而且钉死状态下比当前更旧的版本会被当成「有更新」并执行降级。

项目级 .spark/config.toml 里的 [update] 只能改 enabled, 它的 base_url 与 version 会被忽略。

14.2 spark update 的退出码

码含义
0有可用更新(--check)或更新已应用
1已是最新 / 远端更旧 / 预发布被门槛挡住
2用法错误(由参数解析产生,不是 update 模块)
3检查或升级失败
4另一个更新正持有锁

「--check 发现新版本」是 0,不是 1—— 「什么都没做」才是 1。这一点与大多数工具的直觉相反。

分类顺序也值得注意:先相等 → up_to_date,再比较大小 → remote_older, 然后是预发布门槛,最后才是 update_available。 所以「远端是个更旧的预发布版」得到的是 remote_older,不是「有预发布可用」。

14.3 锁、校验与原子交换

  • 并发锁在 ~/.spark/update.lock,内容是{pid, token, acquiredAt},用 open(…, 'wx') 独占创建。 抢不到时:owner 进程被内核证实已消失(ESRCH)就立刻抢占;否则按 mtime 超过 15 分钟判为陈旧。 总共只尝试 2 次,第二次仍失败就返回码 4。
  • 锁目录刻意不受 SPARK_HOME 影响——把 SPARK_HOME 指到别处并不能换锁位置, 这是为了防止两个更新器竞争同一份安装。
  • 校验是双重的:下载后比对 tarball 的 sha256 与清单,再校验包内package.json 的 name / version 以及 Node engines。--target 路径不读 latest.json,改读版本化 tarball 旁边的 .sha256 sidecar。
  • 交换用两次 rename(不是符号链接):旧包改名到.spark-agent-backup-<pid>/package,暂存目录改名到正式位置。 换完会重链 npm bin 与 $SPARK_HOME/bin 两处 launcher,再跑一次健康检查; 任何一步抛错都会把备份 rename 回去。

两个限制:更新只作用于 @spark/agent 的 npm 全局树, 并且只重链默认的两处 launcher——如果你当初用spark install --bin /usr/local/bin 装的,更新后那个 launcher 不会被刷新。

被 kill 掉的更新会留下 .spark-agent-stage-* 与备份目录, 下次更新时会自动修复或清理。

14.4 更新提示

TUI 启动时会顺手做一次更新检查,规则如下:

项值
触发条件非 --plain、非 --json、输出格式是 text、stdin/stdout 都是 TTY、CI != 'true'、TERM != 'dumb'
检查频率24 小时一次(缓存在 ~/.spark/update-check.json)
硬超时4 秒
打印位置stderr,且在 TUI 退出之后
关闭开关SPARK_UPDATE_CHECK=0(只有字符串 0 有效)或[update] enabled = false

SPARK_UPDATE_CHECK=false 不生效,必须是 0。 另外 --json 模式下这次检查根本不会发起(不是「查了但不打印」), 因为 --json 会让内部 json 标志为真。 提示只针对「非预发布且严格更新」;抓取失败也会写缓存时间,避免每次调用都重试。

15. doctor 与排查

15.1 spark doctor 看什么

models 与 doctor 走的是同一个函数, 区别只是 doctor 额外做两件事:检查配置是否可加载、生成安装报告。 输出分几块:

SparkWork bridge: connected | not connected Diagnostic: … # 桥不可达时的原因 Stale bridge descriptors: N … # 陈旧描述文件数量 Selected model: … # 当前生效模型 Available models: N Configuration: ready | error — … # 配置是否可加载 spark on PATH: … # 安装报告 Node <版本>: …

退出码是「有模型且配置能加载 → 0,否则 1」——一个模型都没有时 doctor 也返回 1。

「Stale bridge descriptors」这个数字与它的文案不符。实现里它统计的是所有探测失败的候选(包括「进程活着但没应答」「权限读不了」「401」的活实例), 但文案写成了「left by SparkWork instances that are no longer running」。 而且这个数字受「最多探测 8 个候选」的截断影响。看到非零不必惊慌,先用spark doctor 的 Diagnostic 行判断真实原因。

15.2 排查对照表

现象最可能的原因怎么确认 / 怎么办
Model … is not defined locally or available from SparkWork之前选过的桥路由被写进 [agent].model,而桌面没开重开桌面应用,或在 TUI 里 /model 换回本地模型
spark models 里看不到某个桌面渠道该渠道不是 Anthropic,也不是 codexApiKind: 'responses' 的 OpenAI去桌面 Provider 设置里确认协议标记;普通 chat 渠道不会进桥
渠道在桌面能用、在 CLI 报错旧版本桥的地址拼接缺陷:上游地址多了一段 /v1(/vN 规则缺失,已在 10.3 所述版本修复)升级桌面端并重启 CLI;若仍复现,比对拼出的上游 URL
调用返回 bridge_request_failed桥侧 5xx,多半是凭据缺失(原始码 503 被通用规则掩盖)去桌面日志找真实原因;检查该渠道的 Key 是否还在
所有 Anthropic 渠道都 401鉴权只实现了 authorization 头,没实现 x-api-key两种头都要接受(Anthropic 协议走 x-api-key)
描述文件明明在却探测不到文件名不是 bridge-<id>.json,或权限宽于 0600,或 endpoint 不是回环CLI 侧会分别报 invalid / unreadable;对照 9.2 的约束
spark sessions 里的 id 喂给 --resume 失败列表显示的是 8 位短 id,匹配用的是完整 id用 spark sessions --json 取完整 id
裸 --resume 报错它需要 TUI;管道或非 TTY 环境下用不了改用 --resume <完整id>
--continue 没接上历史当前目录还没有会话,此时会静默开新会话确认 spark sessions 非空;注意会话按项目目录归档
恢复会话后权限模式变了给了显式 --permission-mode,它会覆盖账本里的模式不传该 flag 即沿用会话原模式
-i 加了却没生效图片只用于一次性任务;配子命令或无提示词都会退出 2写成 spark -p "…" -i a.png
图片被判格式不支持判定按魔数不按扩展名确认文件本身真的是 PNG/JPEG/WEBP/GIF
图片批次被整体拒绝超过 20 张 / 单张 20 MiB / 合计 50 MiB 中的任一条拆成多轮,或先压缩
加了 --json 却拿不到最终结果--json 给的是事件流,不是结果对象改用 --output-format json
--json 报冲突与 --output-format text/json 互斥只保留一个,或显式用 stream-json
子命令输出突然变成 JSON--output-format 会顺手打开所有子命令的 JSON去掉该 flag,或用 --output-format text
任务被取消想区分于失败取消是 130,失败是 1按退出码分支处理
脚本里 spark update --check 判定反了有更新是 0,无更新是 1按此语义写判断,别按常规直觉
更新一直报「另一个更新持有锁」(4)上次更新被中断,锁文件还在且未满 15 分钟确认没有更新的进程后删除 ~/.spark/update.lock
更新完 PATH 上的 launcher 没刷新更新只重链 npm bin 与 $SPARK_HOME/bin用非默认 --bin 装的,更新后需要重跑一次 spark install
SPARK_UPDATE_CHECK=false 没关掉提示只有字符串 0 被识别为关闭改成 SPARK_UPDATE_CHECK=0
改过 SPARK_HOME 但记忆还在原处记忆根是独立的 SPARK_AGENT_HOME ?? ~/.spark-agent要隔离记忆需同时设 SPARK_AGENT_HOME
放了手写的记忆文件但读不到id 不符合 (usr|prj|agt)_… 格式会被整条跳过用 spark memory save 生成,而不是手写
所有读配置的命令突然退出 2全局与项目层各写了 url / command,合并后冲突用 spark config unset 删掉冲突的一侧
spark mcp status 很慢它会串行拉起每个启用的服务器,每个 15 秒超时这是预期行为;临时禁用不需要的服务器可提速
桌面里装的技能 skills list 看不到CLI 只扫四个宿主目录,不读桌面托管目录把技能放到 ~/.claude/skills 等宿主目录,两边都能用
登出后担心 token 还在服务端有效logout 只删本地文件,没有吊销端点按「仍然有效」处理;需要真吊销得走服务端渠道

15.3 已知边界

下面这些是代码层面能确认的限制或缺口,写在这里避免你按「应该有」去排查:

  • spark serve 没有实现,只打印一句说明并退出 2。
  • 桥没有开关,无法通过设置或 IPC 关闭;启动失败也不会重试。
  • 桥没有 CORS / Origin / Host 校验,同一台机器上能读到描述文件的进程即可使用你的渠道。
  • 桥不缓存凭据,每个代理请求都重新取一次;也不做密钥模式脱敏。
  • /v1/health 在仓库里没有任何调用方。
  • 桥与 CLI 的目录 schema 是两份手写实现,没有共享类型也没有跨端契约测试; 任一侧加字段或改名,另一侧只会报「catalog 无效」。
  • 桥暴露的 descriptorPath 与 endpoint 生产代码不使用,只有测试读取。
  • 进程存活判定只靠 kill(pid, 0);pid 被系统复用后,残留描述文件将不会被回收。
  • 没有任何命令行开关能改登录轮询间隔与超时(2 秒 / 300 秒是固定值)。
  • plan set 不校验会话是否存在,可以给不存在的会话创建计划文件。
  • 更新提示的 base 解析不含 --base,所以用自定义源时提示可能指向另一个源。
  • install.ps1 的真机行为未验证:脚本头部自述仅做静态评审,且没有清理临时文件的 trap。

速查表

命令
spark(包名 @spark/agent)
Node 要求
>= 22.14.0
全局数据根
SPARK_HOME ?? ~/.spark
记忆数据根
SPARK_AGENT_HOME ?? ~/.spark-agent(独立于 SPARK_HOME)
项目级配置
<cwd>/.spark/config.toml(不向上查找父目录)
会话账本
~/.spark/projects/<编码目录>/<sessionId>/events.jsonl
桥描述文件
~/.spark/hosts/sparkwork/bridge-<instanceId>.json(目录 0700 / 文件 0600)
桥绑定
127.0.0.1 + 系统分配临时端口(每次启动都变)
桥端点
GET /v1/health · GET /v1/catalog · POST /v1/proxy/<providerId>/v1/(messages|responses)
桥路由 id
sparkwork:<providerId>:<model>
桥鉴权头
authorization: Bearer <token> 或 x-api-key: <token>
桥请求体上限
32 MiB(未完成 SSE 缓冲 8 MiB)
更新锁
~/.spark/update.lock(不受 SPARK_HOME 影响,陈旧阈值 15 分钟)
更新提示关闭
SPARK_UPDATE_CHECK=0(只有字符串 0 生效)
发布源优先级
--base → SPARK_RELEASE_BASE → SPARK_INSTALL_BASE → [update] base_url → 内置
退出码 · 一次任务
0 完成 / 1 失败 / 130 取消
退出码 · 用法
2(含参数解析失败、配置加载失败、模型无法解析)
退出码 · update
0 有更新或已更新 / 1 无更新 / 2 用法 / 3 失败 / 4 锁冲突
图片限额
20 张 / 单张 20 MiB / 合计 50 MiB
图片格式
PNG · JPEG · WEBP · GIF(按魔数判定,不看扩展名)
推理强度
off | low | medium | high | max
权限模式
manual | auto | bypass
技能扫描根
~/.claude/skills · ~/.codex/skills · ~/.agents/skills · ~/.spark/skills
登录轮询
间隔 2 秒 / 总超时 300 秒(无命令行开关)

常见问题

展开 7 条常见问题
  • 不装 Spark CLI 能用吗?

    能。桌面应用内置了完整的 spark 引擎,SDK 在进程内运行。CLI 只在你需要终端命令、TUI、脚本化调用(--output-format json)或把任务接进 shell 流水线时才需要单独安装。

  • 为什么 spark models 里看不到我在桌面上配的渠道?

    桥的筛选规则很窄:只有 Anthropic 协议渠道,以及显式标记 codexApiKind 为 responses 的 OpenAI 渠道会被暴露。普通走 /chat/completions 的 OpenAI 兼容渠道(含通义千问、Google Gemini 这类未标记 codexApiKind 的预设)在 CLI 里完全不可见。另外已禁用的渠道与图片类型渠道也不出现。

  • spark logout 之后服务端 token 会失效吗?

    代码层面无法确认会失效。spark logout 只删除本地的 ~/.spark/credentials.json,CLI 使用的服务端端点里没有任何 logout 或 revoke 接口。按「token 仍然有效」来处理更安全;需要真正吊销要走服务端渠道。

  • --json 和 --output-format json 有什么区别?

    --json 是历史兼容选项,输出的是事实事件 NDJSON 流,不含流式 delta。--output-format json 输出的是一行最终结果对象(含 sessionId、turnId、status、message、terminal)。要拿结构化结果用后者。--json 与 --output-format text 或 json 同时出现会报冲突,但与 stream-json 组合是合法的。

  • spark sessions 列出的 id 为什么不能直接用于 --resume?

    列表显示的是去掉 session_ 前缀后的 8 位短 id,而 --resume 做的是完整 id 的精确字符串匹配。用 spark sessions --json 取完整 id,或在 TUI 里用 /status 查看。

  • 改 SPARK_HOME 能隔离全部状态吗?

    不能。配置、会话账本、凭据、桥描述文件确实跟着 SPARK_HOME 走,但长期记忆的根是独立的 SPARK_AGENT_HOME,默认 ~/.spark-agent,而 CLI 构造记忆存储时不传自定义路径。要隔离记忆必须同时设置 SPARK_AGENT_HOME。

  • 桌面应用关掉后 CLI 还能用吗?

    能用,但只能用在 ~/.spark/config.toml 里显式配置过的本地模型。桥路由会全部消失:如果你之前用 TUI 选过桥路由,它已被写进 [agent].model,启动时会报 Model <id> is not defined locally or available from SparkWork。重开桌面或换回本地模型即可。