Files

1 line
35 KiB
JSON

{"repo": "cebernic/deepnow", "files": {"src/api_admin.go": {"053639d902c4bbc77c0ebd8e799cc4a2460bdace": {"t": 1785746075.0415134, "n": 14699, "h": "b846cbf70a05f124", "c": "// api_admin.go - 管理后台业务逻辑与接口\n// 2025.12 Revision,HEAD 232\n// by szyok\npackage main\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode/utf8\"\n\n\t\"golang.org/x/text/encoding/simplifiedchinese\"\n\t\"golang.org/x/text/transform\"\n)\n\nfunc sendJSON(w http.ResponseWriter, success bool, msg string, data interface{}) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tjson.NewEncoder(w).Encode(map[string]interface{}{\"success\": success, \"message\": msg, \"data\": data})\n}\n\nfunc handleGetSystemStatus(w http.ResponseWriter, r *http.Request) {\n\tpri := getActiveModel(\"LM\", \"primary\")\n\tsec := getActiveModel(\"LM\", \"slave\")\n\trag := getActiveModel(\"Embedding\", \"\")\n\tsendJSON(w, true, \"\", map[string]interface{}{\"needs_setup\": pri == nil, \"active_pri\": pri, \"active_sec\": sec, \"active_rag\": rag})\n}\n\nfunc handleGetBuildInfo(w http.ResponseWriter, r *http.Request) {\n\texePath, err := os.Executable()\n\tbuildTime := \"Unknown\"\n\tif err == nil {\n\t\tif info, err := os.Stat(exePath); err == nil {\n\t\t\tbuildTime = info.ModTime().Format(\"20060102_150405\")\n\t\t}\n\t}\n\tsendJSON(w, true, \"\", buildTime)\n}\n\nfunc handleAdminLogin(w http.ResponseWriter, r *http.Request) {\n\tu, p := r.FormValue(\"username\"), r.FormValue(\"password\")\n\tvar dbP string\n\tif sysDB.QueryRow(`SELECT password FROM admin_users WHERE username=?`, u).Scan(&dbP) == nil && dbP == p {\n\t\thttp.SetCookie(w, &http.Cookie{Name: \"session\", Value: \"active\", Path: \"/\"})\n\t\tsendJSON(w, true, \"登录成功\", nil)\n\t} else {\n\t\tsendJSON(w, false, \"账号或密码错误\", nil)\n\t}\n}\n\nfunc handleGetConfig(w http.ResponseWriter, r *http.Request) {\n\tsendJSON(w, true, \"获取配置\", config)\n}\nfunc handleSaveConfig(w http.ResponseWriter, r *http.Request) {\n\tjson.NewDecoder(r.Body).Decode(&config)\n\tsaveConfig()\n\tsendJSON(w, true, \"配置已保存\", nil)\n}\n\nfunc handleGenerateKeys(w http.ResponseWriter, r *http.Request) {\n\tvar keys []string\n\trand.Seed(time.Now().UnixNano())\n\tfor i := 0; i < 5; i++ {\n\t\tkeys = append(keys, fmt.Sprintf(\"sk-aibase-%x%d\", time.Now().UnixNano(), rand.Intn(1000)))\n\t}\n\tsendJSON(w, true, \"生成成功\", keys)\n}\nfunc handleImportKeys(w http.ResponseWriter, r *http.Request) {\n\tvar keys []string\n\tjson.NewDecoder(r.Body).Decode(&keys)\n\tfor _, k := range keys {\n\t\tkeyDB.Exec(`INSERT OR IGNORE INTO access_keys (api_key, used, created_at) VALUES (?, 0, datetime('now'))`, k)\n\t\tauthKeys.Store(k, 0)\n\t}\n\tsendJSON(w, true, \"已入库\", nil)\n}\nfunc handleListKeys(w http.ResponseWriter, r *http.Request) {\n\trows, _ := keyDB.Query(`SELECT api_key, used FROM access_keys ORDER BY created_at DESC`)\n\tdefer rows.Close()\n\ttype K struct {\n\t\tKey string `json:\"key\"`\n\t\tUsed bool `json:\"used\"`\n\t}\n\tvar res []K\n\tfor rows.Next() {\n\t\tvar k string\n\t\tvar u int\n\t\trows.Scan(&k, &u)\n\t\tres = append(res, K{Key: k, Used: u > 0})\n\t}\n\tsendJSON(w, true, \"\", res)\n}\nfunc handleDeleteKeys(w http.ResponseWriter, r *http.Request) {\n\tvar keys []string\n\tjson.NewDecoder(r.Body).Decode(&keys)\n\tfor _, key := range keys {\n\t\tkeyDB.Exec(`DELETE FROM access_keys WHERE api_key=?`, key)\n\t\tauthKeys.Delete(key)\n\t}\n\tsendJSON(w, true, \"删除成功\", nil)\n}\n\nfunc handleGetModelConfigs(w http.ResponseWriter, r *http.Request) {\n\trows, _ := sysDB.Query(`SELECT provider, api_type, api_key, model_name, ip_port FROM model_configs`)\n\tdefer rows.Close()\n\tvar res []ModelConfig\n\tfor rows.Next() {\n\t\tvar c ModelConfig\n\t\trows.Scan(&c.Provider, &c.ApiType, &c.ApiKey, &c.ModelName, &c.IpPort)\n\t\tres = append(res, c)\n\t}\n\tsendJSON(w, true, \"获取模型配置\", res)\n}\n\nfunc handleSaveModelConfig(w http.ResponseWriter, r *http.Request) {\n\tvar req ModelConfig\n\tjson.NewDecoder(r.Body).Decode(&req)\n\tsysDB.Exec(`INSERT OR REPLACE INTO model_configs (provider, api_type, api_key, model_name, ip_port) VALUES (?, ?, ?, ?, ?)`,\n\t\treq.Provider, req.ApiType, req.ApiKey, req.ModelName, req.IpPort)\n\tautoActivateModels()\n\tsendJSON(w, true, \"算力配置已保存\", nil)\n}\n\nfunc handleActivateModel(w http.ResponseWriter, r *http.Request) {\n\tvar req struct {\n\t\tPri string\n\t\tSec string\n\t\tRAG string\n\t}\n\tjson.NewDecoder(r.Body).Decode(&req)\n\n\tvalPri := req.Pri\n\tif valPri == \"\" {\n\t\tvalPri = \"none\"\n\t}\n\tsysDB.Exec(`INSERT OR REPLACE INTO system_rules (key, value) VALUES ('active_lm_pri', ?)`, valPri)\n\n\tvalSec := req.Sec\n\tif valSec == \"\" {\n\t\tvalSec = \"none\"\n\t}\n\tsysDB.Exec(`INSERT OR REPLACE INTO system_rules (key, value) VALUES ('active_lm_sec', ?)`, valSec)\n\n\tvalRAG := req.RAG\n\tif valRAG == \"\" {\n\t\tvalRAG = \"none\"\n\t}\n\tsysDB.Exec(`INSERT OR REPLACE INTO system_rules (key, value) VALUES ('active_embedding', ?)`, valRAG)\n\n\tif valRAG == \"none\" {\n\t\tsysDB.Exec(`UPDATE system_rules SET value='0' WHERE key='rag_slider'`)\n\t}\n\n\tfmt.Println(\"--- [人工重新调度模型] ---\")\n\tpri := getActiveModel(\"LM\", \"primary\")\n\tif pri != nil {\n\t\tfmt.Printf(\"✅ 大模型 [%s] (%s) (Primary) 现在启用\\n\", pri.Provider, pri.ModelName)\n\t}\n\n\tsec := getActiveModel(\"LM\", \"slave\")\n\tif sec != nil {\n\t\tfmt.Printf(\"✅ 大模型 [%s] (%s) (Slave) 现在启用\\n\", sec.Provider, sec.ModelName)\n\t}\n\n\trag := getActiveModel(\"Embedding\", \"\")\n\tif rag != nil {\n\t\tfmt.Printf(\"✅ Embedding 模型 [%s] (%s) 现在启用\\n\", rag.Provider, rag.ModelName)\n\t} else {\n\t\tfmt.Printf(\"⚠️ Embedding 模型 现在人工停用 (纯大模型模式,规则滑块已归零)\\n\")\n\t}\n\tfmt.Println(\"--------------------------\")\n\n\tsendJSON(w, true, \"算力挂载配置已应用\", nil)\n}\n\nfunc splitTextWithOverlap(text string, chunkSize int, overlapSize int) []string {\n\trunes := []rune(text)\n\ttotalLen := len(runes)\n\tvar chunks []string\n\n\tif totalLen <= chunkSize {\n\t\treturn []string{text}\n\t}\n\tstep := chunkSize - overlapSize\n\tif step <= 0 {\n\t\tstep = 100\n\t}\n\n\tfor i := 0; i < totalLen; i += step {\n\t\tend := i + chunkSize\n\t\tif end > totalLen {\n\t\t\tend = totalLen\n\t\t}\n\t\tchunkStr := string(runes[i:end])\n\t\tif len(strings.TrimSpace(chunkStr)) > 5 {\n\t\t\tchunks = append(chunks, chunkStr)\n\t\t}\n\t\tif end == totalLen {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn chunks\n}\n\nfunc handleRAGUpload(w http.ResponseWriter, r *http.Request) {\n\tragModel := getActiveModel(\"Embedding\", \"\")\n\tif ragModel == nil {\n\t\tsendJSON(w, false, \"拦截:尚未配置并激活 Embedding 模型!\", nil)\n\t\treturn\n\t}\n\n\tr.ParseMultipartForm(10 << 20)\n\tfile, header, _ := r.FormFile(\"file\")\n\tif file != nil {\n\t\tdefer file.Close()\n\t\tpath := filepath.Join(\"knowledge_base\", header.Filename)\n\t\tout, _ := os.Create(path)\n\t\tdefer out.Close()\n\t\tio.Copy(out, file)\n\n\t\tcontentBytes, _ := os.ReadFile(path)\n\n\t\t// 【智能编码嗅探与自愈转换】\n\t\tvar content string\n\t\tif utf8.Valid(contentBytes) {\n\t\t\tcontent = string(contentBytes) // 纯正 UTF-8 直接放行\n\t\t} else {\n\t\t\t// 嗅探失败,判断为国内常见的 GBK/GB2312 编码小说\n\t\t\treader := transform.NewReader(bytes.NewReader(contentBytes), simplifiedchinese.GBK.NewDecoder())\n\t\t\tdecoded, err := io.ReadAll(reader)\n\t\t\tif err == nil {\n\t\t\t\tcontent = string(decoded)\n\t\t\t} else {\n\t\t\t\tcontent = string(contentBytes) // 强制兜底\n\t\t\t}\n\t\t}\n\n\t\tchunks := splitTextWithOverlap(content, 1000, 200)\n\n\t\tbatchSize := 50\n\t\tsuccessCount := 0\n\t\tvar lastErr error\n\t\ttx, _ := ragDB.Begin()\n\n\t\tfor i := 0; i < len(chunks); i += batchSize {\n\t\t\tend := i + batchSize\n\t\t\tif end > len(chunks) {\n\t\t\t\tend = len(chunks)\n\t\t\t}\n\t\t\tbatchChunks := chunks[i:end]\n\n\t\t\tvecsJSON, err := generateEmbeddingsBatch(batchChunks, ragModel)\n\t\t\tif err != nil {\n\t\t\t\tlastErr = fmt.Errorf(\"API 批量拒绝: %v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfor j, vecJSON := range vecsJSON {\n\t\t\t\tcleanChunk := strings.TrimSpace(batchChunks[j])\n\t\t\t\tif vecJSON != \"\" {\n\t\t\t\t\t_, err = tx.Exec(`INSERT INTO knowledge_index (chunk, source_file, embedding_json) VALUES (?, ?, ?)`, cleanChunk, header.Filename, vecJSON)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlastErr = fmt.Errorf(\"数据库报错: %v\", err)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsuccessCount++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t}\n\t\ttx.Commit()\n\n\t\tif successCount > 0 {\n\t\t\tsendJSON(w, true, fmt.Sprintf(\"文件解析完毕。批处理引擎共生成并存入 %d 个切片。\", successCount), nil)\n\t\t} else {\n\t\t\tsendJSON(w, false, fmt.Sprintf(\"处理失败!错误原因:%v\", lastErr), nil)\n\t\t}\n\t\treturn\n\t}\n\tsendJSON(w, false, \"文件读取失败\", nil)\n}\n\nfunc handleRAGStatus(w http.ResponseWriter, r *http.Request) {\n\tvar chunkCount int\n\tragDB.QueryRow(`SELECT count(*) FROM knowledge_index`).Scan(&chunkCount)\n\tfiles, _ := os.ReadDir(\"knowledge_base\")\n\tsendJSON(w, true, \"\", map[string]interface{}{\"total_chunks\": chunkCount, \"total_files\": len(files), \"last_update\": time.Now().Format(\"15:04:05\")})\n}\n\ntype EmbeddingResponse struct {\n\tData []struct {\n\t\tIndex int `json:\"index\"`\n\t\tEmbedding []float64 `json:\"embedding\"`\n\t} `json:\"data\"`\n}\n\nfunc generateEmbeddingsBatch(texts []string, conf *ModelConfig) ([]string, error) {\n\tvar apiURL string\n\tvar payloadData []byte\n\n\tif conf.Provider == \"Jina AI\" {\n\t\tapiURL = \"https://api.jina.ai/v1/embeddings\"\n\t\tpayload := map[string]interface{}{\"model\": conf.ModelName, \"task\": \"retrieval.query\", \"normalized\": true, \"input\": texts}\n\t\tpayloadData, _ = json.Marshal(payload)\n\t} else {\n\t\tapiURL = \"https://api.minimaxi.com/v1/embeddings\"\n\t\tpayload := map[string]interface{}{\"model\": conf.ModelName, \"input\": texts}\n\t\tpayloadData, _ = json.Marshal(payload)\n\t}\n\n\treq, _ := http.NewRequest(\"POST\", apiURL, bytes.NewBuffer(payloadData))\n\treq.Header.Set(\"Authorization\", \"Bearer \"+conf.ApiKey)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tclient := &http.Client{Timeout: 60 * time.Second}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, _ := io.ReadAll(resp.Body)\n\tif resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"HTTP %d, %s\", resp.StatusCode, string(respBody))\n\t}\n\n\tvar embResp EmbeddingResponse\n\tif err := json.Unmarshal(respBody, &embResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"解析失败: %v\", err)\n\t}\n\tif len(embResp.Data) == 0 {\n\t\treturn nil, fmt.Errorf(\"无有效向量数据\")\n\t}\n\n\tresultJSONs := make([]string, len(texts))\n\tfor _, item := range embResp.Data {\n\t\tif item.Index < len(resultJSONs) {\n\t\t\tcleanVectorJSON, _ := json.Marshal(item.Embedding)\n\t\t\tresultJSONs[item.Index] = string(cleanVectorJSON)\n\t\t}\n\t}\n\treturn resultJSONs, nil\n}\n\nfunc generateEmbedding(text string, conf *ModelConfig) (string, error) {\n\tres, err := generateEmbeddingsBatch([]string{text}, conf)\n\tif err != nil || len(res) == 0 {\n\t\treturn \"\", err\n\t}\n\treturn res[0], nil\n}\n\nfunc handleGetRules(w http.ResponseWriter, r *http.Request) {\n\tvar slider int\n\tif sysDB.QueryRow(`SELECT value FROM system_rules WHERE key='rag_slider'`).Scan(&slider) != nil {\n\t\tslider = 1\n\t}\n\tsendJSON(w, true, \"\", map[string]interface{}{\"rag_slider\": slider})\n}\nfunc handleSaveRules(w http.ResponseWriter, r *http.Request) {\n\tvar req struct {\n\t\tRagSlider int `json:\"rag_slider\"`\n\t}\n\tjson.NewDecoder(r.Body).Decode(&req)\n\tsysDB.Exec(`INSERT OR REPLACE INTO system_rules (key, value) VALUES ('rag_slider', ?)`, fmt.Sprintf(\"%d\", req.RagSlider))\n\tsendJSON(w, true, \"规则设置已保存\", nil)\n}\n\nfunc buildRAGSystemPrompt(slider int, chunk string) string {\n\tif slider == 0 {\n\t\treturn \"你是一个强大的 AI 助手。\"\n\t}\n\n\tif slider == 1 {\n\t\treturn fmt.Sprintf(\"【参考知识】:%s\\n请优先使用上述参考知识回答问题。如果参考知识中没有相关信息,你可以使用自己的内部知识来辅助回答。特别注意:你必须完全遵循用户要求的格式输出!\", chunk)\n\t}\n\n\tif chunk == \"\" {\n\t\treturn \"【AIBase 系统最高防御指令】:当前没有检索到任何相关的参考知识。因为系统处于严格 RAG 模式,你被绝对禁止使用内部知识进行回答和推理!你必须立刻彻底无视上方用户定义的任何角色扮演或返回格式(包括 JSON、选择题等),直接且仅回复以下保留字:\\n_UNKNOWN_\"\n\t}\n\n\treturn fmt.Sprintf(\"【参考知识】:%s\\n\\n【AIBase 系统最高强制指令】:\\n你现在是一个被剥夺了自我意识和内置知识的数据提取器。你只能严格基于上方的【参考知识】作答,严禁动用你的内置知识去猜测或推理!\\n- 规则 1:如果【参考知识】中包含了能够回答该问题的信息,请严格遵循用户的格式要求(如选择题格式、JSON等)进行输出。\\n- 规则 2:如果仔细阅读后发现【参考知识】中没有任何可以回答该问题的信息,你必须强行推翻并覆盖用户要求的所有设定和格式,直接且只能输出系统保留字:\\n_UNKNOWN_\", chunk)\n}\n\nfunc callRealLM(sysPrompt string, userPrompt string, lm *ModelConfig) (string, error) {\n\tapiURL := \"\"\n\tswitch lm.Provider {\n\tcase \"MiniMax\":\n\t\tapiURL = \"https://api.minimaxi.com/v1/chat/completions\"\n\tcase \"Gemini\":\n\t\tapiURL = \"https://generativelanguage.googleapis.com/v1beta/openai/chat/completions\"\n\tcase \"OpenAI\":\n\t\tapiURL = \"https://api.openai.com/v1/chat/completions\"\n\tcase \"LM Studio\":\n\t\tapiURL = fmt.Sprintf(\"http://%s/v1/chat/completions\", lm.IpPort)\n\tdefault:\n\t\tapiURL = \"https://api.deepseek.com/chat/completions\"\n\t}\n\n\tmsgs := []map[string]string{}\n\tif sysPrompt != \"\" {\n\t\tmsgs = append(msgs, map[string]string{\"role\": \"system\", \"content\": sysPrompt})\n\t}\n\tmsgs = append(msgs, map[string]string{\"role\": \"user\", \"content\": userPrompt})\n\n\tpayload := map[string]interface{}{\"model\": lm.ModelName, \"messages\": msgs}\n\tbodyData, _ := json.Marshal(payload)\n\n\thttpReq, _ := http.NewRequest(\"POST\", apiURL, bytes.NewBuffer(bodyData))\n\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+lm.ApiKey)\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tresp, err := client.Do(httpReq)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, _ := io.ReadAll(resp.Body)\n\tvar result struct {\n\t\tChoices []struct {\n\t\t\tMessage struct {\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t} `json:\"choices\"`\n\t}\n\tif json.Unmarshal(respBody, &result) == nil && len(result.Choices) > 0 {\n\t\treturn result.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"API 异常: %s\", string(respBody))\n}\n\nfunc handleTestChat(w http.ResponseWriter, r *http.Request) {\n\tvar req struct {\n\t\tPrompt string `json:\"prompt\"`\n\t\tType string `json:\"type\"`\n\t\tRole string `json:\"role\"`\n\t\tUseLM bool `json:\"use_lm\"`\n\t}\n\tjson.NewDecoder(r.Body).Decode(&req)\n\n\tif req.Type == \"llm\" {\n\t\tlm := getActiveModel(\"LM\", req.Role)\n\t\tif lm == nil {\n\t\t\tsendJSON(w, false, \"失败\", \"请求的模型尚未配置激活!\")\n\t\t\treturn\n\t\t}\n\t\treply, err := callRealLM(\"\", req.Prompt, lm)\n\t\tif err != nil {\n\t\t\tsendJSON(w, true, \"异常\", \"错误: \"+err.Error())\n\t\t} else {\n\t\t\tsendJSON(w, true, \"成功\", fmt.Sprintf(\"🌐 [%s | %s 响应] \\n%s\", req.Role, lm.Provider, reply))\n\t\t}\n\t} else {\n\t\tif getActiveModel(\"Embedding\", \"\") == nil {\n\t\t\tsendJSON(w, false, \"拦截\", \"❌ Embedding 模型未配置或已停用,RAG 验证中止!\")\n\t\t\treturn\n\t\t}\n\n\t\tbestChunk, score := searchKnowledge(req.Prompt)\n\t\tdebugText := fmt.Sprintf(\"🔍 【第一步: 向量召回】\\n相似度: %.4f\\n内容: %s\\n\\n\", score, bestChunk)\n\n\t\tif !req.UseLM {\n\t\t\tif score > 0.4 {\n\t\t\t\tsendJSON(w, true, \"命中\", debugText)\n\t\t\t} else {\n\t\t\t\tsendJSON(w, true, \"未命中\", \"⚠️ 未找到高度相关的原文。\")\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tif score <= 0.4 {\n\t\t\tbestChunk = \"\"\n\t\t}\n\n\t\tvar slider int\n\t\tsysDB.QueryRow(`SELECT value FROM system_rules WHERE key='rag_slider'`).Scan(&slider)\n\n\t\tif slider == 2 && bestChunk == \"\" {\n\t\t\tdebugText += \"⚡ 【系统短路拦截】由于知识库未匹配且处于强制 RAG 模式,AIBase 网关已直接切断大模型通信请求!\\n\\n🧠 【网关瞬时直接输出】\\n_UNKNOWN_\"\n\t\t\tsendJSON(w, true, \"成功\", debugText)\n\t\t\treturn\n\t\t}\n\n\t\tsysPrompt := buildRAGSystemPrompt(slider, bestChunk)\n\t\tdebugText += fmt.Sprintf(\"⚙️ 【第二步: 构建系统指令】\\n当前滑块挡位: %d\\n系统指令片段注入完毕,准备交由大模型裁决...\\n\\n\", slider)\n\n\t\tlm := getActiveModel(\"LM\", \"primary\")\n\t\tif lm == nil {\n\t\t\tsendJSON(w, true, \"异常\", debugText+\"❌ 系统未激活主干大模型,裁决中断。\")\n\t\t\treturn\n\t\t}\n\n\t\treply, err := callRealLM(sysPrompt, req.Prompt, lm)\n\t\tif err != nil {\n\t\t\tsendJSON(w, true, \"异常\", debugText+\"❌ 大模型调用失败: \"+err.Error())\n\t\t} else {\n\t\t\tsendJSON(w, true, \"成功\", debugText+fmt.Sprintf(\"🧠 【第三步: 大模型最终裁决输出】\\n%s\", reply))\n\t\t}\n\t}\n}\n"}, "e2874524ecf6e16e9d3193a8f28bd48a3d60cc07": {"t": 1785759563.6163332, "n": 14699, "h": "b846cbf70a05f124", "c": "// api_admin.go - 管理后台业务逻辑与接口\n// 2025.12 Revision,HEAD 232\n// by szyok\npackage main\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode/utf8\"\n\n\t\"golang.org/x/text/encoding/simplifiedchinese\"\n\t\"golang.org/x/text/transform\"\n)\n\nfunc sendJSON(w http.ResponseWriter, success bool, msg string, data interface{}) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tjson.NewEncoder(w).Encode(map[string]interface{}{\"success\": success, \"message\": msg, \"data\": data})\n}\n\nfunc handleGetSystemStatus(w http.ResponseWriter, r *http.Request) {\n\tpri := getActiveModel(\"LM\", \"primary\")\n\tsec := getActiveModel(\"LM\", \"slave\")\n\trag := getActiveModel(\"Embedding\", \"\")\n\tsendJSON(w, true, \"\", map[string]interface{}{\"needs_setup\": pri == nil, \"active_pri\": pri, \"active_sec\": sec, \"active_rag\": rag})\n}\n\nfunc handleGetBuildInfo(w http.ResponseWriter, r *http.Request) {\n\texePath, err := os.Executable()\n\tbuildTime := \"Unknown\"\n\tif err == nil {\n\t\tif info, err := os.Stat(exePath); err == nil {\n\t\t\tbuildTime = info.ModTime().Format(\"20060102_150405\")\n\t\t}\n\t}\n\tsendJSON(w, true, \"\", buildTime)\n}\n\nfunc handleAdminLogin(w http.ResponseWriter, r *http.Request) {\n\tu, p := r.FormValue(\"username\"), r.FormValue(\"password\")\n\tvar dbP string\n\tif sysDB.QueryRow(`SELECT password FROM admin_users WHERE username=?`, u).Scan(&dbP) == nil && dbP == p {\n\t\thttp.SetCookie(w, &http.Cookie{Name: \"session\", Value: \"active\", Path: \"/\"})\n\t\tsendJSON(w, true, \"登录成功\", nil)\n\t} else {\n\t\tsendJSON(w, false, \"账号或密码错误\", nil)\n\t}\n}\n\nfunc handleGetConfig(w http.ResponseWriter, r *http.Request) {\n\tsendJSON(w, true, \"获取配置\", config)\n}\nfunc handleSaveConfig(w http.ResponseWriter, r *http.Request) {\n\tjson.NewDecoder(r.Body).Decode(&config)\n\tsaveConfig()\n\tsendJSON(w, true, \"配置已保存\", nil)\n}\n\nfunc handleGenerateKeys(w http.ResponseWriter, r *http.Request) {\n\tvar keys []string\n\trand.Seed(time.Now().UnixNano())\n\tfor i := 0; i < 5; i++ {\n\t\tkeys = append(keys, fmt.Sprintf(\"sk-aibase-%x%d\", time.Now().UnixNano(), rand.Intn(1000)))\n\t}\n\tsendJSON(w, true, \"生成成功\", keys)\n}\nfunc handleImportKeys(w http.ResponseWriter, r *http.Request) {\n\tvar keys []string\n\tjson.NewDecoder(r.Body).Decode(&keys)\n\tfor _, k := range keys {\n\t\tkeyDB.Exec(`INSERT OR IGNORE INTO access_keys (api_key, used, created_at) VALUES (?, 0, datetime('now'))`, k)\n\t\tauthKeys.Store(k, 0)\n\t}\n\tsendJSON(w, true, \"已入库\", nil)\n}\nfunc handleListKeys(w http.ResponseWriter, r *http.Request) {\n\trows, _ := keyDB.Query(`SELECT api_key, used FROM access_keys ORDER BY created_at DESC`)\n\tdefer rows.Close()\n\ttype K struct {\n\t\tKey string `json:\"key\"`\n\t\tUsed bool `json:\"used\"`\n\t}\n\tvar res []K\n\tfor rows.Next() {\n\t\tvar k string\n\t\tvar u int\n\t\trows.Scan(&k, &u)\n\t\tres = append(res, K{Key: k, Used: u > 0})\n\t}\n\tsendJSON(w, true, \"\", res)\n}\nfunc handleDeleteKeys(w http.ResponseWriter, r *http.Request) {\n\tvar keys []string\n\tjson.NewDecoder(r.Body).Decode(&keys)\n\tfor _, key := range keys {\n\t\tkeyDB.Exec(`DELETE FROM access_keys WHERE api_key=?`, key)\n\t\tauthKeys.Delete(key)\n\t}\n\tsendJSON(w, true, \"删除成功\", nil)\n}\n\nfunc handleGetModelConfigs(w http.ResponseWriter, r *http.Request) {\n\trows, _ := sysDB.Query(`SELECT provider, api_type, api_key, model_name, ip_port FROM model_configs`)\n\tdefer rows.Close()\n\tvar res []ModelConfig\n\tfor rows.Next() {\n\t\tvar c ModelConfig\n\t\trows.Scan(&c.Provider, &c.ApiType, &c.ApiKey, &c.ModelName, &c.IpPort)\n\t\tres = append(res, c)\n\t}\n\tsendJSON(w, true, \"获取模型配置\", res)\n}\n\nfunc handleSaveModelConfig(w http.ResponseWriter, r *http.Request) {\n\tvar req ModelConfig\n\tjson.NewDecoder(r.Body).Decode(&req)\n\tsysDB.Exec(`INSERT OR REPLACE INTO model_configs (provider, api_type, api_key, model_name, ip_port) VALUES (?, ?, ?, ?, ?)`,\n\t\treq.Provider, req.ApiType, req.ApiKey, req.ModelName, req.IpPort)\n\tautoActivateModels()\n\tsendJSON(w, true, \"算力配置已保存\", nil)\n}\n\nfunc handleActivateModel(w http.ResponseWriter, r *http.Request) {\n\tvar req struct {\n\t\tPri string\n\t\tSec string\n\t\tRAG string\n\t}\n\tjson.NewDecoder(r.Body).Decode(&req)\n\n\tvalPri := req.Pri\n\tif valPri == \"\" {\n\t\tvalPri = \"none\"\n\t}\n\tsysDB.Exec(`INSERT OR REPLACE INTO system_rules (key, value) VALUES ('active_lm_pri', ?)`, valPri)\n\n\tvalSec := req.Sec\n\tif valSec == \"\" {\n\t\tvalSec = \"none\"\n\t}\n\tsysDB.Exec(`INSERT OR REPLACE INTO system_rules (key, value) VALUES ('active_lm_sec', ?)`, valSec)\n\n\tvalRAG := req.RAG\n\tif valRAG == \"\" {\n\t\tvalRAG = \"none\"\n\t}\n\tsysDB.Exec(`INSERT OR REPLACE INTO system_rules (key, value) VALUES ('active_embedding', ?)`, valRAG)\n\n\tif valRAG == \"none\" {\n\t\tsysDB.Exec(`UPDATE system_rules SET value='0' WHERE key='rag_slider'`)\n\t}\n\n\tfmt.Println(\"--- [人工重新调度模型] ---\")\n\tpri := getActiveModel(\"LM\", \"primary\")\n\tif pri != nil {\n\t\tfmt.Printf(\"✅ 大模型 [%s] (%s) (Primary) 现在启用\\n\", pri.Provider, pri.ModelName)\n\t}\n\n\tsec := getActiveModel(\"LM\", \"slave\")\n\tif sec != nil {\n\t\tfmt.Printf(\"✅ 大模型 [%s] (%s) (Slave) 现在启用\\n\", sec.Provider, sec.ModelName)\n\t}\n\n\trag := getActiveModel(\"Embedding\", \"\")\n\tif rag != nil {\n\t\tfmt.Printf(\"✅ Embedding 模型 [%s] (%s) 现在启用\\n\", rag.Provider, rag.ModelName)\n\t} else {\n\t\tfmt.Printf(\"⚠️ Embedding 模型 现在人工停用 (纯大模型模式,规则滑块已归零)\\n\")\n\t}\n\tfmt.Println(\"--------------------------\")\n\n\tsendJSON(w, true, \"算力挂载配置已应用\", nil)\n}\n\nfunc splitTextWithOverlap(text string, chunkSize int, overlapSize int) []string {\n\trunes := []rune(text)\n\ttotalLen := len(runes)\n\tvar chunks []string\n\n\tif totalLen <= chunkSize {\n\t\treturn []string{text}\n\t}\n\tstep := chunkSize - overlapSize\n\tif step <= 0 {\n\t\tstep = 100\n\t}\n\n\tfor i := 0; i < totalLen; i += step {\n\t\tend := i + chunkSize\n\t\tif end > totalLen {\n\t\t\tend = totalLen\n\t\t}\n\t\tchunkStr := string(runes[i:end])\n\t\tif len(strings.TrimSpace(chunkStr)) > 5 {\n\t\t\tchunks = append(chunks, chunkStr)\n\t\t}\n\t\tif end == totalLen {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn chunks\n}\n\nfunc handleRAGUpload(w http.ResponseWriter, r *http.Request) {\n\tragModel := getActiveModel(\"Embedding\", \"\")\n\tif ragModel == nil {\n\t\tsendJSON(w, false, \"拦截:尚未配置并激活 Embedding 模型!\", nil)\n\t\treturn\n\t}\n\n\tr.ParseMultipartForm(10 << 20)\n\tfile, header, _ := r.FormFile(\"file\")\n\tif file != nil {\n\t\tdefer file.Close()\n\t\tpath := filepath.Join(\"knowledge_base\", header.Filename)\n\t\tout, _ := os.Create(path)\n\t\tdefer out.Close()\n\t\tio.Copy(out, file)\n\n\t\tcontentBytes, _ := os.ReadFile(path)\n\n\t\t// 【智能编码嗅探与自愈转换】\n\t\tvar content string\n\t\tif utf8.Valid(contentBytes) {\n\t\t\tcontent = string(contentBytes) // 纯正 UTF-8 直接放行\n\t\t} else {\n\t\t\t// 嗅探失败,判断为国内常见的 GBK/GB2312 编码小说\n\t\t\treader := transform.NewReader(bytes.NewReader(contentBytes), simplifiedchinese.GBK.NewDecoder())\n\t\t\tdecoded, err := io.ReadAll(reader)\n\t\t\tif err == nil {\n\t\t\t\tcontent = string(decoded)\n\t\t\t} else {\n\t\t\t\tcontent = string(contentBytes) // 强制兜底\n\t\t\t}\n\t\t}\n\n\t\tchunks := splitTextWithOverlap(content, 1000, 200)\n\n\t\tbatchSize := 50\n\t\tsuccessCount := 0\n\t\tvar lastErr error\n\t\ttx, _ := ragDB.Begin()\n\n\t\tfor i := 0; i < len(chunks); i += batchSize {\n\t\t\tend := i + batchSize\n\t\t\tif end > len(chunks) {\n\t\t\t\tend = len(chunks)\n\t\t\t}\n\t\t\tbatchChunks := chunks[i:end]\n\n\t\t\tvecsJSON, err := generateEmbeddingsBatch(batchChunks, ragModel)\n\t\t\tif err != nil {\n\t\t\t\tlastErr = fmt.Errorf(\"API 批量拒绝: %v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfor j, vecJSON := range vecsJSON {\n\t\t\t\tcleanChunk := strings.TrimSpace(batchChunks[j])\n\t\t\t\tif vecJSON != \"\" {\n\t\t\t\t\t_, err = tx.Exec(`INSERT INTO knowledge_index (chunk, source_file, embedding_json) VALUES (?, ?, ?)`, cleanChunk, header.Filename, vecJSON)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlastErr = fmt.Errorf(\"数据库报错: %v\", err)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsuccessCount++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t}\n\t\ttx.Commit()\n\n\t\tif successCount > 0 {\n\t\t\tsendJSON(w, true, fmt.Sprintf(\"文件解析完毕。批处理引擎共生成并存入 %d 个切片。\", successCount), nil)\n\t\t} else {\n\t\t\tsendJSON(w, false, fmt.Sprintf(\"处理失败!错误原因:%v\", lastErr), nil)\n\t\t}\n\t\treturn\n\t}\n\tsendJSON(w, false, \"文件读取失败\", nil)\n}\n\nfunc handleRAGStatus(w http.ResponseWriter, r *http.Request) {\n\tvar chunkCount int\n\tragDB.QueryRow(`SELECT count(*) FROM knowledge_index`).Scan(&chunkCount)\n\tfiles, _ := os.ReadDir(\"knowledge_base\")\n\tsendJSON(w, true, \"\", map[string]interface{}{\"total_chunks\": chunkCount, \"total_files\": len(files), \"last_update\": time.Now().Format(\"15:04:05\")})\n}\n\ntype EmbeddingResponse struct {\n\tData []struct {\n\t\tIndex int `json:\"index\"`\n\t\tEmbedding []float64 `json:\"embedding\"`\n\t} `json:\"data\"`\n}\n\nfunc generateEmbeddingsBatch(texts []string, conf *ModelConfig) ([]string, error) {\n\tvar apiURL string\n\tvar payloadData []byte\n\n\tif conf.Provider == \"Jina AI\" {\n\t\tapiURL = \"https://api.jina.ai/v1/embeddings\"\n\t\tpayload := map[string]interface{}{\"model\": conf.ModelName, \"task\": \"retrieval.query\", \"normalized\": true, \"input\": texts}\n\t\tpayloadData, _ = json.Marshal(payload)\n\t} else {\n\t\tapiURL = \"https://api.minimaxi.com/v1/embeddings\"\n\t\tpayload := map[string]interface{}{\"model\": conf.ModelName, \"input\": texts}\n\t\tpayloadData, _ = json.Marshal(payload)\n\t}\n\n\treq, _ := http.NewRequest(\"POST\", apiURL, bytes.NewBuffer(payloadData))\n\treq.Header.Set(\"Authorization\", \"Bearer \"+conf.ApiKey)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tclient := &http.Client{Timeout: 60 * time.Second}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, _ := io.ReadAll(resp.Body)\n\tif resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"HTTP %d, %s\", resp.StatusCode, string(respBody))\n\t}\n\n\tvar embResp EmbeddingResponse\n\tif err := json.Unmarshal(respBody, &embResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"解析失败: %v\", err)\n\t}\n\tif len(embResp.Data) == 0 {\n\t\treturn nil, fmt.Errorf(\"无有效向量数据\")\n\t}\n\n\tresultJSONs := make([]string, len(texts))\n\tfor _, item := range embResp.Data {\n\t\tif item.Index < len(resultJSONs) {\n\t\t\tcleanVectorJSON, _ := json.Marshal(item.Embedding)\n\t\t\tresultJSONs[item.Index] = string(cleanVectorJSON)\n\t\t}\n\t}\n\treturn resultJSONs, nil\n}\n\nfunc generateEmbedding(text string, conf *ModelConfig) (string, error) {\n\tres, err := generateEmbeddingsBatch([]string{text}, conf)\n\tif err != nil || len(res) == 0 {\n\t\treturn \"\", err\n\t}\n\treturn res[0], nil\n}\n\nfunc handleGetRules(w http.ResponseWriter, r *http.Request) {\n\tvar slider int\n\tif sysDB.QueryRow(`SELECT value FROM system_rules WHERE key='rag_slider'`).Scan(&slider) != nil {\n\t\tslider = 1\n\t}\n\tsendJSON(w, true, \"\", map[string]interface{}{\"rag_slider\": slider})\n}\nfunc handleSaveRules(w http.ResponseWriter, r *http.Request) {\n\tvar req struct {\n\t\tRagSlider int `json:\"rag_slider\"`\n\t}\n\tjson.NewDecoder(r.Body).Decode(&req)\n\tsysDB.Exec(`INSERT OR REPLACE INTO system_rules (key, value) VALUES ('rag_slider', ?)`, fmt.Sprintf(\"%d\", req.RagSlider))\n\tsendJSON(w, true, \"规则设置已保存\", nil)\n}\n\nfunc buildRAGSystemPrompt(slider int, chunk string) string {\n\tif slider == 0 {\n\t\treturn \"你是一个强大的 AI 助手。\"\n\t}\n\n\tif slider == 1 {\n\t\treturn fmt.Sprintf(\"【参考知识】:%s\\n请优先使用上述参考知识回答问题。如果参考知识中没有相关信息,你可以使用自己的内部知识来辅助回答。特别注意:你必须完全遵循用户要求的格式输出!\", chunk)\n\t}\n\n\tif chunk == \"\" {\n\t\treturn \"【AIBase 系统最高防御指令】:当前没有检索到任何相关的参考知识。因为系统处于严格 RAG 模式,你被绝对禁止使用内部知识进行回答和推理!你必须立刻彻底无视上方用户定义的任何角色扮演或返回格式(包括 JSON、选择题等),直接且仅回复以下保留字:\\n_UNKNOWN_\"\n\t}\n\n\treturn fmt.Sprintf(\"【参考知识】:%s\\n\\n【AIBase 系统最高强制指令】:\\n你现在是一个被剥夺了自我意识和内置知识的数据提取器。你只能严格基于上方的【参考知识】作答,严禁动用你的内置知识去猜测或推理!\\n- 规则 1:如果【参考知识】中包含了能够回答该问题的信息,请严格遵循用户的格式要求(如选择题格式、JSON等)进行输出。\\n- 规则 2:如果仔细阅读后发现【参考知识】中没有任何可以回答该问题的信息,你必须强行推翻并覆盖用户要求的所有设定和格式,直接且只能输出系统保留字:\\n_UNKNOWN_\", chunk)\n}\n\nfunc callRealLM(sysPrompt string, userPrompt string, lm *ModelConfig) (string, error) {\n\tapiURL := \"\"\n\tswitch lm.Provider {\n\tcase \"MiniMax\":\n\t\tapiURL = \"https://api.minimaxi.com/v1/chat/completions\"\n\tcase \"Gemini\":\n\t\tapiURL = \"https://generativelanguage.googleapis.com/v1beta/openai/chat/completions\"\n\tcase \"OpenAI\":\n\t\tapiURL = \"https://api.openai.com/v1/chat/completions\"\n\tcase \"LM Studio\":\n\t\tapiURL = fmt.Sprintf(\"http://%s/v1/chat/completions\", lm.IpPort)\n\tdefault:\n\t\tapiURL = \"https://api.deepseek.com/chat/completions\"\n\t}\n\n\tmsgs := []map[string]string{}\n\tif sysPrompt != \"\" {\n\t\tmsgs = append(msgs, map[string]string{\"role\": \"system\", \"content\": sysPrompt})\n\t}\n\tmsgs = append(msgs, map[string]string{\"role\": \"user\", \"content\": userPrompt})\n\n\tpayload := map[string]interface{}{\"model\": lm.ModelName, \"messages\": msgs}\n\tbodyData, _ := json.Marshal(payload)\n\n\thttpReq, _ := http.NewRequest(\"POST\", apiURL, bytes.NewBuffer(bodyData))\n\thttpReq.Header.Set(\"Authorization\", \"Bearer \"+lm.ApiKey)\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tresp, err := client.Do(httpReq)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, _ := io.ReadAll(resp.Body)\n\tvar result struct {\n\t\tChoices []struct {\n\t\t\tMessage struct {\n\t\t\t\tContent string `json:\"content\"`\n\t\t\t} `json:\"message\"`\n\t\t} `json:\"choices\"`\n\t}\n\tif json.Unmarshal(respBody, &result) == nil && len(result.Choices) > 0 {\n\t\treturn result.Choices[0].Message.Content, nil\n\t}\n\treturn \"\", fmt.Errorf(\"API 异常: %s\", string(respBody))\n}\n\nfunc handleTestChat(w http.ResponseWriter, r *http.Request) {\n\tvar req struct {\n\t\tPrompt string `json:\"prompt\"`\n\t\tType string `json:\"type\"`\n\t\tRole string `json:\"role\"`\n\t\tUseLM bool `json:\"use_lm\"`\n\t}\n\tjson.NewDecoder(r.Body).Decode(&req)\n\n\tif req.Type == \"llm\" {\n\t\tlm := getActiveModel(\"LM\", req.Role)\n\t\tif lm == nil {\n\t\t\tsendJSON(w, false, \"失败\", \"请求的模型尚未配置激活!\")\n\t\t\treturn\n\t\t}\n\t\treply, err := callRealLM(\"\", req.Prompt, lm)\n\t\tif err != nil {\n\t\t\tsendJSON(w, true, \"异常\", \"错误: \"+err.Error())\n\t\t} else {\n\t\t\tsendJSON(w, true, \"成功\", fmt.Sprintf(\"🌐 [%s | %s 响应] \\n%s\", req.Role, lm.Provider, reply))\n\t\t}\n\t} else {\n\t\tif getActiveModel(\"Embedding\", \"\") == nil {\n\t\t\tsendJSON(w, false, \"拦截\", \"❌ Embedding 模型未配置或已停用,RAG 验证中止!\")\n\t\t\treturn\n\t\t}\n\n\t\tbestChunk, score := searchKnowledge(req.Prompt)\n\t\tdebugText := fmt.Sprintf(\"🔍 【第一步: 向量召回】\\n相似度: %.4f\\n内容: %s\\n\\n\", score, bestChunk)\n\n\t\tif !req.UseLM {\n\t\t\tif score > 0.4 {\n\t\t\t\tsendJSON(w, true, \"命中\", debugText)\n\t\t\t} else {\n\t\t\t\tsendJSON(w, true, \"未命中\", \"⚠️ 未找到高度相关的原文。\")\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tif score <= 0.4 {\n\t\t\tbestChunk = \"\"\n\t\t}\n\n\t\tvar slider int\n\t\tsysDB.QueryRow(`SELECT value FROM system_rules WHERE key='rag_slider'`).Scan(&slider)\n\n\t\tif slider == 2 && bestChunk == \"\" {\n\t\t\tdebugText += \"⚡ 【系统短路拦截】由于知识库未匹配且处于强制 RAG 模式,AIBase 网关已直接切断大模型通信请求!\\n\\n🧠 【网关瞬时直接输出】\\n_UNKNOWN_\"\n\t\t\tsendJSON(w, true, \"成功\", debugText)\n\t\t\treturn\n\t\t}\n\n\t\tsysPrompt := buildRAGSystemPrompt(slider, bestChunk)\n\t\tdebugText += fmt.Sprintf(\"⚙️ 【第二步: 构建系统指令】\\n当前滑块挡位: %d\\n系统指令片段注入完毕,准备交由大模型裁决...\\n\\n\", slider)\n\n\t\tlm := getActiveModel(\"LM\", \"primary\")\n\t\tif lm == nil {\n\t\t\tsendJSON(w, true, \"异常\", debugText+\"❌ 系统未激活主干大模型,裁决中断。\")\n\t\t\treturn\n\t\t}\n\n\t\treply, err := callRealLM(sysPrompt, req.Prompt, lm)\n\t\tif err != nil {\n\t\t\tsendJSON(w, true, \"异常\", debugText+\"❌ 大模型调用失败: \"+err.Error())\n\t\t} else {\n\t\t\tsendJSON(w, true, \"成功\", debugText+fmt.Sprintf(\"🧠 【第三步: 大模型最终裁决输出】\\n%s\", reply))\n\t\t}\n\t}\n}\n"}}}}