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,48 @@
|
|
|
1
|
+
// SVG sanitize: allow SMIL animation tags through DOMPurify's strict svg profile so previewed
|
|
2
|
+
// *.svg files animate, but reject <set>/<animate*> retargeting onto on*/href/style — defending
|
|
3
|
+
// against post-sanitize attribute injection via the SMIL animation engine.
|
|
4
|
+
//
|
|
5
|
+
// Uses a PRIVATE DOMPurify instance so the SMIL guard hook stays scoped to SVG sanitize calls
|
|
6
|
+
// (no leakage into the markdown / Mermaid sanitize pipelines that share the default singleton).
|
|
7
|
+
|
|
8
|
+
import DOMPurify from 'dompurify';
|
|
9
|
+
|
|
10
|
+
export const SMIL_TAGS = new Set(['set', 'animate', 'animatemotion', 'animatetransform']);
|
|
11
|
+
export const FORBIDDEN_ANIM_TARGET = /^(on|href$|xlink:href$|style$)/i;
|
|
12
|
+
|
|
13
|
+
export function isHostileAnimAttr(tagName, attrName, attrValue) {
|
|
14
|
+
// DOMPurify lowercases attrName before passing to hooks; the explicit toLowerCase is cheap
|
|
15
|
+
// insurance against version drift / direct callers (unit tests).
|
|
16
|
+
if (String(attrName || '').toLowerCase() !== 'attributename') return false;
|
|
17
|
+
if (!SMIL_TAGS.has((tagName || '').toLowerCase())) return false;
|
|
18
|
+
return FORBIDDEN_ANIM_TARGET.test(String(attrValue || '').trim());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const SVG_SANITIZE_CONFIG = Object.freeze({
|
|
22
|
+
USE_PROFILES: { svg: true },
|
|
23
|
+
ADD_TAGS: ['animate', 'animateMotion', 'animateTransform', 'set', 'mpath'],
|
|
24
|
+
ADD_ATTR: [
|
|
25
|
+
'attributeName', 'attributeType', 'calcMode', 'values', 'keyTimes', 'keySplines',
|
|
26
|
+
'dur', 'repeatCount', 'repeatDur', 'begin', 'end', 'from', 'to', 'by',
|
|
27
|
+
'restart', 'min', 'max', 'additive', 'accumulate', 'path', 'rotate',
|
|
28
|
+
'fill', 'href', 'xlink:href', 'type', 'origin',
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// DOMPurify default export is both an instance and a factory: calling it with a window returns a
|
|
33
|
+
// fresh instance whose hook chain is independent. In SSR / node:test (no window) the default
|
|
34
|
+
// instance is a stub without addHook/sanitize — guard so this module is import-safe everywhere.
|
|
35
|
+
const svgPurify = typeof window !== 'undefined' ? DOMPurify(window) : DOMPurify;
|
|
36
|
+
|
|
37
|
+
if (typeof svgPurify.addHook === 'function') {
|
|
38
|
+
svgPurify.addHook('uponSanitizeAttribute', (node, data) => {
|
|
39
|
+
if (isHostileAnimAttr(node && node.nodeName, data && data.attrName, data && data.attrValue)) {
|
|
40
|
+
data.keepAttr = false;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sanitizeSvg(text) {
|
|
46
|
+
if (typeof svgPurify.sanitize !== 'function') return text;
|
|
47
|
+
return svgPurify.sanitize(text, SVG_SANITIZE_CONFIG);
|
|
48
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const SYSTEM_TAGS = [
|
|
2
|
+
'system-reminder', 'local-command-caveat', 'project-reminder',
|
|
3
|
+
'important-instruction-reminders', 'file-modified-reminder', 'todo-reminder',
|
|
4
|
+
'user-prompt-submit-hook', 'local-command-stdout', 'command-name',
|
|
5
|
+
'task-notification', 'environment_details', 'context'
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export function parseSystemTags(text) {
|
|
9
|
+
if (!text) return { segments: [] };
|
|
10
|
+
const tagPattern = new RegExp(
|
|
11
|
+
'<(' + SYSTEM_TAGS.join('|') + ')\\b[^>]*>([\\s\\S]*?)</\\1>',
|
|
12
|
+
'gi'
|
|
13
|
+
);
|
|
14
|
+
const segments = [];
|
|
15
|
+
let lastIndex = 0;
|
|
16
|
+
let match;
|
|
17
|
+
while ((match = tagPattern.exec(text)) !== null) {
|
|
18
|
+
if (match.index > lastIndex) {
|
|
19
|
+
const before = text.slice(lastIndex, match.index).trim();
|
|
20
|
+
if (before) segments.push({ type: 'text', content: before });
|
|
21
|
+
}
|
|
22
|
+
segments.push({ type: 'system-tag', tag: match[1], content: match[2].trim() });
|
|
23
|
+
lastIndex = match.index + match[0].length;
|
|
24
|
+
}
|
|
25
|
+
if (lastIndex < text.length) {
|
|
26
|
+
const rest = text.slice(lastIndex).trim();
|
|
27
|
+
if (rest) segments.push({ type: 'text', content: rest });
|
|
28
|
+
}
|
|
29
|
+
return { segments };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const _tagCache = new Map();
|
|
33
|
+
const _TAG_CACHE_MAX = 512;
|
|
34
|
+
|
|
35
|
+
export function renderAssistantText(text) {
|
|
36
|
+
if (!text) return { segments: [] };
|
|
37
|
+
const hit = _tagCache.get(text);
|
|
38
|
+
if (hit) return hit;
|
|
39
|
+
|
|
40
|
+
const result = parseSystemTags(text);
|
|
41
|
+
|
|
42
|
+
if (_tagCache.size >= _TAG_CACHE_MAX) {
|
|
43
|
+
_tagCache.delete(_tagCache.keys().next().value);
|
|
44
|
+
}
|
|
45
|
+
_tagCache.set(text, result);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Claude 配置目录感知的 i18n 包装。由 AppBase 在 /api/preferences 响应里调
|
|
2
|
+
// setClaudeConfigDir() 注入;tc(key) 在调用时把 {configDir} 占位符替换成
|
|
3
|
+
// home-friendly 展示路径(默认 "~/.claude",设了 CLAUDE_CONFIG_DIR 时为绝对路径)。
|
|
4
|
+
|
|
5
|
+
import { t } from '../i18n';
|
|
6
|
+
|
|
7
|
+
let _configDir = '~/.claude';
|
|
8
|
+
|
|
9
|
+
export function setClaudeConfigDir(path) {
|
|
10
|
+
if (typeof path === 'string' && path.length > 0) _configDir = path;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getClaudeConfigDir() {
|
|
14
|
+
return _configDir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 注入的 configDir 不可被 params 覆盖——UI 必须反映真实的配置目录。
|
|
18
|
+
export function tc(key, params) {
|
|
19
|
+
return t(key, { ...params, configDir: _configDir });
|
|
20
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build team modal visualization data from requests and sessions.
|
|
3
|
+
* Pure function — no React/state dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { classifyUserContent, isMainAgent, extractDisplayText } from './contentFilter';
|
|
7
|
+
import { restoreSlimmedEntry } from './entry-slim.js';
|
|
8
|
+
import { classifyRequest, formatRequestTag, formatTeammateLabel } from './requestType';
|
|
9
|
+
import { getModelInfo, getEffectiveModel } from './helpers';
|
|
10
|
+
import { getTeammateAvatar } from './teammateAvatars';
|
|
11
|
+
import { buildSubAgentResultMap, buildGlobalToolResultIndex } from './toolResultBuilder';
|
|
12
|
+
|
|
13
|
+
export function buildTeamModalData(team, requests, mainAgentSessions) {
|
|
14
|
+
const startIdx = team.requestIndex;
|
|
15
|
+
const endIdx = team.endRequestIndex != null ? team.endRequestIndex + 1 : requests.length;
|
|
16
|
+
const teamRequests = requests.slice(startIdx, endIdx);
|
|
17
|
+
const teamStartTime = team.startTime;
|
|
18
|
+
const teamEndTime = team.endTime || (requests[endIdx - 1]?.response?.timestamp || requests[endIdx - 1]?.timestamp);
|
|
19
|
+
|
|
20
|
+
// 构建 tsToIndex 和 modelInfo
|
|
21
|
+
const tsToIndex = {};
|
|
22
|
+
let modelName = null;
|
|
23
|
+
for (let i = startIdx; i < endIdx && i < requests.length; i++) {
|
|
24
|
+
const req = requests[i];
|
|
25
|
+
if (req.timestamp) tsToIndex[req.timestamp] = i;
|
|
26
|
+
const effective = getEffectiveModel(req);
|
|
27
|
+
if (effective) modelName = effective;
|
|
28
|
+
}
|
|
29
|
+
const modelInfo = getModelInfo(modelName);
|
|
30
|
+
|
|
31
|
+
// 用户消息提取:展示触发 team 的用户 prompt,帮助理解 team 要解决什么问题。
|
|
32
|
+
const entries = [];
|
|
33
|
+
let hasUserMsg = false;
|
|
34
|
+
|
|
35
|
+
// 策略 1:从 mainAgentSessions 按时间范围提取
|
|
36
|
+
if (mainAgentSessions) {
|
|
37
|
+
let closestBeforeTs = null;
|
|
38
|
+
for (const session of mainAgentSessions) {
|
|
39
|
+
for (const msg of session.messages || []) {
|
|
40
|
+
const ts = msg._timestamp;
|
|
41
|
+
if (!ts || msg.role !== 'user') continue;
|
|
42
|
+
if (ts <= teamStartTime && (!closestBeforeTs || ts > closestBeforeTs)) {
|
|
43
|
+
closestBeforeTs = ts;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const effectiveStart = closestBeforeTs || teamStartTime;
|
|
48
|
+
for (const session of mainAgentSessions) {
|
|
49
|
+
for (const msg of session.messages || []) {
|
|
50
|
+
const ts = msg._timestamp;
|
|
51
|
+
if (!ts || ts < effectiveStart) continue;
|
|
52
|
+
if (teamEndTime && ts > teamEndTime) continue;
|
|
53
|
+
if (msg.role !== 'user') continue;
|
|
54
|
+
const content = msg.content;
|
|
55
|
+
if (Array.isArray(content)) {
|
|
56
|
+
const { textBlocks } = classifyUserContent(content);
|
|
57
|
+
for (const tb of textBlocks) {
|
|
58
|
+
if (tb.text && tb.text.trim()) {
|
|
59
|
+
entries.push({ type: 'user', text: tb.text, timestamp: ts });
|
|
60
|
+
hasUserMsg = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} else if (typeof content === 'string') {
|
|
64
|
+
const dispText = extractDisplayText(content);
|
|
65
|
+
if (dispText) {
|
|
66
|
+
entries.push({ type: 'user', text: dispText, timestamp: ts });
|
|
67
|
+
hasUserMsg = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 策略 2:从 TeamCreate request 的 body.messages 直接提取
|
|
75
|
+
if (!hasUserMsg) {
|
|
76
|
+
const tcRaw = requests[team.requestIndex];
|
|
77
|
+
const tcReq = tcRaw?._slimmed ? restoreSlimmedEntry(tcRaw, requests) : tcRaw;
|
|
78
|
+
const tcMsgs = tcReq?.body?.messages || [];
|
|
79
|
+
for (let m = tcMsgs.length - 1; m >= 0; m--) {
|
|
80
|
+
if (tcMsgs[m].role !== 'user') continue;
|
|
81
|
+
const c = tcMsgs[m].content;
|
|
82
|
+
if (Array.isArray(c)) {
|
|
83
|
+
const { textBlocks } = classifyUserContent(c);
|
|
84
|
+
for (const tb of textBlocks) {
|
|
85
|
+
if (tb.text && tb.text.trim()) {
|
|
86
|
+
entries.push({ type: 'user', text: tb.text, timestamp: teamStartTime });
|
|
87
|
+
hasUserMsg = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else if (typeof c === 'string') {
|
|
91
|
+
const dispText = extractDisplayText(c);
|
|
92
|
+
if (dispText) {
|
|
93
|
+
entries.push({ type: 'user', text: dispText, timestamp: teamStartTime });
|
|
94
|
+
hasUserMsg = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (hasUserMsg) break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 策略 3 兜底:/clear 后 messages=[] 时,用首条 assistant 文本作为上下文
|
|
102
|
+
if (!hasUserMsg) {
|
|
103
|
+
for (let i = 0; i < teamRequests.length; i++) {
|
|
104
|
+
const resp = teamRequests[i].response?.body?.content;
|
|
105
|
+
if (!Array.isArray(resp)) continue;
|
|
106
|
+
for (const block of resp) {
|
|
107
|
+
if (block.type === 'text' && block.text && block.text.trim()) {
|
|
108
|
+
entries.push({ type: 'context', text: block.text.trim(), timestamp: teamRequests[i].response?.timestamp || teamRequests[i].timestamp });
|
|
109
|
+
hasUserMsg = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (hasUserMsg) break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 全局 tool_result 索引(team 范围内):并行 sub-agent 请求穿插,K+1 不可预测;
|
|
118
|
+
// 一次性建 id → result 索引,O(1) 查询。
|
|
119
|
+
const teamGlobalIndex = buildGlobalToolResultIndex(teamRequests);
|
|
120
|
+
|
|
121
|
+
// 收集 assistant + sub-agent 条目
|
|
122
|
+
for (let i = 0; i < teamRequests.length; i++) {
|
|
123
|
+
const req = teamRequests[i];
|
|
124
|
+
const respContent = req.response?.body?.content;
|
|
125
|
+
if (!Array.isArray(respContent) || respContent.length === 0) continue;
|
|
126
|
+
const cls = classifyRequest(req, teamRequests[i + 1]);
|
|
127
|
+
const isMA = isMainAgent(req);
|
|
128
|
+
const isSub = cls.type === 'SubAgent' || cls.type === 'Teammate';
|
|
129
|
+
|
|
130
|
+
if (isMA) {
|
|
131
|
+
entries.push({ type: 'assistant', content: respContent, timestamp: req.response?.timestamp || req.timestamp, requestIndex: startIdx + i, modelInfo });
|
|
132
|
+
} else if (isSub) {
|
|
133
|
+
const subToolResultMap = buildSubAgentResultMap(req, teamGlobalIndex);
|
|
134
|
+
entries.push({
|
|
135
|
+
type: 'sub-agent',
|
|
136
|
+
content: respContent,
|
|
137
|
+
toolResultMap: subToolResultMap,
|
|
138
|
+
label: cls.type === 'Teammate' ? formatTeammateLabel(cls.subType, req.body?.model) : formatRequestTag(cls.type, cls.subType),
|
|
139
|
+
isTeammate: cls.type === 'Teammate',
|
|
140
|
+
timestamp: req.timestamp,
|
|
141
|
+
requestIndex: startIdx + i,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 按时间排序
|
|
147
|
+
entries.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
148
|
+
|
|
149
|
+
// 提取每个 agent 的时间数据(用于状态卡片和甘特图)
|
|
150
|
+
const palette = ['#1668dc', '#52c41a', '#faad14', '#eb2f96', '#722ed1', '#13c2c2', '#fa541c', '#2f54eb'];
|
|
151
|
+
const teamAgents = [];
|
|
152
|
+
const agentMap = new Map();
|
|
153
|
+
const teamTotalStart = new Date(teamStartTime).getTime();
|
|
154
|
+
const teamTotalEnd = new Date(teamEndTime || Date.now()).getTime();
|
|
155
|
+
const leadSegments = [];
|
|
156
|
+
let lastLeadTs = teamTotalStart;
|
|
157
|
+
const _taskCreateSubjects = new Map();
|
|
158
|
+
const _taskOwnerMap = new Map();
|
|
159
|
+
let _taskCreateCounter = 1;
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < teamRequests.length; i++) {
|
|
162
|
+
const req = teamRequests[i];
|
|
163
|
+
const resp = req.response?.body?.content;
|
|
164
|
+
if (!Array.isArray(resp)) continue;
|
|
165
|
+
const tsStr = req.response?.timestamp || req.timestamp;
|
|
166
|
+
const ts = tsStr;
|
|
167
|
+
const tsMs = new Date(tsStr).getTime();
|
|
168
|
+
const isMA = isMainAgent(req);
|
|
169
|
+
for (const block of resp) {
|
|
170
|
+
if (block.type !== 'tool_use') continue;
|
|
171
|
+
const n = block.name;
|
|
172
|
+
const inp = typeof block.input === 'string' ? (() => { try { return JSON.parse(block.input); } catch { return {}; } })() : (block.input || {});
|
|
173
|
+
if (n === 'Agent' && inp.name) {
|
|
174
|
+
const idx = teamAgents.length;
|
|
175
|
+
teamAgents.push({
|
|
176
|
+
name: inp.name,
|
|
177
|
+
color: palette[idx % palette.length],
|
|
178
|
+
type: inp.subagent_type?.split(':').pop() || '',
|
|
179
|
+
spawnTime: ts,
|
|
180
|
+
claimTime: null,
|
|
181
|
+
doneTime: null,
|
|
182
|
+
shutdownTime: null,
|
|
183
|
+
taskSubject: null,
|
|
184
|
+
events: [{ ts: tsMs, label: 'spawn' }],
|
|
185
|
+
});
|
|
186
|
+
agentMap.set(inp.name, idx);
|
|
187
|
+
} else if (n === 'TaskCreate') {
|
|
188
|
+
if (inp.subject) {
|
|
189
|
+
const tId = String(inp.taskId || _taskCreateCounter++);
|
|
190
|
+
_taskCreateSubjects.set(tId, inp.subject);
|
|
191
|
+
}
|
|
192
|
+
} else if (n === 'TaskUpdate') {
|
|
193
|
+
const owner = inp.owner;
|
|
194
|
+
const taskId = inp.taskId != null ? String(inp.taskId) : null;
|
|
195
|
+
if (owner && taskId) _taskOwnerMap.set(taskId, owner);
|
|
196
|
+
|
|
197
|
+
let targetAg = null;
|
|
198
|
+
if (owner && agentMap.has(owner)) {
|
|
199
|
+
targetAg = teamAgents[agentMap.get(owner)];
|
|
200
|
+
} else if (taskId) {
|
|
201
|
+
const prevOwner = _taskOwnerMap.get(taskId);
|
|
202
|
+
if (prevOwner && agentMap.has(prevOwner)) {
|
|
203
|
+
targetAg = teamAgents[agentMap.get(prevOwner)];
|
|
204
|
+
} else {
|
|
205
|
+
const taskNum = parseInt(taskId, 10);
|
|
206
|
+
if (taskNum > 0 && taskNum <= teamAgents.length) {
|
|
207
|
+
targetAg = teamAgents[taskNum - 1];
|
|
208
|
+
_taskOwnerMap.set(taskId, targetAg.name);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (targetAg) {
|
|
213
|
+
if (inp.status === 'in_progress' && !targetAg.claimTime) {
|
|
214
|
+
targetAg.claimTime = ts;
|
|
215
|
+
targetAg.events.push({ ts: tsMs, label: 'claim' });
|
|
216
|
+
}
|
|
217
|
+
if (inp.status === 'completed' && !targetAg.doneTime) {
|
|
218
|
+
targetAg.doneTime = ts;
|
|
219
|
+
targetAg.events.push({ ts: tsMs, label: 'done' });
|
|
220
|
+
}
|
|
221
|
+
if (taskId && _taskCreateSubjects.has(taskId) && !targetAg.taskSubject) {
|
|
222
|
+
targetAg.taskSubject = _taskCreateSubjects.get(taskId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} else if (n === 'SendMessage') {
|
|
226
|
+
if (inp.message?.type === 'shutdown_request' && inp.to && agentMap.has(inp.to)) {
|
|
227
|
+
const ag = teamAgents[agentMap.get(inp.to)];
|
|
228
|
+
ag.shutdownTime = ts;
|
|
229
|
+
ag.events.push({ ts: tsMs, label: 'shutdown' });
|
|
230
|
+
} else if (inp.message?.type === 'shutdown_response' && agentMap.has(inp.to === 'team-lead' ? '' : inp.to)) {
|
|
231
|
+
// skip
|
|
232
|
+
} else if (inp.to && inp.to !== 'team-lead' && agentMap.has(inp.to)) {
|
|
233
|
+
teamAgents[agentMap.get(inp.to)].events.push({ ts: tsMs, label: 'msg-in' });
|
|
234
|
+
} else if (inp.to === 'team-lead') {
|
|
235
|
+
if (typeof inp.message === 'string' || (inp.message && !inp.message.type)) {
|
|
236
|
+
if (tsMs > lastLeadTs) {
|
|
237
|
+
leadSegments.push({ start: lastLeadTs, end: tsMs, label: 'report-received', color: '#52c41a' });
|
|
238
|
+
lastLeadTs = tsMs;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (isMA && (n === 'TeamCreate' || n === 'TaskCreate' || n === 'Agent' || n === 'SendMessage' || n === 'TeamDelete')) {
|
|
244
|
+
const label = n === 'TeamCreate' ? 'create' : n === 'TaskCreate' ? 'tasks' : n === 'Agent' ? 'spawn' : n === 'SendMessage' ? 'msg' : 'cleanup';
|
|
245
|
+
if (tsMs > lastLeadTs) {
|
|
246
|
+
leadSegments.push({ start: lastLeadTs, end: tsMs, label, color: n === 'TeamDelete' ? '#52c41a' : n === 'SendMessage' ? '#ff4d4f' : '#1668dc' });
|
|
247
|
+
lastLeadTs = tsMs;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (isMA) {
|
|
252
|
+
for (const block of resp) {
|
|
253
|
+
if (block.type === 'text' && block.text) {
|
|
254
|
+
if (tsMs > lastLeadTs) {
|
|
255
|
+
leadSegments.push({ start: lastLeadTs, end: tsMs, label: 'text', color: '#196ae1' });
|
|
256
|
+
lastLeadTs = tsMs;
|
|
257
|
+
}
|
|
258
|
+
} else if (block.type === 'thinking') {
|
|
259
|
+
if (tsMs > lastLeadTs) {
|
|
260
|
+
leadSegments.push({ start: lastLeadTs, end: tsMs, label: 'thinking', color: '#722ed1' });
|
|
261
|
+
lastLeadTs = tsMs;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Second pass: teammate own tool calls (non-MainAgent requests)
|
|
268
|
+
for (let i = 0; i < teamRequests.length; i++) {
|
|
269
|
+
const req = teamRequests[i];
|
|
270
|
+
if (isMainAgent(req)) continue;
|
|
271
|
+
const resp = req.response?.body?.content;
|
|
272
|
+
if (!Array.isArray(resp)) continue;
|
|
273
|
+
const tsStr = req.response?.timestamp || req.timestamp;
|
|
274
|
+
const tsMs = new Date(tsStr).getTime();
|
|
275
|
+
const cls = classifyRequest(req, teamRequests[i + 1]);
|
|
276
|
+
const label = cls.type === 'Teammate' ? cls.subType : null;
|
|
277
|
+
if (label) {
|
|
278
|
+
let agIdx = agentMap.has(label) ? agentMap.get(label) : undefined;
|
|
279
|
+
if (agIdx === undefined) {
|
|
280
|
+
for (const [name, idx] of agentMap) {
|
|
281
|
+
if (label.includes(name) || name.includes(label)) { agIdx = idx; break; }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (agIdx !== undefined) {
|
|
285
|
+
const ag = teamAgents[agIdx];
|
|
286
|
+
for (const block of resp) {
|
|
287
|
+
if (block.type === 'tool_use' && block.name) {
|
|
288
|
+
ag.events.push({ ts: tsMs, label: 'tool:' + block.name });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 提取 <teammate-message> 报告内容
|
|
296
|
+
const teammateMessageRe = /<teammate-message\s+teammate_id="([^"]+)"[^>]*summary="([^"]*)"[^>]*>([\s\S]*?)<\/teammate-message>/g;
|
|
297
|
+
const seenTmMsg = new Set();
|
|
298
|
+
teamAgents.forEach(ag => { ag.teammateMessages = []; });
|
|
299
|
+
for (let i = 0; i < teamRequests.length; i++) {
|
|
300
|
+
const raw = teamRequests[i];
|
|
301
|
+
const req = raw?._slimmed ? restoreSlimmedEntry(raw, requests) : raw;
|
|
302
|
+
const msgs = req.body?.messages || [];
|
|
303
|
+
for (const m of msgs) {
|
|
304
|
+
if (m.role !== 'user' || !Array.isArray(m.content)) continue;
|
|
305
|
+
for (const b of m.content) {
|
|
306
|
+
if (b.type !== 'text' || !b.text) continue;
|
|
307
|
+
let match;
|
|
308
|
+
teammateMessageRe.lastIndex = 0;
|
|
309
|
+
while ((match = teammateMessageRe.exec(b.text)) !== null) {
|
|
310
|
+
const [, tid, summary, content] = match;
|
|
311
|
+
if (tid === 'system' || tid === 'team-lead') continue;
|
|
312
|
+
const dedupKey = tid + '|' + summary + '|' + content.trim().slice(0, 100);
|
|
313
|
+
if (seenTmMsg.has(dedupKey)) continue;
|
|
314
|
+
seenTmMsg.add(dedupKey);
|
|
315
|
+
for (const ag of teamAgents) {
|
|
316
|
+
if (tid === ag.name || tid.includes(ag.name) || ag.name.includes(tid)) {
|
|
317
|
+
if (summary && content.trim()) {
|
|
318
|
+
ag.teammateMessages.push({ summary, content: content.trim() });
|
|
319
|
+
const reqTs = req.timestamp || req.response?.timestamp;
|
|
320
|
+
entries.push({ type: 'teammate-report', agentName: ag.name, agentColor: getTeammateAvatar(ag.name).color, summary, content: content.trim(), timestamp: reqTs });
|
|
321
|
+
}
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 闭合 lead 最后一段
|
|
331
|
+
if (lastLeadTs < teamTotalEnd) {
|
|
332
|
+
leadSegments.push({ start: lastLeadTs, end: teamTotalEnd, label: 'idle', color: '#333' });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 从事件节点构建每个 agent 的分段 + 计算持续时间
|
|
336
|
+
const segColors = { spawn: '#555', claim: '#faad14', done: '#52c41a', shutdown: '#ff4d4f', 'msg-in': '#1668dc', report: '#52c41a', 'report-received': '#52c41a', text: '#196ae1', thinking: '#722ed1' };
|
|
337
|
+
teamAgents.forEach(ag => {
|
|
338
|
+
const start = new Date(ag.spawnTime).getTime();
|
|
339
|
+
const end = new Date(ag.doneTime || ag.shutdownTime || teamEndTime || Date.now()).getTime();
|
|
340
|
+
ag.duration = end - start;
|
|
341
|
+
ag.events.sort((a, b) => a.ts - b.ts);
|
|
342
|
+
ag.segments = [];
|
|
343
|
+
for (let e = 0; e < ag.events.length; e++) {
|
|
344
|
+
const ev = ag.events[e];
|
|
345
|
+
const nextTs = ag.events[e + 1]?.ts || (ag.shutdownTime ? new Date(ag.shutdownTime).getTime() : teamTotalEnd);
|
|
346
|
+
ag.segments.push({ start: ev.ts, end: nextTs, label: ev.label, color: segColors[ev.label] || (ev.label.startsWith('tool:') ? '#888' : ag.color) });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return { entries, teamAgents, leadSegments, teamTotalStart, teamTotalEnd, modelInfo, teamRequests };
|
|
351
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse team lifecycle (TeamCreate → TeamDelete) from requests.
|
|
3
|
+
* Pure function, no React/state dependencies.
|
|
4
|
+
*
|
|
5
|
+
* endReason 取值(详见 END_REASON 常量):
|
|
6
|
+
* 'deleteConfirmed' — TeamDelete 返回 success(强证据,终态 ✓)
|
|
7
|
+
* 'successorCreate' — 新 TeamCreate 顶掉旧的(强证据,lead 已转移,等同 ✓)
|
|
8
|
+
* 'shutdownRequest' — 没看到 TeamDelete,但有 shutdown_request(弱证据,待 runtime 收敛)
|
|
9
|
+
* 'logTail' — 纯 log 末尾兜底(弱证据,待 runtime 收敛)
|
|
10
|
+
*
|
|
11
|
+
* _hasInferredEnd 派生含义:**只要 endReason 存在且不是 deleteConfirmed 就为 true**
|
|
12
|
+
* ⚠️ 注意:此字段**同时覆盖强证据 (successorCreate) 和弱证据 (shutdownRequest/logTail)**,
|
|
13
|
+
* 仅用于"是否经过推断流程结束"的存在性提示,不能用来判断强弱。
|
|
14
|
+
* **判断强弱证据请用 `isStrongTerminal(team)`**(它是权威的 API)。
|
|
15
|
+
* 旧代码里的 `_inferredEnd` 已重命名为 `_hasInferredEnd` 以避免"inferred 一定是弱证据"的直觉误读。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { restoreSlimmedEntry } from './entry-slim.js';
|
|
19
|
+
|
|
20
|
+
export const END_REASON = Object.freeze({
|
|
21
|
+
DELETE_CONFIRMED: 'deleteConfirmed',
|
|
22
|
+
SUCCESSOR_CREATE: 'successorCreate',
|
|
23
|
+
SHUTDOWN_REQUEST: 'shutdownRequest',
|
|
24
|
+
LOG_TAIL: 'logTail',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export function isStrongTerminal(team) {
|
|
28
|
+
if (!team) return false;
|
|
29
|
+
return team.endReason === END_REASON.DELETE_CONFIRMED
|
|
30
|
+
|| team.endReason === END_REASON.SUCCESSOR_CREATE;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 从 requests 中提取 Team 会话列表
|
|
34
|
+
export function extractTeamSessions(requests) {
|
|
35
|
+
const teams = [];
|
|
36
|
+
let currentTeamIdx = -1; // 当前唯一打开的 team 在 teams[] 中的 index
|
|
37
|
+
|
|
38
|
+
// 查找 tool_use 对应的 tool_result(在后续 request 的 messages 中)
|
|
39
|
+
// 搜索窗口扩大到 10 以应对空行/非主agent请求插入导致的距离增大
|
|
40
|
+
function findToolResult(toolUseId, fromRequestIdx) {
|
|
41
|
+
for (let j = fromRequestIdx + 1; j < requests.length && j <= fromRequestIdx + 10; j++) {
|
|
42
|
+
const entry = requests[j]?._slimmed ? restoreSlimmedEntry(requests[j], requests) : requests[j];
|
|
43
|
+
const msgs = entry?.body?.messages;
|
|
44
|
+
if (!Array.isArray(msgs)) continue;
|
|
45
|
+
for (const msg of msgs) {
|
|
46
|
+
const blocks = msg.role === 'user' && Array.isArray(msg.content) ? msg.content : [];
|
|
47
|
+
for (const b of blocks) {
|
|
48
|
+
if (b.type === 'tool_result' && b.tool_use_id === toolUseId) {
|
|
49
|
+
return typeof b.content === 'string' ? b.content : JSON.stringify(b.content || '');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isDeleteSuccessful(resultText) {
|
|
58
|
+
// tool_result 可能因 entry-slim/delta 压缩而不可达,默认视为成功
|
|
59
|
+
// 只有明确包含 "Cannot cleanup" 才判定失败
|
|
60
|
+
if (!resultText) return true;
|
|
61
|
+
if (resultText.includes('"success":true') || resultText.includes('"success": true')) return true;
|
|
62
|
+
if (resultText.includes('Cleaned up')) return true;
|
|
63
|
+
if (resultText.includes('Cannot cleanup')) return false;
|
|
64
|
+
// 没有明确失败标记的默认视为成功
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < requests.length; i++) {
|
|
69
|
+
const req = requests[i];
|
|
70
|
+
const respContent = req.response?.body?.content;
|
|
71
|
+
if (!Array.isArray(respContent)) continue;
|
|
72
|
+
for (const block of respContent) {
|
|
73
|
+
if (block.type !== 'tool_use') continue;
|
|
74
|
+
const name = block.name;
|
|
75
|
+
const input = typeof block.input === 'string' ? (() => { try { return JSON.parse(block.input); } catch { return {}; } })() : (block.input || {});
|
|
76
|
+
if (name === 'TeamCreate') {
|
|
77
|
+
// 检查 TeamCreate 是否成功(tool_result 中不能有错误标记)
|
|
78
|
+
const createResult = findToolResult(block.id, i);
|
|
79
|
+
if (createResult && (createResult.includes('"error":') || createResult.includes('"error" :') || createResult.includes('Already leading team'))) continue;
|
|
80
|
+
const teamName = input.team_name || input.teamName || 'unknown';
|
|
81
|
+
const ts = req.timestamp || req.response?.timestamp;
|
|
82
|
+
// 新 TeamCreate 出现时,自动关闭前一个未关闭的 team(避免孤立)
|
|
83
|
+
if (currentTeamIdx >= 0 && !teams[currentTeamIdx].endTime) {
|
|
84
|
+
teams[currentTeamIdx].endTime = ts;
|
|
85
|
+
teams[currentTeamIdx].endRequestIndex = Math.max(teams[currentTeamIdx].requestIndex, i - 1);
|
|
86
|
+
teams[currentTeamIdx].endReason = END_REASON.SUCCESSOR_CREATE;
|
|
87
|
+
teams[currentTeamIdx]._hasInferredEnd = true;
|
|
88
|
+
}
|
|
89
|
+
const team = { name: teamName, startTime: ts, endTime: null, requestIndex: i, endRequestIndex: null, taskCount: 0, teammateCount: 0, _teammates: new Set() };
|
|
90
|
+
teams.push(team);
|
|
91
|
+
currentTeamIdx = teams.length - 1;
|
|
92
|
+
} else if (name === 'TeamDelete') {
|
|
93
|
+
const resultText = findToolResult(block.id, i);
|
|
94
|
+
if (!isDeleteSuccessful(resultText)) continue; // 失败的 TeamDelete 不关闭 team
|
|
95
|
+
const ts = req.timestamp || req.response?.timestamp;
|
|
96
|
+
if (currentTeamIdx < 0) {
|
|
97
|
+
// Cross-file: TeamCreate 在上一个 JSONL 中,从 tool_result 反向推断 team
|
|
98
|
+
let teamName = 'unknown';
|
|
99
|
+
try { const parsed = JSON.parse(resultText); teamName = parsed.team_name || teamName; } catch {}
|
|
100
|
+
// 回溯寻找最早的关联 Agent 调用作为 startTime
|
|
101
|
+
let startIdx = 0;
|
|
102
|
+
let startTs = requests[0]?.timestamp || requests[0]?.response?.timestamp;
|
|
103
|
+
for (let k = 0; k < i; k++) {
|
|
104
|
+
const kResp = requests[k]?.response?.body?.content;
|
|
105
|
+
if (!Array.isArray(kResp)) continue;
|
|
106
|
+
for (const kb of kResp) {
|
|
107
|
+
if (kb.type === 'tool_use' && kb.name === 'Agent') {
|
|
108
|
+
const kInp = typeof kb.input === 'string' ? (() => { try { return JSON.parse(kb.input); } catch { return {}; } })() : (kb.input || {});
|
|
109
|
+
if ((kInp.team_name || kInp.teamName) === teamName) {
|
|
110
|
+
startIdx = k;
|
|
111
|
+
startTs = requests[k]?.timestamp || requests[k]?.response?.timestamp;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (startIdx > 0) break;
|
|
117
|
+
}
|
|
118
|
+
const team = { name: teamName, startTime: startTs, endTime: ts, endReason: END_REASON.DELETE_CONFIRMED, requestIndex: startIdx, endRequestIndex: i, taskCount: 0, teammateCount: 0, _teammates: new Set(), _inferredStart: true };
|
|
119
|
+
// 回填 teammate 和 task 计数
|
|
120
|
+
for (let k = startIdx; k < i; k++) {
|
|
121
|
+
const kResp = requests[k]?.response?.body?.content;
|
|
122
|
+
if (!Array.isArray(kResp)) continue;
|
|
123
|
+
for (const kb of kResp) {
|
|
124
|
+
if (kb.type !== 'tool_use') continue;
|
|
125
|
+
if (kb.name === 'Agent') {
|
|
126
|
+
const kInp = typeof kb.input === 'string' ? (() => { try { return JSON.parse(kb.input); } catch { return {}; } })() : (kb.input || {});
|
|
127
|
+
const an = kInp.name || '';
|
|
128
|
+
if (!team._teammates.has(an)) { team._teammates.add(an); team.teammateCount++; }
|
|
129
|
+
} else if (kb.name === 'TaskCreate' || kb.name === 'TaskUpdate') {
|
|
130
|
+
team.taskCount++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
teams.push(team);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
teams[currentTeamIdx].endTime = ts;
|
|
138
|
+
teams[currentTeamIdx].endRequestIndex = i;
|
|
139
|
+
teams[currentTeamIdx].endReason = END_REASON.DELETE_CONFIRMED;
|
|
140
|
+
currentTeamIdx = -1; // 清理:team 已关闭
|
|
141
|
+
} else if (name === 'SendMessage') {
|
|
142
|
+
// 跟踪 shutdown_request 作为备用结束信号
|
|
143
|
+
if (currentTeamIdx >= 0 && input.message?.type === 'shutdown_request') {
|
|
144
|
+
const shutdownTs = req.timestamp || req.response?.timestamp;
|
|
145
|
+
teams[currentTeamIdx]._lastShutdownTime = shutdownTs;
|
|
146
|
+
teams[currentTeamIdx]._lastShutdownRequestIdx = i;
|
|
147
|
+
}
|
|
148
|
+
} else if (name === 'TaskCreate' || name === 'TaskUpdate') {
|
|
149
|
+
if (currentTeamIdx >= 0) teams[currentTeamIdx].taskCount++;
|
|
150
|
+
} else if (name === 'Agent') {
|
|
151
|
+
const teamName = input.team_name || input.teamName;
|
|
152
|
+
const agentName = input.name || '';
|
|
153
|
+
let targetIdx = -1;
|
|
154
|
+
if (teamName) {
|
|
155
|
+
// 按 team_name 精确匹配(使用反向搜索,优先匹配最近的同名 team)
|
|
156
|
+
for (let ti = teams.length - 1; ti >= 0; ti--) {
|
|
157
|
+
if (teams[ti].name === teamName && !teams[ti].endTime) { targetIdx = ti; break; }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// fallback:如果没有 team_name 但有唯一打开的 team
|
|
161
|
+
if (targetIdx < 0 && currentTeamIdx >= 0) targetIdx = currentTeamIdx;
|
|
162
|
+
if (targetIdx >= 0) {
|
|
163
|
+
const t = teams[targetIdx];
|
|
164
|
+
if (!t._teammates.has(agentName)) { t._teammates.add(agentName); t.teammateCount++; }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// 后处理:为未关闭的 team 推断 endTime
|
|
170
|
+
for (const team of teams) {
|
|
171
|
+
if (team.endTime) continue;
|
|
172
|
+
// 优先使用 shutdown_request 时间戳,其次使用最后一条请求的时间戳
|
|
173
|
+
if (team._lastShutdownTime) {
|
|
174
|
+
team.endTime = team._lastShutdownTime;
|
|
175
|
+
team.endRequestIndex = team._lastShutdownRequestIdx;
|
|
176
|
+
team.endReason = END_REASON.SHUTDOWN_REQUEST;
|
|
177
|
+
team._hasInferredEnd = true;
|
|
178
|
+
} else {
|
|
179
|
+
const lastReq = requests[requests.length - 1];
|
|
180
|
+
const lastTs = lastReq?.response?.timestamp || lastReq?.timestamp;
|
|
181
|
+
if (lastTs && team.startTime !== lastTs) {
|
|
182
|
+
team.endTime = lastTs;
|
|
183
|
+
team.endRequestIndex = requests.length - 1;
|
|
184
|
+
team.endReason = END_REASON.LOG_TAIL;
|
|
185
|
+
team._hasInferredEnd = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return teams;
|
|
190
|
+
}
|