Files
writer-work-flow/apps/web/lib/rules/useRules.ts
Yaojia Wang bf39f50b2f fix(qa): 实施 4 组设计型 QA 项——规则删除/codex 角色/大纲卷过滤/文风回炉锚点
#5 规则 DELETE + id:暴露 RuleView.id(PK);新增 DELETE /projects/{id}/rules/{rule_id}
  (项目/规则不存在→404,成功→204,按 (id,project_id) 限定);rule_repo 加
  list_for_project/delete;RulesPage 每条加删除(乐观删+回滚+toast)。assemble 侧
  RuleView(缓存前缀)不动,列表另立 RuleListItemView。
#7 codex 角色 relations:写侧本已持久化、读端点 _existing_characters 硬编码 []。
  加 _relations_from_jsonb 解析 {name,kind,note},CodexPage 渲染关系 chip。
#8 角色入库幂等:SqlCharacterWriteRepo.create 改 (project_id,name) app 层 upsert——
  重复入库改更新而非插入;不加 UNIQUE/迁移(线上已有重复行会让约束迁移失败)。
#1 大纲卷过滤:GET /outline 支持可选 ?volume(无参=全部,向后兼容);OutlineEditor
  加「查看:全部/卷N」筛选,与生成目标卷解耦。
H3/#9 文风回炉锚点:StyleDriftSegment 加 text(逐字命中段),style.md 指示审稿输出;
  前端按内容锚点定位回炉目标(idx 仅排序),命中失败 → 提示「无法定位该段」而非
  静默 no-op。style golden fixture 已重生成。

契约变更已 pnpm gen:api(RuleView.id / DELETE rules / outline ?volume)。无迁移
(alembic 无漂移)。门禁绿:ruff/mypy(210)/alembic/pytest 760 · 前端 tsc/lint/vitest 329。
2026-06-25 12:53:03 +02:00

94 lines
2.9 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.

"use client";
import { useCallback, useState } from "react";
import { api } from "@/lib/api/client";
import { useToast } from "@/components/Toast";
import type { RuleView } from "@/lib/api/types";
import { buildRuleCreateRequest, hasUsableContent, type RuleLevel } from "./rules";
export interface UseRules {
items: RuleView[];
busy: boolean;
// 新增一条规则(乐观追加 + 失败回滚 + Toast
add: (
projectId: string,
level: RuleLevel,
content: string,
) => Promise<boolean>;
// 删除一条规则(乐观移除 + 失败回滚 + Toast
remove: (projectId: string, ruleId: string) => Promise<boolean>;
}
// 规则页UX §7列出 + 新增规则。乐观追加,失败回滚(仿 useForeshadow
export function useRules(initial: RuleView[]): UseRules {
const [items, setItems] = useState<RuleView[]>(initial);
const [busy, setBusy] = useState(false);
const toast = useToast();
const add = useCallback<UseRules["add"]>(
async (projectId, level, content) => {
if (!hasUsableContent(content)) {
toast("请填写规则正文。", "error");
return false;
}
const snapshot = items;
// 乐观项需一个临时 id删除 handle 之前);服务端返回后被权威行替换。
const optimistic: RuleView = {
id: crypto.randomUUID(),
level,
content: content.trim(),
};
setItems((prev) => [...prev, optimistic]);
setBusy(true);
try {
const { data, error } = await api.POST(
"/projects/{project_id}/rules",
{
params: { path: { project_id: projectId } },
body: buildRuleCreateRequest(level, content),
},
);
if (error || !data) {
setItems(snapshot); // 回滚
toast("新增规则失败,请稍后重试。", "error");
return false;
}
// 用服务端权威行替换乐观行(去掉乐观项、追加返回项)。
setItems((prev) => [...prev.slice(0, snapshot.length), data]);
toast("已新增规则", "success");
return true;
} finally {
setBusy(false);
}
},
[items, toast],
);
const remove = useCallback<UseRules["remove"]>(
async (projectId, ruleId) => {
const snapshot = items;
setItems((prev) => prev.filter((r) => r.id !== ruleId));
setBusy(true);
try {
const { error } = await api.DELETE(
"/projects/{project_id}/rules/{rule_id}",
{ params: { path: { project_id: projectId, rule_id: ruleId } } },
);
if (error) {
setItems(snapshot); // 回滚
toast("删除规则失败,请稍后重试。", "error");
return false;
}
toast("已删除规则", "success");
return true;
} finally {
setBusy(false);
}
},
[items, toast],
);
return { items, busy, add, remove };
}