




















@@ -1,3 +1,4 @@
1+import { createHash } from "node:crypto";
12import { loadAuthProfileStoreWithoutExternalProfiles } from "openclaw/plugin-sdk/agent-runtime";
23import {
34createMigrationItem,
@@ -38,8 +39,12 @@ const HERMES_AUTH_DISPLAY_NAME = "Hermes import";
38393940type HermesCodexAuthCandidate = {
4041access: string;
42+accountId?: string;
4143refresh: string;
44+sourceKind: "hermes-auth-json" | "opencode-auth-json";
45+sourceCredentialIndex?: number;
4246sourceLabel: string;
47+sourcePath: string;
4348updatedAt?: number;
4449};
4550@@ -86,7 +91,7 @@ function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
8691}
8792}
889389-function resolveCodexIdentity(access: string): CodexIdentity {
94+function resolveCodexIdentity(access: string, accountId?: string): CodexIdentity {
9095const payload = decodeJwtPayload(access);
9196const auth = isRecord(payload?.["https://api.openai.com/auth"])
9297 ? payload["https://api.openai.com/auth"]
@@ -95,11 +100,11 @@ function resolveCodexIdentity(access: string): CodexIdentity {
95100 ? payload["https://api.openai.com/profile"]
96101 : {};
97102const email = readString(profile.email);
98-const accountId = readString(auth.chatgpt_account_id);
103+const resolvedAccountId = accountId ?? readString(auth.chatgpt_account_id);
99104const chatgptPlanType = readString(auth.chatgpt_plan_type);
100105if (email) {
101106return {
102- ...(accountId ? { accountId } : {}),
107+ ...(resolvedAccountId ? { accountId: resolvedAccountId } : {}),
103108 ...(chatgptPlanType ? { chatgptPlanType } : {}),
104109 email,
105110profileName: email,
@@ -109,9 +114,10 @@ function resolveCodexIdentity(access: string): CodexIdentity {
109114readString(auth.chatgpt_account_user_id) ??
110115readString(auth.chatgpt_user_id) ??
111116readString(auth.user_id) ??
112-readString(payload?.sub);
117+readString(payload?.sub) ??
118+resolvedAccountId;
113119return {
114- ...(accountId ? { accountId } : {}),
120+ ...(resolvedAccountId ? { accountId: resolvedAccountId } : {}),
115121 ...(chatgptPlanType ? { chatgptPlanType } : {}),
116122 ...(stableSubject
117123 ? { profileName: `id-${Buffer.from(stableSubject).toString("base64url")}` }
@@ -131,7 +137,24 @@ function resolveAccessTokenExpiry(access: string): number | undefined {
131137return undefined;
132138}
133139134-function readProviderTokens(auth: Record<string, unknown>): HermesCodexAuthCandidate | undefined {
140+function sourceCredentialFingerprint(candidate: HermesCodexAuthCandidate): string {
141+const hash = createHash("sha256");
142+for (const part of [
143+candidate.sourceKind,
144+candidate.accountId ?? "",
145+candidate.access,
146+candidate.refresh,
147+]) {
148+hash.update(part);
149+hash.update("\0");
150+}
151+return hash.digest("hex");
152+}
153+154+function readProviderTokens(
155+auth: Record<string, unknown>,
156+sourcePath: string,
157+): HermesCodexAuthCandidate | undefined {
135158const providers = isRecord(auth.providers) ? auth.providers : {};
136159const provider = isRecord(providers[OPENAI_CODEX_PROVIDER_ID])
137160 ? providers[OPENAI_CODEX_PROVIDER_ID]
@@ -145,12 +168,17 @@ function readProviderTokens(auth: Record<string, unknown>): HermesCodexAuthCandi
145168return {
146169 access,
147170 refresh,
171+sourceKind: "hermes-auth-json",
148172sourceLabel: "Hermes active OpenAI Codex provider",
173+ sourcePath,
149174updatedAt: readTimestamp(provider?.last_refresh),
150175};
151176}
152177153-function readPoolTokens(auth: Record<string, unknown>): HermesCodexAuthCandidate[] {
178+function readPoolTokens(
179+auth: Record<string, unknown>,
180+sourcePath: string,
181+): HermesCodexAuthCandidate[] {
154182const pool = isRecord(auth.credential_pool) ? auth.credential_pool : {};
155183const entries = Array.isArray(pool[OPENAI_CODEX_PROVIDER_ID])
156184 ? pool[OPENAI_CODEX_PROVIDER_ID]
@@ -169,7 +197,9 @@ function readPoolTokens(auth: Record<string, unknown>): HermesCodexAuthCandidate
169197candidates.push({
170198 access,
171199 refresh,
200+sourceKind: "hermes-auth-json",
172201sourceLabel: label,
202+ sourcePath,
173203updatedAt: readTimestamp(entry.last_refresh) ?? readTimestamp(entry.last_status_at),
174204});
175205}
@@ -180,7 +210,7 @@ async function readHermesCodexAuthCandidates(
180210authPath: string | undefined,
181211): Promise<HermesCodexAuthCandidate[]> {
182212const raw = await readText(authPath);
183-if (!raw) {
213+if (!raw || !authPath) {
184214return [];
185215}
186216let parsed: unknown;
@@ -192,9 +222,49 @@ async function readHermesCodexAuthCandidates(
192222if (!isRecord(parsed)) {
193223return [];
194224}
195-return [readProviderTokens(parsed), ...readPoolTokens(parsed)]
225+const candidates = [readProviderTokens(parsed, authPath), ...readPoolTokens(parsed, authPath)]
196226.filter((candidate): candidate is HermesCodexAuthCandidate => candidate !== undefined)
197227.toSorted((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0));
228+candidates.forEach((candidate, index) => {
229+candidate.sourceCredentialIndex = index;
230+});
231+return candidates;
232+}
233+234+async function readOpenCodeOpenAICandidates(
235+authPath: string | undefined,
236+): Promise<HermesCodexAuthCandidate[]> {
237+const raw = await readText(authPath);
238+if (!raw || !authPath) {
239+return [];
240+}
241+let parsed: unknown;
242+try {
243+parsed = JSON.parse(raw);
244+} catch {
245+return [];
246+}
247+if (!isRecord(parsed)) {
248+return [];
249+}
250+const openai = isRecord(parsed.openai) ? parsed.openai : undefined;
251+const access = readString(openai?.access);
252+const accountId = readString(openai?.accountId);
253+const refresh = readString(openai?.refresh);
254+if (!access || !refresh) {
255+return [];
256+}
257+return [
258+{
259+ access,
260+ ...(accountId ? { accountId } : {}),
261+ refresh,
262+sourceKind: "opencode-auth-json",
263+sourceCredentialIndex: 0,
264+sourceLabel: "OpenCode OpenAI OAuth credential",
265+sourcePath: authPath,
266+},
267+];
198268}
199269200270function credentialExtra(identity: CodexIdentity): Record<string, unknown> | undefined {
@@ -219,7 +289,7 @@ function buildAuthResult(
219289candidate: HermesCodexAuthCandidate,
220290fallbackProfileName = "hermes-import",
221291): ProviderAuthResult {
222-const identity = resolveCodexIdentity(candidate.access);
292+const identity = resolveCodexIdentity(candidate.access, candidate.accountId);
223293return buildOauthProviderAuthResult({
224294providerId: OPENAI_CODEX_PROVIDER_ID,
225295defaultModel: OPENAI_CODEX_DEFAULT_MODEL,
@@ -243,10 +313,13 @@ function authProfileDedupeKey(profile: HermesCodexAuthProfile): string {
243313return `${profile.credential.provider}:profile:${profile.sourceProfileId}`;
244314}
245315246-async function readHermesCodexAuthProfiles(
247-authPath: string | undefined,
316+async function readCodexAuthProfilesFromSource(
317+source: HermesSource,
248318): Promise<HermesCodexAuthProfile[]> {
249-const candidates = await readHermesCodexAuthCandidates(authPath);
319+const candidates = [
320+ ...(await readHermesCodexAuthCandidates(source.authPath)),
321+ ...(await readOpenCodeOpenAICandidates(source.opencodeAuthPath)),
322+].toSorted((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0));
250323const profiles: HermesCodexAuthProfile[] = [];
251324const seen = new Set<string>();
252325for (const [index, candidate] of candidates.entries()) {
@@ -273,6 +346,24 @@ async function readHermesCodexAuthProfiles(
273346return profiles;
274347}
275348349+async function readCodexAuthProfilesFromPath(params: {
350+sourcePath: string | undefined;
351+sourceKind: unknown;
352+}): Promise<HermesCodexAuthProfile[]> {
353+if (params.sourceKind === "opencode-auth-json") {
354+return await readCodexAuthProfilesFromSource({
355+root: "",
356+archivePaths: [],
357+ ...(params.sourcePath ? { opencodeAuthPath: params.sourcePath } : {}),
358+});
359+}
360+return await readCodexAuthProfilesFromSource({
361+root: "",
362+archivePaths: [],
363+ ...(params.sourcePath ? { authPath: params.sourcePath } : {}),
364+});
365+}
366+276367function findMatchingProfile(
277368store: AuthProfileStore,
278369credential: OAuthCredential,
@@ -305,12 +396,47 @@ function oauthAuthProfileConfig(
305396};
306397}
307398399+function matchesSourceCredentialFingerprint(
400+profile: HermesCodexAuthProfile,
401+fingerprint: string,
402+): boolean {
403+return sourceCredentialFingerprint(profile.candidate) === fingerprint;
404+}
405+406+function findPlannedAuthProfile(params: {
407+profiles: HermesCodexAuthProfile[];
408+sourceProfileId: string;
409+sourceCredentialIndex?: number;
410+sourceCredentialFingerprint?: string;
411+}): HermesCodexAuthProfile | undefined {
412+const bySourceProfileId = params.profiles.find(
413+(entry) => entry.sourceProfileId === params.sourceProfileId,
414+);
415+const fingerprint = params.sourceCredentialFingerprint;
416+if (!fingerprint) {
417+return bySourceProfileId;
418+}
419+if (bySourceProfileId && matchesSourceCredentialFingerprint(bySourceProfileId, fingerprint)) {
420+return bySourceProfileId;
421+}
422+const byIndex =
423+params.sourceCredentialIndex === undefined
424+ ? undefined
425+ : params.profiles.find(
426+(entry) => entry.candidate.sourceCredentialIndex === params.sourceCredentialIndex,
427+);
428+if (byIndex && matchesSourceCredentialFingerprint(byIndex, fingerprint)) {
429+return byIndex;
430+}
431+return params.profiles.find((entry) => matchesSourceCredentialFingerprint(entry, fingerprint));
432+}
433+308434export async function buildAuthItems(params: {
309435ctx: MigrationProviderContext;
310436source: HermesSource;
311437targets: PlannedTargets;
312438}): Promise<MigrationItem[]> {
313-const profiles = await readHermesCodexAuthProfiles(params.source.authPath);
439+const profiles = await readCodexAuthProfilesFromSource(params.source);
314440if (profiles.length === 0) {
315441return [];
316442}
@@ -335,7 +461,7 @@ export async function buildAuthItems(params: {
335461id: itemId,
336462kind: "auth",
337463action: skipped ? "skip" : "create",
338-source: params.source.authPath,
464+source: profile.candidate.sourcePath,
339465target: `${params.targets.agentDir}/auth-profiles.json#${profileId}`,
340466status: skipped ? "skipped" : conflict ? "conflict" : "planned",
341467sensitive: true,
@@ -350,8 +476,12 @@ export async function buildAuthItems(params: {
350476details: {
351477provider: OPENAI_CODEX_PROVIDER_ID,
352478 profileId,
479+ ...(typeof profile.candidate.sourceCredentialIndex === "number"
480+ ? { sourceCredentialIndex: profile.candidate.sourceCredentialIndex }
481+ : {}),
482+sourceCredentialFingerprint: sourceCredentialFingerprint(profile.candidate),
353483sourceProfileId: profile.sourceProfileId,
354-sourceKind: "hermes-auth-json",
484+sourceKind: profile.candidate.sourceKind,
355485sourceLabel: profile.candidate.sourceLabel,
356486},
357487});
@@ -370,12 +500,27 @@ export async function applyAuthItem(
370500const profileId = typeof item.details?.profileId === "string" ? item.details.profileId : "";
371501const sourceProfileId =
372502typeof item.details?.sourceProfileId === "string" ? item.details.sourceProfileId : profileId;
503+const sourceCredentialIndex =
504+typeof item.details?.sourceCredentialIndex === "number"
505+ ? item.details.sourceCredentialIndex
506+ : undefined;
507+const sourceCredentialFingerprint =
508+typeof item.details?.sourceCredentialFingerprint === "string"
509+ ? item.details.sourceCredentialFingerprint
510+ : undefined;
373511if (!source || !profileId) {
374512return markMigrationItemError(item, HERMES_REASON_MISSING_SECRET_METADATA);
375513}
376-const profile = (await readHermesCodexAuthProfiles(source)).find(
377-(entry) => entry.sourceProfileId === sourceProfileId,
378-);
514+const profiles = await readCodexAuthProfilesFromPath({
515+sourcePath: source,
516+sourceKind: item.details?.sourceKind,
517+});
518+const profile = findPlannedAuthProfile({
519+ profiles,
520+ sourceProfileId,
521+ ...(sourceCredentialIndex === undefined ? {} : { sourceCredentialIndex }),
522+ ...(sourceCredentialFingerprint ? { sourceCredentialFingerprint } : {}),
523+});
379524if (!profile) {
380525return markMigrationItemSkipped(item, HERMES_REASON_SECRET_NO_LONGER_PRESENT);
381526}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。