0.11.2: 旗舰版第三轮完善(通用聊天/企微钉钉飞书扫码登录/个人安全策略)
- 门户通用聊天:选择已批准模型直接对话,审批通过后自动开通用户级运行时 API Key(加密落库,限额取批准值),聊天经受管网关统一认证/限流/配额/审计; 会话哈希链完整性 + busy 租约防并发,失败不落库。 - 扫码登录:identity_providers 扩展 wecom/dingtalk/feishu,管理端配置 (AppID/AppSecret/AgentID/回调/自动开户/默认部门),登录页自动展示; one-time state 防 CSRF,provider_uid 全局唯一防多账号绑定,平台端点 固定公网 URL 复用 public-only 拨号。 - 个人安全策略:账号安全页(登录设备管理/吊销非当前会话/登录提醒开关/ 扫码绑定解绑),登录成功发布 security.login_detected 事件按偏好落站内信 (新增 security 类别),会话索引只存令牌摘要并惰性清理。 - 迁移 000038-000041;修复 social update 参数越界/凭据回读/路由挂载缺失; 全量测试 25 包通过,前端 admin/portal 构建通过,端到端验证完成。
This commit is contained in:
@@ -70,6 +70,7 @@ type inboxDraft struct {
|
||||
UserID string // 直接收件人(从 payload 取),空串表示需额外解析
|
||||
AllAdmins bool // 收件人 = 全部启用管理员
|
||||
RequestUser bool // 收件人 = model_access_requests.portal_user_id(payload.request_id)
|
||||
NotifyPref bool // 收件人 = payload.portal_user_id,且其登录通知偏好开启
|
||||
}
|
||||
|
||||
func payloadValue(payload json.RawMessage, key string) string {
|
||||
@@ -115,12 +116,28 @@ func inboxPlan(eventType string, payload json.RawMessage) []inboxDraft {
|
||||
return []inboxDraft{{RecipientKind: "admin", Category: "task_result", Title: "定时任务已执行", Body: "定时任务 " + payloadValue(payload, "task_code") + " 已完成", Link: "/system/scheduled-tasks", UserID: payloadValue(payload, "actor_id")}}
|
||||
case "scheduled_task.failed":
|
||||
return []inboxDraft{{RecipientKind: "admin", Category: "task_result", Title: "定时任务执行失败", Body: "定时任务 " + payloadValue(payload, "task_code") + " 执行失败: " + payloadValue(payload, "error"), Link: "/system/scheduled-tasks", UserID: payloadValue(payload, "actor_id")}}
|
||||
case "security.login_detected":
|
||||
ip := payloadValue(payload, "ip")
|
||||
if ip == "" {
|
||||
ip = "未知地址"
|
||||
}
|
||||
return []inboxDraft{{RecipientKind: "portal", Category: "security", Title: "新设备登录提醒", Body: "你的账号刚刚从 " + ip + " 登录,如非本人操作请立即修改密码", Link: "/portal/security", UserID: payloadValue(payload, "portal_user_id"), NotifyPref: true}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboxService) resolveRecipients(ctx context.Context, draft inboxDraft, payload json.RawMessage) ([]string, error) {
|
||||
switch {
|
||||
case draft.UserID != "" && draft.NotifyPref:
|
||||
// 登录提醒:尊重账号的安全偏好(默认开启)。
|
||||
var notify bool
|
||||
if err := s.assets.pool.QueryRow(ctx, `SELECT COALESCE((SELECT login_notify FROM gateway.portal_security_prefs WHERE portal_user_id=$1),true)`, draft.UserID).Scan(¬ify); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !notify {
|
||||
return nil, nil
|
||||
}
|
||||
return []string{draft.UserID}, nil
|
||||
case draft.UserID != "":
|
||||
return []string{draft.UserID}, nil
|
||||
case draft.RequestUser:
|
||||
|
||||
@@ -26,6 +26,7 @@ func TestInboxPlanMapsEvents(t *testing.T) {
|
||||
wantUserID string
|
||||
wantAll bool
|
||||
wantRequest bool
|
||||
wantPref bool
|
||||
}{
|
||||
{name: "model_access.requested 通知全部管理员审批", eventType: "model_access.requested", values: map[string]any{"model": "gpt-5"}, wantKind: "admin", wantCategory: "approval", wantTitle: "新的模型访问申请", wantAll: true},
|
||||
{name: "model_access.decided 已批准回执给申请用户", eventType: "model_access.decided", values: map[string]any{"status": "approved"}, wantKind: "portal", wantCategory: "approval", wantTitle: "模型申请已处理", wantRequest: true},
|
||||
@@ -36,6 +37,7 @@ func TestInboxPlanMapsEvents(t *testing.T) {
|
||||
{name: "knowledge_document.embedding_failed 降级提示", eventType: "knowledge_document.embedding_failed", values: map[string]any{"actor_id": "22222222-2222-2222-2222-222222222222"}, wantKind: "admin", wantCategory: "system", wantTitle: "知识文档向量化失败", wantUserID: "22222222-2222-2222-2222-222222222222"},
|
||||
{name: "scheduled_task.completed 发给创建者", eventType: "scheduled_task.completed", values: map[string]any{"task_code": "daily-report", "actor_id": "33333333-3333-3333-3333-333333333333"}, wantKind: "admin", wantCategory: "task_result", wantTitle: "定时任务已执行", wantUserID: "33333333-3333-3333-3333-333333333333"},
|
||||
{name: "scheduled_task.failed 发给创建者", eventType: "scheduled_task.failed", values: map[string]any{"task_code": "daily-report", "error": "timeout", "actor_id": "33333333-3333-3333-3333-333333333333"}, wantKind: "admin", wantCategory: "task_result", wantTitle: "定时任务执行失败", wantUserID: "33333333-3333-3333-3333-333333333333"},
|
||||
{name: "security.login_detected 发登录提醒且受偏好约束", eventType: "security.login_detected", values: map[string]any{"portal_user_id": "44444444-4444-4444-4444-444444444444", "ip": "203.0.113.7"}, wantKind: "portal", wantCategory: "security", wantTitle: "新设备登录提醒", wantUserID: "44444444-4444-4444-4444-444444444444", wantPref: true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -63,6 +65,9 @@ func TestInboxPlanMapsEvents(t *testing.T) {
|
||||
if draft.RequestUser != tc.wantRequest {
|
||||
t.Errorf("request_user = %v, want %v", draft.RequestUser, tc.wantRequest)
|
||||
}
|
||||
if draft.NotifyPref != tc.wantPref {
|
||||
t.Errorf("notify_pref = %v, want %v", draft.NotifyPref, tc.wantPref)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user