From 1225b6cbf0a028b765a0ab6d784bcb80459a67bb Mon Sep 17 00:00:00 2001 From: yj <2077506045@qq.com> Date: 星期三, 23 七月 2025 17:59:54 +0800 Subject: [PATCH] 功能更新 --- .gitignore | 8 app/models/conversation.py | 47 app/utils/logger.py | 6 start.bat | 42 + config.production.json | 30 logs/app.log | 13 requirements.txt | 2 app/services/ecloud_client.py | 234 ++++++ app/services/redis_queue.py | 27 app/workers/message_worker.py | 62 + main.py | 13 ecloud_dify.spec | 127 +++ app/api/callback.py | 4 install_service.bat | 102 +++ config.example.json | 34 + CONFIG_GUIDE.md | 103 +++ README.md | 7 uninstall_service.bat | 61 + /dev/null | 16 startup.py | 139 ++++ E云管家接口文档.txt | 176 +++++ app/services/contact_sync.py | 167 +++++ DEPLOYMENT_WINDOWS.md | 186 +++++ config.py | 114 ++ app/services/message_processor.py | 159 ++++ 25 files changed, 1,730 insertions(+), 149 deletions(-) diff --git a/.gitignore b/.gitignore index a878fc4..5b0371a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # 蹇界暐鐗瑰畾鏂囦欢 config.ini +config.json secrets.json # 蹇界暐鏁翠釜鐩綍 @@ -25,4 +26,9 @@ .idea/ # 渚嬪锛氫笉蹇界暐 dist 鐩綍涓嬬殑 important.js -!dist/important.js \ No newline at end of file +!dist/important.js + +# 鍚屾椂蹇界暐鎵�鏈� .pyc 鏂囦欢锛堝彲閫変絾鎺ㄨ崘锛� +*.pyc +*.pyo +*.pyd \ No newline at end of file diff --git a/CONFIG_GUIDE.md b/CONFIG_GUIDE.md new file mode 100644 index 0000000..e7d48d4 --- /dev/null +++ b/CONFIG_GUIDE.md @@ -0,0 +1,103 @@ +# 閰嶇疆鏂囦欢浣跨敤鎸囧崡 + +## 姒傝堪 + +椤圭洰宸蹭粠Python閰嶇疆鏂囦欢锛坈onfig.py锛夎浆鎹负JSON閰嶇疆鏂囦欢锛坈onfig.json锛夛紝杩欐牱鏇撮�傚悎鎵撳寘涓篹xe鏂囦欢銆� + +## 閰嶇疆鏂囦欢缁撴瀯 + +### config.json +```json +{ + "database": { + "url": "mysql+pymysql://root:password@host:port/database" + }, + "redis": { + "url": "redis://localhost:6379/0" + }, + "ecloud": { + "base_url": "http://125.122.152.142:9899", + "authorization": "your_authorization_token" + }, + "dify": { + "base_url": "https://api.dify.ai/v1", + "api_key": "your_dify_api_key" + }, + "server": { + "host": "0.0.0.0", + "port": 7979, + "debug": true + }, + "logging": { + "level": "INFO", + "file": "logs/app.log" + }, + "message_processing": { + "max_retry_count": 3, + "retry_delay": 5, + "queue_timeout": 300 + } +} +``` + +## 閰嶇疆椤硅鏄� + +### 鏁版嵁搴撻厤缃� (database) +- `url`: 鏁版嵁搴撹繛鎺ュ瓧绗︿覆锛屾敮鎸丮ySQL + +### Redis閰嶇疆 (redis) +- `url`: Redis杩炴帴瀛楃涓� + +### E浜戠瀹堕厤缃� (ecloud) +- `base_url`: E浜戠瀹禔PI鍩虹URL +- `authorization`: E浜戠瀹禔PI鎺堟潈浠ょ墝 + +### DifyAI閰嶇疆 (dify) +- `base_url`: DifyAI API鍩虹URL +- `api_key`: DifyAI API瀵嗛挜 + +### 鏈嶅姟鍣ㄩ厤缃� (server) +- `host`: 鏈嶅姟鍣ㄧ洃鍚湴鍧� +- `port`: 鏈嶅姟鍣ㄧ洃鍚鍙� +- `debug`: 鏄惁鍚敤璋冭瘯妯″紡 + +### 鏃ュ織閰嶇疆 (logging) +- `level`: 鏃ュ織绾у埆 (DEBUG, INFO, WARNING, ERROR) +- `file`: 鏃ュ織鏂囦欢璺緞 + +### 娑堟伅澶勭悊閰嶇疆 (message_processing) +- `max_retry_count`: 鏈�澶ч噸璇曟鏁� +- `retry_delay`: 閲嶈瘯寤惰繜鏃堕棿锛堢锛� +- `queue_timeout`: 闃熷垪瓒呮椂鏃堕棿锛堢锛� + +## 浣跨敤鏂规硶 + +### 1. 淇敼閰嶇疆 +鐩存帴缂栬緫 `config.json` 鏂囦欢鍗冲彲锛屽簲鐢ㄤ細鑷姩鍔犺浇鏂伴厤缃�� + +### 2. 閰嶇疆鏂囦欢浣嶇疆 +- 寮�鍙戠幆澧冿細椤圭洰鏍圭洰褰曚笅鐨� `config.json` +- 鐢熶骇鐜锛歟xe鏂囦欢鍚岀洰褰曚笅鐨� `config.json` + +### 3. 閰嶇疆楠岃瘉 +濡傛灉閰嶇疆鏂囦欢涓嶅瓨鍦ㄦ垨鏍煎紡閿欒锛岀郴缁熶細浣跨敤榛樿閰嶇疆骞惰緭鍑洪敊璇俊鎭�� + +## 鍏煎鎬� + +- 鍘熸湁鐨� `from config import settings` 瀵煎叆鏂瑰紡淇濇寔涓嶅彉 +- 鎵�鏈夐厤缃睘鎬х殑璁块棶鏂瑰紡淇濇寔涓嶅彉锛堝 `settings.database_url`锛� +- 鍚戝悗鍏煎锛屼笉闇�瑕佷慨鏀圭幇鏈変唬鐮� + +## 鎵撳寘涓篹xe鐨勪紭鍔� + +1. **閰嶇疆澶栭儴鍖�**: 閰嶇疆鏂囦欢鐙珛浜巈xe鏂囦欢锛屼究浜庨儴缃叉椂淇敼 +2. **鏃犻渶閲嶆柊缂栬瘧**: 淇敼閰嶇疆涓嶉渶瑕侀噸鏂版墦鍖卐xe +3. **鏄撲簬缁存姢**: JSON鏍煎紡鐩磋鏄撹锛屼究浜庤繍缁翠汉鍛橀厤缃� +4. **鐗堟湰鎺у埗鍙嬪ソ**: 鍙互涓轰笉鍚岀幆澧冨噯澶囦笉鍚岀殑閰嶇疆鏂囦欢 + +## 娉ㄦ剰浜嬮」 + +1. 纭繚 `config.json` 鏂囦欢鏍煎紡姝g‘锛屽彲浠ヤ娇鐢↗SON楠岃瘉宸ュ叿妫�鏌� +2. 鏁忔劅淇℃伅锛堝鏁版嵁搴撳瘑鐮併�丄PI瀵嗛挜锛夊簲濡ュ杽淇濈 +3. 鐢熶骇鐜寤鸿灏� `debug` 璁剧疆涓� `false` +4. 鏃ュ織鏂囦欢璺緞纭繚搴旂敤鏈夊啓鍏ユ潈闄� diff --git a/DEPLOYMENT_WINDOWS.md b/DEPLOYMENT_WINDOWS.md new file mode 100644 index 0000000..797b74d --- /dev/null +++ b/DEPLOYMENT_WINDOWS.md @@ -0,0 +1,186 @@ +# E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 Windows閮ㄧ讲鎸囧崡 + +## 姒傝堪 + +鏈枃妗d粙缁嶅浣曞湪Windows鏈嶅姟鍣ㄤ笂閮ㄧ讲E浜戠瀹�-DifyAI瀵规帴鏈嶅姟鐨別xe鐗堟湰銆� + +## 绯荤粺瑕佹眰 + +- Windows Server 2016 鎴栨洿楂樼増鏈� +- Windows 10/11 (鐢ㄤ簬娴嬭瘯) +- 鑷冲皯 2GB 鍙敤鍐呭瓨 +- 鑷冲皯 1GB 鍙敤纾佺洏绌洪棿 +- 缃戠粶杩炴帴锛堢敤浜庤闂暟鎹簱銆丷edis銆丒浜戠瀹跺拰DifyAI鏈嶅姟锛� + +## 閮ㄧ讲姝ラ + +### 1. 鍑嗗閮ㄧ讲鏂囦欢 + +纭繚浠ヤ笅鏂囦欢瀛樺湪浜庨儴缃茬洰褰曚腑锛� +- `ecloud_dify.exe` - 涓荤▼搴忔枃浠� +- `config.production.json` - 鐢熶骇鐜閰嶇疆妯℃澘 +- `config.example.json` - 閰嶇疆绀轰緥鏂囦欢 +- `install_service.bat` - Windows鏈嶅姟瀹夎鑴氭湰 +- `uninstall_service.bat` - Windows鏈嶅姟鍗歌浇鑴氭湰 +- `start.bat` - 鐩存帴鍚姩鑴氭湰锛堥潪鏈嶅姟妯″紡锛� + +### 2. 閰嶇疆鏈嶅姟 + +#### 2.1 鍒涘缓閰嶇疆鏂囦欢 + +1. 澶嶅埗 `config.production.json` 涓� `config.json` +2. 缂栬緫 `config.json` 鏂囦欢锛屼慨鏀逛互涓嬮厤缃細 + +```json +{ + "database": { + "url": "mysql+pymysql://鐢ㄦ埛鍚�:瀵嗙爜@鏁版嵁搴撳湴鍧�:绔彛/鏁版嵁搴撳悕" + }, + "redis": { + "url": "redis://Redis鍦板潃:绔彛/鏁版嵁搴撶紪鍙�" + }, + "ecloud": { + "base_url": "E浜戠瀹禔PI鍦板潃", + "authorization": "E浜戠瀹舵巿鏉冧护鐗�" + }, + "dify": { + "base_url": "DifyAI API鍦板潃", + "api_key": "DifyAI API瀵嗛挜" + }, + "server": { + "host": "0.0.0.0", + "port": 7979, + "debug": false + } +} +``` + +#### 2.2 閰嶇疆璇存槑 + +- **database.url**: MySQL鏁版嵁搴撹繛鎺ュ瓧绗︿覆 +- **redis.url**: Redis鏈嶅姟鍣ㄨ繛鎺ュ瓧绗︿覆 +- **ecloud**: E浜戠瀹剁浉鍏抽厤缃� +- **dify**: DifyAI鐩稿叧閰嶇疆 +- **server**: 鏈嶅姟鍣ㄧ洃鍚厤缃� + +### 3. 瀹夎鍜屽惎鍔ㄦ湇鍔� + +#### 鏂瑰紡涓�锛歐indows鏈嶅姟妯″紡锛堟帹鑽愶級 + +1. **瀹夎鏈嶅姟**锛� + - 鍙抽敭鐐瑰嚮 `install_service.bat` + - 閫夋嫨"浠ョ鐞嗗憳韬唤杩愯" + - 鎸夌収鎻愮ず瀹屾垚瀹夎 + +2. **绠$悊鏈嶅姟**锛� + ```cmd + # 鍚姩鏈嶅姟 + sc start ECloudDifyService + + # 鍋滄鏈嶅姟 + sc stop ECloudDifyService + + # 鏌ョ湅鏈嶅姟鐘舵�� + sc query ECloudDifyService + + # 鍒犻櫎鏈嶅姟 + sc delete ECloudDifyService + ``` + +3. **鍗歌浇鏈嶅姟**锛� + - 鍙抽敭鐐瑰嚮 `uninstall_service.bat` + - 閫夋嫨"浠ョ鐞嗗憳韬唤杩愯" + +#### 鏂瑰紡浜岋細鐩存帴杩愯妯″紡 + +1. 鍙屽嚮 `start.bat` 鍚姩鏈嶅姟 +2. 鎸� Ctrl+C 鍋滄鏈嶅姟 + +### 4. 楠岃瘉閮ㄧ讲 + +1. **妫�鏌ユ湇鍔$姸鎬�**锛� + - 璁块棶 `http://鏈嶅姟鍣↖P:7979/health` + - 搴旇杩斿洖鍋ュ悍妫�鏌ヤ俊鎭� + +2. **鏌ョ湅鏃ュ織**锛� + - 鏃ュ織鏂囦欢浣嶄簬 `logs/app.log` + - 妫�鏌ユ槸鍚︽湁閿欒淇℃伅 + +3. **娴嬭瘯API**锛� + - 璁块棶 `http://鏈嶅姟鍣↖P:7979/` + - 搴旇杩斿洖鏈嶅姟淇℃伅 + +## 闃茬伀澧欓厤缃� + +纭繚Windows闃茬伀澧欏厑璁哥鍙�7979鐨勫叆绔欒繛鎺ワ細 + +```cmd +# 娣诲姞闃茬伀澧欒鍒� +netsh advfirewall firewall add rule name="ECloudDify Service" dir=in action=allow protocol=TCP localport=7979 +``` + +## 鏁呴殰鎺掗櫎 + +### 甯歌闂 + +1. **鏈嶅姟鍚姩澶辫触**锛� + - 妫�鏌ラ厤缃枃浠舵牸寮忔槸鍚︽纭� + - 纭鏁版嵁搴撳拰Redis杩炴帴鏄惁姝e父 + - 鏌ョ湅鏃ュ織鏂囦欢鑾峰彇璇︾粏閿欒淇℃伅 + +2. **绔彛琚崰鐢�**锛� + - 淇敼閰嶇疆鏂囦欢涓殑绔彛鍙� + - 鎴栬�呭仠姝㈠崰鐢ㄧ鍙g殑鍏朵粬绋嬪簭 + +3. **鏉冮檺闂**锛� + - 纭繚浠ョ鐞嗗憳韬唤杩愯瀹夎鑴氭湰 + - 妫�鏌xe鏂囦欢鐨勬墽琛屾潈闄� + +4. **缃戠粶杩炴帴闂**锛� + - 妫�鏌ラ槻鐏璁剧疆 + - 纭缃戠粶杩炴帴姝e父 + +### 鏃ュ織鏌ョ湅 + +鏃ュ織鏂囦欢浣嶇疆锛歚logs/app.log` + +甯哥敤鏃ュ織绾у埆锛� +- INFO: 涓�鑸俊鎭� +- WARNING: 璀﹀憡淇℃伅 +- ERROR: 閿欒淇℃伅 + +## 鎬ц兘浼樺寲 + +1. **鍐呭瓨浼樺寲**锛� + - 鐩戞帶鍐呭瓨浣跨敤鎯呭喌 + - 蹇呰鏃惰皟鏁寸郴缁熷唴瀛樺垎閰� + +2. **缃戠粶浼樺寲**锛� + - 纭繚缃戠粶寤惰繜杈冧綆 + - 浣跨敤楂橀�熺綉缁滆繛鎺� + +3. **鏁版嵁搴撲紭鍖�**锛� + - 浼樺寲鏁版嵁搴撹繛鎺ユ睜璁剧疆 + - 瀹氭湡娓呯悊鏃ュ織鏁版嵁 + +## 瀹夊叏寤鸿 + +1. **閰嶇疆鏂囦欢瀹夊叏**锛� + - 淇濇姢閰嶇疆鏂囦欢涓殑鏁忔劅淇℃伅 + - 璁剧疆閫傚綋鐨勬枃浠舵潈闄� + +2. **缃戠粶瀹夊叏**锛� + - 浣跨敤HTTPS锛堝鏋滄敮鎸侊級 + - 闄愬埗璁块棶IP鑼冨洿 + +3. **瀹氭湡鏇存柊**锛� + - 瀹氭湡鏇存柊鏈嶅姟鐗堟湰 + - 鍏虫敞瀹夊叏琛ヤ竵 + +## 鑱旂郴鏀寔 + +濡傞亣鍒伴棶棰橈紝璇锋彁渚涗互涓嬩俊鎭細 +- 閿欒鏃ュ織鍐呭 +- 閰嶇疆鏂囦欢锛堥殣钘忔晱鎰熶俊鎭級 +- 绯荤粺鐜淇℃伅 +- 闂澶嶇幇姝ラ diff --git "a/E\344\272\221\347\256\241\345\256\266\346\216\245\345\217\243\346\226\207\346\241\243.txt" "b/E\344\272\221\347\256\241\345\256\266\346\216\245\345\217\243\346\226\207\346\241\243.txt" index eda63e0..a275d7f 100644 --- "a/E\344\272\221\347\256\241\345\256\266\346\216\245\345\217\243\346\226\207\346\241\243.txt" +++ "b/E\344\272\221\347\256\241\345\256\266\346\216\245\345\217\243\346\226\207\346\241\243.txt" @@ -16,7 +16,7 @@ 鍙傛暟鍚� 蹇呴�� 绫诲瀷 璇存槑 wId 鏄� String 鐧诲綍瀹炰緥鏍囪瘑 -wcId 鏄� String 濂藉弸寰俊id/缇d +wcId 鏄� String 濂藉弸寰俊id/缇d锛屽涓娇鐢ㄨ嫳鏂囬�楀彿鍒嗛殧 璇锋眰鍙傛暟绀轰緥 @@ -167,4 +167,176 @@ "message": "澶辫触", "code": "1001", "data": null -} \ No newline at end of file +} + + + +# 鍒濆鍖栭�氳褰曞垪琛ㄦ帴鍙� + +绠�瑕佹弿杩帮細 +鍒濆鍖栭�氳褰曞垪琛� + +璇锋眰URL锛� +http://鍩熷悕鍦板潃/initAddressList + +璇锋眰鏂瑰紡锛� +POST + +璇锋眰澶碒eaders锛� +Content-Type锛歛pplication/json +Authorization锛歿Authorization} + +鍙傛暟锛� +鍙傛暟鍚� 蹇呴�� 绫诲瀷 璇存槑 +wId 鏄� String 鐧诲綍瀹炰緥鏍囪瘑 + +璇锋眰鍙傛暟绀轰緥 +{ + "wId": "6a696578-16ea-4edc-ac8b-e609bca39c69" +} + +鎴愬姛杩斿洖绀轰緥 +{ + "message": "鎴愬姛", + "code": "1000", + "data": null +} + +閿欒杩斿洖绀轰緥 +{ + "message": "澶辫触", + "code": "1001", + "data": null +} + +杩斿洖鏁版嵁锛� +鍙傛暟鍚� 绫诲瀷 璇存槑 +code string 1000鎴愬姛銆�1001澶辫触 +msg string 鍙嶉淇℃伅 +data JSONObject 鏃� + + + +# 鑾峰彇閫氳褰曞垪琛ㄦ帴鍙� +绠�瑕佹弿杩帮細 +鑾峰彇閫氳褰曞垪琛� + +璇锋眰URL锛� +http://鍩熷悕鍦板潃/getAddressList + +璇锋眰鏂瑰紡锛� +POST + +璇锋眰澶碒eaders锛� +Content-Type锛歛pplication/json +Authorization锛歿Authorization} + +鍙傛暟锛� + +鍙傛暟鍚� 蹇呴�� 绫诲瀷 璇存槑 +wId 鏄� String 鐧诲綍瀹炰緥鏍囪瘑 +灏忔彁绀猴細 +鑾峰彇閫氳褰曞垪琛ㄤ箣鍓嶏紝蹇呴』璋冪敤鍒濆鍖栭�氳褰曞垪琛ㄦ帴鍙c�� + +璇锋眰鍙傛暟绀轰緥 +{ + "wId": "6a696578-16ea-4edc-ac8b-e609bca39c69" +} + +鎴愬姛杩斿洖绀轰緥 +{ + "code": "1000", + "message": "鑾峰彇閫氳褰曟垚鍔�", + "data": { + "chatrooms": [ + "" + ], + "friends": [ + "" + ], + "ghs": [ + "" + ], + "others": [ + "" + ] + } +} + +閿欒杩斿洖绀轰緥 +{ + "message": "澶辫触", + "code": "1001", + "data": null +} + +杩斿洖鏁版嵁锛� + +鍙傛暟鍚� 绫诲瀷 璇存槑 +code String 1000鎴愬姛 +1001澶辫触 +msg String 鍙嶉淇℃伅 +data JSONObject +chatrooms JSONArray 缇ょ粍鍒楄〃 +friends JSONArray 濂藉弸鍒楄〃 +ghs JSONArray 鍏紬鍙峰垪琛� +others JSONArray 寰俊鍏朵粬鐩稿叧 + + + +# 缇よ亰@鎺ュ彛 +璇锋眰URL锛� +http://鍩熷悕鍦板潃/sendText + +璇锋眰鏂瑰紡锛� +POST + +璇锋眰澶碒eaders锛� +Content-Type锛歛pplication/json +Authorization锛歿Authorization} + +鍙傛暟锛� +鍙傛暟鍚� 蹇呴�� 绫诲瀷 璇存槑 +wId 鏄� string 鐧诲綍瀹炰緥鏍囪瘑 +wcId 鏄� string 鎺ユ敹鏂圭兢id +content 鏄� string 鏂囨湰鍐呭娑堟伅锛園鐨勫井淇℃樀绉伴渶瑕佽嚜宸辨嫾鎺ワ紝蹇呴』鎷兼帴鑹剧壒绗﹀彿锛屼笉鐒朵笉鐢熸晥锛� +at 鏄� string 鑹剧壒鐨勫井淇d锛堝涓互閫楀彿鍒嗗紑锛� + +杩斿洖鏁版嵁锛� +鍙傛暟鍚� 绫诲瀷 璇存槑 +code string 1000鎴愬姛锛�1001澶辫触 +msg string 鍙嶉淇℃伅 +data +data.type int 绫诲瀷 +data.msgId long 娑堟伅msgId +data.newMsgId long 娑堟伅newMsgId +data.createTime long 娑堟伅鍙戦�佹椂闂存埑 +data.wcId string 娑堟伅鎺ユ敹鏂筰d + +璇锋眰鍙傛暟绀轰緥 +{ + "wId": "0000016f-8911-484a-0001-db2943fc2786", + "wcId": "22270365143@chatroom", + "at": "wxid_lr6j4nononb921,wxid_i6qsbbjenjuj22", + "content": "@E浜慣eam_Mr Li@浣犲井绗戞椂鐪熺編 娴嬭瘯" +} + +鎴愬姛杩斿洖绀轰緥 +{ + "code": "1000", + "message": "澶勭悊鎴愬姛", + "data": { + "type": 1, + "msgId": 2562652205, + "newMsgId": 4482117376572170921, + "createTime": 1641457769, + "wcId": "22270365143@chatroom" + } +} + +閿欒杩斿洖绀轰緥 +{ + "message": "澶辫触", + "code": "1001", + "data": null +} \ No newline at end of file diff --git a/README.md b/README.md index 7d0aac0..9170a7a 100644 --- a/README.md +++ b/README.md @@ -143,13 +143,14 @@ ### conversations锛堝璇濊褰曡〃锛� - `id`: 涓婚敭 -- `user_conversation_key`: 鐢ㄦ埛瀵硅瘽鍞竴閿� - `from_user`: 鍙戦�佺敤鎴峰井淇D - `conversation_id`: Dify瀵硅瘽ID -- `user_question`: 鐢ㄦ埛鎻愰棶 -- `ai_answer`: AI鍥炵瓟 +- `group`: 缇ょ粍ID +- `hour`: 灏忔椂鏍囪瘑(YYYYMMDD_HH) +- `content`: 瀵硅瘽鍐呭(JSON鏍煎紡) - `is_processed`: 鏄惁宸插鐞� - `is_sent`: 鏄惁宸插彂閫� +- 鍞竴鎬х害鏉�: (from_user, conversation_id, group, hour) ## 閰嶇疆璇存槑 diff --git a/app/api/callback.py b/app/api/callback.py index 3975cc3..e4c4858 100644 --- a/app/api/callback.py +++ b/app/api/callback.py @@ -54,10 +54,13 @@ # 灏嗘秷鎭姞鍏ラ槦鍒� success = message_processor.enqueue_callback_message(callback_dict) + logger.info(f"娑堟伅鍏ラ槦缁撴灉: success={success}") + if success: # 鑾峰彇鍙戦�佺敤鎴稩D from_user = callback_dict.get("data", {}).get("fromUser") if from_user: + logger.info(f"鍚姩鐢ㄦ埛闃熷垪澶勭悊: from_user={from_user}") # 鍚姩鐢ㄦ埛闃熷垪澶勭悊 message_worker.process_user_queue(from_user) @@ -65,6 +68,7 @@ success=True, message="娑堟伅宸叉垚鍔熷姞鍏ュ鐞嗛槦鍒�", code=200 ) else: + logger.warning("娑堟伅澶勭悊澶辫触锛屾湭鑳藉姞鍏ラ槦鍒�") return CallbackResponse(success=False, message="娑堟伅澶勭悊澶辫触", code=400) except Exception as e: diff --git a/app/models/conversation.py b/app/models/conversation.py index f153429..eb646c5 100644 --- a/app/models/conversation.py +++ b/app/models/conversation.py @@ -1,38 +1,51 @@ """ 瀵硅瘽璁板綍妯″瀷 """ -from sqlalchemy import Column, String, Integer, DateTime, Text, Boolean + +from sqlalchemy import Column, String, Integer, DateTime, Text, Boolean, UniqueConstraint from sqlalchemy.sql import func from .database import Base class Conversation(Base): """瀵硅瘽璁板綍琛�""" + __tablename__ = "conversations" - + id = Column(Integer, primary_key=True, index=True, autoincrement=True) - user_conversation_key = Column(String(200), unique=True, index=True, nullable=False, - comment="鐢ㄦ埛瀵硅瘽鍞竴閿�(fromUser+conversation_id)") - from_user = Column(String(100), index=True, nullable=False, comment="鍙戦�佺敤鎴峰井淇D") + from_user = Column( + String(100), index=True, nullable=False, comment="鍙戦�佺敤鎴峰井淇D" + ) conversation_id = Column(String(100), nullable=False, comment="Dify瀵硅瘽ID") - - # 鐢ㄦ埛闂 - user_question = Column(Text, nullable=False, comment="鐢ㄦ埛鎻愰棶鍐呭") - - # AI鍥炵瓟 - ai_answer = Column(Text, nullable=True, comment="AI鍥炵瓟鍐呭") - + group = Column(String(100), nullable=False, comment="缇ょ粍ID") + hour = Column(String(20), nullable=False, comment="灏忔椂鏍囪瘑(YYYYMMDD_HH)") + + # 瀵硅瘽鍐呭锛圝SON鏍煎紡锛� + content = Column( + Text, + nullable=True, + comment='瀵硅瘽鍐呭JSON鏍煎紡锛屽[{"user":"浣犲ソ","ai":"浣犲ソ鏈変粈涔堣兘甯姪浣犵殑"}]', + ) + # 娑堟伅鐘舵�� is_processed = Column(Boolean, default=False, comment="鏄惁宸插鐞�") is_sent = Column(Boolean, default=False, comment="鏄惁宸插彂閫佸洖澶�") - + # 鏃堕棿鎴� question_time = Column(DateTime, default=func.now(), comment="鎻愰棶鏃堕棿") answer_time = Column(DateTime, nullable=True, comment="鍥炵瓟鏃堕棿") sent_time = Column(DateTime, nullable=True, comment="鍙戦�佹椂闂�") - + created_at = Column(DateTime, default=func.now(), comment="鍒涘缓鏃堕棿") - updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="鏇存柊鏃堕棿") - + updated_at = Column( + DateTime, default=func.now(), onupdate=func.now(), comment="鏇存柊鏃堕棿" + ) + + # 娣诲姞鍥涘瓧娈电粍鍚堢殑鍞竴鎬х害鏉� + __table_args__ = ( + UniqueConstraint('from_user', 'conversation_id', 'group', 'hour', + name='uq_conversation_key'), + ) + def __repr__(self): - return f"<Conversation(from_user='{self.from_user}', conversation_id='{self.conversation_id}')>" + return f"<Conversation(from_user='{self.from_user}', conversation_id='{self.conversation_id}', group='{self.group}', hour='{self.hour}')>" diff --git a/app/services/contact_sync.py b/app/services/contact_sync.py new file mode 100644 index 0000000..42d4acd --- /dev/null +++ b/app/services/contact_sync.py @@ -0,0 +1,167 @@ +""" +鑱旂郴浜哄悓姝ユ湇鍔� +""" + +from typing import List, Optional +from loguru import logger +from sqlalchemy.orm import Session +from app.models.contact import Contact +from app.models.database import get_db +from app.services.ecloud_client import ecloud_client + + +class ContactSyncService: + """鑱旂郴浜哄悓姝ユ湇鍔�""" + + def __init__(self): + pass + + def sync_contacts_on_startup(self, w_id: str) -> bool: + """ + 鍚姩鏃跺悓姝ヨ仈绯讳汉淇℃伅 + + Args: + w_id: 鐧诲綍瀹炰緥鏍囪瘑 + + Returns: + 鍚屾鎴愬姛杩斿洖True锛屽け璐ヨ繑鍥濬alse + """ + try: + logger.info(f"寮�濮嬪悓姝ヨ仈绯讳汉淇℃伅: wId={w_id}") + + # 1. 鍒濆鍖栭�氳褰曞垪琛� + if not ecloud_client.init_address_list(w_id): + logger.error(f"鍒濆鍖栭�氳褰曞垪琛ㄥけ璐�: wId={w_id}") + return False + + # 2. 鑾峰彇閫氳褰曞垪琛� + address_data = ecloud_client.get_address_list(w_id) + if not address_data: + logger.error(f"鑾峰彇閫氳褰曞垪琛ㄥけ璐�: wId={w_id}") + return False + + # 3. 鑾峰彇濂藉弸鍒楄〃涓殑wcid + friends = address_data.get("friends", []) + if not friends: + logger.warning(f"濂藉弸鍒楄〃涓虹┖: wId={w_id}") + return True + + logger.info(f"鑾峰彇鍒板ソ鍙嬪垪琛�: wId={w_id}, count={len(friends)}") + + # 4. 鎵归噺鑾峰彇鑱旂郴浜鸿缁嗕俊鎭� + return self._batch_sync_contacts(w_id, friends) + + except Exception as e: + logger.error(f"鍚屾鑱旂郴浜轰俊鎭紓甯�: wId={w_id}, error={str(e)}") + return False + + def _batch_sync_contacts(self, w_id: str, wc_ids: List[str]) -> bool: + """ + 鎵归噺鍚屾鑱旂郴浜轰俊鎭� + + Args: + w_id: 鐧诲綍瀹炰緥鏍囪瘑 + wc_ids: 寰俊ID鍒楄〃 + + Returns: + 鍚屾鎴愬姛杩斿洖True锛屽け璐ヨ繑鍥濬alse + """ + try: + # 灏唚cid鍒楄〃鐢ㄩ�楀彿鎷兼帴 + wc_ids_str = ",".join(wc_ids) + logger.info(f"寮�濮嬫壒閲忚幏鍙栬仈绯讳汉淇℃伅: wId={w_id}, wc_ids_count={len(wc_ids)}") + + # 璋冪敤鑾峰彇鑱旂郴浜轰俊鎭帴鍙� + contact_info = ecloud_client.get_contact_info(w_id, wc_ids_str) + if not contact_info: + logger.error(f"鎵归噺鑾峰彇鑱旂郴浜轰俊鎭け璐�: wId={w_id}") + return False + + # 濡傛灉杩斿洖鐨勬槸鍗曚釜鑱旂郴浜轰俊鎭紝杞崲涓哄垪琛� + if isinstance(contact_info, dict): + contact_list = [contact_info] + else: + contact_list = contact_info + + logger.info(f"鑾峰彇鍒拌仈绯讳汉璇︾粏淇℃伅: count={len(contact_list)}") + + # 淇濆瓨鍒版暟鎹簱 + return self._save_contacts_to_db(contact_list) + + except Exception as e: + logger.error(f"鎵归噺鍚屾鑱旂郴浜轰俊鎭紓甯�: wId={w_id}, error={str(e)}") + return False + + def _save_contacts_to_db(self, contact_list: List[dict]) -> bool: + """ + 淇濆瓨鑱旂郴浜轰俊鎭埌鏁版嵁搴� + + Args: + contact_list: 鑱旂郴浜轰俊鎭垪琛� + + Returns: + 淇濆瓨鎴愬姛杩斿洖True锛屽け璐ヨ繑鍥濬alse + """ + try: + with next(get_db()) as db: + saved_count = 0 + updated_count = 0 + + for contact_data in contact_list: + wc_id = contact_data.get("userName") or contact_data.get("wcId") + if not wc_id: + logger.warning(f"鑱旂郴浜轰俊鎭己灏憌cId: {contact_data}") + continue + + # 妫�鏌ユ槸鍚﹀凡瀛樺湪 + existing_contact = db.query(Contact).filter(Contact.wc_id == wc_id).first() + + if existing_contact: + # 鏇存柊鐜版湁鑱旂郴浜轰俊鎭� + existing_contact.user_name = contact_data.get("userName") + existing_contact.nick_name = contact_data.get("nickName") + existing_contact.remark = contact_data.get("remark") + existing_contact.signature = contact_data.get("signature") + existing_contact.sex = contact_data.get("sex") + existing_contact.alias_name = contact_data.get("aliasName") + existing_contact.country = contact_data.get("country") + existing_contact.big_head = contact_data.get("bigHead") + existing_contact.small_head = contact_data.get("smallHead") + existing_contact.label_list = contact_data.get("labelList") + existing_contact.v1 = contact_data.get("v1") + updated_count += 1 + logger.debug(f"鏇存柊鑱旂郴浜轰俊鎭�: wc_id={wc_id}, nick_name={contact_data.get('nickName')}") + else: + # 鍒涘缓鏂拌仈绯讳汉璁板綍 + new_contact = Contact( + wc_id=wc_id, + user_name=contact_data.get("userName"), + nick_name=contact_data.get("nickName"), + remark=contact_data.get("remark"), + signature=contact_data.get("signature"), + sex=contact_data.get("sex"), + alias_name=contact_data.get("aliasName"), + country=contact_data.get("country"), + big_head=contact_data.get("bigHead"), + small_head=contact_data.get("smallHead"), + label_list=contact_data.get("labelList"), + v1=contact_data.get("v1"), + ) + db.add(new_contact) + saved_count += 1 + logger.debug(f"鏂板鑱旂郴浜轰俊鎭�: wc_id={wc_id}, nick_name={contact_data.get('nickName')}") + + # 鎻愪氦浜嬪姟 + db.commit() + logger.info(f"鑱旂郴浜轰俊鎭繚瀛樺畬鎴�: 鏂板={saved_count}, 鏇存柊={updated_count}") + return True + + except Exception as e: + logger.error(f"淇濆瓨鑱旂郴浜轰俊鎭埌鏁版嵁搴撳紓甯�: error={str(e)}") + if 'db' in locals(): + db.rollback() + return False + + +# 鍏ㄥ眬鑱旂郴浜哄悓姝ユ湇鍔″疄渚� +contact_sync_service = ContactSyncService() diff --git a/app/services/ecloud_client.py b/app/services/ecloud_client.py index 5a76116..42495c3 100644 --- a/app/services/ecloud_client.py +++ b/app/services/ecloud_client.py @@ -3,7 +3,7 @@ """ import requests -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, List from loguru import logger from config import settings @@ -26,10 +26,12 @@ Args: w_id: 鐧诲綍瀹炰緥鏍囪瘑 - wc_id: 濂藉弸寰俊id/缇d + wc_id: 濂藉弸寰俊id/缇d锛屽涓娇鐢ㄨ嫳鏂囬�楀彿鍒嗛殧 Returns: - 鑱旂郴浜轰俊鎭瓧鍏革紝澶辫触杩斿洖None + 鑱旂郴浜轰俊鎭瓧鍏告垨鍒楄〃锛屽け璐ヨ繑鍥濶one + - 鍗曚釜wcId鏃惰繑鍥炲瓧鍏� + - 澶氫釜wcId鏃惰繑鍥炲垪琛� """ try: url = f"{self.base_url}/getContact" @@ -45,8 +47,14 @@ if result.get("code") == "1000": contact_data = result.get("data", []) if contact_data and len(contact_data) > 0: - logger.info(f"鎴愬姛鑾峰彇鑱旂郴浜轰俊鎭�: wcId={wc_id}") - return contact_data[0] # 杩斿洖绗竴涓仈绯讳汉淇℃伅 + # 濡傛灉鏄崟涓獁cId锛岃繑鍥炵涓�涓仈绯讳汉淇℃伅 + if "," not in wc_id: + logger.info(f"鎴愬姛鑾峰彇鑱旂郴浜轰俊鎭�: wcId={wc_id}") + return contact_data[0] + else: + # 濡傛灉鏄涓獁cId锛岃繑鍥炲畬鏁村垪琛� + logger.info(f"鎴愬姛鑾峰彇鎵归噺鑱旂郴浜轰俊鎭�: count={len(contact_data)}") + return contact_data else: logger.warning(f"鑱旂郴浜轰俊鎭负绌�: wcId={wc_id}") return None @@ -63,7 +71,7 @@ logger.error(f"鑾峰彇鑱旂郴浜轰俊鎭紓甯�: wcId={wc_id}, error={str(e)}") return None - def send_text_message(self, w_id: str, wc_id: str, content: str) -> bool: + def send_text_message(self, w_id: str, wc_id: str, content: str, max_retries: int = None) -> bool: """ 鍙戦�佹枃鏈秷鎭� @@ -71,38 +79,55 @@ w_id: 鐧诲綍瀹炰緥鏍囪瘑 wc_id: 鎺ユ敹浜哄井淇d/缇d content: 鏂囨湰鍐呭娑堟伅 + max_retries: 鏈�澶ч噸璇曟鏁� Returns: 鍙戦�佹垚鍔熻繑鍥濼rue锛屽け璐ヨ繑鍥濬alse """ - try: - url = f"{self.base_url}/sendText" - payload = {"wId": w_id, "wcId": wc_id, "content": content} + if max_retries is None: + from config import settings + max_retries = settings.max_retry_count + + retry_count = 0 + while retry_count <= max_retries: + try: + url = f"{self.base_url}/sendText" + payload = {"wId": w_id, "wcId": wc_id, "content": content} - logger.info( - f"鍙戦�佹枃鏈秷鎭�: wId={w_id}, wcId={wc_id}, content_length={len(content)}" - ) - - response = self.session.post(url, json=payload, timeout=30) - response.raise_for_status() - - result = response.json() - - if result.get("code") == "1000": - logger.info(f"鏂囨湰娑堟伅鍙戦�佹垚鍔�: wcId={wc_id}") - return True - else: - logger.error( - f"鏂囨湰娑堟伅鍙戦�佸け璐�: wcId={wc_id}, code={result.get('code')}, message={result.get('message')}" + logger.info( + f"鍙戦�佹枃鏈秷鎭�: wId={w_id}, wcId={wc_id}, content_length={len(content)}, retry={retry_count}" ) - return False - except requests.exceptions.RequestException as e: - logger.error(f"鍙戦�佹枃鏈秷鎭綉缁滈敊璇�: wcId={wc_id}, error={str(e)}") - return False - except Exception as e: - logger.error(f"鍙戦�佹枃鏈秷鎭紓甯�: wcId={wc_id}, error={str(e)}") - return False + response = self.session.post(url, json=payload, timeout=30) + response.raise_for_status() + + result = response.json() + + if result.get("code") == "1000": + logger.info(f"鏂囨湰娑堟伅鍙戦�佹垚鍔�: wcId={wc_id}") + return True + else: + logger.error( + f"鏂囨湰娑堟伅鍙戦�佸け璐�: wcId={wc_id}, code={result.get('code')}, message={result.get('message')}" + ) + + except requests.exceptions.RequestException as e: + logger.error(f"鍙戦�佹枃鏈秷鎭綉缁滈敊璇�: wcId={wc_id}, retry={retry_count}, error={str(e)}") + except Exception as e: + logger.error(f"鍙戦�佹枃鏈秷鎭紓甯�: wcId={wc_id}, retry={retry_count}, error={str(e)}") + + retry_count += 1 + if retry_count <= max_retries: + from config import settings + wait_time = settings.retry_delay * retry_count + logger.info(f"绛夊緟閲嶈瘯: wcId={wc_id}, wait_time={wait_time}s") + import time + time.sleep(wait_time) + + logger.error( + f"鏂囨湰娑堟伅鍙戦�佸け璐ワ紝宸茶揪鏈�澶ч噸璇曟鏁�: wcId={wc_id}, max_retries={max_retries}" + ) + return False def send_group_message(self, w_id: str, group_id: str, content: str) -> bool: """ @@ -118,6 +143,153 @@ """ return self.send_text_message(w_id, group_id, content) + def init_address_list(self, w_id: str) -> bool: + """ + 鍒濆鍖栭�氳褰曞垪琛� + + Args: + w_id: 鐧诲綍瀹炰緥鏍囪瘑 + + Returns: + 鍒濆鍖栨垚鍔熻繑鍥濼rue锛屽け璐ヨ繑鍥濬alse + """ + try: + url = f"{self.base_url}/initAddressList" + payload = {"wId": w_id} + + logger.info(f"鍒濆鍖栭�氳褰曞垪琛�: wId={w_id}") + + response = self.session.post(url, json=payload, timeout=30) + response.raise_for_status() + + result = response.json() + + if result.get("code") == "1000": + logger.info(f"鍒濆鍖栭�氳褰曞垪琛ㄦ垚鍔�: wId={w_id}") + return True + else: + logger.error( + f"鍒濆鍖栭�氳褰曞垪琛ㄥけ璐�: wId={w_id}, code={result.get('code')}, message={result.get('message')}" + ) + return False + + except requests.exceptions.RequestException as e: + logger.error(f"鍒濆鍖栭�氳褰曞垪琛ㄧ綉缁滈敊璇�: wId={w_id}, error={str(e)}") + return False + except Exception as e: + logger.error(f"鍒濆鍖栭�氳褰曞垪琛ㄥ紓甯�: wId={w_id}, error={str(e)}") + return False + + def get_address_list(self, w_id: str) -> Optional[Dict[str, Any]]: + """ + 鑾峰彇閫氳褰曞垪琛� + + Args: + w_id: 鐧诲綍瀹炰緥鏍囪瘑 + + Returns: + 閫氳褰曟暟鎹瓧鍏革紝澶辫触杩斿洖None + 杩斿洖鏍煎紡: { + "chatrooms": [...], # 缇ょ粍鍒楄〃 + "friends": [...], # 濂藉弸鍒楄〃 + "ghs": [...], # 鍏紬鍙峰垪琛� + "others": [...] # 鍏朵粬 + } + """ + try: + url = f"{self.base_url}/getAddressList" + payload = {"wId": w_id} + + logger.info(f"鑾峰彇閫氳褰曞垪琛�: wId={w_id}") + + response = self.session.post(url, json=payload, timeout=30) + response.raise_for_status() + + result = response.json() + + if result.get("code") == "1000": + address_data = result.get("data", {}) + logger.info(f"鎴愬姛鑾峰彇閫氳褰曞垪琛�: wId={w_id}, friends_count={len(address_data.get('friends', []))}") + return address_data + else: + logger.error( + f"鑾峰彇閫氳褰曞垪琛ㄥけ璐�: wId={w_id}, code={result.get('code')}, message={result.get('message')}" + ) + return None + + except requests.exceptions.RequestException as e: + logger.error(f"鑾峰彇閫氳褰曞垪琛ㄧ綉缁滈敊璇�: wId={w_id}, error={str(e)}") + return None + except Exception as e: + logger.error(f"鑾峰彇閫氳褰曞垪琛ㄥ紓甯�: wId={w_id}, error={str(e)}") + return None + + def send_group_at_message(self, w_id: str, wc_id: str, content: str, at_wc_ids: List[str], max_retries: int = None) -> bool: + """ + 鍙戦�佺兢鑱夽娑堟伅 + + Args: + w_id: 鐧诲綍瀹炰緥鏍囪瘑 + wc_id: 鎺ユ敹鏂圭兢id + content: 鏂囨湰鍐呭娑堟伅锛園鐨勫井淇℃樀绉伴渶瑕佽嚜宸辨嫾鎺ワ紝蹇呴』鎷兼帴鑹剧壒绗﹀彿锛屼笉鐒朵笉鐢熸晥锛� + at_wc_ids: 鑹剧壒鐨勫井淇d鍒楄〃 + max_retries: 鏈�澶ч噸璇曟鏁� + + Returns: + 鍙戦�佹垚鍔熻繑鍥濼rue锛屽け璐ヨ繑鍥濬alse + """ + if max_retries is None: + from config import settings + max_retries = settings.max_retry_count + + retry_count = 0 + while retry_count <= max_retries: + try: + url = f"{self.base_url}/sendText" + # 灏哸t_wc_ids鍒楄〃鐢ㄩ�楀彿鎷兼帴 + at_str = ",".join(at_wc_ids) + payload = { + "wId": w_id, + "wcId": wc_id, + "content": content, + "at": at_str + } + + logger.info( + f"鍙戦�佺兢鑱夽娑堟伅: wId={w_id}, wcId={wc_id}, at={at_str}, content_length={len(content)}, retry={retry_count}" + ) + + response = self.session.post(url, json=payload, timeout=30) + response.raise_for_status() + + result = response.json() + + if result.get("code") == "1000": + logger.info(f"缇よ亰@娑堟伅鍙戦�佹垚鍔�: wcId={wc_id}, at={at_str}") + return True + else: + logger.error( + f"缇よ亰@娑堟伅鍙戦�佸け璐�: wcId={wc_id}, at={at_str}, code={result.get('code')}, message={result.get('message')}" + ) + + except requests.exceptions.RequestException as e: + logger.error(f"鍙戦�佺兢鑱夽娑堟伅缃戠粶閿欒: wcId={wc_id}, at={at_str}, retry={retry_count}, error={str(e)}") + except Exception as e: + logger.error(f"鍙戦�佺兢鑱夽娑堟伅寮傚父: wcId={wc_id}, at={at_str}, retry={retry_count}, error={str(e)}") + + retry_count += 1 + if retry_count <= max_retries: + from config import settings + wait_time = settings.retry_delay * retry_count + logger.info(f"绛夊緟閲嶈瘯: wcId={wc_id}, wait_time={wait_time}s") + import time + time.sleep(wait_time) + + logger.error( + f"缇よ亰@娑堟伅鍙戦�佸け璐ワ紝宸茶揪鏈�澶ч噸璇曟鏁�: wcId={wc_id}, at={at_str}, max_retries={max_retries}" + ) + return False + # 鍏ㄥ眬E浜戠瀹跺鎴风瀹炰緥 ecloud_client = ECloudClient() diff --git a/app/services/message_processor.py b/app/services/message_processor.py index 11308fc..12aaea2 100644 --- a/app/services/message_processor.py +++ b/app/services/message_processor.py @@ -3,7 +3,10 @@ """ import json -from typing import Dict, Any, Optional +import time +import re +from typing import Dict, Any, Optional, List, Tuple +from datetime import datetime from sqlalchemy.orm import Session from loguru import logger @@ -13,6 +16,7 @@ from app.services.redis_queue import redis_queue from app.services.ecloud_client import ecloud_client from app.services.dify_client import dify_client +from config import settings class MessageProcessor: @@ -20,6 +24,51 @@ def __init__(self): pass + + def parse_at_mentions(self, ai_answer: str) -> Tuple[str, List[str]]: + """ + 瑙f瀽AI鍥炲涓殑@瀛楃锛屾彁鍙栧鏈嶅悕绉� + + Args: + ai_answer: AI鍥炲鍐呭 + + Returns: + (澶勭悊鍚庣殑娑堟伅鍐呭, 闇�瑕丂鐨勫鏈峸cid鍒楄〃) + """ + try: + # 鑾峰彇閰嶇疆鐨勫鏈嶅悕绉板垪琛� + customer_service_names = settings.customer_service_names + + # 鏌ユ壘鎵�鏈堾瀛楃鍚庣殑瀹㈡湇鍚嶇О + at_pattern = r'@([^\s]+)' + matches = re.findall(at_pattern, ai_answer) + + valid_at_names = [] + at_wc_ids = [] + + for match in matches: + # 妫�鏌ユ槸鍚﹀湪閰嶇疆鐨勫鏈嶅悕绉板垪琛ㄤ腑 + if match in customer_service_names: + valid_at_names.append(match) + logger.info(f"鍙戠幇鏈夋晥鐨凘瀹㈡湇鍚嶇О: {match}") + + # 濡傛灉鏈夋湁鏁堢殑@瀹㈡湇鍚嶇О锛屾煡璇㈡暟鎹簱鑾峰彇wcid + if valid_at_names: + with next(get_db()) as db: + for name in valid_at_names: + # 鏍规嵁nick_name鏌ユ壘鑱旂郴浜� + contact = db.query(Contact).filter(Contact.nick_name == name).first() + if contact: + at_wc_ids.append(contact.wc_id) + logger.info(f"鎵惧埌瀹㈡湇鑱旂郴浜�: name={name}, wc_id={contact.wc_id}") + else: + logger.warning(f"鏈壘鍒板鏈嶈仈绯讳汉: name={name}") + + return ai_answer, at_wc_ids + + except Exception as e: + logger.error(f"瑙f瀽@瀛楃寮傚父: error={str(e)}") + return ai_answer, [] def is_valid_group_message(self, callback_data: Dict[str, Any]) -> bool: """ @@ -151,17 +200,15 @@ logger.info(f"寮�濮嬪鐞嗘秷鎭�: from_user={from_user}, from_group={from_group}") - # 鑾峰彇鏁版嵁搴撲細璇� - db = next(get_db()) - - try: + # 浣跨敤涓婁笅鏂囩鐞嗗櫒纭繚鏁版嵁搴撲細璇濇纭鐞� + with next(get_db()) as db: # 3.1 纭繚鑱旂郴浜轰俊鎭瓨鍦� if not self.ensure_contact_exists(from_group, w_id, db): logger.error(f"鑱旂郴浜轰俊鎭鐞嗗け璐�: from_group={from_group}") return False - # 3.2 鑾峰彇鐢ㄦ埛鐨刢onversation_id - conversation_id = redis_queue.get_conversation_id(from_user) + # 3.2 鑾峰彇鐢ㄦ埛鍦ㄥ綋鍓嶇兢缁勭殑conversation_id + conversation_id = redis_queue.get_conversation_id(from_user, from_group) # 璋冪敤Dify鎺ュ彛鍙戦�佹秷鎭� dify_response = dify_client.send_chat_message( @@ -176,55 +223,112 @@ ai_answer = dify_response.get("answer", "") new_conversation_id = dify_response.get("conversation_id", "") - # 鏇存柊Redis涓殑conversation_id + # 鏇存柊Redis涓殑conversation_id锛堝熀浜庣敤鎴�+缇ょ粍锛� if new_conversation_id: - redis_queue.set_conversation_id(from_user, new_conversation_id) + redis_queue.set_conversation_id(from_user, new_conversation_id, from_group) # 3.3 淇濆瓨瀵硅瘽璁板綍鍒版暟鎹簱 - user_conversation_key = f"{from_user}_{new_conversation_id}" + # 鎸夌敤鎴枫�佺兢缁勫拰灏忔椂鍒嗙粍瀵硅瘽璁板綍 + current_time = datetime.now() + hour_key = current_time.strftime("%Y%m%d_%H") - # 妫�鏌ユ槸鍚﹀凡瀛樺湪璁板綍 + # 鏌ユ壘褰撳墠鐢ㄦ埛鍦ㄥ綋鍓嶇兢缁勫綋鍓嶅皬鏃剁殑瀵硅瘽璁板綍 existing_conversation = ( db.query(Conversation) - .filter(Conversation.user_conversation_key == user_conversation_key) + .filter( + Conversation.from_user == from_user, + Conversation.conversation_id == new_conversation_id, + Conversation.group == from_group, + Conversation.hour == hour_key + ) .first() ) if existing_conversation: - # 鏇存柊鐜版湁璁板綍 - existing_conversation.user_question = content - existing_conversation.ai_answer = ai_answer - existing_conversation.is_processed = True - logger.info(f"鏇存柊瀵硅瘽璁板綍: key={user_conversation_key}") + # 鏇存柊鐜版湁璁板綍 - 浣跨敤JSON鏍煎紡杩藉姞瀵硅瘽鍐呭锛堝綋鍓嶇敤鎴峰湪褰撳墠缇ょ粍褰撳墠灏忔椂鐨勫璇濓級 + try: + # 瑙f瀽鐜版湁鐨刢ontent JSON + if existing_conversation.content: + content_list = json.loads(existing_conversation.content) + else: + content_list = [] + + # 杩藉姞鏂扮殑瀵硅瘽鍐呭 + content_list.append({ + "user": content, + "ai": ai_answer + }) + + # 鏇存柊璁板綍 + existing_conversation.content = json.dumps(content_list, ensure_ascii=False) + existing_conversation.is_processed = True + logger.info(f"杩藉姞鍒板綋鍓嶇敤鎴风兢缁勫皬鏃跺璇濊褰�: user={from_user}, group={from_group}, hour={hour_key}, 瀵硅瘽杞={len(content_list)}") + except json.JSONDecodeError as e: + logger.error(f"瑙f瀽鐜版湁瀵硅瘽鍐呭JSON澶辫触: {str(e)}, 閲嶆柊鍒涘缓") + # 濡傛灉JSON瑙f瀽澶辫触锛岄噸鏂板垱寤篶ontent + content_list = [{"user": content, "ai": ai_answer}] + existing_conversation.content = json.dumps(content_list, ensure_ascii=False) + existing_conversation.is_processed = True else: - # 鍒涘缓鏂拌褰� + # 鍒涘缓鏂拌褰� - 鏂扮殑鐢ㄦ埛缇ょ粍灏忔椂瀵硅瘽鎴栭娆″璇濓紝浣跨敤JSON鏍煎紡瀛樺偍瀵硅瘽鍐呭 + content_list = [{"user": content, "ai": ai_answer}] new_conversation = Conversation( - user_conversation_key=user_conversation_key, from_user=from_user, conversation_id=new_conversation_id, - user_question=content, - ai_answer=ai_answer, + group=from_group, + hour=hour_key, + content=json.dumps(content_list, ensure_ascii=False), is_processed=True, ) db.add(new_conversation) - logger.info(f"鍒涘缓瀵硅瘽璁板綍: key={user_conversation_key}") + logger.info(f"鍒涘缓鏂扮殑鐢ㄦ埛缇ょ粍灏忔椂瀵硅瘽璁板綍: user={from_user}, group={from_group}, hour={hour_key}, 鍒濆瀵硅瘽杞=1") db.commit() # 鍙戦�丄I鍥炵瓟鍒扮兢鑱� - if ai_answer and ecloud_client.send_group_message( - w_id, from_group, ai_answer - ): + success = False + if ai_answer: + # 瑙f瀽AI鍥炲涓殑@瀛楃 + processed_answer, at_wc_ids = self.parse_at_mentions(ai_answer) + + # 鍙戦�佹秷鎭紝鏈�澶氶噸璇�3娆� + for attempt in range(3): + if at_wc_ids: + # 濡傛灉鏈堾瀹㈡湇锛屼娇鐢ㄧ兢鑱夽鎺ュ彛 + logger.info(f"浣跨敤缇よ亰@鎺ュ彛鍙戦�佹秷鎭�: at_wc_ids={at_wc_ids}") + if ecloud_client.send_group_at_message( + w_id, from_group, processed_answer, at_wc_ids + ): + success = True + break + else: + # 鏅�氱兢鑱婃秷鎭� + logger.info("浣跨敤鏅�氱兢鑱婃帴鍙e彂閫佹秷鎭�") + if ecloud_client.send_group_message( + w_id, from_group, processed_answer + ): + success = True + break + + logger.warning(f"鍙戦�丄I鍥炵瓟澶辫触锛屽皾璇曢噸璇� ({attempt + 1}/3): from_user={from_user}") + if attempt < 2: # 涓嶆槸鏈�鍚庝竴娆″皾璇曪紝绛夊緟涓�娈垫椂闂村啀閲嶈瘯 + time.sleep(2 ** attempt) # 鎸囨暟閫�閬� + + if success: # 鏇存柊鍙戦�佺姸鎬� conversation = ( db.query(Conversation) .filter( - Conversation.user_conversation_key == user_conversation_key + Conversation.from_user == from_user, + Conversation.conversation_id == new_conversation_id, + Conversation.group == from_group, + Conversation.hour == hour_key ) .first() ) if conversation: conversation.is_sent = True + conversation.sent_time = current_time db.commit() logger.info(f"娑堟伅澶勭悊瀹屾垚: from_user={from_user}") @@ -232,9 +336,6 @@ else: logger.error(f"鍙戦�丄I鍥炵瓟澶辫触: from_user={from_user}") return False - - finally: - db.close() except Exception as e: logger.error(f"澶勭悊娑堟伅寮傚父: from_user={from_user}, error={str(e)}") diff --git a/app/services/redis_queue.py b/app/services/redis_queue.py index 792bcc5..54af4a4 100644 --- a/app/services/redis_queue.py +++ b/app/services/redis_queue.py @@ -26,9 +26,14 @@ """鑾峰彇澶勭悊鐘舵�侀敭""" return f"{self.processing_prefix}{from_user}" - def get_conversation_key(self, from_user: str) -> str: + def get_conversation_key(self, from_user: str, group: str = None) -> str: """鑾峰彇鐢ㄦ埛瀵硅瘽ID閿�""" - return f"{self.conversation_prefix}{from_user}" + if group: + # 鍩轰簬鐢ㄦ埛+缇ょ粍鐢熸垚conversation_key锛岀‘淇濅笉鍚岀兢缁勭殑瀵硅瘽鐙珛 + return f"{self.conversation_prefix}{from_user}:{group}" + else: + # 鍏煎鏃х増鏈紝浠呭熀浜庣敤鎴� + return f"{self.conversation_prefix}{from_user}" def enqueue_message(self, from_user: str, message_data: Dict[str, Any]) -> bool: """灏嗘秷鎭姞鍏ョ敤鎴烽槦鍒�""" @@ -36,8 +41,8 @@ queue_key = self.get_user_queue_key(from_user) message_json = json.dumps(message_data, ensure_ascii=False) - # 浣跨敤LPUSH灏嗘秷鎭姞鍏ラ槦鍒楀ご閮� - result = self.redis_client.lpush(queue_key, message_json) + # 浣跨敤RPUSH灏嗘秷鎭姞鍏ラ槦鍒楀熬閮紝纭繚FIFO椤哄簭 + result = self.redis_client.rpush(queue_key, message_json) logger.info(f"娑堟伅宸插姞鍏ラ槦鍒�: user={from_user}, queue_length={result}, message={message_json}") return True @@ -107,28 +112,28 @@ logger.error(f"妫�鏌ュ鐞嗙姸鎬佸け璐�: user={from_user}, error={str(e)}") return False - def get_conversation_id(self, from_user: str) -> Optional[str]: + def get_conversation_id(self, from_user: str, group: str = None) -> Optional[str]: """鑾峰彇鐢ㄦ埛鐨勫璇滻D""" try: - conversation_key = self.get_conversation_key(from_user) + conversation_key = self.get_conversation_key(from_user, group) return self.redis_client.get(conversation_key) except Exception as e: - logger.error(f"鑾峰彇瀵硅瘽ID澶辫触: user={from_user}, error={str(e)}") + logger.error(f"鑾峰彇瀵硅瘽ID澶辫触: user={from_user}, group={group}, error={str(e)}") return None def set_conversation_id( - self, from_user: str, conversation_id: str, ttl: int = 86400 + self, from_user: str, conversation_id: str, group: str = None, ttl: int = 86400 ) -> bool: """璁剧疆鐢ㄦ埛鐨勫璇滻D""" try: - conversation_key = self.get_conversation_key(from_user) + conversation_key = self.get_conversation_key(from_user, group) self.redis_client.setex(conversation_key, ttl, conversation_id) logger.info( - f"璁剧疆瀵硅瘽ID: user={from_user}, conversation_id={conversation_id}" + f"璁剧疆瀵硅瘽ID: user={from_user}, group={group}, conversation_id={conversation_id}" ) return True except Exception as e: - logger.error(f"璁剧疆瀵硅瘽ID澶辫触: user={from_user}, error={str(e)}") + logger.error(f"璁剧疆瀵硅瘽ID澶辫触: user={from_user}, group={group}, error={str(e)}") return False def get_queue_length(self, from_user: str) -> int: diff --git a/app/utils/logger.py b/app/utils/logger.py index 16ad222..ef16c81 100644 --- a/app/utils/logger.py +++ b/app/utils/logger.py @@ -28,11 +28,11 @@ level=settings.log_level, format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", rotation="1 day", - retention="30 days", + retention="7 days", compression="zip", encoding="utf-8" ) - + # 娣诲姞閿欒鏃ュ織鏂囦欢 error_log_file = settings.log_file.replace('.log', '_error.log') logger.add( @@ -40,7 +40,7 @@ level="ERROR", format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", rotation="1 day", - retention="30 days", + retention="7 days", compression="zip", encoding="utf-8" ) diff --git a/app/workers/message_worker.py b/app/workers/message_worker.py index 21a28a3..bb06b8d 100644 --- a/app/workers/message_worker.py +++ b/app/workers/message_worker.py @@ -47,10 +47,34 @@ """鐩戞帶闃熷垪锛屼负鏈夋秷鎭殑鐢ㄦ埛鍚姩澶勭悊绾跨▼""" while self.running: try: - # 杩欓噷鍙互瀹炵幇鏇村鏉傜殑闃熷垪鍙戠幇鏈哄埗 - # 鐩墠绠�鍖栦负妫�鏌ュ凡鐭ョ殑娲昏穬鐢ㄦ埛 - # 鍦ㄥ疄闄呭簲鐢ㄤ腑锛屽彲浠ラ�氳繃Redis鐨凷CAN鍛戒护鎵弿鎵�鏈夐槦鍒� - + # 浣跨敤Redis鐨凷CAN鍛戒护鎵弿鎵�鏈夐槦鍒楅敭 + cursor = 0 + queue_keys = set() + + while self.running: + cursor, keys = self._scan_queue_keys(cursor) + queue_keys.update(keys) + + # 濡傛灉娓告爣涓�0锛岃〃绀烘壂鎻忓畬鎴� + if cursor == 0: + break + + # 涓烘瘡涓湁娑堟伅鐨勯槦鍒楀惎鍔ㄥ鐞嗙嚎绋� + for queue_key in queue_keys: + if not self.running: + break + + # 浠庨槦鍒楅敭涓彁鍙栫敤鎴稩D + if queue_key.startswith(redis_queue.queue_prefix): + from_user = queue_key[len(redis_queue.queue_prefix):] + + # 妫�鏌ラ槦鍒楁槸鍚︽湁娑堟伅 + queue_length = redis_queue.get_queue_length(from_user) + if queue_length > 0: + logger.info(f"鍙戠幇鐢ㄦ埛闃熷垪鏈夋秷鎭�: user={from_user}, length={queue_length}") + # 鍚姩鐢ㄦ埛闃熷垪澶勭悊 + self.process_user_queue(from_user) + # 娓呯悊宸插畬鎴愮殑绾跨▼ self._cleanup_finished_threads() @@ -60,6 +84,29 @@ except Exception as e: logger.error(f"闃熷垪鐩戞帶寮傚父: {str(e)}") time.sleep(10) + + def _scan_queue_keys(self, cursor: int = 0, count: int = 100) -> tuple: + """ + 鎵弿闃熷垪閿� + + Args: + cursor: 鎵弿娓告爣 + count: 姣忔鎵弿鐨勯敭鏁伴噺 + + Returns: + (鏂版父鏍�, 闃熷垪閿垪琛�) + """ + try: + # 浣跨敤SCAN鍛戒护鎵弿鍖归厤妯″紡鐨勯敭 + new_cursor, keys = redis_queue.redis_client.scan( + cursor=cursor, + match=f"{redis_queue.queue_prefix}*", + count=count + ) + return new_cursor, keys + except Exception as e: + logger.error(f"鎵弿闃熷垪閿け璐�: {str(e)}") + return 0, [] def _cleanup_finished_threads(self): """娓呯悊宸插畬鎴愮殑绾跨▼""" @@ -102,6 +149,10 @@ """ try: logger.info(f"寮�濮嬪鐞嗙敤鎴锋秷鎭槦鍒�: {from_user}") + + # 鑾峰彇闃熷垪闀垮害 + queue_length = redis_queue.get_queue_length(from_user) + logger.info(f"鐢ㄦ埛闃熷垪鍒濆闀垮害: {from_user}, length={queue_length}") while self.running: # 妫�鏌ョ敤鎴锋槸鍚︽鍦ㄥ鐞嗕腑锛堥槻姝㈠苟鍙戯級 @@ -132,6 +183,9 @@ if success: logger.info(f"娑堟伅澶勭悊鎴愬姛: {from_user}") + # 鏇存柊闃熷垪闀垮害淇℃伅 + queue_length = redis_queue.get_queue_length(from_user) + logger.info(f"鐢ㄦ埛闃熷垪鍓╀綑闀垮害: {from_user}, length={queue_length}") else: logger.error(f"娑堟伅澶勭悊澶辫触: {from_user}") # 鍙互鑰冭檻灏嗗け璐ョ殑娑堟伅閲嶆柊鍏ラ槦鎴栬褰曞埌閿欒闃熷垪 diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..b2df84d --- /dev/null +++ b/config.example.json @@ -0,0 +1,34 @@ +{ + "database": { + "url": "mysql+pymysql://username:password@host:port/database_name" + }, + "redis": { + "url": "redis://localhost:6379/0" + }, + "ecloud": { + "base_url": "http://your-ecloud-server:port", + "authorization": "your_ecloud_authorization_token", + "w_id": "your_ecloud_w_id" + }, + "dify": { + "base_url": "https://api.dify.ai/v1", + "api_key": "your_dify_api_key" + }, + "server": { + "host": "0.0.0.0", + "port": 7979, + "debug": false + }, + "logging": { + "level": "INFO", + "file": "logs/app.log" + }, + "message_processing": { + "max_retry_count": 3, + "retry_delay": 5, + "queue_timeout": 300 + }, + "customer_service": { + "names": ["瀹㈡湇1", "瀹㈡湇2"] + } +} diff --git a/config.production.json b/config.production.json new file mode 100644 index 0000000..efbd202 --- /dev/null +++ b/config.production.json @@ -0,0 +1,30 @@ +{ + "database": { + "url": "mysql+pymysql://root:password@localhost:3306/ecloud_dify" + }, + "redis": { + "url": "redis://localhost:6379/0" + }, + "ecloud": { + "base_url": "http://125.122.152.142:9899", + "authorization": "your_ecloud_authorization_token" + }, + "dify": { + "base_url": "https://api.dify.ai/v1", + "api_key": "your_dify_api_key" + }, + "server": { + "host": "0.0.0.0", + "port": 7979, + "debug": false + }, + "logging": { + "level": "INFO", + "file": "logs/app.log" + }, + "message_processing": { + "max_retry_count": 3, + "retry_delay": 5, + "queue_timeout": 300 + } +} diff --git a/config.py b/config.py index ae16800..d7919aa 100644 --- a/config.py +++ b/config.py @@ -3,44 +3,104 @@ """ import os -from typing import Optional -from pydantic_settings import BaseSettings +import json -class Settings(BaseSettings): +class Settings: """搴旂敤閰嶇疆""" - # 鏁版嵁搴撻厤缃� - database_url: str = "mysql+pymysql://root:TAI%402019%23Zjun@120.24.39.179:3306/ecloud_dify" + def __init__(self, config_file: str = "config.json"): + """鍒濆鍖栭厤缃紝浠嶫SON鏂囦欢鍔犺浇""" + self.config_file = config_file + self._load_config() - # Redis閰嶇疆 - redis_url: str = "redis://localhost:6379/0" + def _load_config(self): + """浠嶫SON鏂囦欢鍔犺浇閰嶇疆""" + try: + if os.path.exists(self.config_file): + with open(self.config_file, 'r', encoding='utf-8') as f: + config_data = json.load(f) + self._set_config_from_dict(config_data) + else: + # 濡傛灉閰嶇疆鏂囦欢涓嶅瓨鍦紝浣跨敤榛樿鍊� + self._set_default_config() + except Exception as e: + print(f"鍔犺浇閰嶇疆鏂囦欢澶辫触: {e}") + self._set_default_config() - # E浜戠瀹堕厤缃� - ecloud_base_url: str = "http://your-ecloud-domain.com" - ecloud_authorization: str = "your-authorization-token" + def _set_config_from_dict(self, config_data: dict): + """浠庡瓧鍏歌缃厤缃�""" + # 鏁版嵁搴撻厤缃� + self.database_url = config_data.get("database", {}).get("url", "mysql+pymysql://root:TAI%402019%23Zjun@120.24.39.179:3306/ecloud_dify") - # DifyAI閰嶇疆 - dify_base_url: str = "https://api.dify.ai/v1" - dify_api_key: str = "your-dify-api-key" + # Redis閰嶇疆 + self.redis_url = config_data.get("redis", {}).get("url", "redis://localhost:6379/0") - # 鏈嶅姟閰嶇疆 - server_host: str = "0.0.0.0" - server_port: int = 8000 - debug: bool = True + # E浜戠瀹堕厤缃� + ecloud_config = config_data.get("ecloud", {}) + self.ecloud_base_url = ecloud_config.get("base_url", "http://125.122.152.142:9899") + self.ecloud_authorization = ecloud_config.get("authorization", "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxMzYxMTQ1MjE3NSIsInBhc3N3b3JkIjoiJDJhJDEwJEU3Ry5LOEJzekphM2JGQlh0SG8vOXVrUk1NalVweGVVemguUDRnMkJBdHN2YXpBb0JIQWJpIn0.Gd2vbeJjL5pUGFhUngWPLkDTLhD3GUaEPXOkdoTf4KRh9o2FtST1OZJxmZuGdUy7WIYlIPVueoVyIu5iHOyi8A") + self.ecloud_w_id = ecloud_config.get("w_id", "") - # 鏃ュ織閰嶇疆 - log_level: str = "INFO" - log_file: str = "logs/app.log" + # DifyAI閰嶇疆 + dify_config = config_data.get("dify", {}) + self.dify_base_url = dify_config.get("base_url", "https://api.dify.ai/v1") + self.dify_api_key = dify_config.get("api_key", "app-OMnBr7zsf5UTV83Ey8QcSErA") - # 娑堟伅澶勭悊閰嶇疆 - max_retry_count: int = 3 - retry_delay: int = 5 # 绉� - queue_timeout: int = 300 # 绉� + # 鏈嶅姟閰嶇疆 + server_config = config_data.get("server", {}) + self.server_host = server_config.get("host", "0.0.0.0") + self.server_port = server_config.get("port", 7979) + self.debug = server_config.get("debug", True) - class Config: - env_file = ".env" - env_file_encoding = "utf-8" + # 鏃ュ織閰嶇疆 + logging_config = config_data.get("logging", {}) + self.log_level = logging_config.get("level", "INFO") + self.log_file = logging_config.get("file", "logs/app.log") + + # 娑堟伅澶勭悊閰嶇疆 + msg_config = config_data.get("message_processing", {}) + self.max_retry_count = msg_config.get("max_retry_count", 3) + self.retry_delay = msg_config.get("retry_delay", 5) + self.queue_timeout = msg_config.get("queue_timeout", 300) + + # 瀹㈡湇閰嶇疆 + customer_service_config = config_data.get("customer_service", {}) + self.customer_service_names = customer_service_config.get("names", ["瀹㈡湇1", "瀹㈡湇2"]) + + def _set_default_config(self): + """璁剧疆榛樿閰嶇疆""" + # 鏁版嵁搴撻厤缃� + self.database_url = "mysql+pymysql://root:TAI%402019%23Zjun@120.24.39.179:3306/ecloud_dify" + + # Redis閰嶇疆 + self.redis_url = "redis://localhost:6379/0" + + # E浜戠瀹堕厤缃� + self.ecloud_base_url = "http://125.122.152.142:9899" + self.ecloud_authorization = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxMzYxMTQ1MjE3NSIsInBhc3N3b3JkIjoiJDJhJDEwJEU3Ry5LOEJzekphM2JGQlh0SG8vOXVrUk1NalVweGVVemguUDRnMkJBdHN2YXpBb0JIQWJpIn0.Gd2vbeJjL5pUGFhUngWPLkDTLhD3GUaEPXOkdoTf4KRh9o2FtST1OZJxmZuGdUy7WIYlIPVueoVyIu5iHOyi8A" + self.ecloud_w_id = "" + + # DifyAI閰嶇疆 + self.dify_base_url = "https://api.dify.ai/v1" + self.dify_api_key = "app-OMnBr7zsf5UTV83Ey8QcSErA" + + # 鏈嶅姟閰嶇疆 + self.server_host = "0.0.0.0" + self.server_port = 7979 + self.debug = True + + # 鏃ュ織閰嶇疆 + self.log_level = "INFO" + self.log_file = "logs/app.log" + + # 娑堟伅澶勭悊閰嶇疆 + self.max_retry_count = 3 + self.retry_delay = 5 + self.queue_timeout = 300 + + # 瀹㈡湇閰嶇疆 + self.customer_service_names = ["瀹㈡湇1", "瀹㈡湇2"] # 鍏ㄥ眬閰嶇疆瀹炰緥 diff --git a/ecloud_dify.spec b/ecloud_dify.spec new file mode 100644 index 0000000..528fbd5 --- /dev/null +++ b/ecloud_dify.spec @@ -0,0 +1,127 @@ +# -*- mode: python ; coding: utf-8 -*- +import os +import sys +from PyInstaller.utils.hooks import collect_data_files, collect_submodules + +# 鑾峰彇椤圭洰鏍圭洰褰� +project_root = os.path.abspath('.') + +# 鏀堕泦鎵�鏈夋暟鎹枃浠� +datas = [] + +# 娣诲姞閰嶇疆鏂囦欢 +datas.append(('config.example.json', '.')) +datas.append(('config.production.json', '.')) +if os.path.exists('config.json'): + datas.append(('config.json', '.')) + +# 娣诲姞鏃ュ織鐩綍 +if os.path.exists('logs'): + datas.append(('logs', 'logs')) + +# 鏀堕泦FastAPI鍜屽叾浠栧寘鐨勬暟鎹枃浠� +datas.extend(collect_data_files('fastapi')) +datas.extend(collect_data_files('uvicorn')) +datas.extend(collect_data_files('pydantic')) +datas.extend(collect_data_files('sqlalchemy')) + +# 鏀堕泦闅愯棌瀵煎叆 +hiddenimports = [] +hiddenimports.extend(collect_submodules('uvicorn')) +hiddenimports.extend(collect_submodules('fastapi')) +hiddenimports.extend(collect_submodules('pydantic')) +hiddenimports.extend(collect_submodules('sqlalchemy')) +hiddenimports.extend(collect_submodules('pymysql')) +hiddenimports.extend(collect_submodules('redis')) +hiddenimports.extend(collect_submodules('celery')) +hiddenimports.extend(collect_submodules('loguru')) +hiddenimports.extend(collect_submodules('cryptography')) +hiddenimports.extend(collect_submodules('requests')) +hiddenimports.extend(collect_submodules('httpx')) + +# 娣诲姞鐗瑰畾鐨勯殣钘忓鍏� +hiddenimports.extend([ + 'uvicorn.lifespan.on', + 'uvicorn.lifespan.off', + 'uvicorn.protocols.websockets.auto', + 'uvicorn.protocols.websockets.websockets_impl', + 'uvicorn.protocols.http.auto', + 'uvicorn.protocols.http.h11_impl', + 'uvicorn.protocols.http.httptools_impl', + 'uvicorn.loops.auto', + 'uvicorn.loops.asyncio', + 'uvicorn.logging', + 'pymysql.converters', + 'pymysql.cursors', + 'pymysql.connections', + 'sqlalchemy.dialects.mysql.pymysql', + 'sqlalchemy.pool', + 'sqlalchemy.engine.default', + 'app.api.callback', + 'app.models.database', + 'app.workers.message_worker', + 'app.services.message_processor', + 'app.services.dify_client', + 'app.services.ecloud_client', + 'app.services.redis_queue', + 'app.utils.logger', +]) + +# 鎺掗櫎涓嶉渶瑕佺殑妯″潡 +excludes = [ + 'tkinter', + 'matplotlib', + 'numpy', + 'pandas', + 'scipy', + 'PIL', + 'IPython', + 'jupyter', + 'notebook', + 'pytest', + 'test', + 'tests', +] + +block_cipher = None + +a = Analysis( + ['startup.py'], + pathex=[project_root], + binaries=[], + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=excludes, + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='ecloud_dify', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=None, +) diff --git a/init.sql b/init.sql deleted file mode 100644 index ae1312a..0000000 --- a/init.sql +++ /dev/null @@ -1,16 +0,0 @@ --- 鍒濆鍖栨暟鎹簱鑴氭湰 - --- 鍒涘缓鏁版嵁搴擄紙濡傛灉涓嶅瓨鍦級 -CREATE DATABASE IF NOT EXISTS ecloud_dify CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- 浣跨敤鏁版嵁搴� -USE ecloud_dify; - --- 鍒涘缓鐢ㄦ埛锛堝鏋滀笉瀛樺湪锛� -CREATE USER IF NOT EXISTS 'ecloud'@'%' IDENTIFIED BY 'ecloud123'; - --- 鎺堟潈 -GRANT ALL PRIVILEGES ON ecloud_dify.* TO 'ecloud'@'%'; - --- 鍒锋柊鏉冮檺 -FLUSH PRIVILEGES; diff --git a/install_service.bat b/install_service.bat new file mode 100644 index 0000000..4c0b14f --- /dev/null +++ b/install_service.bat @@ -0,0 +1,102 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 Windows鏈嶅姟瀹夎鑴氭湰 +echo ======================================== +echo. + +:: 妫�鏌ョ鐞嗗憳鏉冮檺 +net session >nul 2>&1 +if %errorLevel% == 0 ( + echo 妫�娴嬪埌绠$悊鍛樻潈闄愶紝缁х画瀹夎... +) else ( + echo 閿欒锛氶渶瑕佺鐞嗗憳鏉冮檺鎵嶈兘瀹夎Windows鏈嶅姟 + echo 璇峰彸閿偣鍑绘鑴氭湰锛岄�夋嫨"浠ョ鐞嗗憳韬唤杩愯" + pause + exit /b 1 +) + +:: 鑾峰彇褰撳墠鐩綍 +set "CURRENT_DIR=%~dp0" +set "SERVICE_NAME=ECloudDifyService" +set "SERVICE_DISPLAY_NAME=E浜戠瀹�-DifyAI瀵规帴鏈嶅姟" +set "SERVICE_DESCRIPTION=灏咵浜戠瀹舵秷鎭浆鍙戝埌DifyAI骞惰繑鍥濧I鍥炵瓟鐨勬湇鍔�" +set "EXE_PATH=%CURRENT_DIR%ecloud_dify.exe" + +echo 褰撳墠鐩綍: %CURRENT_DIR% +echo 鏈嶅姟鍚嶇О: %SERVICE_NAME% +echo 鍙墽琛屾枃浠�: %EXE_PATH% +echo. + +:: 妫�鏌xe鏂囦欢鏄惁瀛樺湪 +if not exist "%EXE_PATH%" ( + echo 閿欒锛氭壘涓嶅埌鍙墽琛屾枃浠� %EXE_PATH% + echo 璇风‘淇濆凡缁忓畬鎴愰」鐩墦鍖咃紝骞朵笖ecloud_dify.exe鏂囦欢瀛樺湪 + pause + exit /b 1 +) + +:: 鍋滄骞跺垹闄ゅ凡瀛樺湪鐨勬湇鍔� +echo 妫�鏌ユ槸鍚﹀瓨鍦ㄥ悓鍚嶆湇鍔�... +sc query "%SERVICE_NAME%" >nul 2>&1 +if %errorLevel% == 0 ( + echo 鍙戠幇宸插瓨鍦ㄧ殑鏈嶅姟锛屾鍦ㄥ仠姝㈠苟鍒犻櫎... + sc stop "%SERVICE_NAME%" >nul 2>&1 + timeout /t 3 /nobreak >nul + sc delete "%SERVICE_NAME%" >nul 2>&1 + if %errorLevel% == 0 ( + echo 宸插垹闄ゆ棫鏈嶅姟 + ) else ( + echo 璀﹀憡锛氬垹闄ゆ棫鏈嶅姟澶辫触锛岀户缁畨瑁呮柊鏈嶅姟 + ) + echo. +) + +:: 鍒涘缓Windows鏈嶅姟 +echo 姝e湪鍒涘缓Windows鏈嶅姟... +sc create "%SERVICE_NAME%" binPath= "\"%EXE_PATH%\"" DisplayName= "%SERVICE_DISPLAY_NAME%" start= auto +if %errorLevel% == 0 ( + echo 鏈嶅姟鍒涘缓鎴愬姛 +) else ( + echo 閿欒锛氭湇鍔″垱寤哄け璐� + pause + exit /b 1 +) + +:: 璁剧疆鏈嶅姟鎻忚堪 +sc description "%SERVICE_NAME%" "%SERVICE_DESCRIPTION%" + +:: 璁剧疆鏈嶅姟鎭㈠閫夐」锛堝け璐ユ椂鑷姩閲嶅惎锛� +sc failure "%SERVICE_NAME%" reset= 86400 actions= restart/5000/restart/10000/restart/30000 + +echo. +echo 鏈嶅姟瀹夎瀹屾垚锛� +echo. +echo 鍙敤鐨勬搷浣滐細 +echo 1. 鍚姩鏈嶅姟: sc start %SERVICE_NAME% +echo 2. 鍋滄鏈嶅姟: sc stop %SERVICE_NAME% +echo 3. 鏌ョ湅鏈嶅姟鐘舵��: sc query %SERVICE_NAME% +echo 4. 鍒犻櫎鏈嶅姟: sc delete %SERVICE_NAME% +echo. +echo 鎴栬�呬娇鐢╓indows鏈嶅姟绠$悊鍣� (services.msc) 杩涜绠$悊 +echo. + +:: 璇㈤棶鏄惁绔嬪嵆鍚姩鏈嶅姟 +set /p START_NOW="鏄惁绔嬪嵆鍚姩鏈嶅姟锛�(Y/N): " +if /i "%START_NOW%"=="Y" ( + echo 姝e湪鍚姩鏈嶅姟... + sc start "%SERVICE_NAME%" + if %errorLevel% == 0 ( + echo 鏈嶅姟鍚姩鎴愬姛锛� + echo 鏈嶅姟灏嗗湪绔彛 7979 涓婅繍琛� + echo 鍙互閫氳繃 http://localhost:7979 璁块棶鏈嶅姟 + ) else ( + echo 鏈嶅姟鍚姩澶辫触锛岃妫�鏌ラ厤缃枃浠跺拰鏃ュ織 + ) +) else ( + echo 鏈嶅姟宸插畨瑁呬絾鏈惎鍔� + echo 鍙互绋嶅悗閫氳繃 sc start %SERVICE_NAME% 鍚姩鏈嶅姟 +) + +echo. +pause diff --git a/logs/app.log b/logs/app.log index 6cbf9bc..fec3d52 100644 --- a/logs/app.log +++ b/logs/app.log @@ -1,4 +1,9 @@ -2025-07-22 14:23:00 | INFO | __main__:<module>:94 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 -2025-07-22 14:24:44 | INFO | __main__:<module>:94 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 -2025-07-22 14:25:57 | INFO | __main__:<module>:94 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 -2025-07-22 14:37:50 | INFO | __main__:<module>:94 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-23 14:46:37 | INFO | __main__:<module>:105 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-23 15:03:24 | INFO | __main__:<module>:105 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-23 16:48:21 | INFO | __main__:<module>:121 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-23 16:50:14 | INFO | __main__:<module>:121 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-23 16:55:09 | INFO | __main__:<module>:121 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-23 16:56:49 | INFO | __main__:<module>:121 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-23 17:19:58 | INFO | __main__:<module>:121 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-23 17:43:16 | INFO | __main__:<module>:105 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 +2025-07-23 17:44:16 | INFO | __main__:<module>:105 - 鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 diff --git a/main.py b/main.py index dd776b7..978923a 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,7 @@ from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager from loguru import logger +import time from config import settings from app.api.callback import router as callback_router from app.models.database import create_tables @@ -81,12 +82,22 @@ } +@app.get("/health") +async def health_check(): + """鍋ュ悍妫�鏌ユ帴鍙�""" + return { + "status": "healthy", + "message": "E浜戠瀹�-DifyAI瀵规帴鏈嶅姟杩愯姝e父", + "timestamp": int(time.time()), + } + + if __name__ == "__main__": # 閰嶇疆鏃ュ織 logger.add( settings.log_file, rotation="1 day", - retention="30 days", + retention="7 days", level=settings.log_level, format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} - {message}", ) diff --git a/requirements.txt b/requirements.txt index 6e0f4a5..56a01b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,5 @@ cryptography==45.0.5 pytest==8.4.1 httpx==0.28.1 +pyinstaller==6.3.0 +pywin32==306 diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..cc26592 --- /dev/null +++ b/start.bat @@ -0,0 +1,42 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 鍚姩鑴氭湰 +echo ======================================== +echo. + +:: 鑾峰彇褰撳墠鐩綍 +set "CURRENT_DIR=%~dp0" +set "EXE_PATH=%CURRENT_DIR%ecloud_dify.exe" + +echo 褰撳墠鐩綍: %CURRENT_DIR% +echo 鍙墽琛屾枃浠�: %EXE_PATH% +echo. + +:: 妫�鏌xe鏂囦欢鏄惁瀛樺湪 +if not exist "%EXE_PATH%" ( + echo 閿欒锛氭壘涓嶅埌鍙墽琛屾枃浠� %EXE_PATH% + echo 璇风‘淇濆凡缁忓畬鎴愰」鐩墦鍖咃紝骞朵笖ecloud_dify.exe鏂囦欢瀛樺湪 + pause + exit /b 1 +) + +:: 妫�鏌ラ厤缃枃浠� +if not exist "%CURRENT_DIR%config.json" ( + echo 璀﹀憡锛氭湭鎵惧埌config.json閰嶇疆鏂囦欢 + echo 绋嬪簭灏嗕娇鐢ㄩ粯璁ら厤缃垨config.production.json妯℃澘 + echo. +) + +echo 姝e湪鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟... +echo 鏈嶅姟灏嗗湪绔彛 7979 涓婅繍琛� +echo 鍙互閫氳繃 http://localhost:7979 璁块棶鏈嶅姟 +echo 鎸� Ctrl+C 鍋滄鏈嶅姟 +echo. + +:: 鍚姩鏈嶅姟 +"%EXE_PATH%" + +echo. +echo 鏈嶅姟宸插仠姝� +pause diff --git a/startup.py b/startup.py new file mode 100644 index 0000000..dad9b0e --- /dev/null +++ b/startup.py @@ -0,0 +1,139 @@ +""" +鍚姩鑴氭湰 - 澶勭悊閰嶇疆鏂囦欢鍜岀洰褰曞垵濮嬪寲 +""" + +import os +import sys +import json +import shutil +from pathlib import Path + + +def get_exe_dir(): + """鑾峰彇exe鏂囦欢鎵�鍦ㄧ洰褰�""" + if getattr(sys, 'frozen', False): + # 濡傛灉鏄墦鍖呭悗鐨別xe鏂囦欢 + return os.path.dirname(sys.executable) + else: + # 濡傛灉鏄紑鍙戠幆澧� + return os.path.dirname(os.path.abspath(__file__)) + + +def ensure_directories(): + """纭繚蹇呰鐨勭洰褰曞瓨鍦�""" + exe_dir = get_exe_dir() + + # 鍒涘缓鏃ュ織鐩綍 + logs_dir = os.path.join(exe_dir, 'logs') + if not os.path.exists(logs_dir): + os.makedirs(logs_dir) + print(f"鍒涘缓鏃ュ織鐩綍: {logs_dir}") + + return exe_dir + + +def ensure_config_file(): + """纭繚閰嶇疆鏂囦欢瀛樺湪""" + exe_dir = get_exe_dir() + config_file = os.path.join(exe_dir, 'config.json') + + if not os.path.exists(config_file): + # 濡傛灉config.json涓嶅瓨鍦紝灏濊瘯澶嶅埗鐢熶骇閰嶇疆妯℃澘 + production_config = os.path.join(exe_dir, 'config.production.json') + example_config = os.path.join(exe_dir, 'config.example.json') + + if os.path.exists(production_config): + shutil.copy2(production_config, config_file) + print(f"澶嶅埗鐢熶骇閰嶇疆鏂囦欢: {production_config} -> {config_file}") + elif os.path.exists(example_config): + shutil.copy2(example_config, config_file) + print(f"澶嶅埗绀轰緥閰嶇疆鏂囦欢: {example_config} -> {config_file}") + else: + # 鍒涘缓榛樿閰嶇疆鏂囦欢 + default_config = { + "database": { + "url": "mysql+pymysql://root:password@localhost:3306/ecloud_dify" + }, + "redis": { + "url": "redis://localhost:6379/0" + }, + "ecloud": { + "base_url": "http://125.122.152.142:9899", + "authorization": "your_ecloud_authorization_token", + "w_id": "your_ecloud_w_id" + }, + "dify": { + "base_url": "https://api.dify.ai/v1", + "api_key": "your_dify_api_key" + }, + "server": { + "host": "0.0.0.0", + "port": 7979, + "debug": False + }, + "logging": { + "level": "INFO", + "file": "logs/app.log" + }, + "message_processing": { + "max_retry_count": 3, + "retry_delay": 5, + "queue_timeout": 300 + }, + "customer_service": { + "names": ["瀹㈡湇1", "瀹㈡湇2"] + } + } + + with open(config_file, 'w', encoding='utf-8') as f: + json.dump(default_config, f, indent=2, ensure_ascii=False) + print(f"鍒涘缓榛樿閰嶇疆鏂囦欢: {config_file}") + + return config_file + + +def setup_environment(): + """璁剧疆杩愯鐜""" + exe_dir = ensure_directories() + config_file = ensure_config_file() + + # 璁剧疆宸ヤ綔鐩綍涓篹xe鎵�鍦ㄧ洰褰� + os.chdir(exe_dir) + print(f"璁剧疆宸ヤ綔鐩綍: {exe_dir}") + + return exe_dir, config_file + + +if __name__ == "__main__": + setup_environment() + + # 瀵煎叆骞跺惎鍔ㄤ富搴旂敤 + try: + from main import app + import uvicorn + from config import settings + from loguru import logger + + # 閰嶇疆鏃ュ織 + logger.add( + settings.log_file, + rotation="1 day", + retention="7 days", + level=settings.log_level, + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} - {message}", + ) + + logger.info("鍚姩E浜戠瀹�-DifyAI瀵规帴鏈嶅姟") + + # 鍚姩鏈嶅姟 + uvicorn.run( + app, + host=settings.server_host, + port=settings.server_port, + log_level=settings.log_level.lower(), + ) + + except Exception as e: + print(f"鍚姩澶辫触: {e}") + input("鎸変换鎰忛敭閫�鍑�...") + sys.exit(1) diff --git a/uninstall_service.bat b/uninstall_service.bat new file mode 100644 index 0000000..c361981 --- /dev/null +++ b/uninstall_service.bat @@ -0,0 +1,61 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo E浜戠瀹�-DifyAI瀵规帴鏈嶅姟 Windows鏈嶅姟鍗歌浇鑴氭湰 +echo ======================================== +echo. + +:: 妫�鏌ョ鐞嗗憳鏉冮檺 +net session >nul 2>&1 +if %errorLevel% == 0 ( + echo 妫�娴嬪埌绠$悊鍛樻潈闄愶紝缁х画鍗歌浇... +) else ( + echo 閿欒锛氶渶瑕佺鐞嗗憳鏉冮檺鎵嶈兘鍗歌浇Windows鏈嶅姟 + echo 璇峰彸閿偣鍑绘鑴氭湰锛岄�夋嫨"浠ョ鐞嗗憳韬唤杩愯" + pause + exit /b 1 +) + +set "SERVICE_NAME=ECloudDifyService" + +echo 鏈嶅姟鍚嶇О: %SERVICE_NAME% +echo. + +:: 妫�鏌ユ湇鍔℃槸鍚﹀瓨鍦� +echo 妫�鏌ユ湇鍔℃槸鍚﹀瓨鍦�... +sc query "%SERVICE_NAME%" >nul 2>&1 +if %errorLevel% == 0 ( + echo 鎵惧埌鏈嶅姟锛屾鍦ㄥ嵏杞�... + + :: 鍋滄鏈嶅姟 + echo 姝e湪鍋滄鏈嶅姟... + sc stop "%SERVICE_NAME%" >nul 2>&1 + if %errorLevel% == 0 ( + echo 鏈嶅姟宸插仠姝� + ) else ( + echo 鏈嶅姟鍙兘宸茬粡鍋滄鎴栧仠姝㈠け璐� + ) + + :: 绛夊緟鏈嶅姟瀹屽叏鍋滄 + echo 绛夊緟鏈嶅姟瀹屽叏鍋滄... + timeout /t 5 /nobreak >nul + + :: 鍒犻櫎鏈嶅姟 + echo 姝e湪鍒犻櫎鏈嶅姟... + sc delete "%SERVICE_NAME%" + if %errorLevel% == 0 ( + echo 鏈嶅姟鍒犻櫎鎴愬姛锛� + ) else ( + echo 閿欒锛氭湇鍔″垹闄ゅけ璐� + pause + exit /b 1 + ) +) else ( + echo 鏈壘鍒版湇鍔� %SERVICE_NAME% + echo 鍙兘鏈嶅姟宸茬粡琚垹闄ゆ垨浠庢湭瀹夎 +) + +echo. +echo 鏈嶅姟鍗歌浇瀹屾垚锛� +echo. +pause -- Gitblit v1.9.1