Go 实现 /api/v1/analysis 契约、Demo/Live Worker、DeerFlow SSE;含设计文档与冒烟脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
218 lines
4.8 KiB
Go
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 ©Job
|
|
}
|
|
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 ©Job
|
|
}
|
|
|
|
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))
|
|
}
|