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.
181 lines
8.1 KiB
Python
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() |