This commit is contained in:
parent
6cb68866a4
commit
882171915c
@ -1,4 +1,7 @@
|
||||
param([ValidateSet('backend', 'frontend')][string]$Service = 'backend')
|
||||
param(
|
||||
[ValidateSet('backend', 'frontend')][string]$Service = 'backend',
|
||||
[switch]$SkipBuild
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
@ -9,8 +12,11 @@ $previousRoot = $env:SMAIL_ROOT
|
||||
try {
|
||||
if ($Service -eq 'backend') {
|
||||
$binary = Join-Path $root 'out\backend\smail-dev.exe'
|
||||
if (-not (Test-Path -LiteralPath $binary)) { throw '请先运行 deploy/build.ps1 编译后端' }
|
||||
if (Get-NetTCPConnection -State Listen -LocalPort 443 -ErrorAction SilentlyContinue) { throw '443 端口已占用,请先确认并停止旧后端' }
|
||||
$running = Get-CimInstance Win32_Process | Where-Object {
|
||||
$_.ExecutablePath -and $_.ExecutablePath -eq $binary
|
||||
}
|
||||
if ($running) { throw '开发后端仍在运行,请先核对并停止旧进程,避免编译或启动旧版本' }
|
||||
# 拒绝沿用旧运行目录,避免迁移遗漏时静默创建另一份账号库。
|
||||
$legacyConfig = Join-Path $root 'backend\config'
|
||||
foreach ($name in @('config.json', 'config.dev.json', 'credential.key', 'smail.db', 'smail_temp.db')) {
|
||||
@ -18,9 +24,17 @@ try {
|
||||
throw 'backend/config 中仍有运行文件,请先停止后端并完成根目录 config 迁移'
|
||||
}
|
||||
}
|
||||
# 默认先编译当前工作区,不再把历史 smail-dev.exe 当成最新后端。
|
||||
# 不编译前端,保留 Vite 热更新;缺少首次嵌入资源时要求显式完整构建。
|
||||
if (-not $SkipBuild) {
|
||||
& (Join-Path $PSScriptRoot 'build.ps1') -SkipFrontend
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) { throw '请先运行 deploy/build.ps1 编译后端' }
|
||||
# SMAIL_ROOT 是 config 的父目录,不能指向 config 本身或编译输出目录。
|
||||
$env:SMAIL_ROOT = $root
|
||||
$arguments = @('--dev-disable-admin-reset')
|
||||
Write-Output '开发启动:沿用 config/config.json 和现有数据库;明确停用管理员恢复通道,不请求权限提升。'
|
||||
if ($SkipBuild) { Write-Warning '已跳过编译,请自行确认二进制与当前源码一致。' }
|
||||
# 插件从 out/plugins 加载;账号、配置和密钥仍通过 SMAIL_ROOT 定位到根目录 config。
|
||||
foreach ($name in @('spam_block.exe', 'pmail_telegram_push.exe', 'wechat_push.exe')) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $root "out\plugins\$name") -PathType Leaf)) {
|
||||
@ -36,13 +50,15 @@ try {
|
||||
$arguments = @(('"{0}"' -f $vite), '--host', '127.0.0.1', '--port', '15173', '--strictPort')
|
||||
$workingDirectory = Join-Path $root 'frontend'
|
||||
}
|
||||
$stdout = Join-Path $logs "$Service-$stamp.stdout.log"
|
||||
$stderr = Join-Path $logs "$Service-$stamp.stderr.log"
|
||||
$process = Start-Process -FilePath $binary -ArgumentList $arguments -WorkingDirectory $workingDirectory -WindowStyle Hidden -PassThru `
|
||||
-RedirectStandardOutput (Join-Path $logs "$Service-$stamp.stdout.log") `
|
||||
-RedirectStandardError (Join-Path $logs "$Service-$stamp.stderr.log")
|
||||
-RedirectStandardOutput $stdout -RedirectStandardError $stderr
|
||||
Start-Sleep -Seconds 3
|
||||
$process.Refresh()
|
||||
if ($process.HasExited) { throw "服务启动失败,请检查 test-log/runtime 中的 $Service 日志" }
|
||||
if ($process.HasExited) { throw "服务启动失败;标准输出:$stdout;错误日志:$stderr" }
|
||||
Write-Output "$Service 已启动,进程编号:$($process.Id);日志目录:test-log/runtime"
|
||||
Write-Output "运行文件:$binary;标准输出:$stdout;错误日志:$stderr"
|
||||
} finally {
|
||||
$env:SMAIL_ROOT = $previousRoot
|
||||
}
|
||||
|
||||
@ -20,6 +20,8 @@ powershell -NoProfile -ExecutionPolicy Bypass -File deploy/start-dev.ps1 -Servic
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File deploy/start-dev.ps1 -Service frontend
|
||||
```
|
||||
|
||||
后端启动入口默认先编译当前源码及内置插件,再带明确的开发停用参数启动,不请求 UAC 提升。前端使用 Vite 热更新,不会因此切换后端配置模式。首次需完整构建以准备 Go 嵌入资源;后续只改后端时直接运行 `start-dev.ps1 -Service backend` 即可。已自行完成最新构建时可加 `-SkipBuild`,脚本会提示由调用者确认版本。不要无参数双击历史二进制:无参数属于正式模式,恢复通道权限检查仍会生效。
|
||||
|
||||
此参数只停用本机超级管理员密码重置通道,不改变配置文件选择,仍读取 `config/config.json`。账号、数据库、HTTPS、Cookie 和 CSRF 策略均沿用原配置。开发日志会明确提示重置通道已停用。
|
||||
|
||||
2026-09-05 配置管理修复后,当前 `config/config.json` 已迁移为分区根配置。必须先执行上面的编译命令,再运行新二进制;不要直接运行迁移前保留的旧程序。备份和本次验收边界见《配置管理接入修复验收》。后续修复期间已成功普通权限停止旧后端并启动新后端,下方早期停止受阻记录仅反映当时状态。
|
||||
@ -42,6 +44,7 @@ go run . dev
|
||||
|
||||
- 移除 `--dev-disable-admin-reset`,不要以末尾 `dev` 启动。
|
||||
- Windows 保持受保护命名管道及管理员客户端校验;Linux 保持受保护本机套接字及对端身份校验。
|
||||
- 安装需要的管理员权限不等于业务必须以管理员运行。Linux 服务使用 `smail` 账户和最小端口绑定能力;Windows 使用专用服务身份。正式服务身份对管道的可用性需要在真实安装环境单独验收,不能把普通开发参数作为生产修复。
|
||||
- 通道创建或运行失败仍会让正式后端启动失败或退出,不静默降级。
|
||||
- 没有新增 HTTP、局域网或公网重置接口。
|
||||
- 启动参数属于部署主机权限边界,不能由网页请求切换。不要把开发启动命令复制进正式服务配置。
|
||||
|
||||
@ -4,8 +4,7 @@ import App from './App.vue'
|
||||
import {router} from './router'
|
||||
import {useGlobalStatusStore} from './stores/useGlobalStatusStore'
|
||||
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import './styles/element-services.css'
|
||||
import './assets/main.css'
|
||||
|
||||
const isReservedResetPath = (pathname) => {
|
||||
@ -41,5 +40,4 @@ router.onError((error) => {
|
||||
})
|
||||
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
app.mount('#app')
|
||||
|
||||
8
frontend/src/styles/element-services.css
Normal file
8
frontend/src/styles/element-services.css
Normal file
@ -0,0 +1,8 @@
|
||||
/* 模板组件和指令由解析器按需加载;显式调用的服务需要独立补齐样式。 */
|
||||
@import 'element-plus/theme-chalk/base.css';
|
||||
@import 'element-plus/theme-chalk/el-message.css';
|
||||
@import 'element-plus/theme-chalk/el-message-box.css';
|
||||
@import 'element-plus/theme-chalk/el-notification.css';
|
||||
@import 'element-plus/theme-chalk/el-overlay.css';
|
||||
@import 'element-plus/theme-chalk/el-button.css';
|
||||
@import 'element-plus/theme-chalk/el-input.css';
|
||||
@ -1,28 +1,31 @@
|
||||
import {EpMenu} from "vue-icons-plus/ep";
|
||||
import { Lock, Connection, Setting, FolderOpened, Filter, User, Grid } from '@element-plus/icons-vue';
|
||||
|
||||
import {computed, ref, onMounted, onUnmounted} from 'vue'
|
||||
|
||||
import SecuritySettings from '@/views/components/SecuritySettings/SecuritySettings.vue'
|
||||
|
||||
import ThirdPartyAuthorization from '@/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.vue'
|
||||
|
||||
import ConfigSettings from '@/views/components/ConfigSettings/ConfigSettings.vue'
|
||||
import {computed, ref, onMounted, onUnmounted, defineAsyncComponent} from 'vue'
|
||||
import SettingsPageStatus from '../SettingsPageStatus/SettingsPageStatus.vue'
|
||||
|
||||
import lang from '../../../i18n/i18n';
|
||||
|
||||
import GroupSettings from '../GroupSettings/GroupSettings.vue';
|
||||
|
||||
import RuleSettings from '../RuleSettings/RuleSettings.vue';
|
||||
|
||||
import UserManagement from '../UserManagement/UserManagement.vue';
|
||||
|
||||
import PluginSettings from '../PluginSettings/PluginSettings.vue';
|
||||
|
||||
import {useGlobalStatusStore} from "@/stores/useGlobalStatusStore";
|
||||
|
||||
import BrandLogo from '../BrandLogo/BrandLogo.vue';
|
||||
|
||||
// 分类首次打开时才下载页面,失败时保留可读提示,不能显示空白面板。
|
||||
const lazySettings = loader => defineAsyncComponent({
|
||||
loader,
|
||||
loadingComponent: SettingsPageStatus,
|
||||
errorComponent: SettingsPageStatus,
|
||||
delay: 150,
|
||||
timeout: 30000
|
||||
})
|
||||
const SecuritySettings = lazySettings(() => import('../SecuritySettings/SecuritySettings.vue'))
|
||||
const ThirdPartyAuthorization = lazySettings(() => import('../ThirdPartyAuthorization/ThirdPartyAuthorization.vue'))
|
||||
const ConfigSettings = lazySettings(() => import('../ConfigSettings/ConfigSettings.vue'))
|
||||
const GroupSettings = lazySettings(() => import('../GroupSettings/GroupSettings.vue'))
|
||||
const RuleSettings = lazySettings(() => import('../RuleSettings/RuleSettings.vue'))
|
||||
const UserManagement = lazySettings(() => import('../UserManagement/UserManagement.vue'))
|
||||
const PluginSettings = lazySettings(() => import('../PluginSettings/PluginSettings.vue'))
|
||||
|
||||
export function usePage() {
|
||||
const globalStatus = useGlobalStatusStore();
|
||||
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
.settings-page-status {
|
||||
padding: 24px;
|
||||
color: var(--pm-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.settings-page-status[role='alert'] {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="settings-page-status" :role="error ? 'alert' : 'status'">
|
||||
{{ error ? '设置页面加载失败,请刷新后重试。' : '正在加载设置页面…' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ error: { type: Error, default: null } })
|
||||
</script>
|
||||
|
||||
<style scoped src="./SettingsPageStatus.css"></style>
|
||||
@ -74,13 +74,27 @@ export default defineConfig(({ mode }) => {
|
||||
build: {
|
||||
outDir: fileURLToPath(new URL('../out/frontend', import.meta.url)),
|
||||
emptyOutDir: false,
|
||||
manifest: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'vue-core': ['vue', 'vue-router', 'pinia'],
|
||||
'element-plus': ['element-plus'],
|
||||
'editor': ['@wangeditor/editor', '@wangeditor/editor-for-vue'],
|
||||
'icons': ['@element-plus/icons-vue', 'vue-icons-plus']
|
||||
manualChunks(id) {
|
||||
const modulePath = id.replaceAll('\\', '/')
|
||||
if (!modulePath.includes('/node_modules/')) return
|
||||
if (/\/node_modules\/(?:@vue\/|vue\/|vue-router\/|pinia\/)/.test(modulePath)) {
|
||||
return 'vue-core'
|
||||
}
|
||||
// 编辑器仅在写信页面加载;按包拆分其依赖,避免单个异步块超过浏览器缓存阈值。
|
||||
const packagePath = modulePath.split('/node_modules/')[1]
|
||||
const packageParts = packagePath.split('/')
|
||||
const packageName = packageParts[0].startsWith('@')
|
||||
? `${packageParts[0]}-${packageParts[1]}`
|
||||
: packageParts[0]
|
||||
if (packageName.startsWith('@wangeditor-')) {
|
||||
return `editor-${packageName.slice('@wangeditor-'.length)}`
|
||||
}
|
||||
if (['@uppy-core', '@uppy-xhr-upload', 'slate', 'snabbdom'].includes(packageName)) {
|
||||
return `editor-${packageName.replace(/^@uppy-/, '')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,18 @@ test('前端构建必须自动同步 Go 嵌入资源', async () => {
|
||||
assert.match(await read('plugins/pmail_telegram_push/vite.config.ts'), /\.\.\/\.\.\/out\/plugins\/telegram-ui/)
|
||||
})
|
||||
|
||||
test('前端入口按需加载 Element Plus,并延迟设置页和写信编辑器', async () => {
|
||||
const main = await read('frontend/src/main.js')
|
||||
const vite = await read('frontend/vite.config.js')
|
||||
const header = await read('frontend/src/views/components/HomeHeader/HomeHeader.js')
|
||||
assert.doesNotMatch(main, /app\.use\(ElementPlus\)/)
|
||||
assert.doesNotMatch(main, /element-plus\/dist\/index\.css/)
|
||||
assert.match(main, /element-services\.css/)
|
||||
assert.match(vite, /manualChunks\(id\)/)
|
||||
assert.match(header, /defineAsyncComponent/)
|
||||
assert.match(header, /import\('\.\.\/ConfigSettings\/ConfigSettings\.vue'\)/)
|
||||
})
|
||||
|
||||
test('开发启动必须固定运行配置根目录并集中保存日志', async () => {
|
||||
const content = await read('deploy/start-dev.ps1')
|
||||
assert.match(content, /\$env:SMAIL_ROOT = \$root\r?\n/)
|
||||
@ -38,6 +50,9 @@ test('开发启动必须固定运行配置根目录并集中保存日志', async
|
||||
assert.match(content, /out\\backend\\smail-dev\.exe/)
|
||||
assert.match(content, /test-log\\runtime/)
|
||||
assert.match(content, /-WindowStyle Hidden/)
|
||||
assert.match(content, /\[switch\]\$SkipBuild/)
|
||||
assert.match(content, /--dev-disable-admin-reset/)
|
||||
assert.match(content, /默认先编译当前工作区/)
|
||||
assert.ok(content.charCodeAt(0) === 0xfeff, 'Windows PowerShell 5 脚本应有 UTF-8 BOM')
|
||||
})
|
||||
|
||||
|
||||
@ -36,13 +36,12 @@ test('一级菜单保留能力过滤和当前分类标题联动', () => {
|
||||
const source = read('HomeHeader/HomeHeader.js')
|
||||
const names = [
|
||||
'EpMenu', 'Lock', 'Connection', 'Setting', 'FolderOpened', 'Filter', 'User', 'Grid',
|
||||
'SecuritySettings', 'ThirdPartyAuthorization', 'ConfigSettings', 'GroupSettings',
|
||||
'RuleSettings', 'UserManagement', 'PluginSettings', 'BrandLogo'
|
||||
'SettingsPageStatus', 'BrandLogo'
|
||||
]
|
||||
const createPage = compileFunction(
|
||||
source.replace(/^import .*$/gm, '').replace('export function usePage', 'function usePage')
|
||||
.replace(/export \{[\s\S]*?\}\s*$/, '') + '\nreturn usePage()',
|
||||
['computed', 'ref', 'onMounted', 'onUnmounted', 'window', 'useGlobalStatusStore', 'lang', ...names]
|
||||
['computed', 'ref', 'onMounted', 'onUnmounted', 'window', 'useGlobalStatusStore', 'lang', 'defineAsyncComponent', ...names]
|
||||
)
|
||||
const capabilities = reactive(new Set(['password.change_own', 'user.read']))
|
||||
const store = reactive({
|
||||
@ -50,7 +49,7 @@ test('一级菜单保留能力过滤和当前分类标题联动', () => {
|
||||
hasCapability: value => capabilities.has(value)
|
||||
})
|
||||
const page = createPage(computed, ref, () => {}, () => {}, { innerWidth: 1440 }, () => store,
|
||||
{ security: '安全' }, ...names.map(name => ({ name })))
|
||||
{ security: '安全' }, () => ({}), ...names.map(name => ({ name })))
|
||||
assert.deepEqual(page.settingsMenu.value.map(item => item.key), ['security', 'third-party', 'group', 'rule'])
|
||||
assert.equal(page.settingsMenu.value.find(item => item.key === 'third-party').label, '邮件客户端')
|
||||
assert.equal(page.activeSettings.value.label, '安全')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user