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