@yushaw/sanqian-chat 0.2.32 → 0.2.34
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/dist/core/index.d.mts +10 -2
- package/dist/core/index.d.ts +10 -2
- package/dist/core/index.js +38 -25
- package/dist/core/index.mjs +38 -25
- package/dist/main/index.d.mts +8 -0
- package/dist/main/index.d.ts +8 -0
- package/dist/main/index.js +537 -499
- package/dist/main/index.mjs +537 -499
- package/dist/preload/factories.d.mts +1 -0
- package/dist/preload/factories.d.ts +1 -0
- package/dist/renderer/index.d.mts +11 -1
- package/dist/renderer/index.d.ts +11 -1
- package/dist/renderer/index.js +235 -144
- package/dist/renderer/index.mjs +266 -175
- package/package.json +6 -6
package/dist/main/index.js
CHANGED
|
@@ -368,6 +368,53 @@ var import_os = __toESM(require("os"));
|
|
|
368
368
|
var import_path = __toESM(require("path"));
|
|
369
369
|
|
|
370
370
|
// src/main/hitl.ts
|
|
371
|
+
var DEFAULT_RUN_OWNER_INDEX_MAX_SIZE = 2048;
|
|
372
|
+
function wait(ms) {
|
|
373
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
374
|
+
}
|
|
375
|
+
function createOwnedHitlStreamState(ownerWebContentsId) {
|
|
376
|
+
return {
|
|
377
|
+
cancelled: false,
|
|
378
|
+
runId: null,
|
|
379
|
+
cancelSignalSent: false,
|
|
380
|
+
ownerWebContentsId
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function updateOwnedStreamRunId(stream, runId, rememberRunOwner, options = {}) {
|
|
384
|
+
if (typeof runId !== "string") {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const normalizedRunId = runId.trim();
|
|
388
|
+
if (!normalizedRunId) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
if (!stream.runId || options.overwrite) {
|
|
392
|
+
stream.runId = normalizedRunId;
|
|
393
|
+
}
|
|
394
|
+
rememberRunOwner(normalizedRunId, stream.ownerWebContentsId);
|
|
395
|
+
return normalizedRunId;
|
|
396
|
+
}
|
|
397
|
+
function resolveCancelledStreamLoopAction(stream, sendCancelRun, onCancelError) {
|
|
398
|
+
if (stream.cancelled && stream.runId && !stream.cancelSignalSent) {
|
|
399
|
+
try {
|
|
400
|
+
sendCancelRun(stream.runId);
|
|
401
|
+
stream.cancelSignalSent = true;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
onCancelError?.(error);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!stream.cancelled) {
|
|
407
|
+
return "process";
|
|
408
|
+
}
|
|
409
|
+
return stream.cancelSignalSent ? "break" : "continue";
|
|
410
|
+
}
|
|
411
|
+
function emitStreamErrorAndMarkTerminal(stream, error, emitErrorEvent) {
|
|
412
|
+
if (stream.cancelled) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
emitErrorEvent(error instanceof Error ? error.message : "Stream error");
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
371
418
|
function resolveHitlRunIdFromStreams(params, activeStreams) {
|
|
372
419
|
if (params.runId) {
|
|
373
420
|
return params.runId;
|
|
@@ -390,27 +437,38 @@ function resolveHitlRunIdFromStreams(params, activeStreams) {
|
|
|
390
437
|
const first = activeRunIds.values().next();
|
|
391
438
|
return first.done ? null : first.value;
|
|
392
439
|
}
|
|
393
|
-
function resolveOwnedHitlRunIdFromStreams(params, activeStreams, senderWebContentsId) {
|
|
394
|
-
|
|
395
|
-
const stream = activeStreams.get(params.streamId);
|
|
396
|
-
if (!stream || stream.cancelled) {
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
if (stream.ownerWebContentsId !== senderWebContentsId) {
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
402
|
-
return stream.runId ?? null;
|
|
403
|
-
}
|
|
404
|
-
if (params.runId) {
|
|
440
|
+
function resolveOwnedHitlRunIdFromStreams(params, activeStreams, senderWebContentsId, runOwnerByRunId) {
|
|
441
|
+
const resolveRunIdByOwnership = (runId) => {
|
|
405
442
|
for (const stream of activeStreams.values()) {
|
|
406
443
|
if (stream.cancelled || !stream.runId) continue;
|
|
407
444
|
if (stream.ownerWebContentsId !== senderWebContentsId) continue;
|
|
408
|
-
if (stream.runId ===
|
|
409
|
-
return
|
|
445
|
+
if (stream.runId === runId) {
|
|
446
|
+
return runId;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (runOwnerByRunId?.get(runId) === senderWebContentsId) {
|
|
450
|
+
return runId;
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
453
|
+
};
|
|
454
|
+
if (params.streamId) {
|
|
455
|
+
const stream = activeStreams.get(params.streamId);
|
|
456
|
+
if (stream) {
|
|
457
|
+
if (stream.ownerWebContentsId !== senderWebContentsId) {
|
|
458
|
+
return null;
|
|
410
459
|
}
|
|
460
|
+
if (!stream.cancelled && stream.runId) {
|
|
461
|
+
return stream.runId;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (params.runId) {
|
|
465
|
+
return resolveRunIdByOwnership(params.runId);
|
|
411
466
|
}
|
|
412
467
|
return null;
|
|
413
468
|
}
|
|
469
|
+
if (params.runId) {
|
|
470
|
+
return resolveRunIdByOwnership(params.runId);
|
|
471
|
+
}
|
|
414
472
|
const activeRunIds = /* @__PURE__ */ new Set();
|
|
415
473
|
for (const stream of activeStreams.values()) {
|
|
416
474
|
if (stream.cancelled || !stream.runId) continue;
|
|
@@ -423,6 +481,367 @@ function resolveOwnedHitlRunIdFromStreams(params, activeStreams, senderWebConten
|
|
|
423
481
|
const first = activeRunIds.values().next();
|
|
424
482
|
return first.done ? null : first.value;
|
|
425
483
|
}
|
|
484
|
+
function rememberRunOwnerWithLru(runOwnerByRunId, runId, ownerWebContentsId, maxSize = DEFAULT_RUN_OWNER_INDEX_MAX_SIZE) {
|
|
485
|
+
if (!runId) return;
|
|
486
|
+
if (runOwnerByRunId.has(runId)) {
|
|
487
|
+
runOwnerByRunId.delete(runId);
|
|
488
|
+
}
|
|
489
|
+
runOwnerByRunId.set(runId, ownerWebContentsId);
|
|
490
|
+
while (runOwnerByRunId.size > maxSize) {
|
|
491
|
+
const oldestRunId = runOwnerByRunId.keys().next();
|
|
492
|
+
if (oldestRunId.done) break;
|
|
493
|
+
runOwnerByRunId.delete(oldestRunId.value);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function waitForOwnedStreamRunId(streamId, ownerWebContentsId, activeStreams, timeoutMs = 1200, pollIntervalMs = 20) {
|
|
497
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
498
|
+
while (Date.now() <= deadline) {
|
|
499
|
+
const stream2 = activeStreams.get(streamId);
|
|
500
|
+
if (!stream2 || stream2.cancelled || stream2.ownerWebContentsId !== ownerWebContentsId) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
if (stream2.runId) {
|
|
504
|
+
return stream2.runId;
|
|
505
|
+
}
|
|
506
|
+
await wait(pollIntervalMs);
|
|
507
|
+
}
|
|
508
|
+
const stream = activeStreams.get(streamId);
|
|
509
|
+
if (!stream || stream.cancelled || stream.ownerWebContentsId !== ownerWebContentsId) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
return stream.runId ?? null;
|
|
513
|
+
}
|
|
514
|
+
function shouldForgetRunOwnerAfterStreamEnd(stream, sawTerminalEvent) {
|
|
515
|
+
if (!stream.runId) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
return sawTerminalEvent || stream.cancelled;
|
|
519
|
+
}
|
|
520
|
+
function registerStreamIpcHandlers(ipcMainHandle, ctx) {
|
|
521
|
+
ipcMainHandle("sanqian-chat:stream", async (event, rawParams) => {
|
|
522
|
+
const params = rawParams;
|
|
523
|
+
const webContents = event.sender;
|
|
524
|
+
const { streamId, messages, conversationId, agentId: requestedAgentId, attachedResources, sessionResources } = params;
|
|
525
|
+
const sdk = ctx.getSdk();
|
|
526
|
+
const agentId = ctx.getAgentId(requestedAgentId);
|
|
527
|
+
if (!sdk || !agentId) {
|
|
528
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const activeStreams = ctx.getActiveStreams();
|
|
532
|
+
const streamState = createOwnedHitlStreamState(webContents.id);
|
|
533
|
+
activeStreams.set(streamId, streamState);
|
|
534
|
+
let sawTerminalEvent = false;
|
|
535
|
+
try {
|
|
536
|
+
await sdk.ensureReady();
|
|
537
|
+
const sdkMessages = messages.map((m) => ({ role: m.role, content: m.content }));
|
|
538
|
+
const validResources = attachedResources?.filter((r) => {
|
|
539
|
+
const parts = r.split(":");
|
|
540
|
+
return parts.length >= 3 && parts.every((p) => p.length > 0);
|
|
541
|
+
});
|
|
542
|
+
const chatOptions = {
|
|
543
|
+
conversationId,
|
|
544
|
+
persistHistory: true
|
|
545
|
+
};
|
|
546
|
+
if (validResources?.length) chatOptions.attachedResources = validResources;
|
|
547
|
+
if (sessionResources?.length) chatOptions.sessionResources = sessionResources;
|
|
548
|
+
const stream = sdk.chatStream(agentId, sdkMessages, chatOptions);
|
|
549
|
+
for await (const evt of stream) {
|
|
550
|
+
if (webContents.isDestroyed()) break;
|
|
551
|
+
const evtWithRunId = evt;
|
|
552
|
+
updateOwnedStreamRunId(
|
|
553
|
+
streamState,
|
|
554
|
+
evtWithRunId.run_id,
|
|
555
|
+
(runId, ownerWebContentsId) => ctx.rememberRunOwner(runId, ownerWebContentsId)
|
|
556
|
+
);
|
|
557
|
+
const cancelLoopAction = resolveCancelledStreamLoopAction(
|
|
558
|
+
streamState,
|
|
559
|
+
(runId) => sdk.cancelRun(runId),
|
|
560
|
+
(error) => {
|
|
561
|
+
console.warn(`[${ctx.logTag}] Failed to cancel run:`, error);
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
if (cancelLoopAction === "break") break;
|
|
565
|
+
if (cancelLoopAction === "continue") continue;
|
|
566
|
+
if (ctx.isDevMode()) {
|
|
567
|
+
console.log(`[${ctx.logTag}] SDK event:`, evt.type, JSON.stringify(evt).slice(0, 200));
|
|
568
|
+
}
|
|
569
|
+
switch (evt.type) {
|
|
570
|
+
case "start": {
|
|
571
|
+
const startEvt = evt;
|
|
572
|
+
updateOwnedStreamRunId(
|
|
573
|
+
streamState,
|
|
574
|
+
startEvt.run_id,
|
|
575
|
+
(runId, ownerWebContentsId) => ctx.rememberRunOwner(runId, ownerWebContentsId),
|
|
576
|
+
{ overwrite: true }
|
|
577
|
+
);
|
|
578
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "start", run_id: startEvt.run_id } });
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case "text":
|
|
582
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "text", content: evt.content } });
|
|
583
|
+
break;
|
|
584
|
+
case "thinking":
|
|
585
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "thinking", content: evt.content } });
|
|
586
|
+
break;
|
|
587
|
+
case "tool_call":
|
|
588
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_call", tool_call: evt.tool_call } });
|
|
589
|
+
break;
|
|
590
|
+
case "tool_result":
|
|
591
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_result", tool_call_id: evt.tool_call_id, result: evt.result } });
|
|
592
|
+
break;
|
|
593
|
+
case "done":
|
|
594
|
+
sawTerminalEvent = true;
|
|
595
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "done", conversationId: evt.conversationId, title: evt.title } });
|
|
596
|
+
break;
|
|
597
|
+
case "error":
|
|
598
|
+
sawTerminalEvent = true;
|
|
599
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: evt.error } });
|
|
600
|
+
break;
|
|
601
|
+
case "cancelled": {
|
|
602
|
+
sawTerminalEvent = true;
|
|
603
|
+
const cancelledEvt = evt;
|
|
604
|
+
webContents.send("sanqian-chat:streamEvent", {
|
|
605
|
+
streamId,
|
|
606
|
+
event: { type: "cancelled", run_id: cancelledEvt.run_id }
|
|
607
|
+
});
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
default: {
|
|
611
|
+
const anyEvt = evt;
|
|
612
|
+
if (anyEvt.type === "interrupt") {
|
|
613
|
+
const interruptRunId = anyEvt.run_id || streamState.runId || void 0;
|
|
614
|
+
if (interruptRunId) {
|
|
615
|
+
updateOwnedStreamRunId(
|
|
616
|
+
streamState,
|
|
617
|
+
interruptRunId,
|
|
618
|
+
(runId, ownerWebContentsId) => ctx.rememberRunOwner(runId, ownerWebContentsId),
|
|
619
|
+
{ overwrite: true }
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
webContents.send("sanqian-chat:streamEvent", {
|
|
623
|
+
streamId,
|
|
624
|
+
event: {
|
|
625
|
+
type: "interrupt",
|
|
626
|
+
interrupt_type: anyEvt.interrupt_type,
|
|
627
|
+
interrupt_payload: anyEvt.interrupt_payload,
|
|
628
|
+
run_id: interruptRunId
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
} catch (e) {
|
|
637
|
+
sawTerminalEvent = emitStreamErrorAndMarkTerminal(
|
|
638
|
+
streamState,
|
|
639
|
+
e,
|
|
640
|
+
(errorMessage) => {
|
|
641
|
+
if (!webContents.isDestroyed()) {
|
|
642
|
+
webContents.send("sanqian-chat:streamEvent", {
|
|
643
|
+
streamId,
|
|
644
|
+
event: { type: "error", error: errorMessage }
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
) || sawTerminalEvent;
|
|
649
|
+
} finally {
|
|
650
|
+
if (shouldForgetRunOwnerAfterStreamEnd(streamState, sawTerminalEvent)) {
|
|
651
|
+
ctx.forgetRunOwner(streamState.runId);
|
|
652
|
+
}
|
|
653
|
+
activeStreams.delete(streamId);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
ipcMainHandle("sanqian-chat:cancelStream", async (event, rawParams) => {
|
|
657
|
+
const params = rawParams;
|
|
658
|
+
const senderWebContentsId = event.sender.id;
|
|
659
|
+
const runIdHint = typeof params.runId === "string" && params.runId.trim().length > 0 ? params.runId.trim() : void 0;
|
|
660
|
+
const sdk = ctx.getSdk();
|
|
661
|
+
const activeStreams = ctx.getActiveStreams();
|
|
662
|
+
const stream = activeStreams.get(params.streamId);
|
|
663
|
+
if (!stream) {
|
|
664
|
+
if (sdk && runIdHint && ctx.getRunOwnerByRunId().get(runIdHint) === senderWebContentsId) {
|
|
665
|
+
try {
|
|
666
|
+
await sdk.cancelRun(runIdHint);
|
|
667
|
+
ctx.forgetRunOwner(runIdHint);
|
|
668
|
+
return { success: true };
|
|
669
|
+
} catch (e) {
|
|
670
|
+
console.warn(`[${ctx.logTag}] Failed to cancel fallback run:`, e);
|
|
671
|
+
return { success: false, error: "cancel_failed" };
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return { success: false, error: "stream_not_found" };
|
|
675
|
+
}
|
|
676
|
+
if (stream.ownerWebContentsId !== senderWebContentsId) {
|
|
677
|
+
console.warn(`[${ctx.logTag}] Rejecting cancelStream from non-owner sender`);
|
|
678
|
+
return { success: false, error: "stream_not_owned_by_sender" };
|
|
679
|
+
}
|
|
680
|
+
stream.cancelled = true;
|
|
681
|
+
const runId = stream.runId || runIdHint;
|
|
682
|
+
if (runId && !stream.cancelSignalSent) {
|
|
683
|
+
if (sdk) {
|
|
684
|
+
try {
|
|
685
|
+
await sdk.cancelRun(runId);
|
|
686
|
+
stream.cancelSignalSent = true;
|
|
687
|
+
ctx.rememberRunOwner(runId, senderWebContentsId);
|
|
688
|
+
} catch (e) {
|
|
689
|
+
console.warn(`[${ctx.logTag}] Failed to cancel run:`, e);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return { success: true };
|
|
694
|
+
});
|
|
695
|
+
ipcMainHandle("sanqian-chat:hitlResponse", async (event, rawParams) => {
|
|
696
|
+
const params = rawParams;
|
|
697
|
+
const sdk = ctx.getSdk();
|
|
698
|
+
const senderWebContentsId = event.sender.id;
|
|
699
|
+
const runId = ctx.resolveHitlRunId(
|
|
700
|
+
{ runId: params.runId, streamId: params.streamId },
|
|
701
|
+
senderWebContentsId
|
|
702
|
+
);
|
|
703
|
+
if (sdk && runId) {
|
|
704
|
+
ctx.rememberRunOwner(runId, senderWebContentsId);
|
|
705
|
+
sdk.sendHitlResponse(runId, params.response);
|
|
706
|
+
return { success: true };
|
|
707
|
+
}
|
|
708
|
+
if (params.streamId) {
|
|
709
|
+
const stream = ctx.getActiveStreams().get(params.streamId);
|
|
710
|
+
if (!stream) {
|
|
711
|
+
console.warn(`[${ctx.logTag}] HITL response dropped: stream not found`);
|
|
712
|
+
return { success: false, error: "stream_not_found" };
|
|
713
|
+
}
|
|
714
|
+
if (stream.cancelled) {
|
|
715
|
+
console.warn(`[${ctx.logTag}] HITL response dropped: stream already cancelled`);
|
|
716
|
+
return { success: false, error: "stream_cancelled" };
|
|
717
|
+
}
|
|
718
|
+
if (stream.ownerWebContentsId !== senderWebContentsId) {
|
|
719
|
+
console.warn(`[${ctx.logTag}] HITL response dropped: stream not owned by sender`);
|
|
720
|
+
return { success: false, error: "stream_not_owned_by_sender" };
|
|
721
|
+
}
|
|
722
|
+
if (!sdk) {
|
|
723
|
+
return { success: false, error: "sdk_not_ready" };
|
|
724
|
+
}
|
|
725
|
+
const resolvedRunId = await ctx.waitForOwnedStreamRunId(
|
|
726
|
+
params.streamId,
|
|
727
|
+
senderWebContentsId
|
|
728
|
+
);
|
|
729
|
+
if (resolvedRunId) {
|
|
730
|
+
ctx.rememberRunOwner(resolvedRunId, senderWebContentsId);
|
|
731
|
+
sdk.sendHitlResponse(resolvedRunId, params.response);
|
|
732
|
+
return { success: true };
|
|
733
|
+
}
|
|
734
|
+
if (ctx.isDevMode()) {
|
|
735
|
+
console.warn(
|
|
736
|
+
`[${ctx.logTag}] HITL response dropped: runId unresolved after wait window`,
|
|
737
|
+
params.streamId
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
return { success: false, error: "run_id_unresolved" };
|
|
741
|
+
}
|
|
742
|
+
if (ctx.isDevMode()) {
|
|
743
|
+
console.warn(`[${ctx.logTag}] HITL response dropped: missing or unauthorized runId`);
|
|
744
|
+
}
|
|
745
|
+
return { success: false, error: "run_id_unresolved" };
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/main/common-ipc.ts
|
|
750
|
+
function registerCommonChatIpcHandlers(handle, ctx) {
|
|
751
|
+
handle("sanqian-chat:connect", async () => {
|
|
752
|
+
try {
|
|
753
|
+
const sdk = ctx.getSdk();
|
|
754
|
+
if (!sdk) throw new Error("SDK not available");
|
|
755
|
+
await sdk.ensureReady();
|
|
756
|
+
ctx.onConnected?.();
|
|
757
|
+
return { success: true };
|
|
758
|
+
} catch (e) {
|
|
759
|
+
return { success: false, error: e instanceof Error ? e.message : "Connection failed" };
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
handle("sanqian-chat:isConnected", () => {
|
|
763
|
+
const sdk = ctx.getSdk();
|
|
764
|
+
return sdk?.isConnected() ?? false;
|
|
765
|
+
});
|
|
766
|
+
handle("sanqian-chat:listConversations", async (_, params) => {
|
|
767
|
+
const sdk = ctx.getSdk();
|
|
768
|
+
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
769
|
+
try {
|
|
770
|
+
const result = await sdk.listConversations({
|
|
771
|
+
limit: params?.limit,
|
|
772
|
+
offset: params?.offset
|
|
773
|
+
});
|
|
774
|
+
return { success: true, data: result };
|
|
775
|
+
} catch (e) {
|
|
776
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to list" };
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
handle("sanqian-chat:getConversation", async (_, params) => {
|
|
780
|
+
const sdk = ctx.getSdk();
|
|
781
|
+
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
782
|
+
try {
|
|
783
|
+
const result = await sdk.getConversation(params.conversationId, { messageLimit: params.messageLimit });
|
|
784
|
+
let messages = result?.messages;
|
|
785
|
+
const sdkWithHistory = sdk;
|
|
786
|
+
if (typeof sdkWithHistory.getMessages === "function") {
|
|
787
|
+
try {
|
|
788
|
+
const history = await sdkWithHistory.getMessages(params.conversationId, { limit: params.messageLimit });
|
|
789
|
+
if (history?.messages && history.messages.length > 0) {
|
|
790
|
+
messages = history.messages;
|
|
791
|
+
}
|
|
792
|
+
} catch (e) {
|
|
793
|
+
console.warn(`[${ctx.logTag}] getMessages failed, fallback to getConversation:`, e);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return { success: true, data: { ...result, messages } };
|
|
797
|
+
} catch (e) {
|
|
798
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to get" };
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
handle("sanqian-chat:deleteConversation", async (_, params) => {
|
|
802
|
+
const sdk = ctx.getSdk();
|
|
803
|
+
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
804
|
+
try {
|
|
805
|
+
await sdk.deleteConversation(params.conversationId);
|
|
806
|
+
return { success: true };
|
|
807
|
+
} catch (e) {
|
|
808
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to delete" };
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
handle("sanqian-chat:listResourceProviders", async () => {
|
|
812
|
+
const sdk = ctx.getSdk();
|
|
813
|
+
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
814
|
+
try {
|
|
815
|
+
const agentId = ctx.getAgentId();
|
|
816
|
+
const queryParams = agentId ? `?agent_id=${encodeURIComponent(agentId)}` : "";
|
|
817
|
+
const url = `http://127.0.0.1:${sdk.getPort()}/api/sdk/contexts${queryParams}`;
|
|
818
|
+
const response = await fetch(url);
|
|
819
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
820
|
+
const data = await response.json();
|
|
821
|
+
return { success: true, data: data.contexts || [] };
|
|
822
|
+
} catch (e) {
|
|
823
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to list providers" };
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
handle("sanqian-chat:getResourceList", async (_, params) => {
|
|
827
|
+
const sdk = ctx.getSdk();
|
|
828
|
+
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
829
|
+
try {
|
|
830
|
+
const queryString = new URLSearchParams();
|
|
831
|
+
if (params.query) queryString.set("query", params.query);
|
|
832
|
+
if (params.offset !== void 0) queryString.set("offset", String(params.offset));
|
|
833
|
+
if (params.limit !== void 0) queryString.set("limit", String(params.limit));
|
|
834
|
+
const url = `http://127.0.0.1:${sdk.getPort()}/api/sdk/contexts/${encodeURIComponent(params.providerId)}/list?${queryString}`;
|
|
835
|
+
const response = await fetch(url);
|
|
836
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
837
|
+
const data = await response.json();
|
|
838
|
+
if (data.error) throw new Error(data.error);
|
|
839
|
+
return { success: true, data: { items: data.items || [], hasMore: data.has_more } };
|
|
840
|
+
} catch (e) {
|
|
841
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to get resource list" };
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
}
|
|
426
845
|
|
|
427
846
|
// src/main/FloatingWindow.ts
|
|
428
847
|
var ipcHandlersRegistered = false;
|
|
@@ -436,6 +855,7 @@ var FloatingWindow = class _FloatingWindow {
|
|
|
436
855
|
this.savedState = null;
|
|
437
856
|
this.stateSaveTimer = null;
|
|
438
857
|
this.activeStreams = /* @__PURE__ */ new Map();
|
|
858
|
+
this.runOwnerByRunId = /* @__PURE__ */ new Map();
|
|
439
859
|
this.reconnectAcquired = false;
|
|
440
860
|
if (activeInstance) {
|
|
441
861
|
console.warn("[FloatingWindow] Only one instance supported. Destroying previous.");
|
|
@@ -460,10 +880,31 @@ var FloatingWindow = class _FloatingWindow {
|
|
|
460
880
|
}
|
|
461
881
|
resolveHitlRunId(params, senderWebContentsId) {
|
|
462
882
|
if (typeof senderWebContentsId === "number") {
|
|
463
|
-
return resolveOwnedHitlRunIdFromStreams(
|
|
883
|
+
return resolveOwnedHitlRunIdFromStreams(
|
|
884
|
+
params,
|
|
885
|
+
this.activeStreams,
|
|
886
|
+
senderWebContentsId,
|
|
887
|
+
this.runOwnerByRunId
|
|
888
|
+
);
|
|
464
889
|
}
|
|
465
890
|
return resolveHitlRunIdFromStreams(params, this.activeStreams);
|
|
466
891
|
}
|
|
892
|
+
rememberRunOwner(runId, ownerWebContentsId) {
|
|
893
|
+
rememberRunOwnerWithLru(this.runOwnerByRunId, runId, ownerWebContentsId);
|
|
894
|
+
}
|
|
895
|
+
forgetRunOwner(runId) {
|
|
896
|
+
if (!runId) return;
|
|
897
|
+
this.runOwnerByRunId.delete(runId);
|
|
898
|
+
}
|
|
899
|
+
async waitForOwnedStreamRunId(streamId, ownerWebContentsId, timeoutMs = 1200, pollIntervalMs = 20) {
|
|
900
|
+
return waitForOwnedStreamRunId(
|
|
901
|
+
streamId,
|
|
902
|
+
ownerWebContentsId,
|
|
903
|
+
this.activeStreams,
|
|
904
|
+
timeoutMs,
|
|
905
|
+
pollIntervalMs
|
|
906
|
+
);
|
|
907
|
+
}
|
|
467
908
|
/**
|
|
468
909
|
* Get SDK instance from either getClient or getSdk
|
|
469
910
|
*/
|
|
@@ -502,6 +943,7 @@ var FloatingWindow = class _FloatingWindow {
|
|
|
502
943
|
skipTaskbar: !showInTaskbar,
|
|
503
944
|
webPreferences: {
|
|
504
945
|
preload: preloadPath,
|
|
946
|
+
sandbox: true,
|
|
505
947
|
contextIsolation: true,
|
|
506
948
|
nodeIntegration: false
|
|
507
949
|
}
|
|
@@ -681,211 +1123,35 @@ var FloatingWindow = class _FloatingWindow {
|
|
|
681
1123
|
setupIpcHandlers() {
|
|
682
1124
|
if (ipcHandlersRegistered) return;
|
|
683
1125
|
ipcHandlersRegistered = true;
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
};
|
|
714
|
-
activeInstance?.activeStreams.set(streamId, streamState);
|
|
715
|
-
try {
|
|
716
|
-
await sdk.ensureReady();
|
|
717
|
-
const sdkMessages = messages.map((m) => ({ role: m.role, content: m.content }));
|
|
718
|
-
const stream = sdk.chatStream(
|
|
719
|
-
agentId,
|
|
720
|
-
sdkMessages,
|
|
721
|
-
{ conversationId, persistHistory: true }
|
|
722
|
-
);
|
|
723
|
-
for await (const evt of stream) {
|
|
724
|
-
const evtWithRunId = evt;
|
|
725
|
-
if (evtWithRunId.run_id && !streamState.runId) {
|
|
726
|
-
streamState.runId = evtWithRunId.run_id;
|
|
727
|
-
}
|
|
728
|
-
if (streamState.pendingHitlResponse && streamState.runId) {
|
|
729
|
-
try {
|
|
730
|
-
sdk.sendHitlResponse(streamState.runId, streamState.pendingHitlResponse);
|
|
731
|
-
streamState.pendingHitlResponse = null;
|
|
732
|
-
} catch (e) {
|
|
733
|
-
console.warn("[FloatingWindow] Failed to flush queued HITL response:", e);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
if (streamState.cancelled && streamState.runId && !streamState.cancelSignalSent) {
|
|
737
|
-
try {
|
|
738
|
-
sdk.cancelRun(streamState.runId);
|
|
739
|
-
streamState.cancelSignalSent = true;
|
|
740
|
-
} catch (e) {
|
|
741
|
-
console.warn("[FloatingWindow] Failed to cancel run:", e);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
if (streamState.cancelled) {
|
|
745
|
-
if (streamState.cancelSignalSent) {
|
|
746
|
-
break;
|
|
747
|
-
}
|
|
748
|
-
continue;
|
|
749
|
-
}
|
|
750
|
-
if (activeInstance?.options.devMode) {
|
|
751
|
-
console.log("[FloatingWindow] SDK event:", evt.type, JSON.stringify(evt).slice(0, 200));
|
|
752
|
-
}
|
|
753
|
-
switch (evt.type) {
|
|
754
|
-
case "start": {
|
|
755
|
-
const startEvt = evt;
|
|
756
|
-
if (startEvt.run_id) {
|
|
757
|
-
streamState.runId = startEvt.run_id;
|
|
758
|
-
}
|
|
759
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "start", run_id: startEvt.run_id } });
|
|
760
|
-
break;
|
|
761
|
-
}
|
|
762
|
-
case "text":
|
|
763
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "text", content: evt.content } });
|
|
764
|
-
break;
|
|
765
|
-
case "thinking":
|
|
766
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "thinking", content: evt.content } });
|
|
767
|
-
break;
|
|
768
|
-
case "tool_call":
|
|
769
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_call", tool_call: evt.tool_call } });
|
|
770
|
-
break;
|
|
771
|
-
case "tool_result":
|
|
772
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_result", tool_call_id: evt.tool_call_id, result: evt.result } });
|
|
773
|
-
break;
|
|
774
|
-
case "done":
|
|
775
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "done", conversationId: evt.conversationId, title: evt.title } });
|
|
776
|
-
break;
|
|
777
|
-
case "error":
|
|
778
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: evt.error } });
|
|
779
|
-
break;
|
|
780
|
-
default: {
|
|
781
|
-
const anyEvt = evt;
|
|
782
|
-
if (anyEvt.type === "interrupt") {
|
|
783
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "interrupt", interrupt_type: anyEvt.interrupt_type, interrupt_payload: anyEvt.interrupt_payload, run_id: anyEvt.run_id } });
|
|
784
|
-
}
|
|
785
|
-
break;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
} catch (e) {
|
|
790
|
-
if (!streamState.cancelled) {
|
|
791
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: e instanceof Error ? e.message : "Stream error" } });
|
|
792
|
-
}
|
|
793
|
-
} finally {
|
|
794
|
-
activeInstance?.activeStreams.delete(streamId);
|
|
795
|
-
}
|
|
796
|
-
});
|
|
797
|
-
import_electron.ipcMain.handle("sanqian-chat:cancelStream", (event, params) => {
|
|
798
|
-
const stream = activeInstance?.activeStreams.get(params.streamId);
|
|
799
|
-
if (!stream) {
|
|
800
|
-
return { success: false, error: "stream_not_found" };
|
|
801
|
-
}
|
|
802
|
-
if (stream.ownerWebContentsId !== event.sender.id) {
|
|
803
|
-
console.warn("[FloatingWindow] Rejecting cancelStream from non-owner sender");
|
|
804
|
-
return { success: false, error: "stream_not_owned_by_sender" };
|
|
805
|
-
}
|
|
806
|
-
stream.cancelled = true;
|
|
807
|
-
if (stream.runId && !stream.cancelSignalSent) {
|
|
808
|
-
const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
|
|
809
|
-
if (sdk) {
|
|
810
|
-
try {
|
|
811
|
-
sdk.cancelRun(stream.runId);
|
|
812
|
-
stream.cancelSignalSent = true;
|
|
813
|
-
} catch (e) {
|
|
814
|
-
console.warn("[FloatingWindow] Failed to cancel run:", e);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
return { success: true };
|
|
819
|
-
});
|
|
820
|
-
import_electron.ipcMain.handle("sanqian-chat:hitlResponse", (event, params) => {
|
|
821
|
-
const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
|
|
822
|
-
const senderWebContentsId = event.sender.id;
|
|
823
|
-
const runId = activeInstance?.resolveHitlRunId(
|
|
824
|
-
{ runId: params.runId, streamId: params.streamId },
|
|
825
|
-
senderWebContentsId
|
|
826
|
-
) ?? null;
|
|
827
|
-
if (sdk && runId) {
|
|
828
|
-
sdk.sendHitlResponse(runId, params.response);
|
|
829
|
-
} else if (params.streamId) {
|
|
830
|
-
const stream = activeInstance?.activeStreams.get(params.streamId);
|
|
831
|
-
if (stream && !stream.cancelled && stream.ownerWebContentsId === senderWebContentsId) {
|
|
832
|
-
stream.pendingHitlResponse = params.response;
|
|
833
|
-
if (activeInstance?.options.devMode) {
|
|
834
|
-
console.warn("[FloatingWindow] Queued HITL response while waiting for runId");
|
|
835
|
-
}
|
|
836
|
-
} else {
|
|
837
|
-
console.warn("[FloatingWindow] HITL response dropped: stream not found/cancelled/not-owned");
|
|
838
|
-
}
|
|
839
|
-
} else if (activeInstance?.options.devMode) {
|
|
840
|
-
console.warn("[FloatingWindow] HITL response dropped: missing or unauthorized runId");
|
|
841
|
-
}
|
|
842
|
-
return { success: true };
|
|
843
|
-
});
|
|
844
|
-
import_electron.ipcMain.handle("sanqian-chat:listConversations", async (_, params) => {
|
|
845
|
-
const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
|
|
846
|
-
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
847
|
-
try {
|
|
848
|
-
const result = await sdk.listConversations({
|
|
849
|
-
limit: params?.limit,
|
|
850
|
-
offset: params?.offset
|
|
851
|
-
});
|
|
852
|
-
return { success: true, data: result };
|
|
853
|
-
} catch (e) {
|
|
854
|
-
return { success: false, error: e instanceof Error ? e.message : "Failed to list" };
|
|
855
|
-
}
|
|
856
|
-
});
|
|
857
|
-
import_electron.ipcMain.handle("sanqian-chat:getConversation", async (_, params) => {
|
|
858
|
-
const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
|
|
859
|
-
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
860
|
-
try {
|
|
861
|
-
const result = await sdk.getConversation(params.conversationId, { messageLimit: params.messageLimit });
|
|
862
|
-
let messages = result?.messages;
|
|
863
|
-
const sdkWithHistory = sdk;
|
|
864
|
-
if (typeof sdkWithHistory.getMessages === "function") {
|
|
865
|
-
try {
|
|
866
|
-
const history = await sdkWithHistory.getMessages(params.conversationId, { limit: params.messageLimit });
|
|
867
|
-
if (history?.messages && history.messages.length > 0) {
|
|
868
|
-
messages = history.messages;
|
|
869
|
-
}
|
|
870
|
-
} catch (e) {
|
|
871
|
-
console.warn("[sanqian-chat][main] getMessages failed, fallback to getConversation:", e);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
return { success: true, data: { ...result, messages } };
|
|
875
|
-
} catch (e) {
|
|
876
|
-
return { success: false, error: e instanceof Error ? e.message : "Failed to get" };
|
|
877
|
-
}
|
|
878
|
-
});
|
|
879
|
-
import_electron.ipcMain.handle("sanqian-chat:deleteConversation", async (_, params) => {
|
|
880
|
-
const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
|
|
881
|
-
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
882
|
-
try {
|
|
883
|
-
await sdk.deleteConversation(params.conversationId);
|
|
884
|
-
return { success: true };
|
|
885
|
-
} catch (e) {
|
|
886
|
-
return { success: false, error: e instanceof Error ? e.message : "Failed to delete" };
|
|
887
|
-
}
|
|
888
|
-
});
|
|
1126
|
+
registerCommonChatIpcHandlers(
|
|
1127
|
+
import_electron.ipcMain.handle.bind(import_electron.ipcMain),
|
|
1128
|
+
{
|
|
1129
|
+
getSdk: () => {
|
|
1130
|
+
const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
|
|
1131
|
+
return sdk;
|
|
1132
|
+
},
|
|
1133
|
+
getAgentId: () => activeInstance?.options.getAgentId() ?? void 0,
|
|
1134
|
+
logTag: "FloatingWindow"
|
|
1135
|
+
}
|
|
1136
|
+
);
|
|
1137
|
+
registerStreamIpcHandlers(
|
|
1138
|
+
import_electron.ipcMain.handle.bind(import_electron.ipcMain),
|
|
1139
|
+
{
|
|
1140
|
+
getActiveStreams: () => activeInstance?.["activeStreams"] ?? /* @__PURE__ */ new Map(),
|
|
1141
|
+
getRunOwnerByRunId: () => activeInstance?.["runOwnerByRunId"] ?? /* @__PURE__ */ new Map(),
|
|
1142
|
+
rememberRunOwner: (runId, ownerWebContentsId) => activeInstance?.["rememberRunOwner"](runId, ownerWebContentsId),
|
|
1143
|
+
forgetRunOwner: (runId) => activeInstance?.["forgetRunOwner"](runId),
|
|
1144
|
+
resolveHitlRunId: (params, senderId) => activeInstance?.["resolveHitlRunId"](params, senderId) ?? null,
|
|
1145
|
+
waitForOwnedStreamRunId: (streamId, senderId) => activeInstance?.["waitForOwnedStreamRunId"](streamId, senderId) ?? Promise.resolve(null),
|
|
1146
|
+
getSdk: () => {
|
|
1147
|
+
const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
|
|
1148
|
+
return sdk;
|
|
1149
|
+
},
|
|
1150
|
+
getAgentId: (requested) => requested ?? activeInstance?.options.getAgentId() ?? void 0,
|
|
1151
|
+
isDevMode: () => activeInstance?.options.devMode ?? false,
|
|
1152
|
+
logTag: "FloatingWindow"
|
|
1153
|
+
}
|
|
1154
|
+
);
|
|
889
1155
|
import_electron.ipcMain.handle("sanqian-chat:hide", () => {
|
|
890
1156
|
activeInstance?.hide();
|
|
891
1157
|
return { success: true };
|
|
@@ -914,40 +1180,6 @@ var FloatingWindow = class _FloatingWindow {
|
|
|
914
1180
|
return { success: false, error: e instanceof Error ? e.message : "Failed to set background color" };
|
|
915
1181
|
}
|
|
916
1182
|
});
|
|
917
|
-
import_electron.ipcMain.handle("sanqian-chat:listResourceProviders", async () => {
|
|
918
|
-
const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
|
|
919
|
-
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
920
|
-
try {
|
|
921
|
-
const port = sdk.getPort();
|
|
922
|
-
const agentId = activeInstance?.options.getAgentId();
|
|
923
|
-
const queryParams = agentId ? `?agent_id=${encodeURIComponent(agentId)}` : "";
|
|
924
|
-
const response = await fetch(`http://127.0.0.1:${port}/api/sdk/contexts${queryParams}`);
|
|
925
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
926
|
-
const data = await response.json();
|
|
927
|
-
return { success: true, data: data.contexts || [] };
|
|
928
|
-
} catch (e) {
|
|
929
|
-
return { success: false, error: e instanceof Error ? e.message : "Failed to list providers" };
|
|
930
|
-
}
|
|
931
|
-
});
|
|
932
|
-
import_electron.ipcMain.handle("sanqian-chat:getResourceList", async (_, params) => {
|
|
933
|
-
const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
|
|
934
|
-
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
935
|
-
try {
|
|
936
|
-
const port = sdk.getPort();
|
|
937
|
-
const queryParams = new URLSearchParams();
|
|
938
|
-
if (params.query) queryParams.set("query", params.query);
|
|
939
|
-
if (params.offset !== void 0) queryParams.set("offset", String(params.offset));
|
|
940
|
-
if (params.limit !== void 0) queryParams.set("limit", String(params.limit));
|
|
941
|
-
const url = `http://127.0.0.1:${port}/api/sdk/contexts/${encodeURIComponent(params.providerId)}/list?${queryParams}`;
|
|
942
|
-
const response = await fetch(url);
|
|
943
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
944
|
-
const data = await response.json();
|
|
945
|
-
if (data.error) throw new Error(data.error);
|
|
946
|
-
return { success: true, data: { items: data.items || [], hasMore: data.has_more } };
|
|
947
|
-
} catch (e) {
|
|
948
|
-
return { success: false, error: e instanceof Error ? e.message : "Failed to get resource list" };
|
|
949
|
-
}
|
|
950
|
-
});
|
|
951
1183
|
}
|
|
952
1184
|
// Public API
|
|
953
1185
|
show() {
|
|
@@ -1021,6 +1253,7 @@ var FloatingWindow = class _FloatingWindow {
|
|
|
1021
1253
|
stream.cancelled = true;
|
|
1022
1254
|
});
|
|
1023
1255
|
this.activeStreams.clear();
|
|
1256
|
+
this.runOwnerByRunId.clear();
|
|
1024
1257
|
if (activeInstance === this) {
|
|
1025
1258
|
activeInstance = null;
|
|
1026
1259
|
if (ipcHandlersRegistered) {
|
|
@@ -1095,6 +1328,8 @@ var ChatPanel = class {
|
|
|
1095
1328
|
this.stateSaveTimer = null;
|
|
1096
1329
|
// Active streams for cancel support
|
|
1097
1330
|
this.activeStreams = /* @__PURE__ */ new Map();
|
|
1331
|
+
// Run ownership index for HITL recovery when stream-map entries churn.
|
|
1332
|
+
this.runOwnerByRunId = /* @__PURE__ */ new Map();
|
|
1098
1333
|
// Session resource event cleanup
|
|
1099
1334
|
this.sessionResourceCleanup = null;
|
|
1100
1335
|
// === Private: Host Window Listeners ===
|
|
@@ -1405,6 +1640,7 @@ var ChatPanel = class {
|
|
|
1405
1640
|
this.embeddedView = null;
|
|
1406
1641
|
}
|
|
1407
1642
|
this.sharedView = null;
|
|
1643
|
+
this.runOwnerByRunId.clear();
|
|
1408
1644
|
if (activeInstance2 === this) {
|
|
1409
1645
|
activeInstance2 = null;
|
|
1410
1646
|
this.cleanupIpcHandlers();
|
|
@@ -1619,6 +1855,7 @@ var ChatPanel = class {
|
|
|
1619
1855
|
this.sharedView = new WebContentsViewClass({
|
|
1620
1856
|
webPreferences: {
|
|
1621
1857
|
preload: this.config.preloadPath,
|
|
1858
|
+
sandbox: true,
|
|
1622
1859
|
contextIsolation: true,
|
|
1623
1860
|
nodeIntegration: false
|
|
1624
1861
|
}
|
|
@@ -1792,10 +2029,31 @@ var ChatPanel = class {
|
|
|
1792
2029
|
*/
|
|
1793
2030
|
resolveHitlRunId(params, senderWebContentsId) {
|
|
1794
2031
|
if (typeof senderWebContentsId === "number") {
|
|
1795
|
-
return resolveOwnedHitlRunIdFromStreams(
|
|
2032
|
+
return resolveOwnedHitlRunIdFromStreams(
|
|
2033
|
+
params,
|
|
2034
|
+
this.activeStreams,
|
|
2035
|
+
senderWebContentsId,
|
|
2036
|
+
this.runOwnerByRunId
|
|
2037
|
+
);
|
|
1796
2038
|
}
|
|
1797
2039
|
return resolveHitlRunIdFromStreams(params, this.activeStreams);
|
|
1798
2040
|
}
|
|
2041
|
+
rememberRunOwner(runId, ownerWebContentsId) {
|
|
2042
|
+
rememberRunOwnerWithLru(this.runOwnerByRunId, runId, ownerWebContentsId);
|
|
2043
|
+
}
|
|
2044
|
+
forgetRunOwner(runId) {
|
|
2045
|
+
if (!runId) return;
|
|
2046
|
+
this.runOwnerByRunId.delete(runId);
|
|
2047
|
+
}
|
|
2048
|
+
async waitForOwnedStreamRunId(streamId, ownerWebContentsId, timeoutMs = 1200, pollIntervalMs = 20) {
|
|
2049
|
+
return waitForOwnedStreamRunId(
|
|
2050
|
+
streamId,
|
|
2051
|
+
ownerWebContentsId,
|
|
2052
|
+
this.activeStreams,
|
|
2053
|
+
timeoutMs,
|
|
2054
|
+
pollIntervalMs
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
1799
2057
|
/**
|
|
1800
2058
|
* Setup session resource event forwarding from SDK to renderer
|
|
1801
2059
|
* Called when SDK becomes available (on connect)
|
|
@@ -1833,217 +2091,30 @@ var ChatPanel = class {
|
|
|
1833
2091
|
setupIpcHandlers() {
|
|
1834
2092
|
if (ipcHandlersRegistered2) return;
|
|
1835
2093
|
ipcHandlersRegistered2 = true;
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
activeInstance2?.setupSessionResourceEvents()
|
|
1842
|
-
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
const streamState = {
|
|
1861
|
-
cancelled: false,
|
|
1862
|
-
runId: null,
|
|
1863
|
-
cancelSignalSent: false,
|
|
1864
|
-
pendingHitlResponse: null,
|
|
1865
|
-
ownerWebContentsId: webContents.id
|
|
1866
|
-
};
|
|
1867
|
-
activeInstance2?.activeStreams.set(streamId, streamState);
|
|
1868
|
-
try {
|
|
1869
|
-
await sdk.ensureReady();
|
|
1870
|
-
const sdkMessages = messages.map((m) => ({ role: m.role, content: m.content }));
|
|
1871
|
-
const validResources = attachedResources?.filter((r) => {
|
|
1872
|
-
const parts = r.split(":");
|
|
1873
|
-
return parts.length >= 3 && parts.every((p) => p.length > 0);
|
|
1874
|
-
});
|
|
1875
|
-
const stream = sdk.chatStream(agentId, sdkMessages, {
|
|
1876
|
-
conversationId,
|
|
1877
|
-
persistHistory: true,
|
|
1878
|
-
attachedResources: validResources?.length ? validResources : void 0,
|
|
1879
|
-
sessionResources: sessionResources?.length ? sessionResources : void 0
|
|
1880
|
-
});
|
|
1881
|
-
for await (const evt of stream) {
|
|
1882
|
-
const evtWithRunId = evt;
|
|
1883
|
-
if (evtWithRunId.run_id && !streamState.runId) {
|
|
1884
|
-
streamState.runId = evtWithRunId.run_id;
|
|
1885
|
-
}
|
|
1886
|
-
if (streamState.pendingHitlResponse && streamState.runId) {
|
|
1887
|
-
try {
|
|
1888
|
-
sdk.sendHitlResponse(streamState.runId, streamState.pendingHitlResponse);
|
|
1889
|
-
streamState.pendingHitlResponse = null;
|
|
1890
|
-
} catch (e) {
|
|
1891
|
-
console.warn("[ChatPanel] Failed to flush queued HITL response:", e);
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
if (streamState.cancelled && streamState.runId && !streamState.cancelSignalSent) {
|
|
1895
|
-
try {
|
|
1896
|
-
sdk.cancelRun(streamState.runId);
|
|
1897
|
-
streamState.cancelSignalSent = true;
|
|
1898
|
-
} catch (e) {
|
|
1899
|
-
console.warn("[ChatPanel] Failed to cancel run:", e);
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
if (streamState.cancelled) {
|
|
1903
|
-
if (streamState.cancelSignalSent) {
|
|
1904
|
-
break;
|
|
1905
|
-
}
|
|
1906
|
-
continue;
|
|
1907
|
-
}
|
|
1908
|
-
if (activeInstance2?.config.devMode) {
|
|
1909
|
-
console.log("[ChatPanel] SDK event:", evt.type, JSON.stringify(evt).slice(0, 200));
|
|
1910
|
-
}
|
|
1911
|
-
switch (evt.type) {
|
|
1912
|
-
case "start": {
|
|
1913
|
-
const startEvt = evt;
|
|
1914
|
-
if (startEvt.run_id) {
|
|
1915
|
-
streamState.runId = startEvt.run_id;
|
|
1916
|
-
}
|
|
1917
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "start", run_id: startEvt.run_id } });
|
|
1918
|
-
break;
|
|
1919
|
-
}
|
|
1920
|
-
case "text":
|
|
1921
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "text", content: evt.content } });
|
|
1922
|
-
break;
|
|
1923
|
-
case "thinking":
|
|
1924
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "thinking", content: evt.content } });
|
|
1925
|
-
break;
|
|
1926
|
-
case "tool_call":
|
|
1927
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_call", tool_call: evt.tool_call } });
|
|
1928
|
-
break;
|
|
1929
|
-
case "tool_result":
|
|
1930
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_result", tool_call_id: evt.tool_call_id, result: evt.result } });
|
|
1931
|
-
break;
|
|
1932
|
-
case "done":
|
|
1933
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "done", conversationId: evt.conversationId, title: evt.title } });
|
|
1934
|
-
break;
|
|
1935
|
-
case "error":
|
|
1936
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: evt.error } });
|
|
1937
|
-
break;
|
|
1938
|
-
default: {
|
|
1939
|
-
const anyEvt = evt;
|
|
1940
|
-
if (anyEvt.type === "interrupt") {
|
|
1941
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "interrupt", interrupt_type: anyEvt.interrupt_type, interrupt_payload: anyEvt.interrupt_payload, run_id: anyEvt.run_id } });
|
|
1942
|
-
}
|
|
1943
|
-
break;
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
} catch (e) {
|
|
1948
|
-
if (!streamState.cancelled) {
|
|
1949
|
-
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: e instanceof Error ? e.message : "Stream error" } });
|
|
1950
|
-
}
|
|
1951
|
-
} finally {
|
|
1952
|
-
activeInstance2?.activeStreams.delete(streamId);
|
|
1953
|
-
}
|
|
1954
|
-
});
|
|
1955
|
-
import_electron2.ipcMain.handle("sanqian-chat:cancelStream", (event, params) => {
|
|
1956
|
-
const stream = activeInstance2?.activeStreams.get(params.streamId);
|
|
1957
|
-
if (!stream) {
|
|
1958
|
-
return { success: false, error: "stream_not_found" };
|
|
1959
|
-
}
|
|
1960
|
-
if (stream.ownerWebContentsId !== event.sender.id) {
|
|
1961
|
-
console.warn("[ChatPanel] Rejecting cancelStream from non-owner sender");
|
|
1962
|
-
return { success: false, error: "stream_not_owned_by_sender" };
|
|
1963
|
-
}
|
|
1964
|
-
stream.cancelled = true;
|
|
1965
|
-
if (stream.runId && !stream.cancelSignalSent) {
|
|
1966
|
-
const sdk = activeInstance2?.getSdk();
|
|
1967
|
-
if (sdk) {
|
|
1968
|
-
try {
|
|
1969
|
-
sdk.cancelRun(stream.runId);
|
|
1970
|
-
stream.cancelSignalSent = true;
|
|
1971
|
-
} catch (e) {
|
|
1972
|
-
console.warn("[ChatPanel] Failed to cancel run:", e);
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
return { success: true };
|
|
1977
|
-
});
|
|
1978
|
-
import_electron2.ipcMain.handle("sanqian-chat:hitlResponse", (event, params) => {
|
|
1979
|
-
const sdk = activeInstance2?.getSdk();
|
|
1980
|
-
const senderWebContentsId = event.sender.id;
|
|
1981
|
-
const runId = activeInstance2?.resolveHitlRunId(
|
|
1982
|
-
{ runId: params.runId, streamId: params.streamId },
|
|
1983
|
-
senderWebContentsId
|
|
1984
|
-
) ?? null;
|
|
1985
|
-
if (sdk && runId) {
|
|
1986
|
-
sdk.sendHitlResponse(runId, params.response);
|
|
1987
|
-
} else if (params.streamId) {
|
|
1988
|
-
const stream = activeInstance2?.activeStreams.get(params.streamId);
|
|
1989
|
-
if (stream && !stream.cancelled && stream.ownerWebContentsId === senderWebContentsId) {
|
|
1990
|
-
stream.pendingHitlResponse = params.response;
|
|
1991
|
-
if (activeInstance2?.config.devMode) {
|
|
1992
|
-
console.warn("[ChatPanel] Queued HITL response while waiting for runId");
|
|
1993
|
-
}
|
|
1994
|
-
} else {
|
|
1995
|
-
console.warn("[ChatPanel] HITL response dropped: stream not found/cancelled/not-owned");
|
|
1996
|
-
}
|
|
1997
|
-
} else if (activeInstance2?.config.devMode) {
|
|
1998
|
-
console.warn("[ChatPanel] HITL response dropped: missing or unauthorized runId");
|
|
1999
|
-
}
|
|
2000
|
-
return { success: true };
|
|
2001
|
-
});
|
|
2002
|
-
import_electron2.ipcMain.handle("sanqian-chat:listConversations", async (_, params) => {
|
|
2003
|
-
const sdk = activeInstance2?.getSdk();
|
|
2004
|
-
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
2005
|
-
try {
|
|
2006
|
-
const result = await sdk.listConversations({
|
|
2007
|
-
limit: params?.limit,
|
|
2008
|
-
offset: params?.offset
|
|
2009
|
-
});
|
|
2010
|
-
return { success: true, data: result };
|
|
2011
|
-
} catch (e) {
|
|
2012
|
-
return { success: false, error: e instanceof Error ? e.message : "Failed to list" };
|
|
2013
|
-
}
|
|
2014
|
-
});
|
|
2015
|
-
import_electron2.ipcMain.handle("sanqian-chat:getConversation", async (_, params) => {
|
|
2016
|
-
const sdk = activeInstance2?.getSdk();
|
|
2017
|
-
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
2018
|
-
try {
|
|
2019
|
-
const result = await sdk.getConversation(params.conversationId, { messageLimit: params.messageLimit });
|
|
2020
|
-
let messages = result?.messages;
|
|
2021
|
-
const sdkWithHistory = sdk;
|
|
2022
|
-
if (typeof sdkWithHistory.getMessages === "function") {
|
|
2023
|
-
try {
|
|
2024
|
-
const history = await sdkWithHistory.getMessages(params.conversationId, { limit: params.messageLimit });
|
|
2025
|
-
if (history?.messages && history.messages.length > 0) {
|
|
2026
|
-
messages = history.messages;
|
|
2027
|
-
}
|
|
2028
|
-
} catch (e) {
|
|
2029
|
-
console.warn("[ChatPanel] getMessages failed, fallback to getConversation:", e);
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
return { success: true, data: { ...result, messages } };
|
|
2033
|
-
} catch (e) {
|
|
2034
|
-
return { success: false, error: e instanceof Error ? e.message : "Failed to get" };
|
|
2035
|
-
}
|
|
2036
|
-
});
|
|
2037
|
-
import_electron2.ipcMain.handle("sanqian-chat:deleteConversation", async (_, params) => {
|
|
2038
|
-
const sdk = activeInstance2?.getSdk();
|
|
2039
|
-
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
2040
|
-
try {
|
|
2041
|
-
await sdk.deleteConversation(params.conversationId);
|
|
2042
|
-
return { success: true };
|
|
2043
|
-
} catch (e) {
|
|
2044
|
-
return { success: false, error: e instanceof Error ? e.message : "Failed to delete" };
|
|
2045
|
-
}
|
|
2046
|
-
});
|
|
2094
|
+
registerCommonChatIpcHandlers(
|
|
2095
|
+
import_electron2.ipcMain.handle.bind(import_electron2.ipcMain),
|
|
2096
|
+
{
|
|
2097
|
+
getSdk: () => activeInstance2?.getSdk() ?? null,
|
|
2098
|
+
getAgentId: () => activeInstance2?.config.getAgentId() ?? void 0,
|
|
2099
|
+
onConnected: () => activeInstance2?.setupSessionResourceEvents(),
|
|
2100
|
+
logTag: "ChatPanel"
|
|
2101
|
+
}
|
|
2102
|
+
);
|
|
2103
|
+
registerStreamIpcHandlers(
|
|
2104
|
+
import_electron2.ipcMain.handle.bind(import_electron2.ipcMain),
|
|
2105
|
+
{
|
|
2106
|
+
getActiveStreams: () => activeInstance2?.["activeStreams"] ?? /* @__PURE__ */ new Map(),
|
|
2107
|
+
getRunOwnerByRunId: () => activeInstance2?.["runOwnerByRunId"] ?? /* @__PURE__ */ new Map(),
|
|
2108
|
+
rememberRunOwner: (runId, ownerWebContentsId) => activeInstance2?.["rememberRunOwner"](runId, ownerWebContentsId),
|
|
2109
|
+
forgetRunOwner: (runId) => activeInstance2?.["forgetRunOwner"](runId),
|
|
2110
|
+
resolveHitlRunId: (params, senderId) => activeInstance2?.["resolveHitlRunId"](params, senderId) ?? null,
|
|
2111
|
+
waitForOwnedStreamRunId: (streamId, senderId) => activeInstance2?.["waitForOwnedStreamRunId"](streamId, senderId) ?? Promise.resolve(null),
|
|
2112
|
+
getSdk: () => activeInstance2?.getSdk() ?? null,
|
|
2113
|
+
getAgentId: (requested) => requested ?? activeInstance2?.config.getAgentId() ?? void 0,
|
|
2114
|
+
isDevMode: () => activeInstance2?.config.devMode ?? false,
|
|
2115
|
+
logTag: "ChatPanel"
|
|
2116
|
+
}
|
|
2117
|
+
);
|
|
2047
2118
|
import_electron2.ipcMain.handle("sanqian-chat:hide", () => {
|
|
2048
2119
|
activeInstance2?.hide();
|
|
2049
2120
|
return { success: true };
|
|
@@ -2069,39 +2140,6 @@ var ChatPanel = class {
|
|
|
2069
2140
|
return { success: false, error: e instanceof Error ? e.message : "Failed to set background color" };
|
|
2070
2141
|
}
|
|
2071
2142
|
});
|
|
2072
|
-
import_electron2.ipcMain.handle("sanqian-chat:listResourceProviders", async () => {
|
|
2073
|
-
const sdk = activeInstance2?.getSdk();
|
|
2074
|
-
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
2075
|
-
try {
|
|
2076
|
-
const agentId = activeInstance2?.config.getAgentId();
|
|
2077
|
-
const queryParams = agentId ? `?agent_id=${encodeURIComponent(agentId)}` : "";
|
|
2078
|
-
const url = `http://127.0.0.1:${sdk.getPort()}/api/sdk/contexts${queryParams}`;
|
|
2079
|
-
const response = await fetch(url);
|
|
2080
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
2081
|
-
const data = await response.json();
|
|
2082
|
-
return { success: true, data: data.contexts || [] };
|
|
2083
|
-
} catch (e) {
|
|
2084
|
-
return { success: false, error: e instanceof Error ? e.message : "Failed to list providers" };
|
|
2085
|
-
}
|
|
2086
|
-
});
|
|
2087
|
-
import_electron2.ipcMain.handle("sanqian-chat:getResourceList", async (_, params) => {
|
|
2088
|
-
const sdk = activeInstance2?.getSdk();
|
|
2089
|
-
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
2090
|
-
try {
|
|
2091
|
-
const queryParams = new URLSearchParams();
|
|
2092
|
-
if (params.query) queryParams.set("query", params.query);
|
|
2093
|
-
if (params.offset !== void 0) queryParams.set("offset", String(params.offset));
|
|
2094
|
-
if (params.limit !== void 0) queryParams.set("limit", String(params.limit));
|
|
2095
|
-
const url = `http://127.0.0.1:${sdk.getPort()}/api/sdk/contexts/${encodeURIComponent(params.providerId)}/list?${queryParams}`;
|
|
2096
|
-
const response = await fetch(url);
|
|
2097
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
2098
|
-
const data = await response.json();
|
|
2099
|
-
if (data.error) throw new Error(data.error);
|
|
2100
|
-
return { success: true, data: { items: data.items || [], hasMore: data.has_more } };
|
|
2101
|
-
} catch (e) {
|
|
2102
|
-
return { success: false, error: e instanceof Error ? e.message : "Failed to get resource list" };
|
|
2103
|
-
}
|
|
2104
|
-
});
|
|
2105
2143
|
import_electron2.ipcMain.on("sanqian-chat:getSessionResourcesSync", (event) => {
|
|
2106
2144
|
const sdk = activeInstance2?.getSdk();
|
|
2107
2145
|
event.returnValue = sdk?.getSessionResources?.() ?? [];
|