fix(agent): reload credentials dynamically

This commit is contained in:
Jiang Bohan 2026-02-06 15:48:29 +08:00
parent 3b09d8d44d
commit 24df8b02e7
3 changed files with 106 additions and 11 deletions

View file

@ -1,4 +1,4 @@
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
import { join, dirname } from "node:path";
import { homedir } from "node:os";
import JSON5 from "json5";
@ -90,6 +90,8 @@ export class CredentialManager {
private coreConfig: CredentialsConfig | null = null;
private skillsConfig: SkillsEnvConfig | null = null;
private resolvedSkillsEnv: Record<string, string> | null = null;
private coreMtimeMs: number | null = null;
private skillsMtimeMs: number | null = null;
private isDisabled(): boolean {
if (process.env.SMC_CREDENTIALS_DISABLE === "1") return true;
@ -99,17 +101,32 @@ export class CredentialManager {
private loadCore(): void {
const path = getCredentialsPath();
const disabled = this.isDisabled();
let mtimeMs: number | null = null;
if (this.corePath === path && this.disabledState === disabled && this.coreConfig) {
if (!disabled && existsSync(path)) {
try {
mtimeMs = statSync(path).mtimeMs;
} catch {
mtimeMs = null;
}
}
if (
this.corePath === path
&& this.disabledState === disabled
&& this.coreConfig
&& this.coreMtimeMs === mtimeMs
) {
return;
}
this.corePath = path;
this.disabledState = disabled;
this.coreConfig = null;
this.coreMtimeMs = mtimeMs;
if (disabled) return;
if (!existsSync(path)) return;
if (mtimeMs === null) return;
const raw = readFileSync(path, "utf8");
try {
@ -123,8 +140,22 @@ export class CredentialManager {
private loadSkillsEnv(): void {
const path = getSkillsEnvPath();
const disabled = this.isDisabled();
let mtimeMs: number | null = null;
if (this.skillsPath === path && this.disabledState === disabled && this.resolvedSkillsEnv) {
if (!disabled && existsSync(path)) {
try {
mtimeMs = statSync(path).mtimeMs;
} catch {
mtimeMs = null;
}
}
if (
this.skillsPath === path
&& this.disabledState === disabled
&& this.resolvedSkillsEnv
&& this.skillsMtimeMs === mtimeMs
) {
return;
}
@ -132,9 +163,10 @@ export class CredentialManager {
this.disabledState = disabled;
this.skillsConfig = null;
this.resolvedSkillsEnv = null;
this.skillsMtimeMs = mtimeMs;
if (disabled) return;
if (!existsSync(path)) return;
if (mtimeMs === null) return;
const raw = readFileSync(path, "utf8");
try {
@ -228,6 +260,8 @@ export class CredentialManager {
this.coreConfig = null;
this.skillsConfig = null;
this.resolvedSkillsEnv = null;
this.coreMtimeMs = null;
this.skillsMtimeMs = null;
}
/**

View file

@ -264,7 +264,7 @@ export function getLoginInstructions(providerId: string): string {
if (info.authMethod === "oauth") {
if (info.loginCommand) {
return `Run: ${info.loginCommand}\nThen restart Super Multica to use the credentials.`;
return `Run: ${info.loginCommand}\nThen retry in Super Multica to use the credentials.`;
}
}

View file

@ -95,6 +95,7 @@ export class Agent {
private profileCandidates: string[];
private profileIndex: number;
private readonly pinnedProfile: boolean;
private readonly explicitApiKey: boolean;
/** Current session ID */
readonly sessionId: string;
@ -126,6 +127,7 @@ export class Agent {
// === Auth profile resolution ===
this.pinnedProfile = !!(options.authProfileId || options.apiKey);
this.explicitApiKey = !!options.apiKey;
if (options.apiKey) {
// Explicit API key — no rotation
@ -159,11 +161,9 @@ export class Agent {
: 0;
}
this.agent = new PiAgentCore(
this.currentApiKey
? { getApiKey: (_provider: string) => this.currentApiKey! }
: {},
);
this.agent = new PiAgentCore({
getApiKey: (_provider: string) => this.currentApiKey ?? "",
});
// Load Agent Profile (if profileId is specified)
// Every Agent should have a Profile for memory, tools config, and other settings
@ -354,6 +354,10 @@ export class Agent {
async run(prompt: string): Promise<AgentRunResult> {
await this.ensureInitialized();
this.refreshAuthState();
if (!this.currentApiKey) {
throw new Error(`No API key configured for provider: ${this.resolvedProvider}`);
}
this.output.state.lastAssistantText = "";
const canRotate = !this.pinnedProfile && this.profileCandidates.length > 1;
@ -433,12 +437,67 @@ export class Agent {
this.currentApiKey = apiKey;
this.currentProfileId = candidateId;
this.profileIndex = nextIndex;
this.updateSessionApiKey();
return true;
}
return false;
}
private refreshAuthState(): void {
if (this.explicitApiKey) {
return;
}
const store = loadAuthProfileStore();
if (this.pinnedProfile) {
const profileId = this.currentProfileId ?? this.resolvedProvider;
this.currentApiKey = resolveApiKeyForProfile(profileId) ?? resolveApiKey(this.resolvedProvider);
this.currentProfileId = profileId;
this.profileCandidates = [];
this.profileIndex = 0;
this.updateSessionApiKey();
return;
}
const candidates = resolveAuthProfileOrder(this.resolvedProvider, store);
this.profileCandidates = candidates;
if (this.currentProfileId) {
const currentIndex = candidates.indexOf(this.currentProfileId);
if (currentIndex >= 0) {
const stats = store.usageStats?.[this.currentProfileId];
if (!stats || !isProfileInCooldown(stats)) {
const apiKey = resolveApiKeyForProfile(this.currentProfileId);
if (apiKey) {
this.currentApiKey = apiKey;
this.profileIndex = currentIndex;
this.updateSessionApiKey();
return;
}
}
}
}
const resolved = resolveApiKeyForProvider(this.resolvedProvider);
if (resolved) {
this.currentApiKey = resolved.apiKey;
this.currentProfileId = resolved.profileId;
this.profileIndex = Math.max(0, candidates.indexOf(resolved.profileId));
} else {
this.currentApiKey = undefined;
this.currentProfileId = undefined;
this.profileIndex = 0;
}
this.updateSessionApiKey();
}
private updateSessionApiKey(): void {
if (this.session.getCompactionMode() !== "summary") return;
this.session.setApiKey(this.currentApiKey);
}
private handleSessionEvent(event: AgentEvent) {
if (event.type === "message_end") {
const message = event.message as AgentMessage;
@ -680,6 +739,8 @@ export class Agent {
throw new Error(`No API key configured for provider: ${providerId}`);
}
this.updateSessionApiKey();
// Update the agent's model and API key
const baseUrl = resolveBaseUrl(actualProvider);
const modelWithBaseUrl = baseUrl ? { ...model, baseUrl } : model;