docs(plan): v0.2 adds Pi subagents and scheduled-jobs board
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
b839ed4056
commit
16ab2a6c84
97 files changed
+592
-648
No files matched your search
@@ -51,7 +51,7 @@ const KIND_OPTS: { value: string; label: string }[] = SubscriptionMediaKind.opti
|
||||
}));
|
||||
|
||||
export const PUBLISH_NOTICE =
|
||||
"发布即授权:scope 范围内的正常下载、追更与整理将按此策略持续自动执行,不再逐次审批;升级替换、媒体清理与下载缓存清理另行授权。旧的已发布版本会被替代,其授权随之吊销。";
|
||||
"发布后,作用范围内的下载、追更和整理按此策略自动执行,不再逐次审批。升级替换、媒体清理和下载缓存清理仍需单独审批。旧的已发布版本被替代,授权随之失效。";
|
||||
|
||||
type TemplateDraft = { key: string; name: string; rules: SlotRulesDraft; open: boolean };
|
||||
type PolicyDraft = {
|
||||
@@ -98,10 +98,10 @@ const Meta = z.object({
|
||||
policyKey: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "请填写策略键")
|
||||
.min(1, "填写策略键")
|
||||
.max(64, "最多 64 个字符")
|
||||
.regex(/^[a-z0-9][a-z0-9_.-]*$/, "只用小写字母、数字和 - _ ."),
|
||||
title: z.string().trim().min(1, "请填写标题").max(120, "最多 120 个字符"),
|
||||
title: z.string().trim().min(1, "填写标题").max(120, "最多 120 个字符"),
|
||||
summary: z.string().trim().max(500, "最多 500 个字符"),
|
||||
});
|
||||
|
||||
@@ -119,7 +119,7 @@ function buildPolicy(d: PolicyDraft):
|
||||
if (!meta.success) for (const i of meta.error.issues) errors[String(i.path[0])] ??= i.message;
|
||||
const templates = (list: TemplateDraft[]) =>
|
||||
list.map((t) => {
|
||||
if (!t.name.trim()) errors[`${t.key}.name`] = "请填写模板名称";
|
||||
if (!t.name.trim()) errors[`${t.key}.name`] = "填写模板名称";
|
||||
const r = draftToJson(t.rules);
|
||||
if (!r.ok) {
|
||||
for (const [k, v] of Object.entries(r.errors)) errors[`${t.key}.${k}`] = v;
|
||||
@@ -141,7 +141,7 @@ function buildPolicy(d: PolicyDraft):
|
||||
windowDays = Number(d.windowDays);
|
||||
if (!Number.isInteger(windowDays) || windowDays < 1 || windowDays > 3650) errors.windowDays = "1–3650 的整数";
|
||||
}
|
||||
if (d.newTitleEnabled && newTitleSlots.length === 0) errors.newTitleSlots = "开启新作品收录时至少要有一个目标模板";
|
||||
if (d.newTitleEnabled && newTitleSlots.length === 0) errors.newTitleSlots = "开启新作品收录时至少需要一个模板";
|
||||
const rules = {
|
||||
...(defaultSlots.length ? { defaultSlots } : {}),
|
||||
...(d.newTitleEnabled || newTitleSlots.length
|
||||
@@ -208,9 +208,7 @@ export function PolicyWorkspace({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-caption text-muted-foreground">
|
||||
策略只保存结构化条件。草案可以反复预览,发布才生效;回滚会以旧版本内容发布一个新版本,历史只增不改。
|
||||
</p>
|
||||
<p className="text-caption text-muted-foreground">草案发布后才生效。回滚会用旧版本内容发布一个新版本。</p>
|
||||
{q.isPending ? (
|
||||
<div className="pg-slab rounded-xl p-4">
|
||||
<LoadingState rows={4} />
|
||||
@@ -221,7 +219,7 @@ export function PolicyWorkspace({
|
||||
<EmptyState
|
||||
icon={ScrollText}
|
||||
title="还没有收录策略"
|
||||
description="新建一份草案,设置缺省目标模板与新作品收录条件,预览后再发布。"
|
||||
description="新建草案,预览后发布。"
|
||||
action={
|
||||
<Button onClick={() => onCreatingChange(true)}>
|
||||
<FilePlus2 />
|
||||
@@ -297,7 +295,7 @@ function PolicyGroup({
|
||||
description={
|
||||
published
|
||||
? `当前生效 r${published.revision} · ${formatDateTime(published.publishedAt)} 由 ${published.publishedBy ?? "—"} 发布`
|
||||
: "尚未发布,不产生任何自动操作"
|
||||
: "未发布,不会自动执行"
|
||||
}
|
||||
actions={
|
||||
draft ? (
|
||||
@@ -348,13 +346,13 @@ function PolicyGroup({
|
||||
}
|
||||
|
||||
function RulesSummary({ rules, valid }: { rules: AcquisitionPolicyRules | undefined; valid: boolean }) {
|
||||
if (!valid || !rules) return <p className="text-caption text-destructive">规则结构无法解析(不会被执行)</p>;
|
||||
if (!valid || !rules) return <p className="text-caption text-destructive">规则无法解析,不会执行</p>;
|
||||
const tplLine = (t: SlotTemplate) => `${t.name}:${summarizeHard(t.hardConditions, t.fallback).join(",") || "不限"}`;
|
||||
return (
|
||||
<div className="space-y-2 text-caption">
|
||||
<div>
|
||||
<span className="text-muted-foreground">缺省目标 </span>
|
||||
<ChipList items={(rules.defaultSlots ?? []).map(tplLine)} empty="无(订阅没有目标时不自动建)" />
|
||||
<span className="text-muted-foreground">默认版本目标 </span>
|
||||
<ChipList items={(rules.defaultSlots ?? []).map(tplLine)} empty="无(订阅没有版本目标时不自动创建)" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">新作品收录 </span>
|
||||
@@ -375,9 +373,9 @@ function RulesSummary({ rules, valid }: { rules: AcquisitionPolicyRules | undefi
|
||||
<div className="text-muted-foreground">
|
||||
整理方式{" "}
|
||||
<span className="text-foreground">
|
||||
{rules.organizeMode === "move" ? "Move" : rules.organizeMode === "copy" ? "Copy" : "按系统设置"}
|
||||
{rules.organizeMode === "move" ? "移动" : rules.organizeMode === "copy" ? "复制" : "按系统设置"}
|
||||
</span>
|
||||
{rules.allowCopyWhenHnrPending ? " · H&R 未满足时改用 Copy" : ""}
|
||||
{rules.allowCopyWhenHnrPending ? " · H&R 未满足时改用复制" : ""}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -436,11 +434,11 @@ function PolicyDetailSheet({
|
||||
}
|
||||
title={`发布 ${rev.policyKey} r${rev.revision}?`}
|
||||
description={PUBLISH_NOTICE}
|
||||
confirmLabel="授权并发布"
|
||||
confirmLabel="发布"
|
||||
onConfirm={() =>
|
||||
publish
|
||||
.mutateAsync({ id: rev.id, expectedRevision: rev.revision })
|
||||
.then(() => toast.success(`r${rev.revision} 已发布并生效`))
|
||||
.then(() => toast.success(`r${rev.revision} 已发布`))
|
||||
.catch((e) => {
|
||||
toast.error(errorMessage(e));
|
||||
throw e;
|
||||
@@ -449,7 +447,7 @@ function PolicyDetailSheet({
|
||||
>
|
||||
{!pv?.success ? (
|
||||
<p className="rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-caption text-warning">
|
||||
这个草案还没有预览结果,建议先预览确认影响范围。
|
||||
这个草案还没有预览。先预览可以看到影响范围。
|
||||
</p>
|
||||
) : null}
|
||||
</ConfirmDialog>
|
||||
@@ -469,12 +467,12 @@ function PolicyDetailSheet({
|
||||
</Button>
|
||||
}
|
||||
title={`回滚到 r${rev.revision}?`}
|
||||
description={`将以 r${rev.revision} 的内容发布一个新版本,当前已发布版本标记为已回滚。${PUBLISH_NOTICE}`}
|
||||
confirmLabel="回滚并发布"
|
||||
description={`用 r${rev.revision} 的内容发布一个新版本,当前版本标记为已回滚。${PUBLISH_NOTICE}`}
|
||||
confirmLabel="回滚"
|
||||
onConfirm={() =>
|
||||
rollback
|
||||
.mutateAsync(rev.id)
|
||||
.then((n) => toast.success(`已回滚,新版本 r${n.revision} 生效`))
|
||||
.then((n) => toast.success(`已回滚,r${n.revision} 生效`))
|
||||
.then(onClose)
|
||||
.catch((e) => {
|
||||
toast.error(errorMessage(e));
|
||||
@@ -536,7 +534,7 @@ function PreviewBlock({ value }: { value: JsonObject | null }) {
|
||||
if (!value)
|
||||
return (
|
||||
<p className="text-caption text-muted-foreground">
|
||||
还没有预览。预览用最近已识别的发布评估模板,不产生任何副作用。
|
||||
还没有预览。预览用最近已识别的资源评估模板,不会执行任何操作。
|
||||
</p>
|
||||
);
|
||||
const p = PolicyPreview.safeParse(value);
|
||||
@@ -546,12 +544,12 @@ function PreviewBlock({ value }: { value: JsonObject | null }) {
|
||||
<div className="space-y-3">
|
||||
{!v.valid ? (
|
||||
<p className="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-caption text-destructive">
|
||||
规则结构未通过校验,预览按空模板计算;发布会被拒绝。
|
||||
规则未通过校验,预览按空模板计算,无法发布。
|
||||
</p>
|
||||
) : null}
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
{[
|
||||
["样本发布", v.sampleSize],
|
||||
["样本资源", v.sampleSize],
|
||||
["模板数", v.templates],
|
||||
["会接受", v.matched],
|
||||
["会排除", v.excluded],
|
||||
@@ -563,7 +561,7 @@ function PreviewBlock({ value }: { value: JsonObject | null }) {
|
||||
))}
|
||||
</div>
|
||||
<p className="text-meta text-muted-foreground">
|
||||
计算于 {formatDateTime(v.evaluatedAt)};接受/排除按"发布 × 模板"计数。
|
||||
计算于 {formatDateTime(v.evaluatedAt)};接受/排除按"资源 × 模板"计数。
|
||||
</p>
|
||||
{v.samples.length ? (
|
||||
<ul className="divide-y divide-border/60 rounded-lg border border-border/70">
|
||||
@@ -584,7 +582,7 @@ function PreviewBlock({ value }: { value: JsonObject | null }) {
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-caption text-muted-foreground">没有可用于回放的已识别发布。</p>
|
||||
<p className="text-caption text-muted-foreground">没有可用于回放的已识别资源。</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -632,12 +630,12 @@ function PolicyEditorSheet({
|
||||
open: t.open || Object.keys(r.errors).some((k) => k.startsWith(`${t.key}.`)),
|
||||
})),
|
||||
}));
|
||||
toast.error("请先修正表单中的错误");
|
||||
toast.error("表单有错误");
|
||||
return;
|
||||
}
|
||||
setErrors({});
|
||||
const done = (rev: PolicyRevision) => {
|
||||
toast.success(`草案 r${rev.revision} 已保存,预览确认后再发布`);
|
||||
toast.success(`草案 r${rev.revision} 已保存`);
|
||||
onSaved(rev);
|
||||
};
|
||||
if (editId) {
|
||||
@@ -657,7 +655,7 @@ function PolicyEditorSheet({
|
||||
onOpenChange={(o) => !o && !pending && onClose()}
|
||||
width="lg"
|
||||
title={editId ? `编辑草案 r${base?.revision}` : base ? `基于 r${base.revision} 新建草案` : "新建收录策略草案"}
|
||||
description="保存只生成草案,不会生效;发布时才授权自动执行。"
|
||||
description="保存为草案,发布后才生效。"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" onClick={onClose} disabled={pending}>
|
||||
@@ -676,7 +674,7 @@ function PolicyEditorSheet({
|
||||
htmlFor={`${id}-key`}
|
||||
required
|
||||
error={errors.policyKey}
|
||||
hint="同一键下版本递增,只有一个生效"
|
||||
hint="同一键下只有一个版本生效"
|
||||
>
|
||||
<Input
|
||||
id={`${id}-key`}
|
||||
@@ -696,12 +694,7 @@ function PolicyEditorSheet({
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField
|
||||
label="本版说明"
|
||||
htmlFor={`${id}-sum`}
|
||||
error={errors.summary}
|
||||
hint="写清这一版改了什么,出现在版本历史里"
|
||||
>
|
||||
<FormField label="本版说明" htmlFor={`${id}-sum`} error={errors.summary} hint="这一版改了什么,显示在版本历史里">
|
||||
<Textarea id={`${id}-sum`} rows={2} value={d.summary} onChange={(e) => set("summary", e.target.value)} />
|
||||
</FormField>
|
||||
<FormField label="作用范围" hint="不选即全部作品类型">
|
||||
@@ -715,7 +708,7 @@ function PolicyEditorSheet({
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection
|
||||
title="缺省目标模板"
|
||||
title="默认版本目标模板"
|
||||
actions={
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -727,7 +720,7 @@ function PolicyEditorSheet({
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<p className="mb-2 text-meta text-muted-foreground">订阅对应的作品还没有目标时,按这些模板建 Slot。</p>
|
||||
<p className="mb-2 text-meta text-muted-foreground">订阅的作品没有版本目标时,按这些模板创建。</p>
|
||||
<TemplateList list={d.defaultSlots} onChange={(v) => set("defaultSlots", v)} errors={errors} />
|
||||
</DetailSection>
|
||||
|
||||
@@ -736,7 +729,7 @@ function PolicyEditorSheet({
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-caption font-medium text-foreground">开启新作品收录</p>
|
||||
<p className="text-meta text-muted-foreground">按发行/首播日期判断,站点今天上传的旧片不算新作品</p>
|
||||
<p className="text-meta text-muted-foreground">按发行/首播日期判断,新上传的旧片不算</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={d.newTitleEnabled}
|
||||
@@ -765,7 +758,7 @@ function PolicyEditorSheet({
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-meta text-muted-foreground">新作品的目标模板</p>
|
||||
<p className="text-meta text-muted-foreground">新作品的版本目标模板</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
@@ -792,17 +785,17 @@ function PolicyEditorSheet({
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem value="__default">按系统设置</SelectItem>
|
||||
<SelectItem value="copy">Copy(保留源文件做种)</SelectItem>
|
||||
<SelectItem value="move">Move</SelectItem>
|
||||
<SelectItem value="copy">复制(保留源文件做种)</SelectItem>
|
||||
<SelectItem value="move">移动</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormField>
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2">
|
||||
<p className="min-w-0 text-caption font-medium text-foreground">H&R 未满足时经授权改用 Copy</p>
|
||||
<p className="min-w-0 text-caption font-medium text-foreground">H&R 未满足时经审批改用复制</p>
|
||||
<Switch
|
||||
checked={d.allowCopyWhenHnrPending}
|
||||
onCheckedChange={(v) => set("allowCopyWhenHnrPending", v)}
|
||||
aria-label="H&R 未满足时改用 Copy"
|
||||
aria-label="H&R 未满足时改用复制"
|
||||
/>
|
||||
</div>
|
||||
{errors.rules ? <p className="text-meta text-destructive">{errors.rules}</p> : null}
|
||||
|
||||
Reference in new issue
Block a user