feat: 敏感词管理页面(脱敏词后台化,限 lqq 账号)
- 后端 /api/sensitive-words:全局共享词表,开放读、admin 或 lqq 整体替换写 - 前端 desensitize 种子化:SEED_DESENSITIZE_MAP + 运行时 replaceDesensitizeMap - 启动加载后台配置回灌脱敏映射;管理页支持增删改启停 + 一键导入种子词 - 入口置于左下角设置下拉(全量/简洁两种侧边栏均有),仅用户名 lqq 可见 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
39d7dab3e2
commit
25e4e3dbc9
18 files changed
+788
-5
No files matched your search
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 敏感词(脱敏词)管理 API 客户端 + React Query hooks。
|
||||
*
|
||||
* 后端:`/api/sensitive-words`(见 app/gateway/routers/sensitive_words.py)。
|
||||
* 全局共享、无用户隔离;读对所有登录用户开放(运行时脱敏要用),写仅 admin(整体替换)。
|
||||
* snake_case ⇄ camelCase 在此边界收敛。
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
|
||||
const BASE = () => `${getBackendBaseURL()}/api/sensitive-words`;
|
||||
|
||||
/** 一条敏感词配置(前端视图模型)。 */
|
||||
export interface SensitiveWord {
|
||||
/** 原始词 */
|
||||
word: string;
|
||||
/** 脱敏替换词 */
|
||||
replacement: string;
|
||||
/** 是否启用(停用的词不参与运行时脱敏,但保留以便随时恢复) */
|
||||
enabled: boolean;
|
||||
/** 展示/匹配排序 */
|
||||
sortOrder: number;
|
||||
updatedBy?: string | null;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
|
||||
interface ServerWord {
|
||||
word: string;
|
||||
replacement: string;
|
||||
enabled: boolean;
|
||||
sort_order: number;
|
||||
updated_by?: string | null;
|
||||
updated_at?: string | null;
|
||||
}
|
||||
|
||||
function toWord(s: ServerWord): SensitiveWord {
|
||||
return {
|
||||
word: s.word,
|
||||
replacement: s.replacement ?? "",
|
||||
enabled: s.enabled ?? true,
|
||||
sortOrder: s.sort_order ?? 0,
|
||||
updatedBy: s.updated_by ?? null,
|
||||
updatedAt: s.updated_at ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function toBody(w: SensitiveWord): Pick<ServerWord, "word" | "replacement" | "enabled" | "sort_order"> {
|
||||
return {
|
||||
word: w.word,
|
||||
replacement: w.replacement ?? "",
|
||||
enabled: w.enabled,
|
||||
sort_order: w.sortOrder,
|
||||
};
|
||||
}
|
||||
|
||||
async function readJson<T>(res: Response, ctx: string): Promise<T> {
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null as unknown);
|
||||
const detail =
|
||||
data && typeof data === "object" && "detail" in data
|
||||
? String((data as { detail: unknown }).detail)
|
||||
: `${ctx} failed (${res.status})`;
|
||||
throw new Error(detail);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
export async function listSensitiveWords(): Promise<SensitiveWord[]> {
|
||||
const res = await apiFetch(BASE(), { method: "GET" });
|
||||
const data = await readJson<{ words: ServerWord[] }>(res, "list sensitive words");
|
||||
return (data.words ?? []).map(toWord);
|
||||
}
|
||||
|
||||
export async function replaceSensitiveWords(words: SensitiveWord[]): Promise<SensitiveWord[]> {
|
||||
const res = await apiFetch(BASE(), {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ words: words.map(toBody) }),
|
||||
});
|
||||
const data = await readJson<{ words: ServerWord[] }>(res, "save sensitive words");
|
||||
return (data.words ?? []).map(toWord);
|
||||
}
|
||||
|
||||
/** 把「启用」的词条折叠成运行时脱敏所需的 `原始词 → 替换词` 映射。 */
|
||||
export function wordsToMap(words: SensitiveWord[]): Record<string, string> {
|
||||
const map: Record<string, string> = {};
|
||||
for (const w of words) {
|
||||
if (w.enabled && w.word) map[w.word] = w.replacement ?? "";
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// ── React Query hooks(管理页 + 启动加载共用缓存) ──────────────────────────
|
||||
|
||||
export const SENSITIVE_WORDS_KEY = "sensitive-words";
|
||||
|
||||
export function useSensitiveWords() {
|
||||
return useQuery({
|
||||
queryKey: [SENSITIVE_WORDS_KEY],
|
||||
queryFn: listSensitiveWords,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSensitiveWordActions() {
|
||||
const queryClient = useQueryClient();
|
||||
return {
|
||||
save: useMutation({
|
||||
mutationFn: replaceSensitiveWords,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: [SENSITIVE_WORDS_KEY] }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user