Files
eightbf-analysis-go/internal/service/access.go
T
gozerosandCursor 0c45b86d67 chore: 初始化 eightbf-analysis-go(对齐 Java 会商作业服务)
Go 实现 /api/v1/analysis 契约、Demo/Live Worker、DeerFlow SSE;含设计文档与冒烟脚本。

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

218 lines
4.8 KiB
Go

package service
import (
"fmt"
"strings"
"git.gozeros.com/wsk/eightbf-analysis-go/internal/domain"
)
const (
RoleAdmin = "admin"
RoleTaskOwner = "task_owner"
RolePositionOwner = "position_owner"
RoleViewer = "viewer"
)
// Caller 当前请求调用方(来自 X-Account-Id / X-Analysis-Role)。
type Caller struct {
AccountID string
Role string
}
func NewCaller(accountID, role string) Caller {
r := strings.TrimSpace(role)
if r == "" {
r = RoleViewer
}
return Caller{AccountID: strings.TrimSpace(accountID), Role: r}
}
// AccessService 岗位账号限制与对话投影。
type AccessService struct {
AuthEnabled bool
}
func (a *AccessService) CanMutate(job *domain.AnalysisJob, c Caller) bool {
if !a.AuthEnabled {
return true
}
if c.AccountID == "" {
return false
}
if c.Role == RoleAdmin || c.Role == RoleTaskOwner {
return true
}
if c.AccountID == strings.TrimSpace(job.OwnerAccountID) {
return true
}
if a.isListedTaskOwner(job, c.AccountID) {
return true
}
return c.Role == RolePositionOwner && len(a.boundSeats(job, c.AccountID)) > 0
}
func (a *AccessService) CanCancel(job *domain.AnalysisJob, c Caller) bool {
if c.Role == RolePositionOwner {
if c.AccountID == strings.TrimSpace(job.OwnerAccountID) || a.isListedTaskOwner(job, c.AccountID) {
return true
}
return false
}
if !a.AuthEnabled {
return true
}
if c.AccountID == "" {
return false
}
if c.Role == RoleAdmin || c.Role == RoleTaskOwner {
return true
}
if c.AccountID == strings.TrimSpace(job.OwnerAccountID) {
return true
}
return a.isListedTaskOwner(job, c.AccountID)
}
func (a *AccessService) CanRead(job *domain.AnalysisJob, c Caller) bool {
if !a.AuthEnabled {
return true
}
if c.AccountID == "" {
return false
}
if a.CanMutate(job, c) || a.CanCancel(job, c) {
return true
}
return len(a.boundSeats(job, c.AccountID)) > 0
}
// ProjectForCaller 按角色裁剪对话后返回(责任人全量;岗位只见本席+总控/系统)。
func (a *AccessService) ProjectForCaller(job *domain.AnalysisJob, c Caller) *domain.AnalysisJob {
if job == nil {
return nil
}
if c.AccountID == "" {
return job
}
if c.Role == RoleAdmin || c.Role == RoleTaskOwner ||
c.AccountID == strings.TrimSpace(job.OwnerAccountID) ||
a.isListedTaskOwner(job, c.AccountID) {
return job
}
if c.Role != RolePositionOwner && !a.AuthEnabled {
return job
}
seats := a.boundSeats(job, c.AccountID)
copyJob := *job
if len(seats) == 0 {
var filtered []map[string]interface{}
for _, d := range job.Dialogues {
role := asStr(d["role"])
if role == "system" || role == "user" {
filtered = append(filtered, d)
}
}
copyJob.Dialogues = filtered
if job.Result != nil {
r := cloneMap(job.Result)
delete(r, "seatDeliveries")
copyJob.Result = r
}
return &copyJob
}
var filtered []map[string]interface{}
for _, d := range job.Dialogues {
role := asStr(d["role"])
agentID := asStr(d["agentId"])
if role == "system" || role == "user" || role == "leader" {
filtered = append(filtered, d)
} else if seats[agentID] {
filtered = append(filtered, d)
}
}
copyJob.Dialogues = filtered
if job.Result != nil {
r := cloneMap(job.Result)
if raw, ok := r["seatDeliveries"].([]interface{}); ok {
var kept []interface{}
for _, o := range raw {
m, ok := o.(map[string]interface{})
if !ok {
continue
}
if seats[asStr(m["agentId"])] {
kept = append(kept, m)
}
}
r["seatDeliveries"] = kept
}
copyJob.Result = r
}
return &copyJob
}
func (a *AccessService) boundSeats(job *domain.AnalysisJob, accountID string) map[string]bool {
out := map[string]bool{}
if job == nil || accountID == "" || job.Access == nil {
return out
}
raw, ok := job.Access["seatAccounts"]
if !ok {
return out
}
m, ok := raw.(map[string]interface{})
if !ok {
return out
}
for k, v := range m {
if accountInList(v, accountID) {
out[k] = true
}
}
return out
}
func (a *AccessService) isListedTaskOwner(job *domain.AnalysisJob, accountID string) bool {
if job == nil || job.Access == nil {
return false
}
return accountInList(job.Access["taskOwnerAccountIds"], accountID) ||
accountInList(job.Access["adminAccountIds"], accountID)
}
func accountInList(raw interface{}, accountID string) bool {
if raw == nil || accountID == "" {
return false
}
switch t := raw.(type) {
case []interface{}:
for _, o := range t {
if strings.TrimSpace(asStr(o)) == accountID {
return true
}
}
return false
default:
return strings.TrimSpace(asStr(raw)) == accountID
}
}
func cloneMap(m map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(m))
for k, v := range m {
out[k] = v
}
return out
}
func asStr(v interface{}) string {
if v == nil {
return ""
}
if s, ok := v.(string); ok {
return s
}
return strings.TrimSpace(fmt.Sprint(v))
}