Repository navigation
Releases: twowb/znaide
Release list
znaide 1.0.9
znaide v1.0.9(2026-10-08)
这一版是两件事:模型请求可以带用户自定义的请求头(网关 / 计费 / 审计场景),
长期记忆从"单目录混存"改成 Global / Project 两级。
一、新增:模型请求的预设请求头
以前:只有 Authorization 一个头,没有任何扩展点。网关要求 X-Tenant-Id、
按 HTTP-Referer / X-Title 计费、内网要求按头做会话审计时,只能改代码。
现在:配置文件里写 extra_headers,命令行用 --extra-header K=V(可重复),
只在模型请求上生效(chat/completions、模型列表、连通性验证);网页抓取与更新下载
一个都不带 —— 免得把内部标识泄漏给任意第三方 URL。
{
"extra_headers": { "X-Request-Source": "znaide", "X-Tenant-Id": "${ZNAIDE_TENANT_ID}" },
"providers": {
"openrouter": { "extra_headers": { "HTTP-Referer": "https://github.com/twowb/znaide", "X-Title": "znaide" } },
"deepseek": { "extra_headers_enabled": false }
}
}- 合并:生效头 = 顶层表 ⊕ 当前 provider 表(同名 provider 覆盖;provider 里把值写成
空串 = 删掉继承来的那条)。别的 provider 不受影响。 - 开关:
extra_headers_enabled是总闸(顶层false= 一条都不发;provider 里false= 这家不发);
缺省是开 —— 空表本来就等于"没有头",默认设关只会制造"我配了怎么没生效"。 - 优先级:
--extra-header K=V/--no-extra-headers>ZNAIDE_EXTRA_HEADERS/ZNAIDE_NO_EXTRA_HEADERS配置文件;命令行与环境变量当次生效、不写盘。
- 值里支持
${VAR}/$VAR展开:敏感值走环境变量、不落盘明文;读不到就按原样发送并提示一句
(省得悄悄发一个字面量到网关还不知道)。 - 安全:
Authorization/Host/Content-Type/Content-Length/Content-Encoding/
Transfer-Encoding/Connection这些保留头配了会被忽略并提示;头名/头值不合法的项跳过 + 提示,
不会因此起不来。展示与报错只出头名,值脱敏。 - 配置向导多了一步"预设请求头":只读列出生效的头名 + 空格切换总开关(增删改仍在
config.json)。
二、新增:长期记忆分两级(Global / Project)
以前:只有一层 ~/.znaide/memories/,换个目录启动,别的项目的记忆全在眼前
(frontmatter 里的 type: project 只是个展示标签,不参与存放位置)。
现在:
~/.znaide/memories/ + MEMORY.md # 全局:随用户(个人偏好、机器环境)
<工作目录>/.znaide/memories/ + MEMORY.md # 项目:随仓库(本仓库约定、架构、待办)
- 两级各有独立索引;项目目录按需创建(第一次写项目记忆才建,进任何目录都不会平白多出空文件夹);
想只留本地就在仓库根加一行.gitignore的.znaide/memories/。 - 写:
memory_write加scope(global缺省 /project);读:memory_read缺省两级都读,
同名两条都返回并标注来源(项目在前),query结果按来源分组。 - 同名互不覆盖:
mtype(分类标签)与scope(存放位置)是两回事,可以"项目级存放 +mtype=user"。 - 注入:会话启动注入两段摘要(全局前 60 行、项目前 40 行,各自截断;某级为空则整段省略),
并常驻一行路由说明;项目段前面明确写着项目记忆来自仓库、属不可信输入、只作背景参考
(与项目级技能同一口径)。注入时机不变:新会话 //clear//resume/ 切模式或人格时才重建。 /resume记忆页:行首[G]/[P]标出来源,过滤词里打"项目"/global就只看某一级;
删除按各行自己的作用域走(确认框写明"项目 N 条 / 全局 M 条"),不会误删另一级的同名记忆。
三、验证
- 测试 260 项全绿(core 129 / 端到端 20 + 6 + 6 / tui 99),编译零警告;新增用例都钉在行为上。
- 请求头:mock 端点现在会记录每次请求的请求头(此前只看 body),用例断言流式 / 非流式 /
模型列表三条路都带上预设头、Authorization未被顶掉、网页抓取一个预设头都不带;真机上逐条验过
配置 / 环境变量 / 命令行三层优先级、provider 空串删头、保留头与非法值的提示。 - 记忆:无头跑一个带
<cwd>/.znaide/memories/的目录,抓请求体确认 system prompt 里两级摘要都在、
项目段带不可信声明;换到没有项目记忆的目录,Project 段整段消失;/resume记忆页在真机上验了
[G]/[P]角标与按作用域的删除确认。 - 顺带修了一个测试基础设施问题:两个用例各拿私有环境变量锁却都改数据目录,并行时会互踩
(表现为"单独跑绿、一起跑红"),现在共用一把锁。
四、说明
- 已有 v1.0.8 的:输入
/update或跑znaide --update升级到 1.0.9。 - 默认行为不变:不配
extra_headers= 与 1.0.8 一致;记忆缺省仍然写在全局那份,
老的~/.znaide/memories/原地可用、无需迁移;配置文件不设置就不会写入新键。 - 这一版的两个方向来自一位外部贡献者提的两条 issue。我们采用其需求、按自己的分层重新实现,
未合并其代码。
znaide v1.0.8
znaide v1.0.8(2026-09-29)
这一版是"网络环境"相关:给所有联网出口加了一个统一的代理开关,给模型调用加了弱网重试;
另修一个可能把配置文件读坏后整份写空的问题。
一、新增:代理统一出口
以前:模型请求、网页抓取、更新下载三处各建各的 HTTP 客户端,只有更新那处看了环境变量
—— 而且是手写的探测,地址写错时会静默跳过(等于"代理配错了就悄悄直连",最难排查的一种)。
也没有"一次配好、所有出口都走它"的入口,只能靠每个进程自己 export。
现在:配置文件顶层一个 proxy,三档 —— 跟随环境变量(默认,与旧版一致)/ 强制直连 /
手动指定地址,三处出口统一走这一份;命令行 --proxy <URL>、--no-proxy 当次生效、不写盘。
- 优先级:命令行 >
ZNAIDE_PROXY/ZNAIDE_NO_PROXY> 配置文件 > 标准环境变量;
标准环境变量只在"跟随环境变量"档生效 —— 配置文件里显式写了直连,就不会被 shell 里残留的
HTTPS_PROXY盖掉(否则"我明明配了直连排查,怎么还在走代理"没法查)。 - 手动档仍尊重
NO_PROXY:"国内服务商直连、国外走代理"直接配就行。 - 档位或地址写错不会让程序起不来:回落默认档并提示一句。
- 连接失败时的报错会带上当前生效的代理(用户名密码脱敏),从此能分清"代理没起"和"端点不对"。
二、新增:弱网重试
以前:一次 429、一个 5xx、连接抖一下,整个回合就失败了,只能手动重发;偶发的空回复一样。
现在:--retry N / 配置里的 retry,对一次模型调用重发(空回复 / 429 / 500 / 502 /
503 / 504 / 连接中断与超时),800ms 起指数退避、单次封顶 8s。默认关闭,不传就是旧行为。
- 只重试这一次调用:不会重复执行工具(有副作用的命令只跑一次),不占轮数预算,
失败那次的输出既不写历史也不计 token。 - 退避期间按 Esc 照样中断这一回合。
- 界面上会把失败那次已经吐出来的半截正文撤掉,换成一条
↻ 弱网重试 1/3(原因)的提示
—— 不撤的话会拼成"半截 + 完整"两段。 --no-retry可临时关掉;次数上限 0..=8。
三、修:一份读不懂的配置不再被整份写空
场景:保存配置是"读全量 → 改一项 → 整份写回",而读取失败时会退化成默认值;写盘又是
"先截断再写"。于是同一份 config.json 被两个实例或编辑器同时动过时,可能读到半截文件 →
把一份近乎空的配置写回去 → provider 条目、Key、人格、轮数全部丢失(不可逆)。
现在:写盘改成"同目录临时文件 + fsync + rename"的原子替换 —— 读者只会看到完整的旧文件
或完整的新文件;所有"读全量只为写回"的入口在读不懂时拒绝保存并给出可读的报错,
不再退化成默认值。
四、验证
- 测试 245 项全绿(core 119 / 端到端 19 + 5 + 6 / tui 96),编译零警告。
- 代理:用一个"记账"假代理验证请求到底走了哪条出口(手动档经过它、直连档无视环境变量、
默认档跟随环境变量、网页抓取与更新客户端同一个出口);真机验过死端口 / 非法地址 / 档位写错 /
--no-proxy/ 环境变量跟随等情形,并确认没配代理时配置里不会多出这个键。 - 重试:脚本化假端点验证真实请求次数(500 后成功 = 2 次、400 = 1 次、空回复会重试、
重试用尽 = 1+N 次);真机上默认一次请求即失败,--retry 2发两次拿到回复且 token 只记一次。 - 配置加固:损坏的 config.json 下保存被拒且原文件一字未动;正常保存后目录里无临时文件残留。
五、说明
- 已有 v1.0.7 的:输入
/update或跑znaide --update升级到 1.0.8。 - 默认行为不变:不配代理、不开重试 = 与 1.0.7 一致;配置文件无需迁移(不设置就不会写入这两个键)。
- 这一版的三个方向来自一位外部贡献者提的 PR。我们采用其需求、按自己的分层重新实现,
未合并其代码。
znaide v1.0.7
znaide v1.0.7(2026-09-18)
修一个会把会话历史切两半的问题:恢复会话之后,接着聊的内容不会写回那个会话。
一、修:恢复会话后,新消息跑到别的会话里
现象:/resume 或会话管理窗口恢复一个会话,接着聊 —— 上下文是接得上的(模型记得之前聊过什么),但落盘跑偏了:
- 被恢复的那个历史文件从此不再增长;
- 之后所有消息都写进一个新建的会话文件(文件名是启动时生成的那个 id);
- 状态栏显示的会话 id 也是这个新 id。
一段对话被悄悄切成两半,没有任何提示;再 --resume 哪一半,都只剩半截。
为什么:恢复时只换了"内存里的对话",没换"会话身份"。引擎的身份(session_id、历史文件路径、已经打开的文件句柄)是启动时定死的,而 load_history 只做了"读进来 → 重建系统提示 → 通知界面",这三样一个都没动 —— 后续每条消息自然继续写启动时那个新会话。
现在:恢复即"接着这个会话往下走" ——
- 身份一并接过来:
session_id取被恢复文件的文件名(与启动参数--resume同一规则),历史文件指向它,旧句柄作废,下次写入重新打开正确的文件; - 接上的历史文件不会再被补一条
{"meta":…}(以前每恢复一次就多一条); - 状态栏的会话 id 跟着变(以前一直显示新建的那个);
- 顺带:恢复的目标文件里没有消息时也回执一次,界面会照实清空(以前界面留着旧对话,而引擎的上下文其实已经空了)。
启动参数 --resume <片段> 本来就没这个问题(它一开机身份就是对的),坏的只有会话中途的恢复。
二、验证
- 测试 222 项全绿(core 108 / mock-e2e 19 / tui 95),编译零警告。新增用例
load_history_takes_over_session_identity盯着四条:身份切换、新消息续写回目标文件、不产生新文件、不发第二条 meta,以及SessionInfo/HistoryLoaded都送达界面。 - 真机(隔离数据目录 + 本地 mock 端点):恢复一个 2 条消息的会话 → 聊一句 → 目录里只有那一个会话文件、老内容保留、新的一问一答续写在后面、meta 只有 1 条、状态栏 id 从启动 id 变成被恢复的 id。
- 用一段真实的 148 条消息历史实跑恢复:提示"已恢复历史会话(148 条消息)",发一句后文件续写、端点收到 150 条消息、末尾正是那段对话的尾巴。
三、说明
- 已经装了 v1.0.6 的,
znaide --update会提示升级到 1.0.7(这次是正经的版本号,不是同版本原地替换)。 - 这个 bug 不影响模型上下文(恢复本身是成功的),只影响"新消息记到哪儿"。如果之前踩过,会话目录里会多出几个"半截会话",可以按 id 找出来、把后一段的内容接回原文件(手工就能做)。
- 没动依赖与配置格式,旧配置直接可用。
znaide v1.0.6
znaide v1.0.6(2026-09-16)
三件事:模型干活时打的字不再丢(排队,下一个轮边界带上);修掉"配置都齐了却说没配置";ctx 占用条不再拿 0% 冒充"空"。
一、新:工作中的输入排队
以前:回合跑着的时候输入框能打字,但按 Enter 什么都不发生 —— 不发送、输入框不清空、也没有提示,只能等这轮跑完再按一次。
现在:忙时按 Enter 直接排队。消息区提示 ⏳ 已排队,输入框清空,状态栏显示 队列 N。模型下一次调用就会带上它,当前这轮不停。
"轮"就是状态栏的 轮 N/200:模型要一次结果算一轮,一条消息可能跑几十轮,所以插入时机是"本轮工具跑完、下次调用之前",不用等这条消息干完。插进去的位置紧跟工具结果 —— 实测一条 4 秒的命令跑完,下一次请求就带上了排队的那句话。如果这轮之后没有下一轮(模型已经给出答复,比如纯问答),排队的内容就在回合结束时当新的一条发出去。
排队的内容不会打断正在跑的工具(它可能正改文件改到一半);想立刻停按 Esc,队列里剩下的接着发。
二、修:provider / model / key 都配了,界面却说"尚未完成配置"
用环境变量(或命令行)配好启动时,状态栏已经显示出模型,按 Enter 却回:
⚠ 尚未完成配置,不能对话。先输入 /config 完成配置。
原因是有两处判断"配置好没好":弹向导那处认配置文件、环境变量和命令行,对话这道闸门只认 config.json 在不在。现在闸门改用同一个判断,只用命令行(--provider/--model/--api-key)的情况也一样。真的什么都没配时行为不变:照旧弹向导、不让对话。
三、修:ctx 占用条不再拿 0% 冒充"空"
/resume 恢复会话后,状态栏的 ctx 占用条画的是一条空条 0%。但那是"还没量到"的占位:恢复出来的历史明明占着上下文,要等你发下一条、请求返回,它才跳到真实值(比如 43%)。/compact 之后同理 —— 摘要占多少也要下一轮才知道。
现在这两种情况画 ctx ?,不再冒充 0%。新会话和 /clear 之后照旧画 0%(那两种情况的上下文里确实没有对话)。压缩提示也跟着只认量到的数:没测到就不劝你压缩 —— 拿一个偏小的数字劝你别压缩,比不提醒更误事。
四、验证
- 测试 221 项全绿(core 108 / mock-e2e 18 / tui 95),编译零警告。新增用例盯着两条最容易搞错的边界:插话必须紧跟工具结果之后(顺序错了会让历史留下孤儿调用,恢复会话时端点直接 400)、以及"占用未知"必须画
?而不是 0%。 - 真机跑了五条:① 4 秒的命令一结束就带上排队的内容(轮次 1→2→3 不重置,说明还在同一个回合里);② 排两条,各在下个轮边界插入;③ 这轮之后没有下一轮时,回合结束当新消息发;④
Esc中断后剩下的一条紧接着发;⑤ 恢复会话后ctx ?,发一条(端点报 prompt 20.5k、窗口 40k)变成ctx █████░░░░░,再/clear确认后回到0%。 - 配置判定两个方向:仅环境变量 → 请求真的发出、不弹向导;什么都没配 → 仍弹向导、0 请求。
五、说明
- 忙时输入框不再变灰,标签显示
Enter 排队。 - 以前忙时按
Enter什么都不发生;现在会把字发出去,不想发就按退格删掉。 /compact、技能这类命令不进队列,仍按原路径交给会话引擎。- 恢复会话、压缩上下文之后,状态栏的 ctx 条显示
ctx ?,发一条消息就变回真实百分比。 - 没动依赖和配置格式,旧配置直接可用。
znaide v1.0.5
znaide v1.0.5(2026-09-10)
一次全仓审计后的修复与清理:5 类真 bug、死代码与无用依赖、6 处重复实现合并、2 项渲染/内存优化。
没有新功能,全部改动都在"把已有能力做对、做干净"。
一、修复:会真出事的问题
1. read_file 可被一个超大 limit 打崩 —— limit / offset 来自模型给的 JSON,旧实现用 start + n 直接做 usize 加法:limit: 18446744073709551615 会回绕成 end < start,&lines[start..end] 当场 panic,把整个 agent 循环带走。现在用 saturating_add 计算区间,越界只是空结果。同时把"整文件 read_to_string 再切行"改成逐行流式读:只把要展示的行留在内存里(几 GB 的日志不会再撑爆内存),"共 N 行,显示 a-b"的输出格式不变。
2. --max-turns 在交互模式下被静默忽略 —— 无头模式(-p)一直认这个参数,交互模式却没把它传进会话引擎(只有 config 生效),和"命令行 > config > 默认"不符。现在值经 tui::run → app::run 一路透传到 Session::set_max_turns,/config 改完重读时命令行覆盖依然优先。
3. 首次运行判定两个模式不一致,且漏认环境变量 —— 无头模式按"配置文件 或 任何命令行覆盖/环境变量"判断,交互模式只看配置文件是否存在;于是 --provider deepseek --model X --api-key Y 齐活的情况下,-p 能跑、交互模式却弹配置向导。另外那份手写的环境变量清单漏了 ZNAIDE_PROVIDER / ZNAIDE_API_KEY / OPENAI_API_KEY(实际 resolve() 是认的)。现在两个模式共用一个判定,环境变量清单由 core 统一提供(config::env_configured()),不会再各写一份。
4. undo 静默丢登记 —— manifest 写入失败(let _ =)+ 序列化失败落 unwrap_or_default(),会留下"快照文件在磁盘上、却不在 manifest 里"的孤儿:/undo 看不见、也永远不会被 GC 回收。现在写入失败会把错误报给上层(工具返回"备份失败"),不再假装成功。顺带:每次备份只读一遍 manifest(原来 count_in_session 读一次、紧接着又读一次)。
5. 会话管理窗口的筛选等于没有 —— / 过滤只活在"输入态":敲字时列表会实时筛,但一按 Enter 就退回全量、且输入态下按键全被输入框吃掉,筛出来的结果根本没法上下翻着挑。现在 Enter 保留筛选回到浏览(可继续在命中行之间移动)、Esc 清除;光标只在命中行之间走,筛没了的行不会被 Enter/空格/d 误操作;状态行显示 筛选「x」命中 N 条。
6. 配置向导的模型列表超过 25 个时,游标会移出屏幕 —— 列表固定只画前 25 行,↑↓ 却按全量移动:模型多的服务商(百炼、OpenRouter)会出现 ▶ 消失、回车选中一个看不见的模型。现在带滚动窗口(始终把当前项画在窗口内),并显示"正在显示 a-b";另外删掉了界面上那句"可输入筛选"——实际从来没有实现筛选。
7. glob 匹配最坏是指数级 —— simple_glob_match 是递归回溯实现,*a*a*a*a*…b 这类 pattern 会指数爆炸(而 pattern 来自模型输出,可以被构造出来拖死进程);另外 ** 分支的代码与 * 分支逐字节相同,注释却写着"匹配含多级 /"。现在改为迭代式单星回溯(最坏 O(n·m)),删掉死分支,并把语义写实:* 与 ** 在本工具里等价、都匹配含 / 的任意串,匹配行为与旧版完全一致。
8. token 统计的边界没对齐 —— 状态栏的 in/out/Σ 是计费口径的累计(每次模型往返都会把完整历史重发一遍、服务端按每份 prompt 各计一次),但显示上既没标注、换会话也不清零: /resume 恢复另一个会话后仍报上一个会话的数字,/clear 只清上下文占用而不清累计。现在标签明确为 累计 in/out/Σ,并在 /clear 与 /resume 时归零;/compact 不归零(同一会话,已经花掉的账不该消失)。
二、清理:删掉的死代码与依赖
- 无用依赖:
async-stream(workspace + core,全仓零使用)、futures-util(tui 里零使用,core 在用)。 - 整块不可达代码:
WizardAction::Save从未被构造,其 33 行的落盘分支不可达(真正的落盘走验证回执那条路),变体与分支一并删除;validate()里一个空的if分支;release_asset_name()末尾在当前平台被判定"不可达"的None(改用cfg!链表达,其它架构仍会走到)。 - 无人调用的 API:
env_key_for、is_idle_step、config_lock_notice_shown(只写不读)、UpdateSource::hint、Session的 6 个 getter(mcp/max_turns/current_model/cwd/cancel_token/mode)、list_history_sessions(只有_detailed变体在用)、undo::count_in_session。 - 零碎:
["o4-mini", "o4-mini", …]重复字面量、context_window上过期的"兜底 32k"注释(v1.0.4 已改成"未知"),以及测试里几处let mut/ 未使用变量。
三、合并:同一段逻辑只留一份
- 权限拒绝文案 4 处各抄一份(且已经抄得不一样)→ 收敛为
permissions::NEED_ACCEPT_EDITS/NEED_BYPASS两个常量; - 请求体组装
chat与chat_stream逐行相同(只差stream/stream_options)→ 抽成build_body(),以后加temperature之类不会漏改一边; - shell 的 stdout/stderr 两个
select!分支逐字节相同 → 抽成on_pipe_chunk(); - 按字符截断在 6 处各写一遍 → 新增
core::util::truncate_chars(s, max, suffix)(后缀由调用方给),顺手省掉一次chars().count()遍历; - 界面文案下沉 core:权限模式短标签/说明(
Mode::label()/hint(),CLI 原来叫"超级(YOLO)"、TUI 叫"超级")、更新结果文案(UpdateResult::describe()); - 显示宽度计算
wc()在md.rs与ansi.rs各一份(注释里还写着"两边要一起改")→ 合并到md::wc; - 技能 frontmatter 解析手抄的第二份(
extract_description)→ 复用parse_frontmatter。
四、性能(只动了低风险的两处)
- ctx 占用条的窗口不再每帧重算:
effective_context_window()内含to_lowercase()分配与一长串contains,以前每帧调用一次(以及每次空闲检查一次),现在只在配置变化时刷新并缓存。 - 工具输出入库即封顶 8 KiB:卡片只渲染前 500 字符 / 4 行,而整份输出(命令可能吐几 MB)以前会常驻内存直到会话结束;现在保头 + 一行"未留存"说明(完整内容仍照常交给模型与历史文件)。
五、验证
cargo test --workspace:214 项全绿(core 108 / mock-e2e 17 / tui 89),新增 8 项回归:glob 的形状与"回溯地狱"、read_file的越界/极大 limit/中文行、会话管理筛选的保留与光标、工具输出封顶、--max-turns覆盖、工具输出来源的窗口未知分支等。cargo build --workspace:编译期零警告(此前有dead_code/unreachable_code若干)。cargo build --release通过,--version输出 1.0.5;make build-windows交叉编译通过(校验保留的交叉工具链配置)。
六、行为与兼容性提示
- 界面文字:token 统计加了"累计"前缀;CLI 的权限名从
超级(YOLO)统一为超级(说明里仍带 YOLO);--update的输出文案改由 core 统一生成(措辞略有变化,失败仍走 stderr)。 - 会话管理:筛选现在会保留到你再按
/,浏览时Esc仍是关窗;要清掉筛选请按/后Esc(或在输入态退格删空再回车)。 - 工具输出留存:卡片里的成品输出超过 8 KiB 只保留开头并标注;要看全部内容请让模型重新读取或直接看文件/历史记录。
- 库 API(仅影响把
znaide-core当依赖用的场景):移除了若干未被使用的pubgetter 与list_history_sessions;新增core::util。
znaide v1.0.4
znaide v1.0.4(2026-09-10)
两条线:轮次不再被硬砍(可配 + 撞上限可续跑 + 打转刹车),配置语义修订(多 provider 真正可切换)。
一、轮次预算:从"硬砍 40 轮"到"可配 + 续跑 + 刹车"
旧实现是 max_turns: 40 硬编码、撞上限即停——不总结、不续跑,长任务直接被腰斩,且没有任何配置入口。
1. 上限可配、默认抬高 —— DEFAULT_MAX_TURNS 40 → 200;新增 --max-turns <N> 与 config 的 max_turns(0 = 不限)。优先级:命令行 > config > 200。0 在引擎里映射为无限预算。
2. 撞上限不再丢任务 —— 新增 SessionEvent::RoundsExhausted,交互模式弹"继续执行"确认框(y 再放一批 / n·Esc 停在当前进度);无头模式在结果里说明"已达 N 轮"以及怎么调大(--max-turns N,0 = 不限),不再只丢一句"任务没干完就停了"。
3. "原地打转"刹车(替代靠轮数硬砍) —— 以 (工具名, 原始参数) 完全相同为签名计数:重复到 3 次在工具结果尾部插一句提醒,劝模型换方法(给一次自救机会);重复到 6 次判定"原地打转",中止本轮并点名工具与次数。换参数不触发计数,所以"挨个读 30 个文件"这类正常长任务不会被误杀。
4. 状态栏显示轮次 —— 底部状态栏新增 轮 本轮已用/上限(不限时显示 ∞),数据由引擎每轮发的 SessionEvent::RoundStarted { used, limit } 驱动,不靠 UI 估算;/config 改完上限立即反映。
二、配置面板:轮数上限成为独立一步,并带出当前配置
- 配置向导由 4 步变 5 步:
1 服务商 → 2 模型 → 3 API Key → 4 轮数上限 → 5 验证。第 4 步直接敲数字即可改(0= 不限,留空 = 默认 200),Enter/s验证;第 3 步按Enter进入该步(老习惯直接按s仍可跳过)。 - 重开
/config带出当前已配置的值:服务商/模型/端点/Key/轮数上限,并在顶部显示"当前配置"摘要行;服务商列表游标落在当前那个上;模型列表自动选中当前模型;查询失败转手输时预填当前模型名。 - 两个细节:走
api_key_env的 Key 不会被预填成明文(否则保存会把 Key 落进配置文件),界面标注"来自环境变量,留空即保持";重选同一个服务商不再清空已配的 Key。
三、配置语义修订(v2):多 provider 真正可切换
旧语义有个自相矛盾的死结:顶层 model/base_url 覆盖预设,而 api_key 反过来由预设优先;并且面板每次保存都写顶层三件套——于是用过一次面板后,顶层永久遮蔽所有预设,改 provider 只换 Key 不换端点/模型。
- 面板保存改写进 provider 条目 ——
save()把 model/base_url/api_key 写进providers[当前服务商],不再写顶层三件套,并清空顶层。此后改"provider": "xxx"(或--provider xxx)= 换整套(端点 + 模型 + Key)。 - 三个字段口径统一 ——
model/base_url/api_key一律"顶层 > 预设",不再出现"新 Key 配老端点"的错配。优先级:CLI > 环境变量 > 顶层(手动覆盖)> provider 条目。 context_window下沉到 provider —— 各服务商可各配各的窗口(内置表仍作最后兜底);在 40k 的 ollama 与 1M 的云端模型之间切换时,ctx 占用条不再算错。- 预设按字段合并 —— 内置预设做底,你在
providers里只写要改的字段即可,不再因为写了base_url就顺手丢掉内置的model/api_key_env。 - 老配置自动迁移 —— 启动时把顶层
model/base_url/api_key/context_window搬进当前 provider 条目并清空顶层(一次性自愈,控制台提示"检测到配置由其他版本写入,格式标记已升级")。迁移前后生效配置完全一致,有单测与真配置干跑双重验证。
顶层三件套仍可用作手动临时覆盖(优先级高于预设),只是面板不会再写它们。
验证
cargo test --workspace:core 100 / mock-e2e 17 / tui 80,全绿。- 新增覆盖:轮上限用尽的无头提示、
max_turns=0真不限(连跑 9 轮不被截断)、重复相同调用被中止(≤6 次)、RoundStarted逐轮上报(used, limit)、config 的字段级合并 / provider 窗口优先 / 保存写进条目 / 迁移前后生效一致 /max_turns落盘往返。 - 真配置干跑:迁移后顶层清空、值进
providers.deepseek,启动解析出的模型不变。 cargo build --release通过,--version输出 1.0.4。
兼容性提示
- 老配置会被自动迁移一次(会改写
config.json,建议先备份)。 - 顶层
context_window现在只作兜底:建议把它挪进对应的 provider 条目(迁移会自动做)。 - 之前依赖"预设 Key 盖过顶层 Key"的行为已统一为顶层优先(仅影响同时手写了两处 Key 的配置)。
发布后补入的修复(同一版本号替换)
以下几处在 1.0.4 首次发布后随即修复,随本版本资产一并替换(未升版本号):
- 配置面板输入框支持粘贴 —— 此前向导一打开,粘贴就被整段丢弃,长 API Key / 端点只能手敲。现在文本输入态(API Key / 端点 / 模型名 / 轮数上限)可直接粘贴:只填入、不提交(回车才算),换行会被丢掉(单行输入);权限确认与破坏性确认弹窗(
y/n)仍不接收粘贴。 - 修掉"切回旧服务商时,面板里还是另一家的模型/Key" —— 切服务商时只更新了名字与端点,
model仍沿用上一家的,api_key又只查环境变量而不读该条目的明文(面板保存的 Key 就存在条目里),于是切过去 Key 显示未设置、验证必然失败。现在切换会带出目标服务商自己的模型与 Key(Key 顺序:该条目明文 > 该条目的api_key_env环境变量 > 空);模型列表里没有当前模型时置顶保留并选中,避免"只是切回这家、回车一下模型被换成列表第一项"。另外 Key 来自环境变量时保存不会写成明文,手输明文后该标记自动清除。 - 修掉 ctx 占用条把模型窗口算错(误报"快满了"、劝人做没必要的压缩) —— 分子没错(最近一次请求的真实
prompt_tokens),错在分母:窗口靠模型名查内置表,认不出就回默认 32768,而deepseek-flash(不含deepseek-v4)、qwen3.8-max(被 ollama 的qwen3兜底吞成 40960)这类真实模型 ID 都命中不了 → 占用条成倍虚高、变红,并弹出⚠ 上下文已用约 N%,可以 /compact的提示。压缩是真的会切历史的,这条误报比显示不准更严重。现在:- 内置表查不到就按"未知"处理(不再拿默认值假装知道):状态栏显示
ctx ~21.4k 窗口未知,只报真实绝对量、不给百分比、不触发 90% 压缩提示,也就不会劝你做没必要的压缩; - 表内补齐真实 ID:
deepseek-flash/deepseek-pro归入 deepseek v4 代(1M);新增云端qwen3-max/qwen3.8-max档,不再被本地qwen3的 40k 规则吞掉; /config向导新增第 4 步「上下文窗口」(5 步 → 6 步):直接填数字即固定该窗口,留空 = 按模型名自动;模型不在表里时,这里是唯一能把它填准的地方。窗口与轮数一样按服务商各记各的(切服务商带出那家自己的值、保存写进该 provider 条目),重开面板会带出当前已配的窗口。- 涉及面:
model_context_window()/Resolved::effective_context_window()返回Option,Config::save()增加context_window参数(写进 provider 条目,None/0= 清除固定值)。README.md/README.en.md/docs/使用说明.md同步删掉"未命中默认 32k"的旧口径说明。
- 内置表查不到就按"未知"处理(不再拿默认值假装知道):状态栏显示
znaide v1.0.3
znaide v1.0.3(2026-09-10)
危险命令判定重写。核心变化:不再对命令原文做子串匹配,改为把命令解析成 argv + 目标路径再判定。原先"多打一个空格就绕过"和"具体路径被误伤"这两类问题同时消失。
旧实现的问题
is_dangerous_command() 对整条命令字符串做 contains:
- 漏网:
"rm -rf /"是字面量,rm -rf /(多一个空格)、rm -r -f /、rm --recursive --force /、rm -rf "/"、sudo rm -rf /、mkfs -t ext4 /dev/sdb1(mkfs.的写法才拦)全部漏过;rm -rf ..(删父目录)不在表里。 - 误伤:
rm -rf /tmp/123/命中"rm -rf /"被硬拒;fdisk -l(只读列举)、dd if=x of=./img、git push --force-with-lease、echo "rm -rf /"同理。 - 不可覆盖:交互模式下命中即在弹确认框之前返回 Denied,人坐在旁边也无法放行(只有
yolo能过)。
根因是拿文本当语义:要判的是"目标路径规范化后是否落在高危集",这是路径运算,不是字符串匹配。
新判定流水线
命令字符串
│
├─ 1. 词法切分 split_segments()
│ 按 ; && || | & 换行 切成"简单命令段";处理 '…' / "…" / 反斜杠转义;
│ > < 作为独立 token 保留(重定向目标单独判定);不做变量展开、不解析 $( )
│
├─ 2. 变量替换 collect_assignments() + expand_word()
│ 收集同一条命令串内的"字面赋值"(值里不含 $ 或反引号),预置 HOME / PWD;
│ 展开 $VAR 与 ${VAR};未知变量原样保留 → 后续走 Uncertain
│
├─ 3. 逐段取 argv,剥离前缀 strip_prefixes()
│ 跳过前置赋值 FOO=1 与 sudo / doas / env / command / builtin / exec /
│ nohup / setsid / time / nice / ionice / stdbuf / timeout(含 -u root 这类带值选项)
│ bash\|sh\|zsh… -c '…' 递归判定脚本文本(深度上限 3)
│
├─ 4. 按命令分派规则 judge_segment()
│ flag 集合(argv 层判定,空格数量、flag 顺序、合并写法都不影响)
│ + 目标路径(非选项参数)
│
└─ 5. 目标路径规范化 classify_target() / resolve_path()
展开 ~ → 相对路径按 cwd 绝对化 → 纯词法折叠 . 与 ..(不碰文件系统)
→ 与高危集比较 → Safe / Dangerous / Uncertain
多段命令取最严重的一段(Blocked > Uncertain > Safe);> >> 2> &> 后的目标若为块设备直接判高危。xargs rm … 的目标来自标准输入、判定时看不到 → Uncertain。
判定模型:三态
| 结果 | 含义 | 交互模式 | 无头模式 |
|---|---|---|---|
Blocked |
目标解析出来确实高危 | 弹红色确认框,显示解析出的目标,只给单次放行(不提供"本会话都允许") | 直接拒绝(bypassPermissions 也不放行) |
Uncertain |
目标无法判定(未知变量 / 通配符与 .. 混用 / xargs rm 等间接调用),或属于需点头的中风险(shutdown、reboot) |
同样要人工点头,且不吃"本会话都允许" | 按档位:全自动放行,其余拒绝 |
Safe |
没解析出危险点 | 按权限档位处理 | 按权限档位处理 |
超级 yolo 跳过以上全部判定。
高危目标集(dangerous_why):根目录 /、家目录 ~、当前工作目录或其上级(cwd.starts_with(p),所以 rm -rf .、rm -rf ..、rm -rf .* 都命中)、块设备(前缀表:/dev/sd* /dev/hd* /dev/vd* /dev/xvd* /dev/nvme* /dev/mmcblk* /dev/loop* /dev/mapper/ /dev/dm- /dev/disk* /dev/rdisk* /dev/fd0 /dev/ram* \\.\PhysicalDrive*)。
各命令规则
| 命令 | 判高危的条件 |
|---|---|
rm |
带递归 flag(-r / -R / --recursive,任意顺序与合并写法)且目标落在高危集 |
find |
带 -delete 或 -exec/-execdir rm…,且起始目录落在高危集 |
dd |
of= 的值是块设备(或无法判定) |
mkfs* |
任意参数是块设备 / 高危路径(不要求 mkfs. 点号写法) |
fdisk sfdisk parted sgdisk wipefs |
带 -l / --list → 放行(只读列举);否则目标含块设备 → 高危 |
chmod chown chgrp setfacl |
带 -R / --recursive 且目标落在高危集 |
truncate shred |
目标是块设备 |
git push |
带 --force / -f 且不是 --force-with-lease |
shutdown reboot poweroff halt init |
一律 Uncertain(降级为普通确认,不再当作"磁盘级破坏") |
边界情况处理
- 通配符:取通配符之前的片段所在目录做判定。通配符不会匹配
./..,安全目录内的通配符爬不出去 →rm -rf /tmp/*放行、rm -rf /*与rm -rf .*拦;通配符与..混用(/tmp/*/../..)无法确定 → Uncertain。 - 简单变量:
T=/; rm -rf $T能解析出/→ 直接高危(比"未知变量一律 Uncertain"更强);D=./build; rm -rf $D解析成安全路径 → 放行。 - 引号:
echo "rm -rf /"、grep -rn 'rm -rf /' docs/现在按 argv 判定 → 放行(旧实现全部误伤)。
接口与接线变更
permissions::is_dangerous_command()删除,替换为permissions::inspect_command(cmd, cwd) -> CommandRisk。Permission::check_command()退化为纯档位判定(这个档位是否需要确认),不再内含风险判定——风险判定收敛到 session 层一处,否则交互模式下"用户已确认"的命令会在工具内被二次拒绝。session::check_permission()成为唯一风险闸门:判定 → 高危/无法判定时发PermissionRequest;SessionEvent::PermissionRequest新增warning: Option<String>,带warning时不设置command_always。tools/shell.rs的高危兜底改为只在ctx.events.is_none()(无头 / 宿主直调)时生效。- TUI:带
warning的确认框用红色边框 + "⚠ 高危命令确认"标题、弹窗加高、a(本会话都允许)键失效,并把解析出的目标/原因显示出来。
判定示例
| 命令 | 结果 |
|---|---|
rm -rf / / rm -r -f / / rm --recursive --force / / rm -rfv / |
Blocked(flag 与 argv 层判定,空格与顺序无关) |
rm -rf "/" / sudo -u root rm -rf / / FOO=1 rm -rf / / bash -c 'rm -rf /' |
Blocked |
rm -rf .. / rm -rf . / rm -rf .* / rm -rf /tmp/../.. |
Blocked(规范化后落在 cwd 上级 / 根) |
T=/; rm -rf $T |
Blocked(字面赋值可解析) |
rm -rf /tmp/123/ / rm -rf ~/x / rm -rf ./build |
Safe(具体路径) |
rm -rf /tmp/* |
Safe(通配符出不了 /tmp);rm -rf /* 为 Blocked |
rm -rf $UNKNOWN_DIR / xargs rm -rf / shutdown -h now |
Uncertain |
dd if=/dev/zero of=/dev/sda |
Blocked;dd if=x of=./img、of=/dev/null 放行 |
mkfs -t ext4 /dev/sdb1 / chmod -R 777 / / find / -delete / echo x > /dev/sda |
Blocked |
fdisk -l / fdisk -l /dev/sda / chmod -R 755 ./dir |
Safe |
git push --force origin main、git push -f |
Blocked;git push --force-with-lease、git push origin main 放行 |
已知边界(不宣称安全)
这是启发式减速带,不是安全边界。文本层看不穿:变量间接赋值链、命令替换 $(…)、xargs / make / python -c / npx rimraf 之类的间接调用、以及执行时才产生的值。undo 仍只管 write_file/edit真要限制进程能碰什么,需要内核级隔离(Landlock / 命名空间 / seccomp,见 docs/plan-sandbox.md,这是下一步计划~~~我太忙了)。
验证
cargo test --workspace:core 94 / mock-e2e 13 / tui 77,全绿(其中permissions12 个、tools::shell新增 2 个直接覆盖"无头高危拒绝"与"具体路径不误伤")。cargo build --release通过,--version输出 1.0.3。- 未做:undo 快照仍只覆盖
write_file/edit(设计边界,使用说明 5.3 已如实描述)。
znaide v1.0.2
znaide v1.0.2(2026-09-07)
会话管理大升级:历史会话与长期记忆有了专门的管理窗口,支持批量删除与备注。
新增
- 全屏会话管理窗口(
/resume无参打开,Esc关闭):- 「历史会话 / 长期记忆」双页签,
Tab切换; ↑↓/PgUp/PgDn/Home/End选择,/实时过滤;空格/×多选 +d批量删除(先弹确认;当前会话标注「当前会话」禁删,想重新开始用/clear);Enter恢复选中会话(会话页)/ 查看记忆正文(记忆页);n给会话写/改备注(行内输入,≤200 字,Enter保存 /Esc取消)。
- 「历史会话 / 长期记忆」双页签,
- 会话备注:每个会话旁可存一段备注(数据落在
~/.znaide/sessions/<会话id>.meta.json,不混入消息流;/clear截断对话不丢、/resume del删会话连带删除)。有备注的会话在窗口里显示为「备注」会话ID,一眼认出它是干嘛的。 - 长期记忆管理:记忆可列表、搜索、查看正文、批量删除(删除同步清掉
MEMORY.md索引行)。 - 自动更新双源回退:默认 Gitee 镜像源(国内直连、无需代理),连不上或下载失败自动切 GitHub;更新成功会标明来源。Gitee 镜像:gitee.com/brother-ershui/znaide。
变更
/resume无参从"输出区列 10 条文本"改为打开全屏管理窗口;/resume <序号|片段>恢复、/resume del <序号|片段>删除行为不变。
修复
- 管理窗口
Tab切换页签失效:crossterm 制表键为KeyCode::Tab,此前误配成Char('\t')。
znaide v1.0.1
znaide v1.0.1
全局人格(persona)
- 内置四种人格,
/persona随时切换:毒舌损友 / 耐心老师 / 热血极客 / 极简冷淡风;/persona 无恢复默认助手 - 人格全局生效:切换即写回配置,影响之后所有对话(问答/汇报/建议都带着人设),重启也保留
- 人设只管表达与视角——权限、危险命令拦截、工具纪律等安全底线不被覆盖
- 状态栏常驻显示当前人格,随时知道"是谁在说话"
人格完全可自定义、可编辑
- 人格就是一份 Markdown:往
~/.znaide/personas/放一个<名字>.md(正文即人设说明),马上就是新人格,不用改代码、不用重启配置 - 内置的四种人格同样以文件形式放在该目录——打开就能看到、随手改两句措辞(比如让毒舌损友少损一点),改完即生效
- 想找回原样?删掉对应文件,自动恢复内置默认
- 想批量收藏别人写的人格?复制一份 md 进目录就行
示例技能内置(开箱即用)
- 归档下载 / 系统垃圾清理 / 周报生成 三个示例技能编译进二进制,装好就有,直接说"把下载目录整理一下"即可触发
- 示例内容在
~/.znaide/builtin-skills/,可改可抄;本版起发布不再附带独立技能包
平台产物:Linux x64/arm64 · Windows x64 · macOS x64/arm64 · Android(arm64,Termux 用),共 6 件,均带版本号。--update 可自动更新到本版(GitHub 网络受限时设 HTTPS_PROXY)。
znaide v1.0.0
znaide v1.0.0
首个正式版。跑在终端里的通用 AI 助手:一句话让 AI 自己改文件、跑命令、查资料、整理目录——编程只是应用场景之一。单个二进制、零 Node/零运行时,Linux / macOS / Windows / Android(Termux)都能跑。
亮点
- 通用 Agent:文件读写/精确编辑、命令执行、目录/glob/正则搜索、网页抓取、长期记忆,自主"侦查 → 执行 → 汇报"
- 安全四档权限:询问 / 编辑放行 / 全自动 / 超级(YOLO),随时 Shift+Tab 切换;危险命令黑名单 + 每次写文件自动 undo 快照,改坏了一键回滚
- 模型通吃:任意 OpenAI 兼容端点——本地 ollama/vLLM,云端 DeepSeek、通义、Kimi 等,改个 base_url 就切
- 技能(Skills):一份 Markdown 说明书 + 可选脚本 = 一个能力包,模型可自主调用、也可
/技能名手动触发 - MCP:配置 mcp.json 即可接入任意 MCP server 工具
交互细节
- 工具卡片实时显示:调用内容、动态计时、AI 预估预算(超预算含宽限仍未结束则终止,并把进展反馈给 AI 自行决定重试/拆步/换路)
- 命令执行中带 实时输出区(tail -f 式滚动最新 10 行,ANSI 颜色保留)
- 底部状态栏:ctx 上下文占用条(70%/90% 警示)+ token 实时统计
/compact非破坏性压缩上下文;会话历史落盘,--resume随时恢复,中断/超时不留坏历史- 自动更新:启动时自动检查 GitHub 新版本并提示;
/update或znaide --update立即更新(走 GitHub,网络不通请设HTTPS_PROXY;Linux/macOS 重启生效,Windows 退出程序后自动替换);--version查看当前版本
平台产物
下载对应平台的二进制即可(均带版本号):
| 文件 | 平台 |
|---|---|
| znaide-linux-x64-v1.0.0 | Linux x86_64(musl 静态) |
| znaide-linux-arm64-v1.0.0 | Linux aarch64(musl 静态) |
| znaide-windows-x64-v1.0.0.exe | Windows x86_64 |
| znaide-macos-x64-v1.0.0 | macOS Intel(zig 交叉) |
| znaide-macos-arm64-v1.0.0 | macOS Apple Silicon(zig 交叉) |
| znaide-android-arm64-v1.0.0 | Android aarch64(bionic,Termux 用) |
| skills-v1.0.0.zip | 示例技能包(解压到 ~/.znaide/skills/) |
本仓库只发布二进制,不开源。
--version查看版本;使用说明见文件内--help与 README 文档。