引子
起因是一个很具体的场景:日常在 AI 编程 IDE(Trae)里干活,但手上的 ZCode 账号还有额度没用完,于是很想"在 Trae 里问 ZCode"。
听起来只是"转发一条问题",但真正动手拆解之后,约束有三条:
- 提问必须真正走 ZCode 的通道,消耗它的账号额度,而不是在本地拼一个请求;
- ZCode 桌面端的历史记录里要能看到这次问答,方便回头翻;
- 回答要回到 Trae 当前对话里,后续还能被上下文和记忆引用。
第三条天然满足,麻烦的是前两条——尤其第二条,它把"简单转发"变成了"跨两个工具做数据同步"。这篇记录完整的排查与实现过程。
一、先搞清机制:CLI 和桌面端根本是两套存储
第一反应是"CLI 问完,桌面端自然就有记录"。实际验证后发现不成立,原因在存储分层:
- CLI 侧有一套自己的本地会话库,按工作目录维度记录会话;
- 桌面端的历史列表读的是另一套任务索引库,只认特定 provider 且有 CLI 创建时间戳的记录;
- 两者不共用同一份数据,也没有"扫描 CLI 会话库并自动导入"的逻辑。
测试里还发现一个关键细节:CLI 创建的会话,只有当它的工作目录在桌面端已被登记为项目时,才会进入桌面端的索引范围。在临时目录里跑的会话,桌面端就是看不到。
把这几条串起来,结论很清晰:
CLI 到桌面端是单向的(桌面端把任务交接给 CLI 时会写索引),终端直接发起的 CLI 会话不会自动出现在桌面端历史里。
所以"让历史里有记录"这件事,不能等,得自己把索引补上。
二、为什么最后做成 Skill,而不是 MCP
动手之前有个绕不开的选型问题:这个能力该做成 MCP,还是做成 Skill?
我的判断标准只有一个——这个能力到底是什么形状:
| 维度 | MCP | Skill(本地脚本 + 说明) |
|---|---|---|
| 本质 | 标准化的工具协议,服务端暴露一组能力 | 一段固定的本地流程编排 |
| 适合 | 多客户端复用、需要被动态发现的工具集 | 单机、依赖本地 CLI 与本地数据库的流程 |
| 成本 | 要起服务、定义 schema、维护传输 | 一个脚本,加一份使用说明 |
| 本例匹配度 | 低:能力只有一个,且强依赖本机的 CLI 与 SQLite | 高:调用 CLI → 写索引 → 回传文本,三步固定 |
这个能力没有"被多个客户端发现"的需求,本质上就是本机的一段流程。用 MCP 等于为了一个动作起一个服务,属于过度设计。
最终形态:一个 Skill 目录,包含三件东西——
- 说明文件(告诉模型什么时候用、怎么用、有哪些参数);
- 主脚本(负责调用 CLI、注入索引、返回结果);
- 一个小的同步脚本(负责写本地索引库)。
三、实现的三个步骤
1. 调用:走 CLI 的单次问答模式
CLI 支持"给一个问题、拿一段回答"的单次模式,这是消费账号额度的正路。调用完成后,会话会在本地会话库里留下一条记录,并带有一个会话 id——这个 id 是后面同步的钥匙。
2. 注入:把会话 id 写进桌面端索引
关键点只有一个:桌面端索引里的 task_id,必须与 CLI 会话 id 完全一致。有了这个一致性,桌面端读索引时就能把它当成一条正常的历史任务。
索引记录只需要满足桌面端"能读出来"的最小字段集合,大致是:
task_id = CLI 会话 id
provider = 该 CLI 对应的 provider 标识
cli_created_at = 本次创建时间(非空,否则不会被纳入列表)
title / 摘要 = 由问题首行截取
写成脚本就是一次 INSERT:
import sqlite3, time
def sync_session(session_id, provider, question):
conn = sqlite3.connect(INDEX_DB)
conn.execute(
"INSERT INTO tasks (task_id, provider, title, cli_created_at) VALUES (?, ?, ?, ?)",
(session_id, provider, question[:60], int(time.time())),
)
conn.commit()
conn.close()
这里有个容易忽略的点:顺序不能反。必须先拿到 CLI 的 session id,再写索引;反过来(自己生成一个 id 再去调 CLI)会导致两边 id 不一致,桌面端点开就是空会话。
3. 回传:把回答原样交回对话
脚本最后一步只做一件事:把 CLI 返回的正文打印到标准输出。这样调用方(Trae)拿到的就是纯文本,可以直接进入当前对话,被后续的上下文和记忆机制正常引用。
整体流程串起来就是:
问题 → 调 CLI(消耗额度、产生 session) → 用 session id 写桌面端索引 → 回传回答文本
这套实现已经整理成开源仓库,包含 Skill 说明、CLI 入口脚本和索引同步脚本三部分:github.com/zlw1125/zco…。想直接用的可以按 README 里的步骤安装,想自己改造的,重点看 _sync.py 里那段字段映射。
四、三个必须提前知道的限制
限制 1:CLI 通道的模型可能是锁死的
验证时发现一个反直觉的现象:桌面端已经在用更轻量的 Flash 模型,但 CLI 通道仍然固定用完整版模型——因为 CLI 用的是 OAuth 登录拿到的 account provider,而这个 provider 的默认模型由服务端下发,单次问答模式没有切换入口。
尝试过改配置文件、设环境变量、改数据库里的会话配置,全部无效;能生效的只有"桌面端里用 API Key 类型的 provider"这一条路。
这提醒我们:在封装别人的 CLI 之前,先确认哪些东西是你控制不了的,否则使用者提的第一个诉求就是"换个模型",而你无能为力。
限制 2:桌面端列表有缓存
索引写进去了,桌面端不重启也可能看不到——列表是缓存渲染的,需要重启或刷新索引才会重新读取。
这一点必须写进使用说明,否则使用者会认为"同步失败"。判断方法很简单:查一下索引库,记录在,就说明写入成功,只是展示层还没刷新。
限制 3:工作目录的登记状态会影响可见性
如果希望会话以一个"项目"的形态出现在桌面端,CLI 的工作目录必须是桌面端已登记过的项目路径;在临时目录里跑,索引可能写了但归属不清。
五、验证清单
这类"跨工具同步"最容易出现"看起来成了、其实是假的",所以验证要分三层:
- 额度层:确认这次调用确实走了目标账号(额度会变动),而不是本地伪造的回答;
- 存储层:直接查索引库,
task_id与 CLI 会话 id 是否一致、cli_created_at是否非空; - 展示层:重启桌面端后,历史列表里能否看到这条记录,点开后内容是否完整。
我至少跑了两轮真实会话做交叉验证,两轮都能在索引里查到对应记录——这比"脚本没报错"可靠得多。
结语
这件事本身不复杂,复杂的是"两个工具各自为政"的现实:CLI 有自己的会话库,桌面端有自己的索引库,中间没有桥。
真正有价值的三个判断是:
- 先验证机制,再写代码——"应该会自动同步"是最贵的假设;
- 选形态看形状——一个本地流程用 Skill,一组要被发现的工具才用 MCP;
- 同步靠一致性——两个系统只要约定好一个稳定的 id,就能低成本打通。
如果你的工作流里也有"A 工具干活、B 工具记账"的情况,这套"调用 → 用同一 id 写索引 → 回传结果"的套路,基本可以直接搬。完整代码在这里:github.com/zlw1125/zco…。