Skip to content

GreyWork v0.4.0

Choose a tag to compare

@github-actions github-actions released this 28 Sep 09:02

新增

  • web 端新增登出入口:侧栏底部的占位「用户」行换成真实账户行——显示身份、点击直达助手页,
    服务端态再给一个登出按钮(桌面态无此概念,不渲染)。登出走 POST /api/logout 且二次确认;
    无论请求成败(含 401 与网络异常)本端都会把登录门重新关上——「登出的语义是本端不再持有会话」,
    不把「服务器没收到」当成「还登着」。
  • 服务端命令目录与逐命令能力发现:GET /api/commands 在既有元数据上新增 available 字段
    (「本宿主禁用」与 desktopOnly 的「任何宿主都没有」刻意分开);渲染端新增命令能力表 store
    首用即拉、会话内缓存。能力值三态:true 放行、false 明确禁用、null 未知——读路径
    fail-open(未知不禁),服务端写路径 fail-closed(未知不写),语义由调用方各取所需。
  • 云端 Office 预览:新增「第三方服务」设置,可配置多家云端 Office(WPS / 腾讯文档等)的文档
    预览:选中文件由宿主按声明式配方上传到厂商接口,取回可内嵌的文档地址后在预览区用 iframe
    打开 —— 能看本地解析不了的大文档,且厂商凭证只经环境变量注入宿主、不进渲染端。宿主把真实的
    可内嵌白名单(frame-src 放行的 origin)回给渲染端,渲染端在上传前先判地址是否可内嵌、
    取回后再判一次,不可内嵌时直接回落到本地预览并点明被拦的 origin,避免「白屏且控制台之外
    无提示」。桌面壳的 CSP 写死在打包配置里,所以自定义厂商只在服务端态生效(服务端 CSP 由
    GREYWORK_FRAME_ORIGINS 启动时拼),设置页对此如实标注。
  • 定时任务可交由服务端主执行:新增 GREYWORK_AUTOMATION_HOST_PRIMARY=1,置位后宿主成为定时
    任务的主执行者 —— 到点即由本进程执行,不再需要一台一直开着的电脑。默认仍与桌面端一致
    (宿主只在渲染端缺席 2 分钟后兜底认领),升级不会让既有部署突然自己跑任务;两种角色共用同一套
    行级原子认领,双执行防护不变。注意无人值守的模型凭证取自服务端进程的环境变量,容器里需在
    compose 注入,否则任务会以「无可用模型配置」失败。
  • 服务端可容器化部署(web 端):新增多阶段 Dockerfile(Node 构建 SPA + Rust 编译
    greywork-server,最小 debian-slim 运行时)、docker-compose.yml、.env.example 与部署文档
    (docs/packaging.md)。二进制内置 healthcheck 子命令供容器 HEALTHCHECK(镜像不带 curl / wget);
    监听非环回地址却未开 secure_cookie 时启动告警——提示前置 TLS 反代,避免登录凭据明文过网。
    compose 默认禁提权(no-new-privileges);需要 agent 沙盒的 fs / full 两档时叠加
    docker-compose.sandbox.yml(代价与实测能力矩阵写在文件里)。另有独立的 Docker CI 只做
    「镜像能不能构建、起来后探针与 UI 是否正常」的冒烟。
  • 服务端补齐安全响应头:headless 服务端对每个响应加 Content-Security-Policy(对齐桌面 Tauri
    CSP,另放行入站视频 / 语音缩略图的 media-src blob:)、X-Frame-Options: DENY、
    Referrer-Policy: no-referrer、Permissions-Policy(相机 / 麦克风 / 定位等一律拒绝,唯独不碰
    clipboard-*——界面靠 navigator.clipboard 做复制按钮),并在开启 secure_cookie(即「已在 TLS
    反代之后」的信号)时下发 Strict-Transport-Security(此前只有 nosniff 与 no-store)——
    自托管 Web 端不再缺 CSP 与防点击劫持。策略与真实托管产物的一致性由 e2e 把关:真服务端托管
    构建产物,走登录门并依次打开 markdown / HTML / PDF / xlsx 四类预览,断言零 CSP 违规。
  • 服务端态断线重连自动对齐状态:web 端事件走一条 /api/events WebSocket 且不重放;断线重连时宿主
    先发 host://hello,远程助手 store 据此重拉各通道状态与媒体能力矩阵(跳过首帧连接),修掉重连后
    通道在线状态陈旧的问题(桌面端走 Tauri IPC,不受影响)。
  • 企业微信入站语音接入:企业微信「API 模式 · 长连接」的语音消息只下发转写文本
    (voice.content,没有可下载的原始音频),此前被当作不支持类型丢弃 —— 现按转写文本转达,
    远程助手可直接应答语音内容。其余通道的语音是可下载音频、走既有媒体收发;企业微信是唯一
    以转写文本承载语音的通道,故不进媒体能力矩阵。
  • 微信远程助手支持图片与文件收发:手机发来的图片 / 文件由宿主下载并用 AES-128-ECB 解密后
    落进收件目录,渲染端取走落进会话附件库,可直接喂给本机模型或 ACP 后端;反向也能把桌面端
    选中的文件、以及本回合产出的交付物加密上传后发回手机(单文件上限 20MB)。远程回合会告知
    模型「对方不在本机」,因此被要求发文件时不再答「文件已在本机」——它用回复末尾的 sendfile
    围栏点名要发的路径即可;可发范围限工作区与用户已授权的目录。
  • 远程助手文件传送铺开到全部 7 条通道:图片 / 文件收发从仅微信扩展到 Telegram / Discord /
    飞书 / QQ / 企业微信 / 钉钉,按各协议真实能力落地并在界面上如实标注 —— 前四条与 QQ 双向收发
    (QQ 出站走 4 步分片上传),企业微信收图片 / 文件但只能发图片(被动回复没有文件出口),
    钉钉只能收图片(sessionWebhook 没有媒体通道)。入站媒体由宿主下载后落进各通道自己的收件
    目录(企业微信还要按 aeskey 做 AES-256-CBC 解密),渲染端取走即删、交会话附件库;出站走
    统一的 channel_send_media,授权面与 20MB 上限在宿主侧一处把关。各通道协议端点手写,
    未引入第三方 Rust SDK(微信仍用 wechatbot)。
  • 远程连接归入专属工作区:所有通道的新联系人会话统一进入「远程助手」工作区,默认使用 ~/.greyWork/remote(可自定义);退出登录或清除凭证后重连会自动新开会话,普通断线重连保留上下文。
  • 对话附件新增通用文件类型:附件不再只认图片与文本,PDF / 压缩包 / Office 文档等一律收下
    并落盘(浏览器态无文件系统仍不收)。模型侧不内联内容,只递一条 [文件:名字(本地路径:…)]
    引用 —— 本机 ACP agent 可按路径直接读,纯 LLM 至少知道文件存在。
  • Office 文件可编辑并保存:xlsx / docx / pptx 预览从只读改为可编辑,改动经 Ctrl+S 或工具栏
    保存按钮写回来源(磁盘或 VFS),标签页脏点与关闭前确认统一兜住未保存改动。
    • xlsx:延用 Univer 表格编辑,保存时 Univer 快照经 univerWorkbookToXlsx 反向序列化回 xlsx;
      含图表 / 图片的表拒绝保存(读入时图元已被剥离、exceljs 写入器也不写回,存下去会静默丢失),
      提示改用「用系统应用打开」。
    • docx / pptx:正文文本 run 就地可编辑,保存只对原 zip 里命中的 w:t / a:t 做文本补丁、
      其余字节原样保留(不改结构 / 样式 / 母版 / 媒体);仅支持改文本,不支持增删段落或改版式。
  • 文本 / 代码预览可就地编辑并保存:code 与 raw 两类预览不再只读 —— CodeMirror 去掉
    readOnly、装上历史与键位表,改动经 Ctrl+S 或工具栏保存按钮写回磁盘 / VFS。两条前提由宿主
    探测决定:判为二进制的给占位(不去读内容),非 UTF-8 的保持只读并说明原因。
    • 编码与行尾保真:读盘时 BOM 已被剥掉、CodeMirror 的 doc 一律用 \n,所以保存前按探测
      结果补回 BOM 与 \r\n —— 否则在 Windows 工作区里改一行会变成整文件行尾变更。
  • 图片预览加缩放 / 平移 / 旋转:工具栏加放大 / 缩小 / 适应 / 1:1 / 左转 / 右转,滚轮以指针
    为不动点缩放,放大后可拖拽平移;换图(revision 变化)时视图归位。变换全走 CSS transform,
    不重采样,所以放大是模糊的(换取不动 <img> 的布局盒与 src)。
  • PDF 预览加跳页与缩放:工具栏加页码输入 / 上一页 / 下一页与缩放档位(1 倍 = 适应宽度),
    跳页时先补渲染到目标页。缩放落在渲染阶段而不是套 CSS transform —— 只有 canvas 的 CSS 尺寸
    跟着变,滚动区高度才是真实高度。
  • 错误态与二进制占位给出「用系统应用打开」出口:文本、图片、PDF、文档四个 viewer 的失败态
    统一挂一个兜底按钮(仅当 tab 有磁盘孪生路径时出现),大文件超限不再只是干瞪一行报错。
  • 文件树支持新建 / 重命名 / 删除 / 复制 / 剪切粘贴:宿主补了 5 个命令
    (fs_create_file / fs_create_dir / fs_rename_path / fs_copy_path / fs_delete_path),
    右键菜单按目标给「新建文件 / 新建文件夹 / 重命名 / 复制 / 剪切 / 粘贴 / 删除」。
    • 重命名就地编辑(整行换成输入框:<input> 不能嵌在 <button> 里,所以重命名期间
      该行整体让位,data-ctx 上移到行外层,改名时右键仍命中同一行);新建走弹窗,删除走确认框。
    • 操作后只重列受影响的目录,仍然存在的节点对象原样接回去 —— 不能图省事调 refresh(),
      它会清空展开态,把用户刚整理好的树整棵折回去。
    • 已打开的预览 tab 会跟着改路径(改名 / 移动按前缀搬),删除则关掉受影响的 tab,
      否则编辑器下次保存会写回一个不存在的旧路径。
    • 边界:只作用于授权根内(单独授权的散文件不参与,避免「能删不能改名」的割裂);
      from 一律取条目自身而不是符号链接目标(删软链不该删掉目标文件);
      复制目录时跳过目录内的符号链接(不把根外内容搬进来);跨分区移动降级为「复制 + 删除」
      (顺序是先复制成功再删源,中途失败最多留个多余副本,不会出现「源已删、目标残缺」;
      条目内含符号链接时拒绝降级,否则那些链接会被连同源一起抹掉);目标已存在一律拒绝
      (Unix 的 rename 会静默覆盖)。
  • 变更面板支持逐文件暂存与选择性提交:列表拆成「已暂存 / 未暂存」两区(同一文件两侧都有
    改动时两个分区各出现一次),每行带暂存 / 取消暂存按钮,头部有全部暂存 / 全部取消,
    提交框主按钮只提交已暂存,另留一个「全部变更」保留旧的 add -A 行为。
    • 宿主侧 git_changes 改为分侧上报(index / worktree 各自的状态与行数,不再把两侧相加),
      git_diff 加 staged 参数并按侧取 diff(不再把两段拼在一起),新增 git_stage / git_unstage。
    • 取消暂存处理两个坑:没有提交的仓库没有 HEAD 可 reset,回落 git rm --cached;
      重命名要成对处理(新路径 + 旧路径一起 reset,否则索引里留一条删不掉的幽灵变更)。
    • 顺带修 --numstat 对重命名的解析:git 的花括号压缩形态(dir/{old => new}.txt)
      会被按 => 切错,现在取到的是正确的新路径。
    • 面板文案全部转 i18n(此前整块是硬编码中文)。
  • 变更面板新增「历史」页:只读的提交列表(短 hash、主题、作者、时间、引用装饰),
    点一条就地展开该次提交的 diff;取满一页时给「加载更早的提交」按 skip 翻页。
    • 宿主新增 git_log / git_show:log 用 US/RS 控制字符当字段 / 记录分隔符(主题里含
      分隔符也不会错位),limit 默认 50、上限 200;show 用 --format= 压掉提交头,只回 diff 正文。
    • 没有提交的仓库返回空列表而不是报错;历史惰性加载(第一次切到该页才拉),
      提交 / 暂存后若历史已打开会自动重拉。
    • diff 的行级渲染抽成 GitDiffBody.vue,与变更页共用(两处各写一遍迟早会在行数上限
      与标记符上走岔)。
  • 文件树支持键盘导航与拖拽移动:焦点在某一行上时,方向键在行间移动焦点、右键展开 /
    进入子项、左键收起 / 回到父行、Home / End 跳首尾、F2 就地改名、Delete 走确认后删除、
    Ctrl+C / X / V 走应用内剪贴板(Enter / Space 不接管,交给 button 原生 click)。
    拖拽可把条目移到目录行(拖到文件行 = 移到它所在的目录)或空白区(= 移到根目录),
    拖进自己的子树、落到当前所在目录都会被拒绝,拖拽期间落点行高亮。

