feat: Implement local service management
- Add local service start/stop functionality - Integrate local service port discovery and setup - Update DevChatClient to use dynamic port from environment
This commit is contained in:
parent
f707bef5fc
commit
99f7a1d4da
@ -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",
|
||||
|
@ -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: <command list> }
|
||||
messageHandler.registerHandler('regCommandList', getWorkflowCommandList);
|
||||
messageHandler.registerHandler('regCommandList', handleRegCommandList);
|
||||
// Send a message, send the message entered by the user to AI
|
||||
// Response:
|
||||
// { command: 'receiveMessagePartial', text: <response message text>, user: <user>, date: <date> }
|
||||
|
@ -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<void> {
|
||||
existPannel = panel;
|
||||
}
|
||||
|
||||
export async function getWorkflowCommandList(
|
||||
message: any,
|
||||
panel: vscode.WebviewPanel | vscode.WebviewView
|
||||
): Promise<void> {
|
||||
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;
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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<AxiosResponse> {
|
||||
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<AxiosResponse> {
|
||||
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<ChatResponse> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
// src/apiKey.ts
|
||||
|
||||
import DevChat from '@/toolwrapper/devchat';
|
||||
import { UiUtilWrapper } from './uiUtil';
|
||||
import { DevChatConfig } from './config';
|
||||
import { logger } from './logger';
|
||||
|
||||
|
26
src/util/findServicePort.ts
Normal file
26
src/util/findServicePort.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import net from 'net';
|
||||
|
||||
export async function findAvailablePort(): Promise<number> {
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
104
src/util/localService.ts
Normal file
104
src/util/localService.ts
Normal file
@ -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<number> {
|
||||
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<void> {
|
||||
const maxRetries = 30;
|
||||
const retryInterval = 1000; // 1 second
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const response = await new Promise<string>((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<void> {
|
||||
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();
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user