feat: ship loose-offline dbsync (validate, agent push, LWW audit)
Add UUID/FK channel checks, agent whitelist/push APIs, bindings, super-admin LWW audit with rollback, reconcile rate limits, and sync docs. Default customers stay opt-in; company conflict UI is removed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
119
platform/internal/handler/agent_sync.go
Normal file
119
platform/internal/handler/agent_sync.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/dbsync"
|
||||
"aijianzhan/platform/internal/svc"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
"github.com/zeromicro/go-zero/rest/pathvar"
|
||||
)
|
||||
|
||||
// agent 同步只读白名单 + 推远程 A(形态 B);需 JWT 含「数据同步」权限(人类管理员或智能体均可)。
|
||||
|
||||
func agentSyncWhitelistHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !requireDBSync(svcCtx, w) {
|
||||
return
|
||||
}
|
||||
ch, err := svcCtx.DBSync.Store().GetChannelForTenant(pathvar.Vars(r)["id"], syncTenantID(r))
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
tables := uniqueStringSlice(ch.Local.Tables, ch.Remote.Tables)
|
||||
httpx.OkJson(w, map[string]any{
|
||||
"channel_id": ch.ID,
|
||||
"name": ch.Name,
|
||||
"enabled": ch.Enabled,
|
||||
"direction": ch.Direction,
|
||||
"conflict_policy": ch.ConflictPolicy,
|
||||
"tables": tables,
|
||||
"pk_columns": ch.PKColumns,
|
||||
"hint": "本机 agent 缓存此表白名单;仅白名单表走 local_dbsync",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func agentSyncPushHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !requireDBSync(svcCtx, w) {
|
||||
return
|
||||
}
|
||||
ch, err := svcCtx.DBSync.Store().GetChannelForTenant(pathvar.Vars(r)["id"], syncTenantID(r))
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
var item dbsync.PushItem
|
||||
if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
res, err := dbsync.PushToRemote(r.Context(), ch, svcCtx.DBSync.Store(), item)
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{"success": true, "result": res})
|
||||
}
|
||||
}
|
||||
|
||||
func agentSyncPushBatchHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !requireDBSync(svcCtx, w) {
|
||||
return
|
||||
}
|
||||
ch, err := svcCtx.DBSync.Store().GetChannelForTenant(pathvar.Vars(r)["id"], syncTenantID(r))
|
||||
if err != nil {
|
||||
authx.WriteError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Items []dbsync.PushItem `json:"items"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if len(body.Items) == 0 {
|
||||
authx.WriteError(w, http.StatusBadRequest, "items required")
|
||||
return
|
||||
}
|
||||
if len(body.Items) > 100 {
|
||||
authx.WriteError(w, http.StatusBadRequest, "items limit 100")
|
||||
return
|
||||
}
|
||||
results, err := dbsync.PushBatchToRemote(r.Context(), ch, svcCtx.DBSync.Store(), body.Items)
|
||||
if err != nil {
|
||||
httpx.OkJson(w, map[string]any{
|
||||
"success": false,
|
||||
"error": err.Error(),
|
||||
"results": results,
|
||||
})
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, map[string]any{"success": true, "results": results})
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueStringSlice(a, b []string) []string {
|
||||
seen := map[string]struct{}{}
|
||||
var out []string
|
||||
for _, xs := range [][]string{a, b} {
|
||||
for _, s := range xs {
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[s]; ok {
|
||||
continue
|
||||
}
|
||||
seen[s] = struct{}{}
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user