cc-viewer 1.6.294 → 1.6.295
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/cli.js +7 -2
- package/dist/assets/App-Br-u2TKk.js +2 -0
- package/dist/assets/{MdxEditorPanel-B8xrlDZJ.js → MdxEditorPanel-Cy4egsQx.js} +1 -1
- package/dist/assets/{Mobile-fsi8-Lpb.js → Mobile-ZHF74GQs.js} +1 -1
- package/dist/assets/index-DMuCrfTo.js +2 -0
- package/dist/assets/seqResourceLoaders-C7X23dCJ.js +2 -0
- package/dist/index.html +1 -1
- package/findcc.js +3 -3
- package/package.json +1 -1
- package/server/i18n.js +224 -8
- package/server/interceptor.js +21 -18
- package/server/lib/adapters/dingtalk-adapter.js +62 -0
- package/server/lib/adapters/discord-adapter.js +35 -0
- package/server/lib/adapters/feishu-adapter.js +37 -0
- package/server/lib/ask-store.js +19 -90
- package/server/lib/async-file-lock.js +123 -0
- package/server/lib/async-write-queue.js +131 -0
- package/server/lib/git-diff.js +4 -1
- package/server/lib/im-bridge-core.js +119 -14
- package/server/lib/im-config.js +11 -6
- package/server/lib/im-process-manager.js +1 -1
- package/server/lib/jsonl-archive.js +0 -1
- package/server/lib/log-watcher.js +224 -177
- package/server/lib/plugin-manager.js +1 -1
- package/server/lib/updater.js +4 -2
- package/server/pty-manager.js +1 -1
- package/server/routes/ask-perm.js +2 -2
- package/server/routes/dingtalk.js +2 -0
- package/server/routes/files-fs.js +4 -4
- package/server/routes/project-meta.js +18 -1
- package/server/routes/workspaces.js +7 -10
- package/server/server.js +23 -20
- package/server/workspace-registry.js +9 -53
- package/dist/assets/App-C66LoBEz.js +0 -2
- package/dist/assets/index-BTZqk5O5.js +0 -2
- package/dist/assets/seqResourceLoaders-6k4uXcNn.js +0 -2
|
@@ -30,7 +30,9 @@ const SEEN_MAX = 500;
|
|
|
30
30
|
const RATE_WINDOW_MS = 60_000;
|
|
31
31
|
const MAX_CHUNKS_PER_TURN = 5;
|
|
32
32
|
const MAX_QUEUE = 50; // cap inbound backlog so an authorized sender can't grow it unbounded
|
|
33
|
-
const PENDING_TIMEOUT_MS =
|
|
33
|
+
const PENDING_TIMEOUT_MS = process.env.CCV_IM_PLATFORM ? 2 * 60_000 : 10 * 60_000;
|
|
34
|
+
const IDLE_POLL_INTERVAL_MS = 5_000; // check every 5s if streaming stopped
|
|
35
|
+
const IDLE_POLL_THRESHOLD = 3; // 3 consecutive idle ticks (15s) → synthetic turn_end
|
|
34
36
|
const CONNECT_TIMEOUT_MS = 15_000; // bound adapter.connect() so a hung start can't block others
|
|
35
37
|
const STOP_WORDS = new Set(['/stop', 'stop', '停止', 'esc', '/esc']);
|
|
36
38
|
|
|
@@ -38,6 +40,8 @@ const STOP_WORDS = new Set(['/stop', 'stop', '停止', 'esc', '/esc']);
|
|
|
38
40
|
const instances = new Map(); // platformId → instance
|
|
39
41
|
let activeInjection = null; // { platformId, since, target } — the one in-flight turn
|
|
40
42
|
let activeInjectionTimer = null; // self-heal timer if a turn_end never arrives
|
|
43
|
+
let idlePollTimer = null; // secondary idle detection (IM worker only)
|
|
44
|
+
let idlePollCount = 0; // consecutive ticks where isStreaming() is false
|
|
41
45
|
let fetchImpl = null; // shared test seam
|
|
42
46
|
|
|
43
47
|
// ─── test seams ───
|
|
@@ -59,6 +63,7 @@ function newInstance(adapter) {
|
|
|
59
63
|
queue: [],
|
|
60
64
|
sendTimes: [],
|
|
61
65
|
store: {}, // adapter scratch (token cache, send client)
|
|
66
|
+
ackCardPromise: null, // Promise<handle|null> for the in-flight ack card
|
|
62
67
|
};
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -143,22 +148,63 @@ function queueCap(inst) {
|
|
|
143
148
|
return inst.maxQueueOverride ?? MAX_QUEUE;
|
|
144
149
|
}
|
|
145
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Await the in-flight ack card promise, update it with terminal text/status, and null
|
|
153
|
+
* the promise on `inst`. Returns true if the card was successfully updated. Best-effort:
|
|
154
|
+
* never throws, never blocks the slot release.
|
|
155
|
+
*/
|
|
156
|
+
async function finalizeAckCard(inst, target, text, status) {
|
|
157
|
+
try {
|
|
158
|
+
const handle = await inst.ackCardPromise?.catch(() => null);
|
|
159
|
+
inst.ackCardPromise = null;
|
|
160
|
+
if (handle && typeof inst.adapter.updateAckCard === 'function') {
|
|
161
|
+
const cfg = inst.bridgeDeps?.getConfig();
|
|
162
|
+
if (cfg) return !!(await inst.adapter.updateAckCard(cfg, target, handle, text, status, ctxFor(inst)).catch(() => false));
|
|
163
|
+
}
|
|
164
|
+
} catch { /* best-effort */ }
|
|
165
|
+
inst.ackCardPromise = null;
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
146
169
|
// ─── activeInjection lifecycle ───
|
|
147
170
|
function clearActiveInjection() {
|
|
148
171
|
activeInjection = null;
|
|
149
172
|
if (activeInjectionTimer) { clearTimeout(activeInjectionTimer); activeInjectionTimer = null; }
|
|
173
|
+
if (idlePollTimer) { clearInterval(idlePollTimer); idlePollTimer = null; }
|
|
174
|
+
idlePollCount = 0;
|
|
150
175
|
}
|
|
151
176
|
function armActiveInjection(inst, target, since) {
|
|
152
|
-
activeInjection = { platformId: inst.adapter.id, since, target };
|
|
177
|
+
activeInjection = { platformId: inst.adapter.id, since, target, transcriptPath: null };
|
|
153
178
|
if (activeInjectionTimer) clearTimeout(activeInjectionTimer);
|
|
154
|
-
activeInjectionTimer = setTimeout(() => {
|
|
179
|
+
activeInjectionTimer = setTimeout(async () => {
|
|
155
180
|
// Only fire if THIS injection still owns the slot (symmetry with the inject-failure guard).
|
|
156
181
|
if (!activeInjection || activeInjection.since !== since) return;
|
|
157
182
|
audit(inst, 'reply-timeout', { conversationId: target?.conversationId });
|
|
158
|
-
|
|
183
|
+
const cardUpdated = await finalizeAckCard(inst, target, tr(inst, 'ackTimeout'), 'error');
|
|
184
|
+
if (!activeInjection || activeInjection.since !== since) return;
|
|
185
|
+
clearActiveInjection();
|
|
186
|
+
if (!cardUpdated) void sendReply(inst, target, tr(inst, 'ackTimeout'));
|
|
159
187
|
drainAll(); // …and let any platform's queue proceed
|
|
160
188
|
}, PENDING_TIMEOUT_MS);
|
|
161
189
|
if (typeof activeInjectionTimer.unref === 'function') activeInjectionTimer.unref();
|
|
190
|
+
// Secondary idle detection: poll isStreaming() to catch missed Stop hook events.
|
|
191
|
+
if (idlePollTimer) clearInterval(idlePollTimer);
|
|
192
|
+
idlePollCount = 0;
|
|
193
|
+
let sawStreaming = false;
|
|
194
|
+
idlePollTimer = setInterval(() => {
|
|
195
|
+
if (!activeInjection || activeInjection.since !== since) { clearInterval(idlePollTimer); idlePollTimer = null; return; }
|
|
196
|
+
const d = inst.bridgeDeps;
|
|
197
|
+
if (!d) return;
|
|
198
|
+
if (d.isStreaming()) { sawStreaming = true; idlePollCount = 0; return; }
|
|
199
|
+
if (!sawStreaming) return; // haven't seen streaming start yet — don't count idle
|
|
200
|
+
idlePollCount++;
|
|
201
|
+
if (idlePollCount >= IDLE_POLL_THRESHOLD) {
|
|
202
|
+
clearInterval(idlePollTimer); idlePollTimer = null;
|
|
203
|
+
audit(inst, 'idle-turn-end', { conversationId: target?.conversationId, idleSeconds: idlePollCount * IDLE_POLL_INTERVAL_MS / 1000 });
|
|
204
|
+
notifyTurnEnd(null, since, activeInjection?.transcriptPath || null);
|
|
205
|
+
}
|
|
206
|
+
}, IDLE_POLL_INTERVAL_MS);
|
|
207
|
+
if (typeof idlePollTimer.unref === 'function') idlePollTimer.unref();
|
|
162
208
|
}
|
|
163
209
|
|
|
164
210
|
// ─── small helpers ───
|
|
@@ -212,8 +258,8 @@ function isStopCommand(text) {
|
|
|
212
258
|
return STOP_WORDS.has(text.trim().toLowerCase());
|
|
213
259
|
}
|
|
214
260
|
|
|
215
|
-
function tr(inst, key) {
|
|
216
|
-
return t(`${inst.adapter.i18nNs}.${key}
|
|
261
|
+
function tr(inst, key, params) {
|
|
262
|
+
return t(`${inst.adapter.i18nNs}.${key}`, params);
|
|
217
263
|
}
|
|
218
264
|
|
|
219
265
|
// ─── transcript extraction (the safe outbound text source) ───
|
|
@@ -369,6 +415,11 @@ function handleInboundInner(inst, normalized) {
|
|
|
369
415
|
if (isStopCommand(text)) {
|
|
370
416
|
inst.bridgeDeps.writeToPty('\x1b'); // ESC interrupts the current turn (NOT killPty)
|
|
371
417
|
audit(inst, 'stop', { conversationId });
|
|
418
|
+
const stoppedInst = activeInjection ? instances.get(activeInjection.platformId) : null;
|
|
419
|
+
const stoppedTarget = activeInjection?.target;
|
|
420
|
+
if (stoppedInst && stoppedTarget) {
|
|
421
|
+
void finalizeAckCard(stoppedInst, stoppedTarget, tr(stoppedInst, 'interrupted'), 'interrupted');
|
|
422
|
+
}
|
|
372
423
|
// ESC interrupts whatever turn is live on the shared PTY (possibly another platform's), which
|
|
373
424
|
// may mean its turn_end never fires. Release the global slot and resume all queues so /stop
|
|
374
425
|
// can never wedge the bridge.
|
|
@@ -380,12 +431,13 @@ function handleInboundInner(inst, normalized) {
|
|
|
380
431
|
|
|
381
432
|
if (inst.queue.length >= queueCap(inst)) {
|
|
382
433
|
audit(inst, 'queue-full', { conversationId, queued: inst.queue.length });
|
|
383
|
-
void sendReply(inst, target, tr(inst, 'queueFull'));
|
|
434
|
+
void sendReply(inst, target, tr(inst, 'queueFull', { max: String(queueCap(inst)) }));
|
|
384
435
|
return;
|
|
385
436
|
}
|
|
386
437
|
inst.queue.push({ ...target, senderId, content: text });
|
|
387
438
|
if (activeInjection || inst.bridgeDeps.isStreaming()) {
|
|
388
|
-
|
|
439
|
+
const ahead = inst.queue.length - 1;
|
|
440
|
+
void sendReply(inst, target, tr(inst, 'busyQueued', { ahead: String(ahead), max: String(queueCap(inst)) }));
|
|
389
441
|
}
|
|
390
442
|
drainQueue(inst);
|
|
391
443
|
}
|
|
@@ -419,7 +471,19 @@ function drainQueue(inst) {
|
|
|
419
471
|
}
|
|
420
472
|
const since = Date.now();
|
|
421
473
|
armActiveInjection(inst, item, since);
|
|
422
|
-
|
|
474
|
+
// Instant ack: fire-and-forget so writeToPtySequential is never delayed.
|
|
475
|
+
if (cfg.ackCard !== false && typeof inst.adapter.sendAckCard === 'function') {
|
|
476
|
+
const ackTarget = item;
|
|
477
|
+
inst.ackCardPromise = inst.adapter.sendAckCard(cfg, item, tr(inst, 'ackProcessing'), ctxFor(inst))
|
|
478
|
+
.then((handle) => { if (!handle) void sendReply(inst, ackTarget, tr(inst, 'ackProcessing')); return handle; })
|
|
479
|
+
.catch((e) => { audit(inst, 'ack-card-error', { error: String(e?.message || e) }); void sendReply(inst, ackTarget, tr(inst, 'ackProcessing')); return null; });
|
|
480
|
+
} else if (cfg.ackCard !== false) {
|
|
481
|
+
void sendReply(inst, item, tr(inst, 'ackProcessing'));
|
|
482
|
+
inst.ackCardPromise = null;
|
|
483
|
+
} else {
|
|
484
|
+
inst.ackCardPromise = null;
|
|
485
|
+
}
|
|
486
|
+
if (!isImWorker && skipPerm && cfg.ackCard === false) {
|
|
423
487
|
audit(inst, 'skip-perm-warning', { conversationId: item.conversationId });
|
|
424
488
|
void sendReply(inst, item, tr(inst, 'skipPermWarning'));
|
|
425
489
|
}
|
|
@@ -430,9 +494,13 @@ function drainQueue(inst) {
|
|
|
430
494
|
if (ok) return;
|
|
431
495
|
if (!activeInjection || activeInjection.platformId !== inst.adapter.id || activeInjection.since !== since) return;
|
|
432
496
|
audit(inst, 'inject-failed', { conversationId: item.conversationId });
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
497
|
+
void (async () => {
|
|
498
|
+
const cardUpdated = await finalizeAckCard(inst, item, tr(inst, 'injectFailed'), 'error');
|
|
499
|
+
if (!activeInjection || activeInjection.platformId !== inst.adapter.id || activeInjection.since !== since) return;
|
|
500
|
+
clearActiveInjection();
|
|
501
|
+
if (!cardUpdated) void sendReply(inst, item, tr(inst, 'injectFailed'));
|
|
502
|
+
drainAll();
|
|
503
|
+
})().catch(() => {});
|
|
436
504
|
}, { settleMs: 250 });
|
|
437
505
|
return; // one at a time; resume on the next turn_end
|
|
438
506
|
}
|
|
@@ -447,6 +515,7 @@ function drainAll() {
|
|
|
447
515
|
|
|
448
516
|
// ─── outbound trigger (called from server.js _emitTurnEnd) ───
|
|
449
517
|
export async function notifyTurnEnd(sessionId, ts, transcriptPath) {
|
|
518
|
+
if (transcriptPath && activeInjection) activeInjection.transcriptPath = transcriptPath;
|
|
450
519
|
if (!activeInjection) { drainAll(); return; } // only reply to turns a bridge initiated
|
|
451
520
|
const inst = instances.get(activeInjection.platformId);
|
|
452
521
|
if (!inst) { clearActiveInjection(); drainAll(); return; }
|
|
@@ -455,6 +524,9 @@ export async function notifyTurnEnd(sessionId, ts, transcriptPath) {
|
|
|
455
524
|
// correlation is a v2 item.)
|
|
456
525
|
if (ts && activeInjection.since && ts < activeInjection.since) { drainAll(); return; }
|
|
457
526
|
const target = activeInjection.target;
|
|
527
|
+
// Grab the ack card promise before clearing state.
|
|
528
|
+
const ackP = inst.ackCardPromise;
|
|
529
|
+
inst.ackCardPromise = null;
|
|
458
530
|
clearActiveInjection();
|
|
459
531
|
// Idempotency for a doubled turn_end of the SAME turn (a re-broadcast carries the same ts).
|
|
460
532
|
// Keyed on ts, NOT reply text.
|
|
@@ -466,8 +538,36 @@ export async function notifyTurnEnd(sessionId, ts, transcriptPath) {
|
|
|
466
538
|
drainAll();
|
|
467
539
|
let text = extractLastAssistantText(transcriptPath);
|
|
468
540
|
if (!text) text = tr(inst, 'noTextReply');
|
|
469
|
-
|
|
470
|
-
|
|
541
|
+
|
|
542
|
+
// Try to update the ack card in-place with the reply. Fall back to sendReply on failure.
|
|
543
|
+
const handle = await ackP?.catch(() => null);
|
|
544
|
+
if (handle && typeof inst.adapter.updateAckCard === 'function') {
|
|
545
|
+
const cfg = inst.bridgeDeps.getConfig();
|
|
546
|
+
let chunks = chunkText(text, cfg.maxChunkChars);
|
|
547
|
+
if (chunks.length > MAX_CHUNKS_PER_TURN) {
|
|
548
|
+
chunks = chunks.slice(0, MAX_CHUNKS_PER_TURN);
|
|
549
|
+
chunks[MAX_CHUNKS_PER_TURN - 1] += '\n\n' + tr(inst, 'truncated');
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const updated = await inst.adapter.updateAckCard(cfg, target, handle, chunks[0] || text, 'done', ctxFor(inst));
|
|
553
|
+
if (updated && chunks.length > 1) {
|
|
554
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
555
|
+
try { await rateLimitGate(inst); await inst.adapter.sendOne(cfg, target, chunks[i], ctxFor(inst)); }
|
|
556
|
+
catch (e) { inst.lastError = String(e?.message || e); audit(inst, 'send-error', { error: inst.lastError }); break; }
|
|
557
|
+
}
|
|
558
|
+
} else if (!updated) {
|
|
559
|
+
await sendReply(inst, target, text);
|
|
560
|
+
}
|
|
561
|
+
audit(inst, 'out', { conversationId: target.conversationId, chunks: chunks.length, cardUpdated: !!updated });
|
|
562
|
+
} catch (e) {
|
|
563
|
+
inst.lastError = String(e?.message || e);
|
|
564
|
+
audit(inst, 'card-update-error', { error: inst.lastError });
|
|
565
|
+
try { await sendReply(inst, target, text); } catch { /* already logged in sendReply */ }
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
try { await sendReply(inst, target, text); }
|
|
569
|
+
catch (e) { inst.lastError = String(e?.message || e); audit(inst, 'send-error', { error: inst.lastError }); }
|
|
570
|
+
}
|
|
471
571
|
}
|
|
472
572
|
|
|
473
573
|
// ─── per-platform lifecycle ───
|
|
@@ -504,6 +604,11 @@ export async function startBridge(id, deps) {
|
|
|
504
604
|
export async function stopBridge(id) {
|
|
505
605
|
const inst = instances.get(id);
|
|
506
606
|
if (!inst) return;
|
|
607
|
+
if (inst.ackCardPromise && activeInjection?.platformId === id) {
|
|
608
|
+
await finalizeAckCard(inst, activeInjection.target, tr(inst, 'noSession'), 'error');
|
|
609
|
+
} else {
|
|
610
|
+
inst.ackCardPromise = null;
|
|
611
|
+
}
|
|
507
612
|
try { await inst.adapter.disconnect?.(inst.client, ctxFor(inst)); } catch { /* best-effort */ }
|
|
508
613
|
inst.client = null;
|
|
509
614
|
inst.running = false;
|
package/server/lib/im-config.js
CHANGED
|
@@ -34,7 +34,7 @@ const DESCRIPTORS = {
|
|
|
34
34
|
allowListField: 'allowStaffIds',
|
|
35
35
|
defaults: {
|
|
36
36
|
enabled: false, appKey: '', appSecret: '', allowStaffIds: [],
|
|
37
|
-
maxChunkChars: 3800, blockOnSkipPermissions: false,
|
|
37
|
+
maxChunkChars: 3800, blockOnSkipPermissions: false, ackCard: true, cardTemplateId: '',
|
|
38
38
|
},
|
|
39
39
|
fields: [
|
|
40
40
|
{ key: 'enabled', type: 'bool' },
|
|
@@ -43,6 +43,8 @@ const DESCRIPTORS = {
|
|
|
43
43
|
{ key: 'allowStaffIds', type: 'idlist' },
|
|
44
44
|
{ key: 'maxChunkChars', type: 'chunk' },
|
|
45
45
|
{ key: 'blockOnSkipPermissions', type: 'bool' },
|
|
46
|
+
{ key: 'ackCard', type: 'bool', default: true },
|
|
47
|
+
{ key: 'cardTemplateId', type: 'string' },
|
|
46
48
|
],
|
|
47
49
|
},
|
|
48
50
|
feishu: {
|
|
@@ -50,7 +52,7 @@ const DESCRIPTORS = {
|
|
|
50
52
|
allowListField: 'allowUserIds',
|
|
51
53
|
defaults: {
|
|
52
54
|
enabled: false, appId: '', appSecret: '', region: 'feishu', allowUserIds: [],
|
|
53
|
-
maxChunkChars: 3800, blockOnSkipPermissions: false,
|
|
55
|
+
maxChunkChars: 3800, blockOnSkipPermissions: false, ackCard: true,
|
|
54
56
|
},
|
|
55
57
|
fields: [
|
|
56
58
|
{ key: 'enabled', type: 'bool' },
|
|
@@ -60,6 +62,7 @@ const DESCRIPTORS = {
|
|
|
60
62
|
{ key: 'allowUserIds', type: 'idlist' },
|
|
61
63
|
{ key: 'maxChunkChars', type: 'chunk' },
|
|
62
64
|
{ key: 'blockOnSkipPermissions', type: 'bool' },
|
|
65
|
+
{ key: 'ackCard', type: 'bool', default: true },
|
|
63
66
|
],
|
|
64
67
|
},
|
|
65
68
|
wecom: {
|
|
@@ -67,7 +70,7 @@ const DESCRIPTORS = {
|
|
|
67
70
|
allowListField: 'allowUserIds',
|
|
68
71
|
defaults: {
|
|
69
72
|
enabled: false, botId: '', secret: '', allowUserIds: [],
|
|
70
|
-
maxChunkChars: 3800, blockOnSkipPermissions: false,
|
|
73
|
+
maxChunkChars: 3800, blockOnSkipPermissions: false, ackCard: true,
|
|
71
74
|
},
|
|
72
75
|
fields: [
|
|
73
76
|
{ key: 'enabled', type: 'bool' },
|
|
@@ -76,6 +79,7 @@ const DESCRIPTORS = {
|
|
|
76
79
|
{ key: 'allowUserIds', type: 'idlist' },
|
|
77
80
|
{ key: 'maxChunkChars', type: 'chunk' },
|
|
78
81
|
{ key: 'blockOnSkipPermissions', type: 'bool' },
|
|
82
|
+
{ key: 'ackCard', type: 'bool', default: true },
|
|
79
83
|
],
|
|
80
84
|
},
|
|
81
85
|
discord: {
|
|
@@ -84,7 +88,7 @@ const DESCRIPTORS = {
|
|
|
84
88
|
defaults: {
|
|
85
89
|
// 1900 < Discord's hard 2000-char/message limit (the adapter also hard-splits as defense).
|
|
86
90
|
enabled: false, botToken: '', allowUserIds: [],
|
|
87
|
-
maxChunkChars: 1900, blockOnSkipPermissions: false,
|
|
91
|
+
maxChunkChars: 1900, blockOnSkipPermissions: false, ackCard: true,
|
|
88
92
|
},
|
|
89
93
|
fields: [
|
|
90
94
|
{ key: 'enabled', type: 'bool' },
|
|
@@ -92,6 +96,7 @@ const DESCRIPTORS = {
|
|
|
92
96
|
{ key: 'allowUserIds', type: 'idlist' },
|
|
93
97
|
{ key: 'maxChunkChars', type: 'chunk', default: 1900 }, // < Discord's 2000-char limit
|
|
94
98
|
{ key: 'blockOnSkipPermissions', type: 'bool' },
|
|
99
|
+
{ key: 'ackCard', type: 'bool', default: true },
|
|
95
100
|
],
|
|
96
101
|
},
|
|
97
102
|
};
|
|
@@ -155,7 +160,7 @@ function normalizeIdList(v) {
|
|
|
155
160
|
|
|
156
161
|
function normField(type, v, dflt) {
|
|
157
162
|
switch (type) {
|
|
158
|
-
case 'bool': return !!v;
|
|
163
|
+
case 'bool': return v !== undefined && v !== null ? !!v : (dflt !== undefined ? !!dflt : false);
|
|
159
164
|
case 'cred':
|
|
160
165
|
case 'secret': return typeof v === 'string' ? v.trim() : '';
|
|
161
166
|
case 'idlist': return normalizeIdList(v);
|
|
@@ -169,7 +174,7 @@ function decodeField(type, v, dflt) {
|
|
|
169
174
|
switch (type) {
|
|
170
175
|
case 'cred':
|
|
171
176
|
case 'secret': return decodeSecret(v);
|
|
172
|
-
case 'bool': return !!v;
|
|
177
|
+
case 'bool': return v !== undefined && v !== null ? !!v : (dflt !== undefined ? !!dflt : false);
|
|
173
178
|
case 'idlist': return normalizeIdList(v);
|
|
174
179
|
case 'chunk': return clampChunk(v, dflt);
|
|
175
180
|
case 'region': return v === 'lark' ? 'lark' : 'feishu';
|
|
@@ -22,7 +22,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
22
22
|
export function resolveNodeBinary() {
|
|
23
23
|
if (!process.versions.electron) return process.execPath;
|
|
24
24
|
try {
|
|
25
|
-
const out = execSync(process.platform === 'win32' ? 'where node' : 'which node', { encoding: 'utf-8' });
|
|
25
|
+
const out = execSync(process.platform === 'win32' ? 'where node' : 'which node', { encoding: 'utf-8', windowsHide: true });
|
|
26
26
|
const p = process.platform === 'win32' ? out.split('\n')[0].trim() : out.trim();
|
|
27
27
|
if (p) return p;
|
|
28
28
|
} catch { /* fall through */ }
|
|
@@ -20,7 +20,6 @@ const CACHE_TTL_MS = 7 * 24 * 3600 * 1000;
|
|
|
20
20
|
const ARCHIVE_MAX_BYTES = 400 * 1024 * 1024;
|
|
21
21
|
|
|
22
22
|
function syncSleep(ms) {
|
|
23
|
-
// Atomics.wait 在主线程上真睡眠(不像 busy-wait 占满 CPU 或阻塞 event-loop heuristics)
|
|
24
23
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
25
24
|
}
|
|
26
25
|
|