@dcrays/dcgchat 0.2.25 → 0.2.34
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 +21 -17
- package/package.json +1 -6
- package/src/bot.ts +239 -588
- package/src/channel.ts +110 -178
- package/src/monitor.ts +140 -180
- 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 +110 -194
- package/src/tool.ts +102 -113
- package/src/transport.ts +108 -0
- package/src/types.ts +75 -64
- package/src/utils/constant.ts +7 -0
- package/src/utils/global.ts +117 -0
- package/src/utils/log.ts +15 -0
- package/src/utils/searchFile.ts +214 -0
- 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
package/src/monitor.ts
CHANGED
|
@@ -1,176 +1,153 @@
|
|
|
1
|
-
import type { ClawdbotConfig, RuntimeEnv } from
|
|
2
|
-
import WebSocket from
|
|
3
|
-
import {
|
|
4
|
-
import { resolveAccount } from
|
|
5
|
-
import { setWsConnection } from
|
|
6
|
-
import type { InboundMessage } from
|
|
7
|
-
import { setMsgParams, setMsgStatus } from
|
|
8
|
-
import { installSkill, uninstallSkill } from
|
|
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, getOpenClawConfig } from './utils/global.js'
|
|
6
|
+
import type { InboundMessage } from './types.js'
|
|
7
|
+
import { setMsgParams, setMsgStatus } from './utils/global.js'
|
|
8
|
+
import { installSkill, uninstallSkill } from './skill.js'
|
|
9
|
+
import { dcgLogger } from './utils/log.js'
|
|
10
|
+
import { ignoreToolCommand } from './utils/constant.js'
|
|
9
11
|
|
|
10
12
|
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
|
-
const emptyToolText = [
|
|
20
|
-
"/new",
|
|
21
|
-
"/search",
|
|
22
|
-
"/stop",
|
|
23
|
-
"/abort",
|
|
24
|
-
"/queue interrupt",
|
|
25
|
-
];
|
|
13
|
+
config?: ClawdbotConfig
|
|
14
|
+
runtime?: RuntimeEnv
|
|
15
|
+
abortSignal?: AbortSignal
|
|
16
|
+
accountId?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const RECONNECT_DELAY_MS = 3000
|
|
20
|
+
const HEARTBEAT_INTERVAL_MS = 30_000
|
|
26
21
|
|
|
27
22
|
function buildConnectUrl(account: Record<string, string>): string {
|
|
28
|
-
const { wsUrl, botToken, userId, domainId, appId } = account
|
|
29
|
-
const url = new URL(wsUrl)
|
|
30
|
-
if (botToken) url.searchParams.set(
|
|
31
|
-
if (userId) url.searchParams.set(
|
|
32
|
-
url.searchParams.set(
|
|
33
|
-
url.searchParams.set(
|
|
34
|
-
return url.toString()
|
|
23
|
+
const { wsUrl, botToken, userId, domainId, appId } = account
|
|
24
|
+
const url = new URL(wsUrl)
|
|
25
|
+
if (botToken) url.searchParams.set('bot_token', botToken)
|
|
26
|
+
if (userId) url.searchParams.set('_userId', userId)
|
|
27
|
+
url.searchParams.set('_domainId', domainId || '1000')
|
|
28
|
+
url.searchParams.set('_appId', appId || '100')
|
|
29
|
+
return url.toString()
|
|
35
30
|
}
|
|
36
31
|
|
|
37
|
-
export async function monitorDcgchatProvider(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
runtime?.error?.("dcgchat: no config available");
|
|
45
|
-
return;
|
|
32
|
+
export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<void> {
|
|
33
|
+
const { abortSignal, accountId } = opts
|
|
34
|
+
|
|
35
|
+
const config = getOpenClawConfig()
|
|
36
|
+
if (!config) {
|
|
37
|
+
dcgLogger('no config available', 'error')
|
|
38
|
+
return
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
const account = resolveAccount(
|
|
49
|
-
const log = runtime?.log ?? console.log;
|
|
50
|
-
const err = runtime?.error ?? console.error;
|
|
41
|
+
const account = resolveAccount(config, accountId ?? 'default')
|
|
51
42
|
|
|
52
43
|
if (!account.wsUrl) {
|
|
53
|
-
|
|
54
|
-
return
|
|
44
|
+
dcgLogger(` wsUrl not configured`, 'error')
|
|
45
|
+
return
|
|
55
46
|
}
|
|
56
47
|
|
|
57
|
-
let shouldReconnect = true
|
|
58
|
-
let ws: WebSocket | null = null
|
|
59
|
-
let heartbeatTimer: ReturnType<typeof setInterval> | null = null
|
|
48
|
+
let shouldReconnect = true
|
|
49
|
+
let ws: WebSocket | null = null
|
|
50
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | null = null
|
|
51
|
+
let heartbeatLogCounter = 0
|
|
52
|
+
|
|
53
|
+
const isOpenclawBotHeartbeat = (raw: WebSocket.RawData, parsedMsg: { messageType?: string }): boolean => {
|
|
54
|
+
if (parsedMsg?.messageType === 'openclaw_bot_heartbeat') return true
|
|
55
|
+
if (typeof raw === 'object' && raw !== null && !Buffer.isBuffer(raw)) {
|
|
56
|
+
return (raw as { messageType?: string }).messageType === 'openclaw_bot_heartbeat'
|
|
57
|
+
}
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
60
|
|
|
61
61
|
const stopHeartbeat = () => {
|
|
62
62
|
if (heartbeatTimer) {
|
|
63
|
-
clearInterval(heartbeatTimer)
|
|
64
|
-
heartbeatTimer = null
|
|
63
|
+
clearInterval(heartbeatTimer)
|
|
64
|
+
heartbeatTimer = null
|
|
65
65
|
}
|
|
66
|
-
}
|
|
66
|
+
}
|
|
67
67
|
|
|
68
68
|
const startHeartbeat = () => {
|
|
69
|
-
stopHeartbeat()
|
|
69
|
+
stopHeartbeat()
|
|
70
70
|
heartbeatTimer = setInterval(() => {
|
|
71
71
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
72
72
|
const heartbeat = {
|
|
73
|
-
messageType:
|
|
73
|
+
messageType: 'openclaw_bot_heartbeat',
|
|
74
74
|
_userId: Number(account.userId) || 0,
|
|
75
|
-
source:
|
|
75
|
+
source: 'client',
|
|
76
76
|
content: {
|
|
77
77
|
bot_token: account.botToken,
|
|
78
|
-
status:
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
ws.send(JSON.stringify(heartbeat))
|
|
82
|
-
// log(`dcgchat[${account.accountId}]: heartbeat sent, ${JSON.stringify(heartbeat)}`);
|
|
78
|
+
status: '1'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
ws.send(JSON.stringify(heartbeat))
|
|
83
82
|
}
|
|
84
|
-
}, HEARTBEAT_INTERVAL_MS)
|
|
85
|
-
}
|
|
83
|
+
}, HEARTBEAT_INTERVAL_MS)
|
|
84
|
+
}
|
|
86
85
|
|
|
87
86
|
const connect = () => {
|
|
88
|
-
if (!shouldReconnect) return
|
|
87
|
+
if (!shouldReconnect) return
|
|
89
88
|
|
|
90
|
-
const connectUrl = buildConnectUrl(account as Record<string, any>)
|
|
91
|
-
|
|
92
|
-
ws = new WebSocket(connectUrl)
|
|
89
|
+
const connectUrl = buildConnectUrl(account as Record<string, any>)
|
|
90
|
+
dcgLogger(`connecting to ${connectUrl}`)
|
|
91
|
+
ws = new WebSocket(connectUrl)
|
|
93
92
|
|
|
94
|
-
ws.on(
|
|
95
|
-
|
|
96
|
-
setWsConnection(ws)
|
|
97
|
-
startHeartbeat()
|
|
98
|
-
})
|
|
93
|
+
ws.on('open', () => {
|
|
94
|
+
dcgLogger(`socket connected`)
|
|
95
|
+
setWsConnection(ws)
|
|
96
|
+
startHeartbeat()
|
|
97
|
+
})
|
|
99
98
|
|
|
100
|
-
ws.on(
|
|
101
|
-
|
|
102
|
-
let parsed: { messageType?: string; content: any };
|
|
99
|
+
ws.on('message', async (data) => {
|
|
100
|
+
let parsed: { messageType?: string; content: any }
|
|
103
101
|
try {
|
|
104
|
-
parsed = JSON.parse(data.toString())
|
|
102
|
+
parsed = JSON.parse(data.toString())
|
|
105
103
|
} catch {
|
|
106
|
-
|
|
107
|
-
return
|
|
104
|
+
dcgLogger(`用户消息解析失败: invalid JSON received`, 'error')
|
|
105
|
+
return
|
|
108
106
|
}
|
|
109
107
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
const payloadStr = data.toString()
|
|
109
|
+
const heartbeat = isOpenclawBotHeartbeat(data, parsed)
|
|
110
|
+
if (heartbeat) {
|
|
111
|
+
heartbeatLogCounter += 1
|
|
112
|
+
if (heartbeatLogCounter % 10 === 0) {
|
|
113
|
+
dcgLogger(`${parsed?.messageType}, ${payloadStr}`)
|
|
114
|
+
dcgLogger(`heartbeat ack received, ${payloadStr}`)
|
|
115
|
+
}
|
|
116
|
+
return
|
|
115
117
|
}
|
|
118
|
+
|
|
119
|
+
dcgLogger(`${parsed?.messageType}, ${payloadStr}`)
|
|
116
120
|
try {
|
|
117
|
-
parsed.content = JSON.parse(parsed.content)
|
|
121
|
+
parsed.content = JSON.parse(parsed.content)
|
|
118
122
|
} catch {
|
|
119
|
-
|
|
120
|
-
return
|
|
123
|
+
dcgLogger(`用户消息content字段解析失败: invalid JSON received`, 'error')
|
|
124
|
+
return
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
if (parsed.messageType ==
|
|
124
|
-
const msg = parsed as unknown as InboundMessage
|
|
125
|
-
if (!
|
|
126
|
-
setMsgStatus(
|
|
127
|
-
}
|
|
128
|
-
log(
|
|
129
|
-
`dcgchat[${account.accountId}]: openclaw_bot_chat received, ${JSON.stringify(msg)}`,
|
|
130
|
-
);
|
|
131
|
-
if (msg.content.text === "/stop") {
|
|
132
|
-
const rawConvId = msg.content.session_id as string | undefined;
|
|
133
|
-
const conversationId =
|
|
134
|
-
rawConvId || `${accountId}:${account.botToken}`;
|
|
135
|
-
console.log("🚀 ~ connect ~ conversationId:", conversationId)
|
|
136
|
-
abortMobookappGeneration(conversationId);
|
|
137
|
-
log(`[dcgchat][in] abort conversationId=${conversationId}`);
|
|
138
|
-
return;
|
|
127
|
+
if (parsed.messageType == 'openclaw_bot_chat') {
|
|
128
|
+
const msg = parsed as unknown as InboundMessage
|
|
129
|
+
if (!ignoreToolCommand.includes(msg.content.text?.trim())) {
|
|
130
|
+
setMsgStatus('running')
|
|
139
131
|
}
|
|
132
|
+
// 设置获取用户消息消息参数
|
|
140
133
|
setMsgParams({
|
|
141
134
|
userId: msg._userId,
|
|
142
135
|
token: msg.content.bot_token,
|
|
143
136
|
sessionId: msg.content.session_id,
|
|
144
137
|
messageId: msg.content.message_id,
|
|
145
138
|
domainId: account.domainId || 1000,
|
|
146
|
-
appId: account.appId ||
|
|
139
|
+
appId: account.appId || '100',
|
|
147
140
|
botId: msg.content.bot_id,
|
|
148
|
-
agentId: msg.content.agent_id
|
|
149
|
-
})
|
|
150
|
-
msg.content.app_id = account.appId ||
|
|
151
|
-
msg.content.domain_id = account.domainId ||
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (ws?.readyState === WebSocket.OPEN) {
|
|
159
|
-
const res = { ...reply, content: JSON.stringify(reply.content) };
|
|
160
|
-
ws.send(JSON.stringify(res));
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
} else if (parsed.messageType == "openclaw_bot_event") {
|
|
165
|
-
const {
|
|
166
|
-
event_type,
|
|
167
|
-
operation_type,
|
|
168
|
-
skill_url,
|
|
169
|
-
skill_code,
|
|
170
|
-
skill_id,
|
|
171
|
-
bot_token,
|
|
172
|
-
websocket_trace_id,
|
|
173
|
-
} = parsed.content ? parsed.content : ({} as Record<string, any>);
|
|
141
|
+
agentId: msg.content.agent_id
|
|
142
|
+
})
|
|
143
|
+
msg.content.app_id = account.appId || '100'
|
|
144
|
+
msg.content.domain_id = account.domainId || '1000'
|
|
145
|
+
|
|
146
|
+
await handleDcgchatMessage(msg, account.accountId)
|
|
147
|
+
} else if (parsed.messageType == 'openclaw_bot_event') {
|
|
148
|
+
const { event_type, operation_type, skill_url, skill_code, skill_id, bot_token, websocket_trace_id } = parsed.content
|
|
149
|
+
? parsed.content
|
|
150
|
+
: ({} as Record<string, any>)
|
|
174
151
|
const content = {
|
|
175
152
|
event_type,
|
|
176
153
|
operation_type,
|
|
@@ -178,75 +155,58 @@ export async function monitorDcgchatProvider(
|
|
|
178
155
|
skill_code,
|
|
179
156
|
skill_id,
|
|
180
157
|
bot_token,
|
|
181
|
-
websocket_trace_id
|
|
182
|
-
}
|
|
183
|
-
if (event_type ===
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
) {
|
|
189
|
-
installSkill({ path: skill_url, code: skill_code }, content);
|
|
190
|
-
} else if (
|
|
191
|
-
operation_type === "remove" ||
|
|
192
|
-
operation_type === "disable"
|
|
193
|
-
) {
|
|
194
|
-
uninstallSkill({ code: skill_code }, content);
|
|
158
|
+
websocket_trace_id
|
|
159
|
+
}
|
|
160
|
+
if (event_type === 'skill') {
|
|
161
|
+
if (operation_type === 'install' || operation_type === 'enable' || operation_type === 'update') {
|
|
162
|
+
installSkill({ path: skill_url, code: skill_code }, content)
|
|
163
|
+
} else if (operation_type === 'remove' || operation_type === 'disable') {
|
|
164
|
+
uninstallSkill({ code: skill_code }, content)
|
|
195
165
|
} else {
|
|
196
|
-
|
|
197
|
-
`dcgchat[${account.accountId}]: openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`,
|
|
198
|
-
);
|
|
166
|
+
dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`)
|
|
199
167
|
}
|
|
200
168
|
} else {
|
|
201
|
-
|
|
202
|
-
`dcgchat[${account.accountId}]: openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`,
|
|
203
|
-
);
|
|
169
|
+
dcgLogger(`openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`)
|
|
204
170
|
}
|
|
205
171
|
} else {
|
|
206
|
-
|
|
207
|
-
`dcgchat[${account.accountId}]: ignoring unknown messageType: ${parsed.messageType}`,
|
|
208
|
-
);
|
|
172
|
+
dcgLogger(`ignoring unknown messageType: ${parsed.messageType}`, 'error')
|
|
209
173
|
}
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
ws.on(
|
|
213
|
-
stopHeartbeat()
|
|
214
|
-
setWsConnection(null)
|
|
215
|
-
|
|
216
|
-
`dcgchat[${account.accountId}]: disconnected (code=${code}, reason=${reason?.toString() || ""})`,
|
|
217
|
-
);
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
ws.on('close', (code, reason) => {
|
|
177
|
+
stopHeartbeat()
|
|
178
|
+
setWsConnection(null)
|
|
179
|
+
dcgLogger(`disconnected (code=${code}, reason=${reason?.toString() || ''})`, 'error')
|
|
218
180
|
if (shouldReconnect) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
);
|
|
222
|
-
setTimeout(connect, RECONNECT_DELAY_MS);
|
|
181
|
+
dcgLogger(`reconnecting in ${RECONNECT_DELAY_MS}ms...`)
|
|
182
|
+
setTimeout(connect, RECONNECT_DELAY_MS)
|
|
223
183
|
}
|
|
224
|
-
})
|
|
184
|
+
})
|
|
225
185
|
|
|
226
|
-
ws.on(
|
|
227
|
-
|
|
228
|
-
})
|
|
229
|
-
}
|
|
186
|
+
ws.on('error', (e) => {
|
|
187
|
+
dcgLogger(`WebSocket error: ${String(e)}`, 'error')
|
|
188
|
+
})
|
|
189
|
+
}
|
|
230
190
|
|
|
231
|
-
connect()
|
|
191
|
+
connect()
|
|
232
192
|
|
|
233
193
|
await new Promise<void>((resolve) => {
|
|
234
194
|
if (abortSignal?.aborted) {
|
|
235
|
-
shouldReconnect = false
|
|
236
|
-
ws?.close()
|
|
237
|
-
resolve()
|
|
238
|
-
return
|
|
195
|
+
shouldReconnect = false
|
|
196
|
+
ws?.close()
|
|
197
|
+
resolve()
|
|
198
|
+
return
|
|
239
199
|
}
|
|
240
200
|
abortSignal?.addEventListener(
|
|
241
|
-
|
|
201
|
+
'abort',
|
|
242
202
|
() => {
|
|
243
|
-
|
|
244
|
-
stopHeartbeat()
|
|
245
|
-
shouldReconnect = false
|
|
246
|
-
ws?.close()
|
|
247
|
-
resolve()
|
|
203
|
+
dcgLogger(`socket stopping`)
|
|
204
|
+
stopHeartbeat()
|
|
205
|
+
shouldReconnect = false
|
|
206
|
+
ws?.close()
|
|
207
|
+
resolve()
|
|
248
208
|
},
|
|
249
|
-
{ once: true }
|
|
250
|
-
)
|
|
251
|
-
})
|
|
209
|
+
{ once: true }
|
|
210
|
+
)
|
|
211
|
+
})
|
|
252
212
|
}
|
|
@@ -1,49 +1,46 @@
|
|
|
1
|
-
import { post } from
|
|
2
|
-
import type { IStsToken, IStsTokenReq } from
|
|
3
|
-
import { getUserTokenCache, setUserTokenCache } from
|
|
1
|
+
import { post } from './request.js'
|
|
2
|
+
import type { IStsToken, IStsTokenReq } from '../types.js'
|
|
3
|
+
import { getUserTokenCache, setUserTokenCache } from './userInfo.js'
|
|
4
|
+
import { dcgLogger } from '../utils/log.js'
|
|
4
5
|
|
|
5
6
|
export const getStsToken = async (name: string, botToken: string) => {
|
|
6
7
|
// 确保 userToken 已缓存(如果未缓存会自动获取并缓存)
|
|
7
|
-
await getUserToken(botToken)
|
|
8
|
+
await getUserToken(botToken)
|
|
8
9
|
|
|
9
10
|
const response = await post<IStsTokenReq, IStsToken>(
|
|
10
|
-
|
|
11
|
+
'/user/getStsToken',
|
|
11
12
|
{
|
|
12
13
|
sourceFileName: name,
|
|
13
|
-
isPrivate: 1
|
|
14
|
+
isPrivate: 1
|
|
14
15
|
},
|
|
15
|
-
{ botToken }
|
|
16
|
-
)
|
|
16
|
+
{ botToken }
|
|
17
|
+
)
|
|
17
18
|
|
|
18
19
|
if (!response || !response.data || !response.data.bucket) {
|
|
19
|
-
throw new Error(
|
|
20
|
+
throw new Error('获取 OSS 临时凭证失败')
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
return response.data
|
|
23
|
-
}
|
|
23
|
+
return response.data
|
|
24
|
+
}
|
|
24
25
|
export const generateSignUrl = async (file_url: string, botToken: string) => {
|
|
25
26
|
try {
|
|
26
27
|
// 确保 userToken 已缓存(如果未缓存会自动获取并缓存)
|
|
27
|
-
await getUserToken(botToken)
|
|
28
|
-
|
|
28
|
+
await getUserToken(botToken)
|
|
29
|
+
|
|
29
30
|
const response = await post<any>(
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
{ botToken },
|
|
36
|
-
);
|
|
31
|
+
'/user/generateSignUrl',
|
|
32
|
+
{ loudPlatform: 0, fileName: file_url },
|
|
33
|
+
{ botToken }
|
|
34
|
+
)
|
|
37
35
|
if (response.code === 0 && response.data) {
|
|
38
36
|
// @ts-ignore
|
|
39
37
|
return response.data?.filePath
|
|
40
38
|
}
|
|
41
39
|
return ''
|
|
42
|
-
|
|
43
40
|
} catch (error) {
|
|
44
41
|
return ''
|
|
45
42
|
}
|
|
46
|
-
}
|
|
43
|
+
}
|
|
47
44
|
|
|
48
45
|
/**
|
|
49
46
|
* 通过 botToken 查询 userToken
|
|
@@ -51,16 +48,18 @@ export const generateSignUrl = async (file_url: string, botToken: string) => {
|
|
|
51
48
|
* @returns userToken
|
|
52
49
|
*/
|
|
53
50
|
export const queryUserTokenByBotToken = async (botToken: string): Promise<string> => {
|
|
54
|
-
const response = await post<{botToken: string}, {token: string}>(
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
const response = await post<{ botToken: string }, { token: string }>(
|
|
52
|
+
'/organization/queryUserTokenByBotToken',
|
|
53
|
+
{ botToken }
|
|
54
|
+
)
|
|
57
55
|
|
|
58
56
|
if (!response || !response.data || !response.data.token) {
|
|
59
|
-
|
|
57
|
+
dcgLogger('获取绑定的用户信息失败', 'error')
|
|
58
|
+
return ''
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
return response.data.token
|
|
63
|
-
}
|
|
61
|
+
return response.data.token
|
|
62
|
+
}
|
|
64
63
|
|
|
65
64
|
/**
|
|
66
65
|
* 获取 userToken(优先从缓存获取,缓存未命中则调用 API)
|
|
@@ -69,17 +68,17 @@ export const queryUserTokenByBotToken = async (botToken: string): Promise<string
|
|
|
69
68
|
*/
|
|
70
69
|
export const getUserToken = async (botToken: string): Promise<string> => {
|
|
71
70
|
// 1. 尝试从缓存获取
|
|
72
|
-
const cachedToken = getUserTokenCache(botToken)
|
|
71
|
+
const cachedToken = getUserTokenCache(botToken)
|
|
73
72
|
if (cachedToken) {
|
|
74
|
-
return cachedToken
|
|
73
|
+
return cachedToken
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
// 2. 缓存未命中,调用 API 获取
|
|
78
|
-
|
|
79
|
-
const userToken = await queryUserTokenByBotToken(botToken)
|
|
77
|
+
dcgLogger(`[api] cache miss, fetching userToken for botToken=${botToken.slice(0, 10)}...`)
|
|
78
|
+
const userToken = await queryUserTokenByBotToken(botToken)
|
|
80
79
|
|
|
81
80
|
// 3. 缓存新获取的 token
|
|
82
|
-
setUserTokenCache(botToken, userToken)
|
|
81
|
+
setUserTokenCache(botToken, userToken)
|
|
83
82
|
|
|
84
|
-
return userToken
|
|
85
|
-
}
|
|
83
|
+
return userToken
|
|
84
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs'
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import OSS from 'ali-oss'
|
|
4
|
+
import { getStsToken, getUserToken } from './api.js'
|
|
5
|
+
import { dcgLogger } from '../utils/log.js'
|
|
6
|
+
|
|
7
|
+
/** 将 File/路径/Buffer 转为 ali-oss put 所需的 Buffer 或 ReadableStream */
|
|
8
|
+
async function toUploadContent(
|
|
9
|
+
input: File | string | Buffer
|
|
10
|
+
): Promise<{ content: Buffer | ReturnType<typeof createReadStream>; fileName: string }> {
|
|
11
|
+
if (Buffer.isBuffer(input)) {
|
|
12
|
+
return { content: input, fileName: 'file' }
|
|
13
|
+
}
|
|
14
|
+
if (typeof input === 'string') {
|
|
15
|
+
return {
|
|
16
|
+
content: createReadStream(input),
|
|
17
|
+
fileName: input.split('/').pop() ?? 'file'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// File: ali-oss 需要 Buffer/Stream,用 arrayBuffer 转 Buffer
|
|
21
|
+
const buf = Buffer.from(await input.arrayBuffer())
|
|
22
|
+
return { content: buf, fileName: input.name }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const ossUpload = async (file: File | string | Buffer, botToken: string) => {
|
|
26
|
+
await getUserToken(botToken)
|
|
27
|
+
|
|
28
|
+
const { content, fileName } = await toUploadContent(file)
|
|
29
|
+
const data = await getStsToken(fileName, botToken)
|
|
30
|
+
|
|
31
|
+
const options: OSS.Options = {
|
|
32
|
+
// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
|
|
33
|
+
accessKeyId: data.tempAccessKeyId,
|
|
34
|
+
accessKeySecret: data.tempAccessKeySecret,
|
|
35
|
+
// 从STS服务获取的安全令牌(SecurityToken)。
|
|
36
|
+
stsToken: data.tempSecurityToken,
|
|
37
|
+
// 填写Bucket名称。
|
|
38
|
+
bucket: data.bucket,
|
|
39
|
+
endpoint: data.endPoint,
|
|
40
|
+
region: data.region,
|
|
41
|
+
secure: true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const client = new OSS(options)
|
|
45
|
+
|
|
46
|
+
const name = `${data.uploadDir}${data.ossFileKey}`
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const objectResult = await client.put(name, content)
|
|
50
|
+
if (objectResult?.res?.status !== 200) {
|
|
51
|
+
dcgLogger(`OSS 上传失败, ${objectResult?.res?.status}`)
|
|
52
|
+
}
|
|
53
|
+
dcgLogger(JSON.stringify(objectResult))
|
|
54
|
+
return objectResult.name || objectResult.url
|
|
55
|
+
} catch (error) {
|
|
56
|
+
dcgLogger(`OSS 上传失败: ${error}`, 'error')
|
|
57
|
+
}
|
|
58
|
+
}
|