mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
P0: WebUI重构
- 完整 SPA 仪表盘 (7标签页), //go:embed dashboard.html
P1: OpenClaw兼容 (三通道: SKILL.md / sidecar / simulator)
- Node.js 模拟进程统一加载任意 OpenClaw 插件
- JSON-RPC 2.0 over stdio 协议, go:embed 内嵌
P2: Healthcheck 优化
- 定时自动执行 (startAutoCheck, 30min)
- healthcheck_perf 性能监控工具
其他: agentcli/cmd 插件, integration_test, status.go,
test_deepseek 清理, 多项 bug 修复
674 lines
16 KiB
Go
674 lines
16 KiB
Go
package api
|
||
|
||
import (
|
||
"bufio"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
||
)
|
||
|
||
type Message struct {
|
||
Role string `json:"role"`
|
||
Content string `json:"content"`
|
||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||
ToolCalls []ToolCall `json:"-"`
|
||
}
|
||
|
||
func (m Message) MarshalJSON() ([]byte, error) {
|
||
raw := map[string]interface{}{
|
||
"role": m.Role,
|
||
"content": m.Content,
|
||
}
|
||
if m.ReasoningContent != "" {
|
||
raw["reasoning_content"] = m.ReasoningContent
|
||
}
|
||
if m.ToolCallID != "" {
|
||
raw["tool_call_id"] = m.ToolCallID
|
||
}
|
||
if len(m.ToolCalls) > 0 {
|
||
apiTCs := make([]apiToolCall, len(m.ToolCalls))
|
||
for i, tc := range m.ToolCalls {
|
||
argsBytes, _ := json.Marshal(tc.Arguments)
|
||
apiTCs[i] = apiToolCall{
|
||
ID: tc.ID,
|
||
Type: "function",
|
||
Function: apiFunction{
|
||
Name: tc.Name,
|
||
Arguments: string(argsBytes),
|
||
},
|
||
}
|
||
}
|
||
raw["tool_calls"] = apiTCs
|
||
}
|
||
return json.Marshal(raw)
|
||
}
|
||
|
||
type CompletionRequest struct {
|
||
Model string `json:"model,omitempty"`
|
||
Messages []Message `json:"messages"`
|
||
Temperature float64 `json:"temperature,omitempty"`
|
||
MaxTokens int `json:"max_tokens,omitempty"`
|
||
Stream bool `json:"stream,omitempty"`
|
||
Tools []interface{} `json:"tools,omitempty"`
|
||
ToolChoice interface{} `json:"tool_choice,omitempty"`
|
||
ExtraBody map[string]interface{} `json:"-"`
|
||
}
|
||
|
||
func (r *CompletionRequest) MarshalJSON() ([]byte, error) {
|
||
type Alias CompletionRequest
|
||
data, err := json.Marshal((*Alias)(r))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(r.ExtraBody) == 0 {
|
||
return data, nil
|
||
}
|
||
var raw map[string]interface{}
|
||
if err := json.Unmarshal(data, &raw); err != nil {
|
||
return nil, err
|
||
}
|
||
for k, v := range r.ExtraBody {
|
||
raw[k] = v
|
||
}
|
||
return json.Marshal(raw)
|
||
}
|
||
|
||
type CompletionResponse struct {
|
||
Content string `json:"content"`
|
||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||
FinishReason string `json:"finish_reason,omitempty"`
|
||
TokenUsage TokenUsage `json:"token_usage,omitempty"`
|
||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||
}
|
||
|
||
type TokenUsage struct {
|
||
Prompt int `json:"prompt"`
|
||
Completion int `json:"completion"`
|
||
Total int `json:"total"`
|
||
}
|
||
|
||
type ToolCall struct {
|
||
ID string `json:"id"`
|
||
Type string `json:"type"`
|
||
Name string `json:"name"`
|
||
Arguments map[string]interface{} `json:"arguments"`
|
||
}
|
||
|
||
type apiToolCall struct {
|
||
ID string `json:"id"`
|
||
Type string `json:"type"`
|
||
Function apiFunction `json:"function"`
|
||
}
|
||
|
||
type apiFunction struct {
|
||
Name string `json:"name"`
|
||
Arguments string `json:"arguments"`
|
||
}
|
||
|
||
type StreamChunk struct {
|
||
Content string `json:"content"`
|
||
Done bool `json:"done"`
|
||
ToolCall *ToolCall `json:"tool_call,omitempty"`
|
||
}
|
||
|
||
type Provider interface {
|
||
Name() string
|
||
Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error)
|
||
ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error)
|
||
}
|
||
|
||
type BaseConfig struct {
|
||
Model string `json:"model"`
|
||
BaseURL string `json:"base_url"`
|
||
APIKey string `json:"api_key"`
|
||
Temperature float64 `json:"temperature"`
|
||
MaxTokens int `json:"max_tokens"`
|
||
}
|
||
|
||
type OpenAIProvider struct {
|
||
cfg BaseConfig
|
||
client *http.Client
|
||
}
|
||
|
||
func NewOpenAIProvider(cfg BaseConfig) *OpenAIProvider {
|
||
if cfg.BaseURL == "" {
|
||
cfg.BaseURL = "https://api.openai.com/v1"
|
||
}
|
||
if cfg.Temperature == 0 {
|
||
cfg.Temperature = 0.7
|
||
}
|
||
if cfg.MaxTokens == 0 {
|
||
cfg.MaxTokens = 4096
|
||
}
|
||
return &OpenAIProvider{
|
||
cfg: cfg,
|
||
client: &http.Client{Timeout: 60 * time.Second},
|
||
}
|
||
}
|
||
|
||
func (p *OpenAIProvider) Name() string { return "openai" }
|
||
|
||
func (p *OpenAIProvider) Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) {
|
||
if req.Model == "" {
|
||
req.Model = p.cfg.Model
|
||
}
|
||
|
||
body, _ := json.Marshal(req)
|
||
httpReq, _ := http.NewRequestWithContext(ctx, "POST", p.cfg.BaseURL+"/chat/completions", strings.NewReader(string(body)))
|
||
httpReq.Header.Set("Content-Type", "application/json")
|
||
httpReq.Header.Set("Authorization", "Bearer "+p.cfg.APIKey)
|
||
|
||
resp, err := p.client.Do(httpReq)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("api call: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != 200 {
|
||
respBody, _ := io.ReadAll(resp.Body)
|
||
return nil, fmt.Errorf("api error %d: %s", resp.StatusCode, string(respBody))
|
||
}
|
||
|
||
var rawResult struct {
|
||
Choices []struct {
|
||
Message struct {
|
||
Content *string `json:"content"`
|
||
ReasoningContent *string `json:"reasoning_content"`
|
||
ToolCalls []rawToolCall `json:"tool_calls"`
|
||
Role string `json:"role"`
|
||
} `json:"message"`
|
||
FinishReason string `json:"finish_reason"`
|
||
} `json:"choices"`
|
||
Usage struct {
|
||
PromptTokens int `json:"prompt_tokens"`
|
||
CompletionTokens int `json:"completion_tokens"`
|
||
TotalTokens int `json:"total_tokens"`
|
||
} `json:"usage"`
|
||
}
|
||
|
||
if err := json.NewDecoder(resp.Body).Decode(&rawResult); err != nil {
|
||
return nil, fmt.Errorf("decode: %w", err)
|
||
}
|
||
|
||
if len(rawResult.Choices) == 0 {
|
||
return nil, fmt.Errorf("no choices returned")
|
||
}
|
||
|
||
ch := rawResult.Choices[0]
|
||
content := ""
|
||
if ch.Message.Content != nil {
|
||
content = *ch.Message.Content
|
||
}
|
||
|
||
var toolCalls []ToolCall
|
||
for _, tc := range ch.Message.ToolCalls {
|
||
tc := tc
|
||
args := make(map[string]interface{})
|
||
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
|
||
args["_raw"] = tc.Function.Arguments
|
||
}
|
||
toolCalls = append(toolCalls, ToolCall{
|
||
ID: tc.ID,
|
||
Type: tc.Type,
|
||
Name: tc.Function.Name,
|
||
Arguments: args,
|
||
})
|
||
}
|
||
|
||
return &CompletionResponse{
|
||
Content: content,
|
||
FinishReason: ch.FinishReason,
|
||
TokenUsage: TokenUsage{
|
||
Prompt: rawResult.Usage.PromptTokens,
|
||
Completion: rawResult.Usage.CompletionTokens,
|
||
Total: rawResult.Usage.TotalTokens,
|
||
},
|
||
ToolCalls: toolCalls,
|
||
}, nil
|
||
}
|
||
|
||
func (p *OpenAIProvider) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) {
|
||
req.Stream = true
|
||
ch := make(chan StreamChunk, 64)
|
||
|
||
body, _ := json.Marshal(req)
|
||
httpReq, _ := http.NewRequestWithContext(ctx, "POST", p.cfg.BaseURL+"/chat/completions", strings.NewReader(string(body)))
|
||
httpReq.Header.Set("Content-Type", "application/json")
|
||
httpReq.Header.Set("Authorization", "Bearer "+p.cfg.APIKey)
|
||
|
||
resp, err := p.client.Do(httpReq)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("stream api: %w", err)
|
||
}
|
||
|
||
go func() {
|
||
defer resp.Body.Close()
|
||
defer close(ch)
|
||
|
||
decoder := json.NewDecoder(resp.Body)
|
||
for {
|
||
var line struct {
|
||
Choices []struct {
|
||
Delta struct {
|
||
Content string `json:"content"`
|
||
} `json:"delta"`
|
||
FinishReason *string `json:"finish_reason"`
|
||
} `json:"choices"`
|
||
}
|
||
|
||
if err := decoder.Decode(&line); err != nil {
|
||
return
|
||
}
|
||
|
||
if len(line.Choices) > 0 {
|
||
select {
|
||
case ch <- StreamChunk{
|
||
Content: line.Choices[0].Delta.Content,
|
||
Done: line.Choices[0].FinishReason != nil,
|
||
}:
|
||
case <-ctx.Done():
|
||
return
|
||
}
|
||
}
|
||
}
|
||
}()
|
||
|
||
return ch, nil
|
||
}
|
||
|
||
type OllamaProvider struct {
|
||
cfg BaseConfig
|
||
client *http.Client
|
||
}
|
||
|
||
func NewOllamaProvider(cfg BaseConfig) *OllamaProvider {
|
||
if cfg.BaseURL == "" {
|
||
cfg.BaseURL = "http://localhost:11434"
|
||
}
|
||
if cfg.Temperature == 0 {
|
||
cfg.Temperature = 0.7
|
||
}
|
||
if cfg.MaxTokens == 0 {
|
||
cfg.MaxTokens = 4096
|
||
}
|
||
return &OllamaProvider{
|
||
cfg: cfg,
|
||
client: &http.Client{Timeout: 120 * time.Second},
|
||
}
|
||
}
|
||
|
||
func (p *OllamaProvider) Name() string { return "ollama" }
|
||
|
||
func (p *OllamaProvider) Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) {
|
||
ollamaReq := map[string]interface{}{
|
||
"model": req.Model,
|
||
"messages": req.Messages,
|
||
"stream": false,
|
||
"options": map[string]interface{}{
|
||
"temperature": req.Temperature,
|
||
"num_predict": req.MaxTokens,
|
||
},
|
||
}
|
||
|
||
body, _ := json.Marshal(ollamaReq)
|
||
httpReq, _ := http.NewRequestWithContext(ctx, "POST", p.cfg.BaseURL+"/api/chat", strings.NewReader(string(body)))
|
||
httpReq.Header.Set("Content-Type", "application/json")
|
||
|
||
resp, err := p.client.Do(httpReq)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("ollama chat: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
var result struct {
|
||
Message struct {
|
||
Content string `json:"content"`
|
||
} `json:"message"`
|
||
DoneReason string `json:"done_reason"`
|
||
}
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
return nil, fmt.Errorf("decode: %w", err)
|
||
}
|
||
|
||
return &CompletionResponse{
|
||
Content: result.Message.Content,
|
||
FinishReason: result.DoneReason,
|
||
}, nil
|
||
}
|
||
|
||
func (p *OllamaProvider) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) {
|
||
ch := make(chan StreamChunk, 64)
|
||
|
||
go func() {
|
||
defer close(ch)
|
||
ch <- StreamChunk{Done: true}
|
||
}()
|
||
|
||
return ch, nil
|
||
}
|
||
|
||
// LuaAdaptedProvider 使用 Lua 脚本做请求/响应变换,直接发起 HTTP 调用
|
||
// 不再包裹其他 Provider,协议差异全部在 Lua 层处理
|
||
type LuaAdaptedProvider struct {
|
||
name string
|
||
cfg BaseConfig
|
||
vm *luaVM.VM
|
||
adapter string
|
||
client *http.Client
|
||
}
|
||
|
||
func NewLuaAdaptedProvider(cfg BaseConfig, vm *luaVM.VM, adapter string) *LuaAdaptedProvider {
|
||
if cfg.Temperature == 0 {
|
||
cfg.Temperature = 0.7
|
||
}
|
||
if cfg.MaxTokens == 0 {
|
||
cfg.MaxTokens = 4096
|
||
}
|
||
return &LuaAdaptedProvider{
|
||
name: fmt.Sprintf("lua_%s", adapter),
|
||
cfg: cfg,
|
||
vm: vm,
|
||
adapter: adapter,
|
||
client: &http.Client{Timeout: 120 * time.Second},
|
||
}
|
||
}
|
||
|
||
func (p *LuaAdaptedProvider) Name() string { return p.name }
|
||
|
||
func (p *LuaAdaptedProvider) Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) {
|
||
if req.Model == "" {
|
||
req.Model = p.cfg.Model
|
||
}
|
||
|
||
rawReq, _ := json.Marshal(req)
|
||
|
||
transformedBody, err := p.vm.CallTransformRequest(p.adapter, string(rawReq))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("lua transform_request: %w", err)
|
||
}
|
||
|
||
endpoint := p.vm.GetAdapterEndpoint(p.adapter)
|
||
if endpoint == "" {
|
||
endpoint = "/chat/completions"
|
||
}
|
||
url := strings.TrimRight(p.cfg.BaseURL, "/") + endpoint
|
||
|
||
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(transformedBody))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("create request: %w", err)
|
||
}
|
||
httpReq.Header.Set("Content-Type", "application/json")
|
||
httpReq.Header.Set("Authorization", "Bearer "+p.cfg.APIKey)
|
||
|
||
for k, v := range p.vm.GetAdapterHeaders(p.adapter) {
|
||
httpReq.Header.Set(k, v)
|
||
}
|
||
|
||
resp, err := p.client.Do(httpReq)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("api call: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
rawResp, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("read response: %w", err)
|
||
}
|
||
|
||
if resp.StatusCode != 200 {
|
||
return nil, fmt.Errorf("api error %d: %s", resp.StatusCode, string(rawResp))
|
||
}
|
||
|
||
unifiedJSON, err := p.vm.CallTransformResponse(p.adapter, string(rawResp))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("lua transform_response: %w", err)
|
||
}
|
||
|
||
var result CompletionResponse
|
||
if err := json.Unmarshal([]byte(unifiedJSON), &result); err != nil {
|
||
return nil, fmt.Errorf("unmarshal unified response: %w (body: %s)", err, unifiedJSON)
|
||
}
|
||
|
||
return &result, nil
|
||
}
|
||
|
||
func (p *LuaAdaptedProvider) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) {
|
||
if req.Model == "" {
|
||
req.Model = p.cfg.Model
|
||
}
|
||
req.Stream = true
|
||
rawReq, _ := json.Marshal(req)
|
||
|
||
transformedBody, err := p.vm.CallTransformRequest(p.adapter, string(rawReq))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("lua transform_request (stream): %w", err)
|
||
}
|
||
|
||
endpoint := p.vm.GetAdapterEndpoint(p.adapter)
|
||
if endpoint == "" {
|
||
endpoint = "/chat/completions"
|
||
}
|
||
url := strings.TrimRight(p.cfg.BaseURL, "/") + endpoint
|
||
|
||
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(transformedBody))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("create stream request: %w", err)
|
||
}
|
||
httpReq.Header.Set("Content-Type", "application/json")
|
||
httpReq.Header.Set("Authorization", "Bearer "+p.cfg.APIKey)
|
||
|
||
for k, v := range p.vm.GetAdapterHeaders(p.adapter) {
|
||
httpReq.Header.Set(k, v)
|
||
}
|
||
|
||
resp, err := p.client.Do(httpReq)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("stream api: %w", err)
|
||
}
|
||
|
||
ch := make(chan StreamChunk, 64)
|
||
go func() {
|
||
defer resp.Body.Close()
|
||
defer close(ch)
|
||
|
||
scanner := NewSSEScanner(resp.Body)
|
||
for scanner.Scan() {
|
||
line := scanner.Text()
|
||
if line == "" {
|
||
continue
|
||
}
|
||
|
||
// 尝试用 Lua 变换流块(如果 adapter 定义了 transform_stream_chunk)
|
||
unified, err := p.vm.CallTransformStreamChunk(p.adapter, line)
|
||
if err != nil || unified == line {
|
||
// 无流变换函数或变换透传,尝试标准 SSE 解析
|
||
var raw struct {
|
||
Choices []struct {
|
||
Delta struct {
|
||
Content string `json:"content"`
|
||
} `json:"delta"`
|
||
FinishReason *string `json:"finish_reason"`
|
||
} `json:"choices"`
|
||
}
|
||
if err := json.Unmarshal([]byte(unified), &raw); err != nil {
|
||
continue
|
||
}
|
||
if len(raw.Choices) > 0 {
|
||
select {
|
||
case ch <- StreamChunk{
|
||
Content: raw.Choices[0].Delta.Content,
|
||
Done: raw.Choices[0].FinishReason != nil,
|
||
}:
|
||
case <-ctx.Done():
|
||
return
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
|
||
// Lua 返回了变换后的统一格式
|
||
var chunk StreamChunk
|
||
if err := json.Unmarshal([]byte(unified), &chunk); err == nil {
|
||
select {
|
||
case ch <- chunk:
|
||
case <-ctx.Done():
|
||
return
|
||
}
|
||
}
|
||
}
|
||
}()
|
||
|
||
return ch, nil
|
||
}
|
||
|
||
// SSEScanner 读取 SSE 格式的流(data: ...)
|
||
type SSEScanner struct {
|
||
reader *bufio.Reader
|
||
pending string
|
||
}
|
||
|
||
func NewSSEScanner(r io.Reader) *SSEScanner {
|
||
return &SSEScanner{reader: bufio.NewReader(r)}
|
||
}
|
||
|
||
func (s *SSEScanner) Scan() bool {
|
||
s.pending = ""
|
||
for {
|
||
line, err := s.reader.ReadString('\n')
|
||
if err != nil {
|
||
return false
|
||
}
|
||
line = strings.TrimRight(line, "\r\n")
|
||
if strings.HasPrefix(line, "data: ") {
|
||
s.pending = strings.TrimPrefix(line, "data: ")
|
||
if s.pending == "[DONE]" {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
|
||
func (s *SSEScanner) Text() string { return s.pending }
|
||
|
||
type ProviderManager struct {
|
||
mu sync.RWMutex
|
||
providers map[string]Provider
|
||
default_ string
|
||
}
|
||
|
||
func NewProviderManager() *ProviderManager {
|
||
return &ProviderManager{
|
||
providers: make(map[string]Provider),
|
||
}
|
||
}
|
||
|
||
func (m *ProviderManager) Register(name string, p Provider) {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
m.providers[name] = p
|
||
if m.default_ == "" {
|
||
m.default_ = name
|
||
}
|
||
}
|
||
|
||
func (m *ProviderManager) SetDefault(name string) error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
if _, ok := m.providers[name]; !ok {
|
||
return fmt.Errorf("provider %s not found", name)
|
||
}
|
||
m.default_ = name
|
||
return nil
|
||
}
|
||
|
||
func (m *ProviderManager) Get(name string) Provider {
|
||
m.mu.RLock()
|
||
defer m.mu.RUnlock()
|
||
if name == "" {
|
||
name = m.default_
|
||
}
|
||
return m.providers[name]
|
||
}
|
||
|
||
func (m *ProviderManager) Default() Provider {
|
||
m.mu.RLock()
|
||
defer m.mu.RUnlock()
|
||
return m.providers[m.default_]
|
||
}
|
||
|
||
// QuickChat 向默认 LLM Provider 发送一条简短消息并返回回复。
|
||
// 这是一个"非记忆"调用——直接通过 Provider HTTP 调用,不经过 Agent 的记忆/蒸馏管线。
|
||
// 适用于健康检查、系统自检等不需要产生记忆碎片的场景。
|
||
func (m *ProviderManager) QuickChat(ctx context.Context, prompt string) (*CompletionResponse, error) {
|
||
p := m.Default()
|
||
if p == nil {
|
||
return nil, fmt.Errorf("no default provider")
|
||
}
|
||
return p.Chat(ctx, &CompletionRequest{
|
||
Messages: []Message{
|
||
{Role: "user", Content: prompt},
|
||
},
|
||
MaxTokens: 128,
|
||
})
|
||
}
|
||
|
||
func (m *ProviderManager) List() []string {
|
||
m.mu.RLock()
|
||
defer m.mu.RUnlock()
|
||
var names []string
|
||
for n := range m.providers {
|
||
names = append(names, n)
|
||
}
|
||
return names
|
||
}
|
||
|
||
type rawToolCall struct {
|
||
ID string `json:"id"`
|
||
Type string `json:"type"`
|
||
Function struct {
|
||
Name string `json:"name"`
|
||
Arguments string `json:"arguments"`
|
||
} `json:"function"`
|
||
}
|
||
|
||
func messagesToMap(msgs []Message) []interface{} {
|
||
result := make([]interface{}, len(msgs))
|
||
for i, m := range msgs {
|
||
result[i] = map[string]interface{}{
|
||
"role": m.Role,
|
||
"content": m.Content,
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
func getString(m map[string]interface{}, key string) string {
|
||
if v, ok := m[key]; ok {
|
||
if s, ok := v.(string); ok {
|
||
return s
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func getFloat(m map[string]interface{}, key string) float64 {
|
||
if v, ok := m[key]; ok {
|
||
switch n := v.(type) {
|
||
case float64:
|
||
return n
|
||
case int:
|
||
return float64(n)
|
||
}
|
||
}
|
||
return 0
|
||
}
|