feat(migrations): Harden task audit tracking and optimize article templates

- Added migration to harden task audit tracking by modifying audit_logs and related tables.
- Introduced operator_id to several tables for better tracking of actions.
- Updated article_templates with new prompt templates for various article types, enhancing content generation.
- Created prompt_rules and schedule_tasks tables to manage content generation rules and scheduling.
- Added foreign key constraints to articles for better data integrity.
This commit is contained in:
2026-04-02 00:31:28 +08:00
parent de30497f59
commit b31d8d0096
101 changed files with 16671 additions and 721 deletions
+148 -4
View File
@@ -9,24 +9,48 @@ import type {
BrandRequest,
Competitor,
CompetitorRequest,
GenerateFromRuleRequest,
GenerateFromRuleResponse,
JsonValue,
Keyword,
KeywordRequest,
LoginRequest,
LoginResponse,
PromptRule,
PromptRuleGroup,
PromptRuleGroupRequest,
PromptRuleListParams,
PromptRuleListResponse,
PromptRuleRequest,
PromptRuleSimple,
PromptRuleStatusRequest,
QuotaSummary,
Question,
QuestionRequest,
QuestionVersion,
RecentArticle,
RefreshResponse,
ScheduleTask,
ScheduleTaskListParams,
ScheduleTaskListResponse,
ScheduleTaskRequest,
ScheduleTaskStatusRequest,
TemplateDetail,
TemplateAnalyzeTaskRequest,
TemplateAnalyzeTaskResultResponse,
TemplateAssistTaskCreateResponse,
TemplateOutlineTaskRequest,
TemplateOutlineTaskResultResponse,
TemplateSaveDraftRequest,
TemplateSaveDraftResponse,
TemplateTitleTaskRequest,
TemplateTitleTaskResultResponse,
TemplateGenerateRequest,
TemplateGenerateResponse,
TemplateListItem,
TemplateSchema,
TemplateCard,
UpdateQuestionRequest,
UpdateArticleRequest,
UserInfo,
WorkspaceOverview,
} from "@geo/shared-types";
@@ -98,8 +122,47 @@ export const templatesApi = {
detail(id: number) {
return apiClient.get<TemplateDetail>(`/api/tenant/templates/${id}`);
},
schema(id: number) {
return apiClient.get<TemplateSchema>(`/api/tenant/templates/${id}/schema`);
createAnalyzeTask(id: number, payload: TemplateAnalyzeTaskRequest) {
return apiClient.post<TemplateAssistTaskCreateResponse, TemplateAnalyzeTaskRequest>(
`/api/tenant/templates/${id}/analyze-tasks`,
payload,
);
},
getAnalyzeTaskResult(id: number, taskId: string) {
return apiClient.get<TemplateAnalyzeTaskResultResponse>(
`/api/tenant/templates/${id}/analyze_task_result`,
{ params: { task_id: taskId } },
);
},
createTitleTask(id: number, payload: TemplateTitleTaskRequest) {
return apiClient.post<TemplateAssistTaskCreateResponse, TemplateTitleTaskRequest>(
`/api/tenant/templates/${id}/title-tasks`,
payload,
);
},
getTitleTaskResult(id: number, taskId: string) {
return apiClient.get<TemplateTitleTaskResultResponse>(
`/api/tenant/templates/${id}/gen_title_task_result`,
{ params: { task_id: taskId } },
);
},
createOutlineTask(id: number, payload: TemplateOutlineTaskRequest) {
return apiClient.post<TemplateAssistTaskCreateResponse, TemplateOutlineTaskRequest>(
`/api/tenant/templates/${id}/outline-tasks`,
payload,
);
},
getOutlineTaskResult(id: number, taskId: string) {
return apiClient.get<TemplateOutlineTaskResultResponse>(
`/api/tenant/templates/${id}/gen_outline_task_result`,
{ params: { task_id: taskId } },
);
},
createAssistTask(id: number, payload: TemplateAnalyzeTaskRequest) {
return this.createAnalyzeTask(id, payload);
},
getAssistTask(id: number, taskId: string) {
return this.getAnalyzeTaskResult(id, taskId);
},
generate(id: number, payload: TemplateGenerateRequest) {
return apiClient.post<TemplateGenerateResponse, TemplateGenerateRequest>(
@@ -107,6 +170,12 @@ export const templatesApi = {
payload,
);
},
saveDraft(id: number, payload: TemplateSaveDraftRequest) {
return apiClient.post<TemplateSaveDraftResponse, TemplateSaveDraftRequest>(
`/api/tenant/templates/${id}/drafts`,
payload,
);
},
};
export const articlesApi = {
@@ -116,6 +185,9 @@ export const articlesApi = {
detail(id: number) {
return apiClient.get<ArticleDetail>(`/api/tenant/articles/${id}`);
},
update(id: number, payload: UpdateArticleRequest) {
return apiClient.put<ArticleDetail, UpdateArticleRequest>(`/api/tenant/articles/${id}`, payload);
},
versions(id: number) {
return apiClient.get<ArticleVersion[]>(`/api/tenant/articles/${id}/versions`);
},
@@ -270,10 +342,82 @@ export const brandsApi = {
},
};
export const promptRulesApi = {
list(params?: PromptRuleListParams) {
return apiClient.get<PromptRuleListResponse>("/api/tenant/prompt-rules", { params });
},
listSimple() {
return apiClient.get<PromptRuleSimple[]>("/api/tenant/prompt-rules/simple");
},
detail(id: number) {
return apiClient.get<PromptRule>(`/api/tenant/prompt-rules/${id}`);
},
create(payload: PromptRuleRequest) {
return apiClient.post<PromptRule, PromptRuleRequest>("/api/tenant/prompt-rules", payload);
},
update(id: number, payload: PromptRuleRequest) {
return apiClient.put<null, PromptRuleRequest>(`/api/tenant/prompt-rules/${id}`, payload);
},
remove(id: number) {
return apiClient.remove<null>(`/api/tenant/prompt-rules/${id}`);
},
toggleStatus(id: number, payload: PromptRuleStatusRequest) {
return apiClient.put<null, PromptRuleStatusRequest>(`/api/tenant/prompt-rules/${id}/status`, payload);
},
listGroups() {
return apiClient.get<PromptRuleGroup[]>("/api/tenant/prompt-rules/groups");
},
createGroup(payload: PromptRuleGroupRequest) {
return apiClient.post<PromptRuleGroup, PromptRuleGroupRequest>("/api/tenant/prompt-rules/groups", payload);
},
updateGroup(id: number, payload: PromptRuleGroupRequest) {
return apiClient.put<null, PromptRuleGroupRequest>(`/api/tenant/prompt-rules/groups/${id}`, payload);
},
removeGroup(id: number) {
return apiClient.remove<null>(`/api/tenant/prompt-rules/groups/${id}`);
},
};
export const schedulesApi = {
list(params?: ScheduleTaskListParams) {
return apiClient.get<ScheduleTaskListResponse>("/api/tenant/schedules", { params });
},
detail(id: number) {
return apiClient.get<ScheduleTask>(`/api/tenant/schedules/${id}`);
},
create(payload: ScheduleTaskRequest) {
return apiClient.post<ScheduleTask, ScheduleTaskRequest>("/api/tenant/schedules", payload);
},
update(id: number, payload: ScheduleTaskRequest) {
return apiClient.put<null, ScheduleTaskRequest>(`/api/tenant/schedules/${id}`, payload);
},
remove(id: number) {
return apiClient.remove<null>(`/api/tenant/schedules/${id}`);
},
toggleStatus(id: number, payload: ScheduleTaskStatusRequest) {
return apiClient.put<null, ScheduleTaskStatusRequest>(`/api/tenant/schedules/${id}/status`, payload);
},
};
export const generateApi = {
fromRule(payload: GenerateFromRuleRequest) {
return apiClient.post<GenerateFromRuleResponse, GenerateFromRuleRequest>(
"/api/tenant/articles/generate-from-rule",
payload,
);
},
};
export function normalizeInputParams(
payload: Record<string, JsonValue>,
articleId?: number,
wizardState?: Record<string, JsonValue>,
): TemplateGenerateRequest {
return { input_params: payload };
return {
article_id: articleId,
input_params: payload,
wizard_state: wizardState,
};
}
function consumeSSEBuffer(
+15
View File
@@ -13,6 +13,21 @@ const errorMessageMap: Record<string, string> = {
query_failed: "数据读取失败",
quota_insufficient: "生成额度不足",
llm_unavailable: "生成服务不可用",
analyze_context_required: "请先填写品牌或模板上下文信息",
title_context_required: "请先确认品牌、关键词或竞品信息",
outline_context_required: "请先确认标题和文章结构后再生成大纲",
analyze_queue_unavailable: "品牌分析任务较多,请稍后重试",
title_queue_unavailable: "标题生成任务较多,请稍后重试",
outline_queue_unavailable: "大纲生成任务较多,请稍后重试",
analyze_task_not_found: "品牌分析任务不存在或已失效",
title_task_not_found: "标题生成任务不存在或已失效",
outline_task_not_found: "文章大纲任务不存在或已失效",
article_generating: "文章仍在生成中",
article_not_editable: "只有已完成文章才可编辑",
draft_not_editable: "只有草稿或生成失败的文章才可继续在向导中编辑",
article_title_required: "请输入文章标题",
invalid_payload: "请求参数不合法",
update_failed: "保存文章失败",
network_error: "网络连接失败,请稍后重试",
unknown_error: "发生未知错误",
};
@@ -0,0 +1,72 @@
export interface PublishPlatformOption {
id: string;
name: string;
shortName: string;
accent: string;
accountLabel?: string;
}
const platformCatalog: Record<string, Omit<PublishPlatformOption, "accountLabel">> = {
zhihu: { id: "zhihu", name: "知乎", shortName: "知", accent: "#1677ff" },
baijiahao: { id: "baijiahao", name: "百家号", shortName: "百", accent: "#31445a" },
toutiao: { id: "toutiao", name: "头条号", shortName: "头", accent: "#f5222d" },
souhu: { id: "souhu", name: "搜狐号", shortName: "搜", accent: "#fa8c16" },
qq: { id: "qq", name: "企鹅号", shortName: "Q", accent: "#111827" },
wechat: { id: "wechat", name: "公众号", shortName: "微", accent: "#13c26b" },
bilibili: { id: "bilibili", name: "bilibili", shortName: "B", accent: "#eb2f96" },
jianshu: { id: "jianshu", name: "简书", shortName: "简", accent: "#f56a5e" },
juejin: { id: "juejin", name: "稀土掘金", shortName: "掘", accent: "#1677ff" },
smzdm: { id: "smzdm", name: "什么值得买", shortName: "值", accent: "#f5222d" },
zol: { id: "zol", name: "中关村在线", shortName: "Z", accent: "#ff4d4f" },
dongchedi: { id: "dongchedi", name: "懂车帝", shortName: "懂", accent: "#fadb14" },
};
const mockBoundPlatformIds = ["zhihu", "wechat", "juejin", "bilibili", "jianshu", "baijiahao"];
export function getBoundPublishPlatforms(): PublishPlatformOption[] {
return mockBoundPlatformIds
.map((id) => platformCatalog[id])
.filter((item): item is Omit<PublishPlatformOption, "accountLabel"> => Boolean(item));
}
export function getAllPublishPlatforms(): PublishPlatformOption[] {
return Object.values(platformCatalog);
}
export function isPlatformBound(platformId: string): boolean {
return mockBoundPlatformIds.includes(platformId);
}
export function serializeTargetPlatforms(platformIds: string[]): string | undefined {
const next = Array.from(new Set(platformIds.map((item) => item.trim()).filter(Boolean)));
return next.length ? next.join(",") : undefined;
}
export function parseTargetPlatforms(value?: string | null): string[] {
if (!value) {
return [];
}
return Array.from(
new Set(
value
.split(",")
.map((item) => item.trim())
.filter(Boolean),
),
);
}
export function getPublishPlatformLabel(platformId: string): string {
return platformCatalog[platformId]?.name ?? platformId;
}
export function formatPublishPlatformSummary(value?: string | null): string {
const labels = parseTargetPlatforms(value).map(getPublishPlatformLabel);
return labels.length ? labels.join("、") : "--";
}
export function formatPublishPlatformList(platformIds?: string[] | null): string {
const labels = (platformIds ?? []).map(getPublishPlatformLabel).filter(Boolean);
return labels.length ? labels.join("、") : "--";
}