second commit
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
#!/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}")
|
||||
Reference in New Issue
Block a user