feat: add banned words filter functionality

Add banned words configuration and filtering logic to block messages containing prohibited content. Includes:
- Config file support for banned_words list
- Engine methods to set and check banned words
- Case-insensitive matching during message processing
- Localization support for blocked message response
- Documentation updates in config example

generated by llmgit

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
chenhg5andClaude committed 2026-03-05 13:59:25 +08:00
1 parent 0667e970bf
commit d6a3a2253e
5 files changed
+76 -9

No files matched your search

+8
View File
@@ -205,6 +205,11 @@ func main() {
return config.RemoveAlias(name)
})
// Wire banned words
if len(cfg.BannedWords) > 0 {
engine.SetBannedWords(cfg.BannedWords)
}
// Wire display truncation settings
{
dcfg := core.DisplayCfg{
@@ -569,6 +574,9 @@ func reloadConfig(configPath, projName string, engine *core.Engine) (*core.Confi
engine.AddAlias(a.Name, a.Command)
}
// Reload banned words
engine.SetBannedWords(cfg.BannedWords)
slog.Info("config reloaded", "project", projName)
return result, nil
}
+10
View File
@@ -132,6 +132,16 @@ level = "info" # debug, info, warn, error
# directory (e.g. .claude/commands/*.md, .gemini/commands/*.md).
# 此外,cc-connect 还会自动发现 Agent 的 commands 目录下的 .md 文件。
# =============================================================================
# Banned Words / 违禁词
# =============================================================================
#
# Messages containing any of these words will be blocked and NOT forwarded to
# the AI agent. Matching is case-insensitive.
# 消息中包含以下任一违禁词时,将被拦截,不会转发给 AI Agent。匹配不区分大小写。
# banned_words = ["违禁词1", "违禁词2", "secret_project"]
# =============================================================================
# Command Aliases / 命令别名
# =============================================================================
+10 -9
View File
@@ -16,15 +16,16 @@ var configMu sync.Mutex
var ConfigPath string
type Config struct {
DataDir string `toml:"data_dir"` // session store directory, default ~/.cc-connect
Projects []ProjectConfig `toml:"projects"`
Commands []CommandConfig `toml:"commands"` // global custom slash commands
Aliases []AliasConfig `toml:"aliases"` // global command aliases
Log LogConfig `toml:"log"`
Language string `toml:"language"` // "en" or "zh", default is "en"
Speech SpeechConfig `toml:"speech"`
Display DisplayConfig `toml:"display"`
Quiet *bool `toml:"quiet,omitempty"` // global default for quiet mode; project-level overrides this
DataDir string `toml:"data_dir"` // session store directory, default ~/.cc-connect
Projects []ProjectConfig `toml:"projects"`
Commands []CommandConfig `toml:"commands"` // global custom slash commands
Aliases []AliasConfig `toml:"aliases"` // global command aliases
BannedWords []string `toml:"banned_words"` // messages containing any of these words are blocked
Log LogConfig `toml:"log"`
Language string `toml:"language"` // "en" or "zh", default is "en"
Speech SpeechConfig `toml:"speech"`
Display DisplayConfig `toml:"display"`
Quiet *bool `toml:"quiet,omitempty"` // global default for quiet mode; project-level overrides this
}
// DisplayConfig controls how intermediate messages (thinking, tool output) are shown.
+39
View File
@@ -139,6 +139,9 @@ type Engine struct {
aliasSaveAddFunc func(name, command string) error
aliasSaveDelFunc func(name string) error
bannedWords []string
bannedMu sync.RWMutex
// Interactive agent session management
interactiveMu sync.Mutex
interactiveStates map[string]*interactiveState // key = sessionKey
@@ -293,6 +296,17 @@ func (e *Engine) ClearAliases() {
e.aliases = make(map[string]string)
}
// SetBannedWords replaces the banned words list.
func (e *Engine) SetBannedWords(words []string) {
e.bannedMu.Lock()
defer e.bannedMu.Unlock()
lower := make([]string, len(words))
for i, w := range words {
lower[i] = strings.ToLower(w)
}
e.bannedWords = lower
}
// RemoveCommand removes a custom command by name. Returns false if not found.
func (e *Engine) RemoveCommand(name string) bool {
return e.commands.Remove(name)
@@ -402,6 +416,22 @@ func (e *Engine) Stop() error {
return nil
}
// matchBannedWord returns the first banned word found in content, or "".
func (e *Engine) matchBannedWord(content string) string {
e.bannedMu.RLock()
defer e.bannedMu.RUnlock()
if len(e.bannedWords) == 0 {
return ""
}
lower := strings.ToLower(content)
for _, w := range e.bannedWords {
if strings.Contains(lower, w) {
return w
}
}
return ""
}
// resolveAlias checks if the content (or its first word) matches an alias and replaces it.
func (e *Engine) resolveAlias(content string) string {
e.aliasMu.RLock()
@@ -443,6 +473,15 @@ func (e *Engine) handleMessage(p Platform, msg *Message) {
content = e.resolveAlias(content)
msg.Content = content
// Banned words check (skip for slash commands)
if !strings.HasPrefix(content, "/") {
if word := e.matchBannedWord(content); word != "" {
slog.Info("message blocked by banned word", "word", word, "user", msg.UserName)
e.reply(p, msg.ReplyCtx, e.i18n.T(MsgBannedWordBlocked))
return
}
}
if len(msg.Images) == 0 && strings.HasPrefix(content, "/") {
if e.handleCommand(p, msg, content) {
return
+9
View File
@@ -279,6 +279,8 @@ const (
MsgDeleteSuccess MsgKey = "delete_success"
MsgDeleteActiveDenied MsgKey = "delete_active_denied"
MsgDeleteNotSupported MsgKey = "delete_not_supported"
MsgBannedWordBlocked MsgKey = "banned_word_blocked"
)
var messages = map[MsgKey]map[Language]string{
@@ -1520,6 +1522,13 @@ var messages = map[MsgKey]map[Language]string{
LangJapanese: "❌ このエージェントはセッション削除をサポートしていません。",
LangSpanish: "❌ Este agente no admite la eliminación de sesiones.",
},
MsgBannedWordBlocked: {
LangEnglish: "⚠️ Your message was blocked because it contains a prohibited word.",
LangChinese: "⚠️ 消息已被拦截,包含违禁词。",
LangTraditionalChinese: "⚠️ 訊息已被攔截,包含違禁詞。",
LangJapanese: "⚠️ 禁止ワードが含まれているため、メッセージがブロックされました。",
LangSpanish: "⚠️ Su mensaje fue bloqueado porque contiene una palabra prohibida.",
},
}
func (i *I18n) T(key MsgKey) string {