feat(config): add sensitive words settings for Antigravity and Devin
This commit is contained in:
1 parent
1610fc199f
commit
8f716f9e01
10 files changed
+513
-3
No files matched your search
@@ -21,6 +21,20 @@ pub(crate) fn save_core_tls_settings(
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn get_core_sensitive_words_settings() -> Result<CoreSensitiveWordsSettings, String> {
|
||||
read_core_sensitive_words_settings()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn save_core_sensitive_words_settings(
|
||||
settings: CoreSensitiveWordsSettings,
|
||||
) -> Result<CoreSensitiveWordsSettings, String> {
|
||||
let normalized = normalize_core_sensitive_words_settings(settings);
|
||||
patch_core_sensitive_words_settings(&normalized)?;
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
pub(crate) fn detect_lan_ipv4() -> Option<Ipv4Addr> {
|
||||
for target in ["192.0.2.1:80", "8.8.8.8:80"] {
|
||||
let Ok(socket) = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)) else {
|
||||
|
||||
@@ -224,6 +224,172 @@ pub(crate) fn patch_core_tls_settings(settings: &CoreTlsSettings) -> Result<(),
|
||||
write_yaml_if_changed(&config_path, &updated).map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_core_sensitive_words_settings(
|
||||
settings: CoreSensitiveWordsSettings,
|
||||
) -> CoreSensitiveWordsSettings {
|
||||
let normalize = |words: Vec<String>| {
|
||||
words
|
||||
.into_iter()
|
||||
.map(|word| word.trim().to_string())
|
||||
.filter(|word| !word.is_empty())
|
||||
.collect()
|
||||
};
|
||||
CoreSensitiveWordsSettings {
|
||||
antigravity_sensitive_words: normalize(settings.antigravity_sensitive_words),
|
||||
devin_sensitive_words: normalize(settings.devin_sensitive_words),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn core_sensitive_words_settings_from_value(
|
||||
document: &serde_norway::Value,
|
||||
) -> Result<CoreSensitiveWordsSettings, String> {
|
||||
let root = document
|
||||
.as_mapping()
|
||||
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
|
||||
let read_words = |provider: &str| -> Result<Vec<String>, String> {
|
||||
let Some(section) = yaml_mapping_value(root, provider) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
if section.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let section = section
|
||||
.as_mapping()
|
||||
.ok_or_else(|| format!("{provider} 必须是 YAML 映射"))?;
|
||||
let Some(value) = yaml_mapping_value(section, "sensitive-words") else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
if value.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
value
|
||||
.as_sequence()
|
||||
.ok_or_else(|| format!("{provider}.sensitive-words 必须是字符串列表"))?
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
entry
|
||||
.as_str()
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| format!("{provider}.sensitive-words 必须是字符串列表"))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
Ok(CoreSensitiveWordsSettings {
|
||||
antigravity_sensitive_words: read_words("antigravity")?,
|
||||
devin_sensitive_words: read_words("devin")?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn read_core_sensitive_words_settings() -> Result<CoreSensitiveWordsSettings, String> {
|
||||
let _config_guard = lock_core_config_file()?;
|
||||
let (_, document) = read_core_config_document()?;
|
||||
core_sensitive_words_settings_from_value(document.get())
|
||||
}
|
||||
|
||||
pub(crate) fn patch_core_sensitive_words_yaml(
|
||||
content: &str,
|
||||
settings: &CoreSensitiveWordsSettings,
|
||||
) -> Result<Option<String>, String> {
|
||||
let original = serde_norway::from_str::<serde_norway::Value>(content)
|
||||
.map_err(|error| format!("解析内核配置失败: {error}"))?;
|
||||
let root = original
|
||||
.as_mapping()
|
||||
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
|
||||
let mut prepared = content.to_string();
|
||||
for (provider, words) in [
|
||||
("antigravity", &settings.antigravity_sensitive_words),
|
||||
("devin", &settings.devin_sensitive_words),
|
||||
] {
|
||||
if words.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Some(section) = yaml_mapping_value(root, provider) else {
|
||||
continue;
|
||||
};
|
||||
let needs_normalization = section.is_null()
|
||||
|| section
|
||||
.as_mapping()
|
||||
.and_then(|mapping| yaml_mapping_value(mapping, "sensitive-words"))
|
||||
.is_some_and(serde_norway::Value::is_null);
|
||||
if !needs_normalization {
|
||||
continue;
|
||||
}
|
||||
let mut section = if section.is_null() {
|
||||
serde_norway::Mapping::new()
|
||||
} else {
|
||||
section
|
||||
.as_mapping()
|
||||
.ok_or_else(|| format!("{provider} 必须是 YAML 映射"))?
|
||||
.clone()
|
||||
};
|
||||
section.insert(
|
||||
yaml_key("sensitive-words"),
|
||||
serde_norway::to_value(words).map_err(|error| format!("序列化敏感词失败: {error}"))?,
|
||||
);
|
||||
let mut replacement = serde_norway::Mapping::new();
|
||||
replacement.insert(yaml_key(provider), serde_norway::Value::Mapping(section));
|
||||
let block = serde_norway::to_string(&replacement)
|
||||
.map_err(|error| format!("序列化内核配置区段失败: {error}"))?;
|
||||
prepared = replace_top_level_yaml_block(&prepared, provider, &block);
|
||||
}
|
||||
let updated = patch_core_yaml_document(&prepared, |document| {
|
||||
let mut changed = false;
|
||||
for (provider, words) in [
|
||||
("antigravity", &settings.antigravity_sensitive_words),
|
||||
("devin", &settings.devin_sensitive_words),
|
||||
] {
|
||||
let root = document
|
||||
.as_mapping()
|
||||
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
|
||||
let section = yaml_mapping_value(root, provider);
|
||||
let current_words = section
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.and_then(|mapping| yaml_mapping_value(mapping, "sensitive-words"));
|
||||
if words.is_empty() && current_words.is_none_or(serde_norway::Value::is_null) {
|
||||
continue;
|
||||
}
|
||||
if section.is_none_or(serde_norway::Value::is_null) {
|
||||
changed |= set_core_yaml_top_level_value(
|
||||
document,
|
||||
provider,
|
||||
serde_norway::Value::Mapping(serde_norway::Mapping::new()),
|
||||
)?;
|
||||
}
|
||||
changed |= set_core_yaml_nested_value(
|
||||
document,
|
||||
provider,
|
||||
"sensitive-words",
|
||||
serde_norway::Value::Sequence(
|
||||
words
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(serde_norway::Value::String)
|
||||
.collect(),
|
||||
),
|
||||
)?;
|
||||
}
|
||||
Ok(changed)
|
||||
})?
|
||||
.unwrap_or(prepared);
|
||||
Ok((updated != content).then_some(updated))
|
||||
}
|
||||
|
||||
pub(crate) fn patch_core_sensitive_words_settings(
|
||||
settings: &CoreSensitiveWordsSettings,
|
||||
) -> Result<(), String> {
|
||||
let _config_guard = lock_core_config_file()?;
|
||||
let config_path = core_install_dir()?.join(CORE_CONFIG_FILE);
|
||||
if !config_path.is_file() {
|
||||
return Err("内核配置尚未生成,请先启动 CPA 内核".to_string());
|
||||
}
|
||||
let content = fs::read_to_string(&config_path)
|
||||
.map_err(|error| format!("读取内核配置失败 {}: {error}", path_to_string(&config_path)))?;
|
||||
if let Some(updated) = patch_core_sensitive_words_yaml(&content, settings)? {
|
||||
write_yaml_if_changed(&config_path, &updated)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn patch_core_api_keys(api_keys: &[String]) -> Result<(), String> {
|
||||
let _config_guard = lock_core_config_file()?;
|
||||
let config_path = core_install_dir()?.join(CORE_CONFIG_FILE);
|
||||
|
||||
@@ -1536,6 +1536,13 @@ struct CoreTlsSettings {
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CoreSensitiveWordsSettings {
|
||||
antigravity_sensitive_words: Vec<String>,
|
||||
devin_sensitive_words: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CoreLoggingSettingsInput {
|
||||
@@ -2665,6 +2672,8 @@ fn main() {
|
||||
save_session_routing_settings,
|
||||
get_core_tls_settings,
|
||||
save_core_tls_settings,
|
||||
get_core_sensitive_words_settings,
|
||||
save_core_sensitive_words_settings,
|
||||
get_core_config_settings,
|
||||
save_core_logging_settings,
|
||||
set_core_request_log,
|
||||
|
||||
@@ -1047,6 +1047,96 @@ fn tls_patch_preserves_unrelated_yaml_and_paths_when_disabled() {
|
||||
assert_eq!(document["custom"]["keep"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sensitive_words_patch_preserves_other_config_and_clears_each_provider_independently() {
|
||||
let input = "# keep this comment\nantigravity:\n sensitive-words:\n - old\n signature-cache: true\ndevin:\n sensitive-words:\n - old-devin\ncustom:\n keep: true\n";
|
||||
let settings = normalize_core_sensitive_words_settings(CoreSensitiveWordsSettings {
|
||||
antigravity_sensitive_words: vec![" Hermes ".into(), "".into(), "Nous Research".into()],
|
||||
devin_sensitive_words: Vec::new(),
|
||||
});
|
||||
let patched = patch_core_sensitive_words_yaml(input, &settings)
|
||||
.unwrap()
|
||||
.expect("sensitive words should change");
|
||||
let document = serde_norway::from_str::<serde_norway::Value>(&patched).unwrap();
|
||||
assert!(patched.contains("# keep this comment"));
|
||||
assert_eq!(document["custom"]["keep"], true);
|
||||
assert_eq!(document["antigravity"]["signature-cache"], true);
|
||||
assert_eq!(
|
||||
document["antigravity"]["sensitive-words"],
|
||||
serde_norway::to_value(vec!["Hermes", "Nous Research"]).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
document["devin"]["sensitive-words"],
|
||||
serde_norway::to_value(Vec::<String>::new()).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
core_sensitive_words_settings_from_value(&document).unwrap(),
|
||||
settings
|
||||
);
|
||||
assert!(patch_core_sensitive_words_yaml(&patched, &settings)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sensitive_words_patch_adds_missing_sections_without_touching_unrelated_fields() {
|
||||
let input = "# config\ndebug: true\n";
|
||||
let empty = CoreSensitiveWordsSettings::default();
|
||||
assert!(patch_core_sensitive_words_yaml(input, &empty)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
let settings = CoreSensitiveWordsSettings {
|
||||
antigravity_sensitive_words: vec!["proxy".into()],
|
||||
devin_sensitive_words: vec!["Claude Code".into()],
|
||||
};
|
||||
let patched = patch_core_sensitive_words_yaml(input, &settings)
|
||||
.unwrap()
|
||||
.expect("missing sections should be created");
|
||||
let document = serde_norway::from_str::<serde_norway::Value>(&patched).unwrap();
|
||||
assert!(patched.contains("# config"));
|
||||
assert_eq!(document["debug"], true);
|
||||
assert_eq!(
|
||||
core_sensitive_words_settings_from_value(&document).unwrap(),
|
||||
settings
|
||||
);
|
||||
|
||||
let invalid =
|
||||
serde_norway::from_str::<serde_norway::Value>("devin:\n sensitive-words: not-a-list\n")
|
||||
.unwrap();
|
||||
assert!(core_sensitive_words_settings_from_value(&invalid).is_err());
|
||||
let empty =
|
||||
serde_norway::from_str::<serde_norway::Value>("antigravity:\n sensitive-words:\ndevin:\n")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
core_sensitive_words_settings_from_value(&empty).unwrap(),
|
||||
CoreSensitiveWordsSettings::default()
|
||||
);
|
||||
let updated =
|
||||
patch_core_sensitive_words_yaml("antigravity:\n sensitive-words:\ndevin:\n", &settings)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let parsed = serde_norway::from_str::<serde_norway::Value>(&updated).unwrap();
|
||||
assert_eq!(
|
||||
core_sensitive_words_settings_from_value(&parsed).unwrap(),
|
||||
settings
|
||||
);
|
||||
let only_devin = CoreSensitiveWordsSettings {
|
||||
antigravity_sensitive_words: Vec::new(),
|
||||
devin_sensitive_words: vec!["another phrase".into()],
|
||||
};
|
||||
let patched =
|
||||
patch_core_sensitive_words_yaml("antigravity:\n sensitive-words:\ndevin:\n", &only_devin)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let parsed = serde_norway::from_str::<serde_norway::Value>(&patched).unwrap();
|
||||
assert_eq!(
|
||||
core_sensitive_words_settings_from_value(&parsed).unwrap(),
|
||||
only_devin
|
||||
);
|
||||
assert!(parsed["antigravity"]["sensitive-words"].is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enabled_tls_requires_both_paths() {
|
||||
let missing_key = CoreTlsSettings {
|
||||
|
||||
@@ -680,6 +680,18 @@ export const jaOverrides = {
|
||||
'config.tabs.network': 'ネットワーク設定',
|
||||
'config.tabs.routing': 'セッションルーティング',
|
||||
'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.error.emptyKey': '認証キーを入力してください',
|
||||
'config.error.invalidKey': '空白を含まない表示可能な ASCII 文字のみ使用できます',
|
||||
'config.error.duplicateKey': '同じ認証キーがすでに存在します',
|
||||
|
||||
@@ -677,6 +677,18 @@ export const en: Record<MessageKey, string> = {
|
||||
'config.tabs.network': 'Network Settings',
|
||||
'config.tabs.routing': 'Session Routing',
|
||||
'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.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',
|
||||
|
||||
@@ -676,6 +676,18 @@ export const zhCN = {
|
||||
'config.tabs.network': '网络设置',
|
||||
'config.tabs.routing': '会话路由',
|
||||
'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.error.emptyKey': '鉴权密钥不能为空',
|
||||
'config.error.invalidKey': '只能使用 ASCII 可见字符,且不能包含空格',
|
||||
'config.error.duplicateKey': '该鉴权密钥已经存在',
|
||||
|
||||
@@ -35,6 +35,7 @@ import { useI18n } from '../i18n';
|
||||
import { MessageNotice, FloatingNotice, useAppNotice } from '../appNotice';
|
||||
import { webUiManagementUrl } from '../services/clientAccess';
|
||||
import { ThinkingAliasesPage } from './ThinkingAliasesPage';
|
||||
import { SensitiveWordsPage } from './SensitiveWordsPage';
|
||||
|
||||
type CoreConfigSettings = {
|
||||
apiKeys: CoreApiKey[];
|
||||
@@ -78,7 +79,7 @@ type ConfigAction =
|
||||
| 'tls'
|
||||
| 'software'
|
||||
| null;
|
||||
type ConfigSubpage = 'general' | 'network' | 'routing' | 'software' | 'aliases';
|
||||
type ConfigSubpage = 'general' | 'network' | 'routing' | 'software' | 'aliases' | 'sensitive-words';
|
||||
type CloseBehavior = 'ask' | 'exit' | 'minimize-to-tray';
|
||||
type NetworkDraftField =
|
||||
| 'port'
|
||||
@@ -190,6 +191,7 @@ export function ConfigPanelPage() {
|
||||
<FloatingNotice key={feedback.revision} notice={feedback.notice} onDismiss={feedback.clearNotice} />
|
||||
);
|
||||
const [activeSubpage, setActiveSubpage] = useState<ConfigSubpage>('general');
|
||||
const [sensitiveWordsVisited, setSensitiveWordsVisited] = useState(false);
|
||||
const [portDraft, setPortDraft] = useState('8317');
|
||||
const [hostDraft, setHostDraft] = useState('127.0.0.1');
|
||||
const [proxyUrlDraft, setProxyUrlDraft] = useState('');
|
||||
@@ -1017,6 +1019,21 @@ export function ConfigPanelPage() {
|
||||
>
|
||||
{t('config.tabs.software')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="config-subpage-tab-sensitive-words"
|
||||
role="tab"
|
||||
className={activeSubpage === 'sensitive-words' ? 'active' : ''}
|
||||
aria-selected={activeSubpage === 'sensitive-words'}
|
||||
aria-controls="config-subpage-panel-sensitive-words"
|
||||
tabIndex={activeSubpage === 'sensitive-words' ? 0 : -1}
|
||||
onClick={() => {
|
||||
setSensitiveWordsVisited(true);
|
||||
setActiveSubpage('sensitive-words');
|
||||
}}
|
||||
>
|
||||
{t('config.tabs.sensitiveWords')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeSubpage === 'general' ? (
|
||||
@@ -2110,7 +2127,7 @@ export function ConfigPanelPage() {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : (
|
||||
) : activeSubpage === 'aliases' ? (
|
||||
<div
|
||||
className="config-subpage-panel"
|
||||
id="config-subpage-panel-aliases"
|
||||
@@ -2119,7 +2136,19 @@ export function ConfigPanelPage() {
|
||||
>
|
||||
<ThinkingAliasesPage />
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{sensitiveWordsVisited ? (
|
||||
<div
|
||||
className="config-subpage-panel"
|
||||
id="config-subpage-panel-sensitive-words"
|
||||
role="tabpanel"
|
||||
aria-labelledby="config-subpage-tab-sensitive-words"
|
||||
style={{ display: activeSubpage === 'sensitive-words' ? undefined : 'none' }}
|
||||
>
|
||||
<SensitiveWordsPage />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{addDialogOpen ? (
|
||||
<div className="config-dialog-backdrop" onMouseDown={(event) => {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { Check, RefreshCw, ShieldAlert } from 'lucide-react';
|
||||
import { useI18n } from '../i18n';
|
||||
|
||||
type SensitiveWordsSettings = {
|
||||
antigravitySensitiveWords: string[];
|
||||
devinSensitiveWords: string[];
|
||||
};
|
||||
|
||||
const wordsFromText = (text: string): string[] => text.split(/\r?\n/).map((word) => word.trim()).filter(Boolean);
|
||||
|
||||
export function SensitiveWordsPage() {
|
||||
const { t } = useI18n();
|
||||
const [saved, setSaved] = useState<SensitiveWordsSettings | null>(null);
|
||||
const [antigravityDraft, setAntigravityDraft] = useState('');
|
||||
const [devinDraft, setDevinDraft] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [savedNotice, setSavedNotice] = useState(false);
|
||||
|
||||
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'));
|
||||
setSavedNotice(false);
|
||||
} catch (cause) {
|
||||
setSaved(null);
|
||||
setError(String(cause));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { void load(); }, [load]);
|
||||
|
||||
const antigravitySensitiveWords = wordsFromText(antigravityDraft);
|
||||
const devinSensitiveWords = wordsFromText(devinDraft);
|
||||
const dirty = saved !== null && (
|
||||
JSON.stringify(antigravitySensitiveWords) !== JSON.stringify(saved.antigravitySensitiveWords)
|
||||
|| JSON.stringify(devinSensitiveWords) !== JSON.stringify(saved.devinSensitiveWords)
|
||||
);
|
||||
|
||||
const save = async () => {
|
||||
if (!dirty || saving) return;
|
||||
setSaving(true);
|
||||
setError('');
|
||||
try {
|
||||
const result = await invoke<SensitiveWordsSettings>('save_core_sensitive_words_settings', {
|
||||
settings: { antigravitySensitiveWords, devinSensitiveWords },
|
||||
});
|
||||
setSaved(result);
|
||||
setAntigravityDraft(result.antigravitySensitiveWords.join('\n'));
|
||||
setDevinDraft(result.devinSensitiveWords.join('\n'));
|
||||
setSavedNotice(true);
|
||||
} catch (cause) {
|
||||
setError(String(cause));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="panel config-sensitive-words-panel">
|
||||
<div className="config-panel-heading">
|
||||
<div className="config-heading-title">
|
||||
<ShieldAlert size={18} aria-hidden="true" />
|
||||
<h2>{t('config.sensitiveWords.title')}</h2>
|
||||
</div>
|
||||
<div className="config-heading-actions">
|
||||
{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()}>
|
||||
<RefreshCw size={16} aria-hidden="true" />
|
||||
</button>
|
||||
<button type="button" className="primary-button compact-button" disabled={loading || saving || !dirty}
|
||||
onClick={() => void save()}>
|
||||
<Check size={16} aria-hidden="true" />
|
||||
{saving ? t('common.saving') : t('common.save')}
|
||||
</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>
|
||||
</div>
|
||||
)}
|
||||
{saved ? <small className="config-sensitive-words-intro">{t('config.sensitiveWords.restartHint')}</small> : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -3258,6 +3258,55 @@ textarea:focus-visible {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.config-sensitive-words-panel {
|
||||
display: grid;
|
||||
width: min(960px, 100%);
|
||||
gap: 18px;
|
||||
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;
|
||||
}
|
||||
|
||||
.config-sensitive-words-card {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--theme-e3e1db);
|
||||
border-radius: 8px;
|
||||
background: var(--theme-faf8f2);
|
||||
}
|
||||
|
||||
.config-sensitive-words-card label {
|
||||
color: var(--theme-39352f);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.config-sensitive-words-input {
|
||||
height: 168px;
|
||||
padding: 10px 11px;
|
||||
resize: vertical;
|
||||
line-height: var(--line-height-body);
|
||||
}
|
||||
|
||||
.config-software-panel > .config-panel-heading {
|
||||
width: min(960px, 100%);
|
||||
margin: 0 auto;
|
||||
@@ -14600,6 +14649,7 @@ body.table-col-resizing * {
|
||||
}
|
||||
|
||||
.config-network-grid,
|
||||
.config-sensitive-words-grid,
|
||||
.config-tls-fields,
|
||||
.config-management-form,
|
||||
.client-api-grid,
|
||||
|
||||
Reference in new issue
Block a user