Files
Cyrene/backend/ai-core/cmd/main.go
T
AskaEth 78e3f450c2 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
2026-05-18 20:05:14 +08:00

759 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/joho/godotenv"
"github.com/yourname/cyrene-ai/ai-core/internal/background"
ctxbuild "github.com/yourname/cyrene-ai/ai-core/internal/context"
"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/orchestrator"
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
"github.com/yourname/cyrene-ai/ai-core/internal/tools"
)
func main() {
// 自动加载 .env 文件(来自 backend/.env
if err := godotenv.Load("../.env"); err != nil {
log.Println("ℹ 未找到 .env 文件,将使用环境变量或默认值")
}
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("🧠 AI-Core 服务启动中...")
// 加载配置
cfg := loadConfig()
// 初始化人格加载器
personaDir := cfg.PersonaDir
if personaDir == "" {
personaDir = "./internal/persona"
}
personaLoader, err := persona.NewLoader(personaDir)
if err != nil {
log.Fatalf("加载人格配置失败: %v", err)
}
log.Printf("已加载 %d 个人格: %v", len(personaLoader.List()), personaLoader.List())
// 初始化LLM适配器
llmProvider := llm.NewOpenAIProvider(llm.OpenAIConfig{
BaseURL: cfg.LLMBaseURL,
APIKey: cfg.LLMAPIKey,
Model: cfg.LLMModel,
FallbackModel: cfg.LLMFallbackModel,
Timeout: 120 * time.Second,
})
llmAdapter := llm.NewAdapter(llmProvider)
log.Printf("LLM适配器已就绪: 模型=%s", llmAdapter.ModelName())
// 初始化记忆系统
var memStore *memory.Store
var memRetriever *memory.Retriever
var memExtractor *memory.Extractor
if cfg.DatabaseURL != "" {
memStore = memory.NewStore(cfg.DatabaseURL)
defer memStore.Close()
memRetriever = memory.NewRetriever(memStore, nil)
// 记忆提取器使用LLM
memExtractor = memory.NewExtractor(memStore, func(ctx context.Context, messages []model.LLMMessage) (*model.LLMResponse, error) {
return llmAdapter.Chat(ctx, messages)
})
log.Println("记忆提取器已就绪")
}
// 初始化会话历史存储
convStore := ctxbuild.NewConversationStore(50)
log.Println("会话历史存储已就绪 (上限50条)")
// 初始化上下文构建器
ctxBuilder := ctxbuild.NewBuilder(convStore)
// 初始化 IoT 客户端
var iotClient *tools.IoTClient
if cfg.IoTServiceURL != "" {
iotClient = tools.NewIoTClient(cfg.IoTServiceURL)
log.Printf("IoT 客户端已就绪: %s", cfg.IoTServiceURL)
} else {
log.Println("IoT 客户端未配置 (IOT_DEBUG_SERVICE_URL 为空)")
}
// 初始化工具注册中心
toolRegistry := tools.NewRegistry()
if getEnvBool("ENABLE_TOOLS", true) {
toolRegistry.Register(tools.NewWebFetchTool())
toolRegistry.Register(tools.NewWebSearchTool())
toolRegistry.Register(tools.NewCalculatorTool())
toolRegistry.Register(tools.NewDateTimeTool())
toolRegistry.Register(tools.NewHTTPTool())
toolRegistry.Register(tools.NewJSONTool())
toolRegistry.Register(tools.NewTextTool())
toolRegistry.Register(tools.NewRandomTool())
toolRegistry.Register(tools.NewCryptoTool())
toolRegistry.Register(tools.NewMarkdownTool())
// File tool uses DATA_DIR or defaults to /tmp/cyrene_data
dataDir := getEnv("DATA_DIR", "/tmp/cyrene_data")
toolRegistry.Register(tools.NewFileTool(dataDir))
if iotClient != nil {
toolRegistry.Register(tools.NewIoTQueryTool(iotClient))
toolRegistry.Register(tools.NewIoTControlTool(iotClient))
}
log.Printf("工具注册中心已就绪: %d 个工具 (%v)", len(toolRegistry.ListTools()), toolRegistry.ListTools())
}
// 初始化后台思考器(增强版:支持工具调用和记忆管理)
thinkerCfg := background.DefaultThinkerConfig()
adminUserID := "admin_admin"
adminSessionID := "admin-session-main"
// 创建记忆服务 HTTP 客户端(用于持久化思考日志到 memory-service
memServiceURL := getEnv("MEMORY_SERVICE_URL", "http://localhost:8091")
memClient := memory.NewClient(memServiceURL)
log.Printf("记忆服务客户端已就绪: %s", memServiceURL)
thinker := background.NewThinker(
thinkerCfg,
personaLoader,
memRetriever,
llmAdapter,
iotClient,
memStore,
memExtractor,
toolRegistry,
convStore,
adminUserID,
adminSessionID,
memClient,
)
thinker.Start()
defer thinker.Stop()
// 健康检查与对话API的HTTP mux
mux := http.NewServeMux()
// 手动构建 orchestrator 用于处理(因为现有orchestrator结构体已定义但未导出构造函数)
orch := &orchestrator.Orchestrator{}
// 注册对话API端点
mux.HandleFunc("/api/v1/chat", func(w http.ResponseWriter, r *http.Request) {
handleChat(w, r, orch, ctxBuilder, llmAdapter, personaLoader, memRetriever, memExtractor, iotClient, thinker, toolRegistry)
})
// 注册记忆API端点
mux.HandleFunc("/api/v1/memory/search", func(w http.ResponseWriter, r *http.Request) {
handleMemorySearch(w, r, memRetriever)
})
mux.HandleFunc("/api/v1/memory", func(w http.ResponseWriter, r *http.Request) {
handleMemoryCRUD(w, r, memStore, memExtractor)
})
mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok","service":"ai-core","model":"` + llmAdapter.ModelName() + `"}`))
})
// 启动HTTP服务
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: mux,
}
go func() {
log.Printf("🚀 AI-Core 服务已启动在端口 %s", cfg.Port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("服务启动失败: %v", err)
}
}()
// 优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("正在关闭 AI-Core 服务...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)
log.Println("AI-Core 服务已关闭")
}
// Config AI-Core配置
type Config struct {
Port string
PersonaDir string
LLMBaseURL string
LLMAPIKey string
LLMModel string
LLMFallbackModel string
DatabaseURL string
IoTServiceURL string
}
func loadConfig() Config {
return Config{
Port: getEnv("AI_CORE_PORT", "8081"),
PersonaDir: getEnv("PERSONA_DIR", "./internal/persona"),
LLMBaseURL: getEnv("LLM_API_URL", "https://api.openai.com/v1"),
LLMAPIKey: getEnv("LLM_API_KEY", ""),
LLMModel: getEnv("LLM_MODEL", "gpt-4o"),
LLMFallbackModel: getEnv("LLM_FALLBACK_MODEL", "gpt-4o-mini"),
DatabaseURL: buildDatabaseURL(),
IoTServiceURL: getEnv("IOT_DEBUG_SERVICE_URL", ""),
}
}
func buildDatabaseURL() string {
host := getEnv("POSTGRES_HOST", "localhost")
port := getEnv("POSTGRES_PORT", "5432")
user := getEnv("POSTGRES_USER", "cyrene")
password := getEnv("POSTGRES_PASSWORD", "change_me")
dbname := getEnv("POSTGRES_DB", "cyrene_ai")
sslmode := getEnv("POSTGRES_SSLMODE", "disable")
return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s",
user, password, host, port, dbname, sslmode)
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
func getEnvBool(key string, fallback bool) bool {
v := os.Getenv(key)
if v == "" {
return fallback
}
switch strings.ToLower(v) {
case "true", "1", "yes", "on":
return true
case "false", "0", "no", "off":
return false
default:
return fallback
}
}
// buildOpenAITools 将工具注册中心的定义转换为 LLM 层的 OpenAITool 格式
func buildOpenAITools(registry *tools.Registry) []llm.OpenAITool {
if registry == nil || !registry.IsEnabled() {
return nil
}
defs := registry.GetDefinitions()
if len(defs) == 0 {
return nil
}
result := make([]llm.OpenAITool, 0, len(defs))
for _, d := range defs {
result = append(result, llm.OpenAITool{
Type: "function",
Function: llm.OpenAIToolFunc{
Name: d.Name,
Description: d.Description,
Parameters: d.Parameters,
},
})
}
return result
}
// handleChat 处理对话请求(SSE 流式响应 + 工具调用)
func handleChat(
w http.ResponseWriter,
r *http.Request,
_ *orchestrator.Orchestrator,
ctxBuilder *ctxbuild.Builder,
llmAdapter *llm.Adapter,
personaLoader *persona.Loader,
memRetriever *memory.Retriever,
memExtractor *memory.Extractor,
iotClient *tools.IoTClient,
thinker *background.Thinker,
toolRegistry *tools.Registry,
) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 解析请求
var req struct {
UserID string `json:"user_id"`
SessionID string `json:"session_id"`
Message string `json:"message"`
Mode string `json:"mode"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的请求体", http.StatusBadRequest)
return
}
if req.Mode == "" {
req.Mode = "text"
}
ctx := r.Context()
// 0. 记录用户活动(重置闲置计时器)
if thinker != nil {
thinker.RecordUserMessage()
}
// 0.1 缓存用户消息到会话历史
ctxBuilder.CacheMessage(req.SessionID, model.RoleUser, req.Message)
// 1. 检索相关记忆
var memories []memory.MemoryEntry
if memRetriever != nil {
var err error
memories, err = memRetriever.Retrieve(ctx, req.UserID, req.Message)
if err != nil {
log.Printf("[chat] 记忆检索失败: %v", err)
}
}
// 2. 加载人格配置
personaConfig, err := personaLoader.Get("cyrene")
if err != nil {
http.Error(w, fmt.Sprintf("加载人格失败: %v", err), http.StatusInternalServerError)
return
}
// 2.1 始终获取 IoT 设备状态(去掉关键词门控,让昔涟始终了解家里的状态)
var deviceContext string
if iotClient != nil {
devices := iotClient.GetDevicesForContext()
if len(devices) > 0 {
deviceInfos := make([]ctxbuild.DeviceInfo, 0, len(devices))
for _, d := range devices {
deviceInfos = append(deviceInfos, ctxbuild.DeviceInfo{
Name: d.Name,
Type: d.Type,
Status: d.Status,
Brightness: d.Brightness,
Color: d.Color,
Temperature: d.Temperature,
Mode: d.Mode,
Value: d.Value,
Unit: d.Unit,
Battery: d.Battery,
})
}
deviceContext = ctxbuild.InjectDeviceContext(deviceInfos)
log.Printf("[chat] 已注入 IoT 设备状态 (%d 个设备)", len(deviceInfos))
}
}
// 2.2 获取待处理的后台思考
var pendingThoughts []string
if thinker != nil && thinker.HasPendingThoughts() {
pts := thinker.GetPendingThoughts()
for _, pt := range pts {
if pt.Content != "" {
pendingThoughts = append(pendingThoughts, pt.Content)
}
}
if len(pendingThoughts) > 0 {
log.Printf("[chat] 注入 %d 条后台思考到上下文", len(pendingThoughts))
}
}
// 3. 构建对话上下文
llmMessages, err := ctxBuilder.Build(ctx, ctxbuild.BuildParams{
UserID: req.UserID,
SessionID: req.SessionID,
UserMessage: req.Message,
Persona: personaConfig,
Memories: memories,
HistoryLimit: 20,
DeviceContext: deviceContext,
PendingThoughts: pendingThoughts,
})
if err != nil {
http.Error(w, fmt.Sprintf("构建上下文失败: %v", err), http.StatusInternalServerError)
return
}
// 4. 设置 SSE 响应头
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming not supported", http.StatusInternalServerError)
return
}
// 5. 准备工具定义
openAITools := buildOpenAITools(toolRegistry)
// 5.1 如果启用了工具,先进行同步调用检测是否需要工具调用
if len(openAITools) > 0 {
log.Printf("[chat] 启用工具调用: %d 个工具可用", len(openAITools))
syncResp, syncErr := llmAdapter.ChatWithTools(ctx, llmMessages, openAITools)
if syncErr != nil {
log.Printf("[chat] 工具检测调用失败: %v,降级为普通对话", syncErr)
} else if len(syncResp.ToolCalls) > 0 {
log.Printf("[chat] 模型请求 %d 个工具调用", len(syncResp.ToolCalls))
// 将助手消息(含工具调用)加入上下文
assistantMsg := model.LLMMessage{
Role: model.RoleAssistant,
Content: syncResp.Content,
ToolCalls: syncResp.ToolCalls,
ReasoningContent: syncResp.ReasoningContent,
}
llmMessages = append(llmMessages, assistantMsg)
// 执行每个工具调用并将结果加入上下文
for _, tc := range syncResp.ToolCalls {
var args map[string]interface{}
if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil {
log.Printf("[chat] 工具 %s 参数解析失败: %v", tc.Name, err)
args = make(map[string]interface{})
}
result, execErr := toolRegistry.Execute(ctx, tc.Name, args)
if execErr != nil {
log.Printf("[chat] 工具 %s 执行失败: %v", tc.Name, execErr)
}
resultJSON, _ := json.Marshal(result)
llmMessages = append(llmMessages, model.LLMMessage{
Role: model.RoleTool,
Content: string(resultJSON),
ToolCallID: tc.ID,
})
}
}
// 无论是否有工具调用,继续流式输出最终回复
}
// 5.2 调用LLM流式接口(可能已附加工具结果)
chunkCh, err := llmAdapter.ChatStream(ctx, llmMessages)
if err != nil {
// 流式初始化失败,返回 SSE 格式错误
errData, _ := json.Marshal(map[string]string{"delta": "", "error": fmt.Sprintf("LLM调用失败: %v", err)})
fmt.Fprintf(w, "data: %s\n\n", errData)
flusher.Flush()
fmt.Fprintf(w, "data: [DONE]\n\n")
flusher.Flush()
return
}
messageID := fmt.Sprintf("msg-%d", time.Now().UnixNano())
// 6. 逐 token 推送 SSE
var fullContent string
var segments []llm.Segment
segmenter := llm.NewSegmenter()
for chunk := range chunkCh {
if chunk.Error != nil {
log.Printf("[chat] 流式错误: %v", chunk.Error)
errData, _ := json.Marshal(map[string]string{"delta": "", "error": chunk.Error.Error()})
fmt.Fprintf(w, "data: %s\n\n", errData)
flusher.Flush()
fmt.Fprintf(w, "data: [DONE]\n\n")
flusher.Flush()
return
}
if chunk.Done {
// 流结束,flush 剩余片段
if remaining := segmenter.Flush(); remaining != nil {
segments = append(segments, *remaining)
}
break
}
if chunk.Content != "" {
fullContent += chunk.Content
// 实时断句
newSegs := segmenter.Feed(chunk.Content)
segments = append(segments, newSegs...)
deltaData, _ := json.Marshal(map[string]string{
"delta": chunk.Content,
"message_id": messageID,
})
fmt.Fprintf(w, "data: %s\n\n", deltaData)
flusher.Flush()
}
}
// 7. 发送结束标记(附带元数据)
endData, _ := json.Marshal(map[string]interface{}{
"message_id": messageID,
"mode": req.Mode,
"segments": segments,
"done": true,
})
fmt.Fprintf(w, "data: %s\n\n", endData)
flusher.Flush()
fmt.Fprintf(w, "data: [DONE]\n\n")
flusher.Flush()
// 8. 缓存 LLM 回复到会话历史
if fullContent != "" {
ctxBuilder.CacheMessage(req.SessionID, model.RoleAssistant, fullContent)
}
// 9. 异步提取记忆
if memExtractor != nil && fullContent != "" {
go memExtractor.ExtractAndStore(context.Background(), req.UserID, req.SessionID, req.Message, fullContent)
}
}
// handleMemorySearch 处理记忆搜索请求
func handleMemorySearch(
w http.ResponseWriter,
r *http.Request,
memRetriever *memory.Retriever,
) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
userID := r.URL.Query().Get("user_id")
if userID == "" {
http.Error(w, "缺少 user_id 参数", http.StatusBadRequest)
return
}
query := r.URL.Query().Get("q")
if query == "" {
http.Error(w, "缺少 q 参数", http.StatusBadRequest)
return
}
if memRetriever == nil {
log.Printf("[memory] 记忆检索器未初始化: 数据库不可用")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"query": query,
"memories": []interface{}{},
"error": "记忆系统未就绪",
"errorType": "memory_store_unavailable",
"hint": "PostgreSQL 数据库不可用,请检查数据库连接配置",
})
return
}
ctx := r.Context()
memories, err := memRetriever.Retrieve(ctx, userID, query)
if err != nil {
log.Printf("[memory] 检索失败: %v", err)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"query": query,
"memories": []interface{}{},
"error": fmt.Sprintf("检索失败: %v", err),
"errorType": "retrieve_failed",
})
return
}
if memories == nil {
memories = []memory.MemoryEntry{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"query": query,
"memories": memories,
"total": len(memories),
})
}
// handleMemoryCRUD 处理记忆的 CRUD 操作
func handleMemoryCRUD(
w http.ResponseWriter,
r *http.Request,
memStore *memory.Store,
memExtractor *memory.Extractor,
) {
switch r.Method {
case http.MethodGet:
// 列出用户的所有记忆
userID := r.URL.Query().Get("user_id")
if userID == "" {
http.Error(w, "缺少 user_id 参数", http.StatusBadRequest)
return
}
if memStore == nil {
log.Printf("[memory] 记忆存储未初始化: 数据库不可用")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"memories": []interface{}{},
"error": "记忆系统未就绪",
"errorType": "memory_store_unavailable",
"hint": "PostgreSQL 数据库不可用,请检查数据库连接配置",
})
return
}
ctx := r.Context()
memories, err := memStore.Query(ctx, model.MemoryQuery{
UserID: userID,
Limit: 50,
})
if err != nil {
log.Printf("[memory] 查询失败: %v", err)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"memories": []interface{}{},
"error": fmt.Sprintf("查询失败: %v", err),
"errorType": "query_failed",
})
return
}
if memories == nil {
memories = []model.MemoryEntry{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"memories": memories,
"total": len(memories),
})
case http.MethodDelete:
// 删除单条记忆: DELETE /api/v1/memory?id=xxx
memoryID := r.URL.Query().Get("id")
if memoryID == "" {
http.Error(w, "缺少 id 参数", http.StatusBadRequest)
return
}
if memStore == nil {
log.Printf("[memory] 记忆存储未初始化: 无法删除")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "记忆系统未就绪",
"errorType": "memory_store_unavailable",
"hint": "PostgreSQL 数据库不可用,请检查数据库连接配置",
})
return
}
ctx := r.Context()
if err := memStore.Delete(ctx, memoryID); err != nil {
log.Printf("[memory] 删除失败: %v", err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": fmt.Sprintf("删除失败: %v", err),
"errorType": "delete_failed",
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "deleted",
"memory_id": memoryID,
})
case http.MethodPost:
// 手动添加记忆
var req struct {
UserID string `json:"user_id"`
Content string `json:"content"`
Category string `json:"category"`
Priority int `json:"priority"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的请求体", http.StatusBadRequest)
return
}
if req.UserID == "" || req.Content == "" {
http.Error(w, "缺少 user_id 或 content", http.StatusBadRequest)
return
}
if req.Category == "" {
req.Category = "other"
}
if req.Priority <= 0 {
req.Priority = 1
}
if memStore == nil {
log.Printf("[memory] 记忆存储未初始化: 无法保存")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "记忆系统未就绪",
"errorType": "memory_store_unavailable",
"hint": "PostgreSQL 数据库不可用,请检查数据库连接配置",
})
return
}
entry := &model.MemoryEntry{
UserID: req.UserID,
Content: req.Content,
Category: model.MemoryCategory(req.Category),
Priority: model.MemoryPriority(req.Priority),
}
ctx := r.Context()
if err := memStore.Save(ctx, entry); err != nil {
log.Printf("[memory] 保存失败: %v", err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": fmt.Sprintf("保存失败: %v", err),
"errorType": "save_failed",
})
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "saved",
"memory": entry,
})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}