feat(article): expose auto_publish_platforms derived from publish accounts
Replace the freeform `platforms` field on articles/tasks (parsed out of wizard_state/input_params JSON) with `auto_publish_platforms`, resolved by joining schedule publish accounts to platform_accounts. The migration strips the legacy keys from existing rows so the new field is the single source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -116,7 +116,7 @@ const deleteMutation = useMutation({
|
|||||||
const columns = computed<TableColumnsType<ArticleListItem>>(() => [
|
const columns = computed<TableColumnsType<ArticleListItem>>(() => [
|
||||||
{ title: t("custom.article.titleColumn"), dataIndex: "title", key: "title", width: 340 },
|
{ title: t("custom.article.titleColumn"), dataIndex: "title", key: "title", width: 340 },
|
||||||
{ title: t("custom.filters.promptRule"), dataIndex: "prompt_rule_name", key: "prompt_rule_name", width: 160 },
|
{ title: t("custom.filters.promptRule"), dataIndex: "prompt_rule_name", key: "prompt_rule_name", width: 160 },
|
||||||
{ title: t("custom.schedule.platform"), dataIndex: "platforms", key: "platforms", width: 180 },
|
{ title: t("custom.schedule.autoPublishPlatforms"), dataIndex: "auto_publish_platforms", key: "auto_publish_platforms", width: 180 },
|
||||||
{ title: t("common.generateStatus"), dataIndex: "generate_status", key: "generate_status", width: 128 },
|
{ title: t("common.generateStatus"), dataIndex: "generate_status", key: "generate_status", width: 128 },
|
||||||
{ title: t("common.publishStatus"), dataIndex: "publish_status", key: "publish_status", width: 128 },
|
{ title: t("common.publishStatus"), dataIndex: "publish_status", key: "publish_status", width: 128 },
|
||||||
{ title: t("common.wordCount"), dataIndex: "word_count", key: "word_count", width: 100 },
|
{ title: t("common.wordCount"), dataIndex: "word_count", key: "word_count", width: 100 },
|
||||||
@@ -319,8 +319,8 @@ onBeforeUnmount(() => {
|
|||||||
<template v-else-if="column.key === 'prompt_rule_name'">
|
<template v-else-if="column.key === 'prompt_rule_name'">
|
||||||
{{ record.prompt_rule_name || "--" }}
|
{{ record.prompt_rule_name || "--" }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'platforms'">
|
<template v-else-if="column.key === 'auto_publish_platforms'">
|
||||||
{{ formatPublishPlatformList(record.platforms) }}
|
{{ formatPublishPlatformList(record.auto_publish_platforms) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'generate_status'">
|
<template v-else-if="column.key === 'generate_status'">
|
||||||
<ArticleGenerateStatus :status="record.generate_status" />
|
<ArticleGenerateStatus :status="record.generate_status" />
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const columns = computed<TableColumnsType<InstantTaskItem>>(() => [
|
|||||||
{ title: t("custom.schedule.rule"), dataIndex: "prompt_rule_name", key: "prompt_rule_name", width: 180 },
|
{ title: t("custom.schedule.rule"), dataIndex: "prompt_rule_name", key: "prompt_rule_name", width: 180 },
|
||||||
{ title: t("custom.instant.executionStatus"), dataIndex: "status", key: "status", width: 140 },
|
{ title: t("custom.instant.executionStatus"), dataIndex: "status", key: "status", width: 140 },
|
||||||
{ title: t("custom.instant.executionTime"), dataIndex: "execution_time", key: "execution_time", width: 170 },
|
{ title: t("custom.instant.executionTime"), dataIndex: "execution_time", key: "execution_time", width: 170 },
|
||||||
{ title: t("custom.schedule.platform"), dataIndex: "platforms", key: "platforms", width: 220 },
|
{ title: t("custom.schedule.autoPublishPlatforms"), dataIndex: "auto_publish_platforms", key: "auto_publish_platforms", width: 220 },
|
||||||
{ title: t("custom.task.generateCount"), dataIndex: "generate_count", key: "generate_count", width: 120 },
|
{ title: t("custom.task.generateCount"), dataIndex: "generate_count", key: "generate_count", width: 120 },
|
||||||
{ title: t("custom.instant.createdTime"), dataIndex: "created_at", key: "created_at", width: 170 },
|
{ title: t("custom.instant.createdTime"), dataIndex: "created_at", key: "created_at", width: 170 },
|
||||||
{ title: t("common.actions"), key: "actions", width: 88, fixed: "right", align: "right" },
|
{ title: t("common.actions"), key: "actions", width: 88, fixed: "right", align: "right" },
|
||||||
@@ -235,8 +235,8 @@ onBeforeUnmount(() => {
|
|||||||
<template v-else-if="column.key === 'execution_time'">
|
<template v-else-if="column.key === 'execution_time'">
|
||||||
{{ formatDateTime(record.execution_time) }}
|
{{ formatDateTime(record.execution_time) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'platforms'">
|
<template v-else-if="column.key === 'auto_publish_platforms'">
|
||||||
{{ formatPublishPlatformList(record.platforms) }}
|
{{ formatPublishPlatformList(record.auto_publish_platforms) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'generate_count'">
|
<template v-else-if="column.key === 'generate_count'">
|
||||||
{{ record.generate_count || 1 }}
|
{{ record.generate_count || 1 }}
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ import {
|
|||||||
publishStateLabel,
|
publishStateLabel,
|
||||||
type PublishAccountCard,
|
type PublishAccountCard,
|
||||||
} from "@/lib/publish-account-cards";
|
} from "@/lib/publish-account-cards";
|
||||||
import {
|
|
||||||
normalizePublishPlatformIds,
|
|
||||||
} from "@/lib/publish-platforms";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -93,10 +90,8 @@ watch(
|
|||||||
|
|
||||||
const platformMap = computed(() => buildPublishPlatformMap(platformsQuery.data.value ?? []));
|
const platformMap = computed(() => buildPublishPlatformMap(platformsQuery.data.value ?? []));
|
||||||
|
|
||||||
const articlePlatformIds = computed(() => normalizePublishPlatformIds(detailQuery.data.value?.platforms ?? []));
|
|
||||||
|
|
||||||
const accountCards = computed<PublishAccountCard[]>(() =>
|
const accountCards = computed<PublishAccountCard[]>(() =>
|
||||||
buildPublishAccountCards(accountsQuery.data.value ?? [], platformMap.value, articlePlatformIds.value),
|
buildPublishAccountCards(accountsQuery.data.value ?? [], platformMap.value),
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -137,10 +132,7 @@ watchEffect(() => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferredPlatforms = new Set(articlePlatformIds.value);
|
selectedAccountIds.value = [];
|
||||||
selectedAccountIds.value = accountCards.value
|
|
||||||
.filter((card) => card.selectable && preferredPlatforms.has(card.platformId))
|
|
||||||
.map((card) => card.id);
|
|
||||||
selectionHydrated.value = true;
|
selectionHydrated.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,7 +229,6 @@ async function persistCoverIfNeeded(detail: ArticleDetail): Promise<ArticleDetai
|
|||||||
return articlesApi.update(detail.id, {
|
return articlesApi.update(detail.id, {
|
||||||
title: detail.title?.trim() || t("article.untitled"),
|
title: detail.title?.trim() || t("article.untitled"),
|
||||||
markdown_content: detail.markdown_content ?? "",
|
markdown_content: detail.markdown_content ?? "",
|
||||||
platforms: normalizePublishPlatformIds(detail.platforms ?? []),
|
|
||||||
cover_asset_url: nextUrl || null,
|
cover_asset_url: nextUrl || null,
|
||||||
cover_image_asset_id: coverImageAssetId.value,
|
cover_image_asset_id: coverImageAssetId.value,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import ScheduleTaskModal from "@/components/ScheduleTaskModal.vue";
|
|||||||
import { promptRulesApi, schedulesApi } from "@/lib/api";
|
import { promptRulesApi, schedulesApi } from "@/lib/api";
|
||||||
import { formatCronExecutionTime, formatDateTime } from "@/lib/display";
|
import { formatCronExecutionTime, formatDateTime } from "@/lib/display";
|
||||||
import { formatError } from "@/lib/errors";
|
import { formatError } from "@/lib/errors";
|
||||||
|
import { formatPublishPlatformList } from "@/lib/publish-platforms";
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -84,7 +85,7 @@ const columns = computed<TableColumnsType<ScheduleTask>>(() => [
|
|||||||
{ title: t("custom.schedule.rule"), dataIndex: "prompt_rule_name", key: "prompt_rule_name", width: 180 },
|
{ title: t("custom.schedule.rule"), dataIndex: "prompt_rule_name", key: "prompt_rule_name", width: 180 },
|
||||||
{ title: t("custom.instant.executionStatus"), dataIndex: "status", key: "status", width: 140 },
|
{ title: t("custom.instant.executionStatus"), dataIndex: "status", key: "status", width: 140 },
|
||||||
{ title: t("custom.instant.executionTime"), dataIndex: "cron_expr", key: "cron_expr", width: 170 },
|
{ title: t("custom.instant.executionTime"), dataIndex: "cron_expr", key: "cron_expr", width: 170 },
|
||||||
{ title: t("custom.schedule.autoPublish"), dataIndex: "auto_publish", key: "auto_publish", width: 160 },
|
{ title: t("custom.schedule.autoPublishPlatforms"), dataIndex: "auto_publish_platforms", key: "auto_publish_platforms", width: 200 },
|
||||||
{ title: t("custom.task.generateCount"), dataIndex: "generate_count", key: "generate_count", width: 120 },
|
{ title: t("custom.task.generateCount"), dataIndex: "generate_count", key: "generate_count", width: 120 },
|
||||||
{ title: t("custom.instant.createdTime"), dataIndex: "created_at", key: "created_at", width: 170 },
|
{ title: t("custom.instant.createdTime"), dataIndex: "created_at", key: "created_at", width: 170 },
|
||||||
{ title: t("common.actions"), key: "actions", width: 140, fixed: "right", align: "right" },
|
{ title: t("common.actions"), key: "actions", width: 140, fixed: "right", align: "right" },
|
||||||
@@ -222,11 +223,11 @@ function handleTableChange(nextPage: number, nextPageSize: number): void {
|
|||||||
<template v-else-if="column.key === 'cron_expr'">
|
<template v-else-if="column.key === 'cron_expr'">
|
||||||
{{ formatCronExecutionTime(record.cron_expr) }}
|
{{ formatCronExecutionTime(record.cron_expr) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'auto_publish'">
|
<template v-else-if="column.key === 'auto_publish_platforms'">
|
||||||
<a-tag :color="record.auto_publish ? 'blue' : 'default'">
|
<a-tag :color="record.auto_publish ? 'blue' : 'default'">
|
||||||
{{
|
{{
|
||||||
record.auto_publish
|
record.auto_publish
|
||||||
? t("custom.schedule.autoPublishAccountCount", { count: record.publish_account_ids?.length ?? 0 })
|
? formatPublishPlatformList(record.auto_publish_platforms)
|
||||||
: t("custom.schedule.manualPublish")
|
: t("custom.schedule.manualPublish")
|
||||||
}}
|
}}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
|
|||||||
@@ -1290,9 +1290,9 @@ const enUS = {
|
|||||||
name: "Task name",
|
name: "Task name",
|
||||||
rule: "Prompt rule",
|
rule: "Prompt rule",
|
||||||
cron: "Frequency",
|
cron: "Frequency",
|
||||||
|
autoPublishPlatforms: "Auto publish platforms",
|
||||||
autoPublish: "Auto publish",
|
autoPublish: "Auto publish",
|
||||||
manualPublish: "Manual publish",
|
manualPublish: "Manual publish",
|
||||||
autoPublishAccountCount: "Auto · {count} accounts",
|
|
||||||
nextRun: "Next run",
|
nextRun: "Next run",
|
||||||
startAt: "Start time",
|
startAt: "Start time",
|
||||||
endAt: "End time",
|
endAt: "End time",
|
||||||
|
|||||||
@@ -1300,9 +1300,9 @@ const zhCN = {
|
|||||||
name: "任务名称",
|
name: "任务名称",
|
||||||
rule: "关联规则",
|
rule: "关联规则",
|
||||||
cron: "执行频率",
|
cron: "执行频率",
|
||||||
|
autoPublishPlatforms: "自动发布平台",
|
||||||
autoPublish: "自动发布",
|
autoPublish: "自动发布",
|
||||||
manualPublish: "手动发布",
|
manualPublish: "手动发布",
|
||||||
autoPublishAccountCount: "自动 · {count} 个账号",
|
|
||||||
nextRun: "下次执行",
|
nextRun: "下次执行",
|
||||||
startAt: "开始时间",
|
startAt: "开始时间",
|
||||||
endAt: "结束时间",
|
endAt: "结束时间",
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ const recentTemplateArticles = computed<ArticleListItem[]>(() =>
|
|||||||
prompt_rule_id: null,
|
prompt_rule_id: null,
|
||||||
prompt_rule_name: null,
|
prompt_rule_name: null,
|
||||||
generation_mode: article.generation_mode,
|
generation_mode: article.generation_mode,
|
||||||
platforms: [],
|
auto_publish_platforms: [],
|
||||||
generate_status: article.generate_status,
|
generate_status: article.generate_status,
|
||||||
publish_status: article.publish_status,
|
publish_status: article.publish_status,
|
||||||
title: article.title,
|
title: article.title,
|
||||||
|
|||||||
@@ -557,7 +557,7 @@ export interface ArticleListItem {
|
|||||||
prompt_rule_id: number | null;
|
prompt_rule_id: number | null;
|
||||||
prompt_rule_name: string | null;
|
prompt_rule_name: string | null;
|
||||||
generation_mode: string | null;
|
generation_mode: string | null;
|
||||||
platforms: string[];
|
auto_publish_platforms: string[];
|
||||||
generate_status: string;
|
generate_status: string;
|
||||||
publish_status: string;
|
publish_status: string;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
@@ -582,7 +582,7 @@ export interface ArticleDetail {
|
|||||||
template_id: number | null;
|
template_id: number | null;
|
||||||
template_name: string | null;
|
template_name: string | null;
|
||||||
generation_mode: string | null;
|
generation_mode: string | null;
|
||||||
platforms: string[];
|
auto_publish_platforms: string[];
|
||||||
cover_asset_url: string | null;
|
cover_asset_url: string | null;
|
||||||
cover_image_asset_id: number | null;
|
cover_image_asset_id: number | null;
|
||||||
generate_status: string;
|
generate_status: string;
|
||||||
@@ -632,7 +632,6 @@ export interface GenerateImitationResponse {
|
|||||||
export interface UpdateArticleRequest {
|
export interface UpdateArticleRequest {
|
||||||
title: string;
|
title: string;
|
||||||
markdown_content: string;
|
markdown_content: string;
|
||||||
platforms?: string[];
|
|
||||||
cover_asset_url?: string | null;
|
cover_asset_url?: string | null;
|
||||||
referenced_image_asset_ids?: number[];
|
referenced_image_asset_ids?: number[];
|
||||||
cover_image_asset_id?: number | null;
|
cover_image_asset_id?: number | null;
|
||||||
@@ -1448,6 +1447,7 @@ export interface ScheduleTask {
|
|||||||
cron_expr: string;
|
cron_expr: string;
|
||||||
auto_publish: boolean;
|
auto_publish: boolean;
|
||||||
publish_account_ids: string[];
|
publish_account_ids: string[];
|
||||||
|
auto_publish_platforms: string[];
|
||||||
cover_asset_url: string | null;
|
cover_asset_url: string | null;
|
||||||
cover_image_asset_id: number | null;
|
cover_image_asset_id: number | null;
|
||||||
enable_web_search: boolean;
|
enable_web_search: boolean;
|
||||||
@@ -1534,7 +1534,7 @@ export interface InstantTaskItem {
|
|||||||
name: string;
|
name: string;
|
||||||
status: string;
|
status: string;
|
||||||
execution_time: string | null;
|
execution_time: string | null;
|
||||||
platforms: string[];
|
auto_publish_platforms: string[];
|
||||||
generate_count: number;
|
generate_count: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ type ArticleListItem struct {
|
|||||||
PromptRuleID *int64 `json:"prompt_rule_id"`
|
PromptRuleID *int64 `json:"prompt_rule_id"`
|
||||||
PromptRuleName *string `json:"prompt_rule_name"`
|
PromptRuleName *string `json:"prompt_rule_name"`
|
||||||
GenerationMode *string `json:"generation_mode"`
|
GenerationMode *string `json:"generation_mode"`
|
||||||
Platforms []string `json:"platforms"`
|
AutoPublishPlatforms []string `json:"auto_publish_platforms"`
|
||||||
GenerateStatus string `json:"generate_status"`
|
GenerateStatus string `json:"generate_status"`
|
||||||
PublishStatus string `json:"publish_status"`
|
PublishStatus string `json:"publish_status"`
|
||||||
Title *string `json:"title"`
|
Title *string `json:"title"`
|
||||||
@@ -147,7 +147,7 @@ type ArticleDetailResponse struct {
|
|||||||
TemplateID *int64 `json:"template_id"`
|
TemplateID *int64 `json:"template_id"`
|
||||||
TemplateName *string `json:"template_name"`
|
TemplateName *string `json:"template_name"`
|
||||||
GenerationMode *string `json:"generation_mode"`
|
GenerationMode *string `json:"generation_mode"`
|
||||||
Platforms []string `json:"platforms"`
|
AutoPublishPlatforms []string `json:"auto_publish_platforms"`
|
||||||
CoverAssetURL *string `json:"cover_asset_url"`
|
CoverAssetURL *string `json:"cover_asset_url"`
|
||||||
CoverImageAssetID *int64 `json:"cover_image_asset_id"`
|
CoverImageAssetID *int64 `json:"cover_image_asset_id"`
|
||||||
GenerateStatus string `json:"generate_status"`
|
GenerateStatus string `json:"generate_status"`
|
||||||
@@ -355,7 +355,11 @@ func (s *ArticleService) loadArticles(ctx context.Context, tenantID int64, param
|
|||||||
}
|
}
|
||||||
item.SourceType = dbSourceType
|
item.SourceType = dbSourceType
|
||||||
item.GenerationMode = resolveArticleGenerationMode(dbSourceType, inputParamsJSON)
|
item.GenerationMode = resolveArticleGenerationMode(dbSourceType, inputParamsJSON)
|
||||||
item.Platforms = resolveArticlePlatforms(wizardStateJSON, inputParamsJSON)
|
autoPublishPlatforms, err := loadScheduleAutoPublishPlatforms(ctx, s.pool, tenantID, inputParamsJSON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, response.ErrInternal(50010, "query_failed", "failed to resolve article auto publish platforms")
|
||||||
|
}
|
||||||
|
item.AutoPublishPlatforms = autoPublishPlatforms
|
||||||
item.SourceURL, item.SourceTitle = resolveArticleImitationSourceInfo(dbSourceType, wizardStateJSON, inputParamsJSON)
|
item.SourceURL, item.SourceTitle = resolveArticleImitationSourceInfo(dbSourceType, wizardStateJSON, inputParamsJSON)
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
@@ -407,7 +411,11 @@ func (s *ArticleService) loadArticleDetail(ctx context.Context, tenantID, articl
|
|||||||
_ = json.Unmarshal(wizardStateJSON, &item.WizardState)
|
_ = json.Unmarshal(wizardStateJSON, &item.WizardState)
|
||||||
}
|
}
|
||||||
item.GenerationMode = resolveArticleGenerationMode(dbSourceType, inputParamsJSON)
|
item.GenerationMode = resolveArticleGenerationMode(dbSourceType, inputParamsJSON)
|
||||||
item.Platforms = resolveArticlePlatforms(wizardStateJSON, inputParamsJSON)
|
autoPublishPlatforms, err := loadScheduleAutoPublishPlatforms(ctx, s.pool, tenantID, inputParamsJSON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, response.ErrInternal(50010, "query_failed", "failed to resolve article auto publish platforms")
|
||||||
|
}
|
||||||
|
item.AutoPublishPlatforms = autoPublishPlatforms
|
||||||
item.SourceURL, item.SourceTitle = resolveArticleImitationSourceInfo(dbSourceType, wizardStateJSON, inputParamsJSON)
|
item.SourceURL, item.SourceTitle = resolveArticleImitationSourceInfo(dbSourceType, wizardStateJSON, inputParamsJSON)
|
||||||
item.CoverAssetURL = resolveArticleCoverAssetURL(wizardStateJSON)
|
item.CoverAssetURL = resolveArticleCoverAssetURL(wizardStateJSON)
|
||||||
item.CoverImageAssetID = resolveArticleCoverImageAssetID(wizardStateJSON)
|
item.CoverImageAssetID = resolveArticleCoverImageAssetID(wizardStateJSON)
|
||||||
@@ -495,7 +503,6 @@ func (s *ArticleService) Create(ctx context.Context, req CreateArticleRequest) (
|
|||||||
type UpdateArticleRequest struct {
|
type UpdateArticleRequest struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
MarkdownContent string `json:"markdown_content"`
|
MarkdownContent string `json:"markdown_content"`
|
||||||
Platforms []string `json:"platforms"`
|
|
||||||
CoverAssetURL *string `json:"cover_asset_url"`
|
CoverAssetURL *string `json:"cover_asset_url"`
|
||||||
ReferencedImageAssetIDs []int64 `json:"referenced_image_asset_ids"`
|
ReferencedImageAssetIDs []int64 `json:"referenced_image_asset_ids"`
|
||||||
CoverImageAssetID NullableInt64Input `json:"cover_image_asset_id"`
|
CoverImageAssetID NullableInt64Input `json:"cover_image_asset_id"`
|
||||||
@@ -577,7 +584,6 @@ func (s *ArticleService) Update(ctx context.Context, id int64, req UpdateArticle
|
|||||||
nextWizardStateJSON, err := mergeArticleWizardState(
|
nextWizardStateJSON, err := mergeArticleWizardState(
|
||||||
wizardStateJSON,
|
wizardStateJSON,
|
||||||
title,
|
title,
|
||||||
req.Platforms,
|
|
||||||
req.CoverAssetURL,
|
req.CoverAssetURL,
|
||||||
resolvedCoverImageAssetInput,
|
resolvedCoverImageAssetInput,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,16 +31,16 @@ type InstantTaskListParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InstantTaskResponse struct {
|
type InstantTaskResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
ArticleID *int64 `json:"article_id"`
|
ArticleID *int64 `json:"article_id"`
|
||||||
PromptRuleID *int64 `json:"prompt_rule_id"`
|
PromptRuleID *int64 `json:"prompt_rule_id"`
|
||||||
PromptRuleName *string `json:"prompt_rule_name"`
|
PromptRuleName *string `json:"prompt_rule_name"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
ExecutionTime *string `json:"execution_time"`
|
ExecutionTime *string `json:"execution_time"`
|
||||||
Platforms []string `json:"platforms"`
|
AutoPublishPlatforms []string `json:"auto_publish_platforms"`
|
||||||
GenerateCount int `json:"generate_count"`
|
GenerateCount int `json:"generate_count"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstantTaskListResponse struct {
|
type InstantTaskListResponse struct {
|
||||||
@@ -123,7 +123,11 @@ func (s *InstantTaskService) List(ctx context.Context, params InstantTaskListPar
|
|||||||
|
|
||||||
item.ExecutionTime = stringPtrFromDBValue(executionAt)
|
item.ExecutionTime = stringPtrFromDBValue(executionAt)
|
||||||
item.CreatedAt = fmt.Sprintf("%v", createdAt)
|
item.CreatedAt = fmt.Sprintf("%v", createdAt)
|
||||||
item.Platforms = resolveArticlePlatforms(nil, inputParamsJSON)
|
autoPublishPlatforms, err := loadScheduleAutoPublishPlatforms(ctx, s.pool, actor.TenantID, inputParamsJSON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, response.ErrInternal(50010, "query_failed", "failed to resolve instant task auto publish platforms")
|
||||||
|
}
|
||||||
|
item.AutoPublishPlatforms = autoPublishPlatforms
|
||||||
item.GenerateCount = 1
|
item.GenerateCount = 1
|
||||||
|
|
||||||
if len(inputParamsJSON) > 0 {
|
if len(inputParamsJSON) > 0 {
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ func (s *PromptRuleGenerationService) enqueuePromptRuleGeneration(ctx context.Co
|
|||||||
coverImageAssetID.Set = true
|
coverImageAssetID.Set = true
|
||||||
coverImageAssetID.Value = &rawCoverImageAssetID
|
coverImageAssetID.Value = &rawCoverImageAssetID
|
||||||
}
|
}
|
||||||
wizardStateJSON, _ := mergeArticleWizardState(nil, promptRuleGeneratingTitlePlaceholder, nil, coverAssetURL, coverImageAssetID)
|
wizardStateJSON, _ := mergeArticleWizardState(nil, promptRuleGeneratingTitlePlaceholder, coverAssetURL, coverImageAssetID)
|
||||||
|
|
||||||
tx, err := s.pool.Begin(ctx)
|
tx, err := s.pool.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func normalizePlatformIDs(platformIDs []string) []string {
|
func normalizePlatformIDs(platformIDs []string) []string {
|
||||||
@@ -31,52 +34,79 @@ func normalizePlatformIDs(platformIDs []string) []string {
|
|||||||
return normalized
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePlatformIDs(value string) []string {
|
type scheduleAutoPublishPlatformQuerier interface {
|
||||||
if strings.TrimSpace(value) == "" {
|
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return normalizePlatformIDs(strings.Split(value, ","))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func serializePlatformIDs(platformIDs []string) string {
|
func loadScheduleAutoPublishPlatforms(
|
||||||
normalized := normalizePlatformIDs(platformIDs)
|
ctx context.Context,
|
||||||
if len(normalized) == 0 {
|
db scheduleAutoPublishPlatformQuerier,
|
||||||
return ""
|
tenantID int64,
|
||||||
}
|
inputParamsJSON []byte,
|
||||||
return strings.Join(normalized, ",")
|
) ([]string, error) {
|
||||||
}
|
if db == nil || tenantID <= 0 || len(inputParamsJSON) == 0 {
|
||||||
|
return []string{}, nil
|
||||||
func resolveArticlePlatforms(wizardStateJSON, inputParamsJSON []byte) []string {
|
|
||||||
if platforms := extractPlatformsFromJSONPayload(wizardStateJSON); len(platforms) > 0 {
|
|
||||||
return platforms
|
|
||||||
}
|
|
||||||
if platforms := extractPlatformsFromJSONPayload(inputParamsJSON); len(platforms) > 0 {
|
|
||||||
return platforms
|
|
||||||
}
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractPlatformsFromJSONPayload(raw []byte) []string {
|
|
||||||
if len(raw) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload map[string]interface{}
|
var payload map[string]interface{}
|
||||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
if err := json.Unmarshal(inputParamsJSON, &payload); err != nil {
|
||||||
return nil
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
if !extractBool(payload, "schedule_auto_publish") {
|
||||||
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if platforms := normalizePlatformsFromValue(payload["platforms"]); len(platforms) > 0 {
|
accountIDs := normalizeSchedulePublishAccountIDs(extractStringList(payload["schedule_publish_account_ids"], 64))
|
||||||
return platforms
|
if len(accountIDs) == 0 {
|
||||||
}
|
return []string{}, nil
|
||||||
if platforms := normalizePlatformsFromValue(payload["target_platforms"]); len(platforms) > 0 {
|
|
||||||
return platforms
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetPlatform, ok := payload["target_platform"].(string); ok {
|
return loadPublishAccountPlatformsByIDs(ctx, db, tenantID, accountIDs)
|
||||||
return parsePlatformIDs(targetPlatform)
|
}
|
||||||
|
|
||||||
|
func loadPublishAccountPlatformsByIDs(
|
||||||
|
ctx context.Context,
|
||||||
|
db scheduleAutoPublishPlatformQuerier,
|
||||||
|
tenantID int64,
|
||||||
|
accountIDs []string,
|
||||||
|
) ([]string, error) {
|
||||||
|
accountIDs = normalizeSchedulePublishAccountIDs(accountIDs)
|
||||||
|
if db == nil || tenantID <= 0 || len(accountIDs) == 0 {
|
||||||
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
rows, err := db.Query(ctx, `
|
||||||
|
SELECT DISTINCT platform_id
|
||||||
|
FROM platform_accounts
|
||||||
|
WHERE tenant_id = $1
|
||||||
|
AND desktop_id::text = ANY($2::text[])
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
ORDER BY platform_id
|
||||||
|
`, tenantID, accountIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
platformIDs := make([]string, 0, len(accountIDs))
|
||||||
|
for rows.Next() {
|
||||||
|
var platformID string
|
||||||
|
if err := rows.Scan(&platformID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
platformIDs = append(platformIDs, platformID)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return emptyStringSliceIfNil(normalizePlatformIDs(platformIDs)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func emptyStringSliceIfNil(values []string) []string {
|
||||||
|
if values == nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveArticleCoverAssetURL(wizardStateJSON []byte) *string {
|
func resolveArticleCoverAssetURL(wizardStateJSON []byte) *string {
|
||||||
@@ -146,51 +176,21 @@ func extractInt64FromJSONPayload(raw []byte, key string) (int64, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizePlatformsFromValue(raw interface{}) []string {
|
func mergeArticleWizardState(raw []byte, title string, coverAssetURL *string, coverImageAssetID NullableInt64Input) ([]byte, error) {
|
||||||
switch typed := raw.(type) {
|
|
||||||
case nil:
|
|
||||||
return nil
|
|
||||||
case string:
|
|
||||||
return parsePlatformIDs(typed)
|
|
||||||
case []string:
|
|
||||||
return normalizePlatformIDs(typed)
|
|
||||||
case []interface{}:
|
|
||||||
values := make([]string, 0, len(typed))
|
|
||||||
for _, item := range typed {
|
|
||||||
value, ok := item.(string)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
values = append(values, value)
|
|
||||||
}
|
|
||||||
return normalizePlatformIDs(values)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeArticleWizardState(raw []byte, title string, platforms []string, coverAssetURL *string, coverImageAssetID NullableInt64Input) ([]byte, error) {
|
|
||||||
state := make(map[string]interface{})
|
state := make(map[string]interface{})
|
||||||
if len(raw) > 0 {
|
if len(raw) > 0 {
|
||||||
_ = json.Unmarshal(raw, &state)
|
_ = json.Unmarshal(raw, &state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete(state, "platforms")
|
||||||
|
delete(state, "target_platforms")
|
||||||
|
delete(state, "target_platform")
|
||||||
|
|
||||||
trimmedTitle := strings.TrimSpace(title)
|
trimmedTitle := strings.TrimSpace(title)
|
||||||
if trimmedTitle != "" {
|
if trimmedTitle != "" {
|
||||||
state["title"] = trimmedTitle
|
state["title"] = trimmedTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
if platforms != nil {
|
|
||||||
normalized := normalizePlatformIDs(platforms)
|
|
||||||
if len(normalized) == 0 {
|
|
||||||
delete(state, "platforms")
|
|
||||||
delete(state, "target_platform")
|
|
||||||
} else {
|
|
||||||
state["platforms"] = normalized
|
|
||||||
state["target_platform"] = strings.Join(normalized, ",")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if coverAssetURL != nil {
|
if coverAssetURL != nil {
|
||||||
trimmedCoverURL := strings.TrimSpace(*coverAssetURL)
|
trimmedCoverURL := strings.TrimSpace(*coverAssetURL)
|
||||||
if trimmedCoverURL == "" {
|
if trimmedCoverURL == "" {
|
||||||
|
|||||||
@@ -54,26 +54,27 @@ type ScheduleTaskRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ScheduleTaskResponse struct {
|
type ScheduleTaskResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
WorkspaceID *int64 `json:"workspace_id"`
|
WorkspaceID *int64 `json:"workspace_id"`
|
||||||
PromptRuleID int64 `json:"prompt_rule_id"`
|
PromptRuleID int64 `json:"prompt_rule_id"`
|
||||||
PromptRuleName *string `json:"prompt_rule_name"`
|
PromptRuleName *string `json:"prompt_rule_name"`
|
||||||
BrandID *int64 `json:"brand_id"`
|
BrandID *int64 `json:"brand_id"`
|
||||||
BrandName *string `json:"brand_name"`
|
BrandName *string `json:"brand_name"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CronExpr string `json:"cron_expr"`
|
CronExpr string `json:"cron_expr"`
|
||||||
AutoPublish bool `json:"auto_publish"`
|
AutoPublish bool `json:"auto_publish"`
|
||||||
PublishAccountIDs []string `json:"publish_account_ids"`
|
PublishAccountIDs []string `json:"publish_account_ids"`
|
||||||
CoverAssetURL *string `json:"cover_asset_url"`
|
AutoPublishPlatforms []string `json:"auto_publish_platforms"`
|
||||||
CoverImageAssetID *int64 `json:"cover_image_asset_id"`
|
CoverAssetURL *string `json:"cover_asset_url"`
|
||||||
EnableWebSearch bool `json:"enable_web_search"`
|
CoverImageAssetID *int64 `json:"cover_image_asset_id"`
|
||||||
GenerateCount int `json:"generate_count"`
|
EnableWebSearch bool `json:"enable_web_search"`
|
||||||
StartAt *string `json:"start_at"`
|
GenerateCount int `json:"generate_count"`
|
||||||
EndAt *string `json:"end_at"`
|
StartAt *string `json:"start_at"`
|
||||||
NextRunAt *string `json:"next_run_at"`
|
EndAt *string `json:"end_at"`
|
||||||
Status string `json:"status"`
|
NextRunAt *string `json:"next_run_at"`
|
||||||
CreatedAt string `json:"created_at"`
|
Status string `json:"status"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScheduleTaskListParams struct {
|
type ScheduleTaskListParams struct {
|
||||||
@@ -206,7 +207,7 @@ func (s *ScheduleTaskService) Create(ctx context.Context, req ScheduleTaskReques
|
|||||||
return &ScheduleTaskResponse{
|
return &ScheduleTaskResponse{
|
||||||
ID: id, WorkspaceID: &workspaceID, PromptRuleID: req.PromptRuleID, BrandID: req.BrandID,
|
ID: id, WorkspaceID: &workspaceID, PromptRuleID: req.PromptRuleID, BrandID: req.BrandID,
|
||||||
Name: req.Name, CronExpr: req.CronExpr, AutoPublish: publishConfig.AutoPublish,
|
Name: req.Name, CronExpr: req.CronExpr, AutoPublish: publishConfig.AutoPublish,
|
||||||
PublishAccountIDs: publishConfig.AccountIDs, CoverAssetURL: publishConfig.CoverAssetURL,
|
PublishAccountIDs: publishConfig.AccountIDs, AutoPublishPlatforms: publishConfig.PlatformIDs, CoverAssetURL: publishConfig.CoverAssetURL,
|
||||||
CoverImageAssetID: publishConfig.CoverImageAssetID, EnableWebSearch: enableWebSearch,
|
CoverImageAssetID: publishConfig.CoverImageAssetID, EnableWebSearch: enableWebSearch,
|
||||||
GenerateCount: generateCount,
|
GenerateCount: generateCount,
|
||||||
Status: status, CreatedAt: fmt.Sprintf("%v", createdAt),
|
Status: status, CreatedAt: fmt.Sprintf("%v", createdAt),
|
||||||
@@ -400,6 +401,9 @@ func (s *ScheduleTaskService) loadScheduleTasks(ctx context.Context, tenantID in
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := s.populateScheduleAutoPublishPlatforms(ctx, tenantID, &item); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
return &ScheduleTaskListResponse{Items: items, Total: total}, nil
|
return &ScheduleTaskListResponse{Items: items, Total: total}, nil
|
||||||
@@ -425,9 +429,24 @@ func (s *ScheduleTaskService) loadScheduleTaskDetail(ctx context.Context, tenant
|
|||||||
}
|
}
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
if err := s.populateScheduleAutoPublishPlatforms(ctx, tenantID, &item); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
return &item, true, nil
|
return &item, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ScheduleTaskService) populateScheduleAutoPublishPlatforms(ctx context.Context, tenantID int64, item *ScheduleTaskResponse) error {
|
||||||
|
if item == nil || !item.AutoPublish {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
platformIDs, err := loadPublishAccountPlatformsByIDs(ctx, s.pool, tenantID, item.PublishAccountIDs)
|
||||||
|
if err != nil {
|
||||||
|
return response.ErrInternal(50010, "query_failed", "failed to resolve schedule auto publish platforms")
|
||||||
|
}
|
||||||
|
item.AutoPublishPlatforms = platformIDs
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func scanScheduleTaskResponse(scanner interface {
|
func scanScheduleTaskResponse(scanner interface {
|
||||||
Scan(dest ...interface{}) error
|
Scan(dest ...interface{}) error
|
||||||
}) (ScheduleTaskResponse, error) {
|
}) (ScheduleTaskResponse, error) {
|
||||||
@@ -480,6 +499,7 @@ func (s *ScheduleTaskService) ensurePromptRuleExists(ctx context.Context, tenant
|
|||||||
type scheduleAutoPublishConfig struct {
|
type scheduleAutoPublishConfig struct {
|
||||||
AutoPublish bool
|
AutoPublish bool
|
||||||
AccountIDs []string
|
AccountIDs []string
|
||||||
|
PlatformIDs []string
|
||||||
AccountIDsJSON []byte
|
AccountIDsJSON []byte
|
||||||
CoverAssetURL *string
|
CoverAssetURL *string
|
||||||
CoverImageAssetID *int64
|
CoverImageAssetID *int64
|
||||||
@@ -514,6 +534,7 @@ func (s *ScheduleTaskService) normalizeAutoPublishConfig(ctx context.Context, ac
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return config, err
|
return config, err
|
||||||
}
|
}
|
||||||
|
config.PlatformIDs = normalizePlatformIDs(platformIDs)
|
||||||
if schedulePublishPlatformsRequireCover(platformIDs) && coverURL == "" {
|
if schedulePublishPlatformsRequireCover(platformIDs) && coverURL == "" {
|
||||||
return config, response.ErrBadRequest(40025, "cover_required", "selected publish accounts require cover_asset_url")
|
return config, response.ErrBadRequest(40025, "cover_required", "selected publish accounts require cover_asset_url")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
-- Legacy article target platform JSON keys are intentionally not restored.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
UPDATE articles
|
||||||
|
SET wizard_state_json = wizard_state_json - 'platforms' - 'target_platforms' - 'target_platform',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE wizard_state_json IS NOT NULL
|
||||||
|
AND wizard_state_json ?| ARRAY['platforms', 'target_platforms', 'target_platform'];
|
||||||
|
|
||||||
|
UPDATE generation_tasks
|
||||||
|
SET input_params_json = input_params_json - 'platforms' - 'target_platforms' - 'target_platform',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE input_params_json IS NOT NULL
|
||||||
|
AND input_params_json ?| ARRAY['platforms', 'target_platforms', 'target_platform'];
|
||||||
Reference in New Issue
Block a user