refactor(config): streamline sensitive words editing and simplify UI copy

This commit is contained in:
lzt404 committed 2026-09-23 21:35:59 +08:00
1 parent 8f716f9e01
commit 395ac1c4fc
5 files changed
+242 -93

No files matched your search

+6 -10
View File
@@ -682,16 +682,12 @@ export const jaOverrides = {
'config.tabs.software': 'ソフトウェア設定',
'config.tabs.sensitiveWords': '機密語の難読化',
'config.sensitiveWords.title': 'システムプロンプトの機密語',
'config.sensitiveWords.intro': 'CPA コアの antigravity.sensitive-words と devin.sensitive-words を個別に設定します。1 行に 1 語またはフレーズを入力してください。保存時に空行と前後の空白を除去します。',
'config.sensitiveWords.antigravity': 'Antigravity の機密語の難読化',
'config.sensitiveWords.antigravityDescription': 'システムプロンプト内の一致した語にゼロ幅文字を挿入し、上流の完全一致を避けます。',
'config.sensitiveWords.antigravityPlaceholder': '例:proxy',
'config.sensitiveWords.antigravityHint': '大文字と小文字は区別しません。2 文字未満の項目はコア側で無視されます。',
'config.sensitiveWords.devin': 'Devin のシステムプロンプト機密語',
'config.sensitiveWords.devinDescription': '一致した語を含むシステムプロンプトの行全体を削除し、残りを難読化します。通常のメッセージやツール引数は変更しません。',
'config.sensitiveWords.devinPlaceholder': '例:機密フレーズ',
'config.sensitiveWords.devinHint': '注意:一致するとシステムプロンプトの行全体が削除されます。',
'config.sensitiveWords.restartHint': '実行中のコアが新しい設定を自動で読み込まない場合は、保存後にコアを再起動してください。',
'config.sensitiveWords.addPlaceholder': '語句を入力して Enter で追加',
'config.sensitiveWords.empty': '項目なし',
'config.sensitiveWords.discardTitle': '未保存の機密語を破棄しますか?',
'config.sensitiveWords.discardMessage': '更新すると、未保存の項目は現在のコア設定で上書きされます。',
'config.sensitiveWords.antigravity': 'Antigravity',
'config.sensitiveWords.devin': 'Devin',
'config.error.emptyKey': '認証キーを入力してください',
'config.error.invalidKey': '空白を含まない表示可能な ASCII 文字のみ使用できます',
'config.error.duplicateKey': '同じ認証キーがすでに存在します',
+6 -10
View File
@@ -679,16 +679,12 @@ export const en: Record<MessageKey, string> = {
'config.tabs.software': 'Software Settings',
'config.tabs.sensitiveWords': 'Sensitive Words',
'config.sensitiveWords.title': 'System Prompt Sensitive Words',
'config.sensitiveWords.intro': 'Configure antigravity.sensitive-words and devin.sensitive-words in the CPA core separately. Enter one word or phrase per line; blank lines and surrounding spaces are removed on save.',
'config.sensitiveWords.antigravity': 'Antigravity Word Obfuscation',
'config.sensitiveWords.antigravityDescription': 'Inserts zero-width characters into matching system-prompt terms to reduce exact upstream keyword matches.',
'config.sensitiveWords.antigravityPlaceholder': 'For example: proxy',
'config.sensitiveWords.antigravityHint': 'Case-insensitive; the core ignores entries shorter than two characters.',
'config.sensitiveWords.devin': 'Devin System Prompt Sensitive Words',
'config.sensitiveWords.devinDescription': 'Removes entire system-prompt lines containing a configured term, then obfuscates the remaining prompt. Other messages and tool arguments are not changed.',
'config.sensitiveWords.devinPlaceholder': 'For example: sensitive phrase',
'config.sensitiveWords.devinHint': 'Use caution: a match deletes the entire system-prompt line.',
'config.sensitiveWords.restartHint': 'If the running core does not reload the new configuration automatically, restart it after saving.',
'config.sensitiveWords.addPlaceholder': 'Enter a word and press Enter',
'config.sensitiveWords.empty': 'No entries',
'config.sensitiveWords.discardTitle': 'Discard unsaved sensitive words?',
'config.sensitiveWords.discardMessage': 'Refreshing replaces your unsaved entries with the current core configuration.',
'config.sensitiveWords.antigravity': 'Antigravity',
'config.sensitiveWords.devin': 'Devin',
'config.error.emptyKey': 'The authentication key cannot be empty',
'config.error.invalidKey': 'Use visible ASCII characters only, without spaces',
'config.error.duplicateKey': 'This authentication key already exists',
+6 -10
View File
@@ -678,16 +678,12 @@ export const zhCN = {
'config.tabs.software': '软件设置',
'config.tabs.sensitiveWords': '敏感词混淆',
'config.sensitiveWords.title': '系统提示词敏感词',
'config.sensitiveWords.intro': '分别配置 CPA 内核的 antigravity.sensitive-words 与 devin.sensitive-words。每行输入一个词或短语,保存时自动去除空行和首尾空格。',
'config.sensitiveWords.antigravity': 'Antigravity 敏感词混淆',
'config.sensitiveWords.antigravityDescription': '在系统提示词匹配项中插入零宽字符,降低上游精确关键词匹配。',
'config.sensitiveWords.antigravityPlaceholder': '例如:proxy',
'config.sensitiveWords.antigravityHint': '不区分大小写;少于 2 个字符的词条会被内核忽略。',
'config.sensitiveWords.devin': 'Devin 系统提示词敏感词',
'config.sensitiveWords.devinDescription': '含配置词的整行系统提示词会被删除,其余系统提示词再进行混淆;普通消息及工具参数不受影响。',
'config.sensitiveWords.devinPlaceholder': '例如:敏感短语',
'config.sensitiveWords.devinHint': '请谨慎配置:命中内容会导致整行系统提示词被删除。',
'config.sensitiveWords.restartHint': '保存后若运行中的内核未自动加载新配置,请重启内核。',
'config.sensitiveWords.addPlaceholder': '输入词条,回车添加',
'config.sensitiveWords.empty': '暂无词条',
'config.sensitiveWords.discardTitle': '放弃未保存的敏感词?',
'config.sensitiveWords.discardMessage': '刷新会用当前内核配置覆盖尚未保存的词条。',
'config.sensitiveWords.antigravity': 'Antigravity',
'config.sensitiveWords.devin': 'Devin',
'config.error.emptyKey': '鉴权密钥不能为空',
'config.error.invalidKey': '只能使用 ASCII 可见字符,且不能包含空格',
'config.error.duplicateKey': '该鉴权密钥已经存在',
+175 -35
View File
@@ -1,62 +1,204 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState, type ClipboardEvent } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { Check, RefreshCw, ShieldAlert } from 'lucide-react';
import { Check, Plus, RefreshCw, ShieldAlert, Trash2 } from 'lucide-react';
import { useI18n } from '../i18n';
import { useConfirmation } from '../components/ConfirmationDialog';
type SensitiveWordsSettings = {
antigravitySensitiveWords: string[];
devinSensitiveWords: string[];
};
const wordsFromText = (text: string): string[] => text.split(/\r?\n/).map((word) => word.trim()).filter(Boolean);
type Provider = 'antigravity' | 'devin';
type WordEntry = { id: number; value: string };
type WordDraft = Record<Provider, WordEntry[]>;
const normalizedWords = (entries: WordEntry[]) => entries.map(({ value }) => value.trim()).filter(Boolean);
type WordEditorProps = {
provider: Provider;
entries: WordEntry[];
composer: string;
disabled: boolean;
onAdd: (provider: Provider, value: string) => void;
onCompose: (provider: Provider, value: string) => void;
onChange: (provider: Provider, id: number, value: string) => void;
onRemove: (provider: Provider, id: number) => void;
onPaste: (provider: Provider, id: number, event: ClipboardEvent<HTMLInputElement>) => void;
};
function WordEditor({ provider, entries, composer, disabled, onAdd, onCompose, onChange, onRemove, onPaste }: WordEditorProps) {
const { t } = useI18n();
const title = t(provider === 'antigravity' ? 'config.sensitiveWords.antigravity' : 'config.sensitiveWords.devin');
return (
<section className="config-sensitive-words-card">
<div className="config-sensitive-words-card-heading">
<h3>{title}</h3>
<span className="config-sensitive-words-count" aria-label={`${entries.length} ${title}`}>{entries.length}</span>
</div>
<div className="config-sensitive-words-composer">
<input
className="config-network-input"
type="text"
aria-label={`${title} ${t('common.add')}`}
value={composer}
disabled={disabled}
placeholder={t('config.sensitiveWords.addPlaceholder')}
onChange={(event) => onCompose(provider, event.currentTarget.value)}
onKeyDown={(event) => {
if (event.key !== 'Enter' || event.nativeEvent.isComposing) return;
event.preventDefault();
onAdd(provider, composer);
}}
onPaste={(event) => {
const pasted = event.clipboardData.getData('text');
if (!/[\r\n]/.test(pasted)) return;
event.preventDefault();
const input = event.currentTarget;
const start = input.selectionStart ?? input.value.length;
const end = input.selectionEnd ?? start;
onAdd(provider, input.value.slice(0, start) + pasted + input.value.slice(end));
}}
/>
<button type="button" className="icon-button quiet" disabled={disabled || !composer.trim()}
title={t('common.add')} aria-label={`${t('common.add')} ${title}`}
onClick={() => onAdd(provider, composer)}>
<Plus size={16} aria-hidden="true" />
</button>
</div>
{entries.length === 0 ? (
<p className="config-sensitive-words-empty">{t('config.sensitiveWords.empty')}</p>
) : (
<div className="config-sensitive-words-list">
{entries.map((entry, index) => (
<div className="config-sensitive-words-row" key={entry.id}>
<input
id={`config-${provider}-word-${entry.id}`}
className="config-network-input"
type="text"
aria-label={`${title} ${index + 1}`}
value={entry.value}
disabled={disabled}
onChange={(event) => onChange(provider, entry.id, event.currentTarget.value)}
onPaste={(event) => onPaste(provider, entry.id, event)}
/>
<button type="button" className="icon-button quiet" disabled={disabled}
title={t('common.delete')} aria-label={`${t('common.delete')} ${title} ${index + 1}`}
onClick={() => onRemove(provider, entry.id)}>
<Trash2 size={15} aria-hidden="true" />
</button>
</div>
))}
</div>
)}
</section>
);
}
export function SensitiveWordsPage() {
const { t } = useI18n();
const { askConfirmation, confirmationDialog } = useConfirmation();
const nextEntryId = useRef(0);
const [saved, setSaved] = useState<SensitiveWordsSettings | null>(null);
const [antigravityDraft, setAntigravityDraft] = useState('');
const [devinDraft, setDevinDraft] = useState('');
const [draft, setDraft] = useState<WordDraft>({ antigravity: [], devin: [] });
const [composer, setComposer] = useState<Record<Provider, string>>({ antigravity: '', devin: '' });
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [savedNotice, setSavedNotice] = useState(false);
const replaceDraft = useCallback((settings: SensitiveWordsSettings) => {
const toEntries = (words: string[]) => words.map((value) => ({ id: nextEntryId.current++, value }));
setDraft({
antigravity: toEntries(settings.antigravitySensitiveWords),
devin: toEntries(settings.devinSensitiveWords),
});
setComposer({ antigravity: '', devin: '' });
}, []);
const load = useCallback(async () => {
setLoading(true);
setError('');
try {
const result = await invoke<SensitiveWordsSettings>('get_core_sensitive_words_settings');
setSaved(result);
setAntigravityDraft(result.antigravitySensitiveWords.join('\n'));
setDevinDraft(result.devinSensitiveWords.join('\n'));
replaceDraft(result);
setSavedNotice(false);
} catch (cause) {
setSaved(null);
setError(String(cause));
} finally {
setLoading(false);
}
}, []);
}, [replaceDraft]);
useEffect(() => { void load(); }, [load]);
const antigravitySensitiveWords = wordsFromText(antigravityDraft);
const devinSensitiveWords = wordsFromText(devinDraft);
const words: SensitiveWordsSettings = {
antigravitySensitiveWords: [...normalizedWords(draft.antigravity), ...(composer.antigravity.trim() ? [composer.antigravity.trim()] : [])],
devinSensitiveWords: [...normalizedWords(draft.devin), ...(composer.devin.trim() ? [composer.devin.trim()] : [])],
};
const dirty = saved !== null && (
JSON.stringify(antigravitySensitiveWords) !== JSON.stringify(saved.antigravitySensitiveWords)
|| JSON.stringify(devinSensitiveWords) !== JSON.stringify(saved.devinSensitiveWords)
JSON.stringify(words.antigravitySensitiveWords) !== JSON.stringify(saved.antigravitySensitiveWords)
|| JSON.stringify(words.devinSensitiveWords) !== JSON.stringify(saved.devinSensitiveWords)
);
const changeWord = (provider: Provider, id: number, value: string) => {
setDraft((current) => ({
...current,
[provider]: current[provider].map((entry) => entry.id === id ? { ...entry, value } : entry),
}));
setSavedNotice(false);
};
const composeWord = (provider: Provider, value: string) => {
setComposer((current) => ({ ...current, [provider]: value }));
setSavedNotice(false);
};
const addWords = (provider: Provider, value: string) => {
const entries = value.split(/\r\n|\n|\r/).map((word) => word.trim()).filter(Boolean)
.map((word) => ({ id: nextEntryId.current++, value: word }));
if (entries.length === 0) return;
setDraft((current) => ({ ...current, [provider]: [...current[provider], ...entries] }));
setComposer((current) => ({ ...current, [provider]: '' }));
setSavedNotice(false);
};
const removeWord = (provider: Provider, id: number) => {
setDraft((current) => ({ ...current, [provider]: current[provider].filter((entry) => entry.id !== id) }));
setSavedNotice(false);
};
const pasteWords = (provider: Provider, id: number, event: ClipboardEvent<HTMLInputElement>) => {
const pasted = event.clipboardData.getData('text');
if (!/[\r\n]/.test(pasted)) return;
event.preventDefault();
const input = event.currentTarget;
const lines = pasted.split(/\r\n|\n|\r/).filter((line) => line.trim());
if (lines.length === 0) return;
const start = input.selectionStart ?? input.value.length;
const end = input.selectionEnd ?? start;
const entries = lines.map((value, index) => ({ id: index === 0 ? id : nextEntryId.current++, value }));
entries[0].value = input.value.slice(0, start) + entries[0].value;
entries[entries.length - 1].value += input.value.slice(end);
setDraft((current) => ({
...current,
[provider]: current[provider].flatMap((entry) => entry.id === id ? entries : [entry]),
}));
setSavedNotice(false);
};
const save = async () => {
if (!dirty || saving) return;
setSaving(true);
setError('');
try {
const result = await invoke<SensitiveWordsSettings>('save_core_sensitive_words_settings', {
settings: { antigravitySensitiveWords, devinSensitiveWords },
settings: words,
});
setSaved(result);
setAntigravityDraft(result.antigravitySensitiveWords.join('\n'));
setDevinDraft(result.devinSensitiveWords.join('\n'));
replaceDraft(result);
setSavedNotice(true);
} catch (cause) {
setError(String(cause));
@@ -65,6 +207,15 @@ export function SensitiveWordsPage() {
}
};
const refresh = async () => {
if (dirty && !await askConfirmation({
title: t('config.sensitiveWords.discardTitle'),
message: t('config.sensitiveWords.discardMessage'),
confirmText: t('common.refresh'),
})) return;
await load();
};
return (
<section className="panel config-sensitive-words-panel">
<div className="config-panel-heading">
@@ -76,7 +227,7 @@ export function SensitiveWordsPage() {
{dirty ? <span className="state-pill">{t('config.network.unsaved')}</span>
: savedNotice ? <span className="state-pill success">{t('config.network.saved')}</span> : null}
<button type="button" className="icon-button quiet" title={t('common.refresh')} aria-label={t('common.refresh')}
disabled={loading || saving} onClick={() => void load()}>
disabled={loading || saving} onClick={() => void refresh()}>
<RefreshCw size={16} aria-hidden="true" />
</button>
<button type="button" className="primary-button compact-button" disabled={loading || saving || !dirty}
@@ -86,31 +237,20 @@ export function SensitiveWordsPage() {
</button>
</div>
</div>
<p className="config-sensitive-words-intro">{t('config.sensitiveWords.intro')}</p>
{error ? <div className="config-form-message error" role="alert">{error}</div> : null}
{loading ? <p>{t('common.loading')}</p> : saved === null ? (
<button type="button" className="secondary-button compact-button" onClick={() => void load()}>{t('common.retry')}</button>
) : (
<div className="config-sensitive-words-grid">
<div className="config-sensitive-words-card">
<label htmlFor="config-antigravity-sensitive-words">{t('config.sensitiveWords.antigravity')}</label>
<p>{t('config.sensitiveWords.antigravityDescription')}</p>
<textarea id="config-antigravity-sensitive-words" className="config-network-input config-sensitive-words-input"
value={antigravityDraft} disabled={saving} placeholder={t('config.sensitiveWords.antigravityPlaceholder')}
onChange={(event) => { setAntigravityDraft(event.currentTarget.value); setSavedNotice(false); }} />
<small>{t('config.sensitiveWords.antigravityHint')}</small>
</div>
<div className="config-sensitive-words-card">
<label htmlFor="config-devin-sensitive-words">{t('config.sensitiveWords.devin')}</label>
<p>{t('config.sensitiveWords.devinDescription')}</p>
<textarea id="config-devin-sensitive-words" className="config-network-input config-sensitive-words-input"
value={devinDraft} disabled={saving} placeholder={t('config.sensitiveWords.devinPlaceholder')}
onChange={(event) => { setDevinDraft(event.currentTarget.value); setSavedNotice(false); }} />
<small>{t('config.sensitiveWords.devinHint')}</small>
</div>
<WordEditor provider="antigravity" entries={draft.antigravity} composer={composer.antigravity}
disabled={saving} onAdd={addWords} onCompose={composeWord} onChange={changeWord}
onRemove={removeWord} onPaste={pasteWords} />
<WordEditor provider="devin" entries={draft.devin} composer={composer.devin}
disabled={saving} onAdd={addWords} onCompose={composeWord} onChange={changeWord}
onRemove={removeWord} onPaste={pasteWords} />
</div>
)}
{saved ? <small className="config-sensitive-words-intro">{t('config.sensitiveWords.restartHint')}</small> : null}
{confirmationDialog}
</section>
);
}
+49 -28
View File
@@ -3260,51 +3260,73 @@ textarea:focus-visible {
.config-sensitive-words-panel {
display: grid;
width: min(960px, 100%);
gap: 18px;
width: min(780px, 100%);
gap: 16px;
margin: 0 auto;
}
.config-sensitive-words-intro,
.config-sensitive-words-card p,
.config-sensitive-words-card small {
color: var(--theme-7a746c);
font-size: var(--font-size-meta);
line-height: var(--line-height-body);
}
.config-sensitive-words-intro,
.config-sensitive-words-card p {
margin: 0;
}
.config-sensitive-words-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
gap: 12px;
}
.config-sensitive-words-card {
display: grid;
min-width: 0;
align-content: start;
gap: 10px;
padding: 18px;
gap: 12px;
padding: 16px;
border: 1px solid var(--theme-e3e1db);
border-radius: 8px;
background: var(--theme-faf8f2);
background: var(--theme-fffdf8);
}
.config-sensitive-words-card label {
.config-sensitive-words-card-heading {
display: flex;
align-items: center;
gap: 8px;
}
.config-sensitive-words-card-heading h3 {
margin: 0;
color: var(--theme-39352f);
font-size: var(--font-size-body);
font-weight: var(--font-weight-semibold);
}
.config-sensitive-words-input {
height: 168px;
padding: 10px 11px;
resize: vertical;
line-height: var(--line-height-body);
.config-sensitive-words-count {
min-width: 22px;
padding: 2px 6px;
border-radius: 10px;
background: var(--theme-f4f1ea);
color: var(--theme-7a746c);
font-size: var(--font-size-meta);
text-align: center;
}
.config-sensitive-words-composer,
.config-sensitive-words-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 38px;
align-items: center;
gap: 8px;
}
.config-sensitive-words-composer .icon-button,
.config-sensitive-words-row .icon-button {
width: 38px;
height: 38px;
}
.config-sensitive-words-list {
display: grid;
gap: 6px;
}
.config-sensitive-words-empty {
margin: 0;
padding: 6px 0;
color: var(--theme-7a746c);
font-size: var(--font-size-meta);
}
.config-software-panel > .config-panel-heading {
@@ -14649,7 +14671,6 @@ body.table-col-resizing * {
}
.config-network-grid,
.config-sensitive-words-grid,
.config-tls-fields,
.config-management-form,
.client-api-grid,