From a0b06f93c4de0958a381775476b364b87c170403 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Tue, 13 Oct 2026 20:26:52 +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=EF=BC=88=E5=89=8D=E7=AB=AF=EF=BC=89=EF=BC=9A=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E6=8E=A5=E8=BF=9E=E6=B2=A1=E5=86=99=E5=87=BA=E6=9D=A5?= =?UTF-8?q?=E6=97=B6=EF=BC=8C=E8=BF=9B=E8=A1=8C=E4=B8=AD=E9=86=92=E7=9B=AE?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E6=9C=8D=E5=8A=A1=E7=AB=AF=E7=BB=99=E7=9A=84?= =?UTF-8?q?=E6=8F=90=E9=86=92=EF=BC=88job.notice=EF=BC=89=E2=80=94?= =?UTF-8?q?=E2=80=94=E6=8F=90=E9=86=92=E7=9A=84=E6=A0=B7=E5=BC=8F=E3=80=81?= =?UTF-8?q?=E4=B8=8D=E6=98=AF=E9=94=99=E8=AF=AF=EF=BC=8C=E5=86=99=E5=9C=A8?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E6=9D=A1=E4=B8=8B=E9=9D=A2=EF=BC=9B=E5=85=B3?= =?UTF-8?q?=E6=8E=89=E5=AF=B9=E8=AF=9D=E6=A1=86=E6=97=B6=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E9=A1=B5=E4=B8=8A=E6=96=B9=E7=9A=84=E8=BF=9B=E5=BA=A6=E4=B9=9F?= =?UTF-8?q?=E5=B8=A6=E7=9D=80=E8=BF=99=E4=B8=80=E8=A1=8C=EF=BC=88=E5=85=A8?= =?UTF-8?q?=E6=96=87=E5=9C=A8=E6=82=AC=E5=81=9C=E9=87=8C=EF=BC=89=EF=BC=9B?= =?UTF-8?q?=E5=A5=BD=E8=BD=AC=E5=90=8E=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=B8=85?= =?UTF-8?q?=E6=8E=89=E5=B0=B1=E6=94=B6=E8=B5=B7=EF=BC=8C=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=BB=93=E6=9D=9F=E5=90=8E=E4=B8=8D=E5=86=8D=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E3=80=82=E7=94=A8=E9=87=8F=E9=82=A3=E4=B8=80=E8=A1=8C=E6=8E=A5?= =?UTF-8?q?=E7=9D=80=E5=86=99=E6=B2=A1=E5=86=99=E5=87=BA=E6=9D=A5=E3=80=81?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E7=9A=84=E6=AC=A1=E6=95=B0=EF=BC=88=E6=B2=A1?= =?UTF-8?q?=E5=86=99=E5=87=BA=E6=9D=A5=E7=9A=84=E6=AC=A1=E6=95=B0=E9=87=8C?= =?UTF-8?q?=E5=90=AB=E8=B6=85=E6=97=B6=EF=BC=8C=E5=86=99=E6=88=90=E3=80=8C?= =?UTF-8?q?=E6=B2=A1=E5=86=99=E5=87=BA=E6=9D=A5=204=20=E6=AC=A1=EF=BC=88?= =?UTF-8?q?=E5=85=B6=E4=B8=AD=E8=B6=85=E6=97=B6=203=20=E6=AC=A1=EF=BC=89?= =?UTF-8?q?=E3=80=8D=EF=BC=8C=E4=B8=8D=E8=AF=BB=E6=88=90=E4=B8=A4=E4=B8=AA?= =?UTF-8?q?=E6=95=B0=E7=9B=B8=E5=8A=A0=EF=BC=89=EF=BC=8C=E6=9C=89=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E6=97=B6=E5=86=99=E6=98=8E=E3=80=8C=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E7=9A=84=E8=B0=83=E7=94=A8=E4=B8=8D=E8=AE=A1=E7=94=A8=E9=87=8F?= =?UTF-8?q?=EF=BC=8C=E5=AE=9E=E9=99=85=E7=94=A8=E9=87=8F=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E6=9B=B4=E5=A4=9A=E3=80=8D=EF=BC=9B=E9=A2=9D=E5=BA=A6=E7=94=A8?= =?UTF-8?q?=E5=AE=8C=E3=80=81=E8=B6=85=E6=97=B6=E5=A4=9A=E4=BA=86=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=AB=AF=E6=8A=8A=E4=B9=8B=E5=90=8E=E7=9A=84=E6=AE=B5?= =?UTF-8?q?=E7=BC=A9=E5=B0=8F=E6=97=B6=E5=86=99=E6=98=8E=E3=80=8C=E4=B9=8B?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E6=AE=B5=E6=AF=8F=E6=AE=B5=E6=9C=80=E5=A4=9A?= =?UTF-8?q?=2020=20=E5=88=97=E3=80=8D=E3=80=82=E7=B1=BB=E5=9E=8B=E5=8A=A0?= =?UTF-8?q?=20CatalogDraftJob.part=5Fcolumns=E3=80=81notice=EF=BC=8Cusage?= =?UTF-8?q?=20=E5=8A=A0=20timeouts=E3=80=81failed=5Fcalls=EF=BC=88?= =?UTF-8?q?=E8=80=81=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=B2=A1=E6=9C=89=E8=BF=99?= =?UTF-8?q?=E5=87=A0=E4=B8=AA=E9=94=AE=EF=BC=8C=E6=8C=89=200=20=E5=92=8C?= =?UTF-8?q?=20null=20=E5=A4=84=E7=90=86=EF=BC=89=E3=80=82check-catalog=20?= =?UTF-8?q?=E7=9A=84=E5=81=87=E6=9C=8D=E5=8A=A1=E7=AB=AF=E8=B7=9F=E7=9D=80?= =?UTF-8?q?=E7=BB=99=E8=BF=99=E5=87=A0=E9=A1=B9=EF=BC=8C=E6=96=B0=E5=8A=A0?= =?UTF-8?q?=E4=B8=80=E8=8A=82=E8=A6=86=E7=9B=96=E6=8F=90=E9=86=92=E7=9A=84?= =?UTF-8?q?=E5=87=BA=E7=8E=B0=E4=B8=8E=E6=B6=88=E5=A4=B1=E3=80=81=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E5=92=8C=E6=B2=A1=E5=86=99=E5=87=BA=E6=9D=A5=E7=9A=84?= =?UTF-8?q?=E6=AC=A1=E6=95=B0=E3=80=81=E6=AE=B5=E7=BC=A9=E5=B0=8F=E7=9A=84?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=E3=80=81=E4=BB=BB=E5=8A=A1=E7=BB=93=E6=9D=9F?= =?UTF-8?q?=E5=90=8E=E6=8F=90=E9=86=92=E6=94=B6=E8=B5=B7=EF=BC=8C=E7=AA=84?= =?UTF-8?q?=E5=B1=8F=E7=9A=84=E8=B5=B7=E8=8D=89=E5=BC=B9=E7=AA=97=E5=92=8C?= =?UTF-8?q?=E4=B8=8A=E6=96=B9=E8=BF=9B=E5=BA=A6=E5=B8=A6=E7=9D=80=E6=8F=90?= =?UTF-8?q?=E9=86=92=E4=B8=80=E5=B9=B6=E9=87=8F=EF=BC=9B=E5=AE=BD=E8=A1=A8?= =?UTF-8?q?=E6=B2=A1=E5=86=99=E5=87=BA=E6=9D=A5=E7=9A=84=E5=8E=9F=E5=9B=A0?= =?UTF-8?q?=E7=85=A7=E6=96=B0=E5=86=99=E6=B3=95=E5=86=99=E6=98=8E=E5=88=97?= =?UTF-8?q?=E7=9A=84=E8=8C=83=E5=9B=B4=EF=BC=88=E7=AC=AC=203=20=E6=AE=B5?= =?UTF-8?q?=EF=BC=88promo=5Fattr=5F19=20=E5=88=B0=20promo=5Fattr=5F43?= =?UTF-8?q?=EF=BC=8C=E5=85=B1=2025=20=E5=88=97=EF=BC=89=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/lib/terms.ts | 18 +++- frontend/src/pages/catalog/CatalogPage.tsx | 22 ++++- frontend/src/pages/catalog/DraftDialog.tsx | 39 ++++++--- frontend/src/pages/catalog/draftJob.ts | 3 + frontend/src/types.ts | 25 +++++- scripts/check-catalog.mjs | 95 ++++++++++++++++++++-- 6 files changed, 177 insertions(+), 25 deletions(-) diff --git a/frontend/src/lib/terms.ts b/frontend/src/lib/terms.ts index 6b325b3b..dfb0aa2f 100644 --- a/frontend/src/lib/terms.ts +++ b/frontend/src/lib/terms.ts @@ -2390,8 +2390,11 @@ export const CATALOG_TEXT = { // - 「起草要求」:使用者补充给模型的业务背景和用词约定,存在数据源上,以后每次用模型起草都带上; // 「本次补充说明」只用于这一次(重试时沿用)。两者都只交给模型,不改起草规则:冲突时以规则为准。 // - 「起草模型」:设置里单独的一项(偏好设置 → 起草模型),留空跟随助手的模型。 -// - 宽表的「段」:超过 40 列的表拆成表级一段和若干列分段,逐段起草;写的是「已完成 3 / 9 段」。 +// - 宽表的「段」:超过 40 列的表拆成表级一段和若干列分段,逐段起草;写的是「已完成 3 / 9 段」。模型多次用完额度或 +// 超时后,之后的段缩小(每段最多 20 列、10 列)。 // - 「只重试失败的表」:重新起草失败、部分完成和已停止的表;已经写进目录的内容不回滚、不受影响。 +// - 「没写出来」:一次调用没有写出内容(额度用完、只给空壳、超时、出错)。接连没写出来时服务端给一句「提醒」, +// 好转后收起;提醒是提醒的样式,不是错误。超时的调用不计用量。 // =========================================================================== /** 起草要求、本次补充说明的字数上限(服务端同一个数:超长 422) */ @@ -2441,6 +2444,19 @@ export const CATALOG_DRAFT_TEXT = { modelUsed: (model: string) => `模型:${model}`, usage: (calls: number, input: number, output: number) => `调用模型 ${formatNumber(calls)} 次,输入 ${formatTokens(input, { compact: true })}、输出 ${formatTokens(output, { compact: true })}`, + /** + * 用量那一行接着写的:没写出来、超时的调用各几次(没有的不写)。没写出来的次数里含超时的,写成「其中」,免得 + * 读成两个数相加 + */ + usageFailures: (timeouts: number, failed: number) => { + if (failed && timeouts && timeouts <= failed) return `没写出来 ${formatNumber(failed)} 次(其中超时 ${formatNumber(timeouts)} 次)` + return [failed ? `没写出来 ${formatNumber(failed)} 次` : '', timeouts ? `超时 ${formatNumber(timeouts)} 次` : ''].filter(Boolean).join('、') + }, + usageFailuresHint: '没写出来:调用没有写出内容,包括额度用完、只给空壳、超时和出错', + // 超时的调用服务端照实记 0 个 token,服务方却可能已经生成了内容 + timeoutNote: '超时的调用不计用量,实际用量可能更多', + /** 额度用完、超时多了,服务端把之后的段缩小了 */ + partColumns: (n: number) => `模型多次用完额度或超时,之后的段每段最多 ${formatNumber(n)} 列`, // ---- 结束 title: { queued: '排队中,等待开始…', diff --git a/frontend/src/pages/catalog/CatalogPage.tsx b/frontend/src/pages/catalog/CatalogPage.tsx index 15f8e24e..51097aea 100644 --- a/frontend/src/pages/catalog/CatalogPage.tsx +++ b/frontend/src/pages/catalog/CatalogPage.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { ReactNode } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' -import { ArrowLeft, BookMarked, Database, ListFilter as ListFilterIcon, ScanSearch, Sparkles } from 'lucide-react' +import { ArrowLeft, BookMarked, Database, ListFilter as ListFilterIcon, ScanSearch, Sparkles, TriangleAlert } from 'lucide-react' import clsx from 'clsx' import { ApiError, api } from '../../api/client' import type { CatalogDetail, CatalogList, CatalogTableRow } from '../../types' @@ -203,6 +203,8 @@ export function CatalogPage() { const stripJob: { done: number; total: number; status: string } | null = draftJob && (draftActive || draftJob.id === listActive?.id) ? (draftActive ? draftJob : null) : listActive + // 调用接连没写出来的提醒:关着对话框时也要看得到(上方的进度带着它) + const stripNotice = draftActive ? draftJob?.notice?.trim() || null : null const options = savedOptions?.sourceId === sourceId ? savedOptions.options : source?.options const profileSettings = useMemo(() => profileSettingsOf(options), [options]) @@ -430,7 +432,7 @@ export function CatalogPage() { onGoSource={() => navigate(back)} /> )} {hasTables && usage.used > 0 && !empty && } - {hasTables && stripJob && !draftOpen && } + {hasTables && stripJob && !draftOpen && } {body} {draftOpen && data && ( void }) { +function DraftStrip({ job, notice, onOpen }: { + job: { done: number; total: number; status: string } + notice: string | null + onOpen: () => void +}) { const text = job.status === 'queued' ? DT.stripQueued(job.total) : DT.strip(job.done, job.total) return (

{text}

+ {notice && ( +

+ + {notice} +

+ )}
RETRYABLE.has(t.status)) const [showAll, setShowAll] = useState(false) @@ -342,6 +344,13 @@ function JobView({ job, running, stopping, busy }: { job: CatalogDraftJob; runni const progress = running ? DT.progress(job.done, job.total) : DT.progressDone(ok, job.total) const tone = job.status === 'failed' ? 'var(--err)' : running ? 'var(--accent)' : problems.length ? 'var(--warn)' : 'var(--st-done)' const pct = job.total ? (value / job.total) * 100 : 0 + const { usage } = job + const timeouts = usage.timeouts ?? 0 + const failures = DT.usageFailures(timeouts, usage.failed_calls ?? 0) + // 调用接连没写出来时服务端给的提醒:进行中写在进度条下面(提醒,不是错误);好转后服务端清掉 + const notice = running ? job.notice?.trim() ?? '' : '' + // 额度用完、超时多了,之后的段缩小了:进行中写一句,段数为什么变多看得明白 + const partColumns = running && job.use_model && job.part_columns != null && job.part_columns < DRAFT_PART_COLUMNS ? job.part_columns : null return (
{busy && {DT.busy}} @@ -355,14 +364,22 @@ function JobView({ job, running, stopping, busy }: { job: CatalogDraftJob; runni aria-valuemin={0} aria-valuemax={job.total} aria-valuenow={value}>
+ {notice && {notice}}

{summary}

- {(job.model || job.usage.calls > 0) && ( -

- {job.model && {DT.modelUsed(job.model)}} - {job.usage.calls > 0 && ( - {DT.usage(job.usage.calls, job.usage.input_tokens, job.usage.output_tokens)} - )} -

+ {(job.model || usage.calls > 0 || failures) && ( +
+

+ {job.model && {DT.modelUsed(job.model)}} + {usage.calls > 0 && ( + {DT.usage(usage.calls, usage.input_tokens, usage.output_tokens)} + )} + {failures && ( + {failures} + )} +

+ {timeouts > 0 &&

{DT.timeoutNote}

} + {partColumns != null &&

{DT.partColumns(partColumns)}

} +
)} {running &&

{DT.hideHint}

} {job.status === 'failed' && job.error && {job.error}} diff --git a/frontend/src/pages/catalog/draftJob.ts b/frontend/src/pages/catalog/draftJob.ts index eaa2c5b4..1e56e56b 100644 --- a/frontend/src/pages/catalog/draftJob.ts +++ b/frontend/src/pages/catalog/draftJob.ts @@ -36,6 +36,9 @@ export const retryableCount = (j: CatalogDraftJob): number => j.tasks.filter((t) /** 一张表这次为什么没写全:整张表没起草的原因在前,模型那部分的原因在后 */ export const taskReason = (t: { error: string | null; model_error: string | null }): string => t.error ?? t.model_error ?? '' +/** 每段最多几列(服务端起草的起始上限)。任务里的当前上限比它小:额度用完、超时多了,之后的段缩小了 */ +export const DRAFT_PART_COLUMNS = 40 + /** * 一项都没写进去的任务该怎么说(只对写完了的任务:完成、部分完成)。null:照常写合计。 * 用模型、却没有一张表拿到模型的可用内容:目录没有更新、模型没给内容;有表没写全:目录没有更新、原因见下方; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index ef13a6d2..baf7ed51 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -2901,6 +2901,19 @@ export interface CatalogDraftTask { finished_at: string | null } +/** 起草任务的模型用量 */ +export interface CatalogDraftUsage { + /** 调用次数(没写出来的、超时的也算) */ + calls: number + /** 超时的调用不计 token(照实记 0:服务方可能已经生成了内容) */ + input_tokens: number + output_tokens: number + /** 超时的调用次数。老服务端没有这个键 */ + timeouts?: number + /** 没写出来的调用次数:额度用完、只给空壳、超时、出错。老服务端没有这个键 */ + failed_calls?: number +} + /** 一次起草任务 */ export interface CatalogDraftJob { id: string @@ -2929,7 +2942,17 @@ export interface CatalogDraftJob { updated: number removed: number /** 模型调用的用量;拿不到用量时照实是 0 */ - usage: { calls: number; input_tokens: number; output_tokens: number } + usage: CatalogDraftUsage + /** + * 当前每段最多几列:一次起草里额度用完或超时累计 2 次后,之后新开始的段减半(40 → 20 → 10,最低 10)。 + * 不用模型时可能为 null;老服务端没有这个键 + */ + part_columns?: number | null + /** + * 调用接连没写出来时的提醒(一句中文:最近几次里几次没写出来、原因,建议换起草模型、缩小范围或停止); + * 好转后服务端清掉,为 null。老服务端没有这个键 + */ + notice?: string | null /** 任务整体出错的原因 */ error: string | null tasks: CatalogDraftTask[] diff --git a/scripts/check-catalog.mjs b/scripts/check-catalog.mjs index dc8dde8b..57963898 100644 --- a/scripts/check-catalog.mjs +++ b/scripts/check-catalog.mjs @@ -425,7 +425,12 @@ const JOB_ERR = '起草任务出错:database is locked' /** 起草时每段最多几列(服务端的常量):超过的宽表拆成「表级一段 + 列分段」 */ const PART_COLUMNS = 40 -/** 建一个排队的任务:表结构里没有的表照样建进任务、记 skipped。重试建的记着原任务(retry_of,界面不用) */ +/** + * 建一个排队的任务:表结构里没有的表照样建进任务、记 skipped。重试建的记着原任务(retry_of,界面不用)。 + * 超过 40 列的宽表均匀分段:列分段数 = ceil(列数 / 40),段进度是「表级一段 + 列分段」。 + * 用模型的任务记着当前每段最多几列(part_columns,额度用完、超时多了服务端会减半)、调用接连没写出来时的提醒(notice), + * 用量里另记超时和没写出来的调用次数 + */ function newJob(state, src, { tables, use_model, note = null, retry_of = null }) { const id = `check-job-${++state.jobSeq}` const tasks = tables.map((name) => { @@ -442,7 +447,8 @@ function newJob(state, src, { tables, use_model, note = null, retry_of = null }) const job = { id, source_id: src, status: 'queued', use_model: !!use_model, model: use_model ? 'check-draft-model' : null, guidance: state.guidance[src] ?? null, note, created_at: isoNow(), started_at: null, finished_at: null, created_by: ACTOR, - retry_of, usage: { calls: 0, input_tokens: 0, output_tokens: 0 }, error: null, tasks, + retry_of, part_columns: use_model ? PART_COLUMNS : null, notice: null, + usage: { calls: 0, input_tokens: 0, output_tokens: 0, timeouts: 0, failed_calls: 0 }, error: null, tasks, } state.jobs.push(job) return job @@ -677,8 +683,11 @@ async function startDraft(page, { select = null, model = false, note = '' } = {} /** 起草用的几句话(虚构) */ const GUIDANCE2 = '景区票务库:「核销」指闸机检票入园;金额单位是元。会员相关的表统一写「会员」,不写「用户」' const DRAFT_NOTE = '这一批是闸机和会员相关的表' -const WIDE_ERR = '这张表 50 列,分 3 段起草,第 2 段没写出来(共 10 列):模型的思考和输出用完了额度(32768 个 token),已拆小重试仍没写出。可以只重试这张表' +// 宽表没写出来的那一段写明列的范围(列名照表结构原样);只有真拆过才写「已拆小重试仍没写出」 +const WIDE_ERR = '这张表 50 列,分 3 段起草,第 3 段(promo_attr_19 到 promo_attr_43,共 25 列)没写出来:模型的思考和输出用完了额度(32768 个 token),已拆小重试仍没写出。可以只重试这张表' const CALL_ERR = '调用模型出错:服务暂时不可用(已重试 2 次)' +/** 调用接连没写出来时服务端给的提醒(虚构的一句,照约定写明次数、原因和办法) */ +const DRAFT_NOTICE = '最近 6 次调用有 4 次没写出来(3 次超时、1 次额度用完)。建议在「设置 → 偏好设置」的「起草模型」换一个模型、缩小起草范围,或者停止这次起草' const detail = (page) => page.locator('[data-catalog-detail]') const version = async (page) => Number(await detail(page).getAttribute('data-version')) const panel = (page) => page.locator('[data-item-panel]') @@ -1021,7 +1030,7 @@ await section('数据目录 · 起草:进度、结果与重试', async () => { check('……写明哪些没写全:失败 1 张、部分完成 1 张', problems.includes('失败 1 张') && problems.includes('部分完成 1 张'), problems) const shown = await draftBox(page).locator('[data-draft-task]').evaluateAll((els) => els.map((e) => e.getAttribute('data-draft-task'))) check('……逐表只列没写全的 2 张,各自写原因', shown.join(',') === 'store_sales,members' - && (await draftText(page, '[data-draft-task="store_sales"] [data-draft-task-reason]')).includes('第 2 段没写出来') + && (await draftText(page, '[data-draft-task="store_sales"] [data-draft-task-reason]')).includes('第 3 段(promo_attr_19 到 promo_attr_43,共 25 列)没写出来') && (await draftText(page, '[data-draft-task="members"] [data-draft-task-reason]')).includes('已重试 2 次'), shown.join(',')) await draftBox(page).locator('[data-draft-show-all]').click() check('……点「显示全部」列出 3 张', (await draftBox(page).locator('[data-draft-task]').count()) === 3) @@ -1305,6 +1314,71 @@ await section('数据目录 · 起草:清单上的失败标记与单表重起 await close() }) +await section('数据目录 · 起草:接连没写出来的提醒、超时与段缩小', async () => { + const state = freshState() + const { page, errors, natives, close } = await open(`/data/catalog/${S1}`, { handlers: catalogHandlers(state) }) + await page.locator('[data-catalog-row]').first().waitFor() + check('开始起草(闸机到会员 3 张,用模型)', await startDraft(page, { select: ['gates', 'members'], model: true })) + const job = state.jobs.at(-1) + const usageLine = () => draftText(page, '[data-draft-usage-line]') + settle(state, job, 'store_sales', { status: 'running', parts_done: 1 }) + check('还没有超时、没写出来的调用:用量只写调用次数和 token,没有提醒、超时的说明和段大小的说明', + await until(async () => (await usageLine()).includes('调用模型 1 次')) && !(await usageLine()).includes('超时') + && !(await usageLine()).includes('没写出来') + && await draftBox(page).locator('[data-draft-notice], [data-draft-timeout-note], [data-draft-part-columns]').count() === 0, await usageLine()) + + // 宽表的段接连超时、用完额度:服务端记下超时和没写出来的次数,之后的段缩小到每段 20 列,并给出一句提醒 + Object.assign(job.usage, { calls: 7, timeouts: 3, failed_calls: 4 }) + Object.assign(job, { part_columns: 20, notice: DRAFT_NOTICE }) + const notice = draftBox(page).locator('[data-draft-notice]') + check('接连没写出来:进行中醒目显示服务端的提醒,是提醒的样式(不是错误)', + await until(async () => (await draftText(page, '[data-draft-notice]')) === DRAFT_NOTICE) + && (await notice.getAttribute('data-notice')) === 'warn', await draftText(page, '[data-draft-notice]')) + const above = await page.evaluate(() => { + const n = document.querySelector('[data-draft-notice]')?.getBoundingClientRect() + const list = document.querySelector('[data-draft-tasks]')?.getBoundingClientRect() + return !!n && !!list && n.bottom <= list.top + }) + check('……提醒在逐表进度上面,不用往下翻', above) + // 没写出来的次数里含超时的:写成「其中」,免得读成两个数相加 + check('……用量写「没写出来 4 次(其中超时 3 次)」', (await usageLine()).includes('没写出来 4 次(其中超时 3 次)'), await usageLine()) + check('……写明超时的调用不计用量', (await draftText(page, '[data-draft-timeout-note]')).includes('超时的调用不计用量'), + await draftText(page, '[data-draft-timeout-note]')) + check('……写明之后的段每段最多 20 列', (await draftText(page, '[data-draft-part-columns]')).includes('每段最多 20 列'), + await draftText(page, '[data-draft-part-columns]')) + await shot(page, 'catalog-draft-notice') + + // 关掉对话框:上方的进度也带着提醒;好转后服务端清掉提醒,两处都收起 + await page.locator('[data-draft-hide]').click() + const strip = page.locator('[data-catalog-draft-progress]') + const stripNotice = strip.locator('[data-catalog-draft-notice]') + check('在后台继续:上方的进度也带着提醒(全文在悬停里)', await until(async () => (await stripNotice.count()) === 1) + && (await stripNotice.getAttribute('title')) === DRAFT_NOTICE && (await stripNotice.innerText()).length > 0, + await stripNotice.getAttribute('title', { timeout: 500 }).catch(() => '')) + await shot(page, 'catalog-draft-strip-notice') + job.notice = null + check('……好转后服务端清掉提醒:上方的进度不再带着', await until(async () => (await stripNotice.count()) === 0)) + await strip.locator('[data-catalog-draft-open]').click() + check('……点开对话框:提醒也不再显示,超时和没写出来的次数照旧', await until(async () => (await draftPhase(page)) === 'running') + && await notice.count() === 0 && (await usageLine()).includes('其中超时 3 次'), await usageLine()) + + // 又接连没写出来:提醒再出现;任务结束以后不再显示(停止、缩小范围的建议只对进行中的任务有用),用量里的次数和 + // 超时不计用量的说明照旧,段缩小的说明收起 + Object.assign(job.usage, { timeouts: 5, failed_calls: 6 }) + Object.assign(job, { part_columns: 10, notice: DRAFT_NOTICE }) + check('又接连没写出来:提醒再出现,段缩小到每段 10 列', await until(async () => (await draftText(page, '[data-draft-notice]')) === DRAFT_NOTICE) + && (await draftText(page, '[data-draft-part-columns]')).includes('每段最多 10 列'), await draftText(page, '[data-draft-part-columns]')) + for (const name of ['gates', 'store_sales', 'members']) settle(state, job, name, { added: 1, model_items: 1 }) + finishJob(job) + check('结束:不再显示提醒和段缩小的说明,用量里的次数和超时不计用量的说明照旧', await until(async () => (await draftPhase(page)) === 'done') + && await notice.count() === 0 && await draftBox(page).locator('[data-draft-part-columns]').count() === 0 + && (await usageLine()).includes('没写出来 6 次(其中超时 5 次)') && (await draftText(page, '[data-draft-timeout-note]')).includes('超时的调用不计用量'), + await usageLine()) + check('没有原生对话框', natives.length === 0, natives.join(' | ')) + check('没有运行时报错', errors.length === 0, errors[0] ?? '') + await close() +}) + await section('数据目录 · 表详情与单项审阅', async () => { const state = freshState() const { page, sent, errors, natives, close } = await open(`/data/catalog/${S1}`, { handlers: catalogHandlers(state) }) @@ -2396,14 +2470,19 @@ await section('数据目录 · 窄屏', async () => { const narrowJob = state.jobs.at(-1) settle(state, narrowJob, 'visits', { added: 2 }) settle(state, narrowJob, 'store_sales', { status: 'partial', parts_done: 2, added: 4, model_error: WIDE_ERR }) - await until(async () => (await draftText(page, '[data-draft-task="store_sales"] [data-draft-task-reason]')).includes('第 2 段')) + // 接连没写出来的提醒、超时和没写出来的次数、段缩小的说明也都在 + Object.assign(narrowJob.usage, { timeouts: 3, failed_calls: 4 }) + Object.assign(narrowJob, { part_columns: 20, notice: DRAFT_NOTICE }) + await until(async () => (await draftText(page, '[data-draft-task="store_sales"] [data-draft-task-reason]')).includes('第 3 段') + && (await draftText(page, '[data-draft-notice]')) === DRAFT_NOTICE) const runFit = await draftFits() - check('……起草进行中:段进度和长原因折行,弹窗不横向滚动', runFit.ok, JSON.stringify(runFit)) + check('……起草进行中:段进度、长原因、提醒都折行,弹窗不横向滚动', runFit.ok, JSON.stringify(runFit)) await shot(page, 'catalog-narrow-draft') await page.locator('[data-draft-hide]').click() await until(async () => (await page.locator('[data-catalog-draft-progress]').count()) === 1) - check('……上方的起草进度不撑宽页面', await overflow() <= 1 && !!(await page.locator('[data-catalog-draft-progress]').boundingBox()) - && (await page.locator('[data-catalog-draft-progress]').boundingBox()).width <= 390, String(await overflow())) + check('……上方的起草进度(带着提醒)不撑宽页面', await overflow() <= 1 && !!(await page.locator('[data-catalog-draft-progress]').boundingBox()) + && (await page.locator('[data-catalog-draft-progress]').boundingBox()).width <= 390 + && await page.locator('[data-catalog-draft-notice]').count() === 1, String(await overflow())) stopJob(narrowJob) await until(async () => (await page.locator('[data-catalog-draft-progress]').count()) === 0) // 宽屏照旧是表格(表头在,列表格 860px 起,框里横向滚动)