@soimy/dingtalk 3.5.0 → 3.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/package.json +14 -3
- package/src/ack-reaction-service.ts +1 -1
- package/src/auth.ts +1 -1
- package/src/card/card-action-handler.ts +62 -0
- package/src/card/card-run-registry.ts +118 -0
- package/src/card/card-stop-handler.ts +94 -0
- package/src/card/card-template.ts +20 -0
- package/src/card/reasoning-block-assembler.ts +157 -0
- package/src/card-callback-service.ts +90 -8
- package/src/card-draft-controller.ts +32 -0
- package/src/card-service.ts +189 -128
- package/src/channel.ts +82 -53
- package/src/command/card-stop-command.ts +78 -0
- package/src/command/inbound-command-dispatch-service.ts +464 -0
- package/src/docs-service.ts +5 -5
- package/src/http-client.ts +20 -0
- package/src/inbound-handler.ts +162 -465
- package/src/logger-context.ts +16 -2
- package/src/media-utils.ts +3 -3
- package/src/{attachment-text-extractor.ts → messaging/attachment-text-extractor.ts} +1 -1
- package/src/{quoted-file-service.ts → messaging/quoted-file-service.ts} +5 -5
- package/src/onboarding.ts +4 -32
- package/src/reply-strategy-card.ts +85 -17
- package/src/reply-strategy-markdown.ts +123 -18
- package/src/reply-strategy.ts +5 -0
- package/src/send-service.ts +80 -17
- package/src/targeting/agent-routing.ts +11 -4
- package/src/{group-members-store.ts → targeting/group-members-store.ts} +1 -1
- package/src/types.ts +6 -1
- package/src/utils.ts +165 -0
package/src/send-service.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import axios from "
|
|
2
|
+
import axios from "./http-client";
|
|
3
3
|
import { getAccessToken } from "./auth";
|
|
4
4
|
import {
|
|
5
5
|
isCardInTerminalState,
|
|
@@ -37,6 +37,53 @@ import type {
|
|
|
37
37
|
|
|
38
38
|
export { detectMediaTypeFromExtension } from "./media-utils";
|
|
39
39
|
|
|
40
|
+
const MARKDOWN_LOCAL_IMAGE_RE =
|
|
41
|
+
/!\[([^\]]*)\]\((file:\/\/\/[^)]+|\/(?:tmp|var|private|Users|home|root)[^)]+|[A-Za-z]:[\\/][^)]+)\)/g;
|
|
42
|
+
|
|
43
|
+
function decodeMarkdownLocalImagePath(rawPath: string): string {
|
|
44
|
+
const unescapedPath = rawPath.replace(/\\ /g, " ");
|
|
45
|
+
if (unescapedPath.startsWith("file://")) {
|
|
46
|
+
try {
|
|
47
|
+
return decodeURIComponent(unescapedPath.replace("file://", ""));
|
|
48
|
+
} catch {
|
|
49
|
+
return unescapedPath.replace("file://", "");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return unescapedPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function replaceMarkdownLocalImages(params: {
|
|
56
|
+
config: DingTalkConfig;
|
|
57
|
+
text: string;
|
|
58
|
+
log?: Logger;
|
|
59
|
+
mediaLocalRoots?: string[];
|
|
60
|
+
}): Promise<string> {
|
|
61
|
+
const matches = [...params.text.matchAll(MARKDOWN_LOCAL_IMAGE_RE)];
|
|
62
|
+
if (matches.length === 0) {
|
|
63
|
+
return params.text;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let result = params.text;
|
|
67
|
+
for (const match of matches) {
|
|
68
|
+
const [fullMatch, altText, rawPath] = match;
|
|
69
|
+
const mediaPath = decodeMarkdownLocalImagePath(rawPath);
|
|
70
|
+
const uploadResult = await uploadMedia(params.config, mediaPath, "image", params.log, {
|
|
71
|
+
mediaLocalRoots: params.mediaLocalRoots,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!uploadResult?.mediaId) {
|
|
75
|
+
params.log?.warn?.(
|
|
76
|
+
`[DingTalk] Markdown local image upload failed, keep original reference: ${mediaPath}`,
|
|
77
|
+
);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
result = result.replace(fullMatch, () => ``);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
40
87
|
type ProactiveTextSendResult = AxiosResponse | { tracking: DingTalkTrackingMetadata };
|
|
41
88
|
|
|
42
89
|
function isTrackingResult(result: ProactiveTextSendResult): result is { tracking: DingTalkTrackingMetadata } {
|
|
@@ -225,7 +272,7 @@ export async function sendProactiveTextOrMarkdown(
|
|
|
225
272
|
|
|
226
273
|
// In card mode, use card API to avoid oToMessages/batchSend permission requirement.
|
|
227
274
|
const messageType = config.messageType || "markdown";
|
|
228
|
-
if (messageType === "card" &&
|
|
275
|
+
if (messageType === "card" && !options.forceMarkdown) {
|
|
229
276
|
log?.debug?.(
|
|
230
277
|
`[DingTalk] Using card API for proactive message to user ${resolvedTarget}${proactiveRiskTag}`,
|
|
231
278
|
);
|
|
@@ -252,7 +299,16 @@ export async function sendProactiveTextOrMarkdown(
|
|
|
252
299
|
? "https://api.dingtalk.com/v1.0/robot/groupMessages/send"
|
|
253
300
|
: "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend";
|
|
254
301
|
|
|
255
|
-
const
|
|
302
|
+
const textWithUploadedLocalImages = await replaceMarkdownLocalImages({
|
|
303
|
+
config,
|
|
304
|
+
text,
|
|
305
|
+
log,
|
|
306
|
+
mediaLocalRoots: options.mediaLocalRoots,
|
|
307
|
+
});
|
|
308
|
+
const normalizedText =
|
|
309
|
+
config.convertMarkdownTables !== false
|
|
310
|
+
? convertMarkdownTablesToPlainText(textWithUploadedLocalImages)
|
|
311
|
+
: textWithUploadedLocalImages;
|
|
256
312
|
const { useMarkdown, title } = detectMarkdownAndExtractTitle(normalizedText, options, "OpenClaw 提醒");
|
|
257
313
|
|
|
258
314
|
log?.debug?.(
|
|
@@ -531,7 +587,16 @@ export async function sendBySession(
|
|
|
531
587
|
}
|
|
532
588
|
|
|
533
589
|
// Fallback to text/markdown reply payload.
|
|
534
|
-
const
|
|
590
|
+
const textWithUploadedLocalImages = await replaceMarkdownLocalImages({
|
|
591
|
+
config,
|
|
592
|
+
text,
|
|
593
|
+
log,
|
|
594
|
+
mediaLocalRoots: options.mediaLocalRoots,
|
|
595
|
+
});
|
|
596
|
+
const normalizedText =
|
|
597
|
+
config.convertMarkdownTables !== false
|
|
598
|
+
? convertMarkdownTablesToPlainText(textWithUploadedLocalImages)
|
|
599
|
+
: textWithUploadedLocalImages;
|
|
535
600
|
const { useMarkdown, title } = detectMarkdownAndExtractTitle(normalizedText, options, "Clawdbot 消息");
|
|
536
601
|
const chunks = splitMarkdownChunks(normalizedText, DINGTALK_TEXT_CHUNK_LIMIT);
|
|
537
602
|
|
|
@@ -582,20 +647,18 @@ export async function sendMessage(
|
|
|
582
647
|
return { ok: true };
|
|
583
648
|
}
|
|
584
649
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
return { ok: false, error: proactiveResult.error || "Card send failed" };
|
|
589
|
-
}
|
|
590
|
-
return {
|
|
591
|
-
ok: true,
|
|
592
|
-
tracking: {
|
|
593
|
-
processQueryKey: proactiveResult.processQueryKey,
|
|
594
|
-
outTrackId: proactiveResult.outTrackId,
|
|
595
|
-
cardInstanceId: proactiveResult.cardInstanceId,
|
|
596
|
-
},
|
|
597
|
-
};
|
|
650
|
+
const proactiveResult = await sendProactiveCardText(config, conversationId, text, log);
|
|
651
|
+
if (!proactiveResult.ok) {
|
|
652
|
+
return { ok: false, error: proactiveResult.error || "Card send failed" };
|
|
598
653
|
}
|
|
654
|
+
return {
|
|
655
|
+
ok: true,
|
|
656
|
+
tracking: {
|
|
657
|
+
processQueryKey: proactiveResult.processQueryKey,
|
|
658
|
+
outTrackId: proactiveResult.outTrackId,
|
|
659
|
+
cardInstanceId: proactiveResult.cardInstanceId,
|
|
660
|
+
},
|
|
661
|
+
};
|
|
599
662
|
}
|
|
600
663
|
}
|
|
601
664
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
11
|
+
import { maybeResolveTextAlias } from "openclaw/plugin-sdk/command-auth";
|
|
11
12
|
import { resolveAtAgents } from "./agent-name-matcher";
|
|
12
13
|
import { resolveRobotCode } from "../config";
|
|
13
14
|
import { parseLearnCommand } from "../learning-command-service";
|
|
@@ -93,16 +94,22 @@ export async function resolveSubAgentRoute(params: {
|
|
|
93
94
|
const atMentions = extractedContent.atMentions || [];
|
|
94
95
|
// DM has no @picker list from DingTalk; only group chats provide atUsers for real-user hints.
|
|
95
96
|
const atUserDingtalkIds = isGroup ? extractedContent.atUserDingtalkIds : undefined;
|
|
96
|
-
// Strip quoted prefix before checking
|
|
97
|
-
// when the quoted message itself contains a
|
|
97
|
+
// Strip quoted prefix before checking commands to avoid false positives
|
|
98
|
+
// when the quoted message itself contains a command.
|
|
98
99
|
const textForCommandCheck = extractedContent.text.replace(/^\[引用[^\]]*\]\s*/, "");
|
|
99
100
|
const isLearnCommand = parseLearnCommand(textForCommandCheck).scope !== "unknown";
|
|
101
|
+
// Slash commands like /new, /stop, /reasoning etc. must bypass sub-agent
|
|
102
|
+
// routing so they reach the framework's own command handling layer.
|
|
103
|
+
// Strip leading @mention tokens first since DM text may look like "@Agent /new".
|
|
104
|
+
const textWithoutMentions = textForCommandCheck.replace(/^(?:@\S+\s+)*/u, "").trim();
|
|
105
|
+
const isSlashCommand = maybeResolveTextAlias(textWithoutMentions, cfg) !== null;
|
|
100
106
|
|
|
101
107
|
if (
|
|
102
108
|
atMentions.length === 0 ||
|
|
103
109
|
!cfg.agents?.list ||
|
|
104
110
|
cfg.agents.list.length === 0 ||
|
|
105
|
-
isLearnCommand
|
|
111
|
+
isLearnCommand ||
|
|
112
|
+
isSlashCommand
|
|
106
113
|
) {
|
|
107
114
|
return null;
|
|
108
115
|
}
|
|
@@ -173,7 +180,7 @@ export async function dispatchSubAgents(params: {
|
|
|
173
180
|
dingtalkConfig,
|
|
174
181
|
subAgentOptions: {
|
|
175
182
|
agentId: agentMatch.agentId,
|
|
176
|
-
responsePrefix:
|
|
183
|
+
responsePrefix: `> 🤖 **${sanitizeAgentName(agentMatch.matchedName)}**:\n\n`,
|
|
177
184
|
matchedName: agentMatch.matchedName,
|
|
178
185
|
},
|
|
179
186
|
preDownloadedMedia,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { readNamespaceJson, writeNamespaceJsonAtomic } from "
|
|
3
|
+
import { readNamespaceJson, writeNamespaceJsonAtomic } from "../persistence-store";
|
|
4
4
|
|
|
5
5
|
const GROUP_MEMBERS_NAMESPACE = "members.group-roster";
|
|
6
6
|
|
package/src/types.ts
CHANGED
|
@@ -44,7 +44,9 @@ export interface DingTalkConfig extends OpenClawConfig {
|
|
|
44
44
|
ackReaction?: AckReactionConfigValue;
|
|
45
45
|
debug?: boolean;
|
|
46
46
|
messageType?: "markdown" | "card";
|
|
47
|
+
/** @deprecated 已固定使用内置模板契约 */
|
|
47
48
|
cardTemplateId?: string;
|
|
49
|
+
/** @deprecated 已固定使用内置模板契约 */
|
|
48
50
|
cardTemplateKey?: string;
|
|
49
51
|
groups?: Record<string, { systemPrompt?: string; requireMention?: boolean; groupAllowFrom?: string[] }>;
|
|
50
52
|
accounts?: Record<string, DingTalkConfig>;
|
|
@@ -103,7 +105,9 @@ export interface DingTalkChannelConfig {
|
|
|
103
105
|
ackReaction?: AckReactionConfigValue;
|
|
104
106
|
debug?: boolean;
|
|
105
107
|
messageType?: "markdown" | "card";
|
|
108
|
+
/** @deprecated 已固定使用内置模板契约 */
|
|
106
109
|
cardTemplateId?: string;
|
|
110
|
+
/** @deprecated 已固定使用内置模板契约 */
|
|
107
111
|
cardTemplateKey?: string;
|
|
108
112
|
groups?: Record<string, { systemPrompt?: string; requireMention?: boolean; groupAllowFrom?: string[] }>;
|
|
109
113
|
accounts?: Record<string, DingTalkConfig>;
|
|
@@ -618,6 +622,7 @@ export const AICardStatus = {
|
|
|
618
622
|
PROCESSING: "1",
|
|
619
623
|
INPUTING: "2",
|
|
620
624
|
FINISHED: "3",
|
|
625
|
+
STOPPED: "4",
|
|
621
626
|
FAILED: "5",
|
|
622
627
|
} as const;
|
|
623
628
|
|
|
@@ -639,7 +644,7 @@ export interface AICardInstance {
|
|
|
639
644
|
storePath?: string;
|
|
640
645
|
createdAt: number;
|
|
641
646
|
lastUpdated: number;
|
|
642
|
-
state: AICardState; // Current card state: PROCESSING, INPUTING, FINISHED, FAILED
|
|
647
|
+
state: AICardState; // Current card state: PROCESSING, INPUTING, FINISHED, STOPPED, FAILED
|
|
643
648
|
config?: DingTalkConfig; // Store config reference for token refresh
|
|
644
649
|
lastStreamedContent?: string;
|
|
645
650
|
outTrackId?: string;
|
package/src/utils.ts
CHANGED
|
@@ -5,6 +5,171 @@ import * as os from "node:os";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import type { Logger, RetryOptions } from "./types";
|
|
7
7
|
|
|
8
|
+
type PluginDebugLogParams = {
|
|
9
|
+
accountId: string;
|
|
10
|
+
storePath?: string;
|
|
11
|
+
debug?: boolean;
|
|
12
|
+
baseLog?: Logger;
|
|
13
|
+
now?: () => Date;
|
|
14
|
+
fsImpl?: Pick<typeof fs, "appendFileSync" | "mkdirSync">;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type PluginDebugWriter = {
|
|
18
|
+
filePath: string;
|
|
19
|
+
warned: boolean;
|
|
20
|
+
directoryReady: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const pluginDebugWriters = new Map<string, PluginDebugWriter>();
|
|
24
|
+
const closedPluginDebugScopes = new Set<string>();
|
|
25
|
+
|
|
26
|
+
function padNumber(value: number, width = 2): string {
|
|
27
|
+
return String(value).padStart(width, "0");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatTimezoneOffset(date: Date): string {
|
|
31
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
32
|
+
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
33
|
+
const absoluteMinutes = Math.abs(offsetMinutes);
|
|
34
|
+
const hours = Math.floor(absoluteMinutes / 60);
|
|
35
|
+
const minutes = absoluteMinutes % 60;
|
|
36
|
+
return `${sign}${padNumber(hours)}:${padNumber(minutes)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatPluginDebugTimestamp(date: Date): string {
|
|
40
|
+
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)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatPluginDebugDate(date: Date): string {
|
|
44
|
+
return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolvePluginDebugLogFilePath(params: { storePath: string; accountId: string; date: Date }): string {
|
|
48
|
+
return path.join(
|
|
49
|
+
path.dirname(params.storePath),
|
|
50
|
+
"logs",
|
|
51
|
+
"dingtalk",
|
|
52
|
+
params.accountId,
|
|
53
|
+
`debug-${formatPluginDebugDate(params.date)}.log`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatPluginDebugLine(params: { accountId: string; date: Date; message: string }): string {
|
|
58
|
+
return `[${formatPluginDebugTimestamp(params.date)}] [debug] [dingtalk] [account:${params.accountId}] ${params.message}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildPluginDebugWriterKey(params: { storePath: string; accountId: string; date: Date }): string {
|
|
62
|
+
return JSON.stringify([params.storePath, params.accountId, formatPluginDebugDate(params.date)]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildPluginDebugScopeKey(params: { storePath: string; accountId: string }): string {
|
|
66
|
+
return JSON.stringify([params.storePath, params.accountId]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolvePluginDebugWriter(params: {
|
|
70
|
+
storePath: string;
|
|
71
|
+
accountId: string;
|
|
72
|
+
date: Date;
|
|
73
|
+
}): PluginDebugWriter {
|
|
74
|
+
const key = buildPluginDebugWriterKey(params);
|
|
75
|
+
const existing = pluginDebugWriters.get(key);
|
|
76
|
+
if (existing) {
|
|
77
|
+
return existing;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const created = {
|
|
81
|
+
filePath: resolvePluginDebugLogFilePath(params),
|
|
82
|
+
warned: false,
|
|
83
|
+
directoryReady: false,
|
|
84
|
+
};
|
|
85
|
+
pluginDebugWriters.set(key, created);
|
|
86
|
+
return created;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function resolvePluginDebugLog(params: PluginDebugLogParams): Logger {
|
|
90
|
+
const baseLog = params.baseLog;
|
|
91
|
+
const fsImpl = params.fsImpl ?? fs;
|
|
92
|
+
const scopeKey = params.storePath
|
|
93
|
+
? buildPluginDebugScopeKey({ storePath: params.storePath, accountId: params.accountId })
|
|
94
|
+
: undefined;
|
|
95
|
+
|
|
96
|
+
if (scopeKey) {
|
|
97
|
+
closedPluginDebugScopes.delete(scopeKey);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
debug: (message: string) => {
|
|
102
|
+
if (!params.debug) {
|
|
103
|
+
baseLog?.debug?.(message);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const date = params.now ? params.now() : new Date();
|
|
108
|
+
const line = formatPluginDebugLine({
|
|
109
|
+
accountId: params.accountId,
|
|
110
|
+
date,
|
|
111
|
+
message,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
process.stdout.write(`${line}\n`);
|
|
116
|
+
} catch {
|
|
117
|
+
// Ignore stdout failures so plugin debug logging never breaks message handling.
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (params.storePath && scopeKey && !closedPluginDebugScopes.has(scopeKey)) {
|
|
121
|
+
const writer = resolvePluginDebugWriter({
|
|
122
|
+
storePath: params.storePath,
|
|
123
|
+
accountId: params.accountId,
|
|
124
|
+
date,
|
|
125
|
+
});
|
|
126
|
+
try {
|
|
127
|
+
if (!writer.directoryReady) {
|
|
128
|
+
fsImpl.mkdirSync(path.dirname(writer.filePath), { recursive: true });
|
|
129
|
+
writer.directoryReady = true;
|
|
130
|
+
}
|
|
131
|
+
fsImpl.appendFileSync(writer.filePath, `${line}\n`, "utf8");
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (!writer.warned) {
|
|
134
|
+
writer.warned = true;
|
|
135
|
+
baseLog?.warn?.(
|
|
136
|
+
`[DingTalk] Plugin debug log file unavailable: accountId=${params.accountId} path=${writer.filePath} error=${getErrorMessage(err)}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
baseLog?.debug?.(message);
|
|
144
|
+
} catch {
|
|
145
|
+
// Ignore upstream debug failures so plugin-owned debug remains best-effort.
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
info: (message: string) => baseLog?.info?.(message),
|
|
149
|
+
warn: (message: string) => baseLog?.warn?.(message),
|
|
150
|
+
error: (message: string) => baseLog?.error?.(message),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function closePluginDebugLog(params: { accountId: string; storePath?: string }): void {
|
|
155
|
+
if (!params.storePath) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const scopeKey = buildPluginDebugScopeKey({
|
|
160
|
+
storePath: params.storePath,
|
|
161
|
+
accountId: params.accountId,
|
|
162
|
+
});
|
|
163
|
+
closedPluginDebugScopes.add(scopeKey);
|
|
164
|
+
|
|
165
|
+
for (const key of pluginDebugWriters.keys()) {
|
|
166
|
+
const [writerStorePath, writerAccountId] = JSON.parse(key) as [string, string, string];
|
|
167
|
+
if (writerStorePath === params.storePath && writerAccountId === params.accountId) {
|
|
168
|
+
pluginDebugWriters.delete(key);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
8
173
|
/**
|
|
9
174
|
* Mask sensitive fields in data for safe logging
|
|
10
175
|
* Prevents PII leakage in debug logs
|