Compare commits
4
Commits
4bdeb07a09
...
86dc10a371
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86dc10a371 | ||
|
|
27fccc2e2c | ||
|
|
857ba893fd | ||
|
|
5041a5bdd3 |
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"setup-worktree-windows": [
|
||||||
|
"go mod download",
|
||||||
|
"if (Test-Path web/package.json) { pnpm --dir web install }"
|
||||||
|
],
|
||||||
|
"setup-worktree-unix": [
|
||||||
|
"go mod download",
|
||||||
|
"if [ -f web/package.json ]; then pnpm --dir web install; fi"
|
||||||
|
],
|
||||||
|
"setup-worktree": [
|
||||||
|
"go mod download",
|
||||||
|
"pnpm --dir web install"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
/dist/
|
/dist/
|
||||||
/web/dist/
|
/web/dist/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- errcheck
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- unused
|
||||||
|
- misspell
|
||||||
|
- unconvert
|
||||||
|
settings:
|
||||||
|
govet:
|
||||||
|
enable:
|
||||||
|
- nilness
|
||||||
|
- shadow
|
||||||
|
exclusions:
|
||||||
|
paths:
|
||||||
|
- web/node_modules
|
||||||
|
- third_party
|
||||||
|
- builtin
|
||||||
|
- examples
|
||||||
|
|
||||||
|
issues:
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
vars:
|
||||||
|
BIN_DIR: bin
|
||||||
|
BIN_NAME: nixmsg
|
||||||
|
EMBED_TAG: embeddist
|
||||||
|
EXE: '{{if eq OS "windows"}}.exe{{end}}'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
'*':
|
||||||
|
taskfile: taskfiles/*.yml
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
default:
|
||||||
|
desc: 显示可用任务
|
||||||
|
cmds:
|
||||||
|
- task --list
|
||||||
|
|
||||||
|
web:install:
|
||||||
|
desc: 安装前端依赖
|
||||||
|
dir: web
|
||||||
|
cmds:
|
||||||
|
- pnpm install
|
||||||
|
|
||||||
|
web:dev:
|
||||||
|
desc: 启动前端开发服务器
|
||||||
|
dir: web
|
||||||
|
deps: [web:install]
|
||||||
|
cmds:
|
||||||
|
- pnpm dev
|
||||||
|
|
||||||
|
web:build:
|
||||||
|
desc: 构建前端静态资源到 web/dist
|
||||||
|
dir: web
|
||||||
|
deps: [web:install]
|
||||||
|
cmds:
|
||||||
|
- pnpm build
|
||||||
|
|
||||||
|
build:
|
||||||
|
desc: 构建嵌入前端的单个可执行文件
|
||||||
|
deps: [web:build]
|
||||||
|
cmds:
|
||||||
|
- go build -tags {{.EMBED_TAG}} -o {{.BIN_DIR}}/{{.BIN_NAME}}{{.EXE}} ./cmd/nixmsg
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
|
||||||
|
test:
|
||||||
|
desc: 运行单元测试
|
||||||
|
cmds:
|
||||||
|
- go test ./...
|
||||||
|
|
||||||
|
lint:
|
||||||
|
desc: 运行 golangci-lint
|
||||||
|
cmds:
|
||||||
|
- golangci-lint run ./...
|
||||||
|
|
||||||
|
check:
|
||||||
|
desc: 构建、检查与单元测试
|
||||||
|
cmds:
|
||||||
|
- task: build
|
||||||
|
- task: lint
|
||||||
|
- task: test
|
||||||
|
|
||||||
|
itest:
|
||||||
|
desc: 运行集成测试(T0.5 后可用)
|
||||||
|
cmds:
|
||||||
|
- go test ./test/... -count=1
|
||||||
|
|
||||||
|
docker:
|
||||||
|
desc: 构建开发用 Docker 镜像
|
||||||
|
cmds:
|
||||||
|
- docker build -t nixmsg:dev -f deploy/Dockerfile .
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Fprintln(os.Stderr, "usage: nixmsg <command>")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "version":
|
||||||
|
cmdVersion(os.Args[2:])
|
||||||
|
case "serve":
|
||||||
|
if err := cmdServe(os.Args[2:]); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/config"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/store"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
func cmdServe(_ []string) error {
|
||||||
|
cfgPath := config.PathFromEnv()
|
||||||
|
cfg, err := config.Load(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
return runServe(ctx, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServe(ctx context.Context, cfg config.Config) error {
|
||||||
|
wire()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(cfg.DataDir, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("mkdir data_dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := store.OpenWriter(cfg.DataDir, cfg.SQLiteSynchronous)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = db.Close() }()
|
||||||
|
|
||||||
|
if migErr := store.Migrate(db); migErr != nil {
|
||||||
|
return migErr
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
|
// 确保前端资源被链接进二进制;完整静态托管由后续任务完善。
|
||||||
|
_ = web.Dist()
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", cfg.Listen)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listen %s: %w", cfg.Listen, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeListenAddr(cfg.DataDir, ln.Addr().String()); err != nil {
|
||||||
|
_ = ln.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Handler: mux,
|
||||||
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
errCh <- srv.Serve(ln)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = srv.Shutdown(shutdownCtx)
|
||||||
|
serveErr := <-errCh
|
||||||
|
if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
|
||||||
|
return serveErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case serveErr := <-errCh:
|
||||||
|
if errors.Is(serveErr, http.ErrServerClosed) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return serveErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeListenAddr(dataDir, addr string) error {
|
||||||
|
path := filepath.Join(dataDir, "listen.addr")
|
||||||
|
return os.WriteFile(path, []byte(addr+"\n"), 0o644)
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServeHealthzAndListenAddr(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dataDir := t.TempDir()
|
||||||
|
cfgPath := filepath.Join(dataDir, "config.yaml")
|
||||||
|
cfgYAML := []byte("listen: \"127.0.0.1:0\"\ndata_dir: \"" + filepath.ToSlash(dataDir) + "\"\n")
|
||||||
|
if err := os.WriteFile(cfgPath, cfgYAML, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.Load(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
errCh <- runServe(ctx, cfg)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var addr string
|
||||||
|
deadline := time.Now().Add(5 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
b, readErr := os.ReadFile(filepath.Join(dataDir, "listen.addr"))
|
||||||
|
if readErr == nil {
|
||||||
|
addr = strings.TrimSpace(string(b))
|
||||||
|
if addr != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
}
|
||||||
|
if addr == "" {
|
||||||
|
t.Fatal("listen.addr not written")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get("http://" + addr + "/healthz")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("healthz: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("status=%d body=%s", resp.StatusCode, body)
|
||||||
|
}
|
||||||
|
if string(body) != "ok" {
|
||||||
|
t.Fatalf("unexpected body: %q", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(filepath.Join(dataDir, "nixmsg.db")); err != nil {
|
||||||
|
t.Fatalf("db missing: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("serve exit: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("serve did not stop")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version 由构建注入;未注入时显示开发占位。
|
||||||
|
var Version = "dev"
|
||||||
|
|
||||||
|
func cmdVersion(_ []string) {
|
||||||
|
fmt.Printf("nixmsg %s %s/%s\n", Version, runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// wire 组装各业务模块。T0.1 仅占位,T0.4 起由各线注册。
|
||||||
|
func wire() {}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# 多阶段骨架:完整多架构发布见 Q4。
|
||||||
|
# 语法阶段:Node 构建前端 → Go 编译(嵌入前端)→ distroless 运行。
|
||||||
|
|
||||||
|
FROM node:24-bookworm AS web
|
||||||
|
WORKDIR /src/web
|
||||||
|
COPY web/package.json web/pnpm-lock.yaml ./
|
||||||
|
RUN corepack enable && pnpm install --frozen-lockfile
|
||||||
|
COPY web/ ./
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM golang:1.27-bookworm AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
COPY --from=web /src/web/dist ./web/dist
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
RUN go build -tags embeddist -o /out/nixmsg ./cmd/nixmsg
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/static:nonroot
|
||||||
|
COPY --from=build /out/nixmsg /nixmsg
|
||||||
|
USER nonroot:nonroot
|
||||||
|
ENTRYPOINT ["/nixmsg"]
|
||||||
|
CMD ["serve"]
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
listen: ":7443" # 端接入端口
|
||||||
|
admin_listen: "" # 后台单独监听,如 "127.0.0.1:7444";空表示后台也在 listen 上
|
||||||
|
tls:
|
||||||
|
cert_file: ""
|
||||||
|
key_file: ""
|
||||||
|
allow_plaintext: false
|
||||||
|
trusted_proxies: [] # 反向代理的地址段,如 ["127.0.0.1/32"]
|
||||||
|
data_dir: "./data"
|
||||||
|
limits:
|
||||||
|
max_body_bytes: 262144 # 只能调小
|
||||||
|
max_meta_bytes: 4096
|
||||||
|
max_frame_bytes: 786432
|
||||||
|
max_ttl_seconds: 2592000
|
||||||
|
max_schedule_seconds: 31536000
|
||||||
|
max_group_members: 1000
|
||||||
|
grace_seconds: 60
|
||||||
|
ack_timeout_seconds: 300
|
||||||
|
delivery_window: 32
|
||||||
|
receipt_window: 64
|
||||||
|
requests_per_second: 50 # 除 ack、receipt_ack 外,突发容量为 2 倍
|
||||||
|
max_pending_per_sender: 10000 # 等待发送或投递中的消息数,群消息算一条;0 表示不限
|
||||||
|
max_pending_per_receiver: 10000 # 排队未收下的投递数;0 表示不限
|
||||||
|
session_idle_days: 30 # 会话令牌多少天没用就失效;0 表示不失效
|
||||||
|
record_retention_days: 7 # 0 表示完成后不留记录
|
||||||
|
idempotency_hours: 24
|
||||||
|
receipt_retention_days: 7
|
||||||
|
sqlite_synchronous: FULL # 或 NORMAL
|
||||||
|
metrics:
|
||||||
|
token: "" # 后台和端共用端口时,访问 /metrics 要带它;空表示共用端口时不提供
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
+36
-1
@@ -6,7 +6,42 @@
|
|||||||
|
|
||||||
## 总控 L
|
## 总控 L
|
||||||
|
|
||||||
暂无。
|
### T0.1 2026-09-30
|
||||||
|
|
||||||
|
1. **前端嵌入方式**
|
||||||
|
- 原条款:DEVELOPMENT 第 3 节 `web/embed.go` 用 `//go:embed all:dist`;TASKS T0.1;`.gitignore` 忽略 `web/dist`。
|
||||||
|
- 实际做法:默认构建用 `-tags` 以外的 `embed_stub.go` 嵌入 `web/stub/`;`task build` 加 `-tags embeddist` 嵌入真实 `web/dist`。
|
||||||
|
- 原因:`web/dist` 不进仓库,且要求没有前端产物时也能 `go build`。
|
||||||
|
- 备选方案:提交最小 `dist`;或构建前脚本生成占位目录。
|
||||||
|
- 影响:裸 `go build` 不含真实前端;正式产物必须走 `task build`。
|
||||||
|
|
||||||
|
2. **listen.addr 写入时机**
|
||||||
|
- 原条款:DEVELOPMENT 4.1「端口写 0 时」写 `<data_dir>/listen.addr`。
|
||||||
|
- 实际做法:只要 `serve` 启动成功就写入实际监听地址。
|
||||||
|
- 原因:测试启动器统一读取该文件更简单,固定端口场景也无害。
|
||||||
|
- 备选方案:仅当配置端口为 0 时写入。
|
||||||
|
- 影响:多一个小文件;行为超集,兼容文档要求。
|
||||||
|
|
||||||
|
3. **迁移占位**
|
||||||
|
- 原条款:T0.3 才写完整表与 `VACUUM INTO` 备份;T0.1 允许空执行器加空 0001。
|
||||||
|
- 实际做法:`internal/store.Migrate` 建 `schema_migrations` 并应用 `0001_init.sql`(内容为 `SELECT 1;`);不做迁移前备份。
|
||||||
|
- 原因:保证 serve 可跑通迁移路径,表结构留给 T0.3。
|
||||||
|
- 备选方案:完全空文件 + 只记版本。
|
||||||
|
- 影响:T0.3 需替换 0001 正文并补备份逻辑;已应用的占位版本号仍为 1。
|
||||||
|
|
||||||
|
4. **Taskfile 引入 taskfiles**
|
||||||
|
- 原条款:TASKS T0.1 / 4.2 引入 `taskfiles/*.yml`。
|
||||||
|
- 实际做法:`includes: '*': taskfile: taskfiles/*.yml, optional: true`,并放 `_init.yml` 占位。
|
||||||
|
- 原因:空目录 glob 可能失败;各线稍后加自己的 yml。
|
||||||
|
- 备选方案:主文件逐条 optional include 各线文件名。
|
||||||
|
- 影响:无。
|
||||||
|
|
||||||
|
5. **deploy/Dockerfile**
|
||||||
|
- 原条款:完整多架构镜像属 Q4;T0.1 需要 `task docker` 目标。
|
||||||
|
- 实际做法:提供单架构多阶段 Dockerfile 骨架,供 `task docker` 使用;T0.1 验证另用官方 `golang` 镜像跑 `task check`。
|
||||||
|
- 原因:让 docker 目标可执行,又不抢 Q4 范围。
|
||||||
|
- 备选方案:docker 目标仅 echo 提示。
|
||||||
|
- 影响:镜像发布流程仍由 Q4 定稿。
|
||||||
|
|
||||||
## 平台 P
|
## 平台 P
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
module git.asio.asia/nixevol/NixMsg
|
||||||
|
|
||||||
|
go 1.27
|
||||||
|
|
||||||
|
require (
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5
|
||||||
|
modernc.org/sqlite v1.60.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/sys v0.48.0 // indirect
|
||||||
|
modernc.org/libc v1.77.1 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.12.1 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
|
||||||
|
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||||
|
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||||
|
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||||
|
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||||
|
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||||
|
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
|
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||||
|
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
|
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
|
||||||
|
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
|
||||||
|
modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg=
|
||||||
|
modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
|
modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE=
|
||||||
|
modernc.org/ccgo/v4 v4.36.1/go.mod h1:rrtGc2QkS239nYb/mQNuBMyjq3/y3ZXWbBjPoV3wqzA=
|
||||||
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc=
|
||||||
|
modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs=
|
||||||
|
modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
|
||||||
|
modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||||
|
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw=
|
||||||
|
modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"go.yaml.in/yaml/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config 对应 DEVELOPMENT 第 11.1 节的配置文件。
|
||||||
|
type Config struct {
|
||||||
|
Listen string `yaml:"listen"`
|
||||||
|
AdminListen string `yaml:"admin_listen"`
|
||||||
|
TLS TLSConfig `yaml:"tls"`
|
||||||
|
TrustedProxies []string `yaml:"trusted_proxies"`
|
||||||
|
DataDir string `yaml:"data_dir"`
|
||||||
|
Limits LimitsConfig `yaml:"limits"`
|
||||||
|
SessionIdleDays int `yaml:"session_idle_days"`
|
||||||
|
RecordRetentionDays int `yaml:"record_retention_days"`
|
||||||
|
IdempotencyHours int `yaml:"idempotency_hours"`
|
||||||
|
ReceiptRetentionDays int `yaml:"receipt_retention_days"`
|
||||||
|
SQLiteSynchronous string `yaml:"sqlite_synchronous"`
|
||||||
|
Metrics MetricsConfig `yaml:"metrics"`
|
||||||
|
Log LogConfig `yaml:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TLSConfig struct {
|
||||||
|
CertFile string `yaml:"cert_file"`
|
||||||
|
KeyFile string `yaml:"key_file"`
|
||||||
|
AllowPlaintext bool `yaml:"allow_plaintext"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LimitsConfig struct {
|
||||||
|
MaxBodyBytes int `yaml:"max_body_bytes"`
|
||||||
|
MaxMetaBytes int `yaml:"max_meta_bytes"`
|
||||||
|
MaxFrameBytes int `yaml:"max_frame_bytes"`
|
||||||
|
MaxTTLSeconds int `yaml:"max_ttl_seconds"`
|
||||||
|
MaxScheduleSeconds int `yaml:"max_schedule_seconds"`
|
||||||
|
MaxGroupMembers int `yaml:"max_group_members"`
|
||||||
|
GraceSeconds int `yaml:"grace_seconds"`
|
||||||
|
AckTimeoutSeconds int `yaml:"ack_timeout_seconds"`
|
||||||
|
DeliveryWindow int `yaml:"delivery_window"`
|
||||||
|
ReceiptWindow int `yaml:"receipt_window"`
|
||||||
|
RequestsPerSecond int `yaml:"requests_per_second"`
|
||||||
|
MaxPendingPerSender int `yaml:"max_pending_per_sender"`
|
||||||
|
MaxPendingPerReceiver int `yaml:"max_pending_per_receiver"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricsConfig struct {
|
||||||
|
Token string `yaml:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogConfig struct {
|
||||||
|
Level string `yaml:"level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default 返回与 DEVELOPMENT 11.1 示例一致的默认值。
|
||||||
|
func Default() Config {
|
||||||
|
return Config{
|
||||||
|
Listen: ":7443",
|
||||||
|
AdminListen: "",
|
||||||
|
TLS: TLSConfig{
|
||||||
|
AllowPlaintext: false,
|
||||||
|
},
|
||||||
|
TrustedProxies: nil,
|
||||||
|
DataDir: "./data",
|
||||||
|
Limits: LimitsConfig{
|
||||||
|
MaxBodyBytes: 262144,
|
||||||
|
MaxMetaBytes: 4096,
|
||||||
|
MaxFrameBytes: 786432,
|
||||||
|
MaxTTLSeconds: 2592000,
|
||||||
|
MaxScheduleSeconds: 31536000,
|
||||||
|
MaxGroupMembers: 1000,
|
||||||
|
GraceSeconds: 60,
|
||||||
|
AckTimeoutSeconds: 300,
|
||||||
|
DeliveryWindow: 32,
|
||||||
|
ReceiptWindow: 64,
|
||||||
|
RequestsPerSecond: 50,
|
||||||
|
MaxPendingPerSender: 10000,
|
||||||
|
MaxPendingPerReceiver: 10000,
|
||||||
|
},
|
||||||
|
SessionIdleDays: 30,
|
||||||
|
RecordRetentionDays: 7,
|
||||||
|
IdempotencyHours: 24,
|
||||||
|
ReceiptRetentionDays: 7,
|
||||||
|
SQLiteSynchronous: "FULL",
|
||||||
|
Metrics: MetricsConfig{},
|
||||||
|
Log: LogConfig{
|
||||||
|
Level: "info",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load 从路径加载 YAML 配置;空字段保留默认值。
|
||||||
|
func Load(path string) (Config, error) {
|
||||||
|
cfg := Default()
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("read config: %w", err)
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return Config{}, fmt.Errorf("parse config: %w", err)
|
||||||
|
}
|
||||||
|
if cfg.Listen == "" {
|
||||||
|
cfg.Listen = Default().Listen
|
||||||
|
}
|
||||||
|
if cfg.DataDir == "" {
|
||||||
|
cfg.DataDir = Default().DataDir
|
||||||
|
}
|
||||||
|
if cfg.SQLiteSynchronous == "" {
|
||||||
|
cfg.SQLiteSynchronous = Default().SQLiteSynchronous
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathFromEnv 返回 NIXMSG_CONFIG 或默认 ./config.yaml。
|
||||||
|
func PathFromEnv() string {
|
||||||
|
if p := os.Getenv("NIXMSG_CONFIG"); p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
return "./config.yaml"
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenWriter 按 DEVELOPMENT 7.7 打开写连接(带 _txlock=immediate)。
|
||||||
|
func OpenWriter(dataDir, synchronous string) (*sql.DB, error) {
|
||||||
|
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
||||||
|
return nil, fmt.Errorf("mkdir data_dir: %w", err)
|
||||||
|
}
|
||||||
|
dsn, err := writeDSN(dataDir, synchronous)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
db, err := sql.Open("sqlite", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open sqlite: %w", err)
|
||||||
|
}
|
||||||
|
db.SetMaxOpenConns(1)
|
||||||
|
db.SetMaxIdleConns(1)
|
||||||
|
if err := db.Ping(); err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return nil, fmt.Errorf("ping sqlite: %w", err)
|
||||||
|
}
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeDSN(dataDir, synchronous string) (string, error) {
|
||||||
|
sync := strings.ToUpper(strings.TrimSpace(synchronous))
|
||||||
|
if sync == "" {
|
||||||
|
sync = "FULL"
|
||||||
|
}
|
||||||
|
if sync != "FULL" && sync != "NORMAL" {
|
||||||
|
return "", fmt.Errorf("invalid sqlite_synchronous: %s", synchronous)
|
||||||
|
}
|
||||||
|
dbPath := filepath.ToSlash(filepath.Join(dataDir, "nixmsg.db"))
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"file:%s?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=synchronous(%s)&_pragma=foreign_keys(ON)&_pragma=secure_delete(ON)&_txlock=immediate",
|
||||||
|
dbPath,
|
||||||
|
sync,
|
||||||
|
), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOpenAndMigrate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
dir := t.TempDir()
|
||||||
|
db, err := OpenWriter(dir, "FULL")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = db.Close() }()
|
||||||
|
|
||||||
|
if migErr := Migrate(db); migErr != nil {
|
||||||
|
t.Fatal(migErr)
|
||||||
|
}
|
||||||
|
if migErr := Migrate(db); migErr != nil {
|
||||||
|
t.Fatal(migErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int
|
||||||
|
if scanErr := db.QueryRow(`SELECT COUNT(*) FROM schema_migrations`).Scan(&n); scanErr != nil {
|
||||||
|
t.Fatal(scanErr)
|
||||||
|
}
|
||||||
|
if n != 1 {
|
||||||
|
t.Fatalf("want 1 migration row, got %d", n)
|
||||||
|
}
|
||||||
|
nested, openErr := OpenWriter(filepath.Join(dir, "nested"), "NORMAL")
|
||||||
|
if openErr != nil {
|
||||||
|
t.Fatal(openErr)
|
||||||
|
}
|
||||||
|
_ = nested.Close()
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed migrations/*.sql
|
||||||
|
var migrationFS embed.FS
|
||||||
|
|
||||||
|
// Migrate 应用尚未执行的迁移。T0.1 仅为空执行器加 0001 占位;完整备份与表结构见 T0.3。
|
||||||
|
func Migrate(db *sql.DB) error {
|
||||||
|
if _, err := db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||||
|
version INTEGER PRIMARY KEY,
|
||||||
|
applied_at INTEGER NOT NULL
|
||||||
|
)`); err != nil {
|
||||||
|
return fmt.Errorf("ensure schema_migrations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := fs.ReadDir(migrationFS, "migrations")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read migrations: %w", err)
|
||||||
|
}
|
||||||
|
var names []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".sql") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
names = append(names, e.Name())
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
version, err := parseMigrationVersion(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var exists int
|
||||||
|
err = db.QueryRow(`SELECT 1 FROM schema_migrations WHERE version = ?`, version).Scan(&exists)
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
return fmt.Errorf("check migration %d: %w", version, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := migrationFS.ReadFile("migrations/" + name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read migration %s: %w", name, err)
|
||||||
|
}
|
||||||
|
sqlText := strings.TrimSpace(string(body))
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("begin migration %d: %w", version, err)
|
||||||
|
}
|
||||||
|
if sqlText != "" {
|
||||||
|
if _, err := tx.Exec(sqlText); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return fmt.Errorf("apply migration %d: %w", version, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(
|
||||||
|
`INSERT INTO schema_migrations(version, applied_at) VALUES(?, ?)`,
|
||||||
|
version,
|
||||||
|
time.Now().UnixMilli(),
|
||||||
|
); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return fmt.Errorf("record migration %d: %w", version, err)
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("commit migration %d: %w", version, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMigrationVersion(name string) (int, error) {
|
||||||
|
base := strings.TrimSuffix(name, ".sql")
|
||||||
|
parts := strings.SplitN(base, "_", 2)
|
||||||
|
if len(parts) < 1 {
|
||||||
|
return 0, fmt.Errorf("invalid migration name: %s", name)
|
||||||
|
}
|
||||||
|
v, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid migration version in %s: %w", name, err)
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
SELECT 1;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
# 各开发线把自己的任务写到本目录 <线名>.yml(如 p.yml、n.yml)。
|
||||||
|
# 主 Taskfile 通过 includes 的 glob 引入;本文件仅占位,避免空目录无法匹配。
|
||||||
|
|
||||||
|
tasks: {}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
onlyBuiltDependencies[]=esbuild
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import "io/fs"
|
||||||
|
|
||||||
|
// Dist 返回嵌入的前端静态资源。
|
||||||
|
// 默认(无 embeddist 标签)为 stub 占位,保证裸 go build 可通过;
|
||||||
|
// task build 使用 -tags embeddist 嵌入 pnpm build 产物。
|
||||||
|
func Dist() fs.FS {
|
||||||
|
return distFS
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build embeddist
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed all:dist
|
||||||
|
var distFS embed.FS
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !embeddist
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed all:stub
|
||||||
|
var distFS embed.FS
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module "*.vue" {
|
||||||
|
import type { DefineComponent } from "vue";
|
||||||
|
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>NixMsg</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "nixmsg-web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"typecheck": "vue-tsc -b --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"naive-ui": "^2.43.1",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
|
"vue": "^3.5.22",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^26.6.3",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"typescript": "~5.9.2",
|
||||||
|
"vite": "^7.1.7",
|
||||||
|
"vue-tsc": "^3.0.8"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.17.1",
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": ["esbuild"]
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1207
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NConfigProvider, NMessageProvider, NDialogProvider, zhCN, dateZhCN } from "naive-ui";
|
||||||
|
import { RouterView } from "vue-router";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-config-provider :locale="zhCN" :date-locale="dateZhCN" size="small">
|
||||||
|
<n-message-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<RouterView />
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import { createPinia } from "pinia";
|
||||||
|
import naive from "naive-ui";
|
||||||
|
import App from "./App.vue";
|
||||||
|
import { router } from "./router";
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
app.use(createPinia());
|
||||||
|
app.use(router);
|
||||||
|
app.use(naive);
|
||||||
|
app.mount("#app");
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
|
import HomeView from "../views/HomeView.vue";
|
||||||
|
|
||||||
|
export const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "home",
|
||||||
|
component: HomeView,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
export const useAppStore = defineStore("app", () => {
|
||||||
|
const title = ref("NixMsg");
|
||||||
|
return { title };
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NCard, NSpace, NText } from "naive-ui";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
||||||
|
const { title } = storeToRefs(useAppStore());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-space vertical style="padding: 24px">
|
||||||
|
<n-card :title="title">
|
||||||
|
<n-text>管理后台骨架(T0.1)。后续页面由 W 线实现。</n-text>
|
||||||
|
</n-card>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>NixMsg</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>前端尚未构建。请先执行 <code>task web:build</code> 再 <code>task build</code>。</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "env.d.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(rootDir, "src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": "http://127.0.0.1:7443",
|
||||||
|
"/healthz": "http://127.0.0.1:7443",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user