@microi.net/cli 6.1.7 → 6.1.9
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/.codebuddy-plugin/marketplace.json +2 -2
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.json +1 -1
- package/.workbuddy-plugin/marketplace.json +2 -2
- package/.workbuddy-plugin/plugin.json +1 -1
- package/assets/build-meta.json +4 -4
- package/cordis.patch.yml +1 -1
- package/package.json +1 -1
- package/scripts/codex-marketplace.json +20 -20
- package/scripts/mcp-codex-stdio-adapter.js +189 -189
- package/scripts/mcp-server.js +101 -96
- package/scripts/mcp-trae-windows-launcher.cmd +21 -21
- package/scripts/microi-cli-mcp.js +7 -7
- package/scripts/microi-skills.meta.json +257 -239
- package/skills/.microi-skills-version.json +2 -2
- package/skills/.progressive-disclosure-manifest.json +90 -68
- package/skills/ai-engine/SKILL.md +303 -293
- package/skills/ai-engine/references/ai-employees.md +4 -0
- package/skills/app-store/SKILL.md +3 -1
- package/skills/job-engine/SKILL.md +12 -0
- package/skills/message-notification/SKILL.md +2 -0
- package/skills/microi-client-frontend/SKILL.md +260 -252
- package/skills/microi-codex/SKILL.md +8 -0
- package/skills/microi-datasource-mapping/SKILL.md +4 -0
- package/skills/microi-deployment/SKILL.md +85 -5
- package/skills/microi-deployment/references/deployment-matrix.md +27 -0
- package/skills/microi-deployment/references/offline-installer.md +64 -0
- package/skills/microi-docs-coverage/references/capability-map.md +3 -3
- package/skills/microi-form-engine/SKILL.md +13 -0
- package/skills/microi-form-engine/references/component-catalog.md +1 -1
- package/skills/microi-frontend-sdk/SKILL.md +12 -0
- package/skills/microi-microservice/SKILL.md +18 -1
- package/skills/microi-system-delivery/SKILL.md +8 -6
- package/skills/microi-system-delivery/references/delivery-pitfalls.md +50 -0
- package/skills/microi-system-delivery/references/progressive-01-/346/240/207/345/207/206/345/267/245/344/275/234/346/265/201.md +11 -9
- package/skills/microi-uniapp-frontend/SKILL.md +18 -3
- package/skills/microi.v8.js +445 -0
- package/skills/module-engine/SKILL.md +4 -5
- package/skills/page-engine/SKILL.md +233 -215
- package/skills/print-engine/SKILL.md +5 -0
- package/skills/report-engine/SKILL.md +9 -0
- package/skills/report-engine/scripts/check-demo-coverage.mjs +88 -0
- package/skills/report-engine/scripts/check-demo-coverage.test.mjs +52 -0
- package/skills/scripts/sync-embedded-skills.mjs +4 -1
- package/skills/scripts/sync-public-resource-router.mjs +29 -0
- package/skills/system-observability/SKILL.md +18 -2
- package/skills/ui-design/SKILL.md +1 -1
- package/skills/uniapp-mall-assets/SKILL.md +5 -0
- package/skills/v8-file-upload/SKILL.md +32 -31
- package/skills/v8-file-upload/references/hotspot-offload-operations.md +118 -0
- package/skills/v8-saas-multi-tenant/SKILL.md +17 -4
- package/skills/v8-security/SKILL.md +20 -5
- package/skills/vision-engine/SKILL.md +114 -3
- package/skills/vision-engine/references/architecture-and-acceptance.md +8 -0
- package/skills/workspace-conventions/SKILL.md +9 -3
- package/skills/workspace-conventions/references/progressive-01-/347/211/210/346/234/254/346/233/264/346/226/260/346/227/245/345/277/227/344/277/235/346/212/244/350/247/204/345/210/231-/345/274/272/345/210/266.md +4 -4
- package/skills/workspace-conventions/references/progressive-04-/345/267/245/344/275/234/345/214/272/344/270/216/345/271/263/345/217/260/344/270/223/351/241/271/344/277/235/351/232/234.md +22 -1
package/skills/microi.v8.js
CHANGED
|
@@ -569,12 +569,394 @@ function installDatePrototypeCompat() {
|
|
|
569
569
|
}
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
+
// BEGIN GENERATED PUBLIC RESOURCE ROUTER; source SHA256 d90207991f19b46429dc21a654e2915e1b4fd6e96f191cede20485d823f3a975
|
|
573
|
+
const createSdkPublicResourceClient = (() => {
|
|
574
|
+
// 公有资源路由的纯校验器与实际消费者桥接;私有分支继续走既有授权接口。
|
|
575
|
+
const HASH = /^[a-f0-9]{64}$/;
|
|
576
|
+
const IDENTIFIER = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
577
|
+
const BLOCKED = /^(?:\.git|\.env|private|private-source|privatesource|source|_origin)$/i;
|
|
578
|
+
|
|
579
|
+
function key(value) {
|
|
580
|
+
if (typeof value !== 'string' || !value || value.length > 1000 || /[\\%?#:\u0000-\u001f\u007f]/.test(value)
|
|
581
|
+
|| value.split('/').some(part => !part || part === '.' || part === '..' || BLOCKED.test(part)) || /\.map$/i.test(value)) throw Error('InvalidPublicObjectKey');
|
|
582
|
+
encodeURIComponent(value); // Reject malformed UTF-16 rather than changing the key.
|
|
583
|
+
return value;
|
|
584
|
+
}
|
|
585
|
+
const encodeKey = value => value.split('/').map(encodeURIComponent).join('/');
|
|
586
|
+
function plainBase(value, originOnly = false) {
|
|
587
|
+
if (typeof value !== 'string' || /[\\%?#@\u0000-\u0020\u007f]/.test(value)) throw Error('InvalidPublicBinding');
|
|
588
|
+
const parsed = new URL(value);
|
|
589
|
+
if (parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.search || parsed.hash
|
|
590
|
+
|| (originOnly && parsed.pathname !== '/')) throw Error('InvalidPublicBinding');
|
|
591
|
+
return value.replace(/\/+$/, '');
|
|
592
|
+
}
|
|
593
|
+
function bindingSnapshot(binding) {
|
|
594
|
+
if (!binding || !IDENTIFIER.test(binding.tenant) || !IDENTIFIER.test(binding.storageOwnerTenant)
|
|
595
|
+
|| !HASH.test(binding.targetConfigHash) || !/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(binding.publicBucket)
|
|
596
|
+
|| binding.publicBucket.includes('..') || binding.publicBucket === binding.privateBucket) throw Error('InvalidPublicBinding');
|
|
597
|
+
const value = { tenant: binding.tenant, storageOwnerTenant: binding.storageOwnerTenant.toLowerCase(),
|
|
598
|
+
primaryBase: plainBase(binding.primaryBase), backupOrigin: plainBase(binding.backupOrigin, true),
|
|
599
|
+
publicBucket: binding.publicBucket, targetConfigHash: binding.targetConfigHash,
|
|
600
|
+
sourceMappings: (binding.sourceMappings || []).map(item => ({ origin: plainBase(item.origin, true),
|
|
601
|
+
pathPrefix: item.pathPrefix ? key(item.pathPrefix.replace(/\/+$/, '')) + '/' : '' })) };
|
|
602
|
+
if (!value.sourceMappings.length || value.sourceMappings.length > 20) throw Error('InvalidPublicBinding');
|
|
603
|
+
return { ...value, identity: JSON.stringify(value) };
|
|
604
|
+
}
|
|
605
|
+
function legacyPublicFallback(input, binding) {
|
|
606
|
+
if (!input || input.private === true || typeof input.path !== 'string') return '';
|
|
607
|
+
const path = input.path;
|
|
608
|
+
try {
|
|
609
|
+
if (/^https?:/i.test(path)) {
|
|
610
|
+
const url = new URL(path);
|
|
611
|
+
return !/[\\\u0000-\u0020\u007f]/.test(path) && !url.username && !url.password ? path : '';
|
|
612
|
+
}
|
|
613
|
+
if (path.startsWith('//') || path.startsWith('.')) return '';
|
|
614
|
+
if (/[\\\u0000-\u001f\u007f]/.test(path)) return '';
|
|
615
|
+
const relative = path.startsWith('/') ? path.slice(1) : path;
|
|
616
|
+
const decoratedAt = relative.search(/[?#]/);
|
|
617
|
+
const rawKey = decoratedAt < 0 ? relative : relative.slice(0, decoratedAt);
|
|
618
|
+
const suffix = decoratedAt < 0 ? '' : relative.slice(decoratedAt);
|
|
619
|
+
const objectKey = key(rawKey.split('/').map(part => decodeURIComponent(part)).join('/'));
|
|
620
|
+
return plainBase(binding?.primaryBase) + '/' + encodeKey(objectKey) + suffix;
|
|
621
|
+
} catch { return ''; }
|
|
622
|
+
}
|
|
623
|
+
function identify(input, binding) {
|
|
624
|
+
if (!input || input.private === true) return { input, accepted: false, reason: 'PrivateRoutingNotSupported', primaryUrl: '' };
|
|
625
|
+
const path = input.path;
|
|
626
|
+
try {
|
|
627
|
+
if (typeof path !== 'string' || !path) throw Error('InvalidInput');
|
|
628
|
+
if (/^https?:/i.test(path)) {
|
|
629
|
+
if (/[\\?#@\u0000-\u001f\u007f]/.test(path)) throw Error('SignedOrDecoratedUrlNotEligible');
|
|
630
|
+
const parsed = new URL(path);
|
|
631
|
+
const mapping = binding.sourceMappings.find(value => new URL(value.origin).origin === parsed.origin);
|
|
632
|
+
if (!mapping || parsed.protocol !== 'https:') throw Error('UnmanagedOrigin');
|
|
633
|
+
// Check raw path before URL() can normalize dot segments.
|
|
634
|
+
const begin = path.indexOf('/', path.indexOf('://') + 3);
|
|
635
|
+
if (begin < 0) throw Error('ObjectKeyMissing');
|
|
636
|
+
const objectKey = key(path.slice(begin + 1).split('/').map(part => {
|
|
637
|
+
const decoded = decodeURIComponent(part);
|
|
638
|
+
if (/[\/%\\?#:\u0000-\u001f\u007f]/.test(decoded)) throw Error('InvalidEncodedPath');
|
|
639
|
+
return decoded;
|
|
640
|
+
}).join('/'));
|
|
641
|
+
if (!objectKey.startsWith(mapping.pathPrefix)) throw Error('BucketPrefixMismatch');
|
|
642
|
+
return { accepted: true, objectKey: key(objectKey.slice(mapping.pathPrefix.length)), primaryUrl: path };
|
|
643
|
+
}
|
|
644
|
+
// Protocol-relative, traversal and double-slash paths are never interpreted
|
|
645
|
+
// as the configured public bucket. A single leading slash is legacy input.
|
|
646
|
+
if (path.startsWith('//') || path.startsWith('.')) throw Error('UnmanagedRelativePath');
|
|
647
|
+
const objectKey = key(path.startsWith('/') ? path.slice(1) : path);
|
|
648
|
+
return { accepted: true, objectKey, primaryUrl: binding.primaryBase + '/' + encodeKey(objectKey) };
|
|
649
|
+
} catch (error) { return { accepted: false, reason: error.message, primaryUrl: legacyPublicFallback(input, binding) }; }
|
|
650
|
+
}
|
|
651
|
+
function original(item, reason) {
|
|
652
|
+
return Object.freeze({ useBackup: false, url: item.primaryUrl, primaryUrl: item.primaryUrl,
|
|
653
|
+
objectKey: item.objectKey || null, reason });
|
|
654
|
+
}
|
|
655
|
+
function acceptedRoute(row, item, binding, time) {
|
|
656
|
+
if (!row || row.UseBackup !== true || row.ObjectKey !== item.objectKey || !HASH.test(row.SourceRevision)
|
|
657
|
+
|| !HASH.test(row.ResourceId) || row.TargetConfigHash !== binding.targetConfigHash
|
|
658
|
+
|| !Number.isSafeInteger(row.ExpiresAtUtc) || row.ExpiresAtUtc * 1000 <= time
|
|
659
|
+
|| row.ExpiresAtUtc * 1000 > time + 300000 || typeof row.Url !== 'string' || /[?#@\\]/.test(row.Url)) return null;
|
|
660
|
+
const prefix = binding.backupOrigin + '/' + binding.publicBucket + '/_microi-hotspot/public/v3/'
|
|
661
|
+
+ binding.storageOwnerTenant + '/' + row.ResourceId + '/' + row.SourceRevision + '/';
|
|
662
|
+
if (!row.Url.startsWith(prefix)) return null;
|
|
663
|
+
const rest = row.Url.slice(prefix.length), slash = rest.indexOf('/');
|
|
664
|
+
if (!/^[a-f0-9]{32}$/.test(rest.slice(0, slash)) || rest.slice(slash + 1) !== encodeKey(item.objectKey)) return null;
|
|
665
|
+
return Object.freeze({ useBackup: true, url: row.Url, primaryUrl: item.primaryUrl, objectKey: item.objectKey,
|
|
666
|
+
expiresAt: row.ExpiresAtUtc * 1000, reason: 'VerifiedPublicReplica' });
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function createPublicResourceRouter({ getBinding, resolveBatch, now = Date.now, maxEntries = 512, timeoutMs = 5000 }) {
|
|
670
|
+
if (typeof getBinding !== 'function' || typeof resolveBatch !== 'function' || !Number.isInteger(maxEntries)
|
|
671
|
+
|| maxEntries < 1 || maxEntries > 10000 || !Number.isInteger(timeoutMs)
|
|
672
|
+
|| timeoutMs < 1 || timeoutMs > 30000) throw Error('InvalidRouterDependencies');
|
|
673
|
+
const cache = new Map(), pending = new Map(), cooldown = new Map();
|
|
674
|
+
const cacheKey = (binding, item) => binding.identity + '\n' + item.objectKey;
|
|
675
|
+
const put = (map, id, value) => { map.delete(id); map.set(id, value); while (map.size > maxEntries) map.delete(map.keys().next().value); };
|
|
676
|
+
function cached(binding, item) {
|
|
677
|
+
const id = cacheKey(binding, item), time = now();
|
|
678
|
+
if ((cooldown.get(id) || 0) > time) return original(item, 'BackupFailureCooldown');
|
|
679
|
+
const hit = cache.get(id);
|
|
680
|
+
if (hit && hit.until > time) return hit.route.useBackup ? Object.freeze({ ...hit.route, primaryUrl: item.primaryUrl }) : original(item, hit.route.reason);
|
|
681
|
+
cache.delete(id); cooldown.delete(id); return null;
|
|
682
|
+
}
|
|
683
|
+
function snapshot() { return bindingSnapshot(getBinding()); }
|
|
684
|
+
function fallback(input) { try { return legacyPublicFallback(input, getBinding()); } catch { return legacyPublicFallback(input, null); } }
|
|
685
|
+
|
|
686
|
+
async function resolveMany(inputs) {
|
|
687
|
+
if (!Array.isArray(inputs) || inputs.length > 100) throw Error('ResourceBatchLimitExceeded');
|
|
688
|
+
let binding;
|
|
689
|
+
try { binding = snapshot(); }
|
|
690
|
+
catch { return inputs.map(item => original({ primaryUrl: fallback(item) }, 'BackupBindingUnavailable')); }
|
|
691
|
+
const work = [], created = [];
|
|
692
|
+
const tasks = inputs.map(input => {
|
|
693
|
+
const item = identify(input, binding);
|
|
694
|
+
if (!item.accepted) return Promise.resolve(original(item, item.reason));
|
|
695
|
+
const known = cached(binding, item); if (known) return Promise.resolve(known);
|
|
696
|
+
const id = cacheKey(binding, item);
|
|
697
|
+
if (pending.has(id)) return pending.get(id).then(route => Object.freeze({ ...route, primaryUrl: item.primaryUrl,
|
|
698
|
+
url: route.useBackup ? route.url : item.primaryUrl }));
|
|
699
|
+
let done; const promise = new Promise(resolve => { done = resolve; });
|
|
700
|
+
pending.set(id, promise); work.push(item); created.push({ item, id, done, promise }); return promise;
|
|
701
|
+
});
|
|
702
|
+
if (work.length) {
|
|
703
|
+
// Do not pass secrets, arbitrary URLs or a caller-selected storage endpoint.
|
|
704
|
+
const controller = new AbortController(); let timer;
|
|
705
|
+
Promise.race([
|
|
706
|
+
Promise.resolve().then(() => resolveBatch({ ObjectKeys: work.map(item => item.objectKey), OsClient: binding.tenant }, { signal: controller.signal })),
|
|
707
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(Error('ResolverTimeout')), timeoutMs); })
|
|
708
|
+
])
|
|
709
|
+
.then(response => {
|
|
710
|
+
let current; try { current = snapshot(); } catch { current = null; }
|
|
711
|
+
const data = response?.Code === 1 ? response.Data : null;
|
|
712
|
+
const valid = current?.identity === binding.identity && data?.Protocol === 'GenericPublicResourceRoutes/1'
|
|
713
|
+
&& data.TargetConfigHash === binding.targetConfigHash && Array.isArray(data.Routes) && data.Routes.length <= 100;
|
|
714
|
+
const rows = new Map(), duplicates = new Set();
|
|
715
|
+
if (valid) for (const row of data.Routes) {
|
|
716
|
+
if (rows.has(row?.ObjectKey)) duplicates.add(row.ObjectKey);
|
|
717
|
+
rows.set(row?.ObjectKey, row);
|
|
718
|
+
}
|
|
719
|
+
for (const job of created) {
|
|
720
|
+
const time = now();
|
|
721
|
+
const route = valid && !duplicates.has(job.item.objectKey)
|
|
722
|
+
? acceptedRoute(rows.get(job.item.objectKey), job.item, binding, time) : null;
|
|
723
|
+
const selected = (cooldown.get(job.id) || 0) > time ? original(job.item, 'BackupFailureCooldown')
|
|
724
|
+
: route || original(job.item, current?.identity !== binding.identity ? 'TenantOrBindingChanged' : 'VerifiedRouteUnavailable');
|
|
725
|
+
put(cache, job.id, { route: selected, until: selected.useBackup ? selected.expiresAt : time + 15000 });
|
|
726
|
+
job.done(selected);
|
|
727
|
+
}
|
|
728
|
+
})
|
|
729
|
+
.catch(() => {
|
|
730
|
+
for (const job of created) { const route = original(job.item, 'ResolverUnavailable');
|
|
731
|
+
put(cache, job.id, { route, until: now() + 15000 }); job.done(route); }
|
|
732
|
+
})
|
|
733
|
+
.finally(() => { clearTimeout(timer); controller.abort();
|
|
734
|
+
for (const job of created) if (pending.get(job.id) === job.promise) pending.delete(job.id); });
|
|
735
|
+
}
|
|
736
|
+
return Promise.all(tasks);
|
|
737
|
+
}
|
|
738
|
+
return Object.freeze({
|
|
739
|
+
resolveMany,
|
|
740
|
+
async resolve(input) { return (await resolveMany([input]))[0]; },
|
|
741
|
+
// Sync compatibility is cache-only. Callers must pre-resolve before assigning
|
|
742
|
+
// src/href if their FIRST request must avoid the original CDN/OSS endpoint.
|
|
743
|
+
peek(input) {
|
|
744
|
+
try { const binding = snapshot(), item = identify(input, binding);
|
|
745
|
+
return item.accepted ? cached(binding, item) || original(item, 'NotResolvedYet') : original(item, item.reason); }
|
|
746
|
+
catch { return original({ primaryUrl: fallback(input) }, 'BackupBindingUnavailable'); }
|
|
747
|
+
},
|
|
748
|
+
reportFailure(input) {
|
|
749
|
+
try { const binding = snapshot(), item = identify(input, binding);
|
|
750
|
+
if (!item.accepted) return original(item, item.reason);
|
|
751
|
+
const id = cacheKey(binding, item); cache.delete(id); put(cooldown, id, now() + 30000);
|
|
752
|
+
return original(item, 'BackupFailureCooldown'); }
|
|
753
|
+
catch { return original({ primaryUrl: fallback(input) }, 'BackupBindingUnavailable'); }
|
|
754
|
+
},
|
|
755
|
+
clear() { cache.clear(); cooldown.clear(); }
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* DiyCommon 与真实批量解析器的桥接。配置只从当前租户的原生 Binding 回读,
|
|
761
|
+
* 不将 SaaS 私桶名/凭据塞入前端。首次 src 赋值必须等待 resolve。
|
|
762
|
+
*/
|
|
763
|
+
function createExactAliasRouter({ getBinding, post, now }) {
|
|
764
|
+
const cache = new Map(), pending = new Map(), cooldown = new Map(); let queue = [], scheduled = false;
|
|
765
|
+
const put = (map, id, value) => { map.delete(id); map.set(id, value); while (map.size > 512) map.delete(map.keys().next().value); };
|
|
766
|
+
function pick(input) {
|
|
767
|
+
try {
|
|
768
|
+
const raw = getBinding(), b = bindingSnapshot(raw);
|
|
769
|
+
if (input?.private !== false || raw.aliasUrlRoutingProtocol !== 'PublicSourceUrlRoutes/1'
|
|
770
|
+
|| typeof input.path !== 'string' || !input.path.startsWith('https://') || /[\\?#@\u0000-\u0020\u007f]/.test(input.path)) return null;
|
|
771
|
+
const origin = new URL(input.path).origin;
|
|
772
|
+
if (!raw.aliasCandidates.includes(origin)) return null;
|
|
773
|
+
// Only parse the exact key here. Candidate membership is not authorization:
|
|
774
|
+
// the server must revalidate this complete SourceUrl before returning NAS.
|
|
775
|
+
const item = identify(input, { ...b, sourceMappings: [{ origin, pathPrefix: '' }] });
|
|
776
|
+
return item.accepted ? { ...item, input, origin, binding: b, id: b.identity + '\nURL:' + input.path } : null;
|
|
777
|
+
} catch { return null; }
|
|
778
|
+
}
|
|
779
|
+
async function flush() {
|
|
780
|
+
scheduled = false; const jobs = queue; queue = [];
|
|
781
|
+
while (jobs.length) {
|
|
782
|
+
const origins = new Set(), batch = [];
|
|
783
|
+
const identity = jobs[0].item.binding.identity;
|
|
784
|
+
for (let index = 0; index < jobs.length && batch.length < 100;) {
|
|
785
|
+
const job = jobs[index];
|
|
786
|
+
if (job.item.binding.identity !== identity) { index++; continue; }
|
|
787
|
+
if (!origins.has(job.item.origin) && origins.size === 2) { index++; continue; }
|
|
788
|
+
origins.add(job.item.origin); batch.push(...jobs.splice(index, 1));
|
|
789
|
+
}
|
|
790
|
+
let response, timer;
|
|
791
|
+
try { response = await Promise.race([post({ PublicUrls: batch.map(x => x.item.input.path), OsClient: batch[0].item.binding.tenant }),
|
|
792
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(Error('AliasResolverTimeout')), 5000); })]); }
|
|
793
|
+
catch { response = null; }
|
|
794
|
+
finally { clearTimeout(timer); }
|
|
795
|
+
for (const job of batch) {
|
|
796
|
+
const item = job.item, current = pick(item.input), time = now(), data = response?.Code === 1 ? response.Data : null;
|
|
797
|
+
const rows = data?.Protocol === 'PublicSourceUrlRoutes/1' && data.TargetConfigHash === item.binding.targetConfigHash
|
|
798
|
+
&& Array.isArray(data.Routes) && data.Routes.length <= 100
|
|
799
|
+
? data.Routes.filter(x => x && typeof x === 'object' && !Array.isArray(x) && x.SourceUrl === item.input.path) : [];
|
|
800
|
+
const row = rows.length === 1 ? rows[0] : null;
|
|
801
|
+
const accepted = current?.id === item.id && row?.SourceOrigin === item.origin && HASH.test(row.SourceRoutingFingerprint)
|
|
802
|
+
&& row.ExpiresAtUtc * 1000 <= time + 60000 ? acceptedRoute(row, item, item.binding, time) : null;
|
|
803
|
+
const route = (cooldown.get(item.id) || 0) > time ? original(item, 'BackupFailureCooldown')
|
|
804
|
+
: accepted || original(item, 'VerifiedAliasRouteUnavailable');
|
|
805
|
+
put(cache, item.id, { route, until: route.useBackup ? route.expiresAt : time + 15000 });
|
|
806
|
+
pending.delete(item.id); job.done(route);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
eligible: input => !!pick(input),
|
|
812
|
+
async resolve(input) {
|
|
813
|
+
const item = pick(input); if (!item) return null;
|
|
814
|
+
if ((cooldown.get(item.id) || 0) > now()) return original(item, 'BackupFailureCooldown');
|
|
815
|
+
const old = cache.get(item.id); if (old && old.until > now()) return old.route;
|
|
816
|
+
if (pending.has(item.id)) return pending.get(item.id);
|
|
817
|
+
let done; const result = new Promise(resolve => { done = resolve; }); pending.set(item.id, result);
|
|
818
|
+
queue.push({ item, done }); if (!scheduled) { scheduled = true; queueMicrotask(flush); } return result;
|
|
819
|
+
},
|
|
820
|
+
reportFailure(input) { const item = pick(input); if (!item) return null;
|
|
821
|
+
cache.delete(item.id); put(cooldown, item.id, now() + 30000); return original(item, 'BackupFailureCooldown'); },
|
|
822
|
+
clear() { cache.clear(); cooldown.clear(); }
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function createPublicResourceClient({ getContext, post, now = Date.now, fetchHead = globalThis.fetch }) {
|
|
827
|
+
let active = '', binding = null, bootstrap = null, expires = 0, generation = 0;
|
|
828
|
+
let queue = [], scheduled = false;
|
|
829
|
+
const context = () => {
|
|
830
|
+
const value = getContext() || {};
|
|
831
|
+
return { tenant: value.tenant, primaryBase: value.primaryBase?.replace(/\/+$/, ''),
|
|
832
|
+
backupOrigin: value.backupOrigin?.replace(/\/+$/, '') };
|
|
833
|
+
};
|
|
834
|
+
const signature = value => JSON.stringify(value);
|
|
835
|
+
const router = createPublicResourceRouter({ getBinding: () => binding || context(), resolveBatch: data => post(data), now });
|
|
836
|
+
const aliases = createExactAliasRouter({ getBinding: () => binding, post, now });
|
|
837
|
+
const fallback = input => Object.freeze({ useBackup: false,
|
|
838
|
+
url: legacyPublicFallback(input, context()), primaryUrl: legacyPublicFallback(input, context()), reason: 'BindingUnavailable' });
|
|
839
|
+
async function ensureBinding() {
|
|
840
|
+
const current = context(), id = signature(current);
|
|
841
|
+
if (active !== id) { active = id; binding = null; bootstrap = null; expires = 0; generation++; router.clear(); aliases.clear(); }
|
|
842
|
+
if (binding && expires > now()) return true;
|
|
843
|
+
if (!binding && expires > now()) return false;
|
|
844
|
+
if (bootstrap) return bootstrap;
|
|
845
|
+
if (!current.backupOrigin || !IDENTIFIER.test(current.tenant || '')) return false;
|
|
846
|
+
const revision = generation;
|
|
847
|
+
const run = (async () => {
|
|
848
|
+
let timer;
|
|
849
|
+
try {
|
|
850
|
+
const response = await Promise.race([post({ ObjectKeys: [], OsClient: current.tenant }),
|
|
851
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(Error('BindingTimeout')), 5000); })]);
|
|
852
|
+
const data = response?.Data, source = data?.Binding;
|
|
853
|
+
if (generation !== revision || signature(context()) !== id || response?.Code !== 1 || data?.Protocol !== 'GenericPublicResourceRoutes/1' || !source
|
|
854
|
+
|| source.PrimaryBase?.replace(/\/+$/, '') !== current.primaryBase
|
|
855
|
+
|| source.BackupOrigin?.replace(/\/+$/, '') !== current.backupOrigin
|
|
856
|
+
|| source.TargetConfigHash !== data.TargetConfigHash) return false;
|
|
857
|
+
const candidate = { tenant: current.tenant, storageOwnerTenant: source.StorageOwnerTenant,
|
|
858
|
+
primaryBase: source.PrimaryBase, backupOrigin: source.BackupOrigin, publicBucket: source.PublicBucket,
|
|
859
|
+
privateBucket: '', targetConfigHash: source.TargetConfigHash,
|
|
860
|
+
aliasUrlRoutingProtocol: source.AliasUrlRoutingProtocol,
|
|
861
|
+
aliasCandidates: source.AliasUrlRoutingProtocol === 'PublicSourceUrlRoutes/1'
|
|
862
|
+
? (source.AliasCandidates || []).map(row => plainBase(row.Origin, true)) : [],
|
|
863
|
+
sourceMappings: (source.SourceMappings || []).map(row => ({ origin: row.Origin, pathPrefix: row.PathPrefix || '' })) };
|
|
864
|
+
if (candidate.aliasCandidates.length > 50 || new Set(candidate.aliasCandidates).size !== candidate.aliasCandidates.length) return false;
|
|
865
|
+
bindingSnapshot(candidate);
|
|
866
|
+
binding = candidate; expires = now() + 60000; return true;
|
|
867
|
+
} catch { return false; }
|
|
868
|
+
finally { clearTimeout(timer); }
|
|
869
|
+
})();
|
|
870
|
+
bootstrap = run;
|
|
871
|
+
try { const ready = await run;
|
|
872
|
+
if (!ready && revision === generation && signature(context()) === id) { binding = null; expires = now() + 15000; }
|
|
873
|
+
return ready;
|
|
874
|
+
} finally { if (bootstrap === run) bootstrap = null; }
|
|
875
|
+
}
|
|
876
|
+
async function flush() {
|
|
877
|
+
scheduled = false; const jobs = queue; queue = [];
|
|
878
|
+
for (let start = 0; start < jobs.length; start += 100) {
|
|
879
|
+
const slice = jobs.slice(start, start + 100);
|
|
880
|
+
try { const routes = await router.resolveMany(slice.map(job => job.input));
|
|
881
|
+
slice.forEach((job, index) => job.done(signature(context()) === job.context ? routes[index] : fallback(job.input))); }
|
|
882
|
+
catch { slice.forEach(job => job.done(fallback(job.input))); }
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
async function resolve(input) {
|
|
886
|
+
// 只有明确公有输入才触发任何匿名解析;旧 Limit 缺失由调用者保持原兼容分支。
|
|
887
|
+
if (!input || input.private !== false) return Object.freeze({ useBackup: false, url: '', primaryUrl: '', reason: 'ExplicitPublicRequired' });
|
|
888
|
+
const id = signature(context());
|
|
889
|
+
if (!await ensureBinding() || id !== signature(context())) return fallback(input);
|
|
890
|
+
if (aliases.eligible(input)) {
|
|
891
|
+
const revision = generation, route = await aliases.resolve(input);
|
|
892
|
+
return revision === generation && id === signature(context()) ? route || fallback(input) : fallback(input);
|
|
893
|
+
}
|
|
894
|
+
return await new Promise(done => { queue.push({ input, done, context: id });
|
|
895
|
+
if (!scheduled) { scheduled = true; queueMicrotask(flush); } });
|
|
896
|
+
}
|
|
897
|
+
const reportFailure = input => input?.private === false ? aliases.reportFailure(input) || router.reportFailure(input) : fallback({ private: true });
|
|
898
|
+
return Object.freeze({ resolve,
|
|
899
|
+
resolveMany(inputs) { return Promise.all(inputs.map(resolve)); },
|
|
900
|
+
reportFailure,
|
|
901
|
+
async resolveForNavigation(input) {
|
|
902
|
+
const id = signature(context()), route = await resolve(input);
|
|
903
|
+
if (id !== signature(context())) return fallback(input);
|
|
904
|
+
if (!route.useBackup) return route;
|
|
905
|
+
const revision = generation;
|
|
906
|
+
if (!(route.expiresAt > now())) return fallback(input);
|
|
907
|
+
// 新窗口无法监听跨域导航失败。打开前只做一次有界 HEAD,失败回原地址,不循环重试。
|
|
908
|
+
const controller = new AbortController(), timer = setTimeout(() => controller.abort(), 3000);
|
|
909
|
+
try {
|
|
910
|
+
const response = await fetchHead(route.url, { method: 'HEAD', credentials: 'omit', redirect: 'error', signal: controller.signal });
|
|
911
|
+
if (id !== signature(context()) || revision !== generation || !(route.expiresAt > now())) return fallback(input);
|
|
912
|
+
return response.ok ? route : reportFailure(input);
|
|
913
|
+
} catch { return reportFailure(input); }
|
|
914
|
+
finally { clearTimeout(timer); }
|
|
915
|
+
},
|
|
916
|
+
clear() { generation++; binding = null; bootstrap = null; expires = 0; router.clear(); aliases.clear(); }
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// 历史 Limit 缺失不是公开声明,不能据此新增匿名 NAS 访问。
|
|
921
|
+
function isExplicitPublicResource(metadata, configuredLimit) {
|
|
922
|
+
return metadata && typeof metadata === 'object' && Object.hasOwn(metadata, 'Limit')
|
|
923
|
+
? metadata.Limit === false : configuredLimit === false;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** 表单实例、租户、记录、字段及文件身份共同限定异步地址赋值。 */
|
|
927
|
+
function createPublicResourceAssignmentGuard({ getContext, contains }) {
|
|
928
|
+
const pending = new Map(); let disposed = false;
|
|
929
|
+
const current = ticket => {
|
|
930
|
+
if (disposed || pending.get(ticket.key) !== ticket) return false;
|
|
931
|
+
const context = getContext();
|
|
932
|
+
return Object.keys(ticket.context).every(name => context[name] === ticket.context[name])
|
|
933
|
+
&& contains(ticket.path, ticket.fileId);
|
|
934
|
+
};
|
|
935
|
+
return Object.freeze({
|
|
936
|
+
begin(key, path, fileId = null) {
|
|
937
|
+
const ticket = { key, path, fileId, context: { ...getContext() }, route: null, fallbackUsed: false };
|
|
938
|
+
pending.set(key, ticket); return ticket;
|
|
939
|
+
},
|
|
940
|
+
current,
|
|
941
|
+
get(key) { return pending.get(key); },
|
|
942
|
+
tickets() { return [...pending.values()]; },
|
|
943
|
+
invalidate(key) { pending.delete(key); },
|
|
944
|
+
clear() { pending.clear(); },
|
|
945
|
+
dispose() { disposed = true; pending.clear(); }
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return createPublicResourceClient;
|
|
950
|
+
})();
|
|
951
|
+
// END GENERATED PUBLIC RESOURCE ROUTER
|
|
952
|
+
|
|
572
953
|
export function createMicroiV8(options = {}) {
|
|
573
954
|
// 运行时配置可通过 createMicroiV8(options) 或 client.configure(next) 覆盖。
|
|
574
955
|
let config = {
|
|
575
956
|
apiBase: '',
|
|
576
957
|
webBase: '',
|
|
577
958
|
fileServer: '',
|
|
959
|
+
fileServerBak: '',
|
|
578
960
|
osClient: '',
|
|
579
961
|
token: '',
|
|
580
962
|
clientType: getUni() ? 'Mobile' : 'PC',
|
|
@@ -606,6 +988,9 @@ export function createMicroiV8(options = {}) {
|
|
|
606
988
|
|
|
607
989
|
// 更新配置后立即刷新并发队列,保证 maxConcurrent 热更新生效。
|
|
608
990
|
function configure(next = {}) {
|
|
991
|
+
if (['apiBase','osClient','fileServer','fileServerBak'].some(key => Object.prototype.hasOwnProperty.call(next,key) && next[key] !== config[key])) {
|
|
992
|
+
sdkPublicResourceEpoch++; sdkPublicResourceClient?.clear();
|
|
993
|
+
}
|
|
609
994
|
config = { ...config, ...next };
|
|
610
995
|
if (Object.prototype.hasOwnProperty.call(next, 'maxConcurrent')) {
|
|
611
996
|
runQueued = createQueue(config.maxConcurrent);
|
|
@@ -961,6 +1346,63 @@ export function createMicroiV8(options = {}) {
|
|
|
961
1346
|
}
|
|
962
1347
|
|
|
963
1348
|
// 资源地址统一过滤占位图,并兼容 HDFS 私有文件、FileServer 和绝对地址。
|
|
1349
|
+
// Public media selection is asynchronous. assetUrl remains a synchronous legacy API.
|
|
1350
|
+
let sdkPublicResourceClient = null;
|
|
1351
|
+
let sdkPublicResourceEpoch = 0;
|
|
1352
|
+
function sdkPublicMetadata(value, path, depth = 0) {
|
|
1353
|
+
if (depth > 8 || value == null) return null;
|
|
1354
|
+
if (typeof value === 'string' && /^[\[{]/.test(value.trim())) {
|
|
1355
|
+
try { return sdkPublicMetadata(JSON.parse(value), path, depth + 1); } catch { return null; }
|
|
1356
|
+
}
|
|
1357
|
+
if (Array.isArray(value)) {
|
|
1358
|
+
for (const entry of value) { const selected = sdkPublicMetadata(entry, path, depth + 1); if (selected) return selected; }
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
return typeof value === 'object' && extractUploadPath(value) === path ? value : null;
|
|
1362
|
+
}
|
|
1363
|
+
function sdkPublicInputAllowed(value, options = {}) {
|
|
1364
|
+
const path = extractUploadPath(value), meta = sdkPublicMetadata(value, path);
|
|
1365
|
+
if (options.private === true || (meta && (
|
|
1366
|
+
(Object.prototype.hasOwnProperty.call(meta, 'Limit') && meta.Limit !== false)
|
|
1367
|
+
|| meta.Private === true || meta.IsPrivate === true || meta.Public === false))) return false;
|
|
1368
|
+
return options.private === false || meta?.Limit === false || meta?.Public === true;
|
|
1369
|
+
}
|
|
1370
|
+
function sdkPublicContext() {
|
|
1371
|
+
return { tenant: config.osClient, primaryBase: config.fileServer || '',
|
|
1372
|
+
backupOrigin: config.fileServerBak || client.SysConfig?.FileServerBak || '' };
|
|
1373
|
+
}
|
|
1374
|
+
function sdkPublicIdentity() {
|
|
1375
|
+
return JSON.stringify([sdkPublicResourceEpoch, config.apiBase, sdkPublicContext()]);
|
|
1376
|
+
}
|
|
1377
|
+
function getSdkPublicResourceClient() {
|
|
1378
|
+
if (!sdkPublicResourceClient) sdkPublicResourceClient = createSdkPublicResourceClient({
|
|
1379
|
+
getContext: sdkPublicContext,
|
|
1380
|
+
post: data => apiEngineRun('mci-file-resource-public-url', data, { checkCode: false, silentError: true, timeout: 5000 })
|
|
1381
|
+
});
|
|
1382
|
+
return sdkPublicResourceClient;
|
|
1383
|
+
}
|
|
1384
|
+
async function resolvePublicResource(value, options = {}) {
|
|
1385
|
+
const path = extractUploadPath(value);
|
|
1386
|
+
if (!path || isBlockedAsset(path) || !sdkPublicInputAllowed(value, options))
|
|
1387
|
+
return { useBackup: false, url: '', primaryUrl: '', reason: 'ExplicitPublicRequired' };
|
|
1388
|
+
// Local files and API-owned protected/runtime endpoints keep their existing transport.
|
|
1389
|
+
if (/^(?:data:|blob:|file:|\.?\/static\/)/i.test(path) || /^\/?(?:file\/|micro-app\/v3(?:\/|$))/i.test(path))
|
|
1390
|
+
return { useBackup: false, url: assetUrl(value), primaryUrl: assetUrl(value), reason: 'LocalOrApiResource' };
|
|
1391
|
+
const identity = sdkPublicIdentity(), publicClient = getSdkPublicResourceClient();
|
|
1392
|
+
const route = options.navigation === true
|
|
1393
|
+
? await publicClient.resolveForNavigation({ path, private: false })
|
|
1394
|
+
: await publicClient.resolve({ path, private: false });
|
|
1395
|
+
// The portable SDK also binds the API base, which is not a public storage binding field.
|
|
1396
|
+
return identity === sdkPublicIdentity() ? route
|
|
1397
|
+
: { useBackup: false, url: '', primaryUrl: '', reason: 'ClientContextChanged' };
|
|
1398
|
+
}
|
|
1399
|
+
function reportPublicResourceFailure(value, options = {}) {
|
|
1400
|
+
if (!sdkPublicInputAllowed(value, options)) return { useBackup: false, url: '', primaryUrl: '', reason: 'ExplicitPublicRequired' };
|
|
1401
|
+
const path = extractUploadPath(value);
|
|
1402
|
+
return sdkPublicResourceClient?.reportFailure({ path, private: false })
|
|
1403
|
+
|| { useBackup: false, url: assetUrl(value), primaryUrl: assetUrl(value), reason: 'PrimaryFallback' };
|
|
1404
|
+
}
|
|
1405
|
+
|
|
964
1406
|
function assetUrl(value) {
|
|
965
1407
|
const picked = extractUploadPath(value);
|
|
966
1408
|
if (!picked || isBlockedAsset(picked)) return '';
|
|
@@ -999,6 +1441,7 @@ export function createMicroiV8(options = {}) {
|
|
|
999
1441
|
async function resolveFileUrl(filePathName, options = {}) {
|
|
1000
1442
|
const path = extractUploadPath(filePathName);
|
|
1001
1443
|
if (!path || isBlockedAsset(path)) return '';
|
|
1444
|
+
if (sdkPublicInputAllowed(filePathName, options)) return (await resolvePublicResource(filePathName, options)).url;
|
|
1002
1445
|
if (/^(https?:|blob:|data:|file:)/i.test(path)) return assetUrl(path);
|
|
1003
1446
|
// 文件实际私有标记和私有字段上下文优先,禁止因字段曾改为公有而错误降级 CDN。
|
|
1004
1447
|
const privateAccess = options.private === true || hasPrivateUploadFlag(filePathName);
|
|
@@ -1534,6 +1977,8 @@ export function createMicroiV8(options = {}) {
|
|
|
1534
1977
|
setCurrentUser: setUser,
|
|
1535
1978
|
getCurrentUser: getUser,
|
|
1536
1979
|
assetUrl,
|
|
1980
|
+
resolvePublicResource,
|
|
1981
|
+
reportPublicResourceFailure,
|
|
1537
1982
|
sanitizeAssetUrl: assetUrl,
|
|
1538
1983
|
resolveAssetUrl: assetUrl,
|
|
1539
1984
|
resolveAvatarUrl: resolveFileUrl,
|
|
@@ -72,6 +72,8 @@ PC 卡片优先选择当前终端 Card 视图,没有时复用 Mobile Card;
|
|
|
72
72
|
- 主后台侧栏读取 `sys_menu.IconClass`(CSS/组件图标),不是图片字段 `Icon`。优先填写当前前端已注册的 Element Plus 图标名;使用 FontAwesome/CSS 名称时先核对兼容映射,不能填入不存在的类名后让全部菜单回退为 `Document`。
|
|
73
73
|
- 没有贴切图标时,从实际可用图标集合中随机选一个,并将选择结果保存;可以按稳定菜单 Id 生成确定的随机分配,避免每次刷新或重新发布都变图标。同组菜单应有适当差异,不强求不同业务绝对不重复。
|
|
74
74
|
- 通过 `microi_create_module` / `microi_update_module` 或 Manifest 的模块配置写入,并把图标纳入应用包。只修改图片字段、只检查数据库字符串或只改本地 Manifest 不算完成;必须回读 `IconClass`,在真实侧栏检查图标可见且有区别。
|
|
75
|
+
- 创建菜单优先显式传 `iconClass`(Manifest 同名,保存为 `IconClass`);`icon`/`Icon` 保留桌面图片地址语义。历史调用把CSS图标传入 `icon` 时仅作为兼容输入,不能因此漏写侧栏字段。未指定时生成器按业务语义或稳定名称哈希从实际注册图标中补齐,禁止统一退为 `Document`。
|
|
76
|
+
- 同一父菜单下逐项检查图标与功能对应,分类、客户、产品、商机、报价、合同、回款等应可辨识;无法精确匹配时使用稳定差异图标,不追求全系统绝对唯一。幂等重跑保留既有正确图标,只有明确授权修复空值/错误值时才更新。工具成功后独立回读,旧MCP缺参数时先升级工具,不能把不存在的字段当作已经生效。
|
|
75
77
|
|
|
76
78
|
### 新模块表单打开方式(强制默认)
|
|
77
79
|
|
|
@@ -102,11 +104,8 @@ PC 卡片优先选择当前终端 Card 视图,没有时复用 Mobile Card;
|
|
|
102
104
|
记录版本限制并保留可用页面,不写不存在的组件路径,不擅自升级客户容器。占位页填满
|
|
103
105
|
可用内容区,持续动画表达等待,支持明暗主题、
|
|
104
106
|
窄屏及减少动态效果;没有真实任务状态时禁止伪造开发百分比、倒计时或完成量。
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
`Id/ParentId/Url/Sort`、角色/按钮权限和其它关联。刷新菜单、移除旧占位缓存并回读,
|
|
108
|
-
从原菜单和原 URL 最终验收;失败仅恢复该菜单记录的占位/原正确绑定并列为未完成。
|
|
109
|
-
不要求仍占位的原 URL 提前展示候选页面。MicroService 绑定仍遵守四字段完整性。
|
|
107
|
+
- 页面可运行后立即回读资源 Id,在同一 sys_menu.Id 绑定真实页面,供用户从原菜单预览;保留 Id/ParentId/Url/Sort、角色/按钮权限和关联。刷新菜单与旧占位缓存,从原入口边开发边验收,不能只开放候选 URL 而原菜单仍显示开发中。MicroService 先发布并验证运行产物,绑定仍遵守四字段完整性。
|
|
108
|
+
- 蓝图区分可预览/待验收与验收通过。局部缺陷继续修复;白屏、资源不可达或越权时优先恢复最近可运行且授权正确的绑定,确无可运行版本才恢复占位,不将预览可见当成交付完成。
|
|
110
109
|
- 用 `microi_get_module` 回读,再用真实用户逐菜单点击、切换、刷新检查标题、访问标签、
|
|
111
110
|
权限与缓存;开发中或失败菜单列为未完成,不以占位页、隐藏菜单或修改状态文案冒充交付。
|
|
112
111
|
|