psyclaw 0.30.4 → 0.30.6

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.
Files changed (35) hide show
  1. package/README.md +6 -5
  2. package/dist/src/adapters/pi/extension.js +234 -5
  3. package/dist/src/adapters/pi/extension.js.map +1 -1
  4. package/dist/src/ars/pi-panel-executor.js +14 -4
  5. package/dist/src/ars/pi-panel-executor.js.map +1 -1
  6. package/dist/src/ars/re-review.js +14 -4
  7. package/dist/src/ars/re-review.js.map +1 -1
  8. package/dist/src/index.d.ts +1 -0
  9. package/dist/src/index.js +1 -0
  10. package/dist/src/index.js.map +1 -1
  11. package/dist/src/observability/llm.js +3 -1
  12. package/dist/src/observability/llm.js.map +1 -1
  13. package/dist/src/observability/pi-rpc.d.ts +15 -0
  14. package/dist/src/observability/pi-rpc.js +48 -0
  15. package/dist/src/observability/pi-rpc.js.map +1 -0
  16. package/dist/src/session/help.js +2 -1
  17. package/dist/src/session/help.js.map +1 -1
  18. package/dist/src/skills/biosignal-pack.d.ts +23 -0
  19. package/dist/src/skills/biosignal-pack.js +93 -0
  20. package/dist/src/skills/biosignal-pack.js.map +1 -0
  21. package/dist/src/style/cli-ui.js +2 -1
  22. package/dist/src/style/cli-ui.js.map +1 -1
  23. package/dist/src/tui/biosignal-wizard.d.ts +43 -0
  24. package/dist/src/tui/biosignal-wizard.js +149 -0
  25. package/dist/src/tui/biosignal-wizard.js.map +1 -0
  26. package/dist/src/tui/provider-picker.d.ts +22 -0
  27. package/dist/src/tui/provider-picker.js +58 -0
  28. package/dist/src/tui/provider-picker.js.map +1 -1
  29. package/dist/src/tui/skill-manager.d.ts +2 -0
  30. package/dist/src/tui/skill-manager.js +29 -3
  31. package/dist/src/tui/skill-manager.js.map +1 -1
  32. package/package.json +1 -1
  33. package/skills/recommended/biosignal-pack.json +63 -0
  34. package/skills/recommended/catalog.json +211 -46
  35. package/skills/recommended/mcp-catalog.json +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@ PsyClaw 是面向社会科学研究的智能体工作台。它把研究项目、
4
4
 
5
5
  PsyClaw 自有代码使用 MIT 许可证;随 npm 包内置的 Academic Research Skills 位于 `vendor/ars`,保持上游署名并单独遵循 CC BY-NC 4.0,仅限非商业用途。
6
6
 
7
- 当前版本:`0.30.4`。正式命令、用户配置目录和后续发布统一使用 `psyclaw`。
7
+ 当前版本:`0.30.6`。正式命令、用户配置目录和后续发布统一使用 `psyclaw`。
8
8
 
9
9
  ## 发布流程
10
10
 
@@ -21,19 +21,19 @@ RELEASE_MESSAGE="release: describe the change" pnpm release:push
21
21
  需要 Node.js `>=22.19.0`。官方 npm 源:
22
22
 
23
23
  ```powershell
24
- npm install -g psyclaw@0.30.4
24
+ npm install -g psyclaw@0.30.6
25
25
  ```
26
26
 
27
27
  如果本机 npm 配置把 registry 误写成带有 `~/` 的地址,请显式指定官方源:
28
28
 
29
29
  ```bash
30
- npm install -g psyclaw@0.30.4 --registry=https://registry.npmjs.org/
30
+ npm install -g psyclaw@0.30.6 --registry=https://registry.npmjs.org/
31
31
  ```
32
32
 
33
33
  中国大陆网络较慢或无法访问官方源时:
34
34
 
35
35
  ```powershell
36
- npm install -g psyclaw@0.30.4 --registry=https://registry.npmmirror.com
36
+ npm install -g psyclaw@0.30.6 --registry=https://registry.npmmirror.com
37
37
  ```
38
38
 
39
39
  或安装脚本:
