Files
ai_site/platform/internal/logic/applogic/yuheng_ticket.go
whm cb76824e94 fix: start system default sync channels by default
Create and restart paths enable IsSystemDefault channels; SyncPage auto-starts after save.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 17:50:27 +08:00

211 lines
6.5 KiB
Go
Raw Permalink 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 (
"fmt"
"strings"
"aijianzhan/platform/internal/agentcap"
"aijianzhan/platform/internal/agentstore"
"aijianzhan/platform/internal/authx"
"aijianzhan/platform/internal/dbsync"
"aijianzhan/platform/internal/types"
"aijianzhan/platform/internal/userstore"
"aijianzhan/platform/internal/yuhticket"
)
type YuhengTicketExchangeReq struct {
Ticket string `json:"ticket"`
HostKey string `json:"host_key"` // 须与票内一致
LocalDatabaseID string `json:"local_database_id,omitempty"`
Name string `json:"name,omitempty"`
}
type YuhengTicketExchangeResp struct {
*types.TokenResp
OK bool `json:"ok"`
ClientID string `json:"client_id,omitempty"`
ClientSecret string `json:"client_secret,omitempty"` // 仅新注册/轮换时返回一次
Message string `json:"message,omitempty"`
}
// ExchangeYuhengTicket 宇恒专属:验签凭票 → 绑定落点 → 签发智能体 JWT免智建账号密码登录
func (l *AuthLogic) ExchangeYuhengTicket(req YuhengTicketExchangeReq) (*YuhengTicketExchangeResp, error) {
cfg := l.svcCtx.Config.Agent.YuhengTicket
if !cfg.Enabled {
return nil, fmt.Errorf("宇恒凭票未启用Agent.YuhengTicket.Enabled")
}
if strings.TrimSpace(cfg.Secret) == "" {
return nil, fmt.Errorf("宇恒凭票 Secret 未配置")
}
if l.svcCtx.Users == nil || l.svcCtx.Agents == nil || l.svcCtx.DBSync == nil {
return nil, fmt.Errorf("bind service unavailable")
}
claims, err := yuhticket.Verify(req.Ticket, yuhticket.VerifyOpts{
Secret: cfg.Secret,
Issuer: cfg.Issuer,
Audience: cfg.Audience,
})
if err != nil {
return nil, err
}
hostKey := strings.TrimSpace(req.HostKey)
if hostKey == "" {
hostKey = strings.TrimSpace(claims.HostKey)
}
if hostKey == "" || hostKey != strings.TrimSpace(claims.HostKey) {
return nil, fmt.Errorf("host_key 与凭票不一致")
}
if l.svcCtx.YuhengJTI != nil {
if err := l.svcCtx.YuhengJTI.Consume(claims.JTI, claims.Exp); err != nil {
return nil, err
}
}
phone, err := userstore.NormalizePhone(claims.Phone)
if err != nil {
return nil, fmt.Errorf("凭票手机号无效")
}
u, err := l.svcCtx.Users.GetByPhone(l.ctx, phone)
if err != nil || u == nil {
return nil, fmt.Errorf("无此成员;请先在智建绑定手机或使用绑定码")
}
if u.TenantID <= 0 {
return nil, fmt.Errorf("该账号尚未加入公司")
}
name := strings.TrimSpace(req.Name)
if name == "" {
name = strings.TrimSpace(claims.Name)
}
localID := strings.TrimSpace(req.LocalDatabaseID)
if localID == "" {
localID = strings.TrimSpace(claims.LocalDatabaseID)
}
acc, secret, err := l.bindUserHostSync(u, hostKey, name, localID, "yuheng-ticket")
if err != nil {
return nil, err
}
if err := l.ensureAgentSyncPerm(acc); err != nil {
return nil, err
}
// 重新加载 perms
acc, err = l.svcCtx.Agents.Get(l.ctx, acc.TenantID, acc.AgentID)
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)
capSecret := l.svcCtx.Config.Agent.CapsuleSecret
if capSecret == "" {
capSecret = l.svcCtx.JWT.AccessSecret
}
tr := &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(capSecret, acc.TenantID, acc.AgentID),
AgentID: acc.AgentID,
Permissions: append([]string{}, acc.Perms...),
AppSlugs: append([]string{}, acc.AppSlugs...),
}
fillAgentSyncOnToken(tr, acc)
_ = l.writeBindAudit("yuheng_ticket_exchange", acc.TenantID, acc.AgentID, map[string]any{
"phone": phone, "jti": claims.JTI, "yuheng_user_id": claims.YuhengUserID,
})
return &YuhengTicketExchangeResp{
TokenResp: tr,
OK: true,
ClientID: acc.ClientID,
ClientSecret: secret,
Message: "凭票换票成功(仅宇恒)",
}, nil
}
func (l *AuthLogic) ensureAgentSyncPerm(acc *agentstore.Account) error {
if acc == nil {
return nil
}
has := false
for _, p := range acc.Perms {
if p == authx.Perm数据同步 {
has = true
break
}
}
if has && acc.Status == agentstore.StatusActive {
return nil
}
perms := append([]string{}, acc.Perms...)
if !has {
perms = append(perms, authx.Perm数据同步)
}
st := agentstore.StatusActive
_, err := l.svcCtx.Agents.Update(l.ctx, acc.TenantID, acc.AgentID, agentstore.UpdateInput{
Status: &st,
Perms: &perms,
})
return err
}
// bindUserHostSync 将 host_key 智能体挂到用户公司默认同步落点并写 Binding。
func (l *AuthLogic) bindUserHostSync(u *userstore.User, hostKey, name, localID, note string) (*agentstore.Account, string, error) {
cfg := l.svcCtx.Config.DBSync
driver := dbsync.Driver(strings.TrimSpace(cfg.DefaultRemoteDriver))
if driver == "" {
driver = dbsync.DriverPostgres
}
ch, err := l.svcCtx.DBSync.EnsureAndStartSystemDefaultChannel(dbsync.DefaultChannelOpts{
TenantID: u.TenantID,
RemoteDriver: driver,
RemoteDSN: strings.TrimSpace(cfg.DefaultRemoteDSN),
})
if err != nil {
return nil, "", fmt.Errorf("ensure channel: %w", err)
}
var secret string
acc, err := l.svcCtx.Agents.FindByHostKey(l.ctx, hostKey)
if err != nil {
if name == "" {
name = "离线终端 · " + maskDisplayName(u.DisplayName)
}
created, sec, _, regErr := l.svcCtx.Agents.Register(l.ctx, u.TenantID, name, hostKey)
if regErr != nil {
return nil, "", regErr
}
acc, secret = created, sec
} else if acc.TenantID != u.TenantID && acc.Status == agentstore.StatusPending {
// 允许 pending 迁公司;已激活异租户拒绝
} else if acc.TenantID != u.TenantID {
return nil, "", fmt.Errorf("host_key 已绑定其它公司")
}
online := fmt.Sprintf("%s_u%d", dbsync.ResolveOnlineDBID("", ch.ID), u.UserID)
dbName := strings.TrimSpace(u.DisplayName)
if dbName == "" {
dbName = fmt.Sprintf("user_%d", u.UserID)
}
updated, err := l.svcCtx.Agents.AttachSyncBind(l.ctx, acc.AgentID, u.TenantID, ch.ID, online, dbName, true)
if err != nil {
return nil, "", err
}
if localID == "" {
localID = "host:" + hostKey
}
_, _ = l.svcCtx.DBSync.Store().EnsureBinding(dbsync.Binding{
TenantID: u.TenantID,
UserID: u.UserID,
LocalDatabaseID: localID,
OnlineDBID: online,
ChannelID: ch.ID,
DatabaseName: dbName,
DisplayName: dbName,
Note: note,
})
return updated, secret, nil
}