Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion frontend/src/main/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { IpcServerPushChannel } from '@shared/ipc-server-push-channel'

let backendLogFile: string | null = null
let backendProcess: any = null
let backendOwnedByThisProcess = false
let backendPort = 1733 // Dynamic port, starting from 1733
let backendStatus: 'starting' | 'running' | 'stopped' | 'error' = 'stopped' // Backend service status
let ensureBackendRunningPromise: Promise<void> | null = null
Expand Down Expand Up @@ -52,7 +53,7 @@ function isPortAvailable(port: number): Promise<boolean> {
server.once('error', (err: any) => {
// Port is occupied or other errors
if (err.code === 'EADDRINUSE') {
logToBackendFile(`Port ${port} is in use (EADDRINUSE)`)
logToBackendFile(`Port ${port} is already occupied; checking whether it is an existing MineContext backend`)
} else {
logToBackendFile(`Port ${port} check error: ${err.code} - ${err.message}`)
}
Expand Down Expand Up @@ -241,6 +242,11 @@ export function logToBackendFile(message) {
}

export function stopBackendServer() {
if (!backendOwnedByThisProcess) {
logToBackendFile(`Skipping backend stop for reused external service on port ${backendPort}`)
return
}

if (backendProcess) {
logToBackendFile('Stopping backend server...')
setBackendStatus('stopped')
Expand Down Expand Up @@ -270,6 +276,7 @@ export function stopBackendServer() {
}

backendProcess = null
backendOwnedByThisProcess = false
logToBackendFile('Backend server stop signal sent')
}

Expand Down Expand Up @@ -384,6 +391,7 @@ export async function ensureBackendRunning(mainWindow: BrowserWindow) {
const existingBackend = await findRunningBackendPort(1733, 20)
if (existingBackend) {
backendPort = existingBackend.port
backendOwnedByThisProcess = false
setBackendStatus('running')
mainWindow.webContents.send(IpcServerPushChannel.PushGetInitCheckData, existingBackend.healthCheckResult)
logToBackendFile(`Reused existing backend service on port ${backendPort}`)
Expand Down Expand Up @@ -527,6 +535,7 @@ async function startBackendServer(mainWindow: BrowserWindow) {
cwd: backendDir, // Change to backend directory before executing
env: env
})
backendOwnedByThisProcess = true

// On Unix systems, create a new process group
if (process.platform !== 'win32' && backendProcess.pid) {
Expand Down Expand Up @@ -600,6 +609,7 @@ async function startBackendServer(mainWindow: BrowserWindow) {
backendProcess.on('close', (code) => {
logToBackendFile(`Backend process exited with code ${code}`)
setBackendStatus('stopped')
backendOwnedByThisProcess = false
if (code !== 0 && !healthCheckStarted) {
setBackendStatus('error')
reject(new Error(`Backend process exited with code ${code}`))
Expand Down Expand Up @@ -680,6 +690,11 @@ export async function startBackendInBackground(mainWindow: BrowserWindow) {
export function stopBackendServerSync() {
logToBackendFile('Synchronously stopping backend server...')

if (!backendOwnedByThisProcess) {
logToBackendFile(`Skipping sync backend stop for reused external service on port ${backendPort}`)
return
}

if (backendProcess) {
try {
// 立即发送终止信号
Expand Down Expand Up @@ -714,6 +729,7 @@ export function stopBackendServerSync() {
}

backendProcess = null
backendOwnedByThisProcess = false
}

// 立即尝试清理端口
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/main/background/task/screen-monitor-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class ScreenMonitorTask extends ScheduleNextTask {
fetchFn: async () => {
return await this.getVisibleSourcesUseCache()
},
interval: 3 * 1000,
interval: 3,
immediate: true
})
logger.info('ScreenMonitorTask initialized')
Expand Down Expand Up @@ -150,7 +150,10 @@ class ScreenMonitorTask extends ScheduleNextTask {
}
private async startScreenMonitor() {
try {
const visibleSources = this.configCache?.get()
let visibleSources = this.configCache?.get()
if (!visibleSources || visibleSources.length === 0) {
visibleSources = await this.getVisibleSourcesUseCache()
}
logger.debug(
'visibleSources',
visibleSources?.map((item) => pick(item, ['name', 'type', 'isVisible']))
Expand Down
1 change: 1 addition & 0 deletions frontend/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { IpcChannel } from '@shared/IpcChannel'
import { LatestActivityTask } from './background/task/latest-activity'

initLog()
app.setName('MineContext')
const logger = getLogger('MainEntry')

autoUpdater.logger = logger
Expand Down
15 changes: 12 additions & 3 deletions frontend/src/main/services/AppUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ export default class AppUpdater {
private cancellationToken: CancellationToken = new CancellationToken()
constructor(mainWindow: BrowserWindow) {
autoUpdater.logger = logger
// for dev test
autoUpdater.forceDevUpdateConfig = !app.isPackaged
autoUpdater.forceDevUpdateConfig = false

// if (isDev) {
// const devConfigPath = path.join(process.cwd(), 'dev-app-update.yml')
Expand Down Expand Up @@ -47,13 +46,23 @@ export default class AppUpdater {
})
}
public async checkForUpdates() {
if (!app.isPackaged) {
logger.info('Skip update check in development mode')
return {
currentVersion: app.getVersion(),
updateInfo: null
}
}

try {
this.updateCheckResult = await this.autoUpdater.checkForUpdates()
if (this.updateCheckResult?.isUpdateAvailable && !this.autoUpdater.autoDownload) {
// 如果 autoDownload 为 false,则需要再调用下面的函数触发下
// do not use await, because it will block the return of this function
logger.info('downloadUpdate manual by check for updates', this.cancellationToken)
this.autoUpdater.downloadUpdate(this.cancellationToken)
void this.autoUpdater.downloadUpdate(this.cancellationToken).catch((error) => {
logger.error('Failed to download update:', error as Error)
})
}
logger.info(
`update check result: ${this.updateCheckResult?.isUpdateAvailable}, channel: ${this.autoUpdater.channel}, currentVersion: ${this.autoUpdater.currentVersion}`
Expand Down
7 changes: 2 additions & 5 deletions frontend/src/main/services/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { is } from '@electron-toolkit/utils'
import BetterSqlite3, { type Statement, type RunResult, type Database as BetterSqliteDatabase } from 'better-sqlite3'
import path from 'path'
import fs from 'fs'
import { app } from 'electron'
import { getLogger } from '@shared/logger/main'
import { resolveSqliteDbPath } from './dbPath'
const logger = getLogger('Database')
/**
* Represents the result of a query returning a list of items.
Expand Down Expand Up @@ -67,10 +67,7 @@ export class DB {

public static getInstance(dbName: string = 'app.db', dbPath1?: string): DB {
if (!this.instance) {
// It is recommended to place the database file in a fixed location, such as the data folder in the project root directory
const dbPath =
dbPath1 ||
path.join(!app.isPackaged && is.dev ? 'backend' : app.getPath('userData'), 'persist', 'sqlite', dbName)
const dbPath = dbPath1 || resolveSqliteDbPath(dbName)
this.instance = new DB(dbPath)
}
return this.instance
Expand Down
14 changes: 3 additions & 11 deletions frontend/src/main/services/DatabaseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@

// electron/database.ts - SQLite Database Manager (ES Module Compatible)
import Database from 'better-sqlite3'
import path from 'node:path'
import path from 'path'
import fs from 'fs'
import { app } from 'electron'
import { isValidIsoString, toSqliteDatetime } from '../utils/time'
import { is } from '@electron-toolkit/utils'
import { DB } from './Database'
import { TODOActivity } from '@interface/db/todo'
import { getLogger } from '@shared/logger/main'
import { VaultDatabaseService } from './VaultDatabaseService'
import { resolveSqliteDbPath } from './dbPath'
const logger = getLogger('DatabaseManager')
class DatabaseManager extends VaultDatabaseService {
private db: Database.Database | null = null
Expand All @@ -21,14 +20,7 @@ class DatabaseManager extends VaultDatabaseService {

constructor() {
super()
// Dynamically get the application path
// this.dbPath = path.join(app.getPath('userData'), "persist", "sqlite", "app.db")
this.dbPath = path.join(
!app.isPackaged && is.dev ? 'backend' : app.getPath('userData'),
'persist',
'sqlite',
'app.db'
)
this.dbPath = resolveSqliteDbPath()
logger.info('📁 Database path:', this.dbPath)
}

Expand Down
16 changes: 16 additions & 0 deletions frontend/src/main/services/dbPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd.
// SPDX-License-Identifier: Apache-2.0

import { is } from '@electron-toolkit/utils'
import { app } from 'electron'
import path from 'path'

export function resolveSqliteDbPath(dbName = 'app.db'): string {
if (app.isPackaged || !is.dev) {
return path.join(app.getPath('userData'), 'persist', 'sqlite', dbName)
}

const cwd = process.cwd()
const projectRoot = path.basename(cwd) === 'frontend' ? path.dirname(cwd) : cwd
return path.join(projectRoot, 'persist', 'sqlite', dbName)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const AIToggleButton: React.FC<AIToggleButtonProps> = ({ onClick, isActive = fal
icon={
isActive ? (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#000">
<g clip-path="url(#clip0_3_134833)">
<g clipPath="url(#clip0_3_134833)">
<path
d="M9.66699 14C10.0351 14.0002 10.333 14.2989 10.333 14.667C10.3329 15.035 10.035 15.3329 9.66699 15.333H6.33398C5.96576 15.333 5.66716 15.0557 5.66699 14.6875C5.66699 14.3194 5.96494 14.0001 6.33301 14H9.66699ZM7.93457 0.667969C8.38879 0.659525 8.84049 0.702838 9.28027 0.796875C9.65136 0.87624 9.84128 1.27344 9.71387 1.63086C9.58893 1.98099 9.20574 2.16042 8.83984 2.09473C8.55146 2.04286 8.25684 2.01891 7.96094 2.02441C7.18771 2.0388 6.43282 2.25285 5.77539 2.64258C5.11823 3.03225 4.58236 3.5836 4.22266 4.23828C3.86303 4.89288 3.69267 5.62753 3.72754 6.36621C3.76249 7.10507 4.00158 7.82255 4.42188 8.44336C4.84224 9.06432 5.42832 9.56685 6.12012 9.89746C6.35834 10.0114 6.50971 10.2478 6.50977 10.5068V11.9766H9.5791V10.5068C9.57916 10.2471 9.73149 10.01 9.9707 9.89648C10.091 9.83937 10.3049 9.70824 10.5176 9.57031C10.838 9.36248 11.2683 9.40825 11.5234 9.69238C11.7899 9.98913 11.7605 10.4293 11.4229 10.6416C11.2328 10.7611 11.054 10.8643 10.9688 10.9131V12.1182C10.9687 12.4435 10.8343 12.7534 10.5986 12.9805C10.3631 13.2073 10.0457 13.333 9.7168 13.333H6.37207C6.04321 13.333 5.7257 13.2073 5.49023 12.9805C5.25459 12.7534 5.12021 12.4434 5.12012 12.1182V10.9121C4.37593 10.484 3.7404 9.89639 3.26367 9.19238C2.70576 8.36819 2.38552 7.41414 2.33887 6.42969C2.29229 5.44513 2.52102 4.46674 2.99902 3.59668C3.47697 2.7268 4.18696 1.99693 5.05469 1.48242C5.92254 0.967945 6.91722 0.686908 7.93457 0.667969ZM11.2158 2.67773C11.3349 2.21976 11.9844 2.21825 12.1055 2.67578L12.2256 3.13086C12.3946 3.77147 12.8955 4.27235 13.5361 4.44141L13.9902 4.56152C14.4483 4.68242 14.4469 5.33246 13.9883 5.45117L13.542 5.56738C12.8976 5.73416 12.3926 6.2354 12.2227 6.87891L12.1055 7.32422C11.9844 7.78187 11.3348 7.78035 11.2158 7.32227L11.1025 6.88477C10.9349 6.23743 10.4286 5.73201 9.78125 5.56445L9.34473 5.45117C8.88631 5.33233 8.88486 4.68247 9.34277 4.56152L9.78711 4.44336C10.4307 4.2735 10.9328 3.76943 11.0996 3.125L11.2158 2.67773Z"
fill="white"
Expand All @@ -31,7 +31,7 @@ const AIToggleButton: React.FC<AIToggleButtonProps> = ({ onClick, isActive = fal
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<g clip-path="url(#clip0_3_138818)">
<g clipPath="url(#clip0_3_138818)">
<path
d="M9.66699 13.9999C10.0351 14.0001 10.333 14.2988 10.333 14.6669C10.3329 15.0349 10.035 15.3328 9.66699 15.3329H6.33398C5.96576 15.3329 5.66716 15.0556 5.66699 14.6874C5.66699 14.3194 5.96494 14 6.33301 13.9999H9.66699ZM7.93457 0.667908C8.38879 0.659464 8.84049 0.702777 9.28027 0.796814C9.65136 0.876179 9.84128 1.27337 9.71387 1.6308C9.58893 1.98093 9.20574 2.16036 8.83984 2.09467C8.55146 2.0428 8.25684 2.01885 7.96094 2.02435C7.18771 2.03874 6.43282 2.25279 5.77539 2.64252C5.11823 3.03219 4.58236 3.58354 4.22266 4.23822C3.86303 4.89282 3.69267 5.62747 3.72754 6.36615C3.76249 7.10501 4.00158 7.82249 4.42188 8.4433C4.84224 9.06426 5.42832 9.56678 6.12012 9.8974C6.35834 10.0113 6.50971 10.2477 6.50977 10.5068V11.9765H9.5791V10.5068C9.57916 10.247 9.73149 10.0099 9.9707 9.89642C10.091 9.83931 10.3049 9.70818 10.5176 9.57025C10.838 9.36242 11.2683 9.40819 11.5234 9.69232C11.7899 9.98907 11.7605 10.4293 11.4229 10.6415C11.2328 10.761 11.054 10.8642 10.9688 10.913V12.1181C10.9687 12.4434 10.8343 12.7533 10.5986 12.9804C10.3631 13.2073 10.0457 13.3329 9.7168 13.3329H6.37207C6.04321 13.3329 5.7257 13.2072 5.49023 12.9804C5.25459 12.7533 5.12021 12.4434 5.12012 12.1181V10.912C4.37593 10.484 3.7404 9.89633 3.26367 9.19232C2.70576 8.36813 2.38552 7.41408 2.33887 6.42963C2.29229 5.44507 2.52102 4.46667 2.99902 3.59662C3.47697 2.72673 4.18696 1.99687 5.05469 1.48236C5.92254 0.967884 6.91722 0.686847 7.93457 0.667908ZM11.2158 2.67767C11.3349 2.2197 11.9844 2.21819 12.1055 2.67572L12.2256 3.1308C12.3946 3.7714 12.8955 4.27229 13.5361 4.44135L13.9902 4.56146C14.4483 4.68235 14.4469 5.3324 13.9883 5.45111L13.542 5.56732C12.8976 5.73409 12.3926 6.23534 12.2227 6.87885L12.1055 7.32416C11.9844 7.78181 11.3348 7.78029 11.2158 7.3222L11.1025 6.8847C10.9349 6.23737 10.4286 5.73195 9.78125 5.56439L9.34473 5.45111C8.88631 5.33227 8.88486 4.68241 9.34277 4.56146L9.78711 4.4433C10.4307 4.27344 10.9328 3.76937 11.0996 3.12494L11.2158 2.67767Z"
fill="#3F3F51"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const ModelRadio = ({ value, onChange }: ModelRadioProps) => {
<div className="w-[100px] flex items-center justify-between gap-[16px]">
{ModelInfoList?.map((item) => {
return (
<div className="w-10 cursor-pointer">
<div key={item.value} className="w-10 cursor-pointer">
<div
className={`w-10 h-10 rounded-full border flex items-center justify-center overflow-hidden ${item.value === value ? 'border-[#5252FF]' : 'border-gray-300'} ${item.value === value ? 'border-2' : 'border-1'}`}
onClick={() => {
Expand Down
19 changes: 18 additions & 1 deletion frontend/src/renderer/src/services/GlobalEventService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PushDataTypes } from '@renderer/constant/feed'
import { getLogger } from '@shared/logger/renderer'
const logger = getLogger('GlobalEventService')
const NORMAL_POLLING_INTERVAL = 30 * 1000 // Normal: 30 seconds
const MAX_NOTIFICATION_MESSAGE_LENGTH = 180

class GlobalEventService {
private static instance: GlobalEventService
Expand Down Expand Up @@ -84,7 +85,7 @@ class GlobalEventService {
id: `event-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
type: this.mapEventTypeToNotificationType(event.type),
title: this.getEventTitle(event),
message: removeMarkdownSymbols(event.data.title || '有新的事件通知'),
message: this.getEventMessage(event),
timestamp: Date.now(),
source: 'assistant', // Can be set based on the event source
channel: 'in-app',
Expand Down Expand Up @@ -121,6 +122,22 @@ class GlobalEventService {
}
return titleMap[event.type] || '新通知'
}

private getEventMessage(event: any): string {
const rawMessage =
event.type === PushDataTypes.TIP_GENERATED
? event.data?.content || event.data?.title
: event.data?.title || event.data?.content
return this.compactNotificationMessage(rawMessage || '有新的事件通知')
}

private compactNotificationMessage(message: string): string {
const plainText = removeMarkdownSymbols(String(message)).replace(/\s+/g, ' ').trim()
if (plainText.length <= MAX_NOTIFICATION_MESSAGE_LENGTH) {
return plainText
}
return `${plainText.slice(0, MAX_NOTIFICATION_MESSAGE_LENGTH - 1)}…`
}
}

export default GlobalEventService
13 changes: 11 additions & 2 deletions opencontext/config/global_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,11 @@ def _init_prompt_manager(self) -> bool:
return False

try:
self._prompt_manager = PromptManager(absolute_prompts_path)
user_setting_path = config.get("user_setting_path")
user_prompts_dir = os.path.dirname(user_setting_path) if user_setting_path else None
self._prompt_manager = PromptManager(
absolute_prompts_path, user_prompts_dir=user_prompts_dir
)
self._prompt_path = absolute_prompts_path
self._language = language
logger.info(f"Prompts loaded from: {self._prompt_path} (language: {language})")
Expand Down Expand Up @@ -221,7 +225,12 @@ def set_language(self, language: str) -> bool:
logger.error(f"Prompt file not found: {absolute_prompts_path}")
return False

self._prompt_manager = PromptManager(absolute_prompts_path)
config = self._config_manager.get_config() if self._config_manager else {}
user_setting_path = config.get("user_setting_path") if config else None
user_prompts_dir = os.path.dirname(user_setting_path) if user_setting_path else None
self._prompt_manager = PromptManager(
absolute_prompts_path, user_prompts_dir=user_prompts_dir
)
self._prompt_path = absolute_prompts_path
logger.info(f"Prompts reloaded from: {self._prompt_path} (language: {language})")

Expand Down
14 changes: 12 additions & 2 deletions opencontext/config/prompt_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@


class PromptManager:
def __init__(self, prompt_config_path: str = None):
def __init__(self, prompt_config_path: str = None, user_prompts_dir: str = None):
self.prompts = {}
self.prompt_config_path = prompt_config_path
self.user_prompts_dir = user_prompts_dir
if prompt_config_path and os.path.exists(prompt_config_path):
with open(prompt_config_path, "r", encoding="utf-8") as f:
self.prompts = yaml.safe_load(f)
Expand All @@ -37,12 +38,21 @@ def get_prompt(self, name: str, default: str = None) -> str:
return value if isinstance(value, str) else default

def get_prompt_group(self, name: str) -> Dict[str, str]:
prompt_aliases = {
"processing.extraction.screenshot_analyze": (
"processing.extraction.screenshot_contextual_batch"
)
}
keys = name.split(".")
value = self.prompts
for key in keys:
if isinstance(value, dict) and key in value:
value = value[key]
else:
alias = prompt_aliases.get(name)
if alias:
logger.warning(f"Prompt group '{name}' not found, trying alias '{alias}'.")
return self.get_prompt_group(alias)
logger.warning(f"Prompt group '{name}' not found.")
return {}
return value if isinstance(value, dict) else {}
Expand All @@ -68,7 +78,7 @@ def get_user_prompts_path(self) -> str | None:
base_name = os.path.basename(self.prompt_config_path)
if "_" in base_name:
lang = base_name.split("_")[1].split(".")[0]
dir_name = os.path.dirname(self.prompt_config_path)
dir_name = self.user_prompts_dir or os.path.dirname(self.prompt_config_path)
return os.path.join(dir_name, f"user_prompts_{lang}.yaml")
return None

Expand Down
Loading