dsh-advisor-group 0.1.0 → 0.1.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/lib/client.js CHANGED
@@ -290,6 +290,20 @@ function authHeaders() {
290
290
  const token = advisorGroupToken();
291
291
  return token ? { "x-advisor-group-token": token } : {};
292
292
  }
293
+ /**
294
+ * Browser-side twin of `sanitizeAdvisorContent` (host module cannot be
295
+ * imported by the client bundle). The SSE live overlay streams the RAW model
296
+ * output, whose length usually EXCEEDS the sanitized durable message — so the
297
+ * merge below must sanitize whichever content it picks or the raw tool-call
298
+ * markup would keep rendering until the overlay is gone.
299
+ */
300
+ const CLIENT_RAW_TOOL_CALL_BLOCK = /<(?:[||]{1,2})?(?:tool_calls|DSML)[\s\S]*?<\/(?:[||]{1,2})?(?:tool_calls|DSML)\s*>/g;
301
+ const CLIENT_RAW_TOOL_CALL_TAIL = /<(?:[||]{1,2})?(?:tool_calls|DSML)[\s\S]{0,60}?(?:<invoke|<parameter|<[||]{1,2}DSML)[\s\S]*$/g;
302
+ const CLIENT_EMPTY_BODY_NOTICE = "(顾问未生成正文:输出仅为工具调用描述,请参考其思维链与工具调用记录。)";
303
+ function sanitizeClientContent(content) {
304
+ if (!content.trim()) return content;
305
+ return content.replace(CLIENT_RAW_TOOL_CALL_BLOCK, "").replace(CLIENT_RAW_TOOL_CALL_TAIL, "").trim() || CLIENT_EMPTY_BODY_NOTICE;
306
+ }
293
307
  function locationOf(context) {
294
308
  return context.start?.location ?? context.matches[0]?.location ?? { kind: "unresolved" };
295
309
  }
@@ -312,7 +326,7 @@ const advisorGroupDefinition = {
312
326
  id: event.data.sessionId,
313
327
  role: "start"
314
328
  };
315
- if (event.type === "advisor-group/message" || event.type === "advisor-group/delta" || event.type === "advisor-group/end") return {
329
+ if (event.type === "advisor-group/message" || event.type === "advisor-group/delta" || event.type === "advisor-group/end" || event.type === "advisor-group/resume") return {
316
330
  id: event.data.sessionId,
317
331
  role: "update"
318
332
  };
@@ -385,9 +399,13 @@ const advisorGroupDefinition = {
385
399
  }
386
400
  if (match.event.type === "advisor-group/end") return {
387
401
  ...context.state,
388
- status: "completed",
402
+ status: match.event.data.summary.stopped ? "cancelled" : "completed",
389
403
  summary: match.event.data.summary
390
404
  };
405
+ if (match.event.type === "advisor-group/resume") return {
406
+ ...context.state,
407
+ status: "running"
408
+ };
391
409
  return context.state;
392
410
  },
393
411
  publication: (match) => match.event.type === "advisor-group/message" || match.event.type === "advisor-group/delta" ? "animation-frame" : "immediate",
@@ -465,13 +483,6 @@ const headerStyle = {
465
483
  letterSpacing: 1,
466
484
  fontSize: 11
467
485
  };
