Files
yuqei 124a18a4ec
Deploy VitePress Docs / build (push) Has been cancelled
Deploy VitePress Docs / deploy (push) Has been cancelled
Initialize cc-haha project mirror.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 13:41:51 +08:00

152 lines
5.1 KiB
TypeScript

/**
* Feishu CardKit API 错误码解析与谓词
*
* 参考实现: openclaw-lark/src/card/card-error.ts + src/core/api-error.ts
*
* Lark SDK 抛出的错误对象结构有多种:
* - SDK 把 Feishu 的 {code, msg} 直接挂在 error 对象上
* - Axios 风格: error.response.data.{code, msg}
* - data.code 嵌套(某些包装层)
*
* 此模块把这些统一成 { code, subCode, errMsg } 结构,
* 供 streaming-card-controller 判断是否跳帧重试、或降级到 Patch 路径。
*/
// ---------------------------------------------------------------------------
// Error code constants
// ---------------------------------------------------------------------------
/** 卡片 API 级别错误码。 */
export const CARD_ERROR = {
/** 发送频率限制。需跳过当前帧,下次 flush 继续。 */
RATE_LIMITED: 230020,
/** 卡片内容创建失败(通用码,需看子错误确认具体原因)。 */
CARD_CONTENT_FAILED: 230099,
} as const
/**
* 230099 的子错误码,嵌套在 msg 的 `ErrCode: xxx` 字段中。
* 11310 是通用的"元素超限"码,需配合 errMsg 匹配具体原因。
*/
export const CARD_CONTENT_SUB_ERROR = {
/** 卡片元素(表格等)数量超限 */
ELEMENT_LIMIT: 11310,
} as const
// ---------------------------------------------------------------------------
// Code extraction
// ---------------------------------------------------------------------------
function coerceCode(value: unknown): number | undefined {
if (typeof value === 'number' && Number.isFinite(value)) return value
if (typeof value === 'string') {
const parsed = Number(value)
if (Number.isFinite(parsed)) return parsed
}
return undefined
}
/**
* 从 Lark SDK 抛错对象中提取飞书 API code。支持三种结构:
* - `{ code }` (SDK 直接挂载)
* - `{ data: { code } }` (响应体嵌套)
* - `{ response: { data: { code } } }` (Axios 风格)
*/
export function extractLarkApiCode(err: unknown): number | undefined {
if (!err || typeof err !== 'object') return undefined
const e = err as {
code?: unknown
data?: { code?: unknown }
response?: { data?: { code?: unknown } }
}
return coerceCode(e.code) ?? coerceCode(e.data?.code) ?? coerceCode(e.response?.data?.code)
}
// ---------------------------------------------------------------------------
// Sub-error extraction
// ---------------------------------------------------------------------------
/**
* 从 msg 字符串里提取子错误码(`ErrCode: xxx`)。
*
* 示例输入:
* "Failed to create card content, ext=ErrCode: 11310; ErrMsg: card table number over limit; ..."
* 返回: 11310
*/
export function extractSubCode(msg: string): number | null {
const match = /ErrCode:\s*(\d+)/.exec(msg)
if (!match) return null
const code = Number(match[1])
return Number.isFinite(code) ? code : null
}
// ---------------------------------------------------------------------------
// Structured error parsing
// ---------------------------------------------------------------------------
export type CardApiErrorInfo = {
code: number
subCode: number | null
errMsg: string
}
/**
* 从任意抛错对象中解析卡片 API 错误结构。
*
* 返回 { code, subCode, errMsg }。无法提取 code 时返回 null。
*/
export function parseCardApiError(err: unknown): CardApiErrorInfo | null {
const code = extractLarkApiCode(err)
if (code === undefined) return null
// 按优先级提取 msg 文本
let errMsg = ''
if (err && typeof err === 'object') {
const e = err as {
msg?: unknown
message?: unknown
response?: { data?: { msg?: unknown } }
}
if (typeof e.msg === 'string') {
errMsg = e.msg
} else if (typeof e.response?.data?.msg === 'string') {
errMsg = e.response.data.msg
} else if (typeof e.message === 'string') {
errMsg = e.message
}
}
const subCode = extractSubCode(errMsg)
return { code, subCode, errMsg }
}
// ---------------------------------------------------------------------------
// Helper predicates
// ---------------------------------------------------------------------------
/** 判断错误是否为卡片发送频率限制(230020)。 */
export function isCardRateLimitError(err: unknown): boolean {
const parsed = parseCardApiError(err)
if (!parsed) return false
return parsed.code === CARD_ERROR.RATE_LIMITED
}
/**
* 判断错误是否为卡片表格数超限。
*
* 匹配条件: code 230099 + subCode 11310 + errMsg 含 "table number over limit"
* (11310 是通用元素超限码,光靠它不够;必须同时检查 errMsg 锁定是表格数量问题)。
*
* 实际生产错误格式(openclaw-lark 2026-03 实测):
* "Failed to create card content, ext=ErrCode: 11310; ErrMsg: card table number over limit; ErrorValue: table; "
*/
export function isCardTableLimitError(err: unknown): boolean {
const parsed = parseCardApiError(err)
if (!parsed) return false
return (
parsed.code === CARD_ERROR.CARD_CONTENT_FAILED &&
parsed.subCode === CARD_CONTENT_SUB_ERROR.ELEMENT_LIMIT &&
/table number over limit/i.test(parsed.errMsg)
)
}