aff2c5f6ea
In the admin publish modal, fetch the article's publish records, disable accounts that already succeeded for the same article/platform, label queued/publishing accounts inline, and route the success notification by created vs existing task counts (with a dedicated warning for the `desktop_publish_duplicate` server error). Replace the "再次发送" mutation/popconfirm on both admin and desktop publish management views with a "重新创建发布任务" action that calls the same Create endpoint, only enables on failed/aborted/unknown rows, and reports created vs existing task counts (with a tailored warning for `unknown` results that may have already succeeded). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
185 lines
8.3 KiB
TypeScript
185 lines
8.3 KiB
TypeScript
import { Modal } from 'ant-design-vue'
|
|
import { ApiClientError } from '@geo/http-client'
|
|
|
|
import { router } from '@/router'
|
|
|
|
const authSessionErrorMessages = new Set([
|
|
'not_authenticated',
|
|
'missing_bearer_token',
|
|
'invalid_access_token',
|
|
'invalid_refresh_token',
|
|
'refresh_session_expired',
|
|
'refresh_token_mismatch',
|
|
'token_revoked',
|
|
])
|
|
|
|
const errorMessageMap: Record<string, string> = {
|
|
invalid_credentials: '邮箱或密码错误',
|
|
invalid_old_password: '原密码错误',
|
|
weak_password: '新密码至少 8 位',
|
|
login_rate_limited: '登录请求过于频繁',
|
|
login_locked: '登录已被临时保护',
|
|
login_in_progress: '登录请求正在处理中',
|
|
login_guard_unavailable: '登录保护暂时不可用',
|
|
no_tenant: '当前账号未关联租户',
|
|
not_authenticated: '请先登录',
|
|
missing_bearer_token: '请先登录',
|
|
invalid_access_token: '登录状态已失效',
|
|
user_disabled: '用户被停用,请联系客服',
|
|
invalid_refresh_token: '刷新令牌已失效',
|
|
refresh_session_expired: '登录已过期,请重新登录',
|
|
refresh_token_mismatch: '刷新令牌校验失败,请重新登录',
|
|
token_revoked: '当前会话已经退出',
|
|
tenant_scope_missing: '租户上下文缺失',
|
|
query_failed: '数据读取失败',
|
|
quota_insufficient: '生成额度不足',
|
|
ai_points_insufficient: 'AI 点数余额不足',
|
|
ai_points_unavailable: 'AI 点数服务暂不可用,请稍后重试',
|
|
trial_plan_expired: '免费试用已到期,请联系管理员开通',
|
|
subscription_required: '当前租户尚未开通有效套餐,请联系管理员',
|
|
subscription_inactive: '当前租户套餐已失效,请联系管理员',
|
|
llm_unavailable: '生成服务不可用',
|
|
analyze_context_required: '请先填写品牌或模板上下文信息',
|
|
title_context_required: '请先确认品牌、关键词或竞品信息',
|
|
outline_context_required: '请先确认标题和文章结构后再生成大纲',
|
|
analyze_queue_unavailable: '品牌分析任务较多,请稍后重试',
|
|
title_queue_unavailable: '标题生成任务较多,请稍后重试',
|
|
outline_queue_unavailable: '大纲生成任务较多,请稍后重试',
|
|
analyze_task_not_found: '品牌分析任务不存在或已失效',
|
|
title_task_not_found: '标题生成任务不存在或已失效',
|
|
outline_task_not_found: '文章大纲任务不存在或已失效',
|
|
article_generating: '文章仍在生成中',
|
|
article_not_editable: '只有已完成文章才可编辑',
|
|
article_lookup_failed: '文章状态读取失败',
|
|
draft_not_editable: '只有草稿文章才可继续在向导中编辑',
|
|
article_title_required: '请输入文章标题',
|
|
invalid_image: '请选择图片文件',
|
|
image_upload_too_large: '图片不能超过 10MB',
|
|
image_upload_type_not_supported: '仅支持 PNG、JPG、JPEG、GIF、WebP、BMP、SVG、AVIF 图片',
|
|
image_canvas_context_unavailable: '当前浏览器暂不支持该图片处理能力,请更换浏览器后重试',
|
|
image_load_failed: '图片解析失败,请重新选择图片',
|
|
article_image_too_large: '图片不能超过 10MB',
|
|
article_image_type_not_supported: '仅支持 PNG、JPG、GIF、WebP 图片',
|
|
article_image_upload_failed: '图片上传失败',
|
|
article_image_url_unavailable: '图片地址生成失败',
|
|
object_storage_unavailable: '对象存储未配置或当前不可用',
|
|
invalid_payload: '请求参数不合法',
|
|
update_failed: '保存文章失败',
|
|
brand_exists: '品牌名称已存在',
|
|
brand_limit_reached: '品牌公司数量已达当前套餐上限',
|
|
keyword_exists: '关键词已存在',
|
|
keyword_limit_reached: '关键词数量已达当前套餐上限',
|
|
question_exists: '该品牌下已存在相同问题',
|
|
no_valid_questions: '没有可保存的问题,请调整候选后重试',
|
|
invalid_enum: '请求枚举值不合法',
|
|
llm_timeout: 'AI 扩展超时,AI 点已退还',
|
|
llm_invalid_output: 'AI 输出无法解析,AI 点已退还',
|
|
materialize_failed: '保存问题失败,请稍后重试',
|
|
question_limit_reached: '当前账号问题数量已达上限',
|
|
knowledge_text_name_too_long: '文本名称不能超过 20 个字',
|
|
brand_not_found: '品牌不存在或已删除',
|
|
keyword_not_found: '关键词不存在或已删除',
|
|
publish_cover_required: '当前选择的平台要求上传封面图,请先上传封面图',
|
|
desktop_account_client_missing: '所选账号还没有绑定桌面客户端,暂时无法发布',
|
|
desktop_account_not_found: '所选桌面账号不存在,请刷新后重试',
|
|
desktop_client_offline: '当前登录账号的桌面客户端未在线,请先打开 desktop-client',
|
|
desktop_publish_duplicate: '内容已在发布队列中,未重复提交',
|
|
desktop_publish_job_store_unavailable: '发布队列暂时不可用,请稍后重试',
|
|
compliance_blocked: '内容合规检测已阻断本次发布',
|
|
compliance_needs_ack: '内容合规检测需要确认风险后才能发布',
|
|
compliance_ack_mismatch: '合规确认已失效,请重新检测并确认',
|
|
compliance_engine_unavailable: '合规检测服务暂时不可用',
|
|
compliance_manual_review_pending: '该版本正在等待运营审阅',
|
|
compliance_manual_review_rejected: '该版本已被运营驳回,请修改后重新保存',
|
|
compliance_manual_review_terminal: '该版本已有终态审阅结果',
|
|
compliance_manual_review_not_cancellable: '当前审阅状态不能撤回',
|
|
desktop_media_api_removed: '旧版媒体接口已下线,请刷新页面后重试',
|
|
publisher_plugin_timeout: '浏览器插件响应超时,请刷新当前页面后重试',
|
|
publisher_plugin_empty_response: '浏览器插件未返回数据,请刷新当前页面后重试',
|
|
publisher_plugin_invalid_response: '浏览器插件返回了无效数据,请刷新当前页面后重试',
|
|
publisher_plugin_unknown_error: '浏览器插件调用失败,请稍后重试',
|
|
installation_offline: '桌面客户端未在线,请打开桌面客户端后重试',
|
|
network_error: '网络连接失败,请稍后重试',
|
|
request_timeout: '请求超时,AI 生成耗时较长,请稍后重试或稍候再查看结果',
|
|
unknown_error: '发生未知错误',
|
|
}
|
|
|
|
export function isAiPointsInsufficient(error: unknown): boolean {
|
|
return error instanceof ApiClientError && error.message === 'ai_points_insufficient'
|
|
}
|
|
|
|
let aiPointsModalVisible = false
|
|
|
|
export function showAiPointsInsufficientModal(): void {
|
|
if (aiPointsModalVisible) {
|
|
return
|
|
}
|
|
aiPointsModalVisible = true
|
|
|
|
Modal.confirm({
|
|
title: 'AI 点数不足',
|
|
content: '当前账号的 AI 点数余额已耗尽,无法继续生成内容。请升级套餐或等待下月点数重置。',
|
|
okText: '查看点数明细',
|
|
cancelText: '我知道了',
|
|
centered: true,
|
|
okButtonProps: { type: 'primary' },
|
|
onOk: () => {
|
|
void router.push('/account/ai-points')
|
|
},
|
|
afterClose: () => {
|
|
aiPointsModalVisible = false
|
|
},
|
|
})
|
|
}
|
|
|
|
export function isHandledAuthError(error: unknown): boolean {
|
|
return (
|
|
error instanceof ApiClientError &&
|
|
error.status === 401 &&
|
|
(error.handled === true || authSessionErrorMessages.has(error.message))
|
|
)
|
|
}
|
|
|
|
const TIMEOUT_MESSAGE_PATTERN = /^timeout of \d+ms exceeded$/i
|
|
const VALIDATION_MIN_SEED_TOPIC_PATTERN =
|
|
/QuestionDistillRequest\.SeedTopic|seed_topic.*min|SeedTopic.*min/i
|
|
|
|
function translateRawErrorMessage(raw: string): string | null {
|
|
const mapped = errorMessageMap[raw]
|
|
if (mapped) {
|
|
return mapped
|
|
}
|
|
if (TIMEOUT_MESSAGE_PATTERN.test(raw)) {
|
|
return errorMessageMap.request_timeout
|
|
}
|
|
return null
|
|
}
|
|
|
|
export function formatError(error: unknown): string {
|
|
if (isHandledAuthError(error)) {
|
|
return '登录已过期,请重新登录'
|
|
}
|
|
|
|
if (error instanceof ApiClientError) {
|
|
if (
|
|
error.message === 'invalid_seed_topic' ||
|
|
(error.message === 'invalid_params' &&
|
|
VALIDATION_MIN_SEED_TOPIC_PATTERN.test(error.detail ?? ''))
|
|
) {
|
|
return '用户搜索词太短,请输入至少 2 个字的搜索词'
|
|
}
|
|
const translated = translateRawErrorMessage(error.message)
|
|
const message = translated ?? error.message.replaceAll('_', ' ')
|
|
if (isAiPointsInsufficient(error)) {
|
|
return message
|
|
}
|
|
return error.detail ? `${message}: ${error.detail}` : message
|
|
}
|
|
|
|
if (error instanceof Error) {
|
|
return translateRawErrorMessage(error.message) ?? error.message
|
|
}
|
|
|
|
return '发生未知错误'
|
|
}
|