@sparkelf/dsh-task-bus 0.1.8 → 0.2.0

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/lib/client.js CHANGED
@@ -837,214 +837,523 @@ window.__ModuleLoader__.load({
837
837
  * that stands in for "disabled". A row missing `reviewStatus` counts as pending, which
838
838
  * matches `reviewStatusOf` in the package.
839
839
  */
840
- async function readSkillPending() {
840
+ async function readSkillPlazaRows() {
841
841
  try {
842
- const [listResponse, plazaResponse] = await Promise.all([fetch("/api/dsh-skill-center/list", { cache: "no-store" }), fetch("/integrations/dataops/skill-plaza?mine=1", { cache: "no-store" })]);
842
+ const [listResponse, plazaResponse] = await Promise.all([fetch("api/dsh-skill-center/list", { cache: "no-store" }), fetch("integrations/dataops/skill-plaza?mine=1", { cache: "no-store" })]);
843
843
  const disabled = /* @__PURE__ */ new Set();
844
844
  if (listResponse.ok) {
845
845
  const body = await listResponse.json();
846
846
  for (const group of body.groups ?? []) for (const skill of group.skills ?? []) if (typeof skill.name === "string" && skill.modelInvocable === false) disabled.add(skill.name);
847
847
  }
848
- if (!plazaResponse.ok) return 0;
848
+ if (!plazaResponse.ok) return [];
849
849
  const plaza = await plazaResponse.json();
850
- const names = /* @__PURE__ */ new Set();
850
+ const names = [];
851
851
  for (const row of plaza.skills ?? []) {
852
852
  if (typeof row.name !== "string" || row.name === "") continue;
853
853
  if (row.reviewStatus === "approved") continue;
854
854
  if (disabled.has(row.name)) continue;
855
- names.add(row.name);
855
+ names.push(row.name);
856
856
  }
857
- return names.size;
857
+ return names;
858
858
  } catch {
859
- return 0;
859
+ return [];
860
860
  }
861
861
  }
862
- /** A grouped notification row for the skill plaza. */
863
- function SkillPendingRow({ count, onOpen }) {
862
+ /**
863
+ * The same review inbox the plaza bell used to show, now rendered inside the task center's
864
+ * notification tab so every notification lives in one list.
865
+ */
866
+ async function readReviewInbox() {
867
+ try {
868
+ const response = await fetch("integrations/dataops/review-notifications", { cache: "no-store" });
869
+ if (!response.ok) return [];
870
+ const body = await response.json();
871
+ const rows = [];
872
+ for (const raw of body.items ?? []) {
873
+ if (typeof raw.id !== "string" || typeof raw.title !== "string") continue;
874
+ rows.push({
875
+ id: raw.id,
876
+ type: typeof raw.type === "string" ? raw.type : "info",
877
+ title: raw.title,
878
+ ...typeof raw.body === "string" && raw.body !== "" ? { body: raw.body } : {},
879
+ createdAt: typeof raw.createdAt === "string" ? raw.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
880
+ read: raw.read === true
881
+ });
882
+ }
883
+ return rows.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt)).slice(0, 20);
884
+ } catch {
885
+ return [];
886
+ }
887
+ }
888
+ /** One notification card shared by every source in the notification tab. */
889
+ function NotificationCard({ title, body, time, unread, onClick }) {
864
890
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
865
891
  type: "button",
866
- onClick: onOpen,
892
+ onClick,
893
+ disabled: onClick === void 0,
867
894
  style: {
868
895
  display: "flex",
869
- alignItems: "center",
870
- gap: 8,
896
+ alignItems: "flex-start",
897
+ gap: 10,
871
898
  width: "100%",
872
- padding: "9px 12px",
899
+ padding: "10px 12px",
873
900
  border: "1px solid " + colors.line,
874
901
  borderRadius: 8,
875
902
  background: colors.panelAlt,
876
903
  color: colors.text,
877
904
  fontSize: 12,
878
905
  textAlign: "left",
879
- cursor: "pointer"
906
+ cursor: onClick === void 0 ? "default" : "pointer"
880
907
  },
881
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
882
- style: { flex: 1 },
883
- children: [
884
- "技能广场 · ",
885
- count,
886
- " 个待审核技能"
887
- ]
888
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
889
- style: {
890
- color: colors.muted,
891
- fontSize: 11
892
- },
893
- children: "打开技能中心 →"
894
- })]
908
+ children: [
909
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
910
+ flex: "0 0 auto",
911
+ width: 6,
912
+ height: 6,
913
+ marginTop: 6,
914
+ borderRadius: 99,
915
+ background: unread === true ? colors.accent : "transparent"
916
+ } }),
917
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
918
+ style: {
919
+ flex: 1,
920
+ minWidth: 0
921
+ },
922
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
923
+ style: {
924
+ display: "block",
925
+ fontSize: 12,
926
+ fontWeight: 500,
927
+ overflow: "hidden",
928
+ textOverflow: "ellipsis",
929
+ whiteSpace: "nowrap"
930
+ },
931
+ children: title
932
+ }), body === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
933
+ style: {
934
+ display: "block",
935
+ marginTop: 2,
936
+ fontSize: 11,
937
+ color: colors.muted
938
+ },
939
+ children: body
940
+ })]
941
+ }),
942
+ time === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
943
+ style: {
944
+ fontSize: 10,
945
+ color: colors.muted,
946
+ whiteSpace: "nowrap"
947
+ },
948
+ children: time
949
+ })
950
+ ]
895
951
  });
