feat(compliance): add content compliance detection across tenant, ops, and clients

Implements the Revision 8/9 compliance design: tenant + ops backends, ops-web
content-safety pages, admin-web editor/publish gate integration, desktop publish
block surfacing, and supporting migrations / shared types / config plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 20:48:14 +08:00
parent 81577b6154
commit 745cdd79cf
73 changed files with 12747 additions and 1892 deletions
+106
View File
@@ -0,0 +1,106 @@
package compliance
import "time"
type GateDecision string
const (
GateDecisionPass GateDecision = "pass"
GateDecisionBlock GateDecision = "block"
GateDecisionNeedsAck GateDecision = "needs_ack"
)
type ManualReviewStatus string
const (
ManualReviewStatusNone ManualReviewStatus = "none"
ManualReviewStatusPending ManualReviewStatus = "pending"
ManualReviewStatusApproved ManualReviewStatus = "approved"
ManualReviewStatusRejected ManualReviewStatus = "rejected"
ManualReviewStatusCancelled ManualReviewStatus = "cancelled"
)
type RuntimeStatus struct {
Enabled bool `json:"enabled"`
ConfigEnabled bool `json:"config_enabled"`
MasterEnabled bool `json:"master_enabled"`
EnforcementMode string `json:"enforcement_mode"`
LLMJudgeEnabled bool `json:"llm_judge_enabled"`
SnapshotReloadInterval string `json:"snapshot_reload_interval"`
PublishGateTimeout string `json:"publish_gate_timeout"`
DictionaryVersion int64 `json:"dictionary_version"`
EnabledDictionaries []string `json:"enabled_dictionaries"`
}
type Violation struct {
ID int64 `json:"id"`
RecordID int64 `json:"record_id,omitempty"`
PlatformCode *string `json:"platform_code,omitempty"`
Source string `json:"source"`
DictionaryCode *string `json:"dictionary_code,omitempty"`
DictionaryName *string `json:"dictionary_name,omitempty"`
TermPattern *string `json:"term_pattern,omitempty"`
MatchedText string `json:"matched_text"`
StartOffset int `json:"start_offset"`
EndOffset int `json:"end_offset"`
DomPath *string `json:"dom_path,omitempty"`
Level string `json:"level"`
Hint *string `json:"hint,omitempty"`
ReferenceLaw *string `json:"reference_law,omitempty"`
}
type CheckResult struct {
RecordID int64 `json:"record_id,omitempty"`
Decision GateDecision `json:"decision"`
Passed bool `json:"passed"`
HighestLevel *string `json:"highest_level,omitempty"`
Violations []Violation `json:"violations"`
HitCount int `json:"hit_count"`
StoredHitCount int `json:"stored_hit_count"`
Truncated bool `json:"truncated"`
EnforcementMode string `json:"enforcement_mode"`
ContentHash string `json:"content_hash"`
PolicyFingerprint string `json:"policy_fingerprint"`
TargetPlatforms []string `json:"target_platforms"`
ManualReviewStatus ManualReviewStatus `json:"manual_review_status"`
ManualReviewID *int64 `json:"manual_review_id,omitempty"`
ManualReviewReason *string `json:"manual_review_reason,omitempty"`
ManualReviewDecidedAt *time.Time `json:"manual_review_decided_at,omitempty"`
CheckedAt time.Time `json:"checked_at"`
DurationMS int `json:"duration_ms"`
}
type AckRecord struct {
ID int64 `json:"id"`
CheckRecordID int64 `json:"check_record_id"`
ArticleID int64 `json:"article_id"`
ArticleVersionID int64 `json:"article_version_id"`
ContentHash string `json:"content_hash"`
PolicyFingerprint string `json:"policy_fingerprint"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
}
type ManualReview struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
ArticleID int64 `json:"article_id"`
ArticleVersionID int64 `json:"article_version_id"`
CheckRecordID *int64 `json:"check_record_id,omitempty"`
Status ManualReviewStatus `json:"status"`
RequestedBy int64 `json:"requested_by"`
RequestedAt time.Time `json:"requested_at"`
RequestNote *string `json:"request_note,omitempty"`
ReviewedBy *int64 `json:"reviewed_by,omitempty"`
ReviewedAt *time.Time `json:"reviewed_at,omitempty"`
DecisionReason *string `json:"decision_reason,omitempty"`
CancelledBy *int64 `json:"cancelled_by,omitempty"`
CancelledAt *time.Time `json:"cancelled_at,omitempty"`
CancelReason *string `json:"cancel_reason,omitempty"`
ViolationSummary []Violation `json:"violation_summary"`
OriginalPlatforms []string `json:"original_target_platforms"`
ArticleTitle *string `json:"article_title,omitempty"`
ArticlePlaintext *string `json:"article_plaintext,omitempty"`
ApplicantName *string `json:"applicant_name,omitempty"`
TenantName *string `json:"tenant_name,omitempty"`
}
+96
View File
@@ -7,6 +7,7 @@ import (
"net"
"net/url"
"os"
"strconv"
"strings"
"time"
@@ -34,6 +35,7 @@ type Config struct {
JWT JWTConfig `mapstructure:"jwt"`
Log LogConfig `mapstructure:"log"`
LLM LLMConfig `mapstructure:"llm"`
Compliance ComplianceConfig `mapstructure:"compliance"`
Retrieval RetrievalConfig `mapstructure:"retrieval"`
Generation GenerationConfig `mapstructure:"generation"`
}
@@ -137,6 +139,12 @@ type RabbitMQConfig struct {
ImageAssetDLX string `mapstructure:"image_asset_dlx"`
ImageAssetDLQ string `mapstructure:"image_asset_dlq"`
ImageAssetDLQRouteKey string `mapstructure:"image_asset_dlq_routing_key"`
ComplianceReviewExchange string `mapstructure:"compliance_review_exchange"`
ComplianceReviewRoutingKey string `mapstructure:"compliance_review_routing_key"`
ComplianceReviewQueue string `mapstructure:"compliance_review_queue"`
ComplianceReviewDLX string `mapstructure:"compliance_review_dlx"`
ComplianceReviewDLQ string `mapstructure:"compliance_review_dlq"`
ComplianceReviewDLQRouteKey string `mapstructure:"compliance_review_dlq_routing_key"`
ConsumerPrefetch int `mapstructure:"consumer_prefetch"`
PublishChannelPoolSize int `mapstructure:"publish_channel_pool_size"`
}
@@ -282,6 +290,16 @@ type LLMConfig struct {
WebSearchLimit int32 `mapstructure:"web_search_limit"`
}
type ComplianceConfig struct {
Enabled bool `mapstructure:"enabled"`
LLMJudgeEnabled bool `mapstructure:"llm_judge_enabled"`
SnapshotReloadInterval time.Duration `mapstructure:"snapshot_reload_interval"`
PublishGateTimeout time.Duration `mapstructure:"publish_gate_timeout"`
ReviewWorkerConcurrency int `mapstructure:"review_worker_concurrency"`
ReviewWorkerTimeout time.Duration `mapstructure:"review_worker_timeout"`
ReviewMaxAttempts int `mapstructure:"review_max_attempts"`
}
type RetrievalConfig struct {
Provider string `mapstructure:"provider"`
BaseURL string `mapstructure:"base_url"`
@@ -356,6 +374,7 @@ func loadWithFiles(configPath string) (*Config, []string, error) {
normalizeMonitoringDispatchConfig(&cfg.MonitoringDispatch)
normalizeMembershipConfig(&cfg.Membership)
normalizeBrandLibraryConfig(&cfg.BrandLibrary)
NormalizeComplianceConfig(&cfg.Compliance)
NormalizeGenerationConfig(&cfg.Generation)
files := []string{configFile}
@@ -432,6 +451,10 @@ func applyConfigDefaults(settings map[string]any) {
if _, ok := cacheSettings["metrics_enabled"]; !ok {
cacheSettings["metrics_enabled"] = true
}
complianceSettings := ensureMapSetting(settings, "compliance")
if _, ok := complianceSettings["enabled"]; !ok {
complianceSettings["enabled"] = true
}
}
func ensureMapSetting(settings map[string]any, key string) map[string]any {
@@ -566,6 +589,27 @@ func NormalizeGenerationConfig(cfg *GenerationConfig) {
}
}
func NormalizeComplianceConfig(cfg *ComplianceConfig) {
if cfg == nil {
return
}
if cfg.SnapshotReloadInterval <= 0 {
cfg.SnapshotReloadInterval = 30 * time.Second
}
if cfg.PublishGateTimeout <= 0 {
cfg.PublishGateTimeout = 800 * time.Millisecond
}
if cfg.ReviewWorkerConcurrency <= 0 {
cfg.ReviewWorkerConcurrency = 1
}
if cfg.ReviewWorkerTimeout <= 0 {
cfg.ReviewWorkerTimeout = 45 * time.Second
}
if cfg.ReviewMaxAttempts <= 0 {
cfg.ReviewMaxAttempts = 3
}
}
func candidateConfigPaths(configPath string, local bool) []string {
trimmed := strings.TrimSpace(configPath)
if trimmed == "" {
@@ -628,6 +672,27 @@ func applyEnvOverrides(cfg *Config) {
if model, ok := lookupNonEmptyEnv("LLM_KNOWLEDGE_URL_MODEL"); ok {
cfg.LLM.KnowledgeURLModel = model
}
if enabled, ok := lookupBoolEnv("COMPLIANCE_ENABLED"); ok {
cfg.Compliance.Enabled = enabled
}
if enabled, ok := lookupBoolEnv("COMPLIANCE_LLM_ENABLED"); ok {
cfg.Compliance.LLMJudgeEnabled = enabled
}
if d, ok := lookupDurationEnv("COMPLIANCE_SNAPSHOT_RELOAD_INTERVAL"); ok {
cfg.Compliance.SnapshotReloadInterval = d
}
if d, ok := lookupDurationEnv("COMPLIANCE_PUBLISH_GATE_TIMEOUT"); ok {
cfg.Compliance.PublishGateTimeout = d
}
if n, ok := lookupIntEnv("COMPLIANCE_REVIEW_WORKER_CONCURRENCY"); ok {
cfg.Compliance.ReviewWorkerConcurrency = n
}
if d, ok := lookupDurationEnv("COMPLIANCE_REVIEW_WORKER_TIMEOUT"); ok {
cfg.Compliance.ReviewWorkerTimeout = d
}
if n, ok := lookupIntEnv("COMPLIANCE_REVIEW_MAX_ATTEMPTS"); ok {
cfg.Compliance.ReviewMaxAttempts = n
}
if apiKey, ok := lookupNonEmptyEnv("QDRANT_API_KEY"); ok {
cfg.Qdrant.APIKey = apiKey
}
@@ -832,6 +897,24 @@ func normalizeRabbitMQConfig(cfg *RabbitMQConfig) {
if strings.TrimSpace(cfg.ImageAssetDLQRouteKey) == "" {
cfg.ImageAssetDLQRouteKey = "image.asset.finalize.dlq"
}
if strings.TrimSpace(cfg.ComplianceReviewExchange) == "" {
cfg.ComplianceReviewExchange = "compliance.review"
}
if strings.TrimSpace(cfg.ComplianceReviewRoutingKey) == "" {
cfg.ComplianceReviewRoutingKey = "compliance.review.run"
}
if strings.TrimSpace(cfg.ComplianceReviewQueue) == "" {
cfg.ComplianceReviewQueue = "compliance.review.run"
}
if strings.TrimSpace(cfg.ComplianceReviewDLX) == "" {
cfg.ComplianceReviewDLX = "compliance.review.dlx"
}
if strings.TrimSpace(cfg.ComplianceReviewDLQ) == "" {
cfg.ComplianceReviewDLQ = "compliance.review.run.dlq"
}
if strings.TrimSpace(cfg.ComplianceReviewDLQRouteKey) == "" {
cfg.ComplianceReviewDLQRouteKey = "compliance.review.run.dlq"
}
if cfg.PublishChannelPoolSize <= 0 {
cfg.PublishChannelPoolSize = 16
}
@@ -1076,6 +1159,19 @@ func lookupDurationEnv(key string) (time.Duration, bool) {
return duration, true
}
func lookupIntEnv(key string) (int, bool) {
value, ok := lookupNonEmptyEnv(key)
if !ok {
return 0, false
}
number, err := strconv.Atoi(value)
if err != nil {
return 0, false
}
return number, true
}
func maxInt(value, fallback int) int {
if value < fallback {
return fallback
@@ -213,6 +213,10 @@ func (c *Client) PublishImageAssetTask(ctx context.Context, body []byte) error {
return c.publish(ctx, c.cfg.ImageAssetExchange, c.cfg.ImageAssetRoutingKey, body)
}
func (c *Client) PublishComplianceReviewTask(ctx context.Context, body []byte) error {
return c.publish(ctx, c.cfg.ComplianceReviewExchange, c.cfg.ComplianceReviewRoutingKey, body)
}
func (c *Client) publish(ctx context.Context, exchange, routingKey string, body []byte) error {
for attempt := 0; attempt < 2; attempt++ {
conn, ch, pooled, err := c.acquirePublishChannel()
@@ -303,6 +307,10 @@ func (c *Client) ConsumeImageAssetTask(consumerName string) (<-chan amqp.Deliver
return c.consume(consumerName, c.cfg.ImageAssetQueue)
}
func (c *Client) ConsumeComplianceReviewTask(consumerName string) (<-chan amqp.Delivery, *amqp.Channel, error) {
return c.consume(consumerName, c.cfg.ComplianceReviewQueue)
}
func (c *Client) consume(consumerName, queue string) (<-chan amqp.Delivery, *amqp.Channel, error) {
for attempt := 0; attempt < 2; attempt++ {
conn, ch, err := c.openChannel()
@@ -741,6 +749,18 @@ func (c *Client) ensureTopology(conn *amqp.Connection) error {
return err
}
if err := c.ensureWorkQueue(
ch,
c.cfg.ComplianceReviewExchange,
c.cfg.ComplianceReviewRoutingKey,
c.cfg.ComplianceReviewQueue,
c.cfg.ComplianceReviewDLX,
c.cfg.ComplianceReviewDLQ,
c.cfg.ComplianceReviewDLQRouteKey,
); err != nil {
return err
}
return nil
}
+108 -98
View File
@@ -35,24 +35,24 @@ var routeDocs = map[string]routeDoc{
"POST /api/desktop/clients/revoke": {"吊销客户端", "由桌面端调用,立即注销当前客户端凭证。"},
// --- Desktop:派单与拉取 ---
"GET /api/desktop/dispatch": {"派单 WebSocket", "桌面客户端通过 WebSocket 长连接订阅派单事件,断线后自动重连。"},
"GET /api/desktop/accounts": {"拉取账号列表(桌面)", "桌面客户端拉取本机绑定/可用的媒体账号,支持增量同步。"},
"GET /api/desktop/content/articles/:id": {"拉取文章正文(桌面)", "桌面客户端在执行发布任务时拉取目标文章的最新内容。"},
"GET /api/desktop/content/assets/:token": {"桌面端访问素材", "桌面客户端使用签名 token 拉取文章中的图片/附件。"},
"GET /api/desktop/publish-tasks": {"拉取发布任务(桌面)", "桌面客户端分页查询当前账号需要执行的发布任务。"},
"GET /api/desktop/dispatch": {"派单 WebSocket", "桌面客户端通过 WebSocket 长连接订阅派单事件,断线后自动重连。"},
"GET /api/desktop/accounts": {"拉取账号列表(桌面)", "桌面客户端拉取本机绑定/可用的媒体账号,支持增量同步。"},
"GET /api/desktop/content/articles/:id": {"拉取文章正文(桌面)", "桌面客户端在执行发布任务时拉取目标文章的最新内容。"},
"GET /api/desktop/content/assets/:token": {"桌面端访问素材", "桌面客户端使用签名 token 拉取文章中的图片/附件。"},
"GET /api/desktop/publish-tasks": {"拉取发布任务(桌面)", "桌面客户端分页查询当前账号需要执行的发布任务。"},
"POST /api/desktop/accounts/health-reports": {"上报账号健康状态", "桌面客户端定期上报媒体账号的登录态、风控信号等。"},
// --- Desktop:账号增删改 ---
"POST /api/desktop/accounts": {"上报/绑定媒体账号", "桌面客户端登录媒体平台后,将账号信息上报到云端。"},
"PATCH /api/desktop/accounts/:id": {"更新媒体账号", "桌面客户端更新已绑定的媒体账号字段(昵称、头像、Cookie 等)。"},
"DELETE /api/desktop/accounts/:id": {"删除媒体账号(桌面)", "桌面客户端解绑账号,仅清理本地侧记录。"},
"POST /api/desktop/accounts": {"上报/绑定媒体账号", "桌面客户端登录媒体平台后,将账号信息上报到云端。"},
"PATCH /api/desktop/accounts/:id": {"更新媒体账号", "桌面客户端更新已绑定的媒体账号字段(昵称、头像、Cookie 等)。"},
"DELETE /api/desktop/accounts/:id": {"删除媒体账号(桌面)", "桌面客户端解绑账号,仅清理本地侧记录。"},
// --- Desktop:发布任务执行 ---
"POST /api/desktop/tasks/lease": {"领取发布任务", "桌面客户端从队列领取一批可执行任务,原子加锁。"},
"POST /api/desktop/tasks/:id/lease": {"续锁/重领任务", "对指定任务再次加锁,常用于异常重试场景。"},
"POST /api/desktop/tasks/:id/extend": {"延长任务锁", "执行较慢时延长锁定时间,避免任务被其他客户端抢走。"},
"POST /api/desktop/tasks/:id/cancel": {"取消任务(桌面)", "桌面客户端在执行失败/用户中止时通知服务端取消。"},
"POST /api/desktop/tasks/:id/result": {"上报任务结果", "桌面客户端将发布结果(成功/失败/截图/链接)回传服务端。"},
"POST /api/desktop/tasks/lease": {"领取发布任务", "桌面客户端从队列领取一批可执行任务,原子加锁。"},
"POST /api/desktop/tasks/:id/lease": {"续锁/重领任务", "对指定任务再次加锁,常用于异常重试场景。"},
"POST /api/desktop/tasks/:id/extend": {"延长任务锁", "执行较慢时延长锁定时间,避免任务被其他客户端抢走。"},
"POST /api/desktop/tasks/:id/cancel": {"取消任务(桌面)", "桌面客户端在执行失败/用户中止时通知服务端取消。"},
"POST /api/desktop/tasks/:id/result": {"上报任务结果", "桌面客户端将发布结果(成功/失败/截图/链接)回传服务端。"},
"POST /api/desktop/publish-tasks/:id/retry": {"重试发布任务", "对失败的发布任务发起重试。"},
// --- Desktop:监控采集任务 ---
@@ -63,15 +63,25 @@ var routeDocs = map[string]routeDoc{
"POST /api/desktop/monitoring/tasks/:id/cancel": {"取消监控任务", "桌面端主动取消正在执行的监控任务。"},
// --- Tenant:账号 / 任务统一视图(管理员视角) ---
"GET /api/tenant/accounts": {"租户媒体账号列表", "运营人员在管理后台查看本租户下所有媒体账号。"},
"GET /api/tenant/accounts": {"租户媒体账号列表", "运营人员在管理后台查看本租户下所有媒体账号。"},
"POST /api/tenant/accounts/:id/request-delete": {"申请删除媒体账号", "由后台发起异步删除,桌面端确认后真正解绑。"},
"POST /api/tenant/tasks/:id/reconcile": {"对账发布任务", "管理后台对发布任务做对账:状态校正、结果回填。"},
"POST /api/tenant/tasks/:id/cancel": {"取消发布任务(后台)", "管理员在后台取消还未派发或还在执行的发布任务。"},
"POST /api/tenant/publish-jobs": {"批量创建发布任务", "为一批文章/账号批量生成发布任务,进入派发队列。"},
"POST /api/tenant/tasks/:id/reconcile": {"对账发布任务", "管理后台对发布任务做对账:状态校正、结果回填。"},
"POST /api/tenant/tasks/:id/cancel": {"取消发布任务(后台)", "管理员在后台取消还未派发或还在执行的发布任务。"},
"POST /api/tenant/publish-jobs": {"批量创建发布任务", "为一批文章/账号批量生成发布任务,进入派发队列。"},
// --- Tenant:媒体 ---
"GET /api/tenant/media/platforms": {"媒体平台列表", "返回当前租户可用的媒体平台元数据(图标、能力开关等)。"},
// --- Tenant:内容合规 ---
"GET /api/tenant/compliance/runtime-status": {"合规运行状态", "返回当前租户内容合规模块是否启用、执行模式、词库版本与检测预算。"},
"POST /api/tenant/compliance/check": {"纯文本合规检测", "对标题/正文纯文本执行一次合规检测,返回聚合 check_record 与违规项。"},
"POST /api/tenant/compliance/check-records/:id/ack": {"确认合规风险", "对 advisory 检测结果进行一次性确认,返回发布时可消费的 ack_record_id。"},
"POST /api/tenant/articles/:id/compliance/check": {"文章合规检测", "对指定文章版本和目标平台集合执行一次合规检测。"},
"GET /api/tenant/articles/:id/compliance/latest": {"最新合规记录", "读取指定文章最近一次合规检测记录和违规明细。"},
"POST /api/tenant/articles/:id/compliance/manual-reviews": {"提交人工审阅", "将 block 检测结果提交运营人工审阅,审阅绑定到当前文章版本。"},
"GET /api/tenant/articles/:id/compliance/manual-reviews/latest": {"最新人工审阅", "读取当前文章版本最近的人工审阅状态。"},
"POST /api/tenant/articles/:id/compliance/manual-reviews/:review_id/cancel": {"撤回人工审阅", "作者撤回仍处于 pending 状态的人工审阅申请。"},
// --- Tenant:工作台 ---
"GET /api/tenant/workspace/overview": {"工作台概览", "首页顶部统计:文章数、发布数、KOL 订阅数等。"},
"GET /api/tenant/workspace/recent-articles": {"最近文章", "工作台展示最近创建/更新的若干篇文章。"},
@@ -81,28 +91,28 @@ var routeDocs = map[string]routeDoc{
"GET /api/tenant/workspace/kol-cards": {"KOL 入口卡片", "工作台首页的 KOL 订阅推荐卡片。"},
// --- Tenant:模板 ---
"GET /api/tenant/templates": {"模板列表", "返回当前 Workspace 可见的内容模板。"},
"GET /api/tenant/templates/:id": {"模板详情", "返回模板字段定义、prompt 结构、示例等。"},
"POST /api/tenant/templates/:id/drafts": {"保存模板草稿", "保存用户在模板表单上的临时输入,便于稍后续填。"},
"POST /api/tenant/templates/:id/analyze-tasks": {"创建主题分析任务", "基于用户输入创建主题分析异步任务。"},
"GET /api/tenant/templates/:id/analyze_task_result": {"获取主题分析结果", "轮询主题分析任务结果,需要带上 task_id。"},
"POST /api/tenant/templates/:id/title-tasks": {"创建标题生成任务", "基于主题创建标题候选生成任务。"},
"GET /api/tenant/templates/:id/gen_title_task_result": {"获取标题生成结果", "轮询标题生成任务结果。"},
"POST /api/tenant/templates/:id/outline-tasks": {"创建大纲生成任务", "基于选定标题创建大纲生成任务。"},
"GET /api/tenant/templates/:id/gen_outline_task_result": {"获取大纲生成结果", "轮询大纲生成任务结果。"},
"POST /api/tenant/templates/:id/generate": {"生成正文", "基于模板入参直接生成完整文章并落库。"},
"GET /api/tenant/templates": {"模板列表", "返回当前 Workspace 可见的内容模板。"},
"GET /api/tenant/templates/:id": {"模板详情", "返回模板字段定义、prompt 结构、示例等。"},
"POST /api/tenant/templates/:id/drafts": {"保存模板草稿", "保存用户在模板表单上的临时输入,便于稍后续填。"},
"POST /api/tenant/templates/:id/analyze-tasks": {"创建主题分析任务", "基于用户输入创建主题分析异步任务。"},
"GET /api/tenant/templates/:id/analyze_task_result": {"获取主题分析结果", "轮询主题分析任务结果,需要带上 task_id。"},
"POST /api/tenant/templates/:id/title-tasks": {"创建标题生成任务", "基于主题创建标题候选生成任务。"},
"GET /api/tenant/templates/:id/gen_title_task_result": {"获取标题生成结果", "轮询标题生成任务结果。"},
"POST /api/tenant/templates/:id/outline-tasks": {"创建大纲生成任务", "基于选定标题创建大纲生成任务。"},
"GET /api/tenant/templates/:id/gen_outline_task_result": {"获取大纲生成结果", "轮询大纲生成任务结果。"},
"POST /api/tenant/templates/:id/generate": {"生成正文", "基于模板入参直接生成完整文章并落库。"},
// --- TenantKOL 管理(创作者侧) ---
"GET /api/tenant/kol/manage/profile": {"KOL 主页信息", "获取当前用户作为 KOL 的资料卡。"},
"PUT /api/tenant/kol/manage/profile": {"更新 KOL 主页", "更新 KOL 名称、简介、行业、联系方式等。"},
"POST /api/tenant/kol/manage/profile/avatar": {"上传 KOL 头像", "Multipart 上传 KOL 头像,返回访问 URL。"},
"GET /api/tenant/kol/manage/packages": {"KOL 套餐列表", "KOL 自己管理名下的套餐(产品包)列表。"},
"POST /api/tenant/kol/manage/packages": {"新建 KOL 套餐", "创建新的 KOL 套餐草稿。"},
"PUT /api/tenant/kol/manage/packages/:id": {"更新 KOL 套餐", "修改套餐基础信息(名称、价格、描述、封面等)。"},
"DELETE /api/tenant/kol/manage/packages/:id": {"删除 KOL 套餐", "仅未发布或已归档的套餐可删除。"},
"PUT /api/tenant/kol/manage/packages/:id/publish": {"发布 KOL 套餐", "把草稿态套餐发布到市场,对所有租户可见。"},
"PUT /api/tenant/kol/manage/packages/:id/archive": {"下架 KOL 套餐", "将已发布的套餐归档,不再对外展示。"},
"GET /api/tenant/kol/manage/packages": {"KOL 套餐列表", "KOL 自己管理名下的套餐(产品包)列表。"},
"POST /api/tenant/kol/manage/packages": {"新建 KOL 套餐", "创建新的 KOL 套餐草稿。"},
"PUT /api/tenant/kol/manage/packages/:id": {"更新 KOL 套餐", "修改套餐基础信息(名称、价格、描述、封面等)。"},
"DELETE /api/tenant/kol/manage/packages/:id": {"删除 KOL 套餐", "仅未发布或已归档的套餐可删除。"},
"PUT /api/tenant/kol/manage/packages/:id/publish": {"发布 KOL 套餐", "把草稿态套餐发布到市场,对所有租户可见。"},
"PUT /api/tenant/kol/manage/packages/:id/archive": {"下架 KOL 套餐", "将已发布的套餐归档,不再对外展示。"},
"POST /api/tenant/kol/manage/packages/:id/self-subscription": {"KOL 自订阅套餐", "KOL 自己订阅自己的套餐用于体验/测试。"},
"DELETE /api/tenant/kol/manage/packages/:id/self-subscription": {"取消 KOL 自订阅", "取消 KOL 对自己套餐的自订阅。"},
@@ -112,9 +122,9 @@ var routeDocs = map[string]routeDoc{
"PUT /api/tenant/kol/manage/prompts/:id": {"更新 Prompt 元信息", "更新 Prompt 名称、说明、可见性等元数据。"},
"DELETE /api/tenant/kol/manage/prompts/:id": {"删除 Prompt", "彻底删除该 Prompt 及其历史版本(若已发布且有订阅则禁止)。"},
"POST /api/tenant/kol/manage/prompts/:id/save": {"保存 Prompt 草稿", "保存 Prompt 正文与变量,作为新的草稿版本。"},
"POST /api/tenant/kol/manage/prompts/:id/publish": {"发布 Prompt 版本", "将当前草稿发布为新版本,订阅者可使用。"},
"PUT /api/tenant/kol/manage/prompts/:id/activate": {"激活 Prompt", "把已发布的 Prompt 切换为当前激活版本。"},
"PUT /api/tenant/kol/manage/prompts/:id/archive": {"归档 Prompt", "归档某个 Prompt,新订阅不再可见。"},
"POST /api/tenant/kol/manage/prompts/:id/publish": {"发布 Prompt 版本", "将当前草稿发布为新版本,订阅者可使用。"},
"PUT /api/tenant/kol/manage/prompts/:id/activate": {"激活 Prompt", "把已发布的 Prompt 切换为当前激活版本。"},
"PUT /api/tenant/kol/manage/prompts/:id/archive": {"归档 Prompt", "归档某个 Prompt,新订阅不再可见。"},
"POST /api/tenant/kol/manage/assist": {"KOL 创作助手(同步)", "基于上下文一次性生成 Prompt 优化建议。"},
"POST /api/tenant/kol/manage/assist/stream": {"KOL 创作助手(SSE 流式)", "通过 Server-Sent Events 流式返回创作助手输出。"},
@@ -126,63 +136,63 @@ var routeDocs = map[string]routeDoc{
"GET /api/tenant/kol/dashboard/trend": {"KOL 看板趋势", "按日聚合的订阅、生成、收益趋势曲线。"},
// --- TenantKOL Marketplace(订阅方视角) ---
"GET /api/tenant/kol/marketplace/packages": {"KOL 市场套餐列表", "面向订阅方的 KOL 市场列表,可按行业/关键词筛选。"},
"GET /api/tenant/kol/marketplace/packages/:id": {"KOL 市场套餐详情", "套餐详情页:包含价格、Prompt 列表、案例等。"},
"GET /api/tenant/kol/marketplace/packages": {"KOL 市场套餐列表", "面向订阅方的 KOL 市场列表,可按行业/关键词筛选。"},
"GET /api/tenant/kol/marketplace/packages/:id": {"KOL 市场套餐详情", "套餐详情页:包含价格、Prompt 列表、案例等。"},
"POST /api/tenant/kol/marketplace/packages/:id/subscribe": {"订阅 KOL 套餐", "当前 Workspace 订阅指定套餐,进入计费/试用流程。"},
"GET /api/tenant/kol/subscriptions": {"我的 KOL 订阅", "返回当前 Workspace 已订阅的 KOL 套餐列表。"},
"GET /api/tenant/kol/subscription-prompts": {"我的订阅 Prompt 列表", "已订阅套餐下可用的 Prompt 集合。"},
"GET /api/tenant/kol/subscription-prompts/:id/schema": {"订阅 Prompt 输入 Schema", "返回该 Prompt 的入参字段定义,用于前端渲染表单。"},
"GET /api/tenant/kol/subscriptions": {"我的 KOL 订阅", "返回当前 Workspace 已订阅的 KOL 套餐列表。"},
"GET /api/tenant/kol/subscription-prompts": {"我的订阅 Prompt 列表", "已订阅套餐下可用的 Prompt 集合。"},
"GET /api/tenant/kol/subscription-prompts/:id/schema": {"订阅 Prompt 输入 Schema", "返回该 Prompt 的入参字段定义,用于前端渲染表单。"},
"POST /api/tenant/kol/subscription-prompts/:id/generate": {"用订阅 Prompt 生成内容", "基于订阅 Prompt 生成一篇文章,自动扣 KOL 计费点数。"},
// --- Tenant:文章 ---
"GET /api/tenant/articles": {"文章列表", "支持按生成/发布状态、来源、关键词、时间等筛选。"},
"POST /api/tenant/articles": {"创建文章", "手动创建一篇空文章草稿。"},
"POST /api/tenant/articles/generate-from-rule": {"根据 Prompt 规则生成文章", "基于已配置的 Prompt 规则生成一篇文章。"},
"POST /api/tenant/articles/imitations/generate": {"仿写生成文章", "基于参考文链接/原文进行仿写并生成新文章。"},
"POST /api/tenant/articles/:id/images": {"上传文章配图", "Multipart 上传文章正文使用的图片。"},
"GET /api/tenant/articles": {"文章列表", "支持按生成/发布状态、来源、关键词、时间等筛选。"},
"POST /api/tenant/articles": {"创建文章", "手动创建一篇空文章草稿。"},
"POST /api/tenant/articles/generate-from-rule": {"根据 Prompt 规则生成文章", "基于已配置的 Prompt 规则生成一篇文章。"},
"POST /api/tenant/articles/imitations/generate": {"仿写生成文章", "基于参考文链接/原文进行仿写并生成新文章。"},
"POST /api/tenant/articles/:id/images": {"上传文章配图", "Multipart 上传文章正文使用的图片。"},
"POST /api/tenant/articles/:id/selection-optimize/stream": {"选区优化(SSE 流式)", "针对用户选中的段落做润色/改写,结果流式返回。"},
"GET /api/tenant/articles/:id": {"文章详情", "返回文章的完整结构(标题、大纲、正文、版本号等)。"},
"PUT /api/tenant/articles/:id": {"保存文章", "全量更新文章内容(标题、正文、媒体附件等)。"},
"GET /api/tenant/articles/:id/stream": {"文章生成 SSE 流", "订阅文章正文生成过程的流式输出。"},
"GET /api/tenant/articles/:id/versions": {"文章版本历史", "返回该文章的历史版本列表,可用于回滚。"},
"GET /api/tenant/articles/:id/publish-records": {"文章发布记录", "返回该文章在各媒体平台的发布历史。"},
"DELETE /api/tenant/articles/:id": {"删除文章", "软删除文章;可在回收站撤回(需带 undo 参数)。"},
"GET /api/tenant/articles/:id": {"文章详情", "返回文章的完整结构(标题、大纲、正文、版本号等)。"},
"PUT /api/tenant/articles/:id": {"保存文章", "全量更新文章内容(标题、正文、媒体附件等)。"},
"GET /api/tenant/articles/:id/stream": {"文章生成 SSE 流", "订阅文章正文生成过程的流式输出。"},
"GET /api/tenant/articles/:id/versions": {"文章版本历史", "返回该文章的历史版本列表,可用于回滚。"},
"GET /api/tenant/articles/:id/publish-records": {"文章发布记录", "返回该文章在各媒体平台的发布历史。"},
"DELETE /api/tenant/articles/:id": {"删除文章", "软删除文章;可在回收站撤回(需带 undo 参数)。"},
// --- Tenant:品牌库 ---
"GET /api/tenant/brands": {"品牌列表", "返回当前 Workspace 下的品牌列表。"},
"GET /api/tenant/brands": {"品牌列表", "返回当前 Workspace 下的品牌列表。"},
"GET /api/tenant/brands/library-summary": {"品牌库概览", "品牌、关键词、问题、竞品的总数与最近变更。"},
"POST /api/tenant/brands": {"新建品牌", "创建一个新品牌。"},
"GET /api/tenant/brands/:id": {"品牌详情", "返回品牌基础信息及统计。"},
"PUT /api/tenant/brands/:id": {"更新品牌", "更新品牌名称、行业、官网等。"},
"DELETE /api/tenant/brands/:id": {"删除品牌", "删除品牌及其下属关键词、问题、竞品。"},
"GET /api/tenant/brands/:id/keywords": {"品牌关键词列表", "返回某品牌下的关键词。"},
"POST /api/tenant/brands/:id/keywords": {"新增关键词", "为某品牌添加关键词。"},
"PUT /api/tenant/brands/:id/keywords/:kid": {"更新关键词", "修改关键词文本/分组。"},
"DELETE /api/tenant/brands/:id/keywords/:kid": {"删除关键词", "删除某条关键词。"},
"GET /api/tenant/brands/:id/keywords": {"品牌关键词列表", "返回某品牌下的关键词。"},
"POST /api/tenant/brands/:id/keywords": {"新增关键词", "为某品牌添加关键词。"},
"PUT /api/tenant/brands/:id/keywords/:kid": {"更新关键词", "修改关键词文本/分组。"},
"DELETE /api/tenant/brands/:id/keywords/:kid": {"删除关键词", "删除某条关键词。"},
"GET /api/tenant/brands/:id/questions": {"品牌问题列表", "返回品牌下的监控问题,可按 keyword_id 过滤。"},
"POST /api/tenant/brands/:id/questions": {"新增监控问题", "为品牌添加一条 GEO 监控问题。"},
"PUT /api/tenant/brands/:id/questions/:qid": {"更新监控问题", "修改问题文本或所属关键词。"},
"DELETE /api/tenant/brands/:id/questions/:qid": {"删除监控问题", "删除某条监控问题。"},
"GET /api/tenant/brands/:id/questions": {"品牌问题列表", "返回品牌下的监控问题,可按 keyword_id 过滤。"},
"POST /api/tenant/brands/:id/questions": {"新增监控问题", "为品牌添加一条 GEO 监控问题。"},
"PUT /api/tenant/brands/:id/questions/:qid": {"更新监控问题", "修改问题文本或所属关键词。"},
"DELETE /api/tenant/brands/:id/questions/:qid": {"删除监控问题", "删除某条监控问题。"},
"GET /api/tenant/brands/:id/competitors": {"竞品列表", "返回品牌下登记的竞品。"},
"POST /api/tenant/brands/:id/competitors": {"新增竞品", "为品牌添加竞品记录。"},
"PUT /api/tenant/brands/:id/competitors/:cid": {"更新竞品", "修改竞品名称/官网/备注。"},
"DELETE /api/tenant/brands/:id/competitors/:cid": {"删除竞品", "删除某条竞品记录。"},
"GET /api/tenant/brands/:id/competitors": {"竞品列表", "返回品牌下登记的竞品。"},
"POST /api/tenant/brands/:id/competitors": {"新增竞品", "为品牌添加竞品记录。"},
"PUT /api/tenant/brands/:id/competitors/:cid": {"更新竞品", "修改竞品名称/官网/备注。"},
"DELETE /api/tenant/brands/:id/competitors/:cid": {"删除竞品", "删除某条竞品记录。"},
// --- Tenant:监控(GEO 仪表盘) ---
"GET /api/tenant/monitoring/dashboard/composite": {"监控仪表盘综合视图", "组合返回品牌曝光、引用率、问答覆盖等监控指标。"},
"GET /api/tenant/monitoring/citation-summary": {"引用情况汇总", "按 AI 平台维度返回品牌的引用频次与变化。"},
"GET /api/tenant/monitoring/brands/:brand_id/questions/:question_id/detail": {"问题级监控详情", "某问题在各 AI 平台的回答样本与引用拆解。"},
"POST /api/tenant/monitoring/brands/:brand_id/collect-now": {"立即触发采集", "对某品牌立即下发一次监控采集任务,跳过定时计划。"},
"GET /api/tenant/monitoring/dashboard/composite": {"监控仪表盘综合视图", "组合返回品牌曝光、引用率、问答覆盖等监控指标。"},
"GET /api/tenant/monitoring/citation-summary": {"引用情况汇总", "按 AI 平台维度返回品牌的引用频次与变化。"},
"GET /api/tenant/monitoring/brands/:brand_id/questions/:question_id/detail": {"问题级监控详情", "某问题在各 AI 平台的回答样本与引用拆解。"},
"POST /api/tenant/monitoring/brands/:brand_id/collect-now": {"立即触发采集", "对某品牌立即下发一次监控采集任务,跳过定时计划。"},
// --- Tenant:知识库 ---
"GET /api/tenant/knowledge/groups": {"知识库分组列表", "返回当前 Workspace 的知识库分组。"},
"POST /api/tenant/knowledge/groups": {"新建知识库分组", "创建新的知识库分组。"},
"PUT /api/tenant/knowledge/groups/:gid": {"更新知识库分组", "重命名/移动分组。"},
"DELETE /api/tenant/knowledge/groups/:gid": {"删除知识库分组", "删除分组及其下条目(按业务规则限制)。"},
"GET /api/tenant/knowledge/groups": {"知识库分组列表", "返回当前 Workspace 的知识库分组。"},
"POST /api/tenant/knowledge/groups": {"新建知识库分组", "创建新的知识库分组。"},
"PUT /api/tenant/knowledge/groups/:gid": {"更新知识库分组", "重命名/移动分组。"},
"DELETE /api/tenant/knowledge/groups/:gid": {"删除知识库分组", "删除分组及其下条目(按业务规则限制)。"},
"GET /api/tenant/knowledge/items": {"知识条目列表", "按分组返回知识库条目,可分页。"},
"GET /api/tenant/knowledge/items/:id": {"知识条目详情", "返回条目的解析后正文、来源、向量化状态。"},
@@ -193,17 +203,17 @@ var routeDocs = map[string]routeDoc{
"DELETE /api/tenant/knowledge/items/:id": {"删除知识条目", "彻底删除某条知识条目。"},
// --- TenantPrompt 规则 ---
"GET /api/tenant/prompt-rules": {"Prompt 规则列表", "查询 Prompt 规则,支持分组、状态、关键字筛选。"},
"GET /api/tenant/prompt-rules/simple": {"Prompt 规则下拉项", "为下拉框/选择器提供精简结构(id + 名称)。"},
"POST /api/tenant/prompt-rules": {"新建 Prompt 规则", "创建新的 Prompt 生成规则。"},
"GET /api/tenant/prompt-rules/:id": {"Prompt 规则详情", "返回规则完整配置(变量、模板、约束)。"},
"PUT /api/tenant/prompt-rules/:id": {"更新 Prompt 规则", "修改规则配置。"},
"DELETE /api/tenant/prompt-rules/:id": {"删除 Prompt 规则", "删除规则;若有定时任务在引用则禁止。"},
"PUT /api/tenant/prompt-rules/:id/status": {"切换 Prompt 规则启停", "启用或禁用 Prompt 规则。"},
"GET /api/tenant/prompt-rules/groups": {"Prompt 规则分组列表", "返回 Prompt 规则的分组用于左侧导航。"},
"POST /api/tenant/prompt-rules/groups": {"新建分组", "创建 Prompt 规则分组。"},
"PUT /api/tenant/prompt-rules/groups/:gid": {"更新分组", "重命名分组或调整顺序。"},
"DELETE /api/tenant/prompt-rules/groups/:gid": {"删除分组", "删除空分组。"},
"GET /api/tenant/prompt-rules": {"Prompt 规则列表", "查询 Prompt 规则,支持分组、状态、关键字筛选。"},
"GET /api/tenant/prompt-rules/simple": {"Prompt 规则下拉项", "为下拉框/选择器提供精简结构(id + 名称)。"},
"POST /api/tenant/prompt-rules": {"新建 Prompt 规则", "创建新的 Prompt 生成规则。"},
"GET /api/tenant/prompt-rules/:id": {"Prompt 规则详情", "返回规则完整配置(变量、模板、约束)。"},
"PUT /api/tenant/prompt-rules/:id": {"更新 Prompt 规则", "修改规则配置。"},
"DELETE /api/tenant/prompt-rules/:id": {"删除 Prompt 规则", "删除规则;若有定时任务在引用则禁止。"},
"PUT /api/tenant/prompt-rules/:id/status": {"切换 Prompt 规则启停", "启用或禁用 Prompt 规则。"},
"GET /api/tenant/prompt-rules/groups": {"Prompt 规则分组列表", "返回 Prompt 规则的分组用于左侧导航。"},
"POST /api/tenant/prompt-rules/groups": {"新建分组", "创建 Prompt 规则分组。"},
"PUT /api/tenant/prompt-rules/groups/:gid": {"更新分组", "重命名分组或调整顺序。"},
"DELETE /api/tenant/prompt-rules/groups/:gid": {"删除分组", "删除空分组。"},
// --- Tenant:定时任务 ---
"GET /api/tenant/schedules": {"定时任务列表", "查询定时生成任务,支持按 Prompt 规则、状态、时间筛选。"},
@@ -217,24 +227,24 @@ var routeDocs = map[string]routeDoc{
"GET /api/tenant/instant-tasks": {"即时任务列表", "查询用户手动触发的即时生成任务。"},
// --- Tenant:图片库 ---
"GET /api/tenant/images": {"图片列表", "分页查询图片,可按分组、关键词筛选。"},
"POST /api/tenant/images": {"上传图片", "Multipart 上传单张图片到指定分组。"},
"PUT /api/tenant/images/:id": {"更新图片", "重命名、移动分组或修改 alt。"},
"GET /api/tenant/images/:id/references": {"图片引用情况", "查询图片被哪些文章/草稿引用。"},
"DELETE /api/tenant/images/:id": {"删除图片", "软删除图片,可强制(force=1)跳过引用校验。"},
"GET /api/tenant/images/storage-usage": {"图片存储用量", "返回当前 Workspace 已使用的图片存储与配额。"},
"GET /api/tenant/images": {"图片列表", "分页查询图片,可按分组、关键词筛选。"},
"POST /api/tenant/images": {"上传图片", "Multipart 上传单张图片到指定分组。"},
"PUT /api/tenant/images/:id": {"更新图片", "重命名、移动分组或修改 alt。"},
"GET /api/tenant/images/:id/references": {"图片引用情况", "查询图片被哪些文章/草稿引用。"},
"DELETE /api/tenant/images/:id": {"删除图片", "软删除图片,可强制(force=1)跳过引用校验。"},
"GET /api/tenant/images/storage-usage": {"图片存储用量", "返回当前 Workspace 已使用的图片存储与配额。"},
"GET /api/tenant/images/folders": {"图片分组列表", "返回图片分组树。"},
"POST /api/tenant/images/folders": {"创建图片分组", "新建图片分组。"},
"PUT /api/tenant/images/folders/:id": {"更新图片分组", "重命名或移动分组。"},
"GET /api/tenant/images/folders": {"图片分组列表", "返回图片分组树。"},
"POST /api/tenant/images/folders": {"创建图片分组", "新建图片分组。"},
"PUT /api/tenant/images/folders/:id": {"更新图片分组", "重命名或移动分组。"},
"GET /api/tenant/images/folders/:id/delete-preview": {"删除分组预览", "返回删除该分组将影响的图片数量。"},
"DELETE /api/tenant/images/folders/:id": {"删除图片分组", "删除空分组。"},
"DELETE /api/tenant/images/folders/:id": {"删除图片分组", "删除空分组。"},
// --- Swagger 自身 ---
"GET /swagger": {"Swagger 入口", "重定向至 /swagger/index.html。"},
"GET /swagger/": {"Swagger 入口", "重定向至 /swagger/index.html。"},
"GET /swagger/index.html": {"Swagger UI", "提供本服务的 Swagger UI 页面。"},
"GET /swagger/openapi.json": {"OpenAPI 规范", "返回当前服务自动生成的 OpenAPI 3 文档。"},
"GET /swagger": {"Swagger 入口", "重定向至 /swagger/index.html。"},
"GET /swagger/": {"Swagger 入口", "重定向至 /swagger/index.html。"},
"GET /swagger/index.html": {"Swagger UI", "提供本服务的 Swagger UI 页面。"},
"GET /swagger/openapi.json": {"OpenAPI 规范", "返回当前服务自动生成的 OpenAPI 3 文档。"},
}
// docFor returns the documented summary/description for a route, falling back