11import { type OpenAPIHono , createRoute , z } from "@hono/zod-openapi" ;
22import {
33 botQuotaResponseSchema ,
4+ channelConnectErrorSchema ,
45 channelListResponseSchema ,
56 channelResponseSchema ,
67 connectDingtalkSchema ,
@@ -23,11 +24,223 @@ import {
2324 whatsappQrWaitResponseSchema ,
2425} from "@nexu/shared" ;
2526import type { ControllerContainer } from "../app/container.js" ;
27+ import { isChannelConnectError } from "../lib/channel-connect-error.js" ;
2628import { logger } from "../lib/logger.js" ;
29+ import {
30+ readProxyFetchEnv ,
31+ redactProxyUrl ,
32+ shouldBypassProxy ,
33+ } from "../lib/proxy-fetch.js" ;
2734import type { ControllerBindings } from "../types.js" ;
2835
2936const channelIdParamSchema = z . object ( { channelId : z . string ( ) } ) ;
3037const errorSchema = z . object ( { message : z . string ( ) } ) ;
38+ type ControllerLocale = "en" | "zh-CN" ;
39+
40+ function getOpenclawOrigin ( container : ControllerContainer ) : string | null {
41+ try {
42+ return new URL ( container . env . openclawBaseUrl ) . origin ;
43+ } catch {
44+ return null ;
45+ }
46+ }
47+
48+ async function getControllerLocale (
49+ container : ControllerContainer ,
50+ ) : Promise < ControllerLocale > {
51+ try {
52+ return await container . configStore . getDesktopLocale ( ) ;
53+ } catch {
54+ return "en" ;
55+ }
56+ }
57+
58+ function localizeChannelConnectMessage (
59+ error : unknown ,
60+ locale : ControllerLocale ,
61+ ) : string {
62+ if ( ! isChannelConnectError ( error ) ) {
63+ return locale === "zh-CN"
64+ ? "连接失败,请稍后重试。"
65+ : "Connection failed. Please try again." ;
66+ }
67+
68+ if ( locale === "zh-CN" ) {
69+ switch ( error . code ) {
70+ case "invalid_credentials" :
71+ return "凭证无效,请检查后重试。" ;
72+ case "app_id_mismatch" :
73+ return "Application ID 与 Bot Token 不匹配,请检查后重试。" ;
74+ case "timeout" :
75+ return "请求超时,请检查网络或代理设置后重试。" ;
76+ case "network_error" :
77+ case "proxy_error" :
78+ return "网络请求失败,请检查网络或代理设置后重试。" ;
79+ case "sync_failed" :
80+ return error . phase === "persist_config"
81+ ? "凭证已校验,但本地保存配置失败,请稍后重试。"
82+ : "凭证已校验,但本地运行时同步失败,请稍后重试。" ;
83+ case "upstream_http_error" :
84+ return "上游服务返回异常,请稍后重试。" ;
85+ case "already_connected" :
86+ return "渠道已连接,正在刷新..." ;
87+ }
88+ }
89+
90+ switch ( error . code ) {
91+ case "invalid_credentials" :
92+ return "Credentials are invalid. Check them and try again." ;
93+ case "app_id_mismatch" :
94+ return "Application ID does not match the provided Bot Token." ;
95+ case "timeout" :
96+ return "The request timed out. Check your network or proxy settings and try again." ;
97+ case "network_error" :
98+ case "proxy_error" :
99+ return "The network request failed. Check your network or proxy settings and try again." ;
100+ case "sync_failed" :
101+ return error . phase === "persist_config"
102+ ? "Credentials were verified, but saving the local channel config failed. Please try again."
103+ : "Credentials were verified, but syncing the local runtime failed. Please try again." ;
104+ case "upstream_http_error" :
105+ return "The upstream service returned an error. Please try again later." ;
106+ case "already_connected" :
107+ return "Channel already connected, refreshing..." ;
108+ }
109+ }
110+
111+ function getChannelConnectErrorResponse (
112+ requestId : string ,
113+ locale : ControllerLocale ,
114+ error : unknown ,
115+ ) {
116+ if ( isChannelConnectError ( error ) ) {
117+ return {
118+ status : error . status ,
119+ body : {
120+ message : localizeChannelConnectMessage ( error , locale ) ,
121+ code : error . code ,
122+ requestId,
123+ retryable : error . retryable ,
124+ phase : error . phase ,
125+ } ,
126+ upstreamHost : error . upstreamHost ,
127+ upstreamStatus : error . upstreamStatus ,
128+ } as const ;
129+ }
130+
131+ return {
132+ status : 502 ,
133+ body : {
134+ message : localizeChannelConnectMessage ( error , locale ) ,
135+ code : "network_error" ,
136+ requestId,
137+ retryable : true ,
138+ phase : "verify_credentials" ,
139+ } ,
140+ upstreamHost : null ,
141+ upstreamStatus : null ,
142+ } as const ;
143+ }
144+
145+ function logChannelConnectFailure (
146+ container : ControllerContainer ,
147+ input : {
148+ requestId : string ;
149+ channel : "discord" | "telegram" ;
150+ locale : ControllerLocale ;
151+ error : unknown ;
152+ } ,
153+ ) : {
154+ status : 422 | 502 | 503 | 504 ;
155+ body : z . infer < typeof channelConnectErrorSchema > ;
156+ } {
157+ const response = getChannelConnectErrorResponse (
158+ input . requestId ,
159+ input . locale ,
160+ input . error ,
161+ ) ;
162+ const proxyEnv = readProxyFetchEnv ( ) ;
163+ const proxyTargetBypassed = response . upstreamHost
164+ ? shouldBypassProxy ( response . upstreamHost , proxyEnv . noProxy )
165+ : null ;
166+
167+ logger . error (
168+ {
169+ requestId : input . requestId ,
170+ channel : input . channel ,
171+ error :
172+ input . error instanceof Error
173+ ? input . error . message
174+ : String ( input . error ) ,
175+ errorCode : response . body . code ,
176+ errorPhase : response . body . phase ,
177+ retryable : response . body . retryable ,
178+ httpStatus : response . status ,
179+ upstreamHost : response . upstreamHost ,
180+ upstreamStatus : response . upstreamStatus ,
181+ proxy : {
182+ httpProxyRedacted : redactProxyUrl ( proxyEnv . httpProxy ) ,
183+ httpsProxyRedacted : redactProxyUrl ( proxyEnv . httpsProxy ) ,
184+ allProxyRedacted : redactProxyUrl ( proxyEnv . allProxy ) ,
185+ noProxy : proxyEnv . noProxy ,
186+ bypassedForUpstream : proxyTargetBypassed ,
187+ } ,
188+ runtimeState : {
189+ status : container . runtimeState . status ,
190+ configSyncStatus : container . runtimeState . configSyncStatus ,
191+ skillsSyncStatus : container . runtimeState . skillsSyncStatus ,
192+ templatesSyncStatus : container . runtimeState . templatesSyncStatus ,
193+ gatewayStatus : container . runtimeState . gatewayStatus ,
194+ lastGatewayProbeAt : container . runtimeState . lastGatewayProbeAt ,
195+ lastGatewayError : container . runtimeState . lastGatewayError ,
196+ } ,
197+ runtimeEnv : {
198+ manageOpenclawProcess : container . env . manageOpenclawProcess ,
199+ gatewayProbeEnabled : container . env . gatewayProbeEnabled ,
200+ openclawBaseUrl : getOpenclawOrigin ( container ) ,
201+ } ,
202+ } ,
203+ "channel_connect_failure" ,
204+ ) ;
205+
206+ void container . runtimeHealth
207+ . probe ( { timeoutMs : 1500 } )
208+ . then ( ( runtimeHealth ) => {
209+ logger . warn (
210+ {
211+ requestId : input . requestId ,
212+ channel : input . channel ,
213+ errorCode : response . body . code ,
214+ errorPhase : response . body . phase ,
215+ runtimeHealth,
216+ process : {
217+ nodeVersion : process . version ,
218+ platform : process . platform ,
219+ arch : process . arch ,
220+ } ,
221+ } ,
222+ "channel_connect_failure_context" ,
223+ ) ;
224+ } )
225+ . catch ( ( captureError : unknown ) => {
226+ logger . warn (
227+ {
228+ requestId : input . requestId ,
229+ channel : input . channel ,
230+ error :
231+ captureError instanceof Error
232+ ? captureError . message
233+ : String ( captureError ) ,
234+ } ,
235+ "channel_connect_failure_context_failed" ,
236+ ) ;
237+ } ) ;
238+
239+ return {
240+ status : response . status ,
241+ body : response . body ,
242+ } ;
243+ }
31244
32245export function registerChannelRoutes (
33246 app : OpenAPIHono < ControllerBindings > ,
@@ -159,10 +372,30 @@ export function registerChannelRoutes(
159372 content : { "application/json" : { schema : channelResponseSchema } } ,
160373 description : "Connected discord channel" ,
161374 } ,
162- 409 : {
163- content : { "application/json" : { schema : errorSchema } } ,
375+ 422 : {
376+ content : {
377+ "application/json" : { schema : channelConnectErrorSchema } ,
378+ } ,
164379 description : "Invalid credentials" ,
165380 } ,
381+ 502 : {
382+ content : {
383+ "application/json" : { schema : channelConnectErrorSchema } ,
384+ } ,
385+ description : "Upstream network or proxy failure" ,
386+ } ,
387+ 503 : {
388+ content : {
389+ "application/json" : { schema : channelConnectErrorSchema } ,
390+ } ,
391+ description : "Local runtime sync failed" ,
392+ } ,
393+ 504 : {
394+ content : {
395+ "application/json" : { schema : channelConnectErrorSchema } ,
396+ } ,
397+ description : "Upstream timeout" ,
398+ } ,
166399 } ,
167400 } ) ,
168401 async ( c ) => {
@@ -172,17 +405,15 @@ export function registerChannelRoutes(
172405 200 ,
173406 ) ;
174407 } catch ( error ) {
175- logger . error (
176- { error : error instanceof Error ? error . message : String ( error ) } ,
177- "channel_connect_error_discord" ,
178- ) ;
179- return c . json (
180- {
181- message :
182- error instanceof Error ? error . message : "Discord connect failed" ,
183- } ,
184- 409 ,
185- ) ;
408+ const requestId = c . get ( "requestId" ) ;
409+ const locale = await getControllerLocale ( container ) ;
410+ const response = logChannelConnectFailure ( container , {
411+ requestId,
412+ channel : "discord" ,
413+ locale,
414+ error,
415+ } ) ;
416+ return c . json ( response . body , response . status ) ;
186417 }
187418 } ,
188419 ) ;
@@ -246,10 +477,30 @@ export function registerChannelRoutes(
246477 content : { "application/json" : { schema : channelResponseSchema } } ,
247478 description : "Connected telegram channel" ,
248479 } ,
249- 409 : {
250- content : { "application/json" : { schema : errorSchema } } ,
480+ 422 : {
481+ content : {
482+ "application/json" : { schema : channelConnectErrorSchema } ,
483+ } ,
251484 description : "Invalid credentials" ,
252485 } ,
486+ 502 : {
487+ content : {
488+ "application/json" : { schema : channelConnectErrorSchema } ,
489+ } ,
490+ description : "Upstream network or proxy failure" ,
491+ } ,
492+ 503 : {
493+ content : {
494+ "application/json" : { schema : channelConnectErrorSchema } ,
495+ } ,
496+ description : "Local runtime sync failed" ,
497+ } ,
498+ 504 : {
499+ content : {
500+ "application/json" : { schema : channelConnectErrorSchema } ,
501+ } ,
502+ description : "Upstream timeout" ,
503+ } ,
253504 } ,
254505 } ) ,
255506 async ( c ) => {
@@ -259,19 +510,15 @@ export function registerChannelRoutes(
259510 200 ,
260511 ) ;
261512 } catch ( error ) {
262- logger . error (
263- { error : error instanceof Error ? error . message : String ( error ) } ,
264- "channel_connect_error_telegram" ,
265- ) ;
266- return c . json (
267- {
268- message :
269- error instanceof Error
270- ? error . message
271- : "Telegram connect failed" ,
272- } ,
273- 409 ,
274- ) ;
513+ const requestId = c . get ( "requestId" ) ;
514+ const locale = await getControllerLocale ( container ) ;
515+ const response = logChannelConnectFailure ( container , {
516+ requestId,
517+ channel : "telegram" ,
518+ locale,
519+ error,
520+ } ) ;
521+ return c . json ( response . body , response . status ) ;
275522 }
276523 } ,
277524 ) ;
0 commit comments