896
952
  }
897
- /** The "notifications" tab: the KB task queue, read from the shared key. */
898
- function KbNotificationPanel({ tasks, onClear }) {
899
- const done = tasks.filter((task) => task.stage === "done" || task.stage === "uploaded").length;
953
+ /** How many rows one notification group shows before it pages. */
954
+ const NOTIFICATION_PAGE_SIZE = 10;
955
+ /** Names the reader hid from the review group; clearing is recoverable, so it outlives a reload. */
956
+ const HIDDEN_SKILLS_KEY = "dataops-task-bus-hidden-skills-v1";
957
+ /** Ids the reader cleared from the audit group, so a reload does not bring them back. */
958
+ const CLEARED_NOTIFICATIONS_KEY = "dataops-task-bus-cleared-notifications-v1";
959
+ /** Read a string set the browser persisted; an unreadable value is an empty set. */
960
+ function readStoredSet(key) {
961
+ try {
962
+ const raw = window.localStorage.getItem(key);
963
+ if (raw === null || raw === "") return /* @__PURE__ */ new Set();
964
+ const parsed = JSON.parse(raw);
965
+ return new Set(Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : []);
966
+ } catch {
967
+ return /* @__PURE__ */ new Set();
968
+ }
969
+ }
970
+ /** Persist one string set; storage failures leave the in-memory set authoritative. */
971
+ function writeStoredSet(key, values) {
972
+ try {
973
+ window.localStorage.setItem(key, JSON.stringify([...values]));
974
+ } catch {}
975
+ }
976
+ /** Slice one page out of a list and report the page count a pager needs. */
977
+ function pageOf(items, page) {
978
+ const pages = Math.max(1, Math.ceil(items.length / NOTIFICATION_PAGE_SIZE));
979
+ const current = Math.min(Math.max(0, page), pages - 1);
980
+ return {
981
+ rows: items.slice(current * NOTIFICATION_PAGE_SIZE, (current + 1) * NOTIFICATION_PAGE_SIZE),
982
+ pages,
983
+ page: current
984
+ };
985
+ }
986
+ /** `上一页 / 第 x/y 页 / 下一页`, hidden while a group fits on one page. */
987
+ function NotificationPager({ page, pages, onChange }) {
988
+ if (pages <= 1) return null;
900
989
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
901
990
  style: {
902
991
  display: "flex",
903
- flexDirection: "column",
904
- gap: 10
992
+ alignItems: "center",
993
+ justifyContent: "flex-end",
994
+ gap: 8
995
+ },
996
+ children: [
997
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
998
+ size: "sm",
999
+ variant: "ghost",
1000
+ disabled: page <= 0,
1001
+ onClick: () => onChange(page - 1),
1002
+ children: "上一页"
1003
+ }),
1004
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1005
+ style: {
1006
+ fontSize: 12,
1007
+ color: colors.muted,
1008
+ fontVariantNumeric: "tabular-nums"
1009
+ },
1010
+ children: [
1011
+ "第 ",
1012
+ page + 1,
1013
+ "/",
1014
+ pages,
1015
+ " 页"
1016
+ ]
1017
+ }),
1018
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1019
+ size: "sm",
1020
+ variant: "ghost",
1021
+ disabled: page >= pages - 1,
1022
+ onClick: () => onChange(page + 1),
1023
+ children: "下一页"
1024
+ })
1025
+ ]
1026
+ });
1027
+ }
1028
+ /** The header every group shares: title, total, current page, and its own clear action. */
1029
+ function NotificationGroupHeader({ title, total, pages, page, clearLabel, onClear, restore, testId }) {
1030
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1031
+ "data-testid": testId,
1032
+ style: {
1033
+ display: "flex",
1034
+ alignItems: "center",
1035
+ justifyContent: "space-between",
1036
+ gap: 8
905
1037
  },
906
1038
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
907
1039
  style: {
908
1040
  display: "flex",
909
- alignItems: "center",
910
- justifyContent: "space-between",
911
- gap: 8
1041
+ alignItems: "baseline",
1042
+ gap: 8,
1043
+ minWidth: 0
912
1044
  },
913
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1045
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
914
1046
  style: {
915
- display: "flex",
916
- alignItems: "baseline",
917
- gap: 8
1047
+ fontSize: 13,
1048
+ fontWeight: 600,
1049
+ color: colors.text
918
1050
  },
919
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
920
- style: {
921
- fontSize: 13,
922
- fontWeight: 600,
923
- color: colors.text
924
- },
925
- children: "任务队列"
926
- }), tasks.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
927
- style: {
928
- fontSize: 12,
929
- color: "#8ea6c6"
930
- },
931
- children: [
932
- done,
933
- "/",
934
- tasks.length
935
- ]
936
- }) : null]
937
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1051
+ children: title
1052
+ }), total > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1053
+ style: {
1054
+ fontSize: 12,
1055
+ color: "#8ea6c6",
1056
+ fontVariantNumeric: "tabular-nums"
1057
+ },
1058
+ children: [
1059
+ total,
1060
+ " 条",
1061
+ pages > 1 ? ` · 第 ${page + 1}/${pages} 页` : ""
1062
+ ]
1063
+ }) : null]
1064
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1065
+ style: {
1066
+ display: "flex",
1067
+ alignItems: "center",
1068
+ gap: 6
1069
+ },
1070
+ children: [restore === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
938
1071
  size: "sm",
939
1072
  variant: "ghost",
940
- disabled: tasks.length === 0,
1073
+ onClick: restore.onClick,
1074
+ children: restore.label
1075
+ }), clearLabel === void 0 || onClear === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1076
+ size: "sm",
1077
+ variant: "ghost",
1078
+ "data-testid": testId === void 0 ? void 0 : testId + "-clear",
1079
+ disabled: total === 0,
941
1080
  onClick: onClear,
