mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
设置「偏好设置」加「分析」一节(服务端 analysis 一组):判定用的阈值、Agent 每次执行最多分析几次、最多下钻几层、列出前几段,分成变化拆解、波动基线与趋势判断、差异检验、次数与层数四组。照口径账本一节的写法:草稿由偏好页持有、和其余设置共用一条保存条,存的时候整组写 analysis(没改的照原值带上,不被冲回缺省);叫法、范围照契约 LABELS / RANGES(每项下面写范围,类型、范围当场按契约的说法说,保存键不能点),两两之间的大小交给服务端核、422 的原话写在保存条上;服务端把 2 存成 2.0、写「2.0」不算改;老服务端没有这一组时不显示。check-manage 加一节:十七项的叫法都是契约的、显示生效的值、超范围和整数项、保存条的叫法、整组写、服务端拒收、改回原值、老服务端
This commit is contained in:
@@ -44,6 +44,10 @@ import {
|
|||||||
LedgerSection, ledgerChanged, ledgerDraftOf, ledgerErrors, ledgerLabels, ledgerPayload, type LedgerDraft,
|
LedgerSection, ledgerChanged, ledgerDraftOf, ledgerErrors, ledgerLabels, ledgerPayload, type LedgerDraft,
|
||||||
} from './settings/LedgerSettings'
|
} from './settings/LedgerSettings'
|
||||||
import { LEDGER_UI } from '../lib/termsLedger'
|
import { LEDGER_UI } from '../lib/termsLedger'
|
||||||
|
import {
|
||||||
|
AnalysisSection, analysisChanged, analysisDraftOf, analysisErrors, analysisLabels, analysisPayload, type AnalysisDraft,
|
||||||
|
} from './settings/AnalysisSettings'
|
||||||
|
import { ANALYSIS_TEXT } from '../lib/termsAnalysis'
|
||||||
import { ACCESS_KEY_TEXT } from '../lib/termsApp'
|
import { ACCESS_KEY_TEXT } from '../lib/termsApp'
|
||||||
|
|
||||||
// 提到模块级:tab 名同时是 URL 的最后一段,两处各写一份迟早对不上。
|
// 提到模块级:tab 名同时是 URL 的最后一段,两处各写一份迟早对不上。
|
||||||
@@ -697,6 +701,8 @@ function PrefsTab() {
|
|||||||
const [verifySaves, setVerifySaves] = useState(0)
|
const [verifySaves, setVerifySaves] = useState(0)
|
||||||
// 口径账本一组(自动按口径出具、每日上限、比对时限、升版后重算几组,pages/settings/LedgerSettings):草稿另放一份,共用保存条
|
// 口径账本一组(自动按口径出具、每日上限、比对时限、升版后重算几组,pages/settings/LedgerSettings):草稿另放一份,共用保存条
|
||||||
const [ledgerDraft, setLedgerDraft] = useState<LedgerDraft | null>(null)
|
const [ledgerDraft, setLedgerDraft] = useState<LedgerDraft | null>(null)
|
||||||
|
// 分析一组(判定用的阈值、次数上限,pages/settings/AnalysisSettings):草稿另放一份,共用保存条
|
||||||
|
const [analysisDraft, setAnalysisDraft] = useState<AnalysisDraft | null>(null)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [savedFlash, setSavedFlash] = useState(0)
|
const [savedFlash, setSavedFlash] = useState(0)
|
||||||
const [saveError, setSaveError] = useState<unknown>(null)
|
const [saveError, setSaveError] = useState<unknown>(null)
|
||||||
@@ -791,9 +797,14 @@ function PrefsTab() {
|
|||||||
const ledgerKeys = values?.ledger ? ledgerChanged(ledgerCur, ledgerSaved) : []
|
const ledgerKeys = values?.ledger ? ledgerChanged(ledgerCur, ledgerSaved) : []
|
||||||
const ledgerErrs = ledgerErrors(ledgerCur, ledgerSaved)
|
const ledgerErrs = ledgerErrors(ledgerCur, ledgerSaved)
|
||||||
const ledgerInvalid = Object.values(ledgerErrs).some(Boolean)
|
const ledgerInvalid = Object.values(ledgerErrs).some(Boolean)
|
||||||
const invalid = guardInvalid || judgeInvalid || verifyInvalid || ledgerInvalid || !!appsError
|
const analysisSaved = analysisDraftOf(values?.analysis)
|
||||||
|
const analysisCur = analysisDraft ?? analysisSaved
|
||||||
|
const analysisKeys = values?.analysis ? analysisChanged(analysisCur, analysisSaved) : []
|
||||||
|
const analysisErrs = analysisErrors(analysisCur, analysisSaved)
|
||||||
|
const analysisInvalid = Object.values(analysisErrs).some(Boolean)
|
||||||
|
const invalid = guardInvalid || judgeInvalid || verifyInvalid || ledgerInvalid || analysisInvalid || !!appsError
|
||||||
const changed = (Object.keys(saved) as (keyof typeof saved)[]).filter((k) => cur[k] !== saved[k])
|
const changed = (Object.keys(saved) as (keyof typeof saved)[]).filter((k) => cur[k] !== saved[k])
|
||||||
const dirty = values ? changed.length + verifyKeys.length + ledgerKeys.length : 0
|
const dirty = values ? changed.length + verifyKeys.length + ledgerKeys.length + analysisKeys.length : 0
|
||||||
// 外壳唯一的 blocker 在地址(pathname)变化时问:切标签、导航、⌘K、⌥ 数字、后退都算
|
// 外壳唯一的 blocker 在地址(pathname)变化时问:切标签、导航、⌘K、⌥ 数字、后退都算
|
||||||
useLeaveGuard(dirty > 0, () => confirmLeave(dirty))
|
useLeaveGuard(dirty > 0, () => confirmLeave(dirty))
|
||||||
|
|
||||||
@@ -818,6 +829,11 @@ function PrefsTab() {
|
|||||||
setFieldError(null)
|
setFieldError(null)
|
||||||
setLedgerDraft({ ...ledgerCur, ...patch })
|
setLedgerDraft({ ...ledgerCur, ...patch })
|
||||||
}
|
}
|
||||||
|
const editAnalysis = (patch: Partial<AnalysisDraft>) => {
|
||||||
|
setSaveError(null)
|
||||||
|
setFieldError(null)
|
||||||
|
setAnalysisDraft({ ...analysisCur, ...patch })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 署名只在这台浏览器里,先落本机:它不该跟着后端一起「没存上」。运行默认值
|
* 署名只在这台浏览器里,先落本机:它不该跟着后端一起「没存上」。运行默认值
|
||||||
@@ -853,6 +869,8 @@ function PrefsTab() {
|
|||||||
const verify = verifyKeys.length ? verifyPayload(verifyCur) : null
|
const verify = verifyKeys.length ? verifyPayload(verifyCur) : null
|
||||||
// 口径账本一组同样整组写(LedgerSettings 的 ledgerPayload)
|
// 口径账本一组同样整组写(LedgerSettings 的 ledgerPayload)
|
||||||
const ledger = ledgerKeys.length ? ledgerPayload(ledgerCur) : null
|
const ledger = ledgerKeys.length ? ledgerPayload(ledgerCur) : null
|
||||||
|
// 分析一组同样整组写(AnalysisSettings 的 analysisPayload)
|
||||||
|
const analysis = analysisKeys.length ? analysisPayload(analysisCur) : null
|
||||||
// 起草模型一组:两项一起写,空着的存 null(跟随助手的模型)
|
// 起草模型一组:两项一起写,空着的存 null(跟随助手的模型)
|
||||||
const catalogDraft = draftChanged ? { provider: cur.draftProvider || null, model: cur.draftModel.trim() || null } : null
|
const catalogDraft = draftChanged ? { provider: cur.draftProvider || null, model: cur.draftModel.trim() || null } : null
|
||||||
// 应用一组同样整组写:别的键(以后加的)照原样带回去
|
// 应用一组同样整组写:别的键(以后加的)照原样带回去
|
||||||
@@ -860,7 +878,7 @@ function PrefsTab() {
|
|||||||
? { ...(values.apps ?? {}), on_demand_daily_limit: Number(cur.appsDaily.trim()), user_personal_apps: cur.appsPersonal,
|
? { ...(values.apps ?? {}), on_demand_daily_limit: Number(cur.appsDaily.trim()), user_personal_apps: cur.appsPersonal,
|
||||||
share_links: cur.appsShare }
|
share_links: cur.appsShare }
|
||||||
: null
|
: null
|
||||||
if (runChanged || judge || verify || ledger || catalogDraft || apps) {
|
if (runChanged || judge || verify || ledger || analysis || catalogDraft || apps) {
|
||||||
const run = {
|
const run = {
|
||||||
...(values.run ?? {}),
|
...(values.run ?? {}),
|
||||||
default_memory_scope: cur.scope,
|
default_memory_scope: cur.scope,
|
||||||
@@ -874,7 +892,7 @@ function PrefsTab() {
|
|||||||
}
|
}
|
||||||
const out = await api.settings.put({
|
const out = await api.settings.put({
|
||||||
...(runChanged ? { run } : {}), ...(judge ? { judge } : {}), ...(verify ? { verify } : {}),
|
...(runChanged ? { run } : {}), ...(judge ? { judge } : {}), ...(verify ? { verify } : {}),
|
||||||
...(ledger ? { ledger } : {}),
|
...(ledger ? { ledger } : {}), ...(analysis ? { analysis } : {}),
|
||||||
...(catalogDraft ? { catalog_draft: catalogDraft } : {}), ...(apps ? { apps } : {}),
|
...(catalogDraft ? { catalog_draft: catalogDraft } : {}), ...(apps ? { apps } : {}),
|
||||||
})
|
})
|
||||||
setValues((v) => ({
|
setValues((v) => ({
|
||||||
@@ -883,6 +901,7 @@ function PrefsTab() {
|
|||||||
...(judge ? { judge: out?.judge ?? judge } : {}),
|
...(judge ? { judge: out?.judge ?? judge } : {}),
|
||||||
...(verify ? { verify: out?.verify ?? verify } : {}),
|
...(verify ? { verify: out?.verify ?? verify } : {}),
|
||||||
...(ledger ? { ledger: out?.ledger ?? ledger } : {}),
|
...(ledger ? { ledger: out?.ledger ?? ledger } : {}),
|
||||||
|
...(analysis ? { analysis: out?.analysis ?? analysis } : {}),
|
||||||
...(catalogDraft ? { catalog_draft: out?.catalog_draft ?? catalogDraft } : {}),
|
...(catalogDraft ? { catalog_draft: out?.catalog_draft ?? catalogDraft } : {}),
|
||||||
...(apps ? { apps: out?.apps ?? apps } : {}),
|
...(apps ? { apps: out?.apps ?? apps } : {}),
|
||||||
}))
|
}))
|
||||||
@@ -893,6 +912,7 @@ function PrefsTab() {
|
|||||||
setDraft(null)
|
setDraft(null)
|
||||||
setVerifyDraft(null)
|
setVerifyDraft(null)
|
||||||
setLedgerDraft(null)
|
setLedgerDraft(null)
|
||||||
|
setAnalysisDraft(null)
|
||||||
setSavedFlash(Date.now())
|
setSavedFlash(Date.now())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 署名已经存上了:留在草稿里的只剩运行默认值,条上的计数跟着变少
|
// 署名已经存上了:留在草稿里的只剩运行默认值,条上的计数跟着变少
|
||||||
@@ -1209,6 +1229,8 @@ function PrefsTab() {
|
|||||||
|
|
||||||
{values.ledger && <LedgerSection cur={ledgerCur} errors={ledgerErrs} onEdit={editLedger} />}
|
{values.ledger && <LedgerSection cur={ledgerCur} errors={ledgerErrs} onEdit={editLedger} />}
|
||||||
|
|
||||||
|
{values.analysis && <AnalysisSection cur={analysisCur} errors={analysisErrs} onEdit={editAnalysis} />}
|
||||||
|
|
||||||
{values.catalog_draft && (
|
{values.catalog_draft && (
|
||||||
<DraftModelSection provider={cur.draftProvider} model={cur.draftModel} onEdit={edit}
|
<DraftModelSection provider={cur.draftProvider} model={cur.draftModel} onEdit={edit}
|
||||||
error={fieldError?.field === 'catalog_draft.provider' ? fieldError.message : undefined} />
|
error={fieldError?.field === 'catalog_draft.provider' ? fieldError.message : undefined} />
|
||||||
@@ -1273,7 +1295,7 @@ function PrefsTab() {
|
|||||||
draftProvider: DRAFT_MODEL_SETTING_TEXT.provider, draftModel: DRAFT_MODEL_SETTING_TEXT.model,
|
draftProvider: DRAFT_MODEL_SETTING_TEXT.provider, draftModel: DRAFT_MODEL_SETTING_TEXT.model,
|
||||||
appsDaily: APP_SETTING_TEXT.onDemandLimit, appsPersonal: APP_SETTING_TEXT.personalApps,
|
appsDaily: APP_SETTING_TEXT.onDemandLimit, appsPersonal: APP_SETTING_TEXT.personalApps,
|
||||||
appsShare: APP_SETTING_TEXT.shareLinks,
|
appsShare: APP_SETTING_TEXT.shareLinks,
|
||||||
})[k]).concat(verifyLabels(verifyKeys), ledgerLabels(ledgerKeys)).join('、')}
|
})[k]).concat(verifyLabels(verifyKeys), ledgerLabels(ledgerKeys), analysisLabels(analysisKeys)).join('、')}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{saveError != null && (
|
{saveError != null && (
|
||||||
@@ -1284,13 +1306,17 @@ function PrefsTab() {
|
|||||||
)}
|
)}
|
||||||
<span className="flex-1" />
|
<span className="flex-1" />
|
||||||
<button className="btn btn-sm btn-ghost" disabled={saving}
|
<button className="btn btn-sm btn-ghost" disabled={saving}
|
||||||
onClick={() => { setDraft(null); setVerifyDraft(null); setLedgerDraft(null); setSaveError(null); setFieldError(null) }}>放弃</button>
|
onClick={() => {
|
||||||
|
setDraft(null); setVerifyDraft(null); setLedgerDraft(null); setAnalysisDraft(null); setSaveError(null)
|
||||||
|
setFieldError(null)
|
||||||
|
}}>放弃</button>
|
||||||
<button className="btn btn-sm btn-primary" disabled={saving || invalid}
|
<button className="btn btn-sm btn-primary" disabled={saving || invalid}
|
||||||
title={guardInvalid ? '护栏设置中有无效值,请修正后再保存'
|
title={guardInvalid ? '护栏设置中有无效值,请修正后再保存'
|
||||||
: judgeInvalid ? '证据裁判的上限有无效值,请修正后再保存'
|
: judgeInvalid ? '证据裁判的上限有无效值,请修正后再保存'
|
||||||
: appsError ? APP_SETTING_TEXT.onDemandLimitInvalid
|
: appsError ? APP_SETTING_TEXT.onDemandLimitInvalid
|
||||||
: verifyInvalid ? VERIFY_SETTING_TEXT.section.saveInvalid
|
: verifyInvalid ? VERIFY_SETTING_TEXT.section.saveInvalid
|
||||||
: ledgerInvalid ? LEDGER_UI.settings.saveInvalid : undefined}
|
: ledgerInvalid ? LEDGER_UI.settings.saveInvalid
|
||||||
|
: analysisInvalid ? ANALYSIS_TEXT.settings.invalid : undefined}
|
||||||
onClick={() => void save()}>
|
onClick={() => void save()}>
|
||||||
{saving ? <Spinner size={11} /> : <Check size={12} aria-hidden />} 保存设置
|
{saving ? <Spinner size={11} /> : <Check size={12} aria-hidden />} 保存设置
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Field, SectionBar } from '../../components/ui'
|
||||||
|
import {
|
||||||
|
ANALYSIS_SETTING_GROUPS, ANALYSIS_SETTING_KEYS, ANALYSIS_SETTING_LABEL, ANALYSIS_TEXT, settingRangeText, settingValueProblem,
|
||||||
|
type AnalysisSettingKey,
|
||||||
|
} from '../../lib/termsAnalysis'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 设置页「偏好设置」里「分析」一节(服务端 api/settings.py 的 analysis 一组,docs/design/analysis-round/01-算子与判定.md §10):
|
||||||
|
* 判定用的阈值(「主要来源」的占比、「超出常规波动」的倍数……)、Agent 每次执行最多分析几次、最多下钻几层、列出前几段。
|
||||||
|
*
|
||||||
|
* 照 LedgerSettings.tsx 的写法:草稿由 PrefsTab 持有、和其余设置共用一条保存条(这一节只画、只报改动),存的时候整组写
|
||||||
|
* analysis——服务端对没给的项回落到缺省,只发改过的那一项会把别的项冲回缺省。字段名、范围都照服务端 analysis/contract.py
|
||||||
|
* (LABELS、RANGES,lib/termsAnalysis 一字不差地抄着,check-manage 逐项比对),422 的报错写的也是这些叫法。两两之间的大小
|
||||||
|
* (「明显超出」要大于「超出常规波动」、最多用几期不小于至少几期、「集中」不小于「主要来源」)交给服务端核,报错照原话显示。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type AnalysisDraft = Record<AnalysisSettingKey, string>
|
||||||
|
|
||||||
|
const isRecord = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v)
|
||||||
|
|
||||||
|
/** GET /settings 的 analysis 一组(服务端给的是生效的样子,没存过的补了缺省)→ 草稿 */
|
||||||
|
export function analysisDraftOf(group: unknown): AnalysisDraft {
|
||||||
|
const g = isRecord(group) ? group : {}
|
||||||
|
return Object.fromEntries(ANALYSIS_SETTING_KEYS.map((k) => {
|
||||||
|
const v = g[k]
|
||||||
|
return [k, typeof v === 'number' && Number.isFinite(v) ? String(v) : '']
|
||||||
|
})) as AnalysisDraft
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 改过的项:字不同、而且不是同一个数(服务端把 2 存成 2.0,写「2.0」不算改) */
|
||||||
|
export function analysisChanged(cur: AnalysisDraft, saved: AnalysisDraft): AnalysisSettingKey[] {
|
||||||
|
const same = (a: string, b: string) => a.trim() === b.trim()
|
||||||
|
|| (a.trim() !== '' && b.trim() !== '' && Number.isFinite(Number(a)) && Number(a) === Number(b))
|
||||||
|
return ANALYSIS_SETTING_KEYS.filter((k) => !same(cur[k], saved[k]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存条上这几项的叫法:和字段名、服务端报错同一套 */
|
||||||
|
export const analysisLabels = (keys: AnalysisSettingKey[]): string[] => keys.map((k) => ANALYSIS_SETTING_LABEL[k])
|
||||||
|
|
||||||
|
/** 各项的问题(类型、范围)。只校验改过的:存着的值不合规不该挡住别的设置保存 */
|
||||||
|
export function analysisErrors(cur: AnalysisDraft, saved: AnalysisDraft): Partial<Record<AnalysisSettingKey, string>> {
|
||||||
|
const out: Partial<Record<AnalysisSettingKey, string>> = {}
|
||||||
|
for (const k of analysisChanged(cur, saved)) {
|
||||||
|
const problem = settingValueProblem(k, cur[k])
|
||||||
|
if (problem) out[k] = problem
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 草稿 → 存进去的整组 analysis */
|
||||||
|
export function analysisPayload(cur: AnalysisDraft): Record<string, number> {
|
||||||
|
return Object.fromEntries(ANALYSIS_SETTING_KEYS.map((k) => [k, Number(cur[k].trim())]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = (k: AnalysisSettingKey) => `pref-analysis-${k.replace(/_/g, '-')}`
|
||||||
|
|
||||||
|
export function AnalysisSection({ cur, errors, onEdit }: {
|
||||||
|
cur: AnalysisDraft
|
||||||
|
errors: Partial<Record<AnalysisSettingKey, string>>
|
||||||
|
onEdit: (patch: Partial<AnalysisDraft>) => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section data-analysis-settings="">
|
||||||
|
<SectionBar title={ANALYSIS_TEXT.settings.title} hint={ANALYSIS_TEXT.settings.hint} />
|
||||||
|
<div className="space-y-4">
|
||||||
|
{ANALYSIS_SETTING_GROUPS.map((group) => (
|
||||||
|
<div key={group.title} data-analysis-settings-group={group.title}>
|
||||||
|
<div className="mb-1.5 text-2xs font-medium text-dim">{group.title}</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3 max-sm:grid-cols-1">
|
||||||
|
{group.keys.map((k) => (
|
||||||
|
<Field key={k} label={ANALYSIS_SETTING_LABEL[k]} htmlFor={id(k)} error={errors[k]} hint={settingRangeText(k)}>
|
||||||
|
{(p) => (
|
||||||
|
<input {...p} className="field tnum max-w-40" inputMode="decimal" value={cur[k]} data-analysis-setting={k}
|
||||||
|
onChange={(e) => onEdit({ [k]: e.target.value } as Partial<AnalysisDraft>)} />
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1135,6 +1135,87 @@ await section('设置 · 证据裁判', async () => {
|
|||||||
await old.close()
|
await old.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await section('设置 · 分析(判定用的阈值、次数上限:叫法同契约,整组写 analysis,值不合规不让存,服务端的报错照原话显示)', async () => {
|
||||||
|
// 读的是真后端补齐缺省之后的一组(有就用它),写走假的一份
|
||||||
|
let stored = await get('/settings')
|
||||||
|
if (!stored.analysis) {
|
||||||
|
check('服务端的设置里有 analysis 一组(分析能力一轮)', false, Object.keys(stored).join(','))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const saved = { ...stored.analysis }
|
||||||
|
let reject = null
|
||||||
|
const { page, sent, errors, close } = await open('/settings/prefs', {
|
||||||
|
handlers: [
|
||||||
|
[/^GET \/settings$/, (route) => json(stored)(route)],
|
||||||
|
[/^PUT \/settings$/, (route, { body }) => {
|
||||||
|
if (reject) return json({ detail: reject }, 422)(route)
|
||||||
|
stored = { ...stored, ...body.values }
|
||||||
|
return json(stored)(route)
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const sec = page.locator('[data-analysis-settings]')
|
||||||
|
await sec.waitFor({ timeout: 8000 }).catch(() => {})
|
||||||
|
const py = readFileSync(new URL('../backend/app/analysis/contract.py', import.meta.url), 'utf8')
|
||||||
|
const labels = [...(py.match(/^LABELS: dict\[str, str\] = \{([\s\S]*?)^\}/m)?.[1] ?? '').matchAll(/"([^"]+)":\s*"([^"]+)"/g)]
|
||||||
|
const text = await sec.innerText().catch(() => '')
|
||||||
|
check('这一节在偏好设置里,十七项的叫法都是契约 LABELS 的', labels.length === 17 && labels.every(([, , label]) => text.includes(label)),
|
||||||
|
labels.filter(([, , label]) => !text.includes(label)).map(([, k]) => k).join('、'))
|
||||||
|
check('……分成四组:变化拆解、波动基线与趋势判断、差异检验、次数与层数',
|
||||||
|
(await sec.locator('[data-analysis-settings-group]').evaluateAll((els) => els.map((e) => e.dataset.analysisSettingsGroup))).join('、')
|
||||||
|
=== '变化拆解、波动基线与趋势判断、差异检验、次数与层数')
|
||||||
|
const val = (k) => page.locator(`[data-analysis-setting="${k}"]`).inputValue()
|
||||||
|
check('显示的是服务端生效的值', await val('main_share') === String(saved.main_share) && await val('min_history') === String(saved.min_history)
|
||||||
|
&& await val('max_calls') === String(saved.max_calls), `${await val('main_share')} / ${await val('min_history')} / ${await val('max_calls')}`)
|
||||||
|
check('……每项下面写着范围(同契约 RANGES)', text.includes('0.3 到 0.9 之间') && text.includes('4 到 104 之间的整数'))
|
||||||
|
|
||||||
|
await page.locator('[data-analysis-setting="main_share"]').fill('0.95')
|
||||||
|
const bar = page.locator('[data-prefs-bar]')
|
||||||
|
await bar.waitFor({ timeout: 3000 }).catch(() => {})
|
||||||
|
check('超出范围:当场说(说法同服务端),保存键不能点', (await sec.innerText()).includes('「主要来源」的占比要在 0.3 到 0.9 之间')
|
||||||
|
&& await bar.getByRole('button', { name: '保存设置' }).isDisabled())
|
||||||
|
await page.locator('[data-analysis-setting="min_history"]').fill('9.5')
|
||||||
|
check('整数项填了小数:说要填整数', (await sec.innerText()).includes('至少几期历史要填整数'))
|
||||||
|
await page.locator('[data-analysis-setting="main_share"]').fill('0.6')
|
||||||
|
await page.locator('[data-analysis-setting="min_history"]').fill('10')
|
||||||
|
const barText = await bar.innerText().catch(() => '')
|
||||||
|
check('保存条写改了哪几项(契约的叫法)', barText.includes('有 2 项未保存') && barText.includes('「主要来源」的占比')
|
||||||
|
&& barText.includes('至少几期历史'), barText.replace(/\s+/g, ' '))
|
||||||
|
await bar.getByRole('button', { name: '保存设置' }).click()
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
const put = sent.filter((x) => x.key === 'PUT /settings').at(-1)?.body?.values ?? {}
|
||||||
|
const a = put.analysis ?? {}
|
||||||
|
check('整组写 analysis(十七项都在,没改的照原值、写成数)', Object.keys(a).length === 17 && a.main_share === 0.6 && a.min_history === 10
|
||||||
|
&& a.k_warn === saved.k_warn && a.table_top_k === saved.table_top_k, JSON.stringify(a).slice(0, 160))
|
||||||
|
check('……只改了分析:不碰别的组', Object.keys(put).join(',') === 'analysis', Object.keys(put).join(','))
|
||||||
|
check('……存完显示「已保存」', (await bar.innerText().catch(() => '')).includes('已保存'))
|
||||||
|
|
||||||
|
// 两两之间的大小交给服务端核:「明显超出」不大于「超出常规波动」时 422,原话写在保存条上,草稿留着
|
||||||
|
reject = '「明显超出」的倍数要大于「超出常规波动」的倍数'
|
||||||
|
await page.locator('[data-analysis-setting="k_alert"]').fill('1.5')
|
||||||
|
await bar.getByRole('button', { name: '保存设置' }).click()
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
const failed = await bar.innerText().catch(() => '')
|
||||||
|
check('服务端拒收:保存条照原话写为什么,草稿还在', failed.includes(reject) && await val('k_alert') === '1.5', failed.replace(/\s+/g, ' '))
|
||||||
|
reject = null
|
||||||
|
await page.locator('[data-analysis-setting="k_alert"]').fill(String(stored.analysis.k_alert))
|
||||||
|
check('改回原值:不再算改动', !(await bar.innerText().catch(() => '')).includes('未保存'))
|
||||||
|
if (SHOTS) {
|
||||||
|
await sec.scrollIntoViewIfNeeded()
|
||||||
|
await page.waitForTimeout(200)
|
||||||
|
await sec.screenshot({ path: `${SHOTS}/prefs-analysis-${THEME}.png` })
|
||||||
|
}
|
||||||
|
check('没有运行时报错', errors.length === 0, errors[0] ?? '')
|
||||||
|
await close()
|
||||||
|
|
||||||
|
// 老后端没有 analysis 这一组:不显示,也不写
|
||||||
|
const legacy = { ...stored }
|
||||||
|
delete legacy.analysis
|
||||||
|
const old = await open('/settings/prefs', { handlers: [[/^GET \/settings$/, (route) => json(legacy)(route)]] })
|
||||||
|
check('老后端没有分析设置:这一组不出现', await old.page.locator('[data-analysis-settings]').count() === 0)
|
||||||
|
await old.close()
|
||||||
|
})
|
||||||
|
|
||||||
await section('数据 · 数据库', async () => {
|
await section('数据 · 数据库', async () => {
|
||||||
// 上传的表格按来源标记认(origin),不按路径:上传库的路径随版本变
|
// 上传的表格按来源标记认(origin),不按路径:上传库的路径随版本变
|
||||||
const dbs = sources.filter((s) => s.origin !== 'upload')
|
const dbs = sources.filter((s) => s.origin !== 'upload')
|
||||||
|
|||||||
Reference in New Issue
Block a user