feat: Jan Server, API and decoupled clients (#948)
* chore: expose fs apis * chore: correct electron import path * update download api Signed-off-by: James <james@jan.ai> * update chat_completion Signed-off-by: James <james@jan.ai> * fix electron import Signed-off-by: James <james@jan.ai> * feat: adding API support at 1337 (#991) Signed-off-by: James <james@jan.ai> Co-authored-by: James <james@jan.ai> * feat: Add /chat/completion api and handler * chore: add todo for modelList * chore: read engine.json for openai chat_completion (#1030) Signed-off-by: James <james@jan.ai> Co-authored-by: James <james@jan.ai> * refactor: move routes to shared node module * refactor: exported modules from core with types (#1172) * refactor: exported modules from core with types * fix: fix file reading args * refactor: fileManager handles * fix: app issues with server refactoring * refactor: shared server module (#1210) * chore: resolve main * chore: update makefile --------- Signed-off-by: James <james@jan.ai> Co-authored-by: James <james@jan.ai> Co-authored-by: NamH <NamNh0122@gmail.com> Co-authored-by: hiro <vuonghoainam.work@gmail.com>
This commit is contained in:
parent
cfbc5674fe
commit
5250061c11
1
Makefile
1
Makefile
@ -18,6 +18,7 @@ ifeq ($(OS),Windows_NT)
|
||||
yarn config set network-timeout 300000
|
||||
endif
|
||||
yarn build:core
|
||||
yarn build:server
|
||||
yarn install
|
||||
yarn build:extensions
|
||||
|
||||
|
||||
@ -22,6 +22,27 @@
|
||||
"engines": {
|
||||
"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": {
|
||||
"lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
|
||||
"prebuild": "rimraf dist",
|
||||
|
||||
@ -8,30 +8,69 @@ const pkg = require('./package.json')
|
||||
|
||||
const libraryName = 'core'
|
||||
|
||||
export default {
|
||||
input: `src/index.ts`,
|
||||
output: [
|
||||
{ file: pkg.main, name: libraryName, format: 'umd', sourcemap: true },
|
||||
{ file: pkg.module, format: 'es', sourcemap: true },
|
||||
],
|
||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||
external: [],
|
||||
watch: {
|
||||
include: 'src/**',
|
||||
},
|
||||
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(),
|
||||
export default [
|
||||
{
|
||||
input: `src/index.ts`,
|
||||
output: [
|
||||
{ file: pkg.main, name: libraryName, format: 'umd', sourcemap: true },
|
||||
{ file: pkg.module, format: 'es', sourcemap: true },
|
||||
],
|
||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||
external: ['path'],
|
||||
watch: {
|
||||
include: 'src/**',
|
||||
},
|
||||
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(),
|
||||
],
|
||||
}
|
||||
// Resolve source maps to the original source
|
||||
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,7 +5,6 @@
|
||||
export enum AppRoute {
|
||||
appDataPath = 'appDataPath',
|
||||
appVersion = 'appVersion',
|
||||
getResourcePath = 'getResourcePath',
|
||||
openExternalUrl = 'openExternalUrl',
|
||||
openAppDirectory = 'openAppDirectory',
|
||||
openFileExplore = 'openFileExplorer',
|
||||
@ -41,20 +40,20 @@ export enum ExtensionRoute {
|
||||
uninstallExtension = 'uninstallExtension',
|
||||
}
|
||||
export enum FileSystemRoute {
|
||||
appendFile = 'appendFile',
|
||||
copyFile = 'copyFile',
|
||||
syncFile = 'syncFile',
|
||||
deleteFile = 'deleteFile',
|
||||
exists = 'exists',
|
||||
getResourcePath = 'getResourcePath',
|
||||
appendFileSync = 'appendFileSync',
|
||||
copyFileSync = 'copyFileSync',
|
||||
unlinkSync = 'unlinkSync',
|
||||
existsSync = 'existsSync',
|
||||
readdirSync = 'readdirSync',
|
||||
mkdirSync = 'mkdirSync',
|
||||
readFileSync = 'readFileSync',
|
||||
rmdirSync = 'rmdirSync',
|
||||
writeFileSync = 'writeFileSync',
|
||||
}
|
||||
export enum FileManagerRoute {
|
||||
synceFile = 'syncFile',
|
||||
getUserSpace = 'getUserSpace',
|
||||
isDirectory = 'isDirectory',
|
||||
listFiles = 'listFiles',
|
||||
mkdir = 'mkdir',
|
||||
readFile = 'readFile',
|
||||
readLineByLine = 'readLineByLine',
|
||||
rmdir = 'rmdir',
|
||||
writeFile = 'writeFile',
|
||||
getResourcePath = 'getResourcePath',
|
||||
}
|
||||
|
||||
export type ApiFunction = (...args: any[]) => any
|
||||
@ -83,17 +82,23 @@ export type FileSystemRouteFunctions = {
|
||||
[K in FileSystemRoute]: ApiFunction
|
||||
}
|
||||
|
||||
export type FileManagerRouteFunctions = {
|
||||
[K in FileManagerRoute]: ApiFunction
|
||||
}
|
||||
|
||||
export type APIFunctions = AppRouteFunctions &
|
||||
AppEventFunctions &
|
||||
DownloadRouteFunctions &
|
||||
DownloadEventFunctions &
|
||||
ExtensionRouteFunctions &
|
||||
FileSystemRouteFunctions
|
||||
FileSystemRouteFunctions &
|
||||
FileManagerRoute
|
||||
|
||||
export const APIRoutes = [
|
||||
...Object.values(AppRoute),
|
||||
...Object.values(DownloadRoute),
|
||||
...Object.values(ExtensionRoute),
|
||||
...Object.values(FileSystemRoute),
|
||||
...Object.values(FileManagerRoute),
|
||||
]
|
||||
export const APIEvents = [...Object.values(AppEvent), ...Object.values(DownloadEvent)]
|
||||
|
||||
@ -1,89 +1,74 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
||||
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)
|
||||
const writeFileSync = (...args: any[]) => global.core.api?.writeFileSync(...args)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
* @param {string} path
|
||||
* @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
|
||||
* @param {string} path - The path of the directory to list files.
|
||||
* @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.
|
||||
* @param {string} path - The path of the directory to create.
|
||||
* @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.
|
||||
* @param {string} path - The path of the directory to remove.
|
||||
* @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.
|
||||
* @param {string} path - The path of the file to delete.
|
||||
* @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.
|
||||
* @param path path to the file
|
||||
* @param data data to append
|
||||
*/
|
||||
const appendFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
||||
global.core.api?.appendFile(path, data)
|
||||
|
||||
const copyFile: (src: string, dest: string) => Promise<any> = (src, dest) =>
|
||||
global.core.api?.copyFile(src, dest)
|
||||
const appendFileSync = (...args: any[]) => global.core.api?.appendFileSync(...args)
|
||||
|
||||
/**
|
||||
* 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) =>
|
||||
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 = {
|
||||
isDirectory,
|
||||
writeFile,
|
||||
readFile,
|
||||
exists,
|
||||
listFiles,
|
||||
mkdir,
|
||||
rmdir,
|
||||
deleteFile,
|
||||
appendFile,
|
||||
readLineByLine,
|
||||
copyFile,
|
||||
writeFileSync,
|
||||
readFileSync,
|
||||
existsSync,
|
||||
readdirSync,
|
||||
mkdirSync,
|
||||
rmdirSync,
|
||||
unlinkSync,
|
||||
appendFileSync,
|
||||
copyFileSync,
|
||||
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 { manifest, extract } from 'pacote'
|
||||
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.
|
||||
* 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 {Object} installOptions Options provided to pacote when fetching the manifest.
|
||||
@ -56,10 +56,7 @@ class Extension {
|
||||
* @type {string}
|
||||
*/
|
||||
get specifier() {
|
||||
return (
|
||||
this.origin +
|
||||
(this.installOptions.version ? '@' + this.installOptions.version : '')
|
||||
)
|
||||
return this.origin + (this.installOptions.version ? '@' + this.installOptions.version : '')
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,9 +82,7 @@ class Extension {
|
||||
this.main = mnf.main
|
||||
this.description = mnf.description
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Package ${this.origin} does not contain a valid manifest: ${error}`
|
||||
)
|
||||
throw new Error(`Package ${this.origin} does not contain a valid manifest: ${error}`)
|
||||
}
|
||||
|
||||
return true
|
||||
@ -107,7 +102,7 @@ class Extension {
|
||||
await extract(
|
||||
this.specifier,
|
||||
join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''),
|
||||
this.installOptions
|
||||
this.installOptions,
|
||||
)
|
||||
|
||||
// Set the url using the custom extensions protocol
|
||||
@ -180,11 +175,8 @@ class Extension {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async uninstall() {
|
||||
const extPath = resolve(
|
||||
ExtensionManager.instance.extensionsPath ?? '',
|
||||
this.name ?? ''
|
||||
)
|
||||
await rmdir(extPath, { recursive: true })
|
||||
const extPath = resolve(ExtensionManager.instance.extensionsPath ?? '', this.name ?? '')
|
||||
await rmdirSync(extPath, { recursive: true })
|
||||
|
||||
this.emitUpdate()
|
||||
}
|
||||
@ -200,5 +192,3 @@ class Extension {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export default Extension
|
||||
@ -1,5 +1,5 @@
|
||||
import { readFileSync } from 'fs'
|
||||
import { protocol } from 'electron'
|
||||
|
||||
import { normalize } from 'path'
|
||||
|
||||
import Extension from './extension'
|
||||
@ -12,18 +12,8 @@ import {
|
||||
getActiveExtensions,
|
||||
addExtension,
|
||||
} 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) {
|
||||
// Create extensions protocol to serve extensions to renderer
|
||||
registerExtensionProtocol()
|
||||
@ -41,13 +31,24 @@ export function init(options: any) {
|
||||
* @private
|
||||
* @returns {boolean} Whether the protocol registration was successful
|
||||
*/
|
||||
function registerExtensionProtocol() {
|
||||
return protocol.registerFileProtocol('extension', (request, callback) => {
|
||||
const entry = request.url.substr('extension://'.length - 1)
|
||||
async function registerExtensionProtocol() {
|
||||
let electron: any = undefined
|
||||
|
||||
const url = normalize(ExtensionManager.instance.extensionsPath + entry)
|
||||
callback({ path: url })
|
||||
})
|
||||
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 url = normalize(ExtensionManager.instance.extensionsPath + entry)
|
||||
callback({ path: url })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,8 +58,7 @@ function registerExtensionProtocol() {
|
||||
* @returns {extensionManager} A set of functions used to manage the extension lifecycle.
|
||||
*/
|
||||
export function useExtensions(extensionsPath: string) {
|
||||
if (!extensionsPath)
|
||||
throw Error('A path to the extensions folder is required to use extensions')
|
||||
if (!extensionsPath) throw Error('A path to the extensions folder is required to use extensions')
|
||||
// Store the path to the extensions folder
|
||||
ExtensionManager.instance.setExtensionsPath(extensionsPath)
|
||||
|
||||
@ -69,7 +69,7 @@ export function useExtensions(extensionsPath: string) {
|
||||
|
||||
// Read extension list from extensions folder
|
||||
const extensions = JSON.parse(
|
||||
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8')
|
||||
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8'),
|
||||
)
|
||||
try {
|
||||
// Create and store a Extension instance for each extension in list
|
||||
@ -82,7 +82,7 @@ export function useExtensions(extensionsPath: string) {
|
||||
throw new Error(
|
||||
'Could not successfully rebuild list of installed extensions.\n' +
|
||||
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)
|
||||
extension.subscribe('pe-persist', persistExtensions)
|
||||
}
|
||||
@ -123,7 +122,7 @@ function loadExtension(ext: any) {
|
||||
export function getStore() {
|
||||
if (!ExtensionManager.instance.extensionsPath) {
|
||||
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',
|
||||
)
|
||||
}
|
||||
|
||||
@ -134,4 +133,4 @@ export function getStore() {
|
||||
getActiveExtensions,
|
||||
removeExtension,
|
||||
}
|
||||
}
|
||||
}
|
||||
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 @@
|
||||
/**
|
||||
* Provides access to the extensions stored by Extension Store
|
||||
* @typedef {Object} extensionManager
|
||||
* @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'
|
||||
import { writeFileSync } from "fs";
|
||||
import Extension from "./extension";
|
||||
import { ExtensionManager } from "./manager";
|
||||
|
||||
/**
|
||||
* @module store
|
||||
@ -21,7 +11,7 @@ import { ExtensionManager } from './../managers/extension'
|
||||
* Register 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.
|
||||
@ -31,10 +21,10 @@ const extensions: Record<string, Extension> = {}
|
||||
*/
|
||||
export function getExtension(name: string) {
|
||||
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
|
||||
*/
|
||||
export function getAllExtensions() {
|
||||
return Object.values(extensions)
|
||||
return Object.values(extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,7 +42,7 @@ export function getAllExtensions() {
|
||||
* @alias extensionManager.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
|
||||
*/
|
||||
export function removeExtension(name: string, persist = true) {
|
||||
const del = delete extensions[name]
|
||||
if (persist) persistExtensions()
|
||||
return del
|
||||
const del = delete extensions[name];
|
||||
if (persist) persistExtensions();
|
||||
return del;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -75,10 +65,10 @@ export function removeExtension(name: string, persist = true) {
|
||||
* @returns {void}
|
||||
*/
|
||||
export function addExtension(extension: Extension, persist = true) {
|
||||
if (extension.name) extensions[extension.name] = extension
|
||||
if (extension.name) extensions[extension.name] = extension;
|
||||
if (persist) {
|
||||
persistExtensions()
|
||||
extension.subscribe('pe-persist', persistExtensions)
|
||||
persistExtensions();
|
||||
extension.subscribe("pe-persist", persistExtensions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,15 +77,15 @@ export function addExtension(extension: Extension, persist = true) {
|
||||
* @returns {void}
|
||||
*/
|
||||
export function persistExtensions() {
|
||||
const persistData: Record<string, Extension> = {}
|
||||
const persistData: Record<string, Extension> = {};
|
||||
for (const name in extensions) {
|
||||
persistData[name] = extensions[name]
|
||||
persistData[name] = extensions[name];
|
||||
}
|
||||
writeFileSync(
|
||||
ExtensionManager.instance.getExtensionsFile(),
|
||||
JSON.stringify(persistData),
|
||||
'utf8'
|
||||
)
|
||||
"utf8"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,25 +96,25 @@ export function persistExtensions() {
|
||||
* @alias extensionManager.installExtensions
|
||||
*/
|
||||
export async function installExtensions(extensions: any, store = true) {
|
||||
const installed: Extension[] = []
|
||||
const installed: Extension[] = [];
|
||||
for (const ext of extensions) {
|
||||
// Set install options and activation based on input type
|
||||
const isObject = typeof ext === 'object'
|
||||
const spec = isObject ? [ext.specifier, ext] : [ext]
|
||||
const activate = isObject ? ext.activate !== false : true
|
||||
const isObject = typeof ext === "object";
|
||||
const spec = isObject ? [ext.specifier, ext] : [ext];
|
||||
const activate = isObject ? ext.activate !== false : true;
|
||||
|
||||
// Install and possibly activate extension
|
||||
const extension = new Extension(...spec)
|
||||
await extension._install()
|
||||
if (activate) extension.setActive(true)
|
||||
const extension = new Extension(...spec);
|
||||
await extension._install();
|
||||
if (activate) extension.setActive(true);
|
||||
|
||||
// Add extension to store if needed
|
||||
if (store) addExtension(extension)
|
||||
installed.push(extension)
|
||||
if (store) addExtension(extension);
|
||||
installed.push(extension);
|
||||
}
|
||||
|
||||
// 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.
|
||||
*/
|
||||
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() {
|
||||
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.
|
||||
*/
|
||||
setModule(moduleName: string, nodule: any | undefined) {
|
||||
this.requiredModules[moduleName] = nodule;
|
||||
this.requiredModules[moduleName] = nodule
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all imported modules.
|
||||
*/
|
||||
clearImportedModules() {
|
||||
dispose(this.requiredModules);
|
||||
this.requiredModules = {};
|
||||
this.requiredModules = {}
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"target": "es5",
|
||||
"module": "es2015",
|
||||
"module": "ES2020",
|
||||
"lib": ["es2015", "es2016", "es2017", "dom"],
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { app, ipcMain, shell, nativeTheme } from 'electron'
|
||||
import { ModuleManager } from './../managers/module'
|
||||
import { join } from 'path'
|
||||
import { ExtensionManager } from './../managers/extension'
|
||||
import { WindowManager } from './../managers/window'
|
||||
import { userSpacePath } from './../utils/path'
|
||||
import { AppRoute } from '@janhq/core'
|
||||
import { getResourcePath } from './../utils/path'
|
||||
import {
|
||||
ExtensionManager,
|
||||
ModuleManager,
|
||||
} from '@janhq/core/node'
|
||||
|
||||
export function handleAppIPCs() {
|
||||
/**
|
||||
@ -26,10 +28,6 @@ export function handleAppIPCs() {
|
||||
shell.openPath(userSpacePath)
|
||||
})
|
||||
|
||||
ipcMain.handle(AppRoute.getResourcePath, async (_event) => {
|
||||
return getResourcePath()
|
||||
})
|
||||
|
||||
/**
|
||||
* Opens a URL in the user's default browser.
|
||||
* @param _event - The IPC event object.
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { app, ipcMain } from 'electron'
|
||||
import { DownloadManager } from './../managers/download'
|
||||
import { resolve, join } from 'path'
|
||||
import { WindowManager } from './../managers/window'
|
||||
import request from 'request'
|
||||
import { createWriteStream, renameSync } from 'fs'
|
||||
import { DownloadEvent, DownloadRoute } from '@janhq/core'
|
||||
const progress = require('request-progress')
|
||||
import { DownloadManager } from '@janhq/core/node'
|
||||
|
||||
export function handleDownloaderIPCs() {
|
||||
/**
|
||||
@ -46,6 +46,9 @@ export function handleDownloaderIPCs() {
|
||||
*/
|
||||
ipcMain.handle(DownloadRoute.downloadFile, async (_event, url, fileName) => {
|
||||
const userDataPath = join(app.getPath('home'), 'jan')
|
||||
if (typeof fileName === 'string' && fileName.includes('file:/')) {
|
||||
fileName = fileName.replace('file:/', '')
|
||||
}
|
||||
const destination = resolve(userDataPath, fileName)
|
||||
const rq = request(url)
|
||||
// downloading file to a temp file first
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { ipcMain, webContents } from 'electron'
|
||||
import { readdirSync } from 'fs'
|
||||
import { ModuleManager } from './../managers/module'
|
||||
import { join, extname } from 'path'
|
||||
|
||||
import {
|
||||
getActiveExtensions,
|
||||
getAllExtensions,
|
||||
installExtensions,
|
||||
} from './../extension/store'
|
||||
import { getExtension } from './../extension/store'
|
||||
import { removeExtension } from './../extension/store'
|
||||
import Extension from './../extension/extension'
|
||||
getExtension,
|
||||
removeExtension,
|
||||
getActiveExtensions,
|
||||
ModuleManager
|
||||
} from '@janhq/core/node'
|
||||
|
||||
import { getResourcePath, userSpacePath } from './../utils/path'
|
||||
import { ExtensionRoute } from '@janhq/core'
|
||||
|
||||
@ -81,7 +81,7 @@ export function handleExtensionIPCs() {
|
||||
ExtensionRoute.updateExtension,
|
||||
async (e, extensions, reload) => {
|
||||
// Update all provided extensions
|
||||
const updated: Extension[] = []
|
||||
const updated: any[] = []
|
||||
for (const ext of extensions) {
|
||||
const extension = getExtension(ext)
|
||||
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 * 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.
|
||||
*/
|
||||
export function handleFsIPCs() {
|
||||
/**
|
||||
* Gets the path to the user data directory.
|
||||
* @param event - The event object.
|
||||
* @returns A promise that resolves with the path to the user data directory.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.getUserSpace,
|
||||
(): Promise<string> => Promise.resolve(userSpacePath)
|
||||
)
|
||||
|
||||
/**
|
||||
* 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()
|
||||
const moduleName = 'fs'
|
||||
Object.values(FileSystemRoute).forEach((route) => {
|
||||
ipcMain.handle(route, async (event, ...args) => {
|
||||
return import(moduleName).then((mdl) =>
|
||||
mdl[route](
|
||||
...args.map((arg) =>
|
||||
typeof arg === 'string' && arg.includes('file:/')
|
||||
? join(userSpacePath, arg.replace('file:/', ''))
|
||||
: arg
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 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
|
||||
**/
|
||||
import { WindowManager } from './managers/window'
|
||||
import { ModuleManager } from './managers/module'
|
||||
import { ExtensionManager } from './managers/extension'
|
||||
import { ExtensionManager, ModuleManager } from '@janhq/core/node'
|
||||
|
||||
/**
|
||||
* IPC Handlers
|
||||
**/
|
||||
import { handleDownloaderIPCs } from './handlers/download'
|
||||
import { handleExtensionIPCs } from './handlers/extension'
|
||||
import { handleFileMangerIPCs } from './handlers/fileManager'
|
||||
import { handleAppIPCs } from './handlers/app'
|
||||
import { handleAppUpdates } from './handlers/update'
|
||||
import { handleFsIPCs } from './handlers/fs'
|
||||
import { migrateExtensions } from './utils/migration'
|
||||
|
||||
/**
|
||||
* Server
|
||||
*/
|
||||
import { startServer } from '@janhq/server'
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(createUserSpace)
|
||||
.then(ExtensionManager.instance.migrateExtensions)
|
||||
.then(migrateExtensions)
|
||||
.then(ExtensionManager.instance.setupExtensions)
|
||||
.then(setupMenu)
|
||||
.then(handleIPCs)
|
||||
.then(handleAppUpdates)
|
||||
.then(createMainWindow)
|
||||
.then(startServer)
|
||||
.then(() => {
|
||||
app.on('activate', () => {
|
||||
if (!BrowserWindow.getAllWindows().length) {
|
||||
@ -80,4 +87,5 @@ function handleIPCs() {
|
||||
handleDownloaderIPCs()
|
||||
handleExtensionIPCs()
|
||||
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": {
|
||||
"@alumna/reflect": "^1.1.3",
|
||||
"@janhq/core": "link:./core",
|
||||
"@janhq/server": "link:./server",
|
||||
"@npmcli/arborist": "^7.1.0",
|
||||
"@types/request": "^2.48.12",
|
||||
"@uiball/loaders": "^1.3.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"fs-extra": "^11.2.0",
|
||||
"node-fetch": "2",
|
||||
"pacote": "^17.0.4",
|
||||
"request": "^2.88.2",
|
||||
"request-progress": "^3.0.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"typescript": "^5.3.3",
|
||||
"ulid": "^2.3.0",
|
||||
"use-debounce": "^9.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -15,6 +15,9 @@
|
||||
"paths": { "*": ["node_modules/*"] },
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
},
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// @ts-nocheck
|
||||
const { app, Menu, dialog } = require("electron");
|
||||
import { app, Menu, dialog, shell } from "electron";
|
||||
const isMac = process.platform === "darwin";
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
import { compareSemanticVersions } from "./versionDiff";
|
||||
@ -97,7 +97,6 @@ const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
|
||||
{
|
||||
label: "Learn More",
|
||||
click: async () => {
|
||||
const { shell } = require("electron");
|
||||
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,17 +3,18 @@ import { AssistantExtension } from "@janhq/core";
|
||||
import { join } from "path";
|
||||
|
||||
export default class JanAssistantExtension implements AssistantExtension {
|
||||
private static readonly _homeDir = "assistants";
|
||||
private static readonly _homeDir = "file://assistants";
|
||||
|
||||
type(): ExtensionType {
|
||||
return ExtensionType.Assistant;
|
||||
}
|
||||
|
||||
onLoad(): void {
|
||||
async onLoad() {
|
||||
// making the assistant directory
|
||||
fs.mkdir(JanAssistantExtension._homeDir).then(() => {
|
||||
this.createJanAssistant();
|
||||
});
|
||||
if (!(await fs.existsSync(JanAssistantExtension._homeDir)))
|
||||
fs.mkdirSync(JanAssistantExtension._homeDir).then(() => {
|
||||
this.createJanAssistant();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -23,12 +24,12 @@ export default class JanAssistantExtension implements AssistantExtension {
|
||||
|
||||
async createAssistant(assistant: Assistant): Promise<void> {
|
||||
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
|
||||
const assistantMetadataPath = join(assistantDir, "assistant.json");
|
||||
try {
|
||||
await fs.writeFile(
|
||||
await fs.writeFileSync(
|
||||
assistantMetadataPath,
|
||||
JSON.stringify(assistant, null, 2)
|
||||
);
|
||||
@ -41,18 +42,14 @@ export default class JanAssistantExtension implements AssistantExtension {
|
||||
// get all the assistant directories
|
||||
// get all the assistant metadata json
|
||||
const results: Assistant[] = [];
|
||||
const allFileName: string[] = await fs.listFiles(
|
||||
const allFileName: string[] = await fs.readdirSync(
|
||||
JanAssistantExtension._homeDir
|
||||
);
|
||||
for (const fileName of allFileName) {
|
||||
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"
|
||||
);
|
||||
|
||||
@ -61,9 +58,12 @@ export default class JanAssistantExtension implements AssistantExtension {
|
||||
continue;
|
||||
}
|
||||
|
||||
const assistant: Assistant = JSON.parse(
|
||||
await fs.readFile(join(filePath, jsonFiles[0]))
|
||||
const content = await fs.readFileSync(
|
||||
join(filePath, jsonFiles[0]),
|
||||
"utf-8"
|
||||
);
|
||||
const assistant: Assistant =
|
||||
typeof content === "object" ? content : JSON.parse(content);
|
||||
|
||||
results.push(assistant);
|
||||
}
|
||||
@ -78,7 +78,7 @@ export default class JanAssistantExtension implements AssistantExtension {
|
||||
|
||||
// remove the directory
|
||||
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
||||
await fs.rmdir(assistantDir);
|
||||
await fs.rmdirSync(assistantDir);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import { Thread, ThreadMessage } from '@janhq/core'
|
||||
export default class JSONConversationalExtension
|
||||
implements ConversationalExtension
|
||||
{
|
||||
private static readonly _homeDir = 'threads'
|
||||
private static readonly _homeDir = 'file://threads'
|
||||
private static readonly _threadInfoFileName = 'thread.json'
|
||||
private static readonly _threadMessagesFileName = 'messages.jsonl'
|
||||
|
||||
@ -23,8 +23,9 @@ export default class JSONConversationalExtension
|
||||
/**
|
||||
* Called when the extension is loaded.
|
||||
*/
|
||||
onLoad() {
|
||||
fs.mkdir(JSONConversationalExtension._homeDir)
|
||||
async onLoad() {
|
||||
if (!(await fs.existsSync(JSONConversationalExtension._homeDir)))
|
||||
fs.mkdirSync(JSONConversationalExtension._homeDir)
|
||||
console.debug('JSONConversationalExtension loaded')
|
||||
}
|
||||
|
||||
@ -47,7 +48,9 @@ export default class JSONConversationalExtension
|
||||
const convos = promiseResults
|
||||
.map((result) => {
|
||||
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)
|
||||
@ -76,8 +79,11 @@ export default class JSONConversationalExtension
|
||||
threadDirPath,
|
||||
JSONConversationalExtension._threadInfoFileName,
|
||||
])
|
||||
await fs.mkdir(threadDirPath)
|
||||
await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2))
|
||||
if (!(await fs.existsSync(threadDirPath))) {
|
||||
await fs.mkdirSync(threadDirPath)
|
||||
}
|
||||
|
||||
await fs.writeFileSync(threadJsonPath, JSON.stringify(thread))
|
||||
Promise.resolve()
|
||||
} catch (err) {
|
||||
Promise.reject(err)
|
||||
@ -89,8 +95,9 @@ export default class JSONConversationalExtension
|
||||
* @param threadId The ID of the thread to delete.
|
||||
*/
|
||||
async deleteThread(threadId: string): Promise<void> {
|
||||
return fs.rmdir(
|
||||
await joinPath([JSONConversationalExtension._homeDir, `${threadId}`])
|
||||
return fs.rmdirSync(
|
||||
await joinPath([JSONConversationalExtension._homeDir, `${threadId}`]),
|
||||
{ recursive: true }
|
||||
)
|
||||
}
|
||||
|
||||
@ -104,8 +111,9 @@ export default class JSONConversationalExtension
|
||||
threadDirPath,
|
||||
JSONConversationalExtension._threadMessagesFileName,
|
||||
])
|
||||
await fs.mkdir(threadDirPath)
|
||||
await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n')
|
||||
if (!(await fs.existsSync(threadDirPath)))
|
||||
await fs.mkdirSync(threadDirPath)
|
||||
await fs.appendFileSync(threadMessagePath, JSON.stringify(message) + '\n')
|
||||
Promise.resolve()
|
||||
} catch (err) {
|
||||
Promise.reject(err)
|
||||
@ -125,8 +133,9 @@ export default class JSONConversationalExtension
|
||||
threadDirPath,
|
||||
JSONConversationalExtension._threadMessagesFileName,
|
||||
])
|
||||
await fs.mkdir(threadDirPath)
|
||||
await fs.writeFile(
|
||||
if (!(await fs.existsSync(threadDirPath)))
|
||||
await fs.mkdirSync(threadDirPath)
|
||||
await fs.writeFileSync(
|
||||
threadMessagePath,
|
||||
messages.map((msg) => JSON.stringify(msg)).join('\n') +
|
||||
(messages.length ? '\n' : '')
|
||||
@ -143,12 +152,13 @@ export default class JSONConversationalExtension
|
||||
* @returns data of the thread
|
||||
*/
|
||||
private async readThread(threadDirName: string): Promise<any> {
|
||||
return fs.readFile(
|
||||
return fs.readFileSync(
|
||||
await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
threadDirName,
|
||||
JSONConversationalExtension._threadInfoFileName,
|
||||
])
|
||||
]),
|
||||
'utf-8'
|
||||
)
|
||||
}
|
||||
|
||||
@ -157,23 +167,19 @@ export default class JSONConversationalExtension
|
||||
* @private
|
||||
*/
|
||||
private async getValidThreadDirs(): Promise<string[]> {
|
||||
const fileInsideThread: string[] = await fs.listFiles(
|
||||
const fileInsideThread: string[] = await fs.readdirSync(
|
||||
JSONConversationalExtension._homeDir
|
||||
)
|
||||
|
||||
const threadDirs: string[] = []
|
||||
for (let i = 0; i < fileInsideThread.length; i++) {
|
||||
if (fileInsideThread[i].includes('.DS_Store')) continue
|
||||
const path = await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
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
|
||||
)
|
||||
if (!isHavingThreadInfo) {
|
||||
@ -192,12 +198,8 @@ export default class JSONConversationalExtension
|
||||
JSONConversationalExtension._homeDir,
|
||||
threadId,
|
||||
])
|
||||
const isDir = await fs.isDirectory(threadDirPath)
|
||||
if (!isDir) {
|
||||
throw Error(`${threadDirPath} is not directory`)
|
||||
}
|
||||
|
||||
const files: string[] = await fs.listFiles(threadDirPath)
|
||||
const files: string[] = await fs.readdirSync(threadDirPath)
|
||||
if (
|
||||
!files.includes(JSONConversationalExtension._threadMessagesFileName)
|
||||
) {
|
||||
@ -209,7 +211,14 @@ export default class JSONConversationalExtension
|
||||
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[] = []
|
||||
result.forEach((line: string) => {
|
||||
|
||||
@ -17,9 +17,9 @@ import {
|
||||
ThreadMessage,
|
||||
events,
|
||||
executeOnMain,
|
||||
getUserSpace,
|
||||
fs,
|
||||
Model,
|
||||
joinPath,
|
||||
} from "@janhq/core";
|
||||
import { InferenceExtension } from "@janhq/core";
|
||||
import { requestInference } from "./helpers/sse";
|
||||
@ -58,8 +58,9 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
||||
/**
|
||||
* Subscribes to events emitted by the @janhq/core package.
|
||||
*/
|
||||
onLoad(): void {
|
||||
fs.mkdir(JanInferenceNitroExtension._homeDir);
|
||||
async onLoad() {
|
||||
if (!(await fs.existsSync(JanInferenceNitroExtension._homeDir)))
|
||||
fs.mkdirSync(JanInferenceNitroExtension._homeDir);
|
||||
this.writeDefaultEngineSettings();
|
||||
|
||||
// Events subscription
|
||||
@ -91,12 +92,12 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
||||
JanInferenceNitroExtension._homeDir,
|
||||
JanInferenceNitroExtension._engineMetadataFileName
|
||||
);
|
||||
if (await fs.exists(engineFile)) {
|
||||
JanInferenceNitroExtension._engineSettings = JSON.parse(
|
||||
await fs.readFile(engineFile)
|
||||
);
|
||||
if (await fs.existsSync(engineFile)) {
|
||||
const engine = await fs.readFileSync(engineFile, "utf-8");
|
||||
JanInferenceNitroExtension._engineSettings =
|
||||
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||
} else {
|
||||
await fs.writeFile(
|
||||
await fs.writeFileSync(
|
||||
engineFile,
|
||||
JSON.stringify(JanInferenceNitroExtension._engineSettings, null, 2)
|
||||
);
|
||||
@ -110,8 +111,7 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
||||
if (model.engine !== "nitro") {
|
||||
return;
|
||||
}
|
||||
const userSpacePath = await getUserSpace();
|
||||
const modelFullPath = join(userSpacePath, "models", model.id);
|
||||
const modelFullPath = await joinPath(["models", model.id]);
|
||||
|
||||
const nitroInitResult = await executeOnMain(MODULE, "initModel", {
|
||||
modelFullPath: modelFullPath,
|
||||
|
||||
@ -38,6 +38,10 @@ function stopModel(): Promise<void> {
|
||||
*/
|
||||
async function initModel(wrapper: any): Promise<ModelOperationResponse> {
|
||||
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
|
||||
|
||||
@ -29,7 +29,7 @@ import { join } from "path";
|
||||
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
||||
*/
|
||||
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 _currentModel: OpenAIModel;
|
||||
@ -53,8 +53,9 @@ export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
||||
/**
|
||||
* Subscribes to events emitted by the @janhq/core package.
|
||||
*/
|
||||
onLoad(): void {
|
||||
fs.mkdir(JanInferenceOpenAIExtension._homeDir);
|
||||
async onLoad() {
|
||||
if (!(await fs.existsSync(JanInferenceOpenAIExtension._homeDir)))
|
||||
fs.mkdirSync(JanInferenceOpenAIExtension._homeDir);
|
||||
JanInferenceOpenAIExtension.writeDefaultEngineSettings();
|
||||
|
||||
// Events subscription
|
||||
@ -85,12 +86,12 @@ export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
||||
JanInferenceOpenAIExtension._homeDir,
|
||||
JanInferenceOpenAIExtension._engineMetadataFileName
|
||||
);
|
||||
if (await fs.exists(engineFile)) {
|
||||
JanInferenceOpenAIExtension._engineSettings = JSON.parse(
|
||||
await fs.readFile(engineFile)
|
||||
);
|
||||
if (await fs.existsSync(engineFile)) {
|
||||
const engine = await fs.readFileSync(engineFile, 'utf-8');
|
||||
JanInferenceOpenAIExtension._engineSettings =
|
||||
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||
} else {
|
||||
await fs.writeFile(
|
||||
await fs.writeFileSync(
|
||||
engineFile,
|
||||
JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2)
|
||||
);
|
||||
|
||||
@ -57,9 +57,9 @@ export default class JanInferenceTritonTrtLLMExtension
|
||||
/**
|
||||
* Subscribes to events emitted by the @janhq/core package.
|
||||
*/
|
||||
onLoad(): void {
|
||||
fs.mkdir(JanInferenceTritonTrtLLMExtension._homeDir);
|
||||
JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings();
|
||||
async onLoad() {
|
||||
if (!(await fs.existsSync(JanInferenceTritonTrtLLMExtension._homeDir)))
|
||||
JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings();
|
||||
|
||||
// Events subscription
|
||||
events.on(EventName.OnMessageSent, (data) =>
|
||||
@ -98,12 +98,12 @@ export default class JanInferenceTritonTrtLLMExtension
|
||||
JanInferenceTritonTrtLLMExtension._homeDir,
|
||||
JanInferenceTritonTrtLLMExtension._engineMetadataFileName
|
||||
);
|
||||
if (await fs.exists(engine_json)) {
|
||||
JanInferenceTritonTrtLLMExtension._engineSettings = JSON.parse(
|
||||
await fs.readFile(engine_json)
|
||||
);
|
||||
if (await fs.existsSync(engine_json)) {
|
||||
const engine = await fs.readFileSync(engine_json, "utf-8");
|
||||
JanInferenceTritonTrtLLMExtension._engineSettings =
|
||||
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||
} else {
|
||||
await fs.writeFile(
|
||||
await fs.writeFileSync(
|
||||
engine_json,
|
||||
JSON.stringify(
|
||||
JanInferenceTritonTrtLLMExtension._engineSettings,
|
||||
|
||||
@ -15,7 +15,7 @@ import { ModelExtension, Model } from '@janhq/core'
|
||||
* A extension for models
|
||||
*/
|
||||
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 _supportedModelFormat = '.gguf'
|
||||
private static readonly _incompletedModelFileName = '.download'
|
||||
@ -46,11 +46,11 @@ export default class JanModelExtension implements ModelExtension {
|
||||
|
||||
private async copyModelsToHomeDir() {
|
||||
try {
|
||||
if (
|
||||
localStorage.getItem(`${EXTENSION_NAME}-version`) === VERSION &&
|
||||
(await fs.exists(JanModelExtension._homeDir))
|
||||
) {
|
||||
console.debug('Model already migrated')
|
||||
// list all of the files under the home directory
|
||||
|
||||
if (fs.existsSync(JanModelExtension._homeDir)) {
|
||||
// ignore if the model is already downloaded
|
||||
console.debug('Models already persisted.')
|
||||
return
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ export default class JanModelExtension implements ModelExtension {
|
||||
async downloadModel(model: Model): Promise<void> {
|
||||
// create corresponding directory
|
||||
const modelDirPath = await joinPath([JanModelExtension._homeDir, model.id])
|
||||
await fs.mkdir(modelDirPath)
|
||||
if (!(await fs.existsSync(modelDirPath))) await fs.mkdirSync(modelDirPath)
|
||||
|
||||
// try to retrieve the download file name from the source url
|
||||
// if it fails, use the model ID as the file name
|
||||
@ -116,11 +116,11 @@ export default class JanModelExtension implements ModelExtension {
|
||||
async cancelModelDownload(modelId: string): Promise<void> {
|
||||
return abortDownload(
|
||||
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
||||
).then(async () =>
|
||||
fs.deleteFile(
|
||||
).then(async () => {
|
||||
fs.unlinkSync(
|
||||
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,10 +133,10 @@ export default class JanModelExtension implements ModelExtension {
|
||||
const dirPath = await joinPath([JanModelExtension._homeDir, modelId])
|
||||
|
||||
// remove all files under dirPath except model.json
|
||||
const files = await fs.listFiles(dirPath)
|
||||
const files = await fs.readdirSync(dirPath)
|
||||
const deletePromises = files.map(async (fileName: string) => {
|
||||
if (fileName !== JanModelExtension._modelMetadataFileName) {
|
||||
return fs.deleteFile(await joinPath([dirPath, fileName]))
|
||||
return fs.unlinkSync(await joinPath([dirPath, fileName]))
|
||||
}
|
||||
})
|
||||
await Promise.allSettled(deletePromises)
|
||||
@ -158,7 +158,7 @@ export default class JanModelExtension implements ModelExtension {
|
||||
])
|
||||
|
||||
try {
|
||||
await fs.writeFile(jsonFilePath, JSON.stringify(model, null, 2))
|
||||
await fs.writeFileSync(jsonFilePath, JSON.stringify(model, null, 2))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
@ -175,7 +175,7 @@ export default class JanModelExtension implements ModelExtension {
|
||||
return true
|
||||
}
|
||||
return await fs
|
||||
.listFiles(await joinPath([JanModelExtension._homeDir, modelDir]))
|
||||
.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
|
||||
@ -198,22 +198,17 @@ export default class JanModelExtension implements ModelExtension {
|
||||
selector?: (path: string, model: Model) => Promise<boolean>
|
||||
): Promise<Model[]> {
|
||||
try {
|
||||
const filesUnderJanRoot = await fs.listFiles('')
|
||||
if (!filesUnderJanRoot.includes(JanModelExtension._homeDir)) {
|
||||
if (!(await fs.existsSync(JanModelExtension._homeDir))) {
|
||||
console.debug('model folder not found')
|
||||
return []
|
||||
}
|
||||
|
||||
const files: string[] = await fs.listFiles(JanModelExtension._homeDir)
|
||||
const files: string[] = await fs.readdirSync(JanModelExtension._homeDir)
|
||||
|
||||
const allDirectories: string[] = []
|
||||
for (const file of files) {
|
||||
const isDirectory = await fs.isDirectory(
|
||||
await joinPath([JanModelExtension._homeDir, file])
|
||||
)
|
||||
if (isDirectory) {
|
||||
allDirectories.push(file)
|
||||
}
|
||||
if (file === '.DS_Store') continue
|
||||
allDirectories.push(file)
|
||||
}
|
||||
|
||||
const readJsonPromises = allDirectories.map(async (dirName) => {
|
||||
@ -247,6 +242,7 @@ export default class JanModelExtension implements ModelExtension {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
|
||||
return modelData.filter((e) => !!e)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
@ -255,7 +251,7 @@ export default class JanModelExtension implements ModelExtension {
|
||||
}
|
||||
|
||||
private readModelMetadata(path: string) {
|
||||
return fs.readFile(path)
|
||||
return fs.readFileSync(path, 'utf-8')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"uikit",
|
||||
"core",
|
||||
"electron",
|
||||
"web"
|
||||
"web",
|
||||
"server"
|
||||
],
|
||||
"nohoist": [
|
||||
"uikit",
|
||||
@ -16,7 +17,9 @@
|
||||
"electron",
|
||||
"electron/**",
|
||||
"web",
|
||||
"web/**"
|
||||
"web/**",
|
||||
"server",
|
||||
"server/**"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
@ -28,6 +31,7 @@
|
||||
"test-local": "yarn lint && yarn build:test && yarn test",
|
||||
"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:server": "cd server && 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: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 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}`);
|
||||
})
|
||||
import { startServer } from "./index";
|
||||
|
||||
startServer();
|
||||
|
||||
@ -1,32 +1,37 @@
|
||||
{
|
||||
"name": "jan-server",
|
||||
"name": "@janhq/server",
|
||||
"version": "0.1.3",
|
||||
"main": "./build/main.js",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
"author": "Jan <service@jan.ai>",
|
||||
"license": "AGPL-3.0",
|
||||
"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.",
|
||||
"build": "",
|
||||
"files": [
|
||||
"build/**"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||
"test:e2e": "playwright test --workers=1",
|
||||
"dev": "nodemon .",
|
||||
"dev": "tsc --watch & node --watch build/main.js",
|
||||
"build": "tsc"
|
||||
},
|
||||
"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": {
|
||||
"@types/body-parser": "^1.19.5",
|
||||
"@types/npmcli__arborist": "^5.6.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"fastify": "^4.24.3",
|
||||
"nodemon": "^3.0.1",
|
||||
"run-script-os": "^1.1.6"
|
||||
},
|
||||
"installConfig": {
|
||||
"hoistingLimits": "workspaces"
|
||||
"run-script-os": "^1.1.6",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,10 +13,12 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": { "*": ["node_modules/*"] },
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"ignoreDeprecations": "5.0",
|
||||
"declaration": true
|
||||
},
|
||||
// "sourceMap": true,
|
||||
|
||||
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -81,7 +81,10 @@ export class ExtensionManager {
|
||||
*/
|
||||
async activateExtension(extension: Extension) {
|
||||
// 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) => {
|
||||
// Register class if it has a default export
|
||||
if (
|
||||
|
||||
@ -2,11 +2,14 @@ import { fs, joinPath } from '@janhq/core'
|
||||
|
||||
export const useEngineSettings = () => {
|
||||
const readOpenAISettings = async () => {
|
||||
const settings = await fs.readFile(
|
||||
await joinPath(['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) {
|
||||
return JSON.parse(settings)
|
||||
return typeof settings === 'object' ? settings : JSON.parse(settings)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
@ -17,8 +20,8 @@ export const useEngineSettings = () => {
|
||||
}) => {
|
||||
const settings = await readOpenAISettings()
|
||||
settings.api_key = apiKey
|
||||
await fs.writeFile(
|
||||
await joinPath(['engines', 'openai.json']),
|
||||
await fs.writeFileSync(
|
||||
await joinPath(['file://engines', 'openai.json']),
|
||||
JSON.stringify(settings)
|
||||
)
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
events,
|
||||
Model,
|
||||
ConversationalExtension,
|
||||
ModelRuntimeParams,
|
||||
} from '@janhq/core'
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
|
||||
@ -173,7 +172,7 @@ export default function useSendChatMessage() {
|
||||
updateThreadInitSuccess(activeThread.id)
|
||||
updateThread(updatedThread)
|
||||
|
||||
extensionManager
|
||||
await extensionManager
|
||||
.get<ConversationalExtension>(ExtensionType.Conversational)
|
||||
?.saveThread(updatedThread)
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ const nextConfig = {
|
||||
JSON.stringify(process.env.ANALYTICS_ID) ?? JSON.stringify('xxx'),
|
||||
ANALYTICS_HOST:
|
||||
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',
|
||||
|
||||
@ -51,6 +51,7 @@ const Sidebar: React.FC = () => {
|
||||
let filePath = undefined
|
||||
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||
switch (type) {
|
||||
case 'Engine':
|
||||
case 'Thread':
|
||||
filePath = await joinPath(['threads', activeThread.id])
|
||||
break
|
||||
@ -83,6 +84,7 @@ const Sidebar: React.FC = () => {
|
||||
let filePath = undefined
|
||||
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||
switch (type) {
|
||||
case 'Engine':
|
||||
case 'Thread':
|
||||
filePath = await joinPath(['threads', activeThread.id, 'thread.json'])
|
||||
break
|
||||
|
||||
@ -15,7 +15,7 @@ export default function Models() {
|
||||
const [searchValue, setsearchValue] = useState('')
|
||||
|
||||
const filteredDownloadedModels = downloadedModels.filter((x) => {
|
||||
return x.name.toLowerCase().includes(searchValue.toLowerCase())
|
||||
return x.name?.toLowerCase().includes(searchValue.toLowerCase())
|
||||
})
|
||||
|
||||
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 { restAPI } from './restService'
|
||||
export const setupCoreServices = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
console.debug('undefine', window)
|
||||
@ -10,9 +10,7 @@ export const setupCoreServices = () => {
|
||||
if (!window.core) {
|
||||
window.core = {
|
||||
events: new EventEmitter(),
|
||||
api: window.electronAPI ?? {
|
||||
...restAPI,
|
||||
},
|
||||
api: window.electronAPI ?? restAPI,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,13 +15,10 @@ export const isCoreExtensionInstalled = () => {
|
||||
return true
|
||||
}
|
||||
export const setupBaseExtensions = async () => {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
typeof window.electronAPI === 'undefined'
|
||||
) {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
const baseExtensions = await window.electronAPI.baseExtensions()
|
||||
const baseExtensions = await window.core?.api.baseExtensions()
|
||||
|
||||
if (
|
||||
!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,
|
||||
}
|
||||
1
web/types/index.d.ts
vendored
1
web/types/index.d.ts
vendored
@ -8,6 +8,7 @@ declare global {
|
||||
declare const VERSION: string
|
||||
declare const ANALYTICS_ID: string
|
||||
declare const ANALYTICS_HOST: string
|
||||
declare const API_BASE_URL: string
|
||||
declare const isMac: boolean
|
||||
declare const isWindows: boolean
|
||||
declare const isLinux: boolean
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user