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
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:
@@ -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' ? (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user