@soimy/dingtalk 3.6.2-beta.1 → 3.6.2

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.js CHANGED
@@ -2,9 +2,6 @@
2
2
  import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
3
3
  import { readStringParam as readStringParam2 } from "openclaw/plugin-sdk/param-readers";
4
4
 
5
- // src/channel.ts
6
- import { buildChannelConfigSchema } from "openclaw/plugin-sdk/core";
7
-
8
5
  // src/secret-input.ts
9
6
  import { readFile } from "node:fs/promises";
10
7
  import { z } from "zod";
@@ -410,1240 +407,1273 @@ function resolveDingTalkAccount(cfg, accountId) {
410
407
  };
411
408
  }
412
409
 
413
- // src/config-schema.ts
414
- import { z as z2 } from "zod";
410
+ // src/http-client.ts
411
+ import axios from "axios";
412
+ var DEFAULT_HTTP_TIMEOUT_MS = 1e4;
413
+ var httpClient = typeof axios?.create === "function" ? axios.create({ timeout: DEFAULT_HTTP_TIMEOUT_MS }) : axios;
414
+ httpClient.isAxiosError = axios.isAxiosError;
415
+ var http_client_default = httpClient;
415
416
 
416
- // src/persistence-store.ts
417
+ // src/utils.ts
418
+ import * as dns from "node:dns";
417
419
  import * as fs from "node:fs";
420
+ import * as net from "node:net";
421
+ import * as os2 from "node:os";
418
422
  import * as path2 from "node:path";
419
- var NAMESPACE_ROOT_DIR = "dingtalk-state";
420
- function toErrorMessage(err) {
421
- if (err instanceof Error) {
422
- return err.message;
423
- }
424
- try {
425
- return JSON.stringify(err);
426
- } catch {
427
- return String(err);
428
- }
429
- }
430
- function sanitizeSegment(value) {
431
- return value.replace(/[^a-zA-Z0-9._-]/g, "_");
423
+ var pluginDebugWriters = /* @__PURE__ */ new Map();
424
+ var closedPluginDebugScopes = /* @__PURE__ */ new Set();
425
+ function padNumber(value, width = 2) {
426
+ return String(value).padStart(width, "0");
432
427
  }
433
- function encodeScopeValue(value) {
434
- return Buffer.from(value, "utf8").toString("base64url");
428
+ function formatTimezoneOffset(date) {
429
+ const offsetMinutes = -date.getTimezoneOffset();
430
+ const sign = offsetMinutes >= 0 ? "+" : "-";
431
+ const absoluteMinutes = Math.abs(offsetMinutes);
432
+ const hours = Math.floor(absoluteMinutes / 60);
433
+ const minutes = absoluteMinutes % 60;
434
+ return `${sign}${padNumber(hours)}:${padNumber(minutes)}`;
435
435
  }
436
- function buildScopeSuffix(scope) {
437
- if (!scope) {
438
- return "";
439
- }
440
- const ordered = [
441
- ["accountId", scope.accountId],
442
- ["agentId", scope.agentId],
443
- ["conversationId", scope.conversationId],
444
- ["groupId", scope.groupId],
445
- ["targetId", scope.targetId]
446
- ];
447
- const segments = ordered.filter(([, value]) => Boolean(value && value.trim())).map(([key, value]) => `${key.replace(/Id$/, "")}-${encodeScopeValue((value || "").trim())}`);
448
- if (segments.length === 0) {
449
- return "";
450
- }
451
- return `.${segments.join(".")}`;
436
+ function formatPluginDebugTimestamp(date) {
437
+ return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())} ${padNumber(date.getHours())}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}.${padNumber(date.getMilliseconds(), 3)}${formatTimezoneOffset(date)}`;
452
438
  }
453
- function resolveNamespacePath(namespace, options) {
454
- const format = options.format || "json";
455
- const baseDir = path2.join(path2.dirname(options.storePath), NAMESPACE_ROOT_DIR);
456
- const safeNamespace = sanitizeSegment(namespace.trim());
457
- const suffix = buildScopeSuffix(options.scope);
458
- return path2.join(baseDir, `${safeNamespace}${suffix}.${format}`);
439
+ function formatPluginDebugDate(date) {
440
+ return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}`;
459
441
  }
