RWork 功能说明

本文是功能清单 + 界面示意图,回答「这个应用有什么、长什么样」。 动机与取舍写在 README.md 的「已知取舍」一节;后续要做什么写在 REQUIREMENTS.md。

图是 ASCII 示意,反映真实的控件与排布,但不逐像素精确(终端等宽字体下的近似)。 文案与实际界面一致,取自 i18n 表。


0. 全景

┌────────────────────────────────────────────────────────────────────────────────┐
│  ┌──────────────┐                                                              │
│  │ RWork 工作台  │  ← 侧栏:折叠按钮 + 工作空间(图标 + 提示)                     │
│  │   [📁] [«]   │                                                              │
│  ├──────────────┤  ┌────────────────────────────────────────────────────────┐  │
│  │ 💬 对话      │  │                                                        │  │
│  │ 📖 核心行为   │  │                        主内容区                         │  │
│  │ 📊 用量审计   │  │                   (随左栏导航切换)                     │  │
│  │ ⚙  设置      │  │                                                        │  │
│  ├──────────────┤  │                                                        │  │
│  │ 搜索对话…    │  │                                                        │  │
│  │ ──────────── │  │                                                        │  │
│  │ 介绍一下自己吧│  │                                                        │  │
│  │ 关于 MCP     │  │                                                        │  │
│  │ …            │  │                                                        │  │
│  ├──────────────┤  │                                                        │  │
│  │ + 新建对话   │  └────────────────────────────────────────────────────────┘  │
│  └──────────────┘                                                              │
└────────────────────────────────────────────────────────────────────────────────┘
        ↑ 右边缘 6px 可拖动改宽(200–520px),宽度记进设置

四个页面:对话 / 核心行为 / 用量审计 / 设置。last_page 记在设置里,重启回到上次那页。


1. 对话

1.1 主界面

┌────────────────────────────────────────────────────────────────────────────────┐
│ RWork 工作台  介绍一下自己吧                       deepseek-flash · 1.8K tokens │
├────────────────────────────────────────────────────────────────────────────────┤
│  我                                              2026-09-14 09:12  [⧉]        │
│  ┌──────────────────────────────────────────────────────────────────────────┐  │
│  │ 解释一下这个项目的架构                                                      │  │
│  └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                │
│  ▸ 思考过程  ◐                                     2026-09-14 09:12  [⧉]      │
│  ┌────────────────────────────────────────────────────────┐                    │
│  │ 先看 crate 划分,再找入口函数,最后对着依赖关系说清楚。        │                    │
│  └────────────────────────────────────────────────────────┘                    │
│                                                                                │
│  助手  deepseek-flash  Token 1,821  耗时 3.2s      2026-09-14 09:12 [⧉][📄]   │
│  ┌──────────────────────────────────────────────────────────────────────────┐  │
│  │ 这个项目分成 5 个 crate:                                                 │  │
│  │ - **rwork-core**:领域模型,不做 IO                                        │  │
│  │ …                                                                        │  │
│  └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                │
│  ⚙ bash  ◐                                         2026-09-14 09:12  [⧉]      │
│  ┌────────────────────────────────────────────────────────┐                    │
│  │ command=find crates -name '*.rs' | wc -l  timeout_seconds=120                 │
│  │ 41                                                                            │
│  │ 比上一版多了 6 个文件                                                          │
│  │ [📄 findings.md]  ← 小卡片,点一下用系统默认程序打开                          │
│  └────────────────────────────────────────────────────────┘                    │
│                                                                                │
├────────────────────────────────────────────────────────────────────────────────┤
│ 流水线  顺序即执行顺序,每一位都能看到前面的产出                                  │
│         [代码探查] [方案规划] [代码实现] [故障定位] [代码评审]                     │
├────────────────────────────────────────────────────────────────────────────────┤
│  ┌──────────────────────────────────────────────────────────────────────────┐  │
│  │ 输入消息,Enter 发送,Shift+Enter 换行                                     │  │
│  └──────────────────────────────────────────────────────────────────────────┘  │
│  正在运行 bash…                                           [📎]  [ 发送 ]       │
└────────────────────────────────────────────────────────────────────────────────┘

要点

对话记录是一串块,不是一串气泡:一次助手回合会依次长出思考块、正文块、 工具块与产出物卡片。三种块宽度不同,因为它们的角色不同——正文是主角, 思考与工具是过程;三者一样宽时,一屏里最先被读到的会是工具输出那堆字。

块 宽度 默认形态
正文 最宽(占满正文列) 完整展开,可复制(markdown 原文 + 富文本)
思考 正文列的 82% 收起,只显示最新的一小段(滚动输出),点开看全文;推理中带转圈
工具 正文列的 82% 外面一个框、里面两个小框:「输入」(调用参数,一行一个参数)与「输出」(结果)。默认折叠成一行(工具名 + 参数摘要,超出用省略号,不折行);执行中自动展开并转圈,输出逐行刷出来;跑完自己收回去,想细看就点标题行
产出物 只包住文字的小卡片 文件图标 + 文件名,点一下用系统默认程序打开(挂在写出它的那个工具块下面)

这些宽度都不是写死的像素(见 crates/rwork-app/src/layout.rs):

尺寸 由什么决定
正文列 字号 × 61 字。决定读起来累不累的是一行的字数,不是像素数:同一个 920px,15px 字号下是一行 61 字,18px 下就成了 51 字。设置里改字号,列跟着走(实测 13/15/18 字号 → 793/915/1098 px)
过程块(思考 / 工具) 正文列的 0.82:过程是被读的「过程」,不是主角
页内对话框 窗口宽度的 42%,两端夹在 420–720px:窗口窄时它得让路,窗口宽时也没必要长成一个页面
首次打开的窗口 显示器可用区域的 78%,上限 1600×1000。写死 1440×920 在 4K 屏上只占中间一小块,在 1366×768 的笔记本上又比屏幕还高
侧栏 用户拖过的那个值(收窄档是固定值)

窗口比正文列窄时不需要任何特判:max_w 只封顶、不撑开,内容由父容器自然压窄。 布局值只在一屏里算一次,且变化时会在日志里留一行(布局宽度按字号与窗口重算)—— 调布局时最想知道的就是「这一屏到底算成了多宽」。

