merge: feature/http-error-envelope
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
# HTTP/SSE 错误信封 H1–H3 测试报告
|
||||||
|
|
||||||
|
## 一、 测试背景 (Background)
|
||||||
|
|
||||||
|
H0 盘点公开路由存在纯文本 `err.Error()`、JSON `{error,message}` 与 SSE `error_info` 三套错误体。本波 additive 统一信封,不改状态码。
|
||||||
|
|
||||||
|
## 二、 测试目标 (Test Objectives)
|
||||||
|
|
||||||
|
- H1:`code`/`message`/`request_id`/`error` 双写;SQL/路径/token/XSS 不出现在响应。
|
||||||
|
- H2:前端优先 `code`;未知 code 不靠 substring。
|
||||||
|
- H3:去掉「无权访问该项目」文本匹配;不改状态码故无 ADR。
|
||||||
|
|
||||||
|
## 三、 测试方案与环境 (Test Plan & Environment)
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|----|-----|
|
||||||
|
| 分支 | `feature/http-error-envelope` |
|
||||||
|
| Go | `go test -short ./internal/httpx ./internal/stream ./internal/server` |
|
||||||
|
| 前端 | `npx tsx --test src/api/projectSettingsError.test.ts` |
|
||||||
|
|
||||||
|
## 四、 预期结果 (Expected Results)
|
||||||
|
|
||||||
|
信封 golden 负例 PASS;server 短测 PASS;前端解析 `code` 优先于 `error`。
|
||||||
|
|
||||||
|
## 五、 实际测试结果与判定 (Actual Results & Evaluation)
|
||||||
|
|
||||||
|
### 5.1 httpx + SSE
|
||||||
|
|
||||||
|
- **测试输入 (Input)**:`TestWriteSanitizesInternalAndKeepsLegacyCode`;`TestClassifyError_DoesNotLeakSQL`
|
||||||
|
- **实际输出 (Actual Output)**:`ok httpx`;`ok stream`
|
||||||
|
- **评价判定 (Evaluation / Verdict)**:**PASS**
|
||||||
|
- **性能与耗时 (Duration & Latency)**:<10ms
|
||||||
|
|
||||||
|
### 5.2 server 回归
|
||||||
|
|
||||||
|
- **测试输入 (Input)**:`go test -short ./internal/server`
|
||||||
|
- **实际输出 (Actual Output)**:`ok 10.069s`
|
||||||
|
- **评价判定 (Evaluation / Verdict)**:**PASS**
|
||||||
|
- **性能与耗时 (Duration & Latency)**:10s
|
||||||
|
|
||||||
|
### 5.3 前端
|
||||||
|
|
||||||
|
- **测试输入 (Input)**:`projectSettingsError.test.ts`
|
||||||
|
- **实际输出 (Actual Output)**:5 tests pass
|
||||||
|
- **评价判定 (Evaluation / Verdict)**:**PASS**
|
||||||
|
- **性能与耗时 (Duration & Latency)**:113ms
|
||||||
|
|
||||||
|
## 六、 自动化测试套件汇总 (Test Suite Execution)
|
||||||
|
|
||||||
|
上述均 PASS。未改 HTTP 状态码,无 ADR。
|
||||||
|
|
||||||
|
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
|
||||||
|
|
||||||
|
H1–H3 完成。`error` 字段仍作稳定 code 别名以兼容旧客户端。下一步 PI typed-result。
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package httpx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CodeAUTHZ = "AUTHZ"
|
||||||
|
CodeVALIDATION = "VALIDATION"
|
||||||
|
CodeVERSION_CONFLICT = "VERSION_CONFLICT"
|
||||||
|
CodeIDEMPOTENCY_CONFLICT = "IDEMPOTENCY_CONFLICT"
|
||||||
|
CodeINTERNAL = "INTERNAL"
|
||||||
|
HeaderRequestID = "X-Request-ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Envelope struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Details map[string]any `json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CodeFromStatus(status int) string {
|
||||||
|
switch status {
|
||||||
|
case http.StatusUnauthorized, http.StatusForbidden:
|
||||||
|
return CodeAUTHZ
|
||||||
|
case http.StatusConflict:
|
||||||
|
return CodeVERSION_CONFLICT
|
||||||
|
case http.StatusBadRequest, http.StatusUnprocessableEntity:
|
||||||
|
return CodeVALIDATION
|
||||||
|
default:
|
||||||
|
if status >= 500 {
|
||||||
|
return CodeINTERNAL
|
||||||
|
}
|
||||||
|
if status == http.StatusNotFound {
|
||||||
|
return CodeAUTHZ
|
||||||
|
}
|
||||||
|
return CodeVALIDATION
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SafeMessage(code string) string {
|
||||||
|
switch code {
|
||||||
|
case CodeAUTHZ:
|
||||||
|
return "无权访问"
|
||||||
|
case CodeVALIDATION:
|
||||||
|
return "请求参数无效"
|
||||||
|
case CodeVERSION_CONFLICT:
|
||||||
|
return "资源版本冲突"
|
||||||
|
case CodeIDEMPOTENCY_CONFLICT:
|
||||||
|
return "重复提交"
|
||||||
|
default:
|
||||||
|
return "请求无法完成"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LooksInternal(s string) bool {
|
||||||
|
low := strings.ToLower(s)
|
||||||
|
needles := []string{"sql", "pq:", "postgres", "stack", "panic", "token", "password", "secret", "api_key", "bearer ", "../", "prompt"}
|
||||||
|
for _, n := range needles {
|
||||||
|
if strings.Contains(low, n) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(s, "/home/") || strings.Contains(s, "/var/") || strings.Contains(s, "\\") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequestID(r *http.Request) string {
|
||||||
|
if r == nil {
|
||||||
|
return newRequestID()
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(r.Header.Get(HeaderRequestID)); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return newRequestID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequestID() string {
|
||||||
|
var b [8]byte
|
||||||
|
_, _ = rand.Read(b[:])
|
||||||
|
return "req_" + hex.EncodeToString(b[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func PublicLegacy(code, legacy string) string {
|
||||||
|
legacy = strings.TrimSpace(legacy)
|
||||||
|
if legacy == "" || LooksInternal(legacy) {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(legacy, "<") || strings.Contains(legacy, "<script") {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
return legacy
|
||||||
|
}
|
||||||
|
|
||||||
|
func Write(w http.ResponseWriter, status int, code, legacy, requestID string, details map[string]any) {
|
||||||
|
if code == "" {
|
||||||
|
code = CodeFromStatus(status)
|
||||||
|
}
|
||||||
|
if requestID == "" {
|
||||||
|
requestID = newRequestID()
|
||||||
|
}
|
||||||
|
env := Envelope{
|
||||||
|
Code: code,
|
||||||
|
Message: SafeMessage(code),
|
||||||
|
RequestID: requestID,
|
||||||
|
Error: PublicLegacy(code, legacy),
|
||||||
|
Details: details,
|
||||||
|
}
|
||||||
|
if w.Header().Get("Content-Type") == "" {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
}
|
||||||
|
w.Header().Set(HeaderRequestID, requestID)
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_ = json.NewEncoder(w).Encode(env)
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package httpx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriteSanitizesInternalAndKeepsLegacyCode(t *testing.T) {
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
Write(rec, http.StatusInternalServerError, CodeINTERNAL, "pq: relation foo leaked /home/weichen/secret", "req_test", nil)
|
||||||
|
var env Envelope
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &env); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if env.Code != CodeINTERNAL || env.RequestID != "req_test" {
|
||||||
|
t.Fatalf("%+v", env)
|
||||||
|
}
|
||||||
|
if strings.Contains(rec.Body.String(), "pq:") || strings.Contains(rec.Body.String(), "/home/") {
|
||||||
|
t.Fatalf("leaked: %s", rec.Body.String())
|
||||||
|
}
|
||||||
|
if env.Error != CodeINTERNAL {
|
||||||
|
t.Fatalf("legacy error=%s", env.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteKeepsStablePublicErrorCode(t *testing.T) {
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
Write(rec, http.StatusForbidden, CodeAUTHZ, "ERR_FORBIDDEN", "req_2", nil)
|
||||||
|
var env Envelope
|
||||||
|
_ = json.Unmarshal(rec.Body.Bytes(), &env)
|
||||||
|
if env.Error != "ERR_FORBIDDEN" || env.Code != CodeAUTHZ {
|
||||||
|
t.Fatalf("%+v", env)
|
||||||
|
}
|
||||||
|
if strings.Contains(rec.Body.String(), "<script") {
|
||||||
|
t.Fatal("xss")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLooksInternalXSS(t *testing.T) {
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
Write(rec, http.StatusBadRequest, CodeVALIDATION, "<script>alert(1)</script>", "req_3", nil)
|
||||||
|
if strings.Contains(rec.Body.String(), "<script") {
|
||||||
|
t.Fatalf("%s", rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodeFromStatus(t *testing.T) {
|
||||||
|
if CodeFromStatus(http.StatusUnauthorized) != CodeAUTHZ {
|
||||||
|
t.Fatal("401")
|
||||||
|
}
|
||||||
|
if CodeFromStatus(http.StatusConflict) != CodeVERSION_CONFLICT {
|
||||||
|
t.Fatal("409")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ func (s *Server) handleAdminPermissions(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
catalog := authz.PermissionCatalog()
|
catalog := authz.PermissionCatalog()
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ func (s *Server) handleAdminRoles(w http.ResponseWriter, r *http.Request) {
|
|||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
roles, err := s.authzSvc.ListRoles(r.Context())
|
roles, err := s.authzSvc.ListRoles(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, map[string]any{"roles": roles})
|
writeJSONResponse(w, http.StatusOK, map[string]any{"roles": roles})
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
var req authz.CreateRoleInput
|
var req authz.CreateRoleInput
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "无效的 JSON 请求体", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的 JSON 请求体")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
created, err := s.authzSvc.CreateRole(r.Context(), req)
|
created, err := s.authzSvc.CreateRole(r.Context(), req)
|
||||||
@@ -34,12 +34,12 @@ func (s *Server) handleAdminRoles(w http.ResponseWriter, r *http.Request) {
|
|||||||
if errors.Is(err, authz.ErrRolesNotAvailable) {
|
if errors.Is(err, authz.ErrRolesNotAvailable) {
|
||||||
status = http.StatusServiceUnavailable
|
status = http.StatusServiceUnavailable
|
||||||
}
|
}
|
||||||
http.Error(w, err.Error(), status)
|
writeSafeError(w, r, status, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusCreated, created)
|
writeJSONResponse(w, http.StatusCreated, created)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "不支持的请求方法", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "不支持的请求方法")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ func (s *Server) handleAdminRoleByID(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
roleID := strings.TrimSpace(r.PathValue("id"))
|
roleID := strings.TrimSpace(r.PathValue("id"))
|
||||||
if roleID == "" {
|
if roleID == "" {
|
||||||
http.Error(w, "无效的角色 ID", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的角色 ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ func (s *Server) handleAdminRoleByID(w http.ResponseWriter, r *http.Request) {
|
|||||||
case http.MethodPatch, http.MethodPut:
|
case http.MethodPatch, http.MethodPut:
|
||||||
var req authz.UpdateRoleInput
|
var req authz.UpdateRoleInput
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "无效的 JSON 请求体", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的 JSON 请求体")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
updated, err := s.authzSvc.UpdateRole(r.Context(), roleID, req)
|
updated, err := s.authzSvc.UpdateRole(r.Context(), roleID, req)
|
||||||
@@ -72,7 +72,7 @@ func (s *Server) handleAdminRoleByID(w http.ResponseWriter, r *http.Request) {
|
|||||||
case errors.Is(err, authz.ErrRolesNotAvailable):
|
case errors.Is(err, authz.ErrRolesNotAvailable):
|
||||||
status = http.StatusServiceUnavailable
|
status = http.StatusServiceUnavailable
|
||||||
}
|
}
|
||||||
http.Error(w, err.Error(), status)
|
writeSafeError(w, r, status, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, updated)
|
writeJSONResponse(w, http.StatusOK, updated)
|
||||||
@@ -89,11 +89,11 @@ func (s *Server) handleAdminRoleByID(w http.ResponseWriter, r *http.Request) {
|
|||||||
case errors.Is(err, authz.ErrRolesNotAvailable):
|
case errors.Is(err, authz.ErrRolesNotAvailable):
|
||||||
status = http.StatusServiceUnavailable
|
status = http.StatusServiceUnavailable
|
||||||
}
|
}
|
||||||
http.Error(w, err.Error(), status)
|
writeSafeError(w, r, status, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, map[string]bool{"success": true})
|
writeJSONResponse(w, http.StatusOK, map[string]bool{"success": true})
|
||||||
default:
|
default:
|
||||||
http.Error(w, "不支持的请求方法", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "不支持的请求方法")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,17 +19,17 @@ func (s *Server) handleAdminTemplates(w http.ResponseWriter, r *http.Request) {
|
|||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
assets, err := s.wfDefSvc.ListTemplates(r.Context())
|
assets, err := s.wfDefSvc.ListTemplates(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, map[string]any{"templates": assets})
|
writeJSONResponse(w, http.StatusOK, map[string]any{"templates": assets})
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
if err := r.ParseMultipartForm(defaultUploadBodyLimit); err != nil {
|
if err := r.ParseMultipartForm(defaultUploadBodyLimit); err != nil {
|
||||||
if isRequestBodyTooLarge(err) {
|
if isRequestBodyTooLarge(err) {
|
||||||
http.Error(w, "upload too large", http.StatusRequestEntityTooLarge)
|
writeSafeError(w, r, http.StatusRequestEntityTooLarge, "upload too large")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, "invalid multipart", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid multipart")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
assetKey := strings.TrimSpace(r.FormValue("asset_key"))
|
assetKey := strings.TrimSpace(r.FormValue("asset_key"))
|
||||||
@@ -42,13 +42,13 @@ func (s *Server) handleAdminTemplates(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
file, header, err := r.FormFile("file")
|
file, header, err := r.FormFile("file")
|
||||||
if err != nil || assetKey == "" {
|
if err != nil || assetKey == "" {
|
||||||
http.Error(w, "asset_key and file required", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "asset_key and file required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
data, err := io.ReadAll(io.LimitReader(file, 32<<20))
|
data, err := io.ReadAll(io.LimitReader(file, 32<<20))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "read file failed", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "read file failed")
|
||||||
return
|
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)
|
||||||
@@ -60,12 +60,12 @@ func (s *Server) handleAdminTemplates(w http.ResponseWriter, r *http.Request) {
|
|||||||
case errors.Is(err, workflowdef.ErrNotAvailable):
|
case errors.Is(err, workflowdef.ErrNotAvailable):
|
||||||
status = http.StatusServiceUnavailable
|
status = http.StatusServiceUnavailable
|
||||||
}
|
}
|
||||||
http.Error(w, err.Error(), status)
|
writeSafeError(w, r, status, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusCreated, tv)
|
writeJSONResponse(w, http.StatusCreated, tv)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,17 +74,17 @@ func (s *Server) handleAdminTemplateVersions(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
assetKey := strings.TrimSpace(r.PathValue("key"))
|
assetKey := strings.TrimSpace(r.PathValue("key"))
|
||||||
if assetKey == "" {
|
if assetKey == "" {
|
||||||
http.Error(w, "invalid asset key", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid asset key")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
versions, err := s.wfDefSvc.ListTemplateVersions(r.Context(), assetKey)
|
versions, err := s.wfDefSvc.ListTemplateVersions(r.Context(), assetKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, map[string]any{"versions": versions})
|
writeJSONResponse(w, http.StatusOK, map[string]any{"versions": versions})
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func (s *Server) requireWorkflowAdmin(w http.ResponseWriter, r *http.Request) (*
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
if s.wfDefSvc == nil || !s.wfDefSvc.Available() {
|
if s.wfDefSvc == nil || !s.wfDefSvc.Available() {
|
||||||
http.Error(w, "工作流定义服务需要数据库 IAM", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "工作流定义服务需要数据库 IAM")
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return user, true
|
return user, true
|
||||||
@@ -31,14 +31,14 @@ func (s *Server) handleAdminWorkflows(w http.ResponseWriter, r *http.Request) {
|
|||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
defs, err := s.wfDefSvc.ListDefinitions(r.Context())
|
defs, err := s.wfDefSvc.ListDefinitions(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, map[string]any{"workflows": defs})
|
writeJSONResponse(w, http.StatusOK, map[string]any{"workflows": defs})
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
var req workflowdef.CreateDefinitionInput
|
var req workflowdef.CreateDefinitionInput
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "无效的 JSON", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的 JSON")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
def, ver, err := s.wfDefSvc.CreateDefinition(r.Context(), req, user.ID)
|
def, ver, err := s.wfDefSvc.CreateDefinition(r.Context(), req, user.ID)
|
||||||
@@ -47,12 +47,12 @@ func (s *Server) handleAdminWorkflows(w http.ResponseWriter, r *http.Request) {
|
|||||||
if errors.Is(err, workflowdef.ErrValidationFailed) {
|
if errors.Is(err, workflowdef.ErrValidationFailed) {
|
||||||
status = http.StatusUnprocessableEntity
|
status = http.StatusUnprocessableEntity
|
||||||
}
|
}
|
||||||
http.Error(w, err.Error(), status)
|
writeSafeError(w, r, status, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusCreated, map[string]any{"definition": def, "version": ver})
|
writeJSONResponse(w, http.StatusCreated, map[string]any{"definition": def, "version": ver})
|
||||||
default:
|
default:
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,21 +62,21 @@ func (s *Server) handleAdminWorkflowByID(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || id <= 0 {
|
if err != nil || id <= 0 {
|
||||||
http.Error(w, "invalid workflow id", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid workflow id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
def, err := s.wfDefSvc.GetDefinition(r.Context(), id)
|
def, err := s.wfDefSvc.GetDefinition(r.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
versions, err := s.wfDefSvc.ListVersions(r.Context(), id)
|
versions, err := s.wfDefSvc.ListVersions(r.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, map[string]any{"definition": def, "versions": versions})
|
writeJSONResponse(w, http.StatusOK, map[string]any{"definition": def, "versions": versions})
|
||||||
@@ -89,12 +89,12 @@ func (s *Server) handleAdminWorkflowVersion(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
workflowID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
workflowID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || workflowID <= 0 {
|
if err != nil || workflowID <= 0 {
|
||||||
http.Error(w, "invalid workflow id", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid workflow id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
version, err := strconv.Atoi(r.PathValue("version"))
|
version, err := strconv.Atoi(r.PathValue("version"))
|
||||||
if err != nil || version <= 0 {
|
if err != nil || version <= 0 {
|
||||||
http.Error(w, "invalid version", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid version")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ func (s *Server) handleAdminWorkflowVersion(w http.ResponseWriter, r *http.Reque
|
|||||||
Definition workflowdef.VersionDefinition `json:"definition"`
|
Definition workflowdef.VersionDefinition `json:"definition"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid json")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
updated, err := s.wfDefSvc.UpdateDraft(r.Context(), workflowID, version, body.Definition)
|
updated, err := s.wfDefSvc.UpdateDraft(r.Context(), workflowID, version, body.Definition)
|
||||||
@@ -113,12 +113,12 @@ func (s *Server) handleAdminWorkflowVersion(w http.ResponseWriter, r *http.Reque
|
|||||||
if errors.Is(err, workflowdef.ErrNotDraft) {
|
if errors.Is(err, workflowdef.ErrNotDraft) {
|
||||||
status = http.StatusConflict
|
status = http.StatusConflict
|
||||||
}
|
}
|
||||||
http.Error(w, err.Error(), status)
|
writeSafeError(w, r, status, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, updated)
|
writeJSONResponse(w, http.StatusOK, updated)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
}
|
}
|
||||||
_ = user
|
_ = user
|
||||||
}
|
}
|
||||||
@@ -128,14 +128,14 @@ func (s *Server) handleAdminWorkflowValidate(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
workflowID, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
workflowID, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
version, _ := strconv.Atoi(r.PathValue("version"))
|
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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, res)
|
writeJSONResponse(w, http.StatusOK, res)
|
||||||
@@ -147,7 +147,7 @@ func (s *Server) handleAdminWorkflowPublish(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
workflowID, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
workflowID, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
@@ -158,7 +158,7 @@ func (s *Server) handleAdminWorkflowPublish(w http.ResponseWriter, r *http.Reque
|
|||||||
if errors.Is(err, workflowdef.ErrValidationFailed) {
|
if errors.Is(err, workflowdef.ErrValidationFailed) {
|
||||||
status = http.StatusUnprocessableEntity
|
status = http.StatusUnprocessableEntity
|
||||||
}
|
}
|
||||||
http.Error(w, err.Error(), status)
|
writeSafeError(w, r, status, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if dbCat, ok := s.pwSvc.Catalog().(*workflowdef.DBCatalog); ok {
|
if dbCat, ok := s.pwSvc.Catalog().(*workflowdef.DBCatalog); ok {
|
||||||
@@ -172,14 +172,14 @@ func (s *Server) handleAdminWorkflowRetire(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
workflowID, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
workflowID, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
version, _ := strconv.Atoi(r.PathValue("version"))
|
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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if dbCat, ok := s.pwSvc.Catalog().(*workflowdef.DBCatalog); ok {
|
if dbCat, ok := s.pwSvc.Catalog().(*workflowdef.DBCatalog); ok {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type projectAssetResponse struct {
|
|||||||
func (s *Server) handleUploadProjectAsset(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleUploadProjectAsset(w http.ResponseWriter, r *http.Request) {
|
||||||
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || projectID <= 0 {
|
if err != nil || projectID <= 0 {
|
||||||
http.Error(w, "invalid project id", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid project id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, ok := s.requireProjectAccess(w, r, projectID)
|
user, ok := s.requireProjectAccess(w, r, projectID)
|
||||||
@@ -32,26 +32,26 @@ func (s *Server) handleUploadProjectAsset(w http.ResponseWriter, r *http.Request
|
|||||||
userID := user.ID
|
userID := user.ID
|
||||||
if err := r.ParseMultipartForm(defaultUploadBodyLimit); err != nil {
|
if err := r.ParseMultipartForm(defaultUploadBodyLimit); err != nil {
|
||||||
if isRequestBodyTooLarge(err) {
|
if isRequestBodyTooLarge(err) {
|
||||||
http.Error(w, "upload too large", http.StatusRequestEntityTooLarge)
|
writeSafeError(w, r, http.StatusRequestEntityTooLarge, "upload too large")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, "invalid multipart form: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid multipart form: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file, header, err := r.FormFile("file")
|
file, header, err := r.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "file required", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "file required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
data, err := io.ReadAll(file)
|
data, err := io.ReadAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "read file failed", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "read file failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
http.Error(w, "cannot store empty file", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "cannot store empty file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,18 +62,18 @@ func (s *Server) handleUploadProjectAsset(w http.ResponseWriter, r *http.Request
|
|||||||
declaredMIME := header.Header.Get("Content-Type")
|
declaredMIME := header.Header.Get("Content-Type")
|
||||||
mimeType, verr := upload.ValidateProjectMaterial(filename, declaredMIME, data)
|
mimeType, verr := upload.ValidateProjectMaterial(filename, declaredMIME, data)
|
||||||
if verr != nil {
|
if verr != nil {
|
||||||
http.Error(w, "upload rejected: "+verr.Error(), http.StatusUnsupportedMediaType)
|
writeSafeError(w, r, http.StatusUnsupportedMediaType, "upload rejected: "+verr.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cas := s.cas
|
cas := s.cas
|
||||||
if cas == nil {
|
if cas == nil {
|
||||||
http.Error(w, "storage unavailable", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "storage unavailable")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ref, err := cas.StoreFile(r.Context(), projectID, userID, filepath.Base(filename), "material", data, mimeType)
|
ref, err := cas.StoreFile(r.Context(), projectID, userID, filepath.Base(filename), "material", data, mimeType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "store asset failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "store asset failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ func (s *Server) handleUploadProjectAsset(w http.ResponseWriter, r *http.Request
|
|||||||
func (s *Server) handleListProjectAssets(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleListProjectAssets(w http.ResponseWriter, r *http.Request) {
|
||||||
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || projectID <= 0 {
|
if err != nil || projectID <= 0 {
|
||||||
http.Error(w, "invalid project id", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid project id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := s.requireProjectAccess(w, r, projectID); !ok {
|
if _, ok := s.requireProjectAccess(w, r, projectID); !ok {
|
||||||
@@ -103,7 +103,7 @@ func (s *Server) handleListProjectAssets(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
refs, err := cas.ListProjectRefs(r.Context(), projectID)
|
refs, err := cas.ListProjectRefs(r.Context(), projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
out := make([]projectAssetResponse, 0, len(refs))
|
out := make([]projectAssetResponse, 0, len(refs))
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func (s *Server) handleAuthLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "无效的 JSON 请求体", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的 JSON 请求体")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,14 +51,14 @@ func (s *Server) handleAuthLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
errMsg := err.Error()
|
errMsg := err.Error()
|
||||||
if strings.Contains(errMsg, "审核") {
|
if strings.Contains(errMsg, "审核") {
|
||||||
http.Error(w, errMsg, http.StatusForbidden)
|
writeSafeError(w, r, http.StatusForbidden, errMsg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.Contains(errMsg, "停用") {
|
if strings.Contains(errMsg, "停用") {
|
||||||
http.Error(w, errMsg, http.StatusForbidden)
|
writeSafeError(w, r, http.StatusForbidden, errMsg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, errMsg, http.StatusUnauthorized)
|
writeSafeError(w, r, http.StatusUnauthorized, errMsg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,13 +70,13 @@ func (s *Server) handleAuthLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) handleAuthRegister(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAuthRegister(w http.ResponseWriter, r *http.Request) {
|
||||||
var req auth.RegisterRequest
|
var req auth.RegisterRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "无效的 JSON 请求体", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的 JSON 请求体")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := s.authSvc.Register(r.Context(), req)
|
resp, err := s.authSvc.Register(r.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ func (s *Server) handleAuthLogout(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) handleAuthChangePassword(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAuthChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||||
user, err := s.authenticateUser(r)
|
user, err := s.authenticateUser(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "未登录或登录已过期", http.StatusUnauthorized)
|
writeSafeError(w, r, http.StatusUnauthorized, "未登录或登录已过期")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,12 +107,12 @@ func (s *Server) handleAuthChangePassword(w http.ResponseWriter, r *http.Request
|
|||||||
NewPassword string `json:"new_password"`
|
NewPassword string `json:"new_password"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "无效的 JSON 请求体", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的 JSON 请求体")
|
||||||
return
|
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 {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ func (s *Server) handleAuthChangePassword(w http.ResponseWriter, r *http.Request
|
|||||||
func (s *Server) handleAuthMe(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAuthMe(w http.ResponseWriter, r *http.Request) {
|
||||||
user, err := s.authenticateUser(r)
|
user, err := s.authenticateUser(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "未登录或登录已过期", http.StatusUnauthorized)
|
writeSafeError(w, r, http.StatusUnauthorized, "未登录或登录已过期")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,14 +163,14 @@ func (s *Server) resolveUserCapabilities(ctx context.Context, user *auth.UserAcc
|
|||||||
func (s *Server) handleAuthSessions(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAuthSessions(w http.ResponseWriter, r *http.Request) {
|
||||||
user, err := s.authenticateUser(r)
|
user, err := s.authenticateUser(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "未登录或登录已过期", http.StatusUnauthorized)
|
writeSafeError(w, r, http.StatusUnauthorized, "未登录或登录已过期")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
token := extractBearerToken(r)
|
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 {
|
if listErr != nil {
|
||||||
http.Error(w, listErr.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, listErr.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if sessions == nil {
|
if sessions == nil {
|
||||||
@@ -179,26 +179,26 @@ func (s *Server) handleAuthSessions(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSONResponse(w, http.StatusOK, map[string]any{"sessions": sessions})
|
writeJSONResponse(w, http.StatusOK, map[string]any{"sessions": sessions})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, "不支持的请求方法", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "不支持的请求方法")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleAuthSessionByID(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAuthSessionByID(w http.ResponseWriter, r *http.Request) {
|
||||||
user, err := s.authenticateUser(r)
|
user, err := s.authenticateUser(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "未登录或登录已过期", http.StatusUnauthorized)
|
writeSafeError(w, r, http.StatusUnauthorized, "未登录或登录已过期")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.Method != http.MethodDelete {
|
if r.Method != http.MethodDelete {
|
||||||
http.Error(w, "不支持的请求方法", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "不支持的请求方法")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sessionID := strings.TrimSpace(r.PathValue("id"))
|
sessionID := strings.TrimSpace(r.PathValue("id"))
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
http.Error(w, "无效的会话 ID", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的会话 ID")
|
||||||
return
|
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 {
|
||||||
http.Error(w, "会话不存在或已失效", http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, "会话不存在或已失效")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, map[string]bool{"success": true})
|
writeJSONResponse(w, http.StatusOK, map[string]bool{"success": true})
|
||||||
@@ -213,7 +213,7 @@ func (s *Server) handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
users, err := s.authSvc.ListUsers(r.Context())
|
users, err := s.authSvc.ListUsers(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
@@ -224,12 +224,12 @@ func (s *Server) handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
var req auth.CreateUserRequest
|
var req auth.CreateUserRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "无效的 JSON 请求体", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的 JSON 请求体")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
created, err := s.authSvc.CreateUserByAdmin(r.Context(), req)
|
created, err := s.authSvc.CreateUserByAdmin(r.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
@@ -237,7 +237,7 @@ func (s *Server) handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Error(w, "不支持的请求方法", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "不支持的请求方法")
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAdminUpdateRole 管理员为用户指定职位与角色
|
// handleAdminUpdateRole 管理员为用户指定职位与角色
|
||||||
@@ -248,7 +248,7 @@ func (s *Server) handleAdminUpdateRole(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
targetID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
targetID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || targetID <= 0 {
|
if err != nil || targetID <= 0 {
|
||||||
http.Error(w, "无效的用户ID", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的用户ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,13 +257,13 @@ func (s *Server) handleAdminUpdateRole(w http.ResponseWriter, r *http.Request) {
|
|||||||
Position string `json:"position"`
|
Position string `json:"position"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "无效的请求体", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的请求体")
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +279,7 @@ func (s *Server) handleAdminUpdateStatus(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
targetID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
targetID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || targetID <= 0 {
|
if err != nil || targetID <= 0 {
|
||||||
http.Error(w, "无效的用户ID", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的用户ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,20 +289,20 @@ func (s *Server) handleAdminUpdateStatus(w http.ResponseWriter, r *http.Request)
|
|||||||
Position string `json:"position,omitempty"`
|
Position string `json:"position,omitempty"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "无效的请求体", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的请求体")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.RoleID != "" {
|
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 {
|
||||||
http.Error(w, roleErr.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, roleErr.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updated, err := s.authSvc.UpdateUserStatus(r.Context(), targetID, req.Status)
|
updated, err := s.authSvc.UpdateUserStatus(r.Context(), targetID, req.Status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,12 +313,12 @@ func (s *Server) handleAdminUpdateStatus(w http.ResponseWriter, r *http.Request)
|
|||||||
// handleAuthPublicConfig 返回登录/注册页所需的公开准入配置(无需鉴权)
|
// handleAuthPublicConfig 返回登录/注册页所需的公开准入配置(无需鉴权)
|
||||||
func (s *Server) handleAuthPublicConfig(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAuthPublicConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
http.Error(w, "不支持的方法", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "不支持的方法")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cfg, err := s.authSvc.GetConfig(r.Context())
|
cfg, err := s.authSvc.GetConfig(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
@@ -334,7 +334,7 @@ func (s *Server) handleAdminConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
cfg, err := s.authSvc.GetConfig(r.Context())
|
cfg, err := s.authSvc.GetConfig(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
@@ -345,11 +345,11 @@ func (s *Server) handleAdminConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
if r.Method == http.MethodPut || r.Method == http.MethodPost {
|
if r.Method == http.MethodPut || r.Method == http.MethodPost {
|
||||||
var req auth.SystemAuthConfig
|
var req auth.SystemAuthConfig
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "无效的请求体", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "无效的请求体")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := s.authSvc.UpdateConfig(r.Context(), req); err != nil {
|
if err := s.authSvc.UpdateConfig(r.Context(), req); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
@@ -357,5 +357,5 @@ func (s *Server) handleAdminConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Error(w, "不支持的方法", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "不支持的方法")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func writeAuthzError(w http.ResponseWriter, err error) {
|
|||||||
"message": "请先登录",
|
"message": "请先登录",
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, nil, http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ func (s *Server) requireAuthenticatedUser(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
user, err := s.authenticateUser(r)
|
user, err := s.authenticateUser(r)
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
http.Error(w, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`, http.StatusUnauthorized)
|
writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`)
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return user, true
|
return user, true
|
||||||
@@ -77,7 +77,7 @@ func (s *Server) requireAuthenticatedUser(w http.ResponseWriter, r *http.Request
|
|||||||
// assertSessionOwnedByUser 校验会话归属当前用户;会话不存在或归属他人返回 false。
|
// assertSessionOwnedByUser 校验会话归属当前用户;会话不存在或归属他人返回 false。
|
||||||
func (s *Server) assertSessionOwnedByUser(w http.ResponseWriter, r *http.Request, sessionID string, userID int64) bool {
|
func (s *Server) assertSessionOwnedByUser(w http.ResponseWriter, r *http.Request, sessionID string, userID int64) bool {
|
||||||
if s.ctxMgr == nil {
|
if s.ctxMgr == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
ownerID, err := s.ctxMgr.GetSessionOwnerID(r.Context(), sessionID)
|
ownerID, err := s.ctxMgr.GetSessionOwnerID(r.Context(), sessionID)
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import (
|
|||||||
// GET /api/v1/chat/digests?session_id=&from=&to=
|
// GET /api/v1/chat/digests?session_id=&from=&to=
|
||||||
func (s *Server) handleListDailyDigests(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleListDailyDigests(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "Method Not Allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.chatLog == nil {
|
if s.chatLog == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ func (s *Server) handleListDailyDigests(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
sessionID := strings.TrimSpace(r.URL.Query().Get("session_id"))
|
sessionID := strings.TrimSpace(r.URL.Query().Get("session_id"))
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
http.Error(w, "missing required query param 'session_id'", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing required query param 'session_id'")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,12 +60,12 @@ type GenerateDigestRequest struct {
|
|||||||
// POST /api/v1/chat/digests/generate
|
// POST /api/v1/chat/digests/generate
|
||||||
func (s *Server) handleGenerateDailyDigest(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGenerateDailyDigest(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "Method Not Allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.chatLog == nil {
|
if s.chatLog == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ func (s *Server) handleGenerateDailyDigest(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
var req GenerateDigestRequest
|
var req GenerateDigestRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request json body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request json body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ func (s *Server) handleGenerateDailyDigest(w http.ResponseWriter, r *http.Reques
|
|||||||
req.DayKey = strings.TrimSpace(req.DayKey)
|
req.DayKey = strings.TrimSpace(req.DayKey)
|
||||||
|
|
||||||
if req.SessionID == "" || req.DayKey == "" {
|
if req.SessionID == "" || req.DayKey == "" {
|
||||||
http.Error(w, "missing required fields 'session_id' or 'day_key'", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing required fields 'session_id' or 'day_key'")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
|
|
||||||
func (s *Server) handleChatExportMessages(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleChatExportMessages(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "Method Not Allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,12 +28,12 @@ func (s *Server) handleChatExportMessages(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
sessionID := strings.TrimSpace(r.URL.Query().Get("session_id"))
|
sessionID := strings.TrimSpace(r.URL.Query().Get("session_id"))
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
http.Error(w, "missing required query param 'session_id'", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing required query param 'session_id'")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.chatLog == nil {
|
if s.chatLog == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func (s *Server) authorizeSessionRead(
|
|||||||
user *auth.UserAccount,
|
user *auth.UserAccount,
|
||||||
) (*chatHistoryAuth, bool) {
|
) (*chatHistoryAuth, bool) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
http.Error(w, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`, http.StatusUnauthorized)
|
writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`)
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
if !s.assertSessionOwnedByUser(w, r, sessionID, user.ID) {
|
if !s.assertSessionOwnedByUser(w, r, sessionID, user.ID) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type recategorizeReq struct {
|
|||||||
|
|
||||||
func (s *Server) handleChatRecategorize(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleChatRecategorize(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.ctxMgr == nil {
|
if s.ctxMgr == nil {
|
||||||
http.Error(w, "context manager unavailable", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager unavailable")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
// GET /api/v1/chat/search?session_id=&q=&mode=&category=&date_from=&date_to=&limit=&user_id=
|
// GET /api/v1/chat/search?session_id=&q=&mode=&category=&date_from=&date_to=&limit=&user_id=
|
||||||
func (s *Server) handleChatSearch(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleChatSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "Method Not Allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,19 +24,19 @@ func (s *Server) handleChatSearch(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.chatLog == nil {
|
if s.chatLog == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID := strings.TrimSpace(r.URL.Query().Get("session_id"))
|
sessionID := strings.TrimSpace(r.URL.Query().Get("session_id"))
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
http.Error(w, "missing required query param 'session_id'", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing required query param 'session_id'")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
query := strings.TrimSpace(r.URL.Query().Get("q"))
|
query := strings.TrimSpace(r.URL.Query().Get("q"))
|
||||||
if query == "" {
|
if query == "" {
|
||||||
http.Error(w, "missing or empty query param 'q'", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing or empty query param 'q'")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,23 +13,23 @@ func (s *Server) handleGetChatTurn(w http.ResponseWriter, r *http.Request) {
|
|||||||
clientTurnID := r.PathValue("client_turn_id")
|
clientTurnID := r.PathValue("client_turn_id")
|
||||||
sessionID := r.URL.Query().Get("session_id")
|
sessionID := r.URL.Query().Get("session_id")
|
||||||
if clientTurnID == "" || sessionID == "" {
|
if clientTurnID == "" || sessionID == "" {
|
||||||
http.Error(w, "client_turn_id and session_id required", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "client_turn_id and session_id required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctxMgr := s.ctxMgr
|
ctxMgr := s.ctxMgr
|
||||||
if ctxMgr == nil {
|
if ctxMgr == nil {
|
||||||
http.Error(w, "context manager unavailable", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager unavailable")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rec, err := ctxMgr.GetChatTurn(r.Context(), sessionID, clientTurnID)
|
rec, err := ctxMgr.GetChatTurn(r.Context(), sessionID, clientTurnID)
|
||||||
if errors.Is(err, ctxmgr.ErrChatTurnNotFound) {
|
if errors.Is(err, ctxmgr.ErrChatTurnNotFound) {
|
||||||
http.Error(w, "turn not found", http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, "turn not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func (s *Server) handleChatUnread(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
sessionID := r.URL.Query().Get("session_id")
|
sessionID := r.URL.Query().Get("session_id")
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
http.Error(w, "missing session_id", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing session_id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ func (s *Server) handleChatUnread(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (s *Server) handleChatRead(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleChatRead(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,16 +76,16 @@ func (s *Server) handleChatRead(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req ctxmgr.MarkChatReadRequest
|
var req ctxmgr.MarkChatReadRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.SessionID == "" {
|
if req.SessionID == "" {
|
||||||
http.Error(w, "missing session_id", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing session_id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.chatLog == nil {
|
if s.chatLog == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
// GET /api/v1/chat/messages/window?session_id=&days=&before_day=
|
// GET /api/v1/chat/messages/window?session_id=&days=&before_day=
|
||||||
func (s *Server) handleGetChatWindow(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetChatWindow(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.chatLog == nil {
|
if s.chatLog == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ func (s *Server) handleGetChatWindow(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
sessionID := r.URL.Query().Get("session_id")
|
sessionID := r.URL.Query().Get("session_id")
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
http.Error(w, "missing session_id query param", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing session_id query param")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ func (s *Server) handleGetChatWindow(w http.ResponseWriter, r *http.Request) {
|
|||||||
// GET /api/v1/chat/messages/day?session_id=&date=&anchor_id=&before_id=&after_id=&limit=
|
// GET /api/v1/chat/messages/day?session_id=&date=&anchor_id=&before_id=&after_id=&limit=
|
||||||
func (s *Server) handleGetChatDayMessages(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetChatDayMessages(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.chatLog == nil {
|
if s.chatLog == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ func (s *Server) handleGetChatDayMessages(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
sessionID := r.URL.Query().Get("session_id")
|
sessionID := r.URL.Query().Get("session_id")
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
http.Error(w, "missing session_id query param", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing session_id query param")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ func (s *Server) handleGetChatDayMessages(w http.ResponseWriter, r *http.Request
|
|||||||
anchorID, _ := strconv.ParseInt(anchorIDStr, 10, 64)
|
anchorID, _ := strconv.ParseInt(anchorIDStr, 10, 64)
|
||||||
|
|
||||||
if date == "" && anchorID <= 0 {
|
if date == "" && anchorID <= 0 {
|
||||||
http.Error(w, "missing date/day_key or anchor_id query param", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing date/day_key or anchor_id query param")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ func (s *Server) handleGetChatDayMessages(w http.ResponseWriter, r *http.Request
|
|||||||
// GET /api/v1/chat/dates?session_id=
|
// GET /api/v1/chat/dates?session_id=
|
||||||
func (s *Server) handleListChatDates(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleListChatDates(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.chatLog == nil {
|
if s.chatLog == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ func (s *Server) handleListChatDates(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
sessionID := r.URL.Query().Get("session_id")
|
sessionID := r.URL.Query().Get("session_id")
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
http.Error(w, "missing session_id query param", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing session_id query param")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ func (s *Server) handleListChatDates(w http.ResponseWriter, r *http.Request) {
|
|||||||
// GET /api/v1/chat/categories?session_id=
|
// GET /api/v1/chat/categories?session_id=
|
||||||
func (s *Server) handleListChatCategories(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleListChatCategories(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.chatLog == nil {
|
if s.chatLog == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ func (s *Server) handleListChatCategories(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
sessionID := r.URL.Query().Get("session_id")
|
sessionID := r.URL.Query().Get("session_id")
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
http.Error(w, "missing session_id query param", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing session_id query param")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,14 +163,14 @@ func (s *Server) handleListChatCategories(w http.ResponseWriter, r *http.Request
|
|||||||
// POST /api/v1/chat/messages/import
|
// POST /api/v1/chat/messages/import
|
||||||
func (s *Server) handleImportChatMessages(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleImportChatMessages(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.ctxMgr == nil {
|
if s.ctxMgr == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 安全门禁:生产环境默认禁止调用,仅在 dev 或显式开启时开放
|
// 安全门禁:生产环境默认禁止调用,仅在 dev 或显式开启时开放
|
||||||
isDev := s.cfg.Env == "dev" || s.cfg.Env == "development" || s.cfg.Env == "" || os.Getenv("ENABLE_DEV_IMPORT") == "true"
|
isDev := s.cfg.Env == "dev" || s.cfg.Env == "development" || s.cfg.Env == "" || os.Getenv("ENABLE_DEV_IMPORT") == "true"
|
||||||
if !isDev {
|
if !isDev {
|
||||||
http.Error(w, "chat messages import endpoint is only available in dev/demo environment", http.StatusForbidden)
|
writeSafeError(w, r, http.StatusForbidden, "chat messages import endpoint is only available in dev/demo environment")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ func (s *Server) handleImportChatMessages(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
var items []importMessageItem
|
var items []importMessageItem
|
||||||
if err := json.NewDecoder(r.Body).Decode(&items); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&items); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func (s *Server) chatPrincipal(user *auth.UserAccount, action chatflow.Action) c
|
|||||||
func (s *Server) writeChatLogError(w http.ResponseWriter, err error) {
|
func (s *Server) writeChatLogError(w http.ResponseWriter, err error) {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, chatflow.ErrUnauthenticated):
|
case errors.Is(err, chatflow.ErrUnauthenticated):
|
||||||
http.Error(w, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`, http.StatusUnauthorized)
|
writeSafeError(w, nil, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`)
|
||||||
case errors.Is(err, chatflow.ErrForbidden):
|
case errors.Is(err, chatflow.ErrForbidden):
|
||||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||||
"error_code": "SESSION_FORBIDDEN",
|
"error_code": "SESSION_FORBIDDEN",
|
||||||
@@ -30,8 +30,8 @@ func (s *Server) writeChatLogError(w http.ResponseWriter, err error) {
|
|||||||
"message": "指定会话不存在或已被删除",
|
"message": "指定会话不存在或已被删除",
|
||||||
})
|
})
|
||||||
case errors.Is(err, chatflow.ErrClosed):
|
case errors.Is(err, chatflow.ErrClosed):
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, nil, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
default:
|
default:
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, nil, http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func extractDocIDFromPath(r *http.Request, actionSuffix string) string {
|
|||||||
// POST /api/v1/freeform-documents — 直接创建(上传附件注册服务端事实源,仅 my_private)
|
// POST /api/v1/freeform-documents — 直接创建(上传附件注册服务端事实源,仅 my_private)
|
||||||
func (s *Server) handleCreateFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleCreateFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.freeDoc == nil {
|
if s.freeDoc == nil {
|
||||||
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, ok := s.requirePrincipal(w, r)
|
user, ok := s.requirePrincipal(w, r)
|
||||||
@@ -107,7 +107,7 @@ func (s *Server) handleCreateFreeformDoc(w http.ResponseWriter, r *http.Request)
|
|||||||
// POST /api/v1/freeform-documents/from-message
|
// POST /api/v1/freeform-documents/from-message
|
||||||
func (s *Server) handleCreateFreeformDocFromMessage(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleCreateFreeformDocFromMessage(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.freeDoc == nil {
|
if s.freeDoc == nil {
|
||||||
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ func (s *Server) handleCreateFreeformDocFromMessage(w http.ResponseWriter, r *ht
|
|||||||
// GET /api/v1/freeform-documents
|
// GET /api/v1/freeform-documents
|
||||||
func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.freeDoc == nil {
|
if s.freeDoc == nil {
|
||||||
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request)
|
|||||||
// GET /api/v1/freeform-documents/{doc_id}
|
// GET /api/v1/freeform-documents/{doc_id}
|
||||||
func (s *Server) handleGetFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.freeDoc == nil {
|
if s.freeDoc == nil {
|
||||||
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +256,7 @@ func (s *Server) handleGetFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
|||||||
// PUT /api/v1/freeform-documents/{doc_id}
|
// PUT /api/v1/freeform-documents/{doc_id}
|
||||||
func (s *Server) handleUpdateFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleUpdateFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.freeDoc == nil {
|
if s.freeDoc == nil {
|
||||||
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ func (s *Server) handleUpdateFreeformDoc(w http.ResponseWriter, r *http.Request)
|
|||||||
// POST /api/v1/freeform-documents/{doc_id}/share
|
// POST /api/v1/freeform-documents/{doc_id}/share
|
||||||
func (s *Server) handleShareFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleShareFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.freeDoc == nil {
|
if s.freeDoc == nil {
|
||||||
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +373,7 @@ func (s *Server) handleShareFreeformDoc(w http.ResponseWriter, r *http.Request)
|
|||||||
// POST /api/v1/freeform-documents/{doc_id}/unshare
|
// POST /api/v1/freeform-documents/{doc_id}/unshare
|
||||||
func (s *Server) handleUnshareFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleUnshareFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.freeDoc == nil {
|
if s.freeDoc == nil {
|
||||||
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +414,7 @@ func (s *Server) handleUnshareFreeformDoc(w http.ResponseWriter, r *http.Request
|
|||||||
// DELETE /api/v1/freeform-documents/{doc_id}
|
// DELETE /api/v1/freeform-documents/{doc_id}
|
||||||
func (s *Server) handleDeleteFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleDeleteFreeformDoc(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.freeDoc == nil {
|
if s.freeDoc == nil {
|
||||||
http.Error(w, "freeform doc service not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "freeform doc service not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"chat-agent-system/internal/bizdev"
|
"chat-agent-system/internal/bizdev"
|
||||||
"chat-agent-system/internal/config"
|
"chat-agent-system/internal/config"
|
||||||
"chat-agent-system/internal/server"
|
"chat-agent-system/internal/server"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupTestServerForFreeformDoc(t *testing.T) (*server.Server, http.Handler, int64, int64) {
|
func setupTestServerForFreeformDoc(t *testing.T) (*server.Server, http.Handler, int64, int64) {
|
||||||
@@ -66,8 +66,8 @@ func TestFreeformDocAPI_EndToEnd(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusCreated, recValid.Code)
|
require.Equal(t, http.StatusCreated, recValid.Code)
|
||||||
|
|
||||||
var createResp struct {
|
var createResp struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
IsExisting bool `json:"is_existing"`
|
IsExisting bool `json:"is_existing"`
|
||||||
Data struct {
|
Data struct {
|
||||||
Document struct {
|
Document struct {
|
||||||
DocID string `json:"doc_id"`
|
DocID string `json:"doc_id"`
|
||||||
|
|||||||
+78
-73
@@ -16,6 +16,7 @@ import (
|
|||||||
"chat-agent-system/internal/bizdev"
|
"chat-agent-system/internal/bizdev"
|
||||||
"chat-agent-system/internal/ctxmgr"
|
"chat-agent-system/internal/ctxmgr"
|
||||||
"chat-agent-system/internal/docgov"
|
"chat-agent-system/internal/docgov"
|
||||||
|
"chat-agent-system/internal/httpx"
|
||||||
"chat-agent-system/internal/projectinstance"
|
"chat-agent-system/internal/projectinstance"
|
||||||
"chat-agent-system/internal/projectworkflow"
|
"chat-agent-system/internal/projectworkflow"
|
||||||
"chat-agent-system/internal/squad"
|
"chat-agent-system/internal/squad"
|
||||||
@@ -132,13 +133,13 @@ func (s *Server) buildHealthResponse(strict bool) (HealthResponse, int) {
|
|||||||
func (s *Server) handleGetProject(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetProject(w http.ResponseWriter, r *http.Request) {
|
||||||
sessionID := r.URL.Query().Get("session_id")
|
sessionID := r.URL.Query().Get("session_id")
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
http.Error(w, "missing session_id query param", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing session_id query param")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sess := s.getOrCreateSession(sessionID, "", "", "", "", "", "", "", "")
|
sess := s.getOrCreateSession(sessionID, "", "", "", "", "", "", "", "")
|
||||||
if sess == nil || sess.UserCtx == nil {
|
if sess == nil || sess.UserCtx == nil {
|
||||||
http.Error(w, "session or project context not found", http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, "session or project context not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,11 +154,11 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req ChatRequest
|
var req ChatRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Message == "" {
|
if req.Message == "" {
|
||||||
http.Error(w, "message cannot be empty", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "message cannot be empty")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.WorkDir = ""
|
req.WorkDir = ""
|
||||||
@@ -192,7 +193,7 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
resp, err := sess.SendMessage(r.Context(), req.Message)
|
resp, err := sess.SendMessage(r.Context(), req.Message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "process chat message failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "process chat message failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +208,7 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req TaskRunRequestDTO
|
var req TaskRunRequestDTO
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.UserID = user.ID
|
req.UserID = user.ID
|
||||||
@@ -362,7 +363,7 @@ func writeTaskError(w http.ResponseWriter, err error, payload any) {
|
|||||||
writeJSONResponse(w, status, payload)
|
writeJSONResponse(w, status, payload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, err.Error(), status)
|
writeSafeError(w, nil, status, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeDocGovError(w http.ResponseWriter, err error) {
|
func writeDocGovError(w http.ResponseWriter, err error) {
|
||||||
@@ -375,22 +376,22 @@ func writeDocGovError(w http.ResponseWriter, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, docgov.ErrDocumentBusy) {
|
if errors.Is(err, docgov.ErrDocumentBusy) {
|
||||||
http.Error(w, err.Error(), http.StatusConflict)
|
writeSafeError(w, nil, http.StatusConflict, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, docgov.ErrDocumentLocked) {
|
if errors.Is(err, docgov.ErrDocumentLocked) {
|
||||||
http.Error(w, err.Error(), http.StatusConflict)
|
writeSafeError(w, nil, http.StatusConflict, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, docgov.ErrDocumentNotFound) {
|
if errors.Is(err, docgov.ErrDocumentNotFound) {
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
writeSafeError(w, nil, http.StatusNotFound, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, docgov.ErrLeaseNotHeld) || errors.Is(err, docgov.ErrLeaseExpired) {
|
if errors.Is(err, docgov.ErrLeaseNotHeld) || errors.Is(err, docgov.ErrLeaseExpired) {
|
||||||
http.Error(w, err.Error(), http.StatusConflict)
|
writeSafeError(w, nil, http.StatusConflict, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
writeSafeError(w, nil, http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeJSONResponse(w http.ResponseWriter, status int, data any) {
|
func writeJSONResponse(w http.ResponseWriter, status int, data any) {
|
||||||
@@ -399,6 +400,10 @@ func writeJSONResponse(w http.ResponseWriter, status int, data any) {
|
|||||||
_ = json.NewEncoder(w).Encode(data)
|
_ = json.NewEncoder(w).Encode(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeSafeError(w http.ResponseWriter, r *http.Request, status int, legacy string) {
|
||||||
|
httpx.Write(w, status, httpx.CodeFromStatus(status), legacy, httpx.RequestID(r), nil)
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// 多级上下文与偏好记忆 RESTful 控制器
|
// 多级上下文与偏好记忆 RESTful 控制器
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -416,13 +421,13 @@ func (s *Server) handleGetUserContext(w http.ResponseWriter, r *http.Request) {
|
|||||||
projectID, _ := strconv.ParseInt(projectIDStr, 10, 64)
|
projectID, _ := strconv.ParseInt(projectIDStr, 10, 64)
|
||||||
|
|
||||||
if s.ctxMgr == nil {
|
if s.ctxMgr == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uCtx, err := s.ctxMgr.GetUserContext(r.Context(), projectID, userID, sessionID)
|
uCtx, err := s.ctxMgr.GetUserContext(r.Context(), projectID, userID, sessionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "get user context failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "get user context failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,16 +447,16 @@ type UpdateUserPreferenceRequest struct {
|
|||||||
func (s *Server) handleUpdateUserPreference(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleUpdateUserPreference(w http.ResponseWriter, r *http.Request) {
|
||||||
var req UpdateUserPreferenceRequest
|
var req UpdateUserPreferenceRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.UserID <= 0 {
|
if req.UserID <= 0 {
|
||||||
http.Error(w, "user_id must be positive", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "user_id must be positive")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.ctxMgr == nil {
|
if s.ctxMgr == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,7 +469,7 @@ func (s *Server) handleUpdateUserPreference(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.ctxMgr.UpdateUserPreference(r.Context(), pref); err != nil {
|
if err := s.ctxMgr.UpdateUserPreference(r.Context(), pref); err != nil {
|
||||||
http.Error(w, "save user preference failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "save user preference failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +486,7 @@ func (s *Server) handleGetProjectContext(w http.ResponseWriter, r *http.Request)
|
|||||||
projectIDStr := r.URL.Query().Get("project_id")
|
projectIDStr := r.URL.Query().Get("project_id")
|
||||||
projectID, _ := strconv.ParseInt(projectIDStr, 10, 64)
|
projectID, _ := strconv.ParseInt(projectIDStr, 10, 64)
|
||||||
if projectID <= 0 {
|
if projectID <= 0 {
|
||||||
http.Error(w, "missing or invalid project_id query param", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing or invalid project_id query param")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,13 +496,13 @@ func (s *Server) handleGetProjectContext(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.ctxMgr == nil {
|
if s.ctxMgr == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pCtx, err := s.ctxMgr.GetProjectContext(r.Context(), projectID)
|
pCtx, err := s.ctxMgr.GetProjectContext(r.Context(), projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "get project context failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "get project context failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,18 +517,18 @@ func (s *Server) handleGetProjectContext(w http.ResponseWriter, r *http.Request)
|
|||||||
func (s *Server) handleDocGovGetDocument(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleDocGovGetDocument(w http.ResponseWriter, r *http.Request) {
|
||||||
docID := r.URL.Query().Get("doc_id")
|
docID := r.URL.Query().Get("doc_id")
|
||||||
if docID == "" {
|
if docID == "" {
|
||||||
http.Error(w, "missing doc_id query param", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing doc_id query param")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := s.gov.GetDocument(r.Context(), docID)
|
doc, err := s.gov.GetDocument(r.Context(), docID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "get document failed: "+err.Error(), http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, "get document failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,22 +552,22 @@ func (s *Server) handleDocGovClaim(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req DocGovClaimRequest
|
var req DocGovClaimRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.DocID == "" {
|
if req.DocID == "" {
|
||||||
http.Error(w, "doc_id cannot be empty", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "doc_id cannot be empty")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := s.gov.RecordDocumentViewed(r.Context(), req.DocID, user.ID, docgov.UserRole(req.UserRole), user.Name)
|
doc, err := s.gov.RecordDocumentViewed(r.Context(), req.DocID, user.ID, docgov.UserRole(req.UserRole), user.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "claim document failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "claim document failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,12 +590,12 @@ func (s *Server) handleDocGovSubmitReview(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
var req DocGovSubmitReviewRequest
|
var req DocGovSubmitReviewRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !s.enforceDocGovWorkflowAction(w, r.Context(), user.ID, req.DocID, projectinstance.DocAccessActionSubmit) {
|
if !s.enforceDocGovWorkflowAction(w, r.Context(), user.ID, req.DocID, projectinstance.DocAccessActionSubmit) {
|
||||||
@@ -599,7 +604,7 @@ func (s *Server) handleDocGovSubmitReview(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
doc, err := s.gov.SubmitForReview(r.Context(), req.DocID, user.ID, docgov.RoleAuthor, user.Name)
|
doc, err := s.gov.SubmitForReview(r.Context(), req.DocID, user.ID, docgov.RoleAuthor, user.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "submit review failed: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "submit review failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,12 +631,12 @@ func (s *Server) handleDocGovReviewDecision(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
var req DocGovDecisionRequest
|
var req DocGovDecisionRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !s.enforceDocGovWorkflowAction(w, r.Context(), user.ID, req.DocID, projectinstance.DocAccessActionReview) {
|
if !s.enforceDocGovWorkflowAction(w, r.Context(), user.ID, req.DocID, projectinstance.DocAccessActionReview) {
|
||||||
@@ -651,12 +656,12 @@ func (s *Server) handleDocGovReviewDecision(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
doc, err = s.gov.ReviewReject(r.Context(), req.DocID, user.ID, docgov.RoleReviewer, user.Name, reason)
|
doc, err = s.gov.ReviewReject(r.Context(), req.DocID, user.ID, docgov.RoleReviewer, user.Name, reason)
|
||||||
} else {
|
} else {
|
||||||
http.Error(w, "invalid action: must be approve or reject", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid action: must be approve or reject")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "review decision failed: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "review decision failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,12 +677,12 @@ func (s *Server) handleDocGovApproverDecision(w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
var req DocGovDecisionRequest
|
var req DocGovDecisionRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !s.enforceDocGovWorkflowAction(w, r.Context(), user.ID, req.DocID, projectinstance.DocAccessActionApprove) {
|
if !s.enforceDocGovWorkflowAction(w, r.Context(), user.ID, req.DocID, projectinstance.DocAccessActionApprove) {
|
||||||
@@ -687,7 +692,7 @@ func (s *Server) handleDocGovApproverDecision(w http.ResponseWriter, r *http.Req
|
|||||||
action := strings.ToLower(strings.TrimSpace(req.Action))
|
action := strings.ToLower(strings.TrimSpace(req.Action))
|
||||||
cmds := s.docflowCommands()
|
cmds := s.docflowCommands()
|
||||||
if cmds == nil || cmds.Gov == nil {
|
if cmds == nil || cmds.Gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p := docflow.Principal{UserID: user.ID, Name: user.Name, Role: docgov.RoleApprover}
|
p := docflow.Principal{UserID: user.ID, Name: user.Name, Role: docgov.RoleApprover}
|
||||||
@@ -705,20 +710,20 @@ func (s *Server) handleDocGovApproverDecision(w http.ResponseWriter, r *http.Req
|
|||||||
DocID: req.DocID, RejectReason: reason, RollbackTo: docgov.DocumentState(req.RollbackTo),
|
DocID: req.DocID, RejectReason: reason, RollbackTo: docgov.DocumentState(req.RollbackTo),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
http.Error(w, "invalid action: must be approve or reject", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid action: must be approve or reject")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, uow.ErrPermissionDenied) {
|
if errors.Is(err, uow.ErrPermissionDenied) {
|
||||||
http.Error(w, "approver decision failed: "+err.Error(), http.StatusForbidden)
|
writeSafeError(w, r, http.StatusForbidden, "approver decision failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, uow.ErrBusy) {
|
if errors.Is(err, uow.ErrBusy) {
|
||||||
http.Error(w, "approver decision failed: "+err.Error(), http.StatusConflict)
|
writeSafeError(w, r, http.StatusConflict, "approver decision failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, "approver decision failed: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "approver decision failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,18 +747,18 @@ func (s *Server) handleDocGovDecideProposal(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
var req DecideProposalRequest
|
var req DecideProposalRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
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 err != nil {
|
||||||
http.Error(w, "decide proposal failed: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "decide proposal failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,18 +769,18 @@ func (s *Server) handleDocGovDecideProposal(w http.ResponseWriter, r *http.Reque
|
|||||||
func (s *Server) handleDocGovListComments(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleDocGovListComments(w http.ResponseWriter, r *http.Request) {
|
||||||
docID := r.URL.Query().Get("doc_id")
|
docID := r.URL.Query().Get("doc_id")
|
||||||
if docID == "" {
|
if docID == "" {
|
||||||
http.Error(w, "missing doc_id query param", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing doc_id query param")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
comments, err := s.gov.ListComments(r.Context(), docID)
|
comments, err := s.gov.ListComments(r.Context(), docID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "list comments failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "list comments failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,12 +802,12 @@ type AddCommentRequestDTO struct {
|
|||||||
func (s *Server) handleDocGovAddComment(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleDocGovAddComment(w http.ResponseWriter, r *http.Request) {
|
||||||
var req AddCommentRequestDTO
|
var req AddCommentRequestDTO
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -827,18 +832,18 @@ func (s *Server) handleDocGovAddComment(w http.ResponseWriter, r *http.Request)
|
|||||||
func (s *Server) handleDocGovListAuditLogs(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleDocGovListAuditLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
docID := r.URL.Query().Get("doc_id")
|
docID := r.URL.Query().Get("doc_id")
|
||||||
if docID == "" {
|
if docID == "" {
|
||||||
http.Error(w, "missing doc_id query param", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing doc_id query param")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logs, err := s.gov.ListAuditLogs(r.Context(), docID)
|
logs, err := s.gov.ListAuditLogs(r.Context(), docID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "list audit logs failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "list audit logs failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,7 +857,7 @@ func (s *Server) handleDocGovListAuditLogs(w http.ResponseWriter, r *http.Reques
|
|||||||
// handleListChatSessions 分页查询用户/项目关联的历史会话列表
|
// handleListChatSessions 分页查询用户/项目关联的历史会话列表
|
||||||
func (s *Server) handleListChatSessions(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleListChatSessions(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.ctxMgr == nil {
|
if s.ctxMgr == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,7 +884,7 @@ func (s *Server) handleListChatSessions(w http.ResponseWriter, r *http.Request)
|
|||||||
Offset: offset,
|
Offset: offset,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "list chat sessions failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "list chat sessions failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,23 +901,23 @@ type upsertChatSessionRequest struct {
|
|||||||
// handleUpsertChatSession 注册或更新会话元数据(立项/成员加入时同步)。
|
// handleUpsertChatSession 注册或更新会话元数据(立项/成员加入时同步)。
|
||||||
func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.ctxMgr == nil {
|
if s.ctxMgr == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req upsertChatSessionRequest
|
var req upsertChatSessionRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.SessionID == "" {
|
if req.SessionID == "" {
|
||||||
http.Error(w, "missing session_id", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing session_id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err := s.ctxMgr.EnsureChatSession(r.Context(), req.SessionID, req.UserID, req.ProjectID, req.Title)
|
summary, err := s.ctxMgr.EnsureChatSession(r.Context(), req.SessionID, req.UserID, req.ProjectID, req.Title)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "upsert chat session failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "upsert chat session failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -922,13 +927,13 @@ func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request)
|
|||||||
// handleGetChatMessages 分页获取指定会话的历史多轮消息记录
|
// handleGetChatMessages 分页获取指定会话的历史多轮消息记录
|
||||||
func (s *Server) handleGetChatMessages(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetChatMessages(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.ctxMgr == nil {
|
if s.ctxMgr == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID := r.URL.Query().Get("session_id")
|
sessionID := r.URL.Query().Get("session_id")
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
http.Error(w, "missing session_id query param", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing session_id query param")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -949,7 +954,7 @@ func (s *Server) handleGetChatMessages(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.chatLog == nil {
|
if s.chatLog == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -973,14 +978,14 @@ func (s *Server) handleRecommendCases(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.ctxMgr == nil {
|
if s.ctxMgr == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
projectID, _ := strconv.ParseInt(r.URL.Query().Get("project_id"), 10, 64)
|
projectID, _ := strconv.ParseInt(r.URL.Query().Get("project_id"), 10, 64)
|
||||||
query := r.URL.Query().Get("query")
|
query := r.URL.Query().Get("query")
|
||||||
if query == "" {
|
if query == "" {
|
||||||
http.Error(w, "missing query param", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "missing query param")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1006,7 +1011,7 @@ func (s *Server) handleRecommendCases(w http.ResponseWriter, r *http.Request) {
|
|||||||
MinScore: minScore,
|
MinScore: minScore,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "search similar cases failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "search similar cases failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1040,11 +1045,11 @@ func (s *Server) handleDocGovSaveContent(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
var req SaveContentRequestDTO
|
var req SaveContentRequestDTO
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
role := docgov.RoleAuthor
|
role := docgov.RoleAuthor
|
||||||
@@ -1076,11 +1081,11 @@ func (s *Server) handleDocGovAcquireLease(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
var req LeaseRequestDTO
|
var req LeaseRequestDTO
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
workDir, werr := s.resolver.ResolveDocWorkDir(r.Context(), req.DocID)
|
workDir, werr := s.resolver.ResolveDocWorkDir(r.Context(), req.DocID)
|
||||||
@@ -1108,11 +1113,11 @@ func (s *Server) handleDocGovHeartbeatLease(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
var req LeaseRequestDTO
|
var req LeaseRequestDTO
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ttl := time.Duration(req.TTLSeconds) * time.Second
|
ttl := time.Duration(req.TTLSeconds) * time.Second
|
||||||
@@ -1130,11 +1135,11 @@ func (s *Server) handleDocGovReleaseLease(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
var req LeaseRequestDTO
|
var req LeaseRequestDTO
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
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)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (s *Server) handleMeProfile(w http.ResponseWriter, r *http.Request) {
|
|||||||
case http.MethodPatch:
|
case http.MethodPatch:
|
||||||
var patch userProfilePatch
|
var patch userProfilePatch
|
||||||
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
|
||||||
http.Error(w, "invalid body", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
updated := *user
|
updated := *user
|
||||||
@@ -46,12 +46,12 @@ func (s *Server) handleMeProfile(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
saved, err := s.authSvc.UpdateUserProfile(r.Context(), &updated)
|
saved, err := s.authSvc.UpdateUserProfile(r.Context(), &updated)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, saved)
|
writeJSONResponse(w, http.StatusOK, saved)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,23 +65,23 @@ func (s *Server) handleMePreferences(w http.ResponseWriter, r *http.Request) {
|
|||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
prefs, err := s.authSvc.GetUserPreferences(r.Context(), user.ID)
|
prefs, err := s.authSvc.GetUserPreferences(r.Context(), user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, prefs)
|
writeJSONResponse(w, http.StatusOK, prefs)
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
var body map[string]any
|
var body map[string]any
|
||||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
http.Error(w, "invalid body", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
prefs, err := s.authSvc.SaveUserPreferences(r.Context(), user.ID, body)
|
prefs, err := s.authSvc.SaveUserPreferences(r.Context(), user.ID, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, prefs)
|
writeJSONResponse(w, http.StatusOK, prefs)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ func (s *Server) handleListProjectNotifications(w http.ResponseWriter, r *http.R
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.notifySvc == nil || !s.notifySvc.Available() {
|
if s.notifySvc == nil || !s.notifySvc.Available() {
|
||||||
http.Error(w, "notification service unavailable", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "notification service unavailable")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || projectID <= 0 {
|
if err != nil || projectID <= 0 {
|
||||||
http.Error(w, "invalid project id", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid project id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ func (s *Server) handleListProjectNotifications(w http.ResponseWriter, r *http.R
|
|||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
http.Error(w, "list notifications failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "list notifications failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, map[string]any{
|
writeJSONResponse(w, http.StatusOK, map[string]any{
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ func TestPiAgent_FullLifecycle_MultiRole_RealWorld_E2E(t *testing.T) {
|
|||||||
targetMarket := "中国NMPA三类创新医疗器械"
|
targetMarket := "中国NMPA三类创新医疗器械"
|
||||||
|
|
||||||
// 与 dev 种子账号对齐(internal/auth/store.go)
|
// 与 dev 种子账号对齐(internal/auth/store.go)
|
||||||
authorID := int64(2) // zhanggong / 张工
|
authorID := int64(2) // zhanggong / 张工
|
||||||
authorName := "张工"
|
authorName := "张工"
|
||||||
reviewerID := int64(3) // lizhuguan / 李主管
|
reviewerID := int64(3) // lizhuguan / 李主管
|
||||||
reviewerName := "李主管"
|
reviewerName := "李主管"
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ func (s *Server) repoRoot() string {
|
|||||||
func (s *Server) handleListProjectWorkflowDocuments(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleListProjectWorkflowDocuments(w http.ResponseWriter, r *http.Request) {
|
||||||
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || projectID <= 0 {
|
if err != nil || projectID <= 0 {
|
||||||
http.Error(w, "invalid project id", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid project id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
category := r.URL.Query().Get("category")
|
category := r.URL.Query().Get("category")
|
||||||
if category != "" && category != "workflow" {
|
if category != "" && category != "workflow" {
|
||||||
http.Error(w, "unsupported category", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "unsupported category")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,13 +79,13 @@ func (s *Server) handleListProjectWorkflowDocuments(w http.ResponseWriter, r *ht
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.gov == nil {
|
if s.gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
docs, err := s.gov.ListWorkflowDocumentsByProject(r.Context(), projectID)
|
docs, err := s.gov.ListWorkflowDocumentsByProject(r.Context(), projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "list project documents failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "list project documents failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
docs = s.filterWorkflowDocsByStageAccess(r.Context(), projectID, user.ID, docs)
|
docs = s.filterWorkflowDocsByStageAccess(r.Context(), projectID, user.ID, docs)
|
||||||
@@ -96,7 +96,7 @@ func (s *Server) handleListProjectWorkflowDocuments(w http.ResponseWriter, r *ht
|
|||||||
func (s *Server) handleDocGovGetDocumentContent(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleDocGovGetDocumentContent(w http.ResponseWriter, r *http.Request) {
|
||||||
docID := r.PathValue("doc_id")
|
docID := r.PathValue("doc_id")
|
||||||
if docID == "" {
|
if docID == "" {
|
||||||
http.Error(w, "doc_id required", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "doc_id required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ func (s *Server) handleDocGovGetDocumentContent(w http.ResponseWriter, r *http.R
|
|||||||
}
|
}
|
||||||
q := s.docflowQuery()
|
q := s.docflowQuery()
|
||||||
if q == nil || q.Docs == nil || q.Docs.Gov == nil {
|
if q == nil || q.Docs == nil || q.Docs.Gov == nil {
|
||||||
http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
view, err := q.Get(r.Context(), user.ID, docID)
|
view, err := q.Get(r.Context(), user.ID, docID)
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ import (
|
|||||||
func (s *Server) handleCreateProject(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleCreateProject(w http.ResponseWriter, r *http.Request) {
|
||||||
user, err := s.authenticateUser(r)
|
user, err := s.authenticateUser(r)
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
http.Error(w, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`, http.StatusUnauthorized)
|
writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ok, permErr := s.userHasSystemPermission(r.Context(), user, "project:create"); permErr != nil {
|
if ok, permErr := s.userHasSystemPermission(r.Context(), user, "project:create"); permErr != nil {
|
||||||
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+permErr.Error()+`"}`, http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+permErr.Error()+`"}`)
|
||||||
return
|
return
|
||||||
} else if !ok {
|
} else if !ok {
|
||||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||||
@@ -33,21 +33,21 @@ func (s *Server) handleCreateProject(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req projectworkflow.CreateProjectInput
|
var req projectworkflow.CreateProjectInput
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
proj, err := s.pwSvc.CreateProject(r.Context(), user, req)
|
proj, err := s.pwSvc.CreateProject(r.Context(), user, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, projectworkflow.ErrInvalidInput) {
|
if errors.Is(err, projectworkflow.ErrInvalidInput) {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"`+err.Error()+`"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"`+err.Error()+`"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, projectworkflow.ErrWorkflowNotFound) {
|
if errors.Is(err, projectworkflow.ErrWorkflowNotFound) {
|
||||||
http.Error(w, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"`+err.Error()+`"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"`+err.Error()+`"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ func (s *Server) handleListProjects(w http.ResponseWriter, r *http.Request) {
|
|||||||
list, err = s.pwSvc.ListProjects(r.Context(), user.ID)
|
list, err = s.pwSvc.ListProjects(r.Context(), user.ID)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if list == nil {
|
if list == nil {
|
||||||
@@ -101,7 +101,7 @@ func (s *Server) handleGetProjectByID(w http.ResponseWriter, r *http.Request) {
|
|||||||
idStr := r.PathValue("id")
|
idStr := r.PathValue("id")
|
||||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
if err != nil || id <= 0 {
|
if err != nil || id <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,10 +112,10 @@ func (s *Server) handleGetProjectByID(w http.ResponseWriter, r *http.Request) {
|
|||||||
proj, err := s.pwSvc.GetProject(r.Context(), id, 0)
|
proj, err := s.pwSvc.GetProject(r.Context(), id, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, projectworkflow.ErrProjectNotFound) {
|
if errors.Is(err, projectworkflow.ErrProjectNotFound) {
|
||||||
http.Error(w, `{"error":"ERR_PROJECT_NOT_FOUND","message":"项目不存在或无权限访问"}`, http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_PROJECT_NOT_FOUND","message":"项目不存在或无权限访问"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ func (s *Server) handleListProjectWorkflows(w http.ResponseWriter, r *http.Reque
|
|||||||
idStr := r.PathValue("id")
|
idStr := r.PathValue("id")
|
||||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
if err != nil || id <= 0 {
|
if err != nil || id <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := s.requireProjectAccess(w, r, id); !ok {
|
if _, ok := s.requireProjectAccess(w, r, id); !ok {
|
||||||
@@ -136,7 +136,7 @@ func (s *Server) handleListProjectWorkflows(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
wfs, err := s.pwSvc.ListProjectWorkflows(r.Context(), id)
|
wfs, err := s.pwSvc.ListProjectWorkflows(r.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if wfs == nil {
|
if wfs == nil {
|
||||||
@@ -151,18 +151,18 @@ func (s *Server) handleActivateWorkflow(w http.ResponseWriter, r *http.Request)
|
|||||||
idStr := r.PathValue("id")
|
idStr := r.PathValue("id")
|
||||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
if err != nil || id <= 0 {
|
if err != nil || id <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := s.authenticateUser(r)
|
user, err := s.authenticateUser(r)
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
http.Error(w, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`, http.StatusUnauthorized)
|
writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.instSvc != nil && s.instSvc.Available() {
|
if s.instSvc != nil && s.instSvc.Available() {
|
||||||
http.Error(w, `{"error":"WORKFLOW_SWITCH_API_RETIRED","message":"请通过项目设置完成工作流规划与激活"}`, http.StatusGone)
|
writeSafeError(w, r, http.StatusGone, `{"error":"WORKFLOW_SWITCH_API_RETIRED","message":"请通过项目设置完成工作流规划与激活"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,29 +172,29 @@ func (s *Server) handleActivateWorkflow(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
var req projectworkflow.ActivateWorkflowInput
|
var req projectworkflow.ActivateWorkflowInput
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"无效的请求体"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"无效的请求体"}`)
|
||||||
return
|
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 err != nil {
|
||||||
if errors.Is(err, projectworkflow.ErrForbiddenNotCreator) {
|
if errors.Is(err, projectworkflow.ErrForbiddenNotCreator) {
|
||||||
http.Error(w, `{"error":"ERR_NOT_PROJECT_CREATOR","message":"仅项目创建人有权切换激活工作流"}`, http.StatusForbidden)
|
writeSafeError(w, r, http.StatusForbidden, `{"error":"ERR_NOT_PROJECT_CREATOR","message":"仅项目创建人有权切换激活工作流"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, projectworkflow.ErrConcurrentConflict) {
|
if errors.Is(err, projectworkflow.ErrConcurrentConflict) {
|
||||||
http.Error(w, `{"error":"ERR_CONCURRENT_SWITCH","message":"工作流选择版本落后或并发冲突,请刷新后重试"}`, http.StatusConflict)
|
writeSafeError(w, r, http.StatusConflict, `{"error":"ERR_CONCURRENT_SWITCH","message":"工作流选择版本落后或并发冲突,请刷新后重试"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, projectworkflow.ErrWorkflowNotFound) {
|
if errors.Is(err, projectworkflow.ErrWorkflowNotFound) {
|
||||||
http.Error(w, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"工作流定义不存在或未在项目启用"}`, http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"工作流定义不存在或未在项目启用"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, projectworkflow.ErrProjectNotFound) {
|
if errors.Is(err, projectworkflow.ErrProjectNotFound) {
|
||||||
http.Error(w, `{"error":"ERR_PROJECT_NOT_FOUND","message":"项目不存在"}`, http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_PROJECT_NOT_FOUND","message":"项目不存在"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,17 +212,17 @@ func (s *Server) handleRunProjectWorkflow(w http.ResponseWriter, r *http.Request
|
|||||||
idStr := r.PathValue("id")
|
idStr := r.PathValue("id")
|
||||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
if err != nil || id <= 0 {
|
if err != nil || id <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
wfKey := r.PathValue("workflow_key")
|
wfKey := r.PathValue("workflow_key")
|
||||||
if wfKey == "" {
|
if wfKey == "" {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"workflow_key is required"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"workflow_key is required"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
def, ok := s.pwSvc.Catalog().Get(wfKey)
|
def, ok := s.pwSvc.Catalog().Get(wfKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"unknown workflow"}`, http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_WORKFLOW_NOT_FOUND","message":"unknown workflow"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@ func (s *Server) handleRunProjectWorkflow(w http.ResponseWriter, r *http.Request
|
|||||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||||
}
|
}
|
||||||
if req.TaskKey != "" && req.TaskKey != def.TaskKey && s.failClosedAuthz() {
|
if req.TaskKey != "" && req.TaskKey != def.TaskKey && s.failClosedAuthz() {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"客户端不可覆盖 task_key"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"客户端不可覆盖 task_key"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !s.allowWorkflowDocTaskAccess(r.Context(), id, user.ID, def.TaskKey) {
|
if !s.allowWorkflowDocTaskAccess(r.Context(), id, user.ID, def.TaskKey) {
|
||||||
@@ -258,7 +258,7 @@ func (s *Server) handleListProjectWorkflowRuns(w http.ResponseWriter, r *http.Re
|
|||||||
idStr := r.PathValue("id")
|
idStr := r.PathValue("id")
|
||||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
if err != nil || id <= 0 {
|
if err != nil || id <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := s.requireProjectAccess(w, r, id); !ok {
|
if _, ok := s.requireProjectAccess(w, r, id); !ok {
|
||||||
@@ -267,7 +267,7 @@ func (s *Server) handleListProjectWorkflowRuns(w http.ResponseWriter, r *http.Re
|
|||||||
wfKey := r.PathValue("workflow_key")
|
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 {
|
if err != nil {
|
||||||
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if runs == nil {
|
if runs == nil {
|
||||||
@@ -281,7 +281,7 @@ func (s *Server) handleGetProjectLineage(w http.ResponseWriter, r *http.Request)
|
|||||||
idStr := r.PathValue("id")
|
idStr := r.PathValue("id")
|
||||||
projectID, _ := strconv.ParseInt(idStr, 10, 64)
|
projectID, _ := strconv.ParseInt(idStr, 10, 64)
|
||||||
if projectID <= 0 {
|
if projectID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := s.requireProjectAccess(w, r, projectID); !ok {
|
if _, ok := s.requireProjectAccess(w, r, projectID); !ok {
|
||||||
@@ -297,12 +297,12 @@ func (s *Server) handleGetProjectLineage(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if docID == "" {
|
if docID == "" {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"doc_id is required"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"doc_id is required"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lineages, err := s.pwSvc.GetLineageForDownstream(r.Context(), docID, 0)
|
lineages, err := s.pwSvc.GetLineageForDownstream(r.Context(), docID, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(lineages) == 0 {
|
if len(lineages) == 0 {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func resolveUpdateProjectRole(requested string, existing *authz.ProjectMemberRec
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeDutySyncError(w http.ResponseWriter) {
|
func writeDutySyncError(w http.ResponseWriter) {
|
||||||
http.Error(w, `{"error":"ERR_DUTY_SYNC","message":"成员已保存,但受控文档职责同步失败"}`, http.StatusInternalServerError)
|
writeSafeError(w, nil, http.StatusInternalServerError, `{"error":"ERR_DUTY_SYNC","message":"成员已保存,但受控文档职责同步失败"}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) healProjectDuties(ctx context.Context, projectID int64) error {
|
func (s *Server) healProjectDuties(ctx context.Context, projectID int64) error {
|
||||||
@@ -59,7 +59,7 @@ func (s *Server) handleProjectMembers(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || projectID <= 0 {
|
if err != nil || projectID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ func (s *Server) handleProjectMembers(w http.ResponseWriter, r *http.Request) {
|
|||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
s.handleRemoveProjectMember(w, r, user, projectID)
|
s.handleRemoveProjectMember(w, r, user, projectID)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ func (s *Server) handleListProjectMembers(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
members, err := s.authorizer.ListProjectMembers(r.Context(), projectID)
|
members, err := s.authorizer.ListProjectMembers(r.Context(), projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if members == nil {
|
if members == nil {
|
||||||
@@ -95,11 +95,11 @@ 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 {
|
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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if !can {
|
if !can {
|
||||||
http.Error(w, `{"error":"ERR_FORBIDDEN","message":"无权管理项目成员"}`, http.StatusForbidden)
|
writeSafeError(w, r, http.StatusForbidden, `{"error":"ERR_FORBIDDEN","message":"无权管理项目成员"}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -111,7 +111,7 @@ func (s *Server) handleAddProjectMember(w http.ResponseWriter, r *http.Request,
|
|||||||
}
|
}
|
||||||
var req projectMemberRequest
|
var req projectMemberRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.UserID <= 0 {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.UserID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"user_id and project_role required"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"user_id and project_role required"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.ProjectRole == "" {
|
if req.ProjectRole == "" {
|
||||||
@@ -121,7 +121,7 @@ func (s *Server) handleAddProjectMember(w http.ResponseWriter, r *http.Request,
|
|||||||
ProjectRole: req.ProjectRole,
|
ProjectRole: req.ProjectRole,
|
||||||
DocumentDuty: req.DocumentDuty,
|
DocumentDuty: req.DocumentDuty,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := s.healProjectDuties(r.Context(), projectID); err != nil {
|
if err := s.healProjectDuties(r.Context(), projectID); err != nil {
|
||||||
@@ -137,40 +137,40 @@ func (s *Server) handleUpdateProjectMember(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
var req projectMemberRequest
|
var req projectMemberRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.UserID <= 0 {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.UserID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"user_id required"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"user_id required"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var existing *authz.ProjectMemberRecord
|
var existing *authz.ProjectMemberRecord
|
||||||
if s.authorizer != nil {
|
if s.authorizer != nil {
|
||||||
rec, err := s.authorizer.GetProjectMember(r.Context(), projectID, req.UserID)
|
rec, err := s.authorizer.GetProjectMember(r.Context(), projectID, req.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
existing = rec
|
existing = rec
|
||||||
} else if req.ProjectRole == "" && req.DocumentDuty != nil {
|
} else if req.ProjectRole == "" && req.DocumentDuty != nil {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"project_role required with document_duty"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"project_role required with document_duty"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
wrote := false
|
wrote := false
|
||||||
if req.ProjectRole != "" || req.DocumentDuty != nil {
|
if req.ProjectRole != "" || req.DocumentDuty != nil {
|
||||||
role, err := resolveUpdateProjectRole(req.ProjectRole, existing)
|
role, err := resolveUpdateProjectRole(req.ProjectRole, existing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"project_role required when member does not exist"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"project_role required when member does not exist"}`)
|
||||||
return
|
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,
|
ProjectRole: role,
|
||||||
DocumentDuty: req.DocumentDuty,
|
DocumentDuty: req.DocumentDuty,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
wrote = true
|
wrote = true
|
||||||
}
|
}
|
||||||
if req.Status != "" {
|
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 {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
wrote = true
|
wrote = true
|
||||||
@@ -191,11 +191,11 @@ func (s *Server) handleRemoveProjectMember(w http.ResponseWriter, r *http.Reques
|
|||||||
userIDStr := r.URL.Query().Get("user_id")
|
userIDStr := r.URL.Query().Get("user_id")
|
||||||
targetID, err := strconv.ParseInt(userIDStr, 10, 64)
|
targetID, err := strconv.ParseInt(userIDStr, 10, 64)
|
||||||
if err != nil || targetID <= 0 {
|
if err != nil || targetID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"user_id query required"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"user_id query required"}`)
|
||||||
return
|
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 {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := s.healProjectDuties(r.Context(), projectID); err != nil {
|
if err := s.healProjectDuties(r.Context(), projectID); err != nil {
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import (
|
|||||||
func (s *Server) handleGetDefaultWorkflowPlan(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetDefaultWorkflowPlan(w http.ResponseWriter, r *http.Request) {
|
||||||
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || projectID <= 0 {
|
if err != nil || projectID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := s.requireProjectAccess(w, r, projectID); !ok {
|
if _, ok := s.requireProjectAccess(w, r, projectID); !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.instSvc == nil || !s.instSvc.Available() {
|
if s.instSvc == nil || !s.instSvc.Available() {
|
||||||
http.Error(w, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`, http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
workflowKey := r.URL.Query().Get("workflow_key")
|
workflowKey := r.URL.Query().Get("workflow_key")
|
||||||
@@ -36,7 +36,7 @@ func (s *Server) handleGetDefaultWorkflowPlan(w http.ResponseWriter, r *http.Req
|
|||||||
func (s *Server) handleGetProjectSettings(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetProjectSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || projectID <= 0 {
|
if err != nil || projectID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, ok := s.requireProjectAccess(w, r, projectID)
|
user, ok := s.requireProjectAccess(w, r, projectID)
|
||||||
@@ -44,7 +44,7 @@ func (s *Server) handleGetProjectSettings(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.instSvc == nil || !s.instSvc.Available() {
|
if s.instSvc == nil || !s.instSvc.Available() {
|
||||||
http.Error(w, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用,请先执行数据库迁移"}`, http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用,请先执行数据库迁移"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
||||||
@@ -81,7 +81,7 @@ type workflowEventSummary struct {
|
|||||||
func (s *Server) handleSaveWorkflowPlan(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleSaveWorkflowPlan(w http.ResponseWriter, r *http.Request) {
|
||||||
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || projectID <= 0 {
|
if err != nil || projectID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, ok := s.requireProjectAccess(w, r, projectID)
|
user, ok := s.requireProjectAccess(w, r, projectID)
|
||||||
@@ -89,13 +89,13 @@ func (s *Server) handleSaveWorkflowPlan(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.instSvc == nil || !s.instSvc.Available() {
|
if s.instSvc == nil || !s.instSvc.Available() {
|
||||||
http.Error(w, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`, http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
||||||
var req projectinstance.SavePlanInput
|
var req projectinstance.SavePlanInput
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
settings, err := s.instSvc.SaveWorkflowPlan(r.Context(), projectID, user.ID, isCreator, req)
|
settings, err := s.instSvc.SaveWorkflowPlan(r.Context(), projectID, user.ID, isCreator, req)
|
||||||
@@ -109,7 +109,7 @@ func (s *Server) handleSaveWorkflowPlan(w http.ResponseWriter, r *http.Request)
|
|||||||
func (s *Server) handleActivationPreview(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleActivationPreview(w http.ResponseWriter, r *http.Request) {
|
||||||
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || projectID <= 0 {
|
if err != nil || projectID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, ok := s.requireProjectAccess(w, r, projectID)
|
user, ok := s.requireProjectAccess(w, r, projectID)
|
||||||
@@ -117,7 +117,7 @@ func (s *Server) handleActivationPreview(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.instSvc == nil || !s.instSvc.Available() {
|
if s.instSvc == nil || !s.instSvc.Available() {
|
||||||
http.Error(w, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`, http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
||||||
@@ -132,7 +132,7 @@ func (s *Server) handleActivationPreview(w http.ResponseWriter, r *http.Request)
|
|||||||
func (s *Server) handleActivateWorkflowInstance(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleActivateWorkflowInstance(w http.ResponseWriter, r *http.Request) {
|
||||||
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || projectID <= 0 {
|
if err != nil || projectID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, ok := s.requireProjectAccess(w, r, projectID)
|
user, ok := s.requireProjectAccess(w, r, projectID)
|
||||||
@@ -140,13 +140,13 @@ func (s *Server) handleActivateWorkflowInstance(w http.ResponseWriter, r *http.R
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.instSvc == nil || !s.instSvc.Available() {
|
if s.instSvc == nil || !s.instSvc.Available() {
|
||||||
http.Error(w, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`, http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), projectID, user.ID)
|
||||||
var req projectinstance.ActivateInput
|
var req projectinstance.ActivateInput
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`)
|
||||||
return
|
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)
|
||||||
@@ -160,13 +160,13 @@ func (s *Server) handleActivateWorkflowInstance(w http.ResponseWriter, r *http.R
|
|||||||
func writeProjectInstanceError(w http.ResponseWriter, err error) {
|
func writeProjectInstanceError(w http.ResponseWriter, err error) {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, projectinstance.ErrForbiddenNotCreator):
|
case errors.Is(err, projectinstance.ErrForbiddenNotCreator):
|
||||||
http.Error(w, `{"error":"ERR_NOT_PROJECT_CREATOR","message":"仅项目创建人可配置工作流"}`, http.StatusForbidden)
|
writeSafeError(w, nil, http.StatusForbidden, `{"error":"ERR_NOT_PROJECT_CREATOR","message":"仅项目创建人可配置工作流"}`)
|
||||||
case errors.Is(err, projectinstance.ErrPlanFrozen):
|
case errors.Is(err, projectinstance.ErrPlanFrozen):
|
||||||
http.Error(w, `{"error":"PLAN_FROZEN","message":"工作流已激活,规划已冻结"}`, http.StatusConflict)
|
writeSafeError(w, nil, http.StatusConflict, `{"error":"PLAN_FROZEN","message":"工作流已激活,规划已冻结"}`)
|
||||||
case errors.Is(err, projectinstance.ErrWorkflowAlreadyActive):
|
case errors.Is(err, projectinstance.ErrWorkflowAlreadyActive):
|
||||||
http.Error(w, `{"error":"WORKFLOW_ALREADY_ACTIVE","message":"项目已有运行中的工作流实例"}`, http.StatusConflict)
|
writeSafeError(w, nil, http.StatusConflict, `{"error":"WORKFLOW_ALREADY_ACTIVE","message":"项目已有运行中的工作流实例"}`)
|
||||||
case errors.Is(err, projectinstance.ErrPlanChangedReconfirm):
|
case errors.Is(err, projectinstance.ErrPlanChangedReconfirm):
|
||||||
http.Error(w, `{"error":"PLAN_CHANGED_RECONFIRM","message":"规划已变更,请重新预览并确认"}`, http.StatusConflict)
|
writeSafeError(w, nil, http.StatusConflict, `{"error":"PLAN_CHANGED_RECONFIRM","message":"规划已变更,请重新预览并确认"}`)
|
||||||
case errors.Is(err, projectinstance.ErrValidationFailed):
|
case errors.Is(err, projectinstance.ErrValidationFailed):
|
||||||
var validationErr *projectinstance.ValidationError
|
var validationErr *projectinstance.ValidationError
|
||||||
if errors.As(err, &validationErr) {
|
if errors.As(err, &validationErr) {
|
||||||
@@ -177,36 +177,36 @@ func writeProjectInstanceError(w http.ResponseWriter, err error) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, `{"error":"VALIDATION_FAILED","message":"工作流配置校验未通过"}`, http.StatusBadRequest)
|
writeSafeError(w, nil, http.StatusBadRequest, `{"error":"VALIDATION_FAILED","message":"工作流配置校验未通过"}`)
|
||||||
case errors.Is(err, projectinstance.ErrTokenInvalid):
|
case errors.Is(err, projectinstance.ErrTokenInvalid):
|
||||||
http.Error(w, `{"error":"TOKEN_INVALID","message":"激活令牌无效或已过期"}`, http.StatusBadRequest)
|
writeSafeError(w, nil, http.StatusBadRequest, `{"error":"TOKEN_INVALID","message":"激活令牌无效或已过期"}`)
|
||||||
case errors.Is(err, projectinstance.ErrConcurrentConflict):
|
case errors.Is(err, projectinstance.ErrConcurrentConflict):
|
||||||
http.Error(w, `{"error":"ERR_CONCURRENT_SWITCH","message":"项目版本冲突,请刷新后重试"}`, http.StatusConflict)
|
writeSafeError(w, nil, http.StatusConflict, `{"error":"ERR_CONCURRENT_SWITCH","message":"项目版本冲突,请刷新后重试"}`)
|
||||||
case errors.Is(err, projectinstance.ErrPlanRevisionConflict):
|
case errors.Is(err, projectinstance.ErrPlanRevisionConflict):
|
||||||
http.Error(w, `{"error":"PLAN_REVISION_CONFLICT","message":"规划版本冲突,请刷新后重试"}`, http.StatusConflict)
|
writeSafeError(w, nil, http.StatusConflict, `{"error":"PLAN_REVISION_CONFLICT","message":"规划版本冲突,请刷新后重试"}`)
|
||||||
case errors.Is(err, projectinstance.ErrProjectNotFound):
|
case errors.Is(err, projectinstance.ErrProjectNotFound):
|
||||||
http.Error(w, `{"error":"ERR_PROJECT_NOT_FOUND","message":"项目不存在"}`, http.StatusNotFound)
|
writeSafeError(w, nil, http.StatusNotFound, `{"error":"ERR_PROJECT_NOT_FOUND","message":"项目不存在"}`)
|
||||||
case errors.Is(err, projectinstance.ErrInvalidPlan):
|
case errors.Is(err, projectinstance.ErrInvalidPlan):
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PLAN","message":"`+err.Error()+`"}`, http.StatusBadRequest)
|
writeSafeError(w, nil, http.StatusBadRequest, `{"error":"ERR_INVALID_PLAN","message":"`+err.Error()+`"}`)
|
||||||
case errors.Is(err, projectinstance.ErrInstanceNotFound):
|
case errors.Is(err, projectinstance.ErrInstanceNotFound):
|
||||||
http.Error(w, `{"error":"ERR_INSTANCE_NOT_FOUND","message":"工作流实例不存在"}`, http.StatusNotFound)
|
writeSafeError(w, nil, http.StatusNotFound, `{"error":"ERR_INSTANCE_NOT_FOUND","message":"工作流实例不存在"}`)
|
||||||
case errors.Is(err, projectinstance.ErrStageGateNotReady):
|
case errors.Is(err, projectinstance.ErrStageGateNotReady):
|
||||||
http.Error(w, `{"error":"STAGE_GATE_NOT_READY","message":"阶段门禁未满足,必需文档尚未全部批准"}`, http.StatusConflict)
|
writeSafeError(w, nil, http.StatusConflict, `{"error":"STAGE_GATE_NOT_READY","message":"阶段门禁未满足,必需文档尚未全部批准"}`)
|
||||||
case errors.Is(err, projectinstance.ErrStageNotActive):
|
case errors.Is(err, projectinstance.ErrStageNotActive):
|
||||||
http.Error(w, `{"error":"STAGE_NOT_ACTIVE","message":"目标阶段不是当前活动阶段"}`, http.StatusConflict)
|
writeSafeError(w, nil, http.StatusConflict, `{"error":"STAGE_NOT_ACTIVE","message":"目标阶段不是当前活动阶段"}`)
|
||||||
case errors.Is(err, projectinstance.ErrInvalidStage):
|
case errors.Is(err, projectinstance.ErrInvalidStage):
|
||||||
http.Error(w, `{"error":"ERR_INVALID_STAGE","message":"无效的阶段"}`, http.StatusBadRequest)
|
writeSafeError(w, nil, http.StatusBadRequest, `{"error":"ERR_INVALID_STAGE","message":"无效的阶段"}`)
|
||||||
case errors.Is(err, projectinstance.ErrInstanceRevisionConflict):
|
case errors.Is(err, projectinstance.ErrInstanceRevisionConflict):
|
||||||
http.Error(w, `{"error":"INSTANCE_REVISION_CONFLICT","message":"实例版本冲突,请刷新后重试"}`, http.StatusConflict)
|
writeSafeError(w, nil, http.StatusConflict, `{"error":"INSTANCE_REVISION_CONFLICT","message":"实例版本冲突,请刷新后重试"}`)
|
||||||
default:
|
default:
|
||||||
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
writeSafeError(w, nil, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) blockLegacyWorkflowSwitch(w http.ResponseWriter, r *http.Request, projectID int64) bool {
|
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 == "" {
|
if err == nil && proj != nil && proj.LifecycleStatus == "planning" && proj.ActiveWorkflowKey == "" {
|
||||||
http.Error(w, `{"error":"LEGACY_SWITCH_BLOCKED","message":"新项目请通过项目设置完成工作流激活"}`, http.StatusConflict)
|
writeSafeError(w, r, http.StatusConflict, `{"error":"LEGACY_SWITCH_BLOCKED","message":"新项目请通过项目设置完成工作流激活"}`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if s.instSvc == nil || !s.instSvc.Available() {
|
if s.instSvc == nil || !s.instSvc.Available() {
|
||||||
@@ -214,11 +214,11 @@ func (s *Server) blockLegacyWorkflowSwitch(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
live, err := s.instSvc.HasLiveInstance(r.Context(), projectID)
|
live, err := s.instSvc.HasLiveInstance(r.Context(), projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if live {
|
if live {
|
||||||
http.Error(w, `{"error":"WORKFLOW_ALREADY_ACTIVE","message":"项目已有运行中的工作流实例,请使用项目设置查看流程"}`, http.StatusConflict)
|
writeSafeError(w, r, http.StatusConflict, `{"error":"WORKFLOW_ALREADY_ACTIVE","message":"项目已有运行中的工作流实例,请使用项目设置查看流程"}`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -256,4 +256,3 @@ func TestProjectWorkflow_TestManagerAndLineageStale(t *testing.T) {
|
|||||||
require.NotEmpty(t, lineagesAfter)
|
require.NotEmpty(t, lineagesAfter)
|
||||||
assert.True(t, lineagesAfter[0].IsStale)
|
assert.True(t, lineagesAfter[0].IsStale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,33 +16,33 @@ func (s *Server) handleReconcileWorkspace(w http.ResponseWriter, r *http.Request
|
|||||||
RunID string `json:"run_id"`
|
RunID string `json:"run_id"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
http.Error(w, "invalid body", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if body.RunID == "" {
|
if body.RunID == "" {
|
||||||
http.Error(w, "run_id required", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "run_id required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rec, err := s.taskRuns.GetRun(body.RunID)
|
rec, err := s.taskRuns.GetRun(body.RunID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "run not found", http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, "run not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if rec.WorkspaceID == "" {
|
if rec.WorkspaceID == "" {
|
||||||
http.Error(w, "workspace not available for run", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "workspace not available for run")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
workDir, err := s.resolver.ResolveStoredWorkDir(rec.WorkspaceID)
|
workDir, err := s.resolver.ResolveStoredWorkDir(rec.WorkspaceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "invalid workspace path", http.StatusForbidden)
|
writeSafeError(w, r, http.StatusForbidden, "invalid workspace path")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
report, err := squad.ReconcileWorkspaceStatus(workDir)
|
report, err := squad.ReconcileWorkspaceStatus(workDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, report)
|
writeJSONResponse(w, http.StatusOK, report)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func (s *Server) handleGetExportCapability(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.ctxMgr == nil {
|
if s.ctxMgr == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ func (s *Server) handleGetExportSource(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.ctxMgr == nil {
|
if s.ctxMgr == nil {
|
||||||
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,11 +172,11 @@ func TestPenetration_ProjectAssetRejectsScriptContent(t *testing.T) {
|
|||||||
|
|
||||||
func TestPenetration_ReadyStrictWhenDBConfiguredButDown(t *testing.T) {
|
func TestPenetration_ReadyStrictWhenDBConfiguredButDown(t *testing.T) {
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
DataDir: t.TempDir(),
|
DataDir: t.TempDir(),
|
||||||
AgentBackend: "stub",
|
AgentBackend: "stub",
|
||||||
ContextDbHost: "127.0.0.1",
|
ContextDbHost: "127.0.0.1",
|
||||||
ContextDbPort: 59999,
|
ContextDbPort: 59999,
|
||||||
AllowInMemory: false,
|
AllowInMemory: false,
|
||||||
}
|
}
|
||||||
srv := NewServer(cfg, bizdev.MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"}))
|
srv := NewServer(cfg, bizdev.MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"}))
|
||||||
srv.cfg = cfg
|
srv.cfg = cfg
|
||||||
|
|||||||
@@ -129,14 +129,14 @@ func replayExistingTurn(w http.ResponseWriter, flusher http.Flusher, rec *ctxmgr
|
|||||||
func (s *Server) handleChatStream(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleChatStream(w http.ResponseWriter, r *http.Request) {
|
||||||
flusher, ok := w.(http.Flusher)
|
flusher, ok := w.(http.Flusher)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "streaming unsupported by underlying connection", http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "streaming unsupported by underlying connection")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req StreamChatRequest
|
var req StreamChatRequest
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -161,7 +161,7 @@ func (s *Server) handleChatStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Message == "" {
|
if req.Message == "" {
|
||||||
http.Error(w, "message cannot be empty", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "message cannot be empty")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +305,7 @@ func (s *Server) handleChatStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) handleRunTaskStream(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleRunTaskStream(w http.ResponseWriter, r *http.Request) {
|
||||||
flusher, ok := w.(http.Flusher)
|
flusher, ok := w.(http.Flusher)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "streaming unsupported")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +381,7 @@ func (s *Server) handleRunTaskStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
http.Error(w, "start task run failed: "+err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "start task run failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = includeDetails
|
_ = includeDetails
|
||||||
@@ -389,7 +389,7 @@ func (s *Server) handleRunTaskStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
ch, replay, err := s.taskRuns.Subscribe(created.RunID, 0)
|
ch, replay, err := s.taskRuns.Subscribe(created.RunID, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer s.taskRuns.Unsubscribe(created.RunID, ch)
|
defer s.taskRuns.Unsubscribe(created.RunID, ch)
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
type taskRunAction string
|
type taskRunAction string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
taskRunView taskRunAction = "view"
|
taskRunView taskRunAction = "view"
|
||||||
taskRunCancel taskRunAction = "cancel"
|
taskRunCancel taskRunAction = "cancel"
|
||||||
taskRunResume taskRunAction = "resume"
|
taskRunResume taskRunAction = "resume"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) assertTaskRunAccess(w http.ResponseWriter, ctx context.Context, runID string, user *auth.UserAccount, action taskRunAction) bool {
|
func (s *Server) assertTaskRunAccess(w http.ResponseWriter, ctx context.Context, runID string, user *auth.UserAccount, action taskRunAction) bool {
|
||||||
@@ -25,7 +25,7 @@ func (s *Server) assertTaskRunAccess(w http.ResponseWriter, ctx context.Context,
|
|||||||
}
|
}
|
||||||
rec, err := s.taskRuns.GetRun(runID)
|
rec, err := s.taskRuns.GetRun(runID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "run not found", http.StatusNotFound)
|
writeSafeError(w, nil, http.StatusNotFound, "run not found")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if s.isSystemAdminUser(ctx, user) {
|
if s.isSystemAdminUser(ctx, user) {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
var req createTaskRunRequest
|
var req createTaskRunRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.UserID = user.ID
|
req.UserID = user.ID
|
||||||
@@ -65,7 +65,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := bizdev.ValidateTaskRunIdentity(req.SessionID, req.ProjectID); err != nil {
|
if err := bizdev.ValidateTaskRunIdentity(req.SessionID, req.ProjectID); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.TaskKey == "" && req.Stage == "" {
|
if req.TaskKey == "" && req.Stage == "" {
|
||||||
@@ -85,7 +85,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
idemKey := taskrun.BuildTaskRunIdempotencyKey(req.SessionID, req.ClientTurnID)
|
idemKey := taskrun.BuildTaskRunIdempotencyKey(req.SessionID, req.ClientTurnID)
|
||||||
if idemKey != "" && s.taskRuns != nil {
|
if idemKey != "" && s.taskRuns != nil {
|
||||||
if existing, findErr := s.taskRuns.FindByIdempotencyKey(r.Context(), idemKey); findErr != nil {
|
if existing, findErr := s.taskRuns.FindByIdempotencyKey(r.Context(), idemKey); findErr != nil {
|
||||||
http.Error(w, findErr.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, findErr.Error())
|
||||||
return
|
return
|
||||||
} else if existing != nil {
|
} else if existing != nil {
|
||||||
if !taskRunMatchesIdempotentOwner(existing, req) {
|
if !taskRunMatchesIdempotentOwner(existing, req) {
|
||||||
@@ -122,7 +122,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if busy, preflightErr := s.preflightTaskDocumentBusy(r.Context(), req); preflightErr != nil {
|
if busy, preflightErr := s.preflightTaskDocumentBusy(r.Context(), req); preflightErr != nil {
|
||||||
http.Error(w, preflightErr.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, preflightErr.Error())
|
||||||
return
|
return
|
||||||
} else if busy != nil {
|
} else if busy != nil {
|
||||||
s.recordBusyPreflightMessages(r.Context(), ctxMgr, req, taskScope, busy)
|
s.recordBusyPreflightMessages(r.Context(), ctxMgr, req, taskScope, busy)
|
||||||
@@ -153,7 +153,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
claimed, created, claimErr := s.taskRuns.ClaimRun(r.Context(), record)
|
claimed, created, claimErr := s.taskRuns.ClaimRun(r.Context(), record)
|
||||||
if claimErr != nil {
|
if claimErr != nil {
|
||||||
http.Error(w, claimErr.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, claimErr.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !created {
|
if !created {
|
||||||
@@ -205,7 +205,7 @@ func (s *Server) handleCreateTaskRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
stageReq := s.buildBizdevStageRequest(req.TaskRunRequestDTO, req.SessionID, req.ClientTurnID, claimed.RunID)
|
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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ func (s *Server) createBlockedGovernanceRun(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
claimed, created, claimErr := s.taskRuns.ClaimRun(r.Context(), record)
|
claimed, created, claimErr := s.taskRuns.ClaimRun(r.Context(), record)
|
||||||
if claimErr != nil {
|
if claimErr != nil {
|
||||||
http.Error(w, claimErr.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, claimErr.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !created {
|
if !created {
|
||||||
@@ -387,7 +387,7 @@ func (s *Server) handleTaskRunEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
runID := r.PathValue("run_id")
|
runID := r.PathValue("run_id")
|
||||||
if runID == "" {
|
if runID == "" {
|
||||||
http.Error(w, "run_id required", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "run_id required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !s.assertTaskRunOwnedByUser(w, r.Context(), runID, user) {
|
if !s.assertTaskRunOwnedByUser(w, r.Context(), runID, user) {
|
||||||
@@ -395,7 +395,7 @@ func (s *Server) handleTaskRunEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
flusher, ok := w.(http.Flusher)
|
flusher, ok := w.(http.Flusher)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, "streaming unsupported")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +414,7 @@ func (s *Server) handleTaskRunEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
ch, replay, err := s.taskRuns.Subscribe(runID, afterSeq)
|
ch, replay, err := s.taskRuns.Subscribe(runID, afterSeq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer s.taskRuns.Unsubscribe(runID, ch)
|
defer s.taskRuns.Unsubscribe(runID, ch)
|
||||||
@@ -475,14 +475,14 @@ func (s *Server) handleCancelTaskRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
runID := r.PathValue("run_id")
|
runID := r.PathValue("run_id")
|
||||||
if runID == "" {
|
if runID == "" {
|
||||||
http.Error(w, "run_id required", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "run_id required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !s.assertTaskRunAccess(w, r.Context(), runID, user, taskRunCancel) {
|
if !s.assertTaskRunAccess(w, r.Context(), runID, user, taskRunCancel) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := s.taskRuns.Cancel(runID); err != nil {
|
if err := s.taskRuns.Cancel(runID); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if rec, getErr := s.taskRuns.GetRun(runID); getErr == nil && rec.WorkspaceID != "" {
|
if rec, getErr := s.taskRuns.GetRun(runID); getErr == nil && rec.WorkspaceID != "" {
|
||||||
@@ -522,7 +522,7 @@ func (s *Server) handleGetTaskRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
rec, err := s.taskRuns.GetRun(runID)
|
rec, err := s.taskRuns.GetRun(runID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONResponse(w, http.StatusOK, s.buildTaskRunResponse(rec))
|
writeJSONResponse(w, http.StatusOK, s.buildTaskRunResponse(rec))
|
||||||
@@ -535,7 +535,7 @@ func (s *Server) handleResumeTaskRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
runID := r.PathValue("run_id")
|
runID := r.PathValue("run_id")
|
||||||
if runID == "" {
|
if runID == "" {
|
||||||
http.Error(w, "run_id required", http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "run_id required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !s.assertTaskRunAccess(w, r.Context(), runID, user, taskRunResume) {
|
if !s.assertTaskRunAccess(w, r.Context(), runID, user, taskRunResume) {
|
||||||
@@ -544,7 +544,7 @@ func (s *Server) handleResumeTaskRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req resumeTaskRunRequest
|
var req resumeTaskRunRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !req.Confirm {
|
if !req.Confirm {
|
||||||
@@ -557,7 +557,7 @@ func (s *Server) handleResumeTaskRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
rec, err := s.taskRuns.GetRun(runID)
|
rec, err := s.taskRuns.GetRun(runID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if rec.Status != "interrupted" {
|
if rec.Status != "interrupted" {
|
||||||
@@ -575,7 +575,7 @@ func (s *Server) handleResumeTaskRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.resolver == nil {
|
if s.resolver == nil {
|
||||||
http.Error(w, "workspace resolver unavailable", http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, "workspace resolver unavailable")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
workDir, werr := s.resolver.ResolveStoredWorkDir(rec.WorkspaceID)
|
workDir, werr := s.resolver.ResolveStoredWorkDir(rec.WorkspaceID)
|
||||||
@@ -608,7 +608,7 @@ func (s *Server) handleResumeTaskRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,11 +258,11 @@ func TestTaskStream_MaterialsInjection(t *testing.T) {
|
|||||||
b64Content := "55So5oi35LiK5Lyg5paH5qGj5qC45b+D6ZyA5rGC77ya6ZyA6KaB5pSv5oyBIDEwODBQIOi2heWjsOWunuaXtua4suafk+OAgg=="
|
b64Content := "55So5oi35LiK5Lyg5paH5qGj5qC45b+D6ZyA5rGC77ya6ZyA6KaB5pSv5oyBIDEwODBQIOi2heWjsOWunuaXtua4suafk+OAgg=="
|
||||||
|
|
||||||
body, _ := json.Marshal(map[string]any{
|
body, _ := json.Marshal(map[string]any{
|
||||||
"task_key": "intake",
|
"task_key": "intake",
|
||||||
"project_name": "超声材料注入测试",
|
"project_name": "超声材料注入测试",
|
||||||
"customer_name": "北京水木东方",
|
"customer_name": "北京水木东方",
|
||||||
"device_type": "超声诊断设备",
|
"device_type": "超声诊断设备",
|
||||||
"backend": "stub",
|
"backend": "stub",
|
||||||
"materials": []map[string]string{
|
"materials": []map[string]string{
|
||||||
{
|
{
|
||||||
"filename": "initial_requirements.txt",
|
"filename": "initial_requirements.txt",
|
||||||
@@ -301,4 +301,3 @@ func TestTaskStream_MaterialsInjection(t *testing.T) {
|
|||||||
t.Fatalf("material content mismatch: got %q, want %q", string(content), rawText)
|
t.Fatalf("material content mismatch: got %q, want %q", string(content), rawText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ type workflowCatalogItem struct {
|
|||||||
|
|
||||||
func (s *Server) handleWorkflowCatalog(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleWorkflowCatalog(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.wfDefSvc == nil || !s.wfDefSvc.Available() {
|
if s.wfDefSvc == nil || !s.wfDefSvc.Available() {
|
||||||
http.Error(w, `{"error":"ERR_NOT_AVAILABLE","message":"workflow catalog unavailable"}`, http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"workflow catalog unavailable"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defs, err := s.wfDefSvc.Store().ListProjectSelectableDefinitions(r.Context())
|
defs, err := s.wfDefSvc.Store().ListProjectSelectableDefinitions(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]workflowCatalogItem, 0, len(workflowdef.OfficialWorkflowKeys()))
|
items := make([]workflowCatalogItem, 0, len(workflowdef.OfficialWorkflowKeys()))
|
||||||
|
|||||||
@@ -193,8 +193,10 @@ func runPiAgentGovernanceE2E(t *testing.T, spec piAgentGovernanceCase) {
|
|||||||
t.Fatalf("pi-agent task HTTP %d: %s", respTask.StatusCode, buf.String())
|
t.Fatalf("pi-agent task HTTP %d: %s", respTask.StatusCode, buf.String())
|
||||||
}
|
}
|
||||||
var taskRes struct {
|
var taskRes struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
DocRecord *struct{ DocID string `json:"doc_id"` } `json:"doc_record"`
|
DocRecord *struct {
|
||||||
|
DocID string `json:"doc_id"`
|
||||||
|
} `json:"doc_record"`
|
||||||
ArtifactTitle string `json:"artifact_title"`
|
ArtifactTitle string `json:"artifact_title"`
|
||||||
}
|
}
|
||||||
_ = json.NewDecoder(respTask.Body).Decode(&taskRes)
|
_ = json.NewDecoder(respTask.Body).Decode(&taskRes)
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import (
|
|||||||
func (s *Server) handleGetWorkflowInstance(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetWorkflowInstance(w http.ResponseWriter, r *http.Request) {
|
||||||
instanceID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
instanceID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || instanceID <= 0 {
|
if err != nil || instanceID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid instance id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid instance id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.instSvc == nil || !s.instSvc.Available() {
|
if s.instSvc == nil || !s.instSvc.Available() {
|
||||||
http.Error(w, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`, http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
||||||
@@ -32,12 +32,12 @@ func (s *Server) handleGetWorkflowInstance(w http.ResponseWriter, r *http.Reques
|
|||||||
func (s *Server) handleGetStageReadiness(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetStageReadiness(w http.ResponseWriter, r *http.Request) {
|
||||||
instanceID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
instanceID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || instanceID <= 0 {
|
if err != nil || instanceID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid instance id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid instance id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stageKey := r.PathValue("stage")
|
stageKey := r.PathValue("stage")
|
||||||
if s.instSvc == nil || !s.instSvc.Available() {
|
if s.instSvc == nil || !s.instSvc.Available() {
|
||||||
http.Error(w, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`, http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
||||||
@@ -61,12 +61,12 @@ func (s *Server) handleGetStageReadiness(w http.ResponseWriter, r *http.Request)
|
|||||||
func (s *Server) handleAdvanceStage(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAdvanceStage(w http.ResponseWriter, r *http.Request) {
|
||||||
instanceID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
instanceID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || instanceID <= 0 {
|
if err != nil || instanceID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"invalid instance id"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid instance id"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stageKey := r.PathValue("stage")
|
stageKey := r.PathValue("stage")
|
||||||
if s.instSvc == nil || !s.instSvc.Available() {
|
if s.instSvc == nil || !s.instSvc.Available() {
|
||||||
http.Error(w, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`, http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE","message":"项目实例服务不可用"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
||||||
@@ -81,7 +81,7 @@ func (s *Server) handleAdvanceStage(w http.ResponseWriter, r *http.Request) {
|
|||||||
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
|
var req projectinstance.AdvanceStageInput
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && r.ContentLength > 0 {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && r.ContentLength > 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.StageKey == "" {
|
if req.StageKey == "" {
|
||||||
|
|||||||
@@ -11,22 +11,22 @@ import (
|
|||||||
|
|
||||||
func (s *Server) handleTerminationPreview(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleTerminationPreview(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.instSvc == nil || !s.instSvc.Available() {
|
if s.instSvc == nil || !s.instSvc.Available() {
|
||||||
http.Error(w, `{"error":"ERR_NOT_AVAILABLE"}`, http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
instanceID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
instanceID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || instanceID <= 0 {
|
if err != nil || instanceID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, err := s.authenticateUser(r)
|
user, err := s.authenticateUser(r)
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
http.Error(w, `{"error":"ERR_UNAUTHENTICATED"}`, http.StatusUnauthorized)
|
writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, `{"error":"ERR_NOT_FOUND"}`, http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_NOT_FOUND"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), detail.Instance.ProjectID, user.ID)
|
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), detail.Instance.ProjectID, user.ID)
|
||||||
@@ -40,27 +40,27 @@ func (s *Server) handleTerminationPreview(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
func (s *Server) handleTerminateWorkflowInstance(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() {
|
||||||
http.Error(w, `{"error":"ERR_NOT_AVAILABLE"}`, http.StatusServiceUnavailable)
|
writeSafeError(w, r, http.StatusServiceUnavailable, `{"error":"ERR_NOT_AVAILABLE"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
instanceID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
instanceID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || instanceID <= 0 {
|
if err != nil || instanceID <= 0 {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, err := s.authenticateUser(r)
|
user, err := s.authenticateUser(r)
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
http.Error(w, `{"error":"ERR_UNAUTHENTICATED"}`, http.StatusUnauthorized)
|
writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
detail, err := s.instSvc.GetInstanceDetail(r.Context(), instanceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, `{"error":"ERR_NOT_FOUND"}`, http.StatusNotFound)
|
writeSafeError(w, r, http.StatusNotFound, `{"error":"ERR_NOT_FOUND"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var in projectinstance.TerminateInput
|
var in projectinstance.TerminateInput
|
||||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
http.Error(w, `{"error":"ERR_INVALID_PARAM"}`, http.StatusBadRequest)
|
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM"}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), detail.Instance.ProjectID, user.ID)
|
isCreator, _ := s.pwSvc.IsProjectCreator(r.Context(), detail.Instance.ProjectID, user.ID)
|
||||||
@@ -68,11 +68,11 @@ func (s *Server) handleTerminateWorkflowInstance(w http.ResponseWriter, r *http.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, projectinstance.ErrForbiddenNotCreator):
|
case errors.Is(err, projectinstance.ErrForbiddenNotCreator):
|
||||||
http.Error(w, `{"error":"ERR_FORBIDDEN"}`, http.StatusForbidden)
|
writeSafeError(w, r, http.StatusForbidden, `{"error":"ERR_FORBIDDEN"}`)
|
||||||
case errors.Is(err, projectinstance.ErrTerminationNotAllowed):
|
case errors.Is(err, projectinstance.ErrTerminationNotAllowed):
|
||||||
http.Error(w, `{"error":"ERR_TERMINATION_NOT_ALLOWED"}`, http.StatusConflict)
|
writeSafeError(w, r, http.StatusConflict, `{"error":"ERR_TERMINATION_NOT_ALLOWED"}`)
|
||||||
default:
|
default:
|
||||||
http.Error(w, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`, http.StatusInternalServerError)
|
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,14 +15,17 @@ func ClassifyError(err error, phaseID, requestID, sessionID, workspaceID string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
msg := strings.ToLower(err.Error())
|
msg := strings.ToLower(err.Error())
|
||||||
upstream := truncateRunes(err.Error(), 80)
|
|
||||||
|
|
||||||
info := ErrorInfo{
|
info := ErrorInfo{
|
||||||
PhaseID: phaseID,
|
PhaseID: phaseID,
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
SessionID: sessionID,
|
SessionID: sessionID,
|
||||||
WorkspaceID: workspaceID,
|
WorkspaceID: workspaceID,
|
||||||
Upstream: upstream,
|
}
|
||||||
|
|
||||||
|
raw := err.Error()
|
||||||
|
if !containsInternalLeak(raw) {
|
||||||
|
info.Upstream = truncateRunes(raw, 80)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@@ -57,9 +60,20 @@ func ClassifyError(err error, phaseID, requestID, sessionID, workspaceID string)
|
|||||||
info.Actions = []string{"restart"}
|
info.Actions = []string{"restart"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info.Message = info.UserMessage
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func containsInternalLeak(s string) bool {
|
||||||
|
low := strings.ToLower(s)
|
||||||
|
for _, n := range []string{"sql", "pq:", "token", "password", "/home/", "prompt"} {
|
||||||
|
if strings.Contains(low, n) || strings.Contains(s, n) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func isTimeoutError(msg string) bool {
|
func isTimeoutError(msg string) bool {
|
||||||
return strings.Contains(msg, "timeout") ||
|
return strings.Contains(msg, "timeout") ||
|
||||||
strings.Contains(msg, "deadline") ||
|
strings.Contains(msg, "deadline") ||
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package stream
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,6 +24,16 @@ func TestClassifyError_TimeoutIsTransient(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClassifyError_DoesNotLeakSQL(t *testing.T) {
|
||||||
|
info := ClassifyError(errors.New("pq: password authentication failed /home/weichen/x"), "dialogue_generating", "req_x", "sess_1", "")
|
||||||
|
if strings.Contains(info.Upstream, "pq:") || strings.Contains(info.Upstream, "/home/") || strings.Contains(info.UserMessage, "pq:") {
|
||||||
|
t.Fatalf("%+v", info)
|
||||||
|
}
|
||||||
|
if info.Message != info.UserMessage {
|
||||||
|
t.Fatal("message alias")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestClassifyError_GenericIsFatal(t *testing.T) {
|
func TestClassifyError_GenericIsFatal(t *testing.T) {
|
||||||
info := ClassifyError(errors.New("invalid project config"), "intent_recognizing", "req_2", "sess_1", "")
|
info := ClassifyError(errors.New("invalid project config"), "intent_recognizing", "req_2", "sess_1", "")
|
||||||
if info.Class != ErrorClassFatal {
|
if info.Class != ErrorClassFatal {
|
||||||
|
|||||||
+31
-30
@@ -23,18 +23,18 @@ const (
|
|||||||
|
|
||||||
// GovernancePayload 文档治理生命周期与认领变更载荷
|
// GovernancePayload 文档治理生命周期与认领变更载荷
|
||||||
type GovernancePayload struct {
|
type GovernancePayload struct {
|
||||||
DocID string `json:"doc_id"`
|
DocID string `json:"doc_id"`
|
||||||
Action string `json:"action"` // claimed, proposal_required, proposal_decided, submitted_review, rejected, approved
|
Action string `json:"action"` // claimed, proposal_required, proposal_decided, submitted_review, rejected, approved
|
||||||
CurrentState string `json:"current_state"` // drafting, reviewing, approving, approved
|
CurrentState string `json:"current_state"` // drafting, reviewing, approving, approved
|
||||||
IsLocked bool `json:"is_locked"`
|
IsLocked bool `json:"is_locked"`
|
||||||
AuthorClaimed bool `json:"author_claimed"`
|
AuthorClaimed bool `json:"author_claimed"`
|
||||||
ProposalID string `json:"proposal_id,omitempty"`
|
ProposalID string `json:"proposal_id,omitempty"`
|
||||||
ProposedChanges string `json:"proposed_changes,omitempty"`
|
ProposedChanges string `json:"proposed_changes,omitempty"`
|
||||||
ImpactSummary string `json:"impact_summary,omitempty"`
|
ImpactSummary string `json:"impact_summary,omitempty"`
|
||||||
RiskSummary string `json:"risk_summary,omitempty"`
|
RiskSummary string `json:"risk_summary,omitempty"`
|
||||||
AuthorDecision string `json:"author_decision,omitempty"`
|
AuthorDecision string `json:"author_decision,omitempty"`
|
||||||
NotificationTitle string `json:"notification_title"` // 用户前台高亮简报标题
|
NotificationTitle string `json:"notification_title"` // 用户前台高亮简报标题
|
||||||
NotificationMsg string `json:"notification_msg"` // 用户前台高亮简报内容
|
NotificationMsg string `json:"notification_msg"` // 用户前台高亮简报内容
|
||||||
}
|
}
|
||||||
|
|
||||||
// PhasePayload 阶段性任务状态更新载荷
|
// PhasePayload 阶段性任务状态更新载荷
|
||||||
@@ -66,12 +66,12 @@ type SummaryPayload struct {
|
|||||||
|
|
||||||
// DetailPayload 详细执行与推演过程载荷 (按需显示,默认折叠)
|
// DetailPayload 详细执行与推演过程载荷 (按需显示,默认折叠)
|
||||||
type DetailPayload struct {
|
type DetailPayload struct {
|
||||||
Category string `json:"category"` // agent_thought, step_output, tool_call, log
|
Category string `json:"category"` // agent_thought, step_output, tool_call, log
|
||||||
StepID string `json:"step_id,omitempty"` // 步骤标识 (如 tasks/01_regulatory)
|
StepID string `json:"step_id,omitempty"` // 步骤标识 (如 tasks/01_regulatory)
|
||||||
Role string `json:"role,omitempty"` // 对应角色
|
Role string `json:"role,omitempty"` // 对应角色
|
||||||
Title string `json:"title"` // 详情标题
|
Title string `json:"title"` // 详情标题
|
||||||
Content string `json:"content"` // 完整推演正文或技术草稿
|
Content string `json:"content"` // 完整推演正文或技术草稿
|
||||||
Collapsible bool `json:"collapsible"` // 客户端是否应当默认折叠显示
|
Collapsible bool `json:"collapsible"` // 客户端是否应当默认折叠显示
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeliverablePayload A轨正式合规文档交付载荷
|
// DeliverablePayload A轨正式合规文档交付载荷
|
||||||
@@ -117,6 +117,7 @@ const (
|
|||||||
|
|
||||||
type ErrorInfo struct {
|
type ErrorInfo struct {
|
||||||
UserMessage string `json:"user_message"`
|
UserMessage string `json:"user_message"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Class ErrorClass `json:"class"`
|
Class ErrorClass `json:"class"`
|
||||||
PhaseID string `json:"phase_id,omitempty"`
|
PhaseID string `json:"phase_id,omitempty"`
|
||||||
@@ -133,16 +134,16 @@ type StreamEvent struct {
|
|||||||
Type EventType `json:"type"`
|
Type EventType `json:"type"`
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
|
||||||
Phase *PhasePayload `json:"phase,omitempty"`
|
Phase *PhasePayload `json:"phase,omitempty"`
|
||||||
Status *StatusPayload `json:"status,omitempty"`
|
Status *StatusPayload `json:"status,omitempty"`
|
||||||
Summary *SummaryPayload `json:"summary,omitempty"`
|
Summary *SummaryPayload `json:"summary,omitempty"`
|
||||||
Briefing any `json:"briefing,omitempty"`
|
Briefing any `json:"briefing,omitempty"`
|
||||||
Detail *DetailPayload `json:"detail,omitempty"`
|
Detail *DetailPayload `json:"detail,omitempty"`
|
||||||
Delta string `json:"delta,omitempty"`
|
Delta string `json:"delta,omitempty"`
|
||||||
Deliverable *DeliverablePayload `json:"deliverable,omitempty"`
|
Deliverable *DeliverablePayload `json:"deliverable,omitempty"`
|
||||||
Governance *GovernancePayload `json:"governance,omitempty"`
|
Governance *GovernancePayload `json:"governance,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
ErrorInfo *ErrorInfo `json:"error_info,omitempty"`
|
ErrorInfo *ErrorInfo `json:"error_info,omitempty"`
|
||||||
Turn *TurnAckPayload `json:"turn,omitempty"`
|
Turn *TurnAckPayload `json:"turn,omitempty"`
|
||||||
Done *DonePayload `json:"done,omitempty"`
|
Done *DonePayload `json:"done,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2323,7 +2323,7 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (httpStatus === 403) {
|
if (httpStatus === 403) {
|
||||||
if (code === 'ERR_FORBIDDEN' || detail.includes('无权访问该项目')) {
|
if (code === 'ERR_FORBIDDEN' || code === 'AUTHZ') {
|
||||||
return {
|
return {
|
||||||
userMessage: detail || '无权访问该项目,请确认登录账号与项目成员身份',
|
userMessage: detail || '无权访问该项目,请确认登录账号与项目成员身份',
|
||||||
code: code || 'ERR_FORBIDDEN',
|
code: code || 'ERR_FORBIDDEN',
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ describe('projectSettingsError', () => {
|
|||||||
assert.equal(body.issues[0]?.code, 'DUTY_PARTIAL_ASSIGNMENT');
|
assert.equal(body.issues[0]?.code, 'DUTY_PARTIAL_ASSIGNMENT');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prefers envelope code over legacy error field', () => {
|
||||||
|
const body = parseProjectSettingsErrorBody(
|
||||||
|
JSON.stringify({ code: 'AUTHZ', error: 'ERR_FORBIDDEN', message: '无权访问' }),
|
||||||
|
);
|
||||||
|
assert.equal(body.code, 'AUTHZ');
|
||||||
|
});
|
||||||
|
|
||||||
it('retains the local draft when a late tab hits PLAN_FROZEN', () => {
|
it('retains the local draft when a late tab hits PLAN_FROZEN', () => {
|
||||||
const draft = { workflow_definition_key: 'main', workflow_version: 1, stages: [] };
|
const draft = { workflow_definition_key: 'main', workflow_version: 1, stages: [] };
|
||||||
assert.equal(retainDraftOnFrozenConflict(draft), draft);
|
assert.equal(retainDraftOnFrozenConflict(draft), draft);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export function parseProjectSettingsErrorBody(text: string): {
|
|||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(text) as {
|
const parsed = JSON.parse(text) as {
|
||||||
error?: string;
|
error?: string;
|
||||||
|
code?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
issues?: ProjectSettingsIssue[];
|
issues?: ProjectSettingsIssue[];
|
||||||
};
|
};
|
||||||
@@ -46,7 +47,7 @@ export function parseProjectSettingsErrorBody(text: string): {
|
|||||||
.filter((msg): msg is string => Boolean(msg))
|
.filter((msg): msg is string => Boolean(msg))
|
||||||
.join(';');
|
.join(';');
|
||||||
return {
|
return {
|
||||||
code: String(parsed.error || ''),
|
code: String(parsed.code || parsed.error || ''),
|
||||||
message: issueText || parsed.message || parsed.error || text || '请求失败',
|
message: issueText || parsed.message || parsed.error || text || '请求失败',
|
||||||
issues,
|
issues,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ function bucketHasMessages(days: ChatDayBucket[]): boolean {
|
|||||||
function isForbiddenResponse(err: unknown): boolean {
|
function isForbiddenResponse(err: unknown): boolean {
|
||||||
if (!(err instanceof Error)) return false;
|
if (!(err instanceof Error)) return false;
|
||||||
const msg = err.message.toLowerCase();
|
const msg = err.message.toLowerCase();
|
||||||
return msg.includes('403') || msg.includes('forbidden') || msg.includes('无权');
|
return msg.includes('403') || msg.includes('forbidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChatWindow({
|
export function useChatWindow({
|
||||||
|
|||||||
Reference in New Issue
Block a user