@@ -46,7 +46,7 @@ PSYCLAW_CN=1 curl -fsSL https://exekiel179.github.io/psyclaw/install.sh | sh
46
46
  ```powershell
47
47
  # Windows PowerShell
48
48
  irm https://exekiel179.github.io/psyclaw/install.ps1 | iex
49
- # 可选:$env:PSYCLAW_CN = "1";$env:PSYCLAW_VERSION = "0.30.4"
49
+ # 可选:$env:PSYCLAW_CN = "1";$env:PSYCLAW_VERSION = "0.30.6"
50
50
  ```
51
51
 
52
52
  检测到国内 npm registry(如 npmmirror)、`PSYCLAW_CN=1` 或 `PSYCLAW_GITHUB_MIRROR` 时,内置路径统一走国内可用路由:
@@ -218,3 +218,4 @@ rm -rf ~/.psyclaw
218
218
  - 遥测默认开启(匿名产品使用与错误,不含研究正文)。首次启动会说明如何关闭:`psyclaw telemetry off`。详见 [docs/telemetry.md](docs/telemetry.md)。
219
219
 
220
220
  许可证:MIT。
221
+
@@ -16,13 +16,16 @@ import { dirname } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { coreSkillNames, enabledRecommendedSkillPaths, normalizeRecommendedSkillId, readRecommendationState, readRecommendedCatalog, recommendedSkillTarget, saveRecommendationState, validateModelInstalledRecommendedSkill, } from "../../skills/recommended.js";
18
18
  import { SkillManagerComponent } from "../../tui/skill-manager.js";
19
+ import { BiosignalWizardComponent } from "../../tui/biosignal-wizard.js";
20
+ import { readBiosignalPackCatalog, resolveBiosignalInstallPlan, } from "../../skills/biosignal-pack.js";
19
21
  import { WakeOptionsComponent } from "../../tui/wake-options.js";
20
22
  import { applyWakeVerifySync, buildWakePrompt, createWakeVerifyChecklist, formatWakeResult, labelsFor, waitForPanelWakeAnswer, } from "../../wake-options/runtime.js";
21
23
  import { panelHub } from "../../panel/hub.js";
22
24
  import { appendChoiceRecord } from "../../research/choice.js";
23
25
  import { enabledLocalSkillPaths, enabledLocalPromptPaths, readUserSkillState, scanLocalSkills, setLocalSkillEnabled, setLocalSkillsEnabled, skillNamesInPaths, userSkillId, } from "../../skills/user-skills.js";
24
26
  import { RuntimeMcpRegistry, setUserMcpConfigEnabled, } from "../../integrations/mcp-runtime.js";
25
- import { SecretInputComponent, ProviderPickerComponent, } from "../../tui/provider-picker.js";
27
+ import { SecretInputComponent, TextInputComponent, ProviderPickerComponent, } from "../../tui/provider-picker.js";
28
+ import { resolveProviderBaseUrl } from "../../core/provider-endpoint.js";
26
29
  import { isArsPiActive, isArsPiTurn, psyclawArsPatch, setArsPiSessionActive, } from "../../ars/profile.js";
27
30
  import { buildArsDoctorReport, ensurePdfEngineForExport } from "../../ars/doctor.js";
28
31
  import { formatAcademicSoftRouteInvocation, rankAcademicSoftRoutes, resolveAcademicSoftRoute, } from "../../ars/academic-router.js";
@@ -131,6 +134,48 @@ async function promptProviderKey(ctx, providerName, envName) {
131
134
  const result = await ctx.ui.custom((tui, theme, keybindings, done) => new SecretInputComponent(`配置 ${providerName}`, envName, tui, theme, keybindings, done));
132
135
  return result.type === "submit" ? result.value.trim() : undefined;
133
136
  }
