@lievclouds/dsh-cortex 0.5.4 → 1.0.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.

Potentially problematic release.


This version of @lievclouds/dsh-cortex might be problematic. Click here for more details.

Files changed (44) hide show
  1. package/README.md +1 -1
  2. package/client.js +434 -412
  3. package/index.js +7 -0
  4. package/locale/en.json +6 -6
  5. package/locale/zh.json +6 -6
  6. package/package.json +115 -110
  7. package/src/cortex-ui/dsh/css-overrides.css +8 -0
  8. package/src/cortex-ui/renderer/App.tsx +39 -0
  9. package/src/cortex-ui/renderer/components/miniapp/FileTree.tsx +161 -0
  10. package/src/cortex-ui/renderer/components/miniapp/MiniappCard.tsx +263 -0
  11. package/src/cortex-ui/renderer/components/miniapp/MiniappDetail.tsx +1493 -0
  12. package/src/cortex-ui/renderer/components/retest/CurlImportDialog.tsx +158 -0
  13. package/src/cortex-ui/renderer/components/retest/RepeaterCollectionsDialog.tsx +327 -0
  14. package/src/cortex-ui/renderer/components/retest/RepeaterHistoryDialog.tsx +242 -0
  15. package/src/cortex-ui/renderer/components/retest/RepeaterPanel.tsx +291 -137
  16. package/src/cortex-ui/renderer/components/retest/YakitSendPanel.tsx +411 -0
  17. package/src/cortex-ui/renderer/i18n/messages/common.ts +2 -0
  18. package/src/cortex-ui/renderer/i18n/messages/fuzzer.ts +32 -0
  19. package/src/cortex-ui/renderer/i18n/messages/index.ts +5 -1
  20. package/src/cortex-ui/renderer/i18n/messages/miniapp.ts +392 -0
  21. package/src/cortex-ui/renderer/i18n/messages/retest.ts +144 -2
  22. package/src/cortex-ui/renderer/lib/curl.ts +635 -0
  23. package/src/cortex-ui/renderer/lib/miniappApi.ts +459 -0
  24. package/src/cortex-ui/renderer/lib/navigation.ts +13 -1
  25. package/src/cortex-ui/renderer/lib/repeaterRaw.ts +102 -0
  26. package/src/cortex-ui/renderer/lib/repeaterStoreApi.ts +163 -0
  27. package/src/cortex-ui/renderer/lib/retestResponse.ts +64 -17
  28. package/src/cortex-ui/renderer/pages/FuzzerPage.tsx +212 -0
  29. package/src/cortex-ui/renderer/pages/MiniappPage.tsx +664 -0
  30. package/src/host/cortex/index.js +42 -1
  31. package/src/host/cortex/repeater-store.js +411 -0
  32. package/src/host/cortex/repeater.js +15 -2
  33. package/src/host/cortex/tools.js +2 -0
  34. package/src/host/miniapp/analyze.js +1078 -0
  35. package/src/host/miniapp/assets-import.js +294 -0
  36. package/src/host/miniapp/detect.js +426 -0
  37. package/src/host/miniapp/format.js +263 -0
  38. package/src/host/miniapp/roots.js +360 -0
  39. package/src/host/miniapp/routes.js +304 -0
  40. package/src/host/miniapp/service.js +559 -0
  41. package/src/host/miniapp/store.js +756 -0
  42. package/src/host/miniapp/tools.js +335 -0
  43. package/src/host/miniapp/unpack.js +1010 -0
  44. package/src/host/miniapp/wxapkg.js +416 -0
