sloth-d2c-mcp 1.0.4-beta100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/cli/run.js +328 -0
- package/cli/sloth-server.log +1622 -0
- package/dist/build/config-manager/index.js +240 -0
- package/dist/build/core/prompt-builder.js +366 -0
- package/dist/build/core/sampling.js +375 -0
- package/dist/build/core/types.js +1 -0
- package/dist/build/index.js +852 -0
- package/dist/build/interceptor/client.js +142 -0
- package/dist/build/interceptor/vscode.js +143 -0
- package/dist/build/interceptor/web.js +28 -0
- package/dist/build/plugin/index.js +4 -0
- package/dist/build/plugin/loader.js +349 -0
- package/dist/build/plugin/manager.js +129 -0
- package/dist/build/plugin/types.js +6 -0
- package/dist/build/server.js +2116 -0
- package/dist/build/socket-client.js +166 -0
- package/dist/build/socket-server.js +260 -0
- package/dist/build/utils/client-capabilities.js +143 -0
- package/dist/build/utils/extract.js +168 -0
- package/dist/build/utils/file-manager.js +868 -0
- package/dist/build/utils/image-matcher.js +154 -0
- package/dist/build/utils/logger.js +90 -0
- package/dist/build/utils/opencv-loader.js +70 -0
- package/dist/build/utils/prompt-parser.js +46 -0
- package/dist/build/utils/tj.js +139 -0
- package/dist/build/utils/update.js +100 -0
- package/dist/build/utils/utils.js +184 -0
- package/dist/build/utils/vscode-logger.js +133 -0
- package/dist/build/utils/webpack-substitutions.js +196 -0
- package/dist/interceptor-web/dist/build-report.json +18 -0
- package/dist/interceptor-web/dist/detail.html +1 -0
- package/dist/interceptor-web/dist/index.html +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
// ES模块中获取__dirname的替代方案
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
// 存储等待中的请求 { token: { resolve } }
|
|
11
|
+
const pendingRequests = new Map();
|
|
12
|
+
let server = null;
|
|
13
|
+
let app = null;
|
|
14
|
+
let serverPort = 3005;
|
|
15
|
+
// 初始化Express应用和服务器
|
|
16
|
+
function initializeServer() {
|
|
17
|
+
if (app && server) {
|
|
18
|
+
return Promise.resolve(); // 已经初始化过了
|
|
19
|
+
}
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
app = express();
|
|
22
|
+
app.use(express.urlencoded({ extended: true }));
|
|
23
|
+
app.use(express.json());
|
|
24
|
+
// 接收Tauri应用发送的数据
|
|
25
|
+
app.post('/callback/:token', (req, res) => {
|
|
26
|
+
const token = req.params.token;
|
|
27
|
+
const { result } = req.body;
|
|
28
|
+
console.log(`收到来自Tauri应用的数据 - Token: ${token}, Result: ${result}`);
|
|
29
|
+
const request = pendingRequests.get(token);
|
|
30
|
+
if (request && request.resolve) {
|
|
31
|
+
request.resolve(result);
|
|
32
|
+
res.json({ success: true, message: 'Data received successfully' });
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
res.status(404).json({ success: false, message: 'Invalid or expired token' });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
// 启动服务器,如果端口被占用则尝试下一个端口
|
|
39
|
+
server = http.createServer(app);
|
|
40
|
+
const tryListen = (port) => {
|
|
41
|
+
server.listen(port, () => {
|
|
42
|
+
serverPort = port;
|
|
43
|
+
console.log(`Tauri callback server running on http://localhost:${port}`);
|
|
44
|
+
resolve();
|
|
45
|
+
});
|
|
46
|
+
server.on('error', (err) => {
|
|
47
|
+
if (err.code === 'EADDRINUSE') {
|
|
48
|
+
console.log(`Port ${port} is busy, trying ${port + 1}...`);
|
|
49
|
+
tryListen(port + 1);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
reject(err);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
tryListen(serverPort);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// 打开Tauri应用并获取用户输入
|
|
60
|
+
export async function getUserInputFromTauri(prompt = "Please provide input") {
|
|
61
|
+
return new Promise(async (resolve, reject) => {
|
|
62
|
+
try {
|
|
63
|
+
// 确保服务器已初始化
|
|
64
|
+
await initializeServer();
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
reject(new Error(`Failed to initialize server: ${error}`));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const token = uuidv4();
|
|
71
|
+
// 声明heartbeat变量
|
|
72
|
+
let heartbeat;
|
|
73
|
+
// 设置超时 - 增加到10分钟
|
|
74
|
+
const timeout = setTimeout(() => {
|
|
75
|
+
if (pendingRequests.has(token)) {
|
|
76
|
+
pendingRequests.delete(token);
|
|
77
|
+
clearInterval(heartbeat);
|
|
78
|
+
console.log(`Token ${token} 超时,用户未在10分钟内提供输入`);
|
|
79
|
+
reject(new Error('Timeout: User did not provide input within 10 minutes'));
|
|
80
|
+
}
|
|
81
|
+
}, 600000); // 10分钟超时
|
|
82
|
+
console.log(`Token ${token} 已创建,等待用户输入...`);
|
|
83
|
+
// 添加心跳机制,每30秒输出一次状态
|
|
84
|
+
heartbeat = setInterval(() => {
|
|
85
|
+
if (pendingRequests.has(token)) {
|
|
86
|
+
console.log(`Token ${token} 仍在等待用户输入...`);
|
|
87
|
+
}
|
|
88
|
+
}, 30000);
|
|
89
|
+
// 存储解析函数
|
|
90
|
+
pendingRequests.set(token, {
|
|
91
|
+
resolve: (value) => {
|
|
92
|
+
console.log(`Token ${token} 收到用户输入,正在解析...`);
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
clearInterval(heartbeat);
|
|
95
|
+
pendingRequests.delete(token);
|
|
96
|
+
resolve(value);
|
|
97
|
+
},
|
|
98
|
+
timeout
|
|
99
|
+
});
|
|
100
|
+
// 启动Tauri应用
|
|
101
|
+
const tauriAppPath = path.resolve(__dirname, '../../client');
|
|
102
|
+
console.log(`正在启动Tauri应用: ${tauriAppPath}`);
|
|
103
|
+
console.log(`Token: ${token}`);
|
|
104
|
+
// 设置环境变量传递token和端口
|
|
105
|
+
const env = {
|
|
106
|
+
...process.env,
|
|
107
|
+
MCP_TOKEN: token,
|
|
108
|
+
MCP_PORT: serverPort.toString()
|
|
109
|
+
};
|
|
110
|
+
// 使用pnpm tauri dev启动应用
|
|
111
|
+
const tauriProcess = spawn('pnpm', ['tauri', 'dev'], {
|
|
112
|
+
cwd: tauriAppPath,
|
|
113
|
+
stdio: 'ignore',
|
|
114
|
+
detached: false,
|
|
115
|
+
env: env
|
|
116
|
+
});
|
|
117
|
+
tauriProcess.stdout?.on('data', (data) => {
|
|
118
|
+
console.log(`Tauri stdout: ${data}`);
|
|
119
|
+
});
|
|
120
|
+
tauriProcess.stderr?.on('data', (data) => {
|
|
121
|
+
console.error(`Tauri stderr: ${data}`);
|
|
122
|
+
});
|
|
123
|
+
tauriProcess.on('error', (error) => {
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
clearInterval(heartbeat);
|
|
126
|
+
pendingRequests.delete(token);
|
|
127
|
+
reject(new Error(`Failed to start Tauri app: ${error.message}`));
|
|
128
|
+
});
|
|
129
|
+
tauriProcess.on('close', (code) => {
|
|
130
|
+
console.log(`Tauri process exited with code ${code}`);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// 清理函数
|
|
135
|
+
export function cleanup() {
|
|
136
|
+
if (server) {
|
|
137
|
+
server.close();
|
|
138
|
+
server = null;
|
|
139
|
+
app = null;
|
|
140
|
+
}
|
|
141
|
+
pendingRequests.clear();
|
|
142
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as net from 'net';
|
|
2
|
+
/**
|
|
3
|
+
* 与VSCode扩展通信的工具类
|
|
4
|
+
*/
|
|
5
|
+
export class VSCodeCommunicator {
|
|
6
|
+
host = '127.0.0.1';
|
|
7
|
+
port = 13141;
|
|
8
|
+
// 新增: 生成会话ID
|
|
9
|
+
generateSessionId() {
|
|
10
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 长连接模式 - 直接等待用户输入,无超时限制
|
|
14
|
+
* @param sessionId 会话ID
|
|
15
|
+
* @param payload 传递给webview的数据(包含 clientApiSupport)
|
|
16
|
+
* @returns Promise<string> 返回用户输入的数据
|
|
17
|
+
*/
|
|
18
|
+
waitForUserInputLongConnection(sessionId, payload) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const client = net.connect({ port: this.port, host: this.host }, () => {
|
|
21
|
+
console.log(`[MCP] 建立长连接等待用户输入,会话ID: ${sessionId}`);
|
|
22
|
+
// 发送长连接等待命令,包含 payload 数据(clientApiSupport 已在 payload 中)
|
|
23
|
+
const requestPayload = JSON.stringify({
|
|
24
|
+
cmd: 'waitForInput',
|
|
25
|
+
sessionId,
|
|
26
|
+
payload: payload
|
|
27
|
+
});
|
|
28
|
+
client.write(requestPayload);
|
|
29
|
+
});
|
|
30
|
+
let isResolved = false;
|
|
31
|
+
client.on('data', (data) => {
|
|
32
|
+
const response = data.toString();
|
|
33
|
+
console.log('[MCP] 收到响应:', response);
|
|
34
|
+
if (response === 'WAITING_FOR_INPUT') {
|
|
35
|
+
console.log('[MCP] 服务器确认等待状态,webview已打开,等待用户输入...');
|
|
36
|
+
}
|
|
37
|
+
else if (response === 'SERVER_STOPPING') {
|
|
38
|
+
if (!isResolved) {
|
|
39
|
+
isResolved = true;
|
|
40
|
+
client.end();
|
|
41
|
+
reject(new Error('服务器正在关闭'));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// 收到用户数据
|
|
46
|
+
if (!isResolved) {
|
|
47
|
+
isResolved = true;
|
|
48
|
+
console.log('[MCP] 成功获取用户输入:', response);
|
|
49
|
+
resolve(response);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
client.on('error', (err) => {
|
|
54
|
+
if (!isResolved) {
|
|
55
|
+
isResolved = true;
|
|
56
|
+
reject(new Error(`VSCode扩展连接失败: ${err.message}`));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
client.on('close', () => {
|
|
60
|
+
if (!isResolved) {
|
|
61
|
+
isResolved = true;
|
|
62
|
+
reject(new Error('连接意外关闭'));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 从VSCode获取用户输入 - 主要方法
|
|
69
|
+
* 使用长连接模式,无超时限制
|
|
70
|
+
* @param payload 传输参数(包含 clientApiSupport)
|
|
71
|
+
* @returns Promise<string> 返回用户输入的数据
|
|
72
|
+
*/
|
|
73
|
+
async getUserInputFromVSCode(payload) {
|
|
74
|
+
try {
|
|
75
|
+
console.log('[MCP] 正在打开VSCode webview表单...');
|
|
76
|
+
// 生成会话ID
|
|
77
|
+
const sessionId = this.generateSessionId();
|
|
78
|
+
console.log(`[MCP] 生成会话ID: ${sessionId}`);
|
|
79
|
+
console.log('[MCP] 请在VSCode webview中输入内容并提交...');
|
|
80
|
+
console.log('[MCP] 使用长连接模式,无超时限制,请耐心等待用户输入');
|
|
81
|
+
// 使用长连接模式等待用户输入
|
|
82
|
+
return await this.waitForUserInputLongConnection(sessionId, payload);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
throw new Error(`获取用户输入失败: ${error}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 检查VSCode扩展是否可用
|
|
90
|
+
* @returns Promise<boolean> 返回扩展是否可用
|
|
91
|
+
*/
|
|
92
|
+
async isVSCodeExtensionAvailable() {
|
|
93
|
+
try {
|
|
94
|
+
// 简单的连接测试
|
|
95
|
+
const client = net.connect({ port: this.port, host: this.host });
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
client.on('connect', () => {
|
|
98
|
+
client.end();
|
|
99
|
+
resolve(true);
|
|
100
|
+
});
|
|
101
|
+
client.on('error', () => {
|
|
102
|
+
resolve(false);
|
|
103
|
+
});
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
client.destroy();
|
|
106
|
+
resolve(false);
|
|
107
|
+
}, 1000);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// 创建单例实例
|
|
116
|
+
const vscodeComm = new VSCodeCommunicator();
|
|
117
|
+
/**
|
|
118
|
+
* 从VSCode获取用户输入的便捷函数
|
|
119
|
+
* @param payload 传输参数(包含 clientApiSupport)
|
|
120
|
+
* @returns Promise<string> 返回用户输入的数据
|
|
121
|
+
*/
|
|
122
|
+
export async function getUserInputFromVSCode(payload) {
|
|
123
|
+
return vscodeComm.getUserInputFromVSCode(payload);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 检查VSCode扩展是否可用的便捷函数
|
|
127
|
+
* @returns Promise<boolean> 返回扩展是否可用
|
|
128
|
+
*/
|
|
129
|
+
export async function isVSCodeAvailable() {
|
|
130
|
+
return vscodeComm.isVSCodeExtensionAvailable();
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 清理资源的函数
|
|
134
|
+
*/
|
|
135
|
+
export function cleanup() {
|
|
136
|
+
console.log('[MCP] VSCode通信模块清理完成');
|
|
137
|
+
// 这里可以添加任何需要清理的资源
|
|
138
|
+
// 比如关闭连接池、清理缓存等
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 导出通信器实例,供高级用法使用
|
|
142
|
+
*/
|
|
143
|
+
export { vscodeComm };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getUserInput as getServerUserInput } from '../server.js';
|
|
2
|
+
import ConfigManager from '../config-manager/index.js';
|
|
3
|
+
const configManager = new ConfigManager("d2c-mcp");
|
|
4
|
+
// 获取用户输入的主函数 - 现在使用共享的MCP服务器
|
|
5
|
+
export async function getUserInput(payload) {
|
|
6
|
+
try {
|
|
7
|
+
return await getServerUserInput(payload);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
throw new Error(`Failed to get user input: ${error.message}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// 获取配置的辅助函数
|
|
14
|
+
export async function getConfig() {
|
|
15
|
+
if (await configManager.exists()) {
|
|
16
|
+
return await configManager.load();
|
|
17
|
+
}
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
// 保存配置的辅助函数
|
|
21
|
+
export async function saveConfig(config) {
|
|
22
|
+
await configManager.save(config);
|
|
23
|
+
}
|
|
24
|
+
// 清理函数 - 现在只需要清理本地资源
|
|
25
|
+
export function cleanup() {
|
|
26
|
+
// 本地清理逻辑,如果有的话
|
|
27
|
+
console.log('Web interceptor cleanup completed');
|
|
28
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import envPaths from 'env-paths';
|
|
6
|
+
import { PLUGIN_HOOKS } from './types.js';
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
// 检测是否存在 pnpm
|
|
11
|
+
function hasPnpm() {
|
|
12
|
+
try {
|
|
13
|
+
execSync('pnpm --version', { stdio: 'ignore' });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// 获取包管理器命令
|
|
21
|
+
function getPackageManager() {
|
|
22
|
+
return 'npm';
|
|
23
|
+
}
|
|
24
|
+
// 使用 env-paths 获取配置目录(与 config-manager 保持一致)
|
|
25
|
+
const paths = envPaths('d2c-mcp');
|
|
26
|
+
// 获取全局 sloth-d2c-mcp 包的目录
|
|
27
|
+
function getGlobalPackageDir() {
|
|
28
|
+
// 在构建后的目录中: dist/build/plugin/loader.js
|
|
29
|
+
// 需要向上三级才能找到包根目录
|
|
30
|
+
// return path.resolve(__dirname, '..', '..', '..')
|
|
31
|
+
try {
|
|
32
|
+
const pm = getPackageManager();
|
|
33
|
+
return execSync(`${pm} root -g`, { encoding: 'utf-8' }).trim();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// 获取全局插件配置文件路径(与 config.json 在同一目录)
|
|
40
|
+
function getGlobalPluginsConfigPath() {
|
|
41
|
+
return path.join(paths.config, 'plugins.json');
|
|
42
|
+
}
|
|
43
|
+
// 获取工作区插件目录路径
|
|
44
|
+
function getWorkspacePluginDir(workspaceRoot) {
|
|
45
|
+
return path.join(workspaceRoot, '.sloth', 'plugin');
|
|
46
|
+
}
|
|
47
|
+
// 获取工作区插件配置文件路径
|
|
48
|
+
function getWorkspacePluginsConfigPath(workspaceRoot) {
|
|
49
|
+
return path.join(getWorkspacePluginDir(workspaceRoot), 'plugins.json');
|
|
50
|
+
}
|
|
51
|
+
// 加载全局插件配置
|
|
52
|
+
async function loadGlobalPluginsConfig() {
|
|
53
|
+
const configPath = getGlobalPluginsConfigPath();
|
|
54
|
+
try {
|
|
55
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
56
|
+
return JSON.parse(content);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { plugins: [] };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 保存全局插件配置
|
|
63
|
+
async function saveGlobalPluginsConfig(config) {
|
|
64
|
+
const configPath = getGlobalPluginsConfigPath();
|
|
65
|
+
// 确保目录存在
|
|
66
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
67
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
68
|
+
}
|
|
69
|
+
// 加载工作区插件配置
|
|
70
|
+
async function loadWorkspacePluginsConfig(workspaceRoot) {
|
|
71
|
+
const configPath = getWorkspacePluginsConfigPath(workspaceRoot);
|
|
72
|
+
try {
|
|
73
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
74
|
+
return JSON.parse(content);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return { plugins: [] };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function loadPluginFromGlobalDirAsync(packageName) {
|
|
81
|
+
const globalDir = getGlobalPackageDir();
|
|
82
|
+
const require = createRequire(path.join(globalDir));
|
|
83
|
+
const resolvedPath = require.resolve(packageName);
|
|
84
|
+
return await import(resolvedPath);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 解析插件配置项,统一返回 { name, config } 格式
|
|
88
|
+
*/
|
|
89
|
+
function parsePluginConfigItem(item) {
|
|
90
|
+
if (typeof item === 'string') {
|
|
91
|
+
return { name: item };
|
|
92
|
+
}
|
|
93
|
+
return { name: item.name, config: item.config };
|
|
94
|
+
}
|
|
95
|
+
// 检查本地插件是否存在
|
|
96
|
+
async function checkLocalPlugin(workspaceRoot, pluginName) {
|
|
97
|
+
const pluginDir = getWorkspacePluginDir(workspaceRoot);
|
|
98
|
+
const localPluginPath = path.join(pluginDir, pluginName, 'index.cjs');
|
|
99
|
+
try {
|
|
100
|
+
await fs.access(localPluginPath);
|
|
101
|
+
return localPluginPath;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// 检查 npm 包是否已安装
|
|
108
|
+
async function isNpmPackageInstalled(packageName) {
|
|
109
|
+
try {
|
|
110
|
+
await loadPluginFromGlobalDirAsync(packageName);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// 尝试安装 npm 包
|
|
118
|
+
async function tryInstallNpmPackage(packageName) {
|
|
119
|
+
const globalDir = getGlobalPackageDir();
|
|
120
|
+
const pm = getPackageManager();
|
|
121
|
+
try {
|
|
122
|
+
console.log(`[sloth] 尝试安装插件包: ${packageName}`);
|
|
123
|
+
execSync(`${pm} install ${packageName}`, {
|
|
124
|
+
cwd: globalDir,
|
|
125
|
+
stdio: 'pipe',
|
|
126
|
+
});
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.error(`[sloth] 安装插件包 ${packageName} 失败:`, error);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 安装插件 npm 包到全局(sloth add 命令)
|
|
136
|
+
* 注意:这只是安装到全局,不会直接生效
|
|
137
|
+
* 需要在工作区的 .sloth/plugin/plugins.json 中声明才会生效
|
|
138
|
+
* @param packageName 插件包名
|
|
139
|
+
*/
|
|
140
|
+
export async function installPlugin(packageName) {
|
|
141
|
+
const globalDir = getGlobalPackageDir();
|
|
142
|
+
console.log(`[sloth] 正在安装插件: ${packageName}`);
|
|
143
|
+
// console.log(`[sloth] 安装目录: ${globalDir}`)
|
|
144
|
+
try {
|
|
145
|
+
const pm = getPackageManager();
|
|
146
|
+
// 在全局包目录下执行安装
|
|
147
|
+
execSync(`${pm} install -g ${packageName}`, {
|
|
148
|
+
cwd: globalDir,
|
|
149
|
+
stdio: 'inherit',
|
|
150
|
+
});
|
|
151
|
+
// 更新全局插件配置(记录已安装的插件)
|
|
152
|
+
const config = await loadGlobalPluginsConfig();
|
|
153
|
+
const existingNames = config.plugins.map(item => parsePluginConfigItem(item).name);
|
|
154
|
+
if (!existingNames.includes(packageName)) {
|
|
155
|
+
config.plugins.push(packageName);
|
|
156
|
+
await saveGlobalPluginsConfig(config);
|
|
157
|
+
}
|
|
158
|
+
console.log(`[sloth] 插件 ${packageName} 安装成功`);
|
|
159
|
+
console.log(`[sloth] 注意:需要在工作区的 .sloth/plugin/plugins.json 中声明才会生效`);
|
|
160
|
+
return {
|
|
161
|
+
success: true,
|
|
162
|
+
message: `插件 ${packageName} 安装成功。请在工作区的 .sloth/plugin/plugins.json 中添加该插件以启用。`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
167
|
+
console.error(`[sloth] 插件安装失败: ${errorMessage}`);
|
|
168
|
+
return { success: false, message: `插件安装失败: ${errorMessage}` };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 卸载插件 npm 包
|
|
173
|
+
* @param packageName 插件包名
|
|
174
|
+
*/
|
|
175
|
+
export async function uninstallPlugin(packageName) {
|
|
176
|
+
const globalDir = getGlobalPackageDir();
|
|
177
|
+
console.log(`[sloth] 正在卸载插件: ${packageName}`);
|
|
178
|
+
try {
|
|
179
|
+
const pm = getPackageManager();
|
|
180
|
+
// 在全局包目录下执行卸载
|
|
181
|
+
execSync(`${pm} uninstall ${packageName}`, {
|
|
182
|
+
cwd: globalDir,
|
|
183
|
+
stdio: 'inherit',
|
|
184
|
+
});
|
|
185
|
+
// 更新全局插件配置
|
|
186
|
+
const config = await loadGlobalPluginsConfig();
|
|
187
|
+
config.plugins = config.plugins.filter((p) => parsePluginConfigItem(p).name !== packageName);
|
|
188
|
+
await saveGlobalPluginsConfig(config);
|
|
189
|
+
console.log(`[sloth] 插件 ${packageName} 卸载成功`);
|
|
190
|
+
return { success: true, message: `插件 ${packageName} 卸载成功` };
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
194
|
+
console.error(`[sloth] 插件卸载失败: ${errorMessage}`);
|
|
195
|
+
return { success: false, message: `插件卸载失败: ${errorMessage}` };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* 列出全局已安装的插件
|
|
200
|
+
*/
|
|
201
|
+
export async function listPlugins() {
|
|
202
|
+
const config = await loadGlobalPluginsConfig();
|
|
203
|
+
return config.plugins.map(item => parsePluginConfigItem(item).name);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* 列出工作区声明的插件
|
|
207
|
+
* @param workspaceRoot 工作区根目录
|
|
208
|
+
*/
|
|
209
|
+
export async function listWorkspacePlugins(workspaceRoot) {
|
|
210
|
+
const config = await loadWorkspacePluginsConfig(workspaceRoot);
|
|
211
|
+
return config.plugins.map(item => parsePluginConfigItem(item));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 校验并加载工作区声明的所有插件
|
|
215
|
+
* 1. 先尝试查找 .sloth/plugin/ 文件夹下的本地插件
|
|
216
|
+
* 2. 再尝试通过 pnpm/npm 安装声明的 npm 包
|
|
217
|
+
* 3. 都失败则抛出错误
|
|
218
|
+
* @param workspaceRoot 工作区根目录
|
|
219
|
+
*/
|
|
220
|
+
export async function loadWorkspacePlugins(workspaceRoot) {
|
|
221
|
+
const config = await loadWorkspacePluginsConfig(workspaceRoot);
|
|
222
|
+
const plugins = [];
|
|
223
|
+
const failedPlugins = [];
|
|
224
|
+
console.log(`[sloth] 开始加载工作区插件,工作区: ${workspaceRoot}`);
|
|
225
|
+
const pluginNames = config.plugins.map(item => parsePluginConfigItem(item).name);
|
|
226
|
+
console.log(`[sloth] 声明的插件: ${pluginNames.join(', ') || '无'}`);
|
|
227
|
+
for (const pluginItem of config.plugins) {
|
|
228
|
+
const { name: pluginName, config: pluginConfig } = parsePluginConfigItem(pluginItem);
|
|
229
|
+
let loaded = false;
|
|
230
|
+
console.log('pluginConfig', pluginConfig);
|
|
231
|
+
// 1. 先尝试查找本地插件
|
|
232
|
+
const localPluginPath = await checkLocalPlugin(workspaceRoot, pluginName);
|
|
233
|
+
if (localPluginPath) {
|
|
234
|
+
try {
|
|
235
|
+
console.log(`[sloth] 尝试加载本地插件: ${localPluginPath}`);
|
|
236
|
+
const pluginModule = await import(localPluginPath);
|
|
237
|
+
const plugin = {
|
|
238
|
+
name: pluginName,
|
|
239
|
+
// 只取出PluginHooks
|
|
240
|
+
...PLUGIN_HOOKS.reduce((acc, hookName) => {
|
|
241
|
+
acc[hookName] = (pluginModule.default || pluginModule)[hookName];
|
|
242
|
+
return acc;
|
|
243
|
+
}, {}),
|
|
244
|
+
// ...(pluginModule.default || pluginModule),
|
|
245
|
+
_config: pluginConfig,
|
|
246
|
+
};
|
|
247
|
+
plugins.push(plugin);
|
|
248
|
+
console.log(`[sloth] 本地插件 ${pluginName} 加载成功`);
|
|
249
|
+
loaded = true;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.error(`[sloth] 本地插件 ${pluginName} 加载失败:`, error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// 2. 如果本地插件不存在或加载失败,尝试加载 npm 包
|
|
256
|
+
if (!loaded) {
|
|
257
|
+
// 检查是否已安装
|
|
258
|
+
const isInstalled = await isNpmPackageInstalled(pluginName);
|
|
259
|
+
if (!isInstalled) {
|
|
260
|
+
// 尝试安装
|
|
261
|
+
console.log(`[sloth] 插件 ${pluginName} 未安装,尝试安装...`);
|
|
262
|
+
// const installSuccess = await tryInstallNpmPackage(pluginName)
|
|
263
|
+
// if (!installSuccess) {
|
|
264
|
+
// failedPlugins.push(pluginName)
|
|
265
|
+
// continue
|
|
266
|
+
// }
|
|
267
|
+
}
|
|
268
|
+
// 加载 npm 包
|
|
269
|
+
try {
|
|
270
|
+
const pluginModule = await loadPluginFromGlobalDirAsync(pluginName);
|
|
271
|
+
console.log('pluginModule', pluginModule);
|
|
272
|
+
const plugin = {
|
|
273
|
+
name: pluginName,
|
|
274
|
+
...PLUGIN_HOOKS.reduce((acc, hookName) => {
|
|
275
|
+
acc[hookName] = (pluginModule.default || pluginModule)[hookName];
|
|
276
|
+
return acc;
|
|
277
|
+
}, {}),
|
|
278
|
+
_config: pluginConfig,
|
|
279
|
+
};
|
|
280
|
+
plugins.push(plugin);
|
|
281
|
+
console.log(`[sloth] npm 插件 ${pluginName} 加载成功`);
|
|
282
|
+
loaded = true;
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
console.error(`[sloth] npm 插件 ${pluginName} 加载失败:`, error);
|
|
286
|
+
failedPlugins.push(pluginName);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// 如果有插件加载失败,打印错误
|
|
291
|
+
if (failedPlugins.length > 0) {
|
|
292
|
+
console.error(`以下插件加载失败: ${failedPlugins.join(', ')}。` +
|
|
293
|
+
`请确保插件存在于 .sloth/plugin/<pluginName>/index.js 或为有效的 npm 包。`);
|
|
294
|
+
}
|
|
295
|
+
return plugins;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* 动态加载所有已安装的全局插件(旧方法,保留兼容)
|
|
299
|
+
* @deprecated 请使用 loadWorkspacePlugins 代替
|
|
300
|
+
*/
|
|
301
|
+
export async function loadAllPlugins() {
|
|
302
|
+
const config = await loadGlobalPluginsConfig();
|
|
303
|
+
const plugins = [];
|
|
304
|
+
for (const pluginItem of config.plugins) {
|
|
305
|
+
const { name: packageName, config: pluginConfig } = parsePluginConfigItem(pluginItem);
|
|
306
|
+
try {
|
|
307
|
+
// 动态导入插件模块
|
|
308
|
+
const pluginModule = await import(packageName);
|
|
309
|
+
console.log('pluginModule', pluginModule);
|
|
310
|
+
console.log('pluginConfig', pluginConfig);
|
|
311
|
+
const plugin = {
|
|
312
|
+
name: packageName,
|
|
313
|
+
...PLUGIN_HOOKS.reduce((acc, hookName) => {
|
|
314
|
+
acc[hookName] = (pluginModule.default || pluginModule)[hookName];
|
|
315
|
+
return acc;
|
|
316
|
+
}, {}),
|
|
317
|
+
_config: pluginConfig,
|
|
318
|
+
};
|
|
319
|
+
plugins.push(plugin);
|
|
320
|
+
console.log(`[sloth] 插件 ${packageName} 加载成功`);
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
console.error(`[sloth] 插件 ${packageName} 加载失败:`, error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return plugins;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* 动态加载单个插件
|
|
330
|
+
* @param packageName 插件包名
|
|
331
|
+
*/
|
|
332
|
+
export async function loadPlugin(packageName) {
|
|
333
|
+
try {
|
|
334
|
+
const pluginModule = await import(packageName);
|
|
335
|
+
const plugin = {
|
|
336
|
+
name: packageName,
|
|
337
|
+
...PLUGIN_HOOKS.reduce((acc, hookName) => {
|
|
338
|
+
acc[hookName] = (pluginModule.default || pluginModule)[hookName];
|
|
339
|
+
return acc;
|
|
340
|
+
}, {}),
|
|
341
|
+
};
|
|
342
|
+
console.log(`[sloth] 插件 ${packageName} 加载成功`);
|
|
343
|
+
return plugin;
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
console.error(`[sloth] 插件 ${packageName} 加载失败:`, error);
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|