dsh-advisor-group 0.1.0 → 0.1.2

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
@@ -283,12 +283,74 @@ const DEFAULT_ADVISOR_PROMPT = `
283
283
  * advisor-group/message -> update (live bubbles)
284
284
  * advisor-group/end -> update (final synopsis)
285
285
  */
286
- function advisorGroupToken() {
286
+ /** The per-boot token as injected into the page, when that injection arrived. */
287
+ function injectedAdvisorGroupToken() {
287
288
  return globalThis.__ADVISOR_GROUP_TOKEN__ ?? "";
288
289
  }
289
- function authHeaders() {
290
- const token = advisorGroupToken();
291
- return token ? { "x-advisor-group-token": token } : {};
290
+ let advisorGroupTokenCache = "";
291
+ let advisorGroupTokenFetch = null;
292
+ /** Token known so far: the injected one wins, otherwise the last handshake. */
293
+ function currentAdvisorGroupToken() {
294
+ return injectedAdvisorGroupToken() || advisorGroupTokenCache;
295
+ }
296
+ /**
297
+ * The host normally injects the per-boot token into the served index. The
298
+ * desktop shell can compose its window before this plugin registers that
299
+ * injection, so the page may never receive it — fall back to fetching it from
300
+ * the plugin's own handshake route.
301
+ */
302
+ async function resolveAdvisorGroupToken(force = false) {
303
+ if (!force) {
304
+ const known = currentAdvisorGroupToken();
305
+ if (known) return known;
306
+ }
307
+ if (!advisorGroupTokenFetch) advisorGroupTokenFetch = (async () => {
308
+ try {
309
+ const res = await fetch(`/advisor-group/boot-token`, { headers: { accept: "application/json" } });
310
+ if (!res.ok) return "";
311
+ const body = await res.json();
312
+ return typeof body.token === "string" ? body.token : "";
313
+ } catch {
314
+ return "";
315
+ }
316
+ })().then((token) => {
317
+ advisorGroupTokenCache = token;
318
+ advisorGroupTokenFetch = null;
319
+ return token;
320
+ });
321
+ return await advisorGroupTokenFetch || injectedAdvisorGroupToken();
322
+ }
323
+ /**
324
+ * `fetch` with the per-boot token attached. A 401 means the token rotated (a
325
+ * live plugin reload mints a new one) or that the injected copy was stale, so
326
+ * refresh it once and retry before surfacing the failure.
327
+ */
328
+ async function authedFetch(path, init = {}) {
329
+ const send = (token) => fetch(path, {
330
+ ...init,
331
+ headers: {
332
+ ...init.headers,
333
+ ...token ? { "x-advisor-group-token": token } : {}
334
+ }
335
+ });
336
+ const res = await send(await resolveAdvisorGroupToken());
337
+ if (res.status !== 401) return res;
338
+ const refreshed = await resolveAdvisorGroupToken(true);
339
+ return refreshed ? send(refreshed) : res;
340
+ }
341
+ /**
342
+ * Browser-side twin of `sanitizeAdvisorContent` (host module cannot be
343
+ * imported by the client bundle). The SSE live overlay streams the RAW model
344
+ * output, whose length usually EXCEEDS the sanitized durable message — so the
345
+ * merge below must sanitize whichever content it picks or the raw tool-call
346
+ * markup would keep rendering until the overlay is gone.
347
+ */
348
+ const CLIENT_RAW_TOOL_CALL_BLOCK = /<(?:[||]{1,2})?(?:tool_calls|DSML)[\s\S]*?<\/(?:[||]{1,2})?(?:tool_calls|DSML)\s*>/g;
349
+ const CLIENT_RAW_TOOL_CALL_TAIL = /<(?:[||]{1,2})?(?:tool_calls|DSML)[\s\S]{0,60}?(?:<invoke|<parameter|<[||]{1,2}DSML)[\s\S]*$/g;
350
+ const CLIENT_EMPTY_BODY_NOTICE = "(顾问未生成正文:输出仅为工具调用描述,请参考其思维链与工具调用记录。)";
351
+ function sanitizeClientContent(content) {
352
+ if (!content.trim()) return content;
353
+ return content.replace(CLIENT_RAW_TOOL_CALL_BLOCK, "").replace(CLIENT_RAW_TOOL_CALL_TAIL, "").trim() || CLIENT_EMPTY_BODY_NOTICE;
292
354
  }
293
355
  function locationOf(context) {
294
356
  return context.start?.location ?? context.matches[0]?.location ?? { kind: "unresolved" };
@@ -301,7 +363,8 @@ function viewData(state) {
301
363
  ...state.context === void 0 ? {} : { context: state.context },
302
364
  advisors: state.advisors,
303
365
  messages: state.messages,
304
- ...state.summary === void 0 ? {} : { summary: state.summary }
366
+ ...state.summary === void 0 ? {} : { summary: state.summary },
367
+ ...state.mode === void 0 ? {} : { mode: state.mode }
305
368
  };
306
369
  }
307
370
  const advisorGroupDefinition = {
@@ -312,7 +375,7 @@ const advisorGroupDefinition = {
312
375
  id: event.data.sessionId,
313
376
  role: "start"
314
377
  };
315
- if (event.type === "advisor-group/message" || event.type === "advisor-group/delta" || event.type === "advisor-group/end") return {
378
+ if (event.type === "advisor-group/message" || event.type === "advisor-group/delta" || event.type === "advisor-group/end" || event.type === "advisor-group/resume") return {
316
379
  id: event.data.sessionId,
317
380
  role: "update"
318
381
  };
@@ -321,7 +384,7 @@ const advisorGroupDefinition = {
321
384
  start: (_context, match) => {
322
385
  if (match.event.type !== "advisor-group/start") throw new Error("advisor-group requires advisor-group/start");
323
386
  const data = match.event.data;
324
- return {
387
+ const base = {
325
388
  sessionId: data.sessionId,
326
389
  turn: data.turn,
327
390
  step: data.step,
@@ -337,6 +400,11 @@ const advisorGroupDefinition = {
337
400
  }],
338
401
  status: "running"
339
402
  };
403
+ if (data.kind === "ask") return {
404
+ ...base,
405
+ mode: "ask"
406
+ };
407
+ return base;
340
408
  },
341
409
  update: (context, match) => {
342
410
  if (match.event.type === "advisor-group/message") {
@@ -385,9 +453,13 @@ const advisorGroupDefinition = {
385
453
  }
386
454
  if (match.event.type === "advisor-group/end") return {
387
455
  ...context.state,
388
- status: "completed",
456
+ status: match.event.data.summary.stopped ? "cancelled" : "completed",
389
457
  summary: match.event.data.summary
390
458
  };
459
+ if (match.event.type === "advisor-group/resume") return {
460
+ ...context.state,
461
+ status: "running"
462
+ };
391
463
  return context.state;
392
464
  },
393
465
  publication: (match) => match.event.type === "advisor-group/message" || match.event.type === "advisor-group/delta" ? "animation-frame" : "immediate",
@@ -465,13 +537,6 @@ const headerStyle = {
465
537
  letterSpacing: 1,
466
538
  fontSize: 11
467
539
  };
468
- const questionStyle = {
469
- color: "#d9f99d",
470
- fontWeight: 700,
471
- whiteSpace: "pre-wrap",
472
- wordBreak: "break-word",
473
- marginBottom: 6
474
- };
475
540
  const advisorsLineStyle = {
476
541
  fontSize: 11,
477
542
  color: "#4ade80",
@@ -507,81 +572,327 @@ const waitingStyle = {
507
572
  color: "#fbbf24",
508
573
  marginTop: 6
509
574
  };
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]);
575
+ const noticeStyle = {
576
+ color: "#fda4af",
577
+ marginTop: 6
578
+ };
579
+ /** Internal announcements that must never reach the user (the tool list). */
580
+ const INTERNAL_TOOL_STEP_NAMES = /* @__PURE__ */ new Set(["⚙️ tools", "⚙ tools"]);
581
+ /** True when the tool result represents a failure — the wrapper prefix, or a
582
+ * JSON payload carrying `error`/`detail`/HTTP error code (unconfigured/401
583
+ * tools), robust to nested objects before the error key. */
584
+ function isFailedToolResult(text) {
585
+ if (text.startsWith("(工具执行失败")) return true;
586
+ try {
587
+ const parsed = JSON.parse(text);
588
+ if (parsed && (parsed.error !== void 0 || parsed.detail !== void 0)) return true;
589
+ if (parsed && typeof parsed.code === "number" && parsed.code >= 400 && parsed.code <= 599) return true;
590
+ } catch {}
591
+ return false;
592
+ }
593
+ /**
594
+ * Pair call/result tool steps into one `ToolStepView` per invocation, in
595
+ * stream order, and drop internal announcements (「本次可用」).
596
+ */
597
+ function buildToolStepViews(steps) {
598
+ const out = [];
599
+ let last;
600
+ for (const step of steps ?? []) {
601
+ if (INTERNAL_TOOL_STEP_NAMES.has(step.name)) continue;
602
+ if (step.kind === "call") {
603
+ last = {
604
+ name: step.name,
605
+ input: step.text,
606
+ atMs: step.atMs
607
+ };
608
+ out.push(last);
609
+ } else if (step.kind === "result") {
610
+ if (last && last.output === void 0 && last.input !== void 0) {
611
+ last.output = step.text;
612
+ if (isFailedToolResult(step.text)) last.failed = true;
613
+ } else out.push({
614
+ name: step.name,
615
+ output: step.text,
616
+ atMs: step.atMs,
617
+ ...isFailedToolResult(step.text) ? { failed: true } : {}
618
+ });
619
+ }
620
+ }
621
+ return out.filter((view) => view.input !== void 0 || view.output !== void 0);
622
+ }
623
+ const stepRowShellStyle = {
624
+ border: "1px solid rgba(34,197,94,0.28)",
625
+ borderRadius: 3,
626
+ margin: "5px 0",
627
+ background: "rgba(34,197,94,0.05)",
628
+ overflow: "hidden"
629
+ };
630
+ const stepRowHeaderStyle = (color) => ({
631
+ width: "100%",
632
+ display: "flex",
633
+ alignItems: "center",
634
+ gap: 8,
635
+ cursor: "pointer",
636
+ background: "transparent",
637
+ border: "none",
638
+ color,
639
+ fontSize: 12,
640
+ textAlign: "left",
641
+ padding: "4px 8px"
642
+ });
643
+ const stepRowBodyStyle = {
644
+ borderTop: "1px dashed rgba(47,158,68,0.45)",
645
+ padding: "5px 8px"
646
+ };
647
+ function StepRow({ title, badges, defaultOpen, color = "#4ade80", body }) {
648
+ const [open, setOpen] = (0, react.useState)(defaultOpen);
649
+ return (0, react.createElement)("div", { style: stepRowShellStyle }, (0, react.createElement)("button", {
650
+ type: "button",
651
+ onClick: () => setOpen((value) => !value),
652
+ style: stepRowHeaderStyle(color)
653
+ }, (0, react.createElement)("span", null, open ? "▾" : "▸"), (0, react.createElement)("span", { style: { fontWeight: 700 } }, title), ...badges ?? []), open ? (0, react.createElement)("div", { style: stepRowBodyStyle }, body) : null);
654
+ }
655
+ /**
656
+ * Inline thinking annotation: an UNFRAMED block that sits directly above the
657
+ * tool row it led to (each step reads: 💭 思考文本 → ⛭ 工具), with the full
658
+ * segment visible (scrollable) — not a separate bordered card.
659
+ */
660
+ const thinkingInlineStyle = {
661
+ margin: "4px 0 2px",
662
+ padding: "4px 8px",
663
+ borderLeft: "2px solid rgba(251,191,36,0.45)",
664
+ background: "rgba(217,119,6,0.04)"
665
+ };
666
+ const thinkingTagStyle = {
667
+ color: "#fbbf24",
668
+ fontSize: 11,
669
+ marginBottom: 2,
670
+ fontWeight: 700
671
+ };
672
+ const descriptionBodyStyle = {
673
+ whiteSpace: "pre-wrap",
674
+ wordBreak: "break-word",
675
+ color: "#d9f99d",
676
+ fontSize: 12,
677
+ padding: "2px 6px 4px"
678
+ };
679
+ /**
680
+ * Short "next-action narrative" of a thinking segment — the visible 📋 行动·N
681
+ * block shown directly after the 💭 思考·N panel and before the ⛭ tool row.
682
+ * Standard-agent style: pick the FIRST SHORT sentence that says what the model
683
+ * is about to do (action markers like 我先/接下来/我需要…), else the first
684
+ * non-generic short sentence; hard-cap at ~140 chars. The long internal
685
+ * monologue stays in the thinking panel.
686
+ */
687
+ function extractActionDescription(segment) {
688
+ const text = segment.trim();
689
+ if (!text) return "";
690
+ const sentences = (text.match(/[^。!?!?]+[。!?!?]?/g) ?? []).map((sentence) => sentence.replace(/\s+/g, " ").trim()).filter(Boolean);
691
+ const cut = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value;
692
+ const ACTION = /(我先|接下来|下一步|下面|准备|打算|我需要|先做|先看|先检查|先核对|先核实|先验证|先确认|先读|先搜索|先检索|先找|计划|步骤|让我先|我会|需要先|先补|先抓|先取|先查|先分析|先整理|先修改|先跟踪)/;
693
+ const GENERIC = /^(让我|我明白|好的|嗯|OK|Okay|我是|我是一位|我的角色|Let me|Sure|The user|用户)/;
694
+ for (const sentence of sentences.slice(0, 5)) if (sentence.length <= 160 && ACTION.test(sentence)) return cut(sentence, 140);
695
+ for (const sentence of sentences.slice(0, 3)) if (sentence.length <= 120 && !GENERIC.test(sentence)) return cut(sentence, 120);
696
+ return cut(sentences[0] ?? text, 100);
697
+ }
698
+ const thinkingBodyStyle = {
699
+ maxHeight: 200,
700
+ overflowY: "auto",
701
+ whiteSpace: "pre-wrap",
702
+ wordBreak: "break-word",
703
+ color: "#fde68a",
704
+ fontSize: 12,
705
+ padding: "4px 6px"
706
+ };
707
+ const toolBodyStyle = {
708
+ fontSize: 12,
709
+ color: "#38bdf8"
710
+ };
711
+ const toolIoTitleStyle = {
712
+ color: "#86efac",
713
+ fontWeight: 700,
714
+ margin: "2px 0 2px"
715
+ };
716
+ const toolIoBodyStyle = {
717
+ whiteSpace: "pre-wrap",
718
+ wordBreak: "break-word",
719
+ color: "var(--dsh-color-muted, #8b90a0)",
720
+ marginBottom: 6
721
+ };
722
+ const badgeStyle = (color) => ({
723
+ color,
724
+ fontSize: 11,
725
+ opacity: .95
726
+ });
727
+ /**
728
+ * Interleave action announcements, thinking segments and tool views in the
729
+ * official DSH block order (text → reasoning → tool-call), then the body.
730
+ * Each round contributes at most one 行动 / 思考 / 工具 item; missing actions
731
+ * are derived from the thinking segment so pre-actionDescriptions logs still
732
+ * render sensibly.
733
+ */
734
+ function buildStepSequence(actionDescriptions, thinkingSegments, toolViews) {
735
+ const items = [];
736
+ const count = Math.max(actionDescriptions.length, thinkingSegments.length, toolViews.length);
737
+ for (let index = 0; index < count; index += 1) {
738
+ const step = index + 1;
739
+ const described = actionDescriptions[index]?.trim();
740
+ const segment = thinkingSegments[index];
741
+ const action = described || (segment !== void 0 && segment.trim() ? extractActionDescription(segment) : "");
742
+ if (action) items.push({
743
+ kind: "action",
744
+ text: action,
745
+ step
746
+ });
747
+ if (segment !== void 0 && segment.trim()) items.push({
748
+ kind: "thinking",
749
+ segment,
750
+ step
751
+ });
752
+ const view = toolViews[index];
753
+ if (view !== void 0) items.push({
754
+ kind: "tool",
755
+ view
756
+ });
757
+ }
758
+ items.push({ kind: "body" });
759
+ return items;
760
+ }
761
+ /**
762
+ * Render one consultation message as a stack of STEP rows (one row per step,
763
+ * appearing in stream order, each independently expandable):
764
+ * 💭 思考·1 → ⛭ 工具×1(输入/输出)→ 💭 思考·2 → ⛭ 工具×2 → … → 📄 正文
765
+ * Main/system messages stay a single collapsible row.
766
+ */
767
+ function AdvisorSteps({ message, plain = false }) {
516
768
  const isMain = message.role === "main";
517
769
  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
770
  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",
771
+ const group = isMain || isSystem || plain ? label : `${label} · R${message.round ?? 1}`;
772
+ const truncated = message.truncated;
773
+ const toolViews = buildToolStepViews(message.toolSteps);
774
+ const thinkingSegments = message.thinkingSegments && message.thinkingSegments.length > 0 ? message.thinkingSegments : message.thinking ? [message.thinking] : [];
775
+ const baseColor = isSystem ? "#fcd34d" : "#4ade80";
776
+ if (isMain || isSystem) return (0, react.createElement)(StepRow, {
777
+ title: group,
778
+ defaultOpen: isMain,
779
+ color: baseColor,
780
+ body: (0, react.createElement)("div", { style: {
562
781
  whiteSpace: "pre-wrap",
563
- wordBreak: "break-word",
564
- padding: "4px 6px",
565
- color: "#fde68a",
566
- fontSize: 12,
567
- borderTop: "1px dashed #a16207"
782
+ wordBreak: "break-word"
783
+ } }, message.content)
784
+ });
785
+ const sequence = buildStepSequence(message.actionDescriptions ?? [], thinkingSegments, toolViews);
786
+ const live = message;
787
+ const liveToolPhase = live.livePhase === "tool";
788
+ const liveAction = liveToolPhase && live.liveBody?.trim() ? live.liveBody.trim() : void 0;
789
+ const children = [];
790
+ for (const [index, item] of sequence.entries()) {
791
+ if (item.kind === "action") {
792
+ children.push((0, react.createElement)("div", {
793
+ key: `action-${index}`,
794
+ style: thinkingInlineStyle
795
+ }, (0, react.createElement)("div", { style: thinkingTagStyle }, `${group} 📋 行动·${item.step}`), (0, react.createElement)("div", { style: descriptionBodyStyle }, item.text)));
796
+ continue;
797
+ }
798
+ if (item.kind === "thinking") {
799
+ children.push((0, react.createElement)(StepRow, {
800
+ key: `think-${index}`,
801
+ title: `${group} 💭 思考·${item.step}`,
802
+ badges: [(0, react.createElement)("span", { style: badgeStyle("#fbbf24") }, `${item.segment.length} 字符`)],
803
+ defaultOpen: false,
804
+ color: "#fbbf24",
805
+ body: (0, react.createElement)("div", { style: thinkingBodyStyle }, item.segment)
806
+ }));
807
+ continue;
568
808
  }
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))));
809
+ if (item.kind === "tool") {
810
+ const view = item.view;
811
+ children.push((0, react.createElement)(StepRow, {
812
+ key: `tool-${index}`,
813
+ title: `${group} ⛭ ${view.name}`,
814
+ badges: [(0, react.createElement)("span", { style: badgeStyle(view.failed ? "#f87171" : "#38bdf8") }, view.output === void 0 ? "调用中…" : view.failed ? "⚠ 失败" : "调用完成")],
815
+ defaultOpen: false,
816
+ color: view.failed ? "#f87171" : "#38bdf8",
817
+ 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)
818
+ }));
819
+ continue;
820
+ }
821
+ if (liveToolPhase) continue;
822
+ children.push((0, react.createElement)(StepRow, {
823
+ key: `body-${index}`,
824
+ title: plain ? group : `${group} 📄 正文`,
825
+ badges: truncated ? [(0, react.createElement)("span", { style: badgeStyle("#f87171") }, truncated.reason === "timeout" ? "⏱ 响应超时截断" : "⚠ 流中断")] : [],
826
+ defaultOpen: true,
827
+ color: "#4ade80",
828
+ body: (0, react.createElement)("div", { style: { wordBreak: "break-word" } }, truncated ? (0, react.createElement)("div", { style: {
829
+ fontSize: 11,
830
+ color: "#f87171",
831
+ border: "1px solid #dc2626",
832
+ borderRadius: 3,
833
+ padding: "2px 6px",
834
+ display: "inline-block",
835
+ marginBottom: 3
836
+ } }, truncated.reason === "timeout" ? "⏱ 响应超时截断 — 思考已收到,正文未完成" : "⚠ 流中断 — 正文可能不完整") : null, renderMarkdown(message.content))
837
+ }));
838
+ }
839
+ if (liveAction !== void 0) children.push((0, react.createElement)("div", {
840
+ key: "live-action",
841
+ style: thinkingInlineStyle
842
+ }, (0, react.createElement)("div", { style: thinkingTagStyle }, `${group} 📋 行动·${toolViews.length + 1}`), (0, react.createElement)("div", { style: descriptionBodyStyle }, liveAction)));
843
+ return (0, react.createElement)("div", null, children);
573
844
  }
845
+ /** Auto-hide delay for transient card notices (request failures, resync). */
846
+ const NOTICE_TTL_MS = 6e3;
574
847
  function AdvisorGroupNodeView(props) {
575
848
  const data = props.node.data;
576
849
  const sessionId = data.sessionId;
850
+ const isAsk = data.mode === "ask";
577
851
  const [live, setLive] = (0, react.useState)({});
578
852
  const [contextCollapsed, setContextCollapsed] = (0, react.useState)(true);
579
853
  const lastEventIdRef = (0, react.useRef)(0);
580
854
  const lastBootIdRef = (0, react.useRef)("");
855
+ const [transientNotice, setTransientNotice] = (0, react.useState)(null);
856
+ const noticeTimerRef = (0, react.useRef)(null);
857
+ const showTransientNotice = (text, kind, ttlMs) => {
858
+ if (noticeTimerRef.current !== null) {
859
+ clearTimeout(noticeTimerRef.current);
860
+ noticeTimerRef.current = null;
861
+ }
862
+ setTransientNotice({
863
+ text,
864
+ kind
865
+ });
866
+ if (ttlMs !== void 0) noticeTimerRef.current = setTimeout(() => {
867
+ noticeTimerRef.current = null;
868
+ setTransientNotice(null);
869
+ }, ttlMs);
870
+ };
871
+ const clearStreamNotice = () => {
872
+ setTransientNotice((prev) => prev && prev.kind === "stream" ? null : prev);
873
+ };
874
+ const [streamTokenReady, setStreamTokenReady] = (0, react.useState)(() => currentAdvisorGroupToken() !== "");
581
875
  (0, react.useEffect)(() => {
582
- if (!sessionId) return;
583
- const es = new EventSource(`/advisor-group/stream?sessionId=${encodeURIComponent(sessionId)}&lastEventId=${lastEventIdRef.current}&bootId=${encodeURIComponent(lastBootIdRef.current)}&token=${encodeURIComponent(advisorGroupToken())}`);
876
+ if (streamTokenReady) return;
877
+ let alive = true;
878
+ resolveAdvisorGroupToken().then((token) => {
879
+ if (alive && token) setStreamTokenReady(true);
880
+ });
881
+ return () => {
882
+ alive = false;
883
+ };
884
+ }, [streamTokenReady]);
885
+ (0, react.useEffect)(() => {
886
+ if (!sessionId || isAsk || !streamTokenReady) return;
887
+ const es = new EventSource(`/advisor-group/stream?sessionId=${encodeURIComponent(sessionId)}&lastEventId=${lastEventIdRef.current}&bootId=${encodeURIComponent(lastBootIdRef.current)}&token=${encodeURIComponent(currentAdvisorGroupToken())}`);
888
+ es.onopen = () => {
889
+ clearStreamNotice();
890
+ };
891
+ es.onerror = () => {
892
+ showTransientNotice("连接中断,正在重连…", "stream");
893
+ };
584
894
  es.onmessage = (event) => {
895
+ clearStreamNotice();
585
896
  try {
586
897
  const delta = JSON.parse(event.data);
587
898
  const bootId = delta.bootId ?? "";
@@ -593,53 +904,165 @@ function AdvisorGroupNodeView(props) {
593
904
  const eventId = Number(delta.eventId ?? 0);
594
905
  if (eventId > 0 && eventId <= lastEventIdRef.current) return;
595
906
  if (eventId > 0) lastEventIdRef.current = eventId;
907
+ const bucket = `${delta.advisorId}::${delta.round ?? 1}`;
596
908
  setLive((prev) => {
597
- const current = prev[delta.advisorId] ?? {
909
+ const current = prev[bucket] ?? {
598
910
  content: "",
599
- thinking: ""
911
+ thinking: "",
912
+ thinkingSegments: [],
913
+ committedLen: 0,
914
+ contentSegments: [],
915
+ committedContentLen: 0,
916
+ phase: "answer",
917
+ finalized: false,
918
+ toolSteps: []
600
919
  };
920
+ let thinkingSegments = current.thinkingSegments;
921
+ let committedLen = current.committedLen;
922
+ let contentSegments = current.contentSegments;
923
+ let committedContentLen = current.committedContentLen;
924
+ if (delta.toolStep) {
925
+ const segment = current.thinking.slice(committedLen);
926
+ if (segment.trim()) thinkingSegments = [...current.thinkingSegments, segment];
927
+ committedLen = current.thinking.length;
928
+ const contentSegment = current.content.slice(committedContentLen);
929
+ if (contentSegment.trim()) contentSegments = [...current.contentSegments, contentSegment];
930
+ committedContentLen = current.content.length;
931
+ }
601
932
  return {
602
933
  ...prev,
603
- [delta.advisorId]: {
934
+ [bucket]: {
604
935
  content: current.content + (delta.contentDelta ?? ""),
605
- thinking: current.thinking + (delta.thinkingDelta ?? "")
936
+ thinking: current.thinking + (delta.thinkingDelta ?? ""),
937
+ thinkingSegments,
938
+ committedLen,
939
+ contentSegments,
940
+ committedContentLen,
941
+ phase: delta.phase ?? current.phase,
942
+ finalized: current.finalized || delta.done === true,
943
+ toolSteps: delta.toolStep ? [...current.toolSteps, delta.toolStep] : current.toolSteps
606
944
  }
607
945
  };
608
946
  });
609
947
  } catch {}
610
948
  };
611
- es.addEventListener("resync", () => {
949
+ es.addEventListener("resync", (event) => {
612
950
  lastEventIdRef.current = 0;
613
951
  setLive({});
952
+ let reason = "";
953
+ try {
954
+ reason = JSON.parse(event.data).reason ?? "";
955
+ } catch {}
956
+ showTransientNotice(reason === "restart" ? "服务端已重启,正在补齐对话内容…" : reason === "gap" ? "正在补齐断开期间的内容…" : "正在补齐对话内容…", "stream", NOTICE_TTL_MS);
614
957
  });
615
- return () => es.close();
616
- }, [sessionId]);
958
+ return () => {
959
+ es.close();
960
+ if (noticeTimerRef.current !== null) {
961
+ clearTimeout(noticeTimerRef.current);
962
+ noticeTimerRef.current = null;
963
+ }
964
+ };
965
+ }, [sessionId, streamTokenReady]);
617
966
  const mergedMessages = data.messages.map((message) => {
618
967
  if (message.role !== "advisor") return message;
619
968
  const advisorId = message.advisorId ?? "";
620
- const streamed = live[advisorId];
969
+ const streamed = live[`${advisorId}::${message.round ?? 1}`];
621
970
  if (!streamed) return message;
622
- const content = (streamed.content?.length ?? 0) > (message.content?.length ?? 0) ? streamed.content : message.content;
971
+ if (streamed.finalized) return message;
972
+ const liveBody = sanitizeClientContent(streamed.content.slice(streamed.committedContentLen));
973
+ const content = liveBody.trim() ? liveBody : sanitizeClientContent(message.content);
623
974
  const thinking = (streamed.thinking?.length ?? 0) > (message.thinking?.length ?? 0) ? streamed.thinking : message.thinking;
975
+ const toolSteps = (streamed.toolSteps?.length ?? 0) > (message.toolSteps?.length ?? 0) ? streamed.toolSteps : message.toolSteps ?? [];
976
+ const liveSegments = streamed.thinkingSegments?.length ? streamed.thinkingSegments : [];
977
+ const baseSegments = message.thinkingSegments?.length ? message.thinkingSegments : message.thinking ? [message.thinking] : [];
978
+ const thinkingSegments = liveSegments.length ? liveSegments : baseSegments;
979
+ const liveActions = streamed.contentSegments?.length ? streamed.contentSegments.map((segment) => sanitizeClientContent(segment)) : [];
980
+ const baseActions = message.actionDescriptions?.length ? message.actionDescriptions : [];
981
+ const actionDescriptions = liveActions.length ? liveActions : baseActions;
624
982
  return {
625
983
  ...message,
626
984
  content,
627
- thinking
985
+ thinking,
986
+ thinkingSegments,
987
+ toolSteps,
988
+ actionDescriptions,
989
+ ...streamed.phase === "tool" ? {
990
+ liveBody,
991
+ livePhase: streamed.phase
992
+ } : {}
628
993
  };
629
994
  });
630
995
  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";
996
+ const title = isAsk ? data.status === "completed" ? "跨会话提问 · 已完成" : "跨会话提问 · 等待应答" : data.status === "completed" ? "ADVISOR GROUP · DONE" : data.status === "cancelled" ? "ADVISOR GROUP · STOPPED" : "ADVISOR GROUP · LIVE";
632
997
  return (0, react.createElement)("div", {
633
998
  style: rootStyle,
634
999
  role: "log",
635
1000
  "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", {
1001
+ }, (0, react.createElement)("div", { style: scanlineStyle }), (0, react.createElement)("div", { style: headerStyle }, (0, react.createElement)("span", { key: "title" }, title), (0, react.createElement)("span", {
1002
+ key: "stop",
1003
+ style: {
1004
+ display: "flex",
1005
+ alignItems: "center",
1006
+ gap: 8
1007
+ }
1008
+ }, data.status === "running" && !isAsk ? (0, react.createElement)("button", {
1009
+ type: "button",
1010
+ onClick: () => {
1011
+ (async () => {
1012
+ try {
1013
+ const body = await (await authedFetch("/advisor-group/stop", {
1014
+ method: "POST",
1015
+ headers: { "content-type": "application/json" },
1016
+ body: JSON.stringify({ sessionId })
1017
+ })).json();
1018
+ if (body.ok === false) showTransientNotice(body.error ?? "请求未被服务端接受,请稍后重试。", "action", NOTICE_TTL_MS);
1019
+ } catch {
1020
+ showTransientNotice("网络异常,请稍后重试。", "action", NOTICE_TTL_MS);
1021
+ }
1022
+ })();
1023
+ },
1024
+ style: {
1025
+ cursor: "pointer",
1026
+ border: "1px solid #e11d48",
1027
+ background: "transparent",
1028
+ color: "#fda4af",
1029
+ borderRadius: 4,
1030
+ padding: "2px 8px",
1031
+ fontSize: 11
1032
+ }
1033
+ }, "⏹ 停止") : data.status === "cancelled" && !isAsk ? (0, react.createElement)("button", {
1034
+ type: "button",
1035
+ onClick: () => {
1036
+ (async () => {
1037
+ try {
1038
+ const body = await (await authedFetch("/advisor-group/resume", {
1039
+ method: "POST",
1040
+ headers: { "content-type": "application/json" },
1041
+ body: JSON.stringify({ sessionId })
1042
+ })).json();
1043
+ if (body.ok === false) showTransientNotice(body.error ?? "请求未被服务端接受,请稍后重试。", "action", NOTICE_TTL_MS);
1044
+ } catch {
1045
+ showTransientNotice("网络异常,请稍后重试。", "action", NOTICE_TTL_MS);
1046
+ }
1047
+ })();
1048
+ },
1049
+ style: {
1050
+ cursor: "pointer",
1051
+ border: "1px solid #22c55e",
1052
+ background: "transparent",
1053
+ color: "#86efac",
1054
+ borderRadius: 4,
1055
+ padding: "2px 8px",
1056
+ fontSize: 11
1057
+ }
1058
+ }, "▶ 继续聊天") : null, (0, react.createElement)("span", { key: "id" }, `#${shortId}`))), data.context ? (0, react.createElement)("div", { style: contextBoxStyle }, (0, react.createElement)("button", {
637
1059
  type: "button",
