From c47b1d00fb02bf32b356ae784cde8d5d60c6970b Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 11 Dec 2023 16:19:51 +0700 Subject: [PATCH 01/16] chore: expose fs apis --- core/rollup.config.ts | 76 ++++-- core/src/api/index.ts | 21 +- core/src/fs.ts | 66 ++--- .../src/node}/extension/extension.ts | 108 ++++---- .../src/node}/extension/index.ts | 46 ++-- core/src/node/extension/manager.ts | 60 +++++ .../src/node}/extension/store.ts | 66 ++--- core/src/node/index.ts | 4 + electron/handlers/app.ts | 2 +- electron/handlers/extension.ts | 8 +- electron/handlers/fs.ts | 240 +----------------- electron/main.ts | 2 +- electron/managers/extension.ts | 85 ------- electron/tsconfig.json | 2 +- electron/utils/migration.ts | 30 +++ extensions/assistant-extension/src/index.ts | 32 +-- .../conversational-extension/src/index.ts | 54 ++-- .../inference-nitro-extension/src/index.ts | 11 +- .../inference-openai-extension/src/index.ts | 22 +- extensions/model-extension/src/index.ts | 47 ++-- server/data/.gitkeep | 0 server/data/models/.gitkeep | 0 server/data/threads/.gitkeep | 0 server/icons/icon.png | Bin 38651 -> 0 bytes server/lib/.gitkeep | 0 server/main.ts | 41 ++- server/package.json | 3 + server/tsconfig.json | 5 +- server/v1/assistants/index.ts | 8 - server/v1/chat/index.ts | 11 - server/v1/extension/index.ts | 38 +++ server/v1/fs/index.ts | 34 +++ server/v1/index.ts | 45 +--- server/v1/models/downloadModel.ts | 23 -- server/v1/models/index.ts | 61 ----- server/v1/models/modelOp.ts | 11 - server/v1/threads/index.ts | 8 - web/extension/ExtensionManager.ts | 5 +- web/next.config.js | 1 + web/screens/Settings/Models/index.tsx | 2 +- web/services/cloudNativeService.ts | 76 ------ web/services/coreService.ts | 6 +- web/services/extensionService.ts | 7 +- web/services/restService.ts | 59 +++++ web/types/index.d.ts | 1 + web/utils/json.ts | 9 + 46 files changed, 578 insertions(+), 858 deletions(-) rename {electron => core/src/node}/extension/extension.ts (70%) rename {electron => core/src/node}/extension/index.ts (73%) create mode 100644 core/src/node/extension/manager.ts rename {electron => core/src/node}/extension/store.ts (67%) create mode 100644 core/src/node/index.ts delete mode 100644 electron/managers/extension.ts create mode 100644 electron/utils/migration.ts delete mode 100644 server/data/.gitkeep delete mode 100644 server/data/models/.gitkeep delete mode 100644 server/data/threads/.gitkeep delete mode 100644 server/icons/icon.png delete mode 100644 server/lib/.gitkeep delete mode 100644 server/v1/assistants/index.ts delete mode 100644 server/v1/chat/index.ts create mode 100644 server/v1/extension/index.ts create mode 100644 server/v1/fs/index.ts delete mode 100644 server/v1/models/downloadModel.ts delete mode 100644 server/v1/models/index.ts delete mode 100644 server/v1/models/modelOp.ts delete mode 100644 server/v1/threads/index.ts delete mode 100644 web/services/cloudNativeService.ts create mode 100644 web/services/restService.ts create mode 100644 web/utils/json.ts diff --git a/core/rollup.config.ts b/core/rollup.config.ts index 5e1762c967..89f57e35ff 100644 --- a/core/rollup.config.ts +++ b/core/rollup.config.ts @@ -8,30 +8,56 @@ 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/**', +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(), + ], }, - 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(), + { + input: `src/node/index.ts`, + output: [{ file: 'dist/node/index.cjs', 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'], + 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(), - ], -} + // Resolve source maps to the original source + sourceMaps(), + ], + }, +] diff --git a/core/src/api/index.ts b/core/src/api/index.ts index 4f2b45f5f1..3f4f782617 100644 --- a/core/src/api/index.ts +++ b/core/src/api/index.ts @@ -41,20 +41,17 @@ export enum ExtensionRoute { uninstallExtension = 'uninstallExtension', } export enum FileSystemRoute { - appendFile = 'appendFile', - copyFile = 'copyFile', - syncFile = 'syncFile', - deleteFile = 'deleteFile', - exists = 'exists', + appendFileSync = 'appendFileSync', + copyFileSync = 'copyFileSync', + unlinkSync = 'unlinkSync', + existsSync = 'existsSync', + readdirSync = 'readdirSync', + mkdirSync = 'mkdirSync', + readFileSync = 'readFileSync', + rmdirSync = 'rmdirSync', + writeFileSync = 'writeFileSync', getResourcePath = 'getResourcePath', getUserSpace = 'getUserSpace', - isDirectory = 'isDirectory', - listFiles = 'listFiles', - mkdir = 'mkdir', - readFile = 'readFile', - readLineByLine = 'readLineByLine', - rmdir = 'rmdir', - writeFile = 'writeFile', } export type ApiFunction = (...args: any[]) => any diff --git a/core/src/fs.ts b/core/src/fs.ts index 6ada7d3e2b..80f3117fd9 100644 --- a/core/src/fs.ts +++ b/core/src/fs.ts @@ -1,89 +1,67 @@ /** * 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} A Promise that resolves when the file is written successfully. */ -const writeFile: (path: string, data: string) => Promise = (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 => 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} A Promise that resolves with the contents of the file. */ -const readFile: (path: string) => Promise = (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 => 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} A Promise that resolves with the contents of the directory. */ -const listFiles: (path: string) => Promise = (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} A Promise that resolves when the directory is created successfully. */ -const mkdir: (path: string) => Promise = (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} A Promise that resolves when the directory is removed successfully. */ -const rmdir: (path: string) => Promise = (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} A Promise that resolves when the file is deleted. */ -const deleteFile: (path: string) => Promise = (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 = (path, data) => - global.core.api?.appendFile(path, data) - -const copyFile: (src: string, dest: string) => Promise = (src, dest) => - global.core.api?.copyFile(src, dest) +const appendFileSync = (...args: any[]) => global.core.api?.appendFileSync(...args) const syncFile: (src: string, dest: string) => Promise = (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} A promise that resolves to the lines of the file. + * Copy file sync. */ -const readLineByLine: (path: string) => Promise = (path) => - global.core.api?.readLineByLine(path) +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, } diff --git a/electron/extension/extension.ts b/core/src/node/extension/extension.ts similarity index 70% rename from electron/extension/extension.ts rename to core/src/node/extension/extension.ts index 1bd11611da..9dbd74e880 100644 --- a/electron/extension/extension.ts +++ b/core/src/node/extension/extension.ts @@ -1,14 +1,14 @@ -import { rmdir } from 'fs/promises' -import { resolve, join } from 'path' -import { manifest, extract } from 'pacote' -import * as Arborist from '@npmcli/arborist' -import { ExtensionManager } from './../managers/extension' +import { rmdirSync } from "fs"; +import { resolve, join } from "path"; +import { manifest, extract } from "pacote"; +import * as Arborist from "@npmcli/arborist"; +import { ExtensionManager, userSpacePath } 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. @@ -18,22 +18,22 @@ class Extension { * @property {string} main The entry point as defined in the main entry of the manifest. * @property {string} description The description of extension as defined in the manifest. */ - origin?: string - installOptions: any - name?: string - url?: string - version?: string - main?: string - description?: string + origin?: string; + installOptions: any; + name?: string; + url?: string; + version?: string; + main?: string; + description?: string; /** @private */ - _active = false + _active = false; /** * @private * @property {Object.} #listeners A list of callbacks to be executed when the Extension is updated. */ - listeners: Record void> = {} + listeners: Record void> = {}; /** * Set installOptions with defaults for options that have not been provided. @@ -45,10 +45,10 @@ class Extension { version: false, fullMetadata: false, Arborist, - } + }; - this.origin = origin - this.installOptions = { ...defaultOpts, ...options } + this.origin = origin; + this.installOptions = { ...defaultOpts, ...options }; } /** @@ -58,8 +58,8 @@ class Extension { get specifier() { return ( this.origin + - (this.installOptions.version ? '@' + this.installOptions.version : '') - ) + (this.installOptions.version ? "@" + this.installOptions.version : "") + ); } /** @@ -67,7 +67,7 @@ class Extension { * @type {boolean} */ get active() { - return this._active + return this._active; } /** @@ -77,20 +77,20 @@ class Extension { async getManifest() { // Get the package's manifest (package.json object) try { - const mnf = await manifest(this.specifier, this.installOptions) + const mnf = await manifest(this.specifier, this.installOptions); // set the Package properties based on the it's manifest - this.name = mnf.name - this.version = mnf.version - this.main = mnf.main - this.description = mnf.description + this.name = mnf.name; + this.version = mnf.version; + this.main = mnf.main; + this.description = mnf.description; } catch (error) { throw new Error( `Package ${this.origin} does not contain a valid manifest: ${error}` - ) + ); } - return true + return true; } /** @@ -101,26 +101,26 @@ class Extension { async _install() { try { // import the manifest details - await this.getManifest() + await this.getManifest(); // Install the package in a child folder of the given folder await extract( this.specifier, - join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''), + join(ExtensionManager.instance.extensionsPath ?? "", this.name ?? ""), this.installOptions - ) + ); // Set the url using the custom extensions protocol - this.url = `extension://${this.name}/${this.main}` + this.url = `extension://${this.name}/${this.main}`; - this.emitUpdate() + this.emitUpdate(); } catch (err) { // Ensure the extension is not stored and the folder is removed if the installation fails - this.setActive(false) - throw err + this.setActive(false); + throw err; } - return [this] + return [this]; } /** @@ -129,7 +129,7 @@ class Extension { * @param {callback} cb The function to execute on update */ subscribe(name: string, cb: () => void) { - this.listeners[name] = cb + this.listeners[name] = cb; } /** @@ -137,7 +137,7 @@ class Extension { * @param {string} name name of the callback to remove */ unsubscribe(name: string) { - delete this.listeners[name] + delete this.listeners[name]; } /** @@ -145,7 +145,7 @@ class Extension { */ emitUpdate() { for (const cb in this.listeners) { - this.listeners[cb].call(null, this) + this.listeners[cb].call(null, this); } } @@ -156,12 +156,12 @@ class Extension { */ async update(version = false) { if (await this.isUpdateAvailable()) { - this.installOptions.version = version - await this._install() - return true + this.installOptions.version = version; + await this._install(); + return true; } - return false + return false; } /** @@ -170,8 +170,8 @@ class Extension { */ async isUpdateAvailable() { if (this.origin) { - const mnf = await manifest(this.origin) - return mnf.version !== this.version ? mnf.version : false + const mnf = await manifest(this.origin); + return mnf.version !== this.version ? mnf.version : false; } } @@ -181,12 +181,12 @@ class Extension { */ async uninstall() { const extPath = resolve( - ExtensionManager.instance.extensionsPath ?? '', - this.name ?? '' - ) - await rmdir(extPath, { recursive: true }) + ExtensionManager.instance.extensionsPath ?? "", + this.name ?? "" + ); + await rmdirSync(extPath, { recursive: true }); - this.emitUpdate() + this.emitUpdate(); } /** @@ -195,10 +195,8 @@ class Extension { * @returns {Extension} This extension */ setActive(active: boolean) { - this._active = active - this.emitUpdate() - return this + this._active = active; + this.emitUpdate(); + return this; } } - -export default Extension diff --git a/electron/extension/index.ts b/core/src/node/extension/index.ts similarity index 73% rename from electron/extension/index.ts rename to core/src/node/extension/index.ts index c6a6cc0c06..538a91f9fa 100644 --- a/electron/extension/index.ts +++ b/core/src/node/extension/index.ts @@ -1,5 +1,13 @@ import { readFileSync } from 'fs' -import { protocol } from 'electron' + +let electron: any = undefined + +try { + electron = require('electron') +} catch (err) { + console.error('Electron is not available') +} + import { normalize } from 'path' import Extension from './extension' @@ -12,18 +20,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() @@ -42,12 +40,14 @@ export function init(options: any) { * @returns {boolean} Whether the protocol registration was successful */ function registerExtensionProtocol() { - return protocol.registerFileProtocol('extension', (request, callback) => { - const entry = request.url.substr('extension://'.length - 1) + 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 }) - }) + const url = normalize(ExtensionManager.instance.extensionsPath + entry) + callback({ path: url }) + }) + } } /** @@ -57,8 +57,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 +68,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 +81,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 +110,6 @@ function loadExtension(ext: any) { }) } } - addExtension(extension, false) extension.subscribe('pe-persist', persistExtensions) } @@ -123,7 +121,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 +132,4 @@ export function getStore() { getActiveExtensions, removeExtension, } -} +} \ No newline at end of file diff --git a/core/src/node/extension/manager.ts b/core/src/node/extension/manager.ts new file mode 100644 index 0000000000..ea4a6faf27 --- /dev/null +++ b/core/src/node/extension/manager.ts @@ -0,0 +1,60 @@ +import { join, resolve } from "path"; + +import { existsSync, mkdirSync, writeFileSync } from "fs"; +import { init } from "./index"; +/** + * Manages extension installation and migration. + */ + +export const userSpacePath = join(require("os").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"); + } +} diff --git a/electron/extension/store.ts b/core/src/node/extension/store.ts similarity index 67% rename from electron/extension/store.ts rename to core/src/node/extension/store.ts index 4857ef27a9..8e11c2bb24 100644 --- a/electron/extension/store.ts +++ b/core/src/node/extension/store.ts @@ -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.} extension - List of installed extensions */ -const extensions: Record = {} +const extensions: Record = {}; /** * Get a extension from the stored extensions. @@ -31,10 +21,10 @@ const extensions: Record = {} */ 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 = {} + const persistData: Record = {}; 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; } /** diff --git a/core/src/node/index.ts b/core/src/node/index.ts new file mode 100644 index 0000000000..0fdc821bcc --- /dev/null +++ b/core/src/node/index.ts @@ -0,0 +1,4 @@ +export * from './extension/index' +export * from './extension/extension' +export * from './extension/manager' +export * from './extension/store' diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts index ff88cd8f1d..d10e26d020 100644 --- a/electron/handlers/app.ts +++ b/electron/handlers/app.ts @@ -1,7 +1,7 @@ import { app, ipcMain, shell, nativeTheme } from 'electron' import { ModuleManager } from './../managers/module' import { join } from 'path' -import { ExtensionManager } from './../managers/extension' +import { ExtensionManager } from '../../common/extension' import { WindowManager } from './../managers/window' import { userSpacePath } from './../utils/path' import { AppRoute } from '@janhq/core' diff --git a/electron/handlers/extension.ts b/electron/handlers/extension.ts index f89206eac3..b1c1ef3145 100644 --- a/electron/handlers/extension.ts +++ b/electron/handlers/extension.ts @@ -6,10 +6,10 @@ import { getActiveExtensions, getAllExtensions, installExtensions, -} from './../extension/store' -import { getExtension } from './../extension/store' -import { removeExtension } from './../extension/store' -import Extension from './../extension/extension' +} from '../../common/extension/store' +import { getExtension } from '../../common/extension/store' +import { removeExtension } from '../../common/extension/store' +import Extension from '../../common/extension/extension' import { getResourcePath, userSpacePath } from './../utils/path' import { ExtensionRoute } from '@janhq/core' diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index f8ac089e49..8bb7604919 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -1,238 +1,22 @@ 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' +const fs = require('fs') /** * 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 => 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 => { - const fullPath = join(userSpacePath, path) - return Promise.resolve( - fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory() + Object.values(FileSystemRoute).forEach((route) => { + ipcMain.handle(route, async (event, ...args) => { + return fs[route]( + ...args.map((arg) => + 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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) - } - }) - } - ) } diff --git a/electron/main.ts b/electron/main.ts index 542875312b..e7a6d94f46 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -8,7 +8,7 @@ import { createUserSpace } from './utils/path' **/ import { WindowManager } from './managers/window' import { ModuleManager } from './managers/module' -import { ExtensionManager } from './managers/extension' +import { ExtensionManager } from '../common/extension' /** * IPC Handlers diff --git a/electron/managers/extension.ts b/electron/managers/extension.ts deleted file mode 100644 index 7eef24877e..0000000000 --- a/electron/managers/extension.ts +++ /dev/null @@ -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') - } -} diff --git a/electron/tsconfig.json b/electron/tsconfig.json index 3cc218f93e..8c612de854 100644 --- a/electron/tsconfig.json +++ b/electron/tsconfig.json @@ -15,6 +15,6 @@ "paths": { "*": ["node_modules/*"] }, "typeRoots": ["node_modules/@types"] }, - "include": ["./**/*.ts"], + "include": ["./**/*.ts", "../common/extension/manager.ts"], "exclude": ["core", "build", "dist", "tests", "node_modules"] } diff --git a/electron/utils/migration.ts b/electron/utils/migration.ts new file mode 100644 index 0000000000..cf66270c6c --- /dev/null +++ b/electron/utils/migration.ts @@ -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) + } + }) +} diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts index 9b48ce6c94..24285e671c 100644 --- a/extensions/assistant-extension/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -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 { 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,13 @@ 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( + const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter( (file: string) => file === "assistant.json" ); @@ -61,9 +57,9 @@ 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])); + const assistant: Assistant = + typeof content === "object" ? content : JSON.parse(content); results.push(assistant); } @@ -78,7 +74,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(); } diff --git a/extensions/conversational-extension/src/index.ts b/extensions/conversational-extension/src/index.ts index 8aae791e89..cfea275698 100644 --- a/extensions/conversational-extension/src/index.ts +++ b/extensions/conversational-extension/src/index.ts @@ -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,7 +95,7 @@ export default class JSONConversationalExtension * @param threadId The ID of the thread to delete. */ async deleteThread(threadId: string): Promise { - return fs.rmdir( + return fs.rmdirSync( await joinPath([JSONConversationalExtension._homeDir, `${threadId}`]) ) } @@ -104,8 +110,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 +132,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,7 +151,7 @@ export default class JSONConversationalExtension * @returns data of the thread */ private async readThread(threadDirName: string): Promise { - return fs.readFile( + return fs.readFileSync( await joinPath([ JSONConversationalExtension._homeDir, threadDirName, @@ -157,7 +165,7 @@ export default class JSONConversationalExtension * @private */ private async getValidThreadDirs(): Promise { - const fileInsideThread: string[] = await fs.listFiles( + const fileInsideThread: string[] = await fs.readdirSync( JSONConversationalExtension._homeDir ) @@ -167,13 +175,8 @@ export default class JSONConversationalExtension 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 +195,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 +208,12 @@ export default class JSONConversationalExtension JSONConversationalExtension._threadMessagesFileName, ]) - const result = await fs.readLineByLine(messageFilePath) + const result = await fs.readFileSync(messageFilePath).then((content) => + content + .toString() + .split('\n') + .filter((line) => line !== '') + ) const messages: ThreadMessage[] = [] result.forEach((line: string) => { diff --git a/extensions/inference-nitro-extension/src/index.ts b/extensions/inference-nitro-extension/src/index.ts index d19f3853cf..d6db104b6b 100644 --- a/extensions/inference-nitro-extension/src/index.ts +++ b/extensions/inference-nitro-extension/src/index.ts @@ -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)) { + if (await fs.existsSync(engineFile)) { JanInferenceNitroExtension._engineSettings = JSON.parse( - await fs.readFile(engineFile) + await fs.readFileSync(engineFile) ); } else { - await fs.writeFile( + await fs.writeFileSync( engineFile, JSON.stringify(JanInferenceNitroExtension._engineSettings, null, 2) ); diff --git a/extensions/inference-openai-extension/src/index.ts b/extensions/inference-openai-extension/src/index.ts index 80d211d951..c38fb7c5ac 100644 --- a/extensions/inference-openai-extension/src/index.ts +++ b/extensions/inference-openai-extension/src/index.ts @@ -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,17 @@ 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)) { + try { + JanInferenceOpenAIExtension._engineSettings = JSON.parse( + await fs.readFileSync(engineFile) + ); + } catch { + JanInferenceOpenAIExtension._engineSettings = + await fs.readFileSync(engineFile); + } } else { - await fs.writeFile( + await fs.writeFileSync( engineFile, JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2) ); diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index 9580afd9b5..ac42537ac5 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -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 { // 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,8 @@ export default class JanModelExtension implements ModelExtension { async cancelModelDownload(modelId: string): Promise { return abortDownload( await joinPath([JanModelExtension._homeDir, modelId, modelId]) - ).then(async () => - fs.deleteFile( - await joinPath([JanModelExtension._homeDir, modelId, modelId]) - ) - ) + ).then(async () => { + fs.unlinkSync(await joinPath([JanModelExtension._homeDir, modelId, modelId])) } /** @@ -133,10 +130,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.listFilesSync(dirPath) const deletePromises = files.map(async (fileName: string) => { if (fileName !== JanModelExtension._modelMetadataFileName) { - return fs.deleteFile(await joinPath([dirPath, fileName])) + return fs.deleteFileSync(await joinPath([dirPath, fileName])) } }) await Promise.allSettled(deletePromises) @@ -158,7 +155,10 @@ 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) } @@ -198,13 +198,12 @@ export default class JanModelExtension implements ModelExtension { selector?: (path: string, model: Model) => Promise ): Promise { 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) { @@ -242,12 +241,10 @@ export default class JanModelExtension implements ModelExtension { console.debug(`Unable to parse model metadata: ${result.value}`) return undefined } - } else { - console.error(result.reason) - return undefined - } - }) - return modelData.filter((e) => !!e) + }) + .filter((e) => !!e) + + return modelData } catch (err) { console.error(err) return [] @@ -255,7 +252,7 @@ export default class JanModelExtension implements ModelExtension { } private readModelMetadata(path: string) { - return fs.readFile(path) + return fs.readFileSync(path) } /** diff --git a/server/data/.gitkeep b/server/data/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/server/data/models/.gitkeep b/server/data/models/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/server/data/threads/.gitkeep b/server/data/threads/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/server/icons/icon.png b/server/icons/icon.png deleted file mode 100644 index 289f99ded85e1e3cc91b17af6b607d2cafd97cd7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38651 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 { + childContext.register(require("@fastify/static"), { + root: process.env.EXTENSION_ROOT, + wildcard: false, + }); -server.listen({ - port: JAN_API_PORT, - host: JAN_API_HOST -}).then(() => { - console.log(`JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`); -}) + done(); + }, + { prefix: "extensions" } +); +server.register(v1API, { prefix: "/api/v1" }); +server + .listen({ + port: JAN_API_PORT, + host: JAN_API_HOST, + }) + .then(() => { + console.log(`JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`); + }); diff --git a/server/package.json b/server/package.json index 1fd06a4826..3d8dd51f31 100644 --- a/server/package.json +++ b/server/package.json @@ -14,6 +14,9 @@ "build": "tsc" }, "dependencies": { + "@fastify/cors": "^8.4.2", + "@fastify/static": "^6.12.0", + "core": "link:./core" }, "devDependencies": { "@types/body-parser": "^1.19.5", diff --git a/server/tsconfig.json b/server/tsconfig.json index 3363fdba62..be5eb64055 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -13,10 +13,11 @@ "allowJs": true, "skipLibCheck": true, "paths": { "*": ["node_modules/*"] }, - "typeRoots": ["node_modules/@types"] + "typeRoots": ["node_modules/@types"], + "ignoreDeprecations": "5.0" }, // "sourceMap": true, - + "include": ["./**/*.ts"], "exclude": ["core", "build", "dist", "tests", "node_modules"] } diff --git a/server/v1/assistants/index.ts b/server/v1/assistants/index.ts deleted file mode 100644 index c722195d02..0000000000 --- a/server/v1/assistants/index.ts +++ /dev/null @@ -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; \ No newline at end of file diff --git a/server/v1/chat/index.ts b/server/v1/chat/index.ts deleted file mode 100644 index cb5fbf120a..0000000000 --- a/server/v1/chat/index.ts +++ /dev/null @@ -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; \ No newline at end of file diff --git a/server/v1/extension/index.ts b/server/v1/extension/index.ts new file mode 100644 index 0000000000..e4a8ca7cab --- /dev/null +++ b/server/v1/extension/index.ts @@ -0,0 +1,38 @@ +import { + FastifyInstance, + FastifyPluginAsync, + FastifyPluginOptions, +} from "fastify"; +import { join, extname } from "path"; +import { ExtensionRoute } from "@janhq/core"; + +import { readdirSync } from "fs"; + +const node = require("@janhq/core/dist/node/index.cjs"); + +const router: FastifyPluginAsync = async ( + app: FastifyInstance, + opts: FastifyPluginOptions +) => { + // TODO: Share code between node projects + app.post(`/${ExtensionRoute.getActiveExtensions}`, async (req, res) => { + const activeExtensions = await node.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 node.installExtensions(JSON.parse(extensions)[0]); + return JSON.parse(JSON.stringify(installed)); + }); +}; +export default router; diff --git a/server/v1/fs/index.ts b/server/v1/fs/index.ts new file mode 100644 index 0000000000..99f7de415e --- /dev/null +++ b/server/v1/fs/index.ts @@ -0,0 +1,34 @@ +import { + FastifyInstance, + FastifyPluginAsync, + FastifyPluginOptions, +} from "fastify"; +import { FileSystemRoute } from "@janhq/core"; +import { join } from "path"; +const { userSpacePath } = require("@janhq/core/dist/node/index.cjs"); +const fs = require("fs"); + +const router: FastifyPluginAsync = async ( + app: FastifyInstance, + opts: FastifyPluginOptions +) => { + // 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 fs[route]( + ...body.map((arg: any) => + arg.includes("file:/") + ? join(userSpacePath, arg.replace("file:/", "")) + : arg + ) + ); + res.status(200).send(result); + } catch (ex) { + console.log(ex); + } + }); + }); +}; +export default router; diff --git a/server/v1/index.ts b/server/v1/index.ts index 89d73200b1..e82855b2ff 100644 --- a/server/v1/index.ts +++ b/server/v1/index.ts @@ -1,37 +1,14 @@ -import assistantsAPI from './assistants' -import chatCompletionAPI from './chat' -import modelsAPI from './models' -import threadsAPI from './threads' +import fsAPI from "./fs"; +import extAPI from "./extension" -import { FastifyInstance, FastifyPluginAsync } from 'fastify' +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; \ No newline at end of file + app.register(fsAPI, { + prefix: "/fs", + }); + app.register(extAPI, { + prefix: "/extension", + }); +}; +export default router; diff --git a/server/v1/models/downloadModel.ts b/server/v1/models/downloadModel.ts deleted file mode 100644 index d564a22074..0000000000 --- a/server/v1/models/downloadModel.ts +++ /dev/null @@ -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; \ No newline at end of file diff --git a/server/v1/models/index.ts b/server/v1/models/index.ts deleted file mode 100644 index 22c5513000..0000000000 --- a/server/v1/models/index.ts +++ /dev/null @@ -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(); -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; \ No newline at end of file diff --git a/server/v1/models/modelOp.ts b/server/v1/models/modelOp.ts deleted file mode 100644 index f2c7ffe75b..0000000000 --- a/server/v1/models/modelOp.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {FastifyRequest, FastifyReply} from 'fastify' - -export async function startModel(req: FastifyRequest, res: FastifyReply): Promise { - - -} - -export async function stopModel(req: FastifyRequest, res: FastifyReply): Promise { - - -} \ No newline at end of file diff --git a/server/v1/threads/index.ts b/server/v1/threads/index.ts deleted file mode 100644 index e63f9e8d8e..0000000000 --- a/server/v1/threads/index.ts +++ /dev/null @@ -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; \ No newline at end of file diff --git a/web/extension/ExtensionManager.ts b/web/extension/ExtensionManager.ts index 4a53cb4915..43847f3447 100644 --- a/web/extension/ExtensionManager.ts +++ b/web/extension/ExtensionManager.ts @@ -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 ( diff --git a/web/next.config.js b/web/next.config.js index b5a583f18f..455ba70fc7 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -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', diff --git a/web/screens/Settings/Models/index.tsx b/web/screens/Settings/Models/index.tsx index 79859c2758..3c5a0c6e3f 100644 --- a/web/screens/Settings/Models/index.tsx +++ b/web/screens/Settings/Models/index.tsx @@ -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 ( diff --git a/web/services/cloudNativeService.ts b/web/services/cloudNativeService.ts deleted file mode 100644 index a300ac02d9..0000000000 --- a/web/services/cloudNativeService.ts +++ /dev/null @@ -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 { - 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 { - 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) - } -} diff --git a/web/services/coreService.ts b/web/services/coreService.ts index 89b6dcfd1e..c010c6cec5 100644 --- a/web/services/coreService.ts +++ b/web/services/coreService.ts @@ -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, } } } diff --git a/web/services/extensionService.ts b/web/services/extensionService.ts index d79bda0655..b18fb3cc1e 100644 --- a/web/services/extensionService.ts +++ b/web/services/extensionService.ts @@ -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) || diff --git a/web/services/restService.ts b/web/services/restService.ts new file mode 100644 index 0000000000..9c78078add --- /dev/null +++ b/web/services/restService.ts @@ -0,0 +1,59 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { safeJsonParse } from '@/utils/json' +import { + AppRoute, + DownloadRoute, + ExtensionRoute, + FileSystemRoute, +} from '@janhq/core' + +// 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}/api/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, + invokeExtensionFunc: () => {}, +} diff --git a/web/types/index.d.ts b/web/types/index.d.ts index bd46b8a6f7..328fc8f535 100644 --- a/web/types/index.d.ts +++ b/web/types/index.d.ts @@ -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 diff --git a/web/utils/json.ts b/web/utils/json.ts new file mode 100644 index 0000000000..89115aa5fd --- /dev/null +++ b/web/utils/json.ts @@ -0,0 +1,9 @@ +export const safeJsonParse = (str: string) => { + try { + const jsonValue: T = JSON.parse(str) + + return jsonValue + } catch { + return undefined + } +} From 80a254587261fc5ee54d4b373afa310fcb563184 Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 12 Dec 2023 22:03:29 +0700 Subject: [PATCH 02/16] chore: correct electron import path --- core/src/api/index.ts | 2 ++ electron/handlers/app.ts | 2 +- electron/handlers/extension.ts | 16 ++++++++-------- electron/main.ts | 3 ++- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/core/src/api/index.ts b/core/src/api/index.ts index 3f4f782617..79527289c5 100644 --- a/core/src/api/index.ts +++ b/core/src/api/index.ts @@ -51,6 +51,8 @@ export enum FileSystemRoute { rmdirSync = 'rmdirSync', writeFileSync = 'writeFileSync', getResourcePath = 'getResourcePath', + + isDirectory = 'isDirectory', getUserSpace = 'getUserSpace', } diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts index d10e26d020..d91132558c 100644 --- a/electron/handlers/app.ts +++ b/electron/handlers/app.ts @@ -1,11 +1,11 @@ import { app, ipcMain, shell, nativeTheme } from 'electron' import { ModuleManager } from './../managers/module' import { join } from 'path' -import { ExtensionManager } from '../../common/extension' import { WindowManager } from './../managers/window' import { userSpacePath } from './../utils/path' import { AppRoute } from '@janhq/core' import { getResourcePath } from './../utils/path' +const { ExtensionManager } = require('@janhq/core/dist/node/index.cjs') export function handleAppIPCs() { /** diff --git a/electron/handlers/extension.ts b/electron/handlers/extension.ts index b1c1ef3145..7c5d8b37f6 100644 --- a/electron/handlers/extension.ts +++ b/electron/handlers/extension.ts @@ -2,14 +2,14 @@ import { ipcMain, webContents } from 'electron' import { readdirSync } from 'fs' import { ModuleManager } from './../managers/module' import { join, extname } from 'path' -import { - getActiveExtensions, - getAllExtensions, + +const { installExtensions, -} from '../../common/extension/store' -import { getExtension } from '../../common/extension/store' -import { removeExtension } from '../../common/extension/store' -import Extension from '../../common/extension/extension' + getExtension, + removeExtension, + getActiveExtensions, +} = require('@janhq/core/dist/node/index.cjs') + 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() diff --git a/electron/main.ts b/electron/main.ts index e7a6d94f46..abf65576c7 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -8,7 +8,6 @@ import { createUserSpace } from './utils/path' **/ import { WindowManager } from './managers/window' import { ModuleManager } from './managers/module' -import { ExtensionManager } from '../common/extension' /** * IPC Handlers @@ -19,6 +18,8 @@ import { handleAppIPCs } from './handlers/app' import { handleAppUpdates } from './handlers/update' import { handleFsIPCs } from './handlers/fs' +const { ExtensionManager } = require('@janhq/core/dist/node/index.cjs') + app .whenReady() .then(createUserSpace) From a7e1780649839933fd02c848567326d931ba958b Mon Sep 17 00:00:00 2001 From: James Date: Tue, 12 Dec 2023 22:49:25 +0700 Subject: [PATCH 03/16] update download api Signed-off-by: James --- .../managers => core/src/node}/download.ts | 0 core/src/node/index.ts | 1 + electron/handlers/download.ts | 2 +- electron/tsconfig.json | 2 +- server/package.json | 7 +- server/v1/download/index.ts | 66 +++++++++++++++++++ server/v1/index.ts | 6 +- 7 files changed, 79 insertions(+), 5 deletions(-) rename {electron/managers => core/src/node}/download.ts (100%) create mode 100644 server/v1/download/index.ts diff --git a/electron/managers/download.ts b/core/src/node/download.ts similarity index 100% rename from electron/managers/download.ts rename to core/src/node/download.ts diff --git a/core/src/node/index.ts b/core/src/node/index.ts index 0fdc821bcc..605207534e 100644 --- a/core/src/node/index.ts +++ b/core/src/node/index.ts @@ -2,3 +2,4 @@ export * from './extension/index' export * from './extension/extension' export * from './extension/manager' export * from './extension/store' +export * from './download' diff --git a/electron/handlers/download.ts b/electron/handlers/download.ts index 145174ac2b..77d6e294e1 100644 --- a/electron/handlers/download.ts +++ b/electron/handlers/download.ts @@ -1,5 +1,5 @@ import { app, ipcMain } from 'electron' -import { DownloadManager } from './../managers/download' +import { DownloadManager } from '@janhq/core/src/node/download' import { resolve, join } from 'path' import { WindowManager } from './../managers/window' import request from 'request' diff --git a/electron/tsconfig.json b/electron/tsconfig.json index 8c612de854..3cc218f93e 100644 --- a/electron/tsconfig.json +++ b/electron/tsconfig.json @@ -15,6 +15,6 @@ "paths": { "*": ["node_modules/*"] }, "typeRoots": ["node_modules/@types"] }, - "include": ["./**/*.ts", "../common/extension/manager.ts"], + "include": ["./**/*.ts"], "exclude": ["core", "build", "dist", "tests", "node_modules"] } diff --git a/server/package.json b/server/package.json index 3d8dd51f31..cd6ced3a02 100644 --- a/server/package.json +++ b/server/package.json @@ -16,7 +16,9 @@ "dependencies": { "@fastify/cors": "^8.4.2", "@fastify/static": "^6.12.0", - "core": "link:./core" + "core": "link:./core", + "request": "^2.88.2", + "request-progress": "^3.0.0" }, "devDependencies": { "@types/body-parser": "^1.19.5", @@ -27,7 +29,8 @@ "eslint-plugin-react": "^7.33.2", "fastify": "^4.24.3", "nodemon": "^3.0.1", - "run-script-os": "^1.1.6" + "run-script-os": "^1.1.6", + "typescript": "^5.2.2" }, "installConfig": { "hoistingLimits": "workspaces" diff --git a/server/v1/download/index.ts b/server/v1/download/index.ts new file mode 100644 index 0000000000..a19b4220d4 --- /dev/null +++ b/server/v1/download/index.ts @@ -0,0 +1,66 @@ +import { + FastifyInstance, + FastifyPluginAsync, + FastifyPluginOptions, +} from "fastify"; +import { DownloadRoute } from "@janhq/core"; +import { join } from "path"; +const { + userSpacePath, + DownloadManager, +} = require("@janhq/core/dist/node/index.cjs"); +const request = require("request"); +const progress = require("request-progress"); +const { createWriteStream } = require("fs"); + +const router: FastifyPluginAsync = async ( + app: FastifyInstance, + opts: FastifyPluginOptions +) => { + 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(); + }); +}; + +export default router; diff --git a/server/v1/index.ts b/server/v1/index.ts index e82855b2ff..64f52ffde4 100644 --- a/server/v1/index.ts +++ b/server/v1/index.ts @@ -1,5 +1,6 @@ import fsAPI from "./fs"; -import extAPI from "./extension" +import extAPI from "./extension"; +import downloadAPI from "./download"; import { FastifyInstance, FastifyPluginAsync } from "fastify"; @@ -10,5 +11,8 @@ const router: FastifyPluginAsync = async (app: FastifyInstance, opts) => { app.register(extAPI, { prefix: "/extension", }); + app.register(downloadAPI, { + prefix: "/download", + }); }; export default router; From f79c61777b1d70c0df792354d20c41683246cf61 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 13 Dec 2023 12:36:54 +0700 Subject: [PATCH 04/16] update chat_completion Signed-off-by: James --- core/src/node/index.ts | 1 + .../managers => core/src/node}/module.ts | 13 ++++------- electron/handlers/app.ts | 2 +- electron/handlers/extension.ts | 2 +- electron/main.ts | 2 +- .../inference-nitro-extension/src/index.ts | 3 +-- server/v1/extension/index.ts | 23 ++++++++++++++++++- web/services/restService.ts | 1 - 8 files changed, 32 insertions(+), 15 deletions(-) rename {electron/managers => core/src/node}/module.ts (59%) diff --git a/core/src/node/index.ts b/core/src/node/index.ts index 605207534e..081a94358c 100644 --- a/core/src/node/index.ts +++ b/core/src/node/index.ts @@ -3,3 +3,4 @@ export * from './extension/extension' export * from './extension/manager' export * from './extension/store' export * from './download' +export * from './module' diff --git a/electron/managers/module.ts b/core/src/node/module.ts similarity index 59% rename from electron/managers/module.ts rename to core/src/node/module.ts index dc16d0d227..2201fb4006 100644 --- a/electron/managers/module.ts +++ b/core/src/node/module.ts @@ -1,16 +1,14 @@ -import { dispose } from "./../utils/disposable"; - /** * Manages imported modules. */ export class ModuleManager { - public requiredModules: Record = {}; + public requiredModules: Record = {} - 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 = {} } } diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts index d91132558c..a94b563ad4 100644 --- a/electron/handlers/app.ts +++ b/electron/handlers/app.ts @@ -1,5 +1,5 @@ import { app, ipcMain, shell, nativeTheme } from 'electron' -import { ModuleManager } from './../managers/module' +import { ModuleManager } from '@janhq/core/src/node/module' import { join } from 'path' import { WindowManager } from './../managers/window' import { userSpacePath } from './../utils/path' diff --git a/electron/handlers/extension.ts b/electron/handlers/extension.ts index 7c5d8b37f6..92c843f52f 100644 --- a/electron/handlers/extension.ts +++ b/electron/handlers/extension.ts @@ -1,6 +1,6 @@ import { ipcMain, webContents } from 'electron' import { readdirSync } from 'fs' -import { ModuleManager } from './../managers/module' +import { ModuleManager } from '@janhq/core/src/node/module' import { join, extname } from 'path' const { diff --git a/electron/main.ts b/electron/main.ts index abf65576c7..995ffd869f 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -7,7 +7,7 @@ import { createUserSpace } from './utils/path' * Managers **/ import { WindowManager } from './managers/window' -import { ModuleManager } from './managers/module' +import { ModuleManager } from '@janhq/core/src/node/module' /** * IPC Handlers diff --git a/extensions/inference-nitro-extension/src/index.ts b/extensions/inference-nitro-extension/src/index.ts index d6db104b6b..9334efa2ec 100644 --- a/extensions/inference-nitro-extension/src/index.ts +++ b/extensions/inference-nitro-extension/src/index.ts @@ -111,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 = join("models", model.id, model.id); const nitroInitResult = await executeOnMain(MODULE, "initModel", { modelFullPath: modelFullPath, diff --git a/server/v1/extension/index.ts b/server/v1/extension/index.ts index e4a8ca7cab..c2c646cc4e 100644 --- a/server/v1/extension/index.ts +++ b/server/v1/extension/index.ts @@ -5,7 +5,10 @@ import { } from "fastify"; import { join, extname } from "path"; import { ExtensionRoute } from "@janhq/core"; - +const { + userSpacePath, + ModuleManager, +} = require("@janhq/core/dist/node/index.cjs"); import { readdirSync } from "fs"; const node = require("@janhq/core/dist/node/index.cjs"); @@ -34,5 +37,23 @@ const router: FastifyPluginAsync = async ( const installed = await node.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 = require(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.`); + } + }); }; export default router; diff --git a/web/services/restService.ts b/web/services/restService.ts index 9c78078add..a88a859ab7 100644 --- a/web/services/restService.ts +++ b/web/services/restService.ts @@ -55,5 +55,4 @@ export const restAPI = { }, {}), openExternalUrl, appVersion, - invokeExtensionFunc: () => {}, } From 938f365ff56abababe101f7b94ef5ddbbae138fa Mon Sep 17 00:00:00 2001 From: James Date: Wed, 13 Dec 2023 20:28:06 +0700 Subject: [PATCH 05/16] fix electron import Signed-off-by: James --- electron/handlers/app.ts | 6 ++++-- electron/handlers/download.ts | 2 +- electron/handlers/extension.ts | 2 +- electron/main.ts | 7 ++++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts index a94b563ad4..67e2fce566 100644 --- a/electron/handlers/app.ts +++ b/electron/handlers/app.ts @@ -1,11 +1,13 @@ import { app, ipcMain, shell, nativeTheme } from 'electron' -import { ModuleManager } from '@janhq/core/src/node/module' import { join } from 'path' import { WindowManager } from './../managers/window' import { userSpacePath } from './../utils/path' import { AppRoute } from '@janhq/core' import { getResourcePath } from './../utils/path' -const { ExtensionManager } = require('@janhq/core/dist/node/index.cjs') +const { + ExtensionManager, + ModuleManager, +} = require('@janhq/core/dist/node/index.cjs') export function handleAppIPCs() { /** diff --git a/electron/handlers/download.ts b/electron/handlers/download.ts index 77d6e294e1..81f5181f59 100644 --- a/electron/handlers/download.ts +++ b/electron/handlers/download.ts @@ -1,11 +1,11 @@ import { app, ipcMain } from 'electron' -import { DownloadManager } from '@janhq/core/src/node/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') +const { DownloadManager } = require('@janhq/core/dist/node/index.cjs') export function handleDownloaderIPCs() { /** diff --git a/electron/handlers/extension.ts b/electron/handlers/extension.ts index 92c843f52f..ab8a36f3a6 100644 --- a/electron/handlers/extension.ts +++ b/electron/handlers/extension.ts @@ -1,7 +1,7 @@ import { ipcMain, webContents } from 'electron' import { readdirSync } from 'fs' -import { ModuleManager } from '@janhq/core/src/node/module' import { join, extname } from 'path' +const { ModuleManager } = require('@janhq/core/dist/node/index.cjs') const { installExtensions, diff --git a/electron/main.ts b/electron/main.ts index 995ffd869f..6e1a60d489 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -7,7 +7,10 @@ import { createUserSpace } from './utils/path' * Managers **/ import { WindowManager } from './managers/window' -import { ModuleManager } from '@janhq/core/src/node/module' +const { + ExtensionManager, + ModuleManager, +} = require('@janhq/core/dist/node/index.cjs') /** * IPC Handlers @@ -18,8 +21,6 @@ import { handleAppIPCs } from './handlers/app' import { handleAppUpdates } from './handlers/update' import { handleFsIPCs } from './handlers/fs' -const { ExtensionManager } = require('@janhq/core/dist/node/index.cjs') - app .whenReady() .then(createUserSpace) From 088beab3cf6835e2eafdea68e8873110de06a4f2 Mon Sep 17 00:00:00 2001 From: NamH Date: Thu, 14 Dec 2023 14:00:43 +0700 Subject: [PATCH 06/16] feat: adding API support at 1337 (#991) Signed-off-by: James Co-authored-by: James --- electron/api/index.ts | 31 ++++ electron/api/models/index.ts | 288 +++++++++++++++++++++++++++++++++++ electron/main.ts | 72 +++++++++ electron/package.json | 4 + 4 files changed, 395 insertions(+) create mode 100644 electron/api/index.ts create mode 100644 electron/api/models/index.ts diff --git a/electron/api/index.ts b/electron/api/index.ts new file mode 100644 index 0000000000..88e5ffb61e --- /dev/null +++ b/electron/api/index.ts @@ -0,0 +1,31 @@ +export const JanApiRouteConfiguration: Record = { + 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 + } +} diff --git a/electron/api/models/index.ts b/electron/api/models/index.ts new file mode 100644 index 0000000000..ecf43887a6 --- /dev/null +++ b/electron/api/models/index.ts @@ -0,0 +1,288 @@ +const fs = require('fs') +import { rimraf } from 'rimraf' +import { JanApiRouteConfiguration, RouteConfiguration } from '../index' +import { join } from 'path' +import { ThreadMessage } from '@janhq/core/dist/types/types' +import { ulid } from 'ulid' +const progress = require('request-progress') +import request from 'request' +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.includes('.')) allDirectories.push(file) + } + + const readJsonPromises = allDirectories.map((dirName) => { + const jsonPath = join( + directoryPath, + dirName, + configuration.metadataFileName + ) + return readModelMetadata(jsonPath) + }) + + const results = await Promise.allSettled(readJsonPromises) + const modelData = results + .map((result) => { + if (result.status === 'fulfilled') { + try { + return JSON.parse(result.value) + } catch { + return result.value + } + } else { + console.error(result.reason) + return undefined + } + }) + .filter((e) => !!e) + + return modelData + } catch (err) { + console.error(err) + return [] + } +} + +const readModelMetadata = (path: string) => { + return fs.readFileSync(join(path)) +} + +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) + rimraf.sync(myPath) + 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) + .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}`, + } +} diff --git a/electron/main.ts b/electron/main.ts index 6e1a60d489..2ebfe531a7 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -2,6 +2,7 @@ import { app, BrowserWindow } from 'electron' import { join } from 'path' import { setupMenu } from './utils/menu' import { createUserSpace } from './utils/path' +import Fastify from 'fastify' /** * Managers @@ -20,6 +21,77 @@ import { handleExtensionIPCs } from './handlers/extension' import { handleAppIPCs } from './handlers/app' import { handleAppUpdates } from './handlers/update' import { handleFsIPCs } from './handlers/fs' +import { + createMessage, + createThread, + deleteBuilder, + downloadModel, + getBuilder, + getMessages, + retrieveBuilder, + retrieveMesasge, + updateThread, +} from './api/models' +import { JanApiRouteConfiguration } from './api' + +// TODO: refactor this, this API piece of code should not belong here +const version = 'v1' + +const fastify = Fastify({ + logger: true, +}) + +fastify.listen({ port: 1337 }, function (err, address) { + if (err) { + fastify.log.error(err) + process.exit(1) + } +}) + +Object.keys(JanApiRouteConfiguration).forEach((key) => { + fastify.get(`/${version}/${key}`, async (_request) => + getBuilder(JanApiRouteConfiguration[key]) + ) + + fastify.get(`/${version}/${key}/:id`, async (request: any) => + retrieveBuilder(JanApiRouteConfiguration[key], request.params.id) + ) + + fastify.delete(`/${version}/${key}/:id`, async (request: any) => + deleteBuilder(JanApiRouteConfiguration[key], request.params.id) + ) +}) + +// get messages of thread id +fastify.get(`/${version}/threads/:threadId/messages`, async (request: any) => + getMessages(request.params.threadId) +) + +// retrieve message +fastify.get( + `/${version}/threads/:threadId/messages/:messageId`, + async (request: any) => + retrieveMesasge(request.params.threadId, request.params.messageId) +) + +// create thread +fastify.post(`/${version}/threads`, async (request: any) => + createThread(request.body) +) + +// create message +fastify.post(`/${version}/threads/:threadId/messages`, async (request: any) => + createMessage(request.params.threadId, request.body) +) + +// modify thread +fastify.patch(`/${version}/threads/:threadId`, async (request: any) => + updateThread(request.params.threadId, request.body) +) + +fastify.get(`/${version}/models/download/:modelId`, async (request: any) => + downloadModel(request.params.modelId) +) app .whenReady() diff --git a/electron/package.json b/electron/package.json index 864934d568..0be3326664 100644 --- a/electron/package.json +++ b/electron/package.json @@ -77,10 +77,14 @@ "@uiball/loaders": "^1.3.0", "electron-store": "^8.1.0", "electron-updater": "^6.1.7", + "fastify": "^4.25.0", "fs-extra": "^11.2.0", "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": { From b42f30a23bf056760bcec67ac158239fe24f1a85 Mon Sep 17 00:00:00 2001 From: hiro Date: Fri, 15 Dec 2023 04:57:01 +0700 Subject: [PATCH 07/16] feat: Add /chat/completion api and handler --- electron/api/models/index.ts | 47 ++++++++++++++++++++++++++++++++++++ electron/main.ts | 6 +++++ electron/package.json | 1 + 3 files changed, 54 insertions(+) diff --git a/electron/api/models/index.ts b/electron/api/models/index.ts index ecf43887a6..e2bb94f99a 100644 --- a/electron/api/models/index.ts +++ b/electron/api/models/index.ts @@ -3,6 +3,8 @@ import { rimraf } from 'rimraf' import { JanApiRouteConfiguration, RouteConfiguration } from '../index' import { join } from 'path' import { ThreadMessage } from '@janhq/core/dist/types/types' + +import fetch from 'node-fetch' import { ulid } from 'ulid' const progress = require('request-progress') import request from 'request' @@ -286,3 +288,48 @@ export const downloadModel = async (modelId: string) => { message: `Starting download ${modelId}`, } } + +export const chatCompletions = async (request: any, reply: any) => { + // By default it's Local Nitro server + let apiUrl: string = + 'http://127.0.0.1:3928/inferences/llamacpp/chat_completion' + const modelId = request.body.model + const oaiModelId = [ + 'gpt-4', + 'gpt-3.5-turbo-1106', + 'gpt-3.5-turbo-16k-0613', + 'gpt-4-0314', + 'gpt-3.5-turbo', + 'gpt-4-0613', + 'gpt-3.5-turbo-0301', + ] + if (oaiModelId.includes(modelId)) { + apiUrl = 'https://api.openai.com/v1/chat/completions' + // don't know how to handle engine here, maybe send engine with body? + } + // Else if modelList.includes(modelId) {} + // With Nitro, add FIFO queue to handle multiple requests at once while loading/ unloading properly + const apiKey = process.env.OPENAI_KEY || 'YOUR_OPENAI_API_KEY' // Replace with your API key + console.log(`chatCompletions: modelId=${modelId} with apiUrl=${apiUrl}`) + reply.raw.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }) + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'api-key': '${apiKey}', + }, + body: JSON.stringify(request.body), + }) + if (response.status !== 200) { + console.error(response) + return + } else { + response.body.pipe(reply.raw) + } +} diff --git a/electron/main.ts b/electron/main.ts index 2ebfe531a7..3a9556cbca 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -31,6 +31,7 @@ import { retrieveBuilder, retrieveMesasge, updateThread, + chatCompletions, } from './api/models' import { JanApiRouteConfiguration } from './api' @@ -93,6 +94,11 @@ fastify.get(`/${version}/models/download/:modelId`, async (request: any) => downloadModel(request.params.modelId) ) +// Endpoints +fastify.post(`/${version}/chat/completions`, async (request: any, reply: any) => + chatCompletions(request, reply) +) + app .whenReady() .then(createUserSpace) diff --git a/electron/package.json b/electron/package.json index 0be3326664..93d55ddd29 100644 --- a/electron/package.json +++ b/electron/package.json @@ -79,6 +79,7 @@ "electron-updater": "^6.1.7", "fastify": "^4.25.0", "fs-extra": "^11.2.0", + "node-fetch": "2", "pacote": "^17.0.4", "request": "^2.88.2", "request-progress": "^3.0.0", From 88edb00e0790d957381710d86e3d0bc9d773d6c2 Mon Sep 17 00:00:00 2001 From: hiro Date: Fri, 15 Dec 2023 10:44:21 +0700 Subject: [PATCH 08/16] chore: add todo for modelList --- electron/api/models/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electron/api/models/index.ts b/electron/api/models/index.ts index e2bb94f99a..a38738f94c 100644 --- a/electron/api/models/index.ts +++ b/electron/api/models/index.ts @@ -293,6 +293,8 @@ export const chatCompletions = async (request: any, reply: any) => { // By default it's Local Nitro server let apiUrl: string = 'http://127.0.0.1:3928/inferences/llamacpp/chat_completion' + // getBuilder() => modelList + // Read from engines.json const modelId = request.body.model const oaiModelId = [ 'gpt-4', From 79e882f6c87b2e7f70ea736f9244442d79f3c2cd Mon Sep 17 00:00:00 2001 From: NamH Date: Fri, 15 Dec 2023 14:18:47 +0700 Subject: [PATCH 09/16] chore: read engine.json for openai chat_completion (#1030) Signed-off-by: James Co-authored-by: James --- electron/api/models/index.ts | 93 ++++++++++++++++--------- extensions/model-extension/src/index.ts | 8 +-- 2 files changed, 61 insertions(+), 40 deletions(-) diff --git a/electron/api/models/index.ts b/electron/api/models/index.ts index a38738f94c..67444f9e11 100644 --- a/electron/api/models/index.ts +++ b/electron/api/models/index.ts @@ -2,7 +2,7 @@ const fs = require('fs') import { rimraf } from 'rimraf' import { JanApiRouteConfiguration, RouteConfiguration } from '../index' import { join } from 'path' -import { ThreadMessage } from '@janhq/core/dist/types/types' +import { Model, ThreadMessage } from '@janhq/core/dist/types/types' import fetch from 'node-fetch' import { ulid } from 'ulid' @@ -25,16 +25,17 @@ export const getBuilder = async (configuration: RouteConfiguration) => { const allDirectories: string[] = [] for (const file of files) { - if (!file.includes('.')) allDirectories.push(file) + if (file === '.DS_Store') continue + allDirectories.push(file) } - const readJsonPromises = allDirectories.map((dirName) => { + const readJsonPromises = allDirectories.map(async (dirName) => { const jsonPath = join( directoryPath, dirName, configuration.metadataFileName ) - return readModelMetadata(jsonPath) + return await readModelMetadata(jsonPath) }) const results = await Promise.allSettled(readJsonPromises) @@ -43,8 +44,8 @@ export const getBuilder = async (configuration: RouteConfiguration) => { if (result.status === 'fulfilled') { try { return JSON.parse(result.value) - } catch { - return result.value + } catch (err) { + console.error(err) } } else { console.error(result.reason) @@ -60,8 +61,8 @@ export const getBuilder = async (configuration: RouteConfiguration) => { } } -const readModelMetadata = (path: string) => { - return fs.readFileSync(join(path)) +const readModelMetadata = async (path: string) => { + return fs.readFileSync(path, 'utf-8') } export const retrieveBuilder = async ( @@ -290,42 +291,56 @@ export const downloadModel = async (modelId: string) => { } export const chatCompletions = async (request: any, reply: any) => { - // By default it's Local Nitro server - let apiUrl: string = - 'http://127.0.0.1:3928/inferences/llamacpp/chat_completion' - // getBuilder() => modelList - // Read from engines.json + const modelList = await getBuilder(JanApiRouteConfiguration.models) const modelId = request.body.model - const oaiModelId = [ - 'gpt-4', - 'gpt-3.5-turbo-1106', - 'gpt-3.5-turbo-16k-0613', - 'gpt-4-0314', - 'gpt-3.5-turbo', - 'gpt-4-0613', - 'gpt-3.5-turbo-0301', - ] - if (oaiModelId.includes(modelId)) { - apiUrl = 'https://api.openai.com/v1/chat/completions' - // don't know how to handle engine here, maybe send engine with body? + + 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 } - // Else if modelList.includes(modelId) {} - // With Nitro, add FIFO queue to handle multiple requests at once while loading/ unloading properly - const apiKey = process.env.OPENAI_KEY || 'YOUR_OPENAI_API_KEY' // Replace with your API key - console.log(`chatCompletions: modelId=${modelId} with apiUrl=${apiUrl}`) + + 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 = { + '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: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - 'api-key': '${apiKey}', - }, + headers: headers, body: JSON.stringify(request.body), }) if (response.status !== 200) { @@ -335,3 +350,13 @@ export const chatCompletions = async (request: any, reply: any) => { 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, 'utf8') + return JSON.parse(data) +} diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index ac42537ac5..a25029eaff 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -207,12 +207,8 @@ export default class JanModelExtension implements ModelExtension { 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) => { From e2e382004f72060f57f83cce53058f1348f6576f Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 21 Dec 2023 16:17:22 +0700 Subject: [PATCH 10/16] refactor: move routes to shared node module --- core/rollup.config.ts | 15 ++- core/src/node/api/HttpServer.ts | 8 ++ .../src/node/api/common/builder.ts | 71 ++++-------- .../src/node/api/common/configuration.ts | 0 core/src/node/api/index.ts | 2 + core/src/node/api/routes/common.ts | 31 +++++ core/src/node/api/routes/download.ts | 54 +++++++++ core/src/node/api/routes/extension.ts | 51 +++++++++ core/src/node/api/routes/fs.ts | 23 ++++ core/src/node/api/routes/index.ts | 6 + core/src/node/api/routes/thread.ts | 30 +++++ core/src/node/api/routes/v1.ts | 22 ++++ core/src/node/extension/extension.ts | 108 ++++++++---------- core/src/node/index.ts | 1 + electron/main.ts | 68 +---------- server/main.ts | 4 +- server/v1/download/index.ts | 66 ----------- server/v1/extension/index.ts | 59 ---------- server/v1/fs/index.ts | 34 ------ server/v1/index.ts | 18 --- 20 files changed, 320 insertions(+), 351 deletions(-) create mode 100644 core/src/node/api/HttpServer.ts rename electron/api/models/index.ts => core/src/node/api/common/builder.ts (84%) rename electron/api/index.ts => core/src/node/api/common/configuration.ts (100%) create mode 100644 core/src/node/api/index.ts create mode 100644 core/src/node/api/routes/common.ts create mode 100644 core/src/node/api/routes/download.ts create mode 100644 core/src/node/api/routes/extension.ts create mode 100644 core/src/node/api/routes/fs.ts create mode 100644 core/src/node/api/routes/index.ts create mode 100644 core/src/node/api/routes/thread.ts create mode 100644 core/src/node/api/routes/v1.ts delete mode 100644 server/v1/download/index.ts delete mode 100644 server/v1/extension/index.ts delete mode 100644 server/v1/fs/index.ts delete mode 100644 server/v1/index.ts diff --git a/core/rollup.config.ts b/core/rollup.config.ts index 89f57e35ff..c1616e4c57 100644 --- a/core/rollup.config.ts +++ b/core/rollup.config.ts @@ -40,7 +40,20 @@ export default [ input: `src/node/index.ts`, output: [{ file: 'dist/node/index.cjs', 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'], + external: [ + 'fs/promises', + 'path', + 'pacote', + '@types/pacote', + '@npmcli/arborist', + 'ulid', + 'node-fetch', + 'fs', + 'request', + 'crypto', + 'url', + 'http' + ], watch: { include: 'src/node/**', }, diff --git a/core/src/node/api/HttpServer.ts b/core/src/node/api/HttpServer.ts new file mode 100644 index 0000000000..32d5977175 --- /dev/null +++ b/core/src/node/api/HttpServer.ts @@ -0,0 +1,8 @@ +export interface HttpServer { + post: (route: string, handler: (req: any, res: any) => Promise) => void + get: (route: string, handler: (req: any, res: any) => Promise) => void + patch: (route: string, handler: (req: any, res: any) => Promise) => void + put: (route: string, handler: (req: any, res: any) => Promise) => void + delete: (route: string, handler: (req: any, res: any) => Promise) => void + register: (router: any, opts?: any) => void +} diff --git a/electron/api/models/index.ts b/core/src/node/api/common/builder.ts similarity index 84% rename from electron/api/models/index.ts rename to core/src/node/api/common/builder.ts index 67444f9e11..faeca8e742 100644 --- a/electron/api/models/index.ts +++ b/core/src/node/api/common/builder.ts @@ -1,13 +1,13 @@ -const fs = require('fs') -import { rimraf } from 'rimraf' -import { JanApiRouteConfiguration, RouteConfiguration } from '../index' +import fs from 'fs' +import { JanApiRouteConfiguration, RouteConfiguration } from './configuration' import { join } from 'path' -import { Model, ThreadMessage } from '@janhq/core/dist/types/types' +import { Model, ThreadMessage } from './../../../index' import fetch from 'node-fetch' import { ulid } from 'ulid' -const progress = require('request-progress') import request from 'request' + +const progress = require('request-progress') const os = require('os') const path = join(os.homedir(), 'jan') @@ -30,29 +30,20 @@ export const getBuilder = async (configuration: RouteConfiguration) => { } const readJsonPromises = allDirectories.map(async (dirName) => { - const jsonPath = join( - directoryPath, - dirName, - configuration.metadataFileName - ) + const jsonPath = join(directoryPath, dirName, configuration.metadataFileName) return await readModelMetadata(jsonPath) }) - const results = await Promise.allSettled(readJsonPromises) + const results = await Promise.all(readJsonPromises) const modelData = results - .map((result) => { - if (result.status === 'fulfilled') { - try { - return JSON.parse(result.value) - } catch (err) { - console.error(err) - } - } else { - console.error(result.reason) - return undefined + .map((result: any) => { + try { + return JSON.parse(result) + } catch (err) { + console.error(err) } }) - .filter((e) => !!e) + .filter((e: any) => !!e) return modelData } catch (err) { @@ -65,10 +56,7 @@ const readModelMetadata = async (path: string) => { return fs.readFileSync(path, 'utf-8') } -export const retrieveBuilder = async ( - configuration: RouteConfiguration, - id: string -) => { +export const retrieveBuilder = async (configuration: RouteConfiguration, id: string) => { const data = await getBuilder(configuration) const filteredData = data.filter((d: any) => d.id === id)[0] @@ -79,10 +67,7 @@ export const retrieveBuilder = async ( return filteredData } -export const deleteBuilder = async ( - configuration: RouteConfiguration, - id: string -) => { +export const deleteBuilder = async (configuration: RouteConfiguration, id: string) => { if (configuration.dirName === 'assistants' && id === 'jan') { return { message: 'Cannot delete Jan assistant', @@ -99,7 +84,7 @@ export const deleteBuilder = async ( } const myPath = join(directoryPath, id) - rimraf.sync(myPath) + fs.rmdirSync(myPath, { recursive: true }) return { id: id, object: configuration.delete.object, @@ -174,10 +159,7 @@ export const createThread = async (thread: any) => { fs.mkdirSync(threadDirPath) } - await fs.writeFileSync( - threadJsonPath, - JSON.stringify(updatedThread, null, 2) - ) + await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2)) return updatedThread } catch (err) { return { @@ -188,10 +170,7 @@ export const createThread = async (thread: any) => { export const updateThread = async (threadId: string, thread: any) => { const threadMetadataFileName = 'thread.json' - const currentThreadData = await retrieveBuilder( - JanApiRouteConfiguration.threads, - threadId - ) + const currentThreadData = await retrieveBuilder(JanApiRouteConfiguration.threads, threadId) if (!currentThreadData) { return { message: 'Thread not found', @@ -210,10 +189,7 @@ export const updateThread = async (threadId: string, thread: any) => { const threadDirPath = join(path, 'threads', updatedThread.id) const threadJsonPath = join(threadDirPath, threadMetadataFileName) - await fs.writeFileSync( - threadJsonPath, - JSON.stringify(updatedThread, null, 2) - ) + await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2)) return updatedThread } catch (err) { return { @@ -309,13 +285,10 @@ export const chatCompletions = async (request: any, reply: any) => { } const requestedModel = matchedModels[0] - const engineConfiguration = await getEngineConfiguration( - requestedModel.engine - ) + 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 + let apiUrl: string = 'http://127.0.0.1:3928/inferences/llamacpp/chat_completion' // default nitro url if (engineConfiguration) { apiKey = engineConfiguration.api_key @@ -325,7 +298,7 @@ export const chatCompletions = async (request: any, reply: any) => { reply.raw.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', + Connection: 'keep-alive', }) const headers: Record = { diff --git a/electron/api/index.ts b/core/src/node/api/common/configuration.ts similarity index 100% rename from electron/api/index.ts rename to core/src/node/api/common/configuration.ts diff --git a/core/src/node/api/index.ts b/core/src/node/api/index.ts new file mode 100644 index 0000000000..4c3041ba3f --- /dev/null +++ b/core/src/node/api/index.ts @@ -0,0 +1,2 @@ +export * from './HttpServer' +export * from './routes' diff --git a/core/src/node/api/routes/common.ts b/core/src/node/api/routes/common.ts new file mode 100644 index 0000000000..55cf5b4a30 --- /dev/null +++ b/core/src/node/api/routes/common.ts @@ -0,0 +1,31 @@ +import { HttpServer } from '../HttpServer' +import { + chatCompletions, + deleteBuilder, + downloadModel, + getBuilder, + retrieveBuilder, +} from '../common/builder' + +import { JanApiRouteConfiguration } from '../common/configuration' + +export const commonRouter = async (app: HttpServer) => { + 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), + ) + }) + + app.get(`/models/download/:modelId`, async (request: any) => + downloadModel(request.params.modelId), + ) + + // Endpoints + app.post(`/chat/completions`, async (request: any, reply: any) => chatCompletions(request, reply)) +} diff --git a/core/src/node/api/routes/download.ts b/core/src/node/api/routes/download.ts new file mode 100644 index 0000000000..26945bada3 --- /dev/null +++ b/core/src/node/api/routes/download.ts @@ -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() + }) +} diff --git a/core/src/node/api/routes/extension.ts b/core/src/node/api/routes/extension.ts new file mode 100644 index 0000000000..19f717d5f0 --- /dev/null +++ b/core/src/node/api/routes/extension.ts @@ -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 = require(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.`) + } + }) +} diff --git a/core/src/node/api/routes/fs.ts b/core/src/node/api/routes/fs.ts new file mode 100644 index 0000000000..08f113a95b --- /dev/null +++ b/core/src/node/api/routes/fs.ts @@ -0,0 +1,23 @@ +import { FileSystemRoute } from '../../../api' +import { join } from 'path' +import { HttpServer, userSpacePath } from '../../index' +const fs = require('fs') + +export const fsRouter = async (app: HttpServer) => { + // 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 fs[route]( + ...body.map((arg: any) => + arg.includes('file:/') ? join(userSpacePath, arg.replace('file:/', '')) : arg, + ), + ) + res.status(200).send(result) + } catch (ex) { + console.log(ex) + } + }) + }) +} diff --git a/core/src/node/api/routes/index.ts b/core/src/node/api/routes/index.ts new file mode 100644 index 0000000000..e6edc62f7c --- /dev/null +++ b/core/src/node/api/routes/index.ts @@ -0,0 +1,6 @@ +export * from './download' +export * from './extension' +export * from './fs' +export * from './thread' +export * from './common' +export * from './v1' diff --git a/core/src/node/api/routes/thread.ts b/core/src/node/api/routes/thread.ts new file mode 100644 index 0000000000..4066d27165 --- /dev/null +++ b/core/src/node/api/routes/thread.ts @@ -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), + ) +} diff --git a/core/src/node/api/routes/v1.ts b/core/src/node/api/routes/v1.ts new file mode 100644 index 0000000000..aaf98683ad --- /dev/null +++ b/core/src/node/api/routes/v1.ts @@ -0,0 +1,22 @@ +import { HttpServer } from '../HttpServer' +import { commonRouter, threadRouter, fsRouter, extensionRouter, downloadRouter } from './index' + +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', + }) +} +export default v1Router diff --git a/core/src/node/extension/extension.ts b/core/src/node/extension/extension.ts index 9dbd74e880..0460eca5e1 100644 --- a/core/src/node/extension/extension.ts +++ b/core/src/node/extension/extension.ts @@ -1,8 +1,8 @@ -import { rmdirSync } from "fs"; -import { resolve, join } from "path"; -import { manifest, extract } from "pacote"; -import * as Arborist from "@npmcli/arborist"; -import { ExtensionManager, userSpacePath } from "./manager"; +import { rmdirSync } from 'fs' +import { resolve, join } from 'path' +import { manifest, extract } from 'pacote' +import * as Arborist from '@npmcli/arborist' +import { ExtensionManager } from './manager' /** * An NPM package that can be used as an extension. @@ -18,22 +18,22 @@ export default class Extension { * @property {string} main The entry point as defined in the main entry of the manifest. * @property {string} description The description of extension as defined in the manifest. */ - origin?: string; - installOptions: any; - name?: string; - url?: string; - version?: string; - main?: string; - description?: string; + origin?: string + installOptions: any + name?: string + url?: string + version?: string + main?: string + description?: string /** @private */ - _active = false; + _active = false /** * @private * @property {Object.} #listeners A list of callbacks to be executed when the Extension is updated. */ - listeners: Record void> = {}; + listeners: Record void> = {} /** * Set installOptions with defaults for options that have not been provided. @@ -45,10 +45,10 @@ export default class Extension { version: false, fullMetadata: false, Arborist, - }; + } - this.origin = origin; - this.installOptions = { ...defaultOpts, ...options }; + this.origin = origin + this.installOptions = { ...defaultOpts, ...options } } /** @@ -56,10 +56,7 @@ export default class Extension { * @type {string} */ get specifier() { - return ( - this.origin + - (this.installOptions.version ? "@" + this.installOptions.version : "") - ); + return this.origin + (this.installOptions.version ? '@' + this.installOptions.version : '') } /** @@ -67,7 +64,7 @@ export default class Extension { * @type {boolean} */ get active() { - return this._active; + return this._active } /** @@ -77,20 +74,18 @@ export default class Extension { async getManifest() { // Get the package's manifest (package.json object) try { - const mnf = await manifest(this.specifier, this.installOptions); + const mnf = await manifest(this.specifier, this.installOptions) // set the Package properties based on the it's manifest - this.name = mnf.name; - this.version = mnf.version; - this.main = mnf.main; - this.description = mnf.description; + this.name = mnf.name + this.version = mnf.version + 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; + return true } /** @@ -101,26 +96,26 @@ export default class Extension { async _install() { try { // import the manifest details - await this.getManifest(); + await this.getManifest() // Install the package in a child folder of the given folder await extract( this.specifier, - join(ExtensionManager.instance.extensionsPath ?? "", this.name ?? ""), - this.installOptions - ); + join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''), + this.installOptions, + ) // Set the url using the custom extensions protocol - this.url = `extension://${this.name}/${this.main}`; + this.url = `extension://${this.name}/${this.main}` - this.emitUpdate(); + this.emitUpdate() } catch (err) { // Ensure the extension is not stored and the folder is removed if the installation fails - this.setActive(false); - throw err; + this.setActive(false) + throw err } - return [this]; + return [this] } /** @@ -129,7 +124,7 @@ export default class Extension { * @param {callback} cb The function to execute on update */ subscribe(name: string, cb: () => void) { - this.listeners[name] = cb; + this.listeners[name] = cb } /** @@ -137,7 +132,7 @@ export default class Extension { * @param {string} name name of the callback to remove */ unsubscribe(name: string) { - delete this.listeners[name]; + delete this.listeners[name] } /** @@ -145,7 +140,7 @@ export default class Extension { */ emitUpdate() { for (const cb in this.listeners) { - this.listeners[cb].call(null, this); + this.listeners[cb].call(null, this) } } @@ -156,12 +151,12 @@ export default class Extension { */ async update(version = false) { if (await this.isUpdateAvailable()) { - this.installOptions.version = version; - await this._install(); - return true; + this.installOptions.version = version + await this._install() + return true } - return false; + return false } /** @@ -170,8 +165,8 @@ export default class Extension { */ async isUpdateAvailable() { if (this.origin) { - const mnf = await manifest(this.origin); - return mnf.version !== this.version ? mnf.version : false; + const mnf = await manifest(this.origin) + return mnf.version !== this.version ? mnf.version : false } } @@ -180,13 +175,10 @@ export default class Extension { * @returns {Promise} */ async uninstall() { - const extPath = resolve( - ExtensionManager.instance.extensionsPath ?? "", - this.name ?? "" - ); - await rmdirSync(extPath, { recursive: true }); + const extPath = resolve(ExtensionManager.instance.extensionsPath ?? '', this.name ?? '') + await rmdirSync(extPath, { recursive: true }) - this.emitUpdate(); + this.emitUpdate() } /** @@ -195,8 +187,8 @@ export default class Extension { * @returns {Extension} This extension */ setActive(active: boolean) { - this._active = active; - this.emitUpdate(); - return this; + this._active = active + this.emitUpdate() + return this } } diff --git a/core/src/node/index.ts b/core/src/node/index.ts index 081a94358c..49c2c3c26e 100644 --- a/core/src/node/index.ts +++ b/core/src/node/index.ts @@ -4,3 +4,4 @@ export * from './extension/manager' export * from './extension/store' export * from './download' export * from './module' +export * from './api' diff --git a/electron/main.ts b/electron/main.ts index 3a9556cbca..5e7a6807cd 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -11,6 +11,7 @@ import { WindowManager } from './managers/window' const { ExtensionManager, ModuleManager, + threadRouter, } = require('@janhq/core/dist/node/index.cjs') /** @@ -21,22 +22,7 @@ import { handleExtensionIPCs } from './handlers/extension' import { handleAppIPCs } from './handlers/app' import { handleAppUpdates } from './handlers/update' import { handleFsIPCs } from './handlers/fs' -import { - createMessage, - createThread, - deleteBuilder, - downloadModel, - getBuilder, - getMessages, - retrieveBuilder, - retrieveMesasge, - updateThread, - chatCompletions, -} from './api/models' -import { JanApiRouteConfiguration } from './api' - -// TODO: refactor this, this API piece of code should not belong here -const version = 'v1' +const { v1Router } = require('@janhq/core/dist/node/index.cjs') const fastify = Fastify({ logger: true, @@ -49,56 +35,10 @@ fastify.listen({ port: 1337 }, function (err, address) { } }) -Object.keys(JanApiRouteConfiguration).forEach((key) => { - fastify.get(`/${version}/${key}`, async (_request) => - getBuilder(JanApiRouteConfiguration[key]) - ) - - fastify.get(`/${version}/${key}/:id`, async (request: any) => - retrieveBuilder(JanApiRouteConfiguration[key], request.params.id) - ) - - fastify.delete(`/${version}/${key}/:id`, async (request: any) => - deleteBuilder(JanApiRouteConfiguration[key], request.params.id) - ) +fastify.register(v1Router, { + prefix: '/api/v1', }) -// get messages of thread id -fastify.get(`/${version}/threads/:threadId/messages`, async (request: any) => - getMessages(request.params.threadId) -) - -// retrieve message -fastify.get( - `/${version}/threads/:threadId/messages/:messageId`, - async (request: any) => - retrieveMesasge(request.params.threadId, request.params.messageId) -) - -// create thread -fastify.post(`/${version}/threads`, async (request: any) => - createThread(request.body) -) - -// create message -fastify.post(`/${version}/threads/:threadId/messages`, async (request: any) => - createMessage(request.params.threadId, request.body) -) - -// modify thread -fastify.patch(`/${version}/threads/:threadId`, async (request: any) => - updateThread(request.params.threadId, request.body) -) - -fastify.get(`/${version}/models/download/:modelId`, async (request: any) => - downloadModel(request.params.modelId) -) - -// Endpoints -fastify.post(`/${version}/chat/completions`, async (request: any, reply: any) => - chatCompletions(request, reply) -) - app .whenReady() .then(createUserSpace) diff --git a/server/main.ts b/server/main.ts index d485089141..5442081936 100644 --- a/server/main.ts +++ b/server/main.ts @@ -1,6 +1,6 @@ import fastify from "fastify"; import dotenv from "dotenv"; -import v1API from "./v1"; +const { v1Router } = require("@janhq/core/dist/node/index.cjs"); dotenv.config(); @@ -20,7 +20,7 @@ server.register( }, { prefix: "extensions" } ); -server.register(v1API, { prefix: "/api/v1" }); +server.register(v1Router, { prefix: "/api/v1" }); server .listen({ diff --git a/server/v1/download/index.ts b/server/v1/download/index.ts deleted file mode 100644 index a19b4220d4..0000000000 --- a/server/v1/download/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - FastifyInstance, - FastifyPluginAsync, - FastifyPluginOptions, -} from "fastify"; -import { DownloadRoute } from "@janhq/core"; -import { join } from "path"; -const { - userSpacePath, - DownloadManager, -} = require("@janhq/core/dist/node/index.cjs"); -const request = require("request"); -const progress = require("request-progress"); -const { createWriteStream } = require("fs"); - -const router: FastifyPluginAsync = async ( - app: FastifyInstance, - opts: FastifyPluginOptions -) => { - 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(); - }); -}; - -export default router; diff --git a/server/v1/extension/index.ts b/server/v1/extension/index.ts deleted file mode 100644 index c2c646cc4e..0000000000 --- a/server/v1/extension/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - FastifyInstance, - FastifyPluginAsync, - FastifyPluginOptions, -} from "fastify"; -import { join, extname } from "path"; -import { ExtensionRoute } from "@janhq/core"; -const { - userSpacePath, - ModuleManager, -} = require("@janhq/core/dist/node/index.cjs"); -import { readdirSync } from "fs"; - -const node = require("@janhq/core/dist/node/index.cjs"); - -const router: FastifyPluginAsync = async ( - app: FastifyInstance, - opts: FastifyPluginOptions -) => { - // TODO: Share code between node projects - app.post(`/${ExtensionRoute.getActiveExtensions}`, async (req, res) => { - const activeExtensions = await node.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 node.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 = require(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.`); - } - }); -}; -export default router; diff --git a/server/v1/fs/index.ts b/server/v1/fs/index.ts deleted file mode 100644 index 99f7de415e..0000000000 --- a/server/v1/fs/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - FastifyInstance, - FastifyPluginAsync, - FastifyPluginOptions, -} from "fastify"; -import { FileSystemRoute } from "@janhq/core"; -import { join } from "path"; -const { userSpacePath } = require("@janhq/core/dist/node/index.cjs"); -const fs = require("fs"); - -const router: FastifyPluginAsync = async ( - app: FastifyInstance, - opts: FastifyPluginOptions -) => { - // 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 fs[route]( - ...body.map((arg: any) => - arg.includes("file:/") - ? join(userSpacePath, arg.replace("file:/", "")) - : arg - ) - ); - res.status(200).send(result); - } catch (ex) { - console.log(ex); - } - }); - }); -}; -export default router; diff --git a/server/v1/index.ts b/server/v1/index.ts deleted file mode 100644 index 64f52ffde4..0000000000 --- a/server/v1/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import fsAPI from "./fs"; -import extAPI from "./extension"; -import downloadAPI from "./download"; - -import { FastifyInstance, FastifyPluginAsync } from "fastify"; - -const router: FastifyPluginAsync = async (app: FastifyInstance, opts) => { - app.register(fsAPI, { - prefix: "/fs", - }); - app.register(extAPI, { - prefix: "/extension", - }); - app.register(downloadAPI, { - prefix: "/download", - }); -}; -export default router; From eab0885f6e135314b31e7d42827fcea5a3e6d383 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 22 Dec 2023 21:24:42 +0700 Subject: [PATCH 11/16] refactor: exported modules from core with types (#1172) * refactor: exported modules from core with types * fix: fix file reading args --- core/package.json | 21 +++++++++++++++++++ core/rollup.config.ts | 4 ++-- core/src/node/api/common/builder.ts | 2 +- core/src/node/api/routes/extension.ts | 2 +- core/src/node/api/routes/fs.ts | 5 +++-- core/src/node/api/routes/v1.ts | 3 +-- core/src/node/extension/index.ts | 19 +++++++++-------- core/src/node/extension/manager.ts | 3 ++- core/tsconfig.json | 2 +- electron/handlers/app.ts | 4 ++-- electron/handlers/download.ts | 5 ++++- electron/handlers/extension.ts | 6 +++--- electron/handlers/fs.ts | 7 ++++--- electron/main.ts | 11 ++++------ electron/tsconfig.json | 3 +++ electron/utils/menu.ts | 3 +-- extensions/assistant-extension/src/index.ts | 4 ++-- .../conversational-extension/src/index.ts | 5 ++--- extensions/model-extension/src/index.ts | 4 ++-- server/main.ts | 2 +- 20 files changed, 70 insertions(+), 45 deletions(-) diff --git a/core/package.json b/core/package.json index a599fc3c2f..ce0714c277 100644 --- a/core/package.json +++ b/core/package.json @@ -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", diff --git a/core/rollup.config.ts b/core/rollup.config.ts index c1616e4c57..d982db6597 100644 --- a/core/rollup.config.ts +++ b/core/rollup.config.ts @@ -38,7 +38,7 @@ export default [ }, { input: `src/node/index.ts`, - output: [{ file: 'dist/node/index.cjs', format: 'cjs', sourcemap: true }], + 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', @@ -52,7 +52,7 @@ export default [ 'request', 'crypto', 'url', - 'http' + 'http', ], watch: { include: 'src/node/**', diff --git a/core/src/node/api/common/builder.ts b/core/src/node/api/common/builder.ts index faeca8e742..85c9c88516 100644 --- a/core/src/node/api/common/builder.ts +++ b/core/src/node/api/common/builder.ts @@ -330,6 +330,6 @@ const getEngineConfiguration = async (engineId: string) => { } const directoryPath = join(path, 'engines') const filePath = join(directoryPath, `${engineId}.json`) - const data = await fs.readFileSync(filePath, 'utf8') + const data = await fs.readFileSync(filePath, 'utf-8') return JSON.parse(data) } diff --git a/core/src/node/api/routes/extension.ts b/core/src/node/api/routes/extension.ts index 19f717d5f0..abd6a6f0c1 100644 --- a/core/src/node/api/routes/extension.ts +++ b/core/src/node/api/routes/extension.ts @@ -34,7 +34,7 @@ export const extensionRouter = async (app: HttpServer) => { app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => { const args = JSON.parse(req.body as any) console.debug(args) - const module = require(join(userSpacePath, 'extensions', args[0])) + const module = await import(join(userSpacePath, 'extensions', args[0])) ModuleManager.instance.setModule(args[0], module) const method = args[1] diff --git a/core/src/node/api/routes/fs.ts b/core/src/node/api/routes/fs.ts index 08f113a95b..b4934604b4 100644 --- a/core/src/node/api/routes/fs.ts +++ b/core/src/node/api/routes/fs.ts @@ -1,19 +1,20 @@ import { FileSystemRoute } from '../../../api' import { join } from 'path' import { HttpServer, userSpacePath } from '../../index' -const fs = require('fs') 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 fs[route]( + const result = await import(moduleName).then(mdl => { return mdl[route]( ...body.map((arg: any) => arg.includes('file:/') ? join(userSpacePath, arg.replace('file:/', '')) : arg, ), ) + }) res.status(200).send(result) } catch (ex) { console.log(ex) diff --git a/core/src/node/api/routes/v1.ts b/core/src/node/api/routes/v1.ts index aaf98683ad..4f637e3922 100644 --- a/core/src/node/api/routes/v1.ts +++ b/core/src/node/api/routes/v1.ts @@ -1,7 +1,7 @@ import { HttpServer } from '../HttpServer' import { commonRouter, threadRouter, fsRouter, extensionRouter, downloadRouter } from './index' -const v1Router = async (app: HttpServer) => { +export const v1Router = async (app: HttpServer) => { // MARK: External Routes app.register(commonRouter) app.register(threadRouter, { @@ -19,4 +19,3 @@ const v1Router = async (app: HttpServer) => { prefix: '/download', }) } -export default v1Router diff --git a/core/src/node/extension/index.ts b/core/src/node/extension/index.ts index 538a91f9fa..e685fc0ae3 100644 --- a/core/src/node/extension/index.ts +++ b/core/src/node/extension/index.ts @@ -1,13 +1,5 @@ import { readFileSync } from 'fs' -let electron: any = undefined - -try { - electron = require('electron') -} catch (err) { - console.error('Electron is not available') -} - import { normalize } from 'path' import Extension from './extension' @@ -39,7 +31,16 @@ export function init(options: any) { * @private * @returns {boolean} Whether the protocol registration was successful */ -function registerExtensionProtocol() { +async function registerExtensionProtocol() { + let electron: any = undefined + + try { + const moduleName = "electron" + electron = await import(moduleName) + } catch (err) { + console.error('Electron is not available') + } + if (electron) { return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => { const entry = request.url.substr('extension://'.length - 1) diff --git a/core/src/node/extension/manager.ts b/core/src/node/extension/manager.ts index ea4a6faf27..9173504a7c 100644 --- a/core/src/node/extension/manager.ts +++ b/core/src/node/extension/manager.ts @@ -2,11 +2,12 @@ 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(require("os").homedir(), "jan"); +export const userSpacePath = join(homedir(), "jan"); export class ExtensionManager { public static instance: ExtensionManager = new ExtensionManager(); diff --git a/core/tsconfig.json b/core/tsconfig.json index 19b2d29ad0..81991f46ad 100644 --- a/core/tsconfig.json +++ b/core/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "moduleResolution": "node", "target": "es5", - "module": "es2015", + "module": "ES2020", "lib": ["es2015", "es2016", "es2017", "dom"], "strict": true, "sourceMap": true, diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts index 67e2fce566..d9c3296f28 100644 --- a/electron/handlers/app.ts +++ b/electron/handlers/app.ts @@ -4,10 +4,10 @@ import { WindowManager } from './../managers/window' import { userSpacePath } from './../utils/path' import { AppRoute } from '@janhq/core' import { getResourcePath } from './../utils/path' -const { +import { ExtensionManager, ModuleManager, -} = require('@janhq/core/dist/node/index.cjs') +} from '@janhq/core/node' export function handleAppIPCs() { /** diff --git a/electron/handlers/download.ts b/electron/handlers/download.ts index 81f5181f59..9c49b44f34 100644 --- a/electron/handlers/download.ts +++ b/electron/handlers/download.ts @@ -5,7 +5,7 @@ import request from 'request' import { createWriteStream, renameSync } from 'fs' import { DownloadEvent, DownloadRoute } from '@janhq/core' const progress = require('request-progress') -const { DownloadManager } = require('@janhq/core/dist/node/index.cjs') +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 diff --git a/electron/handlers/extension.ts b/electron/handlers/extension.ts index ab8a36f3a6..ad8b59a99a 100644 --- a/electron/handlers/extension.ts +++ b/electron/handlers/extension.ts @@ -1,14 +1,14 @@ import { ipcMain, webContents } from 'electron' import { readdirSync } from 'fs' import { join, extname } from 'path' -const { ModuleManager } = require('@janhq/core/dist/node/index.cjs') -const { +import { installExtensions, getExtension, removeExtension, getActiveExtensions, -} = require('@janhq/core/dist/node/index.cjs') + ModuleManager +} from '@janhq/core/node' import { getResourcePath, userSpacePath } from './../utils/path' import { ExtensionRoute } from '@janhq/core' diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index 8bb7604919..4b5fc2cc90 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -3,19 +3,20 @@ import { ipcMain } from 'electron' import { FileSystemRoute } from '@janhq/core' import { userSpacePath } from '../utils/path' import { join } from 'path' -const fs = require('fs') /** * Handles file system operations. */ export function handleFsIPCs() { + const moduleName = "fs" Object.values(FileSystemRoute).forEach((route) => { ipcMain.handle(route, async (event, ...args) => { - return fs[route]( + return import(moduleName).then(mdl => + mdl[route]( ...args.map((arg) => arg.includes('file:/') ? join(userSpacePath, arg.replace('file:/', '')) : arg - ) + )) ) }) }) diff --git a/electron/main.ts b/electron/main.ts index 5e7a6807cd..6af3190a81 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -8,11 +8,7 @@ import Fastify from 'fastify' * Managers **/ import { WindowManager } from './managers/window' -const { - ExtensionManager, - ModuleManager, - threadRouter, -} = require('@janhq/core/dist/node/index.cjs') +import { ExtensionManager, ModuleManager } from '@janhq/core/node' /** * IPC Handlers @@ -22,7 +18,8 @@ import { handleExtensionIPCs } from './handlers/extension' import { handleAppIPCs } from './handlers/app' import { handleAppUpdates } from './handlers/update' import { handleFsIPCs } from './handlers/fs' -const { v1Router } = require('@janhq/core/dist/node/index.cjs') +import { migrateExtensions } from './utils/migration' +import { v1Router } from '@janhq/core/node' const fastify = Fastify({ logger: true, @@ -42,7 +39,7 @@ fastify.register(v1Router, { app .whenReady() .then(createUserSpace) - .then(ExtensionManager.instance.migrateExtensions) + .then(migrateExtensions) .then(ExtensionManager.instance.setupExtensions) .then(setupMenu) .then(handleIPCs) diff --git a/electron/tsconfig.json b/electron/tsconfig.json index 3cc218f93e..11c9d85770 100644 --- a/electron/tsconfig.json +++ b/electron/tsconfig.json @@ -15,6 +15,9 @@ "paths": { "*": ["node_modules/*"] }, "typeRoots": ["node_modules/@types"] }, + "ts-node": { + "esm": true + }, "include": ["./**/*.ts"], "exclude": ["core", "build", "dist", "tests", "node_modules"] } diff --git a/electron/utils/menu.ts b/electron/utils/menu.ts index 65e009aefc..97a2afd984 100644 --- a/electron/utils/menu.ts +++ b/electron/utils/menu.ts @@ -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/"); }, }, diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts index 24285e671c..92ca8f2fda 100644 --- a/extensions/assistant-extension/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -20,7 +20,7 @@ export default class JanAssistantExtension implements AssistantExtension { /** * Called when the extension is unloaded. */ - onUnload(): void {} + onUnload(): void { } async createAssistant(assistant: Assistant): Promise { const assistantDir = join(JanAssistantExtension._homeDir, assistant.id); @@ -57,7 +57,7 @@ export default class JanAssistantExtension implements AssistantExtension { continue; } - const content = await fs.readFileSync(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); diff --git a/extensions/conversational-extension/src/index.ts b/extensions/conversational-extension/src/index.ts index cfea275698..7a76a7ed9e 100644 --- a/extensions/conversational-extension/src/index.ts +++ b/extensions/conversational-extension/src/index.ts @@ -7,8 +7,7 @@ import { Thread, ThreadMessage } from '@janhq/core' * functionality for managing threads. */ export default class JSONConversationalExtension - implements ConversationalExtension -{ + implements ConversationalExtension { private static readonly _homeDir = 'file://threads' private static readonly _threadInfoFileName = 'thread.json' private static readonly _threadMessagesFileName = 'messages.jsonl' @@ -208,7 +207,7 @@ export default class JSONConversationalExtension JSONConversationalExtension._threadMessagesFileName, ]) - const result = await fs.readFileSync(messageFilePath).then((content) => + const result = await fs.readFileSync(messageFilePath, 'utf-8').then((content) => content .toString() .split('\n') diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index a25029eaff..1e30b96447 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -42,7 +42,7 @@ export default class JanModelExtension implements ModelExtension { * Called when the extension is unloaded. * @override */ - onUnload(): void {} + onUnload(): void { } private async copyModelsToHomeDir() { try { @@ -248,7 +248,7 @@ export default class JanModelExtension implements ModelExtension { } private readModelMetadata(path: string) { - return fs.readFileSync(path) + return fs.readFileSync(join(path), 'utf-8') } /** diff --git a/server/main.ts b/server/main.ts index 5442081936..a3a349bf2e 100644 --- a/server/main.ts +++ b/server/main.ts @@ -1,6 +1,6 @@ import fastify from "fastify"; import dotenv from "dotenv"; -const { v1Router } = require("@janhq/core/dist/node/index.cjs"); +import { v1Router } from '@janhq/core/node' dotenv.config(); From dc61b5f591a11c267f32062fda57ae1c05e161a0 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 25 Dec 2023 11:24:54 +0700 Subject: [PATCH 12/16] refactor: fileManager handles --- core/src/api/index.ts | 16 +++++--- core/src/fs.ts | 7 ++++ electron/handlers/app.ts | 4 -- electron/handlers/fileManager.ts | 37 +++++++++++++++++++ electron/handlers/fs.ts | 17 +++++---- electron/main.ts | 2 + .../src/index.ts | 8 ++-- web/hooks/useEngineSettings.ts | 4 +- 8 files changed, 72 insertions(+), 23 deletions(-) create mode 100644 electron/handlers/fileManager.ts diff --git a/core/src/api/index.ts b/core/src/api/index.ts index 79527289c5..23d5f6f874 100644 --- a/core/src/api/index.ts +++ b/core/src/api/index.ts @@ -5,7 +5,6 @@ export enum AppRoute { appDataPath = 'appDataPath', appVersion = 'appVersion', - getResourcePath = 'getResourcePath', openExternalUrl = 'openExternalUrl', openAppDirectory = 'openAppDirectory', openFileExplore = 'openFileExplorer', @@ -50,10 +49,11 @@ export enum FileSystemRoute { readFileSync = 'readFileSync', rmdirSync = 'rmdirSync', writeFileSync = 'writeFileSync', - getResourcePath = 'getResourcePath', - - isDirectory = 'isDirectory', +} +export enum FileManagerRoute { + synceFile = 'syncFile', getUserSpace = 'getUserSpace', + getResourcePath = 'getResourcePath', } export type ApiFunction = (...args: any[]) => any @@ -82,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)] diff --git a/core/src/fs.ts b/core/src/fs.ts index 80f3117fd9..34d915ef15 100644 --- a/core/src/fs.ts +++ b/core/src/fs.ts @@ -44,8 +44,15 @@ const unlinkSync = (...args: any[]) => global.core.api?.unlinkSync(...args) */ 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} - A promise that resolves when the file has been successfully synchronized. + */ const syncFile: (src: string, dest: string) => Promise = (src, dest) => global.core.api?.syncFile(src, dest) + /** * Copy file sync. */ diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts index d9c3296f28..2966ef888d 100644 --- a/electron/handlers/app.ts +++ b/electron/handlers/app.ts @@ -28,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. diff --git a/electron/handlers/fileManager.ts b/electron/handlers/fileManager.ts new file mode 100644 index 0000000000..5dc4483443 --- /dev/null +++ b/electron/handlers/fileManager.ts @@ -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 => 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() + }) +} diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index 4b5fc2cc90..7e92fc0f12 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -7,16 +7,17 @@ import { join } from 'path' * Handles file system operations. */ export function handleFsIPCs() { - const moduleName = "fs" + const moduleName = 'fs' Object.values(FileSystemRoute).forEach((route) => { ipcMain.handle(route, async (event, ...args) => { - return import(moduleName).then(mdl => - mdl[route]( - ...args.map((arg) => - arg.includes('file:/') - ? join(userSpacePath, arg.replace('file:/', '')) - : arg - )) + return import(moduleName).then((mdl) => + mdl[route]( + ...args.map((arg) => + arg.includes('file:/') + ? join(userSpacePath, arg.replace('file:/', '')) + : arg + ) + ) ) }) }) diff --git a/electron/main.ts b/electron/main.ts index 6af3190a81..b9861395b8 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -15,6 +15,7 @@ import { ExtensionManager, ModuleManager } from '@janhq/core/node' **/ 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' @@ -97,4 +98,5 @@ function handleIPCs() { handleDownloaderIPCs() handleExtensionIPCs() handleAppIPCs() + handleFileMangerIPCs() } diff --git a/extensions/inference-triton-trtllm-extension/src/index.ts b/extensions/inference-triton-trtllm-extension/src/index.ts index 0720ed3acc..0970b19345 100644 --- a/extensions/inference-triton-trtllm-extension/src/index.ts +++ b/extensions/inference-triton-trtllm-extension/src/index.ts @@ -58,7 +58,7 @@ export default class JanInferenceTritonTrtLLMExtension * Subscribes to events emitted by the @janhq/core package. */ onLoad(): void { - fs.mkdir(JanInferenceTritonTrtLLMExtension._homeDir); + fs.mkdirSync(JanInferenceTritonTrtLLMExtension._homeDir); JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings(); // Events subscription @@ -98,12 +98,12 @@ export default class JanInferenceTritonTrtLLMExtension JanInferenceTritonTrtLLMExtension._homeDir, JanInferenceTritonTrtLLMExtension._engineMetadataFileName ); - if (await fs.exists(engine_json)) { + if (await fs.existsSync(engine_json)) { JanInferenceTritonTrtLLMExtension._engineSettings = JSON.parse( - await fs.readFile(engine_json) + await fs.readFileSync(engine_json) ); } else { - await fs.writeFile( + await fs.writeFileSync( engine_json, JSON.stringify( JanInferenceTritonTrtLLMExtension._engineSettings, diff --git a/web/hooks/useEngineSettings.ts b/web/hooks/useEngineSettings.ts index 14f32d4b47..3ec1bf93e7 100644 --- a/web/hooks/useEngineSettings.ts +++ b/web/hooks/useEngineSettings.ts @@ -2,7 +2,7 @@ import { fs, joinPath } from '@janhq/core' export const useEngineSettings = () => { const readOpenAISettings = async () => { - const settings = await fs.readFile( + const settings = await fs.readFileSync( await joinPath(['engines', 'openai.json']) ) if (settings) { @@ -17,7 +17,7 @@ export const useEngineSettings = () => { }) => { const settings = await readOpenAISettings() settings.api_key = apiKey - await fs.writeFile( + await fs.writeFileSync( await joinPath(['engines', 'openai.json']), JSON.stringify(settings) ) From 0c02e246678c842b0207350470ae7dae65c54250 Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 26 Dec 2023 16:01:05 +0700 Subject: [PATCH 13/16] fix: app issues with server refactoring --- core/src/node/api/common/builder.ts | 2 +- core/src/node/api/routes/fs.ts | 15 +++++++----- electron/handlers/fs.ts | 2 +- extensions/assistant-extension/src/index.ts | 8 +++++-- .../conversational-extension/src/index.ts | 24 ++++++++++++------- .../inference-nitro-extension/src/index.ts | 7 +++--- .../inference-nitro-extension/src/module.ts | 4 ++++ .../inference-openai-extension/src/index.ts | 11 +++------ .../src/index.ts | 12 +++++----- web/hooks/useEngineSettings.ts | 8 ++++--- web/hooks/useSendChatMessage.ts | 2 +- 11 files changed, 54 insertions(+), 41 deletions(-) diff --git a/core/src/node/api/common/builder.ts b/core/src/node/api/common/builder.ts index 85c9c88516..4081e859f8 100644 --- a/core/src/node/api/common/builder.ts +++ b/core/src/node/api/common/builder.ts @@ -107,7 +107,7 @@ export const getMessages = async (threadId: string) => { const messageFilePath = join(threadDirPath, messageFile) const lines = fs - .readFileSync(messageFilePath) + .readFileSync(messageFilePath, 'utf-8') .toString() .split('\n') .filter((line: any) => line !== '') diff --git a/core/src/node/api/routes/fs.ts b/core/src/node/api/routes/fs.ts index b4934604b4..f0cf1eeedc 100644 --- a/core/src/node/api/routes/fs.ts +++ b/core/src/node/api/routes/fs.ts @@ -3,17 +3,20 @@ import { join } from 'path' import { HttpServer, userSpacePath } from '../../index' export const fsRouter = async (app: HttpServer) => { - const moduleName = "fs" + 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) => - arg.includes('file:/') ? join(userSpacePath, arg.replace('file:/', '')) : arg, - ), - ) + 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) { diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index 7e92fc0f12..fdfaba6063 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -13,7 +13,7 @@ export function handleFsIPCs() { return import(moduleName).then((mdl) => mdl[route]( ...args.map((arg) => - arg.includes('file:/') + typeof arg === 'string' && arg.includes('file:/') ? join(userSpacePath, arg.replace('file:/', '')) : arg ) diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts index 92ca8f2fda..0c5745b3d9 100644 --- a/extensions/assistant-extension/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -20,7 +20,7 @@ export default class JanAssistantExtension implements AssistantExtension { /** * Called when the extension is unloaded. */ - onUnload(): void { } + onUnload(): void {} async createAssistant(assistant: Assistant): Promise { const assistantDir = join(JanAssistantExtension._homeDir, assistant.id); @@ -48,6 +48,7 @@ export default class JanAssistantExtension implements AssistantExtension { for (const fileName of allFileName) { const filePath = join(JanAssistantExtension._homeDir, fileName); + if (filePath.includes(".DS_Store")) continue; const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter( (file: string) => file === "assistant.json" ); @@ -57,7 +58,10 @@ export default class JanAssistantExtension implements AssistantExtension { continue; } - const content = await fs.readFileSync(join(filePath, jsonFiles[0]), 'utf-8'); + const content = await fs.readFileSync( + join(filePath, jsonFiles[0]), + "utf-8" + ); const assistant: Assistant = typeof content === "object" ? content : JSON.parse(content); diff --git a/extensions/conversational-extension/src/index.ts b/extensions/conversational-extension/src/index.ts index 7a76a7ed9e..6531c489a0 100644 --- a/extensions/conversational-extension/src/index.ts +++ b/extensions/conversational-extension/src/index.ts @@ -7,7 +7,8 @@ import { Thread, ThreadMessage } from '@janhq/core' * functionality for managing threads. */ export default class JSONConversationalExtension - implements ConversationalExtension { + implements ConversationalExtension +{ private static readonly _homeDir = 'file://threads' private static readonly _threadInfoFileName = 'thread.json' private static readonly _threadMessagesFileName = 'messages.jsonl' @@ -95,7 +96,8 @@ export default class JSONConversationalExtension */ async deleteThread(threadId: string): Promise { return fs.rmdirSync( - await joinPath([JSONConversationalExtension._homeDir, `${threadId}`]) + await joinPath([JSONConversationalExtension._homeDir, `${threadId}`]), + { recursive: true } ) } @@ -155,7 +157,8 @@ export default class JSONConversationalExtension JSONConversationalExtension._homeDir, threadDirName, JSONConversationalExtension._threadInfoFileName, - ]) + ]), + 'utf-8' ) } @@ -170,6 +173,7 @@ export default class JSONConversationalExtension 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], @@ -207,12 +211,14 @@ export default class JSONConversationalExtension JSONConversationalExtension._threadMessagesFileName, ]) - const result = await fs.readFileSync(messageFilePath, 'utf-8').then((content) => - content - .toString() - .split('\n') - .filter((line) => line !== '') - ) + const result = await fs + .readFileSync(messageFilePath, 'utf-8') + .then((content) => + content + .toString() + .split('\n') + .filter((line) => line !== '') + ) const messages: ThreadMessage[] = [] result.forEach((line: string) => { diff --git a/extensions/inference-nitro-extension/src/index.ts b/extensions/inference-nitro-extension/src/index.ts index 9334efa2ec..76d29fa283 100644 --- a/extensions/inference-nitro-extension/src/index.ts +++ b/extensions/inference-nitro-extension/src/index.ts @@ -17,7 +17,6 @@ import { ThreadMessage, events, executeOnMain, - getUserSpace, fs, Model, } from "@janhq/core"; @@ -93,9 +92,9 @@ export default class JanInferenceNitroExtension implements InferenceExtension { JanInferenceNitroExtension._engineMetadataFileName ); if (await fs.existsSync(engineFile)) { - JanInferenceNitroExtension._engineSettings = JSON.parse( - await fs.readFileSync(engineFile) - ); + const engine = await fs.readFileSync(engineFile, 'utf-8'); + JanInferenceNitroExtension._engineSettings = + typeof engine === "object" ? engine : JSON.parse(engine); } else { await fs.writeFileSync( engineFile, diff --git a/extensions/inference-nitro-extension/src/module.ts b/extensions/inference-nitro-extension/src/module.ts index 37b9e5b3bf..38092ab362 100644 --- a/extensions/inference-nitro-extension/src/module.ts +++ b/extensions/inference-nitro-extension/src/module.ts @@ -38,6 +38,10 @@ function stopModel(): Promise { */ async function initModel(wrapper: any): Promise { 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 diff --git a/extensions/inference-openai-extension/src/index.ts b/extensions/inference-openai-extension/src/index.ts index c38fb7c5ac..d612e474e9 100644 --- a/extensions/inference-openai-extension/src/index.ts +++ b/extensions/inference-openai-extension/src/index.ts @@ -87,14 +87,9 @@ export default class JanInferenceOpenAIExtension implements InferenceExtension { JanInferenceOpenAIExtension._engineMetadataFileName ); if (await fs.existsSync(engineFile)) { - try { - JanInferenceOpenAIExtension._engineSettings = JSON.parse( - await fs.readFileSync(engineFile) - ); - } catch { - JanInferenceOpenAIExtension._engineSettings = - await fs.readFileSync(engineFile); - } + const engine = await fs.readFileSync(engineFile, 'utf-8'); + JanInferenceOpenAIExtension._engineSettings = + typeof engine === "object" ? engine : JSON.parse(engine); } else { await fs.writeFileSync( engineFile, diff --git a/extensions/inference-triton-trtllm-extension/src/index.ts b/extensions/inference-triton-trtllm-extension/src/index.ts index 0970b19345..7ef5270e70 100644 --- a/extensions/inference-triton-trtllm-extension/src/index.ts +++ b/extensions/inference-triton-trtllm-extension/src/index.ts @@ -57,9 +57,9 @@ export default class JanInferenceTritonTrtLLMExtension /** * Subscribes to events emitted by the @janhq/core package. */ - onLoad(): void { - fs.mkdirSync(JanInferenceTritonTrtLLMExtension._homeDir); - JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings(); + async onLoad() { + if (!(await fs.existsSync(JanInferenceTritonTrtLLMExtension._homeDir))) + JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings(); // Events subscription events.on(EventName.OnMessageSent, (data) => @@ -99,9 +99,9 @@ export default class JanInferenceTritonTrtLLMExtension JanInferenceTritonTrtLLMExtension._engineMetadataFileName ); if (await fs.existsSync(engine_json)) { - JanInferenceTritonTrtLLMExtension._engineSettings = JSON.parse( - await fs.readFileSync(engine_json) - ); + const engine = await fs.readFileSync(engine_json, "utf-8"); + JanInferenceTritonTrtLLMExtension._engineSettings = + typeof engine === "object" ? engine : JSON.parse(engine); } else { await fs.writeFileSync( engine_json, diff --git a/web/hooks/useEngineSettings.ts b/web/hooks/useEngineSettings.ts index 3ec1bf93e7..d8e4330ad3 100644 --- a/web/hooks/useEngineSettings.ts +++ b/web/hooks/useEngineSettings.ts @@ -2,11 +2,13 @@ import { fs, joinPath } from '@janhq/core' export const useEngineSettings = () => { const readOpenAISettings = async () => { + if (!fs.existsSync(join('file://engines', 'openai.json'))) return {} const settings = await fs.readFileSync( - await joinPath(['engines', 'openai.json']) + await joinPath(['file://engines', 'openai.json']), + 'utf-8' ) if (settings) { - return JSON.parse(settings) + return typeof settings === 'object' ? settings : JSON.parse(settings) } return {} } @@ -18,7 +20,7 @@ export const useEngineSettings = () => { const settings = await readOpenAISettings() settings.api_key = apiKey await fs.writeFileSync( - await joinPath(['engines', 'openai.json']), + await joinPath(['file://engines', 'openai.json']), JSON.stringify(settings) ) } diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index fb9761d600..47ec5bfa91 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -173,7 +173,7 @@ export default function useSendChatMessage() { updateThreadInitSuccess(activeThread.id) updateThread(updatedThread) - extensionManager + await extensionManager .get(ExtensionType.Conversational) ?.saveThread(updatedThread) } From cd5aac00ead0e281f066b26dd4e9a10da9884fc6 Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 27 Dec 2023 08:26:10 +0700 Subject: [PATCH 14/16] refactor: shared server module (#1210) --- electron/main.ts | 21 +++++------------- electron/package.json | 2 +- server/index.ts | 43 +++++++++++++++++++++++++++++++++++++ server/main.ts | 33 ++-------------------------- server/package.json | 16 ++++++-------- server/tsconfig.json | 3 ++- web/services/restService.ts | 3 ++- 7 files changed, 62 insertions(+), 59 deletions(-) create mode 100644 server/index.ts diff --git a/electron/main.ts b/electron/main.ts index b9861395b8..fae3a1ffab 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -2,7 +2,6 @@ import { app, BrowserWindow } from 'electron' import { join } from 'path' import { setupMenu } from './utils/menu' import { createUserSpace } from './utils/path' -import Fastify from 'fastify' /** * Managers @@ -20,22 +19,11 @@ import { handleAppIPCs } from './handlers/app' import { handleAppUpdates } from './handlers/update' import { handleFsIPCs } from './handlers/fs' import { migrateExtensions } from './utils/migration' -import { v1Router } from '@janhq/core/node' -const fastify = Fastify({ - logger: true, -}) - -fastify.listen({ port: 1337 }, function (err, address) { - if (err) { - fastify.log.error(err) - process.exit(1) - } -}) - -fastify.register(v1Router, { - prefix: '/api/v1', -}) +/** + * Server + */ +import { startServer } from '@janhq/server' app .whenReady() @@ -46,6 +34,7 @@ app .then(handleIPCs) .then(handleAppUpdates) .then(createMainWindow) + .then(startServer) .then(() => { app.on('activate', () => { if (!BrowserWindow.getAllWindows().length) { diff --git a/electron/package.json b/electron/package.json index 93d55ddd29..f7127f8acc 100644 --- a/electron/package.json +++ b/electron/package.json @@ -72,12 +72,12 @@ "dependencies": { "@alumna/reflect": "^1.1.3", "@janhq/core": "link:./core", + "@janhq/server": "file:../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", - "fastify": "^4.25.0", "fs-extra": "^11.2.0", "node-fetch": "2", "pacote": "^17.0.4", diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000000..a0ccd5f01c --- /dev/null +++ b/server/index.ts @@ -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: "/api/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(); +}; diff --git a/server/main.ts b/server/main.ts index a3a349bf2e..c3eb691356 100644 --- a/server/main.ts +++ b/server/main.ts @@ -1,32 +1,3 @@ -import fastify from "fastify"; -import dotenv from "dotenv"; -import { v1Router } from '@janhq/core/node' +import { startServer } from "./index"; -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, - wildcard: false, - }); - - done(); - }, - { prefix: "extensions" } -); -server.register(v1Router, { prefix: "/api/v1" }); - -server - .listen({ - port: JAN_API_PORT, - host: JAN_API_HOST, - }) - .then(() => { - console.log(`JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`); - }); +startServer(); diff --git a/server/package.json b/server/package.json index cd6ced3a02..d90c9807f1 100644 --- a/server/package.json +++ b/server/package.json @@ -1,22 +1,25 @@ { - "name": "jan-server", + "name": "@janhq/server", "version": "0.1.3", - "main": "./build/main.js", + "main": "./build/index.js", "author": "Jan ", "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", "core": "link:./core", + "fastify": "^4.24.3", "request": "^2.88.2", "request-progress": "^3.0.0" }, @@ -27,12 +30,7 @@ "@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", "typescript": "^5.2.2" - }, - "installConfig": { - "hoistingLimits": "workspaces" } } diff --git a/server/tsconfig.json b/server/tsconfig.json index be5eb64055..2c4fc4a64e 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -14,7 +14,8 @@ "skipLibCheck": true, "paths": { "*": ["node_modules/*"] }, "typeRoots": ["node_modules/@types"], - "ignoreDeprecations": "5.0" + "ignoreDeprecations": "5.0", + "declaration": true }, // "sourceMap": true, diff --git a/web/services/restService.ts b/web/services/restService.ts index a88a859ab7..b17fd76090 100644 --- a/web/services/restService.ts +++ b/web/services/restService.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { safeJsonParse } from '@/utils/json' import { AppRoute, DownloadRoute, @@ -7,6 +6,8 @@ import { 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') From 38a0e1acfa80a885eb26bd35e5945332a1262397 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 28 Dec 2023 14:38:41 +0700 Subject: [PATCH 15/16] chore: resolve main --- core/src/node/api/routes/common.ts | 13 ++++++++- .../inference-nitro-extension/src/index.ts | 5 ++-- extensions/model-extension/src/index.ts | 29 ++++++++++--------- server/index.ts | 2 +- server/package.json | 4 +-- web/hooks/useEngineSettings.ts | 3 +- web/hooks/useSendChatMessage.ts | 1 - web/screens/Chat/Sidebar/index.tsx | 2 ++ web/services/restService.ts | 2 +- 9 files changed, 39 insertions(+), 22 deletions(-) diff --git a/core/src/node/api/routes/common.ts b/core/src/node/api/routes/common.ts index 55cf5b4a30..194429c66a 100644 --- a/core/src/node/api/routes/common.ts +++ b/core/src/node/api/routes/common.ts @@ -1,4 +1,6 @@ +import { AppRoute } from '../../../api' import { HttpServer } from '../HttpServer' +import { join } from 'path' import { chatCompletions, deleteBuilder, @@ -10,6 +12,7 @@ import { 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])) @@ -22,10 +25,18 @@ export const commonRouter = async (app: HttpServer) => { ) }) + // Download Model Routes app.get(`/models/download/:modelId`, async (request: any) => downloadModel(request.params.modelId), ) - // Endpoints + // 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]))) + }) } diff --git a/extensions/inference-nitro-extension/src/index.ts b/extensions/inference-nitro-extension/src/index.ts index 76d29fa283..e1ccc9f2af 100644 --- a/extensions/inference-nitro-extension/src/index.ts +++ b/extensions/inference-nitro-extension/src/index.ts @@ -19,6 +19,7 @@ import { executeOnMain, fs, Model, + joinPath, } from "@janhq/core"; import { InferenceExtension } from "@janhq/core"; import { requestInference } from "./helpers/sse"; @@ -92,7 +93,7 @@ export default class JanInferenceNitroExtension implements InferenceExtension { JanInferenceNitroExtension._engineMetadataFileName ); if (await fs.existsSync(engineFile)) { - const engine = await fs.readFileSync(engineFile, 'utf-8'); + const engine = await fs.readFileSync(engineFile, "utf-8"); JanInferenceNitroExtension._engineSettings = typeof engine === "object" ? engine : JSON.parse(engine); } else { @@ -110,7 +111,7 @@ export default class JanInferenceNitroExtension implements InferenceExtension { if (model.engine !== "nitro") { return; } - const modelFullPath = join("models", model.id, model.id); + const modelFullPath = await joinPath(["models", model.id]); const nitroInitResult = await executeOnMain(MODULE, "initModel", { modelFullPath: modelFullPath, diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index 1e30b96447..38cf28ef02 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -42,7 +42,7 @@ export default class JanModelExtension implements ModelExtension { * Called when the extension is unloaded. * @override */ - onUnload(): void { } + onUnload(): void {} private async copyModelsToHomeDir() { try { @@ -117,7 +117,10 @@ export default class JanModelExtension implements ModelExtension { return abortDownload( await joinPath([JanModelExtension._homeDir, modelId, modelId]) ).then(async () => { - fs.unlinkSync(await joinPath([JanModelExtension._homeDir, modelId, modelId])) + fs.unlinkSync( + await joinPath([JanModelExtension._homeDir, modelId, modelId]) + ) + }) } /** @@ -130,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.listFilesSync(dirPath) + const files = await fs.readdirSync(dirPath) const deletePromises = files.map(async (fileName: string) => { if (fileName !== JanModelExtension._modelMetadataFileName) { - return fs.deleteFileSync(await joinPath([dirPath, fileName])) + return fs.unlinkSync(await joinPath([dirPath, fileName])) } }) await Promise.allSettled(deletePromises) @@ -155,10 +158,7 @@ export default class JanModelExtension implements ModelExtension { ]) try { - await fs.writeFileSync( - 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 @@ -237,10 +237,13 @@ export default class JanModelExtension implements ModelExtension { console.debug(`Unable to parse model metadata: ${result.value}`) return undefined } - }) - .filter((e) => !!e) + } else { + console.error(result.reason) + return undefined + } + }) - return modelData + return modelData.filter((e) => !!e) } catch (err) { console.error(err) return [] @@ -248,7 +251,7 @@ export default class JanModelExtension implements ModelExtension { } private readModelMetadata(path: string) { - return fs.readFileSync(join(path), 'utf-8') + return fs.readFileSync(path, 'utf-8') } /** diff --git a/server/index.ts b/server/index.ts index a0ccd5f01c..a5fbfe0e6a 100644 --- a/server/index.ts +++ b/server/index.ts @@ -23,7 +23,7 @@ server.register( }, { prefix: "extensions" } ); -server.register(v1Router, { prefix: "/api/v1" }); +server.register(v1Router, { prefix: "/v1" }); export const startServer = () => { server diff --git a/server/package.json b/server/package.json index d90c9807f1..c3ff370fce 100644 --- a/server/package.json +++ b/server/package.json @@ -21,14 +21,14 @@ "core": "link:./core", "fastify": "^4.24.3", "request": "^2.88.2", - "request-progress": "^3.0.0" + "request-progress": "^3.0.0", + "dotenv": "^16.3.1" }, "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", "run-script-os": "^1.1.6", "typescript": "^5.2.2" diff --git a/web/hooks/useEngineSettings.ts b/web/hooks/useEngineSettings.ts index d8e4330ad3..349275e96b 100644 --- a/web/hooks/useEngineSettings.ts +++ b/web/hooks/useEngineSettings.ts @@ -2,7 +2,8 @@ import { fs, joinPath } from '@janhq/core' export const useEngineSettings = () => { const readOpenAISettings = async () => { - if (!fs.existsSync(join('file://engines', 'openai.json'))) return {} + if (!fs.existsSync(await joinPath(['file://engines', 'openai.json']))) + return {} const settings = await fs.readFileSync( await joinPath(['file://engines', 'openai.json']), 'utf-8' diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index 47ec5bfa91..f7643dfeaa 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -13,7 +13,6 @@ import { events, Model, ConversationalExtension, - ModelRuntimeParams, } from '@janhq/core' import { useAtom, useAtomValue, useSetAtom } from 'jotai' diff --git a/web/screens/Chat/Sidebar/index.tsx b/web/screens/Chat/Sidebar/index.tsx index 35b2eb7652..81833cd1d4 100644 --- a/web/screens/Chat/Sidebar/index.tsx +++ b/web/screens/Chat/Sidebar/index.tsx @@ -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 diff --git a/web/services/restService.ts b/web/services/restService.ts index b17fd76090..6fd7ad069e 100644 --- a/web/services/restService.ts +++ b/web/services/restService.ts @@ -36,7 +36,7 @@ export const restAPI = { ...acc, [proxy.route]: (...args: any) => { // For each route, define a function that sends a request to the API - return fetch(`${API_BASE_URL}/api/v1/${proxy.path}/${proxy.route}`, { + return fetch(`${API_BASE_URL}/v1/${proxy.path}/${proxy.route}`, { method: 'POST', body: JSON.stringify(args), headers: { contentType: 'application/json' }, From e794f52b1acf917edcc3b886a89bcd55ca7f6329 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 28 Dec 2023 16:19:40 +0700 Subject: [PATCH 16/16] chore: update makefile --- Makefile | 1 + electron/package.json | 2 +- package.json | 8 ++++++-- server/package.json | 9 +++++---- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index b0f5ed5432..a61ae92bbf 100644 --- a/Makefile +++ b/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 diff --git a/electron/package.json b/electron/package.json index f7127f8acc..3cc5e7680f 100644 --- a/electron/package.json +++ b/electron/package.json @@ -72,7 +72,7 @@ "dependencies": { "@alumna/reflect": "^1.1.3", "@janhq/core": "link:./core", - "@janhq/server": "file:../server", + "@janhq/server": "link:./server", "@npmcli/arborist": "^7.1.0", "@types/request": "^2.48.12", "@uiball/loaders": "^1.3.0", diff --git a/package.json b/package.json index c12ae12818..e4a1cae893 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/package.json b/server/package.json index c3ff370fce..2d350a3178 100644 --- a/server/package.json +++ b/server/package.json @@ -1,7 +1,8 @@ { "name": "@janhq/server", "version": "0.1.3", - "main": "./build/index.js", + "main": "build/index.js", + "types": "build/index.d.ts", "author": "Jan ", "license": "AGPL-3.0", "homepage": "https://jan.ai", @@ -18,11 +19,11 @@ "dependencies": { "@fastify/cors": "^8.4.2", "@fastify/static": "^6.12.0", - "core": "link:./core", + "@janhq/core": "link:./core", + "dotenv": "^16.3.1", "fastify": "^4.24.3", "request": "^2.88.2", - "request-progress": "^3.0.0", - "dotenv": "^16.3.1" + "request-progress": "^3.0.0" }, "devDependencies": { "@types/body-parser": "^1.19.5",