@soimy/dingtalk 3.5.1 → 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/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