feat(identity): open identity_unresolved decisions for resources with no candidates

no_match resources (and lookup failures after auto-review gives up) now
open a decision in the inbox with options to set a TMDB ID, pick a local
work, or dismiss; ambiguous decisions get the same manual options.
Resolve accepts identity {tmdb | mediaEntityId} and assigns with a manual
lock. The inbox option dialog collects the TMDB type/ID or searches local
works; the identity pool links each pending item to its inbox filter.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 13:44:57 +08:00
1 parent a0b0a64246
commit fb7727cf66
14 files changed
+280 -37

No files matched your search

+20
View File
@@ -142,6 +142,8 @@ export const DecisionKind = z.enum([
"metadata_conflict",
"source_unreachable",
"other",
/** 待识别资源没有可选候选(no_match / 复查耗尽的查询失败):手动指定 TMDB id、选本地作品或忽略 */
"identity_unresolved",
]);
export type DecisionKind = z.infer<typeof DecisionKind>;
export const DecisionStatus = z.enum(["open", "investigating", "awaiting_user", "resolved", "dismissed", "expired"]);
@@ -220,6 +222,24 @@ export const DecisionResolveInput = z.object({
optionKey: z.string().min(1).optional(),
dismiss: z.boolean().optional(),
note: z.string().max(2000).optional(),
/**
* 身份类事项的手动指定(选项 input.requires 标明需要哪一项):
* requires=tmdb → tmdb {type, id};requires=mediaEntityId → mediaEntityId
*/
identity: z
.object({
mediaEntityId: Id.optional(),
tmdb: z
.object({
type: z.enum(["movie", "tv"]),
id: z
.string()
.trim()
.regex(/^\d{1,10}$/),
})
.optional(),
})
.optional(),
});
export type DecisionResolveInput = z.infer<typeof DecisionResolveInput>;
@@ -0,0 +1,33 @@
import type { Kysely } from "kysely";
import { execDdl, oneOf } from "../src/migration-helpers.ts";
// W8-A 待识别资源没有匹配候选(no_match / 复查耗尽的查询失败)也进待处理中心:decision.kind 增加 identity_unresolved,
// 处理选项为手动指定 TMDB id / 选本地作品 / 忽略。
const OLD_KINDS = [
"identity_ambiguous",
"download_stalled",
"download_failed",
"path_conflict",
"space_insufficient",
"approval_required",
"metadata_conflict",
"source_unreachable",
"other",
];
const NEW_KINDS = [...OLD_KINDS, "identity_unresolved"];
const swap = (values: readonly string[]) => `
alter table decision drop constraint decision_kind_check;
alter table decision add constraint decision_kind_check ${oneOf("kind", values)};
`;
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
export async function up(db: Kysely<any>): Promise<void> {
await execDdl(db, swap(NEW_KINDS));
}
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function down(db: Kysely<any>): Promise<void> {
await execDdl(db, `update decision set kind = 'other' where kind = 'identity_unresolved';\n${swap(OLD_KINDS)}`);
}
+2 -1
View File
@@ -104,7 +104,8 @@ export type DecisionKind =
| "approval_required"
| "metadata_conflict"
| "source_unreachable"
| "other";
| "other"
| "identity_unresolved";
export type DecisionStatus = "open" | "investigating" | "awaiting_user" | "resolved" | "dismissed" | "expired";
export type DecisionSeverity = "info" | "warning" | "critical";
export type DownloadTaskOrigin = "submitted" | "adopted";