init push

This commit is contained in:
2026-05-21 19:52:45 +08:00
commit e3f75311ab
1280 changed files with 179173 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
package fns
import (
"context"
"fmt"
"github.com/samber/lo"
"gorm.io/gorm"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/log"
"github.com/chaitin/panda-wiki/repo/mq"
"github.com/chaitin/panda-wiki/usecase"
)
type MigrationNodeVersion struct {
Name string
logger *log.Logger
nodeUsecase *usecase.NodeUsecase
kbUsecase *usecase.KnowledgeBaseUsecase
ragRepo *mq.RAGRepository
}
func NewMigrationNodeVersion(logger *log.Logger, nodeUsecase *usecase.NodeUsecase, kbUsecase *usecase.KnowledgeBaseUsecase, ragRepo *mq.RAGRepository) *MigrationNodeVersion {
return &MigrationNodeVersion{
Name: "0001_migrate_node_version",
logger: logger,
nodeUsecase: nodeUsecase,
kbUsecase: kbUsecase,
ragRepo: ragRepo,
}
}
func (m *MigrationNodeVersion) Execute(tx *gorm.DB) error {
ctx := context.Background()
// 1. create kb release for all kb
kbList, err := m.kbUsecase.GetKnowledgeBaseList(ctx)
if err != nil {
return fmt.Errorf("get kb list failed: %w", err)
}
for _, kb := range kbList {
nodes, err := m.nodeUsecase.GetList(ctx, &domain.GetNodeListReq{
KBID: kb.ID,
})
if err != nil {
return fmt.Errorf("get node list failed: %w", err)
}
nodeIDs := lo.Map(nodes, func(node *domain.NodeListItemResp, _ int) string {
return node.ID
})
releaseID, err := m.kbUsecase.CreateKBRelease(ctx, &domain.CreateKBReleaseReq{
KBID: kb.ID,
Message: "release all old nodes",
Tag: "init",
NodeIDs: nodeIDs,
}, "")
if err != nil {
return fmt.Errorf("create kb release failed: %w", err)
}
m.logger.Info("create kb release success", log.String("kb_id", kb.ID), log.String("release_id", releaseID))
}
// 2. get all old node doc ids and delete in rag service
var nodes []domain.Node
if err := tx.Model(&domain.Node{}).
Select("id", "kb_id", "doc_id").
Find(&nodes).Error; err != nil {
return fmt.Errorf("get node doc ids failed: %w", err)
}
if len(nodes) > 0 {
nodeReleaseVectorRequests := make([]*domain.NodeReleaseVectorRequest, 0)
for _, node := range nodes {
if node.DocID == "" {
continue
}
nodeReleaseVectorRequests = append(nodeReleaseVectorRequests, &domain.NodeReleaseVectorRequest{
KBID: node.KBID,
DocID: node.DocID,
Action: "delete",
})
}
if len(nodeReleaseVectorRequests) > 0 {
if err := m.ragRepo.AsyncUpdateNodeReleaseVector(ctx, nodeReleaseVectorRequests); err != nil {
return fmt.Errorf("delete node release vector failed: %w", err)
}
}
}
return nil
}

View File

