2026-05-23 19:54:42 +08:00
# ruff: noqa: E501
from __future__ import annotations
import html
from dataclasses import dataclass
2026-05-26 09:15:14 +08:00
from typing import Any
2026-05-23 19:54:42 +08:00
@dataclass ( frozen = True )
class RiskRuleFlowDiagramField :
key : str
label : str
@dataclass ( frozen = True )
class RiskRuleFlowDiagramSpec :
title : str
domain_label : str
severity : str
severity_label : str
fields : tuple [ RiskRuleFlowDiagramField , . . . ]
start : str
evidence : str
decision : str
basis : str
pass_text : str
fail_text : str
2026-05-26 09:15:14 +08:00
fact_lines : tuple [ str , . . . ] = ( )
condition_lines : tuple [ str , . . . ] = ( )
hit_logic : str = " "
2026-05-23 19:54:42 +08:00
@dataclass ( frozen = True )
class RiskRuleFlowDiagramPalette :
accent : str
accent_dark : str
border : str
surface : str
class RiskRuleFlowDiagramRenderer :
""" 按 fireworks-tech-graph Style 7 OpenAI Official 生成只读流程 SVG。 """
_FONT = (
" -apple-system, BlinkMacSystemFont, ' Segoe UI ' , Roboto, Helvetica Neue, "
" ' PingFang SC ' , ' Microsoft YaHei ' , ' Microsoft JhengHei ' , ' SimHei ' , sans-serif "
)
_TEXT = " #0d0d0d "
_MUTED = " #6e6e80 "
_NEUTRAL_LINE = " #cbd5e1 "
_NEUTRAL_BORDER = " #e2e8f0 "
_NEUTRAL_SURFACE = " #ffffff "
_PALETTES = {
" low " : RiskRuleFlowDiagramPalette (
accent = " #2563eb " ,
accent_dark = " #1d4ed8 " ,
border = " #bfdbfe " ,
surface = " #eff6ff " ,
) ,
" medium " : RiskRuleFlowDiagramPalette (
accent = " #f97316 " ,
accent_dark = " #c2410c " ,
border = " #fed7aa " ,
surface = " #fff7ed " ,
) ,
" high " : RiskRuleFlowDiagramPalette (
accent = " #dc2626 " ,
accent_dark = " #b91c1c " ,
border = " #fecaca " ,
surface = " #fef2f2 " ,
) ,
2026-05-26 09:15:14 +08:00
" critical " : RiskRuleFlowDiagramPalette (
accent = " #991b1b " ,
accent_dark = " #7f1d1d " ,
border = " #fca5a5 " ,
surface = " #fff1f2 " ,
) ,
2026-05-23 19:54:42 +08:00
}
def render ( self , spec : RiskRuleFlowDiagramSpec ) - > str :
title = self . _truncate ( spec . title , 26 )
palette = self . _palette ( spec . severity )
2026-05-26 09:15:14 +08:00
fact_lines = spec . fact_lines or self . _field_lines ( spec . fields )
condition_lines = spec . condition_lines or ( spec . basis , )
hit_logic = spec . hit_logic or spec . basis
2026-05-23 19:54:42 +08:00
2026-05-26 09:15:14 +08:00
return f """ <svg xmlns= " http://www.w3.org/2000/svg " width= " 860 " height= " 360 " viewBox= " 0 0 860 360 " data-risk-flow-style= " review-node-only " data-risk-flow-detail= " logic-v2 " role= " img " aria-labelledby= " risk-flow-title risk-flow-desc " >
2026-05-23 19:54:42 +08:00
< title id = " risk-flow-title " > { self . _escape ( title ) } 流程说明 < / title >
2026-05-26 09:15:14 +08:00
< desc id = " risk-flow-desc " > 风险规则只读流程图 , 展示字段事实 、 集合交集 、 日期范围 、 例外说明和命中路径 。 < / desc >
2026-05-23 19:54:42 +08:00
< defs >
< marker id = " arrow-neutral " markerWidth = " 10 " markerHeight = " 7 " refX = " 9 " refY = " 3.5 " orient = " auto " >
< polygon points = " 0 0, 10 3.5, 0 7 " fill = " {self._NEUTRAL_LINE} " / >
< / marker >
2026-05-26 09:15:14 +08:00
< marker id = " arrow-risk " markerWidth = " 10 " markerHeight = " 7 " refX = " 9 " refY = " 3.5 " orient = " auto " >
< polygon points = " 0 0, 10 3.5, 0 7 " fill = " {palette.accent} " / >
< / marker >
2026-05-23 19:54:42 +08:00
< / defs >
2026-05-26 09:15:14 +08:00
< rect width = " 860 " height = " 360 " fill = " #ffffff " / >
< rect x = " 18 " y = " 18 " width = " 824 " height = " 324 " rx = " 8 " ry = " 8 " fill = " none " stroke = " {self._NEUTRAL_BORDER} " stroke - width = " 1 " stroke - dasharray = " 4,3 " / >
2026-05-23 19:54:42 +08:00
< text x = " 34 " y = " 43 " fill = " {self._MUTED} " font - family = " {self._FONT} " font - size = " 11 " font - weight = " 500 " > RULE FLOW < / text >
2026-05-26 09:15:14 +08:00
{ self . _node ( " 业务输入 " , spec . start , 38 , 142 , 120 , 62 ) }
{ self . _panel ( " 字段事实 " , fact_lines , 196 , 64 , 240 , 128 ) }
{ self . _panel ( " 判断条件 " , condition_lines , 196 , 216 , 382 , 104 ) }
{ self . _diamond ( " 命中逻辑 " , hit_logic , 494 , 80 , 122 , 122 ) }
{ self . _node ( " 继续流转 " , spec . pass_text , 688 , 76 , 122 , 60 ) }
{ self . _node ( " 进入复核 " , spec . fail_text , 688 , 226 , 122 , 68 , palette = palette ) }
< path d = " M 158 173 H 176 V 128 H 196 " fill = " none " stroke = " {self._NEUTRAL_LINE} " stroke - width = " 1.45 " stroke - linecap = " round " stroke - linejoin = " round " marker - end = " url(#arrow-neutral) " / >
< line x1 = " 316 " y1 = " 192 " x2 = " 316 " y2 = " 216 " stroke = " {self._NEUTRAL_LINE} " stroke - width = " 1.45 " stroke - linecap = " round " marker - end = " url(#arrow-neutral) " / >
< path d = " M 436 128 H 466 V 141 H 494 " fill = " none " stroke = " {self._NEUTRAL_LINE} " stroke - width = " 1.45 " stroke - linecap = " round " stroke - linejoin = " round " marker - end = " url(#arrow-neutral) " / >
< line x1 = " 555 " y1 = " 216 " x2 = " 555 " y2 = " 202 " stroke = " {self._NEUTRAL_LINE} " stroke - width = " 1.35 " stroke - linecap = " round " marker - end = " url(#arrow-neutral) " / >
< path d = " M 616 125 H 648 V 106 H 688 " fill = " none " stroke = " {self._NEUTRAL_LINE} " stroke - width = " 1.35 " stroke - linecap = " round " stroke - linejoin = " round " marker - end = " url(#arrow-neutral) " / >
< text x = " 651 " y = " 119 " text - anchor = " middle " fill = " {self._MUTED} " font - family = " {self._FONT} " font - size = " 10.5 " font - weight = " 500 " > 否 < / text >
< path d = " M 616 166 H 648 V 260 H 688 " fill = " none " stroke = " {palette.accent} " stroke - width = " 1.8 " stroke - linecap = " round " stroke - linejoin = " round " marker - end = " url(#arrow-risk) " / >
< text x = " 651 " y = " 214 " text - anchor = " middle " fill = " {palette.accent_dark} " font - family = " {self._FONT} " font - size = " 10.5 " font - weight = " 700 " > 是 < / text >
2026-05-23 19:54:42 +08:00
< / svg > """
def _node (
self ,
title : str ,
body : str ,
x : int ,
y : int ,
width : int ,
height : int ,
palette : RiskRuleFlowDiagramPalette | None = None ,
) - > str :
body_lines = self . _wrap ( body , 10 if width < = 126 else 11 , 1 )
border = palette . border if palette else self . _NEUTRAL_BORDER
stripe = palette . accent if palette else self . _NEUTRAL_LINE
surface = palette . surface if palette else self . _NEUTRAL_SURFACE
return f """ <g>
< rect x = " {x} " y = " {y} " width = " {width} " height = " {height} " rx = " 7 " ry = " 7 " fill = " {surface} " stroke = " {border} " stroke - width = " 1.2 " / >
< rect x = " {x} " y = " {y} " width = " 3.5 " height = " {height} " rx = " 1.75 " ry = " 1.75 " fill = " {stripe} " / >
< text x = " { x + width / 2:.0f} " y = " { y + 24} " text - anchor = " middle " fill = " {self._TEXT} " font - family = " {self._FONT} " font - size = " 13 " font - weight = " 600 " > { self . _escape ( title ) } < / text >
{ self . _text_lines ( body_lines , x + width / 2 , y + 43 , " middle " , self . _MUTED , 11 ) }
< / g > """
def _diamond (
self ,
title : str ,
body : str ,
x : int ,
y : int ,
width : int ,
height : int ,
) - > str :
cx = x + width / 2
cy = y + height / 2
points = f " { cx } , { y } { x + width } , { cy } { cx } , { y + height } { x } , { cy } "
body_lines = self . _wrap ( body , 8 , 2 )
return f """ <g>
< polygon points = " {points} " fill = " #ffffff " stroke = " {self._NEUTRAL_BORDER} " stroke - width = " 1.25 " / >
< text x = " {cx:.0f} " y = " { cy - 10:.0f} " text - anchor = " middle " fill = " {self._TEXT} " font - family = " {self._FONT} " font - size = " 12.5 " font - weight = " 600 " > { self . _escape ( title ) } < / text >
{ self . _text_lines ( body_lines , cx , cy + 11 , " middle " , self . _MUTED , 10.2 ) }
< / g > """
2026-05-26 09:15:14 +08:00
def _panel (
self ,
title : str ,
lines : tuple [ str , . . . ] ,
x : int ,
y : int ,
width : int ,
height : int ,
) - > str :
visible = [ self . _truncate ( line , 36 ) for line in list ( lines ) [ : 4 ] ]
if not visible :
visible = [ " 读取规则字段并归一化为判断事实 " ]
rows = " \n " . join (
f ' <text x= " { x + 16 } " y= " { y + 48 + index * 18 } " fill= " { self . _TEXT } " font-family= " { self . _FONT } " font-size= " 11 " font-weight= " 400 " > { self . _escape ( line ) } </text> '
for index , line in enumerate ( visible )
)
return f """ <g>
< rect x = " {x} " y = " {y} " width = " {width} " height = " {height} " rx = " 7 " ry = " 7 " fill = " #ffffff " stroke = " {self._NEUTRAL_BORDER} " stroke - width = " 1.2 " / >
< text x = " { x + 16} " y = " { y + 26} " fill = " {self._TEXT} " font - family = " {self._FONT} " font - size = " 13 " font - weight = " 650 " > { self . _escape ( title ) } < / text >
{ rows }
< / g > """
2026-05-23 19:54:42 +08:00
def _note (
self ,
body : str ,
x : int ,
y : int ,
width : int ,
height : int ,
) - > str :
lines = self . _wrap ( body , 22 , 1 )
return f """ <g>
< rect x = " {x} " y = " {y} " width = " {width} " height = " {height} " rx = " 7 " ry = " 7 " fill = " #ffffff " stroke = " {self._NEUTRAL_BORDER} " stroke - width = " 1 " stroke - dasharray = " 4,3 " / >
< text x = " { x + 12} " y = " { y + 22} " fill = " {self._MUTED} " font - family = " {self._FONT} " font - size = " 10 " font - weight = " 500 " > BASIS < / text >
{ self . _text_lines ( lines , x + 54 , y + 22 , " start " , self . _TEXT , 10.2 ) }
< / g > """
2026-05-26 09:15:14 +08:00
def _field_lines ( self , fields : tuple [ RiskRuleFlowDiagramField , . . . ] ) - > tuple [ str , . . . ] :
rows = [ ]
for index , field in enumerate ( fields [ : 4 ] ) :
label = field . label or field . key
rows . append ( f " { chr ( 65 + index ) } = { label } [ { field . key } ] " )
return tuple ( rows )
2026-05-23 19:54:42 +08:00
def _text_lines (
self ,
lines : list [ str ] ,
x : float ,
y : float ,
anchor : str ,
color : str ,
font_size : float ,
) - > str :
return " \n " . join (
f ' <text x= " { x : .0f } " y= " { y + index * ( font_size + 5 ) : .0f } " text-anchor= " { anchor } " fill= " { color } " font-family= " { self . _FONT } " font-size= " { font_size } " font-weight= " 400 " > { self . _escape ( line ) } </text> '
for index , line in enumerate ( lines )
)
@staticmethod
def _wrap ( value : str , width : int , max_lines : int ) - > list [ str ] :
text = str ( value or " " ) . strip ( )
if not text :
return [ " " ]
lines = [ text [ index : index + width ] for index in range ( 0 , len ( text ) , width ) ]
if len ( lines ) > max_lines :
lines = lines [ : max_lines ]
lines [ - 1 ] = f " { lines [ - 1 ] [ : max ( 0 , width - 1 ) ] } … "
return lines
@staticmethod
def _truncate ( value : str , length : int ) - > str :
text = str ( value or " " ) . strip ( )
return text if len ( text ) < = length else f " { text [ : length - 1 ] } … "
@staticmethod
def _escape ( value : str ) - > str :
return html . escape ( str ( value or " " ) , quote = True )
@classmethod
def _palette ( cls , severity : str ) - > RiskRuleFlowDiagramPalette :
return cls . _PALETTES . get ( str ( severity or " " ) . strip ( ) . lower ( ) , cls . _PALETTES [ " medium " ] )
2026-05-26 09:15:14 +08:00
def build_risk_rule_flow_diagram_details (
payload : dict [ str , Any ] ,
fields : list [ RiskRuleFlowDiagramField ] ,
) - > dict [ str , tuple [ str , . . . ] | str ] :
params = payload . get ( " params " ) if isinstance ( payload . get ( " params " ) , dict ) else { }
rule_ir = params . get ( " rule_ir " ) if isinstance ( params . get ( " rule_ir " ) , dict ) else { }
facts = rule_ir . get ( " facts " ) if isinstance ( rule_ir . get ( " facts " ) , list ) else [ ]
fact_lines = _build_fact_lines ( facts , fields )
condition_lines = _build_condition_lines ( params , fields )
hit_logic = _format_hit_logic ( params . get ( " hit_logic " ) ) or str (
params . get ( " formula " ) or params . get ( " condition_summary " ) or " "
) . strip ( )
return {
" fact_lines " : tuple ( fact_lines ) ,
" condition_lines " : tuple ( condition_lines ) ,
" hit_logic " : hit_logic ,
}
2026-05-30 15:46:51 +08:00
def build_risk_rule_flow_diagram_spec (
payload : dict [ str , Any ] ,
* ,
fields : tuple [ RiskRuleFlowDiagramField , . . . ] ,
domain_label : str ,
severity : str ,
severity_label : str ,
flow_model : dict [ str , Any ] | None = None ,
) - > RiskRuleFlowDiagramSpec :
model_spec = _spec_from_flow_model (
payload ,
fields = fields ,
domain_label = domain_label ,
severity = severity ,
severity_label = severity_label ,
flow_model = flow_model or { } ,
)
if model_spec :
return model_spec
metadata = payload . get ( " metadata " ) if isinstance ( payload . get ( " metadata " ) , dict ) else { }
flow = metadata . get ( " flow " ) if isinstance ( metadata . get ( " flow " ) , dict ) else { }
details = build_risk_rule_flow_diagram_details ( payload , list ( fields ) )
summary = str ( metadata . get ( " condition_summary " ) or " " ) . strip ( )
return RiskRuleFlowDiagramSpec (
title = str ( payload . get ( " name " ) or " " ) . strip ( ) or " 风险规则判断流程 " ,
domain_label = domain_label ,
severity = severity ,
severity_label = severity_label ,
fields = fields ,
start = str ( flow . get ( " start " ) or " " ) . strip ( ) or " 业务单据提交 " ,
evidence = str ( flow . get ( " evidence " ) or " " ) . strip ( ) or " 读取规则字段 " ,
decision = str ( flow . get ( " decision " ) or " " ) . strip ( ) or summary or " 判断是否命中风险 " ,
basis = summary or str ( flow . get ( " decision " ) or " " ) . strip ( ) or " 根据规则字段判断 " ,
pass_text = str ( flow . get ( " pass " ) or " " ) . strip ( ) or " 未命中风险,继续流转 " ,
fail_text = str ( flow . get ( " fail " ) or " " ) . strip ( ) or f " 命中 { severity_label } ,进入人工复核 " ,
fact_lines = details [ " fact_lines " ] ,
condition_lines = details [ " condition_lines " ] ,
hit_logic = str ( details [ " hit_logic " ] or " " ) ,
)
def _spec_from_flow_model (
payload : dict [ str , Any ] ,
* ,
fields : tuple [ RiskRuleFlowDiagramField , . . . ] ,
domain_label : str ,
severity : str ,
severity_label : str ,
flow_model : dict [ str , Any ] ,
) - > RiskRuleFlowDiagramSpec | None :
nodes = flow_model . get ( " nodes " ) if isinstance ( flow_model , dict ) else [ ]
if not isinstance ( nodes , list ) or not nodes :
return None
by_type : dict [ str , list [ dict [ str , Any ] ] ] = { }
for node in nodes :
if isinstance ( node , dict ) :
by_type . setdefault ( str ( node . get ( " type " ) or " " ) . strip ( ) , [ ] ) . append ( node )
decisions = by_type . get ( " decision " ) or [ ]
if not decisions :
return None
start = _node_description ( by_type . get ( " start " ) , " 业务单据提交 " )
evidence = _node_description ( by_type . get ( " evidence " ) , " 读取规则字段 " )
pass_text = _node_description ( by_type . get ( " pass " ) , " 未命中风险,继续流转 " )
fail_text = _node_description ( by_type . get ( " risk " ) , f " 命中 { severity_label } ,进入人工复核 " )
condition_lines = _condition_lines_from_flow_nodes ( decisions )
basis = condition_lines [ 0 ] if condition_lines else _node_description ( decisions , " 判断是否命中风险 " )
return RiskRuleFlowDiagramSpec (
title = str ( payload . get ( " name " ) or " " ) . strip ( ) or " 风险规则判断流程 " ,
domain_label = domain_label ,
severity = severity ,
severity_label = severity_label ,
fields = fields ,
start = start ,
evidence = evidence ,
decision = _node_description ( decisions , basis ) ,
basis = basis ,
pass_text = pass_text ,
fail_text = fail_text ,
fact_lines = tuple ( _field_lines_from_flow_nodes ( by_type . get ( " evidence " ) , fields ) ) ,
condition_lines = tuple ( condition_lines ) ,
hit_logic = _hit_logic_from_flow_model ( flow_model , condition_lines ) ,
)
def _node_description ( nodes : list [ dict [ str , Any ] ] | None , fallback : str ) - > str :
node = nodes [ 0 ] if nodes else { }
return str ( node . get ( " description " ) or node . get ( " title " ) or fallback ) . strip ( )
def _condition_lines_from_flow_nodes ( nodes : list [ dict [ str , Any ] ] ) - > list [ str ] :
visible = [
f " { str ( node . get ( ' title ' ) or node . get ( ' id ' ) or ' 判断 ' ) . strip ( ) } : { str ( node . get ( ' description ' ) or ' ' ) . strip ( ) } "
for node in nodes [ : 4 ]
]
if len ( nodes ) > 4 :
visible [ - 1 ] = f " { visible [ - 1 ] } ;另有 { len ( nodes ) - 4 } 个判断节点按命中逻辑汇总 "
return visible
def _field_lines_from_flow_nodes (
nodes : list [ dict [ str , Any ] ] | None ,
fields : tuple [ RiskRuleFlowDiagramField , . . . ] ,
) - > list [ str ] :
field_keys = _read_string_list ( ( nodes [ 0 ] if nodes else { } ) . get ( " fields " ) )
if not field_keys :
return [
f " { chr ( 65 + index ) } = { field . label or field . key } [ { field . key } ] "
for index , field in enumerate ( fields [ : 4 ] )
]
label_by_key = { field . key : field . label or field . key for field in fields }
return [
f " { chr ( 65 + index ) } = { label_by_key . get ( key , key ) } [ { key } ] "
for index , key in enumerate ( field_keys [ : 4 ] )
]
def _hit_logic_from_flow_model ( flow_model : dict [ str , Any ] , condition_lines : list [ str ] ) - > str :
metadata = flow_model . get ( " metadata " ) if isinstance ( flow_model . get ( " metadata " ) , dict ) else { }
logic = str ( metadata . get ( " hit_logic " ) or " " ) . strip ( )
if logic :
return logic
return " AND " . join ( line . split ( " : " , 1 ) [ 0 ] for line in condition_lines [ : 4 ] if line )
2026-05-26 09:15:14 +08:00
def _build_fact_lines (
facts : list [ Any ] ,
fields : list [ RiskRuleFlowDiagramField ] ,
) - > list [ str ] :
label_by_key = { field . key : field . label or field . key for field in fields }
rows : list [ str ] = [ ]
for fact in facts [ : 4 ] :
if not isinstance ( fact , dict ) :
continue
fact_id = str ( fact . get ( " id " ) or " " ) . strip ( )
label = str ( fact . get ( " label " ) or fact_id or " 事实 " ) . strip ( )
field_keys = _read_string_list ( fact . get ( " fields " ) )
field_text = " ∪ " . join ( label_by_key . get ( key , key ) for key in field_keys [ : 3 ] )
rows . append ( f " { fact_id + ' = ' if fact_id else ' ' } { label } : { field_text or ' 规则字段 ' } " )
if rows :
return rows
return [
f " { chr ( 65 + index ) } = { field . label or field . key } [ { field . key } ] "
for index , field in enumerate ( fields [ : 4 ] )
]
def _build_condition_lines (
params : dict [ str , Any ] ,
fields : list [ RiskRuleFlowDiagramField ] ,
) - > list [ str ] :
label_by_key = { field . key : field . label or field . key for field in fields }
rows : list [ str ] = [ ]
conditions = params . get ( " conditions " ) if isinstance ( params . get ( " conditions " ) , list ) else [ ]
for index , condition in enumerate ( conditions [ : 4 ] , start = 1 ) :
if not isinstance ( condition , dict ) :
continue
rows . append ( _format_condition ( condition , label_by_key , index ) )
if rows :
return rows
summary = str ( params . get ( " condition_summary " ) or " " ) . strip ( )
return [ summary ] if summary else [ ]
def _format_condition ( condition : dict [ str , Any ] , label_by_key : dict [ str , str ] , index : int ) - > str :
operator = str ( condition . get ( " operator " ) or " " ) . strip ( )
condition_id = str ( condition . get ( " id " ) or f " C { index } " ) . strip ( )
prefix = f " { condition_id } : "
if operator in { " not_in_scope " , " not_in_set " , " not_overlap " } :
left = _field_group ( condition . get ( " left_fields " ) , label_by_key )
right = _field_group ( condition . get ( " right_fields " ) , label_by_key )
return f " { prefix } { left } ∩ { right } = ∅ "
if operator in { " in_scope " , " overlap " } :
left = _field_group ( condition . get ( " left_fields " ) , label_by_key )
right = _field_group ( condition . get ( " right_fields " ) , label_by_key )
return f " { prefix } { left } ∩ { right } ≠ ∅ "
if operator == " date_outside_range " :
dates = _field_group ( condition . get ( " date_fields " ) , label_by_key )
start = _field_group ( condition . get ( " range_start_fields " ) , label_by_key )
end = _field_group ( condition . get ( " range_end_fields " ) , label_by_key )
return f " { prefix } { dates } 不在 [ { start } , { end } ] "
2026-05-30 15:46:51 +08:00
if operator == " numeric_compare " :
left = _field_group ( condition . get ( " left_fields " ) or condition . get ( " fields " ) , label_by_key )
right = _field_group ( condition . get ( " right_fields " ) , label_by_key )
compare = str ( condition . get ( " compare " ) or " gt " ) . strip ( ) . upper ( )
target = right or str ( condition . get ( " threshold " ) or condition . get ( " value " ) or " 阈值 " ) . strip ( )
return f " { prefix } { left } { compare } { target } "
if operator == " duplicate_value " :
fields = _field_group ( condition . get ( " fields " ) , label_by_key )
return f " { prefix } { fields } 出现重复值 "
2026-05-26 09:15:14 +08:00
if operator in { " contains_any " , " not_contains_any " } :
fields = _field_group ( condition . get ( " fields " ) , label_by_key )
keywords = " 、 " . join ( _read_string_list ( condition . get ( " keywords " ) ) [ : 4 ] )
verb = " 不含 " if operator == " not_contains_any " else " 包含 "
return f " { prefix } { fields } { verb } { keywords or ' 关键词 ' } "
if operator in { " exists_any " , " exists_all " , " all_present " } :
fields = _field_group ( condition . get ( " fields " ) , label_by_key )
verb = " 任一有值 " if operator == " exists_any " else " 全部有值 "
return f " { prefix } { fields } { verb } "
left = str ( condition . get ( " left " ) or " " ) . strip ( )
right = str ( condition . get ( " right " ) or " " ) . strip ( )
if left or right :
return f " { prefix } { label_by_key . get ( left , left ) } { operator or ' compare ' } { label_by_key . get ( right , right ) } "
return f " { prefix } { operator or ' 规则条件 ' } "
def _field_group ( value : Any , label_by_key : dict [ str , str ] ) - > str :
keys = _read_string_list ( value )
if not keys :
return " 字段集合 "
return " ∪ " . join ( label_by_key . get ( key , key ) for key in keys [ : 3 ] )
def _format_hit_logic ( value : Any ) - > str :
if isinstance ( value , str ) :
return value . strip ( )
if isinstance ( value , list ) :
return " AND " . join ( _format_hit_logic ( item ) for item in value if _format_hit_logic ( item ) )
if not isinstance ( value , dict ) :
return " "
if isinstance ( value . get ( " all " ) , list ) :
return " AND " . join ( _wrap_logic_part ( item ) for item in value [ " all " ] )
if isinstance ( value . get ( " any " ) , list ) :
return " OR " . join ( _wrap_logic_part ( item ) for item in value [ " any " ] )
if " not " in value :
return f " NOT { _wrap_logic_part ( value . get ( ' not ' ) ) } "
return " "
def _wrap_logic_part ( value : Any ) - > str :
text = _format_hit_logic ( value )
if isinstance ( value , dict ) and text :
return f " ( { text } ) "
return text
def _read_string_list ( value : Any ) - > list [ str ] :
if not isinstance ( value , list ) :
return [ ]
return [ str ( item or " " ) . strip ( ) for item in value if str ( item or " " ) . strip ( ) ]