Files
ai-gateway-go/internal/workbench/admin_http.go
T
LLMGuardX Dev 87c2b04174 0.11.3: 旗舰版第四轮完善(统一审批中心/工具治理/平台环境变量/数字员工入口/个人渠道/报表多维/租户配额)
- 统一审批中心:模型/资源/渠道/工具四类申请聚合审批,通过自动开通
  (marketplace 安装/渠道授权),outbox 双向站内信;门户可发起/撤回。
- 工具治理:rate_limit_rpm(固定窗口原子 upsert,多实例共享)+ approval_required
  (首次调用自动发起审批,批准前一律拒绝)。
- 平台环境变量:平台级注入 skill/MCP 运行时,个人可覆盖;系统管理员可写。
- 数字员工会话入口:门户列表/对话/调用记录,复用用户运行时凭据。
- 个人渠道:webhook 入站令牌 SHA-256 摘要 + constant-time 校验,绑定已批准
  模型,用量归属用户 Key。
- 报表多维:工具调用/审批授权/安全事件三组统计端点与页面。
- 租户配额:部门 Key/月 Token 上限,运行时凭据开通强制校验,概览展示用量。
- 迁移 000042-000045;修复渠道空 API Key NOT NULL 违约与 inet 扫描;
  25 包测试通过,前后端构建通过,端到端验证完成。
2026-08-13 13:41:22 +08:00

821 lines
26 KiB
Go