变更

  • 服务端态的界面开始说实话:更新检查、系统信息的门从「是不是 Tauri」换成「宿主命令可不可用」,
    浏览器预览态专属的早退不再误伤服务端;关于卡显示运行形态与服务端版本;沙盒与权限档位卡改按
    「钉住」判定——服务端把这两样钉在启动配置里时按钮禁用并说明「以服务端为准」,未钉住时照常可改
    (临时只读开关与档位按钮一并禁)。sys_info 如实回传钉住事实与宿主版本,桌面壳传自身状态,
    服务端取自启动配置、与沙盒覆盖逻辑同源。
  • agent 目录在服务端态只读:db_agents_sync 被服务端禁用时,设置页与对话底栏的写入面都收起来
    (新增 / 编辑 / 启停不给点),并就地说明「改动只落在浏览器本地、会与服务端数据库悄悄分叉」;
    对话底栏选择未启用的后端也不再代用户顺手启用,让「尚未启用」错误就地可见。判据与写路径门同源
    (命令能力表),未来服务端放开该命令时前端零改动自动恢复可写;图标覆盖层不走这条命令,保持可改。
  • 预览查看器与右侧面板外壳的文案接入 i18n:DiffViewer / DocViewer / HtmlViewer /
    LegacyOfficeViewer / MarkdownViewer / SheetViewer / SlideViewer / PdfViewer 的加载、
    读取失败、解析失败、空态与工具条摘要,以及 PreviewSider 的区段标签、工具栏 / 标签的
    aria-label 与空态文案此前都是硬编码中文,现全部走 locales(en 同步补齐)。
    带变量的整句(「共 N 页」「仅显示前 N 行,共 M 行」)做成单键 + 参数,不把半句话拼在模板里。
  • 微信通道的协议实现改为官方 Rust SDK(wechatbot):扫码登录(含配对码 / IDC 重定向 /
    已绑定复用)、长轮询收信、文本与媒体收发、CDN 上传下载、AES-128-ECB 加解密此前都是宿主
    手抄协议,现整条交给该 SDK;宿主只保留它管不到的那层 —— 状态机、凭证落盘(0600)、入站
    媒体落进收件目录等渲染端取走、Tauri 命令与事件、发送者策略。手写协议层连同 aes /
    ecb / md-5 三个依赖一并删除,换来的是:媒体上传按协议用 upload_param 拼地址、
    上传失败自动重试、超长文本自动分段、会话过期自动重登。
    随之的边界(SDK 不暴露 / 不提供,如实记下):
    • 扫码界面不再有「已扫码待确认」中间态(SDK 只在拿到二维码时回调),
      wechat_login_poll 的产出只剩 wait / confirmed / expired;
    • 「正在输入」只能发不能撤(SDK 固定 status=1),取消在宿主侧是 no-op,指示器由微信自行超时;
    • 配对码登录要用户在手机上读码后填回本机,界面没有这个输入面 —— 遇到时记一行日志并让本次扫码失败;
    • 入站消息不再带 messageId(SDK 的线格式里没有该字段,渲染端本来也没读它);
    • 入站媒体由 SDK 一次性收进内存,20MB 上限只能在落盘前按实际字节数拒绝;
    • 会话过期后的重登由 SDK 内部发起,那条路的二维码没有界面承载,宿主把这件事报成
      paused + 「登录态已过期,请重新扫码」。
    • 入站媒体只从固定 CDN 基址下载、出站只朝固定基址上传,服务端下发的任意 URL 不再参与
      取字节(SSRF 面因此消失);登录下发的 baseurl 仍按「https + qq.com 域」校验,
      不合规即删掉刚落的凭证并丢弃 SDK 实例。

