From a34bef50ae840590e9ea90e76f5acf14f751d962 Mon Sep 17 00:00:00 2001 From: hubian <908234780@qq.com> Date: Tue, 14 Apr 2026 09:15:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6=E5=90=8D?= =?UTF-8?q?=E6=A0=87=E8=AE=B0=EF=BC=8C=E5=86=85=E5=AE=B9=E8=87=AA=E7=84=B6?= =?UTF-8?q?=E8=9E=8D=E5=85=A5=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai_chat.db | Bin 86016 -> 90112 bytes main.py | 199 +++++++ main_v2.py | 16 +- ...x.tphai.com_SKLVQXSRMI.blacklisted_devices | 0 ...atrix.tphai.com_SKLVQXSRMI.ignored_devices | 0 ...atrix.tphai.com_SKLVQXSRMI.trusted_devices | 11 + services/__pycache__/__init__.cpython-310.pyc | Bin 396 -> 685 bytes .../conversation_service.cpython-310.pyc | Bin 4065 -> 4984 bytes templates/admin/index.html | 524 +++++++++++++++++- 9 files changed, 720 insertions(+), 30 deletions(-) create mode 100644 matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.blacklisted_devices create mode 100644 matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.ignored_devices create mode 100644 matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.trusted_devices diff --git a/ai_chat.db b/ai_chat.db index ce8b930206f845e67c18f817380b016148464416..cb1f27a156a79f1251475cfe96a80f6d340fb525 100644 GIT binary patch delta 1310 zcmZWpO-vg{6yD`07z^(Z6+=VSvO(#ED8}Ah8{1Nnx<9D`CMp$0YIC3@p=}yulmbR+ zg|Lkw27P>oMr1`bA<}!uq zGzLQ8BjR6Tj(A`7M8&C2M|9+ys+ML&F;raO7upvjzSldp^k|>>8r>q zQ-B=|`~#kX1F#2p@EiCA>_m=#kO16HEk!6m69zcY2UfuEpea(yo(1LV{961FF9rAc z%y(r+z++TARt}ycz(2tsKm?B?N4I|ld1@;shF;HaBp9w}jZ+Zkh_?tc!JvX~t6nY4 z$MMESdAC~Sy`e$g>+2)d%?)RHF=ob;KNX8bT*0pe1^Ko-7VE^!J|EdnTNN0tu6V_w zR`hhWf0dP{xBb!~4Rj_AO;IMJ&eqe>;eMl`7{jY>UOZ+|7qxYDUG3^_vvotxgVuiP zmf-P0Z-`GWN}(v+^uUEF=)1R{ata|oTypKFrj$yZhODW9t4sZSd=`$Z2*aZ_HRwTF zo)=gBnLsdeZ(`Ts6z)Vtc1WB-KtrM{j&9&akWbGF=w{t5j8E=5TqLQaWH8Y`BSe#M z%_~KtaQ;CS4lb++Ykm|ad8ehdupIAPgozkTCZu3cX4>x=mmGtc$?p-WE_ZsYyN+&0!Bs3U>zi#3(`)Q{z%7Agtb!Qw2Ar;G9flJom zPKIoT`%*9=`qyDP%*U6+@K{!Jxe&-cM1??>6_(RNU=eO?kw(k`u5Xz$%B~*H_0bQWElq{h##MH@NrMzY!$Ai z(9FCT1cuU^WLG#Z)+1M_Padb`Q>>P^H(i)?QX0)bns@v`D zw(i!eSE|iq^@*N$zizqS+}3NjMt^3r-RQPAz4xKM-fnC8xbd=y?&_sl>AJT+v0E?L zuLZ)T_?4LL9sG8QQia9^C$=zR3ys1$al&<`QFf>MxNPc|?v!;|Oe4(?W_+V0xBa&| zPpJ%CKZ#eC3Il^kV$1oQ5%*JE&C@FG;WO2j*`iVPNOZx=B3q&Z3?-3nFC8RpO$ZYq zB&M|FgkxR=0TO6V)&GA<*^kFyGElO-{{bW1WG(;z delta 223 zcmV<503iQ>zy*My1&|sB41E9q02PrSJ^~DVv0&pG5iuY!HaavnIy5ydH8n9dH#M_V z8?aOo2Mv<|53dhZ4*m|I4o3~`4U@AmAbky!lYLGMG&MRhF)lSRH8nXov+aEs0ul!g z*Z>dw5B3l659$x(58e;hvmv0u50lt0ED{F~X8;eA4}A}*59<%r55Es*voRo150ht~ zoe~8P=Kufz02L4W5B3l759|-;vk}1550mPz8UslVOp^z&ld~}(vJJCLzPN!91_N#Y Z3djlP2a^Qc1Ox+av5{H>w{AlLk_=5hNtXZs diff --git a/main.py b/main.py index 654e697..a4bc0b6 100644 --- a/main.py +++ b/main.py @@ -382,6 +382,205 @@ async def get_config(db: Session = Depends(get_db)): } +@app.get("/api/admin/ai-config") +async def get_ai_config(db: Session = Depends(get_db)): + """获取AI配置""" + configs = {c.key: c.value for c in db.query(SystemConfig).filter(SystemConfig.key.startswith('ai_')).all()} + + return { + "api_base": configs.get('ai_api_base', 'http://192.168.2.17:19007/v1'), + "api_key": configs.get('ai_api_key', 'xxxx'), + "model": configs.get('ai_model', 'auto'), + "use_mock": ai_service.use_mock + } + + +@app.post("/api/admin/ai-config") +async def update_ai_config(data: dict, db: Session = Depends(get_db)): + """更新AI配置""" + api_base = data.get("api_base") + api_key = data.get("api_key") + model = data.get("model") + + if api_base: + config = db.query(SystemConfig).filter(SystemConfig.key == 'ai_api_base').first() + if config: + config.value = api_base + else: + config = SystemConfig(key='ai_api_base', value=api_base, description='AI API地址') + db.add(config) + + if api_key: + config = db.query(SystemConfig).filter(SystemConfig.key == 'ai_api_key').first() + if config: + config.value = api_key + else: + config = SystemConfig(key='ai_api_key', value=api_key, description='AI API密钥') + db.add(config) + + if model: + config = db.query(SystemConfig).filter(SystemConfig.key == 'ai_model').first() + if config: + config.value = model + else: + config = SystemConfig(key='ai_model', value=model, description='AI模型名称') + db.add(config) + + db.commit() + + # 更新AI服务配置 + configs = {c.key: c.value for c in db.query(SystemConfig).filter(SystemConfig.key.startswith('ai_')).all()} + ai_service.update_config( + configs.get('ai_api_base', 'http://192.168.2.17:19007/v1'), + configs.get('ai_api_key', 'xxxx'), + configs.get('ai_model', 'auto') + ) + + return {"success": True, "message": "AI配置已更新"} + + +@app.get("/api/admin/models") +async def get_available_models(db: Session = Depends(get_db)): + """获取可用模型列表""" + import httpx + + # 从数据库读取最新配置 + configs = {c.key: c.value for c in db.query(SystemConfig).filter(SystemConfig.key.startswith('ai_')).all()} + api_base = configs.get('ai_api_base', '') + api_key = configs.get('ai_api_key', 'xxxx') + + if not api_base: + # 返回默认模型列表 + return { + "models": [ + {"id": "auto", "name": "auto (自动选择)", "owned_by": "system"}, + {"id": "qwen3.5-4b", "name": "qwen3.5-4b", "owned_by": "local"}, + {"id": "dsv32", "name": "dsv32", "owned_by": "deepseek"}, + {"id": "glm-4", "name": "glm-4", "owned_by": "zhipu"}, + {"id": "gpt-4o", "name": "gpt-4o", "owned_by": "openai"}, + {"id": "claude-3-opus", "name": "claude-3-opus", "owned_by": "anthropic"} + ], + "success": False, + "message": "请先配置API地址" + } + + try: + # 从当前配置的API地址获取模型列表 + url = f"{api_base}/models" + headers = {"Authorization": f"Bearer {api_key}"} + + logger.info(f"获取模型列表: url={url}") + + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + models = [] + for m in data.get('data', []): + model_id = m.get('id', '') + if model_id: + models.append({ + "id": model_id, + "name": m.get('name', model_id), + "owned_by": m.get('owned_by', 'unknown') + }) + return {"models": models, "success": True} + except Exception as e: + logger.error(f"获取模型列表失败: {e}") + + # 返回默认模型列表 + return { + "models": [ + {"id": "auto", "name": "auto (自动选择)", "owned_by": "system"}, + {"id": "qwen3.5-4b", "name": "qwen3.5-4b", "owned_by": "local"}, + {"id": "dsv32", "name": "dsv32", "owned_by": "deepseek"}, + {"id": "glm-4", "name": "glm-4", "owned_by": "zhipu"}, + {"id": "gpt-4o", "name": "gpt-4o", "owned_by": "openai"}, + {"id": "claude-3-opus", "name": "claude-3-opus", "owned_by": "anthropic"} + ], + "success": False, + "message": "无法从API获取模型列表,显示默认列表" + } + + +@app.post("/api/admin/test-ai") +async def test_ai_connection(db: Session = Depends(get_db)): + """测试AI连接""" + import httpx + + # 从数据库读取最新配置,如果没有则使用默认值 + configs = {c.key: c.value for c in db.query(SystemConfig).filter(SystemConfig.key.startswith('ai_')).all()} + + # 使用数据库值或默认值 + api_base = configs.get('ai_api_base') or 'http://192.168.2.17:19007/v1' + api_key = configs.get('ai_api_key') or 'xxxx' + model = configs.get('ai_model') or 'auto' + + # 判断是否使用默认值 + using_defaults = not configs.get('ai_api_base') + + try: + url = f"{api_base}/chat/completions" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + payload = { + "model": model, + "messages": [{"role": "user", "content": "测试连接"}], + "max_tokens": 50 + } + + logger.info(f"测试AI连接: url={url}, model={model}, using_defaults={using_defaults}") + + async with httpx.AsyncClient(timeout=15.0) as client: + response = await client.post(url, headers=headers, json=payload) + if response.status_code == 200: + data = response.json() + content = data['choices'][0]['message']['content'] + + result = { + "success": True, + "message": f"连接成功!模型响应: {content[:100]}", + "model": model, + "api_base": api_base + } + + if using_defaults: + result["message"] += "\n(使用默认配置,点击「保存配置」可持久化)" + + return result + else: + error_text = response.text[:200] if response.text else "" + return { + "success": False, + "message": f"连接失败: HTTP {response.status_code} - {error_text}", + "model": model, + "api_base": api_base + } + except httpx.ConnectError as e: + return { + "success": False, + "message": f"无法连接到API地址: {api_base}", + "model": model, + "api_base": api_base + } + except httpx.TimeoutException: + return { + "success": False, + "message": f"连接超时(15秒)", + "model": model, + "api_base": api_base + } + except Exception as e: + return { + "success": False, + "message": f"连接失败: {str(e)}", + "model": model, + "api_base": api_base + } + + @app.post("/api/admin/config") async def update_config(data: dict, db: Session = Depends(get_db)): """更新系统配置""" diff --git a/main_v2.py b/main_v2.py index b03dd06..feba861 100644 --- a/main_v2.py +++ b/main_v2.py @@ -786,21 +786,27 @@ async def websocket_endpoint(websocket: WebSocket, user_id: str): # 处理文件内容,添加到消息 image_contents = [] # 图片内容(用于视觉模型) + text_contents = [] # 文本文件内容 if files: for f in files: if f.get('type') and f['type'].startswith('image/'): - # 图片:记录 base64 数据,后续可能用于视觉模型 + # 图片:记录 base64 数据,用于视觉模型 image_contents.append({ 'name': f['name'], 'type': f['type'], 'data': f.get('content', '') # base64 数据 }) - message += f"\n[图片: {f['name']}]" + # 不添加文件名文本,图片信息保存在 extra_data 中 elif f.get('content'): - # 文本文件:直接添加内容 - message += f"\n\n文件 {f['name']} 内容:\n{f['content'][:3000]}" + # 文本文件:直接添加内容,不带文件名前缀 + text_contents.append(f['content'][:3000]) if len(f['content']) > 3000: - message += "...(内容过长已截断)" + text_contents[-1] += "...(内容过长已截断)" + + # 如果有文本文件内容,追加到消息后面 + if text_contents: + for content in text_contents: + message += f"\n\n{content}" # 保存图片信息到 extra_data(用于历史记录) extra_data_for_msg = None diff --git a/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.blacklisted_devices b/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.blacklisted_devices new file mode 100644 index 0000000..e69de29 diff --git a/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.ignored_devices b/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.ignored_devices new file mode 100644 index 0000000..e69de29 diff --git a/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.trusted_devices b/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.trusted_devices new file mode 100644 index 0000000..3369e07 --- /dev/null +++ b/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.trusted_devices @@ -0,0 +1,11 @@ +@tester:matrix.tphai.com AJFVRTHLJY matrix-ed25519 4mRjLhM8xbwjkwQP2T/iB3UZJoaADgP6cCVUiB8AtSk +@tester:matrix.tphai.com ATYFRXKHEQ matrix-ed25519 WnaxV7S11wrqlojKOR3j2RDlPL7TrO17U2ablFISbnw +@tester:matrix.tphai.com BDTRXIGPBE matrix-ed25519 gjQNtLEpIEYCjmzUx5ma91G498n4UADh84KUmiReJUM +@tester:matrix.tphai.com GALBNVJOSG matrix-ed25519 /a7qD2Od76/+Xrr/naDqWEQJZ982X9XdYkCBbRmKxBU +@tester:matrix.tphai.com GVSFGGYNJL matrix-ed25519 8qV2own4G3m2nki+izFDBOrAxtbGl8RoneM3qUPkThU +@tester:matrix.tphai.com IMEQIQPXTR matrix-ed25519 6Yd4lmhP6jdkkNvh1rIw6TRK331ZUyiAt5G5hPeYqSE +@tester:matrix.tphai.com MIPPYHRVAS matrix-ed25519 s8Ol56sxLCjCOi0Gkv/Kj7LqVMp/8ZmuAJ6QA1rUi7o +@tester:matrix.tphai.com UKJGJYQQLT matrix-ed25519 opC9rhsz1nzrvQqNWMKTF5FxWIGuHTDfixx+q/Y8ea0 +@tester:matrix.tphai.com UPMZGRLESG matrix-ed25519 86c6XPCIYHgesq83C2k5xhXNa0EYMnqTq4jFrTwJX8I +@huangzhuang_bro:matrix.tphai.com BQHGFLQEPR matrix-ed25519 IrEHmvqotfHKLyx1JRJp4RthUVyBT8qQX72qBifRRyQ +@huangzhuang_bro:matrix.tphai.com NTVATQQGPK matrix-ed25519 lKMDsoTFK/Lc8yXoqqHBBeuK2HPKAaFFm9KjxgQzEy0 diff --git a/services/__pycache__/__init__.cpython-310.pyc b/services/__pycache__/__init__.cpython-310.pyc index 1fd9702f10d23c87797e9b198987e4fc98c4deba..c543ee7b1efba250d9ac96a37005340f0d396374 100644 GIT binary patch literal 685 zcmZ8eOK#gR5FJXg^|fOszJwNufNWb7O@pEckQ4z7v?w=gre_KGLI625v3pzxjOt@W+$qD{B>SrDGJ^c5S9Z}!J zUVZtpo$ktQw__r!)2C!MzmQ99y7$ItW5+MQG=(e2#)MEI2>Au($k*uM;X}31l2x~y zs_H~9nW_a{S8Hv)wM$yF<6653Zh1xdalN2jwaedDgmA?>LQa-|^Ifnv{@1oM T2NP7FNjO7+I|Xw0V-fxXg2J>8 delta 189 zcmZ3>+QY1!&&$ij00i?6-^~02q#uJg$bbpRaRB0Ci;3E5;;d1uxolBvj11`vDJ+W^ zqu5hegBdj0CT_8qY|Y4{!Reb=Qj}TYlwYFBS_INr#0(->Km;p@U;`3!iAjxSCvD$7hxMX0#N9v`2WlM^4mlA#FXx+1R0R!q@696+W30}rDBBM%b*o%1DX diff --git a/services/__pycache__/conversation_service.cpython-310.pyc b/services/__pycache__/conversation_service.cpython-310.pyc index d727d0990198c79c8a9ce697301e77ec84d61c89..925acb09a6c7e7889f08ab4aafa316d31df2876d 100644 GIT binary patch literal 4984 zcmb7I>u(&@6`#5D*xB)Vy|xpRfT4y`=mJ^OK%a`zq`W0++B&rAO4VrDJGR%E-F5EF zCb2b^5+#5`8)z#UR36g0>W5N^q^i_HAt_S-jN6aIYv&W+3gwaZckb*ajk2NByAham zvuvuq8CZ3@Y^%N%WXhS90yylzsb|aCx?6VZL*=1*uABo$rab%;t+CV8{QUBFb&dYO z>jWcdUW{eS!#~v;`Z4V>jk|p4w8n={v2uZ1vsy9N`VlVXUcUJ1)`d4V&#!O(^106I zXE$D0pV~8ZbZ_bYsf}kZZk+$y=9x9O^YVqxFHUw&ys>rS)W&nqV)TO(ClAj1jcDur zOIz>!ar3P+opXQMcJGiR5jA%w{|e^DX$ErM z#qY-XI}{)G$34Hwix}C-O_+RIj;ED8>x-kcsxKLTrpi*Pq&~HWNSOtYSdTSNi-yDO|mZjr)wMH%Syd88) z6U5eCePwchfTbSmj5Ln<&&Uk!D!dw(IbI8wg37YjsMLL#Rm0Kpk}vc1N+fE>y&j`2 z{HP@wRSU=T9&$%&N=pKrSX5d{+qiow<*J8^XQZoq;#V~8_#KBuHzP<6%xz5_TjkVV{ z-g|fRU))_Xp20ZGT=~Yk{*>WB!qlQj?Md# z*A!k=_<#ka>RlKQZv&ZRrq1wZC-k$N?dmz*G~KJNZt5#{4KnhL+es^`MS(9}cvPd| z2QYb+MCd)+EXXm+^#~SR*A^9EJj-@#uW4_vRTEk_#IDGUVbhq!1~o*+6a1AH`MEc9C5Jgc;>b3)ukl2!cVJR{nK%q_7xk60 zX|$Myl3Q?Ti|B*WSn!XFVPa8$NuerkAu>tiR*-G>y0)>}R_4;D_uFl}Yp^uWJ=id$ zO?EMxk>Qd*=QEZ`diLjC6Y48>Ka`5-*(A(M1NKx`xdE5Rek$Tf5ETsrBaSetMY>F4 zBT+I$R*E1uOZ2jvdP5BTqVxKhMCwCD zcc9qhJ2G1fyCE!{rT`3k&n!!~wM3bkBdVpz{jdr+2!ft=l+X2PXM%)OV3}4?_!y1| z$qJ++Xz6OfV4ttt^F>;^aRX_JCIUEvT_aQ*(hi#~fsEz)$0JekaLbA;L<_aX;aX#! z%%iv*)D4Y@0N$-5wFGoCUd`ejy@%X@Y^T)z4|GWFqJRvi;u~>p#de&D9iX(cnr&p$ zGn}2ZR za3&g#jZ-?e4x8d}ZZ~wExk-y%FQ@KxD9#8pyyw-aBi$Uq+J)}&JeV} zw1OIPXVpNz_fY>DewFY-)*VzaAjoaX9`Gk8rWzs%b-gu)MQKFq+BqHI$|~1!jgCPA zR5?}!v{4DGg1}3B4FriqWo-p{YP2u1a}?QK<%K7)B87Pqgn3hgw2@wPg({G3dLY1~ zw4|bM4xLzQqsCm&6!>Mff-jXYswn(f4kN!_)iK_C7&;`;ELun&Kabo0EV~LUS%g%J z_zZcS{fZr7l#||M;<`IO4rUb0taQKB>-}Eq?h9}zDzK&fzoDJ!n47hg17DAL4oRY(W= zz>Yo_q9Bw`&uRJv+tr^Cd$)Za%R(wrT~vRjo@0nS?uwf2?Y{ys^jaFwk~7m^XU$fJi)W-E2> z!LEr;sW90VR)a8U9BIi%)tk23D_E$qCCN`8Go*#V<GCa9YA7(_SPW}oN;)LZBOzIo|| zEl$wqBz**Wo^(AARYNPFe$Mlbv?@U|BPhznD2Oz|NF@J{xLv*08c|YE-b*Z``NS(I zXa%KC)KRr+r2cUs4$$DkM4lq@10p{pavgeAuRvoi$T(U^3mh%4PnnZr6E{uREZ0j_ z=0^-b%n&BbWS=pMeTpLd6I9;+W)Az9W!Xo}b^kT_z@(ioOk~xL#Zj5@Jl?EAMjKze zsGfwntGYUQkIaWhf=Wv-%T@FRaAxe+VA_9jGz)rP`xLIo~SQ1Mf8Xe qO(E@MfjW7RkkZ+gXna56s=`oHE0{Ta@}X!nc7f*f9jbsFv;GT#1@WT* literal 4065 zcmb7H-ESOM6`y6T_)pdda>2nrM-74zUC!AfW}nS0ls&Frpo zXEuqgu>?vePEb;)P$b$^+Ld@{UI^lWRBd?UpWwa{Z{nvu@CG#HoIAVD*x5WV);+%8 z_nzN5=XcMx$H!d_zR&*tkLX_$n)Xj@6h9prXP_iC5KOCSj4&FLCauw?UegJ-_1I_{ zHAB^n*lb!gOV!QTuGuhd#ZI$SD>dDk+Z?Nn5$y*Wvzc>MW6l*?^O(7;RZ5+MaDe;Z z{=1LvzW?w~y@$VjtAG2(_Vu2-1N~ZsO5=?vNu~4SwKQtCg1ADY!-ABjQIkX6Ugk*x z14|WM8b3+6klxvL>mnCPfb*sOBNz`>c!;|dzj0`sfs#A|!L=FzB`G6ZuTw^ueiamB z$C<@VWf;9 z#a>dU4aX+b*gSgy#-?!uv^mCJgp&?31K_U6=^{MKTwIJoE-5<~Qov#OVI9j^C}sQ%*^CeN&zprb_k@N8I0U8pdZ&*nb{*GUvKxcLCqQT zLOi6UuG`Zx(;rjw^EntNv~vg!esuHR_S?7mTer5i-rN58qldr$pucsmVu%AUCZcQX3fs+QluBFhy5PyGFx2!;8n)V( zWH}C!)DOZmy2$+?tysdtQ$`#r4a8A-LN3a<&(sP}{kHH!!69%J)RWMkJO^Tq8ic~1 zmCOHnmP))N(WQZ~ZyvFKMC&`%62B z4-nGC9*2%t4%s{oIsjD%9MeSkn$%bMWnsgBGMYGowWBCLhm*|$;Pcq~A_(afci)fs z^Z@MQoMW&mLEo6A2)Tm&Q_oa@lZ}&S3(>cnhh%lcF6!{7*a96!Aw36`XU7L1M>>d3 z?#Vf9;84gXVg`i*;V{-vNihqeLUa992U0-6wAxf%;WUoyI1?ye$l^uE!Xpr!9ao{d4Qm?X2O;#~=o!!FUov@W$~F9TkrI(3wFc6}1&1O1_F1 zp%oA#KTev zh0PfFx{(=O4}BGL@2gs7d{4VVz*F^31-36Dwte%h?YIB($^9*0ng<`<{O%7Qe0crA zhrj9H`t>LGuZ09=4?j$v@TN*>d|_7LRzELol8iLqwhc&wZ`0q=3ly394i!(l?>jIV zQa)OJRg&g@pK}*u$kHK;SUX*Zc`8Rcs%@Nk7K}g~yE(eQ3QOB!y}B5;!yryhLuI&G zWn7@H1)|*N317oebO-S*6sJ(^`T^!McnlxDpemfzFb0lvhAyyQo!R}5!|;wjgJtRo zbs=vth(}#`W6)z9vrIQ$`G3^i^WI<(VL6NYp8&DzJT#-?00Xl0Aq8*HuC5#c%)@e5 zz*lB_tGn}uKjzO0IKiX8-OYQz_^-bS>7jz~Wtf=bh-TjtBBNMjn(>%JwpO&%DlAm* z25N5se;@{<6NeG2EBca{gdLW|7+!$p4frvE%mV*T|>Jd`_lFOW}9_ltb4wH zp%cXUh(He(n1D$=Nrg&o#q+8ewbDF=cnPN33shRN^>$p94c+Q%dP%|py()NAU4&k~M{J4l(D?%Ns zZlN;ja&jRK;&7EW*Q;&O%=I`9J4*{R<#Cd$f=^|BLp*uZm87$nLvtE~JHcBT+NAD5 L?(C3*bV~mp(U1Lw diff --git a/templates/admin/index.html b/templates/admin/index.html index a241c32..cf2fa1e 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -144,6 +144,197 @@ background: #f9f9f9; } + /* AI配置专用样式 */ + .ai-config-section { + background: #f8f9fa; + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + } + + .ai-config-section h3 { + font-size: 18px; + color: #333; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; + } + + .ai-config-section h3 i { + color: #10a37f; + } + + .ai-status { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + background: #fff; + border-radius: 8px; + margin-bottom: 16px; + } + + .ai-status.ok { + border: 1px solid #10a37f; + } + + .ai-status.error { + border: 1px solid #dc3545; + } + + .ai-status-dot { + width: 12px; + height: 12px; + border-radius: 50%; + } + + .ai-status-dot.ok { + background: #10a37f; + } + + .ai-status-dot.error { + background: #dc3545; + } + + .ai-status-text { + flex: 1; + } + + .ai-config-form { + display: grid; + gap: 16px; + } + + .config-row { + display: grid; + grid-template-columns: 150px 1fr; + gap: 16px; + align-items: center; + } + + .config-row label { + font-weight: 500; + color: #555; + } + + .config-row input, .config-row select { + padding: 12px 16px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 14px; + width: 100%; + } + + .config-row input:focus, .config-row select:focus { + outline: none; + border-color: #10a37f; + } + + /* 模型输入组合框 */ + .model-input-wrapper { + display: flex; + position: relative; + } + + .model-input-wrapper input { + flex: 1; + padding-right: 40px; + } + + .btn-model-dropdown { + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + width: 32px; + height: 32px; + background: #f0f0f0; + border: none; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: #666; + transition: all 0.2s; + } + + .btn-model-dropdown:hover { + background: #e0e0e0; + color: #333; + } + + /* datalist样式提示 */ + .model-input-wrapper input::-webkit-calendar-picker-indicator { + opacity: 0; + width: 32px; + height: 32px; + cursor: pointer; + } + + .config-actions { + display: flex; + gap: 12px; + margin-top: 16px; + } + + .btn { + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + border: none; + transition: all 0.2s; + } + + .btn-primary { + background: #10a37f; + color: #fff; + } + + .btn-primary:hover { + background: #0d8c6d; + } + + .btn-secondary { + background: #fff; + color: #333; + border: 1px solid #ddd; + } + + .btn-secondary:hover { + background: #f0f0f0; + } + + .btn-test { + background: #007bff; + color: #fff; + } + + .btn-test:hover { + background: #0056b3; + } + + .test-result { + padding: 16px; + border-radius: 8px; + margin-top: 16px; + } + + .test-result.success { + background: #d4edda; + border: 1px solid #10a37f; + color: #155724; + } + + .test-result.error { + background: #f8d7da; + border: 1px solid #dc3545; + color: #721c24; + } + + /* 原有配置表单样式 */ .config-form { display: flex; gap: 16px; @@ -167,20 +358,6 @@ min-height: 60px; } - .config-form button { - padding: 12px 24px; - background: #10a37f; - color: #fff; - border: none; - border-radius: 8px; - cursor: pointer; - font-size: 14px; - } - - .config-form button:hover { - background: #0d8c6d; - } - .config-list { margin-top: 24px; } @@ -246,6 +423,11 @@ text-overflow: ellipsis; white-space: nowrap; } + + .loading { + opacity: 0.6; + pointer-events: none; + } @@ -273,14 +455,97 @@
- - - + + + +
+ +
+
+

大模型配置

+ +
+
+
检测中...
+
+ +
+
+ + +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + +
+
+
+ +
+ + + +
+ + +
+ +
+

当前状态

+ + + + + + + + + + + + + + + + + + + + + +
配置项当前值状态
API地址--
模型--
连接状态-检测中
+
+
+ -
+
@@ -318,13 +583,13 @@
- +
- +
@@ -338,6 +603,7 @@ // 初始化 document.addEventListener('DOMContentLoaded', () => { loadStats(); + loadAIConfig(); loadUsers(); loadConversations(); loadConfig(); @@ -352,6 +618,208 @@ document.getElementById(`${tabName}Panel`).classList.add('active'); } + // 切换模型下拉显示 + function toggleModelDropdown() { + const input = document.getElementById('aiModel'); + input.focus(); + // 触发datalist显示 + if (input.showPicker) { + input.showPicker(); + } else { + // 兼容性处理:模拟点击 + input.click(); + } + } + + // 加载AI配置 + async function loadAIConfig() { + try { + const response = await fetch('/api/admin/ai-config'); + const data = await response.json(); + + document.getElementById('aiApiBase').value = data.api_base || ''; + document.getElementById('aiApiKey').value = data.api_key || ''; + document.getElementById('aiModel').value = data.model || 'auto'; + + // 更新当前状态显示 + document.getElementById('currentApiBase').textContent = data.api_base || '-'; + document.getElementById('currentModel').textContent = data.model || '-'; + + // 更新状态指示 + if (data.use_mock) { + document.getElementById('aiStatusDot').className = 'ai-status-dot error'; + document.getElementById('aiStatusText').textContent = '当前使用Mock模式(未连接真实API)'; + document.getElementById('aiStatus').className = 'ai-status error'; + document.getElementById('connectionStatus').textContent = 'Mock模式'; + document.getElementById('connectionBadge').className = 'badge inactive'; + document.getElementById('connectionBadge').textContent = '未连接'; + } else { + document.getElementById('aiStatusDot').className = 'ai-status-dot ok'; + document.getElementById('aiStatusText').textContent = '已配置真实API'; + document.getElementById('aiStatus').className = 'ai-status ok'; + document.getElementById('connectionStatus').textContent = '已配置'; + document.getElementById('connectionBadge').className = 'badge active'; + document.getElementById('connectionBadge').textContent = '待测试'; + } + + document.getElementById('apiBaseStatus').className = 'badge active'; + document.getElementById('apiBaseStatus').textContent = '已配置'; + document.getElementById('modelStatus').className = 'badge active'; + document.getElementById('modelStatus').textContent = data.model || 'auto'; + + } catch (error) { + console.error('加载AI配置失败:', error); + } + } + + // 保存AI配置 + async function saveAIConfig() { + const apiBase = document.getElementById('aiApiBase').value.trim(); + const apiKey = document.getElementById('aiApiKey').value.trim(); + const model = document.getElementById('aiModel').value.trim(); + + if (!apiBase) { + alert('请填写API地址'); + return; + } + + try { + const btn = event.target; + btn.classList.add('loading'); + btn.textContent = '保存中...'; + + const response = await fetch('/api/admin/ai-config', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({api_base: apiBase, api_key: apiKey, model: model}) + }); + + const data = await response.json(); + + btn.classList.remove('loading'); + btn.innerHTML = ' 保存配置'; + + if (data.success) { + // 显示成功提示 + const resultDiv = document.getElementById('testResult'); + resultDiv.style.display = 'block'; + resultDiv.className = 'test-result success'; + resultDiv.innerHTML = ` ${data.message}`; + + // 重新加载配置 + loadAIConfig(); + + // 3秒后隐藏提示 + setTimeout(() => resultDiv.style.display = 'none', 3000); + } else { + alert('保存失败: ' + (data.message || '未知错误')); + } + } catch (error) { + console.error('保存AI配置失败:', error); + alert('保存失败: ' + error.message); + event.target.classList.remove('loading'); + event.target.innerHTML = ' 保存配置'; + } + } + + // 测试AI连接 + async function testAIConnection() { + try { + const btn = event.target; + btn.classList.add('loading'); + btn.innerHTML = ' 测试中...'; + + const response = await fetch('/api/admin/test-ai', { + method: 'POST' + }); + + const data = await response.json(); + + btn.classList.remove('loading'); + btn.innerHTML = ' 测试连接'; + + const resultDiv = document.getElementById('testResult'); + resultDiv.style.display = 'block'; + + if (data.success) { + resultDiv.className = 'test-result success'; + resultDiv.innerHTML = ` 连接成功!
模型: ${data.model}
响应: ${data.message}`; + + // 更新连接状态 + document.getElementById('aiStatusDot').className = 'ai-status-dot ok'; + document.getElementById('aiStatusText').textContent = '连接正常'; + document.getElementById('aiStatus').className = 'ai-status ok'; + document.getElementById('connectionStatus').textContent = '正常'; + document.getElementById('connectionBadge').className = 'badge active'; + document.getElementById('connectionBadge').textContent = '已连接'; + } else { + resultDiv.className = 'test-result error'; + resultDiv.innerHTML = ` 连接失败
${data.message}`; + + // 更新连接状态 + document.getElementById('aiStatusDot').className = 'ai-status-dot error'; + document.getElementById('aiStatusText').textContent = '连接失败'; + document.getElementById('aiStatus').className = 'ai-status error'; + document.getElementById('connectionStatus').textContent = '失败'; + document.getElementById('connectionBadge').className = 'badge inactive'; + document.getElementById('connectionBadge').textContent = '错误'; + } + } catch (error) { + console.error('测试连接失败:', error); + event.target.classList.remove('loading'); + event.target.innerHTML = ' 测试连接'; + + const resultDiv = document.getElementById('testResult'); + resultDiv.style.display = 'block'; + resultDiv.className = 'test-result error'; + resultDiv.innerHTML = ` 测试失败: ${error.message}`; + } + } + + // 刷新模型列表 + async function refreshModels() { + try { + const btn = event.target; + btn.classList.add('loading'); + btn.innerHTML = ' 刷新中...'; + + const response = await fetch('/api/admin/models'); + const data = await response.json(); + + btn.classList.remove('loading'); + btn.innerHTML = ' 刷新模型列表'; + + // 更新datalist + const datalist = document.getElementById('modelList'); + datalist.innerHTML = ''; + + for (const model of data.models) { + const option = document.createElement('option'); + option.value = model.id; + option.textContent = model.name; + datalist.appendChild(option); + } + + // 显示提示 + const resultDiv = document.getElementById('testResult'); + resultDiv.style.display = 'block'; + resultDiv.className = 'test-result success'; + resultDiv.innerHTML = ` 获取到 ${data.models.length} 个模型,可在输入框中选择`; + + if (!data.success) { + resultDiv.className = 'test-result error'; + resultDiv.innerHTML = ` ${data.message || '使用默认模型列表'}`; + } + + setTimeout(() => resultDiv.style.display = 'none', 3000); + + } catch (error) { + console.error('刷新模型列表失败:', error); + event.target.classList.remove('loading'); + event.target.innerHTML = ' 刷新模型列表'; + } + } + // 加载统计数据 async function loadStats() { try { @@ -423,7 +891,7 @@ } } - // 加载配置 + // 加载其他配置 async function loadConfig() { try { const response = await fetch('/api/admin/config'); @@ -431,12 +899,15 @@ const container = document.getElementById('configList'); - if (data.configs.length === 0) { - container.innerHTML = '

暂无配置

'; + // 过滤掉AI配置(在AI配置面板单独显示) + const otherConfigs = data.configs.filter(c => !c.key.startsWith('ai_')); + + if (otherConfigs.length === 0) { + container.innerHTML = '

暂无其他配置

'; return; } - container.innerHTML = data.configs.map(config => ` + container.innerHTML = otherConfigs.map(config => `
${config.key}
${config.value}
@@ -448,7 +919,7 @@ } } - // 保存配置 + // 保存其他配置 async function saveConfig() { const key = document.getElementById('configKey').value.trim(); const value = document.getElementById('configValue').value.trim(); @@ -497,6 +968,9 @@ // 定时刷新 setInterval(() => { loadStats(); + if (document.getElementById('aiPanel').classList.contains('active')) { + loadAIConfig(); + } if (document.getElementById('usersPanel').classList.contains('active')) { loadUsers(); }