137
+ async function promptProviderText(ctx, title, hint, initial = "") {
138
+ if (typeof ctx.ui.custom !== "function") {
139
+ throw new Error(`${title}:当前环境不支持交互输入。请在交互式终端运行 /provider。`);
140
+ }
141
+ const result = await ctx.ui.custom((tui, theme, keybindings, done) => new TextInputComponent(title, hint, tui, theme, keybindings, done, initial));
142
+ return result.type === "submit" ? result.value.trim() : undefined;
143
+ }
144
+ /** Interactive setup for the built-in `custom` OpenAI-compatible provider. */
145
+ async function configureCustomProvider(ctx) {
146
+ const preset = PROVIDER_PRESETS.find((item) => item.id === "custom");
147
+ if (!preset)
148
+ throw new Error("内置 custom Provider 预设缺失");
149
+ if (!ctx.hasUI || typeof ctx.ui.custom !== "function") {
150
+ throw new Error("请在交互式终端运行 /provider,选择「自定义 OpenAI 兼容接口」,并填写 Base URL 与模型 ID");
151
+ }
152
+ const baseUrlRaw = await promptProviderText(ctx, "自定义 Provider · Base URL", "例如 https://api.example.com/v1(须为 http/https,不含凭据)");
153
+ if (baseUrlRaw === undefined)
154
+ return undefined;
155
+ let baseUrl = "";
156
+ try {
157
+ baseUrl = resolveProviderBaseUrl(baseUrlRaw);
158
+ }
159
+ catch (error) {
160
+ throw new Error(error instanceof Error ? error.message : "Base URL 无效");
161
+ }
162
+ if (!baseUrl)
163
+ throw new Error("自定义 Provider 必须填写 Base URL");
164
+ const modelId = await promptProviderText(ctx, "自定义 Provider · 模型 ID", "填写网关要求的模型名,例如 gpt-4o-mini 或 deepseek-chat");
165
+ if (modelId === undefined)
166
+ return undefined;
167
+ if (!modelId || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/.test(modelId)) {
168
+ throw new Error("模型 ID 无效;请使用字母数字及 . _ : / -");
169
+ }
170
+ const key = await promptProviderKey(ctx, preset.name, preset.apiKeyEnv);
171
+ if (key === undefined)
172
+ return undefined;
173
+ const credential = await providerCredentialSource(preset);
174
+ if (!key && credential === "missing") {
175
+ throw new Error(`未找到 ${preset.apiKeyEnv};请输入 API Key 后再继续`);
176
+ }
177
+ return { baseUrl, modelId, ...(key ? { apiKey: key } : {}) };
178
+ }
134
179
  function parseModelRef(args) {
135
180
  const value = args.trim();
136
181
  const slash = value.indexOf("/");
@@ -758,6 +803,147 @@ async function queueModelSkillInstall(pi, ctx, row) {
758
803
  ctx.ui.notify(`已将 ${row.name} 的安装任务交给当前模型(默认启用),目标为${skillScopeLabel(scope)}。完成后请执行 /reload。`, "info");
759
804
  });
760
805
  }
