mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 19:45:08 +08:00
perf(feishu): prefetch bot open_id + DCL + 60s negative cache
This commit is contained in:
parent
73ab31a13c
commit
3c3c2a938f
@ -99,9 +99,23 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
|||||||
/** 旧事件过滤默认阈值(秒):超过 30 秒的事件视为重连后回放 */
|
/** 旧事件过滤默认阈值(秒):超过 30 秒的事件视为重连后回放 */
|
||||||
private static final long DEFAULT_STALE_THRESHOLD_SECONDS = 30L;
|
private static final long DEFAULT_STALE_THRESHOLD_SECONDS = 30L;
|
||||||
|
|
||||||
/** 机器人自身 open_id 缓存(用于 require_mention 精确判断,懒加载) */
|
/** Bot's own open_id, fetched once from /open-apis/bot/v3/info and cached. */
|
||||||
private volatile String botOpenId;
|
private volatile String botOpenId;
|
||||||
|
|
||||||
|
/** Serializes lazy bot-open-id fetches so concurrent group messages share one API roundtrip. */
|
||||||
|
private final Object botOpenIdLock = new Object();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last failure timestamp for {@code /open-apis/bot/v3/info}. While the call is in the
|
||||||
|
* {@link #BOT_OPENID_FAILURE_BACKOFF_MS} negative-cache window, {@link #getBotOpenId()}
|
||||||
|
* returns {@code null} fast so a Feishu outage doesn't trigger one synchronous retry
|
||||||
|
* per inbound group message.
|
||||||
|
*/
|
||||||
|
private volatile long botOpenIdLastFailureMs = 0L;
|
||||||
|
|
||||||
|
/** Negative-cache window for {@link #getBotOpenId()} failures (60 s). */
|
||||||
|
private static final long BOT_OPENID_FAILURE_BACKOFF_MS = 60_000L;
|
||||||
|
|
||||||
public FeishuChannelAdapter(ChannelEntity channelEntity,
|
public FeishuChannelAdapter(ChannelEntity channelEntity,
|
||||||
ChannelMessageRouter messageRouter,
|
ChannelMessageRouter messageRouter,
|
||||||
ObjectMapper objectMapper) {
|
ObjectMapper objectMapper) {
|
||||||
@ -132,6 +146,12 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
|||||||
// 定时刷新 Token:过期前 5 分钟自动刷新
|
// 定时刷新 Token:过期前 5 分钟自动刷新
|
||||||
scheduleTokenRefresh();
|
scheduleTokenRefresh();
|
||||||
|
|
||||||
|
// Prefetch the bot's own open_id once the token is valid so the first
|
||||||
|
// require_mention check on the WebSocket dispatch thread doesn't pay
|
||||||
|
// the 5 s API latency. Failure is non-fatal — getBotOpenId() handles
|
||||||
|
// it and the negative cache keeps subsequent retries cheap.
|
||||||
|
getBotOpenId();
|
||||||
|
|
||||||
String connectionMode = getConfigString("connection_mode", "websocket");
|
String connectionMode = getConfigString("connection_mode", "websocket");
|
||||||
if ("websocket".equals(connectionMode)) {
|
if ("websocket".equals(connectionMode)) {
|
||||||
startWebSocket(appId, appSecret);
|
startWebSocket(appId, appSecret);
|
||||||
@ -166,6 +186,8 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
|||||||
|
|
||||||
this.httpClient = null;
|
this.httpClient = null;
|
||||||
this.tenantAccessToken = null;
|
this.tenantAccessToken = null;
|
||||||
|
this.botOpenId = null;
|
||||||
|
this.botOpenIdLastFailureMs = 0L;
|
||||||
this.processedMessageIds.clear();
|
this.processedMessageIds.clear();
|
||||||
this.nicknameCache.clear();
|
this.nicknameCache.clear();
|
||||||
this.quotedMessageCache.clear();
|
this.quotedMessageCache.clear();
|
||||||
@ -189,6 +211,10 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
|||||||
log.warn("[feishu] Token refresh during reconnect failed: {}", e.getMessage());
|
log.warn("[feishu] Token refresh during reconnect failed: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-prefetch bot open_id after reconnect: same dispatch-thread latency
|
||||||
|
// concern as doStart, plus picks up a rotated app identity if any.
|
||||||
|
getBotOpenId();
|
||||||
|
|
||||||
if ("websocket".equals(connectionMode)) {
|
if ("websocket".equals(connectionMode)) {
|
||||||
log.info("[feishu] Reconnecting WebSocket...");
|
log.info("[feishu] Reconnecting WebSocket...");
|
||||||
stopWebSocket();
|
stopWebSocket();
|
||||||
@ -459,33 +485,59 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the bot's own open_id once and caches it. Returns {@code null} when
|
* Fetches the bot's own open_id once and caches it. Returns {@code null}
|
||||||
* the fetch fails — callers that consult this value (currently the
|
* when the identity is unavailable; callers (currently the
|
||||||
* {@code require_mention} gate) must treat {@code null} as "identity unknown"
|
* {@code require_mention} gate) treat {@code null} as "identity unknown"
|
||||||
* and fall open so a transient API outage doesn't silence the bot.
|
* and fall open so a transient API outage doesn't silence the bot.
|
||||||
|
*
|
||||||
|
* <p>Concurrent callers share a single API roundtrip via
|
||||||
|
* {@link #botOpenIdLock}. On failure, {@link #botOpenIdLastFailureMs} is
|
||||||
|
* stamped so callers within the next {@link #BOT_OPENID_FAILURE_BACKOFF_MS}
|
||||||
|
* ms return {@code null} immediately instead of triggering a fresh 5 s
|
||||||
|
* synchronous fetch per inbound message.
|
||||||
*/
|
*/
|
||||||
private String getBotOpenId() {
|
private String getBotOpenId() {
|
||||||
if (botOpenId != null) return botOpenId;
|
String cached = botOpenId;
|
||||||
try {
|
if (cached != null) return cached;
|
||||||
ensureTokenValid();
|
if (withinFailureBackoff()) return null;
|
||||||
String apiBase = getApiBaseUrl();
|
synchronized (botOpenIdLock) {
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
// Re-check under the lock — another thread may have populated the
|
||||||
.uri(URI.create(apiBase + "/open-apis/bot/v3/info"))
|
// cache or stamped a fresh failure while we were waiting.
|
||||||
.header("Authorization", "Bearer " + tenantAccessToken)
|
if (botOpenId != null) return botOpenId;
|
||||||
.GET()
|
if (withinFailureBackoff()) return null;
|
||||||
.timeout(Duration.ofSeconds(5))
|
try {
|
||||||
.build();
|
ensureTokenValid();
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
String apiBase = getApiBaseUrl();
|
||||||
Map<?, ?> body = objectMapper.readValue(response.body(), Map.class);
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
Map<?, ?> bot = (Map<?, ?>) body.get("bot");
|
.uri(URI.create(apiBase + "/open-apis/bot/v3/info"))
|
||||||
if (bot != null) {
|
.header("Authorization", "Bearer " + tenantAccessToken)
|
||||||
botOpenId = (String) bot.get("open_id");
|
.GET()
|
||||||
log.info("[feishu] Bot open_id fetched and cached: {}", botOpenId);
|
.timeout(Duration.ofSeconds(5))
|
||||||
|
.build();
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
Map<?, ?> body = objectMapper.readValue(response.body(), Map.class);
|
||||||
|
Map<?, ?> bot = (Map<?, ?>) body.get("bot");
|
||||||
|
if (bot != null && bot.get("open_id") instanceof String openId && !openId.isBlank()) {
|
||||||
|
botOpenId = openId;
|
||||||
|
log.info("[feishu] Bot open_id fetched and cached: {}", openId);
|
||||||
|
return openId;
|
||||||
|
}
|
||||||
|
// 2xx with no bot.open_id field → treat as transient failure.
|
||||||
|
botOpenIdLastFailureMs = System.currentTimeMillis();
|
||||||
|
log.warn("[feishu] /open-apis/bot/v3/info returned no bot.open_id; require_mention gate falls open for {}s",
|
||||||
|
BOT_OPENID_FAILURE_BACKOFF_MS / 1000);
|
||||||
|
} catch (Exception e) {
|
||||||
|
botOpenIdLastFailureMs = System.currentTimeMillis();
|
||||||
|
log.warn("[feishu] Failed to fetch bot open_id (require_mention gate falls open for {}s): {}",
|
||||||
|
BOT_OPENID_FAILURE_BACKOFF_MS / 1000, e.getMessage());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
return null;
|
||||||
log.warn("[feishu] Failed to fetch bot open_id; require_mention gate will fall open until next attempt: {}", e.getMessage());
|
|
||||||
}
|
}
|
||||||
return botOpenId;
|
}
|
||||||
|
|
||||||
|
private boolean withinFailureBackoff() {
|
||||||
|
long last = botOpenIdLastFailureMs;
|
||||||
|
return last != 0L && System.currentTimeMillis() - last < BOT_OPENID_FAILURE_BACKOFF_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Token 管理 ====================
|
// ==================== Token 管理 ====================
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user