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,589 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrawdBackend — encapsulates the Fastify+Socket.IO server, TTS, coordinator,
|
|
3
|
+
* and chat system. Used by both standalone mode (backend/index.ts) and the
|
|
4
|
+
* OpenClaw plugin (plugin.ts).
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'crypto'
|
|
7
|
+
import { writeFile, mkdir } from 'fs/promises'
|
|
8
|
+
import { join } from 'path'
|
|
9
|
+
import Fastify, { type FastifyInstance } from 'fastify'
|
|
10
|
+
import fastifyStatic from '@fastify/static'
|
|
11
|
+
import cors from '@fastify/cors'
|
|
12
|
+
import { Server } from 'socket.io'
|
|
13
|
+
import OpenAI from 'openai'
|
|
14
|
+
import { pumpfun } from '../lib/pumpfun/v2/index.js'
|
|
15
|
+
import { ChatManager } from '../lib/chat/manager.js'
|
|
16
|
+
import { PumpFunChatClient } from '../lib/chat/pumpfun/client.js'
|
|
17
|
+
import { YouTubeChatClient } from '../lib/chat/youtube/client.js'
|
|
18
|
+
import { Coordinator, OneShotGateway, type CoordinatorConfig, type CoordinatorEvent } from './coordinator.js'
|
|
19
|
+
import { generateShortId } from '../lib/chat/types.js'
|
|
20
|
+
import { configureTikTokTTS, generateTikTokTTS } from '../lib/tts/tiktok.js'
|
|
21
|
+
import type { ChatMessage } from '../lib/chat/types.js'
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Config types
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export type TtsVoiceEntry = {
|
|
28
|
+
provider: 'openai' | 'elevenlabs' | 'tiktok'
|
|
29
|
+
voice: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type CrawdConfig = {
|
|
33
|
+
enabled: boolean
|
|
34
|
+
port: number
|
|
35
|
+
bindHost: string
|
|
36
|
+
backendUrl?: string
|
|
37
|
+
tts: {
|
|
38
|
+
chat: TtsVoiceEntry[]
|
|
39
|
+
bot: TtsVoiceEntry[]
|
|
40
|
+
openaiApiKey?: string
|
|
41
|
+
elevenlabsApiKey?: string
|
|
42
|
+
tiktokSessionId?: string
|
|
43
|
+
}
|
|
44
|
+
vibe: {
|
|
45
|
+
enabled: boolean
|
|
46
|
+
intervalMs: number
|
|
47
|
+
idleAfterMs: number
|
|
48
|
+
sleepAfterIdleMs: number
|
|
49
|
+
prompt?: string
|
|
50
|
+
}
|
|
51
|
+
chat: {
|
|
52
|
+
youtube: {
|
|
53
|
+
enabled: boolean
|
|
54
|
+
videoId?: string
|
|
55
|
+
}
|
|
56
|
+
pumpfun: {
|
|
57
|
+
enabled: boolean
|
|
58
|
+
tokenMint?: string
|
|
59
|
+
authToken?: string
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
gatewayUrl?: string
|
|
63
|
+
gatewayToken?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type CrawdLogger = {
|
|
67
|
+
info: (msg: string, ...args: unknown[]) => void
|
|
68
|
+
warn: (msg: string, ...args: unknown[]) => void
|
|
69
|
+
error: (msg: string, ...args: unknown[]) => void
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const defaultLogger: CrawdLogger = {
|
|
73
|
+
info: (...args) => console.log('[Crawd]', ...args),
|
|
74
|
+
warn: (...args) => console.warn('[Crawd]', ...args),
|
|
75
|
+
error: (...args) => console.error('[Crawd]', ...args),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// CrawdBackend
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
export class CrawdBackend {
|
|
83
|
+
private fastify: FastifyInstance
|
|
84
|
+
private io!: Server
|
|
85
|
+
private config: CrawdConfig
|
|
86
|
+
private logger: CrawdLogger
|
|
87
|
+
|
|
88
|
+
private openai: OpenAI | null = null
|
|
89
|
+
private elevenlabs: any = null
|
|
90
|
+
private ttsDir: string
|
|
91
|
+
private backendUrl: string
|
|
92
|
+
private buildVersion: string
|
|
93
|
+
|
|
94
|
+
private chatManager: ChatManager | null = null
|
|
95
|
+
coordinator: Coordinator | null = null
|
|
96
|
+
private latestMcap: number | null = null
|
|
97
|
+
private mcapInterval: NodeJS.Timeout | null = null
|
|
98
|
+
|
|
99
|
+
constructor(config: CrawdConfig, logger?: CrawdLogger) {
|
|
100
|
+
this.config = config
|
|
101
|
+
this.logger = logger ?? defaultLogger
|
|
102
|
+
this.fastify = Fastify({ logger: true })
|
|
103
|
+
this.ttsDir = join(process.cwd(), 'tmp', 'tts')
|
|
104
|
+
this.backendUrl = config.backendUrl ?? `http://localhost:${config.port}`
|
|
105
|
+
this.buildVersion = randomUUID()
|
|
106
|
+
|
|
107
|
+
// Initialize TTS providers based on config
|
|
108
|
+
if (config.tts.openaiApiKey) {
|
|
109
|
+
this.openai = new OpenAI({ apiKey: config.tts.openaiApiKey })
|
|
110
|
+
}
|
|
111
|
+
if (config.tts.tiktokSessionId) {
|
|
112
|
+
configureTikTokTTS(config.tts.tiktokSessionId)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// =========================================================================
|
|
117
|
+
// Lifecycle
|
|
118
|
+
// =========================================================================
|
|
119
|
+
|
|
120
|
+
async start(): Promise<void> {
|
|
121
|
+
// Lazy-init ElevenLabs (optional dep)
|
|
122
|
+
if (this.config.tts.elevenlabsApiKey && !this.elevenlabs) {
|
|
123
|
+
try {
|
|
124
|
+
const { ElevenLabsClient } = await import('@elevenlabs/elevenlabs-js')
|
|
125
|
+
this.elevenlabs = new ElevenLabsClient({ apiKey: this.config.tts.elevenlabsApiKey })
|
|
126
|
+
} catch {
|
|
127
|
+
this.logger.warn('ElevenLabs SDK not installed, ElevenLabs TTS disabled')
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await this.fastify.register(cors, { origin: true })
|
|
132
|
+
await mkdir(this.ttsDir, { recursive: true })
|
|
133
|
+
await this.fastify.register(fastifyStatic, {
|
|
134
|
+
root: this.ttsDir,
|
|
135
|
+
prefix: '/tts/',
|
|
136
|
+
decorateReply: false,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
this.io = new Server(this.fastify.server, {
|
|
140
|
+
cors: { origin: '*' },
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
this.registerSocketHandlers()
|
|
144
|
+
this.registerHttpRoutes()
|
|
145
|
+
await this.startChatSystem()
|
|
146
|
+
|
|
147
|
+
const host = this.config.bindHost
|
|
148
|
+
await this.fastify.listen({ port: this.config.port, host })
|
|
149
|
+
|
|
150
|
+
this.pollMarketCap()
|
|
151
|
+
this.mcapInterval = setInterval(() => this.pollMarketCap(), 10_000)
|
|
152
|
+
|
|
153
|
+
this.logger.info(`Backend started on ${host}:${this.config.port}`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async stop(): Promise<void> {
|
|
157
|
+
if (this.mcapInterval) {
|
|
158
|
+
clearInterval(this.mcapInterval)
|
|
159
|
+
this.mcapInterval = null
|
|
160
|
+
}
|
|
161
|
+
this.coordinator?.stop()
|
|
162
|
+
this.chatManager?.disconnectAll()
|
|
163
|
+
this.io?.close()
|
|
164
|
+
await this.fastify.close()
|
|
165
|
+
this.logger.info('Backend stopped')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// =========================================================================
|
|
169
|
+
// Public API (used by plugin tool handlers)
|
|
170
|
+
// =========================================================================
|
|
171
|
+
|
|
172
|
+
/** Speak on the livestream — emits overlay event + TTS. */
|
|
173
|
+
async handleTalk(text: string): Promise<{ spoken: boolean }> {
|
|
174
|
+
if (!text || typeof text !== 'string') {
|
|
175
|
+
return { spoken: false }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.coordinator?.notifySpeech()
|
|
179
|
+
|
|
180
|
+
const id = randomUUID()
|
|
181
|
+
try {
|
|
182
|
+
const ttsUrl = await this.generateTTSWithFallback(text, this.config.tts.bot)
|
|
183
|
+
this.logger.info(`TTS generated: ${ttsUrl}`)
|
|
184
|
+
this.io.emit('crawd:talk', { id, message: text, ttsUrl })
|
|
185
|
+
} catch (e) {
|
|
186
|
+
this.logger.error('Failed to generate TTS, emitting without audio', e)
|
|
187
|
+
this.io.emit('crawd:talk', { id, message: text, ttsUrl: '' })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { spoken: true }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Reply to a chat message — reads original aloud (chat voice),
|
|
195
|
+
* then speaks bot reply (bot voice). Emits `crawd:reply-turn`.
|
|
196
|
+
*/
|
|
197
|
+
async handleReply(
|
|
198
|
+
text: string,
|
|
199
|
+
chat: { username: string; message: string },
|
|
200
|
+
): Promise<{ spoken: boolean }> {
|
|
201
|
+
if (!text || typeof text !== 'string') {
|
|
202
|
+
return { spoken: false }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.coordinator?.notifySpeech()
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const [chatTtsUrl, botTtsUrl] = await Promise.all([
|
|
209
|
+
this.generateTTSWithFallback(`Chat says: ${chat.message}`, this.config.tts.chat),
|
|
210
|
+
this.generateTTSWithFallback(text, this.config.tts.bot),
|
|
211
|
+
])
|
|
212
|
+
this.io.emit('crawd:reply-turn', {
|
|
213
|
+
chat: { username: chat.username, message: chat.message },
|
|
214
|
+
botMessage: text,
|
|
215
|
+
chatTtsUrl,
|
|
216
|
+
botTtsUrl,
|
|
217
|
+
})
|
|
218
|
+
} catch (e) {
|
|
219
|
+
this.logger.error('Failed to generate reply-turn TTS, falling back to talk', e)
|
|
220
|
+
const id = randomUUID()
|
|
221
|
+
this.generateTTSWithFallback(text, this.config.tts.bot)
|
|
222
|
+
.then((ttsUrl) => this.io.emit('crawd:talk', { id, message: text, ttsUrl }))
|
|
223
|
+
.catch(() => this.io.emit('crawd:talk', { id, message: text, ttsUrl: '' }))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { spoken: true }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
getIO(): Server {
|
|
230
|
+
return this.io
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// =========================================================================
|
|
234
|
+
// TTS (with ordered fallback chain)
|
|
235
|
+
// =========================================================================
|
|
236
|
+
|
|
237
|
+
async generateTTSWithFallback(text: string, chain: TtsVoiceEntry[]): Promise<string> {
|
|
238
|
+
let lastError: Error | null = null
|
|
239
|
+
|
|
240
|
+
for (const entry of chain) {
|
|
241
|
+
try {
|
|
242
|
+
switch (entry.provider) {
|
|
243
|
+
case 'elevenlabs':
|
|
244
|
+
return await this.generateElevenLabsTTS(text, entry.voice)
|
|
245
|
+
case 'openai':
|
|
246
|
+
return await this.generateOpenAITTS(text, entry.voice)
|
|
247
|
+
case 'tiktok':
|
|
248
|
+
return await this.generateTikTokTTSFile(text, entry.voice)
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
lastError = e instanceof Error ? e : new Error(String(e))
|
|
252
|
+
this.logger.warn(`TTS ${entry.provider}/${entry.voice} failed: ${lastError.message}, trying next...`)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
throw lastError ?? new Error('No TTS providers configured')
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private async generateOpenAITTS(text: string, voice: string): Promise<string> {
|
|
260
|
+
if (!this.openai) throw new Error('OpenAI not configured (missing apiKey)')
|
|
261
|
+
|
|
262
|
+
const response = await this.openai.audio.speech.create({
|
|
263
|
+
model: 'gpt-4o-mini-tts',
|
|
264
|
+
voice: voice as 'onyx',
|
|
265
|
+
input: text,
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const buffer = Buffer.from(await response.arrayBuffer())
|
|
269
|
+
return await this.saveTTSFile(buffer)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private async generateElevenLabsTTS(text: string, voiceId: string): Promise<string> {
|
|
273
|
+
if (!this.elevenlabs) throw new Error('ElevenLabs not configured (missing apiKey)')
|
|
274
|
+
|
|
275
|
+
const audio = await this.elevenlabs.textToSpeech.convert(voiceId, {
|
|
276
|
+
modelId: 'eleven_multilingual_v2',
|
|
277
|
+
text,
|
|
278
|
+
outputFormat: 'mp3_44100_128',
|
|
279
|
+
voiceSettings: {
|
|
280
|
+
stability: 0,
|
|
281
|
+
similarityBoost: 1.0,
|
|
282
|
+
useSpeakerBoost: true,
|
|
283
|
+
speed: 1.0,
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
const response = new Response(audio as any)
|
|
288
|
+
const arrayBuffer = await response.arrayBuffer()
|
|
289
|
+
const buffer = Buffer.from(arrayBuffer)
|
|
290
|
+
|
|
291
|
+
// Check if response is valid MP3
|
|
292
|
+
const isMP3 =
|
|
293
|
+
(buffer[0] === 0x49 && buffer[1] === 0x44 && buffer[2] === 0x33) ||
|
|
294
|
+
(buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0)
|
|
295
|
+
|
|
296
|
+
if (!isMP3) {
|
|
297
|
+
const preview = buffer.subarray(0, 200).toString('utf-8')
|
|
298
|
+
throw new Error(`ElevenLabs returned non-audio response: ${preview.slice(0, 100)}`)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return await this.saveTTSFile(buffer)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private async generateTikTokTTSFile(text: string, voice?: string): Promise<string> {
|
|
305
|
+
const buffer = await generateTikTokTTS(text, voice)
|
|
306
|
+
return await this.saveTTSFile(buffer)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private async saveTTSFile(buffer: Buffer): Promise<string> {
|
|
310
|
+
const filename = `${randomUUID()}.mp3`
|
|
311
|
+
await mkdir(this.ttsDir, { recursive: true })
|
|
312
|
+
await writeFile(join(this.ttsDir, filename), buffer)
|
|
313
|
+
this.logger.info(`TTS file written: ${filename}, size: ${buffer.length} bytes`)
|
|
314
|
+
return `${this.backendUrl}/tts/${filename}`
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// =========================================================================
|
|
318
|
+
// Chat system + Coordinator
|
|
319
|
+
// =========================================================================
|
|
320
|
+
|
|
321
|
+
private async startChatSystem(): Promise<void> {
|
|
322
|
+
this.chatManager = new ChatManager()
|
|
323
|
+
|
|
324
|
+
const pf = this.config.chat.pumpfun
|
|
325
|
+
if (pf.enabled && pf.tokenMint) {
|
|
326
|
+
this.chatManager.registerClient(
|
|
327
|
+
'pumpfun',
|
|
328
|
+
new PumpFunChatClient(pf.tokenMint, pf.authToken ?? null),
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const yt = this.config.chat.youtube
|
|
333
|
+
if (yt.enabled && yt.videoId) {
|
|
334
|
+
this.chatManager.registerClient('youtube', new YouTubeChatClient(yt.videoId))
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this.chatManager.onMessage((msg: ChatMessage) => {
|
|
338
|
+
this.fastify.log.info({ platform: msg.platform, user: msg.username }, 'chat message')
|
|
339
|
+
this.io.emit('crawd:chat', msg)
|
|
340
|
+
this.coordinator?.onMessage(msg)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
if (this.config.gatewayUrl) {
|
|
344
|
+
const gateway = new OneShotGateway(
|
|
345
|
+
this.config.gatewayUrl,
|
|
346
|
+
this.config.gatewayToken ?? '',
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
const coordConfig: Partial<CoordinatorConfig> = {
|
|
350
|
+
vibeEnabled: this.config.vibe.enabled,
|
|
351
|
+
vibeIntervalMs: this.config.vibe.intervalMs,
|
|
352
|
+
idleAfterMs: this.config.vibe.idleAfterMs,
|
|
353
|
+
sleepAfterIdleMs: this.config.vibe.sleepAfterIdleMs,
|
|
354
|
+
}
|
|
355
|
+
if (this.config.vibe.prompt) {
|
|
356
|
+
coordConfig.vibePrompt = this.config.vibe.prompt
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
this.coordinator = new Coordinator(
|
|
360
|
+
gateway.triggerAgent.bind(gateway),
|
|
361
|
+
coordConfig,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
this.coordinator.setOnEvent((event: CoordinatorEvent) => {
|
|
365
|
+
if (event.type === 'stateChange') {
|
|
366
|
+
this.io.emit('crawd:status', { status: event.to })
|
|
367
|
+
} else if (event.type === 'vibeExecuted' && !event.skipped) {
|
|
368
|
+
this.io.emit('crawd:status', { status: 'vibing' })
|
|
369
|
+
} else if (event.type === 'chatProcessed') {
|
|
370
|
+
this.io.emit('crawd:status', { status: 'chatting' })
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
this.coordinator.start()
|
|
375
|
+
this.fastify.log.info('Coordinator started (one-shot gateway)')
|
|
376
|
+
} else {
|
|
377
|
+
this.fastify.log.warn('Gateway not configured — coordinator disabled')
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
await this.chatManager.connectAll()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// =========================================================================
|
|
384
|
+
// Socket.IO handlers
|
|
385
|
+
// =========================================================================
|
|
386
|
+
|
|
387
|
+
private registerSocketHandlers(): void {
|
|
388
|
+
this.io.on('connection', (socket) => {
|
|
389
|
+
this.fastify.log.info(`socket connected: ${socket.id}`)
|
|
390
|
+
|
|
391
|
+
if (this.latestMcap !== null) {
|
|
392
|
+
socket.emit('crawd:mcap', { mcap: this.latestMcap })
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
socket.on('crawd:mock-chat', (data: { username?: string; message?: string }) => {
|
|
396
|
+
const { username, message } = data
|
|
397
|
+
if (!username || !message) return
|
|
398
|
+
const mockMsg: ChatMessage = {
|
|
399
|
+
id: randomUUID(),
|
|
400
|
+
shortId: generateShortId(),
|
|
401
|
+
username,
|
|
402
|
+
message,
|
|
403
|
+
platform: 'youtube',
|
|
404
|
+
timestamp: Date.now(),
|
|
405
|
+
}
|
|
406
|
+
this.fastify.log.info({ username, message }, 'mock chat (socket)')
|
|
407
|
+
this.io.emit('crawd:chat', mockMsg)
|
|
408
|
+
if (this.coordinator) {
|
|
409
|
+
this.coordinator.onMessage(mockMsg)
|
|
410
|
+
} else {
|
|
411
|
+
this.fastify.log.warn('mock chat: coordinator is null — gateway not configured or failed to connect')
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
socket.on('disconnect', () => {
|
|
416
|
+
this.fastify.log.info(`socket disconnected: ${socket.id}`)
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// =========================================================================
|
|
422
|
+
// HTTP routes
|
|
423
|
+
// =========================================================================
|
|
424
|
+
|
|
425
|
+
private registerHttpRoutes(): void {
|
|
426
|
+
this.fastify.post<{ Body: { message: string } }>(
|
|
427
|
+
'/crawd/talk',
|
|
428
|
+
async (request, reply) => {
|
|
429
|
+
const { message } = request.body
|
|
430
|
+
if (!message || typeof message !== 'string') {
|
|
431
|
+
return reply.status(400).send({ error: 'message is required' })
|
|
432
|
+
}
|
|
433
|
+
await this.handleTalk(message)
|
|
434
|
+
return { ok: true }
|
|
435
|
+
},
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
this.fastify.get('/chat/status', async () => {
|
|
439
|
+
return { connected: this.chatManager?.getConnectedKeys() ?? [] }
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
this.fastify.get('/version', async () => {
|
|
443
|
+
return { version: this.buildVersion }
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
this.fastify.get('/coordinator/status', async () => {
|
|
447
|
+
if (!this.coordinator) return { enabled: false }
|
|
448
|
+
return { enabled: true, ...this.coordinator.getState() }
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
this.fastify.post<{ Body: Partial<CoordinatorConfig> }>(
|
|
452
|
+
'/coordinator/config',
|
|
453
|
+
async (request, reply) => {
|
|
454
|
+
if (!this.coordinator) {
|
|
455
|
+
return reply.status(400).send({ error: 'Coordinator not enabled' })
|
|
456
|
+
}
|
|
457
|
+
this.coordinator.updateConfig(request.body)
|
|
458
|
+
return { ok: true, ...this.coordinator.getState() }
|
|
459
|
+
},
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
this.fastify.post<{ Body: { username: string; message: string } }>(
|
|
463
|
+
'/mock/chat',
|
|
464
|
+
async (request, reply) => {
|
|
465
|
+
const { username, message } = request.body
|
|
466
|
+
if (!username || !message) {
|
|
467
|
+
return reply.status(400).send({ error: 'username and message are required' })
|
|
468
|
+
}
|
|
469
|
+
const id = randomUUID()
|
|
470
|
+
const mockMsg: ChatMessage = {
|
|
471
|
+
id,
|
|
472
|
+
shortId: generateShortId(),
|
|
473
|
+
username,
|
|
474
|
+
message,
|
|
475
|
+
platform: 'youtube',
|
|
476
|
+
timestamp: Date.now(),
|
|
477
|
+
}
|
|
478
|
+
this.fastify.log.info({ username, message }, 'mock chat message')
|
|
479
|
+
this.io.emit('crawd:chat', mockMsg)
|
|
480
|
+
this.coordinator?.onMessage(mockMsg)
|
|
481
|
+
return { ok: true, id }
|
|
482
|
+
},
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
this.fastify.post<{ Body: { username: string; message: string; response: string } }>(
|
|
486
|
+
'/mock/turn',
|
|
487
|
+
async (request, reply) => {
|
|
488
|
+
const { username, message, response } = request.body
|
|
489
|
+
if (!username || !message || !response) {
|
|
490
|
+
return reply.status(400).send({ error: 'username, message, and response are required' })
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
const [chatTtsUrl, botTtsUrl] = await Promise.all([
|
|
495
|
+
this.generateTTSWithFallback(`Chat says: ${message}`, this.config.tts.chat),
|
|
496
|
+
this.generateTTSWithFallback(response, this.config.tts.bot),
|
|
497
|
+
])
|
|
498
|
+
this.io.emit('crawd:reply-turn', {
|
|
499
|
+
chat: { username, message },
|
|
500
|
+
botMessage: response,
|
|
501
|
+
chatTtsUrl,
|
|
502
|
+
botTtsUrl,
|
|
503
|
+
})
|
|
504
|
+
return { ok: true }
|
|
505
|
+
} catch (e) {
|
|
506
|
+
this.fastify.log.error(e, 'failed to generate mock turn TTS')
|
|
507
|
+
return reply.status(500).send({ error: 'Failed to generate TTS' })
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// =========================================================================
|
|
514
|
+
// Market cap polling
|
|
515
|
+
// =========================================================================
|
|
516
|
+
|
|
517
|
+
private async pollMarketCap(): Promise<void> {
|
|
518
|
+
const mint = this.config.chat.pumpfun.tokenMint
|
|
519
|
+
if (!mint) return
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
const coin = await pumpfun.getCoin(mint)
|
|
523
|
+
this.latestMcap = coin.usd_market_cap
|
|
524
|
+
this.io.emit('crawd:mcap', { mcap: this.latestMcap })
|
|
525
|
+
} catch (e) {
|
|
526
|
+
this.fastify.log.error(e, 'failed to fetch market cap')
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
// Config builder: create CrawdConfig from environment variables
|
|
533
|
+
// (used by standalone mode in backend/index.ts)
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
|
|
536
|
+
export function configFromEnv(): CrawdConfig {
|
|
537
|
+
const port = Number(process.env.PORT || 4000)
|
|
538
|
+
|
|
539
|
+
const botChain: TtsVoiceEntry[] = []
|
|
540
|
+
const chatChain: TtsVoiceEntry[] = []
|
|
541
|
+
|
|
542
|
+
if (process.env.ELEVENLABS_API_KEY) {
|
|
543
|
+
botChain.push({ provider: 'elevenlabs', voice: process.env.TTS_BOT_VOICE || 'TX3LPaxmHKxFdv7VOQHJ' })
|
|
544
|
+
}
|
|
545
|
+
if (process.env.OPENAI_API_KEY) {
|
|
546
|
+
botChain.push({ provider: 'openai', voice: process.env.TTS_BOT_VOICE || 'onyx' })
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (process.env.TIKTOK_SESSION_ID) {
|
|
550
|
+
chatChain.push({ provider: 'tiktok', voice: process.env.TTS_CHAT_VOICE || 'en_us_002' })
|
|
551
|
+
}
|
|
552
|
+
if (process.env.OPENAI_API_KEY) {
|
|
553
|
+
chatChain.push({ provider: 'openai', voice: process.env.TTS_CHAT_VOICE || 'onyx' })
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
enabled: true,
|
|
558
|
+
port,
|
|
559
|
+
bindHost: process.env.BIND_HOST || '0.0.0.0',
|
|
560
|
+
backendUrl: process.env.BACKEND_URL || `http://localhost:${port}`,
|
|
561
|
+
tts: {
|
|
562
|
+
chat: chatChain,
|
|
563
|
+
bot: botChain,
|
|
564
|
+
openaiApiKey: process.env.OPENAI_API_KEY,
|
|
565
|
+
elevenlabsApiKey: process.env.ELEVENLABS_API_KEY,
|
|
566
|
+
tiktokSessionId: process.env.TIKTOK_SESSION_ID,
|
|
567
|
+
},
|
|
568
|
+
vibe: {
|
|
569
|
+
enabled: process.env.VIBE_ENABLED !== 'false',
|
|
570
|
+
intervalMs: Number(process.env.VIBE_INTERVAL_MS || 30_000),
|
|
571
|
+
idleAfterMs: Number(process.env.IDLE_AFTER_MS || 180_000),
|
|
572
|
+
sleepAfterIdleMs: Number(process.env.SLEEP_AFTER_IDLE_MS || 180_000),
|
|
573
|
+
prompt: process.env.VIBE_PROMPT,
|
|
574
|
+
},
|
|
575
|
+
chat: {
|
|
576
|
+
youtube: {
|
|
577
|
+
enabled: process.env.YOUTUBE_ENABLED === 'true',
|
|
578
|
+
videoId: process.env.YOUTUBE_VIDEO_ID,
|
|
579
|
+
},
|
|
580
|
+
pumpfun: {
|
|
581
|
+
enabled: process.env.PUMPFUN_ENABLED !== 'false',
|
|
582
|
+
tokenMint: process.env.NEXT_PUBLIC_TOKEN_MINT,
|
|
583
|
+
authToken: process.env.PUMPFUN_AUTH_TOKEN,
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
gatewayUrl: process.env.OPENCLAW_GATEWAY_URL,
|
|
587
|
+
gatewayToken: process.env.OPENCLAW_GATEWAY_TOKEN,
|
|
588
|
+
}
|
|
589
|
+
}
|