feat: move log into monitoring extension (#2662)

This commit is contained in:
Louis 2024-04-10 14:35:15 +07:00 committed by GitHub
parent 3be0b3af65
commit 3f23de6c28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 635 additions and 373 deletions

View File

@ -1,9 +1,12 @@
import { basename, isAbsolute, join, relative } from 'path'
import { Processor } from './Processor'
import { getAppConfigurations as appConfiguration, updateAppConfiguration } from '../../helper'
import { log as writeLog, logServer as writeServerLog } from '../../helper/log'
import { appResourcePath } from '../../helper/path'
import {
log as writeLog,
appResourcePath,
getAppConfigurations as appConfiguration,
updateAppConfiguration,
} from '../../helper'
export class App implements Processor {
observer?: Function
@ -56,13 +59,6 @@ export class App implements Processor {
writeLog(args)
}
/**
* Log message to log file.
*/
logServer(args: any) {
writeServerLog(args)
}
getAppConfigurations() {
return appConfiguration()
}

View File

@ -1,7 +1,11 @@
import fs from 'fs'
import { join } from 'path'
import { getJanDataFolderPath, getJanExtensionsPath, getSystemResourceInfo } from '../../../helper'
import { logServer } from '../../../helper/log'
import {
getJanDataFolderPath,
getJanExtensionsPath,
getSystemResourceInfo,
log,
} from '../../../helper'
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
import { Model, ModelSettingParams, PromptTemplate } from '../../../../types'
import {
@ -69,7 +73,7 @@ const runModel = async (modelId: string, settingParams?: ModelSettingParams): Pr
}),
}
logServer(`[NITRO]::Debug: Nitro model settings: ${JSON.stringify(nitroModelSettings)}`)
log(`[SERVER]::Debug: Nitro model settings: ${JSON.stringify(nitroModelSettings)}`)
// Convert settings.prompt_template to system_prompt, user_prompt, ai_prompt
if (modelMetadata.settings.prompt_template) {
@ -140,7 +144,7 @@ const runNitroAndLoadModel = async (modelId: string, modelSettings: NitroModelSe
}
const spawnNitroProcess = async (): Promise<void> => {
logServer(`[NITRO]::Debug: Spawning Nitro subprocess...`)
log(`[SERVER]::Debug: Spawning Nitro subprocess...`)
let binaryFolder = join(
getJanExtensionsPath(),
@ -155,8 +159,8 @@ const spawnNitroProcess = async (): Promise<void> => {
const args: string[] = ['1', LOCAL_HOST, NITRO_DEFAULT_PORT.toString()]
// Execute the binary
logServer(
`[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}`
log(
`[SERVER]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}`
)
subprocess = spawn(
executableOptions.executablePath,
@ -172,20 +176,20 @@ const spawnNitroProcess = async (): Promise<void> => {
// Handle subprocess output
subprocess.stdout.on('data', (data: any) => {
logServer(`[NITRO]::Debug: ${data}`)
log(`[SERVER]::Debug: ${data}`)
})
subprocess.stderr.on('data', (data: any) => {
logServer(`[NITRO]::Error: ${data}`)
log(`[SERVER]::Error: ${data}`)
})
subprocess.on('close', (code: any) => {
logServer(`[NITRO]::Debug: Nitro exited with code: ${code}`)
log(`[SERVER]::Debug: Nitro exited with code: ${code}`)
subprocess = undefined
})
tcpPortUsed.waitUntilUsed(NITRO_DEFAULT_PORT, 300, 30000).then(() => {
logServer(`[NITRO]::Debug: Nitro is ready`)
log(`[SERVER]::Debug: Nitro is ready`)
})
}
@ -267,7 +271,7 @@ const validateModelStatus = async (): Promise<void> => {
retries: 5,
retryDelay: 500,
}).then(async (res: Response) => {
logServer(`[NITRO]::Debug: Validate model state success with response ${JSON.stringify(res)}`)
log(`[SERVER]::Debug: Validate model state success with response ${JSON.stringify(res)}`)
// If the response is OK, check model_loaded status.
if (res.ok) {
const body = await res.json()
@ -282,7 +286,7 @@ const validateModelStatus = async (): Promise<void> => {
}
const loadLLMModel = async (settings: NitroModelSettings): Promise<Response> => {
logServer(`[NITRO]::Debug: Loading model with params ${JSON.stringify(settings)}`)
log(`[SERVER]::Debug: Loading model with params ${JSON.stringify(settings)}`)
const fetchRT = require('fetch-retry')
const fetchRetry = fetchRT(fetch)
@ -296,11 +300,11 @@ const loadLLMModel = async (settings: NitroModelSettings): Promise<Response> =>
retryDelay: 500,
})
.then((res: any) => {
logServer(`[NITRO]::Debug: Load model success with response ${JSON.stringify(res)}`)
log(`[SERVER]::Debug: Load model success with response ${JSON.stringify(res)}`)
return Promise.resolve(res)
})
.catch((err: any) => {
logServer(`[NITRO]::Error: Load model failed with error ${err}`)
log(`[SERVER]::Error: Load model failed with error ${err}`)
return Promise.reject(err)
})
}
@ -323,7 +327,7 @@ export const stopModel = async (_modelId: string) => {
})
}, 5000)
const tcpPortUsed = require('tcp-port-used')
logServer(`[NITRO]::Debug: Request to kill Nitro`)
log(`[SERVER]::Debug: Request to kill Nitro`)
fetch(NITRO_HTTP_KILL_URL, {
method: 'DELETE',
@ -337,7 +341,7 @@ export const stopModel = async (_modelId: string) => {
// don't need to do anything, we still kill the subprocess
})
.then(() => tcpPortUsed.waitUntilFree(NITRO_DEFAULT_PORT, 300, 5000))
.then(() => logServer(`[NITRO]::Debug: Nitro process is terminated`))
.then(() => log(`[SERVER]::Debug: Nitro process is terminated`))
.then(() =>
resolve({
message: 'Model stopped',

View File

@ -150,31 +150,3 @@ export const getEngineConfiguration = async (engineId: string) => {
full_url: undefined,
}
}
/**
* Utility function to get server log path
*
* @returns {string} The log path.
*/
export const getServerLogPath = (): string => {
const appConfigurations = getAppConfigurations()
const logFolderPath = join(appConfigurations.data_folder, 'logs')
if (!fs.existsSync(logFolderPath)) {
fs.mkdirSync(logFolderPath, { recursive: true })
}
return join(logFolderPath, 'server.log')
}
/**
* Utility function to get app log path
*
* @returns {string} The log path.
*/
export const getAppLogPath = (): string => {
const appConfigurations = getAppConfigurations()
const logFolderPath = join(appConfigurations.data_folder, 'logs')
if (!fs.existsSync(logFolderPath)) {
fs.mkdirSync(logFolderPath, { recursive: true })
}
return join(logFolderPath, 'app.log')
}

View File

@ -1,6 +1,6 @@
export * from './config'
export * from './download'
export * from './log'
export * from './logger'
export * from './module'
export * from './path'
export * from './resource'

View File

@ -1,37 +0,0 @@
import fs from 'fs'
import util from 'util'
import { getAppLogPath, getServerLogPath } from './config'
export const log = (message: string) => {
const path = getAppLogPath()
if (!message.startsWith('[')) {
message = `[APP]::${message}`
}
message = `${new Date().toISOString()} ${message}`
writeLog(message, path)
}
export const logServer = (message: string) => {
const path = getServerLogPath()
if (!message.startsWith('[')) {
message = `[SERVER]::${message}`
}
message = `${new Date().toISOString()} ${message}`
writeLog(message, path)
}
const writeLog = (message: string, logPath: string) => {
if (!fs.existsSync(logPath)) {
fs.writeFileSync(logPath, message)
} else {
const logFile = fs.createWriteStream(logPath, {
flags: 'a',
})
logFile.write(util.format(message) + '\n')
logFile.close()
console.debug(message)
}
}

View File

@ -0,0 +1,81 @@
// Abstract Logger class that all loggers should extend.
export abstract class Logger {
// Each logger must have a unique name.
abstract name: string
/**
* Log message to log file.
* This method should be overridden by subclasses to provide specific logging behavior.
*/
abstract log(args: any): void
}
// LoggerManager is a singleton class that manages all registered loggers.
export class LoggerManager {
// Map of registered loggers, keyed by their names.
public loggers = new Map<string, Logger>()
// Array to store logs that are queued before the loggers are registered.
queuedLogs: any[] = []
// Flag to indicate whether flushLogs is currently running.
private isFlushing = false
// Register a new logger. If a logger with the same name already exists, it will be replaced.
register(logger: Logger) {
this.loggers.set(logger.name, logger)
}
// Unregister a logger by its name.
unregister(name: string) {
this.loggers.delete(name)
}
get(name: string) {
return this.loggers.get(name)
}
// Flush queued logs to all registered loggers.
flushLogs() {
// If flushLogs is already running, do nothing.
if (this.isFlushing) {
return
}
this.isFlushing = true
while (this.queuedLogs.length > 0 && this.loggers.size > 0) {
const log = this.queuedLogs.shift()
this.loggers.forEach((logger) => {
logger.log(log)
})
}
this.isFlushing = false
}
// Log message using all registered loggers.
log(args: any) {
this.queuedLogs.push(args)
this.flushLogs()
}
/**
* The instance of the logger.
* If an instance doesn't exist, it creates a new one.
* This ensures that there is only one LoggerManager instance at any time.
*/
static instance(): LoggerManager {
let instance: LoggerManager | undefined = global.core?.logger
if (!instance) {
instance = new LoggerManager()
if (!global.core) global.core = {}
global.core.logger = instance
}
return instance
}
}
export const log = (...args: any) => {
LoggerManager.instance().log(args)
}

View File

@ -1,11 +1,10 @@
import { SystemResourceInfo } from '../../types'
import { physicalCpuCount } from './config'
import { log } from './log'
import { log } from './logger'
export const getSystemResourceInfo = async (): Promise<SystemResourceInfo> => {
const cpu = await physicalCpuCount()
const message = `[NITRO]::CPU informations - ${cpu}`
log(message)
log(`[NITRO]::CPU informations - ${cpu}`)
return {
numCpuPhysicalCore: cpu,

View File

@ -1,3 +1,5 @@
import { GpuSetting, OperatingSystemInfo } from '../miscellaneous'
/**
* Monitoring extension for system monitoring.
* @extends BaseExtension
@ -14,4 +16,14 @@ export interface MonitoringInterface {
* @returns {Promise<any>} A promise that resolves with the current system load.
*/
getCurrentLoad(): Promise<any>
/**
* Returns the GPU configuration.
*/
getGpuSetting(): Promise<GpuSetting>
/**
* Returns information about the operating system.
*/
getOsInfo(): Promise<OperatingSystemInfo>
}

View File

@ -24,7 +24,6 @@ import { cleanUpAndQuit } from './utils/clean'
import { setupExtensions } from './utils/extension'
import { setupCore } from './utils/setup'
import { setupReactDevTool } from './utils/dev'
import { cleanLogs } from './utils/log'
import { trayManager } from './managers/tray'
import { logSystemInfo } from './utils/system'
@ -75,7 +74,6 @@ app
}
})
})
.then(() => cleanLogs())
app.on('second-instance', (_event, _commandLine, _workingDirectory) => {
windowManager.showMainWindow()
@ -111,7 +109,6 @@ function createMainWindow() {
windowManager.createMainWindow(preloadPath, startUrl)
}
/**
* Handles various IPC messages from the renderer process.
*/

View File

@ -1,67 +0,0 @@
import { getJanDataFolderPath } from '@janhq/core/node'
import * as fs from 'fs'
import * as path from 'path'
export function cleanLogs(
maxFileSizeBytes?: number | undefined,
daysToKeep?: number | undefined,
delayMs?: number | undefined
): void {
const size = maxFileSizeBytes ?? 1 * 1024 * 1024 // 1 MB
const days = daysToKeep ?? 7 // 7 days
const delays = delayMs ?? 10000 // 10 seconds
const logDirectory = path.join(getJanDataFolderPath(), 'logs')
// Perform log cleaning
const currentDate = new Date()
fs.readdir(logDirectory, (err, files) => {
if (err) {
console.error('Error reading log directory:', err)
return
}
files.forEach((file) => {
const filePath = path.join(logDirectory, file)
fs.stat(filePath, (err, stats) => {
if (err) {
console.error('Error getting file stats:', err)
return
}
// Check size
if (stats.size > size) {
fs.unlink(filePath, (err) => {
if (err) {
console.error('Error deleting log file:', err)
return
}
console.debug(
`Deleted log file due to exceeding size limit: ${filePath}`
)
})
} else {
// Check age
const creationDate = new Date(stats.ctime)
const daysDifference = Math.floor(
(currentDate.getTime() - creationDate.getTime()) /
(1000 * 3600 * 24)
)
if (daysDifference > days) {
fs.unlink(filePath, (err) => {
if (err) {
console.error('Error deleting log file:', err)
return
}
console.debug(`Deleted old log file: ${filePath}`)
})
}
}
})
})
})
// Schedule the next execution with doubled delays
setTimeout(() => {
cleanLogs(maxFileSizeBytes, daysToKeep, delays * 2)
}, delays)
}

View File

@ -1,14 +1,10 @@
# Jan Assistant plugin
# Create a Jan Extension using Typescript
Created using Jan app example
Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀
# Create a Jan Plugin using Typescript
## Create Your Own Extension
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
## Create Your Own Plugin
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
To create your own extension, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension.
> [!NOTE]
>
@ -43,35 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne
1. :white_check_mark: Check your artifact
There will be a tgz file in your plugin directory now
There will be a tgz file in your extension directory now
## Update the Plugin Metadata
## Update the Extension Metadata
The [`package.json`](package.json) file defines metadata about your plugin, such as
plugin name, main entry, description and version.
The [`package.json`](package.json) file defines metadata about your extension, such as
extension name, main entry, description and version.
When you copy this repository, update `package.json` with the name, description for your plugin.
When you copy this repository, update `package.json` with the name, description for your extension.
## Update the Plugin Code
## Update the Extension Code
The [`src/`](./src/) directory is the heart of your plugin! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace the
The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your extension functions are invoked. You can replace the
contents of this directory with your own code.
There are a few things to keep in mind when writing your plugin code:
There are a few things to keep in mind when writing your extension code:
- Most Jan Plugin Extension functions are processed asynchronously.
- Most Jan Extension functions are processed asynchronously.
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript
import { core } from "@janhq/core";
import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, "run", 0);
return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
}
```
For more information about the Jan Plugin Core module, see the
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your plugin!
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -1,10 +1,10 @@
# Create a Jan Plugin using Typescript
# Create a Jan Extension using Typescript
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀
## Create Your Own Plugin
## Create Your Own Extension
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
To create your own extension, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
@ -14,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension.
> [!NOTE]
>
@ -39,35 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne
1. :white_check_mark: Check your artifact
There will be a tgz file in your plugin directory now
There will be a tgz file in your extension directory now
## Update the Plugin Metadata
## Update the Extension Metadata
The [`package.json`](package.json) file defines metadata about your plugin, such as
plugin name, main entry, description and version.
The [`package.json`](package.json) file defines metadata about your extension, such as
extension name, main entry, description and version.
When you copy this repository, update `package.json` with the name, description for your plugin.
When you copy this repository, update `package.json` with the name, description for your extension.
## Update the Plugin Code
## Update the Extension Code
The [`src/`](./src/) directory is the heart of your plugin! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace the
The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your extension functions are invoked. You can replace the
contents of this directory with your own code.
There are a few things to keep in mind when writing your plugin code:
There are a few things to keep in mind when writing your extension code:
- Most Jan Plugin Extension functions are processed asynchronously.
- Most Jan Extension functions are processed asynchronously.
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript
import { core } from "@janhq/core";
import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, "run", 0);
return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
}
```
For more information about the Jan Plugin Core module, see the
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your plugin!
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -1,14 +1,10 @@
# Jan inference plugin
# Create a Jan Extension using Typescript
Created using Jan app example
Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀
# Create a Jan Plugin using Typescript
## Create Your Own Extension
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
## Create Your Own Plugin
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
To create your own extension, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension.
> [!NOTE]
>
@ -43,36 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne
1. :white_check_mark: Check your artifact
There will be a tgz file in your plugin directory now
There will be a tgz file in your extension directory now
## Update the Plugin Metadata
## Update the Extension Metadata
The [`package.json`](package.json) file defines metadata about your plugin, such as
plugin name, main entry, description and version.
The [`package.json`](package.json) file defines metadata about your extension, such as
extension name, main entry, description and version.
When you copy this repository, update `package.json` with the name, description for your plugin.
When you copy this repository, update `package.json` with the name, description for your extension.
## Update the Plugin Code
## Update the Extension Code
The [`src/`](./src/) directory is the heart of your plugin! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace the
The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your extension functions are invoked. You can replace the
contents of this directory with your own code.
There are a few things to keep in mind when writing your plugin code:
There are a few things to keep in mind when writing your extension code:
- Most Jan Plugin Extension functions are processed asynchronously.
- Most Jan Extension functions are processed asynchronously.
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript
import { core } from "@janhq/core";
import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, "run", 0);
return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
}
```
For more information about the Jan Plugin Core module, see the
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your plugin!
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -1,14 +1,10 @@
# Jan inference plugin
# Create a Jan Extension using Typescript
Created using Jan app example
Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀
# Create a Jan Plugin using Typescript
## Create Your Own Extension
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
## Create Your Own Plugin
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
To create your own extension, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension.
> [!NOTE]
>
@ -43,35 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne
1. :white_check_mark: Check your artifact
There will be a tgz file in your plugin directory now
There will be a tgz file in your extension directory now
## Update the Plugin Metadata
## Update the Extension Metadata
The [`package.json`](package.json) file defines metadata about your plugin, such as
plugin name, main entry, description and version.
The [`package.json`](package.json) file defines metadata about your extension, such as
extension name, main entry, description and version.
When you copy this repository, update `package.json` with the name, description for your plugin.
When you copy this repository, update `package.json` with the name, description for your extension.
## Update the Plugin Code
## Update the Extension Code
The [`src/`](./src/) directory is the heart of your plugin! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace the
The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your extension functions are invoked. You can replace the
contents of this directory with your own code.
There are a few things to keep in mind when writing your plugin code:
There are a few things to keep in mind when writing your extension code:
- Most Jan Plugin Extension functions are processed asynchronously.
- Most Jan Extension functions are processed asynchronously.
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript
import { core } from '@janhq/core'
import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, 'run', 0)
return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
}
```
For more information about the Jan Plugin Core module, see the
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your plugin!
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -1,14 +1,10 @@
# Jan inference plugin
# Create a Jan Extension using Typescript
Created using Jan app example
Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀
# Create a Jan Plugin using Typescript
## Create Your Own Extension
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
## Create Your Own Plugin
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
To create your own extension, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension.
> [!NOTE]
>
@ -43,36 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne
1. :white_check_mark: Check your artifact
There will be a tgz file in your plugin directory now
There will be a tgz file in your extension directory now
## Update the Plugin Metadata
## Update the Extension Metadata
The [`package.json`](package.json) file defines metadata about your plugin, such as
plugin name, main entry, description and version.
The [`package.json`](package.json) file defines metadata about your extension, such as
extension name, main entry, description and version.
When you copy this repository, update `package.json` with the name, description for your plugin.
When you copy this repository, update `package.json` with the name, description for your extension.
## Update the Plugin Code
## Update the Extension Code
The [`src/`](./src/) directory is the heart of your plugin! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace the
The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your extension functions are invoked. You can replace the
contents of this directory with your own code.
There are a few things to keep in mind when writing your plugin code:
There are a few things to keep in mind when writing your extension code:
- Most Jan Plugin Extension functions are processed asynchronously.
- Most Jan Extension functions are processed asynchronously.
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript
import { core } from "@janhq/core";
import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, "run", 0);
return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
}
```
For more information about the Jan Plugin Core module, see the
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your plugin!
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -1,14 +1,10 @@
# Jan Model Management plugin
# Create a Jan Extension using Typescript
Created using Jan app example
Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀
# Create a Jan Plugin using Typescript
## Create Your Own Extension
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
## Create Your Own Plugin
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
To create your own extension, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension.
> [!NOTE]
>
@ -43,36 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne
1. :white_check_mark: Check your artifact
There will be a tgz file in your plugin directory now
There will be a tgz file in your extension directory now
## Update the Plugin Metadata
## Update the Extension Metadata
The [`package.json`](package.json) file defines metadata about your plugin, such as
plugin name, main entry, description and version.
The [`package.json`](package.json) file defines metadata about your extension, such as
extension name, main entry, description and version.
When you copy this repository, update `package.json` with the name, description for your plugin.
When you copy this repository, update `package.json` with the name, description for your extension.
## Update the Plugin Code
## Update the Extension Code
The [`src/`](./src/) directory is the heart of your plugin! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace the
The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your extension functions are invoked. You can replace the
contents of this directory with your own code.
There are a few things to keep in mind when writing your plugin code:
There are a few things to keep in mind when writing your extension code:
- Most Jan Plugin Extension functions are processed asynchronously.
- Most Jan Extension functions are processed asynchronously.
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript
import { core } from "@janhq/core";
import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, "run", 0);
return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
}
```
For more information about the Jan Plugin Core module, see the
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your plugin!
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -1,14 +1,10 @@
# Jan Monitoring plugin
# Create a Jan Extension using Typescript
Created using Jan app example
Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀
# Create a Jan Plugin using Typescript
## Create Your Own Extension
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
## Create Your Own Plugin
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
To create your own extension, you can use this repository as a template! Just follow the below instructions:
1. Click the Use this template button at the top of the repository
2. Select Create a new repository
@ -18,7 +14,7 @@ To create your own plugin, you can use this repository as a template! Just follo
## Initial Setup
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension.
> [!NOTE]
>
@ -43,36 +39,37 @@ After you've cloned the repository to your local machine or codespace, you'll ne
1. :white_check_mark: Check your artifact
There will be a tgz file in your plugin directory now
There will be a tgz file in your extension directory now
## Update the Plugin Metadata
## Update the Extension Metadata
The [`package.json`](package.json) file defines metadata about your plugin, such as
plugin name, main entry, description and version.
The [`package.json`](package.json) file defines metadata about your extension, such as
extension name, main entry, description and version.
When you copy this repository, update `package.json` with the name, description for your plugin.
When you copy this repository, update `package.json` with the name, description for your extension.
## Update the Plugin Code
## Update the Extension Code
The [`src/`](./src/) directory is the heart of your plugin! This contains the
source code that will be run when your plugin extension functions are invoked. You can replace the
The [`src/`](./src/) directory is the heart of your extension! This contains the
source code that will be run when your extension functions are invoked. You can replace the
contents of this directory with your own code.
There are a few things to keep in mind when writing your plugin code:
There are a few things to keep in mind when writing your extension code:
- Most Jan Plugin Extension functions are processed asynchronously.
- Most Jan Extension functions are processed asynchronously.
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
```typescript
import { core } from "@janhq/core";
import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, "run", 0);
return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.inference(data)
)
}
```
For more information about the Jan Plugin Core module, see the
For more information about the Jan Extension Core module, see the
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your plugin!
So, what are you waiting for? Go ahead and start customizing your extension!

