@openclaw-china/dingtalk 0.1.26 → 0.1.27

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.
@@ -0,0 +1,34 @@
1
+ {
2
+ "id": "dingtalk",
3
+ "name": "DingTalk",
4
+ "description": "钉钉消息渠道插件",
5
+ "version": "0.1.1",
6
+ "channels": ["dingtalk"],
7
+ "configSchema": {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "enabled": { "type": "boolean" },
12
+ "clientId": { "type": "string" },
13
+ "clientSecret": { "type": "string" },
14
+ "connectionMode": { "type": "string", "enum": ["stream", "webhook"] },
15
+ "dmPolicy": { "type": "string", "enum": ["open", "pairing", "allowlist"] },
16
+ "groupPolicy": { "type": "string", "enum": ["open", "allowlist", "disabled"] },
17
+ "requireMention": { "type": "boolean" },
18
+ "allowFrom": { "type": "array", "items": { "type": "string" } },
19
+ "groupAllowFrom": { "type": "array", "items": { "type": "string" } },
20
+ "historyLimit": { "type": "integer", "minimum": 0 },
21
+ "textChunkLimit": { "type": "integer", "minimum": 1 },
22
+ "enableAICard": { "type": "boolean" },
23
+ "gatewayToken": { "type": "string" },
24
+ "gatewayPassword": { "type": "string" },
25
+ "maxFileSizeMB": { "type": "number", "minimum": 1 }
26
+ }
27
+ },
28
+ "uiHints": {
29
+ "clientId": { "label": "Client ID (AppKey)" },
30
+ "clientSecret": { "label": "Client Secret (AppSecret)", "sensitive": true },
31
+ "gatewayToken": { "label": "Gateway Token", "sensitive": true },
32
+ "gatewayPassword": { "label": "Gateway Password", "sensitive": true }
33
+ }
34
+ }
package/dist/index.d.ts CHANGED
@@ -184,6 +184,9 @@ interface WizardPrompter {
184
184
  }) => Promise<T | symbol>;
185
185
  }
186
186
 
187
+ declare function normalizeDingtalkMessagingTarget(raw: string): string | undefined;
188
+ declare function looksLikeDingtalkTargetId(raw: string): boolean;
189
+
187
190
  /** 默认账户 ID */
188
191
  declare const DEFAULT_ACCOUNT_ID = "default";
