xiaolongmr commited on
Commit ·
42fac8d
1
Parent(s): 9a72a84
zuhe guize gai wei tong ju pipei; fangxing duanyu he changjing kaiguan ke zai shezhi ye peizhi
Browse files- services/content_filter.py +83 -10
- test/test_content_filter.py +40 -0
- web/src/app/settings/components/config-card.tsx +29 -0
- web/src/app/settings/store.ts +5 -1
- web/src/lib/api.ts +4 -0
services/content_filter.py
CHANGED
|
@@ -175,6 +175,13 @@ _WEAK_COMBINATION_TERMS: frozenset[str] = frozenset({"骨骼", "血色"})
|
|
| 175 |
# 送审时,两个词相隔很远并不构成组合风险,默认只在 120 字窗口内才算命中;0 = 不限制。
|
| 176 |
_DEFAULT_COMBINATION_WINDOW = 120
|
| 177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
# --- 反混淆 ----------------------------------------------------------------
|
| 179 |
# 稀有 CJK 区(扩展 A 区、扩展 B 及以后)几乎不出现在正常提示词里,却是
|
| 180 |
# 「改革㐀历程」这类插入生僻字绕过词表匹配的惯用手法,规范化时直接剔除。
|
|
@@ -205,6 +212,8 @@ class _ReviewSettings(NamedTuple):
|
|
| 205 |
negation_aware: bool
|
| 206 |
multilingual_aware: bool
|
| 207 |
deobfuscate: bool
|
|
|
|
|
|
|
| 208 |
|
| 209 |
|
| 210 |
class _SearchText(NamedTuple):
|
|
@@ -329,6 +338,47 @@ def _normalize_sensitive_text(value: object) -> str:
|
|
| 329 |
return _canonical_forms(str(value or ""), True)[1]
|
| 330 |
|
| 331 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
def _search_variants(raw: str, *, deobfuscate: bool = True) -> tuple[_SearchText, ...]:
|
| 333 |
"""一个请求可能被写成多种等价形态,全部作为搜索空间。"""
|
| 334 |
if not raw:
|
|
@@ -542,6 +592,17 @@ def _local_review_settings(settings_override: dict[str, object] | None = None) -
|
|
| 542 |
negation_aware = _flag(settings, "negation_aware")
|
| 543 |
multilingual_aware = _flag(settings, "multilingual_aware")
|
| 544 |
deobfuscate = _flag(settings, "deobfuscate")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
combination_window = _coerce_window(settings.get("combination_window", _DEFAULT_COMBINATION_WINDOW))
|
| 546 |
raw_strong_terms = settings.get("strong_short_terms")
|
| 547 |
strong_terms = (
|
|
@@ -564,6 +625,8 @@ def _local_review_settings(settings_override: dict[str, object] | None = None) -
|
|
| 564 |
negation_aware,
|
| 565 |
multilingual_aware,
|
| 566 |
deobfuscate,
|
|
|
|
|
|
|
| 567 |
)
|
| 568 |
category_terms: dict[str, tuple[str, ...]] = {}
|
| 569 |
category_block_terms: set[str] = set()
|
|
@@ -614,6 +677,8 @@ def _local_review_settings(settings_override: dict[str, object] | None = None) -
|
|
| 614 |
negation_aware,
|
| 615 |
multilingual_aware,
|
| 616 |
deobfuscate,
|
|
|
|
|
|
|
| 617 |
)
|
| 618 |
|
| 619 |
|
|
@@ -625,6 +690,7 @@ def _local_rule_match(text: str, words: list[str], settings: dict[str, object] |
|
|
| 625 |
variants = _search_variants(text, deobfuscate=review.deobfuscate)
|
| 626 |
if not variants:
|
| 627 |
return None
|
|
|
|
| 628 |
category_match = _first_live_term(variants, review.category_block_terms, negation_aware=negation_aware)
|
| 629 |
if category_match:
|
| 630 |
return {"rule_id": "category-direct-block", "matched_terms": [category_match]}
|
|
@@ -642,16 +708,23 @@ def _local_rule_match(text: str, words: list[str], settings: dict[str, object] |
|
|
| 642 |
continue
|
| 643 |
if _configured_word_match(variants, word, negation_aware=negation_aware):
|
| 644 |
return {"rule_id": "configured-block-word", "matched_terms": [str(word).strip()]}
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
return None
|
| 656 |
|
| 657 |
|
|
|
|
| 175 |
# 送审时,两个词相隔很远并不构成组合风险,默认只在 120 字窗口内才算命中;0 = 不限制。
|
| 176 |
_DEFAULT_COMBINATION_WINDOW = 120
|
| 177 |
|
| 178 |
+
# 组合规则还要求左右词「在同一句话里」。列表式提示词(尤其几十条负面提示词)每行
|
| 179 |
+
# 描述一个独立缺陷,「服装撕裂破损」和「身体拉伸扭曲」分属两条、语义无关,却会落进
|
| 180 |
+
# 同一个 120 字窗口被误配(2026-10-17 实例:整段被判「写实血腥」)。
|
| 181 |
+
# 句边界只认换行与句末标点;逗号 / 顿号不算,否则「血肉模糊的人体,写实风格」这种
|
| 182 |
+
# 同一口气写完的真违规也会被切开。可用 local_review.combination_sentence_aware 关闭。
|
| 183 |
+
_SENTENCE_SPLIT = re.compile(r"[\n\r。!?;!?;]+")
|
| 184 |
+
|
| 185 |
# --- 反混淆 ----------------------------------------------------------------
|
| 186 |
# 稀有 CJK 区(扩展 A 区、扩展 B 及以后)几乎不出现在正常提示词里,却是
|
| 187 |
# 「改革㐀历程」这类插入生僻字绕过词表匹配的惯用手法,规范化时直接剔除。
|
|
|
|
| 212 |
negation_aware: bool
|
| 213 |
multilingual_aware: bool
|
| 214 |
deobfuscate: bool
|
| 215 |
+
sentence_aware: bool
|
| 216 |
+
benign_phrases: frozenset[str]
|
| 217 |
|
| 218 |
|
| 219 |
class _SearchText(NamedTuple):
|
|
|
|
| 338 |
return _canonical_forms(str(value or ""), True)[1]
|
| 339 |
|
| 340 |
|
| 341 |
+
def _sentence_variants(raw: str, *, deobfuscate: bool = True) -> tuple[tuple[_SearchText, ...], ...]:
|
| 342 |
+
"""按句切开分别构造搜索空间,供组合规则做「同句才配对」的近邻判断。
|
| 343 |
+
|
| 344 |
+
只在确实切成多句时返回内容;单句返回空元组,调用方回落到整段搜索空间
|
| 345 |
+
(结果等价,还省一次规范化)。
|
| 346 |
+
"""
|
| 347 |
+
parts = [part for part in _SENTENCE_SPLIT.split(raw) if part.strip()]
|
| 348 |
+
if len(parts) <= 1:
|
| 349 |
+
return ()
|
| 350 |
+
return tuple((_SearchText(*_canonical_forms(part, deobfuscate)),) for part in parts)
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def _apply_benign_phrases(
|
| 354 |
+
variants: tuple[_SearchText, ...],
|
| 355 |
+
phrases: frozenset[str],
|
| 356 |
+
) -> tuple[_SearchText, ...]:
|
| 357 |
+
"""把配置里的良性短语从搜索空间里抹掉,作用等同扩写内置 `_BENIGN_COMPOUNDS`。
|
| 358 |
+
|
| 359 |
+
内置表按「敏感词 → 良性复合词」分组,只能覆盖开发时预想到的搭配。这里是一份
|
| 360 |
+
平铺的短语清单,站长在设置页加一条就能放行一类误伤,不必改代码重新部署
|
| 361 |
+
(2026-10-17 用户要求:不要每次误伤都改代码推代码)。
|
| 362 |
+
|
| 363 |
+
每个形态各用自己那套归一化结果去替换,所以「服 装 撕 裂」这类插空写法同样有效。
|
| 364 |
+
"""
|
| 365 |
+
if not phrases:
|
| 366 |
+
return variants
|
| 367 |
+
needles = [_canonical_forms(phrase, True) for phrase in phrases]
|
| 368 |
+
if not needles:
|
| 369 |
+
return variants
|
| 370 |
+
masked: list[_SearchText] = []
|
| 371 |
+
for search in variants:
|
| 372 |
+
parts = list(search)
|
| 373 |
+
for forms in needles:
|
| 374 |
+
for index in range(len(parts)):
|
| 375 |
+
needle = forms[index]
|
| 376 |
+
if needle and needle in parts[index]:
|
| 377 |
+
parts[index] = parts[index].replace(needle, _BENIGN_COMPOUND_FILLER * len(needle))
|
| 378 |
+
masked.append(_SearchText(*parts))
|
| 379 |
+
return tuple(masked)
|
| 380 |
+
|
| 381 |
+
|
| 382 |
def _search_variants(raw: str, *, deobfuscate: bool = True) -> tuple[_SearchText, ...]:
|
| 383 |
"""一个请求可能被写成多种等价形态,全部作为搜索空间。"""
|
| 384 |
if not raw:
|
|
|
|
| 592 |
negation_aware = _flag(settings, "negation_aware")
|
| 593 |
multilingual_aware = _flag(settings, "multilingual_aware")
|
| 594 |
deobfuscate = _flag(settings, "deobfuscate")
|
| 595 |
+
sentence_aware = _flag(settings, "combination_sentence_aware")
|
| 596 |
+
raw_phrases = settings.get("benign_phrases")
|
| 597 |
+
benign_phrases = (
|
| 598 |
+
frozenset(
|
| 599 |
+
normalized
|
| 600 |
+
for phrase in raw_phrases
|
| 601 |
+
if (normalized := _normalize_sensitive_text(phrase))
|
| 602 |
+
)
|
| 603 |
+
if isinstance(raw_phrases, list)
|
| 604 |
+
else frozenset()
|
| 605 |
+
)
|
| 606 |
combination_window = _coerce_window(settings.get("combination_window", _DEFAULT_COMBINATION_WINDOW))
|
| 607 |
raw_strong_terms = settings.get("strong_short_terms")
|
| 608 |
strong_terms = (
|
|
|
|
| 625 |
negation_aware,
|
| 626 |
multilingual_aware,
|
| 627 |
deobfuscate,
|
| 628 |
+
sentence_aware,
|
| 629 |
+
benign_phrases,
|
| 630 |
)
|
| 631 |
category_terms: dict[str, tuple[str, ...]] = {}
|
| 632 |
category_block_terms: set[str] = set()
|
|
|
|
| 677 |
negation_aware,
|
| 678 |
multilingual_aware,
|
| 679 |
deobfuscate,
|
| 680 |
+
sentence_aware,
|
| 681 |
+
benign_phrases,
|
| 682 |
)
|
| 683 |
|
| 684 |
|
|
|
|
| 690 |
variants = _search_variants(text, deobfuscate=review.deobfuscate)
|
| 691 |
if not variants:
|
| 692 |
return None
|
| 693 |
+
variants = _apply_benign_phrases(variants, review.benign_phrases)
|
| 694 |
category_match = _first_live_term(variants, review.category_block_terms, negation_aware=negation_aware)
|
| 695 |
if category_match:
|
| 696 |
return {"rule_id": "category-direct-block", "matched_terms": [category_match]}
|
|
|
|
| 708 |
continue
|
| 709 |
if _configured_word_match(variants, word, negation_aware=negation_aware):
|
| 710 |
return {"rule_id": "configured-block-word", "matched_terms": [str(word).strip()]}
|
| 711 |
+
# 组合规则要求左右词在同一句话里。列表式提示词每行描述一个独立缺陷,跨行的两个词
|
| 712 |
+
# 语义无关却会落进 120 字窗口被误配,因此逐句判断而不是整段扫。
|
| 713 |
+
search_spaces = (
|
| 714 |
+
_sentence_variants(text, deobfuscate=review.deobfuscate) if review.sentence_aware else ()
|
| 715 |
+
) or (variants,)
|
| 716 |
+
for search_space in search_spaces:
|
| 717 |
+
search_space = _apply_benign_phrases(search_space, review.benign_phrases)
|
| 718 |
+
for rule_id, left_terms, right_terms in review.combination_rules:
|
| 719 |
+
pair = _closest_rule_pair(
|
| 720 |
+
search_space,
|
| 721 |
+
left_terms,
|
| 722 |
+
right_terms,
|
| 723 |
+
review.combination_window,
|
| 724 |
+
negation_aware=negation_aware,
|
| 725 |
+
)
|
| 726 |
+
if pair:
|
| 727 |
+
return {"rule_id": rule_id, "matched_terms": [pair[0], pair[1]]}
|
| 728 |
return None
|
| 729 |
|
| 730 |
|
test/test_content_filter.py
CHANGED
|
@@ -344,6 +344,46 @@ class ContentFilterTests(unittest.TestCase):
|
|
| 344 |
with self.assertRaises(HTTPException):
|
| 345 |
content_filter.check_request("身体被撕裂的人形,真实画面")
|
| 346 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
def test_interior_design_prompt_is_not_treated_as_intimate_photography(self) -> None:
|
| 348 |
settings = {
|
| 349 |
"enabled": True,
|
|
|
|
| 344 |
with self.assertRaises(HTTPException):
|
| 345 |
content_filter.check_request("身体被撕裂的人形,真实画面")
|
| 346 |
|
| 347 |
+
def test_combination_rule_does_not_pair_terms_across_lines(self) -> None:
|
| 348 |
+
# 结构性保障,不针对具体词:列表式提示词每行描述一个独立缺陷,
|
| 349 |
+
# 「身体」在第 3 行、「溃烂」在第 4 行,整段送审时二者落进同一个
|
| 350 |
+
# 120 字窗口,逐行判断后不再配对。(溃烂没有任何良性复合词豁免,
|
| 351 |
+
# 所以这条测试验证的是句子边界本身。)
|
| 352 |
+
with self._patched(self._gore_rule()):
|
| 353 |
+
content_filter.check_request(
|
| 354 |
+
"帮我生成一个20岁男性,CG质感,近景构图,浅景深\n"
|
| 355 |
+
"负面提示词\n"
|
| 356 |
+
"负面:身体拉伸扭曲、躯干变形\n"
|
| 357 |
+
"局部溃烂的画面、材质破损\n"
|
| 358 |
+
"衣物褶皱错乱、布料纹理扭曲\n"
|
| 359 |
+
)
|
| 360 |
+
# 同一行里同时出现时,仍然按写实血腥拦截。
|
| 361 |
+
with self.assertRaises(HTTPException) as raised:
|
| 362 |
+
content_filter.check_request("身体大面积溃烂,写实画面")
|
| 363 |
+
self.assertEqual(raised.exception.detail["rule_id"], "写实血腥")
|
| 364 |
+
|
| 365 |
+
def test_combination_sentence_aware_can_be_disabled(self) -> None:
|
| 366 |
+
# 关掉句子边界后回到旧的「整段 120 字窗口」行为,交给用户按需选择。
|
| 367 |
+
settings = self._gore_rule(combination_sentence_aware=False)
|
| 368 |
+
with self._patched(settings):
|
| 369 |
+
with self.assertRaises(HTTPException):
|
| 370 |
+
content_filter.check_request("身体拉伸扭曲\n局部溃烂的画面")
|
| 371 |
+
|
| 372 |
+
def test_configured_benign_phrases_excuse_single_line_lists(self) -> None:
|
| 373 |
+
# 单行逗号写的负面词列表整段算一句,句子边界救不了,只能靠设置页里的
|
| 374 |
+
# 「良性短语」豁免:加一条就放行,不需要改代码重新部署。
|
| 375 |
+
# 用「溃烂」是因为它没有任何内置良性复合词,能干净地验证这条通路。
|
| 376 |
+
text = "帮我生成一个20岁男性,CG质感\n负面提示词:身体拉伸扭曲、局部溃烂的画面、衣物褶皱错乱"
|
| 377 |
+
with self._patched(self._gore_rule()):
|
| 378 |
+
with self.assertRaises(HTTPException):
|
| 379 |
+
content_filter.check_request(text)
|
| 380 |
+
with self._patched(self._gore_rule(benign_phrases=["局部溃烂"])):
|
| 381 |
+
content_filter.check_request(text)
|
| 382 |
+
# 豁免只针对那一个短语,真血腥语境不受影响。
|
| 383 |
+
with self.assertRaises(HTTPException):
|
| 384 |
+
content_filter.check_request("身体大面积溃烂,写实画面")
|
| 385 |
+
|
| 386 |
+
|
| 387 |
def test_interior_design_prompt_is_not_treated_as_intimate_photography(self) -> None:
|
| 388 |
settings = {
|
| 389 |
"enabled": True,
|
web/src/app/settings/components/config-card.tsx
CHANGED
|
@@ -972,6 +972,35 @@ export function ConfigCard() {
|
|
| 972 |
/>
|
| 973 |
<span className="text-xs text-stone-500">左右两侧命中词必须落在同一段上下文里;0 表示不限距离,默认 120 字。</span>
|
| 974 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 975 |
</div>
|
| 976 |
<div className="space-y-3 border-t border-stone-200 pt-4">
|
| 977 |
<div>
|
|
|
|
| 972 |
/>
|
| 973 |
<span className="text-xs text-stone-500">左右两侧命中词必须落在同一段上下文里;0 表示不限距离,默认 120 字。</span>
|
| 974 |
</div>
|
| 975 |
+
<label className="flex items-start gap-3 text-sm font-medium text-stone-900">
|
| 976 |
+
<Checkbox
|
| 977 |
+
className="mt-0.5"
|
| 978 |
+
checked={config?.local_review?.combination_sentence_aware !== false}
|
| 979 |
+
onCheckedChange={(checked) => setLocalReviewField("combination_sentence_aware", Boolean(checked))}
|
| 980 |
+
disabled={config?.local_review?.enabled === false}
|
| 981 |
+
/>
|
| 982 |
+
<span>
|
| 983 |
+
组合词只在同一句内配对
|
| 984 |
+
<span className="mt-1 block text-xs font-normal leading-6 text-stone-500">
|
| 985 |
+
负面提示词常一行写一个缺陷,「服装撕裂破损」和「身体拉伸扭曲」分属两条、语义无关,却会落进同一个间距窗口被误判成血腥。开启后只认同句(换行 / 句号 / 分号分隔)内的配对;逗号、顿号连写的长句不受影响,同一句里的真违规照旧拦截。
|
| 986 |
+
</span>
|
| 987 |
+
</span>
|
| 988 |
+
</label>
|
| 989 |
+
<div className="space-y-2">
|
| 990 |
+
<label className="text-sm text-stone-700" htmlFor="local-review-benign-phrases">自定义放行短语</label>
|
| 991 |
+
<Textarea
|
| 992 |
+
id="local-review-benign-phrases"
|
| 993 |
+
rows={4}
|
| 994 |
+
value={(config?.local_review?.benign_phrases || []).join("\n")}
|
| 995 |
+
onChange={(event) => setLocalReviewField("benign_phrases", event.target.value.split("\n").map((phrase) => phrase.trim()).filter(Boolean))}
|
| 996 |
+
className="rounded-lg text-sm"
|
| 997 |
+
placeholder={"一行一条,例如:\n服装撕裂\n布料撕裂\n唇形崩坏"}
|
| 998 |
+
disabled={config?.local_review?.enabled === false}
|
| 999 |
+
/>
|
| 1000 |
+
<p className="text-xs text-stone-500">
|
| 1001 |
+
写在这里的短语会在送审前被整段抹掉,其中的敏感词不再触发拦截。用来放行「服装撕裂」「唇形崩坏」这类在正常提示词里必然出现、但被词表当成风险的多义词搭配。<span className="text-stone-700">保存后立刻生效,不需要重新部署。</span>先用下方「规则测试」粘一条被拦的提示词确认效果。
|
| 1002 |
+
</p>
|
| 1003 |
+
</div>
|
| 1004 |
</div>
|
| 1005 |
<div className="space-y-3 border-t border-stone-200 pt-4">
|
| 1006 |
<div>
|
web/src/app/settings/store.ts
CHANGED
|
@@ -173,6 +173,8 @@ const DEFAULT_LOCAL_REVIEW = {
|
|
| 173 |
],
|
| 174 |
negation_aware: true,
|
| 175 |
combination_window: 120,
|
|
|
|
|
|
|
| 176 |
multilingual_aware: true,
|
| 177 |
deobfuscate: true,
|
| 178 |
};
|
|
@@ -224,6 +226,8 @@ function normalizeLocalReview(value: SettingsConfig["local_review"], legacyWords
|
|
| 224 |
})) : DEFAULT_LOCAL_REVIEW.combination_rules,
|
| 225 |
negation_aware: value?.negation_aware !== false,
|
| 226 |
combination_window: Number.isFinite(rawWindow) && rawWindow >= 0 ? Math.round(rawWindow) : DEFAULT_LOCAL_REVIEW.combination_window,
|
|
|
|
|
|
|
| 227 |
multilingual_aware: value?.multilingual_aware !== false,
|
| 228 |
deobfuscate: value?.deobfuscate !== false,
|
| 229 |
};
|
|
@@ -715,7 +719,7 @@ type SettingsStore = {
|
|
| 715 |
setReversePromptField: (key: "provider" | "base_url" | "api_key" | "model", value: string) => void;
|
| 716 |
setProductVisualAgentField: (key: "provider" | "base_url" | "api_key" | "model", value: string) => void;
|
| 717 |
setSensitiveWordsText: (value: string) => void;
|
| 718 |
-
setLocalReviewField: (key: "enabled" | "strong_short_terms" | "categories" | "combination_rules" | "negation_aware" | "combination_window" | "multilingual_aware" | "deobfuscate", value: boolean | number | string[] | NonNullable<SettingsConfig["local_review"]>["categories"] | NonNullable<SettingsConfig["local_review"]>["combination_rules"]) => void;
|
| 719 |
setAIReviewField: (key: "enabled" | "base_url" | "api_key" | "model" | "prompt" | "fail_open", value: string | boolean) => void;
|
| 720 |
setImageStorageField: (key: keyof ImageStorageSettings, value: string | boolean) => void;
|
| 721 |
setProxyRuntimeField: <K extends keyof ProxyRuntimeSettings>(key: K, value: ProxyRuntimeSettings[K]) => void;
|
|
|
|
| 173 |
],
|
| 174 |
negation_aware: true,
|
| 175 |
combination_window: 120,
|
| 176 |
+
combination_sentence_aware: true,
|
| 177 |
+
benign_phrases: [],
|
| 178 |
multilingual_aware: true,
|
| 179 |
deobfuscate: true,
|
| 180 |
};
|
|
|
|
| 226 |
})) : DEFAULT_LOCAL_REVIEW.combination_rules,
|
| 227 |
negation_aware: value?.negation_aware !== false,
|
| 228 |
combination_window: Number.isFinite(rawWindow) && rawWindow >= 0 ? Math.round(rawWindow) : DEFAULT_LOCAL_REVIEW.combination_window,
|
| 229 |
+
combination_sentence_aware: value?.combination_sentence_aware !== false,
|
| 230 |
+
benign_phrases: Array.isArray(value?.benign_phrases) ? value.benign_phrases.map((phrase) => String(phrase || "").trim()).filter(Boolean) : [],
|
| 231 |
multilingual_aware: value?.multilingual_aware !== false,
|
| 232 |
deobfuscate: value?.deobfuscate !== false,
|
| 233 |
};
|
|
|
|
| 719 |
setReversePromptField: (key: "provider" | "base_url" | "api_key" | "model", value: string) => void;
|
| 720 |
setProductVisualAgentField: (key: "provider" | "base_url" | "api_key" | "model", value: string) => void;
|
| 721 |
setSensitiveWordsText: (value: string) => void;
|
| 722 |
+
setLocalReviewField: (key: "enabled" | "strong_short_terms" | "categories" | "combination_rules" | "negation_aware" | "combination_window" | "combination_sentence_aware" | "benign_phrases" | "multilingual_aware" | "deobfuscate", value: boolean | number | string[] | NonNullable<SettingsConfig["local_review"]>["categories"] | NonNullable<SettingsConfig["local_review"]>["combination_rules"]) => void;
|
| 723 |
setAIReviewField: (key: "enabled" | "base_url" | "api_key" | "model" | "prompt" | "fail_open", value: string | boolean) => void;
|
| 724 |
setImageStorageField: (key: keyof ImageStorageSettings, value: string | boolean) => void;
|
| 725 |
setProxyRuntimeField: <K extends keyof ProxyRuntimeSettings>(key: K, value: ProxyRuntimeSettings[K]) => void;
|
web/src/lib/api.ts
CHANGED
|
@@ -727,6 +727,10 @@ export type SettingsConfig = {
|
|
| 727 |
negation_aware?: boolean;
|
| 728 |
/** 组合规则左右两侧命中词的最大间距(按归一化后字符计)。0 = 不限制。默认 120。 */
|
| 729 |
combination_window?: number;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
/** 内置跨语言违规词(英/日/韩/俄/德/法/西/越/泰/阿拉伯…)恒先生效。默认开启。 */
|
| 731 |
multilingual_aware?: boolean;
|
| 732 |
/** 送审前先还原混淆写法:繁→简、URL/HTML/\\u 转义、Base64、稀字剔除、倒序词。默认开启。 */
|
|
|
|
| 727 |
negation_aware?: boolean;
|
| 728 |
/** 组合规则左右两侧命中词的最大间距(按归一化后字符计)。0 = 不限制。默认 120。 */
|
| 729 |
combination_window?: number;
|
| 730 |
+
/** 组合规则要求左右词落在同一句内(换行 / 句号 / 分号分隔)。默认开启。 */
|
| 731 |
+
combination_sentence_aware?: boolean;
|
| 732 |
+
/** 送审前整段抹掉的良性短语,用于放行「服装撕裂」这类多数场合无害的搭配。改完即生效。 */
|
| 733 |
+
benign_phrases?: string[];
|
| 734 |
/** 内置跨语言违规词(英/日/韩/俄/德/法/西/越/泰/阿拉伯…)恒先生效。默认开启。 */
|
| 735 |
multilingual_aware?: boolean;
|
| 736 |
/** 送审前先还原混淆写法:繁→简、URL/HTML/\\u 转义、Base64、稀字剔除、倒序词。默认开启。 */
|