refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries
This commit is contained in:
1 parent
3f1c6d02f6
commit
27c0f27434
268 files changed
+7468
-7852
No files matched your search
@@ -76,6 +76,37 @@ export function tmdbImageUrl(path: string | null | undefined, size: TmdbImageSiz
|
||||
return `/api/v1/img/tmdb/${size}/${p}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部图片代理允许的域名:豆瓣图床(img1–9.doubanio.com 等)与 Bangumi 图床。豆瓣图床对不带 Referer 的请求回 418,
|
||||
* 浏览器 no-referrer 直连拿不到图,所以经服务端带上来源站的 Referer 取(MP SECURITY_IMAGE_DOMAINS 同此)。
|
||||
*/
|
||||
export const EXTERNAL_IMAGE_HOSTS: readonly { suffix: string; referer: string }[] = [
|
||||
{ suffix: ".doubanio.com", referer: "https://movie.douban.com/" },
|
||||
{ suffix: "lain.bgm.tv", referer: "https://bgm.tv/" },
|
||||
];
|
||||
|
||||
/** 外部图片地址是否在代理白名单里(只认 https、图片扩展名) */
|
||||
export function externalImageReferer(url: string): string | null {
|
||||
let u: URL;
|
||||
try {
|
||||
u = new URL(url);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (u.protocol !== "https:" || !/\.(jpe?g|png|webp)$/i.test(u.pathname)) return null;
|
||||
const host = u.hostname.toLowerCase();
|
||||
const hit = EXTERNAL_IMAGE_HOSTS.find((h) =>
|
||||
h.suffix.startsWith(".") ? host.endsWith(h.suffix) : host === h.suffix,
|
||||
);
|
||||
return hit?.referer ?? null;
|
||||
}
|
||||
|
||||
/** 豆瓣 / Bangumi 封面 → 本站图片代理 /api/v1/img/ext?u=…;不在白名单的原样返回 */
|
||||
export function externalImageUrl(url: string | null | undefined): string | null {
|
||||
if (!url) return null;
|
||||
return externalImageReferer(url) ? `/api/v1/img/ext?u=${encodeURIComponent(url)}` : url;
|
||||
}
|
||||
|
||||
// ── 通用缓存(kv_cache)按 region 管理(设置 → 缓存) ──
|
||||
|
||||
/** region 名:小写字母、数字、`.`、`_`、`-` */
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
SiteKind,
|
||||
SiteLastHealth,
|
||||
SiteLoginResult,
|
||||
SiteReleasePage,
|
||||
SiteUserDataPoint,
|
||||
SourceRelease,
|
||||
} from "./sources.ts";
|
||||
@@ -116,7 +117,31 @@ export const ReleaseSearchOutput = z.object({
|
||||
});
|
||||
|
||||
export const ReleaseInspectInput = z.object({ sourceReleaseId: Id, refreshDetail: z.boolean().default(false) });
|
||||
export const ReleaseInspectOutput = z.object({ release: SourceRelease, inventory: z.array(MediaAsset) });
|
||||
export const ReleaseInspectOutput = z.object({
|
||||
release: SourceRelease,
|
||||
inventory: z.array(MediaAsset),
|
||||
page: SiteReleasePage.nullable().optional(),
|
||||
refreshError: z.string().nullable().optional(),
|
||||
});
|
||||
export const ReleaseStaticFacts = z
|
||||
.object({
|
||||
resolution: z.enum(["4320p", "2160p", "1440p", "1080p", "1080i", "720p", "576p", "480p"]).optional(),
|
||||
videoCodec: z.enum(["H.265", "H.264", "AV1", "VC-1", "MPEG-2", "XviD", "DivX"]).optional(),
|
||||
source: z.enum(["REMUX", "BluRay-Disc", "BluRay", "WEB-DL", "WEBRip", "HDTV", "HDRip", "DVDRip", "DVD"]).optional(),
|
||||
releaseGroup: z.string().trim().min(1).max(200).optional(),
|
||||
})
|
||||
.strict();
|
||||
export type ReleaseStaticFacts = z.infer<typeof ReleaseStaticFacts>;
|
||||
export const ReleaseApplyFactsInput = z
|
||||
.object({
|
||||
decisionId: Id,
|
||||
sourceReleaseId: Id,
|
||||
facts: ReleaseStaticFacts,
|
||||
evidence: z.string().trim().min(10).max(2000),
|
||||
evidenceUrl: z.url().optional(),
|
||||
})
|
||||
.strict();
|
||||
export const ReleaseApplyFactsOutput = z.object({ resolved: z.boolean(), remaining: z.array(z.string()) });
|
||||
|
||||
export const IdentityResolveInput = z
|
||||
.object({
|
||||
@@ -1136,6 +1161,7 @@ export const CAPABILITY_IO = {
|
||||
"catalog.lookup": { input: CatalogLookupInput, output: CatalogLookupOutput },
|
||||
"release.search": { input: ReleaseSearchInput, output: ReleaseSearchOutput },
|
||||
"release.inspect": { input: ReleaseInspectInput, output: ReleaseInspectOutput },
|
||||
"release.apply_facts": { input: ReleaseApplyFactsInput, output: ReleaseApplyFactsOutput },
|
||||
"identity.resolve": { input: IdentityResolveInput, output: IdentityResolveOutput },
|
||||
"download.plan": { input: DownloadPlanInput, output: DownloadPlanOutput },
|
||||
"download.submit": { input: DownloadSubmitInput, output: DownloadSubmitOutput },
|
||||
|
||||
@@ -57,6 +57,7 @@ export const CAPABILITY_IDS = [
|
||||
"catalog.lookup",
|
||||
"release.search",
|
||||
"release.inspect",
|
||||
"release.apply_facts",
|
||||
"identity.resolve",
|
||||
"download.plan",
|
||||
"download.submit",
|
||||
@@ -185,6 +186,18 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
||||
true,
|
||||
true,
|
||||
),
|
||||
"release.apply_facts": d(
|
||||
"release.apply_facts",
|
||||
"补全资源属性",
|
||||
"查证资源详情后,仅补全事项里缺失的分辨率、编码、来源或发布组;规则重新判断收录。不改变已有事实和策略。",
|
||||
"execute",
|
||||
"ReleaseApplyFacts",
|
||||
"organize.execute",
|
||||
"medium",
|
||||
true,
|
||||
true,
|
||||
"safe",
|
||||
),
|
||||
"download.plan": d(
|
||||
"download.plan",
|
||||
"准备下载",
|
||||
@@ -704,6 +717,7 @@ export const CAPABILITY_ACTIVITY: Record<KnownCapabilityId, CapabilityActivity>
|
||||
"catalog.lookup": act("查", "作品信息"),
|
||||
"release.search": act("搜", "资源", "次"),
|
||||
"release.inspect": act("看", "资源", "个"),
|
||||
"release.apply_facts": act("补全", "资源属性", "个"),
|
||||
"identity.resolve": act("识别", "作品", "个"),
|
||||
"download.plan": { ...act("准备下载", "资源", "个"), bare: true },
|
||||
"download.submit": act("提交", "下载", "个"),
|
||||
|
||||
@@ -184,6 +184,7 @@ export const ReleaseResolvedPayload = z.object({
|
||||
});
|
||||
|
||||
export const DecisionCreatedPayload = z.object({
|
||||
dedupeKey: z.string().optional(),
|
||||
decisionId: Id,
|
||||
kind: DecisionKind,
|
||||
severity: Severity,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { KnownCapabilityId } from "./capability.ts";
|
||||
import type { EventType } from "./events.ts";
|
||||
import type { HookPoint } from "./hooks.ts";
|
||||
|
||||
// 功能模块约定(ADR-006):外围功能各自声明一份 manifest,汇总成功能清单。
|
||||
// 功能清单只给智能体看(系统提示的「功能清单」一节),不做开关;前端插槽默认直接显示。
|
||||
@@ -31,8 +30,6 @@ export interface FeatureManifest {
|
||||
pages: readonly string[];
|
||||
/** 挂在哪些前端插槽 */
|
||||
slots: readonly SlotPosition[];
|
||||
/** 注册的链式钩子 */
|
||||
hooks: readonly HookPoint[];
|
||||
}
|
||||
|
||||
/** 功能清单。新增外围功能:在这里加一份,服务端模块 manifest 指向它(featureManifest(id)) */
|
||||
@@ -47,7 +44,6 @@ export const FEATURES = [
|
||||
capabilities: [],
|
||||
pages: ["作品页「补字幕」", "整理记录行菜单「补字幕」", "站点页「字幕站」"],
|
||||
slots: ["entity.actions", "organize.record.actions"],
|
||||
hooks: [],
|
||||
},
|
||||
{
|
||||
id: "browser",
|
||||
@@ -64,7 +60,6 @@ export const FEATURES = [
|
||||
],
|
||||
pages: ["站点页「原站视图」"],
|
||||
slots: [],
|
||||
hooks: [],
|
||||
},
|
||||
{
|
||||
id: "desktop",
|
||||
@@ -75,7 +70,6 @@ export const FEATURES = [
|
||||
capabilities: [],
|
||||
pages: ["原站视图「桌面模式」"],
|
||||
slots: [],
|
||||
hooks: [],
|
||||
},
|
||||
{
|
||||
id: "websearch",
|
||||
@@ -86,7 +80,6 @@ export const FEATURES = [
|
||||
capabilities: ["web.search", "web.fetch"],
|
||||
pages: ["设置 → 智能体 → 网页搜索"],
|
||||
slots: [],
|
||||
hooks: [],
|
||||
},
|
||||
{
|
||||
id: "mcp",
|
||||
@@ -97,7 +90,6 @@ export const FEATURES = [
|
||||
capabilities: [],
|
||||
pages: ["设置 → 智能体 → 工具与 MCP"],
|
||||
slots: [],
|
||||
hooks: [],
|
||||
},
|
||||
{
|
||||
id: "schedules",
|
||||
@@ -108,7 +100,6 @@ export const FEATURES = [
|
||||
capabilities: ["schedule.draft_check"],
|
||||
pages: ["日志 → 定时任务"],
|
||||
slots: [],
|
||||
hooks: [],
|
||||
},
|
||||
] as const satisfies readonly FeatureManifest[];
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// 链式钩子挂点(ADR-006,参考 MoviePilot ChainEventType):同步、按注册顺序执行、可改写或拦截。
|
||||
// 这里只登记挂点名称与语义(给功能清单、智能体提示与文档用);入参 / 返回值类型在服务端
|
||||
// apps/server/src/hooks/points.ts,因为它们用到 domain 的解析结果等类型。
|
||||
|
||||
/**
|
||||
* - rewrite:每个钩子拿到上一个钩子的结果,返回新值或 undefined(不改);
|
||||
* - intercept:任一钩子返回拒绝原因即停止,后面的钩子不再执行。
|
||||
*/
|
||||
export type HookKind = "rewrite" | "intercept";
|
||||
|
||||
export const HOOK_POINTS = {
|
||||
"name.recognize": {
|
||||
kind: "rewrite",
|
||||
label: "名称识别",
|
||||
when: "资源标题解析之后、按名称召回作品之前",
|
||||
effect: "改写解析结果(标题、年份、季集、外部 ID)",
|
||||
},
|
||||
"organize.rename": {
|
||||
kind: "rewrite",
|
||||
label: "整理改名",
|
||||
when: "按命名模板生成目标路径之后",
|
||||
effect: "改写主视频的目标文件名(同主干的字幕等附属文件跟着改)",
|
||||
},
|
||||
"organize.intercept": {
|
||||
kind: "intercept",
|
||||
label: "整理拦截",
|
||||
when: "整理计划生成后、写整理记录之前",
|
||||
effect: "返回原因即拒绝本次整理(记录为失败,原因 intercepted:<原因>)",
|
||||
},
|
||||
"resource.selection": {
|
||||
kind: "rewrite",
|
||||
label: "资源选择",
|
||||
when: "订阅 / 收录版本重评时候选排好序之后、逐条评估之前",
|
||||
effect: "调整候选顺序或过滤掉一部分(不能加入新候选)",
|
||||
},
|
||||
"subscription.completion.check": {
|
||||
kind: "intercept",
|
||||
label: "订阅完成判定",
|
||||
when: "订阅满足完成条件、标记完成之前",
|
||||
effect: "追加完成条件:返回原因即本次不完成,订阅继续追踪",
|
||||
},
|
||||
} as const satisfies Record<string, { kind: HookKind; label: string; when: string; effect: string }>;
|
||||
|
||||
export type HookPoint = keyof typeof HOOK_POINTS;
|
||||
@@ -15,7 +15,6 @@ export * from "./episode-mapping.ts";
|
||||
export * from "./events.ts";
|
||||
export * from "./execution.ts";
|
||||
export * from "./features.ts";
|
||||
export * from "./hooks.ts";
|
||||
export * from "./identify-words.ts";
|
||||
export * from "./identity.ts";
|
||||
export * from "./instances.ts";
|
||||
|
||||
@@ -85,7 +85,7 @@ export const Subscription = EntityBase.extend({
|
||||
mediaEntityTitle: z.string().nullable().optional(),
|
||||
});
|
||||
export type Subscription = z.infer<typeof Subscription>;
|
||||
/** 自动补全其他版本:inherit = 取引用规则组(后者优先)的 autoFill,都没设为开 */
|
||||
/** 自动补全其他版本:inherit = 取主版本收录规则链(后者优先)的 autoFill,都没设为关 */
|
||||
export const AutoFillMode = z.enum(["inherit", "on", "off"]);
|
||||
export type AutoFillMode = z.infer<typeof AutoFillMode>;
|
||||
/** 补全预算:只约束非主版本的自动补全计划(按订阅、按自然日);同时仍计入全局每日预算 */
|
||||
@@ -121,7 +121,8 @@ export type SubscriptionEffect = z.infer<typeof SubscriptionEffect>;
|
||||
* siteIds 站点范围;episodeRange 集范围(对照 MP start/total_episode);searchIntervalHours 主动搜索周期(0 = 关);
|
||||
* stopAfter 截止时间;downloaderInstanceId 指定下载器;ruleGroupIds 规则组;keywords 追加检索词;
|
||||
* include/exclude 订阅级关键词正则(作用于全部收录版本);libraryRootId 库根;downloadSavePath 下载保存路径;
|
||||
* autoFill/fillBudget 多版本自动补全;resolution/source/effect 质量预设(MP resolution/quality/effect)。
|
||||
* autoFill/fillBudget 多版本自动补全;resolution/source/effect 仅保留 MP 导入/API 质量预设兼容。
|
||||
* 正常界面逐版本维护质量和规则引用,普通订阅保存保留旧兼容字段。
|
||||
* include/exclude 保存时同步为订阅私有规则组(scope.subscriptionId,随订阅删除),评估只读规则组。
|
||||
* 写入时兼容 MP 字段与旧键(sites/downloader/save_path/start_episode/filter_groups/keyword/autoFillVersions…),
|
||||
* 服务端规范化后只存本表列出的键,见 docs/runbook/subscriptions.md。
|
||||
@@ -714,6 +715,17 @@ export type PreferenceSuppressionUpdate = z.infer<typeof PreferenceSuppressionUp
|
||||
|
||||
// ── 收录策略规则与候选评估(W2-C) ──
|
||||
|
||||
/** 分类名称沿用整理分类;显式选择策略时不再按分类过滤。 */
|
||||
export const AcquisitionPolicyScope = z
|
||||
.object({
|
||||
mediaKinds: z.array(SubscriptionMediaKind).optional(),
|
||||
categories: z
|
||||
.object({ movie: z.array(z.string().min(1)).optional(), tv: z.array(z.string().min(1)).optional() })
|
||||
.optional(),
|
||||
})
|
||||
.passthrough();
|
||||
export type AcquisitionPolicyScope = z.infer<typeof AcquisitionPolicyScope>;
|
||||
|
||||
/** Slot 模板:策略规则里用于自动建 Slot(新作品收录 / 订阅缺省 Slot);字段同 AcquisitionSlot 规则列 */
|
||||
export const SlotTemplate = z.object({
|
||||
name: z.string().min(1).max(64),
|
||||
|
||||
Reference in new issue
Block a user