cc-viewer 1.6.348 → 1.7.1
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 +13 -1
- package/cli.js +116 -37
- package/dist/assets/App-gUSmU6Ny.js +2 -0
- package/dist/assets/{MdxEditorPanel-ualoRFOL.js → MdxEditorPanel-DAR39A3S.js} +1 -1
- package/dist/assets/Mobile-qfRsRth0.js +1 -0
- package/dist/assets/{_baseUniq-CG81mgN_.js → _baseUniq-D9_E5ZDp.js} +1 -1
- package/dist/assets/{arc-C161J4f2.js → arc-D4iFrlhb.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BmbDMdn9.js → architectureDiagram-Q4EWVU46-f1neo7hg.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-DXpMvdxd.js → blockDiagram-DXYQGD6D-DWi9e9tG.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-8Mypg0IU.js → c4Diagram-AHTNJAMY-DAdj49A2.js} +1 -1
- package/dist/assets/{channel-B2khdmvc.js → channel-B3VthT0m.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-I61r1g6z.js → chunk-4BX2VUAB-BrPcAHV3.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-VFPlRE5E.js → chunk-4TB4RGXK-Bvopc1V9.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DvCamg_6.js → chunk-55IACEB6-Juw-7Z3i.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-Dj4Q8RwR.js → chunk-EDXVE4YY-DVYP9tpB.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-_bgQuuJh.js → chunk-FMBD7UC4-VQ7VOXl3.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-3to38LyN.js → chunk-OYMX7WX6-F8Rj4ZUZ.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Zl26wKKz.js → chunk-QZHKN3VN-Dn7K3yi9.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-RLYPrlh0.js → chunk-YZCP3GAM-UjO-KTsn.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DmSozxoO.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DmSozxoO.js +1 -0
- package/dist/assets/clone-BQtvy9Ae.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-CsYOjJ0V.js → cose-bilkent-S5V4N54A-BDhNj8rq.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-C3YXXaFU.js → dagre-KV5264BT-BzdcDuyt.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DUCOg1ld.js → diagram-5BDNPKRD-DP_SX65N.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-BtlLl_zX.js → diagram-G4DWMVQ6-B-pBFw8Q.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-1Qp9xfOk.js → diagram-MMDJMWI5-6XSfN1eN.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Cj7VMp3L.js → diagram-TYMM5635-DJvGxvpE.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-CVZVn541.js → erDiagram-SMLLAGMA-CpqdpGYj.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CiU9Fkkv.js → flowDiagram-DWJPFMVM-C9m7wjwI.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-aRaG97ID.js → ganttDiagram-T4ZO3ILL-CU0XOfY3.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-Cr2LJlvX.js → gitGraphDiagram-UUTBAWPF-ipyGULnP.js} +1 -1
- package/dist/assets/{graph-B59lDyMn.js → graph-BR2KmES9.js} +1 -1
- package/dist/assets/{index-C1ptnCpq.js → index--wgRW_OB.js} +1 -1
- package/dist/assets/index-B51Ci0GL.js +2 -0
- package/dist/assets/{index-CRAWA8PC.js → index-C-be29ey.js} +1 -1
- package/dist/assets/{index-CSM1Kw8l.js → index-C16fiVhv.js} +1 -1
- package/dist/assets/{index-Ddk-kg22.js → index-D5YruOKD.js} +1 -1
- package/dist/assets/{index-BnhNIjbj.js → index-D5tSeMU7.js} +1 -1
- package/dist/assets/{index-vFEQq-NB.js → index-DuZQ6FqR.js} +1 -1
- package/dist/assets/{index-CO2hZ1s4.js → index-OWzSX2T8.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-CnO22bw8.js → infoDiagram-42DDH7IO-BQWQjfFX.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-ByNzpScH.js → ishikawaDiagram-UXIWVN3A-Df-KH_Hz.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-yaVaCWPq.js → journeyDiagram-VCZTEJTY-CMbB_4G9.js} +1 -1
- package/dist/assets/{jszip.min-BdVFP3OW.js → jszip.min-B6k2t9Ch.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-RIPR_RQ3.js → kanban-definition-6JOO6SKY-CImdlKN3.js} +1 -1
- package/dist/assets/{layout-0rzel0v_.js → layout-DQl4aw09.js} +1 -1
- package/dist/assets/{linear-sktN0tB6.js → linear-CLD3aPlK.js} +1 -1
- package/dist/assets/mermaid.core-CN35dvKJ.js +7 -0
- package/dist/assets/{min-rspGOkz_.js → min-_D86henJ.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BsVzZzTz.js → mindmap-definition-QFDTVHPH-2JVcu1nk.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-CsoWsw4f.js → pieDiagram-DEJITSTG-Dik-m5u7.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-CTMKuCAI.js → quadrantDiagram-34T5L4WZ-Bm5l40Jq.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-C67256te.js → requirementDiagram-MS252O5E-Ck9w5rTj.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-X8Y7KnW0.js → sankeyDiagram-XADWPNL6-BbGVCFEf.js} +1 -1
- package/dist/assets/{seqResourceLoaders-B5Zy_CC_.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
- package/dist/assets/seqResourceLoaders-DZJxMMaR.js +2 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-Cils0TAp.js → sequenceDiagram-FGHM5R23-vjvEdksk.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DFnohvpG.js → stateDiagram-FHFEXIEX-DTqMoiB2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BTX_4o81.js → stateDiagram-v2-QKLJ7IA2-CNbAwnhu.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-DrIYFrzZ.js → timeline-definition-GMOUNBTQ-Cw8O-52d.js} +1 -1
- package/dist/assets/{vendor-antd-D5HmSDSg.js → vendor-antd-DI7JL-mE.js} +1 -1
- package/dist/assets/{vendor-codemirror-C5yuj7ze.js → vendor-codemirror-B9c49dtM.js} +1 -1
- package/dist/assets/{vendor-mdxeditor-BcEooTvb.js → vendor-mdxeditor-B6cpBtIE.js} +2 -2
- package/dist/assets/{vendor-qrcode-d9LCn0-9.js → vendor-qrcode-Dn90-s-u.js} +1 -1
- package/dist/assets/{vendor-virtuoso-K9m4MN6a.js → vendor-virtuoso-CpdSKD_6.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-lRqBYTr9.js → vennDiagram-DHZGUBPP-DCuqymb7.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-e8YfxpOe.js → wardley-RL74JXVD-BJ20wJUU.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-Bxrg774h.js → wardleyDiagram-NUSXRM2D-Vz6US3qt.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-DWhZqhq8.js → xychartDiagram-5P7HB3ND-CzkLKrmi.js} +1 -1
- package/dist/index.html +4 -4
- package/findcc.js +4 -3
- package/package.json +3 -1
- package/server/i18n.js +21 -141
- package/server/interceptor.js +211 -403
- package/server/lib/async-write-queue.js +12 -1
- package/server/lib/delta-reconstructor.js +3 -3
- package/server/lib/error-report.js +45 -0
- package/server/lib/im-log-watcher.js +68 -8
- package/server/lib/interceptor-core.js +19 -174
- package/server/lib/log-management.js +214 -247
- package/server/lib/log-stream.js +59 -108
- package/server/lib/log-watcher.js +62 -274
- package/server/lib/log-zip.js +148 -0
- package/server/lib/session-boundary.js +266 -0
- package/server/lib/session-pin-store.js +10 -19
- package/server/lib/stats-worker.js +138 -267
- package/server/lib/user-prompt-extract.js +250 -0
- package/server/lib/v2/adapter.js +1124 -0
- package/server/lib/v2/blob-store.js +44 -0
- package/server/lib/v2/conversation-store.js +217 -0
- package/server/lib/v2/convert-manager.js +79 -0
- package/server/lib/v2/convert-worker.js +42 -0
- package/server/lib/v2/convert.js +409 -0
- package/server/lib/v2/identity.js +204 -0
- package/server/lib/v2/journal.js +62 -0
- package/server/lib/v2/layout.js +167 -0
- package/server/lib/v2/live-feed.js +558 -0
- package/server/lib/v2/meta-rows.js +360 -0
- package/server/lib/v2/migrate-prompt.js +54 -0
- package/server/lib/v2/replay.js +191 -0
- package/server/lib/v2/session-select.js +222 -0
- package/server/lib/v2/singleflight.js +104 -0
- package/server/lib/v2/v2-writer.js +528 -0
- package/server/lib/v2/verify.js +233 -0
- package/server/lib/wire-compress.js +167 -0
- package/server/routes/events.js +120 -150
- package/server/routes/files-fs.js +4 -2
- package/server/routes/im.js +11 -2
- package/server/routes/logs.js +209 -164
- package/server/routes/preferences.js +0 -3
- package/server/routes/project-meta.js +2 -3
- package/server/routes/session-pin.js +7 -8
- package/server/routes/v2.js +60 -0
- package/server/routes/workspaces.js +50 -14
- package/server/server.js +70 -66
- package/server/workspace-registry.js +24 -4
- package/src/utils/apiUrl.js +26 -0
- package/src/utils/askFallback.js +21 -0
- package/src/utils/askOptionDesc.js +55 -0
- package/src/utils/askPortalMatcher.js +54 -0
- package/src/utils/autoApproveOptions.js +27 -0
- package/src/utils/avatarAnimationPostPass.js +49 -0
- package/src/utils/builtinPresets.js +18 -0
- package/src/utils/clearCheckpoint.js +7 -0
- package/src/utils/commandValidator.js +45 -0
- package/src/utils/contentFilter.js +620 -0
- package/src/utils/contextRaw.js +18 -0
- package/src/utils/contextSidebarNavigation.js +16 -0
- package/src/utils/contextTurns.js +117 -0
- package/src/utils/displayScaleHelper.js +47 -0
- package/src/utils/effectiveModel.js +13 -0
- package/src/utils/entry-slim.js +563 -0
- package/src/utils/entryCache.js +144 -0
- package/src/utils/errorReport.js +6 -0
- package/src/utils/fileExpandedPathsStorage.js +70 -0
- package/src/utils/fileIcons.jsx +36 -0
- package/src/utils/fileOpen.js +37 -0
- package/src/utils/formatters.js +48 -0
- package/src/utils/gitApi.js +50 -0
- package/src/utils/gitTreeBuilder.js +19 -0
- package/src/utils/helpers.js +846 -0
- package/src/utils/identityHeal.js +99 -0
- package/src/utils/imConnState.js +59 -0
- package/src/utils/imOrigin.js +24 -0
- package/src/utils/imTr.js +7 -0
- package/src/utils/imageCompress.js +83 -0
- package/src/utils/imageDownscale.js +28 -0
- package/src/utils/imageResize.js +112 -0
- package/src/utils/ingestPipeline.js +47 -0
- package/src/utils/lazyWithReload.js +53 -0
- package/src/utils/markdown.js +30 -0
- package/src/utils/markdownIncremental.js +39 -0
- package/src/utils/markdownProfiler.js +113 -0
- package/src/utils/mdExtensionDetect.js +64 -0
- package/src/utils/memoryLinkParser.js +44 -0
- package/src/utils/modalMask.js +15 -0
- package/src/utils/monotime.js +6 -0
- package/src/utils/pinnedMenu.js +55 -0
- package/src/utils/presetShortcuts.js +14 -0
- package/src/utils/projectAlias.js +135 -0
- package/src/utils/promptClassifier.js +112 -0
- package/src/utils/promptDetect.js +196 -0
- package/src/utils/promptNav.js +60 -0
- package/src/utils/ptyChunkBuilder.js +251 -0
- package/src/utils/quickMenuHoverIntent.js +35 -0
- package/src/utils/rateLimitParser.js +119 -0
- package/src/utils/readResultPool.js +106 -0
- package/src/utils/refreshCachedItemProp.js +30 -0
- package/src/utils/requestType.js +208 -0
- package/src/utils/resizeCalc.js +21 -0
- package/src/utils/resolveLocalized.js +17 -0
- package/src/utils/searchApi.js +47 -0
- package/src/utils/searchReplace.js +51 -0
- package/src/utils/seqResourceLoaders.js +79 -0
- package/src/utils/sessionManager.js +471 -0
- package/src/utils/sessionMerge.js +180 -0
- package/src/utils/skillModalController.js +111 -0
- package/src/utils/skillsParser.js +116 -0
- package/src/utils/slashCommandLabels.js +80 -0
- package/src/utils/splitDragCalc.js +73 -0
- package/src/utils/stickyBottomController.js +643 -0
- package/src/utils/svgSanitize.js +48 -0
- package/src/utils/systemTags.js +47 -0
- package/src/utils/tClaude.js +20 -0
- package/src/utils/teamModalBuilder.js +351 -0
- package/src/utils/teamSessionParser.js +190 -0
- package/src/utils/teammateAvatars.js +194 -0
- package/src/utils/teammateDetector.js +121 -0
- package/src/utils/termDiag.js +114 -0
- package/src/utils/terminalClipboard.js +117 -0
- package/src/utils/terminalWriteQueue.js +398 -0
- package/src/utils/thinkingMerge.js +49 -0
- package/src/utils/toolCatalog.js +29 -0
- package/src/utils/toolResultBuilder.js +393 -0
- package/src/utils/toolResultClassifier.js +16 -0
- package/src/utils/toolResultCore.js +167 -0
- package/src/utils/toolsDiff.js +44 -0
- package/src/utils/toolsXmlFormatter.js +3 -0
- package/src/utils/ultraplanController.js +109 -0
- package/src/utils/ultraplanExperts.js +86 -0
- package/src/utils/ultraplanTemplates.js +154 -0
- package/src/utils/userImageRefs.js +49 -0
- package/src/utils/v3Assembler.js +112 -0
- package/src/utils/v3Rows.js +48 -0
- package/src/utils/voicePackPlayer.js +233 -0
- package/src/utils/webSearchGrouping.js +111 -0
- package/src/utils/workflowFormat.js +52 -0
- package/src/utils/workflowRuns.js +118 -0
- package/src/utils/workflowStore.js +94 -0
- package/dist/assets/App-VTo4NVdo.js +0 -2
- package/dist/assets/Mobile-CcmYvMm_.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BXnyUgsj.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BXnyUgsj.js +0 -1
- package/dist/assets/clone-md-Mc1-T.js +0 -1
- package/dist/assets/index-DCo650PG.js +0 -2
- package/dist/assets/mermaid.core-C2Parj1q.js +0 -7
- package/dist/assets/seqResourceLoaders-BVozq68i.js +0 -2
- package/server/lib/instance-registry.js +0 -57
- package/server/lib/jsonl-archive.js +0 -180
- package/server/lib/teammate-detect.js +0 -32
- package/server/lib/zip-safety.js +0 -68
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { formatTeammateLabel } from './requestType';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Late-identity healing helpers for ChatView.buildAllItems.
|
|
6
|
+
*
|
|
7
|
+
* Background: on a cold load the first build can run before identity data is
|
|
8
|
+
* complete — the producer turn may be in-flight (excluded from the filtered
|
|
9
|
+
* requests array) or split into the live tail, and the teammate-name registry
|
|
10
|
+
* may not have scanned the lead's Agent tool_use yet. The resulting fallbacks
|
|
11
|
+
* ("MainAgent" model identity, "Teammate: X" labels) get baked into cached
|
|
12
|
+
* elements / scan entries and were previously frozen until a new MainAgent
|
|
13
|
+
* turn replaced the session object. These helpers heal them on the next
|
|
14
|
+
* buildAllItems tick instead.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Patch cached ChatMessage elements whose modelInfo was baked as null once the
|
|
19
|
+
* request scan can resolve it. Mirrors refreshCachedItemProp: clones ONLY rows
|
|
20
|
+
* that transition to a different resolved value, returns the SAME array
|
|
21
|
+
* reference when nothing changed (React reconciler then skips everything).
|
|
22
|
+
*
|
|
23
|
+
* Strict-null marker: rows that display model identity always receive
|
|
24
|
+
* `modelInfo` explicitly (null when unresolved); rows that never get the prop
|
|
25
|
+
* have `undefined` and are skipped. Permanently-unresolvable rows stay null
|
|
26
|
+
* and are re-checked cheaply each pass without cloning.
|
|
27
|
+
*
|
|
28
|
+
* Session-fallback rows: rows baked with the session-level model fallback
|
|
29
|
+
* carry a `_fromSession: true` marker ON the modelInfo object (a per-session
|
|
30
|
+
* wrapper around getModelInfo's cached ref) and stay heal-eligible so they
|
|
31
|
+
* upgrade to the precise per-message model once the producer becomes
|
|
32
|
+
* resolvable. The resolver passed here MUST be the precise one (NOT the
|
|
33
|
+
* session-fallback wrapped one), or marked rows would "heal" back to the same
|
|
34
|
+
* fallback. Precise resolutions come from getModelInfo's cache and never carry
|
|
35
|
+
* the marker, so an upgraded row drops out of heal eligibility — the upgrade
|
|
36
|
+
* clones each fallback row exactly once (same cost as today's null→resolved
|
|
37
|
+
* heal), never churns.
|
|
38
|
+
*
|
|
39
|
+
* @param {Array} items - cached ChatMessage element array
|
|
40
|
+
* @param {Function} resolveModelInfo - (ts, role) => modelInfo|null, closing
|
|
41
|
+
* over the CURRENT request-scan caches (precise resolver, no fallback)
|
|
42
|
+
* @returns {Array} same array if clean, else a new array with healed clones
|
|
43
|
+
*/
|
|
44
|
+
export function refreshResolvedModelInfo(items, resolveModelInfo) {
|
|
45
|
+
let dirty = false;
|
|
46
|
+
const out = items.map((m) => {
|
|
47
|
+
if (!m || !m.props || !m.props.timestamp) return m;
|
|
48
|
+
const mi = m.props.modelInfo;
|
|
49
|
+
if (mi !== null && !(mi && mi._fromSession)) return m;
|
|
50
|
+
// Non-assistant rows (plan-prompt/teammate/task-notification) were
|
|
51
|
+
// originally computed with msg.role 'user' — preserve that mapping.
|
|
52
|
+
const role = m.props.role === 'assistant' ? 'assistant' : 'user';
|
|
53
|
+
const next = resolveModelInfo(m.props.timestamp, role);
|
|
54
|
+
// Defense-in-depth: a marked value here means a caller passed the session-fallback WRAPPED
|
|
55
|
+
// resolver by mistake — treating it as a heal would clone marked rows forever (the clone
|
|
56
|
+
// keeps the marker → eligible again next pass). Never "heal" to a fallback value.
|
|
57
|
+
if (!next || next._fromSession) return m;
|
|
58
|
+
dirty = true;
|
|
59
|
+
return React.cloneElement(m, { modelInfo: next });
|
|
60
|
+
});
|
|
61
|
+
return dirty ? out : items;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Heal teammate labels baked into _reqScanCache.subAgentEntries before
|
|
66
|
+
* resolveTeammateNames had injected req.teammate. Entries carry the request
|
|
67
|
+
* OBJECT reference (entry.req) — object identity survives filtered-array
|
|
68
|
+
* rebuilds and mid-array insertions, unlike positional indices, which shift
|
|
69
|
+
* exactly in the late-completing-spawn scenario this fix targets.
|
|
70
|
+
*
|
|
71
|
+
* Mutates entries in place (they live inside the scan cache). Also covers
|
|
72
|
+
* id → real-name upgrades: heals whenever the freshly formatted label differs.
|
|
73
|
+
*
|
|
74
|
+
* @param {Array} entries - subAgentEntries ({ unresolved, req, label, ... })
|
|
75
|
+
*/
|
|
76
|
+
export function healUnresolvedTeammateEntries(entries) {
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (!entry || !entry.unresolved || !entry.req || !entry.req.teammate) continue;
|
|
79
|
+
const label = formatTeammateLabel(entry.req.teammate, entry.req.body?.model);
|
|
80
|
+
if (label !== entry.label) entry.label = label;
|
|
81
|
+
entry.unresolved = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Detects that the requests array changed IDENTITY under an incremental scan
|
|
87
|
+
* cursor (mid-array insertion — e.g. an in-flight producer turn completing and
|
|
88
|
+
* entering the filtered array before the cursor). Append-only growth keeps
|
|
89
|
+
* requests[processedCount-1] identical, so this stays false on the hot path.
|
|
90
|
+
* Accepted residual: a mid-array REPLACEMENT below the cursor with an
|
|
91
|
+
* unchanged element at processedCount-1 does not fire — that can only leave a
|
|
92
|
+
* stale token badge, not the identity bug.
|
|
93
|
+
*
|
|
94
|
+
* @returns {boolean} true when the caller must do a full scan-cache reset
|
|
95
|
+
*/
|
|
96
|
+
export function needsFullReqRescan(requests, processedCount, lastScannedReq) {
|
|
97
|
+
if (!processedCount) return false;
|
|
98
|
+
return requests[processedCount - 1] !== lastScannedReq;
|
|
99
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Shared derivation of the IM bridge connection status for the UI. The server reports
|
|
2
|
+
// `connection: { running, connected, connectionState?, lastError? }` where `connectionState`
|
|
3
|
+
// ('connected' | 'reconnecting' | 'disconnected') is the authoritative tri-state added alongside
|
|
4
|
+
// the legacy `connected` boolean; old workers (pre-tri-state builds) omit it, so both functions
|
|
5
|
+
// must reproduce the legacy behavior exactly when it is absent.
|
|
6
|
+
// Pure and dependency-free so node:test can import it directly.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Chip-level state: 'connected' | 'reconnecting' | 'running' | 'error' | 'stopped'.
|
|
10
|
+
* `reconnecting` outranks `lastError` — the server keeps the disconnect cause in lastError while
|
|
11
|
+
* the SDK retries, so error-first ordering would permanently mask the retry state.
|
|
12
|
+
*/
|
|
13
|
+
export function deriveImConnState(connection) {
|
|
14
|
+
if (!connection) return 'stopped';
|
|
15
|
+
if (connection.connectionState) {
|
|
16
|
+
if (connection.running && connection.connectionState === 'reconnecting') return 'reconnecting';
|
|
17
|
+
if (connection.lastError) return 'error';
|
|
18
|
+
if (connection.running && connection.connectionState === 'connected') return 'connected';
|
|
19
|
+
if (connection.running) return 'running'; // disconnected-while-running without error (legacy parity)
|
|
20
|
+
return 'stopped';
|
|
21
|
+
}
|
|
22
|
+
// Legacy payload (old worker): error → connected → running → stopped.
|
|
23
|
+
if (connection.lastError) return 'error';
|
|
24
|
+
if (connection.running && connection.connected) return 'connected';
|
|
25
|
+
if (connection.running) return 'running';
|
|
26
|
+
return 'stopped';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Proc-state-aware badge decision shared by ImPlatformSettings.renderBadge and
|
|
31
|
+
* ImConversationModal.renderStatus. Returns null when there is nothing to render, else
|
|
32
|
+
* `{ key, fallback, color, withPort, error }` — the caller supplies its own translation call and
|
|
33
|
+
* appends the port suffix when `withPort`. `color` is the antd Tag color (null = default grey).
|
|
34
|
+
*/
|
|
35
|
+
export function imBadgeModel({ procState, connection }) {
|
|
36
|
+
// Reconnecting is only meaningful while the worker itself is alive (ready, or remote/no proc info).
|
|
37
|
+
if (connection?.running && connection?.connectionState === 'reconnecting'
|
|
38
|
+
&& (!procState || procState === 'ready')) {
|
|
39
|
+
return { key: 'ui.im.statusReconnecting', fallback: 'Reconnecting…', color: 'warning', withPort: procState === 'ready', error: null };
|
|
40
|
+
}
|
|
41
|
+
if (connection?.lastError) {
|
|
42
|
+
return { key: 'ui.im.statusError', fallback: 'Error', color: 'error', withPort: false, error: connection.lastError };
|
|
43
|
+
}
|
|
44
|
+
if (procState) {
|
|
45
|
+
if (procState === 'ready') {
|
|
46
|
+
return connection?.connected
|
|
47
|
+
? { key: 'ui.im.statusConnected', fallback: 'Connected', color: 'success', withPort: true, error: null }
|
|
48
|
+
: { key: 'ui.im.statusRunning', fallback: 'Running, connecting…', color: 'processing', withPort: true, error: null };
|
|
49
|
+
}
|
|
50
|
+
if (procState === 'booting') return { key: 'ui.im.statusBooting', fallback: 'Starting…', color: 'processing', withPort: false, error: null };
|
|
51
|
+
if (procState === 'hung') return { key: 'ui.im.statusHung', fallback: 'Not responding', color: 'warning', withPort: false, error: null };
|
|
52
|
+
return { key: 'ui.im.statusDisconnected', fallback: 'Disconnected', color: null, withPort: false, error: null }; // dead
|
|
53
|
+
}
|
|
54
|
+
// Remote fallback (no process info in the trimmed LAN response).
|
|
55
|
+
if (!connection) return null;
|
|
56
|
+
if (connection.connected) return { key: 'ui.im.statusConnected', fallback: 'Connected', color: 'success', withPort: false, error: null };
|
|
57
|
+
if (connection.running) return { key: 'ui.im.statusRunning', fallback: 'Running, connecting…', color: 'processing', withPort: false, error: null };
|
|
58
|
+
return { key: 'ui.im.statusDisconnected', fallback: 'Disconnected', color: null, withPort: false, error: null };
|
|
59
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// IM-origin marker: messages injected by an IM bridge carry a leading sentinel so the
|
|
2
|
+
// conversation view can show which IM a message came from and (optionally) which sender.
|
|
3
|
+
// ⟦im:<id>⟧ — platform only (legacy / when sender unknown)
|
|
4
|
+
// ⟦im:<id>:<senderId>⟧ — platform + opaque sender id (used to look up name/avatar)
|
|
5
|
+
//
|
|
6
|
+
// KEEP IN SYNC with markOrigin() in server/lib/im-bridge-core.js. The senderId group is
|
|
7
|
+
// OPTIONAL so old markers (no sender) still parse → senderId:null → callers fall back to the
|
|
8
|
+
// global user profile (no regression). The bridge only embeds a senderId matching /^[^\s:⟧]+$/,
|
|
9
|
+
// so the `:` separator and the `⟧` terminator are never ambiguous.
|
|
10
|
+
// U+27E6 / U+27E7 (⟦ ⟧) are virtually never typed by a human, so false positives are nil.
|
|
11
|
+
export const IM_ORIGIN_RE = /^⟦im:([a-z0-9_-]+)(?::([^⟧]+))?⟧[ ]?/;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Strip a leading IM-origin marker from a message's text.
|
|
15
|
+
* @param {string} text raw message text
|
|
16
|
+
* @returns {{ text: string, imSource: string|null, senderId: string|null }}
|
|
17
|
+
* stripped text + the IM id (null if no marker) + the sender id (null if absent)
|
|
18
|
+
*/
|
|
19
|
+
export function parseImOrigin(text) {
|
|
20
|
+
if (typeof text !== 'string') return { text, imSource: null, senderId: null };
|
|
21
|
+
const m = text.match(IM_ORIGIN_RE);
|
|
22
|
+
if (!m) return { text, imSource: null, senderId: null };
|
|
23
|
+
return { text: text.slice(m[0].length), imSource: m[1], senderId: m[2] || null };
|
|
24
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { t } from '../i18n';
|
|
2
|
+
|
|
3
|
+
// Safe translate-with-fallback used across the IM settings components: returns the translation
|
|
4
|
+
// when present, otherwise the provided English fallback (never the raw key).
|
|
5
|
+
export const imTr = (key, params, fallback) => {
|
|
6
|
+
try { const r = t(key, params); return (r && r !== key) ? r : fallback; } catch { return fallback; }
|
|
7
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// 浏览器端图片压缩 → base64 dataURL
|
|
2
|
+
// 用于 MDXEditor 的 imageUploadHandler:粘贴/拖入图片时本地压缩后内联到 markdown,
|
|
3
|
+
// 零后端依赖、保证 .md 文件可移植。
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAX_EDGE = 2000;
|
|
6
|
+
const DEFAULT_QUALITY = 0.85;
|
|
7
|
+
// 输入硬上限:原图 > 10MB 时直接拒绝(即使压缩后能控制在 5MB 内,原图占内存也太大)。
|
|
8
|
+
// 5MB 是后端 writeFileContent 的上限;原图按经验 base64 后 +33%,压缩后通常 100-300KB,
|
|
9
|
+
// 给 30x 余量已足。
|
|
10
|
+
const MAX_INPUT_BYTES = 10 * 1024 * 1024;
|
|
11
|
+
// 这里的 KEEP 集合 = 「不走 canvas resize、保留原始数据 base64」的 MIME。
|
|
12
|
+
// - PNG / WEBP:透明区在 canvas 转 JPEG 时会变黑底,且压缩收益取决于内容;保留原图最稳。
|
|
13
|
+
// - GIF:canvas drawImage 只能取首帧,会丢失全部动画——必须直通。
|
|
14
|
+
const PASSTHROUGH_TYPES = new Set(['image/png', 'image/gif', 'image/webp', 'image/svg+xml']);
|
|
15
|
+
|
|
16
|
+
function readAsDataURL(file) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const reader = new FileReader();
|
|
19
|
+
reader.onload = () => resolve(reader.result);
|
|
20
|
+
reader.onerror = () => reject(new Error('FileReader failed'));
|
|
21
|
+
reader.readAsDataURL(file);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadImage(src) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const img = new Image();
|
|
28
|
+
img.onload = () => resolve(img);
|
|
29
|
+
img.onerror = () => reject(new Error('Image decode failed'));
|
|
30
|
+
img.src = src;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function compressImageToDataURL(file, opts = {}) {
|
|
35
|
+
if (!(file instanceof Blob)) {
|
|
36
|
+
throw new Error('compressImageToDataURL: input must be a File or Blob');
|
|
37
|
+
}
|
|
38
|
+
if (!file.type || !file.type.startsWith('image/')) {
|
|
39
|
+
throw new Error(`Unsupported file type: ${file.type || 'unknown'}`);
|
|
40
|
+
}
|
|
41
|
+
if (file.size > MAX_INPUT_BYTES) {
|
|
42
|
+
throw new Error(`Image too large: ${(file.size / 1024 / 1024).toFixed(1)}MB > ${MAX_INPUT_BYTES / 1024 / 1024}MB limit`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const maxEdge = opts.maxEdge ?? DEFAULT_MAX_EDGE;
|
|
46
|
+
const quality = opts.quality ?? DEFAULT_QUALITY;
|
|
47
|
+
|
|
48
|
+
// PNG / GIF / WEBP / SVG 直通 base64(GIF 保留动画,PNG/WEBP 保留透明,SVG 保留矢量)
|
|
49
|
+
if (PASSTHROUGH_TYPES.has(file.type)) {
|
|
50
|
+
return await readAsDataURL(file);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const originalDataURL = await readAsDataURL(file);
|
|
54
|
+
const img = await loadImage(originalDataURL);
|
|
55
|
+
|
|
56
|
+
const longestEdge = Math.max(img.naturalWidth, img.naturalHeight);
|
|
57
|
+
const scale = longestEdge > maxEdge ? maxEdge / longestEdge : 1;
|
|
58
|
+
const targetW = Math.max(1, Math.round(img.naturalWidth * scale));
|
|
59
|
+
const targetH = Math.max(1, Math.round(img.naturalHeight * scale));
|
|
60
|
+
|
|
61
|
+
const canvas = document.createElement('canvas');
|
|
62
|
+
canvas.width = targetW;
|
|
63
|
+
canvas.height = targetH;
|
|
64
|
+
const ctx = canvas.getContext('2d');
|
|
65
|
+
if (!ctx) {
|
|
66
|
+
return originalDataURL;
|
|
67
|
+
}
|
|
68
|
+
ctx.drawImage(img, 0, 0, targetW, targetH);
|
|
69
|
+
|
|
70
|
+
// JPEG 透明区会变黑——在 PASSTHROUGH 已涵盖 PNG/WEBP/GIF 的前提下,能进到这里的
|
|
71
|
+
// 都是 image/jpeg / image/bmp / image/heic 等非透明类型,统一压成 JPEG。
|
|
72
|
+
let compressed;
|
|
73
|
+
try {
|
|
74
|
+
compressed = canvas.toDataURL('image/jpeg', quality);
|
|
75
|
+
} catch {
|
|
76
|
+
return originalDataURL;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!compressed || compressed === 'data:,' || compressed.length >= originalDataURL.length) {
|
|
80
|
+
return originalDataURL;
|
|
81
|
+
}
|
|
82
|
+
return compressed;
|
|
83
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Retina 屏幕截图为 2x 分辨率,上传前按 devicePixelRatio 缩小到 1x,减少文件体积。
|
|
2
|
+
// 非 Retina 屏幕、Canvas 不可用、或 img.onerror 时,静默返回原始文件(graceful fallback)。
|
|
3
|
+
export function downscaleForRetina(file) {
|
|
4
|
+
const dpr = window.devicePixelRatio || 1;
|
|
5
|
+
if (dpr <= 1) return Promise.resolve(file);
|
|
6
|
+
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
const img = new Image();
|
|
9
|
+
const url = URL.createObjectURL(file);
|
|
10
|
+
img.onload = () => {
|
|
11
|
+
URL.revokeObjectURL(url);
|
|
12
|
+
const w = Math.round(img.width / dpr);
|
|
13
|
+
const h = Math.round(img.height / dpr);
|
|
14
|
+
const canvas = document.createElement('canvas');
|
|
15
|
+
canvas.width = w;
|
|
16
|
+
canvas.height = h;
|
|
17
|
+
const ctx = canvas.getContext('2d');
|
|
18
|
+
if (!ctx) { resolve(file); return; }
|
|
19
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
20
|
+
canvas.toBlob((blob) => {
|
|
21
|
+
if (!blob) { resolve(file); return; }
|
|
22
|
+
resolve(new File([blob], file.name || 'clipboard.png', { type: file.type }));
|
|
23
|
+
}, file.type);
|
|
24
|
+
};
|
|
25
|
+
img.onerror = () => { URL.revokeObjectURL(url); resolve(file); };
|
|
26
|
+
img.src = url;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// 图片上传前尺寸压缩:任意一边超过 maxDim 时,按比例缩放至 maxDim 以内。
|
|
2
|
+
// 非图片 / 解码失败时原样返回;HEIC/AVIF/GIF/BMP 等只要浏览器能解码就会处理。
|
|
3
|
+
|
|
4
|
+
// 输出格式:原 PNG/WebP 保留以避免不必要的有损再编码,其余一律转 JPEG。
|
|
5
|
+
const KEEP_FORMAT = new Set(['image/png', 'image/webp']);
|
|
6
|
+
|
|
7
|
+
export function pickOutputType(inputType) {
|
|
8
|
+
return KEEP_FORMAT.has(inputType) ? inputType : 'image/jpeg';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function loadImageBitmap(file) {
|
|
12
|
+
if (typeof createImageBitmap === 'function') {
|
|
13
|
+
return createImageBitmap(file).catch(() => loadViaImgElement(file));
|
|
14
|
+
}
|
|
15
|
+
return loadViaImgElement(file);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function loadViaImgElement(file) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const url = URL.createObjectURL(file);
|
|
21
|
+
const img = new Image();
|
|
22
|
+
img.onload = () => { URL.revokeObjectURL(url); resolve(img); };
|
|
23
|
+
img.onerror = (e) => { URL.revokeObjectURL(url); reject(e); };
|
|
24
|
+
img.src = url;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function canvasToBlob(canvas, type, quality) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
canvas.toBlob(
|
|
31
|
+
(blob) => (blob ? resolve(blob) : reject(new Error('toBlob returned null'))),
|
|
32
|
+
type,
|
|
33
|
+
quality,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function resizeImageIfNeeded(file, maxDim = 2000) {
|
|
39
|
+
if (!file || typeof file !== 'object') return file;
|
|
40
|
+
const type = (file.type || '').toLowerCase();
|
|
41
|
+
if (!type.startsWith('image/')) return file;
|
|
42
|
+
// GIF 走 canvas 会只保留第一帧 + 转 JPEG,动图彻底丢。
|
|
43
|
+
// 直接放行原文件(Claude API 对 GIF 有自己的降采样,即便 >2000px 也由后端处理)。
|
|
44
|
+
if (type === 'image/gif') return file;
|
|
45
|
+
|
|
46
|
+
let source;
|
|
47
|
+
try {
|
|
48
|
+
source = await loadImageBitmap(file);
|
|
49
|
+
} catch {
|
|
50
|
+
return file;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const srcW = source.width || source.naturalWidth;
|
|
54
|
+
const srcH = source.height || source.naturalHeight;
|
|
55
|
+
if (!srcW || !srcH) {
|
|
56
|
+
try { source.close?.(); } catch {}
|
|
57
|
+
return file;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const maxSide = Math.max(srcW, srcH);
|
|
61
|
+
if (maxSide <= maxDim) {
|
|
62
|
+
try { source.close?.(); } catch {}
|
|
63
|
+
return file;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const scale = maxDim / maxSide;
|
|
67
|
+
const dstW = Math.max(1, Math.round(srcW * scale));
|
|
68
|
+
const dstH = Math.max(1, Math.round(srcH * scale));
|
|
69
|
+
|
|
70
|
+
const canvas = document.createElement('canvas');
|
|
71
|
+
canvas.width = dstW;
|
|
72
|
+
canvas.height = dstH;
|
|
73
|
+
const ctx = canvas.getContext('2d');
|
|
74
|
+
if (!ctx) {
|
|
75
|
+
try { source.close?.(); } catch {}
|
|
76
|
+
return file;
|
|
77
|
+
}
|
|
78
|
+
ctx.imageSmoothingEnabled = true;
|
|
79
|
+
ctx.imageSmoothingQuality = 'high';
|
|
80
|
+
try {
|
|
81
|
+
ctx.drawImage(source, 0, 0, dstW, dstH);
|
|
82
|
+
} catch {
|
|
83
|
+
try { source.close?.(); } catch {}
|
|
84
|
+
return file;
|
|
85
|
+
}
|
|
86
|
+
try { source.close?.(); } catch {}
|
|
87
|
+
|
|
88
|
+
const outType = pickOutputType(type);
|
|
89
|
+
const quality = outType === 'image/png' ? undefined : 0.92;
|
|
90
|
+
|
|
91
|
+
let blob;
|
|
92
|
+
try {
|
|
93
|
+
blob = await canvasToBlob(canvas, outType, quality);
|
|
94
|
+
} catch {
|
|
95
|
+
return file;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 走到这里说明 maxSide > maxDim,必须返回缩放版本以满足 API 尺寸限制
|
|
99
|
+
// (即使重编码后字节反而更大 —— 如低色 PNG 转 JPEG —— 也不能回退原图)
|
|
100
|
+
const nameOut = renameForType(file.name || 'image', outType);
|
|
101
|
+
return new File([blob], nameOut, { type: outType, lastModified: Date.now() });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function renameForType(originalName, outType) {
|
|
105
|
+
const ext = outType === 'image/jpeg' ? 'jpg'
|
|
106
|
+
: outType === 'image/png' ? 'png'
|
|
107
|
+
: outType === 'image/webp' ? 'webp'
|
|
108
|
+
: 'img';
|
|
109
|
+
const dot = originalName.lastIndexOf('.');
|
|
110
|
+
const stem = dot > 0 ? originalName.slice(0, dot) : originalName;
|
|
111
|
+
return `${stem}.${ext}`;
|
|
112
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// 冷启动摄取分帧管线基础设施 — 把 load_end 的同步 O(n×m) 处理切成批次,批间让出主线程。
|
|
2
|
+
// 背景:`ccv -c` 大会话的全量 checkpoint(单条数十 MB、数千 messages)让 reconstruct/slim/
|
|
3
|
+
// process 在一个任务里跑数十秒,Windows 上页面全面卡死。分帧不改变任何一步的执行顺序与
|
|
4
|
+
// 语义(同一循环插入 await 让步),只把长任务拆成 <16ms 批次。
|
|
5
|
+
// 纯函数 + 依赖注入,React-free,node:test 可直测。
|
|
6
|
+
|
|
7
|
+
export const INGEST_BATCH_SIZE = 250;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 让出主线程。优先 scheduler.yield()(Chrome/Edge 129+/Electron:带优先级续行,
|
|
11
|
+
* 不会排在无关任务之后);回退 setTimeout(0)(通用;await 链非嵌套,4ms clamp 不生效)。
|
|
12
|
+
* 不用 requestIdleCallback(交互期可被无限饿死,must-complete 管线会卡住)、
|
|
13
|
+
* 不用 rAF(后台标签页完全暂停 —— Windows 用户加载期 alt-tab 是常态)。
|
|
14
|
+
*/
|
|
15
|
+
export const yieldToMain = (typeof scheduler !== 'undefined' && typeof scheduler.yield === 'function')
|
|
16
|
+
? () => scheduler.yield()
|
|
17
|
+
: () => new Promise(resolve => setTimeout(resolve, 0));
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 分帧驱动一个严格有序的 per-index pass:对 i=0..total-1 顺序调用 step(i),
|
|
21
|
+
* 每 batchSize 个让步一次,让步后检查 shouldAbort。
|
|
22
|
+
*
|
|
23
|
+
* 调用序列与同步 for 循环完全一致 —— 仅插入 await,不重排、不切片、不并行,
|
|
24
|
+
* 保证有状态消费者(createEntrySlimmer / mergeMainAgentSessions 链)语义零变化。
|
|
25
|
+
*
|
|
26
|
+
* @param {number} total
|
|
27
|
+
* @param {(i: number) => void} step
|
|
28
|
+
* @param {object} [ctl]
|
|
29
|
+
* @param {() => boolean} [ctl.shouldAbort] - 每次让步后检查;true 则立即终止(不补跑剩余 step)
|
|
30
|
+
* @param {(done: number) => void} [ctl.onProgress] - 每批让步前 + 结束时回报已处理条数(单调递增)
|
|
31
|
+
* @param {() => Promise<void>} [ctl.yieldFn] - 让步原语(测试可注入)
|
|
32
|
+
* @param {number} [ctl.batchSize]
|
|
33
|
+
* @returns {Promise<{aborted: boolean}>}
|
|
34
|
+
*/
|
|
35
|
+
export async function runChunkedPass(total, step, ctl = {}) {
|
|
36
|
+
const { shouldAbort, onProgress, yieldFn = yieldToMain, batchSize = INGEST_BATCH_SIZE } = ctl;
|
|
37
|
+
for (let i = 0; i < total; i++) {
|
|
38
|
+
step(i);
|
|
39
|
+
if ((i + 1) % batchSize === 0 && i + 1 < total) {
|
|
40
|
+
if (onProgress) onProgress(i + 1);
|
|
41
|
+
await yieldFn();
|
|
42
|
+
if (shouldAbort && shouldAbort()) return { aborted: true };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (onProgress && total > 0) onProgress(total);
|
|
46
|
+
return { aborted: false };
|
|
47
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// 共享:动态 chunk 加载失败时的自愈逻辑。
|
|
2
|
+
//
|
|
3
|
+
// 触发场景:服务端升级后,dist/ 里 chunk 哈希变了;浏览器旧标签页 / 缓存的入口 JS
|
|
4
|
+
// 还在请求被替换的旧文件名 → server.js 给 /assets/ 不存在的请求返回 404 →
|
|
5
|
+
// import() 抛 TypeError "Failed to fetch dynamically imported module"。
|
|
6
|
+
//
|
|
7
|
+
// 自愈策略:每个 chunk name 单独维护一个 sessionStorage timestamp,5 分钟外允许
|
|
8
|
+
// reload 一次;窗口内的二次失败抛出原 error 让上游处理(避免连续两次升级死循环)。
|
|
9
|
+
// sessionStorage 在 Safari Private / quota 满 / sandboxed iframe / 严格 CSP 下访问会抛
|
|
10
|
+
// SecurityError 或 QuotaExceededError,全部用 try/catch 静默吞掉 —— 拿不到上次时间戳
|
|
11
|
+
// 时优先 reload(次数过多比"永不刷"更可恢复)。
|
|
12
|
+
//
|
|
13
|
+
// 三个导出:
|
|
14
|
+
// - shouldReloadStaleChunk(name) → boolean。仅判断 + 写时间戳,不做 reload。
|
|
15
|
+
// 用作底层 primitive,便于把"是否要刷"和"何时真刷"解耦。
|
|
16
|
+
// - reloadOnStaleChunk(name) → boolean。立即 reload。给 main.jsx 这种没 UI 的地方用。
|
|
17
|
+
// - handleStaleChunk(name, err, { onReload }) → Promise / throw。给 React.lazy 的 .catch
|
|
18
|
+
// 用:onReload 先跑(toast 之类),200ms 后才真 reload,让 UI 有机会画出来。
|
|
19
|
+
// 返回永不 resolve 的 Promise 把 React 卡在 Suspense fallback 里等 reload 接管。
|
|
20
|
+
|
|
21
|
+
const RELOAD_WINDOW_MS = 5 * 60 * 1000;
|
|
22
|
+
const RELOAD_GRACE_MS = 200; // 留给 toast / message 渲染一帧的时间
|
|
23
|
+
|
|
24
|
+
function safeStorage(key, value) {
|
|
25
|
+
try {
|
|
26
|
+
if (value === undefined) return Number(sessionStorage.getItem(key) || 0);
|
|
27
|
+
sessionStorage.setItem(key, value);
|
|
28
|
+
} catch {
|
|
29
|
+
/* private mode / quota / sandbox — 读返回 0(→ 触发一次 reload,比卡死好),写忽略 */
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function shouldReloadStaleChunk(name) {
|
|
35
|
+
const key = `_chunkReloadedAt:${name}`;
|
|
36
|
+
const last = safeStorage(key);
|
|
37
|
+
if (Date.now() - last <= RELOAD_WINDOW_MS) return false;
|
|
38
|
+
safeStorage(key, String(Date.now()));
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function reloadOnStaleChunk(name) {
|
|
43
|
+
if (!shouldReloadStaleChunk(name)) return false;
|
|
44
|
+
window.location.reload();
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function handleStaleChunk(name, err, { onReload } = {}) {
|
|
49
|
+
if (!shouldReloadStaleChunk(name)) throw err;
|
|
50
|
+
try { onReload?.(); } catch { /* toast 抛错不能阻止 reload */ }
|
|
51
|
+
setTimeout(() => window.location.reload(), RELOAD_GRACE_MS);
|
|
52
|
+
return new Promise(() => {}); // 让 Suspense fallback 一直显示直到 reload 接管
|
|
53
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { marked } from 'marked';
|
|
2
|
+
import DOMPurify from 'dompurify';
|
|
3
|
+
import { escapeHtml } from './helpers';
|
|
4
|
+
import { setupMermaidAutoRender } from '../hooks/useMermaidRender';
|
|
5
|
+
import { measureParse } from './markdownProfiler';
|
|
6
|
+
|
|
7
|
+
setupMermaidAutoRender();
|
|
8
|
+
|
|
9
|
+
const _mdCache = new Map();
|
|
10
|
+
const _MD_CACHE_MAX = 1024;
|
|
11
|
+
|
|
12
|
+
export function renderMarkdown(text) {
|
|
13
|
+
if (!text) return '';
|
|
14
|
+
const hit = _mdCache.get(text);
|
|
15
|
+
if (hit !== undefined) return hit;
|
|
16
|
+
|
|
17
|
+
let html;
|
|
18
|
+
try {
|
|
19
|
+
html = measureParse(() => DOMPurify.sanitize(marked.parse(text, { breaks: true })));
|
|
20
|
+
} catch (e) {
|
|
21
|
+
html = escapeHtml(text);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (_mdCache.size >= _MD_CACHE_MAX) {
|
|
25
|
+
// evict oldest (Map preserves insertion order)
|
|
26
|
+
_mdCache.delete(_mdCache.keys().next().value);
|
|
27
|
+
}
|
|
28
|
+
_mdCache.set(text, html);
|
|
29
|
+
return html;
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { renderMarkdown } from './markdown';
|
|
2
|
+
|
|
3
|
+
// 在"不在未闭合 fence 里"的最后一处 \n\n 切开:前半段是已稳定的完成块,
|
|
4
|
+
// 后半段是正在增长的尾巴。流式渲染时只有 tail 每个 chunk 重新解析,
|
|
5
|
+
// frozen 段走 renderMarkdown 的 _mdCache(几乎必中),解析时间下降 30-50%。
|
|
6
|
+
export function splitFrozenTail(text) {
|
|
7
|
+
if (!text) return ['', ''];
|
|
8
|
+
|
|
9
|
+
// 仍在未闭合 fence 内 —— 不能切,否则会把 ``` 切散
|
|
10
|
+
const fenceCount = (text.match(/^```/gm) || []).length;
|
|
11
|
+
if (fenceCount % 2 === 1) return ['', text];
|
|
12
|
+
|
|
13
|
+
// 存在引用式链接定义时保守回退 —— `[x]: url` 和 `[text][x]` 可能跨越分界
|
|
14
|
+
// 点,一旦定义落到 frozen 而使用落到 tail(或反过来),链接就不解析。
|
|
15
|
+
// LLM 回答里较少见,回退到全量解析是可接受的成本。
|
|
16
|
+
if (/^\[[^\]]+\]:\s/m.test(text)) return ['', text];
|
|
17
|
+
|
|
18
|
+
const idx = text.lastIndexOf('\n\n');
|
|
19
|
+
if (idx < 0) return ['', text];
|
|
20
|
+
|
|
21
|
+
// 分界点紧邻表格行(| 开头)时不切 —— 表头 + 分隔行和数据行要在同一次解析
|
|
22
|
+
// 里才能识别成 <table>,切开会渲染成两段残缺片段。
|
|
23
|
+
const prevLineStart = text.lastIndexOf('\n', idx - 1) + 1;
|
|
24
|
+
const lineBefore = text.slice(prevLineStart, idx);
|
|
25
|
+
const nextLineEnd = text.indexOf('\n', idx + 2);
|
|
26
|
+
const lineAfter = text.slice(idx + 2, nextLineEnd < 0 ? text.length : nextLineEnd);
|
|
27
|
+
if (lineBefore.trimStart().startsWith('|') || lineAfter.trimStart().startsWith('|')) {
|
|
28
|
+
return ['', text];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [text.slice(0, idx + 2), text.slice(idx + 2)];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 仅用于 trailingCursor=true(流式中)的渲染路径;
|
|
35
|
+
// 非流式或完成消息仍走 renderMarkdown 的单次全量解析 + 缓存。
|
|
36
|
+
export function renderIncremental(text) {
|
|
37
|
+
const [frozen, tail] = splitFrozenTail(text);
|
|
38
|
+
return renderMarkdown(frozen) + (tail ? renderMarkdown(tail) : '');
|
|
39
|
+
}
|