mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 04:31:58 +00:00
数据目录起草的几处说法照服务端的实际做法改:点停止后写「正在停止:取消进行中的模型调用,正在写入目录的表写完后停止…」(服务端马上停掉进行中的调用、只等手上那张表写完,不是等当前这次调用结束);任务整体出错时原因照服务端的原话在「起草失败」下面写一次(服务端的原因有的自带「起草任务出错:」开头,界面不再套一层),服务端给没结束的表记的同一句不再逐表重复(服务重启中断同样),各有原因的照写;「只重试失败的表」没建成和开始时同一个说法——起草模型用不了(服务端重试也回 409 catalog_draft_model_unavailable)照原样写那句话、长句不再拆成「标题:原因」,别的写「未能开始起草」;提示里原因为空时不留冒号。check-catalog 先补会失败的检查:停止的说法、重试时起草模型用不了、任务整体出错(原因只写一次、逐表不重复、都能重试),服务重启中断的逐表不再重复原因;假服务端的中断任务照后端记上同一句原因
This commit is contained in:
@@ -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`),
|
||||
|
||||
@@ -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<CatalogDraftJobStatus, string>,
|
||||
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: '服务重启,数据目录起草中断',
|
||||
},
|
||||
|
||||
@@ -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<void>) => {
|
||||
const act = async (fn: () => Promise<void>, 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
|
||||
</p>
|
||||
)}
|
||||
{running && <p className="text-2xs leading-relaxed text-faint">{DT.hideHint}</p>}
|
||||
{job.status === 'failed' && job.error && <Notice tone="err" attr={{ 'data-draft-job-error': '' }}>{DT.jobFailed(job.error)}</Notice>}
|
||||
{job.status === 'failed' && job.error && <Notice tone="err" attr={{ 'data-draft-job-error': '' }}>{job.error}</Notice>}
|
||||
{job.status === 'interrupted' && <Notice tone="warn" attr={{ 'data-draft-interrupted': '' }}>{DT.interrupted}</Notice>}
|
||||
{job.status === 'cancelled' && <p className="text-2xs text-dim" data-draft-cancelled="">{DT.cancelled}</p>}
|
||||
{problemText && (
|
||||
@@ -376,7 +384,7 @@ function JobView({ job, running, stopping, busy }: { job: CatalogDraftJob; runni
|
||||
</div>
|
||||
<ul className="max-h-64 divide-y divide-[var(--hairline)] overflow-y-auto overflow-x-hidden rounded-md border"
|
||||
aria-labelledby="catalog-draft-tasks-label" data-draft-tasks="">
|
||||
{tasks.map((t) => <TaskRow key={t.table_name} task={t} />)}
|
||||
{tasks.map((t) => <TaskRow key={t.table_name} task={t} jobError={job.error} />)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -393,9 +401,13 @@ const TASK_ICON: Record<CatalogDraftTaskStatus, { Icon: LucideIcon | null; color
|
||||
cancelled: { Icon: CircleSlash, color: 'var(--text-faint)' },
|
||||
}
|
||||
|
||||
function TaskRow({ task: t }: { task: CatalogDraftTask }) {
|
||||
/**
|
||||
* 逐表的一行。jobError:任务整体的原因(整体出错、服务重启中断时服务端把没结束的表也记上同一句)。和它一样的不再逐表
|
||||
* 重复——几百张表各写一遍同一句,真正各有原因的那几张反而找不到;任务的原因已经写在上面(标题、提示)
|
||||
*/
|
||||
function TaskRow({ task: t, jobError }: { task: CatalogDraftTask; jobError: string | null }) {
|
||||
const { Icon, color } = TASK_ICON[t.status]
|
||||
const reason = taskReason(t)
|
||||
const reason = jobError && t.error === jobError ? (t.model_error ?? '') : taskReason(t)
|
||||
const parts = t.parts_total != null && t.parts_total > 1 && t.status !== 'pending' && t.status !== 'skipped'
|
||||
const changes = DT.taskAdded(t.added, t.updated)
|
||||
return (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+63
-10
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user