feat(prompts): add prompts loader and configuration management
- Implemented a new prompts loader in `loader.go` to manage prompt templates and configurations. - Introduced caching mechanism for prompt configurations to optimize loading. - Added functions to apply platform-specific prompt template overrides. - Created unit tests in `loader_test.go` to validate prompt configuration loading and reloading behavior. - Ensured that the last valid configuration is retained in case of errors during reload.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ReloadOutlined } from "@ant-design/icons-vue";
|
||||
import { MinusCircleFilled, ReloadOutlined } from "@ant-design/icons-vue";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
import { message, notification } from "ant-design-vue";
|
||||
import type {
|
||||
@@ -86,7 +86,12 @@ watch(
|
||||
}
|
||||
selectionHydrated.value = false;
|
||||
coverHydrated.value = false;
|
||||
await refreshRuntime();
|
||||
await Promise.allSettled([
|
||||
refreshRuntime(),
|
||||
props.articleId ? detailQuery.refetch() : Promise.resolve(),
|
||||
accountsQuery.refetch(),
|
||||
platformsQuery.refetch(),
|
||||
]);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -96,7 +101,10 @@ watchEffect(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!coverHydrated.value && !detailQuery.isPending.value) {
|
||||
if (!coverHydrated.value) {
|
||||
if (detailQuery.isPending.value || detailQuery.isFetching.value) {
|
||||
return;
|
||||
}
|
||||
const initialUrl = resolveApiURL(detailQuery.data.value?.cover_asset_url);
|
||||
coverAssetUrl.value = initialUrl;
|
||||
coverFileName.value = deriveCoverFileName(initialUrl);
|
||||
@@ -110,8 +118,11 @@ watchEffect(() => {
|
||||
|
||||
if (
|
||||
detailQuery.isPending.value ||
|
||||
detailQuery.isFetching.value ||
|
||||
accountsQuery.isPending.value ||
|
||||
accountsQuery.isFetching.value ||
|
||||
platformsQuery.isPending.value ||
|
||||
platformsQuery.isFetching.value ||
|
||||
runtimeLoading.value
|
||||
) {
|
||||
return;
|
||||
@@ -475,35 +486,32 @@ function showPublishFailures(result: PublisherPublishResponse): void {
|
||||
{{ coverRequired ? t("media.publish.messages.coverRequired") : t("media.publish.coverHint") }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="effectiveCoverEnabled"
|
||||
class="publish-modal__cover-body"
|
||||
:class="{ 'publish-modal__cover-body--single': !coverAssetUrl }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="publish-modal__cover-preview"
|
||||
@click="coverPickerOpen = true"
|
||||
>
|
||||
<template v-if="coverAssetUrl">
|
||||
<img :src="coverAssetUrl" alt="cover preview" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="publish-modal__cover-plus">+</span>
|
||||
<span>{{ t("media.publish.coverUpload") }}</span>
|
||||
</template>
|
||||
</button>
|
||||
<div v-if="effectiveCoverEnabled" class="publish-modal__cover-body">
|
||||
<div class="publish-modal__cover-preview-wrap">
|
||||
<button
|
||||
type="button"
|
||||
class="publish-modal__cover-preview"
|
||||
@click="coverPickerOpen = true"
|
||||
>
|
||||
<template v-if="coverAssetUrl">
|
||||
<img :src="coverAssetUrl" alt="cover preview" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="publish-modal__cover-plus">+</span>
|
||||
<span>{{ t("media.publish.coverUpload") }}</span>
|
||||
</template>
|
||||
</button>
|
||||
|
||||
<div v-if="coverAssetUrl" class="publish-modal__cover-side">
|
||||
<div class="publish-modal__cover-actions">
|
||||
<a-button @click="handleRemoveCover">
|
||||
{{ t("article.editor.coverRemove") }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="publish-modal__cover-file">
|
||||
{{ coverFileName || t("article.editor.coverSaved") }}
|
||||
</div>
|
||||
<button
|
||||
v-if="coverAssetUrl"
|
||||
type="button"
|
||||
class="publish-modal__cover-remove"
|
||||
:aria-label="t('article.editor.coverRemove')"
|
||||
:title="t('article.editor.coverRemove')"
|
||||
@click.stop="handleRemoveCover"
|
||||
>
|
||||
<MinusCircleFilled />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -728,20 +736,21 @@ function showPublishFailures(result: PublisherPublishResponse): void {
|
||||
}
|
||||
|
||||
.publish-modal__cover-body {
|
||||
display: grid;
|
||||
grid-template-columns: 176px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.publish-modal__cover-body--single {
|
||||
grid-template-columns: 176px;
|
||||
.publish-modal__cover-preview-wrap {
|
||||
position: relative;
|
||||
width: 176px;
|
||||
flex: 0 0 176px;
|
||||
}
|
||||
|
||||
.publish-modal__cover-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 124px;
|
||||
padding: 0;
|
||||
border: 1px dashed #d3dceb;
|
||||
@@ -774,22 +783,29 @@ function showPublishFailures(result: PublisherPublishResponse): void {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.publish-modal__cover-side {
|
||||
.publish-modal__cover-remove {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.14);
|
||||
color: #ff4d4f;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.publish-modal__cover-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.publish-modal__cover-file {
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
.publish-modal__cover-remove:hover {
|
||||
transform: scale(1.04);
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
|
||||
color: #ff7875;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
@@ -802,7 +818,7 @@ function showPublishFailures(result: PublisherPublishResponse): void {
|
||||
}
|
||||
|
||||
.publish-modal__cover-body {
|
||||
grid-template-columns: 1fr;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -558,6 +558,7 @@ export const monitoringApi = {
|
||||
keyword_id?: number | null;
|
||||
days?: number;
|
||||
business_date?: string;
|
||||
ai_platform_id?: string;
|
||||
}) {
|
||||
return apiClient.get<MonitoringDashboardCompositeResponse>("/api/tenant/monitoring/dashboard/composite", {
|
||||
params,
|
||||
|
||||
@@ -142,6 +142,7 @@ const saveMutation = useMutation({
|
||||
queryClient.invalidateQueries({ queryKey: ["workspace"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["templates"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["articles", "detail", articleId.value] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["articles", "detail", articleId.value, "publish-modal"] }),
|
||||
]);
|
||||
},
|
||||
onError: (error) => {
|
||||
|
||||
@@ -29,6 +29,7 @@ const questionId = computed(() => parsePositiveNumber(route.params.questionId));
|
||||
const questionHash = computed(() => normalizeQueryValue(route.query.question_hash));
|
||||
const questionTitleFallback = computed(() => normalizeQueryValue(route.query.question_text));
|
||||
const keywordId = computed(() => normalizeQueryValue(route.query.keyword_id));
|
||||
const requestedPlatformId = computed(() => normalizeQueryValue(route.query.ai_platform_id));
|
||||
const businessDate = computed(() => normalizeTrackingBusinessDate(route.query.business_date) || trackingToday);
|
||||
const dateFrom = computed(() => {
|
||||
return normalizeQueryValue(route.query.date_from) || businessDate.value;
|
||||
@@ -44,6 +45,7 @@ const detailQuery = useQuery({
|
||||
brandId.value,
|
||||
questionId.value,
|
||||
questionHash.value,
|
||||
requestedPlatformId.value,
|
||||
dateFrom.value,
|
||||
dateTo.value,
|
||||
]),
|
||||
@@ -53,6 +55,7 @@ const detailQuery = useQuery({
|
||||
date_from: dateFrom.value,
|
||||
date_to: dateTo.value,
|
||||
question_hash: questionHash.value || undefined,
|
||||
ai_platform_id: requestedPlatformId.value || undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -66,9 +69,8 @@ watch(
|
||||
return;
|
||||
}
|
||||
|
||||
const requestedPlatformId = normalizeQueryValue(route.query.ai_platform_id);
|
||||
const requestedPlatform = requestedPlatformId
|
||||
? platforms.find((item) => item.ai_platform_id === requestedPlatformId)
|
||||
const requestedPlatform = requestedPlatformId.value
|
||||
? platforms.find((item) => item.ai_platform_id === requestedPlatformId.value)
|
||||
: null;
|
||||
const sampledPlatform = platforms.find((item) => item.sample_status === "sampled");
|
||||
const nextPlatform = requestedPlatform ?? sampledPlatform ?? platforms[0];
|
||||
@@ -130,6 +132,7 @@ function goBack(): void {
|
||||
query: {
|
||||
brand_id: brandId.value ? String(brandId.value) : undefined,
|
||||
keyword_id: keywordId.value || undefined,
|
||||
ai_platform_id: requestedPlatformId.value || undefined,
|
||||
business_date: businessDate.value,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -72,11 +72,26 @@ const platformAppearanceMap: Record<string, { color: string; surface: string; gl
|
||||
kimi: { color: "#111827", surface: "#f3f4f6", glyph: "K" },
|
||||
};
|
||||
|
||||
const trackingPlatformOptions = [
|
||||
{ label: "全部", value: "all" },
|
||||
{ label: "DeepSeek", value: "deepseek" },
|
||||
{ label: "千问", value: "qwen" },
|
||||
{ label: "豆包", value: "doubao" },
|
||||
] as const;
|
||||
|
||||
const trackingPlatformIds = new Set<string>(
|
||||
trackingPlatformOptions
|
||||
.map((item) => item.value)
|
||||
.filter((value) => value !== "all"),
|
||||
);
|
||||
|
||||
const selectedBrandId = useStorage<number | null>("tracking_selected_brand", null);
|
||||
const selectedKeywordId = useStorage<number | null>("tracking_selected_keyword", null);
|
||||
const selectedPlatformId = useStorage<string>("tracking_selected_ai_platform_id", "all");
|
||||
const selectedBusinessDate = useStorage<string>("tracking_selected_business_date", trackingToday);
|
||||
const pluginPreparing = ref(false);
|
||||
|
||||
selectedPlatformId.value = normalizeTrackingPlatformId(selectedPlatformId.value);
|
||||
selectedBusinessDate.value = normalizeTrackingBusinessDate(selectedBusinessDate.value) ?? trackingToday;
|
||||
|
||||
const brandsQuery = useQuery({
|
||||
@@ -151,17 +166,28 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch([selectedBrandId, selectedKeywordId, selectedBusinessDate], ([brandId, keywordId, businessDate]) => {
|
||||
watch(
|
||||
() => route.query.ai_platform_id,
|
||||
(value) => {
|
||||
selectedPlatformId.value = normalizeTrackingPlatformId(value);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch([selectedBrandId, selectedKeywordId, selectedPlatformId, selectedBusinessDate], ([brandId, keywordId, platformId, businessDate]) => {
|
||||
const nextBrandId = brandId ? String(brandId) : undefined;
|
||||
const nextKeywordId = keywordId ? String(keywordId) : undefined;
|
||||
const nextPlatformId = platformId !== "all" ? platformId : undefined;
|
||||
const nextBusinessDate = normalizeTrackingBusinessDate(businessDate) ?? trackingToday;
|
||||
const currentBrandId = normalizeQueryValue(route.query.brand_id) || undefined;
|
||||
const currentKeywordId = normalizeQueryValue(route.query.keyword_id) || undefined;
|
||||
const currentPlatformId = normalizeTrackingPlatformId(route.query.ai_platform_id);
|
||||
const currentBusinessDate = normalizeTrackingBusinessDate(route.query.business_date) ?? trackingToday;
|
||||
|
||||
if (
|
||||
currentBrandId === nextBrandId &&
|
||||
currentKeywordId === nextKeywordId &&
|
||||
currentPlatformId === (nextPlatformId ?? "all") &&
|
||||
currentBusinessDate === nextBusinessDate
|
||||
) {
|
||||
return;
|
||||
@@ -172,6 +198,7 @@ watch([selectedBrandId, selectedKeywordId, selectedBusinessDate], ([brandId, key
|
||||
query: {
|
||||
...(nextBrandId ? { brand_id: nextBrandId } : {}),
|
||||
...(nextKeywordId ? { keyword_id: nextKeywordId } : {}),
|
||||
...(nextPlatformId ? { ai_platform_id: nextPlatformId } : {}),
|
||||
business_date: nextBusinessDate,
|
||||
},
|
||||
});
|
||||
@@ -208,13 +235,21 @@ async function ensureMonitoringPluginReady(options?: { silent?: boolean }): Prom
|
||||
}
|
||||
|
||||
const dashboardQuery = useQuery({
|
||||
queryKey: computed(() => ["tracking", "dashboard", selectedBrandId.value, selectedKeywordId.value, selectedBusinessDate.value]),
|
||||
queryKey: computed(() => [
|
||||
"tracking",
|
||||
"dashboard",
|
||||
selectedBrandId.value,
|
||||
selectedKeywordId.value,
|
||||
selectedPlatformId.value,
|
||||
selectedBusinessDate.value,
|
||||
]),
|
||||
enabled: computed(() => Boolean(selectedBrandId.value)),
|
||||
queryFn: () =>
|
||||
monitoringApi.dashboardComposite({
|
||||
brand_id: selectedBrandId.value ?? undefined,
|
||||
keyword_id: selectedKeywordId.value,
|
||||
days: trackingMaxHistoryDays,
|
||||
ai_platform_id: selectedPlatformId.value !== "all" ? selectedPlatformId.value : undefined,
|
||||
business_date: selectedBusinessDate.value,
|
||||
}),
|
||||
});
|
||||
@@ -451,6 +486,7 @@ function openQuestion(question: MonitoringHotQuestion): void {
|
||||
business_date: selectedBusinessDate.value,
|
||||
date_from: selectedBusinessDate.value,
|
||||
date_to: selectedBusinessDate.value,
|
||||
...(selectedPlatformId.value !== "all" ? { ai_platform_id: selectedPlatformId.value } : {}),
|
||||
...(selectedKeywordId.value ? { keyword_id: String(selectedKeywordId.value) } : {}),
|
||||
},
|
||||
});
|
||||
@@ -554,6 +590,14 @@ function normalizeQueryValue(value: unknown): string {
|
||||
return String(Array.isArray(value) ? value[0] ?? "" : value ?? "").trim();
|
||||
}
|
||||
|
||||
function normalizeTrackingPlatformId(value: unknown): string {
|
||||
const normalized = normalizeQueryValue(value).toLowerCase();
|
||||
if (!normalized) {
|
||||
return "all";
|
||||
}
|
||||
return trackingPlatformIds.has(normalized) ? normalized : "all";
|
||||
}
|
||||
|
||||
function normalizeTrackingBusinessDate(value: unknown): string | null {
|
||||
const normalized = normalizeQueryValue(value);
|
||||
if (!normalized) {
|
||||
@@ -588,6 +632,14 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
>
|
||||
<template #actions>
|
||||
<a-space wrap align="center" class="tracking-actions-wrap">
|
||||
<div class="tracking-action-item">
|
||||
<span class="tracking-action-label">平台</span>
|
||||
<a-select
|
||||
v-model:value="selectedPlatformId"
|
||||
class="tracking-select"
|
||||
:options="trackingPlatformOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="tracking-action-item">
|
||||
<span class="tracking-action-label">品牌</span>
|
||||
<a-select
|
||||
|
||||
@@ -199,7 +199,7 @@ async function publishToutiaohao(context: AdapterContext): Promise<PlatformPubli
|
||||
trends_writing_tag: "0",
|
||||
claim_exclusive: "0",
|
||||
info_source: JSON.stringify({
|
||||
source_type: 3,
|
||||
source_type: 5,
|
||||
source_author_uid: "",
|
||||
time_format: "",
|
||||
position: {},
|
||||
|
||||
@@ -324,8 +324,8 @@ async function publishDraft(draftId: string): Promise<boolean> {
|
||||
comment_permission: "anyone",
|
||||
},
|
||||
creationStatement: {
|
||||
disclaimer_type: "ai_creation",
|
||||
disclaimer_status: "open",
|
||||
disclaimer_type: "",
|
||||
disclaimer_status: "",
|
||||
},
|
||||
contentsTables: {
|
||||
table_of_contents_enabled: false,
|
||||
|
||||
Reference in New Issue
Block a user