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

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ use sea_orm::{
ActiveModelTrait, ColumnTrait, Condition, EntityTrait, IntoActiveModel, QueryFilter,
QueryOrder, Set,
};
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
@@ -18,12 +18,21 @@ struct MarkdownFrontmatter {
title: Option<String>,
slug: Option<String>,
description: Option<String>,
category: Option<String>,
#[serde(
default,
alias = "category",
alias = "categories",
deserialize_with = "deserialize_optional_string_list"
)]
categories: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_optional_string_list")]
tags: Option<Vec<String>>,
post_type: Option<String>,
image: Option<String>,
images: Option<Vec<String>>,
pinned: Option<bool>,
published: Option<bool>,
draft: Option<bool>,
}
#[derive(Debug, Clone, Serialize)]
@@ -36,6 +45,7 @@ pub struct MarkdownPost {
pub tags: Vec<String>,
pub post_type: String,
pub image: Option<String>,
pub images: Vec<String>,
pub pinned: bool,
pub published: bool,
pub file_path: String,
@@ -51,6 +61,7 @@ pub struct MarkdownPostDraft {
pub tags: Vec<String>,
pub post_type: String,
pub image: Option<String>,
pub images: Vec<String>,
pub pinned: bool,
pub published: bool,
}
@@ -104,13 +115,71 @@ fn trim_to_option(input: Option<String>) -> Option<String> {
})
}
fn normalize_string_list(values: Option<Vec<String>>) -> Vec<String> {
values
.unwrap_or_default()
.into_iter()
.map(|item| item.trim().to_string())
.filter(|item| !item.is_empty())
.collect()
}
fn split_inline_list(value: &str) -> Vec<String> {
value
.split([',', ''])
.map(|item| item.trim().to_string())
.filter(|item| !item.is_empty())
.collect()
}
fn deserialize_optional_string_list<'de, D>(
deserializer: D,
) -> std::result::Result<Option<Vec<String>>, D::Error>
where
D: Deserializer<'de>,
{
let raw = Option::<serde_yaml::Value>::deserialize(deserializer)?;
match raw {
None | Some(serde_yaml::Value::Null) => Ok(None),
Some(serde_yaml::Value::String(value)) => {
let items = split_inline_list(&value);
if items.is_empty() && !value.trim().is_empty() {
Ok(Some(vec![value.trim().to_string()]))
} else if items.is_empty() {
Ok(None)
} else {
Ok(Some(items))
}
}
Some(serde_yaml::Value::Sequence(items)) => Ok(Some(
items
.into_iter()
.filter_map(|item| match item {
serde_yaml::Value::String(value) => {
let trimmed = value.trim().to_string();
(!trimmed.is_empty()).then_some(trimmed)
}
serde_yaml::Value::Number(value) => Some(value.to_string()),
_ => None,
})
.collect(),
)),
Some(other) => Err(serde::de::Error::custom(format!(
"unsupported frontmatter list value: {other:?}"
))),
}
}
fn slugify(value: &str) -> String {
let mut slug = String::new();
let mut last_was_dash = false;
for ch in value.trim().chars() {
if ch.is_ascii_alphanumeric() {
slug.push(ch.to_ascii_lowercase());
if ch.is_alphanumeric() {
for lower in ch.to_lowercase() {
slug.push(lower);
}
last_was_dash = false;
} else if (ch.is_whitespace() || ch == '-' || ch == '_') && !last_was_dash {
slug.push('-');
@@ -208,7 +277,9 @@ fn parse_markdown_source(file_stem: &str, raw: &str, file_path: &str) -> Result<
.unwrap_or_else(|| slug.clone());
let description =
trim_to_option(frontmatter.description.clone()).or_else(|| excerpt_from_content(&content));
let category = trim_to_option(frontmatter.category.clone());
let category = normalize_string_list(frontmatter.categories.clone())
.into_iter()
.next();
let tags = frontmatter
.tags
.unwrap_or_default()
@@ -227,8 +298,11 @@ fn parse_markdown_source(file_stem: &str, raw: &str, file_path: &str) -> Result<
post_type: trim_to_option(frontmatter.post_type.clone())
.unwrap_or_else(|| "article".to_string()),
image: trim_to_option(frontmatter.image.clone()),
images: normalize_string_list(frontmatter.images.clone()),
pinned: frontmatter.pinned.unwrap_or(false),
published: frontmatter.published.unwrap_or(true),
published: frontmatter
.published
.unwrap_or(!frontmatter.draft.unwrap_or(false)),
file_path: file_path.to_string(),
})
}
@@ -266,6 +340,13 @@ fn build_markdown_document(post: &MarkdownPost) -> String {
lines.push(format!("image: {}", image));
}
if !post.images.is_empty() {
lines.push("images:".to_string());
for image in &post.images {
lines.push(format!(" - {}", image));
}
}
if !post.tags.is_empty() {
lines.push("tags:".to_string());
for tag in &post.tags {
@@ -307,6 +388,7 @@ fn ensure_markdown_posts_bootstrapped() -> Result<()> {
tags: fixture.tags.unwrap_or_default(),
post_type: "article".to_string(),
image: None,
images: Vec::new(),
pinned: fixture.pinned.unwrap_or(false),
published: fixture.published.unwrap_or(true),
file_path: markdown_post_path(&fixture.slug)
@@ -470,7 +552,11 @@ async fn canonicalize_tags(ctx: &AppContext, raw_tags: &[String]) -> Result<Vec<
}
fn write_markdown_post_to_disk(post: &MarkdownPost) -> Result<()> {
fs::write(markdown_post_path(&post.slug), build_markdown_document(post)).map_err(io_error)
fs::write(
markdown_post_path(&post.slug),
build_markdown_document(post),
)
.map_err(io_error)
}
pub fn rewrite_category_references(
@@ -701,6 +787,17 @@ pub async fn sync_markdown_posts(ctx: &AppContext) -> Result<Vec<MarkdownPost>>
});
model.post_type = Set(Some(post.post_type.clone()));
model.image = Set(post.image.clone());
model.images = Set(if post.images.is_empty() {
None
} else {
Some(Value::Array(
post.images
.iter()
.cloned()
.map(Value::String)
.collect::<Vec<_>>(),
))
});
model.pinned = Set(Some(post.pinned));
if has_existing {
@@ -796,6 +893,7 @@ pub async fn create_markdown_post(
}
},
image: trim_to_option(draft.image),
images: normalize_string_list(Some(draft.images)),
pinned: draft.pinned,
published: draft.published,
file_path: markdown_post_path(&slug).to_string_lossy().to_string(),