#!/usr/bin/env python3 """ Precise GoChat vs Chatwoot route comparison. Normalizes param names for fair comparison. """ import re from collections import defaultdict def normalize(path): """Normalize param names: :account_id, :agent_id → :param for fair matching.""" # Replace all Rails-style params (:xxx_id, :xxx) with generic :param path = re.sub(r':[a-zA-Z_]+', ':param', path) # Remove trailing slashes path = path.rstrip('/') return path def load_routes(filepath): routes = {} with open(filepath) as f: for line in f: line = line.strip() if not line: continue parts = line.split(' ', 1) if len(parts) == 2: method, path = parts norm = normalize(path) routes[(method, norm)] = (method, path) return routes def extract_resource(path): """Extract top-level resource from path for grouping.""" # Remove /api/v1 prefix and param placeholders m = re.match(r'/api/v[12](/platform/api/v1)?(/public)?/([^:]+)', path) if m: prefix = m.group(1) or '' public = m.group(2) or '' rest = m.group(3) return prefix + public + '/' + rest.split('/')[0] return path def main(): gochat = load_routes('/tmp/gochat_routes_all.txt') chatwoot = load_routes('/tmp/chatwoot_routes_all.txt') gochat_keys = set(gochat.keys()) chatwoot_keys = set(chatwoot.keys()) matched = gochat_keys & chatwoot_keys missing_in_gochat = chatwoot_keys - gochat_keys extra_in_gochat = gochat_keys - chatwoot_keys print("=" * 80) print("GoChat vs Chatwoot 路由对比报告 (参数名标准化后)") print("=" * 80) print(f"\nGoChat 路由总数: {len(gochat_keys)}") print(f"Chatwoot API路由总数: {len(chatwoot_keys)}") print(f"匹配路由数: {len(matched)}") print(f"GoChat缺失路由数: {len(missing_in_gochat)}") print(f"GoChat多余路由数: {len(extra_in_gochat)}") print(f"覆盖率: {len(matched)/len(chatwoot_keys)*100:.1f}%") # Group missing routes by resource print(f"\n{'=' * 80}") print("一、GoChat缺失路由(按资源分组)") print("=" * 80) by_resource = defaultdict(list) for key in sorted(missing_in_gochat): method, norm_path = key orig_method, orig_path = chatwoot[key] resource = extract_resource(orig_path) by_resource[resource].append((method, orig_path)) for resource in sorted(by_resource.keys()): routes = by_resource[resource] print(f"\n [{resource}] — 缺失{len(routes)}条路由:") for method, orig_path in sorted(routes): print(f" {method} {orig_path}") # Group extra routes print(f"\n{'=' * 80}") print("二、GoChat多余路由(Chatwoot中不存在)") print("=" * 80) by_resource_extra = defaultdict(list) for key in sorted(extra_in_gochat): method, norm_path = key orig_method, orig_path = gochat[key] resource = extract_resource(orig_path) by_resource_extra[resource].append((method, orig_path)) for resource in sorted(by_resource_extra.keys()): routes = by_resource_extra[resource] if len(routes) <= 5: print(f"\n [{resource}] — 多余{len(routes)}条路由:") for method, orig_path in sorted(routes): print(f" {method} {orig_path}") else: print(f"\n [{resource}] — 多余{len(routes)}条路由 (仅列出前5条):") for method, orig_path in sorted(routes)[:5]: print(f" {method} {orig_path}") # Summary table by resource area print(f"\n{'=' * 80}") print("三、差距汇总(按功能域分组)") print("=" * 80) # Define functional areas areas = { 'Account管理': ['/api/v1/accounts/'], 'Agent/团队成员': ['/api/v1/accounts/:param/agents/', '/api/v1/accounts/:param/team_members/', '/api/v1/accounts/:param/teams/', '/api/v1/accounts/:param/inbox_members/'], '消息/对话': ['/api/v1/accounts/:param/conversations/', '/api/v1/accounts/:param/messages/', '/api/v1/accounts/:param/draft_messages/'], '联系人/公司': ['/api/v1/accounts/:param/contacts/', '/api/v1/accounts/:param/companies/', '/api/v1/accounts/:param/contact_inboxes/', '/api/v1/actions/contact_merge'], 'Inbox渠道': ['/api/v1/accounts/:param/inboxes/', '/api/v1/accounts/:param/inbox_limits/', '/api/v1/accounts/:param/campaigns/'], '自动化/宏': ['/api/v1/accounts/:param/automation_rules/', '/api/v1/accounts/:param/macros/'], 'CSAT/评分': ['/api/v1/accounts/:param/csat_survey_responses/', '/api/v1/accounts/:param/inbox_csat_templates/'], '标签/分类': ['/api/v1/accounts/:param/labels/', '/api/v1/accounts/:param/canned_responses/'], 'Captain/AI助手': ['/api/v1/accounts/:param/captain/'], '自定义属性/过滤器': ['/api/v1/accounts/:param/custom_attribute_definitions/', '/api/v1/accounts/:param/custom_filters/'], '通知设置': ['/api/v1/accounts/:param/notifications/', '/api/v1/accounts/:param/notification_settings/', '/api/v1/accounts/:param/notification_subscriptions/'], '报告': ['/api/v1/accounts/:param/reports/', '/api/v2/accounts/'], 'SLA策略': ['/api/v1/accounts/:param/sla_policies/', '/api/v1/accounts/:param/applied_slas/'], 'Portal/帮助中心': ['/api/v1/accounts/:param/portals/', '/hc/'], 'Platform API': ['/platform/api/v1/'], 'Public API': ['/public/api/v1/'], 'Widget': ['/widget/'], 'Webhook': ['/webhooks/'], '用户/Auth': ['/api/v1/profile/', '/api/v1/auth/', '/api/v1/agent_bots/'], 'Agent Bot': ['/api/v1/accounts/:param/agent_bots/', '/api/v1/agent_bots/'], 'Agent Capacity': ['/api/v1/accounts/:param/agent_capacity_policies/'], 'Assignment Policy': ['/api/v1/accounts/:param/assignment_policies/', '/api/v1/accounts/:param/assignment_policy/', '/api/v1/accounts/:param/assignment_policies_v2/'], 'Dashboard Apps': ['/api/v1/accounts/:param/dashboard_apps/'], 'Hooks/集成': ['/api/v1/accounts/:param/hooks/'], 'MFA/SAML': ['/api/v1/accounts/:param/mfa/', '/api/v1/accounts/:param/saml_settings/'], 'Bulk Actions': ['/api/v1/accounts/:param/bulk_actions/', '/api/v1/actions/'], 'Search': ['/api/v1/accounts/:param/search/'], 'Enterprise Billing': ['/enterprise/api/v1/'], } for area_name, prefixes in sorted(areas.items()): area_matched = 0 area_missing = 0 area_extra = 0 missing_details = [] for prefix in prefixes: norm_prefix = normalize(prefix) for key in chatwoot_keys: method, norm_path = key if norm_path.startswith(norm_prefix): if key in gochat_keys: area_matched += 1 else: area_missing += 1 orig_method, orig_path = chatwoot[key] missing_details.append(f"{orig_method} {orig_path}") for prefix in prefixes: norm_prefix = normalize(prefix) for key in gochat_keys: method, norm_path = key if norm_path.startswith(norm_prefix): if key not in chatwoot_keys: area_extra += 1 total = area_matched + area_missing pct = area_matched / total * 100 if total > 0 else 100 status = '✅' if pct >= 90 else '⚠️' if pct >= 50 else '❌' print(f"\n {status} {area_name}: {area_matched}/{total} 路由匹配 ({pct:.0f}%) — 缺{area_missing}条") if area_missing > 0 and area_missing <= 10: for d in sorted(missing_details): print(f" 缺: {d}") elif area_missing > 10: for d in sorted(missing_details)[:5]: print(f" 缺: {d}") print(f" ... 还有{area_missing-5}条") if __name__ == '__main__': main()