From f57c6e284bce5a5aafe74810ad3fd60d249bdf5d Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Tue, 13 Oct 2026 16:34:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9B=AE=E5=BD=95=E8=B5=B7?= =?UTF-8?q?=E8=8D=89=E7=9A=84=E5=87=A0=E5=A4=84=E8=AF=B4=E6=B3=95=E7=85=A7?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=E7=9A=84=E5=AE=9E=E9=99=85=E5=81=9A?= =?UTF-8?q?=E6=B3=95=E6=94=B9=EF=BC=9A=E7=82=B9=E5=81=9C=E6=AD=A2=E5=90=8E?= =?UTF-8?q?=E5=86=99=E3=80=8C=E6=AD=A3=E5=9C=A8=E5=81=9C=E6=AD=A2=EF=BC=9A?= =?UTF-8?q?=E5=8F=96=E6=B6=88=E8=BF=9B=E8=A1=8C=E4=B8=AD=E7=9A=84=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=B0=83=E7=94=A8=EF=BC=8C=E6=AD=A3=E5=9C=A8=E5=86=99?= =?UTF-8?q?=E5=85=A5=E7=9B=AE=E5=BD=95=E7=9A=84=E8=A1=A8=E5=86=99=E5=AE=8C?= =?UTF-8?q?=E5=90=8E=E5=81=9C=E6=AD=A2=E2=80=A6=E3=80=8D=EF=BC=88=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=AB=AF=E9=A9=AC=E4=B8=8A=E5=81=9C=E6=8E=89=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E4=B8=AD=E7=9A=84=E8=B0=83=E7=94=A8=E3=80=81=E5=8F=AA?= =?UTF-8?q?=E7=AD=89=E6=89=8B=E4=B8=8A=E9=82=A3=E5=BC=A0=E8=A1=A8=E5=86=99?= =?UTF-8?q?=E5=AE=8C=EF=BC=8C=E4=B8=8D=E6=98=AF=E7=AD=89=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E8=BF=99=E6=AC=A1=E8=B0=83=E7=94=A8=E7=BB=93=E6=9D=9F=EF=BC=89?= =?UTF-8?q?=EF=BC=9B=E4=BB=BB=E5=8A=A1=E6=95=B4=E4=BD=93=E5=87=BA=E9=94=99?= =?UTF-8?q?=E6=97=B6=E5=8E=9F=E5=9B=A0=E7=85=A7=E6=9C=8D=E5=8A=A1=E7=AB=AF?= =?UTF-8?q?=E7=9A=84=E5=8E=9F=E8=AF=9D=E5=9C=A8=E3=80=8C=E8=B5=B7=E8=8D=89?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E3=80=8D=E4=B8=8B=E9=9D=A2=E5=86=99=E4=B8=80?= =?UTF-8?q?=E6=AC=A1=EF=BC=88=E6=9C=8D=E5=8A=A1=E7=AB=AF=E7=9A=84=E5=8E=9F?= =?UTF-8?q?=E5=9B=A0=E6=9C=89=E7=9A=84=E8=87=AA=E5=B8=A6=E3=80=8C=E8=B5=B7?= =?UTF-8?q?=E8=8D=89=E4=BB=BB=E5=8A=A1=E5=87=BA=E9=94=99=EF=BC=9A=E3=80=8D?= =?UTF-8?q?=E5=BC=80=E5=A4=B4=EF=BC=8C=E7=95=8C=E9=9D=A2=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E5=A5=97=E4=B8=80=E5=B1=82=EF=BC=89=EF=BC=8C=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF=E7=BB=99=E6=B2=A1=E7=BB=93=E6=9D=9F=E7=9A=84=E8=A1=A8?= =?UTF-8?q?=E8=AE=B0=E7=9A=84=E5=90=8C=E4=B8=80=E5=8F=A5=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E9=80=90=E8=A1=A8=E9=87=8D=E5=A4=8D=EF=BC=88=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E9=87=8D=E5=90=AF=E4=B8=AD=E6=96=AD=E5=90=8C=E6=A0=B7=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E5=90=84=E6=9C=89=E5=8E=9F=E5=9B=A0=E7=9A=84=E7=85=A7?= =?UTF-8?q?=E5=86=99=EF=BC=9B=E3=80=8C=E5=8F=AA=E9=87=8D=E8=AF=95=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E7=9A=84=E8=A1=A8=E3=80=8D=E6=B2=A1=E5=BB=BA=E6=88=90?= =?UTF-8?q?=E5=92=8C=E5=BC=80=E5=A7=8B=E6=97=B6=E5=90=8C=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E8=AF=B4=E6=B3=95=E2=80=94=E2=80=94=E8=B5=B7=E8=8D=89=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E7=94=A8=E4=B8=8D=E4=BA=86=EF=BC=88=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF=E9=87=8D=E8=AF=95=E4=B9=9F=E5=9B=9E=20409=20catalog?= =?UTF-8?q?=5Fdraft=5Fmodel=5Funavailable=EF=BC=89=E7=85=A7=E5=8E=9F?= =?UTF-8?q?=E6=A0=B7=E5=86=99=E9=82=A3=E5=8F=A5=E8=AF=9D=E3=80=81=E9=95=BF?= =?UTF-8?q?=E5=8F=A5=E4=B8=8D=E5=86=8D=E6=8B=86=E6=88=90=E3=80=8C=E6=A0=87?= =?UTF-8?q?=E9=A2=98=EF=BC=9A=E5=8E=9F=E5=9B=A0=E3=80=8D=EF=BC=8C=E5=88=AB?= =?UTF-8?q?=E7=9A=84=E5=86=99=E3=80=8C=E6=9C=AA=E8=83=BD=E5=BC=80=E5=A7=8B?= =?UTF-8?q?=E8=B5=B7=E8=8D=89=E3=80=8D=EF=BC=9B=E6=8F=90=E7=A4=BA=E9=87=8C?= =?UTF-8?q?=E5=8E=9F=E5=9B=A0=E4=B8=BA=E7=A9=BA=E6=97=B6=E4=B8=8D=E7=95=99?= =?UTF-8?q?=E5=86=92=E5=8F=B7=E3=80=82check-catalog=20=E5=85=88=E8=A1=A5?= =?UTF-8?q?=E4=BC=9A=E5=A4=B1=E8=B4=A5=E7=9A=84=E6=A3=80=E6=9F=A5=EF=BC=9A?= =?UTF-8?q?=E5=81=9C=E6=AD=A2=E7=9A=84=E8=AF=B4=E6=B3=95=E3=80=81=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E6=97=B6=E8=B5=B7=E8=8D=89=E6=A8=A1=E5=9E=8B=E7=94=A8?= =?UTF-8?q?=E4=B8=8D=E4=BA=86=E3=80=81=E4=BB=BB=E5=8A=A1=E6=95=B4=E4=BD=93?= =?UTF-8?q?=E5=87=BA=E9=94=99=EF=BC=88=E5=8E=9F=E5=9B=A0=E5=8F=AA=E5=86=99?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E3=80=81=E9=80=90=E8=A1=A8=E4=B8=8D=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E3=80=81=E9=83=BD=E8=83=BD=E9=87=8D=E8=AF=95=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E6=9C=8D=E5=8A=A1=E9=87=8D=E5=90=AF=E4=B8=AD=E6=96=AD?= =?UTF-8?q?=E7=9A=84=E9=80=90=E8=A1=A8=E4=B8=8D=E5=86=8D=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E5=8E=9F=E5=9B=A0=EF=BC=9B=E5=81=87=E6=9C=8D=E5=8A=A1=E7=AB=AF?= =?UTF-8?q?=E7=9A=84=E4=B8=AD=E6=96=AD=E4=BB=BB=E5=8A=A1=E7=85=A7=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E8=AE=B0=E4=B8=8A=E5=90=8C=E4=B8=80=E5=8F=A5=E5=8E=9F?= =?UTF-8?q?=E5=9B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/client.ts | 3 +- frontend/src/lib/terms.ts | 8 ++- frontend/src/pages/catalog/DraftDialog.tsx | 30 ++++++--- frontend/src/pages/catalog/draftJob.ts | 5 +- scripts/check-catalog.mjs | 73 +++++++++++++++++++--- 5 files changed, 95 insertions(+), 24 deletions(-) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index e4e99d80..eb0f3abf 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -436,7 +436,8 @@ export const api = { post<{ job: CatalogDraftJob }>(`${catalogBase(sourceId)}/draft-jobs/${encodeURIComponent(jobId)}/cancel`), /** * 只重试失败的表:新任务只含原任务里失败、部分完成、已停止的表,用模型和补充说明沿用,起草要求取数据源现在的。 - * 没有可重试的表 422;有进行中的任务 409(catalog_draft_busy) + * 没有可重试的表 422;有进行中的任务 409(catalog_draft_busy);和建任务一样,起草模型用不了(原任务用模型时) + * 409 catalog_draft_model_unavailable、没有表结构 409 */ retryDraftJob: (sourceId: string, jobId: string) => post<{ job: CatalogDraftJob }>(`${catalogBase(sourceId)}/draft-jobs/${encodeURIComponent(jobId)}/retry`), diff --git a/frontend/src/lib/terms.ts b/frontend/src/lib/terms.ts index 2c479780..6b325b3b 100644 --- a/frontend/src/lib/terms.ts +++ b/frontend/src/lib/terms.ts @@ -2427,7 +2427,8 @@ export const CATALOG_DRAFT_TEXT = { // ---- 进行中 queued: '排队中,等待开始…', running: '正在起草…', - stopping: '正在停止:当前的模型调用结束后停止…', + // 服务端点了停止就取消进行中的模型调用(不等它答完,一次调用可能要几分钟),只等手上正在写入目录的那张表写完 + stopping: '正在停止:取消进行中的模型调用,正在写入目录的表写完后停止…', progress: (done: number, total: number) => `已完成 ${formatNumber(done)} / ${formatNumber(total)} 张表`, /** 结束以后:全部写出来的表有几张(停止、失败、部分完成的不算) */ progressDone: (ok: number, total: number) => `写完 ${formatNumber(ok)} / ${formatNumber(total)} 张表`, @@ -2452,7 +2453,8 @@ export const CATALOG_DRAFT_TEXT = { } as Record, interrupted: '服务重启时这次起草还没有完成,没写完的表未起草。已写进目录的内容保留;可以只重试这些表,接着起草', cancelled: '已写进目录的内容保留,没写完的表未起草', - jobFailed: (reason: string) => `起草任务出错:${reason}`, + // 任务整体出错的原因(job.error)照服务端的原话写在标题「起草失败」下面,不再加前缀:服务端的原因有的已经是整句 + // (「数据源不存在……」),有的自带「起草任务出错:」开头。没结束的表记失败、原因同任务的,逐表不再重复 /** 没写全的表:失败、部分完成、已停止各几张;跳过的(表结构里没有)另说 */ problems: (c: { failed: number; partial: number; cancelled: number; skipped: number }) => { const parts = [ @@ -2483,7 +2485,7 @@ export const CATALOG_DRAFT_TEXT = { toast: { succeeded: (summary: string) => `数据目录起草完成:${summary}`, partial: (n: number) => `数据目录起草完成,${formatNumber(n)} 张表未写全`, - failed: (reason: string) => `数据目录起草失败:${reason}`, + failed: (reason: string) => (reason ? `数据目录起草失败:${reason}` : '数据目录起草失败'), cancelled: '数据目录起草已停止', interrupted: '服务重启,数据目录起草中断', }, diff --git a/frontend/src/pages/catalog/DraftDialog.tsx b/frontend/src/pages/catalog/DraftDialog.tsx index 47e11c65..a9b6f340 100644 --- a/frontend/src/pages/catalog/DraftDialog.tsx +++ b/frontend/src/pages/catalog/DraftDialog.tsx @@ -27,6 +27,13 @@ type Scope = 'selected' | 'filtered' | 'top' | 'all' /** 字数按服务端的算法:去掉首尾空白后的字符数(按码位,不按 UTF-16 的长度) */ const charCount = (s: string) => [...s.trim()].length +/** + * 建任务(开始、只重试失败的表)没建成怎么说。起草模型用不了(409,建任务和重试都会回):服务端那句话已经写了原因和 + * 去处,照原样,长句也不拆成「标题:原因」;别的错误前面写明没有开始 + */ +const startErrorText = (e: unknown) => + (e instanceof ApiError && e.code === DRAFT_MODEL_UNAVAILABLE ? e.message : DT.startFailed(errorMessage(e))) + export function DraftDialog({ sourceId, sourceDescription, guidance, rows, visible, selected, filtered, job, initial, initialBusy = false, stopping, onStart, onCancel, onRetry, onTrack, onGuidanceSaved, onClose, @@ -156,29 +163,30 @@ export function DraftDialog({ setBusy(r.busy) setMode('job') } catch (e) { - // 起草模型用不了(没有接入、已停用、只有演示接入):服务端那句话已经写了原因和去处 - setStartError(e instanceof ApiError && e.code === DRAFT_MODEL_UNAVAILABLE ? e.message : DT.startFailed(errorMessage(e))) + // 起草模型用不了(没有接入、已停用、只有演示接入)时留在这一步,写明原因 + setStartError(startErrorText(e)) } finally { setStarting(false) } } - const act = async (fn: () => Promise) => { + const act = async (fn: () => Promise, describe: (e: unknown) => string = errorMessage) => { setActing(true) setActionError(null) try { await fn() } catch (e) { - setActionError(errorMessage(e)) + setActionError(describe(e)) } finally { setActing(false) } } const stop = () => act(onCancel) + // 重试是新建一个任务:没建成(起草模型用不了、没有表结构……)和开始时同一个说法,留在结果里 const retry = (from: CatalogDraftJob) => act(async () => { const r = await onRetry(from) setBusy(r.busy) - }) + }, startErrorText) const openLast = (j: CatalogDraftJob) => { onTrack(j) setBusy(false) @@ -357,7 +365,7 @@ function JobView({ job, running, stopping, busy }: { job: CatalogDraftJob; runni

)} {running &&

{DT.hideHint}

} - {job.status === 'failed' && job.error && {DT.jobFailed(job.error)}} + {job.status === 'failed' && job.error && {job.error}} {job.status === 'interrupted' && {DT.interrupted}} {job.status === 'cancelled' &&

{DT.cancelled}

} {problemText && ( @@ -376,7 +384,7 @@ function JobView({ job, running, stopping, busy }: { job: CatalogDraftJob; runni
    - {tasks.map((t) => )} + {tasks.map((t) => )}
@@ -393,9 +401,13 @@ const TASK_ICON: Record 1 && t.status !== 'pending' && t.status !== 'skipped' const changes = DT.taskAdded(t.added, t.updated) return ( diff --git a/frontend/src/pages/catalog/draftJob.ts b/frontend/src/pages/catalog/draftJob.ts index 54a9c36e..eaa2c5b4 100644 --- a/frontend/src/pages/catalog/draftJob.ts +++ b/frontend/src/pages/catalog/draftJob.ts @@ -152,7 +152,10 @@ export function useDraftJob(sourceId: string, active: CatalogActiveDraftJob | nu } }, [accept]) - /** 只重试失败的表:同 start,409 已有任务时改去显示进行中的那一个 */ + /** + * 只重试失败的表:同 start,409 已有任务时改去显示进行中的那一个;起草模型用不了(409)、没有可重试的表(422)等 + * 照原样抛给调用方 + */ const retry = useCallback(async (from: CatalogDraftJob): Promise<{ job: CatalogDraftJob; busy: boolean }> => { const src = sourceRef.current try { diff --git a/scripts/check-catalog.mjs b/scripts/check-catalog.mjs index afdba716..e90d0fc5 100644 --- a/scripts/check-catalog.mjs +++ b/scripts/check-catalog.mjs @@ -407,9 +407,18 @@ const JOB_ACTIVE = new Set(['queued', 'running']) const TASK_END = new Set(['ok', 'partial', 'failed', 'skipped', 'cancelled']) const RETRYABLE = new Set(['failed', 'partial', 'cancelled']) const isoNow = () => new Date().toISOString() -/** 服务端的两种 409(机读码 + 一句中文) */ +/** + * 服务端的几种 409(机读码 + 一句中文)。起草模型用不了有长短两句:没有接入时短(界面原样显示),设置里的起草模型所在 + * 接入停用时长(带句号,也要原样显示) + */ const BUSY = { detail: '这个数据源已有进行中的起草任务,请等它结束或停止后再开始', code: 'catalog_draft_busy' } const MODEL_DOWN = { detail: '未配置模型接入,无法由助手起草数据目录。请到「设置 → 模型接入」添加', code: 'catalog_draft_model_unavailable' } +const MODEL_OFF = { + detail: '起草模型使用的模型接入「check-gw」已停用。请在「设置 → 模型接入」中启用它,或在「设置 → 偏好设置」的「起草模型」中重新选择', + code: 'catalog_draft_model_unavailable', +} +/** 任务整体出错时服务端写的原因(catalog_jobs.JOB_ERROR:已经带着「起草任务出错:」开头) */ +const JOB_ERR = '起草任务出错:database is locked' /** 起草时每段最多几列(服务端的常量):超过的宽表拆成「表级一段 + 列分段」 */ const PART_COLUMNS = 40 @@ -483,15 +492,28 @@ function finishJob(job, status) { job.started_at ??= job.finished_at } -/** 停下:没开始和没写完的表记 cancelled,已经写进目录的不动 */ +/** + * 停下:没开始和没写完的表记 cancelled,已经写进目录的不动。带原因的(服务重启中断)任务和表记同一句(服务端 _close 的 + * 做法);使用者点的停止不带原因 + */ function stopJob(job, status = 'cancelled', error = null) { for (const t of job.tasks) { if (t.status === 'pending' || t.status === 'running') Object.assign(t, { status: 'cancelled', error, finished_at: isoNow() }) } + if (error) job.error = error delete job.stopAsked finishJob(job, status) } +/** 任务整体出错(数据源没了、开始时起草模型已经用不了、意外出错):没结束的表也记失败,原因同任务的(服务端 _end 的做法) */ +function failJob(job, error) { + for (const t of job.tasks) { + if (t.status === 'pending' || t.status === 'running') Object.assign(t, { status: 'failed', error, finished_at: isoNow() }) + } + job.error = error + finishJob(job, 'failed') +} + function lastDraftOf(state, src, name) { for (const job of [...state.jobs].reverse()) { if (job.source_id !== src) continue @@ -543,7 +565,7 @@ function jobHandlers(state, reply) { else job.stopAsked = true return json({ job: jobOut(job) })(route) })], - // 重试:新任务只含失败、部分完成、停止的表,沿用用模型和补充说明 + // 重试:新任务只含失败、部分完成、停止的表,沿用用模型和补充说明。起草模型用不了的 409 由检查用 replies.retry 给 [/^POST \/datasources\/check-cat-[a-z]+\/catalog\/draft-jobs\/[^/]+\/retry$/, reply('retry', (route, { key }) => { const old = jobOf(state, key) if (!old) return NO_JOB(route) @@ -931,7 +953,7 @@ await section('数据目录 · 起草:设置与开始', async () => { // 起草模型用不了:409 带机读码,留在设置阶段写明原因,不进入进行中、不去取任务 replies.createJob.push(json(MODEL_DOWN, 409)) await page.locator('[data-draft-start]').click() - check('起草模型用不了(409):留在设置阶段,写明原因', await until(async () => (await draftText(page, '[data-draft-start-error]')).includes('未配置模型接入')) + check('起草模型用不了(409):留在设置阶段,照原样写服务端那句话', await until(async () => (await draftText(page, '[data-draft-start-error]')) === MODEL_DOWN.detail) && (await draftPhase(page)) === 'setup', await draftText(page, '[data-draft-start-error]')) const keys = writes(sent, /./).map((s) => s.key.replace(/^(\w+) .*\/catalog\//, '$1 ')) check('……起草要求改过:先存起草要求、再建任务', keys.join(',') === 'PUT guidance,POST draft-jobs', keys.join(',')) @@ -961,7 +983,8 @@ await section('数据目录 · 起草:设置与开始', async () => { await section('数据目录 · 起草:进度、结果与重试', async () => { const state = freshState() - const { page, sent, errors, close } = await open(`/data/catalog/${S1}`, { handlers: catalogHandlers(state) }) + const replies = { retry: [] } + const { page, sent, errors, close } = await open(`/data/catalog/${S1}`, { handlers: catalogHandlers(state, replies) }) await page.locator('[data-catalog-row]').first().waitFor() check('开始起草(闸机到会员 3 张,用模型,带补充说明)', await startDraft(page, { select: ['gates', 'members'], model: true, note: DRAFT_NOTE })) const job = state.jobs.at(-1) @@ -1010,9 +1033,17 @@ await section('数据目录 · 起草:进度、结果与重试', async () => { // 只重试失败的表 const retry = page.locator('[data-draft-retry]') check('给「只重试失败的表(2 张)」', (await retry.innerText().catch(() => '')).includes('只重试失败的表(2 张)'), await retry.innerText().catch(() => '')) + // 重试时起草模型已经用不了(服务端和建任务时一样回 409 catalog_draft_model_unavailable):留在结果里,照原样写服务端 + // 那句话(长句也不拆成「标题:原因」),不建新任务 + replies.retry.push(json(MODEL_OFF, 409)) + await retry.click() + check('重试时起草模型用不了(409):留在结果里,照原样写服务端那句话,不建新任务', + await until(async () => (await draftText(page, '[data-draft-action-error]')) === MODEL_OFF.detail) + && (await draftPhase(page)) === 'done' && state.jobs.at(-1) === job, await draftText(page, '[data-draft-action-error]')) await retry.click() check('重试:调重试接口,进入新任务的进行中,只含没写全的 2 张', await until(async () => (await draftPhase(page)) === 'running' - && (await draftBox(page).locator('[data-draft-task]').count()) === 2) && count(sent, new RegExp(`^POST .*draft-jobs/${job.id}/retry$`)) === 1) + && (await draftBox(page).locator('[data-draft-task]').count()) === 2) && count(sent, new RegExp(`^POST .*draft-jobs/${job.id}/retry$`)) === 2) + check('……上一次的报错收起', await draftBox(page).locator('[data-draft-action-error]').count() === 0) const job2 = state.jobs.at(-1) check('……新任务沿用用模型和补充说明', job2.id !== job.id && job2.use_model && job2.note === DRAFT_NOTE) settle(state, job2, 'store_sales', { added: 1, model_items: 3 }) @@ -1082,9 +1113,11 @@ await section('数据目录 · 起草:停止、后台继续与再打开', asyn await strip.locator('[data-catalog-draft-open]').click() check('点开:回到对话框,直接显示进行中的任务', await until(async () => (await draftPhase(page)) === 'running') && (await draftText(page, '[data-draft-progress]')) === '已完成 2 / 20 张表', await draftText(page, '[data-draft-progress]')) - // 停止:调停止接口;服务端停下正在进行的调用之前写「正在停止」 + // 停止:调停止接口;服务端马上取消进行中的模型调用,只等手上那张表写完(停止接口等它收尾才答)。这期间写「正在停止」, + // 说法照服务端的做法:不是「等当前这次调用结束」——一次调用可能要几分钟 await page.locator('[data-draft-stop]').click() - check('点「停止」:调停止接口,写「正在停止」,停止按钮不能再点', await until(async () => (await draftText(page, '[data-draft-title]')).startsWith('正在停止')) + check('点「停止」:调停止接口,写「正在停止:取消进行中的模型调用…」,停止按钮不能再点', + await until(async () => (await draftText(page, '[data-draft-title]')).startsWith('正在停止:取消进行中的模型调用')) && count(sent, new RegExp(`^POST .*draft-jobs/${job.id}/cancel$`)) === 1 && await page.locator('[data-draft-stop]').isDisabled(), await draftText(page, '[data-draft-title]')) stopJob(job) @@ -1128,7 +1161,7 @@ await section('数据目录 · 起草:停止、后台继续与再打开', asyn await close() }) -await section('数据目录 · 起草:已有任务(409)与服务重启中断', async () => { +await section('数据目录 · 起草:已有任务(409)、服务重启中断与整体出错', async () => { const state = freshState() // 上一次起草因服务重启中断:没结束的表记已停止,写明原因 const old = newJob(state, S1, { tables: ['visits', 'orders', 'parks'], use_model: true, note: DRAFT_NOTE }) @@ -1144,7 +1177,10 @@ await section('数据目录 · 起草:已有任务(409)与服务重启中 check('……点开:结果写明服务重启中断,可以接着重试', await until(async () => (await draftPhase(page)) === 'done') && (await draftText(page, '[data-draft-title]')) === '服务重启,起草中断' && (await draftText(page, '[data-draft-interrupted]')).includes('重试'), `${await draftText(page, '[data-draft-title]')} · ${await draftText(page, '[data-draft-interrupted]')}`) - check('……没写的 2 张写原因', (await draftText(page, '[data-draft-task="orders"] [data-draft-task-reason]')).includes('服务重启,起草中断')) + // 服务端把任务和没写完的表都记「服务重启,起草中断」:标题、提示已经写了,逐表只写「已停止」,不再各写一遍 + check('……没写的 2 张记「已停止」,原因不再逐张重复', (await draftText(page, '[data-draft-task="orders"] [data-draft-task-status]')) === '已停止' + && (await draftText(page, '[data-draft-task="parks"] [data-draft-task-status]')) === '已停止' + && await draftBox(page).locator('[data-draft-task-reason]').count() === 0, await draftText(page, '[data-draft-tasks]')) await shot(page, 'catalog-draft-interrupted') await page.locator('[data-draft-retry]').click() check('……「只重试失败的表」接着起草没写的 2 张', await until(async () => (await draftPhase(page)) === 'running') @@ -1156,6 +1192,23 @@ await section('数据目录 · 起草:已有任务(409)与服务重启中 await until(async () => (await draftPhase(page)) === 'done') await page.locator('[data-draft-close]').click() + // 任务整体出错(意外出错、开始时起草模型已经用不了……):服务端把没结束的表也记失败、原因同任务的。原因只在标题下面 + // 写一次,照服务端的原话(它已经带着「起草任务出错:」开头,界面不再套一层);逐表不再重复同一句;这些表都能重试 + check('开始起草(使用次数最多的 20 张,用模型)', await startDraft(page, { model: true })) + const broken = state.jobs.at(-1) + settle(state, broken, 'visits', { added: 2, model_items: 4 }) + settle(state, broken, 'orders', { status: 'failed', model_items: 0, model_error: CALL_ERR }) + failJob(broken, JOB_ERR) + check('任务整体出错:标题写「起草失败」,原因照服务端的原话写一次', await until(async () => (await draftPhase(page)) === 'done') + && (await draftText(page, '[data-draft-title]')) === '起草失败' && (await draftText(page, '[data-draft-job-error]')) === JOB_ERR, + `${await draftText(page, '[data-draft-title]')} · ${await draftText(page, '[data-draft-job-error]')}`) + check('……逐表只列没写的 19 张;和任务相同的原因不再逐张重复,各有原因的照样写', (await draftBox(page).locator('[data-draft-task]').count()) === 19 + && await draftBox(page).locator('[data-draft-task-reason]').count() === 1 + && (await draftText(page, '[data-draft-task="orders"] [data-draft-task-reason]')) === CALL_ERR, await draftText(page, '[data-draft-tasks]')) + check('……这 19 张都能重试', (await page.locator('[data-draft-retry]').innerText().catch(() => '')).includes('(19 张)')) + await shot(page, 'catalog-draft-failed') + await page.locator('[data-draft-close]').click() + // 别处(另一个标签页)又起了一个任务,这一页还不知道:点开始时服务端回 409,转去显示那一个 await page.locator('[data-catalog-draft]').first().click() await draftBox(page).waitFor()