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,247 @@
import { normalizeMarkdown } from '@/lib/markdown-diff'
export type ParsedMarkdownMeta = {
title: string
slug: string
description: string
category: string
postType: string
image: string
images: string[]
pinned: boolean
published: boolean
tags: string[]
}
export type ParsedMarkdownDocument = {
meta: ParsedMarkdownMeta
body: string
markdown: string
}
const defaultMeta: ParsedMarkdownMeta = {
title: '',
slug: '',
description: '',
category: '',
postType: 'article',
image: '',
images: [],
pinned: false,
published: true,
tags: [],
}
function parseScalar(value: string) {
const trimmed = value.trim()
if (!trimmed) {
return ''
}
if (
trimmed.startsWith('"') ||
trimmed.startsWith("'") ||
trimmed.startsWith('[') ||
trimmed.startsWith('{')
) {
try {
return JSON.parse(trimmed)
} catch {
return trimmed.replace(/^['"]|['"]$/g, '')
}
}
if (trimmed === 'true') {
return true
}
if (trimmed === 'false') {
return false
}
return trimmed
}
function toStringList(value: unknown) {
if (Array.isArray(value)) {
return value
.map((item) => String(item).trim())
.filter(Boolean)
}
if (typeof value === 'string') {
return value
.split(/[,]/)
.map((item) => item.trim())
.filter(Boolean)
}
return []
}
export function parseMarkdownDocument(markdown: string): ParsedMarkdownDocument {
const normalized = normalizeMarkdown(markdown)
const meta: ParsedMarkdownMeta = { ...defaultMeta }
if (!normalized.startsWith('---\n')) {
return {
meta,
body: normalized.trimStart(),
markdown: normalized,
}
}
const endIndex = normalized.indexOf('\n---\n', 4)
if (endIndex === -1) {
return {
meta,
body: normalized.trimStart(),
markdown: normalized,
}
}
const frontmatter = normalized.slice(4, endIndex)
const body = normalized.slice(endIndex + 5).trimStart()
let currentListKey: 'tags' | 'images' | 'categories' | null = null
const categories: string[] = []
frontmatter.split('\n').forEach((line) => {
const listItemMatch = line.match(/^\s*-\s*(.+)\s*$/)
if (listItemMatch && currentListKey) {
const parsed = parseScalar(listItemMatch[1])
const nextValue = typeof parsed === 'string' ? parsed.trim() : String(parsed).trim()
if (!nextValue) {
return
}
if (currentListKey === 'tags') {
meta.tags.push(nextValue)
} else if (currentListKey === 'images') {
meta.images.push(nextValue)
} else {
categories.push(nextValue)
}
return
}
currentListKey = null
const keyMatch = line.match(/^([A-Za-z_]+):\s*(.*)$/)
if (!keyMatch) {
return
}
const [, rawKey, rawValue] = keyMatch
const key = rawKey.trim()
const value = parseScalar(rawValue)
if (key === 'tags') {
const tags = toStringList(value)
if (tags.length) {
meta.tags = tags
} else if (!String(rawValue).trim()) {
currentListKey = 'tags'
}
return
}
if (key === 'images') {
const images = toStringList(value)
if (images.length) {
meta.images = images
} else if (!String(rawValue).trim()) {
currentListKey = 'images'
}
return
}
if (key === 'categories' || key === 'category') {
const parsedCategories = toStringList(value)
if (parsedCategories.length) {
categories.push(...parsedCategories)
} else if (!String(rawValue).trim()) {
currentListKey = 'categories'
}
return
}
switch (key) {
case 'title':
meta.title = String(value).trim()
break
case 'slug':
meta.slug = String(value).trim()
break
case 'description':
meta.description = String(value).trim()
break
case 'post_type':
meta.postType = String(value).trim() || 'article'
break
case 'image':
meta.image = String(value).trim()
break
case 'pinned':
meta.pinned = Boolean(value)
break
case 'published':
meta.published = value !== false
break
case 'draft':
if (value === true) {
meta.published = false
}
break
default:
break
}
})
meta.category = categories[0] ?? meta.category
return {
meta,
body,
markdown: normalized,
}
}
export function buildMarkdownDocument(meta: ParsedMarkdownMeta, body: string) {
const lines = [
'---',
`title: ${JSON.stringify(meta.title.trim() || meta.slug || 'untitled-post')}`,
`slug: ${meta.slug.trim() || 'untitled-post'}`,
]
if (meta.description.trim()) {
lines.push(`description: ${JSON.stringify(meta.description.trim())}`)
}
if (meta.category.trim()) {
lines.push(`category: ${JSON.stringify(meta.category.trim())}`)
}
lines.push(`post_type: ${JSON.stringify(meta.postType.trim() || 'article')}`)
lines.push(`pinned: ${meta.pinned ? 'true' : 'false'}`)
lines.push(`published: ${meta.published ? 'true' : 'false'}`)
if (meta.image.trim()) {
lines.push(`image: ${JSON.stringify(meta.image.trim())}`)
}
if (meta.images.length) {
lines.push('images:')
meta.images.forEach((image) => {
lines.push(` - ${JSON.stringify(image)}`)
})
}
if (meta.tags.length) {
lines.push('tags:')
meta.tags.forEach((tag) => {
lines.push(` - ${JSON.stringify(tag)}`)
})
}
return `${lines.join('\n')}\n---\n\n${body.trim()}\n`
}