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,
|
||||
} from './settings/LedgerSettings'
|
||||
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'
|
||||
|
||||
// 提到模块级:tab 名同时是 URL 的最后一段,两处各写一份迟早对不上。
|
||||
@@ -697,6 +701,8 @@ function PrefsTab() {
|
||||
const [verifySaves, setVerifySaves] = useState(0)
|
||||
// 口径账本一组(自动按口径出具、每日上限、比对时限、升版后重算几组,pages/settings/LedgerSettings):草稿另放一份,共用保存条
|
||||
const [ledgerDraft, setLedgerDraft] = useState<LedgerDraft | null>(null)
|
||||
// 分析一组(判定用的阈值、次数上限,pages/settings/AnalysisSettings):草稿另放一份,共用保存条
|
||||
const [analysisDraft, setAnalysisDraft] = useState<AnalysisDraft | null>(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [savedFlash, setSavedFlash] = useState(0)
|
||||
const [saveError, setSaveError] = useState<unknown>(null)
|
||||
@@ -791,9 +797,14 @@ function PrefsTab() {
|
||||
const ledgerKeys = values?.ledger ? ledgerChanged(ledgerCur, ledgerSaved) : []
|
||||
const ledgerErrs = ledgerErrors(ledgerCur, ledgerSaved)
|
||||
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 dirty = values ? changed.length + verifyKeys.length + ledgerKeys.length : 0
|
||||
const dirty = values ? changed.length + verifyKeys.length + ledgerKeys.length + analysisKeys.length : 0
|
||||
// 外壳唯一的 blocker 在地址(pathname)变化时问:切标签、导航、⌘K、⌥ 数字、后退都算
|
||||
useLeaveGuard(dirty > 0, () => confirmLeave(dirty))
|
||||
|
||||
@@ -818,6 +829,11 @@ function PrefsTab() {
|
||||
setFieldError(null)
|
||||
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
|
||||
// 口径账本一组同样整组写(LedgerSettings 的 ledgerPayload)
|
||||
const ledger = ledgerKeys.length ? ledgerPayload(ledgerCur) : null
|
||||
// 分析一组同样整组写(AnalysisSettings 的 analysisPayload)
|
||||
const analysis = analysisKeys.length ? analysisPayload(analysisCur) : 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,
|
||||
share_links: cur.appsShare }
|
||||
: null
|
||||
if (runChanged || judge || verify || ledger || catalogDraft || apps) {
|
||||
if (runChanged || judge || verify || ledger || analysis || catalogDraft || apps) {
|
||||
const run = {
|
||||
...(values.run ?? {}),
|
||||
default_memory_scope: cur.scope,
|
||||
@@ -874,7 +892,7 @@ function PrefsTab() {
|
||||
}
|
||||
const out = await api.settings.put({
|
||||
...(runChanged ? { run } : {}), ...(judge ? { judge } : {}), ...(verify ? { verify } : {}),
|
||||
...(ledger ? { ledger } : {}),
|
||||
...(ledger ? { ledger } : {}), ...(analysis ? { analysis } : {}),
|
||||
...(catalogDraft ? { catalog_draft: catalogDraft } : {}), ...(apps ? { apps } : {}),
|
||||
})
|
||||
setValues((v) => ({
|
||||
@@ -883,6 +901,7 @@ function PrefsTab() {
|
||||
...(judge ? { judge: out?.judge ?? judge } : {}),
|
||||
...(verify ? { verify: out?.verify ?? verify } : {}),
|
||||
...(ledger ? { ledger: out?.ledger ?? ledger } : {}),
|
||||
...(analysis ? { analysis: out?.analysis ?? analysis } : {}),
|
||||
...(catalogDraft ? { catalog_draft: out?.catalog_draft ?? catalogDraft } : {}),
|
||||
...(apps ? { apps: out?.apps ?? apps } : {}),
|
||||
}))
|
||||
@@ -893,6 +912,7 @@ function PrefsTab() {
|
||||
setDraft(null)
|
||||
setVerifyDraft(null)
|
||||
setLedgerDraft(null)
|
||||
setAnalysisDraft(null)
|
||||
setSavedFlash(Date.now())
|
||||
} catch (e) {
|
||||
// 署名已经存上了:留在草稿里的只剩运行默认值,条上的计数跟着变少
|
||||
@@ -1209,6 +1229,8 @@ function PrefsTab() {
|
||||
|
||||
{values.ledger && <LedgerSection cur={ledgerCur} errors={ledgerErrs} onEdit={editLedger} />}
|
||||
|
||||
{values.analysis && <AnalysisSection cur={analysisCur} errors={analysisErrs} onEdit={editAnalysis} />}
|
||||
|
||||
{values.catalog_draft && (
|
||||
<DraftModelSection provider={cur.draftProvider} model={cur.draftModel} onEdit={edit}
|
||||
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,
|
||||
appsDaily: APP_SETTING_TEXT.onDemandLimit, appsPersonal: APP_SETTING_TEXT.personalApps,
|
||||
appsShare: APP_SETTING_TEXT.shareLinks,
|
||||
})[k]).concat(verifyLabels(verifyKeys), ledgerLabels(ledgerKeys)).join('、')}
|
||||
})[k]).concat(verifyLabels(verifyKeys), ledgerLabels(ledgerKeys), analysisLabels(analysisKeys)).join('、')}
|
||||
</span>
|
||||
</span>
|
||||
{saveError != null && (
|
||||
@@ -1284,13 +1306,17 @@ function PrefsTab() {
|
||||
)}
|
||||
<span className="flex-1" />
|
||||
<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}
|
||||
title={guardInvalid ? '护栏设置中有无效值,请修正后再保存'
|
||||
: judgeInvalid ? '证据裁判的上限有无效值,请修正后再保存'
|
||||
: appsError ? APP_SETTING_TEXT.onDemandLimitInvalid
|
||||
: 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()}>
|
||||
{saving ? <Spinner size={11} /> : <Check size={12} aria-hidden />} 保存设置
|
||||
</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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user