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

@@ -66,12 +66,7 @@ export function resolveLocale(options: {
return fromCookie;
}
const acceptLanguages = String(options.acceptLanguage || '')
.split(',')
.map((part) => normalizeLocale(part.split(';')[0]))
.filter(Boolean) as Locale[];
return acceptLanguages[0] || DEFAULT_LOCALE;
return DEFAULT_LOCALE;
}
export function translate(locale: Locale, key: string, params?: TranslateParams): string {

View File

@@ -4,7 +4,7 @@ export const messages = {
language: '语言',
languages: {
'zh-CN': '简体中文',
en: 'English',
en: '英文',
},
all: '全部',
search: '搜索',
@@ -63,6 +63,7 @@ export const messages = {
featureOff: '功能未开启',
emptyState: '当前还没有内容。',
apiUnavailable: 'API 暂时不可用',
unknownError: '未知错误',
},
nav: {
articles: '文章',
@@ -77,19 +78,31 @@ export const messages = {
header: {
navigation: '导航',
themeToggle: '切换主题',
themePanelTitle: '外观模式',
themeLight: '浅色',
themeDark: '深色',
themeSystem: '跟随系统',
themeLightHint: '始终使用亮色界面',
themeDarkHint: '始终使用暗色界面',
themeSystemHint: '跟随设备当前主题',
themeResolvedAs: '当前生效:{mode}',
toggleMenu: '切换菜单',
searchModeKeyword: '搜索',
searchModeAi: 'AI',
searchModeKeywordMobile: '关键词搜索',
searchModeAiMobile: 'AI 搜索',
shellLabel: '站点终端',
musicPanel: '播放控制',
searchPromptKeyword: '站内搜索',
searchPromptAi: 'AI 问答',
searchPlaceholderKeyword: "'关键词'",
searchPlaceholderAi: '输入问题,交给站内 AI',
searchHintKeyword: 'articles/*.md',
searchHintAi: '手动确认',
searchHintKeyword: '文章 / 标签 / 分类',
searchHintAi: '前往问答页',
aiModeTitle: 'AI 问答模式',
aiModeHeading: '把这个问题交给站内 AI',
aiModeDescription: 'AI 会先检索站内知识库,再给出总结式回答,并附带相关文章来源。',
aiModeNotice: '进入问答页后不会自动调用模型,需要你手动确认发送。',
aiModeDescription: '在问答页输入问题后,系统会优先参考站内内容并给出整理后的回答。',
aiModeNotice: '回答会附带相关文章,方便继续阅读。',
aiModeCta: '前往 AI 问答页确认',
liveResults: '实时搜索结果',
searching: '正在搜索 {query} ...',
@@ -106,12 +119,22 @@ export const messages = {
copyright: '© {year} {site}. 保留所有权利。',
sitemap: '站点地图',
rss: 'RSS 订阅',
summary: '持续整理文章、记录与站内阅读入口。',
},
home: {
pinned: '置顶',
quickJump: '快速跳转',
about: '关于我',
techStack: '技术栈',
systemStatus: '系统状态',
promptWelcome: 'pwd',
promptDiscoverDefault: "find ./posts -type f | sort",
promptDiscoverFiltered: 'grep -Ril "{filters}" ./posts',
promptPinned: 'grep -Ril "^pinned: true$" ./posts',
promptPostsDefault: "find ./posts -type f | head -n {count}",
promptPostsFiltered: 'grep -Ril "{filters}" ./posts | head -n {count}',
promptFriends: "find ./links -maxdepth 1 -type f | sort",
promptAbout: "sed -n '1,80p' ~/profile.md",
},
articlesPage: {
title: '文章索引',
@@ -131,16 +154,20 @@ export const messages = {
filePath: '文件路径',
},
relatedPosts: {
kicker: '关联轨迹',
title: '相关文章',
description: '基于当前分类与标签关联出的相近内容,延续同一条阅读链路。',
linked: '{count} 条关联',
},
comments: {
title: '评论终端',
kicker: '讨论缓冲区',
description: '这里是整篇文章的讨论区,当前缓冲区共有 {count} 条已展示评论,新的留言提交后会进入审核队列。',
writeComment: '写评论',
nickname: '昵称',
nicknamePlaceholder: '山客',
email: '邮箱',
emailPlaceholder: 'name@example.com',
message: '内容',
messagePlaceholder: "$ echo '留下你的想法...'",
maxChars: '最多 500 字',
@@ -160,15 +187,19 @@ export const messages = {
},
paragraphComments: {
title: '段落评论已启用',
kicker: '段落批注',
intro: '正文里的自然段都会挂一个轻量讨论入口,适合只针对某一段补充上下文、指出问题或继续展开讨论。',
scanning: '正在扫描段落缓冲区...',
noParagraphs: '当前文章没有可挂载评论的自然段。',
summary: '已为 {paragraphCount} 个自然段挂载评论入口,其中 {discussedCount} 段已有讨论,当前共展示 {approvedCount} 条已审核段落评论。',
focusCurrent: '聚焦当前段落',
panelTitle: '段落讨论面板',
panelKicker: '段落讨论线程',
close: '关闭',
nickname: '昵称',
nicknamePlaceholder: '林泉',
email: '邮箱',
emailPlaceholder: 'name@example.com',
comment: '评论',
commentPlaceholder: "$ echo '只评论这一段...'",
maxChars: '最多 500 字',
@@ -192,22 +223,29 @@ export const messages = {
zeroNotes: '评论',
waitingReview: '等待审核',
locateParagraph: '定位段落',
showMarkers: '显示段落评论',
hideMarkers: '隐藏段落评论',
markersHidden: '段落评论入口已隐藏,你仍然可以随时重新打开。',
badgeLabel: '打开这一段的评论面板',
},
ask: {
pageTitle: 'AI 问答',
pageDescription: '基于 {siteName} 内容知识库的站内 AI 问答',
pageDescription: '{siteName} 的站内 AI 问答入口',
title: 'AI 站内问答',
subtitle: '基于博客 Markdown 内容建立索引,回答会优先引用站内真实资料。',
subtitle: '围绕本站内容回答问题,并附上可继续阅读的相关文章。',
terminalLabel: '问答助手',
assistantLabel: '回答输出',
disabledStateLabel: '功能已关闭',
disabledTitle: '后台暂未开启 AI 问答',
disabledDescription: '这个入口已经接好了真实后端,但当前站点设置里没有开启公开问答。管理员开启后,这里会自动变成可用状态,导航也会同步显示。',
textareaPlaceholder: '输入你想问的问题,比如:这个博客关于前端写过哪些内容?',
submit: '开始提问',
idleStatus: '知识库已接入,等待问题输入。',
idleStatus: '可以直接输入问题开始提问。',
examples: '示例问题',
workflow: '工作流',
workflow1: '1. 后台开启 AI 开关并配置聊天模型。',
workflow2: '2. 重建索引,把 Markdown 文章切块后由后端本地生成 embedding并写入 PostgreSQL pgvector。',
workflow3: '3. 前台提问时先在 pgvector 中做相似度检索,再交给聊天模型基于上下文回答。',
guide: '提问建议',
guide1: '1. 直接问主题、文章、观点或站内某类内容。',
guide2: '2. 回答会优先结合本站已有内容,并给出可继续阅读的文章。',
guide3: '3. 如果问题太宽泛,换成更具体的关键词通常会更准确。',
emptyAnswer: '暂无回答。',
requestFailed: '请求失败:{message}',
streamUnsupported: '当前浏览器无法读取流式响应。',
@@ -220,7 +258,15 @@ export const messages = {
streamInterrupted: '流式响应被提前中断。',
retryLater: '这次请求没有成功,可以稍后重试。',
prefixedQuestion: '已带入搜索词,确认后开始提问。',
promptIdle: 'cat > question.txt',
promptEditing: "sed -n '1,12p' question.txt",
promptSubmitting: 'tail -f answer.stream',
promptComplete: "printf 'sources=%s\\n' {count}",
promptFailed: "echo 'retry'",
sources: '来源',
sourceScore: '相关度 {score}',
metaSources: '{count} 篇相关文章',
metaSourcesWithTime: '{count} 篇相关文章 · 更新于 {time}',
},
about: {
pageTitle: '关于',
@@ -236,8 +282,11 @@ export const messages = {
title: '文章分类',
intro: '按内容主题浏览文章,分类页现在和其他列表页保持同一套终端面板语言。',
quickJump: '快速跳转分类文章',
allCategoriesDescription: '查看全部分类下的文章与更新记录。',
categoryPosts: '浏览 {name} 主题下的全部文章和更新记录。',
selectedSummary: '{name} 分类下找到 {count} 篇文章',
empty: '暂无分类数据',
emptyPosts: '当前分类下没有文章',
},
friends: {
pageTitle: '友情链接',
@@ -254,6 +303,9 @@ export const messages = {
name: '名称',
description: '描述',
link: '链接',
promptBrowse: "find ./links -maxdepth 1 -type f | sort",
promptApply: 'cat > friend-link.txt',
promptRules: "sed -n '1,120p' rules.md",
},
friendForm: {
title: '提交友链申请',
@@ -312,6 +364,9 @@ export const messages = {
emptyData: '暂无评价数据,请检查后端 API 连接',
emptyFiltered: '当前筛选下暂无评价',
currentFilter: '当前筛选: {type}',
statusCompleted: '已完成',
statusInProgress: '进行中',
statusDropped: '已弃置',
typeAll: '全部',
typeGame: '游戏',
typeAnime: '动画',
@@ -330,7 +385,7 @@ export const messages = {
time: '时间',
actions: '可执行操作',
actionsIntro: '像命令面板一样,优先给出直接可走的恢复路径。',
searchHint: '也可以直接使用顶部的搜索输入框,在 `articles/*.md` 里重新 grep 一次相关关键字。',
searchHint: '使',
recommended: '',
recommendedIntro: '使 404 ',
cannotLoad: '',
@@ -409,6 +464,7 @@ export const messages = {
featureOff: 'Feature off',
emptyState: 'Nothing here yet.',
apiUnavailable: 'API temporarily unavailable',
unknownError: 'unknown error',
},
nav: {
articles: 'Articles',
@@ -423,19 +479,31 @@ export const messages = {
header: {
navigation: 'Navigation',
themeToggle: 'Toggle theme',
themePanelTitle: 'Appearance',
themeLight: 'Light',
themeDark: 'Dark',
themeSystem: 'System',
themeLightHint: 'Always use the light interface',
themeDarkHint: 'Always use the dark interface',
themeSystemHint: 'Follow the device appearance',
themeResolvedAs: 'Currently applied: {mode}',
toggleMenu: 'Toggle menu',
searchModeKeyword: 'Search',
searchModeAi: 'AI',
searchModeKeywordMobile: 'Keyword Search',
searchModeAiMobile: 'AI Search',
shellLabel: 'Site Terminal',
musicPanel: 'Playback',
searchPromptKeyword: 'Site Search',
searchPromptAi: 'Ask AI',
searchPlaceholderKeyword: "'keyword'",
searchPlaceholderAi: 'Type a question for the site AI',
searchHintKeyword: 'articles/*.md',
searchHintAi: 'manual confirm',
searchHintKeyword: 'posts / tags / categories',
searchHintAi: 'open AI Q&A',
aiModeTitle: 'AI Q&A mode',
aiModeHeading: 'Send this question to the site AI',
aiModeDescription: 'The AI will search the site knowledge base first, then answer with source-backed summaries.',
aiModeNotice: 'The model will not run automatically after navigation. You must confirm manually.',
aiModeDescription: 'Ask on the Q&A page and the system will answer with priority given to on-site content.',
aiModeNotice: 'Answers include related articles so visitors can keep reading.',
aiModeCta: 'Open AI Q&A to confirm',
liveResults: 'Live results',
searching: 'Searching {query} ...',
@@ -452,12 +520,22 @@ export const messages = {
copyright: '© {year} {site}. All rights reserved.',
sitemap: 'Sitemap',
rss: 'RSS feed',
summary: 'A place for posts, notes, and on-site reading paths.',
},
home: {
pinned: 'Pinned',
quickJump: 'Quick jump',
about: 'About',
techStack: 'Tech stack',
systemStatus: 'System status',
promptWelcome: 'pwd',
promptDiscoverDefault: "find ./posts -type f | sort",
promptDiscoverFiltered: 'grep -Ril "{filters}" ./posts',
promptPinned: 'grep -Ril "^pinned: true$" ./posts',
promptPostsDefault: "find ./posts -type f | head -n {count}",
promptPostsFiltered: 'grep -Ril "{filters}" ./posts | head -n {count}',
promptFriends: "find ./links -maxdepth 1 -type f | sort",
promptAbout: "sed -n '1,80p' ~/profile.md",
},
articlesPage: {
title: 'Article Index',
@@ -477,16 +555,20 @@ export const messages = {
filePath: 'File path',
},
relatedPosts: {
kicker: 'Related traces',
title: 'Related Posts',
description: 'More nearby reading paths based on the current category and shared tags.',
linked: '{count} linked',
},
comments: {
title: 'Comment Terminal',
kicker: 'Discussion Buffer',
description: 'This is the discussion thread for the whole article. {count} approved comments are shown right now, and new messages enter moderation first.',
writeComment: 'Write comment',
nickname: 'Nickname',
nicknamePlaceholder: 'trail_reader',
email: 'Email',
emailPlaceholder: 'you@example.com',
message: 'Message',
messagePlaceholder: "$ echo 'Leave your thoughts here...'",
maxChars: 'Max 500 chars',
@@ -506,15 +588,19 @@ export const messages = {
},
paragraphComments: {
title: 'Paragraph comments are enabled',
kicker: 'Paragraph Notes',
intro: 'Each natural paragraph in the article gets a lightweight discussion entry point, perfect for focused context, corrections, or follow-up questions.',
scanning: 'Scanning paragraph buffer...',
noParagraphs: 'No commentable paragraphs were found in this article.',
summary: '{paragraphCount} paragraphs have comment entries, {discussedCount} already have discussion, and {approvedCount} approved paragraph comments are currently visible.',
focusCurrent: 'Focus current paragraph',
panelTitle: 'Paragraph discussion panel',
panelKicker: 'Paragraph thread',
close: 'Close',
nickname: 'Nickname',
nicknamePlaceholder: 'inline_reader',
email: 'Email',
emailPlaceholder: 'you@example.com',
comment: 'Comment',
commentPlaceholder: "$ echo 'Comment on this paragraph only...'",
maxChars: 'Max 500 chars',
@@ -538,22 +624,29 @@ export const messages = {
zeroNotes: 'comment',
waitingReview: 'waiting review',
locateParagraph: 'Locate paragraph',
showMarkers: 'Show paragraph comments',
hideMarkers: 'Hide paragraph comments',
markersHidden: 'Paragraph comment markers are hidden. You can turn them back on anytime.',
badgeLabel: 'Open comments for this paragraph',
},
ask: {
pageTitle: 'Ask AI',
pageDescription: 'An on-site AI Q&A experience grounded in the {siteName} knowledge base',
pageDescription: 'An on-site AI Q&A entry for {siteName}',
title: 'On-site AI Q&A',
subtitle: 'Answers are grounded in indexed Markdown content from the blog and prioritize real on-site references.',
subtitle: 'Ask about the site and get answers with related articles attached for follow-up reading.',
terminalLabel: 'Q&A Assistant',
assistantLabel: 'Assistant Output',
disabledStateLabel: 'Feature Disabled',
disabledTitle: 'AI Q&A is not enabled yet',
disabledDescription: 'The real backend integration is already in place, but public Q&A is still disabled in site settings. Once it is enabled, this page and the navigation entry will become available automatically.',
textareaPlaceholder: 'Ask anything, for example: what has this blog written about frontend topics?',
submit: 'Ask now',
idleStatus: 'Knowledge base connected. Waiting for a question.',
idleStatus: 'Type a question to get started.',
examples: 'Example questions',
workflow: 'Workflow',
workflow1: '1. Enable the AI switch in the admin and configure the chat model.',
workflow2: '2. Rebuild the index so Markdown content is chunked, embedded locally by the backend, and written into PostgreSQL pgvector.',
workflow3: '3. Each user question retrieves similar chunks from pgvector first, then the chat model answers with that context.',
guide: 'Asking tips',
guide1: '1. Ask directly about topics, posts, viewpoints, or recurring themes on the site.',
guide2: '2. Answers prioritize on-site material and include related reading when available.',
guide3: '3. If the answer feels broad, try a more specific keyword or article topic.',
emptyAnswer: 'No answer yet.',
requestFailed: 'Request failed: {message}',
streamUnsupported: 'This browser cannot read streaming responses.',
@@ -566,7 +659,15 @@ export const messages = {
streamInterrupted: 'The streaming response ended early.',
retryLater: 'This request did not complete successfully. Please try again later.',
prefixedQuestion: 'The search query has been prefilled. Confirm manually to ask AI.',
promptIdle: 'cat > question.txt',
promptEditing: "sed -n '1,12p' question.txt",
promptSubmitting: 'tail -f answer.stream',
promptComplete: "printf 'sources=%s\\n' {count}",
promptFailed: "echo 'retry'",
sources: 'Sources',
sourceScore: 'Score {score}',
metaSources: '{count} related articles',
metaSourcesWithTime: '{count} related articles · updated {time}',
},
about: {
pageTitle: 'About',
@@ -582,8 +683,11 @@ export const messages = {
title: 'Categories',
intro: 'Browse posts by topic. This page now follows the same terminal language as the other list views.',
quickJump: 'Jump straight into category posts',
allCategoriesDescription: 'Browse posts and updates from every category.',
categoryPosts: 'Browse all posts and updates under {name}.',
selectedSummary: '{count} posts in {name}',
empty: 'No category data yet',
emptyPosts: 'No posts found in this category',
},
friends: {
pageTitle: 'Links',
@@ -600,6 +704,9 @@ export const messages = {
name: 'Name',
description: 'Description',
link: 'Link',
promptBrowse: "find ./links -maxdepth 1 -type f | sort",
promptApply: 'cat > friend-link.txt',
promptRules: "sed -n '1,120p' rules.md",
},
friendForm: {
title: 'Submit a link request',
@@ -658,6 +765,9 @@ export const messages = {
emptyData: 'No review data yet. Please check the backend API connection.',
emptyFiltered: 'No reviews match the current filter',
currentFilter: 'Current filter: {type}',
statusCompleted: 'Completed',
statusInProgress: 'In progress',
statusDropped: 'Dropped',
typeAll: 'All',
typeGame: 'Games',
typeAnime: 'Anime',
@@ -676,7 +786,7 @@ export const messages = {
time: 'time',
actions: 'Actions',
actionsIntro: 'Like a command palette, this page surfaces the most direct recovery paths first.',
searchHint: 'You can also use the search box in the header and grep through `articles/*.md` again.',
searchHint: 'You can also use the search box in the header to search related posts again.',
recommended: 'Recommended entries',
recommendedIntro: 'These use real article data so the 404 page does not send people into more dead ends.',
cannotLoad: 'Unable to load the article list right now.',