638
1060
  onClick: () => setContextCollapsed((value) => !value),
639
1061
  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, {
1062
+ }, `📋 项目背景 ${contextCollapsed ? "▸" : "▾"}`), !contextCollapsed ? (0, react.createElement)("div", { style: contextBodyStyle }, data.context) : null) : null, (0, react.createElement)("div", { style: advisorsLineStyle }, isAsk ? `提问方: ${data.advisors[0]?.name ?? "?"} → 目标会话: ${data.advisors[1]?.name ?? "?"}` : `ADVISORS: ${data.advisors.map((a) => a.avatar ? `${a.avatar} ${a.name}` : a.name).join(" · ")}`), mergedMessages.map((message, index) => (0, react.createElement)(AdvisorSteps, {
641
1063
  key: index,
642
- message
1064
+ message,
1065
+ plain: isAsk
643
1066
  })), data.status === "completed" && data.summary?.conclusion ? (0, react.createElement)("div", { style: {
644
1067
  borderTop: "1px dashed #2f9e44",
645
1068
  marginTop: 8,
@@ -648,7 +1071,32 @@ function AdvisorGroupNodeView(props) {
648
1071
  color: "#86efac",
649
1072
  fontWeight: 700,
650
1073
  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);
1074
+ } }, isAsk ? "📌 投递说明" : "📌 综合结论"), (0, react.createElement)("div", null, renderMarkdown(data.summary.conclusion))) : null, data.status === "completed" && (data.summary?.riskNotes ?? []).length > 0 ? (0, react.createElement)("div", { style: {
1075
+ borderTop: "1px dashed #2f9e44",
1076
+ marginTop: 8,
1077
+ paddingTop: 8
1078
+ } }, (data.summary?.riskNotes ?? []).map((note, index) => (0, react.createElement)("div", {
1079
+ key: index,
1080
+ style: {
1081
+ color: "#fbbf24",
1082
+ fontSize: 12,
1083
+ marginTop: 2
1084
+ }
1085
+ }, note))) : null, data.status === "cancelled" ? (0, react.createElement)("div", { style: {
1086
+ borderTop: "1px dashed #2f9e44",
1087
+ marginTop: 8,
1088
+ paddingTop: 8
1089
+ } }, (0, react.createElement)("div", { style: {
1090
+ color: "#fde68a",
1091
+ marginBottom: 4
1092
+ } }, "本次讨论已停止;点 ▶ 继续聊天 可从断点续跑。"), (data.summary?.riskNotes ?? []).map((note, index) => (0, react.createElement)("div", {
1093
+ key: index,
1094
+ style: {
1095
+ color: "#fbbf24",
1096
+ fontSize: 12,
1097
+ marginTop: 2
1098
+ }
1099
+ }, note))) : null, data.status === "running" ? (0, react.createElement)("div", { style: waitingStyle }, isAsk ? "▊ 已投递,等待对方回应…" : "▊ AWAITING RESPONSES…") : null, transientNotice ? (0, react.createElement)("div", { style: noticeStyle }, transientNotice.text) : null);
652
1100
  }
