feat: Round 5 - Memory Service, Tool Engine, Call Records, Thinking Logs

- Fix: Session history flash (race condition + WS guard)
- Fix: Chat background overlay + sidebar transparency
- Fix: IoT device control (Chinese action names, status field)
- Feat: Independent memory-service (port 8091, 13 endpoints)
- Feat: Independent tool-engine service (port 8092, 13 tools)
- Feat: Tool call logs with paginated DevTools panel
- Feat: Thinking log records with DevTools panel
- Feat: Future development roadmap document
- Chore: Updated .gitignore, go.work, DevTools config
- Chore: 5-service health check, project review docs
This commit is contained in:
2026-05-18 20:05:14 +08:00
parent b6ec36886c
commit 78e3f450c2
54 changed files with 7846 additions and 106 deletions
@@ -0,0 +1,318 @@
package tools
import (
"context"
"crypto/rand"
"encoding/json"
"fmt"
"math/big"
mathrand "math/rand"
"strings"
"github.com/yourname/cyrene-ai/tool-engine/internal/model"
)
// RandomTool provides random generation utilities for the LLM.
type RandomTool struct{}
// NewRandomTool creates a random generation tool.
func NewRandomTool() *RandomTool {
return &RandomTool{}
}
// Definition returns the tool definition for LLM function calling.
func (t *RandomTool) Definition() model.ToolDefinition {
return model.ToolDefinition{
Name: "random",
Description: "随机生成工具。生成随机数、UUID、安全密码,或从列表中随机选取/打乱元素。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"number", "uuid", "password", "pick", "shuffle"},
"description": "操作类型。number: 生成随机整数;uuid: 生成UUID v4password: 生成安全密码;pick: 从列表随机选取;shuffle: 随机打乱列表",
},
"min": map[string]interface{}{
"type": "number",
"description": "随机数最小值(用于 number 操作),默认 0",
},
"max": map[string]interface{}{
"type": "number",
"description": "随机数最大值(用于 number 操作),默认 100",
},
"length": map[string]interface{}{
"type": "integer",
"description": "密码长度(用于 password 操作),默认 16",
},
"items": map[string]interface{}{
"type": "array",
"description": "列表项(用于 pick/shuffle 操作),字符串数组",
"items": map[string]interface{}{
"type": "string",
},
},
"count": map[string]interface{}{
"type": "integer",
"description": "选取数量(用于 pick 操作),默认 1",
},
},
"required": []string{"action"},
},
}
}
// Execute performs random generation operations.
func (t *RandomTool) Execute(ctx context.Context, arguments map[string]interface{}) (*model.ToolResult, error) {
action, ok := arguments["action"].(string)
if !ok || action == "" {
return &model.ToolResult{ID: "", Error: "缺少 action 参数"}, nil
}
switch action {
case "number":
return t.handleNumber(arguments)
case "uuid":
return t.handleUUID()
case "password":
return t.handlePassword(arguments)
case "pick":
return t.handlePick(arguments)
case "shuffle":
return t.handleShuffle(arguments)
default:
return &model.ToolResult{
ID: "",
Error: fmt.Sprintf("未知操作: %s,支持: number, uuid, password, pick, shuffle", action),
}, nil
}
}
func (t *RandomTool) handleNumber(arguments map[string]interface{}) (*model.ToolResult, error) {
minVal := getFloatArg(arguments, "min", 0)
maxVal := getFloatArg(arguments, "max", 100)
if minVal > maxVal {
minVal, maxVal = maxVal, minVal
}
minI := int64(minVal)
maxI := int64(maxVal)
rangeVal := maxI - minI + 1
if rangeVal <= 0 {
return &model.ToolResult{ID: "", Error: "无效的数值范围"}, nil
}
n, err := rand.Int(rand.Reader, big.NewInt(rangeVal))
if err != nil {
result := minI + mathrand.Int63n(rangeVal)
return &model.ToolResult{
ID: "",
Output: fmt.Sprintf("随机整数 [%d, %d]: %d", minI, maxI, result),
}, nil
}
result := minI + n.Int64()
return &model.ToolResult{
ID: "",
Output: fmt.Sprintf("随机整数 [%d, %d]: %d", minI, maxI, result),
}, nil
}
func (t *RandomTool) handleUUID() (*model.ToolResult, error) {
uuid := make([]byte, 16)
_, err := rand.Read(uuid)
if err != nil {
return &model.ToolResult{ID: "", Error: fmt.Sprintf("生成UUID失败: %v", err)}, nil
}
uuid[6] = (uuid[6] & 0x0f) | 0x40
uuid[8] = (uuid[8] & 0x3f) | 0x80
uuidStr := fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16])
return &model.ToolResult{
ID: "",
Output: fmt.Sprintf("UUID v4: %s", uuidStr),
}, nil
}
func (t *RandomTool) handlePassword(arguments map[string]interface{}) (*model.ToolResult, error) {
length := getIntArg(arguments, "length", 16)
if length < 4 {
length = 16
}
if length > 128 {
length = 128
}
uppercase := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lowercase := "abcdefghijklmnopqrstuvwxyz"
digits := "0123456789"
symbols := "!@#$%^&*()_+-=[]{}|;:,.<>?"
allChars := uppercase + lowercase + digits + symbols
password := make([]byte, length)
password[0] = uppercase[secureIndex(len(uppercase))]
password[1] = lowercase[secureIndex(len(lowercase))]
password[2] = digits[secureIndex(len(digits))]
password[3] = symbols[secureIndex(len(symbols))]
for i := 4; i < length; i++ {
password[i] = allChars[secureIndex(len(allChars))]
}
shuffleBytes(password)
passwordStr := string(password)
return &model.ToolResult{
ID: "",
Output: fmt.Sprintf("安全密码 (长度: %d):\n%s\n\n字符集: 大写字母 + 小写字母 + 数字 + 特殊符号",
length, passwordStr),
}, nil
}
func (t *RandomTool) handlePick(arguments map[string]interface{}) (*model.ToolResult, error) {
items := getStringSliceArg(arguments, "items")
if len(items) == 0 {
return &model.ToolResult{ID: "", Error: "缺少 items 参数或列表为空"}, nil
}
count := getIntArg(arguments, "count", 1)
if count < 1 {
count = 1
}
if count > len(items) {
count = len(items)
}
indices := make([]int, len(items))
for i := range indices {
indices[i] = i
}
shuffleInts(indices)
picked := make([]string, 0, count)
for i := 0; i < count; i++ {
picked = append(picked, items[indices[i]])
}
var result strings.Builder
result.WriteString(fmt.Sprintf("从 %d 个选项中随机选取 %d 个:\n", len(items), count))
for i, p := range picked {
result.WriteString(fmt.Sprintf(" %d. %s\n", i+1, p))
}
return &model.ToolResult{ID: "", Output: result.String()}, nil
}
func (t *RandomTool) handleShuffle(arguments map[string]interface{}) (*model.ToolResult, error) {
items := getStringSliceArg(arguments, "items")
if len(items) == 0 {
return &model.ToolResult{ID: "", Error: "缺少 items 参数或列表为空"}, nil
}
shuffled := make([]string, len(items))
copy(shuffled, items)
shuffleStrings(shuffled)
var result strings.Builder
result.WriteString(fmt.Sprintf("随机打乱结果 (共 %d 项):\n", len(shuffled)))
for i, s := range shuffled {
result.WriteString(fmt.Sprintf(" %d. %s\n", i+1, s))
}
return &model.ToolResult{ID: "", Output: result.String()}, nil
}
// --- Helper functions ---
func getFloatArg(arguments map[string]interface{}, key string, fallback float64) float64 {
if v, ok := arguments[key]; ok {
switch val := v.(type) {
case float64:
return val
case int:
return float64(val)
case int64:
return float64(val)
case json.Number:
f, err := val.Float64()
if err == nil {
return f
}
}
}
return fallback
}
func getIntArg(arguments map[string]interface{}, key string, fallback int) int {
if v, ok := arguments[key]; ok {
switch val := v.(type) {
case float64:
return int(val)
case int:
return val
case int64:
return int(val)
}
}
return fallback
}
func getStringSliceArg(arguments map[string]interface{}, key string) []string {
if v, ok := arguments[key]; ok {
switch val := v.(type) {
case []interface{}:
result := make([]string, 0, len(val))
for _, item := range val {
if s, ok := item.(string); ok {
result = append(result, s)
} else {
result = append(result, fmt.Sprintf("%v", item))
}
}
return result
case []string:
return val
}
}
return nil
}
func secureIndex(max int) int {
if max <= 1 {
return 0
}
n, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
if err != nil {
return mathrand.Intn(max)
}
return int(n.Int64())
}
func shuffleBytes(data []byte) {
for i := len(data) - 1; i > 0; i-- {
j := secureIndex(i + 1)
data[i], data[j] = data[j], data[i]
}
}
func shuffleInts(data []int) {
for i := len(data) - 1; i > 0; i-- {
j := secureIndex(i + 1)
data[i], data[j] = data[j], data[i]
}
}
func shuffleStrings(data []string) {
for i := len(data) - 1; i > 0; i-- {
j := secureIndex(i + 1)
data[i], data[j] = data[j], data[i]
}
}