diff --git a/src/contributes/commands.ts b/src/contributes/commands.ts index 8fe0710..23969a9 100644 --- a/src/contributes/commands.ts +++ b/src/contributes/commands.ts @@ -13,6 +13,8 @@ import { chatWithDevChat } from '../handler/chatHandler'; import { focusDevChatInput } from '../handler/focusHandler'; import { DevChatConfig } from '../util/config'; import { MessageHandler } from "../handler/messageHandler"; +import { startLocalService } from '../util/localService'; +import { logger } from "../util/logger"; const readdir = util.promisify(fs.readdir); const mkdir = util.promisify(fs.mkdir); @@ -222,6 +224,28 @@ export function registerInstallCommandsCommand( } +export function registerStartLocalServiceCommand( + context: vscode.ExtensionContext +) { + let disposable = vscode.commands.registerCommand( + "DevChat.StartLocalService", + async () => { + try { + const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath() ?? ''; + logger.channel()?.debug(`extensionPath: ${context.extensionPath}`); + logger.channel()?.debug(`workspacePath: ${workspaceDir}`); + const port = await startLocalService(context.extensionPath, workspaceDir); + logger.channel()?.debug(`Local service started on port ${port}`); + } catch (error) { + logger.channel()?.error('Failed to start local service:', error); + } + } + ); + + context.subscriptions.push(disposable); +} + + export function registerDevChatChatCommand(context: vscode.ExtensionContext) { let disposable = vscode.commands.registerCommand( "DevChat.Chat", diff --git a/src/handler/handlerRegister.ts b/src/handler/handlerRegister.ts index cdcb1d9..12545cf 100644 --- a/src/handler/handlerRegister.ts +++ b/src/handler/handlerRegister.ts @@ -3,7 +3,7 @@ import { insertCodeBlockToFile } from './codeBlockHandler'; import { replaceCodeBlockToFile } from './codeBlockHandler'; import { doCommit } from './commitHandler'; import { getHistoryMessages } from './historyMessagesHandler'; -import { getWorkflowCommandList } from './workflowCommandHandler'; +import { handleRegCommandList } from './workflowCommandHandler'; import { sendMessage, stopDevChat, regeneration, deleteChatMessage, userInput } from './sendMessage'; import { applyCodeWithDiff } from './diffHandler'; import { addConext } from './contextHandler'; @@ -36,7 +36,7 @@ messageHandler.registerHandler('doCommit', doCommit); messageHandler.registerHandler('historyMessages', getHistoryMessages); // Register the command list // Response: { command: 'regCommandList', result: } -messageHandler.registerHandler('regCommandList', getWorkflowCommandList); +messageHandler.registerHandler('regCommandList', handleRegCommandList); // Send a message, send the message entered by the user to AI // Response: // { command: 'receiveMessagePartial', text: , user: , date: } diff --git a/src/handler/workflowCommandHandler.ts b/src/handler/workflowCommandHandler.ts index 6147477..115caa2 100644 --- a/src/handler/workflowCommandHandler.ts +++ b/src/handler/workflowCommandHandler.ts @@ -12,11 +12,17 @@ regOutMessage({ command: "regCommandList", result: [{ name: "", pattern: "", description: "" }], }); -export async function getWorkflowCommandList( +export async function handleRegCommandList( message: any, panel: vscode.WebviewPanel | vscode.WebviewView ): Promise { existPannel = panel; +} + +export async function getWorkflowCommandList( + message: any, + panel: vscode.WebviewPanel | vscode.WebviewView +): Promise { const dcClient = new DevChatClient(); // All workflows registered in DevChat @@ -34,10 +40,12 @@ export async function getWorkflowCommandList( recommend: recommends.indexOf(workflow.name), })); - MessageHandler.sendMessage(panel, { - command: "regCommandList", - result: commandList, - }); + if (commandList.length > 0) { + MessageHandler.sendMessage(panel, { + command: "regCommandList", + result: commandList, + }); + } return; } diff --git a/src/panel/statusBarView.ts b/src/panel/statusBarView.ts index 9f2ada8..c44d84e 100644 --- a/src/panel/statusBarView.ts +++ b/src/panel/statusBarView.ts @@ -51,6 +51,7 @@ export function createStatusBarItem(context: vscode.ExtensionContext): vscode.St // install devchat workflow commands if (!hasInstallCommands) { hasInstallCommands = true; + await vscode.commands.executeCommand('DevChat.StartLocalService'); await vscode.commands.executeCommand('DevChat.InstallCommands'); // vscode.commands.executeCommand('DevChat.InstallCommandPython'); } diff --git a/src/toolwrapper/devchatClient.ts b/src/toolwrapper/devchatClient.ts index dd4b438..a599ccd 100644 --- a/src/toolwrapper/devchatClient.ts +++ b/src/toolwrapper/devchatClient.ts @@ -8,6 +8,14 @@ import { getFileContent } from "../util/commonUtil"; import { UiUtilWrapper } from "../util/uiUtil"; + +class DCLocalServicePortNotSetError extends Error { + constructor() { + super("DC_LOCALSERVICE_PORT is not set"); + this.name = "DCLocalServicePortNotSetError"; + } +} + function timeThis( target: Object, propertyKey: string, @@ -44,6 +52,11 @@ function catchAndReturn(defaultReturn: any) { try { return await originalMethod.apply(this, args); } catch (error) { + if (error instanceof DCLocalServicePortNotSetError) { + logger.channel()?.warn(`DC_LOCALSERVICE_PORT is not set in [${propertyKey}]`); + return defaultReturn; + } + logger.channel()?.error(`Error in [${propertyKey}]: ${error}`); return defaultReturn; } @@ -126,25 +139,26 @@ export async function buildRoleContextsFromFiles( return contexts; } -// TODO: 在插件启动为每个vscode窗口启动一个devchat local service -// 1. 分配单独的端口号,该窗口的所有请求都通过该端口号发送 (22222仅为作为开发默认端口号,不应用于生产) -// 2. 启动local service时要配置多个worker,以便处理并发请求 -// TODO: 在插件关闭时,关闭其对应的devchat local service - export class DevChatClient { - private baseURL: string; + private baseURL: string | undefined; private _cancelMessageToken: CancelTokenSource | null = null; static readonly logRawDataSizeLimit = 4 * 1024; - // TODO: init devchat client with a port number - // TODO: the default 22222 is for dev only, should not be used in production - constructor(port: number = 22222) { - this.baseURL = `http://localhost:${port}`; + constructor() { } async _get(path: string, config?: any): Promise { + if (!this.baseURL) { + if (!process.env.DC_LOCALSERVICE_PORT) { + logger.channel()?.info("No local service port found."); + throw new DCLocalServicePortNotSetError(); + } + const port: number = parseInt(process.env.DC_LOCALSERVICE_PORT || '8008', 10); + this.baseURL = `http://localhost:${port}`; + } + try { logger.channel()?.debug(`GET request to ${this.baseURL}${path}`); const response = await axios.get(`${this.baseURL}${path}`, config); @@ -155,6 +169,15 @@ export class DevChatClient { } } async _post(path: string, data: any = undefined): Promise { + if (!this.baseURL) { + if (!process.env.DC_LOCALSERVICE_PORT) { + logger.channel()?.info("No local service port found."); + throw new DCLocalServicePortNotSetError(); + } + const port: number = parseInt(process.env.DC_LOCALSERVICE_PORT || '8008', 10); + this.baseURL = `http://localhost:${port}`; + } + try { logger.channel()?.debug(`POST request to ${this.baseURL}${path}`); const response = await axios.post(`${this.baseURL}${path}`, data); @@ -211,6 +234,14 @@ export class DevChatClient { message: ChatRequest, onData: (data: ChatResponse) => void ): Promise { + if (!this.baseURL) { + if (!process.env.DC_LOCALSERVICE_PORT) { + logger.channel()?.info("No local service port found."); + } + const port: number = parseInt(process.env.DC_LOCALSERVICE_PORT || '8008', 10); + this.baseURL = `http://localhost:${port}`; + } + this._cancelMessageToken = axios.CancelToken.source(); const workspace = UiUtilWrapper.workspaceFoldersFirstPath(); // const workspace = undefined; @@ -441,4 +472,4 @@ export class DevChatClient { this.cancelMessage(); // add other requests here if needed } -} +} \ No newline at end of file diff --git a/src/util/apiKey.ts b/src/util/apiKey.ts index eece373..e7a5046 100644 --- a/src/util/apiKey.ts +++ b/src/util/apiKey.ts @@ -1,7 +1,5 @@ // src/apiKey.ts -import DevChat from '@/toolwrapper/devchat'; -import { UiUtilWrapper } from './uiUtil'; import { DevChatConfig } from './config'; import { logger } from './logger'; diff --git a/src/util/findServicePort.ts b/src/util/findServicePort.ts new file mode 100644 index 0000000..a94be06 --- /dev/null +++ b/src/util/findServicePort.ts @@ -0,0 +1,26 @@ +import net from 'net'; + +export async function findAvailablePort(): Promise { + return new Promise((resolve, reject) => { + const server = net.createServer().listen(); + + server.on('listening', () => { + const address = server.address(); + if (typeof address !== 'object' || !address?.port) { + server.close(); + reject(new Error('Failed to get port from server')); + return; + } + server.close(() => resolve(address.port)); + }); + + server.on('error', (err) => { + const errWithCode = err as NodeJS.ErrnoException; + if (errWithCode.code === 'EADDRINUSE') { + reject(new Error('Port already in use')); + } else { + reject(err); + } + }); + }); +} diff --git a/src/util/localService.ts b/src/util/localService.ts new file mode 100644 index 0000000..7f43ec1 --- /dev/null +++ b/src/util/localService.ts @@ -0,0 +1,104 @@ +import { spawn, ChildProcess } from 'child_process'; +import { findAvailablePort } from './findServicePort'; +import * as http from 'http'; +import { logger } from './logger'; +import { DevChatConfig } from './config'; + +let serviceProcess: ChildProcess | null = null; + +export async function startLocalService(extensionPath: string, workspacePath: string): Promise { + if (serviceProcess) { + throw new Error('Local service is already running'); + } + + try { + // 1. 获取可用端口号 + const port = await findAvailablePort(); + + // 2. 设置环境变量 DC_SVC_PORT + process.env.DC_SVC_PORT = port.toString(); + + // 3. 设置 DC_SVC_WORKSPACE 环境变量 + process.env.DC_SVC_WORKSPACE = workspacePath; + + // 新增:设置 PYTHONPATH 环境变量 + process.env.PYTHONPATH = `${extensionPath}/tools/site-packages`; + + // 4. 启动进程 python main.py + const mainPyPath = extensionPath + "/tools/site-packages/devchat/_service/main.py"; + const pythonApp = + DevChatConfig.getInstance().get("python_for_chat") || "python3"; + serviceProcess = spawn(pythonApp, [mainPyPath], { + env: { ...process.env }, + stdio: 'inherit' + }); + + serviceProcess.on('error', (err) => { + logger.channel()?.error('Failed to start local service:', err); + serviceProcess = null; + }); + + serviceProcess.on('exit', (code) => { + logger.channel()?.info(`Local service exited with code ${code}`); + serviceProcess = null; + }); + + // 5. 等待服务启动并验证 + await waitForServiceToStart(port); + + // 6. 服务启动成功后,记录启动的端口号到环境变量 + process.env.DC_LOCALSERVICE_PORT = port.toString(); + logger.channel()?.info(`Local service port recorded: ${port}`); + + return port; + } catch (error) { + logger.channel()?.error('Error starting local service:', error); + throw error; + } +} + +async function waitForServiceToStart(port: number): Promise { + const maxRetries = 30; + const retryInterval = 1000; // 1 second + + for (let i = 0; i < maxRetries; i++) { + try { + const response = await new Promise((resolve, reject) => { + http.get(`http://localhost:${port}/ping`, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => resolve(data)); + }).on('error', reject); + }); + + if (response === '{"message":"pong"}') { + logger.channel()?.info('Local service started successfully'); + return; + } + } catch (error) { + // Ignore errors and continue retrying + } + + await new Promise(resolve => setTimeout(resolve, retryInterval)); + } + + throw new Error('Failed to start local service: timeout'); +} + +export async function stopLocalService(): Promise { + return new Promise((resolve) => { + if (!serviceProcess) { + logger.channel()?.warn('No local service is running'); + resolve(); + return; + } + + serviceProcess.on('exit', () => { + serviceProcess = null; + logger.channel()?.info('Local service stopped'); + resolve(); + }); + + serviceProcess.kill(); + }); +} \ No newline at end of file