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,233 @@
|
|
|
1
|
+
// Singleton voice-pack playback for ApprovalModal / AppBase / Mobile.
|
|
2
|
+
//
|
|
3
|
+
// Responsibilities:
|
|
4
|
+
// 1. Resolve event → URL based on per-event binding ('default' | <uuid> | null).
|
|
5
|
+
// 2. Serialise playback through one shared <Audio> element (no overlap when
|
|
6
|
+
// multiple lifecycle events fire simultaneously —).
|
|
7
|
+
// 3. Honor a per-event cooldown so noisy events (turnEnd) don't spam the user.
|
|
8
|
+
// 4. Survive autoplay-policy block: play() rejection falls back to Web Audio
|
|
9
|
+
// chime so the user still gets *some* signal even before they've interacted.
|
|
10
|
+
// 5. Keep a separate Audio element for Settings preview so previews don't
|
|
11
|
+
// compete with live event playback.
|
|
12
|
+
//
|
|
13
|
+
// Pure module state — one queue per browsing context. Multi-tab Electron each
|
|
14
|
+
// have their own module instance (different webContents = different module);
|
|
15
|
+
// that's fine: each tab unlocks its own audio independently per browser policy.
|
|
16
|
+
|
|
17
|
+
import { apiUrl } from './apiUrl.js';
|
|
18
|
+
import { BUNDLED_PACK_IDS } from '../../server/lib/voice-pack-events.js';
|
|
19
|
+
|
|
20
|
+
// Mirror BUNDLED_PACK_IDS into a Set for O(1) lookup in the hot URL-building
|
|
21
|
+
// path. Frozen array ↔ Set conversion at module load is fine; the source list
|
|
22
|
+
// is itself frozen so the Set can't drift.
|
|
23
|
+
const BUNDLED_PACK_ID_SET = new Set(BUNDLED_PACK_IDS);
|
|
24
|
+
|
|
25
|
+
let mainAudio = null;
|
|
26
|
+
let previewAudio = null;
|
|
27
|
+
const queue = [];
|
|
28
|
+
let playing = false;
|
|
29
|
+
let unlocked = false;
|
|
30
|
+
const lastFireAt = new Map(); // eventKey → ts
|
|
31
|
+
const lastDedupeKey = new Map(); // eventKey → caller-supplied dedupe key
|
|
32
|
+
|
|
33
|
+
function urlForBinding(eventKey, binding) {
|
|
34
|
+
const head = '/api/voice-pack/audio';
|
|
35
|
+
// Bundled packs route as /<packId>/<eventKey>; user uploads as /<uuid>.
|
|
36
|
+
// null / undefined binding falls back to 'default' so the chime-fallback layer
|
|
37
|
+
// still gets a real URL to attempt (404 → chime is handled in startPlay).
|
|
38
|
+
// The Set check (not equality with 'default') is critical — without it,
|
|
39
|
+
// binding='sanguo' would route to /api/voice-pack/audio/sanguo (no event
|
|
40
|
+
// suffix), hit the uuid branch, fail isValidId, and 404 → silent chime.
|
|
41
|
+
const packId = (!binding) ? 'default' : binding;
|
|
42
|
+
const path = BUNDLED_PACK_ID_SET.has(packId)
|
|
43
|
+
? `${head}/${packId}/${encodeURIComponent(eventKey)}`
|
|
44
|
+
: `${head}/${encodeURIComponent(binding)}`;
|
|
45
|
+
// apiUrl injects the ?token=... LAN auth query when present — same path other API calls use.
|
|
46
|
+
return apiUrl(path);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getMainAudio() {
|
|
50
|
+
if (mainAudio) return mainAudio;
|
|
51
|
+
mainAudio = new Audio();
|
|
52
|
+
mainAudio.preload = 'auto';
|
|
53
|
+
mainAudio.addEventListener('ended', advanceQueue);
|
|
54
|
+
// 'error' also drains so a 404 / decode failure doesn't deadlock the queue.
|
|
55
|
+
mainAudio.addEventListener('error', () => { playChimeFallback(); advanceQueue(); });
|
|
56
|
+
return mainAudio;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let _webAudioCtx = null;
|
|
60
|
+
// Two-tone chime — exported so ApprovalModal can also use it for the legacy
|
|
61
|
+
// `soundEnabled` path. Single Web Audio context lives here (was duplicated
|
|
62
|
+
// across player + ApprovalModal until they were deduped here).
|
|
63
|
+
export function playChimeFallback() {
|
|
64
|
+
try {
|
|
65
|
+
const Ctx = window.AudioContext || window.webkitAudioContext;
|
|
66
|
+
if (!Ctx) return;
|
|
67
|
+
if (!_webAudioCtx) _webAudioCtx = new Ctx();
|
|
68
|
+
const ctx = _webAudioCtx;
|
|
69
|
+
const now = ctx.currentTime;
|
|
70
|
+
const osc = ctx.createOscillator();
|
|
71
|
+
const gain = ctx.createGain();
|
|
72
|
+
osc.type = 'sine';
|
|
73
|
+
osc.frequency.setValueAtTime(660, now);
|
|
74
|
+
osc.frequency.linearRampToValueAtTime(880, now + 0.18);
|
|
75
|
+
gain.gain.setValueAtTime(0, now);
|
|
76
|
+
gain.gain.linearRampToValueAtTime(0.18, now + 0.02);
|
|
77
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.24);
|
|
78
|
+
osc.connect(gain).connect(ctx.destination);
|
|
79
|
+
osc.start(now);
|
|
80
|
+
osc.stop(now + 0.26);
|
|
81
|
+
} catch { /* ignore */ }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function advanceQueue() {
|
|
85
|
+
playing = false;
|
|
86
|
+
const next = queue.shift();
|
|
87
|
+
if (next) startPlay(next);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function startPlay({ url, volume }) {
|
|
91
|
+
const audio = getMainAudio();
|
|
92
|
+
playing = true;
|
|
93
|
+
try {
|
|
94
|
+
audio.src = url;
|
|
95
|
+
audio.volume = Math.max(0, Math.min(1, volume));
|
|
96
|
+
const p = audio.play();
|
|
97
|
+
if (p && typeof p.catch === 'function') {
|
|
98
|
+
p.catch(() => {
|
|
99
|
+
// Autoplay blocked OR audio 404 — fire chime so user is at least notified,
|
|
100
|
+
// and immediately drain so we don't sit on a poisoned queue.
|
|
101
|
+
playChimeFallback();
|
|
102
|
+
advanceQueue();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
playChimeFallback();
|
|
107
|
+
advanceQueue();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Per-event cooldown (review: kept as a module constant, not user-configurable.
|
|
112
|
+
// The previous prefs.cooldownMs override was phantom config — neither persisted
|
|
113
|
+
// nor exposed in Settings — so it was removed to avoid half-built
|
|
114
|
+
// flexibility users couldn't actually use).
|
|
115
|
+
// SUNSET-MARKER: ccv-turn-end-debounce
|
|
116
|
+
// Frontend caller-side cooldown,作为 server 端 trailing debounce 漏掉时的兜底二保险
|
|
117
|
+
// (SSE 重连重发、server 罕见广播失序等)。**与 server `CCV_TURN_END_DEBOUNCE_MS`
|
|
118
|
+
// 自动对齐**:AppBase.jsx 监听 SSE `server_config` 事件并调 `setTurnEndCooldownMs` 注入
|
|
119
|
+
// server 实际生效的 debounce 值,无需运维手工同步两边常量。
|
|
120
|
+
const COOLDOWN_MS = {
|
|
121
|
+
turnEnd: 10_000,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Update the turnEnd caller-side cooldown at runtime — called by AppBase on
|
|
126
|
+
* receipt of `server_config` SSE event. Keeps frontend in sync with the server's
|
|
127
|
+
* `CCV_TURN_END_DEBOUNCE_MS` env override without redeploy.
|
|
128
|
+
* Mirrors server clamp [100, 60000] so a malformed/tampered server_config payload
|
|
129
|
+
* (n=0 disables cooldown → voice spam) can't kill the二保险.
|
|
130
|
+
*/
|
|
131
|
+
export function setTurnEndCooldownMs(ms) {
|
|
132
|
+
const n = Number(ms);
|
|
133
|
+
if (!Number.isFinite(n) || n < 100 || n > 60_000) {
|
|
134
|
+
try { console.warn(`[voicePack] setTurnEndCooldownMs(${ms}) out of [100,60000]; keeping`, COOLDOWN_MS.turnEnd); } catch {}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
COOLDOWN_MS.turnEnd = n;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Main entry point.
|
|
141
|
+
// eventKey: 'planApproval' | 'askQuestion' | 'turnEnd'
|
|
142
|
+
// prefs: prefs.approvalModal.voicePack — { enabled, volume, events: { ... } }
|
|
143
|
+
// opts.dedupeKey: optional string; second call with same key for same event is ignored
|
|
144
|
+
export function playEvent(eventKey, prefs, opts = {}) {
|
|
145
|
+
if (typeof window === 'undefined') return false;
|
|
146
|
+
if (!prefs || prefs.enabled !== true) return false;
|
|
147
|
+
const binding = prefs.events && prefs.events[eventKey];
|
|
148
|
+
if (binding === null || binding === undefined) return false; // event disabled
|
|
149
|
+
|
|
150
|
+
// Per-event cooldown (constants only — no prefs override; see COOLDOWN_MS).
|
|
151
|
+
const cooldown = COOLDOWN_MS[eventKey] || 0;
|
|
152
|
+
if (cooldown > 0) {
|
|
153
|
+
const last = lastFireAt.get(eventKey) || 0;
|
|
154
|
+
if (Date.now() - last < cooldown) return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Caller-side dedupe (e.g. same approval modal kind re-rendering shouldn't
|
|
158
|
+
// refire). Per-event key isolation so concurrent kinds don't suppress each other.
|
|
159
|
+
if (opts.dedupeKey) {
|
|
160
|
+
if (lastDedupeKey.get(eventKey) === opts.dedupeKey) return false;
|
|
161
|
+
lastDedupeKey.set(eventKey, opts.dedupeKey);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
lastFireAt.set(eventKey, Date.now());
|
|
165
|
+
const url = urlForBinding(eventKey, binding);
|
|
166
|
+
const volume = typeof prefs.volume === 'number' ? Math.max(0, Math.min(1, prefs.volume)) : 0.3;
|
|
167
|
+
|
|
168
|
+
if (playing) {
|
|
169
|
+
queue.push({ url, volume });
|
|
170
|
+
} else {
|
|
171
|
+
startPlay({ url, volume });
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Settings preview — independent of the live queue, so previewing while a
|
|
177
|
+
// real event is playing doesn't either kill the live audio or wait behind it.
|
|
178
|
+
export function previewEvent(eventKey, prefs) {
|
|
179
|
+
if (typeof window === 'undefined') return Promise.resolve();
|
|
180
|
+
if (!previewAudio) previewAudio = new Audio();
|
|
181
|
+
try {
|
|
182
|
+
previewAudio.pause();
|
|
183
|
+
previewAudio.currentTime = 0;
|
|
184
|
+
} catch { /* ignore */ }
|
|
185
|
+
const binding = (prefs?.events && prefs.events[eventKey]) || 'default';
|
|
186
|
+
previewAudio.src = urlForBinding(eventKey, binding);
|
|
187
|
+
previewAudio.volume = typeof prefs?.volume === 'number' ? Math.max(0, Math.min(1, prefs.volume)) : 0.3;
|
|
188
|
+
const p = previewAudio.play();
|
|
189
|
+
if (p && typeof p.catch === 'function') {
|
|
190
|
+
return p.catch(() => { playChimeFallback(); });
|
|
191
|
+
}
|
|
192
|
+
return Promise.resolve();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function stopPreview() {
|
|
196
|
+
if (previewAudio) {
|
|
197
|
+
try { previewAudio.pause(); previewAudio.currentTime = 0; } catch { /* ignore */ }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Call from a click / keypress handler (Settings open is a good spot) to satisfy
|
|
202
|
+
// Chrome/Safari/Firefox autoplay policy. Plays a silent 1-sample WAV so the
|
|
203
|
+
// audio element is "blessed" for programmatic playback for the rest of the tab's life.
|
|
204
|
+
export function unlockAudio() {
|
|
205
|
+
if (unlocked) return Promise.resolve(true);
|
|
206
|
+
if (typeof window === 'undefined') return Promise.resolve(false);
|
|
207
|
+
try {
|
|
208
|
+
const audio = getMainAudio();
|
|
209
|
+
audio.volume = 0;
|
|
210
|
+
audio.src = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=';
|
|
211
|
+
const p = audio.play();
|
|
212
|
+
if (p && typeof p.then === 'function') {
|
|
213
|
+
return p.then(() => { unlocked = true; return true; }).catch(() => false);
|
|
214
|
+
}
|
|
215
|
+
unlocked = true;
|
|
216
|
+
return Promise.resolve(true);
|
|
217
|
+
} catch { return Promise.resolve(false); }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Test hook — reset module state between unit tests. **Do not call from app code.**
|
|
221
|
+
// Exported as a named function so an "unused exports" linter can see it's referenced
|
|
222
|
+
// by test/voice-pack-player.test.js (— without this, a future cleanup pass
|
|
223
|
+
// could mistake it for dead code).
|
|
224
|
+
export function _resetForTests() {
|
|
225
|
+
mainAudio = null;
|
|
226
|
+
previewAudio = null;
|
|
227
|
+
queue.length = 0;
|
|
228
|
+
playing = false;
|
|
229
|
+
unlocked = false;
|
|
230
|
+
lastFireAt.clear();
|
|
231
|
+
lastDedupeKey.clear();
|
|
232
|
+
_webAudioCtx = null;
|
|
233
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web search rendering helpers.
|
|
3
|
+
*
|
|
4
|
+
* Anthropic 的 server-side web_search 工具在 assistant message 的 content 数组里
|
|
5
|
+
* 直接放两个兄弟块:server_tool_use(搜索发起)+ web_search_tool_result(结果)。
|
|
6
|
+
* 紧随其后的 text 块(含带 citations 的)是模型基于结果生成的综合回复。
|
|
7
|
+
*
|
|
8
|
+
* 本文件提供:
|
|
9
|
+
* - extractWebSearchGroups(content): 识别 [server_tool_use, web_search_tool_result, ...synthesis text]
|
|
10
|
+
* 子序列,返回 groups + content 全局 index 的 consumedIndices Set。
|
|
11
|
+
* - safeHref(url): URL 协议白名单(仅 http/https),防 javascript:/data:/file:// 协议 XSS。
|
|
12
|
+
* - getHostname(url): try/catch 包装的 new URL(url).hostname。
|
|
13
|
+
*
|
|
14
|
+
* 设计上故意做成纯 JS(无 React/CSS 依赖),让 Node test runner 能直接 import。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export function extractWebSearchGroups(content) {
|
|
18
|
+
const groups = [];
|
|
19
|
+
const consumedIndices = new Set();
|
|
20
|
+
|
|
21
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
22
|
+
return { groups, consumedIndices };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < content.length; i++) {
|
|
26
|
+
const block = content[i];
|
|
27
|
+
if (!block || typeof block !== 'object') continue;
|
|
28
|
+
|
|
29
|
+
if (block.type === 'server_tool_use') {
|
|
30
|
+
let resultIdx = -1;
|
|
31
|
+
for (let j = i + 1; j < content.length; j++) {
|
|
32
|
+
const b = content[j];
|
|
33
|
+
if (!b || typeof b !== 'object') continue;
|
|
34
|
+
if (b.type === 'thinking') continue;
|
|
35
|
+
if (b.type === 'web_search_tool_result' && b.tool_use_id === block.id) {
|
|
36
|
+
resultIdx = j;
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (resultIdx === -1) {
|
|
42
|
+
groups.push({
|
|
43
|
+
serverToolUseIndex: i,
|
|
44
|
+
serverToolUse: block,
|
|
45
|
+
webSearchResultIndex: -1,
|
|
46
|
+
webSearchResult: null,
|
|
47
|
+
synthesisTextIndices: [],
|
|
48
|
+
});
|
|
49
|
+
consumedIndices.add(i);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const synthesisIndices = [];
|
|
54
|
+
for (let k = resultIdx + 1; k < content.length; k++) {
|
|
55
|
+
const b = content[k];
|
|
56
|
+
if (!b || typeof b !== 'object') break;
|
|
57
|
+
if (b.type === 'text') {
|
|
58
|
+
synthesisIndices.push(k);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (b.type === 'thinking') break;
|
|
62
|
+
if (b.type === 'tool_use' || b.type === 'server_tool_use') break;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
groups.push({
|
|
67
|
+
serverToolUseIndex: i,
|
|
68
|
+
serverToolUse: block,
|
|
69
|
+
webSearchResultIndex: resultIdx,
|
|
70
|
+
webSearchResult: content[resultIdx],
|
|
71
|
+
synthesisTextIndices: synthesisIndices,
|
|
72
|
+
});
|
|
73
|
+
consumedIndices.add(i);
|
|
74
|
+
consumedIndices.add(resultIdx);
|
|
75
|
+
for (const k of synthesisIndices) consumedIndices.add(k);
|
|
76
|
+
} else if (block.type === 'web_search_tool_result') {
|
|
77
|
+
if (consumedIndices.has(i)) continue;
|
|
78
|
+
groups.push({
|
|
79
|
+
serverToolUseIndex: -1,
|
|
80
|
+
serverToolUse: null,
|
|
81
|
+
webSearchResultIndex: i,
|
|
82
|
+
webSearchResult: block,
|
|
83
|
+
synthesisTextIndices: [],
|
|
84
|
+
});
|
|
85
|
+
consumedIndices.add(i);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { groups, consumedIndices };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const SAFE_PROTOCOLS = new Set(['http:', 'https:']);
|
|
93
|
+
|
|
94
|
+
export function safeHref(url) {
|
|
95
|
+
if (typeof url !== 'string' || !url) return null;
|
|
96
|
+
try {
|
|
97
|
+
const u = new URL(url);
|
|
98
|
+
return SAFE_PROTOCOLS.has(u.protocol) ? url : null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getHostname(url) {
|
|
105
|
+
if (typeof url !== 'string' || !url) return '';
|
|
106
|
+
try {
|
|
107
|
+
return new URL(url).hostname;
|
|
108
|
+
} catch {
|
|
109
|
+
return url.length > 40 ? url.slice(0, 40) + '…' : url;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow 面板共享格式化工具(WorkflowPanel 与 WorkflowLiveHud 共用,纯函数无 CSS 依赖)。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const TERMINAL_STATES = new Set(['done', 'completed', 'failed', 'error', 'cancelled', 'skipped']);
|
|
6
|
+
|
|
7
|
+
// 工作流状态 → i18n key(缺失时调用方回退到原始 status 字符串)。
|
|
8
|
+
export const STATUS_KEYS = {
|
|
9
|
+
running: 'ui.workflow.status.running',
|
|
10
|
+
finishing: 'ui.workflow.status.finishing',
|
|
11
|
+
completed: 'ui.workflow.status.completed',
|
|
12
|
+
failed: 'ui.workflow.status.failed',
|
|
13
|
+
paused: 'ui.workflow.status.paused',
|
|
14
|
+
cancelled: 'ui.workflow.status.cancelled',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function fmtDuration(ms) {
|
|
18
|
+
if (typeof ms !== 'number' || ms < 0) return '';
|
|
19
|
+
const s = Math.round(ms / 1000);
|
|
20
|
+
if (s < 60) return `${s}s`;
|
|
21
|
+
const m = Math.floor(s / 60);
|
|
22
|
+
const rem = s % 60;
|
|
23
|
+
return rem ? `${m}m ${rem}s` : `${m}m`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function fmtTokens(n) {
|
|
27
|
+
if (typeof n !== 'number' || n <= 0) return '0';
|
|
28
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
29
|
+
return String(n);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isRunning(state) {
|
|
33
|
+
return !TERMINAL_STATES.has(state);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function stateGlyph(state) {
|
|
37
|
+
if (state === 'done' || state === 'completed') return '✓';
|
|
38
|
+
if (state === 'failed' || state === 'error') return '✗';
|
|
39
|
+
if (state === 'queued') return '○';
|
|
40
|
+
return '●';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 甘特完成条按阶段着色:柔和、明暗主题皆可读的循环色相调色板。
|
|
44
|
+
// 失败/运行中/排队仍走语义色(红/主色脉冲/灰),不取这里的值。
|
|
45
|
+
const PHASE_HUES = [206, 265, 150, 32, 322, 188, 96, 0];
|
|
46
|
+
|
|
47
|
+
export function phaseColor(phaseIndex) {
|
|
48
|
+
const n = PHASE_HUES.length;
|
|
49
|
+
const i = typeof phaseIndex === 'number' && phaseIndex >= 0 ? phaseIndex : 0;
|
|
50
|
+
const h = PHASE_HUES[i % n];
|
|
51
|
+
return `hsl(${h}, 48%, 50%)`;
|
|
52
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从当前会话的 requests 提取所有 Workflow(UltraCode)run。
|
|
3
|
+
*
|
|
4
|
+
* 数据源:tool_result 块在 requests[i].body.messages 的 **user 轮**(与 toolResultBuilder.js
|
|
5
|
+
* 的全局索引扫描同源,见其 role==='user' 分支)。每个 Workflow 工具的后台启动返回文本
|
|
6
|
+
* 原生携带 Task ID / Run ID / Transcript dir 路径(含 sessionId),由 parseWorkflowFromText 解析,
|
|
7
|
+
* 无需服务端 _ccvWorkflow 注入——历史日志(含旧日志)同样可枚举。
|
|
8
|
+
*
|
|
9
|
+
* 镜像 teamSessionParser.js 的 extractTeamSessions 模式:纯函数、无副作用,便于 node --test 直接 import。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { extractToolResultText, parseWorkflowFromText } from './toolResultCore.js';
|
|
13
|
+
|
|
14
|
+
const SUMMARY_RE = /Summary:\s*(.+)/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 枚举 requests 里的全部 workflow run,按 taskId 主键去重(回填缺失的 runId/sessionId),
|
|
18
|
+
* 按最早出现时间倒序(最新在前)。
|
|
19
|
+
*
|
|
20
|
+
* @param {Array} requests - ChatView 的 requests 数组(每项含 timestamp 与 body.messages)
|
|
21
|
+
* @returns {Array<{ runId: string|null, taskId: string|null, sessionId: string|null,
|
|
22
|
+
* resultText: string, timestamp: any, summary: string|null }>}
|
|
23
|
+
*/
|
|
24
|
+
export function extractWorkflowRuns(requests) {
|
|
25
|
+
if (!Array.isArray(requests)) return [];
|
|
26
|
+
|
|
27
|
+
// 以「逻辑 run 键」聚合:taskId 优先(launch 文本必有、1:1 对应一次 run),
|
|
28
|
+
// 缺失才退回 runId。合并同键时回填先到条目缺失的字段,避免「仅 taskId」与
|
|
29
|
+
// 「taskId+runId」因键漂移而漏去重。
|
|
30
|
+
const byKey = new Map();
|
|
31
|
+
|
|
32
|
+
// tool_use_id → 工具名。仅认 Workflow 工具产生的 tool_result——否则其它工具
|
|
33
|
+
// (Read/Bash/Write 等)若回显含 "Workflow launched in background"/"Run ID:" 字面量的
|
|
34
|
+
// 源码或日志,会被 parseWorkflowFromText 误判为一次真实启动(自引用误检)。
|
|
35
|
+
// 与 ToolResultView 渲染内联面板前判 toolName==='Workflow' 同源,扫法对齐 toolResultBuilder。
|
|
36
|
+
const toolNameById = buildToolUseNameMap(requests);
|
|
37
|
+
|
|
38
|
+
for (const req of requests) {
|
|
39
|
+
const msgs = req?.body?.messages;
|
|
40
|
+
if (!Array.isArray(msgs)) continue;
|
|
41
|
+
for (const m of msgs) {
|
|
42
|
+
if (m?.role !== 'user' || !Array.isArray(m.content)) continue;
|
|
43
|
+
for (const block of m.content) {
|
|
44
|
+
if (block?.type !== 'tool_result') continue;
|
|
45
|
+
if (toolNameById.get(block.tool_use_id) !== 'Workflow') continue;
|
|
46
|
+
const txt = extractToolResultText(block);
|
|
47
|
+
const wf = parseWorkflowFromText(txt);
|
|
48
|
+
if (!wf) continue;
|
|
49
|
+
|
|
50
|
+
const key = wf.taskId || wf.runId;
|
|
51
|
+
if (!key) continue;
|
|
52
|
+
const summary = (txt.match(SUMMARY_RE) || [])[1]?.trim() || null;
|
|
53
|
+
|
|
54
|
+
const existing = byKey.get(key);
|
|
55
|
+
if (!existing) {
|
|
56
|
+
byKey.set(key, {
|
|
57
|
+
runId: wf.runId,
|
|
58
|
+
taskId: wf.taskId,
|
|
59
|
+
sessionId: wf.sessionId,
|
|
60
|
+
resultText: txt,
|
|
61
|
+
timestamp: req?.timestamp ?? null,
|
|
62
|
+
summary,
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
// 回填缺失字段(取任一非空);timestamp 保留最早出现的一条。
|
|
66
|
+
if (!existing.runId && wf.runId) existing.runId = wf.runId;
|
|
67
|
+
if (!existing.taskId && wf.taskId) existing.taskId = wf.taskId;
|
|
68
|
+
if (!existing.sessionId && wf.sessionId) existing.sessionId = wf.sessionId;
|
|
69
|
+
if (!existing.summary && summary) existing.summary = summary;
|
|
70
|
+
if (isEarlier(req?.timestamp, existing.timestamp)) {
|
|
71
|
+
existing.timestamp = req.timestamp;
|
|
72
|
+
existing.resultText = txt;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 最新在前:timestamp 降序;无 timestamp 的排末尾。
|
|
80
|
+
return Array.from(byKey.values()).sort((a, b) => tsValue(b.timestamp) - tsValue(a.timestamp));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 扫 assistant 轮(body.messages)与当前轮 response(response.body.content)的 tool_use,
|
|
85
|
+
* 建 id → name 映射。tool_result 凭 tool_use_id 反查工具名,只放行 Workflow。
|
|
86
|
+
*/
|
|
87
|
+
function buildToolUseNameMap(requests) {
|
|
88
|
+
const map = new Map();
|
|
89
|
+
for (const req of requests) {
|
|
90
|
+
const msgs = req?.body?.messages;
|
|
91
|
+
if (Array.isArray(msgs)) {
|
|
92
|
+
for (const m of msgs) {
|
|
93
|
+
if (m?.role === 'assistant' && Array.isArray(m.content)) {
|
|
94
|
+
for (const b of m.content) {
|
|
95
|
+
if (b?.type === 'tool_use' && b.id) map.set(b.id, b.name);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const respContent = req?.response?.body?.content;
|
|
101
|
+
if (Array.isArray(respContent)) {
|
|
102
|
+
for (const b of respContent) {
|
|
103
|
+
if (b?.type === 'tool_use' && b.id) map.set(b.id, b.name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return map;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function tsValue(ts) {
|
|
111
|
+
if (ts == null) return -Infinity;
|
|
112
|
+
const n = typeof ts === 'number' ? ts : Date.parse(ts);
|
|
113
|
+
return Number.isNaN(n) ? -Infinity : n;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isEarlier(candidate, current) {
|
|
117
|
+
return tsValue(candidate) < tsValue(current);
|
|
118
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow live store
|
|
3
|
+
*
|
|
4
|
+
* 轻量模块级发布订阅,承接服务端 SSE `workflow_update` 事件,按 runId(及 taskId 别名)
|
|
5
|
+
* 分发给对应的 WorkflowPanel。避免 AppBase→ChatView→ChatMessage→ToolResultView→Panel
|
|
6
|
+
* 深层 prop 穿线。
|
|
7
|
+
*
|
|
8
|
+
* - publish(payload): AppBase 收到 workflow_update 时调用,payload = { runId, taskId, data, ... }。
|
|
9
|
+
* - subscribe(key, cb): WorkflowPanel 按自身 runId 或 taskId 订阅,返回退订函数。
|
|
10
|
+
* - getLatest(key): 取已缓存的最新 journal(panel 挂载时若已先到事件可即时用)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const _subs = new Map(); // key(runId|taskId) → Set<cb>
|
|
14
|
+
const _latest = new Map(); // key → normalized journal data
|
|
15
|
+
const _authoritative = new Set(); // keys that已收到 live===false 的权威完成快照
|
|
16
|
+
|
|
17
|
+
// 「活跃工作流」追踪:供右下角悬浮 HUD 常驻展示运行中的工作流。
|
|
18
|
+
const _active = new Map(); // runId → 最新 data(仅运行中/收尾中)
|
|
19
|
+
const _activeSubs = new Set(); // Set<cb(list)>
|
|
20
|
+
|
|
21
|
+
function _emit(key, data) {
|
|
22
|
+
const set = _subs.get(key);
|
|
23
|
+
if (!set) return;
|
|
24
|
+
for (const cb of set) {
|
|
25
|
+
try { cb(data); } catch {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function _emitActive() {
|
|
30
|
+
const list = [..._active.values()];
|
|
31
|
+
for (const cb of _activeSubs) {
|
|
32
|
+
try { cb(list); } catch {}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _isActive(data) {
|
|
37
|
+
return data.live === true || data.status === 'running' || data.status === 'finishing';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function publish(payload) {
|
|
41
|
+
if (!payload || typeof payload !== 'object') return;
|
|
42
|
+
const data = payload.data;
|
|
43
|
+
if (!data || typeof data !== 'object') return;
|
|
44
|
+
const keys = [];
|
|
45
|
+
if (payload.runId) keys.push(payload.runId);
|
|
46
|
+
if (data.runId && data.runId !== payload.runId) keys.push(data.runId);
|
|
47
|
+
if (payload.taskId) keys.push(payload.taskId);
|
|
48
|
+
if (data.taskId && data.taskId !== payload.taskId) keys.push(data.taskId);
|
|
49
|
+
// 权威完成快照(live!==true)一旦到达即锁定该 key:忽略其后乱序到达的运行中逐帧,
|
|
50
|
+
// 否则尾随的 live 帧会把面板从「已完成」回退成「运行中」。
|
|
51
|
+
const isLive = data.live === true;
|
|
52
|
+
for (const k of keys) {
|
|
53
|
+
if (isLive && _authoritative.has(k)) continue;
|
|
54
|
+
if (!isLive) _authoritative.add(k);
|
|
55
|
+
_latest.set(k, data);
|
|
56
|
+
_emit(k, data);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 维护活跃工作流集合(按 canonical runId 去重,避免 taskId 重复计数)
|
|
60
|
+
const rk = data.runId || payload.runId;
|
|
61
|
+
if (rk && !(isLive && _authoritative.has(rk))) {
|
|
62
|
+
if (_isActive(data)) { _active.set(rk, data); _emitActive(); }
|
|
63
|
+
else if (_active.delete(rk)) { _emitActive(); }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 订阅活跃(运行中/收尾中)工作流列表变化;立即不回调,调用 getActiveWorkflows 取初值。 */
|
|
68
|
+
export function subscribeActive(cb) {
|
|
69
|
+
if (typeof cb !== 'function') return () => {};
|
|
70
|
+
_activeSubs.add(cb);
|
|
71
|
+
return () => { _activeSubs.delete(cb); };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 取当前活跃工作流列表(最新 data 数组)。 */
|
|
75
|
+
export function getActiveWorkflows() {
|
|
76
|
+
return [..._active.values()];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function subscribe(key, cb) {
|
|
80
|
+
if (!key || typeof cb !== 'function') return () => {};
|
|
81
|
+
let set = _subs.get(key);
|
|
82
|
+
if (!set) { set = new Set(); _subs.set(key, set); }
|
|
83
|
+
set.add(cb);
|
|
84
|
+
return () => {
|
|
85
|
+
const s = _subs.get(key);
|
|
86
|
+
if (!s) return;
|
|
87
|
+
s.delete(cb);
|
|
88
|
+
if (s.size === 0) _subs.delete(key);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getLatest(key) {
|
|
93
|
+
return key ? (_latest.get(key) || null) : null;
|
|
94
|
+
}
|