feat: 企微 API 代理 + 菜单部署修复

config.py:
- 新增 WECOM_API_BASE 环境变量 (默认 https://qyapi.weixin.qq.com)

wecom.py:
- 所有 API 调用改用 settings.WECOM_API_BASE (支持代理)
- 修复 _post_with_retry URL 拼接 bug (? vs &)
- 菜单名去掉 emoji (企微限制 16 字符)

api/wecom.py:
- setup-menu 端点菜单名修正

.env.example:
- 新增 WECOM_API_BASE 说明

当前 .env 配置: WECOM_API_BASE=https://api.v6ole.top
已验证: 通过代理获取 token , 菜单创建 

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-26 11:58:12 +08:00
parent da1f24c710
commit 1d2dacc3ab
4 changed files with 15 additions and 19 deletions
+2
View File
@@ -36,6 +36,8 @@ WECOM_AGENT_ID=your-agent-id
WECOM_SECRET=your-app-secret
WECOM_TOKEN=your-token
WECOM_ENCODING_AES_KEY=your-encoding-aes-key
# 企微 API 代理(解决 IP 白名单问题),默认直连 https://qyapi.weixin.qq.com
WECOM_API_BASE=https://qyapi.weixin.qq.com
# ── AI / LLM (OpenAI 兼容接口,用于周报摘要) ──
# 支持 OpenAI / DeepSeek / 通义千问 / 本地 Ollama 等
+2 -10
View File
@@ -141,16 +141,8 @@ async def test_message(
async def setup_menu(current_user: dict = Depends(require_director)):
"""Create the standard app menu (开始填报 + 绑定账号)."""
buttons = [
{
"type": "view",
"name": "📝 开始填报",
"url": "https://qj.dhdx.fun/m",
},
{
"type": "view",
"name": "🔗 绑定账号",
"url": "https://qj.dhdx.fun/bind-wechat",
},
{"type": "view", "name": "开始填报", "url": "https://qj.dhdx.fun/m"},
{"type": "view", "name": "绑定账号", "url": "https://qj.dhdx.fun/bind-wechat"},
]
success = await wecom_client.create_menu(buttons)
return {"success": success, "message": "菜单已部署" if success else "菜单创建失败"}
+1
View File
@@ -36,6 +36,7 @@ class Settings(BaseSettings):
WECOM_SECRET: str = ""
WECOM_TOKEN: str = ""
WECOM_ENCODING_AES_KEY: str = ""
WECOM_API_BASE: str = "https://qyapi.weixin.qq.com"
# AI / LLM (OpenAI-compatible)
AI_API_URL: str = ""
+10 -9
View File
@@ -19,7 +19,7 @@ class WecomClient:
if self._access_token and time.time() < self._token_expires_at - 60:
return self._access_token
url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={self.corp_id}&corpsecret={self.secret}"
url = f"{settings.WECOM_API_BASE}/cgi-bin/gettoken?corpid={self.corp_id}&corpsecret={self.secret}"
async with httpx.AsyncClient() as client:
resp = await client.get(url, timeout=10)
data = resp.json()
@@ -35,7 +35,8 @@ class WecomClient:
for attempt in range(max_retries):
try:
token = await self._get_token()
full_url = f"{url}?access_token={token}"
sep = "&" if "?" in url else "?"
full_url = f"{url}{sep}access_token={token}"
async with httpx.AsyncClient() as client:
resp = await client.post(full_url, json=body, timeout=10)
data = resp.json()
@@ -70,7 +71,7 @@ class WecomClient:
"""Exchange OAuth2 code for userid (used in silent login)."""
try:
token = await self._get_token()
url = f"https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token={token}&code={code}"
url = f"{settings.WECOM_API_BASE}/cgi-bin/auth/getuserinfo?access_token={token}&code={code}"
async with httpx.AsyncClient() as client:
resp = await client.get(url, timeout=10)
data = resp.json()
@@ -92,7 +93,7 @@ class WecomClient:
"text": {"content": content},
}
await self._post_with_retry(
"https://qyapi.weixin.qq.com/cgi-bin/message/send", body
f"{settings.WECOM_API_BASE}/cgi-bin/message/send", body
)
return True
except Exception:
@@ -110,7 +111,7 @@ class WecomClient:
"markdown": {"content": content},
}
await self._post_with_retry(
"https://qyapi.weixin.qq.com/cgi-bin/message/send", body
f"{settings.WECOM_API_BASE}/cgi-bin/message/send", body
)
return True
except Exception:
@@ -149,7 +150,7 @@ class WecomClient:
},
}
await self._post_with_retry(
"https://qyapi.weixin.qq.com/cgi-bin/message/send", body
f"{settings.WECOM_API_BASE}/cgi-bin/message/send", body
)
return True
except Exception:
@@ -165,7 +166,7 @@ class WecomClient:
try:
body = {"button": buttons}
await self._post_with_retry(
f"https://qyapi.weixin.qq.com/cgi-bin/menu/create?agentid={settings.WECOM_AGENT_ID}",
f"{settings.WECOM_API_BASE}/cgi-bin/menu/create?agentid={settings.WECOM_AGENT_ID}",
body,
)
return True
@@ -178,7 +179,7 @@ class WecomClient:
return None
try:
token = await self._get_token()
url = f"https://qyapi.weixin.qq.com/cgi-bin/menu/get?access_token={token}&agentid={settings.WECOM_AGENT_ID}"
url = f"{settings.WECOM_API_BASE}/cgi-bin/menu/get?access_token={token}&agentid={settings.WECOM_AGENT_ID}"
async with httpx.AsyncClient() as client:
resp = await client.get(url, timeout=10)
data = resp.json()
@@ -194,7 +195,7 @@ class WecomClient:
return False
try:
token = await self._get_token()
url = f"https://qyapi.weixin.qq.com/cgi-bin/menu/delete?access_token={token}&agentid={settings.WECOM_AGENT_ID}"
url = f"{settings.WECOM_API_BASE}/cgi-bin/menu/delete?access_token={token}&agentid={settings.WECOM_AGENT_ID}"
async with httpx.AsyncClient() as client:
resp = await client.get(url, timeout=10)
data = resp.json()