Files
DadaoTask/backend/app/errors.py
T
Steven 90b99f8253
CI / backend (push) Canceled after 0s
feat(integrations): 微信里发一句话,变成一条任务或知识
一条通道一张令牌,机器人把消息 POST 到 /integrations/clawbot/{token},
正文按前缀落到任务或知识草稿:没有前缀就是任务,「知识:」「笔记:」记成知识,
「帮助」只回用法。首行当标题、其余当正文,`#标签` 摘出来挂到事项上。
同一个 msg_id 只处理一次——微信会重推,重推不能变成两条任务。

为什么不塞进 system_setting 的 JSON:一个工作区可能挂多个通道,还得能单独停用。
令牌库里只存 sha256,只在创建/重新生成时显示一次;收消息时还不知道是哪个工作区,
所以 bot_channel 的 RLS 额外放行 token_hash = current_setting('app.bot_token_hash'),
读到通道再 bind_tenant 进上下文,按通道创建者记账。

- 后端:models_integrations.py(bot_channel / bot_message)、迁移 0012(RLS)、
  api/integrations.py(收消息 + 管理接口)、错误码 9002/9003
- Web:/w/:wsId/integrations「接入」页——建通道、复制回调地址与令牌、最近消息、
  停用/换令牌/删除,只有 owner/admin 能改
- 测试:test_clawbot.py 10 例;web/e2e/integrations.mjs 7 项(冒充 clawbot 真推一条)

验证:pytest 150 passed,ruff check/format 全绿,tsc 0,vite build 通过,e2e 7 项全绿。
2026-11-05 22:04:34 +08:00

