4 |
5 |
6 |
7 | chat-xiuliu
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const { BrowserWindow, app, ipcMain, shell, Menu, dialog } = require('electron')
2 | const path = require('node:path')
3 | const fs = require('node:fs')
4 | const { format } = require('node:util')
5 | const { pathToFileURL } = require('node:url')
6 | const { nanoid } = require('nanoid')
7 | const sound = require('sound-play')
8 | const _ = require('lodash')
9 | const windowStateKeeper = require('electron-window-state')
10 | const { EdgeTTS } = require('node-edge-tts')
11 |
12 | const { STORE_PATH, LOG_PATH, AUDIO_PATH } = require('./utils/initFile.js')
13 | const { getRootPath } = require('./utils/fileTool.js')
14 | const { getStore, setStore } = require('./modules/store.js')
15 | const { getSpeechText } = require('./modules/whisper.js')
16 | const { getTokenLength } = require('./modules/tiktoken.js')
17 | const { openaiChatStream, openaiEmbedding } = require('./modules/common.js')
18 | const { functionAction, functionInfo, functionList } = require('./modules/functions.js')
19 | const { addText, searchSimilarText, cosineSimilarity } = require('./modules/vectorDb.js')
20 | const { config } = require('./utils/loadConfig.js')
21 | const {
22 | DEFAULT_MODEL,
23 | SpeechSynthesisVoiceName,
24 | ADMIN_NAME, AI_NAME,
25 | systemPrompt,
26 | useProxy,
27 | proxyObject,
28 | historyRoundLimit = 12,
29 | functionCallingRoundLimit = 3,
30 | disableFunctions = [],
31 | searchResultLimit = 5,
32 | webPageContentTokenLengthLimit = 6000,
33 | } = config
34 | const proxyString = `${proxyObject.protocol}://${proxyObject.host}:${proxyObject.port}`
35 |
36 | let pdfjsLib
37 | ;(async () => {
38 | const pdfjsDistUrl = pathToFileURL(path.join(getRootPath(), 'resources/extraResources/pdfjs-4.2.67-legacy-dist/build/pdf.mjs'))
39 | pdfjsLib = await import(pdfjsDistUrl)
40 | })()
41 |
42 | const logFile = fs.createWriteStream(path.join(LOG_PATH, `log-${new Date().toLocaleString('zh-CN').replace(/[\/:]/gi, '-')}.txt`), { flags: 'w' })
43 | const messageLog = (message) => {
44 | logFile.write(format(new Date().toLocaleString('zh-CN'), JSON.stringify(message)) + '\n')
45 | }
46 | const messageSend = (message) => {
47 | mainWindow.webContents.send('send-message', message)
48 | }
49 | const messageLogAndSend = (message) => {
50 | messageLog(message)
51 | messageSend(message)
52 | }
53 |
54 | let errorlogFile = fs.createWriteStream(path.join(LOG_PATH, 'error_log.txt'), { flags: 'w' })
55 | console.error = (...message)=>{
56 | errorlogFile.write('\n' + format(new Date().toLocaleString()) + '\n')
57 | errorlogFile.write(format(...message) + '\n')
58 | process.stderr.write(format(...message) + '\n')
59 | }
60 | process
61 | .on('unhandledRejection', (reason, promise) => {
62 | console.error('Unhandled Rejection at:', promise, 'reason:', reason)
63 | })
64 | .on('uncaughtException', err => {
65 | console.error(err, 'Uncaught Exception thrown')
66 | process.exit(1)
67 | })
68 |
69 | const STATUS = {
70 | isSpeechTalk: false,
71 | isAudioPlay: false,
72 | recordStatus: 'Recording',
73 | speakIndex: 0,
74 | answeringId: null,
75 | breakAnswerId: null
76 | }
77 |
78 | let speakTextList = []
79 | let tts = new EdgeTTS({
80 | voice: SpeechSynthesisVoiceName,
81 | lang: 'zh-CN',
82 | outputFormat: 'audio-24khz-96kbitrate-mono-mp3',
83 | proxy: useProxy ? proxyString : undefined,
84 | saveSubtitles: true
85 | })
86 |
87 | let mainWindow
88 | const createWindow = () => {
89 | let mainWindowState = windowStateKeeper({
90 | defaultWidth: 1280,
91 | defaultHeight: 720
92 | })
93 | const win = new BrowserWindow({
94 | x: mainWindowState.x,
95 | y: mainWindowState.y,
96 | width: mainWindowState.width,
97 | height: mainWindowState.height,
98 | webPreferences: {
99 | webSecurity: app.isPackaged ? true : false,
100 | preload: path.join(__dirname, 'preload.js')
101 | },
102 | show: false
103 | })
104 | mainWindowState.manage(win)
105 | if (app.isPackaged) {
106 | win.loadFile('dist/index.html')
107 | } else {
108 | win.loadURL('http://localhost:5174')
109 | }
110 | win.setMenuBarVisibility(false)
111 | win.setAutoHideMenuBar(true)
112 | const template = [
113 | {
114 | label: 'File',
115 | submenu: [
116 | {
117 | role: 'quit',
118 | accelerator: 'CommandOrControl+Q'
119 | }
120 | ]
121 | },
122 | {
123 | label: 'View',
124 | submenu: [
125 | { role: 'reload' },
126 | { role: 'toggleDevTools' },
127 | {
128 | role: 'toggleDevTools',
129 | accelerator: 'F12',
130 | visible: false
131 | },
132 | { type: 'separator' },
133 | { role: 'resetZoom' },
134 | { role: 'zoomIn' },
135 | {
136 | role: 'zoomIn',
137 | accelerator: 'CommandOrControl+=',
138 | visible: false
139 | },
140 | { role: 'zoomOut' },
141 | { type: 'separator' },
142 | { role: 'minimize' },
143 | { role: 'togglefullscreen' }
144 | ]
145 | }
146 | ]
147 | let menu = Menu.buildFromTemplate(template)
148 | Menu.setApplicationMenu(menu)
149 | win.webContents.on('did-finish-load', () => {
150 | win.setTitle(app.getName() + ' ' + app.getVersion())
151 | })
152 | win.once('ready-to-show', () => {
153 | win.show()
154 | })
155 | return win
156 | }
157 |
158 | const useOpenaiEmbeddingFunction = openaiEmbedding
159 | app.whenReady().then(async () => {
160 | mainWindow = createWindow()
161 | setInterval(() => mainWindow.webContents.send('send-status', STATUS), 1000)
162 |
163 | const currentArchiveId = getStore('current_archive_id')
164 | if (currentArchiveId) {
165 | const archives = getStore('history_archives') || []
166 | const archive = archives.find(a => a.id === currentArchiveId)
167 |
168 | if (archive) {
169 | setStore('history', archive.history)
170 | }
171 | }
172 | })
173 |
174 |
175 | app.on('activate', () => {
176 | if (BrowserWindow.getAllWindows().length === 0) {
177 | mainWindow = createWindow()
178 | }
179 | })
180 | app.on('window-all-closed', () => {
181 | if (process.platform !== 'darwin') {
182 | app.quit()
183 | }
184 | })
185 |
186 | /**
187 | * Executes text-to-speech and plays audio prompts.
188 | *
189 | * @param {Object} options - The options object.
190 | * @param {string} options.text - The text to be converted to speech.
191 | * @param {string} options.preAudioPath - The path to the pre-recorded audio prompt.
192 | * @return {Promise} A promise that resolves when the audio prompts have been played successfully.
193 | */
194 | const speakPrompt = async ({ text, preAudioPath }) => {
195 | try {
196 | let nextAudioPath = path.join(AUDIO_PATH, `${nanoid()}.mp3`)
197 | if (text) {
198 | if (preAudioPath) {
199 | await Promise.allSettled([
200 | tts.ttsPromise(text, nextAudioPath),
201 | sound.play(preAudioPath)
202 | ])
203 | } else {
204 | await tts.ttsPromise(text, nextAudioPath)
205 | }
206 | resolveSpeakTextList(nextAudioPath)
207 | } else if (preAudioPath) {
208 | await sound.play(preAudioPath)
209 | resolveSpeakTextList()
210 | }
211 | } catch (e) {
212 | console.error(e)
213 | resolveSpeakTextList()
214 | }
215 | }
216 |
217 | /**
218 | * Resolves the speak text list by sorting it based on the speak index. If a preAudioPath is provided, it will
219 | * get the first speak text from the list and call the speakPrompt function with the text and the preAudioPath.
220 | * If the list is empty after shifting the first element, it will call the speakPrompt function with only the preAudioPath.
221 | * If no preAudioPath is provided, it will get the first speak text from the list and call the speakPrompt function with the text.
222 | * If the list is empty after shifting the first element, it will wait for 1000 milliseconds and then call itself again.
223 | *
224 | * @param {string} preAudioPath - The path to the pre-recorded audio file.
225 | * @return {undefined}
226 | */
227 | const resolveSpeakTextList = async (preAudioPath) => {
228 | speakTextList = _.sortBy(speakTextList, 'speakIndex')
229 | if (preAudioPath) {
230 | if (speakTextList.length > 0) {
231 | let { text, triggerRecord } = speakTextList.shift()
232 | if (triggerRecord) {
233 | await sound.play(preAudioPath)
234 | triggerSpeech()
235 | setTimeout(resolveSpeakTextList, 1000)
236 | } else {
237 | speakPrompt({ text, preAudioPath })
238 | }
239 | } else {
240 | speakPrompt({ preAudioPath })
241 | }
242 | } else if (speakTextList.length > 0) {
243 | let { text, triggerRecord } = speakTextList.shift()
244 | if (triggerRecord) {
245 | triggerSpeech()
246 | setTimeout(resolveSpeakTextList, 1000)
247 | } else {
248 | speakPrompt({ text })
249 | }
250 | } else {
251 | setTimeout(resolveSpeakTextList, 1000)
252 | }
253 | }
254 |
255 | resolveSpeakTextList()
256 |
257 | const addHistory = (lines) => {
258 | let history = getStore('history') || []
259 | history.push(...lines)
260 | history = _.takeRight(history, 1000)
261 | setStore('history', history)
262 | }
263 |
264 | const useOpenaiChatStreamFunction = openaiChatStream
265 | const additionalParam = {
266 | searchResultLimit,
267 | webPageContentTokenLengthLimit
268 | }
269 | const resolveMessages = async ({ resToolCalls, resText, resTextTemp, messages, from, useFunctionCalling = false, clientMessageId, model }) => {
270 |
271 | console.log(`use ${DEFAULT_MODEL}`)
272 |
273 | STATUS.answeringId = clientMessageId
274 | let speakIndex = STATUS.speakIndex
275 | STATUS.speakIndex += 1
276 |
277 | if (!_.isEmpty(resToolCalls)) {
278 | for (let toolCall of resToolCalls) {
279 | let functionCallResult
280 | let functionCallResultMessageId = nanoid()
281 | try {
282 | messageLogAndSend({
283 | id: nanoid(),
284 | from,
285 | content: functionAction[toolCall.function.name](JSON.parse(toolCall.function.arguments))
286 | })
287 | messageLogAndSend({
288 | id: functionCallResultMessageId,
289 | from: 'Function Calling',
290 | content: ''
291 | })
292 | functionCallResult = await functionList[toolCall.function.name](JSON.parse(toolCall.function.arguments), additionalParam)
293 | } catch (e) {
294 | console.error(e)
295 | functionCallResult = e.message
296 | }
297 | messages.push({ role: 'tool', tool_call_id: toolCall.id, content: functionCallResult + '' })
298 | messageLogAndSend({
299 | id: functionCallResultMessageId,
300 | from: 'Function Calling',
301 | content: functionCallResult + ''
302 | })
303 | }
304 | }
305 | resToolCalls = []
306 | let prepareChatOption = { messages, model }
307 |
308 | if (useFunctionCalling) {
309 | prepareChatOption.tools = functionInfo.filter(f => !disableFunctions.includes(f?.function?.name))
310 | if (!_.isEmpty(prepareChatOption.tools)) prepareChatOption.tool_choice = 'auto'
311 | }
312 |
313 | // alert chat
314 | messageSend({
315 | id: clientMessageId,
316 | from,
317 | content: ''
318 | })
319 |
320 | let resTextReason = ''
321 | const reasonMessageId = nanoid()
322 |
323 | let _usage = {}
324 |
325 | try {
326 | for await (const { token, r_token, f_token, usage } of useOpenaiChatStreamFunction(prepareChatOption)) {
327 | if (token) {
328 | resTextTemp += token
329 | resText += token
330 | messageSend({
331 | id: clientMessageId,
332 | from,
333 | content: resText,
334 | allowBreak: STATUS.breakAnswerId !== clientMessageId
335 | })
336 | if (resTextTemp.includes('\n')) {
337 | let splitResText = resTextTemp.split('\n')
338 | splitResText = _.compact(splitResText)
339 | if (splitResText.length > 1) {
340 | resTextTemp = splitResText.pop()
341 | } else {
342 | resTextTemp = ''
343 | }
344 | if (STATUS.isAudioPlay) {
345 | let speakText = splitResText.join('\n')
346 | speakTextList.push({
347 | text: speakText,
348 | speakIndex,
349 | })
350 | }
351 | if (STATUS.breakAnswerId === clientMessageId) {
352 | STATUS.breakAnswerId = null
353 | break
354 | }
355 | }
356 | }
357 | if (r_token) {
358 | resTextReason += r_token
359 | messageSend({
360 | id: clientMessageId,
361 | from,
362 | action: 'revoke'
363 | })
364 | messageSend({
365 | id: reasonMessageId,
366 | from: 'CoT',
367 | content: resTextReason
368 | })
369 | }
370 | if (!_.isEmpty(f_token)) {
371 | let [{ index, id, type, function: { name, arguments: arg} } = { function: {} }] = f_token
372 | if (index !== undefined ) {
373 | if (resToolCalls[index]) {
374 | if (id) resToolCalls[index].id = id
375 | if (type) resToolCalls[index].type = type
376 | if (name) resToolCalls[index].function.name = name
377 | if (arg) resToolCalls[index].function.arguments += arg
378 | } else {
379 | resToolCalls[index] = {
380 | id, type, function: { name, arguments: arg }
381 | }
382 | }
383 | }
384 | }
385 | if (!_.isEmpty(usage)) {
386 | _usage = usage
387 | }
388 | }
389 | } catch (error) {
390 | messageSend({
391 | id: clientMessageId,
392 | from,
393 | content: `Error: ${error.message}`
394 | })
395 | throw error
396 | }
397 |
398 | if (STATUS.isAudioPlay) {
399 | if (resTextTemp) {
400 | let speakText = resTextTemp
401 | speakTextList.push({
402 | text: speakText,
403 | speakIndex,
404 | })
405 | }
406 | }
407 | STATUS.answeringId = null
408 | return {
409 | messages,
410 | resToolCalls,
411 | resTextTemp,
412 | resText,
413 | usage: _usage
414 | }
415 | }
416 |
417 | /**
418 | * Asynchronously resolves an admin prompt by generating a response based on a given prompt and trigger record.
419 | *
420 | * @param {Object} options - An object containing the prompt and trigger record.
421 | * @param {string} options.prompt - The user prompt.
422 | * @param {Object} options.triggerRecord - The trigger record object.
423 | * @return {Promise} - A promise that resolves with the generated response.
424 | */
425 | const resloveAdminPrompt = async ({ prompt, promptType = 'string', triggerRecord, givenSystemPrompt, useFullPDF }) => {
426 | let from = triggerRecord ? `(${AI_NAME})` : AI_NAME
427 | let history = getStore('history') || []
428 | let context = _.takeRight(history, historyRoundLimit)
429 |
430 | let fullSystemPrompt = givenSystemPrompt ? givenSystemPrompt : systemPrompt
431 | if (contextFileName && fileContext.length > 0) {
432 | let contextText
433 | if (useFullPDF) {
434 | contextText = fileContext.map(chunk => chunk.text).join('\n')
435 | } else {
436 | let promptText = Array.isArray(prompt) ? prompt.filter(part => part.type === 'text').map(part => part.text).join('\n') : prompt
437 | let closestChunks = findClosestEmbeddedChunks(await useOpenaiEmbeddingFunction({ input: promptText }), fileContext)
438 | contextText = closestChunks.map(chunk => chunk.text).join('\n')
439 | }
440 | fullSystemPrompt = `${fullSystemPrompt}\n\nContext: \n\n${contextText}`
441 | }
442 |
443 | let messages = [
444 | { role: 'system', content: fullSystemPrompt },
445 | // { role: 'user', content: `Hello, my name is ${ADMIN_NAME}` },
446 | // { role: 'assistant', content: `Hello, ${ADMIN_NAME}` },
447 | ...context,
448 | { role: 'user', content: prompt }
449 | ]
450 |
451 | messageLog({
452 | id: nanoid(),
453 | from: triggerRecord ? `(${ADMIN_NAME})` : ADMIN_NAME,
454 | content: prompt
455 | })
456 |
457 | let resTextTemp = ''
458 | let resText = ''
459 | let resToolCalls = []
460 | let useFunctionCalling = config.enableFunctionCalling
461 | try {
462 | let round = 0
463 | while ((resText === '' || resToolCalls.length > 0) && round <= functionCallingRoundLimit + 1) {
464 | let usage = {}
465 | if (useFunctionCalling) useFunctionCalling = round > functionCallingRoundLimit ? false : true
466 | if (!useFunctionCalling) console.log('Reached the functionCallingRoundlimit')
467 | const clientMessageId = nanoid()
468 | ;({ messages, resToolCalls, resText, resTextTemp, usage } = await resolveMessages({
469 | resToolCalls, resText, resTextTemp, messages, from, useFunctionCalling, clientMessageId
470 | }))
471 | round += 1
472 | if (!_.isEmpty(resToolCalls)) {
473 | messageLogAndSend({
474 | id: nanoid(),
475 | from,
476 | countToken: true,
477 | tokenCount: usage.total_tokens,
478 | content: 'use Function Calling'
479 | })
480 | messages.push({ role: 'assistant', content: null, tool_calls: resToolCalls })
481 | }
482 | if (_.isEmpty(resText)) {
483 | messageSend({
484 | id: clientMessageId,
485 | from,
486 | action: 'revoke'
487 | })
488 | } else {
489 | messageSend({
490 | id: clientMessageId,
491 | from,
492 | countToken: true,
493 | tokenCount: usage.total_tokens,
494 | content: resText,
495 | allowBreak: false,
496 | useContext: contextFileName,
497 | allowSave: true
498 | })
499 | }
500 | }
501 | messageLog({
502 | id: nanoid(),
503 | from,
504 | content: resText
505 | })
506 | addHistory([{ role: 'user', content: prompt }])
507 | addHistory([{ role: 'assistant', content: resText }])
508 | if (triggerRecord) {
509 | let speakIndex = STATUS.speakIndex
510 | STATUS.speakIndex += 1
511 | speakTextList.push({
512 | triggerRecord: true,
513 | speakIndex
514 | })
515 | }
516 | } catch (e) {
517 | console.error(e)
518 | if (triggerRecord && STATUS.isSpeechTalk) triggerSpeech()
519 | }
520 | return resText
521 | }
522 |
523 | const sendHistory = (limit) => {
524 | let history = getStore('history') || []
525 | history = _.takeRight(history, limit)
526 | history.forEach((item) => {
527 | switch (item.role) {
528 | case 'user':
529 | messageSend({
530 | id: nanoid(),
531 | from: ADMIN_NAME,
532 | content: item.content
533 | })
534 | break
535 | case 'assistant':
536 | let text = ''
537 | try {
538 | if (item.content !== null) {
539 | text = item.content
540 | } else {
541 | text = item.tool_calls.map( item => {
542 | return functionAction[item.function.name](JSON.parse(item.function.arguments))
543 | }).join('\n')
544 | }
545 | } catch {}
546 | messageSend({
547 | id: nanoid(),
548 | from: AI_NAME,
549 | content: text,
550 | allowSave: true
551 | })
552 | break
553 | case 'tool':
554 | messageSend({
555 | id: nanoid(),
556 | from: 'Function Calling',
557 | content: item.content
558 | })
559 | break
560 | }
561 | })
562 | }
563 |
564 | /**
565 | * Trigger speech function that listens for admin prompts and handles them accordingly.
566 | *
567 | * @return {Promise} Returns a promise that resolves when the function is complete.
568 | */
569 | const triggerSpeech = async () => {
570 | if (STATUS.isSpeechTalk) {
571 | STATUS.recordStatus = 'Recording'
572 | mainWindow.setProgressBar(100, { mode: 'indeterminate' })
573 | let adminTalk = await getSpeechText(STATUS)
574 | console.log(adminTalk)
575 | if (!STATUS.isSpeechTalk) {
576 | throw new Error('Speech is not enabled now.')
577 | }
578 | STATUS.recordStatus = 'Answering'
579 | mainWindow.setProgressBar(-1)
580 | messageLogAndSend({
581 | id: nanoid(),
582 | from: `(${ADMIN_NAME})`,
583 | content: adminTalk
584 | })
585 | resloveAdminPrompt({ prompt: adminTalk, triggerRecord: true })
586 | }
587 | }
588 |
589 | const breakAnswer = () => {
590 | if (STATUS.answeringId) {
591 | STATUS.breakAnswerId = STATUS.answeringId
592 | messageSend({
593 | id: STATUS.answeringId,
594 | allowBreak: false
595 | })
596 | }
597 | }
598 | ipcMain.handle('send-prompt', async (event, prompt) => {
599 | breakAnswer()
600 | resloveAdminPrompt({
601 | prompt: prompt.content,
602 | promptType: prompt.type,
603 | useFullPDF: prompt.useFullPDF,
604 | })
605 | })
606 | ipcMain.handle('break-answer', async () => {
607 | breakAnswer()
608 | })
609 | ipcMain.handle('switch-speech-talk', async () => {
610 | STATUS.isSpeechTalk = !STATUS.isSpeechTalk
611 | STATUS.isAudioPlay = STATUS.isSpeechTalk
612 | mainWindow.setProgressBar(-1)
613 | if (STATUS.isSpeechTalk) {
614 | triggerSpeech()
615 | }
616 | })
617 | ipcMain.handle('switch-audio', async () => {
618 | STATUS.isAudioPlay = !STATUS.isAudioPlay
619 | })
620 | ipcMain.handle('empty-history', async () => {
621 | setStore('history', [])
622 | })
623 | ipcMain.handle('load-history', async() => {
624 | sendHistory(20)
625 | })
626 | ipcMain.handle('restart-app', async()=>{
627 | app.relaunch()
628 | app.exit(0)
629 | })
630 | ipcMain.handle('save-message', async (event, message) => {
631 | const saveMessage = getStore('saveMessage') || []
632 | saveMessage.push(...message)
633 | setStore('saveMessage', saveMessage)
634 | })
635 | ipcMain.handle('load-saved-message', async () => {
636 | return getStore('saveMessage') || []
637 | })
638 | ipcMain.handle('delete-saved-message', async (event, messageIds) => {
639 | let saveMessage = getStore('saveMessage') || []
640 | saveMessage = saveMessage.filter(item => !messageIds.includes(item.id))
641 | setStore('saveMessage', saveMessage)
642 | })
643 |
644 | // setting
645 | ipcMain.handle('select-folder', async () => {
646 | let result = await dialog.showOpenDialog(mainWindow, {
647 | properties: ['openDirectory']
648 | })
649 | if (!result.canceled) {
650 | return result.filePaths[0]
651 | } else {
652 | return undefined
653 | }
654 | })
655 |
656 | ipcMain.handle('select-file', async (event, { filters } = {}) => {
657 | let result = await dialog.showOpenDialog(mainWindow, {
658 | properties: ['openFile'],
659 | filters
660 | })
661 | if (!result.canceled) {
662 | return result.filePaths[0]
663 | } else {
664 | return undefined
665 | }
666 | })
667 |
668 | ipcMain.handle('load-setting', async () => {
669 | return config
670 | })
671 |
672 | ipcMain.handle('save-setting', async (event, receiveSetting) => {
673 | return await fs.promises.writeFile(path.join(STORE_PATH, 'config.json'), JSON.stringify(receiveSetting, null, ' '), { encoding: 'utf-8' })
674 | })
675 |
676 | ipcMain.handle('get-function-info', async () => {
677 | return functionInfo
678 | })
679 |
680 | ipcMain.handle('open-external', async (event, url) => {
681 | shell.openExternal(url)
682 | })
683 |
684 | let fileContext = []
685 | let contextFileName
686 |
687 | ipcMain.handle('resolve-pdf', async (event, pdfPath) => {
688 |
689 | contextFileName = path.basename(pdfPath)
690 | const clientMessageId = nanoid()
691 | messageSend({
692 | id: clientMessageId,
693 | from: AI_NAME,
694 | content: `正在读取和解析 ${contextFileName} ...`
695 | })
696 |
697 | //检查pdfPath + '.json'是否存在,如果存在则直接返回
698 | if (fs.existsSync(pdfPath + '.json')) {
699 | fileContext = JSON.parse(await fs.promises.readFile(pdfPath + '.json', { encoding: 'utf-8' }))
700 | messageSend({
701 | id: clientMessageId,
702 | from: AI_NAME,
703 | content: `已从缓存文件中读取 ${contextFileName} 的解析结果。`
704 | })
705 | return
706 | }
707 |
708 | const data = new Uint8Array(await fs.promises.readFile(pdfPath))
709 | const pdfDocument = await pdfjsLib.getDocument({ data }).promise
710 |
711 | let chunks = []
712 | let currentChunk = ''
713 | let currentTokenCount = 0
714 | const maxTokenLength = 1024
715 |
716 | for (let pageNum = 1; pageNum <= pdfDocument.numPages; pageNum++) {
717 | const page = await pdfDocument.getPage(pageNum)
718 | const textContent = await page.getTextContent()
719 | let lastY = -1
720 |
721 | for (const item of textContent.items) {
722 | const itemText = item.str
723 | const itemTokenLength = getTokenLength(itemText)
724 |
725 | // 检查是否需要添加换行符
726 | if (lastY !== -1 && Math.abs(item.transform[5] - lastY) > 5) {
727 | if (currentTokenCount + 1 > maxTokenLength) {
728 | chunks.push({ text: currentChunk, index: chunks.length })
729 | currentChunk = ''
730 | currentTokenCount = 0
731 | }
732 | currentChunk += '\n'
733 | currentTokenCount += 1 // 换行符也算作一个token
734 | }
735 |
736 | // 检查添加该文本项是否会超过限制
737 | if (currentTokenCount + itemTokenLength > maxTokenLength) {
738 | chunks.push({ text: currentChunk, index: chunks.length })
739 | currentChunk = itemText
740 | currentTokenCount = itemTokenLength
741 | } else {
742 | currentChunk += itemText
743 | currentTokenCount += itemTokenLength
744 | }
745 |
746 | lastY = item.transform[5]
747 | }
748 |
749 | // 每页之间添加额外的换行以分隔
750 | if (currentTokenCount + 2 <= maxTokenLength) {
751 | currentChunk += '\n\n'
752 | currentTokenCount += 2
753 | } else {
754 | chunks.push({ text: currentChunk, index: chunks.length })
755 | currentChunk = '\n\n'
756 | currentTokenCount = 2
757 | }
758 | }
759 |
760 | // 添加最后一块
761 | if (currentChunk.trim().length > 0) {
762 | chunks.push({ text: currentChunk, index: chunks.length })
763 | }
764 |
765 | const chunkSize = 5
766 | const embeddedChunks = []
767 |
768 | for (let i = 0; i < chunks.length; i += chunkSize) {
769 | let batch = chunks.slice(i, i + chunkSize)
770 | let results = await Promise.allSettled(batch.map(chunk => useOpenaiEmbeddingFunction({ input: chunk.text })))
771 |
772 | // 处理结果,只收集成功的
773 | results.forEach((result, index) => {
774 | if (result.status === 'fulfilled') {
775 | embeddedChunks.push({ index: batch[index].index, text: batch[index].text, embedding: result.value })
776 | } else {
777 | console.error('Embedding failed for: ', batch[index])
778 | embeddedChunks.push({ index: batch[index].index, text: batch[index].text })
779 | }
780 | })
781 | }
782 |
783 | await fs.promises.writeFile(pdfPath + '.json', JSON.stringify(embeddedChunks, null, ' '), { encoding: 'utf-8' })
784 |
785 | fileContext = embeddedChunks
786 |
787 | messageSend({
788 | id: clientMessageId,
789 | from: AI_NAME,
790 | content: `已解析 ${contextFileName} 。`
791 | })
792 | })
793 |
794 | function findClosestEmbeddedChunks(newEmbedded, embeddedChunks) {
795 |
796 | // 为每个chunk计算与新嵌入向量的距离
797 | let similarities = embeddedChunks.map(chunk => ({
798 | chunk: chunk,
799 | similarity: cosineSimilarity(newEmbedded, chunk.embedding)
800 | }))
801 |
802 | // 按相似度降序排序,获取最相似的前两个
803 | similarities.sort((a, b) => b.similarity - a.similarity)
804 |
805 | // 只返回前三个最接近的chunks
806 | let closestChunks = similarities.slice(0, 3).map(item => item.chunk)
807 |
808 | // 按index属性对这些chunks进行升序排序
809 | closestChunks.sort((a, b) => a.index - b.index)
810 |
811 | return closestChunks
812 | }
813 |
814 | ipcMain.handle('remove-context', async (event) => {
815 | fileContext = []
816 | contextFileName = undefined
817 | })
818 |
819 | // 存档相关功能
820 | ipcMain.handle('archive-history', async (event, name) => {
821 | const history = getStore('history') || []
822 | const archives = getStore('history_archives') || []
823 |
824 | const newArchive = {
825 | id: nanoid(),
826 | name,
827 | date: Date.now(),
828 | history: _.cloneDeep(history)
829 | }
830 |
831 | archives.push(newArchive)
832 | setStore('history_archives', archives)
833 | return newArchive.id
834 | })
835 |
836 | ipcMain.handle('get-history-archives', async () => {
837 | const archives = getStore('history_archives') || []
838 | return archives.map(archive => ({
839 | id: archive.id,
840 | name: archive.name,
841 | date: archive.date
842 | }))
843 | })
844 |
845 | ipcMain.handle('switch-to-archive', async (event, archiveId) => {
846 | const archives = getStore('history_archives') || []
847 | const archive = archives.find(a => a.id === archiveId)
848 |
849 | if (archive) {
850 | setStore('history', archive.history)
851 | return true
852 | }
853 | return false
854 | })
855 |
856 | ipcMain.handle('update-archive', async (event, archiveId, newName) => {
857 | const archives = getStore('history_archives') || []
858 | const archiveIndex = archives.findIndex(a => a.id === archiveId)
859 |
860 | if (archiveIndex !== -1) {
861 | const currentHistory = getStore('history') || []
862 |
863 | archives[archiveIndex] = {
864 | ...archives[archiveIndex],
865 | name: newName,
866 | date: Date.now(),
867 | history: _.cloneDeep(currentHistory)
868 | }
869 |
870 | setStore('history_archives', archives)
871 | return true
872 | }
873 | return false
874 | })
875 |
876 | ipcMain.handle('delete-archive', async (event, archiveId) => {
877 | const archives = getStore('history_archives') || []
878 | const newArchives = archives.filter(a => a.id !== archiveId)
879 |
880 | setStore('history_archives', newArchives)
881 | return true
882 | })
883 |
884 | ipcMain.handle('save-current-archive-id', async (event, archiveId) => {
885 | setStore('current_archive_id', archiveId)
886 | return true
887 | })
888 |
889 | ipcMain.handle('get-current-archive-id', async () => {
890 | return getStore('current_archive_id') || null
891 | })
--------------------------------------------------------------------------------
/modules/common.js:
--------------------------------------------------------------------------------
1 | const OpenAI = require('openai')
2 | const { HttpsProxyAgent } = require('https-proxy-agent')
3 | const _ = require('lodash')
4 |
5 | const { config: {
6 | OPENAI_API_KEY, OPENAI_API_ENDPOINT, DEFAULT_MODEL,
7 | useProxy, proxyObject
8 | } } = require('../utils/loadConfig.js')
9 | const proxyString = `${proxyObject.protocol}://${proxyObject.host}:${proxyObject.port}`
10 |
11 | let httpAgent
12 | try {
13 | httpAgent = useProxy ? new HttpsProxyAgent(proxyString) :undefined
14 | } catch {}
15 |
16 | let openai
17 | try {
18 | openai = new OpenAI({
19 | apiKey: OPENAI_API_KEY,
20 | baseURL: OPENAI_API_ENDPOINT ? OPENAI_API_ENDPOINT : 'https://api.openai.com/v1',
21 | httpAgent,
22 | timeout: 40000
23 | })
24 | } catch {}
25 |
26 |
27 | /**
28 | * Generates a chat response using the OpenAI API.
29 | * @param {*} chatOption
30 | * @returns {Promise} The response from the chat API.
31 | */
32 | const openaiChat = async (chatOption) => {
33 | chatOption.model = chatOption.model || DEFAULT_MODEL
34 | const response = await openai.chat.completions.create(chatOption)
35 | return response.data.choices[0].message
36 | }
37 |
38 | /**
39 | * Generates a chat stream using the OpenAI API.
40 | *
41 | * @param {object} options - An object containing the following properties:
42 | * - model {string}: The model to use for generating the chat stream.
43 | * - messages {array}: An array of message objects representing the conversation.
44 | * @return {generator} A generator that yields tokens from the chat stream.
45 | */
46 | const openaiChatStream = async function* ({ model = DEFAULT_MODEL, messages, tools, tool_choice }) {
47 | let response
48 | if (tools) {
49 | response = await openai.chat.completions.create({
50 | model, messages, tools, tool_choice,
51 | stream: true, stream_options: { include_usage: true }
52 | })
53 | } else {
54 | response = await openai.chat.completions.create({
55 | model, messages,
56 | stream: true, stream_options: { include_usage: true }
57 | })
58 | }
59 | for await (const part of response) {
60 | if (['stop', 'tool_calls'].includes(_.get(part, 'choices[0].delta.finish_reason'))) return
61 | const token = _.get(part, 'choices[0].delta.content')
62 | const r_token = _.get(part, 'choices[0].delta.reasoning_content')
63 | const f_token = _.get(part, 'choices[0].delta.tool_calls', [])
64 | const usage = _.get(part, 'usage', {})
65 | if (token || r_token || !_.isEmpty(f_token) || !_.isEmpty(usage)) yield { token, r_token, f_token, usage }
66 | }
67 | }
68 |
69 | const openaiEmbedding = async ({ input, model = 'text-embedding-3-small' }) => {
70 | const res = await openai.embeddings.create({
71 | model, input
72 | })
73 | return _.get(res, 'data[0].embedding')
74 | }
75 |
76 | const openaiImageCreate = async ({ model = 'gpt-image-1', prompt, n = 1, size = 'auto', quality = 'auto', background = 'auto' }) => {
77 | const response = await openai.images.generate({
78 | model, prompt, n, size, quality, background,
79 | moderation: 'low',
80 | output_format: 'png'
81 | })
82 | return response.data[0]
83 | }
84 |
85 | // reload OpenAI instance by providing new API key and endpoint
86 | const reloadOpenAI = (apiKey, apiEndpoint) => {
87 | openai = new OpenAI({
88 | apiKey,
89 | baseURL: apiEndpoint ? apiEndpoint : OPENAI_API_ENDPOINT,
90 | httpAgent,
91 | timeout: 40000
92 | })
93 | }
94 |
95 |
96 |
97 | module.exports = {
98 | openaiChat,
99 | openaiChatStream,
100 | openaiEmbedding,
101 | openaiImageCreate,
102 | reloadOpenAI
103 | }
--------------------------------------------------------------------------------
/modules/functions.js:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs')
2 | const path = require('node:path')
3 | const axios = require('axios')
4 | const { convert } = require('html-to-text')
5 | const { getQuickJS, shouldInterruptAfterDeadline } = require('quickjs-emscripten')
6 | const { shell } = require('electron')
7 | const { js: beautify } = require('js-beautify/js')
8 | const dayjs = require('dayjs')
9 |
10 | let { config: { useProxy, proxyObject, AI_NAME, writeFolder, allowPowerfulInterpreter, CustomSearchAPI } } = require('../utils/loadConfig.js')
11 | const proxyString = `${proxyObject.protocol}://${proxyObject.host}:${proxyObject.port}`
12 |
13 | const { sliceStringbyTokenLength } = require('./tiktoken.js')
14 | const { nodejs_interpreter } = require('./vm.js')
15 | const { openaiImageCreate } = require('./common.js')
16 | const { STORE_PATH } = require('../utils/fileTool.js')
17 |
18 | if (!writeFolder) writeFolder = path.join(STORE_PATH, 'storage')
19 | if (!fs.existsSync(writeFolder)) {
20 | try {
21 | fs.mkdirSync(writeFolder, { recursive: true })
22 | } catch (error) {
23 | console.error(`Error creating write folder: ${error.message}`)
24 | }
25 | }
26 |
27 | const functionInfo = [
28 | {
29 | "name": "get_information_from_google",
30 | "description": "Fetch information from Google based on a query string",
31 | "parameters": {
32 | "type": "object",
33 | "properties": {
34 | "query_string": {
35 | "type": "string",
36 | "description": "The search term to lookup",
37 | },
38 | },
39 | "required": ["query_string"],
40 | }
41 | },
42 | {
43 | "name": "get_text_content_of_webpage",
44 | "description": "get text content of webpage based on url.",
45 | "parameters": {
46 | "type": "object",
47 | "properties": {
48 | "url": {
49 | "type": "string",
50 | "description": "The url of webpage",
51 | },
52 | },
53 | "required": ["url"],
54 | }
55 | },
56 | {
57 | "name": "download_file_to_local",
58 | "description": "download file from url to local.",
59 | "parameters": {
60 | "type": "object",
61 | "properties": {
62 | "file_url": {
63 | "type": "string",
64 | "description": "The url of file",
65 | },
66 | "file_name": {
67 | "type": "string",
68 | "description": "The name of file",
69 | }
70 | },
71 | "required": ["file_url", "file_name"],
72 | }
73 | },
74 | {
75 | "name": "write_file_to_local",
76 | "description": "Write file to local disk.",
77 | "parameters": {
78 | "type": "object",
79 | "properties": {
80 | "relative_file_path": {
81 | "type": "string",
82 | "description": "Relative file path, relative to the storage folder",
83 | },
84 | "content": {
85 | "type": "string",
86 | "description": "The content of file",
87 | }
88 | },
89 | "required": ["relative_file_path", "content"],
90 | }
91 | },
92 | {
93 | "name": "read_file_from_local",
94 | "description": "read file from local disk.",
95 | "parameters": {
96 | "type": "object",
97 | "properties": {
98 | "file_path": {
99 | "type": "string",
100 | "description": "The path of file to read",
101 | }
102 | },
103 | "required": ["file_path"],
104 | }
105 | },
106 | {
107 | "name": "java_script_interpreter",
108 | "description": "Useful for running JavaScript code in sandbox. Input is a string of JavaScript code, output is the result of the code.",
109 | "parameters": {
110 | "type": "object",
111 | "properties": {
112 | "code": {
113 | "type": "string",
114 | "description": "The javascript code to run",
115 | }
116 | },
117 | "required": ["code"],
118 | }
119 | },
120 | {
121 | "name": "open_local_file_or_webpage",
122 | "description": "Open local file or webpage, display it to the user",
123 | "parameters": {
124 | "type": "object",
125 | "properties": {
126 | "file_path": {
127 | "type": "string",
128 | "description": "The path of file to open",
129 | },
130 | "url": {
131 | "type": "string",
132 | "description": "The url of webpage to open",
133 | },
134 | "type": {
135 | "type": "string",
136 | "description": "The type of file to open",
137 | "enum": ["file", "webpage"],
138 | }
139 | },
140 | "required": ["type"],
141 | }
142 | },
143 | {
144 | "name": "create_image_use_GPT",
145 | "description": `Create image using gpt-image-1. If the description is not in English, then translate it.`,
146 | "parameters": {
147 | "type": "object",
148 | "properties": {
149 | "prompt": {
150 | "type": "string",
151 | "description": "A text description of the desired image(s). The maximum length is 32000 characters",
152 | },
153 | "background": {
154 | "type": "string",
155 | "description": "Allows to set transparency for the background of the generated image(s).",
156 | "enum": ["auto", "transparent", "opaque"],
157 | },
158 | "size": {
159 | "type": "string",
160 | "description": "The size of the generated images. Must be one of 1024x1024, 1536x1024 (landscape), 1024x1536 (portrait), or auto (default value)",
161 | "enum": ["1024x1024", "1536x1024", "1024x1536", "auto"],
162 | },
163 | "quality": {
164 | "type": "string",
165 | "description": "The quality of the image that will be generated.auto (default value) will automatically select the best quality for the given model. high, medium and low are supported",
166 | "enum": ["auto", "high", "medium", "low"],
167 | },
168 | "n": {
169 | "type": "number",
170 | "description": "The number of images to generate. Must be between 1 and 10."
171 | }
172 | },
173 | "required": ["prompt"],
174 | }
175 | }
176 | ].map(f => {
177 | return {
178 | type: 'function',
179 | function: f
180 | }
181 | })
182 |
183 | if (allowPowerfulInterpreter) {
184 | let findExistInterpreter = functionInfo.findIndex(f => f.function.name === 'java_script_interpreter')
185 | if (findExistInterpreter !== -1) {
186 | functionInfo.splice(findExistInterpreter, 1, {
187 | type: 'function',
188 | function: {
189 | "name": "nodejs_interpreter",
190 | "description": `Useful for running JavaScript code in node.js(version 18) VM.
191 | You need to use global.variable = value when declaring global variables.
192 | Input is a string of JavaScript code, output is the result of the code.
193 | You can require node modules except fs, and use lodash directly.
194 | You can only store variables in the "global" object for future use, like "global.hello = function () {return 'hello'}"`,
195 | "parameters": {
196 | "type": "object",
197 | "properties": {
198 | "code": {
199 | "type": "string",
200 | "description": "The javascript code to run, write the result variable in the last line to output the result.",
201 | }
202 | },
203 | "required": ["code"],
204 | }
205 | }
206 | })
207 | }
208 | }
209 |
210 | const functionAction = {
211 | get_information_from_google ({ query_string }) {
212 | return `${AI_NAME}正在搜索 ${query_string}`
213 | },
214 | get_text_content_of_webpage ({ url }) {
215 | return `${AI_NAME}正在访问 ${url}`
216 | },
217 | download_file_to_local ({ file_url, file_name }) {
218 | return `${AI_NAME}下载了 ${file_url} 到 ${file_name}`
219 | },
220 | write_file_to_local ({ relative_file_path, content }) {
221 | return `${AI_NAME}保存\n\n${content}\n\n到 ${relative_file_path}`
222 | },
223 | read_file_from_local ({ file_path }) {
224 | return `${AI_NAME}读取了 ${file_path}`
225 | },
226 | java_script_interpreter ({ code }) {
227 | code = beautify(code, {
228 | indent_size: 2,
229 | space_after_anon_function: true,
230 | space_after_named_function: true,
231 | })
232 | return `${AI_NAME}运行了\n\n\`\`\`javascript\n${code}\n\`\`\``
233 | },
234 | nodejs_interpreter ({ code }) {
235 | code = beautify(code, {
236 | indent_size: 2,
237 | space_after_anon_function: true,
238 | space_after_named_function: true,
239 | })
240 | return `${AI_NAME}运行了\n\n\`\`\`javascript\n${code}\n\`\`\``
241 | },
242 | open_local_file_or_webpage ({ file_path, url, type }) {
243 | return `${AI_NAME}请求打开 ${type === 'file' ? file_path : url}`
244 | },
245 | create_image_use_GPT ({ prompt, n, size, quality, background }) {
246 | return `${AI_NAME}正在生成${n ? n : 1}张\`${size ? size : 'auto'}\`大小,
247 | 质量为\`${quality ? quality : 'auto'}\`, 背景为\`${background ? background : 'auto'}\`的图片.
248 | Prompt: \n\n\`\`\`json\n${prompt}\n\`\`\``
249 | }
250 | }
251 |
252 | const get_information_from_google = async ({ query_string }, { searchResultLimit }) => {
253 | const response = await axios.get(`${CustomSearchAPI}${encodeURIComponent(query_string)}`, {
254 | proxy: useProxy ? proxyObject : undefined
255 | })
256 | if (response.data.items) {
257 | return response.data.items.filter(i => i.title && i.snippet).map(i => `[${i.title}](${i.link}): ${i.snippet}`).slice(0, searchResultLimit).join('\n')
258 | } else {
259 | return '没有找到相关信息'
260 | }
261 | }
262 |
263 | const get_text_content_of_webpage = async ({ url }, { webPageContentTokenLengthLimit }) => {
264 | return await axios.get(url, { proxy: useProxy ? proxyObject : undefined })
265 | .then(async res=>{
266 | let html = await res.data
267 | let content = convert(html, {
268 | baseElements: { selectors: ['dl', 'pre', 'p'] },
269 | wordwrap: false,
270 | selectors: [
271 | // { selector: 'a', options: { ignoreHref: true } },
272 | { selector: 'img', format: 'skip' },
273 | ]
274 | })
275 | return sliceStringbyTokenLength(content, webPageContentTokenLengthLimit)
276 | })
277 | }
278 |
279 | const download_file_to_local = async ({ file_url, file_name }) => {
280 | let writefile_path = path.join(writeFolder, file_name)
281 | const response = await axios({
282 | method: 'GET',
283 | url: file_url,
284 | responseType: 'arraybuffer',
285 | proxy: useProxy ? proxyObject : undefined
286 | })
287 | await fs.promises.writeFile(writefile_path, response.data)
288 | return writefile_path
289 | }
290 |
291 | const write_file_to_local = async ({ relative_file_path, content }) => {
292 | let writefile_path = path.join(writeFolder, relative_file_path)
293 | await fs.promises.mkdir(path.dirname(writefile_path), { recursive: true })
294 | await fs.promises.writeFile(writefile_path, content)
295 | return writefile_path
296 | }
297 |
298 | const read_file_from_local = async ({ file_path }) => {
299 | return await fs.promises.readFile(file_path, { encoding: 'utf-8' })
300 | }
301 |
302 | const java_script_interpreter = async ({ code }) => {
303 | const quickjs = await getQuickJS()
304 | let result = quickjs.evalCode(code, {
305 | shouldInterrupt: shouldInterruptAfterDeadline(Date.now() + 10000),
306 | memoryLimitBytes: 100 * 1024 * 1024,
307 | })
308 | return JSON.stringify(result)
309 | }
310 |
311 | const open_local_file_or_webpage = async ({ file_path, url, type }) => {
312 | if (type === 'file') {
313 | shell.openPath(file_path)
314 | } else {
315 | shell.openExternal(url)
316 | }
317 | return `${AI_NAME}打开了 ${type === 'file' ? file_path : url}`
318 | }
319 |
320 | const _downloadImage = async (rsp) => {
321 | try {
322 | const fileId = dayjs().format('YYYYMMDDTHHmmssSSS')
323 | const image_base64 = rsp.data[0].b64_json
324 | const image_bytes = Buffer.from(image_base64, "base64")
325 | const imagePath = path.join(writeFolder, fileId + '_image.png')
326 | fs.writeFileSync(imagePath, image_bytes)
327 | await fs.promises.writeFile(path.join(writeFolder, fileId + '_prompt.txt'), rsp.revised_prompt)
328 | } catch (e) {
329 | console.error(e)
330 | }
331 | }
332 |
333 | const create_image_use_GPT = async ({ prompt, n, size, quality, background }) => {
334 | let result = await openaiImageCreate({
335 | prompt, n, size, quality, background
336 | })
337 | console.log(result)
338 | _downloadImage(result)
339 | return JSON.stringify(result)
340 | }
341 |
342 | module.exports = {
343 | functionInfo,
344 | functionAction,
345 | functionList: {
346 | get_information_from_google,
347 | get_text_content_of_webpage,
348 | download_file_to_local,
349 | write_file_to_local,
350 | read_file_from_local,
351 | java_script_interpreter,
352 | nodejs_interpreter,
353 | open_local_file_or_webpage,
354 | create_image_use_GPT
355 | }
356 | }
--------------------------------------------------------------------------------
/modules/record.js:
--------------------------------------------------------------------------------
1 | const { spawn } = require('node:child_process')
2 | const path = require('node:path')
3 | const { nanoid } = require('nanoid')
4 |
5 | const { STORE_PATH, getRootPath } = require('../utils/fileTool.js')
6 |
7 | const SPEECH_AUDIO_PATH = path.join(STORE_PATH, 'speechAudio')
8 |
9 | const sox = path.join(getRootPath(), 'resources/extraResources/sox/sox.exe')
10 | const recordPromise = () => {
11 | let audioFilePath = path.join(SPEECH_AUDIO_PATH, nanoid() + '.wav')
12 | return new Promise((resolve, reject) => {
13 | const spawned = spawn(sox, ['-d', '-t', 'waveaudio', 'default', audioFilePath, 'silence', '1', '0.1', '3%', '1', '3.0', '3%'])
14 | spawned.on('error', data => {
15 | reject(data)
16 | })
17 | spawned.on('exit', code => {
18 | if (code === 0) {
19 | resolve(audioFilePath)
20 | } else {
21 | reject('sox close code is ' + code)
22 | }
23 | })
24 | })
25 | }
26 |
27 |
28 | module.exports = {
29 | recordPromise
30 | }
--------------------------------------------------------------------------------
/modules/store.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path')
2 | const fs = require('node:fs')
3 | const { get, cloneDeep } = require('lodash')
4 |
5 | const { STORE_PATH } = require('../utils/fileTool.js')
6 |
7 | let storeData
8 | try {
9 | storeData = JSON.parse(fs.readFileSync(path.join(STORE_PATH, 'storeData.json'), { encoding: 'utf-8' }))
10 | } catch {
11 | storeData = {
12 | history: []
13 | }
14 | fs.writeFileSync(path.join(STORE_PATH, 'storeData.json'), JSON.stringify(storeData, null, ' '), { encoding: 'utf-8' })
15 | }
16 |
17 | const getStore = (key) => {
18 | return cloneDeep(get(storeData, key, null))
19 | }
20 |
21 | const setStore = (key, value) => {
22 | storeData[key] = cloneDeep(value)
23 | fs.writeFileSync(path.join(STORE_PATH, 'storeData.json'), JSON.stringify(storeData, null, ' '), { encoding: 'utf-8' })
24 | }
25 |
26 | module.exports = {
27 | getStore,
28 | setStore
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/modules/tiktoken.js:
--------------------------------------------------------------------------------
1 | const { getEncoding } = require('js-tiktoken')
2 |
3 | const tokenizer = getEncoding('o200k_base')
4 |
5 | const getTokenLength = (str = '') => {
6 | return tokenizer.encode(str).length
7 | }
8 |
9 | /**
10 | * Slices a given string by a specified token length.
11 | *
12 | * @param {string} str - The string to be sliced.
13 | * @param {number} length - The length of the token.
14 | * @return {string} The sliced string.
15 | */
16 | const sliceStringbyTokenLength = (str, length) => {
17 | let resultParts = []
18 | let currentLength = 0
19 | let strSplitList = str.split(/\n/)
20 |
21 | for (let part of strSplitList) {
22 | currentLength += getTokenLength(part)
23 | if (currentLength > length) {
24 | break
25 | }
26 | resultParts.push(part)
27 | }
28 | return resultParts.join('')
29 | }
30 |
31 | module.exports = {
32 | getTokenLength,
33 | sliceStringbyTokenLength
34 | }
--------------------------------------------------------------------------------
/modules/vectorDb.js:
--------------------------------------------------------------------------------
1 | const { Sequelize, DataTypes } = require('sequelize')
2 | const { openaiEmbedding } = require('./common.js')
3 | const { join } = require('path')
4 |
5 | const { STORE_PATH } = require('../utils/fileTool.js')
6 |
7 | const useOpenaiEmbeddingFunction = openaiEmbedding
8 |
9 | function dotProduct(vecA, vecB) {
10 | return vecA.reduce((sum, a, i) => sum + a * vecB[i], 0)
11 | }
12 |
13 | function magnitude(vec) {
14 | return Math.sqrt(vec.reduce((sum, val) => sum + val * val, 0))
15 | }
16 |
17 | function cosineSimilarity(vecA, vecB) {
18 | const dotProd = dotProduct(vecA, vecB)
19 | const magnitudeA = magnitude(vecA)
20 | const magnitudeB = magnitude(vecB)
21 | if (magnitudeA === 0 || magnitudeB === 0) return 0
22 | return dotProd / (magnitudeA * magnitudeB)
23 | }
24 |
25 | // 初始化 SQLite 数据库
26 | const sequelize = new Sequelize({
27 | dialect: 'sqlite',
28 | storage: join(STORE_PATH, 'vector.sqlite'),
29 | logging: false
30 | })
31 |
32 | // 定义 Text 模型
33 | const Text = sequelize.define('Text', {
34 | id: {
35 | type: DataTypes.UUID,
36 | allowNull: false,
37 | defaultValue: DataTypes.UUIDV4,
38 | primaryKey: true
39 | },
40 | content: {
41 | type: DataTypes.JSON,
42 | allowNull: false
43 | },
44 | type: DataTypes.STRING,
45 | embedding: {
46 | type: DataTypes.JSON,
47 | allowNull: false
48 | }
49 | })
50 |
51 | /**
52 | * 添加文本并生成嵌入向量
53 | * @param {Object} content - 添加到数据库的内容
54 | * @param {string} content.text - 文本内容
55 | * @param {string} type - 文本类型
56 | * @returns {Promise} - 无返回值
57 | */
58 | async function addText(content, type) {
59 | const embedding = await useOpenaiEmbeddingFunction({ input: content.text })
60 | await Text.create({ content, embedding, type })
61 | }
62 |
63 | /**
64 | * 搜索相似文本
65 | * @param {string} query - 查询文本
66 | * @param {number} count - 返回文本数量
67 | * @returns {Promise} - 相似文本列表, 按相似度降序排列
68 | */
69 | async function searchSimilarText(query, count) {
70 | const queryEmbedding = await useOpenaiEmbeddingFunction({ input: query })
71 | const texts = await Text.findAll()
72 |
73 | const results = texts.map(item => {
74 | const similarity = cosineSimilarity(queryEmbedding, item.embedding)
75 | return { content: item.content, similarity }
76 | })
77 |
78 | results.sort((a, b) => b.similarity - a.similarity)
79 | return results.slice(0, count).map(item => item.content)
80 | }
81 |
82 | /**
83 | * 随机获取特定数量的文本
84 | * @param {number} count - 文本数量
85 | * @returns {Promise} - 文本列表
86 | */
87 | async function getRandomTexts(count) {
88 | const texts = await Text.findAll({
89 | order: sequelize.literal('RANDOM()'),
90 | limit: count
91 | })
92 | return texts.map(item => item.content)
93 | }
94 |
95 |
96 | // 同步数据库
97 | sequelize.sync()
98 |
99 | module.exports = {
100 | addText,
101 | searchSimilarText,
102 | getRandomTexts,
103 | cosineSimilarity
104 | }
--------------------------------------------------------------------------------
/modules/vm.js:
--------------------------------------------------------------------------------
1 | const vm = require('node:vm')
2 | const _ = require('lodash')
3 |
4 |
5 | const env = {
6 | _,
7 | lodash: _,
8 | require,
9 | console,
10 | Buffer,
11 | global: {}
12 | }
13 |
14 | const context = new vm.createContext(env)
15 |
16 | const nodejs_interpreter = ({ code }) => {
17 | const script = new vm.Script(`${code}`)
18 | const result = script.runInContext(context)
19 | return JSON.stringify(result)
20 | }
21 |
22 | module.exports = {
23 | nodejs_interpreter
24 | }
--------------------------------------------------------------------------------
/modules/whisper.js:
--------------------------------------------------------------------------------
1 | const { spawn } = require('node:child_process')
2 | const path = require('node:path')
3 | const fs = require('node:fs')
4 | const { recordPromise } = require('./record.js')
5 | const { getRootPath } = require('../utils/fileTool.js')
6 |
7 | const whisper = path.join(getRootPath(), 'resources/extraResources/whisper/whisper-faster.exe')
8 |
9 | const getSpeechAudioJSON = (audioFilePath) => {
10 | return new Promise((resolve, reject) => {
11 | const spawned = spawn(whisper, [
12 | audioFilePath,
13 | '-m', 'large-v3',
14 | '--output_format', 'json',
15 | '--output_dir', path.dirname(audioFilePath),
16 | '--beep_off',
17 | '-l', 'Chinese',
18 | '-prompt', '休留,讲普通话。'
19 | ])
20 | spawned.on('error', data => {
21 | reject(data)
22 | })
23 | spawned.on('exit', code => {
24 | if (code === 0) {
25 | resolve()
26 | } else {
27 | reject('whisper close code is ' + code)
28 | }
29 | })
30 | })
31 | }
32 |
33 | const changeExtension = (filePath, extension) => {
34 | const basename = path.basename(filePath, path.extname(filePath))
35 | return path.join(path.dirname(filePath), basename + extension)
36 | }
37 |
38 | /**
39 | * Retrieves the text from a speech audio file.
40 | *
41 | * @return {string} The text extracted from the speech audio file.
42 | */
43 | const getSpeechText = async (STATUS) => {
44 | let audioFilePath = await recordPromise()
45 | STATUS.recordStatus = 'Recognizing'
46 | let jsonFilePath = changeExtension(audioFilePath, '.json')
47 | await getSpeechAudioJSON(audioFilePath)
48 | let resp = JSON.parse(fs.readFileSync(jsonFilePath, { encoding: 'utf-8' }))
49 | return resp.segments.map(s => s.text).join('')
50 | }
51 |
52 | module.exports = {
53 | getSpeechText
54 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat-xiuliu",
3 | "private": true,
4 | "version": "2.3.2",
5 | "description": "ChatGPT Client with Function Calling",
6 | "author": "SchneeHertz",
7 | "scripts": {
8 | "dev": "vite",
9 | "build": "vite build",
10 | "preview": "vite preview",
11 | "start": "chcp 65001 && electron --trace-warnings .",
12 | "pack": "electron-builder --dir",
13 | "dist": "vite build && electron-builder"
14 | },
15 | "build": {
16 | "appId": "electron.chat.xiuliu",
17 | "directories": {
18 | "output": "out"
19 | },
20 | "files": [
21 | "dist/index.html",
22 | "dist/assets/*",
23 | "index.js",
24 | "preload.js",
25 | "modules/*",
26 | "utils/*"
27 | ],
28 | "extraResources": [
29 | {
30 | "from": "resources/extraResources",
31 | "to": "extraResources",
32 | "filter": [
33 | "**/*",
34 | "!whisper/**/*"
35 | ]
36 | }
37 | ],
38 | "win": {
39 | "target": [
40 | "zip"
41 | ],
42 | "icon": "public/icon.ico"
43 | }
44 | },
45 | "devDependencies": {
46 | "@vicons/fa": "^0.12.0",
47 | "@vicons/fluent": "^0.12.0",
48 | "@vicons/ionicons4": "^0.12.0",
49 | "@vitejs/plugin-vue": "^4.0.0",
50 | "electron": "^26.2.3",
51 | "electron-builder": "^24.6.4",
52 | "highlight.js": "^11.8.0",
53 | "highlightjs-copy": "^1.0.4",
54 | "markdown-it": "^14.0.0",
55 | "markdown-it-katex-gpt": "^1.0.0",
56 | "naive-ui": "^2.38.2",
57 | "pinia": "^2.1.7",
58 | "stylus": "^0.59.0",
59 | "unplugin-auto-import": "^0.16.6",
60 | "unplugin-vue-components": "^0.25.1",
61 | "vite": "^4.1.0",
62 | "vue": "^3.2.45"
63 | },
64 | "dependencies": {
65 | "axios": "^1.4.0",
66 | "dayjs": "^1.11.10",
67 | "electron-window-state": "^5.0.3",
68 | "html-to-text": "^9.0.5",
69 | "html2canvas": "^1.4.1",
70 | "https-proxy-agent": "^7.0.1",
71 | "js-beautify": "^1.14.9",
72 | "js-tiktoken": "^1.0.12",
73 | "lodash": "^4.17.21",
74 | "nanoid": "^3.3.6",
75 | "node-edge-tts": "^1.2.2",
76 | "openai": "^4.55.0",
77 | "quickjs-emscripten": "^0.23.0",
78 | "reconnecting-websocket": "^4.4.0",
79 | "sequelize": "^6.37.3",
80 | "sound-play": "^1.1.0",
81 | "sqlite3": "^5.1.6",
82 | "ws": "^8.14.1"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/preload.js:
--------------------------------------------------------------------------------
1 | const { contextBridge, ipcRenderer } = require('electron')
2 |
3 | contextBridge.exposeInMainWorld('ipcRenderer', {
4 | invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
5 | on: (channel, listener) => ipcRenderer.on(channel, listener)
6 | })
--------------------------------------------------------------------------------
/public/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SchneeHertz/chat-xiuliu/5c60a8380cbafcdb268d1d588a474de5cb18f170/public/icon.ico
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/extraResources/pdfjs-4.2.67-legacy-dist/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
--------------------------------------------------------------------------------
/resources/extraResources/sox/LICENSE.GPL.txt:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 |
294 | Copyright (C)
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | , 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/resources/extraResources/sox/libgomp-1.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SchneeHertz/chat-xiuliu/5c60a8380cbafcdb268d1d588a474de5cb18f170/resources/extraResources/sox/libgomp-1.dll
--------------------------------------------------------------------------------
/resources/extraResources/sox/pthreadgc2.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SchneeHertz/chat-xiuliu/5c60a8380cbafcdb268d1d588a474de5cb18f170/resources/extraResources/sox/pthreadgc2.dll
--------------------------------------------------------------------------------
/resources/extraResources/sox/sox.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SchneeHertz/chat-xiuliu/5c60a8380cbafcdb268d1d588a474de5cb18f170/resources/extraResources/sox/sox.exe
--------------------------------------------------------------------------------
/resources/extraResources/sox/zlib1.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SchneeHertz/chat-xiuliu/5c60a8380cbafcdb268d1d588a474de5cb18f170/resources/extraResources/sox/zlib1.dll
--------------------------------------------------------------------------------
/screenshots/code_interpreter.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SchneeHertz/chat-xiuliu/5c60a8380cbafcdb268d1d588a474de5cb18f170/screenshots/code_interpreter.jpg
--------------------------------------------------------------------------------
/screenshots/screenshot_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SchneeHertz/chat-xiuliu/5c60a8380cbafcdb268d1d588a474de5cb18f170/screenshots/screenshot_1.jpg
--------------------------------------------------------------------------------
/screenshots/screenshot_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SchneeHertz/chat-xiuliu/5c60a8380cbafcdb268d1d588a474de5cb18f170/screenshots/screenshot_2.jpg
--------------------------------------------------------------------------------
/screenshots/setting.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SchneeHertz/chat-xiuliu/5c60a8380cbafcdb268d1d588a474de5cb18f170/screenshots/setting.jpg
--------------------------------------------------------------------------------
/screenshots/setting2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SchneeHertz/chat-xiuliu/5c60a8380cbafcdb268d1d588a474de5cb18f170/screenshots/setting2.jpg
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
331 |
332 |
333 |
334 |
335 |
340 |
341 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |