@nextclaw/server 0.15.21 → 0.15.22

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/dist/index.js CHANGED
@@ -8,9 +8,9 @@ import { mkdir, open, readFile, readdir, realpath, stat } from "node:fs/promises
8
8
  import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
9
9
  import { createHash, randomBytes, randomUUID } from "node:crypto";
10
10
  import * as NextclawCore from "@nextclaw/core";
11
- import { ConfigSchema, DEFAULT_WORKSPACE_PATH, buildConfigSchema, createAgentProfile, expandHome, findEffectiveAgentProfile, getDataDir, getPackageVersion, getProviderName, hasSecretRef, isSensitiveConfigPath, loadConfig, mergeExtensionConfigView, normalizeModelThinkingCapability, normalizeProviderModelConfig, probeFeishu, readAgentAvatarContent, removeAgentProfile, resolveEffectiveAgentProfiles, saveConfig, toExtensionConfigView, updateAgentProfile } from "@nextclaw/core";
12
- import { homedir, platform } from "node:os";
11
+ import { ConfigSchema, DEFAULT_WORKSPACE_PATH, ProviderModelDiscoveryHttpError, buildConfigSchema, createAgentProfile, expandHome, findEffectiveAgentProfile, getDataDir, getPackageVersion, getProviderName, hasSecretRef, isSensitiveConfigPath, loadConfig, mergeExtensionConfigView, normalizeModelThinkingCapability, normalizeProviderModelConfig, probeFeishu, readAgentAvatarContent, removeAgentProfile, resolveEffectiveAgentProfiles, saveConfig, toExtensionConfigView, updateAgentProfile } from "@nextclaw/core";
13
12
  import { findBuiltinProviderByName, listBuiltinProviders } from "@nextclaw/runtime";
13
+ import { homedir, platform } from "node:os";
14
14
  import { McpInstalledViewService } from "@nextclaw/mcp";
15
15
  import { serveStatic } from "hono/serve-static";
16
16
  //#region src/features/auth/utils/auth-bridge.utils.ts
@@ -458,6 +458,8 @@ const ingressKeys = {
458
458
  agentRun: {
459
459
  send: createTypedKey("agent-run.send"),
460
460
  abort: createTypedKey("agent-run.abort"),
461
+ editMessage: createTypedKey("agent-run.edit-message"),
462
+ continue: createTypedKey("agent-run.continue"),
461
463
  sessionMessageRequest: createTypedKey("agent-run.session-message.request")
462
464
  }
463
465
  };
