Skip to content

Releases: twowb/znaide

znaide 1.0.9

Choose a tag to compare

@twowb twowb released this 08 Oct 04:10

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

Choose a tag to compare

@twowb twowb released this 29 Sep 02:11

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

Choose a tag to compare

@twowb twowb released this 17 Sep 16:23

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

Choose a tag to compare

@twowb twowb released this 15 Sep 16:23

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

Choose a tag to compare

@twowb twowb released this 10 Sep 13:56

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 当依赖用的场景):移除了若干未被使用的 pub getter 与 list_history_sessions;新增 core::util。

znaide v1.0.4

Choose a tag to compare

@twowb twowb released this 10 Sep 07:54

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

Choose a tag to compare

@twowb twowb released this 10 Sep 04:04

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,全绿(其中 permissions 12 个、tools::shell 新增 2 个直接覆盖"无头高危拒绝"与"具体路径不误伤")。
  • cargo build --release 通过,--version 输出 1.0.3。
  • 未做:undo 快照仍只覆盖 write_file / edit(设计边界,使用说明 5.3 已如实描述)。

znaide v1.0.2

Choose a tag to compare

@twowb twowb released this 07 Sep 13:53

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

Choose a tag to compare

@twowb twowb released this 07 Sep 14:03

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

Choose a tag to compare

@twowb twowb released this 05 Sep 15:13
f8dba98

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 文档。