chore: directory migration - gateway→server, web→client/electron

This commit is contained in:
2026-09-08 19:16:35 +08:00
parent fd9f99a3f9
commit f9d757b5e5
243 changed files with 5095 additions and 228 deletions

View File

@ -110,7 +110,7 @@
以下 DDL PostgreSQL 方言书写SQLite 侧结构与语义完全一致仅方言不同
`UUID``TEXT``TIMESTAMPTZ``DATETIME``JSONB``TEXT``VARCHAR(n)``TEXT`
`gateway/internal/db/migrations/init_sqlite.sql`
`server/internal/db/migrations/init_sqlite.sql`
### 3.1 agents 表

View File

@ -105,10 +105,10 @@ SSE 连接的 handler 在首次连接时从缓冲区头部开始(客户端传
**修法**同上sessionrate.go 重写为调用 RateLimitCheckAndRecord
### P2 — 组件级测试
**现状**有结构性回归`web/test/narrow-layout.test.mjs`28 条断言
**现状**有结构性回归`client/electron/test/narrow-layout.test.mjs`28 条断言
读源码验形态与一套 playwright 手工脚本**没有渲染组件跑断言的测试**。
**范围**关键组件AddressInput 补全PermissionPanel 决策WorkCard 预算渲染)。
**已有的浏览器实测**`web/test/manual/``npm run test:narrow` /
**已有的浏览器实测**`client/electron/test/manual/``npm run test:narrow` /
`npm run test:wide`)—— 连本机共享 Chromium 量真实盒子与命中区
不在 `npm test` 因为要一个跑着的浏览器加一个活的 Gateway
剩下的是把它接进 CI需要一个 headless 环境与一个测试用 Gateway 实例)。
@ -176,7 +176,7 @@ playwright 端到端 18 项全通过(含「底部导航六项都命中自己
工具按钮命中区 >= 44px」、五个页面各有纵向滚动容器、无横向溢出、
320px 无横向溢出);宽屏回归 5 项全通过(三栏并排、常驻侧栏仍有退出登录、
无返回按钮、`.tap` 伪元素在宽屏不生效、无溢出)。
`web/test/narrow-layout.test.mjs` 从 20 条扩到 37 条,把上述每一条都钉住。
`client/electron/test/narrow-layout.test.mjs` 从 20 条扩到 37 条,把上述每一条都钉住。
滚动检查的判据是「**有**滚动容器」而不是「当前正在滚动」:
内容暂时不够高时后者为假,但页面是健康的 —— 真正的 bug 是根本没有那一层。
@ -342,7 +342,7 @@ T 前的 `M`(月)与正号 trigger 一律忽略而不是乱换算)。
**TRIGGER duration 全形态**/**导出导入往返**/**默认模板必须是变量形式**
- `internal/scheduler/calendar_test.go`:模板渲染 7 例(含**本地时区**与
**同一时刻不同 Location 渲染一致**
- `web/test/components/calendar.test.tsx` 35 例
- `client/electron/test/components/calendar.test.tsx` 35 例
- 生产端到端:建事件 → 30 秒内触发 → 邮件入库、正文变量已替换成本地时间 →
重复事件推进到次日 → **连续两个 tick 零重发**(修复前每 tick 一封)→
iCal 导入 `-PT45M`/`FREQ=WEEKLY` 正确落库 → 导出再导入闭环一致 →

View File

@ -22,12 +22,12 @@ Phase 6 (Week 6+) 进阶功能 — 对话树 / 转发 / 配额 / DSH
### 1.1 项目初始化 ✅
- [x] 创建项目目录 `gateway/`
- [x] 创建项目目录 `server/`
- [x] `go mod init github.com/agentmail/gateway`
- [x] 目录结构规划:
```
gateway/
server/
├── cmd/
│ └── server/
│ └── main.go # 入口,启动 HTTP 服务
@ -213,7 +213,7 @@ GET /api/v1/events/status → SSE 连接状态
- [x] 两份 schema`migrations/init.sql`PG`migrations/init_sqlite.sql`
- [x] `DATABASE_URL` 识别 `postgres://``sqlite://``file:`、裸 `.db` 路径;空值 = 内置 SQLite
- [x] SQLite 连接开 `WAL` + `busy_timeout=5000` + `foreign_keys=ON`,连接池限 1单写者
- [x] 删除 `docker-compose.yml` 与重复的 `gateway/migrations/`
- [x] 删除 `docker-compose.yml` 与重复的 `server/migrations/`
### 1.9 部署systemd 单元
@ -305,12 +305,12 @@ Agent 不需要手动调工具。opencode 的插件契约没有提供拦截这
### 3.1 项目初始化
- [x] `web/` 目录Vite + React + TypeScript + TailwindCSS
- [x] `client/electron/` 目录Vite + React + TypeScript + TailwindCSS
### 3.2 目录结构(已落地)
```
web/src/
client/electron/src/
├── api/ client.tsHTTP/ sse.tsEventSource + 指数退避重连)
├── stores/ mail / session / contact / ui / authZustand
├── components/ Sidebar / MailList / MailView / ComposePage / ContactPanel
@ -743,7 +743,7 @@ execute: {
缺头回 `Missing X-Agent-Name or X-Agent-Secret header`
- [x] 畸形三维地址 name to别名不存在均有明确错误文案
- [x] 邮件内容 XSS 防护react-markdown 默认不解析 raw HTML 且清空非 http(s)/mailto 协议 URL
新增回归测试 `web/test/markdown-xss.test.mjs``npm test`守住这两个前提
新增回归测试 `client/electron/test/markdown-xss.test.mjs``npm test`守住这两个前提
防止日后为支持 HTML 邮件加上 rehype-raw 而无声开口子
- [x] 并发安全`sse.Manager` `sync.RWMutex``go build -race` 通过
实测 10 客户端并发连接断开后计数归零+ 20 封并发发信全部成功SQLite WAL + busy_timeout
@ -1018,7 +1018,7 @@ execute: {
**只有这两个端点接受 query 令牌**其余一律 401 —— URL 里的令牌会进访问日志与 Referer
附件下载因此单独挂在 `UserAuthAllowQueryToken` 中间件下
- [x] CORS 暴露 `Content-Disposition` `Content-Length`前者是取文件名所必需
- [x] `web/src/api/config.ts` 集中基地址与令牌
- [x] `client/electron/src/api/config.ts` 集中基地址与令牌
`VITE_API_BASE` 构建期注入`window.__AGENTMAIL_API_BASE__` 运行时覆盖`setToken()` 切凭证
业务代码不感知 Cookie 与密钥的差异`src/api/` 可整体抽成 SDK
- [x] `docs/API.md`完整接口清单三类调用者的认证边界错误码约定
@ -1343,7 +1343,7 @@ MVP 计划Phase 1-6已全部落地并在 systemd 部署态实测通过。
只滑走覆盖层的话下次进列表又会弹回来
- [x] `narrowPane` 在宽屏下**也维护**否则从窄屏拖宽再拖回来
用户会发现自己回到了列表刚打开的邮件不见了
- [x] `web/test/narrow-layout.test.mjs`结构性断言 28
- [x] `client/electron/test/narrow-layout.test.mjs`结构性断言 28
钉住覆盖而非分栏」「延迟卸载」「双层 rAF」「条件渲染而非 md:hidden
无裸 px-6等不变量不做像素级视觉快照 —— 字体差异下极脆
@ -1388,7 +1388,7 @@ MVP 计划Phase 1-6已全部落地并在 systemd 部署态实测通过。
空间不足时自动退化为 0于是矮屏变成正常的顶对齐可滚布局
而高屏仍然垂直居中390x844 1280x800 实测 centered=true
验证`web/test/manual/``npm run test:narrow` / `test:wide`
验证`client/electron/test/manual/``npm run test:narrow` / `test:wide`
窄屏 18 + 宽屏 5 项全通过结构性断言从 20 条扩到 37
把每一条修复都钉住
@ -1589,7 +1589,7 @@ plan/workspace/full **一一对应** —— 不是巧合,是同一个问题的
- 前端有 Markdown XSS 与窄屏结构性回归,但没有**渲染组件跑断言**的测试
- 深色主题未做
- 窄屏实测脚本已入库(`web/test/manual/`),但没进 CI ——
- 窄屏实测脚本已入库(`client/electron/test/manual/`),但没进 CI ——
要一个 headless 环境加一个测试用 Gateway 实例
- 登录限速与新建会话限速已改为 DB 事务rate_limits 表),多实例部署不再各自计数
- SQLite 抄送查询走 `json_each` 全表展开,无索引;单机量级下够用,
@ -1680,6 +1680,67 @@ $ sqlite3 "SELECT COUNT(*) FROM attachments WHERE mail_id='2a64fdc8…'"
- [ ] C`GET /mail/{id}` 返回 `from_human` / `to_human``addr-verify` 两项转绿
- [ ] D契约文档有 `B-5.6` 条款demo 按 `from_human` 决定是否回信
### 7.14 DSH 主动询问邮件桥接(严重阻塞缺口)
本轮生产问题DSH 邮件会话调用 `ask_user_question` 后一直停在询问状态AgentMail
里没有任何待处理邮件。根因不是 SSE 或 Gateway 丢信,而是桥只监听了
`approval/request`(危险工具的审批 seam没有接管 `ask_user_question` 使用的
`ctx.userQuestions` seam。DSH Web UI 是后者唯一 provider邮件驱动会话没有人在 DSH
页面作答,所以工具 Promise 永远不返回。
修复边界:
1. 在 DSH 的 `tools/execute` around-dispatch 中只拦截**邮件驱动会话**的
`ask_user_question`,不替换全局 `userQuestions` provider该服务只允许一个 provider
强行注册会与 Web UI 冲突)。
2. 每个问题按原始 `id/question/options/multi_select` 生成一封 Gateway 待处理邮件;
单选保留选项原文,自由文本与多选用备注输入承载。多问题按顺序询问,全部回答后
还原 DSH 要求的 `{answers:[{id,selected,custom?}]}` 工具结果。
3. Gateway 的权限请求端点增加 `kind=question`:复用现有决策人追溯、幂等 relay_key、
待办列表与 SSE 回传,但**不套权限档位判定**plan/full 也可能需要补充信息)。
`kind=permission` 保持原行为。
4. 待决映射必须在发 HTTP 请求前登记堵住“人快速作答、SSE 先于 map 写入”的竞态;
abort、插件卸载、永久 HTTP 失败均 fail closed不留下悬挂 Promise。
5. 前端把询问类待办显示为“等待回答”,自由文本/多选回答未填写时禁止提交;仍复用
现有待处理入口,避免再造第二套不可见队列。
验收:
- [ ] 单选询问经邮件选择后DSH 工具拿到原始选项标签并继续运行
- [ ] 无选项询问要求填写文本,空回答不能提交
- [ ] 多选询问可提交多个原始标签,未知标签不被伪造为有效选择
- [ ] 两个问题按顺序往返,最终答案数组保持原始 question id 与顺序
- [ ] plan/workspace/full 三档中的主动询问均可送达;危险工具审批仍只在 workspace 产生
- [ ] 非邮件驱动 DSH 会话继续使用 Web UI provider不受桥影响
### 7.13 桥进程异常上报与可靠重试
本轮起因:桥进程意外终止只留在 journal用户邮箱没有任何提示同时“自动重连”
与“处理中任务重试”被混为一谈。现状其实已有两层systemd 对进程做指数退避重启,
四桥 SSE 断线后自动重连;但它们不能回答“刚才正在处理的那封邮件怎么办”。
按三层修复,职责不能混:
1. **进程层由 systemd 自愈**:保留 `Restart=` + `RestartSteps=6` +
`RestartMaxDelaySec=5min`。插件不在进程内部造第二个 supervisor。
2. **异常退出必须发邮件**:共用 `deploy/service-failure-notify.mjs` 挂到
`ExecStopPost`,读取 systemd 的 `SERVICE_RESULT/EXIT_CODE/EXIT_STATUS`;正常停止不报,
exit-code/signal/oom-kill/timeout/watchdog 才报。Gateway 暂不可达时落本地 spool
下次 `ExecStartPost --flush` 补发。通知用稳定 `relay_key` 幂等,避免即时发送与补发重复。
3. **处理中任务必须重试**pi worker 无 `done` 就按 1s/2s 有界重投,最多 3 次;
`done(ok=false)` 属于已经给出明确失败结论,不盲目重跑。重试耗尽后给原发件人一封
故障邮件。整进程重启后仍由未读补拉兜底homeagent 已有落盘 ledger保留现状。
4. **网络层继续重连**SSE 35 秒重连、心跳下一周期再试,不把短暂网络抖动升级成
进程重启。
验收:
- [ ] notifier dry-run 能区分正常停止与异常退出,敏感密钥不进入正文/日志
- [ ] Gateway 暂不可达时报告落盘,`--flush` 后只发一次
- [ ] pi worker 首次异常退出后自动重投,同一会话仍串行
- [ ] 连续三次异常后停止重试并调用失败回报,不形成无限崩溃循环
- [ ] 四个桥宿主服务保留指数退避重启;正常 `systemctl restart` 不产生误报警邮件
---
## 文件清单(完整)
@ -1689,7 +1750,7 @@ agentmail/
├── docs/
│ ├── MVP-SPEC.md # MVP 技术规格书
│ └── PLAN.md # 本文件
├── gateway/
├── server/
│ ├── cmd/server/main.go # 入口 + 路由编排
│ ├── internal/
│ │ ├── config/config.go
@ -1730,7 +1791,7 @@ agentmail/
│ └── opencode-mail-bridge/ # 第一个接入平台
│ ├── package.json
│ └── index.js # 凭证 + HTTP + SSE + 四个工具 + event 钩子
├── web/
├── client/electron/
│ ├── src/
│ │ ├── api/
│ │ │ ├── client.ts

View File

@ -1665,7 +1665,7 @@ socat 被 `Requires` 带停后再没起来。加 `PartOf` 后又发现 `start`
### 9.14 改名提议的标记拼错是**静默失败**
`<!-- agentmail:rename-session alias="x" reason="y" -->` 由服务端正则解析
`gateway/internal/handler/rename_proposal.go`)。少个空格、把双引号写成单引号、
`server/internal/handler/rename_proposal.go`)。少个空格、把双引号写成单引号、
或把 `reason=""` 写成空属性 —— 邮件照常发出,提议凭空消失,而模型以为自己提过了,
在后续对话里当作已完成的事引用。