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

@ -0,0 +1,87 @@
package repo
import (
"context"
"errors"
"github.com/agentmail/gateway/internal/db"
"github.com/google/uuid"
)
// ---------- 插件自动转发(免配额通道) ----------
//
// **基本原则:配额约束的是模型的自主发信,不是 harness 的转发。**
//
// 配额存在的意义是防止 Agent 无限自我循环。而插件代劳搬运的两类消息不属于此列:
// 1. 平台原生的权限询问(opencode 的 permission.updated)—— 不转给人,人就看不到,
// Agent 卡在那里等一个永远不会来的回答
// 2. 本轮的最终总结(session.idle 时最后一条 assistant 消息)—— 模型已经把话说完了,
// 插件只是把它搬到邮件里;对它收费会导致配额用尽时 Agent 连交代都做不了
//
// 防滥用不靠计数,靠**幂等键**:relay_key 是上游那条消息的稳定标识
// (permission id / assistant message id)。唯一约束让同一条上游消息只能转一次,
// 于是插件重试与 SSE 重放不会产生第二封,想多转就得拿出不同的上游消息 id ——
// 而那些 id 由平台生成,模型伪造不出来。
// ErrRelayDuplicate 表示这条上游消息已经转发过了。
var ErrRelayDuplicate = errors.New("relay already recorded")
// ClaimRelay 占用一次免配额转发名额。
//
// 判断与占用在同一条 INSERT 里(靠主键唯一约束),并发重试下只有一个能成功 ——
// 分成「先查有没有、再插入」两步的话,插件的两次重试会双双通过检查各插一条。
//
// 返回 ErrRelayDuplicate 表示重复,调用方应当据此跳过发信而不是报错:
// 重复转发是插件重试的正常结果,不是故障。
func ClaimRelay(ctx context.Context, agentName, relayKey, kind string) error {
_, err := db.DB.ExecContext(ctx,
`INSERT INTO relayed_mails (agent_name, relay_key, kind) VALUES ($1, $2, $3)`,
agentName, relayKey, kind)
if err != nil {
if db.IsUniqueViolation(err) {
return ErrRelayDuplicate
}
return err
}
return nil
}
// BindRelayMail 把已占用的名额关联到真正发出的邮件,便于事后审计
// 「这封免配额的信是从哪条上游消息来的」。
//
// 关联失败不该让发信失败:邮件已经入库,缺一条审计关联不影响功能。
func BindRelayMail(ctx context.Context, agentName, relayKey string, mailID uuid.UUID) error {
_, err := db.DB.ExecContext(ctx,
`UPDATE relayed_mails SET mail_id = $1 WHERE agent_name = $2 AND relay_key = $3`,
mailID, agentName, relayKey)
return err
}
// ReleaseRelay 撤销名额占用。
//
// 占用成功但发信失败时必须还回去,否则那条上游消息永远转不出来了 ——
// 幂等键会一直认为它已经转过。
func ReleaseRelay(ctx context.Context, agentName, relayKey string) error {
_, err := db.DB.ExecContext(ctx,
`DELETE FROM relayed_mails WHERE agent_name = $1 AND relay_key = $2 AND mail_id IS NULL`,
agentName, relayKey)
return err
}
// RelayKeyForMail 反查某封邮件对应的上游消息 id。
//
// 人类决策一条权限请求后,插件需要知道该回复 opencode 的哪个 permission ——
// 光有 AgentMail 的 mail_id 是不够的,两边的 id 空间不同。
// 插件重启后内存映射会丢,所以这个映射必须在服务端持久化。
//
// 无记录时返回空串(例如旧数据,或压根没走 relay 通道的请求)。
func RelayKeyForMail(ctx context.Context, mailID uuid.UUID) (string, string) {
var key, kind string
err := db.DB.QueryRowContext(ctx,
`SELECT relay_key, kind FROM relayed_mails WHERE mail_id = $1 LIMIT 1`,
mailID).Scan(&key, &kind)
if err != nil {
return "", ""
}
return key, kind
}