openclaw-glance-plugin 0.1.18 → 0.1.21
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/openclaw.plugin.json +0 -4
- package/package.json +1 -1
- package/skills/glance-watch/SKILL.md +57 -481
- package/skills/glance-watch/references/channels.md +80 -0
- package/skills/glance-watch/references/examples.md +43 -0
- package/skills/glance-watch/references/query-and-symbol.md +69 -0
- package/skills/glance-watch/references/troubleshooting.md +40 -0
- package/skills/glance-watch/references/watch-contract.md +132 -0
- package/src/config/runtime-config.js +1 -15
- package/src/plugin/index.js +19 -41
- package/src/plugin/watch-notify-contacts.js +0 -478
|
@@ -1,478 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
deriveOpenclawRouting,
|
|
6
|
-
mergeContextMetadata,
|
|
7
|
-
pickFirstSenderIdentifier,
|
|
8
|
-
pickFirstString,
|
|
9
|
-
unwrapSenderContextObject
|
|
10
|
-
} from '../openclawRouting.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 同一联系人文件读改写串行化,避免并发丢更新。
|
|
14
|
-
* 仅单进程内有效;多进程同时写仍可能竞态,需外部协调或独占部署。
|
|
15
|
-
*/
|
|
16
|
-
const contactFileQueues = new Map();
|
|
17
|
-
|
|
18
|
-
export function runContactsFileSerialized(filePath, fn) {
|
|
19
|
-
const key = path.resolve(String(filePath));
|
|
20
|
-
const prev = contactFileQueues.get(key) || Promise.resolve();
|
|
21
|
-
const next = prev.then(
|
|
22
|
-
() => fn(),
|
|
23
|
-
() => fn()
|
|
24
|
-
);
|
|
25
|
-
contactFileQueues.set(key, next);
|
|
26
|
-
return next.finally(() => {
|
|
27
|
-
if (contactFileQueues.get(key) === next) {
|
|
28
|
-
contactFileQueues.delete(key);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 从 OpenClaw / 宿主上下文解析发送者维度主键(channel:sender_id)。
|
|
35
|
-
* 与 OpenClaw `buildSenderContext` 对齐:优先 `context.senderContext`,其次顶层 `senderId`;
|
|
36
|
-
* 仍兼容 `senderDingtalkId`、metadata、路由字段等历史来源。
|
|
37
|
-
*/
|
|
38
|
-
export function extractSenderContext({ context = {}, params = {} } = {}) {
|
|
39
|
-
const metadata = mergeContextMetadata(context);
|
|
40
|
-
const sc = unwrapSenderContextObject(context);
|
|
41
|
-
const routing = deriveOpenclawRouting({ params, context });
|
|
42
|
-
|
|
43
|
-
const channelRaw = pickFirstString(
|
|
44
|
-
sc.channel,
|
|
45
|
-
sc.sourceChannel,
|
|
46
|
-
sc.source_channel,
|
|
47
|
-
routing.channel,
|
|
48
|
-
params?.source_channel,
|
|
49
|
-
metadata?.channel,
|
|
50
|
-
metadata?.channelId,
|
|
51
|
-
context?.channel,
|
|
52
|
-
context?.channelId
|
|
53
|
-
);
|
|
54
|
-
const channel = String(channelRaw || 'unknown')
|
|
55
|
-
.toLowerCase()
|
|
56
|
-
.trim();
|
|
57
|
-
|
|
58
|
-
const senderId = pickFirstSenderIdentifier(
|
|
59
|
-
sc.senderId,
|
|
60
|
-
sc.sender_id,
|
|
61
|
-
sc.userId,
|
|
62
|
-
sc.user_id,
|
|
63
|
-
sc.casId,
|
|
64
|
-
sc.cas_id,
|
|
65
|
-
context.senderId,
|
|
66
|
-
context.sender_id,
|
|
67
|
-
context.userId,
|
|
68
|
-
context.user_id,
|
|
69
|
-
context.casId,
|
|
70
|
-
context.cas_id,
|
|
71
|
-
metadata.senderId,
|
|
72
|
-
metadata.sender_id,
|
|
73
|
-
metadata.senderDingtalkId,
|
|
74
|
-
metadata.sender_dingtalk_id,
|
|
75
|
-
context.senderDingtalkId,
|
|
76
|
-
metadata.userId,
|
|
77
|
-
metadata.user_id,
|
|
78
|
-
metadata.openId,
|
|
79
|
-
params.senderId,
|
|
80
|
-
params.sender_id
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
const senderName = pickFirstString(
|
|
84
|
-
sc.senderName,
|
|
85
|
-
sc.sender_name,
|
|
86
|
-
sc.displayName,
|
|
87
|
-
sc.display_name,
|
|
88
|
-
sc.nickname,
|
|
89
|
-
metadata.senderName,
|
|
90
|
-
metadata.sender_name,
|
|
91
|
-
metadata.displayName,
|
|
92
|
-
metadata.display_name,
|
|
93
|
-
metadata.nickname,
|
|
94
|
-
context.senderName,
|
|
95
|
-
context.displayName
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
if (!senderId) {
|
|
99
|
-
return { channel, senderId: null, senderName: senderName || null, senderKey: null };
|
|
100
|
-
}
|
|
101
|
-
const id = String(senderId).trim();
|
|
102
|
-
const senderKey = `${channel}:${id}`;
|
|
103
|
-
return { channel, senderId: id, senderName: senderName || null, senderKey };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* 与 OpenClaw 侧 `buildSenderContext(context)` 单参用法兼容的别名。
|
|
108
|
-
*/
|
|
109
|
-
export function buildSenderContext(context = {}, params = {}) {
|
|
110
|
-
return extractSenderContext({ context, params });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** @returns {string|null} 仅数字,含简单 +86 剥离 */
|
|
114
|
-
export function normalizePhone(raw) {
|
|
115
|
-
if (raw == null) return null;
|
|
116
|
-
let s = String(raw).trim().replace(/\s+/g, '');
|
|
117
|
-
if (!s) return null;
|
|
118
|
-
s = s.replace(/^\+86/, '').replace(/^86/, '');
|
|
119
|
-
const digits = s.replace(/\D/g, '');
|
|
120
|
-
if (digits.length === 11) return digits;
|
|
121
|
-
if (digits.length === 13 && digits.startsWith('86')) return digits.slice(2);
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** @returns {string|null} 非空 trim,用于钉钉 cas_id 等 */
|
|
126
|
-
export function normalizeCasId(raw) {
|
|
127
|
-
if (raw == null) return null;
|
|
128
|
-
const s = String(raw).trim();
|
|
129
|
-
return s || null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** @returns {string|null} 合法邮箱则返回 trim 后地址 */
|
|
133
|
-
export function normalizeEmail(raw) {
|
|
134
|
-
if (raw == null) return null;
|
|
135
|
-
const s = String(raw).trim();
|
|
136
|
-
if (!s) return null;
|
|
137
|
-
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)) return null;
|
|
138
|
-
return s;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function emptyContactsDoc() {
|
|
142
|
-
return { version: 1, senders: {} };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function backupCorruptContactsFile(filePath, raw) {
|
|
146
|
-
const dir = path.dirname(filePath);
|
|
147
|
-
const base = path.basename(filePath);
|
|
148
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
149
|
-
const backupPath = path.join(dir, `${base}.corrupt.${stamp}.bak`);
|
|
150
|
-
await mkdir(dir, { recursive: true });
|
|
151
|
-
await writeFile(backupPath, raw, 'utf8');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export async function loadContactsFile(filePath) {
|
|
155
|
-
try {
|
|
156
|
-
const raw = await readFile(filePath, 'utf8');
|
|
157
|
-
let data;
|
|
158
|
-
try {
|
|
159
|
-
data = JSON.parse(raw);
|
|
160
|
-
} catch (_parseErr) {
|
|
161
|
-
await backupCorruptContactsFile(filePath, raw);
|
|
162
|
-
return emptyContactsDoc();
|
|
163
|
-
}
|
|
164
|
-
if (!data || typeof data !== 'object') return emptyContactsDoc();
|
|
165
|
-
if (!data.senders || typeof data.senders !== 'object') {
|
|
166
|
-
data.senders = {};
|
|
167
|
-
}
|
|
168
|
-
if (data.version == null) data.version = 1;
|
|
169
|
-
return data;
|
|
170
|
-
} catch (err) {
|
|
171
|
-
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) {
|
|
172
|
-
return emptyContactsDoc();
|
|
173
|
-
}
|
|
174
|
-
throw err;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export async function saveContactsFile(filePath, doc) {
|
|
179
|
-
const dir = path.dirname(filePath);
|
|
180
|
-
await mkdir(dir, { recursive: true });
|
|
181
|
-
const text = `${JSON.stringify(doc, null, 2)}\n`;
|
|
182
|
-
await writeFile(filePath, text, 'utf8');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function getEntry(doc, senderKey) {
|
|
186
|
-
if (!senderKey || !doc?.senders) return null;
|
|
187
|
-
return doc.senders[senderKey] || null;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function touchEntry(doc, senderKey, senderId, senderName) {
|
|
191
|
-
if (!senderKey) return;
|
|
192
|
-
if (!doc.senders) doc.senders = {};
|
|
193
|
-
const prev = doc.senders[senderKey] || {
|
|
194
|
-
sender_id: senderId,
|
|
195
|
-
sender_name: senderName,
|
|
196
|
-
defaults: {},
|
|
197
|
-
updated_at: null
|
|
198
|
-
};
|
|
199
|
-
doc.senders[senderKey] = {
|
|
200
|
-
...prev,
|
|
201
|
-
sender_id: senderId || prev.sender_id,
|
|
202
|
-
sender_name: senderName || prev.sender_name || undefined
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 宿主未带会话 channel 时,用已合并的 openclaw 路由里的 channel 推断记忆主键,减少 unknown:<id> 串号。
|
|
208
|
-
*/
|
|
209
|
-
function refineSessionFromOpenclawConfig(ctx, openclaw) {
|
|
210
|
-
let { channel, senderId, senderName, senderKey } = ctx;
|
|
211
|
-
if (channel !== 'unknown' || !senderId) return ctx;
|
|
212
|
-
if (!openclaw || typeof openclaw !== 'object') return ctx;
|
|
213
|
-
const hint = pickFirstString(
|
|
214
|
-
openclaw.channel,
|
|
215
|
-
openclaw.source_channel,
|
|
216
|
-
openclaw.sourceChannel
|
|
217
|
-
);
|
|
218
|
-
if (!hint) return ctx;
|
|
219
|
-
const c = String(hint).toLowerCase().trim();
|
|
220
|
-
if (!c || c === 'unknown') return ctx;
|
|
221
|
-
const id = String(senderId).trim();
|
|
222
|
-
return {
|
|
223
|
-
channel: c,
|
|
224
|
-
senderId: id,
|
|
225
|
-
senderName,
|
|
226
|
-
senderKey: `${c}:${id}`
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* 合并 watch.create / submitWatchDemand 的 channel_configs。
|
|
232
|
-
* 缺字段时按记忆与钉钉 sender_id 规则补全;有 senderKey 时回写记忆。
|
|
233
|
-
*/
|
|
234
|
-
export async function mergeAndPersistWatchContacts(filePath, mergedPayload, context) {
|
|
235
|
-
const channels = Array.isArray(mergedPayload?.channels)
|
|
236
|
-
? mergedPayload.channels.map((x) => String(x).toLowerCase().trim()).filter(Boolean)
|
|
237
|
-
: [];
|
|
238
|
-
if (channels.length === 0) {
|
|
239
|
-
return mergedPayload;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return runContactsFileSerialized(filePath, async () => {
|
|
243
|
-
let senderCtx = extractSenderContext({
|
|
244
|
-
context,
|
|
245
|
-
params: mergedPayload || {}
|
|
246
|
-
});
|
|
247
|
-
senderCtx = refineSessionFromOpenclawConfig(
|
|
248
|
-
senderCtx,
|
|
249
|
-
mergedPayload?.channel_configs?.openclaw
|
|
250
|
-
);
|
|
251
|
-
const { senderKey, senderId, senderName, channel: sessionChannel } = senderCtx;
|
|
252
|
-
|
|
253
|
-
let doc = await loadContactsFile(filePath);
|
|
254
|
-
const entry = getEntry(doc, senderKey);
|
|
255
|
-
const defaults = entry?.defaults && typeof entry.defaults === 'object' ? entry.defaults : {};
|
|
256
|
-
|
|
257
|
-
const channelConfigs = { ...(mergedPayload.channel_configs || {}) };
|
|
258
|
-
|
|
259
|
-
const isDingtalkSession = sessionChannel === 'dingtalk';
|
|
260
|
-
|
|
261
|
-
if (channels.includes('sms')) {
|
|
262
|
-
const sms = { ...(channelConfigs.sms && typeof channelConfigs.sms === 'object' ? channelConfigs.sms : {}) };
|
|
263
|
-
let phone = normalizePhone(pickFirstString(sms.receiver, sms.phone));
|
|
264
|
-
if (!phone && defaults.sms?.phone) {
|
|
265
|
-
phone = normalizePhone(defaults.sms.phone);
|
|
266
|
-
if (phone) {
|
|
267
|
-
sms.receiver = phone;
|
|
268
|
-
if (sms.phone != null) delete sms.phone;
|
|
269
|
-
}
|
|
270
|
-
} else if (phone) {
|
|
271
|
-
sms.receiver = phone;
|
|
272
|
-
}
|
|
273
|
-
channelConfigs.sms = sms;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (channels.includes('dingtalk')) {
|
|
277
|
-
const dt = {
|
|
278
|
-
...(channelConfigs.dingtalk && typeof channelConfigs.dingtalk === 'object'
|
|
279
|
-
? channelConfigs.dingtalk
|
|
280
|
-
: {})
|
|
281
|
-
};
|
|
282
|
-
const casId =
|
|
283
|
-
normalizeCasId(pickFirstString(dt.cas_id, dt.casId)) ||
|
|
284
|
-
normalizeCasId(defaults.dingtalk?.cas_id) ||
|
|
285
|
-
(isDingtalkSession ? normalizeCasId(senderId) : null);
|
|
286
|
-
if (casId) {
|
|
287
|
-
dt.cas_id = casId;
|
|
288
|
-
if (dt.casId != null) delete dt.casId;
|
|
289
|
-
}
|
|
290
|
-
channelConfigs.dingtalk = dt;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (channels.includes('email')) {
|
|
294
|
-
const em = {
|
|
295
|
-
...(channelConfigs.email && typeof channelConfigs.email === 'object' ? channelConfigs.email : {})
|
|
296
|
-
};
|
|
297
|
-
const addr =
|
|
298
|
-
normalizeEmail(pickFirstString(em.to_address, em.toAddress)) ||
|
|
299
|
-
normalizeEmail(defaults.email?.to_address);
|
|
300
|
-
if (addr) {
|
|
301
|
-
em.to_address = addr;
|
|
302
|
-
if (em.toAddress != null) delete em.toAddress;
|
|
303
|
-
}
|
|
304
|
-
channelConfigs.email = em;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (channels.includes('call')) {
|
|
308
|
-
const ca = {
|
|
309
|
-
...(channelConfigs.call && typeof channelConfigs.call === 'object' ? channelConfigs.call : {})
|
|
310
|
-
};
|
|
311
|
-
let phone = normalizePhone(pickFirstString(ca.phone, ca.receiver));
|
|
312
|
-
if (!phone && defaults.call?.phone) {
|
|
313
|
-
phone = normalizePhone(defaults.call.phone);
|
|
314
|
-
}
|
|
315
|
-
if (phone) ca.phone = phone;
|
|
316
|
-
|
|
317
|
-
let name = pickFirstString(ca.customer_name, ca.customerName);
|
|
318
|
-
if (!name && defaults.call?.customer_name) {
|
|
319
|
-
name = String(defaults.call.customer_name).trim();
|
|
320
|
-
}
|
|
321
|
-
if (!name && senderName) {
|
|
322
|
-
name = String(senderName).trim();
|
|
323
|
-
}
|
|
324
|
-
if (name) {
|
|
325
|
-
ca.customer_name = name;
|
|
326
|
-
if (ca.customerName != null) delete ca.customerName;
|
|
327
|
-
}
|
|
328
|
-
channelConfigs.call = ca;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const next = { ...mergedPayload, channel_configs: channelConfigs };
|
|
332
|
-
|
|
333
|
-
if (senderKey && senderId) {
|
|
334
|
-
touchEntry(doc, senderKey, senderId, senderName);
|
|
335
|
-
const patch = {};
|
|
336
|
-
const smsCfg = channelConfigs.sms;
|
|
337
|
-
const pSms = normalizePhone(pickFirstString(smsCfg?.receiver, smsCfg?.phone));
|
|
338
|
-
if (pSms) patch.sms = { phone: pSms };
|
|
339
|
-
|
|
340
|
-
const dtCfg = channelConfigs.dingtalk;
|
|
341
|
-
const cas = normalizeCasId(pickFirstString(dtCfg?.cas_id, dtCfg?.casId));
|
|
342
|
-
if (cas) patch.dingtalk = { cas_id: cas };
|
|
343
|
-
|
|
344
|
-
const emCfg = channelConfigs.email;
|
|
345
|
-
const to = normalizeEmail(pickFirstString(emCfg?.to_address, emCfg?.toAddress));
|
|
346
|
-
if (to) patch.email = { to_address: to };
|
|
347
|
-
|
|
348
|
-
const caCfg = channelConfigs.call;
|
|
349
|
-
const cPhone = normalizePhone(pickFirstString(caCfg?.phone, caCfg?.receiver));
|
|
350
|
-
const cName = pickFirstString(caCfg?.customer_name, caCfg?.customerName);
|
|
351
|
-
if (cPhone || cName) {
|
|
352
|
-
patch.call = {
|
|
353
|
-
...(cPhone ? { phone: cPhone } : {}),
|
|
354
|
-
...(cName ? { customer_name: String(cName).trim() } : {})
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (Object.keys(patch).length > 0) {
|
|
359
|
-
const cur = doc.senders[senderKey];
|
|
360
|
-
cur.defaults = { ...(cur.defaults || {}), ...patch };
|
|
361
|
-
cur.updated_at = new Date().toISOString();
|
|
362
|
-
await saveContactsFile(filePath, doc);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return next;
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* 合并 notify.send 单渠道 payload。
|
|
372
|
-
*/
|
|
373
|
-
export async function mergeAndPersistNotifyContacts(filePath, notifyChannel, payload, context) {
|
|
374
|
-
const ch = String(notifyChannel || '')
|
|
375
|
-
.toLowerCase()
|
|
376
|
-
.trim();
|
|
377
|
-
const allowedNotify = new Set(['sms', 'email', 'call', 'dingtalk']);
|
|
378
|
-
if (!ch || !allowedNotify.has(ch)) {
|
|
379
|
-
return { ...(payload && typeof payload === 'object' ? payload : {}) };
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return runContactsFileSerialized(filePath, async () => {
|
|
383
|
-
let senderCtx = extractSenderContext({
|
|
384
|
-
context,
|
|
385
|
-
params: payload || {}
|
|
386
|
-
});
|
|
387
|
-
const routingHint = deriveOpenclawRouting({ params: payload || {}, context });
|
|
388
|
-
senderCtx = refineSessionFromOpenclawConfig(
|
|
389
|
-
senderCtx,
|
|
390
|
-
routingHint.channel ? { channel: routingHint.channel } : null
|
|
391
|
-
);
|
|
392
|
-
const { senderKey, senderId, senderName, channel: sessionChannel } = senderCtx;
|
|
393
|
-
const out = { ...(payload && typeof payload === 'object' ? payload : {}) };
|
|
394
|
-
|
|
395
|
-
let doc = await loadContactsFile(filePath);
|
|
396
|
-
const entry = getEntry(doc, senderKey);
|
|
397
|
-
const defaults = entry?.defaults && typeof entry.defaults === 'object' ? entry.defaults : {};
|
|
398
|
-
const isDingtalkSession = sessionChannel === 'dingtalk';
|
|
399
|
-
|
|
400
|
-
if (ch === 'sms') {
|
|
401
|
-
let phone = normalizePhone(pickFirstString(out.receiver, out.phone));
|
|
402
|
-
if (!phone && defaults.sms?.phone) {
|
|
403
|
-
phone = normalizePhone(defaults.sms.phone);
|
|
404
|
-
}
|
|
405
|
-
if (phone) {
|
|
406
|
-
out.receiver = phone;
|
|
407
|
-
if (out.phone != null) delete out.phone;
|
|
408
|
-
}
|
|
409
|
-
} else if (ch === 'dingtalk') {
|
|
410
|
-
const casId =
|
|
411
|
-
normalizeCasId(pickFirstString(out.cas_id, out.casId)) ||
|
|
412
|
-
normalizeCasId(defaults.dingtalk?.cas_id) ||
|
|
413
|
-
(isDingtalkSession ? normalizeCasId(senderId) : null);
|
|
414
|
-
if (casId) {
|
|
415
|
-
out.cas_id = casId;
|
|
416
|
-
if (out.casId != null) delete out.casId;
|
|
417
|
-
}
|
|
418
|
-
} else if (ch === 'email') {
|
|
419
|
-
const addr =
|
|
420
|
-
normalizeEmail(pickFirstString(out.to_address, out.toAddress)) ||
|
|
421
|
-
normalizeEmail(defaults.email?.to_address);
|
|
422
|
-
if (addr) {
|
|
423
|
-
out.to_address = addr;
|
|
424
|
-
if (out.toAddress != null) delete out.toAddress;
|
|
425
|
-
}
|
|
426
|
-
} else if (ch === 'call') {
|
|
427
|
-
let phone = normalizePhone(pickFirstString(out.phone, out.receiver));
|
|
428
|
-
if (!phone && defaults.call?.phone) {
|
|
429
|
-
phone = normalizePhone(defaults.call.phone);
|
|
430
|
-
}
|
|
431
|
-
if (phone) out.phone = phone;
|
|
432
|
-
|
|
433
|
-
let name = pickFirstString(out.customer_name, out.customerName);
|
|
434
|
-
if (!name && defaults.call?.customer_name) {
|
|
435
|
-
name = String(defaults.call.customer_name).trim();
|
|
436
|
-
}
|
|
437
|
-
if (!name && senderName) {
|
|
438
|
-
name = String(senderName).trim();
|
|
439
|
-
}
|
|
440
|
-
if (name) {
|
|
441
|
-
out.customer_name = name;
|
|
442
|
-
if (out.customerName != null) delete out.customerName;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (senderKey && senderId) {
|
|
447
|
-
touchEntry(doc, senderKey, senderId, senderName);
|
|
448
|
-
const patch = {};
|
|
449
|
-
if (ch === 'sms') {
|
|
450
|
-
const p = normalizePhone(pickFirstString(out.receiver, out.phone));
|
|
451
|
-
if (p) patch.sms = { phone: p };
|
|
452
|
-
} else if (ch === 'dingtalk') {
|
|
453
|
-
const c = normalizeCasId(pickFirstString(out.cas_id, out.casId));
|
|
454
|
-
if (c) patch.dingtalk = { cas_id: c };
|
|
455
|
-
} else if (ch === 'email') {
|
|
456
|
-
const t = normalizeEmail(pickFirstString(out.to_address, out.toAddress));
|
|
457
|
-
if (t) patch.email = { to_address: t };
|
|
458
|
-
} else if (ch === 'call') {
|
|
459
|
-
const p = normalizePhone(pickFirstString(out.phone, out.receiver));
|
|
460
|
-
const n = pickFirstString(out.customer_name, out.customerName);
|
|
461
|
-
if (p || n) {
|
|
462
|
-
patch.call = {
|
|
463
|
-
...(p ? { phone: p } : {}),
|
|
464
|
-
...(n ? { customer_name: String(n).trim() } : {})
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
if (Object.keys(patch).length > 0) {
|
|
469
|
-
const cur = doc.senders[senderKey];
|
|
470
|
-
cur.defaults = { ...(cur.defaults || {}), ...patch };
|
|
471
|
-
cur.updated_at = new Date().toISOString();
|
|
472
|
-
await saveContactsFile(filePath, doc);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return out;
|
|
477
|
-
});
|
|
478
|
-
}
|