653
1101
  const settingsRootStyle = {
654
1102
  fontFamily: "'Inter', 'Segoe UI', system-ui, sans-serif",
@@ -815,6 +1263,21 @@ const AVATAR_OPTIONS = [
815
1263
  "🚀",
816
1264
  "🛡️"
817
1265
  ];
1266
+ function defaultJevConfig() {
1267
+ return {
1268
+ enabled: false,
1269
+ provider: "typesafe",
1270
+ model: "jev-latest",
1271
+ baseURL: "",
1272
+ timeoutMs: 1e4,
1273
+ confidenceThreshold: .6,
1274
+ useEnglishState: false
1275
+ };
1276
+ }
1277
+ const JEV_DEFAULT_ENDPOINTS = {
1278
+ typesafe: "https://api.typesafe.ai/v1/systemone",
1279
+ openrouter: "https://openrouter.ai/api/alpha/decisions"
1280
+ };
818
1281
  function AdvisorGroupSettingsTab() {
819
1282
  const [config, setConfig] = (0, react.useState)(null);
820
1283
  const [providers, setProviders] = (0, react.useState)([]);
@@ -833,7 +1296,7 @@ function AdvisorGroupSettingsTab() {
833
1296
  setMsg("");
834
1297
  setError("");
835
1298
  try {
836
- const [configData, providerData] = await Promise.all([fetch("/advisor-group/config", { headers: authHeaders() }).then((r) => r.json()), fetch("/advisor-group/providers", { headers: authHeaders() }).then((r) => r.json())]);
1299
+ const [configData, providerData] = await Promise.all([authedFetch("/advisor-group/config").then((r) => r.json()), authedFetch("/advisor-group/providers").then((r) => r.json())]);
837
1300
  if (configData.ok && configData.config) {
838
1301
  setConfig(configData.config);
839
1302
  savedJsonRef.current = JSON.stringify(configData.config);
@@ -889,12 +1352,9 @@ function AdvisorGroupSettingsTab() {
889
1352
  setError("");
890
1353
  setMsg("");
891
1354
  try {
892
- const data = await (await fetch("/advisor-group/models", {
1355
+ const data = await (await authedFetch("/advisor-group/models", {
893
1356
  method: "POST",
894
- headers: {
895
- "content-type": "application/json",
896
- ...authHeaders()
897
- },
1357
+ headers: { "content-type": "application/json" },
898
1358
  body: JSON.stringify({
899
1359
  advisorId: advisor.id,
900
1360
  provider: advisor.provider,
@@ -930,12 +1390,9 @@ function AdvisorGroupSettingsTab() {
930
1390
  setError("");
931
1391
  setMsg("");
932
1392
  try {
933
- const data = await (await fetch("/advisor-group/test-connection", {
1393
+ const data = await (await authedFetch("/advisor-group/test-connection", {
934
1394
  method: "POST",
935
- headers: {
936
- "content-type": "application/json",
937
- ...authHeaders()
938
- },
1395
+ headers: { "content-type": "application/json" },
939
1396
  body: JSON.stringify({
940
1397
  advisorId: advisor.id,
941
1398
  provider: advisor.provider,
@@ -963,14 +1420,15 @@ function AdvisorGroupSettingsTab() {
963
1420
  try {
964
1421
  const configPayload = {
965
1422
  ...config,
1423
+ trigger: {
1424
+ ...config.trigger,
1425
+ jev: config.trigger.jev ? (({ apiKeyMeta: _meta, ...rest }) => rest)(config.trigger.jev) : defaultJevConfig()
1426
+ },
966
1427
  advisors: config.advisors.map(({ apiKeyMetaByProvider: _meta, ...rest }) => rest)
967
1428
  };
968
- const data = await (await fetch("/advisor-group/config", {
1429
+ const data = await (await authedFetch("/advisor-group/config", {
969
1430
  method: "POST",
970
- headers: {
971
- "content-type": "application/json",
972
- ...authHeaders()
973
- },
1431
+ headers: { "content-type": "application/json" },
974
1432
  body: JSON.stringify({
975
1433
  config: configPayload,
976
1434
  expectedRevision: revision
@@ -1003,11 +1461,43 @@ function AdvisorGroupSettingsTab() {
1003
1461
  ...config,
1004
1462
  enabled: e.target.checked
1005
1463
  })
1006
- }), label("启用顾问群")), dailyGuard ? (0, react.createElement)("div", { style: {
1007
- marginTop: 6,
1464
+ }), label("启用顾问群")), (0, react.createElement)("div", { style: {
1465
+ display: "flex",
1466
+ alignItems: "center",
1467
+ gap: 8,
1468
+ marginTop: 4
1469
+ } }, (0, react.createElement)("input", {
1470
+ type: "checkbox",
1471
+ checked: config.quota.enabled,
1472
+ onChange: (e) => setConfig({
1473
+ ...config,
1474
+ quota: {
1475
+ ...config.quota,
1476
+ enabled: e.target.checked
1477
+ }
1478
+ })
1479
+ }), label("启用每日咨询上限(成本安全阀)")), (0, react.createElement)("div", { style: { marginTop: 6 } }, label(`每日上限次数(${config.quota.enabled ? "生效中" : "已关闭,不拦截"})`), (0, react.createElement)("input", {
1480
+ type: "number",
1481
+ min: 1,
1482
+ max: 1e5,
1483
+ value: String(config.quota.maxPerDay),
1484
+ disabled: !config.quota.enabled,
1485
+ onChange: (e) => setConfig({
1486
+ ...config,
1487
+ quota: {
1488
+ ...config.quota,
1489
+ maxPerDay: Math.max(1, Number(e.target.value))
1490
+ }
1491
+ }),
1492
+ style: {
1493
+ ...settingsInputStyle,
1494
+ width: 120,
1495
+ marginTop: 4
1496
+ }
1497
+ }))), (0, react.createElement)("div", { style: settingsSectionStyle }, dailyGuard ? (0, react.createElement)("div", { style: {
1008
1498
  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: {
1499
+ color: !dailyGuard.enabled || dailyGuard.remaining > 0 ? "var(--dsh-color-muted, #8b90a0)" : "#e11d48"
1500
+ } }, 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
1501
  display: "grid",
1012
1502
  gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
1013
1503
  gap: 8
@@ -1040,7 +1530,7 @@ function AdvisorGroupSettingsTab() {
1040
1530
  min: 1e3,
1041
1531
  max: 6e5,
1042
1532
  step: 1e3,
1043
- value: String(config.discussion.advisorTimeoutMs ?? 12e4),
1533
+ value: String(config.discussion.advisorTimeoutMs ?? 6e5),
1044
1534
  onChange: (e) => setConfig({
1045
1535
  ...config,
1046
1536
  discussion: {
@@ -1049,6 +1539,20 @@ function AdvisorGroupSettingsTab() {
1049
1539
  }
1050
1540
  }),
1051
1541
  style: settingsInputStyle
1542
+ })), (0, react.createElement)("div", null, label("驱动模型生成超时(毫秒)"), (0, react.createElement)("input", {
1543
+ type: "number",
1544
+ min: 1e3,
1545
+ max: 12e5,
1546
+ step: 1e3,
1547
+ value: String(config.discussion.driverTimeoutMs ?? 6e5),
1548
+ onChange: (e) => setConfig({
1549
+ ...config,
1550
+ discussion: {
1551
+ ...config.discussion,
1552
+ driverTimeoutMs: Math.max(1e3, Number(e.target.value))
1553
+ }
1554
+ }),
1555
+ style: settingsInputStyle
1052
1556
  }))), (0, react.createElement)("div", { style: { marginTop: 8 } }, label("置信度阈值(0-1,低于此值升级顾问群)"), (0, react.createElement)("input", {
1053
1557
  type: "number",
1054
1558
  min: 0,
@@ -1063,7 +1567,188 @@ function AdvisorGroupSettingsTab() {
1063
1567
  }
1064
1568
  }),
1065
1569
  style: settingsInputStyle
1066
- }))), (0, react.createElement)("div", { style: settingsSectionStyle }, (0, react.createElement)("div", { style: {
1570
+ }))), (0, react.createElement)("div", { style: settingsSectionStyle }, (0, react.createElement)("strong", null, "Jev 前置分类"), (0, react.createElement)("div", { style: {
1571
+ fontSize: 12,
1572
+ color: "var(--dsh-color-muted, #8b90a0)",
1573
+ margin: "4px 0 8px"
1574
+ } }, "启用后由 Jev 判断是否需要顾问群;未配置或调用失败时自动使用本地分类器。"), (0, react.createElement)("div", { style: {
1575
+ display: "flex",
1576
+ alignItems: "center",
1577
+ gap: 8,
1578
+ marginBottom: 8
1579
+ } }, (0, react.createElement)("input", {
1580
+ type: "checkbox",
1581
+ checked: config.trigger.jev?.enabled ?? false,
1582
+ onChange: (e) => setConfig({
1583
+ ...config,
1584
+ trigger: {
1585
+ ...config.trigger,
1586
+ jev: {
1587
+ ...config.trigger.jev ?? defaultJevConfig(),
1588
+ enabled: e.target.checked
1589
+ }
1590
+ }
1591
+ })
1592
+ }), label("启用 Jev")), (0, react.createElement)("div", { style: {
1593
+ display: "grid",
1594
+ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
1595
+ gap: 8
1596
+ } }, (0, react.createElement)("div", null, label("Jev 渠道"), (0, react.createElement)("select", {
1597
+ value: config.trigger.jev?.provider ?? "typesafe",
1598
+ onChange: (e) => {
1599
+ const current = config.trigger.jev ?? defaultJevConfig();
1600
+ const previousProvider = current.provider ?? "typesafe";
1601
+ const currentURL = current.baseURL ?? "";
1602
+ const usesDefault = !currentURL || currentURL === JEV_DEFAULT_ENDPOINTS[previousProvider];
1603
+ setConfig({
1604
+ ...config,
1605
+ trigger: {
1606
+ ...config.trigger,
1607
+ jev: {
1608
+ ...current,
1609
+ provider: e.target.value,
1610
+ ...usesDefault ? { baseURL: JEV_DEFAULT_ENDPOINTS[e.target.value] } : {}
1611
+ }
1612
+ }
1613
+ });
1614
+ },
1615
+ style: settingsInputStyle
1616
+ }, (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", {
1617
+ value: config.trigger.jev?.model ?? "jev-latest",
1618
+ onChange: (e) => setConfig({
1619
+ ...config,
1620
+ trigger: {
1621
+ ...config.trigger,
1622
+ jev: {
1623
+ ...config.trigger.jev ?? defaultJevConfig(),
1624
+ model: e.target.value
1625
+ }
1626
+ }
1627
+ }),
1628
+ style: settingsInputStyle
1629
+ })), (0, react.createElement)("div", null, label("判断阈值(0-1)"), (0, react.createElement)("input", {
1630
+ type: "number",
1631
+ min: 0,
1632
+ max: 1,
1633
+ step: .05,
1634
+ value: String(config.trigger.jev?.confidenceThreshold ?? .6),
1635
+ onChange: (e) => setConfig({
1636
+ ...config,
1637
+ trigger: {
1638
+ ...config.trigger,
1639
+ jev: {
1640
+ ...config.trigger.jev ?? defaultJevConfig(),
1641
+ confidenceThreshold: Number(e.target.value)
1642
+ }
1643
+ }
1644
+ }),
1645
+ style: settingsInputStyle
1646
+ })), (0, react.createElement)("div", null, label("高风险阈值(0-1,空=跟随判断阈值)"), (0, react.createElement)("input", {
1647
+ type: "number",
1648
+ min: 0,
1649
+ max: 1,
1650
+ step: .05,
1651
+ placeholder: String(config.trigger.jev?.confidenceThreshold ?? .6),
1652
+ value: config.trigger.jev?.highRiskThreshold !== void 0 ? String(config.trigger.jev.highRiskThreshold) : "",
1653
+ onChange: (e) => setConfig({
1654
+ ...config,
1655
+ trigger: {
1656
+ ...config.trigger,
1657
+ jev: {
1658
+ ...config.trigger.jev ?? defaultJevConfig(),
1659
+ highRiskThreshold: e.target.value === "" ? void 0 : Number(e.target.value)
1660
+ }
1661
+ }
1662
+ }),
1663
+ style: settingsInputStyle
1664
+ })), (0, react.createElement)("div", null, label("请求超时(毫秒)"), (0, react.createElement)("input", {
1665
+ type: "number",
1666
+ min: 1e3,
1667
+ max: 6e4,
1668
+ step: 1e3,
1669
+ value: String(config.trigger.jev?.timeoutMs ?? 1e4),
1670
+ onChange: (e) => setConfig({
1671
+ ...config,
1672
+ trigger: {
1673
+ ...config.trigger,
1674
+ jev: {
1675
+ ...config.trigger.jev ?? defaultJevConfig(),
1676
+ timeoutMs: Math.max(1e3, Number(e.target.value))
1677
+ }
1678
+ }
1679
+ }),
1680
+ style: settingsInputStyle
1681
+ }))), (0, react.createElement)("div", { style: { marginTop: 8 } }, label("API 地址(留空使用渠道默认地址)"), (0, react.createElement)("input", {
1682
+ value: config.trigger.jev?.baseURL || JEV_DEFAULT_ENDPOINTS[config.trigger.jev?.provider ?? "typesafe"],
1683
+ placeholder: JEV_DEFAULT_ENDPOINTS[config.trigger.jev?.provider ?? "typesafe"],
1684
+ onChange: (e) => setConfig({
1685
+ ...config,
1686
+ trigger: {
1687
+ ...config.trigger,
1688
+ jev: {
1689
+ ...config.trigger.jev ?? defaultJevConfig(),
1690
+ baseURL: e.target.value
1691
+ }
1692
+ }
1693
+ }),
1694
+ style: settingsInputStyle
1695
+ })), (0, react.createElement)("div", { style: {
1696
+ display: "grid",
1697
+ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
1698
+ gap: 8,
1699
+ marginTop: 8
1700
+ } }, (0, react.createElement)("div", null, label("Jev API Key"), (0, react.createElement)("input", {
1701
+ type: "password",
1702
+ placeholder: config.trigger.jev?.apiKeyMeta?.configured ? `已配置 ••••${config.trigger.jev.apiKeyMeta.last4 ?? ""}` : "输入 API Key",
1703
+ value: config.trigger.jev?.apiKey ?? "",
1704
+ onChange: (e) => setConfig({
1705
+ ...config,
1706
+ trigger: {
1707
+ ...config.trigger,
1708
+ jev: {
1709
+ ...config.trigger.jev ?? defaultJevConfig(),
1710
+ apiKey: e.target.value
1711
+ }
1712
+ }
1713
+ }),
1714
+ style: settingsInputStyle
1715
+ })), (0, react.createElement)("div", null, label("API Key 环境变量名(可选)"), (0, react.createElement)("input", {
1716
+ placeholder: "TYPESAFE_API_KEY 或 OPENROUTER_API_KEY",
1717
+ value: config.trigger.jev?.apiKeyEnv ?? "",
1718
+ onChange: (e) => setConfig({
1719
+ ...config,
1720
+ trigger: {
1721
+ ...config.trigger,
1722
+ jev: {
1723
+ ...config.trigger.jev ?? defaultJevConfig(),
1724
+ apiKeyEnv: e.target.value
1725
+ }
1726
+ }
1727
+ }),
1728
+ style: settingsInputStyle
1729
+ }))), (0, react.createElement)("div", { style: {
1730
+ display: "flex",
1731
+ alignItems: "center",
1732
+ gap: 8,
1733
+ marginTop: 8
1734
+ } }, (0, react.createElement)("input", {
1735
+ type: "checkbox",
1736
+ checked: config.trigger.jev?.useEnglishState ?? false,
1737
+ onChange: (e) => setConfig({
1738
+ ...config,
1739
+ trigger: {
1740
+ ...config.trigger,
1741
+ jev: {
1742
+ ...config.trigger.jev ?? defaultJevConfig(),
1743
+ useEnglishState: e.target.checked
1744
+ }
1745
+ }
1746
+ })
1747
+ }), label("Jev 英文判定(questionEn 概要)")), (0, react.createElement)("div", { style: {
1748
+ fontSize: 12,
1749
+ color: "var(--dsh-color-muted, #8b90a0)",
1750
+ margin: "4px 0 0"
1751
+ } }, "开启后,主模型提供的英文概要(questionEn)会作为 Jev 的判定输入;展示问句、会话记录与重复检测仍用中文。默认关闭。")), (0, react.createElement)("div", { style: settingsSectionStyle }, (0, react.createElement)("div", { style: {
1067
1752
  display: "flex",
1068
1753
  alignItems: "center",
1069
1754
  gap: 8
@@ -1364,7 +2049,55 @@ function AdvisorGroupSettingsTab() {
1364
2049
  marginTop: 4,
1365
2050
  resize: "vertical"
1366
2051
  }
1367
- }), (0, react.createElement)("div", { style: {
2052
+ }), (() => {
2053
+ const supportsTools = !providers.some((p) => p.id === advisor.provider && p.kind === "llm") || Boolean(advisor.baseURL || advisor.apiKeyEnv);
2054
+ const globalDefault = config.discussion.advisorTools ?? "readonly";
2055
+ const mode = advisor.tools ?? globalDefault;
2056
+ const modeLabel = {
2057
+ readonly: "只读工具",
2058
+ all: "全部会话工具(含可写,慎用)",
2059
+ off: "关闭"
2060
+ };
2061
+ const toolPromptNote = "工具启用时,系统提示词会自动追加:『对于你不了解、不熟悉的内容,尤其是项目背景、代码细节、仓库状态等不在知识范围内的情况,请优先使用联网工具(如 web_search、web_fetch)搜索核实后再作答,不要编造未核实的事实。』";
2062
+ if (supportsTools) return [
2063
+ label(`顾问工具调用范围(未单独设置时跟随全局默认:${modeLabel[globalDefault] ?? globalDefault})`),
2064
+ (0, react.createElement)("select", {
2065
+ "aria-label": "顾问工具调用范围",
2066
+ value: mode,
2067
+ onChange: (e) => updateAdvisor(index, { tools: e.target.value }),
2068
+ style: settingsInputStyle
2069
+ }, (0, react.createElement)("option", { value: "readonly" }, "只读工具"), (0, react.createElement)("option", { value: "all" }, "全部会话工具(含可写,慎用)"), (0, react.createElement)("option", { value: "off" }, "关闭")),
2070
+ mode === "off" ? null : (0, react.createElement)("div", { style: {
2071
+ fontSize: 12,
2072
+ color: "var(--dsh-color-muted, #8b90a0)",
2073
+ border: "1px dashed var(--dsh-color-border, #3a3f4b)",
2074
+ borderRadius: 4,
2075
+ padding: "4px 6px",
2076
+ marginTop: 4
2077
+ } }, toolPromptNote)
2078
+ ];
2079
+ return [label("顾问工具调用范围"), (0, react.createElement)("div", { style: {
2080
+ fontSize: 12,
2081
+ color: "var(--dsh-color-muted, #8b90a0)",
2082
+ border: "1px solid var(--dsh-color-border, #3a3f4b)",
2083
+ borderRadius: 4,
2084
+ padding: "4px 6px"
2085
+ } }, "❌ 当前为 DSH 内置通道(复用 DSH 凭据),不支持工具调用;为其配置 API 地址 / Key / 环境变量以走直连通道后即可启用(工具指引提示词也会随之生效)。")];
2086
+ })(), label("思考模式 / 思考强度(未设置 = 跟随模型与供应商默认)"), (0, react.createElement)("div", { style: {
2087
+ display: "flex",
2088
+ gap: 8,
2089
+ marginTop: 4
2090
+ } }, (0, react.createElement)("select", {
2091
+ "aria-label": "思考模式",
2092
+ value: advisor.thinking ?? "",
2093
+ onChange: (e) => updateAdvisor(index, { thinking: e.target.value === "" ? void 0 : e.target.value }),
2094
+ style: settingsInputStyle
2095
+ }, (0, react.createElement)("option", { value: "" }, "思考:默认"), (0, react.createElement)("option", { value: "enabled" }, "思考:开启"), (0, react.createElement)("option", { value: "disabled" }, "思考:关闭")), (0, react.createElement)("select", {
2096
+ "aria-label": "思考强度",
2097
+ value: advisor.reasoningEffort ?? "",
2098
+ onChange: (e) => updateAdvisor(index, { reasoningEffort: e.target.value === "" ? void 0 : e.target.value }),
2099
+ style: settingsInputStyle
2100
+ }, (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
2101
  display: "flex",
1369
2102
  gap: 8,
1370
2103
  marginTop: 4
@@ -1430,15 +2163,21 @@ function apply(ctx) {
1430
2163
  key: "advisor-group",
1431
2164
  locale: "chat"
1432
2165
  }, AdvisorGroupNodeView));
1433
- ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
1434
- name: "settings.plugin.item",
1435
- key: "advisor-group"
2166
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
2167
+ name: "settings.section",
2168
+ id: "dsh-advisor-group",
2169
+ order: 0,
2170
+ label: "顾问群"
1436
2171
  }, AdvisorGroupSettingsTab));
1437
2172
  }
1438
2173
 
1439
2174
  //#endregion
1440
2175
  exports.advisorGroupDefinition = advisorGroupDefinition;
1441
2176
  exports.apply = apply;
2177
+ exports.buildStepSequence = buildStepSequence;
2178
+ exports.buildToolStepViews = buildToolStepViews;
2179
+ exports.extractActionDescription = extractActionDescription;
1442
2180
  exports.inject = inject;
1443
2181
  exports.name = name;
2182
+ exports.sanitizeClientContent = sanitizeClientContent;
1444
2183
  return module.exports; } });