@@ -701,6 +701,25 @@ def _build_astrbot_conversion_sections(
701701 ) -> tuple [dict [str , Any ], list [str ]]:
702702 issues : list [str ] = []
703703 sections : dict [str , Any ] = {}
704+ field_diagnostics : list [dict [str , Any ]] = [
705+ self ._astrbot_field_diagnostic (
706+ source_path = f"astrbot:{ source_type } " ,
707+ target_path = "sharelife_meta.astrbot_import.source_type" ,
708+ outcome = "mapped" ,
709+ issue_code = "astrbot_raw_import_converted" ,
710+ note = "raw astrbot payload converted into standard profile-pack sections" ,
711+ )
712+ ]
713+ if source_type == "astrbot_backup_zip" :
714+ field_diagnostics .append (
715+ self ._astrbot_field_diagnostic (
716+ source_path = "manifest.json" ,
717+ target_path = "sharelife_meta.astrbot_import.backup_manifest" ,
718+ outcome = "omitted_runtime_payload" ,
719+ issue_code = "astrbot_backup_runtime_payload_omitted" ,
720+ note = "backup runtime payload omitted; backup manifest retained for traceability" ,
721+ )
722+ )
704723 personas_payload , personas_summary = self ._build_astrbot_conversion_personas (raw_config )
705724 if personas_payload :
706725 sections ["personas" ] = personas_payload
@@ -711,8 +730,19 @@ def _build_astrbot_conversion_sections(
711730
712731 core_payload : dict [str , Any ] = {}
713732 operator_keys = {"dashboard" , "admins_id" }
714- if any (key in raw_config for key in operator_keys ):
733+ operator_omitted = [key for key in sorted (operator_keys ) if key in raw_config ]
734+ if operator_omitted :
715735 issues .append ("astrbot_operator_fields_omitted" )
736+ for key in operator_omitted :
737+ field_diagnostics .append (
738+ self ._astrbot_field_diagnostic (
739+ source_path = key ,
740+ target_path = f"astrbot_core.{ key } " ,
741+ outcome = "omitted" ,
742+ issue_code = "astrbot_operator_fields_omitted" ,
743+ note = "operator-managed field omitted from member-importable profile-pack payload" ,
744+ )
745+ )
716746 extracted_keys = {
717747 "provider" ,
718748 "plugin_set" ,
@@ -734,12 +764,14 @@ def _build_astrbot_conversion_sections(
734764 if core_payload :
735765 sections ["astrbot_core" ] = core_payload
736766
737- providers_payload = self ._build_astrbot_conversion_providers (raw_config )
767+ providers_payload , provider_diagnostics = self ._build_astrbot_conversion_providers (raw_config )
768+ field_diagnostics .extend (provider_diagnostics )
738769 if providers_payload :
739770 sections ["providers" ] = providers_payload
740771
741- plugins_payload , plugin_issues = self ._build_astrbot_conversion_plugins (raw_config )
772+ plugins_payload , plugin_issues , plugin_diagnostics = self ._build_astrbot_conversion_plugins (raw_config )
742773 issues .extend (plugin_issues )
774+ field_diagnostics .extend (plugin_diagnostics )
743775 if plugins_payload is not None :
744776 sections ["plugins" ] = plugins_payload
745777
@@ -748,6 +780,7 @@ def _build_astrbot_conversion_sections(
748780 personas_summary = personas_summary ,
749781 environment_summary = environment_summary ,
750782 plugins_payload = plugins_payload ,
783+ field_diagnostics = field_diagnostics ,
751784 )
752785
753786 sections ["sharelife_meta" ] = {
@@ -763,35 +796,83 @@ def _build_astrbot_conversion_sections(
763796 }
764797 return sections , self ._dedupe_issue_codes (issues )
765798
766- def _build_astrbot_conversion_providers (self , raw_config : dict [str , Any ]) -> dict [str , Any ]:
799+ def _build_astrbot_conversion_providers (self , raw_config : dict [str , Any ]) -> tuple [ dict [str , Any ], list [ dict [ str , Any ]] ]:
767800 providers = raw_config .get ("provider" , [])
768801 if not isinstance (providers , list ):
769- return {}
802+ return {}, []
770803 out : dict [str , Any ] = {}
804+ diagnostics : list [dict [str , Any ]] = []
771805 for index , item in enumerate (providers ):
772806 if not isinstance (item , dict ):
773807 continue
774808 provider_id = str (item .get ("id" , "" ) or "" ).strip () or f"provider-{ index + 1 } "
809+ source_provider_path = f"provider[{ index } ]"
775810 normalized = deepcopy (item )
776811 if "key" in normalized and "api_key" not in normalized :
777812 normalized ["api_key" ] = normalized .pop ("key" )
813+ diagnostics .append (
814+ self ._astrbot_field_diagnostic (
815+ source_path = f"{ source_provider_path } .key" ,
816+ target_path = f"providers.{ provider_id } .api_key" ,
817+ outcome = "mapped_redacted" ,
818+ issue_code = "astrbot_raw_import_converted" ,
819+ note = "provider key renamed to api_key and redacted" ,
820+ )
821+ )
822+ elif "api_key" in normalized :
823+ diagnostics .append (
824+ self ._astrbot_field_diagnostic (
825+ source_path = f"{ source_provider_path } .api_key" ,
826+ target_path = f"providers.{ provider_id } .api_key" ,
827+ outcome = "mapped_redacted" ,
828+ issue_code = "astrbot_raw_import_converted" ,
829+ note = "provider api_key retained and redacted" ,
830+ )
831+ )
778832 normalized = self ._sanitize_astrbot_conversion_value (
779833 normalized ,
780834 path = f"providers.{ provider_id } " ,
781835 )
782836 out [provider_id ] = normalized
783- return out
837+ return out , self . _dedupe_astrbot_field_diagnostics ( diagnostics )
784838
785- def _build_astrbot_conversion_plugins (self , raw_config : dict [str , Any ]) -> tuple [dict [str , Any ] | None , list [str ]]:
839+ def _build_astrbot_conversion_plugins (
840+ self ,
841+ raw_config : dict [str , Any ],
842+ ) -> tuple [dict [str , Any ] | None , list [str ], list [dict [str , Any ]]]:
786843 plugin_set = raw_config .get ("plugin_set" )
787844 if plugin_set is None :
788- return None , []
845+ return None , [], []
789846 if not isinstance (plugin_set , list ):
790- return {}, []
847+ return {}, [], []
791848 normalized = [str (item or "" ).strip () for item in plugin_set if str (item or "" ).strip ()]
792849 if "*" in normalized :
793- return None , ["astrbot_plugin_wildcard_unresolved" ]
794- return {plugin_id : {"enabled" : True } for plugin_id in normalized }, []
850+ return (
851+ None ,
852+ ["astrbot_plugin_wildcard_unresolved" ],
853+ [
854+ self ._astrbot_field_diagnostic (
855+ source_path = "plugin_set" ,
856+ target_path = "plugins" ,
857+ outcome = "requires_manual_resolution" ,
858+ issue_code = "astrbot_plugin_wildcard_unresolved" ,
859+ note = "plugin wildcard cannot be resolved into deterministic plugin ids" ,
860+ )
861+ ],
862+ )
863+ return (
864+ {plugin_id : {"enabled" : True } for plugin_id in normalized },
865+ [],
866+ [
867+ self ._astrbot_field_diagnostic (
868+ source_path = "plugin_set" ,
869+ target_path = "plugins" ,
870+ outcome = "mapped" ,
871+ issue_code = "astrbot_raw_import_converted" ,
872+ note = "plugin_set converted into plugins section" ,
873+ )
874+ ] if normalized else [],
875+ )
795876
796877 def _build_astrbot_conversion_personas (self , raw_config : dict [str , Any ]) -> tuple [dict [str , Any ] | None , dict [str , Any ]]:
797878 payload : dict [str , Any ] = {}
@@ -895,6 +976,7 @@ def _build_astrbot_conversion_summary(
895976 personas_summary : dict [str , Any ],
896977 environment_summary : dict [str , Any ],
897978 plugins_payload : dict [str , Any ] | None ,
979+ field_diagnostics : list [dict [str , Any ]] | None = None ,
898980 ) -> dict [str , Any ]:
899981 summary : dict [str , Any ] = {}
900982 summary .update (personas_summary )
@@ -904,8 +986,63 @@ def _build_astrbot_conversion_summary(
904986 provider_count = raw_config .get ("provider" )
905987 if isinstance (provider_count , list ):
906988 summary ["provider_count" ] = len (provider_count )
989+ normalized_diagnostics = self ._dedupe_astrbot_field_diagnostics (field_diagnostics or [])
990+ if normalized_diagnostics :
991+ summary ["field_diagnostics" ] = normalized_diagnostics
992+ summary ["field_diagnostic_count" ] = len (normalized_diagnostics )
993+ outcome_counts : dict [str , int ] = {}
994+ for item in normalized_diagnostics :
995+ outcome = str (item .get ("outcome" , "" ) or "" ).strip () or "unknown"
996+ outcome_counts [outcome ] = outcome_counts .get (outcome , 0 ) + 1
997+ summary ["field_diagnostic_outcomes" ] = outcome_counts
907998 return summary
908999
1000+ @staticmethod
1001+ def _astrbot_field_diagnostic (
1002+ * ,
1003+ source_path : str ,
1004+ target_path : str = "" ,
1005+ outcome : str ,
1006+ issue_code : str = "" ,
1007+ note : str = "" ,
1008+ ) -> dict [str , Any ]:
1009+ return {
1010+ "source_path" : str (source_path or "" ).strip (),
1011+ "target_path" : str (target_path or "" ).strip (),
1012+ "outcome" : str (outcome or "" ).strip (),
1013+ "issue_code" : str (issue_code or "" ).strip (),
1014+ "note" : str (note or "" ).strip (),
1015+ }
1016+
1017+ @staticmethod
1018+ def _dedupe_astrbot_field_diagnostics (values : list [dict [str , Any ]]) -> list [dict [str , Any ]]:
1019+ out : list [dict [str , Any ]] = []
1020+ seen : set [tuple [str , str , str , str , str ]] = set ()
1021+ for item in values :
1022+ if not isinstance (item , dict ):
1023+ continue
1024+ source_path = str (item .get ("source_path" , "" ) or "" ).strip ()
1025+ target_path = str (item .get ("target_path" , "" ) or "" ).strip ()
1026+ outcome = str (item .get ("outcome" , "" ) or "" ).strip ()
1027+ issue_code = str (item .get ("issue_code" , "" ) or "" ).strip ()
1028+ note = str (item .get ("note" , "" ) or "" ).strip ()
1029+ if not source_path and not target_path :
1030+ continue
1031+ key = (source_path , target_path , outcome , issue_code , note )
1032+ if key in seen :
1033+ continue
1034+ seen .add (key )
1035+ out .append (
1036+ {
1037+ "source_path" : source_path ,
1038+ "target_path" : target_path ,
1039+ "outcome" : outcome ,
1040+ "issue_code" : issue_code ,
1041+ "note" : note ,
1042+ }
1043+ )
1044+ return out
1045+
9091046 def _sanitize_astrbot_conversion_value (self , value : Any , * , path : str ) -> Any :
9101047 if isinstance (value , dict ):
9111048 out : dict [str , Any ] = {}
@@ -2934,6 +3071,8 @@ def compatibility_issue_details(
29343071 "sections" : related_sections ,
29353072 "related_paths" : related_paths ,
29363073 "evidence_refs" : cls ._compatibility_issue_evidence_refs (
3074+ issue_code = issue_code ,
3075+ sections = normalized_sections ,
29373076 scan_summary = scan_summary ,
29383077 related_sections = related_sections ,
29393078 related_paths = related_paths ,
@@ -3134,13 +3273,13 @@ def visit(node: Any, path: str, depth: int) -> None:
31343273 def _compatibility_issue_evidence_refs (
31353274 cls ,
31363275 * ,
3276+ issue_code : str ,
3277+ sections : dict [str , Any ] | None ,
31373278 scan_summary : dict [str , Any ] | None ,
31383279 related_sections : list [str ],
31393280 related_paths : list [str ],
31403281 ) -> list [dict [str , Any ]]:
31413282 rows = list ((scan_summary or {}).get ("risk_evidence" , []) or [])
3142- if not rows :
3143- return []
31443283 section_files = {f"sections/{ section } .json" for section in related_sections if section }
31453284 normalized_paths = [str (path or "" ).strip () for path in related_paths if str (path or "" ).strip ()]
31463285 out : list [dict [str , Any ]] = []
@@ -3185,6 +3324,82 @@ def _compatibility_issue_evidence_refs(
31853324 continue
31863325 seen .add (key )
31873326 out .append (record )
3327+ if out :
3328+ return out
3329+ return cls ._compatibility_issue_conversion_evidence_refs (
3330+ issue_code = issue_code ,
3331+ sections = sections ,
3332+ related_paths = normalized_paths ,
3333+ )
3334+
3335+ @classmethod
3336+ def _compatibility_issue_conversion_evidence_refs (
3337+ cls ,
3338+ * ,
3339+ issue_code : str ,
3340+ sections : dict [str , Any ] | None ,
3341+ related_paths : list [str ],
3342+ ) -> list [dict [str , Any ]]:
3343+ if cls ._compatibility_issue_bucket (issue_code ) != "conversion" :
3344+ return []
3345+ meta = (sections or {}).get ("sharelife_meta" )
3346+ if not isinstance (meta , dict ):
3347+ return []
3348+ astrbot_import = meta .get ("astrbot_import" )
3349+ if not isinstance (astrbot_import , dict ):
3350+ return []
3351+ summary = astrbot_import .get ("summary" )
3352+ if not isinstance (summary , dict ):
3353+ return []
3354+ diagnostics = summary .get ("field_diagnostics" )
3355+ if not isinstance (diagnostics , list ):
3356+ return []
3357+ normalized_issue_code = str (issue_code or "" ).strip ()
3358+ out : list [dict [str , Any ]] = []
3359+ seen : set [tuple [str , str , str , str , str ]] = set ()
3360+ for item in diagnostics :
3361+ if len (out ) >= 8 :
3362+ break
3363+ if not isinstance (item , dict ):
3364+ continue
3365+ row_issue_code = str (item .get ("issue_code" , "" ) or "" ).strip ()
3366+ if row_issue_code and row_issue_code != normalized_issue_code :
3367+ continue
3368+ target_path = str (item .get ("target_path" , "" ) or "" ).strip ()
3369+ source_path = str (item .get ("source_path" , "" ) or "" ).strip ()
3370+ diagnostic_path = target_path or source_path
3371+ if not diagnostic_path :
3372+ continue
3373+ if related_paths :
3374+ matched = False
3375+ for path_ref in related_paths :
3376+ ref = str (path_ref or "" ).strip ()
3377+ if not ref or ref .startswith ("manifest." ):
3378+ continue
3379+ if diagnostic_path == ref or diagnostic_path .startswith (f"{ ref } ." ) or diagnostic_path .startswith (f"{ ref } [" ):
3380+ matched = True
3381+ break
3382+ if not matched :
3383+ continue
3384+ outcome = str (item .get ("outcome" , "" ) or "" ).strip () or "conversion"
3385+ record = {
3386+ "file" : "sections/sharelife_meta.json" ,
3387+ "path" : diagnostic_path ,
3388+ "line" : 0 ,
3389+ "column" : 0 ,
3390+ "rule" : f"conversion_{ outcome } " ,
3391+ }
3392+ key = (
3393+ record ["file" ],
3394+ record ["path" ],
3395+ str (record ["line" ]),
3396+ str (record ["column" ]),
3397+ record ["rule" ],
3398+ )
3399+ if key in seen :
3400+ continue
3401+ seen .add (key )
3402+ out .append (record )
31883403 return out
31893404
31903405 @staticmethod
0 commit comments