crawd 0.8.0
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 +176 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +975 -0
- package/dist/client.d.ts +53 -0
- package/dist/client.js +40 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.js +0 -0
- package/openclaw.plugin.json +108 -0
- package/package.json +86 -0
- package/skills/crawd/SKILL.md +81 -0
- package/src/backend/coordinator.ts +883 -0
- package/src/backend/index.ts +581 -0
- package/src/backend/server.ts +589 -0
- package/src/cli.ts +130 -0
- package/src/client.ts +101 -0
- package/src/commands/auth.ts +145 -0
- package/src/commands/config.ts +43 -0
- package/src/commands/down.ts +15 -0
- package/src/commands/logs.ts +32 -0
- package/src/commands/skill.ts +189 -0
- package/src/commands/start.ts +120 -0
- package/src/commands/status.ts +73 -0
- package/src/commands/stop.ts +16 -0
- package/src/commands/stream-key.ts +45 -0
- package/src/commands/talk.ts +30 -0
- package/src/commands/up.ts +59 -0
- package/src/commands/update.ts +92 -0
- package/src/config/schema.ts +66 -0
- package/src/config/store.ts +185 -0
- package/src/daemon/manager.ts +280 -0
- package/src/daemon/pid.ts +102 -0
- package/src/lib/chat/base.ts +13 -0
- package/src/lib/chat/manager.ts +105 -0
- package/src/lib/chat/pumpfun/client.ts +56 -0
- package/src/lib/chat/types.ts +48 -0
- package/src/lib/chat/youtube/client.ts +131 -0
- package/src/lib/pumpfun/live/client.ts +69 -0
- package/src/lib/pumpfun/live/index.ts +3 -0
- package/src/lib/pumpfun/live/types.ts +38 -0
- package/src/lib/pumpfun/v2/client.ts +139 -0
- package/src/lib/pumpfun/v2/index.ts +5 -0
- package/src/lib/pumpfun/v2/socket/client.ts +60 -0
- package/src/lib/pumpfun/v2/socket/index.ts +6 -0
- package/src/lib/pumpfun/v2/socket/types.ts +7 -0
- package/src/lib/pumpfun/v2/types.ts +234 -0
- package/src/lib/tts/tiktok.ts +91 -0
- package/src/plugin.ts +280 -0
- package/src/types.ts +78 -0
- package/src/utils/logger.ts +43 -0
- package/src/utils/paths.ts +55 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'child_process'
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
createWriteStream,
|
|
6
|
+
cpSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'fs'
|
|
9
|
+
import { join } from 'path'
|
|
10
|
+
import {
|
|
11
|
+
LOGS_DIR,
|
|
12
|
+
LOG_FILES,
|
|
13
|
+
OVERLAY_DIR,
|
|
14
|
+
BACKEND_DIR,
|
|
15
|
+
BACKEND_TEMPLATE_DIR,
|
|
16
|
+
CRAWD_HOME,
|
|
17
|
+
TTS_CACHE_DIR,
|
|
18
|
+
} from '../utils/paths.js'
|
|
19
|
+
import { log } from '../utils/logger.js'
|
|
20
|
+
import { writePid, killProcess, isRunning, type ProcessName } from './pid.js'
|
|
21
|
+
import { loadConfig, loadEnv } from '../config/store.js'
|
|
22
|
+
|
|
23
|
+
/** Ensure all required directories exist */
|
|
24
|
+
export function ensureDirectories() {
|
|
25
|
+
const dirs = [CRAWD_HOME, LOGS_DIR, OVERLAY_DIR, BACKEND_DIR, TTS_CACHE_DIR]
|
|
26
|
+
for (const dir of dirs) {
|
|
27
|
+
if (!existsSync(dir)) {
|
|
28
|
+
mkdirSync(dir, { recursive: true })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Copy overlay template to user directory if needed */
|
|
34
|
+
export function ensureOverlay(force = false) {
|
|
35
|
+
if (force || !existsSync(join(OVERLAY_DIR, 'package.json'))) {
|
|
36
|
+
log.info('Setting up overlay...')
|
|
37
|
+
|
|
38
|
+
if (!existsSync(OVERLAY_TEMPLATE_DIR)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Overlay template not found at ${OVERLAY_TEMPLATE_DIR}. ` +
|
|
41
|
+
'This is a bug in the crawd package.'
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
cpSync(OVERLAY_TEMPLATE_DIR, OVERLAY_DIR, { recursive: true })
|
|
46
|
+
log.success('Overlay installed to ' + OVERLAY_DIR)
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Copy backend template to user directory if needed */
|
|
53
|
+
export function ensureBackend(force = false) {
|
|
54
|
+
if (force || !existsSync(join(BACKEND_DIR, 'index.ts'))) {
|
|
55
|
+
log.info('Setting up backend...')
|
|
56
|
+
|
|
57
|
+
if (!existsSync(BACKEND_TEMPLATE_DIR)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Backend template not found at ${BACKEND_TEMPLATE_DIR}. ` +
|
|
60
|
+
'This is a bug in the crawd package.'
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
cpSync(BACKEND_TEMPLATE_DIR, BACKEND_DIR, { recursive: true })
|
|
65
|
+
|
|
66
|
+
// Create a minimal package.json for the backend
|
|
67
|
+
const backendPkg = {
|
|
68
|
+
name: '@crawd/backend',
|
|
69
|
+
type: 'module',
|
|
70
|
+
private: true,
|
|
71
|
+
dependencies: {
|
|
72
|
+
'@fastify/cors': '^10.0.2',
|
|
73
|
+
'@fastify/static': '^8.1.0',
|
|
74
|
+
'dotenv': '^16.4.7',
|
|
75
|
+
'fastify': '^5.2.1',
|
|
76
|
+
'socket.io': '^4.8.1',
|
|
77
|
+
},
|
|
78
|
+
optionalDependencies: {
|
|
79
|
+
'openai': '^4.77.0',
|
|
80
|
+
'@elevenlabs/elevenlabs-js': '^1.0.0',
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
writeFileSync(
|
|
85
|
+
join(BACKEND_DIR, 'package.json'),
|
|
86
|
+
JSON.stringify(backendPkg, null, 2)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
log.success('Backend installed to ' + BACKEND_DIR)
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Check if backend dependencies are installed */
|
|
96
|
+
function backendHasNodeModules(): boolean {
|
|
97
|
+
return existsSync(join(BACKEND_DIR, 'node_modules'))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Install backend dependencies */
|
|
101
|
+
async function installBackendDeps(): Promise<void> {
|
|
102
|
+
log.info('Installing backend dependencies...')
|
|
103
|
+
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const child = spawn('pnpm', ['install'], {
|
|
106
|
+
cwd: BACKEND_DIR,
|
|
107
|
+
stdio: 'inherit',
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
child.on('close', (code) => {
|
|
111
|
+
if (code === 0) {
|
|
112
|
+
log.success('Backend dependencies installed')
|
|
113
|
+
resolve()
|
|
114
|
+
} else {
|
|
115
|
+
reject(new Error(`pnpm install failed with code ${code}`))
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
child.on('error', reject)
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Check if overlay dependencies are installed */
|
|
124
|
+
function overlayHasNodeModules(): boolean {
|
|
125
|
+
return existsSync(join(OVERLAY_DIR, 'node_modules'))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Install overlay dependencies */
|
|
129
|
+
async function installOverlayDeps(): Promise<void> {
|
|
130
|
+
log.info('Installing overlay dependencies...')
|
|
131
|
+
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
const child = spawn('pnpm', ['install'], {
|
|
134
|
+
cwd: OVERLAY_DIR,
|
|
135
|
+
stdio: 'inherit',
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
child.on('close', (code) => {
|
|
139
|
+
if (code === 0) {
|
|
140
|
+
log.success('Overlay dependencies installed')
|
|
141
|
+
resolve()
|
|
142
|
+
} else {
|
|
143
|
+
reject(new Error(`pnpm install failed with code ${code}`))
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
child.on('error', reject)
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Build environment variables for child processes */
|
|
152
|
+
function buildEnv(): NodeJS.ProcessEnv {
|
|
153
|
+
const config = loadConfig()
|
|
154
|
+
const secrets = loadEnv()
|
|
155
|
+
const env: NodeJS.ProcessEnv = { ...process.env }
|
|
156
|
+
|
|
157
|
+
// Secrets from ~/.crawd/.env
|
|
158
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
159
|
+
env[key] = value
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Config from ~/.crawd/config.json
|
|
163
|
+
env.CRAWD_GATEWAY_URL = config.gateway.url
|
|
164
|
+
env.CRAWD_CHANNEL_ID = config.gateway.channelId
|
|
165
|
+
env.CRAWD_BACKEND_PORT = String(config.ports.backend)
|
|
166
|
+
env.CRAWD_OVERLAY_PORT = String(config.ports.overlay)
|
|
167
|
+
|
|
168
|
+
return env
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Start a daemon process */
|
|
172
|
+
function startDaemon(
|
|
173
|
+
name: ProcessName,
|
|
174
|
+
command: string,
|
|
175
|
+
args: string[],
|
|
176
|
+
cwd: string,
|
|
177
|
+
env: NodeJS.ProcessEnv
|
|
178
|
+
): ChildProcess {
|
|
179
|
+
// Ensure log directory exists
|
|
180
|
+
if (!existsSync(LOGS_DIR)) {
|
|
181
|
+
mkdirSync(LOGS_DIR, { recursive: true })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const logStream = createWriteStream(LOG_FILES[name], { flags: 'a' })
|
|
185
|
+
|
|
186
|
+
const child = spawn(command, args, {
|
|
187
|
+
cwd,
|
|
188
|
+
env,
|
|
189
|
+
detached: true,
|
|
190
|
+
stdio: ['ignore', logStream, logStream],
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
child.unref()
|
|
194
|
+
|
|
195
|
+
if (child.pid) {
|
|
196
|
+
writePid(name, child.pid)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return child
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Start the backend server */
|
|
203
|
+
export async function startBackend(): Promise<ChildProcess | null> {
|
|
204
|
+
if (isRunning('backend')) {
|
|
205
|
+
log.warn('Backend is already running')
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Ensure backend is set up
|
|
210
|
+
ensureBackend()
|
|
211
|
+
|
|
212
|
+
// Install dependencies if needed
|
|
213
|
+
if (!backendHasNodeModules()) {
|
|
214
|
+
await installBackendDeps()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const config = loadConfig()
|
|
218
|
+
const env = buildEnv()
|
|
219
|
+
|
|
220
|
+
// Set TTS directory
|
|
221
|
+
env.CRAWD_TTS_DIR = TTS_CACHE_DIR
|
|
222
|
+
|
|
223
|
+
const backendEntry = join(BACKEND_DIR, 'index.ts')
|
|
224
|
+
|
|
225
|
+
log.info(`Starting backend on port ${config.ports.backend}...`)
|
|
226
|
+
return startDaemon('backend', 'npx', ['tsx', backendEntry], BACKEND_DIR, env)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Start the overlay dev server */
|
|
230
|
+
export async function startOverlay(): Promise<ChildProcess | null> {
|
|
231
|
+
if (isRunning('overlay')) {
|
|
232
|
+
log.warn('Overlay is already running')
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
ensureOverlay()
|
|
237
|
+
|
|
238
|
+
if (!overlayHasNodeModules()) {
|
|
239
|
+
await installOverlayDeps()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const config = loadConfig()
|
|
243
|
+
const env = buildEnv()
|
|
244
|
+
|
|
245
|
+
// Set Vite port
|
|
246
|
+
env.PORT = String(config.ports.overlay)
|
|
247
|
+
|
|
248
|
+
log.info(`Starting overlay on port ${config.ports.overlay}...`)
|
|
249
|
+
return startDaemon('overlay', 'pnpm', ['dev'], OVERLAY_DIR, env)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Stop a specific daemon */
|
|
253
|
+
export function stopDaemon(name: ProcessName): boolean {
|
|
254
|
+
if (!isRunning(name)) {
|
|
255
|
+
log.dim(`${name} is not running`)
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const killed = killProcess(name)
|
|
260
|
+
if (killed) {
|
|
261
|
+
log.success(`Stopped ${name}`)
|
|
262
|
+
}
|
|
263
|
+
return killed
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Stop all daemons */
|
|
267
|
+
export function stopAll() {
|
|
268
|
+
stopDaemon('backend')
|
|
269
|
+
stopDaemon('overlay')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Start all daemons */
|
|
273
|
+
export async function startAll() {
|
|
274
|
+
ensureDirectories()
|
|
275
|
+
|
|
276
|
+
const backend = await startBackend()
|
|
277
|
+
const overlay = await startOverlay()
|
|
278
|
+
|
|
279
|
+
return { backend, overlay }
|
|
280
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs'
|
|
2
|
+
import { dirname } from 'path'
|
|
3
|
+
import { PID_FILES, PIDS_DIR } from '../utils/paths.js'
|
|
4
|
+
|
|
5
|
+
export type ProcessName = 'backend' | 'overlay' | 'crawdbot'
|
|
6
|
+
|
|
7
|
+
/** Write a PID file */
|
|
8
|
+
export function writePid(name: ProcessName, pid: number) {
|
|
9
|
+
if (!existsSync(PIDS_DIR)) {
|
|
10
|
+
mkdirSync(PIDS_DIR, { recursive: true })
|
|
11
|
+
}
|
|
12
|
+
writeFileSync(PID_FILES[name], String(pid))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Read a PID from file */
|
|
16
|
+
export function readPid(name: ProcessName): number | null {
|
|
17
|
+
const path = PID_FILES[name]
|
|
18
|
+
if (!existsSync(path)) {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const content = readFileSync(path, 'utf-8').trim()
|
|
23
|
+
const pid = parseInt(content, 10)
|
|
24
|
+
return isNaN(pid) ? null : pid
|
|
25
|
+
} catch {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Remove a PID file */
|
|
31
|
+
export function removePid(name: ProcessName) {
|
|
32
|
+
const path = PID_FILES[name]
|
|
33
|
+
if (existsSync(path)) {
|
|
34
|
+
unlinkSync(path)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Check if a process is running by PID */
|
|
39
|
+
export function isProcessRunning(pid: number): boolean {
|
|
40
|
+
try {
|
|
41
|
+
// kill with signal 0 doesn't kill the process, just checks if it exists
|
|
42
|
+
process.kill(pid, 0)
|
|
43
|
+
return true
|
|
44
|
+
} catch {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Check if a named process is running */
|
|
50
|
+
export function isRunning(name: ProcessName): boolean {
|
|
51
|
+
const pid = readPid(name)
|
|
52
|
+
if (pid === null) return false
|
|
53
|
+
return isProcessRunning(pid)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Kill a process by name if running */
|
|
57
|
+
export function killProcess(name: ProcessName): boolean {
|
|
58
|
+
const pid = readPid(name)
|
|
59
|
+
if (pid === null) return false
|
|
60
|
+
|
|
61
|
+
if (!isProcessRunning(pid)) {
|
|
62
|
+
removePid(name)
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
process.kill(pid, 'SIGTERM')
|
|
68
|
+
// Give it a moment to terminate gracefully
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
if (isProcessRunning(pid)) {
|
|
71
|
+
try {
|
|
72
|
+
process.kill(pid, 'SIGKILL')
|
|
73
|
+
} catch {
|
|
74
|
+
// Process may have already exited
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}, 2000)
|
|
78
|
+
removePid(name)
|
|
79
|
+
return true
|
|
80
|
+
} catch {
|
|
81
|
+
removePid(name)
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Get status of all processes */
|
|
87
|
+
export function getProcessStatus(): Record<ProcessName, { running: boolean; pid: number | null }> {
|
|
88
|
+
return {
|
|
89
|
+
backend: {
|
|
90
|
+
running: isRunning('backend'),
|
|
91
|
+
pid: readPid('backend'),
|
|
92
|
+
},
|
|
93
|
+
overlay: {
|
|
94
|
+
running: isRunning('overlay'),
|
|
95
|
+
pid: readPid('overlay'),
|
|
96
|
+
},
|
|
97
|
+
crawdbot: {
|
|
98
|
+
running: isRunning('crawdbot'),
|
|
99
|
+
pid: readPid('crawdbot'),
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EventEmitter } from 'events'
|
|
2
|
+
import type { ChatPlatform } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Abstract base class for chat clients.
|
|
6
|
+
* Uses EventEmitter - listen with: client.on('message', (msg) => ...)
|
|
7
|
+
*/
|
|
8
|
+
export abstract class BaseChatClient extends EventEmitter {
|
|
9
|
+
abstract readonly platform: ChatPlatform
|
|
10
|
+
abstract connect(): Promise<void>
|
|
11
|
+
abstract disconnect(): void
|
|
12
|
+
abstract isConnected(): boolean
|
|
13
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ChatMessage } from './types'
|
|
2
|
+
import type { BaseChatClient } from './base'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Orchestrates multiple chat clients with reconnection policy.
|
|
6
|
+
* Uses string keys to allow multiple clients per platform.
|
|
7
|
+
*/
|
|
8
|
+
export class ChatManager {
|
|
9
|
+
private clients = new Map<string, BaseChatClient>()
|
|
10
|
+
private messageHandlers: ((msg: ChatMessage) => void)[] = []
|
|
11
|
+
private reconnectTimers = new Map<string, NodeJS.Timeout>()
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register a client with a unique key.
|
|
15
|
+
* Key can be platform name or custom (e.g., 'youtube:stream1')
|
|
16
|
+
*/
|
|
17
|
+
registerClient(key: string, client: BaseChatClient): void {
|
|
18
|
+
this.clients.set(key, client)
|
|
19
|
+
|
|
20
|
+
client.on('message', (msg: ChatMessage) => {
|
|
21
|
+
this.messageHandlers.forEach(handler => handler(msg))
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
client.on('connected', () => {
|
|
25
|
+
console.log(`[ChatManager] ${key} connected`)
|
|
26
|
+
this.clearReconnectTimer(key)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
client.on('disconnected', () => {
|
|
30
|
+
console.log(`[ChatManager] ${key} disconnected`)
|
|
31
|
+
this.scheduleReconnect(key)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
client.on('error', (error: Error) => {
|
|
35
|
+
console.error(`[ChatManager] ${key} error:`, error.message)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private scheduleReconnect(key: string, attempt = 1): void {
|
|
40
|
+
const maxAttempts = 5
|
|
41
|
+
if (attempt > maxAttempts) {
|
|
42
|
+
console.error(`[ChatManager] ${key} failed after ${maxAttempts} attempts, giving up`)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const delay = Math.min(5000 * Math.pow(2, attempt - 1), 60000)
|
|
47
|
+
console.log(`[ChatManager] ${key} reconnecting in ${delay}ms (attempt ${attempt})`)
|
|
48
|
+
|
|
49
|
+
const timer = setTimeout(async () => {
|
|
50
|
+
const client = this.clients.get(key)
|
|
51
|
+
if (!client || client.isConnected()) return
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await client.connect()
|
|
55
|
+
} catch {
|
|
56
|
+
this.scheduleReconnect(key, attempt + 1)
|
|
57
|
+
}
|
|
58
|
+
}, delay)
|
|
59
|
+
|
|
60
|
+
this.reconnectTimers.set(key, timer)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private clearReconnectTimer(key: string): void {
|
|
64
|
+
const timer = this.reconnectTimers.get(key)
|
|
65
|
+
if (timer) {
|
|
66
|
+
clearTimeout(timer)
|
|
67
|
+
this.reconnectTimers.delete(key)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async connectAll(): Promise<void> {
|
|
72
|
+
await Promise.allSettled(
|
|
73
|
+
Array.from(this.clients.entries()).map(async ([key, client]) => {
|
|
74
|
+
try {
|
|
75
|
+
await client.connect()
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(`[ChatManager] ${key} initial connect failed:`, err)
|
|
78
|
+
this.scheduleReconnect(key)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
disconnectAll(): void {
|
|
85
|
+
this.reconnectTimers.forEach(timer => clearTimeout(timer))
|
|
86
|
+
this.reconnectTimers.clear()
|
|
87
|
+
this.clients.forEach(client => {
|
|
88
|
+
if (client.isConnected()) client.disconnect()
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onMessage(handler: (msg: ChatMessage) => void): void {
|
|
93
|
+
this.messageHandlers.push(handler)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getConnectedKeys(): string[] {
|
|
97
|
+
return Array.from(this.clients.entries())
|
|
98
|
+
.filter(([_, client]) => client.isConnected())
|
|
99
|
+
.map(([key]) => key)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getClient(key: string): BaseChatClient | undefined {
|
|
103
|
+
return this.clients.get(key)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { PumpfunSocketClient } from '../../pumpfun/v2/socket/client'
|
|
2
|
+
import type { PumpfunMessage } from '../../pumpfun/v2/socket/types'
|
|
3
|
+
import { BaseChatClient } from '../base'
|
|
4
|
+
import { generateShortId } from '../types'
|
|
5
|
+
import type { ChatPlatform, ChatMessage } from '../types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Adapter wrapping existing PumpfunSocketClient to unified interface.
|
|
9
|
+
*/
|
|
10
|
+
export class PumpFunChatClient extends BaseChatClient {
|
|
11
|
+
readonly platform: ChatPlatform = 'pumpfun'
|
|
12
|
+
|
|
13
|
+
private client: PumpfunSocketClient
|
|
14
|
+
private roomId: string
|
|
15
|
+
private _connected = false
|
|
16
|
+
|
|
17
|
+
constructor(roomId: string, authToken: string | null) {
|
|
18
|
+
super()
|
|
19
|
+
this.roomId = roomId
|
|
20
|
+
this.client = new PumpfunSocketClient(authToken)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async connect(): Promise<void> {
|
|
24
|
+
this.client.connect()
|
|
25
|
+
await this.client.joinRoom(this.roomId)
|
|
26
|
+
|
|
27
|
+
this.client.onMessage((msg: PumpfunMessage) => {
|
|
28
|
+
const chatMsg: ChatMessage = {
|
|
29
|
+
id: `pf:${msg.id}`,
|
|
30
|
+
shortId: generateShortId(),
|
|
31
|
+
platform: 'pumpfun',
|
|
32
|
+
username: msg.username,
|
|
33
|
+
message: msg.message,
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
metadata: {
|
|
36
|
+
userAddress: msg.userAddress,
|
|
37
|
+
roomId: msg.roomId
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this.emit('message', chatMsg)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
this._connected = true
|
|
44
|
+
this.emit('connected')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
disconnect(): void {
|
|
48
|
+
this.client.disconnect()
|
|
49
|
+
this._connected = false
|
|
50
|
+
this.emit('disconnected')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
isConnected(): boolean {
|
|
54
|
+
return this._connected
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
|
|
3
|
+
export type ChatPlatform = 'pumpfun' | 'youtube' | 'twitch' | 'twitter'
|
|
4
|
+
|
|
5
|
+
/** Generate a short ID for chat messages (6 chars) */
|
|
6
|
+
export const generateShortId = () => nanoid(6)
|
|
7
|
+
|
|
8
|
+
export type SuperChatInfo = {
|
|
9
|
+
amountDisplayString: string
|
|
10
|
+
backgroundColor: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ChatMessageMetadata = {
|
|
14
|
+
// YouTube
|
|
15
|
+
channelId?: string
|
|
16
|
+
isModerator?: boolean
|
|
17
|
+
isMember?: boolean
|
|
18
|
+
superChat?: SuperChatInfo
|
|
19
|
+
|
|
20
|
+
// Pump Fun
|
|
21
|
+
userAddress?: string
|
|
22
|
+
roomId?: string
|
|
23
|
+
|
|
24
|
+
// Common
|
|
25
|
+
avatarUrl?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Unified message type for all chat platforms.
|
|
30
|
+
* `platform` is optional for backward compatibility.
|
|
31
|
+
*/
|
|
32
|
+
export type ChatMessage = {
|
|
33
|
+
id: string
|
|
34
|
+
/** Short ID for agent prompt/response (6 chars, alphanumeric) */
|
|
35
|
+
shortId: string
|
|
36
|
+
username: string
|
|
37
|
+
message: string
|
|
38
|
+
platform?: ChatPlatform
|
|
39
|
+
timestamp?: number
|
|
40
|
+
metadata?: ChatMessageMetadata
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ChatClientEvents = {
|
|
44
|
+
message: (msg: ChatMessage) => void
|
|
45
|
+
connected: () => void
|
|
46
|
+
disconnected: () => void
|
|
47
|
+
error: (error: Error) => void
|
|
48
|
+
}
|