敏感词

This commit is contained in:
fengxiangdebaozi committed 2026-05-25 18:20:40 +08:00
1 parent 6396ccc701
commit f472bba9f0
38 files changed
+540 -173

No files matched your search

+235
View File
@@ -0,0 +1,235 @@
# 深蓝色 UI 规范
---
## 1. 色彩
### 1.1 标准色
色彩体系由四个色系构成,每个色系分主色与辅色两组,每组按明度从浅到深编号(01 最浅,数字越大越深)。
---
#### 1.1.1 色系总览
| 色系 | 用途定位 | 代码前缀 |
|------|----------|----------|
| 渐红系 | 警示、错误、强调操作 | ZS-001 |
| 渐蓝系 | 信息、链接、次级操作 | ZS-002 |
| 渐青系 | 成功、在线状态、数据高亮 | ZS-003 |
| 深蓝系 | 页面背景、容器、导航底色 | ZS-004 |
---
#### 1.1.2 渐红系(ZS-001)
用于错误提示、危险操作、重要告警。
| 编号 | 色值 | 说明 |
|------|------|------|
| ZS-001-01 | `#FFF1F0` | 极浅红,错误背景底色 |
| ZS-001-02 | `#FFCCC7` | 浅红,输入框错误边框 |
| ZS-001-03 | `#FFA39E` | — |
| ZS-001-04 | `#FF7875` | — |
| ZS-001-05 | `#FF4D4F` | 标准红,Tag / Badge |
| ZS-001-06 | `#F5222D` | 主色红,按钮 / 图标 |
| ZS-001-07 | `#CF1322` | 深红,Hover 状态 |
| ZS-001-08 | `#A8071A` | 深红,Active / Press |
| ZS-001-09 | `#820014` | 极深红 |
| ZS-001-10 | `#5C0011` | 最深红 |
---
#### 1.1.3 渐蓝系(ZS-002)
用于信息提示、链接、次级按钮。
| 编号 | 色值 | 说明 |
|------|------|------|
| ZS-002-01 | `#E6F4FF` | 极浅蓝,信息背景底色 |
| ZS-002-02 | `#BAE0FF` | 浅蓝 |
| ZS-002-03 | `#91CAFF` | — |
| ZS-002-04 | `#69B1FF` | — |
| ZS-002-05 | `#4096FF` | 标准蓝,链接 / 信息 Tag |
| ZS-002-06 | `#1677FF` | 主色蓝,主操作按钮 |
| ZS-002-07 | `#0958D9` | 深蓝,Hover |
| ZS-002-08 | `#003EB3` | 深蓝,Active |
| ZS-002-09 | `#002C8C` | 极深蓝 |
| ZS-002-10 | `#001D6C` | 最深蓝 |
---
#### 1.1.4 渐青系(ZS-003)
用于成功状态、在线标识、数据图表高亮。
| 编号 | 色值 | 说明 |
|------|------|------|
| ZS-003-01 | `#E6FFFB` | 极浅青,成功背景 |
| ZS-003-02 | `#B5F5EC` | 浅青 |
| ZS-003-03 | `#87E8DE` | — |
| ZS-003-04 | `#5CDBD3` | — |
| ZS-003-05 | `#36CFC9` | 标准青,成功 Tag |
| ZS-003-06 | `#13C2C2` | 主色青,在线点 / 图标 |
| ZS-003-07 | `#08979C` | 深青,Hover |
| ZS-003-08 | `#006D75` | 深青,Active |
| ZS-003-09 | `#00474F` | 极深青 |
| ZS-003-10 | `#002329` | 最深青 |
---
#### 1.1.5 深蓝系(ZS-004)
用于页面背景、导航栏、侧边栏、卡片容器。这是深蓝主题的核心色系。
| 编号 | 色值 | 说明 |
|------|------|------|
| ZS-004-01 | `#E8EDF5` | 极浅,分割线 / 禁用底色 |
| ZS-004-02 | `#C5CFE0` | 浅,边框 |
| ZS-004-03 | `#9BADC8` | — |
| ZS-004-04 | `#6E88B0` | — |
| ZS-004-05 | `#4A6491` | 中深蓝,次级文字 |
| ZS-004-06 | `#2D4473` | 深蓝,卡片容器背景 |
| ZS-004-07 | `#1A2F5A` | 深蓝,侧边栏背景 |
| ZS-004-08 | `#122347` | 深蓝,导航栏背景 |
| ZS-004-09 | `#0C1836` | 极深蓝,页面背景 |
| ZS-004-10 | `#060E24` | 最深,全局底色 |
---
#### 1.1.6 功能色(主色 / 字体色 / 辅色 / 背景色)
从标准色系中提取,用于具体 UI 角色指定。
| 角色 | 色值 | 来源 |
|------|------|------|
| 主色(Primary) | `#1677FF` | ZS-002-06 |
| 主色 Hover | `#0958D9` | ZS-002-07 |
| 主色 Active | `#003EB3` | ZS-002-08 |
| 正常字体色 | `#C5CFE0` | ZS-004-02 |
| 次级字体色 | `#6E88B0` | ZS-004-04 |
| 禁用字体色 | `#4A6491` | ZS-004-05 |
| 辅色(Accent) | `#13C2C2` | ZS-003-06 |
| 页面背景色 | `#0C1836` | ZS-004-09 |
| 容器背景色 | `#1A2F5A` | ZS-004-07 |
| 白色 | `#FFFFFF` | — |
---
#### 1.1.7 小面积装饰色
用于图标点缀、徽标、少量强调,不用于大面积背景。
| 色值 | 说明 |
|------|------|
| `#6388FF` | 紫蓝,图标装饰 |
| `#F5222D` | 红色,危险徽标 |
| `#12306C` | 深蓝,品牌点缀 |
| `#478880` | 青绿,辅助点缀 |
| `#950404` | 深红,重要告警点 |
---
#### 1.1.8 图表配色
**通用图表色序**(面积较大,如柱状图、折线图、饼图):
| 序号 | 色值 |
|------|------|
| 1 | `#1677FF` |
| 2 | `#13C2C2` |
| 3 | `#4096FF` |
| 4 | `#36CFC9` |
| 5 | `#69B1FF` |
| 6 | `#5CDBD3` |
| 7 | `#91CAFF` |
| 8 | `#87E8DE` |
**小面积图表色序**(面积较小,如散点、小徽章、迷你图):
| 序号 | 色值 |
|------|------|
| 1 | `#4096FF` |
| 2 | `#36CFC9` |
| 3 | `#FF4D4F` |
| 4 | `#FFA39E` |
| 5 | `#6388FF` |
| 6 | `#478880` |
---
## 2. 字体
### 2.1 全局样式 - 字体
---
#### 2.1.1 大屏页面字体规范
##### 中文字体:Noto Sans S Chinese
| 样式 | 使用场景 |
|------|----------|
| 24px Bold | 一级标题 |
| 24px Medium | 次要一级标题 |
| 22px Medium | 二级标题 |
| 22px Regular | 正文 |
| 20px Regular | 排版空间有限的正文 |
##### 数字字体:D-DIN
| 样式 | 使用场景 |
|------|----------|
| 32px Bold | 重要数据展示 |
| 32px Regular | 普通数据展示 |
| 26px Bold | 常规数据展示 |
| 26px Regular | 常规数据展示 |
##### 特殊数字:DS-Digital
| 样式 | 使用场景 |
|------|----------|
| 50px Bold | 大屏主数据展示 |
##### 表格字体:Arial
| 样式 | 使用场景 |
|------|----------|
| 20px Regular | 表格横纵轴数字 |
---
#### 2.1.2 后台系统字体规范
##### 中文字体:Noto Sans S Chinese
| 样式 | 使用场景 |
|------|----------|
| 18px Bold | 导航、一级标题 |
| 18px Medium | 次要一级标题 |
| 16px Bold | 二级标题 |
| 16px Medium | 次要二级标题 |
| 16px Regular | 正文、按钮内部文字等 |
| 16px Light | 次要正文 |
##### 数字字体:D-DIN
| 样式 | 使用场景 |
|------|----------|
| 26px Bold | 重要数据展示 |
| 26px Regular | 普通数据展示 |
| 24px Bold | 常规数据展示(小) |
| 24px Regular | 常规数据展示(小) |
##### LOGO 字体:DOUYUFont
| 样式 | 使用场景 |
|------|----------|
| 20px 文字样式 | 页面顶部系统名称 |
##### 表格数字:Arial
| 样式 | 使用场景 |
|------|----------|
| 16px Regular | 表格横纵轴数字 |
| 14px Regular | 表格横轴数字(空间有限) |
+12 -49
View File
@@ -400,63 +400,26 @@ export function PageSidebar({
onOpenChange={setNotificationOpen}
/>
<aside
className={`fixed left-0 top-0 z-40 flex h-screen flex-col overflow-hidden shadow-sm theme-transition transition-all duration-300 ${
className={`fixed left-0 top-16 z-40 flex h-[calc(100vh-4rem)] flex-col overflow-hidden shadow-sm theme-transition transition-all duration-300 ${
isDark
? "border-r border-blue-800/30 bg-slate-900"
: "border-r border-red-100 bg-white"
} ${collapsed ? "w-16" : "w-64"}`}
>
<div
className={`flex h-16 items-center theme-transition ${
isDark ? "border-b border-blue-800/30" : "border-b border-red-100"
} ${collapsed ? "justify-center px-2" : "justify-between px-6"}`}
<button
type="button"
onClick={onToggleCollapsed}
title={collapsed ? "展开侧边栏" : "收起侧边栏"}
className={`absolute -right-3 top-6 z-50 flex h-6 w-6 items-center justify-center rounded-full shadow-md theme-transition ${
isDark
? "bg-slate-700 text-slate-300 hover:bg-slate-600"
: "bg-white text-gray-500 hover:bg-red-50 hover:text-red-600 border border-red-100"
}`}
>
<div className="flex min-w-0 items-center gap-3">
<div
className={`flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg transition-transform duration-300 ease-out ${
isDark
? "bg-gradient-to-br from-blue-500 to-cyan-400"
: "bg-gradient-to-br from-red-500 to-red-400"
}`}
>
<FileText className="h-4 w-4 text-white" />
</div>
{!collapsed && (
<div className="overflow-hidden whitespace-nowrap animate-in fade-in-0 slide-in-from-left-2 duration-300">
<h1
className={`text-sm font-semibold theme-transition ${
isDark ? "text-white" : "text-gray-900"
}`}
>
CM助手
</h1>
</div>
)}
</div>
{!collapsed && (
<button
type="button"
onClick={onToggleCollapsed}
className={iconButtonClass}
title="Collapse sidebar"
>
<ChevronLeft className="h-4 w-4" />
</button>
)}
</div>
{collapsed ? <ChevronRight className="h-3 w-3" /> : <ChevronLeft className="h-3 w-3" />}
</button>
<nav className={`flex-1 overflow-y-auto ${collapsed ? "p-2" : "p-4"} space-y-2 transition-all duration-300 ease-out`}>
{collapsed && (
<button
type="button"
onClick={onToggleCollapsed}
className={`flex w-full items-center justify-center ${iconButtonClass}`}
title="Expand sidebar"
>
<ChevronRight className="h-5 w-5" />
</button>
)}
{visibleMenuItems.map((item) =>
item.children ? renderBranch(item) : renderLeaf(item),
)}
@@ -3,6 +3,7 @@
import { BotIcon } from "lucide-react";
import { type Agent } from "@/core/agents";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
export function AgentWelcome({
@@ -14,8 +15,10 @@ export function AgentWelcome({
agent: Agent | null | undefined;
agentName: string;
}) {
const displayName = agent?.name ?? agentName;
const description = agent?.description;
const displayName = desensitize(agent?.name ?? agentName);
const description = agent?.description
? desensitize(agent.description)
: undefined;
return (
<div
@@ -48,6 +48,7 @@ import type { Agent } from "@/core/agents";
import { getStoredAuth } from "@/core/auth";
import { useI18n } from "@/core/i18n/hooks";
import { useSkills } from "@/core/skills/hooks";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
interface AgentCardProps {
@@ -195,12 +196,12 @@ export function AgentCard({ agent }: AgentCardProps) {
<BotIcon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1">
<CardTitle className="truncate text-base" title={agent.name}>
{agent.name}
<CardTitle className="truncate text-base" title={desensitize(agent.name)}>
{desensitize(agent.name)}
</CardTitle>
{agent.model && (
<Badge variant="secondary" className="mt-0.5 text-xs">
{agent.model}
{desensitize(agent.model)}
</Badge>
)}
</div>
@@ -252,7 +253,7 @@ export function AgentCard({ agent }: AgentCardProps) {
</div>
{agent.description && (
<CardDescription className="mt-2 line-clamp-2 text-sm">
{agent.description}
{desensitize(agent.description)}
</CardDescription>
)}
</CardHeader>
@@ -272,7 +273,7 @@ export function AgentCard({ agent }: AgentCardProps) {
variant="outline"
className="text-xs"
>
{group}
{desensitize(group)}
</Badge>
))}
{agent.skills?.map((skill) => (
@@ -286,7 +287,7 @@ export function AgentCard({ agent }: AgentCardProps) {
setDetailSkillName(skill);
}}
>
{skill}
{desensitize(skill)}
</Badge>
))}
</div>
@@ -377,7 +378,7 @@ export function AgentCard({ agent }: AgentCardProps) {
<Dialog open={editOpen} onOpenChange={setEditOpen}>
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>编辑智能体:{agent.name}</DialogTitle>
<DialogTitle>编辑智能体:{desensitize(agent.name)}</DialogTitle>
<DialogDescription>
可以修改名称、描述、技能白名单、SOUL 内容与发布状态。
</DialogDescription>
@@ -428,10 +429,10 @@ export function AgentCard({ agent }: AgentCardProps) {
size="sm"
variant={checked ? "default" : "outline"}
onClick={() => toggleSkill(skill.name)}
title={skill.description}
title={desensitize(skill.description ?? "")}
className="h-8 max-w-[200px] rounded-r-none"
>
<span className="truncate">{skill.name}</span>
<span className="truncate">{desensitize(skill.name)}</span>
</Button>
<Button
type="button"
@@ -50,6 +50,7 @@ import { installSkill } from "@/core/skills/api";
import { streamdownPlugins } from "@/core/streamdown";
import { checkCodeFile, getFileName } from "@/core/utils/files";
import { env } from "@/env";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
import { ArtifactLink } from "../citations/artifact-link";
@@ -608,7 +609,7 @@ export function ArtifactFileDetail({
viewMode === "preview" &&
(language === "markdown" || language === "html") && (
<ArtifactFilePreview
content={displayContent}
content={desensitize(displayContent ?? "")}
language={language ?? "text"}
animated={showPreviewSwitchPulse}
activeCursor={isApplyingEdit}
@@ -618,7 +619,11 @@ export function ArtifactFileDetail({
<div className="size-full">
<CodeEditor
className="size-full resize-none rounded-none border-none bg-transparent"
value={displayContent ?? ""}
value={
canEditArtifact
? (displayContent ?? "")
: desensitize(displayContent ?? "")
}
readonly={!canEditArtifact}
onChange={
canEditArtifact
@@ -8,6 +8,7 @@ import {
type MessageResponseProps,
} from "@/components/ai-elements/message";
import { streamdownPlugins } from "@/core/streamdown";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
import { CitationLink } from "../citations/citation-link";
@@ -63,6 +64,8 @@ export function MarkdownContent({
if (!content) return null;
const displayContent = desensitize(content);
return (
<MessageResponse
className={className}
@@ -70,7 +73,7 @@ export function MarkdownContent({
rehypePlugins={rehypePlugins}
components={components}
>
{content}
{displayContent}
</MessageResponse>
);
}
@@ -37,6 +37,7 @@ import {
} from "@/core/messages/utils";
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
import { humanMessagePlugins } from "@/core/streamdown";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
import { CopyButton } from "../copy-button";
@@ -226,7 +227,7 @@ function MessageContent_({
<TaskTrigger title="">
<div className="text-muted-foreground flex w-full cursor-default items-center gap-2 text-sm select-none">
<Loader className="size-4" />
<span>{contentToDisplay}</span>
<span>{desensitize(contentToDisplay)}</span>
</div>
</TaskTrigger>
</Task>
@@ -240,7 +241,7 @@ function MessageContent_({
<AIElementMessageContent className={className}>
<Reasoning isStreaming={isLoading}>
<ReasoningTrigger />
<ReasoningContent>{reasoningContent}</ReasoningContent>
<ReasoningContent>{desensitize(reasoningContent)}</ReasoningContent>
</Reasoning>
<MessageTokenUsage
enabled={tokenUsageEnabled}
@@ -259,7 +260,7 @@ function MessageContent_({
components={components}
parseIncompleteMarkdown={false}
>
{contentToDisplay}
{desensitize(contentToDisplay)}
</AIElementMessageResponse>
) : null;
return (
@@ -58,6 +58,7 @@ import {
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
import { env } from "@/env";
import { desensitize } from "@/lib/desensitize";
import { isIMEComposing } from "@/lib/ime";
export function RecentChatList() {
@@ -211,7 +212,7 @@ export function RecentChatList() {
className="block w-full whitespace-nowrap group-hover/side-menu-item:overflow-hidden"
href={pathOfThread(thread)}
>
{titleOfThread(thread)}
{desensitize(titleOfThread(thread))}
</Link>
</SidebarMenuButton>
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true" && (
@@ -42,6 +42,7 @@ import type {
MemoryTarget,
MemoryV2Config,
} from "@/core/memory/types";
import { desensitize } from "@/lib/desensitize";
import { SettingsSection } from "./settings-section";
@@ -173,7 +174,7 @@ export function MemorySettingsPage() {
className="flex items-start justify-between gap-3 rounded-md border p-3"
>
<p className="min-w-0 flex-1 whitespace-pre-wrap text-sm [overflow-wrap:anywhere]">
{entry}
{desensitize(entry)}
</p>
<div className="flex shrink-0 items-center gap-1">
<Button
@@ -234,7 +235,7 @@ export function MemorySettingsPage() {
</SelectItem>
{experts.map((a) => (
<SelectItem key={a.id} value={a.id}>
{a.name}
{desensitize(a.name)}
</SelectItem>
))}
</SelectContent>
@@ -272,7 +273,7 @@ export function MemorySettingsPage() {
</SelectItem>
{experts.map((a) => (
<SelectItem key={a.id} value={a.id}>
{a.name}
{desensitize(a.name)}
</SelectItem>
))}
</SelectContent>
@@ -356,7 +357,7 @@ export function MemorySettingsPage() {
<div className="text-muted-foreground mb-1 font-medium">
{m.entryToDeleteLabel}
</div>
<p className="break-words">{entryToDelete.content}</p>
<p className="break-words">{desensitize(entryToDelete.content)}</p>
</div>
) : null}
<DialogFooter>
@@ -539,10 +540,10 @@ function MemoryV1View({
{Math.round(fact.confidence * 100)}%
</span>
</div>
<p className="text-sm whitespace-pre-wrap [overflow-wrap:anywhere]">{fact.content}</p>
<p className="text-sm whitespace-pre-wrap [overflow-wrap:anywhere]">{desensitize(fact.content)}</p>
{fact.sourceError && (
<p className="text-xs text-red-600 bg-red-50 rounded px-2 py-1">
避免:{fact.sourceError}
避免:{desensitize(fact.sourceError)}
</p>
)}
</div>
@@ -566,7 +567,7 @@ function MemoryV1View({
<dt className="font-medium text-muted-foreground mb-0.5">
{CTX_LABELS[key] ?? key}
</dt>
<dd className="whitespace-pre-wrap [overflow-wrap:anywhere]">{value}</dd>
<dd className="whitespace-pre-wrap [overflow-wrap:anywhere]">{desensitize(value)}</dd>
</div>
))}
</dl>
@@ -585,7 +586,7 @@ function MemoryV1View({
<dt className="font-medium text-muted-foreground mb-0.5">
{HISTORY_LABELS[key] ?? key}
</dt>
<dd className="whitespace-pre-wrap [overflow-wrap:anywhere]">{value}</dd>
<dd className="whitespace-pre-wrap [overflow-wrap:anywhere]">{desensitize(value)}</dd>
</div>
))}
</dl>
@@ -686,7 +687,7 @@ function MemorySearchPanel() {
</SelectItem>
{experts.map((a) => (
<SelectItem key={a.id} value={a.id}>
{a.name}
{desensitize(a.name)}
</SelectItem>
))}
</SelectContent>
@@ -719,7 +720,7 @@ function MemorySearchPanel() {
) : results !== null ? (
mode === "reflect" ? (
<div className="rounded-md border p-3 text-sm whitespace-pre-wrap [overflow-wrap:anywhere]">
{reflectText.trim() || m.searchNoResults}
{reflectText.trim() ? desensitize(reflectText.trim()) : m.searchNoResults}
</div>
) : results.length === 0 ? (
<div className="text-muted-foreground rounded-md border border-dashed p-3 text-sm">
@@ -735,7 +736,7 @@ function MemorySearchPanel() {
key={`${index}-${item.slice(0, 16)}`}
className="rounded-md border p-3 text-sm whitespace-pre-wrap [overflow-wrap:anywhere]"
>
{item}
{desensitize(item)}
</div>
))}
</div>
@@ -68,6 +68,7 @@ import {
} from "@/core/skills/hooks";
import type { Skill } from "@/core/skills/type";
import { env } from "@/env";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
import { SettingsSection } from "./settings-section";
@@ -404,7 +405,7 @@ function SkillRow({
>
<ItemTitle>
<div className="flex flex-wrap items-center gap-2">
<span>{skill.name}</span>
<span>{desensitize(skill.name)}</span>
{isBuiltin && (
<Badge variant="secondary" className="text-xs">
{t.settings.skills.badgeBuiltin}
@@ -474,7 +475,7 @@ function SkillRow({
</div>
</ItemTitle>
<ItemDescription className="line-clamp-4">
{skill.description}
{desensitize(skill.description ?? "")}
</ItemDescription>
{skill.use_count !== undefined && (
<div className="text-muted-foreground mt-1 text-xs">
@@ -560,7 +561,9 @@ function SkillRow({
<DialogHeader>
<DialogTitle>{t.settings.skills.deleteAction}</DialogTitle>
<DialogDescription>
{t.settings.skills.deleteConfirm.replace("{name}", skill.name)}
{desensitize(
t.settings.skills.deleteConfirm.replace("{name}", skill.name),
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
@@ -809,22 +812,22 @@ function InstallSkillDialog({
<div className="flex flex-col gap-2 rounded-md border p-3 text-sm">
<div className="flex items-center gap-2 text-emerald-600">
<CheckCircle2 className="size-4" />
<span className="font-medium">{validation.skill_name}</span>
<span className="font-medium">{desensitize(validation.skill_name)}</span>
</div>
<Field label={dialog.descriptionLabel}>
<span className="line-clamp-3 whitespace-pre-wrap">
{validation.description || "—"}
{validation.description ? desensitize(validation.description) : "—"}
</span>
</Field>
{validation.license && (
<Field label={dialog.licenseLabel}>{validation.license}</Field>
<Field label={dialog.licenseLabel}>{desensitize(validation.license)}</Field>
)}
{validation.allowed_tools.length > 0 && (
<Field label={dialog.toolsLabel}>
<div className="flex flex-wrap gap-1">
{validation.allowed_tools.map((tool) => (
<Badge key={tool} variant="secondary" className="text-xs">
{tool}
{desensitize(tool)}
</Badge>
))}
</div>
@@ -834,7 +837,7 @@ function InstallSkillDialog({
<Field label={dialog.warningsLabel}>
<ul className="list-disc pl-4 text-xs">
{validation.warnings.map((w, idx) => (
<li key={idx}>{w}</li>
<li key={idx}>{desensitize(w)}</li>
))}
</ul>
</Field>
@@ -843,7 +846,9 @@ function InstallSkillDialog({
<div className="text-amber-600 mt-1 flex items-start gap-2 text-xs">
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
<span>
{dialog.conflictCustom.replace("{name}", validation.skill_name)}
{desensitize(
dialog.conflictCustom.replace("{name}", validation.skill_name),
)}
</span>
</div>
)}
@@ -851,7 +856,9 @@ function InstallSkillDialog({
<div className={cn("mt-1 flex items-start gap-2 text-xs", adminCanOverwritePublic ? "text-amber-600" : "text-destructive")}>
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
<span>
{(adminCanOverwritePublic ? dialog.conflictPublicAdminHint : dialog.conflictPublic).replace("{name}", validation.skill_name)}
{desensitize(
(adminCanOverwritePublic ? dialog.conflictPublicAdminHint : dialog.conflictPublic).replace("{name}", validation.skill_name),
)}
</span>
</div>
)}
@@ -30,6 +30,7 @@ import {
import { useI18n } from "@/core/i18n/hooks";
import { useDeleteCustomSkill, useSetSkillState } from "@/core/skills/hooks";
import type { Skill } from "@/core/skills/type";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
type Kind = "stale" | "archived";
@@ -146,7 +147,7 @@ function SkillStateRow({ skill, kind }: { skill: Skill; kind: Kind }) {
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<span className="font-medium">{skill.name}</span>
<span className="font-medium">{desensitize(skill.name)}</span>
{isArchived ? (
<Badge variant="secondary" className="text-xs" title={tt.badgeArchivedTitle}>
{tt.badgeArchived}
@@ -180,7 +181,7 @@ function SkillStateRow({ skill, kind }: { skill: Skill; kind: Kind }) {
)}
</div>
<p className="text-muted-foreground mt-1 line-clamp-2 text-sm">
{skill.description}
{desensitize(skill.description ?? "")}
</p>
<p className="text-muted-foreground mt-1 flex flex-wrap gap-x-3 text-xs">
<span>{timeLabel}</span>
@@ -254,7 +255,7 @@ function SkillStateRow({ skill, kind }: { skill: Skill; kind: Kind }) {
<DialogHeader>
<DialogTitle>{tt.deleteAction}</DialogTitle>
<DialogDescription>
{tt.deleteConfirm.replace("{name}", skill.name)}
{desensitize(tt.deleteConfirm.replace("{name}", skill.name))}
</DialogDescription>
</DialogHeader>
<DialogFooter>
@@ -295,12 +296,12 @@ function SkillViewDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{skill.name}</DialogTitle>
<DialogTitle>{desensitize(skill.name)}</DialogTitle>
<DialogDescription>{tv.title}</DialogDescription>
</DialogHeader>
<div className="grid gap-3 text-sm">
<Row label={tv.descriptionLabel}>
<span className="whitespace-pre-wrap">{skill.description}</span>
<span className="whitespace-pre-wrap">{desensitize(skill.description ?? "")}</span>
</Row>
<Row label={tv.stateLabel}>
<span className={cn("font-medium", stateColor(skill.state))}>
@@ -34,6 +34,7 @@ import {
} from "@/core/skills/hooks";
import type { Skill } from "@/core/skills/type";
import { reasoningPlugins } from "@/core/streamdown";
import { desensitize } from "@/lib/desensitize";
const markdownClassName =
"!h-auto !w-full max-w-none min-w-0 text-sm leading-7 " +
@@ -147,7 +148,7 @@ export function SkillDetailDialog({
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<DialogTitle className="truncate text-base">
{skill?.name ?? skillName ?? ""}
{desensitize(skill?.name ?? skillName ?? "")}
</DialogTitle>
{skill?.builtin && (
<Badge variant="secondary" className="text-xs">
@@ -165,7 +166,7 @@ export function SkillDetailDialog({
)}
</div>
<DialogDescription className="mt-1 line-clamp-2 text-sm leading-relaxed">
{skill?.description || "-"}
{skill?.description ? desensitize(skill.description) : "-"}
</DialogDescription>
</div>
</div>
@@ -328,7 +329,7 @@ export function SkillDetailDialog({
</div>
) : showRawSource ? (
<pre className="text-foreground/80 max-w-full overflow-x-auto rounded-lg border bg-muted p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap">
{contentData?.content}
{desensitize(contentData?.content ?? "")}
</pre>
) : (
<MarkdownContent
@@ -1,5 +1,6 @@
import { XIcon } from "lucide-react";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
import type { Tag } from "@/core/tags";
@@ -24,7 +25,7 @@ export function TagChip({
className,
)}
>
{tag.name}
{desensitize(tag.name)}
{onRemove && (
<button
type="button"
@@ -3,6 +3,7 @@ import { useEffect } from "react";
import { useI18n } from "@/core/i18n/hooks";
import type { AgentThreadState } from "@/core/threads";
import { desensitize } from "@/lib/desensitize";
import { useThreadChat } from "./chats";
import { FlipDisplay } from "./flip-display";
@@ -44,7 +45,7 @@ export function ThreadTitle({
}
return (
<FlipDisplay uniqueKey={threadId}>
{thread.values.title ?? "Untitled"}
{desensitize(thread.values.title ?? "Untitled")}
</FlipDisplay>
);
}
+28 -2
View File
@@ -1,6 +1,8 @@
"use client";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { desensitize } from "@/lib/desensitize";
import { useI18nContext } from "./context";
import { getLocaleFromCookie, setLocaleInCookie } from "./cookies";
@@ -13,10 +15,34 @@ import {
type Locale,
} from "./index";
function wrapWithDesensitize<T>(value: T): T {
if (typeof value === "string") {
return desensitize(value) as T;
}
if (typeof value === "function") {
return ((...args: unknown[]) => {
const result = (value as (...a: unknown[]) => unknown)(...args);
return wrapWithDesensitize(result);
}) as T;
}
if (Array.isArray(value)) {
return value.map((item) => wrapWithDesensitize(item)) as T;
}
if (value !== null && typeof value === "object") {
return new Proxy(value as object, {
get(target, prop, receiver) {
return wrapWithDesensitize(Reflect.get(target, prop, receiver));
},
}) as T;
}
return value;
}
export function useI18n() {
const { locale, setLocale } = useI18nContext();
const t = translations[locale] ?? translations[DEFAULT_LOCALE];
const rawT = translations[locale] ?? translations[DEFAULT_LOCALE];
const t = useMemo(() => wrapWithDesensitize(rawT), [rawT]);
const changeLocale = (newLocale: Locale) => {
setLocale(newLocale);
+71
View File
@@ -0,0 +1,71 @@
/**
* 脱敏词映射表
* key: 原始词(支持正则特殊字符,会被转义)
* value: 脱敏后替换词
*/
export const DESENSITIZE_MAP: Record<string, string> = {
态势: "TS",
情报: "QB",
指挥: "ZH",
信息准备: "XX准备",
行为体: "XWT",
驱动因素: "QDYS",
叙事: "XS",
前三定: "QSD",
受众目标: "SZMB",
作战: "ZZ",
目的: "MD",
关键传播节点: "GJCBJD",
战法: "ZF",
行动: "XD",
打击: "DJ",
目标: "MB",
战力: "ZL",
筹划: "COH",
策划: "CEH",
模拟: "MN",
推演: "TY",
认知: "RZ",
触及: "CJ"
};
/**
* 将 DESENSITIZE_MAP 编译为单个正则,避免多次遍历字符串。
* 仅在模块首次加载时执行一次。
*/
function buildRegex(map: Record<string, string>): RegExp | null {
const keys = Object.keys(map);
if (keys.length === 0) return null;
const pattern = keys.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
return new RegExp(pattern, "g");
}
let _regex: RegExp | null = buildRegex(DESENSITIZE_MAP);
/**
* 对文本进行脱敏处理,将 DESENSITIZE_MAP 中的词替换为对应脱敏词。
*
* @param text 原始文本
* @returns 脱敏后的文本
*
* @example
* desensitize("当前态势分析") // => "当前TS分析"
*/
export function desensitize(text: string): string {
if (!_regex) return text;
// 每次 exec 需要重置 lastIndex,使用 replace 更安全
return text.replace(_regex, (matched) => DESENSITIZE_MAP[matched] ?? matched);
}
/**
* 运行时动态追加脱敏词,追加后自动重编译正则。
*
* @param entries 要追加的词条,格式同 DESENSITIZE_MAP
*
* @example
* addDesensitizeEntries({ 情报: "QB", 指挥: "ZH" });
*/
export function addDesensitizeEntries(entries: Record<string, string>): void {
Object.assign(DESENSITIZE_MAP, entries);
_regex = buildRegex(DESENSITIZE_MAP);
}
@@ -1,5 +1,7 @@
import { FileText, PenLine } from 'lucide-react';
import { desensitize } from '@/lib/desensitize';
import { useCanvasGraphContext } from '../../contexts/CanvasGraphContext';
import { getCurrentArtifactContent, isCodeArtifact } from '../../utils/artifact';
import { TextArtifactRenderer } from '../artifact/TextArtifactRenderer';
@@ -45,7 +47,7 @@ export function AIWritingDraftPanel() {
<div className="h-10 border-b flex items-center gap-2 px-4 shrink-0 bg-muted/20">
<FileText className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
<span className="text-sm font-medium truncate flex-1">
{content?.title ?? '草稿预览'}
{content?.title ? desensitize(content.title) : '草稿预览'}
</span>
</div>
@@ -1,5 +1,7 @@
import { CheckCircle2, ChevronRight, FileText, Search, PenLine, ClipboardEdit, Star } from 'lucide-react';
import { desensitize } from '@/lib/desensitize';
import type { AIWritingSessionSummary, CompletedInterventionRecord } from '../../api/ai-writing-sessions';
import { AIWritingTimeline } from './AIWritingTimeline';
@@ -79,10 +81,10 @@ export function AIWritingHistoryView({ session }: AIWritingHistoryViewProps) {
{/* 元信息 */}
<div className="px-4 pt-3">
<div className="rounded-lg border bg-muted/30 px-3 py-2.5 space-y-1">
<p className="text-xs font-semibold truncate">{session.draft_title ?? session.title}</p>
<p className="text-xs font-semibold truncate">{desensitize(session.draft_title ?? session.title ?? "")}</p>
{session.user_intent && (
<p className="text-[11px] text-muted-foreground leading-relaxed line-clamp-2">
{session.user_intent}
{desensitize(session.user_intent)}
</p>
)}
{createdAt && (
@@ -4,6 +4,7 @@ import { ChevronsDown, Play, Plus, RotateCcw, Settings2, Square, Trash2 } from '
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { desensitize } from '@/lib/desensitize';
import { cn } from '@/lib/utils';
import {
Dialog,
@@ -251,7 +252,7 @@ export function AIWritingPanel({
<div className="flex flex-col h-full overflow-hidden bg-background">
<div className="flex items-center justify-between px-4 py-2.5 border-b shrink-0">
<span className="text-sm font-semibold truncate flex-1 min-w-0 mr-2">
{historySession.draft_title ?? historySession.title}
{desensitize(historySession.draft_title ?? historySession.title)}
</span>
<div className="flex items-center gap-1.5 shrink-0">
<Button
@@ -501,7 +502,7 @@ export function AIWritingPanel({
<span className="inline-block size-1.5 rounded-full bg-blue-500 animate-pulse" />
)}
{state.status === 'researching' && state.researchProgress
? state.researchProgress
? desensitize(state.researchProgress)
: STATUS_LABEL[state.status] ?? state.status}
</>
)}
@@ -517,16 +518,16 @@ export function AIWritingPanel({
</DialogHeader>
<div className="space-y-3 py-1 text-sm">
<ReqRow label="写作意图">
<p className="text-muted-foreground whitespace-pre-wrap leading-relaxed">{state.request.userIntent}</p>
<p className="text-muted-foreground whitespace-pre-wrap leading-relaxed">{desensitize(state.request.userIntent ?? "")}</p>
</ReqRow>
<ReqRow label="文章类型">
<span className="text-muted-foreground">
{articleTypes.find((t) => t.key === state.request!.articleType)?.label ?? state.request.articleType}
{desensitize(articleTypes.find((t) => t.key === state.request!.articleType)?.label ?? state.request.articleType ?? "")}
</span>
</ReqRow>
{!hideAdvancedFields && (
<ReqRow label="目标读者">
<span className="text-muted-foreground">{state.request.targetAudience}</span>
<span className="text-muted-foreground">{desensitize(state.request.targetAudience ?? "")}</span>
</ReqRow>
)}
<ReqRow label="目标字数">
@@ -546,7 +547,7 @@ export function AIWritingPanel({
)}
{state.request.modelName && (
<ReqRow label="写作模型">
<span className="text-muted-foreground">{state.request.modelName}</span>
<span className="text-muted-foreground">{desensitize(state.request.modelName)}</span>
</ReqRow>
)}
</div>
@@ -4,6 +4,7 @@ import {
ExternalLink, ListTree, FileText,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { desensitize } from '@/lib/desensitize';
import { buildIntranetSourceUrl } from '@/core/config';
import type { AIMaterial, CompletedIntervention } from '../../types/ai-writing';
import { OutlineCard } from './OutlineCard';
@@ -104,7 +105,7 @@ function CompletedMaterialItem({ material }: { material: AIMaterial }) {
)}
>
<div className="flex items-start gap-1.5">
<span className="flex-1 font-medium leading-snug line-clamp-2">{material.title}</span>
<span className="flex-1 font-medium leading-snug line-clamp-2">{desensitize(material.title ?? "")}</span>
{hasDetail && (
<ChevronRight
className={cn(
@@ -115,8 +116,8 @@ function CompletedMaterialItem({ material }: { material: AIMaterial }) {
)}
</div>
<div className="flex items-center gap-2 text-muted-foreground">
<span className="truncate max-w-[100px]">{material.source}</span>
{material.publishedAt && <><span className="text-muted-foreground/40">·</span><span>{material.publishedAt}</span></>}
<span className="truncate max-w-[100px]">{desensitize(material.source ?? "")}</span>
{material.publishedAt && <><span className="text-muted-foreground/40">·</span><span>{desensitize(material.publishedAt)}</span></>}
<div className="flex flex-1 items-center gap-1 min-w-0">
<div className="flex-1 h-1 rounded-full bg-muted overflow-hidden min-w-[40px]">
<div className={cn('h-full rounded-full', barColor)} style={{ width: `${pct}%` }} />
@@ -131,7 +132,7 @@ function CompletedMaterialItem({ material }: { material: AIMaterial }) {
<div className="px-2.5 pb-2 pt-1.5 space-y-1.5 border-t border-border/40">
{material.content && (
<p className="text-muted-foreground leading-relaxed line-clamp-6">
{material.content.slice(0, 300)}
{desensitize(material.content.slice(0, 300))}
{material.content.length > 300 && '…'}
</p>
)}
@@ -169,7 +170,7 @@ function OutlineCompleted({ record, expanded, onToggle }: { record: CompletedInt
已确认大纲
{record.outline?.title && (
<span className="ml-1 text-muted-foreground font-normal">
《{record.outline.title}》
《{desensitize(record.outline.title)}》
</span>
)}
<span className="ml-1 text-muted-foreground font-normal">共 {sectionCount} 章节</span>
@@ -196,7 +197,7 @@ function OutlineCompleted({ record, expanded, onToggle }: { record: CompletedInt
// ── 草稿确认历史卡 ────────────────────────────────────────────────────────────
function DraftCompleted({ record, expanded, onToggle }: { record: CompletedIntervention; expanded: boolean; onToggle: () => void }) {
const title = record.draftTitle ? `《${record.draftTitle}》` : '草稿';
const title = record.draftTitle ? `《${desensitize(record.draftTitle)}》` : '草稿';
const wordCount = record.draftWordCount ?? 0;
const hasReview = !!record.reviewResult;
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { Brain, ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { desensitize } from '@/lib/desensitize';
import type { StreamingDraftSection } from '../../types/ai-writing';
import { CollapseTransition } from './CollapseTransition';
@@ -49,7 +50,7 @@ function SectionThinking({ thinkingText, isDone }: { thinkingText: string; isDon
ref={preRef}
className="text-[10px] font-mono leading-relaxed px-2.5 py-2 text-amber-700/70 dark:text-amber-300/60 whitespace-pre-wrap max-h-32 overflow-y-auto italic"
>
{thinkingText}
{desensitize(thinkingText)}
{!isDone && (
<span className="inline-block w-0.5 h-3 bg-amber-400 ml-0.5 align-text-bottom animate-[blink_1s_step-end_infinite]" />
)}
@@ -71,7 +72,7 @@ function SectionBody({ text, showCursor }: { text: string; showCursor: boolean }
if (heading) {
return (
<p key={i} className="text-xs font-semibold text-foreground mt-2 mb-1">
{line.slice(heading[0].length)}
{desensitize(line.slice(heading[0].length))}
</p>
);
}
@@ -81,7 +82,7 @@ function SectionBody({ text, showCursor }: { text: string; showCursor: boolean }
key={i}
className="text-xs leading-relaxed text-foreground/90 whitespace-pre-wrap m-0 mb-1"
>
{line}
{desensitize(line)}
</p>
);
})}
@@ -108,7 +109,7 @@ export function DraftStreamingView({ title, sections }: DraftStreamingViewProps)
<div className="px-4 pb-4">
<div className="rounded-xl border bg-card shadow-sm p-4 prose prose-sm dark:prose-invert max-w-none">
{title && (
<h1 className="text-base font-bold text-center mb-4 text-foreground">{title}</h1>
<h1 className="text-base font-bold text-center mb-4 text-foreground">{desensitize(title)}</h1>
)}
{sections.map((sec) => (
@@ -117,7 +118,7 @@ export function DraftStreamingView({ title, sections }: DraftStreamingViewProps)
'text-sm font-semibold mb-1.5',
sec.isDone ? 'text-foreground' : 'text-purple-500',
)}>
{sec.sectionTitle}
{desensitize(sec.sectionTitle ?? "")}
</h2>
{/* 该章节的思考过程(可折叠) */}
@@ -1,5 +1,7 @@
import { Sparkles } from 'lucide-react';
import { desensitize } from '@/lib/desensitize';
import type { ParsedIntent } from '../../types/ai-writing';
import type { ArticleType } from '../../api/ai-writing-article-types';
@@ -32,13 +34,13 @@ export function IntentResultCard({
</span>
</div>
<div className="px-3 py-2.5 space-y-1.5 text-xs">
<Row label="文章类型" value={typeLabel} />
<Row label="文章类型" value={desensitize(typeLabel ?? "")} />
{!hideTargetAudience && (
<Row label="目标读者" value={intent.targetAudience} />
<Row label="目标读者" value={desensitize(intent.targetAudience ?? "")} />
)}
<Row label="字数目标" value={`${intent.wordCountTarget} 字`} />
{intent.refinedIntent && (
<Row label="精炼意图" value={intent.refinedIntent} multiline />
<Row label="精炼意图" value={desensitize(intent.refinedIntent)} multiline />
)}
</div>
</div>
@@ -1,6 +1,8 @@
import { useEffect, useRef, useState } from 'react';
import { Sparkles, ChevronDown, ChevronRight, Brain } from 'lucide-react';
import { desensitize } from '@/lib/desensitize';
interface IntentStreamingViewProps {
text: string;
thinkingText?: string;
@@ -54,7 +56,7 @@ export function IntentStreamingView({ text, thinkingText }: IntentStreamingViewP
ref={thinkRef}
className="text-[11px] font-mono leading-relaxed p-3 text-amber-700/70 dark:text-amber-300/60 whitespace-pre-wrap max-h-40 overflow-y-auto italic"
>
{thinkingText}
{desensitize(thinkingText)}
{!text && (
<span className="inline-block w-0.5 h-3.5 bg-amber-400 ml-0.5 align-text-bottom animate-[blink_1s_step-end_infinite]" />
)}
@@ -84,7 +86,7 @@ export function IntentStreamingView({ text, thinkingText }: IntentStreamingViewP
ref={preRef}
className="text-[11px] font-mono leading-relaxed p-3 text-muted-foreground whitespace-pre-wrap max-h-52 overflow-y-auto"
>
{text}
{desensitize(text)}
<span className="inline-block w-0.5 h-3.5 bg-blue-400 ml-0.5 align-text-bottom animate-[blink_1s_step-end_infinite]" />
</pre>
</div>
@@ -6,6 +6,7 @@ import {
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { desensitize } from '@/lib/desensitize';
import type {
AIMaterial, AIOutline, ReviewResult, ReviewIssue,
ProgressEvent, UserInterventionPayload,
@@ -380,7 +381,7 @@ function DraftConfirmCard({ draftTitle, revisionCount, reviewResult, onSubmit, d
<div>
<p className="text-xs font-medium">草稿已完成,请确认后继续</p>
<p className="text-xs text-muted-foreground mt-0.5">
{draftTitle ? `《${draftTitle}》` : '草稿'}
{draftTitle ? `《${desensitize(draftTitle)}》` : '草稿'}
{revisionCount > 0 ? `(第 ${revisionCount} 次修改稿)` : '(初稿)'}
· 右侧可预览完整内容
</p>
@@ -616,8 +617,8 @@ function SectionHelpCard({ blockedSections, onSubmit, disabled }: SectionHelpCar
{blockedSections.map((b) => (
<div key={b.sectionTitle} className="rounded-md border bg-background p-2.5 space-y-2">
<div>
<p className="text-xs font-medium">{b.sectionTitle}</p>
<p className="text-[11px] text-muted-foreground mt-0.5">{b.reason}</p>
<p className="text-xs font-medium">{desensitize(b.sectionTitle ?? "")}</p>
<p className="text-[11px] text-muted-foreground mt-0.5">{desensitize(b.reason ?? "")}</p>
</div>
<div className="flex gap-1">
{DECISION_OPTIONS.map((opt) => {
@@ -1,4 +1,5 @@
import { cn } from '@/lib/utils';
import { desensitize } from '@/lib/desensitize';
import type { ReviewIssue } from '../../types/ai-writing';
interface IssueListProps {
@@ -93,14 +94,14 @@ function IssueGroup({ label, labelClass, items, selectable, selectedIndices, onT
<span className={cn('rounded px-1.5 py-0.5 text-[10px] font-medium', CATEGORY_COLOR[issue.category] ?? 'bg-muted text-muted-foreground')}>
{CATEGORY_LABEL[issue.category] ?? issue.category}
</span>
<span className="text-muted-foreground truncate">{issue.location}</span>
<span className="text-muted-foreground truncate">{desensitize(issue.location ?? "")}</span>
</div>
<p className={cn('text-foreground leading-snug', selectable ? 'pl-5' : '')}>
{issue.issue}
{desensitize(issue.issue ?? "")}
</p>
<p className={cn('text-muted-foreground leading-snug', selectable ? 'pl-5' : '')}>
<span className="text-primary mr-1">→</span>
{issue.suggestion}
{desensitize(issue.suggestion ?? "")}
</p>
</div>
);
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { ChevronRight, ExternalLink } from 'lucide-react';
import { cn } from '@/lib/utils';
import { desensitize } from '@/lib/desensitize';
import { buildIntranetSourceUrl } from '@/core/config';
import type { AIMaterial } from '../../types/ai-writing';
import { CollapseTransition } from './CollapseTransition';
@@ -51,7 +52,7 @@ export function MaterialItem({ material, checked, onToggle }: MaterialItemProps)
className="flex flex-1 items-start gap-1.5 text-left"
>
<span className={cn('flex-1 font-medium leading-snug line-clamp-2', !checked && 'text-muted-foreground')}>
{material.title}
{desensitize(material.title ?? "")}
</span>
<ChevronRight
className={cn(
@@ -64,11 +65,11 @@ export function MaterialItem({ material, checked, onToggle }: MaterialItemProps)
{/* 元信息行 */}
<div className="flex items-center gap-2 px-2.5 pb-2 pl-8">
<span className="text-muted-foreground truncate max-w-[120px]">{material.source}</span>
<span className="text-muted-foreground truncate max-w-[120px]">{desensitize(material.source ?? "")}</span>
{material.publishedAt && (
<>
<span className="text-muted-foreground/40">·</span>
<span className="text-muted-foreground shrink-0">{material.publishedAt}</span>
<span className="text-muted-foreground shrink-0">{desensitize(material.publishedAt)}</span>
</>
)}
{/* 相关度色条 */}
@@ -90,7 +91,7 @@ export function MaterialItem({ material, checked, onToggle }: MaterialItemProps)
<CollapseTransition open={expanded}>
<div className="px-2.5 pb-2.5 pl-8 space-y-1.5 border-t">
<p className="text-muted-foreground leading-relaxed pt-2 line-clamp-6">
{material.content.slice(0, 300)}
{desensitize(material.content.slice(0, 300))}
{material.content.length > 300 && '…'}
</p>
{material.recUuid && (
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { ListTree, Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { desensitize } from '@/lib/desensitize';
import type { AIOutline, OutlineSection as OutlineSectionType } from '../../types/ai-writing';
import { OutlineSection } from './OutlineSection';
import { CollapseTransition } from './CollapseTransition';
@@ -92,7 +93,7 @@ export function OutlineCard({ outline, onChange, readOnly = false }: OutlineCard
className={editable ? 'flex-1 font-semibold cursor-text hover:text-primary' : 'flex-1 font-semibold'}
onClick={() => editable && setEditingTitle(true)}
>
{outline.title || <span className="text-muted-foreground italic font-normal">(未设置标题)</span>}
{outline.title ? desensitize(outline.title) : <span className="text-muted-foreground italic font-normal">(未设置标题)</span>}
</span>
)}
</div>
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { ChevronRight, Plus, X, GripVertical } from 'lucide-react';
import { desensitize } from '@/lib/desensitize';
import { cn } from '@/lib/utils';
import type { OutlineSection as OutlineSectionType } from '../../types/ai-writing';
import { CollapseTransition } from './CollapseTransition';
@@ -100,7 +101,7 @@ export function OutlineSection({
className={cn('flex-1 font-medium', !readOnly && 'cursor-text hover:text-primary')}
onClick={() => !readOnly && setEditingTitle(true)}
>
{section.sectionTitle || <span className="text-muted-foreground italic">(未命名)</span>}
{section.sectionTitle ? desensitize(section.sectionTitle) : <span className="text-muted-foreground italic">(未命名)</span>}
</span>
)}
@@ -119,7 +120,7 @@ export function OutlineSection({
<div key={i} className="flex items-center gap-1.5 pt-1.5">
<span className="text-muted-foreground/40 shrink-0">•</span>
{readOnly ? (
<span className="flex-1 text-muted-foreground">{kp}</span>
<span className="flex-1 text-muted-foreground">{desensitize(kp)}</span>
) : (
<input
value={kp}
@@ -1,6 +1,8 @@
import { useEffect, useRef, useState } from 'react';
import { FileText, ChevronDown, ChevronRight, Brain } from 'lucide-react';
import { desensitize } from '@/lib/desensitize';
interface OutlineStreamingViewProps {
text: string;
thinkingText?: string;
@@ -50,7 +52,7 @@ export function OutlineStreamingView({ text, thinkingText }: OutlineStreamingVie
ref={thinkRef}
className="text-[11px] font-mono leading-relaxed p-3 text-amber-700/70 dark:text-amber-300/60 whitespace-pre-wrap max-h-40 overflow-y-auto italic"
>
{thinkingText}
{desensitize(thinkingText)}
{/* 思考仍在进行时显示光标 */}
{!text && (
<span className="inline-block w-0.5 h-3.5 bg-amber-400 ml-0.5 align-text-bottom animate-[blink_1s_step-end_infinite]" />
@@ -82,7 +84,7 @@ export function OutlineStreamingView({ text, thinkingText }: OutlineStreamingVie
ref={preRef}
className="text-[11px] font-mono leading-relaxed p-3 text-muted-foreground whitespace-pre-wrap max-h-52 overflow-y-auto"
>
{text}
{desensitize(text)}
<span className="inline-block w-0.5 h-3.5 bg-purple-400 ml-0.5 align-text-bottom animate-[blink_1s_step-end_infinite]" />
</pre>
</div>
@@ -1,6 +1,7 @@
import { useEffect, useRef } from 'react';
import { CheckCircle2, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { desensitize } from '@/lib/desensitize';
import type { StreamingReviewItem, ReviewIssue } from '../../types/ai-writing';
const CATEGORY_META: Record<
@@ -41,9 +42,9 @@ function IssueRow({ issue }: { issue: ReviewIssue }) {
)}>
{issue.severity === 'major' ? '严重' : '轻微'}
</span>
<span className="text-muted-foreground">{issue.issue}</span>
<span className="text-muted-foreground">{desensitize(issue.issue ?? "")}</span>
{issue.suggestion && (
<span className="text-orange-600 dark:text-orange-400"> → {issue.suggestion}</span>
<span className="text-orange-600 dark:text-orange-400"> → {desensitize(issue.suggestion)}</span>
)}
</div>
);
@@ -73,10 +74,10 @@ function StreamingText({ item }: { item: StreamingReviewItem }) {
>
{item.thinkingText && (
<span className="italic text-amber-600/70 dark:text-amber-400/60">
{item.thinkingText}
{desensitize(item.thinkingText)}
</span>
)}
{item.streamingText}
{desensitize(item.streamingText ?? "")}
<span className="inline-block w-0.5 h-3 bg-orange-400 ml-0.5 align-text-bottom animate-[blink_1s_step-end_infinite]" />
</pre>
);
@@ -1,5 +1,6 @@
import { CheckCircle2, XCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import { desensitize } from '@/lib/desensitize';
import type { RevisionSummary } from '../../types/ai-writing';
interface RevisionSummaryCardProps {
@@ -49,7 +50,7 @@ export function RevisionSummaryCard({ summary }: RevisionSummaryCardProps) {
)}>
{CATEGORY_LABEL[iss.category] ?? iss.category}
</span>
<span>{iss.issue}</span>
<span>{desensitize(iss.issue ?? "")}</span>
</div>
))}
</div>
@@ -64,7 +65,7 @@ export function RevisionSummaryCard({ summary }: RevisionSummaryCardProps) {
</p>
{skippedIssues.map((iss, i) => (
<div key={i} className="flex items-start gap-1.5 pl-5 text-muted-foreground/60 leading-snug line-through">
<span>{iss.issue}</span>
<span>{desensitize(iss.issue ?? "")}</span>
</div>
))}
</div>
@@ -1,5 +1,6 @@
import { AlertCircle, AlertTriangle, CheckCircle2, Circle, Loader2, PauseCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import { desensitize } from '@/lib/desensitize';
import type { ProgressEvent } from '../../types/ai-writing';
type StepStatus = 'done' | 'running' | 'paused' | 'waiting' | 'error' | 'warning';
@@ -49,7 +50,7 @@ export function TimelineStep({ event, isLast, children }: TimelineStepProps) {
AGENT_COLOR[event.agentName] ?? 'text-muted-foreground',
)}
>
{event.agentName}
{desensitize(event.agentName)}
</span>
)}
@@ -70,11 +71,11 @@ export function TimelineStep({ event, isLast, children }: TimelineStepProps) {
: 'text-amber-600 dark:text-amber-500',
)}
>
{event.title || event.message || (isError ? '出错了' : '提示')}
{desensitize(event.title || event.message || (isError ? '出错了' : '提示'))}
</p>
{event.detail && (
<p className="mt-0.5 text-[11px] leading-relaxed text-muted-foreground break-words">
{event.detail}
{desensitize(event.detail)}
</p>
)}
{/* 接口解析失败时的原始错误响应体,默认折叠,点击展开 */}
@@ -84,13 +85,13 @@ export function TimelineStep({ event, isLast, children }: TimelineStepProps) {
错误返回结果(点击展开)
</summary>
<pre className="mt-1 max-h-60 overflow-auto whitespace-pre-wrap break-all rounded bg-muted/60 p-2 text-[10px] leading-relaxed text-muted-foreground">
{event.raw}
{desensitize(event.raw)}
</pre>
</details>
)}
</div>
) : (
<span className="ml-1.5 text-xs text-muted-foreground">{event.message}</span>
<span className="ml-1.5 text-xs text-muted-foreground">{desensitize(event.message ?? "")}</span>
)}
{children}
@@ -1,4 +1,5 @@
import { cn } from '@/lib/utils';
import { desensitize } from '@/lib/desensitize';
import type { WritingStatus } from '../../types/ai-writing';
interface WritingSkeletonCardProps {
@@ -37,7 +38,7 @@ function MaterialsSkeleton({ researchProgress }: { researchProgress?: string })
{/* 标题行:展示素材收集专家的实时进度 */}
<div className="flex items-center gap-2 text-xs text-blue-500 font-medium">
<ThinkingDots color="bg-blue-400" />
<span>{researchProgress || '素材收集专家搜索中…'}</span>
<span>{researchProgress ? desensitize(researchProgress) : '素材收集专家搜索中…'}</span>
</div>
{/* 关键词标签占位 */}
@@ -4,6 +4,7 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Button } from "@/components/ui/button";
import { desensitize } from "@/lib/desensitize";
import type { CanvasArtifactText } from "../../types/artifact";
import { TiptapEditor } from "../../editor/TiptapEditor";
import { useWritingEditor } from "../../hooks/useWritingEditor";
@@ -247,7 +248,7 @@ export function TextArtifactRenderer({ content, isStreaming }: TextArtifactRende
},
}}
>
{content.fullMarkdown}
{desensitize(content.fullMarkdown)}
</ReactMarkdown>
<span className="inline-block w-0.5 h-4 bg-primary animate-pulse align-middle ml-0.5" />
</div>
+4 -2
View File
@@ -30,6 +30,7 @@ import { useThreadSettings } from "@/core/settings";
import { useThreadStream } from "@/core/threads/hooks";
import { textOfMessage } from "@/core/threads/utils";
import { env } from "@/env";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
export default function AgentChatPage() {
@@ -61,8 +62,9 @@ export default function AgentChatPage() {
);
const { agent, isLoading: isAgentLoading } = useAgent(agent_id);
const agentDisplayName =
agent?.name ?? (isAgentLoading ? t.common.loading : "");
const agentDisplayName = desensitize(
agent?.name ?? (isAgentLoading ? t.common.loading : ""),
);
const { threadId, setThreadId, isNewThread, setIsNewThread } =
useThreadChat();
+2 -1
View File
@@ -17,6 +17,7 @@ import { useI18n } from "@/core/i18n/hooks";
import { useThreads } from "@/core/threads/hooks";
import { usePathOfThread, titleOfThread } from "@/core/threads/utils";
import { formatTimeAgo } from "@/core/utils/datetime";
import { desensitize } from "@/lib/desensitize";
export default function ChatsPage() {
const pathOfThread = usePathOfThread();
@@ -69,7 +70,7 @@ export default function ChatsPage() {
<Link key={thread.thread_id} href={pathOfThread(thread)}>
<div className="flex flex-col gap-2 border-b p-4">
<div>
<div>{titleOfThread(thread)}</div>
<div>{desensitize(titleOfThread(thread))}</div>
</div>
{thread.updated_at && (
<div className="text-muted-foreground text-sm">
+14 -14
View File
@@ -15,20 +15,20 @@ function PageLayout() {
const sidebarEnabled = useMemo(() => readSidebarEnabled(), []);
return (
<div className="h-screen flex bg-muted/20">
{sidebarEnabled && (
<PageSidebar
collapsed={collapsed}
onToggleCollapsed={() => setCollapsed((prev) => !prev)}
/>
)}
<div
className={`flex flex-col flex-1 overflow-hidden transition-[padding] duration-300 ${
!sidebarEnabled ? "" : collapsed ? "pl-16" : "pl-64"
}`}
>
{sidebarEnabled && <Header />}
<main className="flex-1 overflow-hidden">
<div className="h-screen flex flex-col bg-muted/20">
{sidebarEnabled && <Header collapsed={collapsed} />}
<div className="flex flex-1 overflow-hidden">
{sidebarEnabled && (
<PageSidebar
collapsed={collapsed}
onToggleCollapsed={() => setCollapsed((prev) => !prev)}
/>
)}
<main
className={`flex-1 overflow-hidden transition-[padding] duration-300 ${
!sidebarEnabled ? "" : collapsed ? "pl-16" : "pl-64"
}`}
>
<Routes>
<Route index element={<Navigate to="workspace" replace />} />
<Route
@@ -1,4 +1,4 @@
import { Bell, ChevronDown, Moon, Palette, User } from "lucide-react";
import { Bell, ChevronDown, FileText, Moon, Palette, User } from "lucide-react";
import { useState } from "react";
import { NotificationDialog } from "@/components/workspace/notification-dialog";
@@ -16,7 +16,11 @@ const iconButtonClass = (isDark: boolean) =>
: "text-white hover:bg-[var(--brand-950)] active:bg-[#8a0618]",
);
const Header = () => {
type HeaderProps = {
collapsed?: boolean;
};
const Header = ({ collapsed }: HeaderProps) => {
const { theme, toggleTheme } = useTheme();
const isDark = theme === "dark";
const [notificationOpen, setNotificationOpen] = useState(false);
@@ -36,7 +40,23 @@ const Header = () => {
: "border-[var(--brand-950)] bg-[var(--brand-900)]",
)}
>
<div className="flex min-w-0 flex-1 items-center gap-4" />
<div className="flex min-w-0 flex-1 items-center gap-3">
<div
className={cn(
"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg",
isDark
? "bg-gradient-to-br from-blue-500 to-cyan-400"
: "bg-white/20",
)}
>
<FileText className="h-4 w-4 text-white" />
</div>
{!collapsed && (
<span className={cn("text-sm font-semibold", isDark ? "text-white" : "text-white")}>
CM助手
</span>
)}
</div>
<div className="flex shrink-0 items-center gap-2">
<Button