commit
This commit is contained in:
146
cmd/message-migrator/README.md
Normal file
146
cmd/message-migrator/README.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Message Migrator 数据迁移工具
|
||||
|
||||
这个工具用于将指定项目的消息数据从源数据库迁移到目标数据库。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 按项目ID迁移数据
|
||||
- ✅ 支持可选的UID替换
|
||||
- ✅ 自动处理表之间的ID关联映射
|
||||
- ✅ 使用事务保证数据一致性
|
||||
- ✅ 按正确的依赖顺序迁移:`m_stream_messages` → `m_stream_contents` → `m_context_messages` → `m_task_messages`
|
||||
|
||||
## 涉及的表
|
||||
|
||||
1. `m_stream_messages` - 流式消息记录表(主表)
|
||||
2. `m_stream_contents` - 流式消息内容详情及分段表
|
||||
3. `m_context_messages` - Agent上下文消息记录表
|
||||
4. `m_task_messages` - 任务聚合消息记录表
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
go get github.com/go-sql-driver/mysql
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
在使用前,请修改 `main.go` 中的数据库连接配置:
|
||||
|
||||
```go
|
||||
const (
|
||||
// 源数据库 DSN 格式: user:password@tcp(host:port)/database?parseTime=true
|
||||
SOURCE_DSN = "user:password@tcp(localhost:3306)/goalfymax_prod?parseTime=true"
|
||||
// 目标数据库 DSN
|
||||
TARGET_DSN = "user:password@tcp(localhost:3306)/goalfymax_archive?parseTime=true"
|
||||
)
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本用法(保持原始UID)
|
||||
|
||||
```bash
|
||||
go run main.go -project <project_id>
|
||||
```
|
||||
|
||||
示例:
|
||||
```bash
|
||||
go run main.go -project 123
|
||||
```
|
||||
|
||||
### 替换UID
|
||||
|
||||
```bash
|
||||
go run main.go -project <project_id> -uid <new_uid>
|
||||
```
|
||||
|
||||
示例:
|
||||
```bash
|
||||
go run main.go -project 123 -uid 456
|
||||
```
|
||||
|
||||
### 自定义数据库连接
|
||||
|
||||
```bash
|
||||
go run main.go -project 123 \
|
||||
-source "root:pass@tcp(source-host:3306)/goalfymax_prod?parseTime=true" \
|
||||
-target "root:pass@tcp(target-host:3306)/goalfymax_archive?parseTime=true"
|
||||
```
|
||||
|
||||
### 编译后使用
|
||||
|
||||
```bash
|
||||
# 编译
|
||||
go build -o message-migrator
|
||||
|
||||
# 运行
|
||||
./message-migrator -project 123
|
||||
./message-migrator -project 123 -uid 456
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 必需 | 说明 | 示例 |
|
||||
| ---------- | ---- | ------------------------------------------- | --------------------------------------- |
|
||||
| `-project` | 是 | 要迁移的项目ID | `-project 123` |
|
||||
| `-uid` | 否 | 新的用户ID(不指定则使用原始UID) | `-uid 456` |
|
||||
| `-source` | 否 | 源数据库DSN(不指定则使用代码中的默认值) | `-source "user:pass@tcp(host:3306)/db"` |
|
||||
| `-target` | 否 | 目标数据库DSN(不指定则使用代码中的默认值) | `-target "user:pass@tcp(host:3306)/db"` |
|
||||
|
||||
## 迁移流程
|
||||
|
||||
1. **连接数据库**:连接到源数据库和目标数据库
|
||||
2. **开启事务**:在两个数据库上分别开启事务
|
||||
3. **迁移主表**:迁移 `m_stream_messages`,记录旧ID到新ID的映射
|
||||
4. **迁移关联表**:
|
||||
- 迁移 `m_stream_contents`,使用映射更新 `main_message_id`
|
||||
- 迁移 `m_context_messages`,使用映射更新 `main_message_id`
|
||||
- 迁移 `m_task_messages`,使用映射更新 `main_message_id`
|
||||
5. **提交事务**:所有数据迁移成功后提交事务
|
||||
|
||||
## 注意事项
|
||||
|
||||
⚠️ **重要提示**:
|
||||
|
||||
1. **备份数据**:在执行迁移前,请务必备份源数据库和目标数据库
|
||||
2. **目标表存在**:工具不会创建表,请确保目标数据库中已存在所有需要的表结构
|
||||
3. **事务一致性**:迁移过程使用事务,如果中途失败会自动回滚
|
||||
4. **ID映射**:工具会自动处理 `main_message_id` 的映射关系,确保关联正确
|
||||
5. **UID替换**:如果指定了 `-uid` 参数,所有表中的 `uid` 字段都会被替换为新值
|
||||
6. **重复执行**:请注意,重复执行相同的迁移命令可能会导致数据重复
|
||||
|
||||
## 日志输出
|
||||
|
||||
工具会输出详细的迁移日志:
|
||||
|
||||
```
|
||||
2025/11/07 10:00:00 Starting migration for project_id=123
|
||||
2025/11/07 10:00:00 Will replace UID with: 456
|
||||
2025/11/07 10:00:01 Step 1: Migrating m_stream_messages...
|
||||
2025/11/07 10:00:02 Migrated 150 stream messages
|
||||
2025/11/07 10:00:02 Step 2: Migrating m_stream_contents...
|
||||
2025/11/07 10:00:03 Migrated 300 stream contents
|
||||
2025/11/07 10:00:03 Step 3: Migrating m_context_messages...
|
||||
2025/11/07 10:00:04 Migrated 200 context messages
|
||||
2025/11/07 10:00:04 Step 4: Migrating m_task_messages...
|
||||
2025/11/07 10:00:05 Migrated 180 task messages
|
||||
2025/11/07 10:00:05 Migration completed successfully!
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
如果迁移过程中遇到错误:
|
||||
|
||||
- 事务会自动回滚,不会产生部分数据
|
||||
- 错误信息会显示在日志中
|
||||
- 检查数据库连接、权限和表结构是否正确
|
||||
|
||||
## 性能建议
|
||||
|
||||
对于大量数据的迁移:
|
||||
|
||||
1. 在非高峰期执行
|
||||
2. 确保数据库有足够的临时空间
|
||||
3. 可以考虑先迁移到测试环境验证
|
||||
4. 监控数据库性能指标
|
||||
569
cmd/message-migrator/main.go
Normal file
569
cmd/message-migrator/main.go
Normal file
@@ -0,0 +1,569 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
// 配置常量 - 请根据实际情况修改
|
||||
const (
|
||||
// 源数据库 DSN 格式: user:password@tcp(host:port)/database?parseTime=true
|
||||
SOURCE_DSN = "goalfymax_prod:X6cQDaOLOifFBOMq@tcp(goalfyagent-aurora-mysql-staging.cb2sq6y2mg93.us-west-2.rds.amazonaws.com:3306)/goalfymax_prod?charset=utf8mb4&parseTime=True&loc=UTC"
|
||||
// 目标数据库 DSN
|
||||
TARGET_DSN = "root:123456@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
)
|
||||
|
||||
type MigrationConfig struct {
|
||||
ProjectID uint64
|
||||
NewUID *uint64 // 如果为 nil,则使用原始 UID
|
||||
SourceDB *sql.DB
|
||||
TargetDB *sql.DB
|
||||
}
|
||||
|
||||
// StreamMessage 对应 m_stream_messages 表
|
||||
type StreamMessage struct {
|
||||
ID uint64
|
||||
UID uint64
|
||||
ProjectID uint64
|
||||
SenderID sql.NullString
|
||||
SenderAgentType sql.NullString
|
||||
Type string
|
||||
MessageTimestamp string
|
||||
AgentGenMessageID sql.NullString
|
||||
Content string
|
||||
FormattedContent string
|
||||
TaskID sql.NullString
|
||||
TaskStatus sql.NullString
|
||||
TurnID sql.NullInt32
|
||||
AgentMessageID sql.NullString
|
||||
Source string
|
||||
Metadata sql.NullString
|
||||
ReceivedAt string
|
||||
CreatedAt string
|
||||
Show int
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 命令行参数
|
||||
projectID := flag.Uint64("project", 0, "Project ID to migrate (required)")
|
||||
newUID := flag.Uint64("uid", 0, "New UID for migrated data (optional, use 0 to keep original)")
|
||||
sourceDSN := flag.String("source", SOURCE_DSN, "Source database DSN")
|
||||
targetDSN := flag.String("target", TARGET_DSN, "Target database DSN")
|
||||
flag.Parse()
|
||||
|
||||
if *projectID == 0 {
|
||||
fmt.Println("Usage: go run main.go -project <project_id> [-uid <new_uid>] [-source <dsn>] [-target <dsn>]")
|
||||
fmt.Println("\nExample:")
|
||||
fmt.Println(" go run main.go -project 123")
|
||||
fmt.Println(" go run main.go -project 123 -uid 456")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 连接数据库
|
||||
sourceDB, err := sql.Open("mysql", *sourceDSN)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to source database: %v", err)
|
||||
}
|
||||
defer sourceDB.Close()
|
||||
|
||||
targetDB, err := sql.Open("mysql", *targetDSN)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to target database: %v", err)
|
||||
}
|
||||
defer targetDB.Close()
|
||||
|
||||
// 测试连接
|
||||
if err := sourceDB.Ping(); err != nil {
|
||||
log.Fatalf("Failed to ping source database: %v", err)
|
||||
}
|
||||
if err := targetDB.Ping(); err != nil {
|
||||
log.Fatalf("Failed to ping target database: %v", err)
|
||||
}
|
||||
|
||||
config := &MigrationConfig{
|
||||
ProjectID: *projectID,
|
||||
SourceDB: sourceDB,
|
||||
TargetDB: targetDB,
|
||||
}
|
||||
|
||||
if *newUID != 0 {
|
||||
config.NewUID = newUID
|
||||
}
|
||||
|
||||
log.Printf("Starting migration for project_id=%d", *projectID)
|
||||
if config.NewUID != nil {
|
||||
log.Printf("Will replace UID with: %d", *config.NewUID)
|
||||
}
|
||||
|
||||
// 执行迁移
|
||||
if err := migrateProject(config); err != nil {
|
||||
log.Fatalf("Migration failed: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Migration completed successfully!")
|
||||
}
|
||||
|
||||
func migrateProject(config *MigrationConfig) error {
|
||||
// 开启事务
|
||||
txSource, err := config.SourceDB.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin source transaction: %w", err)
|
||||
}
|
||||
defer txSource.Rollback()
|
||||
|
||||
txTarget, err := config.TargetDB.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin target transaction: %w", err)
|
||||
}
|
||||
defer txTarget.Rollback()
|
||||
|
||||
// 步骤0: 清除目标库中已存在的该项目数据
|
||||
log.Println("Step 0: Cleaning existing data in target database...")
|
||||
if err := cleanExistingData(txTarget, config); err != nil {
|
||||
return fmt.Errorf("failed to clean existing data: %w", err)
|
||||
}
|
||||
log.Println(" Existing data cleaned")
|
||||
|
||||
// 步骤1: 迁移 m_stream_messages (主表)
|
||||
log.Println("Step 1: Migrating m_stream_messages...")
|
||||
oldToNewStreamMsgID, err := migrateStreamMessages(txSource, txTarget, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to migrate stream messages: %w", err)
|
||||
}
|
||||
log.Printf(" Migrated %d stream messages", len(oldToNewStreamMsgID))
|
||||
|
||||
// 步骤2: 迁移 m_stream_contents (依赖 main_message_id)
|
||||
log.Println("Step 2: Migrating m_stream_contents...")
|
||||
count, err := migrateStreamContents(txSource, txTarget, config, oldToNewStreamMsgID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to migrate stream contents: %w", err)
|
||||
}
|
||||
log.Printf(" Migrated %d stream contents", count)
|
||||
|
||||
// 步骤3: 迁移 m_context_messages (依赖 main_message_id)
|
||||
log.Println("Step 3: Migrating m_context_messages...")
|
||||
count, err = migrateContextMessages(txSource, txTarget, config, oldToNewStreamMsgID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to migrate context messages: %w", err)
|
||||
}
|
||||
log.Printf(" Migrated %d context messages", count)
|
||||
|
||||
// 步骤4: 迁移 m_task_messages (依赖 main_message_id)
|
||||
log.Println("Step 4: Migrating m_task_messages...")
|
||||
count, err = migrateTaskMessages(txSource, txTarget, config, oldToNewStreamMsgID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to migrate task messages: %w", err)
|
||||
}
|
||||
log.Printf(" Migrated %d task messages", count)
|
||||
|
||||
// 提交事务
|
||||
if err := txTarget.Commit(); err != nil {
|
||||
return fmt.Errorf("failed to commit target transaction: %w", err)
|
||||
}
|
||||
if err := txSource.Commit(); err != nil {
|
||||
return fmt.Errorf("failed to commit source transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanExistingData 清除目标库中已存在的该项目数据
|
||||
// 按照依赖关系逆序删除:先删除依赖表,最后删除主表
|
||||
func cleanExistingData(txTarget *sql.Tx, config *MigrationConfig) error {
|
||||
// 首先获取该项目在目标库中的所有 stream_message IDs
|
||||
var streamMessageIDs []uint64
|
||||
query := `SELECT id FROM m_stream_messages WHERE project_id = ?`
|
||||
rows, err := txTarget.Query(query, config.ProjectID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query existing stream messages: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var id uint64
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
streamMessageIDs = append(streamMessageIDs, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(streamMessageIDs) == 0 {
|
||||
log.Println(" No existing data found for this project in target database")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf(" Found %d existing stream messages to clean", len(streamMessageIDs))
|
||||
|
||||
// 步骤1: 删除 m_task_messages (依赖 main_message_id)
|
||||
result, err := txTarget.Exec(`DELETE FROM m_task_messages WHERE project_id = ?`, config.ProjectID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete task messages: %w", err)
|
||||
}
|
||||
if affected, _ := result.RowsAffected(); affected > 0 {
|
||||
log.Printf(" Deleted %d task messages", affected)
|
||||
}
|
||||
|
||||
// 步骤2: 删除 m_context_messages (依赖 main_message_id)
|
||||
result, err = txTarget.Exec(`DELETE FROM m_context_messages WHERE project_id = ?`, config.ProjectID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete context messages: %w", err)
|
||||
}
|
||||
if affected, _ := result.RowsAffected(); affected > 0 {
|
||||
log.Printf(" Deleted %d context messages", affected)
|
||||
}
|
||||
|
||||
// 步骤3: 删除 m_stream_contents (依赖 main_message_id)
|
||||
// 需要通过 main_message_id IN (...) 来删除
|
||||
if len(streamMessageIDs) > 0 {
|
||||
// 构建 IN 子句
|
||||
query := `DELETE FROM m_stream_contents WHERE main_message_id IN (`
|
||||
args := make([]interface{}, len(streamMessageIDs))
|
||||
for i, id := range streamMessageIDs {
|
||||
if i > 0 {
|
||||
query += ","
|
||||
}
|
||||
query += "?"
|
||||
args[i] = id
|
||||
}
|
||||
query += ")"
|
||||
|
||||
result, err = txTarget.Exec(query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete stream contents: %w", err)
|
||||
}
|
||||
if affected, _ := result.RowsAffected(); affected > 0 {
|
||||
log.Printf(" Deleted %d stream contents", affected)
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤4: 删除 m_stream_messages (主表)
|
||||
result, err = txTarget.Exec(`DELETE FROM m_stream_messages WHERE project_id = ?`, config.ProjectID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete stream messages: %w", err)
|
||||
}
|
||||
if affected, _ := result.RowsAffected(); affected > 0 {
|
||||
log.Printf(" Deleted %d stream messages", affected)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatTimestamp 将时间字符串转换为 MySQL datetime 格式
|
||||
// 输入可能是 "2025-11-07T02:37:23Z" 或已经是 "2025-11-07 02:37:23" 格式
|
||||
func formatTimestamp(ts string) string {
|
||||
// 尝试解析 ISO 8601 格式
|
||||
if t, err := time.Parse(time.RFC3339, ts); err == nil {
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
// 尝试解析带纳秒的 ISO 8601 格式
|
||||
if t, err := time.Parse(time.RFC3339Nano, ts); err == nil {
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
// 如果已经是正确格式或解析失败,返回原值
|
||||
return ts
|
||||
}
|
||||
|
||||
// migrateStreamMessages 迁移主消息表,返回旧ID到新ID的映射
|
||||
func migrateStreamMessages(txSource, txTarget *sql.Tx, config *MigrationConfig) (map[uint64]uint64, error) {
|
||||
query := `SELECT id, uid, project_id, sender_id, sender_agent_type, type,
|
||||
message_timestamp, agent_gen_message_id, content, formatted_content,
|
||||
task_id, task_status, turn_id, agent_message_id, source, metadata,
|
||||
received_at, created_at, ` + "`show`" + `
|
||||
FROM m_stream_messages WHERE project_id = ?`
|
||||
|
||||
rows, err := txSource.Query(query, config.ProjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
oldToNewID := make(map[uint64]uint64)
|
||||
insertQuery := `INSERT INTO m_stream_messages
|
||||
(uid, project_id, sender_id, sender_agent_type, type, message_timestamp,
|
||||
agent_gen_message_id, content, formatted_content, task_id, task_status,
|
||||
turn_id, agent_message_id, source, metadata, received_at, created_at, ` + "`show`" + `)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
|
||||
for rows.Next() {
|
||||
var msg StreamMessage
|
||||
err := rows.Scan(&msg.ID, &msg.UID, &msg.ProjectID, &msg.SenderID, &msg.SenderAgentType,
|
||||
&msg.Type, &msg.MessageTimestamp, &msg.AgentGenMessageID, &msg.Content,
|
||||
&msg.FormattedContent, &msg.TaskID, &msg.TaskStatus, &msg.TurnID,
|
||||
&msg.AgentMessageID, &msg.Source, &msg.Metadata, &msg.ReceivedAt,
|
||||
&msg.CreatedAt, &msg.Show)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果指定了新UID,替换
|
||||
uid := msg.UID
|
||||
if config.NewUID != nil {
|
||||
uid = *config.NewUID
|
||||
}
|
||||
|
||||
// 转换时间戳格式
|
||||
messageTimestamp := formatTimestamp(msg.MessageTimestamp)
|
||||
receivedAt := formatTimestamp(msg.ReceivedAt)
|
||||
createdAt := formatTimestamp(msg.CreatedAt)
|
||||
|
||||
result, err := txTarget.Exec(insertQuery,
|
||||
uid, msg.ProjectID, msg.SenderID, msg.SenderAgentType, msg.Type,
|
||||
messageTimestamp, msg.AgentGenMessageID, msg.Content,
|
||||
msg.FormattedContent, msg.TaskID, msg.TaskStatus, msg.TurnID,
|
||||
msg.AgentMessageID, msg.Source, msg.Metadata, receivedAt,
|
||||
createdAt, msg.Show)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oldToNewID[msg.ID] = uint64(newID)
|
||||
}
|
||||
|
||||
return oldToNewID, rows.Err()
|
||||
}
|
||||
|
||||
// migrateStreamContents 迁移流式内容表
|
||||
func migrateStreamContents(txSource, txTarget *sql.Tx, config *MigrationConfig, idMap map[uint64]uint64) (int, error) {
|
||||
query := `SELECT id, main_message_id, agent_gen_message_id, type, content,
|
||||
formatted_content, metadata, message_timestamp, ` + "`show`" + `, received_at, created_at
|
||||
FROM m_stream_contents
|
||||
WHERE main_message_id IN (
|
||||
SELECT id FROM m_stream_messages WHERE project_id = ?
|
||||
)`
|
||||
|
||||
rows, err := txSource.Query(query, config.ProjectID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
insertQuery := `INSERT INTO m_stream_contents
|
||||
(main_message_id, agent_gen_message_id, type, content, formatted_content,
|
||||
metadata, message_timestamp, ` + "`show`" + `, received_at, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
var (
|
||||
id uint64
|
||||
mainMessageID uint64
|
||||
agentGenMessageID sql.NullString
|
||||
msgType string
|
||||
content string
|
||||
formattedContent string
|
||||
metadata sql.NullString
|
||||
messageTimestamp string
|
||||
show int
|
||||
receivedAt string
|
||||
createdAt string
|
||||
)
|
||||
|
||||
err := rows.Scan(&id, &mainMessageID, &agentGenMessageID, &msgType, &content,
|
||||
&formattedContent, &metadata, &messageTimestamp, &show, &receivedAt, &createdAt)
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
|
||||
// 映射新的 main_message_id
|
||||
newMainMessageID, ok := idMap[mainMessageID]
|
||||
if !ok {
|
||||
log.Printf("Warning: main_message_id %d not found in mapping, skipping content id %d", mainMessageID, id)
|
||||
continue
|
||||
}
|
||||
|
||||
// 转换时间戳格式
|
||||
messageTimestamp = formatTimestamp(messageTimestamp)
|
||||
receivedAt = formatTimestamp(receivedAt)
|
||||
createdAt = formatTimestamp(createdAt)
|
||||
|
||||
_, err = txTarget.Exec(insertQuery, newMainMessageID, agentGenMessageID, msgType,
|
||||
content, formattedContent, metadata, messageTimestamp, show, receivedAt, createdAt)
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
count++
|
||||
}
|
||||
|
||||
return count, rows.Err()
|
||||
}
|
||||
|
||||
// migrateContextMessages 迁移上下文消息表
|
||||
func migrateContextMessages(txSource, txTarget *sql.Tx, config *MigrationConfig, idMap map[uint64]uint64) (int, error) {
|
||||
query := `SELECT id, main_message_id, ownership, uid, project_id, turn_id,
|
||||
agent_message_id, sender_id, sender_agent_type, type, source, content,
|
||||
formatted_content, metadata, message_timestamp, agent_gen_message_id,
|
||||
` + "`show`" + `, received_at, created_at
|
||||
FROM m_context_messages WHERE project_id = ?`
|
||||
|
||||
rows, err := txSource.Query(query, config.ProjectID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
insertQuery := `INSERT INTO m_context_messages
|
||||
(main_message_id, ownership, uid, project_id, turn_id, agent_message_id,
|
||||
sender_id, sender_agent_type, type, source, content, formatted_content,
|
||||
metadata, message_timestamp, agent_gen_message_id, ` + "`show`" + `, received_at, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
var (
|
||||
id uint64
|
||||
mainMessageID uint64
|
||||
ownership string
|
||||
uid uint64
|
||||
projectID uint64
|
||||
turnID sql.NullInt32
|
||||
agentMessageID sql.NullString
|
||||
senderID sql.NullString
|
||||
senderAgentType sql.NullString
|
||||
msgType string
|
||||
source string
|
||||
content sql.NullString
|
||||
formattedContent sql.NullString
|
||||
metadata sql.NullString
|
||||
messageTimestamp string
|
||||
agentGenMessageID sql.NullString
|
||||
show int
|
||||
receivedAt string
|
||||
createdAt string
|
||||
)
|
||||
|
||||
err := rows.Scan(&id, &mainMessageID, &ownership, &uid, &projectID, &turnID,
|
||||
&agentMessageID, &senderID, &senderAgentType, &msgType, &source, &content,
|
||||
&formattedContent, &metadata, &messageTimestamp, &agentGenMessageID,
|
||||
&show, &receivedAt, &createdAt)
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
|
||||
// 映射新的 main_message_id
|
||||
newMainMessageID, ok := idMap[mainMessageID]
|
||||
if !ok {
|
||||
log.Printf("Warning: main_message_id %d not found in mapping, skipping context message id %d", mainMessageID, id)
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果指定了新UID,替换
|
||||
targetUID := uid
|
||||
if config.NewUID != nil {
|
||||
targetUID = *config.NewUID
|
||||
}
|
||||
|
||||
// 转换时间戳格式
|
||||
messageTimestamp = formatTimestamp(messageTimestamp)
|
||||
receivedAt = formatTimestamp(receivedAt)
|
||||
createdAt = formatTimestamp(createdAt)
|
||||
|
||||
_, err = txTarget.Exec(insertQuery, newMainMessageID, ownership, targetUID, projectID,
|
||||
turnID, agentMessageID, senderID, senderAgentType, msgType, source, content,
|
||||
formattedContent, metadata, messageTimestamp, agentGenMessageID, show,
|
||||
receivedAt, createdAt)
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
count++
|
||||
}
|
||||
|
||||
return count, rows.Err()
|
||||
}
|
||||
|
||||
// migrateTaskMessages 迁移任务消息表
|
||||
func migrateTaskMessages(txSource, txTarget *sql.Tx, config *MigrationConfig, idMap map[uint64]uint64) (int, error) {
|
||||
query := `SELECT id, main_message_id, uid, project_id, sender_id, sender_agent_type,
|
||||
type, message_timestamp, agent_gen_message_id, content, formatted_content,
|
||||
task_status, turn_id, agent_message_id, source, metadata, ` + "`show`" + `,
|
||||
received_at, created_at
|
||||
FROM m_task_messages WHERE project_id = ?`
|
||||
|
||||
rows, err := txSource.Query(query, config.ProjectID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
insertQuery := `INSERT INTO m_task_messages
|
||||
(main_message_id, uid, project_id, sender_id, sender_agent_type, type,
|
||||
message_timestamp, agent_gen_message_id, content, formatted_content,
|
||||
task_status, turn_id, agent_message_id, source, metadata, ` + "`show`" + `,
|
||||
received_at, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
var (
|
||||
id uint64
|
||||
mainMessageID uint64
|
||||
uid uint64
|
||||
projectID uint64
|
||||
senderID sql.NullString
|
||||
senderAgentType sql.NullString
|
||||
msgType string
|
||||
messageTimestamp string
|
||||
agentGenMessageID sql.NullString
|
||||
content string
|
||||
formattedContent string
|
||||
taskStatus sql.NullString
|
||||
turnID sql.NullInt32
|
||||
agentMessageID sql.NullString
|
||||
source sql.NullString
|
||||
metadata sql.NullString
|
||||
show int
|
||||
receivedAt string
|
||||
createdAt string
|
||||
)
|
||||
|
||||
err := rows.Scan(&id, &mainMessageID, &uid, &projectID, &senderID, &senderAgentType,
|
||||
&msgType, &messageTimestamp, &agentGenMessageID, &content, &formattedContent,
|
||||
&taskStatus, &turnID, &agentMessageID, &source, &metadata, &show,
|
||||
&receivedAt, &createdAt)
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
|
||||
// 映射新的 main_message_id
|
||||
newMainMessageID, ok := idMap[mainMessageID]
|
||||
if !ok {
|
||||
log.Printf("Warning: main_message_id %d not found in mapping, skipping task message id %d", mainMessageID, id)
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果指定了新UID,替换
|
||||
targetUID := uid
|
||||
if config.NewUID != nil {
|
||||
targetUID = *config.NewUID
|
||||
}
|
||||
|
||||
// 转换时间戳格式
|
||||
messageTimestamp = formatTimestamp(messageTimestamp)
|
||||
receivedAt = formatTimestamp(receivedAt)
|
||||
createdAt = formatTimestamp(createdAt)
|
||||
|
||||
_, err = txTarget.Exec(insertQuery, newMainMessageID, targetUID, projectID, senderID,
|
||||
senderAgentType, msgType, messageTimestamp, agentGenMessageID, content,
|
||||
formattedContent, taskStatus, turnID, agentMessageID, source, metadata,
|
||||
show, receivedAt, createdAt)
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
count++
|
||||
}
|
||||
|
||||
return count, rows.Err()
|
||||
}
|
||||
BIN
cmd/message-migrator/message-migrator
Executable file
BIN
cmd/message-migrator/message-migrator
Executable file
Binary file not shown.
124
cmd/redis-pub-sub/main.go
Normal file
124
cmd/redis-pub-sub/main.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// 定义 Redis 配置和频道名称
|
||||
const redisAddr = "localhost:6379" // 替换成您的 Redis 地址
|
||||
const channelName = "stream:10"
|
||||
|
||||
// 全局 Redis 客户端
|
||||
var rdb *redis.Client
|
||||
|
||||
// 定义命令行参数
|
||||
var mode = flag.String("mode", "", "运行模式:必须是 'pub' (发布者) 或 'sub' (订阅者)")
|
||||
|
||||
// 初始化 Redis 客户端并检查连接
|
||||
func initRedis() {
|
||||
rdb = redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
Password: "", // 您的密码(如果没有则为空)
|
||||
DB: 7, // 默认 DB
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := rdb.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
log.Fatalf("无法连接到 Redis (%s): %v", redisAddr, err)
|
||||
}
|
||||
fmt.Println("成功连接到 Redis。")
|
||||
}
|
||||
|
||||
// 订阅者逻辑
|
||||
func runSubscriber() {
|
||||
fmt.Println("--- 启动订阅者 (Subscriber) 模式 ---")
|
||||
|
||||
// 订阅频道
|
||||
// Subscribes to the given channels.
|
||||
pubsub := rdb.Subscribe(context.Background(), channelName)
|
||||
defer pubsub.Close() // 确保在函数退出时关闭连接
|
||||
|
||||
// 接收并处理消息
|
||||
fmt.Printf("已订阅频道: %s。等待消息...\n", channelName)
|
||||
|
||||
// 使用 Channel() 方法获取一个 Go Channel 来接收消息
|
||||
ch := pubsub.Channel()
|
||||
|
||||
// 循环接收消息
|
||||
for msg := range ch {
|
||||
fmt.Printf("\n--- 收到消息 ---\n")
|
||||
fmt.Printf("频道 (Channel): %s\n", msg.Channel)
|
||||
fmt.Printf("消息 (Payload): %s\n", msg.Payload)
|
||||
fmt.Printf("----------------\n")
|
||||
|
||||
// 添加退出逻辑
|
||||
if msg.Payload == "exit" {
|
||||
fmt.Println("\n收到 'exit' 消息,退出订阅循环。")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("订阅者程序退出。")
|
||||
}
|
||||
|
||||
// 发布者逻辑
|
||||
func runPublisher() {
|
||||
fmt.Println("--- 启动发布者 (Publisher) 模式 ---")
|
||||
|
||||
messages := []string{
|
||||
"Hello Redis Pub/Sub!",
|
||||
"This is message two from publisher.",
|
||||
"A third message is here.",
|
||||
"exit", // 发送退出信号给订阅者
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 循环发布消息
|
||||
for i, msg := range messages {
|
||||
// Publish publishes message to channel.
|
||||
// Result() 返回值是接收到消息的订阅者数量 (int64)
|
||||
result, err := rdb.Publish(ctx, channelName, msg).Result()
|
||||
if err != nil {
|
||||
log.Fatalf("发布消息失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("发布消息 %d: '%s' 到频道 '%s',有 %d 个订阅者接收。\n",
|
||||
i+1, msg, channelName, result)
|
||||
|
||||
// 每次发送后等待一小段时间
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
fmt.Println("发布者程序完成。")
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 解析命令行参数
|
||||
flag.Parse()
|
||||
|
||||
// 检查模式参数
|
||||
if *mode != "pub" && *mode != "sub" {
|
||||
fmt.Println("错误:请指定有效的运行模式。")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 初始化 Redis
|
||||
initRedis()
|
||||
|
||||
// 根据模式执行相应逻辑
|
||||
if *mode == "sub" {
|
||||
runSubscriber()
|
||||
} else if *mode == "pub" {
|
||||
runPublisher()
|
||||
}
|
||||
}
|
||||
49
cmd/xb/main.go
Normal file
49
cmd/xb/main.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TzStrToOffset(ianaZone string) (string, error) {
|
||||
loc, err := time.LoadLocation(ianaZone)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("无法加载时区 %s: %v", ianaZone, err)
|
||||
}
|
||||
|
||||
t := time.Date(2025, time.October, 28, 0, 0, 0, 0, loc)
|
||||
|
||||
_, offsetSeconds := t.Zone()
|
||||
|
||||
offset := time.Duration(offsetSeconds) * time.Second
|
||||
|
||||
// 格式化偏移量。time.Time.Format() 无法直接将 Location 格式化成 UTC+HH:MM,
|
||||
// 因此我们需要手动计算并格式化。
|
||||
sign := "+"
|
||||
if offsetSeconds < 0 {
|
||||
sign = "-"
|
||||
offset = -offset // 确保偏移量为正值进行计算
|
||||
}
|
||||
|
||||
hours := int(offset.Hours())
|
||||
minutes := int(offset.Minutes()) % 60
|
||||
|
||||
// 最终的格式化字符串
|
||||
utcOffsetString := fmt.Sprintf("%s%02d%02d", sign, hours, minutes)
|
||||
|
||||
return utcOffsetString, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
ianaZone := "Asia/Shanghai"
|
||||
ianaZone = "UTC"
|
||||
|
||||
utcOffsetString, err := TzStrToOffset(ianaZone)
|
||||
if err != nil {
|
||||
log.Fatalf("错误: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("IANA 时区: %s\n", ianaZone)
|
||||
fmt.Printf("UTC 偏移量: %s\n", utcOffsetString)
|
||||
}
|
||||
Reference in New Issue
Block a user