5abc705eae
- 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>
129 lines
4.0 KiB
Go
129 lines
4.0 KiB
Go
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 | 实体关系跳跃 |
|
||
|
||
三路结果用 **RRF(Reciprocal 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
|
||
}
|