refactor(tenant): drop legacy plugin_installations, migrate monitoring to desktop_clients
Hard cutover from the browser-extension plugin flow to desktop clients: remove plugin_installations/plugin_sessions tables and related service, handler, router, and generated model code; migrate monitoring quotas and collector types to desktop_clients (UUID primary_client_id); recreate platform_access_snapshots keyed by client_id; update dev-seed and callback types accordingly; mark legacy design docs as historical. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
# AI 品牌曝光监测系统技术方案 V4
|
# AI 品牌曝光监测系统技术方案 V4(旧版设计)
|
||||||
|
|
||||||
|
> 旧版设计说明:本文描述的是基于浏览器插件采集、`installation_token`、`/api/plugin/monitoring/*` 的旧监控方案。自 2026-04-20 起,当前实现已切换到 `desktop_clients` / desktop client 架构。本文保留用于工作记录与方案追溯,不再作为当前实现依据。
|
||||||
|
|
||||||
## 1. 文档信息
|
## 1. 文档信息
|
||||||
|
|
||||||
@@ -6,7 +8,7 @@
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 文档名称 | AI 品牌曝光监测系统技术方案 V4(插件采集 + 配额约束) |
|
| 文档名称 | AI 品牌曝光监测系统技术方案 V4(插件采集 + 配额约束) |
|
||||||
| 文档版本 | V4.0 |
|
| 文档版本 | V4.0 |
|
||||||
| 文档状态 | 待评审 |
|
| 文档状态 | 旧版设计(保留工作记录) |
|
||||||
| 创建日期 | 2026-04-08 |
|
| 创建日期 | 2026-04-08 |
|
||||||
| 基线文档 | `docs/ai-brand-monitoring-tech-design-v3.md` |
|
| 基线文档 | `docs/ai-brand-monitoring-tech-design-v3.md` |
|
||||||
| 适用范围 | 数据追踪模块全部页面 |
|
| 适用范围 | 数据追踪模块全部页面 |
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# AI 品牌曝光监测系统技术方案 V5
|
# AI 品牌曝光监测系统技术方案 V5(旧版设计)
|
||||||
|
|
||||||
|
> 旧版设计说明:本文描述的是基于浏览器插件采样、`plugin_installations`、`primary_installation_id`、`/api/plugin/monitoring/*` 的旧监控方案。自 2026-04-20 起,当前实现已切换到 `desktop_clients` / desktop client 架构。本文保留用于工作记录与方案追溯,不再作为当前实现依据。
|
||||||
|
|
||||||
## 1. 文档信息
|
## 1. 文档信息
|
||||||
|
|
||||||
@@ -6,7 +8,7 @@
|
|||||||
| -------- | --------------------------------------------------------------------- |
|
| -------- | --------------------------------------------------------------------- |
|
||||||
| 文档名称 | AI 品牌曝光监测系统技术方案 V5(插件采样首期交付 / API 精确采集预留) |
|
| 文档名称 | AI 品牌曝光监测系统技术方案 V5(插件采样首期交付 / API 精确采集预留) |
|
||||||
| 文档版本 | V5.9 |
|
| 文档版本 | V5.9 |
|
||||||
| 文档状态 | 待评审 |
|
| 文档状态 | 旧版设计(保留工作记录) |
|
||||||
| 创建日期 | 2026-04-09 |
|
| 创建日期 | 2026-04-09 |
|
||||||
| 基线文档 | `docs/ai-brand-monitoring-tech-design-v4.md` |
|
| 基线文档 | `docs/ai-brand-monitoring-tech-design-v4.md` |
|
||||||
| 适用范围 | 数据追踪模块全部页面 |
|
| 适用范围 | 数据追踪模块全部页面 |
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# GEO 双后台与后端技术架构方案 V1
|
# GEO 双后台与后端技术架构方案 V1
|
||||||
|
|
||||||
|
> 工作记录说明:本文仍保留作为历史架构记录。其中涉及浏览器插件执行链路、`/api/callback/plugin/*`、`plugin_sessions`、`installation_token` 的相关章节,自 2026-04-20 起视为旧版设计;当前实现请以 desktop client 方案和最新代码为准。
|
||||||
|
|
||||||
## 1. 文档信息
|
## 1. 文档信息
|
||||||
|
|
||||||
| 项目 | 内容 |
|
| 项目 | 内容 |
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# V1 技术方案:绑定媒体账号与手动发送文章
|
# V1 技术方案:绑定媒体账号与手动发送文章(旧版设计)
|
||||||
|
|
||||||
|
> 旧版设计说明:本文描述的是基于浏览器插件回调、`plugin_sessions`、`/api/callback/plugin/*` 的旧方案。自 2026-04-20 起,当前实现已切换到 desktop client 架构。本文保留用于工作记录与方案追溯,不再作为当前实现依据。
|
||||||
|
|
||||||
> 口径说明:本文的插件执行流程、接口路径与会话模型已同步到主架构文档 [geo-platform-ops-admin-tech-architecture-v1.md](D:/project/geo/docs/geo-platform-ops-admin-tech-architecture-v1.md) 的 `§13.8 浏览器插件执行面闭环`、`§13.6.3 插件回调与内部接口`、`§13.6.4 统一错误响应与错误码`。若两份文档存在任何冲突,以主架构文档为准。
|
> 口径说明:本文的插件执行流程、接口路径与会话模型已同步到主架构文档 [geo-platform-ops-admin-tech-architecture-v1.md](D:/project/geo/docs/geo-platform-ops-admin-tech-architecture-v1.md) 的 `§13.8 浏览器插件执行面闭环`、`§13.6.3 插件回调与内部接口`、`§13.6.4 统一错误响应与错误码`。若两份文档存在任何冲突,以主架构文档为准。
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# Media Publisher Extension Runtime V1
|
# Media Publisher Extension Runtime V1(旧版设计)
|
||||||
|
|
||||||
|
> 旧版设计说明:本文描述的是基于浏览器插件、`plugin_installations`、`plugin_sessions` 的旧执行方案。自 2026-04-20 起,当前实现已切换到 `desktop_clients` / desktop client 架构。本文保留用于工作记录与方案追溯,不再作为当前实现依据。
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 2026-04-20 Legacy Plugin Design Status
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
2026-04-20 这次代码与数据库清理,已经移除了旧浏览器插件链路中的 `plugin_installations`、`plugin_sessions` 及其主要业务引用,并将监控主链路切换到 `desktop_clients` / desktop client 体系。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
本次**不删除**历史设计文档。
|
||||||
|
|
||||||
|
保留原因:
|
||||||
|
|
||||||
|
- 作为架构演进的工作记录,方便回溯旧方案的边界、假设和历史实现背景
|
||||||
|
- 便于对照为什么当前实现从 browser extension 迁移到 desktop client
|
||||||
|
- 避免后续排查历史字段、旧迁移、旧接口时失去上下文
|
||||||
|
|
||||||
|
## 本次文档处理方式
|
||||||
|
|
||||||
|
对明显描述旧插件执行模型的文档,统一补充“旧版设计”标识;对仍有其他参考价值的综合架构文档,补充“相关章节为旧版设计”的说明,而不删除正文内容。
|
||||||
|
|
||||||
|
## 已标记文档
|
||||||
|
|
||||||
|
- `docs/media-publisher-extension-runtime-v1.md`
|
||||||
|
- `docs/media-binding-and-publish-v1.md`
|
||||||
|
- `docs/ai-brand-monitoring-tech-design-v4.md`
|
||||||
|
- `docs/ai-brand-monitoring-tech-design-v5.md`
|
||||||
|
- `docs/geo-platform-ops-admin-tech-architecture-v1.md`
|
||||||
|
|
||||||
|
## 当前口径
|
||||||
|
|
||||||
|
当前实现以 desktop client 架构为准,旧插件设计文档仅用于历史追溯,不再作为新开发或现状判断依据。
|
||||||
|
|
||||||
|
建议优先参考:
|
||||||
|
|
||||||
|
- `docs/superpowers/specs/2026-04-18-electron-desktop-client-design.md`
|
||||||
|
- 代码中的 `desktop_clients`、`/api/desktop/*`、监控与发布最新实现
|
||||||
+40
-32
@@ -11,11 +11,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
sharedauth "github.com/geo-platform/tenant-api/internal/shared/auth"
|
|
||||||
"github.com/geo-platform/tenant-api/internal/shared/config"
|
"github.com/geo-platform/tenant-api/internal/shared/config"
|
||||||
"github.com/geo-platform/tenant-api/internal/tenant/prompts"
|
"github.com/geo-platform/tenant-api/internal/tenant/prompts"
|
||||||
"github.com/geo-platform/tenant-api/internal/tenant/repository"
|
"github.com/geo-platform/tenant-api/internal/tenant/repository"
|
||||||
@@ -35,7 +35,7 @@ type seedState struct {
|
|||||||
QuestionSecondaryID int64
|
QuestionSecondaryID int64
|
||||||
QuestionTertiaryID int64
|
QuestionTertiaryID int64
|
||||||
ArticleID int64
|
ArticleID int64
|
||||||
PluginInstallationID int64
|
DesktopClientID uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
type monitoringRunSeed struct {
|
type monitoringRunSeed struct {
|
||||||
@@ -137,7 +137,7 @@ func main() {
|
|||||||
fmt.Printf(" Test User ID: %d (test@geo.local / Test@123)\n", state.TestUserID)
|
fmt.Printf(" Test User ID: %d (test@geo.local / Test@123)\n", state.TestUserID)
|
||||||
fmt.Printf(" Plan ID: %d (free)\n", state.PlanID)
|
fmt.Printf(" Plan ID: %d (free)\n", state.PlanID)
|
||||||
fmt.Printf(" Brand ID: %d (轻氧口腔)\n", state.BrandID)
|
fmt.Printf(" Brand ID: %d (轻氧口腔)\n", state.BrandID)
|
||||||
fmt.Printf(" Plugin Installation ID: %d\n", state.PluginInstallationID)
|
fmt.Printf(" Desktop Client ID: %s\n", state.DesktopClientID.String())
|
||||||
fmt.Println(" Templates: 4 platform presets")
|
fmt.Println(" Templates: 4 platform presets")
|
||||||
fmt.Println(" Monitoring: brand daily snapshots + question runs + citation facts")
|
fmt.Println(" Monitoring: brand daily snapshots + question runs + citation facts")
|
||||||
}
|
}
|
||||||
@@ -241,7 +241,7 @@ func seedBusinessData(ctx context.Context, pool *pgxpool.Pool, membershipCfg con
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
state.PluginInstallationID, err = ensurePluginInstallation(ctx, tx, state.TenantID, state.UserID)
|
state.DesktopClientID, err = ensureDesktopClient(ctx, tx, state.TenantID, state.WorkspaceID, state.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -641,47 +641,55 @@ func ensureArticle(ctx context.Context, tx pgx.Tx, tenantID int64) (int64, error
|
|||||||
return articleID, nil
|
return articleID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensurePluginInstallation(ctx context.Context, tx pgx.Tx, tenantID, userID int64) (int64, error) {
|
func ensureDesktopClient(ctx context.Context, tx pgx.Tx, tenantID, workspaceID, userID int64) (uuid.UUID, error) {
|
||||||
installationKey := "demo-monitoring-installation"
|
clientID := uuid.NewSHA1(uuid.NameSpaceOID, []byte("geo-rankly-demo-monitoring-client"))
|
||||||
tokenHash := sharedauth.HashToken("demo-monitoring-installation-token")
|
tokenHash := sha256.Sum256([]byte("demo-monitoring-client-token"))
|
||||||
|
|
||||||
var installationID int64
|
var ensuredID uuid.UUID
|
||||||
err := tx.QueryRow(ctx, `
|
err := tx.QueryRow(ctx, `
|
||||||
WITH ensured AS (
|
WITH ensured AS (
|
||||||
INSERT INTO plugin_installations (
|
INSERT INTO desktop_clients (
|
||||||
tenant_id, user_id, installation_key, installation_name, browser_name,
|
id, tenant_id, workspace_id, user_id, token_hash, device_name, os, cpu_arch, client_version, channel, last_seen_at
|
||||||
client_version, installation_token_hash, status, last_seen_at
|
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, $2, $3, 'GEO Monitoring Chrome', 'Chrome',
|
$1, $2, $3, $4, $5, 'GEO Monitoring Desktop', 'macOS', 'arm64', '0.1.0', 'stable', NOW()
|
||||||
'0.1.0', $4, 'active', NOW()
|
|
||||||
)
|
)
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT (id) DO UPDATE
|
||||||
|
SET tenant_id = EXCLUDED.tenant_id,
|
||||||
|
workspace_id = EXCLUDED.workspace_id,
|
||||||
|
user_id = EXCLUDED.user_id,
|
||||||
|
token_hash = EXCLUDED.token_hash,
|
||||||
|
device_name = EXCLUDED.device_name,
|
||||||
|
os = EXCLUDED.os,
|
||||||
|
cpu_arch = EXCLUDED.cpu_arch,
|
||||||
|
client_version = EXCLUDED.client_version,
|
||||||
|
channel = EXCLUDED.channel,
|
||||||
|
last_seen_at = NOW(),
|
||||||
|
revoked_at = NULL
|
||||||
RETURNING id
|
RETURNING id
|
||||||
)
|
)
|
||||||
SELECT id FROM ensured
|
SELECT id FROM ensured
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM plugin_installations
|
FROM desktop_clients
|
||||||
WHERE tenant_id = $1 AND installation_key = $3 AND deleted_at IS NULL
|
WHERE id = $1
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`, tenantID, userID, installationKey, tokenHash).Scan(&installationID)
|
`, clientID, tenantID, workspaceID, userID, tokenHash[:]).Scan(&ensuredID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, wrapSeedErr("insert plugin installation", err)
|
return uuid.Nil, wrapSeedErr("insert desktop client", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Exec(ctx, `
|
_, err = tx.Exec(ctx, `
|
||||||
UPDATE plugin_installations
|
UPDATE desktop_clients
|
||||||
SET status = 'active',
|
SET token_hash = $2,
|
||||||
installation_token_hash = $2,
|
|
||||||
last_seen_at = NOW(),
|
last_seen_at = NOW(),
|
||||||
updated_at = NOW()
|
revoked_at = NULL
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`, installationID, tokenHash)
|
`, ensuredID, tokenHash[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, wrapSeedErr("touch plugin installation", err)
|
return uuid.Nil, wrapSeedErr("touch desktop client", err)
|
||||||
}
|
}
|
||||||
return installationID, nil
|
return ensuredID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureMonitoringQuota(ctx context.Context, tx pgx.Tx, state *seedState) error {
|
func ensureMonitoringQuota(ctx context.Context, tx pgx.Tx, state *seedState) error {
|
||||||
@@ -689,20 +697,20 @@ func ensureMonitoringQuota(ctx context.Context, tx pgx.Tx, state *seedState) err
|
|||||||
_, err := tx.Exec(ctx, `
|
_, err := tx.Exec(ctx, `
|
||||||
INSERT INTO tenant_monitoring_quotas (
|
INSERT INTO tenant_monitoring_quotas (
|
||||||
tenant_id, workspace_id, max_brands, collection_mode, question_selection_mode, collect_frequency,
|
tenant_id, workspace_id, max_brands, collection_mode, question_selection_mode, collect_frequency,
|
||||||
enabled_platforms, primary_installation_id, task_daily_hard_cap, plan_tier
|
enabled_platforms, primary_client_id, task_daily_hard_cap, plan_tier
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, 1, 'plugin', 'best_effort_all', 'daily', $3::jsonb, $4, 36, 'free')
|
VALUES ($1, $2, 1, 'desktop', 'best_effort_all', 'daily', $3::jsonb, $4, 36, 'free')
|
||||||
ON CONFLICT (tenant_id) DO UPDATE
|
ON CONFLICT (tenant_id) DO UPDATE
|
||||||
SET workspace_id = COALESCE(tenant_monitoring_quotas.workspace_id, EXCLUDED.workspace_id),
|
SET workspace_id = EXCLUDED.workspace_id,
|
||||||
collection_mode = EXCLUDED.collection_mode,
|
collection_mode = EXCLUDED.collection_mode,
|
||||||
question_selection_mode = EXCLUDED.question_selection_mode,
|
question_selection_mode = EXCLUDED.question_selection_mode,
|
||||||
collect_frequency = EXCLUDED.collect_frequency,
|
collect_frequency = EXCLUDED.collect_frequency,
|
||||||
enabled_platforms = EXCLUDED.enabled_platforms,
|
enabled_platforms = EXCLUDED.enabled_platforms,
|
||||||
primary_installation_id = EXCLUDED.primary_installation_id,
|
primary_client_id = EXCLUDED.primary_client_id,
|
||||||
task_daily_hard_cap = EXCLUDED.task_daily_hard_cap,
|
task_daily_hard_cap = EXCLUDED.task_daily_hard_cap,
|
||||||
plan_tier = EXCLUDED.plan_tier,
|
plan_tier = EXCLUDED.plan_tier,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
`, state.TenantID, state.WorkspaceID, string(enabledPlatforms), state.PluginInstallationID)
|
`, state.TenantID, state.WorkspaceID, string(enabledPlatforms), state.DesktopClientID)
|
||||||
return wrapSeedErr("upsert monitoring quota", err)
|
return wrapSeedErr("upsert monitoring quota", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,11 +807,11 @@ func insertPlatformAccessSnapshots(ctx context.Context, tx pgx.Tx, state *seedSt
|
|||||||
for _, item := range entries {
|
for _, item := range entries {
|
||||||
if _, err := tx.Exec(ctx, `
|
if _, err := tx.Exec(ctx, `
|
||||||
INSERT INTO monitoring_platform_access_snapshots (
|
INSERT INTO monitoring_platform_access_snapshots (
|
||||||
tenant_id, installation_id, ai_platform_id, business_date, access_status,
|
tenant_id, client_id, ai_platform_id, business_date, access_status,
|
||||||
connected, accessible, detected_at, reason_code, reason_message
|
connected, accessible, detected_at, reason_code, reason_message
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4::date, $5, $6, $7, NOW(), $8, $9)
|
VALUES ($1, $2, $3, $4::date, $5, $6, $7, NOW(), $8, $9)
|
||||||
`, state.TenantID, state.PluginInstallationID, item.PlatformID, businessDate, item.AccessStatus, item.Connected, item.Accessible, item.ReasonCode, item.ReasonMessage); err != nil {
|
`, state.TenantID, state.DesktopClientID, item.PlatformID, businessDate, item.AccessStatus, item.Connected, item.Accessible, item.ReasonCode, item.ReasonMessage); err != nil {
|
||||||
return wrapSeedErr("insert platform access snapshot", err)
|
return wrapSeedErr("insert platform access snapshot", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -100,7 +100,7 @@ type MonitoringHeartbeatPlatform struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MonitoringHeartbeatResponse struct {
|
type MonitoringHeartbeatResponse struct {
|
||||||
InstallationID int64 `json:"installation_id"`
|
InstallationID string `json:"installation_id"`
|
||||||
BusinessDate string `json:"business_date"`
|
BusinessDate string `json:"business_date"`
|
||||||
UpdatedPlatformCount int `json:"updated_platform_count"`
|
UpdatedPlatformCount int `json:"updated_platform_count"`
|
||||||
ReconciledSkippedTaskCount int64 `json:"reconciled_skipped_task_count"`
|
ReconciledSkippedTaskCount int64 `json:"reconciled_skipped_task_count"`
|
||||||
@@ -166,8 +166,9 @@ type MonitoringSkipTaskResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type monitoringInstallationIdentity struct {
|
type monitoringInstallationIdentity struct {
|
||||||
ID int64
|
ID string
|
||||||
TenantID int64
|
TenantID int64
|
||||||
|
WorkspaceID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type monitoringPlatformAccessSnapshotState struct {
|
type monitoringPlatformAccessSnapshotState struct {
|
||||||
@@ -236,7 +237,7 @@ type monitoringTaskResultEnvelope struct {
|
|||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
TaskID int64 `json:"task_id"`
|
TaskID int64 `json:"task_id"`
|
||||||
TenantID int64 `json:"tenant_id"`
|
TenantID int64 `json:"tenant_id"`
|
||||||
InstallationID int64 `json:"installation_id"`
|
InstallationID string `json:"installation_id"`
|
||||||
ReceivedAt time.Time `json:"received_at"`
|
ReceivedAt time.Time `json:"received_at"`
|
||||||
QueueStatus string `json:"queue_status"`
|
QueueStatus string `json:"queue_status"`
|
||||||
QueueClaimedAt *time.Time `json:"queue_claimed_at"`
|
QueueClaimedAt *time.Time `json:"queue_claimed_at"`
|
||||||
@@ -256,7 +257,7 @@ type monitoringTaskResultQueueMetaPatch struct {
|
|||||||
|
|
||||||
type MonitoringTaskResultQueueMetaPatch = monitoringTaskResultQueueMetaPatch
|
type MonitoringTaskResultQueueMetaPatch = monitoringTaskResultQueueMetaPatch
|
||||||
|
|
||||||
func (s *MonitoringCallbackService) HandleTaskResult(ctx context.Context, installationID int64, installationToken string, taskID int64, req MonitoringTaskResultRequest) (*MonitoringTaskResultResponse, error) {
|
func (s *MonitoringCallbackService) HandleTaskResult(ctx context.Context, installationID string, installationToken string, taskID int64, req MonitoringTaskResultRequest) (*MonitoringTaskResultResponse, error) {
|
||||||
installation, err := s.authenticateInstallation(ctx, installationID, installationToken)
|
installation, err := s.authenticateInstallation(ctx, installationID, installationToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -268,10 +269,10 @@ func (s *MonitoringCallbackService) HandleTaskResult(ctx context.Context, instal
|
|||||||
}
|
}
|
||||||
|
|
||||||
if task.CollectorType != monitoringCollectorType {
|
if task.CollectorType != monitoringCollectorType {
|
||||||
return nil, response.ErrConflict(40941, "collector_type_mismatch", "task collector type is not plugin")
|
return nil, response.ErrConflict(40941, "collector_type_mismatch", "task collector type is not desktop")
|
||||||
}
|
}
|
||||||
|
|
||||||
if task.LeasedToExecutor.Valid && strings.TrimSpace(task.LeasedToExecutor.String) != "" && task.LeasedToExecutor.String != strconv.FormatInt(installation.ID, 10) {
|
if task.LeasedToExecutor.Valid && strings.TrimSpace(task.LeasedToExecutor.String) != "" && task.LeasedToExecutor.String != installation.ID {
|
||||||
return nil, response.ErrUnauthorized(40141, "lease_owner_mismatch", "task is leased to another installation")
|
return nil, response.ErrUnauthorized(40141, "lease_owner_mismatch", "task is leased to another installation")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,13 +357,13 @@ func (s *MonitoringCallbackService) HandleTaskResult(ctx context.Context, instal
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringCallbackService) Heartbeat(ctx context.Context, installationID int64, installationToken string, req MonitoringHeartbeatRequest) (*MonitoringHeartbeatResponse, error) {
|
func (s *MonitoringCallbackService) Heartbeat(ctx context.Context, installationID string, installationToken string, req MonitoringHeartbeatRequest) (*MonitoringHeartbeatResponse, error) {
|
||||||
installation, err := s.authenticateInstallation(ctx, installationID, installationToken)
|
installation, err := s.authenticateInstallation(ctx, installationID, installationToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
isPrimaryInstallation, err := s.resolveHeartbeatPrimaryInstallation(ctx, installation.TenantID, installation.ID)
|
isPrimaryInstallation, err := s.resolveHeartbeatPrimaryInstallation(ctx, installation.TenantID, installation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -442,7 +443,7 @@ func (s *MonitoringCallbackService) Heartbeat(ctx context.Context, installationI
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringCallbackService) LeaseTasks(ctx context.Context, installationID int64, installationToken string, req MonitoringLeaseTasksRequest) (*MonitoringLeaseTasksResponse, error) {
|
func (s *MonitoringCallbackService) LeaseTasks(ctx context.Context, installationID string, installationToken string, req MonitoringLeaseTasksRequest) (*MonitoringLeaseTasksResponse, error) {
|
||||||
installation, err := s.authenticateInstallation(ctx, installationID, installationToken)
|
installation, err := s.authenticateInstallation(ctx, installationID, installationToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -522,7 +523,7 @@ func (s *MonitoringCallbackService) LeaseTasks(ctx context.Context, installation
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringCallbackService) ResumeTasks(ctx context.Context, installationID int64, installationToken string, req MonitoringResumeTasksRequest) (*MonitoringResumeTasksResponse, error) {
|
func (s *MonitoringCallbackService) ResumeTasks(ctx context.Context, installationID string, installationToken string, req MonitoringResumeTasksRequest) (*MonitoringResumeTasksResponse, error) {
|
||||||
installation, err := s.authenticateInstallation(ctx, installationID, installationToken)
|
installation, err := s.authenticateInstallation(ctx, installationID, installationToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -601,7 +602,7 @@ func (s *MonitoringCallbackService) ResumeTasks(ctx context.Context, installatio
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringCallbackService) SkipTask(ctx context.Context, installationID int64, installationToken string, taskID int64, req MonitoringSkipTaskRequest) (*MonitoringSkipTaskResponse, error) {
|
func (s *MonitoringCallbackService) SkipTask(ctx context.Context, installationID string, installationToken string, taskID int64, req MonitoringSkipTaskRequest) (*MonitoringSkipTaskResponse, error) {
|
||||||
installation, err := s.authenticateInstallation(ctx, installationID, installationToken)
|
installation, err := s.authenticateInstallation(ctx, installationID, installationToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -704,48 +705,47 @@ func (s *MonitoringCallbackService) ProcessQueuedTaskResult(ctx context.Context,
|
|||||||
return s.processTaskResult(ctx, task, envelope.CallbackResult)
|
return s.processTaskResult(ctx, task, envelope.CallbackResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringCallbackService) authenticateInstallation(ctx context.Context, installationID int64, installationToken string) (*monitoringInstallationIdentity, error) {
|
func (s *MonitoringCallbackService) authenticateInstallation(ctx context.Context, installationID string, installationToken string) (*monitoringInstallationIdentity, error) {
|
||||||
tokenHash := sharedauth.HashToken(strings.TrimSpace(installationToken))
|
tokenHash := HashDesktopClientToken(strings.TrimSpace(installationToken))
|
||||||
|
|
||||||
row := &monitoringInstallationIdentity{}
|
row := &monitoringInstallationIdentity{}
|
||||||
if err := s.businessPool.QueryRow(ctx, `
|
if err := s.businessPool.QueryRow(ctx, `
|
||||||
SELECT id, tenant_id
|
SELECT id::text, tenant_id, workspace_id
|
||||||
FROM plugin_installations
|
FROM desktop_clients
|
||||||
WHERE id = $1
|
WHERE id = $1::uuid
|
||||||
AND installation_token_hash = $2
|
AND token_hash = $2
|
||||||
AND status = 'active'
|
AND revoked_at IS NULL
|
||||||
AND deleted_at IS NULL
|
`, strings.TrimSpace(installationID), tokenHash).Scan(&row.ID, &row.TenantID, &row.WorkspaceID); err != nil {
|
||||||
`, installationID, tokenHash).Scan(&row.ID, &row.TenantID); err != nil {
|
|
||||||
if err == pgx.ErrNoRows {
|
if err == pgx.ErrNoRows {
|
||||||
return nil, response.ErrUnauthorized(40141, "plugin_installation_unauthorized", "plugin installation is invalid or expired")
|
return nil, response.ErrUnauthorized(40141, "desktop_client_unauthorized", "desktop client is invalid or expired")
|
||||||
}
|
}
|
||||||
return nil, response.ErrInternal(50041, "plugin_installation_lookup_failed", "failed to validate plugin installation")
|
return nil, response.ErrInternal(50041, "desktop_client_lookup_failed", "failed to validate desktop client")
|
||||||
}
|
}
|
||||||
|
|
||||||
return row, nil
|
return row, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringCallbackService) resolveHeartbeatPrimaryInstallation(ctx context.Context, tenantID, installationID int64) (bool, error) {
|
func (s *MonitoringCallbackService) resolveHeartbeatPrimaryInstallation(ctx context.Context, tenantID int64, installation *monitoringInstallationIdentity) (bool, error) {
|
||||||
var preferred sql.NullInt64
|
var preferred sql.NullString
|
||||||
err := s.businessPool.QueryRow(ctx, `
|
err := s.businessPool.QueryRow(ctx, `
|
||||||
SELECT primary_installation_id
|
SELECT primary_client_id::text
|
||||||
FROM tenant_monitoring_quotas
|
FROM tenant_monitoring_quotas
|
||||||
WHERE tenant_id = $1
|
WHERE tenant_id = $1
|
||||||
`, tenantID).Scan(&preferred)
|
`, tenantID).Scan(&preferred)
|
||||||
if err != nil && err != pgx.ErrNoRows {
|
if err != nil && err != pgx.ErrNoRows {
|
||||||
return false, response.ErrInternal(50041, "query_failed", "failed to inspect primary monitoring installation")
|
return false, response.ErrInternal(50041, "query_failed", "failed to inspect primary monitoring desktop client")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !preferred.Valid || preferred.Int64 == installationID {
|
if !preferred.Valid || preferred.String == installation.ID {
|
||||||
if !preferred.Valid {
|
if !preferred.Valid {
|
||||||
if err := s.persistHeartbeatPrimaryInstallationID(ctx, tenantID, installationID); err != nil {
|
if err := s.persistHeartbeatPrimaryInstallationID(ctx, tenantID, installation); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
online, err := s.isMonitoringInstallationRecentlyOnline(ctx, tenantID, preferred.Int64)
|
online, err := s.isMonitoringInstallationRecentlyOnline(ctx, tenantID, installation.WorkspaceID, preferred.String)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -753,27 +753,27 @@ func (s *MonitoringCallbackService) resolveHeartbeatPrimaryInstallation(ctx cont
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.persistHeartbeatPrimaryInstallationID(ctx, tenantID, installationID); err != nil {
|
if err := s.persistHeartbeatPrimaryInstallationID(ctx, tenantID, installation); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringCallbackService) isMonitoringInstallationRecentlyOnline(ctx context.Context, tenantID, installationID int64) (bool, error) {
|
func (s *MonitoringCallbackService) isMonitoringInstallationRecentlyOnline(ctx context.Context, tenantID, workspaceID int64, installationID string) (bool, error) {
|
||||||
var lastSeenAt sql.NullTime
|
var lastSeenAt sql.NullTime
|
||||||
err := s.businessPool.QueryRow(ctx, `
|
err := s.businessPool.QueryRow(ctx, `
|
||||||
SELECT last_seen_at
|
SELECT last_seen_at
|
||||||
FROM plugin_installations
|
FROM desktop_clients
|
||||||
WHERE id = $1
|
WHERE id = $1::uuid
|
||||||
AND tenant_id = $2
|
AND tenant_id = $2
|
||||||
AND status = 'active'
|
AND workspace_id = $3
|
||||||
AND deleted_at IS NULL
|
AND revoked_at IS NULL
|
||||||
`, installationID, tenantID).Scan(&lastSeenAt)
|
`, strings.TrimSpace(installationID), tenantID, workspaceID).Scan(&lastSeenAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == pgx.ErrNoRows {
|
if err == pgx.ErrNoRows {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return false, response.ErrInternal(50041, "query_failed", "failed to inspect monitoring installation heartbeat")
|
return false, response.ErrInternal(50041, "query_failed", "failed to inspect monitoring desktop client heartbeat")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !lastSeenAt.Valid {
|
if !lastSeenAt.Valid {
|
||||||
@@ -782,22 +782,22 @@ func (s *MonitoringCallbackService) isMonitoringInstallationRecentlyOnline(ctx c
|
|||||||
return time.Since(lastSeenAt.Time) <= monitoringOnlineDuration, nil
|
return time.Since(lastSeenAt.Time) <= monitoringOnlineDuration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringCallbackService) persistHeartbeatPrimaryInstallationID(ctx context.Context, tenantID, installationID int64) error {
|
func (s *MonitoringCallbackService) persistHeartbeatPrimaryInstallationID(ctx context.Context, tenantID int64, installation *monitoringInstallationIdentity) error {
|
||||||
if _, err := s.businessPool.Exec(ctx, `
|
if _, err := s.businessPool.Exec(ctx, `
|
||||||
INSERT INTO tenant_monitoring_quotas (
|
INSERT INTO tenant_monitoring_quotas (
|
||||||
tenant_id, workspace_id, primary_installation_id
|
tenant_id, workspace_id, primary_client_id
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1,
|
$1,
|
||||||
(SELECT id FROM workspaces WHERE tenant_id = $1 AND is_default = TRUE LIMIT 1),
|
$2,
|
||||||
$2
|
$3::uuid
|
||||||
)
|
)
|
||||||
ON CONFLICT (tenant_id) DO UPDATE
|
ON CONFLICT (tenant_id) DO UPDATE
|
||||||
SET workspace_id = COALESCE(tenant_monitoring_quotas.workspace_id, EXCLUDED.workspace_id),
|
SET workspace_id = EXCLUDED.workspace_id,
|
||||||
primary_installation_id = EXCLUDED.primary_installation_id,
|
primary_client_id = EXCLUDED.primary_client_id,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
`, tenantID, installationID); err != nil {
|
`, tenantID, installation.WorkspaceID, installation.ID); err != nil {
|
||||||
return response.ErrInternal(50041, "update_failed", "failed to persist primary monitoring installation")
|
return response.ErrInternal(50041, "update_failed", "failed to persist primary monitoring desktop client")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1020,11 +1020,12 @@ func (s *MonitoringCallbackService) selectPendingLeaseTasks(
|
|||||||
func (s *MonitoringCallbackService) selectLeasedTasksForInstallation(
|
func (s *MonitoringCallbackService) selectLeasedTasksForInstallation(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tx pgx.Tx,
|
tx pgx.Tx,
|
||||||
tenantID, installationID int64,
|
tenantID int64,
|
||||||
|
installationID string,
|
||||||
businessDate string,
|
businessDate string,
|
||||||
eligiblePlatformIDs []string,
|
eligiblePlatformIDs []string,
|
||||||
) ([]monitoringLeasedTaskRow, error) {
|
) ([]monitoringLeasedTaskRow, error) {
|
||||||
queryArgs := []interface{}{tenantID, monitoringCollectorType, businessDate, strconv.FormatInt(installationID, 10)}
|
queryArgs := []interface{}{tenantID, monitoringCollectorType, businessDate, installationID}
|
||||||
query := `
|
query := `
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
@@ -1108,7 +1109,7 @@ func (s *MonitoringCallbackService) selectLeasedTasksForInstallation(
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringCallbackService) markTaskLeased(ctx context.Context, tx pgx.Tx, installationID, taskID int64, leaseToken string, leasedAt, leaseExpiresAt time.Time) error {
|
func (s *MonitoringCallbackService) markTaskLeased(ctx context.Context, tx pgx.Tx, installationID string, taskID int64, leaseToken string, leasedAt, leaseExpiresAt time.Time) error {
|
||||||
if _, err := tx.Exec(ctx, `
|
if _, err := tx.Exec(ctx, `
|
||||||
UPDATE monitoring_collect_tasks
|
UPDATE monitoring_collect_tasks
|
||||||
SET status = 'leased',
|
SET status = 'leased',
|
||||||
@@ -1120,7 +1121,7 @@ func (s *MonitoringCallbackService) markTaskLeased(ctx context.Context, tx pgx.T
|
|||||||
error_message = NULL,
|
error_message = NULL,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`, taskID, sharedauth.HashToken(strings.TrimSpace(leaseToken)), strconv.FormatInt(installationID, 10), leasedAt, leaseExpiresAt); err != nil {
|
`, taskID, sharedauth.HashToken(strings.TrimSpace(leaseToken)), installationID, leasedAt, leaseExpiresAt); err != nil {
|
||||||
return response.ErrInternal(50041, "lease_update_failed", "failed to update monitoring lease task")
|
return response.ErrInternal(50041, "lease_update_failed", "failed to update monitoring lease task")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -1165,11 +1166,11 @@ func (s *MonitoringCallbackService) upsertPlatformAccessSnapshot(ctx context.Con
|
|||||||
|
|
||||||
if _, err := tx.Exec(ctx, `
|
if _, err := tx.Exec(ctx, `
|
||||||
INSERT INTO monitoring_platform_access_snapshots (
|
INSERT INTO monitoring_platform_access_snapshots (
|
||||||
tenant_id, installation_id, ai_platform_id, business_date, access_status,
|
tenant_id, client_id, ai_platform_id, business_date, access_status,
|
||||||
connected, accessible, detected_at, reason_code, reason_message
|
connected, accessible, detected_at, reason_code, reason_message
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4::date, $5, $6, $7, $8, $9, $10)
|
VALUES ($1, $2::uuid, $3, $4::date, $5, $6, $7, $8, $9, $10)
|
||||||
ON CONFLICT (tenant_id, installation_id, ai_platform_id, business_date)
|
ON CONFLICT (tenant_id, client_id, ai_platform_id, business_date)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
access_status = EXCLUDED.access_status,
|
access_status = EXCLUDED.access_status,
|
||||||
connected = EXCLUDED.connected,
|
connected = EXCLUDED.connected,
|
||||||
@@ -1195,7 +1196,7 @@ func (s *MonitoringCallbackService) upsertPlatformAccessSnapshot(ctx context.Con
|
|||||||
return hasPlatformAccessSnapshotChanged(previous, nextState), nil
|
return hasPlatformAccessSnapshotChanged(previous, nextState), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringCallbackService) loadPlatformAccessSnapshotState(ctx context.Context, tx pgx.Tx, tenantID, installationID int64, businessDate, aiPlatformID string) (*monitoringPlatformAccessSnapshotState, error) {
|
func (s *MonitoringCallbackService) loadPlatformAccessSnapshotState(ctx context.Context, tx pgx.Tx, tenantID int64, installationID string, businessDate, aiPlatformID string) (*monitoringPlatformAccessSnapshotState, error) {
|
||||||
item := &monitoringPlatformAccessSnapshotState{}
|
item := &monitoringPlatformAccessSnapshotState{}
|
||||||
var reasonCode sql.NullString
|
var reasonCode sql.NullString
|
||||||
var reasonMessage sql.NullString
|
var reasonMessage sql.NullString
|
||||||
@@ -1203,7 +1204,7 @@ func (s *MonitoringCallbackService) loadPlatformAccessSnapshotState(ctx context.
|
|||||||
SELECT access_status, connected, accessible, reason_code, reason_message
|
SELECT access_status, connected, accessible, reason_code, reason_message
|
||||||
FROM monitoring_platform_access_snapshots
|
FROM monitoring_platform_access_snapshots
|
||||||
WHERE tenant_id = $1
|
WHERE tenant_id = $1
|
||||||
AND installation_id = $2
|
AND client_id = $2::uuid
|
||||||
AND ai_platform_id = $3
|
AND ai_platform_id = $3
|
||||||
AND business_date = $4::date
|
AND business_date = $4::date
|
||||||
`, tenantID, installationID, aiPlatformID, businessDate).Scan(
|
`, tenantID, installationID, aiPlatformID, businessDate).Scan(
|
||||||
@@ -1721,11 +1722,11 @@ func (s *MonitoringCallbackService) loadExistingRunSummary(ctx context.Context,
|
|||||||
return &value, citationSourceCount, contentCitationCount, nil
|
return &value, citationSourceCount, contentCitationCount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringCallbackService) touchInstallation(ctx context.Context, installationID int64) error {
|
func (s *MonitoringCallbackService) touchInstallation(ctx context.Context, installationID string) error {
|
||||||
_, err := s.businessPool.Exec(ctx, `
|
_, err := s.businessPool.Exec(ctx, `
|
||||||
UPDATE plugin_installations
|
UPDATE desktop_clients
|
||||||
SET last_seen_at = NOW(), updated_at = NOW()
|
SET last_seen_at = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1::uuid
|
||||||
`, installationID)
|
`, installationID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1821,7 +1822,7 @@ func buildMonitoringRawPayload(req MonitoringTaskResultRequest) ([]byte, []Monit
|
|||||||
return data, sourceInputs
|
return data, sourceInputs
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMonitoringTaskResultEnvelope(task *monitoringCollectTask, installationID int64, receivedAt time.Time, req MonitoringTaskResultRequest) monitoringTaskResultEnvelope {
|
func newMonitoringTaskResultEnvelope(task *monitoringCollectTask, installationID string, receivedAt time.Time, req MonitoringTaskResultRequest) monitoringTaskResultEnvelope {
|
||||||
return monitoringTaskResultEnvelope{
|
return monitoringTaskResultEnvelope{
|
||||||
Version: "v1",
|
Version: "v1",
|
||||||
TaskID: task.ID,
|
TaskID: task.ID,
|
||||||
@@ -2090,8 +2091,8 @@ func normalizeMonitoringAccessStatus(accessStatus *string, connected *bool, acce
|
|||||||
return "", false, false, response.ErrBadRequest(40041, "invalid_access_status", "access status requires access_status, connected, or accessible")
|
return "", false, false, response.ErrBadRequest(40041, "invalid_access_status", "access status requires access_status, connected, or accessible")
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateTaskLease(task *monitoringCollectTask, installationID int64, leaseToken string) error {
|
func validateTaskLease(task *monitoringCollectTask, installationID string, leaseToken string) error {
|
||||||
if task.LeasedToExecutor.Valid && strings.TrimSpace(task.LeasedToExecutor.String) != "" && task.LeasedToExecutor.String != strconv.FormatInt(installationID, 10) {
|
if task.LeasedToExecutor.Valid && strings.TrimSpace(task.LeasedToExecutor.String) != "" && task.LeasedToExecutor.String != installationID {
|
||||||
return response.ErrUnauthorized(40141, "lease_owner_mismatch", "task is leased to another installation")
|
return response.ErrUnauthorized(40141, "lease_owner_mismatch", "task is leased to another installation")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,12 +48,12 @@ func TestNewMonitoringTaskResultEnvelope(t *testing.T) {
|
|||||||
TenantID: 34,
|
TenantID: 34,
|
||||||
}
|
}
|
||||||
|
|
||||||
envelope := newMonitoringTaskResultEnvelope(task, 56, receivedAt, req)
|
envelope := newMonitoringTaskResultEnvelope(task, "00000000-0000-0000-0000-000000000056", receivedAt, req)
|
||||||
|
|
||||||
assert.Equal(t, "v1", envelope.Version)
|
assert.Equal(t, "v1", envelope.Version)
|
||||||
assert.Equal(t, int64(12), envelope.TaskID)
|
assert.Equal(t, int64(12), envelope.TaskID)
|
||||||
assert.Equal(t, int64(34), envelope.TenantID)
|
assert.Equal(t, int64(34), envelope.TenantID)
|
||||||
assert.Equal(t, int64(56), envelope.InstallationID)
|
assert.Equal(t, "00000000-0000-0000-0000-000000000056", envelope.InstallationID)
|
||||||
assert.Equal(t, receivedAt, envelope.ReceivedAt)
|
assert.Equal(t, receivedAt, envelope.ReceivedAt)
|
||||||
assert.Equal(t, "pending", envelope.QueueStatus)
|
assert.Equal(t, "pending", envelope.QueueStatus)
|
||||||
assert.Nil(t, envelope.QueueEnqueuedAt)
|
assert.Nil(t, envelope.QueueEnqueuedAt)
|
||||||
@@ -69,7 +69,7 @@ func TestDecodeMonitoringTaskResultEnvelope(t *testing.T) {
|
|||||||
Version: "v1",
|
Version: "v1",
|
||||||
TaskID: 12,
|
TaskID: 12,
|
||||||
TenantID: 34,
|
TenantID: 34,
|
||||||
InstallationID: 56,
|
InstallationID: "00000000-0000-0000-0000-000000000056",
|
||||||
ReceivedAt: time.Date(2026, 4, 10, 12, 0, 0, 0, time.UTC),
|
ReceivedAt: time.Date(2026, 4, 10, 12, 0, 0, 0, time.UTC),
|
||||||
QueueStatus: "queued",
|
QueueStatus: "queued",
|
||||||
CallbackResult: MonitoringTaskResultRequest{LeaseToken: "lease-token", Status: "succeeded"},
|
CallbackResult: MonitoringTaskResultRequest{LeaseToken: "lease-token", Status: "succeeded"},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
defaultTrackingDays = 7
|
defaultTrackingDays = 7
|
||||||
maxTrackingDays = 30
|
maxTrackingDays = 30
|
||||||
monitoringCollectorType = "plugin"
|
monitoringCollectorType = "desktop"
|
||||||
monitoringOnlineDuration = 20 * time.Minute
|
monitoringOnlineDuration = 20 * time.Minute
|
||||||
monitoringCollectNowQuestionLimit = 5
|
monitoringCollectNowQuestionLimit = 5
|
||||||
)
|
)
|
||||||
@@ -190,7 +191,7 @@ type MonitoringCollectNowResponse struct {
|
|||||||
|
|
||||||
type monitoringQuotaConfig struct {
|
type monitoringQuotaConfig struct {
|
||||||
CollectionMode string
|
CollectionMode string
|
||||||
PrimaryInstallationID *int64
|
PrimaryClientID *uuid.UUID
|
||||||
EnabledPlatforms []string
|
EnabledPlatforms []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +302,7 @@ func (s *MonitoringService) DashboardComposite(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
accessStates, err := s.loadAccessStates(ctx, actor.TenantID, quota.PrimaryInstallationID, endDate)
|
accessStates, err := s.loadAccessStates(ctx, actor.TenantID, quota.PrimaryClientID, endDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -378,7 +379,7 @@ func (s *MonitoringService) QuestionDetail(ctx context.Context, brandID, questio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
accessStates, err := s.loadAccessStates(ctx, actor.TenantID, quota.PrimaryInstallationID, toDate)
|
accessStates, err := s.loadAccessStates(ctx, actor.TenantID, quota.PrimaryClientID, toDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -497,15 +498,15 @@ func (s *MonitoringService) CollectNow(ctx context.Context, brandID int64, keywo
|
|||||||
platforms := resolveMonitoringPlatforms(quota.EnabledPlatforms)
|
platforms := resolveMonitoringPlatforms(quota.EnabledPlatforms)
|
||||||
questionIDs := configuredQuestionIDs(configuredQuestions)
|
questionIDs := configuredQuestionIDs(configuredQuestions)
|
||||||
|
|
||||||
installationID, err := s.resolveCollectNowInstallationID(ctx, actor.TenantID, quota.PrimaryInstallationID)
|
clientID, err := s.resolveCollectNowClientID(ctx, actor.TenantID, actor.PrimaryWorkspaceID, quota.PrimaryClientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if installationID == nil {
|
if clientID == nil {
|
||||||
return nil, response.ErrConflict(40941, "installation_offline", "primary monitoring installation is offline")
|
return nil, response.ErrConflict(40941, "desktop_client_offline", "primary monitoring desktop client is offline")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.ensureInstallationOnline(ctx, actor.TenantID, *installationID); err != nil {
|
if err := s.ensureClientOnline(ctx, actor.TenantID, actor.PrimaryWorkspaceID, *clientID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,9 +585,9 @@ func (s *MonitoringService) CollectNow(ctx context.Context, brandID int64, keywo
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringService) resolveCollectNowInstallationID(ctx context.Context, tenantID int64, preferred *int64) (*int64, error) {
|
func (s *MonitoringService) resolveCollectNowClientID(ctx context.Context, tenantID, workspaceID int64, preferred *uuid.UUID) (*uuid.UUID, error) {
|
||||||
if preferred != nil {
|
if preferred != nil {
|
||||||
online, err := s.isInstallationOnline(ctx, tenantID, *preferred)
|
online, err := s.isClientOnline(ctx, tenantID, workspaceID, *preferred)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -595,7 +596,7 @@ func (s *MonitoringService) resolveCollectNowInstallationID(ctx context.Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fallbackID, err := s.findLatestOnlineInstallation(ctx, tenantID)
|
fallbackID, err := s.findLatestOnlineClient(ctx, tenantID, workspaceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -604,7 +605,7 @@ func (s *MonitoringService) resolveCollectNowInstallationID(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if preferred == nil || *preferred != *fallbackID {
|
if preferred == nil || *preferred != *fallbackID {
|
||||||
if err := s.persistPrimaryInstallationID(ctx, tenantID, *fallbackID); err != nil {
|
if err := s.persistPrimaryClientID(ctx, tenantID, workspaceID, *fallbackID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -612,44 +613,44 @@ func (s *MonitoringService) resolveCollectNowInstallationID(ctx context.Context,
|
|||||||
return fallbackID, nil
|
return fallbackID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringService) findLatestOnlineInstallation(ctx context.Context, tenantID int64) (*int64, error) {
|
func (s *MonitoringService) findLatestOnlineClient(ctx context.Context, tenantID, workspaceID int64) (*uuid.UUID, error) {
|
||||||
var installationID int64
|
var clientID uuid.UUID
|
||||||
err := s.businessPool.QueryRow(ctx, `
|
err := s.businessPool.QueryRow(ctx, `
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM plugin_installations
|
FROM desktop_clients
|
||||||
WHERE tenant_id = $1
|
WHERE tenant_id = $1
|
||||||
AND status = 'active'
|
AND workspace_id = $2
|
||||||
AND deleted_at IS NULL
|
AND revoked_at IS NULL
|
||||||
AND last_seen_at IS NOT NULL
|
AND last_seen_at IS NOT NULL
|
||||||
AND last_seen_at >= NOW() - $2::interval
|
AND last_seen_at >= NOW() - $3::interval
|
||||||
ORDER BY last_seen_at DESC, updated_at DESC, id DESC
|
ORDER BY last_seen_at DESC, created_at DESC, id DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`, tenantID, formatPgInterval(monitoringOnlineDuration)).Scan(&installationID)
|
`, tenantID, workspaceID, formatPgInterval(monitoringOnlineDuration)).Scan(&clientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == pgx.ErrNoRows {
|
if err == pgx.ErrNoRows {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, response.ErrInternal(50041, "query_failed", "failed to inspect online monitoring installation")
|
return nil, response.ErrInternal(50041, "query_failed", "failed to inspect online monitoring desktop client")
|
||||||
}
|
}
|
||||||
return &installationID, nil
|
return &clientID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringService) persistPrimaryInstallationID(ctx context.Context, tenantID, installationID int64) error {
|
func (s *MonitoringService) persistPrimaryClientID(ctx context.Context, tenantID, workspaceID int64, clientID uuid.UUID) error {
|
||||||
if _, err := s.businessPool.Exec(ctx, `
|
if _, err := s.businessPool.Exec(ctx, `
|
||||||
INSERT INTO tenant_monitoring_quotas (
|
INSERT INTO tenant_monitoring_quotas (
|
||||||
tenant_id, workspace_id, primary_installation_id
|
tenant_id, workspace_id, primary_client_id
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1,
|
$1,
|
||||||
(SELECT id FROM workspaces WHERE tenant_id = $1 AND is_default = TRUE LIMIT 1),
|
$2,
|
||||||
$2
|
$3
|
||||||
)
|
)
|
||||||
ON CONFLICT (tenant_id) DO UPDATE
|
ON CONFLICT (tenant_id) DO UPDATE
|
||||||
SET workspace_id = COALESCE(tenant_monitoring_quotas.workspace_id, EXCLUDED.workspace_id),
|
SET workspace_id = EXCLUDED.workspace_id,
|
||||||
primary_installation_id = EXCLUDED.primary_installation_id,
|
primary_client_id = EXCLUDED.primary_client_id,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
`, tenantID, installationID); err != nil {
|
`, tenantID, workspaceID, clientID); err != nil {
|
||||||
return response.ErrInternal(50041, "update_failed", "failed to persist primary monitoring installation")
|
return response.ErrInternal(50041, "update_failed", "failed to persist primary monitoring desktop client")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -894,12 +895,12 @@ func (s *MonitoringService) loadQuota(ctx context.Context, tenantID int64) (moni
|
|||||||
}
|
}
|
||||||
|
|
||||||
var enabledJSON []byte
|
var enabledJSON []byte
|
||||||
var installationID sql.NullInt64
|
var clientID uuid.NullUUID
|
||||||
err := s.businessPool.QueryRow(ctx, `
|
err := s.businessPool.QueryRow(ctx, `
|
||||||
SELECT collection_mode, enabled_platforms, primary_installation_id
|
SELECT collection_mode, enabled_platforms, primary_client_id
|
||||||
FROM tenant_monitoring_quotas
|
FROM tenant_monitoring_quotas
|
||||||
WHERE tenant_id = $1
|
WHERE tenant_id = $1
|
||||||
`, tenantID).Scan(&cfg.CollectionMode, &enabledJSON, &installationID)
|
`, tenantID).Scan(&cfg.CollectionMode, &enabledJSON, &clientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == pgx.ErrNoRows {
|
if err == pgx.ErrNoRows {
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
@@ -913,9 +914,9 @@ func (s *MonitoringService) loadQuota(ctx context.Context, tenantID int64) (moni
|
|||||||
cfg.EnabledPlatforms = enabled
|
cfg.EnabledPlatforms = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if installationID.Valid {
|
if clientID.Valid {
|
||||||
id := installationID.Int64
|
id := clientID.UUID
|
||||||
cfg.PrimaryInstallationID = &id
|
cfg.PrimaryClientID = &id
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(cfg.CollectionMode) == "" {
|
if strings.TrimSpace(cfg.CollectionMode) == "" {
|
||||||
cfg.CollectionMode = monitoringCollectorType
|
cfg.CollectionMode = monitoringCollectorType
|
||||||
@@ -1700,9 +1701,9 @@ func (s *MonitoringService) loadTimeBucketsForPlatform(
|
|||||||
return buckets, nil
|
return buckets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringService) loadAccessStates(ctx context.Context, tenantID int64, installationID *int64, businessDate time.Time) (map[string]monitoringAccessState, error) {
|
func (s *MonitoringService) loadAccessStates(ctx context.Context, tenantID int64, clientID *uuid.UUID, businessDate time.Time) (map[string]monitoringAccessState, error) {
|
||||||
states := map[string]monitoringAccessState{}
|
states := map[string]monitoringAccessState{}
|
||||||
if installationID == nil {
|
if clientID == nil {
|
||||||
return states, nil
|
return states, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1710,9 +1711,9 @@ func (s *MonitoringService) loadAccessStates(ctx context.Context, tenantID int64
|
|||||||
SELECT ai_platform_id, access_status
|
SELECT ai_platform_id, access_status
|
||||||
FROM monitoring_platform_access_snapshots
|
FROM monitoring_platform_access_snapshots
|
||||||
WHERE tenant_id = $1
|
WHERE tenant_id = $1
|
||||||
AND installation_id = $2
|
AND client_id = $2
|
||||||
AND business_date = $3::date
|
AND business_date = $3::date
|
||||||
`, tenantID, *installationID, businessDate.Format("2006-01-02"))
|
`, tenantID, *clientID, businessDate.Format("2006-01-02"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load platform access states")
|
return nil, response.ErrInternal(50041, "query_failed", "failed to load platform access states")
|
||||||
}
|
}
|
||||||
@@ -2452,32 +2453,32 @@ func (s *MonitoringService) loadQuestionContentCitations(ctx context.Context, te
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringService) ensureInstallationOnline(ctx context.Context, tenantID, installationID int64) error {
|
func (s *MonitoringService) ensureClientOnline(ctx context.Context, tenantID, workspaceID int64, clientID uuid.UUID) error {
|
||||||
online, err := s.isInstallationOnline(ctx, tenantID, installationID)
|
online, err := s.isClientOnline(ctx, tenantID, workspaceID, clientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !online {
|
if !online {
|
||||||
return response.ErrConflict(40941, "installation_offline", "primary monitoring installation is offline")
|
return response.ErrConflict(40941, "desktop_client_offline", "primary monitoring desktop client is offline")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitoringService) isInstallationOnline(ctx context.Context, tenantID, installationID int64) (bool, error) {
|
func (s *MonitoringService) isClientOnline(ctx context.Context, tenantID, workspaceID int64, clientID uuid.UUID) (bool, error) {
|
||||||
var lastSeenAt sql.NullTime
|
var lastSeenAt sql.NullTime
|
||||||
err := s.businessPool.QueryRow(ctx, `
|
err := s.businessPool.QueryRow(ctx, `
|
||||||
SELECT last_seen_at
|
SELECT last_seen_at
|
||||||
FROM plugin_installations
|
FROM desktop_clients
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND tenant_id = $2
|
AND tenant_id = $2
|
||||||
AND status = 'active'
|
AND workspace_id = $3
|
||||||
AND deleted_at IS NULL
|
AND revoked_at IS NULL
|
||||||
`, installationID, tenantID).Scan(&lastSeenAt)
|
`, clientID, tenantID, workspaceID).Scan(&lastSeenAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == pgx.ErrNoRows {
|
if err == pgx.ErrNoRows {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return false, response.ErrInternal(50041, "query_failed", "failed to inspect monitoring installation")
|
return false, response.ErrInternal(50041, "query_failed", "failed to inspect monitoring desktop client")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !lastSeenAt.Valid || time.Since(lastSeenAt.Time) > monitoringOnlineDuration {
|
if !lastSeenAt.Valid || time.Since(lastSeenAt.Time) > monitoringOnlineDuration {
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ type publishableArticleMeta struct {
|
|||||||
CoverAssetURL *string
|
CoverAssetURL *string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type platformAccountSeed struct {
|
||||||
|
ID int64
|
||||||
|
PlatformID string
|
||||||
|
PlatformName string
|
||||||
|
PlatformUID string
|
||||||
|
Nickname string
|
||||||
|
}
|
||||||
|
|
||||||
type desktopPublishSyncOutcome struct {
|
type desktopPublishSyncOutcome struct {
|
||||||
TenantID int64
|
TenantID int64
|
||||||
ArticleID int64
|
ArticleID int64
|
||||||
@@ -242,6 +250,65 @@ func publishBatchStatusToArticleStatus(batchStatus string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeStringPointer(value *string) *string {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
trimmed := strings.TrimSpace(*value)
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
func recalculatePublishBatchStatus(ctx context.Context, tx pgx.Tx, publishBatchID int64) (string, error) {
|
||||||
|
rows, err := tx.Query(ctx, `
|
||||||
|
SELECT status
|
||||||
|
FROM publish_records
|
||||||
|
WHERE publish_batch_id = $1
|
||||||
|
`, publishBatchID)
|
||||||
|
if err != nil {
|
||||||
|
return "", response.ErrInternal(50054, "publish_batch_status_query_failed", "failed to inspect publish batch status")
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
statuses := make([]string, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var status string
|
||||||
|
if scanErr := rows.Scan(&status); scanErr != nil {
|
||||||
|
return "", response.ErrInternal(50054, "publish_batch_status_scan_failed", "failed to parse publish batch status")
|
||||||
|
}
|
||||||
|
statuses = append(statuses, normalizePublishStatus(status))
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return "", response.ErrInternal(50054, "publish_batch_status_scan_failed", "failed to iterate publish batch status")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(statuses) == 0 {
|
||||||
|
return "publishing", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
counts := map[string]int{
|
||||||
|
"success": 0,
|
||||||
|
"failed": 0,
|
||||||
|
"publishing": 0,
|
||||||
|
}
|
||||||
|
for _, status := range statuses {
|
||||||
|
counts[normalizePublishStatus(status)]++
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case counts["publishing"] > 0:
|
||||||
|
return "publishing", nil
|
||||||
|
case counts["success"] > 0 && counts["failed"] > 0:
|
||||||
|
return "partial_success", nil
|
||||||
|
case counts["success"] > 0:
|
||||||
|
return "success", nil
|
||||||
|
default:
|
||||||
|
return "failed", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func resolvePublishArticleID(contentRef map[string]any) (int64, error) {
|
func resolvePublishArticleID(contentRef map[string]any) (int64, error) {
|
||||||
if articleID, ok := extractInt64FromMap(contentRef, "article_id", "id"); ok && articleID > 0 {
|
if articleID, ok := extractInt64FromMap(contentRef, "article_id", "id"); ok && articleID > 0 {
|
||||||
return articleID, nil
|
return articleID, nil
|
||||||
|
|||||||
@@ -484,40 +484,6 @@ type PlatformUserRole struct {
|
|||||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginInstallation struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
TenantID int64 `json:"tenant_id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
InstallationKey string `json:"installation_key"`
|
|
||||||
InstallationName string `json:"installation_name"`
|
|
||||||
BrowserName pgtype.Text `json:"browser_name"`
|
|
||||||
ClientVersion pgtype.Text `json:"client_version"`
|
|
||||||
InstallationTokenHash string `json:"installation_token_hash"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
LastSeenAt pgtype.Timestamptz `json:"last_seen_at"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PluginSession struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
TenantID int64 `json:"tenant_id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
PluginInstallationID pgtype.Int8 `json:"plugin_installation_id"`
|
|
||||||
ActionType string `json:"action_type"`
|
|
||||||
ResourceType pgtype.Text `json:"resource_type"`
|
|
||||||
ResourceID pgtype.Int8 `json:"resource_id"`
|
|
||||||
PlatformAccountID pgtype.Int8 `json:"platform_account_id"`
|
|
||||||
PlatformID string `json:"platform_id"`
|
|
||||||
SessionToken string `json:"session_token"`
|
|
||||||
ClientVersion pgtype.Text `json:"client_version"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
ExpireAt pgtype.Timestamptz `json:"expire_at"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PromptRule struct {
|
type PromptRule struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
TenantID int64 `json:"tenant_id"`
|
TenantID int64 `json:"tenant_id"`
|
||||||
@@ -681,7 +647,7 @@ type TenantMonitoringQuota struct {
|
|||||||
QuestionSelectionMode string `json:"question_selection_mode"`
|
QuestionSelectionMode string `json:"question_selection_mode"`
|
||||||
CollectFrequency string `json:"collect_frequency"`
|
CollectFrequency string `json:"collect_frequency"`
|
||||||
EnabledPlatforms []byte `json:"enabled_platforms"`
|
EnabledPlatforms []byte `json:"enabled_platforms"`
|
||||||
PrimaryInstallationID pgtype.Int8 `json:"primary_installation_id"`
|
PrimaryClientID pgtype.UUID `json:"primary_client_id"`
|
||||||
TaskDailyHardCap int32 `json:"task_daily_hard_cap"`
|
TaskDailyHardCap int32 `json:"task_daily_hard_cap"`
|
||||||
PlanTier string `json:"plan_tier"`
|
PlanTier string `json:"plan_tier"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"github.com/geo-platform/tenant-api/internal/bootstrap"
|
"github.com/geo-platform/tenant-api/internal/bootstrap"
|
||||||
"github.com/geo-platform/tenant-api/internal/shared/response"
|
"github.com/geo-platform/tenant-api/internal/shared/response"
|
||||||
@@ -13,13 +12,11 @@ import (
|
|||||||
|
|
||||||
type MediaHandler struct {
|
type MediaHandler struct {
|
||||||
svc *app.MediaService
|
svc *app.MediaService
|
||||||
logger *zap.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMediaHandler(a *bootstrap.App) *MediaHandler {
|
func NewMediaHandler(a *bootstrap.App) *MediaHandler {
|
||||||
return &MediaHandler{
|
return &MediaHandler{
|
||||||
svc: app.NewMediaService(a.DB, a.Logger).WithCache(a.Cache),
|
svc: app.NewMediaService(a.DB),
|
||||||
logger: a.Logger,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,75 +29,6 @@ func (h *MediaHandler) ListPlatforms(c *gin.Context) {
|
|||||||
response.Success(c, data)
|
response.Success(c, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *MediaHandler) ListPlatformAccounts(c *gin.Context) {
|
|
||||||
data, err := h.svc.ListPlatformAccounts(c.Request.Context())
|
|
||||||
if err != nil {
|
|
||||||
response.Error(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.Success(c, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MediaHandler) DeletePlatformAccount(c *gin.Context) {
|
|
||||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
response.Error(c, response.ErrBadRequest(40001, "invalid_id", "platform account id must be a number"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := h.svc.DeletePlatformAccount(c.Request.Context(), id); err != nil {
|
|
||||||
response.Error(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.Success(c, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MediaHandler) CreatePluginSession(c *gin.Context) {
|
|
||||||
var req app.CreatePluginSessionRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := h.svc.CreatePluginSession(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
response.Error(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.Success(c, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MediaHandler) RegisterPluginInstallation(c *gin.Context) {
|
|
||||||
var req app.RegisterPluginInstallationRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := h.svc.RegisterPluginInstallation(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
response.Error(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.Success(c, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MediaHandler) CreatePublishBatch(c *gin.Context) {
|
|
||||||
articleID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
response.Error(c, response.ErrBadRequest(40001, "invalid_id", "article id must be a number"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req app.CreatePublishBatchRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := h.svc.CreatePublishBatch(c.Request.Context(), articleID, req)
|
|
||||||
if err != nil {
|
|
||||||
response.Error(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.SuccessWithStatus(c, 201, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MediaHandler) ListPublishRecords(c *gin.Context) {
|
func (h *MediaHandler) ListPublishRecords(c *gin.Context) {
|
||||||
articleID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
articleID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -114,61 +42,3 @@ func (h *MediaHandler) ListPublishRecords(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
response.Success(c, data)
|
response.Success(c, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *MediaHandler) PluginBindCallback(c *gin.Context) {
|
|
||||||
var req app.PluginBindCallbackRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := h.svc.HandleBindCallback(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
if h.logger != nil {
|
|
||||||
appErr := response.Normalize(err)
|
|
||||||
h.logger.Warn("publisher bind callback failed",
|
|
||||||
zap.String("request_id", c.GetString("request_id")),
|
|
||||||
zap.Int64("plugin_session_id", req.PluginSessionID),
|
|
||||||
zap.String("platform_id", req.PlatformID),
|
|
||||||
zap.String("platform_uid", req.PlatformUID),
|
|
||||||
zap.Int("code", appErr.Code),
|
|
||||||
zap.String("message", appErr.Message),
|
|
||||||
zap.String("detail", appErr.Detail),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
response.Error(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.Success(c, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MediaHandler) PluginPublishCallback(c *gin.Context) {
|
|
||||||
var req app.PluginPublishCallbackRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := h.svc.HandlePublishCallback(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
if h.logger != nil {
|
|
||||||
appErr := response.Normalize(err)
|
|
||||||
h.logger.Warn("publisher publish callback failed",
|
|
||||||
zap.String("request_id", c.GetString("request_id")),
|
|
||||||
zap.Int64("plugin_session_id", req.PluginSessionID),
|
|
||||||
zap.Int64("publish_record_id", req.PublishRecordID),
|
|
||||||
zap.Int64("platform_account_id", req.PlatformAccountID),
|
|
||||||
zap.Int64("article_id", req.ArticleID),
|
|
||||||
zap.String("platform_id", req.PlatformID),
|
|
||||||
zap.String("status", req.Status),
|
|
||||||
zap.Stringp("error_message", req.ErrorMessage),
|
|
||||||
zap.Int("code", appErr.Code),
|
|
||||||
zap.String("message", appErr.Message),
|
|
||||||
zap.String("detail", appErr.Detail),
|
|
||||||
zap.Any("request_payload", req.RequestPayload),
|
|
||||||
zap.Any("response_payload", req.ResponsePayload),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
response.Error(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.Success(c, data)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ func RegisterRoutes(app *bootstrap.App) {
|
|||||||
publicAssets := NewAssetHandler(app)
|
publicAssets := NewAssetHandler(app)
|
||||||
app.Engine.GET("/api/public/assets/:token", publicAssets.Serve)
|
app.Engine.GET("/api/public/assets/:token", publicAssets.Serve)
|
||||||
|
|
||||||
pluginHandler := NewMediaHandler(app)
|
mediaHandler := NewMediaHandler(app)
|
||||||
callbacks := app.Engine.Group("/api/callback/plugin")
|
|
||||||
callbacks.POST("/bind", pluginHandler.PluginBindCallback)
|
|
||||||
callbacks.POST("/publish", pluginHandler.PluginPublishCallback)
|
|
||||||
|
|
||||||
protected := app.Engine.Group("/api")
|
protected := app.Engine.Group("/api")
|
||||||
protected.Use(auth.Middleware(app.JWT, app.Sessions))
|
protected.Use(auth.Middleware(app.JWT, app.Sessions))
|
||||||
@@ -65,7 +62,7 @@ func RegisterRoutes(app *bootstrap.App) {
|
|||||||
tenantProtected.POST("/publish-jobs", publishJobHandler.Create)
|
tenantProtected.POST("/publish-jobs", publishJobHandler.Create)
|
||||||
|
|
||||||
media := tenantProtected.Group("/media")
|
media := tenantProtected.Group("/media")
|
||||||
media.GET("/platforms", pluginHandler.ListPlatforms)
|
media.GET("/platforms", mediaHandler.ListPlatforms)
|
||||||
|
|
||||||
workspace := tenantProtected.Group("/workspace")
|
workspace := tenantProtected.Group("/workspace")
|
||||||
wsHandler := NewWorkspaceHandler(app)
|
wsHandler := NewWorkspaceHandler(app)
|
||||||
@@ -149,8 +146,7 @@ func RegisterRoutes(app *bootstrap.App) {
|
|||||||
articles.PUT("/:id", artHandler.Update)
|
articles.PUT("/:id", artHandler.Update)
|
||||||
articles.GET("/:id/stream", artHandler.Stream)
|
articles.GET("/:id/stream", artHandler.Stream)
|
||||||
articles.GET("/:id/versions", artHandler.Versions)
|
articles.GET("/:id/versions", artHandler.Versions)
|
||||||
articles.GET("/:id/publish-records", pluginHandler.ListPublishRecords)
|
articles.GET("/:id/publish-records", mediaHandler.ListPublishRecords)
|
||||||
articles.POST("/:id/publish-batches", pluginHandler.CreatePublishBatch)
|
|
||||||
articles.DELETE("/:id", artHandler.Delete)
|
articles.DELETE("/:id", artHandler.Delete)
|
||||||
|
|
||||||
brands := tenantProtected.Group("/brands")
|
brands := tenantProtected.Group("/brands")
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
DROP TABLE IF EXISTS publish_records;
|
DROP TABLE IF EXISTS publish_records;
|
||||||
DROP TABLE IF EXISTS publish_batches;
|
DROP TABLE IF EXISTS publish_batches;
|
||||||
DROP TABLE IF EXISTS plugin_sessions;
|
|
||||||
DROP TABLE IF EXISTS plugin_installations;
|
|
||||||
DROP TABLE IF EXISTS platform_accounts;
|
DROP TABLE IF EXISTS platform_accounts;
|
||||||
DROP TABLE IF EXISTS media_platforms;
|
DROP TABLE IF EXISTS media_platforms;
|
||||||
|
|||||||
@@ -53,50 +53,6 @@ CREATE UNIQUE INDEX uk_platform_accounts_identity_active
|
|||||||
WHERE deleted_at IS NULL;
|
WHERE deleted_at IS NULL;
|
||||||
CREATE INDEX idx_platform_accounts_tenant_platform ON platform_accounts(tenant_id, platform_id, status, created_at DESC);
|
CREATE INDEX idx_platform_accounts_tenant_platform ON platform_accounts(tenant_id, platform_id, status, created_at DESC);
|
||||||
|
|
||||||
CREATE TABLE plugin_installations (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
|
||||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
|
||||||
installation_key VARCHAR(128) NOT NULL,
|
|
||||||
installation_name VARCHAR(255) NOT NULL,
|
|
||||||
browser_name VARCHAR(64),
|
|
||||||
client_version VARCHAR(32),
|
|
||||||
installation_token_hash VARCHAR(128) NOT NULL,
|
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
||||||
last_seen_at TIMESTAMPTZ,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
deleted_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX uk_plugin_installations_key_active
|
|
||||||
ON plugin_installations(tenant_id, installation_key)
|
|
||||||
WHERE deleted_at IS NULL;
|
|
||||||
CREATE INDEX idx_plugin_installations_tenant_status
|
|
||||||
ON plugin_installations(tenant_id, status, created_at DESC);
|
|
||||||
|
|
||||||
CREATE TABLE plugin_sessions (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
|
||||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
|
||||||
plugin_installation_id BIGINT REFERENCES plugin_installations(id),
|
|
||||||
action_type VARCHAR(20) NOT NULL,
|
|
||||||
resource_type VARCHAR(30),
|
|
||||||
resource_id BIGINT,
|
|
||||||
platform_account_id BIGINT REFERENCES platform_accounts(id),
|
|
||||||
platform_id VARCHAR(64) NOT NULL REFERENCES media_platforms(platform_id),
|
|
||||||
session_token VARCHAR(128) NOT NULL,
|
|
||||||
client_version VARCHAR(32),
|
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
||||||
expire_at TIMESTAMPTZ NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX uk_plugin_sessions_token ON plugin_sessions(session_token);
|
|
||||||
CREATE INDEX idx_plugin_sessions_status_expire ON plugin_sessions(status, expire_at);
|
|
||||||
CREATE INDEX idx_plugin_sessions_tenant_action ON plugin_sessions(tenant_id, action_type, created_at DESC);
|
|
||||||
|
|
||||||
CREATE TABLE publish_batches (
|
CREATE TABLE publish_batches (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||||
|
|||||||
@@ -1,4 +1 @@
|
|||||||
-- No-op rollback.
|
-- Legacy no-op rollback.
|
||||||
-- This migration repairs databases that already applied an older variant of
|
|
||||||
-- 20260402140000 without plugin_installations. Dropping these objects on
|
|
||||||
-- rollback would break databases created from the current baseline migration.
|
|
||||||
|
|||||||
@@ -1,39 +1 @@
|
|||||||
CREATE TABLE IF NOT EXISTS plugin_installations (
|
-- Legacy no-op.
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
|
||||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
|
||||||
installation_key VARCHAR(128) NOT NULL,
|
|
||||||
installation_name VARCHAR(255) NOT NULL,
|
|
||||||
browser_name VARCHAR(64),
|
|
||||||
client_version VARCHAR(32),
|
|
||||||
installation_token_hash VARCHAR(128) NOT NULL,
|
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
||||||
last_seen_at TIMESTAMPTZ,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
deleted_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_plugin_installations_key_active
|
|
||||||
ON plugin_installations(tenant_id, installation_key)
|
|
||||||
WHERE deleted_at IS NULL;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_plugin_installations_tenant_status
|
|
||||||
ON plugin_installations(tenant_id, status, created_at DESC);
|
|
||||||
|
|
||||||
ALTER TABLE plugin_sessions
|
|
||||||
ADD COLUMN IF NOT EXISTS plugin_installation_id BIGINT;
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM pg_constraint
|
|
||||||
WHERE conname = 'plugin_sessions_plugin_installation_id_fkey'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE plugin_sessions
|
|
||||||
ADD CONSTRAINT plugin_sessions_plugin_installation_id_fkey
|
|
||||||
FOREIGN KEY (plugin_installation_id)
|
|
||||||
REFERENCES plugin_installations(id);
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
CREATE TABLE tenant_monitoring_quotas (
|
CREATE TABLE tenant_monitoring_quotas (
|
||||||
tenant_id BIGINT PRIMARY KEY REFERENCES tenants(id),
|
tenant_id BIGINT PRIMARY KEY REFERENCES tenants(id),
|
||||||
max_brands INT NOT NULL DEFAULT 1,
|
max_brands INT NOT NULL DEFAULT 1,
|
||||||
collection_mode VARCHAR(20) NOT NULL DEFAULT 'plugin',
|
collection_mode VARCHAR(20) NOT NULL DEFAULT 'desktop',
|
||||||
question_selection_mode VARCHAR(20) NOT NULL DEFAULT 'best_effort_all',
|
question_selection_mode VARCHAR(20) NOT NULL DEFAULT 'best_effort_all',
|
||||||
collect_frequency VARCHAR(20) NOT NULL DEFAULT 'daily',
|
collect_frequency VARCHAR(20) NOT NULL DEFAULT 'daily',
|
||||||
enabled_platforms JSONB NOT NULL DEFAULT '["deepseek","qwen","doubao"]'::jsonb,
|
enabled_platforms JSONB NOT NULL DEFAULT '["deepseek","qwen","doubao"]'::jsonb,
|
||||||
primary_installation_id BIGINT REFERENCES plugin_installations(id),
|
primary_client_id UUID,
|
||||||
task_daily_hard_cap INT NOT NULL DEFAULT 36,
|
task_daily_hard_cap INT NOT NULL DEFAULT 36,
|
||||||
plan_tier VARCHAR(20) NOT NULL DEFAULT 'free',
|
plan_tier VARCHAR(20) NOT NULL DEFAULT 'free',
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS plugin_installations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||||
|
installation_key VARCHAR(128) NOT NULL,
|
||||||
|
installation_name VARCHAR(255) NOT NULL,
|
||||||
|
browser_name VARCHAR(64),
|
||||||
|
client_version VARCHAR(32),
|
||||||
|
installation_token_hash VARCHAR(128) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||||
|
last_seen_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_plugin_installations_key_active
|
||||||
|
ON plugin_installations(tenant_id, installation_key)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS plugin_sessions (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||||
|
plugin_installation_id BIGINT REFERENCES plugin_installations(id),
|
||||||
|
action_type VARCHAR(20) NOT NULL,
|
||||||
|
resource_type VARCHAR(30),
|
||||||
|
resource_id BIGINT,
|
||||||
|
platform_account_id BIGINT REFERENCES platform_accounts(id),
|
||||||
|
platform_id VARCHAR(64) NOT NULL REFERENCES media_platforms(platform_id),
|
||||||
|
session_token VARCHAR(128) NOT NULL,
|
||||||
|
client_version VARCHAR(32),
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||||
|
expire_at TIMESTAMPTZ NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE tenant_monitoring_quotas
|
||||||
|
DROP CONSTRAINT IF EXISTS tenant_monitoring_quotas_primary_client_id_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE tenant_monitoring_quotas
|
||||||
|
ADD COLUMN IF NOT EXISTS primary_installation_id BIGINT REFERENCES plugin_installations(id);
|
||||||
|
|
||||||
|
UPDATE tenant_monitoring_quotas
|
||||||
|
SET collection_mode = 'plugin'
|
||||||
|
WHERE collection_mode = 'desktop';
|
||||||
|
|
||||||
|
ALTER TABLE tenant_monitoring_quotas
|
||||||
|
ALTER COLUMN collection_mode SET DEFAULT 'plugin';
|
||||||
|
|
||||||
|
ALTER TABLE tenant_monitoring_quotas
|
||||||
|
DROP COLUMN IF EXISTS primary_client_id;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
ALTER TABLE tenant_monitoring_quotas
|
||||||
|
DROP CONSTRAINT IF EXISTS tenant_monitoring_quotas_primary_installation_id_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE tenant_monitoring_quotas
|
||||||
|
ADD COLUMN IF NOT EXISTS primary_client_id UUID;
|
||||||
|
|
||||||
|
UPDATE tenant_monitoring_quotas
|
||||||
|
SET collection_mode = 'desktop'
|
||||||
|
WHERE collection_mode = 'plugin';
|
||||||
|
|
||||||
|
ALTER TABLE tenant_monitoring_quotas
|
||||||
|
ALTER COLUMN collection_mode SET DEFAULT 'desktop';
|
||||||
|
|
||||||
|
ALTER TABLE tenant_monitoring_quotas
|
||||||
|
DROP COLUMN IF EXISTS primary_installation_id;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'tenant_monitoring_quotas_primary_client_id_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE tenant_monitoring_quotas
|
||||||
|
ADD CONSTRAINT tenant_monitoring_quotas_primary_client_id_fkey
|
||||||
|
FOREIGN KEY (primary_client_id)
|
||||||
|
REFERENCES desktop_clients(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS plugin_sessions;
|
||||||
|
DROP TABLE IF EXISTS plugin_installations;
|
||||||
@@ -21,7 +21,7 @@ CREATE INDEX idx_monitoring_question_snapshot_active
|
|||||||
CREATE TABLE monitoring_platform_access_snapshots (
|
CREATE TABLE monitoring_platform_access_snapshots (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
tenant_id BIGINT NOT NULL,
|
tenant_id BIGINT NOT NULL,
|
||||||
installation_id BIGINT NOT NULL,
|
client_id UUID NOT NULL,
|
||||||
ai_platform_id VARCHAR(30) NOT NULL,
|
ai_platform_id VARCHAR(30) NOT NULL,
|
||||||
business_date DATE NOT NULL,
|
business_date DATE NOT NULL,
|
||||||
access_status VARCHAR(20) NOT NULL,
|
access_status VARCHAR(20) NOT NULL,
|
||||||
@@ -32,7 +32,7 @@ CREATE TABLE monitoring_platform_access_snapshots (
|
|||||||
reason_message TEXT,
|
reason_message TEXT,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
CONSTRAINT uk_platform_access_snapshot UNIQUE (tenant_id, installation_id, ai_platform_id, business_date)
|
CONSTRAINT uk_platform_access_snapshot UNIQUE (tenant_id, client_id, ai_platform_id, business_date)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_platform_access_snapshot_lookup
|
CREATE INDEX idx_platform_access_snapshot_lookup
|
||||||
@@ -45,9 +45,9 @@ CREATE TABLE monitoring_collect_tasks (
|
|||||||
question_id BIGINT NOT NULL,
|
question_id BIGINT NOT NULL,
|
||||||
question_hash BYTEA NOT NULL,
|
question_hash BYTEA NOT NULL,
|
||||||
ai_platform_id VARCHAR(30) NOT NULL,
|
ai_platform_id VARCHAR(30) NOT NULL,
|
||||||
collector_type VARCHAR(20) NOT NULL DEFAULT 'plugin',
|
collector_type VARCHAR(20) NOT NULL DEFAULT 'desktop',
|
||||||
trigger_source VARCHAR(20) NOT NULL DEFAULT 'automatic',
|
trigger_source VARCHAR(20) NOT NULL DEFAULT 'automatic',
|
||||||
run_mode VARCHAR(30) NOT NULL DEFAULT 'plugin_standard',
|
run_mode VARCHAR(30) NOT NULL DEFAULT 'desktop_standard',
|
||||||
business_date DATE NOT NULL,
|
business_date DATE NOT NULL,
|
||||||
planned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
planned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||||
@@ -77,9 +77,9 @@ CREATE TABLE question_monitor_runs (
|
|||||||
question_text_snapshot TEXT NOT NULL,
|
question_text_snapshot TEXT NOT NULL,
|
||||||
question_hash BYTEA NOT NULL,
|
question_hash BYTEA NOT NULL,
|
||||||
ai_platform_id VARCHAR(30) NOT NULL,
|
ai_platform_id VARCHAR(30) NOT NULL,
|
||||||
collector_type VARCHAR(20) NOT NULL DEFAULT 'plugin',
|
collector_type VARCHAR(20) NOT NULL DEFAULT 'desktop',
|
||||||
trigger_source VARCHAR(20) NOT NULL DEFAULT 'automatic',
|
trigger_source VARCHAR(20) NOT NULL DEFAULT 'automatic',
|
||||||
run_mode VARCHAR(30) NOT NULL DEFAULT 'plugin_standard',
|
run_mode VARCHAR(30) NOT NULL DEFAULT 'desktop_standard',
|
||||||
business_date DATE NOT NULL,
|
business_date DATE NOT NULL,
|
||||||
provider_model VARCHAR(100),
|
provider_model VARCHAR(100),
|
||||||
provider_request_id VARCHAR(100),
|
provider_request_id VARCHAR(100),
|
||||||
@@ -187,7 +187,7 @@ CREATE TABLE monitoring_brand_daily (
|
|||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
tenant_id BIGINT NOT NULL,
|
tenant_id BIGINT NOT NULL,
|
||||||
brand_id BIGINT NOT NULL,
|
brand_id BIGINT NOT NULL,
|
||||||
collector_type VARCHAR(20) NOT NULL DEFAULT 'plugin',
|
collector_type VARCHAR(20) NOT NULL DEFAULT 'desktop',
|
||||||
business_date DATE NOT NULL,
|
business_date DATE NOT NULL,
|
||||||
sample_status VARCHAR(20) NOT NULL,
|
sample_status VARCHAR(20) NOT NULL,
|
||||||
desired_sample_count INT NOT NULL DEFAULT 0,
|
desired_sample_count INT NOT NULL DEFAULT 0,
|
||||||
@@ -221,7 +221,7 @@ CREATE TABLE monitoring_brand_platform_daily (
|
|||||||
tenant_id BIGINT NOT NULL,
|
tenant_id BIGINT NOT NULL,
|
||||||
brand_id BIGINT NOT NULL,
|
brand_id BIGINT NOT NULL,
|
||||||
ai_platform_id VARCHAR(30) NOT NULL,
|
ai_platform_id VARCHAR(30) NOT NULL,
|
||||||
collector_type VARCHAR(20) NOT NULL DEFAULT 'plugin',
|
collector_type VARCHAR(20) NOT NULL DEFAULT 'desktop',
|
||||||
business_date DATE NOT NULL,
|
business_date DATE NOT NULL,
|
||||||
platform_sample_status VARCHAR(20) NOT NULL,
|
platform_sample_status VARCHAR(20) NOT NULL,
|
||||||
planned_sample_count INT NOT NULL DEFAULT 0,
|
planned_sample_count INT NOT NULL DEFAULT 0,
|
||||||
|
|||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
DROP TABLE IF EXISTS monitoring_platform_access_snapshots;
|
||||||
|
|
||||||
|
CREATE TABLE monitoring_platform_access_snapshots (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant_id BIGINT NOT NULL,
|
||||||
|
installation_id BIGINT NOT NULL,
|
||||||
|
ai_platform_id VARCHAR(30) NOT NULL,
|
||||||
|
business_date DATE NOT NULL,
|
||||||
|
access_status VARCHAR(20) NOT NULL,
|
||||||
|
connected BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
accessible BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
detected_at TIMESTAMPTZ NOT NULL,
|
||||||
|
reason_code VARCHAR(50),
|
||||||
|
reason_message TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uk_platform_access_snapshot UNIQUE (tenant_id, installation_id, ai_platform_id, business_date)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_platform_access_snapshot_lookup
|
||||||
|
ON monitoring_platform_access_snapshots(tenant_id, business_date, ai_platform_id, access_status);
|
||||||
|
|
||||||
|
ALTER TABLE monitoring_collect_tasks
|
||||||
|
ALTER COLUMN collector_type SET DEFAULT 'plugin';
|
||||||
|
|
||||||
|
ALTER TABLE monitoring_collect_tasks
|
||||||
|
ALTER COLUMN run_mode SET DEFAULT 'plugin_standard';
|
||||||
|
|
||||||
|
UPDATE monitoring_collect_tasks
|
||||||
|
SET collector_type = 'plugin'
|
||||||
|
WHERE collector_type = 'desktop';
|
||||||
|
|
||||||
|
UPDATE monitoring_collect_tasks
|
||||||
|
SET run_mode = 'plugin_standard'
|
||||||
|
WHERE run_mode = 'desktop_standard';
|
||||||
|
|
||||||
|
ALTER TABLE question_monitor_runs
|
||||||
|
ALTER COLUMN collector_type SET DEFAULT 'plugin';
|
||||||
|
|
||||||
|
ALTER TABLE question_monitor_runs
|
||||||
|
ALTER COLUMN run_mode SET DEFAULT 'plugin_standard';
|
||||||
|
|
||||||
|
UPDATE question_monitor_runs
|
||||||
|
SET collector_type = 'plugin'
|
||||||
|
WHERE collector_type = 'desktop';
|
||||||
|
|
||||||
|
UPDATE question_monitor_runs
|
||||||
|
SET run_mode = 'plugin_standard'
|
||||||
|
WHERE run_mode = 'desktop_standard';
|
||||||
|
|
||||||
|
ALTER TABLE monitoring_brand_daily
|
||||||
|
ALTER COLUMN collector_type SET DEFAULT 'plugin';
|
||||||
|
|
||||||
|
UPDATE monitoring_brand_daily
|
||||||
|
SET collector_type = 'plugin'
|
||||||
|
WHERE collector_type = 'desktop';
|
||||||
|
|
||||||
|
ALTER TABLE monitoring_brand_platform_daily
|
||||||
|
ALTER COLUMN collector_type SET DEFAULT 'plugin';
|
||||||
|
|
||||||
|
UPDATE monitoring_brand_platform_daily
|
||||||
|
SET collector_type = 'plugin'
|
||||||
|
WHERE collector_type = 'desktop';
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
DROP TABLE IF EXISTS monitoring_platform_access_snapshots;
|
||||||
|
|
||||||
|
CREATE TABLE monitoring_platform_access_snapshots (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant_id BIGINT NOT NULL,
|
||||||
|
client_id UUID NOT NULL,
|
||||||
|
ai_platform_id VARCHAR(30) NOT NULL,
|
||||||
|
business_date DATE NOT NULL,
|
||||||
|
access_status VARCHAR(20) NOT NULL,
|
||||||
|
connected BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
accessible BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
detected_at TIMESTAMPTZ NOT NULL,
|
||||||
|
reason_code VARCHAR(50),
|
||||||
|
reason_message TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uk_platform_access_snapshot UNIQUE (tenant_id, client_id, ai_platform_id, business_date)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_platform_access_snapshot_lookup
|
||||||
|
ON monitoring_platform_access_snapshots(tenant_id, business_date, ai_platform_id, access_status);
|
||||||
|
|
||||||
|
ALTER TABLE monitoring_collect_tasks
|
||||||
|
ALTER COLUMN collector_type SET DEFAULT 'desktop';
|
||||||
|
|
||||||
|
ALTER TABLE monitoring_collect_tasks
|
||||||
|
ALTER COLUMN run_mode SET DEFAULT 'desktop_standard';
|
||||||
|
|
||||||
|
UPDATE monitoring_collect_tasks
|
||||||
|
SET collector_type = 'desktop'
|
||||||
|
WHERE collector_type = 'plugin';
|
||||||
|
|
||||||
|
UPDATE monitoring_collect_tasks
|
||||||
|
SET run_mode = 'desktop_standard'
|
||||||
|
WHERE run_mode = 'plugin_standard';
|
||||||
|
|
||||||
|
ALTER TABLE question_monitor_runs
|
||||||
|
ALTER COLUMN collector_type SET DEFAULT 'desktop';
|
||||||
|
|
||||||
|
ALTER TABLE question_monitor_runs
|
||||||
|
ALTER COLUMN run_mode SET DEFAULT 'desktop_standard';
|
||||||
|
|
||||||
|
UPDATE question_monitor_runs
|
||||||
|
SET collector_type = 'desktop'
|
||||||
|
WHERE collector_type = 'plugin';
|
||||||
|
|
||||||
|
UPDATE question_monitor_runs
|
||||||
|
SET run_mode = 'desktop_standard'
|
||||||
|
WHERE run_mode = 'plugin_standard';
|
||||||
|
|
||||||
|
ALTER TABLE monitoring_brand_daily
|
||||||
|
ALTER COLUMN collector_type SET DEFAULT 'desktop';
|
||||||
|
|
||||||
|
UPDATE monitoring_brand_daily
|
||||||
|
SET collector_type = 'desktop'
|
||||||
|
WHERE collector_type = 'plugin';
|
||||||
|
|
||||||
|
ALTER TABLE monitoring_brand_platform_daily
|
||||||
|
ALTER COLUMN collector_type SET DEFAULT 'desktop';
|
||||||
|
|
||||||
|
UPDATE monitoring_brand_platform_daily
|
||||||
|
SET collector_type = 'desktop'
|
||||||
|
WHERE collector_type = 'plugin';
|
||||||
Reference in New Issue
Block a user