@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 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(true)
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(creds);
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 base;
6093
+ return prompt ? `${baseText}
6094
+
6095
+ ${prompt}` : baseText;
5999
6096
  }
6000
6097
  const list = Array.from(files).map((p) => `- ${p}`).join("\n");
6001
- return `${base}
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" && raw.content) {
6156
- const contentObj = typeof raw.content === "string" ? (() => {
6157
- try {
6158
- return JSON.parse(raw.content);
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
- try {
6476
- extractedFileInfo = extractFileFromMessage(raw);
6477
- if (extractedFileInfo && channelCfg?.clientId && channelCfg?.clientSecret) {
6478
- const accessToken = await getAccessToken(channelCfg.clientId, channelCfg.clientSecret);
6479
- downloadedMedia = await downloadDingTalkFile({
6480
- downloadCode: extractedFileInfo.downloadCode,
6481
- robotCode: channelCfg.clientId,
6482
- accessToken,
6483
- fileName: extractedFileInfo.fileName,
6484
- msgType: extractedFileInfo.msgType,
6485
- log: logger,
6486
- maxFileSizeMB: channelCfg.maxFileSizeMB
6487
- });
6488
- logger.debug(`downloaded media file: ${downloadedMedia.path} (${downloadedMedia.size} bytes)`);
6489
- mediaBody = buildFileContextMessage(
6490
- extractedFileInfo.msgType,
6491
- extractedFileInfo.fileName
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 connected");
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);