189
192
  /**
@@ -419,14 +422,31 @@ declare const dingtalkPlugin: {
419
422
  cfg: OutboundConfig;
420
423
  to: string;
421
424
  text: string;
425
+ accountId?: string;
426
+ replyToId?: string | null;
427
+ threadId?: string | number | null;
428
+ deps?: unknown;
429
+ gifPlayback?: boolean;
422
430
  }) => Promise<SendResult>;
423
431
  sendMedia: (params: {
424
432
  cfg: OutboundConfig;
425
433
  to: string;
426
434
  text?: string;
427
435
  mediaUrl?: string;
436
+ accountId?: string;
437
+ replyToId?: string | null;
438
+ threadId?: string | number | null;
439
+ deps?: unknown;
440
+ gifPlayback?: boolean;
428
441
  }) => Promise<SendResult>;
429
442
  };
443
+ /**
444
+ * Messaging target normalization
445
+ */
446
+ messagingAdapter: {
447
+ normalizeTarget: typeof normalizeDingtalkMessagingTarget;
448
+ looksLikeId: typeof looksLikeDingtalkTargetId;
449
+ };
430
450
  /**
431
451
  * Gateway 连接管理适配器
432
452
  * Requirements: 3.1
@@ -533,9 +553,9 @@ interface PluginRuntime {
533
553
  };
534
554
  };
535
555
  session?: {
536
- resolveStorePath?: (store: unknown, params: {
556
+ resolveStorePath?: (store: unknown, opts: {
537
557
  agentId?: string;
538
- }) => string | undefined;
558
+ }) => string;
539
559
  recordInboundSession?: (params: {
540
560
  storePath: string;
541
561
  sessionKey: string;
@@ -547,7 +567,7 @@ interface PluginRuntime {
547
567
  accountId?: string;
548
568
  threadId?: string | number;
549
569
  };
550
- onRecordError?: (err: unknown) => void;
570
+ onRecordError: (err: unknown) => void;
551
571
  }) => Promise<void>;
552
572
  };
553
573
  reply?: {
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(false)
4091
+ replyFinalOnly: external_exports.boolean().optional().default(true)
4092
4092
  });
4093
4093
  function isConfigured(config) {
4094
4094
  return Boolean(config?.clientId && config?.clientSecret);
@@ -4110,8 +4110,7 @@ function createDingtalkClient(opts) {
4110
4110
  }
4111
4111
  const client = new DWClient({
4112
4112
  clientId: opts.clientId,
4113
- clientSecret: opts.clientSecret,
4114
- ...opts.ua ? { ua: opts.ua } : {}
4113
+ clientSecret: opts.clientSecret
4115
4114
  });
4116
4115
  cachedClient = client;
4117
4116
  cachedConfig = {
@@ -4125,10 +4124,7 @@ function createDingtalkClientFromConfig(cfg) {
4125
4124
  if (!creds) {
4126
4125
  throw new Error("DingTalk credentials not configured (clientId, clientSecret required)");
4127
4126
  }
4128
- return createDingtalkClient({
4129
- ...creds,
4130
- ua: "openclaw-dingtalk"
4131
- });
4127
+ return createDingtalkClient(creds);
4132
4128
  }
4133
4129
  var DINGTALK_OAUTH_URL = "https://api.dingtalk.com/v1.0/oauth2/accessToken";
4134
4130
  var TOKEN_REQUEST_TIMEOUT = 1e4;
@@ -4958,96 +4954,6 @@ function extractMediaFromText(text, options = {}) {
4958
4954
  all: [...images, ...files]
4959
4955
  };
4960
4956
  }
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
- }
5051
4957
  var FileSizeLimitError2 = class _FileSizeLimitError extends Error {
5052
4958
  /** Actual file size in bytes */
5053
4959
  actualSize;
