Files
writer-work-flow/apps/web/lib/review/locate.ts
Yaojia Wang 29265fa0fd feat(web): 审稿改稿闭环——一键采纳/AI改写/正文高亮定位编辑/伏笔确认/草稿自动保存/章节目录
- 冲突「采纳改法」:有补丁瞬时改入终稿并选中;无补丁但可定位则采纳时调 AI(refiner)重写该段套入;定位不到提示手改
- 正文改为行内高亮叠层编辑器(与页面同底色、随内容增高、无滚动条),点冲突卡/锚点整页滚到并选中+闪烁
- 仅对可在正文定位的冲突显示「跳转」/锚点;locate 支持从 where 引号抽原文,杜绝定位失败刷屏
- 伏笔建议卡:新埋一键登记(预填表单)/ 疑似回收一键标记 CLOSED(复用现有端点)
- 审稿页草稿自动保存(防抖 PUT /draft),与「验收本章」解耦,刷新不丢
- 写作路由带章号 ?chapter=N + 验收成功面板/大纲页「写下一章」入口;写作目录从大纲列出全部章节

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:28:29 +02:00

67 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 在终稿里定位某条冲突对应的正文区间(供编辑器 setSelectionRange 高亮)。
// 纯逻辑(可单测,不依赖 DOM。定位优先级精确 original → where 引号内原文 → 段级 snippet。
import type { ReviewConflict } from "./sse";
import { conflictSnippet } from "./snippet";
export interface DraftRange {
start: number;
end: number;
}
// 抽取一段文本里所有成对引号内的内容(「」/“”// 直双引号 / 直单引号)。
// where 多形如 草稿中写"<草稿原文>" —— 引号里就是正文精确子串,是最可靠的定位锚。
// 返回去重后按长度降序(长串更具体、误命中概率低)。
const QUOTE_PATTERNS: RegExp[] = [
/「([^」]{2,})」/g,
/“([^”]{2,})”/g,
/"([^"]{2,})"/g,
/([^]{2,})/g,
/'([^']{2,})'/g,
];
export function extractDraftQuotes(where: string): string[] {
const found = new Set<string>();
for (const re of QUOTE_PATTERNS) {
for (const m of where.matchAll(re)) {
const inner = m[1]?.trim();
if (inner && inner.length >= 2) found.add(inner);
}
}
return [...found].sort((a, b) => b.length - a.length);
}
// 返回冲突在 text 中的字符区间;定位不到(无 original/引号/段号,或已被改动)→ null。
export function locateInDraft(
text: string,
conflict: Pick<ReviewConflict, "original" | "where">,
): DraftRange | null {
// 1) 精确原文片段(审稿提议的 original逐字匹配——最可靠。
if (conflict.original) {
const idx = text.indexOf(conflict.original);
if (idx !== -1) {
return { start: idx, end: idx + conflict.original.length };
}
}
// 2) 从 where 的引号里抽出草稿原文逐字匹配(覆盖 `草稿中写"…"` 这类定位)。
for (const quote of extractDraftQuotes(conflict.where)) {
const idx = text.indexOf(quote);
if (idx !== -1) {
return { start: idx, end: idx + quote.length };
}
}
// 3) 回退:按 where 段号取段级预览定位该段。snippet 超长会带省略号,
// 去掉末尾「…」后用其前缀(仍是该段真实前缀)做 indexOf。
const snippet = conflictSnippet(text, conflict.where);
if (snippet) {
const needle = snippet.endsWith("…") ? snippet.slice(0, -1) : snippet;
if (needle.length > 0) {
const idx = text.indexOf(needle);
if (idx !== -1) {
return { start: idx, end: idx + needle.length };
}
}
}
return null;
}