package workbench
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"aigateway.local/core/internal/identity"
"aigateway.local/core/internal/platform/apiresponse"
)
type AdminHTTPHandler struct {
service *Service
tools *ToolService
notifications *NotificationService
identity *identity.Service
mux *http.ServeMux
}
func NewAdminHTTPHandler(service *Service, tools *ToolService, notifications *NotificationService, identityService *identity.Service) *AdminHTTPHandler {
h := &AdminHTTPHandler{service: service, tools: tools, notifications: notifications, identity: identityService, mux: http.NewServeMux()}
h.mux.HandleFunc("GET /api/v1/admin/prompt-categories", h.listPromptCategories)
h.mux.HandleFunc("POST /api/v1/admin/prompt-categories", h.createPromptCategory)
h.mux.HandleFunc("DELETE /api/v1/admin/prompt-categories/{id}", h.deletePromptCategory)
h.mux.HandleFunc("GET /api/v1/admin/prompts", h.listPrompts)
h.mux.HandleFunc("POST /api/v1/admin/prompts", h.createPrompt)
h.mux.HandleFunc("GET /api/v1/admin/prompts/{id}", h.getPrompt)
h.mux.HandleFunc("PUT /api/v1/admin/prompts/{id}", h.updatePrompt)
h.mux.HandleFunc("DELETE /api/v1/admin/prompts/{id}", h.deletePrompt)
h.mux.HandleFunc("GET /api/v1/admin/prompts/{id}/versions", h.listPromptVersions)
h.mux.HandleFunc("POST /api/v1/admin/prompts/{id}/versions", h.addPromptVersion)
h.mux.HandleFunc("POST /api/v1/admin/prompts/{id}/versions/{version}/activate", h.activatePromptVersion)
h.mux.HandleFunc("POST /api/v1/admin/prompts/{id}/render", h.previewPrompt)
h.mux.HandleFunc("GET /api/v1/admin/knowledge-bases", h.listKnowledgeBases)
h.mux.HandleFunc("POST /api/v1/admin/knowledge-bases", h.createKnowledgeBase)
h.mux.HandleFunc("PUT /api/v1/admin/knowledge-bases/{id}", h.updateKnowledgeBase)
h.mux.HandleFunc("DELETE /api/v1/admin/knowledge-bases/{id}", h.deleteKnowledgeBase)
h.mux.HandleFunc("GET /api/v1/admin/knowledge-bases/{id}/documents", h.listDocuments)
h.mux.HandleFunc("POST /api/v1/admin/knowledge-bases/{id}/documents", h.addDocument)
h.mux.HandleFunc("DELETE /api/v1/admin/knowledge-bases/{id}/documents/{document_id}", h.deleteDocument)
h.mux.HandleFunc("POST /api/v1/admin/knowledge-bases/{id}/documents/{document_id}/reprocess", h.reprocessDocument)
h.mux.HandleFunc("POST /api/v1/admin/knowledge-bases/{id}/search", h.searchKnowledge)
h.mux.HandleFunc("GET /api/v1/admin/tools", h.listTools)
h.mux.HandleFunc("POST /api/v1/admin/tools", h.createTool)
h.mux.HandleFunc("PUT /api/v1/admin/tools/{id}", h.updateTool)
h.mux.HandleFunc("DELETE /api/v1/admin/tools/{id}", h.deleteTool)
h.mux.HandleFunc("POST /api/v1/admin/tools/{id}/test", h.testTool)
h.mux.HandleFunc("GET /api/v1/admin/tool-approvals", h.listToolApprovals)
h.mux.HandleFunc("POST /api/v1/admin/tool-approvals/{id}/decide", h.decideToolApproval)
h.mux.HandleFunc("GET /api/v1/admin/applications", h.listApplications)
h.mux.HandleFunc("POST /api/v1/admin/applications", h.createApplication)
h.mux.HandleFunc("GET /api/v1/admin/applications/catalog", h.applicationCatalog)
h.mux.HandleFunc("GET /api/v1/admin/applications/asset-dependencies", h.applicationDependencies)
h.mux.HandleFunc("GET /api/v1/admin/applications/{id}", h.getApplication)
h.mux.HandleFunc("PUT /api/v1/admin/applications/{id}", h.updateApplication)
h.mux.HandleFunc("DELETE /api/v1/admin/applications/{id}", h.deleteApplication)
h.mux.HandleFunc("POST /api/v1/admin/applications/{id}/publish", h.publishApplication)
h.mux.HandleFunc("POST /api/v1/admin/applications/{id}/rollback/{version}", h.rollbackApplication)
h.mux.HandleFunc("GET /api/v1/admin/applications/{id}/versions", h.listApplicationVersions)
h.mux.HandleFunc("GET /api/v1/admin/applications/{id}/runs", h.listApplicationRuns)
h.mux.HandleFunc("GET /api/v1/admin/notification-channels", h.listChannels)
h.mux.HandleFunc("POST /api/v1/admin/notification-channels", h.createChannel)
h.mux.HandleFunc("PUT /api/v1/admin/notification-channels/{id}", h.updateChannel)
h.mux.HandleFunc("DELETE /api/v1/admin/notification-channels/{id}", h.deleteChannel)
h.mux.HandleFunc("GET /api/v1/admin/notification-deliveries", h.listDeliveries)
h.mux.HandleFunc("POST /api/v1/admin/notification-deliveries/{id}/retry", h.retryDelivery)
return h
}
func (h *AdminHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mux.ServeHTTP(w, r) }
func (h *AdminHTTPHandler) require(w http.ResponseWriter, r *http.Request, permission string) (identity.Account, bool) {
account, err := h.identity.Authenticate(r.Context(), identity.KindAdmin, r.Header.Get("Authorization"))
if err != nil {
apiresponse.Error(w, 401, "登录状态无效")
return account, false
}
if !identity.HasPermission(account, permission) {
apiresponse.Error(w, 403, "缺少 AI 资产管理权限")
return account, false
}
return account, true
}
func decodeAsset(w http.ResponseWriter, r *http.Request, target any) bool {
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 3<<20))
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
apiresponse.Error(w, 400, "请求格式无效")
return false
}
return true
}
func assetError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, ErrNotFound):
apiresponse.Error(w, 404, "资源不存在")
case errors.Is(err, ErrConflict):
apiresponse.Error(w, 409, "资源仍被已发布或草稿应用引用")
default:
apiresponse.Error(w, 400, err.Error())
}
}
func (h *AdminHTTPHandler) listPromptCategories(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionPromptRead); !ok {
return
}
items, err := h.service.ListPromptCategories(r.Context())
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, items)
}
func (h *AdminHTTPHandler) createPromptCategory(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionPromptManage)
if !ok {
return
}
var input struct {
Name string `json:"name"`
Description string `json:"description"`
}
if !decodeAsset(w, r, &input) {
return
}
item, err := h.service.CreatePromptCategory(r.Context(), input.Name, input.Description, a.ID)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) deletePromptCategory(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionPromptManage)
if !ok {
return
}
if err := h.service.DeletePromptCategory(r.Context(), r.PathValue("id"), a.ID); err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, map[string]bool{"deleted": true})
}
type promptPayload struct {
Name string `json:"name"`
Description string `json:"description"`
CategoryID *string `json:"category_id"`
Tags []string `json:"tags"`
DepartmentIDs []string `json:"department_ids"`
Enabled bool `json:"enabled"`
Content string `json:"content"`
Variables []Variable `json:"variables"`
ChangeNote string `json:"change_note"`
}
func promptInput(p promptPayload) PromptInput {
return PromptInput{Name: p.Name, Description: p.Description, CategoryID: p.CategoryID, Tags: p.Tags, DepartmentIDs: p.DepartmentIDs, Enabled: p.Enabled, Content: p.Content, Variables: p.Variables, ChangeNote: p.ChangeNote}
}
func (h *AdminHTTPHandler) listPrompts(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionPromptRead); !ok {
return
}
items, err := h.service.ListPrompts(r.Context())
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, items)
}
func (h *AdminHTTPHandler) getPrompt(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionPromptRead); !ok {
return
}
item, err := h.service.GetPrompt(r.Context(), r.PathValue("id"))
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) createPrompt(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionPromptManage)
if !ok {
return
}
var p promptPayload
if !decodeAsset(w, r, &p) {
return
}
item, err := h.service.CreatePrompt(r.Context(), promptInput(p), a.ID)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) updatePrompt(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionPromptManage)
if !ok {
return
}
var p promptPayload
if !decodeAsset(w, r, &p) {
return
}
item, err := h.service.UpdatePrompt(r.Context(), r.PathValue("id"), promptInput(p), a.ID)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) deletePrompt(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionPromptManage)
if !ok {
return
}
if err := h.service.DeletePrompt(r.Context(), r.PathValue("id"), a.ID); err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, map[string]bool{"deleted": true})
}
func (h *AdminHTTPHandler) listPromptVersions(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionPromptRead); !ok {
return
}
items, err := h.service.ListPromptVersions(r.Context(), r.PathValue("id"))
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, items)
}
func (h *AdminHTTPHandler) addPromptVersion(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionPromptManage)
if !ok {
return
}
var p struct {
Content string `json:"content"`
Variables []Variable `json:"variables"`
ChangeNote string `json:"change_note"`
Activate bool `json:"activate"`
}
if !decodeAsset(w, r, &p) {
return
}
item, err := h.service.AddPromptVersion(r.Context(), r.PathValue("id"), p.Content, p.Variables, p.ChangeNote, a.ID, p.Activate)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) activatePromptVersion(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionPromptManage)
if !ok {
return
}
version, err := strconv.Atoi(r.PathValue("version"))
if err != nil {
apiresponse.Error(w, 400, "版本号无效")
return
}
if err = h.service.ActivatePromptVersion(r.Context(), r.PathValue("id"), version, a.ID); err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, map[string]bool{"activated": true})
}
func (h *AdminHTTPHandler) previewPrompt(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionPromptRead); !ok {
return
}
var p struct {
Variables map[string]any `json:"variables"`
}
if !decodeAsset(w, r, &p) {
return
}
prompt, err := h.service.GetPrompt(r.Context(), r.PathValue("id"))
if err != nil {
assetError(w, err)
return
}
if prompt.Current == nil {
apiresponse.Error(w, 409, "Prompt 没有生效版本")
return
}
rendered, err := RenderPrompt(prompt.Current.Content, prompt.Current.Variables, p.Variables)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, map[string]any{"rendered": rendered, "version": prompt.Current.Version})
}
func (h *AdminHTTPHandler) listKnowledgeBases(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionKnowledgeRead); !ok {
return
}
items, err := h.service.ListKnowledgeBases(r.Context())
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, items)
}
func (h *AdminHTTPHandler) createKnowledgeBase(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionKnowledgeManage)
if !ok {
return
}
var k KnowledgeBase
if !decodeAsset(w, r, &k) {
return
}
item, err := h.service.SaveKnowledgeBase(r.Context(), k, a.ID, true)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) updateKnowledgeBase(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionKnowledgeManage)
if !ok {
return
}
var k KnowledgeBase
if !decodeAsset(w, r, &k) {
return
}
k.ID = r.PathValue("id")
item, err := h.service.SaveKnowledgeBase(r.Context(), k, a.ID, false)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) deleteKnowledgeBase(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionKnowledgeManage)
if !ok {
return
}
if err := h.service.DeleteKnowledgeBase(r.Context(), r.PathValue("id"), a.ID); err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, map[string]bool{"deleted": true})
}
func (h *AdminHTTPHandler) listDocuments(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionKnowledgeRead); !ok {
return
}
items, err := h.service.ListKnowledgeDocuments(r.Context(), r.PathValue("id"))
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, items)
}
func (h *AdminHTTPHandler) addDocument(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionKnowledgeManage)
if !ok {
return
}
var p struct {
Title string `json:"title"`
SourceType string `json:"source_type"`
SourceURI string `json:"source_uri"`
Content string `json:"content"`
}
if !decodeAsset(w, r, &p) {
return
}
item, err := h.service.AddKnowledgeDocument(r.Context(), r.PathValue("id"), p.Title, p.SourceType, p.SourceURI, p.Content, a.ID)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) deleteDocument(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionKnowledgeManage)
if !ok {
return
}
if err := h.service.DeleteKnowledgeDocument(r.Context(), r.PathValue("id"), r.PathValue("document_id"), a.ID); err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, map[string]bool{"deleted": true})
}
func (h *AdminHTTPHandler) reprocessDocument(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionKnowledgeManage)
if !ok {
return
}
item, err := h.service.ReprocessKnowledgeDocument(r.Context(), r.PathValue("id"), r.PathValue("document_id"), a.ID)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) searchKnowledge(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionKnowledgeRead); !ok {
return
}
var p struct {
Query string `json:"query"`
TopK int `json:"top_k"`
}
if !decodeAsset(w, r, &p) {
return
}
items, err := NewRetriever(h.service, h.service.Embedder()).Search(r.Context(), r.PathValue("id"), p.Query, p.TopK)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, items)
}
type toolPayload struct {
Code string `json:"code"`
Name string `json:"name"`
Description string `json:"description"`
EndpointURL string `json:"endpoint_url"`
HTTPMethod string `json:"http_method"`
Headers *map[string]string `json:"headers"`
InputSchema json.RawMessage `json:"input_schema"`
TimeoutSeconds int `json:"timeout_seconds"`
DepartmentIDs []string `json:"department_ids"`
RateLimitRPM int `json:"rate_limit_rpm"`
ApprovalReq bool `json:"approval_required"`
Enabled bool `json:"enabled"`
}
func toolInput(p toolPayload, create bool) ToolInput {
var headers map[string]string
if p.Headers != nil {
headers = *p.Headers
} else if create {
headers = map[string]string{}
}
return ToolInput{Code: p.Code, Name: p.Name, Description: p.Description, EndpointURL: p.EndpointURL, HTTPMethod: p.HTTPMethod, Headers: headers, InputSchema: p.InputSchema, TimeoutSeconds: p.TimeoutSeconds, DepartmentIDs: p.DepartmentIDs, RateLimitRPM: p.RateLimitRPM, ApprovalRequired: p.ApprovalReq, Enabled: p.Enabled}
}
// listToolApprovals 工具审批申请列表(治理中心)。
func (h *AdminHTTPHandler) listToolApprovals(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionToolRead); !ok {
return
}
items, err := h.tools.ListApprovalRequests(r.Context(), r.URL.Query().Get("status"))
if err != nil {
apiresponse.Error(w, http.StatusServiceUnavailable, "工具审批查询失败")
return
}
apiresponse.OK(w, items)
}
// decideToolApproval 审批工具申请(通过后工具可调用)。
func (h *AdminHTTPHandler) decideToolApproval(w http.ResponseWriter, r *http.Request) {
admin, ok := h.require(w, r, identity.PermissionToolManage)
if !ok {
return
}
var input struct {
Status string `json:"status"`
Note string `json:"note"`
}
if !decodeAsset(w, r, &input) {
return
}
if err := h.tools.DecideApprovalRequest(r.Context(), r.PathValue("id"), input.Status, input.Note, admin.ID); err != nil {
apiresponse.Error(w, http.StatusBadRequest, err.Error())
return
}
apiresponse.OK(w, map[string]bool{"decided": true})
}
func (h *AdminHTTPHandler) listTools(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionToolRead); !ok {
return
}
items, err := h.tools.List(r.Context())
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, items)
}
func (h *AdminHTTPHandler) createTool(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionToolManage)
if !ok {
return
}
var p toolPayload
if !decodeAsset(w, r, &p) {
return
}
item, err := h.tools.Save(r.Context(), "", toolInput(p, true), a.ID, true)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) updateTool(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionToolManage)
if !ok {
return
}
var p toolPayload
if !decodeAsset(w, r, &p) {
return
}
item, err := h.tools.Save(r.Context(), r.PathValue("id"), toolInput(p, false), a.ID, false)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) deleteTool(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionToolManage)
if !ok {
return
}
if err := h.tools.Delete(r.Context(), r.PathValue("id"), a.ID); err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, map[string]bool{"deleted": true})
}
func (h *AdminHTTPHandler) testTool(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionToolManage); !ok {
return
}
var p struct {
Input map[string]any `json:"input"`
}
if !decodeAsset(w, r, &p) {
return
}
tool, err := h.tools.Get(r.Context(), r.PathValue("id"))
if err == nil {
result, executeErr := h.tools.Execute(r.Context(), tool, p.Input, "", "admin-test")
err = executeErr
if err == nil {
apiresponse.OK(w, result)
return
}
}
assetError(w, err)
}
func (h *AdminHTTPHandler) listApplications(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionApplicationRead); !ok {
return
}
items, err := h.service.ListApplications(r.Context())
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, items)
}
func (h *AdminHTTPHandler) getApplication(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionApplicationRead); !ok {
return
}
item, err := h.service.GetApplication(r.Context(), r.PathValue("id"))
if err != nil {
assetError(w, err)
return
}
versions, err := h.service.ListApplicationVersions(r.Context(), item.ID)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, map[string]any{"application": item, "versions": versions})
}
func (h *AdminHTTPHandler) applicationCatalog(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionApplicationRead); !ok {
return
}
prompts, err := h.service.ListPrompts(r.Context())
if err != nil {
assetError(w, err)
return
}
knowledge, err := h.service.ListKnowledgeBases(r.Context())
if err != nil {
assetError(w, err)
return
}
tools, err := h.tools.List(r.Context())
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, map[string]any{"prompts": prompts, "knowledge_bases": knowledge, "tools": tools})
}
func (h *AdminHTTPHandler) applicationDependencies(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionApplicationRead); !ok {
return
}
assetType := strings.TrimSpace(r.URL.Query().Get("asset_type"))
assetID := strings.TrimSpace(r.URL.Query().Get("asset_id"))
if assetID == "" || (assetType != "prompt" && assetType != "knowledge" && assetType != "tool") {
apiresponse.Error(w, 400, "asset_type 或 asset_id 无效")
return
}
items, err := h.service.ApplicationDependencies(r.Context(), assetType, assetID)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, items)
}
func (h *AdminHTTPHandler) createApplication(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionApplicationManage)
if !ok {
return
}
var app Application
if !decodeAsset(w, r, &app) {
return
}
item, err := h.service.SaveApplication(r.Context(), app, a.ID, true)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) updateApplication(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionApplicationManage)
if !ok {
return
}
var app Application
if !decodeAsset(w, r, &app) {
return
}
app.ID = r.PathValue("id")
item, err := h.service.SaveApplication(r.Context(), app, a.ID, false)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) deleteApplication(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionApplicationManage)
if !ok {
return
}
if err := h.service.DeleteApplication(r.Context(), r.PathValue("id"), a.ID); err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, map[string]bool{"deleted": true})
}
func (h *AdminHTTPHandler) publishApplication(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionApplicationManage)
if !ok {
return
}
var p struct {
ChangeNote string `json:"change_note"`
}
if !decodeAsset(w, r, &p) {
return
}
app, err := h.service.PublishApplication(r.Context(), r.PathValue("id"), p.ChangeNote, a.ID)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, app)
}
func (h *AdminHTTPHandler) rollbackApplication(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionApplicationManage)
if !ok {
return
}
version, err := strconv.Atoi(r.PathValue("version"))
if err != nil || version < 1 {
apiresponse.Error(w, 400, "版本号无效")
return
}
var input struct {
ChangeNote string `json:"change_note"`
}
if !decodeAsset(w, r, &input) {
return
}
item, err := h.service.RollbackApplication(r.Context(), r.PathValue("id"), version, input.ChangeNote, a.ID)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) listApplicationVersions(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionApplicationRead); !ok {
return
}
items, err := h.service.ListApplicationVersions(r.Context(), r.PathValue("id"))
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, items)
}
func (h *AdminHTTPHandler) listApplicationRuns(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionApplicationRead); !ok {
return
}
items, err := h.service.ListApplicationRuns(r.Context(), r.PathValue("id"), 100)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, items)
}
type channelPayload struct {
Name string `json:"name"`
WebhookURL string `json:"webhook_url"`
SigningSecret *string `json:"signing_secret"`
EventPatterns []string `json:"event_patterns"`
Enabled bool `json:"enabled"`
}
func (h *AdminHTTPHandler) listChannels(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionNotificationRead); !ok {
return
}
items, err := h.notifications.ListChannels(r.Context())
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, items)
}
func (h *AdminHTTPHandler) createChannel(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionNotificationManage)
if !ok {
return
}
var p channelPayload
if !decodeAsset(w, r, &p) {
return
}
item, err := h.notifications.SaveChannel(r.Context(), "", NotificationInput{Name: p.Name, WebhookURL: p.WebhookURL, SigningSecret: p.SigningSecret, EventPatterns: p.EventPatterns, Enabled: p.Enabled}, a.ID, true)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) updateChannel(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionNotificationManage)
if !ok {
return
}
var p channelPayload
if !decodeAsset(w, r, &p) {
return
}
item, err := h.notifications.SaveChannel(r.Context(), r.PathValue("id"), NotificationInput{Name: p.Name, WebhookURL: p.WebhookURL, SigningSecret: p.SigningSecret, EventPatterns: p.EventPatterns, Enabled: p.Enabled}, a.ID, false)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, item)
}
func (h *AdminHTTPHandler) deleteChannel(w http.ResponseWriter, r *http.Request) {
a, ok := h.require(w, r, identity.PermissionNotificationManage)
if !ok {
return
}
if err := h.notifications.DeleteChannel(r.Context(), r.PathValue("id"), a.ID); err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, map[string]bool{"deleted": true})
}
func (h *AdminHTTPHandler) listDeliveries(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionNotificationRead); !ok {
return
}
items, err := h.notifications.ListDeliveries(r.Context(), 100)
if err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, items)
}
func (h *AdminHTTPHandler) retryDelivery(w http.ResponseWriter, r *http.Request) {
if _, ok := h.require(w, r, identity.PermissionNotificationManage); !ok {
return
}
if err := h.notifications.RetryDelivery(r.Context(), r.PathValue("id")); err != nil {
assetError(w, err)
return
}
apiresponse.OK(w, map[string]bool{"retried": true})
}
var _ = strings.TrimSpace