Merge branch 'main' into add/model-list
This commit is contained in:
commit
a2a81e9639
1
Makefile
1
Makefile
@ -18,6 +18,7 @@ ifeq ($(OS),Windows_NT)
|
|||||||
yarn config set network-timeout 300000
|
yarn config set network-timeout 300000
|
||||||
endif
|
endif
|
||||||
yarn build:core
|
yarn build:core
|
||||||
|
yarn build:server
|
||||||
yarn install
|
yarn install
|
||||||
yarn build:extensions
|
yarn build:extensions
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,27 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
},
|
},
|
||||||
|
"exports": {
|
||||||
|
".": "./dist/core.umd.js",
|
||||||
|
"./sdk": "./dist/core.umd.js",
|
||||||
|
"./node": "./dist/node/index.cjs.js"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
".": [
|
||||||
|
"./dist/core.es5.js.map",
|
||||||
|
"./dist/types/index.d.ts"
|
||||||
|
],
|
||||||
|
"sdk": [
|
||||||
|
"./dist/core.es5.js.map",
|
||||||
|
"./dist/types/index.d.ts"
|
||||||
|
],
|
||||||
|
"node": [
|
||||||
|
"./dist/node/index.cjs.js.map",
|
||||||
|
"./dist/types/node/index.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
|
"lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
|
||||||
"prebuild": "rimraf dist",
|
"prebuild": "rimraf dist",
|
||||||
|
|||||||
@ -8,14 +8,15 @@ const pkg = require('./package.json')
|
|||||||
|
|
||||||
const libraryName = 'core'
|
const libraryName = 'core'
|
||||||
|
|
||||||
export default {
|
export default [
|
||||||
|
{
|
||||||
input: `src/index.ts`,
|
input: `src/index.ts`,
|
||||||
output: [
|
output: [
|
||||||
{ file: pkg.main, name: libraryName, format: 'umd', sourcemap: true },
|
{ file: pkg.main, name: libraryName, format: 'umd', sourcemap: true },
|
||||||
{ file: pkg.module, format: 'es', sourcemap: true },
|
{ file: pkg.module, format: 'es', sourcemap: true },
|
||||||
],
|
],
|
||||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||||
external: [],
|
external: ['path'],
|
||||||
watch: {
|
watch: {
|
||||||
include: 'src/**',
|
include: 'src/**',
|
||||||
},
|
},
|
||||||
@ -34,4 +35,42 @@ export default {
|
|||||||
// Resolve source maps to the original source
|
// Resolve source maps to the original source
|
||||||
sourceMaps(),
|
sourceMaps(),
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
input: `src/node/index.ts`,
|
||||||
|
output: [{ file: 'dist/node/index.cjs.js', format: 'cjs', sourcemap: true }],
|
||||||
|
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||||
|
external: [
|
||||||
|
'fs/promises',
|
||||||
|
'path',
|
||||||
|
'pacote',
|
||||||
|
'@types/pacote',
|
||||||
|
'@npmcli/arborist',
|
||||||
|
'ulid',
|
||||||
|
'node-fetch',
|
||||||
|
'fs',
|
||||||
|
'request',
|
||||||
|
'crypto',
|
||||||
|
'url',
|
||||||
|
'http',
|
||||||
|
],
|
||||||
|
watch: {
|
||||||
|
include: 'src/node/**',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// Allow json resolution
|
||||||
|
json(),
|
||||||
|
// Compile TypeScript files
|
||||||
|
typescript({ useTsconfigDeclarationDir: true }),
|
||||||
|
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
|
||||||
|
commonjs(),
|
||||||
|
// Allow node_modules resolution, so you can use 'external' to control
|
||||||
|
// which external modules to include in the bundle
|
||||||
|
// https://github.com/rollup/rollup-plugin-node-resolve#usage
|
||||||
|
resolve(),
|
||||||
|
|
||||||
|
// Resolve source maps to the original source
|
||||||
|
sourceMaps(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|||||||
@ -5,11 +5,11 @@
|
|||||||
export enum AppRoute {
|
export enum AppRoute {
|
||||||
appDataPath = 'appDataPath',
|
appDataPath = 'appDataPath',
|
||||||
appVersion = 'appVersion',
|
appVersion = 'appVersion',
|
||||||
getResourcePath = 'getResourcePath',
|
|
||||||
openExternalUrl = 'openExternalUrl',
|
openExternalUrl = 'openExternalUrl',
|
||||||
openAppDirectory = 'openAppDirectory',
|
openAppDirectory = 'openAppDirectory',
|
||||||
openFileExplore = 'openFileExplorer',
|
openFileExplore = 'openFileExplorer',
|
||||||
relaunch = 'relaunch',
|
relaunch = 'relaunch',
|
||||||
|
joinPath = 'joinPath'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AppEvent {
|
export enum AppEvent {
|
||||||
@ -40,20 +40,20 @@ export enum ExtensionRoute {
|
|||||||
uninstallExtension = 'uninstallExtension',
|
uninstallExtension = 'uninstallExtension',
|
||||||
}
|
}
|
||||||
export enum FileSystemRoute {
|
export enum FileSystemRoute {
|
||||||
appendFile = 'appendFile',
|
appendFileSync = 'appendFileSync',
|
||||||
copyFile = 'copyFile',
|
copyFileSync = 'copyFileSync',
|
||||||
syncFile = 'syncFile',
|
unlinkSync = 'unlinkSync',
|
||||||
deleteFile = 'deleteFile',
|
existsSync = 'existsSync',
|
||||||
exists = 'exists',
|
readdirSync = 'readdirSync',
|
||||||
getResourcePath = 'getResourcePath',
|
mkdirSync = 'mkdirSync',
|
||||||
|
readFileSync = 'readFileSync',
|
||||||
|
rmdirSync = 'rmdirSync',
|
||||||
|
writeFileSync = 'writeFileSync',
|
||||||
|
}
|
||||||
|
export enum FileManagerRoute {
|
||||||
|
synceFile = 'syncFile',
|
||||||
getUserSpace = 'getUserSpace',
|
getUserSpace = 'getUserSpace',
|
||||||
isDirectory = 'isDirectory',
|
getResourcePath = 'getResourcePath',
|
||||||
listFiles = 'listFiles',
|
|
||||||
mkdir = 'mkdir',
|
|
||||||
readFile = 'readFile',
|
|
||||||
readLineByLine = 'readLineByLine',
|
|
||||||
rmdir = 'rmdir',
|
|
||||||
writeFile = 'writeFile',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiFunction = (...args: any[]) => any
|
export type ApiFunction = (...args: any[]) => any
|
||||||
@ -82,17 +82,23 @@ export type FileSystemRouteFunctions = {
|
|||||||
[K in FileSystemRoute]: ApiFunction
|
[K in FileSystemRoute]: ApiFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FileManagerRouteFunctions = {
|
||||||
|
[K in FileManagerRoute]: ApiFunction
|
||||||
|
}
|
||||||
|
|
||||||
export type APIFunctions = AppRouteFunctions &
|
export type APIFunctions = AppRouteFunctions &
|
||||||
AppEventFunctions &
|
AppEventFunctions &
|
||||||
DownloadRouteFunctions &
|
DownloadRouteFunctions &
|
||||||
DownloadEventFunctions &
|
DownloadEventFunctions &
|
||||||
ExtensionRouteFunctions &
|
ExtensionRouteFunctions &
|
||||||
FileSystemRouteFunctions
|
FileSystemRouteFunctions &
|
||||||
|
FileManagerRoute
|
||||||
|
|
||||||
export const APIRoutes = [
|
export const APIRoutes = [
|
||||||
...Object.values(AppRoute),
|
...Object.values(AppRoute),
|
||||||
...Object.values(DownloadRoute),
|
...Object.values(DownloadRoute),
|
||||||
...Object.values(ExtensionRoute),
|
...Object.values(ExtensionRoute),
|
||||||
...Object.values(FileSystemRoute),
|
...Object.values(FileSystemRoute),
|
||||||
|
...Object.values(FileManagerRoute),
|
||||||
]
|
]
|
||||||
export const APIEvents = [...Object.values(AppEvent), ...Object.values(DownloadEvent)]
|
export const APIEvents = [...Object.values(AppEvent), ...Object.values(DownloadEvent)]
|
||||||
|
|||||||
@ -44,6 +44,13 @@ const getUserSpace = (): Promise<string> => global.core.api?.getUserSpace()
|
|||||||
const openFileExplorer: (path: string) => Promise<any> = (path) =>
|
const openFileExplorer: (path: string) => Promise<any> = (path) =>
|
||||||
global.core.api?.openFileExplorer(path)
|
global.core.api?.openFileExplorer(path)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joins multiple paths together.
|
||||||
|
* @param paths - The paths to join.
|
||||||
|
* @returns {Promise<string>} A promise that resolves with the joined path.
|
||||||
|
*/
|
||||||
|
const joinPath: (paths: string[]) => Promise<string> = (paths) => global.core.api?.joinPath(paths)
|
||||||
|
|
||||||
const getResourcePath: () => Promise<string> = () => global.core.api?.getResourcePath()
|
const getResourcePath: () => Promise<string> = () => global.core.api?.getResourcePath()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -66,4 +73,5 @@ export {
|
|||||||
getUserSpace,
|
getUserSpace,
|
||||||
openFileExplorer,
|
openFileExplorer,
|
||||||
getResourcePath,
|
getResourcePath,
|
||||||
|
joinPath,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,89 +1,74 @@
|
|||||||
/**
|
/**
|
||||||
* Writes data to a file at the specified path.
|
* Writes data to a file at the specified path.
|
||||||
* @param {string} path - The path to the file.
|
|
||||||
* @param {string} data - The data to write to the file.
|
|
||||||
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
|
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
|
||||||
*/
|
*/
|
||||||
const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
const writeFileSync = (...args: any[]) => global.core.api?.writeFileSync(...args)
|
||||||
global.core.api?.writeFile(path, data)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the path is a directory.
|
|
||||||
* @param path - The path to check.
|
|
||||||
* @returns {boolean} A boolean indicating whether the path is a directory.
|
|
||||||
*/
|
|
||||||
const isDirectory = (path: string): Promise<boolean> => global.core.api?.isDirectory(path)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the contents of a file at the specified path.
|
* Reads the contents of a file at the specified path.
|
||||||
* @param {string} path - The path of the file to read.
|
|
||||||
* @returns {Promise<any>} A Promise that resolves with the contents of the file.
|
* @returns {Promise<any>} A Promise that resolves with the contents of the file.
|
||||||
*/
|
*/
|
||||||
const readFile: (path: string) => Promise<any> = (path) => global.core.api?.readFile(path)
|
const readFileSync = (...args: any[]) => global.core.api?.readFileSync(...args)
|
||||||
/**
|
/**
|
||||||
* Check whether the file exists
|
* Check whether the file exists
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
* @returns {boolean} A boolean indicating whether the path is a file.
|
* @returns {boolean} A boolean indicating whether the path is a file.
|
||||||
*/
|
*/
|
||||||
const exists = (path: string): Promise<boolean> => global.core.api?.exists(path)
|
const existsSync = (...args: any[]) => global.core.api?.existsSync(...args)
|
||||||
/**
|
/**
|
||||||
* List the directory files
|
* List the directory files
|
||||||
* @param {string} path - The path of the directory to list files.
|
|
||||||
* @returns {Promise<any>} A Promise that resolves with the contents of the directory.
|
* @returns {Promise<any>} A Promise that resolves with the contents of the directory.
|
||||||
*/
|
*/
|
||||||
const listFiles: (path: string) => Promise<any> = (path) => global.core.api?.listFiles(path)
|
const readdirSync = (...args: any[]) => global.core.api?.readdirSync(...args)
|
||||||
/**
|
/**
|
||||||
* Creates a directory at the specified path.
|
* Creates a directory at the specified path.
|
||||||
* @param {string} path - The path of the directory to create.
|
|
||||||
* @returns {Promise<any>} A Promise that resolves when the directory is created successfully.
|
* @returns {Promise<any>} A Promise that resolves when the directory is created successfully.
|
||||||
*/
|
*/
|
||||||
const mkdir: (path: string) => Promise<any> = (path) => global.core.api?.mkdir(path)
|
const mkdirSync = (...args: any[]) => global.core.api?.mkdirSync(...args)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a directory at the specified path.
|
* Removes a directory at the specified path.
|
||||||
* @param {string} path - The path of the directory to remove.
|
|
||||||
* @returns {Promise<any>} A Promise that resolves when the directory is removed successfully.
|
* @returns {Promise<any>} A Promise that resolves when the directory is removed successfully.
|
||||||
*/
|
*/
|
||||||
const rmdir: (path: string) => Promise<any> = (path) => global.core.api?.rmdir(path)
|
const rmdirSync = (...args: any[]) =>
|
||||||
|
global.core.api?.rmdirSync(...args, { recursive: true, force: true })
|
||||||
/**
|
/**
|
||||||
* Deletes a file from the local file system.
|
* Deletes a file from the local file system.
|
||||||
* @param {string} path - The path of the file to delete.
|
* @param {string} path - The path of the file to delete.
|
||||||
* @returns {Promise<any>} A Promise that resolves when the file is deleted.
|
* @returns {Promise<any>} A Promise that resolves when the file is deleted.
|
||||||
*/
|
*/
|
||||||
const deleteFile: (path: string) => Promise<any> = (path) => global.core.api?.deleteFile(path)
|
const unlinkSync = (...args: any[]) => global.core.api?.unlinkSync(...args)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends data to a file at the specified path.
|
* Appends data to a file at the specified path.
|
||||||
* @param path path to the file
|
|
||||||
* @param data data to append
|
|
||||||
*/
|
*/
|
||||||
const appendFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
const appendFileSync = (...args: any[]) => global.core.api?.appendFileSync(...args)
|
||||||
global.core.api?.appendFile(path, data)
|
|
||||||
|
|
||||||
const copyFile: (src: string, dest: string) => Promise<any> = (src, dest) =>
|
|
||||||
global.core.api?.copyFile(src, dest)
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes a file from a source path to a destination path.
|
||||||
|
* @param {string} src - The source path of the file to be synchronized.
|
||||||
|
* @param {string} dest - The destination path where the file will be synchronized to.
|
||||||
|
* @returns {Promise<any>} - A promise that resolves when the file has been successfully synchronized.
|
||||||
|
*/
|
||||||
const syncFile: (src: string, dest: string) => Promise<any> = (src, dest) =>
|
const syncFile: (src: string, dest: string) => Promise<any> = (src, dest) =>
|
||||||
global.core.api?.syncFile(src, dest)
|
global.core.api?.syncFile(src, dest)
|
||||||
/**
|
|
||||||
* Reads a file line by line.
|
|
||||||
* @param {string} path - The path of the file to read.
|
|
||||||
* @returns {Promise<any>} A promise that resolves to the lines of the file.
|
|
||||||
*/
|
|
||||||
const readLineByLine: (path: string) => Promise<any> = (path) =>
|
|
||||||
global.core.api?.readLineByLine(path)
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy file sync.
|
||||||
|
*/
|
||||||
|
const copyFileSync = (...args: any[]) => global.core.api?.copyFileSync(...args)
|
||||||
|
|
||||||
|
// TODO: Export `dummy` fs functions automatically
|
||||||
|
// Currently adding these manually
|
||||||
export const fs = {
|
export const fs = {
|
||||||
isDirectory,
|
writeFileSync,
|
||||||
writeFile,
|
readFileSync,
|
||||||
readFile,
|
existsSync,
|
||||||
exists,
|
readdirSync,
|
||||||
listFiles,
|
mkdirSync,
|
||||||
mkdir,
|
rmdirSync,
|
||||||
rmdir,
|
unlinkSync,
|
||||||
deleteFile,
|
appendFileSync,
|
||||||
appendFile,
|
copyFileSync,
|
||||||
readLineByLine,
|
|
||||||
copyFile,
|
|
||||||
syncFile,
|
syncFile,
|
||||||
}
|
}
|
||||||
|
|||||||
8
core/src/node/api/HttpServer.ts
Normal file
8
core/src/node/api/HttpServer.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface HttpServer {
|
||||||
|
post: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||||
|
get: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||||
|
patch: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||||
|
put: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||||
|
delete: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||||
|
register: (router: any, opts?: any) => void
|
||||||
|
}
|
||||||
335
core/src/node/api/common/builder.ts
Normal file
335
core/src/node/api/common/builder.ts
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import { JanApiRouteConfiguration, RouteConfiguration } from './configuration'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { Model, ThreadMessage } from './../../../index'
|
||||||
|
|
||||||
|
import fetch from 'node-fetch'
|
||||||
|
import { ulid } from 'ulid'
|
||||||
|
import request from 'request'
|
||||||
|
|
||||||
|
const progress = require('request-progress')
|
||||||
|
const os = require('os')
|
||||||
|
|
||||||
|
const path = join(os.homedir(), 'jan')
|
||||||
|
|
||||||
|
export const getBuilder = async (configuration: RouteConfiguration) => {
|
||||||
|
const directoryPath = join(path, configuration.dirName)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!(await fs.existsSync(directoryPath))) {
|
||||||
|
console.debug('model folder not found')
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: string[] = await fs.readdirSync(directoryPath)
|
||||||
|
|
||||||
|
const allDirectories: string[] = []
|
||||||
|
for (const file of files) {
|
||||||
|
if (file === '.DS_Store') continue
|
||||||
|
allDirectories.push(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const readJsonPromises = allDirectories.map(async (dirName) => {
|
||||||
|
const jsonPath = join(directoryPath, dirName, configuration.metadataFileName)
|
||||||
|
return await readModelMetadata(jsonPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = await Promise.all(readJsonPromises)
|
||||||
|
const modelData = results
|
||||||
|
.map((result: any) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(result)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((e: any) => !!e)
|
||||||
|
|
||||||
|
return modelData
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const readModelMetadata = async (path: string) => {
|
||||||
|
return fs.readFileSync(path, 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const retrieveBuilder = async (configuration: RouteConfiguration, id: string) => {
|
||||||
|
const data = await getBuilder(configuration)
|
||||||
|
const filteredData = data.filter((d: any) => d.id === id)[0]
|
||||||
|
|
||||||
|
if (!filteredData) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredData
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteBuilder = async (configuration: RouteConfiguration, id: string) => {
|
||||||
|
if (configuration.dirName === 'assistants' && id === 'jan') {
|
||||||
|
return {
|
||||||
|
message: 'Cannot delete Jan assistant',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const directoryPath = join(path, configuration.dirName)
|
||||||
|
try {
|
||||||
|
const data = await retrieveBuilder(configuration, id)
|
||||||
|
if (!data || !data.keys) {
|
||||||
|
return {
|
||||||
|
message: 'Not found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const myPath = join(directoryPath, id)
|
||||||
|
fs.rmdirSync(myPath, { recursive: true })
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
object: configuration.delete.object,
|
||||||
|
deleted: true,
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMessages = async (threadId: string) => {
|
||||||
|
const threadDirPath = join(path, 'threads', threadId)
|
||||||
|
const messageFile = 'messages.jsonl'
|
||||||
|
try {
|
||||||
|
const files: string[] = await fs.readdirSync(threadDirPath)
|
||||||
|
if (!files.includes(messageFile)) {
|
||||||
|
throw Error(`${threadDirPath} not contains message file`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageFilePath = join(threadDirPath, messageFile)
|
||||||
|
|
||||||
|
const lines = fs
|
||||||
|
.readFileSync(messageFilePath, 'utf-8')
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.filter((line: any) => line !== '')
|
||||||
|
|
||||||
|
const messages: ThreadMessage[] = []
|
||||||
|
lines.forEach((line: string) => {
|
||||||
|
messages.push(JSON.parse(line) as ThreadMessage)
|
||||||
|
})
|
||||||
|
return messages
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const retrieveMesasge = async (threadId: string, messageId: string) => {
|
||||||
|
const messages = await getMessages(threadId)
|
||||||
|
const filteredMessages = messages.filter((m) => m.id === messageId)
|
||||||
|
if (!filteredMessages || filteredMessages.length === 0) {
|
||||||
|
return {
|
||||||
|
message: 'Not found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredMessages[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createThread = async (thread: any) => {
|
||||||
|
const threadMetadataFileName = 'thread.json'
|
||||||
|
// TODO: add validation
|
||||||
|
if (!thread.assistants || thread.assistants.length === 0) {
|
||||||
|
return {
|
||||||
|
message: 'Thread must have at least one assistant',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const threadId = generateThreadId(thread.assistants[0].assistant_id)
|
||||||
|
try {
|
||||||
|
const updatedThread = {
|
||||||
|
...thread,
|
||||||
|
id: threadId,
|
||||||
|
created: Date.now(),
|
||||||
|
updated: Date.now(),
|
||||||
|
}
|
||||||
|
const threadDirPath = join(path, 'threads', updatedThread.id)
|
||||||
|
const threadJsonPath = join(threadDirPath, threadMetadataFileName)
|
||||||
|
|
||||||
|
if (!fs.existsSync(threadDirPath)) {
|
||||||
|
fs.mkdirSync(threadDirPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2))
|
||||||
|
return updatedThread
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
error: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateThread = async (threadId: string, thread: any) => {
|
||||||
|
const threadMetadataFileName = 'thread.json'
|
||||||
|
const currentThreadData = await retrieveBuilder(JanApiRouteConfiguration.threads, threadId)
|
||||||
|
if (!currentThreadData) {
|
||||||
|
return {
|
||||||
|
message: 'Thread not found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we don't want to update the id and object
|
||||||
|
delete thread.id
|
||||||
|
delete thread.object
|
||||||
|
|
||||||
|
const updatedThread = {
|
||||||
|
...currentThreadData,
|
||||||
|
...thread,
|
||||||
|
updated: Date.now(),
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const threadDirPath = join(path, 'threads', updatedThread.id)
|
||||||
|
const threadJsonPath = join(threadDirPath, threadMetadataFileName)
|
||||||
|
|
||||||
|
await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2))
|
||||||
|
return updatedThread
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
message: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateThreadId = (assistantId: string) => {
|
||||||
|
return `${assistantId}_${(Date.now() / 1000).toFixed(0)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMessage = async (threadId: string, message: any) => {
|
||||||
|
const threadMessagesFileName = 'messages.jsonl'
|
||||||
|
// TODO: add validation
|
||||||
|
try {
|
||||||
|
const msgId = ulid()
|
||||||
|
const createdAt = Date.now()
|
||||||
|
const threadMessage: ThreadMessage = {
|
||||||
|
...message,
|
||||||
|
id: msgId,
|
||||||
|
thread_id: threadId,
|
||||||
|
created: createdAt,
|
||||||
|
updated: createdAt,
|
||||||
|
object: 'thread.message',
|
||||||
|
}
|
||||||
|
|
||||||
|
const threadDirPath = join(path, 'threads', threadId)
|
||||||
|
const threadMessagePath = join(threadDirPath, threadMessagesFileName)
|
||||||
|
|
||||||
|
if (!fs.existsSync(threadDirPath)) {
|
||||||
|
fs.mkdirSync(threadDirPath)
|
||||||
|
}
|
||||||
|
fs.appendFileSync(threadMessagePath, JSON.stringify(threadMessage) + '\n')
|
||||||
|
return threadMessage
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
message: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const downloadModel = async (modelId: string) => {
|
||||||
|
const model = await retrieveBuilder(JanApiRouteConfiguration.models, modelId)
|
||||||
|
if (!model || model.object !== 'model') {
|
||||||
|
return {
|
||||||
|
message: 'Model not found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const directoryPath = join(path, 'models', modelId)
|
||||||
|
if (!fs.existsSync(directoryPath)) {
|
||||||
|
fs.mkdirSync(directoryPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// path to model binary
|
||||||
|
const modelBinaryPath = join(directoryPath, modelId)
|
||||||
|
const rq = request(model.source_url)
|
||||||
|
|
||||||
|
progress(rq, {})
|
||||||
|
.on('progress', function (state: any) {
|
||||||
|
console.log('progress', JSON.stringify(state, null, 2))
|
||||||
|
})
|
||||||
|
.on('error', function (err: Error) {
|
||||||
|
console.error('error', err)
|
||||||
|
})
|
||||||
|
.on('end', function () {
|
||||||
|
console.log('end')
|
||||||
|
})
|
||||||
|
.pipe(fs.createWriteStream(modelBinaryPath))
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Starting download ${modelId}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chatCompletions = async (request: any, reply: any) => {
|
||||||
|
const modelList = await getBuilder(JanApiRouteConfiguration.models)
|
||||||
|
const modelId = request.body.model
|
||||||
|
|
||||||
|
const matchedModels = modelList.filter((model: Model) => model.id === modelId)
|
||||||
|
if (matchedModels.length === 0) {
|
||||||
|
const error = {
|
||||||
|
error: {
|
||||||
|
message: `The model ${request.body.model} does not exist`,
|
||||||
|
type: 'invalid_request_error',
|
||||||
|
param: null,
|
||||||
|
code: 'model_not_found',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
reply.code(404).send(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedModel = matchedModels[0]
|
||||||
|
const engineConfiguration = await getEngineConfiguration(requestedModel.engine)
|
||||||
|
|
||||||
|
let apiKey: string | undefined = undefined
|
||||||
|
let apiUrl: string = 'http://127.0.0.1:3928/inferences/llamacpp/chat_completion' // default nitro url
|
||||||
|
|
||||||
|
if (engineConfiguration) {
|
||||||
|
apiKey = engineConfiguration.api_key
|
||||||
|
apiUrl = engineConfiguration.full_url
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.raw.writeHead(200, {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
})
|
||||||
|
|
||||||
|
const headers: Record<string, any> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['Authorization'] = `Bearer ${apiKey}`
|
||||||
|
headers['api-key'] = apiKey
|
||||||
|
}
|
||||||
|
console.log(apiUrl)
|
||||||
|
console.log(JSON.stringify(headers))
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
|
body: JSON.stringify(request.body),
|
||||||
|
})
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error(response)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
response.body.pipe(reply.raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEngineConfiguration = async (engineId: string) => {
|
||||||
|
if (engineId !== 'openai') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const directoryPath = join(path, 'engines')
|
||||||
|
const filePath = join(directoryPath, `${engineId}.json`)
|
||||||
|
const data = await fs.readFileSync(filePath, 'utf-8')
|
||||||
|
return JSON.parse(data)
|
||||||
|
}
|
||||||
31
core/src/node/api/common/configuration.ts
Normal file
31
core/src/node/api/common/configuration.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export const JanApiRouteConfiguration: Record<string, RouteConfiguration> = {
|
||||||
|
models: {
|
||||||
|
dirName: 'models',
|
||||||
|
metadataFileName: 'model.json',
|
||||||
|
delete: {
|
||||||
|
object: 'model',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assistants: {
|
||||||
|
dirName: 'assistants',
|
||||||
|
metadataFileName: 'assistant.json',
|
||||||
|
delete: {
|
||||||
|
object: 'assistant',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
threads: {
|
||||||
|
dirName: 'threads',
|
||||||
|
metadataFileName: 'thread.json',
|
||||||
|
delete: {
|
||||||
|
object: 'thread',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RouteConfiguration = {
|
||||||
|
dirName: string
|
||||||
|
metadataFileName: string
|
||||||
|
delete: {
|
||||||
|
object: string
|
||||||
|
}
|
||||||
|
}
|
||||||
2
core/src/node/api/index.ts
Normal file
2
core/src/node/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './HttpServer'
|
||||||
|
export * from './routes'
|
||||||
42
core/src/node/api/routes/common.ts
Normal file
42
core/src/node/api/routes/common.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { AppRoute } from '../../../api'
|
||||||
|
import { HttpServer } from '../HttpServer'
|
||||||
|
import { join } from 'path'
|
||||||
|
import {
|
||||||
|
chatCompletions,
|
||||||
|
deleteBuilder,
|
||||||
|
downloadModel,
|
||||||
|
getBuilder,
|
||||||
|
retrieveBuilder,
|
||||||
|
} from '../common/builder'
|
||||||
|
|
||||||
|
import { JanApiRouteConfiguration } from '../common/configuration'
|
||||||
|
|
||||||
|
export const commonRouter = async (app: HttpServer) => {
|
||||||
|
// Common Routes
|
||||||
|
Object.keys(JanApiRouteConfiguration).forEach((key) => {
|
||||||
|
app.get(`/${key}`, async (_request) => getBuilder(JanApiRouteConfiguration[key]))
|
||||||
|
|
||||||
|
app.get(`/${key}/:id`, async (request: any) =>
|
||||||
|
retrieveBuilder(JanApiRouteConfiguration[key], request.params.id),
|
||||||
|
)
|
||||||
|
|
||||||
|
app.delete(`/${key}/:id`, async (request: any) =>
|
||||||
|
deleteBuilder(JanApiRouteConfiguration[key], request.params.id),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Download Model Routes
|
||||||
|
app.get(`/models/download/:modelId`, async (request: any) =>
|
||||||
|
downloadModel(request.params.modelId),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Chat Completion Routes
|
||||||
|
app.post(`/chat/completions`, async (request: any, reply: any) => chatCompletions(request, reply))
|
||||||
|
|
||||||
|
// App Routes
|
||||||
|
app.post(`/app/${AppRoute.joinPath}`, async (request: any, reply: any) => {
|
||||||
|
const args = JSON.parse(request.body) as any[]
|
||||||
|
console.debug('joinPath: ', ...args[0])
|
||||||
|
reply.send(JSON.stringify(join(...args[0])))
|
||||||
|
})
|
||||||
|
}
|
||||||
54
core/src/node/api/routes/download.ts
Normal file
54
core/src/node/api/routes/download.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { DownloadRoute } from '../../../api'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { userSpacePath, DownloadManager, HttpServer } from '../../index'
|
||||||
|
import { createWriteStream } from 'fs'
|
||||||
|
|
||||||
|
const request = require('request')
|
||||||
|
const progress = require('request-progress')
|
||||||
|
|
||||||
|
export const downloadRouter = async (app: HttpServer) => {
|
||||||
|
app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
|
||||||
|
const body = JSON.parse(req.body as any)
|
||||||
|
const normalizedArgs = body.map((arg: any) => {
|
||||||
|
if (typeof arg === 'string' && arg.includes('file:/')) {
|
||||||
|
return join(userSpacePath, arg.replace('file:/', ''))
|
||||||
|
}
|
||||||
|
return arg
|
||||||
|
})
|
||||||
|
|
||||||
|
const localPath = normalizedArgs[1]
|
||||||
|
const fileName = localPath.split('/').pop() ?? ''
|
||||||
|
|
||||||
|
const rq = request(normalizedArgs[0])
|
||||||
|
progress(rq, {})
|
||||||
|
.on('progress', function (state: any) {
|
||||||
|
console.log('download onProgress', state)
|
||||||
|
})
|
||||||
|
.on('error', function (err: Error) {
|
||||||
|
console.log('download onError', err)
|
||||||
|
})
|
||||||
|
.on('end', function () {
|
||||||
|
console.log('download onEnd')
|
||||||
|
})
|
||||||
|
.pipe(createWriteStream(normalizedArgs[1]))
|
||||||
|
|
||||||
|
DownloadManager.instance.setRequest(fileName, rq)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
|
||||||
|
const body = JSON.parse(req.body as any)
|
||||||
|
const normalizedArgs = body.map((arg: any) => {
|
||||||
|
if (typeof arg === 'string' && arg.includes('file:/')) {
|
||||||
|
return join(userSpacePath, arg.replace('file:/', ''))
|
||||||
|
}
|
||||||
|
return arg
|
||||||
|
})
|
||||||
|
|
||||||
|
const localPath = normalizedArgs[0]
|
||||||
|
const fileName = localPath.split('/').pop() ?? ''
|
||||||
|
console.debug('fileName', fileName)
|
||||||
|
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||||
|
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||||
|
rq?.abort()
|
||||||
|
})
|
||||||
|
}
|
||||||
51
core/src/node/api/routes/extension.ts
Normal file
51
core/src/node/api/routes/extension.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { join, extname } from 'path'
|
||||||
|
import { ExtensionRoute } from '../../../api'
|
||||||
|
import {
|
||||||
|
userSpacePath,
|
||||||
|
ModuleManager,
|
||||||
|
getActiveExtensions,
|
||||||
|
installExtensions,
|
||||||
|
HttpServer,
|
||||||
|
} from '../../index'
|
||||||
|
import { readdirSync } from 'fs'
|
||||||
|
|
||||||
|
export const extensionRouter = async (app: HttpServer) => {
|
||||||
|
// TODO: Share code between node projects
|
||||||
|
app.post(`/${ExtensionRoute.getActiveExtensions}`, async (req, res) => {
|
||||||
|
const activeExtensions = await getActiveExtensions()
|
||||||
|
res.status(200).send(activeExtensions)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post(`/${ExtensionRoute.baseExtensions}`, async (req, res) => {
|
||||||
|
const baseExtensionPath = join(__dirname, '..', '..', '..', 'pre-install')
|
||||||
|
const extensions = readdirSync(baseExtensionPath)
|
||||||
|
.filter((file) => extname(file) === '.tgz')
|
||||||
|
.map((file) => join(baseExtensionPath, file))
|
||||||
|
|
||||||
|
res.status(200).send(extensions)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post(`/${ExtensionRoute.installExtension}`, async (req, res) => {
|
||||||
|
const extensions = req.body as any
|
||||||
|
const installed = await installExtensions(JSON.parse(extensions)[0])
|
||||||
|
return JSON.parse(JSON.stringify(installed))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => {
|
||||||
|
const args = JSON.parse(req.body as any)
|
||||||
|
console.debug(args)
|
||||||
|
const module = await import(join(userSpacePath, 'extensions', args[0]))
|
||||||
|
|
||||||
|
ModuleManager.instance.setModule(args[0], module)
|
||||||
|
const method = args[1]
|
||||||
|
if (typeof module[method] === 'function') {
|
||||||
|
// remove first item from args
|
||||||
|
const newArgs = args.slice(2)
|
||||||
|
console.log(newArgs)
|
||||||
|
return module[method](...args.slice(2))
|
||||||
|
} else {
|
||||||
|
console.debug(module[method])
|
||||||
|
console.error(`Function "${method}" does not exist in the module.`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
27
core/src/node/api/routes/fs.ts
Normal file
27
core/src/node/api/routes/fs.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { FileSystemRoute } from '../../../api'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { HttpServer, userSpacePath } from '../../index'
|
||||||
|
|
||||||
|
export const fsRouter = async (app: HttpServer) => {
|
||||||
|
const moduleName = 'fs'
|
||||||
|
// Generate handlers for each fs route
|
||||||
|
Object.values(FileSystemRoute).forEach((route) => {
|
||||||
|
app.post(`/${route}`, async (req, res) => {
|
||||||
|
const body = JSON.parse(req.body as any)
|
||||||
|
try {
|
||||||
|
const result = await import(moduleName).then((mdl) => {
|
||||||
|
return mdl[route](
|
||||||
|
...body.map((arg: any) =>
|
||||||
|
typeof arg === 'string' && arg.includes('file:/')
|
||||||
|
? join(userSpacePath, arg.replace('file:/', ''))
|
||||||
|
: arg,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
res.status(200).send(result)
|
||||||
|
} catch (ex) {
|
||||||
|
console.log(ex)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
6
core/src/node/api/routes/index.ts
Normal file
6
core/src/node/api/routes/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './download'
|
||||||
|
export * from './extension'
|
||||||
|
export * from './fs'
|
||||||
|
export * from './thread'
|
||||||
|
export * from './common'
|
||||||
|
export * from './v1'
|
||||||
30
core/src/node/api/routes/thread.ts
Normal file
30
core/src/node/api/routes/thread.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { HttpServer } from '../HttpServer'
|
||||||
|
import {
|
||||||
|
createMessage,
|
||||||
|
createThread,
|
||||||
|
getMessages,
|
||||||
|
retrieveMesasge,
|
||||||
|
updateThread,
|
||||||
|
} from '../common/builder'
|
||||||
|
|
||||||
|
export const threadRouter = async (app: HttpServer) => {
|
||||||
|
// create thread
|
||||||
|
app.post(`/`, async (req, res) => createThread(req.body))
|
||||||
|
|
||||||
|
app.get(`/:threadId/messages`, async (req, res) => getMessages(req.params.threadId))
|
||||||
|
|
||||||
|
// retrieve message
|
||||||
|
app.get(`/:threadId/messages/:messageId`, async (req, res) =>
|
||||||
|
retrieveMesasge(req.params.threadId, req.params.messageId),
|
||||||
|
)
|
||||||
|
|
||||||
|
// create message
|
||||||
|
app.post(`/:threadId/messages`, async (req, res) =>
|
||||||
|
createMessage(req.params.threadId as any, req.body as any),
|
||||||
|
)
|
||||||
|
|
||||||
|
// modify thread
|
||||||
|
app.patch(`/:threadId`, async (request: any) =>
|
||||||
|
updateThread(request.params.threadId, request.body),
|
||||||
|
)
|
||||||
|
}
|
||||||
21
core/src/node/api/routes/v1.ts
Normal file
21
core/src/node/api/routes/v1.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { HttpServer } from '../HttpServer'
|
||||||
|
import { commonRouter, threadRouter, fsRouter, extensionRouter, downloadRouter } from './index'
|
||||||
|
|
||||||
|
export const v1Router = async (app: HttpServer) => {
|
||||||
|
// MARK: External Routes
|
||||||
|
app.register(commonRouter)
|
||||||
|
app.register(threadRouter, {
|
||||||
|
prefix: '/thread',
|
||||||
|
})
|
||||||
|
|
||||||
|
// MARK: Internal Application Routes
|
||||||
|
app.register(fsRouter, {
|
||||||
|
prefix: '/fs',
|
||||||
|
})
|
||||||
|
app.register(extensionRouter, {
|
||||||
|
prefix: '/extension',
|
||||||
|
})
|
||||||
|
app.register(downloadRouter, {
|
||||||
|
prefix: '/download',
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import { rmdir } from 'fs/promises'
|
import { rmdirSync } from 'fs'
|
||||||
import { resolve, join } from 'path'
|
import { resolve, join } from 'path'
|
||||||
import { manifest, extract } from 'pacote'
|
import { manifest, extract } from 'pacote'
|
||||||
import * as Arborist from '@npmcli/arborist'
|
import * as Arborist from '@npmcli/arborist'
|
||||||
import { ExtensionManager } from './../managers/extension'
|
import { ExtensionManager } from './manager'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An NPM package that can be used as an extension.
|
* An NPM package that can be used as an extension.
|
||||||
* Used to hold all the information and functions necessary to handle the extension lifecycle.
|
* Used to hold all the information and functions necessary to handle the extension lifecycle.
|
||||||
*/
|
*/
|
||||||
class Extension {
|
export default class Extension {
|
||||||
/**
|
/**
|
||||||
* @property {string} origin Original specification provided to fetch the package.
|
* @property {string} origin Original specification provided to fetch the package.
|
||||||
* @property {Object} installOptions Options provided to pacote when fetching the manifest.
|
* @property {Object} installOptions Options provided to pacote when fetching the manifest.
|
||||||
@ -56,10 +56,7 @@ class Extension {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
get specifier() {
|
get specifier() {
|
||||||
return (
|
return this.origin + (this.installOptions.version ? '@' + this.installOptions.version : '')
|
||||||
this.origin +
|
|
||||||
(this.installOptions.version ? '@' + this.installOptions.version : '')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -85,9 +82,7 @@ class Extension {
|
|||||||
this.main = mnf.main
|
this.main = mnf.main
|
||||||
this.description = mnf.description
|
this.description = mnf.description
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(`Package ${this.origin} does not contain a valid manifest: ${error}`)
|
||||||
`Package ${this.origin} does not contain a valid manifest: ${error}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@ -107,7 +102,7 @@ class Extension {
|
|||||||
await extract(
|
await extract(
|
||||||
this.specifier,
|
this.specifier,
|
||||||
join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''),
|
join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''),
|
||||||
this.installOptions
|
this.installOptions,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Set the url using the custom extensions protocol
|
// Set the url using the custom extensions protocol
|
||||||
@ -180,11 +175,8 @@ class Extension {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
async uninstall() {
|
async uninstall() {
|
||||||
const extPath = resolve(
|
const extPath = resolve(ExtensionManager.instance.extensionsPath ?? '', this.name ?? '')
|
||||||
ExtensionManager.instance.extensionsPath ?? '',
|
await rmdirSync(extPath, { recursive: true })
|
||||||
this.name ?? ''
|
|
||||||
)
|
|
||||||
await rmdir(extPath, { recursive: true })
|
|
||||||
|
|
||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
}
|
}
|
||||||
@ -200,5 +192,3 @@ class Extension {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Extension
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
import { protocol } from 'electron'
|
|
||||||
import { normalize } from 'path'
|
import { normalize } from 'path'
|
||||||
|
|
||||||
import Extension from './extension'
|
import Extension from './extension'
|
||||||
@ -12,18 +12,8 @@ import {
|
|||||||
getActiveExtensions,
|
getActiveExtensions,
|
||||||
addExtension,
|
addExtension,
|
||||||
} from './store'
|
} from './store'
|
||||||
import { ExtensionManager } from './../managers/extension'
|
import { ExtensionManager } from './manager'
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up the required communication between the main and renderer processes.
|
|
||||||
* Additionally sets the extensions up using {@link useExtensions} if a extensionsPath is provided.
|
|
||||||
* @param {Object} options configuration for setting up the renderer facade.
|
|
||||||
* @param {confirmInstall} [options.confirmInstall] Function to validate that a extension should be installed.
|
|
||||||
* @param {Boolean} [options.useFacade=true] Whether to make a facade to the extensions available in the renderer.
|
|
||||||
* @param {string} [options.extensionsPath] Optional path to the extensions folder.
|
|
||||||
* @returns {extensionManager|Object} A set of functions used to manage the extension lifecycle if useExtensions is provided.
|
|
||||||
* @function
|
|
||||||
*/
|
|
||||||
export function init(options: any) {
|
export function init(options: any) {
|
||||||
// Create extensions protocol to serve extensions to renderer
|
// Create extensions protocol to serve extensions to renderer
|
||||||
registerExtensionProtocol()
|
registerExtensionProtocol()
|
||||||
@ -41,13 +31,24 @@ export function init(options: any) {
|
|||||||
* @private
|
* @private
|
||||||
* @returns {boolean} Whether the protocol registration was successful
|
* @returns {boolean} Whether the protocol registration was successful
|
||||||
*/
|
*/
|
||||||
function registerExtensionProtocol() {
|
async function registerExtensionProtocol() {
|
||||||
return protocol.registerFileProtocol('extension', (request, callback) => {
|
let electron: any = undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
const moduleName = "electron"
|
||||||
|
electron = await import(moduleName)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Electron is not available')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (electron) {
|
||||||
|
return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => {
|
||||||
const entry = request.url.substr('extension://'.length - 1)
|
const entry = request.url.substr('extension://'.length - 1)
|
||||||
|
|
||||||
const url = normalize(ExtensionManager.instance.extensionsPath + entry)
|
const url = normalize(ExtensionManager.instance.extensionsPath + entry)
|
||||||
callback({ path: url })
|
callback({ path: url })
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,8 +58,7 @@ function registerExtensionProtocol() {
|
|||||||
* @returns {extensionManager} A set of functions used to manage the extension lifecycle.
|
* @returns {extensionManager} A set of functions used to manage the extension lifecycle.
|
||||||
*/
|
*/
|
||||||
export function useExtensions(extensionsPath: string) {
|
export function useExtensions(extensionsPath: string) {
|
||||||
if (!extensionsPath)
|
if (!extensionsPath) throw Error('A path to the extensions folder is required to use extensions')
|
||||||
throw Error('A path to the extensions folder is required to use extensions')
|
|
||||||
// Store the path to the extensions folder
|
// Store the path to the extensions folder
|
||||||
ExtensionManager.instance.setExtensionsPath(extensionsPath)
|
ExtensionManager.instance.setExtensionsPath(extensionsPath)
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ export function useExtensions(extensionsPath: string) {
|
|||||||
|
|
||||||
// Read extension list from extensions folder
|
// Read extension list from extensions folder
|
||||||
const extensions = JSON.parse(
|
const extensions = JSON.parse(
|
||||||
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8')
|
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8'),
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
// Create and store a Extension instance for each extension in list
|
// Create and store a Extension instance for each extension in list
|
||||||
@ -82,7 +82,7 @@ export function useExtensions(extensionsPath: string) {
|
|||||||
throw new Error(
|
throw new Error(
|
||||||
'Could not successfully rebuild list of installed extensions.\n' +
|
'Could not successfully rebuild list of installed extensions.\n' +
|
||||||
error +
|
error +
|
||||||
'\nPlease check the extensions.json file in the extensions folder.'
|
'\nPlease check the extensions.json file in the extensions folder.',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +111,6 @@ function loadExtension(ext: any) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addExtension(extension, false)
|
addExtension(extension, false)
|
||||||
extension.subscribe('pe-persist', persistExtensions)
|
extension.subscribe('pe-persist', persistExtensions)
|
||||||
}
|
}
|
||||||
@ -123,7 +122,7 @@ function loadExtension(ext: any) {
|
|||||||
export function getStore() {
|
export function getStore() {
|
||||||
if (!ExtensionManager.instance.extensionsPath) {
|
if (!ExtensionManager.instance.extensionsPath) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'The extension path has not yet been set up. Please run useExtensions before accessing the store'
|
'The extension path has not yet been set up. Please run useExtensions before accessing the store',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
61
core/src/node/extension/manager.ts
Normal file
61
core/src/node/extension/manager.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { join, resolve } from "path";
|
||||||
|
|
||||||
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||||
|
import { init } from "./index";
|
||||||
|
import { homedir } from "os"
|
||||||
|
/**
|
||||||
|
* Manages extension installation and migration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const userSpacePath = join(homedir(), "jan");
|
||||||
|
|
||||||
|
export class ExtensionManager {
|
||||||
|
public static instance: ExtensionManager = new ExtensionManager();
|
||||||
|
|
||||||
|
extensionsPath: string | undefined = join(userSpacePath, "extensions");
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (ExtensionManager.instance) {
|
||||||
|
return ExtensionManager.instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the extensions by initializing the `extensions` module with the `confirmInstall` and `extensionsPath` options.
|
||||||
|
* The `confirmInstall` function always returns `true` to allow extension installation.
|
||||||
|
* The `extensionsPath` option specifies the path to install extensions to.
|
||||||
|
*/
|
||||||
|
setupExtensions() {
|
||||||
|
init({
|
||||||
|
// Function to check from the main process that user wants to install a extension
|
||||||
|
confirmInstall: async (_extensions: string[]) => {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
// Path to install extension to
|
||||||
|
extensionsPath: join(userSpacePath, "extensions"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setExtensionsPath(extPath: string) {
|
||||||
|
// Create folder if it does not exist
|
||||||
|
let extDir;
|
||||||
|
try {
|
||||||
|
extDir = resolve(extPath);
|
||||||
|
if (extDir.length < 2) throw new Error();
|
||||||
|
|
||||||
|
if (!existsSync(extDir)) mkdirSync(extDir);
|
||||||
|
|
||||||
|
const extensionsJson = join(extDir, "extensions.json");
|
||||||
|
if (!existsSync(extensionsJson))
|
||||||
|
writeFileSync(extensionsJson, "{}", "utf8");
|
||||||
|
|
||||||
|
this.extensionsPath = extDir;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("Invalid path provided to the extensions folder");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getExtensionsFile() {
|
||||||
|
return join(this.extensionsPath ?? "", "extensions.json");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,16 +1,6 @@
|
|||||||
/**
|
import { writeFileSync } from "fs";
|
||||||
* Provides access to the extensions stored by Extension Store
|
import Extension from "./extension";
|
||||||
* @typedef {Object} extensionManager
|
import { ExtensionManager } from "./manager";
|
||||||
* @prop {getExtension} getExtension
|
|
||||||
* @prop {getAllExtensions} getAllExtensions
|
|
||||||
* @prop {getActiveExtensions} getActiveExtensions
|
|
||||||
* @prop {installExtensions} installExtensions
|
|
||||||
* @prop {removeExtension} removeExtension
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { writeFileSync } from 'fs'
|
|
||||||
import Extension from './extension'
|
|
||||||
import { ExtensionManager } from './../managers/extension'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module store
|
* @module store
|
||||||
@ -21,7 +11,7 @@ import { ExtensionManager } from './../managers/extension'
|
|||||||
* Register of installed extensions
|
* Register of installed extensions
|
||||||
* @type {Object.<string, Extension>} extension - List of installed extensions
|
* @type {Object.<string, Extension>} extension - List of installed extensions
|
||||||
*/
|
*/
|
||||||
const extensions: Record<string, Extension> = {}
|
const extensions: Record<string, Extension> = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a extension from the stored extensions.
|
* Get a extension from the stored extensions.
|
||||||
@ -31,10 +21,10 @@ const extensions: Record<string, Extension> = {}
|
|||||||
*/
|
*/
|
||||||
export function getExtension(name: string) {
|
export function getExtension(name: string) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(extensions, name)) {
|
if (!Object.prototype.hasOwnProperty.call(extensions, name)) {
|
||||||
throw new Error(`Extension ${name} does not exist`)
|
throw new Error(`Extension ${name} does not exist`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return extensions[name]
|
return extensions[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,7 +33,7 @@ export function getExtension(name: string) {
|
|||||||
* @alias extensionManager.getAllExtensions
|
* @alias extensionManager.getAllExtensions
|
||||||
*/
|
*/
|
||||||
export function getAllExtensions() {
|
export function getAllExtensions() {
|
||||||
return Object.values(extensions)
|
return Object.values(extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,7 +42,7 @@ export function getAllExtensions() {
|
|||||||
* @alias extensionManager.getActiveExtensions
|
* @alias extensionManager.getActiveExtensions
|
||||||
*/
|
*/
|
||||||
export function getActiveExtensions() {
|
export function getActiveExtensions() {
|
||||||
return Object.values(extensions).filter((extension) => extension.active)
|
return Object.values(extensions).filter((extension) => extension.active);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,9 +53,9 @@ export function getActiveExtensions() {
|
|||||||
* @alias extensionManager.removeExtension
|
* @alias extensionManager.removeExtension
|
||||||
*/
|
*/
|
||||||
export function removeExtension(name: string, persist = true) {
|
export function removeExtension(name: string, persist = true) {
|
||||||
const del = delete extensions[name]
|
const del = delete extensions[name];
|
||||||
if (persist) persistExtensions()
|
if (persist) persistExtensions();
|
||||||
return del
|
return del;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -75,10 +65,10 @@ export function removeExtension(name: string, persist = true) {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function addExtension(extension: Extension, persist = true) {
|
export function addExtension(extension: Extension, persist = true) {
|
||||||
if (extension.name) extensions[extension.name] = extension
|
if (extension.name) extensions[extension.name] = extension;
|
||||||
if (persist) {
|
if (persist) {
|
||||||
persistExtensions()
|
persistExtensions();
|
||||||
extension.subscribe('pe-persist', persistExtensions)
|
extension.subscribe("pe-persist", persistExtensions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,15 +77,15 @@ export function addExtension(extension: Extension, persist = true) {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function persistExtensions() {
|
export function persistExtensions() {
|
||||||
const persistData: Record<string, Extension> = {}
|
const persistData: Record<string, Extension> = {};
|
||||||
for (const name in extensions) {
|
for (const name in extensions) {
|
||||||
persistData[name] = extensions[name]
|
persistData[name] = extensions[name];
|
||||||
}
|
}
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
ExtensionManager.instance.getExtensionsFile(),
|
ExtensionManager.instance.getExtensionsFile(),
|
||||||
JSON.stringify(persistData),
|
JSON.stringify(persistData),
|
||||||
'utf8'
|
"utf8"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -106,25 +96,25 @@ export function persistExtensions() {
|
|||||||
* @alias extensionManager.installExtensions
|
* @alias extensionManager.installExtensions
|
||||||
*/
|
*/
|
||||||
export async function installExtensions(extensions: any, store = true) {
|
export async function installExtensions(extensions: any, store = true) {
|
||||||
const installed: Extension[] = []
|
const installed: Extension[] = [];
|
||||||
for (const ext of extensions) {
|
for (const ext of extensions) {
|
||||||
// Set install options and activation based on input type
|
// Set install options and activation based on input type
|
||||||
const isObject = typeof ext === 'object'
|
const isObject = typeof ext === "object";
|
||||||
const spec = isObject ? [ext.specifier, ext] : [ext]
|
const spec = isObject ? [ext.specifier, ext] : [ext];
|
||||||
const activate = isObject ? ext.activate !== false : true
|
const activate = isObject ? ext.activate !== false : true;
|
||||||
|
|
||||||
// Install and possibly activate extension
|
// Install and possibly activate extension
|
||||||
const extension = new Extension(...spec)
|
const extension = new Extension(...spec);
|
||||||
await extension._install()
|
await extension._install();
|
||||||
if (activate) extension.setActive(true)
|
if (activate) extension.setActive(true);
|
||||||
|
|
||||||
// Add extension to store if needed
|
// Add extension to store if needed
|
||||||
if (store) addExtension(extension)
|
if (store) addExtension(extension);
|
||||||
installed.push(extension)
|
installed.push(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return list of all installed extensions
|
// Return list of all installed extensions
|
||||||
return installed
|
return installed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
7
core/src/node/index.ts
Normal file
7
core/src/node/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export * from './extension/index'
|
||||||
|
export * from './extension/extension'
|
||||||
|
export * from './extension/manager'
|
||||||
|
export * from './extension/store'
|
||||||
|
export * from './download'
|
||||||
|
export * from './module'
|
||||||
|
export * from './api'
|
||||||
@ -1,16 +1,14 @@
|
|||||||
import { dispose } from "./../utils/disposable";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages imported modules.
|
* Manages imported modules.
|
||||||
*/
|
*/
|
||||||
export class ModuleManager {
|
export class ModuleManager {
|
||||||
public requiredModules: Record<string, any> = {};
|
public requiredModules: Record<string, any> = {}
|
||||||
|
|
||||||
public static instance: ModuleManager = new ModuleManager();
|
public static instance: ModuleManager = new ModuleManager()
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (ModuleManager.instance) {
|
if (ModuleManager.instance) {
|
||||||
return ModuleManager.instance;
|
return ModuleManager.instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,14 +18,13 @@ export class ModuleManager {
|
|||||||
* @param {any | undefined} nodule - The module to set, or undefined to clear the module.
|
* @param {any | undefined} nodule - The module to set, or undefined to clear the module.
|
||||||
*/
|
*/
|
||||||
setModule(moduleName: string, nodule: any | undefined) {
|
setModule(moduleName: string, nodule: any | undefined) {
|
||||||
this.requiredModules[moduleName] = nodule;
|
this.requiredModules[moduleName] = nodule
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all imported modules.
|
* Clears all imported modules.
|
||||||
*/
|
*/
|
||||||
clearImportedModules() {
|
clearImportedModules() {
|
||||||
dispose(this.requiredModules);
|
this.requiredModules = {}
|
||||||
this.requiredModules = {};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,13 +67,6 @@ export type Model = {
|
|||||||
*/
|
*/
|
||||||
description: string
|
description: string
|
||||||
|
|
||||||
/**
|
|
||||||
* The model state.
|
|
||||||
* Default: "to_download"
|
|
||||||
* Enum: "to_download" "downloading" "ready" "running"
|
|
||||||
*/
|
|
||||||
state?: ModelState
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The model settings.
|
* The model settings.
|
||||||
*/
|
*/
|
||||||
@ -101,15 +94,6 @@ export type ModelMetadata = {
|
|||||||
cover?: string
|
cover?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The Model transition states.
|
|
||||||
*/
|
|
||||||
export enum ModelState {
|
|
||||||
Downloading = 'downloading',
|
|
||||||
Ready = 'ready',
|
|
||||||
Running = 'running',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The available model settings.
|
* The available model settings.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"module": "es2015",
|
"module": "ES2020",
|
||||||
"lib": ["es2015", "es2016", "es2017", "dom"],
|
"lib": ["es2015", "es2016", "es2017", "dom"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { app, ipcMain, shell, nativeTheme } from 'electron'
|
import { app, ipcMain, shell, nativeTheme } from 'electron'
|
||||||
import { ModuleManager } from './../managers/module'
|
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { ExtensionManager } from './../managers/extension'
|
|
||||||
import { WindowManager } from './../managers/window'
|
import { WindowManager } from './../managers/window'
|
||||||
import { userSpacePath } from './../utils/path'
|
import { userSpacePath } from './../utils/path'
|
||||||
import { AppRoute } from '@janhq/core'
|
import { AppRoute } from '@janhq/core'
|
||||||
import { getResourcePath } from './../utils/path'
|
import { getResourcePath } from './../utils/path'
|
||||||
|
import {
|
||||||
|
ExtensionManager,
|
||||||
|
ModuleManager,
|
||||||
|
} from '@janhq/core/node'
|
||||||
|
|
||||||
export function handleAppIPCs() {
|
export function handleAppIPCs() {
|
||||||
/**
|
/**
|
||||||
@ -26,10 +28,6 @@ export function handleAppIPCs() {
|
|||||||
shell.openPath(userSpacePath)
|
shell.openPath(userSpacePath)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle(AppRoute.getResourcePath, async (_event) => {
|
|
||||||
return getResourcePath()
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a URL in the user's default browser.
|
* Opens a URL in the user's default browser.
|
||||||
* @param _event - The IPC event object.
|
* @param _event - The IPC event object.
|
||||||
@ -48,6 +46,13 @@ export function handleAppIPCs() {
|
|||||||
shell.openPath(url)
|
shell.openPath(url)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joins multiple paths together, respect to the current OS.
|
||||||
|
*/
|
||||||
|
ipcMain.handle(AppRoute.joinPath, async (_event, paths: string[]) =>
|
||||||
|
join(...paths)
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relaunches the app in production - reload window in development.
|
* Relaunches the app in production - reload window in development.
|
||||||
* @param _event - The IPC event object.
|
* @param _event - The IPC event object.
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { app, ipcMain } from 'electron'
|
import { app, ipcMain } from 'electron'
|
||||||
import { DownloadManager } from './../managers/download'
|
|
||||||
import { resolve, join } from 'path'
|
import { resolve, join } from 'path'
|
||||||
import { WindowManager } from './../managers/window'
|
import { WindowManager } from './../managers/window'
|
||||||
import request from 'request'
|
import request from 'request'
|
||||||
import { createWriteStream } from 'fs'
|
import { createWriteStream, renameSync } from 'fs'
|
||||||
import { DownloadEvent, DownloadRoute } from '@janhq/core'
|
import { DownloadEvent, DownloadRoute } from '@janhq/core'
|
||||||
const progress = require('request-progress')
|
const progress = require('request-progress')
|
||||||
|
import { DownloadManager } from '@janhq/core/node'
|
||||||
|
|
||||||
export function handleDownloaderIPCs() {
|
export function handleDownloaderIPCs() {
|
||||||
/**
|
/**
|
||||||
@ -46,8 +46,13 @@ export function handleDownloaderIPCs() {
|
|||||||
*/
|
*/
|
||||||
ipcMain.handle(DownloadRoute.downloadFile, async (_event, url, fileName) => {
|
ipcMain.handle(DownloadRoute.downloadFile, async (_event, url, fileName) => {
|
||||||
const userDataPath = join(app.getPath('home'), 'jan')
|
const userDataPath = join(app.getPath('home'), 'jan')
|
||||||
|
if (typeof fileName === 'string' && fileName.includes('file:/')) {
|
||||||
|
fileName = fileName.replace('file:/', '')
|
||||||
|
}
|
||||||
const destination = resolve(userDataPath, fileName)
|
const destination = resolve(userDataPath, fileName)
|
||||||
const rq = request(url)
|
const rq = request(url)
|
||||||
|
// downloading file to a temp file first
|
||||||
|
const downloadingTempFile = `${destination}.download`
|
||||||
|
|
||||||
progress(rq, {})
|
progress(rq, {})
|
||||||
.on('progress', function (state: any) {
|
.on('progress', function (state: any) {
|
||||||
@ -70,6 +75,9 @@ export function handleDownloaderIPCs() {
|
|||||||
})
|
})
|
||||||
.on('end', function () {
|
.on('end', function () {
|
||||||
if (DownloadManager.instance.networkRequests[fileName]) {
|
if (DownloadManager.instance.networkRequests[fileName]) {
|
||||||
|
// Finished downloading, rename temp file to actual file
|
||||||
|
renameSync(downloadingTempFile, destination)
|
||||||
|
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
WindowManager?.instance.currentWindow?.webContents.send(
|
||||||
DownloadEvent.onFileDownloadSuccess,
|
DownloadEvent.onFileDownloadSuccess,
|
||||||
{
|
{
|
||||||
@ -87,7 +95,7 @@ export function handleDownloaderIPCs() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.pipe(createWriteStream(destination))
|
.pipe(createWriteStream(downloadingTempFile))
|
||||||
|
|
||||||
DownloadManager.instance.setRequest(fileName, rq)
|
DownloadManager.instance.setRequest(fileName, rq)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { ipcMain, webContents } from 'electron'
|
import { ipcMain, webContents } from 'electron'
|
||||||
import { readdirSync } from 'fs'
|
import { readdirSync } from 'fs'
|
||||||
import { ModuleManager } from './../managers/module'
|
|
||||||
import { join, extname } from 'path'
|
import { join, extname } from 'path'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getActiveExtensions,
|
|
||||||
getAllExtensions,
|
|
||||||
installExtensions,
|
installExtensions,
|
||||||
} from './../extension/store'
|
getExtension,
|
||||||
import { getExtension } from './../extension/store'
|
removeExtension,
|
||||||
import { removeExtension } from './../extension/store'
|
getActiveExtensions,
|
||||||
import Extension from './../extension/extension'
|
ModuleManager
|
||||||
|
} from '@janhq/core/node'
|
||||||
|
|
||||||
import { getResourcePath, userSpacePath } from './../utils/path'
|
import { getResourcePath, userSpacePath } from './../utils/path'
|
||||||
import { ExtensionRoute } from '@janhq/core'
|
import { ExtensionRoute } from '@janhq/core'
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ export function handleExtensionIPCs() {
|
|||||||
ExtensionRoute.updateExtension,
|
ExtensionRoute.updateExtension,
|
||||||
async (e, extensions, reload) => {
|
async (e, extensions, reload) => {
|
||||||
// Update all provided extensions
|
// Update all provided extensions
|
||||||
const updated: Extension[] = []
|
const updated: any[] = []
|
||||||
for (const ext of extensions) {
|
for (const ext of extensions) {
|
||||||
const extension = getExtension(ext)
|
const extension = getExtension(ext)
|
||||||
const res = await extension.update()
|
const res = await extension.update()
|
||||||
|
|||||||
37
electron/handlers/fileManager.ts
Normal file
37
electron/handlers/fileManager.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { ipcMain } from 'electron'
|
||||||
|
// @ts-ignore
|
||||||
|
import reflect from '@alumna/reflect'
|
||||||
|
|
||||||
|
import { FileManagerRoute, getResourcePath } from '@janhq/core'
|
||||||
|
import { userSpacePath } from './../utils/path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles file system extensions operations.
|
||||||
|
*/
|
||||||
|
export function handleFileMangerIPCs() {
|
||||||
|
// Handles the 'synceFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path.
|
||||||
|
ipcMain.handle(
|
||||||
|
FileManagerRoute.synceFile,
|
||||||
|
async (_event, src: string, dest: string) => {
|
||||||
|
return reflect({
|
||||||
|
src,
|
||||||
|
dest,
|
||||||
|
recursive: true,
|
||||||
|
delete: false,
|
||||||
|
overwrite: true,
|
||||||
|
errorOnExist: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handles the 'getUserSpace' IPC event. This event is triggered to get the user space path.
|
||||||
|
ipcMain.handle(
|
||||||
|
FileManagerRoute.getUserSpace,
|
||||||
|
(): Promise<string> => Promise.resolve(userSpacePath)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path.
|
||||||
|
ipcMain.handle(FileManagerRoute.getResourcePath, async (_event) => {
|
||||||
|
return getResourcePath()
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,238 +1,24 @@
|
|||||||
import { ipcMain } from 'electron'
|
import { ipcMain } from 'electron'
|
||||||
import * as fs from 'fs'
|
|
||||||
import fse from 'fs-extra'
|
|
||||||
import { join } from 'path'
|
|
||||||
import readline from 'readline'
|
|
||||||
import { userSpacePath } from './../utils/path'
|
|
||||||
import { FileSystemRoute } from '@janhq/core'
|
|
||||||
const reflect = require('@alumna/reflect')
|
|
||||||
|
|
||||||
|
import { FileSystemRoute } from '@janhq/core'
|
||||||
|
import { userSpacePath } from '../utils/path'
|
||||||
|
import { join } from 'path'
|
||||||
/**
|
/**
|
||||||
* Handles file system operations.
|
* Handles file system operations.
|
||||||
*/
|
*/
|
||||||
export function handleFsIPCs() {
|
export function handleFsIPCs() {
|
||||||
/**
|
const moduleName = 'fs'
|
||||||
* Gets the path to the user data directory.
|
Object.values(FileSystemRoute).forEach((route) => {
|
||||||
* @param event - The event object.
|
ipcMain.handle(route, async (event, ...args) => {
|
||||||
* @returns A promise that resolves with the path to the user data directory.
|
return import(moduleName).then((mdl) =>
|
||||||
*/
|
mdl[route](
|
||||||
ipcMain.handle(
|
...args.map((arg) =>
|
||||||
FileSystemRoute.getUserSpace,
|
typeof arg === 'string' && arg.includes('file:/')
|
||||||
(): Promise<string> => Promise.resolve(userSpacePath)
|
? join(userSpacePath, arg.replace('file:/', ''))
|
||||||
|
: arg
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the path is a directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path to check.
|
|
||||||
* @returns A promise that resolves with a boolean indicating whether the path is a directory.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.isDirectory,
|
|
||||||
(_event, path: string): Promise<boolean> => {
|
|
||||||
const fullPath = join(userSpacePath, path)
|
|
||||||
return Promise.resolve(
|
|
||||||
fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory()
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a file from the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the file to read.
|
|
||||||
* @returns A promise that resolves with the contents of the file.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.readFile,
|
|
||||||
async (event, path: string): Promise<string> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.readFile(join(userSpacePath, path), 'utf8', (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve(data)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a file exists in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the file to check.
|
|
||||||
* @returns A promise that resolves with a boolean indicating whether the file exists.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(FileSystemRoute.exists, async (_event, path: string) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const fullPath = join(userSpacePath, path)
|
|
||||||
fs.existsSync(fullPath) ? resolve(true) : resolve(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes data to a file in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the file to write to.
|
|
||||||
* @param data - The data to write to the file.
|
|
||||||
* @returns A promise that resolves when the file has been written.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.writeFile,
|
|
||||||
async (event, path: string, data: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await fs.writeFileSync(join(userSpacePath, path), data, 'utf8')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`writeFile ${path} result: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a directory in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the directory to create.
|
|
||||||
* @returns A promise that resolves when the directory has been created.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.mkdir,
|
|
||||||
async (event, path: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(join(userSpacePath, path), { recursive: true })
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`mkdir ${path} result: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a directory in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the directory to remove.
|
|
||||||
* @returns A promise that resolves when the directory is removed successfully.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.rmdir,
|
|
||||||
async (event, path: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await fs.rmSync(join(userSpacePath, path), { recursive: true })
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`rmdir ${path} result: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists the files in a directory in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the directory to list files from.
|
|
||||||
* @returns A promise that resolves with an array of file names.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.listFiles,
|
|
||||||
async (event, path: string): Promise<string[]> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.readdir(join(userSpacePath, path), (err, files) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve(files)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a file from the user data folder.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param filePath - The path to the file to delete.
|
|
||||||
* @returns A string indicating the result of the operation.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(FileSystemRoute.deleteFile, async (_event, filePath) => {
|
|
||||||
try {
|
|
||||||
await fs.unlinkSync(join(userSpacePath, filePath))
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`unlink ${filePath} result: ${err}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends data to a file in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the file to append to.
|
|
||||||
* @param data - The data to append to the file.
|
|
||||||
* @returns A promise that resolves when the file has been written.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.appendFile,
|
|
||||||
async (_event, path: string, data: string) => {
|
|
||||||
try {
|
|
||||||
await fs.appendFileSync(join(userSpacePath, path), data, 'utf8')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`appendFile ${path} result: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.syncFile,
|
|
||||||
async (_event, src: string, dest: string) => {
|
|
||||||
console.debug(`Copying file from ${src} to ${dest}`)
|
|
||||||
|
|
||||||
return reflect({
|
|
||||||
src,
|
|
||||||
dest,
|
|
||||||
recursive: true,
|
|
||||||
delete: false,
|
|
||||||
overwrite: true,
|
|
||||||
errorOnExist: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.copyFile,
|
|
||||||
async (_event, src: string, dest: string) => {
|
|
||||||
console.debug(`Copying file from ${src} to ${dest}`)
|
|
||||||
|
|
||||||
return fse.copySync(src, dest, {
|
|
||||||
overwrite: false,
|
|
||||||
recursive: true,
|
|
||||||
errorOnExist: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a file line by line.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the file to read.
|
|
||||||
* @returns A promise that resolves with the contents of the file.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.readLineByLine,
|
|
||||||
async (_event, path: string) => {
|
|
||||||
const fullPath = join(userSpacePath, path)
|
|
||||||
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
try {
|
|
||||||
const readInterface = readline.createInterface({
|
|
||||||
input: fs.createReadStream(fullPath),
|
|
||||||
})
|
|
||||||
const lines: any = []
|
|
||||||
readInterface
|
|
||||||
.on('line', function (line) {
|
|
||||||
lines.push(line)
|
|
||||||
})
|
|
||||||
.on('close', function () {
|
|
||||||
res(lines)
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
rej(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,27 +7,34 @@ import { createUserSpace } from './utils/path'
|
|||||||
* Managers
|
* Managers
|
||||||
**/
|
**/
|
||||||
import { WindowManager } from './managers/window'
|
import { WindowManager } from './managers/window'
|
||||||
import { ModuleManager } from './managers/module'
|
import { ExtensionManager, ModuleManager } from '@janhq/core/node'
|
||||||
import { ExtensionManager } from './managers/extension'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IPC Handlers
|
* IPC Handlers
|
||||||
**/
|
**/
|
||||||
import { handleDownloaderIPCs } from './handlers/download'
|
import { handleDownloaderIPCs } from './handlers/download'
|
||||||
import { handleExtensionIPCs } from './handlers/extension'
|
import { handleExtensionIPCs } from './handlers/extension'
|
||||||
|
import { handleFileMangerIPCs } from './handlers/fileManager'
|
||||||
import { handleAppIPCs } from './handlers/app'
|
import { handleAppIPCs } from './handlers/app'
|
||||||
import { handleAppUpdates } from './handlers/update'
|
import { handleAppUpdates } from './handlers/update'
|
||||||
import { handleFsIPCs } from './handlers/fs'
|
import { handleFsIPCs } from './handlers/fs'
|
||||||
|
import { migrateExtensions } from './utils/migration'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server
|
||||||
|
*/
|
||||||
|
import { startServer } from '@janhq/server'
|
||||||
|
|
||||||
app
|
app
|
||||||
.whenReady()
|
.whenReady()
|
||||||
.then(createUserSpace)
|
.then(createUserSpace)
|
||||||
.then(ExtensionManager.instance.migrateExtensions)
|
.then(migrateExtensions)
|
||||||
.then(ExtensionManager.instance.setupExtensions)
|
.then(ExtensionManager.instance.setupExtensions)
|
||||||
.then(setupMenu)
|
.then(setupMenu)
|
||||||
.then(handleIPCs)
|
.then(handleIPCs)
|
||||||
.then(handleAppUpdates)
|
.then(handleAppUpdates)
|
||||||
.then(createMainWindow)
|
.then(createMainWindow)
|
||||||
|
.then(startServer)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (!BrowserWindow.getAllWindows().length) {
|
if (!BrowserWindow.getAllWindows().length) {
|
||||||
@ -80,4 +87,5 @@ function handleIPCs() {
|
|||||||
handleDownloaderIPCs()
|
handleDownloaderIPCs()
|
||||||
handleExtensionIPCs()
|
handleExtensionIPCs()
|
||||||
handleAppIPCs()
|
handleAppIPCs()
|
||||||
|
handleFileMangerIPCs()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,85 +0,0 @@
|
|||||||
import { app } from 'electron'
|
|
||||||
import { init } from './../extension'
|
|
||||||
import { join, resolve } from 'path'
|
|
||||||
import { rmdir } from 'fs'
|
|
||||||
import Store from 'electron-store'
|
|
||||||
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
|
||||||
import { userSpacePath } from './../utils/path'
|
|
||||||
/**
|
|
||||||
* Manages extension installation and migration.
|
|
||||||
*/
|
|
||||||
export class ExtensionManager {
|
|
||||||
public static instance: ExtensionManager = new ExtensionManager()
|
|
||||||
|
|
||||||
extensionsPath: string | undefined = undefined
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
if (ExtensionManager.instance) {
|
|
||||||
return ExtensionManager.instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up the extensions by initializing the `extensions` module with the `confirmInstall` and `extensionsPath` options.
|
|
||||||
* The `confirmInstall` function always returns `true` to allow extension installation.
|
|
||||||
* The `extensionsPath` option specifies the path to install extensions to.
|
|
||||||
*/
|
|
||||||
setupExtensions() {
|
|
||||||
init({
|
|
||||||
// Function to check from the main process that user wants to install a extension
|
|
||||||
confirmInstall: async (_extensions: string[]) => {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
// Path to install extension to
|
|
||||||
extensionsPath: join(userSpacePath, 'extensions'),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates the extensions by deleting the `extensions` directory in the user data path.
|
|
||||||
* If the `migrated_version` key in the `Store` object does not match the current app version,
|
|
||||||
* the function deletes the `extensions` directory and sets the `migrated_version` key to the current app version.
|
|
||||||
* @returns A Promise that resolves when the migration is complete.
|
|
||||||
*/
|
|
||||||
migrateExtensions() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const store = new Store()
|
|
||||||
if (store.get('migrated_version') !== app.getVersion()) {
|
|
||||||
console.debug('start migration:', store.get('migrated_version'))
|
|
||||||
const fullPath = join(userSpacePath, 'extensions')
|
|
||||||
|
|
||||||
rmdir(fullPath, { recursive: true }, function (err) {
|
|
||||||
if (err) console.error(err)
|
|
||||||
store.set('migrated_version', app.getVersion())
|
|
||||||
console.debug('migrate extensions done')
|
|
||||||
resolve(undefined)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
resolve(undefined)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setExtensionsPath(extPath: string) {
|
|
||||||
// Create folder if it does not exist
|
|
||||||
let extDir
|
|
||||||
try {
|
|
||||||
extDir = resolve(extPath)
|
|
||||||
if (extDir.length < 2) throw new Error()
|
|
||||||
|
|
||||||
if (!existsSync(extDir)) mkdirSync(extDir)
|
|
||||||
|
|
||||||
const extensionsJson = join(extDir, 'extensions.json')
|
|
||||||
if (!existsSync(extensionsJson))
|
|
||||||
writeFileSync(extensionsJson, '{}', 'utf8')
|
|
||||||
|
|
||||||
this.extensionsPath = extDir
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error('Invalid path provided to the extensions folder')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getExtensionsFile() {
|
|
||||||
return join(this.extensionsPath ?? '', 'extensions.json')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -72,15 +72,20 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alumna/reflect": "^1.1.3",
|
"@alumna/reflect": "^1.1.3",
|
||||||
"@janhq/core": "link:./core",
|
"@janhq/core": "link:./core",
|
||||||
|
"@janhq/server": "link:./server",
|
||||||
"@npmcli/arborist": "^7.1.0",
|
"@npmcli/arborist": "^7.1.0",
|
||||||
"@types/request": "^2.48.12",
|
"@types/request": "^2.48.12",
|
||||||
"@uiball/loaders": "^1.3.0",
|
"@uiball/loaders": "^1.3.0",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"electron-updater": "^6.1.7",
|
"electron-updater": "^6.1.7",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
|
"node-fetch": "2",
|
||||||
"pacote": "^17.0.4",
|
"pacote": "^17.0.4",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-progress": "^3.0.0",
|
"request-progress": "^3.0.0",
|
||||||
|
"rimraf": "^5.0.5",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"ulid": "^2.3.0",
|
||||||
"use-debounce": "^9.0.4"
|
"use-debounce": "^9.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -15,6 +15,9 @@
|
|||||||
"paths": { "*": ["node_modules/*"] },
|
"paths": { "*": ["node_modules/*"] },
|
||||||
"typeRoots": ["node_modules/@types"]
|
"typeRoots": ["node_modules/@types"]
|
||||||
},
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"esm": true
|
||||||
|
},
|
||||||
"include": ["./**/*.ts"],
|
"include": ["./**/*.ts"],
|
||||||
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
const { app, Menu, dialog } = require("electron");
|
import { app, Menu, dialog, shell } from "electron";
|
||||||
const isMac = process.platform === "darwin";
|
const isMac = process.platform === "darwin";
|
||||||
const { autoUpdater } = require("electron-updater");
|
const { autoUpdater } = require("electron-updater");
|
||||||
import { compareSemanticVersions } from "./versionDiff";
|
import { compareSemanticVersions } from "./versionDiff";
|
||||||
@ -97,7 +97,6 @@ const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
|
|||||||
{
|
{
|
||||||
label: "Learn More",
|
label: "Learn More",
|
||||||
click: async () => {
|
click: async () => {
|
||||||
const { shell } = require("electron");
|
|
||||||
await shell.openExternal("https://jan.ai/");
|
await shell.openExternal("https://jan.ai/");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
30
electron/utils/migration.ts
Normal file
30
electron/utils/migration.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { app } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
import { rmdir } from 'fs'
|
||||||
|
import Store from 'electron-store'
|
||||||
|
import { userSpacePath } from './path'
|
||||||
|
/**
|
||||||
|
* Migrates the extensions by deleting the `extensions` directory in the user data path.
|
||||||
|
* If the `migrated_version` key in the `Store` object does not match the current app version,
|
||||||
|
* the function deletes the `extensions` directory and sets the `migrated_version` key to the current app version.
|
||||||
|
* @returns A Promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
export function migrateExtensions() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const store = new Store()
|
||||||
|
if (store.get('migrated_version') !== app.getVersion()) {
|
||||||
|
console.debug('start migration:', store.get('migrated_version'))
|
||||||
|
const fullPath = join(userSpacePath, 'extensions')
|
||||||
|
|
||||||
|
rmdir(fullPath, { recursive: true }, function (err) {
|
||||||
|
if (err) console.error(err)
|
||||||
|
store.set('migrated_version', app.getVersion())
|
||||||
|
console.debug('migrate extensions done')
|
||||||
|
resolve(undefined)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
resolve(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -3,15 +3,16 @@ import { AssistantExtension } from "@janhq/core";
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
export default class JanAssistantExtension implements AssistantExtension {
|
export default class JanAssistantExtension implements AssistantExtension {
|
||||||
private static readonly _homeDir = "assistants";
|
private static readonly _homeDir = "file://assistants";
|
||||||
|
|
||||||
type(): ExtensionType {
|
type(): ExtensionType {
|
||||||
return ExtensionType.Assistant;
|
return ExtensionType.Assistant;
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad(): void {
|
async onLoad() {
|
||||||
// making the assistant directory
|
// making the assistant directory
|
||||||
fs.mkdir(JanAssistantExtension._homeDir).then(() => {
|
if (!(await fs.existsSync(JanAssistantExtension._homeDir)))
|
||||||
|
fs.mkdirSync(JanAssistantExtension._homeDir).then(() => {
|
||||||
this.createJanAssistant();
|
this.createJanAssistant();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -23,12 +24,12 @@ export default class JanAssistantExtension implements AssistantExtension {
|
|||||||
|
|
||||||
async createAssistant(assistant: Assistant): Promise<void> {
|
async createAssistant(assistant: Assistant): Promise<void> {
|
||||||
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
||||||
await fs.mkdir(assistantDir);
|
if (!(await fs.existsSync(assistantDir))) await fs.mkdirSync(assistantDir);
|
||||||
|
|
||||||
// store the assistant metadata json
|
// store the assistant metadata json
|
||||||
const assistantMetadataPath = join(assistantDir, "assistant.json");
|
const assistantMetadataPath = join(assistantDir, "assistant.json");
|
||||||
try {
|
try {
|
||||||
await fs.writeFile(
|
await fs.writeFileSync(
|
||||||
assistantMetadataPath,
|
assistantMetadataPath,
|
||||||
JSON.stringify(assistant, null, 2)
|
JSON.stringify(assistant, null, 2)
|
||||||
);
|
);
|
||||||
@ -41,18 +42,14 @@ export default class JanAssistantExtension implements AssistantExtension {
|
|||||||
// get all the assistant directories
|
// get all the assistant directories
|
||||||
// get all the assistant metadata json
|
// get all the assistant metadata json
|
||||||
const results: Assistant[] = [];
|
const results: Assistant[] = [];
|
||||||
const allFileName: string[] = await fs.listFiles(
|
const allFileName: string[] = await fs.readdirSync(
|
||||||
JanAssistantExtension._homeDir
|
JanAssistantExtension._homeDir
|
||||||
);
|
);
|
||||||
for (const fileName of allFileName) {
|
for (const fileName of allFileName) {
|
||||||
const filePath = join(JanAssistantExtension._homeDir, fileName);
|
const filePath = join(JanAssistantExtension._homeDir, fileName);
|
||||||
const isDirectory = await fs.isDirectory(filePath);
|
|
||||||
if (!isDirectory) {
|
|
||||||
// if not a directory, ignore
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonFiles: string[] = (await fs.listFiles(filePath)).filter(
|
if (filePath.includes(".DS_Store")) continue;
|
||||||
|
const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter(
|
||||||
(file: string) => file === "assistant.json"
|
(file: string) => file === "assistant.json"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -61,9 +58,12 @@ export default class JanAssistantExtension implements AssistantExtension {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistant: Assistant = JSON.parse(
|
const content = await fs.readFileSync(
|
||||||
await fs.readFile(join(filePath, jsonFiles[0]))
|
join(filePath, jsonFiles[0]),
|
||||||
|
"utf-8"
|
||||||
);
|
);
|
||||||
|
const assistant: Assistant =
|
||||||
|
typeof content === "object" ? content : JSON.parse(content);
|
||||||
|
|
||||||
results.push(assistant);
|
results.push(assistant);
|
||||||
}
|
}
|
||||||
@ -78,7 +78,7 @@ export default class JanAssistantExtension implements AssistantExtension {
|
|||||||
|
|
||||||
// remove the directory
|
// remove the directory
|
||||||
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
||||||
await fs.rmdir(assistantDir);
|
await fs.rmdirSync(assistantDir);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { ExtensionType, fs } from '@janhq/core'
|
import { ExtensionType, fs, joinPath } from '@janhq/core'
|
||||||
import { ConversationalExtension } from '@janhq/core'
|
import { ConversationalExtension } from '@janhq/core'
|
||||||
import { Thread, ThreadMessage } from '@janhq/core'
|
import { Thread, ThreadMessage } from '@janhq/core'
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSONConversationalExtension is a ConversationalExtension implementation that provides
|
* JSONConversationalExtension is a ConversationalExtension implementation that provides
|
||||||
@ -10,7 +9,7 @@ import { join } from 'path'
|
|||||||
export default class JSONConversationalExtension
|
export default class JSONConversationalExtension
|
||||||
implements ConversationalExtension
|
implements ConversationalExtension
|
||||||
{
|
{
|
||||||
private static readonly _homeDir = 'threads'
|
private static readonly _homeDir = 'file://threads'
|
||||||
private static readonly _threadInfoFileName = 'thread.json'
|
private static readonly _threadInfoFileName = 'thread.json'
|
||||||
private static readonly _threadMessagesFileName = 'messages.jsonl'
|
private static readonly _threadMessagesFileName = 'messages.jsonl'
|
||||||
|
|
||||||
@ -24,8 +23,9 @@ export default class JSONConversationalExtension
|
|||||||
/**
|
/**
|
||||||
* Called when the extension is loaded.
|
* Called when the extension is loaded.
|
||||||
*/
|
*/
|
||||||
onLoad() {
|
async onLoad() {
|
||||||
fs.mkdir(JSONConversationalExtension._homeDir)
|
if (!(await fs.existsSync(JSONConversationalExtension._homeDir)))
|
||||||
|
fs.mkdirSync(JSONConversationalExtension._homeDir)
|
||||||
console.debug('JSONConversationalExtension loaded')
|
console.debug('JSONConversationalExtension loaded')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +48,9 @@ export default class JSONConversationalExtension
|
|||||||
const convos = promiseResults
|
const convos = promiseResults
|
||||||
.map((result) => {
|
.map((result) => {
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
return JSON.parse(result.value) as Thread
|
return typeof result.value === 'object'
|
||||||
|
? result.value
|
||||||
|
: JSON.parse(result.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((convo) => convo != null)
|
.filter((convo) => convo != null)
|
||||||
@ -69,16 +71,19 @@ export default class JSONConversationalExtension
|
|||||||
*/
|
*/
|
||||||
async saveThread(thread: Thread): Promise<void> {
|
async saveThread(thread: Thread): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const threadDirPath = join(
|
const threadDirPath = await joinPath([
|
||||||
JSONConversationalExtension._homeDir,
|
JSONConversationalExtension._homeDir,
|
||||||
thread.id
|
thread.id,
|
||||||
)
|
])
|
||||||
const threadJsonPath = join(
|
const threadJsonPath = await joinPath([
|
||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalExtension._threadInfoFileName
|
JSONConversationalExtension._threadInfoFileName,
|
||||||
)
|
])
|
||||||
await fs.mkdir(threadDirPath)
|
if (!(await fs.existsSync(threadDirPath))) {
|
||||||
await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2))
|
await fs.mkdirSync(threadDirPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFileSync(threadJsonPath, JSON.stringify(thread))
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Promise.reject(err)
|
Promise.reject(err)
|
||||||
@ -89,22 +94,26 @@ export default class JSONConversationalExtension
|
|||||||
* Delete a thread with the specified ID.
|
* Delete a thread with the specified ID.
|
||||||
* @param threadId The ID of the thread to delete.
|
* @param threadId The ID of the thread to delete.
|
||||||
*/
|
*/
|
||||||
deleteThread(threadId: string): Promise<void> {
|
async deleteThread(threadId: string): Promise<void> {
|
||||||
return fs.rmdir(join(JSONConversationalExtension._homeDir, `${threadId}`))
|
return fs.rmdirSync(
|
||||||
|
await joinPath([JSONConversationalExtension._homeDir, `${threadId}`]),
|
||||||
|
{ recursive: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async addNewMessage(message: ThreadMessage): Promise<void> {
|
async addNewMessage(message: ThreadMessage): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const threadDirPath = join(
|
const threadDirPath = await joinPath([
|
||||||
JSONConversationalExtension._homeDir,
|
JSONConversationalExtension._homeDir,
|
||||||
message.thread_id
|
message.thread_id,
|
||||||
)
|
])
|
||||||
const threadMessagePath = join(
|
const threadMessagePath = await joinPath([
|
||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalExtension._threadMessagesFileName
|
JSONConversationalExtension._threadMessagesFileName,
|
||||||
)
|
])
|
||||||
await fs.mkdir(threadDirPath)
|
if (!(await fs.existsSync(threadDirPath)))
|
||||||
await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n')
|
await fs.mkdirSync(threadDirPath)
|
||||||
|
await fs.appendFileSync(threadMessagePath, JSON.stringify(message) + '\n')
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Promise.reject(err)
|
Promise.reject(err)
|
||||||
@ -116,13 +125,17 @@ export default class JSONConversationalExtension
|
|||||||
messages: ThreadMessage[]
|
messages: ThreadMessage[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const threadDirPath = join(JSONConversationalExtension._homeDir, threadId)
|
const threadDirPath = await joinPath([
|
||||||
const threadMessagePath = join(
|
JSONConversationalExtension._homeDir,
|
||||||
|
threadId,
|
||||||
|
])
|
||||||
|
const threadMessagePath = await joinPath([
|
||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalExtension._threadMessagesFileName
|
JSONConversationalExtension._threadMessagesFileName,
|
||||||
)
|
])
|
||||||
await fs.mkdir(threadDirPath)
|
if (!(await fs.existsSync(threadDirPath)))
|
||||||
await fs.writeFile(
|
await fs.mkdirSync(threadDirPath)
|
||||||
|
await fs.writeFileSync(
|
||||||
threadMessagePath,
|
threadMessagePath,
|
||||||
messages.map((msg) => JSON.stringify(msg)).join('\n') +
|
messages.map((msg) => JSON.stringify(msg)).join('\n') +
|
||||||
(messages.length ? '\n' : '')
|
(messages.length ? '\n' : '')
|
||||||
@ -139,12 +152,13 @@ export default class JSONConversationalExtension
|
|||||||
* @returns data of the thread
|
* @returns data of the thread
|
||||||
*/
|
*/
|
||||||
private async readThread(threadDirName: string): Promise<any> {
|
private async readThread(threadDirName: string): Promise<any> {
|
||||||
return fs.readFile(
|
return fs.readFileSync(
|
||||||
join(
|
await joinPath([
|
||||||
JSONConversationalExtension._homeDir,
|
JSONConversationalExtension._homeDir,
|
||||||
threadDirName,
|
threadDirName,
|
||||||
JSONConversationalExtension._threadInfoFileName
|
JSONConversationalExtension._threadInfoFileName,
|
||||||
)
|
]),
|
||||||
|
'utf-8'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,23 +167,19 @@ export default class JSONConversationalExtension
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async getValidThreadDirs(): Promise<string[]> {
|
private async getValidThreadDirs(): Promise<string[]> {
|
||||||
const fileInsideThread: string[] = await fs.listFiles(
|
const fileInsideThread: string[] = await fs.readdirSync(
|
||||||
JSONConversationalExtension._homeDir
|
JSONConversationalExtension._homeDir
|
||||||
)
|
)
|
||||||
|
|
||||||
const threadDirs: string[] = []
|
const threadDirs: string[] = []
|
||||||
for (let i = 0; i < fileInsideThread.length; i++) {
|
for (let i = 0; i < fileInsideThread.length; i++) {
|
||||||
const path = join(
|
if (fileInsideThread[i].includes('.DS_Store')) continue
|
||||||
|
const path = await joinPath([
|
||||||
JSONConversationalExtension._homeDir,
|
JSONConversationalExtension._homeDir,
|
||||||
fileInsideThread[i]
|
fileInsideThread[i],
|
||||||
)
|
])
|
||||||
const isDirectory = await fs.isDirectory(path)
|
|
||||||
if (!isDirectory) {
|
|
||||||
console.debug(`Ignore ${path} because it is not a directory`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const isHavingThreadInfo = (await fs.listFiles(path)).includes(
|
const isHavingThreadInfo = (await fs.readdirSync(path)).includes(
|
||||||
JSONConversationalExtension._threadInfoFileName
|
JSONConversationalExtension._threadInfoFileName
|
||||||
)
|
)
|
||||||
if (!isHavingThreadInfo) {
|
if (!isHavingThreadInfo) {
|
||||||
@ -184,25 +194,31 @@ export default class JSONConversationalExtension
|
|||||||
|
|
||||||
async getAllMessages(threadId: string): Promise<ThreadMessage[]> {
|
async getAllMessages(threadId: string): Promise<ThreadMessage[]> {
|
||||||
try {
|
try {
|
||||||
const threadDirPath = join(JSONConversationalExtension._homeDir, threadId)
|
const threadDirPath = await joinPath([
|
||||||
const isDir = await fs.isDirectory(threadDirPath)
|
JSONConversationalExtension._homeDir,
|
||||||
if (!isDir) {
|
threadId,
|
||||||
throw Error(`${threadDirPath} is not directory`)
|
])
|
||||||
}
|
|
||||||
|
|
||||||
const files: string[] = await fs.listFiles(threadDirPath)
|
const files: string[] = await fs.readdirSync(threadDirPath)
|
||||||
if (
|
if (
|
||||||
!files.includes(JSONConversationalExtension._threadMessagesFileName)
|
!files.includes(JSONConversationalExtension._threadMessagesFileName)
|
||||||
) {
|
) {
|
||||||
throw Error(`${threadDirPath} not contains message file`)
|
throw Error(`${threadDirPath} not contains message file`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageFilePath = join(
|
const messageFilePath = await joinPath([
|
||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalExtension._threadMessagesFileName
|
JSONConversationalExtension._threadMessagesFileName,
|
||||||
)
|
])
|
||||||
|
|
||||||
const result = await fs.readLineByLine(messageFilePath)
|
const result = await fs
|
||||||
|
.readFileSync(messageFilePath, 'utf-8')
|
||||||
|
.then((content) =>
|
||||||
|
content
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line !== '')
|
||||||
|
)
|
||||||
|
|
||||||
const messages: ThreadMessage[] = []
|
const messages: ThreadMessage[] = []
|
||||||
result.forEach((line: string) => {
|
result.forEach((line: string) => {
|
||||||
|
|||||||
@ -17,9 +17,9 @@ import {
|
|||||||
ThreadMessage,
|
ThreadMessage,
|
||||||
events,
|
events,
|
||||||
executeOnMain,
|
executeOnMain,
|
||||||
getUserSpace,
|
|
||||||
fs,
|
fs,
|
||||||
Model,
|
Model,
|
||||||
|
joinPath,
|
||||||
} from "@janhq/core";
|
} from "@janhq/core";
|
||||||
import { InferenceExtension } from "@janhq/core";
|
import { InferenceExtension } from "@janhq/core";
|
||||||
import { requestInference } from "./helpers/sse";
|
import { requestInference } from "./helpers/sse";
|
||||||
@ -58,8 +58,9 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
|||||||
/**
|
/**
|
||||||
* Subscribes to events emitted by the @janhq/core package.
|
* Subscribes to events emitted by the @janhq/core package.
|
||||||
*/
|
*/
|
||||||
onLoad(): void {
|
async onLoad() {
|
||||||
fs.mkdir(JanInferenceNitroExtension._homeDir);
|
if (!(await fs.existsSync(JanInferenceNitroExtension._homeDir)))
|
||||||
|
fs.mkdirSync(JanInferenceNitroExtension._homeDir);
|
||||||
this.writeDefaultEngineSettings();
|
this.writeDefaultEngineSettings();
|
||||||
|
|
||||||
// Events subscription
|
// Events subscription
|
||||||
@ -91,12 +92,12 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
|||||||
JanInferenceNitroExtension._homeDir,
|
JanInferenceNitroExtension._homeDir,
|
||||||
JanInferenceNitroExtension._engineMetadataFileName
|
JanInferenceNitroExtension._engineMetadataFileName
|
||||||
);
|
);
|
||||||
if (await fs.exists(engineFile)) {
|
if (await fs.existsSync(engineFile)) {
|
||||||
JanInferenceNitroExtension._engineSettings = JSON.parse(
|
const engine = await fs.readFileSync(engineFile, "utf-8");
|
||||||
await fs.readFile(engineFile)
|
JanInferenceNitroExtension._engineSettings =
|
||||||
);
|
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||||
} else {
|
} else {
|
||||||
await fs.writeFile(
|
await fs.writeFileSync(
|
||||||
engineFile,
|
engineFile,
|
||||||
JSON.stringify(JanInferenceNitroExtension._engineSettings, null, 2)
|
JSON.stringify(JanInferenceNitroExtension._engineSettings, null, 2)
|
||||||
);
|
);
|
||||||
@ -110,8 +111,7 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
|||||||
if (model.engine !== "nitro") {
|
if (model.engine !== "nitro") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const userSpacePath = await getUserSpace();
|
const modelFullPath = await joinPath(["models", model.id]);
|
||||||
const modelFullPath = join(userSpacePath, "models", model.id, model.id);
|
|
||||||
|
|
||||||
const nitroInitResult = await executeOnMain(MODULE, "initModel", {
|
const nitroInitResult = await executeOnMain(MODULE, "initModel", {
|
||||||
modelFullPath: modelFullPath,
|
modelFullPath: modelFullPath,
|
||||||
|
|||||||
@ -13,10 +13,11 @@ const NITRO_HTTP_LOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/
|
|||||||
const NITRO_HTTP_UNLOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/unloadModel`;
|
const NITRO_HTTP_UNLOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/unloadModel`;
|
||||||
const NITRO_HTTP_VALIDATE_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/modelstatus`;
|
const NITRO_HTTP_VALIDATE_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/modelstatus`;
|
||||||
const NITRO_HTTP_KILL_URL = `${NITRO_HTTP_SERVER_URL}/processmanager/destroy`;
|
const NITRO_HTTP_KILL_URL = `${NITRO_HTTP_SERVER_URL}/processmanager/destroy`;
|
||||||
|
const SUPPORTED_MODEL_FORMAT = ".gguf";
|
||||||
|
|
||||||
// The subprocess instance for Nitro
|
// The subprocess instance for Nitro
|
||||||
let subprocess = undefined;
|
let subprocess = undefined;
|
||||||
let currentModelFile = undefined;
|
let currentModelFile: string = undefined;
|
||||||
let currentSettings = undefined;
|
let currentSettings = undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,6 +38,21 @@ function stopModel(): Promise<void> {
|
|||||||
*/
|
*/
|
||||||
async function initModel(wrapper: any): Promise<ModelOperationResponse> {
|
async function initModel(wrapper: any): Promise<ModelOperationResponse> {
|
||||||
currentModelFile = wrapper.modelFullPath;
|
currentModelFile = wrapper.modelFullPath;
|
||||||
|
const janRoot = path.join(require("os").homedir(), "jan");
|
||||||
|
if (!currentModelFile.includes(janRoot)) {
|
||||||
|
currentModelFile = path.join(janRoot, currentModelFile);
|
||||||
|
}
|
||||||
|
const files: string[] = fs.readdirSync(currentModelFile);
|
||||||
|
|
||||||
|
// Look for GGUF model file
|
||||||
|
const ggufBinFile = files.find(
|
||||||
|
(file) =>
|
||||||
|
file === path.basename(currentModelFile) ||
|
||||||
|
file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT)
|
||||||
|
);
|
||||||
|
|
||||||
|
currentModelFile = path.join(currentModelFile, ggufBinFile);
|
||||||
|
|
||||||
if (wrapper.model.engine !== "nitro") {
|
if (wrapper.model.engine !== "nitro") {
|
||||||
return Promise.resolve({ error: "Not a nitro model" });
|
return Promise.resolve({ error: "Not a nitro model" });
|
||||||
} else {
|
} else {
|
||||||
@ -66,14 +82,14 @@ async function initModel(wrapper: any): Promise<ModelOperationResponse> {
|
|||||||
async function loadModel(nitroResourceProbe: any | undefined) {
|
async function loadModel(nitroResourceProbe: any | undefined) {
|
||||||
// Gather system information for CPU physical cores and memory
|
// Gather system information for CPU physical cores and memory
|
||||||
if (!nitroResourceProbe) nitroResourceProbe = await getResourcesInfo();
|
if (!nitroResourceProbe) nitroResourceProbe = await getResourcesInfo();
|
||||||
return killSubprocess()
|
return (
|
||||||
|
killSubprocess()
|
||||||
.then(() => tcpPortUsed.waitUntilFree(PORT, 300, 5000))
|
.then(() => tcpPortUsed.waitUntilFree(PORT, 300, 5000))
|
||||||
// wait for 500ms to make sure the port is free for windows platform
|
// wait for 500ms to make sure the port is free for windows platform
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
return sleep(500);
|
return sleep(500);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return sleep(0);
|
return sleep(0);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -84,7 +100,8 @@ async function loadModel(nitroResourceProbe: any | undefined) {
|
|||||||
console.error("error: ", err);
|
console.error("error: ", err);
|
||||||
// TODO: Broadcast error so app could display proper error message
|
// TODO: Broadcast error so app could display proper error message
|
||||||
return { error: err, currentModelFile };
|
return { error: err, currentModelFile };
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add function sleep
|
// Add function sleep
|
||||||
@ -259,11 +276,11 @@ function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
|
|||||||
function getResourcesInfo(): Promise<ResourcesInfo> {
|
function getResourcesInfo(): Promise<ResourcesInfo> {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
const cpu = await si.cpu();
|
const cpu = await si.cpu();
|
||||||
const mem = await si.mem();
|
// const mem = await si.mem();
|
||||||
|
|
||||||
const response = {
|
const response: ResourcesInfo = {
|
||||||
numCpuPhysicalCore: cpu.physicalCores,
|
numCpuPhysicalCore: cpu.physicalCores,
|
||||||
memAvailable: mem.available,
|
memAvailable: 0,
|
||||||
};
|
};
|
||||||
resolve(response);
|
resolve(response);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import { join } from "path";
|
|||||||
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
||||||
*/
|
*/
|
||||||
export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
||||||
private static readonly _homeDir = "engines";
|
private static readonly _homeDir = "file://engines";
|
||||||
private static readonly _engineMetadataFileName = "openai.json";
|
private static readonly _engineMetadataFileName = "openai.json";
|
||||||
|
|
||||||
private static _currentModel: OpenAIModel;
|
private static _currentModel: OpenAIModel;
|
||||||
@ -53,8 +53,9 @@ export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
|||||||
/**
|
/**
|
||||||
* Subscribes to events emitted by the @janhq/core package.
|
* Subscribes to events emitted by the @janhq/core package.
|
||||||
*/
|
*/
|
||||||
onLoad(): void {
|
async onLoad() {
|
||||||
fs.mkdir(JanInferenceOpenAIExtension._homeDir);
|
if (!(await fs.existsSync(JanInferenceOpenAIExtension._homeDir)))
|
||||||
|
fs.mkdirSync(JanInferenceOpenAIExtension._homeDir);
|
||||||
JanInferenceOpenAIExtension.writeDefaultEngineSettings();
|
JanInferenceOpenAIExtension.writeDefaultEngineSettings();
|
||||||
|
|
||||||
// Events subscription
|
// Events subscription
|
||||||
@ -85,12 +86,12 @@ export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
|||||||
JanInferenceOpenAIExtension._homeDir,
|
JanInferenceOpenAIExtension._homeDir,
|
||||||
JanInferenceOpenAIExtension._engineMetadataFileName
|
JanInferenceOpenAIExtension._engineMetadataFileName
|
||||||
);
|
);
|
||||||
if (await fs.exists(engineFile)) {
|
if (await fs.existsSync(engineFile)) {
|
||||||
JanInferenceOpenAIExtension._engineSettings = JSON.parse(
|
const engine = await fs.readFileSync(engineFile, 'utf-8');
|
||||||
await fs.readFile(engineFile)
|
JanInferenceOpenAIExtension._engineSettings =
|
||||||
);
|
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||||
} else {
|
} else {
|
||||||
await fs.writeFile(
|
await fs.writeFileSync(
|
||||||
engineFile,
|
engineFile,
|
||||||
JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2)
|
JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -57,8 +57,8 @@ export default class JanInferenceTritonTrtLLMExtension
|
|||||||
/**
|
/**
|
||||||
* Subscribes to events emitted by the @janhq/core package.
|
* Subscribes to events emitted by the @janhq/core package.
|
||||||
*/
|
*/
|
||||||
onLoad(): void {
|
async onLoad() {
|
||||||
fs.mkdir(JanInferenceTritonTrtLLMExtension._homeDir);
|
if (!(await fs.existsSync(JanInferenceTritonTrtLLMExtension._homeDir)))
|
||||||
JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings();
|
JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings();
|
||||||
|
|
||||||
// Events subscription
|
// Events subscription
|
||||||
@ -98,12 +98,12 @@ export default class JanInferenceTritonTrtLLMExtension
|
|||||||
JanInferenceTritonTrtLLMExtension._homeDir,
|
JanInferenceTritonTrtLLMExtension._homeDir,
|
||||||
JanInferenceTritonTrtLLMExtension._engineMetadataFileName
|
JanInferenceTritonTrtLLMExtension._engineMetadataFileName
|
||||||
);
|
);
|
||||||
if (await fs.exists(engine_json)) {
|
if (await fs.existsSync(engine_json)) {
|
||||||
JanInferenceTritonTrtLLMExtension._engineSettings = JSON.parse(
|
const engine = await fs.readFileSync(engine_json, "utf-8");
|
||||||
await fs.readFile(engine_json)
|
JanInferenceTritonTrtLLMExtension._engineSettings =
|
||||||
);
|
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||||
} else {
|
} else {
|
||||||
await fs.writeFile(
|
await fs.writeFileSync(
|
||||||
engine_json,
|
engine_json,
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
JanInferenceTritonTrtLLMExtension._engineSettings,
|
JanInferenceTritonTrtLLMExtension._engineSettings,
|
||||||
|
|||||||
@ -5,16 +5,21 @@ import {
|
|||||||
abortDownload,
|
abortDownload,
|
||||||
getResourcePath,
|
getResourcePath,
|
||||||
getUserSpace,
|
getUserSpace,
|
||||||
|
InferenceEngine,
|
||||||
|
joinPath,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { ModelExtension, Model, ModelState } from '@janhq/core'
|
import { basename } from 'path'
|
||||||
import { join } from 'path'
|
import { ModelExtension, Model } from '@janhq/core'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A extension for models
|
* A extension for models
|
||||||
*/
|
*/
|
||||||
export default class JanModelExtension implements ModelExtension {
|
export default class JanModelExtension implements ModelExtension {
|
||||||
private static readonly _homeDir = 'models'
|
private static readonly _homeDir = 'file://models'
|
||||||
private static readonly _modelMetadataFileName = 'model.json'
|
private static readonly _modelMetadataFileName = 'model.json'
|
||||||
|
private static readonly _supportedModelFormat = '.gguf'
|
||||||
|
private static readonly _incompletedModelFileName = '.download'
|
||||||
|
private static readonly _offlineInferenceEngine = InferenceEngine.nitro
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements type from JanExtension.
|
* Implements type from JanExtension.
|
||||||
@ -41,11 +46,11 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
|
|
||||||
private async copyModelsToHomeDir() {
|
private async copyModelsToHomeDir() {
|
||||||
try {
|
try {
|
||||||
if (
|
// list all of the files under the home directory
|
||||||
localStorage.getItem(`${EXTENSION_NAME}-version`) === VERSION &&
|
|
||||||
(await fs.exists(JanModelExtension._homeDir))
|
if (fs.existsSync(JanModelExtension._homeDir)) {
|
||||||
) {
|
// ignore if the model is already downloaded
|
||||||
console.debug('Model already migrated')
|
console.debug('Models already persisted.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,10 +59,10 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
|
|
||||||
// copy models folder from resources to home directory
|
// copy models folder from resources to home directory
|
||||||
const resourePath = await getResourcePath()
|
const resourePath = await getResourcePath()
|
||||||
const srcPath = join(resourePath, 'models')
|
const srcPath = await joinPath([resourePath, 'models'])
|
||||||
|
|
||||||
const userSpace = await getUserSpace()
|
const userSpace = await getUserSpace()
|
||||||
const destPath = join(userSpace, JanModelExtension._homeDir)
|
const destPath = await joinPath([userSpace, JanModelExtension._homeDir])
|
||||||
|
|
||||||
await fs.syncFile(srcPath, destPath)
|
await fs.syncFile(srcPath, destPath)
|
||||||
|
|
||||||
@ -88,11 +93,18 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
*/
|
*/
|
||||||
async downloadModel(model: Model): Promise<void> {
|
async downloadModel(model: Model): Promise<void> {
|
||||||
// create corresponding directory
|
// create corresponding directory
|
||||||
const directoryPath = join(JanModelExtension._homeDir, model.id)
|
const modelDirPath = await joinPath([JanModelExtension._homeDir, model.id])
|
||||||
await fs.mkdir(directoryPath)
|
if (!(await fs.existsSync(modelDirPath))) await fs.mkdirSync(modelDirPath)
|
||||||
|
|
||||||
// path to model binary
|
// try to retrieve the download file name from the source url
|
||||||
const path = join(directoryPath, model.id)
|
// if it fails, use the model ID as the file name
|
||||||
|
const extractedFileName = basename(model.source_url)
|
||||||
|
const fileName = extractedFileName
|
||||||
|
.toLowerCase()
|
||||||
|
.endsWith(JanModelExtension._supportedModelFormat)
|
||||||
|
? extractedFileName
|
||||||
|
: model.id
|
||||||
|
const path = await joinPath([modelDirPath, fileName])
|
||||||
downloadFile(model.source_url, path)
|
downloadFile(model.source_url, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,9 +115,11 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
*/
|
*/
|
||||||
async cancelModelDownload(modelId: string): Promise<void> {
|
async cancelModelDownload(modelId: string): Promise<void> {
|
||||||
return abortDownload(
|
return abortDownload(
|
||||||
join(JanModelExtension._homeDir, modelId, modelId)
|
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
||||||
).then(() => {
|
).then(async () => {
|
||||||
fs.deleteFile(join(JanModelExtension._homeDir, modelId, modelId))
|
fs.unlinkSync(
|
||||||
|
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,27 +130,16 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
*/
|
*/
|
||||||
async deleteModel(modelId: string): Promise<void> {
|
async deleteModel(modelId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const dirPath = join(JanModelExtension._homeDir, modelId)
|
const dirPath = await joinPath([JanModelExtension._homeDir, modelId])
|
||||||
|
|
||||||
// remove all files under dirPath except model.json
|
// remove all files under dirPath except model.json
|
||||||
const files = await fs.listFiles(dirPath)
|
const files = await fs.readdirSync(dirPath)
|
||||||
const deletePromises = files.map((fileName: string) => {
|
const deletePromises = files.map(async (fileName: string) => {
|
||||||
if (fileName !== JanModelExtension._modelMetadataFileName) {
|
if (fileName !== JanModelExtension._modelMetadataFileName) {
|
||||||
return fs.deleteFile(join(dirPath, fileName))
|
return fs.unlinkSync(await joinPath([dirPath, fileName]))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await Promise.allSettled(deletePromises)
|
await Promise.allSettled(deletePromises)
|
||||||
|
|
||||||
// update the state as default
|
|
||||||
const jsonFilePath = join(
|
|
||||||
dirPath,
|
|
||||||
JanModelExtension._modelMetadataFileName
|
|
||||||
)
|
|
||||||
const json = await fs.readFile(jsonFilePath)
|
|
||||||
const model = JSON.parse(json) as Model
|
|
||||||
delete model.state
|
|
||||||
|
|
||||||
await fs.writeFile(jsonFilePath, JSON.stringify(model, null, 2))
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@ -148,24 +151,14 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
* @returns A Promise that resolves when the model is saved.
|
* @returns A Promise that resolves when the model is saved.
|
||||||
*/
|
*/
|
||||||
async saveModel(model: Model): Promise<void> {
|
async saveModel(model: Model): Promise<void> {
|
||||||
const jsonFilePath = join(
|
const jsonFilePath = await joinPath([
|
||||||
JanModelExtension._homeDir,
|
JanModelExtension._homeDir,
|
||||||
model.id,
|
model.id,
|
||||||
JanModelExtension._modelMetadataFileName
|
JanModelExtension._modelMetadataFileName,
|
||||||
)
|
])
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.writeFile(
|
await fs.writeFileSync(jsonFilePath, JSON.stringify(model, null, 2))
|
||||||
jsonFilePath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
...model,
|
|
||||||
state: ModelState.Ready,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@ -176,43 +169,70 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
* @returns A Promise that resolves with an array of all models.
|
* @returns A Promise that resolves with an array of all models.
|
||||||
*/
|
*/
|
||||||
async getDownloadedModels(): Promise<Model[]> {
|
async getDownloadedModels(): Promise<Model[]> {
|
||||||
const models = await this.getModelsMetadata()
|
return await this.getModelsMetadata(
|
||||||
return models.filter((model) => model.state === ModelState.Ready)
|
async (modelDir: string, model: Model) => {
|
||||||
|
if (model.engine !== JanModelExtension._offlineInferenceEngine) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return await fs
|
||||||
|
.readdirSync(await joinPath([JanModelExtension._homeDir, modelDir]))
|
||||||
|
.then((files: string[]) => {
|
||||||
|
// or model binary exists in the directory
|
||||||
|
// model binary name can match model ID or be a .gguf file and not be an incompleted model file
|
||||||
|
return (
|
||||||
|
files.includes(modelDir) ||
|
||||||
|
files.some(
|
||||||
|
(file) =>
|
||||||
|
file
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(JanModelExtension._supportedModelFormat) &&
|
||||||
|
!file.endsWith(JanModelExtension._incompletedModelFileName)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getModelsMetadata(): Promise<Model[]> {
|
private async getModelsMetadata(
|
||||||
|
selector?: (path: string, model: Model) => Promise<boolean>
|
||||||
|
): Promise<Model[]> {
|
||||||
try {
|
try {
|
||||||
const filesUnderJanRoot = await fs.listFiles('')
|
if (!(await fs.existsSync(JanModelExtension._homeDir))) {
|
||||||
if (!filesUnderJanRoot.includes(JanModelExtension._homeDir)) {
|
|
||||||
console.debug('model folder not found')
|
console.debug('model folder not found')
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const files: string[] = await fs.listFiles(JanModelExtension._homeDir)
|
const files: string[] = await fs.readdirSync(JanModelExtension._homeDir)
|
||||||
|
|
||||||
const allDirectories: string[] = []
|
const allDirectories: string[] = []
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const isDirectory = await fs.isDirectory(
|
if (file === '.DS_Store') continue
|
||||||
join(JanModelExtension._homeDir, file)
|
|
||||||
)
|
|
||||||
if (isDirectory) {
|
|
||||||
allDirectories.push(file)
|
allDirectories.push(file)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const readJsonPromises = allDirectories.map((dirName) => {
|
const readJsonPromises = allDirectories.map(async (dirName) => {
|
||||||
const jsonPath = join(
|
// filter out directories that don't match the selector
|
||||||
|
|
||||||
|
// read model.json
|
||||||
|
const jsonPath = await joinPath([
|
||||||
JanModelExtension._homeDir,
|
JanModelExtension._homeDir,
|
||||||
dirName,
|
dirName,
|
||||||
JanModelExtension._modelMetadataFileName
|
JanModelExtension._modelMetadataFileName,
|
||||||
)
|
])
|
||||||
return this.readModelMetadata(jsonPath)
|
let model = await this.readModelMetadata(jsonPath)
|
||||||
|
model = typeof model === 'object' ? model : JSON.parse(model)
|
||||||
|
|
||||||
|
if (selector && !(await selector?.(dirName, model))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return model
|
||||||
})
|
})
|
||||||
const results = await Promise.allSettled(readJsonPromises)
|
const results = await Promise.allSettled(readJsonPromises)
|
||||||
const modelData = results.map((result) => {
|
const modelData = results.map((result) => {
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(result.value) as Model
|
return result.value as Model
|
||||||
} catch {
|
} catch {
|
||||||
console.debug(`Unable to parse model metadata: ${result.value}`)
|
console.debug(`Unable to parse model metadata: ${result.value}`)
|
||||||
return undefined
|
return undefined
|
||||||
@ -222,6 +242,7 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return modelData.filter((e) => !!e)
|
return modelData.filter((e) => !!e)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@ -230,7 +251,7 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readModelMetadata(path: string) {
|
private readModelMetadata(path: string) {
|
||||||
return fs.readFile(join(path))
|
return fs.readFileSync(path, 'utf-8')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -4,11 +4,11 @@ const getResourcesInfo = async () =>
|
|||||||
new Promise(async (resolve) => {
|
new Promise(async (resolve) => {
|
||||||
const cpu = await si.cpu();
|
const cpu = await si.cpu();
|
||||||
const mem = await si.mem();
|
const mem = await si.mem();
|
||||||
const gpu = await si.graphics();
|
// const gpu = await si.graphics();
|
||||||
const response = {
|
const response = {
|
||||||
cpu,
|
cpu,
|
||||||
mem,
|
mem,
|
||||||
gpu,
|
// gpu,
|
||||||
};
|
};
|
||||||
resolve(response);
|
resolve(response);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,7 +6,8 @@
|
|||||||
"uikit",
|
"uikit",
|
||||||
"core",
|
"core",
|
||||||
"electron",
|
"electron",
|
||||||
"web"
|
"web",
|
||||||
|
"server"
|
||||||
],
|
],
|
||||||
"nohoist": [
|
"nohoist": [
|
||||||
"uikit",
|
"uikit",
|
||||||
@ -16,7 +17,9 @@
|
|||||||
"electron",
|
"electron",
|
||||||
"electron/**",
|
"electron/**",
|
||||||
"web",
|
"web",
|
||||||
"web/**"
|
"web/**",
|
||||||
|
"server",
|
||||||
|
"server/**"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -28,6 +31,7 @@
|
|||||||
"test-local": "yarn lint && yarn build:test && yarn test",
|
"test-local": "yarn lint && yarn build:test && yarn test",
|
||||||
"dev:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit dev",
|
"dev:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit dev",
|
||||||
"build:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build",
|
"build:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build",
|
||||||
|
"build:server": "cd server && yarn install && yarn run build",
|
||||||
"build:core": "cd core && yarn install && yarn run build",
|
"build:core": "cd core && yarn install && yarn run build",
|
||||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||||
"build:electron": "cpx \"models/**\" \"electron/models/\" && yarn workspace jan build",
|
"build:electron": "cpx \"models/**\" \"electron/models/\" && yarn workspace jan build",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
43
server/index.ts
Normal file
43
server/index.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import fastify from "fastify";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import { v1Router } from "@janhq/core/node";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const JAN_API_HOST = process.env.JAN_API_HOST || "0.0.0.0";
|
||||||
|
const JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || "1337");
|
||||||
|
|
||||||
|
const server = fastify();
|
||||||
|
server.register(require("@fastify/cors"), {});
|
||||||
|
server.register(
|
||||||
|
(childContext, _, done) => {
|
||||||
|
childContext.register(require("@fastify/static"), {
|
||||||
|
root:
|
||||||
|
process.env.EXTENSION_ROOT ||
|
||||||
|
path.join(require("os").homedir(), "jan", "extensions"),
|
||||||
|
wildcard: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
{ prefix: "extensions" }
|
||||||
|
);
|
||||||
|
server.register(v1Router, { prefix: "/v1" });
|
||||||
|
|
||||||
|
export const startServer = () => {
|
||||||
|
server
|
||||||
|
.listen({
|
||||||
|
port: JAN_API_PORT,
|
||||||
|
host: JAN_API_HOST,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log(
|
||||||
|
`JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stopServer = () => {
|
||||||
|
server.close();
|
||||||
|
};
|
||||||
@ -1,19 +1,3 @@
|
|||||||
import fastify from 'fastify'
|
import { startServer } from "./index";
|
||||||
import dotenv from 'dotenv'
|
|
||||||
import v1API from './v1'
|
|
||||||
const server = fastify()
|
|
||||||
|
|
||||||
dotenv.config()
|
|
||||||
server.register(v1API, {prefix: "/api/v1"})
|
|
||||||
|
|
||||||
|
|
||||||
const JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || '1337')
|
|
||||||
const JAN_API_HOST = process.env.JAN_API_HOST || "0.0.0.0"
|
|
||||||
|
|
||||||
server.listen({
|
|
||||||
port: JAN_API_PORT,
|
|
||||||
host: JAN_API_HOST
|
|
||||||
}).then(() => {
|
|
||||||
console.log(`JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`);
|
|
||||||
})
|
|
||||||
|
|
||||||
|
startServer();
|
||||||
|
|||||||
@ -1,32 +1,37 @@
|
|||||||
{
|
{
|
||||||
"name": "jan-server",
|
"name": "@janhq/server",
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"main": "./build/main.js",
|
"main": "build/index.js",
|
||||||
|
"types": "build/index.d.ts",
|
||||||
"author": "Jan <service@jan.ai>",
|
"author": "Jan <service@jan.ai>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"homepage": "https://jan.ai",
|
"homepage": "https://jan.ai",
|
||||||
"description": "Use offline LLMs with your own data. Run open source models like Llama2 or Falcon on your internal computers/servers.",
|
"description": "Use offline LLMs with your own data. Run open source models like Llama2 or Falcon on your internal computers/servers.",
|
||||||
"build": "",
|
"files": [
|
||||||
|
"build/**"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||||
"test:e2e": "playwright test --workers=1",
|
"test:e2e": "playwright test --workers=1",
|
||||||
"dev": "nodemon .",
|
"dev": "tsc --watch & node --watch build/main.js",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cors": "^8.4.2",
|
||||||
|
"@fastify/static": "^6.12.0",
|
||||||
|
"@janhq/core": "link:./core",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"fastify": "^4.24.3",
|
||||||
|
"request": "^2.88.2",
|
||||||
|
"request-progress": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/body-parser": "^1.19.5",
|
"@types/body-parser": "^1.19.5",
|
||||||
"@types/npmcli__arborist": "^5.6.4",
|
"@types/npmcli__arborist": "^5.6.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||||
"@typescript-eslint/parser": "^6.7.3",
|
"@typescript-eslint/parser": "^6.7.3",
|
||||||
"dotenv": "^16.3.1",
|
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"fastify": "^4.24.3",
|
"run-script-os": "^1.1.6",
|
||||||
"nodemon": "^3.0.1",
|
"typescript": "^5.2.2"
|
||||||
"run-script-os": "^1.1.6"
|
|
||||||
},
|
|
||||||
"installConfig": {
|
|
||||||
"hoistingLimits": "workspaces"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,9 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"paths": { "*": ["node_modules/*"] },
|
"paths": { "*": ["node_modules/*"] },
|
||||||
"typeRoots": ["node_modules/@types"]
|
"typeRoots": ["node_modules/@types"],
|
||||||
|
"ignoreDeprecations": "5.0",
|
||||||
|
"declaration": true
|
||||||
},
|
},
|
||||||
// "sourceMap": true,
|
// "sourceMap": true,
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
|
||||||
|
|
||||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
|
||||||
//TODO: Add controllers for assistants here
|
|
||||||
// app.get("/", controller)
|
|
||||||
// app.post("/", controller)
|
|
||||||
}
|
|
||||||
export default router;
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
|
||||||
|
|
||||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
|
||||||
//TODO: Add controllers for here
|
|
||||||
// app.get("/", controller)
|
|
||||||
|
|
||||||
app.post("/", (req, res) => {
|
|
||||||
req.body
|
|
||||||
})
|
|
||||||
}
|
|
||||||
export default router;
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import assistantsAPI from './assistants'
|
|
||||||
import chatCompletionAPI from './chat'
|
|
||||||
import modelsAPI from './models'
|
|
||||||
import threadsAPI from './threads'
|
|
||||||
|
|
||||||
import { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
|
||||||
|
|
||||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts) => {
|
|
||||||
app.register(
|
|
||||||
assistantsAPI,
|
|
||||||
{
|
|
||||||
prefix: "/assistants"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
app.register(
|
|
||||||
chatCompletionAPI,
|
|
||||||
{
|
|
||||||
prefix: "/chat/completion"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
app.register(
|
|
||||||
modelsAPI,
|
|
||||||
{
|
|
||||||
prefix: "/models"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
app.register(
|
|
||||||
threadsAPI,
|
|
||||||
{
|
|
||||||
prefix: "/threads"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default router;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { RouteHandlerMethod, FastifyRequest, FastifyReply } from 'fastify'
|
|
||||||
import { MODEL_FOLDER_PATH } from "./index"
|
|
||||||
import fs from 'fs/promises'
|
|
||||||
|
|
||||||
const controller: RouteHandlerMethod = async (req: FastifyRequest, res: FastifyReply) => {
|
|
||||||
//TODO: download models impl
|
|
||||||
//Mirror logic from JanModelExtension.downloadModel?
|
|
||||||
let model = req.body.model;
|
|
||||||
|
|
||||||
// Fetching logic
|
|
||||||
// const directoryPath = join(MODEL_FOLDER_PATH, model.id)
|
|
||||||
// await fs.mkdir(directoryPath)
|
|
||||||
|
|
||||||
// const path = join(directoryPath, model.id)
|
|
||||||
// downloadFile(model.source_url, path)
|
|
||||||
// TODO: Different model downloader from different model vendor
|
|
||||||
|
|
||||||
res.status(200).send({
|
|
||||||
status: "Ok"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default controller;
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
|
|
||||||
export const MODEL_FOLDER_PATH = "./data/models"
|
|
||||||
export const _modelMetadataFileName = 'model.json'
|
|
||||||
|
|
||||||
import fs from 'fs/promises'
|
|
||||||
import { Model } from '@janhq/core'
|
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
// map string => model object
|
|
||||||
let modelIndex = new Map<String, Model>();
|
|
||||||
async function buildModelIndex(){
|
|
||||||
let modelIds = await fs.readdir(MODEL_FOLDER_PATH);
|
|
||||||
// TODO: read modelFolders to get model info, mirror JanModelExtension?
|
|
||||||
try{
|
|
||||||
for(let modelId in modelIds){
|
|
||||||
let path = join(MODEL_FOLDER_PATH, modelId)
|
|
||||||
let fileData = await fs.readFile(join(path, _modelMetadataFileName))
|
|
||||||
modelIndex.set(modelId, JSON.parse(fileData.toString("utf-8")) as Model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(err){
|
|
||||||
console.error("build model index failed. ", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildModelIndex()
|
|
||||||
|
|
||||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
|
||||||
import downloadModelController from './downloadModel'
|
|
||||||
import { startModel, stopModel } from './modelOp'
|
|
||||||
|
|
||||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
|
||||||
//TODO: Add controllers declaration here
|
|
||||||
|
|
||||||
///////////// CRUD ////////////////
|
|
||||||
// Model listing
|
|
||||||
app.get("/", async (req, res) => {
|
|
||||||
res.status(200).send(
|
|
||||||
modelIndex.values()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Retrieve model info
|
|
||||||
app.get("/:id", (req, res) => {
|
|
||||||
res.status(200).send(
|
|
||||||
modelIndex.get(req.params.id)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Delete model
|
|
||||||
app.delete("/:id", (req, res) => {
|
|
||||||
modelIndex.delete(req.params)
|
|
||||||
|
|
||||||
// TODO: delete on disk
|
|
||||||
})
|
|
||||||
|
|
||||||
///////////// Other ops ////////////////
|
|
||||||
app.post("/", downloadModelController)
|
|
||||||
app.put("/start", startModel)
|
|
||||||
app.put("/stop", stopModel)
|
|
||||||
}
|
|
||||||
export default router;
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import {FastifyRequest, FastifyReply} from 'fastify'
|
|
||||||
|
|
||||||
export async function startModel(req: FastifyRequest, res: FastifyReply): Promise<void> {
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function stopModel(req: FastifyRequest, res: FastifyReply): Promise<void> {
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
|
||||||
|
|
||||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
|
||||||
//TODO: Add controllers declaration here
|
|
||||||
|
|
||||||
// app.get()
|
|
||||||
}
|
|
||||||
export default router;
|
|
||||||
@ -29,6 +29,13 @@ export default function CardSidebar({
|
|||||||
|
|
||||||
useClickOutside(() => setMore(false), null, [menu, toggle])
|
useClickOutside(() => setMore(false), null, [menu, toggle])
|
||||||
|
|
||||||
|
let openFolderTitle: string = 'Open Containing Folder'
|
||||||
|
if (isMac) {
|
||||||
|
openFolderTitle = 'Reveal in Finder'
|
||||||
|
} else if (isWindows) {
|
||||||
|
openFolderTitle = 'Reveal in File Explorer'
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@ -74,7 +81,7 @@ export default function CardSidebar({
|
|||||||
>
|
>
|
||||||
<FolderOpenIcon size={16} className="text-muted-foreground" />
|
<FolderOpenIcon size={16} className="text-muted-foreground" />
|
||||||
<span className="text-bold text-black dark:text-muted-foreground">
|
<span className="text-bold text-black dark:text-muted-foreground">
|
||||||
Reveal in Finder
|
{openFolderTitle}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
|
|
||||||
import { ExtensionType } from '@janhq/core'
|
|
||||||
import { ModelExtension } from '@janhq/core'
|
|
||||||
import {
|
import {
|
||||||
Progress,
|
Progress,
|
||||||
Modal,
|
Modal,
|
||||||
@ -12,14 +10,19 @@ import {
|
|||||||
ModalTrigger,
|
ModalTrigger,
|
||||||
} from '@janhq/uikit'
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||||
|
|
||||||
import { formatDownloadPercentage } from '@/utils/converter'
|
import { formatDownloadPercentage } from '@/utils/converter'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension'
|
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
export default function DownloadingState() {
|
export default function DownloadingState() {
|
||||||
const { downloadStates } = useDownloadState()
|
const { downloadStates } = useDownloadState()
|
||||||
|
const downloadingModels = useAtomValue(downloadingModelsAtom)
|
||||||
|
const { abortModelDownload } = useDownloadModel()
|
||||||
|
|
||||||
const totalCurrentProgress = downloadStates
|
const totalCurrentProgress = downloadStates
|
||||||
.map((a) => a.size.transferred + a.size.transferred)
|
.map((a) => a.size.transferred + a.size.transferred)
|
||||||
@ -73,9 +76,10 @@ export default function DownloadingState() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item?.modelId) {
|
if (item?.modelId) {
|
||||||
extensionManager
|
const model = downloadingModels.find(
|
||||||
.get<ModelExtension>(ExtensionType.Model)
|
(model) => model.id === item.modelId
|
||||||
?.cancelModelDownload(item.modelId)
|
)
|
||||||
|
if (model) abortModelDownload(model)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import { ModelExtension, ExtensionType } from '@janhq/core'
|
|
||||||
import { Model } from '@janhq/core'
|
import { Model } from '@janhq/core'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -17,11 +16,12 @@ import {
|
|||||||
|
|
||||||
import { atom, useAtomValue } from 'jotai'
|
import { atom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||||
|
|
||||||
import { formatDownloadPercentage } from '@/utils/converter'
|
import { formatDownloadPercentage } from '@/utils/converter'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension'
|
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
model: Model
|
model: Model
|
||||||
@ -30,6 +30,7 @@ type Props = {
|
|||||||
|
|
||||||
export default function ModalCancelDownload({ model, isFromList }: Props) {
|
export default function ModalCancelDownload({ model, isFromList }: Props) {
|
||||||
const { modelDownloadStateAtom } = useDownloadState()
|
const { modelDownloadStateAtom } = useDownloadState()
|
||||||
|
const downloadingModels = useAtomValue(downloadingModelsAtom)
|
||||||
const downloadAtom = useMemo(
|
const downloadAtom = useMemo(
|
||||||
() => atom((get) => get(modelDownloadStateAtom)[model.id]),
|
() => atom((get) => get(modelDownloadStateAtom)[model.id]),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -37,6 +38,7 @@ export default function ModalCancelDownload({ model, isFromList }: Props) {
|
|||||||
)
|
)
|
||||||
const downloadState = useAtomValue(downloadAtom)
|
const downloadState = useAtomValue(downloadAtom)
|
||||||
const cancelText = `Cancel ${formatDownloadPercentage(downloadState.percent)}`
|
const cancelText = `Cancel ${formatDownloadPercentage(downloadState.percent)}`
|
||||||
|
const { abortModelDownload } = useDownloadModel()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal>
|
<Modal>
|
||||||
@ -80,9 +82,10 @@ export default function ModalCancelDownload({ model, isFromList }: Props) {
|
|||||||
themes="danger"
|
themes="danger"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (downloadState?.modelId) {
|
if (downloadState?.modelId) {
|
||||||
extensionManager
|
const model = downloadingModels.find(
|
||||||
.get<ModelExtension>(ExtensionType.Model)
|
(model) => model.id === downloadState.modelId
|
||||||
?.cancelModelDownload(downloadState.modelId)
|
)
|
||||||
|
if (model) abortModelDownload(model)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,34 +1,35 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import { PropsWithChildren, useEffect, useRef } from 'react'
|
import { basename } from 'path'
|
||||||
|
|
||||||
import { ExtensionType } from '@janhq/core'
|
import { PropsWithChildren, useEffect, useRef } from 'react'
|
||||||
import { ModelExtension } from '@janhq/core'
|
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||||
|
|
||||||
|
import { modelBinFileName } from '@/utils/model'
|
||||||
|
|
||||||
import EventHandler from './EventHandler'
|
import EventHandler from './EventHandler'
|
||||||
|
|
||||||
import { appDownloadProgress } from './Jotai'
|
import { appDownloadProgress } from './Jotai'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension/ExtensionManager'
|
|
||||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
||||||
const setProgress = useSetAtom(appDownloadProgress)
|
const setProgress = useSetAtom(appDownloadProgress)
|
||||||
const models = useAtomValue(downloadingModelsAtom)
|
const models = useAtomValue(downloadingModelsAtom)
|
||||||
const modelsRef = useRef(models)
|
const modelsRef = useRef(models)
|
||||||
useEffect(() => {
|
|
||||||
modelsRef.current = models
|
|
||||||
}, [models])
|
|
||||||
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
||||||
const { setDownloadState, setDownloadStateSuccess, setDownloadStateFailed } =
|
const { setDownloadState, setDownloadStateSuccess, setDownloadStateFailed } =
|
||||||
useDownloadState()
|
useDownloadState()
|
||||||
const downloadedModelRef = useRef(downloadedModels)
|
const downloadedModelRef = useRef(downloadedModels)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
modelsRef.current = models
|
||||||
|
}, [models])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
downloadedModelRef.current = downloadedModels
|
downloadedModelRef.current = downloadedModels
|
||||||
}, [downloadedModels])
|
}, [downloadedModels])
|
||||||
@ -38,40 +39,36 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
|||||||
window.electronAPI.onFileDownloadUpdate(
|
window.electronAPI.onFileDownloadUpdate(
|
||||||
(_event: string, state: any | undefined) => {
|
(_event: string, state: any | undefined) => {
|
||||||
if (!state) return
|
if (!state) return
|
||||||
|
const model = modelsRef.current.find(
|
||||||
|
(model) => modelBinFileName(model) === basename(state.fileName)
|
||||||
|
)
|
||||||
|
if (model)
|
||||||
setDownloadState({
|
setDownloadState({
|
||||||
...state,
|
...state,
|
||||||
modelId: state.fileName.split('/').pop() ?? '',
|
modelId: model.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
window.electronAPI.onFileDownloadError(
|
window.electronAPI.onFileDownloadError((_event: string, state: any) => {
|
||||||
(_event: string, callback: any) => {
|
console.error('Download error', state)
|
||||||
console.error('Download error', callback)
|
const model = modelsRef.current.find(
|
||||||
const modelId = callback.fileName.split('/').pop() ?? ''
|
(model) => modelBinFileName(model) === basename(state.fileName)
|
||||||
setDownloadStateFailed(modelId)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
if (model) setDownloadStateFailed(model.id)
|
||||||
|
})
|
||||||
|
|
||||||
window.electronAPI.onFileDownloadSuccess(
|
window.electronAPI.onFileDownloadSuccess((_event: string, state: any) => {
|
||||||
(_event: string, callback: any) => {
|
if (state && state.fileName) {
|
||||||
if (callback && callback.fileName) {
|
const model = modelsRef.current.find(
|
||||||
const modelId = callback.fileName.split('/').pop() ?? ''
|
(model) => modelBinFileName(model) === basename(state.fileName)
|
||||||
|
)
|
||||||
const model = modelsRef.current.find((e) => e.id === modelId)
|
if (model) {
|
||||||
|
setDownloadStateSuccess(model.id)
|
||||||
setDownloadStateSuccess(modelId)
|
|
||||||
|
|
||||||
if (model)
|
|
||||||
extensionManager
|
|
||||||
.get<ModelExtension>(ExtensionType.Model)
|
|
||||||
?.saveModel(model)
|
|
||||||
.then(() => {
|
|
||||||
setDownloadedModels([...downloadedModelRef.current, model])
|
setDownloadedModels([...downloadedModelRef.current, model])
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
window.electronAPI.onAppUpdateDownloadUpdate(
|
window.electronAPI.onAppUpdateDownloadUpdate(
|
||||||
(_event: string, progress: any) => {
|
(_event: string, progress: any) => {
|
||||||
|
|||||||
@ -81,7 +81,10 @@ export class ExtensionManager {
|
|||||||
*/
|
*/
|
||||||
async activateExtension(extension: Extension) {
|
async activateExtension(extension: Extension) {
|
||||||
// Import class
|
// Import class
|
||||||
await import(/* webpackIgnore: true */ extension.url).then(
|
const extensionUrl = window.electronAPI
|
||||||
|
? extension.url
|
||||||
|
: extension.url.replace('extension://', `${API_BASE_URL}/extensions/`)
|
||||||
|
await import(/* webpackIgnore: true */ extensionUrl).then(
|
||||||
(extensionClass) => {
|
(extensionClass) => {
|
||||||
// Register class if it has a default export
|
// Register class if it has a default export
|
||||||
if (
|
if (
|
||||||
|
|||||||
@ -1,7 +1,15 @@
|
|||||||
import { Model, ExtensionType, ModelExtension } from '@janhq/core'
|
import {
|
||||||
|
Model,
|
||||||
|
ExtensionType,
|
||||||
|
ModelExtension,
|
||||||
|
abortDownload,
|
||||||
|
joinPath,
|
||||||
|
} from '@janhq/core'
|
||||||
|
|
||||||
import { useSetAtom } from 'jotai'
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { modelBinFileName } from '@/utils/model'
|
||||||
|
|
||||||
import { useDownloadState } from './useDownloadState'
|
import { useDownloadState } from './useDownloadState'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension/ExtensionManager'
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
@ -33,8 +41,14 @@ export default function useDownloadModel() {
|
|||||||
.get<ModelExtension>(ExtensionType.Model)
|
.get<ModelExtension>(ExtensionType.Model)
|
||||||
?.downloadModel(model)
|
?.downloadModel(model)
|
||||||
}
|
}
|
||||||
|
const abortModelDownload = async (model: Model) => {
|
||||||
|
await abortDownload(
|
||||||
|
await joinPath(['models', model.id, modelBinFileName(model)])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
downloadModel,
|
downloadModel,
|
||||||
|
abortModelDownload,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import { join } from 'path'
|
import { fs, joinPath } from '@janhq/core'
|
||||||
|
|
||||||
import { fs } from '@janhq/core'
|
|
||||||
|
|
||||||
export const useEngineSettings = () => {
|
export const useEngineSettings = () => {
|
||||||
const readOpenAISettings = async () => {
|
const readOpenAISettings = async () => {
|
||||||
const settings = await fs.readFile(join('engines', 'openai.json'))
|
if (!fs.existsSync(await joinPath(['file://engines', 'openai.json'])))
|
||||||
|
return {}
|
||||||
|
const settings = await fs.readFileSync(
|
||||||
|
await joinPath(['file://engines', 'openai.json']),
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
if (settings) {
|
if (settings) {
|
||||||
return JSON.parse(settings)
|
return typeof settings === 'object' ? settings : JSON.parse(settings)
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
@ -17,7 +20,10 @@ export const useEngineSettings = () => {
|
|||||||
}) => {
|
}) => {
|
||||||
const settings = await readOpenAISettings()
|
const settings = await readOpenAISettings()
|
||||||
settings.api_key = apiKey
|
settings.api_key = apiKey
|
||||||
await fs.writeFile(join('engines', 'openai.json'), JSON.stringify(settings))
|
await fs.writeFileSync(
|
||||||
|
await joinPath(['file://engines', 'openai.json']),
|
||||||
|
JSON.stringify(settings)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return { readOpenAISettings, saveOpenAISettings }
|
return { readOpenAISettings, saveOpenAISettings }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import {
|
|||||||
events,
|
events,
|
||||||
Model,
|
Model,
|
||||||
ConversationalExtension,
|
ConversationalExtension,
|
||||||
ModelRuntimeParams,
|
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
@ -173,7 +172,7 @@ export default function useSendChatMessage() {
|
|||||||
updateThreadInitSuccess(activeThread.id)
|
updateThreadInitSuccess(activeThread.id)
|
||||||
updateThread(updatedThread)
|
updateThread(updatedThread)
|
||||||
|
|
||||||
extensionManager
|
await extensionManager
|
||||||
.get<ConversationalExtension>(ExtensionType.Conversational)
|
.get<ConversationalExtension>(ExtensionType.Conversational)
|
||||||
?.saveThread(updatedThread)
|
?.saveThread(updatedThread)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,10 @@ const nextConfig = {
|
|||||||
JSON.stringify(process.env.ANALYTICS_ID) ?? JSON.stringify('xxx'),
|
JSON.stringify(process.env.ANALYTICS_ID) ?? JSON.stringify('xxx'),
|
||||||
ANALYTICS_HOST:
|
ANALYTICS_HOST:
|
||||||
JSON.stringify(process.env.ANALYTICS_HOST) ?? JSON.stringify('xxx'),
|
JSON.stringify(process.env.ANALYTICS_HOST) ?? JSON.stringify('xxx'),
|
||||||
|
API_BASE_URL: JSON.stringify('http://localhost:1337'),
|
||||||
|
isMac: process.platform === 'darwin',
|
||||||
|
isWindows: process.platform === 'win32',
|
||||||
|
isLinux: process.platform === 'linux',
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
return config
|
return config
|
||||||
|
|||||||
@ -116,7 +116,7 @@ const ChatBody: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<ScrollToBottom className="flex h-full w-full flex-col">
|
<ScrollToBottom className="flex h-full w-full flex-col">
|
||||||
{messages.map((message, index) => (
|
{messages.map((message, index) => (
|
||||||
<>
|
<div key={message.id}>
|
||||||
<ChatItem {...message} key={message.id} />
|
<ChatItem {...message} key={message.id} />
|
||||||
|
|
||||||
{message.status === MessageStatus.Error &&
|
{message.status === MessageStatus.Error &&
|
||||||
@ -126,8 +126,8 @@ const ChatBody: React.FC = () => {
|
|||||||
className="mt-10 flex flex-col items-center"
|
className="mt-10 flex flex-col items-center"
|
||||||
>
|
>
|
||||||
<span className="mb-3 text-center text-sm font-medium text-gray-500">
|
<span className="mb-3 text-center text-sm font-medium text-gray-500">
|
||||||
Oops! The generation was interrupted. Let's
|
Oops! The generation was interrupted. Let's give it
|
||||||
give it another go!
|
another go!
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
className="w-min"
|
className="w-min"
|
||||||
@ -140,7 +140,7 @@ const ChatBody: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
))}
|
))}
|
||||||
</ScrollToBottom>
|
</ScrollToBottom>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { getUserSpace, openFileExplorer } from '@janhq/core'
|
import { getUserSpace, openFileExplorer, joinPath } from '@janhq/core'
|
||||||
|
|
||||||
import { Input, Textarea } from '@janhq/uikit'
|
import { Input, Textarea } from '@janhq/uikit'
|
||||||
|
|
||||||
@ -53,24 +51,24 @@ const Sidebar: React.FC = () => {
|
|||||||
let filePath = undefined
|
let filePath = undefined
|
||||||
const assistantId = activeThread.assistants[0]?.assistant_id
|
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case 'Engine':
|
||||||
case 'Thread':
|
case 'Thread':
|
||||||
filePath = join('threads', activeThread.id)
|
filePath = await joinPath(['threads', activeThread.id])
|
||||||
break
|
break
|
||||||
case 'Model':
|
case 'Model':
|
||||||
if (!selectedModel) return
|
if (!selectedModel) return
|
||||||
filePath = join('models', selectedModel.id)
|
filePath = await joinPath(['models', selectedModel.id])
|
||||||
break
|
break
|
||||||
case 'Assistant':
|
case 'Assistant':
|
||||||
if (!assistantId) return
|
if (!assistantId) return
|
||||||
filePath = join('assistants', assistantId)
|
filePath = await joinPath(['assistants', assistantId])
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filePath) return
|
if (!filePath) return
|
||||||
|
const fullPath = await joinPath([userSpace, filePath])
|
||||||
const fullPath = join(userSpace, filePath)
|
|
||||||
openFileExplorer(fullPath)
|
openFileExplorer(fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,24 +84,24 @@ const Sidebar: React.FC = () => {
|
|||||||
let filePath = undefined
|
let filePath = undefined
|
||||||
const assistantId = activeThread.assistants[0]?.assistant_id
|
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case 'Engine':
|
||||||
case 'Thread':
|
case 'Thread':
|
||||||
filePath = join('threads', activeThread.id, 'thread.json')
|
filePath = await joinPath(['threads', activeThread.id, 'thread.json'])
|
||||||
break
|
break
|
||||||
case 'Model':
|
case 'Model':
|
||||||
if (!selectedModel) return
|
if (!selectedModel) return
|
||||||
filePath = join('models', selectedModel.id, 'model.json')
|
filePath = await joinPath(['models', selectedModel.id, 'model.json'])
|
||||||
break
|
break
|
||||||
case 'Assistant':
|
case 'Assistant':
|
||||||
if (!assistantId) return
|
if (!assistantId) return
|
||||||
filePath = join('assistants', assistantId, 'assistant.json')
|
filePath = await joinPath(['assistants', assistantId, 'assistant.json'])
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filePath) return
|
if (!filePath) return
|
||||||
|
const fullPath = await joinPath([userSpace, filePath])
|
||||||
const fullPath = join(userSpace, filePath)
|
|
||||||
openFileExplorer(fullPath)
|
openFileExplorer(fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,6 @@ import React, { useState, useEffect, useRef, useContext } from 'react'
|
|||||||
|
|
||||||
import { Button } from '@janhq/uikit'
|
import { Button } from '@janhq/uikit'
|
||||||
|
|
||||||
import Loader from '@/containers/Loader'
|
|
||||||
|
|
||||||
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||||
|
|
||||||
import { useGetAppVersion } from '@/hooks/useGetAppVersion'
|
import { useGetAppVersion } from '@/hooks/useGetAppVersion'
|
||||||
@ -18,7 +16,6 @@ import { extensionManager } from '@/extension'
|
|||||||
const ExtensionCatalog = () => {
|
const ExtensionCatalog = () => {
|
||||||
const [activeExtensions, setActiveExtensions] = useState<any[]>([])
|
const [activeExtensions, setActiveExtensions] = useState<any[]>([])
|
||||||
const [extensionCatalog, setExtensionCatalog] = useState<any[]>([])
|
const [extensionCatalog, setExtensionCatalog] = useState<any[]>([])
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const { version } = useGetAppVersion()
|
const { version } = useGetAppVersion()
|
||||||
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||||
@ -95,8 +92,6 @@ const ExtensionCatalog = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) return <Loader description="Installing ..." />
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block w-full">
|
<div className="block w-full">
|
||||||
{extensionCatalog
|
{extensionCatalog
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export default function Models() {
|
|||||||
const [searchValue, setsearchValue] = useState('')
|
const [searchValue, setsearchValue] = useState('')
|
||||||
|
|
||||||
const filteredDownloadedModels = downloadedModels.filter((x) => {
|
const filteredDownloadedModels = downloadedModels.filter((x) => {
|
||||||
return x.name.toLowerCase().includes(searchValue.toLowerCase())
|
return x.name?.toLowerCase().includes(searchValue.toLowerCase())
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
const API_BASE_PATH: string = '/api/v1'
|
|
||||||
|
|
||||||
export function openExternalUrl(url: string) {
|
|
||||||
window?.open(url, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function appVersion() {
|
|
||||||
return Promise.resolve(VERSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function invokeExtensionFunc(
|
|
||||||
modulePath: string,
|
|
||||||
extensionFunc: string,
|
|
||||||
...args: any
|
|
||||||
): Promise<any> {
|
|
||||||
return fetchApi(modulePath, extensionFunc, args).catch((err: Error) => {
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function downloadFile(downloadUrl: string, fileName: string) {
|
|
||||||
return fetchApi('', 'downloadFile', {
|
|
||||||
downloadUrl: downloadUrl,
|
|
||||||
fileName: fileName,
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteFile(fileName: string) {
|
|
||||||
return fetchApi('', 'deleteFile', fileName).catch((err: Error) => {
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchApi(
|
|
||||||
modulePath: string,
|
|
||||||
extensionFunc: string,
|
|
||||||
args: any
|
|
||||||
): Promise<any> {
|
|
||||||
const response = await fetch(API_BASE_PATH + '/invokeFunction', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
modulePath: modulePath,
|
|
||||||
method: extensionFunc,
|
|
||||||
args: args,
|
|
||||||
}),
|
|
||||||
headers: { contentType: 'application/json', Authorization: '' },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const json = await response.json()
|
|
||||||
if (json && json.error) {
|
|
||||||
toast.error(json.error, {
|
|
||||||
position: 'bottom-left',
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: 'light',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const text = await response.text()
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(text)
|
|
||||||
return Promise.resolve(json)
|
|
||||||
} catch (err) {
|
|
||||||
return Promise.resolve(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import * as restAPI from './cloudNativeService'
|
|
||||||
import { EventEmitter } from './eventsService'
|
import { EventEmitter } from './eventsService'
|
||||||
|
import { restAPI } from './restService'
|
||||||
export const setupCoreServices = () => {
|
export const setupCoreServices = () => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
console.debug('undefine', window)
|
console.debug('undefine', window)
|
||||||
@ -10,9 +10,7 @@ export const setupCoreServices = () => {
|
|||||||
if (!window.core) {
|
if (!window.core) {
|
||||||
window.core = {
|
window.core = {
|
||||||
events: new EventEmitter(),
|
events: new EventEmitter(),
|
||||||
api: window.electronAPI ?? {
|
api: window.electronAPI ?? restAPI,
|
||||||
...restAPI,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,13 +15,10 @@ export const isCoreExtensionInstalled = () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
export const setupBaseExtensions = async () => {
|
export const setupBaseExtensions = async () => {
|
||||||
if (
|
if (typeof window === 'undefined') {
|
||||||
typeof window === 'undefined' ||
|
|
||||||
typeof window.electronAPI === 'undefined'
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const baseExtensions = await window.electronAPI.baseExtensions()
|
const baseExtensions = await window.core?.api.baseExtensions()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!extensionManager.get(ExtensionType.Conversational) ||
|
!extensionManager.get(ExtensionType.Conversational) ||
|
||||||
|
|||||||
59
web/services/restService.ts
Normal file
59
web/services/restService.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import {
|
||||||
|
AppRoute,
|
||||||
|
DownloadRoute,
|
||||||
|
ExtensionRoute,
|
||||||
|
FileSystemRoute,
|
||||||
|
} from '@janhq/core'
|
||||||
|
|
||||||
|
import { safeJsonParse } from '@/utils/json'
|
||||||
|
|
||||||
|
// Function to open an external URL in a new browser window
|
||||||
|
export function openExternalUrl(url: string) {
|
||||||
|
window?.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async function to get the application version
|
||||||
|
export async function appVersion() {
|
||||||
|
return Promise.resolve(VERSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define API routes based on different route types
|
||||||
|
export const APIRoutes = [
|
||||||
|
...Object.values(AppRoute).map((r) => ({ path: 'app', route: r })),
|
||||||
|
...Object.values(DownloadRoute).map((r) => ({ path: `download`, route: r })),
|
||||||
|
...Object.values(ExtensionRoute).map((r) => ({
|
||||||
|
path: `extension`,
|
||||||
|
route: r,
|
||||||
|
})),
|
||||||
|
...Object.values(FileSystemRoute).map((r) => ({ path: `fs`, route: r })),
|
||||||
|
]
|
||||||
|
|
||||||
|
// Define the restAPI object with methods for each API route
|
||||||
|
export const restAPI = {
|
||||||
|
...Object.values(APIRoutes).reduce((acc, proxy) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[proxy.route]: (...args: any) => {
|
||||||
|
// For each route, define a function that sends a request to the API
|
||||||
|
return fetch(`${API_BASE_URL}/v1/${proxy.path}/${proxy.route}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(args),
|
||||||
|
headers: { contentType: 'application/json' },
|
||||||
|
}).then(async (res) => {
|
||||||
|
try {
|
||||||
|
if (proxy.path === 'fs') {
|
||||||
|
const text = await res.text()
|
||||||
|
return safeJsonParse(text) ?? text
|
||||||
|
}
|
||||||
|
return await res.json()
|
||||||
|
} catch (err) {
|
||||||
|
console.debug('Op: ', proxy, args, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, {}),
|
||||||
|
openExternalUrl,
|
||||||
|
appVersion,
|
||||||
|
}
|
||||||
4
web/types/index.d.ts
vendored
4
web/types/index.d.ts
vendored
@ -8,6 +8,10 @@ declare global {
|
|||||||
declare const VERSION: string
|
declare const VERSION: string
|
||||||
declare const ANALYTICS_ID: string
|
declare const ANALYTICS_ID: string
|
||||||
declare const ANALYTICS_HOST: string
|
declare const ANALYTICS_HOST: string
|
||||||
|
declare const API_BASE_URL: string
|
||||||
|
declare const isMac: boolean
|
||||||
|
declare const isWindows: boolean
|
||||||
|
declare const isLinux: boolean
|
||||||
interface Core {
|
interface Core {
|
||||||
api: APIFunctions
|
api: APIFunctions
|
||||||
events: EventEmitter
|
events: EventEmitter
|
||||||
|
|||||||
9
web/utils/json.ts
Normal file
9
web/utils/json.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export const safeJsonParse = <T>(str: string) => {
|
||||||
|
try {
|
||||||
|
const jsonValue: T = JSON.parse(str)
|
||||||
|
|
||||||
|
return jsonValue
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
12
web/utils/model.ts
Normal file
12
web/utils/model.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { basename } from 'path'
|
||||||
|
|
||||||
|
import { Model } from '@janhq/core'
|
||||||
|
|
||||||
|
export const modelBinFileName = (model: Model) => {
|
||||||
|
const modelFormatExt = '.gguf'
|
||||||
|
const extractedFileName = basename(model.source_url) ?? model.id
|
||||||
|
const fileName = extractedFileName.toLowerCase().endsWith(modelFormatExt)
|
||||||
|
? extractedFileName
|
||||||
|
: model.id
|
||||||
|
return fileName
|
||||||
|
}
|
||||||
@ -22,8 +22,7 @@ export const toRuntimeParams = (
|
|||||||
|
|
||||||
for (const [key, value] of Object.entries(modelParams)) {
|
for (const [key, value] of Object.entries(modelParams)) {
|
||||||
if (key in defaultModelParams) {
|
if (key in defaultModelParams) {
|
||||||
// @ts-ignore
|
runtimeParams[key as keyof ModelRuntimeParams] = value
|
||||||
runtimeParams[key] = value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,8 +45,7 @@ export const toSettingParams = (
|
|||||||
|
|
||||||
for (const [key, value] of Object.entries(modelParams)) {
|
for (const [key, value] of Object.entries(modelParams)) {
|
||||||
if (key in defaultSettingParams) {
|
if (key in defaultSettingParams) {
|
||||||
// @ts-ignore
|
settingParams[key as keyof ModelSettingParams] = value
|
||||||
settingParams[key] = value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user