This commit is contained in:
Wyle.Gong-巩文昕
2025-04-22 16:42:48 +08:00
commit 67b0ad2723
95 changed files with 10508 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
package graph
import (
"app/cfg"
"app/models/project"
"app/models/project/stage"
"app/utils"
"errors"
"fmt"
"github.com/veypi/OneBD/rest"
)
var _ = Router.Get("", getHandle)
var _ = Router.Get("/:stage_id/", getHandle)
// getHandle 获取完整的图形数据,包括节点和连接
func getHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
stageID, ok := x.Params.Get("stage_id")
if !ok {
return nil, errors.New("缺少stage_id")
}
// 验证阶段是否存在
var stageOBJ project.Stage
if err := cfg.DB().Where("id = ?", stageID).First(&stageOBJ).Error; err != nil {
return nil, errors.New("阶段不存在")
}
// 获取该阶段的所有节点
var nodes []stage.GraphNode
if err := cfg.DB().Where("stage_id = ?", stageID).Find(&nodes).Error; err != nil {
return nil, err
}
// 获取该阶段的所有连接
var links []stage.GraphLink
if err := cfg.DB().Where("stage_id = ?", stageID).Find(&links).Error; err != nil {
return nil, err
}
// 返回完整的图形数据
return map[string]interface{}{
"stage": stageOBJ,
"nodes": nodes,
"links": links,
}, nil
}
var _ = Router.Post("/:stage_id/", saveHandle)
func saveHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
fmt.Println("saveHandle")
stageID, ok := x.Params.Get("stage_id")
if !ok {
return nil, errors.New("缺少stage_id")
}
// 验证阶段是否存在
var stageOBJ project.Stage
if err := cfg.DB().Where("id = ?", stageID).First(&stageOBJ).Error; err != nil {
return nil, errors.New("阶段不存在")
}
// 解析请求体
var data struct {
Nodes []stage.GraphNode `json:"nodes"`
Links []stage.GraphLink `json:"links"`
}
if err := x.Parse(&data); err != nil {
return nil, err
}
// fmt.Printf("%+v\n", data)
// 开始事务
tx := cfg.DB().Begin()
// 删除原有的节点和连接
if err := tx.Where("stage_id = ?", stageID).Delete(&stage.GraphNode{}).Error; err != nil {
tx.Rollback()
return nil, err
}
if err := tx.Where("stage_id = ?", stageID).Delete(&stage.GraphLink{}).Error; err != nil {
tx.Rollback()
return nil, err
}
// 保存新的节点和连接
for _, node := range data.Nodes {
node.StageID = stageID
if err := tx.Create(&node).Error; err != nil {
tx.Rollback()
return nil, err
}
}
for _, link := range data.Links {
link.StageID = stageID
if err := tx.Create(&link).Error; err != nil {
tx.Rollback()
return nil, err
}
}
// 提交事务
if err := tx.Commit().Error; err != nil {
return nil, err
}
return map[string]interface{}{
"message": "保存成功",
}, nil
}
var _ = Router.Post("/:stage_id/generate", generateHandle)
func generateHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
return nil, nil
}
var _ = Router.Any("/*", anyHandle)
// anyHandle 处理其他请求
func anyHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
return nil, nil
}
+7
View File
@@ -0,0 +1,7 @@
package graph
import (
"github.com/veypi/OneBD/rest"
)
var Router = rest.NewRouter()
+742
View File
@@ -0,0 +1,742 @@
// 文件: app/stage/graph_processor.go
package stage // 确认包名是 stage
import (
M "app/models" // 确认 M 是 models 包
"context"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
// --- 项目内引用 ---
"app/cfg" // 对应你的 cfg
"app/models/project" // 对应你的 project 模型
"app/models/project/stage" // GraphNode/Link 定义在此
// --- 第三方库 ---
"github.com/google/uuid"
"github.com/openai/openai-go" // 主 SDK 包
"github.com/openai/openai-go/option" // !! 确保这个导入存在 !!
"github.com/xeipuuv/gojsonschema"
"gorm.io/gorm"
)
// --- 配置常量 ---
const (
llmAPIKeyEnvVar = "sk-0213c70194624703a1d0d80e0f762b0e"
llmBaseURLEnvVar = "https://dashscope.aliyuncs.com/compatible-mode/v1/"
llmModelName = "qwen-plus" // 保持与你的 doc 包一致
toolNodesFilePath = "cfg/tool_node_definations.json"
llmRequestTimeout = 120 * time.Second
defaultQueueSize = 100
workerCount = 2
)
// --- 任务定义 ---
type graphGenerationTask struct {
StageID string
}
// --- LLM 输出解析结构体 ---
// --- 处理器定义 ---
type GraphProcessor struct {
taskQueue chan graphGenerationTask
llmClient *openai.Client
db *gorm.DB
logger *log.Logger // 主日志 logger
llmIOLogger *log.Logger
toolNodeDefinitionsJSON string
initialized bool
initMutex sync.Mutex
}
var (
processorInstance *GraphProcessor
processorOnce sync.Once
)
func setupLogger() (*log.Logger, error) { // 返回 error 以便处理创建失败的情况
logDir := filepath.Join("logs", "graph") // 在 logs 目录下创建 graph 子目录
if err := os.MkdirAll(logDir, 0755); err != nil {
log.Printf("错误:无法创建日志目录 %s: %v\n", logDir, err)
return nil, fmt.Errorf("无法创建日志目录 %s: %w", logDir, err)
}
// 注意:日志文件名应包含 'graph' 以区分
timestamp := time.Now().Format("20060102_150405")
logFilename := filepath.Join(logDir, fmt.Sprintf("graph_llm_io_%s.log", timestamp))
file, err := os.OpenFile(logFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Printf("错误:无法打开日志文件 %s: %v\n", logFilename, err)
return nil, fmt.Errorf("无法打开日志文件 %s: %w", logFilename, err)
}
// 返回一个新的 logger 实例,写入到特定文件
// 使用不同的前缀区分日志条目类型
return log.New(file, "[LLM_IO] ", log.LstdFlags), nil
}
// InitGraphProcessor 初始化 GraphProcessor 单例及其依赖 (导出)
func InitGraphProcessor() error {
var initErr error
processorOnce.Do(func() {
instance := &GraphProcessor{
logger: log.New(log.Writer(), "[StageGraphProcessor] ", log.LstdFlags),
}
instance.initMutex.Lock()
defer instance.initMutex.Unlock()
if instance.initialized {
processorInstance = instance
return
}
// 1. 加载工具定义
toolDefs, err := instance.loadToolDefinitions(toolNodesFilePath)
if err != nil {
initErr = fmt.Errorf("无法加载工具定义: %w", err)
return
}
instance.toolNodeDefinitionsJSON = toolDefs
// 2. 初始化 LLM Client (严格按照你的 doc 包示例)
client, err := instance.initLLMClient() // 使用修正后的初始化
if err != nil {
initErr = fmt.Errorf("无法初始化 LLM 客户端: %w", err)
return
}
instance.llmClient = client
llmLogger, err := setupLogger()
if err != nil {
// 如果 LLM 日志创建失败,可以选择继续运行但发出警告,或直接失败
instance.logger.Printf("警告:无法创建 LLM I/O 日志文件,将不记录 LLM 交互细节: %v", err)
// initErr = fmt.Errorf("无法创建 LLM I/O 日志: %w", err)
// return
}
instance.llmIOLogger = llmLogger
// 3. 获取数据库连接
dbConn := cfg.DB()
if dbConn == nil {
initErr = errors.New("数据库连接不可用")
return
}
instance.db = dbConn
// 4. 创建任务队列
instance.taskQueue = make(chan graphGenerationTask, defaultQueueSize)
// 5. 启动 Workers
for i := 0; i < workerCount; i++ {
go instance.worker(i + 1)
}
instance.initialized = true
instance.logger.Println("Graph processor 初始化成功,启动", workerCount, "个 worker。")
processorInstance = instance
})
return initErr
}
// GetGraphProcessor 获取 GraphProcessor 单例 (导出)
func GetGraphProcessor() (*GraphProcessor, error) {
if processorInstance == nil || !processorInstance.initialized {
return nil, errors.New("GraphProcessor 未初始化")
}
return processorInstance, nil
}
// loadToolDefinitions 加载工具定义 (未导出)
func (p *GraphProcessor) loadToolDefinitions(filePath string) (string, error) {
content, err := os.ReadFile(filePath)
if err != nil {
return "", fmt.Errorf("读取工具定义文件 %s 失败: %w", filePath, err)
}
var js json.RawMessage
if err := json.Unmarshal(content, &js); err != nil {
return "", fmt.Errorf("工具定义文件 %s 不是有效的 JSON: %w", filePath, err)
}
p.logger.Printf("成功从 %s 加载工具定义。", filePath)
return string(content), nil
}
// initLLMClient 初始化 OpenAI 客户端 (参考你的 doc 包) (未导出)
func (p *GraphProcessor) initLLMClient() (*openai.Client, error) {
apiKey := llmAPIKeyEnvVar
baseURL := llmBaseURLEnvVar
// 必须提供 Key 和 URL (根据你的 doc 包 NewDemandAnalyzer 逻辑)
if apiKey == "" {
p.logger.Printf("错误: 环境变量 %s 未设置。", llmAPIKeyEnvVar)
return nil, fmt.Errorf("环境变量 %s 未设置", llmAPIKeyEnvVar)
}
if baseURL == "" {
p.logger.Printf("错误: 环境变量 %s 未设置。", llmBaseURLEnvVar)
return nil, fmt.Errorf("环境变量 %s 未设置", llmBaseURLEnvVar)
}
// 直接使用 option.WithXXX 作为参数
client := openai.NewClient(
option.WithAPIKey(apiKey),
option.WithBaseURL(baseURL),
// 你可以在这里添加其他固定的 option, 如 option.WithTimeout(60*time.Second)
)
p.logger.Printf("LLM 客户端已初始化 (Base URL: %s)", baseURL) // 不打印 Key
return client, nil
}
// AddTask 添加任务到队列 (导出)
func (p *GraphProcessor) AddTask(task graphGenerationTask) error {
if !p.initialized {
return errors.New("GraphProcessor 未初始化")
}
select {
case p.taskQueue <- task:
p.logger.Printf("任务已添加到队列 (StageID: %s)", task.StageID)
return nil
case <-time.After(2 * time.Second):
p.logger.Printf("错误: 添加任务超时 (StageID %s): 队列已满或无响应", task.StageID)
return errors.New("添加任务超时,队列可能已满")
}
}
// worker Go 协程,处理任务 (未导出)
func (p *GraphProcessor) worker(id int) {
p.logger.Printf("Worker %d 启动,等待任务...", id)
for task := range p.taskQueue {
p.logger.Printf("Worker %d: 开始处理任务 (StageID: %s)", id, task.StageID)
ctx, cancel := context.WithTimeout(context.Background(), llmRequestTimeout+60*time.Second)
err := p.processTask(ctx, task)
if err != nil {
p.logger.Printf("Worker %d: 处理任务失败 (StageID %s): %v", id, task.StageID, err)
p.updateStageStatus(task.StageID, "failed", err.Error())
} else {
p.logger.Printf("Worker %d: 成功处理任务 (StageID: %s)", id, task.StageID)
p.updateStageStatus(task.StageID, "completed", "")
}
cancel()
}
p.logger.Printf("Worker %d 停止。", id)
}
// updateStageStatus 更新 Stage 状态 (未导出)
func (p *GraphProcessor) updateStageStatus(stageID, status, errMsg string) {
if len(errMsg) > 1000 {
errMsg = errMsg[:1000] + "..."
}
updates := map[string]interface{}{"graph_status": status, "graph_error": errMsg}
db := cfg.DB()
if db == nil {
p.logger.Println("!!! 无法更新阶段状态:数据库连接不可用。")
return
}
if err := db.Model(&project.Stage{}).Where("id = ?", stageID).Updates(updates).Error; err != nil {
p.logger.Printf("!!! 更新阶段状态失败 (StageID %s -> %s): %v", stageID, status, err)
} else {
p.logger.Printf("阶段状态已更新 (StageID %s -> %s)", stageID, status)
}
}
// processTask 核心处理逻辑 (未导出, 使用流式 + 中文 Prompt + 不含根节点)
func (p *GraphProcessor) processTask(ctx context.Context, task graphGenerationTask) (errResult error) {
stageID := task.StageID
p.logger.Printf("开始 processTask (StageID: %s)", stageID)
defer func() {
if r := recover(); r != nil {
p.logger.Printf("!!! 处理任务时发生 Panic (StageID: %s): %v", stageID, r)
errResult = fmt.Errorf("处理任务时发生内部错误: %v", r)
// 确保状态被更新为 failed
p.updateStageStatus(stageID, "failed", fmt.Sprintf("Panic: %v", r))
}
}()
// 0. 更新状态为 processing
p.updateStageStatus(stageID, "processing", "")
// --- 数据库操作 ---
dbCtx, dbCancel := context.WithTimeout(ctx, 30*time.Second)
defer dbCancel()
db := cfg.DB().WithContext(dbCtx) // 使用 cfg 获取 DB 连接
// 1. 获取 Stage, RootDemand, Descendants
var stageObj project.Stage
if err := db.Where("id = ?", stageID).First(&stageObj).Error; err != nil {
return fmt.Errorf("获取阶段失败 (StageID: %s): %w", stageID, err)
}
if stageObj.DemandID == "" {
return fmt.Errorf("阶段 '%s' 未关联根需求 ID", stageID)
}
rootDemandID := stageObj.DemandID
var rootDemand project.Demand
if err := db.Where("id = ?", rootDemandID).First(&rootDemand).Error; err != nil {
return fmt.Errorf("获取根需求节点失败 (DemandID: %s): %w", rootDemandID, err)
}
descendants, err := rootDemand.Descendants(db)
if err != nil {
return fmt.Errorf("获取子孙需求节点失败 (Root DemandID: %s): %w", rootDemandID, err)
}
p.logger.Printf("Stage %s: 获取到 %d 个子孙节点", stageID, len(descendants))
// 4. 准备 Prompt
rootDemandJSON, _ := json.MarshalIndent(rootDemand, "", " ")
descendantInfo := make([]map[string]string, 0, len(descendants))
for _, d := range descendants {
descendantInfo = append(descendantInfo, map[string]string{
"id": d.ID,
"req_id": d.ReqID,
"name": d.Name,
"description": d.Description,
"parent_req_id": d.ParentReqID,
"parent_id": d.ParentID,
})
}
descendantsJSON, _ := json.MarshalIndent(descendantInfo, "", " ")
graphSchema, err := p.generateGraphJSONSchema()
if err != nil {
return fmt.Errorf("内部错误:生成 Schema 失败: %w", err)
}
prompt := fmt.Sprintf(`
你是一位专业的软件测试流程设计专家。
**任务目标:** 请根据下面提供的需求信息和可用的工具节点,生成一个 JSON 格式的软件测试流程图。这个流程图需要清晰地展示针对“待验证节点”的测试步骤的以及逻辑顺序。
**待验证节点:**
1. **待验证节点 (这是需要验证测试的节点,请勿包含在输出的图中):**
%s
2. **子需求节点列表 (这些是需要包含在图中的需求节点):**
%s
3. **可用的工具节点类型 (用于构建流程,以及通过这些工具进行多种测试,类型定义如下):**
%s
**指令:**
1. 创建一个 JSON 对象,包含 "nodes" 和 "links" 两个键。
2. **关于 "nodes" (节点列表):**
* 将“子需求节点列表”中的每个需求表示为 type: "demand" 的节点。在这些节点的 original_demand_id 字段中填入该需求对应的数据库主键 ID (即上述列表中的 "id" 字段值)。
* “子需求节点列表”中的每个需求节点代表一个测试流图,这个测试节点内部的测试逻辑就不在当前的图中体现了,这个图中的测试工具是针对当前的待验证节点或者子需求节点的组合的。
* **请不要在最终输出的 "nodes" 列表中包含上面第 1 点中提到的“待验证节点”本身。**
* 从“可用的工具节点类型”中选择合适的节点(如 "start", "end", "httpRequest", "condition" 等)来构建测试流程的逻辑结构(例如起点、终点、判断、并行、测试操作等)。
* “可用的工具节点类型”中的各种测试工具节能添加的最好加上,让测试完备一些
* 为生成的**每一个**节点(包括需求节点和工具节点)分配一个唯一的字符串 temp_id。
* 为每个节点提供一个有意义的 name。
* 设置每个节点正确的 type。
* 根据工具节点的定义,在节点的 meta 字段中包含必要的配置信息(必须是 JSON 字符串格式,例如 httpRequest 节点需要包含方法、路径等)。
* 为节点提供建议的布局坐标 x 和 y (数字类型)。通常 "start" 节点可以放在 (350, 150) 附近,其他节点按逻辑排布。
3. **关于 "links" (连接列表):**
* 使用 links 在逻辑上连接你创建的节点。
* 每个 link 必须包含 source_temp_id 和 target_temp_id,引用你创建的节点的 temp_id。
* 可以为 link 添加 meta 字段(JSON 字符串格式)来提供额外信息,例如条件分支的标签。
* 适合并行的测试可以考虑并行,不一定完全串行。
4. **输出格式:** 最终的输出**必须**是一个**完整且有效**的 JSON 对象,严格符合下方指定的 JSON Schema。不要在 JSON 对象前后包含任何额外的文字或解释。
**要求的 JSON 输出 Schema:**
%s
请根据以上信息,生成测试流程图的 JSON:
`, string(rootDemandJSON), string(descendantsJSON), p.toolNodeDefinitionsJSON, graphSchema)
p.logger.Printf("Stage %s: Prompt 已准备。", stageID)
if p.llmIOLogger != nil {
p.llmIOLogger.Printf("StageID: %s\n--- LLM 输入 (Prompt) ---\n%s\n--- Prompt 结束 ---", stageID, prompt)
} else {
p.logger.Printf("Stage %s: LLM I/O logger 未初始化,无法记录 Prompt。", stageID) // fallback 到主 logger
}
// 5. 调用 LLM (严格按照 doc 包的流式风格)
llmCtx, llmCancel := context.WithTimeout(ctx, llmRequestTimeout)
defer llmCancel()
p.logger.Printf("Stage %s: 发送流式请求到 LLM (模型: %s)...", stageID, llmModelName)
// --- 使用 NewStreaming,无 ResponseFormat,单返回值 ---
stream := p.llmClient.Chat.Completions.NewStreaming(llmCtx, openai.ChatCompletionNewParams{
Model: openai.F(llmModelName),
Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
openai.SystemMessage("你是一位专业的软件测试流程设计专家,负责生成 JSON 格式的测试流程图。"), // 可以考虑 system prompt 是否需要
openai.UserMessage(prompt),
}),
// !! 移除 ResponseFormat !!
})
// --- 修正结束 ---
// --- 处理流式响应 (与你的 doc 包一致) ---
var responseBuilder strings.Builder
p.logger.Printf("Stage %s: Reading LLM stream...", stageID)
streamErr := (error)(nil) // 用于接收 stream.Err()
for stream.Next() {
chunk := stream.Current()
// 不再检查 FinishReason,直接拼接
if len(chunk.Choices) > 0 {
responseBuilder.WriteString(chunk.Choices[0].Delta.Content)
}
}
// 在循环结束后检查 stream.Err()
streamErr = stream.Err()
stream.Close() // 确保关闭
if streamErr != nil {
// 检查 context 错误
if errors.Is(streamErr, context.Canceled) {
return fmt.Errorf("LLM stream canceled: %w", streamErr)
}
if errors.Is(streamErr, context.DeadlineExceeded) {
return fmt.Errorf("LLM stream timed out: %w", streamErr)
}
p.logger.Printf("Stage %s: LLM stream error: %v", stageID, streamErr)
return fmt.Errorf("LLM stream error: %w", streamErr)
}
llmOutputRaw := responseBuilder.String()
if llmOutputRaw == "" {
return errors.New("LLM stream returned empty content")
}
p.logger.Printf("Stage %s: LLM stream finished successfully.", stageID)
// --- 流式处理结束 ---
if p.llmIOLogger != nil {
p.llmIOLogger.Printf("StageID: %s\n--- LLM 输出 (Raw) ---\n%s\n--- 输出结束 ---", stageID, llmOutputRaw)
} else {
p.logger.Printf("Stage %s: LLM I/O logger 未初始化,无法记录原始输出。", stageID) // fallback 到主 logger
}
// 6. 清理和验证 LLM 输出
llmOutputJSON, err := p.cleanLLMResponse(llmOutputRaw) // 使用包内方法
if err != nil {
p.logger.Printf("Stage %s: 首次清理 LLM 响应失败: %v。将尝试调用 LLM 进行修复...", stageID, err)
// --- 调用修复逻辑 ---
// 传递原始的 context (或者可以创建一个带新超时的子 context)
repairedJSON, repairErr := p.repairGraphJSON(ctx, llmOutputRaw, err)
if repairErr != nil {
// 修复失败,记录两个错误,然后返回失败
p.logger.Printf("Stage %s: JSON 修复尝试失败: %v。原始清理错误: %v。原始响应: %s", stageID, repairErr, err, llmOutputRaw)
// 将原始错误和修复错误合并返回可能更有信息量
return fmt.Errorf("清理 LLM 响应失败 (%v) 且后续修复尝试也失败 (%v)", err, repairErr)
}
// 修复成功
p.logger.Printf("Stage %s: LLM 自动修复 JSON 成功。", stageID)
llmOutputJSON = repairedJSON // 使用修复后的 JSON 继续后续流程
// --- 修复逻辑结束 ---
}
// 【注意】验证 Schema 的函数需要适配图的 Schema,而不是需求的 Schema
// 假设 validateGraphJSON 是适配过的
if err := p.validateGraphJSON([]byte(llmOutputJSON)); err != nil {
// 【可选】尝试修复
p.logger.Printf("Stage %s: LLM 响应验证失败: %v. JSON: %s", stageID, err, llmOutputJSON)
return fmt.Errorf("LLM 响应验证失败: %w", err)
}
p.logger.Printf("Stage %s: LLM 响应已清理并验证。", stageID)
// 7. 解析 JSON
var llmGraph llmGraphOutput
if err := json.Unmarshal([]byte(llmOutputJSON), &llmGraph); err != nil {
return fmt.Errorf("解析 LLM JSON 失败: %w", err)
}
p.logger.Printf("Stage %s: 解析到 %d 个节点和 %d 个连接。", stageID, len(llmGraph.Nodes), len(llmGraph.Links))
// 8. 处理和转换数据 (数据库模型)
// ... (省略,与上一轮回复中 processTask 内的代码相同:创建 finalNodes, finalLinks, 检查根节点, 映射 ID) ...
finalNodes := make([]stage.GraphNode, 0, len(llmGraph.Nodes))
finalLinks := make([]stage.GraphLink, 0, len(llmGraph.Links))
tempIDToInstanceID := make(map[string]string)
for _, llmNode := range llmGraph.Nodes {
// 【检查】确保 LLM 没有错误地包含根节点
if llmNode.Type == "demand" && llmNode.OriginalDemandID == rootDemandID {
p.logger.Printf("Stage %s: Warning - LLM included root node (DemandID: %s) despite instructions. Skipping it.", stageID, rootDemandID)
continue
}
instanceID := uuid.New().String()[0:32]
if _, exists := tempIDToInstanceID[llmNode.TempID]; exists {
p.logger.Printf("Stage %s: Warning - duplicate temp_id '%s' from LLM, skipping node '%s'.", stageID, llmNode.TempID, llmNode.Name)
continue
}
tempIDToInstanceID[llmNode.TempID] = instanceID
dbNode := stage.GraphNode{
BaseModel: M.BaseModel{ID: uuid.New().String()[0:32]},
StageID: stageID,
InstanceID: instanceID,
Name: llmNode.Name,
Type: llmNode.Type,
Meta: llmNode.Meta,
X: llmNode.X,
Y: llmNode.Y,
FX: llmNode.X, FY: llmNode.Y, Level: 0, ParentID: "",
}
// 设置 DemandID
if llmNode.Type == "demand" {
if llmNode.OriginalDemandID != "" {
dbNode.DemandID = llmNode.OriginalDemandID
} else {
p.logger.Printf("Stage %s: Warning - LLM demand node '%s' (temp_id: %s) missing original_demand_id.", stageID, llmNode.Name, llmNode.TempID)
dbNode.DemandID = "" // 明确设为空
}
} else {
// 工具节点关联到 Stage 的根需求 ID
dbNode.DemandID = rootDemandID
}
finalNodes = append(finalNodes, dbNode)
}
// 处理 Links
for _, llmLink := range llmGraph.Links {
sourceInstanceID, sourceOk := tempIDToInstanceID[llmLink.SourceTempID]
targetInstanceID, targetOk := tempIDToInstanceID[llmLink.TargetTempID]
if !sourceOk || !targetOk || sourceInstanceID == targetInstanceID {
p.logger.Printf("Stage %s: Warning - Skipping invalid link from LLM: %s -> %s", stageID, llmLink.SourceTempID, llmLink.TargetTempID)
continue
}
dbLink := stage.GraphLink{
BaseModel: M.BaseModel{ID: uuid.New().String()[0:32]},
StageID: stageID,
SourceID: sourceInstanceID,
TargetID: targetInstanceID,
LinkID: uuid.New().String()[0:32],
Meta: llmLink.Meta,
}
finalLinks = append(finalLinks, dbLink)
}
p.logger.Printf("Stage %s: 处理完成 %d 个节点和 %d 个连接。", stageID, len(finalNodes), len(finalLinks))
// 9. 保存到数据库 (事务)
tx := p.db.Begin()
if tx.Error != nil {
return fmt.Errorf("启动数据库事务失败: %w", tx.Error)
}
p.logger.Printf("Stage %s: 事务已启动。", stageID)
err = func(tx *gorm.DB) error { // 事务内操作
if err := tx.Where("stage_id = ?", stageID).Delete(&stage.GraphNode{}).Error; err != nil {
return fmt.Errorf("删除旧节点失败: %w", err)
}
if err := tx.Where("stage_id = ?", stageID).Delete(&stage.GraphLink{}).Error; err != nil {
return fmt.Errorf("删除旧连接失败: %w", err)
}
if len(finalNodes) > 0 {
if err := tx.Create(&finalNodes).Error; err != nil {
return fmt.Errorf("创建新节点失败: %w", err)
}
}
if len(finalLinks) > 0 {
if err := tx.Create(&finalLinks).Error; err != nil {
return fmt.Errorf("创建新连接失败: %w", err)
}
}
p.logger.Printf("Stage %s: 事务内数据操作完成。", stageID)
return nil
}(tx)
if err != nil {
tx.Rollback()
p.logger.Printf("Stage %s: 事务已回滚: %v", stageID, err)
return err
}
if err := tx.Commit().Error; err != nil {
p.logger.Printf("Stage %s: 事务提交失败: %v", stageID, err)
return fmt.Errorf("数据库事务提交失败: %w", err)
}
p.logger.Printf("Stage %s: 事务已成功提交。", stageID)
return nil // 任务成功
}
func (p *GraphProcessor) repairGraphJSON(ctx context.Context, malformedJSON string, originalError error) (string, error) {
p.logger.Printf("Stage (unknown): Attempting to repair JSON with LLM. Original Error: %v", originalError)
// --- 修正 Prompt 定义 ---
// 使用 Go 的原始字符串字面量 ``
// 将内部的 (如 ```json) 修改为 (例如 "```json") 或其他不含反引号的描述
repairPrompt := fmt.Sprintf(`你是一个 JSON 修复专家。我收到了一个来自 LLM 的响应,但它似乎不是一个有效的 JSON 字符串,导致了解析或清理错误。
原始的、格式可能错误的响应文本:
---
%s
---
遇到的错误信息:
%v
请仔细检查上面的原始响应文本,并尽力将其修复成一个结构良好、语法正确的 JSON 对象。
修复要求:
1. 确保所有键名和字符串值都使用双引号。
2. 确保括号、逗号、冒号等都使用正确。
3. 尽量保留原始数据和结构,只修正格式错误。
4. 最终结果必须是一个单独的、完整的 JSON 对象。
请只返回修复后的 JSON 字符串,不要包含其他无关文本。`, malformedJSON, originalError) // <-- 修改了这里
// --- 修正结束 ---
repairCtx, cancel := context.WithTimeout(ctx, 60*time.Second) // 为修复设置一个独立的超时
defer cancel()
// 使用流式调用进行修复 (与你的 doc 包类似)
stream := p.llmClient.Chat.Completions.NewStreaming(repairCtx, openai.ChatCompletionNewParams{
Model: openai.F(llmModelName), // 可以考虑用更强大的模型进行修复?
Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
openai.UserMessage(repairPrompt),
}),
// Temperature 设低一点可能有助于精确修复? (例如 0.3)
// Temperature: openai.F(float32(0.3)),
})
var responseBuilder strings.Builder
streamErr := (error)(nil)
for stream.Next() {
chunk := stream.Current()
if len(chunk.Choices) > 0 {
responseBuilder.WriteString(chunk.Choices[0].Delta.Content)
}
}
streamErr = stream.Err()
stream.Close()
if streamErr != nil {
p.logger.Printf("Stage (unknown): JSON 修复 LLM 调用失败: %v", streamErr)
return "", fmt.Errorf("调用 LLM 修复 JSON 失败: %w", streamErr)
}
fixedJSON := responseBuilder.String()
if fixedJSON == "" {
p.logger.Println("Stage (unknown): LLM 返回了空的修复结果。")
return "", errors.New("LLM 返回了空的修复结果")
}
p.logger.Println("Stage (unknown): LLM 返回了修复后的 JSON,将进行基本验证。")
// 对修复后的结果进行最基本的清理和验证
// 这里可以复用部分的 cleanLLMResponse 逻辑,或者简化验证
fixedJSON = strings.TrimSpace(fixedJSON)
// 移除可能的 ```json ```
if strings.HasPrefix(fixedJSON, "```json") {
fixedJSON = strings.TrimPrefix(fixedJSON, "```json")
}
if strings.HasPrefix(fixedJSON, "```") {
fixedJSON = strings.TrimPrefix(fixedJSON, "```")
}
if strings.HasSuffix(fixedJSON, "```") {
fixedJSON = strings.TrimSuffix(fixedJSON, "```")
}
fixedJSON = strings.TrimSpace(fixedJSON)
if !strings.HasPrefix(fixedJSON, "{") || !strings.HasSuffix(fixedJSON, "}") {
p.logger.Printf("Stage (unknown): 修复后的 JSON 仍然缺少对象边界: %s", fixedJSON)
return "", fmt.Errorf("修复后的 JSON 仍然缺少对象边界 '{...}'")
}
var checker interface{}
if err := json.Unmarshal([]byte(fixedJSON), &checker); err != nil {
p.logger.Printf("Stage (unknown): 修复后的 JSON 仍然无效: %v. Repaired: %s", err, fixedJSON)
return "", fmt.Errorf("修复后的 JSON 仍然无法解析: %w", err)
}
p.logger.Println("Stage (unknown): 修复后的 JSON 通过了基本验证。")
return fixedJSON, nil // 返回修复结果或错误
}
// --- 包内辅助函数 ---
func (p *GraphProcessor) generateGraphJSONSchema() (string, error) {
// ... (省略 Schema 字符串,同上) ...
schemaStr := `{
"type": "object",
"properties": {
"nodes": {
"type": "array",
"description": "List of graph nodes",
"items": { "$ref": "#/definitions/graphNode" }
},
"links": {
"type": "array",
"description": "List of graph links connecting nodes",
"items": { "$ref": "#/definitions/graphLink" }
}
},
"required": ["nodes", "links"],
"definitions": {
"graphNode": {
"type": "object",
"properties": {
"temp_id": { "type": "string", "description": "Unique temporary string ID for this node within the response" },
"name": { "type": "string", "description": "Display name of the node" },
"type": { "type": "string", "description": "Type of the node (e.g., 'start', 'end', 'demand', 'httpRequest', 'condition')" },
"original_demand_id": { "type": "string", "description": "The database ID of the original demand node if type is 'demand', otherwise null or empty" },
"meta": { "type": "string", "description": "JSON string containing node-specific metadata (e.g., '{\"method\":\"GET\"}' for httpRequest)" },
"x": { "type": "number", "description": "Suggested X coordinate for layout" },
"y": { "type": "number", "description": "Suggested Y coordinate for layout" }
},
"required": ["temp_id", "name", "type", "meta", "x", "y"]
},
"graphLink": {
"type": "object",
"properties": {
"source_temp_id": { "type": "string", "description": "Temporary ID of the source node" },
"target_temp_id": { "type": "string", "description": "Temporary ID of the target node" },
"meta": { "type": "string", "description": "Optional JSON string for link metadata (e.g., condition label)" }
},
"required": ["source_temp_id", "target_temp_id"]
}
}
}`
var testSchema interface{}
if err := json.Unmarshal([]byte(schemaStr), &testSchema); err != nil {
return "", fmt.Errorf("内部错误: 图 JSON schema 无效: %v", err)
}
prettySchemaBytes, _ := json.MarshalIndent(testSchema, "", " ")
return string(prettySchemaBytes), nil
}
func (p *GraphProcessor) validateGraphJSON(jsonData []byte) error {
// ... (省略验证逻辑,同上) ...
schemaStr, err := p.generateGraphJSONSchema()
if err != nil {
return fmt.Errorf("无法生成图 Schema 用于验证: %v", err)
}
schemaLoader := gojsonschema.NewStringLoader(schemaStr)
documentLoader := gojsonschema.NewBytesLoader(jsonData)
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
return fmt.Errorf("JSON Schema 验证器错误: %v", err)
}
if !result.Valid() { /* ... 构造错误信息 ... */
return fmt.Errorf("LLM 输出未通过 JSON Schema 验证: ...")
}
return nil
}
func (p *GraphProcessor) cleanLLMResponse(response string) (string, error) {
// ... (省略清理逻辑,同上) ...
response = strings.TrimSpace(response)
// ... (移除 ```json, ``` 等) ...
if !strings.HasPrefix(response, "{") || !strings.HasSuffix(response, "}") {
start := strings.Index(response, "{")
end := strings.LastIndex(response, "}")
if start == -1 || end == -1 || start >= end {
return "", fmt.Errorf("无法在响应中找到有效的 JSON 对象边界 '{...}'")
}
response = response[start : end+1]
}
var checker interface{}
if err := json.Unmarshal([]byte(response), &checker); err != nil {
return "", fmt.Errorf("清理后的内容不是有效的 JSON: %w", err)
}
return response, nil
}
// 确保 llmGraphNode, llmGraphLink, llmGraphOutput 定义完整
type llmGraphNode struct {
TempID string `json:"temp_id"`
Name string `json:"name"`
Type string `json:"type"`
Meta string `json:"meta"`
OriginalDemandID string `json:"original_demand_id,omitempty"`
X float64 `json:"x"`
Y float64 `json:"y"`
}
type llmGraphLink struct {
SourceTempID string `json:"source_temp_id"`
TargetTempID string `json:"target_temp_id"`
Meta string `json:"meta,omitempty"`
}
type llmGraphOutput struct {
Nodes []llmGraphNode `json:"nodes"`
Links []llmGraphLink `json:"links"`
}
+242
View File
@@ -0,0 +1,242 @@
package graph_link
import (
"app/cfg"
"app/models/project/stage"
"app/utils"
"encoding/json"
"errors"
"fmt"
"io"
"github.com/google/uuid"
"github.com/veypi/OneBD/rest"
"gorm.io/gorm"
)
var _ = Router.Get("/:id/", getHandle) //TODO
var _ = Router.Get("", getHandle)
func getHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
stageID, ok := x.Params.Get("id")
if !ok {
return nil, errors.New("缺少stage_id")
}
var links []stage.GraphLink
if err := cfg.DB().Where("stage_id = ?", stageID).Find(&links).Error; err != nil {
return nil, err
}
return links, nil
}
// 获取单个连接详情
func getOneHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
id, ok := x.Params.Get("id")
if !ok {
return nil, errors.New("缺少id")
}
var link stage.GraphLink
if err := cfg.DB().Where("id = ?", id).First(&link).Error; err != nil {
return nil, err
}
sourceNode, _ := link.SourceNode(cfg.DB())
targetNode, _ := link.TargetNode(cfg.DB())
return map[string]interface{}{
"link": link,
"source_node": sourceNode,
"target_node": targetNode,
}, nil
}
var _ = Router.Post("", postHandle)
// 创建图形连接
func postHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
link := &stage.GraphLink{}
if err := json.NewDecoder(x.Request.Body).Decode(&link); err != nil {
return nil, err
}
link.ID = uuid.New().String()[0:32]
// 如果没有提供LinkID,则生成一个
if link.LinkID == "" {
link.LinkID = fmt.Sprintf("%s-%s", link.SourceID, link.TargetID)
}
// 验证源节点是否存在
var sourceNode stage.GraphNode
if err := cfg.DB().Where("instance_id = ?", link.SourceID).First(&sourceNode).Error; err != nil {
return nil, errors.New("源节点不存在")
}
// 验证目标节点是否存在
var targetNode stage.GraphNode
if err := cfg.DB().Where("instance_id = ?", link.TargetID).First(&targetNode).Error; err != nil {
return nil, errors.New("目标节点不存在")
}
// 验证两个节点是否在同一个阶段
if sourceNode.StageID != targetNode.StageID {
return nil, errors.New("源节点和目标节点必须在同一个阶段")
}
// 设置阶段ID
link.StageID = sourceNode.StageID
// 验证是否已存在相同的连接
var count int64
if err := cfg.DB().Model(&stage.GraphLink{}).Where("source_id = ? AND target_id = ?", link.SourceID, link.TargetID).Count(&count).Error; err != nil {
return nil, err
}
if count > 0 {
return nil, errors.New("已存在相同的连接")
}
if err := cfg.DB().Create(&link).Error; err != nil {
return nil, err
}
return link, nil
}
var _ = Router.Patch("/:id/", patchHandle)
// 更新图形连接
func patchHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
// 从URL获取ID
body, err := io.ReadAll(x.Request.Body)
if err != nil {
return nil, fmt.Errorf("读取请求体失败: %v", err)
}
id, ok := x.Params.Get("id")
if !ok {
return nil, errors.New("缺少id")
}
var updateMap map[string]interface{}
if err := json.Unmarshal(body, &updateMap); err != nil {
return nil, fmt.Errorf("解析JSON失败: %v", err)
}
// 先检查连接是否存在
var link stage.GraphLink
if err := cfg.DB().Where("id = ?", id).First(&link).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("连接不存在")
}
return nil, err
}
// 如果更新包含source_id或target_id,需要验证节点是否存在和是否在同一阶段
sourceID, hasSourceID := updateMap["source_id"]
targetID, hasTargetID := updateMap["target_id"]
if hasSourceID || hasTargetID {
newSourceID := link.SourceID
newTargetID := link.TargetID
if hasSourceID {
var ok bool
newSourceID, ok = sourceID.(string)
if !ok {
return nil, errors.New("source_id 必须是字符串类型")
}
}
if hasTargetID {
var ok bool
newTargetID, ok = targetID.(string)
if !ok {
return nil, errors.New("target_id 必须是字符串类型")
}
}
// 验证源节点是否存在
var sourceNode stage.GraphNode
if err := cfg.DB().Where("instance_id = ?", newSourceID).First(&sourceNode).Error; err != nil {
return nil, errors.New("源节点不存在")
}
// 验证目标节点是否存在
var targetNode stage.GraphNode
if err := cfg.DB().Where("instance_id = ?", newTargetID).First(&targetNode).Error; err != nil {
return nil, errors.New("目标节点不存在")
}
// 验证两个节点是否在同一个阶段
if sourceNode.StageID != targetNode.StageID {
return nil, errors.New("源节点和目标节点必须在同一个阶段")
}
// 验证是否已存在相同的连接
var count int64
if err := cfg.DB().Model(&stage.GraphLink{}).Where("id != ? AND source_id = ? AND target_id = ?", id, newSourceID, newTargetID).Count(&count).Error; err != nil {
return nil, err
}
if count > 0 {
return nil, errors.New("已存在相同的连接")
}
// 更新阶段ID
updateMap["stage_id"] = sourceNode.StageID
// 更新LinkID
updateMap["link_id"] = fmt.Sprintf("%s-%s", newSourceID, newTargetID)
}
// 删除不允许更新的字段
delete(updateMap, "id")
// 只更新提供的字段
if len(updateMap) > 0 {
if err := cfg.DB().Model(&link).Updates(updateMap).Error; err != nil {
return nil, err
}
}
// 重新查询更新后的完整数据
if err := cfg.DB().Where("id = ?", id).First(&link).Error; err != nil {
return nil, err
}
return link, nil
}
var _ = Router.Delete("/:id/", deleteHandle)
// 删除图形连接
func deleteHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
id, ok := x.Params.Get("id")
if !ok {
return nil, errors.New("缺少id")
}
var link stage.GraphLink
if err := cfg.DB().Where("id = ?", id).First(&link).Error; err != nil {
return nil, err
}
// 删除连接
if err := cfg.DB().Delete(&link).Error; err != nil {
return nil, err
}
return map[string]interface{}{
"msg": "删除成功",
}, nil
}
var _ = Router.Any("/*", anyHandle)
func anyHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
return nil, nil
}
+7
View File
@@ -0,0 +1,7 @@
package graph_link
import (
"github.com/veypi/OneBD/rest"
)
var Router = rest.NewRouter()
+251
View File
@@ -0,0 +1,251 @@
package graph_node
import (
"app/cfg"
"app/models"
"app/models/project"
"app/models/project/stage"
"app/utils"
"encoding/json"
"errors"
"fmt"
"io"
"github.com/google/uuid"
"github.com/veypi/OneBD/rest"
"gorm.io/gorm"
)
var _ = Router.Get("/:id/", getHandle) //TODO第一个命名生效
var _ = Router.Get("", getHandle)
func getHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
stageID, ok := x.Params.Get("id")
if !ok {
return nil, errors.New("缺少stage_id")
}
var nodes []stage.GraphNode
if err := cfg.DB().Where("stage_id = ?", stageID).Find(&nodes).Error; err != nil {
return nil, err
}
return nodes, nil
}
// 获取单个节点详情
func getOneHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
id, ok := x.Params.Get("id")
if !ok {
return nil, errors.New("缺少id")
}
var node stage.GraphNode
if err := cfg.DB().Where("id = ?", id).First(&node).Error; err != nil {
return nil, err
}
tree, _ := node.Tree(cfg.DB())
links, _ := node.Links(cfg.DB())
return map[string]interface{}{
"node": node,
"tree": tree,
"links": links,
}, nil
}
var _ = Router.Post("", postHandle)
// 创建图形节点
func postHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
node := &stage.GraphNode{}
if err := json.NewDecoder(x.Request.Body).Decode(&node); err != nil {
return nil, err
}
node.ID = uuid.New().String()[0:32]
// 验证树节点是否存在
var tree project.Demand
if err := cfg.DB().Where("id = ?", node.DemandID).First(&tree).Error; err != nil {
return nil, errors.New("树节点不存在")
}
// 验证阶段是否存在
var stage project.Stage
if err := cfg.DB().Where("id = ?", node.StageID).First(&stage).Error; err != nil {
return nil, errors.New("stage不存在")
}
// 验证树节点是否是阶段指定树节点的子孙节点
if stage.DemandID != "" && stage.DemandID != "root" {
var stageTree models.Demand
if err := cfg.DB().Where("id = ?", stage.DemandID).First(&stageTree).Error; err != nil {
return nil, errors.New("阶段指定的树节点不存在")
}
// 获取阶段树节点的所有子孙节点
descendants, err := stageTree.Descendants(cfg.DB())
if err != nil {
return nil, err
}
// 检查当前树节点是否是阶段树节点或其子孙节点
isValid := tree.ID == stageTree.ID
for _, d := range descendants {
if tree.ID == d.ID {
isValid = true
break
}
}
if !isValid {
return nil, errors.New("树节点不满足阶段要求")
}
}
if err := cfg.DB().Create(&node).Error; err != nil {
return nil, err
}
return node, nil
}
var _ = Router.Patch("/:id/", patchHandle)
// 更新图形节点
func patchHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
// 从URL获取ID
body, err := io.ReadAll(x.Request.Body)
if err != nil {
return nil, fmt.Errorf("读取请求体失败: %v", err)
}
id, ok := x.Params.Get("id")
if !ok {
return nil, errors.New("缺少id")
}
var updateMap map[string]interface{}
if err := json.Unmarshal(body, &updateMap); err != nil {
return nil, fmt.Errorf("解析JSON失败: %v", err)
}
// 先检查节点是否存在
var node stage.GraphNode
if err := cfg.DB().Where("id = ?", id).First(&node).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("节点不存在")
}
return nil, err
}
// 如果更新包含tree_id,需要验证树节点是否存在和是否符合阶段要求
if treeID, ok := updateMap["tree_id"]; ok {
newDemandID, ok := treeID.(string)
if !ok {
return nil, errors.New("tree_id 必须是字符串类型")
}
var tree models.Tree
if err := cfg.DB().Where("id = ?", newDemandID).First(&tree).Error; err != nil {
return nil, errors.New("树节点不存在")
}
var stage models.Stage
if err := cfg.DB().Where("id = ?", node.StageID).First(&stage).Error; err != nil {
return nil, errors.New("阶段不存在")
}
// 验证树节点是否是阶段指定树节点的子孙节点
if stage.DemandID != "" && stage.DemandID != "root" {
var stageTree models.Tree
if err := cfg.DB().Where("id = ?", stage.DemandID).First(&stageTree).Error; err != nil {
return nil, errors.New("阶段指定的树节点不存在")
}
// 获取阶段树节点的所有子孙节点
descendants, err := stageTree.Descendants(cfg.DB())
if err != nil {
return nil, err
}
// 检查当前树节点是否是阶段树节点或其子孙节点
isValid := tree.ID == stageTree.ID
for _, d := range descendants {
if tree.ID == d.ID {
isValid = true
break
}
}
if !isValid {
return nil, errors.New("树节点不满足阶段要求")
}
}
}
// 删除不允许更新的字段
delete(updateMap, "id")
// 只更新提供的字段
if len(updateMap) > 0 {
if err := cfg.DB().Model(&node).Updates(updateMap).Error; err != nil {
return nil, err
}
}
// 重新查询更新后的完整数据
if err := cfg.DB().Where("id = ?", id).First(&node).Error; err != nil {
return nil, err
}
return node, nil
}
var _ = Router.Delete("/:id/", deleteHandle)
// 删除图形节点
func deleteHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
id, ok := x.Params.Get("id")
if !ok {
return nil, errors.New("缺少id")
}
var node stage.GraphNode
if err := cfg.DB().Where("id = ?", id).First(&node).Error; err != nil {
return nil, err
}
// 开始事务
tx := cfg.DB().Begin()
// 删除与该节点关联的所有连接
if err := tx.Where("prev_id = ? OR next_id = ?", id, id).Delete(&stage.GraphLink{}).Error; err != nil {
tx.Rollback()
return nil, err
}
// 删除节点
if err := tx.Delete(&node).Error; err != nil {
tx.Rollback()
return nil, err
}
tx.Commit()
return map[string]interface{}{
"msg": "删除成功",
}, nil
}
var _ = Router.Any("/*", anyHandle)
func anyHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
return nil, nil
}
+7
View File
@@ -0,0 +1,7 @@
package graph_node
import (
"github.com/veypi/OneBD/rest"
)
var Router = rest.NewRouter()
+229
View File
@@ -0,0 +1,229 @@
package stage
import (
"app/cfg"
"app/models/project"
"app/models/project/stage"
"app/utils"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"github.com/google/uuid"
"github.com/veypi/OneBD/rest"
"gorm.io/gorm"
)
var _ = Router.Get("/:project_id/", getHandle)
var _ = Router.Get("/:project_id/:id", getHandle)
var _ = Router.Get("", getHandle)
// 获取阶段
func getHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
project_id, project_ok := x.Params.Get("project_id")
id, ok := x.Params.Get("id")
if !project_ok {
return nil, errors.New("缺少project_id")
}
if !ok {
// 获取所有阶段
var stages []project.Stage
if err := cfg.DB().Where("project_id = ?", project_id).Find(&stages).Error; err != nil {
return nil, err
}
return stages, nil
}
// 获取特定阶段
var stage project.Stage
if err := cfg.DB().Where("id = ? and project_id =?", id, project_id).First(&stage).Error; err != nil {
return nil, err
}
// 获取可用的树节点
availableNodes, _ := stage.AvailableNodes(cfg.DB())
var res = map[string]interface{}{
"stage": stage,
"available_nodes": availableNodes,
}
return res, nil
}
var _ = Router.Post("", postHandle)
// 创建阶段
func postHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
stage := &project.Stage{}
if err := json.NewDecoder(x.Request.Body).Decode(&stage); err != nil {
return nil, err
}
stage.ID = uuid.New().String()[0:32]
if err := cfg.DB().Create(&stage).Error; err != nil {
return nil, err
}
return stage, nil
}
var _ = Router.Patch("/:project_id/:id", patchHandle)
// TODO
// 更新阶段
func patchHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
// 从URL获取ID
body, err := io.ReadAll(x.Request.Body)
if err != nil {
return nil, fmt.Errorf("读取请求体失败: %v", err)
}
id, ok := x.Params.Get("id")
if !ok {
return nil, errors.New("缺少id")
}
var updateMap map[string]interface{}
if err := json.Unmarshal(body, &updateMap); err != nil {
return nil, fmt.Errorf("解析JSON失败: %v", err)
}
// 先检查阶段是否存在
var stage project.Stage
if err := cfg.DB().Where("id = ?", id).First(&stage).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("阶段不存在")
}
return nil, err
}
// 删除不允许更新的字段
delete(updateMap, "id")
// 只更新提供的字段
if len(updateMap) > 0 {
if err := cfg.DB().Model(&stage).Updates(updateMap).Error; err != nil {
return nil, err
}
}
// 重新查询更新后的完整数据
if err := cfg.DB().Where("id = ?", id).First(&stage).Error; err != nil {
return nil, err
}
return stage, nil
}
var _ = Router.Delete("/:project_id/:id", deleteHandle)
// todo
// 删除阶段
func deleteHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
id, ok := x.Params.Get("id")
if !ok {
return nil, errors.New("缺少id")
}
var stageOBJ project.Stage
if err := cfg.DB().Where("id = ?", id).First(&stageOBJ).Error; err != nil {
return nil, err
}
// 开始事务
tx := cfg.DB().Begin()
// 删除与该阶段关联的所有图形节点和连接
if err := tx.Where("stage_id = ?", id).Delete(&stage.GraphNode{}).Error; err != nil {
tx.Rollback()
return nil, err
}
if err := tx.Where("stage_id = ?", id).Delete(&stage.GraphLink{}).Error; err != nil {
tx.Rollback()
return nil, err
}
// 删除阶段
if err := tx.Delete(&stageOBJ).Error; err != nil {
tx.Rollback()
return nil, err
}
tx.Commit()
return map[string]interface{}{
"msg": "删除成功",
}, nil
}
var _ = Router.Post("/:project_id/:id/generate", generateGraphHandle)
func generateGraphHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
log.Println("generateGraphHandle called (async trigger)")
stageID, ok := x.Params.Get("id")
if !ok {
log.Println("错误: 请求路径缺少 'stage_id'")
return nil, errors.New("请求路径缺少 'stage_id'")
}
log.Printf("收到图生成请求 (StageID: %s)", stageID)
// --- 基本验证 ---
db := cfg.DB()
if db == nil {
log.Println("错误: 数据库连接不可用")
return nil, errors.New("服务器内部错误 (数据库不可用)")
}
var stageObj project.Stage
err := db.Select("id, demand_id, graph_status").Where("id = ?", stageID).First(&stageObj).Error
if err != nil { /* ... 错误处理,返回 404 或 500 ... */
}
if stageObj.DemandID == "" { /* ... 错误处理,返回 400 ... */
}
if stageObj.GraphStatus == "processing" { /* ... 返回任务进行中 ... */
}
// --- 更新状态为 pending ---
updateData := map[string]interface{}{"graph_status": "pending", "graph_error": ""}
if err := db.Model(&project.Stage{}).Where("id = ?", stageID).Updates(updateData).Error; err != nil {
log.Printf("警告: 更新阶段状态为 pending 失败 (StageID %s): %v", stageID, err)
}
// --- 创建任务并添加到队列 ---
task := graphGenerationTask{StageID: stageID}
processor, err := GetGraphProcessor() // 获取同包内的单例
if err != nil {
log.Printf("错误: 无法获取 GraphProcessor: %v", err)
return nil, errors.New("服务器内部错误 (处理器未就绪)")
}
if err := processor.AddTask(task); err != nil {
log.Printf("添加任务失败 (StageID %s): %v", stageID, err)
return nil, fmt.Errorf("无法添加任务到队列: %w", err)
}
log.Printf("任务已添加 (StageID %s).", stageID)
// --- 返回 202 Accepted 对应的消息 ---
return map[string]string{
"message": fmt.Sprintf("图生成任务已加入队列 (Stage ID: %s)", stageID),
"stage_id": stageID,
"status": "pending",
}, nil
}
var _ = Router.Any("/*", anyHandle)
func anyHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
return nil, nil
}
+7
View File
@@ -0,0 +1,7 @@
package stage
import (
"github.com/veypi/OneBD/rest"
)
var Router = rest.NewRouter()
+108
View File
@@ -0,0 +1,108 @@
package endpoint
import (
"app/cfg"
"app/utils"
"encoding/json"
"github.com/veypi/OneBD/rest"
M "app/models/project/stage/tools"
)
var _ = Router.Post("", postHandle)
func postHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
var endpoint M.Endpoint
if err := json.NewDecoder(x.Request.Body).Decode(&endpoint); err != nil {
return nil, err
}
if err := cfg.DB().Create(&endpoint).Error; err != nil {
return nil, err
}
return endpoint, nil
}
var _ = Router.Get("/:id/", getDetail)
func getDetail(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
id, _ := x.Params.Get("id")
var endpoint M.Endpoint
if err := cfg.DB().First(&endpoint, "id = ?", id).Error; err != nil {
return nil, rest.ErrNotFound
}
return endpoint, nil
}
var _ = Router.Get("/list/:project_id/:stage_id/", getList)
func getList(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
var endpoints []M.Endpoint
stageID, stageOK := x.Params.Get("stage_id")
projectID, projectOK := x.Params.Get("project_id")
db := cfg.DB()
if stageOK {
db = db.Where("stage_id = ?", stageID)
}
if projectOK {
db = db.Where("project_id = ?", projectID)
}
if err := db.Find(&endpoints).Error; err != nil {
return nil, err
}
return endpoints, nil
}
var _ = Router.Patch("/:id/", patchUpdate)
func patchUpdate(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
id, _ := x.Params.Get("id")
var endpoint M.Endpoint
if err := cfg.DB().First(&endpoint, "id = ?", id).Error; err != nil {
return nil, rest.ErrNotFound
}
if err := json.NewDecoder(x.Request.Body).Decode(&endpoint); err != nil {
return nil, err
}
if err := cfg.DB().Save(&endpoint).Error; err != nil {
return nil, err
}
return endpoint, nil
}
var _ = Router.Delete("/:id/", deleteHandle)
func deleteHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
id, _ := x.Params.Get("id")
var endpoint M.Endpoint
if err := cfg.DB().First(&endpoint, "id = ?", id).Error; err != nil {
return nil, rest.ErrNotFound
}
if err := cfg.DB().Delete(&endpoint).Error; err != nil {
return nil, err
}
return map[string]interface{}{
"msg": "删除成功",
}, nil
}
var _ = Router.Any("/*", anyHandle)
func anyHandle(x *rest.X) (any, error) {
utils.SetCORSHeaders(x)
return nil, nil
}
+7
View File
@@ -0,0 +1,7 @@
package endpoint
import (
"github.com/veypi/OneBD/rest"
)
var Router = rest.NewRouter()
+7
View File
@@ -0,0 +1,7 @@
package tools
import (
"github.com/veypi/OneBD/rest"
)
var Router = rest.NewRouter()