feat: 官网 + 管理端 + Gin/GORM 后端首个完整版本

- web/: 产品官网(Hero 事件流终端、功能矩阵、架构、快速开始、下载、博客 + Markdown 详情页),青瓷绿双主题
- admin/: 内容管理(JWT 登录、文章分页/搜索/CRUD、草稿与发布),embed 挂 /admin
- server/: Gin + GORM,MySQL(DSN 走 .env,SQLite 兜底);规范落地:sundynix_ 表前缀、字符串雪花主键、snake_case 列名、统一响应信封、公共分页参数
- 双 SPA embed 单二进制部署,make build 一键出包

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-17 11:03:31 +08:00
parent d46267dde1
commit 5abc705eae
53 changed files with 5774 additions and 38 deletions
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"log"
"os"
"strings"
"github.com/joho/godotenv"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/auth"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/idgen"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/router"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/store"
)
func main() {
// 依次尝试仓库根 / server 目录下的 .env(已存在的环境变量优先)
_ = godotenv.Load(".env")
_ = godotenv.Load("../.env")
// 未配置 SUNDYNIX_DB 时回落到本地 SQLite,方便零依赖起步
dsn := envOr("SUNDYNIX_DB", "sundynix-site.db")
// 8080 常被 agentix gateway 占用,site 默认 8090
addr := envOr("SUNDYNIX_ADDR", ":8090")
if err := idgen.Init(); err != nil {
log.Fatalf("初始化雪花节点失败: %v", err)
}
auth.Init()
db, err := store.Open(dsn)
if err != nil {
log.Fatalf("打开数据库失败: %v", err)
}
r, err := router.New(db)
if err != nil {
log.Fatalf("初始化路由失败: %v", err)
}
log.Printf("sundynix-site 启动于 %s (db=%s)", addr, maskDSN(dsn))
if err := r.Run(addr); err != nil {
log.Fatalf("服务退出: %v", err)
}
}
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
// maskDSN 日志脱敏:user:pass@tcp(...) → user:***@tcp(...)
func maskDSN(dsn string) string {
at := strings.Index(dsn, "@")
if at < 0 {
return dsn
}
colon := strings.Index(dsn[:at], ":")
if colon < 0 {
return dsn
}
return dsn[:colon+1] + "***" + dsn[at:]
}
+57
View File
@@ -0,0 +1,57 @@
module git.sundynix.cn/Blizzard/sundynix-site/server
go 1.26.4
require (
github.com/bwmarrin/snowflake v0.3.0
github.com/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/joho/godotenv v1.5.1
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.2
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
)
+132
View File
@@ -0,0 +1,132 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
+58
View File
@@ -0,0 +1,58 @@
package handler
import (
"crypto/subtle"
"log"
"os"
"github.com/gin-gonic/gin"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/auth"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/resp"
)
// AdminAuthHandler 管理端登录。
// 账号密码走环境变量 SUNDYNIX_ADMIN_USER / SUNDYNIX_ADMIN_PASS。
type AdminAuthHandler struct {
username string
password string
}
func NewAdminAuthHandler() *AdminAuthHandler {
u := os.Getenv("SUNDYNIX_ADMIN_USER")
p := os.Getenv("SUNDYNIX_ADMIN_PASS")
if u == "" {
u = "admin"
}
if p == "" {
p = "admin123"
log.Println("[WARN] SUNDYNIX_ADMIN_PASS 未设置,使用开发默认密码 admin123,生产环境务必配置")
}
return &AdminAuthHandler{username: u, password: p}
}
type loginReq struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// Login POST /api/admin/login
func (h *AdminAuthHandler) Login(c *gin.Context) {
var body loginReq
if err := c.ShouldBindJSON(&body); err != nil {
resp.BadRequest(c, "用户名和密码不能为空")
return
}
userOK := subtle.ConstantTimeCompare([]byte(body.Username), []byte(h.username)) == 1
passOK := subtle.ConstantTimeCompare([]byte(body.Password), []byte(h.password)) == 1
if !userOK || !passOK {
resp.Unauthorized(c, "用户名或密码错误")
return
}
token, err := auth.Sign(body.Username)
if err != nil {
resp.ServerError(c, "签发 token 失败")
return
}
resp.OK(c, gin.H{"token": token, "username": body.Username})
}
+158
View File
@@ -0,0 +1,158 @@
package handler
import (
"errors"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/model"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/req"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/resp"
)
// AdminPostHandler 管理端文章 CRUD(含草稿)。
type AdminPostHandler struct {
db *gorm.DB
}
func NewAdminPostHandler(db *gorm.DB) *AdminPostHandler {
return &AdminPostHandler{db: db}
}
type postForm struct {
Slug string `json:"slug" binding:"required"`
Title string `json:"title" binding:"required"`
Category string `json:"category"`
Summary string `json:"summary"`
Content string `json:"content"`
Published bool `json:"published"`
}
// List GET /api/admin/posts — 分页 + 关键词,含草稿
func (h *AdminPostHandler) List(c *gin.Context) {
var q req.PageQuery
if err := c.ShouldBindQuery(&q); err != nil {
resp.BadRequest(c, "分页参数不合法")
return
}
q.Normalize()
tx := h.db.Model(&model.Post{})
if q.Keyword != "" {
kw := "%" + q.Keyword + "%"
tx = tx.Where("title LIKE ? OR slug LIKE ? OR category LIKE ?", kw, kw, kw)
}
var total int64
if err := tx.Count(&total).Error; err != nil {
resp.ServerError(c, "查询失败")
return
}
var items []model.PostListItem
err := tx.Order("created_at DESC").
Offset(q.Offset()).Limit(q.PageSize).
Find(&items).Error
if err != nil {
resp.ServerError(c, "查询失败")
return
}
resp.Page(c, items, total, q.Page, q.PageSize)
}
// Get GET /api/admin/posts/:id
func (h *AdminPostHandler) Get(c *gin.Context) {
var post model.Post
err := h.db.First(&post, "id = ?", c.Param("id")).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
resp.NotFound(c, "文章不存在")
return
}
if err != nil {
resp.ServerError(c, "查询失败")
return
}
resp.OK(c, post)
}
// Create POST /api/admin/posts
func (h *AdminPostHandler) Create(c *gin.Context) {
var form postForm
if err := c.ShouldBindJSON(&form); err != nil {
resp.BadRequest(c, "slug 和标题不能为空")
return
}
post := model.Post{
Slug: form.Slug,
Title: form.Title,
Category: form.Category,
Summary: form.Summary,
Content: form.Content,
}
if form.Published {
now := time.Now()
post.PublishedAt = &now
}
if err := h.db.Create(&post).Error; err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
resp.BadRequest(c, "slug 已存在")
return
}
resp.ServerError(c, "创建失败:"+err.Error())
return
}
resp.OK(c, post)
}
// Update PUT /api/admin/posts/:id
func (h *AdminPostHandler) Update(c *gin.Context) {
var form postForm
if err := c.ShouldBindJSON(&form); err != nil {
resp.BadRequest(c, "slug 和标题不能为空")
return
}
var post model.Post
err := h.db.First(&post, "id = ?", c.Param("id")).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
resp.NotFound(c, "文章不存在")
return
}
if err != nil {
resp.ServerError(c, "查询失败")
return
}
post.Slug = form.Slug
post.Title = form.Title
post.Category = form.Category
post.Summary = form.Summary
post.Content = form.Content
// 发布状态切换:首次发布记时间,撤回清空,重复发布保留原时间
if form.Published && post.PublishedAt == nil {
now := time.Now()
post.PublishedAt = &now
} else if !form.Published {
post.PublishedAt = nil
}
if err := h.db.Save(&post).Error; err != nil {
resp.ServerError(c, "更新失败:"+err.Error())
return
}
resp.OK(c, post)
}
// Delete DELETE /api/admin/posts/:id
func (h *AdminPostHandler) Delete(c *gin.Context) {
res := h.db.Delete(&model.Post{}, "id = ?", c.Param("id"))
if res.Error != nil {
resp.ServerError(c, "删除失败")
return
}
if res.RowsAffected == 0 {
resp.NotFound(c, "文章不存在")
return
}
resp.OK(c, nil)
}
+51
View File
@@ -0,0 +1,51 @@
package handler
import (
"errors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/model"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/resp"
)
// PostHandler 面向用户端的公开文章接口。
type PostHandler struct {
db *gorm.DB
}
func NewPostHandler(db *gorm.DB) *PostHandler {
return &PostHandler{db: db}
}
// List GET /api/posts — 已发布文章列表(不含正文)
func (h *PostHandler) List(c *gin.Context) {
var items []model.PostListItem
err := h.db.Model(&model.Post{}).
Where("published_at IS NOT NULL").
Order("published_at DESC").
Find(&items).Error
if err != nil {
resp.ServerError(c, "查询失败")
return
}
resp.OK(c, items)
}
// Get GET /api/posts/:slug — 文章详情(含 Markdown 正文)
func (h *PostHandler) Get(c *gin.Context) {
var post model.Post
err := h.db.
Where("slug = ? AND published_at IS NOT NULL", c.Param("slug")).
First(&post).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
resp.NotFound(c, "文章不存在")
return
}
if err != nil {
resp.ServerError(c, "查询失败")
return
}
resp.OK(c, post)
}
+29
View File
@@ -0,0 +1,29 @@
package middleware
import (
"strings"
"github.com/gin-gonic/gin"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/auth"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/resp"
)
// Auth 校验 Authorization: Bearer <token>,通过后把用户名放进上下文。
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
h := c.GetHeader("Authorization")
token, ok := strings.CutPrefix(h, "Bearer ")
if !ok || token == "" {
resp.Unauthorized(c, "未登录")
return
}
username, err := auth.Parse(token)
if err != nil {
resp.Unauthorized(c, err.Error())
return
}
c.Set("username", username)
c.Next()
}
}
+24
View File
@@ -0,0 +1,24 @@
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
)
// CORS 开发期跨域放行;生产同源部署(embed)时不会触发预检。
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
if origin != "" {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
}
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
+25
View File
@@ -0,0 +1,25 @@
package model
import (
"time"
"gorm.io/gorm"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/idgen"
)
// BaseModel 所有表的公共字段:字符串雪花主键 + 时间戳。
// 列名由 GORM NamingStrategy 统一转 snake_case。
type BaseModel struct {
ID string `gorm:"primaryKey;size:20" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// BeforeCreate 主键为空时自动生成雪花 ID。
func (m *BaseModel) BeforeCreate(*gorm.DB) error {
if m.ID == "" {
m.ID = idgen.Next()
}
return nil
}
+27
View File
@@ -0,0 +1,27 @@
package model
import "time"
// Post 博客文章,表名 sundynix_postNamingStrategy 统一加前缀)。
// Content 为 Markdown 原文,渲染交给前端;PublishedAt 为空即草稿。
type Post struct {
BaseModel
Slug string `gorm:"uniqueIndex;size:128" json:"slug"`
Title string `gorm:"size:256" json:"title"`
Category string `gorm:"size:64;index" json:"category"`
Summary string `gorm:"size:512" json:"summary"`
Content string `gorm:"type:text" json:"content,omitempty"`
PublishedAt *time.Time `gorm:"index" json:"published_at"`
}
// PostListItem 列表项投影,不带正文。
type PostListItem struct {
ID string `json:"id"`
Slug string `json:"slug"`
Title string `json:"title"`
Category string `json:"category"`
Summary string `json:"summary"`
PublishedAt *time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+52
View File
@@ -0,0 +1,52 @@
// Package auth JWT 签发与校验。
package auth
import (
"errors"
"log"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
)
var secret []byte
// Init 读取 JWT 密钥;未配置时使用开发默认值并告警。
func Init() {
s := os.Getenv("SUNDYNIX_JWT_SECRET")
if s == "" {
s = "sundynix-dev-secret-change-me"
log.Println("[WARN] SUNDYNIX_JWT_SECRET 未设置,使用开发默认密钥,生产环境务必配置")
}
secret = []byte(s)
}
// Sign 为用户名签发 24h 有效期的 token。
func Sign(username string) (string, error) {
claims := jwt.RegisteredClaims{
Subject: username,
IssuedAt: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
Issuer: "sundynix-site",
}
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secret)
}
// Parse 校验 token 并返回用户名。
func Parse(token string) (string, error) {
t, err := jwt.ParseWithClaims(token, &jwt.RegisteredClaims{}, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("非法签名算法")
}
return secret, nil
})
if err != nil || !t.Valid {
return "", errors.New("token 无效或已过期")
}
claims, ok := t.Claims.(*jwt.RegisteredClaims)
if !ok {
return "", errors.New("token 载荷异常")
}
return claims.Subject, nil
}
+34
View File
@@ -0,0 +1,34 @@
// Package idgen 全局雪花 ID 生成器,主键统一用字符串形式。
package idgen
import (
"os"
"strconv"
"github.com/bwmarrin/snowflake"
)
var node *snowflake.Node
// Init 初始化雪花节点;nodeID 取 SUNDYNIX_NODE_ID,默认 1。
func Init() error {
nodeID := int64(1)
if v := os.Getenv("SUNDYNIX_NODE_ID"); v != "" {
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return err
}
nodeID = n
}
n, err := snowflake.NewNode(nodeID)
if err != nil {
return err
}
node = n
return nil
}
// Next 生成一个字符串雪花 ID。
func Next() string {
return node.Generate().String()
}
+27
View File
@@ -0,0 +1,27 @@
// Package req 公共请求参数。
package req
// PageQuery 分页 + 关键词,所有列表接口通用。
type PageQuery struct {
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=10"`
Keyword string `form:"keyword"`
}
// Normalize 约束分页边界。
func (q *PageQuery) Normalize() {
if q.Page < 1 {
q.Page = 1
}
if q.PageSize < 1 {
q.PageSize = 10
}
if q.PageSize > 100 {
q.PageSize = 100
}
}
// Offset 计算偏移量。
func (q *PageQuery) Offset() int {
return (q.Page - 1) * q.PageSize
}
+55
View File
@@ -0,0 +1,55 @@
// Package resp 统一结果响应:{code, message, data}。
// code = 0 表示成功,非 0 为业务错误码。
package resp
import (
"net/http"
"github.com/gin-gonic/gin"
)
const (
CodeOK = 0
CodeBadRequest = 40000
CodeUnauthorized = 40100
CodeNotFound = 40400
CodeServerError = 50000
)
type Body struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
// PageResult 分页数据统一结构。
type PageResult struct {
List any `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
func OK(c *gin.Context, data any) {
c.JSON(http.StatusOK, Body{Code: CodeOK, Message: "ok", Data: data})
}
func Page(c *gin.Context, list any, total int64, page, pageSize int) {
OK(c, PageResult{List: list, Total: total, Page: page, PageSize: pageSize})
}
func BadRequest(c *gin.Context, message string) {
c.JSON(http.StatusBadRequest, Body{Code: CodeBadRequest, Message: message})
}
func Unauthorized(c *gin.Context, message string) {
c.AbortWithStatusJSON(http.StatusUnauthorized, Body{Code: CodeUnauthorized, Message: message})
}
func NotFound(c *gin.Context, message string) {
c.JSON(http.StatusNotFound, Body{Code: CodeNotFound, Message: message})
}
func ServerError(c *gin.Context, message string) {
c.JSON(http.StatusInternalServerError, Body{Code: CodeServerError, Message: message})
}
+86
View File
@@ -0,0 +1,86 @@
package router
import (
"io/fs"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/handler"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/middleware"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/resp"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/webfs"
)
func New(db *gorm.DB) (*gin.Engine, error) {
r := gin.New()
r.Use(gin.Logger(), gin.Recovery(), middleware.CORS())
// ── 公开 API ──
api := r.Group("/api")
{
api.GET("/healthz", func(c *gin.Context) {
resp.OK(c, gin.H{"status": "ok"})
})
posts := handler.NewPostHandler(db)
api.GET("/posts", posts.List)
api.GET("/posts/:slug", posts.Get)
}
// ── 管理端 API ──
adminAuth := handler.NewAdminAuthHandler()
api.POST("/admin/login", adminAuth.Login)
adminAPI := api.Group("/admin", middleware.Auth())
{
posts := handler.NewAdminPostHandler(db)
adminAPI.GET("/posts", posts.List)
adminAPI.POST("/posts", posts.Create)
adminAPI.GET("/posts/:id", posts.Get)
adminAPI.PUT("/posts/:id", posts.Update)
adminAPI.DELETE("/posts/:id", posts.Delete)
}
// ── 静态站点:/admin → 管理端,其余 → 用户端 ──
webDist, err := webfs.WebDist()
if err != nil {
return nil, err
}
adminDist, err := webfs.AdminDist()
if err != nil {
return nil, err
}
webServer := http.FileServer(http.FS(webDist))
adminServer := http.StripPrefix("/admin", http.FileServer(http.FS(adminDist)))
r.NoRoute(func(c *gin.Context) {
path := c.Request.URL.Path
if strings.HasPrefix(path, "/api/") {
resp.NotFound(c, "接口不存在")
return
}
if path == "/admin" || strings.HasPrefix(path, "/admin/") {
serveSPA(c, adminDist, adminServer, strings.TrimPrefix(path, "/admin"), "/admin/")
return
}
serveSPA(c, webDist, webServer, path, "/")
})
return r, nil
}
// serveSPA 真实文件直接吐,否则回退 index.html 交给前端路由。
func serveSPA(c *gin.Context, dist fs.FS, server http.Handler, rel, fallback string) {
p := strings.TrimPrefix(rel, "/")
if p != "" {
if _, err := fs.Stat(dist, p); err == nil {
server.ServeHTTP(c.Writer, c.Request)
return
}
}
c.Request.URL.Path = fallback
server.ServeHTTP(c.Writer, c.Request)
}
+128
View File
@@ -0,0 +1,128 @@
package store
import (
"strings"
"time"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/model"
)
// Open 打开数据库并完成迁移与种子数据。
// DSN 含 "@tcp(" 走 MySQL,否则按 SQLite 文件路径处理(本地开发兜底)。
// 命名规范:表前缀 sundynix_、单数表名、snake_case 列名。
func Open(dsn string) (*gorm.DB, error) {
var dialector gorm.Dialector
if strings.Contains(dsn, "@tcp(") {
dialector = mysql.Open(dsn)
} else {
dialector = sqlite.Open(dsn)
}
db, err := gorm.Open(dialector, &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
TranslateError: true, // 唯一索引冲突 → gorm.ErrDuplicatedKey
NamingStrategy: schema.NamingStrategy{
TablePrefix: "sundynix_",
SingularTable: true,
},
})
if err != nil {
return nil, err
}
if err := db.AutoMigrate(&model.Post{}); err != nil {
return nil, err
}
if err := seed(db); err != nil {
return nil, err
}
return db, nil
}
// seed 空库时写入示例文章,方便前端联调。
func seed(db *gorm.DB) error {
var count int64
if err := db.Model(&model.Post{}).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
at := func(s string) *time.Time {
t, _ := time.Parse("2006-01-02", s)
return &t
}
posts := []model.Post{
{
Slug: "v0-1-2-release",
Title: "sundynix agentix v0.1.2 发布:应用内更新与团队视图",
Category: "RELEASE",
Summary: "v0.1.2 带来应用内更新横幅、卡通办公室团队视图,以及发布流水线自动出包。",
PublishedAt: at("2026-07-02"),
Content: `## 亮点
- **应用内更新**:新版本发布后,工作台顶部出现更新横幅,一键升级。
- **团队视图**:多智能体协作时,AI 角色会在卡通办公室里走动干活。
- **发布流水线**GitHub Actions 自动产出 macOS universal .app 与 Windows .exe。
## 升级方式
桌面版直接点更新横幅;自托管用户:
` + "```bash\ndocker compose pull && docker compose up -d\n```" + `
完整变更见 Releases 页面。`,
},
{
Slug: "hybrid-retrieval",
Title: "三路混合检索是怎么工作的:vector + fulltext + graph",
Category: "ENGINEERING",
Summary: "向量召回语义、全文召回关键词、图谱召回关系,RRF 把三路结果融成一路。",
PublishedAt: at("2026-06-18"),
Content: `## 为什么一路不够
单靠向量检索,专有名词和精确匹配经常翻车;单靠全文检索,又抓不到语义近邻。
我们的做法是三路并发:
| 通路 | 引擎 | 擅长 |
|------|------|------|
| 向量 | Milvus | 语义相似 |
| 全文 | Bleve | 关键词精确匹配 |
| 图谱 | Neo4j | 实体关系跳跃 |
三路结果用 **RRFReciprocal Rank Fusion** 融合,再过一遍 rerank。
## 调试
检索控制台会展示每一路的召回与得分,坏 case 一眼定位。`,
},
{
Slug: "why-event-driven",
Title: "为什么我们选择事件驱动:NATS 零拷贝骨干网设计记",
Category: "ARCHITECTURE",
Summary: "Agent 的一切都是流:token、执行轨迹、工具调用。事件总线是最自然的骨架。",
PublishedAt: at("2026-05-30"),
Content: `## 流式优先
LLM 的输出天生是 token 流,Agent 的执行天生是事件序列。与其在 HTTP 请求-响应模型上硬凑,不如让整个系统跑在消息总线上。
## 主题设计
` + "```\nsundynix.tasks.* # 任务派发\nsundynix.streams.<id> # token 流\nsundynix.tools.go.* # Go 工具调用\nsundynix.tools.py.* # Python 工具调用\n```" + `
网关订阅流主题直接转 SSE/WS,中间零拷贝。
## 演进
单体先行(Monolith First):现在所有模块跑在一个进程里,但彼此只通过总线说话——拆微服务时只需要把订阅者搬走(Morph B)。`,
},
}
return db.Create(&posts).Error
}
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/admin/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
<title>sundynix admin — 内容管理</title>
<script>
// 避免主题闪烁:React 挂载前先落 dark class
;(function () {
var t = localStorage.getItem('sundynix-theme')
var dark =
t === 'dark' ||
((t === null || t === 'system') &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
if (dark) document.documentElement.classList.add('dark')
})()
</script>
<script type="module" crossorigin src="/admin/assets/index-B2HotWD6.js"></script>
<link rel="stylesheet" crossorigin href="/admin/assets/index-BFElCR1f.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="sundynix agentix — 事件驱动的 AI Agent 工作台。画布编排智能体,多 Agent 团队研究、检索、生成真正的 Word 报告。"
/>
<title>sundynix agentix — 事件驱动的 AI Agent 工作台</title>
<script>
// 避免主题闪烁:React 挂载前先落 dark class
;(function () {
var t = localStorage.getItem('sundynix-theme')
var dark =
t === 'dark' ||
((t === null || t === 'system') &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
if (dark) document.documentElement.classList.add('dark')
})()
</script>
<script type="module" crossorigin src="/assets/index-BpINEBaL.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-nl4Y73rF.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
// Package webfs 把前端构建产物打进二进制。
// dist/(用户端)与 admin_dist/(管理端)默认只有占位页;
// make build 会先构建 web/ 和 admin/ 并拷贝产物到这里,再编译 Go。
package webfs
import (
"embed"
"io/fs"
)
//go:embed all:dist
var embeddedWeb embed.FS
//go:embed all:admin_dist
var embeddedAdmin embed.FS
// WebDist 用户端静态文件系统(挂 /)。
func WebDist() (fs.FS, error) {
return fs.Sub(embeddedWeb, "dist")
}
// AdminDist 管理端静态文件系统(挂 /admin)。
func AdminDist() (fs.FS, error) {
return fs.Sub(embeddedAdmin, "admin_dist")
}