diff --git a/docs/IM-前后端对接文档.md b/docs/IM-前后端对接文档.md index 3cc4fb7..1cd37ed 100644 --- a/docs/IM-前后端对接文档.md +++ b/docs/IM-前后端对接文档.md @@ -658,7 +658,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key> "data": { "tenant": {"code": "acme", "name": "示例企业", "tenant_desc": null, "branding": {}}, "endpoints": { - "websocket": "wss://im.example.com/v0/channels", + "websocket": "wss://im.example.com/v0/channels?apikey=AQ...", "api": "https://im.example.com/v1/im", "file": "https://im.example.com/v0/file" }, @@ -955,7 +955,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key> | 接口 | 输入示例 | 成功 `data` 示例 | | --- | --- | --- | -| `POST /v1/im/tenants/resolve` | Header `X-Tenant-Code: default`;Body `{"tenant_code":"default","platform":"ios","app_version":"1.0.0"}` | `{"tenant":{"code":"default","name":"Default Tenant","tenant_desc":null,"branding":{}},"endpoints":{"websocket":"wss://api.118im.com/v0/channels","api":"https://api.118im.com/v1/im","file":"https://api.118im.com/v0/file"},"auth_methods":["password","sms"],"password_policy":{"min_length":8,"max_length":64},"app_update":{"type":"none"}}` | +| `POST /v1/im/tenants/resolve` | Header `X-Tenant-Code: default`;Body `{"tenant_code":"default","platform":"ios","app_version":"1.0.0"}` | `{"tenant":{"code":"default","name":"Default Tenant","tenant_desc":null,"branding":{}},"endpoints":{"websocket":"wss://api.118im.com/v0/channels?apikey=AQ...","api":"https://api.118im.com/v1/im","file":"https://api.118im.com/v0/file"},"auth_methods":["password","sms"],"password_policy":{"min_length":8,"max_length":64},"app_update":{"type":"none"}}` | | `POST /v1/im/public/uploads` 企业注册/反馈附件上传 | Multipart `purpose=enterprise_license\|tenant_entry_feedback`、`file=@license.png` | `{"file_id":"file_01K...","purpose":"enterprise_license","mime_type":"image/png","size":1024000,"expires_at":"2026-08-25T08:30:15Z"}` | | `POST /v1/im/tenant-applications` 企业注册申请 | `{"credit_code":"91310000MA1K000000","enterprise_email":"admin@example.com","license_file_id":"file_01K...","tenant_code_hint":"acme","client_context":{"platform":"android","app_version":"1.0.0"}}` | `{"application_id":"entapp_01K...","status":"received","estimated_review_hours":24,"submitted_at":"2026-08-24T08:30:15.123Z"}` | | `POST /v1/im/tenant-entry-feedback` 企业入口问题反馈 | `{"description":"企业码 acme 提示不可用,无法进入企业。","contact":"user@example.com","tenant_code_hint":"acme","screenshot_file_ids":["file_01K..."],"client_context":{"platform":"ios","app_version":"1.0.0"}}` | `{"feedback_id":"entryfb_01K...","status":"received","submitted_at":"2026-08-24T08:35:15.123Z"}` | diff --git a/server/im_http.go b/server/im_http.go index f1e508e..b2e623f 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -856,8 +856,12 @@ func imPublicEndpoints(req *http.Request) map[string]any { } else if strings.HasPrefix(wsBase, "http://") { wsBase = "ws://" + strings.TrimPrefix(wsBase, "http://") } + wsEndpoint := strings.TrimRight(wsBase, "/") + "/v0/channels" + if apiKey := strings.TrimSpace(getAPIKey(req)); apiKey != "" { + wsEndpoint += "?apikey=" + url.QueryEscape(apiKey) + } return map[string]any{ - "websocket": strings.TrimRight(wsBase, "/") + "/v0/channels", + "websocket": wsEndpoint, "api": strings.TrimRight(base, "/") + "/v1/im", "file": strings.TrimRight(base, "/") + "/v0/file", } diff --git a/server/im_http_test.go b/server/im_http_test.go index fbd0b95..a83728e 100644 --- a/server/im_http_test.go +++ b/server/im_http_test.go @@ -1472,7 +1472,8 @@ func TestServeIMTenantResolveSuccessFromHeader(t *testing.T) { globals.servingAt = "https://im.example.com/" req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve", nil) - req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t)) + apiKey := makeTestAPIKey(t) + req.Header.Set("X-MeetingIM-APIKey", apiKey) req.Header.Set("X-Tenant-Code", " acme ") req.Header.Set("X-Request-ID", "req-1") rec := httptest.NewRecorder() @@ -1495,7 +1496,7 @@ func TestServeIMTenantResolveSuccessFromHeader(t *testing.T) { t.Fatalf("tenant payload must not expose internal id: %#v", tenant) } endpoints := data["endpoints"].(map[string]any) - if endpoints["websocket"] != "wss://im.example.com/v0/channels" { + if endpoints["websocket"] != "wss://im.example.com/v0/channels?apikey="+url.QueryEscape(apiKey) { t.Fatalf("unexpected websocket endpoint: %#v", endpoints) } } @@ -1536,7 +1537,8 @@ func TestServeIMTenantResolveUsesForwardedPublicHost(t *testing.T) { req.Host = "internal:6060" req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Host", "api.118im.com") - req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t)) + apiKey := makeTestAPIKey(t) + req.Header.Set("X-MeetingIM-APIKey", apiKey) req.Header.Set("X-Tenant-Code", "acme") rec := httptest.NewRecorder() @@ -1547,7 +1549,7 @@ func TestServeIMTenantResolveUsesForwardedPublicHost(t *testing.T) { } data := decodeIMHTTPResponse(t, rec).Data.(map[string]any) endpoints := data["endpoints"].(map[string]any) - if endpoints["websocket"] != "wss://api.118im.com/v0/channels" || + if endpoints["websocket"] != "wss://api.118im.com/v0/channels?apikey="+url.QueryEscape(apiKey) || endpoints["api"] != "https://api.118im.com/v1/im" || endpoints["file"] != "https://api.118im.com/v0/file" { t.Fatalf("unexpected endpoints: %#v", endpoints)