dsh-advisor-group 0.1.1 → 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/README.md CHANGED
@@ -17,6 +17,7 @@
17
17
  - **Three ways to activate** — `@顾问群` mention (force-start), same question repeated 3 times without resolution, or main-model self-assessed confidence below the threshold.
18
18
  - **Optional Jev semantic pre-classification (off by default)** — when `trigger.jev.enabled` is on, a configured Jev model judges non-force-started consultations first (escalation / high-risk / web-search fit, thresholds under `trigger.jev.*`); if Jev is unavailable the local rules-based classifier still decides. With `trigger.jev.useEnglishState`, the judge reads the caller-supplied English gist (`questionEn`) instead of the Chinese question — display and session records stay Chinese.
19
19
  - **Retro CRT chat cards** — green/amber/blue CRT themes, scanlines, LIVE/DONE headers, auto-expanded thinking panel with auto-scroll; advisor Markdown rendered with a link-protocol whitelist (headings, lists, code, quotes, links, tables).
20
+ - **Ask another session (`ask_session`)** — hand one question to another live session — by exact session id, exact title, or a natural partial name such as the workspace name (`极简遥控器`) — and read what that session said between the delivery receipt and its next whole-session idle. The result is explicitly **not** a one-to-one reply, and the asking side renders the exchange in the same retro chat-group card. Guards: hop ≤ 2, no self/subagent target, one wait per session, a 32 KB payload budget; the target's turn is never cancelled.
20
21
  - **Provider presets (11 platforms · 26 presets)** — DeepSeek, Moonshot Kimi, Kimi Code, Aliyun Bailian, Zhipu AI, OpenAI, Claude, Gemini, SiliconFlow, AIHubMix, OpenRouter (OpenAI/Anthropic-compatible variants included).
21
22
  - **Security-minded by design** — API keys use official `SecretField` semantics (never returned to the browser; `apiKeysByProvider` key history is server-side only), SSRF-guarded diagnostics (https-only / loopback, no IP literals, no redirects), per-boot token auth on `/advisor-group/*` routes, and an atomic **configurable daily consultation cap** (default 50, can be disabled) persisted across restarts.
22
23
  - **Runtime toggle** — `toggle_advisor_group` enables/disables the plugin and persists the flag to settings.
@@ -58,7 +59,7 @@ npx @deepseek-ai/dsh web
58
59
  - type `@顾问群` in your question to force a consultation, or
59
60
  - ask a professional/uncertain question — the plugin escalates automatically when appropriate.
60
61
 
61
- `ask_advisors` tools available to the model: `ask_advisors`, `toggle_advisor_group`.
62
+ `ask_advisors` tools available to the model: `ask_advisors`, `toggle_advisor_group`, `ask_session`.
62
63
 
63
64
  ## ⚙️ Configuration
64
65
 
