weapp-vite 7.3.0 → 7.5.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 +154 -6
- package/bin/bootstrap.js +14 -1
- package/dist/adapters-CcHrsYaj.mjs +1 -0
- package/dist/analyze-ZJUvEdaY.mjs +248 -0
- package/dist/auto-import-components/resolvers.mjs +1 -2
- package/dist/auto-import-presets/wevu-router.mjs +1 -2
- package/dist/auto-import-presets/wevu.mjs +1 -2
- package/dist/auto-routes.mjs +2 -1
- package/dist/budget-BD7VqLCl.d.mts +178 -0
- package/dist/chunkStrategy-DRA5WpB-.mjs +1436 -0
- package/dist/cli-acceptance.d.mts +1 -0
- package/dist/cli-acceptance.mjs +21 -0
- package/dist/cli.mjs +1918 -2089
- package/dist/compatibility.mjs +1 -2
- package/dist/{config-1J4rToMR.d.mts → config-V0L-IAJf.d.mts} +419 -39
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +1 -1
- package/dist/constants-DEZdcsMn.mjs +85 -0
- package/dist/{createContext-cqr9Y4EV.mjs → createCompilerContextInstance-DJH_P_nZ.mjs} +17602 -15886
- package/dist/createContext-BqYzHhY9.mjs +12 -0
- package/dist/dashboard/analyze.d.mts +48 -0
- package/dist/dashboard/analyze.mjs +1 -0
- package/dist/dashboard/vite.d.mts +9 -0
- package/dist/dashboard/vite.mjs +44 -0
- package/dist/dashboard-DvqF3Key.mjs +1842 -0
- package/dist/dashboard.d.mts +3 -0
- package/dist/dashboard.mjs +1 -0
- package/dist/dashboardViteBridge-Dy3nAD4m.mjs +299 -0
- package/dist/dev-fR5GiJRO.mjs +63 -0
- package/dist/devProvider-BGBkw28J.mjs +1050 -0
- package/dist/docs/README.md +154 -6
- package/dist/docs/ai-workflows.md +4 -0
- package/dist/docs/doctor.md +38 -0
- package/dist/docs/getting-started.md +52 -1
- package/dist/docs/index.md +6 -2
- package/dist/docs/mcp.md +94 -1
- package/dist/docs/testing.md +29 -2
- package/dist/docs/troubleshooting.md +8 -0
- package/dist/docs/uni-app-component-libraries.md +8 -7
- package/dist/docs/upload.md +226 -0
- package/dist/docs/vite-plugin.md +171 -0
- package/dist/docs/vue-sfc.md +19 -0
- package/dist/docs/weapp-config.md +374 -2
- package/dist/docs/wevu-authoring.md +10 -0
- package/dist/doctor-worker.d.mts +1 -0
- package/dist/doctor-worker.mjs +59 -0
- package/dist/doctor.d.mts +144 -0
- package/dist/doctor.mjs +848 -0
- package/dist/eslint.mjs +1 -2
- package/dist/file-DRb4eWqw.mjs +1 -0
- package/dist/{file-CKB0Tn8Z.mjs → file-sC3jD7Fh.mjs} +16 -93
- package/dist/files-BqxFIPRp.mjs +45 -0
- package/dist/getInstance-DiGSzBNI.mjs +28 -0
- package/dist/getInstance-SiE6WZRh.mjs +1 -0
- package/dist/index-C9HjUX5c.d.mts +741 -0
- package/dist/index.d.mts +10 -8
- package/dist/index.mjs +3 -2
- package/dist/json.d.mts +1 -1
- package/dist/json.mjs +1 -2
- package/dist/lifecycle-BROTlwSL.mjs +320 -0
- package/dist/loadConfig-C3BQhz7F.mjs +59 -0
- package/dist/loadConfig-VGtBF4qm.mjs +1 -0
- package/dist/logger-DGn5iEVN.mjs +1 -0
- package/dist/{mcp-BG6TliEg.mjs → mcp-Cj8Maqo6.mjs} +16 -7
- package/dist/mcp-D3Yh24Cj.mjs +529 -0
- package/dist/mcp.d.mts +6 -3
- package/dist/mcp.mjs +1 -2
- package/dist/package-7swhyLmf.mjs +91 -0
- package/dist/path-doV773b7.mjs +103 -0
- package/dist/platform-CnkySBKz.mjs +175 -0
- package/dist/processCleanup-B6KA30Zc.mjs +77 -0
- package/dist/protocol-vg8HGJ4n.mjs +138 -0
- package/dist/{resolvers-icNxkpcy.mjs → resolvers-Dod-euqX.mjs} +4 -1
- package/dist/resolvers.mjs +1 -2
- package/dist/runtime.mjs +1 -2
- package/dist/session-PskhD-nZ.mjs +390 -0
- package/dist/test.d.mts +1 -0
- package/dist/test.mjs +201 -44
- package/dist/types.d.mts +2 -2
- package/dist/upload-D4msblBf.mjs +562 -0
- package/dist/upload-worker.d.mts +1 -0
- package/dist/upload-worker.mjs +161 -0
- package/dist/utils-CJSCgEj6.mjs +1094 -0
- package/dist/vite.d.mts +9 -0
- package/dist/vite.mjs +270 -0
- package/dist/volar.d.mts +2 -2
- package/dist/web-apis.mjs +1 -2
- package/package.json +85 -37
- package/dist/file-CvgkbGIx.mjs +0 -2
- package/dist/getInstance-CZ1s63zd.mjs +0 -2
- package/dist/logger-mt4mSTqV.mjs +0 -2
|
@@ -0,0 +1,1842 @@
|
|
|
1
|
+
import { t as VERSION } from "./constants-DEZdcsMn.mjs";
|
|
2
|
+
import { i as createAnalyzeBudgetCheck, n as createModuleByteMap, r as createAnalyzeComparison, t as createDuplicateModuleInsights } from "./analyze-ZJUvEdaY.mjs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import path from "pathe";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
8
|
+
import { Buffer } from "node:buffer";
|
|
9
|
+
import { defineDevframe, defineRpcFunction } from "devframe";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
//#region src/dashboard/artifacts.ts
|
|
12
|
+
const MAX_DASHBOARD_FILE_CONTENT_BYTES = 2097152;
|
|
13
|
+
/** 保留同一次分析的产物内容,不读取或改写开发构建目录。 */
|
|
14
|
+
function createDashboardArtifactSnapshot() {
|
|
15
|
+
const files = /* @__PURE__ */ new Map();
|
|
16
|
+
let retainedBytes = 0;
|
|
17
|
+
return {
|
|
18
|
+
files,
|
|
19
|
+
capture(fileName, source) {
|
|
20
|
+
const key = fileName.replaceAll("\\", "/");
|
|
21
|
+
const previous = files.get(key);
|
|
22
|
+
if (previous?.content !== void 0) retainedBytes -= previous.size;
|
|
23
|
+
const size = typeof source === "string" ? Buffer.byteLength(source, "utf8") : source.byteLength;
|
|
24
|
+
if (size > 2097152) {
|
|
25
|
+
files.set(key, {
|
|
26
|
+
size,
|
|
27
|
+
error: `文件超过 ${MAX_DASHBOARD_FILE_CONTENT_BYTES} 字节,已拒绝读取。`
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (retainedBytes + size > 33554432) {
|
|
32
|
+
files.set(key, {
|
|
33
|
+
size,
|
|
34
|
+
error: "分析产物内容超过快照容量上限,已拒绝读取。"
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const content = typeof source === "string" ? source : Buffer.from(source.buffer, source.byteOffset, source.byteLength).toString("utf8");
|
|
39
|
+
files.set(key, {
|
|
40
|
+
content,
|
|
41
|
+
size
|
|
42
|
+
});
|
|
43
|
+
retainedBytes += size;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/dashboard/excerpt.ts
|
|
49
|
+
/** 按已校验的 UTF-16 范围截取内容,保留换行与代理项,不改变完整文件大小。 */
|
|
50
|
+
function createDashboardFileExcerpt(content, range) {
|
|
51
|
+
if (!range) return { content };
|
|
52
|
+
const totalCharacters = content.length;
|
|
53
|
+
if (range.offset > totalCharacters) throw new RangeError("文件读取偏移超出内容范围。");
|
|
54
|
+
const end = Math.min(totalCharacters, range.offset + range.limit);
|
|
55
|
+
return {
|
|
56
|
+
content: content.slice(range.offset, end),
|
|
57
|
+
range: {
|
|
58
|
+
offset: range.offset,
|
|
59
|
+
totalCharacters,
|
|
60
|
+
nextOffset: end < totalCharacters ? end : null
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/dashboard/paths.ts
|
|
66
|
+
function normalizeDashboardRelativePath(value) {
|
|
67
|
+
return value.replaceAll("\\", "/");
|
|
68
|
+
}
|
|
69
|
+
function stripDashboardFileQuery(value) {
|
|
70
|
+
const queryIndex = value.indexOf("?");
|
|
71
|
+
return queryIndex === -1 ? value : value.slice(0, queryIndex);
|
|
72
|
+
}
|
|
73
|
+
function addDashboardAllowedPath(paths, value) {
|
|
74
|
+
if (!value || value.includes("\0")) return;
|
|
75
|
+
const normalizedPath = normalizeDashboardRelativePath(stripDashboardFileQuery(value));
|
|
76
|
+
if (!normalizedPath || path.isAbsolute(normalizedPath)) return;
|
|
77
|
+
paths.add(normalizedPath);
|
|
78
|
+
}
|
|
79
|
+
function addDashboardAllowedSourcePath(paths, value, rootKind) {
|
|
80
|
+
if (!value || value.includes("\0")) return;
|
|
81
|
+
const normalizedPath = normalizeDashboardRelativePath(stripDashboardFileQuery(value));
|
|
82
|
+
if (!normalizedPath || path.isAbsolute(normalizedPath)) return;
|
|
83
|
+
const rootKinds = paths.get(normalizedPath) ?? /* @__PURE__ */ new Set();
|
|
84
|
+
rootKinds.add(rootKind);
|
|
85
|
+
paths.set(normalizedPath, rootKinds);
|
|
86
|
+
}
|
|
87
|
+
function resolveDashboardSourceRootKind(sourceType) {
|
|
88
|
+
if (sourceType === "plugin") return "plugin";
|
|
89
|
+
if (sourceType === "workspace" || sourceType === "node_modules") return "project";
|
|
90
|
+
return "src";
|
|
91
|
+
}
|
|
92
|
+
function createDashboardContentAllowlist(result) {
|
|
93
|
+
const artifactPaths = /* @__PURE__ */ new Set();
|
|
94
|
+
const sourcePaths = /* @__PURE__ */ new Map();
|
|
95
|
+
for (const packageReport of result.packages) for (const file of packageReport.files) {
|
|
96
|
+
addDashboardAllowedPath(artifactPaths, file.file);
|
|
97
|
+
const fileSourceType = file.sourceType ?? file.modules?.find((module) => module.source === file.source)?.sourceType;
|
|
98
|
+
addDashboardAllowedSourcePath(sourcePaths, file.source, resolveDashboardSourceRootKind(fileSourceType));
|
|
99
|
+
for (const module of file.modules ?? []) addDashboardAllowedSourcePath(sourcePaths, module.source, module.sourceType === "src" ? "src" : module.sourceType === "plugin" ? "plugin" : "project");
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
artifactPaths,
|
|
103
|
+
sourcePaths
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function resolveDashboardContentPath(root, requestPath, options) {
|
|
107
|
+
if (!root || !requestPath || requestPath.includes("\0")) return;
|
|
108
|
+
const normalizedRequestPath = normalizeDashboardRelativePath(stripDashboardFileQuery(requestPath));
|
|
109
|
+
if (path.isAbsolute(normalizedRequestPath) || !options.allowedPaths.has(normalizedRequestPath)) return;
|
|
110
|
+
const rootPath = path.resolve(root);
|
|
111
|
+
const absolutePath = path.resolve(rootPath, normalizedRequestPath);
|
|
112
|
+
const relativePath = path.relative(rootPath, absolutePath);
|
|
113
|
+
const allowParent = options.allowParent === true;
|
|
114
|
+
if (!relativePath || !allowParent && (relativePath.startsWith("..") || path.isAbsolute(relativePath))) return;
|
|
115
|
+
return {
|
|
116
|
+
absolutePath,
|
|
117
|
+
allowParent,
|
|
118
|
+
relativePath: allowParent ? normalizedRequestPath : normalizeDashboardRelativePath(relativePath),
|
|
119
|
+
rootPath
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function resolveDashboardSourceContentPaths(roots, requestPath, allowedPaths) {
|
|
123
|
+
const normalizedRequestPath = normalizeDashboardRelativePath(stripDashboardFileQuery(requestPath));
|
|
124
|
+
const rootKinds = allowedPaths.get(normalizedRequestPath);
|
|
125
|
+
if (!rootKinds || rootKinds.size !== 1) return [];
|
|
126
|
+
const [rootKind] = rootKinds;
|
|
127
|
+
if (rootKind === "project") {
|
|
128
|
+
const resolved = resolveDashboardContentPath(roots.projectRoot, normalizedRequestPath, {
|
|
129
|
+
allowParent: true,
|
|
130
|
+
allowedPaths: /* @__PURE__ */ new Set([normalizedRequestPath])
|
|
131
|
+
});
|
|
132
|
+
return resolved ? [resolved] : [];
|
|
133
|
+
}
|
|
134
|
+
if (rootKind === "plugin") {
|
|
135
|
+
if (!roots.pluginRoot) return [];
|
|
136
|
+
const pluginBase = path.basename(roots.pluginRoot);
|
|
137
|
+
if (normalizedRequestPath !== pluginBase && !normalizedRequestPath.startsWith(`${pluginBase}/`)) return [];
|
|
138
|
+
const pluginRelativePath = normalizedRequestPath === pluginBase ? "" : normalizedRequestPath.slice(pluginBase.length + 1);
|
|
139
|
+
const resolved = resolveDashboardContentPath(roots.pluginRoot, pluginRelativePath, { allowedPaths: /* @__PURE__ */ new Set([pluginRelativePath]) });
|
|
140
|
+
return resolved ? [{
|
|
141
|
+
...resolved,
|
|
142
|
+
relativePath: normalizedRequestPath
|
|
143
|
+
}] : [];
|
|
144
|
+
}
|
|
145
|
+
if (!roots.srcRoot) return [];
|
|
146
|
+
const candidates = (normalizedRequestPath.startsWith("src/") ? [normalizedRequestPath, normalizedRequestPath.slice(4)] : [normalizedRequestPath]).flatMap((relativePath) => {
|
|
147
|
+
const resolved = resolveDashboardContentPath(roots.srcRoot, relativePath, { allowedPaths: /* @__PURE__ */ new Set([relativePath]) });
|
|
148
|
+
return resolved ? [{
|
|
149
|
+
...resolved,
|
|
150
|
+
relativePath: normalizedRequestPath
|
|
151
|
+
}] : [];
|
|
152
|
+
});
|
|
153
|
+
return [...new Map(candidates.map((candidate) => [candidate.absolutePath, candidate])).values()];
|
|
154
|
+
}
|
|
155
|
+
function resolveDashboardArtifactPath(requestPath, allowedPaths) {
|
|
156
|
+
const normalizedPath = normalizeDashboardRelativePath(stripDashboardFileQuery(requestPath));
|
|
157
|
+
if (!normalizedPath || normalizedPath.includes("\0") || path.isAbsolute(normalizedPath) || path.posix.normalize(normalizedPath).startsWith("..") || !allowedPaths.has(normalizedPath)) return;
|
|
158
|
+
return normalizedPath;
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/dashboard/investigations/schema.ts
|
|
162
|
+
const id = z.string().uuid();
|
|
163
|
+
const version = z.number().int().positive();
|
|
164
|
+
const text = z.string().trim().min(1).max(4096);
|
|
165
|
+
const path$1 = z.string().min(1).max(2048);
|
|
166
|
+
const bytes = z.number().nonnegative().nullable();
|
|
167
|
+
const reportIdentitySchema = z.strictObject({
|
|
168
|
+
sessionId: id,
|
|
169
|
+
revision: z.number().int().nonnegative(),
|
|
170
|
+
reportHash: z.string().regex(/^[a-f0-9]{64}$/)
|
|
171
|
+
});
|
|
172
|
+
const investigationTargetSchema = z.discriminatedUnion("kind", [
|
|
173
|
+
z.strictObject({
|
|
174
|
+
kind: z.literal("package"),
|
|
175
|
+
packageId: path$1
|
|
176
|
+
}),
|
|
177
|
+
z.strictObject({
|
|
178
|
+
kind: z.literal("artifact"),
|
|
179
|
+
packageId: path$1,
|
|
180
|
+
file: path$1
|
|
181
|
+
}),
|
|
182
|
+
z.strictObject({
|
|
183
|
+
kind: z.literal("module"),
|
|
184
|
+
packageId: path$1,
|
|
185
|
+
file: path$1,
|
|
186
|
+
moduleId: path$1
|
|
187
|
+
})
|
|
188
|
+
]);
|
|
189
|
+
const measurementsSchema = z.strictObject({
|
|
190
|
+
label: z.string(),
|
|
191
|
+
rawBytes: bytes,
|
|
192
|
+
gzipBytes: bytes,
|
|
193
|
+
brotliBytes: bytes,
|
|
194
|
+
attributedBytes: bytes,
|
|
195
|
+
sourceBytes: bytes
|
|
196
|
+
});
|
|
197
|
+
const investigationProposalInputSchema = z.strictObject({
|
|
198
|
+
summary: text,
|
|
199
|
+
changes: z.array(z.strictObject({
|
|
200
|
+
path: path$1,
|
|
201
|
+
description: text
|
|
202
|
+
})).min(1).max(64),
|
|
203
|
+
checks: z.array(text).min(1).max(32),
|
|
204
|
+
risks: z.array(text).max(32)
|
|
205
|
+
});
|
|
206
|
+
const investigationReceiptInputSchema = z.strictObject({
|
|
207
|
+
outcome: z.enum(["completed", "failed"]),
|
|
208
|
+
summary: text,
|
|
209
|
+
changedFiles: z.array(path$1).max(64),
|
|
210
|
+
checks: z.array(z.strictObject({
|
|
211
|
+
command: text,
|
|
212
|
+
outcome: z.enum([
|
|
213
|
+
"passed",
|
|
214
|
+
"failed",
|
|
215
|
+
"not-run"
|
|
216
|
+
]),
|
|
217
|
+
summary: text
|
|
218
|
+
})).max(32)
|
|
219
|
+
});
|
|
220
|
+
const investigationSchema = z.strictObject({
|
|
221
|
+
id,
|
|
222
|
+
version,
|
|
223
|
+
createdAt: z.string().datetime(),
|
|
224
|
+
updatedAt: z.string().datetime(),
|
|
225
|
+
report: reportIdentitySchema,
|
|
226
|
+
target: investigationTargetSchema,
|
|
227
|
+
question: text,
|
|
228
|
+
evidence: measurementsSchema,
|
|
229
|
+
status: z.enum([
|
|
230
|
+
"submitted",
|
|
231
|
+
"claimed",
|
|
232
|
+
"proposed",
|
|
233
|
+
"authorized",
|
|
234
|
+
"executing",
|
|
235
|
+
"completed",
|
|
236
|
+
"failed",
|
|
237
|
+
"cancelled",
|
|
238
|
+
"stale",
|
|
239
|
+
"verified"
|
|
240
|
+
]),
|
|
241
|
+
agent: z.strictObject({
|
|
242
|
+
name: z.string().trim().min(1).max(120),
|
|
243
|
+
claimedAt: z.string().datetime()
|
|
244
|
+
}).nullable(),
|
|
245
|
+
proposal: investigationProposalInputSchema.extend({ id }).nullable(),
|
|
246
|
+
authorization: z.strictObject({
|
|
247
|
+
proposalId: id,
|
|
248
|
+
authorizedAt: z.string().datetime()
|
|
249
|
+
}).nullable(),
|
|
250
|
+
receipt: investigationReceiptInputSchema.extend({ reportedAt: z.string().datetime() }).nullable(),
|
|
251
|
+
verification: z.strictObject({
|
|
252
|
+
report: reportIdentitySchema,
|
|
253
|
+
summary: text,
|
|
254
|
+
after: measurementsSchema.nullable(),
|
|
255
|
+
verifiedAt: z.string().datetime()
|
|
256
|
+
}).nullable()
|
|
257
|
+
});
|
|
258
|
+
const investigationsStateSchema = z.strictObject({
|
|
259
|
+
version: z.number().int().nonnegative(),
|
|
260
|
+
items: z.array(investigationSchema).max(32)
|
|
261
|
+
});
|
|
262
|
+
const getInvestigationRequestSchema = z.strictObject({ id });
|
|
263
|
+
const createInvestigationRequestSchema = z.strictObject({
|
|
264
|
+
report: reportIdentitySchema,
|
|
265
|
+
target: investigationTargetSchema,
|
|
266
|
+
question: text
|
|
267
|
+
});
|
|
268
|
+
const investigationRequestSchema = z.strictObject({
|
|
269
|
+
id,
|
|
270
|
+
version
|
|
271
|
+
});
|
|
272
|
+
const claimInvestigationRequestSchema = investigationRequestSchema.extend({ agentName: z.string().trim().min(1).max(120) });
|
|
273
|
+
const investigationClaimSchema = z.strictObject({
|
|
274
|
+
investigation: investigationSchema,
|
|
275
|
+
claimToken: z.string().regex(/^[a-f0-9]{64}$/)
|
|
276
|
+
});
|
|
277
|
+
const agentInvestigationRequestSchema = investigationRequestSchema.extend({ claimToken: z.string().regex(/^[a-f0-9]{64}$/) });
|
|
278
|
+
const proposeInvestigationRequestSchema = agentInvestigationRequestSchema.extend({ proposal: investigationProposalInputSchema });
|
|
279
|
+
const authorizeInvestigationRequestSchema = investigationRequestSchema.extend({ proposalId: id });
|
|
280
|
+
const completeInvestigationRequestSchema = agentInvestigationRequestSchema.extend({ receipt: investigationReceiptInputSchema });
|
|
281
|
+
const verifyInvestigationRequestSchema = investigationRequestSchema.extend({
|
|
282
|
+
report: reportIdentitySchema,
|
|
283
|
+
summary: text
|
|
284
|
+
});
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/dashboard/schema.ts
|
|
287
|
+
const nonnegativeInteger = z.number().int().nonnegative();
|
|
288
|
+
const revisionSchema = nonnegativeInteger.describe("Revision from get-dashboard-state; refresh state if it becomes stale.");
|
|
289
|
+
const analyzeTargetSchema = z.enum(["current", "previous"]);
|
|
290
|
+
const analyzeDescriptorSchema = z.object({
|
|
291
|
+
characters: nonnegativeInteger,
|
|
292
|
+
hash: z.string(),
|
|
293
|
+
pages: z.number().int().positive()
|
|
294
|
+
});
|
|
295
|
+
const dashboardAnalyzePageRequestSchema = z.object({
|
|
296
|
+
index: nonnegativeInteger.describe("Zero-based page index, less than the selected descriptor pages count."),
|
|
297
|
+
revision: revisionSchema,
|
|
298
|
+
target: analyzeTargetSchema
|
|
299
|
+
});
|
|
300
|
+
const dashboardAnalyzePageSchema = z.object({
|
|
301
|
+
content: z.string(),
|
|
302
|
+
descriptor: analyzeDescriptorSchema,
|
|
303
|
+
index: nonnegativeInteger,
|
|
304
|
+
revision: revisionSchema,
|
|
305
|
+
target: analyzeTargetSchema
|
|
306
|
+
});
|
|
307
|
+
const dashboardFileRangeRequestSchema = z.object({
|
|
308
|
+
offset: nonnegativeInteger.describe("Zero-based UTF-16 code-unit offset; the content length is a valid EOF offset."),
|
|
309
|
+
limit: z.number().int().min(1).max(16384).describe("Maximum number of UTF-16 code units to return.")
|
|
310
|
+
});
|
|
311
|
+
const dashboardFileRangeSchema = z.object({
|
|
312
|
+
offset: nonnegativeInteger,
|
|
313
|
+
totalCharacters: nonnegativeInteger,
|
|
314
|
+
nextOffset: nonnegativeInteger.nullable()
|
|
315
|
+
});
|
|
316
|
+
const dashboardFileRequestSchema = z.object({
|
|
317
|
+
kind: z.enum(["source", "artifact"]),
|
|
318
|
+
path: z.string().describe("Report-listed relative path; source uses configured roots and artifact uses captured build content."),
|
|
319
|
+
range: dashboardFileRangeRequestSchema.optional()
|
|
320
|
+
});
|
|
321
|
+
const dashboardFileReadRequestSchema = dashboardFileRequestSchema.extend({ revision: revisionSchema });
|
|
322
|
+
const dashboardFileContentSchema = dashboardFileRequestSchema.extend({
|
|
323
|
+
content: z.string(),
|
|
324
|
+
language: z.string(),
|
|
325
|
+
size: nonnegativeInteger,
|
|
326
|
+
range: dashboardFileRangeSchema.optional()
|
|
327
|
+
});
|
|
328
|
+
const runtimeEventProfileSchema = z.object({
|
|
329
|
+
timestamp: z.string().optional(),
|
|
330
|
+
totalMs: z.number().optional(),
|
|
331
|
+
eventId: z.string().optional(),
|
|
332
|
+
event: z.string().optional(),
|
|
333
|
+
file: z.string().optional(),
|
|
334
|
+
relativeFile: z.string().optional(),
|
|
335
|
+
sourceRootFile: z.string().optional(),
|
|
336
|
+
buildCoreMs: z.number().optional(),
|
|
337
|
+
buildStartMs: z.number().optional(),
|
|
338
|
+
pluginResolveMs: z.number().optional(),
|
|
339
|
+
transformMs: z.number().optional(),
|
|
340
|
+
snapshotResolveMs: z.number().optional(),
|
|
341
|
+
snapshotBuildMs: z.number().optional(),
|
|
342
|
+
writeMs: z.number().optional(),
|
|
343
|
+
watchToDirtyMs: z.number().optional(),
|
|
344
|
+
emitMs: z.number().optional(),
|
|
345
|
+
sharedChunkResolveMs: z.number().optional(),
|
|
346
|
+
resolveCount: z.number().optional(),
|
|
347
|
+
dirtyCount: z.number().optional(),
|
|
348
|
+
pendingCount: z.number().optional(),
|
|
349
|
+
emittedCount: z.number().optional(),
|
|
350
|
+
dirtyReasonSummary: z.array(z.string()).optional(),
|
|
351
|
+
pendingReasonSummary: z.array(z.string()).optional()
|
|
352
|
+
});
|
|
353
|
+
const dashboardRuntimeEventSchema = z.object({
|
|
354
|
+
id: z.string(),
|
|
355
|
+
timestamp: z.string(),
|
|
356
|
+
occurredAt: z.string().datetime(),
|
|
357
|
+
source: z.string(),
|
|
358
|
+
kind: z.enum([
|
|
359
|
+
"command",
|
|
360
|
+
"build",
|
|
361
|
+
"diagnostic",
|
|
362
|
+
"hmr",
|
|
363
|
+
"system"
|
|
364
|
+
]),
|
|
365
|
+
level: z.enum([
|
|
366
|
+
"info",
|
|
367
|
+
"success",
|
|
368
|
+
"warning",
|
|
369
|
+
"error"
|
|
370
|
+
]),
|
|
371
|
+
title: z.string(),
|
|
372
|
+
detail: z.string(),
|
|
373
|
+
durationMs: z.number().optional(),
|
|
374
|
+
tags: z.array(z.string()).optional(),
|
|
375
|
+
profile: runtimeEventProfileSchema.optional()
|
|
376
|
+
});
|
|
377
|
+
const dashboardStateSchema = z.object({
|
|
378
|
+
sessionId: z.string().uuid(),
|
|
379
|
+
investigations: investigationsStateSchema,
|
|
380
|
+
analyze: z.object({
|
|
381
|
+
current: analyzeDescriptorSchema,
|
|
382
|
+
previous: analyzeDescriptorSchema.nullable()
|
|
383
|
+
}),
|
|
384
|
+
revision: revisionSchema,
|
|
385
|
+
runtimeEvents: z.array(dashboardRuntimeEventSchema)
|
|
386
|
+
});
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region src/dashboard/content.ts
|
|
389
|
+
function isPathWithin(rootPath, targetPath) {
|
|
390
|
+
const relativePath = path.relative(rootPath, targetPath);
|
|
391
|
+
return !relativePath || !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
|
|
392
|
+
}
|
|
393
|
+
function resolveInspectionRoot(resolved) {
|
|
394
|
+
let inspectionRoot = resolved.rootPath;
|
|
395
|
+
if (!resolved.allowParent) return inspectionRoot;
|
|
396
|
+
while (!isPathWithin(inspectionRoot, resolved.absolutePath)) {
|
|
397
|
+
const parent = path.dirname(inspectionRoot);
|
|
398
|
+
if (parent === inspectionRoot) return;
|
|
399
|
+
inspectionRoot = parent;
|
|
400
|
+
}
|
|
401
|
+
return inspectionRoot;
|
|
402
|
+
}
|
|
403
|
+
async function assertDashboardPathHasNoSymlink(resolved) {
|
|
404
|
+
const inspectionRoot = resolveInspectionRoot(resolved);
|
|
405
|
+
if (!inspectionRoot) throw new Error("文件路径超出允许范围。");
|
|
406
|
+
const relativePath = path.relative(inspectionRoot, resolved.absolutePath);
|
|
407
|
+
let currentPath = inspectionRoot;
|
|
408
|
+
for (const segment of relativePath.split(path.sep).filter(Boolean)) {
|
|
409
|
+
currentPath = path.join(currentPath, segment);
|
|
410
|
+
if ((await fs.promises.lstat(currentPath)).isSymbolicLink()) throw new Error("文件路径包含不允许的符号链接。");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function isSameOpenedFile(pathStat, handleStat) {
|
|
414
|
+
if (pathStat.dev !== handleStat.dev) return false;
|
|
415
|
+
return pathStat.ino === 0 || handleStat.ino === 0 || pathStat.ino === handleStat.ino;
|
|
416
|
+
}
|
|
417
|
+
async function openDashboardFile(resolved) {
|
|
418
|
+
await assertDashboardPathHasNoSymlink(resolved);
|
|
419
|
+
const flags = process.platform === "win32" ? fs.constants.O_RDONLY : fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW;
|
|
420
|
+
const handle = await fs.promises.open(resolved.absolutePath, flags);
|
|
421
|
+
try {
|
|
422
|
+
await assertDashboardPathHasNoSymlink(resolved);
|
|
423
|
+
const [pathStat, handleStat] = await Promise.all([fs.promises.stat(resolved.absolutePath), handle.stat()]);
|
|
424
|
+
if (!isSameOpenedFile(pathStat, handleStat)) throw new Error("文件路径在读取期间发生变化。");
|
|
425
|
+
return handle;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
await handle.close();
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function readDashboardFileHandle(handle) {
|
|
432
|
+
const stat = await handle.stat();
|
|
433
|
+
if (!stat.isFile()) throw new Error("目标路径不是文件。");
|
|
434
|
+
if (stat.size > 2097152) throw new Error(`文件超过 ${MAX_DASHBOARD_FILE_CONTENT_BYTES} 字节,已拒绝读取。`);
|
|
435
|
+
let content = Buffer.allocUnsafe(Math.min(Math.max(stat.size + 1, 1), 2097153));
|
|
436
|
+
let offset = 0;
|
|
437
|
+
while (true) {
|
|
438
|
+
if (offset === content.length) {
|
|
439
|
+
if (content.length === 2097153) throw new Error(`文件超过 ${MAX_DASHBOARD_FILE_CONTENT_BYTES} 字节,已拒绝读取。`);
|
|
440
|
+
const expanded = Buffer.allocUnsafe(Math.min(content.length * 2, 2097153));
|
|
441
|
+
content.copy(expanded, 0, 0, offset);
|
|
442
|
+
content = expanded;
|
|
443
|
+
}
|
|
444
|
+
const { bytesRead } = await handle.read(content, offset, content.length - offset, offset);
|
|
445
|
+
if (bytesRead === 0) break;
|
|
446
|
+
offset += bytesRead;
|
|
447
|
+
if (offset > 2097152) throw new Error(`文件超过 ${MAX_DASHBOARD_FILE_CONTENT_BYTES} 字节,已拒绝读取。`);
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
content: content.subarray(0, offset).toString("utf8"),
|
|
451
|
+
size: offset
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function resolveDashboardFileLanguage(filePath) {
|
|
455
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
456
|
+
if ([
|
|
457
|
+
".js",
|
|
458
|
+
".mjs",
|
|
459
|
+
".cjs",
|
|
460
|
+
".wxs",
|
|
461
|
+
".sjs"
|
|
462
|
+
].includes(extension)) return "javascript";
|
|
463
|
+
if ([
|
|
464
|
+
".ts",
|
|
465
|
+
".mts",
|
|
466
|
+
".cts"
|
|
467
|
+
].includes(extension)) return "typescript";
|
|
468
|
+
if ([".json", ".map"].includes(extension)) return "json";
|
|
469
|
+
if ([
|
|
470
|
+
".css",
|
|
471
|
+
".wxss",
|
|
472
|
+
".scss",
|
|
473
|
+
".sass",
|
|
474
|
+
".less"
|
|
475
|
+
].includes(extension)) return "css";
|
|
476
|
+
if ([
|
|
477
|
+
".vue",
|
|
478
|
+
".wxml",
|
|
479
|
+
".html"
|
|
480
|
+
].includes(extension)) return "html";
|
|
481
|
+
return "plaintext";
|
|
482
|
+
}
|
|
483
|
+
async function readAllowedDashboardFile(input, roots, allowlist, artifacts) {
|
|
484
|
+
const request = dashboardFileRequestSchema.parse(input);
|
|
485
|
+
if (request.kind === "artifact") {
|
|
486
|
+
const filePath = resolveDashboardArtifactPath(request.path, allowlist.artifactPaths);
|
|
487
|
+
if (!filePath) throw new Error("必须传入合法的 kind 和相对路径。");
|
|
488
|
+
const file = artifacts.get(filePath);
|
|
489
|
+
if (!file) throw new Error("分析快照中没有此产物内容。");
|
|
490
|
+
if (file.error || file.content === void 0) throw new Error(file.error ?? "分析快照中没有此产物内容。");
|
|
491
|
+
return {
|
|
492
|
+
kind: request.kind,
|
|
493
|
+
path: filePath,
|
|
494
|
+
language: resolveDashboardFileLanguage(filePath),
|
|
495
|
+
size: file.size,
|
|
496
|
+
...createDashboardFileExcerpt(file.content, request.range)
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const resolvedCandidates = resolveDashboardSourceContentPaths(roots, request.path, allowlist.sourcePaths);
|
|
500
|
+
if (resolvedCandidates.length === 0) throw new Error("必须传入合法的 kind 和相对路径。");
|
|
501
|
+
const openedCandidates = [];
|
|
502
|
+
try {
|
|
503
|
+
for (const resolved of resolvedCandidates) try {
|
|
504
|
+
openedCandidates.push({
|
|
505
|
+
handle: await openDashboardFile(resolved),
|
|
506
|
+
resolved
|
|
507
|
+
});
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if ((typeof error === "object" && error && "code" in error ? String(error.code) : "") !== "ENOENT") throw error;
|
|
510
|
+
}
|
|
511
|
+
if (openedCandidates.length === 0) throw new Error("文件不存在。");
|
|
512
|
+
if (openedCandidates.length > 1) throw new Error("源码路径存在多个候选文件,已拒绝读取。");
|
|
513
|
+
const [{ handle, resolved }] = openedCandidates;
|
|
514
|
+
const file = await readDashboardFileHandle(handle);
|
|
515
|
+
return {
|
|
516
|
+
kind: request.kind,
|
|
517
|
+
path: resolved.relativePath,
|
|
518
|
+
language: resolveDashboardFileLanguage(resolved.relativePath),
|
|
519
|
+
size: file.size,
|
|
520
|
+
...createDashboardFileExcerpt(file.content, request.range)
|
|
521
|
+
};
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (error instanceof Error && (error.message === "文件不存在。" || error instanceof RangeError || error.message === "源码路径存在多个候选文件,已拒绝读取。" || error.message === "目标路径不是文件。" || error.message.startsWith("文件超过 ") || error.message.startsWith("文件路径"))) throw error;
|
|
524
|
+
throw new Error("读取文件失败。");
|
|
525
|
+
} finally {
|
|
526
|
+
await Promise.allSettled(openedCandidates.map((candidate) => candidate.handle.close()));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function createDashboardFileReader(roots, result, artifacts) {
|
|
530
|
+
let allowlist = createDashboardContentAllowlist(result);
|
|
531
|
+
let currentArtifacts = artifacts;
|
|
532
|
+
let disposed = false;
|
|
533
|
+
return {
|
|
534
|
+
read: async (input) => {
|
|
535
|
+
if (disposed || !currentArtifacts) throw new Error("Dashboard 文件读取会话已关闭。");
|
|
536
|
+
return await readAllowedDashboardFile(input, roots, allowlist, currentArtifacts);
|
|
537
|
+
},
|
|
538
|
+
update(nextResult, nextArtifacts) {
|
|
539
|
+
if (disposed) return;
|
|
540
|
+
allowlist = createDashboardContentAllowlist(nextResult);
|
|
541
|
+
currentArtifacts = nextArtifacts;
|
|
542
|
+
},
|
|
543
|
+
dispose() {
|
|
544
|
+
disposed = true;
|
|
545
|
+
currentArtifacts = void 0;
|
|
546
|
+
allowlist.artifactPaths.clear();
|
|
547
|
+
allowlist.sourcePaths.clear();
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
const occurredAtFilterSchema = z.string().datetime({ offset: true }).describe("Inclusive ISO timestamp with Z or an explicit timezone offset.");
|
|
552
|
+
const dashboardRuntimeEventsQueryRequestSchema = z.object({
|
|
553
|
+
kind: dashboardRuntimeEventSchema.shape.kind.optional(),
|
|
554
|
+
level: dashboardRuntimeEventSchema.shape.level.optional(),
|
|
555
|
+
source: z.string().optional().describe("Exact event source, case-sensitive."),
|
|
556
|
+
query: z.string().optional().describe("Case-insensitive text in title, detail, kind, level, source or tags."),
|
|
557
|
+
since: occurredAtFilterSchema.optional(),
|
|
558
|
+
until: occurredAtFilterSchema.optional(),
|
|
559
|
+
limit: z.number().int().min(1).max(24).optional()
|
|
560
|
+
}).refine((input) => !input.since || !input.until || Date.parse(input.since) <= Date.parse(input.until), {
|
|
561
|
+
message: "事件查询的 since 不得晚于 until。",
|
|
562
|
+
path: ["until"]
|
|
563
|
+
});
|
|
564
|
+
const dashboardRuntimeEventsPageSchema = z.object({
|
|
565
|
+
items: z.array(dashboardRuntimeEventSchema).max(24),
|
|
566
|
+
total: z.number().int().nonnegative().max(24),
|
|
567
|
+
retention: z.object({
|
|
568
|
+
capacity: z.literal(24),
|
|
569
|
+
retained: z.number().int().nonnegative().max(24),
|
|
570
|
+
dropped: z.number().int().nonnegative(),
|
|
571
|
+
oldestOccurredAt: dashboardRuntimeEventSchema.shape.occurredAt.nullable()
|
|
572
|
+
})
|
|
573
|
+
});
|
|
574
|
+
function matchesEventText(event, query) {
|
|
575
|
+
return [
|
|
576
|
+
event.title,
|
|
577
|
+
event.detail,
|
|
578
|
+
event.kind,
|
|
579
|
+
event.level,
|
|
580
|
+
event.source,
|
|
581
|
+
...event.tags ?? []
|
|
582
|
+
].join(" ").toLowerCase().includes(query);
|
|
583
|
+
}
|
|
584
|
+
/** 查询当前保留的事件;匹配总数与丢弃计数不受返回条数影响。 */
|
|
585
|
+
function queryDashboardRuntimeEvents(events, dropped, input) {
|
|
586
|
+
const request = dashboardRuntimeEventsQueryRequestSchema.parse(input);
|
|
587
|
+
const since = request.since === void 0 ? void 0 : Date.parse(request.since);
|
|
588
|
+
const until = request.until === void 0 ? void 0 : Date.parse(request.until);
|
|
589
|
+
const query = request.query?.trim().toLowerCase();
|
|
590
|
+
const limit = request.limit ?? 24;
|
|
591
|
+
const items = [];
|
|
592
|
+
let total = 0;
|
|
593
|
+
let oldestOccurredAt = null;
|
|
594
|
+
for (const event of events) {
|
|
595
|
+
if (oldestOccurredAt === null || event.occurredAt < oldestOccurredAt) oldestOccurredAt = event.occurredAt;
|
|
596
|
+
if (request.kind !== void 0 && event.kind !== request.kind) continue;
|
|
597
|
+
if (request.level !== void 0 && event.level !== request.level) continue;
|
|
598
|
+
if (request.source !== void 0 && event.source !== request.source) continue;
|
|
599
|
+
if (since !== void 0 || until !== void 0) {
|
|
600
|
+
const occurredAt = Date.parse(event.occurredAt);
|
|
601
|
+
if (since !== void 0 && occurredAt < since || until !== void 0 && occurredAt > until) continue;
|
|
602
|
+
}
|
|
603
|
+
if (query && !matchesEventText(event, query)) continue;
|
|
604
|
+
total++;
|
|
605
|
+
if (items.length < limit) items.push(event);
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
items,
|
|
609
|
+
total,
|
|
610
|
+
retention: {
|
|
611
|
+
capacity: 24,
|
|
612
|
+
retained: events.length,
|
|
613
|
+
dropped,
|
|
614
|
+
oldestOccurredAt
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
//#endregion
|
|
619
|
+
//#region src/dashboard/events.ts
|
|
620
|
+
function createDashboardRuntimeEvent(input) {
|
|
621
|
+
const occurredAt = /* @__PURE__ */ new Date();
|
|
622
|
+
return {
|
|
623
|
+
id: `dashboard:${occurredAt.getTime()}:${Math.random().toString(36).slice(2, 8)}`,
|
|
624
|
+
kind: input.kind,
|
|
625
|
+
level: input.level,
|
|
626
|
+
title: input.title,
|
|
627
|
+
detail: input.detail,
|
|
628
|
+
timestamp: occurredAt.toLocaleTimeString("zh-CN", { hour12: false }),
|
|
629
|
+
occurredAt: occurredAt.toISOString(),
|
|
630
|
+
source: input.source ?? "weapp-vite",
|
|
631
|
+
durationMs: input.durationMs,
|
|
632
|
+
tags: input.tags,
|
|
633
|
+
profile: input.profile
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
/** 在当前宿主会话内保留有限事件,不伪造已丢弃的历史。 */
|
|
637
|
+
function createDashboardRuntimeEventStore(initialInputs) {
|
|
638
|
+
let events = [];
|
|
639
|
+
let dropped = 0;
|
|
640
|
+
let disposed = false;
|
|
641
|
+
function assertActive() {
|
|
642
|
+
if (disposed) throw new Error("Dashboard 事件会话已关闭。");
|
|
643
|
+
}
|
|
644
|
+
function prepend(inputs) {
|
|
645
|
+
if (disposed || inputs.length === 0) return;
|
|
646
|
+
dropped += Math.max(0, events.length + inputs.length - 24);
|
|
647
|
+
const next = [];
|
|
648
|
+
for (let index = 0; index < Math.min(inputs.length, 24); index++) next.push(createDashboardRuntimeEvent(inputs[index]));
|
|
649
|
+
for (let index = 0; index < events.length && next.length < 24; index++) next.push(events[index]);
|
|
650
|
+
events = next;
|
|
651
|
+
}
|
|
652
|
+
prepend(initialInputs);
|
|
653
|
+
return {
|
|
654
|
+
read() {
|
|
655
|
+
assertActive();
|
|
656
|
+
return [...events];
|
|
657
|
+
},
|
|
658
|
+
prepend,
|
|
659
|
+
query(input) {
|
|
660
|
+
assertActive();
|
|
661
|
+
return queryDashboardRuntimeEvents(events, dropped, input);
|
|
662
|
+
},
|
|
663
|
+
dispose() {
|
|
664
|
+
disposed = true;
|
|
665
|
+
events = [];
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/dashboard/investigations/rpc.ts
|
|
671
|
+
/** 浏览器所有者操作刻意不声明 agent,由 DevFrame 的默认拒绝边界隔离。 */
|
|
672
|
+
function createDashboardInvestigationRpc(store) {
|
|
673
|
+
return [
|
|
674
|
+
defineRpcFunction({
|
|
675
|
+
name: "list-investigations",
|
|
676
|
+
type: "query",
|
|
677
|
+
jsonSerializable: true,
|
|
678
|
+
args: [],
|
|
679
|
+
returns: investigationsStateSchema,
|
|
680
|
+
agent: {
|
|
681
|
+
title: "List object investigations",
|
|
682
|
+
safety: "read",
|
|
683
|
+
description: "Read up to 32 session-local investigation records. Tasks pin session, revision, report hash and exact report placement. Agent names and receipts are self-reported, not authentication or independent verification."
|
|
684
|
+
},
|
|
685
|
+
handler: store.list
|
|
686
|
+
}),
|
|
687
|
+
defineRpcFunction({
|
|
688
|
+
name: "get-investigation",
|
|
689
|
+
type: "query",
|
|
690
|
+
jsonSerializable: true,
|
|
691
|
+
args: [getInvestigationRequestSchema],
|
|
692
|
+
returns: investigationSchema,
|
|
693
|
+
agent: {
|
|
694
|
+
title: "Read object investigation",
|
|
695
|
+
safety: "read",
|
|
696
|
+
description: "Read a task, its current CAS version, exact proposal and browser authorization. Private claim tokens are never present in task state. Re-read after a version conflict; do not replay actions."
|
|
697
|
+
},
|
|
698
|
+
handler: store.get
|
|
699
|
+
}),
|
|
700
|
+
defineRpcFunction({
|
|
701
|
+
name: "create-investigation",
|
|
702
|
+
type: "action",
|
|
703
|
+
jsonSerializable: true,
|
|
704
|
+
args: [createInvestigationRequestSchema],
|
|
705
|
+
returns: investigationSchema,
|
|
706
|
+
handler: store.create
|
|
707
|
+
}),
|
|
708
|
+
defineRpcFunction({
|
|
709
|
+
name: "cancel-investigation",
|
|
710
|
+
type: "action",
|
|
711
|
+
jsonSerializable: true,
|
|
712
|
+
args: [investigationRequestSchema],
|
|
713
|
+
returns: investigationSchema,
|
|
714
|
+
handler: store.cancel
|
|
715
|
+
}),
|
|
716
|
+
defineRpcFunction({
|
|
717
|
+
name: "authorize-investigation",
|
|
718
|
+
type: "action",
|
|
719
|
+
jsonSerializable: true,
|
|
720
|
+
args: [authorizeInvestigationRequestSchema],
|
|
721
|
+
returns: investigationSchema,
|
|
722
|
+
handler: store.authorize
|
|
723
|
+
}),
|
|
724
|
+
defineRpcFunction({
|
|
725
|
+
name: "verify-investigation",
|
|
726
|
+
type: "action",
|
|
727
|
+
jsonSerializable: true,
|
|
728
|
+
args: [verifyInvestigationRequestSchema],
|
|
729
|
+
returns: investigationSchema,
|
|
730
|
+
handler: store.verify
|
|
731
|
+
}),
|
|
732
|
+
defineRpcFunction({
|
|
733
|
+
name: "claim-investigation",
|
|
734
|
+
type: "action",
|
|
735
|
+
jsonSerializable: true,
|
|
736
|
+
args: [claimInvestigationRequestSchema],
|
|
737
|
+
returns: investigationClaimSchema,
|
|
738
|
+
agent: {
|
|
739
|
+
title: "Claim object investigation",
|
|
740
|
+
safety: "action",
|
|
741
|
+
description: "Claim a submitted task using its current id/version and a self-reported agentName. Only the winning claim returns a private claimToken; keep it private. This changes task metadata only, not source files or execution permissions."
|
|
742
|
+
},
|
|
743
|
+
handler: store.claim
|
|
744
|
+
}),
|
|
745
|
+
defineRpcFunction({
|
|
746
|
+
name: "propose-investigation",
|
|
747
|
+
type: "action",
|
|
748
|
+
jsonSerializable: true,
|
|
749
|
+
args: [proposeInvestigationRequestSchema],
|
|
750
|
+
returns: investigationSchema,
|
|
751
|
+
agent: {
|
|
752
|
+
title: "Propose investigation changes",
|
|
753
|
+
safety: "action",
|
|
754
|
+
description: "Submit exact proposed paths, descriptions, checks and risks using id/version/claimToken. Proposals may be replaced before browser authorization only. Report hashes identify reports, not current source contents; obtain external OS/workspace permission separately."
|
|
755
|
+
},
|
|
756
|
+
handler: store.propose
|
|
757
|
+
}),
|
|
758
|
+
defineRpcFunction({
|
|
759
|
+
name: "start-investigation",
|
|
760
|
+
type: "action",
|
|
761
|
+
jsonSerializable: true,
|
|
762
|
+
args: [agentInvestigationRequestSchema],
|
|
763
|
+
returns: investigationSchema,
|
|
764
|
+
agent: {
|
|
765
|
+
title: "Start authorized investigation",
|
|
766
|
+
safety: "action",
|
|
767
|
+
description: "Consume the current exact browser-authorized proposal once with id/version/claimToken. Rejects stale reports and replay. Only records execution intent: no commands or filesystem writes run here; actual work and OS permissions remain external."
|
|
768
|
+
},
|
|
769
|
+
handler: store.start
|
|
770
|
+
}),
|
|
771
|
+
defineRpcFunction({
|
|
772
|
+
name: "complete-investigation",
|
|
773
|
+
type: "action",
|
|
774
|
+
jsonSerializable: true,
|
|
775
|
+
args: [completeInvestigationRequestSchema],
|
|
776
|
+
returns: investigationSchema,
|
|
777
|
+
agent: {
|
|
778
|
+
title: "Report external investigation result",
|
|
779
|
+
safety: "action",
|
|
780
|
+
description: "Report external completion or failure with changedFiles and actual check outcomes using id/version/claimToken. Rebuilds during execution are allowed; cancelled tasks reject receipts. A receipt is self-reported and never marks human verification complete."
|
|
781
|
+
},
|
|
782
|
+
handler: store.complete
|
|
783
|
+
})
|
|
784
|
+
];
|
|
785
|
+
}
|
|
786
|
+
//#endregion
|
|
787
|
+
//#region src/dashboard/investigations/target.ts
|
|
788
|
+
/** 不把缺失测量计为零,也不混合原始、压缩与模块归因体积。 */
|
|
789
|
+
function sumMeasured(files, key) {
|
|
790
|
+
let total = 0;
|
|
791
|
+
for (const file of files) {
|
|
792
|
+
if (file[key] === void 0) return null;
|
|
793
|
+
total += file[key];
|
|
794
|
+
}
|
|
795
|
+
return total;
|
|
796
|
+
}
|
|
797
|
+
/** 只解析报告中真实存在的包、产物及精确模块放置关系,不访问文件系统。 */
|
|
798
|
+
function resolveInvestigationTarget(result, target) {
|
|
799
|
+
const pkg = result.packages.find((pkg) => pkg.id === target.packageId);
|
|
800
|
+
if (!pkg) return null;
|
|
801
|
+
if (target.kind === "package") return {
|
|
802
|
+
label: pkg.label,
|
|
803
|
+
rawBytes: sumMeasured(pkg.files, "size"),
|
|
804
|
+
gzipBytes: sumMeasured(pkg.files, "gzipSize"),
|
|
805
|
+
brotliBytes: sumMeasured(pkg.files, "brotliSize"),
|
|
806
|
+
attributedBytes: null,
|
|
807
|
+
sourceBytes: null
|
|
808
|
+
};
|
|
809
|
+
const file = pkg.files.find((file) => file.file === target.file);
|
|
810
|
+
if (!file) return null;
|
|
811
|
+
if (target.kind === "artifact") return {
|
|
812
|
+
label: file.file,
|
|
813
|
+
rawBytes: file.size ?? null,
|
|
814
|
+
gzipBytes: file.gzipSize ?? null,
|
|
815
|
+
brotliBytes: file.brotliSize ?? null,
|
|
816
|
+
attributedBytes: null,
|
|
817
|
+
sourceBytes: null
|
|
818
|
+
};
|
|
819
|
+
const occurrence = file.modules?.find((module) => module.id === target.moduleId);
|
|
820
|
+
const placement = occurrence ? void 0 : result.modules.find((module) => module.id === target.moduleId && module.packages.some((placement) => placement.packageId === pkg.id && placement.files.includes(file.file)));
|
|
821
|
+
if (!occurrence && !placement) return null;
|
|
822
|
+
return {
|
|
823
|
+
label: occurrence?.source ?? placement.source,
|
|
824
|
+
rawBytes: null,
|
|
825
|
+
gzipBytes: null,
|
|
826
|
+
brotliBytes: null,
|
|
827
|
+
attributedBytes: occurrence?.bytes ?? null,
|
|
828
|
+
sourceBytes: occurrence?.originalBytes ?? null
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
//#endregion
|
|
832
|
+
//#region src/dashboard/investigations/store.ts
|
|
833
|
+
const terminal = {
|
|
834
|
+
completed: true,
|
|
835
|
+
failed: true,
|
|
836
|
+
cancelled: true,
|
|
837
|
+
stale: true,
|
|
838
|
+
verified: true
|
|
839
|
+
};
|
|
840
|
+
/** 会话内唯一任务所有者;同步完成校验与 CAS 提交,再通知传输层。 */
|
|
841
|
+
function createDashboardInvestigationStore(readReport, onChange) {
|
|
842
|
+
const entries = /* @__PURE__ */ new Map();
|
|
843
|
+
let version = 0;
|
|
844
|
+
function requireCurrent(identity) {
|
|
845
|
+
const current = readReport();
|
|
846
|
+
if (identity.sessionId !== current.identity.sessionId || identity.revision !== current.identity.revision || identity.reportHash !== current.identity.reportHash) throw new Error("Investigation report is stale; refresh the Dashboard report.");
|
|
847
|
+
return current.result;
|
|
848
|
+
}
|
|
849
|
+
function getEntry(id) {
|
|
850
|
+
readReport();
|
|
851
|
+
const entry = entries.get(id);
|
|
852
|
+
if (!entry) throw new Error("Investigation does not exist in this Dashboard session.");
|
|
853
|
+
return entry;
|
|
854
|
+
}
|
|
855
|
+
function requireVersion(input, statuses) {
|
|
856
|
+
const entry = getEntry(input.id);
|
|
857
|
+
if (entry.task.version !== input.version) throw new Error("Investigation version conflict; refresh the task before retrying.");
|
|
858
|
+
if (!statuses.includes(entry.task.status)) throw new Error(`Investigation cannot transition from ${entry.task.status}.`);
|
|
859
|
+
return entry;
|
|
860
|
+
}
|
|
861
|
+
function requireAgent(input, statuses) {
|
|
862
|
+
const entry = requireVersion(input, statuses);
|
|
863
|
+
if (!entry.claimToken || entry.claimToken !== input.claimToken) throw new Error("Invalid investigation claim token.");
|
|
864
|
+
return entry;
|
|
865
|
+
}
|
|
866
|
+
function commit(entry, status) {
|
|
867
|
+
entry.task.status = status;
|
|
868
|
+
entry.task.version += 1;
|
|
869
|
+
entry.task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
870
|
+
version += 1;
|
|
871
|
+
if (terminal[status]) delete entry.claimToken;
|
|
872
|
+
onChange();
|
|
873
|
+
return structuredClone(entry.task);
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
list() {
|
|
877
|
+
readReport();
|
|
878
|
+
return {
|
|
879
|
+
version,
|
|
880
|
+
items: Array.from(entries.values(), (entry) => structuredClone(entry.task))
|
|
881
|
+
};
|
|
882
|
+
},
|
|
883
|
+
get(input) {
|
|
884
|
+
const { id } = getInvestigationRequestSchema.parse(input);
|
|
885
|
+
return structuredClone(getEntry(id).task);
|
|
886
|
+
},
|
|
887
|
+
create(input) {
|
|
888
|
+
const request = createInvestigationRequestSchema.parse(input);
|
|
889
|
+
const evidence = resolveInvestigationTarget(requireCurrent(request.report), request.target);
|
|
890
|
+
if (!evidence) throw new Error("Investigation target is not present in the current report placement.");
|
|
891
|
+
if (entries.size === 32) {
|
|
892
|
+
const oldestTerminal = Array.from(entries.values()).find((entry) => terminal[entry.task.status]);
|
|
893
|
+
if (!oldestTerminal) throw new Error("Dashboard already has 32 active investigations; cancel one before submitting another.");
|
|
894
|
+
entries.delete(oldestTerminal.task.id);
|
|
895
|
+
}
|
|
896
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
897
|
+
const task = {
|
|
898
|
+
id: randomUUID(),
|
|
899
|
+
version: 1,
|
|
900
|
+
createdAt: now,
|
|
901
|
+
updatedAt: now,
|
|
902
|
+
report: request.report,
|
|
903
|
+
target: request.target,
|
|
904
|
+
question: request.question,
|
|
905
|
+
evidence,
|
|
906
|
+
status: "submitted",
|
|
907
|
+
agent: null,
|
|
908
|
+
proposal: null,
|
|
909
|
+
authorization: null,
|
|
910
|
+
receipt: null,
|
|
911
|
+
verification: null
|
|
912
|
+
};
|
|
913
|
+
entries.set(task.id, { task });
|
|
914
|
+
version += 1;
|
|
915
|
+
onChange();
|
|
916
|
+
return structuredClone(task);
|
|
917
|
+
},
|
|
918
|
+
cancel(input) {
|
|
919
|
+
const entry = requireVersion(investigationRequestSchema.parse(input), [
|
|
920
|
+
"submitted",
|
|
921
|
+
"claimed",
|
|
922
|
+
"proposed",
|
|
923
|
+
"authorized",
|
|
924
|
+
"executing"
|
|
925
|
+
]);
|
|
926
|
+
entry.task.authorization = null;
|
|
927
|
+
return commit(entry, "cancelled");
|
|
928
|
+
},
|
|
929
|
+
claim(input) {
|
|
930
|
+
const request = claimInvestigationRequestSchema.parse(input);
|
|
931
|
+
const entry = requireVersion(request, ["submitted"]);
|
|
932
|
+
requireCurrent(entry.task.report);
|
|
933
|
+
const claimToken = randomBytes(32).toString("hex");
|
|
934
|
+
entry.claimToken = claimToken;
|
|
935
|
+
entry.task.agent = {
|
|
936
|
+
name: request.agentName,
|
|
937
|
+
claimedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
938
|
+
};
|
|
939
|
+
return {
|
|
940
|
+
investigation: commit(entry, "claimed"),
|
|
941
|
+
claimToken
|
|
942
|
+
};
|
|
943
|
+
},
|
|
944
|
+
propose(input) {
|
|
945
|
+
const request = proposeInvestigationRequestSchema.parse(input);
|
|
946
|
+
const entry = requireAgent(request, ["claimed", "proposed"]);
|
|
947
|
+
requireCurrent(entry.task.report);
|
|
948
|
+
entry.task.proposal = {
|
|
949
|
+
...request.proposal,
|
|
950
|
+
id: randomUUID()
|
|
951
|
+
};
|
|
952
|
+
return commit(entry, "proposed");
|
|
953
|
+
},
|
|
954
|
+
authorize(input) {
|
|
955
|
+
const request = authorizeInvestigationRequestSchema.parse(input);
|
|
956
|
+
const entry = requireVersion(request, ["proposed"]);
|
|
957
|
+
requireCurrent(entry.task.report);
|
|
958
|
+
if (entry.task.proposal?.id !== request.proposalId) throw new Error("Investigation proposal changed; review the exact current proposal.");
|
|
959
|
+
entry.task.authorization = {
|
|
960
|
+
proposalId: request.proposalId,
|
|
961
|
+
authorizedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
962
|
+
};
|
|
963
|
+
return commit(entry, "authorized");
|
|
964
|
+
},
|
|
965
|
+
start(input) {
|
|
966
|
+
const entry = requireAgent(agentInvestigationRequestSchema.parse(input), ["authorized"]);
|
|
967
|
+
requireCurrent(entry.task.report);
|
|
968
|
+
if (!entry.task.proposal || entry.task.authorization?.proposalId !== entry.task.proposal.id) throw new Error("The exact investigation proposal has not been authorized.");
|
|
969
|
+
return commit(entry, "executing");
|
|
970
|
+
},
|
|
971
|
+
complete(input) {
|
|
972
|
+
const request = completeInvestigationRequestSchema.parse(input);
|
|
973
|
+
const entry = requireAgent(request, ["executing"]);
|
|
974
|
+
entry.task.receipt = {
|
|
975
|
+
...request.receipt,
|
|
976
|
+
reportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
977
|
+
};
|
|
978
|
+
return commit(entry, request.receipt.outcome);
|
|
979
|
+
},
|
|
980
|
+
verify(input) {
|
|
981
|
+
const request = verifyInvestigationRequestSchema.parse(input);
|
|
982
|
+
const entry = requireVersion(request, ["completed"]);
|
|
983
|
+
const result = requireCurrent(request.report);
|
|
984
|
+
if (request.report.revision <= entry.task.report.revision) throw new Error("Human verification requires a newer report than the investigation baseline.");
|
|
985
|
+
entry.task.verification = {
|
|
986
|
+
report: request.report,
|
|
987
|
+
summary: request.summary,
|
|
988
|
+
after: resolveInvestigationTarget(result, entry.task.target),
|
|
989
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
990
|
+
};
|
|
991
|
+
return commit(entry, "verified");
|
|
992
|
+
},
|
|
993
|
+
reportUpdated() {
|
|
994
|
+
for (const entry of entries.values()) if ([
|
|
995
|
+
"submitted",
|
|
996
|
+
"claimed",
|
|
997
|
+
"proposed",
|
|
998
|
+
"authorized"
|
|
999
|
+
].includes(entry.task.status)) {
|
|
1000
|
+
entry.task.status = "stale";
|
|
1001
|
+
entry.task.version += 1;
|
|
1002
|
+
entry.task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1003
|
+
entry.task.authorization = null;
|
|
1004
|
+
delete entry.claimToken;
|
|
1005
|
+
version += 1;
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
dispose() {
|
|
1009
|
+
entries.clear();
|
|
1010
|
+
version = 0;
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
const dashboardLogo = `data:image/svg+xml,${encodeURIComponent(`<svg width="36" height="35" viewBox="0 0 36 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1015
|
+
<path d="M35.0904 5.15683L18.9345 33.6611C18.6009 34.2496 17.744 34.253 17.4054 33.6674L0.929122 5.15958C0.560269 4.52138 1.11338 3.74834 1.84637 3.8776L18.0196 6.72991C18.1227 6.7481 18.2284 6.74793 18.3315 6.72938L34.1665 3.88169C34.8971 3.75032 35.4528 4.51737 35.0904 5.15683Z" fill="url(#paint0_linear_3_14)"/>
|
|
1016
|
+
<path d="M27.852 15.3948L18.0994 29.6346C17.996 29.7856 17.8465 29.8987 17.6735 29.9568C17.5005 30.0148 17.3135 30.0143 17.1409 29.956C16.9682 29.8974 16.8194 29.7837 16.7169 29.6325C16.6144 29.4809 16.5639 29.3 16.573 29.1171L16.7169 22.5L10.6621 19.9057C10.5274 19.8769 10.402 19.8151 10.2968 19.7259C10.1916 19.6367 10.11 19.5227 10.0592 19.3941C10.0083 19.2655 9.98983 19.1262 10.0053 18.9886C10.0208 18.851 10.0698 18.7193 10.1479 18.6052L19.9006 4.36535C20.004 4.21429 20.1536 4.10122 20.3266 4.04325C20.4996 3.98528 20.6866 3.98558 20.8592 4.04415C21.0319 4.10269 21.1807 4.21628 21.2832 4.3677C21.3857 4.51909 21.4362 4.70002 21.427 4.88305V12L27.3379 14.0944C27.4725 14.1232 27.598 14.185 27.7033 14.2742C27.8085 14.3635 27.89 14.4775 27.9408 14.6061C27.9918 14.7347 28.0101 14.8739 27.9947 15.0115C27.9793 15.1491 27.9302 15.2807 27.852 15.3948Z" fill="#FACC15"/>
|
|
1017
|
+
<path d="M20.5083 13.0001C21.8824 13.0001 23 13.9633 23 15.1542C23 15.5243 22.8853 15.8865 22.6783 16.2129C22.365 16.6897 21.8638 17.0427 21.2575 17.2107C21.1289 17.2489 20.9952 17.2696 20.8604 17.272C20.7986 17.2731 20.7373 17.2625 20.68 17.2408C20.6226 17.2191 20.5704 17.1867 20.5264 17.1457C20.4824 17.1046 20.4475 17.0556 20.4237 17.0016C20.3999 16.9475 20.3878 16.8895 20.3879 16.831C20.3879 16.583 20.596 16.3905 20.8604 16.3905C20.8789 16.3905 20.9178 16.3905 20.965 16.3734C21.3722 16.2667 21.694 16.047 21.8739 15.7547C21.9966 15.5702 22.0539 15.3654 22.0539 15.1542C22.0539 14.4555 21.3621 13.8833 20.5184 13.8833C20.2274 13.8837 19.9412 13.9534 19.686 14.086C19.2394 14.3238 18.975 14.7206 18.975 15.1542V18.8534C18.975 19.6032 18.529 20.2923 17.7899 20.6811C17.3928 20.8928 16.9462 21 16.4917 21C15.1182 21 14 20.0363 14 18.8454C14 18.4758 14.1153 18.1131 14.3217 17.7867C14.635 17.3099 15.1367 16.9569 15.7425 16.7894C15.9124 16.7451 16.0271 16.7281 16.1396 16.7281C16.4057 16.7281 16.6126 16.9227 16.6126 17.1686C16.6128 17.2271 16.6006 17.2851 16.5769 17.3392C16.5531 17.3932 16.5182 17.4422 16.4742 17.4833C16.4302 17.5243 16.378 17.5567 16.3206 17.5784C16.2633 17.6001 16.2019 17.6107 16.1402 17.6096C16.1211 17.6096 16.0822 17.6096 16.0356 17.6267C15.6278 17.7414 15.3066 17.9627 15.1266 18.2448C15.0034 18.4299 14.9466 18.6342 14.9466 18.8459C14.9466 19.5446 15.6385 20.1163 16.49 20.1163C16.7812 20.1161 17.0676 20.0466 17.323 19.9141C17.7691 19.6757 18.0334 19.279 18.0334 18.8459V15.1558C18.0334 14.4059 18.4795 13.7174 19.2185 13.3281C19.609 13.1095 20.0549 12.9961 20.5083 13.0001Z" fill="white"/>
|
|
1018
|
+
<defs>
|
|
1019
|
+
<linearGradient id="paint0_linear_3_14" x1="0.526842" y1="2.8589" x2="20.2854" y2="30.0554" gradientUnits="userSpaceOnUse">
|
|
1020
|
+
<stop stop-color="#95EC69"/>
|
|
1021
|
+
<stop offset="1" stop-color="#2BA245"/>
|
|
1022
|
+
</linearGradient>
|
|
1023
|
+
</defs>
|
|
1024
|
+
</svg>`)}`;
|
|
1025
|
+
//#endregion
|
|
1026
|
+
//#region src/dashboard/payload.ts
|
|
1027
|
+
const MAX_DASHBOARD_ANALYZE_PAGE_CHARACTERS = 65536;
|
|
1028
|
+
const STALE_DASHBOARD_ANALYZE_REVISION_MESSAGE = "Analyze revision 已变化,请重新获取 Dashboard 状态。";
|
|
1029
|
+
function serializeDashboardAnalyzePayload(result) {
|
|
1030
|
+
const value = JSON.stringify(result);
|
|
1031
|
+
return {
|
|
1032
|
+
descriptor: {
|
|
1033
|
+
characters: value.length,
|
|
1034
|
+
hash: createHash("sha256").update(value).digest("hex"),
|
|
1035
|
+
pages: Math.max(1, Math.ceil(value.length / MAX_DASHBOARD_ANALYZE_PAGE_CHARACTERS))
|
|
1036
|
+
},
|
|
1037
|
+
source: result,
|
|
1038
|
+
value
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function serializeDashboardAnalyzeSnapshot(snapshot, cached) {
|
|
1042
|
+
const previous = snapshot.previous ? cached?.current.source === snapshot.previous ? cached.current : cached?.previous?.source === snapshot.previous ? cached.previous : serializeDashboardAnalyzePayload(snapshot.previous) : null;
|
|
1043
|
+
return {
|
|
1044
|
+
current: cached?.current.source === snapshot.current ? cached.current : serializeDashboardAnalyzePayload(snapshot.current),
|
|
1045
|
+
previous
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function readDashboardAnalyzePage(input, revision, snapshot) {
|
|
1049
|
+
const request = dashboardAnalyzePageRequestSchema.parse(input);
|
|
1050
|
+
if (request.revision !== revision) throw new Error(STALE_DASHBOARD_ANALYZE_REVISION_MESSAGE);
|
|
1051
|
+
const payload = request.target === "current" ? snapshot.current : snapshot.previous;
|
|
1052
|
+
if (!payload || request.index >= payload.descriptor.pages) throw new Error("Analyze 分页不存在。");
|
|
1053
|
+
const start = request.index * MAX_DASHBOARD_ANALYZE_PAGE_CHARACTERS;
|
|
1054
|
+
return {
|
|
1055
|
+
content: payload.value.slice(start, start + MAX_DASHBOARD_ANALYZE_PAGE_CHARACTERS),
|
|
1056
|
+
descriptor: payload.descriptor,
|
|
1057
|
+
index: request.index,
|
|
1058
|
+
revision,
|
|
1059
|
+
target: request.target
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
//#endregion
|
|
1063
|
+
//#region src/dashboard/queries/schema.ts
|
|
1064
|
+
const count = z.number().int().nonnegative();
|
|
1065
|
+
const packageType = z.enum([
|
|
1066
|
+
"main",
|
|
1067
|
+
"subPackage",
|
|
1068
|
+
"independent",
|
|
1069
|
+
"virtual"
|
|
1070
|
+
]);
|
|
1071
|
+
const sourceType = z.enum([
|
|
1072
|
+
"src",
|
|
1073
|
+
"plugin",
|
|
1074
|
+
"node_modules",
|
|
1075
|
+
"workspace"
|
|
1076
|
+
]);
|
|
1077
|
+
const change = z.enum([
|
|
1078
|
+
"added",
|
|
1079
|
+
"removed",
|
|
1080
|
+
"increased",
|
|
1081
|
+
"decreased",
|
|
1082
|
+
"unmeasured"
|
|
1083
|
+
]);
|
|
1084
|
+
const query = z.string().trim().max(200).optional();
|
|
1085
|
+
const revision = count.describe("Revision from get-dashboard-state; refresh state when stale.");
|
|
1086
|
+
const target = z.enum(["current", "previous"]);
|
|
1087
|
+
const pagination = {
|
|
1088
|
+
offset: count.default(0),
|
|
1089
|
+
limit: z.number().int().min(1).max(100).default(20)
|
|
1090
|
+
};
|
|
1091
|
+
const pageResult = {
|
|
1092
|
+
total: count,
|
|
1093
|
+
offset: count,
|
|
1094
|
+
nextOffset: count.nullable()
|
|
1095
|
+
};
|
|
1096
|
+
const reportResult = {
|
|
1097
|
+
revision,
|
|
1098
|
+
target,
|
|
1099
|
+
reportHash: z.string()
|
|
1100
|
+
};
|
|
1101
|
+
const order = z.enum(["asc", "desc"]).default("desc");
|
|
1102
|
+
const analyzeQuerySchema = z.object({
|
|
1103
|
+
revision,
|
|
1104
|
+
target: target.default("current")
|
|
1105
|
+
});
|
|
1106
|
+
const budgetCheckSchema = z.object({
|
|
1107
|
+
id: z.string(),
|
|
1108
|
+
label: z.string(),
|
|
1109
|
+
scope: z.enum([
|
|
1110
|
+
"total",
|
|
1111
|
+
"runtime",
|
|
1112
|
+
"main",
|
|
1113
|
+
"subPackage",
|
|
1114
|
+
"independent",
|
|
1115
|
+
"virtual"
|
|
1116
|
+
]),
|
|
1117
|
+
currentBytes: count,
|
|
1118
|
+
limitBytes: count,
|
|
1119
|
+
ratio: z.number(),
|
|
1120
|
+
status: z.enum([
|
|
1121
|
+
"ok",
|
|
1122
|
+
"warning",
|
|
1123
|
+
"exceeded",
|
|
1124
|
+
"unknown"
|
|
1125
|
+
]),
|
|
1126
|
+
measurement: z.enum([
|
|
1127
|
+
"file-bytes",
|
|
1128
|
+
"upper-bound",
|
|
1129
|
+
"unavailable"
|
|
1130
|
+
])
|
|
1131
|
+
});
|
|
1132
|
+
const analyzeSummarySchema = z.object({
|
|
1133
|
+
...reportResult,
|
|
1134
|
+
previousAvailable: z.boolean(),
|
|
1135
|
+
totals: z.object({
|
|
1136
|
+
packages: count,
|
|
1137
|
+
files: count,
|
|
1138
|
+
modules: count,
|
|
1139
|
+
bytes: count,
|
|
1140
|
+
unmeasuredFiles: count
|
|
1141
|
+
}),
|
|
1142
|
+
totalBudget: budgetCheckSchema.nullable(),
|
|
1143
|
+
runtimeBudget: budgetCheckSchema.nullable(),
|
|
1144
|
+
packageBudgets: z.object({
|
|
1145
|
+
ok: count,
|
|
1146
|
+
warning: count,
|
|
1147
|
+
exceeded: count,
|
|
1148
|
+
unknown: count
|
|
1149
|
+
})
|
|
1150
|
+
});
|
|
1151
|
+
const packagesQuerySchema = analyzeQuerySchema.extend({
|
|
1152
|
+
...pagination,
|
|
1153
|
+
type: packageType.optional(),
|
|
1154
|
+
query,
|
|
1155
|
+
budgetStatus: z.enum([
|
|
1156
|
+
"ok",
|
|
1157
|
+
"warning",
|
|
1158
|
+
"exceeded",
|
|
1159
|
+
"unknown"
|
|
1160
|
+
]).optional(),
|
|
1161
|
+
sortBy: z.enum([
|
|
1162
|
+
"bytes",
|
|
1163
|
+
"name",
|
|
1164
|
+
"budgetRatio"
|
|
1165
|
+
]).default("bytes"),
|
|
1166
|
+
order
|
|
1167
|
+
});
|
|
1168
|
+
const packagesPageSchema = z.object({
|
|
1169
|
+
...reportResult,
|
|
1170
|
+
...pageResult,
|
|
1171
|
+
items: z.array(z.object({
|
|
1172
|
+
id: z.string(),
|
|
1173
|
+
label: z.string(),
|
|
1174
|
+
type: packageType,
|
|
1175
|
+
bytes: count,
|
|
1176
|
+
fileCount: count,
|
|
1177
|
+
moduleCount: count,
|
|
1178
|
+
unmeasuredFiles: count,
|
|
1179
|
+
budget: budgetCheckSchema.nullable()
|
|
1180
|
+
}))
|
|
1181
|
+
});
|
|
1182
|
+
const artifactsQuerySchema = analyzeQuerySchema.extend({
|
|
1183
|
+
...pagination,
|
|
1184
|
+
packageId: z.string().optional(),
|
|
1185
|
+
moduleId: z.string().optional().describe("Exact module id from query-analyze-modules; returns its artifact placements."),
|
|
1186
|
+
type: z.enum(["chunk", "asset"]).optional(),
|
|
1187
|
+
query,
|
|
1188
|
+
sortBy: z.enum(["size", "path"]).default("size"),
|
|
1189
|
+
order
|
|
1190
|
+
});
|
|
1191
|
+
const artifactsPageSchema = z.object({
|
|
1192
|
+
...reportResult,
|
|
1193
|
+
...pageResult,
|
|
1194
|
+
items: z.array(z.object({
|
|
1195
|
+
packageId: z.string(),
|
|
1196
|
+
packageType,
|
|
1197
|
+
file: z.string(),
|
|
1198
|
+
type: z.enum(["chunk", "asset"]),
|
|
1199
|
+
from: z.enum(["main", "independent"]),
|
|
1200
|
+
size: count.nullable(),
|
|
1201
|
+
gzipSize: count.optional(),
|
|
1202
|
+
brotliSize: count.optional(),
|
|
1203
|
+
isEntry: z.boolean().optional(),
|
|
1204
|
+
moduleCount: count,
|
|
1205
|
+
source: z.string().optional(),
|
|
1206
|
+
sourceType: sourceType.optional()
|
|
1207
|
+
}))
|
|
1208
|
+
});
|
|
1209
|
+
const modulesQuerySchema = analyzeQuerySchema.extend({
|
|
1210
|
+
...pagination,
|
|
1211
|
+
packageId: z.string().optional(),
|
|
1212
|
+
artifact: z.string().optional(),
|
|
1213
|
+
sourceType: sourceType.optional(),
|
|
1214
|
+
query,
|
|
1215
|
+
duplicateOnly: z.boolean().default(false),
|
|
1216
|
+
sortBy: z.enum([
|
|
1217
|
+
"bytes",
|
|
1218
|
+
"estimatedSavingBytes",
|
|
1219
|
+
"source"
|
|
1220
|
+
]).default("bytes"),
|
|
1221
|
+
order
|
|
1222
|
+
});
|
|
1223
|
+
const modulesPageSchema = z.object({
|
|
1224
|
+
...reportResult,
|
|
1225
|
+
...pageResult,
|
|
1226
|
+
items: z.array(z.object({
|
|
1227
|
+
id: z.string(),
|
|
1228
|
+
source: z.string(),
|
|
1229
|
+
sourceType,
|
|
1230
|
+
bytes: count.describe("Largest known module occurrence, not the sum of all copies."),
|
|
1231
|
+
packageCount: count,
|
|
1232
|
+
fileCount: count,
|
|
1233
|
+
estimatedSavingBytes: count.describe("Upper-level duplication estimate, not guaranteed removable output bytes."),
|
|
1234
|
+
hasIndependentPackage: z.boolean(),
|
|
1235
|
+
advice: z.string().optional()
|
|
1236
|
+
}))
|
|
1237
|
+
});
|
|
1238
|
+
const comparisonQuerySchema = z.object({
|
|
1239
|
+
revision,
|
|
1240
|
+
...pagination,
|
|
1241
|
+
scope: z.enum([
|
|
1242
|
+
"package",
|
|
1243
|
+
"file",
|
|
1244
|
+
"module"
|
|
1245
|
+
]).describe("Module scope compares recorded chunk contributions by canonical source, not all asset/source placements."),
|
|
1246
|
+
packageId: z.string().optional(),
|
|
1247
|
+
change: change.optional(),
|
|
1248
|
+
query
|
|
1249
|
+
});
|
|
1250
|
+
const comparisonPageSchema = z.object({
|
|
1251
|
+
revision,
|
|
1252
|
+
currentHash: z.string(),
|
|
1253
|
+
previousHash: z.string().nullable(),
|
|
1254
|
+
available: z.boolean(),
|
|
1255
|
+
totals: z.object({
|
|
1256
|
+
currentBytes: count.nullable(),
|
|
1257
|
+
previousBytes: count.nullable(),
|
|
1258
|
+
deltaBytes: z.number().int().nullable(),
|
|
1259
|
+
currentUnmeasuredFiles: count,
|
|
1260
|
+
previousUnmeasuredFiles: count
|
|
1261
|
+
}).nullable(),
|
|
1262
|
+
...pageResult,
|
|
1263
|
+
items: z.array(z.object({
|
|
1264
|
+
key: z.string(),
|
|
1265
|
+
label: z.string(),
|
|
1266
|
+
change,
|
|
1267
|
+
currentBytes: count.nullable(),
|
|
1268
|
+
previousBytes: count.nullable(),
|
|
1269
|
+
deltaBytes: z.number().int().nullable(),
|
|
1270
|
+
packageId: z.string().optional(),
|
|
1271
|
+
packageLabel: z.string().optional(),
|
|
1272
|
+
file: z.string().optional(),
|
|
1273
|
+
moduleId: z.string().optional(),
|
|
1274
|
+
sourceType: sourceType.optional(),
|
|
1275
|
+
category: z.string().optional(),
|
|
1276
|
+
advice: z.string().optional()
|
|
1277
|
+
}))
|
|
1278
|
+
});
|
|
1279
|
+
//#endregion
|
|
1280
|
+
//#region src/dashboard/queries/shared.ts
|
|
1281
|
+
function summarizeBudget(item) {
|
|
1282
|
+
if (!item) return null;
|
|
1283
|
+
const { id, label, scope, currentBytes, limitBytes, ratio, status, measurement } = item;
|
|
1284
|
+
return {
|
|
1285
|
+
id,
|
|
1286
|
+
label,
|
|
1287
|
+
scope,
|
|
1288
|
+
currentBytes,
|
|
1289
|
+
limitBytes,
|
|
1290
|
+
ratio,
|
|
1291
|
+
status,
|
|
1292
|
+
measurement
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function selectAnalyzeReport(read, request) {
|
|
1296
|
+
const snapshot = read(request.revision);
|
|
1297
|
+
const payload = snapshot[request.target];
|
|
1298
|
+
if (!payload) throw new Error("没有上一份分析快照。");
|
|
1299
|
+
return {
|
|
1300
|
+
result: payload.source,
|
|
1301
|
+
context: {
|
|
1302
|
+
revision: request.revision,
|
|
1303
|
+
target: request.target,
|
|
1304
|
+
reportHash: payload.descriptor.hash
|
|
1305
|
+
},
|
|
1306
|
+
previousAvailable: snapshot.previous !== null
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function matchesQuery(query, ...values) {
|
|
1310
|
+
if (!query) return true;
|
|
1311
|
+
const needle = query.toLowerCase();
|
|
1312
|
+
return values.some((value) => value?.toLowerCase().includes(needle));
|
|
1313
|
+
}
|
|
1314
|
+
function compareText(left, right) {
|
|
1315
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1316
|
+
}
|
|
1317
|
+
function ordered(value, order) {
|
|
1318
|
+
return order === "asc" ? value : -value;
|
|
1319
|
+
}
|
|
1320
|
+
function paginate(items, request) {
|
|
1321
|
+
const end = Math.min(request.offset + request.limit, items.length);
|
|
1322
|
+
return {
|
|
1323
|
+
total: items.length,
|
|
1324
|
+
offset: request.offset,
|
|
1325
|
+
nextOffset: end < items.length ? end : null,
|
|
1326
|
+
items: items.slice(request.offset, end)
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
function reportTotals(result) {
|
|
1330
|
+
let bytes = 0;
|
|
1331
|
+
let files = 0;
|
|
1332
|
+
let unmeasuredFiles = 0;
|
|
1333
|
+
for (const pkg of result.packages) {
|
|
1334
|
+
files += pkg.files.length;
|
|
1335
|
+
for (const file of pkg.files) {
|
|
1336
|
+
bytes += file.size ?? 0;
|
|
1337
|
+
unmeasuredFiles += file.size === void 0 ? 1 : 0;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return {
|
|
1341
|
+
packages: result.packages.length,
|
|
1342
|
+
files,
|
|
1343
|
+
modules: result.modules.length,
|
|
1344
|
+
bytes,
|
|
1345
|
+
unmeasuredFiles
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
/** 模块归属以报告的 usage 为准;资源源码不一定出现在 chunk 的贡献列表中。 */
|
|
1349
|
+
function createModulePlacementIndex(result) {
|
|
1350
|
+
const index = /* @__PURE__ */ new Map();
|
|
1351
|
+
for (const module of result.modules) for (const usage of module.packages) {
|
|
1352
|
+
let placement = index.get(usage.packageId);
|
|
1353
|
+
if (!placement) {
|
|
1354
|
+
placement = {
|
|
1355
|
+
moduleIds: /* @__PURE__ */ new Set(),
|
|
1356
|
+
files: /* @__PURE__ */ new Map()
|
|
1357
|
+
};
|
|
1358
|
+
index.set(usage.packageId, placement);
|
|
1359
|
+
}
|
|
1360
|
+
placement.moduleIds.add(module.id);
|
|
1361
|
+
for (const file of usage.files) {
|
|
1362
|
+
let modules = placement.files.get(file);
|
|
1363
|
+
if (!modules) {
|
|
1364
|
+
modules = /* @__PURE__ */ new Set();
|
|
1365
|
+
placement.files.set(file, modules);
|
|
1366
|
+
}
|
|
1367
|
+
modules.add(module.id);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
return index;
|
|
1371
|
+
}
|
|
1372
|
+
//#endregion
|
|
1373
|
+
//#region src/dashboard/queries/analysis.ts
|
|
1374
|
+
function getAnalyzeSummary(read, input) {
|
|
1375
|
+
const { result, context, previousAvailable } = selectAnalyzeReport(read, analyzeQuerySchema.parse(input));
|
|
1376
|
+
const budgets = createAnalyzeBudgetCheck(result);
|
|
1377
|
+
const packageBudgets = {
|
|
1378
|
+
ok: 0,
|
|
1379
|
+
warning: 0,
|
|
1380
|
+
exceeded: 0,
|
|
1381
|
+
unknown: 0
|
|
1382
|
+
};
|
|
1383
|
+
for (const budget of budgets) if (budget.scope !== "total" && budget.scope !== "runtime") packageBudgets[budget.status] += 1;
|
|
1384
|
+
return {
|
|
1385
|
+
...context,
|
|
1386
|
+
previousAvailable,
|
|
1387
|
+
totals: reportTotals(result),
|
|
1388
|
+
totalBudget: summarizeBudget(budgets.find((item) => item.scope === "total")),
|
|
1389
|
+
runtimeBudget: summarizeBudget(budgets.find((item) => item.scope === "runtime")),
|
|
1390
|
+
packageBudgets
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function compareAnalyzeBuilds(read, input) {
|
|
1394
|
+
const request = comparisonQuerySchema.parse(input);
|
|
1395
|
+
const snapshot = read(request.revision);
|
|
1396
|
+
const context = {
|
|
1397
|
+
revision: request.revision,
|
|
1398
|
+
currentHash: snapshot.current.descriptor.hash,
|
|
1399
|
+
previousHash: snapshot.previous?.descriptor.hash ?? null
|
|
1400
|
+
};
|
|
1401
|
+
if (!snapshot.previous) return {
|
|
1402
|
+
...context,
|
|
1403
|
+
available: false,
|
|
1404
|
+
totals: null,
|
|
1405
|
+
total: 0,
|
|
1406
|
+
offset: request.offset,
|
|
1407
|
+
nextOffset: null,
|
|
1408
|
+
items: []
|
|
1409
|
+
};
|
|
1410
|
+
const current = snapshot.current.source;
|
|
1411
|
+
const previous = snapshot.previous.source;
|
|
1412
|
+
const comparison = createAnalyzeComparison(request.packageId === void 0 ? current : { packages: current.packages.filter((pkg) => pkg.id === request.packageId) }, request.packageId === void 0 ? previous : { packages: previous.packages.filter((pkg) => pkg.id === request.packageId) });
|
|
1413
|
+
const currentTotals = request.packageId === void 0 ? null : reportTotals(current);
|
|
1414
|
+
const previousTotals = request.packageId === void 0 ? null : reportTotals(previous);
|
|
1415
|
+
const currentUnmeasuredFiles = currentTotals?.unmeasuredFiles ?? comparison.currentUnmeasuredFiles;
|
|
1416
|
+
const previousUnmeasuredFiles = previousTotals?.unmeasuredFiles ?? comparison.previousUnmeasuredFiles;
|
|
1417
|
+
const currentBytes = currentUnmeasuredFiles > 0 ? null : currentTotals?.bytes ?? comparison.currentBytes;
|
|
1418
|
+
const previousBytes = previousUnmeasuredFiles > 0 ? null : previousTotals?.bytes ?? comparison.previousBytes;
|
|
1419
|
+
const items = comparison[request.scope === "package" ? "packages" : request.scope === "file" ? "files" : "modules"].filter((item) => (request.change === void 0 || item.change === request.change) && matchesQuery(request.query, item.label, item.file, item.moduleId, item.packageLabel));
|
|
1420
|
+
items.sort((a, b) => (b.deltaBytes === null ? -1 : Math.abs(b.deltaBytes)) - (a.deltaBytes === null ? -1 : Math.abs(a.deltaBytes)) || compareText(a.key, b.key));
|
|
1421
|
+
return {
|
|
1422
|
+
...context,
|
|
1423
|
+
available: true,
|
|
1424
|
+
totals: {
|
|
1425
|
+
currentBytes,
|
|
1426
|
+
previousBytes,
|
|
1427
|
+
deltaBytes: currentBytes === null || previousBytes === null ? null : currentBytes - previousBytes,
|
|
1428
|
+
currentUnmeasuredFiles,
|
|
1429
|
+
previousUnmeasuredFiles
|
|
1430
|
+
},
|
|
1431
|
+
...paginate(items, request)
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
//#endregion
|
|
1435
|
+
//#region src/dashboard/queries/catalog.ts
|
|
1436
|
+
function queryAnalyzePackages(read, input) {
|
|
1437
|
+
const request = packagesQuerySchema.parse(input);
|
|
1438
|
+
const { result, context } = selectAnalyzeReport(read, request);
|
|
1439
|
+
const budgets = new Map(createAnalyzeBudgetCheck(result).filter((item) => item.scope !== "total" && item.scope !== "runtime").map((item) => [item.id, item]));
|
|
1440
|
+
const placements = createModulePlacementIndex(result);
|
|
1441
|
+
const items = [];
|
|
1442
|
+
for (const pkg of result.packages) {
|
|
1443
|
+
const budget = summarizeBudget(budgets.get(pkg.id));
|
|
1444
|
+
if (request.type !== void 0 && pkg.type !== request.type || request.budgetStatus !== void 0 && budget?.status !== request.budgetStatus || !matchesQuery(request.query, pkg.id, pkg.label)) continue;
|
|
1445
|
+
let bytes = 0;
|
|
1446
|
+
let unmeasuredFiles = 0;
|
|
1447
|
+
for (const file of pkg.files) {
|
|
1448
|
+
bytes += file.size ?? 0;
|
|
1449
|
+
unmeasuredFiles += file.size === void 0 ? 1 : 0;
|
|
1450
|
+
}
|
|
1451
|
+
items.push({
|
|
1452
|
+
id: pkg.id,
|
|
1453
|
+
label: pkg.label,
|
|
1454
|
+
type: pkg.type,
|
|
1455
|
+
bytes,
|
|
1456
|
+
fileCount: pkg.files.length,
|
|
1457
|
+
moduleCount: placements.get(pkg.id)?.moduleIds.size ?? 0,
|
|
1458
|
+
unmeasuredFiles,
|
|
1459
|
+
budget
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
items.sort((a, b) => {
|
|
1463
|
+
return ordered(request.sortBy === "name" ? compareText(a.label, b.label) : request.sortBy === "budgetRatio" ? (a.budget?.ratio ?? -1) - (b.budget?.ratio ?? -1) : a.bytes - b.bytes, request.order) || compareText(a.id, b.id);
|
|
1464
|
+
});
|
|
1465
|
+
return {
|
|
1466
|
+
...context,
|
|
1467
|
+
...paginate(items, request)
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
function queryAnalyzeArtifacts(read, input) {
|
|
1471
|
+
const request = artifactsQuerySchema.parse(input);
|
|
1472
|
+
const { result, context } = selectAnalyzeReport(read, request);
|
|
1473
|
+
const items = [];
|
|
1474
|
+
const placements = createModulePlacementIndex(result);
|
|
1475
|
+
for (const pkg of result.packages) {
|
|
1476
|
+
if (request.packageId !== void 0 && pkg.id !== request.packageId) continue;
|
|
1477
|
+
const moduleFiles = placements.get(pkg.id)?.files;
|
|
1478
|
+
for (const file of pkg.files) {
|
|
1479
|
+
if (request.type !== void 0 && file.type !== request.type || request.moduleId !== void 0 && !moduleFiles?.get(file.file)?.has(request.moduleId) || !matchesQuery(request.query, file.file, file.source)) continue;
|
|
1480
|
+
items.push({
|
|
1481
|
+
packageId: pkg.id,
|
|
1482
|
+
packageType: pkg.type,
|
|
1483
|
+
file: file.file,
|
|
1484
|
+
type: file.type,
|
|
1485
|
+
from: file.from,
|
|
1486
|
+
size: file.size ?? null,
|
|
1487
|
+
gzipSize: file.gzipSize,
|
|
1488
|
+
brotliSize: file.brotliSize,
|
|
1489
|
+
isEntry: file.isEntry,
|
|
1490
|
+
moduleCount: moduleFiles?.get(file.file)?.size ?? 0,
|
|
1491
|
+
source: file.source,
|
|
1492
|
+
sourceType: file.sourceType
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
items.sort((a, b) => {
|
|
1497
|
+
return ordered(request.sortBy === "path" ? compareText(a.file, b.file) : (a.size ?? -1) - (b.size ?? -1), request.order) || compareText(a.packageId, b.packageId) || compareText(a.file, b.file);
|
|
1498
|
+
});
|
|
1499
|
+
return {
|
|
1500
|
+
...context,
|
|
1501
|
+
...paginate(items, request)
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
function queryAnalyzeModules(read, input) {
|
|
1505
|
+
const request = modulesQuerySchema.parse(input);
|
|
1506
|
+
const { result, context } = selectAnalyzeReport(read, request);
|
|
1507
|
+
const bytes = createModuleByteMap(result);
|
|
1508
|
+
const duplicates = new Map(createDuplicateModuleInsights(result).map((item) => [item.id, item]));
|
|
1509
|
+
const independentPackages = new Set(result.packages.filter((pkg) => pkg.type === "independent").map((pkg) => pkg.id));
|
|
1510
|
+
const items = [];
|
|
1511
|
+
for (const module of result.modules) {
|
|
1512
|
+
const duplicate = duplicates.get(module.id);
|
|
1513
|
+
if (request.sourceType !== void 0 && module.sourceType !== request.sourceType || request.duplicateOnly && !duplicate || !matchesQuery(request.query, module.id, module.source) || (request.packageId !== void 0 || request.artifact !== void 0) && !module.packages.some((pkg) => (request.packageId === void 0 || pkg.packageId === request.packageId) && (request.artifact === void 0 || pkg.files.includes(request.artifact)))) continue;
|
|
1514
|
+
items.push({
|
|
1515
|
+
id: module.id,
|
|
1516
|
+
source: module.source,
|
|
1517
|
+
sourceType: module.sourceType,
|
|
1518
|
+
bytes: bytes.get(module.id) ?? 0,
|
|
1519
|
+
packageCount: module.packages.length,
|
|
1520
|
+
fileCount: module.packages.reduce((total, pkg) => total + pkg.files.length, 0),
|
|
1521
|
+
estimatedSavingBytes: duplicate?.estimatedSavingBytes ?? 0,
|
|
1522
|
+
hasIndependentPackage: duplicate?.hasIndependentPackage ?? module.packages.some((pkg) => independentPackages.has(pkg.packageId)),
|
|
1523
|
+
advice: duplicate?.advice
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
items.sort((a, b) => {
|
|
1527
|
+
return ordered(request.sortBy === "source" ? compareText(a.source, b.source) : a[request.sortBy] - b[request.sortBy], request.order) || compareText(a.id, b.id);
|
|
1528
|
+
});
|
|
1529
|
+
return {
|
|
1530
|
+
...context,
|
|
1531
|
+
...paginate(items, request)
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
//#endregion
|
|
1535
|
+
//#region src/dashboard/queries/index.ts
|
|
1536
|
+
function createDashboardAnalyzeQueries(read) {
|
|
1537
|
+
return {
|
|
1538
|
+
summary: defineRpcFunction({
|
|
1539
|
+
name: "get-analyze-summary",
|
|
1540
|
+
type: "query",
|
|
1541
|
+
jsonSerializable: true,
|
|
1542
|
+
args: [analyzeQuerySchema],
|
|
1543
|
+
returns: analyzeSummarySchema,
|
|
1544
|
+
agent: {
|
|
1545
|
+
title: "Analyze summary and budgets",
|
|
1546
|
+
safety: "read",
|
|
1547
|
+
description: "Start diagnostics here: report totals, total budget and package budget-status counts without downloading JSON pages. Bytes sum known artifact sizes; unmeasuredFiles identifies incomplete size data. Use package queries to locate budget warnings."
|
|
1548
|
+
},
|
|
1549
|
+
handler: (input) => getAnalyzeSummary(read, input)
|
|
1550
|
+
}),
|
|
1551
|
+
packages: defineRpcFunction({
|
|
1552
|
+
name: "query-analyze-packages",
|
|
1553
|
+
type: "query",
|
|
1554
|
+
jsonSerializable: true,
|
|
1555
|
+
args: [packagesQuerySchema],
|
|
1556
|
+
returns: packagesPageSchema,
|
|
1557
|
+
agent: {
|
|
1558
|
+
title: "Analyze packages",
|
|
1559
|
+
safety: "read",
|
|
1560
|
+
description: "Filter packages by type, name or budget status; sort by size, name or budget ratio. Returns bounded rows and nextOffset pinned to a report revision/hash. Select a package id to inspect artifacts and modules."
|
|
1561
|
+
},
|
|
1562
|
+
handler: (input) => queryAnalyzePackages(read, input)
|
|
1563
|
+
}),
|
|
1564
|
+
artifacts: defineRpcFunction({
|
|
1565
|
+
name: "query-analyze-artifacts",
|
|
1566
|
+
type: "query",
|
|
1567
|
+
jsonSerializable: true,
|
|
1568
|
+
args: [artifactsQuerySchema],
|
|
1569
|
+
returns: artifactsPageSchema,
|
|
1570
|
+
agent: {
|
|
1571
|
+
title: "Analyze artifacts and module placements",
|
|
1572
|
+
safety: "read",
|
|
1573
|
+
description: "Find largest artifacts or locate an exact module id within emitted files. Filter by package, module, type or path. Rows omit nested module arrays. Read an artifact excerpt using read-dashboard-file with its file path and the current revision; previous artifacts are metadata only."
|
|
1574
|
+
},
|
|
1575
|
+
handler: (input) => queryAnalyzeArtifacts(read, input)
|
|
1576
|
+
}),
|
|
1577
|
+
modules: defineRpcFunction({
|
|
1578
|
+
name: "query-analyze-modules",
|
|
1579
|
+
type: "query",
|
|
1580
|
+
jsonSerializable: true,
|
|
1581
|
+
args: [modulesQuerySchema],
|
|
1582
|
+
returns: modulesPageSchema,
|
|
1583
|
+
agent: {
|
|
1584
|
+
title: "Analyze modules and duplicates",
|
|
1585
|
+
safety: "read",
|
|
1586
|
+
description: "Filter modules by source, package, artifact or duplicate membership, then sort by bytes or estimated duplication savings. Bytes represent the largest known occurrence. Savings are estimates, not guaranteed removable bytes; independent packages may require isolation. Query artifacts with a module id for placements, not a source-import causal chain."
|
|
1587
|
+
},
|
|
1588
|
+
handler: (input) => queryAnalyzeModules(read, input)
|
|
1589
|
+
}),
|
|
1590
|
+
comparison: defineRpcFunction({
|
|
1591
|
+
name: "compare-analyze-builds",
|
|
1592
|
+
type: "query",
|
|
1593
|
+
jsonSerializable: true,
|
|
1594
|
+
args: [comparisonQuerySchema],
|
|
1595
|
+
returns: comparisonPageSchema,
|
|
1596
|
+
agent: {
|
|
1597
|
+
title: "Compare current and previous builds",
|
|
1598
|
+
safety: "read",
|
|
1599
|
+
description: "Read bounded package, file or canonical-module additions, removals, growth and shrinkage, ordered by absolute delta. Totals always describe whole-build artifact sizes; module and file deltas overlap and must not be added together. No previous snapshot returns available:false and totals:null, not an empty baseline."
|
|
1600
|
+
},
|
|
1601
|
+
handler: (input) => compareAnalyzeBuilds(read, input)
|
|
1602
|
+
})
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
//#endregion
|
|
1606
|
+
//#region src/dashboard/assets.ts
|
|
1607
|
+
const ANALYZE_DASHBOARD_PACKAGE_NAME = "@weapp-vite/dashboard";
|
|
1608
|
+
const require = createRequire(import.meta.url);
|
|
1609
|
+
function parseDashboardManifest(source) {
|
|
1610
|
+
const parsed = JSON.parse(source);
|
|
1611
|
+
const metadata = parsed && typeof parsed === "object" && "weappViteDashboard" in parsed ? parsed.weappViteDashboard : void 0;
|
|
1612
|
+
if (!metadata || typeof metadata !== "object") return;
|
|
1613
|
+
return { weappViteDashboard: {
|
|
1614
|
+
devConfigFile: "devConfigFile" in metadata && typeof metadata.devConfigFile === "string" ? metadata.devConfigFile : void 0,
|
|
1615
|
+
devRoot: "devRoot" in metadata && typeof metadata.devRoot === "string" ? metadata.devRoot : void 0,
|
|
1616
|
+
distDir: "distDir" in metadata && typeof metadata.distDir === "string" ? metadata.distDir : void 0
|
|
1617
|
+
} };
|
|
1618
|
+
}
|
|
1619
|
+
function resolveDashboardPackage(cwd) {
|
|
1620
|
+
const resolvePaths = cwd && cwd !== process.cwd() ? [cwd, process.cwd()] : cwd ? [cwd] : void 0;
|
|
1621
|
+
try {
|
|
1622
|
+
const packageJsonPath = require.resolve(`${ANALYZE_DASHBOARD_PACKAGE_NAME}/package.json`, { paths: resolvePaths });
|
|
1623
|
+
let manifest;
|
|
1624
|
+
try {
|
|
1625
|
+
manifest = parseDashboardManifest(fs.readFileSync(packageJsonPath, "utf8"));
|
|
1626
|
+
} catch {
|
|
1627
|
+
manifest = void 0;
|
|
1628
|
+
}
|
|
1629
|
+
return {
|
|
1630
|
+
root: path.dirname(packageJsonPath),
|
|
1631
|
+
manifest
|
|
1632
|
+
};
|
|
1633
|
+
} catch {
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
function resolveDashboardDistRoot(packageRoot, manifest) {
|
|
1638
|
+
const root = path.resolve(packageRoot, manifest?.weappViteDashboard?.distDir ?? "dist");
|
|
1639
|
+
return fs.existsSync(path.join(root, "index.html")) ? { root } : void 0;
|
|
1640
|
+
}
|
|
1641
|
+
function resolveDashboardDevRoot(packageRoot, manifest) {
|
|
1642
|
+
const devRoot = manifest?.weappViteDashboard?.devRoot;
|
|
1643
|
+
const devConfigFile = manifest?.weappViteDashboard?.devConfigFile;
|
|
1644
|
+
if (!devRoot || !devConfigFile) return;
|
|
1645
|
+
const root = path.resolve(packageRoot, devRoot);
|
|
1646
|
+
const configFile = path.resolve(root, devConfigFile);
|
|
1647
|
+
return fs.existsSync(root) && fs.existsSync(configFile) ? {
|
|
1648
|
+
root,
|
|
1649
|
+
configFile
|
|
1650
|
+
} : void 0;
|
|
1651
|
+
}
|
|
1652
|
+
function resolveDashboardRoot(options) {
|
|
1653
|
+
const resolved = resolveDashboardPackage(options.cwd);
|
|
1654
|
+
if (!resolved) return;
|
|
1655
|
+
return options.watch ? resolveDashboardDevRoot(resolved.root, resolved.manifest) ?? resolveDashboardDistRoot(resolved.root, resolved.manifest) : resolveDashboardDistRoot(resolved.root, resolved.manifest) ?? resolveDashboardDevRoot(resolved.root, resolved.manifest);
|
|
1656
|
+
}
|
|
1657
|
+
/** 定位可选仪表盘包的原生构建 SPA;未安装或尚未构建时返回 undefined。 */
|
|
1658
|
+
function resolveDashboardClientAssets(cwd) {
|
|
1659
|
+
const resolved = resolveDashboardPackage(cwd);
|
|
1660
|
+
return resolved ? resolveDashboardDistRoot(resolved.root, resolved.manifest)?.root : void 0;
|
|
1661
|
+
}
|
|
1662
|
+
//#endregion
|
|
1663
|
+
//#region src/dashboard/index.ts
|
|
1664
|
+
const DEVFRAME_ID = "weapp-vite";
|
|
1665
|
+
/** 创建只读分析与任务元数据能力,不执行外部工作,也不修改宿主认证策略。 */
|
|
1666
|
+
function createAnalyzeDashboardDevframe({ snapshot: initialSnapshot, roots, initialEvents = [], clientAssets }) {
|
|
1667
|
+
const sessionId = randomUUID();
|
|
1668
|
+
let revision = 0;
|
|
1669
|
+
let snapshot = initialSnapshot;
|
|
1670
|
+
let serializedSnapshot = serializeDashboardAnalyzeSnapshot(initialSnapshot);
|
|
1671
|
+
const eventStore = createDashboardRuntimeEventStore(initialEvents);
|
|
1672
|
+
let broadcastDashboardState;
|
|
1673
|
+
const fileReader = createDashboardFileReader(roots, initialSnapshot.current, initialSnapshot.artifacts);
|
|
1674
|
+
const queries = createDashboardAnalyzeQueries((requestedRevision) => {
|
|
1675
|
+
if (!serializedSnapshot || requestedRevision !== revision) throw new Error(STALE_DASHBOARD_ANALYZE_REVISION_MESSAGE);
|
|
1676
|
+
return serializedSnapshot;
|
|
1677
|
+
});
|
|
1678
|
+
const investigations = createDashboardInvestigationStore(() => {
|
|
1679
|
+
if (!snapshot || !serializedSnapshot) throw new Error(STALE_DASHBOARD_ANALYZE_REVISION_MESSAGE);
|
|
1680
|
+
return {
|
|
1681
|
+
identity: {
|
|
1682
|
+
sessionId,
|
|
1683
|
+
revision,
|
|
1684
|
+
reportHash: serializedSnapshot.current.descriptor.hash
|
|
1685
|
+
},
|
|
1686
|
+
result: snapshot.current
|
|
1687
|
+
};
|
|
1688
|
+
}, () => {
|
|
1689
|
+
broadcastDashboardState?.();
|
|
1690
|
+
});
|
|
1691
|
+
const investigationRpc = createDashboardInvestigationRpc(investigations);
|
|
1692
|
+
function getState() {
|
|
1693
|
+
if (!serializedSnapshot) throw new Error(STALE_DASHBOARD_ANALYZE_REVISION_MESSAGE);
|
|
1694
|
+
return {
|
|
1695
|
+
sessionId,
|
|
1696
|
+
investigations: investigations.list(),
|
|
1697
|
+
analyze: {
|
|
1698
|
+
current: serializedSnapshot.current.descriptor,
|
|
1699
|
+
previous: serializedSnapshot.previous?.descriptor ?? null
|
|
1700
|
+
},
|
|
1701
|
+
revision,
|
|
1702
|
+
runtimeEvents: eventStore.read()
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
const getDashboardState = defineRpcFunction({
|
|
1706
|
+
name: "get-dashboard-state",
|
|
1707
|
+
type: "query",
|
|
1708
|
+
jsonSerializable: true,
|
|
1709
|
+
args: [],
|
|
1710
|
+
returns: dashboardStateSchema,
|
|
1711
|
+
agent: {
|
|
1712
|
+
title: "Dashboard state",
|
|
1713
|
+
description: "Read the live Dashboard revision, report descriptors and recent runtime events. Use this revision with domain queries; start with get-analyze-summary instead of downloading whole report pages.",
|
|
1714
|
+
safety: "read"
|
|
1715
|
+
},
|
|
1716
|
+
handler: getState
|
|
1717
|
+
});
|
|
1718
|
+
const getAnalyzePage = defineRpcFunction({
|
|
1719
|
+
name: "get-analyze-page",
|
|
1720
|
+
type: "query",
|
|
1721
|
+
jsonSerializable: true,
|
|
1722
|
+
args: [dashboardAnalyzePageRequestSchema],
|
|
1723
|
+
returns: dashboardAnalyzePageSchema,
|
|
1724
|
+
agent: {
|
|
1725
|
+
title: "Analyze report page",
|
|
1726
|
+
description: "Export one bounded JSON-text page of the current or previous report. Concatenate pages in index order; prefer domain summary, catalog and comparison queries for diagnostics.",
|
|
1727
|
+
safety: "read"
|
|
1728
|
+
},
|
|
1729
|
+
handler: (input) => {
|
|
1730
|
+
if (!serializedSnapshot) throw new Error(STALE_DASHBOARD_ANALYZE_REVISION_MESSAGE);
|
|
1731
|
+
return readDashboardAnalyzePage(input, revision, serializedSnapshot);
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
const readDashboardFile = defineRpcFunction({
|
|
1735
|
+
name: "read-dashboard-file",
|
|
1736
|
+
type: "query",
|
|
1737
|
+
jsonSerializable: true,
|
|
1738
|
+
args: [dashboardFileReadRequestSchema],
|
|
1739
|
+
returns: dashboardFileContentSchema,
|
|
1740
|
+
agent: {
|
|
1741
|
+
title: "Dashboard file content",
|
|
1742
|
+
description: "Read a report-listed source or captured artifact at the current revision. Prefer range:{offset,limit} for bounded UTF-16 excerpts (limit <=16384); omit range only when full content is needed. Size is full UTF-8 bytes. Source files are live disk reads; artifacts come from the snapshot. Allowlist, symlink and file-size protections apply.",
|
|
1743
|
+
safety: "read"
|
|
1744
|
+
},
|
|
1745
|
+
handler: async (input) => {
|
|
1746
|
+
const requestedRevision = input.revision;
|
|
1747
|
+
if (!snapshot || requestedRevision !== revision) throw new Error(STALE_DASHBOARD_ANALYZE_REVISION_MESSAGE);
|
|
1748
|
+
const content = await fileReader.read(input);
|
|
1749
|
+
if (!snapshot || requestedRevision !== revision) throw new Error(STALE_DASHBOARD_ANALYZE_REVISION_MESSAGE);
|
|
1750
|
+
return content;
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
const queryRuntimeEvents = defineRpcFunction({
|
|
1754
|
+
name: "query-runtime-events",
|
|
1755
|
+
type: "query",
|
|
1756
|
+
jsonSerializable: true,
|
|
1757
|
+
args: [dashboardRuntimeEventsQueryRequestSchema],
|
|
1758
|
+
returns: dashboardRuntimeEventsPageSchema,
|
|
1759
|
+
agent: {
|
|
1760
|
+
title: "Recent runtime events",
|
|
1761
|
+
description: "Filter the retained 24-event window by kind, level, source, text or inclusive ISO time bounds, including HMR profiles. Retention reports discarded events; this is not durable history and is independent of build revisions.",
|
|
1762
|
+
safety: "read"
|
|
1763
|
+
},
|
|
1764
|
+
handler: (input) => eventStore.query(input)
|
|
1765
|
+
});
|
|
1766
|
+
return {
|
|
1767
|
+
definition: defineDevframe({
|
|
1768
|
+
id: DEVFRAME_ID,
|
|
1769
|
+
name: "weapp-vite",
|
|
1770
|
+
version: VERSION,
|
|
1771
|
+
packageName: "weapp-vite",
|
|
1772
|
+
importMetaUrl: import.meta.url,
|
|
1773
|
+
homepage: "https://vite.weapp.dev/",
|
|
1774
|
+
description: "weapp-vite 构建分析与小程序开发工具。",
|
|
1775
|
+
icon: dashboardLogo,
|
|
1776
|
+
capabilities: {
|
|
1777
|
+
dev: true,
|
|
1778
|
+
build: false
|
|
1779
|
+
},
|
|
1780
|
+
clientAssets,
|
|
1781
|
+
async setup(ctx) {
|
|
1782
|
+
if (!snapshot) throw new Error(STALE_DASHBOARD_ANALYZE_REVISION_MESSAGE);
|
|
1783
|
+
const dashboard = ctx.scope(DEVFRAME_ID);
|
|
1784
|
+
dashboard.rpc.register(getDashboardState);
|
|
1785
|
+
dashboard.rpc.register(getAnalyzePage);
|
|
1786
|
+
dashboard.rpc.register(readDashboardFile);
|
|
1787
|
+
dashboard.rpc.register(queries.summary);
|
|
1788
|
+
dashboard.rpc.register(queries.packages);
|
|
1789
|
+
dashboard.rpc.register(queries.artifacts);
|
|
1790
|
+
dashboard.rpc.register(queries.modules);
|
|
1791
|
+
dashboard.rpc.register(queries.comparison);
|
|
1792
|
+
dashboard.rpc.register(queryRuntimeEvents);
|
|
1793
|
+
for (const rpc of investigationRpc) dashboard.rpc.register(rpc);
|
|
1794
|
+
broadcastDashboardState = async () => {
|
|
1795
|
+
await dashboard.rpc.broadcast({
|
|
1796
|
+
method: "dashboard-state-updated",
|
|
1797
|
+
args: [getState()],
|
|
1798
|
+
event: true
|
|
1799
|
+
});
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
}),
|
|
1803
|
+
async update(result, artifacts, previousResult) {
|
|
1804
|
+
if (!snapshot) return;
|
|
1805
|
+
const nextSnapshot = {
|
|
1806
|
+
current: result,
|
|
1807
|
+
previous: previousResult ?? snapshot.current,
|
|
1808
|
+
artifacts
|
|
1809
|
+
};
|
|
1810
|
+
const nextSerialized = serializeDashboardAnalyzeSnapshot(nextSnapshot, serializedSnapshot);
|
|
1811
|
+
fileReader.update(result, artifacts);
|
|
1812
|
+
snapshot = nextSnapshot;
|
|
1813
|
+
serializedSnapshot = nextSerialized;
|
|
1814
|
+
revision += 1;
|
|
1815
|
+
investigations.reportUpdated();
|
|
1816
|
+
eventStore.prepend([{
|
|
1817
|
+
kind: "build",
|
|
1818
|
+
level: "info",
|
|
1819
|
+
title: "analyze payload refreshed",
|
|
1820
|
+
detail: `已推送新的 analyze 结果,当前包含 ${result.packages.length} 个包与 ${result.modules.length} 个模块。`,
|
|
1821
|
+
tags: ["analyze", "refresh"]
|
|
1822
|
+
}]);
|
|
1823
|
+
await broadcastDashboardState?.();
|
|
1824
|
+
},
|
|
1825
|
+
emitRuntimeEvents(events) {
|
|
1826
|
+
if (!snapshot || events.length === 0) return;
|
|
1827
|
+
eventStore.prepend(events);
|
|
1828
|
+
broadcastDashboardState?.();
|
|
1829
|
+
},
|
|
1830
|
+
dispose() {
|
|
1831
|
+
if (!snapshot) return;
|
|
1832
|
+
snapshot = void 0;
|
|
1833
|
+
serializedSnapshot = void 0;
|
|
1834
|
+
eventStore.dispose();
|
|
1835
|
+
investigations.dispose();
|
|
1836
|
+
broadcastDashboardState = void 0;
|
|
1837
|
+
fileReader.dispose();
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
//#endregion
|
|
1842
|
+
export { createDashboardArtifactSnapshot as a, resolveDashboardRoot as i, ANALYZE_DASHBOARD_PACKAGE_NAME as n, resolveDashboardClientAssets as r, createAnalyzeDashboardDevframe as t };
|