#!/usr/bin/env python3 """Generate comprehensive gap analysis report comparing GoChat vs Chatwoot. Focus on actionable metrics, not raw counts.""" import re from collections import defaultdict import subprocess import json import os from datetime import datetime now_str = datetime.now().strftime("%Y%m%d_%H%M") def normalize(path): """Normalize param names for comparison. Replace all :xxx_id with generic :id, but keep sub-resource segments (avatar, config, etc.) intact.""" # Replace :xxx_id patterns path = re.sub(r':([a-zA-Z_]+)_id', ':id', path) # Replace other named params (website_token, slug, etc.) path = re.sub(r':(website_token|slug|identifier|channel_type|fb_id|locale|category_slug|article_slug|portal_id|user_id)', ':id', path) # Remove trailing slashes path = path.rstrip('/') return path def normalize_gochat(path): """Normalize GoChat params — same logic but GoChat uses specific param names.""" path = re.sub(r':([a-zA-Z_]+)_id', ':id', path) path = re.sub(r':(website_token|slug|identifier|channel_type|fb_id|locale|category_slug|article_slug|portal_id|user_id|thread_id)', ':id', path) path = path.rstrip('/') return path def load_routes(filepath, normalize_func): 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_func(path) routes[(method, norm)] = line.strip() return routes def extract_resource_name(path): """Extract the top-level resource name from a normalized path.""" # E.g. /api/v1/accounts/:id/agents/:id → accounts m = re.match(r'/api/v\d/(accounts|platform|public)?/[^/]+', path) if m: return m.group(0) # E.g. /api/v1/agent_bots/:id → agent_bots m = re.match(r'/api/v\d/([^/]+)', path) if m: return m.group(0) return path.split('/:id')[0].split('/')[-1] def get_gochat_stats(): """Get GoChat handler/model/service file counts.""" stats = {} # Handler files handler_files = subprocess.run( "find /home/yanghao05/Workspace/gochat/internal/handler -name '*.go' | wc -l", shell=True, capture_output=True ).stdout.decode().decode().strip() stats['handlers'] = int(handler_files) if handler_files.isdigit() else 0 # Model files model_files = subprocess.run( "find /home/yanghao05/Workspace/gochat/internal/model -name '*.go' | wc -l", shell=True, capture_output=True ).stdout.decode().decode().strip() stats['models'] = int(model_files) if model_files.isdigit() else 0 # Service files service_files = subprocess.run( "find /home/yanghao05/Workspace/gochat/internal/service -name '*.go' | wc -l", shell=True, capture_output=True ).stdout.decode().decode().strip() stats['services'] = int(service_files) if service_files.isdigit() else 0 # Test files test_files = subprocess.run( "find /home/yanghao05/Workspace/gochat -path '*_test.go' -name '*.go' | wc -l", shell=True, capture_output=True ).stdout.decode().decode().strip() stats['tests'] = int(test_files) if test_files.isdigit() else 0 # Count handler implementations (stub vs real) stub_count = 0 real_count = 0 for f in subprocess.run( "find /home/yanghao05/Workspace/gochat/internal/handler -name '*.go'", shell=True, capture_output=True ).stdout.decode().decode().strip().split('\n'): if f.strip(): with open(f.strip()) as fh: content = fh.read() if 'TODO' in content or 'placeholder' in content or 'stub' in content.lower(): stub_count += 1 else: real_count += 1 stats['stub_handlers'] = stub_count stats['real_handlers'] = real_count return stats def get_chatwoot_stats(): """Get Chatwoot controller/model/service counts.""" stats = {} # Controller files ctrl_files = subprocess.run( "find /home/yanghao05/Workspace/chatwoot-reference/app/controllers -name '*.rb' | wc -l", shell=True, capture_output=True ).stdout.decode().decode().strip() stats['controllers'] = int(ctrl_files) if ctrl_files.isdigit() else 0 # Model files model_files = subprocess.run( "find /home/yanghao05/Workspace/chatwoot-reference/app/models -name '*.rb' | wc -l", shell=True, capture_output=True ).stdout.decode().decode().strip() stats['models'] = int(model_files) if model_files.isdigit() else 0 # Service files service_files = subprocess.run( "find /home/yanghao05/Workspace/chatwoot-reference/app/services -name '*.rb' | wc -l", shell=True, capture_output=True ).stdout.decode().decode().strip() stats['services'] = int(service_files) if service_files.isdigit() else 0 return stats def get_chatwoot_resources(): """Extract all unique resource names from Chatwoot routes.""" resources = set() result = subprocess.run( "cd /home/yanghao05/Workspace/gochat && python3 scripts/parse_chatwoot_routes_v3.py 2>&1 | grep -E '^(GET|POST|PUT|PATCH|DELETE)' | sort", shell=True, capture_output=True ) for line in result.stdout.decode().decode().strip().split('\n'): m = re.match(r'(GET|POST|PUT|PATCH|DELETE)\s+/api/v\d/(?:accounts/\:id/)?([^/]+)', line) if m: resources.add(m.group(2)) return sorted(resources) def get_gochat_resources(): """Extract all unique resource names from GoChat routes.""" resources = set() result = subprocess.run( "cd /home/yanghao05/Workspace/gochat && ./dump_routes 2>&1 | grep -v 'CONNECT' | sort", shell=True, capture_output=True ) for line in result.stdout.decode().decode().strip().split('\n'): m = re.match(r'(GET|POST|PUT|PATCH|DELETE)\s+/api/v\d/accounts/\:account_id/([^/]+)', line) if m: resources.add(m.group(1)) m = re.match(r'(GET|POST|PUT|PATCH|DELETE)\s+/api/v\d/([^/]+)', line) if m: resources.add(m.group(1)) m = re.match(r'(GET|POST|PUT|PATCH|DELETE)\s+/platform/api/v\d/([^/]+)', line) if m: resources.add(m.group(1)) m = re.match(r'(GET|POST|PUT|PATCH|DELETE)\s+/public/api/v\d/([^/]+)', line) if m: resources.add(m.group(1)) return sorted(resources) def compare_resource_coverage(): """Compare which resources exist in Chatwoot but not in GoChat.""" chatwoot_res = get_chatwoot_resources() gochat_res = get_gochat_resources() missing = chatwoot_res - gochat_res extra = gochat_res - chatwoot_res return chatwoot_res, gochat_res, missing, extra def get_chatwoot_route_details(): """Get detailed Chatwoot route list with action names.""" result = subprocess.run( "cd /home/yanghao05/Workspace/gochat && python3 scripts/parse_chatwoot_routes_v3.py 2>&1 | grep -E '^(GET|POST|PUT|PATCH|DELETE)' | sort", shell=True, capture_output=True ) routes = {} for line in result.stdout.decode().decode().strip().split('\n'): parts = line.strip().split(' ', 2) if len(parts) == 2: method, path = parts # Normalize for comparison norm = normalize(path) routes[(method, norm)] = path return routes def get_gochat_route_details(): """Get GoChat route details.""" result = subprocess.run( "cd /home/yanghao05/Workspace/gochat && ./dump_routes 2>&1 | grep -v 'CONNECT' | sort", shell=True, capture_output=True ) routes = {} for line in result.stdout.decode().decode().strip().split('\n'): parts = line.strip().split(' ', 1) if len(parts) == 2: method, path = parts norm = normalize_gochat(path) routes[(method, norm)] = path return routes def generate_report(): chatwoot = load_routes('/tmp/chatwoot_routes_all.txt', normalize) gochat = load_routes('/tmp/gochat_routes_all.txt', normalize_gochat) chatwoot_keys = set(chatwoot.keys()) gochat_keys = set(gochat.keys()) matched = chatwoot_keys & gochat_keys missing_in_gochat = chatwoot_keys - gochat_keys extra_in_gochat = gochat_keys - chatwoot_keys # Get stats gochat_stats = get_gochat_stats() chatwoot_stats = get_chatwoot_stats() chatwoot_res, gochat_res, missing_res, extra_res = compare_resource_coverage() # Group missing routes by resource missing_by_resource = defaultdict(list) for key in sorted(missing_in_gochat): method, norm_path = key orig_path = chatwoot[key] # Extract resource name parts = norm_path.split('/') if len(parts) >= 4: # /api/v1/accounts/:id/agents/:id → agents resource = '/'.join(parts[:4]) elif len(parts) >= 3: resource = '/'.join(parts[:3]) else: resource = norm_path missing_by_resource[resource].append((method, orig_path)) # Group extra routes by resource extra_by_resource = defaultdict(list) for key in sorted(extra_in_gochat): method, norm_path = key orig_path = gochat[key] parts = norm_path.split('/') if len(parts) >= 4: resource = '/'.join(parts[:4]) elif len(parts) >= 3: resource = '/'.join(parts[:3]) else: resource = norm_path extra_by_resource[resource].append((method, orig_path)) report_name = f"gochat_vs_chatwoot_gap_report_{now_str}.md" with open(report_name, 'w') as f: f.write(f"""# GoChat vs Chatwoot 实现差距报告 **生成时间**: {now_str} ## 一、总览 | | GoChat | Chatwoot | | --- | --- | --- | | 路由总数 | {len(gochat_keys)} | {len(chatwoot_keys)} | | 匹配路由 | {len(matched)} | {len(matched)} | | GoChat缺失 | {len(missing_in_gochat)} | — | | GoChat多余 | {len(extra_in_gochat)} | — | | 覆盖率 | {len(matched)/len(chatwoot_keys)*100:.1f}% | — | | Handler文件 | {gochat_stats.get('handlers', 0)} | {chatwoot_stats.get('controllers', 0)} | | Model文件 | {gochat_stats.get('models', 0)} | {chatwoot_stats.get('models', 0)} | | Service文件 | {gochat_stats.get('services', 0)} | {chatwoot_stats.get('services', 0)} | | Test文件 | {gochat_stats.get('tests', 0)} | — | | Stub Handler | {gochat_stats.get('stub_handlers', 0)} | — | | 完整Handler | {gochat_stats.get('real_handlers', 0)} | — | | Chatwoot资源数 | {len(chatwoot_res)} | — | | GoChat资源数 | {len(gochat_res)} | — | | 缺失资源 | {len(missing_res)} | — | | 多余资源 | {len(extra_res)} | — | ## 二、缺失资源清单 以下{len(missing_res)}个资源在Chatwoot中存在但在GoChat中缺失: """) for res in sorted(missing_res): f.write(f"- {res}\n") f.write(f""" ## 三、缺失路由详情 以下{len(missing_in_gochat)}条路由在Chatwoot中存在但在GoChat中缺失(按资源分组): """) for resource in sorted(missing_by_resource.keys()): routes = missing_by_resource[resource] f.write(f"\n### {resource} — 缺失{len(routes)}条路由\n") for method, orig_path in sorted(routes): f.write(f"- {method} {orig_path}\n") f.write(f""" ## 四、GoChat多余路由 以下{len(extra_in_gochat)}条路由在GoChat中存在但在Chatwoot中不存在(按资源分组): """) for resource in sorted(extra_by_resource.keys()): routes = extra_by_resource[resource] f.write(f"\n### {resource} — 多余{len(routes)}条路由\n") for method, orig_path in sorted(routes): f.write(f"- {method} {orig_path}\n") f.write(f""" ## 五、优化计划 基于差距分析,建议按以下优先级推进实现: ### Phase 1: 路由补全 (P0 — 最高优先级) 当前覆盖率仅 {len(matched)/len(chatwoot_keys)*100:.1f}%,需补齐以下缺失路由: """) # Top 10 missing resources by route count missing_res_count = defaultdict(int) for key in missing_in_gochat: method, norm_path = key parts = norm_path.split('/') if len(parts) >= 4: resource = '/'.join(parts[:4]) else: resource = norm_path missing_res_count[resource] += 1 top_missing = sorted(missing_res_count.items(), key=lambda x: -x[1])[:15] for resource, count in top_missing: f.write(f"1. **{resource}** — 缺失{count}条路由\n") f.write(""" ### Phase 2: Service层补全 当前Service文件仅{}个(Chatwoot {}个),需补齐核心业务逻辑: """.format(gochat_stats.get('services', 0), chatwoot_stats.get('services', 0))) f.write(""" ### Phase 3: Handler逻辑1:1对齐 当前Handler实现质量参差不齐({}个stub/{}个real),需逐模块验证: - 错误码/响应结构必须匹配Chatwoot - 副作用(事件触发、自动分配)必须一致 - 授权scope/过滤器/排序必须等效 """.format(gochat_stats.get('stub_handlers', 0), gochat_stats.get('real_handlers', 0))) f.write(""" ### Phase 4: 测试覆盖 当前测试文件{}个,需提升到834+覆盖所有核心场景: - 每个CRUD操作的正常+异常路径 - 每个filter/sort/pagination组合 - 授权边界(角色/scope/权限) """.format(gochat_stats.get('tests', 0))) f.write(""" --- *报告基于 {now_str} 的代码状态生成,随着实现推进数据将动态更新。* """) print(f"Report generated: {report_name}") return report_name if __name__ == '__main__': report = generate_report() print(f"\n=== SUMMARY ===") print(f"Report saved to: {report}")