refactor(server): remove 24 mirrored Runtime fields via nil-safe accessors; fix Shutdown-vs-background-loop DATA RACE and goose 00071 duplicate version
- Server no longer mirrors app.Runtime services (gov/svc/pwSvc/...): 574 call sites now use accessors in runtime_accessors.go - backgroundLoop (mutex+sync.Once) stops sweeper/publisher/reminder/wsGC goroutines race-free (baseline44161d2areproduced DATA RACE; now green under -race) - goose 00071 duplicate (dialogue merge1da17a28brought two 00071): context-conflict-adjudications renumbered to 00072; dev DB applied only through 00070 so zero data migration - task_finalize tests build minimal app.Runtime; test injection via serverTestOverrides + setOverride* (explicit-nil markers for fail-closed cases)
This commit is contained in:
@@ -16,7 +16,7 @@ func (s *Server) handleAdminRoles(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
roles, err := s.authzSvc.ListRoles(r.Context())
|
||||
roles, err := s.authzSvc().ListRoles(r.Context())
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -28,7 +28,7 @@ func (s *Server) handleAdminRoles(w http.ResponseWriter, r *http.Request) {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "无效的 JSON 请求体")
|
||||
return
|
||||
}
|
||||
created, err := s.authzSvc.CreateRole(r.Context(), req)
|
||||
created, err := s.authzSvc().CreateRole(r.Context(), req)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if errors.Is(err, authz.ErrRolesNotAvailable) {
|
||||
@@ -61,7 +61,7 @@ func (s *Server) handleAdminRoleByID(w http.ResponseWriter, r *http.Request) {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "无效的 JSON 请求体")
|
||||
return
|
||||
}
|
||||
updated, err := s.authzSvc.UpdateRole(r.Context(), roleID, req)
|
||||
updated, err := s.authzSvc().UpdateRole(r.Context(), roleID, req)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
switch {
|
||||
@@ -77,7 +77,7 @@ func (s *Server) handleAdminRoleByID(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
writeJSONResponse(w, http.StatusOK, updated)
|
||||
case http.MethodDelete:
|
||||
if err := s.authzSvc.DeleteRole(r.Context(), roleID); err != nil {
|
||||
if err := s.authzSvc().DeleteRole(r.Context(), roleID); err != nil {
|
||||
status := http.StatusBadRequest
|
||||
switch {
|
||||
case errors.Is(err, authz.ErrRoleNotFound):
|
||||
|
||||
@@ -16,44 +16,44 @@ func (s *Server) handleAdminSystemOverview(w http.ResponseWriter, r *http.Reques
|
||||
if _, ok := s.requireSystemPermission(w, r, authz.PermSystemOverview); !ok {
|
||||
return
|
||||
}
|
||||
if s.sysadmin == nil {
|
||||
if s.sysadmin() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "sysadmin service unavailable")
|
||||
return
|
||||
}
|
||||
writeJSONResponse(w, http.StatusOK, s.sysadmin.Overview(r.Context()))
|
||||
writeJSONResponse(w, http.StatusOK, s.sysadmin().Overview(r.Context()))
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminSystemKB(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := s.requireSystemPermission(w, r, authz.PermSystemKBRead); !ok {
|
||||
return
|
||||
}
|
||||
if s.sysadmin == nil {
|
||||
if s.sysadmin() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "sysadmin service unavailable")
|
||||
return
|
||||
}
|
||||
writeJSONResponse(w, http.StatusOK, s.sysadmin.KBInfo())
|
||||
writeJSONResponse(w, http.StatusOK, s.sysadmin().KBInfo())
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminSystemFeatures(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := s.requireSystemPermission(w, r, authz.PermSystemFeaturesRead); !ok {
|
||||
return
|
||||
}
|
||||
if s.sysadmin == nil {
|
||||
if s.sysadmin() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "sysadmin service unavailable")
|
||||
return
|
||||
}
|
||||
writeJSONResponse(w, http.StatusOK, map[string]any{"features": s.sysadmin.Features()})
|
||||
writeJSONResponse(w, http.StatusOK, map[string]any{"features": s.sysadmin().Features()})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminSystemSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := s.requireSystemPermission(w, r, authz.PermSystemSettingsRead); !ok {
|
||||
return
|
||||
}
|
||||
if s.sysadmin == nil {
|
||||
if s.sysadmin() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "sysadmin service unavailable")
|
||||
return
|
||||
}
|
||||
settings, err := s.sysadmin.ListSettings(r.Context())
|
||||
settings, err := s.sysadmin().ListSettings(r.Context())
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -66,7 +66,7 @@ func (s *Server) handleAdminSystemSettingByKey(w http.ResponseWriter, r *http.Re
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.sysadmin == nil {
|
||||
if s.sysadmin() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "sysadmin service unavailable")
|
||||
return
|
||||
}
|
||||
@@ -79,7 +79,7 @@ func (s *Server) handleAdminSystemSettingByKey(w http.ResponseWriter, r *http.Re
|
||||
writeSafeError(w, r, http.StatusBadRequest, "无效的请求体")
|
||||
return
|
||||
}
|
||||
setting, err := s.sysadmin.UpdateSettingEx(r.Context(), sysadmin.SettingUpdate{
|
||||
setting, err := s.sysadmin().UpdateSettingEx(r.Context(), sysadmin.SettingUpdate{
|
||||
Key: key,
|
||||
Value: req.Value,
|
||||
UpdatedBy: user.ID,
|
||||
@@ -110,7 +110,7 @@ func (s *Server) handleAdminSystemSettingAudits(w http.ResponseWriter, r *http.R
|
||||
if _, ok := s.requireSystemPermission(w, r, authz.PermSystemAuditRead); !ok {
|
||||
return
|
||||
}
|
||||
if s.sysadmin == nil {
|
||||
if s.sysadmin() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "sysadmin service unavailable")
|
||||
return
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func (s *Server) handleAdminSystemSettingAudits(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
limit = n
|
||||
}
|
||||
audits, err := s.sysadmin.ListPolicyAudits(r.Context(), key, limit)
|
||||
audits, err := s.sysadmin().ListPolicyAudits(r.Context(), key, limit)
|
||||
switch {
|
||||
case errors.Is(err, sysadmin.ErrUnknownSetting):
|
||||
writeSafeError(w, r, http.StatusBadRequest, "未知的设置键")
|
||||
|
||||
@@ -285,7 +285,7 @@ func TestAdminSystemSettings_ListAndUpdate(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
srv.cfg.AllowInMemory = false // 关闭 legacy 未认证兜底,严格鉴权
|
||||
// 用带内存 store 的 sysadmin 服务替换装配产物(无 DB 时 store 为 nil)。
|
||||
srv.sysadmin = sysadmin.NewService(srv.cfg, sysadmin.Options{Store: newFakeSettingsStore()})
|
||||
srv.setOverrideSysadmin(sysadmin.NewService(srv.cfg, sysadmin.Options{Store: newFakeSettingsStore()}))
|
||||
token := loginTestAdmin(t, srv)
|
||||
|
||||
w := doSysadminRequest(t, srv, http.MethodGet, "/api/v1/admin/system/settings", token, "")
|
||||
@@ -341,7 +341,7 @@ func TestAdminSystemSettings_AuthorOnlyPolicyCAS(t *testing.T) {
|
||||
store := newFakeSettingsStore()
|
||||
// 模拟迁移种子行
|
||||
_, _, _ = store.Upsert(context.Background(), sysadmin.SettingKeyAllowAuthorOnlyApproval, json.RawMessage(`false`), 0)
|
||||
srv.sysadmin = sysadmin.NewService(srv.cfg, sysadmin.Options{Store: store})
|
||||
srv.setOverrideSysadmin(sysadmin.NewService(srv.cfg, sysadmin.Options{Store: store}))
|
||||
token := loginTestAdmin(t, srv)
|
||||
plainToken := registerTestUser(t, srv, "plain_author_only_probe")
|
||||
|
||||
@@ -387,7 +387,7 @@ func TestAdminSystemSettings_ListAudits(t *testing.T) {
|
||||
srv.cfg.AllowInMemory = false
|
||||
store := newFakeSettingsStore()
|
||||
_, _, _ = store.Upsert(context.Background(), sysadmin.SettingKeyAllowAuthorOnlyApproval, json.RawMessage(`false`), 0)
|
||||
srv.sysadmin = sysadmin.NewService(srv.cfg, sysadmin.Options{Store: store})
|
||||
srv.setOverrideSysadmin(sysadmin.NewService(srv.cfg, sysadmin.Options{Store: store}))
|
||||
token := loginTestAdmin(t, srv)
|
||||
plainToken := registerTestUser(t, srv, "plain_audit_probe")
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ func (s *Server) handleAdminTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
assets, err := s.wfDefSvc.ListTemplates(r.Context())
|
||||
assets, err := s.wfDefSvc().ListTemplates(r.Context())
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -51,7 +51,7 @@ func (s *Server) handleAdminTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "read file failed")
|
||||
return
|
||||
}
|
||||
tv, err := s.wfDefSvc.UploadTemplate(r.Context(), assetKey, name, data, header.Filename, user.ID)
|
||||
tv, err := s.wfDefSvc().UploadTemplate(r.Context(), assetKey, name, data, header.Filename, user.ID)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
switch {
|
||||
@@ -82,7 +82,7 @@ func (s *Server) handleAdminTemplateVersions(w http.ResponseWriter, r *http.Requ
|
||||
writeSafeError(w, r, http.StatusBadRequest, "invalid asset key")
|
||||
return
|
||||
}
|
||||
versions, err := s.wfDefSvc.ListTemplateVersions(r.Context(), assetKey)
|
||||
versions, err := s.wfDefSvc().ListTemplateVersions(r.Context(), assetKey)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
|
||||
@@ -16,7 +16,7 @@ func (s *Server) requireWorkflowAdmin(w http.ResponseWriter, r *http.Request) (*
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if s.wfDefSvc == nil || !s.wfDefSvc.Available() {
|
||||
if s.wfDefSvc() == nil || !s.wfDefSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "工作流定义服务需要数据库 IAM")
|
||||
return nil, false
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func (s *Server) handleAdminWorkflows(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
defs, err := s.wfDefSvc.ListDefinitions(r.Context())
|
||||
defs, err := s.wfDefSvc().ListDefinitions(r.Context())
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -42,7 +42,7 @@ func (s *Server) handleAdminWorkflows(w http.ResponseWriter, r *http.Request) {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "无效的 JSON")
|
||||
return
|
||||
}
|
||||
def, ver, err := s.wfDefSvc.CreateDefinition(r.Context(), req, user.ID)
|
||||
def, ver, err := s.wfDefSvc().CreateDefinition(r.Context(), req, user.ID)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if errors.Is(err, workflowdef.ErrValidationFailed) {
|
||||
@@ -70,12 +70,12 @@ func (s *Server) handleAdminWorkflowByID(w http.ResponseWriter, r *http.Request)
|
||||
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
def, err := s.wfDefSvc.GetDefinition(r.Context(), id)
|
||||
def, err := s.wfDefSvc().GetDefinition(r.Context(), id)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
versions, err := s.wfDefSvc.ListVersions(r.Context(), id)
|
||||
versions, err := s.wfDefSvc().ListVersions(r.Context(), id)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -108,7 +108,7 @@ func (s *Server) handleAdminWorkflowVersion(w http.ResponseWriter, r *http.Reque
|
||||
writeSafeError(w, r, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
updated, err := s.wfDefSvc.UpdateDraft(r.Context(), workflowID, version, body.Definition)
|
||||
updated, err := s.wfDefSvc().UpdateDraft(r.Context(), workflowID, version, body.Definition)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if errors.Is(err, workflowdef.ErrNotDraft) {
|
||||
@@ -134,7 +134,7 @@ func (s *Server) handleAdminWorkflowValidate(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
workflowID, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
version, _ := strconv.Atoi(r.PathValue("version"))
|
||||
res, err := s.wfDefSvc.ValidateVersion(r.Context(), workflowID, version)
|
||||
res, err := s.wfDefSvc().ValidateVersion(r.Context(), workflowID, version)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusNotFound, err.Error())
|
||||
return
|
||||
@@ -153,7 +153,7 @@ func (s *Server) handleAdminWorkflowPublish(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
workflowID, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
version, _ := strconv.Atoi(r.PathValue("version"))
|
||||
ver, err := s.wfDefSvc.PublishVersion(r.Context(), workflowID, version, user.ID)
|
||||
ver, err := s.wfDefSvc().PublishVersion(r.Context(), workflowID, version, user.ID)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if errors.Is(err, workflowdef.ErrValidationFailed) {
|
||||
@@ -162,7 +162,7 @@ func (s *Server) handleAdminWorkflowPublish(w http.ResponseWriter, r *http.Reque
|
||||
writeSafeError(w, r, status, err.Error())
|
||||
return
|
||||
}
|
||||
if refresher, ok := s.pwSvc.Catalog().(interface {
|
||||
if refresher, ok := s.pwSvc().Catalog().(interface {
|
||||
Refresh(context.Context) error
|
||||
}); ok {
|
||||
_ = refresher.Refresh(r.Context())
|
||||
@@ -180,12 +180,12 @@ func (s *Server) handleAdminWorkflowRetire(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
workflowID, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
version, _ := strconv.Atoi(r.PathValue("version"))
|
||||
ver, err := s.wfDefSvc.RetireVersion(r.Context(), workflowID, version)
|
||||
ver, err := s.wfDefSvc().RetireVersion(r.Context(), workflowID, version)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if refresher, ok := s.pwSvc.Catalog().(interface {
|
||||
if refresher, ok := s.pwSvc().Catalog().(interface {
|
||||
Refresh(context.Context) error
|
||||
}); ok {
|
||||
_ = refresher.Refresh(r.Context())
|
||||
|
||||
@@ -72,7 +72,7 @@ func (s *Server) handleUploadProjectAsset(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
cas := s.cas
|
||||
cas := s.cas()
|
||||
if cas == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "storage unavailable")
|
||||
return
|
||||
@@ -84,7 +84,7 @@ func (s *Server) handleUploadProjectAsset(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
parseState := ""
|
||||
if matIdx := bizdev.Snapshot(s.svc).MaterialIndex; matIdx != nil && matIdx.Enabled() && matIdx.HasStore() {
|
||||
if matIdx := bizdev.Snapshot(s.svc()).MaterialIndex; matIdx != nil && matIdx.Enabled() && matIdx.HasStore() {
|
||||
if mineru.SupportsExt(ref.FileName) {
|
||||
matIdx.EnqueueAsync(ref.FileName, ref.BlobSHA256, data, projectID)
|
||||
// Report the real state: same-content uploads hit an existing parse
|
||||
@@ -126,7 +126,7 @@ func (s *Server) handleReparseProjectAsset(w http.ResponseWriter, r *http.Reques
|
||||
if _, ok := s.requireProjectAccess(w, r, projectID); !ok {
|
||||
return
|
||||
}
|
||||
cas := s.cas
|
||||
cas := s.cas()
|
||||
if cas == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "storage unavailable")
|
||||
return
|
||||
@@ -140,7 +140,7 @@ func (s *Server) handleReparseProjectAsset(w http.ResponseWriter, r *http.Reques
|
||||
writeSafeError(w, r, http.StatusNotFound, "asset not found")
|
||||
return
|
||||
}
|
||||
matIdx := bizdev.Snapshot(s.svc).MaterialIndex
|
||||
matIdx := bizdev.Snapshot(s.svc()).MaterialIndex
|
||||
if matIdx == nil || !matIdx.Enabled() || !matIdx.HasStore() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "parse engine unavailable")
|
||||
return
|
||||
@@ -169,7 +169,7 @@ func (s *Server) handleListProjectAssets(w http.ResponseWriter, r *http.Request)
|
||||
if _, ok := s.requireProjectAccess(w, r, projectID); !ok {
|
||||
return
|
||||
}
|
||||
cas := s.cas
|
||||
cas := s.cas()
|
||||
if cas == nil {
|
||||
writeJSONResponse(w, http.StatusOK, []projectAssetResponse{})
|
||||
return
|
||||
@@ -187,7 +187,7 @@ func (s *Server) handleListProjectAssets(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
infos := map[string]materialindex.ParseInfo{}
|
||||
if matIdx := bizdev.Snapshot(s.svc).MaterialIndex; matIdx != nil {
|
||||
if matIdx := bizdev.Snapshot(s.svc()).MaterialIndex; matIdx != nil {
|
||||
infos = matIdx.GetParseInfos(r.Context(), shas)
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ func (s *Server) handleListProjectAssets(w http.ResponseWriter, r *http.Request)
|
||||
// 内容已入库但从未触发解析(如早于 MinerU 接入的历史资产):
|
||||
// 列表读取时自动补触发,前端轮询即可看到 pending→running→done。
|
||||
seenSHA[ref.BlobSHA256] = true
|
||||
if matIdx := bizdev.Snapshot(s.svc).MaterialIndex; matIdx != nil && matIdx.Enabled() && matIdx.HasStore() && mineru.SupportsExt(ref.FileName) {
|
||||
if matIdx := bizdev.Snapshot(s.svc()).MaterialIndex; matIdx != nil && matIdx.Enabled() && matIdx.HasStore() && mineru.SupportsExt(ref.FileName) {
|
||||
if data, _, err := cas.ReadRefBytes(r.Context(), ref.ID); err == nil {
|
||||
matIdx.EnqueueAsync(ref.FileName, ref.BlobSHA256, data, projectID)
|
||||
item.ParseState = "pending"
|
||||
@@ -251,7 +251,7 @@ func (s *Server) handleGetProjectAssetRaw(w http.ResponseWriter, r *http.Request
|
||||
if _, ok := s.requireProjectAccess(w, r, projectID); !ok {
|
||||
return
|
||||
}
|
||||
cas := s.cas
|
||||
cas := s.cas()
|
||||
if cas == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "storage unavailable")
|
||||
return
|
||||
|
||||
@@ -48,7 +48,7 @@ func (s *Server) authenticateUser(r *http.Request) (*auth.UserAccount, error) {
|
||||
token = r.Header.Get("X-Session-Token")
|
||||
}
|
||||
if token != "" {
|
||||
user, err := s.authSvc.GetUserByToken(r.Context(), token)
|
||||
user, err := s.authSvc().GetUserByToken(r.Context(), token)
|
||||
if err == nil && user != nil {
|
||||
return user, nil
|
||||
}
|
||||
@@ -78,7 +78,7 @@ func (s *Server) handleAuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
session, err := s.authSvc.Login(r.Context(), req.Username, req.Password)
|
||||
session, err := s.authSvc().Login(r.Context(), req.Username, req.Password)
|
||||
if err != nil {
|
||||
errMsg := err.Error()
|
||||
if strings.Contains(errMsg, "审核") {
|
||||
@@ -117,7 +117,7 @@ func (s *Server) handleAuthRegister(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := s.authSvc.Register(r.Context(), req)
|
||||
resp, err := s.authSvc().Register(r.Context(), req)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if authIsInfrastructureError(err) {
|
||||
@@ -135,7 +135,7 @@ func (s *Server) handleAuthRegister(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) handleAuthLogout(w http.ResponseWriter, r *http.Request) {
|
||||
token := extractBearerToken(r)
|
||||
if token != "" {
|
||||
s.authSvc.Logout(token)
|
||||
s.authSvc().Logout(token)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
@@ -158,7 +158,7 @@ func (s *Server) handleAuthChangePassword(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.authSvc.ChangePassword(r.Context(), user.ID, req.OldPassword, req.NewPassword); err != nil {
|
||||
if err := s.authSvc().ChangePassword(r.Context(), user.ID, req.OldPassword, req.NewPassword); err != nil {
|
||||
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -198,8 +198,8 @@ func (s *Server) handleAuthMe(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Server) resolveUserCapabilities(ctx context.Context, user *auth.UserAccount) []string {
|
||||
caps := []string{}
|
||||
if s.authzSvc != nil {
|
||||
if listed, capErr := s.authzSvc.ListCapabilities(ctx, user.ID); capErr == nil && len(listed) > 0 {
|
||||
if s.authzSvc() != nil {
|
||||
if listed, capErr := s.authzSvc().ListCapabilities(ctx, user.ID); capErr == nil && len(listed) > 0 {
|
||||
return listed
|
||||
}
|
||||
}
|
||||
@@ -220,7 +220,7 @@ func (s *Server) handleAuthSessions(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if r.Method == http.MethodGet {
|
||||
token := extractBearerToken(r)
|
||||
sessions, listErr := s.authSvc.ListUserSessions(r.Context(), user.ID, token)
|
||||
sessions, listErr := s.authSvc().ListUserSessions(r.Context(), user.ID, token)
|
||||
if listErr != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, listErr.Error())
|
||||
return
|
||||
@@ -249,7 +249,7 @@ func (s *Server) handleAuthSessionByID(w http.ResponseWriter, r *http.Request) {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "无效的会话 ID")
|
||||
return
|
||||
}
|
||||
if err := s.authSvc.RevokeUserSession(r.Context(), user.ID, sessionID); err != nil {
|
||||
if err := s.authSvc().RevokeUserSession(r.Context(), user.ID, sessionID); err != nil {
|
||||
writeSafeError(w, r, http.StatusNotFound, "会话不存在或已失效")
|
||||
return
|
||||
}
|
||||
@@ -263,7 +263,7 @@ func (s *Server) handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
users, err := s.authSvc.ListUsers(r.Context())
|
||||
users, err := s.authSvc().ListUsers(r.Context())
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -279,8 +279,8 @@ func (s *Server) handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "无效的 JSON 请求体")
|
||||
return
|
||||
}
|
||||
if roleID := strings.TrimSpace(req.RoleID); roleID != "" && s.authzSvc != nil && s.authzSvc.Enabled() {
|
||||
roles, listErr := s.authzSvc.ListRoles(r.Context())
|
||||
if roleID := strings.TrimSpace(req.RoleID); roleID != "" && s.authzSvc() != nil && s.authzSvc().Enabled() {
|
||||
roles, listErr := s.authzSvc().ListRoles(r.Context())
|
||||
if listErr != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, listErr.Error())
|
||||
return
|
||||
@@ -297,7 +297,7 @@ func (s *Server) handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
created, err := s.authSvc.CreateUserByAdmin(r.Context(), req)
|
||||
created, err := s.authSvc().CreateUserByAdmin(r.Context(), req)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if authIsInfrastructureError(err) {
|
||||
@@ -335,7 +335,7 @@ func (s *Server) handleAdminUpdateRole(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := s.authSvc.UpdateUserRole(r.Context(), targetID, req.RoleID, req.Position)
|
||||
updated, err := s.authSvc().UpdateUserRole(r.Context(), targetID, req.RoleID, req.Position)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
@@ -368,13 +368,13 @@ func (s *Server) handleAdminUpdateStatus(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
if req.RoleID != "" {
|
||||
if _, roleErr := s.authSvc.UpdateUserRole(r.Context(), targetID, req.RoleID, req.Position); roleErr != nil {
|
||||
if _, roleErr := s.authSvc().UpdateUserRole(r.Context(), targetID, req.RoleID, req.Position); roleErr != nil {
|
||||
writeSafeError(w, r, http.StatusBadRequest, roleErr.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := s.authSvc.UpdateUserStatus(r.Context(), targetID, req.Status)
|
||||
updated, err := s.authSvc().UpdateUserStatus(r.Context(), targetID, req.Status)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
@@ -402,7 +402,7 @@ func (s *Server) handleAdminResetPassword(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
result, err := s.authSvc.AdminResetPassword(r.Context(), targetID, actor.ID, httpx.RequestID(r))
|
||||
result, err := s.authSvc().AdminResetPassword(r.Context(), targetID, actor.ID, httpx.RequestID(r))
|
||||
if err != nil {
|
||||
msg := err.Error()
|
||||
status := http.StatusBadRequest
|
||||
@@ -425,7 +425,7 @@ func (s *Server) handleAuthPublicConfig(w http.ResponseWriter, r *http.Request)
|
||||
writeSafeError(w, r, http.StatusMethodNotAllowed, "不支持的方法")
|
||||
return
|
||||
}
|
||||
cfg, err := s.authSvc.GetConfig(r.Context())
|
||||
cfg, err := s.authSvc().GetConfig(r.Context())
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -441,7 +441,7 @@ func (s *Server) handleAdminConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
cfg, err := s.authSvc.GetConfig(r.Context())
|
||||
cfg, err := s.authSvc().GetConfig(r.Context())
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -457,7 +457,7 @@ func (s *Server) handleAdminConfig(w http.ResponseWriter, r *http.Request) {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "无效的请求体")
|
||||
return
|
||||
}
|
||||
if err := s.authSvc.UpdateConfig(r.Context(), req); err != nil {
|
||||
if err := s.authSvc().UpdateConfig(r.Context(), req); err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func (s *Server) userHasSystemPermission(ctx context.Context, user *auth.UserAcc
|
||||
if s.isSystemAdminUser(ctx, user) {
|
||||
return true, nil
|
||||
}
|
||||
caps, err := s.authzSvc.ListCapabilities(ctx, user.ID)
|
||||
caps, err := s.authzSvc().ListCapabilities(ctx, user.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -77,11 +77,11 @@ func (s *Server) requireAuthenticatedUser(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// assertSessionOwnedByUser 校验会话归属当前用户;会话不存在或归属他人返回 false。
|
||||
func (s *Server) assertSessionOwnedByUser(w http.ResponseWriter, r *http.Request, sessionID string, userID int64) bool {
|
||||
if s.ctxMgr == nil {
|
||||
if s.ctxMgr() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||
return false
|
||||
}
|
||||
ownerID, err := s.ctxMgr.GetSessionOwnerID(r.Context(), sessionID)
|
||||
ownerID, err := s.ctxMgr().GetSessionOwnerID(r.Context(), sessionID)
|
||||
if err != nil || ownerID <= 0 {
|
||||
writeJSONResponse(w, http.StatusNotFound, map[string]string{
|
||||
"error_code": "SESSION_NOT_FOUND",
|
||||
@@ -120,7 +120,7 @@ func (s *Server) requireProjectAccess(w http.ResponseWriter, r *http.Request, pr
|
||||
if s.isSystemAdminUser(r.Context(), user) {
|
||||
return user, true
|
||||
}
|
||||
if err := s.authorizer.RequireProjectAccess(r.Context(), projectID, user.ID); err != nil {
|
||||
if err := s.authorizer().RequireProjectAccess(r.Context(), projectID, user.ID); err != nil {
|
||||
if err == authz.ErrForbidden {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
"error_code": "ERR_FORBIDDEN",
|
||||
@@ -143,7 +143,7 @@ func (s *Server) requireDocGovDocAccess(w http.ResponseWriter, r *http.Request,
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]string{
|
||||
"error_code": "ERR_DOCGOV_UNAVAILABLE",
|
||||
"message": "docgov engine not initialized",
|
||||
@@ -153,7 +153,7 @@ func (s *Server) requireDocGovDocAccess(w http.ResponseWriter, r *http.Request,
|
||||
if s.isSystemAdminUser(r.Context(), user) {
|
||||
return user, true
|
||||
}
|
||||
doc, err := s.gov.GetDocument(r.Context(), docID)
|
||||
doc, err := s.gov().GetDocument(r.Context(), docID)
|
||||
if err != nil || doc == nil {
|
||||
writeJSONResponse(w, http.StatusNotFound, map[string]string{
|
||||
"error_code": "ERR_DOCUMENT_NOT_FOUND",
|
||||
@@ -169,7 +169,7 @@ func (s *Server) requireDocGovDocAccess(w http.ResponseWriter, r *http.Request,
|
||||
})
|
||||
return nil, false
|
||||
}
|
||||
if err := s.authorizer.RequireProjectAccess(r.Context(), doc.ProjectID, user.ID); err != nil {
|
||||
if err := s.authorizer().RequireProjectAccess(r.Context(), doc.ProjectID, user.ID); err != nil {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
"error_code": "ERR_FORBIDDEN",
|
||||
"message": "无权访问该文档所属项目",
|
||||
@@ -181,10 +181,10 @@ func (s *Server) requireDocGovDocAccess(w http.ResponseWriter, r *http.Request,
|
||||
|
||||
// resolveDocGovRole 服务端按文档职责分配解析 docgov 角色,不信任客户端自报角色。
|
||||
func (s *Server) resolveDocGovRole(ctx context.Context, docID string, user *auth.UserAccount) vocabulary.UserRole {
|
||||
if user == nil || s.gov == nil {
|
||||
if user == nil || s.gov() == nil {
|
||||
return vocabulary.UserRole("")
|
||||
}
|
||||
doc, err := s.gov.GetDocument(ctx, docID)
|
||||
doc, err := s.gov().GetDocument(ctx, docID)
|
||||
if err != nil || doc == nil {
|
||||
return vocabulary.UserRole("")
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func (s *Server) authMiddleware(next http.Handler) http.Handler {
|
||||
|
||||
user, err := s.authenticateUser(r)
|
||||
if (err != nil || user == nil) && s.allowLegacyTestPrincipal() {
|
||||
if fallback, fbErr := s.authSvc.GetUserByID(r.Context(), 1); fbErr == nil && fallback != nil {
|
||||
if fallback, fbErr := s.authSvc().GetUserByID(r.Context(), 1); fbErr == nil && fallback != nil {
|
||||
slog.Warn("legacy_test_principal_fallback",
|
||||
"path", r.URL.Path, "method", r.Method,
|
||||
"remote_addr", r.RemoteAddr, "fallback_user_id", fallback.ID)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package server
|
||||
|
||||
import "sync"
|
||||
|
||||
// backgroundLoop 收口 Server 后台循环的停止语义(修 DATA RACE: Shutdown 与
|
||||
// sweeper/publisher/reminder/GC goroutine 对 stop 通道的并发 close/nil 写)。
|
||||
// 通道仅在启动 goroutine 内一次创建并被该 goroutine 独占持有;
|
||||
// Shutdown 经 stopOnce 通知,不触碰通道字段本身。
|
||||
type backgroundLoop struct {
|
||||
mu sync.Mutex
|
||||
stopCh chan struct{}
|
||||
stopOnce sync.Once
|
||||
stopped bool
|
||||
}
|
||||
|
||||
// start 登记停止通道;返回值是否实际启动(幂等:已存在/已停止返回 false)。
|
||||
func (l *backgroundLoop) start() (chan struct{}, bool) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.stopCh != nil || l.stopped {
|
||||
return nil, false
|
||||
}
|
||||
l.stopCh = make(chan struct{})
|
||||
return l.stopCh, true
|
||||
}
|
||||
|
||||
// stop 通知循环退出并防止后续再启动;可安全多次调用。
|
||||
func (l *backgroundLoop) stop() {
|
||||
l.mu.Lock()
|
||||
ch := l.stopCh
|
||||
l.stopped = true
|
||||
l.mu.Unlock()
|
||||
if ch != nil {
|
||||
l.stopOnce.Do(func() { close(ch) })
|
||||
}
|
||||
}
|
||||
@@ -33,21 +33,21 @@ func (s *Server) bindSessionDocumentSummaryReader(sess *chat.Session, user *auth
|
||||
if user != nil {
|
||||
userID = user.ID
|
||||
}
|
||||
if projectID <= 0 || userID <= 0 || s.gov == nil {
|
||||
if projectID <= 0 || userID <= 0 || s.gov() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
sess.UserCtx.DocumentSummaryReader = func(ctx context.Context, target string) (*coordinator.ReadableDocument, error) {
|
||||
if s.authorizer == nil {
|
||||
if s.authorizer() == nil {
|
||||
return nil, fmt.Errorf("project authorization unavailable")
|
||||
}
|
||||
if err := s.authorizer.RequireProjectAccessOrAdmin(ctx, projectID, userID, s.isSystemAdminUser(ctx, user)); err != nil {
|
||||
if err := s.authorizer().RequireProjectAccessOrAdmin(ctx, projectID, userID, s.isSystemAdminUser(ctx, user)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(target) == intent.TargetDocLatestUpload {
|
||||
return s.resolveLatestUploadDocument(ctx, projectID)
|
||||
}
|
||||
listed, err := s.gov.ListWorkflowDocumentsByProject(ctx, projectID)
|
||||
listed, err := s.gov().ListWorkflowDocumentsByProject(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -99,12 +99,12 @@ func (s *Server) bindSessionDocumentSummaryReader(sess *chat.Session, user *auth
|
||||
|
||||
// resolveLatestUploadDocument 读取项目内最新上传的 CAS 资产正文(供 @latest_upload 哨兵)。
|
||||
func (s *Server) resolveLatestUploadDocument(ctx context.Context, projectID int64) (*coordinator.ReadableDocument, error) {
|
||||
if s == nil || s.cas == nil {
|
||||
if s == nil || s.cas() == nil {
|
||||
return nil, &coordinator.ErrNoUploadAsset{
|
||||
Message: "当前项目还没有上传过附件。请先在对话中上传文件后再试总结。",
|
||||
}
|
||||
}
|
||||
refs, err := s.cas.ListProjectRefs(ctx, projectID)
|
||||
refs, err := s.cas().ListProjectRefs(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -115,7 +115,7 @@ func (s *Server) resolveLatestUploadDocument(ctx context.Context, projectID int6
|
||||
}
|
||||
}
|
||||
|
||||
matIdx := bizdev.Snapshot(s.svc).MaterialIndex
|
||||
matIdx := bizdev.Snapshot(s.svc()).MaterialIndex
|
||||
state := ""
|
||||
if matIdx != nil {
|
||||
state = strings.TrimSpace(matIdx.GetParseState(ctx, latest.BlobSHA256))
|
||||
@@ -167,7 +167,7 @@ func (s *Server) resolveLatestUploadDocument(ctx context.Context, projectID int6
|
||||
}
|
||||
}
|
||||
|
||||
data, ref, err := s.cas.ReadRefBytes(ctx, latest.ID)
|
||||
data, ref, err := s.cas().ReadRefBytes(ctx, latest.ID)
|
||||
if err != nil || ref == nil {
|
||||
return nil, fmt.Errorf("load latest upload asset: %w", err)
|
||||
}
|
||||
@@ -302,10 +302,10 @@ func defaultUploadTitle(fileName string) string {
|
||||
|
||||
// activeWorkflowScopeForDocs 读取项目激活工作流键与任务集合;失败时返回空(= 不过滤)。
|
||||
func (s *Server) activeWorkflowScopeForDocs(ctx context.Context, projectID int64) (string, map[string]bool) {
|
||||
if s == nil || s.instSvc == nil || !s.instSvc.Available() || projectID <= 0 {
|
||||
if s == nil || s.instSvc() == nil || !s.instSvc().Available() || projectID <= 0 {
|
||||
return "", nil
|
||||
}
|
||||
workflowKey, taskKeys, err := s.instSvc.ActiveWorkflowScope(ctx, projectID)
|
||||
workflowKey, taskKeys, err := s.instSvc().ActiveWorkflowScope(ctx, projectID)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -145,12 +145,12 @@ func TestResolveLatestUploadDocument_TextAsset(t *testing.T) {
|
||||
cfg := &config.Config{Env: "test", DataDir: t.TempDir()}
|
||||
svc := bizdev.MustNewService(t, cfg)
|
||||
srv := NewServer(cfg, svc)
|
||||
if srv.cas == nil {
|
||||
if srv.cas() == nil {
|
||||
t.Fatal("expected CAS")
|
||||
}
|
||||
projectID := int64(42)
|
||||
userID := int64(7)
|
||||
_, err := srv.cas.StoreFile(context.Background(), projectID, userID, "shockwave.txt", "material",
|
||||
_, err := srv.cas().StoreFile(context.Background(), projectID, userID, "shockwave.txt", "material",
|
||||
[]byte("冲击波治疗仪核心适应症:软组织损伤与疼痛管理。\n关键约束:治疗强度需医师确认。"), "text/plain")
|
||||
if err != nil {
|
||||
t.Fatalf("store: %v", err)
|
||||
|
||||
@@ -33,8 +33,8 @@ func (s *Server) authorizeSessionRead(
|
||||
return nil, false
|
||||
}
|
||||
sessionOwnerID := user.ID
|
||||
if s.ctxMgr != nil {
|
||||
if ownerID, err := s.ctxMgr.GetSessionOwnerID(r.Context(), sessionID); err == nil && ownerID > 0 {
|
||||
if s.ctxMgr() != nil {
|
||||
if ownerID, err := s.ctxMgr().GetSessionOwnerID(r.Context(), sessionID); err == nil && ownerID > 0 {
|
||||
sessionOwnerID = ownerID
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,8 @@ func (s *Server) authorizeSessionRead(
|
||||
SessionOwnerID: sessionOwnerID,
|
||||
ViewerUserID: user.ID,
|
||||
}
|
||||
if s.ctxMgr != nil {
|
||||
if projectID, err := s.ctxMgr.GetSessionProjectID(r.Context(), sessionID); err == nil {
|
||||
if s.ctxMgr() != nil {
|
||||
if projectID, err := s.ctxMgr().GetSessionProjectID(r.Context(), sessionID); err == nil {
|
||||
authCtx.ProjectID = projectID
|
||||
}
|
||||
}
|
||||
@@ -200,10 +200,10 @@ func aggregateCategoryStats(msgs []ctxmgr.ChatMessageRecord) []ctxmgr.ChatCatego
|
||||
}
|
||||
|
||||
func (s *Server) listVisibleSessionMessages(ctx context.Context, authCtx *chatHistoryAuth) ([]ctxmgr.ChatMessageRecord, error) {
|
||||
if s.ctxMgr == nil || authCtx == nil {
|
||||
if s.ctxMgr() == nil || authCtx == nil {
|
||||
return nil, nil
|
||||
}
|
||||
msgs, err := s.ctxMgr.ListSessionMessagesForAuth(ctx, authCtx.SessionID)
|
||||
msgs, err := s.ctxMgr().ListSessionMessagesForAuth(ctx, authCtx.SessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -211,23 +211,23 @@ func (s *Server) listVisibleSessionMessages(ctx context.Context, authCtx *chatHi
|
||||
}
|
||||
|
||||
func (s *Server) resolveTaskMessageScope(ctx context.Context, projectID int64, taskKey string) (*projectinstance.MessageScope, error) {
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
if s.cfg != nil && s.cfg.AllowInMemory {
|
||||
return &projectinstance.MessageScope{ScopeKind: projectinstance.ScopePreActivation}, nil
|
||||
}
|
||||
return nil, projectinstance.ErrTaskScopeNotResolved
|
||||
}
|
||||
return s.instSvc.ResolveMessageScopeForTask(ctx, projectID, taskKey)
|
||||
return s.instSvc().ResolveMessageScopeForTask(ctx, projectID, taskKey)
|
||||
}
|
||||
|
||||
func (s *Server) resolveChatMessageScope(ctx context.Context, projectID int64, activeStageKey string) (*projectinstance.MessageScope, error) {
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
if projectID <= 0 {
|
||||
return &projectinstance.MessageScope{ScopeKind: projectinstance.ScopeNonProjectPrivate}, nil
|
||||
}
|
||||
return &projectinstance.MessageScope{ScopeKind: projectinstance.ScopePreActivation}, nil
|
||||
}
|
||||
return s.instSvc.ResolveMessageScopeForChat(ctx, projectID, activeStageKey)
|
||||
return s.instSvc().ResolveMessageScopeForChat(ctx, projectID, activeStageKey)
|
||||
}
|
||||
|
||||
func (s *Server) applyScopeToMeta(ctx context.Context, meta map[string]any, projectID int64, activeStageKey, taskKey string) error {
|
||||
|
||||
@@ -17,7 +17,7 @@ func (s *Server) handleChatRecategorize(w http.ResponseWriter, r *http.Request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.ctxMgr == nil {
|
||||
if s.ctxMgr() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager unavailable")
|
||||
return
|
||||
}
|
||||
@@ -40,7 +40,7 @@ func (s *Server) handleChatRecategorize(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := s.ctxMgr.RecategorizeMessages(r.Context(), req.SessionID)
|
||||
res, err := s.ctxMgr().RecategorizeMessages(r.Context(), req.SessionID)
|
||||
if err != nil {
|
||||
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
|
||||
"success": false,
|
||||
|
||||
@@ -21,7 +21,7 @@ func (s *Server) handleGetChatTurn(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ctxMgr := s.ctxMgr
|
||||
ctxMgr := s.ctxMgr()
|
||||
if ctxMgr == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager unavailable")
|
||||
return
|
||||
|
||||
@@ -9,21 +9,24 @@ import (
|
||||
// status=processing 且租约过期的回合批量标 failed,防止崩溃/超时后
|
||||
// 幂等键被永久占位。间隔保守(2 分钟),批上限 200。
|
||||
func (s *Server) startChatTurnSweeper() {
|
||||
if s == nil || s.ctxMgr == nil {
|
||||
if s == nil || s.ctxMgr() == nil {
|
||||
return
|
||||
}
|
||||
stopCh, ok := s.chatTurnSweepLoop.start()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
s.chatTurnSweepStop = make(chan struct{})
|
||||
go func() {
|
||||
t := time.NewTicker(2 * time.Minute)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-s.chatTurnSweepStop:
|
||||
case <-stopCh:
|
||||
return
|
||||
case <-t.C:
|
||||
// 独立超时,避免扫描被慢查询拖住 drain 窗口。
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
_, _ = s.ctxMgr.SweepExpiredProcessingTurns(ctx, 200)
|
||||
_, _ = s.ctxMgr().SweepExpiredProcessingTurns(ctx, 200)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ func (s *Server) handleListChatCategories(w http.ResponseWriter, r *http.Request
|
||||
// handleImportChatMessages 一次性批量回填历史消息 (仅允许在 dev/demo 环境下调用)
|
||||
// POST /api/v1/chat/messages/import
|
||||
func (s *Server) handleImportChatMessages(w http.ResponseWriter, r *http.Request) {
|
||||
if s.ctxMgr == nil {
|
||||
if s.ctxMgr() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||
return
|
||||
}
|
||||
@@ -207,7 +207,7 @@ func (s *Server) handleImportChatMessages(w http.ResponseWriter, r *http.Request
|
||||
item.Metadata["created_at"] = item.CreatedAt
|
||||
}
|
||||
|
||||
_, err := s.ctxMgr.RecordUserMessageWithID(
|
||||
_, err := s.ctxMgr().RecordUserMessageWithID(
|
||||
r.Context(),
|
||||
item.SessionID,
|
||||
item.UserID,
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestDocGovCommentsAndAudit_NonMemberIs403_PG(t *testing.T) {
|
||||
|
||||
svc := bizdev.MustNewService(t, cfg)
|
||||
srv := NewServer(cfg, svc)
|
||||
srv.authorizer = authz.NewAuthorizer(pool)
|
||||
srv.setOverrideAuthorizer(authz.NewAuthorizer(pool))
|
||||
|
||||
ctx := context.Background()
|
||||
var projectID int64
|
||||
@@ -75,7 +75,7 @@ func TestDocGovCommentsAndAudit_NonMemberIs403_PG(t *testing.T) {
|
||||
if err := json.NewDecoder(loginW.Body).Decode(&session); err != nil || session.Token == "" || session.User == nil {
|
||||
t.Fatalf("decode outsider session: %v body=%s", err, loginW.Body.String())
|
||||
}
|
||||
if err := srv.authorizer.RequireProjectAccess(ctx, projectID, session.User.ID); err != authz.ErrForbidden {
|
||||
if err := srv.authorizer().RequireProjectAccess(ctx, projectID, session.User.ID); err != authz.ErrForbidden {
|
||||
t.Fatalf("outsider must not be a member of project %d: %v", projectID, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ func (s *Server) resolveDocRevisionRef(ctx context.Context, user *auth.UserAccou
|
||||
if strings.TrimSpace(docID) == "" {
|
||||
return nil, docrevision.ErrInvalidDocID
|
||||
}
|
||||
if s.gov != nil {
|
||||
if doc, err := s.gov.GetDocument(ctx, docID); err == nil && doc != nil {
|
||||
if s.gov() != nil {
|
||||
if doc, err := s.gov().GetDocument(ctx, docID); err == nil && doc != nil {
|
||||
ref := &docRevisionRef{
|
||||
Kind: "controlled", ProjectID: doc.ProjectID, WorkDir: doc.WorkDir,
|
||||
LifecycleState: string(doc.CurrentState), AuthorID: doc.AuthorID,
|
||||
@@ -39,14 +39,14 @@ func (s *Server) resolveDocRevisionRef(ctx context.Context, user *auth.UserAccou
|
||||
return ref, nil
|
||||
}
|
||||
}
|
||||
if content, cErr := s.gov.ReadDocumentContent(ctx, docID, s.dataRoot()); cErr == nil && content != nil {
|
||||
if content, cErr := s.gov().ReadDocumentContent(ctx, docID, s.dataRoot()); cErr == nil && content != nil {
|
||||
ref.Markdown = content.Markdown
|
||||
}
|
||||
return ref, nil
|
||||
}
|
||||
}
|
||||
if s.freeDoc != nil && user != nil {
|
||||
if detail, err := s.freeDoc.GetDocument(ctx, int(user.ID), docID); err == nil && detail != nil {
|
||||
if s.freeDoc() != nil && user != nil {
|
||||
if detail, err := s.freeDoc().GetDocument(ctx, int(user.ID), docID); err == nil && detail != nil {
|
||||
return &docRevisionRef{
|
||||
Kind: "freeform", ProjectID: int64(detail.Document.ProjectID),
|
||||
LifecycleState: "drafting", Markdown: detail.Content, Title: detail.Document.Title,
|
||||
@@ -67,11 +67,11 @@ func (s *Server) authorizeDocRevision(ctx context.Context, user *auth.UserAccoun
|
||||
return docrevision.ErrNotFound
|
||||
}
|
||||
if ref.ProjectID > 0 {
|
||||
if s.authorizer == nil {
|
||||
if s.authorizer() == nil {
|
||||
if s.failClosedAuthz() {
|
||||
return authz.ErrForbidden
|
||||
}
|
||||
} else if err := s.authorizer.RequireProjectAccessOrAdmin(ctx, ref.ProjectID, user.ID, s.isSystemAdminUser(ctx, user)); err != nil {
|
||||
} else if err := s.authorizer().RequireProjectAccessOrAdmin(ctx, ref.ProjectID, user.ID, s.isSystemAdminUser(ctx, user)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if ref.Kind == "controlled" {
|
||||
|
||||
@@ -76,10 +76,10 @@ func TestDocRevision_ControlledDocumentRoutes(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
ctx := context.Background()
|
||||
docID := "doc_proj1_intake"
|
||||
if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底表", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
if _, err := srv.gov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底表", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := srv.rev.OpenTemplateVersion(ctx, docID, "# 摸底\n\n正文\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, ""); err != nil {
|
||||
if _, err := srv.rev().OpenTemplateVersion(ctx, docID, "# 摸底\n\n正文\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
token := loginTestAdmin(t, srv)
|
||||
@@ -102,14 +102,14 @@ func TestDocRevision_PendingCandidateExposedAndDecisionRequiresAuthor(t *testing
|
||||
srv := newTestServer(t)
|
||||
ctx := context.Background()
|
||||
docID := "doc_proj1_pending"
|
||||
if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
if _, err := srv.gov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# base\n\n法规 GB9706\n", docrevision.ActorSnapshot{}, 1, "")
|
||||
st, err := srv.rev().OpenTemplateVersion(ctx, docID, "# base\n\n法规 GB9706\n", docrevision.ActorSnapshot{}, 1, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cand, _, err := srv.rev.CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{
|
||||
cand, _, err := srv.rev().CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{
|
||||
DocID: docID, RunID: "high-1", InputSnapshotID: st.HeadSnapshotID,
|
||||
OutputMarkdown: "# base\n\n", ActorKind: docrevision.ActorAgentSquad,
|
||||
})
|
||||
@@ -141,14 +141,14 @@ func TestDocRevision_PendingCandidateExposedAndDecisionRequiresAuthor(t *testing
|
||||
|
||||
// Non-author cannot decide (author is 1=admin in this fixture — use author 99 doc).
|
||||
docID2 := "doc_proj1_pending_na"
|
||||
if _, err := srv.gov.CreateOrGetDocument(ctx, docID2, 1, "research", "研", "V0.5", "", 99, "other", 2, "r", 3, "a"); err != nil {
|
||||
if _, err := srv.gov().CreateOrGetDocument(ctx, docID2, 1, "research", "研", "V0.5", "", 99, "other", 2, "r", 3, "a"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st2, err := srv.rev.OpenTemplateVersion(ctx, docID2, "# x\n\n法规\n", docrevision.ActorSnapshot{}, 1, "")
|
||||
st2, err := srv.rev().OpenTemplateVersion(ctx, docID2, "# x\n\n法规\n", docrevision.ActorSnapshot{}, 1, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cand2, _, err := srv.rev.CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{
|
||||
cand2, _, err := srv.rev().CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{
|
||||
DocID: docID2, RunID: "high-2", InputSnapshotID: st2.HeadSnapshotID,
|
||||
OutputMarkdown: "# x\n\n", ActorKind: docrevision.ActorAgentSquad,
|
||||
})
|
||||
@@ -191,15 +191,15 @@ func TestDocRevision_CommitWithMissingProposalReturnsGovernanceError(t *testing.
|
||||
srv := newTestServer(t)
|
||||
ctx := context.Background()
|
||||
docID := "doc_proj1_gov_prop"
|
||||
if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
if _, err := srv.gov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# base\n\n法规 GB9706\n", docrevision.ActorSnapshot{}, 1, "")
|
||||
st, err := srv.rev().OpenTemplateVersion(ctx, docID, "# base\n\n法规 GB9706\n", docrevision.ActorSnapshot{}, 1, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ghostProp := "prop_does_not_exist"
|
||||
cand, _, err := srv.rev.CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{
|
||||
cand, _, err := srv.rev().CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{
|
||||
DocID: docID, RunID: "gov-miss-1", ProposalID: ghostProp,
|
||||
InputSnapshotID: st.HeadSnapshotID,
|
||||
OutputMarkdown: "# base\n\n", ActorKind: docrevision.ActorAgentSquad,
|
||||
@@ -230,7 +230,7 @@ func TestDocRevision_CommitWithMissingProposalReturnsGovernanceError(t *testing.
|
||||
t.Fatalf("code=%v body=%#v", body["code"], body)
|
||||
}
|
||||
|
||||
gotCand, err := srv.rev.GetCandidate(ctx, docID, cand.ID)
|
||||
gotCand, err := srv.rev().GetCandidate(ctx, docID, cand.ID)
|
||||
if err != nil || gotCand.Status != docrevision.CandidateCommitted {
|
||||
t.Fatalf("ledger must stay committed after governance failure: %+v err=%v", gotCand, err)
|
||||
}
|
||||
@@ -257,21 +257,21 @@ func TestDocRevision_DecisionIdempotentWithRealProposal(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
ctx := context.Background()
|
||||
docID := "doc_proj1_gov_prop_ok"
|
||||
if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
if _, err := srv.gov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prop, err := srv.gov.ProposeModification(ctx, docID, "agent", "改", "影响", "风险")
|
||||
prop, err := srv.gov().ProposeModification(ctx, docID, "agent", "改", "影响", "风险")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := srv.gov.DecideModificationProposal(ctx, prop.ProposalID, 1, true, "同意"); err != nil {
|
||||
if _, err := srv.gov().DecideModificationProposal(ctx, prop.ProposalID, 1, true, "同意"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# base\n\n法规 GB9706\n", docrevision.ActorSnapshot{}, 1, "")
|
||||
st, err := srv.rev().OpenTemplateVersion(ctx, docID, "# base\n\n法规 GB9706\n", docrevision.ActorSnapshot{}, 1, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cand, _, err := srv.rev.CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{
|
||||
cand, _, err := srv.rev().CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{
|
||||
DocID: docID, RunID: "gov-ok-1", ProposalID: prop.ProposalID,
|
||||
InputSnapshotID: st.HeadSnapshotID,
|
||||
OutputMarkdown: "# base\n\n", ActorKind: docrevision.ActorAgentSquad,
|
||||
@@ -295,7 +295,7 @@ func TestDocRevision_DecisionIdempotentWithRealProposal(t *testing.T) {
|
||||
t.Fatalf("%s decision want 200 got %d %s", label, w.Code, w.Body.String())
|
||||
}
|
||||
if i == 0 {
|
||||
got, err := srv.rev.GetCandidate(ctx, docID, cand.ID)
|
||||
got, err := srv.rev().GetCandidate(ctx, docID, cand.ID)
|
||||
if err != nil || got.Status != docrevision.CandidateCommitted {
|
||||
t.Fatalf("after first: %+v err=%v", got, err)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
// 隔离写权门闩:跳过项目成员库检查,只验证 AuthorID。禁止依赖共享库或默认拒绝。
|
||||
srv.cfg.AllowInMemory = true
|
||||
srv.authorizer = authz.NewAuthorizer(nil)
|
||||
srv.setOverrideAuthorizer(authz.NewAuthorizer(nil))
|
||||
ctx := context.Background()
|
||||
const (
|
||||
authorID int64 = 170
|
||||
@@ -22,7 +22,7 @@ func TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite(t *testing.T) {
|
||||
strangerID int64 = 99
|
||||
docID = "doc_proj_duty_write"
|
||||
)
|
||||
if _, err := srv.gov.CreateOrGetDocument(
|
||||
if _, err := srv.gov().CreateOrGetDocument(
|
||||
ctx, docID, 6145, "intake", "摸底", "V0.1", "",
|
||||
authorID, "编制人A", reviewerID, "审核人B", 0, "",
|
||||
); err != nil {
|
||||
|
||||
@@ -39,10 +39,10 @@ func (s *Server) registerDocumentRevisionRoutes(mux *http.ServeMux) {
|
||||
}
|
||||
|
||||
func (s *Server) docRev() *docrevision.Service {
|
||||
if s == nil || s.svc == nil {
|
||||
if s == nil || s.svc() == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rev
|
||||
return s.rev()
|
||||
}
|
||||
|
||||
func (s *Server) handleDocRevisionState(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -213,9 +213,9 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
|
||||
// 1) Controlled: author duty + lease WITHOUT writing body (fail-closed before ledger).
|
||||
// This UI has no edit-session heartbeat: release the request-scoped human lease on every exit
|
||||
// (success, 409, projection failure) so a 15m residual cannot block a later squad run.
|
||||
if kind == "controlled" && s.gov != nil {
|
||||
if kind == "controlled" && s.gov() != nil {
|
||||
// ActorIsMember=true:上游 enforceDocGovWorkflowAction(Edit) 已按成员级放行。
|
||||
if err := s.gov.AssertHumanCanEdit(r.Context(), docID, user.ID, vocabulary.RoleAuthor, true); err != nil {
|
||||
if err := s.gov().AssertHumanCanEdit(r.Context(), docID, user.ID, vocabulary.RoleAuthor, true); err != nil {
|
||||
writeDocGovError(w, err)
|
||||
return
|
||||
}
|
||||
@@ -224,7 +224,7 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
|
||||
holderID = "human_anon"
|
||||
}
|
||||
defer func() {
|
||||
_, _ = s.gov.ReleaseLease(context.Background(), docID, string(vocabulary.LeaseHolderHuman), holderID)
|
||||
_, _ = s.gov().ReleaseLease(context.Background(), docID, string(vocabulary.LeaseHolderHuman), holderID)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -277,8 +277,8 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
|
||||
// same transaction as the ledger (document_content_projections). Failure keeps that row
|
||||
// pending and returns DOCUMENT_PROJECTION_FAILED. The same Idempotency-Key retries
|
||||
// projection without a second revision. Only a successful projection acks the row.
|
||||
if kind == "controlled" && s.gov != nil {
|
||||
if _, err := s.gov.SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{
|
||||
if kind == "controlled" && s.gov() != nil {
|
||||
if _, err := s.gov().SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{
|
||||
DocID: docID,
|
||||
ActorID: user.ID,
|
||||
ActorRole: vocabulary.RoleAuthor,
|
||||
@@ -304,9 +304,9 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
}
|
||||
if kind == "freeform" && s.freeDoc != nil {
|
||||
if kind == "freeform" && s.freeDoc() != nil {
|
||||
md := body.Markdown
|
||||
if _, err := s.freeDoc.UpdateDocument(r.Context(), int(user.ID), docID, freedoc.UpdateFreeformDocRequest{
|
||||
if _, err := s.freeDoc().UpdateDocument(r.Context(), int(user.ID), docID, freedoc.UpdateFreeformDocRequest{
|
||||
ContentMarkdown: &md,
|
||||
ChangeSummary: "revision ledger human save",
|
||||
}); err != nil {
|
||||
@@ -399,8 +399,8 @@ func (s *Server) handleDocRevisionCandidateDecision(w http.ResponseWriter, r *ht
|
||||
})
|
||||
return
|
||||
}
|
||||
if s.gov != nil && st != nil {
|
||||
if err := s.gov.MarkDocumentContentUpdated(r.Context(), docID, st.HeadSHA256, false); err != nil {
|
||||
if s.gov() != nil && st != nil {
|
||||
if err := s.gov().MarkDocumentContentUpdated(r.Context(), docID, st.HeadSHA256, false); err != nil {
|
||||
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
|
||||
"code": "DOCUMENT_PROJECTION_FAILED",
|
||||
"error": err.Error(),
|
||||
@@ -410,7 +410,7 @@ func (s *Server) handleDocRevisionCandidateDecision(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
if body.Decision == "commit" && cand != nil && strings.TrimSpace(cand.ProposalID) != "" {
|
||||
if err := s.gov.MarkProposalApplied(r.Context(), cand.ProposalID); err != nil {
|
||||
if err := s.gov().MarkProposalApplied(r.Context(), cand.ProposalID); err != nil {
|
||||
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
|
||||
"code": "DOCUMENT_GOVERNANCE_FAILED",
|
||||
"error": err.Error(),
|
||||
@@ -448,7 +448,7 @@ func (s *Server) handlePatchNotification(w http.ResponseWriter, r *http.Request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.notifySvc == nil || !s.notifySvc.Available() {
|
||||
if s.notifySvc() == nil || !s.notifySvc().Available() {
|
||||
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "notifications unavailable"})
|
||||
return
|
||||
}
|
||||
@@ -460,7 +460,7 @@ func (s *Server) handlePatchNotification(w http.ResponseWriter, r *http.Request)
|
||||
writeJSONResponse(w, http.StatusBadRequest, map[string]any{"error": "invalid json"})
|
||||
return
|
||||
}
|
||||
if err := s.notifySvc.UpdateNotificationStatus(r.Context(), user.ID, id, body.Status); err != nil {
|
||||
if err := s.notifySvc().UpdateNotificationStatus(r.Context(), user.ID, id, body.Status); err != nil {
|
||||
writeJSONResponse(w, http.StatusNotFound, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -30,10 +30,10 @@ func TestDocRevision_PatchOrder_AssertBeforeLedger_ProjectionFailureKeepsLedger(
|
||||
workDir := filepath.Join(blocker, "ws-proj")
|
||||
docID := "doc_proj1_patch_order"
|
||||
|
||||
if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
if _, err := srv.gov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
|
||||
st, err := srv.rev().OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func TestDocRevision_PatchOrder_AssertBeforeLedger_ProjectionFailureKeepsLedger(
|
||||
if payload["code"] != "DOCUMENT_PROJECTION_FAILED" {
|
||||
t.Fatalf("code want DOCUMENT_PROJECTION_FAILED got %#v", payload["code"])
|
||||
}
|
||||
after, err := srv.rev.GetRevisionState(ctx, docID)
|
||||
after, err := srv.rev().GetRevisionState(ctx, docID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -77,13 +77,13 @@ func TestDocRevision_PatchOrder_LockedDocDoesNotAdvanceLedger(t *testing.T) {
|
||||
_ = os.MkdirAll(workDir, 0o755)
|
||||
|
||||
// Author is admin (1); lock via submit so drafting edit is denied regardless of membership.
|
||||
if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
if _, err := srv.gov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := srv.gov.SubmitForReview(ctx, docID, 1, vocabulary.RoleAuthor, "admin"); err != nil {
|
||||
if _, err := srv.gov().SubmitForReview(ctx, docID, 1, vocabulary.RoleAuthor, "admin"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# L\n\nx\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
|
||||
st, err := srv.rev().OpenTemplateVersion(ctx, docID, "# L\n\nx\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func TestDocRevision_PatchOrder_LockedDocDoesNotAdvanceLedger(t *testing.T) {
|
||||
if w.Code == http.StatusOK {
|
||||
t.Fatalf("locked doc must not save successfully: %s", w.Body.String())
|
||||
}
|
||||
after, err := srv.rev.GetRevisionState(ctx, docID)
|
||||
after, err := srv.rev().GetRevisionState(ctx, docID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -116,10 +116,10 @@ func TestDocRevision_PatchOrder_SaveDocumentContentSetsAuthorAudit(t *testing.T)
|
||||
_ = os.MkdirAll(workDir, 0o755)
|
||||
docID := "doc_proj1_patch_audit"
|
||||
|
||||
if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
if _, err := srv.gov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
|
||||
st, err := srv.rev().OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func TestDocRevision_PatchOrder_SaveDocumentContentSetsAuthorAudit(t *testing.T)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("want 200 got %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
doc, err := srv.gov.GetDocument(ctx, docID)
|
||||
doc, err := srv.gov().GetDocument(ctx, docID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -159,10 +159,10 @@ func TestDocRevision_PatchReleasesHumanLeaseOnHeadConflict(t *testing.T) {
|
||||
_ = os.MkdirAll(workDir, 0o755)
|
||||
docID := "doc_proj1_patch_conflict_lease"
|
||||
|
||||
if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
if _, err := srv.gov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
|
||||
st, err := srv.rev().OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -196,14 +196,14 @@ func TestDocRevision_PatchReleasesHumanLeaseOnHeadConflict(t *testing.T) {
|
||||
func assertHumanLeaseReleasedSquadCanAcquire(t *testing.T, srv *Server, docID string) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
doc, err := srv.gov.GetDocument(ctx, docID)
|
||||
doc, err := srv.gov().GetDocument(ctx, docID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if doc.HasActiveLease(time.Now()) {
|
||||
t.Fatalf("human lease must be released after PATCH, holder=%s/%s", doc.LeaseHolderType, doc.LeaseHolderID)
|
||||
}
|
||||
leased, err := srv.gov.AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
||||
leased, err := srv.gov().AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
||||
DocID: docID,
|
||||
HolderType: docgov.LeaseHolderSquad,
|
||||
HolderID: "run_after_human_save",
|
||||
@@ -215,7 +215,7 @@ func assertHumanLeaseReleasedSquadCanAcquire(t *testing.T, srv *Server, docID st
|
||||
if leased.LeaseHolderType != docgov.LeaseHolderSquad {
|
||||
t.Fatalf("want squad holder, got %s", leased.LeaseHolderType)
|
||||
}
|
||||
if _, err := srv.gov.ReleaseLease(ctx, docID, string(docgov.LeaseHolderSquad), "run_after_human_save"); err != nil {
|
||||
if _, err := srv.gov().ReleaseLease(ctx, docID, string(docgov.LeaseHolderSquad), "run_after_human_save"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -231,10 +231,10 @@ func TestDocRevision_PatchOrder_RetryProjectsWithoutSecondLedgerCommit(t *testin
|
||||
workDir := filepath.Join(blocker, "ws-retry")
|
||||
docID := "doc_proj1_patch_retry"
|
||||
|
||||
if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
if _, err := srv.gov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := srv.rev.OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
|
||||
st, err := srv.rev().OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -256,11 +256,11 @@ func TestDocRevision_PatchOrder_RetryProjectsWithoutSecondLedgerCommit(t *testin
|
||||
if first.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("first projection want 500 got %d %s", first.Code, first.Body.String())
|
||||
}
|
||||
pending, ok := srv.rev.ContentProjectionPending(docID)
|
||||
pending, ok := srv.rev().ContentProjectionPending(docID)
|
||||
if !ok {
|
||||
t.Fatal("ledger commit must leave a content projection pending")
|
||||
}
|
||||
afterFail, err := srv.rev.GetRevisionState(ctx, docID)
|
||||
afterFail, err := srv.rev().GetRevisionState(ctx, docID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -278,14 +278,14 @@ func TestDocRevision_PatchOrder_RetryProjectsWithoutSecondLedgerCommit(t *testin
|
||||
if second.Code != http.StatusOK {
|
||||
t.Fatalf("retry want 200 got %d %s", second.Code, second.Body.String())
|
||||
}
|
||||
afterRetry, err := srv.rev.GetRevisionState(ctx, docID)
|
||||
afterRetry, err := srv.rev().GetRevisionState(ctx, docID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if afterRetry.HeadRevisionNo != afterFail.HeadRevisionNo || afterRetry.HeadSnapshotID != afterFail.HeadSnapshotID {
|
||||
t.Fatalf("retry must not write a second revision: fail=%+v retry=%+v", afterFail, afterRetry)
|
||||
}
|
||||
if _, still := srv.rev.ContentProjectionPending(docID); still {
|
||||
if _, still := srv.rev().ContentProjectionPending(docID); still {
|
||||
t.Fatal("successful projection must ack the pending row")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(workDir, "output.md")); err != nil {
|
||||
|
||||
@@ -23,12 +23,12 @@ func (s *Server) handleReassignDocTaskDuties(w http.ResponseWriter, r *http.Requ
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||
return
|
||||
}
|
||||
|
||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
isCreator, _ := s.pwSvc().IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
canAssign, permErr := s.userHasSystemPermission(r.Context(), user, "project:assign_lead")
|
||||
if permErr != nil {
|
||||
if s.failClosedAuthz() {
|
||||
@@ -46,7 +46,7 @@ func (s *Server) handleReassignDocTaskDuties(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
req.TaskKey = taskKey
|
||||
|
||||
settings, err := s.instSvc.ReassignDocTaskDuties(r.Context(), projectID, user.ID, allowed, req)
|
||||
settings, err := s.instSvc().ReassignDocTaskDuties(r.Context(), projectID, user.ID, allowed, req)
|
||||
if err != nil {
|
||||
writeProjectInstanceError(w, err)
|
||||
return
|
||||
|
||||
@@ -48,7 +48,7 @@ func (s *Server) allowFreeLaneChatInitiation(projectID, userID int64) bool {
|
||||
// maybeAutoClaimOnView records an author view (and claim when non-template) after a
|
||||
// successful controlled-content read. Failures are logged and never block the read.
|
||||
func (s *Server) maybeAutoClaimOnView(ctx context.Context, user *auth.UserAccount, docID string) {
|
||||
if s == nil || s.gov == nil || user == nil || docID == "" {
|
||||
if s == nil || s.gov() == nil || user == nil || docID == "" {
|
||||
return
|
||||
}
|
||||
if !autoClaimOnViewEnabled() {
|
||||
@@ -58,7 +58,7 @@ func (s *Server) maybeAutoClaimOnView(ctx context.Context, user *auth.UserAccoun
|
||||
if role != vocabulary.RoleAuthor {
|
||||
return
|
||||
}
|
||||
if _, err := s.gov.RecordDocumentViewed(ctx, docID, user.ID, role, user.Name); err != nil {
|
||||
if _, err := s.gov().RecordDocumentViewed(ctx, docID, user.ID, role, user.Name); err != nil {
|
||||
slog.Warn("docgov_auto_claim_on_view_failed", "doc_id", docID, "user_id", user.ID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestCreateFreeformDoc_RejectsDirectProjectShared(t *testing.T) {
|
||||
func TestCreateFreeformDoc_FailClosedWhenAuthorizerNil(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
srv.cfg.AllowInMemory = false
|
||||
srv.authorizer = nil
|
||||
srv.setOverrideAuthorizer(nil)
|
||||
token := loginTestAdmin(t, srv)
|
||||
w := postFreeformCreate(t, srv, token, map[string]any{
|
||||
"project_id": 1,
|
||||
|
||||
@@ -83,7 +83,7 @@ func TestCreateFreeformDoc_NonMemberForbidden_PG(t *testing.T) {
|
||||
|
||||
svc := bizdev.MustNewService(t, cfg)
|
||||
srv := NewServer(cfg, svc)
|
||||
srv.authorizer = authz.NewAuthorizer(pool)
|
||||
srv.setOverrideAuthorizer(authz.NewAuthorizer(pool))
|
||||
|
||||
uname := fmt.Sprintf("outsider_ff_%d", time.Now().UnixNano())
|
||||
pass := "OutsiderPass123!"
|
||||
@@ -111,7 +111,7 @@ func TestCreateFreeformDoc_NonMemberForbidden_PG(t *testing.T) {
|
||||
t.Fatalf("decode outsider session: %v body=%s", err, loginW.Body.String())
|
||||
}
|
||||
|
||||
if err := srv.authorizer.RequireProjectAccess(context.Background(), projectID, session.User.ID); err != authz.ErrForbidden {
|
||||
if err := srv.authorizer().RequireProjectAccess(context.Background(), projectID, session.User.ID); err != authz.ErrForbidden {
|
||||
t.Skipf("registered user unexpectedly has access to project %d: %v", projectID, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ func extractDocIDFromPath(r *http.Request, actionSuffix string) string {
|
||||
|
||||
// POST /api/v1/freeform-documents — 直接创建(上传附件注册服务端事实源,仅 my_private)
|
||||
func (s *Server) handleCreateFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||
if s.freeDoc == nil {
|
||||
if s.freeDoc() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||
return
|
||||
}
|
||||
@@ -72,17 +72,17 @@ func (s *Server) handleCreateFreeformDoc(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
req.Space = "my_private"
|
||||
|
||||
if s.authorizer == nil {
|
||||
if s.authorizer() == nil {
|
||||
if s.failClosedAuthz() {
|
||||
writeAuthzError(w, authz.ErrForbidden)
|
||||
return
|
||||
}
|
||||
} else if err := s.authorizer.RequireProjectAccessOrAdmin(r.Context(), int64(req.ProjectID), user.ID, s.isSystemAdminUser(r.Context(), user)); err != nil {
|
||||
} else if err := s.authorizer().RequireProjectAccessOrAdmin(r.Context(), int64(req.ProjectID), user.ID, s.isSystemAdminUser(r.Context(), user)); err != nil {
|
||||
writeAuthzError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
detail, err := s.freeDoc.CreateDocument(r.Context(), int(user.ID), req)
|
||||
detail, err := s.freeDoc().CreateDocument(r.Context(), int(user.ID), req)
|
||||
if err != nil {
|
||||
if errors.Is(err, freedoc.ErrInvalidParam) {
|
||||
writeJSONResponse(w, http.StatusBadRequest, map[string]any{
|
||||
@@ -106,7 +106,7 @@ func (s *Server) handleCreateFreeformDoc(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// POST /api/v1/freeform-documents/from-message
|
||||
func (s *Server) handleCreateFreeformDocFromMessage(w http.ResponseWriter, r *http.Request) {
|
||||
if s.freeDoc == nil {
|
||||
if s.freeDoc() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||
return
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func (s *Server) handleCreateFreeformDocFromMessage(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
detail, isExisting, err := s.freeDoc.CreateFromMessage(r.Context(), userID, req)
|
||||
detail, isExisting, err := s.freeDoc().CreateFromMessage(r.Context(), userID, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, freedoc.ErrMessageNotExportable) {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]any{
|
||||
@@ -168,7 +168,7 @@ func (s *Server) handleCreateFreeformDocFromMessage(w http.ResponseWriter, r *ht
|
||||
|
||||
// GET /api/v1/freeform-documents
|
||||
func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request) {
|
||||
if s.freeDoc == nil {
|
||||
if s.freeDoc() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||
return
|
||||
}
|
||||
@@ -188,7 +188,7 @@ func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
list, err := s.freeDoc.ListDocuments(r.Context(), userID, projectID, space)
|
||||
list, err := s.freeDoc().ListDocuments(r.Context(), userID, projectID, space)
|
||||
if err != nil {
|
||||
writeJSONResponse(w, http.StatusInternalServerError, map[string]any{
|
||||
"error_code": "LIST_FAILED",
|
||||
@@ -208,7 +208,7 @@ func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// GET /api/v1/freeform-documents/{doc_id}
|
||||
func (s *Server) handleGetFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||
if s.freeDoc == nil {
|
||||
if s.freeDoc() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||
return
|
||||
}
|
||||
@@ -226,7 +226,7 @@ func (s *Server) handleGetFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
detail, err := s.freeDoc.GetDocument(r.Context(), userID, docID)
|
||||
detail, err := s.freeDoc().GetDocument(r.Context(), userID, docID)
|
||||
if err != nil {
|
||||
if errors.Is(err, freedoc.ErrNotFound) {
|
||||
writeJSONResponse(w, http.StatusNotFound, map[string]any{
|
||||
@@ -257,7 +257,7 @@ func (s *Server) handleGetFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// PUT /api/v1/freeform-documents/{doc_id}
|
||||
func (s *Server) handleUpdateFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||
if s.freeDoc == nil {
|
||||
if s.freeDoc() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||
return
|
||||
}
|
||||
@@ -294,7 +294,7 @@ func (s *Server) handleUpdateFreeformDoc(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
detail, err := s.freeDoc.UpdateDocument(r.Context(), userID, docID, req)
|
||||
detail, err := s.freeDoc().UpdateDocument(r.Context(), userID, docID, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, freedoc.ErrNotFound) {
|
||||
writeJSONResponse(w, http.StatusNotFound, map[string]any{
|
||||
@@ -333,7 +333,7 @@ func (s *Server) handleUpdateFreeformDoc(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// POST /api/v1/freeform-documents/{doc_id}/share
|
||||
func (s *Server) handleShareFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||
if s.freeDoc == nil {
|
||||
if s.freeDoc() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||
return
|
||||
}
|
||||
@@ -351,7 +351,7 @@ func (s *Server) handleShareFreeformDoc(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := s.freeDoc.ShareDocument(r.Context(), userID, docID)
|
||||
doc, err := s.freeDoc().ShareDocument(r.Context(), userID, docID)
|
||||
if err != nil {
|
||||
if errors.Is(err, freedoc.ErrNotFound) {
|
||||
writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"})
|
||||
@@ -374,7 +374,7 @@ func (s *Server) handleShareFreeformDoc(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// POST /api/v1/freeform-documents/{doc_id}/unshare
|
||||
func (s *Server) handleUnshareFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||
if s.freeDoc == nil {
|
||||
if s.freeDoc() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||
return
|
||||
}
|
||||
@@ -392,7 +392,7 @@ func (s *Server) handleUnshareFreeformDoc(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := s.freeDoc.UnshareDocument(r.Context(), userID, docID)
|
||||
doc, err := s.freeDoc().UnshareDocument(r.Context(), userID, docID)
|
||||
if err != nil {
|
||||
if errors.Is(err, freedoc.ErrNotFound) {
|
||||
writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"})
|
||||
@@ -415,7 +415,7 @@ func (s *Server) handleUnshareFreeformDoc(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// DELETE /api/v1/freeform-documents/{doc_id}
|
||||
func (s *Server) handleDeleteFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||
if s.freeDoc == nil {
|
||||
if s.freeDoc() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||
return
|
||||
}
|
||||
@@ -433,7 +433,7 @@ func (s *Server) handleDeleteFreeformDoc(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
err := s.freeDoc.DeleteDocument(r.Context(), userID, docID)
|
||||
err := s.freeDoc().DeleteDocument(r.Context(), userID, docID)
|
||||
if err != nil {
|
||||
if errors.Is(err, freedoc.ErrNotFound) {
|
||||
writeJSONResponse(w, http.StatusNotFound, map[string]any{"error_code": "DOC_NOT_FOUND", "message": "文档不存在"})
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// handleListChatSessions 分页查询用户/项目关联的历史会话列表
|
||||
// user_id 仅允许等于当前认证用户本人(或系统管理员),防止水平越权枚举他人会话。
|
||||
func (s *Server) handleListChatSessions(w http.ResponseWriter, r *http.Request) {
|
||||
if s.ctxMgr == nil {
|
||||
if s.ctxMgr() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||
return
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func (s *Server) handleListChatSessions(w http.ResponseWriter, r *http.Request)
|
||||
offset = 0
|
||||
}
|
||||
|
||||
sessions, err := s.ctxMgr.ListChatSessions(r.Context(), ctxmgr.SessionFilter{
|
||||
sessions, err := s.ctxMgr().ListChatSessions(r.Context(), ctxmgr.SessionFilter{
|
||||
UserID: userID,
|
||||
ProjectID: projectID,
|
||||
Limit: limit,
|
||||
@@ -71,7 +71,7 @@ func (s *Server) handleListChatSessions(w http.ResponseWriter, r *http.Request)
|
||||
// handleUpsertChatSession 注册或更新会话元数据(立项/成员加入时同步)。
|
||||
// 默认归属认证用户本人;项目管理员可为其他成员代注册(防水平越权)。
|
||||
func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request) {
|
||||
if s.ctxMgr == nil {
|
||||
if s.ctxMgr() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||
return
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request)
|
||||
})
|
||||
return
|
||||
}
|
||||
can, err := s.authzSvc.CanManageProjectMembers(r.Context(), req.ProjectID, user.ID, user.RoleID)
|
||||
can, err := s.authzSvc().CanManageProjectMembers(r.Context(), req.ProjectID, user.ID, user.RoleID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -114,7 +114,7 @@ func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request)
|
||||
ownerID = req.UserID
|
||||
}
|
||||
|
||||
summary, err := s.ctxMgr.EnsureChatSessionOwned(r.Context(), req.SessionID, ownerID, req.ProjectID, req.Title)
|
||||
summary, err := s.ctxMgr().EnsureChatSessionOwned(r.Context(), req.SessionID, ownerID, req.ProjectID, req.Title)
|
||||
if err != nil {
|
||||
if errors.Is(err, ctxmgr.ErrSessionOwnerMismatch) {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
@@ -132,7 +132,7 @@ func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// handleGetChatMessages 分页获取指定会话的历史多轮消息记录
|
||||
func (s *Server) handleGetChatMessages(w http.ResponseWriter, r *http.Request) {
|
||||
if s.ctxMgr == nil {
|
||||
if s.ctxMgr() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||
return
|
||||
}
|
||||
@@ -183,7 +183,7 @@ func (s *Server) handleRecommendCases(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.ctxMgr == nil {
|
||||
if s.ctxMgr() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||
return
|
||||
}
|
||||
@@ -209,7 +209,7 @@ func (s *Server) handleRecommendCases(w http.ResponseWriter, r *http.Request) {
|
||||
minScore = 0.50
|
||||
}
|
||||
|
||||
cases, err := s.ctxMgr.SearchSimilarCases(r.Context(), ctxmgr.CaseSearchOptions{
|
||||
cases, err := s.ctxMgr().SearchSimilarCases(r.Context(), ctxmgr.CaseSearchOptions{
|
||||
Query: query,
|
||||
TaskKey: taskKey,
|
||||
ExcludeDoc: excludeDoc,
|
||||
|
||||
@@ -133,7 +133,7 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if req.ProjectID > 0 {
|
||||
if !s.isSystemAdminUser(r.Context(), user) {
|
||||
if err := s.authorizer.RequireProjectAccess(r.Context(), req.ProjectID, user.ID); err != nil {
|
||||
if err := s.authorizer().RequireProjectAccess(r.Context(), req.ProjectID, user.ID); err != nil {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
"error_code": "ERR_FORBIDDEN",
|
||||
"message": "无权访问该项目",
|
||||
@@ -153,10 +153,10 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 0. 如果关联了项目,在 projectworkflow 服务中记录运行实例
|
||||
var runRecord *projectworkflow.WorkflowRun
|
||||
if s.pwSvc != nil && req.ProjectID > 0 {
|
||||
if s.pwSvc() != nil && req.ProjectID > 0 {
|
||||
wfKey := req.WorkflowKey
|
||||
if wfKey == "" {
|
||||
if p, _ := s.pwSvc.GetProject(r.Context(), req.ProjectID, req.UserID); p != nil {
|
||||
if p, _ := s.pwSvc().GetProject(r.Context(), req.ProjectID, req.UserID); p != nil {
|
||||
wfKey = p.ActiveWorkflowKey
|
||||
}
|
||||
}
|
||||
@@ -167,7 +167,7 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
|
||||
"fields": req.Fields,
|
||||
"doc_id": req.DocID,
|
||||
}
|
||||
runRecord, _ = s.pwSvc.CreateWorkflowRun(r.Context(), &projectworkflow.WorkflowRun{
|
||||
runRecord, _ = s.pwSvc().CreateWorkflowRun(r.Context(), &projectworkflow.WorkflowRun{
|
||||
ProjectID: req.ProjectID,
|
||||
WorkflowKey: wfKey,
|
||||
Status: "running",
|
||||
@@ -207,7 +207,7 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
if err != nil {
|
||||
if runRecord != nil {
|
||||
_ = s.pwSvc.UpdateWorkflowRun(r.Context(), runRecord.RunID, "failed", "", "", err.Error())
|
||||
_ = s.pwSvc().UpdateWorkflowRun(r.Context(), runRecord.RunID, "failed", "", "", err.Error())
|
||||
}
|
||||
writeTaskError(w, err, stageRes)
|
||||
return
|
||||
@@ -222,7 +222,7 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
|
||||
if outSummary == "" {
|
||||
outSummary = stageRes.StageTitle
|
||||
}
|
||||
_ = s.pwSvc.UpdateWorkflowRun(r.Context(), runRecord.RunID, status, outDocID, outSummary, "")
|
||||
_ = s.pwSvc().UpdateWorkflowRun(r.Context(), runRecord.RunID, status, outDocID, outSummary, "")
|
||||
}
|
||||
writeJSONResponse(w, http.StatusOK, stageRes)
|
||||
return
|
||||
@@ -257,7 +257,7 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
if err != nil {
|
||||
if runRecord != nil {
|
||||
_ = s.pwSvc.UpdateWorkflowRun(r.Context(), runRecord.RunID, "failed", "", "", err.Error())
|
||||
_ = s.pwSvc().UpdateWorkflowRun(r.Context(), runRecord.RunID, "failed", "", "", err.Error())
|
||||
}
|
||||
writeTaskError(w, err, taskRes)
|
||||
return
|
||||
@@ -269,7 +269,7 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
|
||||
if taskRes.DocRecord != nil {
|
||||
outDocID = taskRes.DocRecord.DocID
|
||||
}
|
||||
_ = s.pwSvc.UpdateWorkflowRun(r.Context(), runRecord.RunID, status, outDocID, taskRes.ArtifactTitle, "")
|
||||
_ = s.pwSvc().UpdateWorkflowRun(r.Context(), runRecord.RunID, status, outDocID, taskRes.ArtifactTitle, "")
|
||||
}
|
||||
|
||||
// 隐藏不必要的中间大体积字节
|
||||
|
||||
@@ -36,12 +36,12 @@ func (s *Server) handleGetUserContext(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
projectID, _ := strconv.ParseInt(projectIDStr, 10, 64)
|
||||
|
||||
if s.ctxMgr == nil {
|
||||
if s.ctxMgr() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
uCtx, err := s.ctxMgr.GetUserContext(r.Context(), projectID, userID, sessionID)
|
||||
uCtx, err := s.ctxMgr().GetUserContext(r.Context(), projectID, userID, sessionID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, "get user context failed: "+err.Error())
|
||||
return
|
||||
@@ -72,7 +72,7 @@ func (s *Server) handleUpdateUserPreference(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
if s.ctxMgr == nil {
|
||||
if s.ctxMgr() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||
return
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func (s *Server) handleUpdateUserPreference(w http.ResponseWriter, r *http.Reque
|
||||
CustomInstructions: req.CustomInstructions,
|
||||
}
|
||||
|
||||
if err := s.ctxMgr.UpdateUserPreference(r.Context(), pref); err != nil {
|
||||
if err := s.ctxMgr().UpdateUserPreference(r.Context(), pref); err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, "save user preference failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
@@ -118,12 +118,12 @@ func (s *Server) handleGetProjectContext(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if s.ctxMgr == nil {
|
||||
if s.ctxMgr() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
pCtx, err := s.ctxMgr.GetProjectContext(r.Context(), projectID)
|
||||
pCtx, err := s.ctxMgr().GetProjectContext(r.Context(), projectID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, "get project context failed: "+err.Error())
|
||||
return
|
||||
|
||||
@@ -28,7 +28,7 @@ func (s *Server) handleDocGovGetDocument(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func (s *Server) handleDocGovGetDocument(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := s.gov.GetDocument(r.Context(), docID)
|
||||
doc, err := s.gov().GetDocument(r.Context(), docID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusNotFound, "get document failed: "+err.Error())
|
||||
return
|
||||
@@ -63,12 +63,12 @@ func (s *Server) handleDocGovClaim(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := s.gov.RecordDocumentViewed(r.Context(), req.DocID, user.ID, s.resolveDocGovRole(r.Context(), req.DocID, user), user.Name)
|
||||
doc, err := s.gov().RecordDocumentViewed(r.Context(), req.DocID, user.ID, s.resolveDocGovRole(r.Context(), req.DocID, user), user.Name)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, "claim document failed: "+err.Error())
|
||||
return
|
||||
@@ -90,7 +90,7 @@ func (s *Server) handleDocGovSubmitReview(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func (s *Server) handleDocGovSubmitReview(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := s.gov.SubmitForReview(r.Context(), req.DocID, user.ID, vocabulary.RoleAuthor, user.Name)
|
||||
doc, err := s.gov().SubmitForReview(r.Context(), req.DocID, user.ID, vocabulary.RoleAuthor, user.Name)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "submit review failed: "+err.Error())
|
||||
return
|
||||
@@ -120,7 +120,7 @@ func (s *Server) handleDocGovReviewDecision(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
@@ -133,13 +133,13 @@ func (s *Server) handleDocGovReviewDecision(w http.ResponseWriter, r *http.Reque
|
||||
var err error
|
||||
|
||||
if action == "approve" {
|
||||
doc, err = s.gov.ReviewApprove(r.Context(), req.DocID, user.ID, vocabulary.RoleReviewer, user.Name, req.Comment)
|
||||
doc, err = s.gov().ReviewApprove(r.Context(), req.DocID, user.ID, vocabulary.RoleReviewer, user.Name, req.Comment)
|
||||
} else if action == "reject" {
|
||||
reason := req.Reason
|
||||
if reason == "" {
|
||||
reason = req.Comment
|
||||
}
|
||||
doc, err = s.gov.ReviewReject(r.Context(), req.DocID, user.ID, vocabulary.RoleReviewer, user.Name, reason)
|
||||
doc, err = s.gov().ReviewReject(r.Context(), req.DocID, user.ID, vocabulary.RoleReviewer, user.Name, reason)
|
||||
} else {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "invalid action: must be approve or reject")
|
||||
return
|
||||
@@ -166,7 +166,7 @@ func (s *Server) handleDocGovApproverDecision(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
@@ -239,7 +239,7 @@ func (s *Server) handleDocGovProposeModification(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
@@ -249,7 +249,7 @@ func (s *Server) handleDocGovProposeModification(w http.ResponseWriter, r *http.
|
||||
triggeredBy = fmt.Sprintf("user:%d", user.ID)
|
||||
}
|
||||
|
||||
proposal, err := s.gov.ProposeModification(
|
||||
proposal, err := s.gov().ProposeModification(
|
||||
r.Context(),
|
||||
req.DocID,
|
||||
triggeredBy,
|
||||
@@ -286,12 +286,12 @@ func (s *Server) handleDocGovDecideProposal(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
proposal, err := s.gov.DecideModificationProposal(r.Context(), req.ProposalID, user.ID, req.Approve, req.Reason)
|
||||
proposal, err := s.gov().DecideModificationProposal(r.Context(), req.ProposalID, user.ID, req.Approve, req.Reason)
|
||||
if err != nil {
|
||||
if status, ok := domainHTTPStatus(err); ok {
|
||||
switch status {
|
||||
@@ -308,13 +308,13 @@ func (s *Server) handleDocGovDecideProposal(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
if req.Approve && s.svc != nil && proposal != nil {
|
||||
if req.Approve && s.svc() != nil && proposal != nil {
|
||||
// Auto-apply as author for human/supplement-triggered proposals only.
|
||||
triggered := strings.TrimSpace(proposal.TriggeredBy)
|
||||
if strings.HasPrefix(triggered, "user:") || strings.Contains(triggered, "supplement") {
|
||||
base := bizdev.TaskRunRequest{DocID: proposal.DocID, ProjectID: proposal.ProjectID, ForceDirectEdit: true, UserID: user.ID, AuthorID: user.ID, AuthorName: user.Name}
|
||||
go func(prop *docgov.ChangeProposal, uid int64, req bizdev.TaskRunRequest) {
|
||||
_, _ = s.svc.AfterProposalApproved(context.Background(), prop, uid, req)
|
||||
_, _ = s.svc().AfterProposalApproved(context.Background(), prop, uid, req)
|
||||
}(proposal, user.ID, base)
|
||||
}
|
||||
}
|
||||
@@ -329,7 +329,7 @@ func (s *Server) handleDocGovListComments(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
@@ -338,7 +338,7 @@ func (s *Server) handleDocGovListComments(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
comments, err := s.gov.ListComments(r.Context(), docID)
|
||||
comments, err := s.gov().ListComments(r.Context(), docID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, "list comments failed: "+err.Error())
|
||||
return
|
||||
@@ -356,7 +356,7 @@ func (s *Server) handleDocGovAddComment(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
@@ -371,7 +371,7 @@ func (s *Server) handleDocGovAddComment(w http.ResponseWriter, r *http.Request)
|
||||
cType = vocabulary.CommentGeneral
|
||||
}
|
||||
|
||||
comment, err := s.gov.AddComment(
|
||||
comment, err := s.gov().AddComment(
|
||||
r.Context(), req.DocID, user.ID, user.Name,
|
||||
s.resolveDocGovRole(r.Context(), req.DocID, user), req.TargetSection, cType, req.Content,
|
||||
)
|
||||
@@ -391,7 +391,7 @@ func (s *Server) handleDocGovListAuditLogs(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
@@ -400,7 +400,7 @@ func (s *Server) handleDocGovListAuditLogs(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
logs, err := s.gov.ListAuditLogs(r.Context(), docID)
|
||||
logs, err := s.gov().ListAuditLogs(r.Context(), docID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, "list audit logs failed: "+err.Error())
|
||||
return
|
||||
|
||||
@@ -24,7 +24,7 @@ func (s *Server) handleDocGovSaveContent(w http.ResponseWriter, r *http.Request)
|
||||
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func (s *Server) handleDocGovSaveContent(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
role := vocabulary.RoleAuthor
|
||||
res, err := s.gov.SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{
|
||||
res, err := s.gov().SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{
|
||||
DocID: req.DocID,
|
||||
ActorID: user.ID,
|
||||
ActorRole: role,
|
||||
@@ -59,7 +59,7 @@ func (s *Server) handleDocGovAcquireLease(w http.ResponseWriter, r *http.Request
|
||||
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
@@ -67,18 +67,18 @@ func (s *Server) handleDocGovAcquireLease(w http.ResponseWriter, r *http.Request
|
||||
if !s.enforceDocGovWorkflowAction(w, r.Context(), user.ID, req.DocID, projectinstance.DocAccessActionEdit) {
|
||||
return
|
||||
}
|
||||
if err := s.gov.CheckHumanCanEdit(r.Context(), req.DocID, user.ID, vocabulary.RoleAuthor, true); err != nil {
|
||||
if err := s.gov().CheckHumanCanEdit(r.Context(), req.DocID, user.ID, vocabulary.RoleAuthor, true); err != nil {
|
||||
writeDocGovError(w, err)
|
||||
return
|
||||
}
|
||||
req.HolderID = fmt.Sprintf("%d", user.ID)
|
||||
}
|
||||
workDir, werr := s.resolver.ResolveDocWorkDir(r.Context(), req.DocID)
|
||||
workDir, werr := s.resolver().ResolveDocWorkDir(r.Context(), req.DocID)
|
||||
if werr != nil {
|
||||
workDir = ""
|
||||
}
|
||||
ttl := time.Duration(req.TTLSeconds) * time.Second
|
||||
doc, err := s.gov.AcquireLease(r.Context(), docgov.LeaseAcquireRequest{
|
||||
doc, err := s.gov().AcquireLease(r.Context(), docgov.LeaseAcquireRequest{
|
||||
DocID: req.DocID,
|
||||
HolderType: vocabulary.LeaseHolderType(req.HolderType),
|
||||
HolderID: req.HolderID,
|
||||
@@ -102,7 +102,7 @@ func (s *Server) handleDocGovHeartbeatLease(w http.ResponseWriter, r *http.Reque
|
||||
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func (s *Server) handleDocGovHeartbeatLease(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
ttl := time.Duration(req.TTLSeconds) * time.Second
|
||||
doc, err := s.gov.HeartbeatLease(r.Context(), req.DocID, req.HolderType, req.HolderID, ttl)
|
||||
doc, err := s.gov().HeartbeatLease(r.Context(), req.DocID, req.HolderType, req.HolderID, ttl)
|
||||
if err != nil {
|
||||
writeDocGovError(w, err)
|
||||
return
|
||||
@@ -135,7 +135,7 @@ func (s *Server) handleDocGovReleaseLease(w http.ResponseWriter, r *http.Request
|
||||
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func (s *Server) handleDocGovReleaseLease(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
}
|
||||
doc, err := s.gov.ReleaseLease(r.Context(), req.DocID, req.HolderType, req.HolderID)
|
||||
doc, err := s.gov().ReleaseLease(r.Context(), req.DocID, req.HolderType, req.HolderID)
|
||||
if err != nil {
|
||||
writeDocGovError(w, err)
|
||||
return
|
||||
|
||||
@@ -37,13 +37,13 @@ func (s *Server) buildHealthResponse(ctx context.Context, strict bool) (HealthRe
|
||||
dbCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dbReady := s.svc != nil && s.svc.DatabaseReady()
|
||||
dbReady := s.svc() != nil && s.svc().DatabaseReady()
|
||||
schemaVersion := int64(0)
|
||||
if s.svc != nil {
|
||||
if s.svc() != nil {
|
||||
if dbReady {
|
||||
schemaVersion = s.svc.LiveSchemaVersion(dbCtx)
|
||||
schemaVersion = s.svc().LiveSchemaVersion(dbCtx)
|
||||
} else {
|
||||
schemaVersion = s.svc.SchemaVersion()
|
||||
schemaVersion = s.svc().SchemaVersion()
|
||||
}
|
||||
}
|
||||
schemaReady := schemaVersion > 0 || (s.cfg != nil && s.cfg.AllowInMemory)
|
||||
@@ -70,7 +70,7 @@ func (s *Server) buildHealthResponse(ctx context.Context, strict bool) (HealthRe
|
||||
}
|
||||
}
|
||||
|
||||
kbReady := s.kb != nil && s.kb.Online()
|
||||
kbReady := s.kb() != nil && s.kb().Online()
|
||||
|
||||
eventsReady := true
|
||||
if s.eventsAlive != nil {
|
||||
|
||||
@@ -105,7 +105,7 @@ func shouldDispatchAfterDecision(kind intentflow.DecisionKind, view *intentflow.
|
||||
}
|
||||
|
||||
func (s *Server) planRequestScope(w http.ResponseWriter, r *http.Request) (intentflow.Service, intentflow.Principal, bool) {
|
||||
if s == nil || s.intentPlans == nil {
|
||||
if s == nil || s.intentPlans() == nil {
|
||||
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]string{
|
||||
"error_code": "PLAN_NOT_FOUND_OR_HIDDEN",
|
||||
"message": "计划服务不可用",
|
||||
@@ -151,7 +151,7 @@ func (s *Server) planRequestScope(w http.ResponseWriter, r *http.Request) (inten
|
||||
if s.cfg != nil && strings.TrimSpace(s.cfg.Env) != "" {
|
||||
env = s.cfg.Env
|
||||
}
|
||||
return s.intentPlans, intentflow.Principal{
|
||||
return s.intentPlans(), intentflow.Principal{
|
||||
UserID: user.ID,
|
||||
Environment: env,
|
||||
ProjectID: projectID,
|
||||
@@ -163,20 +163,20 @@ func (s *Server) planProjectAccess(ctx context.Context, projectID, userID int64)
|
||||
if s.planAccessFn != nil {
|
||||
return s.planAccessFn(ctx, projectID, userID)
|
||||
}
|
||||
if s.authorizer == nil {
|
||||
if s.authorizer() == nil {
|
||||
return authz.ErrForbidden
|
||||
}
|
||||
return s.authorizer.RequireProjectAccess(ctx, projectID, userID)
|
||||
return s.authorizer().RequireProjectAccess(ctx, projectID, userID)
|
||||
}
|
||||
|
||||
func (s *Server) planSessionOwner(ctx context.Context, sessionID string) (int64, error) {
|
||||
if s.planSessionOwnerFn != nil {
|
||||
return s.planSessionOwnerFn(ctx, sessionID)
|
||||
}
|
||||
if s.ctxMgr == nil {
|
||||
if s.ctxMgr() == nil {
|
||||
return 0, intentflow.ErrPlanNotFoundOrHidden
|
||||
}
|
||||
return s.ctxMgr.GetSessionOwnerID(ctx, sessionID)
|
||||
return s.ctxMgr().GetSessionOwnerID(ctx, sessionID)
|
||||
}
|
||||
|
||||
func writeIntentPlanError(w http.ResponseWriter, r *http.Request, plans intentflow.Service, prin intentflow.Principal, planID string, err error) {
|
||||
|
||||
@@ -24,7 +24,7 @@ func u5Server(t *testing.T) (*Server, *intentflow.Plans, *intentflow.FakeActionO
|
||||
srv := NewServer(cfg, bizdev.MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"}))
|
||||
owner := intentflow.NewFakeActionOwner()
|
||||
plans := intentflow.NewPlans(&intentflow.SerialUoW{}, intentflow.NewMemoryStore(), intentflow.Options{Owner: owner})
|
||||
srv.intentPlans = plans
|
||||
srv.setOverrideIntentPlans(plans)
|
||||
token, userID := u5Login(t, srv)
|
||||
srv.planAccessFn = func(ctx context.Context, projectID, userID int64) error {
|
||||
if projectID == 99 {
|
||||
|
||||
@@ -13,10 +13,10 @@ func (s *Server) callerOwnsSquadLease(userID int64, holderID string) bool {
|
||||
if userID <= 0 || holderID == "" {
|
||||
return false
|
||||
}
|
||||
if s == nil || s.taskRuns == nil {
|
||||
if s == nil || s.taskRuns() == nil {
|
||||
return false
|
||||
}
|
||||
run, err := s.taskRuns.GetRun(holderID)
|
||||
run, err := s.taskRuns().GetRun(holderID)
|
||||
if err != nil || run == nil {
|
||||
if s.cfg != nil && s.cfg.AllowInMemory {
|
||||
return true
|
||||
|
||||
@@ -48,8 +48,8 @@ func (s *Server) lineageDocVisible(ctx context.Context, projectID, userID int64,
|
||||
}
|
||||
|
||||
func (s *Server) lineageDocPlacement(ctx context.Context, projectID int64, docID string) (taskKey string, placed bool, ok bool) {
|
||||
if s.gov != nil {
|
||||
doc, err := s.gov.GetDocument(ctx, docID)
|
||||
if s.gov() != nil {
|
||||
doc, err := s.gov().GetDocument(ctx, docID)
|
||||
if err != nil {
|
||||
if status, mapped := domainHTTPStatus(err); !(mapped && status == http.StatusNotFound) {
|
||||
return "", false, false
|
||||
|
||||
@@ -20,11 +20,11 @@ import (
|
||||
)
|
||||
|
||||
func (s *Server) handleMaintenanceStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if s.maintenance == nil {
|
||||
if s.maintenance() == nil {
|
||||
writeJSONResponse(w, http.StatusOK, maintenance.StatusView{Status: "none"})
|
||||
return
|
||||
}
|
||||
view, err := s.maintenance.CurrentStatus(r.Context())
|
||||
view, err := s.maintenance().CurrentStatus(r.Context())
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -36,11 +36,11 @@ func (s *Server) handleAdminMaintenanceList(w http.ResponseWriter, r *http.Reque
|
||||
if _, ok := s.requireSystemAdmin(w, r); !ok {
|
||||
return
|
||||
}
|
||||
if s.maintenance == nil {
|
||||
if s.maintenance() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "maintenance service unavailable")
|
||||
return
|
||||
}
|
||||
list, err := s.maintenance.ListRecent(r.Context(), 20)
|
||||
list, err := s.maintenance().ListRecent(r.Context(), 20)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -53,7 +53,7 @@ func (s *Server) handleAdminMaintenanceSchedule(w http.ResponseWriter, r *http.R
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.maintenance == nil {
|
||||
if s.maintenance() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "maintenance service unavailable")
|
||||
return
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func (s *Server) handleAdminMaintenanceSchedule(w http.ResponseWriter, r *http.R
|
||||
if end.IsZero() && req.Hours > 0 {
|
||||
end = req.Start.Add(time.Duration(req.Hours * float64(time.Hour)))
|
||||
}
|
||||
wnd, err := s.maintenance.Schedule(r.Context(), maintenance.ScheduleInput{
|
||||
wnd, err := s.maintenance().Schedule(r.Context(), maintenance.ScheduleInput{
|
||||
Start: req.Start, End: end, Message: req.Message, CreatedBy: user.ID,
|
||||
})
|
||||
switch {
|
||||
@@ -90,7 +90,7 @@ func (s *Server) handleAdminMaintenanceCancel(w http.ResponseWriter, r *http.Req
|
||||
if _, ok := s.requireSystemAdmin(w, r); !ok {
|
||||
return
|
||||
}
|
||||
if s.maintenance == nil {
|
||||
if s.maintenance() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "maintenance service unavailable")
|
||||
return
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func (s *Server) handleAdminMaintenanceCancel(w http.ResponseWriter, r *http.Req
|
||||
writeSafeError(w, r, http.StatusBadRequest, "无效的 id")
|
||||
return
|
||||
}
|
||||
wnd, err := s.maintenance.Cancel(r.Context(), id)
|
||||
wnd, err := s.maintenance().Cancel(r.Context(), id)
|
||||
switch {
|
||||
case errors.Is(err, maintenance.ErrBadStatus):
|
||||
writeSafeError(w, r, http.StatusConflict, "当前状态不可取消")
|
||||
@@ -112,11 +112,11 @@ func (s *Server) handleAdminMaintenanceCancel(w http.ResponseWriter, r *http.Req
|
||||
|
||||
func (s *Server) maintenanceMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if s.maintenance == nil {
|
||||
if s.maintenance() == nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
view, err := s.maintenance.CurrentStatus(r.Context())
|
||||
view, err := s.maintenance().CurrentStatus(r.Context())
|
||||
if err != nil || !view.Active {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
@@ -128,8 +128,8 @@ func (s *Server) maintenanceMiddleware(next http.Handler) http.Handler {
|
||||
}
|
||||
// 管理员仍可访问管理台与维护 API
|
||||
if user, err := s.authenticateUser(r); err == nil && user != nil {
|
||||
if s.authzSvc != nil {
|
||||
if admin, aerr := s.authzSvc.IsSystemAdmin(r.Context(), user.ID, user.RoleID); aerr == nil && admin {
|
||||
if s.authzSvc() != nil {
|
||||
if admin, aerr := s.authzSvc().IsSystemAdmin(r.Context(), user.ID, user.RoleID); aerr == nil && admin {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func (s *Server) handleMeProfile(w http.ResponseWriter, r *http.Request) {
|
||||
if user.ProfileUpdatedBy > 0 {
|
||||
resp["profile_updated_by"] = user.ProfileUpdatedBy
|
||||
resp["profile_updated_at"] = user.ProfileUpdatedAt
|
||||
if admin, err := s.authSvc.GetUserByID(r.Context(), user.ProfileUpdatedBy); err == nil && admin != nil {
|
||||
if admin, err := s.authSvc().GetUserByID(r.Context(), user.ProfileUpdatedBy); err == nil && admin != nil {
|
||||
name := strings.TrimSpace(admin.Name)
|
||||
if name == "" {
|
||||
name = admin.Username
|
||||
@@ -47,7 +47,7 @@ func (s *Server) handleMeProfile(w http.ResponseWriter, r *http.Request) {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "invalid body")
|
||||
return
|
||||
}
|
||||
saved, err := s.authSvc.UpdateUserProfile(r.Context(), user.ID, auth.ProfilePatch{
|
||||
saved, err := s.authSvc().UpdateUserProfile(r.Context(), user.ID, auth.ProfilePatch{
|
||||
Name: patch.Name,
|
||||
Email: patch.Email,
|
||||
Department: patch.Department,
|
||||
@@ -72,7 +72,7 @@ func (s *Server) handleMePreferences(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
prefs, err := s.authSvc.GetUserPreferences(r.Context(), user.ID)
|
||||
prefs, err := s.authSvc().GetUserPreferences(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -84,7 +84,7 @@ func (s *Server) handleMePreferences(w http.ResponseWriter, r *http.Request) {
|
||||
writeSafeError(w, r, http.StatusBadRequest, "invalid body")
|
||||
return
|
||||
}
|
||||
prefs, err := s.authSvc.SaveUserPreferences(r.Context(), user.ID, body)
|
||||
prefs, err := s.authSvc().SaveUserPreferences(r.Context(), user.ID, body)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
|
||||
@@ -17,7 +17,7 @@ func (s *Server) handleListProjectNotifications(w http.ResponseWriter, r *http.R
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.notifySvc == nil || !s.notifySvc.Available() {
|
||||
if s.notifySvc() == nil || !s.notifySvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "notification service unavailable")
|
||||
return
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func (s *Server) handleListProjectNotifications(w http.ResponseWriter, r *http.R
|
||||
sinceID, _ := strconv.ParseInt(r.URL.Query().Get("since_id"), 10, 64)
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
|
||||
items, err := s.notifySvc.ListProjectNotifications(r.Context(), user.ID, projectID, sinceID, limit)
|
||||
items, err := s.notifySvc().ListProjectNotifications(r.Context(), user.ID, projectID, sinceID, limit)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, "list notifications failed: "+err.Error())
|
||||
return
|
||||
@@ -63,7 +63,7 @@ func (s *Server) handleListUserNotifications(w http.ResponseWriter, r *http.Requ
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.notifySvc == nil || !s.notifySvc.Available() {
|
||||
if s.notifySvc() == nil || !s.notifySvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "notification service unavailable")
|
||||
return
|
||||
}
|
||||
@@ -89,7 +89,7 @@ func (s *Server) listAllScopeNotifications(ctx context.Context, userID, sinceID
|
||||
type userLister interface {
|
||||
ListUserNotifications(ctx context.Context, userID, sinceID int64, limit int) ([]notification.Notification, error)
|
||||
}
|
||||
if lister, ok := any(s.notifySvc).(userLister); ok {
|
||||
if lister, ok := any(s.notifySvc()).(userLister); ok {
|
||||
raw, err := lister.ListUserNotifications(ctx, userID, sinceID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -193,10 +193,10 @@ func (s *Server) queryUserNotifications(ctx context.Context, userID, sinceID int
|
||||
}
|
||||
|
||||
func (s *Server) fanOutProjectNotifications(ctx context.Context, userID, sinceID int64, limit int) ([]userNotificationView, error) {
|
||||
if s.pwSvc == nil {
|
||||
if s.pwSvc() == nil {
|
||||
return []userNotificationView{}, nil
|
||||
}
|
||||
projects, err := s.pwSvc.ListProjects(ctx, userID)
|
||||
projects, err := s.pwSvc().ListProjects(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -209,7 +209,7 @@ func (s *Server) fanOutProjectNotifications(ctx context.Context, userID, sinceID
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
chunk, listErr := s.notifySvc.ListProjectNotifications(ctx, userID, p.ID, sinceID, per)
|
||||
chunk, listErr := s.notifySvc().ListProjectNotifications(ctx, userID, p.ID, sinceID, per)
|
||||
if listErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -9,17 +9,20 @@ import (
|
||||
const defaultReminderPollInterval = 15 * time.Minute
|
||||
|
||||
func (s *Server) startNotificationReminderLoop() {
|
||||
if s == nil || s.notifySvc == nil || !s.notifySvc.Available() {
|
||||
if s == nil || s.notifySvc() == nil || !s.notifySvc().Available() {
|
||||
return
|
||||
}
|
||||
stopCh, ok := s.reminderLoop.start()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
s.reminderStop = make(chan struct{})
|
||||
go func() {
|
||||
ticker := time.NewTicker(defaultReminderPollInterval)
|
||||
defer ticker.Stop()
|
||||
s.runStageAdvanceReminders(context.Background())
|
||||
for {
|
||||
select {
|
||||
case <-s.reminderStop:
|
||||
case <-stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.runStageAdvanceReminders(context.Background())
|
||||
@@ -29,10 +32,10 @@ func (s *Server) startNotificationReminderLoop() {
|
||||
}
|
||||
|
||||
func (s *Server) runStageAdvanceReminders(ctx context.Context) {
|
||||
if s.notifySvc == nil || !s.notifySvc.Available() {
|
||||
if s.notifySvc() == nil || !s.notifySvc().Available() {
|
||||
return
|
||||
}
|
||||
n, err := s.notifySvc.ProcessStageAdvanceReminders(ctx)
|
||||
n, err := s.notifySvc().ProcessStageAdvanceReminders(ctx)
|
||||
if err != nil {
|
||||
slog.Warn("stage advance reminder scan failed", "error", err)
|
||||
return
|
||||
|
||||
@@ -52,7 +52,7 @@ func (s *Server) requireSystemAdmin(w http.ResponseWriter, r *http.Request) (*au
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
admin, err := s.authzSvc.IsSystemAdmin(r.Context(), user.ID, user.RoleID)
|
||||
admin, err := s.authzSvc().IsSystemAdmin(r.Context(), user.ID, user.RoleID)
|
||||
if err != nil {
|
||||
writeJSONResponse(w, http.StatusInternalServerError, map[string]string{
|
||||
"error_code": "ERR_AUTHZ",
|
||||
@@ -77,7 +77,7 @@ func (s *Server) requireSystemPermission(w http.ResponseWriter, r *http.Request,
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if s.authzSvc == nil {
|
||||
if s.authzSvc() == nil {
|
||||
if s.failClosedAuthz() {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
"error_code": "ERR_FORBIDDEN",
|
||||
@@ -87,7 +87,7 @@ func (s *Server) requireSystemPermission(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
return user, true
|
||||
}
|
||||
allowed, err := s.authzSvc.HasSystemPermission(r.Context(), user.ID, user.RoleID, perm)
|
||||
allowed, err := s.authzSvc().HasSystemPermission(r.Context(), user.ID, user.RoleID, perm)
|
||||
if err != nil {
|
||||
writeJSONResponse(w, http.StatusInternalServerError, map[string]string{
|
||||
"error_code": "ERR_AUTHZ",
|
||||
|
||||
@@ -19,11 +19,11 @@ import (
|
||||
)
|
||||
|
||||
func (s *Server) docflowQuery() *docflow.Query {
|
||||
if s == nil || s.svc == nil {
|
||||
if s == nil || s.svc() == nil {
|
||||
return nil
|
||||
}
|
||||
return &docflow.Query{Docs: &docquery.Service{
|
||||
Gov: s.gov,
|
||||
Gov: s.gov(),
|
||||
Rev: s.docRev(),
|
||||
RepoRoot: s.repoRoot(),
|
||||
Authorize: func(ctx context.Context, p docquery.Principal, rec *docgov.DocumentRecord) error {
|
||||
@@ -41,12 +41,12 @@ func (s *Server) authorizeControlledRead(ctx context.Context, p docquery.Princip
|
||||
return docquery.ErrClosed
|
||||
}
|
||||
admin := false
|
||||
if s.authzSvc != nil {
|
||||
ok, _ := s.authzSvc.IsSystemAdmin(ctx, p.UserID, "")
|
||||
if s.authzSvc() != nil {
|
||||
ok, _ := s.authzSvc().IsSystemAdmin(ctx, p.UserID, "")
|
||||
admin = ok
|
||||
}
|
||||
if s.authorizer != nil {
|
||||
if err := s.authorizer.RequireProjectAccessOrAdmin(ctx, rec.ProjectID, p.UserID, admin); err != nil {
|
||||
if s.authorizer() != nil {
|
||||
if err := s.authorizer().RequireProjectAccessOrAdmin(ctx, rec.ProjectID, p.UserID, admin); err != nil {
|
||||
return docquery.ErrClosed
|
||||
}
|
||||
}
|
||||
@@ -82,17 +82,17 @@ func (s *Server) handleListProjectWorkflowDocuments(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
if s.gov == nil {
|
||||
if s.gov() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
if s.instSvc != nil {
|
||||
if err := s.instSvc.MaterializeLiveDocs(r.Context(), projectID); err != nil {
|
||||
if s.instSvc() != nil {
|
||||
if err := s.instSvc().MaterializeLiveDocs(r.Context(), projectID); err != nil {
|
||||
slog.Warn("materialize live docs", "project_id", projectID, "err", err)
|
||||
}
|
||||
}
|
||||
docs, err := s.gov.ListWorkflowDocumentsByProject(r.Context(), projectID)
|
||||
docs, err := s.gov().ListWorkflowDocumentsByProject(r.Context(), projectID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, "list project documents failed: "+err.Error())
|
||||
return
|
||||
@@ -138,8 +138,8 @@ func (s *Server) handleDocGovGetDocumentContent(w http.ResponseWriter, r *http.R
|
||||
})
|
||||
return
|
||||
}
|
||||
if s.cfg != nil && s.cfg.FAVDegradedRead && s.gov != nil {
|
||||
docRec, docErr := s.gov.GetDocument(r.Context(), docID)
|
||||
if s.cfg != nil && s.cfg.FAVDegradedRead && s.gov() != nil {
|
||||
docRec, docErr := s.gov().GetDocument(r.Context(), docID)
|
||||
if docErr == nil && docRec != nil {
|
||||
env := s.cfg.Env
|
||||
if env == "" {
|
||||
@@ -170,10 +170,10 @@ func (s *Server) handleDocGovGetDocumentContent(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
|
||||
func (s *Server) loadControlledDocumentContent(ctx context.Context, docID string) (*docgov.DocumentContent, error) {
|
||||
if s == nil || s.gov == nil {
|
||||
if s == nil || s.gov() == nil {
|
||||
return nil, vocabulary.ErrDocumentNotFound
|
||||
}
|
||||
svc := &docquery.Service{Gov: s.gov, Rev: s.docRev(), RepoRoot: s.repoRoot()}
|
||||
svc := &docquery.Service{Gov: s.gov(), Rev: s.docRev(), RepoRoot: s.repoRoot()}
|
||||
view, err := svc.Get(ctx, docquery.Principal{UserID: 1}, docquery.DocRef{DocID: docID})
|
||||
if err != nil && !errors.Is(err, docquery.ErrLedgerMissing) {
|
||||
return nil, err
|
||||
@@ -181,7 +181,7 @@ func (s *Server) loadControlledDocumentContent(ctx context.Context, docID string
|
||||
if view != nil && (view.Markdown != "" || view.IsTemplateOnly) {
|
||||
return view.ToDocumentContent(), nil
|
||||
}
|
||||
return s.gov.ReadDocumentContent(ctx, docID, s.repoRoot())
|
||||
return s.gov().ReadDocumentContent(ctx, docID, s.repoRoot())
|
||||
}
|
||||
|
||||
// serveLedgerHeadOrConflict keeps the historical helper name. Projection is a
|
||||
|
||||
@@ -37,7 +37,7 @@ func (s *Server) handleCreateProject(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
proj, err := s.pwSvc.CreateProject(r.Context(), user, req)
|
||||
proj, err := s.pwSvc().CreateProject(r.Context(), user, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, projectworkflow.ErrInvalidInput) {
|
||||
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"`+err.Error()+`"}`)
|
||||
@@ -58,8 +58,8 @@ func (s *Server) handleCreateProject(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if proj != nil && s.authorizer != nil {
|
||||
_ = s.authorizer.AddOwnerOnCreate(r.Context(), proj.ID, user.ID)
|
||||
if proj != nil && s.authorizer() != nil {
|
||||
_ = s.authorizer().AddOwnerOnCreate(r.Context(), proj.ID, user.ID)
|
||||
}
|
||||
|
||||
writeJSONResponse(w, http.StatusCreated, proj)
|
||||
@@ -77,7 +77,7 @@ func (s *Server) handleListProjects(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin, err := s.authzSvc.IsSystemAdmin(r.Context(), user.ID, user.RoleID)
|
||||
isAdmin, err := s.authzSvc().IsSystemAdmin(r.Context(), user.ID, user.RoleID)
|
||||
if err != nil {
|
||||
writeJSONResponse(w, http.StatusInternalServerError, map[string]string{
|
||||
"error_code": "ERR_AUTHZ",
|
||||
@@ -88,9 +88,9 @@ func (s *Server) handleListProjects(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var list []*projectworkflow.Project
|
||||
if isAdmin {
|
||||
list, err = s.pwSvc.ListAllProjects(r.Context())
|
||||
list, err = s.pwSvc().ListAllProjects(r.Context())
|
||||
} else {
|
||||
list, err = s.pwSvc.ListProjects(r.Context(), user.ID)
|
||||
list, err = s.pwSvc().ListProjects(r.Context(), user.ID)
|
||||
}
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||
@@ -116,7 +116,7 @@ func (s *Server) handleGetProjectByID(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
proj, err := s.pwSvc.GetProject(r.Context(), id, 0)
|
||||
proj, err := s.pwSvc().GetProject(r.Context(), id, 0)
|
||||
if err != nil {
|
||||
if errors.Is(err, projectworkflow.ErrProjectNotFound) {
|
||||
writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_PROJECT_NOT_FOUND","message":"项目不存在或无权限访问"}`)
|
||||
@@ -141,7 +141,7 @@ func (s *Server) handleListProjectWorkflows(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
wfs, err := s.pwSvc.ListProjectWorkflows(r.Context(), id)
|
||||
wfs, err := s.pwSvc().ListProjectWorkflows(r.Context(), id)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||
return
|
||||
@@ -168,7 +168,7 @@ func (s *Server) handleActivateWorkflow(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if s.instSvc != nil && s.instSvc.Available() {
|
||||
if s.instSvc() != nil && s.instSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusGone, `{"error":"WORKFLOW_SWITCH_API_RETIRED","message":"请通过项目设置完成工作流规划与激活"}`)
|
||||
return
|
||||
}
|
||||
@@ -183,7 +183,7 @@ func (s *Server) handleActivateWorkflow(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := s.pwSvc.ActivateWorkflow(r.Context(), id, user.ID, req)
|
||||
res, err := s.pwSvc().ActivateWorkflow(r.Context(), id, user.ID, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, projectworkflow.ErrForbiddenNotCreator) {
|
||||
writeSafeError(w, r, http.StatusForbidden, `{"error":"ERR_NOT_PROJECT_CREATOR","message":"仅项目创建人有权切换激活工作流"}`)
|
||||
@@ -210,7 +210,7 @@ func (s *Server) handleActivateWorkflow(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// handleListWorkflowDefinitions 获取全局工作流规范目录
|
||||
func (s *Server) handleListWorkflowDefinitions(w http.ResponseWriter, r *http.Request) {
|
||||
defs := s.pwSvc.Catalog().List()
|
||||
defs := s.pwSvc().Catalog().List()
|
||||
writeJSONResponse(w, http.StatusOK, defs)
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ func (s *Server) handleRunProjectWorkflow(w http.ResponseWriter, r *http.Request
|
||||
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"workflow_key is required"}`)
|
||||
return
|
||||
}
|
||||
def, ok := s.pwSvc.Catalog().Get(wfKey)
|
||||
def, ok := s.pwSvc().Catalog().Get(wfKey)
|
||||
if !ok {
|
||||
writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"unknown workflow"}`)
|
||||
return
|
||||
@@ -272,7 +272,7 @@ func (s *Server) handleListProjectWorkflowRuns(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
wfKey := r.PathValue("workflow_key")
|
||||
runs, err := s.pwSvc.ListWorkflowRuns(r.Context(), id, wfKey, 20)
|
||||
runs, err := s.pwSvc().ListWorkflowRuns(r.Context(), id, wfKey, 20)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||
return
|
||||
@@ -305,14 +305,14 @@ func (s *Server) handleGetProjectLineage(w http.ResponseWriter, r *http.Request)
|
||||
writeEmptyLineage(w)
|
||||
return
|
||||
}
|
||||
lineages, err := s.pwSvc.GetLineageForProjectDownstream(r.Context(), projectID, docID, 0)
|
||||
lineages, err := s.pwSvc().GetLineageForProjectDownstream(r.Context(), projectID, docID, 0)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||
return
|
||||
}
|
||||
if len(lineages) == 0 {
|
||||
if alt, aliasOK := lineageTestManagerAlias(projectID, docID); aliasOK && s.lineageDocVisible(r.Context(), projectID, user.ID, alt) {
|
||||
if fb, fbErr := s.pwSvc.GetLineageForProjectDownstream(r.Context(), projectID, alt, 0); fbErr == nil && len(fb) > 0 {
|
||||
if fb, fbErr := s.pwSvc().GetLineageForProjectDownstream(r.Context(), projectID, alt, 0); fbErr == nil && len(fb) > 0 {
|
||||
lineages = fb
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestHandleListProjects_AdminCanSeeProjectsOwnedByOtherUsers(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("load creator: %v", err)
|
||||
}
|
||||
created, err := srv.pwSvc.CreateProject(ctx, creator, projectworkflow.CreateProjectInput{
|
||||
created, err := srv.pwSvc().CreateProject(ctx, creator, projectworkflow.CreateProjectInput{
|
||||
Name: "非管理员创建的项目",
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -30,7 +30,7 @@ func writeDutySyncError(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
func (s *Server) healProjectDuties(ctx context.Context, projectID int64) error {
|
||||
if s == nil || s.svc == nil || s.rt == nil || projectID <= 0 {
|
||||
if s == nil || s.svc() == nil || s.rt == nil || projectID <= 0 {
|
||||
return nil
|
||||
}
|
||||
return s.rt.HealProjectDuties(ctx, projectID)
|
||||
@@ -73,7 +73,7 @@ func (s *Server) handleListProjectMembers(w http.ResponseWriter, r *http.Request
|
||||
if _, ok := s.requireProjectAccess(w, r, projectID); !ok {
|
||||
return
|
||||
}
|
||||
members, err := s.authorizer.ListProjectMembers(r.Context(), projectID)
|
||||
members, err := s.authorizer().ListProjectMembers(r.Context(), projectID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -85,7 +85,7 @@ func (s *Server) handleListProjectMembers(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
func (s *Server) requireProjectMemberManage(w http.ResponseWriter, r *http.Request, user *auth.UserAccount, projectID int64) bool {
|
||||
can, err := s.authzSvc.CanManageProjectMembers(r.Context(), projectID, user.ID, user.RoleID)
|
||||
can, err := s.authzSvc().CanManageProjectMembers(r.Context(), projectID, user.ID, user.RoleID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return false
|
||||
@@ -109,7 +109,7 @@ func (s *Server) handleAddProjectMember(w http.ResponseWriter, r *http.Request,
|
||||
if req.ProjectRole == "" {
|
||||
req.ProjectRole = authz.RoleMember
|
||||
}
|
||||
if err := s.authorizer.UpsertProjectMemberSpec(r.Context(), projectID, req.UserID, authz.MemberUpsert{
|
||||
if err := s.authorizer().UpsertProjectMemberSpec(r.Context(), projectID, req.UserID, authz.MemberUpsert{
|
||||
ProjectRole: req.ProjectRole,
|
||||
DocumentDuty: req.DocumentDuty,
|
||||
}); err != nil {
|
||||
@@ -126,17 +126,17 @@ func (s *Server) handleAddProjectMember(w http.ResponseWriter, r *http.Request,
|
||||
|
||||
// ensureMemberChatSession 为项目成员补齐私有会话(sess-p{id}-u{uid}),失败仅记日志。
|
||||
func (s *Server) ensureMemberChatSession(ctx context.Context, projectID, memberUserID int64) {
|
||||
if s == nil || s.ctxMgr == nil || projectID <= 0 || memberUserID <= 0 {
|
||||
if s == nil || s.ctxMgr() == nil || projectID <= 0 || memberUserID <= 0 {
|
||||
return
|
||||
}
|
||||
title := ""
|
||||
if s.pwSvc != nil {
|
||||
if proj, err := s.pwSvc.GetProject(ctx, projectID, 0); err == nil && proj != nil {
|
||||
if s.pwSvc() != nil {
|
||||
if proj, err := s.pwSvc().GetProject(ctx, projectID, 0); err == nil && proj != nil {
|
||||
title = proj.Name
|
||||
}
|
||||
}
|
||||
sessionID := fmt.Sprintf("sess-p%d-u%d", projectID, memberUserID)
|
||||
if _, err := s.ctxMgr.EnsureChatSessionOwned(ctx, sessionID, memberUserID, projectID, title); err != nil {
|
||||
if _, err := s.ctxMgr().EnsureChatSessionOwned(ctx, sessionID, memberUserID, projectID, title); err != nil {
|
||||
log.Printf("[members] ensure chat session for project=%d user=%d failed: %v", projectID, memberUserID, err)
|
||||
}
|
||||
}
|
||||
@@ -151,8 +151,8 @@ func (s *Server) handleUpdateProjectMember(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
var existing *authz.ProjectMemberRecord
|
||||
if s.authorizer != nil {
|
||||
rec, err := s.authorizer.GetProjectMember(r.Context(), projectID, req.UserID)
|
||||
if s.authorizer() != nil {
|
||||
rec, err := s.authorizer().GetProjectMember(r.Context(), projectID, req.UserID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -169,7 +169,7 @@ func (s *Server) handleUpdateProjectMember(w http.ResponseWriter, r *http.Reques
|
||||
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"project_role required when member does not exist"}`)
|
||||
return
|
||||
}
|
||||
if err := s.authorizer.UpsertProjectMemberSpec(r.Context(), projectID, req.UserID, authz.MemberUpsert{
|
||||
if err := s.authorizer().UpsertProjectMemberSpec(r.Context(), projectID, req.UserID, authz.MemberUpsert{
|
||||
ProjectRole: role,
|
||||
DocumentDuty: req.DocumentDuty,
|
||||
}); err != nil {
|
||||
@@ -179,7 +179,7 @@ func (s *Server) handleUpdateProjectMember(w http.ResponseWriter, r *http.Reques
|
||||
wrote = true
|
||||
}
|
||||
if req.Status != "" {
|
||||
if err := s.authorizer.SetProjectMemberStatus(r.Context(), projectID, req.UserID, req.Status); err != nil {
|
||||
if err := s.authorizer().SetProjectMemberStatus(r.Context(), projectID, req.UserID, req.Status); err != nil {
|
||||
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -204,7 +204,7 @@ func (s *Server) handleRemoveProjectMember(w http.ResponseWriter, r *http.Reques
|
||||
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"user_id query required"}`)
|
||||
return
|
||||
}
|
||||
if err := s.authorizer.SetProjectMemberStatus(r.Context(), projectID, targetID, authz.StatusExited); err != nil {
|
||||
if err := s.authorizer().SetProjectMemberStatus(r.Context(), projectID, targetID, authz.StatusExited); err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,13 +19,13 @@ func (s *Server) handleGetDefaultWorkflowPlan(w http.ResponseWriter, r *http.Req
|
||||
if _, ok := s.requireProjectAccess(w, r, projectID); !ok {
|
||||
return
|
||||
}
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||
return
|
||||
}
|
||||
workflowKey := r.URL.Query().Get("workflow_key")
|
||||
version, _ := strconv.Atoi(r.URL.Query().Get("version"))
|
||||
plan, err := s.instSvc.BuildDefaultPlan(r.Context(), workflowKey, version)
|
||||
plan, err := s.instSvc().BuildDefaultPlan(r.Context(), workflowKey, version)
|
||||
if err != nil {
|
||||
writeProjectInstanceError(w, err)
|
||||
return
|
||||
@@ -43,12 +43,12 @@ func (s *Server) handleGetProjectSettings(w http.ResponseWriter, r *http.Request
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用,请先执行数据库迁移"}`)
|
||||
return
|
||||
}
|
||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
settings, err := s.instSvc.GetSettings(r.Context(), projectID, user.ID, isCreator)
|
||||
isCreator, _ := s.pwSvc().IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
settings, err := s.instSvc().GetSettings(r.Context(), projectID, user.ID, isCreator)
|
||||
if err != nil {
|
||||
writeProjectInstanceError(w, err)
|
||||
return
|
||||
@@ -58,8 +58,8 @@ func (s *Server) handleGetProjectSettings(w http.ResponseWriter, r *http.Request
|
||||
RecentWorkflowEvents []workflowEventSummary `json:"recent_workflow_events,omitempty"`
|
||||
}
|
||||
out := settingsResponse{ProjectSettings: settings}
|
||||
if s.notifySvc != nil && s.notifySvc.Available() {
|
||||
items, listErr := s.notifySvc.ListProjectNotifications(r.Context(), user.ID, projectID, 0, 20)
|
||||
if s.notifySvc() != nil && s.notifySvc().Available() {
|
||||
items, listErr := s.notifySvc().ListProjectNotifications(r.Context(), user.ID, projectID, 0, 20)
|
||||
if listErr == nil {
|
||||
for _, n := range items {
|
||||
out.RecentWorkflowEvents = append(out.RecentWorkflowEvents, workflowEventSummary{
|
||||
@@ -88,11 +88,11 @@ func (s *Server) handleSaveWorkflowPlan(w http.ResponseWriter, r *http.Request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||
return
|
||||
}
|
||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
isCreator, _ := s.pwSvc().IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
var req projectinstance.SavePlanInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`)
|
||||
@@ -100,9 +100,9 @@ func (s *Server) handleSaveWorkflowPlan(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
var settings *projectinstance.ProjectSettings
|
||||
if req.LiveEdit {
|
||||
settings, err = s.instSvc.UpdateLivePlan(r.Context(), projectID, user.ID, isCreator, req)
|
||||
settings, err = s.instSvc().UpdateLivePlan(r.Context(), projectID, user.ID, isCreator, req)
|
||||
} else {
|
||||
settings, err = s.instSvc.SaveWorkflowPlan(r.Context(), projectID, user.ID, isCreator, req)
|
||||
settings, err = s.instSvc().SaveWorkflowPlan(r.Context(), projectID, user.ID, isCreator, req)
|
||||
}
|
||||
if err != nil {
|
||||
writeProjectInstanceError(w, err)
|
||||
@@ -121,12 +121,12 @@ func (s *Server) handleActivationPreview(w http.ResponseWriter, r *http.Request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||
return
|
||||
}
|
||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
preview, err := s.instSvc.ActivationPreview(r.Context(), projectID, user.ID, isCreator)
|
||||
isCreator, _ := s.pwSvc().IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
preview, err := s.instSvc().ActivationPreview(r.Context(), projectID, user.ID, isCreator)
|
||||
if err != nil {
|
||||
writeProjectInstanceError(w, err)
|
||||
return
|
||||
@@ -144,17 +144,17 @@ func (s *Server) handleActivateWorkflowInstance(w http.ResponseWriter, r *http.R
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||
return
|
||||
}
|
||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
isCreator, _ := s.pwSvc().IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
var req projectinstance.ActivateInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`)
|
||||
return
|
||||
}
|
||||
result, err := s.instSvc.Activate(r.Context(), projectID, user.ID, isCreator, req)
|
||||
result, err := s.instSvc().Activate(r.Context(), projectID, user.ID, isCreator, req)
|
||||
if err != nil {
|
||||
writeProjectInstanceError(w, err)
|
||||
return
|
||||
@@ -237,15 +237,15 @@ func writeProjectInstanceError(w http.ResponseWriter, err error) {
|
||||
}
|
||||
|
||||
func (s *Server) blockLegacyWorkflowSwitch(w http.ResponseWriter, r *http.Request, projectID int64) bool {
|
||||
proj, err := s.pwSvc.GetProject(r.Context(), projectID, 0)
|
||||
proj, err := s.pwSvc().GetProject(r.Context(), projectID, 0)
|
||||
if err == nil && proj != nil && proj.LifecycleStatus == "planning" && proj.ActiveWorkflowKey == "" {
|
||||
writeSafeError(w, r, http.StatusConflict, `{"error":"LEGACY_SWITCH_BLOCKED","message":"新项目请通过项目设置完成工作流激活"}`)
|
||||
return true
|
||||
}
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
return false
|
||||
}
|
||||
live, err := s.instSvc.HasLiveInstance(r.Context(), projectID)
|
||||
live, err := s.instSvc().HasLiveInstance(r.Context(), projectID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||
return true
|
||||
|
||||
@@ -24,7 +24,7 @@ func (s *Server) handleReconcileWorkspace(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
rec, err := s.taskRuns.GetRun(body.RunID)
|
||||
rec, err := s.taskRuns().GetRun(body.RunID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusNotFound, "run not found")
|
||||
return
|
||||
@@ -34,7 +34,7 @@ func (s *Server) handleReconcileWorkspace(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
workDir, err := s.resolver.ResolveStoredWorkDir(rec.WorkspaceID)
|
||||
workDir, err := s.resolver().ResolveStoredWorkDir(rec.WorkspaceID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusForbidden, "invalid workspace path")
|
||||
return
|
||||
|
||||
@@ -19,7 +19,7 @@ func (s *Server) handleGetExportCapability(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
if s.ctxMgr == nil {
|
||||
if s.ctxMgr() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||
return
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func (s *Server) handleGetExportCapability(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := s.ctxMgr.GetMessageByID(r.Context(), msgID)
|
||||
msg, err := s.ctxMgr().GetMessageByID(r.Context(), msgID)
|
||||
if err != nil || msg == nil {
|
||||
writeJSONResponse(w, http.StatusNotFound, map[string]string{
|
||||
"error_code": "MESSAGE_NOT_FOUND",
|
||||
@@ -86,7 +86,7 @@ func (s *Server) handleGetExportSource(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if s.ctxMgr == nil {
|
||||
if s.ctxMgr() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||
return
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func (s *Server) handleGetExportSource(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := s.ctxMgr.GetMessageByID(r.Context(), msgID)
|
||||
msg, err := s.ctxMgr().GetMessageByID(r.Context(), msgID)
|
||||
if err != nil || msg == nil {
|
||||
writeJSONResponse(w, http.StatusNotFound, map[string]string{
|
||||
"error_code": "MESSAGE_NOT_FOUND",
|
||||
@@ -187,7 +187,7 @@ func (s *Server) handlePinExport(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.ctxMgr == nil || s.vaultIngest == nil {
|
||||
if s.ctxMgr() == nil || s.vaultIngest == nil {
|
||||
http.Error(w, "artifact vault not initialized", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
@@ -199,7 +199,7 @@ func (s *Server) handlePinExport(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
msg, err := s.ctxMgr.GetMessageByID(r.Context(), msgID)
|
||||
msg, err := s.ctxMgr().GetMessageByID(r.Context(), msgID)
|
||||
if err != nil || msg == nil {
|
||||
writeJSONResponse(w, http.StatusNotFound, map[string]string{
|
||||
"error_code": "MESSAGE_NOT_FOUND",
|
||||
@@ -217,7 +217,7 @@ func (s *Server) handlePinExport(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
turn, err := s.ctxMgr.GetChatTurnByAssistantMessageID(r.Context(), msg.SessionID, msg.ID)
|
||||
turn, err := s.ctxMgr().GetChatTurnByAssistantMessageID(r.Context(), msg.SessionID, msg.ID)
|
||||
if err != nil || turn == nil || turn.Status != ctxmgr.TurnStatusCompleted || turn.CompletedAt == nil {
|
||||
writeJSONResponse(w, http.StatusConflict, map[string]string{
|
||||
"error_code": "TURN_NOT_COMPLETED",
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"chat-agent-system/internal/app/intentflow"
|
||||
"chat-agent-system/internal/app/stageflow"
|
||||
"chat-agent-system/internal/auth"
|
||||
"chat-agent-system/internal/authz"
|
||||
"chat-agent-system/internal/bizdev"
|
||||
"chat-agent-system/internal/coordinator"
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/docrevision"
|
||||
"chat-agent-system/internal/freedoc"
|
||||
"chat-agent-system/internal/intent"
|
||||
"chat-agent-system/internal/kb"
|
||||
"chat-agent-system/internal/maintenance"
|
||||
"chat-agent-system/internal/notification"
|
||||
"chat-agent-system/internal/projectinstance"
|
||||
"chat-agent-system/internal/projectworkflow"
|
||||
"chat-agent-system/internal/storage"
|
||||
"chat-agent-system/internal/sysadmin"
|
||||
"chat-agent-system/internal/taskrun"
|
||||
"chat-agent-system/internal/workflowdef"
|
||||
"chat-agent-system/internal/workspace"
|
||||
)
|
||||
|
||||
// 本文件集中承接 Server 对 app.Runtime 领域服务的访问(module-batches-ag 镜像字段收口)。
|
||||
// Server 不再镜像 Runtime 字段;全部经访问器取用,nil 安全。
|
||||
// testOverrides 仅供测试注入替换件;set 字段后(含显式置 nil 经 *X 指针形)覆盖访问器结果,
|
||||
// 生产路径恒为空。显式置 nil 用 ptr 形(authorizerPtr 等)以区分「未设置」与「设为 nil」。
|
||||
|
||||
type serverTestOverrides struct {
|
||||
svc *bizdev.Service
|
||||
pwSvc *projectworkflow.Service
|
||||
engine *coordinator.CoordinatorEngine
|
||||
authSvc *auth.Service
|
||||
taskRuns *taskrun.Manager
|
||||
authorizer *authz.Authorizer
|
||||
authzSvc *authz.Service
|
||||
wfDefSvc *workflowdef.Service
|
||||
instSvc *projectinstance.Service
|
||||
notifySvc *notification.Service
|
||||
resolver *workspace.Resolver
|
||||
wsCap *workspace.Capability
|
||||
cmds *taskrun.Commands
|
||||
stageflow *stageflow.UseCase
|
||||
gov *docgov.GovernanceEngine
|
||||
rev *docrevision.Service
|
||||
ctxMgr *ctxmgr.ContextManager
|
||||
cas *storage.CAS
|
||||
freeDoc *freedoc.Service
|
||||
kb *kb.Client
|
||||
sysadmin *sysadmin.Service
|
||||
maintenance *maintenance.Service
|
||||
|
||||
// 显式置 nil 的位标记(区分「未设置」与「设为 nil」,fail-closed 测试用)。
|
||||
authorizerNil bool
|
||||
instSvcNil bool
|
||||
}
|
||||
|
||||
func (s *Server) overrides() *serverTestOverrides {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.testOverrides
|
||||
}
|
||||
|
||||
func (s *Server) svc() *bizdev.Service {
|
||||
if o := s.overrides(); o != nil && o.svc != nil {
|
||||
return o.svc
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Biz
|
||||
}
|
||||
|
||||
func (s *Server) pwSvc() *projectworkflow.Service {
|
||||
if o := s.overrides(); o != nil && o.pwSvc != nil {
|
||||
return o.pwSvc
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.PW
|
||||
}
|
||||
|
||||
func (s *Server) rec() intent.Recognizer {
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Rec
|
||||
}
|
||||
|
||||
func (s *Server) engine() *coordinator.CoordinatorEngine {
|
||||
if o := s.overrides(); o != nil && o.engine != nil {
|
||||
return o.engine
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Engine
|
||||
}
|
||||
|
||||
func (s *Server) authSvc() *auth.Service {
|
||||
if o := s.overrides(); o != nil && o.authSvc != nil {
|
||||
return o.authSvc
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Auth
|
||||
}
|
||||
|
||||
func (s *Server) taskRuns() *taskrun.Manager {
|
||||
if o := s.overrides(); o != nil && o.taskRuns != nil {
|
||||
return o.taskRuns
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.TaskRuns
|
||||
}
|
||||
|
||||
func (s *Server) authorizer() *authz.Authorizer {
|
||||
if o := s.overrides(); o != nil {
|
||||
if o.authorizerNil {
|
||||
return nil
|
||||
}
|
||||
if o.authorizer != nil {
|
||||
return o.authorizer
|
||||
}
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Authorizer
|
||||
}
|
||||
|
||||
func (s *Server) authzSvc() *authz.Service {
|
||||
if o := s.overrides(); o != nil && o.authzSvc != nil {
|
||||
return o.authzSvc
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Authz
|
||||
}
|
||||
|
||||
func (s *Server) wfDefSvc() *workflowdef.Service {
|
||||
if o := s.overrides(); o != nil && o.wfDefSvc != nil {
|
||||
return o.wfDefSvc
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.WFDef
|
||||
}
|
||||
|
||||
func (s *Server) instSvc() *projectinstance.Service {
|
||||
if o := s.overrides(); o != nil {
|
||||
if o.instSvcNil {
|
||||
return nil
|
||||
}
|
||||
if o.instSvc != nil {
|
||||
return o.instSvc
|
||||
}
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Inst
|
||||
}
|
||||
|
||||
func (s *Server) notifySvc() *notification.Service {
|
||||
if o := s.overrides(); o != nil && o.notifySvc != nil {
|
||||
return o.notifySvc
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Notify
|
||||
}
|
||||
|
||||
func (s *Server) resolver() *workspace.Resolver {
|
||||
if o := s.overrides(); o != nil && o.resolver != nil {
|
||||
return o.resolver
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Resolver
|
||||
}
|
||||
|
||||
func (s *Server) wsCap() *workspace.Capability {
|
||||
if o := s.overrides(); o != nil && o.wsCap != nil {
|
||||
return o.wsCap
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.WSCap
|
||||
}
|
||||
|
||||
func (s *Server) cmds() *taskrun.Commands {
|
||||
if o := s.overrides(); o != nil && o.cmds != nil {
|
||||
return o.cmds
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Commands
|
||||
}
|
||||
|
||||
func (s *Server) stageflow() *stageflow.UseCase {
|
||||
if o := s.overrides(); o != nil && o.stageflow != nil {
|
||||
return o.stageflow
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Stageflow
|
||||
}
|
||||
|
||||
func (s *Server) gov() *docgov.GovernanceEngine {
|
||||
if o := s.overrides(); o != nil && o.gov != nil {
|
||||
return o.gov
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Gov
|
||||
}
|
||||
|
||||
func (s *Server) rev() *docrevision.Service {
|
||||
if o := s.overrides(); o != nil && o.rev != nil {
|
||||
return o.rev
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Rev
|
||||
}
|
||||
|
||||
func (s *Server) ctxMgr() *ctxmgr.ContextManager {
|
||||
if o := s.overrides(); o != nil && o.ctxMgr != nil {
|
||||
return o.ctxMgr
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Ctx
|
||||
}
|
||||
|
||||
func (s *Server) cas() *storage.CAS {
|
||||
if o := s.overrides(); o != nil && o.cas != nil {
|
||||
return o.cas
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.CAS
|
||||
}
|
||||
|
||||
func (s *Server) freeDoc() *freedoc.Service {
|
||||
if o := s.overrides(); o != nil && o.freeDoc != nil {
|
||||
return o.freeDoc
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.FreeDoc
|
||||
}
|
||||
|
||||
func (s *Server) kb() *kb.Client {
|
||||
if o := s.overrides(); o != nil && o.kb != nil {
|
||||
return o.kb
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.KB
|
||||
}
|
||||
|
||||
func (s *Server) sysadmin() *sysadmin.Service {
|
||||
if o := s.overrides(); o != nil && o.sysadmin != nil {
|
||||
return o.sysadmin
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.SysAdmin
|
||||
}
|
||||
|
||||
func (s *Server) maintenance() *maintenance.Service {
|
||||
if o := s.overrides(); o != nil && o.maintenance != nil {
|
||||
return o.maintenance
|
||||
}
|
||||
if s == nil || s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.Maintenance
|
||||
}
|
||||
|
||||
func (s *Server) intentPlans() intentflow.Service {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
if s.intentPlansOverride != nil {
|
||||
return s.intentPlansOverride
|
||||
}
|
||||
if s.rt == nil {
|
||||
return nil
|
||||
}
|
||||
return s.rt.IntentPlans
|
||||
}
|
||||
|
||||
// setOverride* 系列仅供测试注入替换件(serverTestOverrides);生产代码禁止调用。
|
||||
|
||||
func (s *Server) setOverrideIntentPlans(v intentflow.Service) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.intentPlansOverride = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideSvc(v *bizdev.Service) {
|
||||
s.ensureOverrides().svc = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverridePwSvc(v *projectworkflow.Service) {
|
||||
s.ensureOverrides().pwSvc = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideEngine(v *coordinator.CoordinatorEngine) {
|
||||
s.ensureOverrides().engine = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideAuthSvc(v *auth.Service) {
|
||||
s.ensureOverrides().authSvc = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideTaskRuns(v *taskrun.Manager) {
|
||||
s.ensureOverrides().taskRuns = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideAuthorizer(v *authz.Authorizer) {
|
||||
o := s.ensureOverrides()
|
||||
if v == nil {
|
||||
o.authorizerNil = true
|
||||
o.authorizer = nil
|
||||
return
|
||||
}
|
||||
o.authorizerNil = false
|
||||
o.authorizer = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideAuthzSvc(v *authz.Service) {
|
||||
s.ensureOverrides().authzSvc = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideWfDefSvc(v *workflowdef.Service) {
|
||||
s.ensureOverrides().wfDefSvc = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideInstSvc(v *projectinstance.Service) {
|
||||
o := s.ensureOverrides()
|
||||
if v == nil {
|
||||
o.instSvcNil = true
|
||||
o.instSvc = nil
|
||||
return
|
||||
}
|
||||
o.instSvcNil = false
|
||||
o.instSvc = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideNotifySvc(v *notification.Service) {
|
||||
s.ensureOverrides().notifySvc = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideResolver(v *workspace.Resolver) {
|
||||
s.ensureOverrides().resolver = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideWsCap(v *workspace.Capability) {
|
||||
s.ensureOverrides().wsCap = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideCmds(v *taskrun.Commands) {
|
||||
s.ensureOverrides().cmds = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideStageflow(v *stageflow.UseCase) {
|
||||
s.ensureOverrides().stageflow = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideGov(v *docgov.GovernanceEngine) {
|
||||
s.ensureOverrides().gov = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideRev(v *docrevision.Service) {
|
||||
s.ensureOverrides().rev = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideCtxMgr(v *ctxmgr.ContextManager) {
|
||||
s.ensureOverrides().ctxMgr = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideCas(v *storage.CAS) {
|
||||
s.ensureOverrides().cas = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideFreeDoc(v *freedoc.Service) {
|
||||
s.ensureOverrides().freeDoc = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideKb(v *kb.Client) {
|
||||
s.ensureOverrides().kb = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideSysadmin(v *sysadmin.Service) {
|
||||
s.ensureOverrides().sysadmin = v
|
||||
}
|
||||
|
||||
func (s *Server) setOverrideMaintenance(v *maintenance.Service) {
|
||||
s.ensureOverrides().maintenance = v
|
||||
}
|
||||
|
||||
func (s *Server) ensureOverrides() *serverTestOverrides {
|
||||
if s.testOverrides == nil {
|
||||
s.testOverrides = &serverTestOverrides{}
|
||||
}
|
||||
return s.testOverrides
|
||||
}
|
||||
+64
-119
@@ -17,26 +17,16 @@ import (
|
||||
"chat-agent-system/internal/app/stageflow"
|
||||
"chat-agent-system/internal/artifactvault"
|
||||
"chat-agent-system/internal/auth"
|
||||
"chat-agent-system/internal/authz"
|
||||
"chat-agent-system/internal/bizdev"
|
||||
"chat-agent-system/internal/chat"
|
||||
"chat-agent-system/internal/config"
|
||||
"chat-agent-system/internal/coordinator"
|
||||
"chat-agent-system/internal/ctxmgr"
|
||||
"chat-agent-system/internal/docgov"
|
||||
"chat-agent-system/internal/docrevision"
|
||||
"chat-agent-system/internal/freedoc"
|
||||
"chat-agent-system/internal/handoff"
|
||||
"chat-agent-system/internal/intent"
|
||||
"chat-agent-system/internal/kb"
|
||||
"chat-agent-system/internal/maintenance"
|
||||
"chat-agent-system/internal/notification"
|
||||
"chat-agent-system/internal/projectinstance"
|
||||
"chat-agent-system/internal/projectworkflow"
|
||||
"chat-agent-system/internal/storage"
|
||||
"chat-agent-system/internal/sysadmin"
|
||||
"chat-agent-system/internal/taskrun"
|
||||
"chat-agent-system/internal/workflowdef"
|
||||
"chat-agent-system/internal/workspace"
|
||||
)
|
||||
|
||||
@@ -44,43 +34,26 @@ import (
|
||||
// Pool 与领域服务装配归属 app.Runtime;本结构仅保留 HTTP 适配所需句柄。
|
||||
// 残留具体领域字段见 short-term 任务记录(增量收口,本批先去掉 *pgxpool.Pool)。
|
||||
type Server struct {
|
||||
rt *app.Runtime
|
||||
cfg *config.Config
|
||||
svc *bizdev.Service
|
||||
pwSvc *projectworkflow.Service
|
||||
rec intent.Recognizer
|
||||
engine *coordinator.CoordinatorEngine
|
||||
authSvc *auth.Service
|
||||
sessions map[string]*chat.Session
|
||||
sessionsMu sync.RWMutex
|
||||
taskRuns *taskrun.Manager
|
||||
authorizer *authz.Authorizer
|
||||
authzSvc *authz.Service
|
||||
wfDefSvc *workflowdef.Service
|
||||
instSvc *projectinstance.Service
|
||||
notifySvc *notification.Service
|
||||
supplementImpact SupplementImpactService
|
||||
resolver *workspace.Resolver
|
||||
wsCap *workspace.Capability
|
||||
cmds *taskrun.Commands
|
||||
chatLog chatflow.ChatLog
|
||||
assembler chatflow.ContextAssembler
|
||||
stageflow *stageflow.UseCase
|
||||
gov *docgov.GovernanceEngine
|
||||
rev *docrevision.Service
|
||||
ctxMgr *ctxmgr.ContextManager
|
||||
cas *storage.CAS
|
||||
freeDoc *freedoc.Service
|
||||
kb *kb.Client
|
||||
sysadmin *sysadmin.Service
|
||||
maintenance *maintenance.Service
|
||||
listener net.Listener
|
||||
lastMaintToken string
|
||||
wsReg *workspace.FileRegistry
|
||||
wsGCStop chan struct{}
|
||||
chatTurnSweepStop chan struct{}
|
||||
httpServer *http.Server
|
||||
intentPlans intentflow.Service
|
||||
rt *app.Runtime
|
||||
cfg *config.Config
|
||||
sessions map[string]*chat.Session
|
||||
sessionsMu sync.RWMutex
|
||||
supplementImpact SupplementImpactService
|
||||
// intentPlansOverride 仅测试注入用(SetIntentPlansForTest);为空回落 rt.IntentPlans。
|
||||
intentPlansOverride intentflow.Service
|
||||
// testOverrides 仅供测试注入替换件;生产路径恒为 nil。
|
||||
testOverrides *serverTestOverrides
|
||||
// 后台循环停止控制(backgroundLoop,修 Shutdown 与 loop 的 DATA RACE)。
|
||||
wsGCLoop backgroundLoop
|
||||
reminderLoop backgroundLoop
|
||||
vaultLoop backgroundLoop
|
||||
chatTurnSweepLoop backgroundLoop
|
||||
chatLog chatflow.ChatLog
|
||||
assembler chatflow.ContextAssembler
|
||||
listener net.Listener
|
||||
lastMaintToken string
|
||||
wsReg *workspace.FileRegistry
|
||||
httpServer *http.Server
|
||||
planAccessFn func(ctx context.Context, projectID, userID int64) error
|
||||
planSessionOwnerFn func(ctx context.Context, sessionID string) (int64, error)
|
||||
|
||||
@@ -90,8 +63,6 @@ type Server struct {
|
||||
vaultPub *artifactvault.Publisher
|
||||
vaultIngest *artifactvault.Ingest
|
||||
vaultPublisherStarted int32
|
||||
vaultStop chan struct{}
|
||||
reminderStop chan struct{}
|
||||
loginLimiter *loginRateLimiter
|
||||
healthMu sync.Mutex
|
||||
healthCache healthSnapshot
|
||||
@@ -114,42 +85,17 @@ func NewFromRuntime(rt *app.Runtime) *Server {
|
||||
s := &Server{
|
||||
rt: rt,
|
||||
cfg: rt.Cfg,
|
||||
svc: rt.Biz,
|
||||
pwSvc: rt.PW,
|
||||
rec: rt.Rec,
|
||||
engine: rt.Engine,
|
||||
authSvc: rt.Auth,
|
||||
sessions: make(map[string]*chat.Session),
|
||||
taskRuns: rt.TaskRuns,
|
||||
authorizer: rt.Authorizer,
|
||||
authzSvc: rt.Authz,
|
||||
wfDefSvc: rt.WFDef,
|
||||
instSvc: rt.Inst,
|
||||
notifySvc: rt.Notify,
|
||||
supplementImpact: newSupplementImpactAdapter(rt.SupplementImpact),
|
||||
resolver: rt.Resolver,
|
||||
wsCap: rt.WSCap,
|
||||
cmds: rt.Commands,
|
||||
stageflow: rt.Stageflow,
|
||||
gov: rt.Gov,
|
||||
rev: rt.Rev,
|
||||
ctxMgr: rt.Ctx,
|
||||
cas: rt.CAS,
|
||||
freeDoc: rt.FreeDoc,
|
||||
kb: rt.KB,
|
||||
sysadmin: rt.SysAdmin,
|
||||
maintenance: rt.Maintenance,
|
||||
wsReg: workspaceRegistryFor(rt.Cfg),
|
||||
intentPlans: rt.IntentPlans,
|
||||
// 登录/注册限流:10 次 / 5 分钟 / (IP+username) 滑动窗口
|
||||
loginLimiter: newLoginRateLimiter(10, 5*time.Minute),
|
||||
}
|
||||
s.chatLog = chatflow.NewAdapter(s.ctxMgr, s.resolveStageAccess, s.failClosedAuthz())
|
||||
s.chatLog = chatflow.NewAdapter(s.ctxMgr(), s.resolveStageAccess, s.failClosedAuthz())
|
||||
s.assembler = chatflow.NewAssembler(coordinator.NewContextAssembler(), s.chatLog)
|
||||
rt.ChatLog = s.chatLog
|
||||
rt.Assembler = s.assembler
|
||||
if s.instSvc != nil && s.instSvc.Available() && s.svc != nil {
|
||||
s.svc.SetStageAccessGate(newBizdevStageGate(s.instSvc, s.pwSvc))
|
||||
if s.instSvc() != nil && s.instSvc().Available() && s.svc() != nil {
|
||||
s.svc().SetStageAccessGate(newBizdevStageGate(s.instSvc(), s.pwSvc()))
|
||||
}
|
||||
s.initTaskRunExecutors()
|
||||
s.initArtifactVault()
|
||||
@@ -161,6 +107,17 @@ func NewFromRuntime(rt *app.Runtime) *Server {
|
||||
return s
|
||||
}
|
||||
|
||||
// newMinimalRuntime 构造只带测试所需字段的 app.Runtime(task_finalize 系列专用)。
|
||||
func newMinimalRuntime(cfg *config.Config, svc *bizdev.Service, pwSvc *projectworkflow.Service, ctxMgr *ctxmgr.ContextManager) *app.Runtime {
|
||||
return &app.Runtime{
|
||||
Cfg: cfg,
|
||||
Biz: svc,
|
||||
PW: pwSvc,
|
||||
Ctx: ctxMgr,
|
||||
TaskRuns: taskrun.NewManager(pwSvc, ctxMgr),
|
||||
}
|
||||
}
|
||||
|
||||
// NewServer 构造函数(测试兼容:内部走 app.Assemble)
|
||||
func NewServer(cfg *config.Config, svc *bizdev.Service) *Server {
|
||||
rt, err := app.Assemble(cfg, svc)
|
||||
@@ -172,17 +129,17 @@ func NewServer(cfg *config.Config, svc *bizdev.Service) *Server {
|
||||
|
||||
// AuthService 获取认证服务
|
||||
func (s *Server) AuthService() *auth.Service {
|
||||
return s.authSvc
|
||||
return s.authSvc()
|
||||
}
|
||||
|
||||
// BizdevService 获取业务交付服务
|
||||
func (s *Server) BizdevService() *bizdev.Service {
|
||||
return s.svc
|
||||
return s.svc()
|
||||
}
|
||||
|
||||
// TaskRuns 获取长任务管理器(测试与运维钩子)。
|
||||
func (s *Server) TaskRuns() *taskrun.Manager {
|
||||
return s.taskRuns
|
||||
return s.taskRuns()
|
||||
}
|
||||
|
||||
// SetIntentPlansForTest swaps the intent-plan service (Wave 2 restart/refresh harness only).
|
||||
@@ -190,12 +147,12 @@ func (s *Server) SetIntentPlansForTest(plans intentflow.Service) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.intentPlans = plans
|
||||
s.intentPlansOverride = plans
|
||||
}
|
||||
|
||||
// ProjectWorkflowService 获取项目多工作流控制服务
|
||||
func (s *Server) ProjectWorkflowService() *projectworkflow.Service {
|
||||
return s.pwSvc
|
||||
return s.pwSvc()
|
||||
}
|
||||
|
||||
// Routes 构建 HTTP 路由分发器
|
||||
@@ -411,11 +368,11 @@ func (s *Server) Start() error {
|
||||
// 若从维护公告恢复,标记窗口 ended 并清理 token
|
||||
s.finishMaintenanceResume()
|
||||
|
||||
if s.maintenance != nil {
|
||||
s.maintenance.SetHandoff(func(ctx context.Context, w maintenance.Window) error {
|
||||
if s.maintenance() != nil {
|
||||
s.maintenance().SetHandoff(func(ctx context.Context, w maintenance.Window) error {
|
||||
return s.performMaintenanceHandoff(w)
|
||||
})
|
||||
s.maintenance.Start(context.Background())
|
||||
s.maintenance().Start(context.Background())
|
||||
}
|
||||
|
||||
s.httpServer = &http.Server{
|
||||
@@ -431,14 +388,14 @@ func (s *Server) Start() error {
|
||||
}
|
||||
|
||||
func (s *Server) finishMaintenanceResume() {
|
||||
if os.Getenv("MAINT_RESUME") != "1" || s.maintenance == nil {
|
||||
if os.Getenv("MAINT_RESUME") != "1" || s.maintenance() == nil {
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseInt(os.Getenv("MAINT_WINDOW_ID"), 10, 64)
|
||||
if id > 0 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_, _ = s.maintenance.MarkEnded(ctx, id)
|
||||
_, _ = s.maintenance().MarkEnded(ctx, id)
|
||||
}
|
||||
dataDir := config.DefaultDataDir()
|
||||
if s.cfg != nil && s.cfg.DataDir != "" {
|
||||
@@ -451,27 +408,15 @@ func (s *Server) finishMaintenanceResume() {
|
||||
|
||||
// Shutdown 优雅停机
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
if s.wsGCStop != nil {
|
||||
close(s.wsGCStop)
|
||||
s.wsGCStop = nil
|
||||
s.wsGCLoop.stop()
|
||||
s.reminderLoop.stop()
|
||||
if s.maintenance() != nil {
|
||||
s.maintenance().Stop()
|
||||
}
|
||||
if s.reminderStop != nil {
|
||||
close(s.reminderStop)
|
||||
s.reminderStop = nil
|
||||
}
|
||||
if s.maintenance != nil {
|
||||
s.maintenance.Stop()
|
||||
}
|
||||
if s.vaultStop != nil {
|
||||
close(s.vaultStop)
|
||||
s.vaultStop = nil
|
||||
}
|
||||
if s.chatTurnSweepStop != nil {
|
||||
close(s.chatTurnSweepStop)
|
||||
s.chatTurnSweepStop = nil
|
||||
}
|
||||
if s.taskRuns != nil {
|
||||
_ = s.taskRuns.WaitIdle(ctx)
|
||||
s.vaultLoop.stop()
|
||||
s.chatTurnSweepLoop.stop()
|
||||
if s.taskRuns() != nil {
|
||||
_ = s.taskRuns().WaitIdle(ctx)
|
||||
}
|
||||
if s.rt != nil {
|
||||
_ = s.rt.Stop(ctx)
|
||||
@@ -549,9 +494,9 @@ func (s *Server) getOrCreateSession(
|
||||
}
|
||||
|
||||
// 归属校验必须先于任何缓存命中/持久层灌入
|
||||
if callerUserID > 0 && s.ctxMgr != nil {
|
||||
if callerUserID > 0 && s.ctxMgr() != nil {
|
||||
dbCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
owner, err := s.ctxMgr.GetSessionOwnerID(dbCtx, sessionID)
|
||||
owner, err := s.ctxMgr().GetSessionOwnerID(dbCtx, sessionID)
|
||||
cancel()
|
||||
if err == nil && owner > 0 && owner != callerUserID {
|
||||
return nil, ctxmgr.ErrSessionOwnerMismatch
|
||||
@@ -589,9 +534,9 @@ func (s *Server) getOrCreateSession(
|
||||
|
||||
// 尝试从持久层恢复会话上下文 (服务重启后跨进程保持 G3)
|
||||
var restored *ctxmgr.RestoredSessionContext
|
||||
if s.ctxMgr != nil {
|
||||
if s.ctxMgr() != nil {
|
||||
dbCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
r, err := s.ctxMgr.RestoreSessionContext(dbCtx, sessionID)
|
||||
r, err := s.ctxMgr().RestoreSessionContext(dbCtx, sessionID)
|
||||
cancel()
|
||||
if err == nil && r != nil && len(r.Messages) > 0 {
|
||||
restored = r
|
||||
@@ -657,8 +602,8 @@ func (s *Server) getOrCreateSession(
|
||||
}
|
||||
}
|
||||
|
||||
sess := chat.NewSession(sessionID, userCtx, s.rec, s.engine)
|
||||
sess.CtxMgr = s.ctxMgr
|
||||
sess := chat.NewSession(sessionID, userCtx, s.rec(), s.engine())
|
||||
sess.CtxMgr = s.ctxMgr()
|
||||
env := "dev"
|
||||
if s.cfg != nil && s.cfg.Env != "" {
|
||||
env = s.cfg.Env
|
||||
@@ -666,9 +611,9 @@ func (s *Server) getOrCreateSession(
|
||||
// Adapters live in internal/app; use server fields so SetIntentPlansForTest stays effective.
|
||||
sess.ChatLog = app.AdaptChatMemory(s.chatLog)
|
||||
sess.Actions = &chat.ActionServices{
|
||||
DocGov: s.gov,
|
||||
TaskRuns: app.AdaptChatTaskStarter(s.taskRuns),
|
||||
IntentPlans: app.AdaptChatIntentPlans(s.intentPlans),
|
||||
DocGov: s.gov(),
|
||||
TaskRuns: app.AdaptChatTaskStarter(s.taskRuns()),
|
||||
IntentPlans: app.AdaptChatIntentPlans(s.intentPlans()),
|
||||
Environment: env,
|
||||
}
|
||||
if s.vaultIngest != nil {
|
||||
@@ -765,10 +710,10 @@ func (s *Server) taskUseCase() *stageflow.UseCase {
|
||||
if s == nil {
|
||||
return stageflow.New(nil)
|
||||
}
|
||||
if s.stageflow != nil {
|
||||
return s.stageflow
|
||||
if s.stageflow() != nil {
|
||||
return s.stageflow()
|
||||
}
|
||||
return stageflow.New(s.svc)
|
||||
return stageflow.New(s.svc())
|
||||
}
|
||||
|
||||
func (s *Server) corsMiddleware(next http.Handler) http.Handler {
|
||||
|
||||
@@ -126,14 +126,14 @@ func TestServer_ChatDocumentSummaryReadsExistingDocumentWithoutWriteAction(t *te
|
||||
if err := os.WriteFile(filepath.Join(workDir, "output.md"), markdown, 0o644); err != nil {
|
||||
t.Fatalf("write output.md: %v", err)
|
||||
}
|
||||
if _, err := srv.gov.CreateOrGetDocument(
|
||||
if _, err := srv.gov().CreateOrGetDocument(
|
||||
ctx, "doc_proj143_intake", 143, "intake", "用户初始需求收集表", "V0.1_triage", workDir,
|
||||
1, "编制人", 1, "审核人", 1, "批准人",
|
||||
); err != nil {
|
||||
t.Fatalf("create document: %v", err)
|
||||
}
|
||||
hash := fmt.Sprintf("%x", sha256.Sum256(markdown))
|
||||
if err := srv.gov.MarkDocumentContentUpdated(ctx, "doc_proj143_intake", hash, false); err != nil {
|
||||
if err := srv.gov().MarkDocumentContentUpdated(ctx, "doc_proj143_intake", hash, false); err != nil {
|
||||
t.Fatalf("mark content: %v", err)
|
||||
}
|
||||
|
||||
@@ -469,10 +469,10 @@ func TestServer_DocGovProposeAndDecideHTTP(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
docID := "doc_proj1_proposal_http"
|
||||
|
||||
if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "feasibility_reg", "法规评估", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
if _, err := srv.gov().CreateOrGetDocument(ctx, docID, 1, "feasibility_reg", "法规评估", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil {
|
||||
t.Fatalf("CreateOrGetDocument: %v", err)
|
||||
}
|
||||
if _, err := srv.gov.RecordDocumentViewed(ctx, docID, 1, docgov.RoleAuthor, "admin"); err != nil {
|
||||
if _, err := srv.gov().RecordDocumentViewed(ctx, docID, 1, docgov.RoleAuthor, "admin"); err != nil {
|
||||
t.Fatalf("claim: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -404,7 +404,7 @@ func (s *Server) handleRunTaskStream(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if req.ProjectID > 0 {
|
||||
if !s.isSystemAdminUser(r.Context(), user) {
|
||||
if err := s.authorizer.RequireProjectAccess(r.Context(), req.ProjectID, user.ID); err != nil {
|
||||
if err := s.authorizer().RequireProjectAccess(r.Context(), req.ProjectID, user.ID); err != nil {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
"error_code": "ERR_FORBIDDEN",
|
||||
"message": "无权访问该项目",
|
||||
@@ -440,8 +440,8 @@ func (s *Server) handleRunTaskStream(w http.ResponseWriter, r *http.Request) {
|
||||
// 幂等前置:与 REST 路径对齐,EventSource 断线重连命中已有 run 时直接复用,
|
||||
// 避免重复执行同一 client_turn_id 的任务。
|
||||
var reused *taskrun.RunRecord
|
||||
if idemKey := taskrun.BuildTaskRunIdempotencyKey(req.SessionID, req.ClientTurnID); idemKey != "" && s.taskRuns != nil {
|
||||
if existing, findErr := s.taskRuns.FindByIdempotencyKey(r.Context(), idemKey); findErr != nil {
|
||||
if idemKey := taskrun.BuildTaskRunIdempotencyKey(req.SessionID, req.ClientTurnID); idemKey != "" && s.taskRuns() != nil {
|
||||
if existing, findErr := s.taskRuns().FindByIdempotencyKey(r.Context(), idemKey); findErr != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, findErr.Error())
|
||||
return
|
||||
} else if existing != nil {
|
||||
@@ -462,7 +462,7 @@ func (s *Server) handleRunTaskStream(w http.ResponseWriter, r *http.Request) {
|
||||
created = reused
|
||||
} else {
|
||||
var err error
|
||||
created, err = s.taskRuns.CreateAndStart(r.Context(), runID, record, taskReq, stageReq, req.WorkDir)
|
||||
created, err = s.taskRuns().CreateAndStart(r.Context(), runID, record, taskReq, stageReq, req.WorkDir)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, "start task run failed: "+err.Error())
|
||||
return
|
||||
@@ -471,12 +471,12 @@ func (s *Server) handleRunTaskStream(w http.ResponseWriter, r *http.Request) {
|
||||
_ = includeDetails
|
||||
_ = requestID
|
||||
|
||||
ch, replay, err := s.taskRuns.Subscribe(created.RunID, 0)
|
||||
ch, replay, err := s.taskRuns().Subscribe(created.RunID, 0)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
defer s.taskRuns.Unsubscribe(created.RunID, ch)
|
||||
defer s.taskRuns().Unsubscribe(created.RunID, ch)
|
||||
|
||||
w.Header().Set("X-Request-Id", requestID)
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
|
||||
@@ -75,7 +75,7 @@ func TestRunTaskStream_CreateAndStartFails_BeforeSSEFlush(t *testing.T) {
|
||||
|
||||
mgr := taskrun.NewManager(nil, nil)
|
||||
mgr.SetStore(failInsertStore{})
|
||||
srv.taskRuns = mgr
|
||||
srv.setOverrideTaskRuns(mgr)
|
||||
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"task_key": "intake",
|
||||
|
||||
@@ -16,11 +16,11 @@ import (
|
||||
)
|
||||
|
||||
func (s *Server) resolveStageAccess(ctx context.Context, projectID, userID int64) (*projectinstance.StageAccess, error) {
|
||||
if s == nil || s.instSvc == nil || !s.instSvc.Available() || projectID <= 0 || userID <= 0 {
|
||||
if s == nil || s.instSvc() == nil || !s.instSvc().Available() || projectID <= 0 || userID <= 0 {
|
||||
return &projectinstance.StageAccess{}, nil
|
||||
}
|
||||
isCreator, _ := s.pwSvc.IsProjectCreator(ctx, projectID, userID)
|
||||
return s.instSvc.ResolveStageAccess(ctx, projectID, userID, isCreator)
|
||||
isCreator, _ := s.pwSvc().IsProjectCreator(ctx, projectID, userID)
|
||||
return s.instSvc().ResolveStageAccess(ctx, projectID, userID, isCreator)
|
||||
}
|
||||
|
||||
func (s *Server) checkWorkflowDocTaskAccess(ctx context.Context, projectID, userID int64, taskKey, action string) projectinstance.DocAccessDecision {
|
||||
@@ -30,9 +30,9 @@ func (s *Server) checkWorkflowDocTaskAccess(ctx context.Context, projectID, user
|
||||
if action == "" {
|
||||
action = projectinstance.DocAccessActionRun
|
||||
}
|
||||
if s.instSvc != nil && s.instSvc.Available() && projectID > 0 && userID > 0 {
|
||||
isCreator, _ := s.pwSvc.IsProjectCreator(ctx, projectID, userID)
|
||||
return s.instSvc.CheckDocTaskAccess(ctx, projectID, userID, taskKey, action, isCreator)
|
||||
if s.instSvc() != nil && s.instSvc().Available() && projectID > 0 && userID > 0 {
|
||||
isCreator, _ := s.pwSvc().IsProjectCreator(ctx, projectID, userID)
|
||||
return s.instSvc().CheckDocTaskAccess(ctx, projectID, userID, taskKey, action, isCreator)
|
||||
}
|
||||
access, err := s.resolveStageAccess(ctx, projectID, userID)
|
||||
if err != nil {
|
||||
@@ -68,8 +68,8 @@ func (s *Server) enforceDocGovWorkflowAction(w http.ResponseWriter, ctx context.
|
||||
if docID == "" {
|
||||
return true
|
||||
}
|
||||
if s.gov != nil {
|
||||
doc, err := s.gov.GetDocument(ctx, docID)
|
||||
if s.gov() != nil {
|
||||
doc, err := s.gov().GetDocument(ctx, docID)
|
||||
if err == nil && doc != nil && doc.ProjectID > 0 && doc.TaskKey != "" {
|
||||
return s.enforceWorkflowDocTaskAction(w, ctx, doc.ProjectID, userID, doc.TaskKey, action)
|
||||
}
|
||||
@@ -125,12 +125,12 @@ func (s *Server) allowAnyStageAccess(ctx context.Context, projectID, userID int6
|
||||
// View scope assumes the caller already passed project membership
|
||||
// (see ResolveStageAccess comments). Enforce it here: a non-member with a
|
||||
// live governing instance must not read project context.
|
||||
isCreator, _ := s.pwSvc.IsProjectCreator(ctx, projectID, userID)
|
||||
isCreator, _ := s.pwSvc().IsProjectCreator(ctx, projectID, userID)
|
||||
if !isCreator {
|
||||
member := false
|
||||
if s.authorizer != nil {
|
||||
if s.authorizer() != nil {
|
||||
var mErr error
|
||||
member, mErr = s.authorizer.IsProjectMember(ctx, projectID, userID)
|
||||
member, mErr = s.authorizer().IsProjectMember(ctx, projectID, userID)
|
||||
if mErr != nil && s.failClosedAuthz() {
|
||||
return false
|
||||
}
|
||||
@@ -159,8 +159,8 @@ func (s *Server) enrichSessionWorkflowStage(ctx context.Context, sess *chat.Sess
|
||||
if sess == nil {
|
||||
return
|
||||
}
|
||||
if sess.Project() > 0 && s.instSvc != nil && s.instSvc.Available() {
|
||||
settings, err := s.instSvc.GetSettings(ctx, sess.Project(), userID, false)
|
||||
if sess.Project() > 0 && s.instSvc() != nil && s.instSvc().Available() {
|
||||
settings, err := s.instSvc().GetSettings(ctx, sess.Project(), userID, false)
|
||||
if err == nil && settings != nil {
|
||||
sess.ActiveStageKey = settings.ActiveStageKey
|
||||
}
|
||||
@@ -314,10 +314,10 @@ func (s *Server) enforceChatRequiresProject(w http.ResponseWriter, r *http.Reque
|
||||
if s.isSystemAdminUser(r.Context(), user) {
|
||||
return true
|
||||
}
|
||||
if s.authorizer == nil || !s.authorizer.Enabled() {
|
||||
if s.authorizer() == nil || !s.authorizer().Enabled() {
|
||||
return true
|
||||
}
|
||||
ids, err := s.authorizer.ListVisibleProjectIDs(r.Context(), user.ID)
|
||||
ids, err := s.authorizer().ListVisibleProjectIDs(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
writeJSONResponse(w, http.StatusInternalServerError, map[string]string{
|
||||
"error_code": "ERR_INTERNAL",
|
||||
@@ -344,7 +344,7 @@ func (s *Server) enforceChatProjectAccess(w http.ResponseWriter, r *http.Request
|
||||
if s.isSystemAdminUser(r.Context(), user) {
|
||||
return true
|
||||
}
|
||||
if s.authorizer == nil {
|
||||
if s.authorizer() == nil {
|
||||
if s.failClosedAuthz() {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
"error_code": "ERR_FORBIDDEN",
|
||||
@@ -354,7 +354,7 @@ func (s *Server) enforceChatProjectAccess(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
return true
|
||||
}
|
||||
if err := s.authorizer.RequireProjectAccess(r.Context(), projectID, user.ID); err != nil {
|
||||
if err := s.authorizer().RequireProjectAccess(r.Context(), projectID, user.ID); err != nil {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
"error_code": "ERR_FORBIDDEN",
|
||||
"message": "无权访问该项目",
|
||||
|
||||
@@ -147,14 +147,14 @@ func (s *Server) userCanAccessSupplementBatch(ctx context.Context, userID, proje
|
||||
if projectID <= 0 || userID <= 0 {
|
||||
return false
|
||||
}
|
||||
if s.authorizer != nil {
|
||||
ok, err := s.authorizer.IsProjectMember(ctx, projectID, userID)
|
||||
if s.authorizer() != nil {
|
||||
ok, err := s.authorizer().IsProjectMember(ctx, projectID, userID)
|
||||
if err == nil && ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if s.pwSvc != nil {
|
||||
if isCreator, err := s.pwSvc.IsProjectCreator(ctx, projectID, userID); err == nil && isCreator {
|
||||
if s.pwSvc() != nil {
|
||||
if isCreator, err := s.pwSvc().IsProjectCreator(ctx, projectID, userID); err == nil && isCreator {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ func (s *Server) finalizeTaskRun(ctx context.Context, record *taskrun.RunRecord,
|
||||
}
|
||||
cmds := s.taskCommands()
|
||||
if s != nil {
|
||||
cmds.PW = s.pwSvc
|
||||
cmds.Ctx = s.ctxMgr
|
||||
cmds.PW = s.pwSvc()
|
||||
cmds.Ctx = s.ctxMgr()
|
||||
}
|
||||
status, err := cmds.Finalize(ctx, record, result)
|
||||
if err != nil {
|
||||
@@ -44,11 +44,11 @@ func (s *Server) finalizeTaskRun(ctx context.Context, record *taskrun.RunRecord,
|
||||
}
|
||||
|
||||
func (s *Server) taskCommands() *taskrun.Commands {
|
||||
if s != nil && s.cmds != nil {
|
||||
return s.cmds
|
||||
if s != nil && s.cmds() != nil {
|
||||
return s.cmds()
|
||||
}
|
||||
cmds := &taskrun.Commands{
|
||||
PW: s.pwSvc,
|
||||
PW: s.pwSvc(),
|
||||
Env: "test",
|
||||
}
|
||||
if s != nil && s.cfg != nil {
|
||||
@@ -56,13 +56,13 @@ func (s *Server) taskCommands() *taskrun.Commands {
|
||||
cmds.StorageIdentity = "env:" + s.cfg.Env
|
||||
}
|
||||
if s != nil {
|
||||
cmds.Ctx = s.ctxMgr
|
||||
cmds.Ctx = s.ctxMgr()
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
|
||||
func (s *Server) hashDeliverable(ctx context.Context, record *taskrun.RunRecord, result *bizdev.TaskRunResult) string {
|
||||
if s == nil || s.wsCap == nil || record == nil {
|
||||
if s == nil || s.wsCap() == nil || record == nil {
|
||||
return ""
|
||||
}
|
||||
id := workspace.OpaqueID(result.WorkDir, record.WorkspaceID)
|
||||
@@ -80,7 +80,7 @@ func (s *Server) hashDeliverable(ctx context.Context, record *taskrun.RunRecord,
|
||||
RunID: record.RunID,
|
||||
Environment: env,
|
||||
}
|
||||
body, err := s.wsCap.ReadArtifact(ctx, ref, workspace.DeliverableKey)
|
||||
body, err := s.wsCap().ReadArtifact(ctx, ref, workspace.DeliverableKey)
|
||||
if err != nil || len(body) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ func TestFinalizeTaskRun_ReplacesPlaceholderContent(t *testing.T) {
|
||||
Status: "running",
|
||||
})
|
||||
|
||||
srv.pwSvc = pwSvc
|
||||
srv.taskRuns = taskrun.NewManager(pwSvc, ctxMgr)
|
||||
srv.setOverridePwSvc(pwSvc)
|
||||
srv.setOverrideTaskRuns(taskrun.NewManager(pwSvc, ctxMgr))
|
||||
srv.initTaskRunExecutors()
|
||||
|
||||
record := &taskrun.RunRecord{
|
||||
@@ -118,8 +118,8 @@ func TestFinalizeTaskRun_PendingAuthorConfirmation_NotCompleted(t *testing.T) {
|
||||
_, _ = pwSvc.CreateWorkflowRun(context.Background(), &projectworkflow.WorkflowRun{
|
||||
RunID: "run_pending_confirm", ProjectID: 56, TaskKey: "intake", Status: "running",
|
||||
})
|
||||
srv.pwSvc = pwSvc
|
||||
srv.taskRuns = taskrun.NewManager(pwSvc, ctxMgr)
|
||||
srv.setOverridePwSvc(pwSvc)
|
||||
srv.setOverrideTaskRuns(taskrun.NewManager(pwSvc, ctxMgr))
|
||||
srv.initTaskRunExecutors()
|
||||
|
||||
record := &taskrun.RunRecord{
|
||||
@@ -185,8 +185,8 @@ func TestAsyncTaskRun_FinalizesAfterWorkerCompletes(t *testing.T) {
|
||||
workDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# async finalize\n"), 0o644)
|
||||
|
||||
srv := &Server{cfg: cfg, svc: svc, pwSvc: pwSvc, ctxMgr: ctxMgr, taskRuns: taskrun.NewManager(pwSvc, ctxMgr)}
|
||||
srv.taskRuns.SetExecutor(
|
||||
srv := &Server{cfg: cfg, rt: newMinimalRuntime(cfg, svc, pwSvc, ctxMgr)}
|
||||
srv.taskRuns().SetExecutor(
|
||||
func(_ context.Context, _ string, _ bizdev.TaskRunRequest, _ *stream.Streamer) (*bizdev.TaskRunResult, error) {
|
||||
return &bizdev.TaskRunResult{
|
||||
Success: true,
|
||||
@@ -200,7 +200,7 @@ func TestAsyncTaskRun_FinalizesAfterWorkerCompletes(t *testing.T) {
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
srv.taskRuns.SetFinalizer(func(ctx context.Context, record *taskrun.RunRecord, result *bizdev.TaskRunResult) (string, error) {
|
||||
srv.taskRuns().SetFinalizer(func(ctx context.Context, record *taskrun.RunRecord, result *bizdev.TaskRunResult) (string, error) {
|
||||
return srv.finalizeTaskRun(ctx, record, result)
|
||||
})
|
||||
|
||||
@@ -213,14 +213,14 @@ func TestAsyncTaskRun_FinalizesAfterWorkerCompletes(t *testing.T) {
|
||||
record := &taskrun.RunRecord{
|
||||
RunID: runID, SessionID: "sess-async", ProjectID: 1, TaskKey: "intake", AssistantMsgID: asstID,
|
||||
}
|
||||
_, err := srv.taskRuns.CreateAndStart(context.Background(), runID, record, bizdev.TaskRunRequest{TaskKey: "intake", ProjectID: 1}, bizdev.StageRunRequest{}, "")
|
||||
_, err := srv.taskRuns().CreateAndStart(context.Background(), runID, record, bizdev.TaskRunRequest{TaskKey: "intake", ProjectID: 1}, bizdev.StageRunRequest{}, "")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateAndStart: %v", err)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
rec, getErr := srv.taskRuns.GetRun(runID)
|
||||
rec, getErr := srv.taskRuns().GetRun(runID)
|
||||
if getErr == nil && rec.Status != "running" {
|
||||
msg, _ := ctxMgr.GetMessageByID(context.Background(), asstID)
|
||||
if msg != nil && msg.Content != "任务执行中…" {
|
||||
@@ -242,8 +242,8 @@ func TestAsyncTaskRun_PendingConfirmation_FinalizesAsPending(t *testing.T) {
|
||||
workDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# head\n"), 0o644)
|
||||
|
||||
srv := &Server{cfg: cfg, svc: svc, pwSvc: pwSvc, ctxMgr: ctxMgr, taskRuns: taskrun.NewManager(pwSvc, ctxMgr)}
|
||||
srv.taskRuns.SetExecutor(
|
||||
srv := &Server{cfg: cfg, rt: newMinimalRuntime(cfg, svc, pwSvc, ctxMgr)}
|
||||
srv.taskRuns().SetExecutor(
|
||||
func(_ context.Context, _ string, _ bizdev.TaskRunRequest, _ *stream.Streamer) (*bizdev.TaskRunResult, error) {
|
||||
return &bizdev.TaskRunResult{
|
||||
Success: true,
|
||||
@@ -260,7 +260,7 @@ func TestAsyncTaskRun_PendingConfirmation_FinalizesAsPending(t *testing.T) {
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
srv.taskRuns.SetFinalizer(func(ctx context.Context, record *taskrun.RunRecord, result *bizdev.TaskRunResult) (string, error) {
|
||||
srv.taskRuns().SetFinalizer(func(ctx context.Context, record *taskrun.RunRecord, result *bizdev.TaskRunResult) (string, error) {
|
||||
return srv.finalizeTaskRun(ctx, record, result)
|
||||
})
|
||||
|
||||
@@ -273,14 +273,14 @@ func TestAsyncTaskRun_PendingConfirmation_FinalizesAsPending(t *testing.T) {
|
||||
record := &taskrun.RunRecord{
|
||||
RunID: runID, SessionID: "sess-async-pending", ProjectID: 1, TaskKey: "intake", AssistantMsgID: asstID,
|
||||
}
|
||||
_, err := srv.taskRuns.CreateAndStart(context.Background(), runID, record, bizdev.TaskRunRequest{TaskKey: "intake", ProjectID: 1}, bizdev.StageRunRequest{}, "")
|
||||
_, err := srv.taskRuns().CreateAndStart(context.Background(), runID, record, bizdev.TaskRunRequest{TaskKey: "intake", ProjectID: 1}, bizdev.StageRunRequest{}, "")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateAndStart: %v", err)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
rec, getErr := srv.taskRuns.GetRun(runID)
|
||||
rec, getErr := srv.taskRuns().GetRun(runID)
|
||||
if getErr == nil && rec.Status != "running" {
|
||||
msg, _ := ctxMgr.GetMessageByID(context.Background(), asstID)
|
||||
if msg == nil {
|
||||
@@ -307,12 +307,12 @@ func TestFinalizeTaskRun_HashesViaWorkspaceCapability(t *testing.T) {
|
||||
svc := bizdev.MustNewService(t, cfg)
|
||||
srv := NewServer(cfg, svc)
|
||||
ctx := context.Background()
|
||||
ref, err := srv.wsCap.Issue(workspace.WorkspaceRef{ID: "ws_hash", ProjectScope: 1, RunID: "run_hash"})
|
||||
ref, err := srv.wsCap().Issue(workspace.WorkspaceRef{ID: "ws_hash", ProjectScope: 1, RunID: "run_hash"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := []byte("# hashed via capability\n")
|
||||
if err := srv.wsCap.WriteArtifact(ctx, ref, workspace.DeliverableKey, body); err != nil {
|
||||
if err := srv.wsCap().WriteArtifact(ctx, ref, workspace.DeliverableKey, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctxMgr := bizdev.Snapshot(svc).Ctx
|
||||
@@ -324,7 +324,7 @@ func TestFinalizeTaskRun_HashesViaWorkspaceCapability(t *testing.T) {
|
||||
_, _ = pwSvc.CreateWorkflowRun(ctx, &projectworkflow.WorkflowRun{
|
||||
RunID: "run_hash", ProjectID: 1, TaskKey: "intake", Status: "running",
|
||||
})
|
||||
srv.pwSvc = pwSvc
|
||||
srv.setOverridePwSvc(pwSvc)
|
||||
record := &taskrun.RunRecord{RunID: "run_hash", SessionID: "sess-hash", ProjectID: 1, TaskKey: "intake", AssistantMsgID: asstID, WorkspaceID: ref.ID}
|
||||
status, err := srv.finalizeTaskRun(ctx, record, &bizdev.TaskRunResult{
|
||||
Success: true, TaskKey: "intake", WorkDir: ref.ID,
|
||||
|
||||
@@ -23,7 +23,7 @@ func (s *Server) assertTaskRunAccess(w http.ResponseWriter, ctx context.Context,
|
||||
})
|
||||
return false
|
||||
}
|
||||
rec, err := s.taskRuns.GetRun(runID)
|
||||
rec, err := s.taskRuns().GetRun(runID)
|
||||
if err != nil {
|
||||
writeSafeError(w, nil, http.StatusNotFound, "run not found")
|
||||
return false
|
||||
@@ -34,9 +34,9 @@ func (s *Server) assertTaskRunAccess(w http.ResponseWriter, ctx context.Context,
|
||||
isOwner := rec.UserID > 0 && rec.UserID == user.ID
|
||||
isProjectOwner := false
|
||||
isProjectMember := false
|
||||
if rec.ProjectID > 0 && s.authorizer != nil {
|
||||
isProjectOwner, _ = s.authorizer.IsProjectOwner(ctx, rec.ProjectID, user.ID)
|
||||
isProjectMember, _ = s.authorizer.IsProjectMember(ctx, rec.ProjectID, user.ID)
|
||||
if rec.ProjectID > 0 && s.authorizer() != nil {
|
||||
isProjectOwner, _ = s.authorizer().IsProjectOwner(ctx, rec.ProjectID, user.ID)
|
||||
isProjectMember, _ = s.authorizer().IsProjectMember(ctx, rec.ProjectID, user.ID)
|
||||
}
|
||||
switch action {
|
||||
case taskRunView:
|
||||
@@ -64,6 +64,6 @@ func (s *Server) isSystemAdminUser(ctx context.Context, user *auth.UserAccount)
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
admin, err := s.authzSvc.IsSystemAdmin(ctx, user.ID, user.RoleID)
|
||||
admin, err := s.authzSvc().IsSystemAdmin(ctx, user.ID, user.RoleID)
|
||||
return err == nil && admin
|
||||
}
|
||||
|
||||
@@ -17,19 +17,19 @@ func TestCreateTaskRunBusyPreflightDoesNotPersistRunOrMessages(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
ctx := t.Context()
|
||||
docID := "doc_proj143_intake"
|
||||
if _, err := srv.gov.CreateOrGetDocument(
|
||||
if _, err := srv.gov().CreateOrGetDocument(
|
||||
ctx, docID, 143, "intake", "用户初始需求收集表", "V0.1_triage", "",
|
||||
1, "编制人", 1, "审核人", 1, "批准人",
|
||||
); err != nil {
|
||||
t.Fatalf("create document: %v", err)
|
||||
}
|
||||
if _, err := srv.gov.AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
||||
if _, err := srv.gov().AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
||||
DocID: docID, HolderType: docgov.LeaseHolderHuman, HolderID: "editor-9", TTL: time.Minute,
|
||||
}); err != nil {
|
||||
t.Fatalf("acquire fixture lease: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = srv.gov.ReleaseLease(t.Context(), docID, string(docgov.LeaseHolderHuman), "editor-9")
|
||||
_, _ = srv.gov().ReleaseLease(t.Context(), docID, string(docgov.LeaseHolderHuman), "editor-9")
|
||||
}()
|
||||
|
||||
requestBody, _ := json.Marshal(createTaskRunRequest{
|
||||
@@ -53,7 +53,7 @@ func TestCreateTaskRunBusyPreflightDoesNotPersistRunOrMessages(t *testing.T) {
|
||||
if _, exists := payload["run_id"]; exists {
|
||||
t.Fatalf("busy preflight must not return run_id: %+v", payload)
|
||||
}
|
||||
history, err := srv.ctxMgr.GetChatHistory(ctx, "fixture-busy-preflight", 20, 0)
|
||||
history, err := srv.ctxMgr().GetChatHistory(ctx, "fixture-busy-preflight", 20, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("get busy history: %v", err)
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func TestCreateTaskRun_PreActivationAcceptedNot409(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
store := projectinstance.NewMemoryStore()
|
||||
_ = projectinstance.SeedProjectMeta(store, 9001, projectinstance.LifecyclePlanning, "", 1, 0)
|
||||
srv.instSvc = projectinstance.NewService(store, projectinstance.TestResolver(), nil, nil)
|
||||
srv.setOverrideInstSvc(projectinstance.NewService(store, projectinstance.TestResolver(), nil, nil))
|
||||
|
||||
requestBody, _ := json.Marshal(createTaskRunRequest{
|
||||
TaskRunRequestDTO: TaskRunRequestDTO{TaskKey: "intake", ProjectID: 9001, ProjectName: "预激活立项"},
|
||||
@@ -107,10 +107,10 @@ func TestCreateTaskRun_PreActivationAcceptedNot409(t *testing.T) {
|
||||
if payload.Status != "blocked_pending_governance" {
|
||||
t.Fatalf("status=%q want blocked_pending_governance body=%s", payload.Status, w.Body.String())
|
||||
}
|
||||
if rec, err := srv.taskRuns.GetRun(payload.RunID); err == nil && rec != nil && rec.Status == "running" {
|
||||
if rec, err := srv.taskRuns().GetRun(payload.RunID); err == nil && rec != nil && rec.Status == "running" {
|
||||
t.Fatalf("pre-activation run must not start a live worker: %+v", rec)
|
||||
}
|
||||
history, err := srv.ctxMgr.GetChatHistory(t.Context(), "sess-p9001-u1", 20, 0)
|
||||
history, err := srv.ctxMgr().GetChatHistory(t.Context(), "sess-p9001-u1", 20, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("chat history: %v", err)
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func TestCreateTaskRun_ScopeUnresolvedPersistsChat(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
token := loginTestAdmin(t, srv)
|
||||
srv.cfg.AllowInMemory = false
|
||||
srv.instSvc = nil
|
||||
srv.setOverrideInstSvc(nil)
|
||||
|
||||
requestBody, _ := json.Marshal(createTaskRunRequest{
|
||||
TaskRunRequestDTO: TaskRunRequestDTO{TaskKey: "intake", ProjectID: 9102, ProjectName: "作用域失败"},
|
||||
@@ -152,7 +152,7 @@ func TestCreateTaskRun_ScopeUnresolvedPersistsChat(t *testing.T) {
|
||||
if payload["error_code"] != "TASK_SCOPE_UNRESOLVED" {
|
||||
t.Fatalf("payload=%+v", payload)
|
||||
}
|
||||
history, err := srv.ctxMgr.GetChatHistory(t.Context(), "sess-scope-unresolved", 20, 0)
|
||||
history, err := srv.ctxMgr().GetChatHistory(t.Context(), "sess-scope-unresolved", 20, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("history: %v", err)
|
||||
}
|
||||
@@ -175,10 +175,10 @@ func TestDocGovActions_PreActivationRejected(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
store := projectinstance.NewMemoryStore()
|
||||
_ = projectinstance.SeedProjectMeta(store, 9002, projectinstance.LifecyclePlanning, "", 1, 0)
|
||||
srv.instSvc = projectinstance.NewService(store, projectinstance.TestResolver(), nil, nil)
|
||||
srv.setOverrideInstSvc(projectinstance.NewService(store, projectinstance.TestResolver(), nil, nil))
|
||||
|
||||
docID := "doc_proj9002_intake"
|
||||
if _, err := srv.gov.CreateOrGetDocument(
|
||||
if _, err := srv.gov().CreateOrGetDocument(
|
||||
t.Context(), docID, 9002, "intake", "立项文档", "V0.1_triage", "",
|
||||
1, "编制人", 2, "审核人", 3, "批准人",
|
||||
); err != nil {
|
||||
|
||||
@@ -58,7 +58,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
req.UserID = user.ID
|
||||
if req.ProjectID > 0 {
|
||||
if err := s.authorizer.RequireProjectAccess(r.Context(), req.ProjectID, user.ID); err != nil {
|
||||
if err := s.authorizer().RequireProjectAccess(r.Context(), req.ProjectID, user.ID); err != nil {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
"error_code": "ERR_FORBIDDEN",
|
||||
"message": "无权访问该项目",
|
||||
@@ -85,8 +85,8 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 授权后、scope/preflight 前:稳定幂等键命中则直接 202,避免 SSE 断线重试误击 DOC_BUSY / 重复写聊天。
|
||||
idemKey := taskrun.BuildTaskRunIdempotencyKey(req.SessionID, req.ClientTurnID)
|
||||
if idemKey != "" && s.taskRuns != nil {
|
||||
if existing, findErr := s.taskRuns.FindByIdempotencyKey(r.Context(), idemKey); findErr != nil {
|
||||
if idemKey != "" && s.taskRuns() != nil {
|
||||
if existing, findErr := s.taskRuns().FindByIdempotencyKey(r.Context(), idemKey); findErr != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, findErr.Error())
|
||||
return
|
||||
} else if existing != nil {
|
||||
@@ -107,7 +107,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
ctxMgr := s.ctxMgr
|
||||
ctxMgr := s.ctxMgr()
|
||||
if ctxMgr != nil {
|
||||
if _, err := ctxMgr.EnsureChatSessionOwned(r.Context(), req.SessionID, req.UserID, req.ProjectID, req.ProjectName); err != nil {
|
||||
if errors.Is(err, ctxmgr.ErrSessionOwnerMismatch) {
|
||||
@@ -164,7 +164,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
WorkflowStageKey: taskScope.StageKey,
|
||||
Status: "pending",
|
||||
}
|
||||
claimed, created, claimErr := s.taskRuns.ClaimRun(r.Context(), record)
|
||||
claimed, created, claimErr := s.taskRuns().ClaimRun(r.Context(), record)
|
||||
if claimErr != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, claimErr.Error())
|
||||
return
|
||||
@@ -212,7 +212,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
projectinstance.ApplyScopeToMetadata(asstMeta, taskScope)
|
||||
assistantMsgID, _ = ctxMgr.RecordUserMessageWithID(r.Context(), req.SessionID, 0, "assistant", "任务执行中…", "TRIGGER_TASK", "", asstMeta)
|
||||
claimed.AssistantMsgID = assistantMsgID
|
||||
_ = s.taskRuns.BindAssistantMsgID(r.Context(), claimed.RunID, assistantMsgID)
|
||||
_ = s.taskRuns().BindAssistantMsgID(r.Context(), claimed.RunID, assistantMsgID)
|
||||
}
|
||||
|
||||
taskReq := s.buildBizdevTaskRequest(req.TaskRunRequestDTO, req.SessionID, req.ClientTurnID, claimed.RunID)
|
||||
@@ -221,7 +221,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
resumeDir := req.WorkDir
|
||||
stageReq := s.buildBizdevStageRequest(req.TaskRunRequestDTO, req.SessionID, req.ClientTurnID, claimed.RunID)
|
||||
createdRun, err := s.taskRuns.StartClaimedRun(r.Context(), claimed, taskReq, stageReq, resumeDir)
|
||||
createdRun, err := s.taskRuns().StartClaimedRun(r.Context(), claimed, taskReq, stageReq, resumeDir)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -256,7 +256,7 @@ func (s *Server) createBlockedGovernanceRun(w http.ResponseWriter, r *http.Reque
|
||||
record.StageInstanceID = taskScope.StageInstanceID
|
||||
record.WorkflowStageKey = taskScope.StageKey
|
||||
}
|
||||
claimed, created, claimErr := s.taskRuns.ClaimRun(r.Context(), record)
|
||||
claimed, created, claimErr := s.taskRuns().ClaimRun(r.Context(), record)
|
||||
if claimErr != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, claimErr.Error())
|
||||
return
|
||||
@@ -271,7 +271,7 @@ func (s *Server) createBlockedGovernanceRun(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
var assistantMsgID int64
|
||||
ctxMgr := s.ctxMgr
|
||||
ctxMgr := s.ctxMgr()
|
||||
if ctxMgr != nil && req.UserMessage != "" {
|
||||
userMeta := map[string]any{
|
||||
"project_id": req.ProjectID,
|
||||
@@ -303,7 +303,7 @@ func (s *Server) createBlockedGovernanceRun(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
projectinstance.ApplyScopeToMetadata(asstMeta, taskScope)
|
||||
assistantMsgID, _ = ctxMgr.RecordUserMessageWithID(r.Context(), req.SessionID, 0, "assistant", projectinstance.GovernanceBlockedHint, "TRIGGER_TASK", "", asstMeta)
|
||||
_ = s.taskRuns.BindAssistantMsgID(r.Context(), claimed.RunID, assistantMsgID)
|
||||
_ = s.taskRuns().BindAssistantMsgID(r.Context(), claimed.RunID, assistantMsgID)
|
||||
}
|
||||
writeJSONResponse(w, http.StatusAccepted, createTaskRunResponse{
|
||||
RunID: claimed.RunID,
|
||||
@@ -423,18 +423,18 @@ func (s *Server) recordUnresolvedScopeMessages(ctx context.Context, ctxMgr *ctxm
|
||||
// preflightTaskDocumentBusy 在写入聊天占位与 task_runs 之前拦截已知租约占用。
|
||||
// 真正的租约获取仍由 docgov 原子执行;本检查用于避免用户重复点击产生必然失败的 run。
|
||||
func (s *Server) preflightTaskDocumentBusy(ctx context.Context, req createTaskRunRequest) (*docgov.DocumentBusyError, error) {
|
||||
if req.Stage != "" || req.ProjectID <= 0 || req.TaskKey == "" || s.gov == nil {
|
||||
if req.Stage != "" || req.ProjectID <= 0 || req.TaskKey == "" || s.gov() == nil {
|
||||
return nil, nil
|
||||
}
|
||||
taskReq := s.buildBizdevTaskRequest(req.TaskRunRequestDTO, req.SessionID, req.ClientTurnID, "")
|
||||
docID, err := s.svc.ResolveAndValidateDocID(ctx, taskReq)
|
||||
docID, err := s.svc().ResolveAndValidateDocID(ctx, taskReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if docID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
doc, err := s.gov.GetDocument(ctx, docID)
|
||||
doc, err := s.gov().GetDocument(ctx, docID)
|
||||
if status, ok := domainHTTPStatus(err); ok && status == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -445,8 +445,8 @@ func (s *Server) preflightTaskDocumentBusy(ctx context.Context, req createTaskRu
|
||||
return nil, nil
|
||||
}
|
||||
// 已结束的小队 run 由 AcquireLease 的孤儿回收逻辑处理,不在这里误拦截。
|
||||
if doc.LeaseHolderType == vocabulary.LeaseHolderSquad && s.taskRuns != nil {
|
||||
if run, runErr := s.taskRuns.GetRun(doc.LeaseHolderID); runErr == nil && run.Status != "running" {
|
||||
if doc.LeaseHolderType == vocabulary.LeaseHolderSquad && s.taskRuns() != nil {
|
||||
if run, runErr := s.taskRuns().GetRun(doc.LeaseHolderID); runErr == nil && run.Status != "running" {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
@@ -496,12 +496,12 @@ func (s *Server) handleTaskRunEvents(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Subscribe before declaring text/event-stream so NotFound stays a normal JSON error
|
||||
// (same order as handleChatStream / CreateAndStart in sse.go).
|
||||
ch, replay, err := s.taskRuns.Subscribe(runID, afterSeq)
|
||||
ch, replay, err := s.taskRuns().Subscribe(runID, afterSeq)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
defer s.taskRuns.Unsubscribe(runID, ch)
|
||||
defer s.taskRuns().Unsubscribe(runID, ch)
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
@@ -583,11 +583,11 @@ func (s *Server) handleCancelTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.assertTaskRunAccess(w, r.Context(), runID, user, taskRunCancel) {
|
||||
return
|
||||
}
|
||||
if err := s.taskRuns.Cancel(runID); err != nil {
|
||||
if err := s.taskRuns().Cancel(runID); err != nil {
|
||||
writeSafeError(w, r, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
if rec, getErr := s.taskRuns.GetRun(runID); getErr == nil && rec.WorkspaceID != "" {
|
||||
if rec, getErr := s.taskRuns().GetRun(runID); getErr == nil && rec.WorkspaceID != "" {
|
||||
s.cancelPiAgentRunner(rec.WorkspaceID)
|
||||
}
|
||||
writeJSONResponse(w, http.StatusOK, map[string]string{"run_id": runID, "status": "cancelled"})
|
||||
@@ -622,7 +622,7 @@ func (s *Server) handleGetTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.assertTaskRunOwnedByUser(w, r.Context(), runID, user) {
|
||||
return
|
||||
}
|
||||
rec, err := s.taskRuns.GetRun(runID)
|
||||
rec, err := s.taskRuns().GetRun(runID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusNotFound, err.Error())
|
||||
return
|
||||
@@ -657,7 +657,7 @@ func (s *Server) handleResumeTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
rec, err := s.taskRuns.GetRun(runID)
|
||||
rec, err := s.taskRuns().GetRun(runID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusNotFound, err.Error())
|
||||
return
|
||||
@@ -676,11 +676,11 @@ func (s *Server) handleResumeTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
if s.resolver == nil {
|
||||
if s.resolver() == nil {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "workspace resolver unavailable")
|
||||
return
|
||||
}
|
||||
workDir, werr := s.resolver.ResolveStoredWorkDir(rec.WorkspaceID)
|
||||
workDir, werr := s.resolver().ResolveStoredWorkDir(rec.WorkspaceID)
|
||||
if werr != nil {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
"error_code": "ERR_WORKSPACE_FORBIDDEN",
|
||||
@@ -702,7 +702,7 @@ func (s *Server) handleResumeTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
Stage: rec.Stage,
|
||||
}, rec.SessionID, rec.ClientTurnID, rec.RunID)
|
||||
|
||||
resumed, err := s.taskRuns.ResumeInterrupted(r.Context(), rec, rec.WorkspaceID, taskReq, stageReq)
|
||||
resumed, err := s.taskRuns().ResumeInterrupted(r.Context(), rec, rec.WorkspaceID, taskReq, stageReq)
|
||||
if err != nil {
|
||||
if errors.Is(err, taskrun.ErrRunNotResumable) || errors.Is(err, taskrun.ErrRunAlreadyActive) {
|
||||
writeJSONResponse(w, http.StatusConflict, map[string]string{
|
||||
@@ -725,8 +725,8 @@ func (s *Server) handleResumeTaskRun(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Server) buildTaskRunResponse(rec *taskrun.RunRecord) taskRunResponse {
|
||||
resumable := rec != nil && rec.Status == "interrupted" && rec.WorkspaceID != ""
|
||||
if resumable && s.resolver != nil {
|
||||
if _, err := s.resolver.ResolveStoredWorkDir(rec.WorkspaceID); err != nil {
|
||||
if resumable && s.resolver() != nil {
|
||||
if _, err := s.resolver().ResolveStoredWorkDir(rec.WorkspaceID); err != nil {
|
||||
resumable = false
|
||||
}
|
||||
}
|
||||
@@ -816,12 +816,12 @@ func (s *Server) buildBizdevStageRequest(dto TaskRunRequestDTO, sessionID, clien
|
||||
}
|
||||
|
||||
func (s *Server) initTaskRunExecutors() {
|
||||
s.taskRuns.SetExecutor(
|
||||
s.taskRuns().SetExecutor(
|
||||
func(ctx context.Context, _ string, req bizdev.TaskRunRequest, _ *stream.Streamer) (*bizdev.TaskRunResult, error) {
|
||||
return s.taskUseCase().ExecuteTask(ctx, req)
|
||||
},
|
||||
func(ctx context.Context, _ string, workDir string, _ *stream.Streamer) (*bizdev.TaskRunResult, error) {
|
||||
res, err := s.svc.ResumeOrModifyWorkspace(ctx, squad.ModifyWorkflowRequest{WorkDir: workDir})
|
||||
res, err := s.svc().ResumeOrModifyWorkspace(ctx, squad.ModifyWorkflowRequest{WorkDir: workDir})
|
||||
if res == nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -847,7 +847,7 @@ func (s *Server) initTaskRunExecutors() {
|
||||
}, err
|
||||
},
|
||||
)
|
||||
s.taskRuns.SetFinalizer(func(ctx context.Context, record *taskrun.RunRecord, result *bizdev.TaskRunResult) (string, error) {
|
||||
s.taskRuns().SetFinalizer(func(ctx context.Context, record *taskrun.RunRecord, result *bizdev.TaskRunResult) (string, error) {
|
||||
return s.finalizeTaskRun(ctx, record, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@ func TestCreateTaskRun_SameTurnIdempotentReturnsExistingDespiteBusyLease(t *test
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if firstRunID != "" {
|
||||
_ = srv.taskRuns.WaitRunDone(ctx, firstRunID)
|
||||
} else if srv.taskRuns != nil {
|
||||
_ = srv.taskRuns.WaitIdle(ctx)
|
||||
_ = srv.taskRuns().WaitRunDone(ctx, firstRunID)
|
||||
} else if srv.taskRuns() != nil {
|
||||
_ = srv.taskRuns().WaitIdle(ctx)
|
||||
}
|
||||
})
|
||||
srv.taskRuns.SetExecutor(
|
||||
srv.taskRuns().SetExecutor(
|
||||
func(ctx context.Context, _ string, _ bizdev.TaskRunRequest, _ *stream.Streamer) (*bizdev.TaskRunResult, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -53,7 +53,7 @@ func TestCreateTaskRun_SameTurnIdempotentReturnsExistingDespiteBusyLease(t *test
|
||||
)
|
||||
|
||||
docID := "doc_proj143_intake"
|
||||
if _, err := srv.gov.CreateOrGetDocument(
|
||||
if _, err := srv.gov().CreateOrGetDocument(
|
||||
ctx, docID, 143, "intake", "用户初始需求收集表", "V0.1_triage", "",
|
||||
userID, "编制人", userID, "审核人", userID, "批准人",
|
||||
); err != nil {
|
||||
@@ -82,7 +82,7 @@ func TestCreateTaskRun_SameTurnIdempotentReturnsExistingDespiteBusyLease(t *test
|
||||
}
|
||||
firstRunID = first.RunID
|
||||
|
||||
hist1, err := srv.ctxMgr.GetChatHistory(ctx, "sess-idem-busy", 50, 0)
|
||||
hist1, err := srv.ctxMgr().GetChatHistory(ctx, "sess-idem-busy", 50, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("history1: %v", err)
|
||||
}
|
||||
@@ -91,13 +91,13 @@ func TestCreateTaskRun_SameTurnIdempotentReturnsExistingDespiteBusyLease(t *test
|
||||
}
|
||||
|
||||
// 活跃小队租约:模拟长任务仍占文档(旧路径会 409)
|
||||
if _, err := srv.gov.AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
||||
if _, err := srv.gov().AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
||||
DocID: docID, HolderType: docgov.LeaseHolderSquad, HolderID: first.RunID, TTL: time.Minute,
|
||||
}); err != nil {
|
||||
t.Fatalf("acquire squad lease: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = srv.gov.ReleaseLease(context.Background(), docID, string(docgov.LeaseHolderSquad), first.RunID)
|
||||
_, _ = srv.gov().ReleaseLease(context.Background(), docID, string(docgov.LeaseHolderSquad), first.RunID)
|
||||
})
|
||||
|
||||
body2, _ := json.Marshal(createTaskRunRequest{
|
||||
@@ -119,7 +119,7 @@ func TestCreateTaskRun_SameTurnIdempotentReturnsExistingDespiteBusyLease(t *test
|
||||
t.Fatalf("want same run %s, got %s", first.RunID, second.RunID)
|
||||
}
|
||||
|
||||
hist2, err := srv.ctxMgr.GetChatHistory(ctx, "sess-idem-busy", 50, 0)
|
||||
hist2, err := srv.ctxMgr().GetChatHistory(ctx, "sess-idem-busy", 50, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("history2: %v", err)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func TestGetTaskRunJSONUsesSnakeCase(t *testing.T) {
|
||||
TaskKey: "intake",
|
||||
UserID: 1,
|
||||
}
|
||||
if _, err := srv.taskRuns.CreateAndStart(
|
||||
if _, err := srv.taskRuns().CreateAndStart(
|
||||
t.Context(),
|
||||
runID,
|
||||
rec,
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestTaskRun_DisconnectDoesNotCancelWorker(t *testing.T) {
|
||||
runID := "run_disconnect_test"
|
||||
record := &taskrun.RunRecord{RunID: runID, TaskKey: "intake", Status: "running"}
|
||||
taskReq := bizdev.TaskRunRequest{TaskKey: "intake", ProjectName: "disconnect-test"}
|
||||
_, err := srv.taskRuns.CreateAndStart(context.Background(), runID, record, taskReq, bizdev.StageRunRequest{}, "")
|
||||
_, err := srv.taskRuns().CreateAndStart(context.Background(), runID, record, taskReq, bizdev.StageRunRequest{}, "")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateAndStart: %v", err)
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func TestTaskRun_DisconnectDoesNotCancelWorker(t *testing.T) {
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
rec, getErr := srv.taskRuns.GetRun(runID)
|
||||
rec, getErr := srv.taskRuns().GetRun(runID)
|
||||
if getErr == nil && rec.Status != "running" {
|
||||
if rec.Status == "failed" {
|
||||
t.Fatalf("worker marked failed after disconnect, status=%s err=%s", rec.Status, rec.ErrorMessage)
|
||||
|
||||
@@ -110,7 +110,7 @@ func (s *Server) handleProjectTraceDecision(w http.ResponseWriter, r *http.Reque
|
||||
writeSafeError(w, r, http.StatusForbidden, `{"error":"ERR_FORBIDDEN","message":"看不见下游文档,不能裁决"}`)
|
||||
return
|
||||
}
|
||||
creator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
creator, _ := s.pwSvc().IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
if err := s.rt.Trace.DecideCandidate(r.Context(), edgeID, user.ID, creator, body.DownstreamDoc, body.DownstreamAuthor, body.Decision, body.Reason); err != nil {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{"error": "TRACE_DECISION_DENIED", "message": err.Error()})
|
||||
return
|
||||
@@ -130,7 +130,7 @@ func (s *Server) handleProjectTraceReopen(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
req.ProjectID = projectID
|
||||
req.RequestedBy = user.ID
|
||||
creator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
creator, _ := s.pwSvc().IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
if !s.callerMayRequestReopen(r, projectID, user.ID, creator, req) {
|
||||
writeSafeError(w, r, http.StatusForbidden, `{"error":"ERR_FORBIDDEN","message":"只有该阶段文档职责人或创建人可以申请再打开"}`)
|
||||
return
|
||||
@@ -148,7 +148,7 @@ func (s *Server) handleProjectTraceReopenApprove(w http.ResponseWriter, r *http.
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
creator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
creator, _ := s.pwSvc().IsProjectCreator(r.Context(), projectID, user.ID)
|
||||
if !creator {
|
||||
writeSafeError(w, r, http.StatusForbidden, `{"error":"ERR_NOT_PROJECT_CREATOR","message":"仅项目创建人可批准再打开"}`)
|
||||
return
|
||||
@@ -209,8 +209,8 @@ func (s *Server) handleProjectTraceRecheckClear(w http.ResponseWriter, r *http.R
|
||||
writeJSONResponse(w, http.StatusBadRequest, map[string]string{"error": "TRACE_RECHECK_CLEAR_FAILED", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
if body.MessageID > 0 && s.ctxMgr != nil {
|
||||
_ = s.ctxMgr.SetInteractiveCardStatus(r.Context(), body.MessageID, "resolved", "已标记为无需变更")
|
||||
if body.MessageID > 0 && s.ctxMgr() != nil {
|
||||
_ = s.ctxMgr().SetInteractiveCardStatus(r.Context(), body.MessageID, "resolved", "已标记为无需变更")
|
||||
}
|
||||
writeJSONResponse(w, http.StatusOK, map[string]string{"status": "cleared"})
|
||||
}
|
||||
@@ -236,10 +236,10 @@ func (s *Server) callerMayRequestReopen(r *http.Request, projectID, userID int64
|
||||
if creator {
|
||||
return true
|
||||
}
|
||||
if s.instSvc == nil {
|
||||
if s.instSvc() == nil {
|
||||
return false
|
||||
}
|
||||
settings, err := s.instSvc.GetSettings(r.Context(), projectID, userID, false)
|
||||
settings, err := s.instSvc().GetSettings(r.Context(), projectID, userID, false)
|
||||
if err != nil || settings == nil || settings.Plan == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func (s *Server) handleUserDirectory(w http.ResponseWriter, r *http.Request) {
|
||||
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
users, err := s.authSvc.ListUserDirectory(r.Context())
|
||||
users, err := s.authSvc().ListUserDirectory(r.Context())
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -60,7 +60,7 @@ func (s *Server) handleAdminUpdateProfile(w http.ResponseWriter, r *http.Request
|
||||
writeSafeError(w, r, http.StatusBadRequest, "姓名不能为空")
|
||||
return
|
||||
}
|
||||
updated, err := s.authSvc.AdminUpdateUserProfile(r.Context(), targetID, auth.ProfilePatch{
|
||||
updated, err := s.authSvc().AdminUpdateUserProfile(r.Context(), targetID, auth.ProfilePatch{
|
||||
Name: body.Name,
|
||||
Department: body.Department,
|
||||
Email: body.Email,
|
||||
|
||||
@@ -55,13 +55,16 @@ func (s *Server) startVaultPublisher() {
|
||||
if s == nil || s.vaultPub == nil || s.cfg == nil || !s.cfg.FAVPublisherEnabled {
|
||||
return
|
||||
}
|
||||
s.vaultStop = make(chan struct{})
|
||||
stopCh, ok := s.vaultLoop.start()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
t := time.NewTicker(2 * time.Second)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-s.vaultStop:
|
||||
case <-stopCh:
|
||||
return
|
||||
case <-t.C:
|
||||
_ = s.vaultPub.Drain(context.Background())
|
||||
@@ -104,7 +107,7 @@ func (s *Server) wireFinalArtifactHooks() {
|
||||
if s == nil || s.vaultIngest == nil {
|
||||
return
|
||||
}
|
||||
if gov := s.gov; gov != nil {
|
||||
if gov := s.gov(); gov != nil {
|
||||
gov.OnFinalArtifact = func(ctx context.Context, doc *docgov.DocumentRecord, markdown []byte, snapshotID int64) {
|
||||
if doc == nil {
|
||||
return
|
||||
@@ -118,7 +121,7 @@ func (s *Server) wireFinalArtifactHooks() {
|
||||
return s.vaultIngest.ApprovedTX(ctx, tx, doc.ProjectID, doc.DocID, snapshotID, doc.ApprovedAt, markdown)
|
||||
}
|
||||
}
|
||||
if rev := s.rev; rev != nil {
|
||||
if rev := s.rev(); rev != nil {
|
||||
rev.OnFinalArtifact = func(ctx context.Context, kind, docID string, projectID, snapshotID int64, eventID string, payload []byte, sealedAt *time.Time) {
|
||||
switch kind {
|
||||
case "agent_commit":
|
||||
@@ -138,7 +141,7 @@ func (s *Server) wireFinalArtifactHooks() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if fd := s.freeDoc; fd != nil {
|
||||
if fd := s.freeDoc(); fd != nil {
|
||||
fd.OnFinalArtifact = func(ctx context.Context, projectID int64, docID, eventID string, snapshotID int64, payload []byte) {
|
||||
_ = s.vaultIngest.FreeformPublish(ctx, projectID, docID, eventID, snapshotID, payload)
|
||||
}
|
||||
|
||||
@@ -18,24 +18,24 @@ func (s *Server) onWorkflowDocumentApproved(ctx context.Context, doc *docgov.Doc
|
||||
if s.rt != nil && s.rt.WriterLive(ctx, events.ConsumerWorkflowAdvance) {
|
||||
return
|
||||
}
|
||||
if doc == nil || doc.TaskKey == "" || doc.ProjectID <= 0 || s.instSvc == nil {
|
||||
if doc == nil || doc.TaskKey == "" || doc.ProjectID <= 0 || s.instSvc() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := s.instSvc.HandleDocumentApproved(ctx, doc.ProjectID, doc.TaskKey)
|
||||
result, err := s.instSvc().HandleDocumentApproved(ctx, doc.ProjectID, doc.TaskKey)
|
||||
if err != nil || result == nil || !result.StageReady {
|
||||
return
|
||||
}
|
||||
if s.notifySvc == nil || !s.notifySvc.Available() {
|
||||
if s.notifySvc() == nil || !s.notifySvc().Available() {
|
||||
return
|
||||
}
|
||||
|
||||
project, err := s.pwSvc.GetProject(ctx, doc.ProjectID, 0)
|
||||
project, err := s.pwSvc().GetProject(ctx, doc.ProjectID, 0)
|
||||
if err != nil || project == nil || project.CreatorUserID <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = s.notifySvc.EmitStageReadyToAdvance(ctx, project.CreatorUserID, notification.StageReadyPayload{
|
||||
_, _ = s.notifySvc().EmitStageReadyToAdvance(ctx, project.CreatorUserID, notification.StageReadyPayload{
|
||||
ProjectID: doc.ProjectID,
|
||||
WorkflowInstanceID: result.InstanceID,
|
||||
StageInstanceID: result.StageInstanceID,
|
||||
|
||||
@@ -16,11 +16,11 @@ type workflowCatalogItem struct {
|
||||
}
|
||||
|
||||
func (s *Server) handleWorkflowCatalog(w http.ResponseWriter, r *http.Request) {
|
||||
if s.wfDefSvc == nil || !s.wfDefSvc.Available() {
|
||||
if s.wfDefSvc() == nil || !s.wfDefSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"workflow catalog unavailable"}`)
|
||||
return
|
||||
}
|
||||
defs, err := s.wfDefSvc.Store().ListProjectSelectableDefinitions(r.Context())
|
||||
defs, err := s.wfDefSvc().Store().ListProjectSelectableDefinitions(r.Context())
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||
return
|
||||
|
||||
@@ -14,11 +14,11 @@ func (s *Server) handleGetWorkflowInstance(w http.ResponseWriter, r *http.Reques
|
||||
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid instance id"}`)
|
||||
return
|
||||
}
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||
return
|
||||
}
|
||||
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
||||
detail, err := s.instSvc().GetInstanceDetail(r.Context(), instanceID)
|
||||
if err != nil {
|
||||
writeProjectInstanceError(w, err)
|
||||
return
|
||||
@@ -36,11 +36,11 @@ func (s *Server) handleGetStageReadiness(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
stageKey := r.PathValue("stage")
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||
return
|
||||
}
|
||||
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
||||
detail, err := s.instSvc().GetInstanceDetail(r.Context(), instanceID)
|
||||
if err != nil {
|
||||
writeProjectInstanceError(w, err)
|
||||
return
|
||||
@@ -49,8 +49,8 @@ func (s *Server) handleGetStageReadiness(w http.ResponseWriter, r *http.Request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), detail.Instance.ProjectID, user.ID)
|
||||
readiness, err := s.instSvc.GetStageReadiness(r.Context(), instanceID, stageKey, user.ID, isCreator)
|
||||
isCreator, _ := s.pwSvc().IsProjectCreator(r.Context(), detail.Instance.ProjectID, user.ID)
|
||||
readiness, err := s.instSvc().GetStageReadiness(r.Context(), instanceID, stageKey, user.ID, isCreator)
|
||||
if err != nil {
|
||||
writeProjectInstanceError(w, err)
|
||||
return
|
||||
@@ -65,11 +65,11 @@ func (s *Server) handleAdvanceStage(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
stageKey := r.PathValue("stage")
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||
return
|
||||
}
|
||||
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
||||
detail, err := s.instSvc().GetInstanceDetail(r.Context(), instanceID)
|
||||
if err != nil {
|
||||
writeProjectInstanceError(w, err)
|
||||
return
|
||||
@@ -78,7 +78,7 @@ func (s *Server) handleAdvanceStage(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), detail.Instance.ProjectID, user.ID)
|
||||
isCreator, _ := s.pwSvc().IsProjectCreator(r.Context(), detail.Instance.ProjectID, user.ID)
|
||||
var req projectinstance.AdvanceStageInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && r.ContentLength > 0 {
|
||||
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`)
|
||||
@@ -87,13 +87,13 @@ func (s *Server) handleAdvanceStage(w http.ResponseWriter, r *http.Request) {
|
||||
if req.StageKey == "" {
|
||||
req.StageKey = stageKey
|
||||
}
|
||||
result, err := s.instSvc.AdvanceStage(r.Context(), instanceID, user.ID, isCreator, req)
|
||||
result, err := s.instSvc().AdvanceStage(r.Context(), instanceID, user.ID, isCreator, req)
|
||||
if err != nil {
|
||||
writeProjectInstanceError(w, err)
|
||||
return
|
||||
}
|
||||
if s.notifySvc != nil && result.CompletedStageInstanceID > 0 {
|
||||
_ = s.notifySvc.ConfirmStageAdvanceRequest(
|
||||
if s.notifySvc() != nil && result.CompletedStageInstanceID > 0 {
|
||||
_ = s.notifySvc().ConfirmStageAdvanceRequest(
|
||||
r.Context(),
|
||||
result.CompletedStageInstanceID,
|
||||
result.CompletedReadinessRevision,
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func (s *Server) handleTerminationPreview(w http.ResponseWriter, r *http.Request) {
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE"}`)
|
||||
return
|
||||
}
|
||||
@@ -24,13 +24,13 @@ func (s *Server) handleTerminationPreview(w http.ResponseWriter, r *http.Request
|
||||
writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED"}`)
|
||||
return
|
||||
}
|
||||
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
||||
detail, err := s.instSvc().GetInstanceDetail(r.Context(), instanceID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_NOT_FOUND"}`)
|
||||
return
|
||||
}
|
||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), detail.Instance.ProjectID, user.ID)
|
||||
preview, err := s.instSvc.GetTerminationPreview(r.Context(), instanceID, detail.Instance.ProjectID, user.ID, isCreator)
|
||||
isCreator, _ := s.pwSvc().IsProjectCreator(r.Context(), detail.Instance.ProjectID, user.ID)
|
||||
preview, err := s.instSvc().GetTerminationPreview(r.Context(), instanceID, detail.Instance.ProjectID, user.ID, isCreator)
|
||||
if err != nil {
|
||||
writeProjectInstanceError(w, err)
|
||||
return
|
||||
@@ -39,7 +39,7 @@ func (s *Server) handleTerminationPreview(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
func (s *Server) handleTerminateWorkflowInstance(w http.ResponseWriter, r *http.Request) {
|
||||
if s.instSvc == nil || !s.instSvc.Available() {
|
||||
if s.instSvc() == nil || !s.instSvc().Available() {
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE"}`)
|
||||
return
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (s *Server) handleTerminateWorkflowInstance(w http.ResponseWriter, r *http.
|
||||
writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED"}`)
|
||||
return
|
||||
}
|
||||
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
||||
detail, err := s.instSvc().GetInstanceDetail(r.Context(), instanceID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_NOT_FOUND"}`)
|
||||
return
|
||||
@@ -63,8 +63,8 @@ func (s *Server) handleTerminateWorkflowInstance(w http.ResponseWriter, r *http.
|
||||
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM"}`)
|
||||
return
|
||||
}
|
||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), detail.Instance.ProjectID, user.ID)
|
||||
res, err := s.instSvc.TerminateInstance(r.Context(), instanceID, detail.Instance.ProjectID, user.ID, isCreator, in)
|
||||
isCreator, _ := s.pwSvc().IsProjectCreator(r.Context(), detail.Instance.ProjectID, user.ID)
|
||||
res, err := s.instSvc().TerminateInstance(r.Context(), instanceID, detail.Instance.ProjectID, user.ID, isCreator, in)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, projectinstance.ErrForbiddenNotCreator):
|
||||
|
||||
@@ -83,7 +83,7 @@ func (s *Server) serveAdminWorkspaceCleanup(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
}
|
||||
cap := s.wsCap
|
||||
cap := s.wsCap()
|
||||
if cap == nil && s.cfg != nil {
|
||||
cap = workspace.NewCapability(s.cfg.DataDir, nil)
|
||||
}
|
||||
@@ -107,22 +107,21 @@ func workspaceRegistryFor(cfg *config.Config) *workspace.FileRegistry {
|
||||
|
||||
func (s *Server) startWorkspaceGC() {
|
||||
reg := s.workspaceRegistry()
|
||||
if reg == nil || s.cfg == nil || s.wsGCStop != nil {
|
||||
stopCh, ok := s.wsGCLoop.start()
|
||||
if reg == nil || s.cfg == nil || !ok {
|
||||
return
|
||||
}
|
||||
s.wsGCStop = make(chan struct{})
|
||||
ttl := s.cfg.WorkspaceTTL
|
||||
quota := s.cfg.WorkspaceQuotaBytes
|
||||
stop := s.wsGCStop
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
case <-stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
cap := s.wsCap
|
||||
cap := s.wsCap()
|
||||
if cap == nil {
|
||||
cap = workspace.NewCapability(s.cfg.DataDir, nil)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user