@@ -5751,13 +5657,33 @@ function isDingtalkRuntimeInitialized() {
5751
5657
 
5752
5658
  // src/outbound.ts
5753
5659
  function parseTarget(to) {
5754
- if (to.startsWith("chat:")) {
5755
- return { targetId: to.slice(5), chatType: "group" };
5660
+ const raw = to.trim();
5661
+ if (!raw) {
5662
+ return { targetId: "", chatType: "direct" };
5663
+ }
5664
+ let normalized = raw;
5665
+ if (normalized.startsWith("dingtalk:")) {
5666
+ normalized = normalized.slice("dingtalk:".length).trim();
5667
+ } else if (normalized.startsWith("ding:")) {
5668
+ normalized = normalized.slice("ding:".length).trim();
5669
+ } else if (normalized.startsWith("channel:")) {
5670
+ normalized = normalized.slice("channel:".length).trim();
5671
+ if (normalized.startsWith("dingtalk:")) {
5672
+ normalized = normalized.slice("dingtalk:".length).trim();
5673
+ } else if (normalized.startsWith("ding:")) {
5674
+ normalized = normalized.slice("ding:".length).trim();
5675
+ }
5676
+ }
5677
+ if (normalized.startsWith("chat:") || normalized.startsWith("group:")) {
5678
+ return { targetId: normalized.slice(normalized.indexOf(":") + 1), chatType: "group" };
5756
5679
  }
5757
- if (to.startsWith("user:")) {
5758
- return { targetId: to.slice(5), chatType: "direct" };
5680
+ if (normalized.startsWith("user:") || normalized.startsWith("dm:")) {
5681
+ return { targetId: normalized.slice(normalized.indexOf(":") + 1), chatType: "direct" };
5759
5682
  }
5760
- return { targetId: to, chatType: "direct" };
5683
+ if (normalized.startsWith("group:")) {
5684
+ return { targetId: normalized.slice("group:".length), chatType: "group" };
5685
+ }
5686
+ return { targetId: normalized, chatType: "direct" };
5761
5687
  }
5762
5688
  var dingtalkOutbound = {
5763
5689
  /** 投递模式: direct (直接发送) */
@@ -6069,8 +5995,7 @@ async function finishAICard(card, content, log) {
6069
5995
 
6070
5996
  // src/bot.ts
6071
5997
  function buildGatewayUserContent(inboundCtx, logger) {
6072
- const base = inboundCtx.CommandBody ?? inboundCtx.Body ?? "";
6073
- const { base: baseText, prompt } = splitCronHiddenPrompt(base);
5998
+ const base = inboundCtx.Body ?? "";
6074
5999
  const rawPaths = [];
6075
6000
  if (typeof inboundCtx.MediaPath === "string") {
6076
6001
  rawPaths.push(inboundCtx.MediaPath);
@@ -6090,18 +6015,13 @@ function buildGatewayUserContent(inboundCtx, logger) {
6090
6015
  files.add(localPath);
6091
6016
  }
6092
6017
  if (files.size === 0) {
6093
- return prompt ? `${baseText}
6094
-
6095
- ${prompt}` : baseText;
6018
+ return base;
6096
6019
  }
6097
6020
  const list = Array.from(files).map((p) => `- ${p}`).join("\n");
6098
- const content = `${baseText}
6021
+ return `${base}
6099
6022
 
6100
6023
  [local files]
6101
6024
  ${list}`;
6102
- return prompt ? `${content}
6103
-
6104
- ${prompt}` : content;
6105
6025
  }
6106
6026
  function extractLocalMediaFromText(params) {
6107
6027
  const { text, logger } = params;
@@ -6146,22 +6066,6 @@ function extractMediaLinesFromText(params) {
6146
6066
  const mediaUrls = result.all.map((m) => m.isLocal ? m.localPath ?? m.source : m.source).filter((m) => typeof m === "string" && m.trim().length > 0);
6147
6067
  return { text: result.text, mediaUrls };
6148
6068
  }
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
- }
6165
6069
  function resolveGatewayAuthFromConfigFile(logger) {
6166
6070
  try {
6167
6071
  const fs5 = __require("fs");
@@ -6268,10 +6172,16 @@ function parseDingtalkMessage(raw) {
6268
6172
  let content = "";
6269
6173
  if (raw.msgtype === "text" && raw.text?.content) {
6270
6174
  content = raw.text.content.trim();
6271
- } else if (raw.msgtype === "audio") {
6272
- const recognition = resolveAudioRecognition(raw);
6273
- if (recognition) {
6274
- content = recognition;
6175
+ } else if (raw.msgtype === "audio" && raw.content) {
6176
+ const contentObj = typeof raw.content === "string" ? (() => {
6177
+ try {
6178
+ return JSON.parse(raw.content);
6179
+ } catch {
6180
+ return null;
6181
+ }
6182
+ })() : raw.content;
6183
+ if (contentObj && typeof contentObj === "object" && "recognition" in contentObj && typeof contentObj.recognition === "string") {
6184
+ content = contentObj.recognition.trim();
6275
6185
  }
6276
6186
  }
6277
6187
  const mentionedBot = resolveMentionedBot(raw);
@@ -6493,7 +6403,6 @@ async function handleDingtalkMessage(params) {
6493
6403
  });
6494
6404
  const ctx = parseDingtalkMessage(raw);
6495
6405
  const isGroup = ctx.chatType === "group";
6496
- const audioRecognition = resolveAudioRecognition(raw);
6497
6406
  logger.debug(`raw message: msgtype=${raw.msgtype}, hasText=${!!raw.text?.content}, hasContent=${!!raw.content}, textContent="${raw.text?.content ?? ""}"`);
6498
6407
  if (raw.msgtype === "richText") {
6499
6408
  try {
@@ -6583,34 +6492,30 @@ async function handleDingtalkMessage(params) {
6583
6492
  let richTextParseResult = null;
6584
6493
  const mediaTypes = ["picture", "video", "audio", "file"];
6585
6494
  if (mediaTypes.includes(raw.msgtype)) {
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;
6495
+ try {
6496
+ extractedFileInfo = extractFileFromMessage(raw);
6497
+ if (extractedFileInfo && channelCfg?.clientId && channelCfg?.clientSecret) {
6498
+ const accessToken = await getAccessToken(channelCfg.clientId, channelCfg.clientSecret);
6499
+ downloadedMedia = await downloadDingTalkFile({
6500
+ downloadCode: extractedFileInfo.downloadCode,
6501
+ robotCode: channelCfg.clientId,
6502
+ accessToken,
6503
+ fileName: extractedFileInfo.fileName,
6504
+ msgType: extractedFileInfo.msgType,
6505
+ log: logger,
6506
+ maxFileSizeMB: channelCfg.maxFileSizeMB
6507
+ });
6508
+ logger.debug(`downloaded media file: ${downloadedMedia.path} (${downloadedMedia.size} bytes)`);
6509
+ mediaBody = buildFileContextMessage(
6510
+ extractedFileInfo.msgType,
6511
+ extractedFileInfo.fileName
6512
+ );
6613
6513
  }
6514
+ } catch (err) {
6515
+ const errorMessage = err instanceof Error ? err.message : String(err);
6516
+ logger.warn(`media download failed, continuing with text: ${errorMessage}`);
6517
+ downloadedMedia = null;
6518
+ extractedFileInfo = null;
6614
6519
  }
6615
6520
  }
6616
6521
  if (raw.msgtype === "richText") {
@@ -6662,9 +6567,6 @@ async function handleDingtalkMessage(params) {
6662
6567
  }
6663
6568
  }
6664
6569
  const inboundCtx = buildInboundContext(ctx, route?.sessionKey, route?.accountId);
6665
- if (audioRecognition) {
6666
- inboundCtx.Transcript = audioRecognition;
6667
- }
6668
6570
  if (downloadedMedia) {
6669
6571
  inboundCtx.MediaPath = downloadedMedia.path;
6670
6572
  inboundCtx.MediaType = downloadedMedia.contentType;
@@ -6701,50 +6603,35 @@ async function handleDingtalkMessage(params) {
6701
6603
  }
6702
6604
  const finalizeInboundContext = replyApi?.finalizeInboundContext;
6703
6605
  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
- }
6606
+ const sessionApi = coreChannel?.session;
6607
+ const resolveStorePath = sessionApi?.resolveStorePath;
6608
+ const recordInboundSession = sessionApi?.recordInboundSession;
6609
+ if (resolveStorePath && recordInboundSession) {
6610
+ const agentId = route?.agentId;
6611
+ const storePath = resolveStorePath(cfg?.session?.store, {
6612
+ agentId
6747
6613
  });
6614
+ const sessionKey = route?.sessionKey;
6615
+ if (typeof sessionKey !== "string" || !sessionKey.trim()) {
6616
+ logger.debug("recordInboundSession skipped: missing sessionKey");
6617
+ } else {
6618
+ const routeSessionKey = route?.mainSessionKey ?? route?.sessionKey;
6619
+ const updateLastRoute = !isGroup && routeSessionKey && typeof finalCtx.To === "string" ? {
6620
+ sessionKey: routeSessionKey,
6621
+ channel: "dingtalk",
6622
+ to: finalCtx.To,
6623
+ accountId: route?.accountId
6624
+ } : void 0;
6625
+ await recordInboundSession({
6626
+ storePath,
6627
+ sessionKey,
6628
+ ctx: finalCtx,
6629
+ updateLastRoute,
6630
+ onRecordError: (err) => {
6631
+ logger.debug(`recordInboundSession failed: ${String(err)}`);
6632
+ }
6633
+ });
6634
+ }
6748
6635
  }
6749
6636
  const dingtalkCfgResolved = channelCfg;
6750
6637
  if (!dingtalkCfgResolved) {
@@ -7098,14 +6985,6 @@ var currentAccountId = null;
7098
6985
  var currentPromise = null;
7099
6986
  var processedMessages = /* @__PURE__ */ new Map();
7100
6987
  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
- }
7109
6988
  async function monitorDingtalkProvider(opts = {}) {
7110
6989
  const { config, runtime: runtime2, abortSignal, accountId = "default" } = opts;
7111
6990
  const logger = createLogger("dingtalk", {
@@ -7139,47 +7018,7 @@ async function monitorDingtalkProvider(opts = {}) {
7139
7018
  logger.info(`starting Stream connection for account ${accountId}...`);
7140
7019
  currentPromise = new Promise((resolve2, reject) => {
7141
7020
  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
- };
7178
7021
  const cleanup = () => {
7179
- if (watchdogId) {
7180
- clearInterval(watchdogId);
7181
- watchdogId = null;
7182
- }
7183
7022
  if (currentClient === client) {
7184
7023
  currentClient = null;
7185
7024
  currentAccountId = null;
@@ -7277,42 +7116,8 @@ async function monitorDingtalkProvider(opts = {}) {
7277
7116
  logger.error(`error parsing message: ${String(err)}`);
7278
7117
  }
7279
7118
  });
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();
7314
7119
  client.connect();
7315
- logger.info("Stream client connect invoked");
7120
+ logger.info("Stream client connected");
7316
7121
  } catch (err) {
7317
7122
  logger.error(`failed to start Stream connection: ${String(err)}`);
7318
7123
  finalizeReject(err);
@@ -7574,6 +7379,51 @@ var dingtalkOnboardingAdapter = {
7574
7379
  })
7575
7380
  };
7576
7381
 
7382
+ // src/normalize.ts
7383
+ function normalizeDingtalkMessagingTarget(raw) {
7384
+ const trimmed = raw.trim();
7385
+ if (!trimmed) {
7386
+ return void 0;
7387
+ }
7388
+ let normalized = trimmed;
7389
+ if (normalized.startsWith("channel:")) {
7390
+ normalized = normalized.slice("channel:".length).trim();
7391
+ }
7392
+ if (normalized.startsWith("dingtalk:")) {
7393
+ normalized = normalized.slice("dingtalk:".length).trim();
7394
+ } else if (normalized.startsWith("ding:")) {
7395
+ normalized = normalized.slice("ding:".length).trim();
7396
+ }
7397
+ if (!normalized) {
7398
+ return void 0;
7399
+ }
7400
+ const lower = normalized.toLowerCase();
7401
+ if (lower.startsWith("chat:") || lower.startsWith("group:")) {
7402
+ const targetId = normalized.slice(normalized.indexOf(":") + 1).trim();
7403
+ if (!targetId) return void 0;
7404
+ return `dingtalk:group:${targetId}`;
7405
+ }
7406
+ if (lower.startsWith("user:") || lower.startsWith("dm:")) {
7407
+ const targetId = normalized.slice(normalized.indexOf(":") + 1).trim();
7408
+ if (!targetId) return void 0;
7409
+ return `dingtalk:user:${targetId}`;
7410
+ }
7411
+ return `dingtalk:user:${normalized}`;
7412
+ }
7413
+ function looksLikeDingtalkTargetId(raw) {
7414
+ const trimmed = raw.trim();
7415
+ if (!trimmed) {
7416
+ return false;
7417
+ }
7418
+ if (/^(dingtalk|ding|channel):/i.test(trimmed)) {
7419
+ return true;
7420
+ }
7421
+ if (/^(user|dm|chat|group):/i.test(trimmed)) {
7422
+ return true;
7423
+ }
7424
+ return /^[A-Za-z0-9:_-]{6,}$/.test(trimmed);
7425
+ }
7426
+
7577
7427
  // src/channel.ts
7578
7428
  var DEFAULT_ACCOUNT_ID = "default";
7579
7429
  var meta = {
@@ -7764,6 +7614,13 @@ var dingtalkPlugin = {
7764
7614
  * Requirements: 7.1, 7.6
7765
7615
  */
7766
7616
  outbound: dingtalkOutbound,
7617
+ /**
7618
+ * Messaging target normalization
7619
+ */
7620
+ messagingAdapter: {
7621
+ normalizeTarget: normalizeDingtalkMessagingTarget,
7622
+ looksLikeId: looksLikeDingtalkTargetId
7623
+ },
7767
7624
  /**
7768
7625
  * Gateway 连接管理适配器
7769
7626
  * Requirements: 3.1