942
- children: "清空记录"
1081
+ children: clearLabel
943
1082
  })]
944
- }), tasks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
945
- style: {
946
- padding: 18,
947
- textAlign: "center",
948
- color: colors.muted,
949
- fontSize: 12
950
- },
951
- children: "暂无通知"
952
- }) : tasks.map((task) => {
953
- const percent = Math.max(0, Math.min(100, Math.round(task.progress ?? (task.stage === "done" ? 100 : task.stage === "uploaded" ? 25 : 0))));
954
- const tone = task.stage === "failed" ? colors.danger : task.stage === "done" || task.stage === "uploaded" ? colors.success : colors.accent;
955
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1083
+ })]
1084
+ });
1085
+ }
1086
+ /** The "notifications" tab: every source rendered as the same notification card, paged per source. */
1087
+ function KbNotificationPanel({ tasks, skillRows, reviewItems, onOpenSkill, onClear }) {
1088
+ const [auditPage, setAuditPage] = (0, react.useState)(0);
1089
+ const [skillPage, setSkillPage] = (0, react.useState)(0);
1090
+ const [kbPage, setKbPage] = (0, react.useState)(0);
1091
+ const [hiddenSkills, setHiddenSkills] = (0, react.useState)(() => readStoredSet(HIDDEN_SKILLS_KEY));
1092
+ const [clearedIds, setClearedIds] = (0, react.useState)(() => readStoredSet(CLEARED_NOTIFICATIONS_KEY));
1093
+ const audits = reviewItems.filter((item) => !clearedIds.has(item.id));
1094
+ const skills = skillRows.filter((name) => !hiddenSkills.has(name));
1095
+ const audit = pageOf(audits, auditPage);
1096
+ const skill = pageOf(skills, skillPage);
1097
+ const kb = pageOf(tasks, kbPage);
1098
+ const done = tasks.filter((task) => task.stage === "done" || task.stage === "uploaded").length;
1099
+ const timeOf = (value) => {
1100
+ const date = new Date(value);
1101
+ if (Number.isNaN(date.getTime())) return "";
1102
+ return date.toLocaleString("zh-CN", {
1103
+ month: "2-digit",
1104
+ day: "2-digit",
1105
+ hour: "2-digit",
1106
+ minute: "2-digit"
1107
+ });
1108
+ };
1109
+ /** Mark every audit row read on the host, then keep them out of the list so clearing is visible. */
1110
+ const clearAudits = () => {
1111
+ const ids = new Set(clearedIds);
1112
+ for (const item of reviewItems) ids.add(item.id);
1113
+ setClearedIds(ids);
1114
+ writeStoredSet(CLEARED_NOTIFICATIONS_KEY, ids);
1115
+ fetch("integrations/dataops/review-notifications", {
1116
+ method: "POST",
1117
+ headers: { "content-type": "application/json" },
1118
+ body: JSON.stringify({ all: true })
1119
+ }).catch(() => {});
1120
+ };
1121
+ /** Hide the pending skills; the header keeps a 恢复 entry so nothing is lost. */
1122
+ const clearSkills = () => {
1123
+ const names = new Set(hiddenSkills);
1124
+ for (const name of skillRows) names.add(name);
1125
+ setHiddenSkills(names);
1126
+ writeStoredSet(HIDDEN_SKILLS_KEY, names);
1127
+ };
1128
+ const restoreSkills = () => {
1129
+ setHiddenSkills(/* @__PURE__ */ new Set());
1130
+ writeStoredSet(HIDDEN_SKILLS_KEY, []);
1131
+ setSkillPage(0);
1132
+ };
1133
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1134
+ style: {
1135
+ display: "flex",
1136
+ flexDirection: "column",
1137
+ gap: 14
1138
+ },
1139
+ children: [
1140
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
956
1141
  style: {
957
- padding: "10px 12px",
958
- border: "1px solid " + colors.line,
959
- borderRadius: 8,
960
- background: colors.panelAlt,
961
1142
  display: "flex",
962
1143
  flexDirection: "column",
963
- gap: 6
1144
+ gap: 8
964
1145
  },
965
1146
  children: [
966
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1147
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationGroupHeader, {
1148
+ title: "审核通知",
1149
+ testId: "notify-audit",
1150
+ total: audits.length,
1151
+ pages: audit.pages,
1152
+ page: audit.page,
1153
+ clearLabel: "清空",
1154
+ onClear: clearAudits
1155
+ }),
1156
+ audits.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
967
1157
  style: {
968
- display: "flex",
969
- alignItems: "center",
970
- gap: 8
1158
+ padding: 14,
1159
+ textAlign: "center",
1160
+ color: colors.muted,
1161
+ fontSize: 12
1162
+ },
1163
+ children: "暂无审核通知"
1164
+ }) : audit.rows.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationCard, {
1165
+ title: item.title,
1166
+ body: item.body,
1167
+ time: timeOf(item.createdAt),
1168
+ unread: !item.read,
1169
+ onClick: onOpenSkill
1170
+ }, item.id)),
1171
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationPager, {
1172
+ page: audit.page,
1173
+ pages: audit.pages,
1174
+ onChange: setAuditPage
1175
+ })
1176
+ ]
1177
+ }),
1178
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1179
+ style: {
1180
+ display: "flex",
1181
+ flexDirection: "column",
1182
+ gap: 8
1183
+ },
1184
+ children: [
1185
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationGroupHeader, {
1186
+ title: "技能广场待审核",
1187
+ testId: "notify-skills",
1188
+ total: skills.length,
1189
+ pages: skill.pages,
1190
+ page: skill.page,
1191
+ clearLabel: "清空",
1192
+ onClear: clearSkills,
1193
+ ...hiddenSkills.size > 0 ? { restore: {
1194
+ label: `恢复隐藏的 ${hiddenSkills.size} 条`,
1195
+ onClick: restoreSkills
1196
+ } } : {}
1197
+ }),
1198
+ skills.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1199
+ style: {
1200
+ padding: 14,
1201
+ textAlign: "center",
1202
+ color: colors.muted,
1203
+ fontSize: 12
1204
+ },
1205
+ children: "暂无待审核技能"
1206
+ }) : skill.rows.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationCard, {
1207
+ title: "技能「" + name + "」等待审核",
1208
+ body: "提交到技能广场,等待管理员审核",
1209
+ unread: true,
1210
+ onClick: onOpenSkill
1211
+ }, name)),
1212
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationPager, {
1213
+ page: skill.page,
1214
+ pages: skill.pages,
1215
+ onChange: setSkillPage
1216
+ })
1217
+ ]
1218
+ }),
1219
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1220
+ style: {
1221
+ display: "flex",
1222
+ flexDirection: "column",
1223
+ gap: 8
1224
+ },
1225
+ children: [
1226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationGroupHeader, {
1227
+ title: "知识库任务",
1228
+ testId: "notify-kb",
1229
+ total: tasks.length,
1230
+ pages: kb.pages,
1231
+ page: kb.page,
1232
+ clearLabel: "清空记录",
1233
+ onClear
1234
+ }),
1235
+ tasks.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1236
+ style: {
1237
+ fontSize: 12,
1238
+ color: colors.muted,
1239
+ fontVariantNumeric: "tabular-nums"
971
1240
  },
