mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 13:44:32 +00:00
feat: add provider auth plugins
This commit is contained in:
@@ -8,12 +8,21 @@ import {
|
||||
upsertAuthProfile,
|
||||
} from "../../agents/auth-profiles.js";
|
||||
import { normalizeProviderId } from "../../agents/model-selection.js";
|
||||
import { resolveAgentDir, resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js";
|
||||
import { parseDurationMs } from "../../cli/parse-duration.js";
|
||||
import { CONFIG_PATH_CLAWDBOT } from "../../config/config.js";
|
||||
import { CONFIG_PATH_CLAWDBOT, readConfigFileSnapshot } from "../../config/config.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { stylePromptHint, stylePromptMessage } from "../../terminal/prompt-style.js";
|
||||
import { applyAuthProfileConfig } from "../onboard-auth.js";
|
||||
import { isRemoteEnvironment } from "../antigravity-oauth.js";
|
||||
import { openUrl } from "../onboard-helpers.js";
|
||||
import { createVpsAwareOAuthHandlers } from "../oauth-flow.js";
|
||||
import { updateConfig } from "./shared.js";
|
||||
import { resolvePluginProviders } from "../../plugins/providers.js";
|
||||
import { createClackPrompter } from "../../wizard/clack-prompter.js";
|
||||
import type { ProviderAuthMethod, ProviderAuthResult, ProviderPlugin } from "../../plugins/types.js";
|
||||
import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js";
|
||||
|
||||
const confirm = (params: Parameters<typeof clackConfirm>[0]) =>
|
||||
clackConfirm({
|
||||
@@ -215,3 +224,204 @@ export async function modelsAuthAddCommand(_opts: Record<string, never>, runtime
|
||||
|
||||
await modelsAuthPasteTokenCommand({ provider: providerId, profileId, expiresIn }, runtime);
|
||||
}
|
||||
|
||||
type LoginOptions = {
|
||||
provider?: string;
|
||||
method?: string;
|
||||
setDefault?: boolean;
|
||||
};
|
||||
|
||||
function resolveProviderMatch(
|
||||
providers: ProviderPlugin[],
|
||||
rawProvider?: string,
|
||||
): ProviderPlugin | null {
|
||||
const raw = rawProvider?.trim();
|
||||
if (!raw) return null;
|
||||
const normalized = normalizeProviderId(raw);
|
||||
return (
|
||||
providers.find((provider) => normalizeProviderId(provider.id) === normalized) ??
|
||||
providers.find(
|
||||
(provider) =>
|
||||
provider.aliases?.some((alias) => normalizeProviderId(alias) === normalized) ?? false,
|
||||
) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function pickAuthMethod(provider: ProviderPlugin, rawMethod?: string): ProviderAuthMethod | null {
|
||||
const raw = rawMethod?.trim();
|
||||
if (!raw) return null;
|
||||
const normalized = raw.toLowerCase();
|
||||
return (
|
||||
provider.auth.find((method) => method.id.toLowerCase() === normalized) ??
|
||||
provider.auth.find((method) => method.label.toLowerCase() === normalized) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function mergeConfigPatch<T>(base: T, patch: unknown): T {
|
||||
if (!isPlainRecord(base) || !isPlainRecord(patch)) {
|
||||
return patch as T;
|
||||
}
|
||||
|
||||
const next: Record<string, unknown> = { ...base };
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
const existing = next[key];
|
||||
if (isPlainRecord(existing) && isPlainRecord(value)) {
|
||||
next[key] = mergeConfigPatch(existing, value);
|
||||
} else {
|
||||
next[key] = value;
|
||||
}
|
||||
}
|
||||
return next as T;
|
||||
}
|
||||
|
||||
function applyDefaultModel(cfg: ClawdbotConfig, model: string): ClawdbotConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[model] = models[model] ?? {};
|
||||
|
||||
const existingModel = cfg.agents?.defaults?.model;
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
model: {
|
||||
...(existingModel && typeof existingModel === "object" && "fallbacks" in existingModel
|
||||
? { fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks }
|
||||
: undefined),
|
||||
primary: model,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function credentialMode(credential: AuthProfileCredential): "api_key" | "oauth" | "token" {
|
||||
if (credential.type === "api_key") return "api_key";
|
||||
if (credential.type === "token") return "token";
|
||||
return "oauth";
|
||||
}
|
||||
|
||||
export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: RuntimeEnv) {
|
||||
if (!process.stdin.isTTY) {
|
||||
throw new Error("models auth login requires an interactive TTY.");
|
||||
}
|
||||
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
if (!snapshot.valid) {
|
||||
const issues = snapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n");
|
||||
throw new Error(`Invalid config at ${snapshot.path}\n${issues}`);
|
||||
}
|
||||
|
||||
const config = snapshot.config;
|
||||
const defaultAgentId = resolveDefaultAgentId(config);
|
||||
const agentDir = resolveAgentDir(config, defaultAgentId);
|
||||
const workspaceDir =
|
||||
resolveAgentWorkspaceDir(config, defaultAgentId) ?? resolveDefaultAgentWorkspaceDir();
|
||||
|
||||
const providers = resolvePluginProviders({ config, workspaceDir });
|
||||
if (providers.length === 0) {
|
||||
throw new Error("No provider plugins found. Install one via `clawdbot plugins install`.");
|
||||
}
|
||||
|
||||
const prompter = createClackPrompter();
|
||||
const selectedProvider =
|
||||
resolveProviderMatch(providers, opts.provider) ??
|
||||
(await prompter.select({
|
||||
message: "Select a provider",
|
||||
options: providers.map((provider) => ({
|
||||
value: provider.id,
|
||||
label: provider.label,
|
||||
hint: provider.docsPath ? `Docs: ${provider.docsPath}` : undefined,
|
||||
})),
|
||||
}).then((id) => resolveProviderMatch(providers, String(id))));
|
||||
|
||||
if (!selectedProvider) {
|
||||
throw new Error("Unknown provider. Use --provider <id> to pick a provider plugin.");
|
||||
}
|
||||
|
||||
const chosenMethod =
|
||||
pickAuthMethod(selectedProvider, opts.method) ??
|
||||
(selectedProvider.auth.length === 1
|
||||
? selectedProvider.auth[0]
|
||||
: await prompter.select({
|
||||
message: `Auth method for ${selectedProvider.label}`,
|
||||
options: selectedProvider.auth.map((method) => ({
|
||||
value: method.id,
|
||||
label: method.label,
|
||||
hint: method.hint,
|
||||
})),
|
||||
}).then((id) =>
|
||||
selectedProvider.auth.find((method) => method.id === String(id)),
|
||||
));
|
||||
|
||||
if (!chosenMethod) {
|
||||
throw new Error("Unknown auth method. Use --method <id> to select one.");
|
||||
}
|
||||
|
||||
const isRemote = isRemoteEnvironment();
|
||||
const result: ProviderAuthResult = await chosenMethod.run({
|
||||
config,
|
||||
agentDir,
|
||||
workspaceDir,
|
||||
prompter,
|
||||
runtime,
|
||||
isRemote,
|
||||
openUrl: async (url) => {
|
||||
await openUrl(url);
|
||||
},
|
||||
oauth: {
|
||||
createVpsAwareHandlers: (params) => createVpsAwareOAuthHandlers(params),
|
||||
},
|
||||
});
|
||||
|
||||
for (const profile of result.profiles) {
|
||||
upsertAuthProfile({
|
||||
profileId: profile.profileId,
|
||||
credential: profile.credential,
|
||||
agentDir,
|
||||
});
|
||||
}
|
||||
|
||||
await updateConfig((cfg) => {
|
||||
let next = cfg;
|
||||
if (result.configPatch) {
|
||||
next = mergeConfigPatch(next, result.configPatch);
|
||||
}
|
||||
for (const profile of result.profiles) {
|
||||
next = applyAuthProfileConfig(next, {
|
||||
profileId: profile.profileId,
|
||||
provider: profile.credential.provider,
|
||||
mode: credentialMode(profile.credential),
|
||||
});
|
||||
}
|
||||
if (opts.setDefault && result.defaultModel) {
|
||||
next = applyDefaultModel(next, result.defaultModel);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||
for (const profile of result.profiles) {
|
||||
runtime.log(
|
||||
`Auth profile: ${profile.profileId} (${profile.credential.provider}/${credentialMode(profile.credential)})`,
|
||||
);
|
||||
}
|
||||
if (result.defaultModel) {
|
||||
runtime.log(
|
||||
opts.setDefault
|
||||
? `Default model set to ${result.defaultModel}`
|
||||
: `Default model available: ${result.defaultModel} (use --set-default to apply)`,
|
||||
);
|
||||
}
|
||||
if (result.notes && result.notes.length > 0) {
|
||||
await prompter.note(result.notes.join("\n"), "Provider notes");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user