806
+ function modelBiosignalPackInstallTask(root, plan, skillRows, mcpRows, mcpPlans, skillScope) {
807
+ const skillLines = plan.skillIds.map((id) => {
808
+ const row = skillRows.find((candidate) => candidate.id === id);
809
+ const target = recommendedSkillTarget(root, id, skillScope);
810
+ return [
811
+ `- Skill ${row?.name ?? id} (${id})`,
812
+ ` 来源:${row?.sourceRef ?? "未提供"}`,
813
+ ...(row?.installHint ? [` 入口提示:${row.installHint}`] : []),
814
+ ` 目标目录:${target}`,
815
+ row?.collection
816
+ ? " 形态:多 Skill 套件(子目录含 SKILL.md)"
817
+ : " 形态:叶子 Skill(目标目录直接含 SKILL.md)",
818
+ ].join("\n");
819
+ });
820
+ const mcpLines = plan.mcpIds.map((id) => {
821
+ const row = mcpRows.find((candidate) => candidate.id === id);
822
+ const prep = mcpPlans.find((candidate) => candidate.id === id);
823
+ return [
824
+ `- MCP ${row?.name ?? id} (${id})`,
825
+ ` 来源:${row?.sourceRef ?? "未提供"}`,
826
+ ` 参考命令:${typeof prep?.command === "string" ? prep.command : "请阅读来源仓库安装说明"}`,
827
+ ` 建议配置:${typeof prep?.target === "string" ? prep.target : `.psyclaw/mcp/${id}.json`}`,
828
+ ].join("\n");
829
+ });
830
+ return [
831
+ "安装并配置「生理信号分析」能力包。",
832
+ `研究域:${plan.domainNames.join("、")}(${plan.domainIds.join(", ")})`,
833
+ `当前项目:${root}`,
834
+ `Skill 安装位置:${skillScopeLabel(skillScope)}。`,
835
+ "用户已通过 /biosignal 向导明确授权本次打包安装。你可以直接联网、克隆或下载来源、使用所需包管理器安装依赖、执行安装命令,并写入与清单有关的项目或用户级配置;不要再次要求用户批准这些安装步骤。",
836
+ "不要写入 data/raw、.git、凭据或符号链接目标。不要把模型口头声称当作完成证据;每个条目安装后检查目标目录或 MCP 配置是否存在。",
837
+ "",
838
+ "MCP 清单:",
839
+ ...(mcpLines.length > 0 ? mcpLines : ["(无)"]),
840
+ "",
841
+ "Skill 清单:",
842
+ ...(skillLines.length > 0 ? skillLines : ["(无)"]),
843
+ "",
844
+ "目录中的版本、命令和依赖只作参考;请先阅读各来源仓库的实际安装说明再落地。各项已在推荐状态中默认启用;全部完成后执行 /reload,并提醒用户:Shift+Tab 切到 analysis,用自然语言描述数据与分析目标即可开始。",
845
+ "若某一条目安装命令本身失败,直接诊断并修复;只有遇到必须由用户提供的凭据或外部软件许可证时才向用户说明。",
846
+ ].join("\n");
847
+ }
848
+ async function markBiosignalPackDesired(root, plan, skillScope) {
849
+ const state = await readRecommendationState(root);
850
+ const skills = new Set(state.skills);
851
+ const mcp = new Set(state.mcp);
852
+ const skillScopes = { ...(state.skillScopes ?? {}) };
853
+ for (const id of plan.skillIds) {
854
+ const normalized = normalizeRecommendedSkillId(id);
855
+ skills.add(normalized);
856
+ skillScopes[normalized] = skillScope;
857
+ }
858
+ for (const id of plan.mcpIds)
859
+ mcp.add(id);
860
+ state.skills = [...skills];
861
+ state.mcp = [...mcp];
862
+ state.skillScopes = skillScopes;
863
+ await saveRecommendationState(root, state);
864
+ }
865
+ async function queueBiosignalPackInstall(pi, ctx, plan, runtime) {
866
+ if (plan.mcpIds.length === 0 && plan.skillIds.length === 0) {
867
+ ctx.ui.notify("所选研究域没有需要安装的 MCP 或 Skill。", "info");
868
+ return;
869
+ }
870
+ const skillScope = plan.skillIds.length > 0 ? await chooseSkillScope(ctx) : "project";
871
+ if (plan.skillIds.length > 0 && !skillScope)
872
+ return;
873
+ const scope = skillScope ?? "project";
874
+ const skillCatalog = await readRecommendedCatalog();
875
+ const mcpCatalog = await recommendedItems("mcp");
876
+ const missingSkills = plan.skillIds.filter((id) => {
877
+ const item = skillCatalog.items.find((candidate) => normalizeRecommendedSkillId(String(candidate.id ?? "")) === normalizeRecommendedSkillId(id));
878
+ return !item || typeof item.sourceRef !== "string" || !/^https:\/\//.test(item.sourceRef);
879
+ });
880
+ if (missingSkills.length > 0) {
881
+ throw new Error(`能力包 Skill 缺少可安装来源:${missingSkills.join(", ")}`);
882
+ }
883
+ const skillRows = plan.skillIds.map((id) => {
884
+ const item = skillCatalog.items.find((candidate) => normalizeRecommendedSkillId(String(candidate.id ?? "")) === normalizeRecommendedSkillId(id));
885
+ return {
886
+ id: normalizeRecommendedSkillId(id),
887
+ name: String(item.name ?? id),
888
+ description: String(item.description ?? ""),
889
+ sourceRef: String(item.sourceRef),
890
+ ...(typeof item.installHint === "string" ? { installHint: item.installHint } : {}),
891
+ collection: item.skillLayout === "collection",
892
+ installed: false,
893
+ enabled: true,
894
+ scope,
895
+ blocked: false,
896
+ source: "recommended",
897
+ };
898
+ });
899
+ const mcpRows = await mcpManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd), runtime);
900
+ const selectedMcp = plan.mcpIds.map((id) => {
901
+ const row = mcpRows.find((candidate) => candidate.id === id);
902
+ if (!row?.sourceRef)
903
+ throw new Error(`能力包 MCP 缺少来源网址:${id}`);
904
+ return row;
905
+ });
906
+ const summary = [
907
+ `研究域:${plan.domainNames.join("、")}`,
908
+ plan.mcpIds.length > 0 ? `MCP:${plan.mcpIds.join(", ")}` : "MCP:无",
909
+ plan.skillIds.length > 0 ? `Skill:${plan.skillIds.join(", ")}` : "Skill:无",
910
+ plan.skillIds.length > 0 ? `Skill 安装位置:${skillScopeLabel(scope)}` : "",
911
+ ].filter(Boolean).join("\n");
912
+ const approved = await ctx.ui.confirm("交给当前模型安装生理信号能力包?", `${summary}\n\n模型将按现有 /mcp 与 /skill 约定完成下载与配置;安装后默认启用。`);
913
+ if (!approved)
914
+ return;
915
+ await markBiosignalPackDesired(ctx.cwd, plan, scope);
916
+ pi.sendUserMessage(modelBiosignalPackInstallTask(ctx.cwd, plan, skillRows, selectedMcp, mcpCatalog.installPrep, scope), ctx.isIdle() ? {} : { deliverAs: "followUp" });
917
+ ctx.ui.notify("已将生理信号能力包安装任务交给当前模型。完成后请执行 /reload,并 Shift+Tab 切到 analysis 使用。", "info");
918
+ }
919
+ async function showBiosignalPack(pi, ctx, runtime) {
920
+ const catalog = await readBiosignalPackCatalog();
921
+ if (!ctx.hasUI || typeof ctx.ui.custom !== "function") {
922
+ const lines = [
923
+ "生理信号分析能力包(无交互 UI 时仅列出研究域):",
924
+ ...catalog.domains.map((domain) => `- ${domain.id}: ${domain.name} — ${domain.description}`),
925
+ "请在交互式终端运行 /biosignal 完成选型与安装。",
926
+ ];
927
+ ctx.ui.notify(lines.join("\n"), "info");
928
+ return;
929
+ }
930
+ const result = await ctx.ui.custom((tui, theme, keybindings, done) => (new BiosignalWizardComponent(catalog.domains.map((domain) => ({
931
+ id: domain.id,
932
+ name: domain.name,
933
+ description: domain.description,
934
+ })), (domainIds) => {
935
+ const plan = resolveBiosignalInstallPlan(catalog, domainIds);
936
+ return {
937
+ domainNames: plan.domainNames,
938
+ mcpIds: plan.mcpIds,
939
+ skillIds: plan.skillIds,
940
+ };
941
+ }, tui, theme, keybindings, done)));
942
+ if (result.type === "close")
943
+ return;
944
+ const plan = resolveBiosignalInstallPlan(catalog, result.domainIds);
945
+ await queueBiosignalPackInstall(pi, ctx, plan, runtime);
946
+ }
761
947
  async function showSkillManager(pi, ctx) {
762
948
  if (!ctx.hasUI || typeof ctx.ui.custom !== "function") {
763
949
  const state = await readRecommendationState(ctx.cwd);
@@ -1516,6 +1702,18 @@ export default function psyclawExtension(pi) {
1516
1702
  }
1517
1703
  },
1518
1704
  });