972
1241
  children: [
973
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
974
- style: {
975
- flex: 1,
976
- minWidth: 0,
977
- overflow: "hidden",
978
- textOverflow: "ellipsis",
979
- whiteSpace: "nowrap",
980
- fontSize: 12,
981
- fontWeight: 500,
982
- color: colors.text
983
- },
984
- children: task.fileName
985
- }),
986
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
987
- style: {
988
- fontSize: 10,
989
- color: colors.muted
990
- },
991
- children: new Date(task.startedAt).toLocaleTimeString("zh-CN", {
992
- hour: "2-digit",
993
- minute: "2-digit"
994
- })
995
- }),
996
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
997
- style: {
998
- fontSize: 10,
999
- color: tone,
1000
- whiteSpace: "nowrap"
1001
- },
1002
- children: KB_STAGE_LABEL[task.stage]
1003
- })
1242
+ "已完成 ",
1243
+ done,
1244
+ "/",
1245
+ tasks.length
1004
1246
  ]
1005
- }),
1006
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1247
+ }) : null,
1248
+ tasks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1007
1249
  style: {
1008
- display: "flex",
1009
- alignItems: "center",
1010
- gap: 8
1250
+ padding: 14,
1251
+ textAlign: "center",
1252
+ color: colors.muted,
1253
+ fontSize: 12
1011
1254
  },