能力 说明
流式输出 逐字上屏;随时「停止」,已生成内容保留并入库
思考通道 带推理的模型(DeepSeek Reasoner / Claude 思考模式)单独折叠展示,默认收起、只显示最新的那一段,点开看全文
实时命令输出 命令与脚本每打出一行就当场进工具块(stdout 与 stderr 都收,stderr 标出来),不必等它跑完。装包过程同理:pip 的 Collecting… / Downloading… 会一路刷出来,而不是让人对着空白等十几分钟
复制按钮 正文(markdown 原文 + 富文本)、思考(收起时也能复制全文)、工具(输入 + 完整输出)。工具块复制的是完整那一份,不是屏幕上被裁剪、被折叠的显示
选中的文字一定能复制 鼠标选中任意文字后按 Ctrl+C 都有效。复制是绑定在焦点上的动作,而选区是窗口级的事实:焦点停在按钮或别处时,「选中了却复制不出来」是必然的,因此外壳在最外层兜了一次底
运行记录(队列与链路) 「核心行为」页里列出最近 30 次运行(回合 / 派活;定时任务的执行在它自己的「执行历史」里,不重复列在这里):时间、状态、标题、轮数与调用数、产出文件。点开看它跑了哪些步——每一步是工具调用(工具名 + 结果第一行)或上下文(压了几条、丢了几条、省了多少 token、存了几个文件)。压在请求发出之前发生,因此链路上的先后顺序就是真实顺序。进程被杀之后这一页才真正有用——停在「进行中」的记录会在下次启动被标成「被中断」,于是「上次跑到哪」有据可查
被打断会说出来 如果上一轮没跑完(进程被关掉/杀掉),打开会话时输入框上方会有一条提示:停在哪一步、多半是被关掉或杀掉,并给一个「继续」按一下接着做。历史里的缺口此前就已经会自动补好
附件与正文一起 附件在正文里是一个整体(① ② ③…,插在光标处,退格一次就删掉整块,像表情);输入框上方是文件清单(编号 + 文件名 + 大小,点一下移除,同时删掉正文里对应的占位符)。发送时以正文为准:删掉占位符的附件就不发
附件就在输入行里 已选附件显示成「图标 + 文件名」的小卡片,与输入框同处一行、排在文字左边(不是框外或上方另起一行),点一下移除。状态提示(例如「模型不支持图片」)留在框外
从剪贴板添加 输入区的「从剪贴板添加」按钮:复制来的图片落成附件文件(按它自带的格式),复制来的文件直接用路径;纯文本仍由输入框自己粘贴
附件组装说明 带附件的消息会先给模型一份清单:本次包含几段文字、几个文本附件、几张图片,逐条写明名称与大小,并标出「这份只有摘要」和「文档里还有 N 张内嵌图没发」。图片按图发送、正文里没有它的文字,因此点名尤其重要——不点名模型不知道这是哪来的图
会话压缩 内容多到接近上下文窗口时,先把过程压掉:用户说过的话一个字不动,助手的长回答截断并标明,推理整段丢掉,工具输出换成一行摘要(首行 + 末尾的结论/路径)。超过 1200 字的工具输出还会把全文存成文件(.rwork/<会话>/context/tool-<第几条>-<内容哈希>.md,身份是哈希、序号只是让人好认),摘要里留的是开头加一个引用——引用用 read 读得回来,所以「后面还需要那份原始输出」不必重跑一次工具;内容相同的输出每轮都会落到同一个文件上(历史窗口滑动会让序号变,所以不能拿序号当身份),因此重压同一段历史不会多出文件、也不会换引用。存不下时(目录不可写、磁盘满)退回只压不存,绝不给一个读不到的引用;智能体没勾 read 时也不落盘——引用是给它读的,给一个读不了它的模型留下一句「全文在这里」只会让它白试一次。压缩发生后会在请求里说明「哪些被压过」,模型不会把摘要当原始细节。实测 30 轮会话省下 3.4 万 token
上下文预算 压不动了才按条裁剪:丢掉最早的整条消息,并在请求里说明省略了多少条,不静默丢弃。压缩与裁剪都会记进这一轮的统计(压了几条、丢了几条、省了多少 token、存了几个文件),因此「它怎么忘了刚才说的」在会话旁边就能看到,不必去翻日志
历史会话 可搜索,标题取首条消息首行;侧栏可置顶
轮次上限 一条消息最多 12 轮工具调用(可配),到顶会明说原因
工具报错由模型自己收拾 一次调用失败不中止回合:失败结果(退出码、stderr、错误原因)回到模型手里,由它分析、改、重跑。工具报错最常见的原因是环境细节——python3 不在 PATH 里、包还没装、路径写法不对——这些模型拿到错误后通常自己就改对了
工具结果入库 调用请求与工具结果一起落库。少存结果的话,下一次提问会把一个「有调用、没人应答」的历史发给服务端,而那是会被整条拒绝的
轮次用尽也要有交代 一次回合的工具轮次是有预算的(默认 12 轮)。用到最后,不会静默结束:工具会被摘掉,模型必须给出一份收尾汇报——做完了什么(带文件路径)、交付物能不能直接用、还差什么、继续下去的第一步。同时每轮都会把「已用几轮、还剩几轮」告诉模型,让它把交付物排在打磨细节前面
收尾必须说清完成度 每轮结束,助手要明确回答「任务完成了没有」:完成了就给产出文件的相对路径加一句内容说明,没完成就说清还差什么、卡在哪;不许用「已完成」三个字代替清单,也不许把带占位符 / TODO / 示例数据的文件当成品交付(提示词里单列了一条收尾要求,并且会把本会话已经写出的文件清单一起下发,好让它接着改而不是重造)
派活给哪个角色 delegate 可以指定智能体(预设的名字或 id 都认):评审的活派给评审位、实现的活派给实现位,各自带自己的系统提示词与工具范围。不指定就用当前这位;指定了但找不到会报错并列出可选项
长文本自动存成文件 粘贴超过 2000 字的内容,正文里只留一句说明、一个本地文件引用和开头预览,模型需要时自己读文件。对话记录里仍然完整显示你的原文
设置页分区标签 设置分成五个区(外观 / 智能体 / 模型服务 / 工具与扩展 / 数据),标签常驻在页头下面,一次只看一区,不用一直往下滚
保存回执 页头下面常驻保存状态:成功显示绿色「已保存 · 时刻」,失败显示红色并写明原因
派活的过程收在一个块里 子代理做的事以「交给子代理」的方框呈现:任务说明、编号步骤、结论。它与普通对话分开显示——子代理的消息在库里也是普通消息,混着渲染会让人以为「用户说过这句话」
派活的时机 主循环剩不到 2 轮时不再派活(派出去收不回来),会让模型自己收尾
派活(delegate) 模型遇到「要反复试错」的活(调画图脚本、试环境依赖、修格式)可以派给子代理:它有自己的轮次预算和自己的历史,做完把结论与写出的文件带回来。那些试错因此不占主循环的轮次——主循环的轮次留给「把交付物做出来」
执行计划 输入像多件事时,先拆成带完成条件的步骤(依赖构成有向无环图),再按图执行:每做完一步按条件核对(文件存在/非空/含某段文字/JSON 里有某字段),不满足就把差什么告诉模型让它接着补;上游产出失效就退回最早那一步重做,受影响的步骤一起重做。重试与回滚都有上限,到顶如实报告卡在哪;有副作用的步骤(推送、发布、覆盖文件)要重跑时先问用户。步骤清单与当前进度既进系统提示(哪一步、要满足什么、不要跳步),也在对话右侧留白里的「执行计划」面板上画出来:每步的状态、完成条件、每次尝试与退回原因、回滚次数,读的是库里那份,因此重开应用还看得到上一轮停在哪
我的输入(概览面板) 右侧留白那排图标里的第一个入口,点开是一张浮层面板:每一条都是我发过的话的第一行概要加时间,点一条就跳到那一条去。长对话里「找某次输入」不再靠滚
交付物 每个回合末尾列出这一回合真正落到盘上的文件:工具直接写出的,以及模型在回答里点名、磁盘上确实存在的(脚本用 python-docx / matplotlib 产出的也算)。每张卡片点一下用系统默认程序打开
回到最新 长对话往回翻之后,右下角出现一个圆形向下箭头,点一下回到最新;贴着底部时它不出现。与右侧留白里的「回到我发的话」(跳到本轮用户消息)是两个不同的动作
右侧留白三个入口 正文列有阅读上限,窗口再宽右边也空着一条。那里竖排三个按钮:我的输入(跳到某一条我发的话)、这一轮的统计(助手回复数、工具调用数、输入/输出 token、用时、模型用时、输出速度、产出文件)、执行计划(步骤、状态、完成条件、每次尝试与退回原因)。三者共用同一个右侧栈、同一层外壳(标题 + 关闭按钮 + 弹出层底色与阴影),点图标开合两可;「用时」是整轮墙钟、「模型用时」只算模型在生成的那段时间,输出速度按后者算
等模型/下载的时候窗口还能用 装本机 OCR(最多 5 分钟)、手动整合长期记忆、环境探测与装运行时这几处的等待,都不会再让界面「睡住」:等待期间照常点侧栏切页、滚对话。等不到结果时会如实收场(把「正在下载」那句话收掉并给出提示),不会留一句永远停在那里的状态。第 44 批修的(此前这几处是界面线程上的阻塞轮询,实测点一下「整合长期记忆」会把窗口按住 4 秒)
长对话不再越滚越重 对话正文只把看得见的那一段画成真正的元素,视口外的块用等高的占位撑着,因此每次重绘的开销与「这段会话有多长」脱钩(实测 380 条消息的会话:每次重绘 97.7 ms → 18.6 ms)。滚动位置、贴底跟随、跳到某条我发的话三个行为都不变——占位与真块一样高
状态说的是正在发生的事 有工具在跑时,输入区下方与工具块标题显示的是「正在运行 <工具>…」与转圈,而不是「正在生成回复…」。那一刻模型那边什么都没在生成,说成「生成中」会让人一直干等——这正是「用了一次工具之后程序像卡住了」的由来

1.2 智能体流水线(新建对话时)

┌────────────────────────────────────────────────────────────────────────────────┐
│ 流水线  不选则用设置里当前的那一位                                                │
│         [代码探查] [方案规划] [代码实现] [故障定位] [代码评审] [文件盘点] …        │
└────────────────────────────────────────────────────────────────────────────────┘

点了几下之后(顺序即执行顺序):

┌────────────────────────────────────────────────────────────────────────────────┐
│ 流水线  [1 代码探查 ×] [2 代码实现 ×] [3 代码评审 评审 ×]        [故障定位]       │
└────────────────────────────────────────────────────────────────────────────────┘

多选 = 一条流水线,按点击顺序接力。 后一位能看到前一位的全部产出(每一步都已落库, 因此不需要额外的交接结构)。

  • 每位保留自己的模型、系统提示词、工具白名单与审批模式——不合并。 这正是「便宜模型做规划、强模型做执行」成立的前提,也让「第 1 位只读、第 2 位可写、 第 3 位只读复审」天然可行。
  • 顺序即执行顺序,最多 5 位;至多一位评审,且不能排第一(前面没有可评审的产出)。
  • 交接说明:从第二位起自动插一条说明(前一位是谁、你是第几位、这条线几位)。
  • 轮次预算每位各一份——共用一份的话第一位跑满会把后面掐死。
  • 失败不连锁:某一位一次都没做成(工具全部失败、或请求本身就失败)就停在那里, 已完成的产出如实保留。注意这是整位跑完后的判断:中途某次命令报错不会中止这一位。
  • 附件只在第一位那轮带出去(之后已在历史里)。
  • 对话一旦开始,选择锁定:中途换人会让前半段与后半段的约定不一致,事后无从判断谁做了什么。

