merge: fix/q-inline-handling

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei

# Conflicts:
#	apps/server/src/modules/sources-identity/service/episode-resolution.ts
This commit is contained in:
root committed 2026-10-06 17:09:59 +08:00
commit c465b533fd
80 files changed
+2766 -1016

No files matched your search

+17
View File
@@ -359,6 +359,23 @@ export type AgentRunChoiceInput = z.infer<typeof AgentRunChoiceInput>;
export const AgentRunAbortInput = z.object({ reason: z.string().max(500).optional() });
export type AgentRunAbortInput = z.infer<typeof AgentRunAbortInput>;
/**
* 智能体用量(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<typeof AgentSpendKind>;
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<typeof AgentSpendBucket>;
export const AgentSpend = z.object({ today: AgentSpendBucket, last7d: AgentSpendBucket });
export type AgentSpend = z.infer<typeof AgentSpend>;
export const AgentRunListQuery = z.object({
cursor: z.string().min(1).optional(),
limit: z.coerce.number().int().min(1).max(200).default(50),
+9
View File
@@ -149,6 +149,15 @@ export const libraryApi = {
response: organization.Dashboard,
}),
},
attention: {
summary: ep({
method: "get",
path: "/attention",
scope: "read",
summary: "需要关注的汇总:整理记录需要处理的条数、等你批准的智能体操作、系统健康问题",
response: organization.AttentionSummary,
}),
},
downloadCache: {
cleanup: ep({
method: "post",
+16
View File
@@ -49,6 +49,22 @@ export const organizeApi = {
body: organization.OrganizationDeleteRequest,
response: organization.OrganizationDeleteResult,
}),
dismissAttention: ep({
method: "post",
path: "/organization-records/{id}/dismiss-attention",
scope: "organize.execute",
summary: "忽略这条整理记录的问题:不再算需要处理,关联的未关闭事项一并关闭",
params: IdParam,
response: organization.PublicOrganizationRecord,
}),
agentIdentify: ep({
method: "post",
path: "/organization-records/{id}/agent-identify",
scope: "organize.execute",
summary: "让智能体识别这条整理记录的作品(没识别出作品的记录),认出来自动重新整理",
params: IdParam,
response: organization.OrganizationAgentIdentifyResult,
}),
create: ep({
method: "post",
path: "/organization-records",
+7
View File
@@ -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",
+29
View File
@@ -1088,6 +1088,34 @@ export const EpisodeMappingApplyRuleOutput = z.object({
decisionStatus: DecisionStatus,
});
/** organize.assign_identity:给「没识别出作品」的整理事项指定作品并重新整理 */
/** mediaEntityId 与 tmdb 二选一(都给时用 mediaEntityId,都没给由处理器拒绝) */
export const OrganizeAssignIdentityInput = z.object({
/** 「没识别出作品」事项 ID(调查目标里给出) */
decisionId: Id,
mediaEntityId: Id.optional(),
tmdb: z
.object({
type: z.enum(["movie", "tv"]),
id: z
.string()
.trim()
.regex(/^\d{1,10}$/),
})
.optional(),
/** 为什么是这个作品(依据),至少一条 */
reasons: z.array(z.string().trim().min(1).max(300)).min(1).max(10),
});
export const OrganizeAssignIdentityOutput = z.object({
mediaEntityId: Id,
/** 重新整理的记录与状态 */
recordId: Id.nullable(),
status: z.string(),
decisionStatus: DecisionStatus,
});
export type OrganizeAssignIdentityInput = z.infer<typeof OrganizeAssignIdentityInput>;
export type OrganizeAssignIdentityOutput = z.infer<typeof OrganizeAssignIdentityOutput>;
/** 能力 ID → 输入/输出 schema(server 注册处理器、agent 生成工具定义时共用) */
// ── schedule.draft_check:按自然语言需求起草巡检(建成未启用,用户在计划页检查后启用) ──
@@ -1167,6 +1195,7 @@ export const CAPABILITY_IO = {
"episode_mapping.inspect": { input: EpisodeMappingInspectInput, output: EpisodeMappingInspectOutput },
"episode_mapping.simulate": { input: EpisodeMappingSimulateInput, output: EpisodeMappingSimulateOutput },
"episode_mapping.apply_rule": { input: EpisodeMappingApplyRuleInput, output: EpisodeMappingApplyRuleOutput },
"organize.assign_identity": { input: OrganizeAssignIdentityInput, output: OrganizeAssignIdentityOutput },
} as const satisfies Record<KnownCapabilityId, { input: z.ZodType; output: z.ZodType }>;
type CapabilityIo = typeof CAPABILITY_IO;
+15
View File
@@ -100,6 +100,7 @@ export const CAPABILITY_IDS = [
"episode_mapping.inspect",
"episode_mapping.simulate",
"episode_mapping.apply_rule",
"organize.assign_identity",
] as const;
export const KnownCapabilityId = z.enum(CAPABILITY_IDS);
export type KnownCapabilityId = z.infer<typeof KnownCapabilityId>;
@@ -668,6 +669,19 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
false,
true,
),
// 整理时没识别出作品(organize_identity 事项):智能体查到作品后指定,随即按该作品重新整理
"organize.assign_identity": d(
"organize.assign_identity",
"指定整理作品",
"给没识别出作品的整理记录指定作品(本地作品 ID 或 TMDB 类型 + ID),随即按这个作品重新整理。只用于「没识别出作品」事项;先用 catalog.lookup / inventory.query 核对标题、年份、季集,拿不准就不要指定",
"execute",
"OrganizeAssignIdentity",
"organize.execute",
"medium",
false,
false,
"never",
),
};
/**
@@ -733,6 +747,7 @@ export const CAPABILITY_ACTIVITY: Record<KnownCapabilityId, CapabilityActivity>
"episode_mapping.inspect": act("查", "集数映射"),
"episode_mapping.simulate": act("模拟", "映射规则", "次"),
"episode_mapping.apply_rule": act("写入", "映射规则", "条"),
"organize.assign_identity": act("指定", "整理作品", "个"),
};
function d(
+58 -1
View File
@@ -1,5 +1,5 @@
import { z } from "zod";
import { AgentRunStatus, DecisionKind } from "./agent.ts";
import { AgentRunStatus, Decision, DecisionKind } from "./agent.ts";
import { AssetLocation, MediaAsset, PathBinding } from "./assets.ts";
import { ActorType, EntityBase, Id, JsonObject, NonNegInt, Timestamp } from "./common.ts";
import { MediaEntity, MediaKind } from "./content.ts";
@@ -73,6 +73,8 @@ export const OrganizationRecord = EntityBase.extend({
startedAt: Timestamp.nullable(),
finishedAt: Timestamp.nullable(),
changeSeq: ChangeSeq,
/** 用户在整理记录里忽略了这条记录的问题(不再算需要处理) */
attentionClearedAt: Timestamp.nullable(),
});
export type OrganizationRecord = z.infer<typeof OrganizationRecord>;
export const OrganizationRecordCreate = OrganizationRecord.pick({
@@ -211,6 +213,32 @@ export const PublicOrganizationFile = z.object({
});
export type PublicOrganizationFile = z.infer<typeof PublicOrganizationFile>;
/** 整理记录关联的未关闭事项(在整理记录里就地处理;选项与「处理」端点同 /decisions/{id}/resolve) */
export const RecordAttentionDecision = Decision.pick({
id: true,
kind: true,
status: true,
title: true,
summary: true,
options: true,
agentRunId: true,
evidence: true,
});
export type RecordAttentionDecision = z.infer<typeof RecordAttentionDecision>;
/**
* 需要处理(2026-10-06 去掉待处理中心后,整理侧的问题在整理记录里就地处理):
* topic:identity = 没识别出作品(可指定作品 / 让智能体识别);conflict = 目标已存在 / 路径冲突(按选项替换、保留两者或跳过);
* other = 其余失败或部分完成(排除、非视频、卡住、执行失败…,重新整理或忽略)。
* agentIdentify:可以让智能体识别(没识别出作品且有关联下载任务)。
*/
export const RecordAttention = z.object({
topic: z.enum(["identity", "conflict", "other"]),
decision: RecordAttentionDecision.nullable(),
agentIdentify: z.boolean(),
});
export type RecordAttention = z.infer<typeof RecordAttention>;
export const PublicOrganizationRecord = z.object({
/** 稳定记录 ID */
id: Id,
@@ -261,6 +289,8 @@ export const PublicOrganizationRecord = z.object({
finishedAt: Timestamp.nullable(),
/** 该记录最近一次变更的游标值;客户端按 id 去重、以最大 changeSeq 续读 */
changeSeq: ChangeCursor,
/** 需要处理时给出原因与可做的动作;不需要为 null(旧客户端可忽略) */
attention: RecordAttention.nullable().optional(),
});
export type PublicOrganizationRecord = z.infer<typeof PublicOrganizationRecord>;
@@ -303,6 +333,13 @@ export const OrganizationListQuery = z.object({
since: Timestamp.optional(),
/** HIS-1:只看整理类记录(复制/移动/硬链接),不含文件管理的改名、删除、还原 */
organizeOnly: z.union([z.boolean(), z.enum(["true", "false"]).transform((v) => v === "true")]).optional(),
/**
* 只看需要处理的记录:有关联的未关闭事项,或失败 / 部分完成、没被同一下载任务后来的整理取代、也没被忽略的
* (口径见 docs/runbook/organize.md「需要处理」)
*/
attention: z
.union([z.boolean(), z.enum(["true", "false", "1", "0"]).transform((v) => v === "true" || v === "1")])
.optional(),
});
export type OrganizationListQuery = z.infer<typeof OrganizationListQuery>;
@@ -317,6 +354,8 @@ export const OrganizationStats = z.object({
/** 排队 + 执行中 */
running: NonNegInt,
cancelled: NonNegInt,
/** 需要处理(同列表 attention=true 的口径,不受状态筛选影响) */
attention: NonNegInt,
});
export type OrganizationStats = z.infer<typeof OrganizationStats>;
@@ -764,3 +803,21 @@ export const OrganizePlanArtifactContent = z.object({
.optional(),
});
export type OrganizePlanArtifactContent = z.infer<typeof OrganizePlanArtifactContent>;
/** 让智能体识别整理记录里的作品(没识别出作品的记录):入队一次识别会话 */
export const OrganizationAgentIdentifyResult = z.object({ decisionId: Id, queued: z.boolean() });
export type OrganizationAgentIdentifyResult = z.infer<typeof OrganizationAgentIdentifyResult>;
/**
* 需要用户关注的汇总(铃铛、首页、运行状态共用):
* - organize:整理记录里需要处理的条数(点进整理记录「需要处理」);
* - approvals:需要你批准的智能体操作(会话里的审批 / 方案选择、计划审批、原站页面等你接手);
* - health:系统健康问题(站点不可达、下载器离线、下载目录读不到容量、磁盘空间不足、媒体库目录离线),恢复后自动消失。
* 集数待确认、未识别资源等不在这里:前者在订阅详情与作品页集数映射里看原因,后者静默留在未识别里。
*/
export const AttentionSummary = z.object({
organize: NonNegInt,
approvals: z.object({ total: NonNegInt, items: z.array(Decision) }),
health: z.object({ total: NonNegInt, items: z.array(Decision) }),
});
export type AttentionSummary = z.infer<typeof AttentionSummary>;
+4 -4
View File
@@ -1,4 +1,4 @@
// 内部原因码 → 一句人话 + 下一步(DEAI-2)。整理记录、待处理、下载时间线、任务都用这张表;
// 内部原因码 → 一句人话 + 下一步(DEAI-2)。整理记录、下载时间线、任务都用这张表;
// 原码只放在界面的「技术信息」里,不直接给人看。不认识的码返回 null,由调用方决定兜底文字。
export type ReasonText = {
@@ -19,9 +19,9 @@ const REASONS: Record<string, ReasonText> = {
identity_unresolved: { text: "没识别出作品", action: "指定作品" },
episode_mapping: { text: "集数对不上作品的季集", action: "确认集数映射" },
episode_unresolved: { text: "集数对不上作品的季集", action: "确认集数映射" },
target_exists: { text: "媒体库里已有同名文件", action: "在待处理里选择替换或跳过" },
target_exists: { text: "媒体库里已有同名文件", action: "在整理记录里选择替换、保留两者或跳过" },
duplicate_target: { text: "改名后与其他文件同名", action: "改命名模板或排除其中一个" },
replace_requires_grant: { text: "替换已有文件需要你批准", action: "在待处理里批准" },
replace_requires_grant: { text: "替换已有文件需要你批准", action: "在整理记录里选择是否替换" },
hnr_unsatisfied: { text: "做种时间还不够,不能移动", action: "改用复制,或等做种完成" },
hnr_unknown: { text: "不确定做种要求是否满足", action: "改用复制,或确认站点规则" },
orphan_sidecar: { text: "字幕等附属文件找不到对应视频" },
@@ -51,7 +51,7 @@ const REASONS: Record<string, ReasonText> = {
downloader_unavailable: { text: "下载器连不上", action: "在设置 → 下载器里测试连接" },
downloader_error: { text: "下载器报错", action: "查看下载器日志" },
// DL-2b:下载面板整理状态、下载计划里出现的码
blocked: { text: "有冲突要先处理", action: "去待处理" },
blocked: { text: "有冲突要先处理", action: "在整理记录里处理" },
blocked_by_conflict: { text: "有冲突,暂不整理" },
transfer_failed: { text: "文件没有整理成功", action: "在整理记录里查看失败的文件" },
main_failed: { text: "主文件整理失败,附属文件一起跳过" },
+22 -19
View File
@@ -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<typeof BudgetSettings>;
@@ -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",