npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@huui/cdx-switcher-auto-service

v1.0.1

Published

基于 codex-switcher 的账号用量监控与自动切换服务

Readme

codex-switcher-auto-service

1. 背景

codex-switcher-auto-service 是一个本地账号用量监控与自动切换服务。它面向已经具备账号读取与切换能力的来源,在同一进程内持续评估当前账号风险,并在满足固定安全规则时切换到经过验证的候选账号。

服务的默认账号来源为 @huui/cdx-switcher。每轮评估只读取一次全部账号的批量快照;账号池筛选、用量风险计算、候选排序、策略决策和下一轮调度都在内存中完成。业务代码只依赖本项目的公开接口,不依赖 cdx 或其他来源的返回类型和错误码。

服务提供以下能力:

  • 根据所有用量窗口中的最大 usedPercent 评估账号风险:<90% 为正常、90%–<95% 为警戒、>=95% 为需要切换、>=98% 为紧急。
  • 只允许唯一非空标签、用量有效且低于 95% 的非当前账号成为切换候选;候选按显式优先级、最早刷新时间、账号池顺序排序。
  • 成功切换到不同账号后执行固定 5 分钟冷却;冷却期间仍可读取用量和发送事件,但不会再次切换。
  • 提供账号池、切换策略、轮询策略、切换锁和账号来源五个扩展点;固定阈值、候选验证、冷却期和切换前后校验不能被扩展点绕过。
  • 通过状态快照和结构化事件提供可观察性;接口、状态和事件均不包含令牌、账号 ID、邮箱、原始响应或堆栈。

2. 项目链路

自动服务链路与扩展点

一轮评估按以下路径执行:

  1. 接入方调用 enable()、定时器触发或调用 evaluateNow();服务为本轮创建唯一的 evaluationId
  2. AccountSource.readSnapshot() 只执行一次,返回当前账号、所有账号安全摘要和逐项用量或失败状态。
  3. 服务调用账号池扩展点,将其返回的标签与本轮快照交叉验证,生成不可变的评估快照。
  4. 服务以所有用量窗口的最大值计算风险,以最早刷新时间参与候选排序;认证、网络或上游用量失败的账号仅在本轮不可作为候选,下轮会重新读取。
  5. 服务调用策略扩展点,得到 keepswitchstop。策略只负责决策,不能直接发起切换。
  6. keep 时调用轮询策略并安排下一轮;switch 时依次通过冷却期、目标资格、切换锁、切换前校验和切换后确认;stop 或全部已验证候选达到阈值时安全停止服务。
  7. 服务更新 ServiceStatus,并以 lifecycleevaluationswitchschedulehook 五类事件通知接入方。

所有扩展点只接收当前轮冻结的脱敏数据。自定义来源位于防腐层外侧:更换服务商或接入本地模拟器时,只替换 AccountSource 实现,业务编排、策略与事件消费代码无需感知下游协议。

3. 项目接入方式

安装与创建

npm install @huui/cdx-switcher-auto-service

本地开发本项目时使用 npm 安装依赖并执行脚本:

npm install
npm run typecheck
npm run build
import {
  createCodexSwitcherAutoService,
  type ServiceEvent,
} from "@huui/cdx-switcher-auto-service";

const service = createCodexSwitcherAutoService();

const unsubscribe = service.subscribe((event: ServiceEvent) => {
  const reason = event.reason ? ` reason=${event.reason}` : "";
  console.log(`[auto-service] ${event.type}/${event.stage} ${event.outcome}${reason}`);

  if (event.data.error) {
    console.error(event.data.error.code, event.data.error.message);
  }
});

const enabled = await service.enable();
if (!enabled.ok) {
  console.error("首轮评估未完成:", enabled.error.code, enabled.error.message);
}

// 业务需要时可立即发起一轮评估;不会绕过冷却期、候选校验或锁。
const evaluated = await service.evaluateNow();
if (!evaluated.ok) {
  console.error("评估结果:", evaluated.error.code);
}

console.log(service.getStatus());

// 业务进程退出前停用服务,取消下一轮定时器并阻止后续切换。
await service.disable();
unsubscribe();

创建服务本身不会启动轮询;只有 enable() 成功进入生命周期后才会开始首轮评估。默认来源要求本机 cdx 已具备可读取的账号配置;需要自动切换的账号必须拥有唯一且非空的标签。

公开控制接口

| 方法 | 用途 | | --- | --- | | configure(options) | 在 disabledstopped 状态下原子替换完整配置。 | | enable() | 启用服务,并立即执行首轮评估。 | | disable() | 取消定时器、取消等待中的锁,并停止后续动作;可重复调用。 | | evaluateNow() | 立即执行一轮评估;仍受互斥、冷却期和全部安全规则约束。 | | getStatus() | 获取只读的 ServiceStatus,不触发任何网络请求。 | | subscribe(listener) | 订阅结构化事件,返回取消订阅函数。 |