执行计划会跨棒接力(第 26 批)。整条流水线共用一份计划,每个节点记着 上一次是哪一棒做的;退回到别人的产出上时,本棒立刻停手,把序号拨回那一棒:

第 1 棒「代码实现」  写草稿 ✓ ──► 进入第 2 步「生成图表」
第 2 棒「报告成文」  作图 ✓,但顺手把草稿清空了
                     └─► 审计发现上游失效:退回给第 1 棒「代码实现」重做
                         (本棒的回合就此结束——不让它替别人写一半)
第 1 棒              重写草稿 ✓ ──► 第 2 步上次是第 2 棒做的 → 又交回给它
第 2 棒              重做图表 ✓ ──► 全部步骤通过条件核对

对话右侧的「执行计划」面板里,每一步下面会多一行「第 N 棒:谁」, 于是界面上说的和日志里那句「退回给第 2 棒」对得上。来回交棒不会无限—— 每次交棒都由一次回滚触发,而回滚总数受设置里「总回滚上限」约束。

计划停在「等你确认」上时(退回要重跑有副作用的步骤),用户回一句「继续」, 流水线会直接从当初做那一步的那一棒接着做,而不是从第 1 位重跑一遍。

1.3 内置的 10 个子智能体

角色 职责 工具白名单 审批
编程 单人对话的编程档位:先看懂现状,再改,改完自验证 全部(含 write/edit/write_version/run_script/bash) 自动写入
代码探查 只读摸清代码库,给「位置 + 结论」清单 read, glob, grep, refs 每次都问
方案规划 把现状变成可执行、可验收的计划 + skill 每次都问
代码实现 按计划改代码并自验证 全部(含 write/edit/write_version/run_script/bash) 自动写入
故障定位 复现 → 缩小范围 → 给出根因 read, glob, grep, refs, bash, run_script 每次都问
代码评审 独立检查产出,通过 / 退回 + 逐条问题 同上(无写入工具) 每次都问
文件盘点 只读地列出一批文件各是什么、有什么 read, glob, grep, refs 每次都问
数据分析 用脚本把表格/文档算成带数字的结论 read, glob, grep, refs, run_script, skill 每次都问
报告成文 整理成文档,写新版本文件 含 write_version,刻意没有 write/edit 自动写入
数据核对 独立重算关键数字,只报对不上的 read, glob, grep, refs, run_script 每次都问

每个角色都写明「不该做什么」,这是防越界的关键:探查不提出修复方案、排错只回答 「为什么坏」、评审不改自己发现的问题、成文不引入新结论、分析不估算数字。

白名单是硬约束:specs_for 按名字过滤发给模型的工具表,所以「只读角色改不了文件」 是机制保证,不靠提示词自觉。两条最有价值的边界:

  • 报告成文 没有 write/edit → 只能写新版本,一次「整理一下」不可能覆盖你的原始文件。
  • 代码评审 也没有写入工具 → 评审自己动手就失去独立性了。

没有一个角色用「全部放行」——内置角色是开箱默认值,而放宽权限是危险方向。

用户能改、升级能拿到新的、不覆盖用户改过的:

内置角色被用户改动 → 设置里存一份同 id 的「影子」,用户那份胜出
内置角色被用户删除 → 记进 disabled_agents,升级带来的新角色仍默认可用
升级新增的角色     → 直接出现

内置角色不占用户自建预设的额度(20 个)。

1.4 评审位会真的驱动退回

结论:退回
问题(按严重度):
1. [严重] crates/x.rs:42 — 越界未检查 — 输入为空时 panic — 加边界判断
未验证项:无

评审角色的产出被解析首行结论。退回时把意见交给上一位重做一次,最多一轮—— 不加上限的话两位互相不满意会变成不停烧额度的循环,而「什么时候算够好」没有客观判据。

解析只认开头几行,因为按关键词搜全文会有这几种误判(都会让评审变成自动放行):

  • 「不通过」里含有「通过」
  • 正文里写「测试通过」「这段可以通过」
  • 读不出结论时不当作通过(如实提示,不静默放行)

2. 核心行为

一个页面,因为它俩共享同一套作用域概念(项目 / 全局),而作用域切换器只该有一个。

┌────────────────────────────────────────────────────────────────────────────────┐
│ 作用范围  [ 项目:RWork ●] [ 全局 ]                      [ ↻ 整合长期记忆 ]     │
├────────────────────────────────────────────────────────────────────────────────┤
│ 长期记忆                                                                       │
│ 这些会随每次对话一起发给模型。项目范围的记忆只在当前工作空间里生效。               │
│ ┌──────────────────────────────────────────────────────────────────────────┐  │
│ │ 写一条要长期记住的要求或事实,回车添加                                      │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
│ ┌───────────────────────────────────────────────────────────────────  [🗑] ┐  │
│ │ 回答一律用中文,不要用 emoji                                              │  │
│ │ 全局  手动添加  还从未被取用                                             │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
│ ┌───────────────────────────────────────────────────────────────────  [🗑] ┐  │
│ │ 这个项目用 pnpm                                                          │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
│ ┌───────────────────────────────────────────────────────────────────  [🗑] ┐  │
│ │ 用方案 B(而不是队列)                                                   │  │
│ │ 决定  项目 (…\RWork)  整合生成  从第 12–18 条消息总结  被取用 3 次        │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
│ ┌───────────────────────────────────────────────────────────────────  [🗑] ┐  │
│ │ 补上边界测试                                                             │  │
│ │ 待办  全局  整合生成  从第 31 条消息总结                                 │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
│ 向量库里有 1 条记录找不到对应的记忆(记忆被删之后留下的)。  [ 🗑 清理孤儿向量 ]│
├────────────────────────────────────────────────────────────────────────────────┤
│ 任务与定时任务                                            [ + 新建任务 ]       │
│ ┌───────────────────────────────────────────────────────────────────  [🗑] ┐  │
│ │ 生成每日简报                                                            │  │
│ │ 普通 可开始 全局 计划: 0 9 * * * 下次 2026-09-15 09:00                   │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
│ ┌───────────────────────────────────────────────────────────────────  [🗑] ┐  │
│ │ 检查依赖有没有新版本                                                     │  │
│ │ 普通 可开始 全局 智能体:代码评审 计划: 30 8 * * 1 下次 2026-09-15 08:30   │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
│ ┌───────────────────────────────────────────────────────────────────  [🗑] ┐  │
│ │ 每半小时看一次测试                                                       │  │
│ │ 普通 可开始 全局 每 1800 秒 下次 2026-09-14 23:35                        │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
│                                                                                │
│ 填间隔秒数即为定时任务;留空则是一次性任务,先记下来稍后手动做。                   │
└────────────────────────────────────────────────────────────────────────────────┘

列表在前、新建折叠在按钮后面。表单常驻会让这一页看起来像「正在编辑一条」, 而它首先是一份列表。

记忆分三类,类别与出处都显示在条目上。整合时模型认出的「决定 / 待办 / 事实」 连同这次提炼实际读过的那段消息序号一起入库。列表里带类别的条目会显示类别 标签与「从第 12–18 条消息总结」——出处是当时真正读过的区间,不是让模型逐条 标注的(它会写错、会漏写,而写错的那种看起来像有了出处,比没有出处更坏)。

决定与待办不看相关度,每轮都进提示词。召回是按相似度算的,而「三个月前 定下的方案」跟今天的问题可能一个词都不重合——它一旦丢出提示词,模型就会把 已经定下来的方案当成还没定的选项,重新问一遍「你想用 A 还是 B」。所以这一节 按类别 + 当前意图组装,列成「已经定下的 / 接下来要做 / 与当前这件事明显 相关的事实」:决定与待办全收(新的在前、最多 12 条),事实只收最像的那几条 (最多 5 条),被截掉的部分写明还有几条没展开;进过这一节的记忆不再在下面 那一节里重复(同一句话在提示词里说两遍,模型更容易把它当成两件事)。

这一节按会话缓存:换话题、或者记忆集合变了才重建,其余时候逐字不变—— 每轮按当轮召回重排会让同一件事在提示词里进进出出,而这一节说的是「这一段里 已经定下的东西」。

「这条到底有没有被用上」也显示出来了。每条记忆后面跟一句「被取用 N 次」, 一次都没进过提示词的写「还从未被取用」——这两种情形必须分得开:前者可能该删, 后者说明它总是和其他几条一起被命中。取用是发生过的事,一行一次记进 memory_use(只追加);「这次用完有没有起上作用」只在一种情况下说得清—— 那一轮只命中了一条。命中多条时谁也不记功,只标「同时命中」并照实显示: 一次成功给五条各记一次「赢」等于把噪声当信号,而权重会直接改召回排序。 上一次没跑完留下的记录,会在你下一次提问时被标成「没有结论」——否则它会把这 一轮的成败记到一个中途死掉的回合头上。

