Files
gochat/backend/scripts/compare_routes_v2.py
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

181 lines
8.1 KiB
Python

#!/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()