所有异步方法都返回 AutoServiceResult<T>:成功时为 { ok: true, data },可预期失败时为 { ok: false, error }。接入方应使用 error.codeerror.retryable 处理分支,而不是依赖 message 文本。

4. 自定义钩子

创建服务时可传入下列扩展点:

createCodexSwitcherAutoService({
  accountPool,
  policy,
  polling,
  lock,
  lockAcquireTimeoutMs: 30_000,
});

钩子可异步执行。服务会隔离其异常,并验证返回值;账号池和策略钩子失败或返回非法值会结束当前服务生命周期,轮询钩子失败或返回非法延迟会回退到本轮默认延迟。钩子不得修改传入对象、读取凭据或自行调用账号来源。

4.1 账号池:AccountPoolProvider

账号池决定哪些标签参与自动管理,并可指定优先级。它只收到当前轮的账号身份摘要,不会收到用量、失败详情或来源实现。

import type { AccountIdentity } from "@huui/cdx-switcher-auto-service";

interface AccountPoolContext {
  readonly now: string;
  readonly currentAccount: AccountIdentity | null;
  readonly accounts: readonly AccountIdentity[];
}

interface AccountPoolItem {
  label: string;
  priority?: number;
}

interface AccountPoolProvider {
  load(context: AccountPoolContext): Promise<readonly AccountPoolItem[]>;
}

const accountPool: AccountPoolProvider = {
  async load(context) {
    console.log("本轮当前账号:", context.currentAccount?.label ?? "未知");

    return [
      { label: "主账号", priority: 1 },
      { label: "备用账号", priority: 2 },
    ];
  },
};

label 会被去除首尾空格,且必须非空、唯一并存在于本轮快照。数值越小优先级越高;同优先级时服务继续按最早刷新时间和账号池返回顺序排序。省略此钩子时,默认账号池按来源快照顺序使用所有带标签账号。

4.2 切换策略:SwitchPolicy

策略决定本轮保持、切换或停止。服务已经完成用量验证和候选排序,eligibleTargets 只包含可安全切换的账号;策略指定的目标仍会在切换前再次验证。

import type { EvaluatedAccount } from "@huui/cdx-switcher-auto-service";

interface SwitchPolicyContext {
  readonly now: string;
  readonly current: EvaluatedAccount | null;
  readonly accounts: readonly EvaluatedAccount[];
  readonly eligibleTargets: readonly EvaluatedAccount[];
  readonly cooldownUntil: string | null;
}

type SwitchDecision =
  | { action: "keep"; reason?: string }
  | { action: "switch"; targetLabel?: string; reason?: string }
  | { action: "stop"; reason: string };

const policy: SwitchPolicy = {
  async decide(context) {
    if (context.current?.risk === "emergency") {
      return {
        action: "switch",
        targetLabel: context.eligibleTargets[0]?.label ?? undefined,
        reason: "当前账号处于紧急风险",
      };
    }

    return { action: "keep", reason: "当前用量可继续使用" };
  },
};

EvaluatedAccount 提供 labeldisplayNameisCurrentpriorityavailabilityusageeffectiveUsedPercentnextResetAtUnixMsriskunavailableReason。策略不能把未验证账号变成候选,也不能绕过 95% 候选上限、冷却期和锁。

4.3 轮询策略:PollingStrategy

轮询策略只决定成功评估后到下一轮的延迟,不负责创建定时器。服务会把最终延迟限制在 10 秒至 24 小时之间。

import type {
  EvaluatedAccount,
  EvaluationResult,
} from "@huui/cdx-switcher-auto-service";

interface PollingContext {
  readonly defaultDelayMs: number;
  readonly current: EvaluatedAccount | null;
  readonly accounts: readonly EvaluatedAccount[];
  readonly lastEvaluation: EvaluationResult | null;
}

const polling: PollingStrategy = {
  async nextDelayMs(context) {
    if (context.current?.risk === "warning") return 2 * 60_000;
    return context.defaultDelayMs;
  },
};

defaultDelayMs 已按服务固定风险规则计算,适合在自定义逻辑没有特殊要求时直接返回。切换成功后的固定 5 分钟冷却期、来源读取失败和无可验证目标的重试间隔不由该钩子覆盖。

4.4 切换锁:SwitchLock

锁保护真实的 AccountSource.switchTo() 调用。默认使用单进程内存锁;跨进程、文件、Redis 或数据库协调可通过此接口接入。也可以继承导出的 AbstractSwitchLock

import type { AutoServiceResult } from "@huui/cdx-switcher-auto-service";

