#!/usr/bin/env python3 """ 从GoChat router.go提取所有已注册的HTTP路由(带完整group路径), 与Chatwoot 473条路由做对比,输出差距报告。 """ import re # ============ Step 1: 提取GoChat已注册路由 ============ with open('/home/yanghao05/Workspace/gochat/internal/router/router.go') as f: text = f.read() lines = text.split('\n') gochat_routes = [] # [(method, path)] # 跟踪group嵌套路径前缀 group_stack = [] # stack of (prefix, is_named_param) def normalize_go_path(raw_path): """将Go路由路径转为Chatwoot风格的路径以便对比""" # :param → :param_name (保持原样) return raw_path i = 0 while i < len(lines): line = lines[i].strip() # Skip comments and empty lines if line.startswith('//') or line == '' or line.startswith('/*') or line.startswith('*'): i += 1 continue # Match Group declarations: r.Group("/path") or engine.Group("/path") group_match = re.match(r'(\w+)\.Group\("([^"]+)"\)', line) if group_match: prefix = group_match.group(2) group_stack.append(prefix) i += 1 continue # Match route registration: .GET("/path", handler) etc. route_match = re.match(r'\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\("([^"]+)"\s*,', line) if route_match: method = route_match.group(1) path = route_match.group(2) full_path = ''.join(group_stack) + path gochat_routes.append((method, full_path)) i += 1 continue # Detect closing braces that pop group stack # Count { and } to track nesting open_braces = line.count('{') close_braces = line.count('}') if close_braces > 0 and group_stack: # Each } pops one group for _ in range(close_braces - open_braces): if group_stack: group_stack.pop() i += 1 print(f"GoChat registered routes: {len(gochat_routes)}") # ============ Step 2: 加载Chatwoot路由清单 ============ # 从manifest文件导入 import sys sys.path.insert(0, '/home/yanghao05/Workspace/gochat/scripts') from chatwoot_routes_manifest import ALL_CHATWOOT, CHATWOOT_V1_NON_ACCOUNT, CHATWOOT_ACCOUNT_SCOPED, CHATWOOT_V2, CHATWOOT_INTEGRATIONS, CHATWOOT_WIDGET, CHATWOOT_PUBLIC, CHATWOOT_PLATFORM, CHATWOOT_ENTERPRISE, CHATWOOT_WEBHOOKS, CHATWOOT_WS, CHATWOOT_HEALTH print(f"Chatwoot total routes: {len(ALL_CHATWOOT)}") # ============ Step 3: 路径规范化对比 ============ def normalize_for_compare(method, path): """规范化路径以便对比: 去掉版本号差异,统一参数命名""" # GoChat可能用 :accountId 而 Chatwoot用 :account_id # 统一为 :xxx 格式 p = re.sub(r':(\w+)', ':param', path) # 去掉尾部斜杠 p = p.rstrip('/') return f"{method} {p}" # 构建GoChat路由集合(规范化后) gochat_set = set() gochat_original = {} # norm_key → original route for method, path in gochat_routes: norm = normalize_for_compare(method, path) gochat_set.add(norm) gochat_original[norm] = (method, path) # 构建Chatwoot路由集合(规范化后) chatwoot_set = set() chatwoot_original = {} # norm_key → original route for method, path, desc in ALL_CHATWOOT: norm = normalize_for_compare(method, path) chatwoot_set.add(norm) chatwoot_original[norm] = (method, path, desc) # ============ Step 4: 差距分析 ============ missing = chatwoot_set - gochat_set covered = chatwoot_set & gochat_set extra_go = gochat_set - chatwoot_set print(f"\n=== 差距概览 ===") print(f"Chatwoot路由: {len(chatwoot_set)}") print(f"GoChat路由: {len(gochat_set)}") print(f"已覆盖: {len(covered)}") print(f"缺失: {len(missing)}") print(f"GoChat额外路由: {len(extra_go)}") print(f"覆盖率: {len(covered)/len(chatwoot_set)*100:.1f}%") # ============ Step 5: 分类缺失路由 ============ categories = { 'V1非Account级': [], 'V1 Account级-核心': [], 'V1 Account级-联系人': [], 'V1 Account级-会话': [], 'V1 Account级-收件箱': [], 'V1 Account级-通知': [], 'V1 Account级-团队/角色/策略': [], 'V1 Account级-集成': [], 'V1 Account级-帮助中心': [], 'V1 Account级-Captain/AI': [], 'V1 Account级-其他': [], 'V2报表': [], 'Widget': [], 'Public API': [], 'Platform API': [], 'Enterprise API': [], 'Webhooks': [], 'WebSocket/Health': [], } def classify_route(method, path, desc): """将路由分类到对应类别""" if path.startswith('/auth') or path.startswith('/api/v1/profile') or path.startswith('/api/v1/notification_subscriptions') or path.startswith('/api/v1/auth'): return 'V1非Account级' elif path.startswith('/api/v2/'): return 'V2报表' elif path.startswith('/widget/'): return 'Widget' elif path.startswith('/public/'): return 'Public API' elif path.startswith('/platform/'): return 'Platform API' elif path.startswith('/enterprise/'): return 'Enterprise API' elif path.startswith('/webhooks/') or path.startswith('/webhooks'): return 'Webhooks' elif path.startswith('/ws') or path.startswith('/cable') or path.startswith('/health') or path.startswith('/hc/'): return 'WebSocket/Health' elif path.startswith('/api/v1/integrations/webhooks'): return 'V1非Account级' elif path.startswith('/api/v1/accounts/:param'): # 按资源子分类 sub = path.replace('/api/v1/accounts/:param/', '') if sub.startswith('contacts') or sub.startswith('contact_inboxes'): return 'V1 Account级-联系人' elif sub.startswith('conversations') or sub.startswith('unread_counts'): return 'V1 Account级-会话' elif sub.startswith('inboxes') or sub.startswith('inbox_members') or sub.startswith('inbox_limits'): return 'V1 Account级-收件箱' elif sub.startswith('notifications'): return 'V1 Account级-通知' elif sub.startswith('teams') or sub.startswith('custom_roles') or sub.startswith('assignment_policies') or sub.startswith('sla_policies') or sub.startswith('agent_capacity_policies'): return 'V1 Account级-团队/角色/策略' elif sub.startswith('integrations') or sub.startswith('webhooks') or sub.startswith('twitter') or sub.startswith('microsoft') or sub.startswith('google') or sub.startswith('instagram') or sub.startswith('tiktok') or sub.startswith('notion') or sub.startswith('whatsapp/authorization'): return 'V1 Account级-集成' elif sub.startswith('portals') or sub.startswith('categories') or sub.startswith('articles'): return 'V1 Account级-帮助中心' elif sub.startswith('captain'): return 'V1 Account级-Captain/AI' else: return 'V1 Account级-核心' return 'V1 Account级-其他' for norm_key in sorted(missing): method, path, desc = chatwoot_original[norm_key] cat = classify_route(method, path, desc) categories[cat].append((method, path, desc)) print(f"\n=== 缺失路由分类统计 ===") for cat, routes in sorted(categories.items()): if routes: print(f"\n--- {cat} ({len(routes)}条缺失) ---") for method, path, desc in routes: print(f" {method} {path} → {desc}") # ============ Step 6: 输出GoChat额外路由 ============ print(f"\n=== GoChat额外路由(Chatwoot无对应) ===") for norm_key in sorted(extra_go): method, path = gochat_original[norm_key] print(f" {method} {path}") # ============ Step 7: 保存差距报告到文件 ============ report_lines = [] report_lines.append("# GoChat vs Chatwoot 路由差距报告") report_lines.append(f"\n生成时间: {__import__('datetime').datetime.now().isoformat()}") report_lines.append(f"\n## 概览") report_lines.append(f"- Chatwoot路由总数: {len(chatwoot_set)}") report_lines.append(f"- GoChat路由总数: {len(gochat_set)}") report_lines.append(f"- 已覆盖: {len(covered)} ({len(covered)/len(chatwoot_set)*100:.1f}%)") report_lines.append(f"- 缺失: {len(missing)}") report_lines.append(f"- GoChat额外: {len(extra_go)}") report_lines.append(f"\n## 缺失路由清单") for cat, routes in sorted(categories.items()): if routes: report_lines.append(f"\n### {cat} ({len(routes)}条)") for method, path, desc in routes: report_lines.append(f"- `{method} {path}` → {desc}") report_lines.append(f"\n## GoChat额外路由") for norm_key in sorted(extra_go): method, path = gochat_original[norm_key] report_lines.append(f"- `{method} {path}`") with open('/home/yanghao05/Workspace/gochat/ROUTE_GAP_REPORT.md', 'w') as f: f.write('\n'.join(report_lines)) print(f"\n差距报告已保存到 ROUTE_GAP_REPORT.md")