@@ -0,0 +1,411 @@
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import { ArrowRight, Braces, CornerDownRight, Loader2, Play } from 'lucide-react'
3
+ import type { Vulnerability } from '@shared/types'
4
+ import type { RepeaterSendResult } from '@shared/repeater'
5
+ import { Button } from '@renderer/components/ui/button'
6
+ import { Checkbox } from '@renderer/components/ui/checkbox'
7
+ import { Skeleton } from '@renderer/components/ui/skeleton'
8
+ import { useI18n } from '@renderer/i18n'
9
+ import { getApi } from '@renderer/lib/api'
10
+ import {
11
+ formatRequestBody,
12
+ parseRawRequest,
13
+ stripDerivedHeaders
14
+ } from '@renderer/lib/repeaterRaw'
15
+ import {
16
+ defaultRequestIndex,
17
+ extractRepeaterRequests,
18
+ resolveRepeaterUrl
19
+ } from '@renderer/lib/retestRequest'
20
+ import {
21
+ buildHtmlPreview,
22
+ headerValue,
23
+ isHtmlResponse,
24
+ isRedirectResponse,
25
+ prettyJsonBody,
26
+ statusTone
27
+ } from '@renderer/lib/retestResponse'
28
+ import { cn } from '@renderer/lib/utils'
29
+
30
+ interface YakitSendPanelProps {
31
+ vuln: Vulnerability | null
32
+ loading: boolean
33
+ }
34
+
35
+ function describeSendError(cause: unknown, connectionRefused: string): string {
36
+ const raw = cause instanceof Error ? cause.message : String(cause)
37
+ const cleaned = raw
38
+ .replace(/^Error invoking remote method '[^']+':\s*/, '')
39
+ .replace(/^Error:\s*/, '')
40
+ .trim()
41
+ if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i.test(cleaned)) return connectionRefused
42
+ return cleaned === '' ? connectionRefused : cleaned
43
+ }
44
+
45
+ /**
46
+ * Yakit-style manual HTTP sender: the request editor fills the left half with
47
+ * the send button at its top-left, the response viewer fills the right half, and
48
+ * both panes use the full available height. This is the plain Web Fuzzer module
49
+ * (no payload markers, attack modes, batch jobs or result table); every request
50
+ * still goes through the retest proxy and the finding origin allowlist.
51
+ */
52
+ export function YakitSendPanel({ vuln, loading }: YakitSendPanelProps) {
53
+ const { t } = useI18n()
54
+ const [raw, setRaw] = useState('')
55
+ const [step, setStep] = useState(0)
56
+ const [sending, setSending] = useState(false)
57
+ const [followRedirects, setFollowRedirects] = useState(false)
58
+ const [bodyView, setBodyView] = useState<'raw' | 'pretty' | 'html'>('raw')
59
+ const [error, setError] = useState<string | null>(null)
60
+ const [result, setResult] = useState<RepeaterSendResult | null>(null)
61
+
62
+ const requests = useMemo(() => (vuln === null ? [] : extractRepeaterRequests(vuln)), [vuln])
63
+
64
+ useEffect(() => {
65
+ setResult(null)
66
+ setError(null)
67
+ if (vuln === null) {
68
+ setRaw('')
69
+ setStep(0)
70
+ return
71
+ }
72
+ const nextStep = defaultRequestIndex(vuln, requests)
73
+ setStep(nextStep)
74
+ const initial = requests[nextStep]?.raw ?? ''
75
+ setRaw(formatRequestBody(initial) ?? initial)
76
+ // Reset only when the selected vulnerability changes; user edits must survive.
77
+ // eslint-disable-next-line react-hooks/exhaustive-deps
78
+ }, [vuln?.id])
79
+
80
+ const selectStep = (nextStep: number): void => {
81
+ const next = requests[nextStep]
82
+ if (next === undefined) return
83
+ setStep(nextStep)
84
+ setRaw(formatRequestBody(next.raw) ?? next.raw)
85
+ setResult(null)
86
+ setError(null)
87
+ }
88
+
89
+ /** Pretty-print the JSON request body while keeping the request line/headers. */
90
+ const formatRequest = (): void => {
91
+ const formatted = formatRequestBody(raw)
92
+ if (formatted === null) {
93
+ setError(t('retest.repeater.formatRequestFailed'))
94
+ return
95
+ }
96
+ setRaw(formatted)
97
+ setError(null)
98
+ }
99
+
100
+ const rawBytes = useMemo(() => new TextEncoder().encode(raw).byteLength, [raw])
101
+ const responseIsRedirect =
102
+ result !== null && isRedirectResponse(result.response.status, result.response.headers)
103
+ const responseIsHtml =
104
+ result !== null && isHtmlResponse(result.response.body, result.response.headers)
105
+ const formattedJson = useMemo(
106
+ () =>
107
+ result === null
108
+ ? null
109
+ : prettyJsonBody(result.response.body, headerValue(result.response.headers, 'content-type')),
110
+ [result]
111
+ )
112
+ const htmlPreview = useMemo(
113
+ () =>
114
+ result === null || vuln === null
115
+ ? ''
116
+ : buildHtmlPreview(result.response.body, vuln.target_url),
117
+ [result, vuln]
118
+ )
119
+ /** Status line + response headers, shared by the raw and formatted views. */
120
+ const responseHead = useMemo(() => {
121
+ if (result === null) return ''
122
+ const lines = [`HTTP/1.1 ${result.response.status}`]
123
+ for (const [name, value] of Object.entries(result.response.headers)) {
124
+ lines.push(`${name}: ${value}`)
125
+ }
126
+ return lines.join('\n')
127
+ }, [result])
128
+ /** Raw response as one block: status + headers + blank line + body. */
129
+ const rawResponse = useMemo(
130
+ () => (result === null ? '' : `${responseHead}\n\n${result.response.body}`),
131
+ [result, responseHead]
132
+ )
133
+ /** JSON view keeps the status/header block and only formats the body. */
134
+ const jsonResponse = useMemo(
135
+ () => (formattedJson === null ? '' : `${responseHead}\n\n${formattedJson}`),
136
+ [formattedJson, responseHead]
137
+ )
138
+
139
+ const send = async (): Promise<void> => {
140
+ if (vuln === null || sending) return
141
+ const api = getApi()
142
+ if (!api) {
143
+ setError(t('common.apiUnavailable'))
144
+ return
145
+ }
146
+ setSending(true)
147
+ setError(null)
148
+ try {
149
+ const parsed = parseRawRequest(raw)
150
+ const response = await api.sendRepeaterRequest({
151
+ vulnId: vuln.id,
152
+ method: parsed.method,
153
+ url: resolveRepeaterUrl(parsed.url, vuln.target_url),
154
+ headers: stripDerivedHeaders(parsed.headers),
155
+ ...(parsed.body === '' ? {} : { body: parsed.body }),
156
+ followRedirects
157
+ })
158
+ setResult(response)
159
+ // Formatted JSON is the default response view; raw stays one click away.
160
+ setBodyView(
161
+ prettyJsonBody(response.response.body, headerValue(response.response.headers, 'content-type')) !== null
162
+ ? 'pretty'
163
+ : 'raw'
164
+ )
165
+ } catch (cause) {
166
+ setError(describeSendError(cause, t('retest.repeater.connectionFailed')))
167
+ setResult(null)
168
+ } finally {
169
+ setSending(false)
170
+ }
171
+ }
172
+
173
+ if (loading && vuln === null) {
174
+ return (
175
+ <div className="grid min-h-0 flex-1 gap-3 lg:grid-cols-2">
176
+ <Skeleton className="h-full min-h-[360px] w-full" />
177
+ <Skeleton className="h-full min-h-[360px] w-full" />
178
+ </div>
179
+ )
180
+ }
181
+
182
+ if (vuln === null) {
183
+ return (
184
+ <div className="flex min-h-0 flex-1 items-center justify-center rounded-lg border border-dashed border-border bg-background/60 text-[12px] text-muted-foreground">
185
+ {t('retest.repeater.empty')}
186
+ </div>
187
+ )
188
+ }
189
+
190
+ return (
191
+ <div
192
+ data-testid="fuzzer-panel"
193
+ className="grid min-h-0 flex-1 items-stretch gap-3 lg:grid-cols-2"
194
+ >
195
+ {/* ------------------------------------------------------------- request */}
196
+ <section
197
+ data-testid="fuzzer-request-pane"
198
+ aria-label={t('fuzzer.request.paneAria')}
199
+ className="flex min-h-0 flex-col overflow-hidden rounded-lg border border-border bg-background"
200
+ >
201
+ <div className="flex h-12 shrink-0 items-center gap-2 border-b border-border px-3">
202
+ <Button
203
+ size="sm"
204
+ data-testid="fuzzer-send"
205
+ onClick={() => void send()}
206
+ disabled={sending || raw.trim() === ''}
207
+ className="gap-1.5"
208
+ >
209
+ {sending ? (
210
+ <Loader2 aria-hidden="true" className="h-3.5 w-3.5 animate-spin poc-spinner" />
211
+ ) : (
212
+ <Play aria-hidden="true" className="h-3.5 w-3.5" />
213
+ )}
214
+ {sending ? t('retest.repeater.sending') : t('retest.repeater.send')}
215
+ </Button>
216
+ <label className="flex shrink-0 cursor-pointer items-center gap-1.5 text-[10.5px] text-muted-foreground">
217
+ <Checkbox
218
+ data-testid="fuzzer-follow-redirects"
219
+ checked={followRedirects}
220
+ onChange={(event) => setFollowRedirects(event.target.checked)}
221
+ />
222
+ {t('retest.repeater.followRedirects')}
223
+ </label>
224
+ <Button
225
+ variant="outline"
226
+ size="xs"
227
+ data-testid="fuzzer-format-request"
228
+ title={t('retest.repeater.formatRequestTitle')}
229
+ onClick={formatRequest}
230
+ className="shrink-0 gap-1"
231
+ >
232
+ <Braces aria-hidden="true" className="h-3 w-3" />
233
+ {t('retest.repeater.formatRequest')}
234
+ </Button>
235
+ <span className="shrink-0 rounded border border-border bg-sidebar px-1.5 py-0.5 font-mono text-[9.5px] text-muted-foreground">
236
+ {vuln.id}
237
+ </span>
238
+ {requests.length > 0 ? (
239
+ <div
240
+ className="ml-auto flex min-w-0 items-center gap-1 overflow-x-auto"
241
+ role="tablist"
242
+ aria-label={t('retest.repeater.stepsAria')}
243
+ >
244
+ {requests.map((request, index) => (
245
+ <button
246
+ key={`${index}-${request.label}`}
247
+ type="button"
248
+ role="tab"
249
+ aria-selected={index === step}
250
+ title={request.label}
251
+ data-testid={`fuzzer-request-step-${index}`}
252
+ onClick={() => selectStep(index)}
253
+ className={cn(
254
+ 'h-6 shrink-0 rounded border px-2 font-mono text-[10px] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
255
+ index === step
256
+ ? 'border-accent/50 bg-accent-soft font-medium text-accent'
257
+ : 'border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground'
258
+ )}
259
+ >
260
+ {t('retest.repeater.stepN', { n: index + 1 })}
261
+ </button>
262
+ ))}
263
+ </div>
264
+ ) : null}
265
+ <span className={cn('shrink-0 font-mono text-[9.5px] tabular-nums text-muted-foreground', requests.length === 0 && 'ml-auto')}>
266
+ {t('retest.repeater.bytes', { count: rawBytes })}
267
+ </span>
268
+ </div>
269
+
270
+ <div className="flex min-h-0 flex-1 flex-col p-3">
271
+ <textarea
272
+ data-testid="fuzzer-request"
273
+ aria-label={t('retest.repeater.requestAria')}
274
+ value={raw}
275
+ spellCheck={false}
276
+ onChange={(event) => setRaw(event.target.value)}
277
+ className="min-h-0 w-full flex-1 resize-none rounded-lg border border-border bg-white px-3 py-2.5 font-mono text-[11.5px] leading-relaxed text-foreground caret-accent placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
278
+ />
279
+ {error !== null ? (
280
+ <p
281
+ role="alert"
282
+ data-testid="fuzzer-send-error"
283
+ className="mt-2 rounded-md border border-red-200 bg-red-50 px-2.5 py-1.5 text-[10.5px] leading-relaxed text-red-700"
284
+ >
285
+ {error}
286
+ </p>
287
+ ) : null}
288
+ </div>
289
+ </section>
290
+
291
+ {/* ------------------------------------------------------------ response */}
292
+ <section
293
+ data-testid="fuzzer-response-pane"
294
+ aria-label={t('fuzzer.response.aria')}
295
+ className="flex min-h-0 flex-col overflow-hidden rounded-lg border border-border bg-background"
296
+ >
297
+ <div className="flex h-12 shrink-0 items-center gap-2 border-b border-border px-3">
298
+ <ArrowRight aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
299
+ <span className="shrink-0 text-[12px] font-medium text-foreground">{t('retest.repeater.responseTitle')}</span>
300
+ {result !== null ? (
301
+ <>
302
+ <span
303
+ data-testid="fuzzer-response-status"
304
+ className={`rounded border px-1.5 py-0.5 font-mono text-[10px] tabular-nums ${statusTone(result.response.status)}`}
305
+ >
306
+ {result.response.status}
307
+ </span>
308
+ <span className="font-mono text-[9.5px] tabular-nums text-muted-foreground">
309
+ {result.response.durationMs} ms · {result.response.body.length} B
310
+ </span>
311
+ {result.response.truncated ? (
312
+ <span className="text-[9.5px] text-amber-700">{t('retest.repeater.truncated')}</span>
313
+ ) : null}
314
+ {responseIsRedirect ? (
315
+ <Button
316
+ variant="outline"
317
+ size="xs"
318
+ onClick={() => void send()}
319
+ disabled={sending}
320
+ className="gap-1 text-[10.5px]"
321
+ >
322
+ <CornerDownRight aria-hidden="true" className="h-3 w-3" />
323
+ {t('retest.repeater.followRedirect')}
324
+ </Button>
325
+ ) : null}
326
+ {formattedJson !== null || responseIsHtml ? (
327
+ <div className="ml-auto flex shrink-0 items-center gap-0.5 rounded-md border border-border bg-muted p-0.5">
328
+ <button
329
+ type="button"
330
+ data-testid="fuzzer-view-raw"
331
+ onClick={() => setBodyView('raw')}
332
+ className={cn(
333
+ 'rounded px-1.5 py-0.5 text-[10px] transition-colors',
334
+ bodyView === 'raw'
335
+ ? 'bg-background font-medium text-foreground shadow-sm'
336
+ : 'text-muted-foreground hover:text-foreground'
337
+ )}
338
+ >
339
+ {t('retest.repeater.viewRaw')}
340
+ </button>
341
+ {formattedJson !== null ? (
342
+ <button
343
+ type="button"
344
+ data-testid="fuzzer-view-json"
345
+ onClick={() => setBodyView('pretty')}
346
+ className={cn(
347
+ 'rounded px-1.5 py-0.5 text-[10px] transition-colors',
348
+ bodyView === 'pretty'
349
+ ? 'bg-background font-medium text-foreground shadow-sm'
350
+ : 'text-muted-foreground hover:text-foreground'
351
+ )}
352
+ >
353
+ {t('retest.repeater.formatJson')}
354
+ </button>
355
+ ) : null}
356
+ {responseIsHtml ? (
357
+ <button
358
+ type="button"
359
+ data-testid="fuzzer-view-html"
360
+ onClick={() => setBodyView('html')}
361
+ className={cn(
362
+ 'rounded px-1.5 py-0.5 text-[10px] transition-colors',
363
+ bodyView === 'html'
364
+ ? 'bg-background font-medium text-foreground shadow-sm'
365
+ : 'text-muted-foreground hover:text-foreground'
366
+ )}
367
+ >
368
+ {t('retest.repeater.viewHtml')}
369
+ </button>
370
+ ) : null}
371
+ </div>
372
+ ) : null}
373
+ </>
374
+ ) : null}
375
+ </div>
376
+
377
+ {result === null ? (
378
+ <p className="flex flex-1 items-center justify-center px-3 text-[11.5px] text-muted-foreground">
379
+ {t('retest.repeater.emptyResponse')}
380
+ </p>
381
+ ) : (
382
+ <div className="flex min-h-0 flex-1 flex-col p-3">
383
+ {responseIsHtml && bodyView === 'html' ? (
384
+ <iframe
385
+ title={t('retest.repeater.htmlPreview')}
386
+ sandbox=""
387
+ referrerPolicy="no-referrer"
388
+ srcDoc={htmlPreview}
389
+ className="min-h-0 w-full flex-1 rounded-md border border-border bg-white"
390
+ />
391
+ ) : formattedJson !== null && bodyView === 'pretty' ? (
392
+ <pre
393
+ data-testid="fuzzer-response-json"
394
+ className="min-h-0 flex-1 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-white px-3 py-2.5 font-mono text-[11.5px] leading-relaxed text-foreground"
395
+ >
396
+ {jsonResponse}
397
+ </pre>
398
+ ) : (
399
+ <pre
400
+ data-testid="fuzzer-response-body"
401
+ className="min-h-0 flex-1 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-white px-3 py-2.5 font-mono text-[11.5px] leading-relaxed text-foreground"
402
+ >
403
+ {rawResponse.slice(0, 200_000)}
404
+ </pre>
405
+ )}
406
+ </div>
407
+ )}
408
+ </section>
409
+ </div>
410
+ )
411
+ }
@@ -21,6 +21,7 @@ export const commonMessages: MessageBundle = {
21
21
  'nav.snapshots': '认知快照',
22
22
  'nav.mcp': 'MCP 接入',
23
23
  'nav.retest': '漏洞复测',
24
+ 'nav.fuzzer': 'Web Fuzzer',
24
25
  'sidebar.databaseConnected': '数据库已连接',
25
26
  'sidebar.databaseButton': '数据库',
26
27
  'mcp.connect.button': '接入 Agent',
@@ -109,6 +110,7 @@ export const commonMessages: MessageBundle = {
109
110
  'nav.snapshots': 'Snapshots',
110
111
  'nav.mcp': 'MCP Access',
111
112
  'nav.retest': 'Vulnerability Retest',
113
+ 'nav.fuzzer': 'Web Fuzzer',
112
114
  'sidebar.databaseConnected': 'Database connected',
113
115
  'sidebar.databaseButton': 'Database',
114
116
  'mcp.connect.button': 'Connect agents',
@@ -0,0 +1,32 @@
1
+ import type { MessageBundle } from './types'
2
+
3
+ /**
4
+ * Web Fuzzer page chrome (manual Yakit-style sender).
5
+ *
6
+ * The request/response controls reuse the existing `retest.repeater.*` keys, so
7
+ * only the page-level target selector needs its own bundle.
8
+ */
9
+ export const fuzzerMessages: MessageBundle = {
10
+ zh: {
11
+ 'fuzzer.page.description':
12
+ 'Yakit 风格的手动发包页面:左侧编辑请求、右侧查看响应;请求仍受目标漏洞 origin 白名单约束。',
13
+ 'fuzzer.target.title': '目标漏洞',
14
+ 'fuzzer.target.select': '选择目标漏洞',
15
+ 'fuzzer.target.empty': '没有可用的 Web 漏洞',
16
+ 'fuzzer.target.loadFailed': '无法加载 Web 漏洞:{error}',
17
+ 'fuzzer.target.detailFailed': '无法加载漏洞详情',
18
+ 'fuzzer.request.paneAria': '请求编辑与发送',
19
+ 'fuzzer.response.aria': '响应查看面板'
20
+ },
21
+ en: {
22
+ 'fuzzer.page.description':
23
+ 'Yakit-style manual HTTP sender: edit the request on the left and inspect the response on the right. Every request is still constrained by the target finding origin allowlist.',
24
+ 'fuzzer.target.title': 'Target finding',
25
+ 'fuzzer.target.select': 'Select target finding',
26
+ 'fuzzer.target.empty': 'No Web findings available',
27
+ 'fuzzer.target.loadFailed': 'Cannot load Web findings: {error}',
28
+ 'fuzzer.target.detailFailed': 'Cannot load the finding details',
29
+ 'fuzzer.request.paneAria': 'Request editor and send controls',
30
+ 'fuzzer.response.aria': 'Response viewer'
31
+ }
32
+ }
@@ -2,8 +2,10 @@ import type { MessageBundle } from './types'
2
2
  import { assetMessages } from './asset'
3
3
  import { commonMessages } from './common'
4
4
  import { dashboardMessages } from './dashboard'
5
+ import { fuzzerMessages } from './fuzzer'
5
6
  import { internalMessages } from './internal'
6
7
  import { mcpMessages } from './mcp'
8
+ import { miniappMessages } from './miniapp'
7
9
  import { nebulaMessages } from './nebula'
8
10
  import { retestMessages } from './retest'
9
11
  import { snapshotMessages } from './snapshot'
@@ -22,7 +24,9 @@ const bundles: MessageBundle[] = [
22
24
  snapshotMessages,
23
25
  nebulaMessages,
24
26
  mcpMessages,
25
- retestMessages
27
+ retestMessages,
28
+ miniappMessages,
29
+ fuzzerMessages
26
30
  ]
27
31
 
28
32
  function mergeBundles(lang: 'zh' | 'en'): Record<string, string> {