package bizdev import ( "context" "errors" "fmt" "path/filepath" "strings" "chat-agent-system/internal/squad" "chat-agent-system/internal/storage" ) // ResolveMaterials 将请求材料 + 项目 CAS 资产解析为可注入工作区的 squad 材料。 // 项目内全部有效资产自动纳入(含私密文档);显式附件优先并去重。 func (s *Service) ResolveMaterials(ctx context.Context, req TaskRunRequest) ([]squad.MaterialItem, map[string]any, error) { manifest := map[string]any{ "explicit_materials": len(req.Materials), } byKey := make(map[string]squad.MaterialItem) addItem := func(item squad.MaterialItem) error { if strings.TrimSpace(item.Filename) == "" { return errors.New("material filename required") } if len(item.Content) == 0 { return fmt.Errorf("material %q has no extractable content", item.Filename) } key := item.Filename if item.AssetID > 0 { key = fmt.Sprintf("asset:%d", item.AssetID) } byKey[key] = item return nil } // 1. 项目资产池(不要求用户本轮再次附加) if s.cas != nil && req.ProjectID > 0 { refs, err := s.cas.ListProjectRefs(ctx, req.ProjectID) if err != nil { return nil, nil, err } var assetEntries []map[string]any for _, ref := range refs { data, loadedRef, err := s.cas.ReadRefBytes(ctx, ref.ID) if err != nil { continue } text, err := squad.ExtractTextWithAnchors(loadedRef.FileName, data) if err != nil { return nil, nil, fmt.Errorf("extract project asset %q: %w", loadedRef.FileName, err) } if strings.TrimSpace(text) == "" { return nil, nil, fmt.Errorf("project asset %q extracted empty text", loadedRef.FileName) } item := squad.MaterialItem{ Filename: loadedRef.FileName, Content: []byte(text), AssetID: loadedRef.ID, SHA256: loadedRef.BlobSHA256, OwnerID: loadedRef.UserID, } _ = addItem(item) assetEntries = append(assetEntries, map[string]any{ "asset_id": loadedRef.ID, "filename": loadedRef.FileName, "sha256": loadedRef.BlobSHA256, "owner_user_id": loadedRef.UserID, "file_category": loadedRef.FileCategory, "source": "project_asset_pool", }) } manifest["project_assets"] = assetEntries } // 2. 显式附件(asset_id 优先,其次 inline content) for _, m := range req.Materials { if m.AssetID > 0 && s.cas != nil { data, ref, err := s.cas.ReadRefBytes(ctx, m.AssetID) if err != nil { return nil, nil, fmt.Errorf("load attachment asset_id=%d: %w", m.AssetID, err) } if ref.ProjectID > 0 && req.ProjectID > 0 && ref.ProjectID != req.ProjectID { return nil, nil, fmt.Errorf("asset_id=%d belongs to another project", m.AssetID) } filename := m.Filename if filename == "" { filename = ref.FileName } text, err := squad.ExtractTextWithAnchors(filename, data) if err != nil { return nil, nil, fmt.Errorf("extract attachment %q: %w", filename, err) } if strings.TrimSpace(text) == "" { return nil, nil, fmt.Errorf("attachment %q extracted empty text", filename) } if err := addItem(squad.MaterialItem{ Filename: filename, Content: []byte(text), AssetID: ref.ID, SHA256: ref.BlobSHA256, OwnerID: ref.UserID, }); err != nil { return nil, nil, err } continue } filename := strings.TrimSpace(m.Filename) if filename == "" { return nil, nil, errors.New("material filename required when asset_id absent") } if len(m.Content) == 0 { return nil, nil, fmt.Errorf("material %q has empty content and no asset_id", filename) } text, err := squad.ExtractTextWithAnchors(filename, m.Content) if err != nil { return nil, nil, fmt.Errorf("extract inline material %q: %w", filename, err) } if strings.TrimSpace(text) == "" { return nil, nil, fmt.Errorf("inline material %q extracted empty text", filename) } if err := addItem(squad.MaterialItem{ Filename: filepath.Base(filename), Content: []byte(text), }); err != nil { return nil, nil, err } } out := make([]squad.MaterialItem, 0, len(byKey)) for _, item := range byKey { out = append(out, item) } manifest["resolved_count"] = len(out) return out, manifest, nil } // ValidateTaskRunIdentity 拒绝缺失项目/会话标识的任务启动。 func ValidateTaskRunIdentity(sessionID string, projectID int64) error { if strings.TrimSpace(sessionID) == "" { return errors.New("session_id required for task run") } if projectID <= 0 { return errors.New("project_id required for task run") } return nil } // MaterialInputFromRef 由 CAS 引用构造任务材料输入(供上传 API 测试)。 func MaterialInputFromRef(ref *storage.FileReference) MaterialInput { if ref == nil { return MaterialInput{} } return MaterialInput{ Filename: ref.FileName, AssetID: ref.ID, } }