feat: 基础层——phonenumberslite/resolve_region_code/CA字典bug/XFF服务端优先
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -22,9 +22,9 @@ async def ws_chat(websocket: WebSocket):
|
|||||||
|
|
||||||
# Collect client info
|
# Collect client info
|
||||||
client_ip = websocket.client.host if websocket.client else ""
|
client_ip = websocket.client.host if websocket.client else ""
|
||||||
forwarded = websocket.headers.get("x-forwarded-for", "")
|
forwarded = websocket.headers.get("x-forwarded-for", "").split(",")[0].strip()
|
||||||
if forwarded:
|
if forwarded:
|
||||||
client_ip = forwarded.split(",")[0].strip()
|
client_ip = forwarded
|
||||||
user_agent = websocket.headers.get("user-agent", "")
|
user_agent = websocket.headers.get("user-agent", "")
|
||||||
client_info = {"ip": client_ip, "user_agent": user_agent}
|
client_info = {"ip": client_ip, "user_agent": user_agent}
|
||||||
|
|
||||||
@@ -58,6 +58,10 @@ async def ws_chat(websocket: WebSocket):
|
|||||||
init_msg.get("source", "standalone")
|
init_msg.get("source", "standalone")
|
||||||
)
|
)
|
||||||
merged_info = {**client_info, **(init_msg.get("client_info") or {})}
|
merged_info = {**client_info, **(init_msg.get("client_info") or {})}
|
||||||
|
# 服务端 XFF 优先于客户端自报 ip(客户端自报可伪造,可伪造面最小);
|
||||||
|
# 无 XFF 时保留客户端自报,user_agent 等其他字段不受影响
|
||||||
|
if forwarded:
|
||||||
|
merged_info["ip"] = forwarded
|
||||||
conv = await manager.create_conversation(
|
conv = await manager.create_conversation(
|
||||||
source=source,
|
source=source,
|
||||||
client_info=merged_info,
|
client_info=merged_info,
|
||||||
|
|||||||
@@ -32,13 +32,19 @@ class SessionRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/session")
|
@router.post("/session")
|
||||||
async def chat_session(req: SessionRequest):
|
async def chat_session(req: SessionRequest, request: Request):
|
||||||
"""Resume an active conversation by session_id, or create a new app one.
|
"""Resume an active conversation by session_id, or create a new app one.
|
||||||
|
|
||||||
`context` flows into `create_conversation` where it is whitelisted into
|
`context` flows into `create_conversation` where it is whitelisted into
|
||||||
`raw_info` (only when non-None). `client_info` is stored for analytics only
|
`raw_info` (only when non-None). `client_info` is stored for analytics only
|
||||||
(ip/ua/device-id/lang) and never fed to the AI prompt.
|
(ip/ua/device-id/lang) and never fed to the AI prompt.
|
||||||
"""
|
"""
|
||||||
|
# 服务端 XFF 优先于客户端自报 ip(客户端自报可伪造,可伪造面最小)
|
||||||
|
xf = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
|
||||||
|
client_info = dict(req.client_info or {})
|
||||||
|
if xf:
|
||||||
|
client_info["ip"] = xf
|
||||||
|
|
||||||
async with async_session() as db:
|
async with async_session() as db:
|
||||||
manager = ConversationManager(db)
|
manager = ConversationManager(db)
|
||||||
if req.session_id:
|
if req.session_id:
|
||||||
@@ -51,7 +57,7 @@ async def chat_session(req: SessionRequest):
|
|||||||
}
|
}
|
||||||
conv = await manager.create_conversation(
|
conv = await manager.create_conversation(
|
||||||
source=ConversationSource.app,
|
source=ConversationSource.app,
|
||||||
client_info=req.client_info,
|
client_info=client_info,
|
||||||
context=req.context,
|
context=req.context,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ COUNTRY_NAMES = {
|
|||||||
"VN": "越南", "ID": "印尼", "PH": "菲律宾", "MY": "马来西亚",
|
"VN": "越南", "ID": "印尼", "PH": "菲律宾", "MY": "马来西亚",
|
||||||
"JP": "日本", "KR": "韩国", "GB": "英国", "DE": "德国",
|
"JP": "日本", "KR": "韩国", "GB": "英国", "DE": "德国",
|
||||||
"FR": "法国", "RU": "俄罗斯", "BR": "巴西", "MX": "墨西哥",
|
"FR": "法国", "RU": "俄罗斯", "BR": "巴西", "MX": "墨西哥",
|
||||||
"AU": "加拿大", "AU": "澳大利亚", "TR": "土耳其", "IR": "伊朗",
|
"AU": "澳大利亚", "CA": "加拿大", "TR": "土耳其", "IR": "伊朗",
|
||||||
"MM": "缅甸", "KH": "柬埔寨", "LA": "老挝", "LK": "斯里兰卡",
|
"MM": "缅甸", "KH": "柬埔寨", "LA": "老挝", "LK": "斯里兰卡",
|
||||||
"NP": "尼泊尔", "UG": "乌干达", "ET": "埃塞俄比亚", "SN": "塞内加尔",
|
"NP": "尼泊尔", "UG": "乌干达", "ET": "埃塞俄比亚", "SN": "塞内加尔",
|
||||||
"CI": "科特迪瓦", "CM": "喀麦隆", "CD": "刚果(金)", "MZ": "莫桑比克",
|
"CI": "科特迪瓦", "CM": "喀麦隆", "CD": "刚果(金)", "MZ": "莫桑比克",
|
||||||
@@ -26,3 +26,21 @@ def resolve_country(ip: str) -> str:
|
|||||||
return "未知"
|
return "未知"
|
||||||
except Exception:
|
except Exception:
|
||||||
return "未知"
|
return "未知"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_region_code(ip: str) -> str | None:
|
||||||
|
"""解析 ip 为 ISO 3166-1 alpha-2 国家码(如 "US"),查不到返回 None。
|
||||||
|
|
||||||
|
与 resolve_country 的分工:resolve_country 返回中文名(含"本地"/"未知"
|
||||||
|
兜底占位,仅供展示/记录);本函数返回 ISO alpha-2 或 None,供电话区号
|
||||||
|
推断等逻辑消费——绝无中文或占位值,调用方无需再过滤。
|
||||||
|
"""
|
||||||
|
if not ip or ip in ("127.0.0.1", "::1", "localhost"):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
code = country_code(ip)
|
||||||
|
if code and code != "--":
|
||||||
|
return code
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ pydantic-settings>=2.0.0
|
|||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
sniffio>=1.3.0
|
sniffio>=1.3.0
|
||||||
iptocc>=0.1.0
|
iptocc>=0.1.0
|
||||||
|
phonenumberslite
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
def test_resolve_region_code():
|
||||||
|
from app.services.ip_service import resolve_region_code
|
||||||
|
# 公网IP返回 ISO alpha-2 或 None(查不到),绝不返回中文/"本地"/"未知"
|
||||||
|
r = resolve_region_code("104.28.41.169")
|
||||||
|
assert r is None or (isinstance(r, str) and len(r) == 2 and r.isupper() and r.isalpha())
|
||||||
|
assert resolve_region_code("") is None
|
||||||
|
assert resolve_region_code("127.0.0.1") is None
|
||||||
|
assert resolve_region_code("::1") is None
|
||||||
|
|
||||||
|
def test_country_names_ca_bug_fixed():
|
||||||
|
from app.services import ip_service
|
||||||
|
assert ip_service.COUNTRY_NAMES.get("CA") == "加拿大"
|
||||||
|
assert ip_service.COUNTRY_NAMES.get("AU") == "澳大利亚"
|
||||||
Reference in New Issue
Block a user