@@ -583,6 +585,10 @@ var AgentsRoutesController = class {
583
585
  const body = await readJson(c.req.raw);
584
586
  if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
585
587
  try {
588
+ if (typeof body.data.contextTokens === "number") await this.options.kernel.agentContextWindowManager.assertCanSave({
589
+ agentId: body.data.id,
590
+ contextTokens: body.data.contextTokens
591
+ });
586
592
  const agent = createAgent(this.options.configPath, body.data, { initializeAgentHomeDirectory: this.options.initializeAgentHomeDirectory });
587
593
  await this.publishAgentUpdates(["agents.list"]);
588
594
  return c.json(ok(agent));
@@ -595,6 +601,10 @@ var AgentsRoutesController = class {
595
601
  const body = await readJson(c.req.raw);
596
602
  if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
597
603
  try {
604
+ if (typeof body.data.contextTokens === "number") await this.options.kernel.agentContextWindowManager.assertCanSave({
605
+ agentId,
606
+ contextTokens: body.data.contextTokens
607
+ });
598
608
  const agent = updateAgent(this.options.configPath, agentId, body.data);
599
609
  await this.publishAgentUpdates(["agents.list"]);
600
610
  return c.json(ok(agent));
@@ -669,6 +679,89 @@ var AppRoutesController = class {
669
679
  bootstrapStatus = (c) => c.json(ok(this.options.bootstrapStatus?.getStatus() ?? buildFallbackBootstrapStatus()));
670
680
  };
671
681
  //#endregion
682
+ //#region src/features/config/providers/server-builtin-provider.provider.ts
683
+ const SERVER_BUILTIN_PROVIDER_OVERRIDES = [{
684
+ name: "minimax-portal",
685
+ keywords: ["minimax-portal", "minimax"],
686
+ envKey: "MINIMAX_PORTAL_TOKEN",
687
+ displayName: "MiniMax Portal",
688
+ modelPrefix: "minimax-portal",
689
+ litellmPrefix: "minimax-portal",
690
+ skipPrefixes: ["minimax-portal/"],
691
+ envExtras: [],
692
+ isGateway: false,
693
+ isLocal: false,
694
+ detectByKeyPrefix: "",
695
+ detectByBaseKeyword: "",
696
+ defaultApiBase: "https://api.minimax.io/v1",
697
+ defaultModels: [
698
+ "minimax-portal/MiniMax-M3",
699
+ "minimax-portal/MiniMax-M2.5",
700
+ "minimax-portal/MiniMax-M2.5-highspeed"
701
+ ],
702
+ modelDiscovery: false,
703
+ modelConfig: { "minimax-portal/MiniMax-M3": { vision: true } },
704
+ stripModelPrefix: false,
705
+ modelOverrides: [],
706
+ logo: "minimax.svg",
707
+ apiBaseHelp: {
708
+ zh: "OAuth Global 默认使用 https://api.minimax.io/v1;OAuth 中国区默认使用 https://api.minimaxi.com/v1。",
709
+ en: "OAuth Global uses https://api.minimax.io/v1 by default; OAuth CN uses https://api.minimaxi.com/v1."
710
+ },
711
+ auth: {
712
+ kind: "device_code",
713
+ protocol: "minimax_user_code",
714
+ displayName: "MiniMax OAuth",
715
+ baseUrl: "https://api.minimax.io",
716
+ deviceCodePath: "/oauth/code",
717
+ tokenPath: "/oauth/token",
718
+ clientId: "78257093-7e40-4613-99e0-527b14b39113",
719
+ scope: "group_id profile model.completion",
720
+ grantType: "urn:ietf:params:oauth:grant-type:user_code",
721
+ usePkce: true,
722
+ defaultMethodId: "cn",
723
+ methods: [{
724
+ id: "global",
725
+ label: {
726
+ zh: "Global(海外)",
727
+ en: "Global"
728
+ },
729
+ hint: {
730
+ zh: "适用于海外用户,默认 API Base 为 https://api.minimax.io/v1。",
731
+ en: "For international users. Default API base: https://api.minimax.io/v1."
732
+ },
733
+ baseUrl: "https://api.minimax.io",
734
+ defaultApiBase: "https://api.minimax.io/v1"
735
+ }, {
736
+ id: "cn",
737
+ label: {
738
+ zh: "中国区(CN)",
739
+ en: "China Mainland (CN)"
740
+ },
741
+ hint: {
742
+ zh: "适用于中国区用户,默认 API Base 为 https://api.minimaxi.com/v1。",
743
+ en: "For Mainland China users. Default API base: https://api.minimaxi.com/v1."
744
+ },
745
+ baseUrl: "https://api.minimaxi.com",
746
+ defaultApiBase: "https://api.minimaxi.com/v1"
747
+ }],
748
+ note: {
749
+ zh: "通过浏览器完成 MiniMax OAuth 授权后即可使用,无需手动填写 API Key。",
750
+ en: "Complete MiniMax OAuth in browser to use this provider without manually entering an API key."
751
+ }
752
+ }
753
+ }];
754
+ const SERVER_BUILTIN_PROVIDER_OVERRIDE_MAP = new Map(SERVER_BUILTIN_PROVIDER_OVERRIDES.map((provider) => [provider.name, provider]));
755
+ function listServerBuiltinProviders() {
756
+ const merged = /* @__PURE__ */ new Map();
757
+ for (const provider of listBuiltinProviders()) merged.set(provider.name, provider);
758
+ for (const provider of SERVER_BUILTIN_PROVIDER_OVERRIDES) merged.set(provider.name, provider);
759
+ return Array.from(merged.values());
760
+ }
761
+ function findServerBuiltinProviderByName(name) {
762
+ return SERVER_BUILTIN_PROVIDER_OVERRIDE_MAP.get(name) ?? findBuiltinProviderByName(name);
763
+ }
764
+ //#endregion
672
765
  //#region src/features/config/utils/extension-channel-config-projection.utils.ts
673
766
  const DOCS_BASE_URL = "https://docs.nextclaw.io";
674
767
  const CHANNEL_TUTORIAL_URLS = {
@@ -758,1000 +851,569 @@ function mergeProjectedExtensionChannelConfig(config, channelName, mergedChannel
758
851
  });
759
852
  }
760
853
  //#endregion
761
- //#region src/features/config/utils/channel-auth.utils.ts
762
- function cloneChannelConfig(value) {
763
- if (!value || typeof value !== "object" || Array.isArray(value)) return;
764
- return JSON.parse(JSON.stringify(value));
765
- }
766
- function findExtensionChannelBinding(bindings, channelId) {
767
- const normalizedChannelId = channelId.trim().toLowerCase();
768
- return bindings.find((binding) => binding.channelId.trim().toLowerCase() === normalizedChannelId) ?? null;
769
- }
770
- function toPublicChannelAuthPollResult(result) {
854
+ //#region src/features/config/utils/default-provider-config.utils.ts
855
+ function createDefaultProviderConfig(defaultWireApi = "auto", defaultModels = [], modelConfig = {}, providerType, defaultApiBase = null) {
771
856
  return {
772
- channel: result.channel,
773
- status: result.status,
774
- message: result.message,
775
- nextPollMs: result.nextPollMs,
776
- accountId: result.accountId,
777
- notes: result.notes
857
+ enabled: true,
858
+ providerType,
859
+ displayName: "",
860
+ apiKey: "",
861
+ apiBase: defaultApiBase,
862
+ extraHeaders: null,
863
+ wireApi: defaultWireApi,
864
+ models: [...defaultModels],
865
+ modelConfig
778
866
  };
779
867
  }
780
- function applyAuthorizedChannelAuthResult(params) {
781
- const { configPath, binding, result } = params;
782
- if (result.status !== "authorized" || !result.channelConfig) return;
783
- const currentConfig = loadConfigOrDefault(configPath);
784
- saveConfig({
785
- ...currentConfig,
786
- channels: {
787
- ...currentConfig.channels,
788
- [binding.channelId]: result.channelConfig
789
- }
790
- }, configPath);
868
+ function createDefaultProviderConfigFromSpec(spec) {
869
+ return createDefaultProviderConfig(spec?.defaultWireApi ?? "auto", spec?.defaultModels ?? [], normalizeProviderModelConfig(spec?.modelConfig ?? {}), spec?.name, spec?.defaultApiBase ?? null);
791
870
  }
792
- async function startChannelAuth(params) {
793
- const { configPath, channelId, request, bindings } = params;
794
- const binding = findExtensionChannelBinding(bindings, channelId);
795
- const start = binding?.channel.auth?.start;
796
- if (!binding || !start) return null;
797
- const configView = getProjectedConfigView(loadConfigOrDefault(configPath), { extensionChannelBindings: bindings });
798
- return await start({
799
- cfg: configView,
800
- extensionId: binding.extensionId,
801
- channelId: binding.channelId,
802
- channelConfig: cloneChannelConfig(configView.channels?.[binding.channelId]),
803
- accountId: request.accountId?.trim() || null,
804
- baseUrl: request.baseUrl?.trim() || null,
805
- domain: request.domain?.trim() || null
806
- });
871
+ //#endregion
872
+ //#region src/features/config/utils/runtime-entry-config.utils.ts
873
+ function normalizeOptionalString$2(value) {
874
+ if (typeof value !== "string") return null;
875
+ const trimmed = value.trim();
876
+ return trimmed.length > 0 ? trimmed : null;
807
877
  }
808
- async function pollChannelAuth(params) {
809
- const { configPath, channelId, sessionId, bindings } = params;
810
- const binding = findExtensionChannelBinding(bindings, channelId);
811
- const poll = binding?.channel.auth?.poll;
812
- if (!binding || !poll) return null;
813
- const configView = getProjectedConfigView(loadConfigOrDefault(configPath), { extensionChannelBindings: bindings });
814
- const result = await poll({
815
- cfg: configView,
816
- extensionId: binding.extensionId,
817
- channelId: binding.channelId,
818
- channelConfig: cloneChannelConfig(configView.channels?.[binding.channelId]),
819
- sessionId
820
- });
821
- if (!result) return null;
822
- applyAuthorizedChannelAuthResult({
823
- configPath,
824
- binding,
825
- result
826
- });
827
- return toPublicChannelAuthPollResult(result);
878
+ function normalizePositiveInteger(value) {
879
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : NaN;
880
+ if (!Number.isFinite(parsed)) return null;
881
+ const normalized = Math.trunc(parsed);
882
+ return normalized > 0 ? normalized : null;
828
883
  }
829
- async function connectChannelAuth(params) {
830
- const { configPath, channelId, request, bindings } = params;
831
- const binding = findExtensionChannelBinding(bindings, channelId);
832
- const connect = binding?.channel.auth?.connect;
833
- if (!binding || !connect) return null;
834
- const configView = getProjectedConfigView(loadConfigOrDefault(configPath), { extensionChannelBindings: bindings });
835
- const result = await connect({
836
- cfg: configView,
837
- extensionId: binding.extensionId,
838
- channelId: binding.channelId,
839
- channelConfig: cloneChannelConfig(configView.channels?.[binding.channelId]),
840
- accountId: request.accountId?.trim() || null,
841
- domain: request.domain?.trim() || null,
842
- fields: request.fields
843
- });
844
- applyAuthorizedChannelAuthResult({
845
- configPath,
846
- binding,
847
- result
848
- });
849
- return toPublicChannelAuthPollResult(result);
884
+ function normalizeStringArray(value) {
885
+ if (!Array.isArray(value)) return null;
886
+ const entries = value.map((entry) => normalizeOptionalString$2(entry)).filter((entry) => Boolean(entry));
887
+ return entries.length > 0 ? entries : null;
888
+ }
889
+ function normalizeUnknownStringRecord(value) {
890
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
891
+ const entries = Object.entries(value).map(([key, entryValue]) => [key.trim(), normalizeOptionalString$2(entryValue)]).filter(([key, entryValue]) => key.length > 0 && Boolean(entryValue));
892
+ return entries.length > 0 ? Object.fromEntries(entries) : null;
893
+ }
894
+ function normalizeRuntimeModelSelectionMode(value) {
895
+ if (value === "nextclaw" || value === "optional" || value === "runtime-default") return value;
896
+ return null;
897
+ }
898
+ function normalizeRuntimeEntryConfig(type, config) {
899
+ const runtimeDefaultThinking = normalizeModelThinkingCapability(config.runtimeDefaultThinking);
900
+ if (type !== "narp-stdio") return {
901
+ ...Object.fromEntries(Object.entries(config).filter(([key]) => key !== "runtimeDefaultThinking")),
902
+ ...runtimeDefaultThinking ? { runtimeDefaultThinking } : {}
903
+ };
904
+ const command = normalizeOptionalString$2(config.command);
905
+ const args = normalizeStringArray(config.args);
906
+ const cwd = normalizeOptionalString$2(config.cwd);
907
+ const wireDialect = normalizeOptionalString$2(config.wireDialect) ?? "acp";
908
+ const processScope = normalizeOptionalString$2(config.processScope) ?? "per-session";
909
+ const modelSelectionMode = normalizeRuntimeModelSelectionMode(config.modelSelectionMode);
910
+ const supportedModels = normalizeStringArray(config.supportedModels);
911
+ const model = normalizeOptionalString$2(config.model);
912
+ const recommendedModel = normalizeOptionalString$2(config.recommendedModel);
913
+ return {
914
+ wireDialect,
915
+ processScope,
916
+ ...command ? { command } : {},
917
+ ...args ? { args } : {},
918
+ env: normalizeUnknownStringRecord(config.env) ?? {},
919
+ ...cwd ? { cwd } : {},
920
+ ...modelSelectionMode ? { modelSelectionMode } : {},
921
+ ...model ? { model } : {},
922
+ ...recommendedModel ? { recommendedModel } : {},
923
+ ...supportedModels ? { supportedModels } : {},
924
+ ...runtimeDefaultThinking ? { runtimeDefaultThinking } : {},
925
+ startupTimeoutMs: normalizePositiveInteger(config.startupTimeoutMs) ?? 8e3,
926
+ probeTimeoutMs: normalizePositiveInteger(config.probeTimeoutMs) ?? 3e3,
927
+ requestTimeoutMs: normalizePositiveInteger(config.requestTimeoutMs) ?? 12e4
928
+ };
850
929
  }
851
930
  //#endregion
852
- //#region src/features/config/providers/server-builtin-provider.provider.ts
853
- const SERVER_BUILTIN_PROVIDER_OVERRIDES = [{
854
- name: "minimax-portal",
855
- keywords: ["minimax-portal", "minimax"],
856
- envKey: "MINIMAX_PORTAL_TOKEN",
857
- displayName: "MiniMax Portal",
858
- modelPrefix: "minimax-portal",
859
- litellmPrefix: "minimax-portal",
860
- skipPrefixes: ["minimax-portal/"],
861
- envExtras: [],
862
- isGateway: false,
863
- isLocal: false,
864
- detectByKeyPrefix: "",
865
- detectByBaseKeyword: "",
866
- defaultApiBase: "https://api.minimax.io/v1",
867
- defaultModels: [
868
- "minimax-portal/MiniMax-M3",
869
- "minimax-portal/MiniMax-M2.5",
870
- "minimax-portal/MiniMax-M2.5-highspeed"
871
- ],
872
- modelConfig: { "minimax-portal/MiniMax-M3": { vision: true } },
873
- stripModelPrefix: false,
874
- modelOverrides: [],
875
- logo: "minimax.svg",
876
- apiBaseHelp: {
877
- zh: "OAuth Global 默认使用 https://api.minimax.io/v1;OAuth 中国区默认使用 https://api.minimaxi.com/v1。",
878
- en: "OAuth Global uses https://api.minimax.io/v1 by default; OAuth CN uses https://api.minimaxi.com/v1."
879
- },
880
- auth: {
881
- kind: "device_code",
882
- protocol: "minimax_user_code",
883
- displayName: "MiniMax OAuth",
884
- baseUrl: "https://api.minimax.io",
885
- deviceCodePath: "/oauth/code",
886
- tokenPath: "/oauth/token",
887
- clientId: "78257093-7e40-4613-99e0-527b14b39113",
888
- scope: "group_id profile model.completion",
889
- grantType: "urn:ietf:params:oauth:grant-type:user_code",
890
- usePkce: true,
891
- defaultMethodId: "cn",
892
- methods: [{
893
- id: "global",
894
- label: {
895
- zh: "Global(海外)",
896
- en: "Global"
897
- },
898
- hint: {
899
- zh: "适用于海外用户,默认 API Base 为 https://api.minimax.io/v1。",
900
- en: "For international users. Default API base: https://api.minimax.io/v1."
901
- },
902
- baseUrl: "https://api.minimax.io",
903
- defaultApiBase: "https://api.minimax.io/v1"
904
- }, {
905
- id: "cn",
906
- label: {
907
- zh: "中国区(CN)",
908
- en: "China Mainland (CN)"
909
- },
910
- hint: {
911
- zh: "适用于中国区用户,默认 API Base 为 https://api.minimaxi.com/v1。",
912
- en: "For Mainland China users. Default API base: https://api.minimaxi.com/v1."
913
- },
914
- baseUrl: "https://api.minimaxi.com",
915
- defaultApiBase: "https://api.minimaxi.com/v1"
916
- }],
917
- note: {
918
- zh: "通过浏览器完成 MiniMax OAuth 授权后即可使用,无需手动填写 API Key。",
919
- en: "Complete MiniMax OAuth in browser to use this provider without manually entering an API key."
920
- }
921
- }
922
- }];
923
- const SERVER_BUILTIN_PROVIDER_OVERRIDE_MAP = new Map(SERVER_BUILTIN_PROVIDER_OVERRIDES.map((provider) => [provider.name, provider]));
924
- function listServerBuiltinProviders() {
925
- const merged = /* @__PURE__ */ new Map();
926
- for (const provider of listBuiltinProviders()) merged.set(provider.name, provider);
927
- for (const provider of SERVER_BUILTIN_PROVIDER_OVERRIDES) merged.set(provider.name, provider);
928
- return Array.from(merged.values());
931
+ //#region src/features/config/utils/search-config.utils.ts
932
+ const MASK_MIN_LENGTH$1 = 8;
933
+ const BOCHA_OPEN_URL = "https://open.bocha.cn";
934
+ const TAVILY_DOCS_URL = "https://docs.tavily.com/documentation/api-reference/endpoint/search";
935
+ const EXA_DOCS_URL = "https://exa.ai/docs/reference/search";
936
+ const SEARCH_PROVIDER_NAMES = [
937
+ "bocha",
938
+ "tavily",
939
+ "brave",
940
+ "exa"
941
+ ];
942
+ function normalizeOptionalString$1(value) {
943
+ if (typeof value !== "string") return null;
944
+ const trimmed = value.trim();
945
+ return trimmed.length > 0 ? trimmed : null;
929
946
  }
930
- function findServerBuiltinProviderByName(name) {
931
- return SERVER_BUILTIN_PROVIDER_OVERRIDE_MAP.get(name) ?? findBuiltinProviderByName(name);
947
+ function maskApiKey$1(value) {
948
+ if (!value) return { apiKeySet: false };
949
+ if (value.length < MASK_MIN_LENGTH$1) return {
950
+ apiKeySet: true,
951
+ apiKeyMasked: "****"
952
+ };
953
+ return {
954
+ apiKeySet: true,
955
+ apiKeyMasked: `${value.slice(0, 2)}****${value.slice(-4)}`
956
+ };
932
957
  }
933
- //#endregion
934
- //#region src/features/config/utils/provider-auth.utils.ts
935
- const authSessions = /* @__PURE__ */ new Map();
936
- const DEFAULT_AUTH_INTERVAL_MS = 2e3;
937
- const MAX_AUTH_INTERVAL_MS = 1e4;
938
- function normalizePositiveInt(value, fallback) {
939
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return fallback;
940
- return Math.floor(value);
958
+ function clearSecretRef$1(refs, path) {
959
+ if (!refs[path]) return refs;
960
+ const nextRefs = { ...refs };
961
+ delete nextRefs[path];
962
+ return nextRefs;
941
963
  }
942
- function normalizePositiveFloat(value) {
943
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
944
- return value;
964
+ function isSearchProviderName(value) {
965
+ return typeof value === "string" && SEARCH_PROVIDER_NAMES.some((providerName) => providerName === value);
945
966
  }
946
- function toBase64Url(buffer) {
947
- return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
967
+ const SEARCH_PROVIDER_META = [
968
+ {
969
+ name: "bocha",
970
+ displayName: "Bocha Search",
971
+ description: "China-friendly web search with AI-ready summaries.",
972
+ docsUrl: BOCHA_OPEN_URL,
973
+ isDefault: true,
974
+ supportsSummary: true
975
+ },
976
+ {
977
+ name: "tavily",
978
+ displayName: "Tavily Search",
979
+ description: "Research-focused web search with optional synthesized answers.",
980
+ docsUrl: TAVILY_DOCS_URL,
981
+ supportsSummary: true
982
+ },
983
+ {
984
+ name: "brave",
985
+ displayName: "Brave Search",
986
+ description: "Brave web search API kept as an optional provider.",
987
+ supportsSummary: false
988
+ },
989
+ {
990
+ name: "exa",
991
+ displayName: "Exa Search",
992
+ description: "Semantic web search with extracted page content.",
993
+ docsUrl: EXA_DOCS_URL,
994
+ supportsSummary: true
995
+ }
996
+ ];
997
+ function toSearchProviderView(config, providerName, provider) {
998
+ const apiKeyRefSet = hasSecretRef(config, `search.providers.${providerName}.apiKey`);
999
+ const masked = maskApiKey$1(provider.apiKey);
1000
+ const view = {
1001
+ enabled: config.search.enabledProviders.includes(providerName),
1002
+ apiKeySet: masked.apiKeySet || apiKeyRefSet,
1003
+ apiKeyMasked: masked.apiKeyMasked ?? (apiKeyRefSet ? "****" : void 0),
1004
+ baseUrl: provider.baseUrl
1005
+ };
1006
+ if ("docsUrl" in provider) view.docsUrl = provider.docsUrl;
1007
+ if ("summary" in provider) view.summary = provider.summary;
1008
+ if ("freshness" in provider) view.freshness = provider.freshness;
1009
+ if ("searchDepth" in provider) view.searchDepth = provider.searchDepth;
1010
+ if ("includeAnswer" in provider) view.includeAnswer = Boolean(provider.includeAnswer);
1011
+ return view;
948
1012
  }
949
- function buildPkce() {
950
- const verifier = toBase64Url(randomBytes(48));
1013
+ function buildSearchView(config) {
951
1014
  return {
952
- verifier,
953
- challenge: toBase64Url(createHash("sha256").update(verifier).digest())
1015
+ provider: config.search.provider,
1016
+ enabledProviders: [...config.search.enabledProviders],
1017
+ defaults: { maxResults: config.search.defaults.maxResults },
1018
+ providers: {
1019
+ bocha: toSearchProviderView(config, "bocha", config.search.providers.bocha),
1020
+ tavily: toSearchProviderView(config, "tavily", config.search.providers.tavily),
1021
+ brave: toSearchProviderView(config, "brave", config.search.providers.brave),
1022
+ exa: toSearchProviderView(config, "exa", config.search.providers.exa)
1023
+ }
954
1024
  };
955
1025
  }
956
- function withTrailingSlash(value) {
957
- return value.endsWith("/") ? value : `${value}/`;
958
- }
959
- function cleanupExpiredAuthSessions(now = Date.now()) {
960
- for (const [sessionId, session] of authSessions.entries()) if (session.expiresAtMs <= now) authSessions.delete(sessionId);
961
- }
962
- function resolveDeviceCodeEndpoints(baseUrl, deviceCodePath, tokenPath) {
1026
+ function replaceSearchConfig(config, search, refs = config.secrets.refs) {
963
1027
  return {
964
- deviceCodeEndpoint: new URL(deviceCodePath, withTrailingSlash(baseUrl)).toString(),
965
- tokenEndpoint: new URL(tokenPath, withTrailingSlash(baseUrl)).toString()
1028
+ ...config,
1029
+ search,
1030
+ secrets: refs === config.secrets.refs ? config.secrets : {
1031
+ ...config.secrets,
1032
+ refs
1033
+ }
966
1034
  };
967
1035
  }
968
- function resolveAuthNote(params) {
969
- return params.zh ?? params.en;
970
- }
971
- function resolveLocalizedMethodLabel(method, fallbackId) {
972
- return method.label?.zh ?? method.label?.en ?? fallbackId;
1036
+ function applyActiveSearchProviderPatch(config, provider) {
1037
+ if (!isSearchProviderName(provider)) return config;
1038
+ return replaceSearchConfig(config, {
1039
+ ...config.search,
1040
+ provider
1041
+ });
973
1042
  }
974
- function resolveLocalizedMethodHint(method) {
975
- return method.hint?.zh ?? method.hint?.en;
1043
+ function applyEnabledSearchProvidersPatch(config, enabledProviders) {
1044
+ if (!Array.isArray(enabledProviders)) return config;
1045
+ const nextEnabledProviders = Array.from(new Set(enabledProviders.filter((value) => isSearchProviderName(value))));
1046
+ return replaceSearchConfig(config, {
1047
+ ...config.search,
1048
+ enabledProviders: nextEnabledProviders
1049
+ });
976
1050
  }
977
- function normalizeMethodId(value) {
978
- if (typeof value !== "string") return;
979
- const trimmed = value.trim();
980
- return trimmed.length > 0 ? trimmed : void 0;
1051
+ function applySearchDefaultsPatch(config, defaults) {
1052
+ if (!defaults || !Object.prototype.hasOwnProperty.call(defaults, "maxResults")) return config;
1053
+ const nextMaxResults = defaults.maxResults;
1054
+ if (typeof nextMaxResults === "number" && Number.isFinite(nextMaxResults)) return replaceSearchConfig(config, {
1055
+ ...config.search,
1056
+ defaults: {
1057
+ ...config.search.defaults,
1058
+ maxResults: Math.max(1, Math.min(50, Math.trunc(nextMaxResults)))
1059
+ }
1060
+ });
1061
+ return config;
981
1062
  }
982
- function resolveAuthMethod(auth, requestedMethodId) {
983
- const protocol = auth.protocol ?? "rfc8628";
984
- const methods = (auth.methods ?? []).filter((entry) => normalizeMethodId(entry.id));
985
- const cleanRequestedMethodId = normalizeMethodId(requestedMethodId);
986
- if (methods.length === 0) {
987
- if (cleanRequestedMethodId) throw new Error(`provider auth method is not supported: ${cleanRequestedMethodId}`);
988
- return {
989
- protocol,
990
- baseUrl: auth.baseUrl,
991
- deviceCodePath: auth.deviceCodePath,
992
- tokenPath: auth.tokenPath,
993
- clientId: auth.clientId,
994
- scope: auth.scope,
995
- grantType: auth.grantType,
996
- usePkce: Boolean(auth.usePkce)
1063
+ function applyBochaSearchPatch(config, patch) {
1064
+ if (!patch) return config;
1065
+ let nextRefs = config.secrets.refs;
1066
+ let nextProvider = config.search.providers.bocha;
1067
+ if (Object.prototype.hasOwnProperty.call(patch, "apiKey")) {
1068
+ nextProvider = {
1069
+ ...nextProvider,
1070
+ apiKey: patch.apiKey ?? ""
997
1071
  };
1072
+ nextRefs = clearSecretRef$1(nextRefs, "search.providers.bocha.apiKey");
998
1073
  }
999
- let selectedMethod = methods.find((entry) => normalizeMethodId(entry.id) === cleanRequestedMethodId);
1000
- if (!selectedMethod) {
1001
- const fallbackMethodId = normalizeMethodId(auth.defaultMethodId) ?? normalizeMethodId(methods[0]?.id);
1002
- selectedMethod = methods.find((entry) => normalizeMethodId(entry.id) === fallbackMethodId) ?? methods[0];
1003
- }
1004
- const methodId = normalizeMethodId(selectedMethod?.id);
1005
- if (!selectedMethod || !methodId) throw new Error("provider auth method is not configured");
1006
- if (cleanRequestedMethodId && methodId !== cleanRequestedMethodId) throw new Error(`provider auth method is not supported: ${cleanRequestedMethodId}`);
1007
- return {
1008
- id: methodId,
1009
- protocol,
1010
- baseUrl: selectedMethod.baseUrl ?? auth.baseUrl,
1011
- deviceCodePath: selectedMethod.deviceCodePath ?? auth.deviceCodePath,
1012
- tokenPath: selectedMethod.tokenPath ?? auth.tokenPath,
1013
- clientId: selectedMethod.clientId ?? auth.clientId,
1014
- scope: selectedMethod.scope ?? auth.scope,
1015
- grantType: selectedMethod.grantType ?? auth.grantType,
1016
- usePkce: selectedMethod.usePkce ?? Boolean(auth.usePkce),
1017
- defaultApiBase: selectedMethod.defaultApiBase
1074
+ if (Object.prototype.hasOwnProperty.call(patch, "baseUrl")) nextProvider = {
1075
+ ...nextProvider,
1076
+ baseUrl: normalizeOptionalString$1(patch.baseUrl) ?? "https://api.bocha.cn/v1/web-search"
1018
1077
  };
1078
+ if (Object.prototype.hasOwnProperty.call(patch, "docsUrl")) nextProvider = {
1079
+ ...nextProvider,
1080
+ docsUrl: normalizeOptionalString$1(patch.docsUrl) ?? BOCHA_OPEN_URL
1081
+ };
1082
+ if (Object.prototype.hasOwnProperty.call(patch, "summary")) nextProvider = {
1083
+ ...nextProvider,
1084
+ summary: Boolean(patch.summary)
1085
+ };
1086
+ if (Object.prototype.hasOwnProperty.call(patch, "freshness")) {
1087
+ const freshness = normalizeOptionalString$1(patch.freshness);
1088
+ nextProvider = {
1089
+ ...nextProvider,
1090
+ freshness: freshness === "noLimit" || freshness === "oneDay" || freshness === "oneWeek" || freshness === "oneMonth" || freshness === "oneYear" ? freshness : "noLimit"
1091
+ };
1092
+ }
1093
+ return replaceSearchConfig(config, {
1094
+ ...config.search,
1095
+ providers: {
1096
+ ...config.search.providers,
1097
+ bocha: nextProvider
1098
+ }
1099
+ }, nextRefs);
1019
1100
  }
1020
- function parseExpiresAtMs(value, fallbackFromNowMs) {
1021
- const normalized = normalizePositiveFloat(value);
1022
- if (normalized === null) return Date.now() + fallbackFromNowMs;
1023
- if (normalized >= 0xe8d4a51000) return Math.floor(normalized);
1024
- if (normalized >= 1e9) return Math.floor(normalized * 1e3);
1025
- return Date.now() + Math.floor(normalized * 1e3);
1026
- }
1027
- function parsePollIntervalMs(value, fallbackMs) {
1028
- const normalized = normalizePositiveFloat(value);
1029
- if (normalized === null) return fallbackMs;
1030
- if (normalized <= 30) return Math.floor(normalized * 1e3);
1031
- return Math.floor(normalized);
1101
+ function applyTavilySearchPatch(config, patch) {
1102
+ if (!patch) return config;
1103
+ let nextRefs = config.secrets.refs;
1104
+ let nextProvider = config.search.providers.tavily;
1105
+ if (Object.prototype.hasOwnProperty.call(patch, "apiKey")) {
1106
+ nextProvider = {
1107
+ ...nextProvider,
1108
+ apiKey: patch.apiKey ?? ""
1109
+ };
1110
+ nextRefs = clearSecretRef$1(nextRefs, "search.providers.tavily.apiKey");
1111
+ }
1112
+ if (Object.prototype.hasOwnProperty.call(patch, "baseUrl")) nextProvider = {
1113
+ ...nextProvider,
1114
+ baseUrl: normalizeOptionalString$1(patch.baseUrl) ?? "https://api.tavily.com/search"
1115
+ };
1116
+ if (Object.prototype.hasOwnProperty.call(patch, "searchDepth")) {
1117
+ const searchDepth = normalizeOptionalString$1(patch.searchDepth);
1118
+ nextProvider = {
1119
+ ...nextProvider,
1120
+ searchDepth: searchDepth === "advanced" ? "advanced" : "basic"
1121
+ };
1122
+ }
1123
+ if (Object.prototype.hasOwnProperty.call(patch, "includeAnswer")) nextProvider = {
1124
+ ...nextProvider,
1125
+ includeAnswer: Boolean(patch.includeAnswer)
1126
+ };
1127
+ return replaceSearchConfig(config, {
1128
+ ...config.search,
1129
+ providers: {
1130
+ ...config.search.providers,
1131
+ tavily: nextProvider
1132
+ }
1133
+ }, nextRefs);
1032
1134
  }
1033
- function buildMinimaxErrorMessage(payload, fallback) {
1034
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) return fallback;
1035
- const record = payload;
1036
- if (typeof record.error_description === "string" && record.error_description.trim()) return record.error_description.trim();
1037
- if (typeof record.error === "string" && record.error.trim()) return record.error.trim();
1038
- const baseMessage = record.base_resp?.status_msg;
1039
- if (typeof baseMessage === "string" && baseMessage.trim()) return baseMessage.trim();
1040
- return fallback;
1135
+ function applyBraveSearchPatch(config, patch) {
1136
+ if (!patch) return config;
1137
+ let nextRefs = config.secrets.refs;
1138
+ let nextProvider = config.search.providers.brave;
1139
+ if (Object.prototype.hasOwnProperty.call(patch, "apiKey")) {
1140
+ nextProvider = {
1141
+ ...nextProvider,
1142
+ apiKey: patch.apiKey ?? ""
1143
+ };
1144
+ nextRefs = clearSecretRef$1(nextRefs, "search.providers.brave.apiKey");
1145
+ }
1146
+ if (Object.prototype.hasOwnProperty.call(patch, "baseUrl")) nextProvider = {
1147
+ ...nextProvider,
1148
+ baseUrl: normalizeOptionalString$1(patch.baseUrl) ?? "https://api.search.brave.com/res/v1/web/search"
1149
+ };
1150
+ return replaceSearchConfig(config, {
1151
+ ...config.search,
1152
+ providers: {
1153
+ ...config.search.providers,
1154
+ brave: nextProvider
1155
+ }
1156
+ }, nextRefs);
1041
1157
  }
1042
- function classifyMiniMaxErrorStatus(message) {
1043
- const normalized = message.toLowerCase();
1044
- if (normalized.includes("deny") || normalized.includes("rejected")) return "denied";
1045
- if (normalized.includes("expired") || normalized.includes("timeout") || normalized.includes("timed out")) return "expired";
1046
- return "error";
1158
+ function applyExaSearchPatch(config, patch) {
1159
+ if (!patch) return config;
1160
+ let nextRefs = config.secrets.refs;
1161
+ let nextProvider = config.search.providers.exa;
1162
+ if (Object.prototype.hasOwnProperty.call(patch, "apiKey")) {
1163
+ nextProvider = {
1164
+ ...nextProvider,
1165
+ apiKey: patch.apiKey ?? ""
1166
+ };
1167
+ nextRefs = clearSecretRef$1(nextRefs, "search.providers.exa.apiKey");
1168
+ }
1169
+ if (Object.prototype.hasOwnProperty.call(patch, "baseUrl")) nextProvider = {
1170
+ ...nextProvider,
1171
+ baseUrl: normalizeOptionalString$1(patch.baseUrl) ?? "https://api.exa.ai/search"
1172
+ };
1173
+ return replaceSearchConfig(config, {
1174
+ ...config.search,
1175
+ providers: {
1176
+ ...config.search.providers,
1177
+ exa: nextProvider
1178
+ }
1179
+ }, nextRefs);
1047
1180
  }
1048
- function resolveHomePath(inputPath) {
1049
- const trimmed = inputPath.trim();
1050
- if (!trimmed) return trimmed;
1051
- if (trimmed === "~") return homedir();
1052
- if (trimmed.startsWith("~/")) return resolve(homedir(), trimmed.slice(2));
1053
- if (isAbsolute(trimmed)) return trimmed;
1054
- return resolve(trimmed);
1181
+ function updateSearch(configPath, patch) {
1182
+ let nextConfig = loadConfig(configPath);
1183
+ nextConfig = applyActiveSearchProviderPatch(nextConfig, patch.provider);
1184
+ nextConfig = applyEnabledSearchProvidersPatch(nextConfig, patch.enabledProviders);
1185
+ nextConfig = applySearchDefaultsPatch(nextConfig, patch.defaults);
1186
+ nextConfig = applyBochaSearchPatch(nextConfig, patch.providers?.bocha);
1187
+ nextConfig = applyTavilySearchPatch(nextConfig, patch.providers?.tavily);
1188
+ nextConfig = applyBraveSearchPatch(nextConfig, patch.providers?.brave);
1189
+ nextConfig = applyExaSearchPatch(nextConfig, patch.providers?.exa);
1190
+ const next = ConfigSchema.parse(nextConfig);
1191
+ saveConfig(next, configPath);
1192
+ return buildSearchView(next);
1055
1193
  }
1056
- function normalizeExpiresAt(value) {
1057
- if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.floor(value);
1058
- if (typeof value === "string" && value.trim()) {
1059
- const asNumber = Number(value);
1060
- if (Number.isFinite(asNumber) && asNumber > 0) return Math.floor(asNumber);
1061
- const parsedTime = Date.parse(value);
1062
- if (Number.isFinite(parsedTime) && parsedTime > 0) return parsedTime;
1194
+ //#endregion
1195
+ //#region src/features/config/stores/server-config.store.ts
1196
+ const MASK_MIN_LENGTH = 8;
1197
+ const EXTRA_SENSITIVE_PATH_PATTERNS = [
1198
+ /authorization/i,
1199
+ /cookie/i,
1200
+ /session/i,
1201
+ /bearer/i
1202
+ ];
1203
+ const PREFERRED_PROVIDER_ORDER_INDEX = new Map([
1204
+ "nextclaw",
1205
+ "openai",
1206
+ "anthropic",
1207
+ "gemini",
1208
+ "openrouter",
1209
+ "dashscope-coding-plan",
1210
+ "dashscope",
1211
+ "deepseek",
1212
+ "minimax",
1213
+ "moonshot",
1214
+ "kimi-coding",
1215
+ "zhipu"
1216
+ ].map((name, index) => [name, index]));
1217
+ const BUILTIN_PROVIDERS = listServerBuiltinProviders();
1218
+ const CUSTOM_PROVIDER_PREFIX = "custom-";
1219
+ function normalizeOptionalDisplayName(value) {
1220
+ if (typeof value !== "string") return null;
1221
+ const trimmed = value.trim();
1222
+ return trimmed.length > 0 ? trimmed : null;
1223
+ }
1224
+ function resolveCustomProviderFallbackDisplayName(name) {
1225
+ if (name.startsWith(CUSTOM_PROVIDER_PREFIX)) {
1226
+ const suffix = name.slice(7);
1227
+ if (/^\d+$/.test(suffix)) return `Custom ${suffix}`;
1063
1228
  }
1064
- return null;
1229
+ return name;
1065
1230
  }
1066
- function readFieldAsString(source, fieldName) {
1067
- if (!fieldName) return null;
1068
- const rawValue = source[fieldName];
1069
- if (typeof rawValue !== "string") return null;
1070
- const trimmed = rawValue.trim();
1071
- return trimmed.length > 0 ? trimmed : null;
1231
+ function resolveProviderInstanceDisplayName(providerId, provider, spec) {
1232
+ return normalizeOptionalDisplayName(provider?.displayName) ?? spec?.displayName ?? (providerId.startsWith(CUSTOM_PROVIDER_PREFIX) ? resolveCustomProviderFallbackDisplayName(providerId) : providerId);
1072
1233
  }
1073
- function setProviderApiKey({ configPath, provider, accessToken, defaultApiBase }) {
1074
- const config = loadConfig(configPath);
1234
+ function findNextCustomProviderName(config) {
1075
1235
  const providers = config.providers;
1076
- if (!providers[provider]) return;
1077
- const target = providers[provider];
1078
- target.apiKey = accessToken;
1079
- if (defaultApiBase) target.apiBase = defaultApiBase;
1080
- saveConfig(ConfigSchema.parse(config), configPath);
1236
+ let index = 1;
1237
+ while (providers[`${CUSTOM_PROVIDER_PREFIX}${index}`]) index += 1;
1238
+ return `${CUSTOM_PROVIDER_PREFIX}${index}`;
1081
1239
  }
1082
- function resolveProviderAuthTarget(configPath, providerId) {
1083
- const provider = loadConfig(configPath).providers[providerId];
1084
- if (!provider) return null;
1085
- const configuredType = typeof provider.providerType === "string" ? provider.providerType.trim() : "";
1086
- if (configuredType && findServerBuiltinProviderByName(configuredType)) return {
1087
- providerId,
1088
- providerType: configuredType,
1089
- provider
1090
- };
1091
- if (findServerBuiltinProviderByName(providerId)) return {
1092
- providerId,
1093
- providerType: providerId,
1094
- provider
1095
- };
1240
+ function normalizeProviderId(value) {
1241
+ if (typeof value !== "string") return null;
1242
+ const trimmed = value.trim();
1243
+ if (!trimmed || trimmed.includes("/")) return null;
1244
+ return trimmed;
1245
+ }
1246
+ function resolveProviderType(providerId, provider) {
1247
+ const configuredType = normalizeProviderId(provider?.providerType);
1248
+ if (configuredType && findServerBuiltinProviderByName(configuredType)) return configuredType;
1249
+ if (findServerBuiltinProviderByName(providerId)) return providerId;
1096
1250
  return null;
1097
1251
  }
1098
- async function startProviderAuth(configPath, providerId, options) {
1099
- cleanupExpiredAuthSessions();
1100
- const target = resolveProviderAuthTarget(configPath, providerId);
1101
- if (!target) return null;
1102
- const spec = findServerBuiltinProviderByName(target.providerType);
1103
- if (!spec?.auth || spec.auth.kind !== "device_code") return null;
1104
- const resolvedMethod = resolveAuthMethod(spec.auth, options?.methodId);
1105
- const { deviceCodeEndpoint, tokenEndpoint } = resolveDeviceCodeEndpoints(resolvedMethod.baseUrl, resolvedMethod.deviceCodePath, resolvedMethod.tokenPath);
1106
- const pkce = resolvedMethod.usePkce ? buildPkce() : null;
1107
- let authorizationCode = "";
1108
- let tokenCodeField = "device_code";
1109
- let userCode = "";
1110
- let verificationUri = "";
1111
- let intervalMs = DEFAULT_AUTH_INTERVAL_MS;
1112
- let expiresAtMs = Date.now() + 6e5;
1113
- if (resolvedMethod.protocol === "minimax_user_code") {
1114
- if (!pkce) throw new Error("MiniMax OAuth requires PKCE");
1115
- const state = toBase64Url(randomBytes(16));
1116
- const body = new URLSearchParams({
1117
- response_type: "code",
1118
- client_id: resolvedMethod.clientId,
1119
- scope: resolvedMethod.scope,
1120
- code_challenge: pkce.challenge,
1121
- code_challenge_method: "S256",
1122
- state
1123
- });
1124
- const response = await fetch(deviceCodeEndpoint, {
1125
- method: "POST",
1126
- headers: {
1127
- "Content-Type": "application/x-www-form-urlencoded",
1128
- Accept: "application/json",
1129
- "x-request-id": randomUUID()
1130
- },
1131
- body
1132
- });
1133
- const payload = await response.json().catch(() => ({}));
1134
- if (!response.ok) throw new Error(buildMinimaxErrorMessage(payload, response.statusText || "MiniMax OAuth start failed"));
1135
- if (payload.state && payload.state !== state) throw new Error("MiniMax OAuth state mismatch");
1136
- authorizationCode = payload.user_code?.trim() ?? "";
1137
- userCode = authorizationCode;
1138
- verificationUri = payload.verification_uri?.trim() ?? "";
1139
- if (!authorizationCode || !verificationUri) throw new Error("provider auth payload is incomplete");
1140
- tokenCodeField = "user_code";
1141
- intervalMs = Math.min(parsePollIntervalMs(payload.interval, DEFAULT_AUTH_INTERVAL_MS), MAX_AUTH_INTERVAL_MS);
1142
- expiresAtMs = parseExpiresAtMs(payload.expired_in, 6e5);
1143
- } else {
1144
- const body = new URLSearchParams({
1145
- client_id: resolvedMethod.clientId,
1146
- scope: resolvedMethod.scope
1147
- });
1148
- if (pkce) {
1149
- body.set("code_challenge", pkce.challenge);
1150
- body.set("code_challenge_method", "S256");
1151
- }
1152
- const response = await fetch(deviceCodeEndpoint, {
1153
- method: "POST",
1154
- headers: {
1155
- "Content-Type": "application/x-www-form-urlencoded",
1156
- Accept: "application/json"
1157
- },
1158
- body
1159
- });
1160
- const payload = await response.json().catch(() => ({}));
1161
- if (!response.ok) {
1162
- const message = payload.error_description || payload.error || response.statusText || "device code auth failed";
1163
- throw new Error(message);
1164
- }
1165
- authorizationCode = payload.device_code?.trim() ?? "";
1166
- userCode = payload.user_code?.trim() ?? "";
1167
- verificationUri = payload.verification_uri_complete?.trim() || payload.verification_uri?.trim() || "";
1168
- if (!authorizationCode || !userCode || !verificationUri) throw new Error("provider auth payload is incomplete");
1169
- intervalMs = normalizePositiveInt(payload.interval, DEFAULT_AUTH_INTERVAL_MS / 1e3) * 1e3;
1170
- const expiresInSec = normalizePositiveInt(payload.expires_in, 600);
1171
- expiresAtMs = Date.now() + expiresInSec * 1e3;
1172
- }
1173
- const sessionId = randomUUID();
1174
- authSessions.set(sessionId, {
1175
- sessionId,
1176
- providerId,
1177
- providerType: target.providerType,
1178
- configPath,
1179
- authorizationCode,
1180
- tokenCodeField,
1181
- protocol: resolvedMethod.protocol,
1182
- methodId: resolvedMethod.id,
1183
- codeVerifier: pkce?.verifier,
1184
- tokenEndpoint,
1185
- clientId: resolvedMethod.clientId,
1186
- grantType: resolvedMethod.grantType,
1187
- defaultApiBase: resolvedMethod.defaultApiBase ?? spec.defaultApiBase,
1188
- expiresAtMs,
1189
- intervalMs
1190
- });
1191
- const methodConfig = (spec.auth.methods ?? []).find((entry) => normalizeMethodId(entry.id) === resolvedMethod.id);
1192
- const methodLabel = methodConfig ? resolveLocalizedMethodLabel(methodConfig, resolvedMethod.id ?? "") : void 0;
1193
- const methodHint = methodConfig ? resolveLocalizedMethodHint(methodConfig) : void 0;
1194
- return {
1195
- provider: providerId,
1196
- kind: "device_code",
1197
- methodId: resolvedMethod.id,
1198
- sessionId,
1199
- verificationUri,
1200
- userCode,
1201
- expiresAt: new Date(expiresAtMs).toISOString(),
1202
- intervalMs,
1203
- note: methodHint ?? methodLabel ?? resolveAuthNote(spec.auth.note ?? {})
1204
- };
1205
- }
1206
- async function pollProviderAuth(params) {
1207
- const { configPath, providerName: providerId, sessionId } = params;
1208
- cleanupExpiredAuthSessions();
1209
- const session = authSessions.get(sessionId);
1210
- if (!session || session.providerId !== providerId || session.configPath !== configPath) return null;
1211
- if (Date.now() >= session.expiresAtMs) {
1212
- authSessions.delete(sessionId);
1213
- return {
1214
- provider: providerId,
1215
- status: "expired",
1216
- message: "authorization session expired"
1217
- };
1218
- }
1219
- const body = new URLSearchParams({
1220
- grant_type: session.grantType,
1221
- client_id: session.clientId
1222
- });
1223
- body.set(session.tokenCodeField, session.authorizationCode);
1224
- if (session.codeVerifier) body.set("code_verifier", session.codeVerifier);
1225
- const response = await fetch(session.tokenEndpoint, {
1226
- method: "POST",
1227
- headers: {
1228
- "Content-Type": "application/x-www-form-urlencoded",
1229
- Accept: "application/json"
1230
- },
1231
- body
1232
- });
1233
- let accessToken = "";
1234
- if (session.protocol === "minimax_user_code") {
1235
- const raw = await response.text();
1236
- let payload = {};
1237
- if (raw) try {
1238
- payload = JSON.parse(raw);
1239
- } catch {
1240
- payload = {};
1241
- }
1242
- if (!response.ok) return {
1243
- provider: providerId,
1244
- status: "error",
1245
- message: buildMinimaxErrorMessage(payload, raw || response.statusText || "authorization failed")
1246
- };
1247
- const status = payload.status?.trim().toLowerCase();
1248
- if (status === "success") {
1249
- accessToken = payload.access_token?.trim() ?? "";
1250
- if (!accessToken) return {
1251
- provider: providerId,
1252
- status: "error",
1253
- message: "provider token response missing access token"
1254
- };
1255
- } else if (status === "error") {
1256
- const message = buildMinimaxErrorMessage(payload, "authorization failed");
1257
- const classified = classifyMiniMaxErrorStatus(message);
1258
- if (classified === "denied" || classified === "expired") authSessions.delete(sessionId);
1259
- return {
1260
- provider: providerId,
1261
- status: classified,
1262
- message
1263
- };
1264
- } else {
1265
- const nextPollMs = Math.min(Math.floor(session.intervalMs * 1.5), MAX_AUTH_INTERVAL_MS);
1266
- session.intervalMs = nextPollMs;
1267
- authSessions.set(sessionId, session);
1268
- return {
1269
- provider: providerId,
1270
- status: "pending",
1271
- nextPollMs
1272
- };
1273
- }
1274
- } else {
1275
- const payload = await response.json().catch(() => ({}));
1276
- if (!response.ok) {
1277
- const errorCode = payload.error?.trim().toLowerCase();
1278
- if (errorCode === "authorization_pending") return {
1279
- provider: providerId,
1280
- status: "pending",
1281
- nextPollMs: session.intervalMs
1282
- };
1283
- if (errorCode === "slow_down") {
1284
- const nextPollMs = Math.min(Math.floor(session.intervalMs * 1.5), MAX_AUTH_INTERVAL_MS);
1285
- session.intervalMs = nextPollMs;
1286
- authSessions.set(sessionId, session);
1287
- return {
1288
- provider: providerId,
1289
- status: "pending",
1290
- nextPollMs
1291
- };
1292
- }
1293
- if (errorCode === "access_denied") {
1294
- authSessions.delete(sessionId);
1295
- return {
1296
- provider: providerId,
1297
- status: "denied",
1298
- message: payload.error_description || "authorization denied"
1299
- };
1300
- }
1301
- if (errorCode === "expired_token") {
1302
- authSessions.delete(sessionId);
1303
- return {
1304
- provider: providerId,
1305
- status: "expired",
1306
- message: payload.error_description || "authorization session expired"
1307
- };
1308
- }
1309
- return {
1310
- provider: providerId,
1311
- status: "error",
1312
- message: payload.error_description || payload.error || response.statusText || "authorization failed"
1313
- };
1314
- }
1315
- accessToken = payload.access_token?.trim() ?? "";
1316
- if (!accessToken) return {
1317
- provider: providerId,
1318
- status: "error",
1319
- message: "provider token response missing access token"
1320
- };
1321
- }
1322
- setProviderApiKey({
1323
- configPath,
1324
- provider: providerId,
1325
- accessToken,
1326
- defaultApiBase: session.defaultApiBase
1327
- });
1328
- authSessions.delete(sessionId);
1329
- return {
1330
- provider: providerId,
1331
- status: "authorized"
1332
- };
1333
- }
1334
- async function importProviderAuthFromCli(configPath, providerId) {
1335
- const target = resolveProviderAuthTarget(configPath, providerId);
1336
- if (!target) return null;
1337
- const spec = findServerBuiltinProviderByName(target.providerType);
1338
- if (!spec?.auth || spec.auth.kind !== "device_code" || !spec.auth.cliCredential) return null;
1339
- const credentialPath = resolveHomePath(spec.auth.cliCredential.path);
1340
- if (!credentialPath) throw new Error("provider cli credential path is empty");
1341
- let rawContent = "";
1342
- try {
1343
- rawContent = await readFile(credentialPath, "utf8");
1344
- } catch (error) {
1345
- const message = error instanceof Error ? error.message : String(error);
1346
- throw new Error(`failed to read CLI credential: ${message}`);
1347
- }
1348
- let payload;
1349
- try {
1350
- const parsed = JSON.parse(rawContent);
1351
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("credential payload is not an object");
1352
- payload = parsed;
1353
- } catch (error) {
1354
- const message = error instanceof Error ? error.message : String(error);
1355
- throw new Error(`invalid CLI credential JSON: ${message}`);
1356
- }
1357
- const accessToken = readFieldAsString(payload, spec.auth.cliCredential.accessTokenField);
1358
- if (!accessToken) throw new Error(`CLI credential missing access token field: ${spec.auth.cliCredential.accessTokenField}`);
1359
- const expiresAtMs = normalizeExpiresAt(spec.auth.cliCredential.expiresAtField ? payload[spec.auth.cliCredential.expiresAtField] : void 0);
1360
- if (typeof expiresAtMs === "number" && expiresAtMs <= Date.now()) throw new Error("CLI credential has expired, please login again");
1361
- setProviderApiKey({
1362
- configPath,
1363
- provider: providerId,
1364
- accessToken,
1365
- defaultApiBase: spec.defaultApiBase
1366
- });
1367
- return {
1368
- provider: providerId,
1369
- status: "imported",
1370
- source: "cli",
1371
- expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : void 0
1372
- };
1373
- }
1374
- //#endregion
1375
- //#region src/features/config/controllers/config.controller.ts
1376
- var ConfigRoutesController = class {
1377
- channelConfigApplyTasks = /* @__PURE__ */ new Map();
1378
- constructor(options) {
1379
- this.options = options;
1380
- }
1381
- getExtensionConfigProjectionOptions = () => {
1382
- return {
1383
- extensionChannelBindings: this.options.extensions?.getChannelBindings() ?? [],
1384
- extensionUiMetadata: this.options.extensions?.getUiMetadata() ?? []
1385
- };
1386
- };
1387
- publishConfigUpdatedPaths = (paths) => {
1388
- for (const path of paths) emitConfigUpdated(this.options, path);
1389
- };
1390
- publishConfigUpdates = async (paths) => {
1391
- this.publishConfigUpdatedPaths(paths);
1392
- await this.options.applyLiveConfigReload?.();
1393
- };
1394
- publishChannelConfigApplyStatus = (params) => {
1395
- emitChannelConfigApplyStatus(this.options, params);
1396
- };
1397
- enqueueChannelConfigApply = (channel) => {
1398
- const task = (this.channelConfigApplyTasks.get(channel) ?? Promise.resolve()).catch(() => void 0).then(async () => {
1399
- this.publishChannelConfigApplyStatus({
1400
- channel,
1401
- status: "started"
1402
- });
1403
- try {
1404
- await this.options.applyLiveConfigReload?.();
1405
- this.publishChannelConfigApplyStatus({
1406
- channel,
1407
- status: "succeeded"
1408
- });
1409
- } catch (error) {
1410
- const message = error instanceof Error ? error.message : String(error);
1411
- this.publishChannelConfigApplyStatus({
1412
- channel,
1413
- status: "failed",
1414
- message
1415
- });
1416
- emitUiError(this.options, {
1417
- code: "CHANNEL_CONFIG_APPLY_FAILED",
1418
- message: `Failed to apply ${channel} channel config: ${message}`
1419
- });
1420
- }
1421
- }).finally(() => {
1422
- if (this.channelConfigApplyTasks.get(channel) === task) this.channelConfigApplyTasks.delete(channel);
1423
- });
1424
- this.channelConfigApplyTasks.set(channel, task);
1425
- };
1426
- getConfig = (c) => {
1427
- const config = loadConfigOrDefault(this.options.configPath);
1428
- return c.json(ok(buildConfigView(config, this.getExtensionConfigProjectionOptions())));
1429
- };
1430
- getConfigMeta = (c) => {
1431
- const config = loadConfigOrDefault(this.options.configPath);
1432
- return c.json(ok(buildConfigMeta(config, this.getExtensionConfigProjectionOptions())));
1433
- };
1434
- listProviders = (c) => {
1435
- const config = loadConfigOrDefault(this.options.configPath);
1436
- return c.json(ok(buildProvidersView(config)));
1437
- };
1438
- listProviderTemplates = (c) => {
1439
- return c.json(ok(buildProviderTemplatesView()));
1440
- };
1441
- getConfigSchema = (c) => {
1442
- const config = loadConfigOrDefault(this.options.configPath);
1443
- return c.json(ok(buildConfigSchemaView(config, this.getExtensionConfigProjectionOptions())));
1444
- };
1445
- updateConfigModel = async (c) => {
1446
- const body = await readJson(c.req.raw);
1447
- if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
1448
- const hasModel = typeof body.data.model === "string";
1449
- if (!hasModel) return c.json(err("INVALID_BODY", "model is required"), 400);
1450
- const view = updateModel(this.options.configPath, {
1451
- model: body.data.model,
1452
- workspace: body.data.workspace
1453
- });
1454
- const changedPaths = [];
1455
- if (hasModel) changedPaths.push("agents.defaults.model");
1456
- if (typeof body.data.workspace === "string") changedPaths.push("agents.defaults.workspace");
1457
- await this.publishConfigUpdates(changedPaths);
1458
- return c.json(ok({
1459
- model: view.agents.defaults.model,
1460
- workspace: view.agents.defaults.workspace
1461
- }));
1462
- };
1463
- updateConfigSearch = async (c) => {
1464
- const body = await readJson(c.req.raw);
1465
- if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
1466
- const result = updateSearch(this.options.configPath, body.data);
1467
- await this.publishConfigUpdates(["search"]);
1468
- return c.json(ok(result));
1469
- };
1470
- updateProvider = async (c) => {
1471
- const providerId = c.req.param("providerId");
1472
- const body = await readJson(c.req.raw);
1473
- if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
1474
- const result = updateProvider(this.options.configPath, providerId, body.data);
1475
- if (!result) return c.json(err("NOT_FOUND", `unknown provider: ${providerId}`), 404);
1476
- await this.publishConfigUpdates([`providers.${providerId}`]);
1477
- return c.json(ok(result));
1478
- };
1479
- createProvider = async (c) => {
1480
- const body = await readJson(c.req.raw);
1481
- if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
1482
- const result = createProvider(this.options.configPath, body.data);
1483
- if (!result) return c.json(err("PROVIDER_EXISTS", "provider already exists"), 409);
1484
- await this.publishConfigUpdates([`providers.${result.providerId}`]);
1485
- return c.json(ok({
1486
- providerId: result.providerId,
1487
- provider: result.provider
1488
- }));
1489
- };
1490
- deleteProvider = async (c) => {
1491
- const providerId = c.req.param("providerId");
1492
- if (deleteProvider(this.options.configPath, providerId) === null) return c.json(err("NOT_FOUND", `provider not found: ${providerId}`), 404);
1493
- await this.publishConfigUpdates([`providers.${providerId}`]);
1494
- return c.json(ok({
1495
- deleted: true,
1496
- providerId
1497
- }));
1498
- };
1499
- testProviderConnection = async (c) => {
1500
- const provider = c.req.param("providerId");
1501
- const body = await readJson(c.req.raw);
1502
- if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
1503
- const result = await testProviderConnection(this.options.configPath, provider, body.data, this.options.kernel.llmProviders);
1504
- if (!result) return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
1505
- return c.json(ok(result));
1506
- };
1507
- startProviderAuth = async (c) => {
1508
- const provider = c.req.param("providerId");
1509
- let payload = {};
1510
- const rawBody = await c.req.raw.text();
1511
- if (rawBody.trim().length > 0) try {
1512
- payload = JSON.parse(rawBody);
1513
- } catch {
1514
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
1515
- }
1516
- const methodId = typeof payload.methodId === "string" ? payload.methodId.trim() : void 0;
1517
- try {
1518
- const result = await startProviderAuth(this.options.configPath, provider, { methodId });
1519
- if (!result) return c.json(err("NOT_SUPPORTED", `provider auth is not supported: ${provider}`), 404);
1520
- return c.json(ok(result));
1521
- } catch (error) {
1522
- const message = error instanceof Error ? error.message : String(error);
1523
- return c.json(err("AUTH_START_FAILED", message), 400);
1524
- }
1525
- };
1526
- pollProviderAuth = async (c) => {
1527
- const provider = c.req.param("providerId");
1528
- const body = await readJson(c.req.raw);
1529
- if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
1530
- const sessionId = typeof body.data.sessionId === "string" ? body.data.sessionId.trim() : "";
1531
- if (!sessionId) return c.json(err("INVALID_BODY", "sessionId is required"), 400);
1532
- const result = await pollProviderAuth({
1533
- configPath: this.options.configPath,
1534
- providerName: provider,
1535
- sessionId
1536
- });
1537
- if (!result) return c.json(err("NOT_FOUND", "provider auth session not found"), 404);
1538
- if (result.status === "authorized") await this.publishConfigUpdates([`providers.${provider}`]);
1539
- return c.json(ok(result));
1540
- };
1541
- importProviderAuthFromCli = async (c) => {
1542
- const provider = c.req.param("providerId");
1543
- try {
1544
- const result = await importProviderAuthFromCli(this.options.configPath, provider);
1545
- if (!result) return c.json(err("NOT_SUPPORTED", `provider cli auth import is not supported: ${provider}`), 404);
1546
- await this.publishConfigUpdates([`providers.${provider}`]);
1547
- return c.json(ok(result));
1548
- } catch (error) {
1549
- const message = error instanceof Error ? error.message : String(error);
1550
- return c.json(err("AUTH_IMPORT_FAILED", message), 400);
1551
- }
1552
- };
1553
- updateChannel = async (c) => {
1554
- const channel = c.req.param("channel");
1555
- const body = await readJson(c.req.raw);
1556
- if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
1557
- const result = updateChannel(this.options.configPath, channel, body.data, this.getExtensionConfigProjectionOptions());
1558
- if (!result) return c.json(err("NOT_FOUND", `unknown channel: ${channel}`), 404);
1559
- this.publishConfigUpdatedPaths([`channels.${channel}`]);
1560
- this.enqueueChannelConfigApply(channel);
1561
- return c.json(ok(result));
1562
- };
1563
- startChannelAuth = async (c) => {
1564
- const channel = c.req.param("channel");
1565
- let payload = {};
1566
- const rawBody = await c.req.raw.text();
1567
- if (rawBody.trim().length > 0) try {
1568
- payload = JSON.parse(rawBody);
1569
- } catch {
1570
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
1571
- }
1572
- try {
1573
- const result = await startChannelAuth({
1574
- configPath: this.options.configPath,
1575
- channelId: channel,
1576
- request: {
1577
- accountId: typeof payload.accountId === "string" ? payload.accountId : void 0,
1578
- baseUrl: typeof payload.baseUrl === "string" ? payload.baseUrl : void 0,
1579
- domain: typeof payload.domain === "string" ? payload.domain : void 0
1580
- },
1581
- bindings: this.options.extensions?.getChannelBindings() ?? []
1582
- });
1583
- if (!result) return c.json(err("NOT_SUPPORTED", `channel auth is not supported: ${channel}`), 404);
1584
- return c.json(ok(result));
1585
- } catch (error) {
1586
- const message = error instanceof Error ? error.message : String(error);
1587
- return c.json(err("AUTH_START_FAILED", message), 400);
1588
- }
1589
- };
1590
- pollChannelAuth = async (c) => {
1591
- const channel = c.req.param("channel");
1592
- const body = await readJson(c.req.raw);
1593
- if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
1594
- const sessionId = typeof body.data.sessionId === "string" ? body.data.sessionId.trim() : "";
1595
- if (!sessionId) return c.json(err("INVALID_BODY", "sessionId is required"), 400);
1596
- const result = await pollChannelAuth({
1597
- configPath: this.options.configPath,
1598
- channelId: channel,
1599
- sessionId,
1600
- bindings: this.options.extensions?.getChannelBindings() ?? []
1601
- });
1602
- if (!result) return c.json(err("NOT_FOUND", "channel auth session not found"), 404);
1603
- if (result.status === "authorized") await this.publishConfigUpdates([`channels.${channel}`]);
1604
- return c.json(ok(result));
1605
- };
1606
- connectChannelAuth = async (c) => {
1607
- const channel = c.req.param("channel");
1608
- const body = await readJson(c.req.raw);
1609
- if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
1610
- const fields = body.data.fields && typeof body.data.fields === "object" && !Array.isArray(body.data.fields) ? body.data.fields : {};
1611
- try {
1612
- const result = await connectChannelAuth({
1613
- configPath: this.options.configPath,
1614
- channelId: channel,
1615
- request: {
1616
- accountId: typeof body.data.accountId === "string" ? body.data.accountId : void 0,
1617
- domain: typeof body.data.domain === "string" ? body.data.domain : void 0,
1618
- fields
1619
- },
1620
- bindings: this.options.extensions?.getChannelBindings() ?? []
1621
- });
1622
- if (!result) return c.json(err("NOT_SUPPORTED", `channel auth connect is not supported: ${channel}`), 404);
1623
- if (result.status === "authorized") await this.publishConfigUpdates([`channels.${channel}`]);
1624
- return c.json(ok(result));
1625
- } catch (error) {
1626
- const message = error instanceof Error ? error.message : String(error);
1627
- return c.json(err("AUTH_CONNECT_FAILED", message), 400);
1252
+ function findNextProviderId(config, baseProviderId) {
1253
+ const providers = config.providers;
1254
+ let providerId = baseProviderId;
1255
+ let index = 2;
1256
+ while (providers[providerId]) {
1257
+ providerId = `${baseProviderId}-${index}`;
1258
+ index += 1;
1259
+ }
1260
+ return providerId;
1261
+ }
1262
+ function resolveProviderDisplayNameSuffix(providerId, baseProviderId) {
1263
+ if (providerId === baseProviderId) return "";
1264
+ const suffix = providerId.slice(baseProviderId.length + 1).trim();
1265
+ return suffix ? ` ${suffix}` : "";
1266
+ }
1267
+ function buildProviderScopedModels(providerId, models) {
1268
+ return normalizeModelList(models).map((model) => {
1269
+ const slashIndex = model.indexOf("/");
1270
+ const modelSuffix = slashIndex >= 0 ? model.slice(slashIndex + 1).trim() : model;
1271
+ return modelSuffix ? `${providerId}/${modelSuffix}` : "";
1272
+ }).filter(Boolean);
1273
+ }
1274
+ function clearSecretRefsByPrefix(refs, pathPrefix) {
1275
+ return Object.fromEntries(Object.entries(refs).filter(([key]) => key !== pathPrefix && !key.startsWith(`${pathPrefix}.`)));
1276
+ }
1277
+ function matchesExtraSensitivePath(path) {
1278
+ return path !== "session" && !path.startsWith("session.") && EXTRA_SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(path));
1279
+ }
1280
+ function matchHint(path, hints) {
1281
+ const direct = hints[path];
1282
+ if (direct) return direct;
1283
+ const segments = path.split(".");
1284
+ for (const [hintKey, hint] of Object.entries(hints)) {
1285
+ if (!hintKey.includes("*")) continue;
1286
+ const hintSegments = hintKey.split(".");
1287
+ if (hintSegments.length !== segments.length) continue;
1288
+ let match = true;
1289
+ for (let index = 0; index < segments.length; index += 1) if (hintSegments[index] !== "*" && hintSegments[index] !== segments[index]) {
1290
+ match = false;
1291
+ break;
1628
1292
  }
1629
- };
1630
- updateSecrets = async (c) => {
1631
- const body = await readJson(c.req.raw);
1632
- if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
1633
- const result = updateSecrets(this.options.configPath, body.data);
1634
- await this.publishConfigUpdates(["secrets"]);
1635
- return c.json(ok(result));
1636
- };
1637
- updateRuntime = async (c) => {
1638
- const body = await readJson(c.req.raw);
1639
- if (!body.ok || !body.data || typeof body.data !== "object") return c.json(err("INVALID_BODY", "invalid json body"), 400);
1640
- const result = updateRuntime(this.options.configPath, body.data);
1641
- const changedPaths = [];
1642
- if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "contextTokens")) changedPaths.push("agents.defaults.contextTokens");
1643
- if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engine")) changedPaths.push("agents.defaults.engine");
1644
- if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engineConfig")) changedPaths.push("agents.defaults.engineConfig");
1645
- if (body.data.agents?.runtimes && Object.prototype.hasOwnProperty.call(body.data.agents.runtimes, "entries")) changedPaths.push("agents.runtimes.entries");
1646
- if (body.data.companion && Object.prototype.hasOwnProperty.call(body.data.companion, "enabled")) changedPaths.push("companion.enabled");
1647
- changedPaths.push("agents.list", "bindings", "session");
1648
- await this.publishConfigUpdates(changedPaths);
1649
- return c.json(ok(result));
1650
- };
1651
- executeAction = async (c) => {
1652
- const actionId = c.req.param("actionId");
1653
- const body = await readJson(c.req.raw);
1654
- if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
1655
- const result = await executeConfigAction(this.options.configPath, actionId, body.data ?? {});
1656
- if (!result.ok) return c.json(err(result.code, result.message, result.details), 400);
1657
- return c.json(ok(result.data));
1658
- };
1659
- };
1660
- //#endregion
1661
- //#region src/features/config/utils/default-provider-config.utils.ts
1662
- function createDefaultProviderConfig(defaultWireApi = "auto", defaultModels = [], modelConfig = {}, providerType, defaultApiBase = null) {
1663
- return {
1664
- enabled: true,
1665
- providerType,
1666
- displayName: "",
1667
- apiKey: "",
1668
- apiBase: defaultApiBase,
1669
- extraHeaders: null,
1670
- wireApi: defaultWireApi,
1671
- models: [...defaultModels],
1672
- modelConfig
1673
- };
1293
+ if (match) return hint;
1294
+ }
1674
1295
  }
1675
- function createDefaultProviderConfigFromSpec(spec) {
1676
- return createDefaultProviderConfig(spec?.defaultWireApi ?? "auto", spec?.defaultModels ?? [], normalizeProviderModelConfig(spec?.modelConfig ?? {}), spec?.name, spec?.defaultApiBase ?? null);
1296
+ function isSensitivePath(path, hints) {
1297
+ if (hints) {
1298
+ const hint = matchHint(path, hints);
1299
+ if (hint?.sensitive !== void 0) return Boolean(hint.sensitive);
1300
+ }
1301
+ return isSensitiveConfigPath(path) || matchesExtraSensitivePath(path);
1677
1302
  }
1678
- //#endregion
1679
- //#region src/features/config/utils/runtime-entry-config.utils.ts
1680
- function normalizeOptionalString$2(value) {
1681
- if (typeof value !== "string") return null;
1682
- const trimmed = value.trim();
1683
- return trimmed.length > 0 ? trimmed : null;
1303
+ function sanitizePublicConfigValue(value, prefix, hints) {
1304
+ if (Array.isArray(value)) {
1305
+ const nextPath = prefix ? `${prefix}[]` : "[]";
1306
+ return value.map((entry) => sanitizePublicConfigValue(entry, nextPath, hints));
1307
+ }
1308
+ if (!value || typeof value !== "object") return value;
1309
+ const output = {};
1310
+ for (const [key, val] of Object.entries(value)) {
1311
+ const nextPath = prefix ? `${prefix}.${key}` : key;
1312
+ if (isSensitivePath(nextPath, hints)) continue;
1313
+ output[key] = sanitizePublicConfigValue(val, nextPath, hints);
1314
+ }
1315
+ return output;
1684
1316
  }
1685
- function normalizePositiveInteger(value) {
1686
- const parsed = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : NaN;
1687
- if (!Number.isFinite(parsed)) return null;
1688
- const normalized = Math.trunc(parsed);
1689
- return normalized > 0 ? normalized : null;
1317
+ function isObject(value) {
1318
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1690
1319
  }
1691
- function normalizeStringArray(value) {
1692
- if (!Array.isArray(value)) return null;
1693
- const entries = value.map((entry) => normalizeOptionalString$2(entry)).filter((entry) => Boolean(entry));
1694
- return entries.length > 0 ? entries : null;
1320
+ function deepMerge(base, patch) {
1321
+ if (!isObject(base) || !isObject(patch)) return patch;
1322
+ const result = { ...base };
1323
+ for (const [key, value] of Object.entries(patch)) {
1324
+ const previous = result[key];
1325
+ result[key] = deepMerge(previous, value);
1326
+ }
1327
+ return result;
1695
1328
  }
1696
- function normalizeUnknownStringRecord(value) {
1697
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1698
- const entries = Object.entries(value).map(([key, entryValue]) => [key.trim(), normalizeOptionalString$2(entryValue)]).filter(([key, entryValue]) => key.length > 0 && Boolean(entryValue));
1699
- return entries.length > 0 ? Object.fromEntries(entries) : null;
1329
+ function getPathValue(source, path) {
1330
+ if (!source || typeof source !== "object") return;
1331
+ const segments = path.split(".");
1332
+ let current = source;
1333
+ for (const segment of segments) {
1334
+ if (!current || typeof current !== "object") return;
1335
+ current = current[segment];
1336
+ }
1337
+ return current;
1700
1338
  }
1701
- function normalizeRuntimeModelSelectionMode(value) {
1702
- if (value === "nextclaw" || value === "optional" || value === "runtime-default") return value;
1703
- return null;
1339
+ function setPathValue(target, path, value) {
1340
+ const segments = path.split(".");
1341
+ if (segments.length === 0) return;
1342
+ let current = target;
1343
+ for (let index = 0; index < segments.length - 1; index += 1) {
1344
+ const segment = segments[index];
1345
+ const next = current[segment];
1346
+ if (!isObject(next)) current[segment] = {};
1347
+ current = current[segment];
1348
+ }
1349
+ current[segments[segments.length - 1]] = value;
1704
1350
  }
1705
- function normalizeRuntimeEntryConfig(type, config) {
1706
- const runtimeDefaultThinking = normalizeModelThinkingCapability(config.runtimeDefaultThinking);
1707
- if (type !== "narp-stdio") return {
1708
- ...Object.fromEntries(Object.entries(config).filter(([key]) => key !== "runtimeDefaultThinking")),
1709
- ...runtimeDefaultThinking ? { runtimeDefaultThinking } : {}
1351
+ function isMissingRequiredValue(value) {
1352
+ if (value === void 0 || value === null) return true;
1353
+ if (typeof value === "string") return value.trim().length === 0;
1354
+ if (Array.isArray(value)) return value.length === 0;
1355
+ return false;
1356
+ }
1357
+ function resolveRuntimeConfig(config, draftConfig) {
1358
+ if (!draftConfig || Object.keys(draftConfig).length === 0) return config;
1359
+ const merged = deepMerge(config, draftConfig);
1360
+ return ConfigSchema.parse(merged);
1361
+ }
1362
+ function getActionById(config, actionId) {
1363
+ return buildConfigSchemaView(config).actions.find((item) => item.id === actionId) ?? null;
1364
+ }
1365
+ function messageOrDefault(action, kind, fallback) {
1366
+ const text = kind === "success" ? action.success?.message : action.failure?.message;
1367
+ return text?.trim() ? text : fallback;
1368
+ }
1369
+ async function runFeishuVerifyAction(params) {
1370
+ const { config, action } = params;
1371
+ const appId = String(config.channels.feishu.appId ?? "").trim();
1372
+ const appSecret = String(config.channels.feishu.appSecret ?? "").trim();
1373
+ if (!appId || !appSecret) return {
1374
+ ok: false,
1375
+ status: "failed",
1376
+ message: messageOrDefault(action, "failure", "Verification failed: missing credentials"),
1377
+ data: { error: "missing credentials (appId, appSecret)" },
1378
+ nextActions: []
1710
1379
  };
1711
- const command = normalizeOptionalString$2(config.command);
1712
- const args = normalizeStringArray(config.args);
1713
- const cwd = normalizeOptionalString$2(config.cwd);
1714
- const wireDialect = normalizeOptionalString$2(config.wireDialect) ?? "acp";
1715
- const processScope = normalizeOptionalString$2(config.processScope) ?? "per-session";
1716
- const modelSelectionMode = normalizeRuntimeModelSelectionMode(config.modelSelectionMode);
1717
- const supportedModels = normalizeStringArray(config.supportedModels);
1718
- const model = normalizeOptionalString$2(config.model);
1719
- const recommendedModel = normalizeOptionalString$2(config.recommendedModel);
1380
+ const result = await probeFeishu(appId, appSecret);
1381
+ if (!result.ok) return {
1382
+ ok: false,
1383
+ status: "failed",
1384
+ message: `${messageOrDefault(action, "failure", "Verification failed")}: ${result.error}`,
1385
+ data: {
1386
+ error: result.error,
1387
+ appId: result.appId ?? appId
1388
+ },
1389
+ nextActions: []
1390
+ };
1391
+ const responseData = {
1392
+ appId: result.appId,
1393
+ botName: result.botName ?? null,
1394
+ botOpenId: result.botOpenId ?? null
1395
+ };
1396
+ const patch = {};
1397
+ for (const [targetPath, sourcePath] of Object.entries(action.resultMap ?? {})) {
1398
+ const mappedValue = sourcePath.startsWith("response.data.") ? responseData[sourcePath.slice(14)] : void 0;
1399
+ if (mappedValue !== void 0) setPathValue(patch, targetPath, mappedValue);
1400
+ }
1720
1401
  return {
1721
- wireDialect,
1722
- processScope,
1723
- ...command ? { command } : {},
1724
- ...args ? { args } : {},
1725
- env: normalizeUnknownStringRecord(config.env) ?? {},
1726
- ...cwd ? { cwd } : {},
1727
- ...modelSelectionMode ? { modelSelectionMode } : {},
1728
- ...model ? { model } : {},
1729
- ...recommendedModel ? { recommendedModel } : {},
1730
- ...supportedModels ? { supportedModels } : {},
1731
- ...runtimeDefaultThinking ? { runtimeDefaultThinking } : {},
1732
- startupTimeoutMs: normalizePositiveInteger(config.startupTimeoutMs) ?? 8e3,
1733
- probeTimeoutMs: normalizePositiveInteger(config.probeTimeoutMs) ?? 3e3,
1734
- requestTimeoutMs: normalizePositiveInteger(config.requestTimeoutMs) ?? 12e4
1402
+ ok: true,
1403
+ status: "success",
1404
+ message: messageOrDefault(action, "success", "Verified. Please finish Feishu event subscription and app publishing before using."),
1405
+ data: responseData,
1406
+ patch: Object.keys(patch).length > 0 ? patch : void 0,
1407
+ nextActions: []
1735
1408
  };
1736
1409
  }
1737
- //#endregion
1738
- //#region src/features/config/utils/search-config.utils.ts
1739
- const MASK_MIN_LENGTH$1 = 8;
1740
- const BOCHA_OPEN_URL = "https://open.bocha.cn";
1741
- const TAVILY_DOCS_URL = "https://docs.tavily.com/documentation/api-reference/endpoint/search";
1742
- const SEARCH_PROVIDER_NAMES = [
1743
- "bocha",
1744
- "tavily",
1745
- "brave"
1746
- ];
1747
- function normalizeOptionalString$1(value) {
1748
- if (typeof value !== "string") return null;
1749
- const trimmed = value.trim();
1750
- return trimmed.length > 0 ? trimmed : null;
1410
+ const ACTION_HANDLERS = { "channels.feishu.verifyConnection": runFeishuVerifyAction };
1411
+ function buildUiHints(config, options) {
1412
+ return buildConfigSchemaView(config, options).uiHints;
1751
1413
  }
1752
- function maskApiKey$1(value) {
1414
+ function maskApiKey(value) {
1753
1415
  if (!value) return { apiKeySet: false };
1754
- if (value.length < MASK_MIN_LENGTH$1) return {
1416
+ if (value.length < MASK_MIN_LENGTH) return {
1755
1417
  apiKeySet: true,
1756
1418
  apiKeyMasked: "****"
1757
1419
  };
@@ -1760,949 +1422,1412 @@ function maskApiKey$1(value) {
1760
1422
  apiKeyMasked: `${value.slice(0, 2)}****${value.slice(-4)}`
1761
1423
  };
1762
1424
  }
1763
- function clearSecretRef$1(refs, path) {
1764
- if (!refs[path]) return refs;
1765
- const nextRefs = { ...refs };
1766
- delete nextRefs[path];
1425
+ function normalizeModelList(input) {
1426
+ if (!input || input.length === 0) return [];
1427
+ const deduped = /* @__PURE__ */ new Set();
1428
+ for (const item of input) {
1429
+ if (typeof item !== "string") continue;
1430
+ const trimmed = item.trim();
1431
+ if (!trimmed) continue;
1432
+ deduped.add(trimmed);
1433
+ }
1434
+ return [...deduped];
1435
+ }
1436
+ function toProviderView(config, provider, providerId, uiHints, spec) {
1437
+ const providerType = resolveProviderType(providerId, provider);
1438
+ const apiKeyRefSet = hasSecretRef(config, `providers.${providerId}.apiKey`);
1439
+ const masked = maskApiKey(provider.apiKey);
1440
+ const extraHeaders = provider.extraHeaders && Object.keys(provider.extraHeaders).length > 0 ? sanitizePublicConfigValue(provider.extraHeaders, `providers.${providerId}.extraHeaders`, uiHints) : null;
1441
+ const supportsWireApi = Boolean(spec?.supportsWireApi) || providerType === null;
1442
+ return {
1443
+ providerId,
1444
+ providerType,
1445
+ isBuiltInType: providerType !== null,
1446
+ isCustom: providerType === null,
1447
+ enabled: provider.enabled !== false,
1448
+ displayName: resolveProviderInstanceDisplayName(providerId, provider, spec),
1449
+ apiKeyRequired: !spec?.anonymousApiKey,
1450
+ apiKeySet: masked.apiKeySet || apiKeyRefSet,
1451
+ apiKeyMasked: masked.apiKeyMasked ?? (apiKeyRefSet ? "****" : void 0),
1452
+ apiBase: provider.apiBase ?? null,
1453
+ extraHeaders: extraHeaders && Object.keys(extraHeaders).length > 0 ? extraHeaders : null,
1454
+ models: normalizeModelList(provider.models ?? []),
1455
+ modelConfig: normalizeProviderModelConfig(provider.modelConfig ?? {}),
1456
+ wireApi: supportsWireApi ? provider.wireApi ?? spec?.defaultWireApi ?? "auto" : void 0
1457
+ };
1458
+ }
1459
+ function buildConfigView(config, options) {
1460
+ const uiHints = buildUiHints(config, options);
1461
+ const projectedChannels = getProjectedChannelMap(config, options);
1462
+ const providers = {};
1463
+ for (const [providerId, provider] of Object.entries(config.providers)) {
1464
+ const providerConfig = provider;
1465
+ providers[providerId] = toProviderView(config, providerConfig, providerId, uiHints, findServerBuiltinProviderByName(resolveProviderType(providerId, providerConfig) ?? ""));
1466
+ }
1467
+ return {
1468
+ companion: sanitizePublicConfigValue(config.companion, "companion", uiHints),
1469
+ agents: sanitizePublicConfigValue(config.agents, "agents", uiHints),
1470
+ providers,
1471
+ search: buildSearchView(config),
1472
+ channels: sanitizePublicConfigValue(projectedChannels, "channels", uiHints),
1473
+ bindings: sanitizePublicConfigValue(config.bindings, "bindings", uiHints),
1474
+ session: sanitizePublicConfigValue(config.session, "session", uiHints),
1475
+ tools: sanitizePublicConfigValue(config.tools, "tools", uiHints),
1476
+ gateway: sanitizePublicConfigValue(config.gateway, "gateway", uiHints),
1477
+ ui: sanitizePublicConfigValue(config.ui, "ui", uiHints),
1478
+ secrets: {
1479
+ enabled: config.secrets.enabled,
1480
+ defaults: { ...config.secrets.defaults },
1481
+ providers: { ...config.secrets.providers },
1482
+ refs: { ...config.secrets.refs }
1483
+ }
1484
+ };
1485
+ }
1486
+ function normalizeRuntimeEntries(entries) {
1487
+ if (!entries || typeof entries !== "object" || Array.isArray(entries)) return {};
1488
+ const normalized = {};
1489
+ for (const [rawId, rawEntry] of Object.entries(entries)) {
1490
+ const id = rawId.trim();
1491
+ if (!id || !rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
1492
+ const entry = rawEntry;
1493
+ const type = normalizeOptionalString(entry.type);
1494
+ if (!type) continue;
1495
+ const normalizedIcon = normalizeRuntimeEntryIcon(entry.icon);
1496
+ normalized[id] = {
1497
+ enabled: typeof entry.enabled === "boolean" ? entry.enabled : true,
1498
+ ...normalizeOptionalString(entry.label) ? { label: normalizeOptionalString(entry.label) ?? void 0 } : {},
1499
+ ...normalizedIcon ? { icon: normalizedIcon } : {},
1500
+ type,
1501
+ config: normalizeRuntimeEntryConfig(type, entry.config && typeof entry.config === "object" && !Array.isArray(entry.config) ? entry.config : {})
1502
+ };
1503
+ }
1504
+ return normalized;
1505
+ }
1506
+ function normalizeRuntimeEntryIcon(value) {
1507
+ if (typeof value === "string") {
1508
+ const src = value.trim();
1509
+ return src ? {
1510
+ kind: "image",
1511
+ src
1512
+ } : null;
1513
+ }
1514
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1515
+ const src = normalizeOptionalString(value.src);
1516
+ if (!src) return null;
1517
+ const alt = normalizeOptionalString(value.alt);
1518
+ return {
1519
+ kind: "image",
1520
+ src,
1521
+ ...alt ? { alt } : {}
1522
+ };
1523
+ }
1524
+ function clearSecretRef(refs, path) {
1525
+ const { [path]: _removed, ...nextRefs } = refs;
1767
1526
  return nextRefs;
1768
1527
  }
1769
- function isSearchProviderName(value) {
1770
- return typeof value === "string" && SEARCH_PROVIDER_NAMES.some((providerName) => providerName === value);
1528
+ function buildConfigMeta(config, options) {
1529
+ return {
1530
+ search: SEARCH_PROVIDER_META,
1531
+ channels: buildProjectedChannelMeta(config, options)
1532
+ };
1771
1533
  }
1772
- const SEARCH_PROVIDER_META = [
1773
- {
1774
- name: "bocha",
1775
- displayName: "Bocha Search",
1776
- description: "China-friendly web search with AI-ready summaries.",
1777
- docsUrl: BOCHA_OPEN_URL,
1778
- isDefault: true,
1779
- supportsSummary: true
1780
- },
1781
- {
1782
- name: "tavily",
1783
- displayName: "Tavily Search",
1784
- description: "Research-focused web search with optional synthesized answers.",
1785
- docsUrl: TAVILY_DOCS_URL,
1786
- supportsSummary: true
1787
- },
1788
- {
1789
- name: "brave",
1790
- displayName: "Brave Search",
1791
- description: "Brave web search API kept as an optional provider.",
1792
- supportsSummary: false
1534
+ function buildProviderTemplatesView() {
1535
+ return { providerTemplates: BUILTIN_PROVIDERS.map((spec) => {
1536
+ return {
1537
+ id: spec.name,
1538
+ providerType: spec.name,
1539
+ displayName: spec.displayName ?? spec.name,
1540
+ apiProtocol: spec.apiProtocol,
1541
+ modelPrefix: spec.modelPrefix,
1542
+ keywords: spec.keywords,
1543
+ envKey: spec.envKey,
1544
+ isGateway: spec.isGateway,
1545
+ isLocal: spec.isLocal,
1546
+ apiKeyRequired: !spec.anonymousApiKey,
1547
+ defaultApiBase: spec.defaultApiBase,
1548
+ logo: spec.logo,
1549
+ apiBaseHelp: spec.apiBaseHelp,
1550
+ auth: spec.auth ? {
1551
+ kind: spec.auth.kind,
1552
+ displayName: spec.auth.displayName,
1553
+ note: spec.auth.note,
1554
+ methods: spec.auth.methods?.map((method) => ({
1555
+ id: method.id,
1556
+ label: method.label,
1557
+ hint: method.hint
1558
+ })),
1559
+ defaultMethodId: spec.auth.defaultMethodId,
1560
+ supportsCliImport: Boolean(spec.auth.cliCredential)
1561
+ } : void 0,
1562
+ defaultModels: normalizeModelList(spec.defaultModels ?? []),
1563
+ supportsModelDiscovery: Boolean(spec.modelDiscovery),
1564
+ modelConfig: normalizeProviderModelConfig(spec.modelConfig ?? {}),
1565
+ supportsWireApi: spec.supportsWireApi,
1566
+ wireApiOptions: spec.wireApiOptions,
1567
+ defaultWireApi: spec.defaultWireApi
1568
+ };
1569
+ }).sort((left, right) => {
1570
+ const leftRank = PREFERRED_PROVIDER_ORDER_INDEX.get(left.id);
1571
+ const rightRank = PREFERRED_PROVIDER_ORDER_INDEX.get(right.id);
1572
+ if (leftRank !== void 0 && rightRank !== void 0) return leftRank - rightRank;
1573
+ if (leftRank !== void 0) return -1;
1574
+ if (rightRank !== void 0) return 1;
1575
+ return left.id.localeCompare(right.id);
1576
+ }) };
1577
+ }
1578
+ function buildProvidersView(config) {
1579
+ const uiHints = buildUiHints(config);
1580
+ const providers = {};
1581
+ for (const [providerId, provider] of Object.entries(config.providers)) {
1582
+ const providerConfig = provider;
1583
+ providers[providerId] = toProviderView(config, providerConfig, providerId, uiHints, findServerBuiltinProviderByName(resolveProviderType(providerId, providerConfig) ?? ""));
1793
1584
  }
1794
- ];
1795
- function toSearchProviderView(config, providerName, provider) {
1796
- const apiKeyRefSet = hasSecretRef(config, `search.providers.${providerName}.apiKey`);
1797
- const masked = maskApiKey$1(provider.apiKey);
1798
- const view = {
1799
- enabled: config.search.enabledProviders.includes(providerName),
1800
- apiKeySet: masked.apiKeySet || apiKeyRefSet,
1801
- apiKeyMasked: masked.apiKeyMasked ?? (apiKeyRefSet ? "****" : void 0),
1802
- baseUrl: provider.baseUrl
1803
- };
1804
- if ("docsUrl" in provider) view.docsUrl = provider.docsUrl;
1805
- if ("summary" in provider) view.summary = provider.summary;
1806
- if ("freshness" in provider) view.freshness = provider.freshness;
1807
- if ("searchDepth" in provider) view.searchDepth = provider.searchDepth;
1808
- if ("includeAnswer" in provider) view.includeAnswer = Boolean(provider.includeAnswer);
1809
- return view;
1585
+ return { providers };
1810
1586
  }
1811
- function buildSearchView(config) {
1587
+ function buildConfigSchemaView(_config, options) {
1588
+ const base = buildConfigSchema({ version: getPackageVersion() });
1589
+ const extensionUiHints = buildExtensionChannelUiHints(options);
1590
+ if (Object.keys(extensionUiHints).length === 0) return base;
1812
1591
  return {
1813
- provider: config.search.provider,
1814
- enabledProviders: [...config.search.enabledProviders],
1815
- defaults: { maxResults: config.search.defaults.maxResults },
1816
- providers: {
1817
- bocha: toSearchProviderView(config, "bocha", config.search.providers.bocha),
1818
- tavily: toSearchProviderView(config, "tavily", config.search.providers.tavily),
1819
- brave: toSearchProviderView(config, "brave", config.search.providers.brave)
1592
+ ...base,
1593
+ uiHints: {
1594
+ ...base.uiHints,
1595
+ ...extensionUiHints
1820
1596
  }
1821
1597
  };
1822
1598
  }
1823
- function replaceSearchConfig(config, search, refs = config.secrets.refs) {
1824
- return {
1825
- ...config,
1826
- search,
1827
- secrets: refs === config.secrets.refs ? config.secrets : {
1828
- ...config.secrets,
1829
- refs
1599
+ async function executeConfigAction(configPath, actionId, request) {
1600
+ const baseConfig = loadConfigOrDefault(configPath);
1601
+ const action = getActionById(baseConfig, actionId);
1602
+ if (!action) return {
1603
+ ok: false,
1604
+ code: "ACTION_NOT_FOUND",
1605
+ message: `unknown action: ${actionId}`
1606
+ };
1607
+ if (request.scope && request.scope !== action.scope) return {
1608
+ ok: false,
1609
+ code: "ACTION_SCOPE_MISMATCH",
1610
+ message: `scope mismatch: expected ${action.scope}, got ${request.scope}`,
1611
+ details: {
1612
+ expectedScope: action.scope,
1613
+ requestScope: request.scope
1830
1614
  }
1831
1615
  };
1616
+ const runtimeConfig = resolveRuntimeConfig(baseConfig, request.draftConfig);
1617
+ for (const requiredPath of action.requires ?? []) if (isMissingRequiredValue(getPathValue(runtimeConfig, requiredPath))) return {
1618
+ ok: false,
1619
+ code: "ACTION_PRECONDITION_FAILED",
1620
+ message: `required field missing: ${requiredPath}`,
1621
+ details: { path: requiredPath }
1622
+ };
1623
+ const handler = ACTION_HANDLERS[action.id];
1624
+ if (!handler) return {
1625
+ ok: false,
1626
+ code: "ACTION_EXECUTION_FAILED",
1627
+ message: `action handler not found for type ${action.type}`
1628
+ };
1629
+ return {
1630
+ ok: true,
1631
+ data: await handler({
1632
+ config: runtimeConfig,
1633
+ action
1634
+ })
1635
+ };
1832
1636
  }
1833
- function applyActiveSearchProviderPatch(config, provider) {
1834
- if (!isSearchProviderName(provider)) return config;
1835
- return replaceSearchConfig(config, {
1836
- ...config.search,
1837
- provider
1838
- });
1637
+ function loadConfigOrDefault(configPath) {
1638
+ return loadConfig(configPath);
1839
1639
  }
1840
- function applyEnabledSearchProvidersPatch(config, enabledProviders) {
1841
- if (!Array.isArray(enabledProviders)) return config;
1842
- const nextEnabledProviders = Array.from(new Set(enabledProviders.filter((value) => isSearchProviderName(value))));
1843
- return replaceSearchConfig(config, {
1844
- ...config.search,
1845
- enabledProviders: nextEnabledProviders
1846
- });
1640
+ function createProviderForUpdate(providerId) {
1641
+ const spec = findServerBuiltinProviderByName(providerId);
1642
+ if (!spec) return null;
1643
+ return createDefaultProviderConfigFromSpec(spec);
1847
1644
  }
1848
- function applySearchDefaultsPatch(config, defaults) {
1849
- if (!defaults || !Object.prototype.hasOwnProperty.call(defaults, "maxResults")) return config;
1850
- const nextMaxResults = defaults.maxResults;
1851
- if (typeof nextMaxResults === "number" && Number.isFinite(nextMaxResults)) return replaceSearchConfig(config, {
1852
- ...config.search,
1853
- defaults: {
1854
- ...config.search.defaults,
1855
- maxResults: Math.max(1, Math.min(50, Math.trunc(nextMaxResults)))
1856
- }
1857
- });
1858
- return config;
1645
+ function updateModel(configPath, patch) {
1646
+ const config = loadConfigOrDefault(configPath);
1647
+ if (typeof patch.model === "string") config.agents.defaults.model = patch.model;
1648
+ if (typeof patch.workspace === "string") config.agents.defaults.workspace = normalizeOptionalString(patch.workspace) ?? DEFAULT_WORKSPACE_PATH;
1649
+ const next = ConfigSchema.parse(config);
1650
+ saveConfig(next, configPath);
1651
+ return buildConfigView(next);
1859
1652
  }
1860
- function applyBochaSearchPatch(config, patch) {
1861
- if (!patch) return config;
1862
- let nextRefs = config.secrets.refs;
1863
- let nextProvider = config.search.providers.bocha;
1653
+ function updateProvider(configPath, providerId, patch) {
1654
+ const config = loadConfigOrDefault(configPath);
1655
+ const providers = config.providers;
1656
+ const provider = providers[providerId] ?? createProviderForUpdate(providerId);
1657
+ if (!provider) return null;
1658
+ providers[providerId] = provider;
1659
+ const spec = findServerBuiltinProviderByName((Object.prototype.hasOwnProperty.call(patch, "providerType") ? normalizeProviderId(patch.providerType) : resolveProviderType(providerId, provider)) ?? "");
1660
+ if (Object.prototype.hasOwnProperty.call(patch, "providerType")) provider.providerType = spec?.name ?? null;
1661
+ if (Object.prototype.hasOwnProperty.call(patch, "displayName")) provider.displayName = normalizeOptionalDisplayName(patch.displayName) ?? "";
1662
+ if (Object.prototype.hasOwnProperty.call(patch, "enabled")) provider.enabled = patch.enabled !== false;
1864
1663
  if (Object.prototype.hasOwnProperty.call(patch, "apiKey")) {
1865
- nextProvider = {
1866
- ...nextProvider,
1867
- apiKey: patch.apiKey ?? ""
1868
- };
1869
- nextRefs = clearSecretRef$1(nextRefs, "search.providers.bocha.apiKey");
1664
+ provider.apiKey = patch.apiKey ?? "";
1665
+ config.secrets.refs = clearSecretRef(config.secrets.refs, `providers.${providerId}.apiKey`);
1870
1666
  }
1871
- if (Object.prototype.hasOwnProperty.call(patch, "baseUrl")) nextProvider = {
1872
- ...nextProvider,
1873
- baseUrl: normalizeOptionalString$1(patch.baseUrl) ?? "https://api.bocha.cn/v1/web-search"
1667
+ if (Object.prototype.hasOwnProperty.call(patch, "apiBase")) provider.apiBase = patch.apiBase ?? null;
1668
+ if (Object.prototype.hasOwnProperty.call(patch, "extraHeaders")) provider.extraHeaders = patch.extraHeaders ?? null;
1669
+ if (Object.prototype.hasOwnProperty.call(patch, "wireApi") && (spec?.supportsWireApi || !spec)) provider.wireApi = patch.wireApi ?? spec?.defaultWireApi ?? "auto";
1670
+ if (Object.prototype.hasOwnProperty.call(patch, "models")) provider.models = normalizeModelList(patch.models ?? []);
1671
+ if (Object.prototype.hasOwnProperty.call(patch, "modelConfig")) provider.modelConfig = normalizeProviderModelConfig(patch.modelConfig ?? {});
1672
+ const next = ConfigSchema.parse(config);
1673
+ saveConfig(next, configPath);
1674
+ const uiHints = buildUiHints(next);
1675
+ const updated = next.providers[providerId];
1676
+ return toProviderView(next, updated, providerId, uiHints, spec ?? void 0);
1677
+ }
1678
+ function createProvider(configPath, patch = {}) {
1679
+ const config = loadConfigOrDefault(configPath);
1680
+ const providers = config.providers;
1681
+ const requestedProviderType = normalizeProviderId(patch.providerType);
1682
+ const spec = requestedProviderType ? findServerBuiltinProviderByName(requestedProviderType) : void 0;
1683
+ const fallbackProviderId = spec ? spec.name : findNextCustomProviderName(config);
1684
+ const requestedProviderId = normalizeProviderId(patch.providerId);
1685
+ if (requestedProviderId && providers[requestedProviderId]) return null;
1686
+ const providerId = requestedProviderId ? requestedProviderId : findNextProviderId(config, fallbackProviderId);
1687
+ const generatedDisplayName = spec ? `${spec.displayName}${resolveProviderDisplayNameSuffix(providerId, spec.name)}` : resolveCustomProviderFallbackDisplayName(providerId);
1688
+ const defaultModels = spec ? buildProviderScopedModels(providerId, spec.defaultModels ?? []) : [];
1689
+ providers[providerId] = {
1690
+ enabled: patch.enabled !== false,
1691
+ providerType: spec?.name ?? null,
1692
+ displayName: normalizeOptionalDisplayName(patch.displayName) ?? generatedDisplayName,
1693
+ apiKey: normalizeOptionalString(patch.apiKey) ?? "",
1694
+ apiBase: normalizeOptionalString(patch.apiBase) ?? spec?.defaultApiBase ?? null,
1695
+ extraHeaders: normalizeHeaders(patch.extraHeaders ?? null),
1696
+ wireApi: patch.wireApi ?? spec?.defaultWireApi ?? "auto",
1697
+ models: Object.prototype.hasOwnProperty.call(patch, "models") ? normalizeModelList(patch.models ?? []) : defaultModels,
1698
+ modelConfig: normalizeProviderModelConfig(patch.modelConfig ?? spec?.modelConfig ?? {})
1874
1699
  };
1875
- if (Object.prototype.hasOwnProperty.call(patch, "docsUrl")) nextProvider = {
1876
- ...nextProvider,
1877
- docsUrl: normalizeOptionalString$1(patch.docsUrl) ?? BOCHA_OPEN_URL
1700
+ const next = ConfigSchema.parse(config);
1701
+ saveConfig(next, configPath);
1702
+ const uiHints = buildUiHints(next);
1703
+ const created = next.providers[providerId];
1704
+ return {
1705
+ providerId,
1706
+ provider: toProviderView(next, created, providerId, uiHints, spec)
1878
1707
  };
1879
- if (Object.prototype.hasOwnProperty.call(patch, "summary")) nextProvider = {
1880
- ...nextProvider,
1881
- summary: Boolean(patch.summary)
1708
+ }
1709
+ function deleteProvider(configPath, providerId) {
1710
+ const config = loadConfigOrDefault(configPath);
1711
+ const providers = config.providers;
1712
+ if (!providers[providerId]) return null;
1713
+ delete providers[providerId];
1714
+ config.secrets.refs = clearSecretRefsByPrefix(config.secrets.refs, `providers.${providerId}`);
1715
+ saveConfig(ConfigSchema.parse(config), configPath);
1716
+ return true;
1717
+ }
1718
+ function normalizeOptionalString(value) {
1719
+ if (typeof value !== "string") return null;
1720
+ const trimmed = value.trim();
1721
+ return trimmed.length > 0 ? trimmed : null;
1722
+ }
1723
+ function normalizeHeaders(input) {
1724
+ if (!input) return null;
1725
+ const entries = Object.entries(input).map(([key, value]) => [key.trim(), String(value ?? "").trim()]).filter(([key, value]) => key.length > 0 && value.length > 0);
1726
+ if (entries.length === 0) return null;
1727
+ return Object.fromEntries(entries);
1728
+ }
1729
+ function updateChannel(configPath, channelName, patch, options) {
1730
+ const config = loadConfigOrDefault(configPath);
1731
+ const normalizedOptions = normalizeExtensionProjectionOptions(options);
1732
+ const channel = getProjectedChannelConfig(config, channelName, normalizedOptions);
1733
+ if (!channel) return null;
1734
+ for (const key of Object.keys(patch)) {
1735
+ const path = `channels.${channelName}.${key}`;
1736
+ if (isSensitivePath(path)) config.secrets.refs = clearSecretRef(config.secrets.refs, path);
1737
+ }
1738
+ const mergedChannel = {
1739
+ ...channel,
1740
+ ...patch
1882
1741
  };
1883
- if (Object.prototype.hasOwnProperty.call(patch, "freshness")) {
1884
- const freshness = normalizeOptionalString$1(patch.freshness);
1885
- nextProvider = {
1886
- ...nextProvider,
1887
- freshness: freshness === "noLimit" || freshness === "oneDay" || freshness === "oneWeek" || freshness === "oneMonth" || freshness === "oneYear" ? freshness : "noLimit"
1888
- };
1742
+ const mergedExtensionConfig = mergeProjectedExtensionChannelConfig(config, channelName, mergedChannel, normalizedOptions);
1743
+ if (mergedExtensionConfig) {
1744
+ const next = ConfigSchema.parse(mergedExtensionConfig);
1745
+ saveConfig(next, configPath);
1746
+ return sanitizePublicConfigValue(getProjectedChannelConfig(next, channelName, normalizedOptions) ?? {}, `channels.${channelName}`, buildUiHints(next, normalizedOptions));
1889
1747
  }
1890
- return replaceSearchConfig(config, {
1891
- ...config.search,
1892
- providers: {
1893
- ...config.search.providers,
1894
- bocha: nextProvider
1895
- }
1896
- }, nextRefs);
1748
+ config.channels[channelName] = mergedChannel;
1749
+ const next = ConfigSchema.parse(config);
1750
+ saveConfig(next, configPath);
1751
+ return sanitizePublicConfigValue(getProjectedChannelConfig(next, channelName, normalizedOptions) ?? {}, `channels.${channelName}`, buildUiHints(next, normalizedOptions));
1897
1752
  }
1898
- function applyTavilySearchPatch(config, patch) {
1899
- if (!patch) return config;
1900
- let nextRefs = config.secrets.refs;
1901
- let nextProvider = config.search.providers.tavily;
1902
- if (Object.prototype.hasOwnProperty.call(patch, "apiKey")) {
1903
- nextProvider = {
1904
- ...nextProvider,
1905
- apiKey: patch.apiKey ?? ""
1753
+ function applyRuntimeAgentDefaultsPatch(defaults, defaultsPatch) {
1754
+ if (!defaultsPatch) return defaults;
1755
+ let next = defaults;
1756
+ if (Object.prototype.hasOwnProperty.call(defaultsPatch, "contextTokens")) {
1757
+ const nextContextTokens = defaultsPatch.contextTokens;
1758
+ if (typeof nextContextTokens === "number" && Number.isFinite(nextContextTokens)) next = {
1759
+ ...next,
1760
+ contextTokens: Math.trunc(nextContextTokens)
1906
1761
  };
1907
- nextRefs = clearSecretRef$1(nextRefs, "search.providers.tavily.apiKey");
1908
1762
  }
1909
- if (Object.prototype.hasOwnProperty.call(patch, "baseUrl")) nextProvider = {
1910
- ...nextProvider,
1911
- baseUrl: normalizeOptionalString$1(patch.baseUrl) ?? "https://api.tavily.com/search"
1763
+ if (Object.prototype.hasOwnProperty.call(defaultsPatch, "engine")) next = {
1764
+ ...next,
1765
+ engine: normalizeOptionalString(defaultsPatch.engine) ?? "native"
1912
1766
  };
1913
- if (Object.prototype.hasOwnProperty.call(patch, "searchDepth")) {
1914
- const searchDepth = normalizeOptionalString$1(patch.searchDepth);
1915
- nextProvider = {
1916
- ...nextProvider,
1917
- searchDepth: searchDepth === "advanced" ? "advanced" : "basic"
1767
+ if (Object.prototype.hasOwnProperty.call(defaultsPatch, "engineConfig")) {
1768
+ const nextEngineConfig = defaultsPatch.engineConfig;
1769
+ if (nextEngineConfig && typeof nextEngineConfig === "object" && !Array.isArray(nextEngineConfig)) next = {
1770
+ ...next,
1771
+ engineConfig: { ...nextEngineConfig }
1918
1772
  };
1919
1773
  }
1920
- if (Object.prototype.hasOwnProperty.call(patch, "includeAnswer")) nextProvider = {
1921
- ...nextProvider,
1922
- includeAnswer: Boolean(patch.includeAnswer)
1923
- };
1924
- return replaceSearchConfig(config, {
1925
- ...config.search,
1926
- providers: {
1927
- ...config.search.providers,
1928
- tavily: nextProvider
1929
- }
1930
- }, nextRefs);
1774
+ return next;
1931
1775
  }
1932
- function applyBraveSearchPatch(config, patch) {
1933
- if (!patch) return config;
1934
- let nextRefs = config.secrets.refs;
1935
- let nextProvider = config.search.providers.brave;
1936
- if (Object.prototype.hasOwnProperty.call(patch, "apiKey")) {
1937
- nextProvider = {
1938
- ...nextProvider,
1939
- apiKey: patch.apiKey ?? ""
1776
+ function updateRuntime(configPath, patch) {
1777
+ const config = loadConfigOrDefault(configPath);
1778
+ if (patch.companion && Object.prototype.hasOwnProperty.call(patch.companion, "enabled")) config.companion.enabled = Boolean(patch.companion.enabled);
1779
+ config.agents.defaults = applyRuntimeAgentDefaultsPatch(config.agents.defaults, patch.agents?.defaults);
1780
+ if (patch.agents && Object.prototype.hasOwnProperty.call(patch.agents, "list")) config.agents.list = (patch.agents.list ?? []).map((entry) => {
1781
+ const normalizedEngine = normalizeOptionalString(entry.engine);
1782
+ const hasEngineConfig = entry.engineConfig && typeof entry.engineConfig === "object" && !Array.isArray(entry.engineConfig);
1783
+ return {
1784
+ ...entry,
1785
+ default: Boolean(entry.default),
1786
+ ...normalizedEngine ? { engine: normalizedEngine } : {},
1787
+ ...hasEngineConfig ? { engineConfig: { ...entry.engineConfig } } : {}
1940
1788
  };
1941
- nextRefs = clearSecretRef$1(nextRefs, "search.providers.brave.apiKey");
1942
- }
1943
- if (Object.prototype.hasOwnProperty.call(patch, "baseUrl")) nextProvider = {
1944
- ...nextProvider,
1945
- baseUrl: normalizeOptionalString$1(patch.baseUrl) ?? "https://api.search.brave.com/res/v1/web/search"
1789
+ });
1790
+ if (patch.agents?.runtimes && Object.prototype.hasOwnProperty.call(patch.agents.runtimes, "entries")) config.agents.runtimes.entries = normalizeRuntimeEntries(patch.agents.runtimes.entries);
1791
+ if (Object.prototype.hasOwnProperty.call(patch, "bindings")) config.bindings = patch.bindings ?? [];
1792
+ if (patch.session) config.session = {
1793
+ ...config.session,
1794
+ ...patch.session
1795
+ };
1796
+ const next = ConfigSchema.parse(config);
1797
+ saveConfig(next, configPath);
1798
+ const view = buildConfigView(next);
1799
+ return {
1800
+ companion: view.companion,
1801
+ agents: view.agents,
1802
+ bindings: view.bindings ?? [],
1803
+ session: view.session ?? {}
1946
1804
  };
1947
- return replaceSearchConfig(config, {
1948
- ...config.search,
1949
- providers: {
1950
- ...config.search.providers,
1951
- brave: nextProvider
1952
- }
1953
- }, nextRefs);
1954
1805
  }
1955
- function updateSearch(configPath, patch) {
1956
- const nextConfig = applyBraveSearchPatch(applyTavilySearchPatch(applyBochaSearchPatch(applySearchDefaultsPatch(applyEnabledSearchProvidersPatch(applyActiveSearchProviderPatch(loadConfig(configPath), patch.provider), patch.enabledProviders), patch.defaults), patch.providers?.bocha), patch.providers?.tavily), patch.providers?.brave);
1957
- const next = ConfigSchema.parse(nextConfig);
1806
+ function updateSecrets(configPath, patch) {
1807
+ const config = loadConfigOrDefault(configPath);
1808
+ if (Object.prototype.hasOwnProperty.call(patch, "enabled")) config.secrets.enabled = Boolean(patch.enabled);
1809
+ if (patch.defaults) {
1810
+ const nextDefaults = { ...config.secrets.defaults };
1811
+ for (const source of [
1812
+ "env",
1813
+ "file",
1814
+ "exec"
1815
+ ]) {
1816
+ if (!Object.prototype.hasOwnProperty.call(patch.defaults, source)) continue;
1817
+ const value = patch.defaults[source];
1818
+ if (typeof value === "string" && value.trim()) nextDefaults[source] = value.trim();
1819
+ else delete nextDefaults[source];
1820
+ }
1821
+ config.secrets.defaults = nextDefaults;
1822
+ }
1823
+ if (Object.prototype.hasOwnProperty.call(patch, "providers")) config.secrets.providers = patch.providers ?? {};
1824
+ if (Object.prototype.hasOwnProperty.call(patch, "refs")) config.secrets.refs = patch.refs ?? {};
1825
+ const next = ConfigSchema.parse(config);
1958
1826
  saveConfig(next, configPath);
1959
- return buildSearchView(next);
1827
+ return {
1828
+ enabled: next.secrets.enabled,
1829
+ defaults: { ...next.secrets.defaults },
1830
+ providers: { ...next.secrets.providers },
1831
+ refs: { ...next.secrets.refs }
1832
+ };
1960
1833
  }
1961
1834
  //#endregion
1962
- //#region src/features/config/stores/server-config.store.ts
1963
- const MASK_MIN_LENGTH = 8;
1964
- const EXTRA_SENSITIVE_PATH_PATTERNS = [
1965
- /authorization/i,
1966
- /cookie/i,
1967
- /session/i,
1968
- /bearer/i
1969
- ];
1970
- const PREFERRED_PROVIDER_ORDER_INDEX = new Map([
1971
- "nextclaw",
1972
- "openai",
1973
- "anthropic",
1974
- "gemini",
1975
- "openrouter",
1976
- "dashscope-coding-plan",
1977
- "dashscope",
1978
- "deepseek",
1979
- "minimax",
1980
- "moonshot",
1981
- "kimi-coding",
1982
- "zhipu"
1983
- ].map((name, index) => [name, index]));
1984
- const BUILTIN_PROVIDERS = listServerBuiltinProviders();
1985
- const BUILTIN_PROVIDER_NAMES = new Set(BUILTIN_PROVIDERS.map((spec) => spec.name));
1986
- const CUSTOM_PROVIDER_PREFIX = "custom-";
1835
+ //#region src/features/config/services/provider-connectivity.service.ts
1987
1836
  const PROVIDER_TEST_MAX_TOKENS = 16;
1988
- function normalizeOptionalDisplayName(value) {
1989
- if (typeof value !== "string") return null;
1990
- const trimmed = value.trim();
1991
- return trimmed.length > 0 ? trimmed : null;
1837
+ var ProviderConnectivityService = class {
1838
+ constructor(configPath, providerManager) {
1839
+ this.configPath = configPath;
1840
+ this.providerManager = providerManager;
1841
+ }
1842
+ normalizeOptionalString = (value) => {
1843
+ if (typeof value !== "string") return null;
1844
+ const trimmed = value.trim();
1845
+ return trimmed.length > 0 ? trimmed : null;
1846
+ };
1847
+ normalizeHeaders = (input) => {
1848
+ if (!input) return null;
1849
+ const entries = Object.entries(input).map(([key, value]) => [key.trim(), String(value ?? "").trim()]).filter(([key, value]) => key.length > 0 && value.length > 0);
1850
+ return entries.length > 0 ? Object.fromEntries(entries) : null;
1851
+ };
1852
+ normalizeModelList = (input) => {
1853
+ const models = /* @__PURE__ */ new Set();
1854
+ for (const value of input ?? []) {
1855
+ const model = typeof value === "string" ? value.trim() : "";
1856
+ if (model) models.add(model);
1857
+ }
1858
+ return [...models];
1859
+ };
1860
+ resolveProviderType = (providerId, provider) => {
1861
+ const configuredType = this.normalizeOptionalString(provider?.providerType);
1862
+ if (configuredType && findServerBuiltinProviderByName(configuredType)) return configuredType;
1863
+ return findServerBuiltinProviderByName(providerId) ? providerId : null;
1864
+ };
1865
+ resolveRuntimeDraft = (providerId, provider, patch) => {
1866
+ const providerType = this.resolveProviderType(providerId, provider);
1867
+ const spec = findServerBuiltinProviderByName(providerType ?? "");
1868
+ return {
1869
+ providerType,
1870
+ spec,
1871
+ apiKey: Object.prototype.hasOwnProperty.call(patch, "apiKey") ? this.normalizeOptionalString(patch.apiKey) ?? this.normalizeOptionalString(spec?.anonymousApiKey) : this.normalizeOptionalString(provider.apiKey) ?? this.normalizeOptionalString(spec?.anonymousApiKey),
1872
+ apiBase: Object.prototype.hasOwnProperty.call(patch, "apiBase") ? this.normalizeOptionalString(patch.apiBase) ?? spec?.defaultApiBase ?? null : this.normalizeOptionalString(provider.apiBase) ?? spec?.defaultApiBase ?? null,
1873
+ extraHeaders: Object.prototype.hasOwnProperty.call(patch, "extraHeaders") ? this.normalizeHeaders(patch.extraHeaders ?? null) : this.normalizeHeaders(provider.extraHeaders ?? null),
1874
+ wireApi: spec?.supportsWireApi || !spec ? patch.wireApi ?? provider.wireApi ?? spec?.defaultWireApi ?? "auto" : null
1875
+ };
1876
+ };
1877
+ buildScopedModel = (providerName, model, spec) => {
1878
+ const trimmed = model.trim();
1879
+ if (!trimmed || trimmed.includes("/") || !findServerBuiltinProviderByName(providerName)) return trimmed;
1880
+ const prefix = (spec?.modelPrefix ?? providerName).trim();
1881
+ return prefix ? `${prefix}/${trimmed}` : trimmed;
1882
+ };
1883
+ rewriteProviderRoutePrefix = (providerId, targetPrefix, model) => {
1884
+ const prefix = `${providerId}/`;
1885
+ if (!model.startsWith(prefix)) return model;
1886
+ const stripped = model.slice(prefix.length).trim();
1887
+ return stripped ? targetPrefix ? `${targetPrefix}/${stripped}` : stripped : model;
1888
+ };
1889
+ resolveTestModel = (config, providerId, requestedModel, provider, spec) => {
1890
+ if (requestedModel) return this.rewriteProviderRoutePrefix(providerId, spec?.name ?? null, requestedModel);
1891
+ const providerModels = this.normalizeModelList(provider.models).map((modelId) => {
1892
+ const providerModel = this.rewriteProviderRoutePrefix(providerId, null, modelId);
1893
+ return spec ? this.buildScopedModel(spec.name, providerModel, spec) : providerModel;
1894
+ }).filter(Boolean);
1895
+ if (providerModels.length > 0) return providerModels[0] ?? null;
1896
+ const defaultModel = this.normalizeOptionalString(config.agents.defaults.model);
1897
+ if (defaultModel) {
1898
+ const routedProvider = getProviderName(config, defaultModel);
1899
+ if (!routedProvider || routedProvider === providerId) return this.rewriteProviderRoutePrefix(providerId, spec?.name ?? null, defaultModel);
1900
+ }
1901
+ return spec ? this.normalizeModelList(spec.defaultModels)[0] ?? defaultModel : null;
1902
+ };
1903
+ readProvider = (providerId) => {
1904
+ const config = loadConfigOrDefault(this.configPath);
1905
+ const provider = config.providers[providerId];
1906
+ return provider ? {
1907
+ config,
1908
+ provider
1909
+ } : null;
1910
+ };
1911
+ testConnection = async (providerId, patch) => {
1912
+ const resolved = this.readProvider(providerId);
1913
+ if (!resolved) return null;
1914
+ const { config, provider } = resolved;
1915
+ const { providerType, spec, apiKey, apiBase, extraHeaders, wireApi } = this.resolveRuntimeDraft(providerId, provider, patch);
1916
+ if (!apiKey && !spec?.isLocal) return {
1917
+ success: false,
1918
+ provider: providerId,
1919
+ latencyMs: 0,
1920
+ message: "API key is required before testing the connection."
1921
+ };
1922
+ const model = this.resolveTestModel(config, providerId, this.normalizeOptionalString(patch.model), provider, spec);
1923
+ if (!model) return {
1924
+ success: false,
1925
+ provider: providerId,
1926
+ latencyMs: 0,
1927
+ message: "No test model found. Configure provider models or set a default model for this provider, then try again."
1928
+ };
1929
+ const startedAtMs = Date.now();
1930
+ if (!this.providerManager) return {
1931
+ success: false,
1932
+ provider: providerId,
1933
+ model,
1934
+ latencyMs: Date.now() - startedAtMs,
1935
+ message: "Provider manager is unavailable."
1936
+ };
1937
+ try {
1938
+ await this.providerManager.testConnection({
1939
+ providerName: providerType,
1940
+ apiKey,
1941
+ apiBase,
1942
+ defaultModel: model,
1943
+ extraHeaders,
1944
+ wireApi,
1945
+ messages: [{
1946
+ role: "user",
1947
+ content: "ping"
1948
+ }],
1949
+ maxTokens: PROVIDER_TEST_MAX_TOKENS
1950
+ });
1951
+ return {
1952
+ success: true,
1953
+ provider: providerId,
1954
+ model,
1955
+ latencyMs: Date.now() - startedAtMs,
1956
+ message: "Connection test passed."
1957
+ };
1958
+ } catch (error) {
1959
+ const message = error instanceof Error ? error.message : String(error);
1960
+ return {
1961
+ success: false,
1962
+ provider: providerId,
1963
+ model,
1964
+ latencyMs: Date.now() - startedAtMs,
1965
+ message: message.replace(/\s+/g, " ").trim() || "Connection test failed."
1966
+ };
1967
+ }
1968
+ };
1969
+ discoverModels = async (providerId, patch) => {
1970
+ const resolved = this.readProvider(providerId);
1971
+ if (!resolved) return null;
1972
+ if (!this.providerManager) throw new Error("Provider manager is unavailable.");
1973
+ const { providerType, apiKey, apiBase, extraHeaders } = this.resolveRuntimeDraft(providerId, resolved.provider, patch);
1974
+ const result = await this.providerManager.discoverModels({
1975
+ providerName: providerType,
1976
+ apiKey,
1977
+ apiBase,
1978
+ extraHeaders
1979
+ });
1980
+ return {
1981
+ provider: providerId,
1982
+ models: result.models,
1983
+ source: result.source,
1984
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
1985
+ };
1986
+ };
1987
+ };
1988
+ //#endregion
1989
+ //#region src/features/config/utils/channel-auth.utils.ts
1990
+ function cloneChannelConfig(value) {
1991
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
1992
+ return JSON.parse(JSON.stringify(value));
1992
1993
  }
1993
- function isCustomProviderName(name) {
1994
- return name.trim().length > 0 && !BUILTIN_PROVIDER_NAMES.has(name);
1994
+ function findExtensionChannelBinding(bindings, channelId) {
1995
+ const normalizedChannelId = channelId.trim().toLowerCase();
1996
+ return bindings.find((binding) => binding.channelId.trim().toLowerCase() === normalizedChannelId) ?? null;
1995
1997
  }
1996
- function resolveCustomProviderFallbackDisplayName(name) {
1997
- if (name.startsWith(CUSTOM_PROVIDER_PREFIX)) {
1998
- const suffix = name.slice(7);
1999
- if (/^\d+$/.test(suffix)) return `Custom ${suffix}`;
2000
- }
2001
- return name;
1998
+ function toPublicChannelAuthPollResult(result) {
1999
+ return {
2000
+ channel: result.channel,
2001
+ status: result.status,
2002
+ message: result.message,
2003
+ nextPollMs: result.nextPollMs,
2004
+ accountId: result.accountId,
2005
+ notes: result.notes
2006
+ };
2002
2007
  }
2003
- function resolveProviderInstanceDisplayName(providerId, provider, spec) {
2004
- return normalizeOptionalDisplayName(provider?.displayName) ?? spec?.displayName ?? (providerId.startsWith(CUSTOM_PROVIDER_PREFIX) ? resolveCustomProviderFallbackDisplayName(providerId) : providerId);
2008
+ function applyAuthorizedChannelAuthResult(params) {
2009
+ const { configPath, binding, result } = params;
2010
+ if (result.status !== "authorized" || !result.channelConfig) return;
2011
+ const currentConfig = loadConfigOrDefault(configPath);
2012
+ saveConfig({
2013
+ ...currentConfig,
2014
+ channels: {
2015
+ ...currentConfig.channels,
2016
+ [binding.channelId]: result.channelConfig
2017
+ }
2018
+ }, configPath);
2005
2019
  }
2006
- function findNextCustomProviderName(config) {
2007
- const providers = config.providers;
2008
- let index = 1;
2009
- while (providers[`${CUSTOM_PROVIDER_PREFIX}${index}`]) index += 1;
2010
- return `${CUSTOM_PROVIDER_PREFIX}${index}`;
2020
+ async function startChannelAuth(params) {
2021
+ const { configPath, channelId, request, bindings } = params;
2022
+ const binding = findExtensionChannelBinding(bindings, channelId);
2023
+ const start = binding?.channel.auth?.start;
2024
+ if (!binding || !start) return null;
2025
+ const configView = getProjectedConfigView(loadConfigOrDefault(configPath), { extensionChannelBindings: bindings });
2026
+ return await start({
2027
+ cfg: configView,
2028
+ extensionId: binding.extensionId,
2029
+ channelId: binding.channelId,
2030
+ channelConfig: cloneChannelConfig(configView.channels?.[binding.channelId]),
2031
+ accountId: request.accountId?.trim() || null,
2032
+ baseUrl: request.baseUrl?.trim() || null,
2033
+ domain: request.domain?.trim() || null
2034
+ });
2011
2035
  }
2012
- function normalizeProviderId(value) {
2013
- if (typeof value !== "string") return null;
2014
- const trimmed = value.trim();
2015
- if (!trimmed || trimmed.includes("/")) return null;
2016
- return trimmed;
2036
+ async function pollChannelAuth(params) {
2037
+ const { configPath, channelId, sessionId, bindings } = params;
2038
+ const binding = findExtensionChannelBinding(bindings, channelId);
2039
+ const poll = binding?.channel.auth?.poll;
2040
+ if (!binding || !poll) return null;
2041
+ const configView = getProjectedConfigView(loadConfigOrDefault(configPath), { extensionChannelBindings: bindings });
2042
+ const result = await poll({
2043
+ cfg: configView,
2044
+ extensionId: binding.extensionId,
2045
+ channelId: binding.channelId,
2046
+ channelConfig: cloneChannelConfig(configView.channels?.[binding.channelId]),
2047
+ sessionId
2048
+ });
2049
+ if (!result) return null;
2050
+ applyAuthorizedChannelAuthResult({
2051
+ configPath,
2052
+ binding,
2053
+ result
2054
+ });
2055
+ return toPublicChannelAuthPollResult(result);
2017
2056
  }
2018
- function resolveProviderType(providerId, provider) {
2019
- const configuredType = normalizeProviderId(provider?.providerType);
2020
- if (configuredType && findServerBuiltinProviderByName(configuredType)) return configuredType;
2021
- if (findServerBuiltinProviderByName(providerId)) return providerId;
2022
- return null;
2057
+ async function connectChannelAuth(params) {
2058
+ const { configPath, channelId, request, bindings } = params;
2059
+ const binding = findExtensionChannelBinding(bindings, channelId);
2060
+ const connect = binding?.channel.auth?.connect;
2061
+ if (!binding || !connect) return null;
2062
+ const configView = getProjectedConfigView(loadConfigOrDefault(configPath), { extensionChannelBindings: bindings });
2063
+ const result = await connect({
2064
+ cfg: configView,
2065
+ extensionId: binding.extensionId,
2066
+ channelId: binding.channelId,
2067
+ channelConfig: cloneChannelConfig(configView.channels?.[binding.channelId]),
2068
+ accountId: request.accountId?.trim() || null,
2069
+ domain: request.domain?.trim() || null,
2070
+ fields: request.fields
2071
+ });
2072
+ applyAuthorizedChannelAuthResult({
2073
+ configPath,
2074
+ binding,
2075
+ result
2076
+ });
2077
+ return toPublicChannelAuthPollResult(result);
2023
2078
  }
2024
- function findNextProviderId(config, baseProviderId) {
2025
- const providers = config.providers;
2026
- let providerId = baseProviderId;
2027
- let index = 2;
2028
- while (providers[providerId]) {
2029
- providerId = `${baseProviderId}-${index}`;
2030
- index += 1;
2031
- }
2032
- return providerId;
2079
+ //#endregion
2080
+ //#region src/features/config/utils/provider-auth.utils.ts
2081
+ const authSessions = /* @__PURE__ */ new Map();
2082
+ const DEFAULT_AUTH_INTERVAL_MS = 2e3;
2083
+ const MAX_AUTH_INTERVAL_MS = 1e4;
2084
+ function normalizePositiveInt(value, fallback) {
2085
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return fallback;
2086
+ return Math.floor(value);
2033
2087
  }
2034
- function resolveProviderDisplayNameSuffix(providerId, baseProviderId) {
2035
- if (providerId === baseProviderId) return "";
2036
- const suffix = providerId.slice(baseProviderId.length + 1).trim();
2037
- return suffix ? ` ${suffix}` : "";
2088
+ function normalizePositiveFloat(value) {
2089
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
2090
+ return value;
2038
2091
  }
2039
- function buildProviderScopedModels(providerId, models) {
2040
- return normalizeModelList(models).map((model) => {
2041
- const slashIndex = model.indexOf("/");
2042
- const modelSuffix = slashIndex >= 0 ? model.slice(slashIndex + 1).trim() : model;
2043
- return modelSuffix ? `${providerId}/${modelSuffix}` : "";
2044
- }).filter(Boolean);
2092
+ function toBase64Url(buffer) {
2093
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
2045
2094
  }
2046
- function clearSecretRefsByPrefix(refs, pathPrefix) {
2047
- return Object.fromEntries(Object.entries(refs).filter(([key]) => key !== pathPrefix && !key.startsWith(`${pathPrefix}.`)));
2095
+ function buildPkce() {
2096
+ const verifier = toBase64Url(randomBytes(48));
2097
+ return {
2098
+ verifier,
2099
+ challenge: toBase64Url(createHash("sha256").update(verifier).digest())
2100
+ };
2048
2101
  }
2049
- function matchesExtraSensitivePath(path) {
2050
- return path !== "session" && !path.startsWith("session.") && EXTRA_SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(path));
2102
+ function withTrailingSlash(value) {
2103
+ return value.endsWith("/") ? value : `${value}/`;
2051
2104
  }
2052
- function matchHint(path, hints) {
2053
- const direct = hints[path];
2054
- if (direct) return direct;
2055
- const segments = path.split(".");
2056
- for (const [hintKey, hint] of Object.entries(hints)) {
2057
- if (!hintKey.includes("*")) continue;
2058
- const hintSegments = hintKey.split(".");
2059
- if (hintSegments.length !== segments.length) continue;
2060
- let match = true;
2061
- for (let index = 0; index < segments.length; index += 1) if (hintSegments[index] !== "*" && hintSegments[index] !== segments[index]) {
2062
- match = false;
2063
- break;
2064
- }
2065
- if (match) return hint;
2066
- }
2105
+ function cleanupExpiredAuthSessions(now = Date.now()) {
2106
+ for (const [sessionId, session] of authSessions.entries()) if (session.expiresAtMs <= now) authSessions.delete(sessionId);
2067
2107
  }
2068
- function isSensitivePath(path, hints) {
2069
- if (hints) {
2070
- const hint = matchHint(path, hints);
2071
- if (hint?.sensitive !== void 0) return Boolean(hint.sensitive);
2072
- }
2073
- return isSensitiveConfigPath(path) || matchesExtraSensitivePath(path);
2108
+ function resolveDeviceCodeEndpoints(baseUrl, deviceCodePath, tokenPath) {
2109
+ return {
2110
+ deviceCodeEndpoint: new URL(deviceCodePath, withTrailingSlash(baseUrl)).toString(),
2111
+ tokenEndpoint: new URL(tokenPath, withTrailingSlash(baseUrl)).toString()
2112
+ };
2074
2113
  }
2075
- function sanitizePublicConfigValue(value, prefix, hints) {
2076
- if (Array.isArray(value)) {
2077
- const nextPath = prefix ? `${prefix}[]` : "[]";
2078
- return value.map((entry) => sanitizePublicConfigValue(entry, nextPath, hints));
2079
- }
2080
- if (!value || typeof value !== "object") return value;
2081
- const output = {};
2082
- for (const [key, val] of Object.entries(value)) {
2083
- const nextPath = prefix ? `${prefix}.${key}` : key;
2084
- if (isSensitivePath(nextPath, hints)) continue;
2085
- output[key] = sanitizePublicConfigValue(val, nextPath, hints);
2086
- }
2087
- return output;
2114
+ function resolveAuthNote(params) {
2115
+ return params.zh ?? params.en;
2088
2116
  }
2089
- function isObject(value) {
2090
- return typeof value === "object" && value !== null && !Array.isArray(value);
2117
+ function resolveLocalizedMethodLabel(method, fallbackId) {
2118
+ return method.label?.zh ?? method.label?.en ?? fallbackId;
2091
2119
  }
2092
- function deepMerge(base, patch) {
2093
- if (!isObject(base) || !isObject(patch)) return patch;
2094
- const result = { ...base };
2095
- for (const [key, value] of Object.entries(patch)) {
2096
- const previous = result[key];
2097
- result[key] = deepMerge(previous, value);
2098
- }
2099
- return result;
2120
+ function resolveLocalizedMethodHint(method) {
2121
+ return method.hint?.zh ?? method.hint?.en;
2100
2122
  }
2101
- function getPathValue(source, path) {
2102
- if (!source || typeof source !== "object") return;
2103
- const segments = path.split(".");
2104
- let current = source;
2105
- for (const segment of segments) {
2106
- if (!current || typeof current !== "object") return;
2107
- current = current[segment];
2108
- }
2109
- return current;
2123
+ function normalizeMethodId(value) {
2124
+ if (typeof value !== "string") return;
2125
+ const trimmed = value.trim();
2126
+ return trimmed.length > 0 ? trimmed : void 0;
2110
2127
  }
2111
- function setPathValue(target, path, value) {
2112
- const segments = path.split(".");
2113
- if (segments.length === 0) return;
2114
- let current = target;
2115
- for (let index = 0; index < segments.length - 1; index += 1) {
2116
- const segment = segments[index];
2117
- const next = current[segment];
2118
- if (!isObject(next)) current[segment] = {};
2119
- current = current[segment];
2128
+ function resolveAuthMethod(auth, requestedMethodId) {
2129
+ const protocol = auth.protocol ?? "rfc8628";
2130
+ const methods = (auth.methods ?? []).filter((entry) => normalizeMethodId(entry.id));
2131
+ const cleanRequestedMethodId = normalizeMethodId(requestedMethodId);
2132
+ if (methods.length === 0) {
2133
+ if (cleanRequestedMethodId) throw new Error(`provider auth method is not supported: ${cleanRequestedMethodId}`);
2134
+ return {
2135
+ protocol,
2136
+ baseUrl: auth.baseUrl,
2137
+ deviceCodePath: auth.deviceCodePath,
2138
+ tokenPath: auth.tokenPath,
2139
+ clientId: auth.clientId,
2140
+ scope: auth.scope,
2141
+ grantType: auth.grantType,
2142
+ usePkce: Boolean(auth.usePkce)
2143
+ };
2120
2144
  }
2121
- current[segments[segments.length - 1]] = value;
2122
- }
2123
- function isMissingRequiredValue(value) {
2124
- if (value === void 0 || value === null) return true;
2125
- if (typeof value === "string") return value.trim().length === 0;
2126
- if (Array.isArray(value)) return value.length === 0;
2127
- return false;
2145
+ let selectedMethod = methods.find((entry) => normalizeMethodId(entry.id) === cleanRequestedMethodId);
2146
+ if (!selectedMethod) {
2147
+ const fallbackMethodId = normalizeMethodId(auth.defaultMethodId) ?? normalizeMethodId(methods[0]?.id);
2148
+ selectedMethod = methods.find((entry) => normalizeMethodId(entry.id) === fallbackMethodId) ?? methods[0];
2149
+ }
2150
+ const methodId = normalizeMethodId(selectedMethod?.id);
2151
+ if (!selectedMethod || !methodId) throw new Error("provider auth method is not configured");
2152
+ if (cleanRequestedMethodId && methodId !== cleanRequestedMethodId) throw new Error(`provider auth method is not supported: ${cleanRequestedMethodId}`);
2153
+ return {
2154
+ id: methodId,
2155
+ protocol,
2156
+ baseUrl: selectedMethod.baseUrl ?? auth.baseUrl,
2157
+ deviceCodePath: selectedMethod.deviceCodePath ?? auth.deviceCodePath,
2158
+ tokenPath: selectedMethod.tokenPath ?? auth.tokenPath,
2159
+ clientId: selectedMethod.clientId ?? auth.clientId,
2160
+ scope: selectedMethod.scope ?? auth.scope,
2161
+ grantType: selectedMethod.grantType ?? auth.grantType,
2162
+ usePkce: selectedMethod.usePkce ?? Boolean(auth.usePkce),
2163
+ defaultApiBase: selectedMethod.defaultApiBase
2164
+ };
2128
2165
  }
2129
- function resolveRuntimeConfig(config, draftConfig) {
2130
- if (!draftConfig || Object.keys(draftConfig).length === 0) return config;
2131
- const merged = deepMerge(config, draftConfig);
2132
- return ConfigSchema.parse(merged);
2166
+ function parseExpiresAtMs(value, fallbackFromNowMs) {
2167
+ const normalized = normalizePositiveFloat(value);
2168
+ if (normalized === null) return Date.now() + fallbackFromNowMs;
2169
+ if (normalized >= 0xe8d4a51000) return Math.floor(normalized);
2170
+ if (normalized >= 1e9) return Math.floor(normalized * 1e3);
2171
+ return Date.now() + Math.floor(normalized * 1e3);
2133
2172
  }
2134
- function getActionById(config, actionId) {
2135
- return buildConfigSchemaView(config).actions.find((item) => item.id === actionId) ?? null;
2173
+ function parsePollIntervalMs(value, fallbackMs) {
2174
+ const normalized = normalizePositiveFloat(value);
2175
+ if (normalized === null) return fallbackMs;
2176
+ if (normalized <= 30) return Math.floor(normalized * 1e3);
2177
+ return Math.floor(normalized);
2136
2178
  }
2137
- function messageOrDefault(action, kind, fallback) {
2138
- const text = kind === "success" ? action.success?.message : action.failure?.message;
2139
- return text?.trim() ? text : fallback;
2179
+ function buildMinimaxErrorMessage(payload, fallback) {
2180
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return fallback;
2181
+ const record = payload;
2182
+ if (typeof record.error_description === "string" && record.error_description.trim()) return record.error_description.trim();
2183
+ if (typeof record.error === "string" && record.error.trim()) return record.error.trim();
2184
+ const baseMessage = record.base_resp?.status_msg;
2185
+ if (typeof baseMessage === "string" && baseMessage.trim()) return baseMessage.trim();
2186
+ return fallback;
2140
2187
  }
2141
- async function runFeishuVerifyAction(params) {
2142
- const { config, action } = params;
2143
- const appId = String(config.channels.feishu.appId ?? "").trim();
2144
- const appSecret = String(config.channels.feishu.appSecret ?? "").trim();
2145
- if (!appId || !appSecret) return {
2146
- ok: false,
2147
- status: "failed",
2148
- message: messageOrDefault(action, "failure", "Verification failed: missing credentials"),
2149
- data: { error: "missing credentials (appId, appSecret)" },
2150
- nextActions: []
2151
- };
2152
- const result = await probeFeishu(appId, appSecret);
2153
- if (!result.ok) return {
2154
- ok: false,
2155
- status: "failed",
2156
- message: `${messageOrDefault(action, "failure", "Verification failed")}: ${result.error}`,
2157
- data: {
2158
- error: result.error,
2159
- appId: result.appId ?? appId
2160
- },
2161
- nextActions: []
2162
- };
2163
- const responseData = {
2164
- appId: result.appId,
2165
- botName: result.botName ?? null,
2166
- botOpenId: result.botOpenId ?? null
2167
- };
2168
- const patch = {};
2169
- for (const [targetPath, sourcePath] of Object.entries(action.resultMap ?? {})) {
2170
- const mappedValue = sourcePath.startsWith("response.data.") ? responseData[sourcePath.slice(14)] : void 0;
2171
- if (mappedValue !== void 0) setPathValue(patch, targetPath, mappedValue);
2188
+ function classifyMiniMaxErrorStatus(message) {
2189
+ const normalized = message.toLowerCase();
2190
+ if (normalized.includes("deny") || normalized.includes("rejected")) return "denied";
2191
+ if (normalized.includes("expired") || normalized.includes("timeout") || normalized.includes("timed out")) return "expired";
2192
+ return "error";
2193
+ }
2194
+ function resolveHomePath(inputPath) {
2195
+ const trimmed = inputPath.trim();
2196
+ if (!trimmed) return trimmed;
2197
+ if (trimmed === "~") return homedir();
2198
+ if (trimmed.startsWith("~/")) return resolve(homedir(), trimmed.slice(2));
2199
+ if (isAbsolute(trimmed)) return trimmed;
2200
+ return resolve(trimmed);
2201
+ }
2202
+ function normalizeExpiresAt(value) {
2203
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.floor(value);
2204
+ if (typeof value === "string" && value.trim()) {
2205
+ const asNumber = Number(value);
2206
+ if (Number.isFinite(asNumber) && asNumber > 0) return Math.floor(asNumber);
2207
+ const parsedTime = Date.parse(value);
2208
+ if (Number.isFinite(parsedTime) && parsedTime > 0) return parsedTime;
2172
2209
  }
2173
- return {
2174
- ok: true,
2175
- status: "success",
2176
- message: messageOrDefault(action, "success", "Verified. Please finish Feishu event subscription and app publishing before using."),
2177
- data: responseData,
2178
- patch: Object.keys(patch).length > 0 ? patch : void 0,
2179
- nextActions: []
2180
- };
2210
+ return null;
2181
2211
  }
2182
- const ACTION_HANDLERS = { "channels.feishu.verifyConnection": runFeishuVerifyAction };
2183
- function buildUiHints(config, options) {
2184
- return buildConfigSchemaView(config, options).uiHints;
2212
+ function readFieldAsString(source, fieldName) {
2213
+ if (!fieldName) return null;
2214
+ const rawValue = source[fieldName];
2215
+ if (typeof rawValue !== "string") return null;
2216
+ const trimmed = rawValue.trim();
2217
+ return trimmed.length > 0 ? trimmed : null;
2185
2218
  }
2186
- function maskApiKey(value) {
2187
- if (!value) return { apiKeySet: false };
2188
- if (value.length < MASK_MIN_LENGTH) return {
2189
- apiKeySet: true,
2190
- apiKeyMasked: "****"
2219
+ function setProviderApiKey({ configPath, provider, accessToken, defaultApiBase }) {
2220
+ const config = loadConfig(configPath);
2221
+ const providers = config.providers;
2222
+ if (!providers[provider]) return;
2223
+ const target = providers[provider];
2224
+ target.apiKey = accessToken;
2225
+ if (defaultApiBase) target.apiBase = defaultApiBase;
2226
+ saveConfig(ConfigSchema.parse(config), configPath);
2227
+ }
2228
+ function resolveProviderAuthTarget(configPath, providerId) {
2229
+ const provider = loadConfig(configPath).providers[providerId];
2230
+ if (!provider) return null;
2231
+ const configuredType = typeof provider.providerType === "string" ? provider.providerType.trim() : "";
2232
+ if (configuredType && findServerBuiltinProviderByName(configuredType)) return {
2233
+ providerId,
2234
+ providerType: configuredType,
2235
+ provider
2191
2236
  };
2192
- return {
2193
- apiKeySet: true,
2194
- apiKeyMasked: `${value.slice(0, 2)}****${value.slice(-4)}`
2237
+ if (findServerBuiltinProviderByName(providerId)) return {
2238
+ providerId,
2239
+ providerType: providerId,
2240
+ provider
2195
2241
  };
2242
+ return null;
2196
2243
  }
2197
- function normalizeModelList(input) {
2198
- if (!input || input.length === 0) return [];
2199
- const deduped = /* @__PURE__ */ new Set();
2200
- for (const item of input) {
2201
- if (typeof item !== "string") continue;
2202
- const trimmed = item.trim();
2203
- if (!trimmed) continue;
2204
- deduped.add(trimmed);
2244
+ async function startProviderAuth(configPath, providerId, options) {
2245
+ cleanupExpiredAuthSessions();
2246
+ const target = resolveProviderAuthTarget(configPath, providerId);
2247
+ if (!target) return null;
2248
+ const spec = findServerBuiltinProviderByName(target.providerType);
2249
+ if (!spec?.auth || spec.auth.kind !== "device_code") return null;
2250
+ const resolvedMethod = resolveAuthMethod(spec.auth, options?.methodId);
2251
+ const { deviceCodeEndpoint, tokenEndpoint } = resolveDeviceCodeEndpoints(resolvedMethod.baseUrl, resolvedMethod.deviceCodePath, resolvedMethod.tokenPath);
2252
+ const pkce = resolvedMethod.usePkce ? buildPkce() : null;
2253
+ let authorizationCode = "";
2254
+ let tokenCodeField = "device_code";
2255
+ let userCode = "";
2256
+ let verificationUri = "";
2257
+ let intervalMs = DEFAULT_AUTH_INTERVAL_MS;
2258
+ let expiresAtMs = Date.now() + 6e5;
2259
+ if (resolvedMethod.protocol === "minimax_user_code") {
2260
+ if (!pkce) throw new Error("MiniMax OAuth requires PKCE");
2261
+ const state = toBase64Url(randomBytes(16));
2262
+ const body = new URLSearchParams({
2263
+ response_type: "code",
2264
+ client_id: resolvedMethod.clientId,
2265
+ scope: resolvedMethod.scope,
2266
+ code_challenge: pkce.challenge,
2267
+ code_challenge_method: "S256",
2268
+ state
2269
+ });
2270
+ const response = await fetch(deviceCodeEndpoint, {
2271
+ method: "POST",
2272
+ headers: {
2273
+ "Content-Type": "application/x-www-form-urlencoded",
2274
+ Accept: "application/json",
2275
+ "x-request-id": randomUUID()
2276
+ },
2277
+ body
2278
+ });
2279
+ const payload = await response.json().catch(() => ({}));
2280
+ if (!response.ok) throw new Error(buildMinimaxErrorMessage(payload, response.statusText || "MiniMax OAuth start failed"));
2281
+ if (payload.state && payload.state !== state) throw new Error("MiniMax OAuth state mismatch");
2282
+ authorizationCode = payload.user_code?.trim() ?? "";
2283
+ userCode = authorizationCode;
2284
+ verificationUri = payload.verification_uri?.trim() ?? "";
2285
+ if (!authorizationCode || !verificationUri) throw new Error("provider auth payload is incomplete");
2286
+ tokenCodeField = "user_code";
2287
+ intervalMs = Math.min(parsePollIntervalMs(payload.interval, DEFAULT_AUTH_INTERVAL_MS), MAX_AUTH_INTERVAL_MS);
2288
+ expiresAtMs = parseExpiresAtMs(payload.expired_in, 6e5);
2289
+ } else {
2290
+ const body = new URLSearchParams({
2291
+ client_id: resolvedMethod.clientId,
2292
+ scope: resolvedMethod.scope
2293
+ });
2294
+ if (pkce) {
2295
+ body.set("code_challenge", pkce.challenge);
2296
+ body.set("code_challenge_method", "S256");
2297
+ }
2298
+ const response = await fetch(deviceCodeEndpoint, {
2299
+ method: "POST",
2300
+ headers: {
2301
+ "Content-Type": "application/x-www-form-urlencoded",
2302
+ Accept: "application/json"
2303
+ },
2304
+ body
2305
+ });
2306
+ const payload = await response.json().catch(() => ({}));
2307
+ if (!response.ok) {
2308
+ const message = payload.error_description || payload.error || response.statusText || "device code auth failed";
2309
+ throw new Error(message);
2310
+ }
2311
+ authorizationCode = payload.device_code?.trim() ?? "";
2312
+ userCode = payload.user_code?.trim() ?? "";
2313
+ verificationUri = payload.verification_uri_complete?.trim() || payload.verification_uri?.trim() || "";
2314
+ if (!authorizationCode || !userCode || !verificationUri) throw new Error("provider auth payload is incomplete");
2315
+ intervalMs = normalizePositiveInt(payload.interval, DEFAULT_AUTH_INTERVAL_MS / 1e3) * 1e3;
2316
+ const expiresInSec = normalizePositiveInt(payload.expires_in, 600);
2317
+ expiresAtMs = Date.now() + expiresInSec * 1e3;
2205
2318
  }
2206
- return [...deduped];
2207
- }
2208
- function toProviderView(config, provider, providerId, uiHints, spec) {
2209
- const providerType = resolveProviderType(providerId, provider);
2210
- const apiKeyRefSet = hasSecretRef(config, `providers.${providerId}.apiKey`);
2211
- const masked = maskApiKey(provider.apiKey);
2212
- const extraHeaders = provider.extraHeaders && Object.keys(provider.extraHeaders).length > 0 ? sanitizePublicConfigValue(provider.extraHeaders, `providers.${providerId}.extraHeaders`, uiHints) : null;
2213
- const supportsWireApi = Boolean(spec?.supportsWireApi) || providerType === null;
2214
- return {
2319
+ const sessionId = randomUUID();
2320
+ authSessions.set(sessionId, {
2321
+ sessionId,
2215
2322
  providerId,
2216
- providerType,
2217
- isBuiltInType: providerType !== null,
2218
- isCustom: providerType === null,
2219
- enabled: provider.enabled !== false,
2220
- displayName: resolveProviderInstanceDisplayName(providerId, provider, spec),
2221
- apiKeyRequired: !spec?.anonymousApiKey,
2222
- apiKeySet: masked.apiKeySet || apiKeyRefSet,
2223
- apiKeyMasked: masked.apiKeyMasked ?? (apiKeyRefSet ? "****" : void 0),
2224
- apiBase: provider.apiBase ?? null,
2225
- extraHeaders: extraHeaders && Object.keys(extraHeaders).length > 0 ? extraHeaders : null,
2226
- models: normalizeModelList(provider.models ?? []),
2227
- modelConfig: normalizeProviderModelConfig(provider.modelConfig ?? {}),
2228
- wireApi: supportsWireApi ? provider.wireApi ?? spec?.defaultWireApi ?? "auto" : void 0
2323
+ providerType: target.providerType,
2324
+ configPath,
2325
+ authorizationCode,
2326
+ tokenCodeField,
2327
+ protocol: resolvedMethod.protocol,
2328
+ methodId: resolvedMethod.id,
2329
+ codeVerifier: pkce?.verifier,
2330
+ tokenEndpoint,
2331
+ clientId: resolvedMethod.clientId,
2332
+ grantType: resolvedMethod.grantType,
2333
+ defaultApiBase: resolvedMethod.defaultApiBase ?? spec.defaultApiBase,
2334
+ expiresAtMs,
2335
+ intervalMs
2336
+ });
2337
+ const methodConfig = (spec.auth.methods ?? []).find((entry) => normalizeMethodId(entry.id) === resolvedMethod.id);
2338
+ const methodLabel = methodConfig ? resolveLocalizedMethodLabel(methodConfig, resolvedMethod.id ?? "") : void 0;
2339
+ const methodHint = methodConfig ? resolveLocalizedMethodHint(methodConfig) : void 0;
2340
+ return {
2341
+ provider: providerId,
2342
+ kind: "device_code",
2343
+ methodId: resolvedMethod.id,
2344
+ sessionId,
2345
+ verificationUri,
2346
+ userCode,
2347
+ expiresAt: new Date(expiresAtMs).toISOString(),
2348
+ intervalMs,
2349
+ note: methodHint ?? methodLabel ?? resolveAuthNote(spec.auth.note ?? {})
2229
2350
  };
2230
2351
  }
2231
- function buildConfigView(config, options) {
2232
- const uiHints = buildUiHints(config, options);
2233
- const projectedChannels = getProjectedChannelMap(config, options);
2234
- const providers = {};
2235
- for (const [providerId, provider] of Object.entries(config.providers)) {
2236
- const providerConfig = provider;
2237
- providers[providerId] = toProviderView(config, providerConfig, providerId, uiHints, findServerBuiltinProviderByName(resolveProviderType(providerId, providerConfig) ?? ""));
2352
+ async function pollProviderAuth(params) {
2353
+ const { configPath, providerName: providerId, sessionId } = params;
2354
+ cleanupExpiredAuthSessions();
2355
+ const session = authSessions.get(sessionId);
2356
+ if (!session || session.providerId !== providerId || session.configPath !== configPath) return null;
2357
+ if (Date.now() >= session.expiresAtMs) {
2358
+ authSessions.delete(sessionId);
2359
+ return {
2360
+ provider: providerId,
2361
+ status: "expired",
2362
+ message: "authorization session expired"
2363
+ };
2238
2364
  }
2239
- return {
2240
- companion: sanitizePublicConfigValue(config.companion, "companion", uiHints),
2241
- agents: sanitizePublicConfigValue(config.agents, "agents", uiHints),
2242
- providers,
2243
- search: buildSearchView(config),
2244
- channels: sanitizePublicConfigValue(projectedChannels, "channels", uiHints),
2245
- bindings: sanitizePublicConfigValue(config.bindings, "bindings", uiHints),
2246
- session: sanitizePublicConfigValue(config.session, "session", uiHints),
2247
- tools: sanitizePublicConfigValue(config.tools, "tools", uiHints),
2248
- gateway: sanitizePublicConfigValue(config.gateway, "gateway", uiHints),
2249
- ui: sanitizePublicConfigValue(config.ui, "ui", uiHints),
2250
- secrets: {
2251
- enabled: config.secrets.enabled,
2252
- defaults: { ...config.secrets.defaults },
2253
- providers: { ...config.secrets.providers },
2254
- refs: { ...config.secrets.refs }
2365
+ const body = new URLSearchParams({
2366
+ grant_type: session.grantType,
2367
+ client_id: session.clientId
2368
+ });
2369
+ body.set(session.tokenCodeField, session.authorizationCode);
2370
+ if (session.codeVerifier) body.set("code_verifier", session.codeVerifier);
2371
+ const response = await fetch(session.tokenEndpoint, {
2372
+ method: "POST",
2373
+ headers: {
2374
+ "Content-Type": "application/x-www-form-urlencoded",
2375
+ Accept: "application/json"
2376
+ },
2377
+ body
2378
+ });
2379
+ let accessToken = "";
2380
+ if (session.protocol === "minimax_user_code") {
2381
+ const raw = await response.text();
2382
+ let payload = {};
2383
+ if (raw) try {
2384
+ payload = JSON.parse(raw);
2385
+ } catch {
2386
+ payload = {};
2255
2387
  }
2256
- };
2257
- }
2258
- function normalizeRuntimeEntries(entries) {
2259
- if (!entries || typeof entries !== "object" || Array.isArray(entries)) return {};
2260
- const normalized = {};
2261
- for (const [rawId, rawEntry] of Object.entries(entries)) {
2262
- const id = rawId.trim();
2263
- if (!id || !rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
2264
- const entry = rawEntry;
2265
- const type = normalizeOptionalString(entry.type);
2266
- if (!type) continue;
2267
- const normalizedIcon = normalizeRuntimeEntryIcon(entry.icon);
2268
- normalized[id] = {
2269
- enabled: typeof entry.enabled === "boolean" ? entry.enabled : true,
2270
- ...normalizeOptionalString(entry.label) ? { label: normalizeOptionalString(entry.label) ?? void 0 } : {},
2271
- ...normalizedIcon ? { icon: normalizedIcon } : {},
2272
- type,
2273
- config: normalizeRuntimeEntryConfig(type, entry.config && typeof entry.config === "object" && !Array.isArray(entry.config) ? entry.config : {})
2388
+ if (!response.ok) return {
2389
+ provider: providerId,
2390
+ status: "error",
2391
+ message: buildMinimaxErrorMessage(payload, raw || response.statusText || "authorization failed")
2392
+ };
2393
+ const status = payload.status?.trim().toLowerCase();
2394
+ if (status === "success") {
2395
+ accessToken = payload.access_token?.trim() ?? "";
2396
+ if (!accessToken) return {
2397
+ provider: providerId,
2398
+ status: "error",
2399
+ message: "provider token response missing access token"
2400
+ };
2401
+ } else if (status === "error") {
2402
+ const message = buildMinimaxErrorMessage(payload, "authorization failed");
2403
+ const classified = classifyMiniMaxErrorStatus(message);
2404
+ if (classified === "denied" || classified === "expired") authSessions.delete(sessionId);
2405
+ return {
2406
+ provider: providerId,
2407
+ status: classified,
2408
+ message
2409
+ };
2410
+ } else {
2411
+ const nextPollMs = Math.min(Math.floor(session.intervalMs * 1.5), MAX_AUTH_INTERVAL_MS);
2412
+ session.intervalMs = nextPollMs;
2413
+ authSessions.set(sessionId, session);
2414
+ return {
2415
+ provider: providerId,
2416
+ status: "pending",
2417
+ nextPollMs
2418
+ };
2419
+ }
2420
+ } else {
2421
+ const payload = await response.json().catch(() => ({}));
2422
+ if (!response.ok) {
2423
+ const errorCode = payload.error?.trim().toLowerCase();
2424
+ if (errorCode === "authorization_pending") return {
2425
+ provider: providerId,
2426
+ status: "pending",
2427
+ nextPollMs: session.intervalMs
2428
+ };
2429
+ if (errorCode === "slow_down") {
2430
+ const nextPollMs = Math.min(Math.floor(session.intervalMs * 1.5), MAX_AUTH_INTERVAL_MS);
2431
+ session.intervalMs = nextPollMs;
2432
+ authSessions.set(sessionId, session);
2433
+ return {
2434
+ provider: providerId,
2435
+ status: "pending",
2436
+ nextPollMs
2437
+ };
2438
+ }
2439
+ if (errorCode === "access_denied") {
2440
+ authSessions.delete(sessionId);
2441
+ return {
2442
+ provider: providerId,
2443
+ status: "denied",
2444
+ message: payload.error_description || "authorization denied"
2445
+ };
2446
+ }
2447
+ if (errorCode === "expired_token") {
2448
+ authSessions.delete(sessionId);
2449
+ return {
2450
+ provider: providerId,
2451
+ status: "expired",
2452
+ message: payload.error_description || "authorization session expired"
2453
+ };
2454
+ }
2455
+ return {
2456
+ provider: providerId,
2457
+ status: "error",
2458
+ message: payload.error_description || payload.error || response.statusText || "authorization failed"
2459
+ };
2460
+ }
2461
+ accessToken = payload.access_token?.trim() ?? "";
2462
+ if (!accessToken) return {
2463
+ provider: providerId,
2464
+ status: "error",
2465
+ message: "provider token response missing access token"
2274
2466
  };
2275
2467
  }
2276
- return normalized;
2277
- }
2278
- function normalizeRuntimeEntryIcon(value) {
2279
- if (typeof value === "string") {
2280
- const src = value.trim();
2281
- return src ? {
2282
- kind: "image",
2283
- src
2284
- } : null;
2285
- }
2286
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2287
- const src = normalizeOptionalString(value.src);
2288
- if (!src) return null;
2289
- const alt = normalizeOptionalString(value.alt);
2468
+ setProviderApiKey({
2469
+ configPath,
2470
+ provider: providerId,
2471
+ accessToken,
2472
+ defaultApiBase: session.defaultApiBase
2473
+ });
2474
+ authSessions.delete(sessionId);
2290
2475
  return {
2291
- kind: "image",
2292
- src,
2293
- ...alt ? { alt } : {}
2476
+ provider: providerId,
2477
+ status: "authorized"
2294
2478
  };
2295
2479
  }
2296
- function clearSecretRef(refs, path) {
2297
- const { [path]: _removed, ...nextRefs } = refs;
2298
- return nextRefs;
2299
- }
2300
- function buildConfigMeta(config, options) {
2480
+ async function importProviderAuthFromCli(configPath, providerId) {
2481
+ const target = resolveProviderAuthTarget(configPath, providerId);
2482
+ if (!target) return null;
2483
+ const spec = findServerBuiltinProviderByName(target.providerType);
2484
+ if (!spec?.auth || spec.auth.kind !== "device_code" || !spec.auth.cliCredential) return null;
2485
+ const credentialPath = resolveHomePath(spec.auth.cliCredential.path);
2486
+ if (!credentialPath) throw new Error("provider cli credential path is empty");
2487
+ let rawContent = "";
2488
+ try {
2489
+ rawContent = await readFile(credentialPath, "utf8");
2490
+ } catch (error) {
2491
+ const message = error instanceof Error ? error.message : String(error);
2492
+ throw new Error(`failed to read CLI credential: ${message}`);
2493
+ }
2494
+ let payload;
2495
+ try {
2496
+ const parsed = JSON.parse(rawContent);
2497
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("credential payload is not an object");
2498
+ payload = parsed;
2499
+ } catch (error) {
2500
+ const message = error instanceof Error ? error.message : String(error);
2501
+ throw new Error(`invalid CLI credential JSON: ${message}`);
2502
+ }
2503
+ const accessToken = readFieldAsString(payload, spec.auth.cliCredential.accessTokenField);
2504
+ if (!accessToken) throw new Error(`CLI credential missing access token field: ${spec.auth.cliCredential.accessTokenField}`);
2505
+ const expiresAtMs = normalizeExpiresAt(spec.auth.cliCredential.expiresAtField ? payload[spec.auth.cliCredential.expiresAtField] : void 0);
2506
+ if (typeof expiresAtMs === "number" && expiresAtMs <= Date.now()) throw new Error("CLI credential has expired, please login again");
2507
+ setProviderApiKey({
2508
+ configPath,
2509
+ provider: providerId,
2510
+ accessToken,
2511
+ defaultApiBase: spec.defaultApiBase
2512
+ });
2301
2513
  return {
2302
- search: SEARCH_PROVIDER_META,
2303
- channels: buildProjectedChannelMeta(config, options)
2514
+ provider: providerId,
2515
+ status: "imported",
2516
+ source: "cli",
2517
+ expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : void 0
2304
2518
  };
2305
2519
  }
2306
- function buildProviderTemplatesView() {
2307
- return { providerTemplates: BUILTIN_PROVIDERS.map((spec) => {
2520
+ //#endregion
2521
+ //#region src/features/config/controllers/config.controller.ts
2522
+ var ConfigRoutesController = class {
2523
+ channelConfigApplyTasks = /* @__PURE__ */ new Map();
2524
+ providerConnectivity;
2525
+ constructor(options) {
2526
+ this.options = options;
2527
+ this.providerConnectivity = new ProviderConnectivityService(options.configPath, options.kernel.llmProviders);
2528
+ }
2529
+ getExtensionConfigProjectionOptions = () => {
2308
2530
  return {
2309
- id: spec.name,
2310
- providerType: spec.name,
2311
- displayName: spec.displayName ?? spec.name,
2312
- apiProtocol: spec.apiProtocol,
2313
- modelPrefix: spec.modelPrefix,
2314
- keywords: spec.keywords,
2315
- envKey: spec.envKey,
2316
- isGateway: spec.isGateway,
2317
- isLocal: spec.isLocal,
2318
- apiKeyRequired: !spec.anonymousApiKey,
2319
- defaultApiBase: spec.defaultApiBase,
2320
- logo: spec.logo,
2321
- apiBaseHelp: spec.apiBaseHelp,
2322
- auth: spec.auth ? {
2323
- kind: spec.auth.kind,
2324
- displayName: spec.auth.displayName,
2325
- note: spec.auth.note,
2326
- methods: spec.auth.methods?.map((method) => ({
2327
- id: method.id,
2328
- label: method.label,
2329
- hint: method.hint
2330
- })),
2331
- defaultMethodId: spec.auth.defaultMethodId,
2332
- supportsCliImport: Boolean(spec.auth.cliCredential)
2333
- } : void 0,
2334
- defaultModels: normalizeModelList(spec.defaultModels ?? []),
2335
- modelConfig: normalizeProviderModelConfig(spec.modelConfig ?? {}),
2336
- supportsWireApi: spec.supportsWireApi,
2337
- wireApiOptions: spec.wireApiOptions,
2338
- defaultWireApi: spec.defaultWireApi
2531
+ extensionChannelBindings: this.options.extensions?.getChannelBindings() ?? [],
2532
+ extensionUiMetadata: this.options.extensions?.getUiMetadata() ?? []
2339
2533
  };
2340
- }).sort((left, right) => {
2341
- const leftRank = PREFERRED_PROVIDER_ORDER_INDEX.get(left.id);
2342
- const rightRank = PREFERRED_PROVIDER_ORDER_INDEX.get(right.id);
2343
- if (leftRank !== void 0 && rightRank !== void 0) return leftRank - rightRank;
2344
- if (leftRank !== void 0) return -1;
2345
- if (rightRank !== void 0) return 1;
2346
- return left.id.localeCompare(right.id);
2347
- }) };
2348
- }
2349
- function buildProvidersView(config) {
2350
- const uiHints = buildUiHints(config);
2351
- const providers = {};
2352
- for (const [providerId, provider] of Object.entries(config.providers)) {
2353
- const providerConfig = provider;
2354
- providers[providerId] = toProviderView(config, providerConfig, providerId, uiHints, findServerBuiltinProviderByName(resolveProviderType(providerId, providerConfig) ?? ""));
2355
- }
2356
- return { providers };
2357
- }
2358
- function buildConfigSchemaView(_config, options) {
2359
- const base = buildConfigSchema({ version: getPackageVersion() });
2360
- const extensionUiHints = buildExtensionChannelUiHints(options);
2361
- if (Object.keys(extensionUiHints).length === 0) return base;
2362
- return {
2363
- ...base,
2364
- uiHints: {
2365
- ...base.uiHints,
2366
- ...extensionUiHints
2367
- }
2368
2534
  };
2369
- }
2370
- async function executeConfigAction(configPath, actionId, request) {
2371
- const baseConfig = loadConfigOrDefault(configPath);
2372
- const action = getActionById(baseConfig, actionId);
2373
- if (!action) return {
2374
- ok: false,
2375
- code: "ACTION_NOT_FOUND",
2376
- message: `unknown action: ${actionId}`
2535
+ publishConfigUpdatedPaths = (paths) => {
2536
+ for (const path of paths) emitConfigUpdated(this.options, path);
2537
+ };
2538
+ publishConfigUpdates = async (paths) => {
2539
+ this.publishConfigUpdatedPaths(paths);
2540
+ await this.options.applyLiveConfigReload?.();
2541
+ };
2542
+ publishChannelConfigApplyStatus = (params) => {
2543
+ emitChannelConfigApplyStatus(this.options, params);
2544
+ };
2545
+ enqueueChannelConfigApply = (channel) => {
2546
+ const task = (this.channelConfigApplyTasks.get(channel) ?? Promise.resolve()).catch(() => void 0).then(async () => {
2547
+ this.publishChannelConfigApplyStatus({
2548
+ channel,
2549
+ status: "started"
2550
+ });
2551
+ try {
2552
+ await this.options.applyLiveConfigReload?.();
2553
+ this.publishChannelConfigApplyStatus({
2554
+ channel,
2555
+ status: "succeeded"
2556
+ });
2557
+ } catch (error) {
2558
+ const message = error instanceof Error ? error.message : String(error);
2559
+ this.publishChannelConfigApplyStatus({
2560
+ channel,
2561
+ status: "failed",
2562
+ message
2563
+ });
2564
+ emitUiError(this.options, {
2565
+ code: "CHANNEL_CONFIG_APPLY_FAILED",
2566
+ message: `Failed to apply ${channel} channel config: ${message}`
2567
+ });
2568
+ }
2569
+ }).finally(() => {
2570
+ if (this.channelConfigApplyTasks.get(channel) === task) this.channelConfigApplyTasks.delete(channel);
2571
+ });
2572
+ this.channelConfigApplyTasks.set(channel, task);
2573
+ };
2574
+ getConfig = (c) => {
2575
+ const config = loadConfigOrDefault(this.options.configPath);
2576
+ return c.json(ok(buildConfigView(config, this.getExtensionConfigProjectionOptions())));
2377
2577
  };
2378
- if (request.scope && request.scope !== action.scope) return {
2379
- ok: false,
2380
- code: "ACTION_SCOPE_MISMATCH",
2381
- message: `scope mismatch: expected ${action.scope}, got ${request.scope}`,
2382
- details: {
2383
- expectedScope: action.scope,
2384
- requestScope: request.scope
2385
- }
2578
+ getConfigMeta = (c) => {
2579
+ const config = loadConfigOrDefault(this.options.configPath);
2580
+ return c.json(ok(buildConfigMeta(config, this.getExtensionConfigProjectionOptions())));
2386
2581
  };
2387
- const runtimeConfig = resolveRuntimeConfig(baseConfig, request.draftConfig);
2388
- for (const requiredPath of action.requires ?? []) if (isMissingRequiredValue(getPathValue(runtimeConfig, requiredPath))) return {
2389
- ok: false,
2390
- code: "ACTION_PRECONDITION_FAILED",
2391
- message: `required field missing: ${requiredPath}`,
2392
- details: { path: requiredPath }
2582
+ listProviders = (c) => {
2583
+ const config = loadConfigOrDefault(this.options.configPath);
2584
+ return c.json(ok(buildProvidersView(config)));
2393
2585
  };
2394
- const handler = ACTION_HANDLERS[action.id];
2395
- if (!handler) return {
2396
- ok: false,
2397
- code: "ACTION_EXECUTION_FAILED",
2398
- message: `action handler not found for type ${action.type}`
2586
+ listProviderTemplates = (c) => {
2587
+ return c.json(ok(buildProviderTemplatesView()));
2399
2588
  };
2400
- return {
2401
- ok: true,
2402
- data: await handler({
2403
- config: runtimeConfig,
2404
- action
2405
- })
2589
+ listProviderModelCatalog = (c) => {
2590
+ return c.json(ok(this.options.kernel.providerModelCatalog.getSnapshot()));
2406
2591
  };
2407
- }
2408
- function loadConfigOrDefault(configPath) {
2409
- return loadConfig(configPath);
2410
- }
2411
- function createProviderForUpdate(providerId) {
2412
- const spec = findServerBuiltinProviderByName(providerId);
2413
- if (!spec) return null;
2414
- return createDefaultProviderConfigFromSpec(spec);
2415
- }
2416
- function updateModel(configPath, patch) {
2417
- const config = loadConfigOrDefault(configPath);
2418
- if (typeof patch.model === "string") config.agents.defaults.model = patch.model;
2419
- if (typeof patch.workspace === "string") config.agents.defaults.workspace = normalizeOptionalString(patch.workspace) ?? DEFAULT_WORKSPACE_PATH;
2420
- const next = ConfigSchema.parse(config);
2421
- saveConfig(next, configPath);
2422
- return buildConfigView(next);
2423
- }
2424
- function updateProvider(configPath, providerId, patch) {
2425
- const config = loadConfigOrDefault(configPath);
2426
- const providers = config.providers;
2427
- const provider = providers[providerId] ?? createProviderForUpdate(providerId);
2428
- if (!provider) return null;
2429
- providers[providerId] = provider;
2430
- const spec = findServerBuiltinProviderByName((Object.prototype.hasOwnProperty.call(patch, "providerType") ? normalizeProviderId(patch.providerType) : resolveProviderType(providerId, provider)) ?? "");
2431
- if (Object.prototype.hasOwnProperty.call(patch, "providerType")) provider.providerType = spec?.name ?? null;
2432
- if (Object.prototype.hasOwnProperty.call(patch, "displayName")) provider.displayName = normalizeOptionalDisplayName(patch.displayName) ?? "";
2433
- if (Object.prototype.hasOwnProperty.call(patch, "enabled")) provider.enabled = patch.enabled !== false;
2434
- if (Object.prototype.hasOwnProperty.call(patch, "apiKey")) {
2435
- provider.apiKey = patch.apiKey ?? "";
2436
- config.secrets.refs = clearSecretRef(config.secrets.refs, `providers.${providerId}.apiKey`);
2437
- }
2438
- if (Object.prototype.hasOwnProperty.call(patch, "apiBase")) provider.apiBase = patch.apiBase ?? null;
2439
- if (Object.prototype.hasOwnProperty.call(patch, "extraHeaders")) provider.extraHeaders = patch.extraHeaders ?? null;
2440
- if (Object.prototype.hasOwnProperty.call(patch, "wireApi") && (spec?.supportsWireApi || !spec)) provider.wireApi = patch.wireApi ?? spec?.defaultWireApi ?? "auto";
2441
- if (Object.prototype.hasOwnProperty.call(patch, "models")) provider.models = normalizeModelList(patch.models ?? []);
2442
- if (Object.prototype.hasOwnProperty.call(patch, "modelConfig")) provider.modelConfig = normalizeProviderModelConfig(patch.modelConfig ?? {});
2443
- const next = ConfigSchema.parse(config);
2444
- saveConfig(next, configPath);
2445
- const uiHints = buildUiHints(next);
2446
- const updated = next.providers[providerId];
2447
- return toProviderView(next, updated, providerId, uiHints, spec ?? void 0);
2448
- }
2449
- function createProvider(configPath, patch = {}) {
2450
- const config = loadConfigOrDefault(configPath);
2451
- const providers = config.providers;
2452
- const requestedProviderType = normalizeProviderId(patch.providerType);
2453
- const spec = requestedProviderType ? findServerBuiltinProviderByName(requestedProviderType) : void 0;
2454
- const fallbackProviderId = spec ? spec.name : findNextCustomProviderName(config);
2455
- const requestedProviderId = normalizeProviderId(patch.providerId);
2456
- if (requestedProviderId && providers[requestedProviderId]) return null;
2457
- const providerId = requestedProviderId ? requestedProviderId : findNextProviderId(config, fallbackProviderId);
2458
- const generatedDisplayName = spec ? `${spec.displayName}${resolveProviderDisplayNameSuffix(providerId, spec.name)}` : resolveCustomProviderFallbackDisplayName(providerId);
2459
- const defaultModels = spec ? buildProviderScopedModels(providerId, spec.defaultModels ?? []) : [];
2460
- providers[providerId] = {
2461
- enabled: patch.enabled !== false,
2462
- providerType: spec?.name ?? null,
2463
- displayName: normalizeOptionalDisplayName(patch.displayName) ?? generatedDisplayName,
2464
- apiKey: normalizeOptionalString(patch.apiKey) ?? "",
2465
- apiBase: normalizeOptionalString(patch.apiBase) ?? spec?.defaultApiBase ?? null,
2466
- extraHeaders: normalizeHeaders(patch.extraHeaders ?? null),
2467
- wireApi: patch.wireApi ?? spec?.defaultWireApi ?? "auto",
2468
- models: Object.prototype.hasOwnProperty.call(patch, "models") ? normalizeModelList(patch.models ?? []) : defaultModels,
2469
- modelConfig: normalizeProviderModelConfig(patch.modelConfig ?? spec?.modelConfig ?? {})
2592
+ getConfigSchema = (c) => {
2593
+ const config = loadConfigOrDefault(this.options.configPath);
2594
+ return c.json(ok(buildConfigSchemaView(config, this.getExtensionConfigProjectionOptions())));
2470
2595
  };
2471
- const next = ConfigSchema.parse(config);
2472
- saveConfig(next, configPath);
2473
- const uiHints = buildUiHints(next);
2474
- const created = next.providers[providerId];
2475
- return {
2476
- providerId,
2477
- provider: toProviderView(next, created, providerId, uiHints, spec)
2596
+ updateConfigModel = async (c) => {
2597
+ const body = await readJson(c.req.raw);
2598
+ if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
2599
+ const hasModel = typeof body.data.model === "string";
2600
+ if (!hasModel) return c.json(err("INVALID_BODY", "model is required"), 400);
2601
+ const view = updateModel(this.options.configPath, {
2602
+ model: body.data.model,
2603
+ workspace: body.data.workspace
2604
+ });
2605
+ const changedPaths = [];
2606
+ if (hasModel) changedPaths.push("agents.defaults.model");
2607
+ if (typeof body.data.workspace === "string") changedPaths.push("agents.defaults.workspace");
2608
+ await this.publishConfigUpdates(changedPaths);
2609
+ return c.json(ok({
2610
+ model: view.agents.defaults.model,
2611
+ workspace: view.agents.defaults.workspace
2612
+ }));
2478
2613
  };
2479
- }
2480
- function deleteProvider(configPath, providerId) {
2481
- const config = loadConfigOrDefault(configPath);
2482
- const providers = config.providers;
2483
- if (!providers[providerId]) return null;
2484
- delete providers[providerId];
2485
- config.secrets.refs = clearSecretRefsByPrefix(config.secrets.refs, `providers.${providerId}`);
2486
- saveConfig(ConfigSchema.parse(config), configPath);
2487
- return true;
2488
- }
2489
- function normalizeOptionalString(value) {
2490
- if (typeof value !== "string") return null;
2491
- const trimmed = value.trim();
2492
- return trimmed.length > 0 ? trimmed : null;
2493
- }
2494
- function normalizeHeaders(input) {
2495
- if (!input) return null;
2496
- const entries = Object.entries(input).map(([key, value]) => [key.trim(), String(value ?? "").trim()]).filter(([key, value]) => key.length > 0 && value.length > 0);
2497
- if (entries.length === 0) return null;
2498
- return Object.fromEntries(entries);
2499
- }
2500
- function buildScopedProviderModel(providerName, model, spec) {
2501
- const trimmed = model.trim();
2502
- if (!trimmed) return "";
2503
- if (trimmed.includes("/")) return trimmed;
2504
- if (isCustomProviderName(providerName)) return trimmed;
2505
- const prefix = (spec?.modelPrefix ?? providerName).trim();
2506
- if (!prefix) return trimmed;
2507
- return `${prefix}/${trimmed}`;
2508
- }
2509
- function rewriteProviderRoutePrefix(providerId, targetPrefix, model) {
2510
- const prefix = `${providerId}/`;
2511
- if (!model.startsWith(prefix)) return model;
2512
- const stripped = model.slice(prefix.length).trim();
2513
- return stripped ? targetPrefix ? `${targetPrefix}/${stripped}` : stripped : model;
2514
- }
2515
- function resolveTestModel(config, providerId, requestedModel, provider, spec) {
2516
- if (requestedModel) return rewriteProviderRoutePrefix(providerId, spec?.name ?? null, requestedModel);
2517
- const providerModels = normalizeModelList(provider.models ?? []).map((modelId) => {
2518
- const providerModel = rewriteProviderRoutePrefix(providerId, null, modelId);
2519
- return spec ? buildScopedProviderModel(spec.name, providerModel, spec) : providerModel;
2520
- }).filter((modelId) => modelId.length > 0);
2521
- if (providerModels.length > 0) return providerModels[0];
2522
- const defaultModel = normalizeOptionalString(config.agents.defaults.model);
2523
- if (defaultModel) {
2524
- const routedProvider = getProviderName(config, defaultModel);
2525
- if (!routedProvider || routedProvider === providerId) return rewriteProviderRoutePrefix(providerId, spec?.name ?? null, defaultModel);
2526
- }
2527
- if (!spec) return null;
2528
- return normalizeModelList(spec?.defaultModels ?? [])[0] ?? null ?? defaultModel ?? null;
2529
- }
2530
- function stringifyError(error) {
2531
- return (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim();
2532
- }
2533
- async function testProviderConnection(configPath, providerId, patch, providerManager) {
2534
- const config = loadConfigOrDefault(configPath);
2535
- const provider = config.providers[providerId];
2536
- if (!provider) return null;
2537
- const providerType = resolveProviderType(providerId, provider);
2538
- const spec = findServerBuiltinProviderByName(providerType ?? "");
2539
- const hasApiKeyPatch = Object.prototype.hasOwnProperty.call(patch, "apiKey");
2540
- const providedApiKey = normalizeOptionalString(patch.apiKey);
2541
- const currentApiKey = normalizeOptionalString(provider.apiKey);
2542
- const apiKey = (hasApiKeyPatch ? providedApiKey : currentApiKey) ?? normalizeOptionalString(spec?.anonymousApiKey);
2543
- const hasApiBasePatch = Object.prototype.hasOwnProperty.call(patch, "apiBase");
2544
- const patchedApiBase = normalizeOptionalString(patch.apiBase);
2545
- const currentApiBase = normalizeOptionalString(provider.apiBase);
2546
- const apiBase = hasApiBasePatch ? patchedApiBase ?? spec?.defaultApiBase ?? null : currentApiBase ?? spec?.defaultApiBase ?? null;
2547
- const extraHeaders = Object.prototype.hasOwnProperty.call(patch, "extraHeaders") ? normalizeHeaders(patch.extraHeaders ?? null) : normalizeHeaders(provider.extraHeaders ?? null);
2548
- const wireApi = spec?.supportsWireApi || !spec ? patch.wireApi ?? provider.wireApi ?? spec?.defaultWireApi ?? "auto" : null;
2549
- if (!apiKey && !spec?.isLocal) return {
2550
- success: false,
2551
- provider: providerId,
2552
- latencyMs: 0,
2553
- message: "API key is required before testing the connection."
2614
+ updateConfigSearch = async (c) => {
2615
+ const body = await readJson(c.req.raw);
2616
+ if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
2617
+ const result = updateSearch(this.options.configPath, body.data);
2618
+ await this.publishConfigUpdates(["search"]);
2619
+ return c.json(ok(result));
2620
+ };
2621
+ updateProvider = async (c) => {
2622
+ const providerId = c.req.param("providerId");
2623
+ const body = await readJson(c.req.raw);
2624
+ if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
2625
+ const result = updateProvider(this.options.configPath, providerId, body.data);
2626
+ if (!result) return c.json(err("NOT_FOUND", `unknown provider: ${providerId}`), 404);
2627
+ await this.publishConfigUpdates([`providers.${providerId}`]);
2628
+ return c.json(ok(result));
2629
+ };
2630
+ createProvider = async (c) => {
2631
+ const body = await readJson(c.req.raw);
2632
+ if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
2633
+ const result = createProvider(this.options.configPath, body.data);
2634
+ if (!result) return c.json(err("PROVIDER_EXISTS", "provider already exists"), 409);
2635
+ await this.publishConfigUpdates([`providers.${result.providerId}`]);
2636
+ return c.json(ok({
2637
+ providerId: result.providerId,
2638
+ provider: result.provider
2639
+ }));
2640
+ };
2641
+ deleteProvider = async (c) => {
2642
+ const providerId = c.req.param("providerId");
2643
+ if (deleteProvider(this.options.configPath, providerId) === null) return c.json(err("NOT_FOUND", `provider not found: ${providerId}`), 404);
2644
+ await this.publishConfigUpdates([`providers.${providerId}`]);
2645
+ return c.json(ok({
2646
+ deleted: true,
2647
+ providerId
2648
+ }));
2649
+ };
2650
+ testProviderConnection = async (c) => {
2651
+ const provider = c.req.param("providerId");
2652
+ const body = await readJson(c.req.raw);
2653
+ if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
2654
+ const result = await this.providerConnectivity.testConnection(provider, body.data);
2655
+ if (!result) return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
2656
+ return c.json(ok(result));
2657
+ };
2658
+ discoverProviderModels = async (c) => {
2659
+ const provider = c.req.param("providerId");
2660
+ const body = await readJson(c.req.raw);
2661
+ if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
2662
+ try {
2663
+ const result = await this.providerConnectivity.discoverModels(provider, body.data);
2664
+ if (!result) return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
2665
+ return c.json(ok(result));
2666
+ } catch (error) {
2667
+ const message = error instanceof Error ? error.message : String(error);
2668
+ const details = error instanceof ProviderModelDiscoveryHttpError ? { upstreamStatus: error.upstreamStatus } : void 0;
2669
+ return c.json(err("PROVIDER_MODEL_DISCOVERY_FAILED", message, details), 502);
2670
+ }
2671
+ };
2672
+ startProviderAuth = async (c) => {
2673
+ const provider = c.req.param("providerId");
2674
+ let payload = {};
2675
+ const rawBody = await c.req.raw.text();
2676
+ if (rawBody.trim().length > 0) try {
2677
+ payload = JSON.parse(rawBody);
2678
+ } catch {
2679
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2680
+ }
2681
+ const methodId = typeof payload.methodId === "string" ? payload.methodId.trim() : void 0;
2682
+ try {
2683
+ const result = await startProviderAuth(this.options.configPath, provider, { methodId });
2684
+ if (!result) return c.json(err("NOT_SUPPORTED", `provider auth is not supported: ${provider}`), 404);
2685
+ return c.json(ok(result));
2686
+ } catch (error) {
2687
+ const message = error instanceof Error ? error.message : String(error);
2688
+ return c.json(err("AUTH_START_FAILED", message), 400);
2689
+ }
2690
+ };
2691
+ pollProviderAuth = async (c) => {
2692
+ const provider = c.req.param("providerId");
2693
+ const body = await readJson(c.req.raw);
2694
+ if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
2695
+ const sessionId = typeof body.data.sessionId === "string" ? body.data.sessionId.trim() : "";
2696
+ if (!sessionId) return c.json(err("INVALID_BODY", "sessionId is required"), 400);
2697
+ const result = await pollProviderAuth({
2698
+ configPath: this.options.configPath,
2699
+ providerName: provider,
2700
+ sessionId
2701
+ });
2702
+ if (!result) return c.json(err("NOT_FOUND", "provider auth session not found"), 404);
2703
+ if (result.status === "authorized") await this.publishConfigUpdates([`providers.${provider}`]);
2704
+ return c.json(ok(result));
2554
2705
  };
2555
- const model = resolveTestModel(config, providerId, normalizeOptionalString(patch.model), provider, spec ?? void 0);
2556
- if (!model) return {
2557
- success: false,
2558
- provider: providerId,
2559
- latencyMs: 0,
2560
- message: "No test model found. Configure provider models or set a default model for this provider, then try again."
2706
+ importProviderAuthFromCli = async (c) => {
2707
+ const provider = c.req.param("providerId");
2708
+ try {
2709
+ const result = await importProviderAuthFromCli(this.options.configPath, provider);
2710
+ if (!result) return c.json(err("NOT_SUPPORTED", `provider cli auth import is not supported: ${provider}`), 404);
2711
+ await this.publishConfigUpdates([`providers.${provider}`]);
2712
+ return c.json(ok(result));
2713
+ } catch (error) {
2714
+ const message = error instanceof Error ? error.message : String(error);
2715
+ return c.json(err("AUTH_IMPORT_FAILED", message), 400);
2716
+ }
2561
2717
  };
2562
- const startedAtMs = Date.now();
2563
- if (!providerManager) return {
2564
- success: false,
2565
- provider: providerId,
2566
- model,
2567
- latencyMs: Date.now() - startedAtMs,
2568
- message: "Provider manager is unavailable."
2718
+ updateChannel = async (c) => {
2719
+ const channel = c.req.param("channel");
2720
+ const body = await readJson(c.req.raw);
2721
+ if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
2722
+ const result = updateChannel(this.options.configPath, channel, body.data, this.getExtensionConfigProjectionOptions());
2723
+ if (!result) return c.json(err("NOT_FOUND", `unknown channel: ${channel}`), 404);
2724
+ this.publishConfigUpdatedPaths([`channels.${channel}`]);
2725
+ this.enqueueChannelConfigApply(channel);
2726
+ return c.json(ok(result));
2569
2727
  };
2570
- try {
2571
- await providerManager.testConnection({
2572
- providerName: providerType,
2573
- apiKey,
2574
- apiBase,
2575
- defaultModel: model,
2576
- extraHeaders,
2577
- wireApi,
2578
- messages: [{
2579
- role: "user",
2580
- content: "ping"
2581
- }],
2582
- maxTokens: PROVIDER_TEST_MAX_TOKENS
2583
- });
2584
- return {
2585
- success: true,
2586
- provider: providerId,
2587
- model,
2588
- latencyMs: Date.now() - startedAtMs,
2589
- message: "Connection test passed."
2590
- };
2591
- } catch (error) {
2592
- return {
2593
- success: false,
2594
- provider: providerId,
2595
- model,
2596
- latencyMs: Date.now() - startedAtMs,
2597
- message: stringifyError(error) || "Connection test failed."
2598
- };
2599
- }
2600
- }
2601
- function updateChannel(configPath, channelName, patch, options) {
2602
- const config = loadConfigOrDefault(configPath);
2603
- const normalizedOptions = normalizeExtensionProjectionOptions(options);
2604
- const channel = getProjectedChannelConfig(config, channelName, normalizedOptions);
2605
- if (!channel) return null;
2606
- for (const key of Object.keys(patch)) {
2607
- const path = `channels.${channelName}.${key}`;
2608
- if (isSensitivePath(path)) config.secrets.refs = clearSecretRef(config.secrets.refs, path);
2609
- }
2610
- const mergedChannel = {
2611
- ...channel,
2612
- ...patch
2728
+ startChannelAuth = async (c) => {
2729
+ const channel = c.req.param("channel");
2730
+ let payload = {};
2731
+ const rawBody = await c.req.raw.text();
2732
+ if (rawBody.trim().length > 0) try {
2733
+ payload = JSON.parse(rawBody);
2734
+ } catch {
2735
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2736
+ }
2737
+ try {
2738
+ const result = await startChannelAuth({
2739
+ configPath: this.options.configPath,
2740
+ channelId: channel,
2741
+ request: {
2742
+ accountId: typeof payload.accountId === "string" ? payload.accountId : void 0,
2743
+ baseUrl: typeof payload.baseUrl === "string" ? payload.baseUrl : void 0,
2744
+ domain: typeof payload.domain === "string" ? payload.domain : void 0
2745
+ },
2746
+ bindings: this.options.extensions?.getChannelBindings() ?? []
2747
+ });
2748
+ if (!result) return c.json(err("NOT_SUPPORTED", `channel auth is not supported: ${channel}`), 404);
2749
+ return c.json(ok(result));
2750
+ } catch (error) {
2751
+ const message = error instanceof Error ? error.message : String(error);
2752
+ return c.json(err("AUTH_START_FAILED", message), 400);
2753
+ }
2613
2754
  };
2614
- const mergedExtensionConfig = mergeProjectedExtensionChannelConfig(config, channelName, mergedChannel, normalizedOptions);
2615
- if (mergedExtensionConfig) {
2616
- const next = ConfigSchema.parse(mergedExtensionConfig);
2617
- saveConfig(next, configPath);
2618
- return sanitizePublicConfigValue(getProjectedChannelConfig(next, channelName, normalizedOptions) ?? {}, `channels.${channelName}`, buildUiHints(next, normalizedOptions));
2619
- }
2620
- config.channels[channelName] = mergedChannel;
2621
- const next = ConfigSchema.parse(config);
2622
- saveConfig(next, configPath);
2623
- return sanitizePublicConfigValue(getProjectedChannelConfig(next, channelName, normalizedOptions) ?? {}, `channels.${channelName}`, buildUiHints(next, normalizedOptions));
2624
- }
2625
- function applyRuntimeAgentDefaultsPatch(defaults, defaultsPatch) {
2626
- if (!defaultsPatch) return defaults;
2627
- let next = defaults;
2628
- if (Object.prototype.hasOwnProperty.call(defaultsPatch, "contextTokens")) {
2629
- const nextContextTokens = defaultsPatch.contextTokens;
2630
- if (typeof nextContextTokens === "number" && Number.isFinite(nextContextTokens)) next = {
2631
- ...next,
2632
- contextTokens: Math.max(1e3, Math.trunc(nextContextTokens))
2633
- };
2634
- }
2635
- if (Object.prototype.hasOwnProperty.call(defaultsPatch, "engine")) next = {
2636
- ...next,
2637
- engine: normalizeOptionalString(defaultsPatch.engine) ?? "native"
2755
+ pollChannelAuth = async (c) => {
2756
+ const channel = c.req.param("channel");
2757
+ const body = await readJson(c.req.raw);
2758
+ if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
2759
+ const sessionId = typeof body.data.sessionId === "string" ? body.data.sessionId.trim() : "";
2760
+ if (!sessionId) return c.json(err("INVALID_BODY", "sessionId is required"), 400);
2761
+ const result = await pollChannelAuth({
2762
+ configPath: this.options.configPath,
2763
+ channelId: channel,
2764
+ sessionId,
2765
+ bindings: this.options.extensions?.getChannelBindings() ?? []
2766
+ });
2767
+ if (!result) return c.json(err("NOT_FOUND", "channel auth session not found"), 404);
2768
+ if (result.status === "authorized") await this.publishConfigUpdates([`channels.${channel}`]);
2769
+ return c.json(ok(result));
2638
2770
  };
2639
- if (Object.prototype.hasOwnProperty.call(defaultsPatch, "engineConfig")) {
2640
- const nextEngineConfig = defaultsPatch.engineConfig;
2641
- if (nextEngineConfig && typeof nextEngineConfig === "object" && !Array.isArray(nextEngineConfig)) next = {
2642
- ...next,
2643
- engineConfig: { ...nextEngineConfig }
2644
- };
2645
- }
2646
- return next;
2647
- }
2648
- function updateRuntime(configPath, patch) {
2649
- const config = loadConfigOrDefault(configPath);
2650
- if (patch.companion && Object.prototype.hasOwnProperty.call(patch.companion, "enabled")) config.companion.enabled = Boolean(patch.companion.enabled);
2651
- config.agents.defaults = applyRuntimeAgentDefaultsPatch(config.agents.defaults, patch.agents?.defaults);
2652
- if (patch.agents && Object.prototype.hasOwnProperty.call(patch.agents, "list")) config.agents.list = (patch.agents.list ?? []).map((entry) => {
2653
- const normalizedEngine = normalizeOptionalString(entry.engine);
2654
- const hasEngineConfig = entry.engineConfig && typeof entry.engineConfig === "object" && !Array.isArray(entry.engineConfig);
2655
- return {
2656
- ...entry,
2657
- default: Boolean(entry.default),
2658
- ...normalizedEngine ? { engine: normalizedEngine } : {},
2659
- ...hasEngineConfig ? { engineConfig: { ...entry.engineConfig } } : {}
2660
- };
2661
- });
2662
- if (patch.agents?.runtimes && Object.prototype.hasOwnProperty.call(patch.agents.runtimes, "entries")) config.agents.runtimes.entries = normalizeRuntimeEntries(patch.agents.runtimes.entries);
2663
- if (Object.prototype.hasOwnProperty.call(patch, "bindings")) config.bindings = patch.bindings ?? [];
2664
- if (patch.session) config.session = {
2665
- ...config.session,
2666
- ...patch.session
2771
+ connectChannelAuth = async (c) => {
2772
+ const channel = c.req.param("channel");
2773
+ const body = await readJson(c.req.raw);
2774
+ if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
2775
+ const fields = body.data.fields && typeof body.data.fields === "object" && !Array.isArray(body.data.fields) ? body.data.fields : {};
2776
+ try {
2777
+ const result = await connectChannelAuth({
2778
+ configPath: this.options.configPath,
2779
+ channelId: channel,
2780
+ request: {
2781
+ accountId: typeof body.data.accountId === "string" ? body.data.accountId : void 0,
2782
+ domain: typeof body.data.domain === "string" ? body.data.domain : void 0,
2783
+ fields
2784
+ },
2785
+ bindings: this.options.extensions?.getChannelBindings() ?? []
2786
+ });
2787
+ if (!result) return c.json(err("NOT_SUPPORTED", `channel auth connect is not supported: ${channel}`), 404);
2788
+ if (result.status === "authorized") await this.publishConfigUpdates([`channels.${channel}`]);
2789
+ return c.json(ok(result));
2790
+ } catch (error) {
2791
+ const message = error instanceof Error ? error.message : String(error);
2792
+ return c.json(err("AUTH_CONNECT_FAILED", message), 400);
2793
+ }
2667
2794
  };
2668
- const next = ConfigSchema.parse(config);
2669
- saveConfig(next, configPath);
2670
- const view = buildConfigView(next);
2671
- return {
2672
- companion: view.companion,
2673
- agents: view.agents,
2674
- bindings: view.bindings ?? [],
2675
- session: view.session ?? {}
2795
+ updateSecrets = async (c) => {
2796
+ const body = await readJson(c.req.raw);
2797
+ if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
2798
+ const result = updateSecrets(this.options.configPath, body.data);
2799
+ await this.publishConfigUpdates(["secrets"]);
2800
+ return c.json(ok(result));
2676
2801
  };
2677
- }
2678
- function updateSecrets(configPath, patch) {
2679
- const config = loadConfigOrDefault(configPath);
2680
- if (Object.prototype.hasOwnProperty.call(patch, "enabled")) config.secrets.enabled = Boolean(patch.enabled);
2681
- if (patch.defaults) {
2682
- const nextDefaults = { ...config.secrets.defaults };
2683
- for (const source of [
2684
- "env",
2685
- "file",
2686
- "exec"
2687
- ]) {
2688
- if (!Object.prototype.hasOwnProperty.call(patch.defaults, source)) continue;
2689
- const value = patch.defaults[source];
2690
- if (typeof value === "string" && value.trim()) nextDefaults[source] = value.trim();
2691
- else delete nextDefaults[source];
2802
+ updateRuntime = async (c) => {
2803
+ const body = await readJson(c.req.raw);
2804
+ if (!body.ok || !body.data || typeof body.data !== "object") return c.json(err("INVALID_BODY", "invalid json body"), 400);
2805
+ try {
2806
+ const contextTokens = body.data.agents?.defaults?.contextTokens;
2807
+ if (typeof contextTokens === "number") await this.options.kernel.agentContextWindowManager.assertDefaultCanSave(contextTokens);
2808
+ const result = updateRuntime(this.options.configPath, body.data);
2809
+ const changedPaths = [];
2810
+ if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "contextTokens")) changedPaths.push("agents.defaults.contextTokens");
2811
+ if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engine")) changedPaths.push("agents.defaults.engine");
2812
+ if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engineConfig")) changedPaths.push("agents.defaults.engineConfig");
2813
+ if (body.data.agents?.runtimes && Object.prototype.hasOwnProperty.call(body.data.agents.runtimes, "entries")) changedPaths.push("agents.runtimes.entries");
2814
+ if (body.data.companion && Object.prototype.hasOwnProperty.call(body.data.companion, "enabled")) changedPaths.push("companion.enabled");
2815
+ changedPaths.push("agents.list", "bindings", "session");
2816
+ await this.publishConfigUpdates(changedPaths);
2817
+ return c.json(ok(result));
2818
+ } catch (error) {
2819
+ return c.json(err("RUNTIME_CONFIG_UPDATE_FAILED", error instanceof Error ? error.message : String(error)), 400);
2692
2820
  }
2693
- config.secrets.defaults = nextDefaults;
2694
- }
2695
- if (Object.prototype.hasOwnProperty.call(patch, "providers")) config.secrets.providers = patch.providers ?? {};
2696
- if (Object.prototype.hasOwnProperty.call(patch, "refs")) config.secrets.refs = patch.refs ?? {};
2697
- const next = ConfigSchema.parse(config);
2698
- saveConfig(next, configPath);
2699
- return {
2700
- enabled: next.secrets.enabled,
2701
- defaults: { ...next.secrets.defaults },
2702
- providers: { ...next.secrets.providers },
2703
- refs: { ...next.secrets.refs }
2704
2821
  };
2705
- }
2822
+ executeAction = async (c) => {
2823
+ const actionId = c.req.param("actionId");
2824
+ const body = await readJson(c.req.raw);
2825
+ if (!body.ok) return c.json(err("INVALID_BODY", "invalid json body"), 400);
2826
+ const result = await executeConfigAction(this.options.configPath, actionId, body.data ?? {});
2827
+ if (!result.ok) return c.json(err(result.code, result.message, result.details), 400);
2828
+ return c.json(ok(result.data));
2829
+ };
2830
+ };
2706
2831
  //#endregion
2707
2832
  //#region src/features/cron/controllers/cron.controller.ts
2708
2833
  const CRON_LIST_MAX_LIMIT = 100;
@@ -2844,7 +2969,7 @@ var CronRoutesController = class {
2844
2969
  const allJobs = this.options.cron.listJobs(true).map((job) => buildCronJobView(job));
2845
2970
  const enabled = allJobs.filter((job) => job.enabled).length;
2846
2971
  const normalizedQuery = query.query?.trim().toLowerCase() ?? "";
2847
- const filteredJobs = allJobs.filter((job) => matchesCronListStatus(job, status) && matchesCronListQuery(job, normalizedQuery));
2972
+ const filteredJobs = allJobs.filter((job) => matchesCronListStatus(job, status) && matchesCronListQuery(job, normalizedQuery)).sort((a, b) => b.createdAt.localeCompare(a.createdAt) || a.id.localeCompare(b.id));
2848
2973
  const offset = offsetResult.value ?? 0;
2849
2974
  const jobs = limitResult.value === null ? filteredJobs : filteredJobs.slice(offset, offset + limitResult.value);
2850
2975
  return c.json(ok({
@@ -5612,6 +5737,14 @@ function readStreamPayload(url) {
5612
5737
  function isAbortPayload(value) {
5613
5738
  return isRecord(value) && typeof value.sessionId === "string" && value.sessionId.trim().length > 0;
5614
5739
  }
5740
+ function isContinuePayload(value) {
5741
+ return isRecord(value) && typeof value.sessionId === "string" && value.sessionId.trim().length > 0;
5742
+ }
5743
+ function isEditMessagePayload(value) {
5744
+ if (!isRecord(value) || typeof value.sessionId !== "string" || !value.sessionId.trim() || typeof value.messageId !== "string" || !value.messageId.trim() || !isRecord(value.message)) return false;
5745
+ const message = value.message;
5746
+ return typeof message.id === "string" && message.id.trim().length > 0 && message.role === "user" && Array.isArray(message.parts) && message.parts.length > 0;
5747
+ }
5615
5748
  var UiRouteRegistry = class {
5616
5749
  constructor(app, options, controllers) {
5617
5750
  this.app = app;
@@ -5645,6 +5778,24 @@ var UiRouteRegistry = class {
5645
5778
  }, { source: "ui-http" });
5646
5779
  return c.json(ok({ accepted: true }));
5647
5780
  });
5781
+ this.app.post(`${basePath}/edit-message`, async (c) => {
5782
+ const body = await readJson(c.req.raw);
5783
+ if (!body.ok || !isEditMessagePayload(body.data)) return c.json(err("INVALID_BODY", "A valid sessionId, messageId, and user message are required."), 400);
5784
+ const handle = await kernel.ingress.handle({
5785
+ type: ingressKeys.agentRun.editMessage,
5786
+ payload: body.data
5787
+ }, { source: "ui-http" });
5788
+ return c.json(ok(handle));
5789
+ });
5790
+ this.app.post(`${basePath}/continue`, async (c) => {
5791
+ const body = await readJson(c.req.raw);
5792
+ if (!body.ok || !isContinuePayload(body.data)) return c.json(err("INVALID_BODY", "sessionId is required."), 400);
5793
+ const handle = await kernel.ingress.handle({
5794
+ type: ingressKeys.agentRun.continue,
5795
+ payload: body.data
5796
+ }, { source: "ui-http" });
5797
+ return c.json(ok(handle));
5798
+ });
5648
5799
  };
5649
5800
  mountNcpAgentRoutes = (kernel, ncpAsset) => {
5650
5801
  this.mountAgentRunRoutes(NCP_AGENT_BASE_PATH, kernel);
@@ -6043,6 +6194,11 @@ var UiRouteRegistry = class {
6043
6194
  "/api/provider-templates",
6044
6195
  config.listProviderTemplates
6045
6196
  ],
6197
+ [
6198
+ "get",
6199
+ "/api/provider-model-catalog",
6200
+ config.listProviderModelCatalog
6201
+ ],
6046
6202
  [
6047
6203
  "post",
6048
6204
  "/api/providers",
@@ -6063,6 +6219,11 @@ var UiRouteRegistry = class {
6063
6219
  "/api/providers/:providerId/test",
6064
6220
  config.testProviderConnection
6065
6221
  ],
6222
+ [
6223
+ "post",
6224
+ "/api/providers/:providerId/models/discover",
6225
+ config.discoverProviderModels
6226
+ ],
6066
6227
  [
6067
6228
  "post",
6068
6229
  "/api/providers/:providerId/auth/start",
@@ -6481,6 +6642,6 @@ async function startUiServer(gateway) {
6481
6642
  };
6482
6643
  }
6483
6644
  //#endregion
6484
- export { ConfigRoutesController, InboxDeliveriesRoutesController, PanelAppsRoutesController, RuntimeControlRoutesController, ServiceAppsRoutesController, buildConfigMeta, buildConfigSchemaView, buildConfigView, buildProviderTemplatesView, buildProvidersView, createProvider, createUiRouter, deleteProvider, ensureUiBridgeSecret, executeConfigAction, getUiBridgeSecretPath, loadConfigOrDefault, readUiBridgeSecret, startUiServer, testProviderConnection, updateChannel, updateModel, updateProvider, updateRuntime, updateSearch, updateSecrets };
6645
+ export { ConfigRoutesController, InboxDeliveriesRoutesController, PanelAppsRoutesController, RuntimeControlRoutesController, ServiceAppsRoutesController, buildConfigMeta, buildConfigSchemaView, buildConfigView, buildProviderTemplatesView, buildProvidersView, createProvider, createUiRouter, deleteProvider, ensureUiBridgeSecret, executeConfigAction, getUiBridgeSecretPath, loadConfigOrDefault, readUiBridgeSecret, startUiServer, updateChannel, updateModel, updateProvider, updateRuntime, updateSearch, updateSecrets };
6485
6646
 
6486
6647
  //# sourceMappingURL=index.js.map