Files
ai_site/platform/internal/logic/applogic/agents.go
whm 43dc708f1e feat: Z38 grant Yuheng agents ready-to-use access
Complete permissions during every bind and token path, expose effective access in the console, and keep the platform return path globally available.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 16:11:13 +08:00

293 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package applogic
import (
"context"
"fmt"
"strings"
"aijianzhan/platform/internal/agentcap"
"aijianzhan/platform/internal/agentstore"
"aijianzhan/platform/internal/authx"
"aijianzhan/platform/internal/svc"
"aijianzhan/platform/internal/tenantperm"
"aijianzhan/platform/internal/types"
)
type AgentAdminLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewAgentAdminLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AgentAdminLogic {
return &AgentAdminLogic{ctx: ctx, svcCtx: svcCtx}
}
func (l *AgentAdminLogic) store() (agentstore.Store, error) {
if l.svcCtx.Agents == nil {
return nil, fmt.Errorf("agent store unavailable")
}
return l.svcCtx.Agents, nil
}
func (l *AgentAdminLogic) List() ([]agentstore.Account, error) {
st, err := l.store()
if err != nil {
return nil, err
}
items, err := st.List(l.ctx, authx.TenantID(l.ctx))
if err != nil {
return nil, err
}
for i := range items {
l.attachRole(&items[i])
}
return items, nil
}
func (l *AgentAdminLogic) Get(agentID int64) (*agentstore.Account, error) {
st, err := l.store()
if err != nil {
return nil, err
}
acc, err := st.Get(l.ctx, authx.TenantID(l.ctx), agentID)
if err != nil {
return nil, err
}
l.attachRole(acc)
return acc, nil
}
func (l *AgentAdminLogic) attachRole(acc *agentstore.Account) {
if acc == nil || acc.RoleID <= 0 || l.svcCtx.Roles == nil {
return
}
role, err := l.svcCtx.Roles.Get(l.ctx, authx.TenantID(l.ctx), acc.RoleID)
if err != nil {
return
}
acc.RoleCode = role.Code
acc.RoleName = role.Name
}
func (l *AgentAdminLogic) resolvePerms(roleID int64, fallback []string) (int64, []string, error) {
if roleID <= 0 {
return 0, authx.NormalizePerms(fallback), nil
}
if l.svcCtx.Roles == nil {
return 0, nil, fmt.Errorf("role store unavailable")
}
role, err := l.svcCtx.Roles.Get(l.ctx, authx.TenantID(l.ctx), roleID)
if err != nil {
return 0, nil, err
}
return role.RoleID, authx.NormalizePerms(append([]string{}, role.Permissions...)), nil
}
func (l *AgentAdminLogic) Create(req *types.AgentCreateReq) (*types.AgentCreateResp, error) {
st, err := l.store()
if err != nil {
return nil, err
}
roleID, perms, err := l.resolvePerms(req.RoleID, req.Permissions)
if err != nil {
return nil, err
}
if roleID <= 0 && len(perms) == 0 {
return nil, fmt.Errorf("role_id or permissions required")
}
if err := tenantperm.MustAllow(l.ctx, l.svcCtx.TenantPerm, authx.TenantID(l.ctx), perms); err != nil {
return nil, err
}
acc, secret, err := st.Create(l.ctx, authx.TenantID(l.ctx), authx.UserID(l.ctx), agentstore.CreateInput{
Name: req.Name,
Perms: perms,
AppSlugs: req.AppSlugs,
Status: agentstore.StatusActive,
RoleID: roleID,
ChannelID: req.ChannelID,
OnlineDBID: req.OnlineDBID,
DatabaseName: req.DatabaseName,
})
if err != nil {
return nil, err
}
if acc2, err := l.ensureAgentSyncBind(acc); err == nil && acc2 != nil {
acc = acc2
} else if err != nil {
// 自动绑通道失败不阻断创建,但返回提示
l.attachRole(acc)
return &types.AgentCreateResp{Account: *acc, ClientSecret: secret}, nil
}
l.attachRole(acc)
return &types.AgentCreateResp{Account: *acc, ClientSecret: secret}, nil
}
func (l *AuthLogic) SelfRegisterAgent(req *types.AgentSelfRegisterReq) (*types.AgentSelfRegisterResp, error) {
if l.svcCtx.Agents == nil {
return nil, fmt.Errorf("agent store unavailable")
}
want := l.svcCtx.Config.Agent.RegisterSecret
if want == "" {
want = l.svcCtx.Config.Auth.IssueSecret
}
if want == "" {
want = l.svcCtx.JWT.AccessSecret
}
if req.RegisterSecret == "" || req.RegisterSecret != want {
return nil, fmt.Errorf("invalid register secret")
}
tenantID := req.TenantID
if tenantID <= 0 {
tenantID = 1
}
acc, secret, reused, err := l.svcCtx.Agents.Register(l.ctx, tenantID, req.Name, req.HostKey)
if err != nil {
return nil, err
}
msg := "已登记为 pending请管理员在控制台分配角色并启用后再换票"
if reused {
syncBound := strings.TrimSpace(acc.ChannelID) != "" && strings.TrimSpace(acc.OnlineDBID) != ""
if acc.Status == agentstore.StatusActive && syncBound {
msg = "已存在同步绑定账号,已轮换 client_secret请直接用返回的 client_id/client_secret 换票,勿再走绑定表单"
} else {
msg = "已存在 pending 登记,已轮换 client_secret仍须管理员分配角色并启用"
}
}
return &types.AgentSelfRegisterResp{
Account: *acc,
ClientSecret: secret,
Reused: reused,
Message: msg,
}, nil
}
func (l *AgentAdminLogic) Update(agentID int64, req *types.AgentUpdateReq) (*agentstore.Account, error) {
st, err := l.store()
if err != nil {
return nil, err
}
in := agentstore.UpdateInput{}
if req.Name != nil {
in.Name = req.Name
}
if req.Status != nil {
in.Status = req.Status
}
if req.AppSlugs != nil {
in.AppSlugs = req.AppSlugs
}
if req.ChannelID != nil {
in.ChannelID = req.ChannelID
}
if req.OnlineDBID != nil {
in.OnlineDBID = req.OnlineDBID
}
if req.DatabaseName != nil {
in.DatabaseName = req.DatabaseName
}
if req.RoleID != nil {
roleID, perms, err := l.resolvePerms(*req.RoleID, nil)
if err != nil {
return nil, err
}
if err := tenantperm.MustAllow(l.ctx, l.svcCtx.TenantPerm, authx.TenantID(l.ctx), perms); err != nil {
return nil, err
}
in.RoleID = &roleID
in.Perms = &perms
} else if req.Permissions != nil {
n := authx.NormalizePerms(*req.Permissions)
if err := tenantperm.MustAllow(l.ctx, l.svcCtx.TenantPerm, authx.TenantID(l.ctx), n); err != nil {
return nil, err
}
in.Perms = &n
}
acc, err := st.Update(l.ctx, authx.TenantID(l.ctx), agentID, in)
if err != nil {
return nil, err
}
// Z12c启用为 active 且无通道时自动绑默认同步通道
if acc.Status == agentstore.StatusActive {
if acc2, err := l.ensureAgentSyncBind(acc); err == nil && acc2 != nil {
acc = acc2
}
}
l.attachRole(acc)
return acc, nil
}
func (l *AgentAdminLogic) Rotate(agentID int64) (*types.AgentSecretResp, error) {
st, err := l.store()
if err != nil {
return nil, err
}
secret, err := st.RotateSecret(l.ctx, authx.TenantID(l.ctx), agentID)
if err != nil {
return nil, err
}
acc, err := st.Get(l.ctx, authx.TenantID(l.ctx), agentID)
if err != nil {
return nil, err
}
return &types.AgentSecretResp{ClientID: acc.ClientID, ClientSecret: secret}, nil
}
func (l *AgentAdminLogic) Delete(agentID int64) error {
st, err := l.store()
if err != nil {
return err
}
return st.Delete(l.ctx, authx.TenantID(l.ctx), agentID)
}
func (l *AuthLogic) IssueClientCredentials(clientID, clientSecret string) (*types.TokenResp, error) {
if l.svcCtx.Agents == nil {
return nil, fmt.Errorf("agent store unavailable")
}
acc, err := l.svcCtx.Agents.Authenticate(l.ctx, strings.TrimSpace(clientID), clientSecret)
if err != nil {
return nil, err
}
if l.svcCtx.DBSync != nil {
if _, hErr := HealTenantSyncBind(l.ctx, l.svcCtx, acc.TenantID); hErr == nil {
if refreshed, gErr := l.svcCtx.Agents.Get(l.ctx, acc.TenantID, acc.AgentID); gErr == nil && refreshed != nil {
acc = refreshed
}
}
}
// Z38宇恒已绑定账号用 client_credentials 换票时自动修复存量缺权。
if strings.TrimSpace(acc.HostKey) != "" &&
strings.TrimSpace(acc.ChannelID) != "" &&
strings.TrimSpace(acc.OnlineDBID) != "" {
acc, err = l.ensureYuhengAgentPerms(acc)
if err != nil {
return nil, err
}
}
token, exp, err := authx.IssueAgentToken(l.svcCtx.JWT, acc.TenantID, acc.AgentID, acc.Perms)
if err != nil {
return nil, err
}
_ = l.svcCtx.Agents.TouchToken(l.ctx, acc.AgentID)
secret := l.svcCtx.Config.Agent.CapsuleSecret
if secret == "" {
secret = l.svcCtx.JWT.AccessSecret
}
resp := &types.TokenResp{
AccessToken: token,
TokenType: "Bearer",
ExpiresAt: exp,
TenantID: acc.TenantID,
UserID: acc.AgentID,
Username: acc.ClientID,
DisplayName: acc.Name,
Role: authx.RoleAgent,
AgentKey: agentcap.PublicAgentKey(secret, acc.TenantID, acc.AgentID),
AgentID: acc.AgentID,
Permissions: append([]string{}, acc.Perms...),
AppSlugs: append([]string{}, acc.AppSlugs...),
}
fillAgentSyncOnToken(resp, acc)
return resp, nil
}