chore: directory migration - gateway→server, web→client/electron
This commit is contained in:
@ -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 表
|
||||
|
||||
|
||||
@ -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` 正确落库 → 导出再导入闭环一致 →
|
||||
|
||||
85
docs/PLAN.md
85
docs/PLAN.md
@ -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.ts(HTTP)/ sse.ts(EventSource + 指数退避重连)
|
||||
├── stores/ mail / session / contact / ui / auth(Zustand)
|
||||
├── 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 3–5 秒重连、心跳下一周期再试,不把短暂网络抖动升级成
|
||||
进程重启。
|
||||
|
||||
验收:
|
||||
|
||||
- [ ] 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
|
||||
|
||||
@ -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=""` 写成空属性 —— 邮件照常发出,提议凭空消失,而模型以为自己提过了,
|
||||
在后续对话里当作已完成的事引用。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user