feat: DevTools 数据库监看面板 + 隧道控制 + 多项 Bug 修复
**DevTools 新增功能 (Tasks 13-14):** - 首页仪表盘添加数据库实时监看卡片 (5端口状态 + 记忆数) - 侧边栏新增数据库面板,支持自动 5 秒刷新 - 数据库面板显示 PostgreSQL/Redis/Qdrant/MinIO/NATS 端口状态 - 隧道控制按钮 (启动/停止/重启/查看状态) - 新增 API 端点: GET /api/database/status, POST /api/tunnel/:action - 更新 docs/api-reference/ API 文档 **Bug 修复 (Task 15):** - 修复 pgrep -f 自匹配导致隧道状态误判 (添加 ^ssh 锚点) - devtools/src/index.js (dashboard + database/status) - scripts/tunnel.sh (is_tunnel_running + show_status) - 修复数据库面板缺少自动刷新定时器 - 修复侧边栏数据库徽章永远 display:none - 修复僵尸进程场景下按钮死锁问题 **其他改进:** - .gitignore 添加 backend/cmd, backend/iot-debug-service/main - 前端多项改进 (登录/注册/会话/流式动画等)
This commit is contained in:
@@ -389,9 +389,10 @@ func handleChat(
|
||||
|
||||
// 将助手消息(含工具调用)加入上下文
|
||||
assistantMsg := model.LLMMessage{
|
||||
Role: model.RoleAssistant,
|
||||
Content: syncResp.Content,
|
||||
ToolCalls: syncResp.ToolCalls,
|
||||
Role: model.RoleAssistant,
|
||||
Content: syncResp.Content,
|
||||
ToolCalls: syncResp.ToolCalls,
|
||||
ReasoningContent: syncResp.ReasoningContent,
|
||||
}
|
||||
llmMessages = append(llmMessages, assistantMsg)
|
||||
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/model"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/tools"
|
||||
)
|
||||
|
||||
// PendingThought 待推送的后台思考
|
||||
type PendingThought struct {
|
||||
Content string `json:"content"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Consumed bool `json:"consumed"`
|
||||
}
|
||||
|
||||
// Thinker 后台思考器
|
||||
type Thinker struct {
|
||||
mu sync.Mutex
|
||||
enabled bool
|
||||
personaLoader *persona.Loader
|
||||
memRetriever *memory.Retriever
|
||||
llmAdapter *llm.Adapter
|
||||
iotClient *tools.IoTClient
|
||||
idleTimeout time.Duration // 闲置超时
|
||||
thinkInterval time.Duration // 两次思考最小间隔
|
||||
iotQueryInterval time.Duration // IoT查询最小间隔
|
||||
|
||||
pendingThoughts []*PendingThought
|
||||
|
||||
lastUserMessage time.Time
|
||||
lastThinkTime time.Time
|
||||
lastIoTQuery time.Time
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// ThinkerConfig 后台思考配置
|
||||
type ThinkerConfig struct {
|
||||
Enabled bool
|
||||
IdleTimeout time.Duration
|
||||
ThinkInterval time.Duration
|
||||
IoTQueryInterval time.Duration
|
||||
}
|
||||
|
||||
// DefaultThinkerConfig 默认配置
|
||||
func DefaultThinkerConfig() ThinkerConfig {
|
||||
return ThinkerConfig{
|
||||
Enabled: getEnvBool("ENABLE_BACKGROUND_THINKING", true),
|
||||
IdleTimeout: getEnvDuration("THINK_IDLE_TIMEOUT_SEC", 120),
|
||||
ThinkInterval: getEnvDuration("THINK_INTERVAL_SEC", 300),
|
||||
IoTQueryInterval: getEnvDuration("IOT_QUERY_INTERVAL_SEC", 600),
|
||||
}
|
||||
}
|
||||
|
||||
// NewThinker 创建后台思考器
|
||||
func NewThinker(
|
||||
cfg ThinkerConfig,
|
||||
personaLoader *persona.Loader,
|
||||
memRetriever *memory.Retriever,
|
||||
llmAdapter *llm.Adapter,
|
||||
iotClient *tools.IoTClient,
|
||||
) *Thinker {
|
||||
return &Thinker{
|
||||
enabled: cfg.Enabled,
|
||||
personaLoader: personaLoader,
|
||||
memRetriever: memRetriever,
|
||||
llmAdapter: llmAdapter,
|
||||
iotClient: iotClient,
|
||||
idleTimeout: cfg.IdleTimeout,
|
||||
thinkInterval: cfg.ThinkInterval,
|
||||
iotQueryInterval: cfg.IoTQueryInterval,
|
||||
pendingThoughts: make([]*PendingThought, 0),
|
||||
lastUserMessage: time.Now(),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动后台思考循环
|
||||
func (t *Thinker) Start() {
|
||||
if !t.enabled {
|
||||
log.Println("[后台思考] 已禁用 (ENABLE_BACKGROUND_THINKING=false)")
|
||||
return
|
||||
}
|
||||
|
||||
t.wg.Add(1)
|
||||
go t.loop()
|
||||
log.Printf("[后台思考] 已启动 (闲置超时=%v, 思考间隔=%v, IoT查询间隔=%v)",
|
||||
t.idleTimeout, t.thinkInterval, t.iotQueryInterval)
|
||||
}
|
||||
|
||||
// Stop 停止后台思考
|
||||
func (t *Thinker) Stop() {
|
||||
close(t.stopCh)
|
||||
t.wg.Wait()
|
||||
log.Println("[后台思考] 已停止")
|
||||
}
|
||||
|
||||
// RecordUserMessage 记录用户活动时间
|
||||
func (t *Thinker) RecordUserMessage() {
|
||||
t.mu.Lock()
|
||||
t.lastUserMessage = time.Now()
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetPendingThoughts 获取并消费所有待处理的后台思考
|
||||
func (t *Thinker) GetPendingThoughts() []*PendingThought {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if len(t.pendingThoughts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := t.pendingThoughts
|
||||
t.pendingThoughts = make([]*PendingThought, 0)
|
||||
|
||||
// 标记已消费
|
||||
for _, pt := range result {
|
||||
pt.Consumed = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// HasPendingThoughts 检查是否有待处理的思考
|
||||
func (t *Thinker) HasPendingThoughts() bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
return len(t.pendingThoughts) > 0
|
||||
}
|
||||
|
||||
// loop 后台主循环
|
||||
func (t *Thinker) loop() {
|
||||
defer t.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(10 * time.Second) // 每10秒检查一次
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
t.checkAndThink()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkAndThink 检查是否需要触发思考
|
||||
func (t *Thinker) checkAndThink() {
|
||||
t.mu.Lock()
|
||||
|
||||
// 检查空闲时间是否超过阈值
|
||||
idleDuration := time.Since(t.lastUserMessage)
|
||||
if idleDuration < t.idleTimeout {
|
||||
t.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查距离上次思考是否超过最小间隔
|
||||
if time.Since(t.lastThinkTime) < t.thinkInterval {
|
||||
t.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
t.lastThinkTime = time.Now()
|
||||
t.mu.Unlock()
|
||||
|
||||
// 执行后台思考(不持锁)
|
||||
t.performThink()
|
||||
}
|
||||
|
||||
// performThink 执行一次后台思考
|
||||
func (t *Thinker) performThink() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 加载人格配置
|
||||
personaConfig, err := t.personaLoader.Get("cyrene")
|
||||
if err != nil {
|
||||
log.Printf("[后台思考] 加载人格失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 检索最近的记忆
|
||||
var memories []memory.MemoryEntry
|
||||
if t.memRetriever != nil {
|
||||
memories, err = t.memRetriever.Retrieve(ctx, "system", "最近发生了什么 重要的事情")
|
||||
if err != nil {
|
||||
log.Printf("[后台思考] 记忆检索失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询 IoT 设备状态(节制)
|
||||
var deviceSummary string
|
||||
if t.iotClient != nil && time.Since(t.lastIoTQuery) >= t.iotQueryInterval {
|
||||
devices := t.iotClient.GetDevicesForContext()
|
||||
if len(devices) > 0 {
|
||||
deviceSummary = formatDeviceContext(devices)
|
||||
}
|
||||
t.mu.Lock()
|
||||
t.lastIoTQuery = time.Now()
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
// 构建思考提示
|
||||
systemPrompt := personaConfig.BuildSystemPrompt("开拓者", 1)
|
||||
|
||||
memoryContext := ""
|
||||
if len(memories) > 0 {
|
||||
memoryContext = "【最近的记忆】\n"
|
||||
for _, m := range memories {
|
||||
if len(memoryContext)+len(m.Content) > 500 {
|
||||
break // 限制记忆上下文长度
|
||||
}
|
||||
memoryContext += fmt.Sprintf("- %s\n", m.Content)
|
||||
}
|
||||
}
|
||||
|
||||
userPrompt := "昔涟,现在是你的后台思考时间。开拓者暂时没有说话。"
|
||||
userPrompt += "\n请你基于以下信息进行简短思考:你注意到了什么?有什么想对开拓者说的吗?"
|
||||
userPrompt += "\n注意:这是内部思考,不是直接对话,请以第三人称或自省的方式思考。"
|
||||
|
||||
if memoryContext != "" {
|
||||
userPrompt += "\n\n" + memoryContext
|
||||
}
|
||||
if deviceSummary != "" {
|
||||
userPrompt += "\n\n" + deviceSummary
|
||||
}
|
||||
|
||||
// 调用 LLM
|
||||
messages := []model.LLMMessage{
|
||||
{Role: model.RoleSystem, Content: systemPrompt},
|
||||
{Role: model.RoleUser, Content: userPrompt},
|
||||
}
|
||||
|
||||
resp, err := t.llmAdapter.Chat(ctx, messages)
|
||||
if err != nil {
|
||||
log.Printf("[后台思考] LLM调用失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.Content == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 存储思考结果
|
||||
t.mu.Lock()
|
||||
t.pendingThoughts = append(t.pendingThoughts, &PendingThought{
|
||||
Content: resp.Content,
|
||||
CreatedAt: time.Now(),
|
||||
Consumed: false,
|
||||
})
|
||||
|
||||
// 只保留最近5条
|
||||
if len(t.pendingThoughts) > 5 {
|
||||
t.pendingThoughts = t.pendingThoughts[len(t.pendingThoughts)-5:]
|
||||
}
|
||||
count := len(t.pendingThoughts)
|
||||
t.mu.Unlock()
|
||||
|
||||
log.Printf("[后台思考] 完成 (当前累积 %d 条待推送思考)", count)
|
||||
}
|
||||
|
||||
// formatDeviceContext 格式化设备状态为文本
|
||||
func formatDeviceContext(devices []tools.IoTDevice) string {
|
||||
if len(devices) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
summary := "[当前IoT设备状态]\n"
|
||||
for _, d := range devices {
|
||||
switch d.Type {
|
||||
case "light":
|
||||
if d.Status == "on" {
|
||||
summary += fmt.Sprintf("- %s: 开启 (亮度%d%%, %s)\n", d.Name, d.Brightness, d.Color)
|
||||
} else {
|
||||
summary += fmt.Sprintf("- %s: 关闭\n", d.Name)
|
||||
}
|
||||
case "ac":
|
||||
if d.Status == "on" {
|
||||
summary += fmt.Sprintf("- %s: 运行中 (%s%.0f°C)\n", d.Name, modeLabel(d.Mode), d.Temperature)
|
||||
} else {
|
||||
summary += fmt.Sprintf("- %s: 关闭\n", d.Name)
|
||||
}
|
||||
case "curtain":
|
||||
statusLabel := "已关闭"
|
||||
if d.Status == "open" {
|
||||
statusLabel = "已打开"
|
||||
}
|
||||
summary += fmt.Sprintf("- %s: %s\n", d.Name, statusLabel)
|
||||
case "sensor":
|
||||
summary += fmt.Sprintf("- %s: %.1f%s\n", d.Name, d.Value, d.Unit)
|
||||
case "lock":
|
||||
statusLabel := "已锁定"
|
||||
if d.Status == "unlocked" {
|
||||
statusLabel = "已解锁"
|
||||
}
|
||||
summary += fmt.Sprintf("- %s: %s (电量%d%%)\n", d.Name, statusLabel, d.Battery)
|
||||
}
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
func modeLabel(mode string) string {
|
||||
switch mode {
|
||||
case "cool":
|
||||
return "制冷"
|
||||
case "heat":
|
||||
return "制热"
|
||||
case "auto":
|
||||
return "自动"
|
||||
default:
|
||||
return mode
|
||||
}
|
||||
}
|
||||
|
||||
func getEnvBool(key string, fallback bool) bool {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return fallback
|
||||
}
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func getEnvDuration(key string, fallbackSec int) time.Duration {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return time.Duration(fallbackSec) * time.Second
|
||||
}
|
||||
sec, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return time.Duration(fallbackSec) * time.Second
|
||||
}
|
||||
return time.Duration(sec) * time.Second
|
||||
}
|
||||
@@ -4,22 +4,97 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/model"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
|
||||
)
|
||||
|
||||
// IoTDeviceSummary IoT设备摘要接口(避免循环依赖)
|
||||
type IoTDeviceSummary interface {
|
||||
GetName() string
|
||||
GetType() string
|
||||
GetStatus() string
|
||||
}
|
||||
|
||||
// ConversationStore 会话历史存储接口
|
||||
type ConversationStore struct {
|
||||
mu sync.RWMutex
|
||||
messages map[string][]model.LLMMessage // key = sessionID
|
||||
maxHistory int
|
||||
}
|
||||
|
||||
// NewConversationStore 创建会话历史存储
|
||||
func NewConversationStore(maxHistory int) *ConversationStore {
|
||||
return &ConversationStore{
|
||||
messages: make(map[string][]model.LLMMessage),
|
||||
maxHistory: maxHistory,
|
||||
}
|
||||
}
|
||||
|
||||
// AddMessage 添加消息到会话历史
|
||||
func (cs *ConversationStore) AddMessage(sessionID string, msg model.LLMMessage) {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
msgs := cs.messages[sessionID]
|
||||
msgs = append(msgs, msg)
|
||||
|
||||
// 限制历史长度
|
||||
if len(msgs) > cs.maxHistory {
|
||||
// 保留 system 消息在开头,只裁剪 user/assistant 消息
|
||||
cutoff := len(msgs) - cs.maxHistory
|
||||
for cutoff < len(msgs) && msgs[cutoff].Role == model.RoleSystem {
|
||||
cutoff++
|
||||
}
|
||||
if cutoff > 0 {
|
||||
msgs = msgs[cutoff:]
|
||||
}
|
||||
}
|
||||
cs.messages[sessionID] = msgs
|
||||
}
|
||||
|
||||
// GetHistory 获取会话历史
|
||||
func (cs *ConversationStore) GetHistory(sessionID string, limit int) []model.LLMMessage {
|
||||
cs.mu.RLock()
|
||||
defer cs.mu.RUnlock()
|
||||
|
||||
msgs := cs.messages[sessionID]
|
||||
if len(msgs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
start := 0
|
||||
if limit > 0 && len(msgs) > limit {
|
||||
start = len(msgs) - limit
|
||||
}
|
||||
|
||||
result := make([]model.LLMMessage, len(msgs[start:]))
|
||||
copy(result, msgs[start:])
|
||||
return result
|
||||
}
|
||||
|
||||
// Builder 对话上下文构建器
|
||||
type Builder struct{}
|
||||
type Builder struct {
|
||||
convStore *ConversationStore
|
||||
}
|
||||
|
||||
// NewBuilder 创建上下文构建器
|
||||
func NewBuilder(convStore *ConversationStore) *Builder {
|
||||
return &Builder{convStore: convStore}
|
||||
}
|
||||
|
||||
type BuildParams struct {
|
||||
UserID string
|
||||
SessionID string
|
||||
UserMessage string
|
||||
Persona *persona.PersonaConfig
|
||||
Memories []memory.MemoryEntry
|
||||
HistoryLimit int
|
||||
UserID string
|
||||
SessionID string
|
||||
UserMessage string
|
||||
Persona *persona.PersonaConfig
|
||||
Memories []memory.MemoryEntry
|
||||
HistoryLimit int
|
||||
DeviceContext string // 注入的设备状态文本
|
||||
PendingThoughts []string // 待注入的后台思考
|
||||
}
|
||||
|
||||
// Build 构建发送给LLM的完整消息列表
|
||||
@@ -28,9 +103,23 @@ func (b *Builder) Build(ctx context.Context, params BuildParams) ([]model.LLMMes
|
||||
|
||||
// 1. 系统消息 —— 昔涟的人格Prompt
|
||||
systemPrompt := params.Persona.BuildSystemPrompt(
|
||||
params.UserID, // 后续可替换为真实用户名
|
||||
1, // 初始好感度
|
||||
params.UserID,
|
||||
1,
|
||||
)
|
||||
|
||||
// 1.1 注入设备上下文到系统消息
|
||||
if params.DeviceContext != "" {
|
||||
systemPrompt += "\n\n" + params.DeviceContext
|
||||
}
|
||||
|
||||
// 1.2 注入后台思考到系统消息(不打扰地)
|
||||
if len(params.PendingThoughts) > 0 {
|
||||
systemPrompt += "\n\n【昔涟的内心思考(仅供你参考,不要直接复述,请自然地融入对话)】\n"
|
||||
for _, thought := range params.PendingThoughts {
|
||||
systemPrompt += fmt.Sprintf("- %s\n", thought)
|
||||
}
|
||||
}
|
||||
|
||||
messages = append(messages, model.LLMMessage{
|
||||
Role: "system",
|
||||
Content: systemPrompt,
|
||||
@@ -63,8 +152,99 @@ func (b *Builder) Build(ctx context.Context, params BuildParams) ([]model.LLMMes
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
// loadHistory 加载会话历史 (MVP阶段返回空,后续对接数据库)
|
||||
// loadHistory 从 ConversationStore 加载会话历史
|
||||
func (b *Builder) loadHistory(_ context.Context, sessionID string, limit int) ([]model.LLMMessage, error) {
|
||||
log.Printf("[context] 加载会话 %s 历史 (限制 %d 条) - 暂未实现持久化", sessionID, limit)
|
||||
return nil, nil
|
||||
if b.convStore == nil {
|
||||
log.Printf("[context] 会话历史存储未初始化,跳过加载")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
history := b.convStore.GetHistory(sessionID, limit)
|
||||
if len(history) == 0 {
|
||||
log.Printf("[context] 会话 %s 无历史记录", sessionID)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
log.Printf("[context] 加载会话 %s 历史 %d 条", sessionID, len(history))
|
||||
return history, nil
|
||||
}
|
||||
|
||||
// CacheMessage 缓存消息到会话历史(供chat handler在回复后调用)
|
||||
func (b *Builder) CacheMessage(sessionID string, role model.Role, content string) {
|
||||
if b.convStore == nil {
|
||||
return
|
||||
}
|
||||
b.convStore.AddMessage(sessionID, model.LLMMessage{
|
||||
Role: role,
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
// InjectDeviceContext 将设备状态格式化为简洁的文本注入系统上下文
|
||||
func InjectDeviceContext(devices []DeviceInfo) string {
|
||||
if len(devices) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("[当前IoT设备状态]\n")
|
||||
for _, d := range devices {
|
||||
switch d.Type {
|
||||
case "light":
|
||||
if d.Status == "on" {
|
||||
sb.WriteString(fmt.Sprintf("- %s: 开启 (亮度%d%%, %s)\n", d.Name, d.Brightness, d.Color))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("- %s: 关闭\n", d.Name))
|
||||
}
|
||||
case "ac":
|
||||
if d.Status == "on" {
|
||||
modeLabel := acModeLabel(d.Mode)
|
||||
sb.WriteString(fmt.Sprintf("- %s: 运行中 (%s%.0f°C)\n", d.Name, modeLabel, d.Temperature))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("- %s: 关闭\n", d.Name))
|
||||
}
|
||||
case "curtain":
|
||||
statusLabel := "已关闭"
|
||||
if d.Status == "open" {
|
||||
statusLabel = "已打开"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("- %s: %s\n", d.Name, statusLabel))
|
||||
case "sensor":
|
||||
sb.WriteString(fmt.Sprintf("- %s: %.1f%s\n", d.Name, d.Value, d.Unit))
|
||||
case "lock":
|
||||
statusLabel := "已锁定"
|
||||
if d.Status == "unlocked" {
|
||||
statusLabel = "已解锁"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("- %s: %s (电量%d%%)\n", d.Name, statusLabel, d.Battery))
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// DeviceInfo 设备信息(避免循环依赖的简化结构体)
|
||||
type DeviceInfo struct {
|
||||
Name string
|
||||
Type string
|
||||
Status string
|
||||
Brightness int
|
||||
Color string
|
||||
Temperature float64
|
||||
Mode string
|
||||
Value float64
|
||||
Unit string
|
||||
Battery int
|
||||
}
|
||||
|
||||
func acModeLabel(mode string) string {
|
||||
switch mode {
|
||||
case "cool":
|
||||
return "制冷"
|
||||
case "heat":
|
||||
return "制热"
|
||||
case "auto":
|
||||
return "自动"
|
||||
default:
|
||||
return mode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +60,12 @@ type openAIRequest struct {
|
||||
}
|
||||
|
||||
type openAIMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ToolCalls []openAIToolCall `json:"tool_calls,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ToolCalls []openAIToolCall `json:"tool_calls,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"` // DeepSeek 思考链
|
||||
}
|
||||
|
||||
// openAIToolCall OpenAI工具调用
|
||||
@@ -226,10 +227,11 @@ func (p *OpenAIProvider) doChat(ctx context.Context, messages []model.LLMMessage
|
||||
oaiMessages := make([]openAIMessage, len(messages))
|
||||
for i, msg := range messages {
|
||||
oaiMsg := openAIMessage{
|
||||
Role: string(msg.Role),
|
||||
Content: msg.Content,
|
||||
Name: msg.Name,
|
||||
ToolCallID: msg.ToolCallID,
|
||||
Role: string(msg.Role),
|
||||
Content: msg.Content,
|
||||
Name: msg.Name,
|
||||
ToolCallID: msg.ToolCallID,
|
||||
ReasoningContent: msg.ReasoningContent,
|
||||
}
|
||||
// 转换工具调用
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
@@ -303,8 +305,9 @@ func (p *OpenAIProvider) doChat(ctx context.Context, messages []model.LLMMessage
|
||||
// 检查是否有工具调用
|
||||
choice := oaiResp.Choices[0]
|
||||
llmResp := &model.LLMResponse{
|
||||
Content: choice.Message.Content,
|
||||
FinishReason: choice.FinishReason,
|
||||
Content: choice.Message.Content,
|
||||
FinishReason: choice.FinishReason,
|
||||
ReasoningContent: choice.Message.ReasoningContent,
|
||||
Usage: model.Usage{
|
||||
PromptTokens: oaiResp.Usage.PromptTokens,
|
||||
CompletionTokens: oaiResp.Usage.CompletionTokens,
|
||||
@@ -331,10 +334,11 @@ func (p *OpenAIProvider) doChatStream(ctx context.Context, messages []model.LLMM
|
||||
oaiMessages := make([]openAIMessage, len(messages))
|
||||
for i, msg := range messages {
|
||||
oaiMsg := openAIMessage{
|
||||
Role: string(msg.Role),
|
||||
Content: msg.Content,
|
||||
Name: msg.Name,
|
||||
ToolCallID: msg.ToolCallID,
|
||||
Role: string(msg.Role),
|
||||
Content: msg.Content,
|
||||
Name: msg.Name,
|
||||
ToolCallID: msg.ToolCallID,
|
||||
ReasoningContent: msg.ReasoningContent,
|
||||
}
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
oaiMsg.ToolCalls = make([]openAIToolCall, len(msg.ToolCalls))
|
||||
|
||||
@@ -14,11 +14,12 @@ const (
|
||||
|
||||
// LLMMessage 发送给LLM的消息
|
||||
type LLMMessage struct {
|
||||
Role Role `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Name string `json:"name,omitempty"` // 可选发送者名称
|
||||
ToolCallID string `json:"tool_call_id,omitempty"` // 工具调用关联ID (tool role 消息关联调用)
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // 助手消息中的工具调用列表
|
||||
Role Role `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Name string `json:"name,omitempty"` // 可选发送者名称
|
||||
ToolCallID string `json:"tool_call_id,omitempty"` // 工具调用关联ID (tool role 消息关联调用)
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // 助手消息中的工具调用列表
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"` // DeepSeek 思考链内容(需回传)
|
||||
}
|
||||
|
||||
// ChatMessage 数据库存储的对话消息
|
||||
@@ -34,10 +35,11 @@ type ChatMessage struct {
|
||||
|
||||
// LLMResponse LLM返回的响应
|
||||
type LLMResponse struct {
|
||||
Content string `json:"content"`
|
||||
FinishReason string `json:"finish_reason"` // stop | length | tool_calls
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
Usage Usage `json:"usage,omitempty"`
|
||||
Content string `json:"content"`
|
||||
FinishReason string `json:"finish_reason"` // stop | length | tool_calls
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
Usage Usage `json:"usage,omitempty"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"` // DeepSeek 思考链内容
|
||||
}
|
||||
|
||||
// ToolCall 工具调用
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
|
||||
@@ -142,5 +141,3 @@ func isSentenceEnd(r rune) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Ensure unicode is used
|
||||
var _ = unicode.Is
|
||||
|
||||
Reference in New Issue
Block a user