@@ -100,6 +101,8 @@ One consultation is not one call but a chain of them. With the defaults (`maxRou
100
101
 
101
102
  Billing follows each channel: advisors bill on the route and model you gave them, while the driver bills on the current session's model. To keep costs down, lower `maxRounds`, configure fewer advisors, set `tools: 'off'` for advisors, and pick a daily cap you are comfortable with. Resuming after an interruption ("▶ continue") only asks the advisors that had not finished, so it costs less than starting over.
102
103
 
104
+ `ask_session` is outside the consultation quota: it wakes the target session, which answers with one ordinary turn billed on that session's own model, and it never counts as a new consultation.
105
+
103
106
  ## 🔒 Security & privacy
104
107
 
105
108
  - Direct API keys can be stored in the DSH `settings.yaml` (marked secret, never returned to the browser by `describe`); prefer `apiKeyEnv` (env-var mode) if you don't want keys on disk.
@@ -113,6 +116,7 @@ Billing follows each channel: advisors bill on the route and model you gave them
113
116
 
114
117
  - A hard crash of the DSH host can leave an already-open card showing **LIVE** until the page is refreshed (a disconnect notice now appears); refresh restores the true state from the session log.
115
118
  - Risk notes are **fact-based, never text-based**: a consultation reports truncation (an advisor stream cut off by timeout or network) and cancellation/stop. Users see them on **stopped or completed** cards and in the session log, and the main model receives them in the `ask_advisors` result. Advisor prose is not scanned for keywords — the earlier text heuristic was removed in 0.1.1 because it also fired on plain negations such as 「没有任何风险」.
119
+ - Cross-session asking (`ask_session`) has no settings switch yet, so the tool is visible in every session and relies on its own description to keep use to explicit user requests; two sessions asking each other at the same time is not detected.
116
120
 
117
121
  ## 🛠️ Development
118
122
 
package/README.zh.md CHANGED
@@ -19,6 +19,7 @@
19
19
  - **三种触发方式**:`@顾问群` 提及(强制启动)、同一问题重复 3 次未解决、主模型自评置信度低于阈值。
20
20
  - **Jev 语义前置分类(可选,默认关闭)**:开启 `trigger.jev.enabled` 后,未强制启动的咨询先由配置的 Jev 模型语义判断(是否该找顾问 / 是否高风险 / 是否更适合联网,阈值在 `trigger.jev.*`);Jev 不可用时仍由本地规则分类器判定。`trigger.jev.useEnglishState` 开启后,判定改读主模型附带的英文概要(`questionEn`),展示与会话记录仍用中文。
21
21
  - **复古 CRT 聊天组卡片**:绿/琥珀/蓝三主题、扫描线、LIVE/DONE 标题、💭 思考面板默认展开自动滚底;顾问正文走轻量 Markdown 渲染(链接协议白名单,支持标题/列表/代码/引用/链接/表格)。
22
+ - **跨会话提问(`ask_session`)**:把一个问题交给另一个**活着的**会话——用完整会话号、一字不差的标题,或者自然的名字(例如工作目录名「极简遥控器」)都可以——并唤醒它;拿回的是**从投递回执到对方下一次整体空闲**之间它说的话,明确**不是**逐条对应的回答。提问方一侧用同一套复古聊天组卡片呈现这次对话。护栏:跳数 ≤ 2、不能问自己或子会话、同一会话同时只等一个、载荷上限 32 KB;**永不取消对方的回合**。
22
23
  - **供应商预设(11 平台 · 26 预设)**:DeepSeek、月之暗面 Kimi、Kimi Code、阿里云百炼、智谱 AI、OpenAI、Claude、Gemini、硅基流动、AIHubMix、OpenRouter(含 OpenAI / Anthropic 兼容变体)。
23
24
  - **注重安全**:API Key 采用官方 `SecretField` 语义(浏览器永不回显;`apiKeysByProvider` 供应商密钥档案仅存服务端);诊断端点 SSRF 加固(仅 https/loopback、拒绝 IP 字面量与重定向);`/advisor-group/*` 路由启动期 token 鉴权;每日新咨询**可配置原子配额**(默认 50,可关闭),持久化跨重启。
24
25
  - **运行时开关**:`toggle_advisor_group` 启停插件并持久化到设置。
@@ -61,7 +62,7 @@ npx @deepseek-ai/dsh web
61
62
  - 提问时带 `@顾问群` 强制发起咨询,或
62
63
  - 直接提专业/不确定的问题——插件会按需自动升级。
63
64
 
64
- 模型可用工具:`ask_advisors`、`toggle_advisor_group`。
65
+ 模型可用工具:`ask_advisors`、`toggle_advisor_group`、`ask_session`。
65
66
 
66
67
  ## ⚙️ 配置项
67
68
 
@@ -103,6 +104,8 @@ npx @deepseek-ai/dsh web
103
104
 
104
105
  费用分别结算:顾问按你给它们配的通道与模型计费,驱动模型按当前会话的模型计费。想控成本,可以降 `maxRounds`、减少顾问数、给顾问设 `tools: 'off'`,再把每日上限设成你能接受的数字。中断后用「▶ 继续聊天」续跑只补问没答完的顾问,不会整场重来,代价低于重开一次。
105
106
 
107
+ `ask_session` 不走咨询配额:它唤醒目标会话,对方用一次普通回合作答、按其自身模型计费,也不会被算作一次新咨询。
108
+
106
109
  ## 🔒 安全与隐私
107
110
 
108
111
  - 直连 API Key 可存于 DSH `settings.yaml`(标 secret,官方 `describe` 绝不回传浏览器);不想落盘请用 `apiKeyEnv` 环境变量模式。
@@ -116,6 +119,7 @@ npx @deepseek-ai/dsh web
116
119
 
117
120
  - DSH 宿主进程硬崩溃时,已打开的卡片在刷新前可能仍显示 **LIVE**(现在会先出现断线提示);刷新后按会话日志恢复真实状态。
118
121
  - 风险提示**只看事实、不看措辞**:截断(顾问输出被超时或网络中断)与取消(本轮咨询被停止)两种。用户可在**已停止或已完成**的卡片与会话日志中看到,主模型随 `ask_advisors` 返回收到。顾问正文不做关键词扫描——早期那套「凭字眼猜有没有保留意见」的启发式已在 0.1.1 移除,原因是它连「没有任何风险」这样的否定句也会当成风险提示(盲测里 4 条纯否定句全部误报)。
122
+ - 跨会话提问(`ask_session`)目前**没有设置开关**,工具在所有会话里可见,靠工具说明把使用限制在「用户明确要求时」;两个会话互相等待的情况尚未检测。
119
123
 
120
124
  ## 🛠️ 开发
121
125
 
package/lib/client.js CHANGED
@@ -283,12 +283,60 @@ 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;
292
340
  }
293
341
  /**
294
342
  * Browser-side twin of `sanitizeAdvisorContent` (host module cannot be
@@ -315,7 +363,8 @@ function viewData(state) {
315
363
  ...state.context === void 0 ? {} : { context: state.context },
316
364
  advisors: state.advisors,
317
365
  messages: state.messages,
318
- ...state.summary === void 0 ? {} : { summary: state.summary }
366
+ ...state.summary === void 0 ? {} : { summary: state.summary },
367
+ ...state.mode === void 0 ? {} : { mode: state.mode }
319
368
  };
320
369
  }
321
370
  const advisorGroupDefinition = {
@@ -335,7 +384,7 @@ const advisorGroupDefinition = {
335
384
  start: (_context, match) => {
336
385
  if (match.event.type !== "advisor-group/start") throw new Error("advisor-group requires advisor-group/start");
337
386
  const data = match.event.data;
338
- return {
387
+ const base = {
339
388
  sessionId: data.sessionId,
340
389
  turn: data.turn,
341
390
  step: data.step,
@@ -351,6 +400,11 @@ const advisorGroupDefinition = {
351
400
  }],
352
401
  status: "running"
353
402
  };
403
+ if (data.kind === "ask") return {
404
+ ...base,
405
+ mode: "ask"
406
+ };
407
+ return base;
354
408
  },
355
409
  update: (context, match) => {
356
410
  if (match.event.type === "advisor-group/message") {
@@ -710,11 +764,11 @@ function buildStepSequence(actionDescriptions, thinkingSegments, toolViews) {
710
764
  * 💭 思考·1 → ⛭ 工具×1(输入/输出)→ 💭 思考·2 → ⛭ 工具×2 → … → 📄 正文
711
765
  * Main/system messages stay a single collapsible row.
712
766
  */
713
- function AdvisorSteps({ message }) {
767
+ function AdvisorSteps({ message, plain = false }) {
714
768
  const isMain = message.role === "main";
715
769
  const isSystem = message.role === "system";
716
770
  const label = isMain ? "YOU" : isSystem ? "SYSTEM" : message.advisorName ?? message.advisorId ?? "ADVISOR";
717
- const group = isMain ? "YOU" : isSystem ? "SYSTEM" : `${label} · R${message.round ?? 1}`;
771
+ const group = isMain || isSystem || plain ? label : `${label} · R${message.round ?? 1}`;
718
772
  const truncated = message.truncated;
719
773
  const toolViews = buildToolStepViews(message.toolSteps);
720
774
  const thinkingSegments = message.thinkingSegments && message.thinkingSegments.length > 0 ? message.thinkingSegments : message.thinking ? [message.thinking] : [];
@@ -767,7 +821,7 @@ function AdvisorSteps({ message }) {
767
821
  if (liveToolPhase) continue;
768
822
  children.push((0, react.createElement)(StepRow, {
769
823
  key: `body-${index}`,
770
- title: `${group} 📄 正文`,
824
+ title: plain ? group : `${group} 📄 正文`,
771
825
  badges: truncated ? [(0, react.createElement)("span", { style: badgeStyle("#f87171") }, truncated.reason === "timeout" ? "⏱ 响应超时截断" : "⚠ 流中断")] : [],
772
826
  defaultOpen: true,
773
827
  color: "#4ade80",
@@ -793,6 +847,7 @@ const NOTICE_TTL_MS = 6e3;
793
847
  function AdvisorGroupNodeView(props) {
794
848
  const data = props.node.data;
795
849
  const sessionId = data.sessionId;
850
+ const isAsk = data.mode === "ask";
796
851
  const [live, setLive] = (0, react.useState)({});
797
852
  const [contextCollapsed, setContextCollapsed] = (0, react.useState)(true);
798
853
  const lastEventIdRef = (0, react.useRef)(0);
@@ -816,9 +871,20 @@ function AdvisorGroupNodeView(props) {
816
871
  const clearStreamNotice = () => {
817
872
  setTransientNotice((prev) => prev && prev.kind === "stream" ? null : prev);
818
873
  };
874
+ const [streamTokenReady, setStreamTokenReady] = (0, react.useState)(() => currentAdvisorGroupToken() !== "");
875
+ (0, react.useEffect)(() => {
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]);
819
885
  (0, react.useEffect)(() => {
820
- if (!sessionId) return;
821
- const es = new EventSource(`/advisor-group/stream?sessionId=${encodeURIComponent(sessionId)}&lastEventId=${lastEventIdRef.current}&bootId=${encodeURIComponent(lastBootIdRef.current)}&token=${encodeURIComponent(advisorGroupToken())}`);
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())}`);
822
888
  es.onopen = () => {
823
889
  clearStreamNotice();
824
890
  };
@@ -896,7 +962,7 @@ function AdvisorGroupNodeView(props) {
896
962
  noticeTimerRef.current = null;
897
963
  }
898
964
  };
899
- }, [sessionId]);
965
+ }, [sessionId, streamTokenReady]);
900
966
  const mergedMessages = data.messages.map((message) => {
901
967
  if (message.role !== "advisor") return message;
902
968
  const advisorId = message.advisorId ?? "";
@@ -927,7 +993,7 @@ function AdvisorGroupNodeView(props) {
927
993
  };
928
994
  });
929
995
  const shortId = data.sessionId.length > 8 ? data.sessionId.slice(0, 8) : data.sessionId;
930
- const title = data.status === "completed" ? "ADVISOR GROUP · DONE" : data.status === "cancelled" ? "ADVISOR GROUP · STOPPED" : "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";
931
997
  return (0, react.createElement)("div", {
932
998
  style: rootStyle,
933
999
  role: "log",
@@ -939,17 +1005,14 @@ function AdvisorGroupNodeView(props) {
939
1005
  alignItems: "center",
940
1006
  gap: 8
941
1007
  }
942
- }, data.status === "running" ? (0, react.createElement)("button", {
1008
+ }, data.status === "running" && !isAsk ? (0, react.createElement)("button", {
943
1009
  type: "button",
944
1010
  onClick: () => {
945
1011
  (async () => {
946
1012
  try {
947
- const body = await (await fetch("/advisor-group/stop", {
1013
+ const body = await (await authedFetch("/advisor-group/stop", {
948
1014
  method: "POST",
949
- headers: {
950
- "content-type": "application/json",
951
- ...authHeaders()
952
- },
1015
+ headers: { "content-type": "application/json" },
953
1016
  body: JSON.stringify({ sessionId })
954
1017
  })).json();
955
1018
  if (body.ok === false) showTransientNotice(body.error ?? "请求未被服务端接受,请稍后重试。", "action", NOTICE_TTL_MS);
@@ -967,17 +1030,14 @@ function AdvisorGroupNodeView(props) {
967
1030
  padding: "2px 8px",
968
1031
  fontSize: 11
969
1032
  }
970
- }, "⏹ 停止") : data.status === "cancelled" ? (0, react.createElement)("button", {
1033
+ }, "⏹ 停止") : data.status === "cancelled" && !isAsk ? (0, react.createElement)("button", {
971
1034
  type: "button",
972
1035
  onClick: () => {
973
1036
  (async () => {
974
1037
  try {
975
- const body = await (await fetch("/advisor-group/resume", {
1038
+ const body = await (await authedFetch("/advisor-group/resume", {
976
1039
  method: "POST",
977
- headers: {
978
- "content-type": "application/json",
979
- ...authHeaders()
980
- },
1040
+ headers: { "content-type": "application/json" },
981
1041
  body: JSON.stringify({ sessionId })
982
1042
  })).json();
983
1043
  if (body.ok === false) showTransientNotice(body.error ?? "请求未被服务端接受,请稍后重试。", "action", NOTICE_TTL_MS);
@@ -999,9 +1059,10 @@ function AdvisorGroupNodeView(props) {
999
1059
  type: "button",
1000
1060
  onClick: () => setContextCollapsed((value) => !value),
1001
1061
  style: contextToggleStyle
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, {
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, {
1003
1063
  key: index,
1004
- message
1064
+ message,
1065
+ plain: isAsk
1005
1066
  })), data.status === "completed" && data.summary?.conclusion ? (0, react.createElement)("div", { style: {
1006
1067
  borderTop: "1px dashed #2f9e44",
1007
1068
  marginTop: 8,
@@ -1010,7 +1071,7 @@ function AdvisorGroupNodeView(props) {
1010
1071
  color: "#86efac",
1011
1072
  fontWeight: 700,
1012
1073
  marginBottom: 4
1013
- } }, "📌 综合结论"), (0, react.createElement)("div", null, renderMarkdown(data.summary.conclusion))) : null, data.status === "completed" && (data.summary?.riskNotes ?? []).length > 0 ? (0, react.createElement)("div", { style: {
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: {
1014
1075
  borderTop: "1px dashed #2f9e44",
1015
1076
  marginTop: 8,
1016
1077
  paddingTop: 8
@@ -1035,7 +1096,7 @@ function AdvisorGroupNodeView(props) {
1035
1096
  fontSize: 12,
1036
1097
  marginTop: 2
1037
1098
  }
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);
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);
1039
1100
  }
1040
1101
  const settingsRootStyle = {
1041
1102
  fontFamily: "'Inter', 'Segoe UI', system-ui, sans-serif",
@@ -1235,7 +1296,7 @@ function AdvisorGroupSettingsTab() {
1235
1296
  setMsg("");
1236
1297
  setError("");
1237
1298
  try {
1238
- 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())]);
1239
1300
  if (configData.ok && configData.config) {
1240
1301
  setConfig(configData.config);
1241
1302
  savedJsonRef.current = JSON.stringify(configData.config);
@@ -1291,12 +1352,9 @@ function AdvisorGroupSettingsTab() {
1291
1352
  setError("");
1292
1353
  setMsg("");
1293
1354
  try {
1294
- const data = await (await fetch("/advisor-group/models", {
1355
+ const data = await (await authedFetch("/advisor-group/models", {
1295
1356
  method: "POST",
1296
- headers: {
1297
- "content-type": "application/json",
1298
- ...authHeaders()
1299
- },
1357
+ headers: { "content-type": "application/json" },
1300
1358
  body: JSON.stringify({
1301
1359
  advisorId: advisor.id,
1302
1360
  provider: advisor.provider,
@@ -1332,12 +1390,9 @@ function AdvisorGroupSettingsTab() {
1332
1390
  setError("");
1333
1391
  setMsg("");
1334
1392
  try {
1335
- const data = await (await fetch("/advisor-group/test-connection", {
1393
+ const data = await (await authedFetch("/advisor-group/test-connection", {
1336
1394
  method: "POST",
1337
- headers: {
1338
- "content-type": "application/json",
1339
- ...authHeaders()
1340
- },
1395
+ headers: { "content-type": "application/json" },
1341
1396
  body: JSON.stringify({
1342
1397
  advisorId: advisor.id,
1343
1398
  provider: advisor.provider,
@@ -1371,12 +1426,9 @@ function AdvisorGroupSettingsTab() {
1371
1426
  },
1372
1427
  advisors: config.advisors.map(({ apiKeyMetaByProvider: _meta, ...rest }) => rest)
1373
1428
  };
1374
- const data = await (await fetch("/advisor-group/config", {
1429
+ const data = await (await authedFetch("/advisor-group/config", {
1375
1430
  method: "POST",
1376
- headers: {
1377
- "content-type": "application/json",
1378
- ...authHeaders()
1379
- },
1431
+ headers: { "content-type": "application/json" },
1380
1432
  body: JSON.stringify({
1381
1433
  config: configPayload,
1382
1434
  expectedRevision: revision