@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.
- package/README.md +1 -1
- package/client.js +434 -412
- package/index.js +7 -0
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +115 -110
- 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/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 +291 -137
- package/src/cortex-ui/renderer/components/retest/YakitSendPanel.tsx +411 -0
- 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 +144 -2
- 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 -17
- package/src/cortex-ui/renderer/pages/FuzzerPage.tsx +212 -0
- package/src/cortex-ui/renderer/pages/MiniappPage.tsx +664 -0
- package/src/host/cortex/index.js +42 -1
- package/src/host/cortex/repeater-store.js +411 -0
- package/src/host/cortex/repeater.js +15 -2
- package/src/host/cortex/tools.js +2 -0
- 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
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mini-program (wxapkg) root discovery and identity inference.
|
|
3
|
+
*
|
|
4
|
+
* All functions are pure path math over the environment (plus `existsSync`
|
|
5
|
+
* checks in `expandRoots`) so they are trivially testable with fake envs and
|
|
6
|
+
* temp trees. Nothing here writes to disk or touches WeChat's cache.
|
|
7
|
+
*
|
|
8
|
+
* Layouts handled:
|
|
9
|
+
* WeChat 4.x (xwechat) <appdata>/Tencent/xwechat/radium/users/<hash>/applet/packages/<appid>/<version>/*.wxapkg
|
|
10
|
+
* public library <users>/<hash>/applet/publicLib/<version>/publicLib.wxapkg
|
|
11
|
+
* WeChat 3.x <home>/Documents/WeChat Files/.../packages/<appid>/<version>/*.wxapkg
|
|
12
|
+
* devtools <localappdata>/微信web开发者工具/User Data/...
|
|
13
|
+
* custom/env roots arbitrary depth (`CORTEX_MINIAPP_ROOTS`)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync } from 'node:fs'
|
|
17
|
+
import { homedir } from 'node:os'
|
|
18
|
+
import { basename, delimiter, dirname, join, relative, resolve, sep } from 'node:path'
|
|
19
|
+
|
|
20
|
+
/** @typedef {{kind:'xwechat'|'wechat3'|'devtools'|'custom', path:string, label:string, mode:'packages'|'recursive', maxDepth:number}} Root */
|
|
21
|
+
|
|
22
|
+
/** Default recursive scan depth used by discovery and detection. */
|
|
23
|
+
export const DEFAULT_MAX_DEPTH = 10
|
|
24
|
+
|
|
25
|
+
/** Appid directory segment, e.g. `wxdabf06925a55f410`. */
|
|
26
|
+
const APPID_RE = /^wx[0-9a-f]{16}$/i
|
|
27
|
+
|
|
28
|
+
/** Directory names that are never descended into. */
|
|
29
|
+
const SKIP_DIR_NAMES = new Set(['node_modules'])
|
|
30
|
+
|
|
31
|
+
/** @param {NodeJS.ProcessEnv} env */
|
|
32
|
+
function homeDir(env) {
|
|
33
|
+
if (env && typeof env.HOME === 'string' && env.HOME.trim()) return env.HOME.trim()
|
|
34
|
+
if (env && typeof env.USERPROFILE === 'string' && env.USERPROFILE.trim()) return env.USERPROFILE.trim()
|
|
35
|
+
return homedir()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @param {NodeJS.ProcessEnv} env */
|
|
39
|
+
function appDataDir(env) {
|
|
40
|
+
if (env && typeof env.APPDATA === 'string' && env.APPDATA.trim()) return env.APPDATA.trim()
|
|
41
|
+
return join(homeDir(env), 'AppData', 'Roaming')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {NodeJS.ProcessEnv} env */
|
|
45
|
+
function localAppDataDir(env) {
|
|
46
|
+
if (env && typeof env.LOCALAPPDATA === 'string' && env.LOCALAPPDATA.trim()) return env.LOCALAPPDATA.trim()
|
|
47
|
+
return join(homeDir(env), 'AppData', 'Local')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Candidate roots (existing or not; `expandRoots` applies the existence filter).
|
|
52
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
53
|
+
* @returns {Root[]}
|
|
54
|
+
*/
|
|
55
|
+
export function defaultRoots(env = process.env) {
|
|
56
|
+
const roots = [
|
|
57
|
+
{
|
|
58
|
+
kind: 'xwechat',
|
|
59
|
+
path: resolve(join(appDataDir(env), 'Tencent', 'xwechat', 'radium', 'users')),
|
|
60
|
+
label: 'WeChat 4.x (xwechat)',
|
|
61
|
+
mode: 'recursive',
|
|
62
|
+
maxDepth: DEFAULT_MAX_DEPTH,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
kind: 'wechat3',
|
|
66
|
+
path: resolve(join(homeDir(env), 'Documents', 'WeChat Files')),
|
|
67
|
+
label: 'WeChat Files (3.x)',
|
|
68
|
+
mode: 'recursive',
|
|
69
|
+
maxDepth: DEFAULT_MAX_DEPTH,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
kind: 'devtools',
|
|
73
|
+
path: resolve(join(localAppDataDir(env), '微信web开发者工具', 'User Data')),
|
|
74
|
+
label: 'WeChat DevTools',
|
|
75
|
+
mode: 'recursive',
|
|
76
|
+
maxDepth: DEFAULT_MAX_DEPTH,
|
|
77
|
+
},
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
// WeChat 4.x can keep its "files" directory on another drive (or the user's
|
|
81
|
+
// Documents folder). Its account data may still contain applet caches; scan
|
|
82
|
+
// every discovered `Documents` directory without touching the whole drive.
|
|
83
|
+
const documents = new Set([resolve(join(homeDir(env), 'Documents'))])
|
|
84
|
+
// Extra-drive probing is opt-in (host sets the flag at runtime) so tests and
|
|
85
|
+
// embedders with a synthetic env never touch unrelated drives.
|
|
86
|
+
if (env && env.CORTEX_MINIAPP_EXTRA_DRIVES === '1') {
|
|
87
|
+
for (const letter of 'CDEFGHIJKLMNOPQRSTUVWXYZ') {
|
|
88
|
+
const driveDocuments = `${letter}:\\Documents`
|
|
89
|
+
try {
|
|
90
|
+
if (existsSync(driveDocuments)) documents.add(driveDocuments)
|
|
91
|
+
} catch {
|
|
92
|
+
/* inaccessible drive letter */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const directory of documents) {
|
|
97
|
+
const xwechatFiles = join(directory, 'xwechat_files')
|
|
98
|
+
try {
|
|
99
|
+
if (existsSync(xwechatFiles)) {
|
|
100
|
+
roots.push({
|
|
101
|
+
kind: 'xwechat',
|
|
102
|
+
path: resolve(xwechatFiles),
|
|
103
|
+
label: `WeChat 4.x files (${directory.slice(0, 2)})`,
|
|
104
|
+
mode: 'recursive',
|
|
105
|
+
maxDepth: DEFAULT_MAX_DEPTH,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
/* ignore */
|
|
110
|
+
}
|
|
111
|
+
const classic = join(directory, 'WeChat Files')
|
|
112
|
+
try {
|
|
113
|
+
if (existsSync(classic)) {
|
|
114
|
+
roots.push({
|
|
115
|
+
kind: 'wechat3',
|
|
116
|
+
path: resolve(classic),
|
|
117
|
+
label: `WeChat Files (${directory.slice(0, 2)})`,
|
|
118
|
+
mode: 'recursive',
|
|
119
|
+
maxDepth: DEFAULT_MAX_DEPTH,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
/* ignore */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return roots
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extra roots from `CORTEX_MINIAPP_ROOTS` (split on `path.delimiter`).
|
|
131
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
132
|
+
* @returns {string[]} absolute paths
|
|
133
|
+
*/
|
|
134
|
+
export function envRoots(env = process.env) {
|
|
135
|
+
const raw = env && typeof env.CORTEX_MINIAPP_ROOTS === 'string' ? env.CORTEX_MINIAPP_ROOTS : ''
|
|
136
|
+
if (!raw.trim()) return []
|
|
137
|
+
const out = []
|
|
138
|
+
for (const part of raw.split(delimiter)) {
|
|
139
|
+
let value = part.trim()
|
|
140
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1).trim()
|
|
141
|
+
if (!value) continue
|
|
142
|
+
out.push(resolve(value))
|
|
143
|
+
}
|
|
144
|
+
return out
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @param {string} value */
|
|
148
|
+
function rootKey(value) {
|
|
149
|
+
const absolute = resolve(value)
|
|
150
|
+
return process.platform === 'win32' ? absolute.toLowerCase() : absolute
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolve the effective root list: existing defaults first, then custom and
|
|
155
|
+
* env roots. Non-existent roots are dropped unless `includeMissing`.
|
|
156
|
+
* @param {{customRoots?:string[], env?:NodeJS.ProcessEnv, includeMissing?:boolean}} [options]
|
|
157
|
+
* @returns {Root[]}
|
|
158
|
+
*/
|
|
159
|
+
export function expandRoots({ customRoots = [], env = process.env, includeMissing = false } = {}) {
|
|
160
|
+
/** @type {Root[]} */
|
|
161
|
+
const roots = []
|
|
162
|
+
const seen = new Set()
|
|
163
|
+
const push = (root) => {
|
|
164
|
+
const key = rootKey(root.path)
|
|
165
|
+
if (seen.has(key)) return
|
|
166
|
+
seen.add(key)
|
|
167
|
+
roots.push(root)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const root of defaultRoots(env)) {
|
|
171
|
+
const exists = existsSync(root.path)
|
|
172
|
+
if (exists || includeMissing) push({ ...root, path: resolve(root.path) })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const extra = []
|
|
176
|
+
for (const entry of Array.isArray(customRoots) ? customRoots : []) {
|
|
177
|
+
if (typeof entry === 'string' && entry.trim()) extra.push(entry.trim())
|
|
178
|
+
}
|
|
179
|
+
extra.push(...envRoots(env))
|
|
180
|
+
|
|
181
|
+
for (const entry of extra) {
|
|
182
|
+
const absolute = resolve(entry)
|
|
183
|
+
const exists = existsSync(absolute)
|
|
184
|
+
if (!exists && !includeMissing) continue
|
|
185
|
+
push({
|
|
186
|
+
kind: 'custom',
|
|
187
|
+
path: absolute,
|
|
188
|
+
label: basename(absolute) || absolute,
|
|
189
|
+
mode: 'recursive',
|
|
190
|
+
maxDepth: DEFAULT_MAX_DEPTH,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return roots
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Sanitize one path segment for use below the miniapp workspace.
|
|
199
|
+
* @param {string} value
|
|
200
|
+
* @param {string} [fallback]
|
|
201
|
+
* @returns {string}
|
|
202
|
+
*/
|
|
203
|
+
export function sanitizeSegment(value, fallback = '_') {
|
|
204
|
+
const raw = typeof value === 'string' ? value : value === undefined || value === null ? '' : String(value)
|
|
205
|
+
let out = raw
|
|
206
|
+
.replace(/[\u0000-\u001f<>:"/\\|?*]/g, '_')
|
|
207
|
+
.replace(/^[.\s]+/, '')
|
|
208
|
+
.replace(/[.\s]+$/, '')
|
|
209
|
+
if (!out) return fallback
|
|
210
|
+
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(out)) out = `_${out}`
|
|
211
|
+
return out.slice(0, 120)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** @param {string} value */
|
|
215
|
+
function isInsideRoot(rootPath, target) {
|
|
216
|
+
const root = resolve(rootPath)
|
|
217
|
+
const path = resolve(target)
|
|
218
|
+
if (process.platform === 'win32') {
|
|
219
|
+
const lowerRoot = root.toLowerCase()
|
|
220
|
+
const lowerPath = path.toLowerCase()
|
|
221
|
+
return lowerPath === lowerRoot || lowerPath.startsWith(lowerRoot + sep)
|
|
222
|
+
}
|
|
223
|
+
return path === root || path.startsWith(root + sep)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** @param {string} segment */
|
|
227
|
+
function isVersionSegment(segment) {
|
|
228
|
+
if (!segment || segment.length > 32) return false
|
|
229
|
+
if (/^\d+$/.test(segment)) return true
|
|
230
|
+
if (/^\d+(?:\.\d+){1,3}$/.test(segment)) return true
|
|
231
|
+
if (/^\d{4}[-_.]\d{2}[-_.]\d{2}(?:[T _.-].*)?$/.test(segment)) return true
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Strip `_packages_` prefixes and app bookkeeping names, then accept only a
|
|
237
|
+
* canonical wx appid segment.
|
|
238
|
+
* @param {string} segment
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
function appidFromSegment(segment) {
|
|
242
|
+
let value = String(segment ?? '').trim()
|
|
243
|
+
if (!value || SKIP_DIR_NAMES.has(value.toLowerCase())) return ''
|
|
244
|
+
const lower = value.toLowerCase()
|
|
245
|
+
const marker = lower.indexOf('_packages_')
|
|
246
|
+
if (marker >= 0) value = value.slice(marker + '_packages_'.length)
|
|
247
|
+
if (/^_?(?:__app__|publiclib)$/i.test(value)) return ''
|
|
248
|
+
if (APPID_RE.test(value)) return value
|
|
249
|
+
return ''
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Derive `{appid, version, packageDir, packageName, rootPath, rootKind}` from
|
|
254
|
+
* one package file and the scan root that produced it.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} filePath
|
|
257
|
+
* @param {Root|{kind?:string, path:string}|string} root
|
|
258
|
+
*/
|
|
259
|
+
export function inferIdentity(filePath, root) {
|
|
260
|
+
const rootPath = resolve(typeof root === 'string' ? root : root && root.path ? root.path : process.cwd())
|
|
261
|
+
const rootKind = typeof root === 'object' && root && typeof root.kind === 'string' && root.kind ? root.kind : 'custom'
|
|
262
|
+
const fileAbs = resolve(filePath)
|
|
263
|
+
const inside = isInsideRoot(rootPath, fileAbs)
|
|
264
|
+
const packageDir = inside ? dirname(fileAbs) : rootPath
|
|
265
|
+
|
|
266
|
+
const base = basename(fileAbs)
|
|
267
|
+
const packageName = base.toLowerCase().endsWith('.wxapkg') ? base.slice(0, -'.wxapkg'.length) : base
|
|
268
|
+
|
|
269
|
+
/** @type {string} */
|
|
270
|
+
let appid = ''
|
|
271
|
+
/** @type {string} */
|
|
272
|
+
let version = ''
|
|
273
|
+
|
|
274
|
+
if (inside) {
|
|
275
|
+
const segments = relative(rootPath, packageDir)
|
|
276
|
+
.split(/[\\/]+/)
|
|
277
|
+
.filter(Boolean)
|
|
278
|
+
|
|
279
|
+
// 1. publicLib layout: .../applet/publicLib/<version>/publicLib.wxapkg
|
|
280
|
+
let publicLibIndex = -1
|
|
281
|
+
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
282
|
+
if (segments[index].toLowerCase() === 'publiclib') {
|
|
283
|
+
publicLibIndex = index
|
|
284
|
+
break
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (publicLibIndex >= 0) {
|
|
288
|
+
const candidate = segments[publicLibIndex + 1] || ''
|
|
289
|
+
return {
|
|
290
|
+
appid: 'publicLib',
|
|
291
|
+
version: isVersionSegment(candidate) ? candidate : '',
|
|
292
|
+
packageDir,
|
|
293
|
+
packageName,
|
|
294
|
+
rootPath,
|
|
295
|
+
rootKind,
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 2. canonical wx appid directory, nearest the file first.
|
|
300
|
+
let appidIndex = -1
|
|
301
|
+
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
302
|
+
const candidate = appidFromSegment(segments[index])
|
|
303
|
+
if (candidate) {
|
|
304
|
+
appid = candidate
|
|
305
|
+
appidIndex = index
|
|
306
|
+
break
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (appidIndex >= 0) {
|
|
311
|
+
for (let index = appidIndex + 1; index < segments.length; index += 1) {
|
|
312
|
+
if (isVersionSegment(segments[index])) {
|
|
313
|
+
version = segments[index]
|
|
314
|
+
break
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { appid, version, packageDir, packageName, rootPath, rootKind }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Absolute miniapp workspace root (`CORTEX_MINIAPP_WORKSPACE` or
|
|
325
|
+
* `~/.cortex/miniapps`).
|
|
326
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
327
|
+
* @returns {string}
|
|
328
|
+
*/
|
|
329
|
+
export function miniAppWorkspaceRoot(env = process.env) {
|
|
330
|
+
const override =
|
|
331
|
+
env && typeof env.CORTEX_MINIAPP_WORKSPACE === 'string' ? env.CORTEX_MINIAPP_WORKSPACE.trim() : ''
|
|
332
|
+
return resolve(override || join(homeDir(env), '.cortex', 'miniapps'))
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Per-package workspace layout:
|
|
337
|
+
* <root>/<appid>/<version>/
|
|
338
|
+
* unpacked/ extracted app files + manifest.json + overview.json
|
|
339
|
+
*
|
|
340
|
+
* `manifestPath` / `extractedPath` live inside `unpacked` so the unpack and
|
|
341
|
+
* analyze helpers can address them from `unpacked_path` alone.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} appid
|
|
344
|
+
* @param {string} version
|
|
345
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
346
|
+
*/
|
|
347
|
+
export function packageWorkspace(appid, version, env = process.env) {
|
|
348
|
+
const root = join(
|
|
349
|
+
miniAppWorkspaceRoot(env),
|
|
350
|
+
sanitizeSegment(appid, 'unknown'),
|
|
351
|
+
sanitizeSegment(version, 'unknown'),
|
|
352
|
+
)
|
|
353
|
+
const unpacked = join(root, 'unpacked')
|
|
354
|
+
return {
|
|
355
|
+
root,
|
|
356
|
+
unpacked,
|
|
357
|
+
manifestPath: join(unpacked, 'manifest.json'),
|
|
358
|
+
extractedPath: join(unpacked, 'extracted.json'),
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP surface for the mini-program (wxapkg) reverse-engineering feature.
|
|
3
|
+
*
|
|
4
|
+
* Mounted under `/cortex-api/cortex/miniapps/**`. The parent dispatcher
|
|
5
|
+
* (`src/host/cortex/index.js`) already applied the loopback peer/Host fences and
|
|
6
|
+
* the same-origin write fence, so this module only routes, validates input and
|
|
7
|
+
* maps the shared service layer onto JSON responses.
|
|
8
|
+
*
|
|
9
|
+
* The parent imports this file lazily: a failure in the miniapp modules must
|
|
10
|
+
* never take down the rest of the Cortex UI/API.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync } from 'node:fs'
|
|
14
|
+
import { isAbsolute, resolve } from 'node:path'
|
|
15
|
+
import {
|
|
16
|
+
badRequest,
|
|
17
|
+
methodNotAllowed,
|
|
18
|
+
queryBool01,
|
|
19
|
+
queryInt,
|
|
20
|
+
queryString,
|
|
21
|
+
readJsonBody,
|
|
22
|
+
safeDecode,
|
|
23
|
+
sendJson,
|
|
24
|
+
} from '../http.js'
|
|
25
|
+
import { defaultRoots, expandRoots } from './roots.js'
|
|
26
|
+
import {
|
|
27
|
+
analyzeSourceRecord,
|
|
28
|
+
detailPayload,
|
|
29
|
+
fileForSource,
|
|
30
|
+
importAssetsForSource,
|
|
31
|
+
listMiniapps,
|
|
32
|
+
miniappStore,
|
|
33
|
+
openSourceDirectory,
|
|
34
|
+
requireSourceById,
|
|
35
|
+
scan,
|
|
36
|
+
searchSource,
|
|
37
|
+
treeForSource,
|
|
38
|
+
unpackSourceRecord,
|
|
39
|
+
} from './service.js'
|
|
40
|
+
|
|
41
|
+
const MAX_ID = 200
|
|
42
|
+
const MAX_PATH = 4000
|
|
43
|
+
const MAX_CUSTOM_ROOTS = 20
|
|
44
|
+
|
|
45
|
+
function sourceId(ctx) {
|
|
46
|
+
const raw = Array.isArray(ctx.params) ? ctx.params[0] : ''
|
|
47
|
+
const id = safeDecode(raw, 'miniapp id')
|
|
48
|
+
if (id.length === 0) throw badRequest('miniapp id is required')
|
|
49
|
+
if (id.length > MAX_ID) throw badRequest('miniapp id is too long', { max: MAX_ID })
|
|
50
|
+
return id
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Read a JSON body when present; empty POSTs are treated as `{}`. */
|
|
54
|
+
async function bodyOrEmpty(req) {
|
|
55
|
+
const headers = (req && req.headers) || {}
|
|
56
|
+
const length = Number(headers['content-length'] || 0)
|
|
57
|
+
if (length === 0 && headers['transfer-encoding'] === undefined) return {}
|
|
58
|
+
return readJsonBody(req)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function handleList({ res, searchParams }) {
|
|
62
|
+
const search = queryString(searchParams, 'search', { max: 400 }) ?? ''
|
|
63
|
+
const status = queryString(searchParams, 'status', { max: 40 }) ?? ''
|
|
64
|
+
sendJson(res, 200, listMiniapps(miniappStore(), { search, status }))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function handleScan({ req, res }) {
|
|
68
|
+
const body = await bodyOrEmpty(req)
|
|
69
|
+
let customRoots
|
|
70
|
+
// An empty array from the UI means "scan with the saved roots"; only a
|
|
71
|
+
// non-empty array replaces them. Clearing is done through POST /miniapps/roots.
|
|
72
|
+
if (Array.isArray(body.customRoots) && body.customRoots.length > 0) {
|
|
73
|
+
customRoots = normalizeRoots(body.customRoots)
|
|
74
|
+
}
|
|
75
|
+
const result = await scan(miniappStore(), { customRoots })
|
|
76
|
+
sendJson(res, 200, result)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function rootsPayload(customRoots) {
|
|
80
|
+
const all = expandRoots({ customRoots, includeMissing: true }).map((root) => ({
|
|
81
|
+
kind: root.kind,
|
|
82
|
+
path: root.path,
|
|
83
|
+
label: root.label,
|
|
84
|
+
mode: root.mode,
|
|
85
|
+
exists: existsSync(root.path),
|
|
86
|
+
}))
|
|
87
|
+
return { roots: all, customRoots }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function handleRootsGet({ res }) {
|
|
91
|
+
const store = miniappStore()
|
|
92
|
+
sendJson(res, 200, rootsPayload(store.customRoots()))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function handleRootsSet({ req, res }) {
|
|
96
|
+
const body = await readJsonBody(req)
|
|
97
|
+
const roots = normalizeRoots(body.roots)
|
|
98
|
+
const mode = body.mode === 'append' ? 'append' : 'replace'
|
|
99
|
+
const store = miniappStore()
|
|
100
|
+
const next = mode === 'append' ? [...new Set([...store.customRoots(), ...roots])] : roots
|
|
101
|
+
store.setCustomRoots(next)
|
|
102
|
+
sendJson(res, 200, rootsPayload(next))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function handleDetailRoute(ctx) {
|
|
106
|
+
const store = miniappStore()
|
|
107
|
+
const source = requireSourceById(store, sourceId(ctx))
|
|
108
|
+
sendJson(ctx.res, 200, detailPayload(store, source))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function handleUnpack(ctx) {
|
|
112
|
+
const store = miniappStore()
|
|
113
|
+
const source = requireSourceById(store, sourceId(ctx))
|
|
114
|
+
let force = false
|
|
115
|
+
try {
|
|
116
|
+
const body = await bodyOrEmpty(ctx.req)
|
|
117
|
+
force = body.force === true
|
|
118
|
+
} catch {
|
|
119
|
+
force = false
|
|
120
|
+
}
|
|
121
|
+
const result = await unpackSourceRecord({ store, source, force })
|
|
122
|
+
const row = store.getSource(source.id) || source
|
|
123
|
+
sendJson(ctx.res, 200, {
|
|
124
|
+
ok: result.ok === true,
|
|
125
|
+
status: result.status,
|
|
126
|
+
cached: result.cached === true,
|
|
127
|
+
error: result.error || '',
|
|
128
|
+
totals: result.totals || null,
|
|
129
|
+
unpackedPath: result.unpackedPath || '',
|
|
130
|
+
manifest: result.manifest || null,
|
|
131
|
+
miniapp: detailPayload(store, row).miniapp,
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function handleAnalyze(ctx) {
|
|
136
|
+
const store = miniappStore()
|
|
137
|
+
const source = requireSourceById(store, sourceId(ctx))
|
|
138
|
+
let force = false
|
|
139
|
+
try {
|
|
140
|
+
const body = await bodyOrEmpty(ctx.req)
|
|
141
|
+
force = body.force === true
|
|
142
|
+
} catch {
|
|
143
|
+
force = false
|
|
144
|
+
}
|
|
145
|
+
const result = analyzeSourceRecord({ store, source, force })
|
|
146
|
+
const row = store.getSource(source.id) || source
|
|
147
|
+
sendJson(ctx.res, 200, {
|
|
148
|
+
ok: result.ok,
|
|
149
|
+
cached: result.cached === true,
|
|
150
|
+
analysis: result.analysis,
|
|
151
|
+
overviewText: result.overviewText,
|
|
152
|
+
counts: result.counts,
|
|
153
|
+
miniapp: detailPayload(store, row).miniapp,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function handleTreeRoute(ctx) {
|
|
158
|
+
const source = requireSourceById(miniappStore(), sourceId(ctx))
|
|
159
|
+
const path = queryString(ctx.searchParams, 'path', { max: MAX_PATH }) ?? ''
|
|
160
|
+
sendJson(ctx.res, 200, treeForSource(source, path))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function handleFileRoute(ctx) {
|
|
164
|
+
const source = requireSourceById(miniappStore(), sourceId(ctx))
|
|
165
|
+
const path = queryString(ctx.searchParams, 'path', { max: MAX_PATH })
|
|
166
|
+
if (!path) throw badRequest('path is required', { field: 'path' })
|
|
167
|
+
const offset = queryInt(ctx.searchParams, 'offset', { min: 0, max: 10_000_000 }) ?? 0
|
|
168
|
+
const limit = queryInt(ctx.searchParams, 'limit', { min: 1, max: 5000 }) ?? 200
|
|
169
|
+
const maxBytes = queryInt(ctx.searchParams, 'maxBytes', { min: 1024, max: 5 * 1024 * 1024 }) ?? 1024 * 1024
|
|
170
|
+
const rawFormat = queryBool01(ctx.searchParams, 'format')
|
|
171
|
+
const find = queryString(ctx.searchParams, 'find', { max: 400 }) ?? ''
|
|
172
|
+
const findRegex = queryBool01(ctx.searchParams, 'findRegex') === true
|
|
173
|
+
const findMax = queryInt(ctx.searchParams, 'findMax', { min: 1, max: 1000 }) ?? 200
|
|
174
|
+
const result = fileForSource(source, path, {
|
|
175
|
+
offset,
|
|
176
|
+
limit,
|
|
177
|
+
format: rawFormat === false ? false : 'auto',
|
|
178
|
+
maxBytes,
|
|
179
|
+
find,
|
|
180
|
+
findRegex,
|
|
181
|
+
findMax,
|
|
182
|
+
})
|
|
183
|
+
if (result.binary) {
|
|
184
|
+
throw badRequest('binary files cannot be previewed as text', {
|
|
185
|
+
code: 'BINARY_FILE',
|
|
186
|
+
path: result.path,
|
|
187
|
+
size: result.size,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
sendJson(ctx.res, 200, result)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function handleSearchRoute(ctx) {
|
|
194
|
+
const source = requireSourceById(miniappStore(), sourceId(ctx))
|
|
195
|
+
const query = queryString(ctx.searchParams, 'query', { max: 400 })
|
|
196
|
+
if (!query) throw badRequest('query is required', { field: 'query' })
|
|
197
|
+
const regex = queryBool01(ctx.searchParams, 'regex') === true
|
|
198
|
+
const glob = queryString(ctx.searchParams, 'glob', { max: 300 }) ?? ''
|
|
199
|
+
const maxResults = queryInt(ctx.searchParams, 'maxResults', { min: 1, max: 200 }) ?? 50
|
|
200
|
+
sendJson(ctx.res, 200, searchSource(source, { query, regex, glob, maxResults }))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function handleImportAssets(ctx) {
|
|
204
|
+
const store = miniappStore()
|
|
205
|
+
const source = requireSourceById(store, sourceId(ctx))
|
|
206
|
+
const body = await bodyOrEmpty(ctx.req)
|
|
207
|
+
const projectName =
|
|
208
|
+
typeof body.project_name === 'string' && body.project_name.trim().length > 0
|
|
209
|
+
? body.project_name.trim().slice(0, 120)
|
|
210
|
+
: undefined
|
|
211
|
+
const hostPort =
|
|
212
|
+
typeof body.host_port === 'string' && body.host_port.trim().length > 0
|
|
213
|
+
? body.host_port.trim().slice(0, 4000)
|
|
214
|
+
: undefined
|
|
215
|
+
const result = await importAssetsForSource({
|
|
216
|
+
holder: ctx.holder,
|
|
217
|
+
store,
|
|
218
|
+
source,
|
|
219
|
+
projectName,
|
|
220
|
+
hostPort,
|
|
221
|
+
})
|
|
222
|
+
sendJson(ctx.res, 200, result)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function handleOpen(ctx) {
|
|
226
|
+
const store = miniappStore()
|
|
227
|
+
const source = requireSourceById(store, sourceId(ctx))
|
|
228
|
+
sendJson(ctx.res, 200, openSourceDirectory(source))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function handleRename(ctx) {
|
|
232
|
+
const store = miniappStore()
|
|
233
|
+
const source = requireSourceById(store, sourceId(ctx))
|
|
234
|
+
const body = await bodyOrEmpty(ctx.req)
|
|
235
|
+
const raw = typeof body.app_name === 'string' ? body.app_name : ''
|
|
236
|
+
if (raw.length > 120) throw badRequest('app_name is too long', { field: 'app_name', max: 120 })
|
|
237
|
+
const appName = raw.replace(/\s+/g, ' ').trim().slice(0, 60)
|
|
238
|
+
const row = store.updateSource(source.id, { app_name: appName })
|
|
239
|
+
sendJson(ctx.res, 200, { ok: true, miniapp: detailPayload(store, row || source).miniapp })
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function normalizeRoots(raw) {
|
|
243
|
+
if (!Array.isArray(raw)) throw badRequest('roots must be an array', { field: 'roots' })
|
|
244
|
+
if (raw.length > MAX_CUSTOM_ROOTS) throw badRequest(`at most ${MAX_CUSTOM_ROOTS} custom roots`, { max: MAX_CUSTOM_ROOTS })
|
|
245
|
+
const result = []
|
|
246
|
+
for (const value of raw) {
|
|
247
|
+
if (typeof value !== 'string') throw badRequest('each root must be a string', { field: 'roots' })
|
|
248
|
+
const trimmed = value.trim()
|
|
249
|
+
if (trimmed.length === 0) continue
|
|
250
|
+
if (trimmed.length > 2000) throw badRequest('root path is too long', { field: 'roots', max: 2000 })
|
|
251
|
+
if (!isAbsolute(trimmed)) throw badRequest('root path must be absolute', { field: 'roots' })
|
|
252
|
+
const resolved = resolve(trimmed)
|
|
253
|
+
if (!result.includes(resolved)) result.push(resolved)
|
|
254
|
+
}
|
|
255
|
+
return result
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* -------------------------------- routing -------------------------------- */
|
|
259
|
+
|
|
260
|
+
const ROUTES = [
|
|
261
|
+
{ method: 'GET', pattern: /^\/miniapps$/, handler: handleList },
|
|
262
|
+
{ method: 'POST', pattern: /^\/miniapps\/scan$/, handler: handleScan },
|
|
263
|
+
{ method: 'GET', pattern: /^\/miniapps\/roots$/, handler: handleRootsGet },
|
|
264
|
+
{ method: 'POST', pattern: /^\/miniapps\/roots$/, handler: handleRootsSet },
|
|
265
|
+
{ method: 'POST', pattern: /^\/miniapps\/([^/]+)\/unpack$/, handler: handleUnpack },
|
|
266
|
+
{ method: 'POST', pattern: /^\/miniapps\/([^/]+)\/analyze$/, handler: handleAnalyze },
|
|
267
|
+
{ method: 'POST', pattern: /^\/miniapps\/([^/]+)\/import-assets$/, handler: handleImportAssets },
|
|
268
|
+
{ method: 'POST', pattern: /^\/miniapps\/([^/]+)\/open$/, handler: handleOpen },
|
|
269
|
+
{ method: 'POST', pattern: /^\/miniapps\/([^/]+)\/name$/, handler: handleRename },
|
|
270
|
+
{ method: 'GET', pattern: /^\/miniapps\/([^/]+)\/tree$/, handler: handleTreeRoute },
|
|
271
|
+
{ method: 'GET', pattern: /^\/miniapps\/([^/]+)\/file$/, handler: handleFileRoute },
|
|
272
|
+
{ method: 'GET', pattern: /^\/miniapps\/([^/]+)\/search$/, handler: handleSearchRoute },
|
|
273
|
+
{ method: 'GET', pattern: /^\/miniapps\/([^/]+)$/, handler: handleDetailRoute },
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Handle one `/miniapps/**` request. Returns false when the path is not ours
|
|
278
|
+
* (the parent dispatcher then falls through to its own route table).
|
|
279
|
+
* @param {Record<string, any>} ctx
|
|
280
|
+
*/
|
|
281
|
+
export async function handleMiniapps(ctx) {
|
|
282
|
+
const path = String(ctx.path || '')
|
|
283
|
+
const method = String(ctx.method || 'GET').toUpperCase()
|
|
284
|
+
if (path !== '/miniapps' && !path.startsWith('/miniapps/')) return false
|
|
285
|
+
const matched = ROUTES.filter((route) => route.pattern.test(path))
|
|
286
|
+
if (matched.length === 0) return false
|
|
287
|
+
const route = matched.find((candidate) => candidate.method === method)
|
|
288
|
+
if (!route) throw methodNotAllowed()
|
|
289
|
+
const match = route.pattern.exec(path)
|
|
290
|
+
ctx.params = match.slice(1)
|
|
291
|
+
try {
|
|
292
|
+
await route.handler(ctx)
|
|
293
|
+
} catch (error) {
|
|
294
|
+
// Workspace/container errors are client-visible 4xx, never opaque 500s.
|
|
295
|
+
if (error && error.name === 'WxapkgError') {
|
|
296
|
+
throw badRequest(error.message || 'mini-program operation failed', { code: error.code })
|
|
297
|
+
}
|
|
298
|
+
throw error
|
|
299
|
+
}
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Exposed for diagnostics/tests (not part of the public HTTP contract). */
|
|
304
|
+
export { ROUTES as MINIAPP_ROUTES, defaultRoots }
|