@tokensrc/codex 1.14.21 → 1.14.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/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.js +22 -3
- package/dist/client.js.map +1 -1
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +261 -36
- package/dist/commands.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/managed-credential-v3.d.ts +8 -0
- package/dist/managed-credential-v3.js +26 -0
- package/dist/managed-credential-v3.js.map +1 -1
- package/dist/profile.d.ts +1 -0
- package/dist/profile.js +6 -0
- package/dist/profile.js.map +1 -1
- package/dist/protocol.d.ts +134 -0
- package/dist/protocol.js +50 -1
- package/dist/protocol.js.map +1 -1
- package/dist/request-bound-device-v6-flow.d.ts +12 -4
- package/dist/request-bound-device-v6-flow.js +46 -10
- package/dist/request-bound-device-v6-flow.js.map +1 -1
- package/dist/request-bound-device-v6-health.d.ts +42 -0
- package/dist/request-bound-device-v6-health.js +174 -0
- package/dist/request-bound-device-v6-health.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +2 -2
package/dist/commands.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { hostname } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { PACKAGE_VERSION } from "./branding.js";
|
|
4
5
|
import { getStoragePath, loadAccounts, setStoragePath, setStoragePathDirect } from "codex-multi-auth/storage";
|
|
5
6
|
import { normalizeCodexServiceUrl } from "./client.js";
|
|
6
7
|
import { CodexServiceError } from "./errors.js";
|
|
@@ -19,7 +20,9 @@ import { clearCodexSelection, loadCodexSelection, saveCodexSelection } from "./s
|
|
|
19
20
|
import { installCodexShortcuts, removeCodexShortcuts } from "./shortcuts.js";
|
|
20
21
|
import { isCodexAccountAvailable, isCodexSubscriptionExpired, renderCodexAccountDashboard, selectCodexAccount } from "./tui.js";
|
|
21
22
|
import { ensureCodexV6DeviceCredentials, reconcileCodexV6DeviceAccess, reportCodexV6DeviceCredentials, requireAvailableCodexV6DeviceAccess } from "./request-bound-device-v6-flow.js";
|
|
22
|
-
import {
|
|
23
|
+
import { getOrCreateCodexInstallationId } from "./multi-device-v5-state.js";
|
|
24
|
+
import { readCodexV6BindingState, resetCodexV6LocalState } from "./request-bound-device-v6-state.js";
|
|
25
|
+
import { probeCodexV6OnlineCredentialHealth, reportCodexV6OnlineCredentialHealth } from "./request-bound-device-v6-health.js";
|
|
23
26
|
export const NO_AVAILABLE_CHATGPT_ACCOUNT_MESSAGE = "当前用户没有绑定可用的 ChatGPT 账号。请前往 OA 系统提交 IT11 工单,申请独享或共享的 ChatGPT 个人账号。";
|
|
24
27
|
const SCHEMA_V2_LEGACY_MANAGEMENT_DISABLED = "schema_v2_legacy_management_disabled";
|
|
25
28
|
function findV3Assignment(input) {
|
|
@@ -36,6 +39,30 @@ async function json(command, deps) {
|
|
|
36
39
|
function print(output, value) {
|
|
37
40
|
output.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
38
41
|
}
|
|
42
|
+
function safeDiagnostic(deps, stage, details = {}) {
|
|
43
|
+
if (!deps.safeStartupDiagnostics)
|
|
44
|
+
return;
|
|
45
|
+
const safeDetails = Object.fromEntries(Object.entries(details).filter(([, value]) => value !== undefined));
|
|
46
|
+
deps.errorOutput.write(`[numa-debug] ${JSON.stringify({ stage, ...safeDetails })}\n`);
|
|
47
|
+
}
|
|
48
|
+
async function withSafeDiagnostic(deps, stage, operation) {
|
|
49
|
+
const startedAt = Date.now();
|
|
50
|
+
safeDiagnostic(deps, `${stage}.started`);
|
|
51
|
+
try {
|
|
52
|
+
const result = await operation();
|
|
53
|
+
safeDiagnostic(deps, `${stage}.completed`, { elapsedMs: Date.now() - startedAt });
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const diagnostic = error;
|
|
58
|
+
safeDiagnostic(deps, `${stage}.failed`, {
|
|
59
|
+
elapsedMs: Date.now() - startedAt,
|
|
60
|
+
errorCode: typeof diagnostic?.code === "string" ? diagnostic.code : "command_failed",
|
|
61
|
+
httpStatus: typeof diagnostic?.status === "number" ? diagnostic.status : undefined
|
|
62
|
+
});
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
39
66
|
function success(value) {
|
|
40
67
|
return {
|
|
41
68
|
schemaVersion: CODEX_DISTRIBUTION_SCHEMA_VERSION,
|
|
@@ -216,12 +243,113 @@ function formatV6UserBinding(binding) {
|
|
|
216
243
|
+ ` (${binding.installationId}) token-set=${binding.credentialLineageId ?? "WAITING"}`;
|
|
217
244
|
}
|
|
218
245
|
async function v6DeviceAccess(deps, options = {}, deviceLabel) {
|
|
219
|
-
return reconcileCodexV6DeviceAccess({
|
|
246
|
+
return withSafeDiagnostic(deps, "v6.device_access.reconcile", () => reconcileCodexV6DeviceAccess({
|
|
220
247
|
client: deps.clientFor(serviceConnection(options)),
|
|
221
248
|
tenantId: deps.config.connection.tenantId,
|
|
222
249
|
env: deps.env,
|
|
223
250
|
deviceLabel
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
async function loadV6DeviceCandidates(deps, options = {}, onlineHealth) {
|
|
254
|
+
const client = deps.clientFor(serviceConnection(options));
|
|
255
|
+
const installationId = await getOrCreateCodexInstallationId(deps.env);
|
|
256
|
+
const [requests, directory, localBinding] = await Promise.all([
|
|
257
|
+
client.listV6AccountRequests(),
|
|
258
|
+
client.listAccounts(),
|
|
259
|
+
readCodexV6BindingState(deps.env)
|
|
260
|
+
]);
|
|
261
|
+
const bindingLists = await Promise.all(requests.items.map(async (request) => ({
|
|
262
|
+
request,
|
|
263
|
+
bindings: (await client.listV6DeviceBindings(request.id)).items
|
|
264
|
+
})));
|
|
265
|
+
const allBindings = bindingLists.flatMap(({ bindings }) => bindings);
|
|
266
|
+
const replaced = new Set(allBindings
|
|
267
|
+
.filter((binding) => binding.predecessorBindingId !== null
|
|
268
|
+
&& ["READY", "ACTIVE"].includes(binding.status))
|
|
269
|
+
.map((binding) => binding.predecessorBindingId));
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
const candidates = [];
|
|
272
|
+
for (const { request, bindings } of bindingLists) {
|
|
273
|
+
for (const binding of bindings) {
|
|
274
|
+
if (binding.installationId !== installationId
|
|
275
|
+
|| replaced.has(binding.id)
|
|
276
|
+
|| !["READY", "ACTIVE", "REPLACEMENT_PENDING", "NEEDS_REAUTH"].includes(binding.status)
|
|
277
|
+
|| binding.managedAccountId === null || binding.providerAccountId === null
|
|
278
|
+
|| binding.credentialLineageId === null)
|
|
279
|
+
continue;
|
|
280
|
+
const directoryAccount = directory.accounts.find((account) => account.id === binding.managedAccountId || account.accountId === binding.providerAccountId);
|
|
281
|
+
const credentialExpired = binding.credentialExpiresAt === null
|
|
282
|
+
|| !Number.isFinite(Date.parse(binding.credentialExpiresAt))
|
|
283
|
+
|| Date.parse(binding.credentialExpiresAt) <= now + 60_000;
|
|
284
|
+
const serverRejected = binding.status === "NEEDS_REAUTH"
|
|
285
|
+
|| binding.accessTokenHealth === "REJECTED"
|
|
286
|
+
|| binding.refreshTokenHealth === "REJECTED";
|
|
287
|
+
const currentRejected = localBinding?.bindingId === binding.id
|
|
288
|
+
&& onlineHealth !== undefined
|
|
289
|
+
&& ["TOKEN_REVOKED", "AUTH_REJECTED", "CONNECTOR_TOKEN_REVOKED",
|
|
290
|
+
"CONNECTOR_AUTH_REJECTED"].includes(onlineHealth.status);
|
|
291
|
+
const requestUsable = request.status === "APPROVED"
|
|
292
|
+
&& ["AVAILABLE", "PARTIALLY_AVAILABLE"].includes(request.availability)
|
|
293
|
+
&& request.validUntil !== null && Date.parse(request.validUntil) > now;
|
|
294
|
+
const available = requestUsable
|
|
295
|
+
&& !credentialExpired && !serverRejected && !currentRejected;
|
|
296
|
+
candidates.push({
|
|
297
|
+
request,
|
|
298
|
+
binding,
|
|
299
|
+
account: {
|
|
300
|
+
...(directoryAccount ?? {
|
|
301
|
+
id: binding.managedAccountId,
|
|
302
|
+
accountId: binding.providerAccountId,
|
|
303
|
+
label: binding.providerAccountId,
|
|
304
|
+
isDefault: false
|
|
305
|
+
}),
|
|
306
|
+
// A binding is the selectable identity. The same managed account may
|
|
307
|
+
// legitimately appear under more than one active Request.
|
|
308
|
+
id: binding.id,
|
|
309
|
+
accountId: binding.providerAccountId,
|
|
310
|
+
isDefault: localBinding?.bindingId === binding.id,
|
|
311
|
+
credentialStatus: available ? "AVAILABLE"
|
|
312
|
+
: credentialExpired || serverRejected || currentRejected
|
|
313
|
+
? "NEEDS_REAUTH" : "TEMPORARILY_UNAVAILABLE",
|
|
314
|
+
lineageStatus: available ? "ACTIVE" : "NEEDS_REAUTH"
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
installationId,
|
|
321
|
+
candidates,
|
|
322
|
+
...(localBinding ? { currentBindingId: localBinding.bindingId } : {})
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
async function selectV6DeviceCandidate(deps, requested, options = {}) {
|
|
326
|
+
const inventory = await loadV6DeviceCandidates(deps, options);
|
|
327
|
+
let resolvedRequest = requested;
|
|
328
|
+
if (requested) {
|
|
329
|
+
const matches = inventory.candidates.filter(({ request, binding, account }) => [
|
|
330
|
+
request.id,
|
|
331
|
+
binding.id,
|
|
332
|
+
binding.managedAccountId,
|
|
333
|
+
binding.providerAccountId,
|
|
334
|
+
binding.credentialLineageId,
|
|
335
|
+
account.label
|
|
336
|
+
].includes(requested));
|
|
337
|
+
if (matches.length > 1)
|
|
338
|
+
throw new CodexServiceError(`账号选择 ${requested} 不唯一,请使用 Request ID 或 Binding ID。`, "codex_account_reference_ambiguous", 409);
|
|
339
|
+
if (matches.length === 1)
|
|
340
|
+
resolvedRequest = matches[0].binding.id;
|
|
341
|
+
}
|
|
342
|
+
const chosen = await selectCodexAccount(inventory.candidates.map(({ account }) => account), {
|
|
343
|
+
requested: resolvedRequest,
|
|
344
|
+
selectedAccountId: inventory.currentBindingId,
|
|
345
|
+
nonInteractiveCommand: "numa codex use",
|
|
346
|
+
noAvailableMessage: "当前设备没有可领取或可用的独立 Token Set。",
|
|
347
|
+
output: deps.errorOutput
|
|
224
348
|
});
|
|
349
|
+
const candidate = inventory.candidates.find(({ binding }) => binding.id === chosen.id);
|
|
350
|
+
if (!candidate)
|
|
351
|
+
throw new CodexServiceError("选择的 Request/设备 Binding 已不在候选列表中。", "codex_device_binding_not_found", 404);
|
|
352
|
+
return candidate;
|
|
225
353
|
}
|
|
226
354
|
async function loadAccountData(deps, options = {}, allowV4Replacement = false) {
|
|
227
355
|
const previous = await loadCodexSelection(deps.config.persistence.configDirectory);
|
|
@@ -409,32 +537,78 @@ async function installCredentials(deps, selection, service, allowV4Replacement =
|
|
|
409
537
|
async function selectedCredentials(deps, requested, options = {}) {
|
|
410
538
|
if (requestBoundDeviceCredentialsEnabled(deps.config)) {
|
|
411
539
|
const client = deps.clientFor(serviceConnection(options));
|
|
412
|
-
const
|
|
540
|
+
const candidate = requested
|
|
541
|
+
? await selectV6DeviceCandidate(deps, requested, options)
|
|
542
|
+
: undefined;
|
|
543
|
+
let ensured = await withSafeDiagnostic(deps, "v6.device_credentials.ensure", () => ensureCodexV6DeviceCredentials({
|
|
413
544
|
client,
|
|
414
545
|
tenantId: deps.config.connection.tenantId,
|
|
415
|
-
env: deps.env
|
|
416
|
-
|
|
546
|
+
env: deps.env,
|
|
547
|
+
...(candidate ? { target: { request: candidate.request, binding: candidate.binding } } : {})
|
|
548
|
+
}));
|
|
417
549
|
if (ensured.replacementDeferredWarningCode) {
|
|
418
550
|
deps.errorOutput.write(`新设备 Token Set 暂时无法交付 (${ensured.replacementDeferredWarningCode});`
|
|
419
551
|
+ "本次继续使用尚有效的旧 Token Set,稍后会自动重试。\n");
|
|
420
552
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
553
|
+
let binding = ensured.binding;
|
|
554
|
+
let health = await withSafeDiagnostic(deps, "v6.online_health.probe", () => probeCodexV6OnlineCredentialHealth({
|
|
555
|
+
env: deps.env,
|
|
556
|
+
fetch: deps.fetch
|
|
557
|
+
}));
|
|
558
|
+
const healthRejected = () => ["TOKEN_REVOKED", "AUTH_REJECTED", "CONNECTOR_TOKEN_REVOKED",
|
|
559
|
+
"CONNECTOR_AUTH_REJECTED"].includes(health.status);
|
|
560
|
+
let report;
|
|
561
|
+
if (healthRejected()) {
|
|
562
|
+
report = await reportCodexV6OnlineCredentialHealth({ client, health, env: deps.env });
|
|
563
|
+
if (report.replacementBinding) {
|
|
564
|
+
const replacementBinding = report.replacementBinding;
|
|
565
|
+
const request = (await client.getV6AccountRequest(replacementBinding.requestId)).request;
|
|
566
|
+
ensured = await withSafeDiagnostic(deps, "v6.device_credentials.replace", () => ensureCodexV6DeviceCredentials({
|
|
567
|
+
client,
|
|
568
|
+
tenantId: deps.config.connection.tenantId,
|
|
569
|
+
env: deps.env,
|
|
570
|
+
target: { request, binding: replacementBinding }
|
|
571
|
+
}));
|
|
572
|
+
binding = ensured.binding;
|
|
573
|
+
health = await withSafeDiagnostic(deps, "v6.online_health.replacement_probe", () => probeCodexV6OnlineCredentialHealth({ env: deps.env, fetch: deps.fetch }));
|
|
574
|
+
if (!healthRejected()) {
|
|
575
|
+
deps.errorOutput.write(health.status === "HEALTHY"
|
|
576
|
+
? "已自动安装并验证服务端换发的新设备 Token Set。\n"
|
|
577
|
+
: "已自动安装服务端换发的新设备 Token Set;在线探知结果暂不确定。\n");
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
report = await reportCodexV6OnlineCredentialHealth({ client, health, env: deps.env });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (healthRejected()) {
|
|
585
|
+
const connectorOnly = health.status.startsWith("CONNECTOR_");
|
|
586
|
+
throw new CodexServiceError(`当前设备 Token Set 的${connectorOnly ? "关键 ChatGPT/connector 授权" : "主 Codex 授权"}`
|
|
587
|
+
+ `已被上游拒绝${health.safeErrorCode ? ` (${health.safeErrorCode})` : ""};`
|
|
588
|
+
+ (report?.reported
|
|
589
|
+
? "失效状态已安全上报,系统将进入换发流程。"
|
|
590
|
+
: `状态上报未完成${report?.warningCode ? ` (${report.warningCode}`
|
|
591
|
+
+ `${report.warningStatus ? `/HTTP-${report.warningStatus}` : ""})` : ""};`)
|
|
592
|
+
+ "服务器上的“已交付”不代表实时可用,请由管理员换发 Token Set。", ["TOKEN_REVOKED", "CONNECTOR_TOKEN_REVOKED"].includes(health.status)
|
|
593
|
+
? "codex_device_token_revoked" : "codex_device_credential_rejected", (connectorOnly ? health.connectorHttpStatus : health.codexApiHttpStatus) ?? 401);
|
|
430
594
|
}
|
|
595
|
+
if (["FORBIDDEN", "UNREACHABLE"].includes(health.status)) {
|
|
596
|
+
deps.errorOutput.write(`Codex 在线凭据探知暂不可用 (${health.status.toLowerCase()}`
|
|
597
|
+
+ `${health.codexApiHttpStatus ? `, HTTP ${health.codexApiHttpStatus}` : ""});本次仅依据本地与服务端证据继续。\n`);
|
|
598
|
+
}
|
|
599
|
+
const previous = await loadCodexSelection(deps.config.persistence.configDirectory);
|
|
600
|
+
const directoryAccount = candidate?.binding.id === binding.id ? candidate.account
|
|
601
|
+
: (await client.listAccounts()).accounts.find((account) => account.id === binding.managedAccountId || account.accountId === binding.providerAccountId);
|
|
602
|
+
const label = directoryAccount?.label
|
|
603
|
+
?? (previous?.account.bindingId === binding.id ? previous.account.label : undefined)
|
|
604
|
+
?? binding.providerAccountId;
|
|
431
605
|
const selection = await saveCodexSelection(deps.config.persistence.configDirectory, {
|
|
432
606
|
serviceUrl: options.server ?? deps.config.connection.serverUrl,
|
|
433
607
|
allowInsecurePoc: Boolean(options.insecurePoc || options.insecureHttp || deps.config.connection.allowInsecureHttp),
|
|
434
608
|
account: {
|
|
435
609
|
id: binding.managedAccountId,
|
|
436
610
|
accountId: binding.providerAccountId,
|
|
437
|
-
label
|
|
611
|
+
label,
|
|
438
612
|
distributionMode: "REQUEST_BOUND_DEVICE_V6",
|
|
439
613
|
managedAccountId: binding.managedAccountId,
|
|
440
614
|
providerAccountId: binding.providerAccountId,
|
|
@@ -598,7 +772,11 @@ async function launchWithSelectedCredentials(deps, selected, launch) {
|
|
|
598
772
|
env: deps.env
|
|
599
773
|
});
|
|
600
774
|
if (!report.reported && report.warningCode) {
|
|
601
|
-
deps.errorOutput.write(`Codex 独立设备 Token
|
|
775
|
+
deps.errorOutput.write(`Codex 独立设备 Token 代际上报失败:stage=${report.warningStage ?? "UNKNOWN"}`
|
|
776
|
+
+ `${report.warningStatus ? `, HTTP ${report.warningStatus}` : ""}`
|
|
777
|
+
+ `, code=${report.warningCode}。`
|
|
778
|
+
+ `${report.pendingCredentialChange ? "已检测到本地代际变化,状态与重试证据均已保留;" : ""}`
|
|
779
|
+
+ "其他设备不会受影响。\n");
|
|
602
780
|
}
|
|
603
781
|
}
|
|
604
782
|
}
|
|
@@ -653,6 +831,27 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
653
831
|
? program
|
|
654
832
|
: program.command("codex").description("Select an account and launch the official Codex client");
|
|
655
833
|
const includeAuthCommands = options.includeAuthCommands ?? options.nested === false;
|
|
834
|
+
codex.hook("preAction", (_command, actionCommand) => {
|
|
835
|
+
let serviceOrigin = deps.config.connection.serverUrl;
|
|
836
|
+
try {
|
|
837
|
+
serviceOrigin = new URL(serviceOrigin).origin;
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
serviceOrigin = "invalid";
|
|
841
|
+
}
|
|
842
|
+
safeDiagnostic(deps, "startup", {
|
|
843
|
+
cliVersion: PACKAGE_VERSION,
|
|
844
|
+
command: actionCommand.name(),
|
|
845
|
+
credentialModel: requestBoundDeviceCredentialsEnabled(deps.config)
|
|
846
|
+
? "REQUEST_BOUND_DEVICE_V6"
|
|
847
|
+
: "LEGACY",
|
|
848
|
+
profileId: deps.config.profile?.profileId,
|
|
849
|
+
profileRevision: deps.config.profile?.revision,
|
|
850
|
+
environment: deps.config.profile?.environment,
|
|
851
|
+
profileStale: deps.config.profile?.stale,
|
|
852
|
+
serviceOrigin
|
|
853
|
+
});
|
|
854
|
+
});
|
|
656
855
|
if (includeAuthCommands)
|
|
657
856
|
codex.command("login").description("Log in with OIDC").action(async (_options, command) => {
|
|
658
857
|
if (!deps.auth.login)
|
|
@@ -776,31 +975,45 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
776
975
|
serviceOptions(codex.command("list").description("List available Codex accounts without downloading credentials"))
|
|
777
976
|
.action(async (opts, command) => {
|
|
778
977
|
if (requestBoundDeviceCredentialsEnabled(deps.config)) {
|
|
779
|
-
const client = deps.clientFor(serviceConnection(opts));
|
|
780
978
|
const access = await v6DeviceAccess(deps, opts);
|
|
979
|
+
const localHealth = await probeCodexV6OnlineCredentialHealth({
|
|
980
|
+
env: deps.env,
|
|
981
|
+
fetch: deps.fetch
|
|
982
|
+
});
|
|
983
|
+
const rejected = ["TOKEN_REVOKED", "AUTH_REJECTED", "CONNECTOR_TOKEN_REVOKED",
|
|
984
|
+
"CONNECTOR_AUTH_REJECTED"].includes(localHealth.status);
|
|
985
|
+
const healthReport = rejected
|
|
986
|
+
? await reportCodexV6OnlineCredentialHealth({
|
|
987
|
+
client: deps.clientFor(serviceConnection(opts)),
|
|
988
|
+
health: localHealth,
|
|
989
|
+
env: deps.env
|
|
990
|
+
})
|
|
991
|
+
: undefined;
|
|
992
|
+
const inventory = await loadV6DeviceCandidates(deps, opts, localHealth);
|
|
781
993
|
if (await json(command, deps)) {
|
|
782
994
|
print(deps.output, success({
|
|
783
995
|
credentialModel: "REQUEST_BOUND_DEVICE_V6",
|
|
784
996
|
installationId: access.installationId,
|
|
785
|
-
...access.response
|
|
997
|
+
...access.response,
|
|
998
|
+
candidates: inventory.candidates.map(({ request, binding, account }) => ({
|
|
999
|
+
request,
|
|
1000
|
+
binding,
|
|
1001
|
+
account
|
|
1002
|
+
})),
|
|
1003
|
+
localCredentialHealth: localHealth,
|
|
1004
|
+
...(healthReport ? { localCredentialHealthReport: healthReport } : {})
|
|
786
1005
|
}));
|
|
787
1006
|
return;
|
|
788
1007
|
}
|
|
789
|
-
if (access.response.accessStatus !== "AVAILABLE") {
|
|
1008
|
+
if (inventory.candidates.length === 0 && access.response.accessStatus !== "AVAILABLE") {
|
|
790
1009
|
requireAvailableCodexV6DeviceAccess(access);
|
|
791
1010
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
...currentAccount,
|
|
799
|
-
credentialStatus: "AVAILABLE",
|
|
800
|
-
lineageStatus: "ACTIVE"
|
|
801
|
-
};
|
|
802
|
-
deps.output.write(renderCodexAccountDashboard([availableAccount], availableAccount.id));
|
|
803
|
-
deps.output.write(`设备凭据 ${binding.status === "ACTIVE" || binding.status === "READY" ? "可用" : binding.status}\n`);
|
|
1011
|
+
deps.output.write(renderCodexAccountDashboard(inventory.candidates.map(({ account }) => account), inventory.currentBindingId ?? access.response.binding?.id ?? undefined));
|
|
1012
|
+
deps.output.write(`设备凭据 ${rejected ? "已失效,需换发" : "可用"}\n`);
|
|
1013
|
+
if (healthReport?.reported)
|
|
1014
|
+
deps.output.write(healthReport.replacementPending
|
|
1015
|
+
? "失效状态已上报;同账号 Token Set 暂无库存,正在等待换发。\n"
|
|
1016
|
+
: "失效状态已上报;新的设备 Token Set 将在使用时自动换发。\n");
|
|
804
1017
|
if (access.response.migration === "AUTO_MIGRATED") {
|
|
805
1018
|
deps.output.write("既有本机凭据已自动转换为该申请下的第一个独立设备绑定。\n");
|
|
806
1019
|
}
|
|
@@ -831,7 +1044,8 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
831
1044
|
if (requestBoundDeviceCredentialsEnabled(deps.config)) {
|
|
832
1045
|
if (opts.takeover)
|
|
833
1046
|
throw new CodexServiceError("v6 每台设备使用独立 Token Set,不支持接管其他设备的凭据。", "codex_device_takeover_not_allowed", 409);
|
|
834
|
-
const
|
|
1047
|
+
const target = account ?? (await selectV6DeviceCandidate(deps, undefined, opts)).binding.id;
|
|
1048
|
+
const selected = await selectedCredentials(deps, target, opts);
|
|
835
1049
|
if (await json(command, deps))
|
|
836
1050
|
print(deps.output, success({
|
|
837
1051
|
account: selected.selection.account,
|
|
@@ -862,7 +1076,10 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
862
1076
|
.action(async (args, opts) => {
|
|
863
1077
|
if (opts.selectAccount)
|
|
864
1078
|
await clearCodexSelection(deps.config.persistence.configDirectory);
|
|
865
|
-
const
|
|
1079
|
+
const target = requestBoundDeviceCredentialsEnabled(deps.config) && opts.selectAccount
|
|
1080
|
+
? (await selectV6DeviceCandidate(deps, opts.account, opts)).binding.id
|
|
1081
|
+
: opts.account;
|
|
1082
|
+
const selected = await selectedCredentials(deps, target, opts);
|
|
866
1083
|
deps.errorOutput.write(`Using ChatGPT account: ${selected.selection.account.label} (${selected.selection.account.id})\n`);
|
|
867
1084
|
process.exitCode = await launchWithSelectedCredentials(deps, selected, (signal) => deps.run(args, { signal }));
|
|
868
1085
|
});
|
|
@@ -874,7 +1091,10 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
874
1091
|
throw new Error("官方 Codex 桌面 App 仅支持 macOS 和 Windows。");
|
|
875
1092
|
if (opts.selectAccount)
|
|
876
1093
|
await clearCodexSelection(deps.config.persistence.configDirectory);
|
|
877
|
-
const
|
|
1094
|
+
const target = requestBoundDeviceCredentialsEnabled(deps.config) && opts.selectAccount
|
|
1095
|
+
? (await selectV6DeviceCandidate(deps, opts.account, opts)).binding.id
|
|
1096
|
+
: opts.account;
|
|
1097
|
+
const selected = await selectedCredentials(deps, target, opts);
|
|
878
1098
|
deps.errorOutput.write(`Using ChatGPT account: ${selected.selection.account.label} (${selected.selection.account.id})\n`);
|
|
879
1099
|
process.exitCode = await launchWithSelectedCredentials(deps, selected, (signal) => deps.app(workspace, { signal }));
|
|
880
1100
|
});
|
|
@@ -976,13 +1196,18 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
976
1196
|
? "device_token_set.generation_reported"
|
|
977
1197
|
: "device_token_set.current",
|
|
978
1198
|
at: new Date().toISOString(),
|
|
979
|
-
...(result.warningCode ? { warningCode: result.warningCode } : {})
|
|
1199
|
+
...(result.warningCode ? { warningCode: result.warningCode } : {}),
|
|
1200
|
+
...(result.warningStage ? { warningStage: result.warningStage } : {}),
|
|
1201
|
+
...(result.warningStatus ? { warningStatus: result.warningStatus } : {}),
|
|
1202
|
+
...(result.pendingCredentialChange ? { pendingCredentialChange: true } : {})
|
|
980
1203
|
};
|
|
981
1204
|
if (await json(command, deps))
|
|
982
1205
|
print(deps.output, success(event));
|
|
983
1206
|
else
|
|
984
1207
|
deps.output.write(`${event.at} ${event.level} ${event.event}`
|
|
985
|
-
+ `${result.warningCode ? ` (${result.
|
|
1208
|
+
+ `${result.warningCode ? ` (${result.warningStage ?? "UNKNOWN"}`
|
|
1209
|
+
+ `${result.warningStatus ? `/HTTP-${result.warningStatus}` : ""}`
|
|
1210
|
+
+ `/${result.warningCode})` : ""}; other devices unaffected\n`);
|
|
986
1211
|
if (opts.once)
|
|
987
1212
|
break;
|
|
988
1213
|
await new Promise((resolve) => setTimeout(resolve, interval));
|