Merge branch 'dev' into local-server-documentation
This commit is contained in:
commit
8366964faa
@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "jan",
|
||||
"image": "node:20"
|
||||
}
|
||||
"name": "jan",
|
||||
"image": "node:20"
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
name: Jan Electron Linter & Test
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
@ -32,7 +32,10 @@ COPY --from=builder /app/node_modules ./node_modules/
|
||||
COPY --from=builder /app/yarn.lock ./yarn.lock
|
||||
|
||||
# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
|
||||
COPY --from=builder /app/core ./core/
|
||||
COPY --from=builder /app/server ./server/
|
||||
RUN cd core && yarn install && yarn run build
|
||||
RUN yarn workspace @janhq/server install && yarn workspace @janhq/server build
|
||||
COPY --from=builder /app/docs/openapi ./docs/openapi/
|
||||
|
||||
# Copy pre-install dependencies
|
||||
|
||||
@ -56,7 +56,10 @@ COPY --from=builder /app/node_modules ./node_modules/
|
||||
COPY --from=builder /app/yarn.lock ./yarn.lock
|
||||
|
||||
# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
|
||||
COPY --from=builder /app/core ./core/
|
||||
COPY --from=builder /app/server ./server/
|
||||
RUN cd core && yarn install && yarn run build
|
||||
RUN yarn workspace @janhq/server install && yarn workspace @janhq/server build
|
||||
COPY --from=builder /app/docs/openapi ./docs/openapi/
|
||||
|
||||
# Copy pre-install dependencies
|
||||
|
||||
37
README.md
37
README.md
@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
|
||||
<tr style="text-align:center">
|
||||
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
|
||||
<td style="text-align:center">
|
||||
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.6-264.exe'>
|
||||
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.6-271.exe'>
|
||||
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
|
||||
<b>jan.exe</b>
|
||||
</a>
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.6-264.dmg'>
|
||||
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.6-271.dmg'>
|
||||
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
||||
<b>Intel</b>
|
||||
</a>
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.6-264.dmg'>
|
||||
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.6-271.dmg'>
|
||||
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
||||
<b>M1/M2</b>
|
||||
</a>
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.6-264.deb'>
|
||||
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.6-271.deb'>
|
||||
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
||||
<b>jan.deb</b>
|
||||
</a>
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.6-264.AppImage'>
|
||||
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.6-271.AppImage'>
|
||||
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
||||
<b>jan.AppImage</b>
|
||||
</a>
|
||||
@ -167,6 +167,7 @@ To reset your installation:
|
||||
- Clear Application cache in `~/Library/Caches/jan`
|
||||
|
||||
## Requirements for running Jan
|
||||
|
||||
- MacOS: 13 or higher
|
||||
- Windows:
|
||||
- Windows 10 or higher
|
||||
@ -194,17 +195,17 @@ Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) fi
|
||||
|
||||
1. **Clone the repository and prepare:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/janhq/jan
|
||||
cd jan
|
||||
git checkout -b DESIRED_BRANCH
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/janhq/jan
|
||||
cd jan
|
||||
git checkout -b DESIRED_BRANCH
|
||||
```
|
||||
|
||||
2. **Run development and use Jan Desktop**
|
||||
|
||||
```bash
|
||||
make dev
|
||||
```
|
||||
```bash
|
||||
make dev
|
||||
```
|
||||
|
||||
This will start the development server and open the desktop app.
|
||||
|
||||
@ -222,14 +223,15 @@ This will build the app MacOS m1/m2 for production (with code signing already do
|
||||
|
||||
- Supported OS: Linux, WSL2 Docker
|
||||
- Pre-requisites:
|
||||
- `docker` and `docker compose`, follow instruction [here](https://docs.docker.com/engine/install/ubuntu/)
|
||||
|
||||
- Docker Engine and Docker Compose are required to run Jan in Docker mode. Follow the [instructions](https://docs.docker.com/engine/install/ubuntu/) below to get started with Docker Engine on Ubuntu.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh ./get-docker.sh --dry-run
|
||||
```
|
||||
|
||||
- `nvidia-driver` and `nvidia-docker2`, follow instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) (If you want to run with GPU mode)
|
||||
- If you intend to run Jan in GPU mode, you need to install `nvidia-driver` and `nvidia-docker2`. Follow the instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) for installation.
|
||||
|
||||
- Run Jan in Docker mode
|
||||
|
||||
@ -241,7 +243,7 @@ This will build the app MacOS m1/m2 for production (with code signing already do
|
||||
|
||||
- **Option 2**: Run Jan in GPU mode
|
||||
|
||||
- **Step 1**: Check cuda compatibility with your nvidia driver by running `nvidia-smi` and check the cuda version in the output
|
||||
- **Step 1**: Check CUDA compatibility with your NVIDIA driver by running `nvidia-smi` and check the CUDA version in the output
|
||||
|
||||
```bash
|
||||
nvidia-smi
|
||||
@ -274,7 +276,7 @@ This will build the app MacOS m1/m2 for production (with code signing already do
|
||||
|=======================================================================================|
|
||||
```
|
||||
|
||||
- **Step 2**: Go to https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda/tags and find the smallest minor version of image tag that matches the cuda version from the output of `nvidia-smi` (e.g. 12.1 -> 12.1.0)
|
||||
- **Step 2**: Visit [NVIDIA NGC Catalog ](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda/tags) and find the smallest minor version of image tag that matches your CUDA version (e.g., 12.1 -> 12.1.0)
|
||||
|
||||
- **Step 3**: Update the `Dockerfile.gpu` line number 5 with the latest minor version of the image tag from step 2 (e.g. change `FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 AS base` to `FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 AS base`)
|
||||
|
||||
@ -286,6 +288,7 @@ This will build the app MacOS m1/m2 for production (with code signing already do
|
||||
```
|
||||
|
||||
This will start the web server and you can access Jan at `http://localhost:3000`.
|
||||
|
||||
> Note: Currently, Docker mode is only work for development and localhost, production is not supported yet. RAG feature is not supported in Docker mode yet.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
@ -4,4 +4,4 @@ module.exports = {
|
||||
moduleNameMapper: {
|
||||
'@/(.*)': '<rootDir>/src/$1',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,7 +54,8 @@ export default [
|
||||
'url',
|
||||
'http',
|
||||
'os',
|
||||
'util'
|
||||
'util',
|
||||
'child_process',
|
||||
],
|
||||
watch: {
|
||||
include: 'src/node/**',
|
||||
|
||||
@ -1,15 +1,22 @@
|
||||
/**
|
||||
* Native Route APIs
|
||||
* @description Enum of all the routes exposed by the app
|
||||
*/
|
||||
export enum NativeRoute {
|
||||
openExternalUrl = 'openExternalUrl',
|
||||
openAppDirectory = 'openAppDirectory',
|
||||
openFileExplore = 'openFileExplorer',
|
||||
selectDirectory = 'selectDirectory',
|
||||
relaunch = 'relaunch',
|
||||
}
|
||||
|
||||
/**
|
||||
* App Route APIs
|
||||
* @description Enum of all the routes exposed by the app
|
||||
*/
|
||||
export enum AppRoute {
|
||||
openExternalUrl = 'openExternalUrl',
|
||||
openAppDirectory = 'openAppDirectory',
|
||||
openFileExplore = 'openFileExplorer',
|
||||
selectDirectory = 'selectDirectory',
|
||||
getAppConfigurations = 'getAppConfigurations',
|
||||
updateAppConfiguration = 'updateAppConfiguration',
|
||||
relaunch = 'relaunch',
|
||||
joinPath = 'joinPath',
|
||||
isSubdirectory = 'isSubdirectory',
|
||||
baseName = 'baseName',
|
||||
@ -69,6 +76,10 @@ export enum FileManagerRoute {
|
||||
|
||||
export type ApiFunction = (...args: any[]) => any
|
||||
|
||||
export type NativeRouteFunctions = {
|
||||
[K in NativeRoute]: ApiFunction
|
||||
}
|
||||
|
||||
export type AppRouteFunctions = {
|
||||
[K in AppRoute]: ApiFunction
|
||||
}
|
||||
@ -97,7 +108,8 @@ export type FileManagerRouteFunctions = {
|
||||
[K in FileManagerRoute]: ApiFunction
|
||||
}
|
||||
|
||||
export type APIFunctions = AppRouteFunctions &
|
||||
export type APIFunctions = NativeRouteFunctions &
|
||||
AppRouteFunctions &
|
||||
AppEventFunctions &
|
||||
DownloadRouteFunctions &
|
||||
DownloadEventFunctions &
|
||||
@ -105,11 +117,13 @@ export type APIFunctions = AppRouteFunctions &
|
||||
FileSystemRouteFunctions &
|
||||
FileManagerRoute
|
||||
|
||||
export const APIRoutes = [
|
||||
export const CoreRoutes = [
|
||||
...Object.values(AppRoute),
|
||||
...Object.values(DownloadRoute),
|
||||
...Object.values(ExtensionRoute),
|
||||
...Object.values(FileSystemRoute),
|
||||
...Object.values(FileManagerRoute),
|
||||
]
|
||||
|
||||
export const APIRoutes = [...CoreRoutes, ...Object.values(NativeRoute)]
|
||||
export const APIEvents = [...Object.values(AppEvent), ...Object.values(DownloadEvent)]
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
export enum ExtensionTypeEnum {
|
||||
Assistant = "assistant",
|
||||
Conversational = "conversational",
|
||||
Inference = "inference",
|
||||
Model = "model",
|
||||
SystemMonitoring = "systemMonitoring",
|
||||
Assistant = 'assistant',
|
||||
Conversational = 'conversational',
|
||||
Inference = 'inference',
|
||||
Model = 'model',
|
||||
SystemMonitoring = 'systemMonitoring',
|
||||
}
|
||||
|
||||
export interface ExtensionType {
|
||||
type(): ExtensionTypeEnum | undefined;
|
||||
type(): ExtensionTypeEnum | undefined
|
||||
}
|
||||
/**
|
||||
* Represents a base extension.
|
||||
@ -20,16 +20,16 @@ export abstract class BaseExtension implements ExtensionType {
|
||||
* Undefined means its not extending any known extension by the application.
|
||||
*/
|
||||
type(): ExtensionTypeEnum | undefined {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
/**
|
||||
* Called when the extension is loaded.
|
||||
* Any initialization logic for the extension should be put here.
|
||||
*/
|
||||
abstract onLoad(): void;
|
||||
abstract onLoad(): void
|
||||
/**
|
||||
* Called when the extension is unloaded.
|
||||
* Any cleanup logic for the extension should be put here.
|
||||
*/
|
||||
abstract onUnload(): void;
|
||||
abstract onUnload(): void
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Assistant, AssistantInterface } from "../index";
|
||||
import { BaseExtension, ExtensionTypeEnum } from "../extension";
|
||||
import { Assistant, AssistantInterface } from '../index'
|
||||
import { BaseExtension, ExtensionTypeEnum } from '../extension'
|
||||
|
||||
/**
|
||||
* Assistant extension for managing assistants.
|
||||
@ -10,10 +10,10 @@ export abstract class AssistantExtension extends BaseExtension implements Assist
|
||||
* Assistant extension type.
|
||||
*/
|
||||
type(): ExtensionTypeEnum | undefined {
|
||||
return ExtensionTypeEnum.Assistant;
|
||||
return ExtensionTypeEnum.Assistant
|
||||
}
|
||||
|
||||
abstract createAssistant(assistant: Assistant): Promise<void>;
|
||||
abstract deleteAssistant(assistant: Assistant): Promise<void>;
|
||||
abstract getAssistants(): Promise<Assistant[]>;
|
||||
abstract createAssistant(assistant: Assistant): Promise<void>
|
||||
abstract deleteAssistant(assistant: Assistant): Promise<void>
|
||||
abstract getAssistants(): Promise<Assistant[]>
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ export abstract class ConversationalExtension
|
||||
* Conversation extension type.
|
||||
*/
|
||||
type(): ExtensionTypeEnum | undefined {
|
||||
return ExtensionTypeEnum.Conversational;
|
||||
return ExtensionTypeEnum.Conversational
|
||||
}
|
||||
|
||||
abstract getThreads(): Promise<Thread[]>
|
||||
|
||||
@ -2,24 +2,24 @@
|
||||
* Conversational extension. Persists and retrieves conversations.
|
||||
* @module
|
||||
*/
|
||||
export { ConversationalExtension } from "./conversational";
|
||||
export { ConversationalExtension } from './conversational'
|
||||
|
||||
/**
|
||||
* Inference extension. Start, stop and inference models.
|
||||
*/
|
||||
export { InferenceExtension } from "./inference";
|
||||
export { InferenceExtension } from './inference'
|
||||
|
||||
/**
|
||||
* Monitoring extension for system monitoring.
|
||||
*/
|
||||
export { MonitoringExtension } from "./monitoring";
|
||||
export { MonitoringExtension } from './monitoring'
|
||||
|
||||
/**
|
||||
* Assistant extension for managing assistants.
|
||||
*/
|
||||
export { AssistantExtension } from "./assistant";
|
||||
export { AssistantExtension } from './assistant'
|
||||
|
||||
/**
|
||||
* Model extension for managing models.
|
||||
*/
|
||||
export { ModelExtension } from "./model";
|
||||
export { ModelExtension } from './model'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { InferenceInterface, MessageRequest, ThreadMessage } from "../index";
|
||||
import { BaseExtension, ExtensionTypeEnum } from "../extension";
|
||||
import { InferenceInterface, MessageRequest, ThreadMessage } from '../index'
|
||||
import { BaseExtension, ExtensionTypeEnum } from '../extension'
|
||||
|
||||
/**
|
||||
* Inference extension. Start, stop and inference models.
|
||||
@ -9,8 +9,8 @@ export abstract class InferenceExtension extends BaseExtension implements Infere
|
||||
* Inference extension type.
|
||||
*/
|
||||
type(): ExtensionTypeEnum | undefined {
|
||||
return ExtensionTypeEnum.Inference;
|
||||
return ExtensionTypeEnum.Inference
|
||||
}
|
||||
|
||||
abstract inference(data: MessageRequest): Promise<ThreadMessage>;
|
||||
abstract inference(data: MessageRequest): Promise<ThreadMessage>
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { BaseExtension, ExtensionTypeEnum } from "../extension";
|
||||
import { Model, ModelInterface } from "../index";
|
||||
import { BaseExtension, ExtensionTypeEnum } from '../extension'
|
||||
import { Model, ModelInterface } from '../index'
|
||||
|
||||
/**
|
||||
* Model extension for managing models.
|
||||
@ -9,16 +9,16 @@ export abstract class ModelExtension extends BaseExtension implements ModelInter
|
||||
* Model extension type.
|
||||
*/
|
||||
type(): ExtensionTypeEnum | undefined {
|
||||
return ExtensionTypeEnum.Model;
|
||||
return ExtensionTypeEnum.Model
|
||||
}
|
||||
|
||||
abstract downloadModel(
|
||||
model: Model,
|
||||
network?: { proxy: string; ignoreSSL?: boolean },
|
||||
): Promise<void>;
|
||||
abstract cancelModelDownload(modelId: string): Promise<void>;
|
||||
abstract deleteModel(modelId: string): Promise<void>;
|
||||
abstract saveModel(model: Model): Promise<void>;
|
||||
abstract getDownloadedModels(): Promise<Model[]>;
|
||||
abstract getConfiguredModels(): Promise<Model[]>;
|
||||
network?: { proxy: string; ignoreSSL?: boolean }
|
||||
): Promise<void>
|
||||
abstract cancelModelDownload(modelId: string): Promise<void>
|
||||
abstract deleteModel(modelId: string): Promise<void>
|
||||
abstract saveModel(model: Model): Promise<void>
|
||||
abstract getDownloadedModels(): Promise<Model[]>
|
||||
abstract getConfiguredModels(): Promise<Model[]>
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { BaseExtension, ExtensionTypeEnum } from "../extension";
|
||||
import { MonitoringInterface } from "../index";
|
||||
import { BaseExtension, ExtensionTypeEnum } from '../extension'
|
||||
import { MonitoringInterface } from '../index'
|
||||
|
||||
/**
|
||||
* Monitoring extension for system monitoring.
|
||||
@ -10,9 +10,9 @@ export abstract class MonitoringExtension extends BaseExtension implements Monit
|
||||
* Monitoring extension type.
|
||||
*/
|
||||
type(): ExtensionTypeEnum | undefined {
|
||||
return ExtensionTypeEnum.SystemMonitoring;
|
||||
return ExtensionTypeEnum.SystemMonitoring
|
||||
}
|
||||
|
||||
abstract getResourcesInfo(): Promise<any>;
|
||||
abstract getCurrentLoad(): Promise<any>;
|
||||
abstract getResourcesInfo(): Promise<any>
|
||||
abstract getCurrentLoad(): Promise<any>
|
||||
}
|
||||
|
||||
@ -38,3 +38,10 @@ export * from './extension'
|
||||
* @module
|
||||
*/
|
||||
export * from './extensions/index'
|
||||
|
||||
/**
|
||||
* Declare global object
|
||||
*/
|
||||
declare global {
|
||||
var core: any | undefined
|
||||
}
|
||||
|
||||
43
core/src/node/api/common/adapter.ts
Normal file
43
core/src/node/api/common/adapter.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import {
|
||||
AppRoute,
|
||||
DownloadRoute,
|
||||
ExtensionRoute,
|
||||
FileManagerRoute,
|
||||
FileSystemRoute,
|
||||
} from '../../../api'
|
||||
import { Downloader } from '../processors/download'
|
||||
import { FileSystem } from '../processors/fs'
|
||||
import { Extension } from '../processors/extension'
|
||||
import { FSExt } from '../processors/fsExt'
|
||||
import { App } from '../processors/app'
|
||||
|
||||
export class RequestAdapter {
|
||||
downloader: Downloader
|
||||
fileSystem: FileSystem
|
||||
extension: Extension
|
||||
fsExt: FSExt
|
||||
app: App
|
||||
|
||||
constructor(observer?: Function) {
|
||||
this.downloader = new Downloader(observer)
|
||||
this.fileSystem = new FileSystem()
|
||||
this.extension = new Extension()
|
||||
this.fsExt = new FSExt()
|
||||
this.app = new App()
|
||||
}
|
||||
|
||||
// TODO: Clearer Factory pattern here
|
||||
process(route: string, ...args: any) {
|
||||
if (route in DownloadRoute) {
|
||||
return this.downloader.process(route, ...args)
|
||||
} else if (route in FileSystemRoute) {
|
||||
return this.fileSystem.process(route, ...args)
|
||||
} else if (route in ExtensionRoute) {
|
||||
return this.extension.process(route, ...args)
|
||||
} else if (route in FileManagerRoute) {
|
||||
return this.fsExt.process(route, ...args)
|
||||
} else if (route in AppRoute) {
|
||||
return this.app.process(route, ...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
23
core/src/node/api/common/handler.ts
Normal file
23
core/src/node/api/common/handler.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { CoreRoutes } from '../../../api'
|
||||
import { RequestAdapter } from './adapter'
|
||||
|
||||
export type Handler = (route: string, args: any) => any
|
||||
|
||||
export class RequestHandler {
|
||||
handler: Handler
|
||||
adataper: RequestAdapter
|
||||
|
||||
constructor(handler: Handler, observer?: Function) {
|
||||
this.handler = handler
|
||||
this.adataper = new RequestAdapter(observer)
|
||||
}
|
||||
|
||||
handle() {
|
||||
CoreRoutes.map((route) => {
|
||||
this.handler(route, async (...args: any[]) => {
|
||||
const values = await this.adataper.process(route, ...args)
|
||||
return values
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './HttpServer'
|
||||
export * from './routes'
|
||||
export * from './restful/v1'
|
||||
export * from './common/handler'
|
||||
|
||||
3
core/src/node/api/processors/Processor.ts
Normal file
3
core/src/node/api/processors/Processor.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export abstract class Processor {
|
||||
abstract process(key: string, ...args: any[]): any
|
||||
}
|
||||
97
core/src/node/api/processors/app.ts
Normal file
97
core/src/node/api/processors/app.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { basename, isAbsolute, join, relative } from 'path'
|
||||
|
||||
import { AppRoute } from '../../../api'
|
||||
import { Processor } from './Processor'
|
||||
import { getAppConfigurations as appConfiguration, updateAppConfiguration } from '../../helper'
|
||||
import { log as writeLog, logServer as writeServerLog } from '../../helper/log'
|
||||
import { appResourcePath } from '../../helper/path'
|
||||
|
||||
export class App implements Processor {
|
||||
observer?: Function
|
||||
|
||||
constructor(observer?: Function) {
|
||||
this.observer = observer
|
||||
}
|
||||
|
||||
process(key: string, ...args: any[]): any {
|
||||
const instance = this as any
|
||||
const func = instance[key]
|
||||
return func(...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins multiple paths together, respect to the current OS.
|
||||
*/
|
||||
joinPath(args: any[]) {
|
||||
return join(...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given path is a subdirectory of the given directory.
|
||||
*
|
||||
* @param _event - The IPC event object.
|
||||
* @param from - The path to check.
|
||||
* @param to - The directory to check against.
|
||||
*
|
||||
* @returns {Promise<boolean>} - A promise that resolves with the result.
|
||||
*/
|
||||
isSubdirectory(from: any, to: any) {
|
||||
const rel = relative(from, to)
|
||||
const isSubdir = rel && !rel.startsWith('..') && !isAbsolute(rel)
|
||||
|
||||
if (isSubdir === '') return false
|
||||
else return isSubdir
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve basename from given path, respect to the current OS.
|
||||
*/
|
||||
baseName(args: any) {
|
||||
return basename(args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log message to log file.
|
||||
*/
|
||||
log(args: any) {
|
||||
writeLog(args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log message to log file.
|
||||
*/
|
||||
logServer(args: any) {
|
||||
writeServerLog(args)
|
||||
}
|
||||
|
||||
getAppConfigurations() {
|
||||
return appConfiguration()
|
||||
}
|
||||
|
||||
async updateAppConfiguration(args: any) {
|
||||
await updateAppConfiguration(args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Jan API Server.
|
||||
*/
|
||||
async startServer(args?: any) {
|
||||
const { startServer } = require('@janhq/server')
|
||||
return startServer({
|
||||
host: args?.host,
|
||||
port: args?.port,
|
||||
isCorsEnabled: args?.isCorsEnabled,
|
||||
isVerboseEnabled: args?.isVerboseEnabled,
|
||||
schemaPath: join(await appResourcePath(), 'docs', 'openapi', 'jan.yaml'),
|
||||
baseDir: join(await appResourcePath(), 'docs', 'openapi'),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop Jan API Server.
|
||||
*/
|
||||
stopServer() {
|
||||
const { stopServer } = require('@janhq/server')
|
||||
return stopServer()
|
||||
}
|
||||
}
|
||||
106
core/src/node/api/processors/download.ts
Normal file
106
core/src/node/api/processors/download.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { resolve, sep } from 'path'
|
||||
import { DownloadEvent } from '../../../api'
|
||||
import { normalizeFilePath } from '../../helper/path'
|
||||
import { getJanDataFolderPath } from '../../helper'
|
||||
import { DownloadManager } from '../../helper/download'
|
||||
import { createWriteStream, renameSync } from 'fs'
|
||||
import { Processor } from './Processor'
|
||||
import { DownloadState } from '../../../types'
|
||||
|
||||
export class Downloader implements Processor {
|
||||
observer?: Function
|
||||
|
||||
constructor(observer?: Function) {
|
||||
this.observer = observer
|
||||
}
|
||||
|
||||
process(key: string, ...args: any[]): any {
|
||||
const instance = this as any
|
||||
const func = instance[key]
|
||||
return func(this.observer, ...args)
|
||||
}
|
||||
|
||||
downloadFile(observer: any, url: string, localPath: string, network: any) {
|
||||
const request = require('request')
|
||||
const progress = require('request-progress')
|
||||
|
||||
const strictSSL = !network?.ignoreSSL
|
||||
const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined
|
||||
if (typeof localPath === 'string') {
|
||||
localPath = normalizeFilePath(localPath)
|
||||
}
|
||||
const array = localPath.split(sep)
|
||||
const fileName = array.pop() ?? ''
|
||||
const modelId = array.pop() ?? ''
|
||||
|
||||
const destination = resolve(getJanDataFolderPath(), localPath)
|
||||
const rq = request({ url, strictSSL, proxy })
|
||||
|
||||
// Put request to download manager instance
|
||||
DownloadManager.instance.setRequest(localPath, rq)
|
||||
|
||||
// Downloading file to a temp file first
|
||||
const downloadingTempFile = `${destination}.download`
|
||||
|
||||
progress(rq, {})
|
||||
.on('progress', (state: any) => {
|
||||
const downloadState: DownloadState = {
|
||||
...state,
|
||||
modelId,
|
||||
fileName,
|
||||
downloadState: 'downloading',
|
||||
}
|
||||
console.log('progress: ', downloadState)
|
||||
observer?.(DownloadEvent.onFileDownloadUpdate, downloadState)
|
||||
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
||||
})
|
||||
.on('error', (error: Error) => {
|
||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
||||
const downloadState: DownloadState = {
|
||||
...currentDownloadState,
|
||||
error: error.message,
|
||||
downloadState: 'error',
|
||||
}
|
||||
if (currentDownloadState) {
|
||||
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
||||
}
|
||||
|
||||
observer?.(DownloadEvent.onFileDownloadError, downloadState)
|
||||
})
|
||||
.on('end', () => {
|
||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
||||
if (currentDownloadState && DownloadManager.instance.networkRequests[localPath]) {
|
||||
// Finished downloading, rename temp file to actual file
|
||||
renameSync(downloadingTempFile, destination)
|
||||
const downloadState: DownloadState = {
|
||||
...currentDownloadState,
|
||||
downloadState: 'end',
|
||||
}
|
||||
observer?.(DownloadEvent.onFileDownloadSuccess, downloadState)
|
||||
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
||||
}
|
||||
})
|
||||
.pipe(createWriteStream(downloadingTempFile))
|
||||
}
|
||||
|
||||
abortDownload(observer: any, fileName: string) {
|
||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||
if (rq) {
|
||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||
rq?.abort()
|
||||
} else {
|
||||
observer?.(DownloadEvent.onFileDownloadError, {
|
||||
fileName,
|
||||
error: 'aborted',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resumeDownload(observer: any, fileName: any) {
|
||||
DownloadManager.instance.networkRequests[fileName]?.resume()
|
||||
}
|
||||
|
||||
pauseDownload(observer: any, fileName: any) {
|
||||
DownloadManager.instance.networkRequests[fileName]?.pause()
|
||||
}
|
||||
}
|
||||
88
core/src/node/api/processors/extension.ts
Normal file
88
core/src/node/api/processors/extension.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { readdirSync } from 'fs'
|
||||
import { join, extname } from 'path'
|
||||
|
||||
import { Processor } from './Processor'
|
||||
import { ModuleManager } from '../../helper/module'
|
||||
import { getJanExtensionsPath as getPath } from '../../helper'
|
||||
import {
|
||||
getActiveExtensions as getExtensions,
|
||||
getExtension,
|
||||
removeExtension,
|
||||
installExtensions,
|
||||
} from '../../extension/store'
|
||||
import { appResourcePath } from '../../helper/path'
|
||||
|
||||
export class Extension implements Processor {
|
||||
observer?: Function
|
||||
|
||||
constructor(observer?: Function) {
|
||||
this.observer = observer
|
||||
}
|
||||
|
||||
process(key: string, ...args: any[]): any {
|
||||
const instance = this as any
|
||||
const func = instance[key]
|
||||
return func(...args)
|
||||
}
|
||||
|
||||
invokeExtensionFunc(modulePath: string, method: string, ...params: any[]) {
|
||||
const module = require(join(getPath(), modulePath))
|
||||
ModuleManager.instance.setModule(modulePath, module)
|
||||
|
||||
if (typeof module[method] === 'function') {
|
||||
return module[method](...params)
|
||||
} else {
|
||||
console.debug(module[method])
|
||||
console.error(`Function "${method}" does not exist in the module.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the paths of the base extensions.
|
||||
* @returns An array of paths to the base extensions.
|
||||
*/
|
||||
async baseExtensions() {
|
||||
const baseExtensionPath = join(await appResourcePath(), 'pre-install')
|
||||
return readdirSync(baseExtensionPath)
|
||||
.filter((file) => extname(file) === '.tgz')
|
||||
.map((file) => join(baseExtensionPath, file))
|
||||
}
|
||||
|
||||
/**MARK: Extension Manager handlers */
|
||||
async installExtension(extensions: any) {
|
||||
// Install and activate all provided extensions
|
||||
const installed = await installExtensions(extensions)
|
||||
return JSON.parse(JSON.stringify(installed))
|
||||
}
|
||||
|
||||
// Register IPC route to uninstall a extension
|
||||
async uninstallExtension(extensions: any) {
|
||||
// Uninstall all provided extensions
|
||||
for (const ext of extensions) {
|
||||
const extension = getExtension(ext)
|
||||
await extension.uninstall()
|
||||
if (extension.name) removeExtension(extension.name)
|
||||
}
|
||||
|
||||
// Reload all renderer pages if needed
|
||||
return true
|
||||
}
|
||||
|
||||
// Register IPC route to update a extension
|
||||
async updateExtension(extensions: any) {
|
||||
// Update all provided extensions
|
||||
const updated: any[] = []
|
||||
for (const ext of extensions) {
|
||||
const extension = getExtension(ext)
|
||||
const res = await extension.update()
|
||||
if (res) updated.push(extension)
|
||||
}
|
||||
|
||||
// Reload all renderer pages if needed
|
||||
return JSON.parse(JSON.stringify(updated))
|
||||
}
|
||||
|
||||
getActiveExtensions() {
|
||||
return JSON.parse(JSON.stringify(getExtensions()))
|
||||
}
|
||||
}
|
||||
25
core/src/node/api/processors/fs.ts
Normal file
25
core/src/node/api/processors/fs.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { join } from 'path'
|
||||
import { normalizeFilePath } from '../../helper/path'
|
||||
import { getJanDataFolderPath } from '../../helper'
|
||||
import { Processor } from './Processor'
|
||||
|
||||
export class FileSystem implements Processor {
|
||||
observer?: Function
|
||||
private static moduleName = 'fs'
|
||||
|
||||
constructor(observer?: Function) {
|
||||
this.observer = observer
|
||||
}
|
||||
|
||||
process(route: string, ...args: any[]): any {
|
||||
return import(FileSystem.moduleName).then((mdl) =>
|
||||
mdl[route](
|
||||
...args.map((arg: any) =>
|
||||
typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
|
||||
? join(getJanDataFolderPath(), normalizeFilePath(arg))
|
||||
: arg
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
78
core/src/node/api/processors/fsExt.ts
Normal file
78
core/src/node/api/processors/fsExt.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { join } from 'path'
|
||||
import fs from 'fs'
|
||||
import { FileManagerRoute } from '../../../api'
|
||||
import { appResourcePath, normalizeFilePath } from '../../helper/path'
|
||||
import { getJanDataFolderPath, getJanDataFolderPath as getPath } from '../../helper'
|
||||
import { Processor } from './Processor'
|
||||
import { FileStat } from '../../../types'
|
||||
|
||||
export class FSExt implements Processor {
|
||||
observer?: Function
|
||||
|
||||
constructor(observer?: Function) {
|
||||
this.observer = observer
|
||||
}
|
||||
|
||||
process(key: string, ...args: any): any {
|
||||
const instance = this as any
|
||||
const func = instance[key]
|
||||
return func(...args)
|
||||
}
|
||||
|
||||
// Handles the 'syncFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path.
|
||||
syncFile(src: string, dest: string) {
|
||||
const reflect = require('@alumna/reflect')
|
||||
return reflect({
|
||||
src,
|
||||
dest,
|
||||
recursive: true,
|
||||
delete: false,
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Handles the 'getJanDataFolderPath' IPC event. This event is triggered to get the user space path.
|
||||
getJanDataFolderPath() {
|
||||
return Promise.resolve(getPath())
|
||||
}
|
||||
|
||||
// Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path.
|
||||
getResourcePath() {
|
||||
return appResourcePath()
|
||||
}
|
||||
|
||||
// Handles the 'getUserHomePath' IPC event. This event is triggered to get the user home path.
|
||||
getUserHomePath() {
|
||||
return process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME']
|
||||
}
|
||||
|
||||
// handle fs is directory here
|
||||
fileStat(path: string) {
|
||||
const normalizedPath = normalizeFilePath(path)
|
||||
|
||||
const fullPath = join(getJanDataFolderPath(), normalizedPath)
|
||||
const isExist = fs.existsSync(fullPath)
|
||||
if (!isExist) return undefined
|
||||
|
||||
const isDirectory = fs.lstatSync(fullPath).isDirectory()
|
||||
const size = fs.statSync(fullPath).size
|
||||
|
||||
const fileStat: FileStat = {
|
||||
isDirectory,
|
||||
size,
|
||||
}
|
||||
|
||||
return fileStat
|
||||
}
|
||||
|
||||
writeBlob(path: string, data: any) {
|
||||
try {
|
||||
const normalizedPath = normalizeFilePath(path)
|
||||
const dataBuffer = Buffer.from(data, 'base64')
|
||||
fs.writeFileSync(join(getJanDataFolderPath(), normalizedPath), dataBuffer)
|
||||
} catch (err) {
|
||||
console.error(`writeFile ${path} result: ${err}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
23
core/src/node/api/restful/app/download.ts
Normal file
23
core/src/node/api/restful/app/download.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { DownloadRoute } from '../../../../api'
|
||||
import { DownloadManager } from '../../../helper/download'
|
||||
import { HttpServer } from '../../HttpServer'
|
||||
|
||||
export const downloadRouter = async (app: HttpServer) => {
|
||||
app.get(`/download/${DownloadRoute.getDownloadProgress}/:modelId`, async (req, res) => {
|
||||
const modelId = req.params.modelId
|
||||
|
||||
console.debug(`Getting download progress for model ${modelId}`)
|
||||
console.debug(
|
||||
`All Download progress: ${JSON.stringify(DownloadManager.instance.downloadProgressMap)}`
|
||||
)
|
||||
|
||||
// check if null DownloadManager.instance.downloadProgressMap
|
||||
if (!DownloadManager.instance.downloadProgressMap[modelId]) {
|
||||
return res.status(404).send({
|
||||
message: 'Download progress not found',
|
||||
})
|
||||
} else {
|
||||
return res.status(200).send(DownloadManager.instance.downloadProgressMap[modelId])
|
||||
}
|
||||
})
|
||||
}
|
||||
13
core/src/node/api/restful/app/handlers.ts
Normal file
13
core/src/node/api/restful/app/handlers.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { HttpServer } from '../../HttpServer'
|
||||
import { Handler, RequestHandler } from '../../common/handler'
|
||||
|
||||
export function handleRequests(app: HttpServer) {
|
||||
const restWrapper: Handler = (route: string, listener: (...args: any[]) => any) => {
|
||||
app.post(`/app/${route}`, async (request: any, reply: any) => {
|
||||
const args = JSON.parse(request.body) as any[]
|
||||
reply.send(JSON.stringify(await listener(...args)))
|
||||
})
|
||||
}
|
||||
const handler = new RequestHandler(restWrapper)
|
||||
handler.handle()
|
||||
}
|
||||
@ -1,22 +1,24 @@
|
||||
import { AppRoute } from '../../../api'
|
||||
import { HttpServer } from '../HttpServer'
|
||||
import { basename, join } from 'path'
|
||||
import {
|
||||
chatCompletions,
|
||||
deleteBuilder,
|
||||
downloadModel,
|
||||
getBuilder,
|
||||
retrieveBuilder,
|
||||
} from '../common/builder'
|
||||
createMessage,
|
||||
createThread,
|
||||
getMessages,
|
||||
retrieveMesasge,
|
||||
updateThread,
|
||||
} from './helper/builder'
|
||||
|
||||
import { JanApiRouteConfiguration } from '../common/configuration'
|
||||
import { startModel, stopModel } from '../common/startStopModel'
|
||||
import { JanApiRouteConfiguration } from './helper/configuration'
|
||||
import { startModel, stopModel } from './helper/startStopModel'
|
||||
import { ModelSettingParams } from '../../../types'
|
||||
import { getJanDataFolderPath } from '../../utils'
|
||||
import { normalizeFilePath } from '../../path'
|
||||
|
||||
export const commonRouter = async (app: HttpServer) => {
|
||||
// Common Routes
|
||||
// Read & Delete :: Threads | Models | Assistants
|
||||
Object.keys(JanApiRouteConfiguration).forEach((key) => {
|
||||
app.get(`/${key}`, async (_request) => getBuilder(JanApiRouteConfiguration[key]))
|
||||
|
||||
@ -29,7 +31,24 @@ export const commonRouter = async (app: HttpServer) => {
|
||||
)
|
||||
})
|
||||
|
||||
// Download Model Routes
|
||||
// Threads
|
||||
app.post(`/threads/`, async (req, res) => createThread(req.body))
|
||||
|
||||
app.get(`/threads/:threadId/messages`, async (req, res) => getMessages(req.params.threadId))
|
||||
|
||||
app.get(`/threads/:threadId/messages/:messageId`, async (req, res) =>
|
||||
retrieveMesasge(req.params.threadId, req.params.messageId)
|
||||
)
|
||||
|
||||
app.post(`/threads/:threadId/messages`, async (req, res) =>
|
||||
createMessage(req.params.threadId as any, req.body as any)
|
||||
)
|
||||
|
||||
app.patch(`/threads/:threadId`, async (request: any) =>
|
||||
updateThread(request.params.threadId, request.body)
|
||||
)
|
||||
|
||||
// Models
|
||||
app.get(`/models/download/:modelId`, async (request: any) =>
|
||||
downloadModel(request.params.modelId, {
|
||||
ignoreSSL: request.query.ignoreSSL === 'true',
|
||||
@ -48,24 +67,6 @@ export const commonRouter = async (app: HttpServer) => {
|
||||
|
||||
app.put(`/models/:modelId/stop`, async (request: any) => stopModel(request.params.modelId))
|
||||
|
||||
// Chat Completion Routes
|
||||
// Chat Completion
|
||||
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[]
|
||||
|
||||
const paths = args[0].map((arg: string) =>
|
||||
typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
|
||||
? join(getJanDataFolderPath(), normalizeFilePath(arg))
|
||||
: arg
|
||||
)
|
||||
|
||||
reply.send(JSON.stringify(join(...paths)))
|
||||
})
|
||||
|
||||
app.post(`/app/${AppRoute.baseName}`, async (request: any, reply: any) => {
|
||||
const args = JSON.parse(request.body) as any[]
|
||||
reply.send(JSON.stringify(basename(args[0])))
|
||||
})
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
import fs from 'fs'
|
||||
import { JanApiRouteConfiguration, RouteConfiguration } from './configuration'
|
||||
import { join } from 'path'
|
||||
import { ContentType, MessageStatus, Model, ThreadMessage } from './../../../index'
|
||||
import { getEngineConfiguration, getJanDataFolderPath } from '../../utils'
|
||||
import { ContentType, MessageStatus, Model, ThreadMessage } from '../../../../index'
|
||||
import { getEngineConfiguration, getJanDataFolderPath } from '../../../helper'
|
||||
import { DEFAULT_CHAT_COMPLETION_URL } from './consts'
|
||||
|
||||
// TODO: Refactor these
|
||||
export const getBuilder = async (configuration: RouteConfiguration) => {
|
||||
const directoryPath = join(getJanDataFolderPath(), configuration.dirName)
|
||||
try {
|
||||
@ -1,9 +1,9 @@
|
||||
import fs from 'fs'
|
||||
import { join } from 'path'
|
||||
import { getJanDataFolderPath, getJanExtensionsPath, getSystemResourceInfo } from '../../utils'
|
||||
import { logServer } from '../../log'
|
||||
import { getJanDataFolderPath, getJanExtensionsPath, getSystemResourceInfo } from '../../../helper'
|
||||
import { logServer } from '../../../helper/log'
|
||||
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
|
||||
import { Model, ModelSettingParams, PromptTemplate } from '../../../types'
|
||||
import { Model, ModelSettingParams, PromptTemplate } from '../../../../types'
|
||||
import {
|
||||
LOCAL_HOST,
|
||||
NITRO_DEFAULT_PORT,
|
||||
16
core/src/node/api/restful/v1.ts
Normal file
16
core/src/node/api/restful/v1.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { HttpServer } from '../HttpServer'
|
||||
import { commonRouter } from './common'
|
||||
import { downloadRouter } from './app/download'
|
||||
import { handleRequests } from './app/handlers'
|
||||
|
||||
export const v1Router = async (app: HttpServer) => {
|
||||
// MARK: Public API Routes
|
||||
app.register(commonRouter)
|
||||
|
||||
// MARK: Internal Application Routes
|
||||
handleRequests(app)
|
||||
|
||||
// Expanded route for tracking download progress
|
||||
// TODO: Replace by Observer Wrapper (ZeroMQ / Vanilla Websocket)
|
||||
app.register(downloadRouter)
|
||||
}
|
||||
@ -1,112 +0,0 @@
|
||||
import { DownloadRoute } from '../../../api'
|
||||
import { join } from 'path'
|
||||
import { DownloadManager } from '../../download'
|
||||
import { HttpServer } from '../HttpServer'
|
||||
import { createWriteStream } from 'fs'
|
||||
import { getJanDataFolderPath } from '../../utils'
|
||||
import { normalizeFilePath } from '../../path'
|
||||
import { DownloadState } from '../../../types'
|
||||
|
||||
export const downloadRouter = async (app: HttpServer) => {
|
||||
app.get(`/${DownloadRoute.getDownloadProgress}/:modelId`, async (req, res) => {
|
||||
const modelId = req.params.modelId
|
||||
|
||||
console.debug(`Getting download progress for model ${modelId}`)
|
||||
console.debug(
|
||||
`All Download progress: ${JSON.stringify(DownloadManager.instance.downloadProgressMap)}`
|
||||
)
|
||||
|
||||
// check if null DownloadManager.instance.downloadProgressMap
|
||||
if (!DownloadManager.instance.downloadProgressMap[modelId]) {
|
||||
return res.status(404).send({
|
||||
message: 'Download progress not found',
|
||||
})
|
||||
} else {
|
||||
return res.status(200).send(DownloadManager.instance.downloadProgressMap[modelId])
|
||||
}
|
||||
})
|
||||
|
||||
app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
|
||||
const strictSSL = !(req.query.ignoreSSL === 'true')
|
||||
const proxy = req.query.proxy?.startsWith('http') ? req.query.proxy : undefined
|
||||
const body = JSON.parse(req.body as any)
|
||||
const normalizedArgs = body.map((arg: any) => {
|
||||
if (typeof arg === 'string' && arg.startsWith('file:')) {
|
||||
return join(getJanDataFolderPath(), normalizeFilePath(arg))
|
||||
}
|
||||
return arg
|
||||
})
|
||||
|
||||
const localPath = normalizedArgs[1]
|
||||
const array = localPath.split('/')
|
||||
const fileName = array.pop() ?? ''
|
||||
const modelId = array.pop() ?? ''
|
||||
console.debug('downloadFile', normalizedArgs, fileName, modelId)
|
||||
|
||||
const request = require('request')
|
||||
const progress = require('request-progress')
|
||||
|
||||
const rq = request({ url: normalizedArgs[0], strictSSL, proxy })
|
||||
progress(rq, {})
|
||||
.on('progress', function (state: any) {
|
||||
const downloadProps: DownloadState = {
|
||||
...state,
|
||||
modelId,
|
||||
fileName,
|
||||
downloadState: 'downloading',
|
||||
}
|
||||
console.debug(`Download ${modelId} onProgress`, downloadProps)
|
||||
DownloadManager.instance.downloadProgressMap[modelId] = downloadProps
|
||||
})
|
||||
.on('error', function (err: Error) {
|
||||
console.debug(`Download ${modelId} onError`, err.message)
|
||||
|
||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
||||
if (currentDownloadState) {
|
||||
DownloadManager.instance.downloadProgressMap[modelId] = {
|
||||
...currentDownloadState,
|
||||
downloadState: 'error',
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('end', function () {
|
||||
console.debug(`Download ${modelId} onEnd`)
|
||||
|
||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
||||
if (currentDownloadState) {
|
||||
if (currentDownloadState.downloadState === 'downloading') {
|
||||
// if the previous state is downloading, then set the state to end (success)
|
||||
DownloadManager.instance.downloadProgressMap[modelId] = {
|
||||
...currentDownloadState,
|
||||
downloadState: 'end',
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.pipe(createWriteStream(normalizedArgs[1]))
|
||||
|
||||
DownloadManager.instance.setRequest(localPath, rq)
|
||||
res.status(200).send({ message: 'Download started' })
|
||||
})
|
||||
|
||||
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.startsWith('file:')) {
|
||||
return join(getJanDataFolderPath(), normalizeFilePath(arg))
|
||||
}
|
||||
return arg
|
||||
})
|
||||
|
||||
const localPath = normalizedArgs[0]
|
||||
const fileName = localPath.split('/').pop() ?? ''
|
||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||
rq?.abort()
|
||||
if (rq) {
|
||||
res.status(200).send({ message: 'Download aborted' })
|
||||
} else {
|
||||
res.status(404).send({ message: 'Download not found' })
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
import { join, extname } from 'path'
|
||||
import { ExtensionRoute } from '../../../api/index'
|
||||
import { ModuleManager } from '../../module'
|
||||
import { getActiveExtensions, installExtensions } from '../../extension/store'
|
||||
import { HttpServer } from '../HttpServer'
|
||||
|
||||
import { readdirSync } from 'fs'
|
||||
import { getJanExtensionsPath } from '../../utils'
|
||||
|
||||
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) => {
|
||||
const extensions = req.body as any
|
||||
const installed = await installExtensions(JSON.parse(extensions)[0])
|
||||
return JSON.parse(JSON.stringify(installed))
|
||||
})
|
||||
|
||||
app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => {
|
||||
const args = JSON.parse(req.body as any)
|
||||
console.debug(args)
|
||||
const module = await import(join(getJanExtensionsPath(), 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.`)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
import { FileManagerRoute } from '../../../api'
|
||||
import { HttpServer } from '../../index'
|
||||
import { join } from 'path'
|
||||
|
||||
export const fileManagerRouter = async (app: HttpServer) => {
|
||||
app.post(`/fs/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {
|
||||
const reflect = require('@alumna/reflect')
|
||||
const args = JSON.parse(request.body)
|
||||
return reflect({
|
||||
src: args[0],
|
||||
dest: args[1],
|
||||
recursive: true,
|
||||
delete: false,
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
})
|
||||
})
|
||||
|
||||
app.post(`/fs/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) =>
|
||||
global.core.appPath()
|
||||
)
|
||||
|
||||
app.post(`/fs/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) =>
|
||||
join(global.core.appPath(), '../../..')
|
||||
)
|
||||
|
||||
app.post(`/app/${FileManagerRoute.getUserHomePath}`, async (request: any, reply: any) => {})
|
||||
app.post(`/fs/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {})
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
import { FileManagerRoute, FileSystemRoute } from '../../../api'
|
||||
import { join } from 'path'
|
||||
import { HttpServer } from '../HttpServer'
|
||||
import { getJanDataFolderPath } from '../../utils'
|
||||
import { normalizeFilePath } from '../../path'
|
||||
import { writeFileSync } from '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 import(moduleName).then((mdl) => {
|
||||
return mdl[route](
|
||||
...body.map((arg: any) =>
|
||||
typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
|
||||
? join(getJanDataFolderPath(), normalizeFilePath(arg))
|
||||
: arg
|
||||
)
|
||||
)
|
||||
})
|
||||
res.status(200).send(result)
|
||||
} catch (ex) {
|
||||
console.log(ex)
|
||||
}
|
||||
})
|
||||
})
|
||||
app.post(`/${FileManagerRoute.writeBlob}`, async (request: any, reply: any) => {
|
||||
try {
|
||||
const args = JSON.parse(request.body) as any[]
|
||||
console.log('writeBlob:', args[0])
|
||||
const dataBuffer = Buffer.from(args[1], 'base64')
|
||||
writeFileSync(args[0], dataBuffer)
|
||||
} catch (err) {
|
||||
console.error(`writeFile ${request.body} result: ${err}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
export * from './download'
|
||||
export * from './extension'
|
||||
export * from './fs'
|
||||
export * from './thread'
|
||||
export * from './common'
|
||||
export * from './v1'
|
||||
@ -1,30 +0,0 @@
|
||||
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),
|
||||
)
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
import { HttpServer } from '../HttpServer'
|
||||
import { commonRouter } from './common'
|
||||
import { threadRouter } from './thread'
|
||||
import { fsRouter } from './fs'
|
||||
import { extensionRouter } from './extension'
|
||||
import { downloadRouter } from './download'
|
||||
import { fileManagerRouter } from './fileManager'
|
||||
|
||||
export const v1Router = async (app: HttpServer) => {
|
||||
// MARK: External Routes
|
||||
app.register(commonRouter)
|
||||
app.register(threadRouter, {
|
||||
prefix: '/threads',
|
||||
})
|
||||
|
||||
// MARK: Internal Application Routes
|
||||
app.register(fsRouter, {
|
||||
prefix: '/fs',
|
||||
})
|
||||
app.register(fileManagerRouter)
|
||||
|
||||
app.register(extensionRouter, {
|
||||
prefix: '/extension',
|
||||
})
|
||||
app.register(downloadRouter, {
|
||||
prefix: '/download',
|
||||
})
|
||||
}
|
||||
@ -104,7 +104,7 @@ export default class Extension {
|
||||
await pacote.extract(
|
||||
this.specifier,
|
||||
join(ExtensionManager.instance.getExtensionsPath() ?? '', this.name ?? ''),
|
||||
this.installOptions,
|
||||
this.installOptions
|
||||
)
|
||||
|
||||
// Set the url using the custom extensions protocol
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { writeFileSync } from "fs";
|
||||
import Extension from "./extension";
|
||||
import { ExtensionManager } from "./manager";
|
||||
import { writeFileSync } from 'fs'
|
||||
import Extension from './extension'
|
||||
import { ExtensionManager } from './manager'
|
||||
|
||||
/**
|
||||
* @module store
|
||||
@ -11,7 +11,7 @@ import { ExtensionManager } from "./manager";
|
||||
* Register of installed extensions
|
||||
* @type {Object.<string, Extension>} extension - List of installed extensions
|
||||
*/
|
||||
const extensions: Record<string, Extension> = {};
|
||||
const extensions: Record<string, Extension> = {}
|
||||
|
||||
/**
|
||||
* Get a extension from the stored extensions.
|
||||
@ -21,10 +21,10 @@ const extensions: Record<string, Extension> = {};
|
||||
*/
|
||||
export function getExtension(name: string) {
|
||||
if (!Object.prototype.hasOwnProperty.call(extensions, name)) {
|
||||
throw new Error(`Extension ${name} does not exist`);
|
||||
throw new Error(`Extension ${name} does not exist`)
|
||||
}
|
||||
|
||||
return extensions[name];
|
||||
return extensions[name]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -33,7 +33,7 @@ export function getExtension(name: string) {
|
||||
* @alias extensionManager.getAllExtensions
|
||||
*/
|
||||
export function getAllExtensions() {
|
||||
return Object.values(extensions);
|
||||
return Object.values(extensions)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,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)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -53,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
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,14 +77,11 @@ export function addExtension(extension: Extension, persist = true) {
|
||||
* @returns {void}
|
||||
*/
|
||||
export function persistExtensions() {
|
||||
const persistData: Record<string, Extension> = {};
|
||||
const persistData: Record<string, Extension> = {}
|
||||
for (const name in extensions) {
|
||||
persistData[name] = extensions[name];
|
||||
persistData[name] = extensions[name]
|
||||
}
|
||||
writeFileSync(
|
||||
ExtensionManager.instance.getExtensionsFile(),
|
||||
JSON.stringify(persistData),
|
||||
);
|
||||
writeFileSync(ExtensionManager.instance.getExtensionsFile(), JSON.stringify(persistData))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -94,26 +91,29 @@ export function persistExtensions() {
|
||||
* @returns {Promise.<Array.<Extension>>} New extension
|
||||
* @alias extensionManager.installExtensions
|
||||
*/
|
||||
export async function installExtensions(extensions: any, store = true) {
|
||||
const installed: Extension[] = [];
|
||||
export async function installExtensions(extensions: any) {
|
||||
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)
|
||||
if (!extension.origin) {
|
||||
continue
|
||||
}
|
||||
await extension._install()
|
||||
if (activate) extension.setActive(true)
|
||||
|
||||
// Add extension to store if needed
|
||||
if (store) addExtension(extension);
|
||||
installed.push(extension);
|
||||
addExtension(extension)
|
||||
installed.push(extension)
|
||||
}
|
||||
|
||||
// Return list of all installed extensions
|
||||
return installed;
|
||||
return installed
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -2,7 +2,7 @@ import { AppConfiguration, SystemResourceInfo } from '../../types'
|
||||
import { join } from 'path'
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import { log, logServer } from '../log'
|
||||
import { log, logServer } from './log'
|
||||
import childProcess from 'child_process'
|
||||
|
||||
// TODO: move this to core
|
||||
@ -56,34 +56,6 @@ export const updateAppConfiguration = (configuration: AppConfiguration): Promise
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to get server log path
|
||||
*
|
||||
* @returns {string} The log path.
|
||||
*/
|
||||
export const getServerLogPath = (): string => {
|
||||
const appConfigurations = getAppConfigurations()
|
||||
const logFolderPath = join(appConfigurations.data_folder, 'logs')
|
||||
if (!fs.existsSync(logFolderPath)) {
|
||||
fs.mkdirSync(logFolderPath, { recursive: true })
|
||||
}
|
||||
return join(logFolderPath, 'server.log')
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to get app log path
|
||||
*
|
||||
* @returns {string} The log path.
|
||||
*/
|
||||
export const getAppLogPath = (): string => {
|
||||
const appConfigurations = getAppConfigurations()
|
||||
const logFolderPath = join(appConfigurations.data_folder, 'logs')
|
||||
if (!fs.existsSync(logFolderPath)) {
|
||||
fs.mkdirSync(logFolderPath, { recursive: true })
|
||||
}
|
||||
return join(logFolderPath, 'app.log')
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to get data folder path
|
||||
*
|
||||
@ -146,18 +118,6 @@ const exec = async (command: string): Promise<string> => {
|
||||
})
|
||||
}
|
||||
|
||||
export const getSystemResourceInfo = async (): Promise<SystemResourceInfo> => {
|
||||
const cpu = await physicalCpuCount()
|
||||
const message = `[NITRO]::CPU informations - ${cpu}`
|
||||
log(message)
|
||||
logServer(message)
|
||||
|
||||
return {
|
||||
numCpuPhysicalCore: cpu,
|
||||
memAvailable: 0, // TODO: this should not be 0
|
||||
}
|
||||
}
|
||||
|
||||
export const getEngineConfiguration = async (engineId: string) => {
|
||||
if (engineId !== 'openai') {
|
||||
return undefined
|
||||
@ -167,3 +127,31 @@ export const getEngineConfiguration = async (engineId: string) => {
|
||||
const data = fs.readFileSync(filePath, 'utf-8')
|
||||
return JSON.parse(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to get server log path
|
||||
*
|
||||
* @returns {string} The log path.
|
||||
*/
|
||||
export const getServerLogPath = (): string => {
|
||||
const appConfigurations = getAppConfigurations()
|
||||
const logFolderPath = join(appConfigurations.data_folder, 'logs')
|
||||
if (!fs.existsSync(logFolderPath)) {
|
||||
fs.mkdirSync(logFolderPath, { recursive: true })
|
||||
}
|
||||
return join(logFolderPath, 'server.log')
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to get app log path
|
||||
*
|
||||
* @returns {string} The log path.
|
||||
*/
|
||||
export const getAppLogPath = (): string => {
|
||||
const appConfigurations = getAppConfigurations()
|
||||
const logFolderPath = join(appConfigurations.data_folder, 'logs')
|
||||
if (!fs.existsSync(logFolderPath)) {
|
||||
fs.mkdirSync(logFolderPath, { recursive: true })
|
||||
}
|
||||
return join(logFolderPath, 'app.log')
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { DownloadState } from '../types'
|
||||
import { DownloadState } from '../../types'
|
||||
|
||||
/**
|
||||
* Manages file downloads and network requests.
|
||||
6
core/src/node/helper/index.ts
Normal file
6
core/src/node/helper/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './config'
|
||||
export * from './download'
|
||||
export * from './log'
|
||||
export * from './module'
|
||||
export * from './path'
|
||||
export * from './resource'
|
||||
@ -1,6 +1,6 @@
|
||||
import fs from 'fs'
|
||||
import util from 'util'
|
||||
import { getAppLogPath, getServerLogPath } from './utils'
|
||||
import { getAppLogPath, getServerLogPath } from './config'
|
||||
|
||||
export const log = (message: string) => {
|
||||
const path = getAppLogPath()
|
||||
35
core/src/node/helper/path.ts
Normal file
35
core/src/node/helper/path.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { join } from 'path'
|
||||
|
||||
/**
|
||||
* Normalize file path
|
||||
* Remove all file protocol prefix
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
export function normalizeFilePath(path: string): string {
|
||||
return path.replace(/^(file:[\\/]+)([^:\s]+)$/, '$2')
|
||||
}
|
||||
|
||||
export async function appResourcePath(): Promise<string> {
|
||||
let electron: any = undefined
|
||||
|
||||
try {
|
||||
const moduleName = 'electron'
|
||||
electron = await import(moduleName)
|
||||
} catch (err) {
|
||||
console.error('Electron is not available')
|
||||
}
|
||||
|
||||
// electron
|
||||
if (electron && electron.protocol) {
|
||||
let appPath = join(electron.app.getAppPath(), '..', 'app.asar.unpacked')
|
||||
|
||||
if (!electron.app.isPackaged) {
|
||||
// for development mode
|
||||
appPath = join(electron.app.getAppPath())
|
||||
}
|
||||
return appPath
|
||||
}
|
||||
// server
|
||||
return join(global.core.appPath(), '../../..')
|
||||
}
|
||||
15
core/src/node/helper/resource.ts
Normal file
15
core/src/node/helper/resource.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { SystemResourceInfo } from '../../types'
|
||||
import { physicalCpuCount } from './config'
|
||||
import { log, logServer } from './log'
|
||||
|
||||
export const getSystemResourceInfo = async (): Promise<SystemResourceInfo> => {
|
||||
const cpu = await physicalCpuCount()
|
||||
const message = `[NITRO]::CPU informations - ${cpu}`
|
||||
log(message)
|
||||
logServer(message)
|
||||
|
||||
return {
|
||||
numCpuPhysicalCore: cpu,
|
||||
memAvailable: 0, // TODO: this should not be 0
|
||||
}
|
||||
}
|
||||
@ -2,9 +2,5 @@ export * from './extension/index'
|
||||
export * from './extension/extension'
|
||||
export * from './extension/manager'
|
||||
export * from './extension/store'
|
||||
export * from './download'
|
||||
export * from './module'
|
||||
export * from './api'
|
||||
export * from './log'
|
||||
export * from './utils'
|
||||
export * from './path'
|
||||
export * from './helper'
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Normalize file path
|
||||
* Remove all file protocol prefix
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
export function normalizeFilePath(path: string): string {
|
||||
return path.replace(/^(file:[\\/]+)([^:\s]+)$/, "$2");
|
||||
}
|
||||
@ -2,7 +2,6 @@
|
||||
* The `EventName` enumeration contains the names of all the available events in the Jan platform.
|
||||
*/
|
||||
export enum AssistantEvent {
|
||||
/** The `OnAssistantsUpdate` event is emitted when the assistant list is updated. */
|
||||
OnAssistantsUpdate = 'OnAssistantsUpdate',
|
||||
}
|
||||
|
||||
/** The `OnAssistantsUpdate` event is emitted when the assistant list is updated. */
|
||||
OnAssistantsUpdate = 'OnAssistantsUpdate',
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ export type FileStat = {
|
||||
|
||||
export type DownloadState = {
|
||||
modelId: string
|
||||
filename: string
|
||||
fileName: string
|
||||
time: DownloadTime
|
||||
speed: number
|
||||
percent: number
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './messageEntity'
|
||||
export * from './messageInterface'
|
||||
export * from './messageEvent'
|
||||
export * from './messageRequestType'
|
||||
|
||||
@ -27,6 +27,8 @@ export type ThreadMessage = {
|
||||
updated: number
|
||||
/** The additional metadata of this message. **/
|
||||
metadata?: Record<string, unknown>
|
||||
|
||||
type?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,6 +58,8 @@ export type MessageRequest = {
|
||||
/** The thread of this message is belong to. **/
|
||||
// TODO: deprecate threadId field
|
||||
thread?: Thread
|
||||
|
||||
type?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
5
core/src/types/message/messageRequestType.ts
Normal file
5
core/src/types/message/messageRequestType.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum MessageRequestType {
|
||||
Thread = 'Thread',
|
||||
Assistant = 'Assistant',
|
||||
Summary = 'Summary',
|
||||
}
|
||||
@ -10,7 +10,7 @@ export interface ModelInterface {
|
||||
* @param network - Optional object to specify proxy/whether to ignore SSL certificates.
|
||||
* @returns A Promise that resolves when the model has been downloaded.
|
||||
*/
|
||||
downloadModel(model: Model, network?: { ignoreSSL?: boolean, proxy?: string }): Promise<void>
|
||||
downloadModel(model: Model, network?: { ignoreSSL?: boolean; proxy?: string }): Promise<void>
|
||||
|
||||
/**
|
||||
* Cancels the download of a specific model.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { normalizeFilePath } from "../../src/node/path";
|
||||
import { normalizeFilePath } from "../../src/node/helper/path";
|
||||
|
||||
describe("Test file normalize", () => {
|
||||
test("returns no file protocol prefix on Unix", async () => {
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
{
|
||||
"extends": [
|
||||
"tslint-config-standard",
|
||||
"tslint-config-prettier"
|
||||
]
|
||||
}
|
||||
"extends": ["tslint-config-standard", "tslint-config-prettier"]
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ namchuai:
|
||||
url: https://github.com/namchuai
|
||||
image_url: https://avatars.githubusercontent.com/u/10397206?v=4
|
||||
email: james@jan.ai
|
||||
|
||||
|
||||
hiro-v:
|
||||
name: Hiro Vuong
|
||||
title: MLE
|
||||
@ -60,4 +60,17 @@ automaticcat:
|
||||
url: https://github.com/tikikun
|
||||
image_url: https://avatars.githubusercontent.com/u/22268502?v=4
|
||||
email: alan@jan.ai
|
||||
|
||||
|
||||
hieu-jan:
|
||||
name: Henry Ho
|
||||
title: Software Engineer
|
||||
url: https://github.com/hieu-jan
|
||||
image_url: https://avatars.githubusercontent.com/u/150573299?v=4
|
||||
email: hieu@jan.ai
|
||||
|
||||
0xsage:
|
||||
name: Nicole Zhu
|
||||
title: Co-Founder
|
||||
url: https://github.com/0xsage
|
||||
image_url: https://avatars.githubusercontent.com/u/69952136?v=4
|
||||
email: nicole@jan.ai
|
||||
|
||||
102
docs/docs/guides/02-installation/05-docker.md
Normal file
102
docs/docs/guides/02-installation/05-docker.md
Normal file
@ -0,0 +1,102 @@
|
||||
---
|
||||
title: Docker
|
||||
slug: /install/docker
|
||||
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
|
||||
keywords:
|
||||
[
|
||||
Jan AI,
|
||||
Jan,
|
||||
ChatGPT alternative,
|
||||
local AI,
|
||||
private AI,
|
||||
conversational AI,
|
||||
no-subscription fee,
|
||||
large language model,
|
||||
docker installation,
|
||||
]
|
||||
---
|
||||
|
||||
# Installing Jan using Docker
|
||||
|
||||
## Installation
|
||||
|
||||
### Pre-requisites
|
||||
|
||||
:::note
|
||||
|
||||
**Supported OS**: Linux, WSL2 Docker
|
||||
|
||||
:::
|
||||
|
||||
- Docker Engine and Docker Compose are required to run Jan in Docker mode. Follow the [instructions](https://docs.docker.com/engine/install/ubuntu/) below to get started with Docker Engine on Ubuntu.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh ./get-docker.sh --dry-run
|
||||
```
|
||||
|
||||
- If you intend to run Jan in GPU mode, you need to install `nvidia-driver` and `nvidia-docker2`. Follow the instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) for installation.
|
||||
|
||||
### Instructions
|
||||
|
||||
- Run Jan in Docker mode
|
||||
|
||||
- **Option 1**: Run Jan in CPU mode
|
||||
|
||||
```bash
|
||||
docker compose --profile cpu up -d
|
||||
```
|
||||
|
||||
- **Option 2**: Run Jan in GPU mode
|
||||
|
||||
- **Step 1**: Check CUDA compatibility with your NVIDIA driver by running `nvidia-smi` and check the CUDA version in the output
|
||||
|
||||
```bash
|
||||
nvidia-smi
|
||||
|
||||
# Output
|
||||
+---------------------------------------------------------------------------------------+
|
||||
| NVIDIA-SMI 531.18 Driver Version: 531.18 CUDA Version: 12.1 |
|
||||
|-----------------------------------------+----------------------+----------------------+
|
||||
| GPU Name TCC/WDDM | Bus-Id Disp.A | Volatile Uncorr. ECC |
|
||||
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|
||||
| | | MIG M. |
|
||||
|=========================================+======================+======================|
|
||||
| 0 NVIDIA GeForce RTX 4070 Ti WDDM | 00000000:01:00.0 On | N/A |
|
||||
| 0% 44C P8 16W / 285W| 1481MiB / 12282MiB | 2% Default |
|
||||
| | | N/A |
|
||||
+-----------------------------------------+----------------------+----------------------+
|
||||
| 1 NVIDIA GeForce GTX 1660 Ti WDDM | 00000000:02:00.0 Off | N/A |
|
||||
| 0% 49C P8 14W / 120W| 0MiB / 6144MiB | 0% Default |
|
||||
| | | N/A |
|
||||
+-----------------------------------------+----------------------+----------------------+
|
||||
| 2 NVIDIA GeForce GTX 1660 Ti WDDM | 00000000:05:00.0 Off | N/A |
|
||||
| 29% 38C P8 11W / 120W| 0MiB / 6144MiB | 0% Default |
|
||||
| | | N/A |
|
||||
+-----------------------------------------+----------------------+----------------------+
|
||||
|
||||
+---------------------------------------------------------------------------------------+
|
||||
| Processes: |
|
||||
| GPU GI CI PID Type Process name GPU Memory |
|
||||
| ID ID Usage |
|
||||
|=======================================================================================|
|
||||
```
|
||||
|
||||
- **Step 2**: Visit [NVIDIA NGC Catalog ](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda/tags) and find the smallest minor version of image tag that matches your CUDA version (e.g., 12.1 -> 12.1.0)
|
||||
|
||||
- **Step 3**: Update the `Dockerfile.gpu` line number 5 with the latest minor version of the image tag from step 2 (e.g. change `FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 AS base` to `FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 AS base`)
|
||||
|
||||
- **Step 4**: Run command to start Jan in GPU mode
|
||||
|
||||
```bash
|
||||
# GPU mode
|
||||
docker compose --profile gpu up -d
|
||||
```
|
||||
|
||||
This will start the web server and you can access Jan at `http://localhost:3000`.
|
||||
|
||||
:::warning
|
||||
|
||||
- Docker mode is currently only suitable for development and localhost. Production is not supported yet, and the RAG feature is not available in Docker mode.
|
||||
|
||||
:::
|
||||
@ -188,4 +188,6 @@ Troubleshooting tips:
|
||||
|
||||
2. If the issue persists, ensure your (V)RAM is accessible by the application. Some folks have virtual RAM and need additional configuration.
|
||||
|
||||
3. Get help in [Jan Discord](https://discord.gg/mY69SZaMaC).
|
||||
3. If you are facing issues with the installation of RTX issues, please update the NVIDIA driver that supports CUDA 11.7 or higher. Ensure that the CUDA path is added to the environment variable.
|
||||
|
||||
4. Get help in [Jan Discord](https://discord.gg/mY69SZaMaC).
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "consistent",
|
||||
"trailingComma": "es5",
|
||||
"endOfLine": "auto",
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
@ -1,173 +0,0 @@
|
||||
import { app, ipcMain, dialog, shell } from 'electron'
|
||||
import { join, basename, relative as getRelative, isAbsolute } from 'path'
|
||||
import { WindowManager } from './../managers/window'
|
||||
import { getResourcePath } from './../utils/path'
|
||||
import { AppRoute, AppConfiguration } from '@janhq/core'
|
||||
import { ServerConfig, startServer, stopServer } from '@janhq/server'
|
||||
import {
|
||||
ModuleManager,
|
||||
getJanDataFolderPath,
|
||||
getJanExtensionsPath,
|
||||
init,
|
||||
log,
|
||||
logServer,
|
||||
getAppConfigurations,
|
||||
updateAppConfiguration,
|
||||
} from '@janhq/core/node'
|
||||
|
||||
export function handleAppIPCs() {
|
||||
/**
|
||||
* Handles the "openAppDirectory" IPC message by opening the app's user data directory.
|
||||
* The `shell.openPath` method is used to open the directory in the user's default file explorer.
|
||||
* @param _event - The IPC event object.
|
||||
*/
|
||||
ipcMain.handle(AppRoute.openAppDirectory, async (_event) => {
|
||||
shell.openPath(getJanDataFolderPath())
|
||||
})
|
||||
|
||||
/**
|
||||
* Opens a URL in the user's default browser.
|
||||
* @param _event - The IPC event object.
|
||||
* @param url - The URL to open.
|
||||
*/
|
||||
ipcMain.handle(AppRoute.openExternalUrl, async (_event, url) => {
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
/**
|
||||
* Opens a URL in the user's default browser.
|
||||
* @param _event - The IPC event object.
|
||||
* @param url - The URL to open.
|
||||
*/
|
||||
ipcMain.handle(AppRoute.openFileExplore, async (_event, url) => {
|
||||
shell.openPath(url)
|
||||
})
|
||||
|
||||
/**
|
||||
* Joins multiple paths together, respect to the current OS.
|
||||
*/
|
||||
ipcMain.handle(AppRoute.joinPath, async (_event, paths: string[]) =>
|
||||
join(...paths)
|
||||
)
|
||||
|
||||
/**
|
||||
* Checks if the given path is a subdirectory of the given directory.
|
||||
*
|
||||
* @param _event - The IPC event object.
|
||||
* @param from - The path to check.
|
||||
* @param to - The directory to check against.
|
||||
*
|
||||
* @returns {Promise<boolean>} - A promise that resolves with the result.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
AppRoute.isSubdirectory,
|
||||
async (_event, from: string, to: string) => {
|
||||
const relative = getRelative(from, to)
|
||||
const isSubdir =
|
||||
relative && !relative.startsWith('..') && !isAbsolute(relative)
|
||||
|
||||
if (isSubdir === '') return false
|
||||
else return isSubdir
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Retrieve basename from given path, respect to the current OS.
|
||||
*/
|
||||
ipcMain.handle(AppRoute.baseName, async (_event, path: string) =>
|
||||
basename(path)
|
||||
)
|
||||
|
||||
/**
|
||||
* Start Jan API Server.
|
||||
*/
|
||||
ipcMain.handle(AppRoute.startServer, async (_event, configs?: ServerConfig) =>
|
||||
startServer({
|
||||
host: configs?.host,
|
||||
port: configs?.port,
|
||||
isCorsEnabled: configs?.isCorsEnabled,
|
||||
isVerboseEnabled: configs?.isVerboseEnabled,
|
||||
schemaPath: app.isPackaged
|
||||
? join(getResourcePath(), 'docs', 'openapi', 'jan.yaml')
|
||||
: undefined,
|
||||
baseDir: app.isPackaged
|
||||
? join(getResourcePath(), 'docs', 'openapi')
|
||||
: undefined,
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Stop Jan API Server.
|
||||
*/
|
||||
ipcMain.handle(AppRoute.stopServer, stopServer)
|
||||
|
||||
/**
|
||||
* Relaunches the app in production - reload window in development.
|
||||
* @param _event - The IPC event object.
|
||||
* @param url - The URL to reload.
|
||||
*/
|
||||
ipcMain.handle(AppRoute.relaunch, async (_event) => {
|
||||
ModuleManager.instance.clearImportedModules()
|
||||
|
||||
if (app.isPackaged) {
|
||||
app.relaunch()
|
||||
app.exit()
|
||||
} else {
|
||||
for (const modulePath in ModuleManager.instance.requiredModules) {
|
||||
delete require.cache[
|
||||
require.resolve(join(getJanExtensionsPath(), modulePath))
|
||||
]
|
||||
}
|
||||
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: getJanExtensionsPath(),
|
||||
})
|
||||
WindowManager.instance.currentWindow?.reload()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Log message to log file.
|
||||
*/
|
||||
ipcMain.handle(AppRoute.log, async (_event, message) => log(message))
|
||||
|
||||
/**
|
||||
* Log message to log file.
|
||||
*/
|
||||
ipcMain.handle(AppRoute.logServer, async (_event, message) =>
|
||||
logServer(message)
|
||||
)
|
||||
|
||||
ipcMain.handle(AppRoute.selectDirectory, async () => {
|
||||
const mainWindow = WindowManager.instance.currentWindow
|
||||
if (!mainWindow) {
|
||||
console.error('No main window found')
|
||||
return
|
||||
}
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Select a folder',
|
||||
buttonLabel: 'Select Folder',
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
})
|
||||
if (canceled) {
|
||||
return
|
||||
} else {
|
||||
return filePaths[0]
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(AppRoute.getAppConfigurations, async () =>
|
||||
getAppConfigurations()
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
AppRoute.updateAppConfiguration,
|
||||
async (_event, appConfiguration: AppConfiguration) => {
|
||||
await updateAppConfiguration(appConfiguration)
|
||||
}
|
||||
)
|
||||
}
|
||||
25
electron/handlers/common.ts
Normal file
25
electron/handlers/common.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Handler, RequestHandler } from '@janhq/core/node'
|
||||
import { ipcMain } from 'electron'
|
||||
import { WindowManager } from '../managers/window'
|
||||
|
||||
export function injectHandler() {
|
||||
const ipcWrapper: Handler = (
|
||||
route: string,
|
||||
listener: (...args: any[]) => any
|
||||
) => {
|
||||
return ipcMain.handle(route, async (event, ...args: any[]) => {
|
||||
return listener(...args)
|
||||
})
|
||||
}
|
||||
|
||||
const handler = new RequestHandler(
|
||||
ipcWrapper,
|
||||
(channel: string, args: any) => {
|
||||
return WindowManager.instance.currentWindow?.webContents.send(
|
||||
channel,
|
||||
args
|
||||
)
|
||||
}
|
||||
)
|
||||
handler.handle()
|
||||
}
|
||||
@ -1,132 +0,0 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { resolve } from 'path'
|
||||
import { WindowManager } from './../managers/window'
|
||||
import request from 'request'
|
||||
import { createWriteStream, renameSync } from 'fs'
|
||||
import { DownloadEvent, DownloadRoute } from '@janhq/core'
|
||||
const progress = require('request-progress')
|
||||
import {
|
||||
DownloadManager,
|
||||
getJanDataFolderPath,
|
||||
normalizeFilePath,
|
||||
} from '@janhq/core/node'
|
||||
|
||||
export function handleDownloaderIPCs() {
|
||||
/**
|
||||
* Handles the "pauseDownload" IPC message by pausing the download associated with the provided fileName.
|
||||
* @param _event - The IPC event object.
|
||||
* @param fileName - The name of the file being downloaded.
|
||||
*/
|
||||
ipcMain.handle(DownloadRoute.pauseDownload, async (_event, fileName) => {
|
||||
DownloadManager.instance.networkRequests[fileName]?.pause()
|
||||
})
|
||||
|
||||
/**
|
||||
* Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName.
|
||||
* @param _event - The IPC event object.
|
||||
* @param fileName - The name of the file being downloaded.
|
||||
*/
|
||||
ipcMain.handle(DownloadRoute.resumeDownload, async (_event, fileName) => {
|
||||
DownloadManager.instance.networkRequests[fileName]?.resume()
|
||||
})
|
||||
|
||||
/**
|
||||
* Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName.
|
||||
* The network request associated with the fileName is then removed from the networkRequests object.
|
||||
* @param _event - The IPC event object.
|
||||
* @param fileName - The name of the file being downloaded.
|
||||
*/
|
||||
ipcMain.handle(DownloadRoute.abortDownload, async (_event, fileName) => {
|
||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||
if (rq) {
|
||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||
rq?.abort()
|
||||
} else {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadError,
|
||||
{
|
||||
fileName,
|
||||
err: { message: 'aborted' },
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Downloads a file from a given URL.
|
||||
* @param _event - The IPC event object.
|
||||
* @param url - The URL to download the file from.
|
||||
* @param fileName - The name to give the downloaded file.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
DownloadRoute.downloadFile,
|
||||
async (_event, url, localPath, network) => {
|
||||
const strictSSL = !network?.ignoreSSL
|
||||
const proxy = network?.proxy?.startsWith('http')
|
||||
? network.proxy
|
||||
: undefined
|
||||
if (typeof localPath === 'string') {
|
||||
localPath = normalizeFilePath(localPath)
|
||||
}
|
||||
const array = localPath.split('/')
|
||||
const fileName = array.pop() ?? ''
|
||||
const modelId = array.pop() ?? ''
|
||||
|
||||
const destination = resolve(getJanDataFolderPath(), localPath)
|
||||
const rq = request({ url, strictSSL, proxy })
|
||||
|
||||
// Put request to download manager instance
|
||||
DownloadManager.instance.setRequest(localPath, rq)
|
||||
|
||||
// Downloading file to a temp file first
|
||||
const downloadingTempFile = `${destination}.download`
|
||||
|
||||
progress(rq, {})
|
||||
.on('progress', function (state: any) {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadUpdate,
|
||||
{
|
||||
...state,
|
||||
fileName,
|
||||
modelId,
|
||||
}
|
||||
)
|
||||
})
|
||||
.on('error', function (err: Error) {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadError,
|
||||
{
|
||||
fileName,
|
||||
err,
|
||||
modelId,
|
||||
}
|
||||
)
|
||||
})
|
||||
.on('end', function () {
|
||||
if (DownloadManager.instance.networkRequests[localPath]) {
|
||||
// Finished downloading, rename temp file to actual file
|
||||
renameSync(downloadingTempFile, destination)
|
||||
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadSuccess,
|
||||
{
|
||||
fileName,
|
||||
modelId,
|
||||
}
|
||||
)
|
||||
DownloadManager.instance.setRequest(localPath, undefined)
|
||||
} else {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadError,
|
||||
{
|
||||
fileName,
|
||||
modelId,
|
||||
err: { message: 'aborted' },
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
.pipe(createWriteStream(downloadingTempFile))
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
import { ipcMain, webContents } from 'electron'
|
||||
import { readdirSync } from 'fs'
|
||||
import { join, extname } from 'path'
|
||||
|
||||
import {
|
||||
installExtensions,
|
||||
getExtension,
|
||||
removeExtension,
|
||||
getActiveExtensions,
|
||||
ModuleManager,
|
||||
getJanExtensionsPath,
|
||||
} from '@janhq/core/node'
|
||||
|
||||
import { getResourcePath } from './../utils/path'
|
||||
import { ExtensionRoute } from '@janhq/core'
|
||||
|
||||
export function handleExtensionIPCs() {
|
||||
/**MARK: General handlers */
|
||||
/**
|
||||
* Invokes a function from a extension module in main node process.
|
||||
* @param _event - The IPC event object.
|
||||
* @param modulePath - The path to the extension module.
|
||||
* @param method - The name of the function to invoke.
|
||||
* @param args - The arguments to pass to the function.
|
||||
* @returns The result of the invoked function.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
ExtensionRoute.invokeExtensionFunc,
|
||||
async (_event, modulePath, method, ...args) => {
|
||||
const module = require(
|
||||
/* webpackIgnore: true */ join(getJanExtensionsPath(), modulePath)
|
||||
)
|
||||
ModuleManager.instance.setModule(modulePath, module)
|
||||
|
||||
if (typeof module[method] === 'function') {
|
||||
return module[method](...args)
|
||||
} else {
|
||||
console.debug(module[method])
|
||||
console.error(`Function "${method}" does not exist in the module.`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns the paths of the base extensions.
|
||||
* @param _event - The IPC event object.
|
||||
* @returns An array of paths to the base extensions.
|
||||
*/
|
||||
ipcMain.handle(ExtensionRoute.baseExtensions, async (_event) => {
|
||||
const baseExtensionPath = join(getResourcePath(), 'pre-install')
|
||||
return readdirSync(baseExtensionPath)
|
||||
.filter((file) => extname(file) === '.tgz')
|
||||
.map((file) => join(baseExtensionPath, file))
|
||||
})
|
||||
|
||||
/**MARK: Extension Manager handlers */
|
||||
ipcMain.handle(ExtensionRoute.installExtension, async (e, extensions) => {
|
||||
// Install and activate all provided extensions
|
||||
const installed = await installExtensions(extensions)
|
||||
return JSON.parse(JSON.stringify(installed))
|
||||
})
|
||||
|
||||
// Register IPC route to uninstall a extension
|
||||
ipcMain.handle(
|
||||
ExtensionRoute.uninstallExtension,
|
||||
async (e, extensions, reload) => {
|
||||
// Uninstall all provided extensions
|
||||
for (const ext of extensions) {
|
||||
const extension = getExtension(ext)
|
||||
await extension.uninstall()
|
||||
if (extension.name) removeExtension(extension.name)
|
||||
}
|
||||
|
||||
// Reload all renderer pages if needed
|
||||
reload && webContents.getAllWebContents().forEach((wc) => wc.reload())
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
||||
// Register IPC route to update a extension
|
||||
ipcMain.handle(
|
||||
ExtensionRoute.updateExtension,
|
||||
async (e, extensions, reload) => {
|
||||
// Update all provided extensions
|
||||
const updated: any[] = []
|
||||
for (const ext of extensions) {
|
||||
const extension = getExtension(ext)
|
||||
const res = await extension.update()
|
||||
if (res) updated.push(extension)
|
||||
}
|
||||
|
||||
// Reload all renderer pages if needed
|
||||
if (updated.length && reload)
|
||||
webContents.getAllWebContents().forEach((wc) => wc.reload())
|
||||
|
||||
return JSON.parse(JSON.stringify(updated))
|
||||
}
|
||||
)
|
||||
|
||||
// Register IPC route to get the list of active extensions
|
||||
ipcMain.handle(ExtensionRoute.getActiveExtensions, () => {
|
||||
return JSON.parse(JSON.stringify(getActiveExtensions()))
|
||||
})
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
import { ipcMain, app } from 'electron'
|
||||
// @ts-ignore
|
||||
import reflect from '@alumna/reflect'
|
||||
|
||||
import { FileManagerRoute, FileStat } from '@janhq/core'
|
||||
import { getResourcePath } from './../utils/path'
|
||||
import fs from 'fs'
|
||||
import { join } from 'path'
|
||||
import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
|
||||
|
||||
/**
|
||||
* Handles file system extensions operations.
|
||||
*/
|
||||
export function handleFileMangerIPCs() {
|
||||
// Handles the 'syncFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path.
|
||||
ipcMain.handle(
|
||||
FileManagerRoute.syncFile,
|
||||
async (_event, src: string, dest: string) => {
|
||||
return reflect({
|
||||
src,
|
||||
dest,
|
||||
recursive: true,
|
||||
delete: false,
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Handles the 'getJanDataFolderPath' IPC event. This event is triggered to get the user space path.
|
||||
ipcMain.handle(
|
||||
FileManagerRoute.getJanDataFolderPath,
|
||||
(): Promise<string> => Promise.resolve(getJanDataFolderPath())
|
||||
)
|
||||
|
||||
// Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path.
|
||||
ipcMain.handle(FileManagerRoute.getResourcePath, async (_event) =>
|
||||
getResourcePath()
|
||||
)
|
||||
|
||||
// Handles the 'getUserHomePath' IPC event. This event is triggered to get the user home path.
|
||||
ipcMain.handle(FileManagerRoute.getUserHomePath, async (_event) =>
|
||||
app.getPath('home')
|
||||
)
|
||||
|
||||
// handle fs is directory here
|
||||
ipcMain.handle(
|
||||
FileManagerRoute.fileStat,
|
||||
async (_event, path: string): Promise<FileStat | undefined> => {
|
||||
const normalizedPath = normalizeFilePath(path)
|
||||
|
||||
const fullPath = join(getJanDataFolderPath(), normalizedPath)
|
||||
const isExist = fs.existsSync(fullPath)
|
||||
if (!isExist) return undefined
|
||||
|
||||
const isDirectory = fs.lstatSync(fullPath).isDirectory()
|
||||
const size = fs.statSync(fullPath).size
|
||||
|
||||
const fileStat: FileStat = {
|
||||
isDirectory,
|
||||
size,
|
||||
}
|
||||
|
||||
return fileStat
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
FileManagerRoute.writeBlob,
|
||||
async (_event, path: string, data: string): Promise<void> => {
|
||||
try {
|
||||
const normalizedPath = normalizeFilePath(path)
|
||||
const dataBuffer = Buffer.from(data, 'base64')
|
||||
fs.writeFileSync(
|
||||
join(getJanDataFolderPath(), normalizedPath),
|
||||
dataBuffer
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(`writeFile ${path} result: ${err}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
|
||||
import { FileSystemRoute } from '@janhq/core'
|
||||
import { join } from 'path'
|
||||
/**
|
||||
* Handles file system operations.
|
||||
*/
|
||||
export function handleFsIPCs() {
|
||||
const moduleName = 'fs'
|
||||
Object.values(FileSystemRoute).forEach((route) => {
|
||||
ipcMain.handle(route, async (event, ...args) => {
|
||||
return import(moduleName).then((mdl) =>
|
||||
mdl[route](
|
||||
...args.map((arg) =>
|
||||
typeof arg === 'string' &&
|
||||
(arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
|
||||
? join(getJanDataFolderPath(), normalizeFilePath(arg))
|
||||
: arg
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
86
electron/handlers/native.ts
Normal file
86
electron/handlers/native.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { app, ipcMain, dialog, shell } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { WindowManager } from '../managers/window'
|
||||
import {
|
||||
ModuleManager,
|
||||
getJanDataFolderPath,
|
||||
getJanExtensionsPath,
|
||||
init,
|
||||
} from '@janhq/core/node'
|
||||
import { NativeRoute } from '@janhq/core'
|
||||
|
||||
export function handleAppIPCs() {
|
||||
/**
|
||||
* Handles the "openAppDirectory" IPC message by opening the app's user data directory.
|
||||
* The `shell.openPath` method is used to open the directory in the user's default file explorer.
|
||||
* @param _event - The IPC event object.
|
||||
*/
|
||||
ipcMain.handle(NativeRoute.openAppDirectory, async (_event) => {
|
||||
shell.openPath(getJanDataFolderPath())
|
||||
})
|
||||
|
||||
/**
|
||||
* Opens a URL in the user's default browser.
|
||||
* @param _event - The IPC event object.
|
||||
* @param url - The URL to open.
|
||||
*/
|
||||
ipcMain.handle(NativeRoute.openExternalUrl, async (_event, url) => {
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
/**
|
||||
* Opens a URL in the user's default browser.
|
||||
* @param _event - The IPC event object.
|
||||
* @param url - The URL to open.
|
||||
*/
|
||||
ipcMain.handle(NativeRoute.openFileExplore, async (_event, url) => {
|
||||
shell.openPath(url)
|
||||
})
|
||||
|
||||
/**
|
||||
* Relaunches the app in production - reload window in development.
|
||||
* @param _event - The IPC event object.
|
||||
* @param url - The URL to reload.
|
||||
*/
|
||||
ipcMain.handle(NativeRoute.relaunch, async (_event) => {
|
||||
ModuleManager.instance.clearImportedModules()
|
||||
|
||||
if (app.isPackaged) {
|
||||
app.relaunch()
|
||||
app.exit()
|
||||
} else {
|
||||
for (const modulePath in ModuleManager.instance.requiredModules) {
|
||||
delete require.cache[
|
||||
require.resolve(join(getJanExtensionsPath(), modulePath))
|
||||
]
|
||||
}
|
||||
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: getJanExtensionsPath(),
|
||||
})
|
||||
WindowManager.instance.currentWindow?.reload()
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(NativeRoute.selectDirectory, async () => {
|
||||
const mainWindow = WindowManager.instance.currentWindow
|
||||
if (!mainWindow) {
|
||||
console.error('No main window found')
|
||||
return
|
||||
}
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Select a folder',
|
||||
buttonLabel: 'Select Folder',
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
})
|
||||
if (canceled) {
|
||||
return
|
||||
} else {
|
||||
return filePaths[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -36,7 +36,7 @@ export function handleAppUpdates() {
|
||||
autoUpdater.on('error', (info: any) => {
|
||||
WindowManager.instance.currentWindow?.webContents.send(
|
||||
AppEvent.onAppUpdateDownloadError,
|
||||
{}
|
||||
info
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import { app, BrowserWindow, shell } from 'electron'
|
||||
import { join } from 'path'
|
||||
/**
|
||||
* Managers
|
||||
@ -9,12 +9,9 @@ import { log } from '@janhq/core/node'
|
||||
/**
|
||||
* IPC Handlers
|
||||
**/
|
||||
import { handleDownloaderIPCs } from './handlers/download'
|
||||
import { handleExtensionIPCs } from './handlers/extension'
|
||||
import { handleFileMangerIPCs } from './handlers/fileManager'
|
||||
import { handleAppIPCs } from './handlers/app'
|
||||
import { injectHandler } from './handlers/common'
|
||||
import { handleAppUpdates } from './handlers/update'
|
||||
import { handleFsIPCs } from './handlers/fs'
|
||||
import { handleAppIPCs } from './handlers/native'
|
||||
|
||||
/**
|
||||
* Utils
|
||||
@ -25,25 +22,12 @@ import { migrateExtensions } from './utils/migration'
|
||||
import { cleanUpAndQuit } from './utils/clean'
|
||||
import { setupExtensions } from './utils/extension'
|
||||
import { setupCore } from './utils/setup'
|
||||
import { setupReactDevTool } from './utils/dev'
|
||||
import { cleanLogs } from './utils/log'
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(async () => {
|
||||
if (!app.isPackaged) {
|
||||
// Which means you're running from source code
|
||||
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
|
||||
'electron-devtools-installer'
|
||||
) // Don't use import on top level, since the installer package is dev-only
|
||||
try {
|
||||
const name = installExtension(REACT_DEVELOPER_TOOLS)
|
||||
console.log(`Added Extension: ${name}`)
|
||||
} catch (err) {
|
||||
console.log('An error occurred while installing devtools:')
|
||||
console.error(err)
|
||||
// Only log the error and don't throw it because it's not critical
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(setupReactDevTool)
|
||||
.then(setupCore)
|
||||
.then(createUserSpace)
|
||||
.then(migrateExtensions)
|
||||
@ -59,6 +43,7 @@ app
|
||||
}
|
||||
})
|
||||
})
|
||||
.then(() => cleanLogs())
|
||||
|
||||
app.once('window-all-closed', () => {
|
||||
cleanUpAndQuit()
|
||||
@ -92,7 +77,7 @@ function createMainWindow() {
|
||||
|
||||
/* Open external links in the default browser */
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
require('electron').shell.openExternal(url)
|
||||
shell.openExternal(url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
@ -104,11 +89,11 @@ function createMainWindow() {
|
||||
* Handles various IPC messages from the renderer process.
|
||||
*/
|
||||
function handleIPCs() {
|
||||
handleFsIPCs()
|
||||
handleDownloaderIPCs()
|
||||
handleExtensionIPCs()
|
||||
// Inject core handlers for IPCs
|
||||
injectHandler()
|
||||
|
||||
// Handle native IPCs
|
||||
handleAppIPCs()
|
||||
handleFileMangerIPCs()
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@ -9,7 +9,9 @@ const file3 = args[2]
|
||||
|
||||
// check that all arguments are present and throw error instead
|
||||
if (!file1 || !file2 || !file3) {
|
||||
throw new Error('Please provide 3 file paths as arguments: path to file1, to file2 and destination path')
|
||||
throw new Error(
|
||||
'Please provide 3 file paths as arguments: path to file1, to file2 and destination path'
|
||||
)
|
||||
}
|
||||
|
||||
const doc1 = yaml.load(fs.readFileSync(file1, 'utf8'))
|
||||
|
||||
@ -11,7 +11,6 @@
|
||||
"productName": "Jan",
|
||||
"files": [
|
||||
"renderer/**/*",
|
||||
"build/*.{js,map}",
|
||||
"build/**/*.{js,map}",
|
||||
"pre-install",
|
||||
"models/**/*",
|
||||
@ -64,11 +63,11 @@
|
||||
"build:test:darwin": "tsc -p . && electron-builder -p never -m --dir",
|
||||
"build:test:win32": "tsc -p . && electron-builder -p never -w --dir",
|
||||
"build:test:linux": "tsc -p . && electron-builder -p never -l --dir",
|
||||
"build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
|
||||
"build:darwin": "tsc -p . && electron-builder -p never -m",
|
||||
"build:win32": "tsc -p . && electron-builder -p never -w",
|
||||
"build:linux": "tsc -p . && electron-builder -p never -l deb -l AppImage",
|
||||
"build:publish": "yarn copy:assets && run-script-os",
|
||||
"build:publish:darwin": "tsc -p . && electron-builder -p always -m --x64 --arm64",
|
||||
"build:publish:darwin": "tsc -p . && electron-builder -p always -m",
|
||||
"build:publish:win32": "tsc -p . && electron-builder -p always -w",
|
||||
"build:publish:linux": "tsc -p . && electron-builder -p always -l deb -l AppImage"
|
||||
},
|
||||
@ -77,7 +76,6 @@
|
||||
"@janhq/core": "link:./core",
|
||||
"@janhq/server": "link:./server",
|
||||
"@npmcli/arborist": "^7.1.0",
|
||||
"@types/request": "^2.48.12",
|
||||
"@uiball/loaders": "^1.3.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
@ -86,8 +84,6 @@
|
||||
"pacote": "^17.0.4",
|
||||
"request": "^2.88.2",
|
||||
"request-progress": "^3.0.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"typescript": "^5.2.2",
|
||||
"ulid": "^2.3.0",
|
||||
"use-debounce": "^9.0.4"
|
||||
},
|
||||
@ -96,6 +92,7 @@
|
||||
"@playwright/test": "^1.38.1",
|
||||
"@types/npmcli__arborist": "^5.6.4",
|
||||
"@types/pacote": "^11.1.7",
|
||||
"@types/request": "^2.48.12",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"electron": "28.0.0",
|
||||
@ -103,7 +100,9 @@
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-playwright-helpers": "^1.6.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"run-script-os": "^1.1.6"
|
||||
"rimraf": "^5.0.5",
|
||||
"run-script-os": "^1.1.6",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"installConfig": {
|
||||
"hoistingLimits": "workspaces"
|
||||
|
||||
@ -3,14 +3,12 @@ import { PlaywrightTestConfig } from '@playwright/test'
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './tests/e2e',
|
||||
retries: 0,
|
||||
globalTimeout: 300000,
|
||||
globalTimeout: 350000,
|
||||
use: {
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
trace: 'retain-on-failure',
|
||||
},
|
||||
|
||||
reporter: [['html', { outputFolder: './playwright-report' }]],
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
@ -1,44 +1,48 @@
|
||||
const { exec } = require('child_process');
|
||||
const { exec } = require('child_process')
|
||||
|
||||
function sign({
|
||||
path,
|
||||
name,
|
||||
certUrl,
|
||||
clientId,
|
||||
tenantId,
|
||||
clientSecret,
|
||||
certName,
|
||||
timestampServer,
|
||||
version,
|
||||
}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const command = `azuresigntool.exe sign -kvu "${certUrl}" -kvi "${clientId}" -kvt "${tenantId}" -kvs "${clientSecret}" -kvc "${certName}" -tr "${timestampServer}" -v "${path}"`
|
||||
|
||||
function sign({ path, name, certUrl, clientId, tenantId, clientSecret, certName, timestampServer, version }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const command = `azuresigntool.exe sign -kvu "${certUrl}" -kvi "${clientId}" -kvt "${tenantId}" -kvs "${clientSecret}" -kvc "${certName}" -tr "${timestampServer}" -v "${path}"`;
|
||||
|
||||
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Error: ${error}`);
|
||||
return reject(error);
|
||||
}
|
||||
console.log(`stdout: ${stdout}`);
|
||||
console.error(`stderr: ${stderr}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Error: ${error}`)
|
||||
return reject(error)
|
||||
}
|
||||
console.log(`stdout: ${stdout}`)
|
||||
console.error(`stderr: ${stderr}`)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
exports.default = async function (options) {
|
||||
const certUrl = process.env.AZURE_KEY_VAULT_URI
|
||||
const clientId = process.env.AZURE_CLIENT_ID
|
||||
const tenantId = process.env.AZURE_TENANT_ID
|
||||
const clientSecret = process.env.AZURE_CLIENT_SECRET
|
||||
const certName = process.env.AZURE_CERT_NAME
|
||||
const timestampServer = 'http://timestamp.globalsign.com/tsa/r6advanced1'
|
||||
|
||||
exports.default = async function(options) {
|
||||
|
||||
const certUrl = process.env.AZURE_KEY_VAULT_URI;
|
||||
const clientId = process.env.AZURE_CLIENT_ID;
|
||||
const tenantId = process.env.AZURE_TENANT_ID;
|
||||
const clientSecret = process.env.AZURE_CLIENT_SECRET;
|
||||
const certName = process.env.AZURE_CERT_NAME;
|
||||
const timestampServer = 'http://timestamp.globalsign.com/tsa/r6advanced1';
|
||||
|
||||
|
||||
await sign({
|
||||
path: options.path,
|
||||
name: "jan-win-x64",
|
||||
certUrl,
|
||||
clientId,
|
||||
tenantId,
|
||||
clientSecret,
|
||||
certName,
|
||||
timestampServer,
|
||||
version: options.version
|
||||
});
|
||||
};
|
||||
await sign({
|
||||
path: options.path,
|
||||
name: 'jan-win-x64',
|
||||
certUrl,
|
||||
clientId,
|
||||
tenantId,
|
||||
clientSecret,
|
||||
certName,
|
||||
timestampServer,
|
||||
version: options.version,
|
||||
})
|
||||
}
|
||||
|
||||
4
electron/tests/config/constants.ts
Normal file
4
electron/tests/config/constants.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const Constants = {
|
||||
VIDEO_DIR: './playwright-video',
|
||||
TIMEOUT: '300000',
|
||||
}
|
||||
119
electron/tests/config/fixtures.ts
Normal file
119
electron/tests/config/fixtures.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import {
|
||||
_electron as electron,
|
||||
BrowserContext,
|
||||
ElectronApplication,
|
||||
expect,
|
||||
Page,
|
||||
test as base,
|
||||
} from '@playwright/test'
|
||||
import {
|
||||
ElectronAppInfo,
|
||||
findLatestBuild,
|
||||
parseElectronApp,
|
||||
stubDialog,
|
||||
} from 'electron-playwright-helpers'
|
||||
import { Constants } from './constants'
|
||||
import { HubPage } from '../pages/hubPage'
|
||||
import { CommonActions } from '../pages/commonActions'
|
||||
|
||||
export let electronApp: ElectronApplication
|
||||
export let page: Page
|
||||
export let appInfo: ElectronAppInfo
|
||||
export const TIMEOUT = parseInt(process.env.TEST_TIMEOUT || Constants.TIMEOUT)
|
||||
|
||||
export async function setupElectron() {
|
||||
process.env.CI = 'e2e'
|
||||
|
||||
const latestBuild = findLatestBuild('dist')
|
||||
expect(latestBuild).toBeTruthy()
|
||||
|
||||
// parse the packaged Electron app and find paths and other info
|
||||
appInfo = parseElectronApp(latestBuild)
|
||||
expect(appInfo).toBeTruthy()
|
||||
|
||||
electronApp = await electron.launch({
|
||||
args: [appInfo.main], // main file from package.json
|
||||
executablePath: appInfo.executable, // path to the Electron executable
|
||||
// recordVideo: { dir: Constants.VIDEO_DIR }, // Specify the directory for video recordings
|
||||
})
|
||||
await stubDialog(electronApp, 'showMessageBox', { response: 1 })
|
||||
|
||||
page = await electronApp.firstWindow({
|
||||
timeout: TIMEOUT,
|
||||
})
|
||||
}
|
||||
|
||||
export async function teardownElectron() {
|
||||
await page.close()
|
||||
await electronApp.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* this fixture is needed to record and attach videos / screenshot on failed tests when
|
||||
* tests are run in serial mode (i.e. browser is not closed between tests)
|
||||
*/
|
||||
export const test = base.extend<
|
||||
{
|
||||
commonActions: CommonActions
|
||||
hubPage: HubPage
|
||||
attachVideoPage: Page
|
||||
attachScreenshotsToReport: void
|
||||
},
|
||||
{ createVideoContext: BrowserContext }
|
||||
>({
|
||||
commonActions: async ({ request }, use, testInfo) => {
|
||||
await use(new CommonActions(page, testInfo))
|
||||
},
|
||||
hubPage: async ({ commonActions }, use) => {
|
||||
await use(new HubPage(page, commonActions))
|
||||
},
|
||||
createVideoContext: [
|
||||
async ({ playwright }, use) => {
|
||||
const context = electronApp.context()
|
||||
await use(context)
|
||||
},
|
||||
{ scope: 'worker' },
|
||||
],
|
||||
|
||||
attachVideoPage: [
|
||||
async ({ createVideoContext }, use, testInfo) => {
|
||||
await use(page)
|
||||
|
||||
if (testInfo.status !== testInfo.expectedStatus) {
|
||||
const path = await createVideoContext.pages()[0].video()?.path()
|
||||
await createVideoContext.close()
|
||||
await testInfo.attach('video', {
|
||||
path: path,
|
||||
})
|
||||
}
|
||||
},
|
||||
{ scope: 'test', auto: true },
|
||||
],
|
||||
|
||||
attachScreenshotsToReport: [
|
||||
async ({ commonActions }, use, testInfo) => {
|
||||
await use()
|
||||
|
||||
// After the test, we can check whether the test passed or failed.
|
||||
if (testInfo.status !== testInfo.expectedStatus) {
|
||||
await commonActions.takeScreenshot('')
|
||||
}
|
||||
},
|
||||
{ auto: true },
|
||||
],
|
||||
})
|
||||
|
||||
test.setTimeout(TIMEOUT)
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await setupElectron()
|
||||
await page.waitForSelector('img[alt="Jan - Logo"]', {
|
||||
state: 'visible',
|
||||
timeout: TIMEOUT,
|
||||
})
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
// temporally disabling this due to the config for parallel testing WIP
|
||||
// teardownElectron()
|
||||
})
|
||||
@ -1,34 +1,19 @@
|
||||
import {
|
||||
page,
|
||||
test,
|
||||
setupElectron,
|
||||
teardownElectron,
|
||||
TIMEOUT,
|
||||
} from '../pages/basePage'
|
||||
import { test, appInfo } from '../config/fixtures'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
test.beforeAll(async () => {
|
||||
const appInfo = await setupElectron()
|
||||
expect(appInfo.asar).toBe(true)
|
||||
expect(appInfo.executable).toBeTruthy()
|
||||
expect(appInfo.main).toBeTruthy()
|
||||
expect(appInfo.name).toBe('jan')
|
||||
expect(appInfo.packageJson).toBeTruthy()
|
||||
expect(appInfo.packageJson.name).toBe('jan')
|
||||
expect(appInfo.platform).toBeTruthy()
|
||||
expect(appInfo.platform).toBe(process.platform)
|
||||
expect(appInfo.resourcesDir).toBeTruthy()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await teardownElectron()
|
||||
})
|
||||
|
||||
test('explores hub', async () => {
|
||||
await page.getByTestId('Hub').first().click({
|
||||
timeout: TIMEOUT,
|
||||
})
|
||||
await page.getByTestId('hub-container-test-id').isVisible({
|
||||
timeout: TIMEOUT,
|
||||
expect(appInfo).toMatchObject({
|
||||
asar: true,
|
||||
executable: expect.anything(),
|
||||
main: expect.anything(),
|
||||
name: 'jan',
|
||||
packageJson: expect.objectContaining({ name: 'jan' }),
|
||||
platform: process.platform,
|
||||
resourcesDir: expect.anything(),
|
||||
})
|
||||
})
|
||||
|
||||
test('explores hub', async ({ hubPage }) => {
|
||||
await hubPage.navigateByMenu()
|
||||
await hubPage.verifyContainerVisible()
|
||||
})
|
||||
|
||||
@ -1,19 +1,5 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import {
|
||||
page,
|
||||
setupElectron,
|
||||
TIMEOUT,
|
||||
test,
|
||||
teardownElectron,
|
||||
} from '../pages/basePage'
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await setupElectron()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await teardownElectron()
|
||||
})
|
||||
import { page, test, TIMEOUT } from '../config/fixtures'
|
||||
|
||||
test('renders left navigation panel', async () => {
|
||||
const systemMonitorBtn = await page
|
||||
|
||||
@ -1,23 +1,11 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import {
|
||||
setupElectron,
|
||||
teardownElectron,
|
||||
test,
|
||||
page,
|
||||
TIMEOUT,
|
||||
} from '../pages/basePage'
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await setupElectron()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await teardownElectron()
|
||||
})
|
||||
import { test, page, TIMEOUT } from '../config/fixtures'
|
||||
|
||||
test('shows settings', async () => {
|
||||
await page.getByTestId('Settings').first().click({ timeout: TIMEOUT })
|
||||
await page.getByTestId('Settings').first().click({
|
||||
timeout: TIMEOUT,
|
||||
})
|
||||
const settingDescription = page.getByTestId('testid-setting-description')
|
||||
await expect(settingDescription).toBeVisible({ timeout: TIMEOUT })
|
||||
})
|
||||
|
||||
@ -1,67 +1,49 @@
|
||||
import {
|
||||
expect,
|
||||
test as base,
|
||||
_electron as electron,
|
||||
ElectronApplication,
|
||||
Page,
|
||||
} from '@playwright/test'
|
||||
import {
|
||||
findLatestBuild,
|
||||
parseElectronApp,
|
||||
stubDialog,
|
||||
} from 'electron-playwright-helpers'
|
||||
import { Page, expect } from '@playwright/test'
|
||||
import { CommonActions } from './commonActions'
|
||||
import { TIMEOUT } from '../config/fixtures'
|
||||
|
||||
export const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000')
|
||||
export class BasePage {
|
||||
menuId: string
|
||||
|
||||
export let electronApp: ElectronApplication
|
||||
export let page: Page
|
||||
constructor(
|
||||
protected readonly page: Page,
|
||||
readonly action: CommonActions,
|
||||
protected containerId: string
|
||||
) {}
|
||||
|
||||
export async function setupElectron() {
|
||||
process.env.CI = 'e2e'
|
||||
public getValue(key: string) {
|
||||
return this.action.getValue(key)
|
||||
}
|
||||
|
||||
const latestBuild = findLatestBuild('dist')
|
||||
expect(latestBuild).toBeTruthy()
|
||||
public setValue(key: string, value: string) {
|
||||
this.action.setValue(key, value)
|
||||
}
|
||||
|
||||
// parse the packaged Electron app and find paths and other info
|
||||
const appInfo = parseElectronApp(latestBuild)
|
||||
expect(appInfo).toBeTruthy()
|
||||
async takeScreenshot(name: string = '') {
|
||||
await this.action.takeScreenshot(name)
|
||||
}
|
||||
|
||||
electronApp = await electron.launch({
|
||||
args: [appInfo.main], // main file from package.json
|
||||
executablePath: appInfo.executable, // path to the Electron executable
|
||||
})
|
||||
await stubDialog(electronApp, 'showMessageBox', { response: 1 })
|
||||
async navigateByMenu() {
|
||||
await this.page.getByTestId(this.menuId).first().click()
|
||||
}
|
||||
|
||||
page = await electronApp.firstWindow({
|
||||
timeout: TIMEOUT,
|
||||
})
|
||||
// Return appInfo for future use
|
||||
return appInfo
|
||||
async verifyContainerVisible() {
|
||||
const container = this.page.getByTestId(this.containerId)
|
||||
expect(container.isVisible()).toBeTruthy()
|
||||
}
|
||||
|
||||
async waitUpdateLoader() {
|
||||
await this.isElementVisible('img[alt="Jan - Logo"]')
|
||||
}
|
||||
|
||||
//wait and find a specific element with it's selector and return Visible
|
||||
async isElementVisible(selector: any) {
|
||||
let isVisible = true
|
||||
await this.page
|
||||
.waitForSelector(selector, { state: 'visible', timeout: TIMEOUT })
|
||||
.catch(() => {
|
||||
isVisible = false
|
||||
})
|
||||
return isVisible
|
||||
}
|
||||
}
|
||||
|
||||
export async function teardownElectron() {
|
||||
await page.close()
|
||||
await electronApp.close()
|
||||
}
|
||||
|
||||
export const test = base.extend<{
|
||||
attachScreenshotsToReport: void
|
||||
}>({
|
||||
attachScreenshotsToReport: [
|
||||
async ({ request }, use, testInfo) => {
|
||||
await use()
|
||||
|
||||
// After the test, we can check whether the test passed or failed.
|
||||
if (testInfo.status !== testInfo.expectedStatus) {
|
||||
const screenshot = await page.screenshot()
|
||||
await testInfo.attach('screenshot', {
|
||||
body: screenshot,
|
||||
contentType: 'image/png',
|
||||
})
|
||||
}
|
||||
},
|
||||
{ auto: true },
|
||||
],
|
||||
})
|
||||
|
||||
test.setTimeout(TIMEOUT)
|
||||
|
||||
34
electron/tests/pages/commonActions.ts
Normal file
34
electron/tests/pages/commonActions.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Page, TestInfo } from '@playwright/test'
|
||||
import { page } from '../config/fixtures'
|
||||
|
||||
export class CommonActions {
|
||||
private testData = new Map<string, string>()
|
||||
|
||||
constructor(
|
||||
public page: Page,
|
||||
public testInfo: TestInfo
|
||||
) {}
|
||||
|
||||
async takeScreenshot(name: string) {
|
||||
const screenshot = await page.screenshot({
|
||||
fullPage: true,
|
||||
})
|
||||
const attachmentName = `${this.testInfo.title}_${name || new Date().toISOString().slice(5, 19).replace(/[-:]/g, '').replace('T', '_')}`
|
||||
await this.testInfo.attach(attachmentName.replace(/\s+/g, ''), {
|
||||
body: screenshot,
|
||||
contentType: 'image/png',
|
||||
})
|
||||
}
|
||||
|
||||
async hooks() {
|
||||
console.log('hook from the scenario page')
|
||||
}
|
||||
|
||||
setValue(key: string, value: string) {
|
||||
this.testData.set(key, value)
|
||||
}
|
||||
|
||||
getValue(key: string) {
|
||||
return this.testData.get(key)
|
||||
}
|
||||
}
|
||||
15
electron/tests/pages/hubPage.ts
Normal file
15
electron/tests/pages/hubPage.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Page } from '@playwright/test'
|
||||
import { BasePage } from './basePage'
|
||||
import { CommonActions } from './commonActions'
|
||||
|
||||
export class HubPage extends BasePage {
|
||||
readonly menuId: string = 'Hub'
|
||||
static readonly containerId: string = 'hub-container-test-id'
|
||||
|
||||
constructor(
|
||||
public page: Page,
|
||||
readonly action: CommonActions
|
||||
) {
|
||||
super(page, action, HubPage.containerId)
|
||||
}
|
||||
}
|
||||
18
electron/utils/dev.ts
Normal file
18
electron/utils/dev.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { app } from 'electron'
|
||||
|
||||
export const setupReactDevTool = async () => {
|
||||
if (!app.isPackaged) {
|
||||
// Which means you're running from source code
|
||||
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
|
||||
'electron-devtools-installer'
|
||||
) // Don't use import on top level, since the installer package is dev-only
|
||||
try {
|
||||
const name = await installExtension(REACT_DEVELOPER_TOOLS)
|
||||
console.log(`Added Extension: ${name}`)
|
||||
} catch (err) {
|
||||
console.log('An error occurred while installing devtools:')
|
||||
console.error(err)
|
||||
// Only log the error and don't throw it because it's not critical
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
export function dispose(requiredModules: Record<string, any>) {
|
||||
for (const key in requiredModules) {
|
||||
const module = requiredModules[key];
|
||||
if (typeof module["dispose"] === "function") {
|
||||
module["dispose"]();
|
||||
const module = requiredModules[key]
|
||||
if (typeof module['dispose'] === 'function') {
|
||||
module['dispose']()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
67
electron/utils/log.ts
Normal file
67
electron/utils/log.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { getJanDataFolderPath } from '@janhq/core/node'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
export function cleanLogs(
|
||||
maxFileSizeBytes?: number | undefined,
|
||||
daysToKeep?: number | undefined,
|
||||
delayMs?: number | undefined
|
||||
): void {
|
||||
const size = maxFileSizeBytes ?? 1 * 1024 * 1024 // 1 MB
|
||||
const days = daysToKeep ?? 7 // 7 days
|
||||
const delays = delayMs ?? 10000 // 10 seconds
|
||||
const logDirectory = path.join(getJanDataFolderPath(), 'logs')
|
||||
|
||||
// Perform log cleaning
|
||||
const currentDate = new Date()
|
||||
fs.readdir(logDirectory, (err, files) => {
|
||||
if (err) {
|
||||
console.error('Error reading log directory:', err)
|
||||
return
|
||||
}
|
||||
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(logDirectory, file)
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if (err) {
|
||||
console.error('Error getting file stats:', err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check size
|
||||
if (stats.size > size) {
|
||||
fs.unlink(filePath, (err) => {
|
||||
if (err) {
|
||||
console.error('Error deleting log file:', err)
|
||||
return
|
||||
}
|
||||
console.log(
|
||||
`Deleted log file due to exceeding size limit: ${filePath}`
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// Check age
|
||||
const creationDate = new Date(stats.ctime)
|
||||
const daysDifference = Math.floor(
|
||||
(currentDate.getTime() - creationDate.getTime()) /
|
||||
(1000 * 3600 * 24)
|
||||
)
|
||||
if (daysDifference > days) {
|
||||
fs.unlink(filePath, (err) => {
|
||||
if (err) {
|
||||
console.error('Error deleting log file:', err)
|
||||
return
|
||||
}
|
||||
console.log(`Deleted old log file: ${filePath}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Schedule the next execution with doubled delays
|
||||
setTimeout(() => {
|
||||
cleanLogs(maxFileSizeBytes, daysToKeep, delays * 2)
|
||||
}, delays)
|
||||
}
|
||||
@ -1,8 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import { app, Menu, dialog, shell } from 'electron'
|
||||
import { app, Menu, shell } from 'electron'
|
||||
const isMac = process.platform === 'darwin'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import { compareSemanticVersions } from './versionDiff'
|
||||
|
||||
const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
|
||||
{
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { join } from 'path'
|
||||
import { app } from 'electron'
|
||||
import { mkdir } from 'fs-extra'
|
||||
import { existsSync } from 'fs'
|
||||
import { getJanDataFolderPath } from '@janhq/core/node'
|
||||
@ -16,13 +14,3 @@ export async function createUserSpace(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getResourcePath() {
|
||||
let appPath = join(app.getAppPath(), '..', 'app.asar.unpacked')
|
||||
|
||||
if (!app.isPackaged) {
|
||||
// for development mode
|
||||
appPath = join(__dirname, '..', '..')
|
||||
}
|
||||
return appPath
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { app } from 'electron'
|
||||
|
||||
export const setupCore = async () => {
|
||||
// Setup core api for main process
|
||||
global.core = {
|
||||
// Define appPath function for app to retrieve app path globaly
|
||||
appPath: () => app.getPath('userData')
|
||||
}
|
||||
}
|
||||
// Setup core api for main process
|
||||
global.core = {
|
||||
// Define appPath function for app to retrieve app path globaly
|
||||
appPath: () => app.getPath('userData'),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
export const compareSemanticVersions = (a: string, b: string) => {
|
||||
|
||||
// 1. Split the strings into their parts.
|
||||
const a1 = a.split('.');
|
||||
const b1 = b.split('.');
|
||||
// 2. Contingency in case there's a 4th or 5th version
|
||||
const len = Math.min(a1.length, b1.length);
|
||||
// 3. Look through each version number and compare.
|
||||
for (let i = 0; i < len; i++) {
|
||||
const a2 = +a1[ i ] || 0;
|
||||
const b2 = +b1[ i ] || 0;
|
||||
|
||||
if (a2 !== b2) {
|
||||
return a2 > b2 ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. We hit this if the all checked versions so far are equal
|
||||
//
|
||||
return b1.length - a1.length;
|
||||
};
|
||||
@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "@janhq/assistant-extension",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "This extension enables assistants, including Jan, a default assistant that can call all downloaded models",
|
||||
"main": "dist/index.js",
|
||||
"node": "dist/node/index.js",
|
||||
"author": "Jan <service@jan.ai>",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"build": "tsc --module commonjs && rollup -c rollup.config.ts",
|
||||
"clean:modules": "rimraf node_modules/pdf-parse/test && cd node_modules/pdf-parse/lib/pdf.js && rimraf v1.9.426 v1.10.88 v2.0.550",
|
||||
"build": "yarn clean:modules && tsc --module commonjs && rollup -c rollup.config.ts",
|
||||
"build:publish:linux": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install",
|
||||
"build:publish:darwin": "rimraf *.tgz --glob && npm run build && ../../.github/scripts/auto-sign.sh && npm pack && cpx *.tgz ../../pre-install",
|
||||
"build:publish:win32": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install",
|
||||
@ -25,7 +26,7 @@
|
||||
"rollup-plugin-define": "^1.0.1",
|
||||
"rollup-plugin-sourcemaps": "^0.6.3",
|
||||
"rollup-plugin-typescript2": "^0.36.0",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript": "^5.2.2",
|
||||
"run-script-os": "^1.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -44,9 +45,6 @@
|
||||
],
|
||||
"bundleDependencies": [
|
||||
"@janhq/core",
|
||||
"@langchain/community",
|
||||
"hnswlib-node",
|
||||
"langchain",
|
||||
"pdf-parse"
|
||||
"hnswlib-node"
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
import resolve from "@rollup/plugin-node-resolve";
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import sourceMaps from "rollup-plugin-sourcemaps";
|
||||
import typescript from "rollup-plugin-typescript2";
|
||||
import json from "@rollup/plugin-json";
|
||||
import replace from "@rollup/plugin-replace";
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import sourceMaps from 'rollup-plugin-sourcemaps'
|
||||
import typescript from 'rollup-plugin-typescript2'
|
||||
import json from '@rollup/plugin-json'
|
||||
import replace from '@rollup/plugin-replace'
|
||||
|
||||
const packageJson = require("./package.json");
|
||||
const packageJson = require('./package.json')
|
||||
|
||||
const pkg = require("./package.json");
|
||||
const pkg = require('./package.json')
|
||||
|
||||
export default [
|
||||
{
|
||||
input: `src/index.ts`,
|
||||
output: [{ file: pkg.main, format: "es", sourcemap: true }],
|
||||
output: [{ file: pkg.main, format: 'es', sourcemap: true }],
|
||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||
external: [],
|
||||
watch: {
|
||||
include: "src/**",
|
||||
include: 'src/**',
|
||||
},
|
||||
plugins: [
|
||||
replace({
|
||||
@ -35,7 +35,7 @@ export default [
|
||||
// which external modules to include in the bundle
|
||||
// https://github.com/rollup/rollup-plugin-node-resolve#usage
|
||||
resolve({
|
||||
extensions: [".js", ".ts", ".svelte"],
|
||||
extensions: ['.js', '.ts', '.svelte'],
|
||||
}),
|
||||
|
||||
// Resolve source maps to the original source
|
||||
@ -44,18 +44,11 @@ export default [
|
||||
},
|
||||
{
|
||||
input: `src/node/index.ts`,
|
||||
output: [{ dir: "dist/node", format: "cjs", sourcemap: false }],
|
||||
output: [{ dir: 'dist/node', format: 'cjs', sourcemap: false }],
|
||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||
external: [
|
||||
"@janhq/core/node",
|
||||
"@langchain/community",
|
||||
"langchain",
|
||||
"langsmith",
|
||||
"path",
|
||||
"hnswlib-node",
|
||||
],
|
||||
external: ['@janhq/core/node', 'path', 'hnswlib-node'],
|
||||
watch: {
|
||||
include: "src/node/**",
|
||||
include: 'src/node/**',
|
||||
},
|
||||
// inlineDynamicImports: true,
|
||||
plugins: [
|
||||
@ -71,11 +64,11 @@ export default [
|
||||
// which external modules to include in the bundle
|
||||
// https://github.com/rollup/rollup-plugin-node-resolve#usage
|
||||
resolve({
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
extensions: ['.ts', '.js', '.json'],
|
||||
}),
|
||||
|
||||
// Resolve source maps to the original source
|
||||
// sourceMaps(),
|
||||
],
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
declare const NODE: string;
|
||||
declare const EXTENSION_NAME: string;
|
||||
declare const VERSION: string;
|
||||
declare const NODE: string
|
||||
declare const EXTENSION_NAME: string
|
||||
declare const VERSION: string
|
||||
|
||||
@ -10,145 +10,168 @@ import {
|
||||
executeOnMain,
|
||||
AssistantExtension,
|
||||
AssistantEvent,
|
||||
} from "@janhq/core";
|
||||
} from '@janhq/core'
|
||||
|
||||
export default class JanAssistantExtension extends AssistantExtension {
|
||||
private static readonly _homeDir = "file://assistants";
|
||||
private static readonly _homeDir = 'file://assistants'
|
||||
private static readonly _threadDir = 'file://threads'
|
||||
|
||||
controller = new AbortController();
|
||||
isCancelled = false;
|
||||
retrievalThreadId: string | undefined = undefined;
|
||||
controller = new AbortController()
|
||||
isCancelled = false
|
||||
retrievalThreadId: string | undefined = undefined
|
||||
|
||||
async onLoad() {
|
||||
// making the assistant directory
|
||||
const assistantDirExist = await fs.existsSync(
|
||||
JanAssistantExtension._homeDir
|
||||
);
|
||||
)
|
||||
if (
|
||||
localStorage.getItem(`${EXTENSION_NAME}-version`) !== VERSION ||
|
||||
!assistantDirExist
|
||||
) {
|
||||
if (!assistantDirExist)
|
||||
await fs.mkdirSync(JanAssistantExtension._homeDir);
|
||||
if (!assistantDirExist) await fs.mkdirSync(JanAssistantExtension._homeDir)
|
||||
|
||||
// Write assistant metadata
|
||||
await this.createJanAssistant();
|
||||
await this.createJanAssistant()
|
||||
// Finished migration
|
||||
localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION);
|
||||
localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION)
|
||||
// Update the assistant list
|
||||
events.emit(AssistantEvent.OnAssistantsUpdate, {});
|
||||
events.emit(AssistantEvent.OnAssistantsUpdate, {})
|
||||
}
|
||||
|
||||
// Events subscription
|
||||
events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
|
||||
JanAssistantExtension.handleMessageRequest(data, this)
|
||||
);
|
||||
)
|
||||
|
||||
events.on(InferenceEvent.OnInferenceStopped, () => {
|
||||
JanAssistantExtension.handleInferenceStopped(this);
|
||||
});
|
||||
JanAssistantExtension.handleInferenceStopped(this)
|
||||
})
|
||||
}
|
||||
|
||||
private static async handleInferenceStopped(instance: JanAssistantExtension) {
|
||||
instance.isCancelled = true;
|
||||
instance.controller?.abort();
|
||||
instance.isCancelled = true
|
||||
instance.controller?.abort()
|
||||
}
|
||||
|
||||
private static async handleMessageRequest(
|
||||
data: MessageRequest,
|
||||
instance: JanAssistantExtension
|
||||
) {
|
||||
instance.isCancelled = false;
|
||||
instance.controller = new AbortController();
|
||||
instance.isCancelled = false
|
||||
instance.controller = new AbortController()
|
||||
|
||||
if (
|
||||
data.model?.engine !== InferenceEngine.tool_retrieval_enabled ||
|
||||
!data.messages ||
|
||||
// TODO: Since the engine is defined, its unsafe to assume that assistant tools are defined
|
||||
// That could lead to an issue where thread stuck at generating response
|
||||
!data.thread?.assistants[0]?.tools
|
||||
) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const latestMessage = data.messages[data.messages.length - 1];
|
||||
const latestMessage = data.messages[data.messages.length - 1]
|
||||
|
||||
// Ingest the document if needed
|
||||
// 1. Ingest the document if needed
|
||||
if (
|
||||
latestMessage &&
|
||||
latestMessage.content &&
|
||||
typeof latestMessage.content !== "string"
|
||||
typeof latestMessage.content !== 'string' &&
|
||||
latestMessage.content.length > 1
|
||||
) {
|
||||
const docFile = latestMessage.content[1]?.doc_url?.url;
|
||||
const docFile = latestMessage.content[1]?.doc_url?.url
|
||||
if (docFile) {
|
||||
await executeOnMain(
|
||||
NODE,
|
||||
"toolRetrievalIngestNewDocument",
|
||||
'toolRetrievalIngestNewDocument',
|
||||
docFile,
|
||||
data.model?.proxyEngine
|
||||
);
|
||||
)
|
||||
}
|
||||
} else if (
|
||||
// Check whether we need to ingest document or not
|
||||
// Otherwise wrong context will be sent
|
||||
!(await fs.existsSync(
|
||||
await joinPath([
|
||||
JanAssistantExtension._threadDir,
|
||||
data.threadId,
|
||||
'memory',
|
||||
])
|
||||
))
|
||||
) {
|
||||
// No document ingested, reroute the result to inference engine
|
||||
const output = {
|
||||
...data,
|
||||
model: {
|
||||
...data.model,
|
||||
engine: data.model.proxyEngine,
|
||||
},
|
||||
}
|
||||
events.emit(MessageEvent.OnMessageSent, output)
|
||||
return
|
||||
}
|
||||
|
||||
// Load agent on thread changed
|
||||
// 2. Load agent on thread changed
|
||||
if (instance.retrievalThreadId !== data.threadId) {
|
||||
await executeOnMain(NODE, "toolRetrievalLoadThreadMemory", data.threadId);
|
||||
await executeOnMain(NODE, 'toolRetrievalLoadThreadMemory', data.threadId)
|
||||
|
||||
instance.retrievalThreadId = data.threadId;
|
||||
instance.retrievalThreadId = data.threadId
|
||||
|
||||
// Update the text splitter
|
||||
await executeOnMain(
|
||||
NODE,
|
||||
"toolRetrievalUpdateTextSplitter",
|
||||
'toolRetrievalUpdateTextSplitter',
|
||||
data.thread.assistants[0].tools[0]?.settings?.chunk_size ?? 4000,
|
||||
data.thread.assistants[0].tools[0]?.settings?.chunk_overlap ?? 200
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Using the retrieval template with the result and query
|
||||
if (latestMessage.content) {
|
||||
const prompt =
|
||||
typeof latestMessage.content === "string"
|
||||
typeof latestMessage.content === 'string'
|
||||
? latestMessage.content
|
||||
: latestMessage.content[0].text;
|
||||
: latestMessage.content[0].text
|
||||
// Retrieve the result
|
||||
console.debug("toolRetrievalQuery", latestMessage.content);
|
||||
const retrievalResult = await executeOnMain(
|
||||
NODE,
|
||||
"toolRetrievalQueryResult",
|
||||
'toolRetrievalQueryResult',
|
||||
prompt
|
||||
);
|
||||
)
|
||||
console.debug('toolRetrievalQueryResult', retrievalResult)
|
||||
|
||||
// Update the message content
|
||||
// Using the retrieval template with the result and query
|
||||
if (data.thread?.assistants[0].tools)
|
||||
// Update message content
|
||||
if (data.thread?.assistants[0]?.tools && retrievalResult)
|
||||
data.messages[data.messages.length - 1].content =
|
||||
data.thread.assistants[0].tools[0].settings?.retrieval_template
|
||||
?.replace("{CONTEXT}", retrievalResult)
|
||||
.replace("{QUESTION}", prompt);
|
||||
?.replace('{CONTEXT}', retrievalResult)
|
||||
.replace('{QUESTION}', prompt)
|
||||
}
|
||||
|
||||
// Filter out all the messages that are not text
|
||||
data.messages = data.messages.map((message) => {
|
||||
if (
|
||||
message.content &&
|
||||
typeof message.content !== "string" &&
|
||||
typeof message.content !== 'string' &&
|
||||
(message.content.length ?? 0) > 0
|
||||
) {
|
||||
return {
|
||||
...message,
|
||||
content: [message.content[0]],
|
||||
};
|
||||
}
|
||||
}
|
||||
return message;
|
||||
});
|
||||
return message
|
||||
})
|
||||
|
||||
// Reroute the result to inference engine
|
||||
// 4. Reroute the result to inference engine
|
||||
const output = {
|
||||
...data,
|
||||
model: {
|
||||
...data.model,
|
||||
engine: data.model.proxyEngine,
|
||||
},
|
||||
};
|
||||
events.emit(MessageEvent.OnMessageSent, output);
|
||||
}
|
||||
events.emit(MessageEvent.OnMessageSent, output)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -160,107 +183,107 @@ export default class JanAssistantExtension extends AssistantExtension {
|
||||
const assistantDir = await joinPath([
|
||||
JanAssistantExtension._homeDir,
|
||||
assistant.id,
|
||||
]);
|
||||
if (!(await fs.existsSync(assistantDir))) await fs.mkdirSync(assistantDir);
|
||||
])
|
||||
if (!(await fs.existsSync(assistantDir))) await fs.mkdirSync(assistantDir)
|
||||
|
||||
// store the assistant metadata json
|
||||
const assistantMetadataPath = await joinPath([
|
||||
assistantDir,
|
||||
"assistant.json",
|
||||
]);
|
||||
'assistant.json',
|
||||
])
|
||||
try {
|
||||
await fs.writeFileSync(
|
||||
assistantMetadataPath,
|
||||
JSON.stringify(assistant, null, 2)
|
||||
);
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async getAssistants(): Promise<Assistant[]> {
|
||||
// get all the assistant directories
|
||||
// get all the assistant metadata json
|
||||
const results: Assistant[] = [];
|
||||
const results: Assistant[] = []
|
||||
const allFileName: string[] = await fs.readdirSync(
|
||||
JanAssistantExtension._homeDir
|
||||
);
|
||||
)
|
||||
for (const fileName of allFileName) {
|
||||
const filePath = await joinPath([
|
||||
JanAssistantExtension._homeDir,
|
||||
fileName,
|
||||
]);
|
||||
])
|
||||
|
||||
if (filePath.includes(".DS_Store")) continue;
|
||||
if (filePath.includes('.DS_Store')) continue
|
||||
const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter(
|
||||
(file: string) => file === "assistant.json"
|
||||
);
|
||||
(file: string) => file === 'assistant.json'
|
||||
)
|
||||
|
||||
if (jsonFiles.length !== 1) {
|
||||
// has more than one assistant file -> ignore
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
const content = await fs.readFileSync(
|
||||
await joinPath([filePath, jsonFiles[0]]),
|
||||
"utf-8"
|
||||
);
|
||||
'utf-8'
|
||||
)
|
||||
const assistant: Assistant =
|
||||
typeof content === "object" ? content : JSON.parse(content);
|
||||
typeof content === 'object' ? content : JSON.parse(content)
|
||||
|
||||
results.push(assistant);
|
||||
results.push(assistant)
|
||||
}
|
||||
|
||||
return results;
|
||||
return results
|
||||
}
|
||||
|
||||
async deleteAssistant(assistant: Assistant): Promise<void> {
|
||||
if (assistant.id === "jan") {
|
||||
return Promise.reject("Cannot delete Jan Assistant");
|
||||
if (assistant.id === 'jan') {
|
||||
return Promise.reject('Cannot delete Jan Assistant')
|
||||
}
|
||||
|
||||
// remove the directory
|
||||
const assistantDir = await joinPath([
|
||||
JanAssistantExtension._homeDir,
|
||||
assistant.id,
|
||||
]);
|
||||
await fs.rmdirSync(assistantDir);
|
||||
return Promise.resolve();
|
||||
])
|
||||
await fs.rmdirSync(assistantDir)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
private async createJanAssistant(): Promise<void> {
|
||||
const janAssistant: Assistant = {
|
||||
avatar: "",
|
||||
avatar: '',
|
||||
thread_location: undefined,
|
||||
id: "jan",
|
||||
object: "assistant",
|
||||
id: 'jan',
|
||||
object: 'assistant',
|
||||
created_at: Date.now(),
|
||||
name: "Jan",
|
||||
description: "A default assistant that can use all downloaded models",
|
||||
model: "*",
|
||||
instructions: "",
|
||||
name: 'Jan',
|
||||
description: 'A default assistant that can use all downloaded models',
|
||||
model: '*',
|
||||
instructions: '',
|
||||
tools: [
|
||||
{
|
||||
type: "retrieval",
|
||||
type: 'retrieval',
|
||||
enabled: false,
|
||||
settings: {
|
||||
top_k: 2,
|
||||
chunk_size: 1024,
|
||||
chunk_overlap: 64,
|
||||
retrieval_template: `Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.
|
||||
----------------
|
||||
CONTEXT: {CONTEXT}
|
||||
----------------
|
||||
QUESTION: {QUESTION}
|
||||
----------------
|
||||
Helpful Answer:`,
|
||||
----------------
|
||||
CONTEXT: {CONTEXT}
|
||||
----------------
|
||||
QUESTION: {QUESTION}
|
||||
----------------
|
||||
Helpful Answer:`,
|
||||
},
|
||||
},
|
||||
],
|
||||
file_ids: [],
|
||||
metadata: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
await this.createAssistant(janAssistant);
|
||||
await this.createAssistant(janAssistant)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { getJanDataFolderPath } from "@janhq/core/node";
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getJanDataFolderPath } from '@janhq/core/node'
|
||||
|
||||
// Sec: Do not send engine settings over requests
|
||||
// Read it manually instead
|
||||
export const readEmbeddingEngine = (engineName: string) => {
|
||||
const engineSettings = fs.readFileSync(
|
||||
path.join(getJanDataFolderPath(), "engines", `${engineName}.json`),
|
||||
"utf-8",
|
||||
);
|
||||
return JSON.parse(engineSettings);
|
||||
};
|
||||
path.join(getJanDataFolderPath(), 'engines', `${engineName}.json`),
|
||||
'utf-8'
|
||||
)
|
||||
return JSON.parse(engineSettings)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user