0.11.6: 渠道权限管控(部门范围 + 用户级授权)
- 迁移 000047:channels.department_ids(空=全局) + channel_grants 用户级授权 (source=manual/approval 区分来源)。 - 管理端:渠道部门范围配置 + 授权管理(列表/授予/撤销);渠道列表显示范围。 - 门户:我的渠道端点(/api/v1/portal/channels)按部门可见或明确授权返回, 「个人渠道」页新增可使用渠道区(授权方式标识)。 - 审批流:资源申请中的渠道类型通过后自动写 channel_grants(source=approval), 取代'批准记录即授权'的弱语义。 - 端到端验证:部门隔离(demo 无部门看不到)→手动授予→可见→撤销→不可见; 审批通过自动授权。修复 JOIN 列歧义与 uuid/text 比较。
This commit is contained in:
@@ -63,6 +63,8 @@ func NewHTTPHandler(service *Service, identityService *identity.Service) *HTTPHa
|
||||
h.mux.HandleFunc("POST /api/v1/portal/personal-channels", h.createPersonalChannel)
|
||||
h.mux.HandleFunc("POST /api/v1/portal/personal-channels/{id}/token", h.regeneratePersonalToken)
|
||||
h.mux.HandleFunc("DELETE /api/v1/portal/personal-channels/{id}", h.deletePersonalChannel)
|
||||
// 我的渠道:部门可见或已授权。
|
||||
h.mux.HandleFunc("GET /api/v1/portal/channels", h.myChannels)
|
||||
// 数字员工:会话入口 + 调用记录。
|
||||
h.mux.HandleFunc("GET /api/v1/portal/digital-employees", h.digitalEmployees)
|
||||
h.mux.HandleFunc("POST /api/v1/portal/digital-employees/{code}/chat", h.runDigitalEmployee)
|
||||
@@ -925,3 +927,31 @@ func (h *HTTPHandler) myEmployeeRuns(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
apiresponse.OK(w, items)
|
||||
}
|
||||
|
||||
// --- 我的渠道:部门可见或已授权 ---
|
||||
|
||||
func (h *HTTPHandler) myChannels(w http.ResponseWriter, r *http.Request) {
|
||||
a, ok := h.account(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if h.service.channels == nil {
|
||||
apiresponse.OK(w, map[string]any{"channels": []any{}, "granted_codes": []string{}})
|
||||
return
|
||||
}
|
||||
visible, err := h.service.channels.VisibleChannelsForUser(r.Context(), a.ID, a.DepartmentID)
|
||||
if err != nil {
|
||||
portalError(w, err)
|
||||
return
|
||||
}
|
||||
granted, err := h.service.channels.GrantsForUser(r.Context(), a.ID)
|
||||
if err != nil {
|
||||
portalError(w, err)
|
||||
return
|
||||
}
|
||||
grantedCodes := make([]string, 0, len(granted))
|
||||
for _, item := range granted {
|
||||
grantedCodes = append(grantedCodes, item.Code)
|
||||
}
|
||||
apiresponse.OK(w, map[string]any{"channels": visible, "granted_codes": grantedCodes})
|
||||
}
|
||||
|
||||
@@ -180,10 +180,26 @@ func (s *Service) DecideResourceRequest(ctx context.Context, id, status, note, a
|
||||
if err = tx.QueryRow(ctx, `SELECT portal_user_id::text,resource_type,resource_code FROM gateway.resource_access_requests WHERE id=$1`, id).Scan(&userID, &resourceType, &resourceCode); err != nil {
|
||||
return ResourceRequest{}, err
|
||||
}
|
||||
if status == "approved" && resourceType != "channel" && s.market != nil {
|
||||
// 自动安装到申请用户工作区(use 等级)。
|
||||
if _, err = s.market.Install(ctx, resourceType, resourceCode, userID, "use"); err != nil {
|
||||
return ResourceRequest{}, err
|
||||
if status == "approved" {
|
||||
switch resourceType {
|
||||
case "mcp_server", "skill", "digital_employee":
|
||||
if s.market != nil {
|
||||
// 自动安装到申请用户工作区(use 等级)。
|
||||
if _, err = s.market.Install(ctx, resourceType, resourceCode, userID, "use"); err != nil {
|
||||
return ResourceRequest{}, err
|
||||
}
|
||||
}
|
||||
case "channel":
|
||||
// 渠道审批通过 = 写入 channel_grants 用户级授权。
|
||||
if s.channels != nil {
|
||||
var channelID string
|
||||
if err = tx.QueryRow(ctx, `SELECT id::text FROM gateway.channels WHERE code=$1`, resourceCode).Scan(&channelID); err != nil {
|
||||
return ResourceRequest{}, err
|
||||
}
|
||||
if err = s.channels.Grant(ctx, channelID, userID, actorID, "approval"); err != nil {
|
||||
return ResourceRequest{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
payload, _ := json.Marshal(map[string]any{"request_id": id, "portal_user_id": userID, "resource_type": resourceType, "resource_code": resourceCode, "status": status, "actor_id": actorID})
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"aigateway.local/core/internal/channel"
|
||||
"aigateway.local/core/internal/identity"
|
||||
platformid "aigateway.local/core/internal/platform/id"
|
||||
"aigateway.local/core/internal/workbench"
|
||||
@@ -28,6 +29,7 @@ type Service struct {
|
||||
runtime http.Handler
|
||||
gateway http.Handler
|
||||
market *workbench.MarketplaceService
|
||||
channels *channel.Service
|
||||
}
|
||||
|
||||
func NewService(pool *pgxpool.Pool, assets *workbench.Service, tools *workbench.ToolService, identityService *identity.Service) *Service {
|
||||
@@ -46,6 +48,10 @@ func (s *Service) SetApplicationRuntime(credentials *RuntimeCredentials, runtime
|
||||
// /v1/chat/completions with the user's own runtime credential.
|
||||
func (s *Service) SetGateway(gateway http.Handler) { s.gateway = gateway }
|
||||
|
||||
// SetChannelService wires the channel service for approval auto-grant and the
|
||||
// portal "my channels" visibility endpoint.
|
||||
func (s *Service) SetChannelService(service *channel.Service) { s.channels = service }
|
||||
|
||||
// SetMarketplace wires the resource-marketplace service into the portal so the
|
||||
// marketplace pages can browse, install and manage resources.
|
||||
func (s *Service) SetMarketplace(market *workbench.MarketplaceService) {
|
||||
|
||||
Reference in New Issue
Block a user