1012
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1013
- style: {
1014
- flex: 1,
1015
- height: 6,
1016
- borderRadius: 999,
1017
- background: "rgba(255,255,255,.08)",
1018
- overflow: "hidden"
1019
- },
1020
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
1021
- width: String(percent) + "%",
1022
- height: "100%",
1023
- borderRadius: 999,
1024
- background: tone,
1025
- transition: "width 120ms linear"
1026
- } })
1027
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1255
+ children: "暂无提取任务"
1256
+ }) : kb.rows.map((task) => {
1257
+ const percent = Math.max(0, Math.min(100, Math.round(task.progress ?? (task.stage === "done" ? 100 : task.stage === "uploaded" ? 100 : 0))));
1258
+ const tone = task.stage === "failed" ? colors.danger : task.stage === "done" || task.stage === "uploaded" ? colors.success : colors.accent;
1259
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1028
1260
  style: {
1029
- minWidth: 34,
1030
- textAlign: "right",
1031
- fontSize: 11,
1032
- color: colors.muted,
1033
- fontVariantNumeric: "tabular-nums"
1261
+ padding: "10px 12px",
1262
+ border: "1px solid " + colors.line,
1263
+ borderRadius: 8,
1264
+ background: colors.panelAlt
1034
1265
  },
1035
- children: String(percent) + "%"
1036
- })]
1266
+ children: [
1267
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1268
+ style: {
1269
+ display: "flex",
1270
+ alignItems: "center",
1271
+ gap: 8
1272
+ },
1273
+ children: [
1274
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1275
+ style: {
1276
+ flex: 1,
1277
+ minWidth: 0,
1278
+ overflow: "hidden",
1279
+ textOverflow: "ellipsis",
1280
+ whiteSpace: "nowrap",
1281
+ fontSize: 12,
1282
+ fontWeight: 600,
1283
+ color: colors.text
1284
+ },
1285
+ children: task.fileName
1286
+ }),
1287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1288
+ style: {
1289
+ fontSize: 10,
1290
+ color: colors.muted
1291
+ },
1292
+ children: new Date(task.startedAt).toLocaleTimeString("zh-CN", {
1293
+ hour: "2-digit",
1294
+ minute: "2-digit"
1295
+ })
1296
+ }),
1297
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1298
+ style: {
1299
+ fontSize: 10,
1300
+ color: tone,
1301
+ whiteSpace: "nowrap"
1302
+ },
1303
+ children: KB_STAGE_LABEL[task.stage]
1304
+ })
1305
+ ]
1306
+ }),
1307
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1308
+ style: {
1309
+ display: "flex",
1310
+ alignItems: "center",
1311
+ gap: 8
1312
+ },
1313
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1314
+ style: {
1315
+ flex: 1,
1316
+ height: 6,
1317
+ borderRadius: 999,
1318
+ background: "rgba(255,255,255,.08)",
1319
+ overflow: "hidden"
1320
+ },
1321
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
1322
+ width: String(percent) + "%",
1323
+ height: "100%",
1324
+ borderRadius: 999,
1325
+ background: tone,
1326
+ transition: "width 120ms ease"
1327
+ } })
1328
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1329
+ style: {
1330
+ minWidth: 34,
1331
+ textAlign: "right",
1332
+ fontSize: 11,
1333
+ color: colors.muted,
1334
+ fontVariantNumeric: "tabular-nums"
1335
+ },
1336
+ children: String(percent) + "%"
1337
+ })]
1338
+ }),
1339
+ task.error !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1340
+ style: {
1341
+ fontSize: 11,
1342
+ color: colors.danger
1343
+ },
1344
+ children: task.error
1345
+ }) : null
1346
+ ]
1347
+ }, task.id);
1037
1348
  }),