哪些回合不写结论:派活与流水线那几位跑完只说明「这一棒跑完了」,因此只有 你自己那一轮会留下成败;团队跑一轮时记忆只有取用次数、没有胜败。这是有意的: 谁起的作用本来就得靠对照实验才说得清(见 docs/09 的 I1 / I2)。

一句话里几件事,每件事各自去查记忆。你写「1. 改掉那个变量名 2. 部署走内网」 或「先把日志清一下;部署走内网」时,程序会按你写的分项把这句话切开, 每段各自去查一遍记忆,再把结果合并(同一条被两段命中时按更相关的那段算)。 不这么做的后果不明显但很实在:整句算一个向量时它是几件事的平均,跟哪一件 都不够像,于是「已经定下的 / 明显相关的事实」那一节会空掉——而关键词那条路 更直白:整句是当短语查的,长消息几乎必然查不到任何记忆。

切分的边界只认你手打的分项:编号(1. 1、 (1) 一、)、带圈数字 (①)、行首的项目符号(- *)、行首的「另外 / 然后 / 最后 / 其次」这类词、 以及分号。普通的换行不算边界——贴一段日志、一份配置时,几十行里没有一行是 「一件新的事」,按行切只会白花几次嵌入。最多切 5 段,超出的并进最后一段(不会丢) ;切不出第二段就完全按从前那样处理(普通消息的行为一个字没变)。

「换了话题」也按段判:一句话里两处都明显偏离当前话题,就直接算换话题 (以前要等下一轮);只偏一处仍然先观察一轮——一句话说偏可能是换个说法、也可能 是贴了一段报错,不该立刻把上下文切掉。你要是写了「另外问一下……」这类说法, 那就立刻算换,而且新话题是那一段,不是整句的平均。

人机交互的过程也留档(核心行为页的运行链路里看得到)。审批问过的那一步 后面接「问过:总是允许」,没等到回答(进程被杀、框被关)接「问过:问了, 没等到回答」——「没答」和「拒绝」是两句话,前者什么都没发生。记忆与定时任务 的候选也一样:不记 / 不建 这两条否定回答以前什么都不留,现在落一行, 候选卡上因此能写「这类提议你已经跳过 N 次」(按这段对话算)。三类交互、运行 链路那一步、账本那一行共用同一个 id,所以「第 3 轮为什么问我」查得到。

2.0 自动整合长期记忆(默认关闭)

「整合长期记忆」按钮是手动的。打开设置页里的开关后,换话题时会自动跑一次, 但只有这三条同时成立才真的跑(缺一条都不跑):

① 开关打开                          默认关闭:不在你没要求的时候花钱
② 这一轮换了话题                     显式说法(「换个问题」)立刻算,向量漂移要连续两轮
③ 这一段里出现过「该记住」的信号       「以后……」「记住……」「别再……」(只在你说的话里找)

跑之前会明说一句「换了话题,正在整理长期记忆(这一次会花一次模型调用)」—— 什么时候在花钱必须是你看得见的事实。

只发上次之后的消息:会话里记着一个水印(消息序号),整合成功才往前推。 所以第二次换话题不会把上次已经总结过的四十条重发一遍。模型答了但没按格式给出 条目时不推水印——那批内容还没被真正总结过,下次该再试一次。

2.1 错过执行的补跑

┌────────────────────────────────────────────────────────────────────────────────┐
│ ┌──────────────────────────────────────────────────────────── ⚠ 边框 ────────┐ │
│ │ 有 3 次该执行但没执行                                                     │ │
│ │ 应用没运行的那些时刻不会被自动补做。要补哪一次由你决定:                    │ │
│ │ [ ▷ 补做 2026-09-14 09:00 ] [ 不补 ]                                     │ │
│ │ [ ▷ 补做 2026-09-13 09:00 ] [ 不补 ]                                     │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────────┘
  • 总数照实说(3 次),但只列最近两次:用户记得自己最近在做什么,能判断这两次该不该补; 错过 47 次时列出全部他也想不起来。总数让他判断严重性。
  • 不自动补做:「每天 9 点备份」在你开机时自动补做会产生一次你没预期的写入,时机也不对。
  • 间隔型任务不报错过:它的语义是「距上次多久」,开机晚一点本就该顺延。
  • 补做时会把「你补的是哪一次」告诉模型——「生成昨天的日报」这类带时点的内容, 不说清的话模型会按现在写。
  • 失败会再试一次(只一次,约 60 秒后):网络抖一下、模型 429、机器刚启动时文件 还锁着——这类失败不值得等一整天(cron 型任务的下一个周期可能是一天以后)。 重试走的是同一条调度通道,所以「迟到多久算错过」那套判断没变;再失败就照旧 记成失败、等下一个计划点。执行历史里那一行会补一句「将重试 <时刻>」—— 只显示「失败」会让人以为那件事到此为止了。
  • 同一件事不会被并行做两遍:到点触发、你点「补做」、重启后接着做,三条路共用 一道门;上一个还在跑时,后一个会如实报「上一次还在跑」,而不是重复启动 (两边会同时改同一份文件)。

2.2 新建定时任务(手动)

┌────────────────────────────────────────────────────────────────────────────────┐
│ + 新建任务                                                        [收起 ▲]    │
│ ┌──────────────────────────────────────────────────────────────────────────┐  │
│ │ 要做什么,回车添加                                      [间隔秒数]        │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
│ [每隔几分钟] [每天 ●] [每周] [每月] [自定义表达式]                                │
│      时刻  ▼ 09:00 ▲                                                        │
│ 每天 09:00  ·  0 9 * * *                                                     │
│ 下次执行:2026-09-15 09:00                                                    │
│ ┌──────────────────────────────────────────────────────────────────────────┐  │
│ │ 发给模型的提示词(留空则用上面的标题)                                      │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
│ 用哪个智能体  [不指定 ●] [代码审查]                                             │
│ 能力范围与对话窗口一致:不指定智能体时跟随全局设置,指定了就用它的模型、工具与审批模式。│
└────────────────────────────────────────────────────────────────────────────────┘

任务行(右侧三个按钮:编辑 / 执行历史 / 删除):

┌───────────────────────────────────────────────────────────────────────┐
│ 生成每日简报                                    [✎ 编辑] [⌄ 历史] [🗑] │
│ 普通 可开始 全局 计划: 0 9 * * * 下次 2026-09-15 09:00                 │
│ ┌ 执行历史 ─────────────────────────────────────────────────────────┐ │
│ │ 2026-09-14 09:00  按计划 · 成功 · 4.2s · deepseek-flash           │ │
│ │   今天没有值得报告的改动。                                          │ │
│ │ 2026-09-13 09:00  按计划 · 失败 · 连接超时                          │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘

2.3 cron 编辑器:不用手写

频率          [每隔几分钟] [每天] [每周] [每月] [自定义表达式]
按频率显示    每周 →  时刻 ▼ 08:30 ▲    星期 ‹ 周一 ›
中文说明      每周一 08:30
表达式        · 30 8 * * 1
预览          下次执行:2026-09-15 08:30
  • 接受 5 段标准写法(分 时 日 月 周)与 6 段(含秒)。保存时归一化成 6 段,回显保留你的原文。
  • 星期用中文按钮,按周一到周日排(不是按编号——编号是 1=周日)。
  • 「自定义」一档保留给图形化表达不完的形态(「每月最后一天」);你写的表达式原样保留, 认不出就放进自定义档,不静默重置。
  • 时区按本地解释:你写 0 9 * * *,就是你这边的 9 点。

2.4 对话式创建定时任务

你:帮我每天 9 点检查一下依赖有没有新版本
助手:(正常回答你的问题)
─────────────────────────────────────────────────────────────────────────────
┌─────────────────────────────────────────────────────────────── 边框 ──────┐
│ 要不要建成定时任务?                                                       │
│ 检查依赖更新   · 0 9 * * * · 下次 2026-09-15 09:00                        │
│ 对比 package.json 与上周的提交,列出新增的依赖                              │
│ 可用工具:read、grep                                                      │
│                                          [ 建任务 ]  [ 不用 ]             │
└──────────────────────────────────────────────────────────────────────────┘

只提议、不自动建——定时任务会在未来反复执行,时间理解错了它会安静地一直跑下去。 与记忆候选同一个模式。

2.5 定时任务的能力范围

与对话窗口完全一致,不是另一套配置:

选了智能体  →  继承它的模型、系统提示词、工具白名单、审批模式
没选智能体  →  跟随全局设置

工具、审批、提示词一个都不另配——同一条规则配两处,两处必然分叉, 而分叉的表现是「在对话里能干的事,定时任务干不了」这种要查很久的问题。

与 MCP 是间接关系:MCP 工具由连接层注册进注册表,而上面那份白名单 决定它能不能用——与对话里同一条路径。

