mirror of https://github.com/openclaw/openclaw.git
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
parent
acd8966ff0
commit
ef5f47bd39
|
|
@ -103,6 +103,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Gateway/device auth: reuse cached device-token scopes only for cached-token reconnects, while keeping explicit `deviceToken` scope requests and empty-cache fallbacks intact so reconnects preserve `operator.read` without breaking explicit auth flows. (#46032) Thanks @caicongyang.
|
||||
- Agents/scheduling: steer background-now work toward automatic completion wake and treat `process` polling as on-demand inspection or intervention instead of default completion handling. (#60877) Thanks @vincentkoc.
|
||||
- Google Gemini CLI auth: improve OAuth credential discovery across Windows nvm and Homebrew libexec installs, and align Code Assist metadata so Gemini login stops failing on packaged CLI layouts. (#40729) Thanks @hughcube.
|
||||
- Google Gemini CLI auth: detect bundled npm installs by scanning packaged bundle files for the Gemini OAuth client config, so `npm install -g @google/gemini-cli` layouts work again. (#60486) Thanks @wzfmini01.
|
||||
- Mattermost/config schema: accept `groups.*.requireMention` again so existing Mattermost configs no longer fail strict validation after upgrade. (#58271) Thanks @MoerAI.
|
||||
- Agents/failover: scope Anthropic `An unknown error occurred` failover matching by provider so generic internal unknown-error text no longer triggers retryable timeout fallback. (#59325) Thanks @aaron-he-zhu.
|
||||
- Providers/OpenRouter failover: classify `403 "Key limit exceeded"` spending-limit responses as billing so model fallback continues instead of stopping on generic auth. (#59892) Thanks @rockcent.
|
||||
|
|
|
|||
|
|
@ -63,6 +63,12 @@ export function extractGeminiCliCredentials(): { clientId: string; clientSecret:
|
|||
return directCredentials;
|
||||
}
|
||||
|
||||
const bundledCredentials = readGeminiCliCredentialsFromBundle(geminiCliDir);
|
||||
if (bundledCredentials) {
|
||||
cachedGeminiCliCredentials = bundledCredentials;
|
||||
return bundledCredentials;
|
||||
}
|
||||
|
||||
const discoveredCredentials = findGeminiCliCredentialsInTree(geminiCliDir, 10);
|
||||
if (discoveredCredentials) {
|
||||
cachedGeminiCliCredentials = discoveredCredentials;
|
||||
|
|
@ -143,12 +149,16 @@ function readGeminiCliCredentialsFile(
|
|||
function parseGeminiCliCredentials(
|
||||
content: string,
|
||||
): { clientId: string; clientSecret: string } | null {
|
||||
const idMatch = content.match(/(\d+-[a-z0-9]+\.apps\.googleusercontent\.com)/);
|
||||
const secretMatch = content.match(/(GOCSPX-[A-Za-z0-9_-]+)/);
|
||||
if (!idMatch || !secretMatch) {
|
||||
const clientId =
|
||||
content.match(/OAUTH_CLIENT_ID\s*=\s*["']([^"']+)["']/)?.[1] ??
|
||||
content.match(/(\d+-[a-z0-9]+\.apps\.googleusercontent\.com)/)?.[1];
|
||||
const clientSecret =
|
||||
content.match(/OAUTH_CLIENT_SECRET\s*=\s*["']([^"']+)["']/)?.[1] ??
|
||||
content.match(/(GOCSPX-[A-Za-z0-9_-]+)/)?.[1];
|
||||
if (!clientId || !clientSecret) {
|
||||
return null;
|
||||
}
|
||||
return { clientId: idMatch[1], clientSecret: secretMatch[1] };
|
||||
return { clientId, clientSecret };
|
||||
}
|
||||
|
||||
function readGeminiCliCredentialsFromKnownPaths(
|
||||
|
|
@ -189,6 +199,31 @@ function readGeminiCliCredentialsFromKnownPaths(
|
|||
return null;
|
||||
}
|
||||
|
||||
function readGeminiCliCredentialsFromBundle(
|
||||
geminiCliDir: string,
|
||||
): { clientId: string; clientSecret: string } | null {
|
||||
const bundleDir = join(geminiCliDir, "bundle");
|
||||
if (!credentialFs.existsSync(bundleDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const entry of credentialFs.readdirSync(bundleDir, { withFileTypes: true })) {
|
||||
if (!entry.isFile() || !entry.name.endsWith(".js")) {
|
||||
continue;
|
||||
}
|
||||
const credentials = readGeminiCliCredentialsFile(join(bundleDir, entry.name));
|
||||
if (credentials) {
|
||||
return credentials;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore bundle traversal failures and fall back to the recursive search.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findGeminiCliCredentialsInTree(
|
||||
dir: string,
|
||||
depth: number,
|
||||
|
|
|
|||
|
|
@ -156,6 +156,39 @@ describe("extractGeminiCliCredentials", () => {
|
|||
}
|
||||
}
|
||||
|
||||
function installBundledNpmLayout(params: { bundleContent: string }) {
|
||||
const binDir = join(rootDir, "fake", "npm-bundle-bin");
|
||||
const geminiPath = join(binDir, "gemini");
|
||||
const resolvedPath = geminiPath;
|
||||
const geminiCliDir = join(binDir, "node_modules", "@google", "gemini-cli");
|
||||
const packageJsonPath = normalizePath(join(geminiCliDir, "package.json"));
|
||||
const bundleDir = join(geminiCliDir, "bundle");
|
||||
const chunkPath = join(bundleDir, "chunk-ABC123.js");
|
||||
|
||||
process.env.PATH = binDir;
|
||||
mockExistsSync.mockImplementation((p: string) => {
|
||||
const normalized = normalizePath(p);
|
||||
return (
|
||||
normalized === normalizePath(geminiPath) ||
|
||||
normalized === packageJsonPath ||
|
||||
normalized === normalizePath(bundleDir)
|
||||
);
|
||||
});
|
||||
mockRealpathSync.mockReturnValue(resolvedPath);
|
||||
mockReaddirSync.mockImplementation((p: string) => {
|
||||
if (normalizePath(String(p)) === normalizePath(bundleDir)) {
|
||||
return [dirent("chunk-ABC123.js", false)];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
mockReadFileSync.mockImplementation((p: string) => {
|
||||
if (normalizePath(String(p)) === normalizePath(chunkPath)) {
|
||||
return params.bundleContent;
|
||||
}
|
||||
throw new Error(`Unexpected read for ${p}`);
|
||||
});
|
||||
}
|
||||
|
||||
function installHomebrewLibexecLayout(params: { oauth2Content: string }) {
|
||||
const brewPrefix = join(rootDir, "opt", "homebrew");
|
||||
const cellarRoot = join(brewPrefix, "Cellar", "gemini-cli", "1.2.3");
|
||||
|
|
@ -339,6 +372,20 @@ describe("extractGeminiCliCredentials", () => {
|
|||
expectFakeCliCredentials(result);
|
||||
});
|
||||
|
||||
it("extracts credentials from bundled npm installs", async () => {
|
||||
installBundledNpmLayout({
|
||||
bundleContent: `
|
||||
const OAUTH_CLIENT_ID = "${FAKE_CLIENT_ID}";
|
||||
const OAUTH_CLIENT_SECRET = "${FAKE_CLIENT_SECRET}";
|
||||
`,
|
||||
});
|
||||
|
||||
clearCredentialsCache();
|
||||
const result = extractGeminiCliCredentials();
|
||||
|
||||
expectFakeCliCredentials(result);
|
||||
});
|
||||
|
||||
it("extracts credentials from Homebrew libexec installs", async () => {
|
||||
installHomebrewLibexecLayout({ oauth2Content: FAKE_OAUTH2_CONTENT });
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue