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()