@@ -0,0 +1,115 @@
package fns
import (
"context"
"fmt"
"time"
"gorm.io/gorm"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/log"
)
type MigrationCreateBotAuth struct {
Name string
logger *log.Logger
}
func NewMigrationCreateBotAuth(logger *log.Logger) *MigrationCreateBotAuth {
return &MigrationCreateBotAuth{
Name: "0002_create_bot_auth",
logger: logger,
}
}
func (m *MigrationCreateBotAuth) Execute(tx *gorm.DB) error {
ctx := context.Background()
// 获取所有机器人类型的应用
var apps []domain.App
if err := tx.WithContext(ctx).Where("type IN ?", []domain.AppType{
domain.AppTypeWidget,
domain.AppTypeDingTalkBot,
domain.AppTypeFeishuBot,
domain.AppTypeWechatBot,
domain.AppTypeWechatServiceBot,
domain.AppTypeDisCordBot,
domain.AppTypeWechatOfficialAccount,
}).Find(&apps).Error; err != nil {
return fmt.Errorf("failed to get apps: %w", err)
}
m.logger.Info("found apps for bot auth creation", log.Int("count", len(apps)))
for _, app := range apps {
sourceType := app.Type.ToSourceType()
if sourceType == "" {
m.logger.Warn("app type has no corresponding source type", log.String("app_id", app.ID), log.Any("app_type", uint8(app.Type)))
continue
}
// 检查是否需要创建认证记录(检查应用是否启用)
shouldCreateAuth := false
switch app.Type {
case domain.AppTypeWidget:
shouldCreateAuth = app.Settings.WidgetBotSettings.IsOpen
case domain.AppTypeDingTalkBot:
shouldCreateAuth = app.Settings.DingTalkBotIsEnabled != nil && *app.Settings.DingTalkBotIsEnabled
case domain.AppTypeFeishuBot:
shouldCreateAuth = app.Settings.FeishuBotIsEnabled != nil && *app.Settings.FeishuBotIsEnabled
case domain.AppTypeWechatBot:
shouldCreateAuth = app.Settings.WeChatAppIsEnabled != nil && *app.Settings.WeChatAppIsEnabled
case domain.AppTypeWechatServiceBot:
shouldCreateAuth = app.Settings.WeChatServiceIsEnabled != nil && *app.Settings.WeChatServiceIsEnabled
case domain.AppTypeDisCordBot:
shouldCreateAuth = app.Settings.DiscordBotIsEnabled != nil && *app.Settings.DiscordBotIsEnabled
case domain.AppTypeWechatOfficialAccount:
shouldCreateAuth = app.Settings.WechatOfficialAccountIsEnabled != nil && *app.Settings.WechatOfficialAccountIsEnabled
}
if !shouldCreateAuth {
m.logger.Debug("app is not enabled, skipping auth creation", log.String("app_id", app.ID), log.String("source_type", string(sourceType)))
continue
}
// 检查是否已存在该类型的认证记录
var existingAuthCount int64
if err := tx.WithContext(ctx).Model(&domain.Auth{}).
Where("kb_id = ? AND source_type = ?", app.KBID, string(sourceType)).
Count(&existingAuthCount).Error; err != nil {
return fmt.Errorf("failed to check existing auth for kb_id %s, source_type %s: %w", app.KBID, sourceType, err)
}
if existingAuthCount > 0 {
m.logger.Debug("auth already exists, skipping", log.String("kb_id", app.KBID), log.String("source_type", string(sourceType)))
continue
}
// 创建新的认证记录
auth := &domain.Auth{
KBID: app.KBID,
UnionID: fmt.Sprintf("bot_%s_%s", app.ID, sourceType),
SourceType: sourceType,
LastLoginTime: time.Now(),
UserInfo: domain.AuthUserInfo{
Username: sourceType.Name(),
},
}
if err := tx.WithContext(ctx).Create(auth).Error; err != nil {
return fmt.Errorf("failed to create auth for kb_id %s, source_type %s: %w", app.KBID, sourceType, err)
}
m.logger.Info("created bot auth",
log.String("kb_id", app.KBID),
log.String("app_id", app.ID),
log.String("source_type", string(sourceType)),
log.String("union_id", auth.UnionID),
log.Any("auth_id", auth.ID))
}
m.logger.Info("bot auth migration completed successfully")
return nil
}

View File

@@ -0,0 +1,73 @@
package fns
import (
"context"
"fmt"
"github.com/chaitin/panda-wiki/consts"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/log"
"github.com/chaitin/panda-wiki/repo/mq"
"gorm.io/gorm"
)
type MigrationFixGroupIds struct {
Name string
logger *log.Logger
ragRepo *mq.RAGRepository
}
func NewMigrationFixGroupIds(logger *log.Logger, ragRepo *mq.RAGRepository) *MigrationFixGroupIds {
return &MigrationFixGroupIds{
Name: "0003_fix_group_ids",
logger: logger,
ragRepo: ragRepo,
}
}
func (m *MigrationFixGroupIds) Execute(tx *gorm.DB) error {
var nodes []domain.Node
if err := tx.Model(&domain.Node{}).
Select("id", "kb_id", "doc_id").
Where("permissions->>'answerable' = ?", consts.NodeAccessPermClosed).
Find(&nodes).Error; err != nil {
return fmt.Errorf("get node list failed: %w", err)
}
if len(nodes) == 0 {
return nil
}
nodeIds := make([]string, 0, len(nodes))
for _, node := range nodes {
nodeIds = append(nodeIds, node.ID)
}
var nodeReleases []domain.NodeRelease
if err := tx.Model(&domain.NodeRelease{}).
Where("node_id IN (?)", nodeIds).
Select("DISTINCT ON (node_id) id, node_id, kb_id, doc_id").
Order("node_id, updated_at DESC").
Find(&nodeReleases).Error; err != nil {
return fmt.Errorf("get node release list failed: %w", err)
}
var nodeVectorContentRequests []*domain.NodeReleaseVectorRequest
for _, nodeRelease := range nodeReleases {
if nodeRelease.DocID == "" {
continue
}
nodeVectorContentRequests = append(nodeVectorContentRequests, &domain.NodeReleaseVectorRequest{
KBID: nodeRelease.KBID,
DocID: nodeRelease.DocID,
Action: "update_group_ids",
GroupIds: []int{},
})
}
if len(nodeVectorContentRequests) > 0 {
if err := m.ragRepo.AsyncUpdateNodeReleaseVector(context.Background(), nodeVectorContentRequests); err != nil {
return fmt.Errorf("async update node release vector failed: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,36 @@
package fns
import (
"gorm.io/gorm"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/log"
)
type MigrationUpdateNodeStatusUnreleased struct {
Name string
logger *log.Logger
}
func NewMigrationUpdateNodeStatusUnreleased(logger *log.Logger) *MigrationUpdateNodeStatusUnreleased {
return &MigrationUpdateNodeStatusUnreleased{
Name: "0004_update_node_status_unreleased",
logger: logger,
}
}
func (m *MigrationUpdateNodeStatusUnreleased) Execute(tx *gorm.DB) error {
// 将所有 status=1 (Draft) 且从未发布过的节点更新为 status=0 (Unreleased)
// 判断条件node_releases 表中不存在该 node_id 的记录
result := tx.Model(&domain.Node{}).
Where("status = ?", domain.NodeStatusDraft).
Where("id NOT IN (SELECT DISTINCT node_id FROM node_releases)").
Update("status", domain.NodeStatusUnreleased)
if result.Error != nil {
return result.Error
}
m.logger.Info("migration update node status unreleased", log.Int64("affected_rows", result.RowsAffected))
return nil
}

View File

@@ -0,0 +1,86 @@
package fns
import (
"errors"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/log"
)
type MigrationCreateFirstNavs struct {
Name string
logger *log.Logger
}
func NewMigrationCreateFirstNavs(logger *log.Logger) *MigrationCreateFirstNavs {
return &MigrationCreateFirstNavs{
Name: "0005_create_first_navs",
logger: logger,
}
}
func (m *MigrationCreateFirstNavs) Execute(tx *gorm.DB) error {
var kbs []*domain.KnowledgeBaseListItem
if err := tx.Model(&domain.KnowledgeBase{}).
Order("created_at ASC").
Find(&kbs).Error; err != nil {
return err
}
for _, kb := range kbs {
nav := &domain.Nav{
ID: uuid.New().String(),
Name: kb.Name,
KbID: kb.ID,
}
if err := tx.Model(&domain.Nav{}).Create(nav).Error; err != nil {
return err
}
if err := tx.Model(&domain.Node{}).
Where("kb_id = ?", kb.ID).
Update("nav_id", nav.ID).Error; err != nil {
return err
}
var release domain.KBRelease
err := tx.Model(&domain.KBRelease{}).
Where("kb_id = ?", kb.ID).
Order("created_at DESC").
First(&release).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return err
}
navRelease := &domain.NavRelease{
ID: uuid.New().String(),
NavID: nav.ID,
ReleaseID: release.ID,
KbID: release.KBID,
Name: nav.Name,
Position: nav.Position,
CreatedAt: time.Now(),
}
if err := tx.Model(&domain.NavRelease{}).Create(navRelease).Error; err != nil {
return err
}
if err := tx.Model(&domain.KBReleaseNodeRelease{}).
Where("kb_id = ? AND release_id = ?", kb.ID, release.ID).
Update("nav_id", nav.ID).Error; err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,13 @@
package fns
import (
"github.com/google/wire"
)
var ProviderSet = wire.NewSet(
NewMigrationNodeVersion,
NewMigrationCreateBotAuth,
NewMigrationFixGroupIds,
NewMigrationUpdateNodeStatusUnreleased,
NewMigrationCreateFirstNavs,
)