fix(comments): suggestion с вечным 409 (#496)
expectedText брался из debounced REST-снапшота (getAnchoredText по pages/info), а метка ставилась в live-доке — при расхождении (док ушёл вперёд за окно дебаунса) apply строго сравнивал текст под меткой с устаревшим stored selection и давал 409 на каждый вызов. MCP-клиент теперь в transform-фазе (та же версия live-дока, где ставится метка) перечитывает фактическую подстроку под меткой и, если она отличается от сохранённого selection, синкает её через новый эндпоинт POST /comments/resync-suggestion-anchor. Best-effort: сбой синка не откатывает уже заякоренный комментарий, а лишь выдаёт мягкое предупреждение. Совпадающий снапшот не делает лишнего round-trip. Сервер: resyncSuggestionAnchor правит только stored selection незаселённой suggestion своего автора (guards: top-level, есть suggestedText, не applied/resolved, отличается от suggestedText), идемпотентно, без ws-бродкаста. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import { UpdateCommentDto } from './dto/update-comment.dto';
|
||||
import { ResolveCommentDto } from './dto/resolve-comment.dto';
|
||||
import { ApplySuggestionDto } from './dto/apply-suggestion.dto';
|
||||
import { DismissSuggestionDto } from './dto/dismiss-suggestion.dto';
|
||||
import { ResyncSuggestionAnchorDto } from './dto/resync-suggestion-anchor.dto';
|
||||
import { PageIdDto, CommentIdDto } from './dto/comments.input';
|
||||
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
||||
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
||||
@@ -235,6 +236,39 @@ export class CommentController {
|
||||
return this.commentService.applySuggestion(comment, user, provenance);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('resync-suggestion-anchor')
|
||||
async resyncSuggestionAnchor(
|
||||
@Body() dto: ResyncSuggestionAnchorDto,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const comment = await this.commentRepo.findById(dto.commentId, {
|
||||
includeCreator: true,
|
||||
includeResolvedBy: true,
|
||||
});
|
||||
if (!comment) {
|
||||
throw new NotFoundException('Comment not found');
|
||||
}
|
||||
|
||||
const page = await this.pageRepo.findById(comment.pageId);
|
||||
if (!page || page.deletedAt) {
|
||||
throw new NotFoundException('Page not found');
|
||||
}
|
||||
|
||||
// Authorize BEFORE revealing structural detail (mirrors apply/dismiss).
|
||||
// Re-anchoring does NOT change the page text — it only corrects the stored
|
||||
// selection metadata — so the page-level gate is comment access. The service
|
||||
// further restricts it to the suggestion's own author.
|
||||
await this.pageAccessService.validateCanComment(page, user, workspace.id);
|
||||
|
||||
return this.commentService.resyncSuggestionAnchor(
|
||||
comment,
|
||||
dto.selection,
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('dismiss-suggestion')
|
||||
async dismissSuggestion(
|
||||
|
||||
Reference in New Issue
Block a user