@tokensrc/codex 1.14.22 → 1.14.24
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 +14 -2
- 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 +265 -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/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 +44 -9
- package/dist/request-bound-device-v6-flow.js.map +1 -1
- package/dist/request-bound-device-v6-health.d.ts +61 -0
- package/dist/request-bound-device-v6-health.js +233 -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", "SUBSCRIPTION_FREE"].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,82 @@ 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
|
+
const subscriptionUnavailable = () => health.status === "SUBSCRIPTION_FREE";
|
|
561
|
+
let report;
|
|
562
|
+
if (healthRejected()) {
|
|
563
|
+
report = await reportCodexV6OnlineCredentialHealth({ client, health, env: deps.env });
|
|
564
|
+
if (report.replacementBinding) {
|
|
565
|
+
const replacementBinding = report.replacementBinding;
|
|
566
|
+
const request = (await client.getV6AccountRequest(replacementBinding.requestId)).request;
|
|
567
|
+
ensured = await withSafeDiagnostic(deps, "v6.device_credentials.replace", () => ensureCodexV6DeviceCredentials({
|
|
568
|
+
client,
|
|
569
|
+
tenantId: deps.config.connection.tenantId,
|
|
570
|
+
env: deps.env,
|
|
571
|
+
target: { request, binding: replacementBinding }
|
|
572
|
+
}));
|
|
573
|
+
binding = ensured.binding;
|
|
574
|
+
health = await withSafeDiagnostic(deps, "v6.online_health.replacement_probe", () => probeCodexV6OnlineCredentialHealth({ env: deps.env, fetch: deps.fetch }));
|
|
575
|
+
if (!healthRejected()) {
|
|
576
|
+
deps.errorOutput.write(health.status === "HEALTHY"
|
|
577
|
+
? "已自动安装并验证服务端换发的新设备 Token Set。\n"
|
|
578
|
+
: "已自动安装服务端换发的新设备 Token Set;在线探知结果暂不确定。\n");
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
report = await reportCodexV6OnlineCredentialHealth({ client, health, env: deps.env });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (healthRejected()) {
|
|
586
|
+
const connectorOnly = health.status.startsWith("CONNECTOR_");
|
|
587
|
+
throw new CodexServiceError(`当前设备 Token Set 的${connectorOnly ? "关键 ChatGPT/connector 授权" : "主 Codex 授权"}`
|
|
588
|
+
+ `已被上游拒绝${health.safeErrorCode ? ` (${health.safeErrorCode})` : ""};`
|
|
589
|
+
+ (report?.reported
|
|
590
|
+
? "失效状态已安全上报,系统将进入换发流程。"
|
|
591
|
+
: `状态上报未完成${report?.warningCode ? ` (${report.warningCode}`
|
|
592
|
+
+ `${report.warningStatus ? `/HTTP-${report.warningStatus}` : ""})` : ""};`)
|
|
593
|
+
+ "服务器上的“已交付”不代表实时可用,请由管理员换发 Token Set。", ["TOKEN_REVOKED", "CONNECTOR_TOKEN_REVOKED"].includes(health.status)
|
|
594
|
+
? "codex_device_token_revoked" : "codex_device_credential_rejected", (connectorOnly ? health.connectorHttpStatus : health.codexApiHttpStatus) ?? 401);
|
|
430
595
|
}
|
|
596
|
+
if (subscriptionUnavailable()) {
|
|
597
|
+
throw new CodexServiceError(`当前 ChatGPT 账号为 Free 套餐${health.planType ? ` (${health.planType})` : ""},无法用于 Numa;请由管理员更换可用的付费账号。`, "codex_device_subscription_free", 403);
|
|
598
|
+
}
|
|
599
|
+
if (["FORBIDDEN", "UNREACHABLE"].includes(health.status)) {
|
|
600
|
+
deps.errorOutput.write(`Codex 在线凭据探知暂不可用 (${health.status.toLowerCase()}`
|
|
601
|
+
+ `${health.codexApiHttpStatus ? `, HTTP ${health.codexApiHttpStatus}` : ""});本次仅依据本地与服务端证据继续。\n`);
|
|
602
|
+
}
|
|
603
|
+
const previous = await loadCodexSelection(deps.config.persistence.configDirectory);
|
|
604
|
+
const directoryAccount = candidate?.binding.id === binding.id ? candidate.account
|
|
605
|
+
: (await client.listAccounts()).accounts.find((account) => account.id === binding.managedAccountId || account.accountId === binding.providerAccountId);
|
|
606
|
+
const label = directoryAccount?.label
|
|
607
|
+
?? (previous?.account.bindingId === binding.id ? previous.account.label : undefined)
|
|
608
|
+
?? binding.providerAccountId;
|
|
431
609
|
const selection = await saveCodexSelection(deps.config.persistence.configDirectory, {
|
|
432
610
|
serviceUrl: options.server ?? deps.config.connection.serverUrl,
|
|
433
611
|
allowInsecurePoc: Boolean(options.insecurePoc || options.insecureHttp || deps.config.connection.allowInsecureHttp),
|
|
434
612
|
account: {
|
|
435
613
|
id: binding.managedAccountId,
|
|
436
614
|
accountId: binding.providerAccountId,
|
|
437
|
-
label
|
|
615
|
+
label,
|
|
438
616
|
distributionMode: "REQUEST_BOUND_DEVICE_V6",
|
|
439
617
|
managedAccountId: binding.managedAccountId,
|
|
440
618
|
providerAccountId: binding.providerAccountId,
|
|
@@ -598,7 +776,11 @@ async function launchWithSelectedCredentials(deps, selected, launch) {
|
|
|
598
776
|
env: deps.env
|
|
599
777
|
});
|
|
600
778
|
if (!report.reported && report.warningCode) {
|
|
601
|
-
deps.errorOutput.write(`Codex 独立设备 Token
|
|
779
|
+
deps.errorOutput.write(`Codex 独立设备 Token 代际上报失败:stage=${report.warningStage ?? "UNKNOWN"}`
|
|
780
|
+
+ `${report.warningStatus ? `, HTTP ${report.warningStatus}` : ""}`
|
|
781
|
+
+ `, code=${report.warningCode}。`
|
|
782
|
+
+ `${report.pendingCredentialChange ? "已检测到本地代际变化,状态与重试证据均已保留;" : ""}`
|
|
783
|
+
+ "其他设备不会受影响。\n");
|
|
602
784
|
}
|
|
603
785
|
}
|
|
604
786
|
}
|
|
@@ -653,6 +835,27 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
653
835
|
? program
|
|
654
836
|
: program.command("codex").description("Select an account and launch the official Codex client");
|
|
655
837
|
const includeAuthCommands = options.includeAuthCommands ?? options.nested === false;
|
|
838
|
+
codex.hook("preAction", (_command, actionCommand) => {
|
|
839
|
+
let serviceOrigin = deps.config.connection.serverUrl;
|
|
840
|
+
try {
|
|
841
|
+
serviceOrigin = new URL(serviceOrigin).origin;
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
serviceOrigin = "invalid";
|
|
845
|
+
}
|
|
846
|
+
safeDiagnostic(deps, "startup", {
|
|
847
|
+
cliVersion: PACKAGE_VERSION,
|
|
848
|
+
command: actionCommand.name(),
|
|
849
|
+
credentialModel: requestBoundDeviceCredentialsEnabled(deps.config)
|
|
850
|
+
? "REQUEST_BOUND_DEVICE_V6"
|
|
851
|
+
: "LEGACY",
|
|
852
|
+
profileId: deps.config.profile?.profileId,
|
|
853
|
+
profileRevision: deps.config.profile?.revision,
|
|
854
|
+
environment: deps.config.profile?.environment,
|
|
855
|
+
profileStale: deps.config.profile?.stale,
|
|
856
|
+
serviceOrigin
|
|
857
|
+
});
|
|
858
|
+
});
|
|
656
859
|
if (includeAuthCommands)
|
|
657
860
|
codex.command("login").description("Log in with OIDC").action(async (_options, command) => {
|
|
658
861
|
if (!deps.auth.login)
|
|
@@ -776,31 +979,45 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
776
979
|
serviceOptions(codex.command("list").description("List available Codex accounts without downloading credentials"))
|
|
777
980
|
.action(async (opts, command) => {
|
|
778
981
|
if (requestBoundDeviceCredentialsEnabled(deps.config)) {
|
|
779
|
-
const client = deps.clientFor(serviceConnection(opts));
|
|
780
982
|
const access = await v6DeviceAccess(deps, opts);
|
|
983
|
+
const localHealth = await probeCodexV6OnlineCredentialHealth({
|
|
984
|
+
env: deps.env,
|
|
985
|
+
fetch: deps.fetch
|
|
986
|
+
});
|
|
987
|
+
const rejected = ["TOKEN_REVOKED", "AUTH_REJECTED", "CONNECTOR_TOKEN_REVOKED",
|
|
988
|
+
"CONNECTOR_AUTH_REJECTED"].includes(localHealth.status);
|
|
989
|
+
const healthReport = rejected
|
|
990
|
+
? await reportCodexV6OnlineCredentialHealth({
|
|
991
|
+
client: deps.clientFor(serviceConnection(opts)),
|
|
992
|
+
health: localHealth,
|
|
993
|
+
env: deps.env
|
|
994
|
+
})
|
|
995
|
+
: undefined;
|
|
996
|
+
const inventory = await loadV6DeviceCandidates(deps, opts, localHealth);
|
|
781
997
|
if (await json(command, deps)) {
|
|
782
998
|
print(deps.output, success({
|
|
783
999
|
credentialModel: "REQUEST_BOUND_DEVICE_V6",
|
|
784
1000
|
installationId: access.installationId,
|
|
785
|
-
...access.response
|
|
1001
|
+
...access.response,
|
|
1002
|
+
candidates: inventory.candidates.map(({ request, binding, account }) => ({
|
|
1003
|
+
request,
|
|
1004
|
+
binding,
|
|
1005
|
+
account
|
|
1006
|
+
})),
|
|
1007
|
+
localCredentialHealth: localHealth,
|
|
1008
|
+
...(healthReport ? { localCredentialHealthReport: healthReport } : {})
|
|
786
1009
|
}));
|
|
787
1010
|
return;
|
|
788
1011
|
}
|
|
789
|
-
if (access.response.accessStatus !== "AVAILABLE") {
|
|
1012
|
+
if (inventory.candidates.length === 0 && access.response.accessStatus !== "AVAILABLE") {
|
|
790
1013
|
requireAvailableCodexV6DeviceAccess(access);
|
|
791
1014
|
}
|
|
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`);
|
|
1015
|
+
deps.output.write(renderCodexAccountDashboard(inventory.candidates.map(({ account }) => account), inventory.currentBindingId ?? access.response.binding?.id ?? undefined));
|
|
1016
|
+
deps.output.write(`设备凭据 ${rejected ? "已失效,需换发" : "可用"}\n`);
|
|
1017
|
+
if (healthReport?.reported)
|
|
1018
|
+
deps.output.write(healthReport.replacementPending
|
|
1019
|
+
? "失效状态已上报;同账号 Token Set 暂无库存,正在等待换发。\n"
|
|
1020
|
+
: "失效状态已上报;新的设备 Token Set 将在使用时自动换发。\n");
|
|
804
1021
|
if (access.response.migration === "AUTO_MIGRATED") {
|
|
805
1022
|
deps.output.write("既有本机凭据已自动转换为该申请下的第一个独立设备绑定。\n");
|
|
806
1023
|
}
|
|
@@ -831,7 +1048,8 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
831
1048
|
if (requestBoundDeviceCredentialsEnabled(deps.config)) {
|
|
832
1049
|
if (opts.takeover)
|
|
833
1050
|
throw new CodexServiceError("v6 每台设备使用独立 Token Set,不支持接管其他设备的凭据。", "codex_device_takeover_not_allowed", 409);
|
|
834
|
-
const
|
|
1051
|
+
const target = account ?? (await selectV6DeviceCandidate(deps, undefined, opts)).binding.id;
|
|
1052
|
+
const selected = await selectedCredentials(deps, target, opts);
|
|
835
1053
|
if (await json(command, deps))
|
|
836
1054
|
print(deps.output, success({
|
|
837
1055
|
account: selected.selection.account,
|
|
@@ -862,7 +1080,10 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
862
1080
|
.action(async (args, opts) => {
|
|
863
1081
|
if (opts.selectAccount)
|
|
864
1082
|
await clearCodexSelection(deps.config.persistence.configDirectory);
|
|
865
|
-
const
|
|
1083
|
+
const target = requestBoundDeviceCredentialsEnabled(deps.config) && opts.selectAccount
|
|
1084
|
+
? (await selectV6DeviceCandidate(deps, opts.account, opts)).binding.id
|
|
1085
|
+
: opts.account;
|
|
1086
|
+
const selected = await selectedCredentials(deps, target, opts);
|
|
866
1087
|
deps.errorOutput.write(`Using ChatGPT account: ${selected.selection.account.label} (${selected.selection.account.id})\n`);
|
|
867
1088
|
process.exitCode = await launchWithSelectedCredentials(deps, selected, (signal) => deps.run(args, { signal }));
|
|
868
1089
|
});
|
|
@@ -874,7 +1095,10 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
874
1095
|
throw new Error("官方 Codex 桌面 App 仅支持 macOS 和 Windows。");
|
|
875
1096
|
if (opts.selectAccount)
|
|
876
1097
|
await clearCodexSelection(deps.config.persistence.configDirectory);
|
|
877
|
-
const
|
|
1098
|
+
const target = requestBoundDeviceCredentialsEnabled(deps.config) && opts.selectAccount
|
|
1099
|
+
? (await selectV6DeviceCandidate(deps, opts.account, opts)).binding.id
|
|
1100
|
+
: opts.account;
|
|
1101
|
+
const selected = await selectedCredentials(deps, target, opts);
|
|
878
1102
|
deps.errorOutput.write(`Using ChatGPT account: ${selected.selection.account.label} (${selected.selection.account.id})\n`);
|
|
879
1103
|
process.exitCode = await launchWithSelectedCredentials(deps, selected, (signal) => deps.app(workspace, { signal }));
|
|
880
1104
|
});
|
|
@@ -976,13 +1200,18 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
976
1200
|
? "device_token_set.generation_reported"
|
|
977
1201
|
: "device_token_set.current",
|
|
978
1202
|
at: new Date().toISOString(),
|
|
979
|
-
...(result.warningCode ? { warningCode: result.warningCode } : {})
|
|
1203
|
+
...(result.warningCode ? { warningCode: result.warningCode } : {}),
|
|
1204
|
+
...(result.warningStage ? { warningStage: result.warningStage } : {}),
|
|
1205
|
+
...(result.warningStatus ? { warningStatus: result.warningStatus } : {}),
|
|
1206
|
+
...(result.pendingCredentialChange ? { pendingCredentialChange: true } : {})
|
|
980
1207
|
};
|
|
981
1208
|
if (await json(command, deps))
|
|
982
1209
|
print(deps.output, success(event));
|
|
983
1210
|
else
|
|
984
1211
|
deps.output.write(`${event.at} ${event.level} ${event.event}`
|
|
985
|
-
+ `${result.warningCode ? ` (${result.
|
|
1212
|
+
+ `${result.warningCode ? ` (${result.warningStage ?? "UNKNOWN"}`
|
|
1213
|
+
+ `${result.warningStatus ? `/HTTP-${result.warningStatus}` : ""}`
|
|
1214
|
+
+ `/${result.warningCode})` : ""}; other devices unaffected\n`);
|
|
986
1215
|
if (opts.once)
|
|
987
1216
|
break;
|
|
988
1217
|
await new Promise((resolve) => setTimeout(resolve, interval));
|