@dcrays/dcgchat-test 0.6.10 → 0.6.12
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 +167 -0
- package/index.ts +35 -0
- package/openclaw.plugin.json +12 -41
- package/package.json +35 -8
- package/src/agent.ts +568 -0
- package/src/bot.ts +646 -0
- package/src/channel/index.ts +329 -0
- package/src/channel/outboundMedia.ts +144 -0
- package/src/channel/outboundTarget.ts +62 -0
- package/src/channel/uploadMediaUrl.ts +76 -0
- package/src/cron/message.ts +114 -0
- package/src/cron/params.ts +164 -0
- package/src/cron/toolGuard.ts +466 -0
- package/src/cron/types.ts +15 -0
- package/src/gateway/index.ts +430 -0
- package/src/gateway/security.ts +95 -0
- package/src/gateway/socket.ts +256 -0
- package/src/libs/ali-oss-6.23.0.tgz +0 -0
- package/src/libs/axios-1.13.6.tgz +0 -0
- package/src/libs/md5-2.3.0.tgz +0 -0
- package/src/libs/mime-types-3.0.2.tgz +0 -0
- package/src/libs/unzipper-0.12.3.tgz +0 -0
- package/src/libs/ws-8.19.0.tgz +0 -0
- package/src/monitor.ts +269 -0
- package/src/request/api.ts +80 -0
- package/src/request/oss.ts +200 -0
- package/src/request/request.ts +191 -0
- package/src/request/userInfo.ts +44 -0
- package/src/session.ts +28 -0
- package/src/skill.ts +114 -0
- package/src/tool.ts +603 -0
- package/src/tools/messageTool.ts +334 -0
- package/src/tools/toolCallGuard.ts +327 -0
- package/src/transport.ts +217 -0
- package/src/types.ts +139 -0
- package/src/utils/agentErrors.ts +116 -0
- package/src/utils/constant.ts +60 -0
- package/src/utils/env-config.ts +19 -0
- package/src/utils/formatLlmInputEvent.ts +48 -0
- package/src/utils/gatewayMsgHandler.ts +147 -0
- package/src/utils/global.ts +309 -0
- package/src/utils/inboundTurnState.ts +66 -0
- package/src/utils/log.ts +77 -0
- package/src/utils/mediaAttached.ts +244 -0
- package/src/utils/mediaEmitter.ts +54 -0
- package/src/utils/outboundAssistantText.ts +117 -0
- package/src/utils/params.ts +104 -0
- package/src/utils/passTxt.ts +4 -0
- package/src/utils/resolveRegisterConfig.ts +33 -0
- package/src/utils/searchFile.ts +228 -0
- package/src/utils/sessionState.ts +137 -0
- package/src/utils/sessionTermination.ts +228 -0
- package/src/utils/streamMerge.ts +150 -0
- package/src/utils/subagentRunMap.ts +71 -0
- package/src/utils/undiciFetchInterceptor.ts +346 -0
- package/src/utils/workspaceFilePaths.ts +18 -0
- package/src/utils/wsMessageHandler.ts +124 -0
- package/src/utils/zipExtract.ts +97 -0
- package/src/utils/zipPath.ts +24 -0
- package/index.js +0 -304
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { extname } from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import OSS from 'ali-oss'
|
|
5
|
+
import { getStsToken, getUserToken } from './api.js'
|
|
6
|
+
import { dcgLogger } from '../utils/log.js'
|
|
7
|
+
|
|
8
|
+
/** 分片大小:OSS 要求每片 ≥100 KB(最后一片可更小) */
|
|
9
|
+
const MULTIPART_PART_SIZE = 1024 * 1024
|
|
10
|
+
|
|
11
|
+
/** ali-oss 默认 timeout 为 60s,大文件单 PUT 或慢网易触发 ResponseTimeoutError */
|
|
12
|
+
const OSS_HTTP_TIMEOUT_MS = 15 * 60 * 1000
|
|
13
|
+
|
|
14
|
+
/** 归一化入参,避免 file://、包装对象、TypedArray 等导致 SDK 识别失败 */
|
|
15
|
+
function coerceOssFileInput(input: File | string | Buffer): File | string | Buffer {
|
|
16
|
+
if (typeof input === 'string') {
|
|
17
|
+
const t = input.trim()
|
|
18
|
+
if (t.startsWith('file:')) {
|
|
19
|
+
try {
|
|
20
|
+
return fileURLToPath(t)
|
|
21
|
+
} catch {
|
|
22
|
+
return input
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return input
|
|
26
|
+
}
|
|
27
|
+
if (Buffer.isBuffer(input)) {
|
|
28
|
+
return input
|
|
29
|
+
}
|
|
30
|
+
if (input && typeof input === 'object') {
|
|
31
|
+
if (ArrayBuffer.isView(input) && !(input instanceof DataView) && !Buffer.isBuffer(input)) {
|
|
32
|
+
const v = input as ArrayBufferView
|
|
33
|
+
return Buffer.from(v.buffer, v.byteOffset, v.byteLength)
|
|
34
|
+
}
|
|
35
|
+
const o = input as unknown as Record<string, unknown>
|
|
36
|
+
const p = o.path ?? o.filePath
|
|
37
|
+
if (typeof p === 'string' && p.trim()) return p.trim()
|
|
38
|
+
}
|
|
39
|
+
return input
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 常见可在浏览器内联预览的类型(避免一律 application/octet-stream 触发下载) */
|
|
43
|
+
const PREVIEW_EXT_MIME: Record<string, string> = {
|
|
44
|
+
'.jpg': 'image/jpeg',
|
|
45
|
+
'.jpeg': 'image/jpeg',
|
|
46
|
+
'.png': 'image/png',
|
|
47
|
+
'.gif': 'image/gif',
|
|
48
|
+
'.webp': 'image/webp',
|
|
49
|
+
'.bmp': 'image/bmp',
|
|
50
|
+
'.svg': 'image/svg+xml',
|
|
51
|
+
'.ico': 'image/x-icon',
|
|
52
|
+
'.avif': 'image/avif',
|
|
53
|
+
'.heic': 'image/heic',
|
|
54
|
+
'.heif': 'image/heif',
|
|
55
|
+
'.pdf': 'application/pdf',
|
|
56
|
+
'.mp4': 'video/mp4',
|
|
57
|
+
'.webm': 'video/webm',
|
|
58
|
+
'.mov': 'video/quicktime',
|
|
59
|
+
'.mp3': 'audio/mpeg',
|
|
60
|
+
'.wav': 'audio/wav',
|
|
61
|
+
'.ogg': 'audio/ogg',
|
|
62
|
+
'.opus': 'audio/opus',
|
|
63
|
+
'.m4a': 'audio/mp4',
|
|
64
|
+
'.aac': 'audio/aac',
|
|
65
|
+
'.flac': 'audio/flac',
|
|
66
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
67
|
+
'.log': 'text/plain; charset=utf-8',
|
|
68
|
+
'.csv': 'text/csv; charset=utf-8',
|
|
69
|
+
'.html': 'text/html; charset=utf-8',
|
|
70
|
+
'.htm': 'text/html; charset=utf-8',
|
|
71
|
+
'.css': 'text/css; charset=utf-8',
|
|
72
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
73
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
74
|
+
'.json': 'application/json; charset=utf-8',
|
|
75
|
+
'.xml': 'application/xml; charset=utf-8',
|
|
76
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
77
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
78
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
79
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function mimeFromPathOrName(pathOrName: string): string | undefined {
|
|
83
|
+
const base = pathOrName.split(/[/\\]/).pop() ?? pathOrName
|
|
84
|
+
const ext = extname(base).toLowerCase()
|
|
85
|
+
return ext ? PREVIEW_EXT_MIME[ext] : undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 解析上传 Content-Type,并配合 Content-Disposition: inline 便于直链预览 */
|
|
89
|
+
function resolveMime(input: File | string | Buffer, fileNameHint?: string): string {
|
|
90
|
+
if (typeof input === 'string') {
|
|
91
|
+
return mimeFromPathOrName(input) ?? 'application/octet-stream'
|
|
92
|
+
}
|
|
93
|
+
if (Buffer.isBuffer(input)) {
|
|
94
|
+
if (fileNameHint) {
|
|
95
|
+
const fromName = mimeFromPathOrName(fileNameHint)
|
|
96
|
+
if (fromName) return fromName
|
|
97
|
+
}
|
|
98
|
+
return 'application/octet-stream'
|
|
99
|
+
}
|
|
100
|
+
const declared = input.type?.trim()
|
|
101
|
+
if (declared && declared !== 'application/octet-stream') {
|
|
102
|
+
return declared
|
|
103
|
+
}
|
|
104
|
+
const name = typeof input.name === 'string' && input.name ? input.name : ''
|
|
105
|
+
if (name) {
|
|
106
|
+
const fromName = mimeFromPathOrName(name)
|
|
107
|
+
if (fromName) return fromName
|
|
108
|
+
}
|
|
109
|
+
return declared || 'application/octet-stream'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 将 File/路径/Buffer 转为 ali-oss 接受的类型。
|
|
114
|
+
* 本地路径保持为字符串:put 内部用 contentLength + ReadStream,大文件也稳定。
|
|
115
|
+
*/
|
|
116
|
+
async function toUploadContent(input: File | string | Buffer): Promise<{ content: Buffer | string; fileName: string }> {
|
|
117
|
+
if (Buffer.isBuffer(input)) {
|
|
118
|
+
return { content: input, fileName: 'file' }
|
|
119
|
+
}
|
|
120
|
+
if (typeof input === 'string') {
|
|
121
|
+
return {
|
|
122
|
+
content: input,
|
|
123
|
+
fileName: input.split(/[/\\]/).pop() ?? 'file'
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const buf = Buffer.from(await input.arrayBuffer())
|
|
127
|
+
const n = (input as { name?: string }).name
|
|
128
|
+
return { content: buf, fileName: typeof n === 'string' && n ? n : 'file' }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type OssUploadOptions = {
|
|
132
|
+
/** 分片上传进度,p 为 0~1(仅大 Buffer 分片时触发) */
|
|
133
|
+
onProgress?: (p: number) => void
|
|
134
|
+
/** HTTP 超时(毫秒),覆盖默认 15 分钟;可传 `30 * 60 * 1000` 等 */
|
|
135
|
+
timeoutMs?: number
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const ossUpload = async (
|
|
139
|
+
rawFile: File | string | Buffer,
|
|
140
|
+
botToken: string,
|
|
141
|
+
isPrivate: 0 | 1 = 1,
|
|
142
|
+
uploadOptions?: OssUploadOptions
|
|
143
|
+
) => {
|
|
144
|
+
await getUserToken(botToken)
|
|
145
|
+
|
|
146
|
+
const file = coerceOssFileInput(rawFile)
|
|
147
|
+
const { content, fileName } = await toUploadContent(file)
|
|
148
|
+
const data = await getStsToken(fileName, botToken, isPrivate)
|
|
149
|
+
const mime = resolveMime(file, fileName)
|
|
150
|
+
const onProgress = uploadOptions?.onProgress
|
|
151
|
+
|
|
152
|
+
const options: OSS.Options = {
|
|
153
|
+
// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
|
|
154
|
+
accessKeyId: data.tempAccessKeyId,
|
|
155
|
+
accessKeySecret: data.tempAccessKeySecret,
|
|
156
|
+
// 从STS服务获取的安全令牌(SecurityToken)。
|
|
157
|
+
stsToken: data.tempSecurityToken,
|
|
158
|
+
// 填写Bucket名称。
|
|
159
|
+
bucket: data.bucket,
|
|
160
|
+
endpoint: data.endPoint,
|
|
161
|
+
region: data.region,
|
|
162
|
+
secure: true,
|
|
163
|
+
cname: true,
|
|
164
|
+
authorizationV4: true,
|
|
165
|
+
timeout: uploadOptions?.timeoutMs ?? OSS_HTTP_TIMEOUT_MS
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const client = new OSS(options)
|
|
169
|
+
|
|
170
|
+
const name = `${data.uploadDir}${data.ossFileKey}`
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
let objectResult: OSS.PutObjectResult | OSS.CompleteMultipartUploadResult
|
|
174
|
+
|
|
175
|
+
const multipartUploadOptions: OSS.MultipartUploadOptions = {
|
|
176
|
+
progress: (p: number) => {
|
|
177
|
+
onProgress?.(p)
|
|
178
|
+
},
|
|
179
|
+
parallel: 4,
|
|
180
|
+
partSize: MULTIPART_PART_SIZE,
|
|
181
|
+
mime,
|
|
182
|
+
/** 直链打开时优先内联展示,而非附件下载 */
|
|
183
|
+
headers: {
|
|
184
|
+
'Content-Disposition': 'inline'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
objectResult = await client.multipartUpload(name, content, multipartUploadOptions)
|
|
188
|
+
|
|
189
|
+
if (objectResult?.res?.status !== 200) {
|
|
190
|
+
dcgLogger(`OSS 上传失败, ${objectResult?.res?.status}`, 'error')
|
|
191
|
+
return ''
|
|
192
|
+
}
|
|
193
|
+
const requestUrls = objectResult?.res?.requestUrls || []
|
|
194
|
+
const url = requestUrls[0] || ''
|
|
195
|
+
dcgLogger(`OSS 上传成功, ${isPrivate === 1 ? objectResult.name || url : url}`)
|
|
196
|
+
return isPrivate === 1 ? objectResult.name || url : url
|
|
197
|
+
} catch (error) {
|
|
198
|
+
dcgLogger(`OSS 上传失败: ${error}`, 'error')
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
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 { getEffectiveMsgParams } from '../utils/params.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') ? path : `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
|
|
43
|
+
const method = (config.method ?? 'GET').toUpperCase()
|
|
44
|
+
const headers = config.headers ?? {}
|
|
45
|
+
const parts = ['curl', '-X', method, `'${url}'`]
|
|
46
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
47
|
+
if (v !== undefined && v !== '') {
|
|
48
|
+
parts.push('-H', `'${k}: ${v}'`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (method !== 'GET' && config.data !== undefined) {
|
|
52
|
+
const body = typeof config.data === 'string' ? config.data : JSON.stringify(config.data)
|
|
53
|
+
parts.push('-d', `'${body.replace(/'/g, "'\\''")}'`)
|
|
54
|
+
}
|
|
55
|
+
return parts.join(' ')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 生成签名
|
|
60
|
+
* @param {Object} body 请求体
|
|
61
|
+
* @param {number} timestamp 时间戳
|
|
62
|
+
* @param {string} path 请求地址
|
|
63
|
+
* @param {'production' | 'test' | 'develop'} ENV 请求环境
|
|
64
|
+
* @param {string} version 版本号
|
|
65
|
+
* @returns {string} 大写 MD5 签名
|
|
66
|
+
*/
|
|
67
|
+
export function getSignature(
|
|
68
|
+
body: Record<string, unknown>,
|
|
69
|
+
timestamp: number,
|
|
70
|
+
path: string,
|
|
71
|
+
ENV: 'production' | 'test' | 'develop',
|
|
72
|
+
version: string = '1.0.0'
|
|
73
|
+
) {
|
|
74
|
+
// 1. 构造 map
|
|
75
|
+
const map = { timestamp, path, version, ...body }
|
|
76
|
+
// 2. 按 key 进行自然排序
|
|
77
|
+
const sortedKeys = Object.keys(map).sort()
|
|
78
|
+
// 3. 拼接 key + value
|
|
79
|
+
const signStr =
|
|
80
|
+
sortedKeys
|
|
81
|
+
.map((key) => {
|
|
82
|
+
const val = map[key as keyof typeof map]
|
|
83
|
+
return val === undefined ? '' : `${key}${typeof val === 'object' ? JSON.stringify(val) : val}`
|
|
84
|
+
})
|
|
85
|
+
.join('') + signKey[ENV]
|
|
86
|
+
// 4. MD5 加密并转大写
|
|
87
|
+
return md5(signStr).toUpperCase()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildHeaders(data: Record<string, unknown>, url: string, userToken?: string) {
|
|
91
|
+
const timestamp = Date.now()
|
|
92
|
+
|
|
93
|
+
const headers: Record<string, string | number> = {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
appKey: appKey[ENV],
|
|
96
|
+
sign: getSignature(data, timestamp, url, ENV, version),
|
|
97
|
+
timestamp,
|
|
98
|
+
version
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 如果提供了 userToken,添加到 headers
|
|
102
|
+
if (userToken) {
|
|
103
|
+
headers.authorization = userToken
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return headers
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const axiosInstance = axios.create({
|
|
110
|
+
baseURL: apiUrlMap[ENV],
|
|
111
|
+
timeout: 60000
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// 请求拦截器:自动注入 userToken
|
|
115
|
+
axiosInstance.interceptors.request.use(
|
|
116
|
+
(config) => {
|
|
117
|
+
// 如果请求配置中已经有 authorization,优先使用
|
|
118
|
+
if (config.headers?.authorization) {
|
|
119
|
+
return config
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 从请求上下文中获取 botToken(需要在调用时设置)
|
|
123
|
+
const botToken = (config as any).__botToken as string | undefined
|
|
124
|
+
if (botToken) {
|
|
125
|
+
const cachedToken = getUserTokenCache(botToken)
|
|
126
|
+
if (cachedToken) {
|
|
127
|
+
config.headers = config.headers || {}
|
|
128
|
+
config.headers.authorization = cachedToken
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return config
|
|
133
|
+
},
|
|
134
|
+
(error) => {
|
|
135
|
+
return Promise.reject(error)
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// 响应拦截器:打印 curl 便于调试
|
|
140
|
+
axiosInstance.interceptors.response.use(
|
|
141
|
+
(response) => {
|
|
142
|
+
return response.data
|
|
143
|
+
},
|
|
144
|
+
(error) => {
|
|
145
|
+
const config = error.config ?? {}
|
|
146
|
+
const curl = toCurl(config)
|
|
147
|
+
dcgLogger(`[request] curl for backend (failed request): ${curl}`, 'error')
|
|
148
|
+
return Promise.reject(error)
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* POST 请求(支持可选的 userToken 和 botToken)
|
|
154
|
+
* @param url 请求路径
|
|
155
|
+
* @param data 请求体
|
|
156
|
+
* @param options 可选配置
|
|
157
|
+
* @param options.userToken 直接提供的 userToken(优先级最高)
|
|
158
|
+
* @param options.botToken 用于从缓存获取 userToken 的 botToken
|
|
159
|
+
*/
|
|
160
|
+
export function post<T = Record<string, unknown>, R = unknown>(
|
|
161
|
+
url: string,
|
|
162
|
+
data: T,
|
|
163
|
+
options?: {
|
|
164
|
+
userToken?: string
|
|
165
|
+
botToken?: string
|
|
166
|
+
}
|
|
167
|
+
): Promise<IResponse<R>> {
|
|
168
|
+
const params = getEffectiveMsgParams() || { appId: 100 }
|
|
169
|
+
const config: any = {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
url,
|
|
172
|
+
data: {
|
|
173
|
+
...data,
|
|
174
|
+
_appId: params.appId
|
|
175
|
+
},
|
|
176
|
+
headers: buildHeaders(
|
|
177
|
+
{
|
|
178
|
+
...data,
|
|
179
|
+
_appId: params.appId
|
|
180
|
+
} as Record<string, unknown>,
|
|
181
|
+
url,
|
|
182
|
+
options?.userToken
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 将 botToken 附加到配置中,供请求拦截器使用
|
|
187
|
+
if (options?.botToken) {
|
|
188
|
+
config.__botToken = options.botToken
|
|
189
|
+
}
|
|
190
|
+
return axiosInstance.request(config)
|
|
191
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* userToken 缓存管理模块
|
|
3
|
+
* 负责维护 botToken -> userToken 的映射关系,支持自动过期
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// userToken 缓存配置
|
|
7
|
+
const TOKEN_CACHE_DURATION = 60 * 60 * 1000 // 1小时
|
|
8
|
+
|
|
9
|
+
type TokenCacheEntry = {
|
|
10
|
+
token: string
|
|
11
|
+
expiresAt: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 内存缓存:botToken -> { token, expiresAt }
|
|
15
|
+
const tokenCache = new Map<string, TokenCacheEntry>()
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 设置 userToken 缓存
|
|
19
|
+
* @param botToken 机器人 token
|
|
20
|
+
* @param userToken 用户 token
|
|
21
|
+
*/
|
|
22
|
+
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
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 获取 userToken 缓存(自动检查过期)
|
|
29
|
+
* @param botToken 机器人 token
|
|
30
|
+
* @returns userToken 或 null(未找到或已过期)
|
|
31
|
+
*/
|
|
32
|
+
export function getUserTokenCache(botToken: string): string | null {
|
|
33
|
+
const entry = tokenCache.get(botToken)
|
|
34
|
+
if (!entry) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 检查是否过期
|
|
39
|
+
if (Date.now() >= entry.expiresAt) {
|
|
40
|
+
tokenCache.delete(botToken)
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
return entry.token
|
|
44
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { sendGatewayRpc } from './gateway/socket.js'
|
|
2
|
+
import { getSessionKey } from './utils/global.js'
|
|
3
|
+
import { dcgLogger } from './utils/log.js'
|
|
4
|
+
import { cleanupTerminatedSession } from './utils/sessionTermination.js'
|
|
5
|
+
|
|
6
|
+
interface TSession {
|
|
7
|
+
agent_id: string
|
|
8
|
+
session_id: string
|
|
9
|
+
agent_clone_code?: string
|
|
10
|
+
account_id: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const onRemoveSession = async ({ agent_id, session_id, agent_clone_code, account_id }: TSession) => {
|
|
14
|
+
const sessionKey = getSessionKey({ agent_id, session_id, agent_clone_code }, account_id)
|
|
15
|
+
if (!session_id) {
|
|
16
|
+
dcgLogger('onRemoveSession: empty session_id', 'error')
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await sendGatewayRpc({ method: 'sessions.delete', params: { key: sessionKey, deleteTranscript: true } })
|
|
22
|
+
} catch (e) {
|
|
23
|
+
const error = e instanceof Error ? e : new Error(String(e))
|
|
24
|
+
dcgLogger(`onRemoveSession: ${error.message}`, 'error')
|
|
25
|
+
} finally {
|
|
26
|
+
cleanupTerminatedSession(sessionKey)
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/skill.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import fsPromise from 'fs/promises'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { getWorkspaceDir } from './utils/global.js'
|
|
5
|
+
import { getWsConnection } from './utils/global.js'
|
|
6
|
+
import { dcgLogger } from './utils/log.js'
|
|
7
|
+
import { isWsOpen } from './transport.js'
|
|
8
|
+
import { sendGatewayRpc } from './gateway/socket.js'
|
|
9
|
+
import { extractZipBufferToDirectory } from './utils/zipExtract.js'
|
|
10
|
+
import { retryWithBackoff } from './utils/constant.js'
|
|
11
|
+
|
|
12
|
+
type ISkillParams = {
|
|
13
|
+
path: string
|
|
14
|
+
code: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SkillEventMessage {
|
|
18
|
+
event_type?: string
|
|
19
|
+
operation_type?: string
|
|
20
|
+
skill_url?: string
|
|
21
|
+
skill_code?: string
|
|
22
|
+
skill_id?: string
|
|
23
|
+
bot_token?: string
|
|
24
|
+
websocket_trace_id?: string
|
|
25
|
+
status?: string
|
|
26
|
+
[key: string]: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sendEvent(msgContent: SkillEventMessage) {
|
|
30
|
+
const ws = getWsConnection()
|
|
31
|
+
if (isWsOpen()) {
|
|
32
|
+
const msg = JSON.stringify({
|
|
33
|
+
messageType: 'openclaw_bot_event',
|
|
34
|
+
source: 'client',
|
|
35
|
+
content: msgContent
|
|
36
|
+
});
|
|
37
|
+
ws?.send(msg);
|
|
38
|
+
dcgLogger(`[Send]技能安装: ${msg}`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SKILL_CODE_REGEXP = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
43
|
+
|
|
44
|
+
function normalizeCode(code: string) {
|
|
45
|
+
const trimmedCode = code?.trim() || ''
|
|
46
|
+
if (!trimmedCode) {
|
|
47
|
+
dcgLogger('skill code is empty', 'error')
|
|
48
|
+
return ''
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!SKILL_CODE_REGEXP.test(trimmedCode)) {
|
|
52
|
+
dcgLogger(`invalid skill code: ${trimmedCode}`, 'error')
|
|
53
|
+
return ''
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return trimmedCode
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function installSkill(params: ISkillParams, msgContent: SkillEventMessage) {
|
|
60
|
+
const { path: cdnUrl } = params
|
|
61
|
+
|
|
62
|
+
const code = normalizeCode(params.code)
|
|
63
|
+
if (!code) {
|
|
64
|
+
sendEvent({ ...msgContent, status: 'fail' })
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const workspacePath = getWorkspaceDir()
|
|
69
|
+
|
|
70
|
+
// 确保 skills 目录存在
|
|
71
|
+
const skillsDir = path.join(workspacePath, 'skills')
|
|
72
|
+
await fsPromise.mkdir(skillsDir, { recursive: true });
|
|
73
|
+
|
|
74
|
+
// 如果目标目录已存在,先删除
|
|
75
|
+
const skillDir = path.join(skillsDir, code)
|
|
76
|
+
await fsPromise.rm(skillDir, { recursive: true, force: true }).catch((e) =>
|
|
77
|
+
dcgLogger(`skill cleanup before install failed: ${String(e)}`)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const response = await retryWithBackoff(() => axios({
|
|
82
|
+
method: 'get',
|
|
83
|
+
url: cdnUrl,
|
|
84
|
+
responseType: 'arraybuffer'
|
|
85
|
+
}), 3)
|
|
86
|
+
await fsPromise.mkdir(skillDir, { recursive: true })
|
|
87
|
+
// 与 extractZipBufferToDirectory 一致:仅当全体条目共用一个顶层目录时剥掉该层;根上多文件/多文件夹并列则不剥
|
|
88
|
+
await extractZipBufferToDirectory(Buffer.from(response.data), skillDir)
|
|
89
|
+
sendEvent({ ...msgContent, status: 'ok' })
|
|
90
|
+
sendGatewayRpc({ method: 'skills.status', params: {} })
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// 如果安装失败,清理目录
|
|
93
|
+
await fsPromise.rm(skillDir, { recursive: true, force: true }).catch((e) =>
|
|
94
|
+
dcgLogger(`skill cleanup after install failure failed: ${String(e)}`)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
sendEvent({ ...msgContent, status: 'fail' })
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function uninstallSkill(params: Omit<ISkillParams, 'path'>, msgContent: SkillEventMessage) {
|
|
102
|
+
const code = normalizeCode(params.code)
|
|
103
|
+
if (!code) {
|
|
104
|
+
sendEvent({ ...msgContent, status: 'fail' })
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const workspacePath = getWorkspaceDir()
|
|
109
|
+
if (workspacePath) {
|
|
110
|
+
const skillDir = path.join(workspacePath, 'skills', code)
|
|
111
|
+
await fsPromise.rm(skillDir, { recursive: true, force: true }).catch((e) => dcgLogger(`skill cleanup failed: ${String(e)}`))
|
|
112
|
+
}
|
|
113
|
+
sendEvent({ ...msgContent, status: 'ok' })
|
|
114
|
+
}
|