create-next-imagicma 0.1.9 → 0.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-next-imagicma",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-next-imagicma": "./bin/create-next-imagicma.mjs"
@@ -5,9 +5,14 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>hono-app</title>
7
7
  <script src="/imagicma-picker-bridge.js"></script>
8
+ <script src="/imagicma-preview-feedback.js"></script>
8
9
  </head>
9
10
  <body class="antialiased">
10
11
  <div id="root"></div>
11
- <script type="module" src="/src/main.tsx"></script>
12
+ <script type="module">
13
+ import("/src/main.tsx").catch((error) => {
14
+ window.__IMAGICMA_PREVIEW_FEEDBACK__?.reportModuleBootstrapError(error);
15
+ });
16
+ </script>
12
17
  </body>
13
18
  </html>
@@ -0,0 +1,734 @@
1
+ (function () {
2
+ var PREVIEW_REPAIR_CHANNEL = 'imagicma.preview-repair';
3
+ var PREVIEW_REPAIR_VERSION = 1;
4
+ var PROD_PARENT_ORIGINS = {
5
+ 'https://agentma.cn': true,
6
+ 'https://imagicma.cn': true,
7
+ };
8
+ var LOCAL_PARENT_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i;
9
+ var LOCAL_IMAGICMA_PARENT_RE = /^https?:\/\/([a-z0-9-]+\.)?local\.(agentma\.cn|imagicma\.cn)(:\d+)?$/i;
10
+ var MAX_ERROR_MESSAGE_LENGTH = 4000;
11
+ var MAX_RESOURCE_CANDIDATES = 12;
12
+ var PANEL_ID = 'imagicma-preview-feedback-root';
13
+
14
+ var state = {
15
+ panel: null,
16
+ titleEl: null,
17
+ bodyEl: null,
18
+ metaEl: null,
19
+ detailEl: null,
20
+ statusEl: null,
21
+ repairButton: null,
22
+ refreshButton: null,
23
+ error: null,
24
+ requestInFlight: false,
25
+ lastSignature: '',
26
+ observer: null,
27
+ parentOrigin: '',
28
+ };
29
+
30
+ var originalConsoleError = typeof console !== 'undefined' && typeof console.error === 'function'
31
+ ? console.error.bind(console)
32
+ : null;
33
+
34
+ window.__IMAGICMA_PREVIEW_FEEDBACK__ = {
35
+ reportModuleBootstrapError: function (error) {
36
+ if (!error) return;
37
+ resolveLatestViteErrorDetails().then(function (details) {
38
+ if (details) {
39
+ renderError(details);
40
+ return;
41
+ }
42
+ reportStartupError(
43
+ trimText(error.name) || 'ModuleBootstrapError',
44
+ trimText(error.message) || normalizeWhitespace(error),
45
+ trimText(error.stack) || trimText(error.message),
46
+ );
47
+ });
48
+ },
49
+ };
50
+
51
+ if (originalConsoleError) {
52
+ console.error = function () {
53
+ maybeCaptureViteConsoleError(arguments);
54
+ return originalConsoleError.apply(console, arguments);
55
+ };
56
+ }
57
+
58
+ function trimText(value) {
59
+ return typeof value === 'string' ? value.trim() : '';
60
+ }
61
+
62
+ function normalizeWhitespace(value) {
63
+ return trimText(String(value || '').replace(/\r\n?/g, '\n').replace(/\n{3,}/g, '\n\n'));
64
+ }
65
+
66
+ function truncateText(value, maxLength) {
67
+ var text = normalizeWhitespace(value);
68
+ if (!text) return '';
69
+ return text.length > maxLength ? text.slice(0, maxLength) + '\n…' : text;
70
+ }
71
+
72
+ function stringifyConsoleArgs(args) {
73
+ return Array.prototype.map.call(args, function (arg) {
74
+ if (typeof arg === 'string') return arg;
75
+ if (arg && typeof arg === 'object') {
76
+ var message = trimText(arg.message);
77
+ var stack = trimText(arg.stack);
78
+ if (message && stack) return message + '\n' + stack;
79
+ if (message) return message;
80
+ try {
81
+ return JSON.stringify(arg);
82
+ } catch (_error) {
83
+ return String(arg);
84
+ }
85
+ }
86
+ return String(arg);
87
+ }).join('\n');
88
+ }
89
+
90
+ function maybeCaptureViteConsoleError(argsLike) {
91
+ var text = normalizeWhitespace(stringifyConsoleArgs(argsLike));
92
+ if (!text) return;
93
+ if (text.indexOf('[vite] Internal Server Error') !== 0) return;
94
+
95
+ renderError({
96
+ title: '预览编译失败',
97
+ errorName: 'ViteConsoleError',
98
+ errorMessage: text,
99
+ errorStack: text,
100
+ });
101
+ }
102
+
103
+ function isAllowedParentOrigin(origin) {
104
+ if (!origin) return false;
105
+ return !!PROD_PARENT_ORIGINS[origin] || LOCAL_PARENT_RE.test(origin) || LOCAL_IMAGICMA_PARENT_RE.test(origin);
106
+ }
107
+
108
+ function resolveParentOrigin() {
109
+ if (state.parentOrigin && isAllowedParentOrigin(state.parentOrigin)) {
110
+ return state.parentOrigin;
111
+ }
112
+
113
+ if (window.parent === window) {
114
+ state.parentOrigin = '';
115
+ return '';
116
+ }
117
+
118
+ var referrer = trimText(document.referrer);
119
+ if (!referrer) {
120
+ state.parentOrigin = '';
121
+ return '';
122
+ }
123
+
124
+ try {
125
+ var origin = new URL(referrer).origin;
126
+ state.parentOrigin = isAllowedParentOrigin(origin) ? origin : '';
127
+ return state.parentOrigin;
128
+ } catch (_error) {
129
+ state.parentOrigin = '';
130
+ return '';
131
+ }
132
+ }
133
+
134
+ function createRequestId() {
135
+ if (window.crypto && typeof window.crypto.randomUUID === 'function') {
136
+ return window.crypto.randomUUID();
137
+ }
138
+ return 'repair_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
139
+ }
140
+
141
+ function hasBootstrappedApp() {
142
+ var root = document.getElementById('root');
143
+ if (!root) return false;
144
+ if (root.childElementCount > 0) return true;
145
+ return trimText(root.textContent).length > 0;
146
+ }
147
+
148
+ function buildSummary(details) {
149
+ var message = truncateText(details.errorMessage, MAX_ERROR_MESSAGE_LENGTH);
150
+ if (!message) {
151
+ return '检测到预览初始化失败,请刷新页面后重试。';
152
+ }
153
+
154
+ var lines = message.split('\n').map(trimText).filter(Boolean);
155
+ if (lines.length === 0) {
156
+ return '检测到预览初始化失败,请刷新页面后重试。';
157
+ }
158
+
159
+ var firstLine = lines[0];
160
+ if (/^Internal Server Error$/i.test(firstLine) && lines[1]) {
161
+ firstLine = lines[1];
162
+ }
163
+ return firstLine;
164
+ }
165
+
166
+ function getNormalizedLines(text) {
167
+ return normalizeWhitespace(text).split('\n').map(trimText).filter(Boolean);
168
+ }
169
+
170
+ function extractKeyErrorLine(text) {
171
+ var lines = getNormalizedLines(text);
172
+ for (var index = 0; index < lines.length; index += 1) {
173
+ var line = lines[index];
174
+ if (!line) continue;
175
+ if (/^\[vite\]/i.test(line)) continue;
176
+ if (/^Internal Server Error$/i.test(line)) continue;
177
+ if (/^plugin:/i.test(line)) continue;
178
+ if (/^id:/i.test(line)) continue;
179
+ if (/^at\s+/i.test(line)) continue;
180
+ if (/^\(?Error overlay failed to load\)?$/i.test(line)) continue;
181
+ return line;
182
+ }
183
+ return '';
184
+ }
185
+
186
+ function extractFieldValue(text, fieldName) {
187
+ var escapedFieldName = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
188
+ var regexp = new RegExp('^' + escapedFieldName + ':\\s*(.+)$', 'mi');
189
+ var match = normalizeWhitespace(text).match(regexp);
190
+ return match && match[1] ? trimText(match[1]) : '';
191
+ }
192
+
193
+ function extractFrameSnippet(text) {
194
+ var lines = normalizeWhitespace(text).split('\n');
195
+ var caretIndex = -1;
196
+ var index = 0;
197
+ for (; index < lines.length; index += 1) {
198
+ if (lines[index].indexOf('^') >= 0) {
199
+ caretIndex = index;
200
+ break;
201
+ }
202
+ }
203
+
204
+ if (caretIndex >= 0) {
205
+ var start = Math.max(0, caretIndex - 2);
206
+ var end = Math.min(lines.length, caretIndex + 2);
207
+ return lines.slice(start, end).join('\n').trim();
208
+ }
209
+
210
+ var frameLines = lines.filter(function (line) {
211
+ return /^\s*(>?[\s\d|]+|at\s+)/.test(line);
212
+ });
213
+ return frameLines.slice(0, 5).join('\n').trim();
214
+ }
215
+
216
+ function buildRenderedDetail(error) {
217
+ var sourceText = [error.errorMessage, error.errorStack].filter(Boolean).join('\n\n');
218
+ var keyErrorLine = extractKeyErrorLine(sourceText);
219
+ var frameSnippet = extractFrameSnippet(sourceText);
220
+ var plugin = extractFieldValue(sourceText, 'plugin');
221
+ var id = extractFieldValue(sourceText, 'id');
222
+ return {
223
+ keyErrorLine: keyErrorLine,
224
+ detailText: frameSnippet ? ('定位片段\n' + frameSnippet) : (keyErrorLine || error.errorMessage),
225
+ metaText: [
226
+ plugin ? ('plugin: ' + plugin) : '',
227
+ id ? ('id: ' + id) : '',
228
+ ].filter(Boolean).join(' '),
229
+ };
230
+ }
231
+
232
+ function getViteOverlayText(overlay) {
233
+ if (!overlay) return '';
234
+ var root = overlay.shadowRoot || overlay;
235
+ return truncateText(root.textContent || overlay.textContent || '', MAX_ERROR_MESSAGE_LENGTH);
236
+ }
237
+
238
+ function sameOriginUrl(url) {
239
+ try {
240
+ var parsed = new URL(url, window.location.href);
241
+ return parsed.origin === window.location.origin ? parsed : null;
242
+ } catch (_error) {
243
+ return null;
244
+ }
245
+ }
246
+
247
+ function isLikelySourceRequest(pathname) {
248
+ return (
249
+ pathname.indexOf('/src/') === 0
250
+ || pathname.indexOf('/@fs/') === 0
251
+ || /\.(?:[cm]?js|[jt]sx?|css)$/.test(pathname)
252
+ );
253
+ }
254
+
255
+ function listRecentResourceCandidates() {
256
+ if (!window.performance || typeof window.performance.getEntriesByType !== 'function') {
257
+ return [];
258
+ }
259
+
260
+ var entries = window.performance.getEntriesByType('resource');
261
+ var seen = {};
262
+ var urls = [];
263
+
264
+ for (var index = entries.length - 1; index >= 0; index -= 1) {
265
+ var entry = entries[index];
266
+ if (!entry || !entry.name) continue;
267
+ var parsed = sameOriginUrl(entry.name);
268
+ if (!parsed) continue;
269
+ if (!isLikelySourceRequest(parsed.pathname)) continue;
270
+ if (parsed.pathname.indexOf('/imagicma-preview-feedback.js') >= 0) continue;
271
+ if (parsed.pathname.indexOf('/@vite/client') >= 0) continue;
272
+ if (seen[parsed.href]) continue;
273
+ seen[parsed.href] = true;
274
+ urls.push(parsed.href);
275
+ if (urls.length >= MAX_RESOURCE_CANDIDATES) {
276
+ break;
277
+ }
278
+ }
279
+
280
+ return urls;
281
+ }
282
+
283
+ function parseViteErrorPayloadFromHtml(html) {
284
+ var text = typeof html === 'string' ? html : '';
285
+ if (!text) return null;
286
+
287
+ var match = text.match(/const error = (\{[\s\S]*?\})\s*try\s*\{/);
288
+ if (!match || !match[1]) return null;
289
+
290
+ try {
291
+ return JSON.parse(match[1]);
292
+ } catch (_error) {
293
+ return null;
294
+ }
295
+ }
296
+
297
+ function buildDetailedViteMessage(payload, requestUrl) {
298
+ if (!payload) return '';
299
+
300
+ var sections = ['[vite] Internal Server Error'];
301
+ var message = trimText(payload.message);
302
+ var frame = trimText(payload.frame);
303
+ var plugin = trimText(payload.plugin);
304
+ var id = trimText(payload.id) || trimText(requestUrl);
305
+ var stack = trimText(payload.stack);
306
+
307
+ if (message) sections.push(message);
308
+ if (frame) sections.push(frame);
309
+ if (plugin) sections.push('plugin: ' + plugin);
310
+ if (id) sections.push('id: ' + id);
311
+ if (stack) sections.push(stack);
312
+
313
+ return sections.join('\n\n');
314
+ }
315
+
316
+ function fetchViteErrorDetailsFromUrl(url) {
317
+ return window.fetch(url, {
318
+ credentials: 'same-origin',
319
+ cache: 'no-store',
320
+ }).then(function (response) {
321
+ if (!response || response.status < 500) {
322
+ return null;
323
+ }
324
+
325
+ return response.text().then(function (html) {
326
+ var payload = parseViteErrorPayloadFromHtml(html);
327
+ if (!payload) return null;
328
+
329
+ var detailedMessage = buildDetailedViteMessage(payload, url);
330
+ return {
331
+ title: '预览编译失败',
332
+ errorName: trimText(payload.plugin) || 'ViteCompileError',
333
+ errorMessage: detailedMessage || trimText(payload.message) || 'Vite 编译失败',
334
+ errorStack: [trimText(payload.frame), trimText(payload.stack)].filter(Boolean).join('\n\n') || detailedMessage,
335
+ };
336
+ }).catch(function () {
337
+ return null;
338
+ });
339
+ }).catch(function () {
340
+ return null;
341
+ });
342
+ }
343
+
344
+ function resolveLatestViteErrorDetails() {
345
+ var candidates = listRecentResourceCandidates();
346
+ if (candidates.length === 0) {
347
+ return Promise.resolve(null);
348
+ }
349
+
350
+ var index = 0;
351
+
352
+ function next() {
353
+ if (index >= candidates.length) {
354
+ return Promise.resolve(null);
355
+ }
356
+
357
+ var candidate = candidates[index];
358
+ index += 1;
359
+ return fetchViteErrorDetailsFromUrl(candidate).then(function (details) {
360
+ if (details) return details;
361
+ return next();
362
+ });
363
+ }
364
+
365
+ return next();
366
+ }
367
+
368
+ function dismissPanel() {
369
+ if (state.panel && state.panel.parentNode) {
370
+ state.panel.parentNode.removeChild(state.panel);
371
+ }
372
+ state.panel = null;
373
+ state.titleEl = null;
374
+ state.bodyEl = null;
375
+ state.metaEl = null;
376
+ state.detailEl = null;
377
+ state.statusEl = null;
378
+ state.repairButton = null;
379
+ state.refreshButton = null;
380
+ state.error = null;
381
+ state.requestInFlight = false;
382
+ state.lastSignature = '';
383
+ }
384
+
385
+ function ensurePanel() {
386
+ if (state.panel && document.body.contains(state.panel)) {
387
+ return state.panel;
388
+ }
389
+
390
+ var panel = document.createElement('div');
391
+ panel.id = PANEL_ID;
392
+ panel.setAttribute('role', 'alert');
393
+ panel.style.position = 'fixed';
394
+ panel.style.inset = '0';
395
+ panel.style.zIndex = '2147483647';
396
+ panel.style.display = 'flex';
397
+ panel.style.alignItems = 'center';
398
+ panel.style.justifyContent = 'center';
399
+ panel.style.padding = '24px';
400
+ panel.style.background = 'linear-gradient(135deg, #243b53 0%, #3f497f 48%, #145374 100%)';
401
+ panel.style.color = '#fff';
402
+ panel.style.fontFamily = 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif';
403
+
404
+ var card = document.createElement('div');
405
+ card.style.width = 'min(720px, 100%)';
406
+ card.style.maxHeight = 'min(88vh, 900px)';
407
+ card.style.overflow = 'auto';
408
+ card.style.borderRadius = '28px';
409
+ card.style.border = '1px solid rgba(255, 255, 255, 0.16)';
410
+ card.style.background = 'rgba(7, 16, 34, 0.38)';
411
+ card.style.boxShadow = '0 28px 80px rgba(2, 8, 23, 0.45)';
412
+ card.style.backdropFilter = 'blur(18px)';
413
+ card.style.padding = '32px';
414
+
415
+ var badge = document.createElement('div');
416
+ badge.textContent = 'Preview bootstrap error';
417
+ badge.style.display = 'inline-flex';
418
+ badge.style.alignItems = 'center';
419
+ badge.style.borderRadius = '999px';
420
+ badge.style.padding = '6px 10px';
421
+ badge.style.fontSize = '12px';
422
+ badge.style.letterSpacing = '0.08em';
423
+ badge.style.textTransform = 'uppercase';
424
+ badge.style.color = 'rgba(255,255,255,0.82)';
425
+ badge.style.background = 'rgba(255,255,255,0.1)';
426
+
427
+ var title = document.createElement('h1');
428
+ title.style.margin = '18px 0 10px';
429
+ title.style.fontSize = 'clamp(28px, 4vw, 36px)';
430
+ title.style.lineHeight = '1.1';
431
+ title.style.fontWeight = '700';
432
+
433
+ var body = document.createElement('p');
434
+ body.style.margin = '0';
435
+ body.style.fontSize = '15px';
436
+ body.style.lineHeight = '1.7';
437
+ body.style.color = 'rgba(255,255,255,0.82)';
438
+
439
+ var meta = document.createElement('p');
440
+ meta.style.margin = '12px 0 0';
441
+ meta.style.fontSize = '12px';
442
+ meta.style.lineHeight = '1.6';
443
+ meta.style.color = 'rgba(255,255,255,0.62)';
444
+ meta.style.display = 'none';
445
+
446
+ var detail = document.createElement('pre');
447
+ detail.style.margin = '18px 0 0';
448
+ detail.style.padding = '18px';
449
+ detail.style.borderRadius = '20px';
450
+ detail.style.background = 'rgba(2, 6, 23, 0.42)';
451
+ detail.style.border = '1px solid rgba(255, 255, 255, 0.08)';
452
+ detail.style.whiteSpace = 'pre-wrap';
453
+ detail.style.wordBreak = 'break-word';
454
+ detail.style.fontSize = '13px';
455
+ detail.style.lineHeight = '1.65';
456
+ detail.style.color = 'rgba(255,255,255,0.92)';
457
+
458
+ var status = document.createElement('p');
459
+ status.style.minHeight = '22px';
460
+ status.style.margin = '16px 0 0';
461
+ status.style.fontSize = '13px';
462
+ status.style.lineHeight = '1.6';
463
+ status.style.color = 'rgba(255,255,255,0.76)';
464
+
465
+ var actions = document.createElement('div');
466
+ actions.style.display = 'flex';
467
+ actions.style.flexWrap = 'wrap';
468
+ actions.style.gap = '12px';
469
+ actions.style.marginTop = '24px';
470
+
471
+ var repairButton = document.createElement('button');
472
+ repairButton.type = 'button';
473
+ repairButton.textContent = '一键修复';
474
+ repairButton.style.height = '42px';
475
+ repairButton.style.padding = '0 18px';
476
+ repairButton.style.borderRadius = '999px';
477
+ repairButton.style.border = 'none';
478
+ repairButton.style.background = '#ffffff';
479
+ repairButton.style.color = '#0f172a';
480
+ repairButton.style.fontSize = '14px';
481
+ repairButton.style.fontWeight = '600';
482
+ repairButton.style.cursor = 'pointer';
483
+
484
+ var refreshButton = document.createElement('button');
485
+ refreshButton.type = 'button';
486
+ refreshButton.textContent = '刷新页面';
487
+ refreshButton.style.height = '42px';
488
+ refreshButton.style.padding = '0 18px';
489
+ refreshButton.style.borderRadius = '999px';
490
+ refreshButton.style.border = '1px solid rgba(255,255,255,0.18)';
491
+ refreshButton.style.background = 'rgba(255,255,255,0.08)';
492
+ refreshButton.style.color = '#ffffff';
493
+ refreshButton.style.fontSize = '14px';
494
+ refreshButton.style.fontWeight = '600';
495
+ refreshButton.style.cursor = 'pointer';
496
+
497
+ repairButton.addEventListener('click', function () {
498
+ void sendRepairRequest();
499
+ });
500
+ refreshButton.addEventListener('click', function () {
501
+ window.location.reload();
502
+ });
503
+
504
+ actions.appendChild(repairButton);
505
+ actions.appendChild(refreshButton);
506
+
507
+ card.appendChild(badge);
508
+ card.appendChild(title);
509
+ card.appendChild(body);
510
+ card.appendChild(meta);
511
+ card.appendChild(detail);
512
+ card.appendChild(status);
513
+ card.appendChild(actions);
514
+ panel.appendChild(card);
515
+ document.body.appendChild(panel);
516
+
517
+ state.panel = panel;
518
+ state.titleEl = title;
519
+ state.bodyEl = body;
520
+ state.metaEl = meta;
521
+ state.detailEl = detail;
522
+ state.statusEl = status;
523
+ state.repairButton = repairButton;
524
+ state.refreshButton = refreshButton;
525
+
526
+ return panel;
527
+ }
528
+
529
+ function updateRepairButtonState() {
530
+ if (!state.repairButton) return;
531
+ var canRepair = !!resolveParentOrigin() && window.parent !== window && !!state.error;
532
+ state.repairButton.disabled = state.requestInFlight || !canRepair;
533
+ state.repairButton.style.opacity = state.requestInFlight || !canRepair ? '0.7' : '1';
534
+ state.repairButton.style.cursor = state.requestInFlight || !canRepair ? 'not-allowed' : 'pointer';
535
+ if (state.requestInFlight) {
536
+ state.repairButton.textContent = '同步中...';
537
+ return;
538
+ }
539
+ state.repairButton.textContent = canRepair ? '一键修复' : '仅支持在主界面预览中修复';
540
+ }
541
+
542
+ function renderError(details) {
543
+ var signature = [
544
+ trimText(details.title),
545
+ trimText(details.errorName),
546
+ trimText(details.errorMessage),
547
+ ].join('|');
548
+ if (signature === state.lastSignature) {
549
+ return;
550
+ }
551
+
552
+ state.lastSignature = signature;
553
+ state.error = {
554
+ title: trimText(details.title) || '预览暂时不可用',
555
+ errorName: trimText(details.errorName) || 'PreviewBootstrapError',
556
+ errorMessage: truncateText(details.errorMessage, MAX_ERROR_MESSAGE_LENGTH),
557
+ errorStack: truncateText(details.errorStack || details.errorMessage, MAX_ERROR_MESSAGE_LENGTH),
558
+ timestamp: Date.now(),
559
+ };
560
+
561
+ ensurePanel();
562
+
563
+ state.titleEl.textContent = state.error.title;
564
+ var renderedDetail = buildRenderedDetail(state.error);
565
+ state.bodyEl.textContent = renderedDetail.keyErrorLine
566
+ ? ('关键报错:' + renderedDetail.keyErrorLine)
567
+ : '检测到预览初始化阶段的错误。你可以把错误信息同步回主界面,让系统生成修复草稿,或先刷新页面。';
568
+ if (state.metaEl) {
569
+ state.metaEl.textContent = renderedDetail.metaText;
570
+ state.metaEl.style.display = renderedDetail.metaText ? 'block' : 'none';
571
+ }
572
+ state.detailEl.textContent = renderedDetail.detailText;
573
+ state.statusEl.textContent = '';
574
+ state.requestInFlight = false;
575
+ updateRepairButtonState();
576
+ }
577
+
578
+ function sendRepairRequest() {
579
+ var parentOrigin = resolveParentOrigin();
580
+ if (!state.error || !parentOrigin || window.parent === window || state.requestInFlight) {
581
+ updateRepairButtonState();
582
+ return Promise.resolve();
583
+ }
584
+
585
+ state.requestInFlight = true;
586
+ state.statusEl.textContent = '正在同步修复草稿到主界面…';
587
+ updateRepairButtonState();
588
+
589
+ var requestId = createRequestId();
590
+
591
+ return new Promise(function (resolve) {
592
+ var timer = window.setTimeout(function () {
593
+ cleanup();
594
+ state.requestInFlight = false;
595
+ state.statusEl.textContent = '主界面响应超时,请稍后重试或先刷新页面。';
596
+ updateRepairButtonState();
597
+ resolve();
598
+ }, 4000);
599
+
600
+ function cleanup() {
601
+ window.clearTimeout(timer);
602
+ window.removeEventListener('message', handleAck);
603
+ }
604
+
605
+ function handleAck(event) {
606
+ if (event.source !== window.parent) return;
607
+ if (event.origin !== parentOrigin) return;
608
+ var data = event.data;
609
+ if (!data || data.channel !== PREVIEW_REPAIR_CHANNEL || data.version !== PREVIEW_REPAIR_VERSION) return;
610
+ if (data.type !== 'IMAGICMA_PREVIEW_REPAIR_ACK' || data.requestId !== requestId) return;
611
+
612
+ cleanup();
613
+ state.requestInFlight = false;
614
+ state.statusEl.textContent = trimText(data.payload && data.payload.message)
615
+ || (data.payload && data.payload.status === 'ok'
616
+ ? '已同步修复草稿到主界面,请回到对话区确认发送。'
617
+ : '同步失败,请稍后重试。');
618
+ updateRepairButtonState();
619
+ resolve();
620
+ }
621
+
622
+ window.addEventListener('message', handleAck);
623
+
624
+ try {
625
+ window.parent.postMessage(
626
+ {
627
+ channel: PREVIEW_REPAIR_CHANNEL,
628
+ version: PREVIEW_REPAIR_VERSION,
629
+ type: 'IMAGICMA_PREVIEW_REPAIR_REQUEST',
630
+ requestId: requestId,
631
+ payload: {
632
+ pageUrl: window.location.href,
633
+ errorName: state.error.errorName,
634
+ errorMessage: state.error.errorMessage,
635
+ errorStack: state.error.errorStack,
636
+ timestamp: state.error.timestamp,
637
+ },
638
+ },
639
+ parentOrigin,
640
+ );
641
+ } catch (_error) {
642
+ cleanup();
643
+ state.requestInFlight = false;
644
+ state.statusEl.textContent = '同步失败,请稍后重试或先刷新页面。';
645
+ updateRepairButtonState();
646
+ resolve();
647
+ }
648
+ });
649
+ }
650
+
651
+ function maybeReportOverlay() {
652
+ var overlay = document.querySelector('vite-error-overlay');
653
+ if (!overlay) {
654
+ if (state.panel && hasBootstrappedApp()) {
655
+ dismissPanel();
656
+ }
657
+ return;
658
+ }
659
+
660
+ window.setTimeout(function () {
661
+ var text = getViteOverlayText(overlay);
662
+ if (!text) return;
663
+ try {
664
+ overlay.style.display = 'none';
665
+ } catch (_error) {
666
+ // Ignore Vite overlay style write failures.
667
+ }
668
+ renderError({
669
+ title: '预览编译失败',
670
+ errorName: 'ViteCompileError',
671
+ errorMessage: text,
672
+ errorStack: text,
673
+ });
674
+ }, 0);
675
+ }
676
+
677
+ function reportStartupError(errorName, errorMessage, errorStack) {
678
+ if (hasBootstrappedApp()) return;
679
+ if (!trimText(errorMessage)) return;
680
+ renderError({
681
+ title: '预览加载失败',
682
+ errorName: trimText(errorName) || 'PreviewBootstrapError',
683
+ errorMessage: errorMessage,
684
+ errorStack: errorStack || errorMessage,
685
+ });
686
+ }
687
+
688
+ window.addEventListener('message', function (event) {
689
+ if (event.source !== window.parent) return;
690
+ if (!isAllowedParentOrigin(event.origin)) return;
691
+ state.parentOrigin = event.origin;
692
+ updateRepairButtonState();
693
+ });
694
+
695
+ window.addEventListener('error', function (event) {
696
+ var error = event && event.error;
697
+ var errorName = trimText(error && error.name) || 'PreviewBootstrapError';
698
+ var errorMessage = trimText(error && error.message) || trimText(event && event.message);
699
+ var errorStack = trimText(error && error.stack);
700
+ reportStartupError(errorName, errorMessage, errorStack);
701
+ }, true);
702
+
703
+ window.addEventListener('unhandledrejection', function (event) {
704
+ var reason = event && event.reason;
705
+ if (!reason) return;
706
+
707
+ if (typeof reason === 'string') {
708
+ reportStartupError('UnhandledRejection', reason, reason);
709
+ return;
710
+ }
711
+
712
+ reportStartupError(
713
+ trimText(reason.name) || 'UnhandledRejection',
714
+ trimText(reason.message) || normalizeWhitespace(reason),
715
+ trimText(reason.stack) || trimText(reason.message),
716
+ );
717
+ });
718
+
719
+ state.observer = new MutationObserver(function () {
720
+ maybeReportOverlay();
721
+ });
722
+ state.observer.observe(document.documentElement, {
723
+ childList: true,
724
+ subtree: true,
725
+ });
726
+
727
+ if (document.readyState === 'loading') {
728
+ document.addEventListener('DOMContentLoaded', maybeReportOverlay, { once: true });
729
+ } else {
730
+ maybeReportOverlay();
731
+ }
732
+
733
+ window.setTimeout(maybeReportOverlay, 300);
734
+ })();
@@ -12,6 +12,8 @@ const DEBUG_ATTR_PATH = "data-imagicma-path";
12
12
  const DEBUG_ATTR_LINE = "data-imagicma-line";
13
13
  const DEBUG_ATTR_FILE = "data-imagicma-file";
14
14
  const DEBUG_ATTR_COMPONENT = "data-imagicma-component";
15
+ const RUNTIME_TEXT_SEGMENT_ATTR = "data-imagicma-runtime-text-segment";
16
+ const RUNTIME_TEXT_SEGMENT_INDEX_ATTR = "data-imagicma-text-segment-index";
15
17
  const SORT_DRAG_THRESHOLD_PX = 4;
16
18
 
17
19
  type PreviewPickerMode = "single" | "design";
@@ -40,10 +42,16 @@ type PreviewSourceStyleField =
40
42
  | "margin"
41
43
  | "padding";
42
44
 
43
- type PreviewSourceTextBinding = {
44
- kind: "jsx-text";
45
- source: PreviewSourceRef;
46
- };
45
+ type PreviewSourceTextBinding =
46
+ | {
47
+ kind: "jsx-text";
48
+ source: PreviewSourceRef;
49
+ }
50
+ | {
51
+ kind: "jsx-text-segment";
52
+ source: PreviewSourceRef;
53
+ segmentIndex: number;
54
+ };
47
55
 
48
56
  type PreviewSourceRemoveBinding = {
49
57
  kind: "jsx-element";
@@ -172,6 +180,7 @@ type RuntimeState = {
172
180
  parentOrigin: string | null;
173
181
  activeSessionId: string | null;
174
182
  enabled: boolean;
183
+ nodeIdIndex: Map<string, HTMLElement[]>;
175
184
  selectedElement: HTMLElement | null;
176
185
  hoveredElement: HTMLElement | null;
177
186
  overlayRoot: HTMLDivElement | null;
@@ -318,6 +327,15 @@ function setSemanticAttribute(element: HTMLElement, name: string, value: string
318
327
  element.setAttribute(name, nextValue);
319
328
  }
320
329
 
330
+ function copySemanticDebugAttributes(source: HTMLElement, target: HTMLElement) {
331
+ [DEBUG_ATTR_ID, DEBUG_ATTR_PATH, DEBUG_ATTR_LINE, DEBUG_ATTR_FILE, DEBUG_ATTR_COMPONENT].forEach((attribute) => {
332
+ const value = trimText(source.getAttribute(attribute));
333
+ if (value) {
334
+ target.setAttribute(attribute, value);
335
+ }
336
+ });
337
+ }
338
+
321
339
  function toSyntheticNodeId(element: HTMLElement): string | null {
322
340
  const debugId = getSourceId(element);
323
341
  if (debugId) return `runtime:${debugId}`;
@@ -348,8 +366,87 @@ function annotateBaseSemanticNode(element: HTMLElement) {
348
366
  setSemanticAttribute(element, "data-imagicma-source-file", getSourceFile(element));
349
367
  }
350
368
 
351
- function hydrateRuntimeSemantics() {
352
- document.querySelectorAll<HTMLElement>(`[${DEBUG_ATTR_ID}]`).forEach(annotateBaseSemanticNode);
369
+ function isRuntimeTextSegmentElement(element: HTMLElement): boolean {
370
+ return element.getAttribute(RUNTIME_TEXT_SEGMENT_ATTR) === "true";
371
+ }
372
+
373
+ function getOwnedMeaningfulTextNodes(element: HTMLElement): Text[] {
374
+ const nodes: Text[] = [];
375
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
376
+ let current = walker.nextNode();
377
+ while (current) {
378
+ if (current instanceof Text && trimText(current.textContent).length > 0) {
379
+ const owner = current.parentElement?.closest<HTMLElement>("[data-imagicma-node-id]");
380
+ if (owner === element) {
381
+ nodes.push(current);
382
+ }
383
+ }
384
+ current = walker.nextNode();
385
+ }
386
+ return nodes;
387
+ }
388
+
389
+ function createRuntimeTextSegmentNodeId(element: HTMLElement, segmentIndex: number): string | null {
390
+ const ownerNodeId = getElementNodeId(element) || toSyntheticNodeId(element);
391
+ if (!ownerNodeId) return null;
392
+ return `${ownerNodeId}::text:${segmentIndex}`;
393
+ }
394
+
395
+ function annotateRuntimeTextSegments(element: HTMLElement) {
396
+ if (isRuntimeTextSegmentElement(element)) return;
397
+ if (!getSourceMetadata(element)) return;
398
+
399
+ const ownedTextNodes = getOwnedMeaningfulTextNodes(element);
400
+ const shouldSegmentText = ownedTextNodes.length > 1 || (ownedTextNodes.length > 0 && element.children.length > 0);
401
+ if (!shouldSegmentText) return;
402
+
403
+ ownedTextNodes.forEach((textNode, segmentIndex) => {
404
+ if (textNode.parentElement && isRuntimeTextSegmentElement(textNode.parentElement)) {
405
+ return;
406
+ }
407
+
408
+ const nodeId = createRuntimeTextSegmentNodeId(element, segmentIndex);
409
+ if (!nodeId || !textNode.parentNode) return;
410
+
411
+ const wrapper = document.createElement("span");
412
+ copySemanticDebugAttributes(element, wrapper);
413
+ setSemanticAttribute(wrapper, RUNTIME_TEXT_SEGMENT_ATTR, "true", true);
414
+ setSemanticAttribute(wrapper, RUNTIME_TEXT_SEGMENT_INDEX_ATTR, String(segmentIndex), true);
415
+ setSemanticAttribute(wrapper, "data-imagicma-node-id", nodeId, true);
416
+ setSemanticAttribute(wrapper, "data-imagicma-kind", "text", true);
417
+ setSemanticAttribute(wrapper, "data-imagicma-source-file", getSourceFile(element), true);
418
+ textNode.parentNode.insertBefore(wrapper, textNode);
419
+ wrapper.appendChild(textNode);
420
+ });
421
+ }
422
+
423
+ function buildNodeIdIndex(): Map<string, HTMLElement[]> {
424
+ const index = new Map<string, HTMLElement[]>();
425
+ document.querySelectorAll<HTMLElement>("[data-imagicma-node-id]").forEach((element) => {
426
+ const nodeId = getElementNodeId(element);
427
+ if (!nodeId) return;
428
+ const peers = index.get(nodeId);
429
+ if (peers) {
430
+ peers.push(element);
431
+ return;
432
+ }
433
+ index.set(nodeId, [element]);
434
+ });
435
+ return index;
436
+ }
437
+
438
+ function hydrateRuntimeSemantics(state?: RuntimeState) {
439
+ document.querySelectorAll<HTMLElement>(`[${DEBUG_ATTR_ID}]`).forEach((element) => {
440
+ if (isRuntimeTextSegmentElement(element)) return;
441
+ annotateBaseSemanticNode(element);
442
+ });
443
+ document.querySelectorAll<HTMLElement>(`[${DEBUG_ATTR_ID}]`).forEach((element) => {
444
+ if (isRuntimeTextSegmentElement(element)) return;
445
+ annotateRuntimeTextSegments(element);
446
+ });
447
+ if (state) {
448
+ state.nodeIdIndex = buildNodeIdIndex();
449
+ }
353
450
  }
354
451
 
355
452
  function getRepeatItemRoot(element: HTMLElement): HTMLElement | null {
@@ -406,6 +503,40 @@ function getElementNodeId(element: HTMLElement | null | undefined): string {
406
503
  return trimText(element?.getAttribute("data-imagicma-node-id"));
407
504
  }
408
505
 
506
+ function getRepeatItemScopeKey(element: HTMLElement | null | undefined): string | null {
507
+ const repeatRoot = element ? getRepeatItemRoot(element) : null;
508
+ const groupKey = trimText(repeatRoot?.getAttribute("data-imagicma-repeat-group"));
509
+ const itemKey = trimText(repeatRoot?.getAttribute("data-imagicma-sort-key"));
510
+ if (!groupKey || !itemKey) return null;
511
+ return `${groupKey}::${itemKey}`;
512
+ }
513
+
514
+ function getIndexedElementsByNodeKey(state: RuntimeState, nodeKey: string): HTMLElement[] {
515
+ const cached = state.nodeIdIndex.get(nodeKey)?.filter((element) => element.isConnected) ?? [];
516
+ if (cached.length > 0) return cached;
517
+
518
+ return Array.from(
519
+ document.querySelectorAll<HTMLElement>(
520
+ `[data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"]`,
521
+ ),
522
+ );
523
+ }
524
+
525
+ function isRepeatScopedNodeIdentity(peers: HTMLElement[]): boolean {
526
+ if (peers.length <= 1) return false;
527
+
528
+ const seenScopes = new Set<string>();
529
+ for (const peer of peers) {
530
+ const scopeKey = getRepeatItemScopeKey(peer);
531
+ if (!scopeKey || seenScopes.has(scopeKey)) {
532
+ return false;
533
+ }
534
+ seenScopes.add(scopeKey);
535
+ }
536
+
537
+ return seenScopes.size === peers.length;
538
+ }
539
+
409
540
  function getDirectSemanticSiblingItems(parent: HTMLElement | null): HTMLElement[] {
410
541
  if (!parent) return [];
411
542
 
@@ -606,6 +737,12 @@ function isSimpleTextElement(element: HTMLElement): boolean {
606
737
  return trimText(element.textContent).length > 0;
607
738
  }
608
739
 
740
+ function getRuntimeTextSegmentIndex(element: HTMLElement): number | null {
741
+ if (!isRuntimeTextSegmentElement(element)) return null;
742
+ const rawValue = Number(trimText(element.getAttribute(RUNTIME_TEXT_SEGMENT_INDEX_ATTR)));
743
+ return Number.isInteger(rawValue) && rawValue >= 0 ? rawValue : null;
744
+ }
745
+
609
746
  const INLINE_STYLE_BINDINGS: Array<{ field: PreviewSourceStyleField; cssName: string }> = [
610
747
  { field: "fontSize", cssName: "font-size" },
611
748
  { field: "fontWeight", cssName: "font-weight" },
@@ -651,7 +788,14 @@ function buildSourceBindings(element: HTMLElement, source: ReturnType<typeof get
651
788
  },
652
789
  };
653
790
 
654
- if (isSimpleTextElement(element)) {
791
+ const textSegmentIndex = getRuntimeTextSegmentIndex(element);
792
+ if (textSegmentIndex !== null) {
793
+ bindings.textContent = {
794
+ kind: "jsx-text-segment",
795
+ source,
796
+ segmentIndex: textSegmentIndex,
797
+ };
798
+ } else if (isSimpleTextElement(element)) {
655
799
  bindings.textContent = {
656
800
  kind: "jsx-text",
657
801
  source,
@@ -1617,7 +1761,7 @@ function applySortOverrides(state: RuntimeState, entry: PreviewOverridePageEntry
1617
1761
  }
1618
1762
 
1619
1763
  function reapplyVisualState(state: RuntimeState) {
1620
- hydrateRuntimeSemantics();
1764
+ hydrateRuntimeSemantics(state);
1621
1765
  resetDraftDom(state);
1622
1766
 
1623
1767
  const styleEl = ensureDraftStyleEl(state);
@@ -1648,18 +1792,33 @@ function reapplyVisualState(state: RuntimeState) {
1648
1792
  }
1649
1793
  }
1650
1794
 
1651
- function isNodeSelectableElement(element: HTMLElement): boolean {
1652
- return element.hasAttribute("data-imagicma-node-id");
1795
+ function isNodeSelectableElement(state: RuntimeState, element: HTMLElement): boolean {
1796
+ const nodeId = getElementNodeId(element);
1797
+ if (!nodeId) return false;
1798
+
1799
+ const peers = getIndexedElementsByNodeKey(state, nodeId);
1800
+ return peers.length <= 1 || isRepeatScopedNodeIdentity(peers);
1653
1801
  }
1654
1802
 
1655
1803
  function isOverlayElement(state: RuntimeState, element: HTMLElement): boolean {
1656
1804
  return Boolean(state.overlayRoot && state.overlayRoot.contains(element));
1657
1805
  }
1658
1806
 
1659
- function findElementByNodeKey(nodeKey: string): HTMLElement | null {
1660
- return document.querySelector<HTMLElement>(
1661
- `[data-imagicma-node-id="${escapeAttributeValue(nodeKey)}"]`,
1662
- );
1807
+ function findElementByNodeKey(state: RuntimeState, nodeKey: string): HTMLElement | null {
1808
+ const peers = getIndexedElementsByNodeKey(state, nodeKey);
1809
+ return peers.length === 1 ? peers[0] ?? null : null;
1810
+ }
1811
+
1812
+ function resolveSelectedElementForSync(state: RuntimeState, nodeKey: string | null): HTMLElement | null {
1813
+ const normalizedNodeKey = trimText(nodeKey);
1814
+ if (!normalizedNodeKey) return null;
1815
+
1816
+ const currentSelection = state.selectedElement;
1817
+ if (currentSelection?.isConnected && getElementNodeId(currentSelection) === normalizedNodeKey) {
1818
+ return currentSelection;
1819
+ }
1820
+
1821
+ return findElementByNodeKey(state, normalizedNodeKey);
1663
1822
  }
1664
1823
 
1665
1824
  function buildSemanticPath(target: HTMLElement, event: MouseEvent): HTMLElement[] {
@@ -1686,7 +1845,7 @@ function findSelectableElement(state: RuntimeState, event: MouseEvent): HTMLElem
1686
1845
  return null;
1687
1846
  }
1688
1847
 
1689
- return buildSemanticPath(hit, event).find((candidate) => !isOverlayElement(state, candidate) && isNodeSelectableElement(candidate)) ?? null;
1848
+ return buildSemanticPath(hit, event).find((candidate) => !isOverlayElement(state, candidate) && isNodeSelectableElement(state, candidate)) ?? null;
1690
1849
  }
1691
1850
 
1692
1851
  function postToParent(state: RuntimeState, message: Record<string, unknown>) {
@@ -1707,6 +1866,7 @@ function createRuntimeState(): RuntimeState {
1707
1866
  parentOrigin: getBoundPreviewParentOrigin(),
1708
1867
  activeSessionId: null,
1709
1868
  enabled: false,
1869
+ nodeIdIndex: new Map(),
1710
1870
  selectedElement: null,
1711
1871
  hoveredElement: null,
1712
1872
  overlayRoot: null,
@@ -1768,7 +1928,7 @@ export function installPreviewPickerRuntime() {
1768
1928
  parentOrigin: state.parentOrigin,
1769
1929
  frameInstanceId: state.frameInstanceId,
1770
1930
  });
1771
- hydrateRuntimeSemantics();
1931
+ hydrateRuntimeSemantics(state);
1772
1932
  state.throttledRecalculate = throttleRAF(() => {
1773
1933
  reapplyVisualState(state);
1774
1934
  });
@@ -1822,7 +1982,7 @@ export function installPreviewPickerRuntime() {
1822
1982
  window.addEventListener("resize", () => state.throttledRecalculate?.());
1823
1983
  state.mutationObserver = new MutationObserver(() => {
1824
1984
  if (state.suppressMutationObserver > 0) return;
1825
- hydrateRuntimeSemantics();
1985
+ hydrateRuntimeSemantics(state);
1826
1986
  state.throttledRecalculate?.();
1827
1987
  });
1828
1988
  state.mutationObserver.observe(document.body, {
@@ -1940,7 +2100,7 @@ export function installPreviewPickerRuntime() {
1940
2100
  if (event.data.frameInstanceId !== state.frameInstanceId) return;
1941
2101
 
1942
2102
  if (event.data.type === "IMAGICMA_PICKER_STATE_SYNC") {
1943
- hydrateRuntimeSemantics();
2103
+ hydrateRuntimeSemantics(state);
1944
2104
  state.activeSessionId = event.data.sessionId;
1945
2105
  state.enabled = event.data.payload.mode === "picking";
1946
2106
  state.persistedOverrides = event.data.payload.overrides;
@@ -1952,9 +2112,7 @@ export function installPreviewPickerRuntime() {
1952
2112
  orderedSortKeys: event.data.payload.pendingSort.orderedSortKeys,
1953
2113
  }
1954
2114
  : null;
1955
- state.selectedElement = event.data.payload.selectedNodeId
1956
- ? findElementByNodeKey(event.data.payload.selectedNodeId)
1957
- : null;
2115
+ state.selectedElement = resolveSelectedElementForSync(state, event.data.payload.selectedNodeId);
1958
2116
  state.hoveredElement = null;
1959
2117
  clearOverlay(state);
1960
2118
  const sortableMetadata = state.selectedElement ? getSortableMetadata(state.selectedElement) : null;
@@ -8,6 +8,11 @@ const LAUNCH_TOKEN_FILE = path.resolve(
8
8
  ".imagicma",
9
9
  "launch-token.json",
10
10
  );
11
+ const RUNTIME_ENV_FILE = path.resolve(
12
+ process.cwd(),
13
+ ".imagicma",
14
+ "runtime.env",
15
+ );
11
16
 
12
17
  function isScriptLaunch(mode: "dev" | "start") {
13
18
  return (
@@ -51,18 +56,47 @@ async function assertStartAuthorized() {
51
56
  );
52
57
  }
53
58
 
54
- function resolveRuntimePort(raw = process.env.PORT) {
55
- if (raw === undefined || raw === null || raw === "") {
56
- throw new Error("[imagicma] 缺少端口配置:请通过 PORT 提供运行时端口");
59
+ async function readRuntimeEnvPort() {
60
+ try {
61
+ const raw = await fs.readFile(RUNTIME_ENV_FILE, "utf8");
62
+ const lines = raw.split(/\r?\n/);
63
+ for (const line of lines) {
64
+ const trimmed = line.trim();
65
+ if (!trimmed || trimmed.startsWith("#")) continue;
66
+ const separatorIndex = trimmed.indexOf("=");
67
+ if (separatorIndex <= 0) continue;
68
+ const key = trimmed.slice(0, separatorIndex).trim();
69
+ if (key !== "PORT") continue;
70
+ return trimmed.slice(separatorIndex + 1).trim();
71
+ }
72
+ } catch (error) {
73
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
74
+ return null;
75
+ }
76
+ throw error;
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ async function resolveRuntimePort(raw = process.env.PORT) {
83
+ let candidate = raw;
84
+ if (candidate === undefined || candidate === null || candidate === "") {
85
+ candidate = await readRuntimeEnvPort();
86
+ }
87
+
88
+ if (candidate === undefined || candidate === null || candidate === "") {
89
+ throw new Error("[imagicma] 缺少端口配置:请通过 .imagicma/runtime.env 或 PORT 提供运行时端口");
57
90
  }
58
91
 
59
- const port = Number(raw);
92
+ const port = Number(candidate);
60
93
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
61
94
  throw new Error(
62
- `[imagicma] 无效端口配置:PORT=${JSON.stringify(raw)}(期望 1-65535 的整数)`,
95
+ `[imagicma] 无效端口配置:PORT=${JSON.stringify(candidate)}(期望 1-65535 的整数)`,
63
96
  );
64
97
  }
65
98
 
99
+ process.env.PORT = String(port);
66
100
  return port;
67
101
  }
68
102
 
@@ -70,7 +104,7 @@ async function main() {
70
104
  await assertStartAuthorized();
71
105
 
72
106
  const app = createApp({ serveClient: true });
73
- const port = resolveRuntimePort();
107
+ const port = await resolveRuntimePort();
74
108
 
75
109
  serve(
76
110
  {
@@ -14,6 +14,11 @@ const LAUNCH_TOKEN_FILE = path.resolve(
14
14
  ".imagicma",
15
15
  "launch-token.json",
16
16
  );
17
+ const RUNTIME_ENV_FILE = path.resolve(
18
+ __dirname,
19
+ ".imagicma",
20
+ "runtime.env",
21
+ );
17
22
 
18
23
  function isScriptLaunch(mode: "dev" | "start") {
19
24
  return (
@@ -22,18 +27,47 @@ function isScriptLaunch(mode: "dev" | "start") {
22
27
  );
23
28
  }
24
29
 
25
- function resolveRuntimePort(raw = process.env.PORT) {
26
- if (raw === undefined || raw === null || raw === "") {
27
- throw new Error("[imagicma] 缺少端口配置:请通过 PORT 提供运行时端口");
30
+ async function readRuntimeEnvPort() {
31
+ try {
32
+ const raw = await fs.readFile(RUNTIME_ENV_FILE, "utf8");
33
+ const lines = raw.split(/\r?\n/);
34
+ for (const line of lines) {
35
+ const trimmed = line.trim();
36
+ if (!trimmed || trimmed.startsWith("#")) continue;
37
+ const separatorIndex = trimmed.indexOf("=");
38
+ if (separatorIndex <= 0) continue;
39
+ const key = trimmed.slice(0, separatorIndex).trim();
40
+ if (key !== "PORT") continue;
41
+ return trimmed.slice(separatorIndex + 1).trim();
42
+ }
43
+ } catch (error) {
44
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
45
+ return null;
46
+ }
47
+ throw error;
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ async function resolveRuntimePort(raw = process.env.PORT) {
54
+ let candidate = raw;
55
+ if (candidate === undefined || candidate === null || candidate === "") {
56
+ candidate = await readRuntimeEnvPort();
57
+ }
58
+
59
+ if (candidate === undefined || candidate === null || candidate === "") {
60
+ throw new Error("[imagicma] 缺少端口配置:请通过 .imagicma/runtime.env 或 PORT 提供运行时端口");
28
61
  }
29
62
 
30
- const port = Number(raw);
63
+ const port = Number(candidate);
31
64
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
32
65
  throw new Error(
33
- `[imagicma] 无效端口配置:PORT=${JSON.stringify(raw)}(期望 1-65535 的整数)`,
66
+ `[imagicma] 无效端口配置:PORT=${JSON.stringify(candidate)}(期望 1-65535 的整数)`,
34
67
  );
35
68
  }
36
69
 
70
+ process.env.PORT = String(port);
37
71
  return port;
38
72
  }
39
73
 
@@ -73,7 +107,7 @@ async function assertLaunchAuthorized(mode: "dev" | "start") {
73
107
  }
74
108
 
75
109
  export default defineConfig(async ({ command }) => {
76
- const runtimePort = command === "serve" ? resolveRuntimePort() : null;
110
+ const runtimePort = command === "serve" ? await resolveRuntimePort() : null;
77
111
  const enableComponentDebugger = command === "serve";
78
112
  const componentDebugger =
79
113
  enableComponentDebugger