Spaces:
Sleeping
Sleeping
Commit ·
ff120d2
1
Parent(s): 62eb709
更新了
Browse files- app.py +56 -58
- core/database/base.py +9 -9
app.py
CHANGED
|
@@ -18,6 +18,7 @@ from pathlib import Path
|
|
| 18 |
from contextlib import asynccontextmanager
|
| 19 |
import uuid
|
| 20 |
import asyncio
|
|
|
|
| 21 |
|
| 22 |
# 本專案整合版:單一 app.py 作為後端入口,前端靜態檔(index.html/app.js/style.css)放在根目錄
|
| 23 |
|
|
@@ -1210,71 +1211,68 @@ async def websocket_endpoint_with_jwt(websocket: WebSocket, token: str = Query(N
|
|
| 1210 |
|
| 1211 |
finally:
|
| 1212 |
pass
|
| 1213 |
-
elif message_type == "env_snapshot":
|
| 1214 |
# ===== 環境快照上報 =====
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
|
| 1221 |
-
|
| 1222 |
-
|
| 1223 |
-
|
| 1224 |
-
|
| 1225 |
-
|
| 1226 |
-
|
| 1227 |
-
|
| 1228 |
-
|
| 1229 |
-
|
| 1230 |
-
|
| 1231 |
-
|
| 1232 |
-
|
|
|
|
|
|
|
|
|
|
| 1233 |
do_write_snapshot = True
|
| 1234 |
-
else:
|
| 1235 |
-
do_write_snapshot = True
|
| 1236 |
-
|
| 1237 |
-
from geohash2 import encode as gh_encode
|
| 1238 |
-
geohash7 = gh_encode(lat, lon, precision=7) if (lat is not None and lon is not None) else None
|
| 1239 |
-
heading_cardinal = _heading_to_cardinal(heading_deg) if heading_deg is not None else None
|
| 1240 |
-
env_payload = {
|
| 1241 |
-
"lat": lat,
|
| 1242 |
-
"lon": lon,
|
| 1243 |
-
"accuracy_m": acc,
|
| 1244 |
-
"heading_deg": heading_deg,
|
| 1245 |
-
"heading_cardinal": heading_cardinal,
|
| 1246 |
-
"tz": tz,
|
| 1247 |
-
"locale": locale,
|
| 1248 |
-
"device": device,
|
| 1249 |
-
"geohash_7": geohash7,
|
| 1250 |
-
}
|
| 1251 |
-
|
| 1252 |
-
# 更新會話暫存
|
| 1253 |
-
manager.last_env[user_id] = env_payload
|
| 1254 |
-
info = manager.get_client_info(user_id) or {}
|
| 1255 |
-
info['env_context'] = env_payload
|
| 1256 |
-
manager.set_client_info(user_id, info)
|
| 1257 |
|
| 1258 |
-
|
| 1259 |
-
|
| 1260 |
-
|
| 1261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1262 |
|
| 1263 |
-
if do_write_snapshot:
|
| 1264 |
try:
|
| 1265 |
-
|
| 1266 |
-
snap['reason'] = 'threshold'
|
| 1267 |
-
await add_user_env_snapshot(user_id, snap)
|
| 1268 |
except Exception as e:
|
| 1269 |
-
logger.warning(f"寫入環境
|
| 1270 |
|
| 1271 |
-
|
| 1272 |
-
|
| 1273 |
-
|
| 1274 |
-
|
| 1275 |
-
|
| 1276 |
-
|
|
|
|
| 1277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1278 |
except json.JSONDecodeError:
|
| 1279 |
await manager.send_message("消息格式錯誤,無法解析", user_id, "error")
|
| 1280 |
except Exception as e:
|
|
@@ -2249,7 +2247,7 @@ async def list_devices(current_user: dict = Depends(get_current_user_optional)):
|
|
| 2249 |
device_bindings = firestore_db.collection('device_bindings')
|
| 2250 |
|
| 2251 |
devices = []
|
| 2252 |
-
query = device_bindings.where("user_id", "==", current_user["sub"]).where("status", "==", "active")
|
| 2253 |
docs = query.get()
|
| 2254 |
|
| 2255 |
for doc in docs:
|
|
|
|
| 18 |
from contextlib import asynccontextmanager
|
| 19 |
import uuid
|
| 20 |
import asyncio
|
| 21 |
+
from google.cloud.firestore import FieldFilter
|
| 22 |
|
| 23 |
# 本專案整合版:單一 app.py 作為後端入口,前端靜態檔(index.html/app.js/style.css)放在根目錄
|
| 24 |
|
|
|
|
| 1211 |
|
| 1212 |
finally:
|
| 1213 |
pass
|
|
|
|
| 1214 |
# ===== 環境快照上報 =====
|
| 1215 |
+
if message_type == "env_snapshot":
|
| 1216 |
+
try:
|
| 1217 |
+
lat = float(message_data.get("lat")) if message_data.get("lat") is not None else None
|
| 1218 |
+
lon = float(message_data.get("lon")) if message_data.get("lon") is not None else None
|
| 1219 |
+
acc = message_data.get("accuracy_m")
|
| 1220 |
+
acc = float(acc) if acc is not None else None
|
| 1221 |
+
heading_deg = message_data.get("heading_deg")
|
| 1222 |
+
heading_deg = float(heading_deg) if heading_deg is not None else None
|
| 1223 |
+
tz = message_data.get("tz")
|
| 1224 |
+
locale = message_data.get("locale")
|
| 1225 |
+
device = message_data.get("device")
|
| 1226 |
+
|
| 1227 |
+
# 後端節流:距離<100m且方位差<25度則忽略
|
| 1228 |
+
do_write_snapshot = False
|
| 1229 |
+
last = manager.last_env.get(user_id)
|
| 1230 |
+
if last and lat is not None and lon is not None and last.get("lat") is not None:
|
| 1231 |
+
dist = _haversine_m(last.get("lat",0), last.get("lon",0), lat, lon)
|
| 1232 |
+
deg_diff = abs((heading_deg or 0) - (last.get("heading_deg") or 0))
|
| 1233 |
+
if dist >= 100 or deg_diff >= 25:
|
| 1234 |
+
do_write_snapshot = True
|
| 1235 |
+
else:
|
| 1236 |
do_write_snapshot = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1237 |
|
| 1238 |
+
from geohash2 import encode as gh_encode
|
| 1239 |
+
geohash7 = gh_encode(lat, lon, precision=7) if (lat is not None and lon is not None) else None
|
| 1240 |
+
heading_cardinal = _heading_to_cardinal(heading_deg) if heading_deg is not None else None
|
| 1241 |
+
env_payload = {
|
| 1242 |
+
"lat": lat,
|
| 1243 |
+
"lon": lon,
|
| 1244 |
+
"accuracy_m": acc,
|
| 1245 |
+
"heading_deg": heading_deg,
|
| 1246 |
+
"heading_cardinal": heading_cardinal,
|
| 1247 |
+
"tz": tz,
|
| 1248 |
+
"locale": locale,
|
| 1249 |
+
"device": device,
|
| 1250 |
+
"geohash_7": geohash7,
|
| 1251 |
+
}
|
| 1252 |
+
|
| 1253 |
+
# 更新會話暫存
|
| 1254 |
+
manager.last_env[user_id] = env_payload
|
| 1255 |
+
info = manager.get_client_info(user_id) or {}
|
| 1256 |
+
info['env_context'] = env_payload
|
| 1257 |
+
manager.set_client_info(user_id, info)
|
| 1258 |
|
|
|
|
| 1259 |
try:
|
| 1260 |
+
await set_user_env_current(user_id, env_payload)
|
|
|
|
|
|
|
| 1261 |
except Exception as e:
|
| 1262 |
+
logger.warning(f"寫入環境現況失敗: {e}")
|
| 1263 |
|
| 1264 |
+
if do_write_snapshot:
|
| 1265 |
+
try:
|
| 1266 |
+
snap = env_payload.copy()
|
| 1267 |
+
snap['reason'] = 'threshold'
|
| 1268 |
+
await add_user_env_snapshot(user_id, snap)
|
| 1269 |
+
except Exception as e:
|
| 1270 |
+
logger.warning(f"寫入環境快照失敗: {e}")
|
| 1271 |
|
| 1272 |
+
await websocket.send_json({"type": "env_ack", "success": True, "geohash_7": geohash7, "heading_cardinal": heading_cardinal})
|
| 1273 |
+
except Exception as e:
|
| 1274 |
+
logger.error(f"處理 env_snapshot 失敗: {e}")
|
| 1275 |
+
await websocket.send_json({"type": "env_ack", "success": False, "error": str(e)})
|
| 1276 |
except json.JSONDecodeError:
|
| 1277 |
await manager.send_message("消息格式錯誤,無法解析", user_id, "error")
|
| 1278 |
except Exception as e:
|
|
|
|
| 2247 |
device_bindings = firestore_db.collection('device_bindings')
|
| 2248 |
|
| 2249 |
devices = []
|
| 2250 |
+
query = device_bindings.where(filter=FieldFilter("user_id", "==", current_user["sub"])).where(filter=FieldFilter("status", "==", "active"))
|
| 2251 |
docs = query.get()
|
| 2252 |
|
| 2253 |
for doc in docs:
|
core/database/base.py
CHANGED
|
@@ -175,7 +175,7 @@ async def get_user_history(user_id, limit=20):
|
|
| 175 |
try:
|
| 176 |
import asyncio as _asyncio
|
| 177 |
def _fetch_messages():
|
| 178 |
-
docs = messages_collection.where("user_id", "==", user_id)\
|
| 179 |
.order_by("timestamp")\
|
| 180 |
.limit(limit)\
|
| 181 |
.stream()
|
|
@@ -374,7 +374,7 @@ async def get_user_chats(user_id):
|
|
| 374 |
try:
|
| 375 |
import asyncio as _asyncio
|
| 376 |
def _fetch_chats():
|
| 377 |
-
docs = chats_collection.where("user_id", "==", user_id)\
|
| 378 |
.order_by("updated_at", direction=firestore.Query.DESCENDING)\
|
| 379 |
.stream()
|
| 380 |
chats = []
|
|
@@ -422,7 +422,7 @@ async def get_chat(chat_id):
|
|
| 422 |
def _fetch_msgs():
|
| 423 |
q = (
|
| 424 |
messages_collection
|
| 425 |
-
.where("chat_id", "==", chat_id)
|
| 426 |
.order_by("timestamp", direction=_fs.Query.ASCENDING)
|
| 427 |
)
|
| 428 |
return [d.to_dict() for d in q.stream()]
|
|
@@ -490,7 +490,7 @@ async def get_chat_messages(chat_id: str, limit: int | None = None, ascending: b
|
|
| 490 |
from google.cloud import firestore as _fs
|
| 491 |
|
| 492 |
def _query():
|
| 493 |
-
q = messages_collection.where("chat_id", "==", chat_id)
|
| 494 |
direction = _fs.Query.ASCENDING if ascending else _fs.Query.DESCENDING
|
| 495 |
q = q.order_by("timestamp", direction=direction)
|
| 496 |
if limit and limit > 0:
|
|
@@ -731,7 +731,7 @@ async def save_memory(
|
|
| 731 |
|
| 732 |
def _find_existing():
|
| 733 |
docs = (
|
| 734 |
-
col_ref.where("content_hash", "==", content_hash)
|
| 735 |
.limit(1)
|
| 736 |
.stream()
|
| 737 |
)
|
|
@@ -936,9 +936,9 @@ async def get_user_memories(
|
|
| 936 |
|
| 937 |
def _fetch_memories():
|
| 938 |
col_ref = _get_user_memories_collection(user_id)
|
| 939 |
-
query = col_ref.where("importance", ">=", min_importance)
|
| 940 |
if memory_type:
|
| 941 |
-
query = query.where("type", "==", memory_type)
|
| 942 |
docs = (
|
| 943 |
query.order_by("importance", direction=firestore.Query.DESCENDING)
|
| 944 |
.order_by("updated_at", direction=firestore.Query.DESCENDING)
|
|
@@ -1112,8 +1112,8 @@ async def cleanup_old_memories(user_id: str, days_old: int = 90, min_importance:
|
|
| 1112 |
def _delete_old():
|
| 1113 |
col_ref = _get_user_memories_collection(user_id)
|
| 1114 |
docs = (
|
| 1115 |
-
col_ref.where("importance", "<", min_importance)
|
| 1116 |
-
.where("updated_at", "<", cutoff_date)
|
| 1117 |
.stream()
|
| 1118 |
)
|
| 1119 |
deleted_count = 0
|
|
|
|
| 175 |
try:
|
| 176 |
import asyncio as _asyncio
|
| 177 |
def _fetch_messages():
|
| 178 |
+
docs = messages_collection.where(filter=FieldFilter("user_id", "==", user_id))\
|
| 179 |
.order_by("timestamp")\
|
| 180 |
.limit(limit)\
|
| 181 |
.stream()
|
|
|
|
| 374 |
try:
|
| 375 |
import asyncio as _asyncio
|
| 376 |
def _fetch_chats():
|
| 377 |
+
docs = chats_collection.where(filter=FieldFilter("user_id", "==", user_id))\
|
| 378 |
.order_by("updated_at", direction=firestore.Query.DESCENDING)\
|
| 379 |
.stream()
|
| 380 |
chats = []
|
|
|
|
| 422 |
def _fetch_msgs():
|
| 423 |
q = (
|
| 424 |
messages_collection
|
| 425 |
+
.where(filter=FieldFilter("chat_id", "==", chat_id))
|
| 426 |
.order_by("timestamp", direction=_fs.Query.ASCENDING)
|
| 427 |
)
|
| 428 |
return [d.to_dict() for d in q.stream()]
|
|
|
|
| 490 |
from google.cloud import firestore as _fs
|
| 491 |
|
| 492 |
def _query():
|
| 493 |
+
q = messages_collection.where(filter=FieldFilter("chat_id", "==", chat_id))
|
| 494 |
direction = _fs.Query.ASCENDING if ascending else _fs.Query.DESCENDING
|
| 495 |
q = q.order_by("timestamp", direction=direction)
|
| 496 |
if limit and limit > 0:
|
|
|
|
| 731 |
|
| 732 |
def _find_existing():
|
| 733 |
docs = (
|
| 734 |
+
col_ref.where(filter=FieldFilter("content_hash", "==", content_hash))
|
| 735 |
.limit(1)
|
| 736 |
.stream()
|
| 737 |
)
|
|
|
|
| 936 |
|
| 937 |
def _fetch_memories():
|
| 938 |
col_ref = _get_user_memories_collection(user_id)
|
| 939 |
+
query = col_ref.where(filter=FieldFilter("importance", ">=", min_importance))
|
| 940 |
if memory_type:
|
| 941 |
+
query = query.where(filter=FieldFilter("type", "==", memory_type))
|
| 942 |
docs = (
|
| 943 |
query.order_by("importance", direction=firestore.Query.DESCENDING)
|
| 944 |
.order_by("updated_at", direction=firestore.Query.DESCENDING)
|
|
|
|
| 1112 |
def _delete_old():
|
| 1113 |
col_ref = _get_user_memories_collection(user_id)
|
| 1114 |
docs = (
|
| 1115 |
+
col_ref.where(filter=FieldFilter("importance", "<", min_importance))
|
| 1116 |
+
.where(filter=FieldFilter("updated_at", "<", cutoff_date))
|
| 1117 |
.stream()
|
| 1118 |
)
|
| 1119 |
deleted_count = 0
|