1038
- task.error !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1039
- style: {
1040
- fontSize: 11,
1041
- color: colors.danger
1042
- },
1043
- children: task.error
1044
- }) : null
1349
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationPager, {
1350
+ page: kb.page,
1351
+ pages: kb.pages,
1352
+ onChange: setKbPage
1353
+ })
1045
1354
  ]
1046
- }, task.id);
1047
- })]
1355
+ })
1356
+ ]
1048
1357
  });
1049
1358
  }
1050
1359
  const TaskCenterButton = ({ openSession }) => {
@@ -1059,7 +1368,8 @@ window.__ModuleLoader__.load({
1059
1368
  const [anchor, setAnchor] = (0, react.useState)(null);
1060
1369
  const [pendingCount, setPendingCount] = (0, react.useState)(0);
1061
1370
  const [kbTasks, setKbTasks] = (0, react.useState)(() => readKbTasks());
1062
- const [skillPending, setSkillPending] = (0, react.useState)(0);
1371
+ const [skillRows, setSkillRows] = (0, react.useState)([]);
1372
+ const [reviewInbox, setReviewInbox] = (0, react.useState)([]);
1063
1373
  (0, react.useEffect)(() => {
1064
1374
  if (document.getElementById(TASK_BUS_STYLE_ID) !== null) return;
1065
1375
  const style = document.createElement("style");
@@ -1160,8 +1470,11 @@ window.__ModuleLoader__.load({
1160
1470
  (0, react.useEffect)(() => {
1161
1471
  let alive = true;
1162
1472
  const sync = () => {
1163
- readSkillPending().then((count) => {
1164
- if (alive) setSkillPending(count);
1473
+ readSkillPlazaRows().then((rows) => {
1474
+ if (alive) setSkillRows(rows);
1475
+ });
1476
+ readReviewInbox().then((items) => {
1477
+ if (alive) setReviewInbox(items);
1165
1478
  });
1166
1479
  };
1167
1480
  sync();
@@ -1321,7 +1634,7 @@ window.__ModuleLoader__.load({
1321
1634
  strokeLinejoin: "round",
1322
1635
  "aria-hidden": "true",
1323
1636
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.7 21a2 2 0 0 1-3.4 0" })]
1324
- }), pendingCount + kbPending + skillPending > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1637
+ }), pendingCount + kbPending + skillRows.length + reviewInbox.filter((item) => !item.read).length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1325
1638
  style: {
1326
1639
  position: "absolute",
1327
1640
  bottom: -2,
@@ -1337,7 +1650,7 @@ window.__ModuleLoader__.load({
1337
1650
  lineHeight: "14px",
1338
1651
  textAlign: "center"
1339
1652
  },
1340
- children: pendingCount + kbPending + skillPending > 99 ? "99+" : pendingCount + kbPending + skillPending
1653
+ children: pendingCount + kbPending + skillRows.length + reviewInbox.filter((item) => !item.read).length > 99 ? "99+" : pendingCount + kbPending + skillRows.length + reviewInbox.filter((item) => !item.read).length
1341
1654
  }) : null]
1342
1655
  }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1343
1656
  open,
@@ -1433,13 +1746,13 @@ window.__ModuleLoader__.load({
1433
1746
  overflowY: "auto",
1434
1747
  flex: 1
1435
1748
  },
1436
- children: tab === "notifications" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [skillPending > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillPendingRow, {
1437
- count: skillPending,
1438
- onOpen: openSkillCenter
1439
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KbNotificationPanel, {
1749
+ children: tab === "notifications" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KbNotificationPanel, {
1440
1750
  tasks: kbTasks,
1751
+ skillRows,
1752
+ reviewItems: reviewInbox,
1753
+ onOpenSkill: openSkillCenter,
1441
1754
  onClear: clearKbTasks
1442
- })] }) : selected !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskDetail, {
1755
+ }) : selected !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskDetail, {
1443
1756
  task: selected,
1444
1757
  onBack: () => setSelected(null)
1445
1758
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [