@dcrays/dcgchat 0.4.29 → 0.5.1
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.js +292 -0
- package/openclaw.plugin.json +17 -1
- package/package.json +18 -13
- package/schemas/gateway-cron-finished.payload.json +39 -0
- package/index.ts +0 -26
- package/src/agent.ts +0 -128
- package/src/bot.ts +0 -500
- package/src/channel.ts +0 -470
- package/src/cron.ts +0 -194
- package/src/cronToolCall.ts +0 -202
- package/src/gateway/index.ts +0 -447
- package/src/gateway/security.ts +0 -95
- package/src/gateway/socket.ts +0 -285
- 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 +0 -165
- package/src/request/api.ts +0 -70
- package/src/request/oss.ts +0 -212
- package/src/request/request.ts +0 -192
- package/src/request/userInfo.ts +0 -99
- package/src/session.ts +0 -19
- package/src/sessionTermination.ts +0 -154
- package/src/skill.ts +0 -151
- package/src/tool.ts +0 -422
- package/src/tools/messageTool.ts +0 -224
- package/src/transport.ts +0 -203
- package/src/types.ts +0 -139
- package/src/utils/constant.ts +0 -7
- package/src/utils/gatewayMsgHanlder.ts +0 -55
- package/src/utils/global.ts +0 -160
- package/src/utils/log.ts +0 -15
- package/src/utils/params.ts +0 -88
- package/src/utils/searchFile.ts +0 -228
- package/src/utils/wsMessageHandler.ts +0 -64
- package/src/utils/zipExtract.ts +0 -97
- package/src/utils/zipPath.ts +0 -24
package/src/request/oss.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { stat } from 'node:fs/promises'
|
|
2
|
-
import { extname } from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
4
|
-
// @ts-ignore
|
|
5
|
-
import OSS from 'ali-oss'
|
|
6
|
-
import { getStsToken, getUserToken } from './api.js'
|
|
7
|
-
import { dcgLogger } from '../utils/log.js'
|
|
8
|
-
|
|
9
|
-
/** 仅对内存 Buffer 超过此大小使用分片(本地路径一律走 put,避免 multipart 对类型的限制) */
|
|
10
|
-
const MULTIPART_THRESHOLD_BYTES = 1024 * 1024
|
|
11
|
-
|
|
12
|
-
/** 分片大小:OSS 要求每片 ≥100 KB(最后一片可更小) */
|
|
13
|
-
const MULTIPART_PART_SIZE = 1024 * 1024
|
|
14
|
-
|
|
15
|
-
/** ali-oss 默认 timeout 为 60s,大文件单 PUT 或慢网易触发 ResponseTimeoutError */
|
|
16
|
-
const OSS_HTTP_TIMEOUT_MS = 15 * 60 * 1000
|
|
17
|
-
|
|
18
|
-
/** 归一化入参,避免 file://、包装对象、TypedArray 等导致 SDK 识别失败 */
|
|
19
|
-
function coerceOssFileInput(input: File | string | Buffer): File | string | Buffer {
|
|
20
|
-
if (typeof input === 'string') {
|
|
21
|
-
const t = input.trim()
|
|
22
|
-
if (t.startsWith('file:')) {
|
|
23
|
-
try {
|
|
24
|
-
return fileURLToPath(t)
|
|
25
|
-
} catch {
|
|
26
|
-
return input
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return input
|
|
30
|
-
}
|
|
31
|
-
if (Buffer.isBuffer(input)) {
|
|
32
|
-
return input
|
|
33
|
-
}
|
|
34
|
-
if (input && typeof input === 'object') {
|
|
35
|
-
if (ArrayBuffer.isView(input) && !(input instanceof DataView) && !Buffer.isBuffer(input)) {
|
|
36
|
-
const v = input as ArrayBufferView
|
|
37
|
-
return Buffer.from(v.buffer, v.byteOffset, v.byteLength)
|
|
38
|
-
}
|
|
39
|
-
const o = input as unknown as Record<string, unknown>
|
|
40
|
-
const p = o.path ?? o.filePath
|
|
41
|
-
if (typeof p === 'string' && p.trim()) return p.trim()
|
|
42
|
-
}
|
|
43
|
-
return input
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function getUploadByteLength(input: File | string | Buffer): Promise<number> {
|
|
47
|
-
if (Buffer.isBuffer(input)) return input.length
|
|
48
|
-
if (typeof input === 'string') {
|
|
49
|
-
const s = await stat(input)
|
|
50
|
-
return s.size
|
|
51
|
-
}
|
|
52
|
-
return input.size
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** 常见可在浏览器内联预览的类型(避免一律 application/octet-stream 触发下载) */
|
|
56
|
-
const PREVIEW_EXT_MIME: Record<string, string> = {
|
|
57
|
-
'.jpg': 'image/jpeg',
|
|
58
|
-
'.jpeg': 'image/jpeg',
|
|
59
|
-
'.png': 'image/png',
|
|
60
|
-
'.gif': 'image/gif',
|
|
61
|
-
'.webp': 'image/webp',
|
|
62
|
-
'.bmp': 'image/bmp',
|
|
63
|
-
'.svg': 'image/svg+xml',
|
|
64
|
-
'.ico': 'image/x-icon',
|
|
65
|
-
'.avif': 'image/avif',
|
|
66
|
-
'.heic': 'image/heic',
|
|
67
|
-
'.heif': 'image/heif',
|
|
68
|
-
'.pdf': 'application/pdf',
|
|
69
|
-
'.mp4': 'video/mp4',
|
|
70
|
-
'.webm': 'video/webm',
|
|
71
|
-
'.mov': 'video/quicktime',
|
|
72
|
-
'.mp3': 'audio/mpeg',
|
|
73
|
-
'.wav': 'audio/wav',
|
|
74
|
-
'.ogg': 'audio/ogg',
|
|
75
|
-
'.opus': 'audio/opus',
|
|
76
|
-
'.m4a': 'audio/mp4',
|
|
77
|
-
'.aac': 'audio/aac',
|
|
78
|
-
'.flac': 'audio/flac',
|
|
79
|
-
'.txt': 'text/plain; charset=utf-8',
|
|
80
|
-
'.log': 'text/plain; charset=utf-8',
|
|
81
|
-
'.csv': 'text/csv; charset=utf-8',
|
|
82
|
-
'.html': 'text/html; charset=utf-8',
|
|
83
|
-
'.htm': 'text/html; charset=utf-8',
|
|
84
|
-
'.css': 'text/css; charset=utf-8',
|
|
85
|
-
'.js': 'text/javascript; charset=utf-8',
|
|
86
|
-
'.mjs': 'text/javascript; charset=utf-8',
|
|
87
|
-
'.json': 'application/json; charset=utf-8',
|
|
88
|
-
'.xml': 'application/xml; charset=utf-8',
|
|
89
|
-
'.md': 'text/markdown; charset=utf-8',
|
|
90
|
-
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
91
|
-
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
92
|
-
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function mimeFromPathOrName(pathOrName: string): string | undefined {
|
|
96
|
-
const base = pathOrName.split(/[/\\]/).pop() ?? pathOrName
|
|
97
|
-
const ext = extname(base).toLowerCase()
|
|
98
|
-
return ext ? PREVIEW_EXT_MIME[ext] : undefined
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** 解析上传 Content-Type,并配合 Content-Disposition: inline 便于直链预览 */
|
|
102
|
-
function resolveMime(input: File | string | Buffer, fileNameHint?: string): string {
|
|
103
|
-
if (typeof input === 'string') {
|
|
104
|
-
return mimeFromPathOrName(input) ?? 'application/octet-stream'
|
|
105
|
-
}
|
|
106
|
-
if (Buffer.isBuffer(input)) {
|
|
107
|
-
if (fileNameHint) {
|
|
108
|
-
const fromName = mimeFromPathOrName(fileNameHint)
|
|
109
|
-
if (fromName) return fromName
|
|
110
|
-
}
|
|
111
|
-
return 'application/octet-stream'
|
|
112
|
-
}
|
|
113
|
-
const declared = input.type?.trim()
|
|
114
|
-
if (declared && declared !== 'application/octet-stream') {
|
|
115
|
-
return declared
|
|
116
|
-
}
|
|
117
|
-
const name = typeof input.name === 'string' && input.name ? input.name : ''
|
|
118
|
-
if (name) {
|
|
119
|
-
const fromName = mimeFromPathOrName(name)
|
|
120
|
-
if (fromName) return fromName
|
|
121
|
-
}
|
|
122
|
-
return declared || 'application/octet-stream'
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* 将 File/路径/Buffer 转为 ali-oss 接受的类型。
|
|
127
|
-
* 本地路径保持为字符串:put 内部用 contentLength + ReadStream,大文件也稳定。
|
|
128
|
-
*/
|
|
129
|
-
async function toUploadContent(input: File | string | Buffer): Promise<{ content: Buffer | string; fileName: string }> {
|
|
130
|
-
if (Buffer.isBuffer(input)) {
|
|
131
|
-
return { content: input, fileName: 'file' }
|
|
132
|
-
}
|
|
133
|
-
if (typeof input === 'string') {
|
|
134
|
-
return {
|
|
135
|
-
content: input,
|
|
136
|
-
fileName: input.split(/[/\\]/).pop() ?? 'file'
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const buf = Buffer.from(await input.arrayBuffer())
|
|
140
|
-
const n = (input as { name?: string }).name
|
|
141
|
-
return { content: buf, fileName: typeof n === 'string' && n ? n : 'file' }
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export type OssUploadOptions = {
|
|
145
|
-
/** 分片上传进度,p 为 0~1(仅大 Buffer 分片时触发) */
|
|
146
|
-
onProgress?: (p: number) => void
|
|
147
|
-
/** HTTP 超时(毫秒),覆盖默认 15 分钟;可传 `30 * 60 * 1000` 等 */
|
|
148
|
-
timeoutMs?: number
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export const ossUpload = async (
|
|
152
|
-
rawFile: File | string | Buffer,
|
|
153
|
-
botToken: string,
|
|
154
|
-
isPrivate: 0 | 1 = 1,
|
|
155
|
-
uploadOptions?: OssUploadOptions
|
|
156
|
-
) => {
|
|
157
|
-
await getUserToken(botToken)
|
|
158
|
-
|
|
159
|
-
const file = coerceOssFileInput(rawFile)
|
|
160
|
-
const { content, fileName } = await toUploadContent(file)
|
|
161
|
-
const data = await getStsToken(fileName, botToken, isPrivate)
|
|
162
|
-
const mime = resolveMime(file, fileName)
|
|
163
|
-
const onProgress = uploadOptions?.onProgress
|
|
164
|
-
|
|
165
|
-
const options: OSS.Options = {
|
|
166
|
-
// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
|
|
167
|
-
accessKeyId: data.tempAccessKeyId,
|
|
168
|
-
accessKeySecret: data.tempAccessKeySecret,
|
|
169
|
-
// 从STS服务获取的安全令牌(SecurityToken)。
|
|
170
|
-
stsToken: data.tempSecurityToken,
|
|
171
|
-
// 填写Bucket名称。
|
|
172
|
-
bucket: data.bucket,
|
|
173
|
-
endpoint: data.endPoint,
|
|
174
|
-
region: data.region,
|
|
175
|
-
secure: true,
|
|
176
|
-
cname: true,
|
|
177
|
-
authorizationV4: true,
|
|
178
|
-
timeout: uploadOptions?.timeoutMs ?? OSS_HTTP_TIMEOUT_MS
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const client = new OSS(options)
|
|
182
|
-
|
|
183
|
-
const name = `${data.uploadDir}${data.ossFileKey}`
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
let objectResult: OSS.PutObjectResult | OSS.CompleteMultipartUploadResult
|
|
187
|
-
|
|
188
|
-
const multipartUploadOptions: OSS.MultipartUploadOptions = {
|
|
189
|
-
progress: (p: number) => {
|
|
190
|
-
onProgress?.(p)
|
|
191
|
-
},
|
|
192
|
-
parallel: 4,
|
|
193
|
-
partSize: MULTIPART_PART_SIZE,
|
|
194
|
-
mime,
|
|
195
|
-
/** 直链打开时优先内联展示,而非附件下载 */
|
|
196
|
-
headers: {
|
|
197
|
-
'Content-Disposition': 'inline'
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
objectResult = await client.multipartUpload(name, content, multipartUploadOptions)
|
|
201
|
-
|
|
202
|
-
if (objectResult?.res?.status !== 200) {
|
|
203
|
-
dcgLogger(`OSS 上传失败, ${objectResult?.res?.status}`)
|
|
204
|
-
}
|
|
205
|
-
const requestUrls = objectResult?.res?.requestUrls || []
|
|
206
|
-
const url = requestUrls[0] || ''
|
|
207
|
-
dcgLogger(`OSS 上传成功, ${isPrivate === 1 ? objectResult.name || url : url}`)
|
|
208
|
-
return isPrivate === 1 ? objectResult.name || url : url
|
|
209
|
-
} catch (error) {
|
|
210
|
-
dcgLogger(`OSS 上传失败: ${error}`, 'error')
|
|
211
|
-
}
|
|
212
|
-
}
|
package/src/request/request.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
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: 10000
|
|
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
|
-
dcgLogger(`[request] auto-injected userToken from cache for botToken=${botToken.slice(0, 10)}...`)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return config
|
|
134
|
-
},
|
|
135
|
-
(error) => {
|
|
136
|
-
return Promise.reject(error)
|
|
137
|
-
}
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
// 响应拦截器:打印 curl 便于调试
|
|
141
|
-
axiosInstance.interceptors.response.use(
|
|
142
|
-
(response) => {
|
|
143
|
-
return response.data
|
|
144
|
-
},
|
|
145
|
-
(error) => {
|
|
146
|
-
const config = error.config ?? {}
|
|
147
|
-
const curl = toCurl(config)
|
|
148
|
-
dcgLogger(`[request] curl for backend (failed request): ${curl}`)
|
|
149
|
-
return Promise.reject(error)
|
|
150
|
-
}
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* POST 请求(支持可选的 userToken 和 botToken)
|
|
155
|
-
* @param url 请求路径
|
|
156
|
-
* @param data 请求体
|
|
157
|
-
* @param options 可选配置
|
|
158
|
-
* @param options.userToken 直接提供的 userToken(优先级最高)
|
|
159
|
-
* @param options.botToken 用于从缓存获取 userToken 的 botToken
|
|
160
|
-
*/
|
|
161
|
-
export function post<T = Record<string, unknown>, R = unknown>(
|
|
162
|
-
url: string,
|
|
163
|
-
data: T,
|
|
164
|
-
options?: {
|
|
165
|
-
userToken?: string
|
|
166
|
-
botToken?: string
|
|
167
|
-
}
|
|
168
|
-
): Promise<IResponse<R>> {
|
|
169
|
-
const params = getEffectiveMsgParams() || { appId: 100 }
|
|
170
|
-
const config: any = {
|
|
171
|
-
method: 'POST',
|
|
172
|
-
url,
|
|
173
|
-
data: {
|
|
174
|
-
...data,
|
|
175
|
-
_appId: params.appId
|
|
176
|
-
},
|
|
177
|
-
headers: buildHeaders(
|
|
178
|
-
{
|
|
179
|
-
...data,
|
|
180
|
-
_appId: params.appId
|
|
181
|
-
} as Record<string, unknown>,
|
|
182
|
-
url,
|
|
183
|
-
options?.userToken
|
|
184
|
-
)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// 将 botToken 附加到配置中,供请求拦截器使用
|
|
188
|
-
if (options?.botToken) {
|
|
189
|
-
config.__botToken = options.botToken
|
|
190
|
-
}
|
|
191
|
-
return axiosInstance.request(config)
|
|
192
|
-
}
|
package/src/request/userInfo.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* userToken 缓存管理模块
|
|
3
|
-
* 负责维护 botToken -> userToken 的映射关系,支持自动过期
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { dcgLogger } from '../utils/log.js'
|
|
7
|
-
|
|
8
|
-
// userToken 缓存配置
|
|
9
|
-
const TOKEN_CACHE_DURATION = 60 * 60 * 1000 // 1小时
|
|
10
|
-
|
|
11
|
-
type TokenCacheEntry = {
|
|
12
|
-
token: string
|
|
13
|
-
expiresAt: number
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// 内存缓存:botToken -> { token, expiresAt }
|
|
17
|
-
const tokenCache = new Map<string, TokenCacheEntry>()
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 设置 userToken 缓存
|
|
21
|
-
* @param botToken 机器人 token
|
|
22
|
-
* @param userToken 用户 token
|
|
23
|
-
*/
|
|
24
|
-
export function setUserTokenCache(botToken: string, userToken: string): void {
|
|
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
|
-
)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 获取 userToken 缓存(自动检查过期)
|
|
34
|
-
* @param botToken 机器人 token
|
|
35
|
-
* @returns userToken 或 null(未找到或已过期)
|
|
36
|
-
*/
|
|
37
|
-
export function getUserTokenCache(botToken: string): string | null {
|
|
38
|
-
const entry = tokenCache.get(botToken)
|
|
39
|
-
if (!entry) {
|
|
40
|
-
dcgLogger(`[token-cache] no cache found for botToken=${botToken.slice(0, 10)}...`)
|
|
41
|
-
return null
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// 检查是否过期
|
|
45
|
-
if (Date.now() >= entry.expiresAt) {
|
|
46
|
-
dcgLogger(`[token-cache] cache expired for botToken=${botToken.slice(0, 10)}..., removing`)
|
|
47
|
-
tokenCache.delete(botToken)
|
|
48
|
-
return null
|
|
49
|
-
}
|
|
50
|
-
|
|
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
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* 清除指定 botToken 的缓存
|
|
59
|
-
* @param botToken 机器人 token
|
|
60
|
-
*/
|
|
61
|
-
export function clearUserTokenCache(botToken: string): void {
|
|
62
|
-
tokenCache.delete(botToken)
|
|
63
|
-
dcgLogger(`[token-cache] cleared cache for botToken=${botToken.slice(0, 10)}...`)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 清除所有缓存
|
|
68
|
-
*/
|
|
69
|
-
export function clearAllUserTokenCache(): void {
|
|
70
|
-
tokenCache.clear()
|
|
71
|
-
dcgLogger(`[token-cache] cleared all token cache`)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* 获取缓存统计信息(用于调试)
|
|
76
|
-
*/
|
|
77
|
-
export function getTokenCacheStats(): {
|
|
78
|
-
total: number
|
|
79
|
-
valid: number
|
|
80
|
-
expired: number
|
|
81
|
-
} {
|
|
82
|
-
const now = Date.now()
|
|
83
|
-
let valid = 0
|
|
84
|
-
let expired = 0
|
|
85
|
-
|
|
86
|
-
for (const entry of tokenCache.values()) {
|
|
87
|
-
if (now < entry.expiresAt) {
|
|
88
|
-
valid++
|
|
89
|
-
} else {
|
|
90
|
-
expired++
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
total: tokenCache.size,
|
|
96
|
-
valid,
|
|
97
|
-
expired
|
|
98
|
-
}
|
|
99
|
-
}
|
package/src/session.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { sendMessageToGateway } from './gateway/socket.js'
|
|
2
|
-
import { getSessionKey } from './utils/global.js'
|
|
3
|
-
import { dcgLogger } from './utils/log.js'
|
|
4
|
-
|
|
5
|
-
interface TSession {
|
|
6
|
-
agent_id: string
|
|
7
|
-
session_id: string
|
|
8
|
-
agent_clone_code?: string
|
|
9
|
-
account_id: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const onRemoveSession = async ({ agent_id, session_id, agent_clone_code, account_id }: TSession) => {
|
|
13
|
-
const sessionKey = getSessionKey({ agent_id, session_id, agent_clone_code }, account_id)
|
|
14
|
-
if (!session_id) {
|
|
15
|
-
dcgLogger('onRemoveSession: empty session_id', 'error')
|
|
16
|
-
return
|
|
17
|
-
}
|
|
18
|
-
sendMessageToGateway(JSON.stringify({ method: 'sessions.delete', params: { key: sessionKey, deleteTranscript: true } }))
|
|
19
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 会话终止 / 抢占 / 网关 abort 的集中实现,便于单独调整策略与观测。
|
|
3
|
-
* 与 bot 的 generation、流式分片序号、入站业务上下文分离,仅负责:本地 AbortController、流抑制标记、
|
|
4
|
-
* 入站串行队尾、activeRunId、chat.abort 子会话→主会话。
|
|
5
|
-
*/
|
|
6
|
-
import { sendGatewayRpc } from './gateway/socket.js'
|
|
7
|
-
import { sendFinal } from './transport.js'
|
|
8
|
-
import type { IMsgParams } from './types.js'
|
|
9
|
-
import { dcgLogger } from './utils/log.js'
|
|
10
|
-
import { getDescendantSessionKeysForRequester, resetSubagentStateForRequesterSession } from './tool.js'
|
|
11
|
-
|
|
12
|
-
// --- 状态(仅本模块内修改,供 bot 通过下方 API 使用)---
|
|
13
|
-
|
|
14
|
-
/** 当前会话最近一次 agent run 的 runId(网关 chat.abort 主会话时携带) */
|
|
15
|
-
const activeRunIdBySessionKey = new Map<string, string>()
|
|
16
|
-
|
|
17
|
-
/** dispatchReplyFromConfig 使用的 AbortSignal,用于真正掐断工具与模型 */
|
|
18
|
-
const dispatchAbortBySessionKey = new Map<string, AbortController>()
|
|
19
|
-
|
|
20
|
-
/** 打断后抑制 deliver / onPartialReply 继续下发 */
|
|
21
|
-
const sessionStreamSuppressed = new Set<string>()
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 同 sessionKey 入站串行队尾;与 Core session lane 对齐,避免并发 dispatch 过早返回。
|
|
25
|
-
*/
|
|
26
|
-
const inboundTurnTailBySessionKey = new Map<string, Promise<void>>()
|
|
27
|
-
|
|
28
|
-
// --- activeRunId(供 bot 在 onAgentRunStart / 错误收尾时同步)---
|
|
29
|
-
|
|
30
|
-
export function setActiveRunIdForSession(sessionKey: string, runId: string): void {
|
|
31
|
-
activeRunIdBySessionKey.set(sessionKey, runId)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function clearActiveRunIdForSession(sessionKey: string): void {
|
|
35
|
-
activeRunIdBySessionKey.delete(sessionKey)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// --- 流抑制 ---
|
|
39
|
-
|
|
40
|
-
export function isSessionStreamSuppressed(sessionKey: string): boolean {
|
|
41
|
-
return sessionStreamSuppressed.has(sessionKey)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function clearSessionStreamSuppression(sessionKey: string): void {
|
|
45
|
-
sessionStreamSuppressed.delete(sessionKey)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function markSessionStreamSuppressed(sessionKey: string): void {
|
|
49
|
-
sessionStreamSuppressed.add(sessionKey)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// --- 入站串行队列 ---
|
|
53
|
-
|
|
54
|
-
/** /stop 入队前:掐断当前 in-process,并重置队尾,使本条 stop 不必等待已被 abort 的长 turn */
|
|
55
|
-
export function preemptInboundQueueForStop(sessionKey: string): void {
|
|
56
|
-
const c = dispatchAbortBySessionKey.get(sessionKey)
|
|
57
|
-
if (c) {
|
|
58
|
-
c.abort()
|
|
59
|
-
dispatchAbortBySessionKey.delete(sessionKey)
|
|
60
|
-
}
|
|
61
|
-
inboundTurnTailBySessionKey.set(sessionKey, Promise.resolve())
|
|
62
|
-
dcgLogger(`inbound queue: reset tail for /stop sessionKey=${sessionKey}`)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** 将本轮入站处理挂到 sessionKey 队尾,保证同会话顺序执行 */
|
|
66
|
-
export async function runInboundTurnSequenced(sessionKey: string, run: () => Promise<void>): Promise<void> {
|
|
67
|
-
const prev = inboundTurnTailBySessionKey.get(sessionKey) ?? Promise.resolve()
|
|
68
|
-
const next = prev.catch(() => {}).then(run)
|
|
69
|
-
inboundTurnTailBySessionKey.set(sessionKey, next.catch(() => {}))
|
|
70
|
-
await next
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// --- 网关 abort ---
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* 终止网关上仍可能活跃的 run(子会话自深到浅,再主会话)。
|
|
77
|
-
* supersede:仅当有子会话或 mainRunId 时发 RPC;interrupt:主会话始终 chat.abort。
|
|
78
|
-
*/
|
|
79
|
-
export async function abortGatewayRunsForSession(sessionKey: string, reason: 'interrupt' | 'supersede'): Promise<void> {
|
|
80
|
-
const prefix = reason === 'interrupt' ? 'interrupt' : 'supersede'
|
|
81
|
-
const descendantKeys = getDescendantSessionKeysForRequester(sessionKey)
|
|
82
|
-
const abortSubKeys = [...descendantKeys].reverse()
|
|
83
|
-
const mainRunId = activeRunIdBySessionKey.get(sessionKey)
|
|
84
|
-
|
|
85
|
-
if (reason === 'supersede' && abortSubKeys.length === 0 && !mainRunId) {
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (abortSubKeys.length > 0) {
|
|
90
|
-
dcgLogger(`${prefix}: chat.abort ${abortSubKeys.length} subagent session(s) (nested incl.)`)
|
|
91
|
-
}
|
|
92
|
-
for (const subKey of abortSubKeys) {
|
|
93
|
-
try {
|
|
94
|
-
await sendGatewayRpc({ method: 'chat.abort', params: { sessionKey: subKey } })
|
|
95
|
-
} catch (e) {
|
|
96
|
-
dcgLogger(`${prefix}: chat.abort subagent ${subKey}: ${String(e)}`, 'error')
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const shouldMainAbort = reason === 'interrupt' || Boolean(mainRunId)
|
|
101
|
-
if (shouldMainAbort) {
|
|
102
|
-
try {
|
|
103
|
-
await sendGatewayRpc({
|
|
104
|
-
method: 'chat.abort',
|
|
105
|
-
params: mainRunId ? { sessionKey, runId: mainRunId } : { sessionKey }
|
|
106
|
-
})
|
|
107
|
-
} catch (e) {
|
|
108
|
-
dcgLogger(`${prefix}: chat.abort main ${sessionKey}: ${String(e)}`, 'error')
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
activeRunIdBySessionKey.delete(sessionKey)
|
|
113
|
-
resetSubagentStateForRequesterSession(sessionKey)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// --- 本地 dispatch AbortController ---
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* 新一轮非 /stop 用户消息:清除流抑制、abort 上一轮 controller、网关 supersede,并安装新的 AbortController。
|
|
120
|
-
* 调用方须在之前或之后自行 `streamChunkIdxBySessionKey.set(sessionKey, 0)`。
|
|
121
|
-
*/
|
|
122
|
-
export async function beginSupersedingUserTurn(sessionKey: string): Promise<AbortController> {
|
|
123
|
-
sessionStreamSuppressed.delete(sessionKey)
|
|
124
|
-
dispatchAbortBySessionKey.get(sessionKey)?.abort()
|
|
125
|
-
await abortGatewayRunsForSession(sessionKey, 'supersede')
|
|
126
|
-
const ac = new AbortController()
|
|
127
|
-
dispatchAbortBySessionKey.set(sessionKey, ac)
|
|
128
|
-
return ac
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** try/finally 中:仅当仍是当前 controller 时从 map 移除 */
|
|
132
|
-
export function releaseDispatchAbortIfCurrent(sessionKey: string, controller: AbortController | undefined): void {
|
|
133
|
-
if (controller && dispatchAbortBySessionKey.get(sessionKey) === controller) {
|
|
134
|
-
dispatchAbortBySessionKey.delete(sessionKey)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* 用户发送 /stop:本地 abort、对「上一轮对话」发 abort final、标记流抑制、网关 interrupt。
|
|
140
|
-
* 之后的 clearSentMedia、params、UI「已终止」等由 bot 继续处理。
|
|
141
|
-
*/
|
|
142
|
-
export async function interruptLocalDispatchAndGateway(sessionKey: string, ctxForAbort: IMsgParams): Promise<void> {
|
|
143
|
-
dcgLogger(`interrupt command: sessionKey=${sessionKey}`)
|
|
144
|
-
const inFlight = dispatchAbortBySessionKey.get(sessionKey)
|
|
145
|
-
if (inFlight) {
|
|
146
|
-
dcgLogger(`interrupt: AbortController.abort() in-process run sessionKey=${sessionKey}`)
|
|
147
|
-
inFlight.abort()
|
|
148
|
-
dispatchAbortBySessionKey.delete(sessionKey)
|
|
149
|
-
}
|
|
150
|
-
const finalCtx = ctxForAbort.messageId?.trim() ? ctxForAbort : { ...ctxForAbort, messageId: `${Date.now()}` }
|
|
151
|
-
sendFinal(finalCtx, 'abort')
|
|
152
|
-
markSessionStreamSuppressed(sessionKey)
|
|
153
|
-
await abortGatewayRunsForSession(sessionKey, 'interrupt')
|
|
154
|
-
}
|