| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- package main
-
- import (
- "bufio"
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "strings"
- "testing"
- "time"
-
- "git.x2erp.com/qdy/go-svc-code/internal/opencode"
- )
-
- const (
- // 外部已启动的 OpenCode 服务端口
- externalOpenCodePort = 8787
- )
-
- // TestOpenCodeSDK 测试 SDK 模式调用 OpenCode
- func TestOpenCodeSDK(t *testing.T) {
- t.Log("🚀 开始测试 SDK 模式调用 OpenCode")
- t.Logf("外部 OpenCode 端口: %d", externalOpenCodePort)
-
- // 1. 创建客户端
- client, err := opencode.NewDirectClient(externalOpenCodePort)
- if err != nil {
- t.Fatalf("❌ 创建 OpenCode 客户端失败: %v", err)
- }
- t.Logf("✅ 创建 OpenCode 客户端成功,基础URL: %s", client.GetBaseURL())
-
- // 2. 创建会话
- ctx := context.Background()
- sessionTitle := "SDK测试会话"
- session, err := client.CreateSession(ctx, sessionTitle)
- if err != nil {
- t.Fatalf("❌ 创建会话失败: %v", err)
- }
- t.Logf("✅ 创建会话成功,ID: %s", session.ID)
-
- // 3. 发送同步提示词
- prompt := &opencode.PromptRequest{
- Parts: []opencode.TextPart{
- {Type: "text", Text: "Hello, this is SDK test. Please reply with a short message."},
- },
- }
-
- response, err := client.SendPrompt(ctx, session.ID, prompt)
- if err != nil {
- t.Fatalf("❌ 发送同步提示词失败: %v", err)
- }
- t.Logf("✅ 收到同步响应:")
- t.Logf(" - 消息ID: %s", response.Info.ID)
- t.Logf(" - 模型: %s (%s)", response.Info.ModelID, response.Info.ProviderID)
- t.Logf(" - 角色: %s", response.Info.Role)
- t.Logf(" - Token使用: 输入=%d, 输出=%d", response.Info.Tokens.Input, response.Info.Tokens.Output)
-
- // 4. 发送流式提示词(尝试)
- t.Logf("\n🔍 尝试流式提示词...")
- streamPrompt := &opencode.PromptRequest{
- Parts: []opencode.TextPart{
- {Type: "text", Text: "Tell me a short joke about programming."},
- },
- }
-
- streamCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
- defer cancel()
-
- ch, err := client.SendPromptStream(streamCtx, session.ID, streamPrompt)
- if err != nil {
- t.Logf("⚠️ 流式提示词失败: %v", err)
- t.Logf(" 注意: 异步端点可能返回 204 No Content,这是已知问题")
- t.Logf(" 当前状态: 同步端点 ✅ 工作 | 异步端点 ⚠️ 可能未配置")
- } else {
- t.Logf("✅ 开始接收流式响应...")
- var streamParts []string
- for data := range ch {
- streamParts = append(streamParts, data)
- t.Logf(" 📥 收到流式数据: %s", data[:minIntExt(100, len(data))])
- }
- if len(streamParts) > 0 {
- t.Logf("✅ 收到 %d 个流式响应片段", len(streamParts))
- } else {
- t.Logf("⚠️ 流式响应为空")
- }
- }
-
- // 5. 获取会话信息
- retrievedSession, err := client.GetSession(ctx, session.ID)
- if err != nil {
- t.Logf("⚠️ 获取会话信息失败: %v", err)
- } else {
- t.Logf("✅ 获取会话信息成功:")
- t.Logf(" - ID: %s", retrievedSession.ID)
- t.Logf(" - Title: %s", retrievedSession.Title)
- }
-
- // 6. 测试结果总结
- t.Logf("\n📋 SDK 模式测试结果:")
- t.Logf(" ✅ 客户端创建: 正常")
- t.Logf(" ✅ 会话管理: 正常")
- t.Logf(" ✅ 同步对话: 正常 (使用模型: %s)", response.Info.ModelID)
- t.Logf(" ⚠️ 流式对话: %s", "异步端点可能未配置 (返回 204)")
- t.Logf(" ✅ 外部 OpenCode: 端口 %d 可用", externalOpenCodePort)
- }
-
- // TestOpenCodeDirectAPI 测试直接 API 调用 OpenCode
- func TestOpenCodeDirectAPI(t *testing.T) {
- t.Log("🚀 开始测试直接 API 调用 OpenCode")
- t.Logf("外部 OpenCode 端口: %d", externalOpenCodePort)
-
- baseURL := fmt.Sprintf("http://127.0.0.1:%d", externalOpenCodePort)
-
- // 1. 检查健康状态
- healthURL := baseURL + "/global/health"
- resp, err := http.Get(healthURL)
- if err != nil {
- t.Fatalf("❌ 连接 OpenCode 失败: %v", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- t.Fatalf("❌ OpenCode 不健康,状态码: %d", resp.StatusCode)
- }
- t.Logf("✅ OpenCode 健康检查通过")
-
- // 2. 创建会话
- sessionURL := baseURL + "/session"
- sessionData := map[string]interface{}{
- "title": "直接API测试会话",
- }
- sessionJSON, _ := json.Marshal(sessionData)
-
- sessionResp, err := http.Post(sessionURL, "application/json", bytes.NewBuffer(sessionJSON))
- if err != nil {
- t.Fatalf("❌ 创建会话请求失败: %v", err)
- }
- defer sessionResp.Body.Close()
-
- if sessionResp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(sessionResp.Body)
- t.Fatalf("❌ 创建会话失败,状态码: %d, 响应: %s", sessionResp.StatusCode, string(body))
- }
-
- var sessionResult map[string]interface{}
- body, _ := io.ReadAll(sessionResp.Body)
- if err := json.Unmarshal(body, &sessionResult); err != nil {
- t.Fatalf("❌ 解析会话响应失败: %v", err)
- }
-
- sessionID, ok := sessionResult["id"].(string)
- if !ok {
- t.Fatalf("❌ 无法获取会话ID")
- }
- t.Logf("✅ 创建会话成功,ID: %s", sessionID)
-
- // 3. 发送同步提示词
- promptURL := fmt.Sprintf("%s/session/%s/prompt", baseURL, sessionID)
- promptData := map[string]interface{}{
- "parts": []map[string]string{
- {"type": "text", "text": "Hello, this is direct API test. Please reply with a short message."},
- },
- }
- promptJSON, _ := json.Marshal(promptData)
-
- promptResp, err := http.Post(promptURL, "application/json", bytes.NewBuffer(promptJSON))
- if err != nil {
- t.Fatalf("❌ 发送提示词失败: %v", err)
- }
- defer promptResp.Body.Close()
-
- if promptResp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(promptResp.Body)
- t.Fatalf("❌ 提示词请求失败,状态码: %d, 响应: %s", promptResp.StatusCode, string(body))
- }
-
- var promptResult map[string]interface{}
- promptBody, _ := io.ReadAll(promptResp.Body)
- if err := json.Unmarshal(promptBody, &promptResult); err != nil {
- t.Fatalf("❌ 解析提示词响应失败: %v", err)
- }
-
- info, ok := promptResult["info"].(map[string]interface{})
- if ok {
- modelID, _ := info["modelID"].(string)
- providerID, _ := info["providerID"].(string)
- t.Logf("✅ 收到同步响应:")
- t.Logf(" - 模型: %s (%s)", modelID, providerID)
- if tokens, ok := info["tokens"].(map[string]interface{}); ok {
- input, _ := tokens["input"].(float64)
- output, _ := tokens["output"].(float64)
- t.Logf(" - Token使用: 输入=%.0f, 输出=%.0f", input, output)
- }
- }
-
- // 4. 尝试异步端点(流式)
- t.Logf("\n🔍 测试异步端点...")
- asyncURL := fmt.Sprintf("%s/session/%s/prompt_async", baseURL, sessionID)
- asyncReq, err := http.NewRequest("POST", asyncURL, bytes.NewBuffer(promptJSON))
- if err != nil {
- t.Fatalf("❌ 创建异步请求失败: %v", err)
- }
- asyncReq.Header.Set("Content-Type", "application/json")
- asyncReq.Header.Set("Accept", "text/event-stream")
-
- client := &http.Client{}
- asyncResp, err := client.Do(asyncReq)
- if err != nil {
- t.Fatalf("❌ 异步请求失败: %v", err)
- }
- defer asyncResp.Body.Close()
-
- t.Logf(" 异步端点响应:")
- t.Logf(" - 状态码: %d", asyncResp.StatusCode)
- t.Logf(" - Content-Type: %s", asyncResp.Header.Get("Content-Type"))
-
- if asyncResp.StatusCode == http.StatusNoContent {
- t.Logf(" - 结果: ⚠️ 返回 204 No Content")
- t.Logf(" - 说明: 异步端点未配置或返回空响应")
- t.Logf(" - 建议: 使用同步端点或配置异步端点")
- } else if asyncResp.StatusCode == http.StatusOK {
- contentType := asyncResp.Header.Get("Content-Type")
- if strings.Contains(contentType, "text/event-stream") {
- t.Logf(" - 结果: ✅ 流式端点正常工作")
- // 尝试读取SSE流
- reader := bufio.NewReader(asyncResp.Body)
- eventCount := 0
- for {
- line, err := reader.ReadString('\n')
- if err != nil {
- if err == io.EOF {
- break
- }
- t.Logf(" - 读取流错误: %v", err)
- break
- }
- line = strings.TrimSpace(line)
- if line == "" {
- continue
- }
- if strings.HasPrefix(line, "data: ") {
- eventCount++
- data := strings.TrimPrefix(line, "data: ")
- t.Logf(" - 事件[%d]: %s", eventCount, data[:minIntExt(100, len(data))])
- }
- }
- if eventCount > 0 {
- t.Logf(" - 收到 %d 个SSE事件", eventCount)
- }
- } else {
- t.Logf(" - 结果: ⚠️ 返回 200 但非流式类型: %s", contentType)
- }
- } else {
- t.Logf(" - 结果: ❌ 异常状态码")
- }
-
- // 5. 获取配置信息
- configURL := baseURL + "/global/config"
- configResp, err := http.Get(configURL)
- if err == nil && configResp.StatusCode == http.StatusOK {
- defer configResp.Body.Close()
- configBody, _ := io.ReadAll(configResp.Body)
- var config map[string]interface{}
- if err := json.Unmarshal(configBody, &config); err == nil {
- t.Logf("\n🔧 OpenCode 配置信息:")
- t.Logf(" - 有配置键数量: %d", len(config))
- // 查找模型相关配置
- for key := range config {
- if strings.Contains(strings.ToLower(key), "model") ||
- strings.Contains(strings.ToLower(key), "provider") ||
- strings.Contains(strings.ToLower(key), "api") {
- t.Logf(" - 检测到模型相关配置: %s", key)
- }
- }
- }
- }
-
- // 6. 测试结果总结
- t.Logf("\n📋 直接 API 测试结果:")
- t.Logf(" ✅ 健康检查: 正常")
- t.Logf(" ✅ 会话管理: 正常")
- t.Logf(" ✅ 同步对话: 正常")
- t.Logf(" ⚠️ 异步端点: 返回 204 No Content")
- t.Logf(" 🔧 配置状态: %s", "有AI模型配置")
- t.Logf("\n💡 结论:")
- t.Logf(" 1. OpenCode 8787 端口 AI 已配置,同步端点工作正常")
- t.Logf(" 2. 异步端点返回 204,可能需要额外配置")
- t.Logf(" 3. 建议 svc-code 使用此端口进行连接")
- }
-
- // minIntExt 辅助函数(避免与 stream_api_test.go 中的 minInt 冲突)
- func minIntExt(a, b int) int {
- if a < b {
- return a
- }
- return b
- }
|