1705
+ if (!legacyTestApi)
1706
+ pi.registerCommand("biosignal", {
1707
+ description: "生理信号分析能力包:选型研究域并安装 MCP/Skill",
1708
+ handler: async (_args, ctx) => {
1709
+ try {
1710
+ await showBiosignalPack(pi, ctx, runtimeMcps);
1711
+ }
1712
+ catch (error) {
1713
+ await notifyError(ctx, error);
1714
+ }
1715
+ },
1716
+ });
1519
1717
  if (!legacyTestApi)
1520
1718
  pi.registerCommand("ars", {
1521
1719
  description: "学术模式:单独开启,或附带任务文本启动完整流程",
@@ -1604,7 +1802,7 @@ export default function psyclawExtension(pi) {
1604
1802
  });
1605
1803
  if (!legacyTestApi)
1606
1804
  pi.registerCommand("provider", {
1607
- description: "查看或切换模型 Provider",
1805
+ description: "查看、配置或切换 Provider(含自定义 OpenAI 兼容接口)",
1608
1806
  handler: async (args, ctx) => {
1609
1807
  try {
1610
1808
  let requested = args.trim();
@@ -1616,7 +1814,7 @@ export default function psyclawExtension(pi) {
1616
1814
  }
1617
1815
  if (!requested) {
1618
1816
  const current = ctx.model?.provider ?? "none";
1619
- const available = new Map(PROVIDER_PRESETS.filter((preset) => preset.models.length > 0).map((preset) => [preset.id, preset]));
1817
+ const available = new Map(PROVIDER_PRESETS.filter((preset) => preset.models.length > 0 || preset.id === "custom").map((preset) => [preset.id, preset]));
1620
1818
  for (const id of providers.keys())
1621
1819
  if (!available.has(id))
1622
1820
  available.set(id, {
@@ -1629,13 +1827,15 @@ export default function psyclawExtension(pi) {
1629
1827
  });
1630
1828
  if (!ctx.hasUI || typeof ctx.ui.custom !== "function") {
1631
1829
  const lines = [...available].map(([id, preset]) => `${id}${id === current ? " *" : ""} — ${preset.name}`);
1632
- ctx.ui.notify([`当前 Provider: ${current}`, ...lines, "", "切换:/provider <id>"].join("\n"), "info");
1830
+ ctx.ui.notify([`当前 Provider: ${current}`, ...lines, "", "切换:/provider <id>(自定义:/provider custom)"].join("\n"), "info");
1633
1831
  return;
1634
1832
  }
1635
1833
  const selectedProvider = await pickProviderItem(ctx, "选择模型 Provider", [...available].map(([id, preset]) => ({
1636
1834
  id,
1637
1835
  label: preset.name,
1638
- description: `${id} · ${providers.get(id)?.length ?? preset.models.length} 个模型`,
1836
+ description: id === "custom"
1837
+ ? "自建/第三方 OpenAI 兼容网关:填写 Base URL、模型 ID 与 API Key"
1838
+ : `${id} · ${providers.get(id)?.length ?? preset.models.length} 个模型`,
1639
1839
  current: id === current,
1640
1840
  })));
1641
1841
  if (!selectedProvider)
@@ -1644,6 +1844,35 @@ export default function psyclawExtension(pi) {
1644
1844
  }
1645
1845
  if (!/^[A-Za-z0-9._:-]+$/.test(requested))
1646
1846
  throw new Error("请使用 /provider 打开选择,或 /provider <provider-id>");
1847
+ if (requested === "custom") {
1848
+ const custom = await configureCustomProvider(ctx);
1849
+ if (!custom)
1850
+ return;
1851
+ const preset = PROVIDER_PRESETS.find((item) => item.id === "custom");
1852
+ await saveProviderConfig({
1853
+ ...preset,
1854
+ baseUrl: custom.baseUrl,
1855
+ models: [{ id: custom.modelId, name: custom.modelId }],
1856
+ ...(custom.apiKey ? { apiKey: custom.apiKey } : {}),
1857
+ });
1858
+ const refreshed = await Promise.race([
1859
+ ctx.modelRegistry.refresh().then(() => true),
1860
+ new Promise((resolve) => setTimeout(() => resolve(false), 5_000)),
1861
+ ]);
1862
+ if (!refreshed) {
1863
+ ctx.ui.notify("自定义 Provider 已保存;模型目录刷新超时,请重新启动 PsyClaw 后使用。", "warning");
1864
+ return;
1865
+ }
1866
+ const selected = ctx.modelRegistry.getAll().find((model) => model.provider === "custom" && model.id === custom.modelId);
1867
+ if (!selected)
1868
+ throw new Error(`自定义模型未出现在目录中:custom/${custom.modelId};请检查 Base URL 后重试`);
1869
+ const changed = await pi.setModel(selected);
1870
+ if (!changed)
1871
+ throw new Error("未找到 custom 的可用凭据;请重新运行 /provider custom 并输入 API Key");
1872
+ await saveDefaultModel("custom", selected.id);
1873
+ ctx.ui.notify(`已切换并设为默认模型:custom/${selected.id}(${custom.baseUrl})`, "info");
1874
+ return;
1875
+ }
1647
1876
  const preset = PROVIDER_PRESETS.find((item) => item.id === requested);
1648
1877
  let models = providers.get(requested) ?? [];
1649
1878
  const modelChoices = models.length > 0 ? models : (preset?.models ?? []);