dsh-advisor-group 0.0.1 → 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
@@ -518,6 +518,10 @@ const waitingStyle = {
518
518
  color: "#fbbf24",
519
519
  marginTop: 6
520
520
  };
521
+ const noticeStyle = {
522
+ color: "#fda4af",
523
+ marginTop: 6
524
+ };
521
525
  /** Internal announcements that must never reach the user (the tool list). */
522
526
  const INTERNAL_TOOL_STEP_NAMES = /* @__PURE__ */ new Set(["⚙️ tools", "⚙ tools"]);
523
527
  /** True when the tool result represents a failure — the wrapper prefix, or a
@@ -784,6 +788,8 @@ function AdvisorSteps({ message }) {
784
788
  }, (0, react.createElement)("div", { style: thinkingTagStyle }, `${group} 📋 行动·${toolViews.length + 1}`), (0, react.createElement)("div", { style: descriptionBodyStyle }, liveAction)));
785
789
  return (0, react.createElement)("div", null, children);
786
790
  }
791
+ /** Auto-hide delay for transient card notices (request failures, resync). */
792
+ const NOTICE_TTL_MS = 6e3;
787
793
  function AdvisorGroupNodeView(props) {
788
794
  const data = props.node.data;
789
795
  const sessionId = data.sessionId;
@@ -791,10 +797,36 @@ function AdvisorGroupNodeView(props) {
791
797
  const [contextCollapsed, setContextCollapsed] = (0, react.useState)(true);
792
798
  const lastEventIdRef = (0, react.useRef)(0);
793
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
+ };
794
819
  (0, react.useEffect)(() => {
795
820
  if (!sessionId) return;
796
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
+ };
797
828
  es.onmessage = (event) => {
829
+ clearStreamNotice();
798
830
  try {
799
831
  const delta = JSON.parse(event.data);
800
832
  const bootId = delta.bootId ?? "";
@@ -848,11 +880,22 @@ function AdvisorGroupNodeView(props) {
848
880
  });
849
881
  } catch {}
850
882
  };
851
- es.addEventListener("resync", () => {
883
+ es.addEventListener("resync", (event) => {
852
884
  lastEventIdRef.current = 0;
853
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);
854
891
  });
855
- return () => es.close();
892
+ return () => {
893
+ es.close();
894
+ if (noticeTimerRef.current !== null) {
895
+ clearTimeout(noticeTimerRef.current);
896
+ noticeTimerRef.current = null;
897
+ }
898
+ };
856
899
  }, [sessionId]);
857
900
  const mergedMessages = data.messages.map((message) => {
858
901
  if (message.role !== "advisor") return message;
@@ -899,14 +942,21 @@ function AdvisorGroupNodeView(props) {
899
942
  }, data.status === "running" ? (0, react.createElement)("button", {
900
943
  type: "button",
901
944
  onClick: () => {
902
- fetch("/advisor-group/stop", {
903
- method: "POST",
904
- headers: {
905
- "content-type": "application/json",
906
- ...authHeaders()
907
- },
908
- body: JSON.stringify({ sessionId })
909
- }).catch(() => {});
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
+ })();
910
960
  },
911
961
  style: {
912
962
  cursor: "pointer",
@@ -920,14 +970,21 @@ function AdvisorGroupNodeView(props) {
920
970
  }, "⏹ 停止") : data.status === "cancelled" ? (0, react.createElement)("button", {
921
971
  type: "button",
922
972
  onClick: () => {
923
- fetch("/advisor-group/resume", {
924
- method: "POST",
925
- headers: {
926
- "content-type": "application/json",
927
- ...authHeaders()
928
- },
929
- body: JSON.stringify({ sessionId })
930
- }).catch(() => {});
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
+ })();
931
988
  },
932
989
  style: {
933
990
  cursor: "pointer",
@@ -953,7 +1010,32 @@ function AdvisorGroupNodeView(props) {
953
1010
  color: "#86efac",
954
1011
  fontWeight: 700,
955
1012
  marginBottom: 4
956
- } }, "📌 综合结论"), (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);
957
1039
  }
