|
6 | 6 | * globals, fully supporting multi-account concurrent operation. |
7 | 7 | */ |
8 | 8 |
|
| 9 | +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; |
9 | 10 | import type { EngineLogger } from "../types.js"; |
10 | 11 | import { formatErrorMessage } from "../utils/format.js"; |
11 | 12 |
|
@@ -207,56 +208,67 @@ export class TokenManager { |
207 | 208 | this.logger?.debug?.(`[qqbot:token:${appId}] >>> POST ${TOKEN_URL}`); |
208 | 209 |
|
209 | 210 | let response: Response; |
| 211 | + let release: (() => Promise<void>) | undefined; |
210 | 212 | try { |
211 | | - response = await fetch(TOKEN_URL, { |
212 | | - method: "POST", |
213 | | - headers: { |
214 | | - "Content-Type": "application/json", |
215 | | - "User-Agent": this.resolveUserAgent(), |
| 213 | + const guarded = await fetchWithSsrFGuard({ |
| 214 | + url: TOKEN_URL, |
| 215 | + auditContext: "qqbot-token", |
| 216 | + init: { |
| 217 | + method: "POST", |
| 218 | + headers: { |
| 219 | + "Content-Type": "application/json", |
| 220 | + "User-Agent": this.resolveUserAgent(), |
| 221 | + }, |
| 222 | + body: JSON.stringify({ appId, clientSecret }), |
216 | 223 | }, |
217 | | - body: JSON.stringify({ appId, clientSecret }), |
218 | 224 | }); |
| 225 | + response = guarded.response; |
| 226 | + release = guarded.release; |
219 | 227 | } catch (err) { |
220 | 228 | this.logger?.error?.(`[qqbot:token:${appId}] Network error: ${formatErrorMessage(err)}`); |
221 | 229 | throw new Error(`Network error getting access_token: ${formatErrorMessage(err)}`, { |
222 | 230 | cause: err, |
223 | 231 | }); |
224 | 232 | } |
225 | 233 |
|
226 | | - const traceId = response.headers.get("x-tps-trace-id") ?? ""; |
227 | | - this.logger?.debug?.( |
228 | | - `[qqbot:token:${appId}] <<< ${response.status}${traceId ? ` | TraceId: ${traceId}` : ""}`, |
229 | | - ); |
230 | | - |
231 | | - let rawBody: string; |
232 | 234 | try { |
233 | | - rawBody = await response.text(); |
234 | | - } catch (err) { |
235 | | - throw new Error(`Failed to read access_token response: ${formatErrorMessage(err)}`, { |
236 | | - cause: err, |
237 | | - }); |
238 | | - } |
239 | | - const logBody = rawBody.replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token": "***"'); |
240 | | - this.logger?.debug?.(`[qqbot:token:${appId}] <<< Body: ${logBody}`); |
| 235 | + const traceId = response.headers.get("x-tps-trace-id") ?? ""; |
| 236 | + this.logger?.debug?.( |
| 237 | + `[qqbot:token:${appId}] <<< ${response.status}${traceId ? ` | TraceId: ${traceId}` : ""}`, |
| 238 | + ); |
241 | 239 |
|
242 | | - let data: { access_token?: string; expires_in?: number }; |
243 | | - try { |
244 | | - data = JSON.parse(rawBody); |
245 | | - } catch { |
246 | | - throw new Error("QQBot access_token response was malformed JSON"); |
247 | | - } |
| 240 | + let rawBody: string; |
| 241 | + try { |
| 242 | + rawBody = await response.text(); |
| 243 | + } catch (err) { |
| 244 | + throw new Error(`Failed to read access_token response: ${formatErrorMessage(err)}`, { |
| 245 | + cause: err, |
| 246 | + }); |
| 247 | + } |
| 248 | + const logBody = rawBody.replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token": "***"'); |
| 249 | + this.logger?.debug?.(`[qqbot:token:${appId}] <<< Body: ${logBody}`); |
248 | 250 |
|
249 | | - if (!data.access_token) { |
250 | | - throw new Error(`Failed to get access_token: ${JSON.stringify(data)}`); |
251 | | - } |
| 251 | + let data: { access_token?: string; expires_in?: number }; |
| 252 | + try { |
| 253 | + data = JSON.parse(rawBody); |
| 254 | + } catch { |
| 255 | + throw new Error("QQBot access_token response was malformed JSON"); |
| 256 | + } |
252 | 257 |
|
253 | | - const expiresAt = Date.now() + (data.expires_in ?? 7200) * 1000; |
254 | | - this.cache.set(appId, { token: data.access_token, expiresAt, appId }); |
255 | | - this.logger?.debug?.( |
256 | | - `[qqbot:token:${appId}] Cached, expires at: ${new Date(expiresAt).toISOString()}`, |
257 | | - ); |
| 258 | + if (!data.access_token) { |
| 259 | + throw new Error(`Failed to get access_token: ${JSON.stringify(data)}`); |
| 260 | + } |
| 261 | + |
| 262 | + const expiresAt = Date.now() + (data.expires_in ?? 7200) * 1000; |
| 263 | + this.cache.set(appId, { token: data.access_token, expiresAt, appId }); |
| 264 | + this.logger?.debug?.( |
| 265 | + `[qqbot:token:${appId}] Cached, expires at: ${new Date(expiresAt).toISOString()}`, |
| 266 | + ); |
258 | 267 |
|
259 | | - return data.access_token; |
| 268 | + return data.access_token; |
| 269 | + } finally { |
| 270 | + await release?.(); |
| 271 | + } |
260 | 272 | } |
261 | 273 |
|
262 | 274 | private abortableSleep(ms: number, signal: AbortSignal): Promise<void> { |
|
0 commit comments