feat: CVAT 标注引擎、我的标注收件箱与 ADAS Cuboid 送标
- 统一标注引擎为 CVAT:客户端/配置/格式转换、iframe 标注页、docker-compose.cvat.yml 与 no_auth 补丁 - 移除 Label Studio 相关配置与构建脚本,清理 embedded.bak 备份与误提交的 node_modules - 新增「我的标注」:跨 Campaign 收件箱、逐张清单、CVAT frame 跳转 - 飞书任务分配:通讯录同步选人、按量分配、分配后 DM 通知(含 my-tasks 链接) - ADAS cuboid_7cls 数据湖接入:workflow 路径、register-batch、开标上传与标注同步 - 数据湖挂载 AS_DATA_LAKE_ROOT、datasets/adas 符号链接、reset_labeling 运维脚本 - 补充 docs/HANDOVER.md 项目交接文档 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
@@ -9,8 +10,58 @@ import httpx
|
||||
from as_platform.auth.feishu import _get_tenant_access_token
|
||||
from as_platform.config import FEISHU_APP_ID, FEISHU_APP_SECRET, FEISHU_LABELING_CHAT_ID
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
IM_MSG_URL = "https://open.feishu.cn/open-apis/im/v1/messages"
|
||||
|
||||
IM_MESSAGE_SCOPE_URL = (
|
||||
f"https://open.feishu.cn/app/{FEISHU_APP_ID}/auth"
|
||||
"?q=im:message:send_as_bot,im:message:send,im:message"
|
||||
"&op_from=openapi&token_type=tenant"
|
||||
if FEISHU_APP_ID
|
||||
else ""
|
||||
)
|
||||
|
||||
FEISHU_BOT_SETUP_URL = f"https://open.feishu.cn/app/{FEISHU_APP_ID}/bot" if FEISHU_APP_ID else ""
|
||||
FEISHU_PUBLISH_URL = f"https://open.feishu.cn/app/{FEISHU_APP_ID}/appPublish" if FEISHU_APP_ID else ""
|
||||
|
||||
|
||||
def _feishu_send_error_hint(message: str) -> dict[str, str]:
|
||||
"""将飞书 API 英文报错映射为可操作的中文提示。"""
|
||||
msg = message or ""
|
||||
if "Bot ability is not activated" in msg:
|
||||
return {
|
||||
"reason": "bot_not_activated",
|
||||
"help_text": "应用未启用「机器人」能力:开放平台 → 应用能力 → 添加机器人 → 创建版本并发布",
|
||||
"help_url": FEISHU_BOT_SETUP_URL or IM_MESSAGE_SCOPE_URL,
|
||||
}
|
||||
if "NO availability" in msg or "230013" in msg:
|
||||
return {
|
||||
"reason": "user_out_of_scope",
|
||||
"help_text": "你在机器人可用范围外:版本管理与发布 → 编辑可用范围 → 加入全员或你的部门 → 重新发布",
|
||||
"help_url": FEISHU_PUBLISH_URL or IM_MESSAGE_SCOPE_URL,
|
||||
}
|
||||
if "im:message" in msg or "Access denied" in msg:
|
||||
return {
|
||||
"reason": "missing_im_scope",
|
||||
"help_text": "应用未开通「发送消息」权限,请在权限管理中申请 im:message:send_as_bot",
|
||||
"help_url": IM_MESSAGE_SCOPE_URL,
|
||||
}
|
||||
return {
|
||||
"reason": "send_failed",
|
||||
"help_text": msg[:120],
|
||||
"help_url": IM_MESSAGE_SCOPE_URL,
|
||||
}
|
||||
|
||||
|
||||
def _parse_feishu_send_response(resp: httpx.Response) -> dict[str, Any]:
|
||||
data = resp.json() if resp.content else {}
|
||||
if resp.status_code < 400 and data.get("code") in (0, None):
|
||||
return {"ok": True}
|
||||
msg = data.get("msg") or f"HTTP {resp.status_code}"
|
||||
hint = _feishu_send_error_hint(msg)
|
||||
return {"ok": False, "message": msg, **hint}
|
||||
|
||||
|
||||
def is_notify_configured() -> bool:
|
||||
return bool(FEISHU_APP_ID and FEISHU_APP_SECRET and FEISHU_LABELING_CHAT_ID)
|
||||
@@ -32,23 +83,133 @@ def send_chat_text(text: str) -> dict[str, Any]:
|
||||
},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if data.get("code") != 0:
|
||||
return {"ok": False, "message": data.get("msg") or "send failed"}
|
||||
return {"ok": True}
|
||||
return _parse_feishu_send_response(resp)
|
||||
|
||||
|
||||
def send_chat_async(text: str) -> None:
|
||||
"""异步发送飞书消息,不阻塞主流程。"""
|
||||
"""异步发送飞书群消息,不阻塞主流程。"""
|
||||
import threading
|
||||
threading.Thread(target=_send_safe, args=(text,), daemon=True, name="feishu-notify").start()
|
||||
threading.Thread(target=_send_chat_safe, args=(text,), daemon=True, name="feishu-notify").start()
|
||||
|
||||
|
||||
def _send_safe(text: str) -> None:
|
||||
def send_user_text(open_id: str, text: str) -> dict[str, Any]:
|
||||
"""向指定飞书用户发送私聊消息(需应用具备 im:message 权限)。"""
|
||||
if not FEISHU_APP_ID or not FEISHU_APP_SECRET:
|
||||
return {"ok": False, "message": "飞书未配置"}
|
||||
if not open_id:
|
||||
return {"ok": False, "message": "open_id 为空"}
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
token = _get_tenant_access_token(client)
|
||||
resp = client.post(
|
||||
IM_MSG_URL,
|
||||
params={"receive_id_type": "open_id"},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"receive_id": open_id,
|
||||
"msg_type": "text",
|
||||
"content": json.dumps({"text": text}, ensure_ascii=False),
|
||||
},
|
||||
)
|
||||
data = resp.json() if resp.content else {}
|
||||
if resp.status_code >= 400 or data.get("code") not in (0, None):
|
||||
result = _parse_feishu_send_response(resp)
|
||||
logger.warning(
|
||||
"飞书私聊发送失败 open_id=%s reason=%s: %s",
|
||||
open_id[:12],
|
||||
result.get("reason"),
|
||||
result.get("message"),
|
||||
)
|
||||
return result
|
||||
return {"ok": True}
|
||||
except Exception as exc:
|
||||
logger.warning("飞书私聊发送异常 open_id=%s: %s", open_id[:12], exc)
|
||||
hint = _feishu_send_error_hint(str(exc))
|
||||
return {"ok": False, "message": str(exc), **hint}
|
||||
|
||||
|
||||
def send_user_async(open_id: str, text: str) -> None:
|
||||
"""异步向飞书用户发私聊,不阻塞主流程。"""
|
||||
import threading
|
||||
threading.Thread(
|
||||
target=_send_user_safe,
|
||||
args=(open_id, text),
|
||||
daemon=True,
|
||||
name="feishu-user-notify",
|
||||
).start()
|
||||
|
||||
|
||||
def _send_chat_safe(text: str) -> None:
|
||||
try:
|
||||
send_chat_text(text)
|
||||
except Exception:
|
||||
pass # 通知失败不影响业务流程
|
||||
pass
|
||||
|
||||
|
||||
def _send_user_safe(open_id: str, text: str) -> None:
|
||||
try:
|
||||
send_user_text(open_id, text)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def notify_labeling_assignment(
|
||||
*,
|
||||
open_id: str,
|
||||
assignee_name: str,
|
||||
task: str,
|
||||
batch: str,
|
||||
count: int,
|
||||
campaign_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""分配标注任务后向被指派人发送飞书私聊通知,返回发送结果。"""
|
||||
from as_platform.config import FRONTEND_URL
|
||||
|
||||
link = f"{FRONTEND_URL.rstrip('/')}/labeling/my-tasks?campaign={campaign_id}"
|
||||
text = (
|
||||
f"[HSAP] 您有新的标注任务\n"
|
||||
f"被指派人: {assignee_name}\n"
|
||||
f"任务: {task} / 批次: {batch}\n"
|
||||
f"分配数量: {count} 张\n"
|
||||
f"请打开我的标注: {link}"
|
||||
)
|
||||
result = send_user_text(open_id, text)
|
||||
if not result.get("ok") and is_notify_configured():
|
||||
# 私聊失败时尝试发到标注协作群
|
||||
fallback = send_chat_text(
|
||||
f"[HSAP] 任务分配通知\n"
|
||||
f"@{assignee_name} 您有 {count} 张新标注任务\n"
|
||||
f"任务: {task} / 批次: {batch}\n"
|
||||
f"打开我的标注: {link}"
|
||||
)
|
||||
if fallback.get("ok"):
|
||||
return {"ok": True, "channel": "chat", "name": assignee_name}
|
||||
return {**result, "channel": "dm", "name": assignee_name}
|
||||
|
||||
|
||||
def notify_labeling_assignment_async(
|
||||
*,
|
||||
open_id: str,
|
||||
assignee_name: str,
|
||||
task: str,
|
||||
batch: str,
|
||||
count: int,
|
||||
campaign_id: str,
|
||||
) -> None:
|
||||
import threading
|
||||
threading.Thread(
|
||||
target=notify_labeling_assignment,
|
||||
kwargs={
|
||||
"open_id": open_id,
|
||||
"assignee_name": assignee_name,
|
||||
"task": task,
|
||||
"batch": batch,
|
||||
"count": count,
|
||||
"campaign_id": campaign_id,
|
||||
},
|
||||
daemon=True,
|
||||
name="feishu-assign-notify",
|
||||
).start()
|
||||
|
||||
|
||||
def notify_batch_progress(
|
||||
|
||||
Reference in New Issue
Block a user