958
1040
  const settingsRootStyle = {
959
1041
  fontFamily: "'Inter', 'Segoe UI', system-ui, sans-serif",
@@ -1120,6 +1202,21 @@ const AVATAR_OPTIONS = [
1120
1202
  "🚀",
1121
1203
  "🛡️"
1122
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
+ };
1123
1220
  function AdvisorGroupSettingsTab() {
1124
1221
  const [config, setConfig] = (0, react.useState)(null);
1125
1222
  const [providers, setProviders] = (0, react.useState)([]);
@@ -1268,6 +1365,10 @@ function AdvisorGroupSettingsTab() {
1268
1365
  try {
1269
1366
  const configPayload = {
1270
1367
  ...config,
1368
+ trigger: {
1369
+ ...config.trigger,
1370
+ jev: config.trigger.jev ? (({ apiKeyMeta: _meta, ...rest }) => rest)(config.trigger.jev) : defaultJevConfig()
1371
+ },
1271
1372
  advisors: config.advisors.map(({ apiKeyMetaByProvider: _meta, ...rest }) => rest)
1272
1373
  };
1273
1374
  const data = await (await fetch("/advisor-group/config", {
@@ -1414,7 +1515,188 @@ function AdvisorGroupSettingsTab() {
1414
1515
  }
1415
1516
  }),
1416
1517
  style: settingsInputStyle
1417
- }))), (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: {
1418
1700
  display: "flex",
1419
1701
  alignItems: "center",
1420
1702
  gap: 8
@@ -1829,9 +2111,11 @@ function apply(ctx) {
1829
2111
  key: "advisor-group",
1830
2112
  locale: "chat"
1831
2113
  }, AdvisorGroupNodeView));
1832
- ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
1833
- name: "settings.plugin.item",
1834
- 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: "顾问群"
1835
2119
  }, AdvisorGroupSettingsTab));
1836
2120
  }
1837
2121
 
package/lib/index.d.mts CHANGED
@@ -99,7 +99,7 @@ interface DiscussionConfig {
99
99
  * a read-only whitelist of the session's tools (read/grep/glob/web_search…);
100
100
  * 'all' exposes every session-visible tool (incl. writable ones); 'off'
101
101
  * disables tool calling. Only the direct-http OpenAI/Anthropic channel
102
- * supports tool calling (ctx.llm / dsh-llm 0.1.2-rc.1 does not).
102
+ * supports tool calling (ctx.llm / dsh-llm does not expose tool calling).
103
103
  */
104
104
  advisorTools?: 'readonly' | 'all' | 'off';
105
105
  /**
@@ -112,6 +112,26 @@ interface TriggerConfig {
112
112
  requireClassifier: boolean;
113
113
  allowWebFallback: boolean;
114
114
  confidenceThreshold: number;
115
+ jev?: JevConfig;
116
+ }
117
+ type JevProvider = 'typesafe' | 'openrouter';
118
+ interface JevConfig {
119
+ enabled: boolean;
120
+ provider: JevProvider;
121
+ model: string;
122
+ baseURL?: string;
123
+ apiKey?: string;
124
+ apiKeyEnv?: string;
125
+ timeoutMs: number;
126
+ confidenceThreshold: number;
127
+ /** Judge with the caller-provided English gist (questionEn) as the Jev state; default keeps the Chinese question. */
128
+ useEnglishState?: boolean;
129
+ /** Threshold for the high_risk question; unset means follow confidenceThreshold. */
130
+ highRiskThreshold?: number;
131
+ apiKeyMeta?: {
132
+ configured: boolean;
133
+ last4?: string;
134
+ };
115
135
  }
116
136
  interface UiConfig {
117
137
  theme: 'retro-green' | 'retro-amber' | 'retro-blue';