View File

@ -0,0 +1,21 @@
[
{
"key": "log-enabled",
"title": "App Logging Enabled",
"description": "We recommend enabling this setting to help us improve the app. Your data will be kept private on your computer, and you can opt out at any time.",
"controllerType": "checkbox",
"controllerProps": {
"value": true
}
},
{
"key": "log-cleaning-interval",
"title": "Log Cleaning Interval",
"description": "Log cleaning interval in milliseconds.",
"controllerType": "input",
"controllerProps": {
"value": "120000",
"placeholder": "Interval in milliseconds. E.g. 120000"
}
}
]

View File

@ -4,6 +4,7 @@ import sourceMaps from 'rollup-plugin-sourcemaps'
import typescript from 'rollup-plugin-typescript2'
import json from '@rollup/plugin-json'
import replace from '@rollup/plugin-replace'
const settingJson = require('./resources/settings.json')
const packageJson = require('./package.json')
export default [
@ -19,6 +20,7 @@ export default [
replace({
preventAssignment: true,
NODE: JSON.stringify(`${packageJson.name}/${packageJson.node}`),
SETTINGS: JSON.stringify(settingJson),
}),
// Allow json resolution
json(),

View File

@ -1,27 +1,63 @@
import {
GpuSetting,
MonitoringExtension,
MonitoringInterface,
OperatingSystemInfo,
executeOnMain,
} from '@janhq/core'
declare const SETTINGS: Array<any>
enum Settings {
logEnabled = 'log-enabled',
logCleaningInterval = 'log-cleaning-interval',
}
/**
* JanMonitoringExtension is a extension that provides system monitoring functionality.
* It implements the MonitoringExtension interface from the @janhq/core package.
*/
export default class JanMonitoringExtension extends MonitoringExtension {
export default class JanMonitoringExtension
extends MonitoringExtension
implements MonitoringInterface
{
/**
* Called when the extension is loaded.
*/
async onLoad() {
// Register extension settings
this.registerSettings(SETTINGS)
const logEnabled = await this.getSetting<boolean>(Settings.logEnabled, true)
const logCleaningInterval = parseInt(
await this.getSetting<string>(Settings.logCleaningInterval, '120000')
)
// Register File Logger provided by this extension
await executeOnMain(NODE, 'registerLogger', {
logEnabled,
logCleaningInterval: isNaN(logCleaningInterval)
? 120000
: logCleaningInterval,
})
// Attempt to fetch nvidia info
await executeOnMain(NODE, 'updateNvidiaInfo')
}
onSettingUpdate<T>(key: string, value: T): void {
if (key === Settings.logEnabled) {
executeOnMain(NODE, 'updateLogger', { logEnabled: value })
} else if (key === Settings.logCleaningInterval) {
executeOnMain(NODE, 'updateLogger', { logCleaningInterval: value })
}
}
/**
* Called when the extension is unloaded.
*/
onUnload(): void {}
onUnload(): void {
// Register File Logger provided by this extension
executeOnMain(NODE, 'unregisterLogger')
}
/**
* Returns the GPU configuration.
@ -47,6 +83,10 @@ export default class JanMonitoringExtension extends MonitoringExtension {
return executeOnMain(NODE, 'getCurrentLoad')
}
/**
* Returns information about the OS
* @returns
*/
getOsInfo(): Promise<OperatingSystemInfo> {
return executeOnMain(NODE, 'getOsInfo')
}

View File

@ -1,6 +1,7 @@
import {
GpuSetting,
GpuSettingInfo,
LoggerManager,
OperatingSystemInfo,
ResourceInfo,
SupportedPlatforms,
@ -12,6 +13,7 @@ import { exec } from 'child_process'
import { writeFileSync, existsSync, readFileSync, mkdirSync } from 'fs'
import path from 'path'
import os from 'os'
import { FileLogger } from './logger'
/**
* Path to the settings directory
@ -346,3 +348,22 @@ export const getOsInfo = (): OperatingSystemInfo => {
return osInfo
}
export const registerLogger = ({ logEnabled, logCleaningInterval }) => {
const logger = new FileLogger(logEnabled, logCleaningInterval)
LoggerManager.instance().register(logger)
logger.cleanLogs()
}
export const unregisterLogger = () => {
LoggerManager.instance().unregister('file')
}
export const updateLogger = ({ logEnabled, logCleaningInterval }) => {
const logger = LoggerManager.instance().loggers.get('file') as FileLogger
if (logger && logEnabled !== undefined) logger.logEnabled = logEnabled
if (logger && logCleaningInterval)
logger.logCleaningInterval = logCleaningInterval
// Rerun
logger && logger.cleanLogs()
}

View File

@ -0,0 +1,138 @@
import fs from 'fs'
import util from 'util'
import {
getAppConfigurations,
getJanDataFolderPath,
Logger,
} from '@janhq/core/node'
import path, { join } from 'path'
export class FileLogger extends Logger {
name = 'file'
logCleaningInterval: number = 120000
timeout: NodeJS.Timeout | null = null
appLogPath: string = './'
logEnabled: boolean = true
constructor(
logEnabled: boolean = true,
logCleaningInterval: number = 120000
) {
super()
this.logEnabled = logEnabled
if (logCleaningInterval) this.logCleaningInterval = logCleaningInterval
const appConfigurations = getAppConfigurations()
const logFolderPath = join(appConfigurations.data_folder, 'logs')
if (!fs.existsSync(logFolderPath)) {
fs.mkdirSync(logFolderPath, { recursive: true })
}
this.appLogPath = join(logFolderPath, 'app.log')
}
log(args: any) {
if (!this.logEnabled) return
let message = args[0]
const scope = args[1]
if (!message) return
const path = this.appLogPath
if (!scope && !message.startsWith('[')) {
message = `[APP]::${message}`
} else if (scope) {
message = `${scope}::${message}`
}
message = `${new Date().toISOString()} ${message}`
writeLog(message, path)
}
cleanLogs(
maxFileSizeBytes?: number | undefined,
daysToKeep?: number | undefined
): void {
// clear existing timeout
// incase we rerun it with different values
if (this.timeout) clearTimeout(this.timeout)
this.timeout = undefined
if (!this.logEnabled) return
console.log(
'Validating app logs. Next attempt in ',
this.logCleaningInterval
)
const size = maxFileSizeBytes ?? 1 * 1024 * 1024 // 1 MB
const days = daysToKeep ?? 7 // 7 days
const logDirectory = path.join(getJanDataFolderPath(), 'logs')
// Perform log cleaning
const currentDate = new Date()
fs.readdir(logDirectory, (err, files) => {
if (err) {
console.error('Error reading log directory:', err)
return
}
files.forEach((file) => {
const filePath = path.join(logDirectory, file)
fs.stat(filePath, (err, stats) => {
if (err) {
console.error('Error getting file stats:', err)
return
}
// Check size
if (stats.size > size) {
fs.unlink(filePath, (err) => {
if (err) {
console.error('Error deleting log file:', err)
return
}
console.debug(
`Deleted log file due to exceeding size limit: ${filePath}`
)
})
} else {
// Check age
const creationDate = new Date(stats.ctime)
const daysDifference = Math.floor(
(currentDate.getTime() - creationDate.getTime()) /
(1000 * 3600 * 24)
)
if (daysDifference > days) {
fs.unlink(filePath, (err) => {
if (err) {
console.error('Error deleting log file:', err)
return
}
console.debug(`Deleted old log file: ${filePath}`)
})
}
}
})
})
})
// Schedule the next execution with doubled delays
this.timeout = setTimeout(
() => this.cleanLogs(maxFileSizeBytes, daysToKeep),
this.logCleaningInterval
)
}
}
const writeLog = (message: string, logPath: string) => {
if (!fs.existsSync(logPath)) {
fs.writeFileSync(logPath, message)
} else {
const logFile = fs.createWriteStream(logPath, {
flags: 'a',
})
logFile.write(util.format(message) + '\n')
logFile.close()
console.debug(message)
}
}

35
server/helpers/logger.ts Normal file
View File

@ -0,0 +1,35 @@
import { log } from '@janhq/core/node'
import { FastifyBaseLogger } from 'fastify'
import { ChildLoggerOptions } from 'fastify/types/logger'
import pino from 'pino'
export class Logger implements FastifyBaseLogger {
child(
bindings: pino.Bindings,
options?: ChildLoggerOptions | undefined
): FastifyBaseLogger {
return new Logger()
}
level = 'info'
silent = () => {}
info = function (msg: any) {
log(msg)
}
error = function (msg: any) {
log(msg)
}
debug = function (msg: any) {
log(msg)
}
fatal = function (msg: any) {
log(msg)
}
warn = function (msg: any) {
log(msg)
}
trace = function (msg: any) {
log(msg)
}
}

View File

@ -1,13 +1,9 @@
import fastify from 'fastify'
import dotenv from 'dotenv'
import {
getServerLogPath,
v1Router,
logServer,
getJanExtensionsPath,
} from '@janhq/core/node'
import { v1Router, log, getJanExtensionsPath } from '@janhq/core/node'
import { join } from 'path'
import tcpPortUsed from 'tcp-port-used'
import { Logger } from './helpers/logger'
// Load environment variables
dotenv.config()
@ -52,7 +48,7 @@ export const startServer = async (configs?: ServerConfig): Promise<boolean> => {
const inUse = await tcpPortUsed.check(Number(configs.port), configs.host)
if (inUse) {
const errorMessage = `Port ${configs.port} is already in use.`
logServer(errorMessage)
log(errorMessage, '[SERVER]')
throw new Error(errorMessage)
}
}
@ -62,19 +58,15 @@ export const startServer = async (configs?: ServerConfig): Promise<boolean> => {
hostSetting = configs?.host ?? JAN_API_HOST
portSetting = configs?.port ?? JAN_API_PORT
corsEnabled = configs?.isCorsEnabled ?? true
const serverLogPath = getServerLogPath()
// Start the server
try {
// Log server start
if (isVerbose) logServer(`Debug: Starting JAN API server...`)
if (isVerbose) log(`Debug: Starting JAN API server...`, '[SERVER]')
// Initialize Fastify server with logging
server = fastify({
logger: {
level: 'info',
file: serverLogPath,
},
logger: new Logger(),
})
// Register CORS if enabled
@ -134,14 +126,15 @@ export const startServer = async (configs?: ServerConfig): Promise<boolean> => {
.then(() => {
// Log server listening
if (isVerbose)
logServer(
`Debug: JAN API listening at: http://${hostSetting}:${portSetting}`
log(
`Debug: JAN API listening at: http://${hostSetting}:${portSetting}`,
'[SERVER]'
)
})
return true
} catch (e) {
// Log any errors
if (isVerbose) logServer(`Error: ${e}`)
if (isVerbose) log(`Error: ${e}`, '[SERVER]')
}
return false
}
@ -152,11 +145,11 @@ export const startServer = async (configs?: ServerConfig): Promise<boolean> => {
export const stopServer = async () => {
try {
// Log server stop
if (isVerbose) logServer(`Debug: Server stopped`)
if (isVerbose) log(`Debug: Server stopped`, '[SERVER]')
// Stop the server
await server?.close()
} catch (e) {
// Log any errors
if (isVerbose) logServer(`Error: ${e}`)
if (isVerbose) log(`Error: ${e}`, '[SERVER]')
}
}

View File

@ -28,9 +28,11 @@ const ServerLogs = (props: ServerLogsProps) => {
const updateLogs = useCallback(
() =>
getLogs('server').then((log) => {
getLogs('app').then((log) => {
if (typeof log?.split === 'function') {
setLogs(log.split(/\r?\n|\r|\n/g))
setLogs(
log.split(/\r?\n|\r|\n/g).filter((e) => e.includes('[SERVER]::'))
)
}
}),
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -19,7 +19,8 @@ export class ExtensionManager {
* @param extension - The extension to register.
*/
register<T extends BaseExtension>(name: string, extension: T) {
this.extensions.set(extension.type() ?? name, extension)
// Register for naming use
this.extensions.set(name, extension)
// Register AI Engines
if ('provider' in extension && typeof extension.provider === 'string') {
@ -35,10 +36,26 @@ export class ExtensionManager {
* @param type - The type of the extension to retrieve.
* @returns The extension, if found.
*/
get<T extends BaseExtension>(
type: ExtensionTypeEnum | string
): T | undefined {
return this.extensions.get(type) as T | undefined
get<T extends BaseExtension>(type: ExtensionTypeEnum): T | undefined {
return this.getAll().findLast((e) => e.type() === type) as T | undefined
}
/**
* Retrieves a extension by its type.
* @param type - The type of the extension to retrieve.
* @returns The extension, if found.
*/
getByName(name: string): BaseExtension | undefined {
return this.extensions.get(name) as BaseExtension | undefined
}
/**
* Retrieves a extension by its type.
* @param type - The type of the extension to retrieve.
* @returns The extension, if found.
*/
getAll(): BaseExtension[] {
return Array.from(this.extensions.values())
}
/**

View File

@ -25,12 +25,12 @@ export const useLogs = () => {
)
const openServerLog = useCallback(async () => {
const fullPath = await joinPath([janDataFolderPath, 'logs', 'server.log'])
const fullPath = await joinPath([janDataFolderPath, 'logs', 'app.log'])
return openFileExplorer(fullPath)
}, [janDataFolderPath])
const clearServerLog = useCallback(async () => {
await fs.writeFileSync(await joinPath(['file://logs', 'server.log']), '')
await fs.writeFileSync(await joinPath(['file://logs', 'app.log']), '')
}, [])
return { getLogs, openServerLog, clearServerLog }

View File

@ -79,7 +79,7 @@ const TensorRtExtensionItem: React.FC<Props> = ({ item }) => {
useEffect(() => {
const getExtensionInstallationState = async () => {
const extension = extensionManager.get(item.name ?? '')
const extension = extensionManager.getByName(item.name ?? '')
if (!extension) return
if (typeof extension?.installationState === 'function') {
@ -92,13 +92,13 @@ const TensorRtExtensionItem: React.FC<Props> = ({ item }) => {
}, [item.name, isInstalling])
useEffect(() => {
const extension = extensionManager.get(item.name ?? '')
const extension = extensionManager.getByName(item.name ?? '')
if (!extension) return
setCompatibility(extension.compatibility())
}, [setCompatibility, item.name])
const onInstallClick = useCallback(async () => {
const extension = extensionManager.get(item.name ?? '')
const extension = extensionManager.getByName(item.name ?? '')
if (!extension) return
await extension.install()

View File

@ -17,13 +17,12 @@ const ExtensionSetting: React.FC = () => {
const getExtensionSettings = async () => {
if (!selectedExtensionName) return
const allSettings: SettingComponentProps[] = []
const baseExtension = extensionManager.get(selectedExtensionName)
const baseExtension = extensionManager.getByName(selectedExtensionName)
if (!baseExtension) return
if (typeof baseExtension.getSettings === 'function') {
const setting = await baseExtension.getSettings()
if (setting) allSettings.push(...setting)
}
setSettings(allSettings)
}
getExtensionSettings()
@ -40,7 +39,7 @@ const ExtensionSetting: React.FC = () => {
const extensionName = setting.extensionName
if (extensionName) {
extensionManager.get(extensionName)?.updateSettings([setting])
extensionManager.getByName(extensionName)?.updateSettings([setting])
}
return setting

View File

@ -0,0 +1,47 @@
import { CheckboxComponentProps, SettingComponentProps } from '@janhq/core'
import { Switch } from '@janhq/uikit'
import { Marked, Renderer } from 'marked'
type Props = {
settingProps: SettingComponentProps
onValueChanged?: (e: boolean) => void
}
const marked: Marked = new Marked({
renderer: {
link: (href, title, text) => {
return Renderer.prototype.link
?.apply(this, [href, title, text])
.replace('<a', "<a class='text-blue-500' target='_blank'")
},
},
})
const SettingDetailToggleItem: React.FC<Props> = ({
settingProps,
onValueChanged,
}) => {
const { value } = settingProps.controllerProps as CheckboxComponentProps
const description = marked.parse(settingProps.description ?? '', {
async: false,
})
return (
<div className="flex w-full justify-between py-6">
<div className="flex flex-1 flex-col space-y-1">
<h1 className="text-base font-bold">{settingProps.title}</h1>
{
<div
// eslint-disable-next-line @typescript-eslint/naming-convention
dangerouslySetInnerHTML={{ __html: description }}
className="text-sm font-normal text-muted-foreground"
/>
}
</div>
<Switch checked={value} onCheckedChange={onValueChanged} />
</div>
)
}
export default SettingDetailToggleItem

View File

@ -1,6 +1,7 @@
import { SettingComponentProps } from '@janhq/core'
import SettingDetailTextInputItem from './SettingDetailTextInputItem'
import SettingDetailToggleItem from './SettingDetailToggleItem'
type Props = {
componentProps: SettingComponentProps[]
@ -23,6 +24,16 @@ const SettingDetailItem: React.FC<Props> = ({
)
}
case 'checkbox': {
return (
<SettingDetailToggleItem
key={data.key}
settingProps={data}
onValueChanged={(value) => onValueUpdated(data.key, value)}
/>
)
}
default:
return null
}

View File

@ -15,20 +15,13 @@ const SettingMenu: React.FC = () => {
useEffect(() => {
const getAllSettings = async () => {
const activeExtensions = await extensionManager.getActive()
const extensionsMenu: string[] = []
for (const extension of activeExtensions) {
const extensionName = extension.name
if (!extensionName) continue
const baseExtension = extensionManager.get(extensionName)
if (!baseExtension) continue
if (typeof baseExtension.getSettings === 'function') {
const settings = await baseExtension.getSettings()
const extensions = extensionManager.getAll()
for (const extension of extensions) {
if (typeof extension.getSettings === 'function') {
const settings = await extension.getSettings()
if (settings && settings.length > 0) {
extensionsMenu.push(extensionName)
extensionsMenu.push(extension.name ?? extension.url)
}
}
}