This commit is contained in:
parent
9f144c7448
commit
08b1f41350
@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/config"
|
||||
@ -20,8 +21,11 @@ import (
|
||||
|
||||
// 这个服务只负责 HTTP 到 HTTPS 的安全跳转,不承载业务会话。
|
||||
var httpServer *http.Server
|
||||
var httpServerMu sync.Mutex
|
||||
|
||||
func HttpStop() {
|
||||
httpServerMu.Lock()
|
||||
defer httpServerMu.Unlock()
|
||||
if httpServer != nil {
|
||||
if err := httpServer.Close(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.WithError(err).Warn("close HTTP server failed")
|
||||
@ -276,8 +280,29 @@ func allowRouteMethod(w http.ResponseWriter, r *http.Request, allowed ...string)
|
||||
}
|
||||
|
||||
func HttpStart() error {
|
||||
server, err := prepareHTTPServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return serveHTTPServer(server)
|
||||
}
|
||||
|
||||
func serveHTTPServer(server *http.Server) error {
|
||||
log.WithFields(log.Fields{"action": "启动 HTTP 监听", "object": server.Addr, "result": "started"}).Info("开始绑定监听端口")
|
||||
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("serve HTTP: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareHTTPServer() (*http.Server, error) {
|
||||
httpServerMu.Lock()
|
||||
defer httpServerMu.Unlock()
|
||||
if httpServer != nil {
|
||||
return nil, fmt.Errorf("HTTP server is already started")
|
||||
}
|
||||
if config.CurrentConfig() == nil && config.CurrentManager() == nil {
|
||||
return fmt.Errorf("runtime configuration is unavailable")
|
||||
return nil, fmt.Errorf("runtime configuration is unavailable")
|
||||
}
|
||||
cfg := config.CurrentHTTPConfig()
|
||||
httpPort := 80
|
||||
@ -287,7 +312,7 @@ func HttpStart() error {
|
||||
|
||||
handler, err := newHTTPHandler(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpServer = &http.Server{
|
||||
@ -299,11 +324,7 @@ func HttpStart() error {
|
||||
IdleTimeout: time.Second * 30,
|
||||
MaxHeaderBytes: 16 << 10,
|
||||
}
|
||||
log.WithFields(log.Fields{"action": "启动 HTTP 监听", "object": fmt.Sprintf(":%d", httpPort), "result": "started"}).Info("开始绑定监听端口")
|
||||
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("serve HTTP: %w", err)
|
||||
}
|
||||
return nil
|
||||
return httpServer, nil
|
||||
}
|
||||
|
||||
func newHTTPHandler(cfg config.HTTPConfig) (http.Handler, error) {
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/config"
|
||||
@ -32,6 +33,7 @@ import (
|
||||
var local embed.FS
|
||||
|
||||
var httpsServer *http.Server
|
||||
var httpsServerMu sync.Mutex
|
||||
|
||||
type nullWrite struct{}
|
||||
|
||||
@ -41,25 +43,53 @@ func (w *nullWrite) Write(p []byte) (int, error) {
|
||||
|
||||
// HttpsStart 启动仅承载业务的 HTTPS 服务。
|
||||
func HttpsStart() error {
|
||||
server, err := prepareHTTPSServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return serveHTTPSServer(server)
|
||||
}
|
||||
|
||||
func serveHTTPSServer(server *http.Server) error {
|
||||
if server == nil {
|
||||
return nil
|
||||
}
|
||||
log.WithFields(log.Fields{"action": "启动 HTTPS 监听", "object": server.Addr, "result": "started"}).Info("开始绑定监听端口")
|
||||
if err := server.ListenAndServeTLS("", ""); err != nil {
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
log.WithFields(log.Fields{"action": "停止 HTTPS 监听", "object": server.Addr, "result": "success"}).Info("监听已正常关闭")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("serve HTTPS on %s: %w", server.Addr, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareHTTPSServer() (*http.Server, error) {
|
||||
httpsServerMu.Lock()
|
||||
defer httpsServerMu.Unlock()
|
||||
if httpsServer != nil {
|
||||
return nil, fmt.Errorf("HTTPS server is already started")
|
||||
}
|
||||
legacyCfg := config.CurrentConfig()
|
||||
if legacyCfg == nil && config.CurrentManager() == nil {
|
||||
return fmt.Errorf("runtime configuration is unavailable")
|
||||
return nil, fmt.Errorf("runtime configuration is unavailable")
|
||||
}
|
||||
httpCfg := config.CurrentHTTPConfig()
|
||||
if httpCfg.TLSMode == config.HTTPSTLSModeProxy {
|
||||
log.WithFields(log.Fields{"action": "启动内置 HTTPS 监听", "result": "skipped"}).Info("HTTPS 由反向代理处理,跳过内置监听")
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
if err := router(mux); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
publicRecovery, err := newPublicRecoveryHandler()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if session.Instance == nil {
|
||||
return fmt.Errorf("session manager is unavailable")
|
||||
return nil, fmt.Errorf("session manager is unavailable")
|
||||
}
|
||||
|
||||
nullLog := olog.New(&nullWrite{}, "", olog.Ldate)
|
||||
@ -69,14 +99,14 @@ func HttpsStart() error {
|
||||
}
|
||||
|
||||
if config.DevelopmentMode && legacyCfg != nil && legacyCfg.HttpsEnabled == 2 {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
if legacyCfg == nil {
|
||||
return fmt.Errorf("legacy SSL configuration is unavailable")
|
||||
return nil, fmt.Errorf("legacy SSL configuration is unavailable")
|
||||
}
|
||||
tlsConfig, err := config.NewRuntimeTLSConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve HTTPS certificate: %w", err)
|
||||
return nil, fmt.Errorf("resolve HTTPS certificate: %w", err)
|
||||
}
|
||||
|
||||
httpsServer = &http.Server{
|
||||
@ -90,18 +120,12 @@ func HttpsStart() error {
|
||||
MaxHeaderBytes: 16 << 10,
|
||||
ErrorLog: nullLog,
|
||||
}
|
||||
log.WithFields(log.Fields{"action": "启动 HTTPS 监听", "object": httpsServer.Addr, "result": "started"}).Info("开始绑定监听端口")
|
||||
if err := httpsServer.ListenAndServeTLS("", ""); err != nil {
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
log.WithFields(log.Fields{"action": "停止 HTTPS 监听", "object": httpsServer.Addr, "result": "success"}).Info("监听已正常关闭")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("serve HTTPS on port %d: %w", httpsPort, err)
|
||||
}
|
||||
return nil
|
||||
return httpsServer, nil
|
||||
}
|
||||
|
||||
func HttpsStop() {
|
||||
httpsServerMu.Lock()
|
||||
defer httpsServerMu.Unlock()
|
||||
if httpsServer != nil {
|
||||
if err := httpsServer.Close(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.WithError(err).Warn("close HTTPS server failed")
|
||||
|
||||
16
backend/listen/http_server/listeners.go
Normal file
16
backend/listen/http_server/listeners.go
Normal file
@ -0,0 +1,16 @@
|
||||
package http_server
|
||||
|
||||
// PrepareWebServers 在提交监听任务前同步创建实例,防止停止后才发布新的监听器。
|
||||
func PrepareWebServers() (httpRun, httpsRun func() error, err error) {
|
||||
httpInstance, err := prepareHTTPServer()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
httpsInstance, err := prepareHTTPSServer()
|
||||
if err != nil {
|
||||
HttpStop()
|
||||
return nil, nil, err
|
||||
}
|
||||
return func() error { return serveHTTPServer(httpInstance) },
|
||||
func() error { return serveHTTPSServer(httpsInstance) }, nil
|
||||
}
|
||||
89
backend/listen/http_server/listeners_test.go
Normal file
89
backend/listen/http_server/listeners_test.go
Normal file
@ -0,0 +1,89 @@
|
||||
package http_server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/config"
|
||||
)
|
||||
|
||||
func TestWebListenersStopBeforeServe(t *testing.T) {
|
||||
certificateServer := httptest.NewTLSServer(http.NotFoundHandler())
|
||||
tlsConfig := certificateServer.TLS.Clone()
|
||||
certificateServer.Close()
|
||||
for _, secure := range []bool{false, true} {
|
||||
server := &http.Server{Addr: "127.0.0.1:0", TLSConfig: tlsConfig.Clone()}
|
||||
var stop func()
|
||||
var serve func(*http.Server) error
|
||||
if secure {
|
||||
httpsServer = server
|
||||
stop, serve = HttpsStop, serveHTTPSServer
|
||||
} else {
|
||||
httpServer = server
|
||||
stop, serve = HttpStop, serveHTTPServer
|
||||
}
|
||||
stop()
|
||||
if err := serve(server); err != nil {
|
||||
t.Fatalf("secure=%v: stopped listener returned %v", secure, err)
|
||||
}
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebListenersConcurrentStop(t *testing.T) {
|
||||
certificateServer := httptest.NewTLSServer(http.NotFoundHandler())
|
||||
certificates := append([]tls.Certificate(nil), certificateServer.TLS.Certificates...)
|
||||
certificateServer.Close()
|
||||
for _, secure := range []bool{false, true} {
|
||||
server := &http.Server{Addr: "127.0.0.1:0", TLSConfig: &tls.Config{Certificates: certificates}}
|
||||
var stop func()
|
||||
var serve func(*http.Server) error
|
||||
if secure {
|
||||
httpsServer = server
|
||||
stop, serve = HttpsStop, serveHTTPSServer
|
||||
} else {
|
||||
httpServer = server
|
||||
stop, serve = HttpStop, serveHTTPServer
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- serve(server) }()
|
||||
var workers sync.WaitGroup
|
||||
for range 10 {
|
||||
workers.Add(1)
|
||||
go func() { defer workers.Done(); stop() }()
|
||||
}
|
||||
workers.Wait()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("listener did not stop")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareWebServersClosesHTTPOnHTTPSFailure(t *testing.T) {
|
||||
oldConfig, oldManager, oldDevelopment := config.Instance, config.CurrentManager(), config.DevelopmentMode
|
||||
config.SetRuntimeManager(nil)
|
||||
config.Instance = &config.Config{WebDomain: "example.com", HttpsEnabled: 1}
|
||||
config.DevelopmentMode = false
|
||||
t.Cleanup(func() {
|
||||
HttpStop()
|
||||
HttpsStop()
|
||||
config.Instance = oldConfig
|
||||
config.SetRuntimeManager(oldManager)
|
||||
config.DevelopmentMode = oldDevelopment
|
||||
})
|
||||
if _, _, err := PrepareWebServers(); err == nil {
|
||||
t.Fatal("missing HTTPS dependencies were accepted")
|
||||
}
|
||||
if httpServer != nil {
|
||||
t.Fatal("HTTP listener was retained after HTTPS preparation failed")
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package http_server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@ -8,8 +9,50 @@ import (
|
||||
"testing"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/config"
|
||||
"git.pcux.cn/xuans/Smail/dto/response"
|
||||
)
|
||||
|
||||
func TestSetupAuthenticationStatusRequiresBootstrapAuthorization(t *testing.T) {
|
||||
const token = "0123456789abcdefghijklmnop"
|
||||
for _, endpoint := range []string{"/api/user/info", "/api/v1/user/info"} {
|
||||
t.Run(endpoint, func(t *testing.T) {
|
||||
security := withSetupSecurity(t, token)
|
||||
mux := http.NewServeMux()
|
||||
registerSetupAPIRoutes(mux, security, func(http.ResponseWriter, *http.Request) { t.Fatal("setup mutation invoked") })
|
||||
for _, scenario := range []struct {
|
||||
method, source, token string
|
||||
ready bool
|
||||
status int
|
||||
}{
|
||||
{http.MethodPost, "127.0.0.1:1234", token, false, http.StatusOK},
|
||||
{http.MethodPost, "127.0.0.1:1234", "", false, http.StatusForbidden},
|
||||
{http.MethodPost, "192.0.2.1:1234", token, false, http.StatusForbidden},
|
||||
{http.MethodGet, "127.0.0.1:1234", token, false, http.StatusMethodNotAllowed},
|
||||
{http.MethodPost, "127.0.0.1:1234", token, true, http.StatusForbidden},
|
||||
} {
|
||||
config.SetInitState(scenario.ready)
|
||||
request := httptest.NewRequest(scenario.method, endpoint, nil)
|
||||
request.RemoteAddr = scenario.source
|
||||
request.AddCookie(&http.Cookie{Name: setupCookieName, Value: scenario.token})
|
||||
recorder := httptest.NewRecorder()
|
||||
mux.ServeHTTP(recorder, request)
|
||||
if recorder.Code != scenario.status {
|
||||
t.Fatalf("status = %d, want %d", recorder.Code, scenario.status)
|
||||
}
|
||||
if recorder.Code == http.StatusOK {
|
||||
var result response.Response
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &result); err != nil || result.ErrorNo != response.NeedSetup {
|
||||
t.Fatalf("setup state response = %+v, error = %v", result, err)
|
||||
}
|
||||
if recorder.Header().Get("Cache-Control") != "no-store" {
|
||||
t.Fatal("setup state can be cached")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func withSetupSecurity(t *testing.T, token string) *setupSecurity {
|
||||
t.Helper()
|
||||
oldToken := os.Getenv("SMAIL_SETUP_TOKEN")
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"git.pcux.cn/xuans/Smail/config"
|
||||
"git.pcux.cn/xuans/Smail/controllers"
|
||||
"git.pcux.cn/xuans/Smail/dto/response"
|
||||
"git.pcux.cn/xuans/Smail/utils/ip"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cast"
|
||||
@ -89,6 +90,14 @@ func registerSetupAPIRoutes(mux *http.ServeMux, security *setupSecurity, next ht
|
||||
authorized := security.authorize(next)
|
||||
mux.HandleFunc("/api/setup", authorized)
|
||||
mux.HandleFunc("/api/v1/setup", authorized)
|
||||
// 安装阶段尚无业务会话,通过受保护的状态接口引导前端进入安装向导。
|
||||
status := security.authorize(setupOnlyPost(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
response.NewErrorResponse(response.NeedSetup, "请先完成初始化", "").FPrint(w)
|
||||
}))
|
||||
mux.HandleFunc("/api/user/info", status)
|
||||
mux.HandleFunc("/api/v1/user/info", status)
|
||||
}
|
||||
|
||||
func setupOnlyPost(next http.HandlerFunc) http.HandlerFunc {
|
||||
|
||||
@ -215,6 +215,11 @@ func Init(serverVersion string) error {
|
||||
}()
|
||||
}
|
||||
hooks.Init(serverVersion)
|
||||
httpRun, httpsRun, err := http_server.PrepareWebServers()
|
||||
if err != nil {
|
||||
stopServices()
|
||||
return fmt.Errorf("prepare web services: %w", err)
|
||||
}
|
||||
mailSecurity := config.CurrentMailSecurity()
|
||||
// SMTP 25 用于服务器间投递,始终保留并支持机会性 STARTTLS。
|
||||
startService(serviceErrors, "SMTP", smtp_server.Start)
|
||||
@ -225,8 +230,8 @@ func Init(serverVersion string) error {
|
||||
startService(serviceErrors, "SMTP STARTTLS", smtp_server.StartWithTLSNew)
|
||||
}
|
||||
// http server start
|
||||
startService(serviceErrors, "HTTPS", http_server.HttpsStart)
|
||||
startService(serviceErrors, "HTTP", http_server.HttpStart)
|
||||
startService(serviceErrors, "HTTPS", httpsRun)
|
||||
startService(serviceErrors, "HTTP", httpRun)
|
||||
|
||||
// 等待 HTTP 服务启动后再执行需要 HTTP 回调的 ACME 操作。
|
||||
if err := waitHTTPReady(serviceErrors); err != nil {
|
||||
|
||||
@ -29,6 +29,8 @@
|
||||
|
||||
## 验收说明
|
||||
|
||||
Web 功能使用 MCP 模拟键鼠测试;终端仅用于源码检查、构建、部署以及本机重置工具本身的交互,不调用业务 API 或编写功能测试脚本。保留现有测试业务库,不清空邮件、账号或外部邮箱绑定。
|
||||
Web 功能使用 MCP 模拟键鼠测试;终端用于源码检查、构建、部署、授权范围内的数据备份清理以及本机重置工具本身的交互,不调用业务 API 代替页面验收。2026 年 9 月 16 日用户已授权清理测试数据,本轮限定在 `yunx-test`,先备份所有测试数据库再清空,保留数据库文件并重新执行初始化。不得将此授权扩大到其他部署环境。
|
||||
|
||||
具体测试结果记录到 `test-log/日期/测试log`;收到验证码、页面提示请求已受理、构建成功均不单独等于密码恢复闭环完成。
|
||||
|
||||
本轮结果见《清库部署测试记录》。
|
||||
|
||||
55
docs/清库部署测试记录.md
Normal file
55
docs/清库部署测试记录.md
Normal file
@ -0,0 +1,55 @@
|
||||
# 清库部署测试记录
|
||||
|
||||
## 范围与状态
|
||||
|
||||
日期:2026 年 9 月 16 日。仅测试本机 `D:\Desktop\Smail\yunx-test`,使用正式构建包和嵌入前端,未启用开发模式或停用本机恢复通道。
|
||||
|
||||
- [✓] 备份三份 SQLite 数据库和配置,校验备份后清空数据库对象,保留数据库文件。
|
||||
- [✓] 完整构建、部署及部署指纹校验通过。
|
||||
- [x] 首轮通过浏览器完成数据库、超级管理员、域名、DNS 展示和手动证书初始化;正式业务服务未就绪。
|
||||
- [✓] 修复安装根地址认证探测失败,隔离浏览器直接访问根地址后自动进入中文安装向导。
|
||||
- [✓] 修复 HTTP、HTTPS 全局实例在停止期间被清空而导致的崩溃;启动任务发布前同步准备实例,关闭已准备但尚未监听的实例也能正常退出。
|
||||
- [✓] 全量自动回归与定向竞态检查通过。
|
||||
- [!] 正式登录、刷新会话、普通用户及管理员邮件恢复、旧密码与旧会话失效实机验收未完成。
|
||||
- [!] 超级管理员受保护本机恢复通道实机验收未完成。
|
||||
|
||||
## 实机证据
|
||||
|
||||
第一轮直接访问安装根地址出现“认证服务暂时不可用”。安装服务缺少 `/api/v1/user/info` 的初始化状态响应,前端未得到需要初始化的业务码。修复后,两个版本的认证探测路径均复用安装令牌和来源检查,只返回需要初始化状态,不执行账号或配置写入。
|
||||
|
||||
在全新隔离浏览器中访问 `http://127.0.0.1:5201/`,页面自动进入 `/#/setup?redirect=/`,显示中文六步安装向导,未再出现认证服务不可用。已查看页面快照和桌面截图;截图工具不允许写入当前工作区,因此截图仅保留在会话中,未声称落盘。
|
||||
|
||||
第一轮完成初始化后,Windows Named Pipe 在接受连接时返回 `Access is denied`,随后 HTTP 停止与启动并发导致空指针崩溃。最新构建重试相同权限环境后,保留明确权限错误并退出,未再发生空指针崩溃,监听端口已释放。
|
||||
|
||||
当前进程使用未提升的中等完整性令牌,管理员组仅用于拒绝检查。本机通道 ACL 仍限定 SYSTEM 与管理员,未放宽 ACL,未改成公开 HTTP 或 TCP 接口。
|
||||
|
||||
## 自动验证
|
||||
|
||||
- `go test ./listen/http_server ./res_init` 通过。
|
||||
- `node test/run.mjs all` 通过:前端 166 项、部署 4 项以及全部后端包。部分未变更包使用 Go 测试缓存,不能据此认定受权限限制的 Windows IPC 用例已实机执行。
|
||||
- `node test/run.mjs backend -race ./listen/http_server ./res_init` 通过。
|
||||
- 新增覆盖:HTTP 和 HTTPS 停止先于监听、多个停止任务并发、HTTPS 准备失败清理 HTTP,以及安装状态接口的有效令牌、缺少令牌、来源限制、方法限制和初始化完成后拒绝访问。
|
||||
- `git diff --check` 通过,仅存在仓库已有换行策略提示。
|
||||
|
||||
首次误将构建与回归并行调度,触发工作区锁且导致竞态测试读取不到正在移动的嵌入资源。已改用串行调度重跑并通过,此次失败不作为产品回归结论。
|
||||
|
||||
## 备份与日志
|
||||
|
||||
以下路径包含本地测试数据或配置,均为忽略的运行产物,不提交仓库:
|
||||
|
||||
- `test-log/live-20260916-1938/backup/`:清库前配置和数据库。
|
||||
- `test-log/live-20260916-1938/reset-partition-config/`:归档的旧分区配置。
|
||||
- `test-log/live-20260916-1938/before-retest/`:首轮初始化后、第二次清库前的配置和数据库。
|
||||
- `test-log/live-20260916-1938/fresh.stderr.log`:原始崩溃证据。
|
||||
- `test-log/live-20260916-1938/fixed.stdout.log` 与 `fixed.stderr.log`:修复后正式启动权限失败与退出证据。
|
||||
- `test-log/live-20260916-1938/setup-retest.stdout.log`:最新安装向导进程日志。
|
||||
- `test-log/run-2026-09-16T11-49-49-921Z-5240/`:成功的全量回归。
|
||||
- `test-log/run-2026-09-16T11-51-52-136Z-29980/`:成功的竞态检查。
|
||||
|
||||
第二次清库后保留安装向导运行于 `127.0.0.1:5201`,数据库为空、配置未初始化,证书与业务密钥未删除。没有将旧账号备份恢复到当前运行库。
|
||||
|
||||
## 后续条件
|
||||
|
||||
后续正式流程需要符合项目部署要求的 Windows 权限环境或受支持的 Linux 部署环境。当前会话无法提供已提升的 Windows 令牌,不通过绕过权限限制推进验收。
|
||||
|
||||
在具备权限的环境中重新完成初始化并确认服务稳定后,再验证登录、会话刷新和邮箱恢复。邮件恢复还需要确定实际可接收验证码的外部测试邮箱;验证码收到、恢复页面打开或接口返回成功均不单独构成完整验收。
|
||||
Loading…
Reference in New Issue
Block a user