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.
221 lines
8.6 KiB
Python
221 lines
8.6 KiB
Python
#!/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") |