@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.d.ts.map +1 -1
- package/dist/index.js +1283 -1123
- package/dist/index.js.map +4 -4
- package/index.ts +234 -16
- package/package.json +1 -1
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/
|
|
414
|
-
import
|
|
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/
|
|
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
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
|
434
|
-
|
|
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
|
|
437
|
-
|
|
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
|
|
454
|
-
|
|
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
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
|
479
|
-
|
|
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
|
-
|
|
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
|
-
|
|
520
|
-
|
|
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
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
|
537
|
-
|
|
538
|
-
|
|
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
|
|
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
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
return void 0;
|
|
530
|
+
function closePluginDebugLog(params) {
|
|
531
|
+
if (!params.storePath) {
|
|
532
|
+
return;
|
|
546
533
|
}
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
|
557
|
-
|
|
558
|
-
|
|
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 (
|
|
572
|
-
return
|
|
550
|
+
if (typeof data !== "object") {
|
|
551
|
+
return data;
|
|
573
552
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
569
|
+
maskObj(masked);
|
|
570
|
+
return masked;
|
|
598
571
|
}
|
|
599
|
-
function
|
|
600
|
-
if (
|
|
601
|
-
return
|
|
572
|
+
function stringifyUnknown(value) {
|
|
573
|
+
if (typeof value === "string") {
|
|
574
|
+
return value;
|
|
602
575
|
}
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
|
647
|
-
|
|
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 (
|
|
659
|
-
|
|
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 (
|
|
662
|
-
|
|
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
|
-
|
|
665
|
-
|
|
608
|
+
return void 0;
|
|
609
|
+
}
|
|
610
|
+
function getErrorMessage(err) {
|
|
611
|
+
if (err instanceof Error && err.message) {
|
|
612
|
+
return err.message;
|
|
666
613
|
}
|
|
667
|
-
if (
|
|
668
|
-
|
|
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
|
|
620
|
+
return stringifyUnknown(err);
|
|
671
621
|
}
|
|
672
|
-
function
|
|
673
|
-
|
|
622
|
+
function getErrorResponseData(err) {
|
|
623
|
+
if (!err || typeof err !== "object") {
|
|
624
|
+
return void 0;
|
|
625
|
+
}
|
|
626
|
+
return err.response?.data;
|
|
674
627
|
}
|
|
675
|
-
function
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
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
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
730
|
-
|
|
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
|
-
|
|
739
|
-
|
|
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
|
|
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
|
|
759
|
-
|
|
760
|
-
return existing;
|
|
761
|
-
}
|
|
762
|
-
return next;
|
|
668
|
+
function getProxyBypassOption(config) {
|
|
669
|
+
return config?.bypassProxyForSend ? { proxy: false } : {};
|
|
763
670
|
}
|
|
764
|
-
function
|
|
765
|
-
|
|
766
|
-
return existing;
|
|
767
|
-
}
|
|
768
|
-
return next;
|
|
671
|
+
function createResolve4FallbackLookup(log, accountId) {
|
|
672
|
+
return createResolve4FallbackLookupWithDeps(log, accountId, dns, net);
|
|
769
673
|
}
|
|
770
|
-
function
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
|
785
|
-
if (typeof
|
|
786
|
-
return
|
|
719
|
+
function getHeaderCaseInsensitive(headers, key) {
|
|
720
|
+
if (!headers || typeof headers !== "object" || Array.isArray(headers)) {
|
|
721
|
+
return void 0;
|
|
787
722
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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 (
|
|
801
|
-
return
|
|
732
|
+
if (value === null || value === void 0) {
|
|
733
|
+
return void 0;
|
|
802
734
|
}
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
815
|
-
return
|
|
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
|
|
829
|
-
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
|
845
|
-
if (
|
|
846
|
-
|
|
755
|
+
const parts = [`${baseMessage} [DingTalk][ConnectionError][${stage}]`];
|
|
756
|
+
if (endpoint) {
|
|
757
|
+
parts.push(`endpoint=${endpoint}`);
|
|
847
758
|
}
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
|
|
859
|
-
if (!
|
|
860
|
-
|
|
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
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
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
|
-
|
|
784
|
+
parts.push("See docs/connection-troubleshooting.md or run scripts/dingtalk-connection-check.*");
|
|
785
|
+
return parts.join(" ");
|
|
874
786
|
}
|
|
875
|
-
function
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
|
905
|
-
|
|
906
|
-
{
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
870
|
+
{ maxRetries: 3, log }
|
|
912
871
|
);
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
|
958
|
-
return
|
|
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
|
|
967
|
-
return
|
|
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
|
|
974
|
-
|
|
975
|
-
|
|
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
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
|
|
985
|
-
return record && !isRecordExpired(record, nowMs) ? record : null;
|
|
916
|
+
return `.${segments.join(".")}`;
|
|
986
917
|
}
|
|
987
|
-
function
|
|
988
|
-
const
|
|
989
|
-
const
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
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
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
1038
|
-
|
|
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
|
|
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
|
|
1059
|
-
const
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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/
|
|
1082
|
-
var
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
var
|
|
1088
|
-
var
|
|
1089
|
-
var
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
|
1239
|
-
return
|
|
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
|
|
1242
|
-
|
|
1001
|
+
function asRecord(value) {
|
|
1002
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
return value;
|
|
1243
1006
|
}
|
|
1244
|
-
function
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
|
1254
|
-
|
|
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
|
|
1257
|
-
return
|
|
1046
|
+
function normalizeAttachmentTextSource(value) {
|
|
1047
|
+
return value === "text" || value === "html" || value === "pdf" || value === "docx" ? value : void 0;
|
|
1258
1048
|
}
|
|
1259
|
-
function
|
|
1260
|
-
|
|
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
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
if (existing) {
|
|
1266
|
-
return existing;
|
|
1064
|
+
function normalizeMentions(value) {
|
|
1065
|
+
if (!Array.isArray(value)) {
|
|
1066
|
+
return void 0;
|
|
1267
1067
|
}
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
|
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
|
-
|
|
1203
|
+
writeNamespaceJsonAtomic(MESSAGE_CONTEXT_NAMESPACE, {
|
|
1337
1204
|
storePath: params.storePath,
|
|
1338
|
-
accountId: params.accountId
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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
|
|
1375
|
-
if (typeof
|
|
1376
|
-
return
|
|
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
|
|
1389
|
-
if (typeof
|
|
1390
|
-
return
|
|
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
|
|
1233
|
+
return next;
|
|
1411
1234
|
}
|
|
1412
|
-
function
|
|
1413
|
-
if (
|
|
1414
|
-
return
|
|
1235
|
+
function mergeQuotedRef(existing, next) {
|
|
1236
|
+
if (!existing) {
|
|
1237
|
+
return next;
|
|
1415
1238
|
}
|
|
1416
|
-
if (
|
|
1417
|
-
|
|
1418
|
-
if (typeof record.message === "string" && record.message.trim()) {
|
|
1419
|
-
return record.message;
|
|
1420
|
-
}
|
|
1239
|
+
if (!next) {
|
|
1240
|
+
return existing;
|
|
1421
1241
|
}
|
|
1422
|
-
return
|
|
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
|
|
1425
|
-
if (
|
|
1426
|
-
return
|
|
1249
|
+
function mergeStringField(existing, next) {
|
|
1250
|
+
if (typeof next !== "string" || !next.trim()) {
|
|
1251
|
+
return existing;
|
|
1427
1252
|
}
|
|
1428
|
-
return
|
|
1253
|
+
return next.trim();
|
|
1429
1254
|
}
|
|
1430
|
-
function
|
|
1431
|
-
if (
|
|
1432
|
-
return
|
|
1255
|
+
function mergeMentions(existing, next) {
|
|
1256
|
+
if (!next) {
|
|
1257
|
+
return existing;
|
|
1433
1258
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
-
|
|
1446
|
-
|
|
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
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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 (
|
|
1462
|
-
|
|
1279
|
+
if (!next) {
|
|
1280
|
+
return existing;
|
|
1463
1281
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
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
|
|
1468
|
-
return
|
|
1290
|
+
function mergeAttachmentTextSource(existing, next) {
|
|
1291
|
+
return next ?? existing;
|
|
1469
1292
|
}
|
|
1470
|
-
function
|
|
1471
|
-
return
|
|
1293
|
+
function mergeAttachmentTextTruncated(existing, next) {
|
|
1294
|
+
return next === void 0 ? existing : next;
|
|
1472
1295
|
}
|
|
1473
|
-
function
|
|
1474
|
-
|
|
1296
|
+
function mergeAttachmentFileName(existing, next) {
|
|
1297
|
+
if (typeof next !== "string") {
|
|
1298
|
+
return existing;
|
|
1299
|
+
}
|
|
1300
|
+
return next;
|
|
1475
1301
|
}
|
|
1476
|
-
function
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
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
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
|
1522
|
-
if (
|
|
1334
|
+
function computeExpiresAt(nowMs, ttlMs, ttlReferenceMs) {
|
|
1335
|
+
if (typeof ttlMs !== "number" || !Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
1523
1336
|
return void 0;
|
|
1524
1337
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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
|
|
1531
|
-
|
|
1532
|
-
|
|
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
|
-
|
|
1535
|
-
|
|
1351
|
+
const removed = Object.keys(state.records).length - Object.keys(nextRecords).length;
|
|
1352
|
+
if (removed === 0) {
|
|
1353
|
+
return { state, removed: 0 };
|
|
1536
1354
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
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
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|
|
1547
|
-
|
|
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
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
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
|
|
1558
|
-
|
|
1559
|
-
|
|
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
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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
|
-
|
|
1580
|
-
|
|
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 (
|
|
1584
|
-
|
|
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
|
-
|
|
1587
|
-
|
|
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
|
|
1590
|
-
const
|
|
1591
|
-
const
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
const
|
|
1597
|
-
|
|
1598
|
-
|
|
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 (
|
|
1614
|
-
|
|
1534
|
+
if (params.direction && record.direction !== params.direction) {
|
|
1535
|
+
continue;
|
|
1615
1536
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
14432
|
-
|
|
14433
|
-
|
|
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 {
|