460
- function readNamespaceJson(namespace, options) {
461
- const filePath = resolveNamespacePath(namespace, options);
462
- try {
463
- if (!fs.existsSync(filePath)) {
464
- return options.fallback;
465
- }
466
- const raw = fs.readFileSync(filePath, "utf-8");
467
- if (!raw.trim()) {
468
- return options.fallback;
469
- }
470
- return JSON.parse(raw);
471
- } catch (err) {
472
- options.log?.warn?.(
473
- `[DingTalk][Persistence] Failed to read namespace=${namespace} path=${filePath}: ${toErrorMessage(err)}`
474
- );
475
- return options.fallback;
476
- }
442
+ function resolvePluginDebugLogFilePath(params) {
443
+ return path2.join(
444
+ path2.dirname(params.storePath),
445
+ "logs",
446
+ "dingtalk",
447
+ params.accountId,
448
+ `debug-${formatPluginDebugDate(params.date)}.log`
449
+ );
477
450
  }
478
- function writeNamespaceJsonAtomic(namespace, options) {
479
- const filePath = resolveNamespacePath(namespace, options);
480
- const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
481
- try {
482
- fs.mkdirSync(path2.dirname(filePath), { recursive: true });
483
- fs.writeFileSync(tempPath, JSON.stringify(options.data, null, 2));
484
- try {
485
- fs.renameSync(tempPath, filePath);
486
- } catch (err) {
487
- if (fs.existsSync(filePath)) {
488
- fs.rmSync(filePath, { force: true });
489
- fs.renameSync(tempPath, filePath);
490
- } else {
491
- throw err;
492
- }
493
- }
494
- } catch (err) {
495
- options.log?.warn?.(
496
- `[DingTalk][Persistence] Failed to write namespace=${namespace} path=${filePath}: ${toErrorMessage(err)}`
497
- );
498
- if (fs.existsSync(tempPath)) {
499
- fs.rmSync(tempPath, { force: true });
500
- }
501
- }
451
+ function formatPluginDebugLine(params) {
452
+ return `[${formatPluginDebugTimestamp(params.date)}] [debug] [dingtalk] [account:${params.accountId}] ${params.message}`;
502
453
  }
503
-
504
- // src/message-context-store.ts
505
- var MESSAGE_CONTEXT_NAMESPACE = "messages.context";
506
- var MESSAGE_CONTEXT_VERSION = 1;
507
- var DEFAULT_MESSAGE_CONTEXT_TTL_DAYS = 7;
508
- var DEFAULT_CARD_CONTENT_TTL_MS = 24 * 60 * 60 * 1e3;
509
- var DEFAULT_MEDIA_CONTEXT_TTL_MS = 24 * 60 * 60 * 1e3;
510
- var DEFAULT_CREATED_AT_MATCH_WINDOW_MS = 2e3;
511
- var MAX_RECORDS_PER_SCOPE = 1e3;
512
- var DEFAULT_OUTBOUND_SENDER = {
513
- senderId: "bot",
514
- senderName: "OpenClaw"
515
- };
516
- function inferConversationChatType(conversationId) {
517
- return conversationId.startsWith("cid") ? "group" : "direct";
454
+ function buildPluginDebugWriterKey(params) {
455
+ return JSON.stringify([params.storePath, params.accountId, formatPluginDebugDate(params.date)]);
518
456
  }
519
- var stateCache = /* @__PURE__ */ new Map();
520
- function getScopeKey(params) {
521
- return JSON.stringify([
522
- params.storePath || "__memory__",
523
- params.accountId,
524
- params.conversationId || null
525
- ]);
457
+ function buildPluginDebugScopeKey(params) {
458
+ return JSON.stringify([params.storePath, params.accountId]);
526
459
  }
527
- function fallbackState() {
528
- return {
529
- version: MESSAGE_CONTEXT_VERSION,
530
- updatedAt: Date.now(),
531
- records: {},
532
- byAlias: {},
533
- recentByCreatedAt: []
460
+ function resolvePluginDebugWriter(params) {
461
+ const key = buildPluginDebugWriterKey(params);
462
+ const existing = pluginDebugWriters.get(key);
463
+ if (existing) {
464
+ return existing;
465
+ }
466
+ const created = {
467
+ filePath: resolvePluginDebugLogFilePath(params),
468
+ warned: false,
469
+ directoryReady: false
534
470
  };
471
+ pluginDebugWriters.set(key, created);
472
+ return created;
535
473
  }
536
- function asRecord(value) {
537
- if (!value || typeof value !== "object" || Array.isArray(value)) {
538
- return null;
474
+ function resolvePluginDebugLog(params) {
475
+ const baseLog = params.baseLog;
476
+ const fsImpl = params.fsImpl ?? fs;
477
+ const scopeKey = params.storePath ? buildPluginDebugScopeKey({ storePath: params.storePath, accountId: params.accountId }) : void 0;
478
+ if (scopeKey) {
479
+ closedPluginDebugScopes.delete(scopeKey);
539
480
  }
540
- return value;
481
+ return {
482
+ debug: (message) => {
483
+ if (!params.debug) {
484
+ baseLog?.debug?.(message);
485
+ return;
486
+ }
487
+ const date = params.now ? params.now() : /* @__PURE__ */ new Date();
488
+ const line = formatPluginDebugLine({
489
+ accountId: params.accountId,
490
+ date,
491
+ message
492
+ });
493
+ try {
494
+ process.stdout.write(`${line}
495
+ `);
496
+ } catch {
497
+ }
498
+ if (params.storePath && scopeKey && !closedPluginDebugScopes.has(scopeKey)) {
499
+ const writer = resolvePluginDebugWriter({
500
+ storePath: params.storePath,
501
+ accountId: params.accountId,
502
+ date
503
+ });
504
+ try {
505
+ if (!writer.directoryReady) {
506
+ fsImpl.mkdirSync(path2.dirname(writer.filePath), { recursive: true });
507
+ writer.directoryReady = true;
508
+ }
509
+ fsImpl.appendFileSync(writer.filePath, `${line}
510
+ `, "utf8");
511
+ } catch (err) {
512
+ if (!writer.warned) {
513
+ writer.warned = true;
514
+ baseLog?.warn?.(
515
+ `[DingTalk] Plugin debug log file unavailable: accountId=${params.accountId} path=${writer.filePath} error=${getErrorMessage(err)}`
516
+ );
517
+ }
518
+ }
519
+ }
520
+ try {
521
+ baseLog?.debug?.(message);
522
+ } catch {
523
+ }
524
+ },
525
+ info: (message) => baseLog?.info?.(message),
526
+ warn: (message) => baseLog?.warn?.(message),
527
+ error: (message) => baseLog?.error?.(message)
528
+ };
541
529
  }
542
- function normalizeMedia(value) {
543
- const candidate = asRecord(value);
544
- if (!candidate) {
545
- return void 0;
530
+ function closePluginDebugLog(params) {
531
+ if (!params.storePath) {
532
+ return;
546
533
  }
547
- const downloadCode = typeof candidate.downloadCode === "string" && candidate.downloadCode.trim() ? candidate.downloadCode.trim() : void 0;
548
- const downloadCodes = Array.isArray(candidate.downloadCodes) && candidate.downloadCodes.length > 0 ? candidate.downloadCodes.filter((c) => typeof c === "string" && c.trim()) : void 0;
549
- const spaceId = typeof candidate.spaceId === "string" && candidate.spaceId.trim() ? candidate.spaceId.trim() : void 0;
550
- const fileId = typeof candidate.fileId === "string" && candidate.fileId.trim() ? candidate.fileId.trim() : void 0;
551
- if (!downloadCode && (!downloadCodes || downloadCodes.length === 0) && !spaceId && !fileId) {
552
- return void 0;
534
+ const scopeKey = buildPluginDebugScopeKey({
535
+ storePath: params.storePath,
536
+ accountId: params.accountId
537
+ });
538
+ closedPluginDebugScopes.add(scopeKey);
539
+ for (const key of pluginDebugWriters.keys()) {
540
+ const [writerStorePath, writerAccountId] = JSON.parse(key);
541
+ if (writerStorePath === params.storePath && writerAccountId === params.accountId) {
542
+ pluginDebugWriters.delete(key);
543
+ }
553
544
  }
554
- return { downloadCode, downloadCodes, spaceId, fileId };
555
545
  }
556
- function normalizeQuotedRef(value) {
557
- const candidate = asRecord(value);
558
- if (!candidate) {
559
- return void 0;
560
- }
561
- const targetDirection = candidate.targetDirection === "outbound" ? "outbound" : candidate.targetDirection === "inbound" ? "inbound" : void 0;
562
- const key = candidate.key === "msgId" || candidate.key === "messageId" || candidate.key === "processQueryKey" || candidate.key === "outTrackId" || candidate.key === "cardInstanceId" ? candidate.key : void 0;
563
- const valueString = typeof candidate.value === "string" && candidate.value.trim() ? candidate.value.trim() : void 0;
564
- const fallbackCreatedAt = typeof candidate.fallbackCreatedAt === "number" && Number.isFinite(candidate.fallbackCreatedAt) ? candidate.fallbackCreatedAt : void 0;
565
- if (!targetDirection) {
566
- return void 0;
567
- }
568
- if (!key && !fallbackCreatedAt) {
569
- return void 0;
546
+ function maskSensitiveData(data) {
547
+ if (data === null || data === void 0) {
548
+ return data;
570
549
  }
571
- if (key && !valueString) {
572
- return void 0;
550
+ if (typeof data !== "object") {
551
+ return data;
573
552
  }
574
- return {
575
- targetDirection,
576
- key,
577
- value: valueString,
578
- fallbackCreatedAt
579
- };
580
- }
581
- function normalizeAttachmentTextSource(value) {
582
- return value === "text" || value === "html" || value === "pdf" || value === "docx" ? value : void 0;
583
- }
584
- function normalizeDelivery(value) {
585
- const candidate = asRecord(value);
586
- if (!candidate) {
587
- return void 0;
588
- }
589
- const messageId = typeof candidate.messageId === "string" && candidate.messageId.trim() ? candidate.messageId.trim() : void 0;
590
- const processQueryKey = typeof candidate.processQueryKey === "string" && candidate.processQueryKey.trim() ? candidate.processQueryKey.trim() : void 0;
591
- const outTrackId = typeof candidate.outTrackId === "string" && candidate.outTrackId.trim() ? candidate.outTrackId.trim() : void 0;
592
- const cardInstanceId = typeof candidate.cardInstanceId === "string" && candidate.cardInstanceId.trim() ? candidate.cardInstanceId.trim() : void 0;
593
- const kind = typeof candidate.kind === "string" && candidate.kind.trim() ? candidate.kind.trim() : void 0;
594
- if (!messageId && !processQueryKey && !outTrackId && !cardInstanceId && !kind) {
595
- return void 0;
553
+ const masked = JSON.parse(JSON.stringify(data));
554
+ const sensitiveFields = /* @__PURE__ */ new Set(["token", "accessToken"]);
555
+ function maskObj(obj) {
556
+ for (const key in obj) {
557
+ if (sensitiveFields.has(key)) {
558
+ const val = obj[key];
559
+ if (typeof val === "string" && val.length > 6) {
560
+ obj[key] = val.slice(0, 3) + "*".repeat(val.length - 6) + val.slice(-3);
561
+ } else if (typeof val === "string") {
562
+ obj[key] = "*".repeat(val.length);
563
+ }
564
+ } else if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) {
565
+ maskObj(obj[key]);
566
+ }
567
+ }
596
568
  }
597
- return { messageId, processQueryKey, outTrackId, cardInstanceId, kind };
569
+ maskObj(masked);
570
+ return masked;
598
571
  }
599
- function normalizeMentions(value) {
600
- if (!Array.isArray(value)) {
601
- return void 0;
572
+ function stringifyUnknown(value) {
573
+ if (typeof value === "string") {
574
+ return value;
602
575
  }
603
- const normalized = [...new Set(value.map((item) => String(item || "").trim()).filter(Boolean))];
604
- return normalized.length > 0 ? normalized : void 0;
605
- }
606
- function normalizeMessageRecord(value) {
607
- const candidate = asRecord(value);
608
- if (!candidate) {
609
- return null;
576
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
577
+ return String(value);
610
578
  }
611
- const msgId = typeof candidate.msgId === "string" && candidate.msgId.trim() ? candidate.msgId.trim() : "";
612
- const direction = candidate.direction === "outbound" ? "outbound" : candidate.direction === "inbound" ? "inbound" : null;
613
- const accountId = typeof candidate.accountId === "string" && candidate.accountId.trim() ? candidate.accountId.trim() : "";
614
- const conversationId = typeof candidate.conversationId === "string" && candidate.conversationId.trim() ? candidate.conversationId.trim() : null;
615
- const createdAt = typeof candidate.createdAt === "number" && Number.isFinite(candidate.createdAt) ? candidate.createdAt : NaN;
616
- const updatedAt = typeof candidate.updatedAt === "number" && Number.isFinite(candidate.updatedAt) ? candidate.updatedAt : Date.now();
617
- if (!msgId || !direction || !accountId || !Number.isFinite(createdAt)) {
618
- return null;
579
+ try {
580
+ const serialized = JSON.stringify(value);
581
+ return serialized ?? String(value);
582
+ } catch {
583
+ return "[unserializable]";
619
584
  }
620
- const expiresAt = typeof candidate.expiresAt === "number" && Number.isFinite(candidate.expiresAt) ? candidate.expiresAt : void 0;
621
- return {
622
- msgId,
623
- direction,
624
- topic: candidate.topic === null ? null : typeof candidate.topic === "string" ? candidate.topic : null,
625
- accountId,
626
- conversationId,
627
- createdAt,
628
- updatedAt,
629
- expiresAt,
630
- messageType: typeof candidate.messageType === "string" ? candidate.messageType : void 0,
631
- text: typeof candidate.text === "string" ? candidate.text : void 0,
632
- attachmentText: typeof candidate.attachmentText === "string" ? candidate.attachmentText : void 0,
633
- attachmentTextSource: normalizeAttachmentTextSource(candidate.attachmentTextSource),
634
- attachmentTextTruncated: candidate.attachmentTextTruncated === true ? true : void 0,
635
- attachmentFileName: typeof candidate.attachmentFileName === "string" ? candidate.attachmentFileName : void 0,
636
- quotedRef: normalizeQuotedRef(candidate.quotedRef),
637
- senderId: typeof candidate.senderId === "string" && candidate.senderId.trim() ? candidate.senderId.trim() : void 0,
638
- senderName: typeof candidate.senderName === "string" && candidate.senderName.trim() ? candidate.senderName.trim() : void 0,
639
- mentions: normalizeMentions(candidate.mentions),
640
- chatType: candidate.chatType === "direct" || candidate.chatType === "group" ? candidate.chatType : void 0,
641
- quotedMessageId: typeof candidate.quotedMessageId === "string" && candidate.quotedMessageId.trim() ? candidate.quotedMessageId.trim() : void 0,
642
- media: normalizeMedia(candidate.media),
643
- delivery: normalizeDelivery(candidate.delivery)
644
- };
645
585
  }
646
- function buildAliasKey(kind, value) {
647
- return `${kind}:${value.trim()}`;
648
- }
649
- function buildAliasEntries(record) {
650
- const aliases = [];
651
- if (record.direction === "inbound" && record.msgId.trim()) {
652
- aliases.push([buildAliasKey("inboundMsgId", record.msgId), record.msgId]);
653
- }
654
- const delivery = record.delivery;
655
- if (!delivery) {
656
- return aliases;
586
+ function parseBooleanLike(value) {
587
+ if (typeof value === "boolean") {
588
+ return value;
657
589
  }
658
- if (delivery.messageId) {
659
- aliases.push([buildAliasKey("messageId", delivery.messageId), record.msgId]);
590
+ if (typeof value === "number") {
591
+ if (value === 1) {
592
+ return true;
593
+ }
594
+ if (value === 0) {
595
+ return false;
596
+ }
597
+ return void 0;
660
598
  }
661
- if (delivery.processQueryKey) {
662
- aliases.push([buildAliasKey("processQueryKey", delivery.processQueryKey), record.msgId]);
599
+ if (typeof value === "string") {
600
+ const normalized = value.trim().toLowerCase();
601
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) {
602
+ return true;
603
+ }
604
+ if (["0", "false", "no", "n", "off"].includes(normalized)) {
605
+ return false;
606
+ }
663
607
  }
664
- if (delivery.outTrackId) {
665
- aliases.push([buildAliasKey("outTrackId", delivery.outTrackId), record.msgId]);
608
+ return void 0;
609
+ }
610
+ function getErrorMessage(err) {
611
+ if (err instanceof Error && err.message) {
612
+ return err.message;
666
613
  }
667
- if (delivery.cardInstanceId) {
668
- aliases.push([buildAliasKey("cardInstanceId", delivery.cardInstanceId), record.msgId]);
614
+ if (err && typeof err === "object") {
615
+ const record = err;
616
+ if (typeof record.message === "string" && record.message.trim()) {
617
+ return record.message;
618
+ }
669
619
  }
670
- return aliases;
620
+ return stringifyUnknown(err);
671
621
  }
672
- function isRecordExpired(record, nowMs) {
673
- return typeof record.expiresAt === "number" && Number.isFinite(record.expiresAt) && nowMs >= record.expiresAt;
622
+ function getErrorResponseData(err) {
623
+ if (!err || typeof err !== "object") {
624
+ return void 0;
625
+ }
626
+ return err.response?.data;
674
627
  }
675
- function normalizeState(state, nowMs) {
676
- const normalizedRecords = {};
677
- const sortedRecords = Object.values(state.records).map((record) => normalizeMessageRecord(record)).filter((record) => record !== null).filter((record) => !isRecordExpired(record, nowMs)).toSorted((left, right) => left.createdAt - right.createdAt);
678
- const keptRecords = sortedRecords.slice(-MAX_RECORDS_PER_SCOPE);
679
- for (const record of keptRecords) {
680
- normalizedRecords[record.msgId] = record;
628
+ function formatDingTalkErrorPayload(payload) {
629
+ if (payload === null || payload === void 0) {
630
+ return "payload=unknown";
681
631
  }
682
- const byAlias = {};
683
- for (const record of keptRecords) {
684
- for (const [key, value] of buildAliasEntries(record)) {
685
- byAlias[key] = value;
632
+ let code;
633
+ let message;
634
+ if (typeof payload === "object" && !Array.isArray(payload)) {
635
+ const obj = payload;
636
+ if (typeof obj.code === "string" || typeof obj.code === "number") {
637
+ code = String(obj.code);
686
638
  }
687
- }
688
- return {
689
- removed: Object.keys(state.records).length - Object.keys(normalizedRecords).length,
690
- state: {
691
- version: MESSAGE_CONTEXT_VERSION,
692
- updatedAt: state.updatedAt,
693
- records: normalizedRecords,
694
- byAlias,
695
- recentByCreatedAt: keptRecords.map((record) => record.msgId)
639
+ if (typeof obj.message === "string") {
640
+ message = obj.message;
696
641
  }
697
- };
698
- }
699
- function hydrateState(params, nowMs) {
700
- if (!params.storePath) {
701
- return fallbackState();
702
642
  }
703
- const persisted = readNamespaceJson(
704
- MESSAGE_CONTEXT_NAMESPACE,
705
- {
706
- storePath: params.storePath,
707
- scope: { accountId: params.accountId, conversationId: params.conversationId || void 0 },
708
- format: "json",
709
- fallback: { version: MESSAGE_CONTEXT_VERSION, updatedAt: Date.now(), records: {} }
710
- }
711
- );
712
- const parsedRecords = asRecord(persisted.records) || {};
713
- const hydrated = fallbackState();
714
- hydrated.updatedAt = typeof persisted.updatedAt === "number" ? persisted.updatedAt : Date.now();
715
- for (const [key, value] of Object.entries(parsedRecords)) {
716
- const normalized = normalizeMessageRecord(value);
717
- if (normalized) {
718
- hydrated.records[key] = normalized;
643
+ let serialized;
644
+ try {
645
+ serialized = JSON.stringify(maskSensitiveData(payload));
646
+ } catch {
647
+ if (typeof payload === "string") {
648
+ serialized = payload;
649
+ } else if (typeof payload === "number" || typeof payload === "boolean" || typeof payload === "bigint") {
650
+ serialized = `${payload}`;
651
+ } else {
652
+ serialized = "[unserializable-payload]";
719
653
  }
720
654
  }
721
- return normalizeState(hydrated, nowMs).state;
722
- }
723
- function loadState(params, nowMs = Date.now()) {
724
- const scopeKey = getScopeKey(params);
725
- const cached = stateCache.get(scopeKey);
726
- if (cached) {
727
- return cached;
655
+ const parts = [];
656
+ if (code) {
657
+ parts.push(`code=${code}`);
728
658
  }
729
- const hydrated = params.storePath ? hydrateState(params, nowMs) : fallbackState();
730
- stateCache.set(scopeKey, hydrated);
731
- return hydrated;
732
- }
733
- function writeState(params, state) {
734
- stateCache.set(getScopeKey(params), state);
735
- if (!params.storePath) {
736
- return;
659
+ if (message) {
660
+ parts.push(`message=${message}`);
737
661
  }
738
- writeNamespaceJsonAtomic(MESSAGE_CONTEXT_NAMESPACE, {
739
- storePath: params.storePath,
740
- scope: { accountId: params.accountId, conversationId: params.conversationId || void 0 },
741
- format: "json",
742
- data: {
743
- version: MESSAGE_CONTEXT_VERSION,
744
- updatedAt: state.updatedAt,
745
- records: state.records
746
- }
747
- });
662
+ parts.push(`payload=${serialized}`);
663
+ return parts.join(" ");
748
664
  }
749
- function cloneStateForMutation(state) {
750
- return {
751
- version: state.version,
752
- updatedAt: state.updatedAt,
753
- records: { ...state.records },
754
- byAlias: state.byAlias,
755
- recentByCreatedAt: state.recentByCreatedAt
756
- };
665
+ function formatDingTalkErrorPayloadLog(scope, payload, prefix = "[DingTalk]") {
666
+ return `${prefix}[ErrorPayload][${scope}] ${formatDingTalkErrorPayload(payload)}`;
757
667
  }
758
- function mergeText(existing, next) {
759
- if (typeof next !== "string") {
760
- return existing;
761
- }
762
- return next;
668
+ function getProxyBypassOption(config) {
669
+ return config?.bypassProxyForSend ? { proxy: false } : {};
763
670
  }
764
- function mergeAttachmentText(existing, next) {
765
- if (typeof next !== "string") {
766
- return existing;
767
- }
768
- return next;
671
+ function createResolve4FallbackLookup(log, accountId) {
672
+ return createResolve4FallbackLookupWithDeps(log, accountId, dns, net);
769
673
  }
770
- function mergeQuotedRef(existing, next) {
771
- if (!existing) {
772
- return next;
773
- }
774
- if (!next) {
775
- return existing;
776
- }
777
- return {
778
- targetDirection: next.targetDirection,
779
- key: next.key || existing.key,
780
- value: next.value || existing.value,
781
- fallbackCreatedAt: next.fallbackCreatedAt ?? existing.fallbackCreatedAt
674
+ function createResolve4FallbackLookupWithDeps(log, accountId, dnsImpl, netImpl) {
675
+ let fallbackLogged = false;
676
+ return (hostname, options, callback) => {
677
+ const ipFamily = netImpl.isIP(hostname);
678
+ if (ipFamily !== 0) {
679
+ if (options.all) {
680
+ callback(null, [{ address: hostname, family: ipFamily }], ipFamily);
681
+ return;
682
+ }
683
+ callback(null, hostname, ipFamily);
684
+ return;
685
+ }
686
+ dnsImpl.lookup(hostname, options, (lookupErr, address, family) => {
687
+ if (!lookupErr) {
688
+ callback(null, address, family);
689
+ return;
690
+ }
691
+ if (lookupErr.code !== "ENOTFOUND") {
692
+ callback(lookupErr, address, family);
693
+ return;
694
+ }
695
+ dnsImpl.resolve4(hostname, (resolveErr, addresses) => {
696
+ if (resolveErr || !addresses || addresses.length === 0) {
697
+ callback(lookupErr, address, family);
698
+ return;
699
+ }
700
+ if (!fallbackLogged) {
701
+ fallbackLogged = true;
702
+ log?.warn?.(
703
+ `[${accountId ?? "default"}] System DNS lookup failed for ${hostname} (ENOTFOUND); using resolve4 fallback ${addresses[0]}`
704
+ );
705
+ }
706
+ if (options.all) {
707
+ callback(
708
+ null,
709
+ addresses.map((item) => ({ address: item, family: 4 })),
710
+ 4
711
+ );
712
+ return;
713
+ }
714
+ callback(null, addresses[0], 4);
715
+ });
716
+ });
782
717
  };
783
718
  }
784
- function mergeStringField(existing, next) {
785
- if (typeof next !== "string" || !next.trim()) {
786
- return existing;
719
+ function getHeaderCaseInsensitive(headers, key) {
720
+ if (!headers || typeof headers !== "object" || Array.isArray(headers)) {
721
+ return void 0;
787
722
  }
788
- return next.trim();
789
- }
790
- function mergeMentions(existing, next) {
791
- if (!next) {
792
- return existing;
723
+ const entries = Object.entries(headers);
724
+ const matched = entries.find(([name]) => name.toLowerCase() === key.toLowerCase());
725
+ if (!matched) {
726
+ return void 0;
793
727
  }
794
- return normalizeMentions(next) || existing;
795
- }
796
- function mergeMedia(existing, next) {
797
- if (!existing) {
798
- return next;
728
+ const value = matched[1];
729
+ if (Array.isArray(value)) {
730
+ return value.length > 0 ? String(value[0]) : void 0;
799
731
  }
800
- if (!next) {
801
- return existing;
732
+ if (value === null || value === void 0) {
733
+ return void 0;
802
734
  }
803
- return {
804
- downloadCode: next.downloadCode || existing.downloadCode,
805
- downloadCodes: next.downloadCodes || existing.downloadCodes,
806
- spaceId: next.spaceId || existing.spaceId,
807
- fileId: next.fileId || existing.fileId
808
- };
809
- }
810
- function mergeDelivery(existing, next) {
811
- if (!existing) {
812
- return next;
735
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
736
+ return String(value);
813
737
  }
814
- if (!next) {
815
- return existing;
738
+ try {
739
+ return JSON.stringify(value);
740
+ } catch {
741
+ return void 0;
816
742
  }
817
- return {
818
- messageId: next.messageId || existing.messageId,
819
- processQueryKey: next.processQueryKey || existing.processQueryKey,
820
- outTrackId: next.outTrackId || existing.outTrackId,
821
- cardInstanceId: next.cardInstanceId || existing.cardInstanceId,
822
- kind: next.kind || existing.kind
823
- };
824
- }
825
- function mergeAttachmentTextSource(existing, next) {
826
- return next ?? existing;
827
743
  }
828
- function mergeAttachmentTextTruncated(existing, next) {
829
- return next === void 0 ? existing : next;
830
- }
831
- function mergeAttachmentFileName(existing, next) {
832
- if (typeof next !== "string") {
833
- return existing;
744
+ function formatDingTalkConnectionErrorLog(scope, err, baseMessage) {
745
+ if (!err || typeof err !== "object") {
746
+ return null;
834
747
  }
835
- return next;
836
- }
837
- function resolveExistingMsgId(state, params, nowMs) {
838
- if (params.direction === "inbound" && params.msgId) {
839
- const direct = state.records[params.msgId];
840
- if (direct && !isRecordExpired(direct, nowMs)) {
841
- return params.msgId;
842
- }
748
+ const errRecord = err;
749
+ const stage = typeof errRecord.dingtalkConnectionStage === "string" ? errRecord.dingtalkConnectionStage : scope;
750
+ const endpoint = typeof errRecord.dingtalkConnectionEndpoint === "string" ? errRecord.dingtalkConnectionEndpoint : void 0;
751
+ const hasResponse = "response" in errRecord && errRecord.response !== null && errRecord.response !== void 0;
752
+ if (!hasResponse && !endpoint && stage === scope) {
753
+ return null;
843
754
  }
844
- const delivery = params.delivery;
845
- if (!delivery) {
846
- return void 0;
755
+ const parts = [`${baseMessage} [DingTalk][ConnectionError][${stage}]`];
756
+ if (endpoint) {
757
+ parts.push(`endpoint=${endpoint}`);
847
758
  }
848
- const candidates = [
849
- ["messageId", delivery.messageId],
850
- ["processQueryKey", delivery.processQueryKey],
851
- ["outTrackId", delivery.outTrackId],
852
- ["cardInstanceId", delivery.cardInstanceId]
853
- ];
854
- for (const [kind, value] of candidates) {
855
- if (!value) {
856
- continue;
759
+ const response = err.response;
760
+ if (response) {
761
+ if (response.status !== void 0 && response.status !== null) {
762
+ const statusText = typeof response.status === "string" || typeof response.status === "number" || typeof response.status === "boolean" || typeof response.status === "bigint" ? String(response.status) : JSON.stringify(response.status);
763
+ parts.push(`status=${statusText}`);
857
764
  }
858
- const aliasHit = state.byAlias[buildAliasKey(kind, value)];
859
- if (!aliasHit) {
860
- continue;
765
+ let requestId = getHeaderCaseInsensitive(response.headers, "x-acs-dingtalk-request-id");
766
+ if (!requestId && response.data && typeof response.data === "object" && !Array.isArray(response.data)) {
767
+ const data = response.data;
768
+ if (typeof data.requestId === "string") {
769
+ requestId = data.requestId;
770
+ } else if (typeof data.requestid === "string") {
771
+ requestId = data.requestid;
772
+ }
861
773
  }
862
- const record = state.records[aliasHit];
863
- if (record && !isRecordExpired(record, nowMs)) {
864
- return aliasHit;
774
+ if (requestId) {
775
+ parts.push(`requestId=${requestId}`);
776
+ }
777
+ if (response.data !== void 0) {
778
+ parts.push(formatDingTalkErrorPayload(response.data));
865
779
  }
866
780
  }
867
- return void 0;
868
- }
869
- function computeExpiresAt(nowMs, ttlMs, ttlReferenceMs) {
870
- if (typeof ttlMs !== "number" || !Number.isFinite(ttlMs) || ttlMs <= 0) {
871
- return void 0;
781
+ if (stage === "connect.websocket") {
782
+ parts.push("Likely websocket/proxy/WSS issue after connections/open succeeded");
872
783
  }
873
- return (typeof ttlReferenceMs === "number" && Number.isFinite(ttlReferenceMs) ? ttlReferenceMs : nowMs) + ttlMs;
784
+ parts.push("See docs/connection-troubleshooting.md or run scripts/dingtalk-connection-check.*");
785
+ return parts.join(" ");
874
786
  }
875
- function pruneStateByCreatedAt(state, ttlDays, nowMs) {
876
- if (!ttlDays || ttlDays <= 0) {
877
- return { state, removed: 0 };
878
- }
879
- const cutoff = nowMs - ttlDays * 24 * 60 * 60 * 1e3;
880
- const nextRecords = {};
881
- for (const [msgId, record] of Object.entries(state.records)) {
882
- if (record.createdAt >= cutoff) {
883
- nextRecords[msgId] = record;
787
+ function cleanupOrphanedTempFiles(log) {
788
+ const tempDir = os2.tmpdir();
789
+ const dingtalkPattern = /^dingtalk_\d+\..+$/;
790
+ let cleaned = 0;
791
+ try {
792
+ const files = fs.readdirSync(tempDir);
793
+ const now = Date.now();
794
+ const maxAge = 24 * 60 * 60 * 1e3;
795
+ for (const file of files) {
796
+ if (!dingtalkPattern.test(file)) {
797
+ continue;
798
+ }
799
+ const filePath = path2.join(tempDir, file);
800
+ try {
801
+ const stats = fs.statSync(filePath);
802
+ if (now - stats.mtime.getTime() > maxAge) {
803
+ fs.unlinkSync(filePath);
804
+ cleaned++;
805
+ log?.debug?.(`[DingTalk] Cleaned up orphaned temp file: ${file}`);
806
+ }
807
+ } catch (err) {
808
+ log?.debug?.(`[DingTalk] Failed to cleanup temp file ${file}: ${getErrorMessage(err)}`);
809
+ }
884
810
  }
811
+ if (cleaned > 0) {
812
+ log?.info?.(`[DingTalk] Cleaned up ${cleaned} orphaned temp files`);
813
+ }
814
+ } catch (err) {
815
+ log?.debug?.(`[DingTalk] Failed to cleanup temp directory: ${getErrorMessage(err)}`);
885
816
  }
886
- const removed = Object.keys(state.records).length - Object.keys(nextRecords).length;
887
- if (removed === 0) {
888
- return { state, removed: 0 };
889
- }
890
- return {
891
- removed,
892
- state: {
893
- ...state,
894
- records: nextRecords
817
+ return cleaned;
818
+ }
819
+ async function retryWithBackoff(fn, options = {}) {
820
+ const { maxRetries = 3, baseDelayMs = 100, log } = options;
821
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
822
+ try {
823
+ return await fn();
824
+ } catch (err) {
825
+ const statusCode = err.response?.status;
826
+ const isRetryable = statusCode === 401 || statusCode === 429 || statusCode && statusCode >= 500;
827
+ const responseData = getErrorResponseData(err);
828
+ if (responseData !== void 0) {
829
+ log?.debug?.(formatDingTalkErrorPayloadLog("retry.beforeDecision", responseData));
830
+ }
831
+ if (!isRetryable || attempt === maxRetries) {
832
+ throw err;
833
+ }
834
+ const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
835
+ log?.debug?.(`[DingTalk] Retry attempt ${attempt}/${maxRetries} after ${delayMs}ms`);
836
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
895
837
  }
896
- };
838
+ }
839
+ throw new Error("Retry exhausted without returning");
897
840
  }
898
- function upsertRecord(params) {
899
- const nowMs = params.updatedAt ?? Date.now();
900
- let state = cloneStateForMutation(loadState(params, nowMs));
901
- if (params.cleanupCreatedAtTtlDays && params.cleanupCreatedAtTtlDays > 0) {
902
- state = pruneStateByCreatedAt(state, params.cleanupCreatedAtTtlDays, nowMs).state;
841
+ function getCurrentTimestamp() {
842
+ return Date.now();
843
+ }
844
+
845
+ // src/auth.ts
846
+ var accessTokenCache = /* @__PURE__ */ new Map();
847
+ async function getAccessToken(config, log) {
848
+ const cacheKey = config.clientId;
849
+ const now = Date.now();
850
+ const cached = accessTokenCache.get(cacheKey);
851
+ if (cached && cached.expiry > now + 6e4) {
852
+ return cached.accessToken;
903
853
  }
904
- const existingMsgId = resolveExistingMsgId(
905
- state,
906
- {
907
- direction: params.direction,
908
- msgId: params.msgId,
909
- delivery: params.delivery
854
+ const runtimeConfig = await resolveRuntimeConfig(config, log);
855
+ const token = await retryWithBackoff(
856
+ async () => {
857
+ const response = await http_client_default.post(
858
+ "https://api.dingtalk.com/v1.0/oauth2/accessToken",
859
+ {
860
+ appKey: runtimeConfig.clientId,
861
+ appSecret: runtimeConfig.clientSecret
862
+ }
863
+ );
864
+ accessTokenCache.set(cacheKey, {
865
+ accessToken: response.data.accessToken,
866
+ expiry: now + response.data.expireIn * 1e3
867
+ });
868
+ return response.data.accessToken;
910
869
  },
911
- nowMs
870
+ { maxRetries: 3, log }
912
871
  );
913
- const canonicalMsgId = existingMsgId || params.msgId || params.delivery?.messageId || params.delivery?.processQueryKey || params.delivery?.outTrackId;
914
- if (!canonicalMsgId || !canonicalMsgId.trim()) {
915
- return void 0;
872
+ return token;
873
+ }
874
+
875
+ // src/channel.ts
876
+ import { buildChannelConfigSchema } from "openclaw/plugin-sdk/core";
877
+
878
+ // src/config-schema.ts
879
+ import { z as z2 } from "zod";
880
+
881
+ // src/persistence-store.ts
882
+ import * as fs2 from "node:fs";
883
+ import * as path3 from "node:path";
884
+ var NAMESPACE_ROOT_DIR = "dingtalk-state";
885
+ function toErrorMessage(err) {
886
+ if (err instanceof Error) {
887
+ return err.message;
888
+ }
889
+ try {
890
+ return JSON.stringify(err);
891
+ } catch {
892
+ return String(err);
916
893
  }
917
- const existing = state.records[canonicalMsgId];
918
- const expiresAt = computeExpiresAt(nowMs, params.ttlMs, params.ttlReferenceMs);
919
- const normalizedQuotedRef = normalizeQuotedRef(params.quotedRef);
920
- state.records[canonicalMsgId] = {
921
- msgId: canonicalMsgId,
922
- direction: params.direction,
923
- topic: params.topic ?? existing?.topic ?? null,
924
- accountId: params.accountId,
925
- conversationId: params.conversationId,
926
- createdAt: existing?.createdAt ?? params.createdAt,
927
- updatedAt: nowMs,
928
- expiresAt: expiresAt === void 0 ? existing?.expiresAt : Math.max(expiresAt, existing?.expiresAt ?? 0),
929
- messageType: params.messageType || existing?.messageType,
930
- text: mergeText(existing?.text, params.text),
931
- attachmentText: mergeAttachmentText(existing?.attachmentText, params.attachmentText),
932
- attachmentTextSource: mergeAttachmentTextSource(
933
- existing?.attachmentTextSource,
934
- params.attachmentTextSource
935
- ),
936
- attachmentTextTruncated: mergeAttachmentTextTruncated(
937
- existing?.attachmentTextTruncated,
938
- params.attachmentTextTruncated
939
- ),
940
- attachmentFileName: mergeAttachmentFileName(
941
- existing?.attachmentFileName,
942
- params.attachmentFileName
943
- ),
944
- quotedRef: mergeQuotedRef(existing?.quotedRef, normalizedQuotedRef),
945
- senderId: mergeStringField(existing?.senderId, params.senderId),
946
- senderName: mergeStringField(existing?.senderName, params.senderName),
947
- mentions: mergeMentions(existing?.mentions, params.mentions),
948
- chatType: params.chatType || existing?.chatType,
949
- quotedMessageId: mergeStringField(existing?.quotedMessageId, params.quotedMessageId),
950
- media: mergeMedia(existing?.media, params.media),
951
- delivery: mergeDelivery(existing?.delivery, params.delivery)
952
- };
953
- state.updatedAt = nowMs;
954
- writeState(params, normalizeState(state, nowMs).state);
955
- return canonicalMsgId;
956
894
  }
957
- function upsertInboundMessageContext(params) {
958
- return upsertRecord({
959
- ...params,
960
- direction: "inbound",
961
- topic: params.topic ?? null,
962
- msgId: params.msgId,
963
- cleanupCreatedAtTtlDays: params.cleanupCreatedAtTtlDays
964
- }) || params.msgId;
895
+ function sanitizeSegment(value) {
896
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
965
897
  }
966
- function upsertOutboundMessageContext(params) {
967
- return upsertRecord({
968
- ...params,
969
- direction: "outbound",
970
- topic: params.topic ?? null
971
- });
898
+ function encodeScopeValue(value) {
899
+ return Buffer.from(value, "utf8").toString("base64url");
972
900
  }
973
- function resolveByMsgId(params) {
974
- const nowMs = params.nowMs ?? Date.now();
975
- const state = loadState(params, nowMs);
976
- const direct = state.records[params.msgId];
977
- if (direct && !isRecordExpired(direct, nowMs)) {
978
- return direct;
901
+ function buildScopeSuffix(scope) {
902
+ if (!scope) {
903
+ return "";
979
904
  }
980
- const aliasTarget = state.byAlias[buildAliasKey("inboundMsgId", params.msgId)];
981
- if (!aliasTarget) {
982
- return null;
905
+ const ordered = [
906
+ ["accountId", scope.accountId],
907
+ ["agentId", scope.agentId],
908
+ ["conversationId", scope.conversationId],
909
+ ["groupId", scope.groupId],
910
+ ["targetId", scope.targetId]
911
+ ];
912
+ const segments = ordered.filter(([, value]) => Boolean(value && value.trim())).map(([key, value]) => `${key.replace(/Id$/, "")}-${encodeScopeValue((value || "").trim())}`);
913
+ if (segments.length === 0) {
914
+ return "";
983
915
  }
984
- const record = state.records[aliasTarget];
985
- return record && !isRecordExpired(record, nowMs) ? record : null;
916
+ return `.${segments.join(".")}`;
986
917
  }
987
- function resolveByAlias(params) {
988
- const nowMs = params.nowMs ?? Date.now();
989
- const state = loadState(params, nowMs);
990
- const msgId = state.byAlias[buildAliasKey(params.kind, params.value)];
991
- if (!msgId) {
992
- return null;
993
- }
994
- const record = state.records[msgId];
995
- return record && !isRecordExpired(record, nowMs) ? record : null;
918
+ function resolveNamespacePath(namespace, options) {
919
+ const format = options.format || "json";
920
+ const baseDir = path3.join(path3.dirname(options.storePath), NAMESPACE_ROOT_DIR);
921
+ const safeNamespace = sanitizeSegment(namespace.trim());
922
+ const suffix = buildScopeSuffix(options.scope);
923
+ return path3.join(baseDir, `${safeNamespace}${suffix}.${format}`);
996
924
  }
997
- function resolveByQuotedRef(params) {
998
- const nowMs = params.nowMs ?? Date.now();
999
- const quotedRef = normalizeQuotedRef(params.quotedRef);
1000
- const log = params.log;
1001
- if (!quotedRef) {
1002
- log?.debug?.(
1003
- `[DingTalk][QuotedRef] Resolve skipped: invalid quotedRef accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
1004
- );
1005
- return null;
1006
- }
1007
- if (quotedRef.targetDirection === "inbound") {
1008
- if (quotedRef.key !== "msgId" || !quotedRef.value) {
1009
- log?.debug?.(
1010
- `[DingTalk][QuotedRef] Resolve skipped: inbound quotedRef missing msgId accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
1011
- );
1012
- return null;
925
+ function readNamespaceJson(namespace, options) {
926
+ const filePath = resolveNamespacePath(namespace, options);
927
+ try {
928
+ if (!fs2.existsSync(filePath)) {
929
+ return options.fallback;
1013
930
  }
1014
- const record = resolveByMsgId({
1015
- ...params,
1016
- msgId: quotedRef.value,
1017
- nowMs
1018
- });
1019
- log?.debug?.(
1020
- `[DingTalk][QuotedRef] Resolve inbound by msgId=${quotedRef.value} hit=${record ? "yes" : "no"} accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
1021
- );
1022
- return record;
1023
- }
1024
- if (quotedRef.key && quotedRef.value && quotedRef.key !== "msgId") {
1025
- const aliasRecord = resolveByAlias({
1026
- ...params,
1027
- kind: quotedRef.key,
1028
- value: quotedRef.value,
1029
- nowMs
1030
- });
1031
- if (aliasRecord) {
1032
- log?.debug?.(
1033
- `[DingTalk][QuotedRef] Resolve outbound by ${quotedRef.key}=${quotedRef.value} hit=yes accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
1034
- );
1035
- return aliasRecord;
931
+ const raw = fs2.readFileSync(filePath, "utf-8");
932
+ if (!raw.trim()) {
933
+ return options.fallback;
1036
934
  }
1037
- log?.debug?.(
1038
- `[DingTalk][QuotedRef] Resolve outbound by ${quotedRef.key}=${quotedRef.value} hit=no accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
1039
- );
1040
- }
1041
- if (typeof quotedRef.fallbackCreatedAt === "number" && Number.isFinite(quotedRef.fallbackCreatedAt)) {
1042
- const record = resolveByCreatedAtWindow({
1043
- ...params,
1044
- createdAt: quotedRef.fallbackCreatedAt,
1045
- direction: "outbound",
1046
- nowMs
1047
- });
1048
- log?.debug?.(
1049
- `[DingTalk][QuotedRef] Resolve outbound by createdAt=${quotedRef.fallbackCreatedAt} hit=${record ? "yes" : "no"} accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
935
+ return JSON.parse(raw);
936
+ } catch (err) {
937
+ options.log?.warn?.(
938
+ `[DingTalk][Persistence] Failed to read namespace=${namespace} path=${filePath}: ${toErrorMessage(err)}`
1050
939
  );
1051
- return record;
940
+ return options.fallback;
1052
941
  }
1053
- log?.debug?.(
1054
- `[DingTalk][QuotedRef] Resolve outbound missed without createdAt fallback accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
1055
- );
1056
- return null;
1057
942
  }
1058
- function resolveByCreatedAtWindow(params) {
1059
- const nowMs = params.nowMs ?? Date.now();
1060
- const windowMs = params.windowMs ?? DEFAULT_CREATED_AT_MATCH_WINDOW_MS;
1061
- const state = loadState(params, nowMs);
1062
- let bestRecord = null;
1063
- let bestDelta = Infinity;
1064
- for (const msgId of state.recentByCreatedAt) {
1065
- const record = state.records[msgId];
1066
- if (!record || isRecordExpired(record, nowMs)) {
1067
- continue;
1068
- }
1069
- if (params.direction && record.direction !== params.direction) {
1070
- continue;
943
+ function writeNamespaceJsonAtomic(namespace, options) {
944
+ const filePath = resolveNamespacePath(namespace, options);
945
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
946
+ try {
947
+ fs2.mkdirSync(path3.dirname(filePath), { recursive: true });
948
+ fs2.writeFileSync(tempPath, JSON.stringify(options.data, null, 2));
949
+ try {
950
+ fs2.renameSync(tempPath, filePath);
951
+ } catch (err) {
952
+ if (fs2.existsSync(filePath)) {
953
+ fs2.rmSync(filePath, { force: true });
954
+ fs2.renameSync(tempPath, filePath);
955
+ } else {
956
+ throw err;
957
+ }
1071
958
  }
1072
- const delta = Math.abs(record.createdAt - params.createdAt);
1073
- if (delta <= windowMs && delta < bestDelta) {
1074
- bestDelta = delta;
1075
- bestRecord = record;
959
+ } catch (err) {
960
+ options.log?.warn?.(
961
+ `[DingTalk][Persistence] Failed to write namespace=${namespace} path=${filePath}: ${toErrorMessage(err)}`
962
+ );
963
+ if (fs2.existsSync(tempPath)) {
964
+ fs2.rmSync(tempPath, { force: true });
1076
965
  }
1077
966
  }
1078
- return bestRecord;
1079
967
  }
1080
968
 
1081
- // src/config-schema.ts
1082
- var AckReactionSchema = z2.union([
1083
- z2.literal(""),
1084
- z2.enum(["off", "emoji", "kaomoji"]),
1085
- z2.string().min(1)
1086
- ]);
1087
- var CardStreamingModeSchema = z2.enum(["off", "answer", "all"]);
1088
- var ContextVisibilitySchema = z2.enum(["all", "allowlist", "allowlist_quote"]);
1089
- var DingTalkAccountConfigShape = {
1090
- /** Account name (optional display name) */
1091
- name: z2.string().optional(),
1092
- /** Enable or disable this DingTalk channel/account without deleting saved credentials. */
1093
- enabled: z2.boolean().optional().default(true),
1094
- /** DingTalk App Key (Client ID) used to authenticate API and Stream connections. */
1095
- clientId: z2.string().optional(),
1096
- /** DingTalk App Secret (Client Secret) used to obtain DingTalk access tokens. */
1097
- clientSecret: buildSecretInputSchema().optional(),
1098
- /** Direct-message access policy: open, pairing, or allowlist. */
1099
- dmPolicy: z2.enum(["open", "pairing", "allowlist"]).optional().default("open"),
1100
- /** Group-message access policy: open, allowlist, or disabled. */
1101
- groupPolicy: z2.enum(["open", "allowlist", "disabled"]).optional().default("open"),
1102
- /** User IDs allowed when `dmPolicy` is `allowlist`. */
1103
- allowFrom: z2.array(z2.string()).optional(),
1104
- /** Sender IDs allowed when `groupPolicy` is `allowlist`. */
1105
- groupAllowFrom: z2.array(z2.string()).optional(),
1106
- /** Default disabled. Enabling `all` allows learned displayName lookup but may misroute on stale or duplicate names and is available to all callers until upstream exposes requester authz context. */
1107
- displayNameResolution: z2.enum(["disabled", "all"]).optional().default("disabled"),
1108
- /** Controls how much supplemental host context remains visible to the reply runtime. `allowlist_quote` is the safest advanced mode when only explicit quotes or replies should remain visible. */
1109
- contextVisibility: ContextVisibilitySchema.optional(),
1110
- /** Allowed remote media download hosts, IPs, or CIDRs for media fetches. */
1111
- mediaUrlAllowlist: z2.array(z2.string()).optional(),
1112
- /** Native acknowledgement reaction mode: off, emoji, kaomoji, or a custom compatibility string. */
1113
- ackReaction: AckReactionSchema.optional(),
1114
- /** Retention window in days for short-lived message context used by quoting and media recovery. */
1115
- journalTTLDays: z2.number().int().min(1).optional().default(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
1116
- /** Enable verbose DingTalk channel debug logging. */
1117
- debug: z2.boolean().optional().default(false),
1118
- /** Default reply delivery mode: markdown or card. */
1119
- messageType: z2.enum(["markdown", "card"]).optional().default("markdown"),
1120
- /** Deprecated and ignored. AI card replies now always use the built-in DingTalk template contract. Keep only for backward-compatible config parsing. */
1121
- cardTemplateId: z2.string().optional(),
1122
- /** Deprecated and ignored. The built-in AI card contract owns the streaming field mapping. Keep only for backward-compatible config parsing. */
1123
- cardTemplateKey: z2.string().optional().default("content"),
1124
- /** Per-group overrides keyed by conversationId. Supports `*` as a wildcard fallback. */
1125
- groups: z2.record(
1126
- z2.string(),
1127
- z2.object({
1128
- /** Additional system prompt appended for this group. */
1129
- systemPrompt: z2.string().optional(),
1130
- /** Require an explicit @mention before the bot answers in this group. */
1131
- requireMention: z2.boolean().optional(),
1132
- /** Optional per-group sender allowlist for tighter access control than the channel default. */
1133
- groupAllowFrom: z2.array(z2.string()).optional()
1134
- })
1135
- ).optional(),
1136
- /** Connection robustness configuration */
1137
- /** Maximum connection attempts in a single reconnect cycle before backing off or giving up. */
1138
- maxConnectionAttempts: z2.number().int().min(1).optional().default(10),
1139
- /** Initial reconnect backoff delay in milliseconds. */
1140
- initialReconnectDelay: z2.number().int().min(100).optional().default(1e3),
1141
- /** Upper bound for reconnect backoff delay in milliseconds. */
1142
- maxReconnectDelay: z2.number().int().min(1e3).optional().default(6e4),
1143
- /** Randomization factor added to reconnect backoff to avoid synchronized reconnect storms. */
1144
- reconnectJitter: z2.number().min(0).max(1).optional().default(0.3),
1145
- /** Maximum reconnect cycles before the channel stops retrying and waits for the next lifecycle restart. */
1146
- maxReconnectCycles: z2.number().int().min(1).optional().default(10),
1147
- /** Time limit in milliseconds for one reconnect cycle before starting a fresh cycle. */
1148
- reconnectDeadlineMs: z2.number().int().min(5e3).optional().default(5e4),
1149
- /** Enable the plugin connection manager. Disable only when you intentionally rely on DWClient native keepAlive plus autoReconnect behavior. */
1150
- useConnectionManager: z2.boolean().optional().default(true),
1151
- /** Maximum inbound media size in MB accepted by the plugin. When omitted, the runtime default is used. */
1152
- mediaMaxMb: z2.number().int().min(1).optional(),
1153
- /** Enable the underlying Stream client heartbeat. When omitted, runtime derives a default from `useConnectionManager`. */
1154
- keepAlive: z2.boolean().optional(),
1155
- /** Bypass global or system HTTP(S) proxy settings for DingTalk send, upload, and card APIs. */
1156
- bypassProxyForSend: z2.boolean().optional().default(false),
1157
- /** Controls the proactive-send permission reminder shown when a conversation has not granted send rights yet. */
1158
- proactivePermissionHint: z2.object({
1159
- /** Show the proactive-send permission hint when the runtime detects missing DingTalk proactive permission. */
1160
- enabled: z2.boolean().optional().default(true),
1161
- /** Minimum cooldown in hours before the same proactive permission hint can be shown again. */
1162
- cooldownHours: z2.number().int().min(1).max(24 * 30).optional().default(24)
1163
- }).optional().default({ enabled: true, cooldownHours: 24 }),
1164
- /** Deprecated compatibility flag. When true and `cardStreamingMode` is unset, runtime resolves to `cardStreamingMode: "all"`. Do not use in new configs. */
1165
- cardRealTimeStream: z2.boolean().optional(),
1166
- /** Card streaming mode:
1167
- * - off: disable incremental streaming
1168
- * - answer: stream answer text
1169
- * - all: stream answer + reasoning or thinking text */
1170
- cardStreamingMode: CardStreamingModeSchema.optional(),
1171
- /** Throttle interval in milliseconds between AI card streaming updates. */
1172
- cardStreamInterval: z2.number().int().min(200).optional().default(1e3),
1173
- /** Cooldown window in milliseconds after AI card trigger errors. Replies fall back to non-card delivery during this period. */
1174
- aicardDegradeMs: z2.number().int().min(6e4).optional().default(30 * 60 * 1e3),
1175
- /** Enable the local feedback-learning loop for notes, reflections, and command-assisted learning. */
1176
- learningEnabled: z2.boolean().optional(),
1177
- /** Automatically apply generated learning output into session notes or global rules when available. */
1178
- learningAutoApply: z2.boolean().optional(),
1179
- /** Retention window in milliseconds for temporary learning notes. */
1180
- learningNoteTtlMs: z2.number().int().min(6e4).optional(),
1181
- /** Convert markdown tables to plain text before sending when you want more consistent DingTalk rendering. */
1182
- convertMarkdownTables: z2.boolean().optional().default(true),
1183
- /** @mention the sender after card finalization in group chats.
1184
- * Set to a non-empty string (e.g. "✅ 回复完成") to enable — the value is used as the message text.
1185
- * Leave empty or omit to disable. */
1186
- cardAtSender: z2.string().optional(),
1187
- /** Status line visibility toggles for the AI card footer. */
1188
- cardStatusLine: z2.object({
1189
- /** Show model name. */
1190
- model: z2.boolean().optional().default(true),
1191
- /** Show thinking effort level. */
1192
- effort: z2.boolean().optional().default(true),
1193
- /** Show agent display name. */
1194
- agent: z2.boolean().optional().default(true),
1195
- /** Show task elapsed time. */
1196
- taskTime: z2.boolean().optional().default(false),
1197
- /** Show token usage summary (input/output/cache). */
1198
- tokens: z2.boolean().optional().default(false),
1199
- /** Show DingTalk API call count. */
1200
- dapiUsage: z2.boolean().optional().default(false)
1201
- }).optional().default({ model: true, effort: true, agent: true, taskTime: false, tokens: false, dapiUsage: false })
969
+ // src/message-context-store.ts
970
+ var MESSAGE_CONTEXT_NAMESPACE = "messages.context";
971
+ var MESSAGE_CONTEXT_VERSION = 1;
972
+ var DEFAULT_MESSAGE_CONTEXT_TTL_DAYS = 7;
973
+ var DEFAULT_CARD_CONTENT_TTL_MS = 24 * 60 * 60 * 1e3;
974
+ var DEFAULT_MEDIA_CONTEXT_TTL_MS = 24 * 60 * 60 * 1e3;
975
+ var DEFAULT_CREATED_AT_MATCH_WINDOW_MS = 2e3;
976
+ var MAX_RECORDS_PER_SCOPE = 1e3;
977
+ var DEFAULT_OUTBOUND_SENDER = {
978
+ senderId: "bot",
979
+ senderName: "OpenClaw"
1202
980
  };
1203
- var DingTalkAccountConfigSchema = z2.object(DingTalkAccountConfigShape);
1204
- var DingTalkConfigSchema = DingTalkAccountConfigSchema.extend({
1205
- /** Multi-account configuration */
1206
- accounts: z2.record(z2.string(), DingTalkAccountConfigSchema.optional()).optional()
1207
- });
1208
-
1209
- // src/gateway/channel-gateway.ts
1210
- import { DWClient, TOPIC_CARD, TOPIC_ROBOT } from "dingtalk-stream";
1211
-
1212
- // src/http-client.ts
1213
- import axios from "axios";
1214
- var DEFAULT_HTTP_TIMEOUT_MS = 1e4;
1215
- var httpClient = typeof axios?.create === "function" ? axios.create({ timeout: DEFAULT_HTTP_TIMEOUT_MS }) : axios;
1216
- httpClient.isAxiosError = axios.isAxiosError;
1217
- var http_client_default = httpClient;
1218
-
1219
- // src/utils.ts
1220
- import * as dns from "node:dns";
1221
- import * as fs2 from "node:fs";
1222
- import * as net from "node:net";
1223
- import * as os2 from "node:os";
1224
- import * as path3 from "node:path";
1225
- var pluginDebugWriters = /* @__PURE__ */ new Map();
1226
- var closedPluginDebugScopes = /* @__PURE__ */ new Set();
1227
- function padNumber(value, width = 2) {
1228
- return String(value).padStart(width, "0");
981
+ function inferConversationChatType(conversationId) {
982
+ return conversationId.startsWith("cid") ? "group" : "direct";
1229
983
  }
1230
- function formatTimezoneOffset(date) {
1231
- const offsetMinutes = -date.getTimezoneOffset();
1232
- const sign = offsetMinutes >= 0 ? "+" : "-";
1233
- const absoluteMinutes = Math.abs(offsetMinutes);
1234
- const hours = Math.floor(absoluteMinutes / 60);
1235
- const minutes = absoluteMinutes % 60;
1236
- return `${sign}${padNumber(hours)}:${padNumber(minutes)}`;
984
+ var stateCache = /* @__PURE__ */ new Map();
985
+ function getScopeKey(params) {
986
+ return JSON.stringify([
987
+ params.storePath || "__memory__",
988
+ params.accountId,
989
+ params.conversationId || null
990
+ ]);
1237
991
  }
1238
- function formatPluginDebugTimestamp(date) {
1239
- return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())} ${padNumber(date.getHours())}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}.${padNumber(date.getMilliseconds(), 3)}${formatTimezoneOffset(date)}`;
992
+ function fallbackState() {
993
+ return {
994
+ version: MESSAGE_CONTEXT_VERSION,
995
+ updatedAt: Date.now(),
996
+ records: {},
997
+ byAlias: {},
998
+ recentByCreatedAt: []
999
+ };
1240
1000
  }
1241
- function formatPluginDebugDate(date) {
1242
- return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}`;
1001
+ function asRecord(value) {
1002
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1003
+ return null;
1004
+ }
1005
+ return value;
1243
1006
  }
1244
- function resolvePluginDebugLogFilePath(params) {
1245
- return path3.join(
1246
- path3.dirname(params.storePath),
1247
- "logs",
1248
- "dingtalk",
1249
- params.accountId,
1250
- `debug-${formatPluginDebugDate(params.date)}.log`
1251
- );
1007
+ function normalizeMedia(value) {
1008
+ const candidate = asRecord(value);
1009
+ if (!candidate) {
1010
+ return void 0;
1011
+ }
1012
+ const downloadCode = typeof candidate.downloadCode === "string" && candidate.downloadCode.trim() ? candidate.downloadCode.trim() : void 0;
1013
+ const downloadCodes = Array.isArray(candidate.downloadCodes) && candidate.downloadCodes.length > 0 ? candidate.downloadCodes.filter((c) => typeof c === "string" && c.trim()) : void 0;
1014
+ const spaceId = typeof candidate.spaceId === "string" && candidate.spaceId.trim() ? candidate.spaceId.trim() : void 0;
1015
+ const fileId = typeof candidate.fileId === "string" && candidate.fileId.trim() ? candidate.fileId.trim() : void 0;
1016
+ if (!downloadCode && (!downloadCodes || downloadCodes.length === 0) && !spaceId && !fileId) {
1017
+ return void 0;
1018
+ }
1019
+ return { downloadCode, downloadCodes, spaceId, fileId };
1252
1020
  }
1253
- function formatPluginDebugLine(params) {
1254
- return `[${formatPluginDebugTimestamp(params.date)}] [debug] [dingtalk] [account:${params.accountId}] ${params.message}`;
1021
+ function normalizeQuotedRef(value) {
1022
+ const candidate = asRecord(value);
1023
+ if (!candidate) {
1024
+ return void 0;
1025
+ }
1026
+ const targetDirection = candidate.targetDirection === "outbound" ? "outbound" : candidate.targetDirection === "inbound" ? "inbound" : void 0;
1027
+ const key = candidate.key === "msgId" || candidate.key === "messageId" || candidate.key === "processQueryKey" || candidate.key === "outTrackId" || candidate.key === "cardInstanceId" ? candidate.key : void 0;
1028
+ const valueString = typeof candidate.value === "string" && candidate.value.trim() ? candidate.value.trim() : void 0;
1029
+ const fallbackCreatedAt = typeof candidate.fallbackCreatedAt === "number" && Number.isFinite(candidate.fallbackCreatedAt) ? candidate.fallbackCreatedAt : void 0;
1030
+ if (!targetDirection) {
1031
+ return void 0;
1032
+ }
1033
+ if (!key && !fallbackCreatedAt) {
1034
+ return void 0;
1035
+ }
1036
+ if (key && !valueString) {
1037
+ return void 0;
1038
+ }
1039
+ return {
1040
+ targetDirection,
1041
+ key,
1042
+ value: valueString,
1043
+ fallbackCreatedAt
1044
+ };
1255
1045
  }
1256
- function buildPluginDebugWriterKey(params) {
1257
- return JSON.stringify([params.storePath, params.accountId, formatPluginDebugDate(params.date)]);
1046
+ function normalizeAttachmentTextSource(value) {
1047
+ return value === "text" || value === "html" || value === "pdf" || value === "docx" ? value : void 0;
1258
1048
  }
1259
- function buildPluginDebugScopeKey(params) {
1260
- return JSON.stringify([params.storePath, params.accountId]);
1049
+ function normalizeDelivery(value) {
1050
+ const candidate = asRecord(value);
1051
+ if (!candidate) {
1052
+ return void 0;
1053
+ }
1054
+ const messageId = typeof candidate.messageId === "string" && candidate.messageId.trim() ? candidate.messageId.trim() : void 0;
1055
+ const processQueryKey = typeof candidate.processQueryKey === "string" && candidate.processQueryKey.trim() ? candidate.processQueryKey.trim() : void 0;
1056
+ const outTrackId = typeof candidate.outTrackId === "string" && candidate.outTrackId.trim() ? candidate.outTrackId.trim() : void 0;
1057
+ const cardInstanceId = typeof candidate.cardInstanceId === "string" && candidate.cardInstanceId.trim() ? candidate.cardInstanceId.trim() : void 0;
1058
+ const kind = typeof candidate.kind === "string" && candidate.kind.trim() ? candidate.kind.trim() : void 0;
1059
+ if (!messageId && !processQueryKey && !outTrackId && !cardInstanceId && !kind) {
1060
+ return void 0;
1061
+ }
1062
+ return { messageId, processQueryKey, outTrackId, cardInstanceId, kind };
1261
1063
  }
1262
- function resolvePluginDebugWriter(params) {
1263
- const key = buildPluginDebugWriterKey(params);
1264
- const existing = pluginDebugWriters.get(key);
1265
- if (existing) {
1266
- return existing;
1064
+ function normalizeMentions(value) {
1065
+ if (!Array.isArray(value)) {
1066
+ return void 0;
1267
1067
  }
1268
- const created = {
1269
- filePath: resolvePluginDebugLogFilePath(params),
1270
- warned: false,
1271
- directoryReady: false
1068
+ const normalized = [...new Set(value.map((item) => String(item || "").trim()).filter(Boolean))];
1069
+ return normalized.length > 0 ? normalized : void 0;
1070
+ }
1071
+ function normalizeMessageRecord(value) {
1072
+ const candidate = asRecord(value);
1073
+ if (!candidate) {
1074
+ return null;
1075
+ }
1076
+ const msgId = typeof candidate.msgId === "string" && candidate.msgId.trim() ? candidate.msgId.trim() : "";
1077
+ const direction = candidate.direction === "outbound" ? "outbound" : candidate.direction === "inbound" ? "inbound" : null;
1078
+ const accountId = typeof candidate.accountId === "string" && candidate.accountId.trim() ? candidate.accountId.trim() : "";
1079
+ const conversationId = typeof candidate.conversationId === "string" && candidate.conversationId.trim() ? candidate.conversationId.trim() : null;
1080
+ const createdAt = typeof candidate.createdAt === "number" && Number.isFinite(candidate.createdAt) ? candidate.createdAt : NaN;
1081
+ const updatedAt = typeof candidate.updatedAt === "number" && Number.isFinite(candidate.updatedAt) ? candidate.updatedAt : Date.now();
1082
+ if (!msgId || !direction || !accountId || !Number.isFinite(createdAt)) {
1083
+ return null;
1084
+ }
1085
+ const expiresAt = typeof candidate.expiresAt === "number" && Number.isFinite(candidate.expiresAt) ? candidate.expiresAt : void 0;
1086
+ return {
1087
+ msgId,
1088
+ direction,
1089
+ topic: candidate.topic === null ? null : typeof candidate.topic === "string" ? candidate.topic : null,
1090
+ accountId,
1091
+ conversationId,
1092
+ createdAt,
1093
+ updatedAt,
1094
+ expiresAt,
1095
+ messageType: typeof candidate.messageType === "string" ? candidate.messageType : void 0,
1096
+ text: typeof candidate.text === "string" ? candidate.text : void 0,
1097
+ attachmentText: typeof candidate.attachmentText === "string" ? candidate.attachmentText : void 0,
1098
+ attachmentTextSource: normalizeAttachmentTextSource(candidate.attachmentTextSource),
1099
+ attachmentTextTruncated: candidate.attachmentTextTruncated === true ? true : void 0,
1100
+ attachmentFileName: typeof candidate.attachmentFileName === "string" ? candidate.attachmentFileName : void 0,
1101
+ quotedRef: normalizeQuotedRef(candidate.quotedRef),
1102
+ senderId: typeof candidate.senderId === "string" && candidate.senderId.trim() ? candidate.senderId.trim() : void 0,
1103
+ senderName: typeof candidate.senderName === "string" && candidate.senderName.trim() ? candidate.senderName.trim() : void 0,
1104
+ mentions: normalizeMentions(candidate.mentions),
1105
+ chatType: candidate.chatType === "direct" || candidate.chatType === "group" ? candidate.chatType : void 0,
1106
+ quotedMessageId: typeof candidate.quotedMessageId === "string" && candidate.quotedMessageId.trim() ? candidate.quotedMessageId.trim() : void 0,
1107
+ media: normalizeMedia(candidate.media),
1108
+ delivery: normalizeDelivery(candidate.delivery)
1272
1109
  };
1273
- pluginDebugWriters.set(key, created);
1274
- return created;
1275
1110
  }
1276
- function resolvePluginDebugLog(params) {
1277
- const baseLog = params.baseLog;
1278
- const fsImpl = params.fsImpl ?? fs2;
1279
- const scopeKey = params.storePath ? buildPluginDebugScopeKey({ storePath: params.storePath, accountId: params.accountId }) : void 0;
1280
- if (scopeKey) {
1281
- closedPluginDebugScopes.delete(scopeKey);
1111
+ function buildAliasKey(kind, value) {
1112
+ return `${kind}:${value.trim()}`;
1113
+ }
1114
+ function buildAliasEntries(record) {
1115
+ const aliases = [];
1116
+ if (record.direction === "inbound" && record.msgId.trim()) {
1117
+ aliases.push([buildAliasKey("inboundMsgId", record.msgId), record.msgId]);
1118
+ }
1119
+ const delivery = record.delivery;
1120
+ if (!delivery) {
1121
+ return aliases;
1122
+ }
1123
+ if (delivery.messageId) {
1124
+ aliases.push([buildAliasKey("messageId", delivery.messageId), record.msgId]);
1125
+ }
1126
+ if (delivery.processQueryKey) {
1127
+ aliases.push([buildAliasKey("processQueryKey", delivery.processQueryKey), record.msgId]);
1128
+ }
1129
+ if (delivery.outTrackId) {
1130
+ aliases.push([buildAliasKey("outTrackId", delivery.outTrackId), record.msgId]);
1131
+ }
1132
+ if (delivery.cardInstanceId) {
1133
+ aliases.push([buildAliasKey("cardInstanceId", delivery.cardInstanceId), record.msgId]);
1134
+ }
1135
+ return aliases;
1136
+ }
1137
+ function isRecordExpired(record, nowMs) {
1138
+ return typeof record.expiresAt === "number" && Number.isFinite(record.expiresAt) && nowMs >= record.expiresAt;
1139
+ }
1140
+ function normalizeState(state, nowMs) {
1141
+ const normalizedRecords = {};
1142
+ const sortedRecords = Object.values(state.records).map((record) => normalizeMessageRecord(record)).filter((record) => record !== null).filter((record) => !isRecordExpired(record, nowMs)).toSorted((left, right) => left.createdAt - right.createdAt);
1143
+ const keptRecords = sortedRecords.slice(-MAX_RECORDS_PER_SCOPE);
1144
+ for (const record of keptRecords) {
1145
+ normalizedRecords[record.msgId] = record;
1146
+ }
1147
+ const byAlias = {};
1148
+ for (const record of keptRecords) {
1149
+ for (const [key, value] of buildAliasEntries(record)) {
1150
+ byAlias[key] = value;
1151
+ }
1282
1152
  }
1283
1153
  return {
1284
- debug: (message) => {
1285
- if (!params.debug) {
1286
- baseLog?.debug?.(message);
1287
- return;
1288
- }
1289
- const date = params.now ? params.now() : /* @__PURE__ */ new Date();
1290
- const line = formatPluginDebugLine({
1291
- accountId: params.accountId,
1292
- date,
1293
- message
1294
- });
1295
- try {
1296
- process.stdout.write(`${line}
1297
- `);
1298
- } catch {
1299
- }
1300
- if (params.storePath && scopeKey && !closedPluginDebugScopes.has(scopeKey)) {
1301
- const writer = resolvePluginDebugWriter({
1302
- storePath: params.storePath,
1303
- accountId: params.accountId,
1304
- date
1305
- });
1306
- try {
1307
- if (!writer.directoryReady) {
1308
- fsImpl.mkdirSync(path3.dirname(writer.filePath), { recursive: true });
1309
- writer.directoryReady = true;
1310
- }
1311
- fsImpl.appendFileSync(writer.filePath, `${line}
1312
- `, "utf8");
1313
- } catch (err) {
1314
- if (!writer.warned) {
1315
- writer.warned = true;
1316
- baseLog?.warn?.(
1317
- `[DingTalk] Plugin debug log file unavailable: accountId=${params.accountId} path=${writer.filePath} error=${getErrorMessage(err)}`
1318
- );
1319
- }
1320
- }
1321
- }
1322
- try {
1323
- baseLog?.debug?.(message);
1324
- } catch {
1325
- }
1326
- },
1327
- info: (message) => baseLog?.info?.(message),
1328
- warn: (message) => baseLog?.warn?.(message),
1329
- error: (message) => baseLog?.error?.(message)
1154
+ removed: Object.keys(state.records).length - Object.keys(normalizedRecords).length,
1155
+ state: {
1156
+ version: MESSAGE_CONTEXT_VERSION,
1157
+ updatedAt: state.updatedAt,
1158
+ records: normalizedRecords,
1159
+ byAlias,
1160
+ recentByCreatedAt: keptRecords.map((record) => record.msgId)
1161
+ }
1330
1162
  };
1331
1163
  }
1332
- function closePluginDebugLog(params) {
1164
+ function hydrateState(params, nowMs) {
1165
+ if (!params.storePath) {
1166
+ return fallbackState();
1167
+ }
1168
+ const persisted = readNamespaceJson(
1169
+ MESSAGE_CONTEXT_NAMESPACE,
1170
+ {
1171
+ storePath: params.storePath,
1172
+ scope: { accountId: params.accountId, conversationId: params.conversationId || void 0 },
1173
+ format: "json",
1174
+ fallback: { version: MESSAGE_CONTEXT_VERSION, updatedAt: Date.now(), records: {} }
1175
+ }
1176
+ );
1177
+ const parsedRecords = asRecord(persisted.records) || {};
1178
+ const hydrated = fallbackState();
1179
+ hydrated.updatedAt = typeof persisted.updatedAt === "number" ? persisted.updatedAt : Date.now();
1180
+ for (const [key, value] of Object.entries(parsedRecords)) {
1181
+ const normalized = normalizeMessageRecord(value);
1182
+ if (normalized) {
1183
+ hydrated.records[key] = normalized;
1184
+ }
1185
+ }
1186
+ return normalizeState(hydrated, nowMs).state;
1187
+ }
1188
+ function loadState(params, nowMs = Date.now()) {
1189
+ const scopeKey = getScopeKey(params);
1190
+ const cached = stateCache.get(scopeKey);
1191
+ if (cached) {
1192
+ return cached;
1193
+ }
1194
+ const hydrated = params.storePath ? hydrateState(params, nowMs) : fallbackState();
1195
+ stateCache.set(scopeKey, hydrated);
1196
+ return hydrated;
1197
+ }
1198
+ function writeState(params, state) {
1199
+ stateCache.set(getScopeKey(params), state);
1333
1200
  if (!params.storePath) {
1334
1201
  return;
1335
1202
  }
1336
- const scopeKey = buildPluginDebugScopeKey({
1203
+ writeNamespaceJsonAtomic(MESSAGE_CONTEXT_NAMESPACE, {
1337
1204
  storePath: params.storePath,
1338
- accountId: params.accountId
1339
- });
1340
- closedPluginDebugScopes.add(scopeKey);
1341
- for (const key of pluginDebugWriters.keys()) {
1342
- const [writerStorePath, writerAccountId] = JSON.parse(key);
1343
- if (writerStorePath === params.storePath && writerAccountId === params.accountId) {
1344
- pluginDebugWriters.delete(key);
1205
+ scope: { accountId: params.accountId, conversationId: params.conversationId || void 0 },
1206
+ format: "json",
1207
+ data: {
1208
+ version: MESSAGE_CONTEXT_VERSION,
1209
+ updatedAt: state.updatedAt,
1210
+ records: state.records
1345
1211
  }
1346
- }
1212
+ });
1347
1213
  }
1348
- function maskSensitiveData(data) {
1349
- if (data === null || data === void 0) {
1350
- return data;
1351
- }
1352
- if (typeof data !== "object") {
1353
- return data;
1354
- }
1355
- const masked = JSON.parse(JSON.stringify(data));
1356
- const sensitiveFields = /* @__PURE__ */ new Set(["token", "accessToken"]);
1357
- function maskObj(obj) {
1358
- for (const key in obj) {
1359
- if (sensitiveFields.has(key)) {
1360
- const val = obj[key];
1361
- if (typeof val === "string" && val.length > 6) {
1362
- obj[key] = val.slice(0, 3) + "*".repeat(val.length - 6) + val.slice(-3);
1363
- } else if (typeof val === "string") {
1364
- obj[key] = "*".repeat(val.length);
1365
- }
1366
- } else if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) {
1367
- maskObj(obj[key]);
1368
- }
1369
- }
1370
- }
1371
- maskObj(masked);
1372
- return masked;
1214
+ function cloneStateForMutation(state) {
1215
+ return {
1216
+ version: state.version,
1217
+ updatedAt: state.updatedAt,
1218
+ records: { ...state.records },
1219
+ byAlias: state.byAlias,
1220
+ recentByCreatedAt: state.recentByCreatedAt
1221
+ };
1373
1222
  }
1374
- function stringifyUnknown(value) {
1375
- if (typeof value === "string") {
1376
- return value;
1377
- }
1378
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
1379
- return String(value);
1380
- }
1381
- try {
1382
- const serialized = JSON.stringify(value);
1383
- return serialized ?? String(value);
1384
- } catch {
1385
- return "[unserializable]";
1223
+ function mergeText(existing, next) {
1224
+ if (typeof next !== "string") {
1225
+ return existing;
1386
1226
  }
1227
+ return next;
1387
1228
  }
1388
- function parseBooleanLike(value) {
1389
- if (typeof value === "boolean") {
1390
- return value;
1391
- }
1392
- if (typeof value === "number") {
1393
- if (value === 1) {
1394
- return true;
1395
- }
1396
- if (value === 0) {
1397
- return false;
1398
- }
1399
- return void 0;
1400
- }
1401
- if (typeof value === "string") {
1402
- const normalized = value.trim().toLowerCase();
1403
- if (["1", "true", "yes", "y", "on"].includes(normalized)) {
1404
- return true;
1405
- }
1406
- if (["0", "false", "no", "n", "off"].includes(normalized)) {
1407
- return false;
1408
- }
1229
+ function mergeAttachmentText(existing, next) {
1230
+ if (typeof next !== "string") {
1231
+ return existing;
1409
1232
  }
1410
- return void 0;
1233
+ return next;
1411
1234
  }
1412
- function getErrorMessage(err) {
1413
- if (err instanceof Error && err.message) {
1414
- return err.message;
1235
+ function mergeQuotedRef(existing, next) {
1236
+ if (!existing) {
1237
+ return next;
1415
1238
  }
1416
- if (err && typeof err === "object") {
1417
- const record = err;
1418
- if (typeof record.message === "string" && record.message.trim()) {
1419
- return record.message;
1420
- }
1239
+ if (!next) {
1240
+ return existing;
1421
1241
  }
1422
- return stringifyUnknown(err);
1242
+ return {
1243
+ targetDirection: next.targetDirection,
1244
+ key: next.key || existing.key,
1245
+ value: next.value || existing.value,
1246
+ fallbackCreatedAt: next.fallbackCreatedAt ?? existing.fallbackCreatedAt
1247
+ };
1423
1248
  }
1424
- function getErrorResponseData(err) {
1425
- if (!err || typeof err !== "object") {
1426
- return void 0;
1249
+ function mergeStringField(existing, next) {
1250
+ if (typeof next !== "string" || !next.trim()) {
1251
+ return existing;
1427
1252
  }
1428
- return err.response?.data;
1253
+ return next.trim();
1429
1254
  }
1430
- function formatDingTalkErrorPayload(payload) {
1431
- if (payload === null || payload === void 0) {
1432
- return "payload=unknown";
1255
+ function mergeMentions(existing, next) {
1256
+ if (!next) {
1257
+ return existing;
1433
1258
  }
1434
- let code;
1435
- let message;
1436
- if (typeof payload === "object" && !Array.isArray(payload)) {
1437
- const obj = payload;
1438
- if (typeof obj.code === "string" || typeof obj.code === "number") {
1439
- code = String(obj.code);
1440
- }
1441
- if (typeof obj.message === "string") {
1442
- message = obj.message;
1443
- }
1259
+ return normalizeMentions(next) || existing;
1260
+ }
1261
+ function mergeMedia(existing, next) {
1262
+ if (!existing) {
1263
+ return next;
1444
1264
  }
1445
- let serialized;
1446
- try {
1447
- serialized = JSON.stringify(maskSensitiveData(payload));
1448
- } catch {
1449
- if (typeof payload === "string") {
1450
- serialized = payload;
1451
- } else if (typeof payload === "number" || typeof payload === "boolean" || typeof payload === "bigint") {
1452
- serialized = `${payload}`;
1453
- } else {
1454
- serialized = "[unserializable-payload]";
1455
- }
1265
+ if (!next) {
1266
+ return existing;
1456
1267
  }
1457
- const parts = [];
1458
- if (code) {
1459
- parts.push(`code=${code}`);
1268
+ return {
1269
+ downloadCode: next.downloadCode || existing.downloadCode,
1270
+ downloadCodes: next.downloadCodes || existing.downloadCodes,
1271
+ spaceId: next.spaceId || existing.spaceId,
1272
+ fileId: next.fileId || existing.fileId
1273
+ };
1274
+ }
1275
+ function mergeDelivery(existing, next) {
1276
+ if (!existing) {
1277
+ return next;
1460
1278
  }
1461
- if (message) {
1462
- parts.push(`message=${message}`);
1279
+ if (!next) {
1280
+ return existing;
1463
1281
  }
1464
- parts.push(`payload=${serialized}`);
1465
- return parts.join(" ");
1282
+ return {
1283
+ messageId: next.messageId || existing.messageId,
1284
+ processQueryKey: next.processQueryKey || existing.processQueryKey,
1285
+ outTrackId: next.outTrackId || existing.outTrackId,
1286
+ cardInstanceId: next.cardInstanceId || existing.cardInstanceId,
1287
+ kind: next.kind || existing.kind
1288
+ };
1466
1289
  }
1467
- function formatDingTalkErrorPayloadLog(scope, payload, prefix = "[DingTalk]") {
1468
- return `${prefix}[ErrorPayload][${scope}] ${formatDingTalkErrorPayload(payload)}`;
1290
+ function mergeAttachmentTextSource(existing, next) {
1291
+ return next ?? existing;
1469
1292
  }
1470
- function getProxyBypassOption(config) {
1471
- return config?.bypassProxyForSend ? { proxy: false } : {};
1293
+ function mergeAttachmentTextTruncated(existing, next) {
1294
+ return next === void 0 ? existing : next;
1472
1295
  }
1473
- function createResolve4FallbackLookup(log, accountId) {
1474
- return createResolve4FallbackLookupWithDeps(log, accountId, dns, net);
1296
+ function mergeAttachmentFileName(existing, next) {
1297
+ if (typeof next !== "string") {
1298
+ return existing;
1299
+ }
1300
+ return next;
1475
1301
  }
1476
- function createResolve4FallbackLookupWithDeps(log, accountId, dnsImpl, netImpl) {
1477
- let fallbackLogged = false;
1478
- return (hostname, options, callback) => {
1479
- const ipFamily = netImpl.isIP(hostname);
1480
- if (ipFamily !== 0) {
1481
- if (options.all) {
1482
- callback(null, [{ address: hostname, family: ipFamily }], ipFamily);
1483
- return;
1484
- }
1485
- callback(null, hostname, ipFamily);
1486
- return;
1302
+ function resolveExistingMsgId(state, params, nowMs) {
1303
+ if (params.direction === "inbound" && params.msgId) {
1304
+ const direct = state.records[params.msgId];
1305
+ if (direct && !isRecordExpired(direct, nowMs)) {
1306
+ return params.msgId;
1487
1307
  }
1488
- dnsImpl.lookup(hostname, options, (lookupErr, address, family) => {
1489
- if (!lookupErr) {
1490
- callback(null, address, family);
1491
- return;
1492
- }
1493
- if (lookupErr.code !== "ENOTFOUND") {
1494
- callback(lookupErr, address, family);
1495
- return;
1496
- }
1497
- dnsImpl.resolve4(hostname, (resolveErr, addresses) => {
1498
- if (resolveErr || !addresses || addresses.length === 0) {
1499
- callback(lookupErr, address, family);
1500
- return;
1501
- }
1502
- if (!fallbackLogged) {
1503
- fallbackLogged = true;
1504
- log?.warn?.(
1505
- `[${accountId ?? "default"}] System DNS lookup failed for ${hostname} (ENOTFOUND); using resolve4 fallback ${addresses[0]}`
1506
- );
1507
- }
1508
- if (options.all) {
1509
- callback(
1510
- null,
1511
- addresses.map((item) => ({ address: item, family: 4 })),
1512
- 4
1513
- );
1514
- return;
1515
- }
1516
- callback(null, addresses[0], 4);
1517
- });
1518
- });
1519
- };
1308
+ }
1309
+ const delivery = params.delivery;
1310
+ if (!delivery) {
1311
+ return void 0;
1312
+ }
1313
+ const candidates = [
1314
+ ["messageId", delivery.messageId],
1315
+ ["processQueryKey", delivery.processQueryKey],
1316
+ ["outTrackId", delivery.outTrackId],
1317
+ ["cardInstanceId", delivery.cardInstanceId]
1318
+ ];
1319
+ for (const [kind, value] of candidates) {
1320
+ if (!value) {
1321
+ continue;
1322
+ }
1323
+ const aliasHit = state.byAlias[buildAliasKey(kind, value)];
1324
+ if (!aliasHit) {
1325
+ continue;
1326
+ }
1327
+ const record = state.records[aliasHit];
1328
+ if (record && !isRecordExpired(record, nowMs)) {
1329
+ return aliasHit;
1330
+ }
1331
+ }
1332
+ return void 0;
1520
1333
  }
1521
- function getHeaderCaseInsensitive(headers, key) {
1522
- if (!headers || typeof headers !== "object" || Array.isArray(headers)) {
1334
+ function computeExpiresAt(nowMs, ttlMs, ttlReferenceMs) {
1335
+ if (typeof ttlMs !== "number" || !Number.isFinite(ttlMs) || ttlMs <= 0) {
1523
1336
  return void 0;
1524
1337
  }
1525
- const entries = Object.entries(headers);
1526
- const matched = entries.find(([name]) => name.toLowerCase() === key.toLowerCase());
1527
- if (!matched) {
1528
- return void 0;
1338
+ return (typeof ttlReferenceMs === "number" && Number.isFinite(ttlReferenceMs) ? ttlReferenceMs : nowMs) + ttlMs;
1339
+ }
1340
+ function pruneStateByCreatedAt(state, ttlDays, nowMs) {
1341
+ if (!ttlDays || ttlDays <= 0) {
1342
+ return { state, removed: 0 };
1529
1343
  }
1530
- const value = matched[1];
1531
- if (Array.isArray(value)) {
1532
- return value.length > 0 ? String(value[0]) : void 0;
1344
+ const cutoff = nowMs - ttlDays * 24 * 60 * 60 * 1e3;
1345
+ const nextRecords = {};
1346
+ for (const [msgId, record] of Object.entries(state.records)) {
1347
+ if (record.createdAt >= cutoff) {
1348
+ nextRecords[msgId] = record;
1349
+ }
1533
1350
  }
1534
- if (value === null || value === void 0) {
1535
- return void 0;
1351
+ const removed = Object.keys(state.records).length - Object.keys(nextRecords).length;
1352
+ if (removed === 0) {
1353
+ return { state, removed: 0 };
1536
1354
  }
1537
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
1538
- return String(value);
1355
+ return {
1356
+ removed,
1357
+ state: {
1358
+ ...state,
1359
+ records: nextRecords
1360
+ }
1361
+ };
1362
+ }
1363
+ function upsertRecord(params) {
1364
+ const nowMs = params.updatedAt ?? Date.now();
1365
+ let state = cloneStateForMutation(loadState(params, nowMs));
1366
+ if (params.cleanupCreatedAtTtlDays && params.cleanupCreatedAtTtlDays > 0) {
1367
+ state = pruneStateByCreatedAt(state, params.cleanupCreatedAtTtlDays, nowMs).state;
1539
1368
  }
1540
- try {
1541
- return JSON.stringify(value);
1542
- } catch {
1369
+ const existingMsgId = resolveExistingMsgId(
1370
+ state,
1371
+ {
1372
+ direction: params.direction,
1373
+ msgId: params.msgId,
1374
+ delivery: params.delivery
1375
+ },
1376
+ nowMs
1377
+ );
1378
+ const canonicalMsgId = existingMsgId || params.msgId || params.delivery?.messageId || params.delivery?.processQueryKey || params.delivery?.outTrackId;
1379
+ if (!canonicalMsgId || !canonicalMsgId.trim()) {
1543
1380
  return void 0;
1544
1381
  }
1382
+ const existing = state.records[canonicalMsgId];
1383
+ const expiresAt = computeExpiresAt(nowMs, params.ttlMs, params.ttlReferenceMs);
1384
+ const normalizedQuotedRef = normalizeQuotedRef(params.quotedRef);
1385
+ state.records[canonicalMsgId] = {
1386
+ msgId: canonicalMsgId,
1387
+ direction: params.direction,
1388
+ topic: params.topic ?? existing?.topic ?? null,
1389
+ accountId: params.accountId,
1390
+ conversationId: params.conversationId,
1391
+ createdAt: existing?.createdAt ?? params.createdAt,
1392
+ updatedAt: nowMs,
1393
+ expiresAt: expiresAt === void 0 ? existing?.expiresAt : Math.max(expiresAt, existing?.expiresAt ?? 0),
1394
+ messageType: params.messageType || existing?.messageType,
1395
+ text: mergeText(existing?.text, params.text),
1396
+ attachmentText: mergeAttachmentText(existing?.attachmentText, params.attachmentText),
1397
+ attachmentTextSource: mergeAttachmentTextSource(
1398
+ existing?.attachmentTextSource,
1399
+ params.attachmentTextSource
1400
+ ),
1401
+ attachmentTextTruncated: mergeAttachmentTextTruncated(
1402
+ existing?.attachmentTextTruncated,
1403
+ params.attachmentTextTruncated
1404
+ ),
1405
+ attachmentFileName: mergeAttachmentFileName(
1406
+ existing?.attachmentFileName,
1407
+ params.attachmentFileName
1408
+ ),
1409
+ quotedRef: mergeQuotedRef(existing?.quotedRef, normalizedQuotedRef),
1410
+ senderId: mergeStringField(existing?.senderId, params.senderId),
1411
+ senderName: mergeStringField(existing?.senderName, params.senderName),
1412
+ mentions: mergeMentions(existing?.mentions, params.mentions),
1413
+ chatType: params.chatType || existing?.chatType,
1414
+ quotedMessageId: mergeStringField(existing?.quotedMessageId, params.quotedMessageId),
1415
+ media: mergeMedia(existing?.media, params.media),
1416
+ delivery: mergeDelivery(existing?.delivery, params.delivery)
1417
+ };
1418
+ state.updatedAt = nowMs;
1419
+ writeState(params, normalizeState(state, nowMs).state);
1420
+ return canonicalMsgId;
1545
1421
  }
1546
- function formatDingTalkConnectionErrorLog(scope, err, baseMessage) {
1547
- if (!err || typeof err !== "object") {
1422
+ function upsertInboundMessageContext(params) {
1423
+ return upsertRecord({
1424
+ ...params,
1425
+ direction: "inbound",
1426
+ topic: params.topic ?? null,
1427
+ msgId: params.msgId,
1428
+ cleanupCreatedAtTtlDays: params.cleanupCreatedAtTtlDays
1429
+ }) || params.msgId;
1430
+ }
1431
+ function upsertOutboundMessageContext(params) {
1432
+ return upsertRecord({
1433
+ ...params,
1434
+ direction: "outbound",
1435
+ topic: params.topic ?? null
1436
+ });
1437
+ }
1438
+ function resolveByMsgId(params) {
1439
+ const nowMs = params.nowMs ?? Date.now();
1440
+ const state = loadState(params, nowMs);
1441
+ const direct = state.records[params.msgId];
1442
+ if (direct && !isRecordExpired(direct, nowMs)) {
1443
+ return direct;
1444
+ }
1445
+ const aliasTarget = state.byAlias[buildAliasKey("inboundMsgId", params.msgId)];
1446
+ if (!aliasTarget) {
1548
1447
  return null;
1549
1448
  }
1550
- const errRecord = err;
1551
- const stage = typeof errRecord.dingtalkConnectionStage === "string" ? errRecord.dingtalkConnectionStage : scope;
1552
- const endpoint = typeof errRecord.dingtalkConnectionEndpoint === "string" ? errRecord.dingtalkConnectionEndpoint : void 0;
1553
- const hasResponse = "response" in errRecord && errRecord.response !== null && errRecord.response !== void 0;
1554
- if (!hasResponse && !endpoint && stage === scope) {
1449
+ const record = state.records[aliasTarget];
1450
+ return record && !isRecordExpired(record, nowMs) ? record : null;
1451
+ }
1452
+ function resolveByAlias(params) {
1453
+ const nowMs = params.nowMs ?? Date.now();
1454
+ const state = loadState(params, nowMs);
1455
+ const msgId = state.byAlias[buildAliasKey(params.kind, params.value)];
1456
+ if (!msgId) {
1555
1457
  return null;
1556
1458
  }
1557
- const parts = [`${baseMessage} [DingTalk][ConnectionError][${stage}]`];
1558
- if (endpoint) {
1559
- parts.push(`endpoint=${endpoint}`);
1459
+ const record = state.records[msgId];
1460
+ return record && !isRecordExpired(record, nowMs) ? record : null;
1461
+ }
1462
+ function resolveByQuotedRef(params) {
1463
+ const nowMs = params.nowMs ?? Date.now();
1464
+ const quotedRef = normalizeQuotedRef(params.quotedRef);
1465
+ const log = params.log;
1466
+ if (!quotedRef) {
1467
+ log?.debug?.(
1468
+ `[DingTalk][QuotedRef] Resolve skipped: invalid quotedRef accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
1469
+ );
1470
+ return null;
1560
1471
  }
1561
- const response = err.response;
1562
- if (response) {
1563
- if (response.status !== void 0 && response.status !== null) {
1564
- const statusText = typeof response.status === "string" || typeof response.status === "number" || typeof response.status === "boolean" || typeof response.status === "bigint" ? String(response.status) : JSON.stringify(response.status);
1565
- parts.push(`status=${statusText}`);
1566
- }
1567
- let requestId = getHeaderCaseInsensitive(response.headers, "x-acs-dingtalk-request-id");
1568
- if (!requestId && response.data && typeof response.data === "object" && !Array.isArray(response.data)) {
1569
- const data = response.data;
1570
- if (typeof data.requestId === "string") {
1571
- requestId = data.requestId;
1572
- } else if (typeof data.requestid === "string") {
1573
- requestId = data.requestid;
1574
- }
1575
- }
1576
- if (requestId) {
1577
- parts.push(`requestId=${requestId}`);
1472
+ if (quotedRef.targetDirection === "inbound") {
1473
+ if (quotedRef.key !== "msgId" || !quotedRef.value) {
1474
+ log?.debug?.(
1475
+ `[DingTalk][QuotedRef] Resolve skipped: inbound quotedRef missing msgId accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
1476
+ );
1477
+ return null;
1578
1478
  }
1579
- if (response.data !== void 0) {
1580
- parts.push(formatDingTalkErrorPayload(response.data));
1479
+ const record = resolveByMsgId({
1480
+ ...params,
1481
+ msgId: quotedRef.value,
1482
+ nowMs
1483
+ });
1484
+ log?.debug?.(
1485
+ `[DingTalk][QuotedRef] Resolve inbound by msgId=${quotedRef.value} hit=${record ? "yes" : "no"} accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
1486
+ );
1487
+ return record;
1488
+ }
1489
+ if (quotedRef.key && quotedRef.value && quotedRef.key !== "msgId") {
1490
+ const aliasRecord = resolveByAlias({
1491
+ ...params,
1492
+ kind: quotedRef.key,
1493
+ value: quotedRef.value,
1494
+ nowMs
1495
+ });
1496
+ if (aliasRecord) {
1497
+ log?.debug?.(
1498
+ `[DingTalk][QuotedRef] Resolve outbound by ${quotedRef.key}=${quotedRef.value} hit=yes accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
1499
+ );
1500
+ return aliasRecord;
1581
1501
  }
1502
+ log?.debug?.(
1503
+ `[DingTalk][QuotedRef] Resolve outbound by ${quotedRef.key}=${quotedRef.value} hit=no accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
1504
+ );
1582
1505
  }
1583
- if (stage === "connect.websocket") {
1584
- parts.push("Likely websocket/proxy/WSS issue after connections/open succeeded");
1506
+ if (typeof quotedRef.fallbackCreatedAt === "number" && Number.isFinite(quotedRef.fallbackCreatedAt)) {
1507
+ const record = resolveByCreatedAtWindow({
1508
+ ...params,
1509
+ createdAt: quotedRef.fallbackCreatedAt,
1510
+ direction: "outbound",
1511
+ nowMs
1512
+ });
1513
+ log?.debug?.(
1514
+ `[DingTalk][QuotedRef] Resolve outbound by createdAt=${quotedRef.fallbackCreatedAt} hit=${record ? "yes" : "no"} accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
1515
+ );
1516
+ return record;
1585
1517
  }
1586
- parts.push("See docs/connection-troubleshooting.md or run scripts/dingtalk-connection-check.*");
1587
- return parts.join(" ");
1518
+ log?.debug?.(
1519
+ `[DingTalk][QuotedRef] Resolve outbound missed without createdAt fallback accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
1520
+ );
1521
+ return null;
1588
1522
  }
1589
- function cleanupOrphanedTempFiles(log) {
1590
- const tempDir = os2.tmpdir();
1591
- const dingtalkPattern = /^dingtalk_\d+\..+$/;
1592
- let cleaned = 0;
1593
- try {
1594
- const files = fs2.readdirSync(tempDir);
1595
- const now = Date.now();
1596
- const maxAge = 24 * 60 * 60 * 1e3;
1597
- for (const file of files) {
1598
- if (!dingtalkPattern.test(file)) {
1599
- continue;
1600
- }
1601
- const filePath = path3.join(tempDir, file);
1602
- try {
1603
- const stats = fs2.statSync(filePath);
1604
- if (now - stats.mtime.getTime() > maxAge) {
1605
- fs2.unlinkSync(filePath);
1606
- cleaned++;
1607
- log?.debug?.(`[DingTalk] Cleaned up orphaned temp file: ${file}`);
1608
- }
1609
- } catch (err) {
1610
- log?.debug?.(`[DingTalk] Failed to cleanup temp file ${file}: ${getErrorMessage(err)}`);
1611
- }
1523
+ function resolveByCreatedAtWindow(params) {
1524
+ const nowMs = params.nowMs ?? Date.now();
1525
+ const windowMs = params.windowMs ?? DEFAULT_CREATED_AT_MATCH_WINDOW_MS;
1526
+ const state = loadState(params, nowMs);
1527
+ let bestRecord = null;
1528
+ let bestDelta = Infinity;
1529
+ for (const msgId of state.recentByCreatedAt) {
1530
+ const record = state.records[msgId];
1531
+ if (!record || isRecordExpired(record, nowMs)) {
1532
+ continue;
1612
1533
  }
1613
- if (cleaned > 0) {
1614
- log?.info?.(`[DingTalk] Cleaned up ${cleaned} orphaned temp files`);
1534
+ if (params.direction && record.direction !== params.direction) {
1535
+ continue;
1615
1536
  }
1616
- } catch (err) {
1617
- log?.debug?.(`[DingTalk] Failed to cleanup temp directory: ${getErrorMessage(err)}`);
1618
- }
1619
- return cleaned;
1620
- }
1621
- async function retryWithBackoff(fn, options = {}) {
1622
- const { maxRetries = 3, baseDelayMs = 100, log } = options;
1623
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
1624
- try {
1625
- return await fn();
1626
- } catch (err) {
1627
- const statusCode = err.response?.status;
1628
- const isRetryable = statusCode === 401 || statusCode === 429 || statusCode && statusCode >= 500;
1629
- const responseData = getErrorResponseData(err);
1630
- if (responseData !== void 0) {
1631
- log?.debug?.(formatDingTalkErrorPayloadLog("retry.beforeDecision", responseData));
1632
- }
1633
- if (!isRetryable || attempt === maxRetries) {
1634
- throw err;
1635
- }
1636
- const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
1637
- log?.debug?.(`[DingTalk] Retry attempt ${attempt}/${maxRetries} after ${delayMs}ms`);
1638
- await new Promise((resolve3) => setTimeout(resolve3, delayMs));
1537
+ const delta = Math.abs(record.createdAt - params.createdAt);
1538
+ if (delta <= windowMs && delta < bestDelta) {
1539
+ bestDelta = delta;
1540
+ bestRecord = record;
1639
1541
  }
1640
1542
  }
1641
- throw new Error("Retry exhausted without returning");
1642
- }
1643
- function getCurrentTimestamp() {
1644
- return Date.now();
1543
+ return bestRecord;
1645
1544
  }
1646
1545
 
1546
+ // src/config-schema.ts
1547
+ var AckReactionSchema = z2.union([
1548
+ z2.literal(""),
1549
+ z2.enum(["off", "emoji", "kaomoji"]),
1550
+ z2.string().min(1)
1551
+ ]);
1552
+ var CardStreamingModeSchema = z2.enum(["off", "answer", "all"]);
1553
+ var ContextVisibilitySchema = z2.enum(["all", "allowlist", "allowlist_quote"]);
1554
+ var DingTalkAccountConfigShape = {
1555
+ /** Account name (optional display name) */
1556
+ name: z2.string().optional(),
1557
+ /** Enable or disable this DingTalk channel/account without deleting saved credentials. */
1558
+ enabled: z2.boolean().optional().default(true),
1559
+ /** DingTalk App Key (Client ID) used to authenticate API and Stream connections. */
1560
+ clientId: z2.string().optional(),
1561
+ /** DingTalk App Secret (Client Secret) used to obtain DingTalk access tokens. */
1562
+ clientSecret: buildSecretInputSchema().optional(),
1563
+ /** Direct-message access policy: open, pairing, or allowlist. */
1564
+ dmPolicy: z2.enum(["open", "pairing", "allowlist"]).optional().default("open"),
1565
+ /** Group-message access policy: open, allowlist, or disabled. */
1566
+ groupPolicy: z2.enum(["open", "allowlist", "disabled"]).optional().default("open"),
1567
+ /** User IDs allowed when `dmPolicy` is `allowlist`. */
1568
+ allowFrom: z2.array(z2.string()).optional(),
1569
+ /** Sender IDs allowed when `groupPolicy` is `allowlist`. */
1570
+ groupAllowFrom: z2.array(z2.string()).optional(),
1571
+ /** Default disabled. Enabling `all` allows learned displayName lookup but may misroute on stale or duplicate names and is available to all callers until upstream exposes requester authz context. */
1572
+ displayNameResolution: z2.enum(["disabled", "all"]).optional().default("disabled"),
1573
+ /** Controls how much supplemental host context remains visible to the reply runtime. `allowlist_quote` is the safest advanced mode when only explicit quotes or replies should remain visible. */
1574
+ contextVisibility: ContextVisibilitySchema.optional(),
1575
+ /** Allowed remote media download hosts, IPs, or CIDRs for media fetches. */
1576
+ mediaUrlAllowlist: z2.array(z2.string()).optional(),
1577
+ /** Native acknowledgement reaction mode: off, emoji, kaomoji, or a custom compatibility string. */
1578
+ ackReaction: AckReactionSchema.optional(),
1579
+ /** Retention window in days for short-lived message context used by quoting and media recovery. */
1580
+ journalTTLDays: z2.number().int().min(1).optional().default(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
1581
+ /** Enable verbose DingTalk channel debug logging. */
1582
+ debug: z2.boolean().optional().default(false),
1583
+ /** Default reply delivery mode: markdown or card. */
1584
+ messageType: z2.enum(["markdown", "card"]).optional().default("markdown"),
1585
+ /** Deprecated and ignored. AI card replies now always use the built-in DingTalk template contract. Keep only for backward-compatible config parsing. */
1586
+ cardTemplateId: z2.string().optional(),
1587
+ /** Deprecated and ignored. The built-in AI card contract owns the streaming field mapping. Keep only for backward-compatible config parsing. */
1588
+ cardTemplateKey: z2.string().optional().default("content"),
1589
+ /** Per-group overrides keyed by conversationId. Supports `*` as a wildcard fallback. */
1590
+ groups: z2.record(
1591
+ z2.string(),
1592
+ z2.object({
1593
+ /** Additional system prompt appended for this group. */
1594
+ systemPrompt: z2.string().optional(),
1595
+ /** Require an explicit @mention before the bot answers in this group. */
1596
+ requireMention: z2.boolean().optional(),
1597
+ /** Optional per-group sender allowlist for tighter access control than the channel default. */
1598
+ groupAllowFrom: z2.array(z2.string()).optional()
1599
+ })
1600
+ ).optional(),
1601
+ /** Connection robustness configuration */
1602
+ /** Maximum connection attempts in a single reconnect cycle before backing off or giving up. */
1603
+ maxConnectionAttempts: z2.number().int().min(1).optional().default(10),
1604
+ /** Initial reconnect backoff delay in milliseconds. */
1605
+ initialReconnectDelay: z2.number().int().min(100).optional().default(1e3),
1606
+ /** Upper bound for reconnect backoff delay in milliseconds. */
1607
+ maxReconnectDelay: z2.number().int().min(1e3).optional().default(6e4),
1608
+ /** Randomization factor added to reconnect backoff to avoid synchronized reconnect storms. */
1609
+ reconnectJitter: z2.number().min(0).max(1).optional().default(0.3),
1610
+ /** Maximum reconnect cycles before the channel stops retrying and waits for the next lifecycle restart. */
1611
+ maxReconnectCycles: z2.number().int().min(1).optional().default(10),
1612
+ /** Time limit in milliseconds for one reconnect cycle before starting a fresh cycle. */
1613
+ reconnectDeadlineMs: z2.number().int().min(5e3).optional().default(5e4),
1614
+ /** Enable the plugin connection manager. Disable only when you intentionally rely on DWClient native keepAlive plus autoReconnect behavior. */
1615
+ useConnectionManager: z2.boolean().optional().default(true),
1616
+ /** Maximum inbound media size in MB accepted by the plugin. When omitted, the runtime default is used. */
1617
+ mediaMaxMb: z2.number().int().min(1).optional(),
1618
+ /** Enable the underlying Stream client heartbeat. When omitted, runtime derives a default from `useConnectionManager`. */
1619
+ keepAlive: z2.boolean().optional(),
1620
+ /** Bypass global or system HTTP(S) proxy settings for DingTalk send, upload, and card APIs. */
1621
+ bypassProxyForSend: z2.boolean().optional().default(false),
1622
+ /** Controls the proactive-send permission reminder shown when a conversation has not granted send rights yet. */
1623
+ proactivePermissionHint: z2.object({
1624
+ /** Show the proactive-send permission hint when the runtime detects missing DingTalk proactive permission. */
1625
+ enabled: z2.boolean().optional().default(true),
1626
+ /** Minimum cooldown in hours before the same proactive permission hint can be shown again. */
1627
+ cooldownHours: z2.number().int().min(1).max(24 * 30).optional().default(24)
1628
+ }).optional().default({ enabled: true, cooldownHours: 24 }),
1629
+ /** Deprecated compatibility flag. When true and `cardStreamingMode` is unset, runtime resolves to `cardStreamingMode: "all"`. Do not use in new configs. */
1630
+ cardRealTimeStream: z2.boolean().optional(),
1631
+ /** Card streaming mode:
1632
+ * - off: disable incremental streaming
1633
+ * - answer: stream answer text
1634
+ * - all: stream answer + reasoning or thinking text */
1635
+ cardStreamingMode: CardStreamingModeSchema.optional(),
1636
+ /** Throttle interval in milliseconds between AI card streaming updates. */
1637
+ cardStreamInterval: z2.number().int().min(200).optional().default(1e3),
1638
+ /** Cooldown window in milliseconds after AI card trigger errors. Replies fall back to non-card delivery during this period. */
1639
+ aicardDegradeMs: z2.number().int().min(6e4).optional().default(30 * 60 * 1e3),
1640
+ /** Enable the local feedback-learning loop for notes, reflections, and command-assisted learning. */
1641
+ learningEnabled: z2.boolean().optional(),
1642
+ /** Automatically apply generated learning output into session notes or global rules when available. */
1643
+ learningAutoApply: z2.boolean().optional(),
1644
+ /** Retention window in milliseconds for temporary learning notes. */
1645
+ learningNoteTtlMs: z2.number().int().min(6e4).optional(),
1646
+ /** Convert markdown tables to plain text before sending when you want more consistent DingTalk rendering. */
1647
+ convertMarkdownTables: z2.boolean().optional().default(true),
1648
+ /** @mention the sender after card finalization in group chats.
1649
+ * Set to a non-empty string (e.g. "✅ 回复完成") to enable — the value is used as the message text.
1650
+ * Leave empty or omit to disable. */
1651
+ cardAtSender: z2.string().optional(),
1652
+ /** Status line visibility toggles for the AI card footer. */
1653
+ cardStatusLine: z2.object({
1654
+ /** Show model name. */
1655
+ model: z2.boolean().optional().default(true),
1656
+ /** Show thinking effort level. */
1657
+ effort: z2.boolean().optional().default(true),
1658
+ /** Show agent display name. */
1659
+ agent: z2.boolean().optional().default(true),
1660
+ /** Show task elapsed time. */
1661
+ taskTime: z2.boolean().optional().default(false),
1662
+ /** Show token usage summary (input/output/cache). */
1663
+ tokens: z2.boolean().optional().default(false),
1664
+ /** Show DingTalk API call count. */
1665
+ dapiUsage: z2.boolean().optional().default(false)
1666
+ }).optional().default({ model: true, effort: true, agent: true, taskTime: false, tokens: false, dapiUsage: false })
1667
+ };
1668
+ var DingTalkAccountConfigSchema = z2.object(DingTalkAccountConfigShape);
1669
+ var DingTalkConfigSchema = DingTalkAccountConfigSchema.extend({
1670
+ /** Multi-account configuration */
1671
+ accounts: z2.record(z2.string(), DingTalkAccountConfigSchema.optional()).optional()
1672
+ });
1673
+
1674
+ // src/gateway/channel-gateway.ts
1675
+ import { DWClient, TOPIC_CARD, TOPIC_ROBOT } from "dingtalk-stream";
1676
+
1647
1677
  // src/card-callback-service.ts
1648
1678
  var DINGTALK_API = "https://api.dingtalk.com";
1649
1679
  function stringifyCandidate(value) {
@@ -1904,36 +1934,6 @@ import { randomUUID } from "node:crypto";
1904
1934
  import * as fs3 from "node:fs";
1905
1935
  import * as path4 from "node:path";
1906
1936
 
1907
- // src/auth.ts
1908
- var accessTokenCache = /* @__PURE__ */ new Map();
1909
- async function getAccessToken(config, log) {
1910
- const cacheKey = config.clientId;
1911
- const now = Date.now();
1912
- const cached = accessTokenCache.get(cacheKey);
1913
- if (cached && cached.expiry > now + 6e4) {
1914
- return cached.accessToken;
1915
- }
1916
- const runtimeConfig = await resolveRuntimeConfig(config, log);
1917
- const token = await retryWithBackoff(
1918
- async () => {
1919
- const response = await http_client_default.post(
1920
- "https://api.dingtalk.com/v1.0/oauth2/accessToken",
1921
- {
1922
- appKey: runtimeConfig.clientId,
1923
- appSecret: runtimeConfig.clientSecret
1924
- }
1925
- );
1926
- accessTokenCache.set(cacheKey, {
1927
- accessToken: response.data.accessToken,
1928
- expiry: now + response.data.expireIn * 1e3
1929
- });
1930
- return response.data.accessToken;
1931
- },
1932
- { maxRetries: 3, log }
1933
- );
1934
- return token;
1935
- }
1936
-
1937
1937
  // src/card/card-template.ts
1938
1938
  var STOP_ACTION_VISIBLE = true;
1939
1939
  var BUILTIN_DINGTALK_CARD_TEMPLATE_ID = process.env.DINGTALK_CARD_TEMPLATE_ID || "675cde2f-f526-40cb-b828-f5b2b57b8b77.schema";
@@ -14365,7 +14365,7 @@ async function listDocs(config, spaceId, parentId, log = getLogger()) {
14365
14365
 
14366
14366
  // index.ts
14367
14367
  function registerDingTalkDocsGatewayMethods(api) {
14368
- api.registerGatewayMethod("dingtalk.docs.create", async ({ respond, params }) => {
14368
+ const createHandler = async ({ respond, params }) => {
14369
14369
  const accountId = readStringParam2(params, "accountId");
14370
14370
  const spaceId = readStringParam2(params, "spaceId", { required: true });
14371
14371
  const title = readStringParam2(params, "title", { required: true });
@@ -14394,31 +14394,187 @@ function registerDingTalkDocsGatewayMethods(api) {
14394
14394
  }
14395
14395
  throw error;
14396
14396
  }
14397
- });
14398
- api.registerGatewayMethod("dingtalk.docs.append", async ({ respond, params }) => {
14397
+ };
14398
+ const appendHandler = async ({ respond, params }) => {
14399
14399
  const accountId = readStringParam2(params, "accountId");
14400
14400
  const docId = readStringParam2(params, "docId", { required: true });
14401
14401
  const content = readStringParam2(params, "content", { required: true, allowEmpty: false });
14402
14402
  const config = getConfig(api.config, accountId ?? void 0);
14403
14403
  const result = await appendToDoc(config, docId, content, api.logger);
14404
14404
  return respond(true, result);
14405
- });
14406
- api.registerGatewayMethod("dingtalk.docs.search", async ({ respond, params }) => {
14405
+ };
14406
+ const searchHandler = async ({ respond, params }) => {
14407
14407
  const accountId = readStringParam2(params, "accountId");
14408
14408
  const keyword = readStringParam2(params, "keyword", { required: true });
14409
14409
  const spaceId = readStringParam2(params, "spaceId");
14410
14410
  const config = getConfig(api.config, accountId ?? void 0);
14411
14411
  const docs = await searchDocs(config, keyword, spaceId, api.logger);
14412
14412
  return respond(true, { docs });
14413
- });
14414
- api.registerGatewayMethod("dingtalk.docs.list", async ({ respond, params }) => {
14413
+ };
14414
+ const listHandler = async ({ respond, params }) => {
14415
14415
  const accountId = readStringParam2(params, "accountId");
14416
14416
  const spaceId = readStringParam2(params, "spaceId", { required: true });
14417
14417
  const parentId = readStringParam2(params, "parentId");
14418
14418
  const config = getConfig(api.config, accountId ?? void 0);
14419
14419
  const docs = await listDocs(config, spaceId, parentId, api.logger);
14420
14420
  return respond(true, { docs });
14421
- });
14421
+ };
14422
+ api.registerGatewayMethod("dingtalk.docs.create", createHandler);
14423
+ api.registerGatewayMethod("dingtalk.docs.append", appendHandler);
14424
+ api.registerGatewayMethod("dingtalk.docs.search", searchHandler);
14425
+ api.registerGatewayMethod("dingtalk.docs.list", listHandler);
14426
+ api.registerGatewayMethod("dingtalk-connector.docs.create", createHandler);
14427
+ api.registerGatewayMethod("dingtalk-connector.docs.append", appendHandler);
14428
+ api.registerGatewayMethod("dingtalk-connector.docs.search", searchHandler);
14429
+ api.registerGatewayMethod("dingtalk-connector.docs.list", listHandler);
14430
+ }
14431
+ function getContentParam(params) {
14432
+ return readStringParam2(params, "content") ?? readStringParam2(params, "message");
14433
+ }
14434
+ function getErrorMessage2(error, fallback) {
14435
+ return error instanceof Error ? error.message : fallback;
14436
+ }
14437
+ function maskClientId(clientId) {
14438
+ if (!clientId) {
14439
+ return null;
14440
+ }
14441
+ return clientId.length <= 4 ? "****" : `****${clientId.slice(-4)}`;
14442
+ }
14443
+ function isConnectorSendTarget(target) {
14444
+ return /^(user|group):\S+$/.test(target);
14445
+ }
14446
+ async function sendGatewayMessage(params) {
14447
+ const config = getConfig(params.api.config, params.accountId);
14448
+ const accountId = params.accountId ?? "default";
14449
+ if (!config.clientId || !config.clientSecret) {
14450
+ return params.respond(false, { error: "DingTalk not configured" });
14451
+ }
14452
+ let result;
14453
+ try {
14454
+ result = await sendMessage(config, params.target, params.content, {
14455
+ log: params.api.logger,
14456
+ accountId,
14457
+ conversationId: params.target,
14458
+ storePath: params.storePath,
14459
+ forceMarkdown: params.useAICard === false
14460
+ });
14461
+ } catch (error) {
14462
+ const message = getErrorMessage2(error, "send failed");
14463
+ params.api.logger?.warn?.(`[DingTalk][GatewayRPC] send failed: ${message}`);
14464
+ return params.respond(false, { error: message });
14465
+ }
14466
+ return params.respond(
14467
+ result.ok,
14468
+ result.ok ? {
14469
+ ok: true,
14470
+ target: params.target,
14471
+ messageId: result.messageId ?? null,
14472
+ tracking: result.tracking ?? null
14473
+ } : { error: result.error || "send failed" }
14474
+ );
14475
+ }
14476
+ function registerDingTalkConnectorCompatibilityGatewayMethods(api) {
14477
+ api.registerGatewayMethod(
14478
+ "dingtalk-connector.sendToUser",
14479
+ async ({ context, respond, params }) => {
14480
+ const accountId = readStringParam2(params, "accountId");
14481
+ const userId = readStringParam2(params, "userId", { required: true });
14482
+ const content = getContentParam(params);
14483
+ if (!content) {
14484
+ return respond(false, { error: "content or message is required" });
14485
+ }
14486
+ return sendGatewayMessage({
14487
+ api,
14488
+ respond,
14489
+ accountId: accountId ?? void 0,
14490
+ target: `user:${userId}`,
14491
+ content,
14492
+ storePath: context?.cronStorePath,
14493
+ useAICard: params.useAICard
14494
+ });
14495
+ }
14496
+ );
14497
+ api.registerGatewayMethod(
14498
+ "dingtalk-connector.sendToGroup",
14499
+ async ({ context, respond, params }) => {
14500
+ const accountId = readStringParam2(params, "accountId");
14501
+ const openConversationId = readStringParam2(params, "openConversationId", { required: true });
14502
+ const content = getContentParam(params);
14503
+ if (!content) {
14504
+ return respond(false, { error: "content or message is required" });
14505
+ }
14506
+ return sendGatewayMessage({
14507
+ api,
14508
+ respond,
14509
+ accountId: accountId ?? void 0,
14510
+ target: `group:${openConversationId}`,
14511
+ content,
14512
+ storePath: context?.cronStorePath,
14513
+ useAICard: params.useAICard
14514
+ });
14515
+ }
14516
+ );
14517
+ api.registerGatewayMethod(
14518
+ "dingtalk-connector.send",
14519
+ async ({ context, respond, params }) => {
14520
+ const accountId = readStringParam2(params, "accountId");
14521
+ const target = readStringParam2(params, "target", { required: true });
14522
+ const content = getContentParam(params);
14523
+ if (!content) {
14524
+ return respond(false, { error: "content or message is required" });
14525
+ }
14526
+ if (!isConnectorSendTarget(target)) {
14527
+ return respond(false, { error: "target must start with user: or group:" });
14528
+ }
14529
+ return sendGatewayMessage({
14530
+ api,
14531
+ respond,
14532
+ accountId: accountId ?? void 0,
14533
+ target,
14534
+ content,
14535
+ storePath: context?.cronStorePath,
14536
+ useAICard: params.useAICard
14537
+ });
14538
+ }
14539
+ );
14540
+ api.registerGatewayMethod(
14541
+ "dingtalk-connector.status",
14542
+ async ({ respond }) => {
14543
+ const accountIds = listDingTalkAccountIds(api.config);
14544
+ const accounts = accountIds.length > 0 ? accountIds : ["default"];
14545
+ return respond(true, {
14546
+ channel: "dingtalk",
14547
+ accounts: accounts.map((accountId) => {
14548
+ const account = resolveDingTalkAccount(api.config, accountId);
14549
+ return {
14550
+ accountId,
14551
+ configured: account.configured,
14552
+ enabled: account.enabled !== false,
14553
+ name: account.name ?? null,
14554
+ clientId: maskClientId(account.clientId)
14555
+ };
14556
+ })
14557
+ });
14558
+ }
14559
+ );
14560
+ api.registerGatewayMethod(
14561
+ "dingtalk-connector.probe",
14562
+ async ({ respond, params }) => {
14563
+ const accountId = readStringParam2(params, "accountId");
14564
+ const config = getConfig(api.config, accountId ?? void 0);
14565
+ if (!config.clientId || !config.clientSecret) {
14566
+ return respond(false, { error: "DingTalk not configured" });
14567
+ }
14568
+ try {
14569
+ await getAccessToken(config, api.logger);
14570
+ return respond(true, { ok: true, clientId: maskClientId(config.clientId) });
14571
+ } catch (error) {
14572
+ const message = getErrorMessage2(error, "probe failed");
14573
+ api.logger?.warn?.(`[DingTalk][GatewayRPC] probe failed: ${message}`);
14574
+ return respond(false, { error: message });
14575
+ }
14576
+ }
14577
+ );
14422
14578
  }
14423
14579
  var index_default = defineChannelPluginEntry({
14424
14580
  id: "dingtalk",
@@ -14428,11 +14584,15 @@ var index_default = defineChannelPluginEntry({
14428
14584
  setRuntime: setDingTalkRuntime,
14429
14585
  registerFull(api) {
14430
14586
  registerDingTalkDocsGatewayMethods(api);
14431
- api.on("llm_output", (event) => {
14432
- if (event.usage) {
14433
- accumulateUsage(event.runId, event.usage);
14587
+ registerDingTalkConnectorCompatibilityGatewayMethods(api);
14588
+ api.on(
14589
+ "llm_output",
14590
+ (event) => {
14591
+ if (event.usage) {
14592
+ accumulateUsage(event.runId, event.usage);
14593
+ }
14434
14594
  }
14435
- });
14595
+ );
14436
14596
  }
14437
14597
  });
14438
14598
  export {