feat(monitoring): decouple AI platforms from media_platforms and expand catalog to 6

Add ai_platforms table + shared catalog (yuanbao/kimi/wenxin/deepseek/doubao/qwen),
drop FKs from platform_accounts and desktop_tasks to media_platforms, and wire
target_account_id + platform into DesktopTaskEvent so the desktop runtime no
longer has to infer them from local state. Desktop client gains generic AI page
detection for binding, drops stale-business-date monitor tasks at the edge, and
refactors AccountsView/AiPlatformsView around the shared catalog. Admin tracking
view surfaces per-platform sampling status cards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 15:40:18 +08:00
parent 25dad49ed3
commit 09295d11a1
21 changed files with 2073 additions and 843 deletions
@@ -145,7 +145,9 @@ func (s *DesktopAccountService) Upsert(ctx context.Context, client *repository.D
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrConflict(40982, "desktop_account_sync_conflict", "desktop account sync version conflict")
}
return nil, response.ErrInternal(50083, "desktop_account_upsert_failed", "failed to upsert desktop account")
appErr := response.ErrInternal(50083, "desktop_account_upsert_failed", "failed to upsert desktop account")
appErr.Cause = err
return nil, appErr
}
view := s.buildDesktopAccountView(account, nil, nil)
@@ -9,14 +9,16 @@ import (
)
type DesktopTaskEvent struct {
Type string `json:"type"`
TaskID string `json:"task_id"`
JobID string `json:"job_id"`
WorkspaceID int64 `json:"workspace_id"`
TargetClientID string `json:"target_client_id"`
Status string `json:"status"`
Kind string `json:"kind"`
UpdatedAt time.Time `json:"updated_at"`
Type string `json:"type"`
TaskID string `json:"task_id"`
JobID string `json:"job_id"`
WorkspaceID int64 `json:"workspace_id"`
TargetAccountID string `json:"target_account_id"`
TargetClientID string `json:"target_client_id"`
Platform string `json:"platform"`
Status string `json:"status"`
Kind string `json:"kind"`
UpdatedAt time.Time `json:"updated_at"`
}
func publishDesktopTaskEvent(ctx context.Context, client *rabbitmq.Client, event DesktopTaskEvent) error {
@@ -726,14 +726,16 @@ func (s *DesktopTaskService) publishTaskEvent(ctx context.Context, task *reposit
}
err := publishDesktopTaskEvent(ctx, s.messaging, DesktopTaskEvent{
Type: eventType,
TaskID: task.DesktopID.String(),
JobID: task.JobID.String(),
WorkspaceID: task.WorkspaceID,
TargetClientID: task.TargetClientID.String(),
Status: task.Status,
Kind: task.Kind,
UpdatedAt: task.UpdatedAt,
Type: eventType,
TaskID: task.DesktopID.String(),
JobID: task.JobID.String(),
WorkspaceID: task.WorkspaceID,
TargetAccountID: task.TargetAccountID.String(),
TargetClientID: task.TargetClientID.String(),
Platform: task.Platform,
Status: task.Status,
Kind: task.Kind,
UpdatedAt: task.UpdatedAt,
})
if err != nil {
s.logWarn("desktop task event publish failed", err, zap.String("task_id", task.DesktopID.String()), zap.String("event_type", eventType))
@@ -134,7 +134,7 @@ func (s *MonitoringCallbackService) rebuildBrandDailyProjection(ctx context.Cont
}
func loadMonitoringProjectionPlatforms(ctx context.Context, businessPool *pgxpool.Pool, tenantID int64) ([]monitoringPlatformMetadata, error) {
enabled := []string{"deepseek", "qwen", "doubao"}
enabled := []string{"yuanbao", "kimi", "wenxin", "deepseek", "doubao", "qwen"}
var enabledJSON []byte
if err := businessPool.QueryRow(ctx, `
@@ -242,17 +242,21 @@ type monitoringConfiguredQuestion struct {
}
var defaultMonitoringPlatforms = []monitoringPlatformMetadata{
{ID: "yuanbao", Name: "混元 / 元宝"},
{ID: "kimi", Name: "Kimi"},
{ID: "wenxin", Name: "文心一言"},
{ID: "deepseek", Name: "DeepSeek"},
{ID: "qwen", Name: "通义千问"},
{ID: "doubao", Name: "豆包"},
{ID: "qwen", Name: "通义千问"},
}
var monitoringPlatformNames = map[string]string{
"deepseek": "DeepSeek",
"qwen": "通义千问",
"doubao": "豆包",
"kimi": "Kimi",
"yuanbao": "腾讯元宝",
"qwen": "通义千问",
"wenxin": "文心一言",
"yuanbao": "混元 / 元宝",
}
func (s *MonitoringService) DashboardComposite(
@@ -891,7 +895,7 @@ func (s *MonitoringService) ensureCollectNowTasks(
func (s *MonitoringService) loadQuota(ctx context.Context, tenantID int64) (monitoringQuotaConfig, error) {
cfg := monitoringQuotaConfig{
CollectionMode: monitoringCollectorType,
EnabledPlatforms: []string{"deepseek", "qwen", "doubao"},
EnabledPlatforms: []string{"yuanbao", "kimi", "wenxin", "deepseek", "doubao", "qwen"},
}
var enabledJSON []byte
@@ -214,14 +214,16 @@ func (s *PublishJobService) Create(ctx context.Context, actor auth.Actor, req Cr
for _, task := range createdTasks {
if publishErr := publishDesktopTaskEvent(context.Background(), s.messaging, DesktopTaskEvent{
Type: "task_available",
TaskID: task.DesktopID.String(),
JobID: task.JobID.String(),
WorkspaceID: task.WorkspaceID,
TargetClientID: task.TargetClientID.String(),
Status: task.Status,
Kind: task.Kind,
UpdatedAt: task.UpdatedAt,
Type: "task_available",
TaskID: task.DesktopID.String(),
JobID: task.JobID.String(),
WorkspaceID: task.WorkspaceID,
TargetAccountID: task.TargetAccountID.String(),
TargetClientID: task.TargetClientID.String(),
Platform: task.Platform,
Status: task.Status,
Kind: task.Kind,
UpdatedAt: task.UpdatedAt,
}); publishErr != nil {
s.logWarn("desktop task available event publish failed", publishErr, zap.String("task_id", task.DesktopID.String()))
}
@@ -0,0 +1,17 @@
DELETE FROM desktop_tasks
WHERE platform_id IN ('yuanbao', 'kimi', 'wenxin', 'deepseek', 'doubao', 'qwen');
DELETE FROM platform_accounts
WHERE platform_id IN ('yuanbao', 'kimi', 'wenxin', 'deepseek', 'doubao', 'qwen');
ALTER TABLE platform_accounts
ADD CONSTRAINT platform_accounts_platform_id_fkey
FOREIGN KEY (platform_id) REFERENCES media_platforms(platform_id);
ALTER TABLE desktop_tasks
ADD CONSTRAINT desktop_tasks_platform_id_fkey
FOREIGN KEY (platform_id) REFERENCES media_platforms(platform_id);
DROP INDEX IF EXISTS idx_ai_platforms_status_sort;
DROP INDEX IF EXISTS uk_ai_platforms_platform_id;
DROP TABLE IF EXISTS ai_platforms;
@@ -0,0 +1,50 @@
CREATE TABLE IF NOT EXISTS ai_platforms (
id BIGSERIAL PRIMARY KEY,
ai_platform_id VARCHAR(64) NOT NULL,
name VARCHAR(100) NOT NULL,
short_name VARCHAR(16) NOT NULL,
accent_color VARCHAR(16) NOT NULL DEFAULT '#1677ff',
login_url TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'active',
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_ai_platforms_platform_id
ON ai_platforms(ai_platform_id);
CREATE INDEX IF NOT EXISTS idx_ai_platforms_status_sort
ON ai_platforms(status, sort_order, id);
INSERT INTO ai_platforms (
ai_platform_id,
name,
short_name,
accent_color,
login_url,
status,
sort_order
)
VALUES
('yuanbao', '混元 / 元宝', '', '#19bf6b', 'https://yuanbao.tencent.com/', 'active', 10),
('kimi', 'Kimi', 'K', '#111827', 'https://kimi.moonshot.cn/', 'active', 20),
('wenxin', '文心一言', '', '#20c6d7', 'https://yiyan.baidu.com/', 'active', 30),
('deepseek', 'DeepSeek', 'D', '#4468ff', 'https://chat.deepseek.com/', 'active', 40),
('doubao', '豆包', '', '#ff9348', 'https://www.doubao.com/', 'active', 50),
('qwen', '通义千问', '', '#276ef1', 'https://www.qianwen.com/', 'active', 60)
ON CONFLICT (ai_platform_id)
DO UPDATE SET
name = EXCLUDED.name,
short_name = EXCLUDED.short_name,
accent_color = EXCLUDED.accent_color,
login_url = EXCLUDED.login_url,
status = EXCLUDED.status,
sort_order = EXCLUDED.sort_order,
updated_at = NOW();
ALTER TABLE platform_accounts
DROP CONSTRAINT IF EXISTS platform_accounts_platform_id_fkey;
ALTER TABLE desktop_tasks
DROP CONSTRAINT IF EXISTS desktop_tasks_platform_id_fkey;