@xmanrui/dsh-im 4.15.0 → 4.16.0
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.en.md +2 -1
- package/README.md +9 -5
- package/lib/client.js +1729 -614
- package/lib/index.js +303 -293
- package/package.json +6 -2
- package/plugin-src/client/access-policy-settings.js +6 -0
- package/plugin-src/client/channels/feishu/index.js +15 -3
- package/plugin-src/client/channels/wecom-app/api.js +124 -0
- package/plugin-src/client/channels/wecom-app/index.js +612 -0
- package/plugin-src/client/channels/wecom-app/styles.js +25 -0
- package/plugin-src/client/delivery-settings.js +7 -0
- package/plugin-src/client/i18n.js +63 -0
- package/plugin-src/client/index.js +18 -0
- package/plugin-src/client/model-setting.js +135 -69
- package/plugin-src/client/styles.js +22 -7
- package/plugin-src/host/channels/dingtalk/index.mjs +8 -15
- package/plugin-src/host/channels/discord/index.mjs +8 -10
- package/plugin-src/host/channels/feishu/index.mjs +8 -15
- package/plugin-src/host/channels/office/index.mjs +9 -5
- package/plugin-src/host/channels/qq/index.mjs +8 -15
- package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
- package/plugin-src/host/channels/shared/model-setting-rpc.mjs +1 -2
- package/plugin-src/host/channels/shared/startup-error.mjs +53 -0
- package/plugin-src/host/channels/shared/startup.mjs +47 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
- package/plugin-src/host/channels/slack/index.mjs +8 -10
- package/plugin-src/host/channels/telegram/index.mjs +8 -10
- package/plugin-src/host/channels/wecom/index.mjs +8 -15
- package/plugin-src/host/channels/wecom/rpc.mjs +6 -1
- package/plugin-src/host/channels/wecom-app/index.mjs +33 -0
- package/plugin-src/host/channels/wecom-app/production.mjs +217 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +225 -0
- package/plugin-src/host/channels/weixin/index.mjs +8 -15
- package/plugin-src/host/channels/whatsapp/index.mjs +8 -15
- package/plugin-src/host/delivery-adapter.mjs +4 -0
- package/plugin-src/host/delivery-suggestions.mjs +4 -0
- package/plugin-src/host/index.mjs +7 -2
- package/scripts/verify-model-setting.mjs +54 -0
- package/src/channels/feishu/bridge.mjs +295 -9
- package/src/channels/feishu/feishu-cards.mjs +38 -0
- package/src/channels/feishu/feishu-channel.mjs +88 -20
- package/src/channels/shared/bot-workspace-store.mjs +11 -3
- package/src/channels/shared/command-catalog.mjs +102 -0
- package/src/channels/shared/i18n-en/feishu.mjs +12 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +3 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +9 -0
- package/src/channels/shared/i18n-en/wecom-app.mjs +33 -0
- package/src/channels/shared/i18n-en.mjs +2 -0
- package/src/channels/shared/model-setting.mjs +43 -8
- package/src/channels/shared/text-harness-bridge.mjs +2 -26
- package/src/channels/telegram/telegram-api.mjs +18 -6
- package/src/channels/telegram/telegram-runtime.mjs +34 -32
- package/src/channels/wecom/send-error.mjs +31 -0
- package/src/channels/wecom/wecom-bridge.mjs +59 -17
- package/src/channels/wecom-app/callback-server.mjs +471 -0
- package/src/channels/wecom-app/config-store.mjs +240 -0
- package/src/channels/wecom-app/harness-client.mjs +11 -0
- package/src/channels/wecom-app/state-store.mjs +107 -0
- package/src/channels/wecom-app/wecom-app-api.mjs +432 -0
- package/src/channels/wecom-app/wecom-app-bridge.mjs +1059 -0
- package/src/channels/wecom-app/wecom-app-controller.mjs +440 -0
- package/src/channels/wecom-app/wecom-app-runtime.mjs +221 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { t } from '../shared/i18n.mjs';
|
|
6
|
+
|
|
7
|
+
const EMPTY_DOCUMENT = Object.freeze({ version: 1, bots: Object.freeze([]) });
|
|
8
|
+
|
|
9
|
+
function cleanString(value) {
|
|
10
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function safeIntegrationId(value) {
|
|
14
|
+
const id = cleanString(value);
|
|
15
|
+
return id && /^wecomapp_[a-f0-9]{24}$/.test(id) ? id : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function safeRef(value, prefix) {
|
|
19
|
+
const ref = cleanString(value);
|
|
20
|
+
return ref && new RegExp('^' + prefix + '[A-F0-9]{24}$').test(ref) ? ref : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A bot identity is stable per (corpId, agentId) pair so re-binding the same
|
|
24
|
+
// application replaces its credentials in place instead of piling up bots.
|
|
25
|
+
export function deriveWecomAppIdentity({ corpId, agentId }) {
|
|
26
|
+
const id = cleanString(corpId);
|
|
27
|
+
const agent = cleanString(agentId);
|
|
28
|
+
if (!id || !agent) throw new TypeError('Enterprise WeChat corpId and agentId are required');
|
|
29
|
+
const digest = createHash('sha256').update(`${id}:${agent}`).digest('hex').slice(0, 24);
|
|
30
|
+
return {
|
|
31
|
+
botId: `wecomapp_${digest}`,
|
|
32
|
+
secretRef: `DSH_WECOM_APP_SECRET_${digest.toUpperCase()}`,
|
|
33
|
+
callbackTokenRef: `DSH_WECOM_APP_TOKEN_${digest.toUpperCase()}`,
|
|
34
|
+
callbackKeyRef: `DSH_WECOM_APP_AESKEY_${digest.toUpperCase()}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function generateCallbackSecret() {
|
|
39
|
+
return randomBytes(16).toString('hex');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function maskCorpId(corpId) {
|
|
43
|
+
const value = cleanString(corpId) ?? '';
|
|
44
|
+
if (!value) return t('企业微信应用');
|
|
45
|
+
if (value.length <= 10) return `${value.slice(0, 3)}•••`;
|
|
46
|
+
return `${value.slice(0, 6)}••••${value.slice(-4)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function validCorpId(value) {
|
|
50
|
+
const id = cleanString(value);
|
|
51
|
+
return Boolean(id && /^[A-Za-z][A-Za-z0-9_-]{2,127}$/.test(id));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function validAgentId(value) {
|
|
55
|
+
const agent = cleanString(value);
|
|
56
|
+
return Boolean(agent && /^\d{1,32}$/.test(agent) && Number(agent) > 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function validApiBaseUrl(value) {
|
|
60
|
+
const raw = cleanString(value);
|
|
61
|
+
return raw === null || /^https?:\/\//iu.test(raw);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeBot(value) {
|
|
65
|
+
if (!value || typeof value !== 'object') return null;
|
|
66
|
+
const botId = safeIntegrationId(value.botId);
|
|
67
|
+
const corpId = cleanString(value.corpId);
|
|
68
|
+
const agentId = cleanString(value.agentId);
|
|
69
|
+
const secretRef = safeRef(value.secretRef, 'DSH_WECOM_APP_SECRET_');
|
|
70
|
+
const callbackTokenRef = safeRef(value.callbackTokenRef, 'DSH_WECOM_APP_TOKEN_');
|
|
71
|
+
const callbackKeyRef = safeRef(value.callbackKeyRef, 'DSH_WECOM_APP_AESKEY_');
|
|
72
|
+
const callbackSecret = cleanString(value.callbackSecret);
|
|
73
|
+
if (!botId || !corpId || !agentId || !secretRef || !callbackTokenRef || !callbackKeyRef) return null;
|
|
74
|
+
if (!validCorpId(corpId) || !validAgentId(agentId)) return null;
|
|
75
|
+
if (!callbackSecret || !/^[a-f0-9]{32}$/.test(callbackSecret)) return null;
|
|
76
|
+
if (!validApiBaseUrl(value.apiBaseUrl ?? undefined)) return null;
|
|
77
|
+
if (!validApiBaseUrl(value.callbackBaseUrl ?? undefined)) return null;
|
|
78
|
+
const derived = deriveWecomAppIdentity({ corpId, agentId });
|
|
79
|
+
if (derived.botId !== botId || derived.secretRef !== secretRef
|
|
80
|
+
|| derived.callbackTokenRef !== callbackTokenRef || derived.callbackKeyRef !== callbackKeyRef) return null;
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
botId,
|
|
83
|
+
corpId,
|
|
84
|
+
agentId,
|
|
85
|
+
secretRef,
|
|
86
|
+
callbackTokenRef,
|
|
87
|
+
callbackKeyRef,
|
|
88
|
+
callbackSecret,
|
|
89
|
+
apiBaseUrl: cleanString(value.apiBaseUrl) ?? undefined,
|
|
90
|
+
callbackBaseUrl: cleanString(value.callbackBaseUrl) ?? undefined,
|
|
91
|
+
streamEnabled: value.streamEnabled !== false,
|
|
92
|
+
createdAt: cleanString(value.createdAt) ?? new Date().toISOString(),
|
|
93
|
+
connectedAt: cleanString(value.connectedAt),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeDocument(value) {
|
|
98
|
+
if (!value || value.version !== 1 || !Array.isArray(value.bots)) return null;
|
|
99
|
+
const bots = value.bots.map(normalizeBot);
|
|
100
|
+
if (bots.some((bot) => bot === null)) return null;
|
|
101
|
+
const ids = new Set();
|
|
102
|
+
const corpAgents = new Set();
|
|
103
|
+
const secrets = new Set();
|
|
104
|
+
const callbackSecrets = new Set();
|
|
105
|
+
for (const bot of bots) {
|
|
106
|
+
const corpAgent = `${bot.corpId}:${bot.agentId}`;
|
|
107
|
+
if (ids.has(bot.botId) || corpAgents.has(corpAgent)
|
|
108
|
+
|| secrets.has(bot.secretRef) || callbackSecrets.has(bot.callbackSecret)) return null;
|
|
109
|
+
ids.add(bot.botId);
|
|
110
|
+
corpAgents.add(corpAgent);
|
|
111
|
+
secrets.add(bot.secretRef);
|
|
112
|
+
callbackSecrets.add(bot.callbackSecret);
|
|
113
|
+
}
|
|
114
|
+
return Object.freeze({ version: 1, bots: Object.freeze(bots) });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export class WecomAppConfigStore {
|
|
118
|
+
#path;
|
|
119
|
+
#value = EMPTY_DOCUMENT;
|
|
120
|
+
#writeQueue = Promise.resolve();
|
|
121
|
+
|
|
122
|
+
constructor(path) {
|
|
123
|
+
this.#path = path;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async load() {
|
|
127
|
+
try {
|
|
128
|
+
const normalized = normalizeDocument(JSON.parse(await readFile(this.#path, 'utf8')));
|
|
129
|
+
if (!normalized) throw new Error('dsh-im Enterprise WeChat app config contains invalid bot data');
|
|
130
|
+
this.#value = normalized;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
133
|
+
this.#value = EMPTY_DOCUMENT;
|
|
134
|
+
}
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
list() {
|
|
139
|
+
return structuredClone(this.#value.bots);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get(botId) {
|
|
143
|
+
const bot = this.#value.bots.find((candidate) => candidate.botId === botId);
|
|
144
|
+
return bot ? structuredClone(bot) : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getByCorpAgent(corpId, agentId) {
|
|
148
|
+
const bot = this.#value.bots.find((candidate) => candidate.corpId === corpId && candidate.agentId === agentId);
|
|
149
|
+
return bot ? structuredClone(bot) : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async save(value) {
|
|
153
|
+
const normalized = normalizeBot(value);
|
|
154
|
+
if (!normalized) throw new Error('Refusing to persist incomplete Enterprise WeChat app bot data');
|
|
155
|
+
return this.#mutate((bots) => {
|
|
156
|
+
const corpAgent = `${normalized.corpId}:${normalized.agentId}`;
|
|
157
|
+
const collision = bots.find((bot) =>
|
|
158
|
+
(bot.botId === normalized.botId || `${bot.corpId}:${bot.agentId}` === corpAgent
|
|
159
|
+
|| bot.secretRef === normalized.secretRef || bot.callbackSecret === normalized.callbackSecret)
|
|
160
|
+
&& bot.botId !== normalized.botId);
|
|
161
|
+
if (collision) throw new Error('Duplicate Enterprise WeChat app bot identity');
|
|
162
|
+
const index = bots.findIndex((bot) => bot.botId === normalized.botId);
|
|
163
|
+
if (index === -1) bots.push(normalized);
|
|
164
|
+
else bots[index] = normalized;
|
|
165
|
+
return structuredClone(normalized);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async update(botId, patch = {}) {
|
|
170
|
+
const existing = this.get(botId);
|
|
171
|
+
if (!existing) throw new Error('Unknown Enterprise WeChat app bot');
|
|
172
|
+
const merged = { ...existing };
|
|
173
|
+
if (patch.apiBaseUrl !== undefined) {
|
|
174
|
+
merged.apiBaseUrl = cleanString(patch.apiBaseUrl) ?? null;
|
|
175
|
+
if (!validApiBaseUrl(merged.apiBaseUrl ?? undefined)) {
|
|
176
|
+
throw new Error('代理地址必须是 http(s) 地址');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (patch.callbackBaseUrl !== undefined) {
|
|
180
|
+
merged.callbackBaseUrl = cleanString(patch.callbackBaseUrl) ?? null;
|
|
181
|
+
if (!validApiBaseUrl(merged.callbackBaseUrl ?? undefined)) {
|
|
182
|
+
throw new Error('回调基址必须是 http(s) 地址');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (patch.streamEnabled !== undefined) merged.streamEnabled = patch.streamEnabled === true;
|
|
186
|
+
if (patch.callbackSecret !== undefined) {
|
|
187
|
+
const secret = cleanString(patch.callbackSecret);
|
|
188
|
+
if (!secret || !/^[a-f0-9]{32}$/.test(secret)) throw new Error('回调密钥格式不正确');
|
|
189
|
+
merged.callbackSecret = secret;
|
|
190
|
+
}
|
|
191
|
+
return this.save(merged);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async remove(botId) {
|
|
195
|
+
if (!safeIntegrationId(botId)) throw new TypeError('Invalid Enterprise WeChat app bot ID');
|
|
196
|
+
return this.#mutate((bots) => {
|
|
197
|
+
const index = bots.findIndex((candidate) => candidate.botId === botId);
|
|
198
|
+
if (index === -1) return null;
|
|
199
|
+
const [removed] = bots.splice(index, 1);
|
|
200
|
+
return structuredClone(removed);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async clear() {
|
|
205
|
+
const operation = this.#writeQueue.then(async () => {
|
|
206
|
+
try {
|
|
207
|
+
await unlink(this.#path);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
210
|
+
}
|
|
211
|
+
this.#value = EMPTY_DOCUMENT;
|
|
212
|
+
});
|
|
213
|
+
this.#writeQueue = operation.then(() => undefined, () => undefined);
|
|
214
|
+
await operation;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async #mutate(mutator) {
|
|
218
|
+
let result;
|
|
219
|
+
const operation = this.#writeQueue.then(async () => {
|
|
220
|
+
const bots = [...this.#value.bots];
|
|
221
|
+
result = mutator(bots);
|
|
222
|
+
const document = Object.freeze({ version: 1, bots: Object.freeze(bots) });
|
|
223
|
+
await this.#write(document);
|
|
224
|
+
this.#value = document;
|
|
225
|
+
});
|
|
226
|
+
this.#writeQueue = operation.then(() => undefined, () => undefined);
|
|
227
|
+
await operation;
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async #write(document) {
|
|
232
|
+
await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
|
|
233
|
+
const temporary = `${this.#path}.tmp`;
|
|
234
|
+
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, {
|
|
235
|
+
encoding: 'utf8',
|
|
236
|
+
mode: 0o600,
|
|
237
|
+
});
|
|
238
|
+
await rename(temporary, this.#path);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { deferredStateAccess, normalizeDeferredState } from '../shared/deferred-state.mjs';
|
|
2
|
+
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const EMPTY_STATE = Object.freeze({ version: 1, sessions: {}, seenMessageIds: [] });
|
|
6
|
+
|
|
7
|
+
function normalizeState(value) {
|
|
8
|
+
if (!value || typeof value !== 'object') return structuredClone(EMPTY_STATE);
|
|
9
|
+
const sessions = {};
|
|
10
|
+
if (value.sessions && typeof value.sessions === 'object' && !Array.isArray(value.sessions)) {
|
|
11
|
+
for (const [key, sessionId] of Object.entries(value.sessions)) {
|
|
12
|
+
if (typeof key === 'string' && typeof sessionId === 'string' && sessionId) sessions[key] = sessionId;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
version: 1,
|
|
17
|
+
sessions,
|
|
18
|
+
...(value.deferred ? { deferred: normalizeDeferredState(value.deferred) } : {}),
|
|
19
|
+
seenMessageIds: Array.isArray(value.seenMessageIds)
|
|
20
|
+
? value.seenMessageIds.filter((id) => typeof id === 'string').slice(-1_000)
|
|
21
|
+
: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class WecomAppStateStore {
|
|
26
|
+
#path;
|
|
27
|
+
#state = structuredClone(EMPTY_STATE);
|
|
28
|
+
#writeQueue = Promise.resolve();
|
|
29
|
+
#deferred = deferredStateAccess(() => this.#state, () => this.#persist());
|
|
30
|
+
|
|
31
|
+
constructor(path) {
|
|
32
|
+
this.#path = path;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async load() {
|
|
36
|
+
try {
|
|
37
|
+
this.#state = normalizeState(JSON.parse(await readFile(this.#path, 'utf8')));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
40
|
+
this.#state = structuredClone(EMPTY_STATE);
|
|
41
|
+
await this.#persist();
|
|
42
|
+
}
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
deferredEntries() { return this.#deferred.entries(); }
|
|
47
|
+
putDeferred(entry) { return this.#deferred.put(entry); }
|
|
48
|
+
patchDeferred(id, patch) { return this.#deferred.patch(id, patch); }
|
|
49
|
+
removeDeferred(id) { return this.#deferred.remove(id); }
|
|
50
|
+
|
|
51
|
+
sessionFor(key) {
|
|
52
|
+
return this.#state.sessions[key] ?? null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async setSession(key, sessionId) {
|
|
56
|
+
this.#state.sessions[key] = sessionId;
|
|
57
|
+
await this.#persist();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async clearSession(key) {
|
|
61
|
+
delete this.#state.sessions[key];
|
|
62
|
+
await this.#persist();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async clearSessions() {
|
|
66
|
+
this.#state.sessions = {};
|
|
67
|
+
await this.#persist();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
hasSeen(messageId) {
|
|
71
|
+
return this.#state.seenMessageIds.includes(messageId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async markSeen(messageId) {
|
|
75
|
+
if (this.hasSeen(messageId)) return;
|
|
76
|
+
this.#state.seenMessageIds.push(messageId);
|
|
77
|
+
if (this.#state.seenMessageIds.length > 1_000) {
|
|
78
|
+
this.#state.seenMessageIds.splice(0, this.#state.seenMessageIds.length - 1_000);
|
|
79
|
+
}
|
|
80
|
+
await this.#persist();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
snapshot() {
|
|
84
|
+
return structuredClone(this.#state);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async remove() {
|
|
88
|
+
try {
|
|
89
|
+
await unlink(this.#path);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
92
|
+
}
|
|
93
|
+
this.#state = structuredClone(EMPTY_STATE);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async #persist() {
|
|
97
|
+
const snapshot = `${JSON.stringify(this.#state, null, 2)}\n`;
|
|
98
|
+
const operation = this.#writeQueue.then(async () => {
|
|
99
|
+
await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
|
|
100
|
+
const temporary = `${this.#path}.tmp`;
|
|
101
|
+
await writeFile(temporary, snapshot, { encoding: 'utf8', mode: 0o600 });
|
|
102
|
+
await rename(temporary, this.#path);
|
|
103
|
+
});
|
|
104
|
+
this.#writeQueue = operation.then(() => undefined, () => undefined);
|
|
105
|
+
await operation;
|
|
106
|
+
}
|
|
107
|
+
}
|