interface LockAcquireRequest {
  key: string;
  timeoutMs: number;
  signal: AbortSignal;
}

interface SwitchLockLease {
  release(): Promise<void>;
}

interface SwitchLock {
  acquire(request: LockAcquireRequest): Promise<AutoServiceResult<SwitchLockLease>>;
}

const lock: SwitchLock = {
  async acquire({ key, timeoutMs, signal }) {
    if (signal.aborted) {
      return {
        ok: false,
        error: {
          code: "LOCK_OPERATION_FAILED",
          message: "锁请求已取消。",
          retryable: true,
          cause: { source: "lock" },
        },
      };
    }

    const token = await acquireFromYourLockService(key, timeoutMs, signal);
    return {
      ok: true,
      data: {
        async release() {
          await releaseFromYourLockService(token);
        },
      },
    };
  },
};

key 是固定服务锁名,timeoutMs 是本次允许等待的毫秒数,signal 会在服务停用时中止等待。获取失败应返回 AutoServiceResult;获得的租约必须实现幂等的 release(),服务会在切换成功、失败或取消后释放它。

5. 自定义账号来源

账号来源是服务与外部账号系统之间的防腐层。默认的 CdxSwitcherAccountSource 使用 getAccountUsages()switchToAccount();接入其他服务商、本地 HTTP 模拟器或测试数据时,实现相同的 AccountSource 即可。

必须实现的接口

interface AccountSource {
  /** 每轮只会被服务调用一次。 */
  readSnapshot(): Promise<AccountSourceResult<AccountSnapshot>>;
  switchTo(label: string): Promise<AccountSourceResult<SwitchOutcome>>;
}

type AccountSourceResult<T> =
  | { ok: true; data: T }
  | { ok: false; error: AccountSourceFailure };

interface AccountSourceFailure {
  code:
    | "configuration"
    | "authentication"
    | "network"
    | "usage_unavailable"
    | "switch_failed"
    | "unavailable";
  message: string;
  retryable: boolean;
}

interface AccountSnapshot {
  checkedAt: string;
  currentAccount: AccountIdentity | null;
  accounts: readonly AccountObservation[];
}

interface AccountObservation extends AccountIdentity {
  usage: UsageSnapshot | null;
  failure: AccountSourceFailure | null;
}

interface AccountIdentity {
  label: string | null;
  displayName: string;
  isCurrent: boolean;
}

interface UsageSnapshot {
  checkedAt: string;
  windows: readonly UsageWindowSnapshot[];
}

interface UsageWindowSnapshot {
  kind: "primary" | "secondary";
  usedPercent: number;
  resetsAtUnixMs: number;
}

interface SwitchOutcome {
  targetLabel: string;
  currentAccount: AccountIdentity | null;
  changed: boolean;
  switchedAt: string;
}

AccountSnapshot.currentAccount 只有在 accounts 中恰好一个账号的 isCurrenttrue,且身份摘要一致时才可以赋值;无法唯一确认时必须为 null。每个账号的 usagefailure 二选一:单项读取失败应保留账号身份,将 usage 设为 null 并填写 failure,不能中断同批其他账号。

来源错误只能使用以下稳定分类:configurationauthenticationnetworkusage_unavailableswitch_failedunavailable。错误消息必须是脱敏的固定业务描述,不能透传访问令牌、账号 ID、邮箱、原始 HTTP 响应或堆栈。

自定义来源示例

import {
  createCodexSwitcherAutoService,
  type AccountSource,
} from "@huui/cdx-switcher-auto-service";

const accountSource: AccountSource = {
  async readSnapshot() {
    const response = await fetch("http://127.0.0.1:4317/v1/account-usages");
    if (!response.ok) {
      return {
        ok: false,
        error: {
          code: "network",
          message: "账号来源网络请求失败。",
          retryable: true,
        },
      };
    }

    const payload = await response.json();
    return {
      ok: true,
      data: {
        checkedAt: payload.checkedAt,
        currentAccount: payload.currentAccount,
        accounts: payload.accounts,
      },
    };
  },

  async switchTo(label) {
    const response = await fetch("http://127.0.0.1:4317/v1/switch-to-account", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ label }),
    });
    if (!response.ok) {
      return {
        ok: false,
        error: {
          code: "switch_failed",
          message: "账号来源未能完成账号切换。",
          retryable: true,
        },
      };
    }

    return { ok: true, data: await response.json() };
  },
};

const service = createCodexSwitcherAutoService({ accountSource });

示例中的 HTTP 结构仅用于展示映射位置;真实实现必须在来源适配器内校验下游返回数据,并转换为本节的公开契约。服务层、账号池、策略、轮询、锁和事件监听代码不应导入或依赖该下游协议。