@openclaw-china/dingtalk 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +18 -0
- package/dist/index.js +285 -40
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -532,6 +532,24 @@ interface PluginRuntime {
|
|
|
532
532
|
agentId?: string;
|
|
533
533
|
};
|
|
534
534
|
};
|
|
535
|
+
session?: {
|
|
536
|
+
resolveStorePath?: (store: unknown, params: {
|
|
537
|
+
agentId?: string;
|
|
538
|
+
}) => string | undefined;
|
|
539
|
+
recordInboundSession?: (params: {
|
|
540
|
+
storePath: string;
|
|
541
|
+
sessionKey: string;
|
|
542
|
+
ctx: unknown;
|
|
543
|
+
updateLastRoute?: {
|
|
544
|
+
sessionKey: string;
|
|
545
|
+
channel: string;
|
|
546
|
+
to: string;
|
|
547
|
+
accountId?: string;
|
|
548
|
+
threadId?: string | number;
|
|
549
|
+
};
|
|
550
|
+
onRecordError?: (err: unknown) => void;
|
|
551
|
+
}) => Promise<void>;
|
|
552
|
+
};
|
|
535
553
|
reply?: {
|
|
536
554
|
dispatchReplyFromConfig?: (params: {
|
|
537
555
|
ctx: unknown;
|
package/dist/index.js
CHANGED
|
@@ -4088,7 +4088,7 @@ var DingtalkConfigSchema = external_exports.object({
|
|
|
4088
4088
|
/** 媒体文件大小限制 (MB),默认 100MB */
|
|
4089
4089
|
maxFileSizeMB: external_exports.number().positive().optional().default(100),
|
|
4090
4090
|
/** 仅发送最终回复(非流式) */
|
|
4091
|
-
replyFinalOnly: external_exports.boolean().optional().default(
|
|
4091
|
+
replyFinalOnly: external_exports.boolean().optional().default(false)
|
|
4092
4092
|
});
|
|
4093
4093
|
function isConfigured(config) {
|
|
4094
4094
|
return Boolean(config?.clientId && config?.clientSecret);
|
|
@@ -4110,7 +4110,8 @@ function createDingtalkClient(opts) {
|
|
|
4110
4110
|
}
|
|
4111
4111
|
const client = new DWClient({
|
|
4112
4112
|
clientId: opts.clientId,
|
|
4113
|
-
clientSecret: opts.clientSecret
|
|
4113
|
+
clientSecret: opts.clientSecret,
|
|
4114
|
+
...opts.ua ? { ua: opts.ua } : {}
|
|
4114
4115
|
});
|
|
4115
4116
|
cachedClient = client;
|
|
4116
4117
|
cachedConfig = {
|
|
@@ -4124,7 +4125,10 @@ function createDingtalkClientFromConfig(cfg) {
|
|
|
4124
4125
|
if (!creds) {
|
|
4125
4126
|
throw new Error("DingTalk credentials not configured (clientId, clientSecret required)");
|
|
4126
4127
|
}
|
|
4127
|
-
return createDingtalkClient(
|
|
4128
|
+
return createDingtalkClient({
|
|
4129
|
+
...creds,
|
|
4130
|
+
ua: "openclaw-dingtalk"
|
|
4131
|
+
});
|
|
4128
4132
|
}
|
|
4129
4133
|
var DINGTALK_OAUTH_URL = "https://api.dingtalk.com/v1.0/oauth2/accessToken";
|
|
4130
4134
|
var TOKEN_REQUEST_TIMEOUT = 1e4;
|
|
@@ -4954,6 +4958,96 @@ function extractMediaFromText(text, options = {}) {
|
|
|
4954
4958
|
all: [...images, ...files]
|
|
4955
4959
|
};
|
|
4956
4960
|
}
|
|
4961
|
+
|
|
4962
|
+
// ../../packages/shared/src/cron/index.ts
|
|
4963
|
+
var CRON_HIDDEN_PROMPT = `
|
|
4964
|
+
When creating or updating a cron task, use the main session's last route as the delivery target.
|
|
4965
|
+
- Do not ask the user for userId/chatId
|
|
4966
|
+
- sessionTarget="isolated"
|
|
4967
|
+
- payload.kind="agentTurn"
|
|
4968
|
+
- payload.deliver=true
|
|
4969
|
+
- Do not set payload.channel/to (fall back to last route automatically)
|
|
4970
|
+
- Do not call the message tool to send`;
|
|
4971
|
+
var CRON_TRIGGER_KEYWORDS = [
|
|
4972
|
+
"\u5B9A\u65F6",
|
|
4973
|
+
"\u63D0\u9192",
|
|
4974
|
+
"\u6BCF\u5206\u949F",
|
|
4975
|
+
"\u6BCF\u5C0F\u65F6",
|
|
4976
|
+
"\u6BCF\u5929",
|
|
4977
|
+
"\u6BCF\u5468",
|
|
4978
|
+
"\u51E0\u70B9",
|
|
4979
|
+
"\u65E9\u4E0A",
|
|
4980
|
+
"\u665A\u4E0A",
|
|
4981
|
+
"\u5DE5\u4F5C\u65E5",
|
|
4982
|
+
"cron",
|
|
4983
|
+
"remind",
|
|
4984
|
+
"reminder",
|
|
4985
|
+
"schedule",
|
|
4986
|
+
"scheduled",
|
|
4987
|
+
"every minute",
|
|
4988
|
+
"every hour",
|
|
4989
|
+
"every day",
|
|
4990
|
+
"daily",
|
|
4991
|
+
"every week",
|
|
4992
|
+
"weekly",
|
|
4993
|
+
"weekday",
|
|
4994
|
+
"workday",
|
|
4995
|
+
"morning",
|
|
4996
|
+
"evening"
|
|
4997
|
+
];
|
|
4998
|
+
var CRON_TRIGGER_PATTERNS = [
|
|
4999
|
+
/提醒我/u,
|
|
5000
|
+
/帮我定时/u,
|
|
5001
|
+
/每.+提醒/u,
|
|
5002
|
+
/每天.+发/u,
|
|
5003
|
+
/remind me/iu,
|
|
5004
|
+
/set (a )?reminder/iu,
|
|
5005
|
+
/every .+ remind/iu,
|
|
5006
|
+
/every day .+ (send|post|notify)/iu,
|
|
5007
|
+
/schedule .+ (reminder|message|notification)/iu
|
|
5008
|
+
];
|
|
5009
|
+
var CRON_EXCLUDE_PATTERNS = [
|
|
5010
|
+
/是什么意思/u,
|
|
5011
|
+
/区别/u,
|
|
5012
|
+
/为什么/u,
|
|
5013
|
+
/\bhelp\b/iu,
|
|
5014
|
+
/文档/u,
|
|
5015
|
+
/怎么用/u,
|
|
5016
|
+
/what does|what's|meaning of/iu,
|
|
5017
|
+
/difference/iu,
|
|
5018
|
+
/why/iu,
|
|
5019
|
+
/\bdocs?\b/iu,
|
|
5020
|
+
/documentation/iu,
|
|
5021
|
+
/how to/iu,
|
|
5022
|
+
/usage/iu
|
|
5023
|
+
];
|
|
5024
|
+
function shouldInjectCronHiddenPrompt(text) {
|
|
5025
|
+
const normalized = text.trim();
|
|
5026
|
+
if (!normalized) return false;
|
|
5027
|
+
const lowered = normalized.toLowerCase();
|
|
5028
|
+
for (const pattern of CRON_EXCLUDE_PATTERNS) {
|
|
5029
|
+
if (pattern.test(lowered)) return false;
|
|
5030
|
+
}
|
|
5031
|
+
for (const keyword of CRON_TRIGGER_KEYWORDS) {
|
|
5032
|
+
if (lowered.includes(keyword.toLowerCase())) return true;
|
|
5033
|
+
}
|
|
5034
|
+
return CRON_TRIGGER_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
5035
|
+
}
|
|
5036
|
+
function splitCronHiddenPrompt(text) {
|
|
5037
|
+
const idx = text.indexOf(CRON_HIDDEN_PROMPT);
|
|
5038
|
+
if (idx === -1) {
|
|
5039
|
+
return { base: text };
|
|
5040
|
+
}
|
|
5041
|
+
const base = text.slice(0, idx).trimEnd();
|
|
5042
|
+
return { base, prompt: CRON_HIDDEN_PROMPT };
|
|
5043
|
+
}
|
|
5044
|
+
function appendCronHiddenPrompt(text) {
|
|
5045
|
+
if (!shouldInjectCronHiddenPrompt(text)) return text;
|
|
5046
|
+
if (text.includes(CRON_HIDDEN_PROMPT)) return text;
|
|
5047
|
+
return `${text}
|
|
5048
|
+
|
|
5049
|
+
${CRON_HIDDEN_PROMPT}`;
|
|
5050
|
+
}
|
|
4957
5051
|
var FileSizeLimitError2 = class _FileSizeLimitError extends Error {
|
|
4958
5052
|
/** Actual file size in bytes */
|
|
4959
5053
|
actualSize;
|
|
@@ -5975,7 +6069,8 @@ async function finishAICard(card, content, log) {
|
|
|
5975
6069
|
|
|
5976
6070
|
// src/bot.ts
|
|
5977
6071
|
function buildGatewayUserContent(inboundCtx, logger) {
|
|
5978
|
-
const base = inboundCtx.Body ?? "";
|
|
6072
|
+
const base = inboundCtx.CommandBody ?? inboundCtx.Body ?? "";
|
|
6073
|
+
const { base: baseText, prompt } = splitCronHiddenPrompt(base);
|
|
5979
6074
|
const rawPaths = [];
|
|
5980
6075
|
if (typeof inboundCtx.MediaPath === "string") {
|
|
5981
6076
|
rawPaths.push(inboundCtx.MediaPath);
|
|
@@ -5995,13 +6090,18 @@ function buildGatewayUserContent(inboundCtx, logger) {
|
|
|
5995
6090
|
files.add(localPath);
|
|
5996
6091
|
}
|
|
5997
6092
|
if (files.size === 0) {
|
|
5998
|
-
return
|
|
6093
|
+
return prompt ? `${baseText}
|
|
6094
|
+
|
|
6095
|
+
${prompt}` : baseText;
|
|
5999
6096
|
}
|
|
6000
6097
|
const list = Array.from(files).map((p) => `- ${p}`).join("\n");
|
|
6001
|
-
|
|
6098
|
+
const content = `${baseText}
|
|
6002
6099
|
|
|
6003
6100
|
[local files]
|
|
6004
6101
|
${list}`;
|
|
6102
|
+
return prompt ? `${content}
|
|
6103
|
+
|
|
6104
|
+
${prompt}` : content;
|
|
6005
6105
|
}
|
|
6006
6106
|
function extractLocalMediaFromText(params) {
|
|
6007
6107
|
const { text, logger } = params;
|
|
@@ -6046,6 +6146,22 @@ function extractMediaLinesFromText(params) {
|
|
|
6046
6146
|
const mediaUrls = result.all.map((m) => m.isLocal ? m.localPath ?? m.source : m.source).filter((m) => typeof m === "string" && m.trim().length > 0);
|
|
6047
6147
|
return { text: result.text, mediaUrls };
|
|
6048
6148
|
}
|
|
6149
|
+
function resolveAudioRecognition(raw) {
|
|
6150
|
+
if (raw.msgtype !== "audio") return void 0;
|
|
6151
|
+
if (!raw.content) return void 0;
|
|
6152
|
+
const contentObj = typeof raw.content === "string" ? (() => {
|
|
6153
|
+
try {
|
|
6154
|
+
return JSON.parse(raw.content);
|
|
6155
|
+
} catch {
|
|
6156
|
+
return null;
|
|
6157
|
+
}
|
|
6158
|
+
})() : raw.content;
|
|
6159
|
+
if (!contentObj || typeof contentObj !== "object") return void 0;
|
|
6160
|
+
const recognition = contentObj.recognition;
|
|
6161
|
+
if (typeof recognition !== "string") return void 0;
|
|
6162
|
+
const trimmed = recognition.trim();
|
|
6163
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
6164
|
+
}
|
|
6049
6165
|
function resolveGatewayAuthFromConfigFile(logger) {
|
|
6050
6166
|
try {
|
|
6051
6167
|
const fs5 = __require("fs");
|
|
@@ -6152,16 +6268,10 @@ function parseDingtalkMessage(raw) {
|
|
|
6152
6268
|
let content = "";
|
|
6153
6269
|
if (raw.msgtype === "text" && raw.text?.content) {
|
|
6154
6270
|
content = raw.text.content.trim();
|
|
6155
|
-
} else if (raw.msgtype === "audio"
|
|
6156
|
-
const
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
} catch {
|
|
6160
|
-
return null;
|
|
6161
|
-
}
|
|
6162
|
-
})() : raw.content;
|
|
6163
|
-
if (contentObj && typeof contentObj === "object" && "recognition" in contentObj && typeof contentObj.recognition === "string") {
|
|
6164
|
-
content = contentObj.recognition.trim();
|
|
6271
|
+
} else if (raw.msgtype === "audio") {
|
|
6272
|
+
const recognition = resolveAudioRecognition(raw);
|
|
6273
|
+
if (recognition) {
|
|
6274
|
+
content = recognition;
|
|
6165
6275
|
}
|
|
6166
6276
|
}
|
|
6167
6277
|
const mentionedBot = resolveMentionedBot(raw);
|
|
@@ -6383,6 +6493,7 @@ async function handleDingtalkMessage(params) {
|
|
|
6383
6493
|
});
|
|
6384
6494
|
const ctx = parseDingtalkMessage(raw);
|
|
6385
6495
|
const isGroup = ctx.chatType === "group";
|
|
6496
|
+
const audioRecognition = resolveAudioRecognition(raw);
|
|
6386
6497
|
logger.debug(`raw message: msgtype=${raw.msgtype}, hasText=${!!raw.text?.content}, hasContent=${!!raw.content}, textContent="${raw.text?.content ?? ""}"`);
|
|
6387
6498
|
if (raw.msgtype === "richText") {
|
|
6388
6499
|
try {
|
|
@@ -6472,30 +6583,34 @@ async function handleDingtalkMessage(params) {
|
|
|
6472
6583
|
let richTextParseResult = null;
|
|
6473
6584
|
const mediaTypes = ["picture", "video", "audio", "file"];
|
|
6474
6585
|
if (mediaTypes.includes(raw.msgtype)) {
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6586
|
+
if (raw.msgtype === "audio" && audioRecognition) {
|
|
6587
|
+
logger.debug("[audio] recognition present; treat as text and skip audio file download");
|
|
6588
|
+
} else {
|
|
6589
|
+
try {
|
|
6590
|
+
extractedFileInfo = extractFileFromMessage(raw);
|
|
6591
|
+
if (extractedFileInfo && channelCfg?.clientId && channelCfg?.clientSecret) {
|
|
6592
|
+
const accessToken = await getAccessToken(channelCfg.clientId, channelCfg.clientSecret);
|
|
6593
|
+
downloadedMedia = await downloadDingTalkFile({
|
|
6594
|
+
downloadCode: extractedFileInfo.downloadCode,
|
|
6595
|
+
robotCode: channelCfg.clientId,
|
|
6596
|
+
accessToken,
|
|
6597
|
+
fileName: extractedFileInfo.fileName,
|
|
6598
|
+
msgType: extractedFileInfo.msgType,
|
|
6599
|
+
log: logger,
|
|
6600
|
+
maxFileSizeMB: channelCfg.maxFileSizeMB
|
|
6601
|
+
});
|
|
6602
|
+
logger.debug(`downloaded media file: ${downloadedMedia.path} (${downloadedMedia.size} bytes)`);
|
|
6603
|
+
mediaBody = buildFileContextMessage(
|
|
6604
|
+
extractedFileInfo.msgType,
|
|
6605
|
+
extractedFileInfo.fileName
|
|
6606
|
+
);
|
|
6607
|
+
}
|
|
6608
|
+
} catch (err) {
|
|
6609
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
6610
|
+
logger.warn(`media download failed, continuing with text: ${errorMessage}`);
|
|
6611
|
+
downloadedMedia = null;
|
|
6612
|
+
extractedFileInfo = null;
|
|
6493
6613
|
}
|
|
6494
|
-
} catch (err) {
|
|
6495
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
6496
|
-
logger.warn(`media download failed, continuing with text: ${errorMessage}`);
|
|
6497
|
-
downloadedMedia = null;
|
|
6498
|
-
extractedFileInfo = null;
|
|
6499
6614
|
}
|
|
6500
6615
|
}
|
|
6501
6616
|
if (raw.msgtype === "richText") {
|
|
@@ -6547,6 +6662,9 @@ async function handleDingtalkMessage(params) {
|
|
|
6547
6662
|
}
|
|
6548
6663
|
}
|
|
6549
6664
|
const inboundCtx = buildInboundContext(ctx, route?.sessionKey, route?.accountId);
|
|
6665
|
+
if (audioRecognition) {
|
|
6666
|
+
inboundCtx.Transcript = audioRecognition;
|
|
6667
|
+
}
|
|
6550
6668
|
if (downloadedMedia) {
|
|
6551
6669
|
inboundCtx.MediaPath = downloadedMedia.path;
|
|
6552
6670
|
inboundCtx.MediaType = downloadedMedia.contentType;
|
|
@@ -6583,6 +6701,51 @@ async function handleDingtalkMessage(params) {
|
|
|
6583
6701
|
}
|
|
6584
6702
|
const finalizeInboundContext = replyApi?.finalizeInboundContext;
|
|
6585
6703
|
const finalCtx = finalizeInboundContext ? finalizeInboundContext(inboundCtx) : inboundCtx;
|
|
6704
|
+
let cronSource = "";
|
|
6705
|
+
let cronBase = "";
|
|
6706
|
+
if (typeof finalCtx.RawBody === "string" && finalCtx.RawBody) {
|
|
6707
|
+
cronSource = "RawBody";
|
|
6708
|
+
cronBase = finalCtx.RawBody;
|
|
6709
|
+
} else if (typeof finalCtx.Body === "string" && finalCtx.Body) {
|
|
6710
|
+
cronSource = "Body";
|
|
6711
|
+
cronBase = finalCtx.Body;
|
|
6712
|
+
} else if (typeof finalCtx.CommandBody === "string" && finalCtx.CommandBody) {
|
|
6713
|
+
cronSource = "CommandBody";
|
|
6714
|
+
cronBase = finalCtx.CommandBody;
|
|
6715
|
+
}
|
|
6716
|
+
if (cronBase) {
|
|
6717
|
+
const nextCron = appendCronHiddenPrompt(cronBase);
|
|
6718
|
+
const injected = nextCron !== cronBase;
|
|
6719
|
+
if (injected) {
|
|
6720
|
+
finalCtx.BodyForAgent = nextCron;
|
|
6721
|
+
}
|
|
6722
|
+
}
|
|
6723
|
+
const channelSession = coreChannel?.session;
|
|
6724
|
+
const storePath = channelSession?.resolveStorePath?.(
|
|
6725
|
+
cfg?.session?.store,
|
|
6726
|
+
{ agentId: route?.agentId }
|
|
6727
|
+
);
|
|
6728
|
+
if (channelSession?.recordInboundSession && storePath) {
|
|
6729
|
+
const mainSessionKeyRaw = route?.mainSessionKey;
|
|
6730
|
+
const mainSessionKey = typeof mainSessionKeyRaw === "string" && mainSessionKeyRaw.trim() ? mainSessionKeyRaw : void 0;
|
|
6731
|
+
const updateLastRoute = !isGroup && mainSessionKey ? {
|
|
6732
|
+
sessionKey: mainSessionKey,
|
|
6733
|
+
channel: "dingtalk",
|
|
6734
|
+
to: finalCtx.OriginatingTo ?? finalCtx.To ?? `user:${ctx.senderId}`,
|
|
6735
|
+
accountId: route?.accountId
|
|
6736
|
+
} : void 0;
|
|
6737
|
+
const recordSessionKeyRaw = finalCtx.SessionKey ?? route.sessionKey;
|
|
6738
|
+
const recordSessionKey = typeof recordSessionKeyRaw === "string" && recordSessionKeyRaw.trim() ? recordSessionKeyRaw : String(recordSessionKeyRaw ?? "");
|
|
6739
|
+
await channelSession.recordInboundSession({
|
|
6740
|
+
storePath,
|
|
6741
|
+
sessionKey: recordSessionKey,
|
|
6742
|
+
ctx: finalCtx,
|
|
6743
|
+
updateLastRoute,
|
|
6744
|
+
onRecordError: (err) => {
|
|
6745
|
+
logger.error(`dingtalk: failed updating session meta: ${String(err)}`);
|
|
6746
|
+
}
|
|
6747
|
+
});
|
|
6748
|
+
}
|
|
6586
6749
|
const dingtalkCfgResolved = channelCfg;
|
|
6587
6750
|
if (!dingtalkCfgResolved) {
|
|
6588
6751
|
logger.warn("channel config missing, skipping dispatch");
|
|
@@ -6935,6 +7098,14 @@ var currentAccountId = null;
|
|
|
6935
7098
|
var currentPromise = null;
|
|
6936
7099
|
var processedMessages = /* @__PURE__ */ new Map();
|
|
6937
7100
|
var MESSAGE_DEDUP_TTL_MS = 6e4;
|
|
7101
|
+
var WATCHDOG_INTERVAL_MS = 1e4;
|
|
7102
|
+
var CONNECT_TIMEOUT_MS = 3e4;
|
|
7103
|
+
var REGISTER_TIMEOUT_MS = 3e4;
|
|
7104
|
+
var DISCONNECT_GRACE_MS = 15e3;
|
|
7105
|
+
var MIN_RECONNECT_INTERVAL_MS = 1e4;
|
|
7106
|
+
function getClientState(client) {
|
|
7107
|
+
return client;
|
|
7108
|
+
}
|
|
6938
7109
|
async function monitorDingtalkProvider(opts = {}) {
|
|
6939
7110
|
const { config, runtime: runtime2, abortSignal, accountId = "default" } = opts;
|
|
6940
7111
|
const logger = createLogger("dingtalk", {
|
|
@@ -6968,7 +7139,47 @@ async function monitorDingtalkProvider(opts = {}) {
|
|
|
6968
7139
|
logger.info(`starting Stream connection for account ${accountId}...`);
|
|
6969
7140
|
currentPromise = new Promise((resolve2, reject) => {
|
|
6970
7141
|
let stopped = false;
|
|
7142
|
+
let watchdogId = null;
|
|
7143
|
+
let lastSocket = null;
|
|
7144
|
+
let connectStartedAt = Date.now();
|
|
7145
|
+
let lastConnectedAt = null;
|
|
7146
|
+
let lastReconnectAt = 0;
|
|
7147
|
+
const attachSocketListeners = () => {
|
|
7148
|
+
const { socket } = getClientState(client);
|
|
7149
|
+
if (!socket || socket === lastSocket) return;
|
|
7150
|
+
lastSocket = socket;
|
|
7151
|
+
socket.once("open", () => {
|
|
7152
|
+
const now = Date.now();
|
|
7153
|
+
connectStartedAt = now;
|
|
7154
|
+
lastConnectedAt = now;
|
|
7155
|
+
logger.info("Stream socket opened");
|
|
7156
|
+
});
|
|
7157
|
+
socket.once("close", () => {
|
|
7158
|
+
logger.warn("Stream socket closed");
|
|
7159
|
+
});
|
|
7160
|
+
socket.once("error", (err) => {
|
|
7161
|
+
logger.warn(`Stream socket error: ${String(err)}`);
|
|
7162
|
+
});
|
|
7163
|
+
};
|
|
7164
|
+
const forceReconnect = (reason) => {
|
|
7165
|
+
const now = Date.now();
|
|
7166
|
+
if (now - lastReconnectAt < MIN_RECONNECT_INTERVAL_MS) {
|
|
7167
|
+
return;
|
|
7168
|
+
}
|
|
7169
|
+
lastReconnectAt = now;
|
|
7170
|
+
logger.warn(`[reconnect] forcing reconnect: ${reason}`);
|
|
7171
|
+
try {
|
|
7172
|
+
const { socket } = getClientState(client);
|
|
7173
|
+
socket?.terminate?.();
|
|
7174
|
+
} catch (err) {
|
|
7175
|
+
logger.error(`failed to terminate socket: ${String(err)}`);
|
|
7176
|
+
}
|
|
7177
|
+
};
|
|
6971
7178
|
const cleanup = () => {
|
|
7179
|
+
if (watchdogId) {
|
|
7180
|
+
clearInterval(watchdogId);
|
|
7181
|
+
watchdogId = null;
|
|
7182
|
+
}
|
|
6972
7183
|
if (currentClient === client) {
|
|
6973
7184
|
currentClient = null;
|
|
6974
7185
|
currentAccountId = null;
|
|
@@ -7066,8 +7277,42 @@ async function monitorDingtalkProvider(opts = {}) {
|
|
|
7066
7277
|
logger.error(`error parsing message: ${String(err)}`);
|
|
7067
7278
|
}
|
|
7068
7279
|
});
|
|
7280
|
+
watchdogId = setInterval(() => {
|
|
7281
|
+
if (stopped) return;
|
|
7282
|
+
attachSocketListeners();
|
|
7283
|
+
const now = Date.now();
|
|
7284
|
+
const state = getClientState(client);
|
|
7285
|
+
const connected = state.connected === true;
|
|
7286
|
+
const registered = state.registered === true;
|
|
7287
|
+
if (connected) {
|
|
7288
|
+
lastConnectedAt = now;
|
|
7289
|
+
}
|
|
7290
|
+
if (!connected && now - connectStartedAt > CONNECT_TIMEOUT_MS) {
|
|
7291
|
+
forceReconnect("connect timeout");
|
|
7292
|
+
connectStartedAt = now;
|
|
7293
|
+
lastConnectedAt = null;
|
|
7294
|
+
return;
|
|
7295
|
+
}
|
|
7296
|
+
if (connected && !registered && now - connectStartedAt > REGISTER_TIMEOUT_MS) {
|
|
7297
|
+
forceReconnect("register timeout");
|
|
7298
|
+
connectStartedAt = now;
|
|
7299
|
+
lastConnectedAt = null;
|
|
7300
|
+
return;
|
|
7301
|
+
}
|
|
7302
|
+
if (!connected) {
|
|
7303
|
+
const lastSeen = lastConnectedAt ?? connectStartedAt;
|
|
7304
|
+
if (now - lastSeen > DISCONNECT_GRACE_MS) {
|
|
7305
|
+
forceReconnect("client marked disconnected");
|
|
7306
|
+
connectStartedAt = now;
|
|
7307
|
+
lastConnectedAt = null;
|
|
7308
|
+
}
|
|
7309
|
+
}
|
|
7310
|
+
}, WATCHDOG_INTERVAL_MS);
|
|
7311
|
+
connectStartedAt = Date.now();
|
|
7312
|
+
lastConnectedAt = null;
|
|
7313
|
+
attachSocketListeners();
|
|
7069
7314
|
client.connect();
|
|
7070
|
-
logger.info("Stream client
|
|
7315
|
+
logger.info("Stream client connect invoked");
|
|
7071
7316
|
} catch (err) {
|
|
7072
7317
|
logger.error(`failed to start Stream connection: ${String(err)}`);
|
|
7073
7318
|
finalizeReject(err);
|