@lievclouds/dsh-cortex 0.4.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.
- package/README.md +1 -1
- package/client.js +428 -406
- package/index.js +7 -0
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +115 -106
- package/src/cortex-ui/dsh/api.ts +5 -2
- package/src/cortex-ui/dsh/css-overrides.css +8 -0
- package/src/cortex-ui/renderer/App.tsx +39 -0
- package/src/cortex-ui/renderer/components/asset/AssetDetail.tsx +9 -1
- package/src/cortex-ui/renderer/components/miniapp/FileTree.tsx +161 -0
- package/src/cortex-ui/renderer/components/miniapp/MiniappCard.tsx +263 -0
- package/src/cortex-ui/renderer/components/miniapp/MiniappDetail.tsx +1493 -0
- package/src/cortex-ui/renderer/components/retest/CurlImportDialog.tsx +158 -0
- package/src/cortex-ui/renderer/components/retest/RepeaterCollectionsDialog.tsx +327 -0
- package/src/cortex-ui/renderer/components/retest/RepeaterHistoryDialog.tsx +242 -0
- package/src/cortex-ui/renderer/components/retest/RepeaterPanel.tsx +333 -148
- package/src/cortex-ui/renderer/components/retest/YakitSendPanel.tsx +411 -0
- package/src/cortex-ui/renderer/components/vuln/DirectoryPickerDialog.tsx +187 -0
- package/src/cortex-ui/renderer/components/vuln/ExportReportDialog.tsx +173 -59
- package/src/cortex-ui/renderer/components/vuln/VulnDetail.tsx +16 -4
- package/src/cortex-ui/renderer/i18n/messages/common.ts +2 -0
- package/src/cortex-ui/renderer/i18n/messages/fuzzer.ts +32 -0
- package/src/cortex-ui/renderer/i18n/messages/index.ts +5 -1
- package/src/cortex-ui/renderer/i18n/messages/miniapp.ts +392 -0
- package/src/cortex-ui/renderer/i18n/messages/retest.ts +146 -2
- package/src/cortex-ui/renderer/i18n/messages/vuln.ts +26 -6
- package/src/cortex-ui/renderer/lib/curl.ts +635 -0
- package/src/cortex-ui/renderer/lib/miniappApi.ts +459 -0
- package/src/cortex-ui/renderer/lib/navigation.ts +13 -1
- package/src/cortex-ui/renderer/lib/repeaterRaw.ts +102 -0
- package/src/cortex-ui/renderer/lib/repeaterStoreApi.ts +163 -0
- package/src/cortex-ui/renderer/lib/retestResponse.ts +64 -0
- package/src/cortex-ui/renderer/lib/transport/httpClient.ts +1 -1
- package/src/cortex-ui/renderer/pages/DashboardPage.tsx +3 -3
- package/src/cortex-ui/renderer/pages/FuzzerPage.tsx +212 -0
- package/src/cortex-ui/renderer/pages/MiniappPage.tsx +664 -0
- package/src/cortex-ui/shared/ipc.ts +3 -4
- package/src/cortex-ui/shared/types.ts +9 -0
- package/src/host/cortex/fs.js +72 -0
- package/src/host/cortex/index.js +49 -1
- package/src/host/cortex/repeater-store.js +411 -0
- package/src/host/cortex/repeater.js +15 -2
- package/src/host/cortex/settings.js +54 -0
- package/src/host/cortex/tools.js +2 -0
- package/src/host/cortex/vulns.js +36 -13
- package/src/host/miniapp/analyze.js +1078 -0
- package/src/host/miniapp/assets-import.js +294 -0
- package/src/host/miniapp/detect.js +426 -0
- package/src/host/miniapp/format.js +263 -0
- package/src/host/miniapp/roots.js +360 -0
- package/src/host/miniapp/routes.js +304 -0
- package/src/host/miniapp/service.js +559 -0
- package/src/host/miniapp/store.js +756 -0
- package/src/host/miniapp/tools.js +335 -0
- package/src/host/miniapp/unpack.js +1010 -0
- package/src/host/miniapp/wxapkg.js +416 -0
- package/src/host/reports-office.js +341 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared mini-program service layer.
|
|
3
|
+
*
|
|
4
|
+
* Both the HTTP routes and the `cortex_*_miniapp*` agent tools call these
|
|
5
|
+
* functions, so status reporting, locking, workspace paths and asset import
|
|
6
|
+
* behave identically on both transports. The service is transport-agnostic:
|
|
7
|
+
* it returns plain JSON and throws the host `ApiError` type for 4xx cases.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHash } from 'node:crypto'
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
12
|
+
import { spawn } from 'node:child_process'
|
|
13
|
+
import { resolve, sep } from 'node:path'
|
|
14
|
+
import { badRequest, notFound } from '../http.js'
|
|
15
|
+
import { importMiniappAssets } from './assets-import.js'
|
|
16
|
+
import { analyzeUnpacked, buildOverviewText, deriveAppNameFromUnpacked, extractAppNameFromPackage, loadOverview, saveOverview } from './analyze.js'
|
|
17
|
+
import { scanMiniapps } from './detect.js'
|
|
18
|
+
import { miniAppWorkspaceRoot } from './roots.js'
|
|
19
|
+
import { getMiniappStore } from './store.js'
|
|
20
|
+
import { listTree, readManifest, readTextFile, searchFiles, unpackSource } from './unpack.js'
|
|
21
|
+
|
|
22
|
+
/** Running unpack operations, keyed by source id (prevents double work). */
|
|
23
|
+
const unpackLocks = new Set()
|
|
24
|
+
|
|
25
|
+
export function isUnpacking(sourceId) {
|
|
26
|
+
return unpackLocks.has(String(sourceId))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Deterministic newest-first version order (numeric last segment wins). */
|
|
30
|
+
export function compareVersionsDesc(left, right) {
|
|
31
|
+
const a = String(left || '')
|
|
32
|
+
const b = String(right || '')
|
|
33
|
+
const aNum = /^\d+$/.test(a) ? Number(a) : null
|
|
34
|
+
const bNum = /^\d+$/.test(b) ? Number(b) : null
|
|
35
|
+
if (aNum !== null && bNum !== null && aNum !== bNum) return bNum - aNum
|
|
36
|
+
if (aNum !== null && bNum === null) return -1
|
|
37
|
+
if (aNum === null && bNum !== null) return 1
|
|
38
|
+
return b.localeCompare(a)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sortSourcesNewest(rows) {
|
|
42
|
+
return [...(Array.isArray(rows) ? rows : [])].sort((left, right) => {
|
|
43
|
+
const byVersion = compareVersionsDesc(left.version, right.version)
|
|
44
|
+
if (byVersion !== 0) return byVersion
|
|
45
|
+
return String(right.updated_at || '').localeCompare(String(left.updated_at || ''))
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Strip heavy internals from a source row for list/detail responses. */
|
|
50
|
+
export function toMiniappSummary(row, { versionCount = 1, isLatest = true, hasAnalysis = false, sourceCount = 1 } = {}) {
|
|
51
|
+
if (!row) return null
|
|
52
|
+
return {
|
|
53
|
+
id: row.id,
|
|
54
|
+
appid: row.appid,
|
|
55
|
+
app_name: row.app_name || '',
|
|
56
|
+
kind: sourceKind(row),
|
|
57
|
+
version: row.version || '',
|
|
58
|
+
root_kind: row.root_kind,
|
|
59
|
+
package_dir: row.package_dir,
|
|
60
|
+
size_bytes: Number(row.size_bytes) || 0,
|
|
61
|
+
file_count: Number(row.file_count) || 0,
|
|
62
|
+
package_hash: row.package_hash || '',
|
|
63
|
+
format: row.format || 'unknown',
|
|
64
|
+
status: row.status || 'detected',
|
|
65
|
+
unpacked_path: row.unpacked_path || '',
|
|
66
|
+
error: row.error || '',
|
|
67
|
+
detected_at: row.detected_at,
|
|
68
|
+
updated_at: row.updated_at,
|
|
69
|
+
version_count: versionCount,
|
|
70
|
+
is_latest: isLatest,
|
|
71
|
+
has_analysis: hasAnalysis,
|
|
72
|
+
source_count: sourceCount,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Classify a source by its package files (`app` | `plugin` | `library` | `unknown`). */
|
|
77
|
+
export function sourceKind(row) {
|
|
78
|
+
if (!row) return 'unknown'
|
|
79
|
+
if (String(row.appid || '').toLowerCase() === 'publiclib') return 'library'
|
|
80
|
+
const names = (Array.isArray(row.files) ? row.files : []).map((file) =>
|
|
81
|
+
String((file && (file.name || file.path)) || '')
|
|
82
|
+
.replace(/\\/g, '/')
|
|
83
|
+
.toLowerCase(),
|
|
84
|
+
)
|
|
85
|
+
const baseName = (name) => name.split('/').pop() || ''
|
|
86
|
+
if (names.some((name) => baseName(name) === '__app__.wxapkg')) return 'app'
|
|
87
|
+
if (names.some((name) => baseName(name) === '__plugincode__.wxapkg')) return 'plugin'
|
|
88
|
+
if (names.some((name) => name.includes('publiclib'))) return 'library'
|
|
89
|
+
return 'unknown'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Higher is better when one appid+version exists in several WeChat user caches. */
|
|
93
|
+
function sourceQuality(row) {
|
|
94
|
+
const statusRank =
|
|
95
|
+
row.status === 'unpacked' ? 4 : row.status === 'detected' ? 3 : row.status === 'decrypting' ? 2 : 1
|
|
96
|
+
return [statusRank, Number(row.size_bytes) || 0, Number(row.file_count) || 0, String(row.updated_at || '')]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function betterSource(left, right) {
|
|
100
|
+
const a = sourceQuality(left)
|
|
101
|
+
const b = sourceQuality(right)
|
|
102
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
103
|
+
if (a[index] === b[index]) continue
|
|
104
|
+
if (typeof a[index] === 'string') return a[index] > b[index]
|
|
105
|
+
return a[index] > b[index]
|
|
106
|
+
}
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function pickBestSource(rows) {
|
|
111
|
+
let best = null
|
|
112
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
113
|
+
if (!best || betterSource(row, best)) best = row
|
|
114
|
+
}
|
|
115
|
+
return best
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Collapse the same appid+version cached in multiple WeChat user directories.
|
|
120
|
+
* The representative prefers an already-unpacked source, then the largest
|
|
121
|
+
* (most complete) cache, then the newest row.
|
|
122
|
+
*/
|
|
123
|
+
export function dedupeSources(rows) {
|
|
124
|
+
const groups = new Map()
|
|
125
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
126
|
+
const key = `${row.appid}\u0000${String(row.version || '')}`
|
|
127
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
128
|
+
groups.get(key).push(row)
|
|
129
|
+
}
|
|
130
|
+
const result = []
|
|
131
|
+
for (const group of groups.values()) {
|
|
132
|
+
const chosen = pickBestSource(group)
|
|
133
|
+
if (!chosen) continue
|
|
134
|
+
result.push({ ...chosen, source_count: group.length })
|
|
135
|
+
}
|
|
136
|
+
result.sort((left, right) => {
|
|
137
|
+
const byVersion = compareVersionsDesc(left.version, right.version)
|
|
138
|
+
if (byVersion !== 0) return byVersion
|
|
139
|
+
return String(left.appid).localeCompare(String(right.appid))
|
|
140
|
+
})
|
|
141
|
+
return result
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Flat list with per-app version counts + latest flags, de-duplicated by
|
|
146
|
+
* appid+version across WeChat user caches.
|
|
147
|
+
* @param {import('./store.js').MiniappStore} store
|
|
148
|
+
* @param {{search?:string, status?:string}} [filters]
|
|
149
|
+
*/
|
|
150
|
+
export function listMiniapps(store, filters = {}) {
|
|
151
|
+
const rawRows = store.listSources({
|
|
152
|
+
search: filters.search || '',
|
|
153
|
+
status: filters.status || '',
|
|
154
|
+
})
|
|
155
|
+
const rows = dedupeSources(rawRows)
|
|
156
|
+
const versionsByApp = new Map()
|
|
157
|
+
for (const row of rows) {
|
|
158
|
+
if (!versionsByApp.has(row.appid)) versionsByApp.set(row.appid, new Set())
|
|
159
|
+
versionsByApp.get(row.appid).add(String(row.version || ''))
|
|
160
|
+
}
|
|
161
|
+
// Filter support is still applied against the deduplicated representative.
|
|
162
|
+
const visible = filters.status ? rows.filter((row) => row.status === filters.status) : rows
|
|
163
|
+
const latestVersion = new Map()
|
|
164
|
+
for (const appid of new Set(visible.map((row) => row.appid))) {
|
|
165
|
+
const newest = sortSourcesNewest(visible.filter((row) => row.appid === appid))[0]
|
|
166
|
+
if (newest) latestVersion.set(appid, String(newest.version || ''))
|
|
167
|
+
}
|
|
168
|
+
const miniapps = visible.map((row) => {
|
|
169
|
+
let hasAnalysis = false
|
|
170
|
+
try {
|
|
171
|
+
hasAnalysis = Boolean(store.getAnalysis(row.id))
|
|
172
|
+
} catch {
|
|
173
|
+
hasAnalysis = false
|
|
174
|
+
}
|
|
175
|
+
return toMiniappSummary(row, {
|
|
176
|
+
versionCount: versionsByApp.get(row.appid)?.size || 1,
|
|
177
|
+
isLatest: latestVersion.get(row.appid) === String(row.version || ''),
|
|
178
|
+
hasAnalysis,
|
|
179
|
+
sourceCount: row.source_count || 1,
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
const appCount = new Set(miniapps.map((row) => row.appid)).size
|
|
183
|
+
return {
|
|
184
|
+
miniapps,
|
|
185
|
+
total: miniapps.length,
|
|
186
|
+
appCount,
|
|
187
|
+
customRoots: store.customRoots(),
|
|
188
|
+
workspaceRoot: miniAppWorkspaceRoot(),
|
|
189
|
+
lastScanAt: store.getSetting('lastScanAt'),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Resolve a source by appid (+ optional version). `version` omitted -> newest. */
|
|
194
|
+
export function resolveSource(store, appid, version = '') {
|
|
195
|
+
const targetApp = String(appid || '').trim()
|
|
196
|
+
if (targetApp.length === 0) throw badRequest('appid is required', { field: 'appid' })
|
|
197
|
+
const rows = store.findSources({ appid: targetApp, version: String(version || '').trim() })
|
|
198
|
+
if (!rows || rows.length === 0) return null
|
|
199
|
+
if (version) return pickBestSource(rows)
|
|
200
|
+
const newest = sortSourcesNewest(rows)[0]
|
|
201
|
+
if (!newest) return null
|
|
202
|
+
return pickBestSource(rows.filter((row) => String(row.version || '') === String(newest.version || '')))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function requireSource(store, appid, version = '') {
|
|
206
|
+
const source = resolveSource(store, appid, version)
|
|
207
|
+
if (!source) {
|
|
208
|
+
const label = version ? `${appid}@${version}` : String(appid)
|
|
209
|
+
throw notFound(`miniapp source not found: ${label}`)
|
|
210
|
+
}
|
|
211
|
+
return source
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function requireSourceById(store, id) {
|
|
215
|
+
const source = store.getSource(String(id || ''))
|
|
216
|
+
if (!source) throw notFound('miniapp source not found')
|
|
217
|
+
return source
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Skip huge shared libraries during name enrichment (keeps scan fast). */
|
|
221
|
+
const APP_NAME_ENRICH_MAX_BYTES = 80 * 1024 * 1024
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Fill `app_name` for newly detected sources by reading one package per app.
|
|
225
|
+
* Best-effort: decryption failures (e.g. publicLib) simply leave the appid.
|
|
226
|
+
*/
|
|
227
|
+
async function enrichAppNames(store, sources) {
|
|
228
|
+
for (const row of Array.isArray(sources) ? sources : []) {
|
|
229
|
+
if (row.app_name) continue
|
|
230
|
+
const files = Array.isArray(row.files) ? row.files : []
|
|
231
|
+
if (files.length === 0) continue
|
|
232
|
+
const preferred =
|
|
233
|
+
files.find((file) => /(?:^|[\\/])__APP__\.wxapkg$/i.test(String(file.name || file.path))) ||
|
|
234
|
+
[...files].sort((left, right) => Number(right.size) - Number(left.size))[0]
|
|
235
|
+
if (!preferred || !preferred.path || Number(preferred.size) > APP_NAME_ENRICH_MAX_BYTES) continue
|
|
236
|
+
try {
|
|
237
|
+
const { appName } = extractAppNameFromPackage(preferred.path, row.appid)
|
|
238
|
+
if (appName) {
|
|
239
|
+
store.updateSource(row.id, { app_name: appName })
|
|
240
|
+
row.app_name = appName
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
/* name extraction is best effort and must never fail the scan */
|
|
244
|
+
}
|
|
245
|
+
await new Promise((resolveTick) => setImmediate(resolveTick))
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Scan custom roots (persisting them) and return the summary + current rows. */
|
|
250
|
+
export async function scan(store, { customRoots } = {}) {
|
|
251
|
+
if (Array.isArray(customRoots)) store.setCustomRoots(customRoots)
|
|
252
|
+
const roots = store.customRoots()
|
|
253
|
+
const summary = await scanMiniapps({ store, customRoots: roots })
|
|
254
|
+
await enrichAppNames(store, summary.sources)
|
|
255
|
+
store.setSetting('lastScanAt', summary.scannedAt)
|
|
256
|
+
const listing = listMiniapps(store)
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
scannedAt: summary.scannedAt,
|
|
260
|
+
totalSources: summary.totalSources,
|
|
261
|
+
totalPackages: summary.totalPackages,
|
|
262
|
+
added: summary.added,
|
|
263
|
+
updated: summary.updated,
|
|
264
|
+
unchanged: summary.unchanged,
|
|
265
|
+
errors: summary.errors,
|
|
266
|
+
roots: summary.roots,
|
|
267
|
+
miniapps: listing.miniapps,
|
|
268
|
+
appCount: listing.appCount,
|
|
269
|
+
customRoots: listing.customRoots,
|
|
270
|
+
workspaceRoot: listing.workspaceRoot,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function packageHashOf(source) {
|
|
275
|
+
try {
|
|
276
|
+
const hash = createHash('sha1')
|
|
277
|
+
for (const file of source.files || []) {
|
|
278
|
+
hash.update(readFileSync(file.path))
|
|
279
|
+
}
|
|
280
|
+
return hash.digest('hex')
|
|
281
|
+
} catch {
|
|
282
|
+
return ''
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Unpack one source (idempotent unless `force`). Always reports the true
|
|
288
|
+
* status; a decryption failure degrades to `unsupported` and never throws.
|
|
289
|
+
*/
|
|
290
|
+
export async function unpackSourceRecord({ store, source, force = false } = {}) {
|
|
291
|
+
if (!source) throw badRequest('source is required')
|
|
292
|
+
if (isUnpacking(source.id)) {
|
|
293
|
+
throw badRequest('this mini-program is already being unpacked', { code: 'UNPACK_IN_PROGRESS' })
|
|
294
|
+
}
|
|
295
|
+
const manifestPath = source.unpacked_path ? resolve(source.unpacked_path, 'manifest.json') : ''
|
|
296
|
+
if (
|
|
297
|
+
force !== true &&
|
|
298
|
+
source.status === 'unpacked' &&
|
|
299
|
+
source.unpacked_path &&
|
|
300
|
+
existsSync(source.unpacked_path) &&
|
|
301
|
+
(!manifestPath || existsSync(manifestPath))
|
|
302
|
+
) {
|
|
303
|
+
return {
|
|
304
|
+
ok: true,
|
|
305
|
+
status: 'unpacked',
|
|
306
|
+
unpackedPath: source.unpacked_path,
|
|
307
|
+
manifestPath,
|
|
308
|
+
manifest: readManifest(source.unpacked_path),
|
|
309
|
+
error: '',
|
|
310
|
+
totals: null,
|
|
311
|
+
cached: true,
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
unpackLocks.add(source.id)
|
|
316
|
+
let job = null
|
|
317
|
+
try {
|
|
318
|
+
job = store.createJob({ source_id: source.id, kind: 'unpack', status: 'running', message: 'running' })
|
|
319
|
+
} catch {
|
|
320
|
+
job = null
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
store.updateSource(source.id, { status: 'decrypting', error: '' })
|
|
324
|
+
const result = await unpackSource({ source, workspaceRoot: miniAppWorkspaceRoot() })
|
|
325
|
+
const packageHash = result.ok ? packageHashOf(source) : ''
|
|
326
|
+
store.updateSource(source.id, {
|
|
327
|
+
status: result.status,
|
|
328
|
+
unpacked_path: result.unpackedPath || '',
|
|
329
|
+
error: result.error || '',
|
|
330
|
+
...(packageHash ? { package_hash: packageHash } : {}),
|
|
331
|
+
})
|
|
332
|
+
if (result.ok) {
|
|
333
|
+
try {
|
|
334
|
+
const { appName } = deriveAppNameFromUnpacked(result.unpackedPath)
|
|
335
|
+
if (appName) store.updateSource(source.id, { app_name: appName })
|
|
336
|
+
} catch {
|
|
337
|
+
/* display-name extraction is best effort */
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (job) {
|
|
341
|
+
try {
|
|
342
|
+
store.updateJob(job.id, {
|
|
343
|
+
status: result.ok ? 'done' : 'failed',
|
|
344
|
+
progress: 100,
|
|
345
|
+
message: String(result.error || result.status || '').slice(0, 500),
|
|
346
|
+
})
|
|
347
|
+
} catch {
|
|
348
|
+
/* job bookkeeping is best effort */
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return result
|
|
352
|
+
} catch (error) {
|
|
353
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
354
|
+
store.updateSource(source.id, { status: 'failed', error: message.slice(0, 1000) })
|
|
355
|
+
if (job) {
|
|
356
|
+
try {
|
|
357
|
+
store.updateJob(job.id, { status: 'failed', progress: 100, message: message.slice(0, 500) })
|
|
358
|
+
} catch {
|
|
359
|
+
/* ignore */
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
throw error
|
|
363
|
+
} finally {
|
|
364
|
+
unpackLocks.delete(source.id)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function extractedCounts(overview) {
|
|
369
|
+
return {
|
|
370
|
+
domains: Array.isArray(overview && overview.domains) ? overview.domains.length : 0,
|
|
371
|
+
endpoints: Array.isArray(overview && overview.endpoints) ? overview.endpoints.length : 0,
|
|
372
|
+
secrets: Array.isArray(overview && overview.secrets) ? overview.secrets.length : 0,
|
|
373
|
+
parameters: Array.isArray(overview && overview.parameters) ? overview.parameters.length : 0,
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function requireUnpacked(source) {
|
|
378
|
+
if (!source || source.status !== 'unpacked' || !source.unpacked_path) {
|
|
379
|
+
throw badRequest('mini-program is not unpacked yet; call unpack first', {
|
|
380
|
+
status: source ? source.status : 'missing',
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
if (!existsSync(source.unpacked_path)) {
|
|
384
|
+
throw badRequest('unpacked workspace is missing; unpack again', { status: source.status })
|
|
385
|
+
}
|
|
386
|
+
return source.unpacked_path
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Analyze (or reuse) one unpacked source and persist the overview. */
|
|
390
|
+
export function analyzeSourceRecord({ store, source, force = false } = {}) {
|
|
391
|
+
const unpackedPath = requireUnpacked(source)
|
|
392
|
+
if (force !== true) {
|
|
393
|
+
const cached = store.getAnalysis(source.id)
|
|
394
|
+
if (cached && typeof cached === 'object' && Object.keys(cached).length > 0) {
|
|
395
|
+
if (cached.appName && cached.appName !== source.app_name) {
|
|
396
|
+
try {
|
|
397
|
+
store.updateSource(source.id, { app_name: cached.appName })
|
|
398
|
+
} catch {
|
|
399
|
+
/* best effort */
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
ok: true,
|
|
404
|
+
analysis: cached,
|
|
405
|
+
overviewText: buildOverviewText(cached),
|
|
406
|
+
counts: extractedCounts(cached),
|
|
407
|
+
cached: true,
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const fromDisk = loadOverview(unpackedPath)
|
|
411
|
+
if (fromDisk) {
|
|
412
|
+
store.saveAnalysis(source.id, fromDisk)
|
|
413
|
+
return {
|
|
414
|
+
ok: true,
|
|
415
|
+
analysis: fromDisk,
|
|
416
|
+
overviewText: buildOverviewText(fromDisk),
|
|
417
|
+
counts: extractedCounts(fromDisk),
|
|
418
|
+
cached: true,
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const manifest = readManifest(unpackedPath)
|
|
423
|
+
const overview = analyzeUnpacked({
|
|
424
|
+
unpackedPath,
|
|
425
|
+
appid: source.appid,
|
|
426
|
+
version: source.version,
|
|
427
|
+
manifest,
|
|
428
|
+
})
|
|
429
|
+
saveOverview(unpackedPath, overview)
|
|
430
|
+
store.saveAnalysis(source.id, overview)
|
|
431
|
+
if (overview.appName) {
|
|
432
|
+
try {
|
|
433
|
+
store.updateSource(source.id, { app_name: overview.appName })
|
|
434
|
+
} catch {
|
|
435
|
+
/* display-name persistence is best effort */
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
ok: true,
|
|
440
|
+
analysis: overview,
|
|
441
|
+
overviewText: buildOverviewText(overview),
|
|
442
|
+
counts: extractedCounts(overview),
|
|
443
|
+
cached: false,
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Ensure an analysis exists (used by the overview/import tools and routes). */
|
|
448
|
+
export function ensureAnalyzed({ store, source, force = false } = {}) {
|
|
449
|
+
const analysis = store.getAnalysis(source.id)
|
|
450
|
+
if (force !== true && analysis && Object.keys(analysis).length > 0) {
|
|
451
|
+
return { ok: true, analysis, overviewText: buildOverviewText(analysis), counts: extractedCounts(analysis), cached: true }
|
|
452
|
+
}
|
|
453
|
+
return analyzeSourceRecord({ store, source, force })
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Detail payload for `GET /miniapps/:id`. */
|
|
457
|
+
export function detailPayload(store, source) {
|
|
458
|
+
const allSources = store.findSources({ appid: source.appid })
|
|
459
|
+
const versionRows = dedupeSources(allSources)
|
|
460
|
+
const versions = versionRows.map((row) => ({
|
|
461
|
+
id: row.id,
|
|
462
|
+
version: row.version,
|
|
463
|
+
status: row.status,
|
|
464
|
+
format: row.format,
|
|
465
|
+
size_bytes: row.size_bytes,
|
|
466
|
+
file_count: row.file_count,
|
|
467
|
+
updated_at: row.updated_at,
|
|
468
|
+
unpacked_path: row.unpacked_path,
|
|
469
|
+
source_count: row.source_count || 1,
|
|
470
|
+
}))
|
|
471
|
+
const sourceCount =
|
|
472
|
+
allSources.filter((row) => String(row.version || '') === String(source.version || '')).length || 1
|
|
473
|
+
const manifest = source.unpacked_path ? readManifest(source.unpacked_path) : null
|
|
474
|
+
let analysis = null
|
|
475
|
+
try {
|
|
476
|
+
analysis = store.getAnalysis(source.id)
|
|
477
|
+
} catch {
|
|
478
|
+
analysis = null
|
|
479
|
+
}
|
|
480
|
+
if (!analysis && source.unpacked_path) analysis = loadOverview(source.unpacked_path)
|
|
481
|
+
const counts = extractedCounts(analysis)
|
|
482
|
+
return {
|
|
483
|
+
miniapp: {
|
|
484
|
+
...toMiniappSummary(source, { sourceCount }),
|
|
485
|
+
app_name: source.app_name || (analysis && analysis.appName) || '',
|
|
486
|
+
versions,
|
|
487
|
+
},
|
|
488
|
+
manifest,
|
|
489
|
+
analysis,
|
|
490
|
+
overviewText: analysis ? buildOverviewText(analysis) : '',
|
|
491
|
+
extractedCounts: counts,
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export async function importAssetsForSource({ holder, store, source, projectName, hostPort } = {}) {
|
|
496
|
+
const unpackedPath = requireUnpacked(source)
|
|
497
|
+
if (!unpackedPath) throw badRequest('mini-program is not unpacked yet')
|
|
498
|
+
const analyzed = ensureAnalyzed({ store, source })
|
|
499
|
+
return importMiniappAssets({
|
|
500
|
+
holder,
|
|
501
|
+
source,
|
|
502
|
+
overview: analyzed.analysis,
|
|
503
|
+
projectName,
|
|
504
|
+
hostPort,
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/* --------------------------- workspace browsing --------------------------- */
|
|
509
|
+
|
|
510
|
+
export function treeForSource(source, relativePath = '') {
|
|
511
|
+
const root = requireUnpacked(source)
|
|
512
|
+
return listTree({ root, path: relativePath })
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function fileForSource(
|
|
516
|
+
source,
|
|
517
|
+
relativePath,
|
|
518
|
+
{ offset = 0, limit = 200, format = 'auto', maxBytes, find = '', findRegex = false, findMax } = {},
|
|
519
|
+
) {
|
|
520
|
+
const root = requireUnpacked(source)
|
|
521
|
+
return readTextFile({
|
|
522
|
+
root,
|
|
523
|
+
path: relativePath,
|
|
524
|
+
offset,
|
|
525
|
+
limit,
|
|
526
|
+
format,
|
|
527
|
+
find,
|
|
528
|
+
findRegex,
|
|
529
|
+
...(maxBytes ? { maxBytes } : {}),
|
|
530
|
+
...(findMax ? { findMax } : {}),
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function searchSource(source, options = {}) {
|
|
535
|
+
const root = requireUnpacked(source)
|
|
536
|
+
return searchFiles({ root, ...options })
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Open the unpacked workspace in the OS file manager (never the cache dir). */
|
|
540
|
+
export function openSourceDirectory(source) {
|
|
541
|
+
const workspaceRoot = resolve(miniAppWorkspaceRoot())
|
|
542
|
+
const target = resolve(requireUnpacked(source))
|
|
543
|
+
if (target !== workspaceRoot && !target.startsWith(`${workspaceRoot}${sep}`)) {
|
|
544
|
+
throw badRequest('refusing to open a directory outside the mini-program workspace')
|
|
545
|
+
}
|
|
546
|
+
const command = process.platform === 'win32' ? 'explorer.exe' : process.platform === 'darwin' ? 'open' : 'xdg-open'
|
|
547
|
+
try {
|
|
548
|
+
const child = spawn(command, [target], { detached: true, stdio: 'ignore' })
|
|
549
|
+
child.unref()
|
|
550
|
+
} catch {
|
|
551
|
+
throw badRequest('failed to open the directory')
|
|
552
|
+
}
|
|
553
|
+
return { ok: true, path: target }
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Singleton store bound to the configured plugin DB (test-friendly). */
|
|
557
|
+
export function miniappStore() {
|
|
558
|
+
return getMiniappStore()
|
|
559
|
+
}
|