@nextclaw/server 0.15.20 → 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.d.ts +162 -50
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1957 -1794
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
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,1949 +851,1983 @@ function mergeProjectedExtensionChannelConfig(config, channelName, mergedChannel
|
|
|
758
851
|
});
|
|
759
852
|
}
|
|
760
853
|
//#endregion
|
|
761
|
-
//#region src/features/config/utils/
|
|
762
|
-
function
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
|
781
|
-
|
|
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
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
}
|
|
849
|
-
|
|
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/
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
|
931
|
-
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
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
|
|
943
|
-
|
|
944
|
-
return value;
|
|
964
|
+
function isSearchProviderName(value) {
|
|
965
|
+
return typeof value === "string" && SEARCH_PROVIDER_NAMES.some((providerName) => providerName === value);
|
|
945
966
|
}
|
|
946
|
-
|
|
947
|
-
|
|
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
|
|
950
|
-
const verifier = toBase64Url(randomBytes(48));
|
|
1013
|
+
function buildSearchView(config) {
|
|
951
1014
|
return {
|
|
952
|
-
|
|
953
|
-
|
|
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
|
|
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
|
-
|
|
965
|
-
|
|
1028
|
+
...config,
|
|
1029
|
+
search,
|
|
1030
|
+
secrets: refs === config.secrets.refs ? config.secrets : {
|
|
1031
|
+
...config.secrets,
|
|
1032
|
+
refs
|
|
1033
|
+
}
|
|
966
1034
|
};
|
|
967
1035
|
}
|
|
968
|
-
function
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
|
975
|
-
|
|
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
|
|
978
|
-
if (
|
|
979
|
-
const
|
|
980
|
-
|
|
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
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
if (
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
if (
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
|
1034
|
-
if (!
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
if (
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
|
1229
|
+
return name;
|
|
1065
1230
|
}
|
|
1066
|
-
function
|
|
1067
|
-
|
|
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
|
|
1074
|
-
const config = loadConfig(configPath);
|
|
1234
|
+
function findNextCustomProviderName(config) {
|
|
1075
1235
|
const providers = config.providers;
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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;
|
|
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;
|
|
1172
1259
|
}
|
|
1173
|
-
|
|
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
|
-
};
|
|
1260
|
+
return providerId;
|
|
1205
1261
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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
|
-
};
|
|
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;
|
|
1314
1292
|
}
|
|
1315
|
-
|
|
1316
|
-
if (!accessToken) return {
|
|
1317
|
-
provider: providerId,
|
|
1318
|
-
status: "error",
|
|
1319
|
-
message: "provider token response missing access token"
|
|
1320
|
-
};
|
|
1293
|
+
if (match) return hint;
|
|
1321
1294
|
}
|
|
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
1295
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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}`);
|
|
1296
|
+
function isSensitivePath(path, hints) {
|
|
1297
|
+
if (hints) {
|
|
1298
|
+
const hint = matchHint(path, hints);
|
|
1299
|
+
if (hint?.sensitive !== void 0) return Boolean(hint.sensitive);
|
|
1356
1300
|
}
|
|
1357
|
-
|
|
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
|
-
};
|
|
1301
|
+
return isSensitiveConfigPath(path) || matchesExtraSensitivePath(path);
|
|
1373
1302
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
constructor(options) {
|
|
1379
|
-
this.options = options;
|
|
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));
|
|
1380
1307
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
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;
|
|
1316
|
+
}
|
|
1317
|
+
function isObject(value) {
|
|
1318
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1319
|
+
}
|
|
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;
|
|
1328
|
+
}
|
|
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;
|
|
1338
|
+
}
|
|
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;
|
|
1350
|
+
}
|
|
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: []
|
|
1393
1379
|
};
|
|
1394
|
-
|
|
1395
|
-
|
|
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: []
|
|
1396
1390
|
};
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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);
|
|
1391
|
+
const responseData = {
|
|
1392
|
+
appId: result.appId,
|
|
1393
|
+
botName: result.botName ?? null,
|
|
1394
|
+
botOpenId: result.botOpenId ?? null
|
|
1425
1395
|
};
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
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
|
+
}
|
|
1401
|
+
return {
|
|
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: []
|
|
1429
1408
|
};
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1409
|
+
}
|
|
1410
|
+
const ACTION_HANDLERS = { "channels.feishu.verifyConnection": runFeishuVerifyAction };
|
|
1411
|
+
function buildUiHints(config, options) {
|
|
1412
|
+
return buildConfigSchemaView(config, options).uiHints;
|
|
1413
|
+
}
|
|
1414
|
+
function maskApiKey(value) {
|
|
1415
|
+
if (!value) return { apiKeySet: false };
|
|
1416
|
+
if (value.length < MASK_MIN_LENGTH) return {
|
|
1417
|
+
apiKeySet: true,
|
|
1418
|
+
apiKeyMasked: "****"
|
|
1433
1419
|
};
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1420
|
+
return {
|
|
1421
|
+
apiKeySet: true,
|
|
1422
|
+
apiKeyMasked: `${value.slice(0, 2)}****${value.slice(-4)}`
|
|
1437
1423
|
};
|
|
1438
|
-
|
|
1439
|
-
|
|
1424
|
+
}
|
|
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
|
|
1440
1457
|
};
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
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
|
+
}
|
|
1444
1484
|
};
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
const
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
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 } : {}
|
|
1478
1522
|
};
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
}));
|
|
1523
|
+
}
|
|
1524
|
+
function clearSecretRef(refs, path) {
|
|
1525
|
+
const { [path]: _removed, ...nextRefs } = refs;
|
|
1526
|
+
return nextRefs;
|
|
1527
|
+
}
|
|
1528
|
+
function buildConfigMeta(config, options) {
|
|
1529
|
+
return {
|
|
1530
|
+
search: SEARCH_PROVIDER_META,
|
|
1531
|
+
channels: buildProjectedChannelMeta(config, options)
|
|
1489
1532
|
};
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1533
|
+
}
|
|
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) ?? ""));
|
|
1584
|
+
}
|
|
1585
|
+
return { providers };
|
|
1586
|
+
}
|
|
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;
|
|
1591
|
+
return {
|
|
1592
|
+
...base,
|
|
1593
|
+
uiHints: {
|
|
1594
|
+
...base.uiHints,
|
|
1595
|
+
...extensionUiHints
|
|
1596
|
+
}
|
|
1498
1597
|
};
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1598
|
+
}
|
|
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}`
|
|
1506
1606
|
};
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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);
|
|
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
|
|
1524
1614
|
}
|
|
1525
1615
|
};
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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));
|
|
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 }
|
|
1540
1622
|
};
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
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
|
-
}
|
|
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}`
|
|
1552
1628
|
};
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
this.publishConfigUpdatedPaths([`channels.${channel}`]);
|
|
1560
|
-
this.enqueueChannelConfigApply(channel);
|
|
1561
|
-
return c.json(ok(result));
|
|
1629
|
+
return {
|
|
1630
|
+
ok: true,
|
|
1631
|
+
data: await handler({
|
|
1632
|
+
config: runtimeConfig,
|
|
1633
|
+
action
|
|
1634
|
+
})
|
|
1562
1635
|
};
|
|
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);
|
|
1628
|
-
}
|
|
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
|
-
};
|
|
1674
|
-
}
|
|
1675
|
-
function createDefaultProviderConfigFromSpec(spec) {
|
|
1676
|
-
return createDefaultProviderConfig(spec?.defaultWireApi ?? "auto", spec?.defaultModels ?? [], normalizeProviderModelConfig(spec?.modelConfig ?? {}), spec?.name, spec?.defaultApiBase ?? null);
|
|
1677
1636
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
function normalizeOptionalString$2(value) {
|
|
1681
|
-
if (typeof value !== "string") return null;
|
|
1682
|
-
const trimmed = value.trim();
|
|
1683
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
1684
|
-
}
|
|
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;
|
|
1637
|
+
function loadConfigOrDefault(configPath) {
|
|
1638
|
+
return loadConfig(configPath);
|
|
1690
1639
|
}
|
|
1691
|
-
function
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
return
|
|
1640
|
+
function createProviderForUpdate(providerId) {
|
|
1641
|
+
const spec = findServerBuiltinProviderByName(providerId);
|
|
1642
|
+
if (!spec) return null;
|
|
1643
|
+
return createDefaultProviderConfigFromSpec(spec);
|
|
1695
1644
|
}
|
|
1696
|
-
function
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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);
|
|
1700
1652
|
}
|
|
1701
|
-
function
|
|
1702
|
-
|
|
1703
|
-
|
|
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;
|
|
1663
|
+
if (Object.prototype.hasOwnProperty.call(patch, "apiKey")) {
|
|
1664
|
+
provider.apiKey = patch.apiKey ?? "";
|
|
1665
|
+
config.secrets.refs = clearSecretRef(config.secrets.refs, `providers.${providerId}.apiKey`);
|
|
1666
|
+
}
|
|
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);
|
|
1704
1677
|
}
|
|
1705
|
-
function
|
|
1706
|
-
const
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
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 ?? {})
|
|
1710
1699
|
};
|
|
1711
|
-
const
|
|
1712
|
-
|
|
1713
|
-
const
|
|
1714
|
-
const
|
|
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);
|
|
1700
|
+
const next = ConfigSchema.parse(config);
|
|
1701
|
+
saveConfig(next, configPath);
|
|
1702
|
+
const uiHints = buildUiHints(next);
|
|
1703
|
+
const created = next.providers[providerId];
|
|
1720
1704
|
return {
|
|
1721
|
-
|
|
1722
|
-
|
|
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
|
|
1705
|
+
providerId,
|
|
1706
|
+
provider: toProviderView(next, created, providerId, uiHints, spec)
|
|
1735
1707
|
};
|
|
1736
1708
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
const
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
function normalizeOptionalString$1(value) {
|
|
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) {
|
|
1748
1719
|
if (typeof value !== "string") return null;
|
|
1749
1720
|
const trimmed = value.trim();
|
|
1750
1721
|
return trimmed.length > 0 ? trimmed : null;
|
|
1751
1722
|
}
|
|
1752
|
-
function
|
|
1753
|
-
if (!
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
};
|
|
1758
|
-
return {
|
|
1759
|
-
apiKeySet: true,
|
|
1760
|
-
apiKeyMasked: `${value.slice(0, 2)}****${value.slice(-4)}`
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
function clearSecretRef$1(refs, path) {
|
|
1764
|
-
if (!refs[path]) return refs;
|
|
1765
|
-
const nextRefs = { ...refs };
|
|
1766
|
-
delete nextRefs[path];
|
|
1767
|
-
return nextRefs;
|
|
1768
|
-
}
|
|
1769
|
-
function isSearchProviderName(value) {
|
|
1770
|
-
return typeof value === "string" && SEARCH_PROVIDER_NAMES.some((providerName) => providerName === value);
|
|
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);
|
|
1771
1728
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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
|
|
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);
|
|
1793
1737
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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
|
|
1738
|
+
const mergedChannel = {
|
|
1739
|
+
...channel,
|
|
1740
|
+
...patch
|
|
1803
1741
|
};
|
|
1804
|
-
|
|
1805
|
-
if (
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
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));
|
|
1747
|
+
}
|
|
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));
|
|
1810
1752
|
}
|
|
1811
|
-
function
|
|
1812
|
-
return
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
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)
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
if (Object.prototype.hasOwnProperty.call(defaultsPatch, "engine")) next = {
|
|
1764
|
+
...next,
|
|
1765
|
+
engine: normalizeOptionalString(defaultsPatch.engine) ?? "native"
|
|
1821
1766
|
};
|
|
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 }
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
return next;
|
|
1822
1775
|
}
|
|
1823
|
-
function
|
|
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 } } : {}
|
|
1788
|
+
};
|
|
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);
|
|
1824
1799
|
return {
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
refs
|
|
1830
|
-
}
|
|
1800
|
+
companion: view.companion,
|
|
1801
|
+
agents: view.agents,
|
|
1802
|
+
bindings: view.bindings ?? [],
|
|
1803
|
+
session: view.session ?? {}
|
|
1831
1804
|
};
|
|
1832
1805
|
}
|
|
1833
|
-
function
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
}
|
|
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)))
|
|
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];
|
|
1856
1820
|
}
|
|
1857
|
-
|
|
1858
|
-
return config;
|
|
1859
|
-
}
|
|
1860
|
-
function applyBochaSearchPatch(config, patch) {
|
|
1861
|
-
if (!patch) return config;
|
|
1862
|
-
let nextRefs = config.secrets.refs;
|
|
1863
|
-
let nextProvider = config.search.providers.bocha;
|
|
1864
|
-
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");
|
|
1821
|
+
config.secrets.defaults = nextDefaults;
|
|
1870
1822
|
}
|
|
1871
|
-
if (Object.prototype.hasOwnProperty.call(patch, "
|
|
1872
|
-
|
|
1873
|
-
|
|
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);
|
|
1826
|
+
saveConfig(next, configPath);
|
|
1827
|
+
return {
|
|
1828
|
+
enabled: next.secrets.enabled,
|
|
1829
|
+
defaults: { ...next.secrets.defaults },
|
|
1830
|
+
providers: { ...next.secrets.providers },
|
|
1831
|
+
refs: { ...next.secrets.refs }
|
|
1874
1832
|
};
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1833
|
+
}
|
|
1834
|
+
//#endregion
|
|
1835
|
+
//#region src/features/config/services/provider-connectivity.service.ts
|
|
1836
|
+
const PROVIDER_TEST_MAX_TOKENS = 16;
|
|
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];
|
|
1878
1859
|
};
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1860
|
+
resolveProviderType = (providerId, provider) => {
|
|
1861
|
+
const configuredType = this.normalizeOptionalString(provider?.providerType);
|
|
1862
|
+
if (configuredType && findServerBuiltinProviderByName(configuredType)) return configuredType;
|
|
1863
|
+
return findServerBuiltinProviderByName(providerId) ? providerId : null;
|
|
1882
1864
|
};
|
|
1883
|
-
|
|
1884
|
-
const
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
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
|
|
1888
1875
|
};
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
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);
|
|
1895
1900
|
}
|
|
1896
|
-
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
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."
|
|
1906
1921
|
};
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
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
|
+
}
|
|
1912
1968
|
};
|
|
1913
|
-
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
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()
|
|
1918
1985
|
};
|
|
1919
|
-
}
|
|
1920
|
-
if (Object.prototype.hasOwnProperty.call(patch, "includeAnswer")) nextProvider = {
|
|
1921
|
-
...nextProvider,
|
|
1922
|
-
includeAnswer: Boolean(patch.includeAnswer)
|
|
1923
1986
|
};
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
}, nextRefs);
|
|
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));
|
|
1931
1993
|
}
|
|
1932
|
-
function
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
...nextProvider,
|
|
1945
|
-
baseUrl: normalizeOptionalString$1(patch.baseUrl) ?? "https://api.search.brave.com/res/v1/web/search"
|
|
1994
|
+
function findExtensionChannelBinding(bindings, channelId) {
|
|
1995
|
+
const normalizedChannelId = channelId.trim().toLowerCase();
|
|
1996
|
+
return bindings.find((binding) => binding.channelId.trim().toLowerCase() === normalizedChannelId) ?? null;
|
|
1997
|
+
}
|
|
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
|
|
1946
2006
|
};
|
|
1947
|
-
return replaceSearchConfig(config, {
|
|
1948
|
-
...config.search,
|
|
1949
|
-
providers: {
|
|
1950
|
-
...config.search.providers,
|
|
1951
|
-
brave: nextProvider
|
|
1952
|
-
}
|
|
1953
|
-
}, nextRefs);
|
|
1954
2007
|
}
|
|
1955
|
-
function
|
|
1956
|
-
const
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
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);
|
|
1960
2019
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
const
|
|
1964
|
-
const
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
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-";
|
|
1987
|
-
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;
|
|
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
|
+
});
|
|
1992
2035
|
}
|
|
1993
|
-
function
|
|
1994
|
-
|
|
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);
|
|
1995
2056
|
}
|
|
1996
|
-
function
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
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);
|
|
2002
2078
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
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);
|
|
2005
2087
|
}
|
|
2006
|
-
function
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
while (providers[`${CUSTOM_PROVIDER_PREFIX}${index}`]) index += 1;
|
|
2010
|
-
return `${CUSTOM_PROVIDER_PREFIX}${index}`;
|
|
2088
|
+
function normalizePositiveFloat(value) {
|
|
2089
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
|
|
2090
|
+
return value;
|
|
2011
2091
|
}
|
|
2012
|
-
function
|
|
2013
|
-
|
|
2014
|
-
const trimmed = value.trim();
|
|
2015
|
-
if (!trimmed || trimmed.includes("/")) return null;
|
|
2016
|
-
return trimmed;
|
|
2092
|
+
function toBase64Url(buffer) {
|
|
2093
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
2017
2094
|
}
|
|
2018
|
-
function
|
|
2019
|
-
const
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2095
|
+
function buildPkce() {
|
|
2096
|
+
const verifier = toBase64Url(randomBytes(48));
|
|
2097
|
+
return {
|
|
2098
|
+
verifier,
|
|
2099
|
+
challenge: toBase64Url(createHash("sha256").update(verifier).digest())
|
|
2100
|
+
};
|
|
2023
2101
|
}
|
|
2024
|
-
function
|
|
2025
|
-
|
|
2026
|
-
let providerId = baseProviderId;
|
|
2027
|
-
let index = 2;
|
|
2028
|
-
while (providers[providerId]) {
|
|
2029
|
-
providerId = `${baseProviderId}-${index}`;
|
|
2030
|
-
index += 1;
|
|
2031
|
-
}
|
|
2032
|
-
return providerId;
|
|
2102
|
+
function withTrailingSlash(value) {
|
|
2103
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
2033
2104
|
}
|
|
2034
|
-
function
|
|
2035
|
-
if (
|
|
2036
|
-
const suffix = providerId.slice(baseProviderId.length + 1).trim();
|
|
2037
|
-
return suffix ? ` ${suffix}` : "";
|
|
2105
|
+
function cleanupExpiredAuthSessions(now = Date.now()) {
|
|
2106
|
+
for (const [sessionId, session] of authSessions.entries()) if (session.expiresAtMs <= now) authSessions.delete(sessionId);
|
|
2038
2107
|
}
|
|
2039
|
-
function
|
|
2040
|
-
return
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
}).filter(Boolean);
|
|
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
|
+
};
|
|
2045
2113
|
}
|
|
2046
|
-
function
|
|
2047
|
-
return
|
|
2114
|
+
function resolveAuthNote(params) {
|
|
2115
|
+
return params.zh ?? params.en;
|
|
2048
2116
|
}
|
|
2049
|
-
function
|
|
2050
|
-
return
|
|
2117
|
+
function resolveLocalizedMethodLabel(method, fallbackId) {
|
|
2118
|
+
return method.label?.zh ?? method.label?.en ?? fallbackId;
|
|
2051
2119
|
}
|
|
2052
|
-
function
|
|
2053
|
-
|
|
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
|
-
}
|
|
2120
|
+
function resolveLocalizedMethodHint(method) {
|
|
2121
|
+
return method.hint?.zh ?? method.hint?.en;
|
|
2067
2122
|
}
|
|
2068
|
-
function
|
|
2069
|
-
if (
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
}
|
|
2073
|
-
return isSensitiveConfigPath(path) || matchesExtraSensitivePath(path);
|
|
2123
|
+
function normalizeMethodId(value) {
|
|
2124
|
+
if (typeof value !== "string") return;
|
|
2125
|
+
const trimmed = value.trim();
|
|
2126
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
2074
2127
|
}
|
|
2075
|
-
function
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
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
|
+
};
|
|
2086
2144
|
}
|
|
2087
|
-
|
|
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
|
+
};
|
|
2088
2165
|
}
|
|
2089
|
-
function
|
|
2090
|
-
|
|
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);
|
|
2091
2172
|
}
|
|
2092
|
-
function
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
result[key] = deepMerge(previous, value);
|
|
2098
|
-
}
|
|
2099
|
-
return result;
|
|
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);
|
|
2100
2178
|
}
|
|
2101
|
-
function
|
|
2102
|
-
if (!
|
|
2103
|
-
const
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
return current;
|
|
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;
|
|
2110
2187
|
}
|
|
2111
|
-
function
|
|
2112
|
-
const
|
|
2113
|
-
if (
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
const segment = segments[index];
|
|
2117
|
-
const next = current[segment];
|
|
2118
|
-
if (!isObject(next)) current[segment] = {};
|
|
2119
|
-
current = current[segment];
|
|
2120
|
-
}
|
|
2121
|
-
current[segments[segments.length - 1]] = value;
|
|
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";
|
|
2122
2193
|
}
|
|
2123
|
-
function
|
|
2124
|
-
|
|
2125
|
-
if (
|
|
2126
|
-
if (
|
|
2127
|
-
return
|
|
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);
|
|
2128
2201
|
}
|
|
2129
|
-
function
|
|
2130
|
-
if (
|
|
2131
|
-
|
|
2132
|
-
|
|
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;
|
|
2209
|
+
}
|
|
2210
|
+
return null;
|
|
2133
2211
|
}
|
|
2134
|
-
function
|
|
2135
|
-
|
|
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;
|
|
2136
2218
|
}
|
|
2137
|
-
function
|
|
2138
|
-
const
|
|
2139
|
-
|
|
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);
|
|
2140
2227
|
}
|
|
2141
|
-
|
|
2142
|
-
const
|
|
2143
|
-
|
|
2144
|
-
const
|
|
2145
|
-
if (
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
data: { error: "missing credentials (appId, appSecret)" },
|
|
2150
|
-
nextActions: []
|
|
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
|
|
2151
2236
|
};
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
message: `${messageOrDefault(action, "failure", "Verification failed")}: ${result.error}`,
|
|
2157
|
-
data: {
|
|
2158
|
-
error: result.error,
|
|
2159
|
-
appId: result.appId ?? appId
|
|
2160
|
-
},
|
|
2161
|
-
nextActions: []
|
|
2237
|
+
if (findServerBuiltinProviderByName(providerId)) return {
|
|
2238
|
+
providerId,
|
|
2239
|
+
providerType: providerId,
|
|
2240
|
+
provider
|
|
2162
2241
|
};
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2242
|
+
return null;
|
|
2243
|
+
}
|
|
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;
|
|
2318
|
+
}
|
|
2319
|
+
const sessionId = randomUUID();
|
|
2320
|
+
authSessions.set(sessionId, {
|
|
2321
|
+
sessionId,
|
|
2322
|
+
providerId,
|
|
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 ?? {})
|
|
2167
2350
|
};
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2351
|
+
}
|
|
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
|
+
};
|
|
2364
|
+
}
|
|
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 = {};
|
|
2387
|
+
}
|
|
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"
|
|
2466
|
+
};
|
|
2172
2467
|
}
|
|
2468
|
+
setProviderApiKey({
|
|
2469
|
+
configPath,
|
|
2470
|
+
provider: providerId,
|
|
2471
|
+
accessToken,
|
|
2472
|
+
defaultApiBase: session.defaultApiBase
|
|
2473
|
+
});
|
|
2474
|
+
authSessions.delete(sessionId);
|
|
2173
2475
|
return {
|
|
2174
|
-
|
|
2175
|
-
status: "
|
|
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
|
-
};
|
|
2181
|
-
}
|
|
2182
|
-
const ACTION_HANDLERS = { "channels.feishu.verifyConnection": runFeishuVerifyAction };
|
|
2183
|
-
function buildUiHints(config, options) {
|
|
2184
|
-
return buildConfigSchemaView(config, options).uiHints;
|
|
2185
|
-
}
|
|
2186
|
-
function maskApiKey(value) {
|
|
2187
|
-
if (!value) return { apiKeySet: false };
|
|
2188
|
-
if (value.length < MASK_MIN_LENGTH) return {
|
|
2189
|
-
apiKeySet: true,
|
|
2190
|
-
apiKeyMasked: "****"
|
|
2191
|
-
};
|
|
2192
|
-
return {
|
|
2193
|
-
apiKeySet: true,
|
|
2194
|
-
apiKeyMasked: `${value.slice(0, 2)}****${value.slice(-4)}`
|
|
2476
|
+
provider: providerId,
|
|
2477
|
+
status: "authorized"
|
|
2195
2478
|
};
|
|
2196
2479
|
}
|
|
2197
|
-
function
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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}`);
|
|
2205
2493
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
providerId,
|
|
2215
|
-
providerType,
|
|
2216
|
-
isBuiltInType: providerType !== null,
|
|
2217
|
-
isCustom: providerType === null,
|
|
2218
|
-
enabled: provider.enabled !== false,
|
|
2219
|
-
displayName: resolveProviderInstanceDisplayName(providerId, provider, spec),
|
|
2220
|
-
apiKeySet: masked.apiKeySet || apiKeyRefSet,
|
|
2221
|
-
apiKeyMasked: masked.apiKeyMasked ?? (apiKeyRefSet ? "****" : void 0),
|
|
2222
|
-
apiBase: provider.apiBase ?? null,
|
|
2223
|
-
extraHeaders: extraHeaders && Object.keys(extraHeaders).length > 0 ? extraHeaders : null,
|
|
2224
|
-
models: normalizeModelList(provider.models ?? []),
|
|
2225
|
-
modelConfig: normalizeProviderModelConfig(provider.modelConfig ?? {})
|
|
2226
|
-
};
|
|
2227
|
-
if (Boolean(spec?.supportsWireApi) || providerType === null) view.wireApi = provider.wireApi ?? spec?.defaultWireApi ?? "auto";
|
|
2228
|
-
return view;
|
|
2229
|
-
}
|
|
2230
|
-
function buildConfigView(config, options) {
|
|
2231
|
-
const uiHints = buildUiHints(config, options);
|
|
2232
|
-
const projectedChannels = getProjectedChannelMap(config, options);
|
|
2233
|
-
const providers = {};
|
|
2234
|
-
for (const [providerId, provider] of Object.entries(config.providers)) {
|
|
2235
|
-
const providerConfig = provider;
|
|
2236
|
-
providers[providerId] = toProviderView(config, providerConfig, providerId, uiHints, findServerBuiltinProviderByName(resolveProviderType(providerId, providerConfig) ?? ""));
|
|
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}`);
|
|
2237
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
|
+
});
|
|
2238
2513
|
return {
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
channels: sanitizePublicConfigValue(projectedChannels, "channels", uiHints),
|
|
2244
|
-
bindings: sanitizePublicConfigValue(config.bindings, "bindings", uiHints),
|
|
2245
|
-
session: sanitizePublicConfigValue(config.session, "session", uiHints),
|
|
2246
|
-
tools: sanitizePublicConfigValue(config.tools, "tools", uiHints),
|
|
2247
|
-
gateway: sanitizePublicConfigValue(config.gateway, "gateway", uiHints),
|
|
2248
|
-
ui: sanitizePublicConfigValue(config.ui, "ui", uiHints),
|
|
2249
|
-
secrets: {
|
|
2250
|
-
enabled: config.secrets.enabled,
|
|
2251
|
-
defaults: { ...config.secrets.defaults },
|
|
2252
|
-
providers: { ...config.secrets.providers },
|
|
2253
|
-
refs: { ...config.secrets.refs }
|
|
2254
|
-
}
|
|
2514
|
+
provider: providerId,
|
|
2515
|
+
status: "imported",
|
|
2516
|
+
source: "cli",
|
|
2517
|
+
expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : void 0
|
|
2255
2518
|
};
|
|
2256
2519
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
if (!type) continue;
|
|
2266
|
-
const normalizedIcon = normalizeRuntimeEntryIcon(entry.icon);
|
|
2267
|
-
normalized[id] = {
|
|
2268
|
-
enabled: typeof entry.enabled === "boolean" ? entry.enabled : true,
|
|
2269
|
-
...normalizeOptionalString(entry.label) ? { label: normalizeOptionalString(entry.label) ?? void 0 } : {},
|
|
2270
|
-
...normalizedIcon ? { icon: normalizedIcon } : {},
|
|
2271
|
-
type,
|
|
2272
|
-
config: normalizeRuntimeEntryConfig(type, entry.config && typeof entry.config === "object" && !Array.isArray(entry.config) ? entry.config : {})
|
|
2273
|
-
};
|
|
2274
|
-
}
|
|
2275
|
-
return normalized;
|
|
2276
|
-
}
|
|
2277
|
-
function normalizeRuntimeEntryIcon(value) {
|
|
2278
|
-
if (typeof value === "string") {
|
|
2279
|
-
const src = value.trim();
|
|
2280
|
-
return src ? {
|
|
2281
|
-
kind: "image",
|
|
2282
|
-
src
|
|
2283
|
-
} : null;
|
|
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);
|
|
2284
2528
|
}
|
|
2285
|
-
|
|
2286
|
-
const src = normalizeOptionalString(value.src);
|
|
2287
|
-
if (!src) return null;
|
|
2288
|
-
const alt = normalizeOptionalString(value.alt);
|
|
2289
|
-
return {
|
|
2290
|
-
kind: "image",
|
|
2291
|
-
src,
|
|
2292
|
-
...alt ? { alt } : {}
|
|
2293
|
-
};
|
|
2294
|
-
}
|
|
2295
|
-
function clearSecretRef(refs, path) {
|
|
2296
|
-
const { [path]: _removed, ...nextRefs } = refs;
|
|
2297
|
-
return nextRefs;
|
|
2298
|
-
}
|
|
2299
|
-
function buildConfigMeta(config, options) {
|
|
2300
|
-
return {
|
|
2301
|
-
search: SEARCH_PROVIDER_META,
|
|
2302
|
-
channels: buildProjectedChannelMeta(config, options)
|
|
2303
|
-
};
|
|
2304
|
-
}
|
|
2305
|
-
function buildProviderTemplatesView() {
|
|
2306
|
-
return { providerTemplates: BUILTIN_PROVIDERS.map((spec) => {
|
|
2529
|
+
getExtensionConfigProjectionOptions = () => {
|
|
2307
2530
|
return {
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
displayName: spec.displayName ?? spec.name,
|
|
2311
|
-
apiProtocol: spec.apiProtocol,
|
|
2312
|
-
modelPrefix: spec.modelPrefix,
|
|
2313
|
-
keywords: spec.keywords,
|
|
2314
|
-
envKey: spec.envKey,
|
|
2315
|
-
isGateway: spec.isGateway,
|
|
2316
|
-
isLocal: spec.isLocal,
|
|
2317
|
-
defaultApiBase: spec.defaultApiBase,
|
|
2318
|
-
logo: spec.logo,
|
|
2319
|
-
apiBaseHelp: spec.apiBaseHelp,
|
|
2320
|
-
auth: spec.auth ? {
|
|
2321
|
-
kind: spec.auth.kind,
|
|
2322
|
-
displayName: spec.auth.displayName,
|
|
2323
|
-
note: spec.auth.note,
|
|
2324
|
-
methods: spec.auth.methods?.map((method) => ({
|
|
2325
|
-
id: method.id,
|
|
2326
|
-
label: method.label,
|
|
2327
|
-
hint: method.hint
|
|
2328
|
-
})),
|
|
2329
|
-
defaultMethodId: spec.auth.defaultMethodId,
|
|
2330
|
-
supportsCliImport: Boolean(spec.auth.cliCredential)
|
|
2331
|
-
} : void 0,
|
|
2332
|
-
defaultModels: normalizeModelList(spec.defaultModels ?? []),
|
|
2333
|
-
modelConfig: normalizeProviderModelConfig(spec.modelConfig ?? {}),
|
|
2334
|
-
supportsWireApi: spec.supportsWireApi,
|
|
2335
|
-
wireApiOptions: spec.wireApiOptions,
|
|
2336
|
-
defaultWireApi: spec.defaultWireApi
|
|
2531
|
+
extensionChannelBindings: this.options.extensions?.getChannelBindings() ?? [],
|
|
2532
|
+
extensionUiMetadata: this.options.extensions?.getUiMetadata() ?? []
|
|
2337
2533
|
};
|
|
2338
|
-
}).sort((left, right) => {
|
|
2339
|
-
const leftRank = PREFERRED_PROVIDER_ORDER_INDEX.get(left.id);
|
|
2340
|
-
const rightRank = PREFERRED_PROVIDER_ORDER_INDEX.get(right.id);
|
|
2341
|
-
if (leftRank !== void 0 && rightRank !== void 0) return leftRank - rightRank;
|
|
2342
|
-
if (leftRank !== void 0) return -1;
|
|
2343
|
-
if (rightRank !== void 0) return 1;
|
|
2344
|
-
return left.id.localeCompare(right.id);
|
|
2345
|
-
}) };
|
|
2346
|
-
}
|
|
2347
|
-
function buildProvidersView(config) {
|
|
2348
|
-
const uiHints = buildUiHints(config);
|
|
2349
|
-
const providers = {};
|
|
2350
|
-
for (const [providerId, provider] of Object.entries(config.providers)) {
|
|
2351
|
-
const providerConfig = provider;
|
|
2352
|
-
providers[providerId] = toProviderView(config, providerConfig, providerId, uiHints, findServerBuiltinProviderByName(resolveProviderType(providerId, providerConfig) ?? ""));
|
|
2353
|
-
}
|
|
2354
|
-
return { providers };
|
|
2355
|
-
}
|
|
2356
|
-
function buildConfigSchemaView(_config, options) {
|
|
2357
|
-
const base = buildConfigSchema({ version: getPackageVersion() });
|
|
2358
|
-
const extensionUiHints = buildExtensionChannelUiHints(options);
|
|
2359
|
-
if (Object.keys(extensionUiHints).length === 0) return base;
|
|
2360
|
-
return {
|
|
2361
|
-
...base,
|
|
2362
|
-
uiHints: {
|
|
2363
|
-
...base.uiHints,
|
|
2364
|
-
...extensionUiHints
|
|
2365
|
-
}
|
|
2366
2534
|
};
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
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())));
|
|
2375
2577
|
};
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
message: `scope mismatch: expected ${action.scope}, got ${request.scope}`,
|
|
2380
|
-
details: {
|
|
2381
|
-
expectedScope: action.scope,
|
|
2382
|
-
requestScope: request.scope
|
|
2383
|
-
}
|
|
2578
|
+
getConfigMeta = (c) => {
|
|
2579
|
+
const config = loadConfigOrDefault(this.options.configPath);
|
|
2580
|
+
return c.json(ok(buildConfigMeta(config, this.getExtensionConfigProjectionOptions())));
|
|
2384
2581
|
};
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
ok
|
|
2388
|
-
code: "ACTION_PRECONDITION_FAILED",
|
|
2389
|
-
message: `required field missing: ${requiredPath}`,
|
|
2390
|
-
details: { path: requiredPath }
|
|
2582
|
+
listProviders = (c) => {
|
|
2583
|
+
const config = loadConfigOrDefault(this.options.configPath);
|
|
2584
|
+
return c.json(ok(buildProvidersView(config)));
|
|
2391
2585
|
};
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
ok: false,
|
|
2395
|
-
code: "ACTION_EXECUTION_FAILED",
|
|
2396
|
-
message: `action handler not found for type ${action.type}`
|
|
2586
|
+
listProviderTemplates = (c) => {
|
|
2587
|
+
return c.json(ok(buildProviderTemplatesView()));
|
|
2397
2588
|
};
|
|
2398
|
-
|
|
2399
|
-
ok
|
|
2400
|
-
data: await handler({
|
|
2401
|
-
config: runtimeConfig,
|
|
2402
|
-
action
|
|
2403
|
-
})
|
|
2589
|
+
listProviderModelCatalog = (c) => {
|
|
2590
|
+
return c.json(ok(this.options.kernel.providerModelCatalog.getSnapshot()));
|
|
2404
2591
|
};
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
}
|
|
2409
|
-
function createProviderForUpdate(providerId) {
|
|
2410
|
-
const spec = findServerBuiltinProviderByName(providerId);
|
|
2411
|
-
if (!spec) return null;
|
|
2412
|
-
return createDefaultProviderConfigFromSpec(spec);
|
|
2413
|
-
}
|
|
2414
|
-
function updateModel(configPath, patch) {
|
|
2415
|
-
const config = loadConfigOrDefault(configPath);
|
|
2416
|
-
if (typeof patch.model === "string") config.agents.defaults.model = patch.model;
|
|
2417
|
-
if (typeof patch.workspace === "string") config.agents.defaults.workspace = normalizeOptionalString(patch.workspace) ?? DEFAULT_WORKSPACE_PATH;
|
|
2418
|
-
const next = ConfigSchema.parse(config);
|
|
2419
|
-
saveConfig(next, configPath);
|
|
2420
|
-
return buildConfigView(next);
|
|
2421
|
-
}
|
|
2422
|
-
function updateProvider(configPath, providerId, patch) {
|
|
2423
|
-
const config = loadConfigOrDefault(configPath);
|
|
2424
|
-
const providers = config.providers;
|
|
2425
|
-
const provider = providers[providerId] ?? createProviderForUpdate(providerId);
|
|
2426
|
-
if (!provider) return null;
|
|
2427
|
-
providers[providerId] = provider;
|
|
2428
|
-
const spec = findServerBuiltinProviderByName((Object.prototype.hasOwnProperty.call(patch, "providerType") ? normalizeProviderId(patch.providerType) : resolveProviderType(providerId, provider)) ?? "");
|
|
2429
|
-
if (Object.prototype.hasOwnProperty.call(patch, "providerType")) provider.providerType = spec?.name ?? null;
|
|
2430
|
-
if (Object.prototype.hasOwnProperty.call(patch, "displayName")) provider.displayName = normalizeOptionalDisplayName(patch.displayName) ?? "";
|
|
2431
|
-
if (Object.prototype.hasOwnProperty.call(patch, "enabled")) provider.enabled = patch.enabled !== false;
|
|
2432
|
-
if (Object.prototype.hasOwnProperty.call(patch, "apiKey")) {
|
|
2433
|
-
provider.apiKey = patch.apiKey ?? "";
|
|
2434
|
-
config.secrets.refs = clearSecretRef(config.secrets.refs, `providers.${providerId}.apiKey`);
|
|
2435
|
-
}
|
|
2436
|
-
if (Object.prototype.hasOwnProperty.call(patch, "apiBase")) provider.apiBase = patch.apiBase ?? null;
|
|
2437
|
-
if (Object.prototype.hasOwnProperty.call(patch, "extraHeaders")) provider.extraHeaders = patch.extraHeaders ?? null;
|
|
2438
|
-
if (Object.prototype.hasOwnProperty.call(patch, "wireApi") && (spec?.supportsWireApi || !spec)) provider.wireApi = patch.wireApi ?? spec?.defaultWireApi ?? "auto";
|
|
2439
|
-
if (Object.prototype.hasOwnProperty.call(patch, "models")) provider.models = normalizeModelList(patch.models ?? []);
|
|
2440
|
-
if (Object.prototype.hasOwnProperty.call(patch, "modelConfig")) provider.modelConfig = normalizeProviderModelConfig(patch.modelConfig ?? {});
|
|
2441
|
-
const next = ConfigSchema.parse(config);
|
|
2442
|
-
saveConfig(next, configPath);
|
|
2443
|
-
const uiHints = buildUiHints(next);
|
|
2444
|
-
const updated = next.providers[providerId];
|
|
2445
|
-
return toProviderView(next, updated, providerId, uiHints, spec ?? void 0);
|
|
2446
|
-
}
|
|
2447
|
-
function createProvider(configPath, patch = {}) {
|
|
2448
|
-
const config = loadConfigOrDefault(configPath);
|
|
2449
|
-
const providers = config.providers;
|
|
2450
|
-
const requestedProviderType = normalizeProviderId(patch.providerType);
|
|
2451
|
-
const spec = requestedProviderType ? findServerBuiltinProviderByName(requestedProviderType) : void 0;
|
|
2452
|
-
const fallbackProviderId = spec ? spec.name : findNextCustomProviderName(config);
|
|
2453
|
-
const requestedProviderId = normalizeProviderId(patch.providerId);
|
|
2454
|
-
if (requestedProviderId && providers[requestedProviderId]) return null;
|
|
2455
|
-
const providerId = requestedProviderId ? requestedProviderId : findNextProviderId(config, fallbackProviderId);
|
|
2456
|
-
const generatedDisplayName = spec ? `${spec.displayName}${resolveProviderDisplayNameSuffix(providerId, spec.name)}` : resolveCustomProviderFallbackDisplayName(providerId);
|
|
2457
|
-
const defaultModels = spec ? buildProviderScopedModels(providerId, spec.defaultModels ?? []) : [];
|
|
2458
|
-
providers[providerId] = {
|
|
2459
|
-
enabled: patch.enabled !== false,
|
|
2460
|
-
providerType: spec?.name ?? null,
|
|
2461
|
-
displayName: normalizeOptionalDisplayName(patch.displayName) ?? generatedDisplayName,
|
|
2462
|
-
apiKey: normalizeOptionalString(patch.apiKey) ?? "",
|
|
2463
|
-
apiBase: normalizeOptionalString(patch.apiBase) ?? spec?.defaultApiBase ?? null,
|
|
2464
|
-
extraHeaders: normalizeHeaders(patch.extraHeaders ?? null),
|
|
2465
|
-
wireApi: patch.wireApi ?? spec?.defaultWireApi ?? "auto",
|
|
2466
|
-
models: Object.prototype.hasOwnProperty.call(patch, "models") ? normalizeModelList(patch.models ?? []) : defaultModels,
|
|
2467
|
-
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())));
|
|
2468
2595
|
};
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
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
|
+
}));
|
|
2476
2613
|
};
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
const
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
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));
|
|
2552
2705
|
};
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
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
|
+
}
|
|
2559
2717
|
};
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
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));
|
|
2567
2727
|
};
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
latencyMs: Date.now() - startedAtMs,
|
|
2595
|
-
message: stringifyError(error) || "Connection test failed."
|
|
2596
|
-
};
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2599
|
-
function updateChannel(configPath, channelName, patch, options) {
|
|
2600
|
-
const config = loadConfigOrDefault(configPath);
|
|
2601
|
-
const normalizedOptions = normalizeExtensionProjectionOptions(options);
|
|
2602
|
-
const channel = getProjectedChannelConfig(config, channelName, normalizedOptions);
|
|
2603
|
-
if (!channel) return null;
|
|
2604
|
-
for (const key of Object.keys(patch)) {
|
|
2605
|
-
const path = `channels.${channelName}.${key}`;
|
|
2606
|
-
if (isSensitivePath(path)) config.secrets.refs = clearSecretRef(config.secrets.refs, path);
|
|
2607
|
-
}
|
|
2608
|
-
const mergedChannel = {
|
|
2609
|
-
...channel,
|
|
2610
|
-
...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
|
+
}
|
|
2611
2754
|
};
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
const
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
const nextContextTokens = defaultsPatch.contextTokens;
|
|
2628
|
-
if (typeof nextContextTokens === "number" && Number.isFinite(nextContextTokens)) next = {
|
|
2629
|
-
...next,
|
|
2630
|
-
contextTokens: Math.max(1e3, Math.trunc(nextContextTokens))
|
|
2631
|
-
};
|
|
2632
|
-
}
|
|
2633
|
-
if (Object.prototype.hasOwnProperty.call(defaultsPatch, "engine")) next = {
|
|
2634
|
-
...next,
|
|
2635
|
-
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));
|
|
2636
2770
|
};
|
|
2637
|
-
|
|
2638
|
-
const
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
if (patch.agents?.runtimes && Object.prototype.hasOwnProperty.call(patch.agents.runtimes, "entries")) config.agents.runtimes.entries = normalizeRuntimeEntries(patch.agents.runtimes.entries);
|
|
2661
|
-
if (Object.prototype.hasOwnProperty.call(patch, "bindings")) config.bindings = patch.bindings ?? [];
|
|
2662
|
-
if (patch.session) config.session = {
|
|
2663
|
-
...config.session,
|
|
2664
|
-
...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
|
+
}
|
|
2665
2794
|
};
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
bindings: view.bindings ?? [],
|
|
2673
|
-
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));
|
|
2674
2801
|
};
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
"
|
|
2684
|
-
"
|
|
2685
|
-
|
|
2686
|
-
if (
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
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);
|
|
2690
2820
|
}
|
|
2691
|
-
config.secrets.defaults = nextDefaults;
|
|
2692
|
-
}
|
|
2693
|
-
if (Object.prototype.hasOwnProperty.call(patch, "providers")) config.secrets.providers = patch.providers ?? {};
|
|
2694
|
-
if (Object.prototype.hasOwnProperty.call(patch, "refs")) config.secrets.refs = patch.refs ?? {};
|
|
2695
|
-
const next = ConfigSchema.parse(config);
|
|
2696
|
-
saveConfig(next, configPath);
|
|
2697
|
-
return {
|
|
2698
|
-
enabled: next.secrets.enabled,
|
|
2699
|
-
defaults: { ...next.secrets.defaults },
|
|
2700
|
-
providers: { ...next.secrets.providers },
|
|
2701
|
-
refs: { ...next.secrets.refs }
|
|
2702
2821
|
};
|
|
2703
|
-
|
|
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
|
+
};
|
|
2704
2831
|
//#endregion
|
|
2705
2832
|
//#region src/features/cron/controllers/cron.controller.ts
|
|
2706
2833
|
const CRON_LIST_MAX_LIMIT = 100;
|
|
@@ -2842,7 +2969,7 @@ var CronRoutesController = class {
|
|
|
2842
2969
|
const allJobs = this.options.cron.listJobs(true).map((job) => buildCronJobView(job));
|
|
2843
2970
|
const enabled = allJobs.filter((job) => job.enabled).length;
|
|
2844
2971
|
const normalizedQuery = query.query?.trim().toLowerCase() ?? "";
|
|
2845
|
-
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));
|
|
2846
2973
|
const offset = offsetResult.value ?? 0;
|
|
2847
2974
|
const jobs = limitResult.value === null ? filteredJobs : filteredJobs.slice(offset, offset + limitResult.value);
|
|
2848
2975
|
return c.json(ok({
|
|
@@ -3857,7 +3984,6 @@ var SessionSkillsViewBuilder = class {
|
|
|
3857
3984
|
};
|
|
3858
3985
|
//#endregion
|
|
3859
3986
|
//#region src/features/sessions/controllers/sessions.controller.ts
|
|
3860
|
-
const INTERRUPTED_SESSION_STATUS_TEXT = "Run interrupted: no completion or error event was recorded. Please send the message again.";
|
|
3861
3987
|
const DEFAULT_SESSION_MESSAGE_PAGE_SIZE = 80;
|
|
3862
3988
|
const MAX_SESSION_MESSAGE_PAGE_SIZE = 200;
|
|
3863
3989
|
function sessionProjectError(error) {
|
|
@@ -3905,7 +4031,8 @@ function normalizeSessionActivityPreview(session) {
|
|
|
3905
4031
|
last_activity_preview: {
|
|
3906
4032
|
...preview,
|
|
3907
4033
|
state,
|
|
3908
|
-
|
|
4034
|
+
statusKind: state === "failed" ? "run-interrupted" : void 0,
|
|
4035
|
+
statusText: void 0
|
|
3909
4036
|
}
|
|
3910
4037
|
}
|
|
3911
4038
|
};
|
|
@@ -5610,6 +5737,14 @@ function readStreamPayload(url) {
|
|
|
5610
5737
|
function isAbortPayload(value) {
|
|
5611
5738
|
return isRecord(value) && typeof value.sessionId === "string" && value.sessionId.trim().length > 0;
|
|
5612
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
|
+
}
|
|
5613
5748
|
var UiRouteRegistry = class {
|
|
5614
5749
|
constructor(app, options, controllers) {
|
|
5615
5750
|
this.app = app;
|
|
@@ -5643,6 +5778,24 @@ var UiRouteRegistry = class {
|
|
|
5643
5778
|
}, { source: "ui-http" });
|
|
5644
5779
|
return c.json(ok({ accepted: true }));
|
|
5645
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
|
+
});
|
|
5646
5799
|
};
|
|
5647
5800
|
mountNcpAgentRoutes = (kernel, ncpAsset) => {
|
|
5648
5801
|
this.mountAgentRunRoutes(NCP_AGENT_BASE_PATH, kernel);
|
|
@@ -6041,6 +6194,11 @@ var UiRouteRegistry = class {
|
|
|
6041
6194
|
"/api/provider-templates",
|
|
6042
6195
|
config.listProviderTemplates
|
|
6043
6196
|
],
|
|
6197
|
+
[
|
|
6198
|
+
"get",
|
|
6199
|
+
"/api/provider-model-catalog",
|
|
6200
|
+
config.listProviderModelCatalog
|
|
6201
|
+
],
|
|
6044
6202
|
[
|
|
6045
6203
|
"post",
|
|
6046
6204
|
"/api/providers",
|
|
@@ -6061,6 +6219,11 @@ var UiRouteRegistry = class {
|
|
|
6061
6219
|
"/api/providers/:providerId/test",
|
|
6062
6220
|
config.testProviderConnection
|
|
6063
6221
|
],
|
|
6222
|
+
[
|
|
6223
|
+
"post",
|
|
6224
|
+
"/api/providers/:providerId/models/discover",
|
|
6225
|
+
config.discoverProviderModels
|
|
6226
|
+
],
|
|
6064
6227
|
[
|
|
6065
6228
|
"post",
|
|
6066
6229
|
"/api/providers/:providerId/auth/start",
|
|
@@ -6479,6 +6642,6 @@ async function startUiServer(gateway) {
|
|
|
6479
6642
|
};
|
|
6480
6643
|
}
|
|
6481
6644
|
//#endregion
|
|
6482
|
-
export { ConfigRoutesController, InboxDeliveriesRoutesController, PanelAppsRoutesController, RuntimeControlRoutesController, ServiceAppsRoutesController, buildConfigMeta, buildConfigSchemaView, buildConfigView, buildProviderTemplatesView, buildProvidersView, createProvider, createUiRouter, deleteProvider, ensureUiBridgeSecret, executeConfigAction, getUiBridgeSecretPath, loadConfigOrDefault, readUiBridgeSecret, startUiServer,
|
|
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 };
|
|
6483
6646
|
|
|
6484
6647
|
//# sourceMappingURL=index.js.map
|