@dcrays/dcgchat-test 0.1.9

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.
@@ -0,0 +1,11 @@
1
+ import type WebSocket from "ws";
2
+
3
+ let ws: WebSocket | null = null;
4
+
5
+ export function setWsConnection(next: WebSocket | null) {
6
+ ws = next;
7
+ }
8
+
9
+ export function getWsConnection(): WebSocket | null {
10
+ return ws;
11
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/log.ts ADDED
@@ -0,0 +1,46 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const logsDir = path.resolve(__dirname, "../logs");
7
+
8
+ function getLogFilePath(): string {
9
+ const date = new Date();
10
+ const yyyy = date.getFullYear();
11
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
12
+ const dd = String(date.getDate()).padStart(2, "0");
13
+ return path.join(logsDir, `${yyyy}-${mm}-${dd}.log`);
14
+ }
15
+
16
+ function formatLine(level: string, message: string, extra?: unknown): string {
17
+ const now = new Date().toISOString();
18
+ const suffix = extra !== undefined ? " " + JSON.stringify(extra) : "";
19
+ return `[${now}] [${level}] ${message}${suffix}\n`;
20
+ }
21
+
22
+ function writeLog(level: string, message: string, extra?: unknown): void {
23
+ try {
24
+ if (!fs.existsSync(logsDir)) {
25
+ fs.mkdirSync(logsDir, { recursive: true });
26
+ }
27
+ fs.appendFileSync(getLogFilePath(), formatLine(level, message, extra), "utf-8");
28
+ } catch {
29
+ // 写日志失败时静默处理,避免影响主流程
30
+ }
31
+ }
32
+
33
+ export const logDcgchat = {
34
+ info(message: string, extra?: unknown): void {
35
+ writeLog("INFO", message, extra);
36
+ },
37
+ warn(message: string, extra?: unknown): void {
38
+ writeLog("WARN", message, extra);
39
+ },
40
+ error(message: string, extra?: unknown): void {
41
+ writeLog("ERROR", message, extra);
42
+ },
43
+ debug(message: string, extra?: unknown): void {
44
+ writeLog("DEBUG", message, extra);
45
+ },
46
+ };
package/src/monitor.ts ADDED
@@ -0,0 +1,190 @@
1
+ import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
+ import WebSocket from "ws";
3
+ import { handleDcgchatMessage } from "./bot.js";
4
+ import { resolveAccount } from "./channel.js";
5
+ import { setWsConnection } from "./connection.js";
6
+ import type { InboundMessage, OutboundReply } from "./types.js";
7
+ import { setMsgParams } from "./tool.js";
8
+ import { installSkill, uninstallSkill } from "./skill.js";
9
+
10
+ export type MonitorDcgchatOpts = {
11
+ config?: ClawdbotConfig;
12
+ runtime?: RuntimeEnv;
13
+ abortSignal?: AbortSignal;
14
+ accountId?: string;
15
+ };
16
+
17
+ const RECONNECT_DELAY_MS = 3000;
18
+ const HEARTBEAT_INTERVAL_MS = 30_000;
19
+
20
+ function buildConnectUrl(account: Record<string, string>): string {
21
+ const { wsUrl, botToken, userId, domainId, appId } = account;
22
+ const url = new URL(wsUrl);
23
+ if (botToken) url.searchParams.set("bot_token", botToken);
24
+ if (userId) url.searchParams.set("_userId", userId);
25
+ url.searchParams.set("_domainId", domainId || "1000");
26
+ url.searchParams.set("_appId", appId || "100");
27
+ return url.toString();
28
+ }
29
+
30
+ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<void> {
31
+ const { config, runtime, abortSignal, accountId } = opts;
32
+ // @ts-ignore
33
+ const cfg = config ?? (runtime?.config?.() as ClawdbotConfig | undefined);
34
+ if (!cfg) {
35
+ runtime?.error?.("dcgchat: no config available");
36
+ return;
37
+ }
38
+
39
+ const account = resolveAccount(cfg, accountId ?? "default");
40
+ const log = runtime?.log ?? console.log;
41
+ const err = runtime?.error ?? console.error;
42
+
43
+ if (!account.wsUrl) {
44
+ err(`dcgchat[${account.accountId}]: wsUrl not configured`);
45
+ return;
46
+ }
47
+
48
+ let shouldReconnect = true;
49
+ let ws: WebSocket | null = null;
50
+ let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
51
+
52
+ const stopHeartbeat = () => {
53
+ if (heartbeatTimer) {
54
+ clearInterval(heartbeatTimer);
55
+ heartbeatTimer = null;
56
+ }
57
+ };
58
+
59
+ const startHeartbeat = () => {
60
+ stopHeartbeat();
61
+ heartbeatTimer = setInterval(() => {
62
+ if (ws?.readyState === WebSocket.OPEN) {
63
+ const heartbeat = {
64
+ messageType: "openclaw_bot_heartbeat",
65
+ _userId: Number(account.userId) || 0,
66
+ source: "client",
67
+ content: {
68
+ bot_token: account.botToken,
69
+ status: "1",
70
+ },
71
+ };
72
+ ws.send(JSON.stringify(heartbeat));
73
+ // log(`dcgchat[${account.accountId}]: heartbeat sent, ${JSON.stringify(heartbeat)}`);
74
+ }
75
+ }, HEARTBEAT_INTERVAL_MS);
76
+ };
77
+
78
+ const connect = () => {
79
+ if (!shouldReconnect) return;
80
+
81
+ const connectUrl = buildConnectUrl(account as Record<string, any>);
82
+ log(`dcgchat[${account.accountId}]: connecting to ${connectUrl}`);
83
+ ws = new WebSocket(connectUrl);
84
+
85
+ ws.on("open", () => {
86
+ log(`dcgchat[${account.accountId}]: connected`);
87
+ setWsConnection(ws);
88
+ startHeartbeat();
89
+ });
90
+
91
+ ws.on("message", async (data) => {
92
+ log(`dcgchat[${account.accountId}]: on message, ${data.toString()}`);
93
+ let parsed: { messageType?: string; content: any };
94
+ try {
95
+ parsed = JSON.parse(data.toString());
96
+ } catch {
97
+ err(`dcgchat[${account.accountId}]: invalid JSON received`);
98
+ return;
99
+ }
100
+
101
+ if (parsed.messageType === "openclaw_bot_heartbeat") {
102
+ log(`dcgchat[${account.accountId}]: heartbeat ack received, ${data.toString()}`);
103
+ return;
104
+ }
105
+ try {
106
+ parsed.content = JSON.parse(parsed.content);
107
+ } catch {
108
+ err(`dcgchat[${account.accountId}]: invalid JSON received`);
109
+ return;
110
+ }
111
+
112
+ if (parsed.messageType == "openclaw_bot_chat") {
113
+ const msg = parsed as unknown as InboundMessage;
114
+ setMsgParams({
115
+ userId: msg._userId,
116
+ token: msg.content.bot_token,
117
+ sessionId: msg.content.session_id,
118
+ messageId: msg.content.message_id,
119
+ });
120
+ await handleDcgchatMessage({
121
+ cfg,
122
+ msg,
123
+ accountId: account.accountId,
124
+ runtime,
125
+ onChunk: (reply) => {
126
+ if (ws?.readyState === WebSocket.OPEN) {
127
+ const res = { ...reply, content: JSON.stringify(reply.content) };
128
+ ws.send(JSON.stringify(res));
129
+ }
130
+ },
131
+ });
132
+ } else if (parsed.messageType == "openclaw_bot_event") {
133
+ const { event_type, operation_type, skill_url, skill_code, skill_id, bot_token, websocket_trace_id } = parsed.content ? parsed.content : {} as Record<string, any>;
134
+ const content = { event_type, operation_type, skill_url, skill_code, skill_id, bot_token, websocket_trace_id };
135
+ if (event_type === "skill") {
136
+ if (operation_type === "install" || operation_type === "enable") {
137
+ installSkill({ path: skill_url, code: skill_code }, content);
138
+ } else if (operation_type === "remove" || operation_type === "disable") {
139
+ uninstallSkill({ code: skill_code }, content);
140
+ } else {
141
+ log(`dcgchat[${account.accountId}]: openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`);
142
+ }
143
+ } else {
144
+ log(`dcgchat[${account.accountId}]: openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`);
145
+ }
146
+ } else {
147
+ log(`dcgchat[${account.accountId}]: ignoring unknown messageType: ${parsed.messageType}`);
148
+ }
149
+
150
+ });
151
+
152
+ ws.on("close", (code, reason) => {
153
+ stopHeartbeat();
154
+ setWsConnection(null);
155
+ log(
156
+ `dcgchat[${account.accountId}]: disconnected (code=${code}, reason=${reason?.toString() || ""})`,
157
+ );
158
+ if (shouldReconnect) {
159
+ log(`dcgchat[${account.accountId}]: reconnecting in ${RECONNECT_DELAY_MS}ms...`);
160
+ setTimeout(connect, RECONNECT_DELAY_MS);
161
+ }
162
+ });
163
+
164
+ ws.on("error", (e) => {
165
+ err(`dcgchat[${account.accountId}]: WebSocket error: ${String(e)}`);
166
+ });
167
+ };
168
+
169
+ connect();
170
+
171
+ await new Promise<void>((resolve) => {
172
+ if (abortSignal?.aborted) {
173
+ shouldReconnect = false;
174
+ ws?.close();
175
+ resolve();
176
+ return;
177
+ }
178
+ abortSignal?.addEventListener(
179
+ "abort",
180
+ () => {
181
+ log(`dcgchat[${account.accountId}]: stopping`);
182
+ stopHeartbeat();
183
+ shouldReconnect = false;
184
+ ws?.close();
185
+ resolve();
186
+ },
187
+ { once: true },
188
+ );
189
+ });
190
+ }
package/src/oss.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { createReadStream } from "node:fs";
2
+ import OSS from "ali-oss";
3
+ import { getStsToken, getUserToken } from "./api";
4
+
5
+
6
+ /** 将 File/路径/Buffer 转为 ali-oss put 所需的 Buffer 或 ReadableStream */
7
+ async function toUploadContent(
8
+ input: File | string | Buffer,
9
+ ): Promise<{ content: Buffer | ReturnType<typeof createReadStream>; fileName: string }> {
10
+ if (Buffer.isBuffer(input)) {
11
+ return { content: input, fileName: "file" };
12
+ }
13
+ if (typeof input === "string") {
14
+ return {
15
+ content: createReadStream(input),
16
+ fileName: input.split("/").pop() ?? "file",
17
+ };
18
+ }
19
+ // File: ali-oss 需要 Buffer/Stream,用 arrayBuffer 转 Buffer
20
+ const buf = Buffer.from(await input.arrayBuffer());
21
+ return { content: buf, fileName: input.name };
22
+ }
23
+
24
+ export const ossUpload = async (file: File | string | Buffer, botToken: string) => {
25
+ await getUserToken(botToken);
26
+
27
+ const { content, fileName } = await toUploadContent(file);
28
+ const data = await getStsToken(fileName, botToken);
29
+
30
+ const options: OSS.Options = {
31
+ // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
32
+ accessKeyId: data.tempAccessKeyId,
33
+ accessKeySecret: data.tempAccessKeySecret,
34
+ // 从STS服务获取的安全令牌(SecurityToken)。
35
+ stsToken: data.tempSecurityToken,
36
+ // 填写Bucket名称。
37
+ bucket: data.bucket,
38
+ endpoint: data.endPoint,
39
+ region: data.region,
40
+ secure: true,
41
+ // refreshSTSToken: async () => {
42
+ // const tokenResponse = await getStsToken(fileName);
43
+ // return {
44
+ // accessKeyId: tokenResponse.tempAccessKeyId,
45
+ // accessKeySecret: tokenResponse.tempAccessKeySecret,
46
+ // stsToken: tokenResponse.tempSecurityToken,
47
+ // }
48
+ // },
49
+ // // 5 seconds
50
+ // refreshSTSTokenInterval: 5 * 1000,
51
+ // // // 5 minutes
52
+ // // refreshSTSTokenInterval: 5 * 60 * 1000,
53
+ };
54
+
55
+ const client = new OSS(options);
56
+
57
+ const name = `${data.uploadDir}${data.ossFileKey}`;
58
+
59
+ try {
60
+ const objectResult = await client.put(name, content);
61
+ if (objectResult?.res?.status !== 200) {
62
+ throw new Error("OSS 上传失败");
63
+ }
64
+ console.log(objectResult.url);
65
+ // const url = `${data.protocol || 'http'}://${data.bucket}.${data.endPoint}/${data.uploadDir}${data.ossFileKey}`
66
+ return objectResult.url;
67
+ } catch (error) {
68
+ console.error("OSS 上传失败:", error);
69
+ throw error;
70
+ }
71
+ };
72
+
package/src/request.ts ADDED
@@ -0,0 +1,194 @@
1
+ import axios from "axios";
2
+ import md5 from "md5";
3
+ import type { IResponse } from "./types.js";
4
+ import { getUserTokenCache } from "./userInfo.js";
5
+
6
+ export const apiUrlMap = {
7
+ production: "https://api-gateway.shuwenda.com",
8
+ test: "https://api-gateway.shuwenda.icu",
9
+ develop: "https://shenyu-dev.shuwenda.icu",
10
+ };
11
+
12
+ export const appKey = {
13
+ production: "2A1C74D315CB4A01BF3DA8983695AFE2",
14
+ test: "7374A073CCBD4C8CA84FAD33896F0B69",
15
+ develop: "7374A073CCBD4C8CA84FAD33896F0B69",
16
+ };
17
+
18
+ export const signKey = {
19
+ production: "34E9023008EA445AAE6CC075CC954F46",
20
+ test: "FE93D3322CB94E978CE95BD4AA2A37D7",
21
+ develop: "FE93D3322CB94E978CE95BD4AA2A37D7",
22
+ };
23
+
24
+ const env = "test";
25
+ export const version = "1.0.0";
26
+
27
+ /**
28
+ * 根据 axios 请求配置生成等价 curl,便于复制给后端排查
29
+ */
30
+ function toCurl(config: {
31
+ baseURL?: string;
32
+ url?: string;
33
+ method?: string;
34
+ headers?: Record<string, string | number | undefined>;
35
+ data?: unknown;
36
+ }): string {
37
+ const base = config.baseURL ?? "";
38
+ const path = config.url ?? "";
39
+ const url = path.startsWith("http")
40
+ ? path
41
+ : `${base.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
42
+ const method = (config.method ?? "GET").toUpperCase();
43
+ const headers = config.headers ?? {};
44
+ const parts = ["curl", "-X", method, `'${url}'`];
45
+ for (const [k, v] of Object.entries(headers)) {
46
+ if (v !== undefined && v !== "") {
47
+ parts.push("-H", `'${k}: ${v}'`);
48
+ }
49
+ }
50
+ if (method !== "GET" && config.data !== undefined) {
51
+ const body = typeof config.data === "string" ? config.data : JSON.stringify(config.data);
52
+ parts.push("-d", `'${body.replace(/'/g, "'\\''")}'`);
53
+ }
54
+ return parts.join(" ");
55
+ }
56
+
57
+ /**
58
+ * 生成签名
59
+ * @param {Object} body 请求体
60
+ * @param {number} timestamp 时间戳
61
+ * @param {string} path 请求地址
62
+ * @param {'production' | 'test' | 'develop'} env 请求环境
63
+ * @param {string} version 版本号
64
+ * @returns {string} 大写 MD5 签名
65
+ */
66
+ export function getSignature(
67
+ body: Record<string, unknown>,
68
+ timestamp: number,
69
+ path: string,
70
+ env: "production" | "test" | "develop",
71
+ version: string = "1.0.0",
72
+ ) {
73
+ // 1. 构造 map
74
+ const map = {
75
+ timestamp,
76
+ path,
77
+ version,
78
+ ...body,
79
+ };
80
+ // 2. 按 key 进行自然排序
81
+ const sortedKeys = Object.keys(map).sort();
82
+
83
+ // 3. 拼接 key + value
84
+ const signStr =
85
+ sortedKeys
86
+ .map((key) => {
87
+ const val = map[key as keyof typeof map];
88
+ return val === undefined
89
+ ? ""
90
+ : `${key}${typeof val === "object" ? JSON.stringify(val) : val}`;
91
+ })
92
+ .join("") + signKey[env];
93
+
94
+ // 4. MD5 加密并转大写
95
+ return md5(signStr).toUpperCase();
96
+ }
97
+
98
+ function buildHeaders(data: Record<string, unknown>, url: string, userToken?: string) {
99
+ const timestamp = Date.now();
100
+
101
+ const headers: Record<string, string | number> = {
102
+ "Content-Type": "application/json",
103
+ appKey: appKey[env],
104
+ sign: getSignature(data, timestamp, url, env, version),
105
+ timestamp,
106
+ version,
107
+ };
108
+
109
+ // 如果提供了 userToken,添加到 headers
110
+ if (userToken) {
111
+ headers.authorization = userToken;
112
+ }
113
+
114
+ return headers;
115
+ }
116
+
117
+ const axiosInstance = axios.create({
118
+ baseURL: apiUrlMap[env],
119
+ timeout: 10000,
120
+ });
121
+
122
+ // 请求拦截器:自动注入 userToken
123
+ axiosInstance.interceptors.request.use(
124
+ (config) => {
125
+ // 如果请求配置中已经有 authorization,优先使用
126
+ if (config.headers?.authorization) {
127
+ return config;
128
+ }
129
+
130
+ // 从请求上下文中获取 botToken(需要在调用时设置)
131
+ const botToken = (config as any).__botToken as string | undefined;
132
+ if (botToken) {
133
+ const cachedToken = getUserTokenCache(botToken);
134
+ if (cachedToken) {
135
+ config.headers = config.headers || {};
136
+ config.headers.authorization = cachedToken;
137
+ console.log(`[request] auto-injected userToken from cache for botToken=${botToken.slice(0, 10)}...`);
138
+ }
139
+ }
140
+
141
+ return config;
142
+ },
143
+ (error) => {
144
+ return Promise.reject(error);
145
+ },
146
+ );
147
+
148
+ // 响应拦截器:打印 curl 便于调试
149
+ axiosInstance.interceptors.response.use(
150
+ (response) => {
151
+ const curl = toCurl(response.config);
152
+ console.log("[request] curl for backend:", curl);
153
+ return response.data;
154
+ },
155
+ (error) => {
156
+ const config = error.config ?? {};
157
+ const curl = toCurl(config);
158
+ console.log("[request] curl for backend (failed request):", curl);
159
+ return Promise.reject(error);
160
+ },
161
+ );
162
+
163
+
164
+
165
+ /**
166
+ * POST 请求(支持可选的 userToken 和 botToken)
167
+ * @param url 请求路径
168
+ * @param data 请求体
169
+ * @param options 可选配置
170
+ * @param options.userToken 直接提供的 userToken(优先级最高)
171
+ * @param options.botToken 用于从缓存获取 userToken 的 botToken
172
+ */
173
+ export function post<T = Record<string, unknown>, R = unknown>(
174
+ url: string,
175
+ data: T,
176
+ options?: {
177
+ userToken?: string;
178
+ botToken?: string;
179
+ },
180
+ ): Promise<IResponse<R>> {
181
+ const config: any = {
182
+ method: "POST",
183
+ url,
184
+ data,
185
+ headers: buildHeaders(data as Record<string, unknown>, url, options?.userToken),
186
+ };
187
+
188
+ // 将 botToken 附加到配置中,供请求拦截器使用
189
+ if (options?.botToken) {
190
+ config.__botToken = options.botToken;
191
+ }
192
+
193
+ return axiosInstance.request(config);
194
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,38 @@
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk";
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ function getWorkspacePath() {
6
+ const current = process.cwd();
7
+ const workspacePath = path.join(current, '.openclaw/workspace');
8
+ if (fs.existsSync(workspacePath)) {
9
+ return workspacePath;
10
+ }
11
+ return null;
12
+ }
13
+
14
+ let runtime: PluginRuntime | null = null;
15
+ let workspaceDir: string = getWorkspacePath();
16
+
17
+ export function setWorkspaceDir(dir?: string) {
18
+ if (dir) {
19
+ workspaceDir = dir;
20
+ }
21
+ }
22
+ export function getWorkspaceDir(): string {
23
+ if (!workspaceDir) {
24
+ throw new Error("Workspace directory not initialized");
25
+ }
26
+ return workspaceDir;
27
+ }
28
+
29
+ export function setDcgchatRuntime(next: PluginRuntime) {
30
+ runtime = next;
31
+ }
32
+
33
+ export function getDcgchatRuntime(): PluginRuntime {
34
+ if (!runtime) {
35
+ throw new Error("DCG Chat runtime not initialized");
36
+ }
37
+ return runtime;
38
+ }
package/src/skill.ts ADDED
@@ -0,0 +1,111 @@
1
+ /** skill utils */
2
+ import axios from 'axios';
3
+ /** @ts-ignore */
4
+ import unzipper from 'unzipper';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { logDcgchat } from './log.js';
8
+ import { getWorkspaceDir } from './runtime.js';
9
+ import { getWsConnection } from './connection.js';
10
+
11
+ type ISkillParams = {
12
+ path: string;
13
+ code: string;
14
+ }
15
+
16
+ function sendEvent(msgContent: Record<string, any>) {
17
+ const ws = getWsConnection()
18
+ if (ws?.readyState === WebSocket.OPEN) {
19
+ ws.send(JSON.stringify({
20
+ messageType: "openclaw_bot_event",
21
+ source: "client",
22
+ content: msgContent
23
+ }));
24
+ logDcgchat.info(`技能安装: ${JSON.stringify(msgContent)}`);
25
+
26
+ }
27
+ }
28
+
29
+ export async function installSkill(params: ISkillParams, msgContent: Record<string, any>) {
30
+ const { path: cdnUrl, code } = params;
31
+ const workspacePath = getWorkspaceDir();
32
+
33
+ const skillDir = path.join(workspacePath, 'skills', code);
34
+
35
+ // 确保 skills 目录存在
36
+ const skillsDir = path.join(workspacePath, 'skills');
37
+ if (!fs.existsSync(skillsDir)) {
38
+ fs.mkdirSync(skillsDir, { recursive: true });
39
+ }
40
+ // 如果目标目录已存在,先删除
41
+ if (fs.existsSync(skillDir)) {
42
+ fs.rmSync(skillDir, { recursive: true, force: true });
43
+ }
44
+
45
+ try {
46
+ // 下载 zip 文件
47
+ const response = await axios({
48
+ method: 'get',
49
+ url: cdnUrl,
50
+ responseType: 'stream'
51
+ });
52
+ // 创建目标目录
53
+ fs.mkdirSync(skillDir, { recursive: true });
54
+ // 解压文件到目标目录,跳过顶层文件夹
55
+ await new Promise((resolve, reject) => {
56
+ response.data
57
+ .pipe(unzipper.Parse())
58
+ .on('entry', (entry: any) => {
59
+ const entryPath = entry.path;
60
+ // 跳过顶层目录,只处理子文件和文件夹
61
+ const pathParts = entryPath.split('/');
62
+ if (pathParts.length > 1) {
63
+ // 移除第一级目录
64
+ const newPath = pathParts.slice(1).join('/');
65
+ const targetPath = path.join(skillDir, newPath);
66
+
67
+ if (entry.type === 'Directory') {
68
+ fs.mkdirSync(targetPath, { recursive: true });
69
+ entry.autodrain();
70
+ } else {
71
+ // 确保父目录存在
72
+ const parentDir = path.dirname(targetPath);
73
+ if (!fs.existsSync(parentDir)) {
74
+ fs.mkdirSync(parentDir, { recursive: true });
75
+ }
76
+ entry.pipe(fs.createWriteStream(targetPath));
77
+ }
78
+ } else {
79
+ entry.autodrain();
80
+ }
81
+ })
82
+ .on('close', resolve)
83
+ .on('error', reject);
84
+ });
85
+ sendEvent({ ...msgContent, status: 'ok' })
86
+ } catch (error) {
87
+ // 如果安装失败,清理目录
88
+ if (fs.existsSync(skillDir)) {
89
+ fs.rmSync(skillDir, { recursive: true, force: true });
90
+ }
91
+ sendEvent({ ...msgContent, status: 'fail' })
92
+ }
93
+ }
94
+
95
+ export function uninstallSkill(params: Omit<ISkillParams, 'path'>, msgContent: Record<string, any>) {
96
+ const { code } = params;
97
+
98
+ const workspacePath = getWorkspaceDir();
99
+ if (!workspacePath) {
100
+ throw new Error('未找到工作区路径');
101
+ }
102
+
103
+ const skillDir = path.join(workspacePath, 'skills', code);
104
+
105
+ if (fs.existsSync(skillDir)) {
106
+ fs.rmSync(skillDir, { recursive: true, force: true });
107
+ sendEvent({ ...msgContent, status: 'ok' })
108
+ } else {
109
+ sendEvent({ ...msgContent, status: 'fail' })
110
+ }
111
+ }