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 (baseline 44161d2a reproduced DATA RACE; now green under -race)
- goose 00071 duplicate (dialogue merge 1da17a28 brought 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:
weichen
2026-09-25 13:17:22 +00:00
parent 7cfd6851f6
commit a9c9a9182e
76 changed files with 1235 additions and 646 deletions
+4 -4
View File
@@ -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):
+12 -12
View File
@@ -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")
+3 -3
View File
@@ -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
+11 -11
View File
@@ -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())
+8 -8
View File
@@ -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
+20 -20
View File
@@ -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
}
+9 -9
View File
@@ -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("")
}
+1 -1
View File
@@ -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)
+36
View File
@@ -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) })
}
}
+10 -10
View File
@@ -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
}
+2 -2
View File
@@ -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)
+10 -10
View File
@@ -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 {
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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
+7 -4
View File
@@ -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()
}
}
+2 -2
View File
@@ -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)
}
+7 -7
View File
@@ -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" {
+18 -18
View File
@@ -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 {
+14 -14
View File
@@ -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 {
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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)
}
+18 -18
View File
@@ -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": "文档不存在"})
+8 -8
View File
@@ -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,
+8 -8
View File
@@ -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, "")
}
// 隐藏不必要的中间大体积字节
+6 -6
View File
@@ -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
+22 -22
View File
@@ -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
+10 -10
View File
@@ -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
+5 -5
View File
@@ -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 {
+6 -6
View File
@@ -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) {
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+12 -12
View File
@@ -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
}
+4 -4
View File
@@ -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
+7 -7
View File
@@ -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
+3 -3
View File
@@ -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",
+15 -15
View File
@@ -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
+15 -15
View File
@@ -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
}
}
+1 -1
View File
@@ -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 {
+13 -13
View File
@@ -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
}
+20 -20
View File
@@ -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
+2 -2
View File
@@ -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
+7 -7
View File
@@ -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",
+434
View File
@@ -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
View File
@@ -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 {
+4 -4
View File
@@ -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)
}
+6 -6
View File
@@ -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")
+1 -1
View File
@@ -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",
+17 -17
View File
@@ -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
}
}
+8 -8
View File
@@ -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 ""
}
+17 -17
View File
@@ -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,
+5 -5
View File
@@ -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
}
+11 -11
View File
@@ -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 {
+29 -29
View File
@@ -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)
}
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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)
+7 -7
View File
@@ -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
}
+2 -2
View File
@@ -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,
+8 -5
View File
@@ -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)
}
+5 -5
View File
@@ -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,
+2 -2
View File
@@ -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
+12 -12
View File
@@ -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):
+5 -6
View File
@@ -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)
}