@soimy/dingtalk 3.6.2-beta.1 → 3.6.3
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 +1550 -1220
- package/dist/index.js.map +4 -4
- package/dist/src/card-draft-controller.d.ts.map +1 -1
- package/dist/src/card-service.d.ts.map +1 -1
- package/dist/src/message-utils.d.ts +10 -0
- package/dist/src/message-utils.d.ts.map +1 -1
- package/dist/src/reply-strategy-card.d.ts.map +1 -1
- package/dist/src/reply-strategy-markdown.d.ts.map +1 -1
- package/dist/src/reply-strategy-types.d.ts +2 -0
- package/dist/src/reply-strategy-types.d.ts.map +1 -1
- package/dist/src/send-service.d.ts.map +1 -1
- package/index.ts +234 -16
- package/package.json +1 -1
- package/src/card-callback-service.ts +1 -1
- package/src/card-draft-controller.ts +4 -3
- package/src/card-service.ts +28 -4
- package/src/message-utils.ts +103 -7
- package/src/reply-strategy-card.ts +12 -3
- package/src/reply-strategy-markdown.ts +139 -20
- package/src/reply-strategy-types.ts +3 -0
- package/src/send-service.ts +67 -65
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 };
|
|
1020
|
+
}
|
|
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
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
function normalizeAttachmentTextSource(value) {
|
|
1047
|
+
return value === "text" || value === "html" || value === "pdf" || value === "docx" ? value : void 0;
|
|
1048
|
+
}
|
|
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 };
|
|
1063
|
+
}
|
|
1064
|
+
function normalizeMentions(value) {
|
|
1065
|
+
if (!Array.isArray(value)) {
|
|
1066
|
+
return void 0;
|
|
1067
|
+
}
|
|
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)
|
|
1109
|
+
};
|
|
1252
1110
|
}
|
|
1253
|
-
function
|
|
1254
|
-
return
|
|
1111
|
+
function buildAliasKey(kind, value) {
|
|
1112
|
+
return `${kind}:${value.trim()}`;
|
|
1255
1113
|
}
|
|
1256
|
-
function
|
|
1257
|
-
|
|
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;
|
|
1258
1136
|
}
|
|
1259
|
-
function
|
|
1260
|
-
return
|
|
1137
|
+
function isRecordExpired(record, nowMs) {
|
|
1138
|
+
return typeof record.expiresAt === "number" && Number.isFinite(record.expiresAt) && nowMs >= record.expiresAt;
|
|
1261
1139
|
}
|
|
1262
|
-
function
|
|
1263
|
-
const
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
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;
|
|
1267
1146
|
}
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
pluginDebugWriters.set(key, created);
|
|
1274
|
-
return created;
|
|
1275
|
-
}
|
|
1276
|
-
function resolvePluginDebugLog(params) {
|
|
1277
|
-
const baseLog = params.baseLog;
|
|
1278
|
-
const fsImpl = params.fsImpl ?? fs2;
|
|
1279
|
-
const scopeKey = params.storePath ? buildPluginDebugScopeKey({ storePath: params.storePath, accountId: params.accountId }) : void 0;
|
|
1280
|
-
if (scopeKey) {
|
|
1281
|
-
closedPluginDebugScopes.delete(scopeKey);
|
|
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
|
}
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
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
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
function mergeText(existing, next) {
|
|
1224
|
+
if (typeof next !== "string") {
|
|
1225
|
+
return existing;
|
|
1346
1226
|
}
|
|
1227
|
+
return next;
|
|
1347
1228
|
}
|
|
1348
|
-
function
|
|
1349
|
-
if (
|
|
1350
|
-
return
|
|
1229
|
+
function mergeAttachmentText(existing, next) {
|
|
1230
|
+
if (typeof next !== "string") {
|
|
1231
|
+
return existing;
|
|
1351
1232
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1233
|
+
return next;
|
|
1234
|
+
}
|
|
1235
|
+
function mergeQuotedRef(existing, next) {
|
|
1236
|
+
if (!existing) {
|
|
1237
|
+
return next;
|
|
1354
1238
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
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
|
-
}
|
|
1239
|
+
if (!next) {
|
|
1240
|
+
return existing;
|
|
1370
1241
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
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
|
+
};
|
|
1373
1248
|
}
|
|
1374
|
-
function
|
|
1375
|
-
if (typeof
|
|
1376
|
-
return
|
|
1249
|
+
function mergeStringField(existing, next) {
|
|
1250
|
+
if (typeof next !== "string" || !next.trim()) {
|
|
1251
|
+
return existing;
|
|
1377
1252
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1253
|
+
return next.trim();
|
|
1254
|
+
}
|
|
1255
|
+
function mergeMentions(existing, next) {
|
|
1256
|
+
if (!next) {
|
|
1257
|
+
return existing;
|
|
1258
|
+
}
|
|
1259
|
+
return normalizeMentions(next) || existing;
|
|
1260
|
+
}
|
|
1261
|
+
function mergeMedia(existing, next) {
|
|
1262
|
+
if (!existing) {
|
|
1263
|
+
return next;
|
|
1264
|
+
}
|
|
1265
|
+
if (!next) {
|
|
1266
|
+
return existing;
|
|
1267
|
+
}
|
|
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;
|
|
1380
1278
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
return serialized ?? String(value);
|
|
1384
|
-
} catch {
|
|
1385
|
-
return "[unserializable]";
|
|
1279
|
+
if (!next) {
|
|
1280
|
+
return existing;
|
|
1386
1281
|
}
|
|
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
|
+
};
|
|
1387
1289
|
}
|
|
1388
|
-
function
|
|
1389
|
-
|
|
1390
|
-
|
|
1290
|
+
function mergeAttachmentTextSource(existing, next) {
|
|
1291
|
+
return next ?? existing;
|
|
1292
|
+
}
|
|
1293
|
+
function mergeAttachmentTextTruncated(existing, next) {
|
|
1294
|
+
return next === void 0 ? existing : next;
|
|
1295
|
+
}
|
|
1296
|
+
function mergeAttachmentFileName(existing, next) {
|
|
1297
|
+
if (typeof next !== "string") {
|
|
1298
|
+
return existing;
|
|
1391
1299
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1300
|
+
return next;
|
|
1301
|
+
}
|
|
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;
|
|
1398
1307
|
}
|
|
1308
|
+
}
|
|
1309
|
+
const delivery = params.delivery;
|
|
1310
|
+
if (!delivery) {
|
|
1399
1311
|
return void 0;
|
|
1400
1312
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
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;
|
|
1405
1322
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1323
|
+
const aliasHit = state.byAlias[buildAliasKey(kind, value)];
|
|
1324
|
+
if (!aliasHit) {
|
|
1325
|
+
continue;
|
|
1408
1326
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
function getErrorMessage(err) {
|
|
1413
|
-
if (err instanceof Error && err.message) {
|
|
1414
|
-
return err.message;
|
|
1415
|
-
}
|
|
1416
|
-
if (err && typeof err === "object") {
|
|
1417
|
-
const record = err;
|
|
1418
|
-
if (typeof record.message === "string" && record.message.trim()) {
|
|
1419
|
-
return record.message;
|
|
1327
|
+
const record = state.records[aliasHit];
|
|
1328
|
+
if (record && !isRecordExpired(record, nowMs)) {
|
|
1329
|
+
return aliasHit;
|
|
1420
1330
|
}
|
|
1421
1331
|
}
|
|
1422
|
-
return
|
|
1332
|
+
return void 0;
|
|
1423
1333
|
}
|
|
1424
|
-
function
|
|
1425
|
-
if (
|
|
1334
|
+
function computeExpiresAt(nowMs, ttlMs, ttlReferenceMs) {
|
|
1335
|
+
if (typeof ttlMs !== "number" || !Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
1426
1336
|
return void 0;
|
|
1427
1337
|
}
|
|
1428
|
-
return
|
|
1338
|
+
return (typeof ttlReferenceMs === "number" && Number.isFinite(ttlReferenceMs) ? ttlReferenceMs : nowMs) + ttlMs;
|
|
1429
1339
|
}
|
|
1430
|
-
function
|
|
1431
|
-
if (
|
|
1432
|
-
return
|
|
1340
|
+
function pruneStateByCreatedAt(state, ttlDays, nowMs) {
|
|
1341
|
+
if (!ttlDays || ttlDays <= 0) {
|
|
1342
|
+
return { state, removed: 0 };
|
|
1433
1343
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
code = String(obj.code);
|
|
1440
|
-
}
|
|
1441
|
-
if (typeof obj.message === "string") {
|
|
1442
|
-
message = obj.message;
|
|
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;
|
|
1443
1349
|
}
|
|
1444
1350
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
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
|
-
}
|
|
1351
|
+
const removed = Object.keys(state.records).length - Object.keys(nextRecords).length;
|
|
1352
|
+
if (removed === 0) {
|
|
1353
|
+
return { state, removed: 0 };
|
|
1456
1354
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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;
|
|
1460
1368
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
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()) {
|
|
1380
|
+
return void 0;
|
|
1463
1381
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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;
|
|
1472
1421
|
}
|
|
1473
|
-
function
|
|
1474
|
-
return
|
|
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;
|
|
1475
1430
|
}
|
|
1476
|
-
function
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
callback(null, [{ address: hostname, family: ipFamily }], ipFamily);
|
|
1483
|
-
return;
|
|
1484
|
-
}
|
|
1485
|
-
callback(null, hostname, ipFamily);
|
|
1486
|
-
return;
|
|
1487
|
-
}
|
|
1488
|
-
dnsImpl.lookup(hostname, options, (lookupErr, address, family) => {
|
|
1489
|
-
if (!lookupErr) {
|
|
1490
|
-
callback(null, address, family);
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
if (lookupErr.code !== "ENOTFOUND") {
|
|
1494
|
-
callback(lookupErr, address, family);
|
|
1495
|
-
return;
|
|
1496
|
-
}
|
|
1497
|
-
dnsImpl.resolve4(hostname, (resolveErr, addresses) => {
|
|
1498
|
-
if (resolveErr || !addresses || addresses.length === 0) {
|
|
1499
|
-
callback(lookupErr, address, family);
|
|
1500
|
-
return;
|
|
1501
|
-
}
|
|
1502
|
-
if (!fallbackLogged) {
|
|
1503
|
-
fallbackLogged = true;
|
|
1504
|
-
log?.warn?.(
|
|
1505
|
-
`[${accountId ?? "default"}] System DNS lookup failed for ${hostname} (ENOTFOUND); using resolve4 fallback ${addresses[0]}`
|
|
1506
|
-
);
|
|
1507
|
-
}
|
|
1508
|
-
if (options.all) {
|
|
1509
|
-
callback(
|
|
1510
|
-
null,
|
|
1511
|
-
addresses.map((item) => ({ address: item, family: 4 })),
|
|
1512
|
-
4
|
|
1513
|
-
);
|
|
1514
|
-
return;
|
|
1515
|
-
}
|
|
1516
|
-
callback(null, addresses[0], 4);
|
|
1517
|
-
});
|
|
1518
|
-
});
|
|
1519
|
-
};
|
|
1431
|
+
function upsertOutboundMessageContext(params) {
|
|
1432
|
+
return upsertRecord({
|
|
1433
|
+
...params,
|
|
1434
|
+
direction: "outbound",
|
|
1435
|
+
topic: params.topic ?? null
|
|
1436
|
+
});
|
|
1520
1437
|
}
|
|
1521
|
-
function
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
if (!matched) {
|
|
1528
|
-
return void 0;
|
|
1529
|
-
}
|
|
1530
|
-
const value = matched[1];
|
|
1531
|
-
if (Array.isArray(value)) {
|
|
1532
|
-
return value.length > 0 ? String(value[0]) : void 0;
|
|
1533
|
-
}
|
|
1534
|
-
if (value === null || value === void 0) {
|
|
1535
|
-
return void 0;
|
|
1536
|
-
}
|
|
1537
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
1538
|
-
return String(value);
|
|
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;
|
|
1539
1444
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
return void 0;
|
|
1445
|
+
const aliasTarget = state.byAlias[buildAliasKey("inboundMsgId", params.msgId)];
|
|
1446
|
+
if (!aliasTarget) {
|
|
1447
|
+
return null;
|
|
1544
1448
|
}
|
|
1449
|
+
const record = state.records[aliasTarget];
|
|
1450
|
+
return record && !isRecordExpired(record, nowMs) ? record : null;
|
|
1545
1451
|
}
|
|
1546
|
-
function
|
|
1547
|
-
|
|
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) {
|
|
1548
1457
|
return null;
|
|
1549
1458
|
}
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
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
|
+
);
|
|
1555
1470
|
return null;
|
|
1556
1471
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
if (response.status !== void 0 && response.status !== null) {
|
|
1564
|
-
const statusText = typeof response.status === "string" || typeof response.status === "number" || typeof response.status === "boolean" || typeof response.status === "bigint" ? String(response.status) : JSON.stringify(response.status);
|
|
1565
|
-
parts.push(`status=${statusText}`);
|
|
1566
|
-
}
|
|
1567
|
-
let requestId = getHeaderCaseInsensitive(response.headers, "x-acs-dingtalk-request-id");
|
|
1568
|
-
if (!requestId && response.data && typeof response.data === "object" && !Array.isArray(response.data)) {
|
|
1569
|
-
const data = response.data;
|
|
1570
|
-
if (typeof data.requestId === "string") {
|
|
1571
|
-
requestId = data.requestId;
|
|
1572
|
-
} else if (typeof data.requestid === "string") {
|
|
1573
|
-
requestId = data.requestid;
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
if (requestId) {
|
|
1577
|
-
parts.push(`requestId=${requestId}`);
|
|
1472
|
+
if (quotedRef.targetDirection === "inbound") {
|
|
1473
|
+
if (quotedRef.key !== "msgId" || !quotedRef.value) {
|
|
1474
|
+
log?.debug?.(
|
|
1475
|
+
`[DingTalk][QuotedRef] Resolve skipped: inbound quotedRef missing msgId accountId=${params.accountId} conversationId=${params.conversationId || "(none)"}`
|
|
1476
|
+
);
|
|
1477
|
+
return null;
|
|
1578
1478
|
}
|
|
1579
|
-
|
|
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) {
|
|
@@ -1784,7 +1814,7 @@ async function updateCardVariables(outTrackId, params, token, config) {
|
|
|
1784
1814
|
{
|
|
1785
1815
|
outTrackId,
|
|
1786
1816
|
cardData: { cardParamMap: stringMap },
|
|
1787
|
-
cardUpdateOptions: { updateCardDataByKey: true
|
|
1817
|
+
cardUpdateOptions: { updateCardDataByKey: true }
|
|
1788
1818
|
},
|
|
1789
1819
|
{
|
|
1790
1820
|
headers: {
|
|
@@ -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";
|
|
@@ -2614,6 +2614,19 @@ async function createAICard(config, conversationId, log, options = {}) {
|
|
|
2614
2614
|
upsertPendingCard(aiCardInstance, options.storePath, log);
|
|
2615
2615
|
}
|
|
2616
2616
|
clearAICardDegrade(accountId, log);
|
|
2617
|
+
try {
|
|
2618
|
+
await putAICardStreamingField(aiCardInstance, template.contentKey, "", false, log, {
|
|
2619
|
+
suppressDegrade: true
|
|
2620
|
+
});
|
|
2621
|
+
aiCardInstance.state = AICardStatus.INPUTING;
|
|
2622
|
+
if (shouldPersistPending) {
|
|
2623
|
+
upsertPendingCard(aiCardInstance, options.storePath, log);
|
|
2624
|
+
}
|
|
2625
|
+
} catch (kickErr) {
|
|
2626
|
+
log?.debug?.(
|
|
2627
|
+
`[DingTalk][AICard] Non-critical: failed to kick card into streaming mode: ${kickErr.message}`
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2617
2630
|
return aiCardInstance;
|
|
2618
2631
|
} catch (err) {
|
|
2619
2632
|
log?.error?.(`[DingTalk][AICard] Create failed: ${err.message}`);
|
|
@@ -2712,13 +2725,13 @@ async function clearAICardStreamingContent(card, log) {
|
|
|
2712
2725
|
log?.debug?.(`[DingTalk][AICard] Non-critical: failed to clear streaming content: ${message}`);
|
|
2713
2726
|
}
|
|
2714
2727
|
}
|
|
2715
|
-
async function finalizeAICardStreamingLifecycleIfNeeded(card,
|
|
2728
|
+
async function finalizeAICardStreamingLifecycleIfNeeded(card, log) {
|
|
2716
2729
|
if (!card.streamLifecycleOpened) {
|
|
2717
2730
|
return;
|
|
2718
2731
|
}
|
|
2719
2732
|
const template = DINGTALK_CARD_TEMPLATE;
|
|
2720
2733
|
try {
|
|
2721
|
-
await putAICardStreamingField(card, template.streamingKey,
|
|
2734
|
+
await putAICardStreamingField(card, template.streamingKey, "", true, log, {
|
|
2722
2735
|
suppressDegrade: true
|
|
2723
2736
|
});
|
|
2724
2737
|
} catch (err) {
|
|
@@ -2734,7 +2747,7 @@ async function commitAICardBlocks(card, options, log) {
|
|
|
2734
2747
|
return;
|
|
2735
2748
|
}
|
|
2736
2749
|
await ensureFreshToken(card, log);
|
|
2737
|
-
await finalizeAICardStreamingLifecycleIfNeeded(card,
|
|
2750
|
+
await finalizeAICardStreamingLifecycleIfNeeded(card, log);
|
|
2738
2751
|
const template = DINGTALK_CARD_TEMPLATE;
|
|
2739
2752
|
const updates = {
|
|
2740
2753
|
[template.blockListKey]: options.blockListJson,
|
|
@@ -2797,7 +2810,7 @@ async function finalizeStoppedAICard(card, options, log) {
|
|
|
2797
2810
|
await ensureFreshToken(card, log);
|
|
2798
2811
|
const template = DINGTALK_CARD_TEMPLATE;
|
|
2799
2812
|
const payload = buildStoppedCardFinalizePayload(options);
|
|
2800
|
-
await finalizeAICardStreamingLifecycleIfNeeded(card,
|
|
2813
|
+
await finalizeAICardStreamingLifecycleIfNeeded(card, log);
|
|
2801
2814
|
try {
|
|
2802
2815
|
await updateCardVariables(
|
|
2803
2816
|
card.outTrackId || card.cardInstanceId,
|
|
@@ -4475,7 +4488,7 @@ function stripLeadingInvisibleChars(value) {
|
|
|
4475
4488
|
|
|
4476
4489
|
// src/inbound-handler.ts
|
|
4477
4490
|
import fs6 from "node:fs";
|
|
4478
|
-
import * as
|
|
4491
|
+
import * as path10 from "node:path";
|
|
4479
4492
|
import { isAbortRequestText, isBtwRequestText } from "openclaw/plugin-sdk/reply-runtime";
|
|
4480
4493
|
import { parseInlineDirectives } from "openclaw/plugin-sdk/text-runtime";
|
|
4481
4494
|
|
|
@@ -6716,7 +6729,7 @@ function isMarkdownTableSeparator(line) {
|
|
|
6716
6729
|
return false;
|
|
6717
6730
|
}
|
|
6718
6731
|
const cells = normalized.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
|
|
6719
|
-
return cells.length > 0 && cells.every((cell) => /^:?-{
|
|
6732
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{1,}:?$/.test(cell));
|
|
6720
6733
|
}
|
|
6721
6734
|
function isMarkdownTableRow(line) {
|
|
6722
6735
|
const trimmed = line.trim();
|
|
@@ -6725,9 +6738,58 @@ function isMarkdownTableRow(line) {
|
|
|
6725
6738
|
function parseMarkdownTableRow(line) {
|
|
6726
6739
|
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
|
|
6727
6740
|
}
|
|
6728
|
-
function
|
|
6729
|
-
const
|
|
6730
|
-
|
|
6741
|
+
function parseSeparatorAlignment(cell) {
|
|
6742
|
+
const trimmed = cell.trim();
|
|
6743
|
+
const hasLeftColon = trimmed.startsWith(":");
|
|
6744
|
+
const hasRightColon = trimmed.endsWith(":");
|
|
6745
|
+
if (hasLeftColon && hasRightColon) {
|
|
6746
|
+
return "center";
|
|
6747
|
+
}
|
|
6748
|
+
if (hasRightColon) {
|
|
6749
|
+
return "right";
|
|
6750
|
+
}
|
|
6751
|
+
if (hasLeftColon) {
|
|
6752
|
+
return "left";
|
|
6753
|
+
}
|
|
6754
|
+
return "center";
|
|
6755
|
+
}
|
|
6756
|
+
function buildSeparatorRow(alignments) {
|
|
6757
|
+
const cells = alignments.map((align) => {
|
|
6758
|
+
if (align === "left") {
|
|
6759
|
+
return ":---";
|
|
6760
|
+
}
|
|
6761
|
+
if (align === "right") {
|
|
6762
|
+
return "---:";
|
|
6763
|
+
}
|
|
6764
|
+
return ":---:";
|
|
6765
|
+
});
|
|
6766
|
+
return "|" + cells.join("|") + "|";
|
|
6767
|
+
}
|
|
6768
|
+
function renderMarkdownTable(headerLine, separatorLine, dataLines) {
|
|
6769
|
+
const headerCells = parseMarkdownTableRow(headerLine);
|
|
6770
|
+
const separatorCells = parseMarkdownTableRow(separatorLine);
|
|
6771
|
+
const dataRows = dataLines.map(parseMarkdownTableRow).filter((cells) => cells.length > 0);
|
|
6772
|
+
if (headerCells.length === 0) {
|
|
6773
|
+
return "";
|
|
6774
|
+
}
|
|
6775
|
+
const colCount = Math.max(
|
|
6776
|
+
headerCells.length,
|
|
6777
|
+
separatorCells.length,
|
|
6778
|
+
...dataRows.map((cells) => cells.length)
|
|
6779
|
+
);
|
|
6780
|
+
const alignments = [];
|
|
6781
|
+
for (let i = 0; i < colCount; i++) {
|
|
6782
|
+
const sepCell = separatorCells[i] || "";
|
|
6783
|
+
alignments.push(parseSeparatorAlignment(sepCell));
|
|
6784
|
+
}
|
|
6785
|
+
const separator = buildSeparatorRow(alignments);
|
|
6786
|
+
const allRows = [headerCells, ...dataRows];
|
|
6787
|
+
const rendered = allRows.map((cells) => {
|
|
6788
|
+
const padded = cells.length < colCount ? [...cells, ...Array(colCount - cells.length).fill("")] : cells;
|
|
6789
|
+
return "|" + padded.join("|") + "|";
|
|
6790
|
+
});
|
|
6791
|
+
rendered.splice(1, 0, separator);
|
|
6792
|
+
return rendered.join("\n");
|
|
6731
6793
|
}
|
|
6732
6794
|
function convertMarkdownTablesToPlainText(text) {
|
|
6733
6795
|
const lines = text.split("\n");
|
|
@@ -6743,13 +6805,20 @@ function convertMarkdownTablesToPlainText(text) {
|
|
|
6743
6805
|
continue;
|
|
6744
6806
|
}
|
|
6745
6807
|
if (!inCodeFence && index + 1 < lines.length && isMarkdownTableRow(line) && isMarkdownTableSeparator(lines[index + 1] || "")) {
|
|
6746
|
-
const
|
|
6808
|
+
const headerLine = line;
|
|
6809
|
+
const separatorLine = lines[index + 1] || "";
|
|
6810
|
+
const dataLines = [];
|
|
6747
6811
|
index += 2;
|
|
6748
6812
|
while (index < lines.length && isMarkdownTableRow(lines[index] || "")) {
|
|
6749
|
-
|
|
6813
|
+
dataLines.push(lines[index] || "");
|
|
6750
6814
|
index += 1;
|
|
6751
6815
|
}
|
|
6752
|
-
|
|
6816
|
+
const renderedTable = renderMarkdownTable(headerLine, separatorLine, dataLines);
|
|
6817
|
+
const lastOutput = output[output.length - 1];
|
|
6818
|
+
if (lastOutput !== void 0 && lastOutput.trim() !== "") {
|
|
6819
|
+
output.push("");
|
|
6820
|
+
}
|
|
6821
|
+
output.push(renderedTable);
|
|
6753
6822
|
continue;
|
|
6754
6823
|
}
|
|
6755
6824
|
output.push(line);
|
|
@@ -7247,6 +7316,9 @@ function buildPersistedOutboundText(text, options) {
|
|
|
7247
7316
|
}
|
|
7248
7317
|
return text;
|
|
7249
7318
|
}
|
|
7319
|
+
function shouldRouteSessionMediaViaProactive(mediaType) {
|
|
7320
|
+
return mediaType === "voice" || mediaType === "video" || mediaType === "file";
|
|
7321
|
+
}
|
|
7250
7322
|
var DINGTALK_TEXT_CHUNK_LIMIT = 3800;
|
|
7251
7323
|
var CARD_MEDIA_CONTROLLER_ATTACH_WAIT_MS = 150;
|
|
7252
7324
|
var CARD_MEDIA_CONTROLLER_ATTACH_POLL_MS = 25;
|
|
@@ -7643,51 +7715,33 @@ async function sendBySession(config, sessionWebhook, text, options = {}) {
|
|
|
7643
7715
|
const token = await getAccessToken(config, options.log);
|
|
7644
7716
|
const log = options.log || getLogger();
|
|
7645
7717
|
if (options.mediaPath && options.mediaType) {
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
const durationMs = uploadedDurationMs ?? await getVoiceDurationMs(options.mediaPath, options.mediaType, log, { preReadBuffer: buffer });
|
|
7656
|
-
body = { msgtype: "voice", voice: { media_id: mediaId, duration: String(durationMs) } };
|
|
7657
|
-
log?.debug?.(
|
|
7658
|
-
`[DingTalk] Sending session voice message mediaId=${mediaId} durationMs=${durationMs}`
|
|
7659
|
-
);
|
|
7660
|
-
} else if (options.mediaType === "video") {
|
|
7661
|
-
body = { msgtype: "video", video: { media_id: mediaId } };
|
|
7662
|
-
} else if (options.mediaType === "file") {
|
|
7663
|
-
body = { msgtype: "file", file: { media_id: mediaId } };
|
|
7664
|
-
}
|
|
7665
|
-
if (body) {
|
|
7666
|
-
const result = await http_client_default({
|
|
7667
|
-
url: sessionWebhook,
|
|
7668
|
-
method: "POST",
|
|
7669
|
-
data: body,
|
|
7670
|
-
headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
|
|
7671
|
-
...getProxyBypassOption(config)
|
|
7672
|
-
});
|
|
7718
|
+
if (options.mediaType === "image") {
|
|
7719
|
+
const uploadResult = await uploadMedia2(config, options.mediaPath, options.mediaType, log, {
|
|
7720
|
+
mediaLocalRoots: options.mediaLocalRoots
|
|
7721
|
+
});
|
|
7722
|
+
if (uploadResult) {
|
|
7723
|
+
const imageMarkdown = ``;
|
|
7724
|
+
text = text ? `${text}
|
|
7725
|
+
|
|
7726
|
+
${imageMarkdown}` : imageMarkdown;
|
|
7673
7727
|
log?.debug?.(
|
|
7674
|
-
`[DingTalk] Session webhook
|
|
7728
|
+
`[DingTalk] Session webhook image will be delivered as markdown media reference mediaId=${uploadResult.mediaId}`
|
|
7675
7729
|
);
|
|
7676
|
-
|
|
7677
|
-
const
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
}
|
|
7683
|
-
return result.data;
|
|
7730
|
+
} else {
|
|
7731
|
+
const mediaHint = options.mediaUrl || options.mediaPath || options.filePath || "(\u5A92\u4F53\u53D1\u9001\u5931\u8D25)";
|
|
7732
|
+
text = `${text}
|
|
7733
|
+
|
|
7734
|
+
\u{1F4CE} \u5A92\u4F53\u53D1\u9001\u5931\u8D25\uFF0C\u515C\u5E95\u94FE\u63A5/\u8DEF\u5F84\uFF1A${mediaHint}`.trim();
|
|
7735
|
+
log?.warn?.("[DingTalk] Media upload failed, falling back to text description");
|
|
7684
7736
|
}
|
|
7685
7737
|
} else {
|
|
7686
7738
|
const mediaHint = options.mediaUrl || options.mediaPath || options.filePath || "(\u5A92\u4F53\u53D1\u9001\u5931\u8D25)";
|
|
7687
7739
|
text = `${text}
|
|
7688
7740
|
|
|
7689
|
-
\u{1F4CE} \
|
|
7690
|
-
log?.warn?.(
|
|
7741
|
+
\u{1F4CE} \u5F53\u524D\u4F1A\u8BDD\u65E0\u6CD5\u76F4\u63A5\u53D1\u9001 ${options.mediaType}\uFF0C\u515C\u5E95\u94FE\u63A5/\u8DEF\u5F84\uFF1A${mediaHint}`.trim();
|
|
7742
|
+
log?.warn?.(
|
|
7743
|
+
`[DingTalk] Session webhook does not support native ${options.mediaType} replies; falling back to text description`
|
|
7744
|
+
);
|
|
7691
7745
|
}
|
|
7692
7746
|
}
|
|
7693
7747
|
const textWithUploadedLocalImages = await replaceMarkdownLocalImages({
|
|
@@ -7733,6 +7787,48 @@ async function sendMessage(config, conversationId, text, options = {}) {
|
|
|
7733
7787
|
try {
|
|
7734
7788
|
const messageType = config.messageType || "markdown";
|
|
7735
7789
|
const log = options.log || getLogger();
|
|
7790
|
+
if (options.sessionWebhook && options.mediaPath && shouldRouteSessionMediaViaProactive(options.mediaType)) {
|
|
7791
|
+
log?.debug?.(
|
|
7792
|
+
`[DingTalk] Session webhook does not support ${options.mediaType} replies reliably; using proactive media API instead`
|
|
7793
|
+
);
|
|
7794
|
+
const proactiveMediaResult = await sendProactiveMedia(
|
|
7795
|
+
config,
|
|
7796
|
+
conversationId,
|
|
7797
|
+
options.mediaPath,
|
|
7798
|
+
options.mediaType,
|
|
7799
|
+
options
|
|
7800
|
+
);
|
|
7801
|
+
if (!proactiveMediaResult.ok) {
|
|
7802
|
+
log?.warn?.(
|
|
7803
|
+
`[DingTalk] Proactive ${options.mediaType} reply failed; falling back to session markdown: ` + (proactiveMediaResult.error || "unknown")
|
|
7804
|
+
);
|
|
7805
|
+
const data = await sendBySession(config, options.sessionWebhook, text, options);
|
|
7806
|
+
const delivery2 = extractOutboundDeliveryMetadata(data);
|
|
7807
|
+
const messageId2 = delivery2.messageId || delivery2.processQueryKey || delivery2.outTrackId;
|
|
7808
|
+
const persistedText = buildPersistedOutboundText(text, options);
|
|
7809
|
+
persistOutboundMessageContext({
|
|
7810
|
+
storePath: options.storePath,
|
|
7811
|
+
accountId: options.accountId,
|
|
7812
|
+
conversationId: options.conversationId || conversationId,
|
|
7813
|
+
text: persistedText,
|
|
7814
|
+
messageType: "outbound-media",
|
|
7815
|
+
quotedRef: options.quotedRef,
|
|
7816
|
+
log,
|
|
7817
|
+
...DEFAULT_OUTBOUND_SENDER,
|
|
7818
|
+
chatType: inferConversationChatType(options.conversationId || conversationId),
|
|
7819
|
+
delivery: {
|
|
7820
|
+
...delivery2,
|
|
7821
|
+
kind: "session"
|
|
7822
|
+
}
|
|
7823
|
+
});
|
|
7824
|
+
return { ok: true, data, messageId: messageId2 };
|
|
7825
|
+
}
|
|
7826
|
+
return {
|
|
7827
|
+
ok: true,
|
|
7828
|
+
data: proactiveMediaResult.data,
|
|
7829
|
+
messageId: proactiveMediaResult.messageId
|
|
7830
|
+
};
|
|
7831
|
+
}
|
|
7736
7832
|
if (messageType === "card" && options.card && !options.forceMarkdown) {
|
|
7737
7833
|
const card = options.card;
|
|
7738
7834
|
if (isCardInTerminalState(card.state)) {
|
|
@@ -7754,26 +7850,6 @@ async function sendMessage(config, conversationId, text, options = {}) {
|
|
|
7754
7850
|
};
|
|
7755
7851
|
}
|
|
7756
7852
|
}
|
|
7757
|
-
if (options.sessionWebhook && options.mediaPath && options.mediaType === "voice") {
|
|
7758
|
-
log?.debug?.(
|
|
7759
|
-
"[DingTalk] Session webhook does not support voice replies reliably; using proactive media API for this voice response"
|
|
7760
|
-
);
|
|
7761
|
-
const proactiveVoiceResult = await sendProactiveMedia(
|
|
7762
|
-
config,
|
|
7763
|
-
conversationId,
|
|
7764
|
-
options.mediaPath,
|
|
7765
|
-
options.mediaType,
|
|
7766
|
-
options
|
|
7767
|
-
);
|
|
7768
|
-
if (!proactiveVoiceResult.ok) {
|
|
7769
|
-
return { ok: false, error: proactiveVoiceResult.error || "Voice reply send failed" };
|
|
7770
|
-
}
|
|
7771
|
-
return {
|
|
7772
|
-
ok: true,
|
|
7773
|
-
data: proactiveVoiceResult.data,
|
|
7774
|
-
messageId: proactiveVoiceResult.messageId
|
|
7775
|
-
};
|
|
7776
|
-
}
|
|
7777
7853
|
if (options.sessionWebhook) {
|
|
7778
7854
|
const data = await sendBySession(config, options.sessionWebhook, text, options);
|
|
7779
7855
|
const delivery2 = extractOutboundDeliveryMetadata(data);
|
|
@@ -9061,8 +9137,6 @@ function createCardDraftController(params) {
|
|
|
9061
9137
|
if (stopped || failed) {
|
|
9062
9138
|
return;
|
|
9063
9139
|
}
|
|
9064
|
-
await contentLoop.flush();
|
|
9065
|
-
await contentLoop.waitForInFlight();
|
|
9066
9140
|
if (hasStreamingContent) {
|
|
9067
9141
|
await clearStreamingContentFromCard();
|
|
9068
9142
|
}
|
|
@@ -9265,6 +9339,7 @@ function createCardDraftController(params) {
|
|
|
9265
9339
|
discardCurrentAnswer();
|
|
9266
9340
|
} else {
|
|
9267
9341
|
sealCurrentAnswer();
|
|
9342
|
+
queueRender();
|
|
9268
9343
|
}
|
|
9269
9344
|
await beginBoundaryFlush();
|
|
9270
9345
|
return;
|
|
@@ -9482,7 +9557,6 @@ function createCardReplyStrategy(ctx) {
|
|
|
9482
9557
|
};
|
|
9483
9558
|
const { mode, usedDeprecatedCardRealTimeStream } = resolveCardStreamingMode(config);
|
|
9484
9559
|
const streamAnswerLive = mode === "answer" || mode === "all";
|
|
9485
|
-
const renderAnswerBlocksLive = mode === "all";
|
|
9486
9560
|
const streamThinkingLive = mode === "all";
|
|
9487
9561
|
let lifecycleState = "open";
|
|
9488
9562
|
const shouldAcceptAnswerSnapshot = () => lifecycleState === "open";
|
|
@@ -9616,7 +9690,10 @@ function createCardReplyStrategy(ctx) {
|
|
|
9616
9690
|
finalTextForFallback = normalized.answerText;
|
|
9617
9691
|
return;
|
|
9618
9692
|
}
|
|
9619
|
-
await controller.updateAnswer(normalized.answerText
|
|
9693
|
+
await controller.updateAnswer(normalized.answerText, {
|
|
9694
|
+
stream: streamAnswerLive,
|
|
9695
|
+
renderBlocks: !streamAnswerLive
|
|
9696
|
+
});
|
|
9620
9697
|
}
|
|
9621
9698
|
};
|
|
9622
9699
|
const rewriteLocalMarkdownImagesToPlaceholders = (text) => {
|
|
@@ -9652,7 +9729,8 @@ function createCardReplyStrategy(ctx) {
|
|
|
9652
9729
|
}
|
|
9653
9730
|
await controller.updateAnswer(answerSnapshot, {
|
|
9654
9731
|
stream: streamAnswerLive,
|
|
9655
|
-
|
|
9732
|
+
// Active answer previews live in the content field; blockList is committed at boundaries/finalize.
|
|
9733
|
+
renderBlocks: false
|
|
9656
9734
|
});
|
|
9657
9735
|
};
|
|
9658
9736
|
const applySplitTextToTimeline = async (text, options = {}) => {
|
|
@@ -9712,6 +9790,11 @@ function createCardReplyStrategy(ctx) {
|
|
|
9712
9790
|
// Card mode keeps runtime block streaming disabled, but still consumes
|
|
9713
9791
|
// reasoning blocks through explicit callbacks and delivery metadata.
|
|
9714
9792
|
disableBlockStreaming: ctx.disableBlockStreaming ?? true,
|
|
9793
|
+
// DingTalk card mode owns the visible reply surface. In group chats,
|
|
9794
|
+
// OpenClaw defaults source replies to message-tool-only; override that
|
|
9795
|
+
// so final replies are delivered into this card instead of spawning a
|
|
9796
|
+
// separate visible message/card via the message tool.
|
|
9797
|
+
sourceReplyDeliveryMode: "automatic",
|
|
9715
9798
|
onAssistantMessageStart: async () => {
|
|
9716
9799
|
if (isLifecycleSealed() || isStopRequested?.()) {
|
|
9717
9800
|
return;
|
|
@@ -9951,6 +10034,7 @@ function createCardReplyStrategy(ctx) {
|
|
|
9951
10034
|
}
|
|
9952
10035
|
try {
|
|
9953
10036
|
await flushPendingReasoning();
|
|
10037
|
+
await controller.clearStreamingContent?.();
|
|
9954
10038
|
await controller.flush();
|
|
9955
10039
|
await controller.waitForInFlight();
|
|
9956
10040
|
const fallbackAnswer = finalTextForFallback || (sawFinalDelivery ? EMPTY_FINAL_REPLY : void 0);
|
|
@@ -10066,6 +10150,7 @@ function createCardReplyStrategy(ctx) {
|
|
|
10066
10150
|
}
|
|
10067
10151
|
|
|
10068
10152
|
// src/reply-strategy-markdown.ts
|
|
10153
|
+
import path8 from "node:path";
|
|
10069
10154
|
var EMPTY_FINAL_FALLBACK_TEXT = "\u2705 Done";
|
|
10070
10155
|
function renderQuotedSegment(text) {
|
|
10071
10156
|
return text.split("\n").map((line) => line.length > 0 ? `> ${line}` : ">").join("\n");
|
|
@@ -10102,6 +10187,10 @@ function computeSharedPrefixTail(previous, next) {
|
|
|
10102
10187
|
const suffix = current.slice(sharedPrefixLength);
|
|
10103
10188
|
return suffix.trim() ? suffix : "";
|
|
10104
10189
|
}
|
|
10190
|
+
function renderMarkdownImage(mediaPath) {
|
|
10191
|
+
const filename = path8.basename(mediaPath) || "image";
|
|
10192
|
+
return ``;
|
|
10193
|
+
}
|
|
10105
10194
|
function createMarkdownReplyStrategy(ctx) {
|
|
10106
10195
|
let finalText;
|
|
10107
10196
|
let activeAnswerText = "";
|
|
@@ -10125,7 +10214,7 @@ function createMarkdownReplyStrategy(ctx) {
|
|
|
10125
10214
|
}
|
|
10126
10215
|
sentVisibleContent = true;
|
|
10127
10216
|
};
|
|
10128
|
-
const
|
|
10217
|
+
const prepareAnswerSuffix = (text) => {
|
|
10129
10218
|
const current = typeof text === "string" ? text : "";
|
|
10130
10219
|
if (current.length > 0) {
|
|
10131
10220
|
activeAnswerText = current;
|
|
@@ -10133,37 +10222,118 @@ function createMarkdownReplyStrategy(ctx) {
|
|
|
10133
10222
|
}
|
|
10134
10223
|
const suffix = computeIncrementalSuffix(lastSentAnswerText, current);
|
|
10135
10224
|
if (suffix) {
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10225
|
+
return {
|
|
10226
|
+
text: suffix,
|
|
10227
|
+
markSent: () => {
|
|
10228
|
+
lastSentAnswerText = current;
|
|
10229
|
+
}
|
|
10230
|
+
};
|
|
10139
10231
|
}
|
|
10140
10232
|
if (current.trim() && lastSentAnswerText && !current.startsWith(lastSentAnswerText)) {
|
|
10141
10233
|
const suffix2 = computeSharedPrefixTail(lastSentAnswerText, current);
|
|
10142
10234
|
ctx.log?.warn?.(
|
|
10143
10235
|
`[DingTalk][Markdown] answer prefix drift detected; falling back to shared-prefix tail prevLen=${lastSentAnswerText.length} currentLen=${current.length}`
|
|
10144
10236
|
);
|
|
10145
|
-
lastSentAnswerText = "";
|
|
10146
10237
|
if (suffix2) {
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10238
|
+
return {
|
|
10239
|
+
text: suffix2,
|
|
10240
|
+
markSent: () => {
|
|
10241
|
+
lastSentAnswerText = current;
|
|
10242
|
+
}
|
|
10243
|
+
};
|
|
10244
|
+
}
|
|
10245
|
+
return {
|
|
10246
|
+
text: current,
|
|
10247
|
+
markSent: () => {
|
|
10248
|
+
lastSentAnswerText = current;
|
|
10249
|
+
}
|
|
10250
|
+
};
|
|
10251
|
+
}
|
|
10252
|
+
return null;
|
|
10253
|
+
};
|
|
10254
|
+
const emitAnswerSuffix = async (text) => {
|
|
10255
|
+
const suffix = prepareAnswerSuffix(text);
|
|
10256
|
+
if (suffix) {
|
|
10257
|
+
await sendMarkdownSegment(suffix.text);
|
|
10258
|
+
suffix.markSent();
|
|
10259
|
+
}
|
|
10260
|
+
};
|
|
10261
|
+
const prepareMarkdownImageAttachments = async (mediaUrls) => {
|
|
10262
|
+
const imageMarkdown = [];
|
|
10263
|
+
const passthroughMediaUrls = [];
|
|
10264
|
+
const cleanups = [];
|
|
10265
|
+
for (const rawMediaUrl of mediaUrls) {
|
|
10266
|
+
const preparedMedia = await prepareMediaInput(
|
|
10267
|
+
rawMediaUrl,
|
|
10268
|
+
ctx.log,
|
|
10269
|
+
ctx.config.mediaUrlAllowlist
|
|
10270
|
+
);
|
|
10271
|
+
const actualMediaPath = preparedMedia.cleanup ? preparedMedia.path : resolveRelativePath(preparedMedia.path);
|
|
10272
|
+
const mediaType = resolveOutboundMediaType({
|
|
10273
|
+
mediaPath: actualMediaPath,
|
|
10274
|
+
asVoice: false
|
|
10275
|
+
});
|
|
10276
|
+
if (mediaType === "image") {
|
|
10277
|
+
imageMarkdown.push(renderMarkdownImage(actualMediaPath));
|
|
10278
|
+
if (preparedMedia.cleanup) {
|
|
10279
|
+
cleanups.push(preparedMedia.cleanup);
|
|
10280
|
+
}
|
|
10281
|
+
} else {
|
|
10282
|
+
await preparedMedia.cleanup?.();
|
|
10283
|
+
passthroughMediaUrls.push(rawMediaUrl);
|
|
10150
10284
|
}
|
|
10151
|
-
await sendMarkdownSegment(current);
|
|
10152
|
-
lastSentAnswerText = current;
|
|
10153
10285
|
}
|
|
10286
|
+
return { imageMarkdown, passthroughMediaUrls, cleanups };
|
|
10154
10287
|
};
|
|
10155
10288
|
return {
|
|
10156
10289
|
getReplyOptions() {
|
|
10157
10290
|
return {
|
|
10158
|
-
disableBlockStreaming: ctx.disableBlockStreaming === true
|
|
10291
|
+
disableBlockStreaming: ctx.disableBlockStreaming === true,
|
|
10292
|
+
// DingTalk markdown/sessionWebhook mode owns the visible reply surface.
|
|
10293
|
+
// Keep runtime final replies on this strategy even when group chats
|
|
10294
|
+
// default source replies to message-tool-only.
|
|
10295
|
+
sourceReplyDeliveryMode: "automatic"
|
|
10159
10296
|
};
|
|
10160
10297
|
},
|
|
10161
10298
|
async deliver(payload) {
|
|
10299
|
+
let answerTextSentWithImages = false;
|
|
10300
|
+
let toolTextSentWithImages = false;
|
|
10162
10301
|
if (payload.mediaUrls.length > 0) {
|
|
10163
|
-
|
|
10164
|
-
|
|
10302
|
+
const prepared = payload.audioAsVoice === true ? {
|
|
10303
|
+
imageMarkdown: [],
|
|
10304
|
+
passthroughMediaUrls: payload.mediaUrls,
|
|
10305
|
+
cleanups: []
|
|
10306
|
+
} : await prepareMarkdownImageAttachments(payload.mediaUrls);
|
|
10307
|
+
try {
|
|
10308
|
+
if (prepared.passthroughMediaUrls.length > 0) {
|
|
10309
|
+
await ctx.deliverMedia(prepared.passthroughMediaUrls, {
|
|
10310
|
+
audioAsVoice: payload.audioAsVoice
|
|
10311
|
+
});
|
|
10312
|
+
sentVisibleContent = true;
|
|
10313
|
+
}
|
|
10314
|
+
if (prepared.imageMarkdown.length > 0) {
|
|
10315
|
+
const answerSuffix = payload.kind === "block" || payload.kind === "final" ? prepareAnswerSuffix(payload.text) : typeof payload.text === "string" ? { text: renderQuotedSegment(payload.text), markSent: () => {
|
|
10316
|
+
} } : null;
|
|
10317
|
+
const markdownParts = [answerSuffix?.text || "", ...prepared.imageMarkdown].filter(
|
|
10318
|
+
(part) => part.trim().length > 0
|
|
10319
|
+
);
|
|
10320
|
+
if (markdownParts.length > 0) {
|
|
10321
|
+
await sendMarkdownSegment(markdownParts.join("\n\n"));
|
|
10322
|
+
answerSuffix?.markSent();
|
|
10323
|
+
}
|
|
10324
|
+
answerTextSentWithImages = payload.kind === "block" || payload.kind === "final";
|
|
10325
|
+
toolTextSentWithImages = payload.kind === "tool";
|
|
10326
|
+
}
|
|
10327
|
+
} finally {
|
|
10328
|
+
for (const cleanup of prepared.cleanups) {
|
|
10329
|
+
await cleanup();
|
|
10330
|
+
}
|
|
10331
|
+
}
|
|
10165
10332
|
}
|
|
10166
10333
|
if (payload.kind === "tool") {
|
|
10334
|
+
if (toolTextSentWithImages) {
|
|
10335
|
+
return;
|
|
10336
|
+
}
|
|
10167
10337
|
const text = typeof payload.text === "string" ? payload.text : "";
|
|
10168
10338
|
if (!text.trim()) {
|
|
10169
10339
|
return;
|
|
@@ -10171,7 +10341,7 @@ function createMarkdownReplyStrategy(ctx) {
|
|
|
10171
10341
|
await sendMarkdownSegment(renderQuotedSegment(text));
|
|
10172
10342
|
return;
|
|
10173
10343
|
}
|
|
10174
|
-
if ((payload.kind === "block" || payload.kind === "final") && typeof payload.text === "string") {
|
|
10344
|
+
if ((payload.kind === "block" || payload.kind === "final") && typeof payload.text === "string" && !answerTextSentWithImages) {
|
|
10175
10345
|
await emitAnswerSuffix(payload.text);
|
|
10176
10346
|
}
|
|
10177
10347
|
},
|
|
@@ -10495,12 +10665,12 @@ async function dispatchSubAgents(params) {
|
|
|
10495
10665
|
|
|
10496
10666
|
// src/targeting/group-members-store.ts
|
|
10497
10667
|
import * as fs5 from "node:fs";
|
|
10498
|
-
import * as
|
|
10668
|
+
import * as path9 from "node:path";
|
|
10499
10669
|
var GROUP_MEMBERS_NAMESPACE = "members.group-roster";
|
|
10500
10670
|
function groupMembersFilePath(storePath, groupId) {
|
|
10501
|
-
const dir =
|
|
10671
|
+
const dir = path9.join(path9.dirname(storePath), "dingtalk-members");
|
|
10502
10672
|
const safeId = groupId.replace(/\+/g, "-").replace(/\//g, "_");
|
|
10503
|
-
return
|
|
10673
|
+
return path9.join(dir, `${safeId}.json`);
|
|
10504
10674
|
}
|
|
10505
10675
|
function readLegacyRoster(storePath, groupId) {
|
|
10506
10676
|
const filePath = groupMembersFilePath(storePath, groupId);
|
|
@@ -11581,7 +11751,7 @@ async function handleDingTalkMessage(params) {
|
|
|
11581
11751
|
if (!hasPathLikeShape) {
|
|
11582
11752
|
return void 0;
|
|
11583
11753
|
}
|
|
11584
|
-
return STANDALONE_MEDIA_PATH_EXTENSIONS.has(
|
|
11754
|
+
return STANDALONE_MEDIA_PATH_EXTENSIONS.has(path10.extname(trimmed).toLowerCase()) ? trimmed : void 0;
|
|
11585
11755
|
}, extractSharedAudioAsVoice = function(payload, inlineReplyPayload) {
|
|
11586
11756
|
const richPayload = payload;
|
|
11587
11757
|
const sharedValue = parseBooleanLike(richPayload.audioAsVoice);
|
|
@@ -13025,14 +13195,14 @@ var RegistrationError = class extends Error {
|
|
|
13025
13195
|
function asString(value) {
|
|
13026
13196
|
return typeof value === "string" ? value : "";
|
|
13027
13197
|
}
|
|
13028
|
-
async function apiPost(
|
|
13029
|
-
const url = `${REGISTRATION_BASE_URL}${
|
|
13198
|
+
async function apiPost(path11, payload) {
|
|
13199
|
+
const url = `${REGISTRATION_BASE_URL}${path11}`;
|
|
13030
13200
|
const resp = await http_client_default.post(url, payload, { timeout: 15e3 });
|
|
13031
13201
|
const data = resp.data;
|
|
13032
13202
|
const errcode = data.errcode;
|
|
13033
13203
|
if (errcode !== void 0 && errcode !== 0) {
|
|
13034
13204
|
const errmsg = asString(data.errmsg) || "unknown error";
|
|
13035
|
-
throw new RegistrationError(`API error [${
|
|
13205
|
+
throw new RegistrationError(`API error [${path11}]: ${errmsg} (errcode=${typeof errcode === "number" ? errcode : asString(errcode)})`);
|
|
13036
13206
|
}
|
|
13037
13207
|
return data;
|
|
13038
13208
|
}
|
|
@@ -14365,7 +14535,7 @@ async function listDocs(config, spaceId, parentId, log = getLogger()) {
|
|
|
14365
14535
|
|
|
14366
14536
|
// index.ts
|
|
14367
14537
|
function registerDingTalkDocsGatewayMethods(api) {
|
|
14368
|
-
|
|
14538
|
+
const createHandler = async ({ respond, params }) => {
|
|
14369
14539
|
const accountId = readStringParam2(params, "accountId");
|
|
14370
14540
|
const spaceId = readStringParam2(params, "spaceId", { required: true });
|
|
14371
14541
|
const title = readStringParam2(params, "title", { required: true });
|
|
@@ -14394,31 +14564,187 @@ function registerDingTalkDocsGatewayMethods(api) {
|
|
|
14394
14564
|
}
|
|
14395
14565
|
throw error;
|
|
14396
14566
|
}
|
|
14397
|
-
}
|
|
14398
|
-
|
|
14567
|
+
};
|
|
14568
|
+
const appendHandler = async ({ respond, params }) => {
|
|
14399
14569
|
const accountId = readStringParam2(params, "accountId");
|
|
14400
14570
|
const docId = readStringParam2(params, "docId", { required: true });
|
|
14401
14571
|
const content = readStringParam2(params, "content", { required: true, allowEmpty: false });
|
|
14402
14572
|
const config = getConfig(api.config, accountId ?? void 0);
|
|
14403
14573
|
const result = await appendToDoc(config, docId, content, api.logger);
|
|
14404
14574
|
return respond(true, result);
|
|
14405
|
-
}
|
|
14406
|
-
|
|
14575
|
+
};
|
|
14576
|
+
const searchHandler = async ({ respond, params }) => {
|
|
14407
14577
|
const accountId = readStringParam2(params, "accountId");
|
|
14408
14578
|
const keyword = readStringParam2(params, "keyword", { required: true });
|
|
14409
14579
|
const spaceId = readStringParam2(params, "spaceId");
|
|
14410
14580
|
const config = getConfig(api.config, accountId ?? void 0);
|
|
14411
14581
|
const docs = await searchDocs(config, keyword, spaceId, api.logger);
|
|
14412
14582
|
return respond(true, { docs });
|
|
14413
|
-
}
|
|
14414
|
-
|
|
14583
|
+
};
|
|
14584
|
+
const listHandler = async ({ respond, params }) => {
|
|
14415
14585
|
const accountId = readStringParam2(params, "accountId");
|
|
14416
14586
|
const spaceId = readStringParam2(params, "spaceId", { required: true });
|
|
14417
14587
|
const parentId = readStringParam2(params, "parentId");
|
|
14418
14588
|
const config = getConfig(api.config, accountId ?? void 0);
|
|
14419
14589
|
const docs = await listDocs(config, spaceId, parentId, api.logger);
|
|
14420
14590
|
return respond(true, { docs });
|
|
14421
|
-
}
|
|
14591
|
+
};
|
|
14592
|
+
api.registerGatewayMethod("dingtalk.docs.create", createHandler);
|
|
14593
|
+
api.registerGatewayMethod("dingtalk.docs.append", appendHandler);
|
|
14594
|
+
api.registerGatewayMethod("dingtalk.docs.search", searchHandler);
|
|
14595
|
+
api.registerGatewayMethod("dingtalk.docs.list", listHandler);
|
|
14596
|
+
api.registerGatewayMethod("dingtalk-connector.docs.create", createHandler);
|
|
14597
|
+
api.registerGatewayMethod("dingtalk-connector.docs.append", appendHandler);
|
|
14598
|
+
api.registerGatewayMethod("dingtalk-connector.docs.search", searchHandler);
|
|
14599
|
+
api.registerGatewayMethod("dingtalk-connector.docs.list", listHandler);
|
|
14600
|
+
}
|
|
14601
|
+
function getContentParam(params) {
|
|
14602
|
+
return readStringParam2(params, "content") ?? readStringParam2(params, "message");
|
|
14603
|
+
}
|
|
14604
|
+
function getErrorMessage2(error, fallback) {
|
|
14605
|
+
return error instanceof Error ? error.message : fallback;
|
|
14606
|
+
}
|
|
14607
|
+
function maskClientId(clientId) {
|
|
14608
|
+
if (!clientId) {
|
|
14609
|
+
return null;
|
|
14610
|
+
}
|
|
14611
|
+
return clientId.length <= 4 ? "****" : `****${clientId.slice(-4)}`;
|
|
14612
|
+
}
|
|
14613
|
+
function isConnectorSendTarget(target) {
|
|
14614
|
+
return /^(user|group):\S+$/.test(target);
|
|
14615
|
+
}
|
|
14616
|
+
async function sendGatewayMessage(params) {
|
|
14617
|
+
const config = getConfig(params.api.config, params.accountId);
|
|
14618
|
+
const accountId = params.accountId ?? "default";
|
|
14619
|
+
if (!config.clientId || !config.clientSecret) {
|
|
14620
|
+
return params.respond(false, { error: "DingTalk not configured" });
|
|
14621
|
+
}
|
|
14622
|
+
let result;
|
|
14623
|
+
try {
|
|
14624
|
+
result = await sendMessage(config, params.target, params.content, {
|
|
14625
|
+
log: params.api.logger,
|
|
14626
|
+
accountId,
|
|
14627
|
+
conversationId: params.target,
|
|
14628
|
+
storePath: params.storePath,
|
|
14629
|
+
forceMarkdown: params.useAICard === false
|
|
14630
|
+
});
|
|
14631
|
+
} catch (error) {
|
|
14632
|
+
const message = getErrorMessage2(error, "send failed");
|
|
14633
|
+
params.api.logger?.warn?.(`[DingTalk][GatewayRPC] send failed: ${message}`);
|
|
14634
|
+
return params.respond(false, { error: message });
|
|
14635
|
+
}
|
|
14636
|
+
return params.respond(
|
|
14637
|
+
result.ok,
|
|
14638
|
+
result.ok ? {
|
|
14639
|
+
ok: true,
|
|
14640
|
+
target: params.target,
|
|
14641
|
+
messageId: result.messageId ?? null,
|
|
14642
|
+
tracking: result.tracking ?? null
|
|
14643
|
+
} : { error: result.error || "send failed" }
|
|
14644
|
+
);
|
|
14645
|
+
}
|
|
14646
|
+
function registerDingTalkConnectorCompatibilityGatewayMethods(api) {
|
|
14647
|
+
api.registerGatewayMethod(
|
|
14648
|
+
"dingtalk-connector.sendToUser",
|
|
14649
|
+
async ({ context, respond, params }) => {
|
|
14650
|
+
const accountId = readStringParam2(params, "accountId");
|
|
14651
|
+
const userId = readStringParam2(params, "userId", { required: true });
|
|
14652
|
+
const content = getContentParam(params);
|
|
14653
|
+
if (!content) {
|
|
14654
|
+
return respond(false, { error: "content or message is required" });
|
|
14655
|
+
}
|
|
14656
|
+
return sendGatewayMessage({
|
|
14657
|
+
api,
|
|
14658
|
+
respond,
|
|
14659
|
+
accountId: accountId ?? void 0,
|
|
14660
|
+
target: `user:${userId}`,
|
|
14661
|
+
content,
|
|
14662
|
+
storePath: context?.cronStorePath,
|
|
14663
|
+
useAICard: params.useAICard
|
|
14664
|
+
});
|
|
14665
|
+
}
|
|
14666
|
+
);
|
|
14667
|
+
api.registerGatewayMethod(
|
|
14668
|
+
"dingtalk-connector.sendToGroup",
|
|
14669
|
+
async ({ context, respond, params }) => {
|
|
14670
|
+
const accountId = readStringParam2(params, "accountId");
|
|
14671
|
+
const openConversationId = readStringParam2(params, "openConversationId", { required: true });
|
|
14672
|
+
const content = getContentParam(params);
|
|
14673
|
+
if (!content) {
|
|
14674
|
+
return respond(false, { error: "content or message is required" });
|
|
14675
|
+
}
|
|
14676
|
+
return sendGatewayMessage({
|
|
14677
|
+
api,
|
|
14678
|
+
respond,
|
|
14679
|
+
accountId: accountId ?? void 0,
|
|
14680
|
+
target: `group:${openConversationId}`,
|
|
14681
|
+
content,
|
|
14682
|
+
storePath: context?.cronStorePath,
|
|
14683
|
+
useAICard: params.useAICard
|
|
14684
|
+
});
|
|
14685
|
+
}
|
|
14686
|
+
);
|
|
14687
|
+
api.registerGatewayMethod(
|
|
14688
|
+
"dingtalk-connector.send",
|
|
14689
|
+
async ({ context, respond, params }) => {
|
|
14690
|
+
const accountId = readStringParam2(params, "accountId");
|
|
14691
|
+
const target = readStringParam2(params, "target", { required: true });
|
|
14692
|
+
const content = getContentParam(params);
|
|
14693
|
+
if (!content) {
|
|
14694
|
+
return respond(false, { error: "content or message is required" });
|
|
14695
|
+
}
|
|
14696
|
+
if (!isConnectorSendTarget(target)) {
|
|
14697
|
+
return respond(false, { error: "target must start with user: or group:" });
|
|
14698
|
+
}
|
|
14699
|
+
return sendGatewayMessage({
|
|
14700
|
+
api,
|
|
14701
|
+
respond,
|
|
14702
|
+
accountId: accountId ?? void 0,
|
|
14703
|
+
target,
|
|
14704
|
+
content,
|
|
14705
|
+
storePath: context?.cronStorePath,
|
|
14706
|
+
useAICard: params.useAICard
|
|
14707
|
+
});
|
|
14708
|
+
}
|
|
14709
|
+
);
|
|
14710
|
+
api.registerGatewayMethod(
|
|
14711
|
+
"dingtalk-connector.status",
|
|
14712
|
+
async ({ respond }) => {
|
|
14713
|
+
const accountIds = listDingTalkAccountIds(api.config);
|
|
14714
|
+
const accounts = accountIds.length > 0 ? accountIds : ["default"];
|
|
14715
|
+
return respond(true, {
|
|
14716
|
+
channel: "dingtalk",
|
|
14717
|
+
accounts: accounts.map((accountId) => {
|
|
14718
|
+
const account = resolveDingTalkAccount(api.config, accountId);
|
|
14719
|
+
return {
|
|
14720
|
+
accountId,
|
|
14721
|
+
configured: account.configured,
|
|
14722
|
+
enabled: account.enabled !== false,
|
|
14723
|
+
name: account.name ?? null,
|
|
14724
|
+
clientId: maskClientId(account.clientId)
|
|
14725
|
+
};
|
|
14726
|
+
})
|
|
14727
|
+
});
|
|
14728
|
+
}
|
|
14729
|
+
);
|
|
14730
|
+
api.registerGatewayMethod(
|
|
14731
|
+
"dingtalk-connector.probe",
|
|
14732
|
+
async ({ respond, params }) => {
|
|
14733
|
+
const accountId = readStringParam2(params, "accountId");
|
|
14734
|
+
const config = getConfig(api.config, accountId ?? void 0);
|
|
14735
|
+
if (!config.clientId || !config.clientSecret) {
|
|
14736
|
+
return respond(false, { error: "DingTalk not configured" });
|
|
14737
|
+
}
|
|
14738
|
+
try {
|
|
14739
|
+
await getAccessToken(config, api.logger);
|
|
14740
|
+
return respond(true, { ok: true, clientId: maskClientId(config.clientId) });
|
|
14741
|
+
} catch (error) {
|
|
14742
|
+
const message = getErrorMessage2(error, "probe failed");
|
|
14743
|
+
api.logger?.warn?.(`[DingTalk][GatewayRPC] probe failed: ${message}`);
|
|
14744
|
+
return respond(false, { error: message });
|
|
14745
|
+
}
|
|
14746
|
+
}
|
|
14747
|
+
);
|
|
14422
14748
|
}
|
|
14423
14749
|
var index_default = defineChannelPluginEntry({
|
|
14424
14750
|
id: "dingtalk",
|
|
@@ -14428,11 +14754,15 @@ var index_default = defineChannelPluginEntry({
|
|
|
14428
14754
|
setRuntime: setDingTalkRuntime,
|
|
14429
14755
|
registerFull(api) {
|
|
14430
14756
|
registerDingTalkDocsGatewayMethods(api);
|
|
14431
|
-
api
|
|
14432
|
-
|
|
14433
|
-
|
|
14757
|
+
registerDingTalkConnectorCompatibilityGatewayMethods(api);
|
|
14758
|
+
api.on(
|
|
14759
|
+
"llm_output",
|
|
14760
|
+
(event) => {
|
|
14761
|
+
if (event.usage) {
|
|
14762
|
+
accumulateUsage(event.runId, event.usage);
|
|
14763
|
+
}
|
|
14434
14764
|
}
|
|
14435
|
-
|
|
14765
|
+
);
|
|
14436
14766
|
}
|
|
14437
14767
|
});
|
|
14438
14768
|
export {
|