- 冲突「采纳改法」:有补丁瞬时改入终稿并选中;无补丁但可定位则采纳时调 AI(refiner)重写该段套入;定位不到提示手改 - 正文改为行内高亮叠层编辑器(与页面同底色、随内容增高、无滚动条),点冲突卡/锚点整页滚到并选中+闪烁 - 仅对可在正文定位的冲突显示「跳转」/锚点;locate 支持从 where 引号抽原文,杜绝定位失败刷屏 - 伏笔建议卡:新埋一键登记(预填表单)/ 疑似回收一键标记 CLOSED(复用现有端点) - 审稿页草稿自动保存(防抖 PUT /draft),与「验收本章」解耦,刷新不丢 - 写作路由带章号 ?chapter=N + 验收成功面板/大纲页「写下一章」入口;写作目录从大纲列出全部章节 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
67 lines
2.4 KiB
TypeScript
67 lines
2.4 KiB
TypeScript
// 在终稿里定位某条冲突对应的正文区间(供编辑器 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;
|
||
}
|