feat: Refactor service management scripts to use a unified dev script

- Added package.json to manage development scripts.
- Updated restart-services.ps1 to call the new dev script for starting services.
- Refactored start-admin.ps1, start-backend.ps1, start-frontend.ps1, and start-mcp.ps1 to utilize the dev script for starting respective services.
- Enhanced stop-services.ps1 to improve process termination logic by matching command patterns.
This commit is contained in:
2026-03-29 21:36:13 +08:00
parent 84f82c2a7e
commit 92a85eef20
137 changed files with 14181 additions and 2691 deletions

View File

@@ -0,0 +1,302 @@
import { DiffEditor } from '@monaco-editor/react'
import { Bot, CheckCheck, RefreshCcw, WandSparkles } from 'lucide-react'
import { startTransition, useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import {
configureMonaco,
editorTheme,
sharedOptions,
} from '@/components/markdown-workbench'
import { MarkdownPreview } from '@/components/markdown-preview'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { adminApi, ApiError } from '@/lib/api'
import { computeDiffHunks, applySelectedDiffHunks } from '@/lib/markdown-merge'
import {
loadDraftWindowSnapshot,
savePolishWindowResult,
type DraftWindowSnapshot,
} from '@/lib/post-draft-window'
type PolishTarget = 'editor' | 'create'
function getDraftKey() {
if (typeof window === 'undefined') {
return null
}
return new URLSearchParams(window.location.search).get('draftKey')
}
function getTarget(): PolishTarget {
if (typeof window === 'undefined') {
return 'editor'
}
const value = new URLSearchParams(window.location.search).get('target')
return value === 'create' ? 'create' : 'editor'
}
function buildApplyMessage(draftKey: string, markdown: string, target: PolishTarget) {
return {
type: 'termi-admin-post-polish-apply',
draftKey,
markdown,
target,
}
}
export function PostPolishPage() {
const draftKey = getDraftKey()
const target = getTarget()
const [snapshot, setSnapshot] = useState<DraftWindowSnapshot | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [polishing, setPolishing] = useState(false)
const [polishedMarkdown, setPolishedMarkdown] = useState('')
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
useEffect(() => {
const draft = loadDraftWindowSnapshot(draftKey)
if (!draft) {
setError('没有找到要润色的草稿快照,请从文章编辑页重新打开 AI 润色窗口。')
} else {
startTransition(() => {
setSnapshot(draft)
})
}
setLoading(false)
}, [draftKey])
const originalMarkdown = snapshot?.markdown ?? ''
const hunks = useMemo(
() => (polishedMarkdown ? computeDiffHunks(originalMarkdown, polishedMarkdown) : []),
[originalMarkdown, polishedMarkdown],
)
const mergedMarkdown = useMemo(
() => applySelectedDiffHunks(originalMarkdown, hunks, selectedIds),
[hunks, originalMarkdown, selectedIds],
)
const applyAll = () => {
setSelectedIds(new Set(hunks.map((hunk) => hunk.id)))
}
const keepOriginal = () => {
setSelectedIds(new Set())
}
const applyToParent = () => {
if (!draftKey) {
toast.error('当前窗口缺少草稿标识,无法回填。')
return
}
const result = savePolishWindowResult(draftKey, mergedMarkdown, target)
window.opener?.postMessage(buildApplyMessage(draftKey, mergedMarkdown, target), window.location.origin)
toast.success('已把 AI 润色结果回填到原编辑器。')
return result
}
return (
<div className="min-h-screen bg-background px-4 py-6 text-foreground lg:px-6">
<div className="mx-auto max-w-[1560px] space-y-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div className="space-y-3">
<Badge variant="secondary">AI </Badge>
<div>
<h1 className="text-3xl font-semibold tracking-tight">
{snapshot?.title || 'AI 润色与选择性合并'}
</h1>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
稿 AI 稿
</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button
variant="outline"
disabled={!snapshot || polishing}
onClick={async () => {
if (!snapshot) {
return
}
try {
setPolishing(true)
const result = await adminApi.polishPostMarkdown(snapshot.markdown)
const nextHunks = computeDiffHunks(snapshot.markdown, result.polished_markdown)
startTransition(() => {
setPolishedMarkdown(result.polished_markdown)
setSelectedIds(new Set(nextHunks.map((hunk) => hunk.id)))
})
toast.success(`AI 已生成润色稿,共识别 ${nextHunks.length} 个改动块。`)
} catch (requestError) {
toast.error(requestError instanceof ApiError ? requestError.message : 'AI 润色失败。')
} finally {
setPolishing(false)
}
}}
>
<Bot className="h-4 w-4" />
{polishing ? '润色中...' : polishedMarkdown ? '重新生成润色稿' : '生成 AI 润色稿'}
</Button>
<Button variant="outline" disabled={!hunks.length} onClick={applyAll}>
<CheckCheck className="h-4 w-4" />
</Button>
<Button variant="outline" disabled={!hunks.length} onClick={keepOriginal}>
<RefreshCcw className="h-4 w-4" />
</Button>
<Button disabled={!hunks.length} onClick={applyToParent}>
<WandSparkles className="h-4 w-4" />
</Button>
</div>
</div>
{loading ? (
<Card>
<CardContent className="py-12 text-sm text-muted-foreground">稿...</CardContent>
</Card>
) : error ? (
<Card>
<CardHeader>
<CardTitle>AI </CardTitle>
<CardDescription>{error}</CardDescription>
</CardHeader>
</Card>
) : snapshot ? (
<div className="grid gap-6 xl:grid-cols-[1.14fr_0.86fr]">
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle> vs </CardTitle>
<CardDescription>{snapshot.path}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap items-center gap-3">
<Badge variant="secondary"> {hunks.length}</Badge>
<Badge variant="success"> {selectedIds.size}</Badge>
<Badge variant="outline"> {target === 'create' ? '新建草稿' : '现有文章'}</Badge>
</div>
<div className="overflow-hidden rounded-[28px] border border-slate-800 bg-[#1e1e1e]">
<div className="flex items-center justify-between border-b border-slate-800 bg-[#141414] px-4 py-2 text-[11px] uppercase tracking-[0.18em] text-slate-400">
<span>稿</span>
<span></span>
</div>
<div className="h-[560px]">
<DiffEditor
height="100%"
language="markdown"
original={originalMarkdown}
modified={mergedMarkdown}
originalModelPath={`${snapshot.path}#ai-original`}
modifiedModelPath={`${snapshot.path}#ai-merged`}
keepCurrentOriginalModel
keepCurrentModifiedModel
theme={editorTheme}
beforeMount={configureMonaco}
options={{
...sharedOptions,
originalEditable: false,
readOnly: true,
renderSideBySide: true,
}}
/>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="h-[420px] overflow-hidden rounded-[28px] border border-slate-200 bg-white">
<MarkdownPreview markdown={mergedMarkdown || originalMarkdown} />
</div>
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
diff
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{!polishedMarkdown ? (
<div className="rounded-3xl border border-dashed border-border/70 px-5 py-10 text-sm text-muted-foreground">
AI 稿
</div>
) : hunks.length ? (
hunks.map((hunk, index) => {
const accepted = selectedIds.has(hunk.id)
return (
<div
key={hunk.id}
className={`rounded-3xl border p-4 transition ${
accepted
? 'border-emerald-500/30 bg-emerald-500/10'
: 'border-border/70 bg-background/60'
}`}
>
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-medium"> {index + 1}</p>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
{hunk.originalStart}-{Math.max(hunk.originalEnd, hunk.originalStart - 1)}
稿 {hunk.modifiedStart}-{Math.max(hunk.modifiedEnd, hunk.modifiedStart - 1)}
</p>
</div>
<Button
size="sm"
variant={accepted ? 'default' : 'outline'}
onClick={() => {
setSelectedIds((current) => {
const next = new Set(current)
if (next.has(hunk.id)) {
next.delete(hunk.id)
} else {
next.add(hunk.id)
}
return next
})
}}
>
{accepted ? '已采用' : '采用这块'}
</Button>
</div>
<p className="mt-3 rounded-2xl border border-border/60 bg-background/70 px-3 py-2 text-xs leading-6 text-muted-foreground">
{hunk.preview}
</p>
</div>
)
})
) : (
<div className="rounded-3xl border border-border/70 px-5 py-10 text-sm text-muted-foreground">
AI
</div>
)}
</CardContent>
</Card>
</div>
</div>
) : null}
</div>
</div>
)
}