上下文也是同一套:两条路都走 turn::build_request,因此基座系统提示词、 记忆、skill 索引、结构化压缩与按条裁剪、超长工具输出落盘成可读回的引用, 定时任务一个不缺。这一点此前是真的分叉过:定时任务自己拼请求,只发预设 提示词(基座一条都没有)、没有任何上下文预算,跑久一点就把窗口撑爆,而界面上 一点异常都看不到。账本同样是同一本:每次模型调用(用量、费用、耗时、 首字延迟、成败)与每次工具调用(工具名、目标、裁决、耗时)都进 audit_record, 与对话那条路同一张表——此前这条路一次都没写,于是「这个月花了多少」在不跑 对话的时候是假的。仍然有意不同的只有三处,都写在 scheduler.rs 的注释里: 「需要确认」在无人值守时等于不执行、轮次用尽没有收尾轮、不接执行计划与附件。

代价要说清:一个配置成「全部放行」的定时任务在半夜会真的写文件。 这是你配置的结果,不是我们替你做的决定;执行历史会如实记下它做了什么。

执行历史逐次记录:

┌ 执行历史 ──────────────────────────────────────────────────────────────────┐
│ 2026-09-14 09:00  按计划  成功    耗时 4.2s   deepseek-flash               │
│   工具:read ×3、grep ×1                                                   │
│ 2026-09-13 09:00  按计划  失败    超时                                     │
│ 2026-09-12 09:00  补做    成功    耗时 5.1s                                │
└────────────────────────────────────────────────────────────────────────────┘

记的是计划触发的时刻而不是点按钮的时刻——否则补做的记录看起来像一次正常触发。 应用被强杀留下的「进行中」记录会在下次启动时标成失败,否则它会让那一次看起来 「做过了」,于是永远被跳过。

2.6 执行计划:两个上限与历次拆解(第 26 批)

上限可以在设置里改。 「模型服务」分区里,紧随「会话交互轮次」多了一张 「执行计划」卡片:

┌ 执行计划 ──────────────────────────────────────────────────────────────────┐
│ 拆出来的每一步最多重来几次、一次计划最多退回几次。两个上限缺一个都停不下来。 │
│ 单步重试上限(1–10)   [ 3 ]  [ 保存 ]                                     │
│ 同一步骤反复不满足它的完成条件时最多再试几次。到顶就如实报告卡在哪一步。      │
│ 总回滚上限(0–20)     [ 4 ]  [ 保存 ]                                     │
│ 一次计划里最多退回重做几次;0 表示不允许回滚,发现问题直接停下来报告。        │
└────────────────────────────────────────────────────────────────────────────┘

两个数各有分工:单步上限挡「这一步就是做不成」,总回滚上限挡 「两个步骤互相把对方的产出改坏」那种震荡——后者用单步上限是挡不住的 (每一步都在自己的上限之内)。输入框与 AppSettings::sanitize 用同一区间, 不会出现「填进去的数被悄悄改掉」。

历次拆解在核心行为页(任务与运行记录之间):

┌ 执行计划 ──────────────────────────────────────────────────────────────────┐
│ 历次拆解都在这里(当前这次在对话右侧的「执行计划」面板里)。                  │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ 先写一份调研笔记,然后按主题整理成表格并导出 CSV                          │ │
│ │ 已完成  进度 2/2 步  全局  2026-09-16 13:20                             │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ 把这个模块拆成三个文件,顺便补上单元测试                                  │ │
│ │ 等你确认  进度 1/3 步  回滚 2 次  项目(…\RWork)  2026-09-16 13:20      │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘

与对话右侧那个面板分工明确:那个显示当前这次的每一步与每次尝试(含退回原因), 这个按作用域回答「上次让它拆的那件事做到哪了」。回滚次数为 0 时不显示—— 它是这一页里最能解释「为什么卡住」的那个数,而不是每行都该有的装饰。

切进这一页会重读:这一页的内容几乎全是你在别处改掉的(对话里拆出的计划、 那一轮才建出来的记忆向量表),不重读就会显示打开窗口那一刻的快照。


3. 附件

┌────────────────────────────────────────────────────────────────────────────────┐
│  ┌──────────────────────────────────────────────────────────────────────────┐  │
│  │ 输入消息,Enter 发送,Shift+Enter 换行                                     │  │
│  └──────────────────────────────────────────────────────────────────────────┘  │
│  [📄 报表.xlsx ×] [🖼 截图.png ×] [📄 规范.pdf ×]                              │
│  将使用 deepseek-flash                                    [📎]  [ 发送 ]       │
└────────────────────────────────────────────────────────────────────────────────┘
类别 扩展名 处理方式
图片 png, jpg, jpeg, gif, webp, bmp 多模态模型 → base64 直接发;看不了图时走本机 OCR(见下)
表格 csv, tsv, xlsx, xls, xlsm, ods 不内置解析器,把路径交给模型,由它自己写脚本读
文档 pdf, docx, doc, odt, rtf 同上
文本 txt/md/json/yaml/toml/ini/log/xml + 40 余种源码扩展名 ≤ 24000 字节直接内联
  • 上限:单个 32 MB、一次 8 个。
  • 不认识的扩展名一律拒绝,不当成文本硬读。
  • 任何一条转换失败都中止发送:静默丢掉一个文件,你会以为模型看过了而据此判断答案。
  • 附件落库存的是当初真正发出去的内容,重开会话时原样回放。

图片走本机 OCR(当前模型看不了图时)

用户:①这张发票的金额是多少        [🖼 发票.png ×]
      当前模型不支持图片,这些图片会用本机 OCR 识别成文字发过去:发票.png
助手:▸ 识别到「价税合计 1,234.56 元」,金额是 1234.56 元。
  • 用百度 PP-OCRv6(检测 + 识别两个模型,共 6.3MB)+ onnxruntime,纯本机推理, 一张 900×320 的截图约 33ms。
  • 模型与运行时放在程序目录旁的 .models/ocr/ 下——和嵌入模型同一处 (第 42 批统一了此前分居两处的模型目录),整个程序目录能拷给别人用。 用到时自动下载(点发送发现没装就先下,下完提示再发一次),设置页的 「本地 OCR」卡片里也能手动下,两处都有进度与结果;装完逐个文件校验哈希。
  • 发出去的正文里带一行说明(「图片:…,用本机 OCR 识别成文字」)与识别出的 文字,模型不会以为它看到了原图——画面里的颜色、版式、位置它并不知道。

为什么要模型自己写脚本,而不是内置解析器

办公格式的多样性没有尽头(xlsx 的共享字符串表、Excel 1904 时制、PDF 扫描件), 每支持一种就欠一笔债。而模型本来就会写代码。应用只保证环境可用并给路径:

用户:这个表格里第三列的总和是多少
助手:▸ run_script { language: "python", packages: ["openpyxl"],
                     code: "…load_workbook('…/book.xlsx')…" }
      结果:1450

4. 脚本运行环境

┌────────────────────────────────────────────────────────────────────────────────┐
│ 脚本运行环境                                                                   │
│ ✓ node     v22.23.1   C:\Program Files\nodejs\node.exe                       │
│ ⚠ python   没有找到                                    [ ⬇ 下载并安装 ]        │
│   没有可用的解释器时,模型无法处理 Office / PDF / 表格这类文件。                 │
│   将下载约 30 MB 并安装到 %APPDATA%\RWork\runtimes(不改系统、不需要管理员)     │
│ 镜像源:pip 阿里云 · npm npmmirror                          [ 重新检测 ]        │
└────────────────────────────────────────────────────────────────────────────────┘
  • 检测:跑 --version 取版本。存在但跑不起来(如 Windows 商店的占位别名) 不算可用——那种情况下报「不可用」而不是报个版本号。
  • 一键安装:从国内镜像下载(node 走 npmmirror、python 走华为云),解压到用户目录。 不装到系统、不要提权——否则一次「点一下」会变成一串 UAC 弹窗。
  • 镜像源:pip 与 npm 各自的候选列表,以环境变量传给子进程,不写 pip.conf/.npmrc (那是全局配置,改了会影响你的其它项目)。
  • 默认 pip 源用阿里云而非清华:实测清华对所有包返回 403。

run_script 工具

模型要跑代码时用它,它替模型做掉那些与任务无关的机械工作:

run_script { language: "python" | "node" | "shell",
             code: "…",           // 多行脚本,落到 .rwork/scripts/ 再跑
             packages: ["openpyxl"],  // 缺包自动装,走镜像源
             timeout_seconds: 300 }
  • 工具块的文字一律按默认字号渲染:输入输出是要读的内容,不是脚注。 把它们缩成小字是这类界面里最常见的毛病——为了「不抢正文的戏」牺牲可读性, 而它恰恰是最需要看清的那部分。
  • 脚本写成文件再跑,而不是 -c "...":绕开命令行转义。
  • 装包走镜像源;PYTHONIOENCODING=utf-8;python 加 -u 关缓冲。
  • 缺解释器时在写脚本之前就拒绝,不留垃圾文件。
  • 配额:超时 300 秒、输出 24 KB、脚本 64 KB。
  • 装包也有上限(180 秒):pip install 是这条路上唯一会长时间不返回的动作, 镜像不响应时它会一直等下去。超时就掐掉,并把「依赖可能没装上」如实告诉模型—— 否则它接下来看到的 ModuleNotFoundError 会被当成自己代码写错了。
  • 每一步都进日志(见 README 的「日志」一节):起进程、装包、运行、结束或超时。 一次「卡住了」的排查全靠这几行。
  • 审批档位是执行——脚本能做的任何事都等同于一条命令。

