Merge branch 'main' into internal-48
This commit is contained in:
commit
2555dae49f
2
.github/workflows/jan-electron-build.yml
vendored
2
.github/workflows/jan-electron-build.yml
vendored
@ -122,7 +122,7 @@ jobs:
|
||||
yarn build:core
|
||||
yarn install
|
||||
$env:NITRO_VERSION = Get-Content .\plugins\inference-plugin\nitro\version.txt; echo $env:NITRO_VERSION
|
||||
yarn build:plugins-windows
|
||||
yarn build:plugins-win32
|
||||
|
||||
- name: Build and publish app
|
||||
run: |
|
||||
|
||||
@ -95,7 +95,7 @@ jobs:
|
||||
yarn build:core
|
||||
yarn install
|
||||
$env:NITRO_VERSION = Get-Content .\plugins\inference-plugin\nitro\version.txt; echo $env:NITRO_VERSION
|
||||
yarn build:plugins-windows
|
||||
yarn build:plugins-win32
|
||||
yarn build:test-win32
|
||||
$env:CI="e2e"
|
||||
yarn test
|
||||
|
||||
74
.github/workflows/jan-plugin-core.yml
vendored
74
.github/workflows/jan-plugin-core.yml
vendored
@ -1,74 +0,0 @@
|
||||
name: Plugin Core
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "core/**"
|
||||
- "!core/package.json"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "core/**"
|
||||
- ".github/workflows/jan-plugin-core.yml"
|
||||
- "!core/package.json"
|
||||
jobs:
|
||||
build-and-publish-plugins:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: "0"
|
||||
token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
|
||||
|
||||
- name: Install jq
|
||||
uses: dcarbone/install-jq-action@v2.0.1
|
||||
|
||||
- name: "Auto Increase package Version"
|
||||
run: |
|
||||
# Extract current version
|
||||
current_version=$(jq -r '.version' core/package.json)
|
||||
|
||||
# Break the version into its components
|
||||
major_version=$(echo $current_version | cut -d "." -f 1)
|
||||
minor_version=$(echo $current_version | cut -d "." -f 2)
|
||||
patch_version=$(echo $current_version | cut -d "." -f 3)
|
||||
|
||||
# Increment the patch version by one
|
||||
new_patch_version=$((patch_version+1))
|
||||
|
||||
# Construct the new version
|
||||
new_version="$major_version.$minor_version.$new_patch_version"
|
||||
|
||||
# Replace the old version with the new version in package.json
|
||||
jq --arg version "$new_version" '.version = $version' core/package.json > /tmp/package.json && mv /tmp/package.json core/package.json
|
||||
|
||||
# Print the new version
|
||||
echo "Updated package.json version to: $new_version"
|
||||
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: npm install && npm run build
|
||||
working-directory: ./core
|
||||
- run: npm publish --access public
|
||||
if: github.event_name == 'push' && github.event.pull_request.head.repo.full_name != github.repository
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
working-directory: ./core
|
||||
|
||||
- name: "Commit new version to main and create tag"
|
||||
if: github.event_name == 'push' && github.event.pull_request.head.repo.full_name != github.repository
|
||||
run: |
|
||||
version=$(jq -r '.version' core/package.json)
|
||||
git config --global user.email "service@jan.ai"
|
||||
git config --global user.name "Service Account"
|
||||
git add core/package.json
|
||||
git commit -m "${GITHUB_REPOSITORY}: Update tag build $version"
|
||||
git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.PAT_SERVICE_ACCOUNT }}" push origin HEAD:main
|
||||
git tag -a core-$version -m "${GITHUB_REPOSITORY}: Update tag build $version for core"
|
||||
git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.PAT_SERVICE_ACCOUNT }}" push origin core-$version
|
||||
163
.github/workflows/jan-plugins.yml
vendored
163
.github/workflows/jan-plugins.yml
vendored
@ -1,163 +0,0 @@
|
||||
name: Jan Default Plugins
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "plugins/**"
|
||||
- "!plugins/*/package.json"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "plugins/**"
|
||||
- ".github/workflows/jan-plugins.yml"
|
||||
- "!plugins/*/package.json"
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
environment: production
|
||||
outputs:
|
||||
branch_name: ${{ steps.commit_and_tag.outputs.branch_name }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: "0"
|
||||
token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
|
||||
|
||||
- name: Install jq
|
||||
uses: dcarbone/install-jq-action@v2.0.1
|
||||
|
||||
- name: Get Cer for code signing
|
||||
run: base64 -d <<< "$CODE_SIGN_P12_BASE64" > /tmp/codesign.p12
|
||||
shell: bash
|
||||
env:
|
||||
CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
|
||||
|
||||
- uses: apple-actions/import-codesign-certs@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
|
||||
p12-password: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
|
||||
|
||||
- name: Check Path Change
|
||||
run: |
|
||||
git config --global user.email "service@jan.ai"
|
||||
git config --global user.name "Service Account"
|
||||
echo "Changes in these directories trigger the build:"
|
||||
changed_dirs=$(git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.GITHUB_TOKEN }}" diff --name-only HEAD HEAD~1 | grep '^plugins/' | awk -F/ '{print $2}' | uniq)
|
||||
echo $changed_dirs > /tmp/change_dir.txt
|
||||
|
||||
- name: "Auto Increase package Version"
|
||||
run: |
|
||||
cd plugins
|
||||
for dir in $(cat /tmp/change_dir.txt)
|
||||
do
|
||||
echo "$dir"
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "Directory $dir does not exist, plugin might be removed, skipping..."
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract current version
|
||||
current_version=$(jq -r '.version' $dir/package.json)
|
||||
|
||||
# Break the version into its components
|
||||
major_version=$(echo $current_version | cut -d "." -f 1)
|
||||
minor_version=$(echo $current_version | cut -d "." -f 2)
|
||||
patch_version=$(echo $current_version | cut -d "." -f 3)
|
||||
|
||||
# Increment the patch version by one
|
||||
new_patch_version=$((patch_version+1))
|
||||
|
||||
# Construct the new version
|
||||
new_version="$major_version.$minor_version.$new_patch_version"
|
||||
|
||||
# Replace the old version with the new version in package.json
|
||||
jq --arg version "$new_version" '.version = $version' $dir/package.json > /tmp/package.json && mv /tmp/package.json $dir/package.json
|
||||
|
||||
# Print the new version
|
||||
echo "Updated $dir package.json version to: $new_version"
|
||||
done
|
||||
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Build core module
|
||||
run: |
|
||||
cd core
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Publish npm packages
|
||||
run: |
|
||||
cd plugins
|
||||
for dir in $(cat /tmp/change_dir.txt)
|
||||
do
|
||||
echo $dir
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "Directory $dir does not exist, plugin might be removed, skipping..."
|
||||
continue
|
||||
fi
|
||||
cd $dir
|
||||
npm install
|
||||
if [[ $dir == 'data-plugin' ]]; then
|
||||
npm run build:deps
|
||||
fi
|
||||
npm run postinstall && ../../.github/scripts/auto-sign.sh
|
||||
if [[ $GITHUB_EVENT_NAME == 'push' && $GITHUB_EVENT_PULL_REQUEST_HEAD_REPO_FULL_NAME != $GITHUB_REPOSITORY ]]; then
|
||||
npm publish --access public
|
||||
fi
|
||||
cd ..
|
||||
done
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
|
||||
APP_PATH: "."
|
||||
|
||||
- name: "Commit new version to main and create tag"
|
||||
id: commit_and_tag
|
||||
if: github.event_name == 'push' && github.event.pull_request.head.repo.full_name != github.repository
|
||||
run: |
|
||||
rm -rf /tmp/plugin-catalog
|
||||
git clone https://${{ secrets.SERVICE_ACCOUNT_USERNAME }}:${{ secrets.PAT_SERVICE_ACCOUNT }}@github.com/janhq/plugin-catalog.git /tmp/plugin-catalog
|
||||
for dir in $(cat /tmp/change_dir.txt)
|
||||
do
|
||||
echo "$dir"
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "Directory $dir does not exist, plugin might be removed, skipping..."
|
||||
continue
|
||||
fi
|
||||
version=$(jq -r '.version' plugins/$dir/package.json)
|
||||
git config --global user.email "service@jan.ai"
|
||||
git config --global user.name "Service Account"
|
||||
git add plugins/$dir/package.json
|
||||
git commit -m "${GITHUB_REPOSITORY}: Update tag build $version for $dir"
|
||||
git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.PAT_SERVICE_ACCOUNT }}" push origin HEAD:main
|
||||
git tag -a $dir-$version -m "${GITHUB_REPOSITORY}: Update tag build $version for $dir"
|
||||
git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.PAT_SERVICE_ACCOUNT }}" push origin $dir-$version
|
||||
plugin_name=$(jq -r '.name | sub("@janhq/"; "")' plugins/$dir/package.json)
|
||||
cp plugins/$dir/package.json /tmp/plugin-catalog/${plugin_name}.json
|
||||
done
|
||||
cd /tmp/plugin-catalog
|
||||
BRANCH_NAME="update-package-$(date +'%Y%m%d%H%M%S')"
|
||||
git checkout -b $BRANCH_NAME
|
||||
git add .
|
||||
git commit -m "Update plugin catalog"
|
||||
git push origin $BRANCH_NAME
|
||||
cd /tmp && rm -rf /tmp/plugin-catalog
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||
echo "::set-output name=branch_name::$BRANCH_NAME"
|
||||
|
||||
pull_request:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
if: github.event_name == 'push' && github.event.pull_request.head.repo.full_name != github.repository
|
||||
needs: build
|
||||
steps:
|
||||
- run: |
|
||||
gh pr create --title "Update plugin catalog" --body "Update plugin catalog" --base main --head ${{ needs.build.outputs.branch_name }} --repo janhq/plugin-catalog --reviewer louis-jan,hiento09
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }}
|
||||
31
README.md
31
README.md
@ -55,12 +55,33 @@ As Jan is development mode, you might get stuck on a broken build.
|
||||
|
||||
To reset your installation:
|
||||
|
||||
1. Delete Jan Application from /Applications
|
||||
1. Delete Jan from your `/Applications` folder
|
||||
|
||||
1. Clear cache:
|
||||
`rm -rf /Users/$(whoami)/Library/Application\ Support/jan-electron`
|
||||
OR
|
||||
`rm -rf /Users/$(whoami)/Library/Application\ Support/jan`
|
||||
1. Delete Application data:
|
||||
```sh
|
||||
# Newer versions
|
||||
rm -rf /Users/$(whoami)/Library/Application\ Support/jan
|
||||
|
||||
# Versions 0.2.0 and older
|
||||
rm -rf /Users/$(whoami)/Library/Application\ Support/jan-electron
|
||||
```
|
||||
|
||||
1. Clear Application cache:
|
||||
```sh
|
||||
rm -rf /Users/$(whoami)/Library/Caches/jan*
|
||||
```
|
||||
|
||||
1. Use the following commands to remove any dangling backend processes:
|
||||
|
||||
```sh
|
||||
ps aux | grep nitro
|
||||
```
|
||||
|
||||
Look for processes like "nitro" and "nitro_arm_64," and kill them one by one with:
|
||||
|
||||
```sh
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ export { core, deleteFile, invokePluginFunc } from "./core";
|
||||
* Core module exports.
|
||||
* @module
|
||||
*/
|
||||
export { downloadFile, executeOnMain } from "./core";
|
||||
export { downloadFile, executeOnMain, appDataPath } from "./core";
|
||||
|
||||
/**
|
||||
* Events module exports.
|
||||
|
||||
@ -7,21 +7,28 @@ sidebar_position: 5
|
||||
Please note that 👋Jan is in "development mode," and you might encounter issues. If you need to reset your installation, follow these steps:
|
||||
|
||||
## Issue 1: Broken Build
|
||||
1. Delete the Jan Application from your computer.
|
||||
|
||||
2. Clear the cache by running one of the following commands:
|
||||
As Jan is development mode, you might get stuck on a broken build.
|
||||
|
||||
To reset your installation:
|
||||
|
||||
1. Delete Jan from your `/Applications` folder
|
||||
|
||||
1. Delete Application data:
|
||||
```sh
|
||||
# Newer versions
|
||||
rm -rf /Users/$(whoami)/Library/Application\ Support/jan
|
||||
|
||||
# Versions 0.2.0 and older
|
||||
rm -rf /Users/$(whoami)/Library/Application\ Support/jan-electron
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
1. Clear Application cache:
|
||||
```sh
|
||||
rm -rf /Users/$(whoami)/Library/Application\ Support/jan
|
||||
rm -rf /Users/$(whoami)/Library/Caches/jan*
|
||||
```
|
||||
|
||||
3. If the above steps fail, use the following commands to find and kill any problematic processes:
|
||||
1. Use the following commands to remove any dangling backend processes:
|
||||
|
||||
```sh
|
||||
ps aux | grep nitro
|
||||
@ -32,3 +39,4 @@ Please note that 👋Jan is in "development mode," and you might encounter issue
|
||||
```sh
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
|
||||
@ -51,8 +51,8 @@ const config = {
|
||||
[
|
||||
"posthog-docusaurus",
|
||||
{
|
||||
apiKey: process.env.POSTHOG_PROJECT_API_KEY,
|
||||
appUrl: process.env.POSTHOG_APP_URL, // optional
|
||||
apiKey: process.env.POSTHOG_PROJECT_API_KEY || "XXX",
|
||||
appUrl: process.env.POSTHOG_APP_URL || "XXX", // optional
|
||||
enableInDevelopment: false, // optional
|
||||
},
|
||||
],
|
||||
@ -93,7 +93,7 @@ const config = {
|
||||
},
|
||||
// GTM is always inactive in development and only active in production to avoid polluting the analytics statistics.
|
||||
googleTagManager: {
|
||||
containerId: process.env.GTM_ID,
|
||||
containerId: process.env.GTM_ID || "XXX",
|
||||
},
|
||||
// Will be passed to @docusaurus/plugin-content-pages (false to disable)
|
||||
// pages: {},
|
||||
@ -162,7 +162,7 @@ const config = {
|
||||
],
|
||||
},
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
theme: darkCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
additionalLanguages: ["python"],
|
||||
},
|
||||
|
||||
@ -53,20 +53,27 @@ export default function Dropdown() {
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
const changeDefaultSystem = (systems) => {
|
||||
const changeDefaultSystem = async (systems) => {
|
||||
const userAgent = navigator.userAgent;
|
||||
|
||||
const arc = await navigator?.userAgentData?.getHighEntropyValues([
|
||||
"architecture",
|
||||
]);
|
||||
|
||||
if (userAgent.includes("Windows")) {
|
||||
// windows user
|
||||
setDefaultSystem(systems[2]);
|
||||
} else if (userAgent.includes("Linux")) {
|
||||
// linux user
|
||||
setDefaultSystem(systems[3]);
|
||||
} else if (userAgent.includes("Mac OS") && userAgent.includes("Intel")) {
|
||||
// mac intel user
|
||||
setDefaultSystem(systems[1]);
|
||||
} else {
|
||||
// mac user and also default
|
||||
} else if (
|
||||
userAgent.includes("Mac OS") &&
|
||||
arc &&
|
||||
arc.architecture === "arm"
|
||||
) {
|
||||
setDefaultSystem(systems[0]);
|
||||
} else {
|
||||
setDefaultSystem(systems[1]);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
|
||||
@ -2,4 +2,4 @@
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
margin-top: -20px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,10 +27,10 @@
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
body {
|
||||
@apply text-sm;
|
||||
@apply text-base;
|
||||
@apply antialiased;
|
||||
@apply bg-white dark:bg-black;
|
||||
@apply text-gray-700 dark:text-gray-400;
|
||||
@apply text-gray-800 dark:text-gray-300;
|
||||
}
|
||||
img {
|
||||
pointer-events: none;
|
||||
|
||||
@ -92,9 +92,23 @@
|
||||
}
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 16px;
|
||||
padding-left: 32px;
|
||||
li {
|
||||
@apply leading-loose;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdown h1:first-child,
|
||||
.markdown > p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.theme-code-block {
|
||||
font-size: 14px;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.table-of-contents {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
65
electron/handlers/app.ts
Normal file
65
electron/handlers/app.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { app, ipcMain, shell } from "electron";
|
||||
import { ModuleManager } from "../managers/module";
|
||||
import { join } from "path";
|
||||
import { PluginManager } from "../managers/plugin";
|
||||
import { WindowManager } from "../managers/window";
|
||||
|
||||
export function handleAppIPCs() {
|
||||
/**
|
||||
* Retrieves the path to the app data directory using the `coreAPI` object.
|
||||
* If the `coreAPI` object is not available, the function returns `undefined`.
|
||||
* @returns A Promise that resolves with the path to the app data directory, or `undefined` if the `coreAPI` object is not available.
|
||||
*/
|
||||
ipcMain.handle("appDataPath", async (_event) => {
|
||||
return app.getPath("userData");
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the version of the app.
|
||||
* @param _event - The IPC event object.
|
||||
* @returns The version of the app.
|
||||
*/
|
||||
ipcMain.handle("appVersion", async (_event) => {
|
||||
return app.getVersion();
|
||||
});
|
||||
|
||||
/**
|
||||
* 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("openAppDirectory", async (_event) => {
|
||||
shell.openPath(app.getPath("userData"));
|
||||
});
|
||||
|
||||
/**
|
||||
* Opens a URL in the user's default browser.
|
||||
* @param _event - The IPC event object.
|
||||
* @param url - The URL to open.
|
||||
*/
|
||||
ipcMain.handle("openExternalUrl", async (_event, url) => {
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
/**
|
||||
* Relaunches the app in production - reload window in development.
|
||||
* @param _event - The IPC event object.
|
||||
* @param url - The URL to reload.
|
||||
*/
|
||||
ipcMain.handle("relaunch", async (_event, url) => {
|
||||
ModuleManager.instance.clearImportedModules();
|
||||
|
||||
if (app.isPackaged) {
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
} else {
|
||||
for (const modulePath in ModuleManager.instance.requiredModules) {
|
||||
delete require.cache[
|
||||
require.resolve(join(app.getPath("userData"), "plugins", modulePath))
|
||||
];
|
||||
}
|
||||
PluginManager.instance.setupPlugins();
|
||||
WindowManager.instance.currentWindow?.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
106
electron/handlers/download.ts
Normal file
106
electron/handlers/download.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { app, ipcMain } from "electron";
|
||||
import { DownloadManager } from "../managers/download";
|
||||
import { resolve, join } from "path";
|
||||
import { WindowManager } from "../managers/window";
|
||||
import request from "request";
|
||||
import { createWriteStream, unlink } from "fs";
|
||||
const progress = require("request-progress");
|
||||
|
||||
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("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("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("abortDownload", async (_event, fileName) => {
|
||||
const rq = DownloadManager.instance.networkRequests[fileName];
|
||||
DownloadManager.instance.networkRequests[fileName] = undefined;
|
||||
const userDataPath = app.getPath("userData");
|
||||
const fullPath = join(userDataPath, fileName);
|
||||
rq?.abort();
|
||||
let result = "NULL";
|
||||
unlink(fullPath, function (err) {
|
||||
if (err && err.code == "ENOENT") {
|
||||
result = `File not exist: ${err}`;
|
||||
} else if (err) {
|
||||
result = `File delete error: ${err}`;
|
||||
} else {
|
||||
result = "File deleted successfully";
|
||||
}
|
||||
console.log(`Delete file ${fileName} from ${fullPath} result: ${result}`);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 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("downloadFile", async (_event, url, fileName) => {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const destination = resolve(userDataPath, fileName);
|
||||
const rq = request(url);
|
||||
|
||||
progress(rq, {})
|
||||
.on("progress", function (state: any) {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
"FILE_DOWNLOAD_UPDATE",
|
||||
{
|
||||
...state,
|
||||
fileName,
|
||||
}
|
||||
);
|
||||
})
|
||||
.on("error", function (err: Error) {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
"FILE_DOWNLOAD_ERROR",
|
||||
{
|
||||
fileName,
|
||||
err,
|
||||
}
|
||||
);
|
||||
})
|
||||
.on("end", function () {
|
||||
if (DownloadManager.instance.networkRequests[fileName]) {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
"FILE_DOWNLOAD_COMPLETE",
|
||||
{
|
||||
fileName,
|
||||
}
|
||||
);
|
||||
DownloadManager.instance.setRequest(fileName, undefined);
|
||||
} else {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
"FILE_DOWNLOAD_ERROR",
|
||||
{
|
||||
fileName,
|
||||
err: "Download cancelled",
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
.pipe(createWriteStream(destination));
|
||||
|
||||
DownloadManager.instance.setRequest(fileName, rq);
|
||||
});
|
||||
}
|
||||
@ -5,7 +5,7 @@ import { join } from "path";
|
||||
/**
|
||||
* Handles file system operations.
|
||||
*/
|
||||
export function handleFs() {
|
||||
export function handleFsIPCs() {
|
||||
/**
|
||||
* Reads a file from the user data directory.
|
||||
* @param event - The event object.
|
||||
@ -115,4 +115,29 @@ export function handleFs() {
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Deletes a file from the user data folder.
|
||||
* @param _event - The IPC event object.
|
||||
* @param filePath - The path to the file to delete.
|
||||
* @returns A string indicating the result of the operation.
|
||||
*/
|
||||
ipcMain.handle("deleteFile", async (_event, filePath) => {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const fullPath = join(userDataPath, filePath);
|
||||
|
||||
let result = "NULL";
|
||||
fs.unlink(fullPath, function (err) {
|
||||
if (err && err.code == "ENOENT") {
|
||||
result = `File not exist: ${err}`;
|
||||
} else if (err) {
|
||||
result = `File delete error: ${err}`;
|
||||
} else {
|
||||
result = "File deleted successfully";
|
||||
}
|
||||
console.log(`Delete file ${filePath} from ${fullPath} result: ${result}`);
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
118
electron/handlers/plugin.ts
Normal file
118
electron/handlers/plugin.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { app, ipcMain } from "electron";
|
||||
import { readdirSync, rmdir, writeFileSync } from "fs";
|
||||
import { ModuleManager } from "../managers/module";
|
||||
import { join, extname } from "path";
|
||||
import { PluginManager } from "../managers/plugin";
|
||||
import { WindowManager } from "../managers/window";
|
||||
import { manifest, tarball } from "pacote";
|
||||
|
||||
export function handlePluginIPCs() {
|
||||
/**
|
||||
* Invokes a function from a plugin module in main node process.
|
||||
* @param _event - The IPC event object.
|
||||
* @param modulePath - The path to the plugin 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(
|
||||
"invokePluginFunc",
|
||||
async (_event, modulePath, method, ...args) => {
|
||||
const module = require(
|
||||
/* webpackIgnore: true */ join(
|
||||
app.getPath("userData"),
|
||||
"plugins",
|
||||
modulePath
|
||||
)
|
||||
);
|
||||
ModuleManager.instance.setModule(modulePath, module);
|
||||
|
||||
if (typeof module[method] === "function") {
|
||||
return module[method](...args);
|
||||
} else {
|
||||
console.log(module[method]);
|
||||
console.error(`Function "${method}" does not exist in the module.`);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the paths of the base plugins.
|
||||
* @param _event - The IPC event object.
|
||||
* @returns An array of paths to the base plugins.
|
||||
*/
|
||||
ipcMain.handle("basePlugins", async (_event) => {
|
||||
const basePluginPath = join(
|
||||
__dirname,
|
||||
"../",
|
||||
app.isPackaged
|
||||
? "../../app.asar.unpacked/core/pre-install"
|
||||
: "../core/pre-install"
|
||||
);
|
||||
return readdirSync(basePluginPath)
|
||||
.filter((file) => extname(file) === ".tgz")
|
||||
.map((file) => join(basePluginPath, file));
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the path to the user's plugin directory.
|
||||
* @param _event - The IPC event object.
|
||||
* @returns The path to the user's plugin directory.
|
||||
*/
|
||||
ipcMain.handle("pluginPath", async (_event) => {
|
||||
return join(app.getPath("userData"), "plugins");
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes the `plugins` directory in the user data path and disposes of required modules.
|
||||
* If the app is packaged, the function relaunches the app and exits.
|
||||
* Otherwise, the function deletes the cached modules and sets up the plugins and reloads the main window.
|
||||
* @param _event - The IPC event object.
|
||||
* @param url - The URL to reload.
|
||||
*/
|
||||
ipcMain.handle("reloadPlugins", async (_event, url) => {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const fullPath = join(userDataPath, "plugins");
|
||||
|
||||
rmdir(fullPath, { recursive: true }, function (err) {
|
||||
if (err) console.log(err);
|
||||
ModuleManager.instance.clearImportedModules();
|
||||
|
||||
// just relaunch if packaged, should launch manually in development mode
|
||||
if (app.isPackaged) {
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
} else {
|
||||
for (const modulePath in ModuleManager.instance.requiredModules) {
|
||||
delete require.cache[
|
||||
require.resolve(
|
||||
join(app.getPath("userData"), "plugins", modulePath)
|
||||
)
|
||||
];
|
||||
}
|
||||
PluginManager.instance.setupPlugins();
|
||||
WindowManager.instance.currentWindow?.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Installs a remote plugin by downloading its tarball and writing it to a tgz file.
|
||||
* @param _event - The IPC event object.
|
||||
* @param pluginName - The name of the remote plugin to install.
|
||||
* @returns A Promise that resolves to the path of the installed plugin file.
|
||||
*/
|
||||
ipcMain.handle("installRemotePlugin", async (_event, pluginName) => {
|
||||
const destination = join(
|
||||
app.getPath("userData"),
|
||||
pluginName.replace(/^@.*\//, "") + ".tgz"
|
||||
);
|
||||
return manifest(pluginName)
|
||||
.then(async (manifest: any) => {
|
||||
await tarball(manifest._resolved).then((data: Buffer) => {
|
||||
writeFileSync(destination, data);
|
||||
});
|
||||
})
|
||||
.then(() => destination);
|
||||
});
|
||||
}
|
||||
27
electron/handlers/theme.ts
Normal file
27
electron/handlers/theme.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { ipcMain, nativeTheme } from "electron";
|
||||
|
||||
export function handleThemesIPCs() {
|
||||
/**
|
||||
* Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light".
|
||||
* This will change the appearance of the app to the light theme.
|
||||
*/
|
||||
ipcMain.handle("setNativeThemeLight", () => {
|
||||
nativeTheme.themeSource = "light";
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the "setNativeThemeDark" IPC message by setting the native theme source to "dark".
|
||||
* This will change the appearance of the app to the dark theme.
|
||||
*/
|
||||
ipcMain.handle("setNativeThemeDark", () => {
|
||||
nativeTheme.themeSource = "dark";
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the "setNativeThemeSystem" IPC message by setting the native theme source to "system".
|
||||
* This will change the appearance of the app to match the system's current theme.
|
||||
*/
|
||||
ipcMain.handle("setNativeThemeSystem", () => {
|
||||
nativeTheme.themeSource = "system";
|
||||
});
|
||||
}
|
||||
58
electron/handlers/update.ts
Normal file
58
electron/handlers/update.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { app, dialog } from "electron";
|
||||
import { WindowManager } from "../managers/window";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
|
||||
export function handleAppUpdates() {
|
||||
/* Should not check for update during development */
|
||||
if (!app.isPackaged) {
|
||||
return;
|
||||
}
|
||||
/* New Update Available */
|
||||
autoUpdater.on("update-available", async (_info: any) => {
|
||||
const action = await dialog.showMessageBox({
|
||||
message: `Update available. Do you want to download the latest update?`,
|
||||
buttons: ["Download", "Later"],
|
||||
});
|
||||
if (action.response === 0) await autoUpdater.downloadUpdate();
|
||||
});
|
||||
|
||||
/* App Update Completion Message */
|
||||
autoUpdater.on("update-downloaded", async (_info: any) => {
|
||||
WindowManager.instance.currentWindow?.webContents.send(
|
||||
"APP_UPDATE_COMPLETE",
|
||||
{}
|
||||
);
|
||||
const action = await dialog.showMessageBox({
|
||||
message: `Update downloaded. Please restart the application to apply the updates.`,
|
||||
buttons: ["Restart", "Later"],
|
||||
});
|
||||
if (action.response === 0) {
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
});
|
||||
|
||||
/* App Update Error */
|
||||
autoUpdater.on("error", (info: any) => {
|
||||
dialog.showMessageBox({ message: info.message });
|
||||
WindowManager.instance.currentWindow?.webContents.send(
|
||||
"APP_UPDATE_ERROR",
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
/* App Update Progress */
|
||||
autoUpdater.on("download-progress", (progress: any) => {
|
||||
console.log("app update progress: ", progress.percent);
|
||||
WindowManager.instance.currentWindow?.webContents.send(
|
||||
"APP_UPDATE_PROGRESS",
|
||||
{
|
||||
percent: progress.percent,
|
||||
}
|
||||
);
|
||||
});
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
if (process.env.CI !== "e2e") {
|
||||
autoUpdater.checkForUpdates();
|
||||
}
|
||||
}
|
||||
446
electron/main.ts
446
electron/main.ts
@ -1,33 +1,28 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
ipcMain,
|
||||
dialog,
|
||||
shell,
|
||||
nativeTheme,
|
||||
} from "electron";
|
||||
import { readdirSync, writeFileSync } from "fs";
|
||||
import { resolve, join, extname } from "path";
|
||||
import { rmdir, unlink, createWriteStream } from "fs";
|
||||
import { init } from "./core/plugin/index";
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { join } from "path";
|
||||
import { setupMenu } from "./utils/menu";
|
||||
import { dispose } from "./utils/disposable";
|
||||
import { handleFs } from "./handlers/fs";
|
||||
import { handleFsIPCs } from "./handlers/fs";
|
||||
|
||||
const pacote = require("pacote");
|
||||
const request = require("request");
|
||||
const progress = require("request-progress");
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
const Store = require("electron-store");
|
||||
/**
|
||||
* Managers
|
||||
**/
|
||||
import { WindowManager } from "./managers/window";
|
||||
import { ModuleManager } from "./managers/module";
|
||||
import { PluginManager } from "./managers/plugin";
|
||||
|
||||
let requiredModules: Record<string, any> = {};
|
||||
const networkRequests: Record<string, any> = {};
|
||||
let mainWindow: BrowserWindow | undefined = undefined;
|
||||
/**
|
||||
* IPC Handlers
|
||||
**/
|
||||
import { handleDownloaderIPCs } from "./handlers/download";
|
||||
import { handleThemesIPCs } from "./handlers/theme";
|
||||
import { handlePluginIPCs } from "./handlers/plugin";
|
||||
import { handleAppIPCs } from "./handlers/app";
|
||||
import { handleAppUpdates } from "./handlers/update";
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(migratePlugins)
|
||||
.then(setupPlugins)
|
||||
.then(PluginManager.instance.migratePlugins)
|
||||
.then(PluginManager.instance.setupPlugins)
|
||||
.then(setupMenu)
|
||||
.then(handleIPCs)
|
||||
.then(handleAppUpdates)
|
||||
@ -41,27 +36,18 @@ app
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
clearImportedModules();
|
||||
ModuleManager.instance.clearImportedModules();
|
||||
app.quit();
|
||||
});
|
||||
|
||||
app.on("quit", () => {
|
||||
clearImportedModules();
|
||||
ModuleManager.instance.clearImportedModules();
|
||||
app.quit();
|
||||
});
|
||||
|
||||
function createMainWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
minWidth: 800,
|
||||
height: 800,
|
||||
show: false,
|
||||
trafficLightPosition: {
|
||||
x: 10,
|
||||
y: 15,
|
||||
},
|
||||
titleBarStyle: "hidden",
|
||||
vibrancy: "sidebar",
|
||||
/* Create main window */
|
||||
const mainWindow = WindowManager.instance.createWindow({
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
preload: join(__dirname, "preload.js"),
|
||||
@ -73,6 +59,7 @@ function createMainWindow() {
|
||||
? `file://${join(__dirname, "../renderer/index.html")}`
|
||||
: "http://localhost:3000";
|
||||
|
||||
/* Load frontend app to the window */
|
||||
mainWindow.loadURL(startURL);
|
||||
|
||||
mainWindow.once("ready-to-show", () => mainWindow?.show());
|
||||
@ -80,390 +67,17 @@ function createMainWindow() {
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
|
||||
/* Enable dev tools for development */
|
||||
if (!app.isPackaged) mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
function handleAppUpdates() {
|
||||
/*New Update Available*/
|
||||
autoUpdater.on("update-available", async (_info: any) => {
|
||||
const action = await dialog.showMessageBox({
|
||||
message: `Update available. Do you want to download the latest update?`,
|
||||
buttons: ["Download", "Later"],
|
||||
});
|
||||
if (action.response === 0) await autoUpdater.downloadUpdate();
|
||||
});
|
||||
|
||||
/*App Update Completion Message*/
|
||||
autoUpdater.on("update-downloaded", async (_info: any) => {
|
||||
mainWindow?.webContents.send("APP_UPDATE_COMPLETE", {});
|
||||
const action = await dialog.showMessageBox({
|
||||
message: `Update downloaded. Please restart the application to apply the updates.`,
|
||||
buttons: ["Restart", "Later"],
|
||||
});
|
||||
if (action.response === 0) {
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
});
|
||||
|
||||
/*App Update Error */
|
||||
autoUpdater.on("error", (info: any) => {
|
||||
dialog.showMessageBox({ message: info.message });
|
||||
mainWindow?.webContents.send("APP_UPDATE_ERROR", {});
|
||||
});
|
||||
|
||||
/*App Update Progress */
|
||||
autoUpdater.on("download-progress", (progress: any) => {
|
||||
console.log("app update progress: ", progress.percent);
|
||||
mainWindow?.webContents.send("APP_UPDATE_PROGRESS", {
|
||||
percent: progress.percent,
|
||||
});
|
||||
});
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
if (process.env.CI !== "e2e") {
|
||||
autoUpdater.checkForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles various IPC messages from the renderer process.
|
||||
*/
|
||||
function handleIPCs() {
|
||||
handleFs();
|
||||
/**
|
||||
* Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light".
|
||||
* This will change the appearance of the app to the light theme.
|
||||
*/
|
||||
ipcMain.handle("setNativeThemeLight", () => {
|
||||
nativeTheme.themeSource = "light";
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the "setNativeThemeDark" IPC message by setting the native theme source to "dark".
|
||||
* This will change the appearance of the app to the dark theme.
|
||||
*/
|
||||
ipcMain.handle("setNativeThemeDark", () => {
|
||||
nativeTheme.themeSource = "dark";
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the "setNativeThemeSystem" IPC message by setting the native theme source to "system".
|
||||
* This will change the appearance of the app to match the system's current theme.
|
||||
*/
|
||||
ipcMain.handle("setNativeThemeSystem", () => {
|
||||
nativeTheme.themeSource = "system";
|
||||
});
|
||||
|
||||
/**
|
||||
* Invokes a function from a plugin module in main node process.
|
||||
* @param _event - The IPC event object.
|
||||
* @param modulePath - The path to the plugin 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(
|
||||
"invokePluginFunc",
|
||||
async (_event, modulePath, method, ...args) => {
|
||||
const module = require(
|
||||
/* webpackIgnore: true */ join(
|
||||
app.getPath("userData"),
|
||||
"plugins",
|
||||
modulePath
|
||||
)
|
||||
);
|
||||
requiredModules[modulePath] = module;
|
||||
|
||||
if (typeof module[method] === "function") {
|
||||
return module[method](...args);
|
||||
} else {
|
||||
console.log(module[method]);
|
||||
console.error(`Function "${method}" does not exist in the module.`);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the paths of the base plugins.
|
||||
* @param _event - The IPC event object.
|
||||
* @returns An array of paths to the base plugins.
|
||||
*/
|
||||
ipcMain.handle("basePlugins", async (_event) => {
|
||||
const basePluginPath = join(
|
||||
__dirname,
|
||||
"../",
|
||||
app.isPackaged
|
||||
? "../app.asar.unpacked/core/pre-install"
|
||||
: "/core/pre-install"
|
||||
);
|
||||
return readdirSync(basePluginPath)
|
||||
.filter((file) => extname(file) === ".tgz")
|
||||
.map((file) => join(basePluginPath, file));
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the path to the user's plugin directory.
|
||||
* @param _event - The IPC event object.
|
||||
* @returns The path to the user's plugin directory.
|
||||
*/
|
||||
ipcMain.handle("pluginPath", async (_event) => {
|
||||
return join(app.getPath("userData"), "plugins");
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieves the path to the app data directory using the `coreAPI` object.
|
||||
* If the `coreAPI` object is not available, the function returns `undefined`.
|
||||
* @returns A Promise that resolves with the path to the app data directory, or `undefined` if the `coreAPI` object is not available.
|
||||
*/
|
||||
ipcMain.handle("appDataPath", async (_event) => {
|
||||
return app.getPath("userData");
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the version of the app.
|
||||
* @param _event - The IPC event object.
|
||||
* @returns The version of the app.
|
||||
*/
|
||||
ipcMain.handle("appVersion", async (_event) => {
|
||||
return app.getVersion();
|
||||
});
|
||||
|
||||
/**
|
||||
* 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("openAppDirectory", async (_event) => {
|
||||
shell.openPath(app.getPath("userData"));
|
||||
});
|
||||
|
||||
/**
|
||||
* Opens a URL in the user's default browser.
|
||||
* @param _event - The IPC event object.
|
||||
* @param url - The URL to open.
|
||||
*/
|
||||
ipcMain.handle("openExternalUrl", async (_event, url) => {
|
||||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
/**
|
||||
* Relaunches the app in production - reload window in development.
|
||||
* @param _event - The IPC event object.
|
||||
* @param url - The URL to reload.
|
||||
*/
|
||||
ipcMain.handle("relaunch", async (_event, url) => {
|
||||
clearImportedModules();
|
||||
|
||||
if (app.isPackaged) {
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
} else {
|
||||
for (const modulePath in requiredModules) {
|
||||
delete require.cache[
|
||||
require.resolve(join(app.getPath("userData"), "plugins", modulePath))
|
||||
];
|
||||
}
|
||||
setupPlugins();
|
||||
mainWindow?.reload();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes the `plugins` directory in the user data path and disposes of required modules.
|
||||
* If the app is packaged, the function relaunches the app and exits.
|
||||
* Otherwise, the function deletes the cached modules and sets up the plugins and reloads the main window.
|
||||
* @param _event - The IPC event object.
|
||||
* @param url - The URL to reload.
|
||||
*/
|
||||
ipcMain.handle("reloadPlugins", async (_event, url) => {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const fullPath = join(userDataPath, "plugins");
|
||||
|
||||
rmdir(fullPath, { recursive: true }, function (err) {
|
||||
if (err) console.log(err);
|
||||
clearImportedModules();
|
||||
|
||||
// just relaunch if packaged, should launch manually in development mode
|
||||
if (app.isPackaged) {
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
} else {
|
||||
for (const modulePath in requiredModules) {
|
||||
delete require.cache[
|
||||
require.resolve(
|
||||
join(app.getPath("userData"), "plugins", modulePath)
|
||||
)
|
||||
];
|
||||
}
|
||||
setupPlugins();
|
||||
mainWindow?.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a file from the user data folder.
|
||||
* @param _event - The IPC event object.
|
||||
* @param filePath - The path to the file to delete.
|
||||
* @returns A string indicating the result of the operation.
|
||||
*/
|
||||
ipcMain.handle("deleteFile", async (_event, filePath) => {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const fullPath = join(userDataPath, filePath);
|
||||
|
||||
let result = "NULL";
|
||||
unlink(fullPath, function (err) {
|
||||
if (err && err.code == "ENOENT") {
|
||||
result = `File not exist: ${err}`;
|
||||
} else if (err) {
|
||||
result = `File delete error: ${err}`;
|
||||
} else {
|
||||
result = "File deleted successfully";
|
||||
}
|
||||
console.log(`Delete file ${filePath} from ${fullPath} result: ${result}`);
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* 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("downloadFile", async (_event, url, fileName) => {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const destination = resolve(userDataPath, fileName);
|
||||
const rq = request(url);
|
||||
|
||||
progress(rq, {})
|
||||
.on("progress", function (state: any) {
|
||||
mainWindow?.webContents.send("FILE_DOWNLOAD_UPDATE", {
|
||||
...state,
|
||||
fileName,
|
||||
});
|
||||
})
|
||||
.on("error", function (err: Error) {
|
||||
mainWindow?.webContents.send("FILE_DOWNLOAD_ERROR", {
|
||||
fileName,
|
||||
err,
|
||||
});
|
||||
networkRequests[fileName] = undefined;
|
||||
})
|
||||
.on("end", function () {
|
||||
if (networkRequests[fileName]) {
|
||||
mainWindow?.webContents.send("FILE_DOWNLOAD_COMPLETE", {
|
||||
fileName,
|
||||
});
|
||||
networkRequests[fileName] = undefined;
|
||||
} else {
|
||||
mainWindow?.webContents.send("FILE_DOWNLOAD_ERROR", {
|
||||
fileName,
|
||||
err: "Download cancelled",
|
||||
});
|
||||
}
|
||||
})
|
||||
.pipe(createWriteStream(destination));
|
||||
|
||||
networkRequests[fileName] = rq;
|
||||
});
|
||||
|
||||
/**
|
||||
* 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("pauseDownload", async (_event, fileName) => {
|
||||
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("resumeDownload", async (_event, fileName) => {
|
||||
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("abortDownload", async (_event, fileName) => {
|
||||
const rq = networkRequests[fileName];
|
||||
networkRequests[fileName] = undefined;
|
||||
rq?.abort();
|
||||
});
|
||||
|
||||
/**
|
||||
* Installs a remote plugin by downloading its tarball and writing it to a tgz file.
|
||||
* @param _event - The IPC event object.
|
||||
* @param pluginName - The name of the remote plugin to install.
|
||||
* @returns A Promise that resolves to the path of the installed plugin file.
|
||||
*/
|
||||
ipcMain.handle("installRemotePlugin", async (_event, pluginName) => {
|
||||
const destination = join(
|
||||
app.getPath("userData"),
|
||||
pluginName.replace(/^@.*\//, "") + ".tgz"
|
||||
);
|
||||
return pacote
|
||||
.manifest(pluginName)
|
||||
.then(async (manifest: any) => {
|
||||
await pacote.tarball(manifest._resolved).then((data: Buffer) => {
|
||||
writeFileSync(destination, data);
|
||||
});
|
||||
})
|
||||
.then(() => destination);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the plugins by deleting the `plugins` directory in the user data path.
|
||||
* If the `migrated_version` key in the `Store` object does not match the current app version,
|
||||
* the function deletes the `plugins` directory and sets the `migrated_version` key to the current app version.
|
||||
* @returns A Promise that resolves when the migration is complete.
|
||||
*/
|
||||
function migratePlugins() {
|
||||
return new Promise((resolve) => {
|
||||
const store = new Store();
|
||||
if (store.get("migrated_version") !== app.getVersion()) {
|
||||
console.log("start migration:", store.get("migrated_version"));
|
||||
const userDataPath = app.getPath("userData");
|
||||
const fullPath = join(userDataPath, "plugins");
|
||||
|
||||
rmdir(fullPath, { recursive: true }, function (err) {
|
||||
if (err) console.log(err);
|
||||
store.set("migrated_version", app.getVersion());
|
||||
console.log("migrate plugins done");
|
||||
resolve(undefined);
|
||||
});
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the plugins by initializing the `plugins` module with the `confirmInstall` and `pluginsPath` options.
|
||||
* The `confirmInstall` function always returns `true` to allow plugin installation.
|
||||
* The `pluginsPath` option specifies the path to install plugins to.
|
||||
*/
|
||||
function setupPlugins() {
|
||||
init({
|
||||
// Function to check from the main process that user wants to install a plugin
|
||||
confirmInstall: async (_plugins: string[]) => {
|
||||
return true;
|
||||
},
|
||||
// Path to install plugin to
|
||||
pluginsPath: join(app.getPath("userData"), "plugins"),
|
||||
});
|
||||
}
|
||||
|
||||
function clearImportedModules() {
|
||||
dispose(requiredModules);
|
||||
requiredModules = {};
|
||||
handleFsIPCs();
|
||||
handleDownloaderIPCs();
|
||||
handleThemesIPCs();
|
||||
handlePluginIPCs();
|
||||
handleAppIPCs();
|
||||
}
|
||||
|
||||
24
electron/managers/download.ts
Normal file
24
electron/managers/download.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Request } from "request";
|
||||
|
||||
/**
|
||||
* Manages file downloads and network requests.
|
||||
*/
|
||||
export class DownloadManager {
|
||||
public networkRequests: Record<string, any> = {};
|
||||
|
||||
public static instance: DownloadManager = new DownloadManager();
|
||||
|
||||
constructor() {
|
||||
if (DownloadManager.instance) {
|
||||
return DownloadManager.instance;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sets a network request for a specific file.
|
||||
* @param {string} fileName - The name of the file.
|
||||
* @param {Request | undefined} request - The network request to set, or undefined to clear the request.
|
||||
*/
|
||||
setRequest(fileName: string, request: Request | undefined) {
|
||||
this.networkRequests[fileName] = request;
|
||||
}
|
||||
}
|
||||
33
electron/managers/module.ts
Normal file
33
electron/managers/module.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { dispose } from "../utils/disposable";
|
||||
|
||||
/**
|
||||
* Manages imported modules.
|
||||
*/
|
||||
export class ModuleManager {
|
||||
public requiredModules: Record<string, any> = {};
|
||||
|
||||
public static instance: ModuleManager = new ModuleManager();
|
||||
|
||||
constructor() {
|
||||
if (ModuleManager.instance) {
|
||||
return ModuleManager.instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a module.
|
||||
* @param {string} moduleName - The name of the module.
|
||||
* @param {any | undefined} nodule - The module to set, or undefined to clear the module.
|
||||
*/
|
||||
setModule(moduleName: string, nodule: any | undefined) {
|
||||
this.requiredModules[moduleName] = nodule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all imported modules.
|
||||
*/
|
||||
clearImportedModules() {
|
||||
dispose(this.requiredModules);
|
||||
this.requiredModules = {};
|
||||
}
|
||||
}
|
||||
60
electron/managers/plugin.ts
Normal file
60
electron/managers/plugin.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { app } from "electron";
|
||||
import { init } from "../core/plugin/index";
|
||||
import { join } from "path";
|
||||
import { rmdir } from "fs";
|
||||
import Store from "electron-store";
|
||||
|
||||
/**
|
||||
* Manages plugin installation and migration.
|
||||
*/
|
||||
export class PluginManager {
|
||||
public static instance: PluginManager = new PluginManager();
|
||||
|
||||
constructor() {
|
||||
if (PluginManager.instance) {
|
||||
return PluginManager.instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the plugins by initializing the `plugins` module with the `confirmInstall` and `pluginsPath` options.
|
||||
* The `confirmInstall` function always returns `true` to allow plugin installation.
|
||||
* The `pluginsPath` option specifies the path to install plugins to.
|
||||
*/
|
||||
setupPlugins() {
|
||||
init({
|
||||
// Function to check from the main process that user wants to install a plugin
|
||||
confirmInstall: async (_plugins: string[]) => {
|
||||
return true;
|
||||
},
|
||||
// Path to install plugin to
|
||||
pluginsPath: join(app.getPath("userData"), "plugins"),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the plugins by deleting the `plugins` directory in the user data path.
|
||||
* If the `migrated_version` key in the `Store` object does not match the current app version,
|
||||
* the function deletes the `plugins` directory and sets the `migrated_version` key to the current app version.
|
||||
* @returns A Promise that resolves when the migration is complete.
|
||||
*/
|
||||
migratePlugins() {
|
||||
return new Promise((resolve) => {
|
||||
const store = new Store();
|
||||
if (store.get("migrated_version") !== app.getVersion()) {
|
||||
console.log("start migration:", store.get("migrated_version"));
|
||||
const userDataPath = app.getPath("userData");
|
||||
const fullPath = join(userDataPath, "plugins");
|
||||
|
||||
rmdir(fullPath, { recursive: true }, function (err) {
|
||||
if (err) console.log(err);
|
||||
store.set("migrated_version", app.getVersion());
|
||||
console.log("migrate plugins done");
|
||||
resolve(undefined);
|
||||
});
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
37
electron/managers/window.ts
Normal file
37
electron/managers/window.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
|
||||
/**
|
||||
* Manages the current window instance.
|
||||
*/
|
||||
export class WindowManager {
|
||||
public static instance: WindowManager = new WindowManager();
|
||||
public currentWindow?: BrowserWindow;
|
||||
|
||||
constructor() {
|
||||
if (WindowManager.instance) {
|
||||
return WindowManager.instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new window instance.
|
||||
* @param {Electron.BrowserWindowConstructorOptions} options - The options to create the window with.
|
||||
* @returns The created window instance.
|
||||
*/
|
||||
createWindow(options?: Electron.BrowserWindowConstructorOptions | undefined) {
|
||||
this.currentWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
minWidth: 800,
|
||||
height: 800,
|
||||
show: false,
|
||||
trafficLightPosition: {
|
||||
x: 10,
|
||||
y: 15,
|
||||
},
|
||||
titleBarStyle: "hidden",
|
||||
vibrancy: "sidebar",
|
||||
...options,
|
||||
});
|
||||
return this.currentWindow;
|
||||
}
|
||||
}
|
||||
@ -67,6 +67,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@npmcli/arborist": "^7.1.0",
|
||||
"@types/request": "^2.48.12",
|
||||
"@uiball/loaders": "^1.3.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.4",
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
"outDir": "./build",
|
||||
"rootDir": "./",
|
||||
"noEmitOnError": true,
|
||||
"esModuleInterop": true,
|
||||
"baseUrl": ".",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
17
package.json
17
package.json
@ -35,27 +35,22 @@
|
||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||
"build:electron": "yarn workspace jan build",
|
||||
"build:electron:test": "yarn workspace jan build:test",
|
||||
"build:pull-plugins": "rimraf ./electron/core/pre-install/*.tgz && cd ./electron/core/pre-install && npm pack @janhq/inference-plugin @janhq/monitoring-plugin",
|
||||
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install --ignore-scripts && npm run postinstall:dev && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall && npm run build:publish\"",
|
||||
"build:plugins-windows": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install --ignore-scripts && npm run postinstall:windows && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall && npm run build:publish\"",
|
||||
"build:plugins-linux": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall && npm run build:publish\"",
|
||||
"build:plugins-darwin": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall && ../../.github/scripts/auto-sign.sh && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall && npm run build:publish\"",
|
||||
"build:plugins-web": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:deps && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"",
|
||||
"build": "yarn build:web && yarn build:electron",
|
||||
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"",
|
||||
"build:plugins-win32": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish-win32\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"",
|
||||
"build:plugins-linux": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish-linux\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"",
|
||||
"build:plugins-darwin": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish-darwin\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"",
|
||||
"build:test": "yarn build:web && yarn build:electron:test",
|
||||
"build:test-darwin": "yarn build:web && yarn workspace jan build:test-darwin",
|
||||
"build:test-win32": "yarn build:web && yarn workspace jan build:test-win32",
|
||||
"build:test-linux": "yarn build:web && yarn workspace jan build:test-linux",
|
||||
"build": "yarn build:web && yarn build:electron",
|
||||
"build:darwin": "yarn build:web && yarn workspace jan build:darwin",
|
||||
"build:win32": "yarn build:web && yarn workspace jan build:win32",
|
||||
"build:linux": "yarn build:web && yarn workspace jan build:linux",
|
||||
"build:publish": "yarn build:web && yarn workspace jan build:publish",
|
||||
"build:publish-darwin": "yarn build:web && yarn workspace jan build:publish-darwin",
|
||||
"build:publish-win32": "yarn build:web && yarn workspace jan build:publish-win32",
|
||||
"build:publish-linux": "yarn build:web && yarn workspace jan build:publish-linux",
|
||||
"build:web-plugins": "yarn build:web && yarn build:plugins-web && mkdir -p \"./web/out/plugins/conversational-json\" && cp \"./plugins/conversational-json/dist/index.js\" \"./web/out/plugins/conversational-json\" && mkdir -p \"./web/out/plugins/inference-plugin\" && cp \"./plugins/inference-plugin/dist/index.js\" \"./web/out/plugins/inference-plugin\" && mkdir -p \"./web/out/plugins/model-plugin\" && cp \"./plugins/model-plugin/dist/index.js\" \"./web/out/plugins/model-plugin\" && mkdir -p \"./web/out/plugins/monitoring-plugin\" && cp \"./plugins/monitoring-plugin/dist/index.js\" \"./web/out/plugins/monitoring-plugin\"",
|
||||
"server:prod": "yarn workspace server build && yarn build:web-plugins && cpx \"web/out/**\" \"server/build/renderer/\" && mkdir -p ./server/build/@janhq && cp -r ./plugins/* ./server/build/@janhq",
|
||||
"start:server": "yarn server:prod && node server/build/main.js"
|
||||
"build:publish-linux": "yarn build:web && yarn workspace jan build:publish-linux"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.2.1",
|
||||
|
||||
@ -10,9 +10,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"postinstall": "rimraf *.tgz --glob && npm run build",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install",
|
||||
"build:debug": "rimraf *.tgz --glob && npm run build && npm pack"
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
|
||||
@ -11,8 +11,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"postinstall": "rimraf *.tgz --glob && npm run build",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||
"build:publish": "rimraf *.tgz --glob npm run build && && npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
|
||||
@ -1 +1 @@
|
||||
0.1.6
|
||||
0.1.8
|
||||
@ -15,14 +15,16 @@
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"downloadnitro:linux-cpu": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.zip -e --strip 1 -o ./nitro/linux-cpu && chmod +x ./nitro/linux-cpu/nitro && chmod +x ./nitro/linux-start.sh ",
|
||||
"downloadnitro:linux-cuda": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.zip -e --strip 1 -o ./nitro/linux-cuda && chmod +x ./nitro/linux-cuda/nitro && chmod +x ./nitro/linux-start.sh",
|
||||
"downloadnitro:mac-arm64": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.zip -e --strip 1 -o ./nitro/mac-arm64 && chmod +x ./nitro/mac-arm64/nitro",
|
||||
"downloadnitro:mac-x64": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.zip -e --strip 1 -o ./nitro/mac-x64 && chmod +x ./nitro/mac-x64/nitro",
|
||||
"downloadnitro:win-cpu": "download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.zip -e --strip 1 -o ./nitro/win-cpu",
|
||||
"downloadnitro:win-cuda": "download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda.zip -e --strip 1 -o ./nitro/win-cuda",
|
||||
"postinstall": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:linux-cpu && npm run downloadnitro:linux-cuda && npm run downloadnitro:mac-arm64 && npm run downloadnitro:mac-x64 && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
|
||||
"postinstall:dev": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:mac-arm64 && npm run downloadnitro:mac-x64 && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
|
||||
"postinstall:windows": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:win-cpu && npm run downloadnitro:win-cuda && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||
"downloadnitro:darwin-arm64": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.zip -e --strip 1 -o ./nitro/mac-arm64 && chmod +x ./nitro/mac-arm64/nitro",
|
||||
"downloadnitro:darwin-x64": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.zip -e --strip 1 -o ./nitro/mac-x64 && chmod +x ./nitro/mac-x64/nitro",
|
||||
"downloadnitro:win32-cpu": "download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.zip -e --strip 1 -o ./nitro/win-cpu",
|
||||
"downloadnitro:win32-cuda": "download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda.zip -e --strip 1 -o ./nitro/win-cuda",
|
||||
"downloadnitro:all": "npm run downloadnitro:darwin-arm64 && npm run downloadnitro:darwin-x64 && downloadnitro:win32-cpu && npm run downloadnitro:win32-cuda && npm run downloadnitro:linux-cpu && npm run downloadnitro:linux-cuda",
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:darwin-arm64 && npm run downloadnitro:darwin-x64 && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install",
|
||||
"build:publish-darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:darwin-arm64 && npm run downloadnitro:darwin-x64 && ../../.github/scripts/auto-sign.sh && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install",
|
||||
"build:publish-win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:win32-cpu && npm run downloadnitro:win32-cuda && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install",
|
||||
"build:publish-linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:linux-cpu && npm run downloadnitro:linux-cuda && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install",
|
||||
"build:publish-all": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:all && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
@ -37,10 +39,13 @@
|
||||
"dependencies": {
|
||||
"@janhq/core": "file:../../core",
|
||||
"download-cli": "^1.1.1",
|
||||
"fetch-retry": "^5.0.6",
|
||||
"kill-port": "^2.0.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"tcp-port-used": "^1.0.2",
|
||||
"ts-loader": "^9.5.0"
|
||||
"ts-loader": "^9.5.0",
|
||||
"ulid": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@ -52,6 +57,7 @@
|
||||
],
|
||||
"bundleDependencies": [
|
||||
"tcp-port-used",
|
||||
"kill-port"
|
||||
"kill-port",
|
||||
"fetch-retry"
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
export const generateMessageId = () => {
|
||||
return `m-${Date.now()}`
|
||||
}
|
||||
@ -16,7 +16,9 @@ import {
|
||||
} from "@janhq/core";
|
||||
import { InferencePlugin } from "@janhq/core/lib/plugins";
|
||||
import { requestInference } from "./helpers/sse";
|
||||
import { generateMessageId } from "./helpers/message";
|
||||
import { ulid } from "ulid";
|
||||
import { join } from "path";
|
||||
import { appDataPath } from "@janhq/core";
|
||||
|
||||
/**
|
||||
* A class that implements the InferencePlugin interface from the @janhq/core package.
|
||||
@ -48,18 +50,19 @@ export default class JanInferencePlugin implements InferencePlugin {
|
||||
|
||||
/**
|
||||
* Initializes the model with the specified file name.
|
||||
* @param {string} modelFileName - The name of the model file.
|
||||
* @param {string} modelFileName - The file name of the model file.
|
||||
* @returns {Promise<void>} A promise that resolves when the model is initialized.
|
||||
*/
|
||||
initModel(modelFileName: string): Promise<void> {
|
||||
return executeOnMain(MODULE, "initModel", modelFileName);
|
||||
async initModel(modelFileName: string): Promise<void> {
|
||||
const appPath = await appDataPath();
|
||||
return executeOnMain(MODULE, "initModel", join(appPath, modelFileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the model.
|
||||
* @returns {Promise<void>} A promise that resolves when the model is stopped.
|
||||
*/
|
||||
stopModel(): Promise<void> {
|
||||
async stopModel(): Promise<void> {
|
||||
return executeOnMain(MODULE, "killSubprocess");
|
||||
}
|
||||
|
||||
@ -112,13 +115,13 @@ export default class JanInferencePlugin implements InferencePlugin {
|
||||
content: data.message,
|
||||
},
|
||||
];
|
||||
const recentMessages = await (data.history ?? prompts);
|
||||
const recentMessages = data.history ?? prompts;
|
||||
const message = {
|
||||
...data,
|
||||
message: "",
|
||||
user: "assistant",
|
||||
createdAt: new Date().toISOString(),
|
||||
_id: generateMessageId(),
|
||||
_id: ulid(),
|
||||
};
|
||||
events.emit(EventName.OnNewMessageResponse, message);
|
||||
|
||||
|
||||
@ -1,25 +1,164 @@
|
||||
const path = require("path");
|
||||
const { app } = require("electron");
|
||||
const { spawn } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const tcpPortUsed = require("tcp-port-used");
|
||||
const kill = require("kill-port");
|
||||
const path = require("path");
|
||||
const { spawn } = require("child_process");
|
||||
const tcpPortUsed = require("tcp-port-used");
|
||||
const fetchRetry = require("fetch-retry")(global.fetch);
|
||||
|
||||
// The PORT to use for the Nitro subprocess
|
||||
const PORT = 3928;
|
||||
let subprocess = null;
|
||||
const LOCAL_HOST = "127.0.0.1";
|
||||
const NITRO_HTTP_SERVER_URL = `http://${LOCAL_HOST}:${PORT}`;
|
||||
const NITRO_HTTP_LOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/loadmodel`;
|
||||
const NITRO_HTTP_UNLOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/unloadModel`;
|
||||
const NITRO_HTTP_VALIDATE_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/modelstatus`;
|
||||
|
||||
// The subprocess instance for Nitro
|
||||
let subprocess = null;
|
||||
let currentModelFile = null;
|
||||
|
||||
/**
|
||||
* The response from the initModel function.
|
||||
* @property error - An error message if the model fails to load.
|
||||
*/
|
||||
interface InitModelResponse {
|
||||
error?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a Nitro subprocess to load a machine learning model.
|
||||
* @param modelFile - The name of the machine learning model file.
|
||||
* @returns A Promise that resolves when the model is loaded successfully, or rejects with an error message if the model is not found or fails to load.
|
||||
* TODO: Should pass absolute of the model file instead of just the name - So we can modurize the module.ts to npm package
|
||||
* TODO: Should it be startModel instead?
|
||||
*/
|
||||
function initModel(modelFile: string): Promise<InitModelResponse> {
|
||||
// 1. Check if the model file exists
|
||||
currentModelFile = modelFile;
|
||||
|
||||
const initModel = (fileName) => {
|
||||
return (
|
||||
new Promise<void>(async (resolve, reject) => {
|
||||
if (!fileName) {
|
||||
reject("Model not found, please download again.");
|
||||
}
|
||||
resolve(fileName);
|
||||
// 1. Check if the port is used, if used, attempt to unload model / kill nitro process
|
||||
validateModelVersion()
|
||||
.then(checkAndUnloadNitro)
|
||||
// 2. Spawn the Nitro subprocess
|
||||
.then(spawnNitroProcess)
|
||||
// 3. Wait until the port is used (Nitro http server is up)
|
||||
.then(() => tcpPortUsed.waitUntilUsed(PORT, 300, 30000))
|
||||
// 4. Load the model into the Nitro subprocess (HTTP POST request)
|
||||
.then(loadLLMModel)
|
||||
// 5. Check if the model is loaded successfully
|
||||
.then(validateModelStatus)
|
||||
.catch((err) => {
|
||||
return { error: err };
|
||||
})
|
||||
// Spawn Nitro subprocess to load model
|
||||
.then(() => {
|
||||
return tcpPortUsed.check(PORT, "127.0.0.1").then((inUse) => {
|
||||
if (!inUse) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a LLM model into the Nitro subprocess by sending a HTTP POST request.
|
||||
* @returns A Promise that resolves when the model is loaded successfully, or rejects with an error message if the model is not found or fails to load.
|
||||
*/
|
||||
function loadLLMModel(): Promise<Response> {
|
||||
const config = {
|
||||
llama_model_path: currentModelFile,
|
||||
ctx_len: 2048,
|
||||
ngl: 100,
|
||||
embedding: false, // Always enable embedding mode on
|
||||
};
|
||||
|
||||
// Load model config
|
||||
return fetchRetry(NITRO_HTTP_LOAD_MODEL_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
retries: 3,
|
||||
retryDelay: 500,
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
// Fetch error, Nitro server might not started properly
|
||||
throw new Error("Model loading failed.");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the status of a model.
|
||||
* @returns {Promise<InitModelResponse>} A promise that resolves to an object.
|
||||
* If the model is loaded successfully, the object is empty.
|
||||
* If the model is not loaded successfully, the object contains an error message.
|
||||
*/
|
||||
async function validateModelStatus(): Promise<InitModelResponse> {
|
||||
// Send a GET request to the validation URL.
|
||||
// Retry the request up to 3 times if it fails, with a delay of 500 milliseconds between retries.
|
||||
return fetchRetry(NITRO_HTTP_VALIDATE_MODEL_URL, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
retries: 5,
|
||||
retryDelay: 500,
|
||||
})
|
||||
.then(async (res: Response) => {
|
||||
// If the response is OK, check model_loaded status.
|
||||
if (res.ok) {
|
||||
const body = await res.json();
|
||||
// If the model is loaded, return an empty object.
|
||||
// Otherwise, return an object with an error message.
|
||||
if (body.model_loaded) {
|
||||
return { error: undefined };
|
||||
}
|
||||
}
|
||||
return { error: "Model is not loaded successfully" };
|
||||
})
|
||||
.catch((err) => {
|
||||
return { error: `Model is not loaded successfully. ${err.message}` };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates the Nitro subprocess.
|
||||
* @returns A Promise that resolves when the subprocess is terminated successfully, or rejects with an error message if the subprocess fails to terminate.
|
||||
*/
|
||||
function killSubprocess(): Promise<void> {
|
||||
if (subprocess) {
|
||||
subprocess.kill();
|
||||
subprocess = null;
|
||||
console.log("Subprocess terminated.");
|
||||
} else {
|
||||
return kill(PORT, "tcp").then(console.log).catch(console.log);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check port is used or not, if used, attempt to unload model
|
||||
* If unload failed, kill the port
|
||||
*/
|
||||
function checkAndUnloadNitro() {
|
||||
return tcpPortUsed.check(PORT, LOCAL_HOST).then((inUse) => {
|
||||
// If inUse - try unload or kill process, otherwise do nothing
|
||||
if (inUse) {
|
||||
// Attempt to unload model
|
||||
return fetch(NITRO_HTTP_UNLOAD_MODEL_URL, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).catch((err) => {
|
||||
console.log(err);
|
||||
// Fallback to kill the port
|
||||
return killSubprocess();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for the Nitro binary and execute it
|
||||
* Using child-process to spawn the process
|
||||
* Should run exactly platform specified Nitro binary version
|
||||
*/
|
||||
function spawnNitroProcess() {
|
||||
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
|
||||
let binaryName;
|
||||
|
||||
@ -29,11 +168,11 @@ const initModel = (fileName) => {
|
||||
} else if (process.platform === "darwin") {
|
||||
// Mac OS platform
|
||||
if (process.arch === "arm64") {
|
||||
binaryFolder = path.join(binaryFolder, "mac-arm64")
|
||||
binaryFolder = path.join(binaryFolder, "mac-arm64");
|
||||
} else {
|
||||
binaryFolder = path.join(binaryFolder, "mac-x64")
|
||||
binaryFolder = path.join(binaryFolder, "mac-x64");
|
||||
}
|
||||
binaryName = "nitro"
|
||||
binaryName = "nitro";
|
||||
} else {
|
||||
// Linux
|
||||
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
|
||||
@ -43,7 +182,9 @@ const initModel = (fileName) => {
|
||||
const binaryPath = path.join(binaryFolder, binaryName);
|
||||
|
||||
// Execute the binary
|
||||
subprocess = spawn(binaryPath,["0.0.0.0", PORT], { cwd: binaryFolder });
|
||||
subprocess = spawn(binaryPath, [1, "0.0.0.0", PORT], {
|
||||
cwd: binaryFolder,
|
||||
});
|
||||
|
||||
// Handle subprocess output
|
||||
subprocess.stdout.on("data", (data) => {
|
||||
@ -58,62 +199,53 @@ const initModel = (fileName) => {
|
||||
console.log(`child process exited with code ${code}`);
|
||||
subprocess = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => tcpPortUsed.waitUntilUsed(PORT, 300, 30000))
|
||||
.then(() => {
|
||||
const llama_model_path = path.join(appPath(), fileName);
|
||||
|
||||
const config = {
|
||||
llama_model_path,
|
||||
ctx_len: 2048,
|
||||
ngl: 100,
|
||||
embedding: true, // Always enable embedding mode on
|
||||
};
|
||||
|
||||
// Load model config
|
||||
return fetch(`http://127.0.0.1:${PORT}/inferences/llamacpp/loadmodel`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
return {};
|
||||
}
|
||||
throw new Error("Nitro: Model failed to load.");
|
||||
})
|
||||
.catch((err) => {
|
||||
return { error: err };
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
function dispose() {
|
||||
killSubprocess();
|
||||
// clean other registered resources here
|
||||
}
|
||||
|
||||
function killSubprocess() {
|
||||
if (subprocess) {
|
||||
subprocess.kill();
|
||||
subprocess = null;
|
||||
console.log("Subprocess terminated.");
|
||||
/**
|
||||
* Validate the model version, if it is GGUFv1, reject the promise
|
||||
* @returns A Promise that resolves when the model is loaded successfully, or rejects with an error message if the model is not found or fails to load.
|
||||
*/
|
||||
function validateModelVersion(): Promise<void> {
|
||||
// Read the file
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.open(currentModelFile, "r", (err, fd) => {
|
||||
if (err) {
|
||||
console.error(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Buffer to store the byte
|
||||
const buffer = Buffer.alloc(1);
|
||||
|
||||
// Model version will be the 5th byte of the file
|
||||
fs.read(fd, buffer, 0, 1, 4, (err, bytesRead, buffer) => {
|
||||
if (err) {
|
||||
console.error(err.message);
|
||||
} else {
|
||||
kill(PORT, "tcp").then(console.log).catch(console.log);
|
||||
console.error("No subprocess is currently running.");
|
||||
// Interpret the byte as ASCII
|
||||
if (buffer[0] === 0x01) {
|
||||
// This is GGUFv1, which is deprecated
|
||||
reject("GGUFv1 model is deprecated, please try another model.");
|
||||
}
|
||||
}
|
||||
|
||||
// Close the file descriptor
|
||||
fs.close(fd, (err) => {
|
||||
if (err) console.error(err.message);
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function appPath() {
|
||||
if (app) {
|
||||
return app.getPath("userData");
|
||||
}
|
||||
return process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share");
|
||||
/**
|
||||
* Cleans up any registered resources.
|
||||
* Its module specific function, should be called when application is closed
|
||||
*/
|
||||
function dispose() {
|
||||
// clean other registered resources here
|
||||
killSubprocess();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@ -18,7 +18,10 @@ module.exports = {
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||
INFERENCE_URL: JSON.stringify(process.env.INFERENCE_URL || "http://127.0.0.1:3928/inferences/llamacpp/chat_completion"),
|
||||
INFERENCE_URL: JSON.stringify(
|
||||
process.env.INFERENCE_URL ||
|
||||
"http://127.0.0.1:3928/inferences/llamacpp/chat_completion"
|
||||
),
|
||||
}),
|
||||
],
|
||||
output: {
|
||||
@ -28,6 +31,9 @@ module.exports = {
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
fallback: {
|
||||
path: require.resolve("path-browserify"),
|
||||
},
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
|
||||
@ -14,8 +14,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"postinstall": "rimraf *.tgz --glob && npm run build",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
|
||||
@ -14,8 +14,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"postinstall": "rimraf *.tgz --glob && npm run build",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^3.0.2",
|
||||
|
||||
179
server/main.ts
179
server/main.ts
@ -1,179 +0,0 @@
|
||||
import express, { Express, Request, Response, NextFunction } from 'express'
|
||||
import cors from "cors";
|
||||
import { resolve } from "path";
|
||||
const fs = require("fs");
|
||||
const progress = require("request-progress");
|
||||
const path = require("path");
|
||||
const request = require("request");
|
||||
|
||||
// Create app dir
|
||||
const userDataPath = appPath();
|
||||
if (!fs.existsSync(userDataPath)) fs.mkdirSync(userDataPath);
|
||||
|
||||
interface ProgressState {
|
||||
percent?: number;
|
||||
speed?: number;
|
||||
size?: {
|
||||
total: number;
|
||||
transferred: number;
|
||||
};
|
||||
time?: {
|
||||
elapsed: number;
|
||||
remaining: number;
|
||||
};
|
||||
success?: boolean | undefined;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
const options: cors.CorsOptions = { origin: "*" };
|
||||
const requiredModules: Record<string, any> = {};
|
||||
const port = process.env.PORT || 4000;
|
||||
const dataDir = __dirname;
|
||||
type DownloadProgress = Record<string, ProgressState>;
|
||||
const downloadProgress: DownloadProgress = {};
|
||||
const app: Express = express()
|
||||
app.use(express.static(dataDir + '/renderer'))
|
||||
app.use(cors(options))
|
||||
app.use(express.json());
|
||||
|
||||
/**
|
||||
* Execute a plugin module function via API call
|
||||
*
|
||||
* @param modulePath path to module name to import
|
||||
* @param method function name to execute. The methods "deleteFile" and "downloadFile" will call the server function {@link deleteFile}, {@link downloadFile} instead of the plugin function.
|
||||
* @param args arguments to pass to the function
|
||||
* @returns Promise<any>
|
||||
*
|
||||
*/
|
||||
app.post('/api/v1/invokeFunction', (req: Request, res: Response, next: NextFunction): void => {
|
||||
const method = req.body["method"];
|
||||
const args = req.body["args"];
|
||||
switch (method) {
|
||||
case "deleteFile":
|
||||
deleteFile(args).then(() => res.json(Object())).catch((err: any) => next(err));
|
||||
break;
|
||||
case "downloadFile":
|
||||
downloadFile(args.downloadUrl, args.fileName).then(() => res.json(Object())).catch((err: any) => next(err));
|
||||
break;
|
||||
default:
|
||||
const result = invokeFunction(req.body["modulePath"], method, args)
|
||||
if (typeof result === "undefined") {
|
||||
res.json(Object())
|
||||
} else {
|
||||
result?.then((result: any) => {
|
||||
res.json(result)
|
||||
}).catch((err: any) => next(err));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/v1/downloadProgress', (req: Request, res: Response): void => {
|
||||
const fileName = req.body["fileName"];
|
||||
if (fileName && downloadProgress[fileName]) {
|
||||
res.json(downloadProgress[fileName])
|
||||
return;
|
||||
} else {
|
||||
const obj = downloadingFile();
|
||||
if (obj) {
|
||||
res.json(obj)
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.json(Object());
|
||||
});
|
||||
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction): void => {
|
||||
console.error("ErrorHandler", req.url, req.body, err);
|
||||
res.status(500);
|
||||
res.json({ error: err?.message ?? "Internal Server Error" })
|
||||
});
|
||||
|
||||
app.listen(port, () => console.log(`Application is running on port ${port}`));
|
||||
|
||||
|
||||
async function invokeFunction(modulePath: string, method: string, args: any): Promise<any> {
|
||||
console.log(modulePath, method, args);
|
||||
const module = require(/* webpackIgnore: true */ path.join(
|
||||
dataDir,
|
||||
"",
|
||||
modulePath
|
||||
));
|
||||
requiredModules[modulePath] = module;
|
||||
if (typeof module[method] === "function") {
|
||||
return module[method](...args);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function downloadModel(downloadUrl: string, fileName: string): void {
|
||||
const userDataPath = appPath();
|
||||
const destination = resolve(userDataPath, fileName);
|
||||
console.log("Download file", fileName, "to", destination);
|
||||
progress(request(downloadUrl), {})
|
||||
.on("progress", function (state: any) {
|
||||
downloadProgress[fileName] = {
|
||||
...state,
|
||||
fileName,
|
||||
success: undefined
|
||||
};
|
||||
console.log("downloading file", fileName, (state.percent * 100).toFixed(2) + '%');
|
||||
})
|
||||
.on("error", function (err: Error) {
|
||||
downloadProgress[fileName] = {
|
||||
...downloadProgress[fileName],
|
||||
success: false,
|
||||
fileName: fileName,
|
||||
};
|
||||
})
|
||||
.on("end", function () {
|
||||
downloadProgress[fileName] = {
|
||||
success: true,
|
||||
fileName: fileName,
|
||||
};
|
||||
})
|
||||
.pipe(fs.createWriteStream(destination));
|
||||
}
|
||||
|
||||
function deleteFile(filePath: string): Promise<void> {
|
||||
const userDataPath = appPath();
|
||||
const fullPath = resolve(userDataPath, filePath);
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.unlink(fullPath, function (err: any) {
|
||||
if (err && err.code === "ENOENT") {
|
||||
reject(Error(`File does not exist: ${err}`));
|
||||
} else if (err) {
|
||||
reject(Error(`File delete error: ${err}`));
|
||||
} else {
|
||||
console.log(`Delete file ${filePath} from ${fullPath}`)
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function downloadingFile(): ProgressState | undefined {
|
||||
const obj = Object.values(downloadProgress).find(obj => obj && typeof obj.success === "undefined")
|
||||
return obj
|
||||
}
|
||||
|
||||
|
||||
async function downloadFile(downloadUrl: string, fileName: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const obj = downloadingFile();
|
||||
if (obj) {
|
||||
reject(Error(obj.fileName + " is being downloaded!"))
|
||||
return;
|
||||
};
|
||||
(async () => {
|
||||
downloadModel(downloadUrl, fileName);
|
||||
})().catch(e => {
|
||||
console.error("downloadModel", fileName, e);
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function appPath(): string {
|
||||
return process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share")
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"watch": [
|
||||
"main.ts"
|
||||
]
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"electron": "^26.2.1",
|
||||
"express": "^4.18.2",
|
||||
"request": "^2.88.2",
|
||||
"request-progress": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.14",
|
||||
"@types/express": "^4.17.18",
|
||||
"@types/node": "^20.8.2",
|
||||
"nodemon": "^3.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project ./",
|
||||
"dev": "nodemon main.ts",
|
||||
"prod": "node build/main.js"
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"outDir": "./build",
|
||||
"rootDir": "./",
|
||||
"noEmitOnError": true,
|
||||
"baseUrl": ".",
|
||||
"allowJs": true,
|
||||
"paths": { "*": ["node_modules/*"] },
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["core", "build", "dist", "tests"]
|
||||
}
|
||||
@ -2,18 +2,10 @@ import { PropsWithChildren } from 'react'
|
||||
|
||||
import { Metadata } from 'next'
|
||||
|
||||
// import { Inter } from 'next/font/google'
|
||||
|
||||
import Providers from '@/containers/Providers'
|
||||
|
||||
import '@/styles/main.scss'
|
||||
|
||||
// const inter = Inter({
|
||||
// subsets: ['latin'],
|
||||
// display: 'swap',
|
||||
// variable: '--font-inter',
|
||||
// })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Jan',
|
||||
description:
|
||||
|
||||
@ -24,6 +24,7 @@ import {
|
||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||
import { MessageStatus, toChatMessage } from '@/models/ChatMessage'
|
||||
import { pluginManager } from '@/plugin'
|
||||
import { ChatMessage, Conversation } from '@/types/chatMessage'
|
||||
|
||||
let currentConversation: Conversation | undefined = undefined
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { atom } from 'jotai'
|
||||
|
||||
import { getActiveConvoIdAtom } from './Conversation.atom'
|
||||
|
||||
import { MessageStatus } from '@/models/ChatMessage'
|
||||
import { ChatMessage, MessageStatus } from '@/models/ChatMessage'
|
||||
|
||||
/**
|
||||
* Stores all chat messages for all conversations
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Conversation, ConversationState } from '@/types/chatMessage'
|
||||
import { atom } from 'jotai'
|
||||
|
||||
// import { MainViewState, setMainViewStateAtom } from './MainView.atom'
|
||||
|
||||
@ -45,14 +45,14 @@ export function useActiveModel() {
|
||||
|
||||
const res = await initModel(`models/${modelId}`)
|
||||
if (res?.error) {
|
||||
const errorMessage = `Failed to init model: ${res.error}`
|
||||
console.error(errorMessage)
|
||||
const errorMessage = `${res.error}`
|
||||
alert(errorMessage)
|
||||
setStateModel(() => ({
|
||||
state: 'start',
|
||||
loading: false,
|
||||
model: modelId,
|
||||
}))
|
||||
setActiveModel(undefined)
|
||||
} else {
|
||||
console.debug(
|
||||
`Init model ${modelId} successfully!, take ${
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
addNewConversationStateAtom,
|
||||
} from '@/helpers/atoms/Conversation.atom'
|
||||
import { pluginManager } from '@/plugin'
|
||||
import { Conversation } from '@/types/chatMessage'
|
||||
|
||||
export const useCreateConversation = () => {
|
||||
const [userConversations, setUserConversations] = useAtom(
|
||||
|
||||
@ -7,6 +7,7 @@ import { useActiveModel } from './useActiveModel'
|
||||
import { useGetDownloadedModels } from './useGetDownloadedModels'
|
||||
|
||||
import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
|
||||
import { Conversation } from '@/types/chatMessage'
|
||||
|
||||
export default function useGetInputState() {
|
||||
const [inputState, setInputState] = useState<InputType>('loading')
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from '@/helpers/atoms/Conversation.atom'
|
||||
import { toChatMessage } from '@/models/ChatMessage'
|
||||
import { pluginManager } from '@/plugin/PluginManager'
|
||||
import { ChatMessage, ConversationState } from '@/types/chatMessage'
|
||||
|
||||
const useGetUserConversations = () => {
|
||||
const setConversationStates = useSetAtom(conversationStatesAtom)
|
||||
|
||||
@ -12,9 +12,7 @@ import { Message } from '@janhq/core/lib/types'
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
|
||||
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
||||
|
||||
import { generateMessageId } from '@/utils/message'
|
||||
|
||||
import { ulid } from 'ulid'
|
||||
import {
|
||||
addNewMessageAtom,
|
||||
getCurrentChatMessagesAtom,
|
||||
@ -24,9 +22,10 @@ import {
|
||||
updateConversationAtom,
|
||||
updateConversationWaitingForResponseAtom,
|
||||
} from '@/helpers/atoms/Conversation.atom'
|
||||
import { toChatMessage } from '@/models/ChatMessage'
|
||||
import { MessageSenderType, toChatMessage } from '@/models/ChatMessage'
|
||||
|
||||
import { pluginManager } from '@/plugin/PluginManager'
|
||||
import { ChatMessage, Conversation } from '@/types/chatMessage'
|
||||
|
||||
export default function useSendChatMessage() {
|
||||
const currentConvo = useAtomValue(currentConversationAtom)
|
||||
@ -73,16 +72,13 @@ export default function useSendChatMessage() {
|
||||
...updatedConv,
|
||||
name: updatedConv.name ?? '',
|
||||
message: updatedConv.lastMessage ?? '',
|
||||
messages: currentMessages.map<Message>((e: ChatMessage) => {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
messages: currentMessages.map<Message>((e: ChatMessage) => ({
|
||||
_id: e.id,
|
||||
message: e.text,
|
||||
user: e.senderUid,
|
||||
updatedAt: new Date(e.createdAt).toISOString(),
|
||||
createdAt: new Date(e.createdAt).toISOString(),
|
||||
}
|
||||
}),
|
||||
})),
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
@ -98,25 +94,23 @@ export default function useSendChatMessage() {
|
||||
|
||||
const prompt = currentPrompt.trim()
|
||||
const messageHistory: MessageHistory[] = currentMessages
|
||||
.map((msg) => {
|
||||
return {
|
||||
role: msg.senderUid === 'user' ? 'user' : 'assistant',
|
||||
.map((msg) => ({
|
||||
role: msg.senderUid,
|
||||
content: msg.text ?? '',
|
||||
}
|
||||
})
|
||||
}))
|
||||
.reverse()
|
||||
.concat([
|
||||
{
|
||||
role: 'user',
|
||||
role: MessageSenderType.User,
|
||||
content: prompt,
|
||||
} as MessageHistory,
|
||||
])
|
||||
const newMessage: NewMessageRequest = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_id: generateMessageId(),
|
||||
_id: ulid(),
|
||||
conversationId: convoId,
|
||||
message: prompt,
|
||||
user: 'user',
|
||||
user: MessageSenderType.User,
|
||||
createdAt: new Date().toISOString(),
|
||||
history: messageHistory,
|
||||
}
|
||||
@ -124,6 +118,11 @@ export default function useSendChatMessage() {
|
||||
const newChatMessage = toChatMessage(newMessage)
|
||||
addNewMessage(newChatMessage)
|
||||
|
||||
// delay randomly from 50 - 100ms
|
||||
// to prevent duplicate message id
|
||||
const delay = Math.floor(Math.random() * 50) + 50
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
|
||||
events.emit(EventName.OnNewMessageRequest, newMessage)
|
||||
if (!currentConvo?.summary && currentConvo) {
|
||||
const updatedConv: Conversation = {
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwindcss": "3.3.5",
|
||||
"typescript": "5.2.2",
|
||||
"ulid": "^2.3.0",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { forwardRef } from 'react'
|
||||
|
||||
import SimpleTextMessage from '../SimpleTextMessage'
|
||||
import { ChatMessage } from '@/types/chatMessage'
|
||||
|
||||
type Props = {
|
||||
message: ChatMessage
|
||||
@ -8,8 +9,7 @@ type Props = {
|
||||
|
||||
type Ref = HTMLDivElement
|
||||
|
||||
const ChatItem = forwardRef<Ref, Props>(({ message }, ref) => {
|
||||
return (
|
||||
const ChatItem = forwardRef<Ref, Props>(({ message }, ref) => (
|
||||
<div ref={ref} className="py-4 even:bg-secondary dark:even:bg-secondary/20">
|
||||
<SimpleTextMessage
|
||||
status={message.status}
|
||||
@ -21,7 +21,6 @@ const ChatItem = forwardRef<Ref, Props>(({ message }, ref) => {
|
||||
text={message.text}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
))
|
||||
|
||||
export default ChatItem
|
||||
|
||||
@ -56,7 +56,7 @@ const ChatScreen = () => {
|
||||
const conversations = useAtomValue(userConversationsAtom)
|
||||
const isEnableChat = (currentConvo && activeModel) || conversations.length > 0
|
||||
const [isModelAvailable, setIsModelAvailable] = useState(
|
||||
downloadedModels.some((x) => x.name !== currentConvo?.name)
|
||||
downloadedModels.some((x) => x._id === currentConvo?.modelId)
|
||||
)
|
||||
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
@ -72,9 +72,8 @@ const ChatScreen = () => {
|
||||
|
||||
useEffect(() => {
|
||||
setIsModelAvailable(
|
||||
downloadedModels.some((x) => x.name !== currentConvo?.name)
|
||||
downloadedModels.some((x) => x._id === currentConvo?.modelId)
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentConvo, downloadedModels])
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
@ -131,10 +130,10 @@ const ChatScreen = () => {
|
||||
<div
|
||||
className={twMerge(
|
||||
'flex items-center space-x-3',
|
||||
isModelAvailable && '-mt-1'
|
||||
!isModelAvailable && '-mt-1'
|
||||
)}
|
||||
>
|
||||
{isModelAvailable && (
|
||||
{!isModelAvailable && (
|
||||
<Button
|
||||
themes="secondary"
|
||||
size="sm"
|
||||
|
||||
@ -64,11 +64,12 @@
|
||||
border-radius: 0.4rem;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
white-space: normal;
|
||||
}
|
||||
pre > code {
|
||||
display: block;
|
||||
text-indent: 0;
|
||||
white-space: inherit;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
|
||||
2
web/types/chatMessage.d.ts
vendored
2
web/types/chatMessage.d.ts
vendored
@ -6,7 +6,7 @@ enum MessageType {
|
||||
Error = 'Error',
|
||||
}
|
||||
|
||||
enum MessageSenderType {
|
||||
export enum MessageSenderType {
|
||||
Ai = 'assistant',
|
||||
User = 'user',
|
||||
}
|
||||
|
||||
@ -25,7 +25,3 @@ export function mergeAndRemoveDuplicates(
|
||||
|
||||
return result.reverse()
|
||||
}
|
||||
|
||||
export const generateMessageId = () => {
|
||||
return `m-${Date.now()}`
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user