Merge branch 'deai2'
Co-Authored-By: Claude Fable 5.1 <[email protected]> # Conflicts: # apps/server/src/modules/acquisition-organize/services/organize.ts # apps/web/app/routes/console/sites.$siteId.tsx
This commit is contained in:
commit
12bc4664b6
247 files changed
+2658
-2148
No files matched your search
@@ -23,8 +23,8 @@ const PROVIDER_ID = { anthropic: "umt-anthropic", "openai-compatible": "umt-open
|
||||
*/
|
||||
export function resolveModelFromSettings(settings: ResolvedAgentSettings): ResolvedPiModel {
|
||||
if (settings.provider !== "anthropic" && settings.provider !== "openai-compatible")
|
||||
throw new Error(`provider ${settings.provider} 需要按上游装配(PiAgentRuntimeOptions.upstream)`);
|
||||
if (!settings.apiKey) throw new Error("未配置上游密钥");
|
||||
throw new Error(`provider ${settings.provider} 需要按模型服务装配(PiAgentRuntimeOptions.upstream)`);
|
||||
if (!settings.apiKey) throw new Error("未配置模型服务密钥");
|
||||
const providerId = PROVIDER_ID[settings.provider];
|
||||
const common = {
|
||||
id: settings.model,
|
||||
@@ -49,7 +49,7 @@ export function resolveModelFromSettings(settings: ResolvedAgentSettings): Resol
|
||||
models.setProvider(createProvider({ id: providerId, auth, models: [m], api: anthropicMessagesApi() }));
|
||||
model = m;
|
||||
} else {
|
||||
if (!settings.baseUrl) throw new Error("openai-compatible 上游必须配置 baseUrl(带 /v1)");
|
||||
if (!settings.baseUrl) throw new Error("openai-compatible 模型服务必须配置 baseUrl(带 /v1)");
|
||||
const m: Model<"openai-completions"> = {
|
||||
...common,
|
||||
api: "openai-completions",
|
||||
|
||||
@@ -400,7 +400,7 @@ class PiAgentRun implements AgentRunHandle {
|
||||
const err = record && "error" in record ? record.error : undefined;
|
||||
outcome = this.failed("error", redactString(err ? `${err.code}: ${err.message}` : "运行失败"));
|
||||
} else if (status === "suspended") {
|
||||
outcome = this.failed("error", "上游进入延迟模式(deferred),v0.1 不支持");
|
||||
outcome = this.failed("error", "模型服务进入延迟模式(deferred),v0.1 不支持");
|
||||
} else {
|
||||
outcome = { status: "completed", finalText: this.finalText, usage: this.usage };
|
||||
this.emit({ type: "run_completed", finalText: outcome.finalText, usage: this.usage });
|
||||
@@ -540,7 +540,7 @@ export function toolContent(
|
||||
if (!vision)
|
||||
return [
|
||||
{ type: "text", text },
|
||||
{ type: "text", text: "(截图未发送:当前上游模型不支持图片输入;用 browser.snapshot 读取页面文字)" },
|
||||
{ type: "text", text: "(截图未发送:当前模型不支持图片输入;用 browser.snapshot 读取页面文字)" },
|
||||
];
|
||||
return [
|
||||
{ type: "text", text },
|
||||
|
||||
@@ -329,7 +329,7 @@ export class PiUpstreamClient {
|
||||
private pickModel(modelId: string | null | undefined): { model: Model<Api>; inCatalog: boolean } {
|
||||
const all = this.models.getModels(this.providerId);
|
||||
const id = modelId ?? this.spec.model ?? all[0]?.id;
|
||||
if (!id) throw new UpstreamError("model", "该上游没有可用模型,请填写模型 id");
|
||||
if (!id) throw new UpstreamError("model", "该模型服务没有可用模型,请填写模型 id");
|
||||
const found = this.models.getModel(this.providerId, id);
|
||||
if (found) return { model: found, inCatalog: true };
|
||||
const template = all[0];
|
||||
@@ -369,7 +369,7 @@ export class PiUpstreamClient {
|
||||
throw classifyUpstreamError(err);
|
||||
}
|
||||
if (res.stopReason === "error" || res.stopReason === "aborted")
|
||||
throw classifyUpstreamError(new Error(res.errorMessage ?? "上游返回错误"));
|
||||
throw classifyUpstreamError(new Error(res.errorMessage ?? "模型服务返回错误"));
|
||||
}
|
||||
return { source, model: model.id, inCatalog };
|
||||
}
|
||||
|
||||
@@ -719,7 +719,7 @@ export const Api = {
|
||||
path: "/subscriptions/{id}/coverage",
|
||||
scope: "read",
|
||||
summary:
|
||||
"覆盖矩阵:季 → 集 × 版本目标(电影:内容版本 × 版本目标),每格 have/inflight/missing/skipped;缓存 30s,refresh=true 强制重算",
|
||||
"覆盖矩阵:季 → 集 × 收录版本(电影:内容版本 × 收录版本),每格 have/inflight/missing/skipped;缓存 30s,refresh=true 强制重算",
|
||||
params: IdParam,
|
||||
query: z.object({ refresh: z.enum(["true", "false"]).optional() }),
|
||||
response: policy.SubscriptionCoverage,
|
||||
@@ -760,7 +760,7 @@ export const Api = {
|
||||
method: "post",
|
||||
path: "/subscriptions/{id}/slots/{slotId}/fill",
|
||||
scope: "admin",
|
||||
summary: "手动补全某版本目标(不受自动补全开关与补全预算限制)",
|
||||
summary: "手动补全某收录版本(不受自动补全开关与补全预算限制)",
|
||||
params: z.object({ id: Id, slotId: Id }),
|
||||
body: policy.SlotFillRequest,
|
||||
response: z.object({ runId: z.string(), deduplicated: z.boolean() }),
|
||||
@@ -769,7 +769,7 @@ export const Api = {
|
||||
method: "put",
|
||||
path: "/subscriptions/{id}/slot-order",
|
||||
scope: "admin",
|
||||
summary: "版本目标排序(第一个为主版本)",
|
||||
summary: "收录版本排序(第一个为主版本)",
|
||||
params: IdParam,
|
||||
body: policy.SlotOrderSet,
|
||||
response: z.array(policy.AcquisitionSlot),
|
||||
@@ -786,7 +786,7 @@ export const Api = {
|
||||
method: "get",
|
||||
path: "/subscriptions/{id}/progress",
|
||||
scope: "read",
|
||||
summary: "订阅进度:季总集数账本、每集状态(已覆盖/在途/缺失/疑似缺集)、版本目标在途与延后",
|
||||
summary: "订阅进度:季总集数账本、每集状态(已覆盖/在途/缺失/疑似缺集)、收录版本在途与延后",
|
||||
params: IdParam,
|
||||
response: policy.SubscriptionProgress,
|
||||
}),
|
||||
@@ -849,7 +849,7 @@ export const Api = {
|
||||
},
|
||||
acquisitionSlots: crud(
|
||||
"/acquisition-slots",
|
||||
"版本目标",
|
||||
"收录版本",
|
||||
{
|
||||
row: policy.AcquisitionSlot,
|
||||
create: policy.AcquisitionSlotCreate,
|
||||
@@ -1227,7 +1227,7 @@ export const Api = {
|
||||
method: "get",
|
||||
path: "/agent/runs",
|
||||
scope: "read",
|
||||
summary: "Agent 运行列表(新→旧)",
|
||||
summary: "智能体运行列表(新→旧)",
|
||||
query: agent.AgentRunListQuery,
|
||||
response: cursorPage(agent.AgentRun),
|
||||
}),
|
||||
@@ -1235,7 +1235,7 @@ export const Api = {
|
||||
method: "get",
|
||||
path: "/agent/runs/{id}",
|
||||
scope: "read",
|
||||
summary: "Agent 运行详情:事件按 seq 分页、追加消息、服务端核查的目标汇报",
|
||||
summary: "智能体运行详情:事件按 seq 分页、追加消息、服务端核查的目标汇报",
|
||||
params: IdParam,
|
||||
query: agent.AgentRunDetailQuery,
|
||||
response: agent.AgentRunDetail,
|
||||
@@ -1617,7 +1617,7 @@ export const Api = {
|
||||
method: "get",
|
||||
path: "/capabilities/stats",
|
||||
scope: "read",
|
||||
summary: "能力调用统计:每能力调用数、失败数、平均耗时、最近调用、调用来源(内置 Agent / MCP / REST)",
|
||||
summary: "能力调用统计:每能力调用数、失败数、平均耗时、最近调用、调用来源(内置智能体 / MCP / REST)",
|
||||
query: mcp.CapabilityStatsQuery,
|
||||
response: mcp.CapabilityStatsResponse,
|
||||
}),
|
||||
@@ -1775,14 +1775,14 @@ export const Api = {
|
||||
method: "get",
|
||||
path: "/pi/upstreams",
|
||||
scope: "admin",
|
||||
summary: "Agent 上游列表(不含密钥)",
|
||||
summary: "模型服务列表(不含密钥)",
|
||||
response: pi.PiUpstreamList,
|
||||
}),
|
||||
get: ep({
|
||||
method: "get",
|
||||
path: "/pi/upstreams/{id}",
|
||||
scope: "admin",
|
||||
summary: "Agent 上游详情",
|
||||
summary: "模型服务详情",
|
||||
params: IdParam,
|
||||
response: pi.PiUpstream,
|
||||
}),
|
||||
@@ -1791,7 +1791,7 @@ export const Api = {
|
||||
path: "/pi/upstreams",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary: "新建 Agent 上游(apiKey 只写,存 secret 表)",
|
||||
summary: "新建模型服务(apiKey 只写,存 secret 表)",
|
||||
body: pi.PiUpstreamCreate,
|
||||
response: pi.PiUpstream,
|
||||
}),
|
||||
@@ -1800,7 +1800,7 @@ export const Api = {
|
||||
path: "/pi/upstreams/{id}",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary: "修改 Agent 上游(provider 不可改;makeDefault 设默认上游)",
|
||||
summary: "修改模型服务(provider 不可改;makeDefault 设默认模型服务)",
|
||||
params: IdParam,
|
||||
body: pi.PiUpstreamUpdate,
|
||||
response: pi.PiUpstream,
|
||||
@@ -1810,7 +1810,7 @@ export const Api = {
|
||||
path: "/pi/upstreams/{id}",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary: "删除 Agent 上游(连同凭据;是默认上游时清空默认)",
|
||||
summary: "删除模型服务(连同凭据;是默认模型服务时清空默认)",
|
||||
params: IdParam,
|
||||
response: OkResult,
|
||||
}),
|
||||
@@ -1819,7 +1819,7 @@ export const Api = {
|
||||
path: "/pi/upstreams/{id}/test",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary: "测试上游:默认只解析凭据与校验模型(不产生费用);mode=completion 发一次最小补全",
|
||||
summary: "测试模型服务:默认只解析凭据与校验模型(不产生费用);mode=completion 发一次最小补全",
|
||||
params: IdParam,
|
||||
body: pi.PiUpstreamTestRequest,
|
||||
response: pi.PiUpstreamTestResult,
|
||||
@@ -1828,7 +1828,7 @@ export const Api = {
|
||||
method: "get",
|
||||
path: "/pi/upstreams/{id}/models",
|
||||
scope: "admin",
|
||||
summary: "上游模型列表:已获取过远程列表时返回远程与静态目录的合并(远程优先),否则静态目录",
|
||||
summary: "模型列表:已获取过远程列表时返回远程与静态目录的合并(远程优先),否则静态目录",
|
||||
params: IdParam,
|
||||
response: pi.PiUpstreamModels,
|
||||
}),
|
||||
@@ -1837,7 +1837,7 @@ export const Api = {
|
||||
path: "/pi/upstreams/{id}/models/refresh",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary: "获取模型列表:用该上游的凭证调 provider 的模型列表接口并存库;失败或不支持时回静态目录并给出原因",
|
||||
summary: "获取模型列表:用该模型服务的凭证调 provider 的模型列表接口并存库;失败或不支持时回静态目录并给出原因",
|
||||
params: IdParam,
|
||||
response: pi.PiUpstreamModels,
|
||||
}),
|
||||
@@ -1846,7 +1846,7 @@ export const Api = {
|
||||
path: "/pi/models/probe",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary: "新建上游前试拉模型列表(凭证只用于这一次请求,不落库)",
|
||||
summary: "新建模型服务前试拉模型列表(凭证只用于这一次请求,不落库)",
|
||||
body: pi.PiModelsProbeRequest,
|
||||
response: pi.PiUpstreamModels,
|
||||
}),
|
||||
@@ -2112,7 +2112,7 @@ export const Api = {
|
||||
method: "get",
|
||||
path: "/identity-pool",
|
||||
scope: "read",
|
||||
summary: "待识别池(可按原始标题检索;候选不建正式作品)",
|
||||
summary: "未识别资源(可按原始标题检索;候选不建正式作品)",
|
||||
query: identity.IdentityPoolQuery,
|
||||
response: cursorPage(identity.IdentityPoolItem),
|
||||
}),
|
||||
@@ -2120,7 +2120,7 @@ export const Api = {
|
||||
method: "post",
|
||||
path: "/identity-pool/{id}/assign",
|
||||
scope: "organize.execute",
|
||||
summary: "为待识别资源手动指定候选 / 本地作品 / 外部身份",
|
||||
summary: "为未识别资源手动指定候选 / 本地作品 / 外部身份",
|
||||
params: IdParam,
|
||||
body: identity.ReleaseIdentityAssign,
|
||||
response: ReleaseIdentityResult,
|
||||
@@ -2129,7 +2129,7 @@ export const Api = {
|
||||
method: "post",
|
||||
path: "/identity-pool/{id}/dismiss",
|
||||
scope: "organize.execute",
|
||||
summary: "忽略待识别资源(不再自动复查)",
|
||||
summary: "忽略未识别资源(不再自动复查)",
|
||||
params: IdParam,
|
||||
body: z.object({ note: z.string().max(2000).optional() }),
|
||||
response: identity.IdentityCandidate,
|
||||
@@ -2157,7 +2157,7 @@ export const Api = {
|
||||
path: "/identity-pool/{id}/enrich/apply",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary: "应用元数据补全建议:建本地临时作品(或并入同名作品)并关联待识别资源(只接受控制台管理员会话)",
|
||||
summary: "应用元数据补全建议:建本地临时作品(或并入同名作品)并关联未识别资源(只接受控制台管理员会话)",
|
||||
params: IdParam,
|
||||
body: identity.EnrichApplyInput,
|
||||
response: identity.EnrichApplyResult,
|
||||
@@ -2178,7 +2178,7 @@ export const Api = {
|
||||
method: "get",
|
||||
path: "/library/{id}",
|
||||
scope: "read",
|
||||
summary: "作品详情:实际路径、身份映射、元数据来源、各版本目标、候选拒绝理由、下载历史与校验结果",
|
||||
summary: "作品详情:实际路径、身份映射、元数据来源、各收录版本、候选拒绝理由、下载历史与校验结果",
|
||||
params: IdParam,
|
||||
response: organization.LibraryDetail,
|
||||
}),
|
||||
@@ -2205,7 +2205,7 @@ export const Api = {
|
||||
method: "get",
|
||||
path: "/inventory",
|
||||
scope: "read",
|
||||
summary: "库存叠加(站点工作台):按作品返回已有版本 / 版本目标缺口 / 下载中",
|
||||
summary: "库存叠加(站点工作台):按作品返回已有版本 / 收录版本缺口 / 下载中",
|
||||
query: organization.InventoryQuery,
|
||||
response: organization.InventoryResponse,
|
||||
}),
|
||||
@@ -2482,7 +2482,7 @@ export const Api = {
|
||||
method: "post",
|
||||
path: "/browser/sessions/{id}/control/release",
|
||||
scope: "admin",
|
||||
summary: "交还控制权:Agent 恢复前必须重新读取页面,旧元素引用失效",
|
||||
summary: "交还控制权:智能体恢复前必须重新读取页面,旧元素引用失效",
|
||||
params: browser.BrowserSessionParams,
|
||||
body: browser.BrowserControlRelease,
|
||||
response: browser.BrowserControlResult,
|
||||
@@ -2496,7 +2496,7 @@ export const Api = {
|
||||
path: "/schedules",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary: "定时任务看板:全部登记的周期任务与 Agent 巡检、统计、调度器是否在线",
|
||||
summary: "定时任务看板:全部登记的周期任务与智能体巡检、统计、调度器是否在线",
|
||||
response: schedules.ScheduleBoard,
|
||||
}),
|
||||
create: ep({
|
||||
@@ -2504,7 +2504,7 @@ export const Api = {
|
||||
path: "/schedules",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary: "新建自定义 Agent 巡检(目标模板 + 能力白名单 + 预算 + 间隔)",
|
||||
summary: "新建自定义智能体巡检(目标模板 + 能力白名单 + 预算 + 间隔)",
|
||||
body: schedules.AgentCheckCreate,
|
||||
response: schedules.ScheduledJob,
|
||||
}),
|
||||
@@ -2523,7 +2523,7 @@ export const Api = {
|
||||
path: "/schedules/{id}",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary: "删除自定义 Agent 巡检(内置任务不可删)",
|
||||
summary: "删除自定义智能体巡检(内置任务不可删)",
|
||||
params: schedules.ScheduleIdParam,
|
||||
response: OkResult,
|
||||
}),
|
||||
|
||||
@@ -256,7 +256,7 @@ export const SubscriptionBrief = z.object({
|
||||
status: SubscriptionStatus,
|
||||
priority: AcquisitionPriority,
|
||||
statusReason: z.string().nullable(),
|
||||
/** 版本目标:总数 / 已满足数 */
|
||||
/** 收录版本:总数 / 已满足数 */
|
||||
slots: z.object({ total: NonNegInt, satisfied: NonNegInt }),
|
||||
lastSearchedAt: z.string().nullable(),
|
||||
nextSearchAt: z.string().nullable(),
|
||||
|
||||
@@ -133,7 +133,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
||||
"catalog.lookup": d(
|
||||
"catalog.lookup",
|
||||
"查作品",
|
||||
"按标题/别名/外部 ID 查本地作品与元数据候选。确认作品身份、拿作品 ID 时用;查库里已有文件与版本目标用 inventory.query",
|
||||
"按标题/别名/外部 ID 查本地作品与元数据候选。确认作品身份、拿作品 ID 时用;查库里已有文件与收录版本用 inventory.query",
|
||||
"read",
|
||||
"CatalogLookup",
|
||||
"read",
|
||||
@@ -177,7 +177,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
||||
"download.plan": d(
|
||||
"download.plan",
|
||||
"生成下载计划",
|
||||
"评估候选,生成占用版本目标(slot)与预算预留的下载计划;不提交。选定资源后、提交前用;不会提交下载,也不预留版本目标",
|
||||
"评估候选,生成占用收录版本(slot)与预算预留的下载计划;不提交。选定资源后、提交前用;不会提交下载,也不预留收录版本",
|
||||
"preview",
|
||||
"DownloadPlan",
|
||||
"organize.execute",
|
||||
@@ -199,7 +199,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
||||
"download.recheck": d(
|
||||
"download.recheck",
|
||||
"重新校验/恢复下载",
|
||||
"对已纳管的下载任务强制重新校验(action=recheck),或恢复非阈值、非人工的暂停(action=resume);不换源、不删除、不改保存路径。下载失败或文件缺失时用;换源、删除不在此能力内",
|
||||
"对本工具管理的下载任务强制重新校验(action=recheck),或恢复非阈值、非人工的暂停(action=resume);不换源、不删除、不改保存路径。下载失败或文件缺失时用;换源、删除不在此能力内",
|
||||
"execute",
|
||||
"DownloadRecheck",
|
||||
"organize.execute",
|
||||
@@ -287,7 +287,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
||||
"inventory.query": d(
|
||||
"inventory.query",
|
||||
"查库存",
|
||||
"查询本地资产、版本目标满足情况与外部库存证据。查某个作品有哪些文件与版本目标时用;想盘点全部就不给 query,不要用单字母探查",
|
||||
"查询本地资产、收录版本满足情况与外部库存证据。查某个作品有哪些文件与收录版本时用;想盘点全部就不给 query,不要用单字母探查",
|
||||
"read",
|
||||
"InventoryQuery",
|
||||
"read",
|
||||
@@ -299,7 +299,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
||||
"subscription.list": d(
|
||||
"subscription.list",
|
||||
"列出订阅",
|
||||
"按状态或媒体类型分组列出订阅摘要(作品、模式、状态、版本目标满足数、上次/下次搜索)。盘点订阅、找缺版本时用;查单个作品的文件与版本目标用 inventory.query",
|
||||
"按状态或媒体类型分组列出订阅摘要(作品、模式、状态、收录版本满足数、上次/下次搜索)。盘点订阅、找缺版本时用;查单个作品的文件与收录版本用 inventory.query",
|
||||
"read",
|
||||
"SubscriptionList",
|
||||
"read",
|
||||
|
||||
@@ -18,6 +18,7 @@ export * from "./ops.ts";
|
||||
export * from "./organization.ts";
|
||||
export * from "./pi.ts";
|
||||
export * from "./policy.ts";
|
||||
export * from "./reasons.ts";
|
||||
export * from "./redact.ts";
|
||||
export * from "./settings.ts";
|
||||
export * from "./sources.ts";
|
||||
@@ -510,7 +510,7 @@ export const Dashboard = z.object({
|
||||
}),
|
||||
activeSubscriptions: NonNegInt,
|
||||
slotCounts: SlotStatusCounts,
|
||||
/** 版本目标满足率 = satisfied / 全部版本目标(0–1);没有版本目标时为 null */
|
||||
/** 收录版本满足率 = satisfied / 全部收录版本(0–1);没有收录版本时为 null */
|
||||
slotSatisfiedRate: z.number().min(0).max(1).nullable(),
|
||||
/** 本地账本计数:作品不含已合并;季 = content_unit.kind=season(不含 retired);资产 = active 的视频 / 原盘目录 */
|
||||
library: z.object({
|
||||
|
||||
@@ -245,9 +245,9 @@ export const PiUpstreamCreate = z
|
||||
})
|
||||
.superRefine((v, ctx) => {
|
||||
if (v.authKind !== "api_key" && v.apiKey !== undefined)
|
||||
ctx.addIssue({ code: "custom", path: ["apiKey"], message: "只有 api_key 型上游可以填 apiKey" });
|
||||
ctx.addIssue({ code: "custom", path: ["apiKey"], message: "只有 api_key 型模型服务可以填 apiKey" });
|
||||
if (v.authKind === "oauth" && v.baseUrl)
|
||||
ctx.addIssue({ code: "custom", path: ["baseUrl"], message: "订阅登录型上游不能覆盖 baseUrl" });
|
||||
ctx.addIssue({ code: "custom", path: ["baseUrl"], message: "订阅登录型模型服务不能覆盖 baseUrl" });
|
||||
if (v.provider === "openai-compatible") {
|
||||
if (!v.baseUrl) ctx.addIssue({ code: "custom", path: ["baseUrl"], message: "自定义兼容端点必须填 baseUrl" });
|
||||
if (!v.defaultModel)
|
||||
|
||||
@@ -79,7 +79,7 @@ export const Subscription = EntityBase.extend({
|
||||
/** 上次主动搜索时间 / 下次到期(按 params.searchIntervalHours,缺省 24h) */
|
||||
lastSearchedAt: Timestamp.nullable(),
|
||||
nextSearchAt: Timestamp.nullable(),
|
||||
/** 上次进展:任一版本目标的单元转为已入库、或总集数变化时写入;总览"上次更新"另合并计划预留时间 */
|
||||
/** 上次进展:任一收录版本的单元转为已入库、或总集数变化时写入;总览"上次更新"另合并计划预留时间 */
|
||||
lastProgressAt: Timestamp.nullable(),
|
||||
/** 派生字段(列表返回):作品标题,未建档时为 null */
|
||||
mediaEntityTitle: z.string().nullable().optional(),
|
||||
@@ -97,10 +97,10 @@ export const FillBudget = z
|
||||
.strict();
|
||||
export type FillBudget = z.infer<typeof FillBudget>;
|
||||
|
||||
/** MP 分辨率(resolution)→ 版本目标硬条件 resolutions */
|
||||
/** MP 分辨率(resolution)→ 收录版本硬条件 resolutions */
|
||||
export const SubscriptionResolution = z.enum(["4k", "1080p", "720p"]);
|
||||
export type SubscriptionResolution = z.infer<typeof SubscriptionResolution>;
|
||||
/** MP 资源质量(quality / resource_type)→ 版本目标硬条件 sources / codecs;uhd 写进订阅私有规则组的包含 */
|
||||
/** MP 资源质量(quality / resource_type)→ 收录版本硬条件 sources / codecs;uhd 写进订阅私有规则组的包含 */
|
||||
export const SubscriptionSourcePreset = z.enum([
|
||||
"bluray_disc",
|
||||
"remux",
|
||||
@@ -112,7 +112,7 @@ export const SubscriptionSourcePreset = z.enum([
|
||||
"h264",
|
||||
]);
|
||||
export type SubscriptionSourcePreset = z.infer<typeof SubscriptionSourcePreset>;
|
||||
/** MP 特效(effect)→ 版本目标硬条件 hdr + 软偏好 hdr 顺序;atmos 写进订阅私有规则组的包含 */
|
||||
/** MP 特效(effect)→ 收录版本硬条件 hdr + 软偏好 hdr 顺序;atmos 写进订阅私有规则组的包含 */
|
||||
export const SubscriptionEffect = z.enum(["dv", "hdr", "sdr", "atmos"]);
|
||||
export type SubscriptionEffect = z.infer<typeof SubscriptionEffect>;
|
||||
|
||||
@@ -120,7 +120,7 @@ export type SubscriptionEffect = z.infer<typeof SubscriptionEffect>;
|
||||
* subscription.params 的已知键(JSON 列,其余键原样保留):
|
||||
* siteIds 站点范围;episodeRange 集范围(对照 MP start/total_episode);searchIntervalHours 主动搜索周期(0 = 关);
|
||||
* stopAfter 截止时间;downloaderInstanceId 指定下载器;ruleGroupIds 规则组;keywords 追加检索词;
|
||||
* include/exclude 订阅级关键词正则(作用于全部版本目标);libraryRootId 库根;downloadSavePath 下载保存路径;
|
||||
* include/exclude 订阅级关键词正则(作用于全部收录版本);libraryRootId 库根;downloadSavePath 下载保存路径;
|
||||
* autoFill/fillBudget 多版本自动补全;resolution/source/effect 质量预设(MP resolution/quality/effect)。
|
||||
* include/exclude 保存时同步为订阅私有规则组(scope.subscriptionId,随订阅删除),评估只读规则组。
|
||||
* 写入时兼容 MP 字段与旧键(sites/downloader/save_path/start_episode/filter_groups/keyword/autoFillVersions…),
|
||||
@@ -150,7 +150,7 @@ export const SubscriptionParams = z
|
||||
downloadSavePath: z.string().min(1).max(1000).optional(),
|
||||
autoFill: AutoFillMode.optional(),
|
||||
fillBudget: FillBudget.optional(),
|
||||
/** MP resolution / quality / effect:保存时写到该订阅自己的版本目标(硬条件 + 软偏好) */
|
||||
/** MP resolution / quality / effect:保存时写到该订阅自己的收录版本(硬条件 + 软偏好) */
|
||||
resolution: SubscriptionResolution.optional(),
|
||||
source: SubscriptionSourcePreset.optional(),
|
||||
effect: SubscriptionEffect.optional(),
|
||||
@@ -296,11 +296,11 @@ export type SubscriptionGroup = z.infer<typeof SubscriptionGroup>;
|
||||
export const SubscriptionSort = z.enum(["lastUpdated", "nextSearch", "missing", "created", "name"]);
|
||||
export type SubscriptionSort = z.infer<typeof SubscriptionSort>;
|
||||
|
||||
/** 版本目标在订阅范围内的计数摘要(卡片徽标 / 矩阵行尾) */
|
||||
/** 收录版本在订阅范围内的计数摘要(卡片徽标 / 矩阵行尾) */
|
||||
export const SubscriptionSlotSummary = z.object({
|
||||
slotId: Id,
|
||||
name: z.string(),
|
||||
/** 同订阅内顺序最小的版本目标 */
|
||||
/** 同订阅内顺序最小的收录版本 */
|
||||
primary: z.boolean(),
|
||||
status: z.string(),
|
||||
have: z.number().int(),
|
||||
@@ -347,7 +347,7 @@ export const SubscriptionOverviewItem = Subscription.extend({
|
||||
source: z.enum(["subscription", "rule_group", "default"]),
|
||||
budget: FillBudget,
|
||||
}),
|
||||
/** 任一版本目标开了升级且未达顶档 */
|
||||
/** 任一收录版本开了升级且未达顶档 */
|
||||
upgrading: z.boolean(),
|
||||
/** 已完成分组里 continuous 订阅在等下一季 */
|
||||
awaitingNewSeason: z.boolean(),
|
||||
@@ -418,7 +418,7 @@ export const SubscriptionCoverage = z.object({
|
||||
columns: z.array(CoverageColumn),
|
||||
}),
|
||||
),
|
||||
/** 首行 slotId = null 为"库内任一版本";其后每个版本目标一行,按 position */
|
||||
/** 首行 slotId = null 为"库内任一版本";其后每个收录版本一行,按 position */
|
||||
rows: z.array(
|
||||
z.object({
|
||||
slotId: Id.nullable(),
|
||||
@@ -714,7 +714,7 @@ export type PreferenceSuppressionUpdate = z.infer<typeof PreferenceSuppressionUp
|
||||
/** Slot 模板:策略规则里用于自动建 Slot(新作品收录 / 订阅缺省 Slot);字段同 AcquisitionSlot 规则列 */
|
||||
export const SlotTemplate = z.object({
|
||||
name: z.string().min(1).max(64),
|
||||
/** 建版本目标时写入 ruleGroupIds */
|
||||
/** 建收录版本时写入 ruleGroupIds */
|
||||
ruleGroupIds: z.array(Id).optional(),
|
||||
hardConditions: JsonObject.optional(),
|
||||
softPreferences: JsonObject.optional(),
|
||||
@@ -850,7 +850,7 @@ export const RuleGroup = EntityBase.extend({
|
||||
/** 语义指纹(kind + scope + 层 + 硬排除),停用/删除后写抑制记录 */
|
||||
fingerprint: z.string(),
|
||||
evidence: JsonObject,
|
||||
/** 结构化硬条件(include / exclude 正则等),评估时并入版本目标硬条件;订阅私有组用它存表单的包含 / 排除 */
|
||||
/** 结构化硬条件(include / exclude 正则等),评估时并入收录版本硬条件;订阅私有组用它存表单的包含 / 排除 */
|
||||
hard: JsonObject.optional(),
|
||||
/** 非 null = 属于该类默认链,按此排序 */
|
||||
defaultOrder: z.number().int().nullable(),
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// 内部原因码 → 一句人话 + 下一步(DEAI-2)。整理记录、待处理、下载时间线、任务都用这张表;
|
||||
// 原码只放在界面的「技术信息」里,不直接给人看。不认识的码返回 null,由调用方决定兜底文字。
|
||||
|
||||
export type ReasonText = {
|
||||
/** 发生了什么(一句,不带句号) */
|
||||
text: string;
|
||||
/** 下一步能做什么(一句,不带句号);没有可做的就不给 */
|
||||
action?: string;
|
||||
};
|
||||
|
||||
const REASONS: Record<string, ReasonText> = {
|
||||
identity_unknown: { text: "没识别出作品", action: "指定作品后重新整理" },
|
||||
identity_ambiguous: { text: "没识别出作品", action: "指定作品" },
|
||||
identity_unresolved: { text: "没识别出作品", action: "指定作品" },
|
||||
target_exists: { text: "媒体库里已有同名文件", action: "在待处理里选择替换或跳过" },
|
||||
duplicate_target: { text: "改名后与其他文件同名", action: "改命名模板或排除其中一个" },
|
||||
replace_requires_grant: { text: "替换已有文件需要你批准", action: "在待处理里批准" },
|
||||
hnr_unsatisfied: { text: "做种时间还不够,不能移动", action: "改用复制,或等做种完成" },
|
||||
hnr_unknown: { text: "不确定做种要求是否满足", action: "改用复制,或确认站点规则" },
|
||||
orphan_sidecar: { text: "字幕等附属文件找不到对应视频" },
|
||||
no_main_video: { text: "没有可入库的主视频" },
|
||||
below_min_size: { text: "文件小于最小体积,没有识别", action: "需要时在设置里调低最小体积" },
|
||||
too_small: { text: "文件小于最小体积" },
|
||||
source_path_unknown: { text: "下载器没有报告保存路径", action: "在下载器里确认任务还在" },
|
||||
path_mapping_missing: { text: "下载目录没有路径映射", action: "在设置 → 下载器里添加路径映射" },
|
||||
file_list_unavailable: { text: "读不到下载器里的文件列表", action: "稍后重试" },
|
||||
source_missing: { text: "找不到源文件", action: "检查下载目录是否挂载" },
|
||||
source_size_mismatch: { text: "源文件大小与下载器记录不一致", action: "等下载完成后重试" },
|
||||
too_many_files: { text: "文件太多,超出单次整理上限", action: "分批整理" },
|
||||
no_source: { text: "没有要整理的文件" },
|
||||
entity_not_found: { text: "作品已不存在" },
|
||||
no_library_root: { text: "没有可用的媒体库目录", action: "在设置 → 媒体库与目录里添加" },
|
||||
library_root_offline: { text: "媒体库目录不可访问或未挂载", action: "检查挂载后重试" },
|
||||
target_dir_outside_root: { text: "目标目录不在媒体库目录里" },
|
||||
slot_entity_mismatch: { text: "收录版本和作品对不上" },
|
||||
binding_on_other_root: { text: "作品已放在另一个媒体库目录", action: "在作品详情里调整目录" },
|
||||
move_downgraded_to_copy: { text: "做种未完成,已改为复制" },
|
||||
already_in_library: { text: "已在媒体库里" },
|
||||
main_not_transferred: { text: "主文件没有整理,附属文件一起跳过" },
|
||||
timeout: { text: "操作超时", action: "稍后重试" },
|
||||
downloader_unavailable: { text: "下载器连不上", action: "在设置 → 下载器里测试连接" },
|
||||
downloader_error: { text: "下载器报错", action: "查看下载器日志" },
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析原因串:`identity_unknown`、`blocked:target_exists,hnr_unknown`、`move_downgraded_to_copy:hnr_unknown`。
|
||||
* 多个码时合并成一句(用";"分隔)。
|
||||
*/
|
||||
export function reasonText(raw: string | null | undefined): ReasonText | null {
|
||||
if (!raw) return null;
|
||||
const s = raw.trim();
|
||||
const [head, rest] = s.includes(":") ? [s.slice(0, s.indexOf(":")), s.slice(s.indexOf(":") + 1)] : [s, ""];
|
||||
if (head === "blocked" && rest) {
|
||||
const parts = rest
|
||||
.split(",")
|
||||
.map((c) => REASONS[c.trim()])
|
||||
.filter((x): x is ReasonText => !!x);
|
||||
if (!parts.length) return null;
|
||||
return {
|
||||
text: [...new Set(parts.map((p) => p.text))].join(";"),
|
||||
...(parts[0]?.action ? { action: parts[0].action } : {}),
|
||||
};
|
||||
}
|
||||
return REASONS[s] ?? REASONS[head] ?? null;
|
||||
}
|
||||
|
||||
/** 只要一句话:"没识别出作品。指定作品后重新整理";不认识的码返回 fallback */
|
||||
export function reasonSentence(raw: string | null | undefined, fallback = "处理失败"): string {
|
||||
const r = reasonText(raw);
|
||||
if (!r) return raw && !/^[a-z_:.,]+$/.test(raw) ? raw : fallback;
|
||||
return r.action ? `${r.text}。${r.action}` : r.text;
|
||||
}
|
||||
|
||||
/** 看起来像内部码(小写下划线 / 冒号)而不是给人看的句子 */
|
||||
export const looksLikeCode = (v: string | null | undefined) => !!v && /^[a-z0-9_]+([:.,][a-z0-9_:.,-]*)?$/.test(v);
|
||||
@@ -40,7 +40,7 @@ export const LocalMediaHit = z.object({
|
||||
matchedBy: z.enum(["exact", "prefix", "similar"]),
|
||||
/** 命中的别名原文(可能是译名/原名/别名) */
|
||||
matchedAlias: z.string().nullable(),
|
||||
/** 版本目标状态摘要 + 库存 */
|
||||
/** 收录版本状态摘要 + 库存 */
|
||||
slotCounts: SlotStatusCounts,
|
||||
versionCount: NonNegInt,
|
||||
downloadingCount: NonNegInt,
|
||||
@@ -84,7 +84,7 @@ export type SearchReleaseParsed = z.infer<typeof SearchReleaseParsed>;
|
||||
|
||||
/**
|
||||
* 库存叠加状态(只在关联到作品时给出):
|
||||
* downloading 有下载中任务 > gap 有未满足的版本目标 > owned 已有版本 > subscribed 只有订阅 > none 作品已登记但无库存/目标
|
||||
* downloading 有下载中任务 > gap 有未满足的收录版本 > owned 已有版本 > subscribed 只有订阅 > none 作品已登记但无库存/目标
|
||||
*/
|
||||
export const SearchInventoryState = z.enum(["downloading", "gap", "owned", "subscribed", "none"]);
|
||||
export type SearchInventoryState = z.infer<typeof SearchInventoryState>;
|
||||
@@ -193,10 +193,10 @@ export const WorkInventory = z.object({
|
||||
/** 已入库版本数(主视频资产) */
|
||||
versionCount: NonNegInt,
|
||||
downloadingCount: NonNegInt,
|
||||
/** 版本目标:总数 / 已满足 */
|
||||
/** 收录版本:总数 / 已满足 */
|
||||
slotTotal: NonNegInt,
|
||||
slotSatisfied: NonNegInt,
|
||||
/** 未满足的版本目标(empty / attention / upgrade_pending) */
|
||||
/** 未满足的收录版本(empty / attention / upgrade_pending) */
|
||||
missing: z.array(z.object({ slotId: Id, name: z.string() })),
|
||||
});
|
||||
export type WorkInventory = z.infer<typeof WorkInventory>;
|
||||
|
||||
@@ -790,6 +790,7 @@ export const SettingsSectionId = z.enum([
|
||||
"metadata",
|
||||
"agent",
|
||||
"api-keys",
|
||||
"interface",
|
||||
"account",
|
||||
]);
|
||||
export type SettingsSectionId = z.infer<typeof SettingsSectionId>;
|
||||
@@ -812,14 +813,14 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
|
||||
{
|
||||
id: "automation",
|
||||
label: "自动化",
|
||||
description: "总开关、缓存水位、每日预算、保留天数",
|
||||
description: "总开关、下载盘、每日预算、保留天数",
|
||||
legacyTabs: [{ tab: "automation" }, { tab: "storage", anchor: "cache" }],
|
||||
anchors: [
|
||||
{ id: "switches", label: "开关" },
|
||||
{ id: "cache", label: "缓存水位" },
|
||||
{ id: "cache", label: "下载盘" },
|
||||
{ id: "budget", label: "每日预算" },
|
||||
{ id: "retention", label: "保留" },
|
||||
{ id: "storage", label: "存储与保留" },
|
||||
{ id: "advanced", label: "高级" },
|
||||
],
|
||||
fields: [
|
||||
"automation.enabled",
|
||||
@@ -874,10 +875,10 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
|
||||
{
|
||||
id: "library",
|
||||
label: "媒体库与目录",
|
||||
description: "库根、哨兵状态、默认整理方式、命名模板",
|
||||
description: "媒体库目录、默认整理方式、命名模板",
|
||||
legacyTabs: [{ tab: "library-roots", anchor: "roots" }],
|
||||
anchors: [
|
||||
{ id: "roots", label: "库根" },
|
||||
{ id: "roots", label: "媒体库目录" },
|
||||
{ id: "organize", label: "整理方式" },
|
||||
{ id: "naming", label: "命名模板" },
|
||||
],
|
||||
@@ -929,11 +930,11 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
|
||||
},
|
||||
{
|
||||
id: "agent",
|
||||
label: "Agent",
|
||||
description: "Agent 上游、运行预算、自动调查、网页搜索",
|
||||
label: "智能体",
|
||||
description: "模型服务、运行预算、自动调查、网页搜索",
|
||||
legacyTabs: [{ tab: "pi" }],
|
||||
anchors: [
|
||||
{ id: "upstream", label: "上游" },
|
||||
{ id: "upstream", label: "模型服务" },
|
||||
{ id: "runtime", label: "上下文与子会话" },
|
||||
{ id: "budget", label: "预算" },
|
||||
{ id: "investigate", label: "自动调查" },
|
||||
@@ -979,6 +980,15 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
|
||||
fields: [],
|
||||
resources: ["api-keys"],
|
||||
},
|
||||
{
|
||||
id: "interface",
|
||||
label: "界面",
|
||||
description: "只影响当前浏览器",
|
||||
legacyTabs: [],
|
||||
anchors: [{ id: "display", label: "显示" }],
|
||||
fields: [],
|
||||
resources: ["ui-preferences"],
|
||||
},
|
||||
{
|
||||
id: "account",
|
||||
label: "账户与安全",
|
||||
@@ -987,7 +997,7 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
|
||||
anchors: [
|
||||
{ id: "password", label: "密码" },
|
||||
{ id: "totp", label: "二次验证" },
|
||||
{ id: "passkeys", label: "Passkey" },
|
||||
{ id: "passkeys", label: "通行密钥" },
|
||||
{ id: "session", label: "会话" },
|
||||
{ id: "shortcuts", label: "快捷键" },
|
||||
],
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { looksLikeCode, reasonSentence, reasonText } from "../src/reasons.ts";
|
||||
|
||||
describe("reasonText(DEAI-2 原因码人话化)", () => {
|
||||
it("裸码、blocked:多码、带后缀的码都能翻译", () => {
|
||||
expect(reasonText("identity_unknown")).toEqual({ text: "没识别出作品", action: "指定作品后重新整理" });
|
||||
expect(reasonText("blocked:target_exists")?.text).toBe("媒体库里已有同名文件");
|
||||
expect(reasonText("blocked:target_exists,hnr_unknown")?.text).toBe("媒体库里已有同名文件;不确定做种要求是否满足");
|
||||
expect(reasonText("move_downgraded_to_copy:hnr_unknown")?.text).toBe("做种未完成,已改为复制");
|
||||
expect(reasonText("below_min_size")?.text).toContain("最小体积");
|
||||
});
|
||||
|
||||
it("不认识的码返回 null;句子兜底不把码原样给人看", () => {
|
||||
expect(reasonText("some_new_code")).toBeNull();
|
||||
expect(reasonSentence("some_new_code")).toBe("处理失败");
|
||||
expect(reasonSentence("磁盘已满")).toBe("磁盘已满");
|
||||
expect(reasonSentence("identity_unknown")).toBe("没识别出作品。指定作品后重新整理");
|
||||
});
|
||||
|
||||
it("looksLikeCode 只认内部码", () => {
|
||||
expect(looksLikeCode("blocked:target_exists")).toBe(true);
|
||||
expect(looksLikeCode("identity_unknown")).toBe(true);
|
||||
expect(looksLikeCode("目标已存在")).toBe(false);
|
||||
expect(looksLikeCode("Connection refused")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "../src/settings.ts";
|
||||
|
||||
describe("SETTINGS_SECTIONS", () => {
|
||||
it("9 个分区,顺序与 SettingsSectionId 一致", () => {
|
||||
it("10 个分区,顺序与 SettingsSectionId 一致", () => {
|
||||
expect(SETTINGS_SECTIONS.map((s) => s.id)).toEqual(SettingsSectionId.options);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 多版本自动补全(docs/design/subscriptions.md §2.2):非主版本目标在主版本已满足的单元上补下其他版本。
|
||||
// 多版本自动补全(docs/design/subscriptions.md §2.2):非主收录版本在主版本已满足的单元上补下其他版本。
|
||||
// 开关按 订阅 → 规则组(链上后者优先)→ 系统缺省(开)取值;补全预算按订阅、按自然日计,超出记延后。
|
||||
|
||||
export type AutoFillModeValue = "inherit" | "on" | "off";
|
||||
|
||||
@@ -76,7 +76,7 @@ export const NAMING_VARIABLES: readonly NamingVariableDef[] = [
|
||||
ours: true,
|
||||
},
|
||||
{ name: "issue", group: "ours", label: "综艺期号", example: "12", ours: true },
|
||||
{ name: "slotName", group: "ours", label: "版本目标名", example: "4K HDR", ours: true },
|
||||
{ name: "slotName", group: "ours", label: "收录版本名", example: "4K HDR", ours: true },
|
||||
{ name: "libraryRoot", group: "ours", label: "库根名", example: "电视剧-A", ours: true },
|
||||
];
|
||||
|
||||
@@ -124,7 +124,7 @@ function sourceOf(p: Partial<ParsedRelease>): string | undefined {
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染上下文(MP 变量名):作品信息 + 发布解析 + 季集元数据 + 版本目标/库根。
|
||||
* 渲染上下文(MP 变量名):作品信息 + 发布解析 + 季集元数据 + 收录版本/库根。
|
||||
* 标题用本地作品名,不用发布标题。空值不放进上下文(同 MP 剔除 None)。
|
||||
*/
|
||||
export function buildNamingVars(
|
||||
|
||||
@@ -376,14 +376,14 @@ export function currentPri(spec: SlotSpec, current: QualityFacts): number {
|
||||
return effectiveLadder(spec, pseudo, {}, ruleChainOf(spec, pseudo, {})).pri;
|
||||
}
|
||||
|
||||
/** 版本目标引用的规则组链求值;没有规则组时为 null */
|
||||
/** 收录版本引用的规则组链求值;没有规则组时为 null */
|
||||
export function ruleChainOf(spec: SlotSpec, r: ParsedRelease, ctx: EvaluationContext = {}): RuleChainResult | null {
|
||||
if (!spec.rules?.groups.length) return null;
|
||||
return evaluateRuleChain(spec.rules.groups, r, ctx, catalogOf(spec.rules.atoms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际生效的阶梯:版本目标自身 soft.tiers 视为排在最前的匿名组,规则组链里有带层的组时 pri 取最后一个带层的组。
|
||||
* 实际生效的阶梯:收录版本自身 soft.tiers 视为排在最前的匿名组,规则组链里有带层的组时 pri 取最后一个带层的组。
|
||||
*/
|
||||
export function effectiveLadder(
|
||||
spec: SlotSpec,
|
||||
|
||||
@@ -11,9 +11,9 @@ export interface SubscriptionGroupInput {
|
||||
mode: "once" | "continuous";
|
||||
/** 范围内各季总集数是否都已知(电影恒为 true) */
|
||||
totalsKnown: boolean;
|
||||
/** 任一版本目标有已入库或在途单元 */
|
||||
/** 任一收录版本有已入库或在途单元 */
|
||||
anyProgress: boolean;
|
||||
/** 所有版本目标在范围内全覆盖(且总集数已知) */
|
||||
/** 所有收录版本在范围内全覆盖(且总集数已知) */
|
||||
allSlotsComplete: boolean;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export interface CoverageRowResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* 一行(一个版本目标或"库内任一版本")的格状态:已入库 > 在途(含整季通配预留)> 范围外/未播出 > 缺失。
|
||||
* 一行(一个收录版本或"库内任一版本")的格状态:已入库 > 在途(含整季通配预留)> 范围外/未播出 > 缺失。
|
||||
* 在途按计划单元键与列键求交(unitsOverlap:s{季}* 通配与同季各集相交)。
|
||||
*/
|
||||
export function coverageRow(i: CoverageRowInput): CoverageRowResult {
|
||||
|
||||
@@ -116,7 +116,7 @@ export interface SlotUpgradePolicy {
|
||||
|
||||
export type SlotCompletionSpec = { kind: "movie"; cuts?: (Cut | "default")[] } | { kind: "units" };
|
||||
|
||||
/** 版本目标引用的规则组链(已按 scope 过滤、按引用顺序排列)与自定义条件 */
|
||||
/** 收录版本引用的规则组链(已按 scope 过滤、按引用顺序排列)与自定义条件 */
|
||||
export interface SlotRuleChain {
|
||||
groups: RuleGroupSpec[];
|
||||
atoms?: CustomAtom[];
|
||||
|
||||
@@ -125,7 +125,7 @@ describe("变量表与上下文", () => {
|
||||
"libraryRoot",
|
||||
]);
|
||||
});
|
||||
it("季集元数据、版本目标、库根、外部 ID", () => {
|
||||
it("季集元数据、收录版本、库根、外部 ID", () => {
|
||||
const v = buildNamingVars(
|
||||
{
|
||||
title: "绝命毒师",
|
||||
|
||||
@@ -22,7 +22,7 @@ const base = (groups: RuleGroupSpec[], extra: Partial<SlotSpec> = {}): SlotSpec
|
||||
});
|
||||
const rel = (t: string, subtitle?: string) => parseRelease(t, subtitle ? { subtitle } : {});
|
||||
|
||||
describe("版本目标接入规则组", () => {
|
||||
describe("收录版本接入规则组", () => {
|
||||
it("硬排除命中 → reject(rule.hard_exclude),trace 带组名与修订号", () => {
|
||||
const g = group("不要原盘", "", {
|
||||
id: "g1",
|
||||
|
||||
Reference in new issue
Block a user