@morlay/ui-conversation-manager 0.0.2-alpha.2 → 0.0.2-alpha.4
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/dist/client.cjs +240 -182
- package/dist/client.d.cts +20 -6
- package/dist/client.d.mts +20 -6
- package/package.json +13 -13
- package/src/client/ConversationManagerPage.styles.ts +13 -3
- package/src/client/ConversationManagerPage.tsx +187 -135
- package/src/client/controller.ts +17 -5
- package/src/client/format.ts +6 -0
- package/src/client/index.ts +6 -1
- package/src/client/locales.ts +8 -2
package/dist/client.cjs
CHANGED
|
@@ -143,14 +143,20 @@ window.__ModuleLoader__.load({
|
|
|
143
143
|
if (value < 1e9) return `${trim(value / 1e6)}M`;
|
|
144
144
|
return `${trim(value / 1e9)}B`;
|
|
145
145
|
}
|
|
146
|
+
/** @param value - 计数(轮次 / 步骤 / 用户输入 / 工具调用)。 @returns 千分位原文,例如 `1,024`。 */
|
|
147
|
+
function formatCount(value) {
|
|
148
|
+
if (!Number.isFinite(value) || value <= 0) return "0";
|
|
149
|
+
return Math.round(value).toLocaleString("en-US");
|
|
150
|
+
}
|
|
146
151
|
/** @param value - 百分点(0~100)。 @returns 紧凑百分比,例如 `98.4%`、`100%`。 */
|
|
147
152
|
function formatPercent(value) {
|
|
148
153
|
if (!Number.isFinite(value) || value <= 0) return "0%";
|
|
149
154
|
return `${value >= 100 ? Math.round(value) : Math.round(value * 10) / 10}%`;
|
|
150
155
|
}
|
|
151
156
|
/** 「对话管理」页面的样式表(官方 --dsw-* 变量 + 我们的 css-in-js 层)。
|
|
152
|
-
* 页面根是纵向 flex
|
|
153
|
-
*
|
|
157
|
+
* 页面根是纵向 flex 且**自己不滚**:页头(标题 / tab 条 / 导入 / 清理)与搜索行固定在顶部,
|
|
158
|
+
* 只有 `scroll` 那层随内容滚。除滚动层外的项都 `flex: none`,否则内容变长时 flex 会把这些项
|
|
159
|
+
* 压到 min-content(官方 Input 的 32px 高会被压成一行文字高)。 */
|
|
154
160
|
const styles = {
|
|
155
161
|
page: {
|
|
156
162
|
display: "flex",
|
|
@@ -160,9 +166,17 @@ window.__ModuleLoader__.load({
|
|
|
160
166
|
width: "100%",
|
|
161
167
|
height: "100%",
|
|
162
168
|
padding: "24px 28px",
|
|
163
|
-
overflow: "
|
|
169
|
+
overflow: "hidden",
|
|
164
170
|
color: "var(--dsw-alias-label-primary)"
|
|
165
171
|
},
|
|
172
|
+
scroll: {
|
|
173
|
+
display: "flex",
|
|
174
|
+
flexDirection: "column",
|
|
175
|
+
gap: "12px",
|
|
176
|
+
flex: "1 1 auto",
|
|
177
|
+
minHeight: "0",
|
|
178
|
+
overflowY: "auto"
|
|
179
|
+
},
|
|
166
180
|
header: {
|
|
167
181
|
display: "flex",
|
|
168
182
|
alignItems: "center",
|
|
@@ -585,175 +599,182 @@ window.__ModuleLoader__.load({
|
|
|
585
599
|
})
|
|
586
600
|
]
|
|
587
601
|
}),
|
|
588
|
-
view === "usage" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
602
|
+
view === "usage" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
603
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.scroll),
|
|
604
|
+
"data-scroll": "page",
|
|
605
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageView, {
|
|
606
|
+
report: usage,
|
|
607
|
+
loading: usageLoading,
|
|
608
|
+
error: usageError,
|
|
609
|
+
tab: usageTab,
|
|
610
|
+
onTab: setUsageTab,
|
|
611
|
+
range: usageRange,
|
|
612
|
+
onRange: requestUsage,
|
|
613
|
+
t
|
|
614
|
+
})
|
|
615
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
616
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.filters),
|
|
617
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
618
|
+
className: _morlay_dsh_client_ui_primitives_client.styling.className(styles.search),
|
|
619
|
+
"data-filter": "search",
|
|
620
|
+
type: "search",
|
|
621
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}),
|
|
622
|
+
value: query,
|
|
623
|
+
placeholder: t("search"),
|
|
624
|
+
"aria-label": t("search"),
|
|
625
|
+
onChange: (event) => {
|
|
626
|
+
setQuery(event.currentTarget.value);
|
|
627
|
+
setPage(1);
|
|
628
|
+
}
|
|
629
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
630
|
+
"data-filter": "subagents",
|
|
631
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
|
|
632
|
+
checked: showSubagents,
|
|
633
|
+
label: t("showSubagents"),
|
|
634
|
+
onChange: (next) => {
|
|
635
|
+
setShowSubagents(next);
|
|
610
636
|
setPage(1);
|
|
611
637
|
}
|
|
612
|
-
})
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
})
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.list),
|
|
647
|
-
children: visible.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
648
|
-
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.row),
|
|
649
|
-
"data-session-id": String(row.id),
|
|
650
|
-
"data-archived": row.archived ? "true" : "false",
|
|
651
|
-
"data-subagent": row.subagent ? "true" : "false",
|
|
652
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
653
|
-
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.identity),
|
|
638
|
+
})
|
|
639
|
+
})]
|
|
640
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
641
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.scroll),
|
|
642
|
+
"data-scroll": "page",
|
|
643
|
+
children: [
|
|
644
|
+
notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
645
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
|
|
646
|
+
"data-notice": "result",
|
|
647
|
+
children: notice
|
|
648
|
+
}),
|
|
649
|
+
failure === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
650
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.failure),
|
|
651
|
+
"data-failure": "result",
|
|
652
|
+
role: "alert",
|
|
653
|
+
children: failure
|
|
654
|
+
}),
|
|
655
|
+
listed.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
656
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
|
|
657
|
+
"data-status": "empty",
|
|
658
|
+
children: t("empty")
|
|
659
|
+
}) : null,
|
|
660
|
+
listed.length > 0 && matched.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
661
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
|
|
662
|
+
"data-status": "empty-search",
|
|
663
|
+
children: t("emptySearch")
|
|
664
|
+
}) : null,
|
|
665
|
+
visible.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
666
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.list),
|
|
667
|
+
children: visible.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
668
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.row),
|
|
669
|
+
"data-session-id": String(row.id),
|
|
670
|
+
"data-archived": row.archived ? "true" : "false",
|
|
671
|
+
"data-subagent": row.subagent ? "true" : "false",
|
|
654
672
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
655
|
-
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.
|
|
673
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.identity),
|
|
674
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
675
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.titleLine),
|
|
676
|
+
children: [
|
|
677
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
678
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.rowTitle),
|
|
679
|
+
children: row.title
|
|
680
|
+
}),
|
|
681
|
+
row.archived ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
|
|
682
|
+
tone: "neutral",
|
|
683
|
+
children: t("archived")
|
|
684
|
+
}) : null,
|
|
685
|
+
row.subagent ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
|
|
686
|
+
tone: "quiet",
|
|
687
|
+
children: t("subagent")
|
|
688
|
+
}) : null
|
|
689
|
+
]
|
|
690
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
691
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.meta),
|
|
692
|
+
children: [row.workspace, timeLabel(row.updatedAt, now, t)].join(" · ")
|
|
693
|
+
})]
|
|
694
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
695
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.actions),
|
|
656
696
|
children: [
|
|
657
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
658
|
-
|
|
659
|
-
|
|
697
|
+
row.archived ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
698
|
+
variant: "outline",
|
|
699
|
+
size: "sm",
|
|
700
|
+
"data-action": "unarchive",
|
|
701
|
+
"aria-label": t("unarchiveNamed", { title: row.title }),
|
|
702
|
+
onClick: () => {
|
|
703
|
+
run(unarchive(row.id));
|
|
704
|
+
},
|
|
705
|
+
children: t("unarchive")
|
|
706
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
707
|
+
variant: "outline",
|
|
708
|
+
size: "sm",
|
|
709
|
+
"data-action": "archive",
|
|
710
|
+
"aria-label": t("archiveNamed", { title: row.title }),
|
|
711
|
+
onClick: () => {
|
|
712
|
+
run(archive(row.id));
|
|
713
|
+
},
|
|
714
|
+
children: t("archive")
|
|
715
|
+
}),
|
|
716
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
717
|
+
variant: "outline",
|
|
718
|
+
size: "sm",
|
|
719
|
+
"data-action": "export",
|
|
720
|
+
"aria-label": t("exportNamed", { title: row.title }),
|
|
721
|
+
onClick: () => {
|
|
722
|
+
run(exportZip(row.id));
|
|
723
|
+
},
|
|
724
|
+
children: t("export")
|
|
660
725
|
}),
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
726
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
727
|
+
variant: "outline",
|
|
728
|
+
size: "sm",
|
|
729
|
+
disabled: !row.archived,
|
|
730
|
+
"data-action": "remove",
|
|
731
|
+
"aria-label": t("removeNamed", { title: row.title }),
|
|
732
|
+
onClick: () => {
|
|
733
|
+
setFailure(null);
|
|
734
|
+
setNotice(null);
|
|
735
|
+
setConfirming(row);
|
|
736
|
+
},
|
|
737
|
+
children: t("remove")
|
|
738
|
+
})
|
|
669
739
|
]
|
|
670
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
671
|
-
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.meta),
|
|
672
|
-
children: [row.workspace, timeLabel(row.updatedAt, now, t)].join(" · ")
|
|
673
740
|
})]
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
697
|
-
variant: "outline",
|
|
698
|
-
size: "sm",
|
|
699
|
-
"data-action": "export",
|
|
700
|
-
"aria-label": t("exportNamed", { title: row.title }),
|
|
701
|
-
onClick: () => {
|
|
702
|
-
run(exportZip(row.id));
|
|
703
|
-
},
|
|
704
|
-
children: t("export")
|
|
705
|
-
}),
|
|
706
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
707
|
-
variant: "outline",
|
|
708
|
-
size: "sm",
|
|
709
|
-
disabled: !row.archived,
|
|
710
|
-
"data-action": "remove",
|
|
711
|
-
"aria-label": t("removeNamed", { title: row.title }),
|
|
712
|
-
onClick: () => {
|
|
713
|
-
setFailure(null);
|
|
714
|
-
setNotice(null);
|
|
715
|
-
setConfirming(row);
|
|
716
|
-
},
|
|
717
|
-
children: t("remove")
|
|
741
|
+
}, row.id))
|
|
742
|
+
}) : null,
|
|
743
|
+
matched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
744
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.pagination),
|
|
745
|
+
"data-pagination": "",
|
|
746
|
+
"data-page-current": currentPage,
|
|
747
|
+
"data-page-total": pageCount,
|
|
748
|
+
children: [
|
|
749
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
750
|
+
variant: "ghost",
|
|
751
|
+
size: "sm",
|
|
752
|
+
disabled: currentPage <= 1,
|
|
753
|
+
onClick: () => {
|
|
754
|
+
setPage(currentPage - 1);
|
|
755
|
+
},
|
|
756
|
+
children: t("page.previous")
|
|
757
|
+
}),
|
|
758
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
759
|
+
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.paginationLabel),
|
|
760
|
+
children: t("page.label", {
|
|
761
|
+
page: currentPage,
|
|
762
|
+
total: pageCount
|
|
718
763
|
})
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
children: [
|
|
729
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
730
|
-
variant: "ghost",
|
|
731
|
-
size: "sm",
|
|
732
|
-
disabled: currentPage <= 1,
|
|
733
|
-
onClick: () => {
|
|
734
|
-
setPage(currentPage - 1);
|
|
735
|
-
},
|
|
736
|
-
children: t("page.previous")
|
|
737
|
-
}),
|
|
738
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
739
|
-
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.paginationLabel),
|
|
740
|
-
children: t("page.label", {
|
|
741
|
-
page: currentPage,
|
|
742
|
-
total: pageCount
|
|
764
|
+
}),
|
|
765
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
766
|
+
variant: "ghost",
|
|
767
|
+
size: "sm",
|
|
768
|
+
disabled: currentPage >= pageCount,
|
|
769
|
+
onClick: () => {
|
|
770
|
+
setPage(currentPage + 1);
|
|
771
|
+
},
|
|
772
|
+
children: t("page.next")
|
|
743
773
|
})
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
disabled: currentPage >= pageCount,
|
|
749
|
-
onClick: () => {
|
|
750
|
-
setPage(currentPage + 1);
|
|
751
|
-
},
|
|
752
|
-
children: t("page.next")
|
|
753
|
-
})
|
|
754
|
-
]
|
|
755
|
-
}) : null
|
|
756
|
-
] }),
|
|
774
|
+
]
|
|
775
|
+
}) : null
|
|
776
|
+
]
|
|
777
|
+
})] }),
|
|
757
778
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
758
779
|
open: confirmed !== null,
|
|
759
780
|
onClose: () => {
|
|
@@ -844,8 +865,12 @@ window.__ModuleLoader__.load({
|
|
|
844
865
|
if (total <= 0) return 0;
|
|
845
866
|
return totals.cacheReadTokens / total * 100;
|
|
846
867
|
}
|
|
847
|
-
/**
|
|
848
|
-
|
|
868
|
+
/**
|
|
869
|
+
* 显示口径的单项:输入(含缓存输入)、缓存输入、缓存命中率、输出、推理,
|
|
870
|
+
* 以及活动计数(轮次 / 步骤 / 用户输入 / 工具调用)——没有合计项。
|
|
871
|
+
* 活动计数在按模型的行上不显示(事件没有模型归属,见 `withActivity`)。
|
|
872
|
+
*/
|
|
873
|
+
function usageMetrics(totals, t, options = {}) {
|
|
849
874
|
return [
|
|
850
875
|
{
|
|
851
876
|
key: "input",
|
|
@@ -877,12 +902,32 @@ window.__ModuleLoader__.load({
|
|
|
877
902
|
value: totals.reasoningTokens,
|
|
878
903
|
kind: "tokens"
|
|
879
904
|
},
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
905
|
+
...options.withActivity === false ? [] : [
|
|
906
|
+
{
|
|
907
|
+
key: "turns",
|
|
908
|
+
label: t("usage.turns"),
|
|
909
|
+
value: totals.turns,
|
|
910
|
+
kind: "count"
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
key: "steps",
|
|
914
|
+
label: t("usage.steps"),
|
|
915
|
+
value: totals.steps,
|
|
916
|
+
kind: "count"
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
key: "userInputs",
|
|
920
|
+
label: t("usage.userInputs"),
|
|
921
|
+
value: totals.userInputs,
|
|
922
|
+
kind: "count"
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
key: "toolCalls",
|
|
926
|
+
label: t("usage.toolCalls"),
|
|
927
|
+
value: totals.toolCalls,
|
|
928
|
+
kind: "count"
|
|
929
|
+
}
|
|
930
|
+
]
|
|
886
931
|
];
|
|
887
932
|
}
|
|
888
933
|
/** 折叠行的排序口径(不显示):输入(含缓存)+ 输出。 */
|
|
@@ -891,7 +936,10 @@ window.__ModuleLoader__.load({
|
|
|
891
936
|
}
|
|
892
937
|
function emptyTotals() {
|
|
893
938
|
return {
|
|
894
|
-
|
|
939
|
+
turns: 0,
|
|
940
|
+
steps: 0,
|
|
941
|
+
userInputs: 0,
|
|
942
|
+
toolCalls: 0,
|
|
895
943
|
inputTokens: 0,
|
|
896
944
|
outputTokens: 0,
|
|
897
945
|
cacheReadTokens: 0,
|
|
@@ -909,7 +957,6 @@ window.__ModuleLoader__.load({
|
|
|
909
957
|
label: key,
|
|
910
958
|
totals: emptyTotals()
|
|
911
959
|
};
|
|
912
|
-
row.totals.events += bucket.events;
|
|
913
960
|
row.totals.inputTokens += bucket.inputTokens;
|
|
914
961
|
row.totals.outputTokens += bucket.outputTokens;
|
|
915
962
|
row.totals.cacheReadTokens += bucket.cacheReadTokens;
|
|
@@ -920,7 +967,7 @@ window.__ModuleLoader__.load({
|
|
|
920
967
|
return [...folded.values()].sort((left, right) => sortWeight(right.totals) - sortWeight(left.totals));
|
|
921
968
|
}
|
|
922
969
|
/** 一行用量:label 在上,下面是横向排布的单项。 */
|
|
923
|
-
function UsageRow({ rowKey, label, totals, t }) {
|
|
970
|
+
function UsageRow({ rowKey, label, totals, t, withActivity = true }) {
|
|
924
971
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
925
972
|
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageRow),
|
|
926
973
|
"data-usage-key": rowKey,
|
|
@@ -929,7 +976,7 @@ window.__ModuleLoader__.load({
|
|
|
929
976
|
children: label
|
|
930
977
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
931
978
|
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageMetrics),
|
|
932
|
-
children: usageMetrics(totals, t).map((metric) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
979
|
+
children: usageMetrics(totals, t, { withActivity }).map((metric) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
933
980
|
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageMetric),
|
|
934
981
|
"data-usage-cell": metric.key,
|
|
935
982
|
"data-usage-value": metric.value,
|
|
@@ -938,13 +985,13 @@ window.__ModuleLoader__.load({
|
|
|
938
985
|
children: metric.label
|
|
939
986
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
940
987
|
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageMetricValue),
|
|
941
|
-
children: metric.kind === "percent" ? formatPercent(metric.value) : formatTokens(metric.value)
|
|
988
|
+
children: metric.kind === "percent" ? formatPercent(metric.value) : metric.kind === "count" ? formatCount(metric.value) : formatTokens(metric.value)
|
|
942
989
|
})]
|
|
943
990
|
}, metric.key))
|
|
944
991
|
})]
|
|
945
992
|
});
|
|
946
993
|
}
|
|
947
|
-
function UsageList({ rows, t }) {
|
|
994
|
+
function UsageList({ rows, t, withActivity = true }) {
|
|
948
995
|
if (rows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
949
996
|
..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
|
|
950
997
|
"data-usage-status": "empty",
|
|
@@ -956,7 +1003,8 @@ window.__ModuleLoader__.load({
|
|
|
956
1003
|
rowKey: row.key,
|
|
957
1004
|
label: row.label,
|
|
958
1005
|
totals: row.totals,
|
|
959
|
-
t
|
|
1006
|
+
t,
|
|
1007
|
+
withActivity
|
|
960
1008
|
}, row.key))
|
|
961
1009
|
});
|
|
962
1010
|
}
|
|
@@ -1046,7 +1094,8 @@ window.__ModuleLoader__.load({
|
|
|
1046
1094
|
t
|
|
1047
1095
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageList, {
|
|
1048
1096
|
rows,
|
|
1049
|
-
t
|
|
1097
|
+
t,
|
|
1098
|
+
withActivity: tab === "sessions"
|
|
1050
1099
|
})
|
|
1051
1100
|
]
|
|
1052
1101
|
});
|
|
@@ -1090,7 +1139,10 @@ window.__ModuleLoader__.load({
|
|
|
1090
1139
|
"usage.cacheRate": "缓存命中率",
|
|
1091
1140
|
"usage.reasoning": "推理",
|
|
1092
1141
|
"usage.total": "合计",
|
|
1093
|
-
"usage.
|
|
1142
|
+
"usage.turns": "轮次",
|
|
1143
|
+
"usage.steps": "步骤",
|
|
1144
|
+
"usage.userInputs": "用户输入",
|
|
1145
|
+
"usage.toolCalls": "工具调用",
|
|
1094
1146
|
"usage.subagentOnly": "其中子代理",
|
|
1095
1147
|
"usage.unknownModel": "未知模型",
|
|
1096
1148
|
archive: "归档",
|
|
@@ -1162,7 +1214,10 @@ window.__ModuleLoader__.load({
|
|
|
1162
1214
|
"usage.cacheRate": "Cache hit rate",
|
|
1163
1215
|
"usage.reasoning": "Reasoning",
|
|
1164
1216
|
"usage.total": "Total",
|
|
1165
|
-
"usage.
|
|
1217
|
+
"usage.turns": "Turns",
|
|
1218
|
+
"usage.steps": "Steps",
|
|
1219
|
+
"usage.userInputs": "User inputs",
|
|
1220
|
+
"usage.toolCalls": "Tool calls",
|
|
1166
1221
|
"usage.subagentOnly": "Subagent share",
|
|
1167
1222
|
"usage.unknownModel": "Unknown model",
|
|
1168
1223
|
archive: "Archive",
|
|
@@ -1225,10 +1280,13 @@ window.__ModuleLoader__.load({
|
|
|
1225
1280
|
}), "ui-conversation-manager: dictionaries");
|
|
1226
1281
|
const t = ctx.locale.bind(NS);
|
|
1227
1282
|
const sessions = ctx.get("sessions");
|
|
1283
|
+
const refreshList = sessions.refresh;
|
|
1228
1284
|
const controller = new ConversationManagerController({
|
|
1229
1285
|
archiveSession: (sessionId) => ctx.uiWorkspace.archiveSession(sessionId),
|
|
1230
1286
|
unarchiveSession: (sessionId) => ctx.uiWorkspace.unarchiveSession(sessionId),
|
|
1231
|
-
refresh: () =>
|
|
1287
|
+
refresh: async () => {
|
|
1288
|
+
await refreshList?.call(sessions);
|
|
1289
|
+
}
|
|
1232
1290
|
});
|
|
1233
1291
|
ctx.slots.inject("main", () => ctx.slots.register({
|
|
1234
1292
|
name: "main",
|
package/dist/client.d.cts
CHANGED
|
@@ -35,7 +35,10 @@ declare const zh: {
|
|
|
35
35
|
"usage.cacheRate": string;
|
|
36
36
|
"usage.reasoning": string;
|
|
37
37
|
"usage.total": string;
|
|
38
|
-
"usage.
|
|
38
|
+
"usage.turns": string;
|
|
39
|
+
"usage.steps": string;
|
|
40
|
+
"usage.userInputs": string;
|
|
41
|
+
"usage.toolCalls": string;
|
|
39
42
|
"usage.subagentOnly": string;
|
|
40
43
|
"usage.unknownModel": string;
|
|
41
44
|
archive: string;
|
|
@@ -3023,21 +3026,32 @@ declare class SessionStore extends Service {
|
|
|
3023
3026
|
//#region src/client/controller.d.ts
|
|
3024
3027
|
/** 时间范围的语义键(与 session-rdb `./usage` 的 `UsageRangeKey` 镜像)。 */
|
|
3025
3028
|
type UsageRangeKey = "all" | "day" | "week" | "7d" | "30d" | "90d";
|
|
3026
|
-
/**
|
|
3027
|
-
interface
|
|
3028
|
-
|
|
3029
|
+
/** 活动计数(与 session-rdb `./usage` 的回报结构镜像):轮次 / 步骤 / 用户输入 / 工具调用。 */
|
|
3030
|
+
interface UsageActivityTotals {
|
|
3031
|
+
turns: number;
|
|
3032
|
+
steps: number;
|
|
3033
|
+
userInputs: number;
|
|
3034
|
+
toolCalls: number;
|
|
3035
|
+
}
|
|
3036
|
+
/** 一段用量合计:token 用量 + 活动计数。 */
|
|
3037
|
+
interface UsageTotals extends UsageActivityTotals {
|
|
3029
3038
|
inputTokens: number;
|
|
3030
3039
|
outputTokens: number;
|
|
3031
3040
|
cacheReadTokens: number;
|
|
3032
3041
|
reasoningTokens: number;
|
|
3033
3042
|
totalTokens: number;
|
|
3034
3043
|
}
|
|
3035
|
-
/** 一天 × 一个模型 × 是否子代理
|
|
3036
|
-
interface UsageBucket
|
|
3044
|
+
/** 一天 × 一个模型 × 是否子代理 的用量桶:活动计数没有模型归属,只有 token 用量。 */
|
|
3045
|
+
interface UsageBucket {
|
|
3037
3046
|
day: string;
|
|
3038
3047
|
provider: string | null;
|
|
3039
3048
|
model: string | null;
|
|
3040
3049
|
subagent: boolean;
|
|
3050
|
+
inputTokens: number;
|
|
3051
|
+
outputTokens: number;
|
|
3052
|
+
cacheReadTokens: number;
|
|
3053
|
+
reasoningTokens: number;
|
|
3054
|
+
totalTokens: number;
|
|
3041
3055
|
}
|
|
3042
3056
|
/** 一条会话的用量行。 */
|
|
3043
3057
|
interface UsageSessionRow extends UsageTotals {
|
package/dist/client.d.mts
CHANGED
|
@@ -35,7 +35,10 @@ declare const zh: {
|
|
|
35
35
|
"usage.cacheRate": string;
|
|
36
36
|
"usage.reasoning": string;
|
|
37
37
|
"usage.total": string;
|
|
38
|
-
"usage.
|
|
38
|
+
"usage.turns": string;
|
|
39
|
+
"usage.steps": string;
|
|
40
|
+
"usage.userInputs": string;
|
|
41
|
+
"usage.toolCalls": string;
|
|
39
42
|
"usage.subagentOnly": string;
|
|
40
43
|
"usage.unknownModel": string;
|
|
41
44
|
archive: string;
|
|
@@ -3023,21 +3026,32 @@ declare class SessionStore extends Service {
|
|
|
3023
3026
|
//#region src/client/controller.d.ts
|
|
3024
3027
|
/** 时间范围的语义键(与 session-rdb `./usage` 的 `UsageRangeKey` 镜像)。 */
|
|
3025
3028
|
type UsageRangeKey = "all" | "day" | "week" | "7d" | "30d" | "90d";
|
|
3026
|
-
/**
|
|
3027
|
-
interface
|
|
3028
|
-
|
|
3029
|
+
/** 活动计数(与 session-rdb `./usage` 的回报结构镜像):轮次 / 步骤 / 用户输入 / 工具调用。 */
|
|
3030
|
+
interface UsageActivityTotals {
|
|
3031
|
+
turns: number;
|
|
3032
|
+
steps: number;
|
|
3033
|
+
userInputs: number;
|
|
3034
|
+
toolCalls: number;
|
|
3035
|
+
}
|
|
3036
|
+
/** 一段用量合计:token 用量 + 活动计数。 */
|
|
3037
|
+
interface UsageTotals extends UsageActivityTotals {
|
|
3029
3038
|
inputTokens: number;
|
|
3030
3039
|
outputTokens: number;
|
|
3031
3040
|
cacheReadTokens: number;
|
|
3032
3041
|
reasoningTokens: number;
|
|
3033
3042
|
totalTokens: number;
|
|
3034
3043
|
}
|
|
3035
|
-
/** 一天 × 一个模型 × 是否子代理
|
|
3036
|
-
interface UsageBucket
|
|
3044
|
+
/** 一天 × 一个模型 × 是否子代理 的用量桶:活动计数没有模型归属,只有 token 用量。 */
|
|
3045
|
+
interface UsageBucket {
|
|
3037
3046
|
day: string;
|
|
3038
3047
|
provider: string | null;
|
|
3039
3048
|
model: string | null;
|
|
3040
3049
|
subagent: boolean;
|
|
3050
|
+
inputTokens: number;
|
|
3051
|
+
outputTokens: number;
|
|
3052
|
+
cacheReadTokens: number;
|
|
3053
|
+
reasoningTokens: number;
|
|
3054
|
+
totalTokens: number;
|
|
3041
3055
|
}
|
|
3042
3056
|
/** 一条会话的用量行。 */
|
|
3043
3057
|
interface UsageSessionRow extends UsageTotals {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/ui-conversation-manager",
|
|
3
|
-
"version": "0.0.2-alpha.
|
|
3
|
+
"version": "0.0.2-alpha.4",
|
|
4
4
|
"description": "「对话管理」全局面板页(浏览器半):已归档会话的搜索、取消归档、删除,以及导入为新会话。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"archived-sessions",
|
|
@@ -37,18 +37,18 @@
|
|
|
37
37
|
"react-dom": "^18.2.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@deepseek-ai/cordis": "
|
|
41
|
-
"@deepseek-ai/dsh-api-session-controller": "
|
|
42
|
-
"@deepseek-ai/dsh-client-locale": "
|
|
43
|
-
"@deepseek-ai/dsh-client-ui-layout": "
|
|
44
|
-
"@deepseek-ai/dsh-client-ui-primitives": "
|
|
45
|
-
"@deepseek-ai/dsh-client-ui-renderer": "
|
|
46
|
-
"@deepseek-ai/dsh-client-ui-session": "
|
|
47
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "
|
|
48
|
-
"@deepseek-ai/dsh-client-ui-slots": "
|
|
49
|
-
"@deepseek-ai/dsh-client-ui-workspace": "
|
|
50
|
-
"@deepseek-ai/dsh-session": "
|
|
51
|
-
"@morlay/dsh-client-ui-primitives": "
|
|
40
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
41
|
+
"@deepseek-ai/dsh-api-session-controller": "0.1.6-alpha.2",
|
|
42
|
+
"@deepseek-ai/dsh-client-locale": "0.1.6-alpha.2",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.1.6-alpha.2",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.6-alpha.2",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.2",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-session": "0.1.6-alpha.2",
|
|
47
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "0.1.6-alpha.2",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.2",
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-workspace": "0.1.6-alpha.2",
|
|
50
|
+
"@deepseek-ai/dsh-session": "0.1.6-alpha.2",
|
|
51
|
+
"@morlay/dsh-client-ui-primitives": "0.0.2-alpha.4"
|
|
52
52
|
},
|
|
53
53
|
"dsh": {
|
|
54
54
|
"client": {
|
|
@@ -6,8 +6,9 @@ const spin = styling.keyframes({
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
/** 「对话管理」页面的样式表(官方 --dsw-* 变量 + 我们的 css-in-js 层)。
|
|
9
|
-
* 页面根是纵向 flex
|
|
10
|
-
*
|
|
9
|
+
* 页面根是纵向 flex 且**自己不滚**:页头(标题 / tab 条 / 导入 / 清理)与搜索行固定在顶部,
|
|
10
|
+
* 只有 `scroll` 那层随内容滚。除滚动层外的项都 `flex: none`,否则内容变长时 flex 会把这些项
|
|
11
|
+
* 压到 min-content(官方 Input 的 32px 高会被压成一行文字高)。 */
|
|
11
12
|
export const styles = {
|
|
12
13
|
page: {
|
|
13
14
|
display: "flex",
|
|
@@ -17,9 +18,18 @@ export const styles = {
|
|
|
17
18
|
width: "100%",
|
|
18
19
|
height: "100%",
|
|
19
20
|
padding: "24px 28px",
|
|
20
|
-
overflow: "
|
|
21
|
+
overflow: "hidden",
|
|
21
22
|
color: "var(--dsw-alias-label-primary)",
|
|
22
23
|
},
|
|
24
|
+
// 页面里唯一的滚动容器:提示 / 空态 / 列表 / 分页(或 usage 视图)都在它里面。
|
|
25
|
+
scroll: {
|
|
26
|
+
display: "flex",
|
|
27
|
+
flexDirection: "column",
|
|
28
|
+
gap: "12px",
|
|
29
|
+
flex: "1 1 auto",
|
|
30
|
+
minHeight: "0",
|
|
31
|
+
overflowY: "auto",
|
|
32
|
+
},
|
|
23
33
|
header: {
|
|
24
34
|
display: "flex",
|
|
25
35
|
alignItems: "center",
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
type UsageRangeKey,
|
|
22
22
|
type UsageTotals,
|
|
23
23
|
} from "./controller.ts";
|
|
24
|
-
import { formatPercent, formatTokens } from "./format.ts";
|
|
24
|
+
import { formatCount, formatPercent, formatTokens } from "./format.ts";
|
|
25
25
|
import { styles } from "./ConversationManagerPage.styles.ts";
|
|
26
26
|
|
|
27
27
|
/** 一页的行数(会话列表)。 */
|
|
@@ -286,16 +286,18 @@ export function ConversationManagerPage({
|
|
|
286
286
|
/>
|
|
287
287
|
</div>
|
|
288
288
|
{view === "usage" ? (
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
289
|
+
<div {...styling.props(styles.scroll)} data-scroll="page">
|
|
290
|
+
<UsageView
|
|
291
|
+
report={usage}
|
|
292
|
+
loading={usageLoading}
|
|
293
|
+
error={usageError}
|
|
294
|
+
tab={usageTab}
|
|
295
|
+
onTab={setUsageTab}
|
|
296
|
+
range={usageRange}
|
|
297
|
+
onRange={requestUsage}
|
|
298
|
+
t={t}
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
299
301
|
) : (
|
|
300
302
|
<>
|
|
301
303
|
<div {...styling.props(styles.filters)}>
|
|
@@ -323,134 +325,136 @@ export function ConversationManagerPage({
|
|
|
323
325
|
/>
|
|
324
326
|
</span>
|
|
325
327
|
</div>
|
|
326
|
-
{
|
|
327
|
-
|
|
328
|
-
{notice
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
{failure}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
{
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
{
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
{
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
<span {...styling.props(styles.
|
|
358
|
-
<span {...styling.props(styles.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
{
|
|
328
|
+
<div {...styling.props(styles.scroll)} data-scroll="page">
|
|
329
|
+
{notice === null ? null : (
|
|
330
|
+
<p {...styling.props(styles.status)} data-notice="result">
|
|
331
|
+
{notice}
|
|
332
|
+
</p>
|
|
333
|
+
)}
|
|
334
|
+
{failure === null ? null : (
|
|
335
|
+
<p {...styling.props(styles.failure)} data-failure="result" role="alert">
|
|
336
|
+
{failure}
|
|
337
|
+
</p>
|
|
338
|
+
)}
|
|
339
|
+
{listed.length === 0 ? (
|
|
340
|
+
<p {...styling.props(styles.status)} data-status="empty">
|
|
341
|
+
{t("empty")}
|
|
342
|
+
</p>
|
|
343
|
+
) : null}
|
|
344
|
+
{listed.length > 0 && matched.length === 0 ? (
|
|
345
|
+
<p {...styling.props(styles.status)} data-status="empty-search">
|
|
346
|
+
{t("emptySearch")}
|
|
347
|
+
</p>
|
|
348
|
+
) : null}
|
|
349
|
+
{visible.length > 0 ? (
|
|
350
|
+
<ul {...styling.props(styles.list)}>
|
|
351
|
+
{visible.map((row) => (
|
|
352
|
+
<li
|
|
353
|
+
key={row.id}
|
|
354
|
+
{...styling.props(styles.row)}
|
|
355
|
+
data-session-id={String(row.id)}
|
|
356
|
+
data-archived={row.archived ? "true" : "false"}
|
|
357
|
+
data-subagent={row.subagent ? "true" : "false"}
|
|
358
|
+
>
|
|
359
|
+
<span {...styling.props(styles.identity)}>
|
|
360
|
+
<span {...styling.props(styles.titleLine)}>
|
|
361
|
+
<span {...styling.props(styles.rowTitle)}>{row.title}</span>
|
|
362
|
+
{row.archived ? <Tag tone="neutral">{t("archived")}</Tag> : null}
|
|
363
|
+
{row.subagent ? <Tag tone="quiet">{t("subagent")}</Tag> : null}
|
|
364
|
+
</span>
|
|
365
|
+
<span {...styling.props(styles.meta)}>
|
|
366
|
+
{[row.workspace, timeLabel(row.updatedAt, now, t)].join(" · ")}
|
|
367
|
+
</span>
|
|
364
368
|
</span>
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
369
|
+
<span {...styling.props(styles.actions)}>
|
|
370
|
+
{row.archived ? (
|
|
371
|
+
<Button
|
|
372
|
+
variant="outline"
|
|
373
|
+
size="sm"
|
|
374
|
+
data-action="unarchive"
|
|
375
|
+
aria-label={t("unarchiveNamed", { title: row.title })}
|
|
376
|
+
onClick={() => {
|
|
377
|
+
run(unarchive(row.id));
|
|
378
|
+
}}
|
|
379
|
+
>
|
|
380
|
+
{t("unarchive")}
|
|
381
|
+
</Button>
|
|
382
|
+
) : (
|
|
383
|
+
<Button
|
|
384
|
+
variant="outline"
|
|
385
|
+
size="sm"
|
|
386
|
+
data-action="archive"
|
|
387
|
+
aria-label={t("archiveNamed", { title: row.title })}
|
|
388
|
+
onClick={() => {
|
|
389
|
+
run(archive(row.id));
|
|
390
|
+
}}
|
|
391
|
+
>
|
|
392
|
+
{t("archive")}
|
|
393
|
+
</Button>
|
|
394
|
+
)}
|
|
368
395
|
<Button
|
|
369
396
|
variant="outline"
|
|
370
397
|
size="sm"
|
|
371
|
-
data-action="
|
|
372
|
-
aria-label={t("
|
|
398
|
+
data-action="export"
|
|
399
|
+
aria-label={t("exportNamed", { title: row.title })}
|
|
373
400
|
onClick={() => {
|
|
374
|
-
run(
|
|
401
|
+
run(exportZip(row.id));
|
|
375
402
|
}}
|
|
376
403
|
>
|
|
377
|
-
{t("
|
|
404
|
+
{t("export")}
|
|
378
405
|
</Button>
|
|
379
|
-
) : (
|
|
380
406
|
<Button
|
|
381
407
|
variant="outline"
|
|
382
408
|
size="sm"
|
|
383
|
-
|
|
384
|
-
|
|
409
|
+
disabled={!row.archived}
|
|
410
|
+
data-action="remove"
|
|
411
|
+
aria-label={t("removeNamed", { title: row.title })}
|
|
385
412
|
onClick={() => {
|
|
386
|
-
|
|
413
|
+
setFailure(null);
|
|
414
|
+
setNotice(null);
|
|
415
|
+
setConfirming(row);
|
|
387
416
|
}}
|
|
388
417
|
>
|
|
389
|
-
{t("
|
|
418
|
+
{t("remove")}
|
|
390
419
|
</Button>
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
</Button>
|
|
403
|
-
<Button
|
|
404
|
-
variant="outline"
|
|
405
|
-
size="sm"
|
|
406
|
-
disabled={!row.archived}
|
|
407
|
-
data-action="remove"
|
|
408
|
-
aria-label={t("removeNamed", { title: row.title })}
|
|
409
|
-
onClick={() => {
|
|
410
|
-
setFailure(null);
|
|
411
|
-
setNotice(null);
|
|
412
|
-
setConfirming(row);
|
|
413
|
-
}}
|
|
414
|
-
>
|
|
415
|
-
{t("remove")}
|
|
416
|
-
</Button>
|
|
417
|
-
</span>
|
|
418
|
-
</li>
|
|
419
|
-
))}
|
|
420
|
-
</ul>
|
|
421
|
-
) : null}
|
|
422
|
-
{matched.length > 0 ? (
|
|
423
|
-
<div
|
|
424
|
-
{...styling.props(styles.pagination)}
|
|
425
|
-
data-pagination=""
|
|
426
|
-
data-page-current={currentPage}
|
|
427
|
-
data-page-total={pageCount}
|
|
428
|
-
>
|
|
429
|
-
<Button
|
|
430
|
-
variant="ghost"
|
|
431
|
-
size="sm"
|
|
432
|
-
disabled={currentPage <= 1}
|
|
433
|
-
onClick={() => {
|
|
434
|
-
setPage(currentPage - 1);
|
|
435
|
-
}}
|
|
436
|
-
>
|
|
437
|
-
{t("page.previous")}
|
|
438
|
-
</Button>
|
|
439
|
-
<span {...styling.props(styles.paginationLabel)}>
|
|
440
|
-
{t("page.label", { page: currentPage, total: pageCount })}
|
|
441
|
-
</span>
|
|
442
|
-
<Button
|
|
443
|
-
variant="ghost"
|
|
444
|
-
size="sm"
|
|
445
|
-
disabled={currentPage >= pageCount}
|
|
446
|
-
onClick={() => {
|
|
447
|
-
setPage(currentPage + 1);
|
|
448
|
-
}}
|
|
420
|
+
</span>
|
|
421
|
+
</li>
|
|
422
|
+
))}
|
|
423
|
+
</ul>
|
|
424
|
+
) : null}
|
|
425
|
+
{matched.length > 0 ? (
|
|
426
|
+
<div
|
|
427
|
+
{...styling.props(styles.pagination)}
|
|
428
|
+
data-pagination=""
|
|
429
|
+
data-page-current={currentPage}
|
|
430
|
+
data-page-total={pageCount}
|
|
449
431
|
>
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
432
|
+
<Button
|
|
433
|
+
variant="ghost"
|
|
434
|
+
size="sm"
|
|
435
|
+
disabled={currentPage <= 1}
|
|
436
|
+
onClick={() => {
|
|
437
|
+
setPage(currentPage - 1);
|
|
438
|
+
}}
|
|
439
|
+
>
|
|
440
|
+
{t("page.previous")}
|
|
441
|
+
</Button>
|
|
442
|
+
<span {...styling.props(styles.paginationLabel)}>
|
|
443
|
+
{t("page.label", { page: currentPage, total: pageCount })}
|
|
444
|
+
</span>
|
|
445
|
+
<Button
|
|
446
|
+
variant="ghost"
|
|
447
|
+
size="sm"
|
|
448
|
+
disabled={currentPage >= pageCount}
|
|
449
|
+
onClick={() => {
|
|
450
|
+
setPage(currentPage + 1);
|
|
451
|
+
}}
|
|
452
|
+
>
|
|
453
|
+
{t("page.next")}
|
|
454
|
+
</Button>
|
|
455
|
+
</div>
|
|
456
|
+
) : null}
|
|
457
|
+
</div>
|
|
454
458
|
</>
|
|
455
459
|
)}
|
|
456
460
|
<Modal
|
|
@@ -553,9 +557,9 @@ function rangeLabel(range: UsageRange, t: Translate): string {
|
|
|
553
557
|
interface UsageMetric {
|
|
554
558
|
key: string;
|
|
555
559
|
label: string;
|
|
556
|
-
/** 原始值:token
|
|
560
|
+
/** 原始值:token 数、计数,或百分点(`percent` 项)。 */
|
|
557
561
|
value: number;
|
|
558
|
-
kind: "tokens" | "percent";
|
|
562
|
+
kind: "tokens" | "count" | "percent";
|
|
559
563
|
}
|
|
560
564
|
|
|
561
565
|
/** 缓存命中率(百分点):缓存输入占总输入(含缓存)的比例。 */
|
|
@@ -565,8 +569,16 @@ function cacheHitPercent(totals: UsageTotals): number {
|
|
|
565
569
|
return (totals.cacheReadTokens / total) * 100;
|
|
566
570
|
}
|
|
567
571
|
|
|
568
|
-
/**
|
|
569
|
-
|
|
572
|
+
/**
|
|
573
|
+
* 显示口径的单项:输入(含缓存输入)、缓存输入、缓存命中率、输出、推理,
|
|
574
|
+
* 以及活动计数(轮次 / 步骤 / 用户输入 / 工具调用)——没有合计项。
|
|
575
|
+
* 活动计数在按模型的行上不显示(事件没有模型归属,见 `withActivity`)。
|
|
576
|
+
*/
|
|
577
|
+
function usageMetrics(
|
|
578
|
+
totals: UsageTotals,
|
|
579
|
+
t: Translate,
|
|
580
|
+
options: { withActivity?: boolean } = {},
|
|
581
|
+
): UsageMetric[] {
|
|
570
582
|
return [
|
|
571
583
|
{
|
|
572
584
|
key: "input",
|
|
@@ -593,7 +605,24 @@ function usageMetrics(totals: UsageTotals, t: Translate): UsageMetric[] {
|
|
|
593
605
|
value: totals.reasoningTokens,
|
|
594
606
|
kind: "tokens",
|
|
595
607
|
},
|
|
596
|
-
|
|
608
|
+
...(options.withActivity === false
|
|
609
|
+
? []
|
|
610
|
+
: [
|
|
611
|
+
{ key: "turns", label: t("usage.turns"), value: totals.turns, kind: "count" as const },
|
|
612
|
+
{ key: "steps", label: t("usage.steps"), value: totals.steps, kind: "count" as const },
|
|
613
|
+
{
|
|
614
|
+
key: "userInputs",
|
|
615
|
+
label: t("usage.userInputs"),
|
|
616
|
+
value: totals.userInputs,
|
|
617
|
+
kind: "count" as const,
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
key: "toolCalls",
|
|
621
|
+
label: t("usage.toolCalls"),
|
|
622
|
+
value: totals.toolCalls,
|
|
623
|
+
kind: "count" as const,
|
|
624
|
+
},
|
|
625
|
+
]),
|
|
597
626
|
];
|
|
598
627
|
}
|
|
599
628
|
|
|
@@ -610,7 +639,10 @@ interface UsageListRow {
|
|
|
610
639
|
|
|
611
640
|
function emptyTotals(): UsageTotals {
|
|
612
641
|
return {
|
|
613
|
-
|
|
642
|
+
turns: 0,
|
|
643
|
+
steps: 0,
|
|
644
|
+
userInputs: 0,
|
|
645
|
+
toolCalls: 0,
|
|
614
646
|
inputTokens: 0,
|
|
615
647
|
outputTokens: 0,
|
|
616
648
|
cacheReadTokens: 0,
|
|
@@ -628,7 +660,6 @@ function foldBuckets(
|
|
|
628
660
|
for (const bucket of buckets) {
|
|
629
661
|
const key = keyOf(bucket);
|
|
630
662
|
const row = folded.get(key) ?? { key, label: key, totals: emptyTotals() };
|
|
631
|
-
row.totals.events += bucket.events;
|
|
632
663
|
row.totals.inputTokens += bucket.inputTokens;
|
|
633
664
|
row.totals.outputTokens += bucket.outputTokens;
|
|
634
665
|
row.totals.cacheReadTokens += bucket.cacheReadTokens;
|
|
@@ -647,17 +678,19 @@ function UsageRow({
|
|
|
647
678
|
label,
|
|
648
679
|
totals,
|
|
649
680
|
t,
|
|
681
|
+
withActivity = true,
|
|
650
682
|
}: {
|
|
651
683
|
rowKey: string;
|
|
652
684
|
label: string;
|
|
653
685
|
totals: UsageTotals;
|
|
654
686
|
t: Translate;
|
|
687
|
+
withActivity?: boolean;
|
|
655
688
|
}): ReactNode {
|
|
656
689
|
return (
|
|
657
690
|
<li {...styling.props(styles.usageRow)} data-usage-key={rowKey}>
|
|
658
691
|
<span {...styling.props(styles.usageRowLabel)}>{label}</span>
|
|
659
692
|
<span {...styling.props(styles.usageMetrics)}>
|
|
660
|
-
{usageMetrics(totals, t).map((metric) => (
|
|
693
|
+
{usageMetrics(totals, t, { withActivity }).map((metric) => (
|
|
661
694
|
<span
|
|
662
695
|
key={metric.key}
|
|
663
696
|
{...styling.props(styles.usageMetric)}
|
|
@@ -666,7 +699,11 @@ function UsageRow({
|
|
|
666
699
|
>
|
|
667
700
|
<span {...styling.props(styles.usageMetricLabel)}>{metric.label}</span>
|
|
668
701
|
<span {...styling.props(styles.usageMetricValue)}>
|
|
669
|
-
{metric.kind === "percent"
|
|
702
|
+
{metric.kind === "percent"
|
|
703
|
+
? formatPercent(metric.value)
|
|
704
|
+
: metric.kind === "count"
|
|
705
|
+
? formatCount(metric.value)
|
|
706
|
+
: formatTokens(metric.value)}
|
|
670
707
|
</span>
|
|
671
708
|
</span>
|
|
672
709
|
))}
|
|
@@ -675,7 +712,15 @@ function UsageRow({
|
|
|
675
712
|
);
|
|
676
713
|
}
|
|
677
714
|
|
|
678
|
-
function UsageList({
|
|
715
|
+
function UsageList({
|
|
716
|
+
rows,
|
|
717
|
+
t,
|
|
718
|
+
withActivity = true,
|
|
719
|
+
}: {
|
|
720
|
+
rows: readonly UsageListRow[];
|
|
721
|
+
t: Translate;
|
|
722
|
+
withActivity?: boolean;
|
|
723
|
+
}): ReactNode {
|
|
679
724
|
if (rows.length === 0) {
|
|
680
725
|
return (
|
|
681
726
|
<p {...styling.props(styles.status)} data-usage-status="empty">
|
|
@@ -686,7 +731,14 @@ function UsageList({ rows, t }: { rows: readonly UsageListRow[]; t: Translate })
|
|
|
686
731
|
return (
|
|
687
732
|
<ul {...styling.props(styles.usageList)}>
|
|
688
733
|
{rows.map((row) => (
|
|
689
|
-
<UsageRow
|
|
734
|
+
<UsageRow
|
|
735
|
+
key={row.key}
|
|
736
|
+
rowKey={row.key}
|
|
737
|
+
label={row.label}
|
|
738
|
+
totals={row.totals}
|
|
739
|
+
t={t}
|
|
740
|
+
withActivity={withActivity}
|
|
741
|
+
/>
|
|
690
742
|
))}
|
|
691
743
|
</ul>
|
|
692
744
|
);
|
|
@@ -794,7 +846,7 @@ function UsageView({
|
|
|
794
846
|
{report === null ? null : tab === "overview" ? (
|
|
795
847
|
<UsageOverview report={report} t={t} />
|
|
796
848
|
) : (
|
|
797
|
-
<UsageList rows={rows} t={t} />
|
|
849
|
+
<UsageList rows={rows} t={t} withActivity={tab === "sessions"} />
|
|
798
850
|
)}
|
|
799
851
|
</div>
|
|
800
852
|
);
|
package/src/client/controller.ts
CHANGED
|
@@ -10,9 +10,16 @@ export const SESSION_USAGE_PATH = "/api/session.usage";
|
|
|
10
10
|
/** 时间范围的语义键(与 session-rdb `./usage` 的 `UsageRangeKey` 镜像)。 */
|
|
11
11
|
export type UsageRangeKey = "all" | "day" | "week" | "7d" | "30d" | "90d";
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
export interface
|
|
15
|
-
|
|
13
|
+
/** 活动计数(与 session-rdb `./usage` 的回报结构镜像):轮次 / 步骤 / 用户输入 / 工具调用。 */
|
|
14
|
+
export interface UsageActivityTotals {
|
|
15
|
+
turns: number;
|
|
16
|
+
steps: number;
|
|
17
|
+
userInputs: number;
|
|
18
|
+
toolCalls: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 一段用量合计:token 用量 + 活动计数。 */
|
|
22
|
+
export interface UsageTotals extends UsageActivityTotals {
|
|
16
23
|
inputTokens: number;
|
|
17
24
|
outputTokens: number;
|
|
18
25
|
cacheReadTokens: number;
|
|
@@ -20,12 +27,17 @@ export interface UsageTotals {
|
|
|
20
27
|
totalTokens: number;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
/** 一天 × 一个模型 × 是否子代理
|
|
24
|
-
export interface UsageBucket
|
|
30
|
+
/** 一天 × 一个模型 × 是否子代理 的用量桶:活动计数没有模型归属,只有 token 用量。 */
|
|
31
|
+
export interface UsageBucket {
|
|
25
32
|
day: string;
|
|
26
33
|
provider: string | null;
|
|
27
34
|
model: string | null;
|
|
28
35
|
subagent: boolean;
|
|
36
|
+
inputTokens: number;
|
|
37
|
+
outputTokens: number;
|
|
38
|
+
cacheReadTokens: number;
|
|
39
|
+
reasoningTokens: number;
|
|
40
|
+
totalTokens: number;
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
/** 一条会话的用量行。 */
|
package/src/client/format.ts
CHANGED
|
@@ -14,6 +14,12 @@ export function formatTokens(value: number): string {
|
|
|
14
14
|
return `${trim(value / 1_000_000_000)}B`;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** @param value - 计数(轮次 / 步骤 / 用户输入 / 工具调用)。 @returns 千分位原文,例如 `1,024`。 */
|
|
18
|
+
export function formatCount(value: number): string {
|
|
19
|
+
if (!Number.isFinite(value) || value <= 0) return "0";
|
|
20
|
+
return Math.round(value).toLocaleString("en-US");
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
/** @param value - 百分点(0~100)。 @returns 紧凑百分比,例如 `98.4%`、`100%`。 */
|
|
18
24
|
export function formatPercent(value: number): string {
|
|
19
25
|
if (!Number.isFinite(value) || value <= 0) return "0%";
|
package/src/client/index.ts
CHANGED
|
@@ -43,10 +43,15 @@ export function apply(ctx: ClientContext): void {
|
|
|
43
43
|
const t = ctx.locale.bind(NS);
|
|
44
44
|
// ctx.sessions 的类型被别的 client 半的声明占住(SessionStore),按既有做法从服务面取。
|
|
45
45
|
const sessions = ctx.get("sessions") as unknown as ISessions;
|
|
46
|
+
// 列表刷新入口在运行时可能缺席(`refresh` 是类型面的方法,实测并非每个运行时都提供):
|
|
47
|
+
// 有就调、没有就跳过——列表本身由 host 的 `api-session/*` 推送驱动,不会因此停在旧值。
|
|
48
|
+
const refreshList = (sessions as unknown as { refresh?: () => Promise<void> }).refresh;
|
|
46
49
|
const controller = new ConversationManagerController({
|
|
47
50
|
archiveSession: (sessionId) => ctx.uiWorkspace.archiveSession(sessionId),
|
|
48
51
|
unarchiveSession: (sessionId) => ctx.uiWorkspace.unarchiveSession(sessionId),
|
|
49
|
-
refresh: () =>
|
|
52
|
+
refresh: async () => {
|
|
53
|
+
await refreshList?.call(sessions);
|
|
54
|
+
},
|
|
50
55
|
});
|
|
51
56
|
|
|
52
57
|
ctx.slots.inject("main", () =>
|
package/src/client/locales.ts
CHANGED
|
@@ -31,7 +31,10 @@ export const zh = {
|
|
|
31
31
|
"usage.cacheRate": "缓存命中率",
|
|
32
32
|
"usage.reasoning": "推理",
|
|
33
33
|
"usage.total": "合计",
|
|
34
|
-
"usage.
|
|
34
|
+
"usage.turns": "轮次",
|
|
35
|
+
"usage.steps": "步骤",
|
|
36
|
+
"usage.userInputs": "用户输入",
|
|
37
|
+
"usage.toolCalls": "工具调用",
|
|
35
38
|
"usage.subagentOnly": "其中子代理",
|
|
36
39
|
"usage.unknownModel": "未知模型",
|
|
37
40
|
archive: "归档",
|
|
@@ -108,7 +111,10 @@ export const en = {
|
|
|
108
111
|
"usage.cacheRate": "Cache hit rate",
|
|
109
112
|
"usage.reasoning": "Reasoning",
|
|
110
113
|
"usage.total": "Total",
|
|
111
|
-
"usage.
|
|
114
|
+
"usage.turns": "Turns",
|
|
115
|
+
"usage.steps": "Steps",
|
|
116
|
+
"usage.userInputs": "User inputs",
|
|
117
|
+
"usage.toolCalls": "Tool calls",
|
|
112
118
|
"usage.subagentOnly": "Subagent share",
|
|
113
119
|
"usage.unknownModel": "Unknown model",
|
|
114
120
|
archive: "Archive",
|