diff --git a/apps/web/app/api/parse.ts b/apps/web/app/api/parse.ts index 850057f1..a5bc2ef6 100644 --- a/apps/web/app/api/parse.ts +++ b/apps/web/app/api/parse.ts @@ -47,6 +47,7 @@ export async function fetchAllCursorPages

(); let cursor = first.nextCursor; while (cursor) { + // copy-allow: 用词规则上线前已有,文案归其它路改 if (seen.has(cursor)) throw new ApiRequestError("internal", "分页游标重复,请重试"); seen.add(cursor); const page = await fetchPage(cursor); diff --git a/apps/web/app/components/files/directory-dialog.tsx b/apps/web/app/components/files/directory-dialog.tsx index 779269a7..73c91098 100644 --- a/apps/web/app/components/files/directory-dialog.tsx +++ b/apps/web/app/components/files/directory-dialog.tsx @@ -118,6 +118,7 @@ export function DirectoryDialog({

{!allowRoot && selected && !selected.path ? (

+ {/* copy-allow: 用词规则上线前已有,文案归其它路改 */} 请选择子目录,或先新增来源配置,再整理根目录存量。

) : null} diff --git a/apps/web/app/components/organize-wizard/model.ts b/apps/web/app/components/organize-wizard/model.ts index 3fb8ad10..83aa38dc 100644 --- a/apps/web/app/components/organize-wizard/model.ts +++ b/apps/web/app/components/organize-wizard/model.ts @@ -63,8 +63,10 @@ export function rowProblem(r: WizardRow): string | null { if (r.excluded) return null; if (!r.entity) return "未选择作品"; if (isMovieKind(r.entity.kind)) return null; + // copy-allow: 用词规则上线前已有,文案归其它路改 if (!r.episodeTouched) return episodeUnresolved(r) ? "集数待确认,请填写季集或排除" : null; const eps = parseEpisodes(r.episodes); + // copy-allow: 用词规则上线前已有,文案归其它路改 if (eps === null) return "集号格式不对,请填写单集或连续区间,如 3、3-5"; const unique = [...new Set(eps)].sort((a, b) => a - b); if (eps.length > 50) return "单个文件最多指定 50 个集号"; diff --git a/apps/web/app/components/organize-wizard/organize-wizard.tsx b/apps/web/app/components/organize-wizard/organize-wizard.tsx index 3f48a56e..1ec7f010 100644 --- a/apps/web/app/components/organize-wizard/organize-wizard.tsx +++ b/apps/web/app/components/organize-wizard/organize-wizard.tsx @@ -323,6 +323,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on { onSuccess: (r) => { if (signatureRef.current !== snapshot) { + // copy-allow: 用词规则上线前已有,文案归其它路改 setNotice("选项或识别结果已变化,请重新生成预览。"); return; } @@ -357,6 +358,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on const accepted = r.groups.filter((g) => !!g.recordId && !g.error).length; if (accepted) toast.success(`已加入整理队列:${accepted} 组`); if (failed.length) { + // copy-allow: 用词规则上线前已有,文案归其它路改 setNotice(`${accepted} 组已受理,${failed.length} 组未加入队列;请检查下方原因,修改后重新预览。`); const acceptedSources = new Set( r.groups.filter((g) => !!g.recordId && !g.error).flatMap((g) => g.files.map((f) => f.source)), @@ -376,6 +378,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on onError: (e) => { if (filesErrorReason(e) === "plan_stale") { setPreview(null); + // copy-allow: 用词规则上线前已有,文案归其它路改 setNotice("源文件已变化,请重新生成预览。"); } toast.error(filesErrorText(e, "加入队列失败")); @@ -492,6 +495,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on value={inPlace ? "none" : (profile?.id ?? (ambiguous ? "choose" : "none"))} onChange={pickProfile} choices={[ + // copy-allow: 用词规则上线前已有,文案归其它路改 ...(ambiguous ? [{ value: "choose", label: "请选择匹配的来源配置", disabled: true }] : []), { value: "none", label: inPlace ? "原地 · 使用文件所在媒体库" : "手动指定 · 使用全局默认" }, ...matches.map((p) => ({ @@ -784,7 +788,8 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on

{src.error ? filesErrorText(src.error, "读取来源失败") - : "源文件不存在或不在登记目录中,请在文件管理器重新选择。"} + : // copy-allow: 用词规则上线前已有,文案归其它路改 + "源文件不存在或不在登记目录中,请在文件管理器重新选择。"}

) : identify.error ? (
diff --git a/apps/web/app/components/organize/source-selection.tsx b/apps/web/app/components/organize/source-selection.tsx index 51757404..96e2b169 100644 --- a/apps/web/app/components/organize/source-selection.tsx +++ b/apps/web/app/components/organize/source-selection.tsx @@ -17,6 +17,7 @@ export function stockWizardSource(profile: OrganizeSourceView, stock: OrganizeSt sourceConfigId: profile.id, defaultAction: profile.action, label: `整理存量 · ${profile.name}`, + // copy-allow: 用词规则上线前已有,文案归其它路改 scanSummary: `找到 ${stock.items.length} 个文件,跳过 ${stock.skipped} 个,不可读取 ${stock.unavailable} 个${stock.truncated ? ";扫描已达到上限,仅显示本批结果" : ""}。本次扫描不改变自动整理基线,请先预览再执行。`, }; } diff --git a/scripts/check-copy-punctuation.mjs b/scripts/check-copy-punctuation.mjs index 94a8a5d7..572c4863 100644 --- a/scripts/check-copy-punctuation.mjs +++ b/scripts/check-copy-punctuation.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env -S deno run -A -// 文案标点检查(规范见 docs/design/glossary.md「标点」)。只看源码里的字符串字面量、模板字符串与 JSX 文本, +// 文案标点与用词检查(规范见 docs/design/glossary.md「标点」与 .claude/skills/human-copy)。只看源码里的字符串字面量、模板字符串与 JSX 文本, // 不看注释、类型字面量、对象键、比较/匹配用的字符串(includes / split / === 等)和像正则的字符串。 // // deno run -A scripts/check-copy-punctuation.mjs # 报告,warn 模式(退出码 0) @@ -45,9 +45,41 @@ function findIssues(text) { push("括号内有中文应用全角括号", new RegExp(`\\([^()]*[${CJK}][^()]*\\)`, "g")); push("引号应为「」", /[“”]/g); if (termScope) push("处理状态写「整理」不写「入库」(见 glossary)", TERM_RUKU); + for (const m of text.matchAll(BANNED_WORDS)) out.push({ rule: `用词「${m[0]}」(见 human-copy)`, index: m.index }); return out; } +// 用词:human-copy 规范里的客服腔、营销腔、AI 套话与「进行 + 动词」。只收误报低的写法: +// 「请」只拦祈使搭配(请先 / 请检查…),不碰「请求」「申请」;「一键」排除「同一键」;「进行」只拦后接动作名词。 +const BANNED_WORDS = new RegExp( + [ + "您", + "温馨提示", + "非常抱歉", + "敬请谅解", + "不便之处", + "(? 200) console.log(`…另有 ${total - 200} 处未列出`) if (fix) console.log(`已修改 ${changedFiles} 个文件、${editCount} 处字面量`); console.log( total - ? `${strict ? "✗" : "⚠"} 文案标点:${total} 处(${[...byRule].map(([k, v]) => `${k} ${v}`).join(";")})` - : "✓ 文案标点:0 处", + ? `${strict ? "✗" : "⚠"} 文案标点与用词:${total} 处(${[...byRule].map(([k, v]) => `${k} ${v}`).join(";")})` + : "✓ 文案标点与用词:0 处", ); if (strict && total) process.exit(1);