5. Skill(可复用的做法说明)

┌────────────────────────────────────────────────────────────────────────────────┐
│ Skill(可复用的做法说明,按需注入给模型)                     [导入 skill] [添加目录]│
│ ┌───────────────────────────────────────────────────────────────────  [☑] ┐  │
│ │ 生成周报  把本周改动整理成固定格式的周报                                    │  │
│ │ 来源:D:\skills\weekly-report\SKILL.md                                  │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────────────────┘
  • 形式:YAML frontmatter(name + description)+ markdown 正文。 接受 <名>/SKILL.md,也接受平铺的 .md。
  • 导入:.zip / .skill 文件,或选定一个目录(存目录、每次启动重扫, 于是你在应用之外改了 skill 文件,重启即可生效)。
  • 注入策略:系统提示里只放索引(名字 + 一句话),正文由 skill 工具按需读取。 skill 正文动辄几千字,无条件注入会让每一轮对话都为用不到的内容付费。

6. MCP 服务器

┌────────────────────────────────────────────────────────────────────────────────┐
│ MCP 服务器                                                    [+ 添加服务器]   │
│ ┌───────────────────────────────────────────────────────────────────  [☑] ┐  │
│ │ filesystem                                                              │  │
│ │ npx -y @modelcontextprotocol/server-filesystem D:\work                  │  │
│ │ 启动应用时连接并列出它的工具;工具会以「filesystem__工具名」出现,调用前需确认。 │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────────────────┘
  • 配置形状照抄生态({ "mcpServers": { <别名>: {...} } }), 于是你从别处复制的配置能直接粘进来。
  • 支持 JSON 导入 / 导出(导出时以名字为键,不重复写 name)。
  • 一轮连接一次、用完即关:不常驻。定时任务可能在半夜跑,常驻子进程会积累 脏状态;而它们是外部可执行文件,持有会阻止它们退出(你会在任务管理器里 看到一堆不认识的进程)。代价是每次多一次握手,相对一次模型调用可以忽略。
  • 每台服务器给一个工具声明(远端工具的 schema 是运行时才知道的), 参数里用 action 指定调哪个远端工具;可用工具名注册成 <服务器>__<工具>。
  • 与对话/定时任务的关系是间接的:连接层把工具注册进注册表, 而智能体(或全局)的白名单决定它能不能用——两条路径同一条规则。
  • 审批档位是执行:MCP 服务器的实现由外部提供,无法预知它做什么,因此一律先确认。
  • 连接失败如实报错(列出是哪几台、为什么),否则你会对着一个静默少了的 工具表猜为什么模型不用它。
  • 远程传输(HTTP/SSE)如实说不支持,而不是假装连上。
  • 只提供 prompts 的服务器对 tools/list 回 -32601,识别为「连接成功,只是没有工具」。

7. 权限

┌────────────────────────────────────────────────────────────────────────────────┐
│ 工具与权限                                                                     │
│ 审批模式   [每次都问 ●] [自动写入] [全部放行]                                    │
│ 已记住的授权                                                                   │
│ ┌───────────────────────────────────────────────────────────────────  [×] ┐  │
│ │ bash  npx                      手动设置                        用了 3 次  │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
│ ┌───────────────────────────────────────────────────────────────────  [×] ┐  │
│ │ write  D:\work\notes            多次批准后记住                   用了 7 次 │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────────────────┘

三层级的工具声明,可按参数变化:

档位 含义 「每次都问」 「自动写入」 「全部放行」
read 只读取 自动放行 自动放行 自动放行
write 工作区内写入 询问 自动放行 自动放行
exec 执行命令/脚本 询问 询问 自动放行
destructive 删除 询问 询问 仍然询问

bash 遇到 rm -rf 会自己升级成强制确认,遇到 rm -rf / 直接拒绝。

审批对话框画在整页最上层,并且是页内浮层而不是窗口级对话框:循环停驻在一个 channel 上 等回答的期间,输入区与发送键都被禁用,因此无法在第一个回合被答案阻塞时启动第二个回合。 输入区下方那行提示此时显示的是「有一步操作在等你确认」,而不是「正在生成回复…」—— 审批期间模型那边什么都没发生,说成「正在生成」会让人一直等下去。

内容研判在决定前跑(即使模式是「全部放行」):

  • office 文档、PDF、压缩包、图片是容器格式,按文本改写会破坏它们 → 拒绝。
  • 版本库内部目录(.git/ 等)→ 拒绝。
  • 密钥文件(.env、id_rsa…)、工作区外路径、符号链接、只读文件、大文件、整文件覆盖 → 升级为需要确认。

授权作用域分层,更具体优先,且拒绝优先于允许:

本次会话 < 工具 < 工具组(整个 MCP server)< 目录 < 工具+目录 < 单个文件

所以针对 .env 的拒绝不会被目录级允许覆盖。

用户意图:只有你的话能预授权。

  • 「把这几个 import 改一下」→ 预授权它点名的文件。
  • 「只看不改」→ 整轮拒绝所有写入与执行。
  • 绝不从模型输出推断权限——能自己放宽权限的模型会让其它保证失效。

在线学习(从对话过程里学)

奖励信号来自对话本身,没有环境奖励、不做梯度更新:

信号 来源 效果
正 你在弹窗上点「同意」 计数 +1
负 你在弹窗上点「拒绝」 计数 -1(降低通过率)
强负 你在对话里明确说不要(「别动那个文件」) 一票否决
强负 你强制终止(停止生成、取消这一轮) 一票否决

相似度超过 60% 就按同一类处理,因此 src/a.rs 与 src/b.rs 是同一类, src/ 与 docs/ 不是。权重:

同工具 + 同目录        1.0
同工具 + 父子目录      0.9
同工具 + 无关目录      0.6   ← 刚好门槛
不同工具 + 同目录      0.7
两者都不同            0.0

「不同工具但同目录」(0.7) 高于「同工具但目录无关」(0.6) 是有意的: 你在同目录下反复批准过写文件,说明你对「有人动这个目录」放心; 而「同一个工具在完全无关的目录被批准过」不说明新目录安全。

一票否决不给计数:给计数的话「拒 1 次但批了 9 次」会算出 0.9 而被放行, 而你那次拒绝是明确针对这个情况的。否决的作用范围与学到的作用域是同一套相似度 ——拒绝 src/ 不会让 docs/ 也不可用。

学到的规则区分来源:你手写的显示「手动设置」,引擎推断的显示「多次批准后记住」, 两者都能单独撤销。学习只从你真正回答过的问题取数(「没问就执行」不算批准)。


8. 用量审计

┌────────────────────────────────────────────────────────────────────────────────┐
│ 用量审计                                          [近 7 天 ●] [30天] [90天] [全部]│
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐   │
│ │ 调用次数    │ │ 总 Token   │ │ 预估费用   │ │ 成功率     │ │ 平均首字    │   │
│ │ 1,284      │ │ 3.2M       │ │ ¥ 12.84    │ │ 98.2%     │ │ 0.82s      │   │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ └────────────┘   │
│                                                                                │
│ 日历热点图(26 周,周一起始)                                      口径:[费用 ▾]│
│  ░░▒▒▓▓░░▒▒▓▓██▓▓▒▒░░  ░▒▓█▓▒░  …                                              │
│  悬停显示当天明细                                                              │
│                                                                                │
│ 按天趋势:折线=调用次数  柱形=Token                                             │
│  ╭──────────────────────────────────────────────╮                              │
│  │      ╭─╮        ╭─╮                          │                              │
│  │  ╭───╯ ╰──╮ ╭───╯ ╰──╮                       │                              │
│  ╰──╯        ╰─╯        ╰──────────────────────╯                              │
│                                                                                │
│ 按模型汇总                                                                     │
│ 模型             调用   输入     输出    缓存    费用     占比   平均耗时        │
│ deepseek-flash   890    1.2M     0.4M    0.9M    ¥ 6.20   48%    3.1s          │
│ claude-sonnet    394    0.8M     0.3M    0.6M    ¥ 6.64   52%    5.2s          │
│                                                                                │
│ 调用明细(滚动到底自动加载下一页)                                              │
└────────────────────────────────────────────────────────────────────────────────┘
  • 每次调用(成功、失败、取消)都写一条:模型、服务、五个 token 计数、费用、耗时、首字延迟、结果。
  • 窗口可切近 7/30/90 天、近一年、全部。
  • 热点图 26 周、周一起始,可按调用次数 / Token / 费用三种口径着色。
  • 工具调用与模型调用分开统计,所以「求助了多少次 API」和「它实际动了什么」都能查。

价格不在代码里硬编码,从 models.dev 导入(213 个服务、7700+ 模型, gzip 约 457 KB),缓存进数据库后离线可用。

匹配要求命中接口地址或服务名,没有「谁有这个模型 id 就用谁」的兜底:中转站会用同样的 id 发布加价后的价格,挂到直连端点上会污染审计数字。匹配不到的模型如实显示「参数未知」。


9. 设置

┌────────────────────────────────────────────────────────────────────────────────┐
│ 设置                                      外观、模型服务与数据留存                │
├────────────────────────────────────────────────────────────────────────────────┤
│ 1. 智能体               名字 + 系统提示词 + 模型 + 工具白名单 + 审批模式           │
│ 2. 脚本运行环境          node / python 检测与一键安装(见 4)                     │
│ 3. 扩展                  Skill(见 5)+ MCP 服务器(见 6)                       │
│ 4. 外观                  主题、界面字号、等宽字号                                 │
│ 5. 工具与权限             审批模式 + 已记住的授权(见 7)                          │
│ 6. 模型服务              服务名、类型、Base URL、密钥、模型列表(含价格)           │
│ 7. 本地数据              数据库位置与大小、保留策略、清理入口                      │
└────────────────────────────────────────────────────────────────────────────────┘

模型服务

┌────────────────────────────────────────────────────────────────────────────────┐
│ 模型服务                                                        [同步模型目录]   │
│ ┌───────────────────────────────────────────────────────────────────  [🗑] ┐  │
│ │ DeepSeek                                                               │  │
│ │ 类型         [Anthropic ▾]                                              │  │
│ │ 接口地址     https://api.deepseek.com/anthropic                          │  │
│ │ API 密钥     ••••••••  或  环境变量 DEEPSEEK_API_KEY                     │  │
│ │ 默认模型     [deepseek-flash ▾]  [↻ 刷新模型列表]                        │  │
│ │ 模型                                                                   │  │
│ │  deepseek-flash   上下文 1,000,000  输出 384,000  ¥2/¥8  [✨] [×]      │  │
│ │  deepseek-v4-pro  …                                                    │  │
│ │ 定价区(可滚动,用常驻滚动条)                                            │  │
│ └──────────────────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────────────────┘
  • 三种接口方言:OpenAI 兼容 / Anthropic / Ollama。
  • 密钥可从环境变量读:变量名填了却取不到值会明确报错,不静默发空密钥。
  • 「同步模型目录」按缺口填充:只写空的字段,手填的价格不会被覆盖。
  • 每个模型行有「✨」按钮做同样的事。

同一个分区里还有两张跟「花一次调用」有关的卡片:

┌ 会话交互轮次 ──────────────────────────────────────────────────────────────┐
│ 每回合工具轮次上限(1–50)  [ 12 ]  [ 保存 ]                               │
│ 自动整合长期记忆  [ ●]                                                      │
└────────────────────────────────────────────────────────────────────────────┘
┌ 执行计划 ──────────────────────────────────────────────────────────────────┐
│ 单步重试上限(1–10)  [ 3 ]  [ 保存 ]     ← 见 2.6                         │
│ 总回滚上限(0–20)    [ 4 ]  [ 保存 ]                                      │
└────────────────────────────────────────────────────────────────────────────┘

放一起是因为它们回答的是同一类问题:什么时候会为这一轮花掉一次模型调用 (轮次与自动整合),以及一次回合允许重来几次(执行计划的两个上限)。

本地模型与本地 OCR

┌────────────────────────────────────────────────────────────────────────────────┐
│ 本地模型                                                                       │
│ bge-small-zh-v1.5(92 MB)                                            已就绪   │
│ 下载后放在程序目录旁的 .models/ 下…                                    [已下载] │
├────────────────────────────────────────────────────────────────────────────────┤
│ 本地 OCR                                                                       │
│ PP-OCRv6 tiny(19.9 MB)                                      还没有下载 OCR… │
│ 模型来自 PaddleOCR(百度,Apache-2.0)                    [⤓ 下载]             │
│ 放在 .models\ocr 下:D:\…\.models\ocr                                          │
└────────────────────────────────────────────────────────────────────────────────┘
  • 嵌入模型与 OCR 的模型、运行时都放程序目录旁的 .models/(OCR 在它下面的 ocr/)——整个 .models 能拷给别人用。第 42 批把此前分居两处的模型目录统一了: 位置只由 RWORK_MODELS 一个变量决定。
  • 两处的状态都分三种:没文件 → 去下载;文件不对(下了一半、拷错了)→ 删掉重下;就绪 → 没事做。
  • 下载都带进度(按「正在下哪个文件 / 包」显示),完成后立刻校验哈希。

本地数据

┌────────────────────────────────────────────────────────────────────────────────┐
│ 本地数据                                                                       │
│ 数据库    C:\Users\ligen\AppData\Roaming\RWork\rwork.db          12.4 MB       │
│ 保留策略  [✓] 启用    保留 [90 ▾] 天    [✓] 置顶对话豁免                       │
│           [✓] 启动时清理   [ ] 清理后压缩(VACUUM)                            │
│ 上次清理  2026-09-14 09:12   清理 3 段会话、412 条审计记录      [ 立即清理 ]    │
└────────────────────────────────────────────────────────────────────────────────┘

清理前会显示「将清理多少条」,不会先斩后奏。

后台与启动

┌────────────────────────────────────────────────────────────────────────────────┐
│ 后台与启动                                                                     │
│ 关掉窗口之后程序还在不在、开机之后它自己会不会回来——定时任务能不能无人值守,   │
│ 就看这两条。                                                                   │
│                                                                                │
│ 关窗口后继续在后台运行                                        [ ●——]  开      │
│ 关窗口只是把界面收进托盘,定时任务照常执行;要真正退出,用托盘图标右键菜单里的  │
│ 「退出」。关掉这一条时,关窗口就是退出。                                        │
│                                                                                │
│ 开机自动启动                                                  [ ○——]  关      │
│ 开机时静默启动(只驻留托盘,不弹窗口)。写的是 HKCU\Software\Microsoft\…     │
│ \Run 里的一项,关掉它就会删除那一项;目前只支持 Windows。                       │
└────────────────────────────────────────────────────────────────────────────────┘

第一条默认开:定时任务是无人值守的,「关窗口 = 退出」会让它在用户以为它还在跑的 时候悄悄停下。第二条默认关:它是这台机器上的一次持久改动,得由用户点头。

2.7 后台常驻与「被杀之后接着做」

窗口右上角 ✕  ──▶  窗口消失、进程还在(托盘图标留着)
托盘图标 右键   ──▶  ┌ 打开主界面 ┐
                    └ 退出       ┘
再双击一次 exe  ──▶  已经开着的那一个把窗口叫回来,新的那个自己退出(单实例)
  • 关窗口 ≠ 退出:关掉窗口只是把界面收进托盘,定时任务照常执行。真要退出走托盘菜单 那一项(或者把设置里那条开关关掉,那时关窗口就是退出)。第一次收进托盘时会冒一个气泡 告诉你这件事——否则「我明明关了它」是最容易产生误解的地方。
  • 托盘图标:右键有「打开主界面 / 退出」,双击也是打开。用的是系统默认图标(仓库里没有 .ico)。
  • 开机自启:设置页「后台与启动」里打开它,写 HKCU\...\Run 的一条值 ("<程序路径>" --silent);--silent 表示开机时不弹窗口,只驻留托盘、直接开始跑任务。
  • 单实例:第二个实例不会重复跑任务——它把已经开着的那个窗口叫到前台,然后自己退出。 这一条对无人值守是硬要求:due_tasks 是「查一次、改一次」两步,两个进程同时跑会各跑一遍。
  • 被杀之后接着做:进程被杀(或断电)时,那一轮任务在库里留下「跑到哪一步」;下次启动会 接着做一次——用的是原来那段会话,提示词里写清「上一次执行在第 N 步被打断,请接着那次 继续,已经做完的不要重做」。收件箱式的交代就在那段会话里,翻得到。 没有跨任务的通用队列:只有定时任务这一轮会被接着做(docs/10 设想的 job 表仍不存在)。

10. 工作空间

