Merge pull request #33 from ZYD045692/feature/mcp-hot-update-stdio
feat(service-mcp-connect): 实现stdio连接方式的热更新
This commit is contained in:
commit
7e203905de
4713
renderer/package-lock.json
generated
Normal file
4713
renderer/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -35,7 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, inject, onMounted } from 'vue';
|
import { ref, computed, inject, onMounted, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { type ChatStorage, type EnableToolItem, getToolSchema } from '../chat';
|
import { type ChatStorage, type EnableToolItem, getToolSchema } from '../chat';
|
||||||
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
|
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
|
||||||
@ -79,7 +79,8 @@ const disableAllTools = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
// 更新工具列表的方法
|
||||||
|
const updateToolsList = async () => {
|
||||||
// 将新的 tool 和并进入 tabStorage.settings.enableTools 中
|
// 将新的 tool 和并进入 tabStorage.settings.enableTools 中
|
||||||
// 只需要保证 enable 信息同步即可,其余工具默认开启
|
// 只需要保证 enable 信息同步即可,其余工具默认开启
|
||||||
const disableToolNames = new Set<string>(
|
const disableToolNames = new Set<string>(
|
||||||
@ -91,7 +92,8 @@ onMounted(async () => {
|
|||||||
const newTools: EnableToolItem[] = [];
|
const newTools: EnableToolItem[] = [];
|
||||||
|
|
||||||
for (const client of mcpClientAdapter.clients) {
|
for (const client of mcpClientAdapter.clients) {
|
||||||
const tools = await client.getTools();
|
const tools = await client.getTools({ cache: false });
|
||||||
|
if (tools) {
|
||||||
for (const tool of tools.values()) {
|
for (const tool of tools.values()) {
|
||||||
const enabled = !disableToolNames.has(tool.name);
|
const enabled = !disableToolNames.has(tool.name);
|
||||||
|
|
||||||
@ -103,8 +105,18 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tabStorage.settings.enableTools = newTools;
|
tabStorage.settings.enableTools = newTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await updateToolsList();
|
||||||
|
watch(() => mcpClientAdapter.refreshSignal.value, async () => {
|
||||||
|
await updateToolsList();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -455,19 +455,60 @@ export class McpClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加资源刷新方法,支持超时控制
|
||||||
|
public async refreshAllResources(timeoutMs = 30000): Promise<void> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const signal = controller.signal;
|
||||||
|
|
||||||
|
// 设置超时
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
controller.abort();
|
||||||
|
console.error(`[REFRESH TIMEOUT] Client ${this.clientId}`);
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[REFRESH START] Client ${this.clientId}`);
|
||||||
|
|
||||||
|
// 按顺序刷新资源
|
||||||
|
await this.getTools({ cache: false });
|
||||||
|
await this.getPromptTemplates({ cache: false });
|
||||||
|
await this.getResources({ cache: false });
|
||||||
|
await this.getResourceTemplates({ cache: false });
|
||||||
|
console.log(chalk.gray(`[${new Date().toLocaleString()}]`),
|
||||||
|
chalk.green(`🚀 [${this.name}] REFRESH COMPLETE`));
|
||||||
|
} catch (error) {
|
||||||
|
if (signal.aborted) {
|
||||||
|
throw new Error(`Refresh timed out after ${timeoutMs}ms`);
|
||||||
|
}
|
||||||
|
console.error(`[REFRESH ERROR] Client ${this.clientId}:`, error);
|
||||||
|
console.error(
|
||||||
|
chalk.gray(`[${new Date().toLocaleString()}]`),
|
||||||
|
chalk.red(`🚀 [${this.name}] REFRESH FAILED`),
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class McpClientAdapter {
|
class McpClientAdapter {
|
||||||
public clients: Reactive<McpClient[]> = [];
|
public clients: Reactive<McpClient[]> = [];
|
||||||
public currentClientIndex: number = 0;
|
public currentClientIndex: number = 0;
|
||||||
|
public refreshSignal = reactive({ value: 0 });
|
||||||
|
|
||||||
private defaultClient: McpClient = new McpClient();
|
private defaultClient: McpClient = new McpClient();
|
||||||
public connectLogListenerCancel: (() => void) | null = null;
|
public connectLogListenerCancel: (() => void) | null = null;
|
||||||
|
public connectrefreshListener: (() => void) | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public platform: string
|
public platform: string
|
||||||
) { }
|
) {
|
||||||
|
this.addConnectRefreshListener();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 获取连接参数签名
|
* @description 获取连接参数签名
|
||||||
@ -511,6 +552,43 @@ class McpClientAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private findClientIndexByUuid(uuid: string): number {
|
||||||
|
// 检查客户端数组是否存在且不为空
|
||||||
|
if (!this.clients || this.clients.length === 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.clients.findIndex(client => client.clientId === uuid);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public addConnectRefreshListener() {
|
||||||
|
// 创建对于 connect/refresh 的监听
|
||||||
|
if (!this.connectrefreshListener) {
|
||||||
|
const bridge = useMessageBridge();
|
||||||
|
this.connectrefreshListener = bridge.addCommandListener('connect/refresh', async (message) => {
|
||||||
|
const { code, msg } = message;
|
||||||
|
|
||||||
|
if (code === 200) {
|
||||||
|
// 查找目标客户端
|
||||||
|
const clientIndex = this.findClientIndexByUuid(msg.uuid);
|
||||||
|
|
||||||
|
if (clientIndex > -1) {
|
||||||
|
// 刷新该客户端的所有资源
|
||||||
|
await this.clients[clientIndex].refreshAllResources();
|
||||||
|
this.refreshSignal.value++;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
chalk.gray(`[${new Date().toLocaleString()}]`),
|
||||||
|
chalk.red(`No client found with ID: ${msg.uuid}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { once: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async launch() {
|
public async launch() {
|
||||||
// 创建对于 log/output 的监听
|
// 创建对于 log/output 的监听
|
||||||
if (!this.connectLogListenerCancel) {
|
if (!this.connectLogListenerCancel) {
|
||||||
|
4633
service/package-lock.json
generated
Normal file
4633
service/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
133
service/src/mcp/connect-monitor.service.ts
Normal file
133
service/src/mcp/connect-monitor.service.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { McpOptions } from './client.dto.js';
|
||||||
|
import { SingleFileMonitor, FileMonitorConfig } from './file-monitor.service.js';
|
||||||
|
import { PostMessageble } from '../hook/adapter.js';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { pino } from 'pino';
|
||||||
|
|
||||||
|
// 保留现有 logger 配置
|
||||||
|
const logger = pino({
|
||||||
|
transport: {
|
||||||
|
target: 'pino-pretty',
|
||||||
|
options: {
|
||||||
|
colorize: true,
|
||||||
|
levelFirst: true,
|
||||||
|
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
|
||||||
|
ignore: 'pid,hostname',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function getFilePath(options: {
|
||||||
|
cwd?: string;
|
||||||
|
args?: string[];
|
||||||
|
}): string {
|
||||||
|
const baseDir = options.cwd || process.cwd();
|
||||||
|
const targetFile = options.args?.length ? options.args[options.args.length - 1] : '';
|
||||||
|
|
||||||
|
if (!targetFile || path.isAbsolute(targetFile)) {
|
||||||
|
return targetFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.resolve(baseDir, targetFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class McpServerConnectMonitor {
|
||||||
|
private Monitor: SingleFileMonitor | undefined;
|
||||||
|
private Options: McpOptions;
|
||||||
|
private uuid: string;
|
||||||
|
private webview: PostMessageble | undefined;
|
||||||
|
private filePath: string;
|
||||||
|
|
||||||
|
constructor(uuid: string, options: McpOptions, onchange: Function, webview?: PostMessageble) {
|
||||||
|
this.Options = options;
|
||||||
|
this.webview = webview;
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.filePath = getFilePath(options);
|
||||||
|
|
||||||
|
// 记录实例创建
|
||||||
|
logger.info({ uuid, connectionType: options.connectionType }, 'Created new connection monitor instance');
|
||||||
|
|
||||||
|
switch (options.connectionType) {
|
||||||
|
case 'STDIO':
|
||||||
|
this.setupStdioMonitor(onchange);
|
||||||
|
break;
|
||||||
|
case 'SSE':
|
||||||
|
logger.info({ uuid }, 'SSE connection type configured but not implemented');
|
||||||
|
break;
|
||||||
|
case 'STREAMABLE_HTTP':
|
||||||
|
logger.info({ uuid }, 'STREAMABLE_HTTP connection type configured but not implemented');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupStdioMonitor(onchange: Function) {
|
||||||
|
const fileConfig: FileMonitorConfig = {
|
||||||
|
filePath: this.filePath,
|
||||||
|
debounceTime: 500,
|
||||||
|
duplicateCheckTime: 500,
|
||||||
|
onChange: async (curr, prev) => {
|
||||||
|
// 使用 info 级别记录文件修改
|
||||||
|
logger.info({
|
||||||
|
uuid: this.uuid,
|
||||||
|
size: curr.size,
|
||||||
|
mtime: new Date(curr.mtime).toLocaleString()
|
||||||
|
}, 'File modified');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onchange(this.uuid, this.Options);
|
||||||
|
this.sendWebviewMessage('connect/refresh', {
|
||||||
|
code: 200,
|
||||||
|
msg: {
|
||||||
|
message: 'refresh connect success',
|
||||||
|
uuid: this.uuid,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
logger.info({ uuid: this.uuid }, 'Connection refresh successful');
|
||||||
|
} catch (err) {
|
||||||
|
this.sendWebviewMessage('connect/refresh', {
|
||||||
|
code: 500,
|
||||||
|
msg: {
|
||||||
|
message: 'refresh connect failed',
|
||||||
|
uuid: this.uuid,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 使用 error 级别记录错误
|
||||||
|
logger.error({ uuid: this.uuid, error: err }, 'Connection refresh failed');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDelete: () => {
|
||||||
|
// 使用 warn 级别记录文件删除
|
||||||
|
logger.warn({ uuid: this.uuid }, 'Monitored file has been deleted');
|
||||||
|
},
|
||||||
|
onStart: () => {
|
||||||
|
// 使用 info 级别记录监控开始
|
||||||
|
logger.info({ uuid: this.uuid, filePath: path.resolve(fileConfig.filePath) }, 'Started monitoring file');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = fs.statSync(fileConfig.filePath);
|
||||||
|
// 使用 debug 级别记录详细文件信息
|
||||||
|
logger.debug({
|
||||||
|
uuid: this.uuid,
|
||||||
|
size: stats.size,
|
||||||
|
ctime: new Date(stats.ctime).toLocaleString()
|
||||||
|
}, 'File information retrieved');
|
||||||
|
} catch (err) {
|
||||||
|
// 使用 error 级别记录获取文件信息失败
|
||||||
|
logger.error({ uuid: this.uuid, error: err }, 'Failed to retrieve file information');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
// 使用 error 级别记录监控错误
|
||||||
|
logger.error({ uuid: this.uuid, error }, 'Error occurred during monitoring');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Monitor = new SingleFileMonitor(fileConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendWebviewMessage(command: string, data: any) {
|
||||||
|
// 发送消息到webview
|
||||||
|
this.webview?.postMessage({ command, data });
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { RequestClientType } from '../common/index.dto.js';
|
|||||||
import { connect } from './client.service.js';
|
import { connect } from './client.service.js';
|
||||||
import { RestfulResponse } from '../common/index.dto.js';
|
import { RestfulResponse } from '../common/index.dto.js';
|
||||||
import { McpOptions } from './client.dto.js';
|
import { McpOptions } from './client.dto.js';
|
||||||
|
import { McpServerConnectMonitor } from './connect-monitor.service.js';
|
||||||
import * as crypto from 'node:crypto';
|
import * as crypto from 'node:crypto';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
@ -14,7 +15,19 @@ export const clientMap: Map<string, RequestClientType> = new Map();
|
|||||||
export function getClient(clientId?: string): RequestClientType | undefined {
|
export function getClient(clientId?: string): RequestClientType | undefined {
|
||||||
return clientMap.get(clientId || '');
|
return clientMap.get(clientId || '');
|
||||||
}
|
}
|
||||||
|
export const clientMonitorMap: Map<string, McpServerConnectMonitor> = new Map();
|
||||||
|
export async function updateClientMap(uuid: string, options: McpOptions): Promise<{ res: boolean; error?: any }> {
|
||||||
|
try {
|
||||||
|
const client = await connect(options);
|
||||||
|
clientMap.set(uuid, client);
|
||||||
|
const tools = await client.listTools();
|
||||||
|
console.log('[updateClientMap] tools:', tools);
|
||||||
|
return { res: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[updateClientMap] error:', error);
|
||||||
|
return { res: false, error };
|
||||||
|
}
|
||||||
|
}
|
||||||
export function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null {
|
export function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null {
|
||||||
try {
|
try {
|
||||||
const commandString = command + ' ' + args.join(' ');
|
const commandString = command + ' ' + args.join(' ');
|
||||||
@ -290,6 +303,7 @@ export async function connectService(
|
|||||||
|
|
||||||
const client = await connect(option);
|
const client = await connect(option);
|
||||||
clientMap.set(uuid, client);
|
clientMap.set(uuid, client);
|
||||||
|
clientMonitorMap.set(uuid, new McpServerConnectMonitor(uuid, option, updateClientMap, webview));
|
||||||
|
|
||||||
const versionInfo = client.getServerVersion();
|
const versionInfo = client.getServerVersion();
|
||||||
|
|
||||||
|
185
service/src/mcp/file-monitor.service.ts
Normal file
185
service/src/mcp/file-monitor.service.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件监控配置接口
|
||||||
|
*/
|
||||||
|
interface FileMonitorConfig {
|
||||||
|
filePath: string;
|
||||||
|
onChange?: (curr: fs.Stats, prev: fs.Stats) => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
onStart?: () => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
debounceTime?: number; // 防抖时间(毫秒)
|
||||||
|
duplicateCheckTime?: number; // 去重检查时间阈值(毫秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单文件监控类(去重阈值可配置)
|
||||||
|
*/
|
||||||
|
class SingleFileMonitor {
|
||||||
|
private filePath: string;
|
||||||
|
private onChange: (curr: fs.Stats, prev: fs.Stats) => void;
|
||||||
|
private onDelete: () => void;
|
||||||
|
private onError: (error: Error) => void;
|
||||||
|
private watcher: fs.FSWatcher | null = null;
|
||||||
|
private exists: boolean = false;
|
||||||
|
private lastModified: number = 0;
|
||||||
|
private lastSize: number = 0;
|
||||||
|
private debounceTimer: NodeJS.Timeout | null = null;
|
||||||
|
private debounceTime: number;
|
||||||
|
private duplicateCheckTime: number; // 去重检查时间阈值
|
||||||
|
private lastChangeTime: number = 0;
|
||||||
|
private lastChangeSize: number = 0;
|
||||||
|
private isProcessingChange = false;
|
||||||
|
private config: FileMonitorConfig; // 添加config属性
|
||||||
|
|
||||||
|
constructor(config: FileMonitorConfig) {
|
||||||
|
this.config = config; // 保存配置
|
||||||
|
this.filePath = config.filePath;
|
||||||
|
this.onChange = config.onChange || (() => {});
|
||||||
|
this.onDelete = config.onDelete || (() => {});
|
||||||
|
this.onError = config.onError || ((error) => console.error('文件监控错误:', error));
|
||||||
|
this.debounceTime = config.debounceTime || 1000;
|
||||||
|
// 使用配置中的去重时间,默认800ms
|
||||||
|
this.duplicateCheckTime = config.duplicateCheckTime !== undefined
|
||||||
|
? config.duplicateCheckTime
|
||||||
|
: 800;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
// 检查文件是否存在
|
||||||
|
this.checkFileExists()
|
||||||
|
.then(exists => {
|
||||||
|
this.exists = exists;
|
||||||
|
if (exists) {
|
||||||
|
const stats = fs.statSync(this.filePath);
|
||||||
|
this.lastModified = stats.mtimeMs;
|
||||||
|
this.lastSize = stats.size;
|
||||||
|
}
|
||||||
|
this.config.onStart?.(); // 使用保存的config属性
|
||||||
|
this.startWatching();
|
||||||
|
})
|
||||||
|
.catch(this.onError);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startWatching() {
|
||||||
|
try {
|
||||||
|
this.watcher = fs.watch(this.filePath, (eventType) => {
|
||||||
|
if (eventType === 'change') {
|
||||||
|
this.handleFileChange(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`正在监控文件: ${this.filePath}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.onError(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkFileExists(): Promise<boolean> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
fs.access(this.filePath, fs.constants.F_OK, (err) => {
|
||||||
|
resolve(!err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFileChange(isFromWatch: boolean = false) {
|
||||||
|
if (this.isProcessingChange) return;
|
||||||
|
|
||||||
|
if (!this.exists) {
|
||||||
|
this.checkFileExists()
|
||||||
|
.then(exists => {
|
||||||
|
if (exists) {
|
||||||
|
this.exists = true;
|
||||||
|
const stats = fs.statSync(this.filePath);
|
||||||
|
this.lastModified = stats.mtimeMs;
|
||||||
|
this.lastSize = stats.size;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentStats: fs.Stats;
|
||||||
|
try {
|
||||||
|
currentStats = fs.statSync(this.filePath);
|
||||||
|
} catch (error) {
|
||||||
|
this.exists = false;
|
||||||
|
this.onDelete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentMtime = currentStats.mtimeMs;
|
||||||
|
const currentSize = currentStats.size;
|
||||||
|
|
||||||
|
if (currentSize === this.lastSize && currentMtime - this.lastModified < 800) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
// 使用可配置的去重时间阈值
|
||||||
|
if (now - this.lastChangeTime < this.duplicateCheckTime && currentSize === this.lastChangeSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastChangeTime = now;
|
||||||
|
this.lastChangeSize = currentSize;
|
||||||
|
const prevStats = fs.statSync(this.filePath);
|
||||||
|
this.lastModified = currentMtime;
|
||||||
|
this.lastSize = currentSize;
|
||||||
|
|
||||||
|
this.isProcessingChange = true;
|
||||||
|
|
||||||
|
if (this.debounceTimer) {
|
||||||
|
clearTimeout(this.debounceTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debounceTimer = setTimeout(() => {
|
||||||
|
this.checkFileExists()
|
||||||
|
.then(exists => {
|
||||||
|
if (exists) {
|
||||||
|
const currStats = fs.statSync(this.filePath);
|
||||||
|
this.onChange(currStats, prevStats);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(this.onError)
|
||||||
|
.finally(() => {
|
||||||
|
this.isProcessingChange = false;
|
||||||
|
this.debounceTimer = null;
|
||||||
|
});
|
||||||
|
}, this.debounceTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkFileStatus() {
|
||||||
|
this.checkFileExists()
|
||||||
|
.then(exists => {
|
||||||
|
if (this.exists && !exists) {
|
||||||
|
this.exists = false;
|
||||||
|
this.onDelete();
|
||||||
|
} else if (!this.exists && exists) {
|
||||||
|
this.exists = true;
|
||||||
|
const stats = fs.statSync(this.filePath);
|
||||||
|
this.lastModified = stats.mtimeMs;
|
||||||
|
this.lastSize = stats.size;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(this.onError);
|
||||||
|
}
|
||||||
|
|
||||||
|
public close() {
|
||||||
|
if (this.watcher) {
|
||||||
|
// 明确指定close方法的类型,解决TS2554错误
|
||||||
|
(this.watcher.close as (callback?: () => void) => void)(() => {
|
||||||
|
console.log(`已停止监控文件: ${this.filePath}`);
|
||||||
|
});
|
||||||
|
this.watcher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.debounceTimer) {
|
||||||
|
clearTimeout(this.debounceTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SingleFileMonitor, FileMonitorConfig };
|
Loading…
x
Reference in New Issue
Block a user