468
- const questionStyle = {
469
- color: "#d9f99d",
470
- fontWeight: 700,
471
- whiteSpace: "pre-wrap",
472
- wordBreak: "break-word",
473
- marginBottom: 6
474
- };
475
486
  const advisorsLineStyle = {
476
487
  fontSize: 11,
477
488
  color: "#4ade80",
@@ -507,70 +518,278 @@ const waitingStyle = {
507
518
  color: "#fbbf24",
508
519
  marginTop: 6
509
520
  };
510
- function MessageBubble({ message }) {
511
- const [thinkingCollapsed, setThinkingCollapsed] = (0, react.useState)(false);
512
- const thinkingPanelRef = (0, react.useRef)(null);
513
- (0, react.useEffect)(() => {
514
- if (!thinkingCollapsed && thinkingPanelRef.current) thinkingPanelRef.current.scrollTop = thinkingPanelRef.current.scrollHeight;
515
- }, [message.thinking, thinkingCollapsed]);
521
+ const noticeStyle = {
522
+ color: "#fda4af",
523
+ marginTop: 6
524
+ };
525
+ /** Internal announcements that must never reach the user (the tool list). */
526
+ const INTERNAL_TOOL_STEP_NAMES = /* @__PURE__ */ new Set(["⚙️ tools", "⚙ tools"]);
527
+ /** True when the tool result represents a failure — the wrapper prefix, or a
528
+ * JSON payload carrying `error`/`detail`/HTTP error code (unconfigured/401
529
+ * tools), robust to nested objects before the error key. */
530
+ function isFailedToolResult(text) {
531
+ if (text.startsWith("(工具执行失败")) return true;
532
+ try {
533
+ const parsed = JSON.parse(text);
534
+ if (parsed && (parsed.error !== void 0 || parsed.detail !== void 0)) return true;
535
+ if (parsed && typeof parsed.code === "number" && parsed.code >= 400 && parsed.code <= 599) return true;
536
+ } catch {}
537
+ return false;
538
+ }
539
+ /**
540
+ * Pair call/result tool steps into one `ToolStepView` per invocation, in
541
+ * stream order, and drop internal announcements (「本次可用」).
542
+ */
543
+ function buildToolStepViews(steps) {
544
+ const out = [];
545
+ let last;
546
+ for (const step of steps ?? []) {
547
+ if (INTERNAL_TOOL_STEP_NAMES.has(step.name)) continue;
548
+ if (step.kind === "call") {
549
+ last = {
550
+ name: step.name,
551
+ input: step.text,
552
+ atMs: step.atMs
553
+ };
554
+ out.push(last);
555
+ } else if (step.kind === "result") {
556
+ if (last && last.output === void 0 && last.input !== void 0) {
557
+ last.output = step.text;
558
+ if (isFailedToolResult(step.text)) last.failed = true;
559
+ } else out.push({
560
+ name: step.name,
561
+ output: step.text,
562
+ atMs: step.atMs,
563
+ ...isFailedToolResult(step.text) ? { failed: true } : {}
564
+ });
565
+ }
566
+ }
567
+ return out.filter((view) => view.input !== void 0 || view.output !== void 0);
568
+ }
569
+ const stepRowShellStyle = {
570
+ border: "1px solid rgba(34,197,94,0.28)",
571
+ borderRadius: 3,
572
+ margin: "5px 0",
573
+ background: "rgba(34,197,94,0.05)",
574
+ overflow: "hidden"
575
+ };
576
+ const stepRowHeaderStyle = (color) => ({
577
+ width: "100%",
578
+ display: "flex",
579
+ alignItems: "center",
580
+ gap: 8,
581
+ cursor: "pointer",
582
+ background: "transparent",
583
+ border: "none",
584
+ color,
585
+ fontSize: 12,
586
+ textAlign: "left",
587
+ padding: "4px 8px"
588
+ });
589
+ const stepRowBodyStyle = {
590
+ borderTop: "1px dashed rgba(47,158,68,0.45)",
591
+ padding: "5px 8px"
592
+ };
593
+ function StepRow({ title, badges, defaultOpen, color = "#4ade80", body }) {
594
+ const [open, setOpen] = (0, react.useState)(defaultOpen);
595
+ return (0, react.createElement)("div", { style: stepRowShellStyle }, (0, react.createElement)("button", {
596
+ type: "button",
597
+ onClick: () => setOpen((value) => !value),
598
+ style: stepRowHeaderStyle(color)
599
+ }, (0, react.createElement)("span", null, open ? "▾" : "▸"), (0, react.createElement)("span", { style: { fontWeight: 700 } }, title), ...badges ?? []), open ? (0, react.createElement)("div", { style: stepRowBodyStyle }, body) : null);
600
+ }
601
+ /**
602
+ * Inline thinking annotation: an UNFRAMED block that sits directly above the
603
+ * tool row it led to (each step reads: 💭 思考文本 → ⛭ 工具), with the full
604
+ * segment visible (scrollable) — not a separate bordered card.
605
+ */
606
+ const thinkingInlineStyle = {
607
+ margin: "4px 0 2px",
608
+ padding: "4px 8px",
609
+ borderLeft: "2px solid rgba(251,191,36,0.45)",
610
+ background: "rgba(217,119,6,0.04)"
611
+ };
612
+ const thinkingTagStyle = {
613
+ color: "#fbbf24",
614
+ fontSize: 11,
615
+ marginBottom: 2,
616
+ fontWeight: 700
617
+ };
618
+ const descriptionBodyStyle = {
619
+ whiteSpace: "pre-wrap",
620
+ wordBreak: "break-word",
621
+ color: "#d9f99d",
622
+ fontSize: 12,
623
+ padding: "2px 6px 4px"
624
+ };
625
+ /**
626
+ * Short "next-action narrative" of a thinking segment — the visible 📋 行动·N
627
+ * block shown directly after the 💭 思考·N panel and before the ⛭ tool row.
628
+ * Standard-agent style: pick the FIRST SHORT sentence that says what the model
629
+ * is about to do (action markers like 我先/接下来/我需要…), else the first
630
+ * non-generic short sentence; hard-cap at ~140 chars. The long internal
631
+ * monologue stays in the thinking panel.
632
+ */
633
+ function extractActionDescription(segment) {
634
+ const text = segment.trim();
635
+ if (!text) return "";
636
+ const sentences = (text.match(/[^。!?!?]+[。!?!?]?/g) ?? []).map((sentence) => sentence.replace(/\s+/g, " ").trim()).filter(Boolean);
637
+ const cut = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value;
638
+ const ACTION = /(我先|接下来|下一步|下面|准备|打算|我需要|先做|先看|先检查|先核对|先核实|先验证|先确认|先读|先搜索|先检索|先找|计划|步骤|让我先|我会|需要先|先补|先抓|先取|先查|先分析|先整理|先修改|先跟踪)/;
639
+ const GENERIC = /^(让我|我明白|好的|嗯|OK|Okay|我是|我是一位|我的角色|Let me|Sure|The user|用户)/;
640
+ for (const sentence of sentences.slice(0, 5)) if (sentence.length <= 160 && ACTION.test(sentence)) return cut(sentence, 140);
641
+ for (const sentence of sentences.slice(0, 3)) if (sentence.length <= 120 && !GENERIC.test(sentence)) return cut(sentence, 120);
642
+ return cut(sentences[0] ?? text, 100);
643
+ }
644
+ const thinkingBodyStyle = {
645
+ maxHeight: 200,
646
+ overflowY: "auto",
647
+ whiteSpace: "pre-wrap",
648
+ wordBreak: "break-word",
649
+ color: "#fde68a",
650
+ fontSize: 12,
651
+ padding: "4px 6px"
652
+ };
653
+ const toolBodyStyle = {
654
+ fontSize: 12,
655
+ color: "#38bdf8"
656
+ };
657
+ const toolIoTitleStyle = {
658
+ color: "#86efac",
659
+ fontWeight: 700,
660
+ margin: "2px 0 2px"
661
+ };
662
+ const toolIoBodyStyle = {
663
+ whiteSpace: "pre-wrap",
664
+ wordBreak: "break-word",
665
+ color: "var(--dsh-color-muted, #8b90a0)",
666
+ marginBottom: 6
667
+ };
668
+ const badgeStyle = (color) => ({
669
+ color,
670
+ fontSize: 11,
671
+ opacity: .95
672
+ });
673
+ /**
674
+ * Interleave action announcements, thinking segments and tool views in the
675
+ * official DSH block order (text → reasoning → tool-call), then the body.
676
+ * Each round contributes at most one 行动 / 思考 / 工具 item; missing actions
677
+ * are derived from the thinking segment so pre-actionDescriptions logs still
678
+ * render sensibly.
679
+ */
680
+ function buildStepSequence(actionDescriptions, thinkingSegments, toolViews) {
681
+ const items = [];
682
+ const count = Math.max(actionDescriptions.length, thinkingSegments.length, toolViews.length);
683
+ for (let index = 0; index < count; index += 1) {
684
+ const step = index + 1;
685
+ const described = actionDescriptions[index]?.trim();
686
+ const segment = thinkingSegments[index];
687
+ const action = described || (segment !== void 0 && segment.trim() ? extractActionDescription(segment) : "");
688
+ if (action) items.push({
689
+ kind: "action",
690
+ text: action,
691
+ step
692
+ });
693
+ if (segment !== void 0 && segment.trim()) items.push({
694
+ kind: "thinking",
695
+ segment,
696
+ step
697
+ });
698
+ const view = toolViews[index];
699
+ if (view !== void 0) items.push({
700
+ kind: "tool",
701
+ view
702
+ });
703
+ }
704
+ items.push({ kind: "body" });
705
+ return items;
706
+ }
707
+ /**
708
+ * Render one consultation message as a stack of STEP rows (one row per step,
709
+ * appearing in stream order, each independently expandable):
710
+ * 💭 思考·1 → ⛭ 工具×1(输入/输出)→ 💭 思考·2 → ⛭ 工具×2 → … → 📄 正文
711
+ * Main/system messages stay a single collapsible row.
712
+ */
713
+ function AdvisorSteps({ message }) {
516
714
  const isMain = message.role === "main";
517
715
  const isSystem = message.role === "system";
518
- const justify = isMain ? "flex-end" : "flex-start";
519
- const background = isSystem ? "rgba(217,119,6,0.08)" : isMain ? "rgba(34,197,94,0.14)" : "rgba(34,197,94,0.06)";
520
- const borderColor = isSystem ? "#b45309" : "#16a34a";
521
716
  const label = isMain ? "YOU" : isSystem ? "SYSTEM" : message.advisorName ?? message.advisorId ?? "ADVISOR";
522
- const hasThinking = Boolean(message.thinking && message.thinking.length > 0);
523
- return (0, react.createElement)("div", { style: {
524
- display: "flex",
525
- justifyContent: justify,
526
- margin: "4px 0"
527
- } }, (0, react.createElement)("div", { style: {
528
- maxWidth: "92%",
529
- padding: "4px 8px",
530
- border: `1px solid ${borderColor}`,
531
- borderRadius: 3,
532
- background
533
- } }, (0, react.createElement)("div", { style: {
534
- fontSize: 11,
535
- color: isSystem ? "#fcd34d" : "#4ade80",
536
- marginBottom: 2
537
- } }, `${label}${message.round ? ` · R${message.round}` : ""}`), hasThinking ? (0, react.createElement)("div", { style: {
538
- border: "1px dashed #a16207",
539
- borderRadius: 3,
540
- marginBottom: 4,
541
- background: "rgba(217,119,6,0.06)"
542
- } }, (0, react.createElement)("button", {
543
- type: "button",
544
- onClick: () => setThinkingCollapsed((value) => !value),
545
- style: {
546
- width: "100%",
547
- cursor: "pointer",
548
- background: "transparent",
549
- border: "none",
550
- color: "#fbbf24",
551
- fontSize: 11,
552
- textAlign: "left",
553
- padding: "3px 6px"
554
- }
555
- }, `💭 ${thinkingCollapsed ? "已思考(点击展开)" : "思维链(点击收起)"} ${thinkingCollapsed ? "▸" : "▾"}`), !thinkingCollapsed ? (0, react.createElement)("div", {
556
- ref: (el) => {
557
- thinkingPanelRef.current = el;
558
- },
559
- style: {
560
- maxHeight: 160,
561
- overflowY: "auto",
717
+ const group = isMain ? "YOU" : isSystem ? "SYSTEM" : `${label} · R${message.round ?? 1}`;
718
+ const truncated = message.truncated;
719
+ const toolViews = buildToolStepViews(message.toolSteps);
720
+ const thinkingSegments = message.thinkingSegments && message.thinkingSegments.length > 0 ? message.thinkingSegments : message.thinking ? [message.thinking] : [];
721
+ const baseColor = isSystem ? "#fcd34d" : "#4ade80";
722
+ if (isMain || isSystem) return (0, react.createElement)(StepRow, {
723
+ title: group,
724
+ defaultOpen: isMain,
725
+ color: baseColor,
726
+ body: (0, react.createElement)("div", { style: {
562
727
  whiteSpace: "pre-wrap",
563
- wordBreak: "break-word",
564
- padding: "4px 6px",
565
- color: "#fde68a",
566
- fontSize: 12,
567
- borderTop: "1px dashed #a16207"
728
+ wordBreak: "break-word"
729
+ } }, message.content)
730
+ });
731
+ const sequence = buildStepSequence(message.actionDescriptions ?? [], thinkingSegments, toolViews);
732
+ const live = message;
733
+ const liveToolPhase = live.livePhase === "tool";
734
+ const liveAction = liveToolPhase && live.liveBody?.trim() ? live.liveBody.trim() : void 0;
735
+ const children = [];
736
+ for (const [index, item] of sequence.entries()) {
737
+ if (item.kind === "action") {
738
+ children.push((0, react.createElement)("div", {
739
+ key: `action-${index}`,
740
+ style: thinkingInlineStyle
741
+ }, (0, react.createElement)("div", { style: thinkingTagStyle }, `${group} 📋 行动·${item.step}`), (0, react.createElement)("div", { style: descriptionBodyStyle }, item.text)));
742
+ continue;
743
+ }
744
+ if (item.kind === "thinking") {
745
+ children.push((0, react.createElement)(StepRow, {
746
+ key: `think-${index}`,
747
+ title: `${group} 💭 思考·${item.step}`,
748
+ badges: [(0, react.createElement)("span", { style: badgeStyle("#fbbf24") }, `${item.segment.length} 字符`)],
749
+ defaultOpen: false,
750
+ color: "#fbbf24",
751
+ body: (0, react.createElement)("div", { style: thinkingBodyStyle }, item.segment)
752
+ }));
753
+ continue;
754
+ }
755
+ if (item.kind === "tool") {
756
+ const view = item.view;
757
+ children.push((0, react.createElement)(StepRow, {
758
+ key: `tool-${index}`,
759
+ title: `${group} ⛭ ${view.name}`,
760
+ badges: [(0, react.createElement)("span", { style: badgeStyle(view.failed ? "#f87171" : "#38bdf8") }, view.output === void 0 ? "调用中…" : view.failed ? "⚠ 失败" : "调用完成")],
761
+ defaultOpen: false,
762
+ color: view.failed ? "#f87171" : "#38bdf8",
763
+ body: (0, react.createElement)("div", null, view.input !== void 0 ? (0, react.createElement)("div", { style: toolBodyStyle }, (0, react.createElement)("div", { style: toolIoTitleStyle }, "输入"), (0, react.createElement)("div", { style: toolIoBodyStyle }, view.input)) : null, view.output !== void 0 ? (0, react.createElement)("div", { style: toolBodyStyle }, (0, react.createElement)("div", { style: toolIoTitleStyle }, "输出"), (0, react.createElement)("div", { style: toolIoBodyStyle }, view.output)) : null)
764
+ }));
765
+ continue;
568
766
  }
569
- }, message.thinking) : null) : null, isMain || isSystem ? (0, react.createElement)("div", { style: {
570
- whiteSpace: "pre-wrap",
571
- wordBreak: "break-word"
572
- } }, message.content) : (0, react.createElement)("div", { style: { wordBreak: "break-word" } }, renderMarkdown(message.content))));
767
+ if (liveToolPhase) continue;
768
+ children.push((0, react.createElement)(StepRow, {
769
+ key: `body-${index}`,
770
+ title: `${group} 📄 正文`,
771
+ badges: truncated ? [(0, react.createElement)("span", { style: badgeStyle("#f87171") }, truncated.reason === "timeout" ? "⏱ 响应超时截断" : "⚠ 流中断")] : [],
772
+ defaultOpen: true,
773
+ color: "#4ade80",
774
+ body: (0, react.createElement)("div", { style: { wordBreak: "break-word" } }, truncated ? (0, react.createElement)("div", { style: {
775
+ fontSize: 11,
776
+ color: "#f87171",
777
+ border: "1px solid #dc2626",
778
+ borderRadius: 3,
779
+ padding: "2px 6px",
780
+ display: "inline-block",
781
+ marginBottom: 3
782
+ } }, truncated.reason === "timeout" ? "⏱ 响应超时截断 — 思考已收到,正文未完成" : "⚠ 流中断 — 正文可能不完整") : null, renderMarkdown(message.content))
783
+ }));
784
+ }
785
+ if (liveAction !== void 0) children.push((0, react.createElement)("div", {
786
+ key: "live-action",
787
+ style: thinkingInlineStyle
788
+ }, (0, react.createElement)("div", { style: thinkingTagStyle }, `${group} 📋 行动·${toolViews.length + 1}`), (0, react.createElement)("div", { style: descriptionBodyStyle }, liveAction)));
789
+ return (0, react.createElement)("div", null, children);
573
790
  }
791
+ /** Auto-hide delay for transient card notices (request failures, resync). */
792
+ const NOTICE_TTL_MS = 6e3;
574
793
  function AdvisorGroupNodeView(props) {
575
794
  const data = props.node.data;
576
795
  const sessionId = data.sessionId;
@@ -578,10 +797,36 @@ function AdvisorGroupNodeView(props) {
578
797
  const [contextCollapsed, setContextCollapsed] = (0, react.useState)(true);
579
798
  const lastEventIdRef = (0, react.useRef)(0);
580
799
  const lastBootIdRef = (0, react.useRef)("");
800
+ const [transientNotice, setTransientNotice] = (0, react.useState)(null);
801
+ const noticeTimerRef = (0, react.useRef)(null);
802
+ const showTransientNotice = (text, kind, ttlMs) => {
803
+ if (noticeTimerRef.current !== null) {
804
+ clearTimeout(noticeTimerRef.current);
805
+ noticeTimerRef.current = null;
806
+ }
807
+ setTransientNotice({
808
+ text,
809
+ kind
810
+ });
811
+ if (ttlMs !== void 0) noticeTimerRef.current = setTimeout(() => {
812
+ noticeTimerRef.current = null;
813
+ setTransientNotice(null);
814
+ }, ttlMs);
815
+ };
816
+ const clearStreamNotice = () => {
817
+ setTransientNotice((prev) => prev && prev.kind === "stream" ? null : prev);
818
+ };
581
819
  (0, react.useEffect)(() => {
582
820
  if (!sessionId) return;
583
821
  const es = new EventSource(`/advisor-group/stream?sessionId=${encodeURIComponent(sessionId)}&lastEventId=${lastEventIdRef.current}&bootId=${encodeURIComponent(lastBootIdRef.current)}&token=${encodeURIComponent(advisorGroupToken())}`);
822
+ es.onopen = () => {
823
+ clearStreamNotice();
824
+ };
825
+ es.onerror = () => {
826
+ showTransientNotice("连接中断,正在重连…", "stream");
827
+ };
584
828
  es.onmessage = (event) => {
829
+ clearStreamNotice();
585
830
  try {
586
831
  const delta = JSON.parse(event.data);
587
832
  const bootId = delta.bootId ?? "";
@@ -593,51 +838,168 @@ function AdvisorGroupNodeView(props) {
593
838
  const eventId = Number(delta.eventId ?? 0);
594
839
  if (eventId > 0 && eventId <= lastEventIdRef.current) return;
595
840
  if (eventId > 0) lastEventIdRef.current = eventId;
841
+ const bucket = `${delta.advisorId}::${delta.round ?? 1}`;
596
842
  setLive((prev) => {
597
- const current = prev[delta.advisorId] ?? {
843
+ const current = prev[bucket] ?? {
598
844
  content: "",
599
- thinking: ""
845
+ thinking: "",
846
+ thinkingSegments: [],
847
+ committedLen: 0,
848
+ contentSegments: [],
849
+ committedContentLen: 0,
850
+ phase: "answer",
851
+ finalized: false,
852
+ toolSteps: []
600
853
  };
854
+ let thinkingSegments = current.thinkingSegments;
855
+ let committedLen = current.committedLen;
856
+ let contentSegments = current.contentSegments;
857
+ let committedContentLen = current.committedContentLen;
858
+ if (delta.toolStep) {
859
+ const segment = current.thinking.slice(committedLen);
860
+ if (segment.trim()) thinkingSegments = [...current.thinkingSegments, segment];
861
+ committedLen = current.thinking.length;
862
+ const contentSegment = current.content.slice(committedContentLen);
863
+ if (contentSegment.trim()) contentSegments = [...current.contentSegments, contentSegment];
864
+ committedContentLen = current.content.length;
865
+ }
601
866
  return {
602
867
  ...prev,
603
- [delta.advisorId]: {
868
+ [bucket]: {
604
869
  content: current.content + (delta.contentDelta ?? ""),
605
- thinking: current.thinking + (delta.thinkingDelta ?? "")
870
+ thinking: current.thinking + (delta.thinkingDelta ?? ""),
871
+ thinkingSegments,
872
+ committedLen,
873
+ contentSegments,
874
+ committedContentLen,
875
+ phase: delta.phase ?? current.phase,
876
+ finalized: current.finalized || delta.done === true,
877
+ toolSteps: delta.toolStep ? [...current.toolSteps, delta.toolStep] : current.toolSteps
606
878
  }
607
879
  };
608
880
  });
609
881
  } catch {}
610
882
  };
611
- es.addEventListener("resync", () => {
883
+ es.addEventListener("resync", (event) => {
612
884
  lastEventIdRef.current = 0;
613
885
  setLive({});
886
+ let reason = "";
887
+ try {
888
+ reason = JSON.parse(event.data).reason ?? "";
889
+ } catch {}
890
+ showTransientNotice(reason === "restart" ? "服务端已重启,正在补齐对话内容…" : reason === "gap" ? "正在补齐断开期间的内容…" : "正在补齐对话内容…", "stream", NOTICE_TTL_MS);
614
891
  });
615
- return () => es.close();
892
+ return () => {
893
+ es.close();
894
+ if (noticeTimerRef.current !== null) {
895
+ clearTimeout(noticeTimerRef.current);
896
+ noticeTimerRef.current = null;
897
+ }
898
+ };
616
899
  }, [sessionId]);
617
900
  const mergedMessages = data.messages.map((message) => {
618
901
  if (message.role !== "advisor") return message;
619
902
  const advisorId = message.advisorId ?? "";
620
- const streamed = live[advisorId];
903
+ const streamed = live[`${advisorId}::${message.round ?? 1}`];
621
904
  if (!streamed) return message;
622
- const content = (streamed.content?.length ?? 0) > (message.content?.length ?? 0) ? streamed.content : message.content;
905
+ if (streamed.finalized) return message;
906
+ const liveBody = sanitizeClientContent(streamed.content.slice(streamed.committedContentLen));
907
+ const content = liveBody.trim() ? liveBody : sanitizeClientContent(message.content);
623
908
  const thinking = (streamed.thinking?.length ?? 0) > (message.thinking?.length ?? 0) ? streamed.thinking : message.thinking;
909
+ const toolSteps = (streamed.toolSteps?.length ?? 0) > (message.toolSteps?.length ?? 0) ? streamed.toolSteps : message.toolSteps ?? [];
910
+ const liveSegments = streamed.thinkingSegments?.length ? streamed.thinkingSegments : [];
911
+ const baseSegments = message.thinkingSegments?.length ? message.thinkingSegments : message.thinking ? [message.thinking] : [];
912
+ const thinkingSegments = liveSegments.length ? liveSegments : baseSegments;
913
+ const liveActions = streamed.contentSegments?.length ? streamed.contentSegments.map((segment) => sanitizeClientContent(segment)) : [];
914
+ const baseActions = message.actionDescriptions?.length ? message.actionDescriptions : [];
915
+ const actionDescriptions = liveActions.length ? liveActions : baseActions;
624
916
  return {
625
917
  ...message,
626
918
  content,
627
- thinking
919
+ thinking,
920
+ thinkingSegments,
921
+ toolSteps,
922
+ actionDescriptions,
923
+ ...streamed.phase === "tool" ? {
924
+ liveBody,
925
+ livePhase: streamed.phase
926
+ } : {}
628
927
  };
629
928
  });
630
929
  const shortId = data.sessionId.length > 8 ? data.sessionId.slice(0, 8) : data.sessionId;
631
- const title = data.status === "completed" ? "ADVISOR GROUP · DONE" : "ADVISOR GROUP · LIVE";
930
+ const title = data.status === "completed" ? "ADVISOR GROUP · DONE" : data.status === "cancelled" ? "ADVISOR GROUP · STOPPED" : "ADVISOR GROUP · LIVE";
632
931
  return (0, react.createElement)("div", {
633
932
  style: rootStyle,
634
933
  role: "log",
635
934
  "aria-live": "polite"
636
- }, (0, react.createElement)("div", { style: scanlineStyle }), (0, react.createElement)("div", { style: headerStyle }, (0, react.createElement)("span", { key: "title" }, title), (0, react.createElement)("span", { key: "id" }, `#${shortId}`)), (0, react.createElement)("div", { style: questionStyle }, `> ${data.question}`), data.context ? (0, react.createElement)("div", { style: contextBoxStyle }, (0, react.createElement)("button", {
935
+ }, (0, react.createElement)("div", { style: scanlineStyle }), (0, react.createElement)("div", { style: headerStyle }, (0, react.createElement)("span", { key: "title" }, title), (0, react.createElement)("span", {
936
+ key: "stop",
937
+ style: {
938
+ display: "flex",
939
+ alignItems: "center",
940
+ gap: 8
941
+ }
942
+ }, data.status === "running" ? (0, react.createElement)("button", {
943
+ type: "button",
944
+ onClick: () => {
945
+ (async () => {
946
+ try {
947
+ const body = await (await fetch("/advisor-group/stop", {
948
+ method: "POST",
949
+ headers: {
950
+ "content-type": "application/json",
951
+ ...authHeaders()
952
+ },
953
+ body: JSON.stringify({ sessionId })
954
+ })).json();
955
+ if (body.ok === false) showTransientNotice(body.error ?? "请求未被服务端接受,请稍后重试。", "action", NOTICE_TTL_MS);
956
+ } catch {
957
+ showTransientNotice("网络异常,请稍后重试。", "action", NOTICE_TTL_MS);
958
+ }
959
+ })();
960
+ },
961
+ style: {
962
+ cursor: "pointer",
963
+ border: "1px solid #e11d48",
964
+ background: "transparent",
965
+ color: "#fda4af",
966
+ borderRadius: 4,
967
+ padding: "2px 8px",
968
+ fontSize: 11
969
+ }
970
+ }, "⏹ 停止") : data.status === "cancelled" ? (0, react.createElement)("button", {
971
+ type: "button",
972
+ onClick: () => {
973
+ (async () => {
974
+ try {
975
+ const body = await (await fetch("/advisor-group/resume", {
976
+ method: "POST",
977
+ headers: {
978
+ "content-type": "application/json",
979
+ ...authHeaders()
980
+ },
981
+ body: JSON.stringify({ sessionId })
982
+ })).json();
983
+ if (body.ok === false) showTransientNotice(body.error ?? "请求未被服务端接受,请稍后重试。", "action", NOTICE_TTL_MS);
984
+ } catch {
985
+ showTransientNotice("网络异常,请稍后重试。", "action", NOTICE_TTL_MS);
986
+ }
987
+ })();
988
+ },
989
+ style: {
990
+ cursor: "pointer",
991
+ border: "1px solid #22c55e",
992
+ background: "transparent",
993
+ color: "#86efac",
994
+ borderRadius: 4,
995
+ padding: "2px 8px",
996
+ fontSize: 11
997
+ }
998
+ }, "▶ 继续聊天") : null, (0, react.createElement)("span", { key: "id" }, `#${shortId}`))), data.context ? (0, react.createElement)("div", { style: contextBoxStyle }, (0, react.createElement)("button", {
637
999
  type: "button",
638
1000
  onClick: () => setContextCollapsed((value) => !value),
639
1001
  style: contextToggleStyle
640
- }, `📋 项目背景 ${contextCollapsed ? "▸" : "▾"}`), !contextCollapsed ? (0, react.createElement)("div", { style: contextBodyStyle }, data.context) : null) : null, (0, react.createElement)("div", { style: advisorsLineStyle }, `ADVISORS: ${data.advisors.map((a) => a.avatar ? `${a.avatar} ${a.name}` : a.name).join(" · ")}`), mergedMessages.map((message, index) => (0, react.createElement)(MessageBubble, {
1002
+ }, `📋 项目背景 ${contextCollapsed ? "▸" : "▾"}`), !contextCollapsed ? (0, react.createElement)("div", { style: contextBodyStyle }, data.context) : null) : null, (0, react.createElement)("div", { style: advisorsLineStyle }, `ADVISORS: ${data.advisors.map((a) => a.avatar ? `${a.avatar} ${a.name}` : a.name).join(" · ")}`), mergedMessages.map((message, index) => (0, react.createElement)(AdvisorSteps, {
641
1003
  key: index,
642
1004
  message
643
1005
  })), data.status === "completed" && data.summary?.conclusion ? (0, react.createElement)("div", { style: {
@@ -648,7 +1010,32 @@ function AdvisorGroupNodeView(props) {
648
1010
  color: "#86efac",
649
1011
  fontWeight: 700,
650
1012
  marginBottom: 4
651
- } }, "📌 综合结论"), (0, react.createElement)("div", null, renderMarkdown(data.summary.conclusion))) : null, data.status === "running" ? (0, react.createElement)("div", { style: waitingStyle }, "▊ AWAITING RESPONSES…") : null);
1013
+ } }, "📌 综合结论"), (0, react.createElement)("div", null, renderMarkdown(data.summary.conclusion))) : null, data.status === "completed" && (data.summary?.riskNotes ?? []).length > 0 ? (0, react.createElement)("div", { style: {
1014
+ borderTop: "1px dashed #2f9e44",
1015
+ marginTop: 8,
1016
+ paddingTop: 8
1017
+ } }, (data.summary?.riskNotes ?? []).map((note, index) => (0, react.createElement)("div", {
1018
+ key: index,
1019
+ style: {
1020
+ color: "#fbbf24",
1021
+ fontSize: 12,
1022
+ marginTop: 2
1023
+ }
1024
+ }, note))) : null, data.status === "cancelled" ? (0, react.createElement)("div", { style: {
1025
+ borderTop: "1px dashed #2f9e44",
1026
+ marginTop: 8,
1027
+ paddingTop: 8
1028
+ } }, (0, react.createElement)("div", { style: {
1029
+ color: "#fde68a",
1030
+ marginBottom: 4
1031
+ } }, "本次讨论已停止;点 ▶ 继续聊天 可从断点续跑。"), (data.summary?.riskNotes ?? []).map((note, index) => (0, react.createElement)("div", {
1032
+ key: index,
1033
+ style: {
1034
+ color: "#fbbf24",
1035
+ fontSize: 12,
1036
+ marginTop: 2
1037
+ }
1038
+ }, note))) : null, data.status === "running" ? (0, react.createElement)("div", { style: waitingStyle }, "▊ AWAITING RESPONSES…") : null, transientNotice ? (0, react.createElement)("div", { style: noticeStyle }, transientNotice.text) : null);
652
1039
  }
653
1040
  const settingsRootStyle = {
654
1041
  fontFamily: "'Inter', 'Segoe UI', system-ui, sans-serif",
@@ -815,6 +1202,21 @@ const AVATAR_OPTIONS = [
815
1202
  "🚀",
816
1203
  "🛡️"
817
1204
  ];
1205
+ function defaultJevConfig() {
1206
+ return {
1207
+ enabled: false,
1208
+ provider: "typesafe",
1209
+ model: "jev-latest",
1210
+ baseURL: "",
1211
+ timeoutMs: 1e4,
1212
+ confidenceThreshold: .6,
1213
+ useEnglishState: false
1214
+ };
1215
+ }
1216
+ const JEV_DEFAULT_ENDPOINTS = {
1217
+ typesafe: "https://api.typesafe.ai/v1/systemone",
1218
+ openrouter: "https://openrouter.ai/api/alpha/decisions"
1219
+ };
818
1220
  function AdvisorGroupSettingsTab() {
819
1221
  const [config, setConfig] = (0, react.useState)(null);
820
1222
  const [providers, setProviders] = (0, react.useState)([]);
@@ -963,6 +1365,10 @@ function AdvisorGroupSettingsTab() {
963
1365
  try {
964
1366
  const configPayload = {
965
1367
  ...config,
1368
+ trigger: {
1369
+ ...config.trigger,
1370
+ jev: config.trigger.jev ? (({ apiKeyMeta: _meta, ...rest }) => rest)(config.trigger.jev) : defaultJevConfig()
1371
+ },
966
1372
  advisors: config.advisors.map(({ apiKeyMetaByProvider: _meta, ...rest }) => rest)
967
1373
  };
968
1374
  const data = await (await fetch("/advisor-group/config", {
@@ -1003,11 +1409,43 @@ function AdvisorGroupSettingsTab() {
1003
1409
  ...config,
1004
1410
  enabled: e.target.checked
1005
1411
  })
1006
- }), label("启用顾问群")), dailyGuard ? (0, react.createElement)("div", { style: {
1007
- marginTop: 6,
1412
+ }), label("启用顾问群")), (0, react.createElement)("div", { style: {
1413
+ display: "flex",
1414
+ alignItems: "center",
1415
+ gap: 8,
1416
+ marginTop: 4
1417
+ } }, (0, react.createElement)("input", {
1418
+ type: "checkbox",
1419
+ checked: config.quota.enabled,
1420
+ onChange: (e) => setConfig({
1421
+ ...config,
1422
+ quota: {
1423
+ ...config.quota,
1424
+ enabled: e.target.checked
1425
+ }
1426
+ })
1427
+ }), label("启用每日咨询上限(成本安全阀)")), (0, react.createElement)("div", { style: { marginTop: 6 } }, label(`每日上限次数(${config.quota.enabled ? "生效中" : "已关闭,不拦截"})`), (0, react.createElement)("input", {
1428
+ type: "number",
1429
+ min: 1,
1430
+ max: 1e5,
1431
+ value: String(config.quota.maxPerDay),
1432
+ disabled: !config.quota.enabled,
1433
+ onChange: (e) => setConfig({
1434
+ ...config,
1435
+ quota: {
1436
+ ...config.quota,
1437
+ maxPerDay: Math.max(1, Number(e.target.value))
1438
+ }
1439
+ }),
1440
+ style: {
1441
+ ...settingsInputStyle,
1442
+ width: 120,
1443
+ marginTop: 4
1444
+ }
1445
+ }))), (0, react.createElement)("div", { style: settingsSectionStyle }, dailyGuard ? (0, react.createElement)("div", { style: {
1008
1446
  fontSize: 12,
1009
- color: dailyGuard.remaining > 0 ? "var(--dsh-color-muted, #8b90a0)" : "#e11d48"
1010
- } }, dailyGuard.remaining > 0 ? `今日剩余咨询次数:${dailyGuard.remaining} / ${dailyGuard.limit}(UTC 日切)` : `今日咨询次数已用尽(${dailyGuard.used} / ${dailyGuard.limit}),将于 UTC 日切后恢复。`) : null), (0, react.createElement)("div", { style: settingsSectionStyle }, (0, react.createElement)("div", { style: {
1447
+ color: !dailyGuard.enabled || dailyGuard.remaining > 0 ? "var(--dsh-color-muted, #8b90a0)" : "#e11d48"
1448
+ } }, dailyGuard.enabled ? dailyGuard.remaining > 0 ? `今日剩余咨询次数:${dailyGuard.remaining} / ${dailyGuard.limit}(UTC 日切)` : `今日咨询次数已用尽(${dailyGuard.used} / ${dailyGuard.limit}),将于 UTC 日切后恢复。` : `今日已咨询 ${dailyGuard.used} 次(每日上限已关闭)。`) : null), (0, react.createElement)("div", { style: settingsSectionStyle }, (0, react.createElement)("div", { style: {
1011
1449
  display: "grid",
1012
1450
  gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
1013
1451
  gap: 8
@@ -1040,7 +1478,7 @@ function AdvisorGroupSettingsTab() {
1040
1478
  min: 1e3,
1041
1479
  max: 6e5,
1042
1480
  step: 1e3,
1043
- value: String(config.discussion.advisorTimeoutMs ?? 12e4),
1481
+ value: String(config.discussion.advisorTimeoutMs ?? 6e5),
1044
1482
  onChange: (e) => setConfig({
1045
1483
  ...config,
1046
1484
  discussion: {
@@ -1049,6 +1487,20 @@ function AdvisorGroupSettingsTab() {
1049
1487
  }
1050
1488
  }),
1051
1489
  style: settingsInputStyle
1490
+ })), (0, react.createElement)("div", null, label("驱动模型生成超时(毫秒)"), (0, react.createElement)("input", {
1491
+ type: "number",
1492
+ min: 1e3,
1493
+ max: 12e5,
1494
+ step: 1e3,
1495
+ value: String(config.discussion.driverTimeoutMs ?? 6e5),
1496
+ onChange: (e) => setConfig({
1497
+ ...config,
1498
+ discussion: {
1499
+ ...config.discussion,
1500
+ driverTimeoutMs: Math.max(1e3, Number(e.target.value))
1501
+ }
1502
+ }),
1503
+ style: settingsInputStyle
1052
1504
  }))), (0, react.createElement)("div", { style: { marginTop: 8 } }, label("置信度阈值(0-1,低于此值升级顾问群)"), (0, react.createElement)("input", {
1053
1505
  type: "number",
1054
1506
  min: 0,
@@ -1063,7 +1515,188 @@ function AdvisorGroupSettingsTab() {
1063
1515
  }
1064
1516
  }),
1065
1517
  style: settingsInputStyle
1066
- }))), (0, react.createElement)("div", { style: settingsSectionStyle }, (0, react.createElement)("div", { style: {
1518
+ }))), (0, react.createElement)("div", { style: settingsSectionStyle }, (0, react.createElement)("strong", null, "Jev 前置分类"), (0, react.createElement)("div", { style: {
1519
+ fontSize: 12,
1520
+ color: "var(--dsh-color-muted, #8b90a0)",
1521
+ margin: "4px 0 8px"
1522
+ } }, "启用后由 Jev 判断是否需要顾问群;未配置或调用失败时自动使用本地分类器。"), (0, react.createElement)("div", { style: {
1523
+ display: "flex",
1524
+ alignItems: "center",
1525
+ gap: 8,
1526
+ marginBottom: 8
1527
+ } }, (0, react.createElement)("input", {
1528
+ type: "checkbox",
1529
+ checked: config.trigger.jev?.enabled ?? false,
1530
+ onChange: (e) => setConfig({
1531
+ ...config,
1532
+ trigger: {
1533
+ ...config.trigger,
1534
+ jev: {
1535
+ ...config.trigger.jev ?? defaultJevConfig(),
1536
+ enabled: e.target.checked
1537
+ }
1538
+ }
1539
+ })
1540
+ }), label("启用 Jev")), (0, react.createElement)("div", { style: {
1541
+ display: "grid",
1542
+ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
1543
+ gap: 8
1544
+ } }, (0, react.createElement)("div", null, label("Jev 渠道"), (0, react.createElement)("select", {
1545
+ value: config.trigger.jev?.provider ?? "typesafe",
1546
+ onChange: (e) => {
1547
+ const current = config.trigger.jev ?? defaultJevConfig();
1548
+ const previousProvider = current.provider ?? "typesafe";
1549
+ const currentURL = current.baseURL ?? "";
1550
+ const usesDefault = !currentURL || currentURL === JEV_DEFAULT_ENDPOINTS[previousProvider];
1551
+ setConfig({
1552
+ ...config,
1553
+ trigger: {
1554
+ ...config.trigger,
1555
+ jev: {
1556
+ ...current,
1557
+ provider: e.target.value,
1558
+ ...usesDefault ? { baseURL: JEV_DEFAULT_ENDPOINTS[e.target.value] } : {}
1559
+ }
1560
+ }
1561
+ });
1562
+ },
1563
+ style: settingsInputStyle
1564
+ }, (0, react.createElement)("option", { value: "typesafe" }, "TypeSafe 官方 API"), (0, react.createElement)("option", { value: "openrouter" }, "OpenRouter Decisions API"))), (0, react.createElement)("div", null, label("Jev 模型"), (0, react.createElement)("input", {
1565
+ value: config.trigger.jev?.model ?? "jev-latest",
1566
+ onChange: (e) => setConfig({
1567
+ ...config,
1568
+ trigger: {
1569
+ ...config.trigger,
1570
+ jev: {
1571
+ ...config.trigger.jev ?? defaultJevConfig(),
1572
+ model: e.target.value
1573
+ }
1574
+ }
1575
+ }),
1576
+ style: settingsInputStyle
1577
+ })), (0, react.createElement)("div", null, label("判断阈值(0-1)"), (0, react.createElement)("input", {
1578
+ type: "number",
1579
+ min: 0,
1580
+ max: 1,
1581
+ step: .05,
1582
+ value: String(config.trigger.jev?.confidenceThreshold ?? .6),
1583
+ onChange: (e) => setConfig({
1584
+ ...config,
1585
+ trigger: {
1586
+ ...config.trigger,
1587
+ jev: {
1588
+ ...config.trigger.jev ?? defaultJevConfig(),
1589
+ confidenceThreshold: Number(e.target.value)
1590
+ }
1591
+ }
1592
+ }),
1593
+ style: settingsInputStyle
1594
+ })), (0, react.createElement)("div", null, label("高风险阈值(0-1,空=跟随判断阈值)"), (0, react.createElement)("input", {
1595
+ type: "number",
1596
+ min: 0,
1597
+ max: 1,
1598
+ step: .05,
1599
+ placeholder: String(config.trigger.jev?.confidenceThreshold ?? .6),
1600
+ value: config.trigger.jev?.highRiskThreshold !== void 0 ? String(config.trigger.jev.highRiskThreshold) : "",
1601
+ onChange: (e) => setConfig({
1602
+ ...config,
1603
+ trigger: {
1604
+ ...config.trigger,
1605
+ jev: {
1606
+ ...config.trigger.jev ?? defaultJevConfig(),
1607
+ highRiskThreshold: e.target.value === "" ? void 0 : Number(e.target.value)
1608
+ }
1609
+ }
1610
+ }),
1611
+ style: settingsInputStyle
1612
+ })), (0, react.createElement)("div", null, label("请求超时(毫秒)"), (0, react.createElement)("input", {
1613
+ type: "number",
1614
+ min: 1e3,
1615
+ max: 6e4,
1616
+ step: 1e3,
1617
+ value: String(config.trigger.jev?.timeoutMs ?? 1e4),
1618
+ onChange: (e) => setConfig({
1619
+ ...config,
1620
+ trigger: {
1621
+ ...config.trigger,
1622
+ jev: {
1623
+ ...config.trigger.jev ?? defaultJevConfig(),
1624
+ timeoutMs: Math.max(1e3, Number(e.target.value))
1625
+ }
1626
+ }
1627
+ }),
1628
+ style: settingsInputStyle
1629
+ }))), (0, react.createElement)("div", { style: { marginTop: 8 } }, label("API 地址(留空使用渠道默认地址)"), (0, react.createElement)("input", {
1630
+ value: config.trigger.jev?.baseURL || JEV_DEFAULT_ENDPOINTS[config.trigger.jev?.provider ?? "typesafe"],
1631
+ placeholder: JEV_DEFAULT_ENDPOINTS[config.trigger.jev?.provider ?? "typesafe"],
1632
+ onChange: (e) => setConfig({
1633
+ ...config,
1634
+ trigger: {
1635
+ ...config.trigger,
1636
+ jev: {
1637
+ ...config.trigger.jev ?? defaultJevConfig(),
1638
+ baseURL: e.target.value
1639
+ }
1640
+ }
1641
+ }),
1642
+ style: settingsInputStyle
1643
+ })), (0, react.createElement)("div", { style: {
1644
+ display: "grid",
1645
+ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
1646
+ gap: 8,
1647
+ marginTop: 8
1648
+ } }, (0, react.createElement)("div", null, label("Jev API Key"), (0, react.createElement)("input", {
1649
+ type: "password",
1650
+ placeholder: config.trigger.jev?.apiKeyMeta?.configured ? `已配置 ••••${config.trigger.jev.apiKeyMeta.last4 ?? ""}` : "输入 API Key",
1651
+ value: config.trigger.jev?.apiKey ?? "",
1652
+ onChange: (e) => setConfig({
1653
+ ...config,
1654
+ trigger: {
1655
+ ...config.trigger,
1656
+ jev: {
1657
+ ...config.trigger.jev ?? defaultJevConfig(),
1658
+ apiKey: e.target.value
1659
+ }
1660
+ }
1661
+ }),
1662
+ style: settingsInputStyle
1663
+ })), (0, react.createElement)("div", null, label("API Key 环境变量名(可选)"), (0, react.createElement)("input", {
1664
+ placeholder: "TYPESAFE_API_KEY 或 OPENROUTER_API_KEY",
1665
+ value: config.trigger.jev?.apiKeyEnv ?? "",
1666
+ onChange: (e) => setConfig({
1667
+ ...config,
1668
+ trigger: {
1669
+ ...config.trigger,
1670
+ jev: {
1671
+ ...config.trigger.jev ?? defaultJevConfig(),
1672
+ apiKeyEnv: e.target.value
1673
+ }
1674
+ }
1675
+ }),
1676
+ style: settingsInputStyle
1677
+ }))), (0, react.createElement)("div", { style: {
1678
+ display: "flex",
1679
+ alignItems: "center",
1680
+ gap: 8,
1681
+ marginTop: 8
1682
+ } }, (0, react.createElement)("input", {
1683
+ type: "checkbox",
1684
+ checked: config.trigger.jev?.useEnglishState ?? false,
1685
+ onChange: (e) => setConfig({
1686
+ ...config,
1687
+ trigger: {
1688
+ ...config.trigger,
1689
+ jev: {
1690
+ ...config.trigger.jev ?? defaultJevConfig(),
1691
+ useEnglishState: e.target.checked
1692
+ }
1693
+ }
1694
+ })
1695
+ }), label("Jev 英文判定(questionEn 概要)")), (0, react.createElement)("div", { style: {
1696
+ fontSize: 12,
1697
+ color: "var(--dsh-color-muted, #8b90a0)",
1698
+ margin: "4px 0 0"
1699
+ } }, "开启后,主模型提供的英文概要(questionEn)会作为 Jev 的判定输入;展示问句、会话记录与重复检测仍用中文。默认关闭。")), (0, react.createElement)("div", { style: settingsSectionStyle }, (0, react.createElement)("div", { style: {
1067
1700
  display: "flex",
1068
1701
  alignItems: "center",
1069
1702
  gap: 8
@@ -1364,7 +1997,55 @@ function AdvisorGroupSettingsTab() {
1364
1997
  marginTop: 4,
1365
1998
  resize: "vertical"
1366
1999
  }
1367
- }), (0, react.createElement)("div", { style: {
2000
+ }), (() => {
2001
+ const supportsTools = !providers.some((p) => p.id === advisor.provider && p.kind === "llm") || Boolean(advisor.baseURL || advisor.apiKeyEnv);
2002
+ const globalDefault = config.discussion.advisorTools ?? "readonly";
2003
+ const mode = advisor.tools ?? globalDefault;
2004
+ const modeLabel = {
2005
+ readonly: "只读工具",
2006
+ all: "全部会话工具(含可写,慎用)",
2007
+ off: "关闭"
2008
+ };
2009
+ const toolPromptNote = "工具启用时,系统提示词会自动追加:『对于你不了解、不熟悉的内容,尤其是项目背景、代码细节、仓库状态等不在知识范围内的情况,请优先使用联网工具(如 web_search、web_fetch)搜索核实后再作答,不要编造未核实的事实。』";
2010
+ if (supportsTools) return [
2011
+ label(`顾问工具调用范围(未单独设置时跟随全局默认:${modeLabel[globalDefault] ?? globalDefault})`),
2012
+ (0, react.createElement)("select", {
2013
+ "aria-label": "顾问工具调用范围",
2014
+ value: mode,
2015
+ onChange: (e) => updateAdvisor(index, { tools: e.target.value }),
2016
+ style: settingsInputStyle
2017
+ }, (0, react.createElement)("option", { value: "readonly" }, "只读工具"), (0, react.createElement)("option", { value: "all" }, "全部会话工具(含可写,慎用)"), (0, react.createElement)("option", { value: "off" }, "关闭")),
2018
+ mode === "off" ? null : (0, react.createElement)("div", { style: {
2019
+ fontSize: 12,
2020
+ color: "var(--dsh-color-muted, #8b90a0)",
2021
+ border: "1px dashed var(--dsh-color-border, #3a3f4b)",
2022
+ borderRadius: 4,
2023
+ padding: "4px 6px",
2024
+ marginTop: 4
2025
+ } }, toolPromptNote)
2026
+ ];
2027
+ return [label("顾问工具调用范围"), (0, react.createElement)("div", { style: {
2028
+ fontSize: 12,
2029
+ color: "var(--dsh-color-muted, #8b90a0)",
2030
+ border: "1px solid var(--dsh-color-border, #3a3f4b)",
2031
+ borderRadius: 4,
2032
+ padding: "4px 6px"
2033
+ } }, "❌ 当前为 DSH 内置通道(复用 DSH 凭据),不支持工具调用;为其配置 API 地址 / Key / 环境变量以走直连通道后即可启用(工具指引提示词也会随之生效)。")];
2034
+ })(), label("思考模式 / 思考强度(未设置 = 跟随模型与供应商默认)"), (0, react.createElement)("div", { style: {
2035
+ display: "flex",
2036
+ gap: 8,
2037
+ marginTop: 4
2038
+ } }, (0, react.createElement)("select", {
2039
+ "aria-label": "思考模式",
2040
+ value: advisor.thinking ?? "",
2041
+ onChange: (e) => updateAdvisor(index, { thinking: e.target.value === "" ? void 0 : e.target.value }),
2042
+ style: settingsInputStyle
2043
+ }, (0, react.createElement)("option", { value: "" }, "思考:默认"), (0, react.createElement)("option", { value: "enabled" }, "思考:开启"), (0, react.createElement)("option", { value: "disabled" }, "思考:关闭")), (0, react.createElement)("select", {
2044
+ "aria-label": "思考强度",
2045
+ value: advisor.reasoningEffort ?? "",
2046
+ onChange: (e) => updateAdvisor(index, { reasoningEffort: e.target.value === "" ? void 0 : e.target.value }),
2047
+ style: settingsInputStyle
2048
+ }, (0, react.createElement)("option", { value: "" }, "强度:默认"), (0, react.createElement)("option", { value: "off" }, "off(不思考)"), (0, react.createElement)("option", { value: "low" }, "low(低)"), (0, react.createElement)("option", { value: "high" }, "high(高)"), (0, react.createElement)("option", { value: "max" }, "max(最大)"))), (0, react.createElement)("div", { style: {
1368
2049
  display: "flex",
1369
2050
  gap: 8,
1370
2051
  marginTop: 4
@@ -1430,15 +2111,21 @@ function apply(ctx) {
1430
2111
  key: "advisor-group",
1431
2112
  locale: "chat"
1432
2113
  }, AdvisorGroupNodeView));
1433
- ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
1434
- name: "settings.plugin.item",
1435
- key: "advisor-group"
2114
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
2115
+ name: "settings.section",
2116
+ id: "dsh-advisor-group",
2117
+ order: 0,
2118
+ label: "顾问群"
1436
2119
  }, AdvisorGroupSettingsTab));
1437
2120
  }
1438
2121
 
1439
2122
  //#endregion
1440
2123
  exports.advisorGroupDefinition = advisorGroupDefinition;
1441
2124
  exports.apply = apply;
2125
+ exports.buildStepSequence = buildStepSequence;
2126
+ exports.buildToolStepViews = buildToolStepViews;
2127
+ exports.extractActionDescription = extractActionDescription;
1442
2128
  exports.inject = inject;
1443
2129
  exports.name = name;
2130
+ exports.sanitizeClientContent = sanitizeClientContent;
1444
2131
  return module.exports; } });