修复

  • HTTPS 部署下登出清不掉会话 cookie:带 Secure 属性的 cookie 按 RFC 6265bis 只能被同样带
    Secure 的 Set-Cookie 覆盖,而清理用的 Set-Cookie 固定不带——TLS 反代之后登出后旧 cookie
    原样留在浏览器里。现与签发共用同一份 secure_cookie 配置,清理属性与签发必然一致。
  • opencode 2.0 下宿主权限裁决全线失效:opencode 2.0 重命名了权限配置(permission 对象 +
    bash → permissions 数组 + shell),注入的 v1 形状被静默忽略、默认策略全 allow——agent
    对工具直接执行、从不发 session/request_permission,宿主的只读 / 工作区档位就没有裁决入口。
    预设与链路测试改为同时注入 v1 / v2 两种形状(两版本各读各的键互不冲突)。
  • git 查询不再可能永久挂起:宿主跑 git 用的是 Command::output(),既没有超时窗口
    (巨型仓库 / 冷缓存 / 网络盘上的 status、diff 长时间不出结果时,渲染端会一直转圈),
    也会在管道写满而无人读时死锁。现在改成 spawn + 读线程抽干管道 + 主线程轮询退出状态:
    30 秒未退则整组杀进程并报错,让界面能恢复。
  • 微信出站不再把「服务端拒绝」当成发送成功:业务错误是随 HTTP 200 放在响应体里的
    (ret / errcode 非 0),只看 HTTP 状态会把被丢掉的消息当成功 —— 手机端收不到、桌面端
    却看得见回复。这条判定现由 SDK 统一做(非 0 业务码一律报错),宿主另给每条入站(发送者 /
    条目类型 / token 长度)与每条出站(「已回复」或失败原因)各记一行日志,便于定位
    「第一次能回、后面回不了」这类问题。
  • 微信「第一条能回、后面回不了」:微信的 context_token 按条颁发,复用旧 token 会被
    服务端静默丢弃;而自动回复原先在发信那一刻才去读 peer.contextToken —— 那永远是最新一条
    的值。第一条还没回完、第二条就到时,第一条的回复会占用第二条的 token,轮到第二条就成了复用。
    现在宿主按 context_token 记住每条入站消息,回信(文本与媒体)都走 SDK 的
    reply / reply_media,凭据与被回复的那条严格对应。
  • fs_write_binary 的上限按字节数判定:此前拿 base64 编码串的长度去比 20MB,而 base64 会
    把载荷撑大约 4/3,导致 20MB 原始文件编码后约 27MB 被误拒,有效上限只有约 15MB。
  • 微信入站过滤不再依赖协议里并不可靠的 message_type,改判 item_list 里是否含可识别的条目 ——
    否则纯媒体消息可能被整条丢弃。
  • 切区段 / 切标签 / 关窗不再静默丢掉未保存的编辑:此前只有「关标签」有确认,而区段切换与
    切标签都会卸载编辑器并注销它的保存器,关窗更是连问都不问 —— 在 xlsx / docx / pptx / 文本里
    改过的内容会无声消失。现在这几条离开路径共用一个策略:先自动保存,全部成功才继续,
    写不进去才弹确认(放弃并继续 / 取消),并在保存成功后给一条回执(自动写盘是用户没显式要求
    的动作,界面必须能区分「悄悄存了」与「悄悄没了」)。
    • 关窗走渲染端的 onCloseRequested(可就地拦下,无「标志还没推给宿主」的竞态);托盘
      「退出应用」与 Cmd+Q 走的是 app.exit,绕过该事件,改由宿主在 RunEvent::ExitRequested
      里用渲染端同步过来的「有无未保存改动」标志拦一次。
    • 「关闭到托盘」为真时点 X 只是隐藏窗口,不丢数据也不弹窗,该分支优先。
  • 未知扩展名的二进制文件不再被当文本读成满屏乱码:decode_text_bytes 对非法字节只做
    U+FFFD 替换、从不报错,于是 .zip / .exe / .woff 打开就是一屏占位符,看着像文件坏了。
    宿主新增 fs_probe_file(只读前 8KB)判定「像不像二进制」「是不是 UTF-8」「BOM 与行尾」,
    预览据此改给占位;UTF-16 的「字符 + 0x00」交替形态被显式放行,不会误判成二进制。
  • 窄屏自动折叠右栏不再丢未保存的改动:模板里 PreviewSider 是 v-if="preview.available",
    窗口变窄就整个卸载 —— 而卸载会注销预览编辑器的 saver,未保存的改动随之消失(与上一轮修的
    区段 / 标签切换是同一类问题,但那条路径当时漏了)。现在「从桌面变窄」先走一遍守卫:
    面板还挂着的状态下自动保存,全部成功才真的收起来;写不进去会挂起确认弹层。
    用户在窄屏选了「取消」之后不再反复追问,直到视口回到桌面重新武装。
  • 切走 PDF 不再抛错,也不再漏 worker:PDFDocumentProxy.destroy() 在 pdfjs v6 已被移除
    (只剩 cleanup()),预览关闭 / 切标签时那次调用于是抛 destroy is not a function,
    worker 一直没被释放 —— 每开一次 PDF 漏一个。现在改为持有加载任务并调它的 destroy()。
  • 自托管 Web 端的 CSP 不再被自家脚本撞出违规:index.html 里那段首帧主题脚本是内联的,
    在桌面壳与自托管服务端都不含 'unsafe-inline' 的策略下会被 script-src 拦掉(浅色用户
    会先闪一帧深色),已抽成同源外链 boot.js;ACP SDK 依赖的 zod v4 会在构造 schema 时探测
    new Function 以决定是否 JIT 编译校验器,这条探测同样会报 script-src 违规,现显式关掉
    JIT(校验行为不变)。两处都是「功能上会降级、但控制台与 CSP 报表里始终挂着假警报」的形态。