Files
UHD_Media_Tool/apps/web/app/components/settings/agent.tsx
T
rootandClaude Opus 5.5 639fdeacae fix(web): settings page layout pass (FE-7)
- Section header (h2 + description) and section blocks as h3; unified
  block padding and title size
- Global settings forms use label-left / control-right rows, stacked
  below md; control width fixed by type (number short, text/select
  medium, wide/textarea full)
- Per-block SaveBar pinned at the block end with dirty hint; naming
  template saves moved from the header to the bar
- Console shell uses overflow-x-clip so the sticky header and sticky
  section nav actually stick; anchor jumps re-align while async content
  loads and land below the header
- Section content uses a single minmax(0,1fr) column so wide tables
  scroll inside their card instead of widening the page
- useSettings fills missing keys from DEFAULT_SETTINGS so an older
  backend no longer crashes the metadata section

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 17:16:02 +08:00

231 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { AgentToolsBlock } from "./agent-tools.tsx";
import { WEBSEARCH_CARD } from "./agent-websearch.tsx";
import { PiUpstreamsBlock } from "./pi-upstreams.tsx";
import type { FieldValue } from "./resource-section.tsx";
import { num, str } from "./resource-section.tsx";
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
// Agent 分区:上游(pi_upstream)+ 上下文与子代理(pi)+ 预算(budget.agent* 与单次上限)+ 自动调查 + 网页搜索 + 高级
/** 可清空的数字:空串 → null */
const nullNum = (v: FieldValue | undefined) => (str(v) === "" ? null : Number(str(v)));
const RUNTIME_CARD: CardSpec = {
anchor: "runtime",
title: "上下文与子会话",
fields: [
{
name: "pi.contextBudget",
label: "上下文预算(token)",
kind: "number",
hint: "4096–2000000,超过模型窗口时按窗口算",
},
{ name: "pi.subagentsEnabled", label: "允许子会话", kind: "switch" },
{ name: "pi.compaction.enabled", label: "上下文压缩", kind: "switch" },
{
name: "pi.compaction.thresholdRatio",
label: "压缩触发比例",
kind: "number",
hint: "0.3–0.95,用到预算的这个比例时压缩",
visible: (v) => v["pi.compaction.enabled"] === true,
},
],
fromSettings: (s) => ({
"pi.contextBudget": n(s.pi.contextBudget),
"pi.subagentsEnabled": s.pi.subagentsEnabled,
"pi.compaction.enabled": s.pi.compaction.enabled,
"pi.compaction.thresholdRatio": n(s.pi.compaction.thresholdRatio),
}),
toPatch: (v) => ({
pi: {
contextBudget: num(v["pi.contextBudget"]),
subagentsEnabled: v["pi.subagentsEnabled"] === true,
compaction: {
enabled: v["pi.compaction.enabled"] === true,
thresholdRatio: num(v["pi.compaction.thresholdRatio"]),
},
},
}),
};
const BUDGET_CARD: CardSpec = {
anchor: "budget",
title: "预算",
description: "超出每日预算的运行停在「超出预算」,等你放行。",
fields: [
{ name: "budget.agentMaxRunsPerDay", label: "每日运行次数", kind: "number" },
{ name: "budget.agentMaxCostUsdPerDay", label: "每日费用(USD)", kind: "number" },
{ name: "agent.maxToolCalls", label: "单次工具调用上限", kind: "number", hint: "1–500" },
{ name: "agent.maxDurationSec", label: "单次时长上限(秒)", kind: "number" },
{ name: "agent.maxTokensPerRun", label: "单次 token 上限", kind: "number", hint: "留空不限" },
{ name: "agent.maxCostUsdPerRun", label: "单次费用上限(USD)", kind: "number", hint: "留空不限" },
],
fromSettings: (s) => ({
"budget.agentMaxRunsPerDay": n(s.budget.agentMaxRunsPerDay),
"budget.agentMaxCostUsdPerDay": n(s.budget.agentMaxCostUsdPerDay),
"agent.maxToolCalls": n(s.agent.maxToolCalls),
"agent.maxDurationSec": n(s.agent.maxDurationSec),
"agent.maxTokensPerRun": n(s.agent.maxTokensPerRun),
"agent.maxCostUsdPerRun": n(s.agent.maxCostUsdPerRun),
}),
toPatch: (v) => ({
budget: {
agentMaxRunsPerDay: num(v["budget.agentMaxRunsPerDay"]),
agentMaxCostUsdPerDay: num(v["budget.agentMaxCostUsdPerDay"]),
},
agent: {
maxToolCalls: num(v["agent.maxToolCalls"]),
maxDurationSec: num(v["agent.maxDurationSec"]),
maxTokensPerRun: nullNum(v["agent.maxTokensPerRun"]),
maxCostUsdPerRun: nullNum(v["agent.maxCostUsdPerRun"]),
},
}),
};
const INVESTIGATE_CARD: CardSpec = {
anchor: "investigate",
title: "自动调查",
description: "待处理事项(识别有歧义、下载失败等)创建时,自动发起一次有界调查。",
fields: [
{ name: "agent.autoInvestigate", label: "自动调查", kind: "switch", wide: true },
{
name: "agent.autoInvestigateMode",
label: "模式",
kind: "select",
options: [
{ value: "investigate", label: "只读调查,结论交还给你" },
{ value: "recover", label: "授权内恢复(重试任务、重新校验、确认识别)" },
],
hint: "恢复只在已发布策略或持续授权覆盖时执行,换源、删除、清理始终排除;没有授权时按只读调查",
wide: true,
visible: (v) => v["agent.autoInvestigate"] === true,
},
{
name: "agent.investigateMaxToolCalls",
label: "工具调用上限",
kind: "number",
hint: "1–100",
visible: (v) => v["agent.autoInvestigate"] === true,
},
{
name: "agent.investigateMaxAttempts",
label: "同一根因最多恢复次数",
kind: "number",
hint: "1–10,用完后只交还给你",
visible: (v) => v["agent.autoInvestigate"] === true && v["agent.autoInvestigateMode"] === "recover",
},
{
name: "agent.recoverVerifySec",
label: "恢复后核实时长(秒)",
kind: "number",
hint: "等下载或任务状态变化的最长时间",
visible: (v) => v["agent.autoInvestigate"] === true && v["agent.autoInvestigateMode"] === "recover",
},
],
fromSettings: (s) => ({
"agent.autoInvestigate": s.agent.autoInvestigate,
"agent.autoInvestigateMode": s.agent.autoInvestigateMode,
"agent.investigateMaxToolCalls": n(s.agent.investigateMaxToolCalls),
"agent.investigateMaxAttempts": n(s.agent.investigateMaxAttempts),
"agent.recoverVerifySec": n(s.agent.recoverVerifySec),
}),
toPatch: (v) => ({
agent: {
autoInvestigate: v["agent.autoInvestigate"] === true,
autoInvestigateMode: v["agent.autoInvestigateMode"] === "recover" ? "recover" : "investigate",
investigateMaxToolCalls: num(v["agent.investigateMaxToolCalls"]),
investigateMaxAttempts: num(v["agent.investigateMaxAttempts"]),
recoverVerifySec: num(v["agent.recoverVerifySec"]),
},
}),
};
const ADVANCED_CARD: CardSpec = {
anchor: "advanced",
title: "高级",
fields: [
{ name: "agent.goalPollSec", label: "目标轮询间隔(秒)", kind: "number", hint: "等下载、入库结束时的检查间隔" },
{
name: "agent.goalWaitHours",
label: "目标最长等待(小时)",
kind: "number",
hint: "超时后按「仍在等待」汇报结束",
},
{
name: "agent.optionsTimeoutSec",
label: "方案选择等待(秒)",
kind: "number",
hint: "Agent 给出方案后等你选择的最长时间",
},
{
name: "agent.optionsTimeoutAction",
label: "超时未选",
kind: "select",
options: [
{ value: "recommended", label: "自动选推荐项" },
{ value: "stop", label: "不选,Agent 收尾" },
],
},
{
name: "agent.approvalTimeoutSec",
label: "会话内审批有效期(秒)",
kind: "number",
hint: "步骤处的审批卡过期未批准,按拒绝处理",
},
{
name: "agent.sessionDir",
label: "会话目录",
kind: "text",
wide: true,
placeholder: "留空用 DATA_DIR/agent-sessions",
hint: "绝对路径,api 与 worker 都要可读写;有运行中的会话时不要改",
},
{
name: "agent.fakeScript",
label: "fake 脚本(JSON)",
kind: "textarea",
hint: "只对 fake 上游生效,留空用内置演示脚本;格式见 docs/runbook/agent.md",
},
],
fromSettings: (s) => ({
"agent.goalPollSec": n(s.agent.goalPollSec),
"agent.goalWaitHours": n(s.agent.goalWaitHours),
"agent.optionsTimeoutSec": n(s.agent.optionsTimeoutSec),
"agent.optionsTimeoutAction": s.agent.optionsTimeoutAction,
"agent.approvalTimeoutSec": n(s.agent.approvalTimeoutSec),
"agent.sessionDir": s.agent.sessionDir ?? "",
"agent.fakeScript": s.agent.fakeScript ?? "",
}),
toPatch: (v) => ({
agent: {
goalPollSec: num(v["agent.goalPollSec"]),
goalWaitHours: num(v["agent.goalWaitHours"]),
optionsTimeoutSec: num(v["agent.optionsTimeoutSec"]),
optionsTimeoutAction: v["agent.optionsTimeoutAction"] === "stop" ? "stop" : "recommended",
approvalTimeoutSec: num(v["agent.approvalTimeoutSec"]),
sessionDir: str(v["agent.sessionDir"]) || null,
fakeScript: str(v["agent.fakeScript"]) || null,
},
}),
};
export function AgentSection() {
return (
<div className="grid grid-cols-1 gap-8">
<PiUpstreamsBlock />
<SettingsGate>
{(s) => (
<>
<SettingsCard section="agent" spec={RUNTIME_CARD} settings={s} />
<SettingsCard section="agent" spec={BUDGET_CARD} settings={s} />
<SettingsCard section="agent" spec={INVESTIGATE_CARD} settings={s} />
<SettingsCard section="agent" spec={WEBSEARCH_CARD} settings={s} />
<SettingsCard section="agent" spec={ADVANCED_CARD} settings={s} />
</>
)}
</SettingsGate>
<AgentToolsBlock />
</div>
);
}