167 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""错误码与异常。错误码一旦对外发布就**不许改号**(客户端靠它做分支)。"""
from __future__ import annotations
from enum import IntEnum
from typing import Any
class ErrorCode(IntEnum):
OK = 0
# 1xxx 通用
BAD_REQUEST = 1001
VALIDATION_FAILED = 1002
NOT_FOUND = 1004
CONFLICT = 1009
PAYLOAD_TOO_LARGE = 1013
RATE_LIMITED = 1029
INTERNAL = 1500
# 2xxx 认证 / 账号
UNAUTHORIZED = 2001
TOKEN_EXPIRED = 2002
TOKEN_INVALID = 2003
SMS_CODE_INVALID = 2010
SMS_TOO_FREQUENT = 2011
ACCOUNT_DISABLED = 2012
PASSWORD_INVALID = 2013
PASSWORD_NOT_SET = 2014
PASSWORD_WEAK = 2015
LOGIN_TOO_MANY = 2016
LOGIN_METHOD_DISABLED = 2017
EMAIL_INVALID = 2018
EMAIL_TAKEN = 2019
MAIL_SEND_FAILED = 2020
EMAIL_CODE_INVALID = 2021
EMAIL_CODE_TOO_FREQUENT = 2022
# 3xxx 租户 / 权限
WORKSPACE_NOT_FOUND = 3001
NOT_A_MEMBER = 3003
INSUFFICIENT_ROLE = 3004
ALREADY_A_MEMBER = 3005
INVITATION_INVALID = 3010
# 4xxx 并发
VERSION_CONFLICT = 4009
# 5xxx 任务域
ITEM_NOT_FOUND = 5001
ITEM_STATUS_INVALID = 5002
TAG_NOT_FOUND = 5003
LINK_NOT_FOUND = 5004
LINK_CONFLICT = 5005
# 6xxx 工单域
TICKET_NOT_FOUND = 6001
TICKET_ALREADY_EXISTS = 6002
TICKET_STATUS_INVALID = 6003
TICKET_FORBIDDEN = 6004
NOTIFICATION_NOT_FOUND = 6005
# 7xxx 知识域
KNOWLEDGE_NOT_FOUND = 7001
KNOWLEDGE_ALREADY_EXISTS = 7002
KNOWLEDGE_STATUS_INVALID = 7003
KNOWLEDGE_FORBIDDEN = 7004
KNOWLEDGE_SOURCE_REQUIRED = 7005
KNOWLEDGE_CATEGORY_NOT_FOUND = 7006
KNOWLEDGE_CATEGORY_DEPTH = 7007
KNOWLEDGE_CATEGORY_CONFLICT = 7008
# 8xxx 附件
ATTACHMENT_NOT_FOUND = 8001
ATTACHMENT_TOO_LARGE = 8002
ATTACHMENT_FORBIDDEN = 8003
CONFIRM_REQUIRED = 8004
# 9xxx 客户端发布(OTA)
APP_RELEASE_NOT_FOUND = 9001
# 9xxx 外部接入(微信 clawbot 等)
BOT_TOKEN_INVALID = 9002
BOT_DISABLED = 9003
# 默认 HTTP 状态:业务码自己带语义,HTTP 只是传输层
_HTTP_STATUS: dict[ErrorCode, int] = {
ErrorCode.OK: 200,
ErrorCode.BAD_REQUEST: 400,
ErrorCode.VALIDATION_FAILED: 422,
ErrorCode.NOT_FOUND: 404,
ErrorCode.CONFLICT: 409,
ErrorCode.PAYLOAD_TOO_LARGE: 413,
ErrorCode.RATE_LIMITED: 429,
ErrorCode.INTERNAL: 500,
ErrorCode.UNAUTHORIZED: 401,
ErrorCode.TOKEN_EXPIRED: 401,
ErrorCode.TOKEN_INVALID: 401,
ErrorCode.SMS_CODE_INVALID: 400,
ErrorCode.SMS_TOO_FREQUENT: 429,
ErrorCode.ACCOUNT_DISABLED: 403,
ErrorCode.PASSWORD_INVALID: 400,
ErrorCode.PASSWORD_NOT_SET: 400,
ErrorCode.PASSWORD_WEAK: 422,
ErrorCode.LOGIN_TOO_MANY: 429,
ErrorCode.LOGIN_METHOD_DISABLED: 403,
ErrorCode.EMAIL_INVALID: 422,
ErrorCode.EMAIL_TAKEN: 409,
ErrorCode.MAIL_SEND_FAILED: 502,
ErrorCode.EMAIL_CODE_INVALID: 400,
ErrorCode.EMAIL_CODE_TOO_FREQUENT: 429,
ErrorCode.WORKSPACE_NOT_FOUND: 404,
ErrorCode.NOT_A_MEMBER: 403,
ErrorCode.INSUFFICIENT_ROLE: 403,
ErrorCode.ALREADY_A_MEMBER: 409,
ErrorCode.INVITATION_INVALID: 400,
ErrorCode.VERSION_CONFLICT: 409,
ErrorCode.ITEM_NOT_FOUND: 404,
ErrorCode.ITEM_STATUS_INVALID: 409,
ErrorCode.TAG_NOT_FOUND: 404,
ErrorCode.LINK_NOT_FOUND: 404,
ErrorCode.LINK_CONFLICT: 409,
ErrorCode.TICKET_NOT_FOUND: 404,
ErrorCode.TICKET_ALREADY_EXISTS: 409,
ErrorCode.TICKET_STATUS_INVALID: 409,
ErrorCode.TICKET_FORBIDDEN: 403,
ErrorCode.NOTIFICATION_NOT_FOUND: 404,
ErrorCode.KNOWLEDGE_NOT_FOUND: 404,
ErrorCode.KNOWLEDGE_ALREADY_EXISTS: 409,
ErrorCode.KNOWLEDGE_STATUS_INVALID: 409,
ErrorCode.KNOWLEDGE_FORBIDDEN: 403,
ErrorCode.KNOWLEDGE_SOURCE_REQUIRED: 409,
ErrorCode.KNOWLEDGE_CATEGORY_NOT_FOUND: 404,
ErrorCode.KNOWLEDGE_CATEGORY_DEPTH: 422,
ErrorCode.KNOWLEDGE_CATEGORY_CONFLICT: 409,
ErrorCode.ATTACHMENT_NOT_FOUND: 404,
ErrorCode.ATTACHMENT_TOO_LARGE: 413,
ErrorCode.ATTACHMENT_FORBIDDEN: 403,
ErrorCode.CONFIRM_REQUIRED: 409,
ErrorCode.APP_RELEASE_NOT_FOUND: 404,
ErrorCode.BOT_TOKEN_INVALID: 404,
ErrorCode.BOT_DISABLED: 403,
}
class AppError(Exception):
"""业务异常。__str__ 只用于日志,对外一律走 code + message。"""
def __init__(
self,
code: ErrorCode,
message: str | None = None,
*,
http_status: int | None = None,
details: Any = None,
) -> None:
self.code = code
self.message = message or code.name
self.http_status = http_status or _HTTP_STATUS.get(code, 400)
self.details = details
super().__init__(f"[{int(code)}] {self.message}")
def http_status_for(code: ErrorCode) -> int:
return _HTTP_STATUS.get(code, 400)