┌────────────────────────────────────────────────────────────────────────────────┐
│  ┌──────────────────────────────────────────────────────────────────────────┐  │
│  │ RWork 工作台  [📁] [«]                     ← 折叠按钮方向性:收起 › 展开 ‹ │  │
│  └──────────────────────────────────────────────────────────────────────────┘  │
│  点 [📁] 展开:                                                                │
│  ┌──────────────────────────────────────────────────────────────────────────┐  │
│  │ 工作空间                                                                  │  │
│  │ 当前  RWork                                                              │  │
│  │       D:\Develop\workspace\gitonline\RWork                               │  │
│  │ 最近  ▸ 另一个项目                                                       │  │
│  │  [ 📁 选择文件夹 ]   [ + 新建项目 ]                                      │  │
│  │ 新建项目:名字 [________]  [✓] 同时 git init                             │  │
│  └──────────────────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────────────────┘
  • 全部用系统文件夹选择器,没有手输绝对路径的输入框。
  • 主工作空间 + 最多 10 个额外根;最近使用最多记 8 个。
  • 侧栏右边缘 6px 可拖动改宽(200–520px)。
  • 工作空间决定:相对路径的基准、写入是否算「工作区内」、项目范围记忆与任务的可见性。
  • 工作空间跟着会话走:每一段对话都记住自己创建时(或被切换时)的那个目录, 之后在别处切换工作空间不会把这段对话挪走。侧栏顶部显示的、以及工具实际使用的 都是这一段的那个目录——显示全局而实际按会话跑,等于界面在说谎。
  • 切换器同时做两件事:改全局默认(新建会话用哪个目录),并把当前这段会话 钉到新目录。只做前者时,同一个动作对「还没聊过的会话」和「已经聊过的会话」 效果不同,没人能预测。
  • 空字符串表示「跟随全局」——升级前就存在的会话迁移过来就是这一种, 因此它们的行为与升级前完全一致(我们并不知道那段对话当初是在哪个目录里进行的, 回填只会猜错)。

11. 外部智能体(omp)

┌────────────────────────────────────────────────────────────────────────────────┐
│ 设置 → 工具与扩展 → 外部智能体                                                    │
│                                                                                │
│  ┌──────────────────────────────────────────────────────────────────────────┐  │
│  │ 外部智能体接管回合                                            [ ●——]      │  │
│  │ 打开后,这一轮由外部程序(默认 omp --mode rpc)执行……          关           │  │
│  │ omp --mode rpc                                                           │  │
│  │ 注意:当前配置下,它对工作区的写入不经过 RWork——只读工具清单为空(等于把它   │  │
│  │ 的内置工具全部打开),或你在参数里自己写了 --tools 并且里面有 write / edit / │  │
│  │ bash。那些改动没有权限判定、没有内容研判,也不会登记文件出处,记录里只剩转述。│  │
│  └──────────────────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────────────────┘

打开开关后,对话里的每一轮:RWork ──prompt──> omp ──事件流──> 逐字上屏
                                              │
                                              ├─ text_delta        → 正文
                                              ├─ thinking_delta    → 思考通道
                                              ├─ tool_execution_*  → 工具块(它执行的)
                                              └─ turn_end/agent_end → 用量、费用、收尾
                                              ↓
                                     镜像进库(助手消息 + 工具消息 + 一条账本)
  • 它执行,我们记账:这一轮的工具由外部程序决定并运行,RWork 不做二次执行 (工具调用在这里是已经发生的事实,不是「请你去跑」)——因此镜像时 工具结果走一条单独的路,与 StreamEvent::ToolCall 分开。
  • 但写入类工具交回 RWork:
启动:omp --mode rpc --tools read,grep,glob        ← 它的内置工具收成只读
     set_host_tools: rwork_write / rwork_edit / rwork_bash   ← 我们提供
它请求:host_tool_call { toolName: "rwork_write", arguments: {...} }
我们:  prepare → decide(纯函数引擎 / 内容研判 / 历史授权)→ 需要时弹同一个审批框
        → invoke(在工作空间里真的写)→ 审计 + 文件出处登记
回它:  host_tool_result

于是「它每一次改动工作区或执行命令」都要过 RWork 的权限判定——这是 「谁执行谁负责」的落点,也是审计页能回答「它到底动了什么、谁批准的」的原因。 名字必须加 rwork_ 前缀:实测宿主工具不能与它的内置工具重名(即使那一个已经 被 --tools 关掉),否则 set_host_tools 直接回 conflicts with an existing tool。

  • 库里三种结果分得开:host(我们执行的)、external-write(它自己执行的写)、 external(它自己执行的读与搜索)。重放时不会把别人的工具输出当成我们能重跑的东西; 而「这一次改动没经过我们」在对话里也直接写在工具块的名字旁边(未经 RWork 的写入), 不必去翻库。它自己跑的那一次,我们只看得见结果——所以宁可少标,也不乱标: 名字认不出来时按 external 记。
  • 「这一段是转述」也写在名字旁边:它自己执行的读与搜索标成「由它自己执行(转述)」, 与我们执行的那些区分开;写法是陈述语气,与写入那条警告色互斥。这两句合起来回答同一个 问题——这一段对话里哪几次调用真的经过 RWork。
  • 用量与费用取它报的那一份(turn_end 的 usage.cost),记在 omp 名下。 审计页因此能把「它花的」与「RWork 自己花的」分开;价格表是它的, 按我们的表重算只会得到两个都不对的数。
  • 按对话复用一个进程:对方的会话状态(它自己的上下文、它压缩过的历史) 接着上一次才算同一段对话。切走对话或退出应用时收掉(kill 之前先关 stdin, 让它把待发输出发完)。
  • 默认关闭,而设置页里写清了边界:只有写入类工具过 RWork 的权限链; 它的读与搜索、以及它的上下文压缩都是它自己的,那部分在这份记录里是转述。 读为什么不设边界(我们自己的读同样自动放行、同样不做工作区包含检查)、 兜底与残余风险是什么,写在架构文档 §8 第 3 条(威胁模型)。
  • 没装 omp 也不影响任何事:开关默认关着,而开着但起不来时给的是 「启动外部智能体「omp」失败:……」,不是一句「出错了」。宿主工具注册失败时 也会说出来(那一轮它就只能读、改不了工作区),而不是让用户以为写入受管辖。

12. 本地数据与渐进刷新

  • 单文件 SQLite(WAL、synchronous=NORMAL、外键开启),默认 %APPDATA%\RWork\rwork.db,可用 RWORK_DB 覆盖。
  • 审计记录不挂在对话下:删掉对话不会抹掉「曾经花过这些 token」这一事实。
  • 模型运行时与下载的安装包放在和数据库同一个目录下。

首屏渐进刷新:窗口先出现,内容分层补齐,任何一侧慢都不拖住其它部分。

1. 运行时与数据库就绪,内置字体在首帧之前注册(避免中文落到替代字体)
2. 打开窗口,渲染侧栏、导航与启动清单 —— 此时已可交互
3. 启动管线依次报告「打开数据库 → 读取设置 → 载入模型服务 → 汇总审计」
4. 每个卡片各自持有 加载中 | 就绪 | 失败,各自发查询、各自重绘
5. 慢查询(调用明细)滚动到才加载

13. 数据流

GPUI 界面线程 ──渲染──> 页面实体 ──spawn──> tokio 运行时
                              │                  │
                              │                  ├─ spawn_blocking: SQLite 查询
                              │                  └─ reqwest: 流式 SSE
                              │
                       逐帧 delta 重绘 ── 完成后写入消息 + 审计记录

界面线程只负责渲染,不做 IO。一个要点:GPUI 的异步执行器不是 tokio, 而 cx.spawn 的 future 在界面线程上被 poll,因此主线程必须始终处在 tokio 上下文里 (main.rs 的 runtime.enter())——缺了它,任何 reqwest/tokio::time 调用都会 panic 而不是返回错误。

定时任务的调度循环不同:它在普通线程上自建 tokio 运行时,与界面完全解耦。


14. 命令行与配置

cargo run -p rwork-app          # 依赖 Rust 1.90+(edition 2024)与 MSVC 工具链
rwork-app.exe --silent          # 静默启动:窗口建出来但不显示,只驻留托盘(开机自启用它)

--silent 是开机自启写进注册表的那一份命令行。只有一个实例:第二个进程会把已经开着的 那个窗口叫到前台,然后自己退出。

环境变量:RWORK_DB(数据库位置)、RUST_LOG(日志级别,默认 rwork=info,warn)、 以及各 API 密钥的环境变量名(在服务配置里填变量名)。

设置落在数据库 meta 表(键 settings 的 JSON),也可手改后重启。 主要项:locale / theme / font_size / retention / audit_window_days / heatmap_metric / providers[] / approval_mode / workspace / sidebar_width / enabled_tools / tool_policies / learn_from_history / max_tool_rounds / max_context_tokens / agents[] / disabled_agents / skill_dirs / disabled_skills / last_page / run_in_background。 (开机自启不在这个 JSON 里:它的唯一事实是注册表 HKCU\...\Run 里那一项。)

密钥按明文存在本机数据库文件里,不会外传。


15. 快捷键

键 作用
Enter 发送消息(对话页)/添加(记忆、任务输入框)
Shift+Enter 换行
Esc 清空搜索框

附:四个页面的导航

对话 ────────── 与模型交流;工具调用、附件、流水线、权限确认都在这里
  │
核心行为 ────── 长期记忆(项目/全局)+ 任务与定时任务(含补跑、执行历史)
  │
用量审计 ────── token 账本:窗口统计、热点图、按天趋势、按模型汇总、调用明细
  │
设置 ────────── 智能体 / 脚本环境 / 扩展(Skill+MCP) / 外观 / 权限 / 模型服务 / 本地数据