diff --git a/apps/server/src/modules/agent/challenge.ts b/apps/server/src/modules/agent/challenge.ts index 4143e38b..393856be 100644 --- a/apps/server/src/modules/agent/challenge.ts +++ b/apps/server/src/modules/agent/challenge.ts @@ -78,9 +78,8 @@ export function systemChallengeVision(env: () => AgentEnv, registry: CapabilityR success: { textAbsent: req.textAbsent ?? "Just a moment" }, maxSteps: Math.min(12, Math.max(1, req.maxSteps)), budget: { + // 不设 token / 费用上限(2026-10-06);步数与时长是防跑飞限制 maxDurationSec: Math.min(120, s.agent.maxDurationSec), - ...(s.agent.maxTokensPerRun ? { maxTokens: Math.max(1000, s.agent.maxTokensPerRun) } : {}), - ...(s.agent.maxCostUsdPerRun ? { maxCostUsd: s.agent.maxCostUsdPerRun } : {}), }, }; const hctx = { diff --git a/apps/server/src/modules/agent/routes.ts b/apps/server/src/modules/agent/routes.ts index e81ecdce..7db462b8 100644 --- a/apps/server/src/modules/agent/routes.ts +++ b/apps/server/src/modules/agent/routes.ts @@ -19,6 +19,7 @@ import { afterApprovalDecided, answerOptions } from "./interaction.ts"; import { type InvokeLimiter, sharedInvokeLimiter, UpstreamBusyError, UpstreamTimeoutError } from "./invoke-limits.ts"; import { AgentRunError, abortRun, appendMessage, startRun } from "./service.ts"; import { descendants, descendantUsage } from "./subagents.ts"; +import { agentUsage } from "./usage.ts"; // Agent 会话 API、持续授权 / 单次审批、能力目录与调用、业务任务。 @@ -119,6 +120,8 @@ export function registerAgentRoutes( const dispatch = new InternalDispatcher(app); // ── Agent 会话 ── + route(app, Api.agentRuns.usage, async () => agentUsage(db())); + route(app, Api.agentRuns.list, async ({ query }) => { let q = db().selectFrom("agent_run").selectAll(); if (query.status) q = q.where("status", "=", query.status); diff --git a/apps/server/src/modules/agent/runner.ts b/apps/server/src/modules/agent/runner.ts index f1e00c65..532f8f90 100644 --- a/apps/server/src/modules/agent/runner.ts +++ b/apps/server/src/modules/agent/runner.ts @@ -91,12 +91,35 @@ export const RECOVER_CAPABILITIES = [ "download.recheck", "episode_mapping.apply_rule", ] as const; +/** + * 整理没识别出作品时的智能体识别:查作品(TMDB / 媒体库 / 网页)+ 指定作品并重新整理。 + * 不能下载、删除、改别的事项;organize.assign_identity 只放行本事项(authorize.ts) + */ +export const ORGANIZE_IDENTIFY_CAPABILITIES = [ + "catalog.lookup", + "inventory.query", + "release.inspect", + "download.inspect", + "task.inspect", + "decision.list", + "web.search", + "web.fetch", + "organize.assign_identity", +] as const; +/** + * 后台调查会话的去重键 investigate:<事项 id>;用户在整理记录里再点一次「让智能体识别」时加 #<序号> 另起一个会话。 + * 返回事项 id,不是调查会话返回 null + */ +export function investigationDecisionId(dedupeKey: string | null | undefined): string | null { + return dedupeKey?.startsWith("investigate:") ? (dedupeKey.slice("investigate:".length).split("#")[0] ?? null) : null; +} /** 被视为"恢复动作"的能力(结果需系统核实) */ export const RECOVERY_ACTIONS = [ "task.retry", "download.recheck", "identity.resolve", "episode_mapping.apply_rule", + "organize.assign_identity", ] as const; export interface RunScope { @@ -162,8 +185,9 @@ export function agentSettings( upstream: RunUpstream, ): AgentSettings { const pi = s.pi; - const maxTokens = budget.maxTokens ?? s.agent.maxTokensPerRun ?? undefined; - const maxCost = budget.maxCostUsd ?? s.agent.maxCostUsdPerRun ?? undefined; + // 设置里不再有 token / 费用上限(2026-10-06);只有调用方显式划拨(子会话、巡检预算)时才限制 + const maxTokens = budget.maxTokens; + const maxCost = budget.maxCostUsd; const up = upstream.row; return { provider: up.provider, @@ -387,20 +411,6 @@ async function lastOutcomeEvent(env: AgentEnv, runId: string) { return r ?? null; } -async function dailyBudgetBlock(env: AgentEnv, s: GlobalSettings, run: AgentRunRow): Promise { - if (run.started_at) return null; // 续跑不重复计数 - const day = new Date(); - day.setHours(0, 0, 0, 0); - const r = await sql<{ n: number; cost: number | null }>` - select count(*)::int as n, sum(coalesce((usage->>'costUsd')::float8, 0))::float8 as cost - from agent_run where started_at >= ${day} and id <> ${run.id}`.execute(env.ctx.db); - const n = r.rows[0]?.n ?? 0; - const cost = r.rows[0]?.cost ?? 0; - if (n >= s.budget.agentMaxRunsPerDay) return `今日智能体运行次数已达上限 ${s.budget.agentMaxRunsPerDay}`; - if (cost >= s.budget.agentMaxCostUsdPerDay) return `今日智能体费用已达上限 $${s.budget.agentMaxCostUsdPerDay}`; - return null; -} - /** * 驱动一次模型会话直到本轮结束(含追加消息)。整个函数在一个持久步骤里执行:worker 崩溃后新 worker 从头重跑本函数, * 打开同一 JSONL 会话,pendingOperations>0 时对账 + resume,已完成的工具调用不会重放。 @@ -416,9 +426,8 @@ export async function drive( const run = await ctx.db.selectFrom("agent_run").selectAll().where("id", "=", runId).executeTakeFirstOrThrow(); if (run.cancel_requested_at || signal.aborted) return { status: "cancelled", reason: "aborted", budget: null, message: "运行已被取消", finalText: "" }; + // 不设每日次数 / 费用上限(2026-10-06):花费在设置页与运行状态里可见,单次运行只受工具调用次数与时长约束 const s = await ctx.settings.all(); - const blocked = await dailyBudgetBlock(env, s, run); - if (blocked) return { status: "failed", reason: "budget", budget: "daily", message: blocked, finalText: "" }; const scope = (run.scope ?? {}) as unknown as RunScope; // 上游:run.upstream_id(runAgent 的 upstreamId 覆盖)> settings.pi.defaultUpstreamId;都没有直接失败 diff --git a/apps/server/src/modules/agent/subagents.ts b/apps/server/src/modules/agent/subagents.ts index 44b2ab2d..3c431306 100644 --- a/apps/server/src/modules/agent/subagents.ts +++ b/apps/server/src/modules/agent/subagents.ts @@ -471,9 +471,9 @@ function limitsOf(run: AgentRunRow, s: GlobalSettings): Limits { const b = (run.budget ?? {}) as AgentBudget; return { maxToolCalls: b.maxToolCalls ?? s.agent.maxToolCalls, - maxTokens: b.maxTokens ?? s.agent.maxTokensPerRun ?? null, - maxCostUsd: - b.maxCostUsd ?? (s.agent.maxCostUsdPerRun && s.agent.maxCostUsdPerRun > 0 ? s.agent.maxCostUsdPerRun : null), + // token / 费用只认本会话显式划拨的额度;设置里不再有全局上限(2026-10-06) + maxTokens: b.maxTokens ?? null, + maxCostUsd: b.maxCostUsd && b.maxCostUsd > 0 ? b.maxCostUsd : null, maxDurationSec: b.maxDurationSec ?? s.agent.maxDurationSec, }; } diff --git a/apps/server/src/modules/agent/usage.ts b/apps/server/src/modules/agent/usage.ts new file mode 100644 index 00000000..6ffb0b76 --- /dev/null +++ b/apps/server/src/modules/agent/usage.ts @@ -0,0 +1,44 @@ +import type { AgentSpend, AgentSpendBucket, AgentSpendKind } from "@umt/contracts"; +import { type DB, type Kysely, sql } from "@umt/db"; + +// 智能体用量(2026-10-06):不设每日次数 / 费用上限,改为把花费摆出来。按 agent_run 的开始时间(没开始的按创建时间) +// 统计今天与最近 7 天,按会话类型分组;费用取 usage.costUsd(上游不报费用时为 0)。 + +/** 整理没识别出作品时智能体识别的会话发起者(见 investigate.ts) */ +export const ORGANIZE_IDENTIFY_ACTOR = "system:organize-identify"; + +const KIND_SQL = sql`case + when parent_run_id is not null then 'subagent' + when created_by = ${ORGANIZE_IDENTIFY_ACTOR} then 'organize_identify' + when trigger = 'schedule' then 'patrol' + when trigger = 'event' then 'investigate' + else 'chat' end`; + +const ORDER: AgentSpendKind[] = ["chat", "patrol", "investigate", "organize_identify", "subagent"]; + +async function bucket(db: Kysely, since: Date): Promise { + const r = await sql<{ kind: AgentSpendKind; runs: number; tokens: string | null; cost: number | null }>` + select ${KIND_SQL} as kind, count(*)::int as runs, + sum(coalesce((usage->>'totalTokens')::bigint, 0))::bigint as tokens, + sum(coalesce((usage->>'costUsd')::float8, 0))::float8 as cost + from agent_run + where coalesce(started_at, created_at) >= ${since} + group by 1`.execute(db); + const byKind = r.rows + .map((x) => ({ kind: x.kind, runs: x.runs, tokens: Number(x.tokens ?? 0), costUsd: x.cost ?? 0 })) + .sort((a, b) => ORDER.indexOf(a.kind) - ORDER.indexOf(b.kind)); + return { + runs: byKind.reduce((a, x) => a + x.runs, 0), + tokens: byKind.reduce((a, x) => a + x.tokens, 0), + costUsd: byKind.reduce((a, x) => a + x.costUsd, 0), + byKind, + }; +} + +export async function agentUsage(db: Kysely, now = new Date()): Promise { + const today = new Date(now); + today.setHours(0, 0, 0, 0); + const week = new Date(today.getTime() - 6 * 86_400_000); + const [t, w] = await Promise.all([bucket(db, today), bucket(db, week)]); + return { today: t, last7d: w }; +} diff --git a/apps/server/test/agent/spend-no-budget.test.ts b/apps/server/test/agent/spend-no-budget.test.ts new file mode 100644 index 00000000..86318ba6 --- /dev/null +++ b/apps/server/test/agent/spend-no-budget.test.ts @@ -0,0 +1,110 @@ +import { DEFAULT_SETTINGS, DEPRECATED_SETTINGS_FIELDS, type GlobalSettings } from "@umt/contracts"; +import type { PiUpstreamRow } from "@umt/db"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { createAgentModule } from "../../src/modules/agent/index.ts"; +import { agentSettings } from "../../src/modules/agent/runner.ts"; +import { ORGANIZE_IDENTIFY_ACTOR } from "../../src/modules/agent/usage.ts"; +import { createHarness, type Harness, pgAvailable } from "../auth-settings/harness.ts"; +import { DEV_KEY, json } from "../helpers.ts"; + +// 智能体不设硬性预算上限(2026-10-06):每天次数 / 每天花费 / 单次花费 / 单次 token 四个上限不再拦截(旧值兼容读取、不生效), +// 只保留单次工具调用次数与单次最长时长两个防跑飞限制(缺省调宽);花费改为可见:/agent/usage 按会话类型统计今天与最近 7 天。 + +const upstream = { row: { provider: "fake", default_model: null, thinking_level: "off" } as unknown as PiUpstreamRow }; + +describe("预算上限移除,防跑飞限制保留", () => { + it("旧设置里的 token / 费用上限不进运行参数;工具调用次数与时长照用", () => { + const legacy = { + ...DEFAULT_SETTINGS, + agent: { ...DEFAULT_SETTINGS.agent, maxTokensPerRun: 1000, maxCostUsdPerRun: 0.01 }, + budget: { ...DEFAULT_SETTINGS.budget, agentMaxRunsPerDay: 0, agentMaxCostUsdPerDay: 0 }, + } as GlobalSettings; + // biome-ignore lint/suspicious/noExplicitAny: 只取用到的上游字段 + const s = agentSettings(legacy, {}, true, upstream as any); + expect(s).not.toHaveProperty("costBudget"); + expect(s.maxToolCalls).toBe(DEFAULT_SETTINGS.agent.maxToolCalls); + expect(s.maxDurationMs).toBe(DEFAULT_SETTINGS.agent.maxDurationSec * 1000); + // 调用方显式划拨(子会话、巡检预算)仍然生效 + // biome-ignore lint/suspicious/noExplicitAny: 同上 + const sub = agentSettings(legacy, { maxToolCalls: 3, maxCostUsd: 0.5 }, true, upstream as any); + expect(sub.maxToolCalls).toBe(3); + expect(sub.costBudget).toEqual({ maxCostUsd: 0.5 }); + }); + + it("缺省调宽;四个上限登记为弃用字段", () => { + expect(DEFAULT_SETTINGS.agent.maxToolCalls).toBeGreaterThanOrEqual(100); + expect(DEFAULT_SETTINGS.agent.maxDurationSec).toBeGreaterThanOrEqual(1800); + expect(DEFAULT_SETTINGS.agent.investigateMaxToolCalls).toBeGreaterThanOrEqual(20); + expect(DEFAULT_SETTINGS.agent.autoIdentifyOrganize).toBe(true); + expect(DEFAULT_SETTINGS.budget).not.toHaveProperty("agentMaxRunsPerDay"); + expect(DEPRECATED_SETTINGS_FIELDS).toEqual( + expect.arrayContaining([ + "budget.agentMaxRunsPerDay", + "budget.agentMaxCostUsdPerDay", + "agent.maxTokensPerRun", + "agent.maxCostUsdPerRun", + ]), + ); + }); +}); + +const hasPg = await pgAvailable(); + +describe.skipIf(!hasPg)("花费统计 /agent/usage(真实 PG)", () => { + let h: Harness; + beforeAll(async () => { + h = await createHarness({ extraModules: [createAgentModule()] }); + }, 120_000); + afterAll(async () => { + await h?.close(); + }); + + it("今天与最近 7 天按会话类型统计次数、token 与费用;超过 7 天的不算", async () => { + const day = 86_400_000; + const now = Date.now(); + const run = async ( + trigger: string, + at: number, + usage: { totalTokens: number; costUsd: number }, + extra: Record = {}, + ) => + ( + await h.db + .insertInto("agent_run") + .values({ + trigger, + goal: "g", + status: "succeeded", + mode: "execute", + usage: JSON.stringify(usage), + started_at: new Date(at), + created_at: new Date(at), + ...extra, + } as never) + .returning("id") + .executeTakeFirstOrThrow() + ).id; + const chat = await run("user", now, { totalTokens: 1000, costUsd: 0.1 }); + await run("subagent", now, { totalTokens: 500, costUsd: 0.05 }, { parent_run_id: chat }); + await run("event", now, { totalTokens: 200, costUsd: 0.02 }, { created_by: ORGANIZE_IDENTIFY_ACTOR }); + await run("event", now - 3 * day, { totalTokens: 300, costUsd: 0.03 }); + await run("schedule", now - 3 * day, { totalTokens: 400, costUsd: 0.04 }); + await run("user", now - 10 * day, { totalTokens: 9999, costUsd: 9 }); + + const res = await h.app.request("/api/v1/agent/usage", { headers: { authorization: `Bearer ${DEV_KEY}` } }); + expect(res.status).toBe(200); + const { today, last7d } = (await json(res)).data; + expect(today.runs).toBe(3); + expect(today.tokens).toBe(1700); + expect(today.costUsd).toBeCloseTo(0.17); + expect(today.byKind.map((k: { kind: string }) => k.kind)).toEqual(["chat", "organize_identify", "subagent"]); + expect(last7d.runs).toBe(5); + expect(last7d.costUsd).toBeCloseTo(0.24); + expect(last7d.byKind).toEqual( + expect.arrayContaining([ + { kind: "patrol", runs: 1, tokens: 400, costUsd: 0.04 }, + { kind: "investigate", runs: 1, tokens: 300, costUsd: 0.03 }, + ]), + ); + }); +}); diff --git a/docs/runbook/agent.md b/docs/runbook/agent.md index 09521cb1..d17bdc32 100644 --- a/docs/runbook/agent.md +++ b/docs/runbook/agent.md @@ -63,7 +63,7 @@ AG-3 补的只读摘要(类别 read,scope `read`,都在后台调查 / 巡检白 1. `drive` 步骤: - 取上游(provider / baseUrl / 模型 / 思考档位来自 `pi_upstream`,凭证由上游的凭证存储在请求时解密注入,不落日志),从设置读上下文预算、压缩和预算。 - - 检查今日次数和费用预算(`budget.agentMaxRunsPerDay` / `agentMaxCostUsdPerDay`)。 + - 不设每日次数 / 费用上限(2026-10-06 起;`budget.agentMaxRunsPerDay` / `agentMaxCostUsdPerDay` 旧值兼容读取、不生效),花费在设置 → 智能体「用量与限制」与运行状态里可见(`GET /agent/usage`)。 - 打开 `AGENT_SESSION_DIR/.jsonl`。`pendingOperations>0` 说明上次崩溃:先按幂等键对账,再 `resume()`;否则就 `prompt(goal)`。最后逐条消费追加的消息。 2. `goal-settled` 步骤:只在会话正常结束、且不是后台只读调查(内部 `agent_run.mode=plan_only`)时执行。每隔 `agent.goalPollSec` 检查一次,直到本 run 触达的计划、下载和整理记录全部落定,最长等 `agent.goalWaitHours`。等待期间 run 状态为 `waiting`,汇报写的是"已提交≠已入库"。 3. `finalize` 步骤:从业务表汇总计划、下载任务和整理记录,生成 `AgentGoalReport`(artifact kind=report),并据此决定终态: @@ -156,6 +156,20 @@ curl -X POST localhost:4380/api/v1/automation-grants -H "Authorization: Bearer < **有界尝试**:`evidence.autoRecover.attempt` 记录该根因的第几次自动恢复。同一去重键(如 `download_failed:`)下,历史上 `recoverAttempted=true` 的事项数达到 `agent.investigateMaxAttempts`(缺省 2)后,同根因的新事项不再唤醒模型,直接 `awaiting_user`,并写 `skipped=attempts_exhausted`。 +## 整理识别不到作品时叫智能体识别(2026-10-06) + +下载完整理时认不出作品(接管任务、文件名认不出、`metadata.resolve_organize` 也没认出来),`planImport` 照旧记 `identity_unknown` +并开内部事项 `organize_identity:<下载任务>`。`decision.created → resolveDecision → autoInvestigate` 认出这类事项后不看 `autoInvestigate`, +按 `agent.autoIdentifyOrganize`(缺省开,设置 → 智能体 → 自动调查)派一次识别会话(`identifyOrganize`): + +- 会话:`created_by = system:organize-identify`、`dedupe_key = investigate:<事项>`(同一事项只派一次),白名单 + `ORGANIZE_IDENTIFY_CAPABILITIES`(查 TMDB / 媒体库 / 网页 + `organize.assign_identity`),工具调用上限 `investigateMaxToolCalls`。 +- `organize.assign_identity {decisionId, mediaEntityId | tmdb, reasons}`:回填下载任务的作品 → 冻结手动整理计划 → 入队执行,事项关闭 + (与人工「指定作品」同一条路)。授权不走持续授权:只放行 dedupe 指向这条事项的识别会话与用户对话(`authorize.ts`)。 +- 结果按恢复动作核实(事项关闭 = 已指定并重新整理);没认出来事项转 `awaiting_user`,记录留在整理记录「需要处理」里。 +- 用户在整理记录里点「让智能体识别」(`POST /organization-records/{id}/agent-identify`)不看开关,入队 `resolveDecision {trigger: identify}`, + 另起一个会话(`investigate:<事项>#<时间戳>`)。开关关掉时只是不自动派。 + ## 上游与登录(W7-A) 实现:`apps/server/src/modules/pi/`(上游 CRUD、能力表、测试、服务端订阅登录、刷新任务)、`packages/agent/src/pi/{catalog,upstream,fake-upstream}.ts`、 @@ -238,19 +252,24 @@ OpenRouter 换到的是永久 key,不刷新。 "subagents": { "enabled": false, "maxConcurrent": 2, "maxDepth": 1 } // subagentsEnabled 是 enabled 的旧别名 }, "agent": { - "maxToolCalls": 40, "maxDurationSec": 900, // 单次运行预算 - "maxTokensPerRun": null, "maxCostUsdPerRun": null, - "autoInvestigate": false, "investigateMaxToolCalls": 8, + "maxToolCalls": 120, "maxDurationSec": 1800, // 单次运行的防跑飞限制(只保留这两个) + "autoIdentifyOrganize": true, // 整理识别不到作品时叫智能体识别(见下节) + "autoInvestigate": false, "investigateMaxToolCalls": 20, "autoInvestigateMode": "investigate", // 或 "recover"(授权内恢复,见上一节) "investigateMaxAttempts": 2, "recoverVerifySec": 600, "goalPollSec": 60, "goalWaitHours": 72, "optionsTimeoutSec": 1800, "optionsTimeoutAction": "recommended", // 方案选择超时:自动选推荐项或 "stop" "approvalTimeoutSec": 86400 // 会话内审批有效期 - }, - "budget": { "agentMaxRunsPerDay": 50, "agentMaxCostUsdPerDay": 5 } + } } ``` +**不设硬性预算(2026-10-06)**:每天次数(`budget.agentMaxRunsPerDay`)、每天花费(`budget.agentMaxCostUsdPerDay`)、单次花费(`agent.maxCostUsdPerRun`)、 +单次 token(`agent.maxTokensPerRun`)四个上限不再拦截,登记在 `DEPRECATED_SETTINGS_FIELDS`,设置表里的旧值照常读出、不生效。 +调用方显式划拨的额度(子会话预算、巡检的 `budget`)仍然生效。花费改为可见:`GET /agent/usage` 按会话类型 +(对话 / 巡检 / 后台调查 / 整理识别 / 子会话)给今天与最近 7 天的次数、token 与费用(取 `agent_run.usage.costUsd`,上游不报费用时为 0), +设置页「用量与限制」与运行状态页「智能体用量」都显示。 + 上下文预算裁剪模型的 contextWindow;思考档位取上游的 `thinkingLevel`,运行时夹到模型支持的档位。 - 会话目录由环境变量 `AGENT_SESSION_DIR` 指定,缺省为 `/data/agent-sessions`。api 进程和 worker 进程要能读到同一个目录,这是崩溃恢复的前提。 diff --git a/packages/contracts/src/agent.ts b/packages/contracts/src/agent.ts index 423578e2..14e5ce70 100644 --- a/packages/contracts/src/agent.ts +++ b/packages/contracts/src/agent.ts @@ -359,6 +359,23 @@ export type AgentRunChoiceInput = z.infer; export const AgentRunAbortInput = z.object({ reason: z.string().max(500).optional() }); export type AgentRunAbortInput = z.infer; +/** + * 智能体用量(2026-10-06 起不设硬性预算上限,花费改为可见):按会话类型统计次数、token 与费用。 + * chat = 用户对话;patrol = 智能体巡检;investigate = 待处理事项的后台调查 / 授权内恢复; + * organize_identify = 整理没识别出作品时的智能体识别;subagent = 子会话 + */ +export const AgentSpendKind = z.enum(["chat", "patrol", "investigate", "organize_identify", "subagent"]); +export type AgentSpendKind = z.infer; +export const AgentSpendBucket = z.object({ + runs: NonNegInt, + tokens: NonNegInt, + costUsd: z.number().min(0), + byKind: z.array(z.object({ kind: AgentSpendKind, runs: NonNegInt, tokens: NonNegInt, costUsd: z.number().min(0) })), +}); +export type AgentSpendBucket = z.infer; +export const AgentSpend = z.object({ today: AgentSpendBucket, last7d: AgentSpendBucket }); +export type AgentSpend = z.infer; + export const AgentRunListQuery = z.object({ cursor: z.string().min(1).optional(), limit: z.coerce.number().int().min(1).max(200).default(50), diff --git a/packages/contracts/src/api/review.ts b/packages/contracts/src/api/review.ts index 4051eb0a..22e87f73 100644 --- a/packages/contracts/src/api/review.ts +++ b/packages/contracts/src/api/review.ts @@ -77,6 +77,13 @@ export const reviewApi = { ), /** Agent 会话(W2-D):建 run 只入队持久任务 runAgent;增量经全局 SSE(agent.*),断线按 runSeq 用 afterSeq 补读 */ agentRuns: { + usage: ep({ + method: "get", + path: "/agent/usage", + scope: "read", + summary: "智能体用量:今天与最近 7 天按会话类型的次数、token 与费用", + response: agent.AgentSpend, + }), list: ep({ method: "get", path: "/agent/runs", diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index cadbf484..a604c1f8 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -208,10 +208,12 @@ export const AgentRuntimeSettings = z.object({ .int() .min(10) .max(24 * 3600), - /** 单次运行:token 上限(null = 不限) */ - maxTokensPerRun: z.number().int().positive().nullable(), - /** 单次运行:费用上限(美元,null = 不限) */ - maxCostUsdPerRun: z.number().positive().nullable(), + /** 已弃用(2026-10-06 起不限 token,旧值兼容读取、不生效) */ + maxTokensPerRun: z.number().int().positive().nullable().optional(), + /** 已弃用(2026-10-06 起不限单次费用,旧值兼容读取、不生效) */ + maxCostUsdPerRun: z.number().positive().nullable().optional(), + /** 整理时没识别出作品:自动派智能体识别一次,认出来就按该作品重新整理(不需要持续授权) */ + autoIdentifyOrganize: z.boolean(), /** 后台自动调查的工具调用上限(比用户委托更小) */ investigateMaxToolCalls: z.number().int().min(1).max(100), /** 等待目标关联的下载/入库结束时的轮询间隔(秒) */ @@ -432,8 +434,10 @@ export const BudgetSettings = z.object({ maxNewTitlesPerDay: z.number().int().min(0), maxDownloadBytesPerDay: z.number().int().min(0), maxConcurrentDownloads: z.number().int().min(1).max(100), - agentMaxRunsPerDay: z.number().int().min(0), - agentMaxCostUsdPerDay: z.number().min(0), + /** 已弃用(2026-10-06 起智能体不设每日次数上限,旧值兼容读取、不生效) */ + agentMaxRunsPerDay: z.number().int().min(0).optional(), + /** 已弃用(2026-10-06 起智能体不设每日费用上限,旧值兼容读取、不生效) */ + agentMaxCostUsdPerDay: z.number().min(0).optional(), }); export type BudgetSettings = z.infer; @@ -667,19 +671,16 @@ export const DEFAULT_SETTINGS: GlobalSettings = { maxNewTitlesPerDay: 20, maxDownloadBytesPerDay: 500 * 1024 ** 3, maxConcurrentDownloads: 5, - agentMaxRunsPerDay: 50, - agentMaxCostUsdPerDay: 5, }, agent: { autoInvestigate: false, autoInvestigateMode: "investigate", investigateMaxAttempts: 2, recoverVerifySec: 600, - maxToolCalls: 40, - maxDurationSec: 900, - maxTokensPerRun: null, - maxCostUsdPerRun: null, - investigateMaxToolCalls: 8, + maxToolCalls: 120, + maxDurationSec: 1800, + autoIdentifyOrganize: true, + investigateMaxToolCalls: 20, goalPollSec: 60, goalWaitHours: 72, optionsTimeoutSec: 1800, @@ -858,6 +859,11 @@ export const DEPRECATED_SETTINGS_FIELDS = [ "cache.tmdbTtlHours", "cache.emptyResultTtlSec", "identity.poolArchiveDays", + // 2026-10-06:智能体不设硬性预算上限,只保留单次工具调用次数与时长两个防跑飞限制 + "budget.agentMaxRunsPerDay", + "budget.agentMaxCostUsdPerDay", + "agent.maxTokensPerRun", + "agent.maxCostUsdPerRun", ] as const; // ── 设置页分区(docs/design/console-ia.md §3.4) ── @@ -1027,12 +1033,12 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [ { id: "agent", label: "智能体", - description: "模型服务、运行预算、自动调查、网页搜索", + description: "模型服务、用量与限制、自动调查、网页搜索", legacyTabs: [{ tab: "pi" }], anchors: [ { id: "upstream", label: "模型服务" }, { id: "runtime", label: "上下文与子会话" }, - { id: "budget", label: "预算" }, + { id: "budget", label: "用量与限制" }, { id: "investigate", label: "自动调查" }, { id: "websearch", label: "网页搜索" }, { id: "advanced", label: "高级" }, @@ -1045,16 +1051,13 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [ "pi.compaction", "pi.subagentsEnabled", "pi.subagents", - "budget.agentMaxRunsPerDay", - "budget.agentMaxCostUsdPerDay", "agent.autoInvestigate", + "agent.autoIdentifyOrganize", "agent.autoInvestigateMode", "agent.investigateMaxAttempts", "agent.recoverVerifySec", "agent.maxToolCalls", "agent.maxDurationSec", - "agent.maxTokensPerRun", - "agent.maxCostUsdPerRun", "agent.investigateMaxToolCalls", "agent.goalPollSec", "agent.goalWaitHours",