feat: refresh content workflow and verification settings
All checks were successful
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Successful in 43s
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Successful in 25m9s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Successful in 51s

This commit is contained in:
2026-04-01 18:47:17 +08:00
parent f2c07df320
commit 7de4ddc3ee
66 changed files with 1455 additions and 2759 deletions

View File

@@ -179,7 +179,7 @@ export function MarkdownWorkbench({
<span className="h-3 w-3 rounded-full bg-[#ffbd2e]" />
<span className="h-3 w-3 rounded-full bg-[#27c93f]" />
</div>
<p className="font-mono text-xs text-slate-400">{path}</p>
<p className="font-mono text-xs text-slate-400">Markdown </p>
</div>
<div className="flex flex-wrap items-center gap-2">
@@ -258,9 +258,7 @@ export function MarkdownWorkbench({
<span>
{originalLabel} / {modifiedLabel}
</span>
) : (
<span>{path}</span>
)}
) : null}
</div>
{panel === 'edit' ? (

View File

@@ -301,7 +301,9 @@ export interface AdminSiteSettingsResponse {
music_playlist: MusicTrack[]
ai_enabled: boolean
paragraph_comments_enabled: boolean
comment_verification_mode: HumanVerificationMode
comment_turnstile_enabled: boolean
subscription_verification_mode: HumanVerificationMode
subscription_turnstile_enabled: boolean
web_push_enabled: boolean
turnstile_site_key: string | null
@@ -375,7 +377,9 @@ export interface SiteSettingsPayload {
musicPlaylist?: MusicTrack[]
aiEnabled?: boolean
paragraphCommentsEnabled?: boolean
commentVerificationMode?: HumanVerificationMode | null
commentTurnstileEnabled?: boolean
subscriptionVerificationMode?: HumanVerificationMode | null
subscriptionTurnstileEnabled?: boolean
webPushEnabled?: boolean
turnstileSiteKey?: string | null
@@ -416,6 +420,8 @@ export interface SiteSettingsPayload {
searchSynonyms?: string[]
}
export type HumanVerificationMode = 'off' | 'captcha' | 'turnstile' | string
export interface CategoryRecord {
id: number
name: string

View File

@@ -139,7 +139,7 @@ export function PostComparePage({ slugOverride }: { slugOverride?: string }) {
<GitCompareArrows className="h-4 w-4" />
vs 稿
</CardTitle>
<CardDescription>{state.path}</CardDescription>
<CardDescription>稿</CardDescription>
</CardHeader>
</Card>

View File

@@ -177,7 +177,7 @@ export function PostPolishPage() {
<Card>
<CardHeader>
<CardTitle> vs </CardTitle>
<CardDescription>{snapshot.path}</CardDescription>
<CardDescription> AI </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap items-center gap-3">

View File

@@ -237,6 +237,11 @@ function formatWorkbenchStateLabel(
.join(' / ')}`
}
function buildVirtualPostPath(slug: string) {
const normalizedSlug = slug.trim() || 'new-post'
return `article://posts/${normalizedSlug}`
}
function parseImageList(value: string) {
return value
.split('\n')
@@ -1145,9 +1150,7 @@ export function PostsPage() {
setMetadataDialog({
target: 'create',
title: createForm.title.trim() || createForm.slug.trim() || '新建草稿',
path: createForm.slug.trim()
? `backend/content/posts/${createForm.slug.trim()}.md`
: 'backend/content/posts/new-post.md',
path: buildVirtualPostPath(createForm.slug),
proposal: nextProposal,
})
})
@@ -2130,8 +2133,7 @@ export function PostsPage() {
<Badge variant="outline">{editor.markdown.split(/\r?\n/).length} </Badge>
</div>
<div className="rounded-2xl border border-border/70 bg-background/70 p-4">
<p className="break-all font-mono text-xs text-muted-foreground">{editor.path}</p>
<p className="mt-2 text-sm text-muted-foreground">
<p className="text-sm text-muted-foreground">
{formatDateTime(editor.createdAt)} · {formatDateTime(editor.updatedAt)}
</p>
</div>
@@ -2945,11 +2947,7 @@ export function PostsPage() {
value={createForm.markdown}
originalValue={buildCreateMarkdownForWindow(defaultCreateForm)}
diffValue={buildCreateMarkdownForWindow(createForm)}
path={
createForm.slug.trim()
? `backend/content/posts/${createForm.slug.trim()}.md`
: 'backend/content/posts/new-post.md'
}
path={buildVirtualPostPath(createForm.slug)}
workspaceHeightClassName="h-[clamp(620px,74dvh,920px)]"
mode={createMode}
visiblePanels={createPanels}
@@ -3047,9 +3045,6 @@ export function PostsPage() {
<p className="mt-3 text-base font-semibold">
{metadataDialog.title}
</p>
<p className="mt-2 break-all font-mono text-xs text-muted-foreground">
{metadataDialog.path}
</p>
</div>
<div className="grid gap-3 sm:grid-cols-3 xl:grid-cols-1">

View File

@@ -15,6 +15,7 @@ import { adminApi, ApiError } from '@/lib/api'
import type {
AdminSiteSettingsResponse,
AiProviderConfig,
HumanVerificationMode,
MusicTrack,
SiteSettingsPayload,
} from '@/lib/types'
@@ -70,6 +71,30 @@ const NOTIFICATION_CHANNEL_OPTIONS = [
{ value: 'ntfy', label: 'ntfy' },
] as const
const HUMAN_VERIFICATION_MODE_OPTIONS = [
{ value: 'off', label: '关闭' },
{ value: 'captcha', label: '普通验证码' },
{ value: 'turnstile', label: 'Turnstile' },
] as const
function normalizeHumanVerificationMode(
value: string | null | undefined,
fallback: HumanVerificationMode,
): HumanVerificationMode {
switch ((value ?? '').trim().toLowerCase()) {
case 'off':
return 'off'
case 'captcha':
case 'normal':
case 'simple':
return 'captcha'
case 'turnstile':
return 'turnstile'
default:
return fallback
}
}
function isCloudflareProvider(provider: string | null | undefined) {
const normalized = provider?.trim().toLowerCase()
return normalized === 'cloudflare' || normalized === 'cloudflare-workers-ai' || normalized === 'workers-ai'
@@ -94,6 +119,14 @@ function normalizeSettingsResponse(
...input,
ai_providers: aiProviders,
search_synonyms: searchSynonyms,
comment_verification_mode: normalizeHumanVerificationMode(
input.comment_verification_mode,
input.comment_turnstile_enabled ? 'turnstile' : 'captcha',
),
subscription_verification_mode: normalizeHumanVerificationMode(
input.subscription_verification_mode,
input.subscription_turnstile_enabled ? 'turnstile' : 'off',
),
turnstile_site_key: input.turnstile_site_key ?? null,
turnstile_secret_key: input.turnstile_secret_key ?? null,
web_push_vapid_public_key: input.web_push_vapid_public_key ?? null,
@@ -123,6 +156,9 @@ function Field({
}
function toPayload(form: AdminSiteSettingsResponse): SiteSettingsPayload {
const commentTurnstileEnabled = form.comment_verification_mode === 'turnstile'
const subscriptionTurnstileEnabled = form.subscription_verification_mode === 'turnstile'
return {
siteName: form.site_name,
siteShortName: form.site_short_name,
@@ -143,8 +179,10 @@ function toPayload(form: AdminSiteSettingsResponse): SiteSettingsPayload {
musicPlaylist: form.music_playlist,
aiEnabled: form.ai_enabled,
paragraphCommentsEnabled: form.paragraph_comments_enabled,
commentTurnstileEnabled: form.comment_turnstile_enabled,
subscriptionTurnstileEnabled: form.subscription_turnstile_enabled,
commentVerificationMode: form.comment_verification_mode,
commentTurnstileEnabled,
subscriptionVerificationMode: form.subscription_verification_mode,
subscriptionTurnstileEnabled,
webPushEnabled: form.web_push_enabled,
turnstileSiteKey: form.turnstile_site_key,
turnstileSecretKey: form.turnstile_secret_key,
@@ -659,22 +697,28 @@ export function SiteSettingsPage() {
</div>
</label>
<label className="flex items-start gap-3 rounded-2xl border border-border/70 bg-background/60 p-4">
<input
type="checkbox"
checked={form.subscription_turnstile_enabled}
onChange={(event) =>
updateField('subscription_turnstile_enabled', event.target.checked)
}
className="mt-1 h-4 w-4 rounded border-input text-primary focus:ring-ring"
/>
<div>
<div className="font-medium"> Turnstile</div>
<p className="mt-1 text-sm leading-6 text-muted-foreground">
Cloudflare Turnstile key
</p>
</div>
</label>
<div className="rounded-2xl border border-border/70 bg-background/60 p-4">
<Field
label="订阅提交验证方式"
hint="可选 关闭 / 普通验证码 / Turnstile若 Turnstile key 未配置完整,会自动回退到普通验证码。"
>
<Select
value={form.subscription_verification_mode}
onChange={(event) =>
updateField(
'subscription_verification_mode',
normalizeHumanVerificationMode(event.target.value, 'off'),
)
}
>
{HUMAN_VERIFICATION_MODE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</Field>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-2">
@@ -926,22 +970,28 @@ export function SiteSettingsPage() {
</div>
</label>
<label className="flex items-start gap-3 rounded-2xl border border-border/70 bg-background/60 p-4">
<input
type="checkbox"
checked={form.comment_turnstile_enabled}
onChange={(event) =>
updateField('comment_turnstile_enabled', event.target.checked)
}
className="mt-1 h-4 w-4 rounded border-input text-primary focus:ring-ring"
/>
<div>
<div className="font-medium"> Turnstile</div>
<p className="mt-1 text-sm leading-6 text-muted-foreground">
使 Cloudflare Turnstile key / secret退
</p>
</div>
</label>
<div className="rounded-2xl border border-border/70 bg-background/60 p-4">
<Field
label="评论区验证方式"
hint="文章评论和段落评论都走这里;若选择 Turnstile 但 key / secret 不完整,会自动回退到普通验证码。"
>
<Select
value={form.comment_verification_mode}
onChange={(event) =>
updateField(
'comment_verification_mode',
normalizeHumanVerificationMode(event.target.value, 'captcha'),
)
}
>
{HUMAN_VERIFICATION_MODE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</Field>
</div>
</CardContent>
</Card>