@yushaw/sanqian-chat 0.1.1
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 +156 -0
- package/dist/core/index.d.ts +156 -0
- package/dist/core/index.js +176 -0
- package/dist/core/index.mjs +149 -0
- package/dist/main/index.d.mts +58 -0
- package/dist/main/index.d.ts +58 -0
- package/dist/main/index.js +299 -0
- package/dist/main/index.mjs +272 -0
- package/dist/preload/index.d.ts +67 -0
- package/dist/preload/index.js +38 -0
- package/dist/renderer/index.d.mts +340 -0
- package/dist/renderer/index.d.ts +340 -0
- package/dist/renderer/index.js +1171 -0
- package/dist/renderer/index.mjs +1131 -0
- package/package.json +84 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BrowserWindow } from 'electron';
|
|
2
|
+
import { SanqianSDK } from '@yushaw/sanqian-sdk';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @yushaw/sanqian-chat Core Types
|
|
6
|
+
*
|
|
7
|
+
* Re-exports SDK types + chat-specific types
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type WindowPosition = 'center' | 'cursor' | 'remember' | {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
};
|
|
14
|
+
interface FloatingWindowConfig {
|
|
15
|
+
shortcut?: string;
|
|
16
|
+
position?: WindowPosition;
|
|
17
|
+
width?: number;
|
|
18
|
+
height?: number;
|
|
19
|
+
alwaysOnTop?: boolean;
|
|
20
|
+
showInTaskbar?: boolean;
|
|
21
|
+
theme?: 'light' | 'dark' | 'system';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* FloatingWindow - Main process module for floating chat window
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
interface FloatingWindowOptions extends FloatingWindowConfig {
|
|
29
|
+
/** SDK instance getter */
|
|
30
|
+
getSdk: () => SanqianSDK | null;
|
|
31
|
+
/** Agent ID getter */
|
|
32
|
+
getAgentId: () => string | null;
|
|
33
|
+
/** Path to preload script */
|
|
34
|
+
preloadPath: string;
|
|
35
|
+
/** Path to renderer HTML or URL */
|
|
36
|
+
rendererPath: string;
|
|
37
|
+
/** Dev mode - load from URL instead of file */
|
|
38
|
+
devMode?: boolean;
|
|
39
|
+
}
|
|
40
|
+
declare class FloatingWindow {
|
|
41
|
+
private window;
|
|
42
|
+
private options;
|
|
43
|
+
private savedPosition;
|
|
44
|
+
private activeStreams;
|
|
45
|
+
constructor(options: FloatingWindowOptions);
|
|
46
|
+
private createWindow;
|
|
47
|
+
private getInitialPosition;
|
|
48
|
+
private registerShortcut;
|
|
49
|
+
private setupIpcHandlers;
|
|
50
|
+
show(): void;
|
|
51
|
+
hide(): void;
|
|
52
|
+
toggle(): void;
|
|
53
|
+
isVisible(): boolean;
|
|
54
|
+
destroy(): void;
|
|
55
|
+
getWindow(): BrowserWindow | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { FloatingWindow, type FloatingWindowConfig, type FloatingWindowOptions, type WindowPosition };
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/main/index.ts
|
|
21
|
+
var main_exports = {};
|
|
22
|
+
__export(main_exports, {
|
|
23
|
+
FloatingWindow: () => FloatingWindow
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(main_exports);
|
|
26
|
+
|
|
27
|
+
// src/main/FloatingWindow.ts
|
|
28
|
+
var import_electron = require("electron");
|
|
29
|
+
var ipcHandlersRegistered = false;
|
|
30
|
+
var activeInstance = null;
|
|
31
|
+
var FloatingWindow = class {
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.window = null;
|
|
34
|
+
this.savedPosition = null;
|
|
35
|
+
this.activeStreams = /* @__PURE__ */ new Map();
|
|
36
|
+
if (activeInstance) {
|
|
37
|
+
console.warn("[FloatingWindow] Only one instance supported. Destroying previous.");
|
|
38
|
+
activeInstance.destroy();
|
|
39
|
+
}
|
|
40
|
+
activeInstance = this;
|
|
41
|
+
this.options = {
|
|
42
|
+
width: 400,
|
|
43
|
+
height: 500,
|
|
44
|
+
alwaysOnTop: true,
|
|
45
|
+
showInTaskbar: false,
|
|
46
|
+
position: "center",
|
|
47
|
+
...options
|
|
48
|
+
};
|
|
49
|
+
this.setupIpcHandlers();
|
|
50
|
+
if (this.options.shortcut) {
|
|
51
|
+
import_electron.app.whenReady().then(() => this.registerShortcut());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
createWindow() {
|
|
55
|
+
const { width, height, alwaysOnTop, showInTaskbar, preloadPath } = this.options;
|
|
56
|
+
const win = new import_electron.BrowserWindow({
|
|
57
|
+
width,
|
|
58
|
+
height,
|
|
59
|
+
show: false,
|
|
60
|
+
frame: false,
|
|
61
|
+
transparent: true,
|
|
62
|
+
resizable: true,
|
|
63
|
+
alwaysOnTop,
|
|
64
|
+
skipTaskbar: !showInTaskbar,
|
|
65
|
+
webPreferences: {
|
|
66
|
+
preload: preloadPath,
|
|
67
|
+
contextIsolation: true,
|
|
68
|
+
nodeIntegration: false
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (this.options.devMode) {
|
|
72
|
+
win.loadURL(this.options.rendererPath);
|
|
73
|
+
} else {
|
|
74
|
+
win.loadFile(this.options.rendererPath);
|
|
75
|
+
}
|
|
76
|
+
win.on("blur", () => {
|
|
77
|
+
});
|
|
78
|
+
win.on("closed", () => {
|
|
79
|
+
this.window = null;
|
|
80
|
+
});
|
|
81
|
+
return win;
|
|
82
|
+
}
|
|
83
|
+
getInitialPosition() {
|
|
84
|
+
const { position, width = 400, height = 500 } = this.options;
|
|
85
|
+
if (typeof position === "object" && "x" in position) {
|
|
86
|
+
return position;
|
|
87
|
+
}
|
|
88
|
+
if (position === "remember" && this.savedPosition) {
|
|
89
|
+
return this.savedPosition;
|
|
90
|
+
}
|
|
91
|
+
if (position === "cursor") {
|
|
92
|
+
const cursorPos = import_electron.screen.getCursorScreenPoint();
|
|
93
|
+
const display = import_electron.screen.getDisplayNearestPoint(cursorPos);
|
|
94
|
+
const x = Math.min(cursorPos.x, display.bounds.x + display.bounds.width - width);
|
|
95
|
+
const y = Math.min(cursorPos.y, display.bounds.y + display.bounds.height - height);
|
|
96
|
+
return { x, y };
|
|
97
|
+
}
|
|
98
|
+
const primaryDisplay = import_electron.screen.getPrimaryDisplay();
|
|
99
|
+
const { width: screenWidth, height: screenHeight } = primaryDisplay.workAreaSize;
|
|
100
|
+
return {
|
|
101
|
+
x: Math.round((screenWidth - width) / 2),
|
|
102
|
+
y: Math.round((screenHeight - height) / 2)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
registerShortcut() {
|
|
106
|
+
const { shortcut } = this.options;
|
|
107
|
+
if (!shortcut) return;
|
|
108
|
+
try {
|
|
109
|
+
const success = import_electron.globalShortcut.register(shortcut, () => this.toggle());
|
|
110
|
+
if (!success) {
|
|
111
|
+
console.warn(`[FloatingWindow] Failed to register shortcut: ${shortcut}`);
|
|
112
|
+
}
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error(`[FloatingWindow] Shortcut registration error:`, e);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
setupIpcHandlers() {
|
|
118
|
+
if (ipcHandlersRegistered) return;
|
|
119
|
+
ipcHandlersRegistered = true;
|
|
120
|
+
import_electron.ipcMain.handle("sanqian-chat:connect", async () => {
|
|
121
|
+
try {
|
|
122
|
+
const sdk = activeInstance?.options.getSdk();
|
|
123
|
+
if (!sdk) throw new Error("SDK not available");
|
|
124
|
+
await sdk.ensureReady();
|
|
125
|
+
return { success: true };
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return { success: false, error: e instanceof Error ? e.message : "Connection failed" };
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
import_electron.ipcMain.handle("sanqian-chat:isConnected", () => {
|
|
131
|
+
const sdk = activeInstance?.options.getSdk();
|
|
132
|
+
return sdk?.isConnected() ?? false;
|
|
133
|
+
});
|
|
134
|
+
import_electron.ipcMain.handle("sanqian-chat:stream", async (event, params) => {
|
|
135
|
+
const webContents = event.sender;
|
|
136
|
+
const { streamId, messages, conversationId } = params;
|
|
137
|
+
const sdk = activeInstance?.options.getSdk();
|
|
138
|
+
const agentId = activeInstance?.options.getAgentId();
|
|
139
|
+
if (!sdk || !agentId) {
|
|
140
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const streamState = { cancelled: false };
|
|
144
|
+
activeInstance?.activeStreams.set(streamId, streamState);
|
|
145
|
+
try {
|
|
146
|
+
await sdk.ensureReady();
|
|
147
|
+
const sdkMessages = messages.map((m) => ({ role: m.role, content: m.content }));
|
|
148
|
+
const stream = sdk.chatStream(agentId, sdkMessages, { conversationId });
|
|
149
|
+
for await (const evt of stream) {
|
|
150
|
+
if (streamState.cancelled) break;
|
|
151
|
+
switch (evt.type) {
|
|
152
|
+
case "text":
|
|
153
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "text", content: evt.content } });
|
|
154
|
+
break;
|
|
155
|
+
case "thinking":
|
|
156
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "thinking", content: evt.content } });
|
|
157
|
+
break;
|
|
158
|
+
case "tool_call":
|
|
159
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_call", tool_call: evt.tool_call } });
|
|
160
|
+
break;
|
|
161
|
+
case "tool_result":
|
|
162
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_result", tool_call_id: evt.tool_call_id, result: evt.result } });
|
|
163
|
+
break;
|
|
164
|
+
case "done":
|
|
165
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "done", conversationId: evt.conversationId, title: evt.title } });
|
|
166
|
+
break;
|
|
167
|
+
case "error":
|
|
168
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: evt.error } });
|
|
169
|
+
break;
|
|
170
|
+
default:
|
|
171
|
+
const anyEvt = evt;
|
|
172
|
+
if (anyEvt.type === "interrupt") {
|
|
173
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "interrupt", interrupt_type: anyEvt.interrupt_type, interrupt_payload: anyEvt.interrupt_payload, run_id: anyEvt.run_id } });
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {
|
|
179
|
+
if (!streamState.cancelled) {
|
|
180
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: e instanceof Error ? e.message : "Stream error" } });
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
activeInstance?.activeStreams.delete(streamId);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
import_electron.ipcMain.handle("sanqian-chat:cancelStream", (_, params) => {
|
|
187
|
+
const stream = activeInstance?.activeStreams.get(params.streamId);
|
|
188
|
+
if (stream) {
|
|
189
|
+
stream.cancelled = true;
|
|
190
|
+
activeInstance?.activeStreams.delete(params.streamId);
|
|
191
|
+
}
|
|
192
|
+
return { success: true };
|
|
193
|
+
});
|
|
194
|
+
import_electron.ipcMain.handle("sanqian-chat:hitlResponse", (_, params) => {
|
|
195
|
+
const sdk = activeInstance?.options.getSdk();
|
|
196
|
+
if (sdk && params.runId) {
|
|
197
|
+
sdk.sendHitlResponse(params.runId, params.response);
|
|
198
|
+
}
|
|
199
|
+
return { success: true };
|
|
200
|
+
});
|
|
201
|
+
import_electron.ipcMain.handle("sanqian-chat:listConversations", async (_, params) => {
|
|
202
|
+
const sdk = activeInstance?.options.getSdk();
|
|
203
|
+
const agentId = activeInstance?.options.getAgentId();
|
|
204
|
+
if (!sdk || !agentId) return { success: false, error: "SDK not ready" };
|
|
205
|
+
try {
|
|
206
|
+
const result = await sdk.listConversations({ agentId, ...params });
|
|
207
|
+
return { success: true, data: result };
|
|
208
|
+
} catch (e) {
|
|
209
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to list" };
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
import_electron.ipcMain.handle("sanqian-chat:getConversation", async (_, params) => {
|
|
213
|
+
const sdk = activeInstance?.options.getSdk();
|
|
214
|
+
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
215
|
+
try {
|
|
216
|
+
const result = await sdk.getConversation(params.conversationId, { messageLimit: params.messageLimit });
|
|
217
|
+
return { success: true, data: result };
|
|
218
|
+
} catch (e) {
|
|
219
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to get" };
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
import_electron.ipcMain.handle("sanqian-chat:deleteConversation", async (_, params) => {
|
|
223
|
+
const sdk = activeInstance?.options.getSdk();
|
|
224
|
+
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
225
|
+
try {
|
|
226
|
+
await sdk.deleteConversation(params.conversationId);
|
|
227
|
+
return { success: true };
|
|
228
|
+
} catch (e) {
|
|
229
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to delete" };
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
import_electron.ipcMain.handle("sanqian-chat:hide", () => {
|
|
233
|
+
activeInstance?.hide();
|
|
234
|
+
return { success: true };
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// Public API
|
|
238
|
+
show() {
|
|
239
|
+
if (!this.window) {
|
|
240
|
+
this.window = this.createWindow();
|
|
241
|
+
}
|
|
242
|
+
const pos = this.getInitialPosition();
|
|
243
|
+
this.window.setPosition(pos.x, pos.y);
|
|
244
|
+
this.window.show();
|
|
245
|
+
this.window.focus();
|
|
246
|
+
}
|
|
247
|
+
hide() {
|
|
248
|
+
if (this.window) {
|
|
249
|
+
if (this.options.position === "remember") {
|
|
250
|
+
const [x, y] = this.window.getPosition();
|
|
251
|
+
this.savedPosition = { x, y };
|
|
252
|
+
}
|
|
253
|
+
this.window.hide();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
toggle() {
|
|
257
|
+
if (this.window?.isVisible()) {
|
|
258
|
+
this.hide();
|
|
259
|
+
} else {
|
|
260
|
+
this.show();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
isVisible() {
|
|
264
|
+
return this.window?.isVisible() ?? false;
|
|
265
|
+
}
|
|
266
|
+
destroy() {
|
|
267
|
+
if (this.options.shortcut) {
|
|
268
|
+
import_electron.globalShortcut.unregister(this.options.shortcut);
|
|
269
|
+
}
|
|
270
|
+
this.window?.destroy();
|
|
271
|
+
this.window = null;
|
|
272
|
+
this.activeStreams.forEach((stream) => {
|
|
273
|
+
stream.cancelled = true;
|
|
274
|
+
});
|
|
275
|
+
this.activeStreams.clear();
|
|
276
|
+
if (activeInstance === this) {
|
|
277
|
+
activeInstance = null;
|
|
278
|
+
if (ipcHandlersRegistered) {
|
|
279
|
+
import_electron.ipcMain.removeHandler("sanqian-chat:connect");
|
|
280
|
+
import_electron.ipcMain.removeHandler("sanqian-chat:isConnected");
|
|
281
|
+
import_electron.ipcMain.removeHandler("sanqian-chat:stream");
|
|
282
|
+
import_electron.ipcMain.removeHandler("sanqian-chat:cancelStream");
|
|
283
|
+
import_electron.ipcMain.removeHandler("sanqian-chat:hitlResponse");
|
|
284
|
+
import_electron.ipcMain.removeHandler("sanqian-chat:listConversations");
|
|
285
|
+
import_electron.ipcMain.removeHandler("sanqian-chat:getConversation");
|
|
286
|
+
import_electron.ipcMain.removeHandler("sanqian-chat:deleteConversation");
|
|
287
|
+
import_electron.ipcMain.removeHandler("sanqian-chat:hide");
|
|
288
|
+
ipcHandlersRegistered = false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
getWindow() {
|
|
293
|
+
return this.window;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
297
|
+
0 && (module.exports = {
|
|
298
|
+
FloatingWindow
|
|
299
|
+
});
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// src/main/FloatingWindow.ts
|
|
2
|
+
import { BrowserWindow, globalShortcut, screen, ipcMain, app } from "electron";
|
|
3
|
+
var ipcHandlersRegistered = false;
|
|
4
|
+
var activeInstance = null;
|
|
5
|
+
var FloatingWindow = class {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.window = null;
|
|
8
|
+
this.savedPosition = null;
|
|
9
|
+
this.activeStreams = /* @__PURE__ */ new Map();
|
|
10
|
+
if (activeInstance) {
|
|
11
|
+
console.warn("[FloatingWindow] Only one instance supported. Destroying previous.");
|
|
12
|
+
activeInstance.destroy();
|
|
13
|
+
}
|
|
14
|
+
activeInstance = this;
|
|
15
|
+
this.options = {
|
|
16
|
+
width: 400,
|
|
17
|
+
height: 500,
|
|
18
|
+
alwaysOnTop: true,
|
|
19
|
+
showInTaskbar: false,
|
|
20
|
+
position: "center",
|
|
21
|
+
...options
|
|
22
|
+
};
|
|
23
|
+
this.setupIpcHandlers();
|
|
24
|
+
if (this.options.shortcut) {
|
|
25
|
+
app.whenReady().then(() => this.registerShortcut());
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
createWindow() {
|
|
29
|
+
const { width, height, alwaysOnTop, showInTaskbar, preloadPath } = this.options;
|
|
30
|
+
const win = new BrowserWindow({
|
|
31
|
+
width,
|
|
32
|
+
height,
|
|
33
|
+
show: false,
|
|
34
|
+
frame: false,
|
|
35
|
+
transparent: true,
|
|
36
|
+
resizable: true,
|
|
37
|
+
alwaysOnTop,
|
|
38
|
+
skipTaskbar: !showInTaskbar,
|
|
39
|
+
webPreferences: {
|
|
40
|
+
preload: preloadPath,
|
|
41
|
+
contextIsolation: true,
|
|
42
|
+
nodeIntegration: false
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
if (this.options.devMode) {
|
|
46
|
+
win.loadURL(this.options.rendererPath);
|
|
47
|
+
} else {
|
|
48
|
+
win.loadFile(this.options.rendererPath);
|
|
49
|
+
}
|
|
50
|
+
win.on("blur", () => {
|
|
51
|
+
});
|
|
52
|
+
win.on("closed", () => {
|
|
53
|
+
this.window = null;
|
|
54
|
+
});
|
|
55
|
+
return win;
|
|
56
|
+
}
|
|
57
|
+
getInitialPosition() {
|
|
58
|
+
const { position, width = 400, height = 500 } = this.options;
|
|
59
|
+
if (typeof position === "object" && "x" in position) {
|
|
60
|
+
return position;
|
|
61
|
+
}
|
|
62
|
+
if (position === "remember" && this.savedPosition) {
|
|
63
|
+
return this.savedPosition;
|
|
64
|
+
}
|
|
65
|
+
if (position === "cursor") {
|
|
66
|
+
const cursorPos = screen.getCursorScreenPoint();
|
|
67
|
+
const display = screen.getDisplayNearestPoint(cursorPos);
|
|
68
|
+
const x = Math.min(cursorPos.x, display.bounds.x + display.bounds.width - width);
|
|
69
|
+
const y = Math.min(cursorPos.y, display.bounds.y + display.bounds.height - height);
|
|
70
|
+
return { x, y };
|
|
71
|
+
}
|
|
72
|
+
const primaryDisplay = screen.getPrimaryDisplay();
|
|
73
|
+
const { width: screenWidth, height: screenHeight } = primaryDisplay.workAreaSize;
|
|
74
|
+
return {
|
|
75
|
+
x: Math.round((screenWidth - width) / 2),
|
|
76
|
+
y: Math.round((screenHeight - height) / 2)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
registerShortcut() {
|
|
80
|
+
const { shortcut } = this.options;
|
|
81
|
+
if (!shortcut) return;
|
|
82
|
+
try {
|
|
83
|
+
const success = globalShortcut.register(shortcut, () => this.toggle());
|
|
84
|
+
if (!success) {
|
|
85
|
+
console.warn(`[FloatingWindow] Failed to register shortcut: ${shortcut}`);
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.error(`[FloatingWindow] Shortcut registration error:`, e);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
setupIpcHandlers() {
|
|
92
|
+
if (ipcHandlersRegistered) return;
|
|
93
|
+
ipcHandlersRegistered = true;
|
|
94
|
+
ipcMain.handle("sanqian-chat:connect", async () => {
|
|
95
|
+
try {
|
|
96
|
+
const sdk = activeInstance?.options.getSdk();
|
|
97
|
+
if (!sdk) throw new Error("SDK not available");
|
|
98
|
+
await sdk.ensureReady();
|
|
99
|
+
return { success: true };
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return { success: false, error: e instanceof Error ? e.message : "Connection failed" };
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
ipcMain.handle("sanqian-chat:isConnected", () => {
|
|
105
|
+
const sdk = activeInstance?.options.getSdk();
|
|
106
|
+
return sdk?.isConnected() ?? false;
|
|
107
|
+
});
|
|
108
|
+
ipcMain.handle("sanqian-chat:stream", async (event, params) => {
|
|
109
|
+
const webContents = event.sender;
|
|
110
|
+
const { streamId, messages, conversationId } = params;
|
|
111
|
+
const sdk = activeInstance?.options.getSdk();
|
|
112
|
+
const agentId = activeInstance?.options.getAgentId();
|
|
113
|
+
if (!sdk || !agentId) {
|
|
114
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const streamState = { cancelled: false };
|
|
118
|
+
activeInstance?.activeStreams.set(streamId, streamState);
|
|
119
|
+
try {
|
|
120
|
+
await sdk.ensureReady();
|
|
121
|
+
const sdkMessages = messages.map((m) => ({ role: m.role, content: m.content }));
|
|
122
|
+
const stream = sdk.chatStream(agentId, sdkMessages, { conversationId });
|
|
123
|
+
for await (const evt of stream) {
|
|
124
|
+
if (streamState.cancelled) break;
|
|
125
|
+
switch (evt.type) {
|
|
126
|
+
case "text":
|
|
127
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "text", content: evt.content } });
|
|
128
|
+
break;
|
|
129
|
+
case "thinking":
|
|
130
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "thinking", content: evt.content } });
|
|
131
|
+
break;
|
|
132
|
+
case "tool_call":
|
|
133
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_call", tool_call: evt.tool_call } });
|
|
134
|
+
break;
|
|
135
|
+
case "tool_result":
|
|
136
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_result", tool_call_id: evt.tool_call_id, result: evt.result } });
|
|
137
|
+
break;
|
|
138
|
+
case "done":
|
|
139
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "done", conversationId: evt.conversationId, title: evt.title } });
|
|
140
|
+
break;
|
|
141
|
+
case "error":
|
|
142
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: evt.error } });
|
|
143
|
+
break;
|
|
144
|
+
default:
|
|
145
|
+
const anyEvt = evt;
|
|
146
|
+
if (anyEvt.type === "interrupt") {
|
|
147
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "interrupt", interrupt_type: anyEvt.interrupt_type, interrupt_payload: anyEvt.interrupt_payload, run_id: anyEvt.run_id } });
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
if (!streamState.cancelled) {
|
|
154
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: e instanceof Error ? e.message : "Stream error" } });
|
|
155
|
+
}
|
|
156
|
+
} finally {
|
|
157
|
+
activeInstance?.activeStreams.delete(streamId);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
ipcMain.handle("sanqian-chat:cancelStream", (_, params) => {
|
|
161
|
+
const stream = activeInstance?.activeStreams.get(params.streamId);
|
|
162
|
+
if (stream) {
|
|
163
|
+
stream.cancelled = true;
|
|
164
|
+
activeInstance?.activeStreams.delete(params.streamId);
|
|
165
|
+
}
|
|
166
|
+
return { success: true };
|
|
167
|
+
});
|
|
168
|
+
ipcMain.handle("sanqian-chat:hitlResponse", (_, params) => {
|
|
169
|
+
const sdk = activeInstance?.options.getSdk();
|
|
170
|
+
if (sdk && params.runId) {
|
|
171
|
+
sdk.sendHitlResponse(params.runId, params.response);
|
|
172
|
+
}
|
|
173
|
+
return { success: true };
|
|
174
|
+
});
|
|
175
|
+
ipcMain.handle("sanqian-chat:listConversations", async (_, params) => {
|
|
176
|
+
const sdk = activeInstance?.options.getSdk();
|
|
177
|
+
const agentId = activeInstance?.options.getAgentId();
|
|
178
|
+
if (!sdk || !agentId) return { success: false, error: "SDK not ready" };
|
|
179
|
+
try {
|
|
180
|
+
const result = await sdk.listConversations({ agentId, ...params });
|
|
181
|
+
return { success: true, data: result };
|
|
182
|
+
} catch (e) {
|
|
183
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to list" };
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
ipcMain.handle("sanqian-chat:getConversation", async (_, params) => {
|
|
187
|
+
const sdk = activeInstance?.options.getSdk();
|
|
188
|
+
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
189
|
+
try {
|
|
190
|
+
const result = await sdk.getConversation(params.conversationId, { messageLimit: params.messageLimit });
|
|
191
|
+
return { success: true, data: result };
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to get" };
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
ipcMain.handle("sanqian-chat:deleteConversation", async (_, params) => {
|
|
197
|
+
const sdk = activeInstance?.options.getSdk();
|
|
198
|
+
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
199
|
+
try {
|
|
200
|
+
await sdk.deleteConversation(params.conversationId);
|
|
201
|
+
return { success: true };
|
|
202
|
+
} catch (e) {
|
|
203
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to delete" };
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
ipcMain.handle("sanqian-chat:hide", () => {
|
|
207
|
+
activeInstance?.hide();
|
|
208
|
+
return { success: true };
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// Public API
|
|
212
|
+
show() {
|
|
213
|
+
if (!this.window) {
|
|
214
|
+
this.window = this.createWindow();
|
|
215
|
+
}
|
|
216
|
+
const pos = this.getInitialPosition();
|
|
217
|
+
this.window.setPosition(pos.x, pos.y);
|
|
218
|
+
this.window.show();
|
|
219
|
+
this.window.focus();
|
|
220
|
+
}
|
|
221
|
+
hide() {
|
|
222
|
+
if (this.window) {
|
|
223
|
+
if (this.options.position === "remember") {
|
|
224
|
+
const [x, y] = this.window.getPosition();
|
|
225
|
+
this.savedPosition = { x, y };
|
|
226
|
+
}
|
|
227
|
+
this.window.hide();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
toggle() {
|
|
231
|
+
if (this.window?.isVisible()) {
|
|
232
|
+
this.hide();
|
|
233
|
+
} else {
|
|
234
|
+
this.show();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
isVisible() {
|
|
238
|
+
return this.window?.isVisible() ?? false;
|
|
239
|
+
}
|
|
240
|
+
destroy() {
|
|
241
|
+
if (this.options.shortcut) {
|
|
242
|
+
globalShortcut.unregister(this.options.shortcut);
|
|
243
|
+
}
|
|
244
|
+
this.window?.destroy();
|
|
245
|
+
this.window = null;
|
|
246
|
+
this.activeStreams.forEach((stream) => {
|
|
247
|
+
stream.cancelled = true;
|
|
248
|
+
});
|
|
249
|
+
this.activeStreams.clear();
|
|
250
|
+
if (activeInstance === this) {
|
|
251
|
+
activeInstance = null;
|
|
252
|
+
if (ipcHandlersRegistered) {
|
|
253
|
+
ipcMain.removeHandler("sanqian-chat:connect");
|
|
254
|
+
ipcMain.removeHandler("sanqian-chat:isConnected");
|
|
255
|
+
ipcMain.removeHandler("sanqian-chat:stream");
|
|
256
|
+
ipcMain.removeHandler("sanqian-chat:cancelStream");
|
|
257
|
+
ipcMain.removeHandler("sanqian-chat:hitlResponse");
|
|
258
|
+
ipcMain.removeHandler("sanqian-chat:listConversations");
|
|
259
|
+
ipcMain.removeHandler("sanqian-chat:getConversation");
|
|
260
|
+
ipcMain.removeHandler("sanqian-chat:deleteConversation");
|
|
261
|
+
ipcMain.removeHandler("sanqian-chat:hide");
|
|
262
|
+
ipcHandlersRegistered = false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
getWindow() {
|
|
267
|
+
return this.window;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
export {
|
|
271
|
+
FloatingWindow
|
|
272
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { HitlResponse } from '@yushaw/sanqian-sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @yushaw/sanqian-chat Preload
|
|
5
|
+
*
|
|
6
|
+
* Exposes IPC methods to renderer via contextBridge
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface SanqianChatAPI {
|
|
10
|
+
connect(): Promise<{
|
|
11
|
+
success: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
}>;
|
|
14
|
+
isConnected(): Promise<boolean>;
|
|
15
|
+
stream(params: {
|
|
16
|
+
streamId: string;
|
|
17
|
+
messages: Array<{
|
|
18
|
+
role: string;
|
|
19
|
+
content: string;
|
|
20
|
+
}>;
|
|
21
|
+
conversationId?: string;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
cancelStream(params: {
|
|
24
|
+
streamId: string;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
success: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
onStreamEvent(callback: (streamId: string, event: unknown) => void): () => void;
|
|
29
|
+
sendHitlResponse(params: {
|
|
30
|
+
response: HitlResponse;
|
|
31
|
+
runId?: string;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
success: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
listConversations(params?: {
|
|
36
|
+
limit?: number;
|
|
37
|
+
offset?: number;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
success: boolean;
|
|
40
|
+
data?: unknown;
|
|
41
|
+
error?: string;
|
|
42
|
+
}>;
|
|
43
|
+
getConversation(params: {
|
|
44
|
+
conversationId: string;
|
|
45
|
+
messageLimit?: number;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
success: boolean;
|
|
48
|
+
data?: unknown;
|
|
49
|
+
error?: string;
|
|
50
|
+
}>;
|
|
51
|
+
deleteConversation(params: {
|
|
52
|
+
conversationId: string;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
success: boolean;
|
|
55
|
+
error?: string;
|
|
56
|
+
}>;
|
|
57
|
+
hide(): Promise<{
|
|
58
|
+
success: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
declare global {
|
|
62
|
+
interface Window {
|
|
63
|
+
sanqianChat: SanqianChatAPI;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type { SanqianChatAPI };
|