@dcrays/dcgchat-test 0.2.26 → 0.2.28
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/index.ts +19 -17
- package/openclaw.plugin.json +4 -2
- package/package.json +5 -7
- package/src/bot.ts +214 -592
- package/src/channel.ts +119 -184
- package/src/monitor.ts +129 -178
- package/src/{api.ts → request/api.ts} +34 -35
- package/src/request/oss.ts +58 -0
- package/src/request/request.ts +198 -0
- package/src/{userInfo.ts → request/userInfo.ts} +36 -34
- package/src/skill.ts +119 -197
- package/src/tool.ts +109 -113
- package/src/transport.ts +108 -0
- package/src/types.ts +75 -64
- package/src/utils/constant.ts +4 -0
- package/src/utils/global.ts +112 -0
- package/src/utils/log.ts +15 -0
- package/src/utils/searchFile.ts +212 -0
- package/README.md +0 -83
- package/src/connection.ts +0 -11
- package/src/log.ts +0 -46
- package/src/oss.ts +0 -72
- package/src/request.ts +0 -201
- package/src/runtime.ts +0 -40
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import md5 from 'md5'
|
|
4
|
+
import type { IResponse } from '../types.js'
|
|
5
|
+
import { getUserTokenCache } from './userInfo.js'
|
|
6
|
+
import { getMsgParams } from '../utils/global.js'
|
|
7
|
+
import { ENV } from '../utils/constant.js'
|
|
8
|
+
import { dcgLogger } from '../utils/log.js'
|
|
9
|
+
|
|
10
|
+
export const apiUrlMap = {
|
|
11
|
+
production: 'https://api-gateway.shuwenda.com',
|
|
12
|
+
test: 'https://api-gateway.shuwenda.icu',
|
|
13
|
+
develop: 'https://shenyu-dev.shuwenda.icu'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const appKey = {
|
|
17
|
+
production: '2A1C74D315CB4A01BF3DA8983695AFE2',
|
|
18
|
+
test: '7374A073CCBD4C8CA84FAD33896F0B69',
|
|
19
|
+
develop: '7374A073CCBD4C8CA84FAD33896F0B69'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const signKey = {
|
|
23
|
+
production: '34E9023008EA445AAE6CC075CC954F46',
|
|
24
|
+
test: 'FE93D3322CB94E978CE95BD4AA2A37D7',
|
|
25
|
+
develop: 'FE93D3322CB94E978CE95BD4AA2A37D7'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const version = '1.0.0'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 根据 axios 请求配置生成等价 curl,便于复制给后端排查
|
|
32
|
+
*/
|
|
33
|
+
function toCurl(config: {
|
|
34
|
+
baseURL?: string
|
|
35
|
+
url?: string
|
|
36
|
+
method?: string
|
|
37
|
+
headers?: Record<string, string | number | undefined>
|
|
38
|
+
data?: unknown
|
|
39
|
+
}): string {
|
|
40
|
+
const base = config.baseURL ?? ''
|
|
41
|
+
const path = config.url ?? ''
|
|
42
|
+
const url = path.startsWith('http')
|
|
43
|
+
? path
|
|
44
|
+
: `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
|
|
45
|
+
const method = (config.method ?? 'GET').toUpperCase()
|
|
46
|
+
const headers = config.headers ?? {}
|
|
47
|
+
const parts = ['curl', '-X', method, `'${url}'`]
|
|
48
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
49
|
+
if (v !== undefined && v !== '') {
|
|
50
|
+
parts.push('-H', `'${k}: ${v}'`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (method !== 'GET' && config.data !== undefined) {
|
|
54
|
+
const body = typeof config.data === 'string' ? config.data : JSON.stringify(config.data)
|
|
55
|
+
parts.push('-d', `'${body.replace(/'/g, "'\\''")}'`)
|
|
56
|
+
}
|
|
57
|
+
return parts.join(' ')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 生成签名
|
|
62
|
+
* @param {Object} body 请求体
|
|
63
|
+
* @param {number} timestamp 时间戳
|
|
64
|
+
* @param {string} path 请求地址
|
|
65
|
+
* @param {'production' | 'test' | 'develop'} ENV 请求环境
|
|
66
|
+
* @param {string} version 版本号
|
|
67
|
+
* @returns {string} 大写 MD5 签名
|
|
68
|
+
*/
|
|
69
|
+
export function getSignature(
|
|
70
|
+
body: Record<string, unknown>,
|
|
71
|
+
timestamp: number,
|
|
72
|
+
path: string,
|
|
73
|
+
ENV: 'production' | 'test' | 'develop',
|
|
74
|
+
version: string = '1.0.0'
|
|
75
|
+
) {
|
|
76
|
+
// 1. 构造 map
|
|
77
|
+
const map = { timestamp, path, version, ...body }
|
|
78
|
+
// 2. 按 key 进行自然排序
|
|
79
|
+
const sortedKeys = Object.keys(map).sort()
|
|
80
|
+
// 3. 拼接 key + value
|
|
81
|
+
const signStr =
|
|
82
|
+
sortedKeys
|
|
83
|
+
.map((key) => {
|
|
84
|
+
const val = map[key as keyof typeof map]
|
|
85
|
+
return val === undefined
|
|
86
|
+
? ''
|
|
87
|
+
: `${key}${typeof val === 'object' ? JSON.stringify(val) : val}`
|
|
88
|
+
})
|
|
89
|
+
.join('') + signKey[ENV]
|
|
90
|
+
// 4. MD5 加密并转大写
|
|
91
|
+
return md5(signStr).toUpperCase()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildHeaders(data: Record<string, unknown>, url: string, userToken?: string) {
|
|
95
|
+
const timestamp = Date.now()
|
|
96
|
+
|
|
97
|
+
const headers: Record<string, string | number> = {
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
appKey: appKey[ENV],
|
|
100
|
+
sign: getSignature(data, timestamp, url, ENV, version),
|
|
101
|
+
timestamp,
|
|
102
|
+
version
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 如果提供了 userToken,添加到 headers
|
|
106
|
+
if (userToken) {
|
|
107
|
+
headers.authorization = userToken
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return headers
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const axiosInstance = axios.create({
|
|
114
|
+
baseURL: apiUrlMap[ENV],
|
|
115
|
+
timeout: 10000
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// 请求拦截器:自动注入 userToken
|
|
119
|
+
axiosInstance.interceptors.request.use(
|
|
120
|
+
(config) => {
|
|
121
|
+
// 如果请求配置中已经有 authorization,优先使用
|
|
122
|
+
if (config.headers?.authorization) {
|
|
123
|
+
return config
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 从请求上下文中获取 botToken(需要在调用时设置)
|
|
127
|
+
const botToken = (config as any).__botToken as string | undefined
|
|
128
|
+
if (botToken) {
|
|
129
|
+
const cachedToken = getUserTokenCache(botToken)
|
|
130
|
+
if (cachedToken) {
|
|
131
|
+
config.headers = config.headers || {}
|
|
132
|
+
config.headers.authorization = cachedToken
|
|
133
|
+
dcgLogger(
|
|
134
|
+
`[request] auto-injected userToken from cache for botToken=${botToken.slice(0, 10)}...`
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return config
|
|
140
|
+
},
|
|
141
|
+
(error) => {
|
|
142
|
+
return Promise.reject(error)
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// 响应拦截器:打印 curl 便于调试
|
|
147
|
+
axiosInstance.interceptors.response.use(
|
|
148
|
+
(response) => {
|
|
149
|
+
return response.data
|
|
150
|
+
},
|
|
151
|
+
(error) => {
|
|
152
|
+
const config = error.config ?? {}
|
|
153
|
+
const curl = toCurl(config)
|
|
154
|
+
dcgLogger(`[request] curl for backend (failed request): ${curl}`)
|
|
155
|
+
return Promise.reject(error)
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* POST 请求(支持可选的 userToken 和 botToken)
|
|
161
|
+
* @param url 请求路径
|
|
162
|
+
* @param data 请求体
|
|
163
|
+
* @param options 可选配置
|
|
164
|
+
* @param options.userToken 直接提供的 userToken(优先级最高)
|
|
165
|
+
* @param options.botToken 用于从缓存获取 userToken 的 botToken
|
|
166
|
+
*/
|
|
167
|
+
export function post<T = Record<string, unknown>, R = unknown>(
|
|
168
|
+
url: string,
|
|
169
|
+
data: T,
|
|
170
|
+
options?: {
|
|
171
|
+
userToken?: string
|
|
172
|
+
botToken?: string
|
|
173
|
+
}
|
|
174
|
+
): Promise<IResponse<R>> {
|
|
175
|
+
const params = getMsgParams() || {}
|
|
176
|
+
const config: any = {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
url,
|
|
179
|
+
data: {
|
|
180
|
+
...data,
|
|
181
|
+
_appId: params.appId
|
|
182
|
+
},
|
|
183
|
+
headers: buildHeaders(
|
|
184
|
+
{
|
|
185
|
+
...data,
|
|
186
|
+
_appId: params.appId
|
|
187
|
+
} as Record<string, unknown>,
|
|
188
|
+
url,
|
|
189
|
+
options?.userToken
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 将 botToken 附加到配置中,供请求拦截器使用
|
|
194
|
+
if (options?.botToken) {
|
|
195
|
+
config.__botToken = options.botToken
|
|
196
|
+
}
|
|
197
|
+
return axiosInstance.request(config)
|
|
198
|
+
}
|
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
* 负责维护 botToken -> userToken 的映射关系,支持自动过期
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { dcgLogger } from '../utils/log.js'
|
|
7
|
+
|
|
6
8
|
// userToken 缓存配置
|
|
7
|
-
const TOKEN_CACHE_DURATION = 60 * 60 * 1000
|
|
9
|
+
const TOKEN_CACHE_DURATION = 60 * 60 * 1000 // 1小时
|
|
8
10
|
|
|
9
11
|
type TokenCacheEntry = {
|
|
10
|
-
token: string
|
|
11
|
-
expiresAt: number
|
|
12
|
-
}
|
|
12
|
+
token: string
|
|
13
|
+
expiresAt: number
|
|
14
|
+
}
|
|
13
15
|
|
|
14
16
|
// 内存缓存:botToken -> { token, expiresAt }
|
|
15
|
-
const tokenCache = new Map<string, TokenCacheEntry>()
|
|
17
|
+
const tokenCache = new Map<string, TokenCacheEntry>()
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* 设置 userToken 缓存
|
|
@@ -20,11 +22,11 @@ const tokenCache = new Map<string, TokenCacheEntry>();
|
|
|
20
22
|
* @param userToken 用户 token
|
|
21
23
|
*/
|
|
22
24
|
export function setUserTokenCache(botToken: string, userToken: string): void {
|
|
23
|
-
const expiresAt = Date.now() + TOKEN_CACHE_DURATION
|
|
24
|
-
tokenCache.set(botToken, { token: userToken, expiresAt })
|
|
25
|
-
|
|
26
|
-
`[token-cache] cached userToken for botToken=${botToken.slice(0, 10)}..., expires at ${new Date(expiresAt).toISOString()}
|
|
27
|
-
)
|
|
25
|
+
const expiresAt = Date.now() + TOKEN_CACHE_DURATION
|
|
26
|
+
tokenCache.set(botToken, { token: userToken, expiresAt })
|
|
27
|
+
dcgLogger(
|
|
28
|
+
`[token-cache] cached userToken for botToken=${botToken.slice(0, 10)}..., expires at ${new Date(expiresAt).toISOString()}`
|
|
29
|
+
)
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -33,23 +35,23 @@ export function setUserTokenCache(botToken: string, userToken: string): void {
|
|
|
33
35
|
* @returns userToken 或 null(未找到或已过期)
|
|
34
36
|
*/
|
|
35
37
|
export function getUserTokenCache(botToken: string): string | null {
|
|
36
|
-
const entry = tokenCache.get(botToken)
|
|
38
|
+
const entry = tokenCache.get(botToken)
|
|
37
39
|
if (!entry) {
|
|
38
|
-
|
|
39
|
-
return null
|
|
40
|
+
dcgLogger(`[token-cache] no cache found for botToken=${botToken.slice(0, 10)}...`)
|
|
41
|
+
return null
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
// 检查是否过期
|
|
43
45
|
if (Date.now() >= entry.expiresAt) {
|
|
44
|
-
|
|
45
|
-
tokenCache.delete(botToken)
|
|
46
|
-
return null
|
|
46
|
+
dcgLogger(`[token-cache] cache expired for botToken=${botToken.slice(0, 10)}..., removing`)
|
|
47
|
+
tokenCache.delete(botToken)
|
|
48
|
+
return null
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
`[token-cache] cache hit for botToken=${botToken.slice(0, 10)}..., valid until ${new Date(entry.expiresAt).toISOString()}
|
|
51
|
-
)
|
|
52
|
-
return entry.token
|
|
51
|
+
dcgLogger(
|
|
52
|
+
`[token-cache] cache hit for botToken=${botToken.slice(0, 10)}..., valid until ${new Date(entry.expiresAt).toISOString()}`
|
|
53
|
+
)
|
|
54
|
+
return entry.token
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/**
|
|
@@ -57,41 +59,41 @@ export function getUserTokenCache(botToken: string): string | null {
|
|
|
57
59
|
* @param botToken 机器人 token
|
|
58
60
|
*/
|
|
59
61
|
export function clearUserTokenCache(botToken: string): void {
|
|
60
|
-
tokenCache.delete(botToken)
|
|
61
|
-
|
|
62
|
+
tokenCache.delete(botToken)
|
|
63
|
+
dcgLogger(`[token-cache] cleared cache for botToken=${botToken.slice(0, 10)}...`)
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
/**
|
|
65
67
|
* 清除所有缓存
|
|
66
68
|
*/
|
|
67
69
|
export function clearAllUserTokenCache(): void {
|
|
68
|
-
tokenCache.clear()
|
|
69
|
-
|
|
70
|
+
tokenCache.clear()
|
|
71
|
+
dcgLogger(`[token-cache] cleared all token cache`)
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
|
73
75
|
* 获取缓存统计信息(用于调试)
|
|
74
76
|
*/
|
|
75
77
|
export function getTokenCacheStats(): {
|
|
76
|
-
total: number
|
|
77
|
-
valid: number
|
|
78
|
-
expired: number
|
|
78
|
+
total: number
|
|
79
|
+
valid: number
|
|
80
|
+
expired: number
|
|
79
81
|
} {
|
|
80
|
-
const now = Date.now()
|
|
81
|
-
let valid = 0
|
|
82
|
-
let expired = 0
|
|
82
|
+
const now = Date.now()
|
|
83
|
+
let valid = 0
|
|
84
|
+
let expired = 0
|
|
83
85
|
|
|
84
86
|
for (const entry of tokenCache.values()) {
|
|
85
87
|
if (now < entry.expiresAt) {
|
|
86
|
-
valid
|
|
88
|
+
valid++
|
|
87
89
|
} else {
|
|
88
|
-
expired
|
|
90
|
+
expired++
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
return {
|
|
93
95
|
total: tokenCache.size,
|
|
94
96
|
valid,
|
|
95
|
-
expired
|
|
96
|
-
}
|
|
97
|
+
expired
|
|
98
|
+
}
|
|
97
99
|
}
|
package/src/skill.ts
CHANGED
|
@@ -1,239 +1,161 @@
|
|
|
1
|
-
|
|
2
|
-
import axios from 'axios';
|
|
1
|
+
import axios from 'axios'
|
|
3
2
|
/** @ts-ignore */
|
|
4
|
-
import unzipper from 'unzipper'
|
|
5
|
-
import { pipeline } from
|
|
6
|
-
import fs from 'fs'
|
|
7
|
-
import path from 'path'
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { resolveAccount } from './channel.js';
|
|
13
|
-
import { getMsgParams } from './tool.js';
|
|
3
|
+
import unzipper from 'unzipper'
|
|
4
|
+
import { pipeline } from 'stream/promises'
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import { getWorkspaceDir } from './utils/global.js'
|
|
8
|
+
import { getWsConnection } from './utils/global.js'
|
|
9
|
+
import { dcgLogger } from './utils/log.js'
|
|
10
|
+
import { isWsOpen } from './transport.js'
|
|
14
11
|
|
|
15
12
|
type ISkillParams = {
|
|
16
|
-
path: string
|
|
17
|
-
code: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
type SkillContext = {
|
|
21
|
-
cfg: ClawdbotConfig;
|
|
22
|
-
accountId: string;
|
|
23
|
-
runtime?: RuntimeEnv;
|
|
13
|
+
path: string
|
|
14
|
+
code: string
|
|
24
15
|
}
|
|
25
16
|
|
|
26
17
|
function sendEvent(msgContent: Record<string, any>) {
|
|
27
18
|
const ws = getWsConnection()
|
|
28
|
-
if (
|
|
29
|
-
ws
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
19
|
+
if (isWsOpen()) {
|
|
20
|
+
ws?.send(
|
|
21
|
+
JSON.stringify({
|
|
22
|
+
messageType: 'openclaw_bot_event',
|
|
23
|
+
source: 'client',
|
|
24
|
+
content: msgContent
|
|
25
|
+
})
|
|
26
|
+
)
|
|
27
|
+
dcgLogger(`技能安装: ${JSON.stringify(msgContent)}`)
|
|
36
28
|
}
|
|
37
29
|
}
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// const userId = String(params.userId);
|
|
46
|
-
|
|
47
|
-
// const route = core.channel.routing.resolveAgentRoute({
|
|
48
|
-
// cfg: ctx.cfg,
|
|
49
|
-
// channel: "dcgchat",
|
|
50
|
-
// accountId: account.accountId,
|
|
51
|
-
// peer: { kind: "direct", id: userId },
|
|
52
|
-
// });
|
|
53
|
-
|
|
54
|
-
// const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(ctx.cfg);
|
|
55
|
-
// const bodyFormatted = core.channel.reply.formatAgentEnvelope({
|
|
56
|
-
// channel: "书灵墨宝",
|
|
57
|
-
// from: userId,
|
|
58
|
-
// timestamp: new Date(),
|
|
59
|
-
// envelope: envelopeOptions,
|
|
60
|
-
// body: "/new",
|
|
61
|
-
// });
|
|
62
|
-
|
|
63
|
-
// const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
64
|
-
// Body: bodyFormatted,
|
|
65
|
-
// RawBody: "/new",
|
|
66
|
-
// CommandBody: "/new",
|
|
67
|
-
// From: userId,
|
|
68
|
-
// To: userId,
|
|
69
|
-
// SessionKey: route.sessionKey,
|
|
70
|
-
// AccountId: params.sessionId,
|
|
71
|
-
// ChatType: "direct",
|
|
72
|
-
// SenderName: userId,
|
|
73
|
-
// SenderId: userId,
|
|
74
|
-
// Provider: "dcgchat" as const,
|
|
75
|
-
// Surface: "dcgchat" as const,
|
|
76
|
-
// MessageSid: Date.now().toString(),
|
|
77
|
-
// Timestamp: Date.now(),
|
|
78
|
-
// WasMentioned: true,
|
|
79
|
-
// CommandAuthorized: true,
|
|
80
|
-
// OriginatingChannel: "dcgchat" as const,
|
|
81
|
-
// OriginatingTo: `user:${userId}`,
|
|
82
|
-
// });
|
|
83
|
-
|
|
84
|
-
// const noopDispatcher = {
|
|
85
|
-
// sendToolResult: () => false,
|
|
86
|
-
// sendBlockReply: () => false,
|
|
87
|
-
// sendFinalReply: () => false,
|
|
88
|
-
// waitForIdle: async () => {},
|
|
89
|
-
// getQueuedCounts: () => ({ tool: 0, block: 0, final: 0 }),
|
|
90
|
-
// markComplete: () => {},
|
|
91
|
-
// };
|
|
92
|
-
|
|
93
|
-
// await core.channel.reply.withReplyDispatcher({
|
|
94
|
-
// dispatcher: noopDispatcher,
|
|
95
|
-
// run: () =>
|
|
96
|
-
// core.channel.reply.dispatchReplyFromConfig({
|
|
97
|
-
// ctx: ctxPayload,
|
|
98
|
-
// cfg: ctx.cfg,
|
|
99
|
-
// dispatcher: noopDispatcher,
|
|
100
|
-
// }),
|
|
101
|
-
// });
|
|
102
|
-
|
|
103
|
-
// log(`dcgchat: /new command dispatched silently after skill install`);
|
|
104
|
-
// } catch (err) {
|
|
105
|
-
// logDcgchat.error(`sendNewSessionCommand failed: ${err}`);
|
|
106
|
-
// }
|
|
107
|
-
// }
|
|
108
|
-
|
|
109
|
-
export async function installSkill(params: ISkillParams, msgContent: Record<string, any>, ctx?: SkillContext) {
|
|
110
|
-
const { path: cdnUrl, code } = params;
|
|
111
|
-
const workspacePath = getWorkspaceDir();
|
|
112
|
-
|
|
113
|
-
const skillDir = path.join(workspacePath, 'skills', code);
|
|
114
|
-
|
|
31
|
+
export async function installSkill(params: ISkillParams, msgContent: Record<string, any>) {
|
|
32
|
+
const { path: cdnUrl, code } = params
|
|
33
|
+
const workspacePath = getWorkspaceDir()
|
|
34
|
+
|
|
35
|
+
const skillDir = path.join(workspacePath, 'skills', code)
|
|
36
|
+
|
|
115
37
|
// 确保 skills 目录存在
|
|
116
|
-
const skillsDir = path.join(workspacePath, 'skills')
|
|
38
|
+
const skillsDir = path.join(workspacePath, 'skills')
|
|
117
39
|
if (!fs.existsSync(skillsDir)) {
|
|
118
|
-
fs.mkdirSync(skillsDir, { recursive: true })
|
|
40
|
+
fs.mkdirSync(skillsDir, { recursive: true })
|
|
119
41
|
}
|
|
120
42
|
// 如果目标目录已存在,先删除
|
|
121
43
|
if (fs.existsSync(skillDir)) {
|
|
122
|
-
fs.rmSync(skillDir, { recursive: true, force: true })
|
|
44
|
+
fs.rmSync(skillDir, { recursive: true, force: true })
|
|
123
45
|
}
|
|
124
|
-
|
|
46
|
+
|
|
125
47
|
try {
|
|
126
48
|
// 下载 zip 文件
|
|
127
49
|
const response = await axios({
|
|
128
50
|
method: 'get',
|
|
129
51
|
url: cdnUrl,
|
|
130
52
|
responseType: 'stream'
|
|
131
|
-
})
|
|
53
|
+
})
|
|
132
54
|
// 创建目标目录
|
|
133
|
-
fs.mkdirSync(skillDir, { recursive: true })
|
|
55
|
+
fs.mkdirSync(skillDir, { recursive: true })
|
|
134
56
|
// 解压文件到目标目录,跳过顶层文件夹
|
|
135
57
|
await new Promise((resolve, reject) => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
58
|
+
const tasks: Promise<void>[] = []
|
|
59
|
+
let rootDir: string | null = null
|
|
60
|
+
let hasError = false
|
|
61
|
+
|
|
62
|
+
response.data
|
|
63
|
+
.pipe(unzipper.Parse())
|
|
64
|
+
.on('entry', (entry: any) => {
|
|
65
|
+
if (hasError) {
|
|
66
|
+
entry.autodrain()
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const flags = entry.props?.flags ?? 0
|
|
72
|
+
const isUtf8 = (flags & 0x800) !== 0
|
|
73
|
+
let entryPath: string
|
|
74
|
+
if (!isUtf8 && entry.props?.pathBuffer) {
|
|
75
|
+
entryPath = new TextDecoder('gbk').decode(entry.props.pathBuffer)
|
|
76
|
+
} else {
|
|
77
|
+
entryPath = entry.path
|
|
78
|
+
}
|
|
79
|
+
const pathParts = entryPath.split('/')
|
|
80
|
+
|
|
81
|
+
// 检测根目录
|
|
82
|
+
if (!rootDir && pathParts.length > 1) {
|
|
83
|
+
rootDir = pathParts[0]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let newPath = entryPath
|
|
87
|
+
|
|
88
|
+
// 移除顶层文件夹
|
|
89
|
+
if (rootDir && entryPath.startsWith(rootDir + '/')) {
|
|
90
|
+
newPath = entryPath.slice(rootDir.length + 1)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!newPath) {
|
|
94
|
+
entry.autodrain()
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const targetPath = path.join(skillDir, newPath)
|
|
99
|
+
|
|
100
|
+
if (entry.type === 'Directory') {
|
|
101
|
+
fs.mkdirSync(targetPath, { recursive: true })
|
|
102
|
+
entry.autodrain()
|
|
103
|
+
} else {
|
|
104
|
+
const parentDir = path.dirname(targetPath)
|
|
105
|
+
fs.mkdirSync(parentDir, { recursive: true })
|
|
106
|
+
const writeStream = fs.createWriteStream(targetPath)
|
|
107
|
+
const task = pipeline(entry, writeStream).catch((err) => {
|
|
108
|
+
hasError = true
|
|
109
|
+
throw new Error(`解压文件失败 ${entryPath}: ${err.message}`)
|
|
110
|
+
})
|
|
111
|
+
tasks.push(task)
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
hasError = true
|
|
115
|
+
entry.autodrain()
|
|
116
|
+
reject(new Error(`处理entry失败: ${err}`))
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
.on('close', async () => {
|
|
120
|
+
try {
|
|
121
|
+
await Promise.all(tasks)
|
|
122
|
+
resolve(null)
|
|
123
|
+
} catch (err) {
|
|
124
|
+
reject(err)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
.on('error', (err: { message: any }) => {
|
|
128
|
+
hasError = true
|
|
129
|
+
reject(new Error(`解压流错误: ${err.message}`))
|
|
130
|
+
})
|
|
196
131
|
})
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
await Promise.all(tasks);
|
|
200
|
-
resolve(null);
|
|
201
|
-
} catch (err) {
|
|
202
|
-
reject(err);
|
|
203
|
-
}
|
|
204
|
-
})
|
|
205
|
-
.on("error", (err: { message: any; }) => {
|
|
206
|
-
hasError = true;
|
|
207
|
-
reject(new Error(`解压流错误: ${err.message}`));
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
sendEvent({ ...msgContent, status: 'ok' })
|
|
211
|
-
// if (ctx) {
|
|
212
|
-
// await sendNewSessionCommand(ctx);
|
|
213
|
-
// }
|
|
132
|
+
sendEvent({ ...msgContent, status: 'ok' })
|
|
214
133
|
} catch (error) {
|
|
215
134
|
// 如果安装失败,清理目录
|
|
216
135
|
if (fs.existsSync(skillDir)) {
|
|
217
|
-
fs.rmSync(skillDir, { recursive: true, force: true })
|
|
136
|
+
fs.rmSync(skillDir, { recursive: true, force: true })
|
|
218
137
|
}
|
|
219
138
|
sendEvent({ ...msgContent, status: 'fail' })
|
|
220
139
|
}
|
|
221
140
|
}
|
|
222
141
|
|
|
223
|
-
export function uninstallSkill(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
142
|
+
export function uninstallSkill(
|
|
143
|
+
params: Omit<ISkillParams, 'path'>,
|
|
144
|
+
msgContent: Record<string, any>
|
|
145
|
+
) {
|
|
146
|
+
const { code } = params
|
|
147
|
+
|
|
148
|
+
const workspacePath = getWorkspaceDir()
|
|
227
149
|
if (!workspacePath) {
|
|
228
150
|
sendEvent({ ...msgContent, status: 'ok' })
|
|
229
151
|
}
|
|
230
|
-
|
|
231
|
-
const skillDir = path.join(workspacePath, 'skills', code)
|
|
232
|
-
|
|
152
|
+
|
|
153
|
+
const skillDir = path.join(workspacePath, 'skills', code)
|
|
154
|
+
|
|
233
155
|
if (fs.existsSync(skillDir)) {
|
|
234
|
-
fs.rmSync(skillDir, { recursive: true, force: true })
|
|
156
|
+
fs.rmSync(skillDir, { recursive: true, force: true })
|
|
235
157
|
sendEvent({ ...msgContent, status: 'ok' })
|
|
236
158
|
} else {
|
|
237
159
|
sendEvent({ ...msgContent, status: 'ok' })
|
|
238
160
|
}
|
|
239
|
-
}
|
|
161
|
+
}
|